refactor: standardize code formatting and linting configuration

- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility
- Add .prettierrc configuration for consistent code formatting
- Update package.json with new lint and format scripts
- Add nodemon.json for development hot reloading configuration
- Standardize code formatting across all JavaScript and Vue files
- Update web admin SPA with improved linting rules and formatting
- Add prettier configuration to web admin SPA

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
千羽
2025-08-07 18:19:31 +09:00
parent 4a0eba117c
commit 8a74bf5afe
124 changed files with 20878 additions and 18757 deletions

View File

@@ -5,71 +5,74 @@
* 用于恢复会话窗口数据
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const zlib = require('zlib');
const redis = require('../src/models/redis');
const claudeAccountService = require('../src/services/claudeAccountService');
const logger = require('../src/utils/logger');
const fs = require('fs')
const path = require('path')
const readline = require('readline')
const zlib = require('zlib')
const redis = require('../src/models/redis')
class LogSessionAnalyzer {
constructor() {
// 更新正则表达式以匹配实际的日志格式
this.accountUsagePattern = /🎯 Using sticky session shared account: (.+?) \(([a-f0-9-]{36})\) for session ([a-f0-9]+)/;
this.processingPattern = /📡 Processing streaming API request with usage capture for key: (.+?), account: ([a-f0-9-]{36}), session: ([a-f0-9]+)/;
this.completedPattern = /🔗 ✅ Request completed in (\d+)ms for key: (.+)/;
this.usageRecordedPattern = /🔗 📊 Stream usage recorded \(real\) - Model: (.+?), Input: (\d+), Output: (\d+), Cache Create: (\d+), Cache Read: (\d+), Total: (\d+) tokens/;
this.timestampPattern = /\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/;
this.accounts = new Map();
this.requestHistory = [];
this.sessions = new Map(); // 记录会话信息
this.accountUsagePattern =
/🎯 Using sticky session shared account: (.+?) \(([a-f0-9-]{36})\) for session ([a-f0-9]+)/
this.processingPattern =
/📡 Processing streaming API request with usage capture for key: (.+?), account: ([a-f0-9-]{36}), session: ([a-f0-9]+)/
this.completedPattern = /🔗 ✅ Request completed in (\d+)ms for key: (.+)/
this.usageRecordedPattern =
/🔗 📊 Stream usage recorded \(real\) - Model: (.+?), Input: (\d+), Output: (\d+), Cache Create: (\d+), Cache Read: (\d+), Total: (\d+) tokens/
this.timestampPattern = /\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/
this.accounts = new Map()
this.requestHistory = []
this.sessions = new Map() // 记录会话信息
}
// 解析时间戳
parseTimestamp(line) {
const match = line.match(this.timestampPattern);
const match = line.match(this.timestampPattern)
if (match) {
return new Date(match[1]);
return new Date(match[1])
}
return null;
return null
}
// 分析单个日志文件
async analyzeLogFile(filePath) {
console.log(`📖 分析日志文件: ${filePath}`);
let fileStream = fs.createReadStream(filePath);
console.log(`📖 分析日志文件: ${filePath}`)
let fileStream = fs.createReadStream(filePath)
// 如果是gz文件需要先解压
if (filePath.endsWith('.gz')) {
console.log(` 🗜️ 检测到gz压缩文件正在解压...`);
fileStream = fileStream.pipe(zlib.createGunzip());
console.log(' 🗜️ 检测到gz压缩文件正在解压...')
fileStream = fileStream.pipe(zlib.createGunzip())
}
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let lineCount = 0;
let requestCount = 0;
let usageCount = 0;
})
let lineCount = 0
let requestCount = 0
let usageCount = 0
for await (const line of rl) {
lineCount++;
lineCount++
// 解析时间戳
const timestamp = this.parseTimestamp(line);
if (!timestamp) continue;
const timestamp = this.parseTimestamp(line)
if (!timestamp) {
continue
}
// 查找账户使用记录
const accountUsageMatch = line.match(this.accountUsagePattern);
const accountUsageMatch = line.match(this.accountUsagePattern)
if (accountUsageMatch) {
const accountName = accountUsageMatch[1];
const accountId = accountUsageMatch[2];
const sessionId = accountUsageMatch[3];
const accountName = accountUsageMatch[1]
const accountId = accountUsageMatch[2]
const sessionId = accountUsageMatch[3]
if (!this.accounts.has(accountId)) {
this.accounts.set(accountId, {
accountId,
@@ -79,27 +82,27 @@ class LogSessionAnalyzer {
lastRequest: timestamp,
totalRequests: 0,
sessions: new Set()
});
})
}
const account = this.accounts.get(accountId);
account.sessions.add(sessionId);
const account = this.accounts.get(accountId)
account.sessions.add(sessionId)
if (timestamp < account.firstRequest) {
account.firstRequest = timestamp;
account.firstRequest = timestamp
}
if (timestamp > account.lastRequest) {
account.lastRequest = timestamp;
account.lastRequest = timestamp
}
}
// 查找请求处理记录
const processingMatch = line.match(this.processingPattern);
const processingMatch = line.match(this.processingPattern)
if (processingMatch) {
const apiKeyName = processingMatch[1];
const accountId = processingMatch[2];
const sessionId = processingMatch[3];
const apiKeyName = processingMatch[1]
const accountId = processingMatch[2]
const sessionId = processingMatch[3]
if (!this.accounts.has(accountId)) {
this.accounts.set(accountId, {
accountId,
@@ -109,25 +112,25 @@ class LogSessionAnalyzer {
lastRequest: timestamp,
totalRequests: 0,
sessions: new Set()
});
})
}
const account = this.accounts.get(accountId);
const account = this.accounts.get(accountId)
account.requests.push({
timestamp,
apiKeyName,
sessionId,
type: 'processing'
});
account.sessions.add(sessionId);
account.totalRequests++;
requestCount++;
})
account.sessions.add(sessionId)
account.totalRequests++
requestCount++
if (timestamp > account.lastRequest) {
account.lastRequest = timestamp;
account.lastRequest = timestamp
}
// 记录到全局请求历史
this.requestHistory.push({
timestamp,
@@ -135,36 +138,36 @@ class LogSessionAnalyzer {
apiKeyName,
sessionId,
type: 'processing'
});
})
}
// 查找请求完成记录
const completedMatch = line.match(this.completedPattern);
const completedMatch = line.match(this.completedPattern)
if (completedMatch) {
const duration = parseInt(completedMatch[1]);
const apiKeyName = completedMatch[2];
const duration = parseInt(completedMatch[1])
const apiKeyName = completedMatch[2]
// 记录到全局请求历史
this.requestHistory.push({
timestamp,
apiKeyName,
duration,
type: 'completed'
});
})
}
// 查找使用统计记录
const usageMatch = line.match(this.usageRecordedPattern);
const usageMatch = line.match(this.usageRecordedPattern)
if (usageMatch) {
const model = usageMatch[1];
const inputTokens = parseInt(usageMatch[2]);
const outputTokens = parseInt(usageMatch[3]);
const cacheCreateTokens = parseInt(usageMatch[4]);
const cacheReadTokens = parseInt(usageMatch[5]);
const totalTokens = parseInt(usageMatch[6]);
usageCount++;
const model = usageMatch[1]
const inputTokens = parseInt(usageMatch[2])
const outputTokens = parseInt(usageMatch[3])
const cacheCreateTokens = parseInt(usageMatch[4])
const cacheReadTokens = parseInt(usageMatch[5])
const totalTokens = parseInt(usageMatch[6])
usageCount++
// 记录到全局请求历史
this.requestHistory.push({
timestamp,
@@ -175,119 +178,119 @@ class LogSessionAnalyzer {
cacheCreateTokens,
cacheReadTokens,
totalTokens
});
})
}
}
console.log(` 📊 解析完成: ${lineCount} 行, 找到 ${requestCount} 个请求记录, ${usageCount} 个使用统计`);
console.log(
` 📊 解析完成: ${lineCount} 行, 找到 ${requestCount} 个请求记录, ${usageCount} 个使用统计`
)
}
// 分析日志目录中的所有文件
async analyzeLogDirectory(logDir = './logs') {
console.log(`🔍 扫描日志目录: ${logDir}\n`);
console.log(`🔍 扫描日志目录: ${logDir}\n`)
try {
const files = fs.readdirSync(logDir);
const files = fs.readdirSync(logDir)
const logFiles = files
.filter(file => {
return file.includes('claude-relay') && (
file.endsWith('.log') ||
file.endsWith('.log.1') ||
file.endsWith('.log.gz') ||
file.match(/\.log\.\d+\.gz$/) ||
file.match(/\.log\.\d+$/)
);
})
.filter(
(file) =>
file.includes('claude-relay') &&
(file.endsWith('.log') ||
file.endsWith('.log.1') ||
file.endsWith('.log.gz') ||
file.match(/\.log\.\d+\.gz$/) ||
file.match(/\.log\.\d+$/))
)
.sort()
.reverse(); // 最新的文件优先
.reverse() // 最新的文件优先
if (logFiles.length === 0) {
console.log('❌ 没有找到日志文件');
return;
console.log('❌ 没有找到日志文件')
return
}
console.log(`📁 找到 ${logFiles.length} 个日志文件:`);
logFiles.forEach(file => console.log(` - ${file}`));
console.log('');
console.log(`📁 找到 ${logFiles.length} 个日志文件:`)
logFiles.forEach((file) => console.log(` - ${file}`))
console.log('')
// 分析每个文件
for (const file of logFiles) {
const filePath = path.join(logDir, file);
await this.analyzeLogFile(filePath);
const filePath = path.join(logDir, file)
await this.analyzeLogFile(filePath)
}
} catch (error) {
console.error(`❌ 读取日志目录失败: ${error.message}`);
throw error;
console.error(`❌ 读取日志目录失败: ${error.message}`)
throw error
}
}
// 分析单个日志文件(支持直接传入文件路径)
async analyzeSingleFile(filePath) {
console.log(`🔍 分析单个日志文件: ${filePath}\n`);
console.log(`🔍 分析单个日志文件: ${filePath}\n`)
try {
if (!fs.existsSync(filePath)) {
console.log('❌ 文件不存在');
return;
console.log('❌ 文件不存在')
return
}
await this.analyzeLogFile(filePath);
await this.analyzeLogFile(filePath)
} catch (error) {
console.error(`❌ 分析文件失败: ${error.message}`);
throw error;
console.error(`❌ 分析文件失败: ${error.message}`)
throw error
}
}
// 计算会话窗口
calculateSessionWindow(requestTime) {
const hour = requestTime.getHours();
const windowStartHour = Math.floor(hour / 5) * 5;
const windowStart = new Date(requestTime);
windowStart.setHours(windowStartHour, 0, 0, 0);
const windowEnd = new Date(windowStart);
windowEnd.setHours(windowEnd.getHours() + 5);
return { windowStart, windowEnd };
const hour = requestTime.getHours()
const windowStartHour = Math.floor(hour / 5) * 5
const windowStart = new Date(requestTime)
windowStart.setHours(windowStartHour, 0, 0, 0)
const windowEnd = new Date(windowStart)
windowEnd.setHours(windowEnd.getHours() + 5)
return { windowStart, windowEnd }
}
// 分析会话窗口
analyzeSessionWindows() {
console.log('🕐 分析会话窗口...\n');
const now = new Date();
const results = [];
console.log('🕐 分析会话窗口...\n')
const now = new Date()
const results = []
for (const [accountId, accountData] of this.accounts) {
const sessions = [];
const requests = accountData.requests.sort((a, b) => a.timestamp - b.timestamp);
const requests = accountData.requests.sort((a, b) => a.timestamp - b.timestamp)
// 按会话窗口分组请求
const windowGroups = new Map();
const windowGroups = new Map()
for (const request of requests) {
const { windowStart, windowEnd } = this.calculateSessionWindow(request.timestamp);
const windowKey = `${windowStart.getTime()}-${windowEnd.getTime()}`;
const { windowStart, windowEnd } = this.calculateSessionWindow(request.timestamp)
const windowKey = `${windowStart.getTime()}-${windowEnd.getTime()}`
if (!windowGroups.has(windowKey)) {
windowGroups.set(windowKey, {
windowStart,
windowEnd,
requests: [],
isActive: now >= windowStart && now < windowEnd
});
})
}
windowGroups.get(windowKey).requests.push(request);
windowGroups.get(windowKey).requests.push(request)
}
// 转换为数组并排序
const windowArray = Array.from(windowGroups.values())
.sort((a, b) => b.windowStart - a.windowStart); // 最新的窗口优先
const windowArray = Array.from(windowGroups.values()).sort(
(a, b) => b.windowStart - a.windowStart
) // 最新的窗口优先
const result = {
accountId,
accountName: accountData.accountName,
@@ -296,240 +299,247 @@ class LogSessionAnalyzer {
lastRequest: accountData.lastRequest,
sessions: accountData.sessions,
windows: windowArray,
currentActiveWindow: windowArray.find(w => w.isActive) || null,
currentActiveWindow: windowArray.find((w) => w.isActive) || null,
mostRecentWindow: windowArray[0] || null
};
results.push(result);
}
results.push(result)
}
return results.sort((a, b) => b.lastRequest - a.lastRequest);
return results.sort((a, b) => b.lastRequest - a.lastRequest)
}
// 显示分析结果
displayResults(results) {
console.log('📊 分析结果:\n');
console.log('='.repeat(80));
console.log('📊 分析结果:\n')
console.log('='.repeat(80))
for (const result of results) {
console.log(`🏢 账户: ${result.accountName || 'Unknown'} (${result.accountId})`);
console.log(` 总请求数: ${result.totalRequests}`);
console.log(` 会话数: ${result.sessions ? result.sessions.size : 0}`);
console.log(` 首次请求: ${result.firstRequest.toLocaleString()}`);
console.log(` 最后请求: ${result.lastRequest.toLocaleString()}`);
console.log(`🏢 账户: ${result.accountName || 'Unknown'} (${result.accountId})`)
console.log(` 总请求数: ${result.totalRequests}`)
console.log(` 会话数: ${result.sessions ? result.sessions.size : 0}`)
console.log(` 首次请求: ${result.firstRequest.toLocaleString()}`)
console.log(` 最后请求: ${result.lastRequest.toLocaleString()}`)
if (result.currentActiveWindow) {
console.log(` ✅ 当前活跃窗口: ${result.currentActiveWindow.windowStart.toLocaleString()} - ${result.currentActiveWindow.windowEnd.toLocaleString()}`);
console.log(` 窗口内请求: ${result.currentActiveWindow.requests.length}`);
const progress = this.calculateWindowProgress(result.currentActiveWindow.windowStart, result.currentActiveWindow.windowEnd);
console.log(` 窗口进度: ${progress}%`);
console.log(
` ✅ 当前活跃窗口: ${result.currentActiveWindow.windowStart.toLocaleString()} - ${result.currentActiveWindow.windowEnd.toLocaleString()}`
)
console.log(` 窗口内请求: ${result.currentActiveWindow.requests.length}`)
const progress = this.calculateWindowProgress(
result.currentActiveWindow.windowStart,
result.currentActiveWindow.windowEnd
)
console.log(` 窗口进度: ${progress}%`)
} else if (result.mostRecentWindow) {
const window = result.mostRecentWindow;
console.log(` ⏰ 最近窗口(已过期): ${window.windowStart.toLocaleString()} - ${window.windowEnd.toLocaleString()}`);
console.log(` 窗口内请求: ${window.requests.length}`);
const hoursAgo = Math.round((new Date() - window.windowEnd) / (1000 * 60 * 60));
console.log(` 过期时间: ${hoursAgo} 小时前`);
const window = result.mostRecentWindow
console.log(
` ⏰ 最近窗口(已过期): ${window.windowStart.toLocaleString()} - ${window.windowEnd.toLocaleString()}`
)
console.log(` 窗口内请求: ${window.requests.length} `)
const hoursAgo = Math.round((new Date() - window.windowEnd) / (1000 * 60 * 60))
console.log(` 过期时间: ${hoursAgo} 小时前`)
} else {
console.log(` ❌ 无会话窗口数据`);
console.log(' ❌ 无会话窗口数据')
}
// 显示最近几个窗口
if (result.windows.length > 1) {
console.log(` 📈 历史窗口: ${result.windows.length}`);
const recentWindows = result.windows.slice(0, 3);
console.log(` 📈 历史窗口: ${result.windows.length}`)
const recentWindows = result.windows.slice(0, 3)
for (let i = 0; i < recentWindows.length; i++) {
const window = recentWindows[i];
const status = window.isActive ? '活跃' : '已过期';
console.log(` ${i + 1}. ${window.windowStart.toLocaleString()} - ${window.windowEnd.toLocaleString()} (${status}, ${window.requests.length}次请求)`);
const window = recentWindows[i]
const status = window.isActive ? '活跃' : '已过期'
console.log(
` ${i + 1}. ${window.windowStart.toLocaleString()} - ${window.windowEnd.toLocaleString()} (${status}, ${window.requests.length}次请求)`
)
}
}
// 显示最近几个会话的API Key使用情况
const accountData = this.accounts.get(result.accountId);
const accountData = this.accounts.get(result.accountId)
if (accountData && accountData.requests && accountData.requests.length > 0) {
const recentRequests = accountData.requests.slice(-5); // 最近5个请求
const apiKeyStats = {};
const apiKeyStats = {}
for (const req of accountData.requests) {
if (!apiKeyStats[req.apiKeyName]) {
apiKeyStats[req.apiKeyName] = 0;
apiKeyStats[req.apiKeyName] = 0
}
apiKeyStats[req.apiKeyName]++;
apiKeyStats[req.apiKeyName]++
}
console.log(` 🔑 API Key使用统计:`);
console.log(' 🔑 API Key使用统计:')
for (const [keyName, count] of Object.entries(apiKeyStats)) {
console.log(` - ${keyName}: ${count}`);
console.log(` - ${keyName}: ${count}`)
}
}
console.log('');
console.log('')
}
console.log('='.repeat(80));
console.log(`总计: ${results.length} 个账户, ${this.requestHistory.length} 个日志记录\n`);
console.log('='.repeat(80))
console.log(`总计: ${results.length} 个账户, ${this.requestHistory.length} 个日志记录\n`)
}
// 计算窗口进度百分比
calculateWindowProgress(windowStart, windowEnd) {
const now = new Date();
const totalDuration = windowEnd.getTime() - windowStart.getTime();
const elapsedTime = now.getTime() - windowStart.getTime();
return Math.max(0, Math.min(100, Math.round((elapsedTime / totalDuration) * 100)));
const now = new Date()
const totalDuration = windowEnd.getTime() - windowStart.getTime()
const elapsedTime = now.getTime() - windowStart.getTime()
return Math.max(0, Math.min(100, Math.round((elapsedTime / totalDuration) * 100)))
}
// 更新Redis中的会话窗口数据
async updateRedisSessionWindows(results, dryRun = true) {
if (dryRun) {
console.log('🧪 模拟模式 - 不会实际更新Redis数据\n');
console.log('🧪 模拟模式 - 不会实际更新Redis数据\n')
} else {
console.log('💾 更新Redis中的会话窗口数据...\n');
await redis.connect();
console.log('💾 更新Redis中的会话窗口数据...\n')
await redis.connect()
}
let updatedCount = 0;
let skippedCount = 0;
let updatedCount = 0
let skippedCount = 0
for (const result of results) {
try {
const accountData = await redis.getClaudeAccount(result.accountId);
const accountData = await redis.getClaudeAccount(result.accountId)
if (!accountData || Object.keys(accountData).length === 0) {
console.log(`⚠️ 账户 ${result.accountId} 在Redis中不存在跳过`);
skippedCount++;
continue;
console.log(`⚠️ 账户 ${result.accountId} 在Redis中不存在跳过`)
skippedCount++
continue
}
console.log(`🔄 处理账户: ${accountData.name || result.accountId}`);
console.log(`🔄 处理账户: ${accountData.name || result.accountId}`)
// 确定要设置的会话窗口
let targetWindow = null;
let targetWindow = null
if (result.currentActiveWindow) {
targetWindow = result.currentActiveWindow;
console.log(` ✅ 使用当前活跃窗口: ${targetWindow.windowStart.toLocaleString()} - ${targetWindow.windowEnd.toLocaleString()}`);
targetWindow = result.currentActiveWindow
console.log(
` ✅ 使用当前活跃窗口: ${targetWindow.windowStart.toLocaleString()} - ${targetWindow.windowEnd.toLocaleString()}`
)
} else if (result.mostRecentWindow) {
const window = result.mostRecentWindow;
const now = new Date();
const window = result.mostRecentWindow
const now = new Date()
// 如果最近窗口是在过去24小时内的可以考虑恢复
const hoursSinceWindow = (now - window.windowEnd) / (1000 * 60 * 60);
const hoursSinceWindow = (now - window.windowEnd) / (1000 * 60 * 60)
if (hoursSinceWindow <= 24) {
console.log(` 🕐 最近窗口在24小时内但已过期: ${window.windowStart.toLocaleString()} - ${window.windowEnd.toLocaleString()}`);
console.log(` ❌ 不恢复已过期窗口(${hoursSinceWindow.toFixed(1)}小时前过期)`);
console.log(
` 🕐 最近窗口在24小时内但已过期: ${window.windowStart.toLocaleString()} - ${window.windowEnd.toLocaleString()}`
)
console.log(` ❌ 不恢复已过期窗口(${hoursSinceWindow.toFixed(1)}小时前过期)`)
} else {
console.log(` ⏰ 最近窗口超过24小时前不予恢复`);
console.log(' ⏰ 最近窗口超过24小时前不予恢复')
}
}
if (targetWindow && !dryRun) {
// 更新Redis中的会话窗口数据
accountData.sessionWindowStart = targetWindow.windowStart.toISOString();
accountData.sessionWindowEnd = targetWindow.windowEnd.toISOString();
accountData.lastUsedAt = result.lastRequest.toISOString();
accountData.lastRequestTime = result.lastRequest.toISOString();
await redis.setClaudeAccount(result.accountId, accountData);
updatedCount++;
console.log(` ✅ 已更新会话窗口数据`);
accountData.sessionWindowStart = targetWindow.windowStart.toISOString()
accountData.sessionWindowEnd = targetWindow.windowEnd.toISOString()
accountData.lastUsedAt = result.lastRequest.toISOString()
accountData.lastRequestTime = result.lastRequest.toISOString()
await redis.setClaudeAccount(result.accountId, accountData)
updatedCount++
console.log(' ✅ 已更新会话窗口数据')
} else if (targetWindow) {
updatedCount++;
console.log(` 🧪 [模拟] 将设置会话窗口: ${targetWindow.windowStart.toLocaleString()} - ${targetWindow.windowEnd.toLocaleString()}`);
updatedCount++
console.log(
` 🧪 [模拟] 将设置会话窗口: ${targetWindow.windowStart.toLocaleString()} - ${targetWindow.windowEnd.toLocaleString()}`
)
} else {
skippedCount++;
console.log(` ⏭️ 跳过(无有效窗口)`);
skippedCount++
console.log(' ⏭️ 跳过(无有效窗口)')
}
console.log('');
console.log('')
} catch (error) {
console.error(`❌ 处理账户 ${result.accountId} 时出错: ${error.message}`);
skippedCount++;
console.error(`❌ 处理账户 ${result.accountId} 时出错: ${error.message}`)
skippedCount++
}
}
if (!dryRun) {
await redis.disconnect();
await redis.disconnect()
}
console.log('📊 更新结果:');
console.log(` ✅ 已更新: ${updatedCount}`);
console.log(` ⏭️ 已跳过: ${skippedCount}`);
console.log(` 📋 总计: ${results.length}`);
console.log('📊 更新结果:')
console.log(` ✅ 已更新: ${updatedCount}`)
console.log(` ⏭️ 已跳过: ${skippedCount}`)
console.log(` 📋 总计: ${results.length}`)
}
// 主分析函数
async analyze(options = {}) {
const {
logDir = './logs',
singleFile = null,
updateRedis = false,
dryRun = true
} = options;
const { logDir = './logs', singleFile = null, updateRedis = false, dryRun = true } = options
try {
console.log('🔍 Claude账户会话窗口分析工具\n');
console.log('🔍 Claude账户会话窗口分析工具\n')
// 分析日志文件
if (singleFile) {
await this.analyzeSingleFile(singleFile);
await this.analyzeSingleFile(singleFile)
} else {
await this.analyzeLogDirectory(logDir);
await this.analyzeLogDirectory(logDir)
}
if (this.accounts.size === 0) {
console.log('❌ 没有找到任何Claude账户的请求记录');
return;
console.log('❌ 没有找到任何Claude账户的请求记录')
return []
}
// 分析会话窗口
const results = this.analyzeSessionWindows();
const results = this.analyzeSessionWindows()
// 显示结果
this.displayResults(results);
this.displayResults(results)
// 更新Redis如果需要
if (updateRedis) {
await this.updateRedisSessionWindows(results, dryRun);
await this.updateRedisSessionWindows(results, dryRun)
}
return results;
return results
} catch (error) {
console.error('❌ 分析失败:', error);
throw error;
console.error('❌ 分析失败:', error)
throw error
}
}
}
// 命令行参数解析
function parseArgs() {
const args = process.argv.slice(2);
const args = process.argv.slice(2)
const options = {
logDir: './logs',
singleFile: null,
updateRedis: false,
dryRun: true
};
}
for (const arg of args) {
if (arg.startsWith('--log-dir=')) {
options.logDir = arg.split('=')[1];
options.logDir = arg.split('=')[1]
} else if (arg.startsWith('--file=')) {
options.singleFile = arg.split('=')[1];
options.singleFile = arg.split('=')[1]
} else if (arg === '--update-redis') {
options.updateRedis = true;
options.updateRedis = true
} else if (arg === '--no-dry-run') {
options.dryRun = false;
options.dryRun = false
} else if (arg === '--help' || arg === '-h') {
showHelp();
process.exit(0);
showHelp()
process.exit(0)
}
}
return options;
return options
}
// 显示帮助信息
@@ -570,28 +580,27 @@ Claude账户会话窗口日志分析工具
- 窗口按整点对齐(如 05:00-10:00, 10:00-15:00
- 只有当前时间在窗口内的才被认为是活跃窗口
- 工具会自动识别并恢复活跃的会话窗口
`);
`)
}
// 主函数
async function main() {
try {
const options = parseArgs();
const analyzer = new LogSessionAnalyzer();
await analyzer.analyze(options);
console.log('🎉 分析完成');
const options = parseArgs()
const analyzer = new LogSessionAnalyzer()
await analyzer.analyze(options)
console.log('🎉 分析完成')
} catch (error) {
console.error('💥 程序执行失败:', error);
process.exit(1);
console.error('💥 程序执行失败:', error)
process.exit(1)
}
}
// 如果直接运行此脚本
if (require.main === module) {
main();
main()
}
module.exports = LogSessionAnalyzer;
module.exports = LogSessionAnalyzer

View File

