UNPKG

claude-key-manager

Version:

🚀 Claude Key Manager 4.1.3 - 修复密钥验证问题,支持更多密钥格式

385 lines (327 loc) 10.9 kB
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;