#!/usr/bin/env node /** * 会话窗口管理脚本 * 用于调试、恢复和管理Claude账户的会话窗口 */ 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) }) } // 辅助函数:解析时间输入 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() 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) if (now < recoveredWindowEnd) { stats.canRecover++ console.log( ` 可恢复窗口: ✅ ${recoveredWindowStart.toLocaleString()} - ${recoveredWindowEnd.toLocaleString()}` ) } else { stats.expired++ console.log( ` 可恢复窗口: ❌ 已过期 (${recoveredWindowStart.toLocaleString()} - ${recoveredWindowEnd.toLocaleString()})` ) } } } else { 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) if (now < windowEnd) { stats.canRecover++ console.log( ` 可恢复窗口: ✅ ${windowStart.toLocaleString()} - ${windowEnd.toLocaleString()}` ) } else { 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}分钟前)`) } 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 \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 }