@@ -2,51 +2,52 @@
* 检查 Redis 中的所有键
*/
const redis = require('../src/models/redis');
const redis = require('../src/models/redis')
async function checkRedisKeys() {
console.log('🔍 检查 Redis 中的所有键...\n');
console.log('🔍 检查 Redis 中的所有键...\n')
try {
// 确保 Redis 已连接
await redis.connect();
await redis.connect()
// 获取所有键
const allKeys = await redis.client.keys('*');
console.log(`找到 ${allKeys.length} 个键\n`);
const allKeys = await redis.client.keys('*')
console.log(`找到 ${allKeys.length} 个键\n`)
// 按类型分组
const keysByType = {};
allKeys.forEach(key => {
const prefix = key.split(':')[0];
const keysByType = {}
allKeys.forEach((key) => {
const prefix = key.split(':')[0]
if (!keysByType[prefix]) {
keysByType[prefix] = [];
keysByType[prefix] = []
}
keysByType[prefix].push(key);
});
keysByType[prefix].push(key)
})
// 显示各类型的键
Object.keys(keysByType).sort().forEach(type => {
console.log(`\n📁 ${type}: ${keysByType[type].length}`);
// 显示前 5 个键作为示例
const keysToShow = keysByType[type].slice(0, 5);
keysToShow.forEach(key => {
console.log(` - ${key}`);
});
if (keysByType[type].length > 5) {
console.log(` ... 还有 ${keysByType[type].length - 5}`);
}
});
Object.keys(keysByType)
.sort()
.forEach((type) => {
console.log(`\n📁 ${type}: ${keysByType[type].length}`)
// 显示前 5 个键作为示例
const keysToShow = keysByType[type].slice(0, 5)
keysToShow.forEach((key) => {
console.log(` - ${key}`)
})
if (keysByType[type].length > 5) {
console.log(` ... 还有 ${keysByType[type].length - 5}`)
}
})
} catch (error) {
console.error('❌ 错误:', error);
console.error(error.stack);
console.error('❌ 错误:', error)
console.error(error.stack)
} finally {
process.exit(0);
process.exit(0)
}
}
checkRedisKeys();
checkRedisKeys()

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,11 @@
/**
* 数据导出/导入工具
*
*
* 使用方法:
* 导出: node scripts/data-transfer.js export --output=backup.json [options]
* 导入: node scripts/data-transfer.js import --input=backup.json [options]
*
*
* 选项:
* --types: 要导出/导入的数据类型apikeys,accounts,admins,all
* --sanitize: 导出时脱敏敏感数据
@@ -14,437 +14,445 @@
* --skip-conflicts: 导入时跳过冲突的数据
*/
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const redis = require('../src/models/redis');
const logger = require('../src/utils/logger');
const readline = require('readline');
const fs = require('fs').promises
const redis = require('../src/models/redis')
const logger = require('../src/utils/logger')
const readline = require('readline')
// 解析命令行参数
const args = process.argv.slice(2);
const command = args[0];
const params = {};
const args = process.argv.slice(2)
const command = args[0]
const params = {}
args.slice(1).forEach(arg => {
const [key, value] = arg.split('=');
params[key.replace('--', '')] = value || true;
});
args.slice(1).forEach((arg) => {
const [key, value] = arg.split('=')
params[key.replace('--', '')] = value || true
})
// 创建 readline 接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
})
async function askConfirmation(question) {
return new Promise((resolve) => {
rl.question(question + ' (yes/no): ', (answer) => {
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y');
});
});
rl.question(`${question} (yes/no): `, (answer) => {
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y')
})
})
}
// 数据脱敏函数
function sanitizeData(data, type) {
const sanitized = { ...data };
const sanitized = { ...data }
switch (type) {
case 'apikey':
// 隐藏 API Key 的大部分内容
if (sanitized.apiKey) {
sanitized.apiKey = sanitized.apiKey.substring(0, 10) + '...[REDACTED]';
sanitized.apiKey = `${sanitized.apiKey.substring(0, 10)}...[REDACTED]`
}
break;
break
case 'claude_account':
case 'gemini_account':
// 隐藏 OAuth tokens
if (sanitized.accessToken) {
sanitized.accessToken = '[REDACTED]';
sanitized.accessToken = '[REDACTED]'
}
if (sanitized.refreshToken) {
sanitized.refreshToken = '[REDACTED]';
sanitized.refreshToken = '[REDACTED]'
}
if (sanitized.claudeAiOauth) {
sanitized.claudeAiOauth = '[REDACTED]';
sanitized.claudeAiOauth = '[REDACTED]'
}
// 隐藏代理密码
if (sanitized.proxyPassword) {
sanitized.proxyPassword = '[REDACTED]';
sanitized.proxyPassword = '[REDACTED]'
}
break;
break
case 'admin':
// 隐藏管理员密码
if (sanitized.password) {
sanitized.password = '[REDACTED]';
sanitized.password = '[REDACTED]'
}
break;
break
}
return sanitized;
return sanitized
}
// 导出数据
async function exportData() {
try {
const outputFile = params.output || `backup-${new Date().toISOString().split('T')[0]}.json`;
const types = params.types ? params.types.split(',') : ['all'];
const shouldSanitize = params.sanitize === true;
logger.info('🔄 Starting data export...');
logger.info(`📁 Output file: ${outputFile}`);
logger.info(`📋 Data types: ${types.join(', ')}`);
logger.info(`🔒 Sanitize sensitive data: ${shouldSanitize ? 'YES' : 'NO'}`);
const outputFile = params.output || `backup-${new Date().toISOString().split('T')[0]}.json`
const types = params.types ? params.types.split(',') : ['all']
const shouldSanitize = params.sanitize === true
logger.info('🔄 Starting data export...')
logger.info(`📁 Output file: ${outputFile}`)
logger.info(`📋 Data types: ${types.join(', ')}`)
logger.info(`🔒 Sanitize sensitive data: ${shouldSanitize ? 'YES' : 'NO'}`)
// 连接 Redis
await redis.connect();
logger.success('✅ Connected to Redis');
const exportData = {
await redis.connect()
logger.success('✅ Connected to Redis')
const exportDataObj = {
metadata: {
version: '1.0',
exportDate: new Date().toISOString(),
sanitized: shouldSanitize,
types: types
types
},
data: {}
};
}
// 导出 API Keys
if (types.includes('all') || types.includes('apikeys')) {
logger.info('📤 Exporting API Keys...');
const keys = await redis.client.keys('apikey:*');
const apiKeys = [];
logger.info('📤 Exporting API Keys...')
const keys = await redis.client.keys('apikey:*')
const apiKeys = []
for (const key of keys) {
if (key === 'apikey:hash_map') continue;
if (key === 'apikey:hash_map') {
continue
}
// 使用 hgetall 而不是 get因为数据存储在哈希表中
const data = await redis.client.hgetall(key);
const data = await redis.client.hgetall(key)
if (data && Object.keys(data).length > 0) {
apiKeys.push(shouldSanitize ? sanitizeData(data, 'apikey') : data);
apiKeys.push(shouldSanitize ? sanitizeData(data, 'apikey') : data)
}
}
exportData.data.apiKeys = apiKeys;
logger.success(`✅ Exported ${apiKeys.length} API Keys`);
exportDataObj.data.apiKeys = apiKeys
logger.success(`✅ Exported ${apiKeys.length} API Keys`)
}
// 导出 Claude 账户
if (types.includes('all') || types.includes('accounts')) {
logger.info('📤 Exporting Claude accounts...');
logger.info('📤 Exporting Claude accounts...')
// 注意Claude 账户使用 claude:account: 前缀,不是 claude_account:
const keys = await redis.client.keys('claude:account:*');
logger.info(`Found ${keys.length} Claude account keys in Redis`);
const accounts = [];
const keys = await redis.client.keys('claude:account:*')
logger.info(`Found ${keys.length} Claude account keys in Redis`)
const accounts = []
for (const key of keys) {
// 使用 hgetall 而不是 get因为数据存储在哈希表中
const data = await redis.client.hgetall(key);
const data = await redis.client.hgetall(key)
if (data && Object.keys(data).length > 0) {
// 解析 JSON 字段(如果存在)
if (data.claudeAiOauth) {
try {
data.claudeAiOauth = JSON.parse(data.claudeAiOauth);
data.claudeAiOauth = JSON.parse(data.claudeAiOauth)
} catch (e) {
// 保持原样
}
}
accounts.push(shouldSanitize ? sanitizeData(data, 'claude_account') : data);
accounts.push(shouldSanitize ? sanitizeData(data, 'claude_account') : data)
}
}
exportData.data.claudeAccounts = accounts;
logger.success(`✅ Exported ${accounts.length} Claude accounts`);
exportDataObj.data.claudeAccounts = accounts
logger.success(`✅ Exported ${accounts.length} Claude accounts`)
// 导出 Gemini 账户
logger.info('📤 Exporting Gemini accounts...');
const geminiKeys = await redis.client.keys('gemini_account:*');
logger.info(`Found ${geminiKeys.length} Gemini account keys in Redis`);
const geminiAccounts = [];
logger.info('📤 Exporting Gemini accounts...')
const geminiKeys = await redis.client.keys('gemini_account:*')
logger.info(`Found ${geminiKeys.length} Gemini account keys in Redis`)
const geminiAccounts = []
for (const key of geminiKeys) {
// 使用 hgetall 而不是 get因为数据存储在哈希表中
const data = await redis.client.hgetall(key);
const data = await redis.client.hgetall(key)
if (data && Object.keys(data).length > 0) {
geminiAccounts.push(shouldSanitize ? sanitizeData(data, 'gemini_account') : data);
geminiAccounts.push(shouldSanitize ? sanitizeData(data, 'gemini_account') : data)
}
}
exportData.data.geminiAccounts = geminiAccounts;
logger.success(`✅ Exported ${geminiAccounts.length} Gemini accounts`);
exportDataObj.data.geminiAccounts = geminiAccounts
logger.success(`✅ Exported ${geminiAccounts.length} Gemini accounts`)
}
// 导出管理员
if (types.includes('all') || types.includes('admins')) {
logger.info('📤 Exporting admins...');
const keys = await redis.client.keys('admin:*');
const admins = [];
logger.info('📤 Exporting admins...')
const keys = await redis.client.keys('admin:*')
const admins = []
for (const key of keys) {
if (key.includes('admin_username:')) continue;
if (key.includes('admin_username:')) {
continue
}
// 使用 hgetall 而不是 get因为数据存储在哈希表中
const data = await redis.client.hgetall(key);
const data = await redis.client.hgetall(key)
if (data && Object.keys(data).length > 0) {
admins.push(shouldSanitize ? sanitizeData(data, 'admin') : data);
admins.push(shouldSanitize ? sanitizeData(data, 'admin') : data)
}
}
exportData.data.admins = admins;
logger.success(`✅ Exported ${admins.length} admins`);
exportDataObj.data.admins = admins
logger.success(`✅ Exported ${admins.length} admins`)
}
// 写入文件
await fs.writeFile(outputFile, JSON.stringify(exportData, null, 2));
await fs.writeFile(outputFile, JSON.stringify(exportData, null, 2))
// 显示导出摘要
console.log('\n' + '='.repeat(60));
console.log('✅ Export Complete!');
console.log('='.repeat(60));
console.log(`Output file: ${outputFile}`);
console.log(`File size: ${(await fs.stat(outputFile)).size} bytes`);
if (exportData.data.apiKeys) {
console.log(`API Keys: ${exportData.data.apiKeys.length}`);
console.log(`\n${'='.repeat(60)}`)
console.log('✅ Export Complete!')
console.log('='.repeat(60))
console.log(`Output file: ${outputFile}`)
console.log(`File size: ${(await fs.stat(outputFile)).size} bytes`)
if (exportDataObj.data.apiKeys) {
console.log(`API Keys: ${exportDataObj.data.apiKeys.length}`)
}
if (exportData.data.claudeAccounts) {
console.log(`Claude Accounts: ${exportData.data.claudeAccounts.length}`);
if (exportDataObj.data.claudeAccounts) {
console.log(`Claude Accounts: ${exportDataObj.data.claudeAccounts.length}`)
}
if (exportData.data.geminiAccounts) {
console.log(`Gemini Accounts: ${exportData.data.geminiAccounts.length}`);
if (exportDataObj.data.geminiAccounts) {
console.log(`Gemini Accounts: ${exportDataObj.data.geminiAccounts.length}`)
}
if (exportData.data.admins) {
console.log(`Admins: ${exportData.data.admins.length}`);
if (exportDataObj.data.admins) {
console.log(`Admins: ${exportDataObj.data.admins.length}`)
}
console.log('='.repeat(60));
console.log('='.repeat(60))
if (shouldSanitize) {
logger.warn('⚠️ Sensitive data has been sanitized in this export.');
logger.warn('⚠️ Sensitive data has been sanitized in this export.')
}
} catch (error) {
logger.error('💥 Export failed:', error);
process.exit(1);
logger.error('💥 Export failed:', error)
process.exit(1)
} finally {
await redis.disconnect();
rl.close();
await redis.disconnect()
rl.close()
}
}
// 导入数据
async function importData() {
try {
const inputFile = params.input;
const inputFile = params.input
if (!inputFile) {
logger.error('❌ Please specify input file with --input=filename.json');
process.exit(1);
logger.error('❌ Please specify input file with --input=filename.json')
process.exit(1)
}
const forceOverwrite = params.force === true;
const skipConflicts = params['skip-conflicts'] === true;
logger.info('🔄 Starting data import...');
logger.info(`📁 Input file: ${inputFile}`);
logger.info(`⚡ Mode: ${forceOverwrite ? 'FORCE OVERWRITE' : (skipConflicts ? 'SKIP CONFLICTS' : 'ASK ON CONFLICT')}`);
const forceOverwrite = params.force === true
const skipConflicts = params['skip-conflicts'] === true
logger.info('🔄 Starting data import...')
logger.info(`📁 Input file: ${inputFile}`)
logger.info(
`⚡ Mode: ${forceOverwrite ? 'FORCE OVERWRITE' : skipConflicts ? 'SKIP CONFLICTS' : 'ASK ON CONFLICT'}`
)
// 读取文件
const fileContent = await fs.readFile(inputFile, 'utf8');
const importData = JSON.parse(fileContent);
const fileContent = await fs.readFile(inputFile, 'utf8')
const importDataObj = JSON.parse(fileContent)
// 验证文件格式
if (!importData.metadata || !importData.data) {
logger.error('❌ Invalid backup file format');
process.exit(1);
if (!importDataObj.metadata || !importDataObj.data) {
logger.error('❌ Invalid backup file format')
process.exit(1)
}
logger.info(`📅 Backup date: ${importData.metadata.exportDate}`);
logger.info(`🔒 Sanitized: ${importData.metadata.sanitized ? 'YES' : 'NO'}`);
if (importData.metadata.sanitized) {
logger.warn('⚠️ This backup contains sanitized data. Sensitive fields will be missing!');
const proceed = await askConfirmation('Continue with sanitized data?');
logger.info(`📅 Backup date: ${importDataObj.metadata.exportDate}`)
logger.info(`🔒 Sanitized: ${importDataObj.metadata.sanitized ? 'YES' : 'NO'}`)
if (importDataObj.metadata.sanitized) {
logger.warn('⚠️ This backup contains sanitized data. Sensitive fields will be missing!')
const proceed = await askConfirmation('Continue with sanitized data?')
if (!proceed) {
logger.info('❌ Import cancelled');
return;
logger.info('❌ Import cancelled')
return
}
}
// 显示导入摘要
console.log('\n' + '='.repeat(60));
console.log('📋 Import Summary:');
console.log('='.repeat(60));
if (importData.data.apiKeys) {
console.log(`API Keys to import: ${importData.data.apiKeys.length}`);
console.log(`\n${'='.repeat(60)}`)
console.log('📋 Import Summary:')
console.log('='.repeat(60))
if (importDataObj.data.apiKeys) {
console.log(`API Keys to import: ${importDataObj.data.apiKeys.length}`)
}
if (importData.data.claudeAccounts) {
console.log(`Claude Accounts to import: ${importData.data.claudeAccounts.length}`);
if (importDataObj.data.claudeAccounts) {
console.log(`Claude Accounts to import: ${importDataObj.data.claudeAccounts.length}`)
}
if (importData.data.geminiAccounts) {
console.log(`Gemini Accounts to import: ${importData.data.geminiAccounts.length}`);
if (importDataObj.data.geminiAccounts) {
console.log(`Gemini Accounts to import: ${importDataObj.data.geminiAccounts.length}`)
}
if (importData.data.admins) {
console.log(`Admins to import: ${importData.data.admins.length}`);
if (importDataObj.data.admins) {
console.log(`Admins to import: ${importDataObj.data.admins.length}`)
}
console.log('='.repeat(60) + '\n');
console.log(`${'='.repeat(60)}\n`)
// 确认导入
const confirmed = await askConfirmation('⚠️ Proceed with import?');
const confirmed = await askConfirmation('⚠️ Proceed with import?')
if (!confirmed) {
logger.info('❌ Import cancelled');
return;
logger.info('❌ Import cancelled')
return
}
// 连接 Redis
await redis.connect();
logger.success('✅ Connected to Redis');
await redis.connect()
logger.success('✅ Connected to Redis')
const stats = {
imported: 0,
skipped: 0,
errors: 0
};
}
// 导入 API Keys
if (importData.data.apiKeys) {
logger.info('\n📥 Importing API Keys...');
for (const apiKey of importData.data.apiKeys) {
if (importDataObj.data.apiKeys) {
logger.info('\n📥 Importing API Keys...')
for (const apiKey of importDataObj.data.apiKeys) {
try {
const exists = await redis.client.exists(`apikey:${apiKey.id}`);
const exists = await redis.client.exists(`apikey:${apiKey.id}`)
if (exists && !forceOverwrite) {
if (skipConflicts) {
logger.warn(`⏭️ Skipped existing API Key: ${apiKey.name} (${apiKey.id})`);
stats.skipped++;
continue;
logger.warn(`⏭️ Skipped existing API Key: ${apiKey.name} (${apiKey.id})`)
stats.skipped++
continue
} else {
const overwrite = await askConfirmation(`API Key "${apiKey.name}" (${apiKey.id}) exists. Overwrite?`);
const overwrite = await askConfirmation(
`API Key "${apiKey.name}" (${apiKey.id}) exists. Overwrite?`
)
if (!overwrite) {
stats.skipped++;
continue;
stats.skipped++
continue
}
}
}
// 使用 hset 存储到哈希表
const pipeline = redis.client.pipeline();
const pipeline = redis.client.pipeline()
for (const [field, value] of Object.entries(apiKey)) {
pipeline.hset(`apikey:${apiKey.id}`, field, value);
pipeline.hset(`apikey:${apiKey.id}`, field, value)
}
await pipeline.exec();
await pipeline.exec()
// 更新哈希映射
if (apiKey.apiKey && !importData.metadata.sanitized) {
await redis.client.hset('apikey:hash_map', apiKey.apiKey, apiKey.id);
if (apiKey.apiKey && !importDataObj.metadata.sanitized) {
await redis.client.hset('apikey:hash_map', apiKey.apiKey, apiKey.id)
}
logger.success(`✅ Imported API Key: ${apiKey.name} (${apiKey.id})`);
stats.imported++;
logger.success(`✅ Imported API Key: ${apiKey.name} (${apiKey.id})`)
stats.imported++
} catch (error) {
logger.error(`❌ Failed to import API Key ${apiKey.id}:`, error.message);
stats.errors++;
logger.error(`❌ Failed to import API Key ${apiKey.id}:`, error.message)
stats.errors++
}
}
}
// 导入 Claude 账户
if (importData.data.claudeAccounts) {
logger.info('\n📥 Importing Claude accounts...');
for (const account of importData.data.claudeAccounts) {
if (importDataObj.data.claudeAccounts) {
logger.info('\n📥 Importing Claude accounts...')
for (const account of importDataObj.data.claudeAccounts) {
try {
const exists = await redis.client.exists(`claude_account:${account.id}`);
const exists = await redis.client.exists(`claude_account:${account.id}`)
if (exists && !forceOverwrite) {
if (skipConflicts) {
logger.warn(`⏭️ Skipped existing Claude account: ${account.name} (${account.id})`);
stats.skipped++;
continue;
logger.warn(`⏭️ Skipped existing Claude account: ${account.name} (${account.id})`)
stats.skipped++
continue
} else {
const overwrite = await askConfirmation(`Claude account "${account.name}" (${account.id}) exists. Overwrite?`);
const overwrite = await askConfirmation(
`Claude account "${account.name}" (${account.id}) exists. Overwrite?`
)
if (!overwrite) {
stats.skipped++;
continue;
stats.skipped++
continue
}
}
}
// 使用 hset 存储到哈希表
const pipeline = redis.client.pipeline();
const pipeline = redis.client.pipeline()
for (const [field, value] of Object.entries(account)) {
// 如果是对象,需要序列化
if (field === 'claudeAiOauth' && typeof value === 'object') {
pipeline.hset(`claude_account:${account.id}`, field, JSON.stringify(value));
pipeline.hset(`claude_account:${account.id}`, field, JSON.stringify(value))
} else {
pipeline.hset(`claude_account:${account.id}`, field, value);
pipeline.hset(`claude_account:${account.id}`, field, value)
}
}
await pipeline.exec();
logger.success(`✅ Imported Claude account: ${account.name} (${account.id})`);
stats.imported++;
await pipeline.exec()
logger.success(`✅ Imported Claude account: ${account.name} (${account.id})`)
stats.imported++
} catch (error) {
logger.error(`❌ Failed to import Claude account ${account.id}:`, error.message);
stats.errors++;
logger.error(`❌ Failed to import Claude account ${account.id}:`, error.message)
stats.errors++
}
}
}
// 导入 Gemini 账户
if (importData.data.geminiAccounts) {
logger.info('\n📥 Importing Gemini accounts...');
for (const account of importData.data.geminiAccounts) {
if (importDataObj.data.geminiAccounts) {
logger.info('\n📥 Importing Gemini accounts...')
for (const account of importDataObj.data.geminiAccounts) {
try {
const exists = await redis.client.exists(`gemini_account:${account.id}`);
const exists = await redis.client.exists(`gemini_account:${account.id}`)
if (exists && !forceOverwrite) {
if (skipConflicts) {
logger.warn(`⏭️ Skipped existing Gemini account: ${account.name} (${account.id})`);
stats.skipped++;
continue;
logger.warn(`⏭️ Skipped existing Gemini account: ${account.name} (${account.id})`)
stats.skipped++
continue
} else {
const overwrite = await askConfirmation(`Gemini account "${account.name}" (${account.id}) exists. Overwrite?`);
const overwrite = await askConfirmation(
`Gemini account "${account.name}" (${account.id}) exists. Overwrite?`
)
if (!overwrite) {
stats.skipped++;
continue;
stats.skipped++
continue
}
}
}
// 使用 hset 存储到哈希表
const pipeline = redis.client.pipeline();
const pipeline = redis.client.pipeline()
for (const [field, value] of Object.entries(account)) {
pipeline.hset(`gemini_account:${account.id}`, field, value);
pipeline.hset(`gemini_account:${account.id}`, field, value)
}
await pipeline.exec();
logger.success(`✅ Imported Gemini account: ${account.name} (${account.id})`);
stats.imported++;
await pipeline.exec()
logger.success(`✅ Imported Gemini account: ${account.name} (${account.id})`)
stats.imported++
} catch (error) {
logger.error(`❌ Failed to import Gemini account ${account.id}:`, error.message);
stats.errors++;
logger.error(`❌ Failed to import Gemini account ${account.id}:`, error.message)
stats.errors++
}
}
}
// 显示导入结果
console.log('\n' + '='.repeat(60));
console.log('✅ Import Complete!');
console.log('='.repeat(60));
console.log(`Successfully imported: ${stats.imported}`);
console.log(`Skipped: ${stats.skipped}`);
console.log(`Errors: ${stats.errors}`);
console.log('='.repeat(60));
console.log(`\n${'='.repeat(60)}`)
console.log('✅ Import Complete!')
console.log('='.repeat(60))
console.log(`Successfully imported: ${stats.imported}`)
console.log(`Skipped: ${stats.skipped}`)
console.log(`Errors: ${stats.errors}`)
console.log('='.repeat(60))
} catch (error) {
logger.error('💥 Import failed:', error);
process.exit(1);
logger.error('💥 Import failed:', error)
process.exit(1)
} finally {
await redis.disconnect();
rl.close();
await redis.disconnect()
rl.close()
}
}
@@ -484,34 +492,34 @@ Examples:
# Export specific data types
node scripts/data-transfer.js export --types=apikeys,accounts --output=prod-data.json
`);
`)
}
// 主函数
async function main() {
if (!command || command === '--help' || command === 'help') {
showHelp();
process.exit(0);
showHelp()
process.exit(0)
}
switch (command) {
case 'export':
await exportData();
break;
await exportData()
break
case 'import':
await importData();
break;
await importData()
break
default:
logger.error(`❌ Unknown command: ${command}`);
showHelp();
process.exit(1);
logger.error(`❌ Unknown command: ${command}`)
showHelp()
process.exit(1)
}
}
// 运行
main().catch(error => {
logger.error('💥 Unexpected error:', error);
process.exit(1);
});
main().catch((error) => {
logger.error('💥 Unexpected error:', error)
process.exit(1)
})

View File

@@ -5,19 +5,19 @@
* 用于查看 Redis 中存储的所有键和数据结构
*/
const redis = require('../src/models/redis');
const logger = require('../src/utils/logger');
const redis = require('../src/models/redis')
const logger = require('../src/utils/logger')
async function debugRedisKeys() {
try {
logger.info('🔄 Connecting to Redis...');
await redis.connect();
logger.success('✅ Connected to Redis');
logger.info('🔄 Connecting to Redis...')
await redis.connect()
logger.success('✅ Connected to Redis')
// 获取所有键
const allKeys = await redis.client.keys('*');
logger.info(`\n📊 Total keys in Redis: ${allKeys.length}\n`);
const allKeys = await redis.client.keys('*')
logger.info(`\n📊 Total keys in Redis: ${allKeys.length}\n`)
// 按类型分组
const keysByType = {
apiKeys: [],
@@ -27,97 +27,100 @@ async function debugRedisKeys() {
sessions: [],
usage: [],
other: []
};
}
// 分类键
for (const key of allKeys) {
if (key.startsWith('apikey:')) {
keysByType.apiKeys.push(key);
keysByType.apiKeys.push(key)
} else if (key.startsWith('claude_account:')) {
keysByType.claudeAccounts.push(key);
keysByType.claudeAccounts.push(key)
} else if (key.startsWith('gemini_account:')) {
keysByType.geminiAccounts.push(key);
keysByType.geminiAccounts.push(key)
} else if (key.startsWith('admin:') || key.startsWith('admin_username:')) {
keysByType.admins.push(key);
keysByType.admins.push(key)
} else if (key.startsWith('session:')) {
keysByType.sessions.push(key);
} else if (key.includes('usage') || key.includes('rate_limit') || key.includes('concurrency')) {
keysByType.usage.push(key);
keysByType.sessions.push(key)
} else if (
key.includes('usage') ||
key.includes('rate_limit') ||
key.includes('concurrency')
) {
keysByType.usage.push(key)
} else {
keysByType.other.push(key);
keysByType.other.push(key)
}
}
// 显示分类结果
console.log('='.repeat(60));
console.log('📂 Keys by Category:');
console.log('='.repeat(60));
console.log(`API Keys: ${keysByType.apiKeys.length}`);
console.log(`Claude Accounts: ${keysByType.claudeAccounts.length}`);
console.log(`Gemini Accounts: ${keysByType.geminiAccounts.length}`);
console.log(`Admins: ${keysByType.admins.length}`);
console.log(`Sessions: ${keysByType.sessions.length}`);
console.log(`Usage/Rate Limit: ${keysByType.usage.length}`);
console.log(`Other: ${keysByType.other.length}`);
console.log('='.repeat(60));
console.log('='.repeat(60))
console.log('📂 Keys by Category:')
console.log('='.repeat(60))
console.log(`API Keys: ${keysByType.apiKeys.length}`)
console.log(`Claude Accounts: ${keysByType.claudeAccounts.length}`)
console.log(`Gemini Accounts: ${keysByType.geminiAccounts.length}`)
console.log(`Admins: ${keysByType.admins.length}`)
console.log(`Sessions: ${keysByType.sessions.length}`)
console.log(`Usage/Rate Limit: ${keysByType.usage.length}`)
console.log(`Other: ${keysByType.other.length}`)
console.log('='.repeat(60))
// 详细显示每个类别的键
if (keysByType.apiKeys.length > 0) {
console.log('\n🔑 API Keys:');
console.log('\n🔑 API Keys:')
for (const key of keysByType.apiKeys.slice(0, 5)) {
console.log(` - ${key}`);
console.log(` - ${key}`)
}
if (keysByType.apiKeys.length > 5) {
console.log(` ... and ${keysByType.apiKeys.length - 5} more`);
console.log(` ... and ${keysByType.apiKeys.length - 5} more`)
}
}
if (keysByType.claudeAccounts.length > 0) {
console.log('\n🤖 Claude Accounts:');
console.log('\n🤖 Claude Accounts:')
for (const key of keysByType.claudeAccounts) {
console.log(` - ${key}`);
console.log(` - ${key}`)
}
}
if (keysByType.geminiAccounts.length > 0) {
console.log('\n💎 Gemini Accounts:');
console.log('\n💎 Gemini Accounts:')
for (const key of keysByType.geminiAccounts) {
console.log(` - ${key}`);
console.log(` - ${key}`)
}
}
if (keysByType.other.length > 0) {
console.log('\n❓ Other Keys:');
console.log('\n❓ Other Keys:')
for (const key of keysByType.other.slice(0, 10)) {
console.log(` - ${key}`);
console.log(` - ${key}`)
}
if (keysByType.other.length > 10) {
console.log(` ... and ${keysByType.other.length - 10} more`);
console.log(` ... and ${keysByType.other.length - 10} more`)
}
}
// 检查数据类型
console.log('\n' + '='.repeat(60));
console.log('🔍 Checking Data Types:');
console.log('='.repeat(60));
console.log(`\n${'='.repeat(60)}`)
console.log('🔍 Checking Data Types:')
console.log('='.repeat(60))
// 随机检查几个键的类型
const sampleKeys = allKeys.slice(0, Math.min(10, allKeys.length));
const sampleKeys = allKeys.slice(0, Math.min(10, allKeys.length))
for (const key of sampleKeys) {
const type = await redis.client.type(key);
console.log(`${key} => ${type}`);
const type = await redis.client.type(key)
console.log(`${key} => ${type}`)
}
} catch (error) {
logger.error('💥 Debug failed:', error);
logger.error('💥 Debug failed:', error)
} finally {
await redis.disconnect();
logger.info('👋 Disconnected from Redis');
await redis.disconnect()
logger.info('👋 Disconnected from Redis')
}
}
// 运行调试
debugRedisKeys().catch(error => {
logger.error('💥 Unexpected error:', error);
process.exit(1);
});
debugRedisKeys().catch((error) => {
logger.error('💥 Unexpected error:', error)
process.exit(1)
})

View File

@@ -5,28 +5,25 @@
* 降级到支持 CommonJS 的版本
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process')
console.log('🔧 修复 inquirer ESM 兼容性问题...\n');
console.log('🔧 修复 inquirer ESM 兼容性问题...\n')
try {
// 卸载当前版本
console.log('📦 卸载当前 inquirer 版本...');
execSync('npm uninstall inquirer', { stdio: 'inherit' });
console.log('📦 卸载当前 inquirer 版本...')
execSync('npm uninstall inquirer', { stdio: 'inherit' })
// 安装兼容 CommonJS 的版本 (8.x 是最后支持 CommonJS 的主要版本)
console.log('\n📦 安装兼容版本 inquirer@8.2.6...');
execSync('npm install inquirer@8.2.6', { stdio: 'inherit' });
console.log('\n✅ 修复完成!');
console.log('\n现在可以正常使用 CLI 工具了:');
console.log(' npm run cli admin');
console.log(' npm run cli keys');
console.log(' npm run cli status');
console.log('\n📦 安装兼容版本 inquirer@8.2.6...')
execSync('npm install inquirer@8.2.6', { stdio: 'inherit' })
console.log('\n✅ 修复完成!')
console.log('\n现在可以正常使用 CLI 工具了:')
console.log(' npm run cli admin')
console.log(' npm run cli keys')
console.log(' npm run cli status')
} catch (error) {
console.error('❌ 修复失败:', error.message);
process.exit(1);
}
console.error('❌ 修复失败:', error.message)
process.exit(1)
}

View File

@@ -2,226 +2,226 @@
/**
* 数据迁移脚本:修复历史使用统计数据
*
*
* 功能:
* 1. 统一 totalTokens 和 allTokens 字段
* 2. 确保 allTokens 包含所有类型的 tokens
* 3. 修复历史数据的不一致性
*
*
* 使用方法:
* node scripts/fix-usage-stats.js [--dry-run]
*/
require('dotenv').config();
const redis = require('../src/models/redis');
const logger = require('../src/utils/logger');
require('dotenv').config()
const redis = require('../src/models/redis')
const logger = require('../src/utils/logger')
// 解析命令行参数
const args = process.argv.slice(2);
const isDryRun = args.includes('--dry-run');
const args = process.argv.slice(2)
const isDryRun = args.includes('--dry-run')
async function fixUsageStats() {
try {
logger.info('🔧 开始修复使用统计数据...');
logger.info('🔧 开始修复使用统计数据...')
if (isDryRun) {
logger.info('📝 DRY RUN 模式 - 不会实际修改数据');
logger.info('📝 DRY RUN 模式 - 不会实际修改数据')
}
// 连接到 Redis
await redis.connect();
logger.success('✅ 已连接到 Redis');
const client = redis.getClientSafe();
await redis.connect()
logger.success('✅ 已连接到 Redis')
const client = redis.getClientSafe()
// 统计信息
let stats = {
const stats = {
totalKeys: 0,
fixedTotalKeys: 0,
fixedDailyKeys: 0,
fixedMonthlyKeys: 0,
fixedModelKeys: 0,
errors: 0
};
}
// 1. 修复 API Key 级别的总统计
logger.info('\n📊 修复 API Key 总统计数据...');
const apiKeyPattern = 'apikey:*';
const apiKeys = await client.keys(apiKeyPattern);
stats.totalKeys = apiKeys.length;
logger.info('\n📊 修复 API Key 总统计数据...')
const apiKeyPattern = 'apikey:*'
const apiKeys = await client.keys(apiKeyPattern)
stats.totalKeys = apiKeys.length
for (const apiKeyKey of apiKeys) {
const keyId = apiKeyKey.replace('apikey:', '');
const usageKey = `usage:${keyId}`;
const keyId = apiKeyKey.replace('apikey:', '')
const usageKey = `usage:${keyId}`
try {
const usageData = await client.hgetall(usageKey);
const usageData = await client.hgetall(usageKey)
if (usageData && Object.keys(usageData).length > 0) {
const inputTokens = parseInt(usageData.totalInputTokens) || 0;
const outputTokens = parseInt(usageData.totalOutputTokens) || 0;
const cacheCreateTokens = parseInt(usageData.totalCacheCreateTokens) || 0;
const cacheReadTokens = parseInt(usageData.totalCacheReadTokens) || 0;
const currentAllTokens = parseInt(usageData.totalAllTokens) || 0;
const inputTokens = parseInt(usageData.totalInputTokens) || 0
const outputTokens = parseInt(usageData.totalOutputTokens) || 0
const cacheCreateTokens = parseInt(usageData.totalCacheCreateTokens) || 0
const cacheReadTokens = parseInt(usageData.totalCacheReadTokens) || 0
const currentAllTokens = parseInt(usageData.totalAllTokens) || 0
// 计算正确的 allTokens
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) {
logger.info(` 修复 ${keyId}: ${currentAllTokens} -> ${correctAllTokens}`);
logger.info(` 修复 ${keyId}: ${currentAllTokens} -> ${correctAllTokens}`)
if (!isDryRun) {
await client.hset(usageKey, 'totalAllTokens', correctAllTokens);
await client.hset(usageKey, 'totalAllTokens', correctAllTokens)
}
stats.fixedTotalKeys++;
stats.fixedTotalKeys++
}
}
} catch (error) {
logger.error(` 错误处理 ${keyId}: ${error.message}`);
stats.errors++;
logger.error(` 错误处理 ${keyId}: ${error.message}`)
stats.errors++
}
}
// 2. 修复每日统计数据
logger.info('\n📅 修复每日统计数据...');
const dailyPattern = 'usage:daily:*';
const dailyKeys = await client.keys(dailyPattern);
logger.info('\n📅 修复每日统计数据...')
const dailyPattern = 'usage:daily:*'
const dailyKeys = await client.keys(dailyPattern)
for (const dailyKey of dailyKeys) {
try {
const data = await client.hgetall(dailyKey);
const data = await client.hgetall(dailyKey)
if (data && Object.keys(data).length > 0) {
const inputTokens = parseInt(data.inputTokens) || 0;
const outputTokens = parseInt(data.outputTokens) || 0;
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
const currentAllTokens = parseInt(data.allTokens) || 0;
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
const inputTokens = parseInt(data.inputTokens) || 0
const outputTokens = parseInt(data.outputTokens) || 0
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0
const currentAllTokens = parseInt(data.allTokens) || 0
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) {
if (!isDryRun) {
await client.hset(dailyKey, 'allTokens', correctAllTokens);
await client.hset(dailyKey, 'allTokens', correctAllTokens)
}
stats.fixedDailyKeys++;
stats.fixedDailyKeys++
}
}
} catch (error) {
logger.error(` 错误处理 ${dailyKey}: ${error.message}`);
stats.errors++;
logger.error(` 错误处理 ${dailyKey}: ${error.message}`)
stats.errors++
}
}
// 3. 修复每月统计数据
logger.info('\n📆 修复每月统计数据...');
const monthlyPattern = 'usage:monthly:*';
const monthlyKeys = await client.keys(monthlyPattern);
logger.info('\n📆 修复每月统计数据...')
const monthlyPattern = 'usage:monthly:*'
const monthlyKeys = await client.keys(monthlyPattern)
for (const monthlyKey of monthlyKeys) {
try {
const data = await client.hgetall(monthlyKey);
const data = await client.hgetall(monthlyKey)
if (data && Object.keys(data).length > 0) {
const inputTokens = parseInt(data.inputTokens) || 0;
const outputTokens = parseInt(data.outputTokens) || 0;
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
const currentAllTokens = parseInt(data.allTokens) || 0;
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
const inputTokens = parseInt(data.inputTokens) || 0
const outputTokens = parseInt(data.outputTokens) || 0
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0
const currentAllTokens = parseInt(data.allTokens) || 0
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) {
if (!isDryRun) {
await client.hset(monthlyKey, 'allTokens', correctAllTokens);
await client.hset(monthlyKey, 'allTokens', correctAllTokens)
}
stats.fixedMonthlyKeys++;
stats.fixedMonthlyKeys++
}
}
} catch (error) {
logger.error(` 错误处理 ${monthlyKey}: ${error.message}`);
stats.errors++;
logger.error(` 错误处理 ${monthlyKey}: ${error.message}`)
stats.errors++
}
}
// 4. 修复模型级别的统计数据
logger.info('\n🤖 修复模型级别统计数据...');
logger.info('\n🤖 修复模型级别统计数据...')
const modelPatterns = [
'usage:model:daily:*',
'usage:model:monthly:*',
'usage:*:model:daily:*',
'usage:*:model:monthly:*'
];
]
for (const pattern of modelPatterns) {
const modelKeys = await client.keys(pattern);
const modelKeys = await client.keys(pattern)
for (const modelKey of modelKeys) {
try {
const data = await client.hgetall(modelKey);
const data = await client.hgetall(modelKey)
if (data && Object.keys(data).length > 0) {
const inputTokens = parseInt(data.inputTokens) || 0;
const outputTokens = parseInt(data.outputTokens) || 0;
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
const currentAllTokens = parseInt(data.allTokens) || 0;
const correctAllTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
const inputTokens = parseInt(data.inputTokens) || 0
const outputTokens = parseInt(data.outputTokens) || 0
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0
const currentAllTokens = parseInt(data.allTokens) || 0
const correctAllTokens =
inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
if (currentAllTokens !== correctAllTokens && correctAllTokens > 0) {
if (!isDryRun) {
await client.hset(modelKey, 'allTokens', correctAllTokens);
await client.hset(modelKey, 'allTokens', correctAllTokens)
}
stats.fixedModelKeys++;
stats.fixedModelKeys++
}
}
} catch (error) {
logger.error(` 错误处理 ${modelKey}: ${error.message}`);
stats.errors++;
logger.error(` 错误处理 ${modelKey}: ${error.message}`)
stats.errors++
}
}
}
// 5. 验证修复结果
if (!isDryRun) {
logger.info('\n✅ 验证修复结果...');
logger.info('\n✅ 验证修复结果...')
// 随机抽样验证
const sampleSize = Math.min(5, apiKeys.length);
const sampleSize = Math.min(5, apiKeys.length)
for (let i = 0; i < sampleSize; i++) {
const randomIndex = Math.floor(Math.random() * apiKeys.length);
const keyId = apiKeys[randomIndex].replace('apikey:', '');
const usage = await redis.getUsageStats(keyId);
logger.info(` 样本 ${keyId}:`);
logger.info(` Total tokens: ${usage.total.tokens}`);
logger.info(` All tokens: ${usage.total.allTokens}`);
logger.info(` 一致性: ${usage.total.tokens === usage.total.allTokens ? '✅' : '❌'}`);
const randomIndex = Math.floor(Math.random() * apiKeys.length)
const keyId = apiKeys[randomIndex].replace('apikey:', '')
const usage = await redis.getUsageStats(keyId)
logger.info(` 样本 ${keyId}:`)
logger.info(` Total tokens: ${usage.total.tokens}`)
logger.info(` All tokens: ${usage.total.allTokens}`)
logger.info(` 一致性: ${usage.total.tokens === usage.total.allTokens ? '✅' : '❌'}`)
}
}
// 打印统计结果
logger.info('\n📊 修复统计:');
logger.info(` 总 API Keys: ${stats.totalKeys}`);
logger.info(` 修复的总统计: ${stats.fixedTotalKeys}`);
logger.info(` 修复的日统计: ${stats.fixedDailyKeys}`);
logger.info(` 修复的月统计: ${stats.fixedMonthlyKeys}`);
logger.info(` 修复的模型统计: ${stats.fixedModelKeys}`);
logger.info(` 错误数: ${stats.errors}`);
logger.info('\n📊 修复统计:')
logger.info(` 总 API Keys: ${stats.totalKeys}`)
logger.info(` 修复的总统计: ${stats.fixedTotalKeys}`)
logger.info(` 修复的日统计: ${stats.fixedDailyKeys}`)
logger.info(` 修复的月统计: ${stats.fixedMonthlyKeys}`)
logger.info(` 修复的模型统计: ${stats.fixedModelKeys}`)
logger.info(` 错误数: ${stats.errors}`)
if (isDryRun) {
logger.info('\n💡 这是 DRY RUN - 没有实际修改数据');
logger.info(' 运行不带 --dry-run 参数来实际执行修复');
logger.info('\n💡 这是 DRY RUN - 没有实际修改数据')
logger.info(' 运行不带 --dry-run 参数来实际执行修复')
} else {
logger.success('\n✅ 数据修复完成!');
logger.success('\n✅ 数据修复完成!')
}
} catch (error) {
logger.error('❌ 修复过程出错:', error);
process.exit(1);
logger.error('❌ 修复过程出错:', error)
process.exit(1)
} finally {
await redis.disconnect();
await redis.disconnect()
}
}
// 执行修复
fixUsageStats().catch(error => {
logger.error('❌ 未处理的错误:', error);
process.exit(1);
});
fixUsageStats().catch((error) => {
logger.error('❌ 未处理的错误:', error)
process.exit(1)
})

View File

@@ -3,20 +3,20 @@
/**
* 历史数据生成脚本
* 用于测试不同时间范围的Token统计功能
*
*
* 使用方法:
* node scripts/generate-test-data.js [--clean]
*
*
* 选项:
* --clean: 清除所有测试数据
*/
const redis = require('../src/models/redis');
const logger = require('../src/utils/logger');
const redis = require('../src/models/redis')
const logger = require('../src/utils/logger')
// 解析命令行参数
const args = process.argv.slice(2);
const shouldClean = args.includes('--clean');
const args = process.argv.slice(2)
const shouldClean = args.includes('--clean')
// 模拟的模型列表
const models = [
@@ -24,41 +24,41 @@ const models = [
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229'
];
]
// 生成指定日期的数据
async function generateDataForDate(apiKeyId, date, dayOffset) {
const client = redis.getClientSafe();
const dateStr = date.toISOString().split('T')[0];
const month = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
const client = redis.getClientSafe()
const dateStr = date.toISOString().split('T')[0]
const month = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
// 根据日期偏移量调整数据量(越近的日期数据越多)
const requestCount = Math.max(5, 20 - dayOffset * 2); // 5-20个请求
logger.info(`📊 Generating ${requestCount} requests for ${dateStr}`);
const requestCount = Math.max(5, 20 - dayOffset * 2) // 5-20个请求
logger.info(`📊 Generating ${requestCount} requests for ${dateStr}`)
for (let i = 0; i < requestCount; i++) {
// 随机选择模型
const model = models[Math.floor(Math.random() * models.length)];
const model = models[Math.floor(Math.random() * models.length)]
// 生成随机Token数据
const inputTokens = Math.floor(Math.random() * 2000) + 500; // 500-2500
const outputTokens = Math.floor(Math.random() * 3000) + 1000; // 1000-4000
const cacheCreateTokens = Math.random() > 0.7 ? Math.floor(Math.random() * 1000) : 0; // 30%概率有缓存创建
const cacheReadTokens = Math.random() > 0.5 ? Math.floor(Math.random() * 500) : 0; // 50%概率有缓存读取
const coreTokens = inputTokens + outputTokens;
const allTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
const inputTokens = Math.floor(Math.random() * 2000) + 500 // 500-2500
const outputTokens = Math.floor(Math.random() * 3000) + 1000 // 1000-4000
const cacheCreateTokens = Math.random() > 0.7 ? Math.floor(Math.random() * 1000) : 0 // 30%概率有缓存创建
const cacheReadTokens = Math.random() > 0.5 ? Math.floor(Math.random() * 500) : 0 // 50%概率有缓存读取
const coreTokens = inputTokens + outputTokens
const allTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
// 更新各种统计键
const totalKey = `usage:${apiKeyId}`;
const dailyKey = `usage:daily:${apiKeyId}:${dateStr}`;
const monthlyKey = `usage:monthly:${apiKeyId}:${month}`;
const modelDailyKey = `usage:model:daily:${model}:${dateStr}`;
const modelMonthlyKey = `usage:model:monthly:${model}:${month}`;
const keyModelDailyKey = `usage:${apiKeyId}:model:daily:${model}:${dateStr}`;
const keyModelMonthlyKey = `usage:${apiKeyId}:model:monthly:${model}:${month}`;
const totalKey = `usage:${apiKeyId}`
const dailyKey = `usage:daily:${apiKeyId}:${dateStr}`
const monthlyKey = `usage:monthly:${apiKeyId}:${month}`
const modelDailyKey = `usage:model:daily:${model}:${dateStr}`
const modelMonthlyKey = `usage:model:monthly:${model}:${month}`
const keyModelDailyKey = `usage:${apiKeyId}:model:daily:${model}:${dateStr}`
const keyModelMonthlyKey = `usage:${apiKeyId}:model:monthly:${model}:${month}`
await Promise.all([
// 总计数据
client.hincrby(totalKey, 'totalTokens', coreTokens),
@@ -68,7 +68,7 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(totalKey, 'totalCacheReadTokens', cacheReadTokens),
client.hincrby(totalKey, 'totalAllTokens', allTokens),
client.hincrby(totalKey, 'totalRequests', 1),
// 每日统计
client.hincrby(dailyKey, 'tokens', coreTokens),
client.hincrby(dailyKey, 'inputTokens', inputTokens),
@@ -77,7 +77,7 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(dailyKey, 'cacheReadTokens', cacheReadTokens),
client.hincrby(dailyKey, 'allTokens', allTokens),
client.hincrby(dailyKey, 'requests', 1),
// 每月统计
client.hincrby(monthlyKey, 'tokens', coreTokens),
client.hincrby(monthlyKey, 'inputTokens', inputTokens),
@@ -86,7 +86,7 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(monthlyKey, 'cacheReadTokens', cacheReadTokens),
client.hincrby(monthlyKey, 'allTokens', allTokens),
client.hincrby(monthlyKey, 'requests', 1),
// 模型统计 - 每日
client.hincrby(modelDailyKey, 'totalInputTokens', inputTokens),
client.hincrby(modelDailyKey, 'totalOutputTokens', outputTokens),
@@ -94,7 +94,7 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(modelDailyKey, 'totalCacheReadTokens', cacheReadTokens),
client.hincrby(modelDailyKey, 'totalAllTokens', allTokens),
client.hincrby(modelDailyKey, 'requests', 1),
// 模型统计 - 每月
client.hincrby(modelMonthlyKey, 'totalInputTokens', inputTokens),
client.hincrby(modelMonthlyKey, 'totalOutputTokens', outputTokens),
@@ -102,7 +102,7 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(modelMonthlyKey, 'totalCacheReadTokens', cacheReadTokens),
client.hincrby(modelMonthlyKey, 'totalAllTokens', allTokens),
client.hincrby(modelMonthlyKey, 'requests', 1),
// API Key级别的模型统计 - 每日
// 同时存储带total前缀和不带前缀的字段保持兼容性
client.hincrby(keyModelDailyKey, 'inputTokens', inputTokens),
@@ -116,7 +116,7 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(keyModelDailyKey, 'totalCacheReadTokens', cacheReadTokens),
client.hincrby(keyModelDailyKey, 'totalAllTokens', allTokens),
client.hincrby(keyModelDailyKey, 'requests', 1),
// API Key级别的模型统计 - 每月
client.hincrby(keyModelMonthlyKey, 'inputTokens', inputTokens),
client.hincrby(keyModelMonthlyKey, 'outputTokens', outputTokens),
@@ -128,27 +128,27 @@ async function generateDataForDate(apiKeyId, date, dayOffset) {
client.hincrby(keyModelMonthlyKey, 'totalCacheCreateTokens', cacheCreateTokens),
client.hincrby(keyModelMonthlyKey, 'totalCacheReadTokens', cacheReadTokens),
client.hincrby(keyModelMonthlyKey, 'totalAllTokens', allTokens),
client.hincrby(keyModelMonthlyKey, 'requests', 1),
]);
client.hincrby(keyModelMonthlyKey, 'requests', 1)
])
}
}
// 清除测试数据
async function cleanTestData() {
const client = redis.getClientSafe();
const apiKeyService = require('../src/services/apiKeyService');
logger.info('🧹 Cleaning test data...');
const client = redis.getClientSafe()
const apiKeyService = require('../src/services/apiKeyService')
logger.info('🧹 Cleaning test data...')
// 获取所有API Keys
const allKeys = await apiKeyService.getAllApiKeys();
const allKeys = await apiKeyService.getAllApiKeys()
// 找出所有测试 API Keys
const testKeys = allKeys.filter(key => key.name && key.name.startsWith('Test API Key'));
const testKeys = allKeys.filter((key) => key.name && key.name.startsWith('Test API Key'))
for (const testKey of testKeys) {
const apiKeyId = testKey.id;
const apiKeyId = testKey.id
// 获取所有相关的键
const patterns = [
`usage:${apiKeyId}`,
@@ -156,32 +156,29 @@ async function cleanTestData() {
`usage:monthly:${apiKeyId}:*`,
`usage:${apiKeyId}:model:daily:*`,
`usage:${apiKeyId}:model:monthly:*`
];
]
for (const pattern of patterns) {
const keys = await client.keys(pattern);
const keys = await client.keys(pattern)
if (keys.length > 0) {
await client.del(...keys);
logger.info(`🗑️ Deleted ${keys.length} keys matching pattern: ${pattern}`);
await client.del(...keys)
logger.info(`🗑️ Deleted ${keys.length} keys matching pattern: ${pattern}`)
}
}
// 删除 API Key 本身
await apiKeyService.deleteApiKey(apiKeyId);
logger.info(`🗑️ Deleted test API Key: ${testKey.name} (${apiKeyId})`);
await apiKeyService.deleteApiKey(apiKeyId)
logger.info(`🗑️ Deleted test API Key: ${testKey.name} (${apiKeyId})`)
}
// 清除模型统计
const modelPatterns = [
'usage:model:daily:*',
'usage:model:monthly:*'
];
const modelPatterns = ['usage:model:daily:*', 'usage:model:monthly:*']
for (const pattern of modelPatterns) {
const keys = await client.keys(pattern);
const keys = await client.keys(pattern)
if (keys.length > 0) {
await client.del(...keys);
logger.info(`🗑️ Deleted ${keys.length} keys matching pattern: ${pattern}`);
await client.del(...keys)
logger.info(`🗑️ Deleted ${keys.length} keys matching pattern: ${pattern}`)
}
}
}
@@ -189,17 +186,17 @@ async function cleanTestData() {
// 主函数
async function main() {
try {
await redis.connect();
logger.success('✅ Connected to Redis');
await redis.connect()
logger.success('✅ Connected to Redis')
// 创建测试API Keys
const apiKeyService = require('../src/services/apiKeyService');
let testApiKeys = [];
let createdKeys = [];
const apiKeyService = require('../src/services/apiKeyService')
const testApiKeys = []
const createdKeys = []
// 总是创建新的测试 API Keys
logger.info('📝 Creating test API Keys...');
logger.info('📝 Creating test API Keys...')
for (let i = 1; i <= 3; i++) {
const newKey = await apiKeyService.generateApiKey({
name: `Test API Key ${i}`,
@@ -208,77 +205,76 @@ async function main() {
concurrencyLimit: 10,
rateLimitWindow: 60,
rateLimitRequests: 100
});
testApiKeys.push(newKey.id);
createdKeys.push(newKey);
logger.success(`✅ Created test API Key: ${newKey.name} (${newKey.id})`);
logger.info(` 🔑 API Key: ${newKey.apiKey}`);
})
testApiKeys.push(newKey.id)
createdKeys.push(newKey)
logger.success(`✅ Created test API Key: ${newKey.name} (${newKey.id})`)
logger.info(` 🔑 API Key: ${newKey.apiKey}`)
}
if (shouldClean) {
await cleanTestData();
logger.success('✅ Test data cleaned successfully');
return;
await cleanTestData()
logger.success('✅ Test data cleaned successfully')
return
}
// 生成历史数据
const now = new Date();
const now = new Date()
for (const apiKeyId of testApiKeys) {
logger.info(`\n🔄 Generating data for API Key: ${apiKeyId}`);
logger.info(`\n🔄 Generating data for API Key: ${apiKeyId}`)
// 生成过去30天的数据
for (let dayOffset = 0; dayOffset < 30; dayOffset++) {
const date = new Date(now);
date.setDate(date.getDate() - dayOffset);
await generateDataForDate(apiKeyId, date, dayOffset);
const date = new Date(now)
date.setDate(date.getDate() - dayOffset)
await generateDataForDate(apiKeyId, date, dayOffset)
}
logger.success(`✅ Generated 30 days of historical data for API Key: ${apiKeyId}`);
logger.success(`✅ Generated 30 days of historical data for API Key: ${apiKeyId}`)
}
// 显示统计摘要
logger.info('\n📊 Test Data Summary:');
logger.info('='.repeat(60));
logger.info('\n📊 Test Data Summary:')
logger.info('='.repeat(60))
for (const apiKeyId of testApiKeys) {
const totalKey = `usage:${apiKeyId}`;
const totalData = await redis.getClientSafe().hgetall(totalKey);
const totalKey = `usage:${apiKeyId}`
const totalData = await redis.getClientSafe().hgetall(totalKey)
if (totalData && Object.keys(totalData).length > 0) {
logger.info(`\nAPI Key: ${apiKeyId}`);
logger.info(` Total Requests: ${totalData.totalRequests || 0}`);
logger.info(` Total Tokens (Core): ${totalData.totalTokens || 0}`);
logger.info(` Total Tokens (All): ${totalData.totalAllTokens || 0}`);
logger.info(` Input Tokens: ${totalData.totalInputTokens || 0}`);
logger.info(` Output Tokens: ${totalData.totalOutputTokens || 0}`);
logger.info(` Cache Create Tokens: ${totalData.totalCacheCreateTokens || 0}`);
logger.info(` Cache Read Tokens: ${totalData.totalCacheReadTokens || 0}`);
logger.info(`\nAPI Key: ${apiKeyId}`)
logger.info(` Total Requests: ${totalData.totalRequests || 0}`)
logger.info(` Total Tokens (Core): ${totalData.totalTokens || 0}`)
logger.info(` Total Tokens (All): ${totalData.totalAllTokens || 0}`)
logger.info(` Input Tokens: ${totalData.totalInputTokens || 0}`)
logger.info(` Output Tokens: ${totalData.totalOutputTokens || 0}`)
logger.info(` Cache Create Tokens: ${totalData.totalCacheCreateTokens || 0}`)
logger.info(` Cache Read Tokens: ${totalData.totalCacheReadTokens || 0}`)
}
}
logger.info('\n' + '='.repeat(60));
logger.success('\n✅ Test data generation completed!');
logger.info('\n📋 Created API Keys:');
logger.info(`\n${'='.repeat(60)}`)
logger.success('\n✅ Test data generation completed!')
logger.info('\n📋 Created API Keys:')
for (const key of createdKeys) {
logger.info(`- ${key.name}: ${key.apiKey}`);
logger.info(`- ${key.name}: ${key.apiKey}`)
}
logger.info('\n💡 Tips:');
logger.info('- Check the admin panel to see the different time ranges');
logger.info('- Use --clean flag to remove all test data and API Keys');
logger.info('- The script generates more recent data to simulate real usage patterns');
logger.info('\n💡 Tips:')
logger.info('- Check the admin panel to see the different time ranges')
logger.info('- Use --clean flag to remove all test data and API Keys')
logger.info('- The script generates more recent data to simulate real usage patterns')
} catch (error) {
logger.error('❌ Error:', error);
logger.error('❌ Error:', error)
} finally {
await redis.disconnect();
await redis.disconnect()
}
}
// 运行脚本
main().catch(error => {
logger.error('💥 Unexpected error:', error);
process.exit(1);
});
main().catch((error) => {
logger.error('💥 Unexpected error:', error)
process.exit(1)
})

View File

@@ -5,548 +5,557 @@
* 用于调试、恢复和管理Claude账户的会话窗口
*/
const redis = require('../src/models/redis');
const claudeAccountService = require('../src/services/claudeAccountService');
const logger = require('../src/utils/logger');
const readline = require('readline');
const redis = require('../src/models/redis')
const claudeAccountService = require('../src/services/claudeAccountService')
const readline = require('readline')
// 创建readline接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
})
// 辅助函数:询问用户输入
function askQuestion(question) {
return new Promise((resolve) => {
rl.question(question, resolve);
});
rl.question(question, resolve)
})
}
// 辅助函数:解析时间输入
function parseTimeInput(input) {
const now = new Date();
const now = new Date()
// 如果是 HH:MM 格式
const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/);
const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/)
if (timeMatch) {
const hour = parseInt(timeMatch[1]);
const minute = parseInt(timeMatch[2]);
const hour = parseInt(timeMatch[1])
const minute = parseInt(timeMatch[2])
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
const time = new Date(now);
time.setHours(hour, minute, 0, 0);
return time;
const time = new Date(now)
time.setHours(hour, minute, 0, 0)
return time
}
}
// 如果是相对时间(如 "2小时前"
const relativeMatch = input.match(/^(\d+)(小时|分钟)前$/);
const relativeMatch = input.match(/^(\d+)(小时|分钟)前$/)
if (relativeMatch) {
const amount = parseInt(relativeMatch[1]);
const unit = relativeMatch[2];
const time = new Date(now);
const amount = parseInt(relativeMatch[1])
const unit = relativeMatch[2]
const time = new Date(now)
if (unit === '小时') {
time.setHours(time.getHours() - amount);
time.setHours(time.getHours() - amount)
} else if (unit === '分钟') {
time.setMinutes(time.getMinutes() - amount);
time.setMinutes(time.getMinutes() - amount)
}
return time;
return time
}
// 如果是 ISO 格式或其他日期格式
const parsedDate = new Date(input);
const parsedDate = new Date(input)
if (!isNaN(parsedDate.getTime())) {
return parsedDate;
return parsedDate
}
return null;
return null
}
// 辅助函数:显示可用的时间窗口选项
function showTimeWindowOptions() {
const now = new Date();
console.log('\n⏰ 可用的5小时时间窗口:');
const now = new Date()
console.log('\n⏰ 可用的5小时时间窗口:')
for (let hour = 0; hour < 24; hour += 5) {
const start = hour;
const end = hour + 5;
const startStr = `${String(start).padStart(2, '0')}:00`;
const endStr = `${String(end).padStart(2, '0')}:00`;
const currentHour = now.getHours();
const isActive = currentHour >= start && currentHour < end;
const status = isActive ? ' 🟢 (当前活跃)' : '';
console.log(` ${start/5 + 1}. ${startStr} - ${endStr}${status}`);
const start = hour
const end = hour + 5
const startStr = `${String(start).padStart(2, '0')}:00`
const endStr = `${String(end).padStart(2, '0')}:00`
const currentHour = now.getHours()
const isActive = currentHour >= start && currentHour < end
const status = isActive ? ' 🟢 (当前活跃)' : ''
console.log(` ${start / 5 + 1}. ${startStr} - ${endStr}${status}`)
}
console.log('');
console.log('')
}
const commands = {
// 调试所有账户的会话窗口状态
async debug() {
console.log('🔍 开始调试会话窗口状态...\n');
const accounts = await redis.getAllClaudeAccounts();
console.log(`📊 找到 ${accounts.length} 个Claude账户\n`);
console.log('🔍 开始调试会话窗口状态...\n')
const accounts = await redis.getAllClaudeAccounts()
console.log(`📊 找到 ${accounts.length} 个Claude账户\n`)
const stats = {
total: accounts.length,
hasWindow: 0,
hasLastUsed: 0,
canRecover: 0,
expired: 0
};
}
for (const account of accounts) {
console.log(`🏢 ${account.name} (${account.id})`);
console.log(` 状态: ${account.isActive === 'true' ? '✅ 活跃' : '❌ 禁用'}`);
console.log(`🏢 ${account.name} (${account.id})`)
console.log(` 状态: ${account.isActive === 'true' ? '✅ 活跃' : '❌ 禁用'}`)
if (account.sessionWindowStart && account.sessionWindowEnd) {
stats.hasWindow++;
const windowStart = new Date(account.sessionWindowStart);
const windowEnd = new Date(account.sessionWindowEnd);
const now = new Date();
const isActive = now < windowEnd;
console.log(` 会话窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
console.log(` 窗口状态: ${isActive ? '✅ 活跃' : '❌ 已过期'}`);
stats.hasWindow++
const windowStart = new Date(account.sessionWindowStart)
const windowEnd = new Date(account.sessionWindowEnd)
const now = new Date()
const isActive = now < windowEnd
console.log(` 会话窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`)
console.log(` 窗口状态: ${isActive ? '✅ 活跃' : '❌ 已过期'}`)
// 只有在窗口已过期时才显示可恢复窗口
if (!isActive && account.lastUsedAt) {
const lastUsed = new Date(account.lastUsedAt);
const recoveredWindowStart = claudeAccountService._calculateSessionWindowStart(lastUsed);
const recoveredWindowEnd = claudeAccountService._calculateSessionWindowEnd(recoveredWindowStart);
const lastUsed = new Date(account.lastUsedAt)
const recoveredWindowStart = claudeAccountService._calculateSessionWindowStart(lastUsed)
const recoveredWindowEnd =
claudeAccountService._calculateSessionWindowEnd(recoveredWindowStart)
if (now < recoveredWindowEnd) {
stats.canRecover++;
console.log(` 可恢复窗口: ✅ ${recoveredWindowStart.toLocaleString()} - ${recoveredWindowEnd.toLocaleString()}`);
stats.canRecover++
console.log(
` 可恢复窗口: ✅ ${recoveredWindowStart.toLocaleString()} - ${recoveredWindowEnd.toLocaleString()}`
)
} else {
stats.expired++;
console.log(` 可恢复窗口: ❌ 已过期 (${recoveredWindowStart.toLocaleString()} - ${recoveredWindowEnd.toLocaleString()})`);
stats.expired++
console.log(
` 可恢复窗口: ❌ 已过期 (${recoveredWindowStart.toLocaleString()} - ${recoveredWindowEnd.toLocaleString()})`
)
}
}
} else {
console.log(` 会话窗口: ❌ 无`);
console.log(' 会话窗口: ❌ 无')
// 没有会话窗口时,检查是否有可恢复的窗口
if (account.lastUsedAt) {
const lastUsed = new Date(account.lastUsedAt);
const now = new Date();
const windowStart = claudeAccountService._calculateSessionWindowStart(lastUsed);
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart);
const lastUsed = new Date(account.lastUsedAt)
const now = new Date()
const windowStart = claudeAccountService._calculateSessionWindowStart(lastUsed)
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart)
if (now < windowEnd) {
stats.canRecover++;
console.log(` 可恢复窗口: ✅ ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
stats.canRecover++
console.log(
` 可恢复窗口: ✅ ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`
)
} else {
stats.expired++;
console.log(` 可恢复窗口: ❌ 已过期 (${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()})`);
stats.expired++
console.log(
` 可恢复窗口: ❌ 已过期 (${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()})`
)
}
}
}
if (account.lastUsedAt) {
stats.hasLastUsed++;
const lastUsed = new Date(account.lastUsedAt);
const now = new Date();
const minutesAgo = Math.round((now - lastUsed) / (1000 * 60));
console.log(` 最后使用: ${lastUsed.toLocaleString()} (${minutesAgo}分钟前)`);
stats.hasLastUsed++
const lastUsed = new Date(account.lastUsedAt)
const now = new Date()
const minutesAgo = Math.round((now - lastUsed) / (1000 * 60))
console.log(` 最后使用: ${lastUsed.toLocaleString()} (${minutesAgo}分钟前)`)
} else {
console.log(` 最后使用: ❌ 无记录`);
console.log(' 最后使用: ❌ 无记录')
}
console.log('');
console.log('')
}
console.log('📈 汇总统计:');
console.log(` 总账户数: ${stats.total}`);
console.log(` 有会话窗口: ${stats.hasWindow}`);
console.log(` 有使用记录: ${stats.hasLastUsed}`);
console.log(` 可以恢复: ${stats.canRecover}`);
console.log(` 窗口已过期: ${stats.expired}`);
console.log('📈 汇总统计:')
console.log(` 总账户数: ${stats.total}`)
console.log(` 有会话窗口: ${stats.hasWindow}`)
console.log(` 有使用记录: ${stats.hasLastUsed}`)
console.log(` 可以恢复: ${stats.canRecover}`)
console.log(` 窗口已过期: ${stats.expired}`)
},
// 初始化会话窗口(默认行为)
async init() {
console.log('🔄 初始化会话窗口...\n');
const result = await claudeAccountService.initializeSessionWindows();
console.log('\n📊 初始化结果:');
console.log(` 总账户数: ${result.total}`);
console.log(` 成功初始化: ${result.initialized}`);
console.log(` 已跳过(已有窗口): ${result.skipped}`);
console.log(` 窗口已过期: ${result.expired}`);
console.log(` 无使用数据: ${result.noData}`);
console.log('🔄 初始化会话窗口...\n')
const result = await claudeAccountService.initializeSessionWindows()
console.log('\n📊 初始化结果:')
console.log(` 总账户数: ${result.total}`)
console.log(` 成功初始化: ${result.initialized}`)
console.log(` 已跳过(已有窗口): ${result.skipped}`)
console.log(` 窗口已过期: ${result.expired}`)
console.log(` 无使用数据: ${result.noData}`)
if (result.error) {
console.log(` 错误: ${result.error}`);
console.log(` 错误: ${result.error}`)
}
},
// 强制重新计算所有会话窗口
async force() {
console.log('🔄 强制重新计算所有会话窗口...\n');
const result = await claudeAccountService.initializeSessionWindows(true);
console.log('\n📊 强制重算结果:');
console.log(` 总账户数: ${result.total}`);
console.log(` 成功初始化: ${result.initialized}`);
console.log(` 窗口已过期: ${result.expired}`);
console.log(` 无使用数据: ${result.noData}`);
console.log('🔄 强制重新计算所有会话窗口...\n')
const result = await claudeAccountService.initializeSessionWindows(true)
console.log('\n📊 强制重算结果:')
console.log(` 总账户数: ${result.total}`)
console.log(` 成功初始化: ${result.initialized}`)
console.log(` 窗口已过期: ${result.expired}`)
console.log(` 无使用数据: ${result.noData}`)
if (result.error) {
console.log(` 错误: ${result.error}`);
console.log(` 错误: ${result.error}`)
}
},
// 清除所有会话窗口
async clear() {
console.log('🗑️ 清除所有会话窗口...\n');
const accounts = await redis.getAllClaudeAccounts();
let clearedCount = 0;
console.log('🗑️ 清除所有会话窗口...\n')
const accounts = await redis.getAllClaudeAccounts()
let clearedCount = 0
for (const account of accounts) {
if (account.sessionWindowStart || account.sessionWindowEnd) {
delete account.sessionWindowStart;
delete account.sessionWindowEnd;
delete account.lastRequestTime;
await redis.setClaudeAccount(account.id, account);
clearedCount++;
console.log(`✅ 清除账户 ${account.name} (${account.id}) 的会话窗口`);
delete account.sessionWindowStart
delete account.sessionWindowEnd
delete account.lastRequestTime
await redis.setClaudeAccount(account.id, account)
clearedCount++
console.log(`✅ 清除账户 ${account.name} (${account.id}) 的会话窗口`)
}
}
console.log(`\n📊 清除完成: 共清除 ${clearedCount} 个账户的会话窗口`);
console.log(`\n📊 清除完成: 共清除 ${clearedCount} 个账户的会话窗口`)
},
// 创建测试会话窗口将lastUsedAt设置为当前时间
async test() {
console.log('🧪 创建测试会话窗口...\n');
const accounts = await redis.getAllClaudeAccounts();
console.log('🧪 创建测试会话窗口...\n')
const accounts = await redis.getAllClaudeAccounts()
if (accounts.length === 0) {
console.log('❌ 没有找到Claude账户');
return;
console.log('❌ 没有找到Claude账户')
return
}
const now = new Date();
let updatedCount = 0;
const now = new Date()
let updatedCount = 0
for (const account of accounts) {
if (account.isActive === 'true') {
// 设置为当前时间(模拟刚刚使用)
account.lastUsedAt = now.toISOString();
account.lastUsedAt = now.toISOString()
// 计算新的会话窗口
const windowStart = claudeAccountService._calculateSessionWindowStart(now);
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart);
account.sessionWindowStart = windowStart.toISOString();
account.sessionWindowEnd = windowEnd.toISOString();
account.lastRequestTime = now.toISOString();
await redis.setClaudeAccount(account.id, account);
updatedCount++;
console.log(`✅ 为账户 ${account.name} 创建测试会话窗口:`);
console.log(` 窗口时间: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
console.log(` 最后使用: ${now.toLocaleString()}\n`);
const windowStart = claudeAccountService._calculateSessionWindowStart(now)
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart)
account.sessionWindowStart = windowStart.toISOString()
account.sessionWindowEnd = windowEnd.toISOString()
account.lastRequestTime = now.toISOString()
await redis.setClaudeAccount(account.id, account)
updatedCount++
console.log(`✅ 为账户 ${account.name} 创建测试会话窗口:`)
console.log(` 窗口时间: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`)
console.log(` 最后使用: ${now.toLocaleString()}\n`)
}
}
console.log(`📊 测试完成: 为 ${updatedCount} 个活跃账户创建了测试会话窗口`);
console.log(`📊 测试完成: 为 ${updatedCount} 个活跃账户创建了测试会话窗口`)
},
// 手动设置账户的会话窗口
async set() {
console.log('🔧 手动设置会话窗口...\n');
console.log('🔧 手动设置会话窗口...\n')
// 获取所有账户
const accounts = await redis.getAllClaudeAccounts();
const accounts = await redis.getAllClaudeAccounts()
if (accounts.length === 0) {
console.log('❌ 没有找到Claude账户');
return;
console.log('❌ 没有找到Claude账户')
return
}
// 显示账户列表
console.log('📋 可用的Claude账户:');
console.log('📋 可用的Claude账户:')
accounts.forEach((account, index) => {
const status = account.isActive === 'true' ? '✅' : '❌';
const hasWindow = account.sessionWindowStart ? '🕐' : '';
console.log(` ${index + 1}. ${status} ${hasWindow} ${account.name} (${account.id})`);
});
const status = account.isActive === 'true' ? '✅' : '❌'
const hasWindow = account.sessionWindowStart ? '🕐' : ''
console.log(` ${index + 1}. ${status} ${hasWindow} ${account.name} (${account.id})`)
})
// 让用户选择账户
const accountIndex = await askQuestion('\n请选择账户 (输入编号): ');
const selectedIndex = parseInt(accountIndex) - 1;
const accountIndex = await askQuestion('\n请选择账户 (输入编号): ')
const selectedIndex = parseInt(accountIndex) - 1
if (selectedIndex < 0 || selectedIndex >= accounts.length) {
console.log('❌ 无效的账户编号');
return;
console.log('❌ 无效的账户编号')
return
}
const selectedAccount = accounts[selectedIndex];
console.log(`\n🎯 已选择账户: ${selectedAccount.name}`);
const selectedAccount = accounts[selectedIndex]
console.log(`\n🎯 已选择账户: ${selectedAccount.name}`)
// 显示当前会话窗口状态
if (selectedAccount.sessionWindowStart && selectedAccount.sessionWindowEnd) {
const windowStart = new Date(selectedAccount.sessionWindowStart);
const windowEnd = new Date(selectedAccount.sessionWindowEnd);
const now = new Date();
const isActive = now >= windowStart && now < windowEnd;
console.log(`📊 当前会话窗口:`);
console.log(` 时间: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
console.log(` 状态: ${isActive ? '✅ 活跃' : '❌ 已过期'}`);
const windowStart = new Date(selectedAccount.sessionWindowStart)
const windowEnd = new Date(selectedAccount.sessionWindowEnd)
const now = new Date()
const isActive = now >= windowStart && now < windowEnd
console.log('📊 当前会话窗口:')
console.log(` 时间: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`)
console.log(` 状态: ${isActive ? '✅ 活跃' : '❌ 已过期'}`)
} else {
console.log(`📊 当前会话窗口: ❌ 无`);
console.log('📊 当前会话窗口: ❌ 无')
}
// 显示设置选项
console.log('\n🛠 设置选项:');
console.log(' 1. 使用预设时间窗口');
console.log(' 2. 自定义最后使用时间');
console.log(' 3. 直接设置窗口时间');
console.log(' 4. 清除会话窗口');
const option = await askQuestion('\n请选择设置方式 (1-4): ');
console.log('\n🛠 设置选项:')
console.log(' 1. 使用预设时间窗口')
console.log(' 2. 自定义最后使用时间')
console.log(' 3. 直接设置窗口时间')
console.log(' 4. 清除会话窗口')
const option = await askQuestion('\n请选择设置方式 (1-4): ')
switch (option) {
case '1':
await setPresetWindow(selectedAccount);
break;
await setPresetWindow(selectedAccount)
break
case '2':
await setCustomLastUsed(selectedAccount);
break;
await setCustomLastUsed(selectedAccount)
break
case '3':
await setDirectWindow(selectedAccount);
break;
await setDirectWindow(selectedAccount)
break
case '4':
await clearAccountWindow(selectedAccount);
break;
await clearAccountWindow(selectedAccount)
break
default:
console.log('❌ 无效的选项');
return;
console.log('❌ 无效的选项')
return
}
},
// 显示帮助信息
help() {
console.log('🔧 会话窗口管理脚本\n');
console.log('用法: node scripts/manage-session-windows.js <command>\n');
console.log('命令:');
console.log(' debug - 调试所有账户的会话窗口状态');
console.log(' init - 初始化会话窗口(跳过已有窗口的账户)');
console.log(' force - 强制重新计算所有会话窗口');
console.log(' test - 创建测试会话窗口(设置当前时间为使用时间)');
console.log(' set - 手动设置特定账户的会话窗口 🆕');
console.log(' clear - 清除所有会话窗口');
console.log(' help - 显示此帮助信息\n');
console.log('示例:');
console.log(' node scripts/manage-session-windows.js debug');
console.log(' node scripts/manage-session-windows.js set');
console.log(' node scripts/manage-session-windows.js test');
console.log(' node scripts/manage-session-windows.js force');
console.log('🔧 会话窗口管理脚本\n')
console.log('用法: node scripts/manage-session-windows.js <command>\n')
console.log('命令:')
console.log(' debug - 调试所有账户的会话窗口状态')
console.log(' init - 初始化会话窗口(跳过已有窗口的账户)')
console.log(' force - 强制重新计算所有会话窗口')
console.log(' test - 创建测试会话窗口(设置当前时间为使用时间)')
console.log(' set - 手动设置特定账户的会话窗口 🆕')
console.log(' clear - 清除所有会话窗口')
console.log(' help - 显示此帮助信息\n')
console.log('示例:')
console.log(' node scripts/manage-session-windows.js debug')
console.log(' node scripts/manage-session-windows.js set')
console.log(' node scripts/manage-session-windows.js test')
console.log(' node scripts/manage-session-windows.js force')
}
};
}
// 设置函数实现
// 使用预设时间窗口
async function setPresetWindow(account) {
showTimeWindowOptions();
const windowChoice = await askQuestion('请选择时间窗口 (1-5): ');
const windowIndex = parseInt(windowChoice) - 1;
showTimeWindowOptions()
const windowChoice = await askQuestion('请选择时间窗口 (1-5): ')
const windowIndex = parseInt(windowChoice) - 1
if (windowIndex < 0 || windowIndex >= 5) {
console.log('❌ 无效的窗口选择');
return;
console.log('❌ 无效的窗口选择')
return
}
const now = new Date();
const startHour = windowIndex * 5;
const now = new Date()
const startHour = windowIndex * 5
// 创建窗口开始时间
const windowStart = new Date(now);
windowStart.setHours(startHour, 0, 0, 0);
const windowStart = new Date(now)
windowStart.setHours(startHour, 0, 0, 0)
// 创建窗口结束时间
const windowEnd = new Date(windowStart);
windowEnd.setHours(windowEnd.getHours() + 5);
const windowEnd = new Date(windowStart)
windowEnd.setHours(windowEnd.getHours() + 5)
// 如果选择的窗口已经过期,则设置为明天的同一时间段
if (windowEnd <= now) {
windowStart.setDate(windowStart.getDate() + 1);
windowEnd.setDate(windowEnd.getDate() + 1);
windowStart.setDate(windowStart.getDate() + 1)
windowEnd.setDate(windowEnd.getDate() + 1)
}
// 询问是否要设置为当前时间作为最后使用时间
const setLastUsed = await askQuestion('是否设置当前时间为最后使用时间? (y/N): ');
const setLastUsed = await askQuestion('是否设置当前时间为最后使用时间? (y/N): ')
// 更新账户数据
account.sessionWindowStart = windowStart.toISOString();
account.sessionWindowEnd = windowEnd.toISOString();
account.lastRequestTime = now.toISOString();
account.sessionWindowStart = windowStart.toISOString()
account.sessionWindowEnd = windowEnd.toISOString()
account.lastRequestTime = now.toISOString()
if (setLastUsed.toLowerCase() === 'y' || setLastUsed.toLowerCase() === 'yes') {
account.lastUsedAt = now.toISOString();
account.lastUsedAt = now.toISOString()
}
await redis.setClaudeAccount(account.id, account);
console.log(`\n✅ 已设置会话窗口:`);
console.log(` 账户: ${account.name}`);
console.log(` 窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
console.log(` 状态: ${now >= windowStart && now < windowEnd ? '✅ 活跃' : '⏰ 未来窗口'}`);
await redis.setClaudeAccount(account.id, account)
console.log('\n✅ 已设置会话窗口:')
console.log(` 账户: ${account.name}`)
console.log(` 窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`)
console.log(` 状态: ${now >= windowStart && now < windowEnd ? '✅ 活跃' : '⏰ 未来窗口'}`)
}
// 自定义最后使用时间
async function setCustomLastUsed(account) {
console.log('\n📝 设置最后使用时间:');
console.log('支持的时间格式:');
console.log(' - HH:MM (如: 14:30)');
console.log(' - 相对时间 (如: 2小时前, 30分钟前)');
console.log(' - ISO格式 (如: 2025-07-28T14:30:00)');
const timeInput = await askQuestion('\n请输入最后使用时间: ');
const lastUsedTime = parseTimeInput(timeInput);
console.log('\n📝 设置最后使用时间:')
console.log('支持的时间格式:')
console.log(' - HH:MM (如: 14:30)')
console.log(' - 相对时间 (如: 2小时前, 30分钟前)')
console.log(' - ISO格式 (如: 2025-07-28T14:30:00)')
const timeInput = await askQuestion('\n请输入最后使用时间: ')
const lastUsedTime = parseTimeInput(timeInput)
if (!lastUsedTime) {
console.log('❌ 无效的时间格式');
return;
console.log('❌ 无效的时间格式')
return
}
// 基于最后使用时间计算会话窗口
const windowStart = claudeAccountService._calculateSessionWindowStart(lastUsedTime);
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart);
const windowStart = claudeAccountService._calculateSessionWindowStart(lastUsedTime)
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart)
// 更新账户数据
account.lastUsedAt = lastUsedTime.toISOString();
account.sessionWindowStart = windowStart.toISOString();
account.sessionWindowEnd = windowEnd.toISOString();
account.lastRequestTime = lastUsedTime.toISOString();
await redis.setClaudeAccount(account.id, account);
console.log(`\n✅ 已设置会话窗口:`);
console.log(` 账户: ${account.name}`);
console.log(` 最后使用: ${lastUsedTime.toLocaleString()}`);
console.log(` 窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
const now = new Date();
console.log(` 状态: ${now >= windowStart && now < windowEnd ? '✅ 活跃' : '❌ 已过期'}`);
account.lastUsedAt = lastUsedTime.toISOString()
account.sessionWindowStart = windowStart.toISOString()
account.sessionWindowEnd = windowEnd.toISOString()
account.lastRequestTime = lastUsedTime.toISOString()
await redis.setClaudeAccount(account.id, account)
console.log('\n✅ 已设置会话窗口:')
console.log(` 账户: ${account.name}`)
console.log(` 最后使用: ${lastUsedTime.toLocaleString()}`)
console.log(` 窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`)
const now = new Date()
console.log(` 状态: ${now >= windowStart && now < windowEnd ? '✅ 活跃' : '❌ 已过期'}`)
}
// 直接设置窗口时间
async function setDirectWindow(account) {
console.log('\n⏰ 直接设置窗口时间:');
const startInput = await askQuestion('请输入窗口开始时间 (HH:MM): ');
const startTime = parseTimeInput(startInput);
console.log('\n⏰ 直接设置窗口时间:')
const startInput = await askQuestion('请输入窗口开始时间 (HH:MM): ')
const startTime = parseTimeInput(startInput)
if (!startTime) {
console.log('❌ 无效的开始时间格式');
return;
console.log('❌ 无效的开始时间格式')
return
}
// 自动计算结束时间(开始时间+5小时
const endTime = new Date(startTime);
endTime.setHours(endTime.getHours() + 5);
const endTime = new Date(startTime)
endTime.setHours(endTime.getHours() + 5)
// 如果跨天,询问是否确认
if (endTime.getDate() !== startTime.getDate()) {
const confirm = await askQuestion(`窗口将跨天到次日 ${endTime.getHours()}:00确认吗? (y/N): `);
const confirm = await askQuestion(`窗口将跨天到次日 ${endTime.getHours()}:00确认吗? (y/N): `)
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
console.log('❌ 已取消设置');
return;
console.log('❌ 已取消设置')
return
}
}
const now = new Date();
const now = new Date()
// 更新账户数据
account.sessionWindowStart = startTime.toISOString();
account.sessionWindowEnd = endTime.toISOString();
account.lastRequestTime = now.toISOString();
account.sessionWindowStart = startTime.toISOString()
account.sessionWindowEnd = endTime.toISOString()
account.lastRequestTime = now.toISOString()
// 询问是否更新最后使用时间
const updateLastUsed = await askQuestion('是否将最后使用时间设置为窗口开始时间? (y/N): ');
const updateLastUsed = await askQuestion('是否将最后使用时间设置为窗口开始时间? (y/N): ')
if (updateLastUsed.toLowerCase() === 'y' || updateLastUsed.toLowerCase() === 'yes') {
account.lastUsedAt = startTime.toISOString();
account.lastUsedAt = startTime.toISOString()
}
await redis.setClaudeAccount(account.id, account);
console.log(`\n✅ 已设置会话窗口:`);
console.log(` 账户: ${account.name}`);
console.log(` 窗口: ${startTime.toLocaleString()} - ${endTime.toLocaleString()}`);
console.log(` 状态: ${now >= startTime && now < endTime ? '✅ 活跃' : (now < startTime ? '⏰ 未来窗口' : '❌ 已过期')}`);
await redis.setClaudeAccount(account.id, account)
console.log('\n✅ 已设置会话窗口:')
console.log(` 账户: ${account.name}`)
console.log(` 窗口: ${startTime.toLocaleString()} - ${endTime.toLocaleString()}`)
console.log(
` 状态: ${now >= startTime && now < endTime ? '✅ 活跃' : now < startTime ? '⏰ 未来窗口' : '❌ 已过期'}`
)
}
// 清除账户会话窗口
async function clearAccountWindow(account) {
const confirm = await askQuestion(`确认清除账户 "${account.name}" 的会话窗口吗? (y/N): `);
const confirm = await askQuestion(`确认清除账户 "${account.name}" 的会话窗口吗? (y/N): `)
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
console.log('❌ 已取消操作');
return;
console.log('❌ 已取消操作')
return
}
// 清除会话窗口相关数据
delete account.sessionWindowStart;
delete account.sessionWindowEnd;
delete account.lastRequestTime;
await redis.setClaudeAccount(account.id, account);
console.log(`\n✅ 已清除账户 "${account.name}" 的会话窗口`);
delete account.sessionWindowStart
delete account.sessionWindowEnd
delete account.lastRequestTime
await redis.setClaudeAccount(account.id, account)
console.log(`\n✅ 已清除账户 "${account.name}" 的会话窗口`)
}
async function main() {
const command = process.argv[2] || 'help';
const command = process.argv[2] || 'help'
if (!commands[command]) {
console.error(`❌ 未知命令: ${command}`);
commands.help();
process.exit(1);
console.error(`❌ 未知命令: ${command}`)
commands.help()
process.exit(1)
}
if (command === 'help') {
commands.help();
return;
commands.help()
return
}
try {
// 连接Redis
await redis.connect();
await redis.connect()
// 执行命令
await commands[command]();
await commands[command]()
} catch (error) {
console.error('❌ 执行失败:', error);
process.exit(1);
console.error('❌ 执行失败:', error)
process.exit(1)
} finally {
await redis.disconnect();
rl.close();
await redis.disconnect()
rl.close()
}
}
// 如果直接运行此脚本
if (require.main === module) {
main().then(() => {
console.log('\n🎉 操作完成');
process.exit(0);
});
console.log('\n🎉 操作完成')
process.exit(0)
})
}
module.exports = { commands };
module.exports = { commands }

