From a3666e3a3e11433e398fb26893aab66643ae0ab1 Mon Sep 17 00:00:00 2001 From: wfunc <114468522+wfunc@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:54:30 +0800 Subject: [PATCH] feat: add rate limit recovery webhook notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加限流恢复的 webhook 通知功能,当账户从限流状态自动恢复时发送通知。 主要改进: 1. **新增通知类型** (webhookConfigService.js) - 添加 `rateLimitRecovery` 通知类型 - 在配置获取和保存时自动合并默认通知类型 - 确保新增的通知类型有默认值 2. **增强限流清理服务** (rateLimitCleanupService.js) - 改进自动停止账户的检测逻辑 - 在 `finally` 块中确保 `clearedAccounts` 列表被重置,避免重复通知 - 对自动停止的账户显式调用 `removeAccountRateLimit` - 为 Claude 和 Claude Console 账户添加 `autoStopped` 和 `needsAutoStopRecovery` 检测 3. **改进 Claude Console 限流移除** (claudeConsoleAccountService.js) - 检测并恢复因自动停止而禁用调度的账户 - 清理过期的 `rateLimitAutoStopped` 标志 - 增加详细的日志记录 4. **前端 UI 支持** (SettingsView.vue) - 在 Webhook 设置中添加"限流恢复"通知类型选项 - 更新默认通知类型配置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/services/claudeConsoleAccountService.js | 15 +++++++-- src/services/rateLimitCleanupService.js | 22 +++++++++--- src/services/webhookConfigService.js | 20 ++++++++++- web/admin-spa/src/views/SettingsView.vue | 37 ++++++++++++++++----- 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index 85d27376..81bb178e 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -490,20 +490,29 @@ class ClaudeConsoleAccountService { errorMessage: '' } + const hadAutoStop = accountData.rateLimitAutoStopped === 'true' + // 只恢复因限流而自动停止的账户 - if (accountData.rateLimitAutoStopped === 'true' && accountData.schedulable === 'false') { + if (hadAutoStop && accountData.schedulable === 'false') { updateData.schedulable = 'true' // 恢复调度 - // 删除限流自动停止标记 - await client.hdel(accountKey, 'rateLimitAutoStopped') logger.info( `✅ Auto-resuming scheduling for Claude Console account ${accountId} after rate limit cleared` ) } + if (hadAutoStop) { + await client.hdel(accountKey, 'rateLimitAutoStopped') + } + await client.hset(accountKey, updateData) logger.success(`✅ Rate limit removed and account re-enabled: ${accountId}`) } } else { + if (await client.hdel(accountKey, 'rateLimitAutoStopped')) { + logger.info( + `ℹ️ Removed stale auto-stop flag for Claude Console account ${accountId} during rate limit recovery` + ) + } logger.success(`✅ Rate limit removed for Claude Console account: ${accountId}`) } diff --git a/src/services/rateLimitCleanupService.js b/src/services/rateLimitCleanupService.js index 9978660b..ebf8f44b 100644 --- a/src/services/rateLimitCleanupService.js +++ b/src/services/rateLimitCleanupService.js @@ -110,9 +110,6 @@ class RateLimitCleanupService { ) } - // 清空已清理账户列表 - this.clearedAccounts = [] - // 记录错误 const allErrors = [ ...results.openai.errors, @@ -125,6 +122,8 @@ class RateLimitCleanupService { } catch (error) { logger.error('❌ Rate limit cleanup failed:', error) } finally { + // 确保无论成功或失败都重置列表,避免重复通知 + this.clearedAccounts = [] this.isRunning = false } } @@ -199,8 +198,12 @@ class RateLimitCleanupService { typeof account.rateLimitStatus === 'object' && account.rateLimitStatus.status === 'limited') + const autoStopped = account.rateLimitAutoStopped === 'true' + const needsAutoStopRecovery = + autoStopped && (account.rateLimitEndAt || account.schedulable === 'false') + // 检查所有可能处于限流状态的账号,包括自动停止的账号 - if (isRateLimited || account.rateLimitedAt || account.rateLimitAutoStopped === 'true') { + if (isRateLimited || account.rateLimitedAt || needsAutoStopRecovery) { result.checked++ try { @@ -208,6 +211,9 @@ class RateLimitCleanupService { const isStillLimited = await claudeAccountService.isAccountRateLimited(account.id) if (!isStillLimited) { + if (!isRateLimited && autoStopped) { + await claudeAccountService.removeAccountRateLimit(account.id) + } result.cleared++ logger.info( `🧹 Auto-cleared expired rate limit for Claude account: ${account.name} (${account.id})` @@ -286,10 +292,13 @@ class RateLimitCleanupService { typeof account.rateLimitStatus === 'object' && account.rateLimitStatus.status === 'limited') + const autoStopped = account.rateLimitAutoStopped === 'true' + const needsAutoStopRecovery = autoStopped && account.schedulable === 'false' + // 检查两种状态字段:rateLimitStatus 和 status const hasStatusRateLimited = account.status === 'rate_limited' - if (isRateLimited || hasStatusRateLimited) { + if (isRateLimited || hasStatusRateLimited || needsAutoStopRecovery) { result.checked++ try { @@ -299,6 +308,9 @@ class RateLimitCleanupService { ) if (!isStillLimited) { + if (!isRateLimited && autoStopped) { + await claudeConsoleAccountService.removeAccountRateLimit(account.id) + } result.cleared++ // 如果 status 字段是 rate_limited,需要额外清理 diff --git a/src/services/webhookConfigService.js b/src/services/webhookConfigService.js index 39ca7265..ea689853 100644 --- a/src/services/webhookConfigService.js +++ b/src/services/webhookConfigService.js @@ -18,7 +18,17 @@ class WebhookConfigService { // 返回默认配置 return this.getDefaultConfig() } - return JSON.parse(configStr) + + const storedConfig = JSON.parse(configStr) + const defaultConfig = this.getDefaultConfig() + + // 合并默认通知类型,确保新增类型有默认值 + storedConfig.notificationTypes = { + ...defaultConfig.notificationTypes, + ...(storedConfig.notificationTypes || {}) + } + + return storedConfig } catch (error) { logger.error('获取webhook配置失败:', error) return this.getDefaultConfig() @@ -30,6 +40,13 @@ class WebhookConfigService { */ async saveConfig(config) { try { + const defaultConfig = this.getDefaultConfig() + + config.notificationTypes = { + ...defaultConfig.notificationTypes, + ...(config.notificationTypes || {}) + } + // 验证配置 this.validateConfig(config) @@ -312,6 +329,7 @@ class WebhookConfigService { quotaWarning: true, // 配额警告 systemError: true, // 系统错误 securityAlert: true, // 安全警报 + rateLimitRecovery: true, // 限流恢复 test: true // 测试通知 }, retrySettings: { diff --git a/web/admin-spa/src/views/SettingsView.vue b/web/admin-spa/src/views/SettingsView.vue index 19e29cff..ee68ab11 100644 --- a/web/admin-spa/src/views/SettingsView.vue +++ b/web/admin-spa/src/views/SettingsView.vue @@ -1255,15 +1255,18 @@ const testingConnection = ref(false) const savingPlatform = ref(false) // Webhook 配置 +const DEFAULT_WEBHOOK_NOTIFICATION_TYPES = { + accountAnomaly: true, + quotaWarning: true, + systemError: true, + securityAlert: true, + rateLimitRecovery: true +} + const webhookConfig = ref({ enabled: false, platforms: [], - notificationTypes: { - accountAnomaly: true, - quotaWarning: true, - systemError: true, - securityAlert: true - }, + notificationTypes: { ...DEFAULT_WEBHOOK_NOTIFICATION_TYPES }, retrySettings: { maxRetries: 3, retryDelay: 1000, @@ -1475,7 +1478,14 @@ const loadWebhookConfig = async () => { signal: abortController.value.signal }) if (response.success && isMounted.value) { - webhookConfig.value = response.config + const config = response.config || {} + webhookConfig.value = { + ...config, + notificationTypes: { + ...DEFAULT_WEBHOOK_NOTIFICATION_TYPES, + ...(config.notificationTypes || {}) + } + } } } catch (error) { if (error.name === 'AbortError') return @@ -1489,10 +1499,19 @@ const loadWebhookConfig = async () => { const saveWebhookConfig = async () => { if (!isMounted.value) return try { - const response = await apiClient.post('/admin/webhook/config', webhookConfig.value, { + const payload = { + ...webhookConfig.value, + notificationTypes: { + ...DEFAULT_WEBHOOK_NOTIFICATION_TYPES, + ...(webhookConfig.value.notificationTypes || {}) + } + } + + const response = await apiClient.post('/admin/webhook/config', payload, { signal: abortController.value.signal }) if (response.success && isMounted.value) { + webhookConfig.value = payload showToast('配置已保存', 'success') } } catch (error) { @@ -1930,6 +1949,7 @@ const getNotificationTypeName = (type) => { quotaWarning: '配额警告', systemError: '系统错误', securityAlert: '安全警报', + rateLimitRecovery: '限流恢复', test: '测试通知' } return names[type] || type @@ -1941,6 +1961,7 @@ const getNotificationTypeDescription = (type) => { quotaWarning: 'API调用配额不足警告', systemError: '系统运行错误和故障', securityAlert: '安全相关的警报通知', + rateLimitRecovery: '限流状态恢复时发送提醒', test: '用于测试Webhook连接是否正常' } return descriptions[type] || ''