claude-key-manager
Version:
🚀 Claude Key Manager 4.1.3 - 修复密钥验证问题,支持更多密钥格式
385 lines (327 loc) • 10.9 kB
JavaScript
const keytar = require('keytar');
const Conf = require('conf');
const inquirer = require('inquirer');
const chalk = require('chalk');
class KeyManager {
constructor() {
this.serviceName = 'claude-key-manager';
this.config = new Conf({
projectName: 'claude-key-manager',
schema: {
keys: {
type: 'object',
default: {}
},
activeKey: {
type: ['string', 'null'],
default: null
},
version: {
type: 'string',
default: '4.1.3'
}
}
});
}
/**
* 验证Claude API密钥格式 (宽松但安全的验证)
*/
validateKey(key) {
if (!key || typeof key !== 'string') {
throw new Error('密钥不能为空');
}
// 移除首尾空格
key = key.trim();
// 基本格式检查:必须以 sk-ant- 开头
if (!key.startsWith('sk-ant-')) {
throw new Error('无效的Claude API密钥格式,必须以 sk-ant- 开头');
}
// 更宽松的格式验证:允许更多字符组合
// 支持各种可能的Claude密钥格式变化
const claudeKeyPattern = /^sk-ant-[A-Za-z0-9._-]{15,}$/;
if (!claudeKeyPattern.test(key)) {
throw new Error('Claude API密钥包含无效字符,请检查密钥格式');
}
// 降低最小长度要求(一些测试密钥可能较短)
if (key.length < 25) {
throw new Error('密钥长度过短,请确认密钥完整');
}
// 只检查最明显的占位符,避免误判真实密钥
const obviousPlaceholders = ['your-key-here', 'insert-key', 'api-key-here', 'replace-with'];
const keyLower = key.toLowerCase();
for (const placeholder of obviousPlaceholders) {
if (keyLower.includes(placeholder)) {
throw new Error(`请替换占位符 "${placeholder}" 为真实的Claude API密钥`);
}
}
return true;
}
/**
* 验证API Base URL格式
*/
validateApiUrl(url) {
if (!url || typeof url !== 'string') {
return 'https://api.anthropic.com'; // 默认URL
}
url = url.trim();
// 如果用户只输入了空字符串,使用默认
if (!url) {
return 'https://api.anthropic.com';
}
// 简单的URL格式检查
try {
const urlObj = new URL(url);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
throw new Error('API Base URL必须是http或https协议');
}
return url;
} catch (error) {
throw new Error(`无效的API Base URL格式: ${error.message}`);
}
}
/**
* 添加新密钥 (支持API Base URL)
*/
async addKey(key, name = 'default', description = '', apiUrl = 'https://api.anthropic.com') {
this.validateKey(key);
const validatedApiUrl = this.validateApiUrl(apiUrl);
// 检查名称是否已存在
const keys = this.config.get('keys', {});
if (keys[name]) {
const { overwrite } = await inquirer.prompt([{
type: 'confirm',
name: 'overwrite',
message: `密钥 '${name}' 已存在,是否覆盖?`,
default: false
}]);
if (!overwrite) {
throw new Error('操作已取消');
}
}
try {
// 保存到系统密钥存储
await keytar.setPassword(this.serviceName, name, key);
// 保存元数据 (包含API URL)
keys[name] = {
description,
apiUrl: validatedApiUrl,
createdAt: new Date().toISOString(),
lastUsed: null
};
this.config.set('keys', keys);
// 如果是第一个密钥,自动设为活跃
if (Object.keys(keys).length === 1) {
this.config.set('activeKey', name);
}
return true;
} catch (error) {
throw new Error(`保存密钥失败: ${error.message}`);
}
}
/**
* 列出所有密钥
*/
async listKeys(verbose = false) {
const keys = this.config.get('keys', {});
const activeKey = this.config.get('activeKey');
const result = [];
for (const [name, metadata] of Object.entries(keys)) {
const keyData = {
name,
active: name === activeKey,
description: metadata.description || '',
apiUrl: metadata.apiUrl || 'https://api.anthropic.com',
createdAt: metadata.createdAt
};
if (verbose) {
keyData.lastUsed = metadata.lastUsed;
// 检查密钥是否在系统中存在
try {
const key = await keytar.getPassword(this.serviceName, name);
keyData.exists = !!key;
keyData.preview = key ? `${key.substring(0, 12)}...${key.slice(-4)}` : 'N/A';
} catch (error) {
keyData.exists = false;
keyData.preview = 'Error';
}
}
result.push(keyData);
}
return result.sort((a, b) => a.name.localeCompare(b.name));
}
/**
* 切换活跃密钥
*/
async switchKey(name) {
const keys = this.config.get('keys', {});
if (!keys[name]) {
throw new Error(`密钥 '${name}' 不存在`);
}
// 验证密钥在系统中存在
const key = await keytar.getPassword(this.serviceName, name);
if (!key) {
throw new Error(`密钥 '${name}' 在系统密钥存储中不存在`);
}
// 更新活跃密钥和使用时间
this.config.set('activeKey', name);
keys[name].lastUsed = new Date().toISOString();
this.config.set('keys', keys);
// 设置环境变量 (你要的功能!)
await this.setEnvironmentVariables(name, keys[name]);
return true;
}
/**
* 删除密钥 (你要的功能!)
*/
async deleteKey(name, force = false) {
const keys = this.config.get('keys', {});
if (!keys[name]) {
throw new Error(`密钥 '${name}' 不存在`);
}
// 如果不是强制删除,需要确认
if (!force) {
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: `⚠️ 确定要删除密钥 '${name}' 吗?此操作不可撤销!`,
default: false
}]);
if (!confirm) {
console.log(chalk.yellow('❌ 删除操作已取消'));
return false;
}
}
try {
// 从系统密钥存储中删除
await keytar.deletePassword(this.serviceName, name);
// 从配置中删除
delete keys[name];
this.config.set('keys', keys);
// 如果删除的是当前活跃密钥,清除活跃状态
const activeKey = this.config.get('activeKey');
if (activeKey === name) {
const remainingKeys = Object.keys(keys);
if (remainingKeys.length > 0) {
// 自动切换到第一个可用密钥
this.config.set('activeKey', remainingKeys[0]);
console.log(chalk.blue(`🔄 已自动切换到密钥: ${remainingKeys[0]}`));
} else {
this.config.set('activeKey', null);
console.log(chalk.yellow('⚠️ 没有其他密钥可用'));
}
}
return true;
} catch (error) {
throw new Error(`删除密钥失败: ${error.message}`);
}
}
/**
* 获取当前活跃密钥
*/
async getCurrentKey() {
const activeKeyName = this.config.get('activeKey');
if (!activeKeyName) return null;
const keys = this.config.get('keys', {});
const keyData = keys[activeKeyName];
if (!keyData) return null;
try {
const key = await keytar.getPassword(this.serviceName, activeKeyName);
return {
name: activeKeyName,
description: keyData.description,
key: key,
preview: key ? `${key.substring(0, 12)}...${key.slice(-4)}` : null
};
} catch (error) {
return null;
}
}
/**
* 获取指定密钥的实际值
*/
async getKey(name) {
try {
return await keytar.getPassword(this.serviceName, name);
} catch (error) {
throw new Error(`获取密钥失败: ${error.message}`);
}
}
/**
* 设置环境变量 (你要的功能!)
*/
async setEnvironmentVariables(keyName, keyMetadata) {
const fs = require('fs');
const os = require('os');
const path = require('path');
try {
// 获取密钥值
const apiKey = await keytar.getPassword(this.serviceName, keyName);
if (!apiKey) {
throw new Error(`无法获取密钥 ${keyName} 的值`);
}
// 准备环境变量内容
const envContent = [
`# Claude Key Manager - 当前活跃配置: ${keyName}`,
`# 生成时间: ${new Date().toISOString()}`,
`# 描述: ${keyMetadata.description || '无描述'}`,
``,
`export ANTHROPIC_API_KEY="${apiKey}"`,
`export CLAUDE_API_KEY="${apiKey}"`,
`export ANTHROPIC_BASE_URL="${keyMetadata.apiUrl || 'https://api.anthropic.com'}"`,
`export CLAUDE_BASE_URL="${keyMetadata.apiUrl || 'https://api.anthropic.com'}"`,
``,
`# 当前密钥名称`,
`export CLAUDE_KEY_NAME="${keyName}"`,
``
].join('\n');
// 写入到用户目录的配置文件
const envFile = path.join(os.homedir(), '.claude-key-manager', 'current.env');
const configDir = path.dirname(envFile);
// 确保目录存在
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(envFile, envContent);
console.log(chalk.blue(`🔧 环境变量已更新:`));
console.log(chalk.gray(` 配置文件: ${envFile}`));
console.log(chalk.gray(` API URL: ${keyMetadata.apiUrl}`));
console.log(chalk.yellow(`\n💡 要使用这些环境变量,请运行:`));
console.log(chalk.cyan(` source ${envFile}`));
console.log(chalk.gray(` 或将以下内容添加到你的 ~/.bashrc 或 ~/.zshrc:`));
console.log(chalk.cyan(` source ${envFile}`));
return envFile;
} catch (error) {
console.log(chalk.yellow(`⚠️ 设置环境变量失败: ${error.message}`));
return null;
}
}
/**
* 获取环境变量设置指南
*/
getEnvironmentSetupGuide() {
const os = require('os');
const path = require('path');
const envFile = path.join(os.homedir(), '.claude-key-manager', 'current.env');
const shell = process.env.SHELL || '/bin/bash';
const shellName = path.basename(shell);
let rcFile;
switch (shellName) {
case 'zsh':
rcFile = '~/.zshrc';
break;
case 'fish':
rcFile = '~/.config/fish/config.fish';
break;
default:
rcFile = '~/.bashrc';
}
return {
envFile,
rcFile,
shellName,
setupCommand: `echo 'source ${envFile}' >> ${rcFile}`,
manualSource: `source ${envFile}`
};
}
}
module.exports = KeyManager;