Files
claude-relay-service/scripts/manage-session-windows.js
KevinLiao 718733b78b feat: 增加账号session窗口管理与显示。后续可以据此优化账号轮转逻辑。
scripts目录有相关管理脚本,请自行探索
2025-07-28 15:51:38 +08:00

532 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

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

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

#!/usr/bin/env node
/**
* 会话窗口管理脚本
* 用于调试、恢复和管理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 };