View File

@@ -1,235 +1,232 @@
#!/usr/bin/env node
const { spawn, exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const process = require('process');
const { spawn, exec } = require('child_process')
const fs = require('fs')
const path = require('path')
const process = require('process')
const PID_FILE = path.join(__dirname, '..', 'claude-relay-service.pid');
const LOG_FILE = path.join(__dirname, '..', 'logs', 'service.log');
const ERROR_LOG_FILE = path.join(__dirname, '..', 'logs', 'service-error.log');
const APP_FILE = path.join(__dirname, '..', 'src', 'app.js');
const PID_FILE = path.join(__dirname, '..', 'claude-relay-service.pid')
const LOG_FILE = path.join(__dirname, '..', 'logs', 'service.log')
const ERROR_LOG_FILE = path.join(__dirname, '..', 'logs', 'service-error.log')
const APP_FILE = path.join(__dirname, '..', 'src', 'app.js')
class ServiceManager {
constructor() {
this.ensureLogDir();
constructor() {
this.ensureLogDir()
}
ensureLogDir() {
const logDir = path.dirname(LOG_FILE)
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
}
getPid() {
try {
if (fs.existsSync(PID_FILE)) {
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim())
return pid
}
} catch (error) {
console.error('读取PID文件失败:', error.message)
}
return null
}
isProcessRunning(pid) {
try {
process.kill(pid, 0)
return true
} catch (error) {
return false
}
}
writePid(pid) {
try {
fs.writeFileSync(PID_FILE, pid.toString())
console.log(`✅ PID ${pid} 已保存到 ${PID_FILE}`)
} catch (error) {
console.error('写入PID文件失败:', error.message)
}
}
removePidFile() {
try {
if (fs.existsSync(PID_FILE)) {
fs.unlinkSync(PID_FILE)
console.log('🗑️ 已清理PID文件')
}
} catch (error) {
console.error('清理PID文件失败:', error.message)
}
}
getStatus() {
const pid = this.getPid()
if (pid && this.isProcessRunning(pid)) {
return { running: true, pid }
}
return { running: false, pid: null }
}
start(daemon = false) {
const status = this.getStatus()
if (status.running) {
console.log(`⚠️ 服务已在运行中 (PID: ${status.pid})`)
return false
}
ensureLogDir() {
const logDir = path.dirname(LOG_FILE);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
}
console.log('🚀 启动 Claude Relay Service...')
getPid() {
try {
if (fs.existsSync(PID_FILE)) {
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
return pid;
}
} catch (error) {
console.error('读取PID文件失败:', error.message);
}
return null;
}
if (daemon) {
// 后台运行模式 - 使用nohup实现真正的后台运行
const { exec: execChild } = require('child_process')
isProcessRunning(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return false;
}
}
const command = `nohup node "${APP_FILE}" > "${LOG_FILE}" 2> "${ERROR_LOG_FILE}" & echo $!`
writePid(pid) {
try {
fs.writeFileSync(PID_FILE, pid.toString());
console.log(`✅ PID ${pid} 已保存到 ${PID_FILE}`);
} catch (error) {
console.error('写入PID文件失败:', error.message);
}
}
removePidFile() {
try {
if (fs.existsSync(PID_FILE)) {
fs.unlinkSync(PID_FILE);
console.log('🗑️ 已清理PID文件');
}
} catch (error) {
console.error('清理PID文件失败:', error.message);
}
}
getStatus() {
const pid = this.getPid();
if (pid && this.isProcessRunning(pid)) {
return { running: true, pid };
}
return { running: false, pid: null };
}
start(daemon = false) {
const status = this.getStatus();
if (status.running) {
console.log(`⚠️ 服务已在运行中 (PID: ${status.pid})`);
return false;
execChild(command, (error, stdout) => {
if (error) {
console.error('❌ 后台启动失败:', error.message)
return
}
console.log('🚀 启动 Claude Relay Service...');
if (daemon) {
// 后台运行模式 - 使用nohup实现真正的后台运行
const { exec } = require('child_process');
const command = `nohup node "${APP_FILE}" > "${LOG_FILE}" 2> "${ERROR_LOG_FILE}" & echo $!`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error('❌ 后台启动失败:', error.message);
return;
}
const pid = parseInt(stdout.trim());
if (pid && !isNaN(pid)) {
this.writePid(pid);
console.log(`🔄 服务已在后台启动 (PID: ${pid})`);
console.log(`📝 日志文件: ${LOG_FILE}`);
console.log(`❌ 错误日志: ${ERROR_LOG_FILE}`);
console.log('✅ 终端现在可以安全关闭');
} else {
console.error('❌ 无法获取进程ID');
}
});
// 给exec一点时间执行
setTimeout(() => {
process.exit(0);
}, 1000);
const pid = parseInt(stdout.trim())
if (pid && !isNaN(pid)) {
this.writePid(pid)
console.log(`🔄 服务已在后台启动 (PID: ${pid})`)
console.log(`📝 日志文件: ${LOG_FILE}`)
console.log(`❌ 错误日志: ${ERROR_LOG_FILE}`)
console.log('✅ 终端现在可以安全关闭')
} else {
// 前台运行模式
const child = spawn('node', [APP_FILE], {
stdio: 'inherit'
});
console.error('❌ 无法获取进程ID')
}
})
console.log(`🔄 服务已启动 (PID: ${child.pid})`);
this.writePid(child.pid);
// 给exec一点时间执行
setTimeout(() => {
process.exit(0)
}, 1000)
} else {
// 前台运行模式
const child = spawn('node', [APP_FILE], {
stdio: 'inherit'
})
// 监听进程退出
child.on('exit', (code, signal) => {
this.removePidFile();
if (code !== 0) {
console.log(`💥 进程退出 (代码: ${code}, 信号: ${signal})`);
}
});
console.log(`🔄 服务已启动 (PID: ${child.pid})`)
child.on('error', (error) => {
console.error('❌ 启动失败:', error.message);
this.removePidFile();
});
this.writePid(child.pid)
// 监听进程退出
child.on('exit', (code, signal) => {
this.removePidFile()
if (code !== 0) {
console.log(`💥 进程退出 (代码: ${code}, 信号: ${signal})`)
}
})
child.on('error', (error) => {
console.error('❌ 启动失败:', error.message)
this.removePidFile()
})
}
return true
}
stop() {
const status = this.getStatus()
if (!status.running) {
console.log('⚠️ 服务未在运行')
this.removePidFile() // 清理可能存在的过期PID文件
return false
}
console.log(`🛑 停止服务 (PID: ${status.pid})...`)
try {
// 优雅关闭先发送SIGTERM
process.kill(status.pid, 'SIGTERM')
// 等待进程退出
let attempts = 0
const maxAttempts = 30 // 30秒超时
const checkExit = setInterval(() => {
attempts++
if (!this.isProcessRunning(status.pid)) {
clearInterval(checkExit)
console.log('✅ 服务已停止')
this.removePidFile()
return
}
return true;
}
stop() {
const status = this.getStatus();
if (!status.running) {
console.log('⚠️ 服务未在运行');
this.removePidFile(); // 清理可能存在的过期PID文件
return false;
if (attempts >= maxAttempts) {
clearInterval(checkExit)
console.log('⚠️ 优雅关闭超时,强制终止进程...')
try {
process.kill(status.pid, 'SIGKILL')
console.log('✅ 服务已强制停止')
} catch (error) {
console.error('❌ 强制停止失败:', error.message)
}
this.removePidFile()
}
}, 1000)
} catch (error) {
console.error('❌ 停止服务失败:', error.message)
this.removePidFile()
return false
}
console.log(`🛑 停止服务 (PID: ${status.pid})...`);
return true
}
try {
// 优雅关闭先发送SIGTERM
process.kill(status.pid, 'SIGTERM');
restart(daemon = false) {
console.log('🔄 重启服务...')
// 等待进程退出
let attempts = 0;
const maxAttempts = 30; // 30秒超时
// 等待停止完成
setTimeout(() => {
this.start(daemon)
}, 2000)
const checkExit = setInterval(() => {
attempts++;
if (!this.isProcessRunning(status.pid)) {
clearInterval(checkExit);
console.log('✅ 服务已停止');
this.removePidFile();
return;
}
return true
}
if (attempts >= maxAttempts) {
clearInterval(checkExit);
console.log('⚠️ 优雅关闭超时,强制终止进程...');
try {
process.kill(status.pid, 'SIGKILL');
console.log('✅ 服务已强制停止');
} catch (error) {
console.error('❌ 强制停止失败:', error.message);
}
this.removePidFile();
}
}, 1000);
status() {
const status = this.getStatus()
if (status.running) {
console.log(`✅ 服务正在运行 (PID: ${status.pid})`)
} catch (error) {
console.error('❌ 停止服务失败:', error.message);
this.removePidFile();
return false;
// 显示进程信息
exec(`ps -p ${status.pid} -o pid,ppid,pcpu,pmem,etime,cmd --no-headers`, (error, stdout) => {
if (!error && stdout.trim()) {
console.log('\n📊 进程信息:')
console.log('PID\tPPID\tCPU%\tMEM%\tTIME\t\tCOMMAND')
console.log(stdout.trim())
}
return true;
})
} else {
console.log('❌ 服务未运行')
}
return status.running
}
restart(daemon = false) {
console.log('🔄 重启服务...');
const stopResult = this.stop();
// 等待停止完成
setTimeout(() => {
this.start(daemon);
}, 2000);
return true;
}
logs(lines = 50) {
console.log(`📖 最近 ${lines} 行日志:\n`)
status() {
const status = this.getStatus();
if (status.running) {
console.log(`✅ 服务正在运行 (PID: ${status.pid})`);
// 显示进程信息
exec(`ps -p ${status.pid} -o pid,ppid,pcpu,pmem,etime,cmd --no-headers`, (error, stdout) => {
if (!error && stdout.trim()) {
console.log('\n📊 进程信息:');
console.log('PID\tPPID\tCPU%\tMEM%\tTIME\t\tCOMMAND');
console.log(stdout.trim());
}
});
} else {
console.log('❌ 服务未运行');
}
return status.running;
}
exec(`tail -n ${lines} ${LOG_FILE}`, (error, stdout) => {
if (error) {
console.error('读取日志失败:', error.message)
return
}
console.log(stdout)
})
}
logs(lines = 50) {
console.log(`📖 最近 ${lines} 行日志:\n`);
exec(`tail -n ${lines} ${LOG_FILE}`, (error, stdout) => {
if (error) {
console.error('读取日志失败:', error.message);
return;
}
console.log(stdout);
});
}
help() {
console.log(`
help() {
console.log(`
🔧 Claude Relay Service 进程管理器
用法: npm run service <command> [options]
@@ -281,55 +278,56 @@ class ServiceManager {
PID文件: ${PID_FILE}
日志文件: ${LOG_FILE}
错误日志: ${ERROR_LOG_FILE}
`);
}
`)
}
}
// 主程序
function main() {
const manager = new ServiceManager();
const args = process.argv.slice(2);
const command = args[0];
const isDaemon = args.includes('-d') || args.includes('--daemon');
const manager = new ServiceManager()
const args = process.argv.slice(2)
const command = args[0]
const isDaemon = args.includes('-d') || args.includes('--daemon')
switch (command) {
case 'start':
case 's':
manager.start(isDaemon);
break;
case 'stop':
case 'halt':
manager.stop();
break;
case 'restart':
case 'r':
manager.restart(isDaemon);
break;
case 'status':
case 'st':
manager.status();
break;
case 'logs':
case 'log':
case 'l':
const lines = parseInt(args[1]) || 50;
manager.logs(lines);
break;
case 'help':
case '--help':
case '-h':
case 'h':
manager.help();
break;
default:
console.log('❌ 未知命令:', command);
manager.help();
process.exit(1);
switch (command) {
case 'start':
case 's':
manager.start(isDaemon)
break
case 'stop':
case 'halt':
manager.stop()
break
case 'restart':
case 'r':
manager.restart(isDaemon)
break
case 'status':
case 'st':
manager.status()
break
case 'logs':
case 'log':
case 'l': {
const lines = parseInt(args[1]) || 50
manager.logs(lines)
break
}
case 'help':
case '--help':
case '-h':
case 'h':
manager.help()
break
default:
console.log('❌ 未知命令:', command)
manager.help()
process.exit(1)
}
}
if (require.main === module) {
main();
main()
}
module.exports = ServiceManager;
module.exports = ServiceManager

View File

@@ -2,58 +2,58 @@
/**
* 数据迁移脚本:为现有 API Key 设置默认有效期
*
*
* 使用方法:
* node scripts/migrate-apikey-expiry.js [--days=30] [--dry-run]
*
*
* 参数:
* --days: 设置默认有效期天数默认30天
* --dry-run: 仅模拟运行,不实际修改数据
*/
const redis = require('../src/models/redis');
const logger = require('../src/utils/logger');
const readline = require('readline');
const redis = require('../src/models/redis')
const logger = require('../src/utils/logger')
const readline = require('readline')
// 解析命令行参数
const args = process.argv.slice(2);
const params = {};
args.forEach(arg => {
const [key, value] = arg.split('=');
params[key.replace('--', '')] = value || true;
});
const args = process.argv.slice(2)
const params = {}
args.forEach((arg) => {
const [key, value] = arg.split('=')
params[key.replace('--', '')] = value || true
})
const DEFAULT_DAYS = params.days ? parseInt(params.days) : 30;
const DRY_RUN = params['dry-run'] === true;
const DEFAULT_DAYS = params.days ? parseInt(params.days) : 30
const DRY_RUN = params['dry-run'] === true
// 创建 readline 接口用于用户确认
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
})
async function askConfirmation(question) {
return new Promise((resolve) => {
rl.question(question + ' (yes/no): ', (answer) => {
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y');
});
});
rl.question(`${question} (yes/no): `, (answer) => {
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y')
})
})
}
async function migrateApiKeys() {
try {
logger.info('🔄 Starting API Key expiry migration...');
logger.info(`📅 Default expiry period: ${DEFAULT_DAYS} days`);
logger.info(`🔍 Mode: ${DRY_RUN ? 'DRY RUN (no changes will be made)' : 'LIVE RUN'}`);
logger.info('🔄 Starting API Key expiry migration...')
logger.info(`📅 Default expiry period: ${DEFAULT_DAYS} days`)
logger.info(`🔍 Mode: ${DRY_RUN ? 'DRY RUN (no changes will be made)' : 'LIVE RUN'}`)
// 连接 Redis
await redis.connect();
logger.success('✅ Connected to Redis');
await redis.connect()
logger.success('✅ Connected to Redis')
// 获取所有 API Keys
const apiKeys = await redis.getAllApiKeys();
logger.info(`📊 Found ${apiKeys.length} API Keys in total`);
const apiKeys = await redis.getAllApiKeys()
logger.info(`📊 Found ${apiKeys.length} API Keys in total`)
// 统计信息
const stats = {
total: apiKeys.length,
@@ -61,98 +61,99 @@ async function migrateApiKeys() {
alreadyHasExpiry: 0,
migrated: 0,
errors: 0
};
}
// 需要迁移的 Keys
const keysToMigrate = [];
const keysToMigrate = []
// 分析每个 API Key
for (const key of apiKeys) {
if (!key.expiresAt || key.expiresAt === 'null' || key.expiresAt === '') {
keysToMigrate.push(key);
stats.needsMigration++;
logger.info(`📌 API Key "${key.name}" (${key.id}) needs migration`);
keysToMigrate.push(key)
stats.needsMigration++
logger.info(`📌 API Key "${key.name}" (${key.id}) needs migration`)
} else {
stats.alreadyHasExpiry++;
const expiryDate = new Date(key.expiresAt);
logger.info(`✓ API Key "${key.name}" (${key.id}) already has expiry: ${expiryDate.toLocaleString()}`);
stats.alreadyHasExpiry++
const expiryDate = new Date(key.expiresAt)
logger.info(
`✓ API Key "${key.name}" (${key.id}) already has expiry: ${expiryDate.toLocaleString()}`
)
}
}
if (keysToMigrate.length === 0) {
logger.success('✨ No API Keys need migration!');
return;
logger.success('✨ No API Keys need migration!')
return
}
// 显示迁移摘要
console.log('\n' + '='.repeat(60));
console.log('📋 Migration Summary:');
console.log('='.repeat(60));
console.log(`Total API Keys: ${stats.total}`);
console.log(`Already have expiry: ${stats.alreadyHasExpiry}`);
console.log(`Need migration: ${stats.needsMigration}`);
console.log(`Default expiry: ${DEFAULT_DAYS} days from now`);
console.log('='.repeat(60) + '\n');
console.log(`\n${'='.repeat(60)}`)
console.log('📋 Migration Summary:')
console.log('='.repeat(60))
console.log(`Total API Keys: ${stats.total}`)
console.log(`Already have expiry: ${stats.alreadyHasExpiry}`)
console.log(`Need migration: ${stats.needsMigration}`)
console.log(`Default expiry: ${DEFAULT_DAYS} days from now`)
console.log(`${'='.repeat(60)}\n`)
// 如果不是 dry run请求确认
if (!DRY_RUN) {
const confirmed = await askConfirmation(
`⚠️ This will set expiry dates for ${keysToMigrate.length} API Keys. Continue?`
);
)
if (!confirmed) {
logger.warn('❌ Migration cancelled by user');
return;
logger.warn('❌ Migration cancelled by user')
return
}
}
// 计算新的过期时间
const newExpiryDate = new Date();
newExpiryDate.setDate(newExpiryDate.getDate() + DEFAULT_DAYS);
const newExpiryISO = newExpiryDate.toISOString();
logger.info(`\n🚀 Starting migration... New expiry date: ${newExpiryDate.toLocaleString()}`);
const newExpiryDate = new Date()
newExpiryDate.setDate(newExpiryDate.getDate() + DEFAULT_DAYS)
const newExpiryISO = newExpiryDate.toISOString()
logger.info(`\n🚀 Starting migration... New expiry date: ${newExpiryDate.toLocaleString()}`)
// 执行迁移
for (const key of keysToMigrate) {
try {
if (!DRY_RUN) {
// 直接更新 Redis 中的数据
// 使用 hset 更新单个字段
await redis.client.hset(`apikey:${key.id}`, 'expiresAt', newExpiryISO);
logger.success(`✅ Migrated: "${key.name}" (${key.id})`);
await redis.client.hset(`apikey:${key.id}`, 'expiresAt', newExpiryISO)
logger.success(`✅ Migrated: "${key.name}" (${key.id})`)
} else {
logger.info(`[DRY RUN] Would migrate: "${key.name}" (${key.id})`);
logger.info(`[DRY RUN] Would migrate: "${key.name}" (${key.id})`)
}
stats.migrated++;
stats.migrated++
} catch (error) {
logger.error(`❌ Error migrating "${key.name}" (${key.id}):`, error.message);
stats.errors++;
logger.error(`❌ Error migrating "${key.name}" (${key.id}):`, error.message)
stats.errors++
}
}
// 显示最终结果
console.log('\n' + '='.repeat(60));
console.log('✅ Migration Complete!');
console.log('='.repeat(60));
console.log(`Successfully migrated: ${stats.migrated}`);
console.log(`Errors: ${stats.errors}`);
console.log(`New expiry date: ${newExpiryDate.toLocaleString()}`);
console.log('='.repeat(60) + '\n');
console.log(`\n${'='.repeat(60)}`)
console.log('✅ Migration Complete!')
console.log('='.repeat(60))
console.log(`Successfully migrated: ${stats.migrated}`)
console.log(`Errors: ${stats.errors}`)
console.log(`New expiry date: ${newExpiryDate.toLocaleString()}`)
console.log(`${'='.repeat(60)}\n`)
if (DRY_RUN) {
logger.warn('⚠️ This was a DRY RUN. No actual changes were made.');
logger.info('💡 Run without --dry-run flag to apply changes.');
logger.warn('⚠️ This was a DRY RUN. No actual changes were made.')
logger.info('💡 Run without --dry-run flag to apply changes.')
}
} catch (error) {
logger.error('💥 Migration failed:', error);
process.exit(1);
logger.error('💥 Migration failed:', error)
process.exit(1)
} finally {
// 清理
rl.close();
await redis.disconnect();
logger.info('👋 Disconnected from Redis');
rl.close()
await redis.disconnect()
logger.info('👋 Disconnected from Redis')
}
}
@@ -180,12 +181,12 @@ Examples:
# Test run without making changes
node scripts/migrate-apikey-expiry.js --dry-run
`);
process.exit(0);
`)
process.exit(0)
}
// 运行迁移
migrateApiKeys().catch(error => {
logger.error('💥 Unexpected error:', error);
process.exit(1);
});
migrateApiKeys().catch((error) => {
logger.error('💥 Unexpected error:', error)
process.exit(1)
})

