mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
163
scripts/setup.js
163
scripts/setup.js
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user