diff --git a/src/models/redis.js b/src/models/redis.js index 502254cd..6cffa6a9 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -3181,6 +3181,7 @@ redisClient.scanConcurrencyQueueStatsKeys = async function () { const ACCOUNT_TEST_HISTORY_MAX = 5 // 保留最近5次测试记录 const ACCOUNT_TEST_HISTORY_TTL = 86400 * 30 // 30天过期 +const ACCOUNT_TEST_CONFIG_TTL = 86400 * 365 // 测试配置保留1年(用户通常长期使用) /** * 保存账户测试结果 @@ -3300,6 +3301,8 @@ redisClient.saveAccountTestConfig = async function (accountId, platform, testCon model: testConfig.model || 'claude-sonnet-4-5-20250929', // 默认模型 updatedAt: new Date().toISOString() }) + // 设置过期时间(1年) + await client.expire(key, ACCOUNT_TEST_CONFIG_TTL) } catch (error) { logger.error(`Failed to save test config for ${accountId}:`, error) } diff --git a/src/routes/admin/claudeAccounts.js b/src/routes/admin/claudeAccounts.js index ef06c6e1..52980877 100644 --- a/src/routes/admin/claudeAccounts.js +++ b/src/routes/admin/claudeAccounts.js @@ -9,6 +9,7 @@ const router = express.Router() const claudeAccountService = require('../../services/claudeAccountService') const claudeRelayService = require('../../services/claudeRelayService') const accountGroupService = require('../../services/accountGroupService') +const accountTestSchedulerService = require('../../services/accountTestSchedulerService') const apiKeyService = require('../../services/apiKeyService') const redis = require('../../models/redis') const { authenticateAdmin } = require('../../middleware/auth') @@ -959,7 +960,7 @@ router.put('/claude-accounts/:accountId/test-config', authenticateAdmin, async ( const { enabled, cronExpression, model } = req.body try { - // 验证参数 + // 验证 enabled 参数 if (typeof enabled !== 'boolean') { return res.status(400).json({ error: 'Invalid parameter', @@ -967,7 +968,7 @@ router.put('/claude-accounts/:accountId/test-config', authenticateAdmin, async ( }) } - // 验证 cron 表达式 + // 验证 cronExpression 参数 if (!cronExpression || typeof cronExpression !== 'string') { return res.status(400).json({ error: 'Invalid parameter', @@ -975,23 +976,38 @@ router.put('/claude-accounts/:accountId/test-config', authenticateAdmin, async ( }) } - // 使用 node-cron 验证表达式 - const cron = require('node-cron') - if (!cron.validate(cronExpression)) { + // 限制 cronExpression 长度防止 DoS + const MAX_CRON_LENGTH = 100 + if (cronExpression.length > MAX_CRON_LENGTH) { + return res.status(400).json({ + error: 'Invalid parameter', + message: `cronExpression too long (max ${MAX_CRON_LENGTH} characters)` + }) + } + + // 使用 service 的方法验证 cron 表达式 + if (!accountTestSchedulerService.validateCronExpression(cronExpression)) { return res.status(400).json({ error: 'Invalid parameter', message: `Invalid cron expression: ${cronExpression}. Format: "minute hour day month weekday" (e.g., "0 8 * * *" for daily at 8:00)` }) } - // 验证模型(可选,有默认值) + // 验证模型参数 const testModel = model || 'claude-sonnet-4-5-20250929' + if (typeof testModel !== 'string' || testModel.length > 256) { + return res.status(400).json({ + error: 'Invalid parameter', + message: 'model must be a valid string (max 256 characters)' + }) + } // 检查账户是否存在 const account = await claudeAccountService.getAccount(accountId) if (!account) { return res.status(404).json({ - error: 'Account not found' + error: 'Account not found', + message: `Claude account ${accountId} not found` }) } @@ -1033,7 +1049,8 @@ router.post('/claude-accounts/:accountId/test-sync', authenticateAdmin, async (r const account = await claudeAccountService.getAccount(accountId) if (!account) { return res.status(404).json({ - error: 'Account not found' + error: 'Account not found', + message: `Claude account ${accountId} not found` }) } diff --git a/src/services/accountTestSchedulerService.js b/src/services/accountTestSchedulerService.js index c545e5ec..866d8472 100644 --- a/src/services/accountTestSchedulerService.js +++ b/src/services/accountTestSchedulerService.js @@ -14,8 +14,6 @@ class AccountTestSchedulerService { // 定期刷新配置的间隔 (毫秒) this.refreshIntervalMs = 60 * 1000 this.refreshInterval = null - // 测试并发限制 - this.maxConcurrentTests = 3 // 当前正在测试的账户 this.testingAccounts = new Set() // 是否已启动 @@ -28,6 +26,10 @@ class AccountTestSchedulerService { * @returns {boolean} */ validateCronExpression(cronExpression) { + // 长度检查(防止 DoS) + if (!cronExpression || cronExpression.length > 100) { + return false + } return cron.validate(cronExpression) } @@ -85,41 +87,53 @@ class AccountTestSchedulerService { const platforms = ['claude', 'gemini', 'openai'] const activeAccountKeys = new Set() - for (const platform of platforms) { - const enabledAccounts = await redis.getEnabledTestAccounts(platform) + // 并行加载所有平台的配置 + const allEnabledAccounts = await Promise.all( + platforms.map((platform) => + redis + .getEnabledTestAccounts(platform) + .then((accounts) => accounts.map((acc) => ({ ...acc, platform }))) + .catch((error) => { + logger.warn(`⚠️ Failed to load test accounts for platform ${platform}:`, error) + return [] + }) + ) + ) - for (const { accountId, cronExpression, model } of enabledAccounts) { - if (!cronExpression) { - logger.warn( - `⚠️ Account ${accountId} (${platform}) has no valid cron expression, skipping` - ) + // 展平平台数据 + const flatAccounts = allEnabledAccounts.flat() + + for (const { accountId, cronExpression, model, platform } of flatAccounts) { + if (!cronExpression) { + logger.warn( + `⚠️ Account ${accountId} (${platform}) has no valid cron expression, skipping` + ) + continue + } + + const accountKey = `${platform}:${accountId}` + activeAccountKeys.add(accountKey) + + // 检查是否需要更新任务 + const existingTask = this.scheduledTasks.get(accountKey) + if (existingTask) { + // 如果 cron 表达式和模型都没变,不需要更新 + if (existingTask.cronExpression === cronExpression && existingTask.model === model) { continue } - - const accountKey = `${platform}:${accountId}` - activeAccountKeys.add(accountKey) - - // 检查是否需要更新任务 - const existingTask = this.scheduledTasks.get(accountKey) - if (existingTask) { - // 如果 cron 表达式和模型都没变,不需要更新 - if (existingTask.cronExpression === cronExpression && existingTask.model === model) { - continue - } - // 配置变了,停止旧任务 - existingTask.task.stop() - logger.info( - `🔄 Updating cron task for ${accountKey}: ${cronExpression}, model: ${model}` - ) - } else { - logger.info( - `➕ Creating cron task for ${accountKey}: ${cronExpression}, model: ${model}` - ) - } - - // 创建新的 cron 任务 - this._createCronTask(accountId, platform, cronExpression, model) + // 配置变了,停止旧任务 + existingTask.task.stop() + logger.info( + `🔄 Updating cron task for ${accountKey}: ${cronExpression}, model: ${model}` + ) + } else { + logger.info( + `➕ Creating cron task for ${accountKey}: ${cronExpression}, model: ${model}` + ) } + + // 创建新的 cron 任务 + this._createCronTask(accountId, platform, cronExpression, model) } // 清理已删除或禁用的账户任务 @@ -397,7 +411,6 @@ class AccountTestSchedulerService { return { running: this.isStarted, refreshIntervalMs: this.refreshIntervalMs, - maxConcurrentTests: this.maxConcurrentTests, scheduledTasksCount: this.scheduledTasks.size, scheduledTasks: tasks, currentlyTesting: Array.from(this.testingAccounts) diff --git a/web/admin-spa/src/components/accounts/AccountScheduledTestModal.vue b/web/admin-spa/src/components/accounts/AccountScheduledTestModal.vue index 5246e3bb..12c56571 100644 --- a/web/admin-spa/src/components/accounts/AccountScheduledTestModal.vue +++ b/web/admin-spa/src/components/accounts/AccountScheduledTestModal.vue @@ -221,6 +221,7 @@