View File

@@ -1,123 +1,128 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const chalk = require('chalk');
const ora = require('ora');
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const chalk = require('chalk')
const ora = require('ora')
const config = require('../config/config');
const config = require('../config/config')
async function setup() {
console.log(chalk.blue.bold('\n🚀 Claude Relay Service 初始化设置\n'));
const spinner = ora('正在进行初始化设置...').start();
console.log(chalk.blue.bold('\n🚀 Claude Relay Service 初始化设置\n'))
const spinner = ora('正在进行初始化设置...').start()
try {
// 1. 创建必要目录
const directories = [
'logs',
'data',
'temp'
];
directories.forEach(dir => {
const dirPath = path.join(__dirname, '..', dir);
const directories = ['logs', 'data', 'temp']
directories.forEach((dir) => {
const dirPath = path.join(__dirname, '..', dir)
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
fs.mkdirSync(dirPath, { recursive: true })
}
});
})
// 2. 生成环境配置文件
if (!fs.existsSync(path.join(__dirname, '..', '.env'))) {
const envTemplate = fs.readFileSync(path.join(__dirname, '..', '.env.example'), 'utf8');
const envTemplate = fs.readFileSync(path.join(__dirname, '..', '.env.example'), 'utf8')
// 生成随机密钥
const jwtSecret = crypto.randomBytes(64).toString('hex');
const encryptionKey = crypto.randomBytes(32).toString('hex');
const jwtSecret = crypto.randomBytes(64).toString('hex')
const encryptionKey = crypto.randomBytes(32).toString('hex')
const envContent = envTemplate
.replace('your-jwt-secret-here', jwtSecret)
.replace('your-encryption-key-here', encryptionKey);
fs.writeFileSync(path.join(__dirname, '..', '.env'), envContent);
.replace('your-encryption-key-here', encryptionKey)
fs.writeFileSync(path.join(__dirname, '..', '.env'), envContent)
}
// 3. 生成或使用环境变量中的管理员凭据
const adminUsername = process.env.ADMIN_USERNAME || `cr_admin_${crypto.randomBytes(4).toString('hex')}`;
const adminPassword = process.env.ADMIN_PASSWORD || crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
const adminUsername =
process.env.ADMIN_USERNAME || `cr_admin_${crypto.randomBytes(4).toString('hex')}`
const adminPassword =
process.env.ADMIN_PASSWORD ||
crypto
.randomBytes(16)
.toString('base64')
.replace(/[^a-zA-Z0-9]/g, '')
.substring(0, 16)
// 如果使用了环境变量,显示提示
if (process.env.ADMIN_USERNAME || process.env.ADMIN_PASSWORD) {
console.log(chalk.yellow('\n📌 使用环境变量中的管理员凭据'));
console.log(chalk.yellow('\n📌 使用环境变量中的管理员凭据'))
}
// 4. 创建初始化完成标记文件
const initData = {
initializedAt: new Date().toISOString(),
adminUsername,
adminPassword,
version: '1.0.0'
};
}
fs.writeFileSync(
path.join(__dirname, '..', 'data', 'init.json'),
path.join(__dirname, '..', 'data', 'init.json'),
JSON.stringify(initData, null, 2)
);
spinner.succeed('初始化设置完成');
console.log(chalk.green('\n✅ 设置完成!\n'));
console.log(chalk.yellow('📋 重要信息:\n'));
console.log(` 管理员用户名: ${chalk.cyan(adminUsername)}`);
console.log(` 管理员密码: ${chalk.cyan(adminPassword)}`);
)
spinner.succeed('初始化设置完成')
console.log(chalk.green('\n✅ 设置完成!\n'))
console.log(chalk.yellow('📋 重要信息:\n'))
console.log(` 管理员用户名: ${chalk.cyan(adminUsername)}`)
console.log(` 管理员密码: ${chalk.cyan(adminPassword)}`)
// 如果是自动生成的凭据,强调需要保存
if (!process.env.ADMIN_USERNAME && !process.env.ADMIN_PASSWORD) {
console.log(chalk.red('\n⚠ 请立即保存这些凭据!首次登录后建议修改密码。'));
console.log(chalk.yellow('\n💡 提示: 也可以通过环境变量 ADMIN_USERNAME 和 ADMIN_PASSWORD 预设管理员凭据。\n'));
console.log(chalk.red('\n⚠ 请立即保存这些凭据!首次登录后建议修改密码。'))
console.log(
chalk.yellow(
'\n💡 提示: 也可以通过环境变量 ADMIN_USERNAME 和 ADMIN_PASSWORD 预设管理员凭据。\n'
)
)
} else {
console.log(chalk.green('\n✅ 已使用预设的管理员凭据。\n'));
console.log(chalk.green('\n✅ 已使用预设的管理员凭据。\n'))
}
console.log(chalk.blue('🚀 启动服务:\n'));
console.log(' npm start - 启动生产服务');
console.log(' npm run dev - 启动开发服务');
console.log(' npm run cli admin - 管理员CLI工具\n');
console.log(chalk.blue('🌐 访问地址:\n'));
console.log(` Web管理界面: http://localhost:${config.server.port}/web`);
console.log(` API端点: http://localhost:${config.server.port}/api/v1/messages`);
console.log(` 健康检查: http://localhost:${config.server.port}/health\n`);
console.log(chalk.blue('🚀 启动服务:\n'))
console.log(' npm start - 启动生产服务')
console.log(' npm run dev - 启动开发服务')
console.log(' npm run cli admin - 管理员CLI工具\n')
console.log(chalk.blue('🌐 访问地址:\n'))
console.log(` Web管理界面: http://localhost:${config.server.port}/web`)
console.log(` API端点: http://localhost:${config.server.port}/api/v1/messages`)
console.log(` 健康检查: http://localhost:${config.server.port}/health\n`)
} catch (error) {
spinner.fail('初始化设置失败');
console.error(chalk.red('❌ 错误:'), error.message);
process.exit(1);
spinner.fail('初始化设置失败')
console.error(chalk.red('❌ 错误:'), error.message)
process.exit(1)
}
}
// 检查是否已初始化
function checkInitialized() {
const initFile = path.join(__dirname, '..', 'data', 'init.json');
const initFile = path.join(__dirname, '..', 'data', 'init.json')
if (fs.existsSync(initFile)) {
const initData = JSON.parse(fs.readFileSync(initFile, 'utf8'));
console.log(chalk.yellow('⚠️ 服务已经初始化过了!'));
console.log(` 初始化时间: ${new Date(initData.initializedAt).toLocaleString()}`);
console.log(` 管理员用户名: ${initData.adminUsername}`);
console.log('\n如需重新初始化请删除 data/init.json 文件后再运行此命令。');
console.log(chalk.red('\n⚠ 重要提示:'));
console.log(' 1. 删除 init.json 文件后运行 npm run setup');
console.log(' 2. 生成新的账号密码后,需要重启服务才能生效');
console.log(' 3. 使用 npm run service:restart 重启服务\n');
return true;
const initData = JSON.parse(fs.readFileSync(initFile, 'utf8'))
console.log(chalk.yellow('⚠️ 服务已经初始化过了!'))
console.log(` 初始化时间: ${new Date(initData.initializedAt).toLocaleString()}`)
console.log(` 管理员用户名: ${initData.adminUsername}`)
console.log('\n如需重新初始化请删除 data/init.json 文件后再运行此命令。')
console.log(chalk.red('\n⚠ 重要提示:'))
console.log(' 1. 删除 init.json 文件后运行 npm run setup')
console.log(' 2. 生成新的账号密码后,需要重启服务才能生效')
console.log(' 3. 使用 npm run service:restart 重启服务\n')
return true
}
return false;
return false
}
if (require.main === module) {
if (!checkInitialized()) {
setup();
setup()
}
}
module.exports = { setup, checkInitialized };
module.exports = { setup, checkInitialized }

