fix: APIKey列表费用及Token显示不准确的问题,目前显示总数

feat: 增加APIKey过期设置,以及到期续期的能力
This commit is contained in:
KevinLiao
2025-07-25 09:34:40 +08:00
parent 561f5ffc7f
commit f614d54ab5
19 changed files with 3908 additions and 90 deletions

View File

@@ -4,7 +4,7 @@ const { Command } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const ora = require('ora');
const Table = require('table').table;
const { table } = require('table');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const fs = require('fs');
@@ -54,6 +54,43 @@ program
});
// 🔑 API Key 管理
program
.command('keys')
.description('API Key 管理操作')
.action(async () => {
await initialize();
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: '请选择操作:',
choices: [
{ name: '📋 查看所有 API Keys', value: 'list' },
{ name: '🔧 修改 API Key 过期时间', value: 'update-expiry' },
{ name: '🔄 续期即将过期的 API Key', value: 'renew' },
{ name: '🗑️ 删除 API Key', value: 'delete' }
]
}]);
switch (action) {
case 'list':
await listApiKeys();
break;
case 'update-expiry':
await updateApiKeyExpiry();
break;
case 'renew':
await renewApiKeys();
break;
case 'delete':
await deleteApiKey();
break;
}
await redis.disconnect();
});
// 📊 系统状态
program
.command('status')
@@ -201,6 +238,329 @@ async function createInitialAdmin() {
// API Key 管理功能
async function listApiKeys() {
const spinner = ora('正在获取 API Keys...').start();
try {
const apiKeys = await apiKeyService.getAllApiKeys();
spinner.succeed(`找到 ${apiKeys.length} 个 API Keys`);
if (apiKeys.length === 0) {
console.log(styles.warning('没有找到任何 API Keys'));
return;
}
const tableData = [
['名称', 'API Key', '状态', '过期时间', '使用量', 'Token限制']
];
apiKeys.forEach(key => {
const now = new Date();
const expiresAt = key.expiresAt ? new Date(key.expiresAt) : null;
let expiryStatus = '永不过期';
if (expiresAt) {
if (expiresAt < now) {
expiryStatus = styles.error(`已过期 (${expiresAt.toLocaleDateString()})`);
} else {
const daysLeft = Math.ceil((expiresAt - now) / (1000 * 60 * 60 * 24));
if (daysLeft <= 7) {
expiryStatus = styles.warning(`${daysLeft}天后过期 (${expiresAt.toLocaleDateString()})`);
} else {
expiryStatus = styles.success(`${expiresAt.toLocaleDateString()}`);
}
}
}
tableData.push([
key.name,
key.apiKey ? key.apiKey.substring(0, 20) + '...' : '-',
key.isActive ? '🟢 活跃' : '🔴 停用',
expiryStatus,
`${(key.usage?.total?.tokens || 0).toLocaleString()}`,
key.tokenLimit ? key.tokenLimit.toLocaleString() : '无限制'
]);
});
console.log(styles.title('\n🔑 API Keys 列表:\n'));
console.log(table(tableData));
} catch (error) {
spinner.fail('获取 API Keys 失败');
console.error(styles.error(error.message));
}
}
async function updateApiKeyExpiry() {
try {
// 获取所有 API Keys
const apiKeys = await apiKeyService.getAllApiKeys();
if (apiKeys.length === 0) {
console.log(styles.warning('没有找到任何 API Keys'));
return;
}
// 选择要修改的 API Key
const { selectedKey } = await inquirer.prompt([{
type: 'list',
name: 'selectedKey',
message: '选择要修改的 API Key:',
choices: apiKeys.map(key => ({
name: `${key.name} (${key.apiKey?.substring(0, 20)}...) - ${key.expiresAt ? new Date(key.expiresAt).toLocaleDateString() : '永不过期'}`,
value: key
}))
}]);
console.log(`\n当前 API Key: ${selectedKey.name}`);
console.log(`当前过期时间: ${selectedKey.expiresAt ? new Date(selectedKey.expiresAt).toLocaleString() : '永不过期'}`);
// 选择新的过期时间
const { expiryOption } = await inquirer.prompt([{
type: 'list',
name: 'expiryOption',
message: '选择新的过期时间:',
choices: [
{ name: '⏰ 1分后测试用', value: '1m' },
{ name: '⏰ 1小时后测试用', value: '1h' },
{ name: '📅 1天后', value: '1d' },
{ name: '📅 7天后', value: '7d' },
{ name: '📅 30天后', value: '30d' },
{ name: '📅 90天后', value: '90d' },
{ name: '📅 365天后', value: '365d' },
{ name: '♾️ 永不过期', value: 'never' },
{ name: '🎯 自定义日期时间', value: 'custom' }
]
}]);
let newExpiresAt = null;
if (expiryOption === 'never') {
newExpiresAt = null;
} else if (expiryOption === 'custom') {
const { customDate, customTime } = await inquirer.prompt([
{
type: 'input',
name: 'customDate',
message: '输入日期 (YYYY-MM-DD):',
default: new Date().toISOString().split('T')[0],
validate: input => {
const date = new Date(input);
return !isNaN(date.getTime()) || '请输入有效的日期格式';
}
},
{
type: 'input',
name: 'customTime',
message: '输入时间 (HH:MM):',
default: '00:00',
validate: input => {
return /^\d{2}:\d{2}$/.test(input) || '请输入有效的时间格式 (HH:MM)';
}
}
]);
newExpiresAt = new Date(`${customDate}T${customTime}:00`).toISOString();
} else {
// 计算新的过期时间
const now = new Date();
const durations = {
'1m': 60 * 1000,
'1h': 60 * 60 * 1000,
'1d': 24 * 60 * 60 * 1000,
'7d': 7 * 24 * 60 * 60 * 1000,
'30d': 30 * 24 * 60 * 60 * 1000,
'90d': 90 * 24 * 60 * 60 * 1000,
'365d': 365 * 24 * 60 * 60 * 1000
};
newExpiresAt = new Date(now.getTime() + durations[expiryOption]).toISOString();
}
// 确认修改
const confirmMsg = newExpiresAt
? `确认将过期时间修改为: ${new Date(newExpiresAt).toLocaleString()}?`
: '确认设置为永不过期?';
const { confirmed } = await inquirer.prompt([{
type: 'confirm',
name: 'confirmed',
message: confirmMsg,
default: true
}]);
if (!confirmed) {
console.log(styles.info('已取消修改'));
return;
}
// 执行修改
const spinner = ora('正在修改过期时间...').start();
try {
await apiKeyService.updateApiKey(selectedKey.id, { expiresAt: newExpiresAt });
spinner.succeed('过期时间修改成功');
console.log(styles.success(`\n✅ API Key "${selectedKey.name}" 的过期时间已更新`));
console.log(`新的过期时间: ${newExpiresAt ? new Date(newExpiresAt).toLocaleString() : '永不过期'}`);
} catch (error) {
spinner.fail('修改失败');
console.error(styles.error(error.message));
}
} catch (error) {
console.error(styles.error('操作失败:', error.message));
}
}
async function renewApiKeys() {
const spinner = ora('正在查找即将过期的 API Keys...').start();
try {
const apiKeys = await apiKeyService.getAllApiKeys();
const now = new Date();
const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
// 筛选即将过期的 Keys7天内
const expiringKeys = apiKeys.filter(key => {
if (!key.expiresAt) return false;
const expiresAt = new Date(key.expiresAt);
return expiresAt > now && expiresAt <= sevenDaysLater;
});
spinner.stop();
if (expiringKeys.length === 0) {
console.log(styles.info('没有即将过期的 API Keys7天内'));
return;
}
console.log(styles.warning(`\n找到 ${expiringKeys.length} 个即将过期的 API Keys:\n`));
expiringKeys.forEach((key, index) => {
const daysLeft = Math.ceil((new Date(key.expiresAt) - now) / (1000 * 60 * 60 * 24));
console.log(`${index + 1}. ${key.name} - ${daysLeft}天后过期 (${new Date(key.expiresAt).toLocaleDateString()})`);
});
const { renewOption } = await inquirer.prompt([{
type: 'list',
name: 'renewOption',
message: '选择续期方式:',
choices: [
{ name: '📅 全部续期30天', value: 'all30' },
{ name: '📅 全部续期90天', value: 'all90' },
{ name: '🎯 逐个选择续期', value: 'individual' }
]
}]);
if (renewOption.startsWith('all')) {
const days = renewOption === 'all30' ? 30 : 90;
const renewSpinner = ora(`正在为所有 API Keys 续期 ${days} 天...`).start();
for (const key of expiringKeys) {
try {
const newExpiresAt = new Date(new Date(key.expiresAt).getTime() + days * 24 * 60 * 60 * 1000).toISOString();
await apiKeyService.updateApiKey(key.id, { expiresAt: newExpiresAt });
} catch (error) {
renewSpinner.fail(`续期 ${key.name} 失败: ${error.message}`);
}
}
renewSpinner.succeed(`成功续期 ${expiringKeys.length} 个 API Keys`);
} else {
// 逐个选择续期
for (const key of expiringKeys) {
console.log(`\n处理: ${key.name}`);
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: '选择操作:',
choices: [
{ name: '续期30天', value: '30' },
{ name: '续期90天', value: '90' },
{ name: '跳过', value: 'skip' }
]
}]);
if (action !== 'skip') {
const days = parseInt(action);
const newExpiresAt = new Date(new Date(key.expiresAt).getTime() + days * 24 * 60 * 60 * 1000).toISOString();
try {
await apiKeyService.updateApiKey(key.id, { expiresAt: newExpiresAt });
console.log(styles.success(`✅ 已续期 ${days}`));
} catch (error) {
console.log(styles.error(`❌ 续期失败: ${error.message}`));
}
}
}
}
} catch (error) {
spinner.fail('操作失败');
console.error(styles.error(error.message));
}
}
async function deleteApiKey() {
try {
const apiKeys = await apiKeyService.getAllApiKeys();
if (apiKeys.length === 0) {
console.log(styles.warning('没有找到任何 API Keys'));
return;
}
const { selectedKeys } = await inquirer.prompt([{
type: 'checkbox',
name: 'selectedKeys',
message: '选择要删除的 API Keys (空格选择,回车确认):',
choices: apiKeys.map(key => ({
name: `${key.name} (${key.apiKey?.substring(0, 20)}...)`,
value: key.id
}))
}]);
if (selectedKeys.length === 0) {
console.log(styles.info('未选择任何 API Key'));
return;
}
const { confirmed } = await inquirer.prompt([{
type: 'confirm',
name: 'confirmed',
message: styles.warning(`确认删除 ${selectedKeys.length} 个 API Keys?`),
default: false
}]);
if (!confirmed) {
console.log(styles.info('已取消删除'));
return;
}
const spinner = ora('正在删除 API Keys...').start();
let successCount = 0;
for (const keyId of selectedKeys) {
try {
await apiKeyService.deleteApiKey(keyId);
successCount++;
} catch (error) {
spinner.fail(`删除失败: ${error.message}`);
}
}
spinner.succeed(`成功删除 ${successCount}/${selectedKeys.length} 个 API Keys`);
} catch (error) {
console.error(styles.error('删除失败:', error.message));
}
}
async function listClaudeAccounts() {
const spinner = ora('正在获取 Claude 账户...').start();
@@ -251,6 +611,7 @@ 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 status - 查看系统状态');
console.log('\n使用 --help 查看详细帮助信息');
}