mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
refactor: optimize cron test support feature
**优化内容:** 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 <noreply@anthropic.com>
This commit is contained in:
@@ -3181,6 +3181,7 @@ redisClient.scanConcurrencyQueueStatsKeys = async function () {
|
|||||||
|
|
||||||
const ACCOUNT_TEST_HISTORY_MAX = 5 // 保留最近5次测试记录
|
const ACCOUNT_TEST_HISTORY_MAX = 5 // 保留最近5次测试记录
|
||||||
const ACCOUNT_TEST_HISTORY_TTL = 86400 * 30 // 30天过期
|
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', // 默认模型
|
model: testConfig.model || 'claude-sonnet-4-5-20250929', // 默认模型
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
})
|
})
|
||||||
|
// 设置过期时间(1年)
|
||||||
|
await client.expire(key, ACCOUNT_TEST_CONFIG_TTL)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to save test config for ${accountId}:`, error)
|
logger.error(`Failed to save test config for ${accountId}:`, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const router = express.Router()
|
|||||||
const claudeAccountService = require('../../services/claudeAccountService')
|
const claudeAccountService = require('../../services/claudeAccountService')
|
||||||
const claudeRelayService = require('../../services/claudeRelayService')
|
const claudeRelayService = require('../../services/claudeRelayService')
|
||||||
const accountGroupService = require('../../services/accountGroupService')
|
const accountGroupService = require('../../services/accountGroupService')
|
||||||
|
const accountTestSchedulerService = require('../../services/accountTestSchedulerService')
|
||||||
const apiKeyService = require('../../services/apiKeyService')
|
const apiKeyService = require('../../services/apiKeyService')
|
||||||
const redis = require('../../models/redis')
|
const redis = require('../../models/redis')
|
||||||
const { authenticateAdmin } = require('../../middleware/auth')
|
const { authenticateAdmin } = require('../../middleware/auth')
|
||||||
@@ -959,7 +960,7 @@ router.put('/claude-accounts/:accountId/test-config', authenticateAdmin, async (
|
|||||||
const { enabled, cronExpression, model } = req.body
|
const { enabled, cronExpression, model } = req.body
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 验证参数
|
// 验证 enabled 参数
|
||||||
if (typeof enabled !== 'boolean') {
|
if (typeof enabled !== 'boolean') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Invalid parameter',
|
error: 'Invalid parameter',
|
||||||
@@ -967,7 +968,7 @@ router.put('/claude-accounts/:accountId/test-config', authenticateAdmin, async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 cron 表达式
|
// 验证 cronExpression 参数
|
||||||
if (!cronExpression || typeof cronExpression !== 'string') {
|
if (!cronExpression || typeof cronExpression !== 'string') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Invalid parameter',
|
error: 'Invalid parameter',
|
||||||
@@ -975,23 +976,38 @@ router.put('/claude-accounts/:accountId/test-config', authenticateAdmin, async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 node-cron 验证表达式
|
// 限制 cronExpression 长度防止 DoS
|
||||||
const cron = require('node-cron')
|
const MAX_CRON_LENGTH = 100
|
||||||
if (!cron.validate(cronExpression)) {
|
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({
|
return res.status(400).json({
|
||||||
error: 'Invalid parameter',
|
error: 'Invalid parameter',
|
||||||
message: `Invalid cron expression: ${cronExpression}. Format: "minute hour day month weekday" (e.g., "0 8 * * *" for daily at 8:00)`
|
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'
|
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)
|
const account = await claudeAccountService.getAccount(accountId)
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return res.status(404).json({
|
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)
|
const account = await claudeAccountService.getAccount(accountId)
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
error: 'Account not found'
|
error: 'Account not found',
|
||||||
|
message: `Claude account ${accountId} not found`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ class AccountTestSchedulerService {
|
|||||||
// 定期刷新配置的间隔 (毫秒)
|
// 定期刷新配置的间隔 (毫秒)
|
||||||
this.refreshIntervalMs = 60 * 1000
|
this.refreshIntervalMs = 60 * 1000
|
||||||
this.refreshInterval = null
|
this.refreshInterval = null
|
||||||
// 测试并发限制
|
|
||||||
this.maxConcurrentTests = 3
|
|
||||||
// 当前正在测试的账户
|
// 当前正在测试的账户
|
||||||
this.testingAccounts = new Set()
|
this.testingAccounts = new Set()
|
||||||
// 是否已启动
|
// 是否已启动
|
||||||
@@ -28,6 +26,10 @@ class AccountTestSchedulerService {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
validateCronExpression(cronExpression) {
|
validateCronExpression(cronExpression) {
|
||||||
|
// 长度检查(防止 DoS)
|
||||||
|
if (!cronExpression || cronExpression.length > 100) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return cron.validate(cronExpression)
|
return cron.validate(cronExpression)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,41 +87,53 @@ class AccountTestSchedulerService {
|
|||||||
const platforms = ['claude', 'gemini', 'openai']
|
const platforms = ['claude', 'gemini', 'openai']
|
||||||
const activeAccountKeys = new Set()
|
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) {
|
const flatAccounts = allEnabledAccounts.flat()
|
||||||
logger.warn(
|
|
||||||
`⚠️ Account ${accountId} (${platform}) has no valid cron expression, skipping`
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
// 配置变了,停止旧任务
|
||||||
const accountKey = `${platform}:${accountId}`
|
existingTask.task.stop()
|
||||||
activeAccountKeys.add(accountKey)
|
logger.info(
|
||||||
|
`🔄 Updating cron task for ${accountKey}: ${cronExpression}, model: ${model}`
|
||||||
// 检查是否需要更新任务
|
)
|
||||||
const existingTask = this.scheduledTasks.get(accountKey)
|
} else {
|
||||||
if (existingTask) {
|
logger.info(
|
||||||
// 如果 cron 表达式和模型都没变,不需要更新
|
`➕ Creating cron task for ${accountKey}: ${cronExpression}, model: ${model}`
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建新的 cron 任务
|
||||||
|
this._createCronTask(accountId, platform, cronExpression, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理已删除或禁用的账户任务
|
// 清理已删除或禁用的账户任务
|
||||||
@@ -397,7 +411,6 @@ class AccountTestSchedulerService {
|
|||||||
return {
|
return {
|
||||||
running: this.isStarted,
|
running: this.isStarted,
|
||||||
refreshIntervalMs: this.refreshIntervalMs,
|
refreshIntervalMs: this.refreshIntervalMs,
|
||||||
maxConcurrentTests: this.maxConcurrentTests,
|
|
||||||
scheduledTasksCount: this.scheduledTasks.size,
|
scheduledTasksCount: this.scheduledTasks.size,
|
||||||
scheduledTasks: tasks,
|
scheduledTasks: tasks,
|
||||||
currentlyTesting: Array.from(this.testingAccounts)
|
currentlyTesting: Array.from(this.testingAccounts)
|
||||||
|
|||||||
@@ -221,6 +221,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { API_PREFIX } from '@/config/api'
|
import { API_PREFIX } from '@/config/api'
|
||||||
|
import { showToast } from '@/utils/toast'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -326,7 +327,7 @@ async function loadConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load test config:', err)
|
showToast('加载配置失败: ' + err.message, 'error')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -363,15 +364,15 @@ async function saveConfig() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
showToast('配置已保存', 'success')
|
||||||
emit('saved')
|
emit('saved')
|
||||||
handleClose()
|
handleClose()
|
||||||
} else {
|
} else {
|
||||||
const errorData = await res.json().catch(() => ({}))
|
const errorData = await res.json().catch(() => ({}))
|
||||||
alert(errorData.message || '保存失败')
|
showToast(errorData.message || '保存失败', 'error')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save test config:', err)
|
showToast('保存失败: ' + err.message, 'error')
|
||||||
alert('保存失败: ' + err.message)
|
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user