View File

@@ -2,143 +2,142 @@
* 测试账号显示问题是否已修复
*/
const axios = require('axios');
const config = require('../config/config');
const axios = require('axios')
const config = require('../config/config')
// 从 init.json 读取管理员凭据
const fs = require('fs');
const path = require('path');
const fs = require('fs')
const path = require('path')
async function testAccountDisplay() {
console.log('🔍 测试账号显示问题...\n');
console.log('🔍 测试账号显示问题...\n')
try {
// 读取管理员凭据
const initPath = path.join(__dirname, '..', 'config', 'init.json');
const initPath = path.join(__dirname, '..', 'config', 'init.json')
if (!fs.existsSync(initPath)) {
console.error('❌ 找不到 init.json 文件,请运行 npm run setup');
process.exit(1);
console.error('❌ 找不到 init.json 文件,请运行 npm run setup')
process.exit(1)
}
const initData = JSON.parse(fs.readFileSync(initPath, 'utf8'));
const adminUser = initData.admins?.[0];
const initData = JSON.parse(fs.readFileSync(initPath, 'utf8'))
const adminUser = initData.admins?.[0]
if (!adminUser) {
console.error('❌ 没有找到管理员账号');
process.exit(1);
console.error('❌ 没有找到管理员账号')
process.exit(1)
}
const baseURL = `http://localhost:${config.server.port}`;
const baseURL = `http://localhost:${config.server.port}`
// 登录获取 token
console.log('🔐 登录管理员账号...');
console.log('🔐 登录管理员账号...')
const loginResp = await axios.post(`${baseURL}/admin/login`, {
username: adminUser.username,
password: adminUser.password
});
})
if (!loginResp.data.success) {
console.error('❌ 登录失败');
process.exit(1);
console.error('❌ 登录失败')
process.exit(1)
}
const token = loginResp.data.token;
console.log('✅ 登录成功\n');
const { token } = loginResp.data
console.log('✅ 登录成功\n')
// 设置请求头
const headers = {
'Authorization': `Bearer ${token}`,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
// 获取 Claude OAuth 账号
console.log('📋 获取 Claude OAuth 账号...');
const claudeResp = await axios.get(`${baseURL}/admin/claude-accounts`, { headers });
const claudeAccounts = claudeResp.data.data || [];
console.log(`找到 ${claudeAccounts.length} 个 Claude OAuth 账号`);
console.log('📋 获取 Claude OAuth 账号...')
const claudeResp = await axios.get(`${baseURL}/admin/claude-accounts`, { headers })
const claudeAccounts = claudeResp.data.data || []
console.log(`找到 ${claudeAccounts.length} 个 Claude OAuth 账号`)
// 分类显示
const claudeDedicated = claudeAccounts.filter(a => a.accountType === 'dedicated');
const claudeGroup = claudeAccounts.filter(a => a.accountType === 'group');
const claudeShared = claudeAccounts.filter(a => a.accountType === 'shared');
console.log(`- 专属账号: ${claudeDedicated.length}`);
console.log(`- 分组账号: ${claudeGroup.length}`);
console.log(`- 共享账号: ${claudeShared.length}`);
const claudeDedicated = claudeAccounts.filter((a) => a.accountType === 'dedicated')
const claudeGroup = claudeAccounts.filter((a) => a.accountType === 'group')
const claudeShared = claudeAccounts.filter((a) => a.accountType === 'shared')
console.log(`- 专属账号: ${claudeDedicated.length}`)
console.log(`- 分组账号: ${claudeGroup.length}`)
console.log(`- 共享账号: ${claudeShared.length}`)
// 检查 platform 字段
console.log('\n检查 platform 字段:');
claudeAccounts.slice(0, 3).forEach(acc => {
console.log(`- ${acc.name}: platform=${acc.platform}, accountType=${acc.accountType}`);
});
console.log('\n检查 platform 字段:')
claudeAccounts.slice(0, 3).forEach((acc) => {
console.log(`- ${acc.name}: platform=${acc.platform}, accountType=${acc.accountType}`)
})
// 获取 Claude Console 账号
console.log('\n📋 获取 Claude Console 账号...');
const consoleResp = await axios.get(`${baseURL}/admin/claude-console-accounts`, { headers });
const consoleAccounts = consoleResp.data.data || [];
console.log(`找到 ${consoleAccounts.length} 个 Claude Console 账号`);
console.log('\n📋 获取 Claude Console 账号...')
const consoleResp = await axios.get(`${baseURL}/admin/claude-console-accounts`, { headers })
const consoleAccounts = consoleResp.data.data || []
console.log(`找到 ${consoleAccounts.length} 个 Claude Console 账号`)
// 分类显示
const consoleDedicated = consoleAccounts.filter(a => a.accountType === 'dedicated');
const consoleGroup = consoleAccounts.filter(a => a.accountType === 'group');
const consoleShared = consoleAccounts.filter(a => a.accountType === 'shared');
console.log(`- 专属账号: ${consoleDedicated.length}`);
console.log(`- 分组账号: ${consoleGroup.length}`);
console.log(`- 共享账号: ${consoleShared.length}`);
const consoleDedicated = consoleAccounts.filter((a) => a.accountType === 'dedicated')
const consoleGroup = consoleAccounts.filter((a) => a.accountType === 'group')
const consoleShared = consoleAccounts.filter((a) => a.accountType === 'shared')
console.log(`- 专属账号: ${consoleDedicated.length}`)
console.log(`- 分组账号: ${consoleGroup.length}`)
console.log(`- 共享账号: ${consoleShared.length}`)
// 检查 platform 字段
console.log('\n检查 platform 字段:');
consoleAccounts.slice(0, 3).forEach(acc => {
console.log(`- ${acc.name}: platform=${acc.platform}, accountType=${acc.accountType}`);
});
console.log('\n检查 platform 字段:')
consoleAccounts.slice(0, 3).forEach((acc) => {
console.log(`- ${acc.name}: platform=${acc.platform}, accountType=${acc.accountType}`)
})
// 获取账号分组
console.log('\n📋 获取账号分组...');
const groupsResp = await axios.get(`${baseURL}/admin/account-groups`, { headers });
const groups = groupsResp.data.data || [];
console.log(`找到 ${groups.length} 个账号分组`);
const claudeGroups = groups.filter(g => g.platform === 'claude');
const geminiGroups = groups.filter(g => g.platform === 'gemini');
console.log(`- Claude 分组: ${claudeGroups.length}`);
console.log(`- Gemini 分组: ${geminiGroups.length}`);
console.log('\n📋 获取账号分组...')
const groupsResp = await axios.get(`${baseURL}/admin/account-groups`, { headers })
const groups = groupsResp.data.data || []
console.log(`找到 ${groups.length} 个账号分组`)
const claudeGroups = groups.filter((g) => g.platform === 'claude')
const geminiGroups = groups.filter((g) => g.platform === 'gemini')
console.log(`- Claude 分组: ${claudeGroups.length}`)
console.log(`- Gemini 分组: ${geminiGroups.length}`)
// 测试结果总结
console.log('\n📊 测试结果总结:');
console.log('✅ Claude OAuth 账号已包含 platform 字段');
console.log('✅ Claude Console 账号已包含 platform 字段');
console.log('✅ 账号分组功能正常');
const totalDedicated = claudeDedicated.length + consoleDedicated.length;
const totalGroups = claudeGroups.length;
console.log('\n📊 测试结果总结:')
console.log('✅ Claude OAuth 账号已包含 platform 字段')
console.log('✅ Claude Console 账号已包含 platform 字段')
console.log('✅ 账号分组功能正常')
const totalDedicated = claudeDedicated.length + consoleDedicated.length
const totalGroups = claudeGroups.length
if (totalDedicated > 0) {
console.log(`\n✅ 共有 ${totalDedicated} 个专属账号应该显示在下拉框中`);
console.log(`\n✅ 共有 ${totalDedicated} 个专属账号应该显示在下拉框中`)
} else {
console.log('\n⚠ 没有找到专属账号,请在账号管理页面设置账号类型为"专属账户"');
console.log('\n⚠ 没有找到专属账号,请在账号管理页面设置账号类型为"专属账户"')
}
if (totalGroups > 0) {
console.log(`✅ 共有 ${totalGroups} 个分组应该显示在下拉框中`);
console.log(`✅ 共有 ${totalGroups} 个分组应该显示在下拉框中`)
}
console.log('\n💡 请在浏览器中测试创建/编辑 API Key检查下拉框是否正确显示三个类别');
console.log(' 1. 调度分组');
console.log(' 2. Claude OAuth 账号');
console.log(' 3. Claude Console 账号');
console.log('\n💡 请在浏览器中测试创建/编辑 API Key检查下拉框是否正确显示三个类别')
console.log(' 1. 调度分组')
console.log(' 2. Claude OAuth 账号')
console.log(' 3. Claude Console 账号')
} catch (error) {
console.error('❌ 测试失败:', error.message);
console.error('❌ 测试失败:', error.message)
if (error.response) {
console.error('响应数据:', error.response.data);
console.error('响应数据:', error.response.data)
}
} finally {
process.exit(0);
process.exit(0)
}
}
testAccountDisplay();
testAccountDisplay()

