mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge pull request #106 from kevinconan/main - feat: 账号管理页面,给Claude账号添加了会话窗口管理与显示
This commit is contained in:
597
scripts/analyze-log-sessions.js
Normal file
597
scripts/analyze-log-sessions.js
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从日志文件分析Claude账户请求时间的CLI工具
|
||||||
|
* 用于恢复会话窗口数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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(); // 记录会话信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析时间戳
|
||||||
|
parseTimestamp(line) {
|
||||||
|
const match = line.match(this.timestampPattern);
|
||||||
|
if (match) {
|
||||||
|
return new Date(match[1]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析单个日志文件
|
||||||
|
async analyzeLogFile(filePath) {
|
||||||
|
console.log(`📖 分析日志文件: ${filePath}`);
|
||||||
|
|
||||||
|
let fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
// 如果是gz文件,需要先解压
|
||||||
|
if (filePath.endsWith('.gz')) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
for await (const line of rl) {
|
||||||
|
lineCount++;
|
||||||
|
|
||||||
|
// 解析时间戳
|
||||||
|
const timestamp = this.parseTimestamp(line);
|
||||||
|
if (!timestamp) continue;
|
||||||
|
|
||||||
|
// 查找账户使用记录
|
||||||
|
const accountUsageMatch = line.match(this.accountUsagePattern);
|
||||||
|
if (accountUsageMatch) {
|
||||||
|
const accountName = accountUsageMatch[1];
|
||||||
|
const accountId = accountUsageMatch[2];
|
||||||
|
const sessionId = accountUsageMatch[3];
|
||||||
|
|
||||||
|
if (!this.accounts.has(accountId)) {
|
||||||
|
this.accounts.set(accountId, {
|
||||||
|
accountId,
|
||||||
|
accountName,
|
||||||
|
requests: [],
|
||||||
|
firstRequest: timestamp,
|
||||||
|
lastRequest: timestamp,
|
||||||
|
totalRequests: 0,
|
||||||
|
sessions: new Set()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = this.accounts.get(accountId);
|
||||||
|
account.sessions.add(sessionId);
|
||||||
|
|
||||||
|
if (timestamp < account.firstRequest) {
|
||||||
|
account.firstRequest = timestamp;
|
||||||
|
}
|
||||||
|
if (timestamp > account.lastRequest) {
|
||||||
|
account.lastRequest = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找请求处理记录
|
||||||
|
const processingMatch = line.match(this.processingPattern);
|
||||||
|
if (processingMatch) {
|
||||||
|
const apiKeyName = processingMatch[1];
|
||||||
|
const accountId = processingMatch[2];
|
||||||
|
const sessionId = processingMatch[3];
|
||||||
|
|
||||||
|
if (!this.accounts.has(accountId)) {
|
||||||
|
this.accounts.set(accountId, {
|
||||||
|
accountId,
|
||||||
|
accountName: 'Unknown',
|
||||||
|
requests: [],
|
||||||
|
firstRequest: timestamp,
|
||||||
|
lastRequest: timestamp,
|
||||||
|
totalRequests: 0,
|
||||||
|
sessions: new Set()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = this.accounts.get(accountId);
|
||||||
|
account.requests.push({
|
||||||
|
timestamp,
|
||||||
|
apiKeyName,
|
||||||
|
sessionId,
|
||||||
|
type: 'processing'
|
||||||
|
});
|
||||||
|
|
||||||
|
account.sessions.add(sessionId);
|
||||||
|
account.totalRequests++;
|
||||||
|
requestCount++;
|
||||||
|
|
||||||
|
if (timestamp > account.lastRequest) {
|
||||||
|
account.lastRequest = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录到全局请求历史
|
||||||
|
this.requestHistory.push({
|
||||||
|
timestamp,
|
||||||
|
accountId,
|
||||||
|
apiKeyName,
|
||||||
|
sessionId,
|
||||||
|
type: 'processing'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找请求完成记录
|
||||||
|
const completedMatch = line.match(this.completedPattern);
|
||||||
|
if (completedMatch) {
|
||||||
|
const duration = parseInt(completedMatch[1]);
|
||||||
|
const apiKeyName = completedMatch[2];
|
||||||
|
|
||||||
|
// 记录到全局请求历史
|
||||||
|
this.requestHistory.push({
|
||||||
|
timestamp,
|
||||||
|
apiKeyName,
|
||||||
|
duration,
|
||||||
|
type: 'completed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找使用统计记录
|
||||||
|
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++;
|
||||||
|
|
||||||
|
// 记录到全局请求历史
|
||||||
|
this.requestHistory.push({
|
||||||
|
timestamp,
|
||||||
|
type: 'usage',
|
||||||
|
model,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
totalTokens
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` 📊 解析完成: ${lineCount} 行, 找到 ${requestCount} 个请求记录, ${usageCount} 个使用统计`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析日志目录中的所有文件
|
||||||
|
async analyzeLogDirectory(logDir = './logs') {
|
||||||
|
console.log(`🔍 扫描日志目录: ${logDir}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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+$/)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort()
|
||||||
|
.reverse(); // 最新的文件优先
|
||||||
|
|
||||||
|
if (logFiles.length === 0) {
|
||||||
|
console.log('❌ 没有找到日志文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 读取日志目录失败: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析单个日志文件(支持直接传入文件路径)
|
||||||
|
async analyzeSingleFile(filePath) {
|
||||||
|
console.log(`🔍 分析单个日志文件: ${filePath}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.log('❌ 文件不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.analyzeLogFile(filePath);
|
||||||
|
|
||||||
|
} catch (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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析会话窗口
|
||||||
|
analyzeSessionWindows() {
|
||||||
|
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 windowGroups = new Map();
|
||||||
|
|
||||||
|
for (const request of requests) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为数组并排序
|
||||||
|
const windowArray = Array.from(windowGroups.values())
|
||||||
|
.sort((a, b) => b.windowStart - a.windowStart); // 最新的窗口优先
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
accountId,
|
||||||
|
accountName: accountData.accountName,
|
||||||
|
totalRequests: accountData.totalRequests,
|
||||||
|
firstRequest: accountData.firstRequest,
|
||||||
|
lastRequest: accountData.lastRequest,
|
||||||
|
sessions: accountData.sessions,
|
||||||
|
windows: windowArray,
|
||||||
|
currentActiveWindow: windowArray.find(w => w.isActive) || null,
|
||||||
|
mostRecentWindow: windowArray[0] || null
|
||||||
|
};
|
||||||
|
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.sort((a, b) => b.lastRequest - a.lastRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示分析结果
|
||||||
|
displayResults(results) {
|
||||||
|
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()}`);
|
||||||
|
|
||||||
|
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}%`);
|
||||||
|
} 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} 小时前`);
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ 无会话窗口数据`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示最近几个窗口
|
||||||
|
if (result.windows.length > 1) {
|
||||||
|
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}次请求)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示最近几个会话的API Key使用情况
|
||||||
|
const accountData = this.accounts.get(result.accountId);
|
||||||
|
if (accountData && accountData.requests && accountData.requests.length > 0) {
|
||||||
|
const recentRequests = accountData.requests.slice(-5); // 最近5个请求
|
||||||
|
const apiKeyStats = {};
|
||||||
|
|
||||||
|
for (const req of accountData.requests) {
|
||||||
|
if (!apiKeyStats[req.apiKeyName]) {
|
||||||
|
apiKeyStats[req.apiKeyName] = 0;
|
||||||
|
}
|
||||||
|
apiKeyStats[req.apiKeyName]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` 🔑 API Key使用统计:`);
|
||||||
|
for (const [keyName, count] of Object.entries(apiKeyStats)) {
|
||||||
|
console.log(` - ${keyName}: ${count} 次`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新Redis中的会话窗口数据
|
||||||
|
async updateRedisSessionWindows(results, dryRun = true) {
|
||||||
|
if (dryRun) {
|
||||||
|
console.log('🧪 模拟模式 - 不会实际更新Redis数据\n');
|
||||||
|
} else {
|
||||||
|
console.log('💾 更新Redis中的会话窗口数据...\n');
|
||||||
|
await redis.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(result.accountId);
|
||||||
|
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
console.log(`⚠️ 账户 ${result.accountId} 在Redis中不存在,跳过`);
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔄 处理账户: ${accountData.name || result.accountId}`);
|
||||||
|
|
||||||
|
// 确定要设置的会话窗口
|
||||||
|
let targetWindow = null;
|
||||||
|
|
||||||
|
if (result.currentActiveWindow) {
|
||||||
|
targetWindow = result.currentActiveWindow;
|
||||||
|
console.log(` ✅ 使用当前活跃窗口: ${targetWindow.windowStart.toLocaleString()} - ${targetWindow.windowEnd.toLocaleString()}`);
|
||||||
|
} else if (result.mostRecentWindow) {
|
||||||
|
const window = result.mostRecentWindow;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 如果最近窗口是在过去24小时内的,可以考虑恢复
|
||||||
|
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)}小时前过期)`);
|
||||||
|
} else {
|
||||||
|
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(` ✅ 已更新会话窗口数据`);
|
||||||
|
} else if (targetWindow) {
|
||||||
|
updatedCount++;
|
||||||
|
console.log(` 🧪 [模拟] 将设置会话窗口: ${targetWindow.windowStart.toLocaleString()} - ${targetWindow.windowEnd.toLocaleString()}`);
|
||||||
|
} else {
|
||||||
|
skippedCount++;
|
||||||
|
console.log(` ⏭️ 跳过(无有效窗口)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 处理账户 ${result.accountId} 时出错: ${error.message}`);
|
||||||
|
skippedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await redis.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Claude账户会话窗口分析工具\n');
|
||||||
|
|
||||||
|
// 分析日志文件
|
||||||
|
if (singleFile) {
|
||||||
|
await this.analyzeSingleFile(singleFile);
|
||||||
|
} else {
|
||||||
|
await this.analyzeLogDirectory(logDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.accounts.size === 0) {
|
||||||
|
console.log('❌ 没有找到任何Claude账户的请求记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析会话窗口
|
||||||
|
const results = this.analyzeSessionWindows();
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
this.displayResults(results);
|
||||||
|
|
||||||
|
// 更新Redis(如果需要)
|
||||||
|
if (updateRedis) {
|
||||||
|
await this.updateRedisSessionWindows(results, dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 分析失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令行参数解析
|
||||||
|
function parseArgs() {
|
||||||
|
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];
|
||||||
|
} else if (arg.startsWith('--file=')) {
|
||||||
|
options.singleFile = arg.split('=')[1];
|
||||||
|
} else if (arg === '--update-redis') {
|
||||||
|
options.updateRedis = true;
|
||||||
|
} else if (arg === '--no-dry-run') {
|
||||||
|
options.dryRun = false;
|
||||||
|
} else if (arg === '--help' || arg === '-h') {
|
||||||
|
showHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示帮助信息
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
Claude账户会话窗口日志分析工具
|
||||||
|
|
||||||
|
从日志文件中分析Claude账户的请求时间,计算会话窗口,并可选择性地更新Redis数据。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
node scripts/analyze-log-sessions.js [选项]
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--log-dir=PATH 日志文件目录 (默认: ./logs)
|
||||||
|
--file=PATH 分析单个日志文件
|
||||||
|
--update-redis 更新Redis中的会话窗口数据
|
||||||
|
--no-dry-run 实际执行Redis更新(默认为模拟模式)
|
||||||
|
--help, -h 显示此帮助信息
|
||||||
|
|
||||||
|
示例:
|
||||||
|
# 分析默认日志目录
|
||||||
|
node scripts/analyze-log-sessions.js
|
||||||
|
|
||||||
|
# 分析指定目录的日志
|
||||||
|
node scripts/analyze-log-sessions.js --log-dir=/path/to/logs
|
||||||
|
|
||||||
|
# 分析单个日志文件
|
||||||
|
node scripts/analyze-log-sessions.js --file=/path/to/logfile.log
|
||||||
|
|
||||||
|
# 模拟更新Redis数据(不实际更新)
|
||||||
|
node scripts/analyze-log-sessions.js --file=/path/to/logfile.log --update-redis
|
||||||
|
|
||||||
|
# 实际更新Redis数据
|
||||||
|
node scripts/analyze-log-sessions.js --file=/path/to/logfile.log --update-redis --no-dry-run
|
||||||
|
|
||||||
|
会话窗口规则:
|
||||||
|
- Claude官方规定每5小时为一个会话窗口
|
||||||
|
- 窗口按整点对齐(如 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('🎉 分析完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('💥 程序执行失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果直接运行此脚本
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LogSessionAnalyzer;
|
||||||
532
scripts/manage-session-windows.js
Normal file
532
scripts/manage-session-windows.js
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话窗口管理脚本
|
||||||
|
* 用于调试、恢复和管理Claude账户的会话窗口
|
||||||
|
*/
|
||||||
|
|
||||||
|
const redis = require('../src/models/redis');
|
||||||
|
const claudeAccountService = require('../src/services/claudeAccountService');
|
||||||
|
const logger = require('../src/utils/logger');
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:解析时间输入
|
||||||
|
function parseTimeInput(input) {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 如果是 HH:MM 格式
|
||||||
|
const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/);
|
||||||
|
if (timeMatch) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是相对时间(如 "2小时前")
|
||||||
|
const relativeMatch = input.match(/^(\d+)(小时|分钟)前$/);
|
||||||
|
if (relativeMatch) {
|
||||||
|
const amount = parseInt(relativeMatch[1]);
|
||||||
|
const unit = relativeMatch[2];
|
||||||
|
const time = new Date(now);
|
||||||
|
|
||||||
|
if (unit === '小时') {
|
||||||
|
time.setHours(time.getHours() - amount);
|
||||||
|
} else if (unit === '分钟') {
|
||||||
|
time.setMinutes(time.getMinutes() - amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 ISO 格式或其他日期格式
|
||||||
|
const parsedDate = new Date(input);
|
||||||
|
if (!isNaN(parsedDate.getTime())) {
|
||||||
|
return parsedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:显示可用的时间窗口选项
|
||||||
|
function showTimeWindowOptions() {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
// 调试所有账户的会话窗口状态
|
||||||
|
async debug() {
|
||||||
|
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' ? '✅ 活跃' : '❌ 禁用'}`);
|
||||||
|
|
||||||
|
if (account.sessionWindowStart && account.sessionWindowEnd) {
|
||||||
|
stats.hasWindow++;
|
||||||
|
const windowStart = new Date(account.sessionWindowStart);
|
||||||
|
const windowEnd = new Date(account.sessionWindowEnd);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
console.log(` 会话窗口: ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
|
||||||
|
console.log(` 窗口状态: ${now < windowEnd ? '✅ 活跃' : '❌ 已过期'}`);
|
||||||
|
} else {
|
||||||
|
console.log(` 会话窗口: ❌ 无`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}分钟前)`);
|
||||||
|
|
||||||
|
// 计算基于lastUsedAt的窗口
|
||||||
|
const windowStart = claudeAccountService._calculateSessionWindowStart(lastUsed);
|
||||||
|
const windowEnd = claudeAccountService._calculateSessionWindowEnd(windowStart);
|
||||||
|
|
||||||
|
if (now < windowEnd) {
|
||||||
|
stats.canRecover++;
|
||||||
|
console.log(` 可恢复窗口: ✅ ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}`);
|
||||||
|
} else {
|
||||||
|
stats.expired++;
|
||||||
|
console.log(` 可恢复窗口: ❌ 已过期 (${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化会话窗口(默认行为)
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
if (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}`);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.log(` 错误: ${result.error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除所有会话窗口
|
||||||
|
async clear() {
|
||||||
|
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}) 的会话窗口`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📊 清除完成: 共清除 ${clearedCount} 个账户的会话窗口`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建测试会话窗口(将lastUsedAt设置为当前时间)
|
||||||
|
async test() {
|
||||||
|
console.log('🧪 创建测试会话窗口...\n');
|
||||||
|
|
||||||
|
const accounts = await redis.getAllClaudeAccounts();
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
console.log('❌ 没有找到Claude账户');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
if (account.isActive === 'true') {
|
||||||
|
// 设置为当前时间(模拟刚刚使用)
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 测试完成: 为 ${updatedCount} 个活跃账户创建了测试会话窗口`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 手动设置账户的会话窗口
|
||||||
|
async set() {
|
||||||
|
console.log('🔧 手动设置会话窗口...\n');
|
||||||
|
|
||||||
|
// 获取所有账户
|
||||||
|
const accounts = await redis.getAllClaudeAccounts();
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
console.log('❌ 没有找到Claude账户');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示账户列表
|
||||||
|
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 accountIndex = await askQuestion('\n请选择账户 (输入编号): ');
|
||||||
|
const selectedIndex = parseInt(accountIndex) - 1;
|
||||||
|
|
||||||
|
if (selectedIndex < 0 || selectedIndex >= accounts.length) {
|
||||||
|
console.log('❌ 无效的账户编号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ? '✅ 活跃' : '❌ 已过期'}`);
|
||||||
|
} else {
|
||||||
|
console.log(`📊 当前会话窗口: ❌ 无`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示设置选项
|
||||||
|
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;
|
||||||
|
case '2':
|
||||||
|
await setCustomLastUsed(selectedAccount);
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
await setDirectWindow(selectedAccount);
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
|
await clearAccountWindow(selectedAccount);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置函数实现
|
||||||
|
|
||||||
|
// 使用预设时间窗口
|
||||||
|
async function setPresetWindow(account) {
|
||||||
|
showTimeWindowOptions();
|
||||||
|
|
||||||
|
const windowChoice = await askQuestion('请选择时间窗口 (1-5): ');
|
||||||
|
const windowIndex = parseInt(windowChoice) - 1;
|
||||||
|
|
||||||
|
if (windowIndex < 0 || windowIndex >= 5) {
|
||||||
|
console.log('❌ 无效的窗口选择');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const startHour = windowIndex * 5;
|
||||||
|
|
||||||
|
// 创建窗口开始时间
|
||||||
|
const windowStart = new Date(now);
|
||||||
|
windowStart.setHours(startHour, 0, 0, 0);
|
||||||
|
|
||||||
|
// 创建窗口结束时间
|
||||||
|
const windowEnd = new Date(windowStart);
|
||||||
|
windowEnd.setHours(windowEnd.getHours() + 5);
|
||||||
|
|
||||||
|
// 如果选择的窗口已经过期,则设置为明天的同一时间段
|
||||||
|
if (windowEnd <= now) {
|
||||||
|
windowStart.setDate(windowStart.getDate() + 1);
|
||||||
|
windowEnd.setDate(windowEnd.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 询问是否要设置为当前时间作为最后使用时间
|
||||||
|
const setLastUsed = await askQuestion('是否设置当前时间为最后使用时间? (y/N): ');
|
||||||
|
|
||||||
|
// 更新账户数据
|
||||||
|
account.sessionWindowStart = windowStart.toISOString();
|
||||||
|
account.sessionWindowEnd = windowEnd.toISOString();
|
||||||
|
account.lastRequestTime = now.toISOString();
|
||||||
|
|
||||||
|
if (setLastUsed.toLowerCase() === 'y' || setLastUsed.toLowerCase() === 'yes') {
|
||||||
|
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 ? '✅ 活跃' : '⏰ 未来窗口'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义最后使用时间
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!lastUsedTime) {
|
||||||
|
console.log('❌ 无效的时间格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于最后使用时间计算会话窗口
|
||||||
|
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 ? '✅ 活跃' : '❌ 已过期'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接设置窗口时间
|
||||||
|
async function setDirectWindow(account) {
|
||||||
|
console.log('\n⏰ 直接设置窗口时间:');
|
||||||
|
|
||||||
|
const startInput = await askQuestion('请输入窗口开始时间 (HH:MM): ');
|
||||||
|
const startTime = parseTimeInput(startInput);
|
||||||
|
|
||||||
|
if (!startTime) {
|
||||||
|
console.log('❌ 无效的开始时间格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动计算结束时间(开始时间+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): `);
|
||||||
|
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
|
||||||
|
console.log('❌ 已取消设置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 更新账户数据
|
||||||
|
account.sessionWindowStart = startTime.toISOString();
|
||||||
|
account.sessionWindowEnd = endTime.toISOString();
|
||||||
|
account.lastRequestTime = now.toISOString();
|
||||||
|
|
||||||
|
// 询问是否更新最后使用时间
|
||||||
|
const updateLastUsed = await askQuestion('是否将最后使用时间设置为窗口开始时间? (y/N): ');
|
||||||
|
if (updateLastUsed.toLowerCase() === 'y' || updateLastUsed.toLowerCase() === 'yes') {
|
||||||
|
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 ? '⏰ 未来窗口' : '❌ 已过期')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除账户会话窗口
|
||||||
|
async function clearAccountWindow(account) {
|
||||||
|
const confirm = await askQuestion(`确认清除账户 "${account.name}" 的会话窗口吗? (y/N): `);
|
||||||
|
|
||||||
|
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
|
||||||
|
console.log('❌ 已取消操作');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除会话窗口相关数据
|
||||||
|
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';
|
||||||
|
|
||||||
|
if (!commands[command]) {
|
||||||
|
console.error(`❌ 未知命令: ${command}`);
|
||||||
|
commands.help();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === 'help') {
|
||||||
|
commands.help();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 连接Redis
|
||||||
|
await redis.connect();
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
await commands[command]();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 执行失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await redis.disconnect();
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果直接运行此脚本
|
||||||
|
if (require.main === module) {
|
||||||
|
main().then(() => {
|
||||||
|
console.log('\n🎉 操作完成');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { commands };
|
||||||
@@ -62,6 +62,11 @@ class Application {
|
|||||||
logger.info(`💰 Cost initialization completed: ${result.processed} processed, ${result.errors} errors`);
|
logger.info(`💰 Cost initialization completed: ${result.processed} processed, ${result.errors} errors`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🕐 初始化Claude账户会话窗口
|
||||||
|
logger.info('🕐 Initializing Claude account session windows...');
|
||||||
|
const claudeAccountService = require('./services/claudeAccountService');
|
||||||
|
await claudeAccountService.initializeSessionWindows();
|
||||||
|
|
||||||
// 🛡️ 安全中间件
|
// 🛡️ 安全中间件
|
||||||
this.app.use(helmet({
|
this.app.use(helmet({
|
||||||
contentSecurityPolicy: false, // 允许内联样式和脚本
|
contentSecurityPolicy: false, // 允许内联样式和脚本
|
||||||
|
|||||||
@@ -270,8 +270,9 @@ class ClaudeAccountService {
|
|||||||
throw new Error('No access token available');
|
throw new Error('No access token available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后使用时间
|
// 更新最后使用时间和会话窗口
|
||||||
accountData.lastUsedAt = new Date().toISOString();
|
accountData.lastUsedAt = new Date().toISOString();
|
||||||
|
await this.updateSessionWindow(accountId, accountData);
|
||||||
await redis.setClaudeAccount(accountId, accountData);
|
await redis.setClaudeAccount(accountId, accountData);
|
||||||
|
|
||||||
return accessToken;
|
return accessToken;
|
||||||
@@ -286,11 +287,14 @@ class ClaudeAccountService {
|
|||||||
try {
|
try {
|
||||||
const accounts = await redis.getAllClaudeAccounts();
|
const accounts = await redis.getAllClaudeAccounts();
|
||||||
|
|
||||||
// 处理返回数据,移除敏感信息并添加限流状态
|
// 处理返回数据,移除敏感信息并添加限流状态和会话窗口信息
|
||||||
const processedAccounts = await Promise.all(accounts.map(async account => {
|
const processedAccounts = await Promise.all(accounts.map(async account => {
|
||||||
// 获取限流状态信息
|
// 获取限流状态信息
|
||||||
const rateLimitInfo = await this.getAccountRateLimitInfo(account.id);
|
const rateLimitInfo = await this.getAccountRateLimitInfo(account.id);
|
||||||
|
|
||||||
|
// 获取会话窗口信息
|
||||||
|
const sessionWindowInfo = await this.getSessionWindowInfo(account.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
@@ -310,7 +314,16 @@ class ClaudeAccountService {
|
|||||||
isRateLimited: rateLimitInfo.isRateLimited,
|
isRateLimited: rateLimitInfo.isRateLimited,
|
||||||
rateLimitedAt: rateLimitInfo.rateLimitedAt,
|
rateLimitedAt: rateLimitInfo.rateLimitedAt,
|
||||||
minutesRemaining: rateLimitInfo.minutesRemaining
|
minutesRemaining: rateLimitInfo.minutesRemaining
|
||||||
} : null
|
} : null,
|
||||||
|
// 添加会话窗口信息
|
||||||
|
sessionWindow: sessionWindowInfo || {
|
||||||
|
hasActiveWindow: false,
|
||||||
|
windowStart: null,
|
||||||
|
windowEnd: null,
|
||||||
|
progress: 0,
|
||||||
|
remainingTime: null,
|
||||||
|
lastRequestTime: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -817,6 +830,210 @@ class ClaudeAccountService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🕐 更新会话窗口
|
||||||
|
async updateSessionWindow(accountId, accountData = null) {
|
||||||
|
try {
|
||||||
|
// 如果没有传入accountData,从Redis获取
|
||||||
|
if (!accountData) {
|
||||||
|
accountData = await redis.getClaudeAccount(accountId);
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
throw new Error('Account not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentTime = now.getTime();
|
||||||
|
|
||||||
|
// 检查当前是否有活跃的会话窗口
|
||||||
|
if (accountData.sessionWindowStart && accountData.sessionWindowEnd) {
|
||||||
|
const windowEnd = new Date(accountData.sessionWindowEnd).getTime();
|
||||||
|
|
||||||
|
// 如果当前时间在窗口内,不需要更新
|
||||||
|
if (currentTime < windowEnd) {
|
||||||
|
accountData.lastRequestTime = now.toISOString();
|
||||||
|
return accountData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算新的会话窗口
|
||||||
|
const windowStart = this._calculateSessionWindowStart(now);
|
||||||
|
const windowEnd = this._calculateSessionWindowEnd(windowStart);
|
||||||
|
|
||||||
|
// 更新会话窗口信息
|
||||||
|
accountData.sessionWindowStart = windowStart.toISOString();
|
||||||
|
accountData.sessionWindowEnd = windowEnd.toISOString();
|
||||||
|
accountData.lastRequestTime = now.toISOString();
|
||||||
|
|
||||||
|
logger.info(`🕐 Updated session window for account ${accountData.name} (${accountId}): ${windowStart.toISOString()} - ${windowEnd.toISOString()}`);
|
||||||
|
|
||||||
|
return accountData;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to update session window for account ${accountId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🕐 计算会话窗口开始时间
|
||||||
|
_calculateSessionWindowStart(requestTime) {
|
||||||
|
const hour = requestTime.getHours();
|
||||||
|
const windowStartHour = Math.floor(hour / 5) * 5; // 向下取整到最近的5小时边界
|
||||||
|
|
||||||
|
const windowStart = new Date(requestTime);
|
||||||
|
windowStart.setHours(windowStartHour, 0, 0, 0);
|
||||||
|
|
||||||
|
return windowStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🕐 计算会话窗口结束时间
|
||||||
|
_calculateSessionWindowEnd(startTime) {
|
||||||
|
const endTime = new Date(startTime);
|
||||||
|
endTime.setHours(endTime.getHours() + 5); // 加5小时
|
||||||
|
return endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📊 获取会话窗口信息
|
||||||
|
async getSessionWindowInfo(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId);
|
||||||
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有会话窗口信息,返回null
|
||||||
|
if (!accountData.sessionWindowStart || !accountData.sessionWindowEnd) {
|
||||||
|
return {
|
||||||
|
hasActiveWindow: false,
|
||||||
|
windowStart: null,
|
||||||
|
windowEnd: null,
|
||||||
|
progress: 0,
|
||||||
|
remainingTime: null,
|
||||||
|
lastRequestTime: accountData.lastRequestTime || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const windowStart = new Date(accountData.sessionWindowStart);
|
||||||
|
const windowEnd = new Date(accountData.sessionWindowEnd);
|
||||||
|
const currentTime = now.getTime();
|
||||||
|
|
||||||
|
// 检查窗口是否已过期
|
||||||
|
if (currentTime >= windowEnd.getTime()) {
|
||||||
|
return {
|
||||||
|
hasActiveWindow: false,
|
||||||
|
windowStart: accountData.sessionWindowStart,
|
||||||
|
windowEnd: accountData.sessionWindowEnd,
|
||||||
|
progress: 100,
|
||||||
|
remainingTime: 0,
|
||||||
|
lastRequestTime: accountData.lastRequestTime || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算进度百分比
|
||||||
|
const totalDuration = windowEnd.getTime() - windowStart.getTime();
|
||||||
|
const elapsedTime = currentTime - windowStart.getTime();
|
||||||
|
const progress = Math.round((elapsedTime / totalDuration) * 100);
|
||||||
|
|
||||||
|
// 计算剩余时间(分钟)
|
||||||
|
const remainingTime = Math.round((windowEnd.getTime() - currentTime) / (1000 * 60));
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasActiveWindow: true,
|
||||||
|
windowStart: accountData.sessionWindowStart,
|
||||||
|
windowEnd: accountData.sessionWindowEnd,
|
||||||
|
progress,
|
||||||
|
remainingTime,
|
||||||
|
lastRequestTime: accountData.lastRequestTime || null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to get session window info for account ${accountId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 初始化所有账户的会话窗口(从历史数据恢复)
|
||||||
|
async initializeSessionWindows(forceRecalculate = false) {
|
||||||
|
try {
|
||||||
|
logger.info('🔄 Initializing session windows for all Claude accounts...');
|
||||||
|
|
||||||
|
const accounts = await redis.getAllClaudeAccounts();
|
||||||
|
let initializedCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
let expiredCount = 0;
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
// 如果已经有会话窗口信息且不强制重算,跳过
|
||||||
|
if (account.sessionWindowStart && account.sessionWindowEnd && !forceRecalculate) {
|
||||||
|
skippedCount++;
|
||||||
|
logger.debug(`⏭️ Skipped account ${account.name} (${account.id}) - already has session window`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有lastUsedAt,基于它恢复会话窗口
|
||||||
|
if (account.lastUsedAt) {
|
||||||
|
const lastUsedTime = new Date(account.lastUsedAt);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 计算时间差(分钟)
|
||||||
|
const timeSinceLastUsed = Math.round((now.getTime() - lastUsedTime.getTime()) / (1000 * 60));
|
||||||
|
|
||||||
|
// 计算lastUsedAt对应的会话窗口
|
||||||
|
const windowStart = this._calculateSessionWindowStart(lastUsedTime);
|
||||||
|
const windowEnd = this._calculateSessionWindowEnd(windowStart);
|
||||||
|
|
||||||
|
// 计算窗口剩余时间(分钟)
|
||||||
|
const timeUntilWindowExpires = Math.round((windowEnd.getTime() - now.getTime()) / (1000 * 60));
|
||||||
|
|
||||||
|
logger.info(`🔍 Analyzing account ${account.name} (${account.id}):`);
|
||||||
|
logger.info(` Last used: ${lastUsedTime.toISOString()} (${timeSinceLastUsed} minutes ago)`);
|
||||||
|
logger.info(` Calculated window: ${windowStart.toISOString()} - ${windowEnd.toISOString()}`);
|
||||||
|
logger.info(` Window expires in: ${timeUntilWindowExpires > 0 ? timeUntilWindowExpires + ' minutes' : 'EXPIRED'}`);
|
||||||
|
|
||||||
|
// 只有窗口未过期才恢复
|
||||||
|
if (now.getTime() < windowEnd.getTime()) {
|
||||||
|
account.sessionWindowStart = windowStart.toISOString();
|
||||||
|
account.sessionWindowEnd = windowEnd.toISOString();
|
||||||
|
account.lastRequestTime = account.lastUsedAt;
|
||||||
|
|
||||||
|
await redis.setClaudeAccount(account.id, account);
|
||||||
|
initializedCount++;
|
||||||
|
|
||||||
|
logger.success(`✅ Initialized session window for account ${account.name} (${account.id})`);
|
||||||
|
} else {
|
||||||
|
expiredCount++;
|
||||||
|
logger.warn(`⏰ Window expired for account ${account.name} (${account.id}) - will create new window on next request`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`📭 No lastUsedAt data for account ${account.name} (${account.id}) - will create window on first request`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`✅ Session window initialization completed:`);
|
||||||
|
logger.success(` 📊 Total accounts: ${accounts.length}`);
|
||||||
|
logger.success(` ✅ Initialized: ${initializedCount}`);
|
||||||
|
logger.success(` ⏭️ Skipped (existing): ${skippedCount}`);
|
||||||
|
logger.success(` ⏰ Expired: ${expiredCount}`);
|
||||||
|
logger.success(` 📭 No usage data: ${accounts.length - initializedCount - skippedCount - expiredCount}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: accounts.length,
|
||||||
|
initialized: initializedCount,
|
||||||
|
skipped: skippedCount,
|
||||||
|
expired: expiredCount,
|
||||||
|
noData: accounts.length - initializedCount - skippedCount - expiredCount
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to initialize session windows:', error);
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
initialized: 0,
|
||||||
|
skipped: 0,
|
||||||
|
expired: 0,
|
||||||
|
noData: 0,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ClaudeAccountService();
|
module.exports = new ClaudeAccountService();
|
||||||
@@ -2538,6 +2538,34 @@ const app = createApp({
|
|||||||
return number.toLocaleString();
|
return number.toLocaleString();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 格式化会话窗口时间
|
||||||
|
formatSessionWindow(windowStart, windowEnd) {
|
||||||
|
if (!windowStart || !windowEnd) return '--';
|
||||||
|
|
||||||
|
const start = new Date(windowStart);
|
||||||
|
const end = new Date(windowEnd);
|
||||||
|
|
||||||
|
const startHour = start.getHours().toString().padStart(2, '0');
|
||||||
|
const startMin = start.getMinutes().toString().padStart(2, '0');
|
||||||
|
const endHour = end.getHours().toString().padStart(2, '0');
|
||||||
|
const endMin = end.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${startHour}:${startMin} - ${endHour}:${endMin}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化剩余时间
|
||||||
|
formatRemainingTime(minutes) {
|
||||||
|
if (!minutes || minutes <= 0) return '已结束';
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const mins = minutes % 60;
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}小时${mins}分钟`;
|
||||||
|
}
|
||||||
|
return `${mins}分钟`;
|
||||||
|
},
|
||||||
|
|
||||||
// 格式化运行时间
|
// 格式化运行时间
|
||||||
formatUptime(seconds) {
|
formatUptime(seconds) {
|
||||||
if (!seconds) return '0s';
|
if (!seconds) return '0s';
|
||||||
|
|||||||
@@ -1019,6 +1019,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">会话窗口</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1107,6 +1108,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400 text-xs">暂无数据</div>
|
<div v-else class="text-gray-400 text-xs">暂无数据</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow" class="space-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-24 bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: account.sessionWindow.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-700 font-medium min-w-[32px]">
|
||||||
|
{{ account.sessionWindow.progress }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
<div>{{ formatSessionWindow(account.sessionWindow.windowStart, account.sessionWindow.windowEnd) }}</div>
|
||||||
|
<div v-if="account.sessionWindow.remainingTime > 0" class="text-indigo-600 font-medium">
|
||||||
|
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="account.platform === 'claude'" class="text-gray-400 text-sm">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400 text-sm">
|
||||||
|
<span class="text-xs">N/A</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
|
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user