From cd3f51e9e2ae62b0f07765807baa4fff9fe0f440 Mon Sep 17 00:00:00 2001 From: guoyongchang Date: Fri, 19 Dec 2025 13:39:39 +0800 Subject: [PATCH] refactor: optimize cron test support feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **优化内容:** 1. **验证和安全性加强** - 移除cron验证重复,统一使用accountTestSchedulerService.validateCronExpression()方法 - 添加model参数类型和长度验证(max 256 chars) - 限制cronExpression长度至100字符防止DoS攻击 - 双层验证:service层和route层都进行长度检查 2. **性能优化** - 优化_refreshAllTasks()使用Promise.all()并行加载所有平台配置(之前是顺序加载) - 改进错误处理,平台加载失败时继续处理其他平台 3. **数据管理改进** - 为test config添加1年TTL过期机制(之前没有过期设置) - 保证test history已有30天TTL和5条记录限制 4. **错误响应标准化** - 统一所有API响应格式,确保error状态都包含message字段 - 改进错误消息的可读性和上下文信息 5. **用户体验改进** - Vue组件使用showToast()替代原生alert() - 移除console.error()改用toast通知用户 - 成功保存时显示成功提示 6. **代码整理** - 移除未使用的maxConcurrentTests变量及其getStatus()中的引用 - 保持代码整洁性 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- src/models/redis.js | 3 + src/routes/admin/claudeAccounts.js | 33 ++++++-- src/services/accountTestSchedulerService.js | 81 +++++++++++-------- .../accounts/AccountScheduledTestModal.vue | 9 ++- 4 files changed, 80 insertions(+), 46 deletions(-) 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 @@