View File

@@ -2,127 +2,140 @@
* 测试 API 响应中的账号数据
*/
const redis = require('../src/models/redis');
const claudeAccountService = require('../src/services/claudeAccountService');
const claudeConsoleAccountService = require('../src/services/claudeConsoleAccountService');
const accountGroupService = require('../src/services/accountGroupService');
const redis = require('../src/models/redis')
const claudeAccountService = require('../src/services/claudeAccountService')
const claudeConsoleAccountService = require('../src/services/claudeConsoleAccountService')
const accountGroupService = require('../src/services/accountGroupService')
async function testApiResponse() {
console.log('🔍 测试 API 响应数据...\n');
console.log('🔍 测试 API 响应数据...\n')
try {
// 确保 Redis 已连接
await redis.connect();
await redis.connect()
// 1. 测试 Claude OAuth 账号服务
console.log('📋 测试 Claude OAuth 账号服务...');
const claudeAccounts = await claudeAccountService.getAllAccounts();
console.log(`找到 ${claudeAccounts.length} 个 Claude OAuth 账号`);
console.log('📋 测试 Claude OAuth 账号服务...')
const claudeAccounts = await claudeAccountService.getAllAccounts()
console.log(`找到 ${claudeAccounts.length} 个 Claude OAuth 账号`)
// 检查前3个账号的数据结构
console.log('\n账号数据结构示例:');
claudeAccounts.slice(0, 3).forEach(acc => {
console.log(`\n账号: ${acc.name}`);
console.log(` - ID: ${acc.id}`);
console.log(` - accountType: ${acc.accountType}`);
console.log(` - platform: ${acc.platform}`);
console.log(` - status: ${acc.status}`);
console.log(` - isActive: ${acc.isActive}`);
});
console.log('\n账号数据结构示例:')
claudeAccounts.slice(0, 3).forEach((acc) => {
console.log(`\n账号: ${acc.name}`)
console.log(` - ID: ${acc.id}`)
console.log(` - accountType: ${acc.accountType}`)
console.log(` - platform: ${acc.platform}`)
console.log(` - status: ${acc.status}`)
console.log(` - isActive: ${acc.isActive}`)
})
// 统计专属账号
const claudeDedicated = claudeAccounts.filter(a => a.accountType === 'dedicated');
const claudeGroup = claudeAccounts.filter(a => a.accountType === 'group');
console.log(`\n统计结果:`);
console.log(` - 专属账号: ${claudeDedicated.length}`);
console.log(` - 分组账号: ${claudeGroup.length}`);
const claudeDedicated = claudeAccounts.filter((a) => a.accountType === 'dedicated')
const claudeGroup = claudeAccounts.filter((a) => a.accountType === 'group')
console.log('\n统计结果:')
console.log(` - 专属账号: ${claudeDedicated.length}`)
console.log(` - 分组账号: ${claudeGroup.length}`)
// 2. 测试 Claude Console 账号服务
console.log('\n\n📋 测试 Claude Console 账号服务...');
const consoleAccounts = await claudeConsoleAccountService.getAllAccounts();
console.log(`找到 ${consoleAccounts.length} 个 Claude Console 账号`);
console.log('\n\n📋 测试 Claude Console 账号服务...')
const consoleAccounts = await claudeConsoleAccountService.getAllAccounts()
console.log(`找到 ${consoleAccounts.length} 个 Claude Console 账号`)
// 检查前3个账号的数据结构
console.log('\n账号数据结构示例:');
consoleAccounts.slice(0, 3).forEach(acc => {
console.log(`\n账号: ${acc.name}`);
console.log(` - ID: ${acc.id}`);
console.log(` - accountType: ${acc.accountType}`);
console.log(` - platform: ${acc.platform}`);
console.log(` - status: ${acc.status}`);
console.log(` - isActive: ${acc.isActive}`);
});
console.log('\n账号数据结构示例:')
consoleAccounts.slice(0, 3).forEach((acc) => {
console.log(`\n账号: ${acc.name}`)
console.log(` - ID: ${acc.id}`)
console.log(` - accountType: ${acc.accountType}`)
console.log(` - platform: ${acc.platform}`)
console.log(` - status: ${acc.status}`)
console.log(` - isActive: ${acc.isActive}`)
})
// 统计专属账号
const consoleDedicated = consoleAccounts.filter(a => a.accountType === 'dedicated');
const consoleGroup = consoleAccounts.filter(a => a.accountType === 'group');
console.log(`\n统计结果:`);
console.log(` - 专属账号: ${consoleDedicated.length}`);
console.log(` - 分组账号: ${consoleGroup.length}`);
const consoleDedicated = consoleAccounts.filter((a) => a.accountType === 'dedicated')
const consoleGroup = consoleAccounts.filter((a) => a.accountType === 'group')
console.log('\n统计结果:')
console.log(` - 专属账号: ${consoleDedicated.length}`)
console.log(` - 分组账号: ${consoleGroup.length}`)
// 3. 测试账号分组服务
console.log('\n\n📋 测试账号分组服务...');
const groups = await accountGroupService.getAllGroups();
console.log(`找到 ${groups.length} 个账号分组`);
console.log('\n\n📋 测试账号分组服务...')
const groups = await accountGroupService.getAllGroups()
console.log(`找到 ${groups.length} 个账号分组`)
// 显示分组信息
groups.forEach(group => {
console.log(`\n分组: ${group.name}`);
console.log(` - ID: ${group.id}`);
console.log(` - platform: ${group.platform}`);
console.log(` - memberCount: ${group.memberCount}`);
});
groups.forEach((group) => {
console.log(`\n分组: ${group.name}`)
console.log(` - ID: ${group.id}`)
console.log(` - platform: ${group.platform}`)
console.log(` - memberCount: ${group.memberCount}`)
})
// 4. 验证结果
console.log('\n\n📊 验证结果:');
console.log('\n\n📊 验证结果:')
// 检查 platform 字段
const claudeWithPlatform = claudeAccounts.filter(a => a.platform === 'claude-oauth');
const consoleWithPlatform = consoleAccounts.filter(a => a.platform === 'claude-console');
const claudeWithPlatform = claudeAccounts.filter((a) => a.platform === 'claude-oauth')
const consoleWithPlatform = consoleAccounts.filter((a) => a.platform === 'claude-console')
if (claudeWithPlatform.length === claudeAccounts.length) {
console.log('✅ 所有 Claude OAuth 账号都有正确的 platform 字段');
console.log('✅ 所有 Claude OAuth 账号都有正确的 platform 字段')
} else {
console.log(`⚠️ 只有 ${claudeWithPlatform.length}/${claudeAccounts.length} 个 Claude OAuth 账号有正确的 platform 字段`);
console.log(
`⚠️ 只有 ${claudeWithPlatform.length}/${claudeAccounts.length} 个 Claude OAuth 账号有正确的 platform 字段`
)
}
if (consoleWithPlatform.length === consoleAccounts.length) {
console.log('✅ 所有 Claude Console 账号都有正确的 platform 字段');
console.log('✅ 所有 Claude Console 账号都有正确的 platform 字段')
} else {
console.log(`⚠️ 只有 ${consoleWithPlatform.length}/${consoleAccounts.length} 个 Claude Console 账号有正确的 platform 字段`);
console.log(
`⚠️ 只有 ${consoleWithPlatform.length}/${consoleAccounts.length} 个 Claude Console 账号有正确的 platform 字段`
)
}
// 总结
const totalDedicated = claudeDedicated.length + consoleDedicated.length;
const totalGroup = claudeGroup.length + consoleGroup.length;
const totalGroups = groups.filter(g => g.platform === 'claude').length;
console.log('\n📈 总结:');
console.log(`- 专属账号总数: ${totalDedicated} 个 (Claude OAuth: ${claudeDedicated.length}, Console: ${consoleDedicated.length})`);
console.log(`- 分组账号总数: ${totalGroup} 个 (Claude OAuth: ${claudeGroup.length}, Console: ${consoleGroup.length})`);
console.log(`- 账号分组总数: ${totalGroups}`);
const totalDedicated = claudeDedicated.length + consoleDedicated.length
const totalGroup = claudeGroup.length + consoleGroup.length
const totalGroups = groups.filter((g) => g.platform === 'claude').length
console.log('\n📈 总结:')
console.log(
`- 专属账号总数: ${totalDedicated} 个 (Claude OAuth: ${claudeDedicated.length}, Console: ${consoleDedicated.length})`
)
console.log(
`- 分组账号总数: ${totalGroup} 个 (Claude OAuth: ${claudeGroup.length}, Console: ${consoleGroup.length})`
)
console.log(`- 账号分组总数: ${totalGroups}`)
if (totalDedicated + totalGroups > 0) {
console.log('\n✅ 前端下拉框应该能够显示:');
if (totalGroups > 0) console.log(' - 调度分组');
if (claudeDedicated.length > 0) console.log(' - Claude OAuth 专属账号 (仅 dedicated 类型)');
if (consoleDedicated.length > 0) console.log(' - Claude Console 专属账号 (仅 dedicated 类型)');
console.log('\n✅ 前端下拉框应该能够显示:')
if (totalGroups > 0) {
console.log(' - 调度分组')
}
if (claudeDedicated.length > 0) {
console.log(' - Claude OAuth 专属账号 (仅 dedicated 类型)')
}
if (consoleDedicated.length > 0) {
console.log(' - Claude Console 专属账号 (仅 dedicated 类型)')
}
} else {
console.log('\n⚠ 没有找到任何专属账号或分组,请检查账号配置');
console.log('\n⚠ 没有找到任何专属账号或分组,请检查账号配置')
}
console.log('\n💡 说明:');
console.log('- 专属账号下拉框只显示 accountType="dedicated" 的账号');
console.log('- accountType="group" 的账号通过分组调度,不在专属账号中显示');
console.log('\n💡 说明:')
console.log('- 专属账号下拉框只显示 accountType="dedicated" 的账号')
console.log('- accountType="group" 的账号通过分组调度,不在专属账号中显示')
} catch (error) {
console.error('❌ 测试失败:', error);
console.error(error.stack);
console.error('❌ 测试失败:', error)
console.error(error.stack)
} finally {
process.exit(0);
process.exit(0)
}
}
testApiResponse();
testApiResponse()

View File

@@ -1,35 +1,33 @@
#!/usr/bin/env node
const bedrockAccountService = require('../src/services/bedrockAccountService');
const bedrockRelayService = require('../src/services/bedrockRelayService');
const logger = require('../src/utils/logger');
const bedrockRelayService = require('../src/services/bedrockRelayService')
async function testBedrockModels() {
try {
console.log('🧪 测试Bedrock模型配置...');
console.log('🧪 测试Bedrock模型配置...')
// 测试可用模型列表
const models = await bedrockRelayService.getAvailableModels();
console.log(`📋 找到 ${models.length} 个可用模型:`);
models.forEach(model => {
console.log(` - ${model.id} (${model.name})`);
});
const models = await bedrockRelayService.getAvailableModels()
console.log(`📋 找到 ${models.length} 个可用模型:`)
models.forEach((model) => {
console.log(` - ${model.id} (${model.name})`)
})
// 测试默认模型
console.log(`\n🎯 系统默认模型: ${bedrockRelayService.defaultModel}`);
console.log(`🎯 系统默认小模型: ${bedrockRelayService.defaultSmallModel}`);
console.log('\n✅ Bedrock模型配置测试完成');
process.exit(0);
console.log(`\n🎯 系统默认模型: ${bedrockRelayService.defaultModel}`)
console.log(`🎯 系统默认小模型: ${bedrockRelayService.defaultSmallModel}`)
console.log('\n✅ Bedrock模型配置测试完成')
process.exit(0)
} catch (error) {
console.error('❌ Bedrock模型测试失败:', error);
process.exit(1);
console.error('❌ Bedrock模型测试失败:', error)
process.exit(1)
}
}
// 如果直接运行此脚本
if (require.main === module) {
testBedrockModels();
testBedrockModels()
}
module.exports = { testBedrockModels };
module.exports = { testBedrockModels }

View File

