Files
claude-relay-service/cli/index.js
shaw 567e3b25aa feat: 优化并发控制和移除冗余限制功能
主要改进:
1. 改进并发控制机制
   - 使用 once 代替 on 避免重复监听
   - 监听多个事件确保可靠性(close、finish)
   - 支持客户端断开时立即释放并发槽位

2. 支持非流式请求的客户端断开处理
   - 客户端断开时立即中断上游请求
   - 避免资源浪费和不必要的 API 调用

3. 移除 requestLimit(请求数限制)功能
   - 移除配置和验证逻辑
   - 保留请求统计用于监控分析

4. 移除速率限制(Rate Limit)功能
   - 移除 RATE_LIMIT_* 配置
   - 简化中间件逻辑
   - 避免与并发控制重复

现在系统仅保留:
- Token 使用量限制
- 并发数限制(更精确的资源控制)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 14:40:37 +08:00

766 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
const { Command } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const ora = require('ora');
const Table = require('table').table;
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const config = require('../config/config');
const redis = require('../src/models/redis');
const apiKeyService = require('../src/services/apiKeyService');
const claudeAccountService = require('../src/services/claudeAccountService');
const program = new Command();
// 🎨 样式
const styles = {
title: chalk.bold.blue,
success: chalk.green,
error: chalk.red,
warning: chalk.yellow,
info: chalk.cyan,
dim: chalk.dim
};
// 🔧 初始化
async function initialize() {
const spinner = ora('正在连接 Redis...').start();
try {
await redis.connect();
spinner.succeed('Redis 连接成功');
} catch (error) {
spinner.fail('Redis 连接失败');
console.error(styles.error(error.message));
process.exit(1);
}
}
// 🔐 管理员账户管理
program
.command('admin')
.description('管理员账户操作')
.action(async () => {
await initialize();
const { action } = await inquirer.prompt({
type: 'list',
name: 'action',
message: '选择操作:',
choices: [
{ name: '🔑 设置管理员密码', value: 'set-password' },
{ name: '👤 创建初始管理员', value: 'create-admin' },
{ name: '🔄 重置管理员密码', value: 'reset-password' },
{ name: '📊 查看管理员信息', value: 'view-admin' }
]
});
switch (action) {
case 'set-password':
await setAdminPassword();
break;
case 'create-admin':
await createInitialAdmin();
break;
case 'reset-password':
await resetAdminPassword();
break;
case 'view-admin':
await viewAdminInfo();
break;
}
await redis.disconnect();
});
// 🔑 API Key 管理
program
.command('keys')
.description('API Key 管理')
.action(async () => {
await initialize();
// 尝试兼容不同版本的inquirer
let prompt = inquirer.prompt || inquirer.default?.prompt || inquirer;
if (typeof prompt !== 'function') {
prompt = (await import('inquirer')).default;
}
const { action } = await prompt({
type: 'list',
name: 'action',
message: '选择操作:',
choices: [
{ name: '📋 列出所有 API Keys', value: 'list' },
{ name: '🔑 创建新的 API Key', value: 'create' },
{ name: '📝 更新 API Key', value: 'update' },
{ name: '🗑️ 删除 API Key', value: 'delete' },
{ name: '📊 查看使用统计', value: 'stats' },
{ name: '🧹 重置所有统计数据', value: 'reset-stats' }
]
});
switch (action) {
case 'list':
await listApiKeys();
break;
case 'create':
await createApiKey();
break;
case 'update':
await updateApiKey();
break;
case 'delete':
await deleteApiKey();
break;
case 'stats':
await viewApiKeyStats();
break;
case 'reset-stats':
await resetAllApiKeyStats();
break;
}
await redis.disconnect();
});
// 🏢 Claude 账户管理
program
.command('accounts')
.description('Claude 账户管理')
.action(async () => {
await initialize();
const { action } = await inquirer.prompt({
type: 'list',
name: 'action',
message: '选择操作:',
choices: [
{ name: '📋 列出所有账户', value: 'list' },
{ name: '🏢 创建新账户', value: 'create' },
{ name: '📝 更新账户', value: 'update' },
{ name: '🗑️ 删除账户', value: 'delete' },
{ name: '🔄 刷新 Token', value: 'refresh' },
{ name: '🧪 测试账户', value: 'test' }
]
});
switch (action) {
case 'list':
await listClaudeAccounts();
break;
case 'create':
await createClaudeAccount();
break;
case 'update':
await updateClaudeAccount();
break;
case 'delete':
await deleteClaudeAccount();
break;
case 'refresh':
await refreshAccountToken();
break;
case 'test':
await testClaudeAccount();
break;
}
await redis.disconnect();
});
// 🧹 重置统计数据命令
program
.command('reset-stats')
.description('重置所有API Key的统计数据')
.option('--force', '跳过确认直接重置')
.option('--debug', '显示详细的Redis键调试信息')
.action(async (options) => {
await initialize();
console.log(styles.title('\n🧹 重置所有API Key统计数据\n'));
// 如果启用调试显示当前Redis键
if (options.debug) {
console.log(styles.info('🔍 调试模式: 检查Redis中的实际键...\n'));
try {
const usageKeys = await redis.getClient().keys('usage:*');
const apiKeyKeys = await redis.getClient().keys('apikey:*');
console.log(styles.dim('API Key 键:'));
apiKeyKeys.forEach(key => console.log(` ${key}`));
console.log(styles.dim('\nUsage 键:'));
usageKeys.forEach(key => console.log(` ${key}`));
// 检查今日统计键
const today = new Date().toISOString().split('T')[0];
const dailyKeys = await redis.getClient().keys(`usage:daily:*:${today}`);
console.log(styles.dim(`\n今日统计键 (${today}):`));
dailyKeys.forEach(key => console.log(` ${key}`));
console.log('');
} catch (error) {
console.error(styles.error('调试信息获取失败:', error.message));
}
}
// 显示警告信息
console.log(styles.warning('⚠️ 警告: 此操作将删除所有API Key的使用统计数据!'));
console.log(styles.dim(' 包括: Token使用量、请求数量、每日/每月统计、最后使用时间等'));
console.log(styles.dim(' 此操作不可逆,请谨慎操作!\n'));
if (!options.force) {
console.log(styles.info('如需强制执行,请使用: npm run cli reset-stats -- --force\n'));
console.log(styles.error('操作已取消 - 请添加 --force 参数确认重置'));
await redis.disconnect();
return;
}
// 获取当前统计概览
const spinner = ora('正在获取当前统计数据...').start();
try {
const apiKeys = await apiKeyService.getAllApiKeys();
const totalTokens = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
const totalRequests = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
spinner.succeed('统计数据获取完成');
console.log(styles.info('\n📊 当前统计概览:'));
console.log(` API Keys 数量: ${apiKeys.length}`);
console.log(` 总 Token 使用量: ${totalTokens.toLocaleString()}`);
console.log(` 总请求数量: ${totalRequests.toLocaleString()}\n`);
// 执行重置操作
const resetSpinner = ora('正在重置所有API Key统计数据...').start();
const stats = await redis.resetAllUsageStats();
resetSpinner.succeed('所有统计数据重置完成');
// 显示重置结果
console.log(styles.success('\n✅ 重置操作完成!\n'));
console.log(styles.info('📊 重置详情:'));
console.log(` 重置的API Key数量: ${stats.resetApiKeys}`);
console.log(` 删除的总体统计: ${stats.deletedKeys}`);
console.log(` 删除的每日统计: ${stats.deletedDailyKeys}`);
console.log(` 删除的每月统计: ${stats.deletedMonthlyKeys}`);
console.log(styles.warning('\n💡 提示: API Key本身未被删除只是清空了使用统计数据'));
} catch (error) {
spinner.fail('重置操作失败');
console.error(styles.error(error.message));
}
await redis.disconnect();
});
// 📊 系统状态
program
.command('status')
.description('查看系统状态')
.action(async () => {
await initialize();
const spinner = ora('正在获取系统状态...').start();
try {
const [systemStats, apiKeys, accounts] = await Promise.all([
redis.getSystemStats(),
apiKeyService.getAllApiKeys(),
claudeAccountService.getAllAccounts()
]);
spinner.succeed('系统状态获取成功');
console.log(styles.title('\n📊 系统状态概览\n'));
const statusData = [
['项目', '数量', '状态'],
['API Keys', apiKeys.length, `${apiKeys.filter(k => k.isActive).length} 活跃`],
['Claude 账户', accounts.length, `${accounts.filter(a => a.isActive).length} 活跃`],
['Redis 连接', redis.isConnected ? '已连接' : '未连接', redis.isConnected ? '🟢' : '🔴'],
['运行时间', `${Math.floor(process.uptime() / 60)} 分钟`, '🕐']
];
console.log(table(statusData));
// 使用统计
const totalTokens = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
const totalRequests = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
console.log(styles.title('\n📈 使用统计\n'));
console.log(`总 Token 使用量: ${styles.success(totalTokens.toLocaleString())}`);
console.log(`总请求数: ${styles.success(totalRequests.toLocaleString())}`);
} catch (error) {
spinner.fail('获取系统状态失败');
console.error(styles.error(error.message));
}
await redis.disconnect();
});
// 🧹 清理命令
program
.command('cleanup')
.description('清理过期数据')
.action(async () => {
await initialize();
const { confirm } = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: '确定要清理过期数据吗?',
default: false
});
if (!confirm) {
console.log(styles.warning('操作已取消'));
await redis.disconnect();
return;
}
const spinner = ora('正在清理过期数据...').start();
try {
const [expiredKeys, errorAccounts] = await Promise.all([
apiKeyService.cleanupExpiredKeys(),
claudeAccountService.cleanupErrorAccounts()
]);
await redis.cleanup();
spinner.succeed('清理完成');
console.log(`${styles.success('✅')} 清理了 ${expiredKeys} 个过期 API Key`);
console.log(`${styles.success('✅')} 重置了 ${errorAccounts} 个错误账户`);
} catch (error) {
spinner.fail('清理失败');
console.error(styles.error(error.message));
}
await redis.disconnect();
});
// 实现具体功能函数
async function createInitialAdmin() {
console.log(styles.title('\n🔐 创建初始管理员账户\n'));
const adminData = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: '用户名:',
default: 'admin',
validate: input => input.length >= 3 || '用户名至少3个字符'
},
{
type: 'password',
name: 'password',
message: '密码:',
validate: input => input.length >= 8 || '密码至少8个字符'
},
{
type: 'password',
name: 'confirmPassword',
message: '确认密码:',
validate: (input, answers) => input === answers.password || '密码不匹配'
}
]);
const spinner = ora('正在创建管理员账户...').start();
try {
const passwordHash = await bcrypt.hash(adminData.password, 12);
const credentials = {
username: adminData.username,
passwordHash,
createdAt: new Date().toISOString(),
id: crypto.randomBytes(16).toString('hex')
};
await redis.setSession('admin_credentials', credentials, 0); // 永不过期
spinner.succeed('管理员账户创建成功');
console.log(`${styles.success('✅')} 用户名: ${adminData.username}`);
console.log(`${styles.info('')} 请妥善保管登录凭据`);
} catch (error) {
spinner.fail('创建管理员账户失败');
console.error(styles.error(error.message));
}
}
async function setAdminPassword() {
console.log(styles.title('\n🔑 设置管理员密码\n'));
const passwordData = await inquirer.prompt([
{
type: 'password',
name: 'newPassword',
message: '新密码:',
validate: input => input.length >= 8 || '密码至少8个字符'
},
{
type: 'password',
name: 'confirmPassword',
message: '确认密码:',
validate: (input, answers) => input === answers.newPassword || '密码不匹配'
}
]);
const spinner = ora('正在更新密码...').start();
try {
const adminData = await redis.getSession('admin_credentials');
if (!adminData || Object.keys(adminData).length === 0) {
spinner.fail('未找到管理员账户');
console.log(styles.warning('请先创建初始管理员账户'));
return;
}
const passwordHash = await bcrypt.hash(passwordData.newPassword, 12);
adminData.passwordHash = passwordHash;
adminData.updatedAt = new Date().toISOString();
await redis.setSession('admin_credentials', adminData, 0);
spinner.succeed('密码更新成功');
console.log(`${styles.success('✅')} 管理员密码已更新`);
} catch (error) {
spinner.fail('密码更新失败');
console.error(styles.error(error.message));
}
}
async function listApiKeys() {
const spinner = ora('正在获取 API Keys...').start();
try {
const apiKeys = await apiKeyService.getAllApiKeys();
spinner.succeed(`找到 ${apiKeys.length} 个 API Key`);
if (apiKeys.length === 0) {
console.log(styles.warning('没有找到任何 API Key'));
return;
}
const tableData = [
['ID', '名称', '状态', 'Token使用', '请求数', '创建时间']
];
apiKeys.forEach(key => {
tableData.push([
key.id.substring(0, 8) + '...',
key.name,
key.isActive ? '🟢 活跃' : '🔴 停用',
key.usage?.total?.tokens?.toLocaleString() || '0',
key.usage?.total?.requests?.toLocaleString() || '0',
new Date(key.createdAt).toLocaleDateString()
]);
});
console.log('\n📋 API Keys 列表:\n');
console.log(table(tableData));
} catch (error) {
spinner.fail('获取 API Keys 失败');
console.error(styles.error(error.message));
}
}
async function createApiKey() {
console.log(styles.title('\n🔑 创建新的 API Key\n'));
const keyData = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'API Key 名称:',
validate: input => input.length > 0 || '名称不能为空'
},
{
type: 'input',
name: 'description',
message: '描述 (可选):'
},
{
type: 'number',
name: 'tokenLimit',
message: 'Token 限制 (0=无限制):',
default: 1000000
},
]);
const spinner = ora('正在创建 API Key...').start();
try {
const newKey = await apiKeyService.generateApiKey(keyData);
spinner.succeed('API Key 创建成功');
console.log(`${styles.success('✅')} API Key: ${styles.warning(newKey.apiKey)}`);
console.log(`${styles.info('')} 请妥善保管此 API Key它只会显示一次`);
} catch (error) {
spinner.fail('创建 API Key 失败');
console.error(styles.error(error.message));
}
}
async function resetAllApiKeyStats() {
console.log(styles.title('\n🧹 重置所有API Key统计数据\n'));
// 显示警告信息
console.log(styles.warning('⚠️ 警告: 此操作将删除所有API Key的使用统计数据!'));
console.log(styles.dim(' 包括: Token使用量、请求数量、每日/每月统计、最后使用时间等'));
console.log(styles.dim(' 此操作不可逆,请谨慎操作!\n'));
// 第一次确认
const { firstConfirm } = await inquirer.prompt({
type: 'confirm',
name: 'firstConfirm',
message: '您确定要重置所有API Key的统计数据吗',
default: false
});
if (!firstConfirm) {
console.log(styles.info('操作已取消'));
return;
}
// 获取当前统计概览
const spinner = ora('正在获取当前统计数据...').start();
try {
const apiKeys = await apiKeyService.getAllApiKeys();
const totalTokens = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
const totalRequests = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
spinner.succeed('统计数据获取完成');
console.log(styles.info('\n📊 当前统计概览:'));
console.log(` API Keys 数量: ${apiKeys.length}`);
console.log(` 总 Token 使用量: ${totalTokens.toLocaleString()}`);
console.log(` 总请求数量: ${totalRequests.toLocaleString()}\n`);
// 第二次确认(需要输入"RESET"
const { confirmation } = await inquirer.prompt({
type: 'input',
name: 'confirmation',
message: '请输入 "RESET" 来确认重置操作:',
validate: input => input === 'RESET' || '请输入正确的确认文本 "RESET"'
});
if (confirmation !== 'RESET') {
console.log(styles.info('操作已取消'));
return;
}
// 执行重置操作
const resetSpinner = ora('正在重置所有API Key统计数据...').start();
const stats = await redis.resetAllUsageStats();
resetSpinner.succeed('所有统计数据重置完成');
// 显示重置结果
console.log(styles.success('\n✅ 重置操作完成!\n'));
console.log(styles.info('📊 重置详情:'));
console.log(` 重置的API Key数量: ${stats.resetApiKeys}`);
console.log(` 删除的总体统计: ${stats.deletedKeys}`);
console.log(` 删除的每日统计: ${stats.deletedDailyKeys}`);
console.log(` 删除的每月统计: ${stats.deletedMonthlyKeys}`);
console.log(styles.warning('\n💡 提示: API Key本身未被删除只是清空了使用统计数据'));
} catch (error) {
spinner.fail('重置操作失败');
console.error(styles.error(error.message));
}
}
async function viewApiKeyStats() {
console.log(styles.title('\n📊 API Key 使用统计\n'));
const spinner = ora('正在获取统计数据...').start();
try {
const apiKeys = await apiKeyService.getAllApiKeys();
if (apiKeys.length === 0) {
spinner.succeed('获取完成');
console.log(styles.warning('没有找到任何 API Key'));
return;
}
spinner.succeed(`找到 ${apiKeys.length} 个 API Key 的统计数据`);
const tableData = [
['名称', 'Token总量', '输入Token', '输出Token', '请求数', '最后使用']
];
let totalTokens = 0;
let totalRequests = 0;
apiKeys.forEach(key => {
const usage = key.usage?.total || {};
const tokens = usage.tokens || 0;
const inputTokens = usage.inputTokens || 0;
const outputTokens = usage.outputTokens || 0;
const requests = usage.requests || 0;
totalTokens += tokens;
totalRequests += requests;
tableData.push([
key.name,
tokens.toLocaleString(),
inputTokens.toLocaleString(),
outputTokens.toLocaleString(),
requests.toLocaleString(),
key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : '从未使用'
]);
});
console.log(table(tableData));
console.log(styles.info('\n📈 总计统计:'));
console.log(`总 Token 使用量: ${styles.success(totalTokens.toLocaleString())}`);
console.log(`总请求数量: ${styles.success(totalRequests.toLocaleString())}`);
} catch (error) {
spinner.fail('获取统计数据失败');
console.error(styles.error(error.message));
}
}
async function updateApiKey() {
console.log(styles.title('\n📝 更新 API Key\n'));
console.log(styles.warning('功能开发中...'));
}
async function deleteApiKey() {
console.log(styles.title('\n🗑 删除 API Key\n'));
console.log(styles.warning('功能开发中...'));
}
async function resetAdminPassword() {
console.log(styles.title('\n🔄 重置管理员密码\n'));
console.log(styles.warning('功能开发中...'));
}
async function viewAdminInfo() {
console.log(styles.title('\n👤 管理员信息\n'));
const spinner = ora('正在获取管理员信息...').start();
try {
const adminData = await redis.getSession('admin_credentials');
if (!adminData || Object.keys(adminData).length === 0) {
spinner.fail('未找到管理员账户');
console.log(styles.warning('请先创建初始管理员账户'));
return;
}
spinner.succeed('管理员信息获取成功');
console.log(`用户名: ${styles.info(adminData.username)}`);
console.log(`创建时间: ${styles.dim(new Date(adminData.createdAt).toLocaleString())}`);
console.log(`最后登录: ${adminData.lastLogin ? styles.dim(new Date(adminData.lastLogin).toLocaleString()) : '从未登录'}`);
} catch (error) {
spinner.fail('获取管理员信息失败');
console.error(styles.error(error.message));
}
}
async function createClaudeAccount() {
console.log(styles.title('\n🏢 创建 Claude 账户\n'));
console.log(styles.warning('功能开发中... 请使用Web界面创建OAuth账户'));
}
async function updateClaudeAccount() {
console.log(styles.title('\n📝 更新 Claude 账户\n'));
console.log(styles.warning('功能开发中...'));
}
async function deleteClaudeAccount() {
console.log(styles.title('\n🗑 删除 Claude 账户\n'));
console.log(styles.warning('功能开发中...'));
}
async function refreshAccountToken() {
console.log(styles.title('\n🔄 刷新账户 Token\n'));
console.log(styles.warning('功能开发中...'));
}
async function testClaudeAccount() {
console.log(styles.title('\n🧪 测试 Claude 账户\n'));
console.log(styles.warning('功能开发中...'));
}
async function listClaudeAccounts() {
const spinner = ora('正在获取 Claude 账户...').start();
try {
const accounts = await claudeAccountService.getAllAccounts();
spinner.succeed(`找到 ${accounts.length} 个 Claude 账户`);
if (accounts.length === 0) {
console.log(styles.warning('没有找到任何 Claude 账户'));
return;
}
const tableData = [
['ID', '名称', '邮箱', '状态', '代理', '最后使用']
];
accounts.forEach(account => {
tableData.push([
account.id.substring(0, 8) + '...',
account.name,
account.email || '-',
account.isActive ? (account.status === 'active' ? '🟢 活跃' : '🟡 待激活') : '🔴 停用',
account.proxy ? '🌐 是' : '-',
account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '-'
]);
});
console.log('\n🏢 Claude 账户列表:\n');
console.log(table(tableData));
} catch (error) {
spinner.fail('获取 Claude 账户失败');
console.error(styles.error(error.message));
}
}
// 程序信息
program
.name('claude-relay-cli')
.description('Claude Relay Service 命令行管理工具')
.version('1.0.0');
// 解析命令行参数
program.parse();
// 如果没有提供命令,显示帮助
if (!process.argv.slice(2).length) {
console.log(styles.title('🚀 Claude Relay Service CLI\n'));
console.log('使用以下命令管理服务:\n');
console.log(' claude-relay-cli admin - 管理员账户操作');
console.log(' claude-relay-cli keys - API Key 管理 (包含重置统计数据)');
console.log(' claude-relay-cli accounts - Claude 账户管理');
console.log(' claude-relay-cli status - 查看系统状态');
console.log(' claude-relay-cli cleanup - 清理过期数据');
console.log(' claude-relay-cli reset-stats - 重置所有API Key统计数据');
console.log('\n使用 --help 查看详细帮助信息');
}