@@ -2,131 +2,132 @@
* 测试专属账号显示问题
*/
const redis = require('../src/models/redis');
const redis = require('../src/models/redis')
async function testDedicatedAccounts() {
console.log('🔍 检查专属账号...\n');
console.log('🔍 检查专属账号...\n')
try {
// 确保 Redis 已连接
await redis.connect();
await redis.connect()
// 获取所有 Claude 账号
const claudeKeys = await redis.client.keys('claude:account:*');
console.log(`找到 ${claudeKeys.length} 个 Claude 账号\n`);
const dedicatedAccounts = [];
const groupAccounts = [];
const sharedAccounts = [];
const claudeKeys = await redis.client.keys('claude:account:*')
console.log(`找到 ${claudeKeys.length} 个 Claude 账号\n`)
const dedicatedAccounts = []
const groupAccounts = []
const sharedAccounts = []
for (const key of claudeKeys) {
const account = await redis.client.hgetall(key);
const accountType = account.accountType || 'shared';
const account = await redis.client.hgetall(key)
const accountType = account.accountType || 'shared'
const accountInfo = {
id: account.id,
name: account.name,
accountType: accountType,
accountType,
status: account.status,
isActive: account.isActive,
createdAt: account.createdAt
};
}
if (accountType === 'dedicated') {
dedicatedAccounts.push(accountInfo);
dedicatedAccounts.push(accountInfo)
} else if (accountType === 'group') {
groupAccounts.push(accountInfo);
groupAccounts.push(accountInfo)
} else {
sharedAccounts.push(accountInfo);
sharedAccounts.push(accountInfo)
}
}
console.log('📊 账号统计:');
console.log(`- 专属账号: ${dedicatedAccounts.length}`);
console.log(`- 分组账号: ${groupAccounts.length}`);
console.log(`- 共享账号: ${sharedAccounts.length}`);
console.log('');
console.log('📊 账号统计:')
console.log(`- 专属账号: ${dedicatedAccounts.length}`)
console.log(`- 分组账号: ${groupAccounts.length}`)
console.log(`- 共享账号: ${sharedAccounts.length}`)
console.log('')
if (dedicatedAccounts.length > 0) {
console.log('✅ 专属账号列表:');
dedicatedAccounts.forEach(acc => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`);
});
console.log('');
console.log('✅ 专属账号列表:')
dedicatedAccounts.forEach((acc) => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`)
})
console.log('')
} else {
console.log('⚠️ 没有找到专属账号!');
console.log('💡 提示: 请确保在账号管理页面将账号类型设置为"专属账户"');
console.log('');
console.log('⚠️ 没有找到专属账号!')
console.log('💡 提示: 请确保在账号管理页面将账号类型设置为"专属账户"')
console.log('')
}
if (groupAccounts.length > 0) {
console.log('📁 分组账号列表:');
groupAccounts.forEach(acc => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`);
});
console.log('');
console.log('📁 分组账号列表:')
groupAccounts.forEach((acc) => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`)
})
console.log('')
}
// 检查分组
const groupKeys = await redis.client.keys('account_group:*');
console.log(`\n找到 ${groupKeys.length} 个账号分组`);
const groupKeys = await redis.client.keys('account_group:*')
console.log(`\n找到 ${groupKeys.length} 个账号分组`)
if (groupKeys.length > 0) {
console.log('📋 分组列表:');
console.log('📋 分组列表:')
for (const key of groupKeys) {
const group = await redis.client.hgetall(key);
console.log(` - ${group.name} (平台: ${group.platform}, 成员数: ${group.memberCount || 0})`);
const group = await redis.client.hgetall(key)
console.log(
` - ${group.name} (平台: ${group.platform}, 成员数: ${group.memberCount || 0})`
)
}
}
// 检查 Claude Console 账号
const consoleKeys = await redis.client.keys('claude_console_account:*');
console.log(`\n找到 ${consoleKeys.length} 个 Claude Console 账号`);
const dedicatedConsoleAccounts = [];
const groupConsoleAccounts = [];
const consoleKeys = await redis.client.keys('claude_console_account:*')
console.log(`\n找到 ${consoleKeys.length} 个 Claude Console 账号`)
const dedicatedConsoleAccounts = []
const groupConsoleAccounts = []
for (const key of consoleKeys) {
const account = await redis.client.hgetall(key);
const accountType = account.accountType || 'shared';
const account = await redis.client.hgetall(key)
const accountType = account.accountType || 'shared'
if (accountType === 'dedicated') {
dedicatedConsoleAccounts.push({
id: account.id,
name: account.name,
accountType: accountType,
accountType,
status: account.status
});
})
} else if (accountType === 'group') {
groupConsoleAccounts.push({
id: account.id,
name: account.name,
accountType: accountType,
accountType,
status: account.status
});
})
}
}
if (dedicatedConsoleAccounts.length > 0) {
console.log('\n✅ Claude Console 专属账号:');
dedicatedConsoleAccounts.forEach(acc => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`);
});
console.log('\n✅ Claude Console 专属账号:')
dedicatedConsoleAccounts.forEach((acc) => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`)
})
}
if (groupConsoleAccounts.length > 0) {
console.log('\n📁 Claude Console 分组账号:');
groupConsoleAccounts.forEach(acc => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`);
});
console.log('\n📁 Claude Console 分组账号:')
groupConsoleAccounts.forEach((acc) => {
console.log(` - ${acc.name} (ID: ${acc.id}, 状态: ${acc.status})`)
})
}
} catch (error) {
console.error('❌ 错误:', error);
console.error(error.stack);
console.error('❌ 错误:', error)
console.error(error.stack)
} finally {
process.exit(0);
process.exit(0)
}
}
testDedicatedAccounts();
testDedicatedAccounts()

View File

@@ -4,141 +4,142 @@
* 测试 Gemini token 刷新功能
*/
const path = require('path');
const dotenv = require('dotenv');
const path = require('path')
const dotenv = require('dotenv')
// 加载环境变量
dotenv.config({ path: path.join(__dirname, '..', '.env') });
dotenv.config({ path: path.join(__dirname, '..', '.env') })
const redis = require('../src/models/redis');
const geminiAccountService = require('../src/services/geminiAccountService');
const logger = require('../src/utils/logger');
const crypto = require('crypto');
const config = require('../config/config');
const redis = require('../src/models/redis')
const geminiAccountService = require('../src/services/geminiAccountService')
const crypto = require('crypto')
const config = require('../config/config')
// 加密相关常量(与 geminiAccountService 保持一致)
const ALGORITHM = 'aes-256-cbc';
const ENCRYPTION_SALT = 'gemini-account-salt'; // 注意:是 'gemini-account-salt' 不是其他值!
const ALGORITHM = 'aes-256-cbc'
const ENCRYPTION_SALT = 'gemini-account-salt' // 注意:是 'gemini-account-salt' 不是其他值!
// 生成加密密钥
function generateEncryptionKey() {
return crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32);
return crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32)
}
// 解密函数(用于调试)
function debugDecrypt(text) {
if (!text) return { success: false, error: 'Empty text' };
if (!text) {
return { success: false, error: 'Empty text' }
}
try {
const key = generateEncryptionKey();
const ivHex = text.substring(0, 32);
const encryptedHex = text.substring(33);
const iv = Buffer.from(ivHex, 'hex');
const encryptedText = Buffer.from(encryptedHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return { success: true, value: decrypted.toString() };
const key = generateEncryptionKey()
const ivHex = text.substring(0, 32)
const encryptedHex = text.substring(33)
const iv = Buffer.from(ivHex, 'hex')
const encryptedText = Buffer.from(encryptedHex, 'hex')
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
let decrypted = decipher.update(encryptedText)
decrypted = Buffer.concat([decrypted, decipher.final()])
return { success: true, value: decrypted.toString() }
} catch (error) {
return { success: false, error: error.message };
return { success: false, error: error.message }
}
}
async function testGeminiTokenRefresh() {
try {
console.log('🚀 开始测试 Gemini token 刷新功能...\n');
console.log('🚀 开始测试 Gemini token 刷新功能...\n')
// 显示配置信息
console.log('📋 配置信息:');
console.log(` 加密密钥: ${config.security.encryptionKey}`);
console.log(` 加密盐值: ${ENCRYPTION_SALT}`);
console.log();
console.log('📋 配置信息:')
console.log(` 加密密钥: ${config.security.encryptionKey}`)
console.log(` 加密盐值: ${ENCRYPTION_SALT}`)
console.log()
// 1. 连接 Redis
console.log('📡 连接 Redis...');
await redis.connect();
console.log('✅ Redis 连接成功\n');
console.log('📡 连接 Redis...')
await redis.connect()
console.log('✅ Redis 连接成功\n')
// 2. 获取所有 Gemini 账户
console.log('🔍 获取 Gemini 账户列表...');
const accounts = await geminiAccountService.getAllAccounts();
const geminiAccounts = accounts.filter(acc => acc.platform === 'gemini');
console.log('🔍 获取 Gemini 账户列表...')
const accounts = await geminiAccountService.getAllAccounts()
const geminiAccounts = accounts.filter((acc) => acc.platform === 'gemini')
if (geminiAccounts.length === 0) {
console.log('❌ 没有找到 Gemini 账户');
process.exit(1);
console.log('❌ 没有找到 Gemini 账户')
process.exit(1)
}
console.log(`✅ 找到 ${geminiAccounts.length} 个 Gemini 账户\n`);
console.log(`✅ 找到 ${geminiAccounts.length} 个 Gemini 账户\n`)
// 3. 测试每个账户的 token 刷新
for (const account of geminiAccounts) {
console.log(`\n📋 测试账户: ${account.name} (${account.id})`);
console.log(` 状态: ${account.status}`);
console.log(`\n📋 测试账户: ${account.name} (${account.id})`)
console.log(` 状态: ${account.status}`)
try {
// 获取原始账户数据(用于调试)
const client = redis.getClient();
const rawData = await client.hgetall(`gemini_account:${account.id}`);
console.log(' 📊 原始数据检查:');
console.log(` refreshToken 存在: ${rawData.refreshToken ? '是' : '否'}`);
const client = redis.getClient()
const rawData = await client.hgetall(`gemini_account:${account.id}`)
console.log(' 📊 原始数据检查:')
console.log(` refreshToken 存在: ${rawData.refreshToken ? '是' : '否'}`)
if (rawData.refreshToken) {
console.log(` refreshToken 长度: ${rawData.refreshToken.length}`);
console.log(` refreshToken 前50字符: ${rawData.refreshToken.substring(0, 50)}...`);
console.log(` refreshToken 长度: ${rawData.refreshToken.length}`)
console.log(` refreshToken 前50字符: ${rawData.refreshToken.substring(0, 50)}...`)
// 尝试手动解密
const decryptResult = debugDecrypt(rawData.refreshToken);
const decryptResult = debugDecrypt(rawData.refreshToken)
if (decryptResult.success) {
console.log(` ✅ 手动解密成功`);
console.log(` 解密后前20字符: ${decryptResult.value.substring(0, 20)}...`);
console.log(' ✅ 手动解密成功')
console.log(` 解密后前20字符: ${decryptResult.value.substring(0, 20)}...`)
} else {
console.log(` ❌ 手动解密失败: ${decryptResult.error}`);
console.log(` ❌ 手动解密失败: ${decryptResult.error}`)
}
}
// 获取完整账户信息(包括解密的 token
const fullAccount = await geminiAccountService.getAccount(account.id);
const fullAccount = await geminiAccountService.getAccount(account.id)
if (!fullAccount.refreshToken) {
console.log(' ⚠️ 跳过:该账户无 refresh token\n');
continue;
console.log(' ⚠️ 跳过:该账户无 refresh token\n')
continue
}
console.log(` ✅ 找到 refresh token`);
console.log(` 📝 解密后的 refresh token 前20字符: ${fullAccount.refreshToken.substring(0, 20)}...`);
console.log(' 🔄 开始刷新 token...');
const startTime = Date.now();
console.log(' ✅ 找到 refresh token')
console.log(
` 📝 解密后的 refresh token 前20字符: ${fullAccount.refreshToken.substring(0, 20)}...`
)
console.log(' 🔄 开始刷新 token...')
const startTime = Date.now()
// 执行 token 刷新
const newTokens = await geminiAccountService.refreshAccountToken(account.id);
const duration = Date.now() - startTime;
console.log(` ✅ Token 刷新成功!耗时: ${duration}ms`);
console.log(` 📅 新的过期时间: ${new Date(newTokens.expiry_date).toLocaleString()}`);
console.log(` 🔑 Access Token: ${newTokens.access_token.substring(0, 20)}...`);
const newTokens = await geminiAccountService.refreshAccountToken(account.id)
const duration = Date.now() - startTime
console.log(` ✅ Token 刷新成功!耗时: ${duration}ms`)
console.log(` 📅 新的过期时间: ${new Date(newTokens.expiry_date).toLocaleString()}`)
console.log(` 🔑 Access Token: ${newTokens.access_token.substring(0, 20)}...`)
// 验证账户状态已更新
const updatedAccount = await geminiAccountService.getAccount(account.id);
console.log(` 📊 账户状态: ${updatedAccount.status}`);
const updatedAccount = await geminiAccountService.getAccount(account.id)
console.log(` 📊 账户状态: ${updatedAccount.status}`)
} catch (error) {
console.log(` ❌ Token 刷新失败: ${error.message}`);
console.log(` 🔍 错误详情:`, error);
console.log(` ❌ Token 刷新失败: ${error.message}`)
console.log(' 🔍 错误详情:', error)
}
}
console.log('\n✅ 测试完成!');
console.log('\n✅ 测试完成!')
} catch (error) {
console.error('❌ 测试失败:', error);
console.error('❌ 测试失败:', error)
} finally {
// 断开 Redis 连接
await redis.disconnect();
process.exit(0);
await redis.disconnect()
process.exit(0)
}
}
// 运行测试
testGeminiTokenRefresh();
testGeminiTokenRefresh()

View File

@@ -3,26 +3,25 @@
* 用于测试账户分组管理和调度逻辑的正确性
*/
require('dotenv').config();
const { v4: uuidv4 } = require('uuid');
const redis = require('../src/models/redis');
const accountGroupService = require('../src/services/accountGroupService');
const claudeAccountService = require('../src/services/claudeAccountService');
const claudeConsoleAccountService = require('../src/services/claudeConsoleAccountService');
const apiKeyService = require('../src/services/apiKeyService');
const unifiedClaudeScheduler = require('../src/services/unifiedClaudeScheduler');
const logger = require('../src/utils/logger');
require('dotenv').config()
const { v4: uuidv4 } = require('uuid')
const redis = require('../src/models/redis')
const accountGroupService = require('../src/services/accountGroupService')
const claudeAccountService = require('../src/services/claudeAccountService')
const claudeConsoleAccountService = require('../src/services/claudeConsoleAccountService')
const apiKeyService = require('../src/services/apiKeyService')
const unifiedClaudeScheduler = require('../src/services/unifiedClaudeScheduler')
// 测试配置
const TEST_PREFIX = 'test_group_';
const CLEANUP_ON_FINISH = true; // 测试完成后是否清理数据
const TEST_PREFIX = 'test_group_'
const CLEANUP_ON_FINISH = true // 测试完成后是否清理数据
// 测试数据存储
const testData = {
groups: [],
accounts: [],
apiKeys: []
};
}
// 颜色输出
const colors = {
@@ -31,68 +30,69 @@ const colors = {
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m'
};
}
function log(message, type = 'info') {
const color = {
success: colors.green,
error: colors.red,
warning: colors.yellow,
info: colors.blue
}[type] || colors.reset;
console.log(`${color}${message}${colors.reset}`);
const color =
{
success: colors.green,
error: colors.red,
warning: colors.yellow,
info: colors.blue
}[type] || colors.reset
console.log(`${color}${message}${colors.reset}`)
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms))
}
// 清理测试数据
async function cleanup() {
log('\n🧹 清理测试数据...', 'info');
log('\n🧹 清理测试数据...', 'info')
// 删除测试API Keys
for (const apiKey of testData.apiKeys) {
try {
await apiKeyService.deleteApiKey(apiKey.id);
log(`✅ 删除测试API Key: ${apiKey.name}`, 'success');
await apiKeyService.deleteApiKey(apiKey.id)
log(`✅ 删除测试API Key: ${apiKey.name}`, 'success')
} catch (error) {
log(`❌ 删除API Key失败: ${error.message}`, 'error');
log(`❌ 删除API Key失败: ${error.message}`, 'error')
}
}
// 删除测试账户
for (const account of testData.accounts) {
try {
if (account.type === 'claude') {
await claudeAccountService.deleteAccount(account.id);
await claudeAccountService.deleteAccount(account.id)
} else if (account.type === 'claude-console') {
await claudeConsoleAccountService.deleteAccount(account.id);
await claudeConsoleAccountService.deleteAccount(account.id)
}
log(`✅ 删除测试账户: ${account.name}`, 'success');
log(`✅ 删除测试账户: ${account.name}`, 'success')
} catch (error) {
log(`❌ 删除账户失败: ${error.message}`, 'error');
log(`❌ 删除账户失败: ${error.message}`, 'error')
}
}
// 删除测试分组
for (const group of testData.groups) {
try {
await accountGroupService.deleteGroup(group.id);
log(`✅ 删除测试分组: ${group.name}`, 'success');
await accountGroupService.deleteGroup(group.id)
log(`✅ 删除测试分组: ${group.name}`, 'success')
} catch (error) {
// 可能因为还有成员而删除失败,先移除所有成员
if (error.message.includes('分组内还有账户')) {
const members = await accountGroupService.getGroupMembers(group.id);
const members = await accountGroupService.getGroupMembers(group.id)
for (const memberId of members) {
await accountGroupService.removeAccountFromGroup(memberId, group.id);
await accountGroupService.removeAccountFromGroup(memberId, group.id)
}
// 重试删除
await accountGroupService.deleteGroup(group.id);
log(`✅ 删除测试分组: ${group.name} (清空成员后)`, 'success');
await accountGroupService.deleteGroup(group.id)
log(`✅ 删除测试分组: ${group.name} (清空成员后)`, 'success')
} else {
log(`❌ 删除分组失败: ${error.message}`, 'error');
log(`❌ 删除分组失败: ${error.message}`, 'error')
}
}
}
@@ -100,446 +100,449 @@ async function cleanup() {
// 测试1: 创建分组
async function test1_createGroups() {
log('\n📝 测试1: 创建账户分组', 'info');
log('\n📝 测试1: 创建账户分组', 'info')
try {
// 创建Claude分组
const claudeGroup = await accountGroupService.createGroup({
name: TEST_PREFIX + 'Claude组',
name: `${TEST_PREFIX}Claude组`,
platform: 'claude',
description: '测试用Claude账户分组'
});
testData.groups.push(claudeGroup);
log(`✅ 创建Claude分组成功: ${claudeGroup.name} (ID: ${claudeGroup.id})`, 'success');
})
testData.groups.push(claudeGroup)
log(`✅ 创建Claude分组成功: ${claudeGroup.name} (ID: ${claudeGroup.id})`, 'success')
// 创建Gemini分组
const geminiGroup = await accountGroupService.createGroup({
name: TEST_PREFIX + 'Gemini组',
name: `${TEST_PREFIX}Gemini组`,
platform: 'gemini',
description: '测试用Gemini账户分组'
});
testData.groups.push(geminiGroup);
log(`✅ 创建Gemini分组成功: ${geminiGroup.name} (ID: ${geminiGroup.id})`, 'success');
})
testData.groups.push(geminiGroup)
log(`✅ 创建Gemini分组成功: ${geminiGroup.name} (ID: ${geminiGroup.id})`, 'success')
// 验证分组信息
const allGroups = await accountGroupService.getAllGroups();
const testGroups = allGroups.filter(g => g.name.startsWith(TEST_PREFIX));
const allGroups = await accountGroupService.getAllGroups()
const testGroups = allGroups.filter((g) => g.name.startsWith(TEST_PREFIX))
if (testGroups.length === 2) {
log(`✅ 分组创建验证通过,共创建 ${testGroups.length} 个测试分组`, 'success');
log(`✅ 分组创建验证通过,共创建 ${testGroups.length} 个测试分组`, 'success')
} else {
throw new Error(`分组数量不正确期望2个实际${testGroups.length}`);
throw new Error(`分组数量不正确期望2个实际${testGroups.length}`)
}
} catch (error) {
log(`❌ 测试1失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试1失败: ${error.message}`, 'error')
throw error
}
}
// 测试2: 创建账户并添加到分组
async function test2_createAccountsAndAddToGroup() {
log('\n📝 测试2: 创建账户并添加到分组', 'info');
log('\n📝 测试2: 创建账户并添加到分组', 'info')
try {
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
const claudeGroup = testData.groups.find((g) => g.platform === 'claude')
// 创建Claude OAuth账户
const claudeAccount1 = await claudeAccountService.createAccount({
name: TEST_PREFIX + 'Claude账户1',
name: `${TEST_PREFIX}Claude账户1`,
email: 'test1@example.com',
refreshToken: 'test_refresh_token_1',
accountType: 'group'
});
testData.accounts.push({ ...claudeAccount1, type: 'claude' });
log(`✅ 创建Claude OAuth账户1成功: ${claudeAccount1.name}`, 'success');
})
testData.accounts.push({ ...claudeAccount1, type: 'claude' })
log(`✅ 创建Claude OAuth账户1成功: ${claudeAccount1.name}`, 'success')
const claudeAccount2 = await claudeAccountService.createAccount({
name: TEST_PREFIX + 'Claude账户2',
name: `${TEST_PREFIX}Claude账户2`,
email: 'test2@example.com',
refreshToken: 'test_refresh_token_2',
accountType: 'group'
});
testData.accounts.push({ ...claudeAccount2, type: 'claude' });
log(`✅ 创建Claude OAuth账户2成功: ${claudeAccount2.name}`, 'success');
})
testData.accounts.push({ ...claudeAccount2, type: 'claude' })
log(`✅ 创建Claude OAuth账户2成功: ${claudeAccount2.name}`, 'success')
// 创建Claude Console账户
const consoleAccount = await claudeConsoleAccountService.createAccount({
name: TEST_PREFIX + 'Console账户',
name: `${TEST_PREFIX}Console账户`,
apiUrl: 'https://api.example.com',
apiKey: 'test_api_key',
accountType: 'group'
});
testData.accounts.push({ ...consoleAccount, type: 'claude-console' });
log(`✅ 创建Claude Console账户成功: ${consoleAccount.name}`, 'success');
})
testData.accounts.push({ ...consoleAccount, type: 'claude-console' })
log(`✅ 创建Claude Console账户成功: ${consoleAccount.name}`, 'success')
// 添加账户到分组
await accountGroupService.addAccountToGroup(claudeAccount1.id, claudeGroup.id, 'claude');
log(`✅ 添加账户1到分组成功`, 'success');
await accountGroupService.addAccountToGroup(claudeAccount2.id, claudeGroup.id, 'claude');
log(`✅ 添加账户2到分组成功`, 'success');
await accountGroupService.addAccountToGroup(consoleAccount.id, claudeGroup.id, 'claude');
log(`✅ 添加Console账户到分组成功`, 'success');
await accountGroupService.addAccountToGroup(claudeAccount1.id, claudeGroup.id, 'claude')
log('✅ 添加账户1到分组成功', 'success')
await accountGroupService.addAccountToGroup(claudeAccount2.id, claudeGroup.id, 'claude')
log('✅ 添加账户2到分组成功', 'success')
await accountGroupService.addAccountToGroup(consoleAccount.id, claudeGroup.id, 'claude')
log('✅ 添加Console账户到分组成功', 'success')
// 验证分组成员
const members = await accountGroupService.getGroupMembers(claudeGroup.id);
const members = await accountGroupService.getGroupMembers(claudeGroup.id)
if (members.length === 3) {
log(`✅ 分组成员验证通过,共有 ${members.length} 个成员`, 'success');
log(`✅ 分组成员验证通过,共有 ${members.length} 个成员`, 'success')
} else {
throw new Error(`分组成员数量不正确期望3个实际${members.length}`);
throw new Error(`分组成员数量不正确期望3个实际${members.length}`)
}
} catch (error) {
log(`❌ 测试2失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试2失败: ${error.message}`, 'error')
throw error
}
}
// 测试3: 平台一致性验证
async function test3_platformConsistency() {
log('\n📝 测试3: 平台一致性验证', 'info');
log('\n📝 测试3: 平台一致性验证', 'info')
try {
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
const geminiGroup = testData.groups.find(g => g.platform === 'gemini');
const geminiGroup = testData.groups.find((g) => g.platform === 'gemini')
// 尝试将Claude账户添加到Gemini分组应该失败
const claudeAccount = testData.accounts.find(a => a.type === 'claude');
const claudeAccount = testData.accounts.find((a) => a.type === 'claude')
try {
await accountGroupService.addAccountToGroup(claudeAccount.id, geminiGroup.id, 'claude');
throw new Error('平台验证失败Claude账户不应该能添加到Gemini分组');
await accountGroupService.addAccountToGroup(claudeAccount.id, geminiGroup.id, 'claude')
throw new Error('平台验证失败Claude账户不应该能添加到Gemini分组')
} catch (error) {
if (error.message.includes('平台与分组平台不匹配')) {
log(`✅ 平台一致性验证通过:${error.message}`, 'success');
log(`✅ 平台一致性验证通过:${error.message}`, 'success')
} else {
throw error;
throw error
}
}
} catch (error) {
log(`❌ 测试3失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试3失败: ${error.message}`, 'error')
throw error
}
}
// 测试4: API Key绑定分组
async function test4_apiKeyBindGroup() {
log('\n📝 测试4: API Key绑定分组', 'info');
log('\n📝 测试4: API Key绑定分组', 'info')
try {
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
const claudeGroup = testData.groups.find((g) => g.platform === 'claude')
// 创建绑定到分组的API Key
const apiKey = await apiKeyService.generateApiKey({
name: TEST_PREFIX + 'API Key',
name: `${TEST_PREFIX}API Key`,
description: '测试分组调度的API Key',
claudeAccountId: `group:${claudeGroup.id}`,
permissions: 'claude'
});
testData.apiKeys.push(apiKey);
log(`✅ 创建API Key成功: ${apiKey.name} (绑定到分组: ${claudeGroup.name})`, 'success');
})
testData.apiKeys.push(apiKey)
log(`✅ 创建API Key成功: ${apiKey.name} (绑定到分组: ${claudeGroup.name})`, 'success')
// 验证API Key信息
const keyInfo = await redis.getApiKey(apiKey.id);
const keyInfo = await redis.getApiKey(apiKey.id)
if (keyInfo && keyInfo.claudeAccountId === `group:${claudeGroup.id}`) {
log(`✅ API Key分组绑定验证通过`, 'success');
log('✅ API Key分组绑定验证通过', 'success')
} else {
throw new Error('API Key分组绑定信息不正确');
throw new Error('API Key分组绑定信息不正确')
}
} catch (error) {
log(`❌ 测试4失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试4失败: ${error.message}`, 'error')
throw error
}
}
// 测试5: 分组调度负载均衡
async function test5_groupSchedulingLoadBalance() {
log('\n📝 测试5: 分组调度负载均衡', 'info');
log('\n📝 测试5: 分组调度负载均衡', 'info')
try {
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
const apiKey = testData.apiKeys[0];
const apiKey = testData.apiKeys[0]
// 记录每个账户被选中的次数
const selectionCount = {};
const totalSelections = 30;
const selectionCount = {}
const totalSelections = 30
for (let i = 0; i < totalSelections; i++) {
// 模拟不同的会话
const sessionHash = uuidv4();
const result = await unifiedClaudeScheduler.selectAccountForApiKey({
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
}, sessionHash);
const sessionHash = uuidv4()
const result = await unifiedClaudeScheduler.selectAccountForApiKey(
{
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
},
sessionHash
)
if (!selectionCount[result.accountId]) {
selectionCount[result.accountId] = 0;
selectionCount[result.accountId] = 0
}
selectionCount[result.accountId]++;
selectionCount[result.accountId]++
// 短暂延迟,模拟真实请求间隔
await sleep(50);
await sleep(50)
}
// 分析选择分布
log(`\n📊 负载均衡分布统计 (共${totalSelections}次选择):`, 'info');
const accounts = Object.keys(selectionCount);
log(`\n📊 负载均衡分布统计 (共${totalSelections}次选择):`, 'info')
const accounts = Object.keys(selectionCount)
for (const accountId of accounts) {
const count = selectionCount[accountId];
const percentage = ((count / totalSelections) * 100).toFixed(1);
const accountInfo = testData.accounts.find(a => a.id === accountId);
log(` ${accountInfo.name}: ${count}次 (${percentage}%)`, 'info');
const count = selectionCount[accountId]
const percentage = ((count / totalSelections) * 100).toFixed(1)
const accountInfo = testData.accounts.find((a) => a.id === accountId)
log(` ${accountInfo.name}: ${count}次 (${percentage}%)`, 'info')
}
// 验证是否实现了负载均衡
const counts = Object.values(selectionCount);
const avgCount = totalSelections / accounts.length;
const variance = counts.reduce((sum, count) => sum + Math.pow(count - avgCount, 2), 0) / counts.length;
const stdDev = Math.sqrt(variance);
log(`\n 平均选择次数: ${avgCount.toFixed(1)}`, 'info');
log(` 标准差: ${stdDev.toFixed(1)}`, 'info');
const counts = Object.values(selectionCount)
const avgCount = totalSelections / accounts.length
const variance =
counts.reduce((sum, count) => sum + Math.pow(count - avgCount, 2), 0) / counts.length
const stdDev = Math.sqrt(variance)
log(`\n 平均选择次数: ${avgCount.toFixed(1)}`, 'info')
log(` 标准差: ${stdDev.toFixed(1)}`, 'info')
// 如果标准差小于平均值的50%,认为负载均衡效果良好
if (stdDev < avgCount * 0.5) {
log(`✅ 负载均衡验证通过,分布相对均匀`, 'success');
log('✅ 负载均衡验证通过,分布相对均匀', 'success')
} else {
log(`⚠️ 负载分布不够均匀,但这可能是正常的随机波动`, 'warning');
log('⚠️ 负载分布不够均匀,但这可能是正常的随机波动', 'warning')
}
} catch (error) {
log(`❌ 测试5失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试5失败: ${error.message}`, 'error')
throw error
}
}
// 测试6: 会话粘性测试
async function test6_stickySession() {
log('\n📝 测试6: 会话粘性Sticky Session测试', 'info');
log('\n📝 测试6: 会话粘性Sticky Session测试', 'info')
try {
const apiKey = testData.apiKeys[0];
const sessionHash = 'test_session_' + uuidv4();
const apiKey = testData.apiKeys[0]
const sessionHash = `test_session_${uuidv4()}`
// 第一次选择
const firstSelection = await unifiedClaudeScheduler.selectAccountForApiKey({
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
}, sessionHash);
log(` 首次选择账户: ${firstSelection.accountId}`, 'info');
// 使用相同的sessionHash多次请求
let consistentCount = 0;
const testCount = 10;
for (let i = 0; i < testCount; i++) {
const selection = await unifiedClaudeScheduler.selectAccountForApiKey({
const firstSelection = await unifiedClaudeScheduler.selectAccountForApiKey(
{
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
}, sessionHash);
},
sessionHash
)
log(` 首次选择账户: ${firstSelection.accountId}`, 'info')
// 使用相同的sessionHash多次请求
let consistentCount = 0
const testCount = 10
for (let i = 0; i < testCount; i++) {
const selection = await unifiedClaudeScheduler.selectAccountForApiKey(
{
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
},
sessionHash
)
if (selection.accountId === firstSelection.accountId) {
consistentCount++;
consistentCount++
}
await sleep(100);
await sleep(100)
}
log(` 会话一致性: ${consistentCount}/${testCount} 次选择了相同账户`, 'info');
log(` 会话一致性: ${consistentCount}/${testCount} 次选择了相同账户`, 'info')
if (consistentCount === testCount) {
log(`✅ 会话粘性验证通过,同一会话始终选择相同账户`, 'success');
log('✅ 会话粘性验证通过,同一会话始终选择相同账户', 'success')
} else {
throw new Error(`会话粘性失败,只有${consistentCount}/${testCount}次选择了相同账户`);
throw new Error(`会话粘性失败,只有${consistentCount}/${testCount}次选择了相同账户`)
}
} catch (error) {
log(`❌ 测试6失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试6失败: ${error.message}`, 'error')
throw error
}
}
// 测试7: 账户可用性检查
async function test7_accountAvailability() {
log('\n📝 测试7: 账户可用性检查', 'info');
log('\n📝 测试7: 账户可用性检查', 'info')
try {
const apiKey = testData.apiKeys[0];
const accounts = testData.accounts.filter(a => a.type === 'claude' || a.type === 'claude-console');
const apiKey = testData.apiKeys[0]
const accounts = testData.accounts.filter(
(a) => a.type === 'claude' || a.type === 'claude-console'
)
// 禁用第一个账户
const firstAccount = accounts[0];
const firstAccount = accounts[0]
if (firstAccount.type === 'claude') {
await claudeAccountService.updateAccount(firstAccount.id, { isActive: false });
await claudeAccountService.updateAccount(firstAccount.id, { isActive: false })
} else {
await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: false });
await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: false })
}
log(` 已禁用账户: ${firstAccount.name}`, 'info');
log(` 已禁用账户: ${firstAccount.name}`, 'info')
// 多次选择,验证不会选择到禁用的账户
const selectionResults = [];
const selectionResults = []
for (let i = 0; i < 20; i++) {
const sessionHash = uuidv4(); // 每次使用新会话
const result = await unifiedClaudeScheduler.selectAccountForApiKey({
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
}, sessionHash);
selectionResults.push(result.accountId);
const sessionHash = uuidv4() // 每次使用新会话
const result = await unifiedClaudeScheduler.selectAccountForApiKey(
{
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
},
sessionHash
)
selectionResults.push(result.accountId)
}
// 检查是否选择了禁用的账户
const selectedDisabled = selectionResults.includes(firstAccount.id);
const selectedDisabled = selectionResults.includes(firstAccount.id)
if (!selectedDisabled) {
log(`✅ 账户可用性验证通过,未选择禁用的账户`, 'success');
log('✅ 账户可用性验证通过,未选择禁用的账户', 'success')
} else {
throw new Error('错误:选择了已禁用的账户');
throw new Error('错误:选择了已禁用的账户')
}
// 重新启用账户
if (firstAccount.type === 'claude') {
await claudeAccountService.updateAccount(firstAccount.id, { isActive: true });
await claudeAccountService.updateAccount(firstAccount.id, { isActive: true })
} else {
await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: true });
await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: true })
}
} catch (error) {
log(`❌ 测试7失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试7失败: ${error.message}`, 'error')
throw error
}
}
// 测试8: 分组成员管理
async function test8_groupMemberManagement() {
log('\n📝 测试8: 分组成员管理', 'info');
log('\n📝 测试8: 分组成员管理', 'info')
try {
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
const account = testData.accounts.find(a => a.type === 'claude');
const claudeGroup = testData.groups.find((g) => g.platform === 'claude')
const account = testData.accounts.find((a) => a.type === 'claude')
// 获取账户所属分组
const accountGroup = await accountGroupService.getAccountGroup(account.id);
const accountGroup = await accountGroupService.getAccountGroup(account.id)
if (accountGroup && accountGroup.id === claudeGroup.id) {
log(`✅ 账户分组查询验证通过`, 'success');
log('✅ 账户分组查询验证通过', 'success')
} else {
throw new Error('账户分组查询结果不正确');
throw new Error('账户分组查询结果不正确')
}
// 从分组移除账户
await accountGroupService.removeAccountFromGroup(account.id, claudeGroup.id);
log(` 从分组移除账户: ${account.name}`, 'info');
await accountGroupService.removeAccountFromGroup(account.id, claudeGroup.id)
log(` 从分组移除账户: ${account.name}`, 'info')
// 验证账户已不在分组中
const membersAfterRemove = await accountGroupService.getGroupMembers(claudeGroup.id);
const membersAfterRemove = await accountGroupService.getGroupMembers(claudeGroup.id)
if (!membersAfterRemove.includes(account.id)) {
log(`✅ 账户移除验证通过`, 'success');
log('✅ 账户移除验证通过', 'success')
} else {
throw new Error('账户移除失败');
throw new Error('账户移除失败')
}
// 重新添加账户
await accountGroupService.addAccountToGroup(account.id, claudeGroup.id, 'claude');
log(` 重新添加账户到分组`, 'info');
await accountGroupService.addAccountToGroup(account.id, claudeGroup.id, 'claude')
log(' 重新添加账户到分组', 'info')
} catch (error) {
log(`❌ 测试8失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试8失败: ${error.message}`, 'error')
throw error
}
}
// 测试9: 空分组处理
async function test9_emptyGroupHandling() {
log('\n📝 测试9: 空分组处理', 'info');
log('\n📝 测试9: 空分组处理', 'info')
try {
// 创建一个空分组
const emptyGroup = await accountGroupService.createGroup({
name: TEST_PREFIX + '空分组',
name: `${TEST_PREFIX}空分组`,
platform: 'claude',
description: '测试空分组'
});
testData.groups.push(emptyGroup);
})
testData.groups.push(emptyGroup)
// 创建绑定到空分组的API Key
const apiKey = await apiKeyService.generateApiKey({
name: TEST_PREFIX + '空分组API Key',
name: `${TEST_PREFIX}空分组API Key`,
claudeAccountId: `group:${emptyGroup.id}`,
permissions: 'claude'
});
testData.apiKeys.push(apiKey);
})
testData.apiKeys.push(apiKey)
// 尝试从空分组选择账户(应该失败)
try {
await unifiedClaudeScheduler.selectAccountForApiKey({
id: apiKey.id,
claudeAccountId: apiKey.claudeAccountId,
name: apiKey.name
});
throw new Error('空分组选择账户应该失败');
})
throw new Error('空分组选择账户应该失败')
} catch (error) {
if (error.message.includes('has no members')) {
log(`✅ 空分组处理验证通过:${error.message}`, 'success');
log(`✅ 空分组处理验证通过:${error.message}`, 'success')
} else {
throw error;
throw error
}
}
} catch (error) {
log(`❌ 测试9失败: ${error.message}`, 'error');
throw error;
log(`❌ 测试9失败: ${error.message}`, 'error')
throw error
}
}
// 主测试函数
async function runTests() {
log('\n🚀 开始分组调度功能测试\n', 'info');
log('\n🚀 开始分组调度功能测试\n', 'info')
try {
// 连接Redis
await redis.connect();
log('✅ Redis连接成功', 'success');
await redis.connect()
log('✅ Redis连接成功', 'success')
// 执行测试
await test1_createGroups();
await test2_createAccountsAndAddToGroup();
await test3_platformConsistency();
await test4_apiKeyBindGroup();
await test5_groupSchedulingLoadBalance();
await test6_stickySession();
await test7_accountAvailability();
await test8_groupMemberManagement();
await test9_emptyGroupHandling();
log('\n🎉 所有测试通过!分组调度功能工作正常', 'success');
await test1_createGroups()
await test2_createAccountsAndAddToGroup()
await test3_platformConsistency()
await test4_apiKeyBindGroup()
await test5_groupSchedulingLoadBalance()
await test6_stickySession()
await test7_accountAvailability()
await test8_groupMemberManagement()
await test9_emptyGroupHandling()
log('\n🎉 所有测试通过!分组调度功能工作正常', 'success')
} catch (error) {
log(`\n❌ 测试失败: ${error.message}`, 'error');
console.error(error);
log(`\n❌ 测试失败: ${error.message}`, 'error')
console.error(error)
} finally {
// 清理测试数据
if (CLEANUP_ON_FINISH) {
await cleanup();
await cleanup()
} else {
log('\n⚠ 测试数据未清理,请手动清理', 'warning');
log('\n⚠ 测试数据未清理,请手动清理', 'warning')
}
// 关闭Redis连接
await redis.disconnect();
process.exit(0);
await redis.disconnect()
process.exit(0)
}
}
// 运行测试
runTests();
runTests()

View File

@@ -1,47 +1,47 @@
#!/usr/bin/env node
const bedrockRelayService = require('../src/services/bedrockRelayService');
const bedrockRelayService = require('../src/services/bedrockRelayService')
function testModelMapping() {
console.log('🧪 测试模型映射功能...');
console.log('🧪 测试模型映射功能...')
// 测试用例
const testCases = [
// 标准Claude模型名
'claude-3-5-haiku-20241022',
'claude-3-5-sonnet-20241022',
'claude-3-5-sonnet-20241022',
'claude-3-5-sonnet',
'claude-3-5-haiku',
'claude-sonnet-4',
'claude-opus-4-1',
'claude-3-7-sonnet',
// 已经是Bedrock格式的
'us.anthropic.claude-sonnet-4-20250514-v1:0',
'anthropic.claude-3-5-haiku-20241022-v1:0',
// 未知模型
'unknown-model'
];
]
console.log('\n📋 模型映射测试结果:');
testCases.forEach(testModel => {
const mappedModel = bedrockRelayService._mapToBedrockModel(testModel);
const isChanged = mappedModel !== testModel;
const status = isChanged ? '🔄' : '✅';
console.log(`${status} ${testModel}`);
console.log('\n📋 模型映射测试结果:')
testCases.forEach((testModel) => {
const mappedModel = bedrockRelayService._mapToBedrockModel(testModel)
const isChanged = mappedModel !== testModel
const status = isChanged ? '🔄' : '✅'
console.log(`${status} ${testModel}`)
if (isChanged) {
console.log(`${mappedModel}`);
console.log(`${mappedModel}`)
}
});
})
console.log('\n✅ 模型映射测试完成');
console.log('\n✅ 模型映射测试完成')
}
// 如果直接运行此脚本
if (require.main === module) {
testModelMapping();
testModelMapping()
}
module.exports = { testModelMapping };
module.exports = { testModelMapping }

View File

@@ -1,92 +1,91 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const fs = require('fs')
const path = require('path')
// 测试定价服务的fallback机制
async function testPricingFallback() {
console.log('🧪 Testing pricing service fallback mechanism...\n');
console.log('🧪 Testing pricing service fallback mechanism...\n')
// 备份现有的模型定价文件
const dataDir = path.join(process.cwd(), 'data');
const pricingFile = path.join(dataDir, 'model_pricing.json');
const backupFile = path.join(dataDir, 'model_pricing.backup.json');
const dataDir = path.join(process.cwd(), 'data')
const pricingFile = path.join(dataDir, 'model_pricing.json')
const backupFile = path.join(dataDir, 'model_pricing.backup.json')
// 1. 备份现有文件
if (fs.existsSync(pricingFile)) {
console.log('📦 Backing up existing pricing file...');
fs.copyFileSync(pricingFile, backupFile);
console.log('📦 Backing up existing pricing file...')
fs.copyFileSync(pricingFile, backupFile)
}
try {
// 2. 删除现有定价文件以触发fallback
if (fs.existsSync(pricingFile)) {
console.log('🗑️ Removing existing pricing file to test fallback...');
fs.unlinkSync(pricingFile);
console.log('🗑️ Removing existing pricing file to test fallback...')
fs.unlinkSync(pricingFile)
}
// 3. 初始化定价服务
console.log('🚀 Initializing pricing service...\n');
console.log('🚀 Initializing pricing service...\n')
// 清除require缓存以确保重新加载
delete require.cache[require.resolve('../src/services/pricingService')];
const pricingService = require('../src/services/pricingService');
delete require.cache[require.resolve('../src/services/pricingService')]
const pricingService = require('../src/services/pricingService')
// 模拟网络失败强制使用fallback
const originalDownload = pricingService._downloadFromRemote;
pricingService._downloadFromRemote = function() {
return Promise.reject(new Error('Simulated network failure for testing'));
};
const originalDownload = pricingService._downloadFromRemote
pricingService._downloadFromRemote = function () {
return Promise.reject(new Error('Simulated network failure for testing'))
}
// 初始化服务
await pricingService.initialize();
await pricingService.initialize()
// 4. 验证fallback是否工作
console.log('\n📊 Verifying fallback data...');
const status = pricingService.getStatus();
console.log(` - Initialized: ${status.initialized}`);
console.log(` - Model count: ${status.modelCount}`);
console.log(` - Last updated: ${status.lastUpdated}`);
console.log('\n📊 Verifying fallback data...')
const status = pricingService.getStatus()
console.log(` - Initialized: ${status.initialized}`)
console.log(` - Model count: ${status.modelCount}`)
console.log(` - Last updated: ${status.lastUpdated}`)
// 5. 测试获取模型定价
const testModels = ['claude-3-opus-20240229', 'gpt-4', 'gemini-pro'];
console.log('\n💰 Testing model pricing retrieval:');
const testModels = ['claude-3-opus-20240229', 'gpt-4', 'gemini-pro']
console.log('\n💰 Testing model pricing retrieval:')
for (const model of testModels) {
const pricing = pricingService.getModelPricing(model);
const pricing = pricingService.getModelPricing(model)
if (pricing) {
console.log(`${model}: Found pricing data`);
console.log(`${model}: Found pricing data`)
} else {
console.log(`${model}: No pricing data`);
console.log(`${model}: No pricing data`)
}
}
// 6. 验证文件是否被创建
if (fs.existsSync(pricingFile)) {
console.log('\n✅ Fallback successfully created pricing file in data directory');
const fileStats = fs.statSync(pricingFile);
console.log(` - File size: ${(fileStats.size / 1024).toFixed(2)} KB`);
console.log('\n✅ Fallback successfully created pricing file in data directory')
const fileStats = fs.statSync(pricingFile)
console.log(` - File size: ${(fileStats.size / 1024).toFixed(2)} KB`)
} else {
console.log('\n❌ Fallback failed to create pricing file');
console.log('\n❌ Fallback failed to create pricing file')
}
// 恢复原始下载函数
pricingService._downloadFromRemote = originalDownload;
pricingService._downloadFromRemote = originalDownload
} finally {
// 7. 恢复备份文件
if (fs.existsSync(backupFile)) {
console.log('\n📦 Restoring original pricing file...');
fs.copyFileSync(backupFile, pricingFile);
fs.unlinkSync(backupFile);
console.log('\n📦 Restoring original pricing file...')
fs.copyFileSync(backupFile, pricingFile)
fs.unlinkSync(backupFile)
}
}
console.log('\n✨ Fallback mechanism test completed!');
console.log('\n✨ Fallback mechanism test completed!')
}
// 运行测试
testPricingFallback().catch(error => {
console.error('❌ Test failed:', error);
process.exit(1);
});
testPricingFallback().catch((error) => {
console.error('❌ Test failed:', error)
process.exit(1)
})

View File

@@ -5,9 +5,9 @@
* 从 LiteLLM 仓库下载最新的模型价格和上下文窗口信息
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const fs = require('fs')
const path = require('path')
const https = require('https')
// 颜色输出
const colors = {
@@ -18,7 +18,7 @@ const colors = {
yellow: '\x1b[33m',
blue: '\x1b[36m',
magenta: '\x1b[35m'
};
}
// 日志函数
const log = {
@@ -26,23 +26,29 @@ const log = {
success: (msg) => console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`),
error: (msg) => console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`),
warn: (msg) => console.warn(`${colors.yellow}[WARNING]${colors.reset} ${msg}`)
};
}
// 配置
const config = {
dataDir: path.join(process.cwd(), 'data'),
pricingFile: path.join(process.cwd(), 'data', 'model_pricing.json'),
pricingUrl: 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json',
fallbackFile: path.join(process.cwd(), 'resources', 'model-pricing', 'model_prices_and_context_window.json'),
pricingUrl:
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json',
fallbackFile: path.join(
process.cwd(),
'resources',
'model-pricing',
'model_prices_and_context_window.json'
),
backupFile: path.join(process.cwd(), 'data', 'model_pricing.backup.json'),
timeout: 30000 // 30秒超时
};
}
// 创建数据目录
function ensureDataDir() {
if (!fs.existsSync(config.dataDir)) {
fs.mkdirSync(config.dataDir, { recursive: true });
log.info('Created data directory');
fs.mkdirSync(config.dataDir, { recursive: true })
log.info('Created data directory')
}
}
@@ -50,213 +56,217 @@ function ensureDataDir() {
function backupExistingFile() {
if (fs.existsSync(config.pricingFile)) {
try {
fs.copyFileSync(config.pricingFile, config.backupFile);
log.info('Backed up existing pricing file');
return true;
fs.copyFileSync(config.pricingFile, config.backupFile)
log.info('Backed up existing pricing file')
return true
} catch (error) {
log.warn(`Failed to backup existing file: ${error.message}`);
return false;
log.warn(`Failed to backup existing file: ${error.message}`)
return false
}
}
return false;
return false
}
// 恢复备份
function restoreBackup() {
if (fs.existsSync(config.backupFile)) {
try {
fs.copyFileSync(config.backupFile, config.pricingFile);
log.info('Restored from backup');
return true;
fs.copyFileSync(config.backupFile, config.pricingFile)
log.info('Restored from backup')
return true
} catch (error) {
log.error(`Failed to restore backup: ${error.message}`);
return false;
log.error(`Failed to restore backup: ${error.message}`)
return false
}
}
return false;
return false
}
// 下载价格数据
function downloadPricingData() {
return new Promise((resolve, reject) => {
log.info(`Downloading model pricing data from LiteLLM...`);
log.info(`URL: ${config.pricingUrl}`);
log.info('Downloading model pricing data from LiteLLM...')
log.info(`URL: ${config.pricingUrl}`)
const request = https.get(config.pricingUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`))
return
}
let data = '';
let downloadedBytes = 0;
let data = ''
let downloadedBytes = 0
response.on('data', (chunk) => {
data += chunk;
downloadedBytes += chunk.length;
data += chunk
downloadedBytes += chunk.length
// 显示下载进度
process.stdout.write(`\rDownloading... ${Math.round(downloadedBytes / 1024)}KB`);
});
process.stdout.write(`\rDownloading... ${Math.round(downloadedBytes / 1024)}KB`)
})
response.on('end', () => {
process.stdout.write('\n'); // 换行
process.stdout.write('\n') // 换行
try {
const jsonData = JSON.parse(data);
const jsonData = JSON.parse(data)
// 验证数据结构
if (typeof jsonData !== 'object' || Object.keys(jsonData).length === 0) {
throw new Error('Invalid pricing data structure');
throw new Error('Invalid pricing data structure')
}
// 保存到文件
fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2));
const modelCount = Object.keys(jsonData).length;
const fileSize = Math.round(fs.statSync(config.pricingFile).size / 1024);
log.success(`Downloaded pricing data for ${modelCount} models (${fileSize}KB)`);
fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2))
const modelCount = Object.keys(jsonData).length
const fileSize = Math.round(fs.statSync(config.pricingFile).size / 1024)
log.success(`Downloaded pricing data for ${modelCount} models (${fileSize}KB)`)
// 显示一些统计信息
const claudeModels = Object.keys(jsonData).filter(k => k.includes('claude')).length;
const gptModels = Object.keys(jsonData).filter(k => k.includes('gpt')).length;
const geminiModels = Object.keys(jsonData).filter(k => k.includes('gemini')).length;
log.info(`Model breakdown:`);
log.info(` - Claude models: ${claudeModels}`);
log.info(` - GPT models: ${gptModels}`);
log.info(` - Gemini models: ${geminiModels}`);
log.info(` - Other models: ${modelCount - claudeModels - gptModels - geminiModels}`);
resolve(jsonData);
const claudeModels = Object.keys(jsonData).filter((k) => k.includes('claude')).length
const gptModels = Object.keys(jsonData).filter((k) => k.includes('gpt')).length
const geminiModels = Object.keys(jsonData).filter((k) => k.includes('gemini')).length
log.info('Model breakdown:')
log.info(` - Claude models: ${claudeModels}`)
log.info(` - GPT models: ${gptModels}`)
log.info(` - Gemini models: ${geminiModels}`)
log.info(` - Other models: ${modelCount - claudeModels - gptModels - geminiModels}`)
resolve(jsonData)
} catch (error) {
reject(new Error(`Failed to parse pricing data: ${error.message}`));
reject(new Error(`Failed to parse pricing data: ${error.message}`))
}
});
});
})
})
request.on('error', (error) => {
reject(new Error(`Network error: ${error.message}`));
});
reject(new Error(`Network error: ${error.message}`))
})
request.setTimeout(config.timeout, () => {
request.destroy();
reject(new Error(`Download timeout after ${config.timeout / 1000} seconds`));
});
});
request.destroy()
reject(new Error(`Download timeout after ${config.timeout / 1000} seconds`))
})
})
}
// 使用 fallback 文件
function useFallback() {
log.warn('Attempting to use fallback pricing data...');
log.warn('Attempting to use fallback pricing data...')
if (!fs.existsSync(config.fallbackFile)) {
log.error(`Fallback file not found: ${config.fallbackFile}`);
return false;
log.error(`Fallback file not found: ${config.fallbackFile}`)
return false
}
try {
const fallbackData = fs.readFileSync(config.fallbackFile, 'utf8');
const jsonData = JSON.parse(fallbackData);
const fallbackData = fs.readFileSync(config.fallbackFile, 'utf8')
const jsonData = JSON.parse(fallbackData)
// 保存到data目录
fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2));
const modelCount = Object.keys(jsonData).length;
log.warn(`Using fallback pricing data for ${modelCount} models`);
log.info('Note: Fallback data may be outdated. Try updating again later.');
return true;
fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2))
const modelCount = Object.keys(jsonData).length
log.warn(`Using fallback pricing data for ${modelCount} models`)
log.info('Note: Fallback data may be outdated. Try updating again later.')
return true
} catch (error) {
log.error(`Failed to use fallback: ${error.message}`);
return false;
log.error(`Failed to use fallback: ${error.message}`)
return false
}
}
// 显示当前状态
function showCurrentStatus() {
if (fs.existsSync(config.pricingFile)) {
const stats = fs.statSync(config.pricingFile);
const fileAge = Date.now() - stats.mtime.getTime();
const ageInHours = Math.round(fileAge / (60 * 60 * 1000));
const ageInDays = Math.floor(ageInHours / 24);
let ageString = '';
const stats = fs.statSync(config.pricingFile)
const fileAge = Date.now() - stats.mtime.getTime()
const ageInHours = Math.round(fileAge / (60 * 60 * 1000))
const ageInDays = Math.floor(ageInHours / 24)
let ageString = ''
if (ageInDays > 0) {
ageString = `${ageInDays} day${ageInDays > 1 ? 's' : ''} and ${ageInHours % 24} hour${(ageInHours % 24) !== 1 ? 's' : ''}`;
ageString = `${ageInDays} day${ageInDays > 1 ? 's' : ''} and ${ageInHours % 24} hour${ageInHours % 24 !== 1 ? 's' : ''}`
} else {
ageString = `${ageInHours} hour${ageInHours !== 1 ? 's' : ''}`;
ageString = `${ageInHours} hour${ageInHours !== 1 ? 's' : ''}`
}
log.info(`Current pricing file age: ${ageString}`);
log.info(`Current pricing file age: ${ageString}`)
try {
const data = JSON.parse(fs.readFileSync(config.pricingFile, 'utf8'));
log.info(`Current file contains ${Object.keys(data).length} models`);
const data = JSON.parse(fs.readFileSync(config.pricingFile, 'utf8'))
log.info(`Current file contains ${Object.keys(data).length} models`)
} catch (error) {
log.warn('Current file exists but could not be parsed');
log.warn('Current file exists but could not be parsed')
}
} else {
log.info('No existing pricing file found');
log.info('No existing pricing file found')
}
}
// 主函数
async function main() {
console.log(`${colors.bright}${colors.blue}======================================${colors.reset}`);
console.log(`${colors.bright} Model Pricing Update Tool${colors.reset}`);
console.log(`${colors.bright}${colors.blue}======================================${colors.reset}\n`);
console.log(`${colors.bright}${colors.blue}======================================${colors.reset}`)
console.log(`${colors.bright} Model Pricing Update Tool${colors.reset}`)
console.log(
`${colors.bright}${colors.blue}======================================${colors.reset}\n`
)
// 显示当前状态
showCurrentStatus();
console.log('');
showCurrentStatus()
console.log('')
// 确保数据目录存在
ensureDataDir();
ensureDataDir()
// 备份现有文件
const hasBackup = backupExistingFile();
const hasBackup = backupExistingFile()
try {
// 尝试下载最新数据
await downloadPricingData();
await downloadPricingData()
// 清理备份文件(成功下载后)
if (hasBackup && fs.existsSync(config.backupFile)) {
fs.unlinkSync(config.backupFile);
log.info('Cleaned up backup file');
fs.unlinkSync(config.backupFile)
log.info('Cleaned up backup file')
}
console.log(`\n${colors.green}✅ Model pricing updated successfully!${colors.reset}`);
process.exit(0);
console.log(`\n${colors.green}✅ Model pricing updated successfully!${colors.reset}`)
process.exit(0)
} catch (error) {
log.error(`Download failed: ${error.message}`);
log.error(`Download failed: ${error.message}`)
// 尝试恢复备份
if (hasBackup) {
if (restoreBackup()) {
log.info('Original file restored');
log.info('Original file restored')
}
}
// 尝试使用 fallback
if (useFallback()) {
console.log(`\n${colors.yellow}⚠️ Using fallback data (update completed with warnings)${colors.reset}`);
process.exit(0);
console.log(
`\n${colors.yellow}⚠️ Using fallback data (update completed with warnings)${colors.reset}`
)
process.exit(0)
} else {
console.log(`\n${colors.red}❌ Failed to update model pricing${colors.reset}`);
process.exit(1);
console.log(`\n${colors.red}❌ Failed to update model pricing${colors.reset}`)
process.exit(1)
}
}
}
// 处理未捕获的错误
process.on('unhandledRejection', (error) => {
log.error(`Unhandled error: ${error.message}`);
process.exit(1);
});
log.error(`Unhandled error: ${error.message}`)
process.exit(1)
})
// 运行主函数
main().catch((error) => {
log.error(`Fatal error: ${error.message}`);
process.exit(1);
});
log.error(`Fatal error: ${error.message}`)
process.exit(1)
})