From 97da7d44babcc9f9007fc6e93c7003e2ead5be00 Mon Sep 17 00:00:00 2001 From: Gemini Wen Date: Mon, 8 Sep 2025 22:54:23 +0800 Subject: [PATCH 01/11] ui: improve group tags layout in AccountsView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add vertical margin and flex-wrap for better multi-line display - Remove left margin to align properly with container 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- web/admin-spa/src/views/AccountsView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 7c1d4613..7a8719df 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -276,12 +276,12 @@
{{ group.name }} From 908e323db0ffeaf92cf0aca851ff468db26cdf53 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 00:46:40 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E4=B8=BA=E6=99=AE=E9=80=9AClaude?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E6=B7=BB=E5=8A=A0529=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加可配置的529错误处理机制,通过CLAUDE_OVERLOAD_HANDLING_MINUTES环境变量控制 - 支持流式和非流式请求的529错误检测 - 自动标记过载账户并在指定时间后恢复 - 成功请求后自动清除过载状态 - 默认禁用,需手动配置启用(0表示禁用,>0表示过载持续分钟数) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .env.example | 4 + config/config.example.js | 9 ++- src/services/claudeAccountService.js | 102 +++++++++++++++++++++++++ src/services/claudeRelayService.js | 65 ++++++++++++++++ src/services/unifiedClaudeScheduler.js | 5 +- 5 files changed, 183 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index f3791b06..fa640f15 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,10 @@ CLAUDE_API_URL=https://api.anthropic.com/v1/messages CLAUDE_API_VERSION=2023-06-01 CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14 +# 🚫 529错误处理配置 +# 启用529错误处理,0表示禁用,>0表示过载状态持续时间(分钟) +CLAUDE_OVERLOAD_HANDLING_MINUTES=0 + # 🌐 代理配置 DEFAULT_PROXY_TIMEOUT=600000 MAX_PROXY_RETRIES=3 diff --git a/config/config.example.js b/config/config.example.js index 5fae5fde..3f8d71d7 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -46,7 +46,14 @@ const config = { apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01', betaHeader: process.env.CLAUDE_BETA_HEADER || - 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14' + 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14', + overloadHandling: { + enabled: (() => { + const minutes = parseInt(process.env.CLAUDE_OVERLOAD_HANDLING_MINUTES) || 0 + // 验证配置值:限制在0-1440分钟(24小时)内 + return Math.max(0, Math.min(minutes, 1440)) + })() + } }, // ☁️ Bedrock API配置 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 336e8828..9219a7f5 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -2137,6 +2137,108 @@ class ClaudeAccountService { logger.error(`❌ Failed to update session window status for account ${accountId}:`, error) } } + + // 🚫 标记账号为过载状态(529错误) + async markAccountOverloaded(accountId) { + try { + const accountData = await redis.getClaudeAccount(accountId) + if (!accountData) { + throw new Error('Account not found') + } + + // 获取配置的过载处理时间(分钟) + const overloadMinutes = config.overloadHandling?.enabled || 0 + + if (overloadMinutes === 0) { + logger.info('⏭️ 529 error handling is disabled') + return { success: false, error: '529 error handling is disabled' } + } + + const overloadKey = `account:overload:${accountId}` + const ttl = overloadMinutes * 60 // 转换为秒 + + await redis.setex( + overloadKey, + ttl, + JSON.stringify({ + accountId, + accountName: accountData.name, + markedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + ttl * 1000).toISOString() + }) + ) + + logger.warn( + `🚫 Account ${accountData.name} (${accountId}) marked as overloaded for ${overloadMinutes} minutes` + ) + + // 在账号上记录最后一次529错误 + const updates = { + lastOverloadAt: new Date().toISOString(), + errorMessage: `529错误 - 过载${overloadMinutes}分钟` + } + + const updatedAccountData = { ...accountData, ...updates } + await redis.setClaudeAccount(accountId, updatedAccountData) + + return { success: true, accountName: accountData.name, duration: overloadMinutes } + } catch (error) { + logger.error(`❌ Failed to mark account as overloaded: ${accountId}`, error) + // 不抛出错误,避免影响主请求流程 + return { success: false, error: error.message } + } + } + + // ✅ 检查账号是否过载 + async isAccountOverloaded(accountId) { + try { + // 如果529处理未启用,直接返回false + const overloadMinutes = config.overloadHandling?.enabled || 0 + if (overloadMinutes === 0) { + return false + } + + const overloadKey = `account:overload:${accountId}` + const overloadData = await redis.get(overloadKey) + + if (overloadData) { + // 账号处于过载状态 + return true + } + + // 账号未过载 + return false + } catch (error) { + logger.error(`❌ Failed to check if account is overloaded: ${accountId}`, error) + return false + } + } + + // 🔄 移除账号的过载状态 + async removeAccountOverload(accountId) { + try { + const accountData = await redis.getClaudeAccount(accountId) + if (!accountData) { + throw new Error('Account not found') + } + + const overloadKey = `account:overload:${accountId}` + await redis.del(overloadKey) + + logger.info(`✅ Account ${accountData.name} (${accountId}) overload status removed`) + + // 清理账号上的错误信息 + if (accountData.errorMessage && accountData.errorMessage.includes('529错误')) { + const updatedAccountData = { ...accountData } + delete updatedAccountData.errorMessage + delete updatedAccountData.lastOverloadAt + await redis.setClaudeAccount(accountId, updatedAccountData) + } + } catch (error) { + logger.error(`❌ Failed to remove overload status for account: ${accountId}`, error) + // 不抛出错误,移除过载状态失败不应该影响主流程 + } + } } module.exports = new ClaudeAccountService() diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index f4e03334..050da9a2 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -208,6 +208,24 @@ class ClaudeRelayService { ) await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash) } + // 检查是否为529状态码(服务过载) + else if (response.statusCode === 529) { + logger.warn(`🚫 Overload error (529) detected for account ${accountId}`) + + // 检查是否启用了529错误处理 + if (config.claude.overloadHandling.enabled > 0) { + try { + await claudeAccountService.markAccountOverloaded(accountId) + logger.info( + `🚫 Account ${accountId} marked as overloaded for ${config.claude.overloadHandling.enabled} minutes` + ) + } catch (overloadError) { + logger.error(`❌ Failed to mark account as overloaded: ${accountId}`, overloadError) + } + } else { + logger.info(`🚫 529 error handling is disabled, skipping account overload marking`) + } + } // 检查是否为5xx状态码 else if (response.statusCode >= 500 && response.statusCode < 600) { logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`) @@ -296,6 +314,19 @@ class ClaudeRelayService { await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType) } + // 如果请求成功,检查并移除过载状态 + try { + const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId) + if (isOverloaded) { + await claudeAccountService.removeAccountOverload(accountId) + } + } catch (overloadError) { + logger.error( + `❌ Failed to check/remove overload status for account ${accountId}:`, + overloadError + ) + } + // 只有真实的 Claude Code 请求才更新 headers if ( clientHeaders && @@ -1002,6 +1033,27 @@ class ClaudeRelayService { `🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked` ) await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash) + } else if (res.statusCode === 529) { + logger.warn(`🚫 [Stream] Overload error (529) detected for account ${accountId}`) + + // 检查是否启用了529错误处理 + if (config.claude.overloadHandling.enabled > 0) { + try { + await claudeAccountService.markAccountOverloaded(accountId) + logger.info( + `🚫 [Stream] Account ${accountId} marked as overloaded for ${config.claude.overloadHandling.enabled} minutes` + ) + } catch (overloadError) { + logger.error( + `❌ [Stream] Failed to mark account as overloaded: ${accountId}`, + overloadError + ) + } + } else { + logger.info( + `🚫 [Stream] 529 error handling is disabled, skipping account overload marking` + ) + } } else if (res.statusCode >= 500 && res.statusCode < 600) { logger.warn( `🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}` @@ -1327,6 +1379,19 @@ class ClaudeRelayService { await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType) } + // 如果流式请求成功,检查并移除过载状态 + try { + const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId) + if (isOverloaded) { + await claudeAccountService.removeAccountOverload(accountId) + } + } catch (overloadError) { + logger.error( + `❌ [Stream] Failed to check/remove overload status for account ${accountId}:`, + overloadError + ) + } + // 只有真实的 Claude Code 请求才更新 headers(流式请求) if ( clientHeaders && diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 7bceb040..5af5d726 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -528,7 +528,10 @@ class UnifiedClaudeScheduler { return false } - return !(await claudeAccountService.isAccountRateLimited(accountId)) + // 检查是否限流或过载 + const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId) + const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId) + return !isRateLimited && !isOverloaded } else if (accountType === 'claude-console') { const account = await claudeConsoleAccountService.getAccount(accountId) if (!account || !account.isActive) { From 4c2660a2d387e3cf120f7f7f4ece88298fa1af57 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 02:33:50 +0800 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=93?= =?UTF-8?q?=E5=B1=9E=E8=B4=A6=E5=8F=B7=E5=81=9C=E6=AD=A2=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E5=90=8E=E4=BB=8D=E8=83=BD=E4=BD=BF=E7=94=A8=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 selectAccountForApiKey 方法中为所有专属账号类型添加 schedulable 检查 - 在 _getAllAvailableAccounts 方法中为所有专属账号类型添加 schedulable 检查 - 改进日志输出,显示账号不可用的具体原因(isActive、status、schedulable 状态) 问题描述: 当 Claude Console/OAuth/Bedrock 账号设置为专属账号并停止调度(schedulable=false)后, 系统仍然会使用该账号,没有正确回退到账号池。 修复内容: 1. Claude OAuth 账号:添加 this._isSchedulable(boundAccount.schedulable) 检查 2. Claude Console 账号:添加 this._isSchedulable(boundConsoleAccount.schedulable) 检查 3. Bedrock 账号:添加 this._isSchedulable(boundBedrockAccountResult.data.schedulable) 检查 兼容性: _isSchedulable 方法已处理向后兼容,当 schedulable 字段为 undefined/null 时默认返回 true 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/services/unifiedClaudeScheduler.js | 44 +++++++++++++++++++------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 7bceb040..50e92e33 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -107,7 +107,12 @@ class UnifiedClaudeScheduler { // 普通专属账户 const boundAccount = await redis.getClaudeAccount(apiKeyData.claudeAccountId) - if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') { + if ( + boundAccount && + boundAccount.isActive === 'true' && + boundAccount.status !== 'error' && + this._isSchedulable(boundAccount.schedulable) + ) { logger.info( `🎯 Using bound dedicated Claude OAuth account: ${boundAccount.name} (${apiKeyData.claudeAccountId}) for API key ${apiKeyData.name}` ) @@ -117,7 +122,7 @@ class UnifiedClaudeScheduler { } } else { logger.warn( - `⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available, falling back to pool` + `⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available (isActive: ${boundAccount?.isActive}, status: ${boundAccount?.status}, schedulable: ${boundAccount?.schedulable}), falling back to pool` ) } } @@ -130,7 +135,8 @@ class UnifiedClaudeScheduler { if ( boundConsoleAccount && boundConsoleAccount.isActive === true && - boundConsoleAccount.status === 'active' + boundConsoleAccount.status === 'active' && + this._isSchedulable(boundConsoleAccount.schedulable) ) { logger.info( `🎯 Using bound dedicated Claude Console account: ${boundConsoleAccount.name} (${apiKeyData.claudeConsoleAccountId}) for API key ${apiKeyData.name}` @@ -141,7 +147,7 @@ class UnifiedClaudeScheduler { } } else { logger.warn( - `⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available, falling back to pool` + `⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available (isActive: ${boundConsoleAccount?.isActive}, status: ${boundConsoleAccount?.status}, schedulable: ${boundConsoleAccount?.schedulable}), falling back to pool` ) } } @@ -151,7 +157,11 @@ class UnifiedClaudeScheduler { const boundBedrockAccountResult = await bedrockAccountService.getAccount( apiKeyData.bedrockAccountId ) - if (boundBedrockAccountResult.success && boundBedrockAccountResult.data.isActive === true) { + if ( + boundBedrockAccountResult.success && + boundBedrockAccountResult.data.isActive === true && + this._isSchedulable(boundBedrockAccountResult.data.schedulable) + ) { logger.info( `🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId}) for API key ${apiKeyData.name}` ) @@ -161,7 +171,7 @@ class UnifiedClaudeScheduler { } } else { logger.warn( - `⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available, falling back to pool` + `⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available (isActive: ${boundBedrockAccountResult?.data?.isActive}, schedulable: ${boundBedrockAccountResult?.data?.schedulable}), falling back to pool` ) } } @@ -251,7 +261,8 @@ class UnifiedClaudeScheduler { boundAccount.isActive === 'true' && boundAccount.status !== 'error' && boundAccount.status !== 'blocked' && - boundAccount.status !== 'temp_error' + boundAccount.status !== 'temp_error' && + this._isSchedulable(boundAccount.schedulable) ) { const isRateLimited = await claudeAccountService.isAccountRateLimited(boundAccount.id) if (!isRateLimited) { @@ -269,7 +280,9 @@ class UnifiedClaudeScheduler { ] } } else { - logger.warn(`⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available`) + logger.warn( + `⚠️ Bound Claude OAuth account ${apiKeyData.claudeAccountId} is not available (isActive: ${boundAccount?.isActive}, status: ${boundAccount?.status}, schedulable: ${boundAccount?.schedulable})` + ) } } @@ -281,7 +294,8 @@ class UnifiedClaudeScheduler { if ( boundConsoleAccount && boundConsoleAccount.isActive === true && - boundConsoleAccount.status === 'active' + boundConsoleAccount.status === 'active' && + this._isSchedulable(boundConsoleAccount.schedulable) ) { // 主动触发一次额度检查 try { @@ -317,7 +331,7 @@ class UnifiedClaudeScheduler { } } else { logger.warn( - `⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available` + `⚠️ Bound Claude Console account ${apiKeyData.claudeConsoleAccountId} is not available (isActive: ${boundConsoleAccount?.isActive}, status: ${boundConsoleAccount?.status}, schedulable: ${boundConsoleAccount?.schedulable})` ) } } @@ -327,7 +341,11 @@ class UnifiedClaudeScheduler { const boundBedrockAccountResult = await bedrockAccountService.getAccount( apiKeyData.bedrockAccountId ) - if (boundBedrockAccountResult.success && boundBedrockAccountResult.data.isActive === true) { + if ( + boundBedrockAccountResult.success && + boundBedrockAccountResult.data.isActive === true && + this._isSchedulable(boundBedrockAccountResult.data.schedulable) + ) { logger.info( `🎯 Using bound dedicated Bedrock account: ${boundBedrockAccountResult.data.name} (${apiKeyData.bedrockAccountId})` ) @@ -341,7 +359,9 @@ class UnifiedClaudeScheduler { } ] } else { - logger.warn(`⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available`) + logger.warn( + `⚠️ Bound Bedrock account ${apiKeyData.bedrockAccountId} is not available (isActive: ${boundBedrockAccountResult?.data?.isActive}, schedulable: ${boundBedrockAccountResult?.data?.schedulable})` + ) } } From 756918b0ce2f843e8b3cb411d4e975270af1a1fb Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 02:45:47 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=E5=9C=A8=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E4=B8=AD=E6=B7=BB=E5=8A=A0=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 claudeRelayService.js 的所有错误日志中添加账号名称或 ID - 在 claudeConsoleRelayService.js 的错误日志中添加账号信息 - 便于排查 529 (过载) 和 504 (超时) 错误对应的具体账号 问题背景: 用户反馈错误日志中没有账号信息,无法定位是哪个账号出现问题, 特别是 529 和 504 错误频繁出现时难以排查。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/services/claudeConsoleRelayService.js | 12 ++++++------ src/services/claudeRelayService.js | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/services/claudeConsoleRelayService.js b/src/services/claudeConsoleRelayService.js index 61e4f2f3..1e040ee9 100644 --- a/src/services/claudeConsoleRelayService.js +++ b/src/services/claudeConsoleRelayService.js @@ -222,7 +222,7 @@ class ClaudeConsoleRelayService { throw new Error('Client disconnected') } - logger.error('❌ Claude Console Claude relay request failed:', error.message) + logger.error(`❌ Claude Console relay request failed (Account: ${account?.name || accountId}):`, error.message) // 不再因为模型不支持而block账号 @@ -297,7 +297,7 @@ class ClaudeConsoleRelayService { // 更新最后使用时间 await this._updateLastUsedTime(accountId) } catch (error) { - logger.error('❌ Claude Console Claude stream relay failed:', error) + logger.error(`❌ Claude Console stream relay failed (Account: ${account?.name || accountId}):`, error) throw error } } @@ -376,7 +376,7 @@ class ClaudeConsoleRelayService { // 错误响应处理 if (response.status !== 200) { - logger.error(`❌ Claude Console API returned error status: ${response.status}`) + logger.error(`❌ Claude Console API returned error status: ${response.status} | Account: ${account?.name || accountId}`) if (response.status === 401) { claudeConsoleAccountService.markAccountUnauthorized(accountId) @@ -528,7 +528,7 @@ class ClaudeConsoleRelayService { } } } catch (error) { - logger.error('❌ Error processing Claude Console stream data:', error) + logger.error(`❌ Error processing Claude Console stream data (Account: ${account?.name || accountId}):`, error) if (!responseStream.destroyed) { responseStream.write('event: error\n') responseStream.write( @@ -570,7 +570,7 @@ class ClaudeConsoleRelayService { }) response.data.on('error', (error) => { - logger.error('❌ Claude Console stream error:', error) + logger.error(`❌ Claude Console stream error (Account: ${account?.name || accountId}):`, error) if (!responseStream.destroyed) { responseStream.write('event: error\n') responseStream.write( @@ -590,7 +590,7 @@ class ClaudeConsoleRelayService { return } - logger.error('❌ Claude Console Claude stream request error:', error.message) + logger.error(`❌ Claude Console stream request error (Account: ${account?.name || accountId}):`, error.message) // 检查错误状态 if (error.response) { diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index f4e03334..577c6236 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -741,7 +741,7 @@ class ClaudeRelayService { resolve(response) } catch (error) { - logger.error('❌ Failed to parse Claude API response:', error) + logger.error(`❌ Failed to parse Claude API response (Account: ${accountId}):`, error) reject(error) } }) @@ -754,7 +754,7 @@ class ClaudeRelayService { req.on('error', async (error) => { console.error(': ❌ ', error) - logger.error('❌ Claude API request error:', error.message, { + logger.error(`❌ Claude API request error (Account: ${accountId}):`, error.message, { code: error.code, errno: error.errno, syscall: error.syscall, @@ -781,7 +781,7 @@ class ClaudeRelayService { req.on('timeout', async () => { req.destroy() - logger.error('❌ Claude API request timeout') + logger.error(`❌ Claude API request timeout (Account: ${accountId})`) await this._handleServerError(accountId, 504, null, 'Request') @@ -889,7 +889,7 @@ class ClaudeRelayService { options ) } catch (error) { - logger.error('❌ Claude stream relay with usage capture failed:', error) + logger.error(`❌ Claude stream relay with usage capture failed:`, error) throw error } } @@ -1015,7 +1015,7 @@ class ClaudeRelayService { logger.error('❌ Error in stream error handler:', err) }) - logger.error(`❌ Claude API returned error status: ${res.statusCode}`) + logger.error(`❌ Claude API returned error status: ${res.statusCode} | Account: ${account?.name || accountId}`) let errorData = '' res.on('data', (chunk) => { @@ -1024,7 +1024,7 @@ class ClaudeRelayService { res.on('end', () => { console.error(': ❌ ', errorData) - logger.error('❌ Claude API error response:', errorData) + logger.error(`❌ Claude API error response (Account: ${account?.name || accountId}):`, errorData) if (!responseStream.destroyed) { // 发送错误事件 responseStream.write('event: error\n') @@ -1343,7 +1343,7 @@ class ClaudeRelayService { }) req.on('error', async (error) => { - logger.error('❌ Claude stream request error:', error.message, { + logger.error(`❌ Claude stream request error (Account: ${account?.name || accountId}):`, error.message, { code: error.code, errno: error.errno, syscall: error.syscall @@ -1391,7 +1391,7 @@ class ClaudeRelayService { req.on('timeout', async () => { req.destroy() - logger.error('❌ Claude stream request timeout') + logger.error(`❌ Claude stream request timeout | Account: ${account?.name || accountId}`) if (!responseStream.headersSent) { responseStream.writeHead(504, { @@ -1493,7 +1493,7 @@ class ClaudeRelayService { }) req.on('error', async (error) => { - logger.error('❌ Claude stream request error:', error.message, { + logger.error(`❌ Claude stream request error (Account: ${account?.name || accountId}):`, error.message, { code: error.code, errno: error.errno, syscall: error.syscall @@ -1541,7 +1541,7 @@ class ClaudeRelayService { req.on('timeout', async () => { req.destroy() - logger.error('❌ Claude stream request timeout') + logger.error(`❌ Claude stream request timeout | Account: ${account?.name || accountId}`) if (!responseStream.headersSent) { responseStream.writeHead(504, { From 283362acd07f5c6af93e1ca575810cb674f38727 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 04:00:35 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0SMTP=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 支持SMTP邮件通知平台,可通过邮件接收系统通知 - 支持配置SMTP服务器、端口、用户名、密码、发件人和收件人 - 支持TLS/SSL加密连接 - 提供美观的HTML邮件模板和纯文本备用格式 代码优化: - 重构邮件格式化逻辑,提取buildNotificationDetails减少重复代码 - 优化前端表单验证逻辑,提取validatePlatformForm统一验证 - 清理UI中的冗余提示信息和配置项 UI改进: - 移除SMTP配置说明文字 - 移除超时设置和忽略TLS证书验证选项 - 简化测试成功提示消息 - SMTP平台显示收件人邮箱而非URL 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package-lock.json | 10 + package.json | 1 + src/routes/webhook.js | 49 +++- src/services/webhookConfigService.js | 52 +++- src/services/webhookService.js | 164 ++++++++++++- web/admin-spa/src/views/SettingsView.vue | 297 ++++++++++++++++++++--- 6 files changed, 529 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb50813f..10462d48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "ioredis": "^5.3.2", "ldapjs": "^3.0.7", "morgan": "^1.10.0", + "nodemailer": "^7.0.6", "ora": "^5.4.1", "rate-limiter-flexible": "^5.0.5", "socks-proxy-agent": "^8.0.2", @@ -7077,6 +7078,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz", + "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.10.tgz", diff --git a/package.json b/package.json index 86424cea..7e0c9e98 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "ioredis": "^5.3.2", "ldapjs": "^3.0.7", "morgan": "^1.10.0", + "nodemailer": "^7.0.6", "ora": "^5.4.1", "rate-limiter-flexible": "^5.0.5", "socks-proxy-agent": "^8.0.2", diff --git a/src/routes/webhook.js b/src/routes/webhook.js index f4c70b41..4af05fb6 100644 --- a/src/routes/webhook.js +++ b/src/routes/webhook.js @@ -124,7 +124,16 @@ router.post('/test', authenticateAdmin, async (req, res) => { serverUrl, level, sound, - group + group, + // SMTP 相关字段 + host, + port, + secure, + user, + pass, + from, + to, + ignoreTLS } = req.body // Bark平台特殊处理 @@ -149,6 +158,34 @@ router.post('/test', authenticateAdmin, async (req, res) => { } logger.info(`🧪 测试webhook: ${type} - Device Key: ${deviceKey.substring(0, 8)}...`) + } else if (type === 'smtp') { + // SMTP平台验证 + if (!host) { + return res.status(400).json({ + error: 'Missing SMTP host', + message: '请提供SMTP服务器地址' + }) + } + if (!user) { + return res.status(400).json({ + error: 'Missing SMTP user', + message: '请提供SMTP用户名' + }) + } + if (!pass) { + return res.status(400).json({ + error: 'Missing SMTP password', + message: '请提供SMTP密码' + }) + } + if (!to) { + return res.status(400).json({ + error: 'Missing recipient email', + message: '请提供收件人邮箱' + }) + } + + logger.info(`🧪 测试webhook: ${type} - ${host}:${port || 587} -> ${to}`) } else { // 其他平台验证URL if (!url) { @@ -188,6 +225,16 @@ router.post('/test', authenticateAdmin, async (req, res) => { platform.level = level platform.sound = sound platform.group = group + } else if (type === 'smtp') { + // 添加SMTP特有字段 + platform.host = host + platform.port = port || 587 + platform.secure = secure || false + platform.user = user + platform.pass = pass + platform.from = from + platform.to = to + platform.ignoreTLS = ignoreTLS || false } const result = await webhookService.testWebhook(platform) diff --git a/src/services/webhookConfigService.js b/src/services/webhookConfigService.js index 59955cd1..df0f36fb 100644 --- a/src/services/webhookConfigService.js +++ b/src/services/webhookConfigService.js @@ -63,7 +63,8 @@ class WebhookConfigService { 'slack', 'discord', 'custom', - 'bark' + 'bark', + 'smtp' ] for (const platform of config.platforms) { @@ -71,8 +72,8 @@ class WebhookConfigService { throw new Error(`不支持的平台类型: ${platform.type}`) } - // Bark平台使用deviceKey而不是url - if (platform.type !== 'bark') { + // Bark和SMTP平台不使用标准URL + if (platform.type !== 'bark' && platform.type !== 'smtp') { if (!platform.url || !this.isValidUrl(platform.url)) { throw new Error(`无效的webhook URL: ${platform.url}`) } @@ -201,6 +202,51 @@ class WebhookConfigService { logger.warn('⚠️ Bark点击跳转URL格式可能不正确') } break + case 'smtp': { + // 验证SMTP必需配置 + if (!platform.host) { + throw new Error('SMTP平台必须提供主机地址') + } + if (!platform.user) { + throw new Error('SMTP平台必须提供用户名') + } + if (!platform.pass) { + throw new Error('SMTP平台必须提供密码') + } + if (!platform.to) { + throw new Error('SMTP平台必须提供接收邮箱') + } + + // 验证端口 + if (platform.port && (platform.port < 1 || platform.port > 65535)) { + throw new Error('SMTP端口必须在1-65535之间') + } + + // 验证邮箱格式 + // 支持两种格式:1. 纯邮箱 user@domain.com 2. 带名称 Name + const simpleEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + + // 验证接收邮箱 + const toEmails = Array.isArray(platform.to) ? platform.to : [platform.to] + for (const email of toEmails) { + // 提取实际邮箱地址(如果是 Name 格式) + const actualEmail = email.includes('<') ? email.match(/<([^>]+)>/)?.[1] : email + if (!actualEmail || !simpleEmailRegex.test(actualEmail)) { + throw new Error(`无效的接收邮箱格式: ${email}`) + } + } + + // 验证发送邮箱(支持 Name 格式) + if (platform.from) { + const actualFromEmail = platform.from.includes('<') + ? platform.from.match(/<([^>]+)>/)?.[1] + : platform.from + if (!actualFromEmail || !simpleEmailRegex.test(actualFromEmail)) { + throw new Error(`无效的发送邮箱格式: ${platform.from}`) + } + } + break + } } } diff --git a/src/services/webhookService.js b/src/services/webhookService.js index c0d049c5..4aa66cd9 100755 --- a/src/services/webhookService.js +++ b/src/services/webhookService.js @@ -1,5 +1,6 @@ const axios = require('axios') const crypto = require('crypto') +const nodemailer = require('nodemailer') const logger = require('../utils/logger') const webhookConfigService = require('./webhookConfigService') const { getISOStringWithTimezone } = require('../utils/dateHelper') @@ -14,7 +15,8 @@ class WebhookService { slack: this.sendToSlack.bind(this), discord: this.sendToDiscord.bind(this), custom: this.sendToCustom.bind(this), - bark: this.sendToBark.bind(this) + bark: this.sendToBark.bind(this), + smtp: this.sendToSMTP.bind(this) } this.timezone = appConfig.system.timezone || 'Asia/Shanghai' } @@ -243,6 +245,51 @@ class WebhookService { await this.sendHttpRequest(url, payload, platform.timeout || 10000) } + /** + * SMTP邮件通知 + */ + async sendToSMTP(platform, type, data) { + try { + // 创建SMTP传输器 + const transporter = nodemailer.createTransport({ + host: platform.host, + port: platform.port || 587, + secure: platform.secure || false, // true for 465, false for other ports + auth: { + user: platform.user, + pass: platform.pass + }, + // 可选的TLS配置 + tls: platform.ignoreTLS ? { rejectUnauthorized: false } : undefined, + // 连接超时 + connectionTimeout: platform.timeout || 10000 + }) + + // 构造邮件内容 + const subject = this.getNotificationTitle(type) + const htmlContent = this.formatMessageForEmail(type, data) + const textContent = this.formatMessageForEmailText(type, data) + + // 邮件选项 + const mailOptions = { + from: platform.from || platform.user, // 发送者 + to: platform.to, // 接收者(必填) + subject: `[Claude Relay Service] ${subject}`, + text: textContent, + html: htmlContent + } + + // 发送邮件 + const info = await transporter.sendMail(mailOptions) + logger.info(`✅ 邮件发送成功: ${info.messageId}`) + + return info + } catch (error) { + logger.error('SMTP邮件发送失败:', error) + throw error + } + } + /** * 发送HTTP请求 */ @@ -459,6 +506,121 @@ class WebhookService { return lines.join('\n') } + /** + * 构建通知详情数据 + */ + buildNotificationDetails(data) { + const details = [] + + if (data.accountName) { + details.push({ label: '账号', value: data.accountName }) + } + if (data.platform) { + details.push({ label: '平台', value: data.platform }) + } + if (data.status) { + details.push({ label: '状态', value: data.status, color: this.getStatusColor(data.status) }) + } + if (data.errorCode) { + details.push({ label: '错误代码', value: data.errorCode, isCode: true }) + } + if (data.reason) { + details.push({ label: '原因', value: data.reason }) + } + if (data.message) { + details.push({ label: '消息', value: data.message }) + } + if (data.quota) { + details.push({ label: '配额', value: `${data.quota.remaining}/${data.quota.total}` }) + } + if (data.usage) { + details.push({ label: '使用率', value: `${data.usage}%` }) + } + + return details + } + + /** + * 格式化邮件HTML内容 + */ + formatMessageForEmail(type, data) { + const title = this.getNotificationTitle(type) + const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone }) + const details = this.buildNotificationDetails(data) + + let content = ` +
+
+

${title}

+

Claude Relay Service

+
+
+
+ ` + + // 使用统一的详情数据渲染 + details.forEach((detail) => { + if (detail.isCode) { + content += `

${detail.label}: ${detail.value}

` + } else if (detail.color) { + content += `

${detail.label}: ${detail.value}

` + } else { + content += `

${detail.label}: ${detail.value}

` + } + }) + + content += ` +
+
+

发送时间: ${timestamp}

+

此邮件由 Claude Relay Service 自动发送

+
+
+
+ ` + + return content + } + + /** + * 格式化邮件纯文本内容 + */ + formatMessageForEmailText(type, data) { + const title = this.getNotificationTitle(type) + const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone }) + const details = this.buildNotificationDetails(data) + + let content = `${title}\n` + content += `=====================================\n\n` + + // 使用统一的详情数据渲染 + details.forEach((detail) => { + content += `${detail.label}: ${detail.value}\n` + }) + + content += `\n发送时间: ${timestamp}\n` + content += `服务: Claude Relay Service\n` + content += `=====================================\n` + content += `此邮件由系统自动发送,请勿回复。` + + return content + } + + /** + * 获取状态颜色 + */ + getStatusColor(status) { + const colors = { + error: '#dc3545', + unauthorized: '#fd7e14', + blocked: '#6f42c1', + disabled: '#6c757d', + active: '#28a745', + warning: '#ffc107' + } + return colors[status] || '#007bff' + } + /** * 格式化通知详情 */ diff --git a/web/admin-spa/src/views/SettingsView.vue b/web/admin-spa/src/views/SettingsView.vue index 3fd7b3ad..770f43c3 100644 --- a/web/admin-spa/src/views/SettingsView.vue +++ b/web/admin-spa/src/views/SettingsView.vue @@ -471,10 +471,22 @@
-
+
{{ platform.url }}
+
+ + {{ + Array.isArray(platform.to) ? platform.to.join(', ') : platform.to + }} +
🟣 Slack +
@@ -684,8 +697,8 @@ />
- -
+ +
+ +
+ +
+ + +
+ + +
+
+ + +

+ 默认: 587 (TLS) 或 465 (SSL) +

+
+ +
+ + +
+
+ + +
+ + +
+ + +
+ + +

+ 建议使用应用专用密码,而非邮箱登录密码 +

+
+ + +
+ + +
+ + +
+ + +

接收通知的邮箱地址

+
+
+
{ if (platformForm.value.type === 'bark') { // Bark平台需要deviceKey return !!platformForm.value.deviceKey + } else if (platformForm.value.type === 'smtp') { + // SMTP平台需要必要的配置 + return !!( + platformForm.value.host && + platformForm.value.user && + platformForm.value.pass && + platformForm.value.to + ) } else { // 其他平台需要URL且URL格式正确 return !!platformForm.value.url && !urlError.value @@ -1134,8 +1337,8 @@ const saveWebhookConfig = async () => { // 验证 URL const validateUrl = () => { - // Bark平台不需要验证URL - if (platformForm.value.type === 'bark') { + // Bark和SMTP平台不需要验证URL + if (platformForm.value.type === 'bark' || platformForm.value.type === 'smtp') { urlError.value = false urlValid.value = false return @@ -1163,27 +1366,46 @@ const validateUrl = () => { } } -// 添加/更新平台 -const savePlatform = async () => { - if (!isMounted.value) return - - // Bark平台只需要deviceKey,其他平台需要URL +// 验证平台配置 +const validatePlatformForm = () => { if (platformForm.value.type === 'bark') { if (!platformForm.value.deviceKey) { showToast('请输入Bark设备密钥', 'error') - return + return false + } + } else if (platformForm.value.type === 'smtp') { + const requiredFields = [ + { field: 'host', message: 'SMTP服务器' }, + { field: 'user', message: '用户名' }, + { field: 'pass', message: '密码' }, + { field: 'to', message: '收件人邮箱' } + ] + + for (const { field, message } of requiredFields) { + if (!platformForm.value[field]) { + showToast(`请输入${message}`, 'error') + return false + } } } else { if (!platformForm.value.url) { showToast('请输入Webhook URL', 'error') - return + return false } - if (urlError.value) { showToast('请输入有效的Webhook URL', 'error') - return + return false } } + return true +} + +// 添加/更新平台 +const savePlatform = async () => { + if (!isMounted.value) return + + // 验证表单 + if (!validatePlatformForm()) return savingPlatform.value = true try { @@ -1300,7 +1522,7 @@ const testPlatform = async (platform) => { signal: abortController.value.signal }) if (response.success && isMounted.value) { - showToast('测试成功,webhook连接正常', 'success') + showToast('测试成功', 'success') } } catch (error) { if (error.name === 'AbortError') return @@ -1314,24 +1536,8 @@ const testPlatform = async (platform) => { const testPlatformForm = async () => { if (!isMounted.value) return - // Bark平台验证 - if (platformForm.value.type === 'bark') { - if (!platformForm.value.deviceKey) { - showToast('请先输入Bark设备密钥', 'error') - return - } - } else { - // 其他平台验证URL - if (!platformForm.value.url) { - showToast('请先输入Webhook URL', 'error') - return - } - - if (urlError.value) { - showToast('请输入有效的Webhook URL', 'error') - return - } - } + // 验证表单 + if (!validatePlatformForm()) return testingConnection.value = true try { @@ -1339,7 +1545,7 @@ const testPlatformForm = async () => { signal: abortController.value.signal }) if (response.success && isMounted.value) { - showToast('测试成功,webhook连接正常', 'success') + showToast('测试成功', 'success') } } catch (error) { if (error.name === 'AbortError') return @@ -1397,7 +1603,17 @@ const closePlatformModal = () => { serverUrl: '', level: '', sound: '', - group: '' + group: '', + // SMTP特有字段 + host: '', + port: null, + secure: false, + user: '', + pass: '', + from: '', + to: '', + timeout: null, + ignoreTLS: false } urlError.value = false urlValid.value = false @@ -1415,6 +1631,7 @@ const getPlatformName = (type) => { slack: 'Slack', discord: 'Discord', bark: 'Bark', + smtp: '邮件通知', custom: '自定义' } return names[type] || type @@ -1428,6 +1645,7 @@ const getPlatformIcon = (type) => { slack: 'fab fa-slack text-purple-600', discord: 'fab fa-discord text-indigo-600', bark: 'fas fa-bell text-orange-500', + smtp: 'fas fa-envelope text-blue-600', custom: 'fas fa-webhook text-gray-600' } return icons[type] || 'fas fa-bell' @@ -1441,6 +1659,7 @@ const getWebhookHint = (type) => { slack: '请在Slack应用的Incoming Webhooks中获取地址', discord: '请在Discord服务器的集成设置中创建Webhook', bark: '请在Bark App中查看您的设备密钥', + smtp: '请配置SMTP服务器信息,支持Gmail、QQ邮箱等', custom: '请输入完整的Webhook接收地址' } return hints[type] || '' From 268a2622811f8d442bc8de550d6581b03ebaf611 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 04:02:13 +0800 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DSMTP=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=B5=8B=E8=AF=95=E6=8C=89=E9=92=AE=E7=BC=BA=E5=B0=91?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E5=AD=97=E6=AE=B5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在testPlatform函数中添加SMTP平台的完整字段传递 - 确保测试时包含host, port, user, pass, to等必填字段 --- web/admin-spa/src/views/SettingsView.vue | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/admin-spa/src/views/SettingsView.vue b/web/admin-spa/src/views/SettingsView.vue index 770f43c3..e8a57c99 100644 --- a/web/admin-spa/src/views/SettingsView.vue +++ b/web/admin-spa/src/views/SettingsView.vue @@ -1514,6 +1514,15 @@ const testPlatform = async (platform) => { testData.level = platform.level testData.sound = platform.sound testData.group = platform.group + } else if (platform.type === 'smtp') { + testData.host = platform.host + testData.port = platform.port + testData.secure = platform.secure + testData.user = platform.user + testData.pass = platform.pass + testData.from = platform.from + testData.to = platform.to + testData.ignoreTLS = platform.ignoreTLS } else { testData.url = platform.url } From 5d677b4f170562ee432554ecd8c56efa9bf7bd29 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 04:09:51 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96UI=E6=96=87?= =?UTF-8?q?=E6=A1=88=E5=92=8C=E4=BF=AE=E5=A4=8DSMTP=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复SMTP平台测试按钮传递必要字段 - 添加"测试通知"类型的友好显示文本和描述 - 简化标题:"启用 Webhook 通知" → "启用通知" --- web/admin-spa/src/views/SettingsView.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/admin-spa/src/views/SettingsView.vue b/web/admin-spa/src/views/SettingsView.vue index e8a57c99..4c6355c5 100644 --- a/web/admin-spa/src/views/SettingsView.vue +++ b/web/admin-spa/src/views/SettingsView.vue @@ -377,9 +377,7 @@ >
-

- 启用 Webhook 通知 -

+

启用通知

开启后,系统将按配置发送通知到指定平台

@@ -1679,7 +1677,8 @@ const getNotificationTypeName = (type) => { accountAnomaly: '账号异常', quotaWarning: '配额警告', systemError: '系统错误', - securityAlert: '安全警报' + securityAlert: '安全警报', + test: '测试通知' } return names[type] || type } @@ -1689,7 +1688,8 @@ const getNotificationTypeDescription = (type) => { accountAnomaly: '账号状态异常、认证失败等', quotaWarning: 'API调用配额不足警告', systemError: '系统运行错误和故障', - securityAlert: '安全相关的警报通知' + securityAlert: '安全相关的警报通知', + test: '用于测试Webhook连接是否正常' } return descriptions[type] || '' } From 52820a7e49789142c89a4e8143bfdc1c26153eed Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 04:14:27 +0800 Subject: [PATCH 08/11] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=20Prettier=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 格式化 src/app.js - 格式化 src/services/claudeConsoleRelayService.js - 格式化 src/services/claudeRelayService.js --- src/app.js | 4 +-- src/services/claudeConsoleRelayService.js | 29 ++++++++++++++---- src/services/claudeRelayService.js | 37 +++++++++++++++-------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/app.js b/src/app.js index 67f26bfe..a1f06b36 100644 --- a/src/app.js +++ b/src/app.js @@ -537,7 +537,7 @@ class Application { logger.info( `🔄 Cleanup tasks scheduled every ${config.system.cleanupInterval / 1000 / 60} minutes` ) - + // 🚨 启动限流状态自动清理服务 // 每5分钟检查一次过期的限流状态,确保账号能及时恢复调度 const rateLimitCleanupService = require('./services/rateLimitCleanupService') @@ -563,7 +563,7 @@ class Application { } catch (error) { logger.error('❌ Error cleaning up pricing service:', error) } - + // 停止限流清理服务 try { const rateLimitCleanupService = require('./services/rateLimitCleanupService') diff --git a/src/services/claudeConsoleRelayService.js b/src/services/claudeConsoleRelayService.js index 1e040ee9..6a88166d 100644 --- a/src/services/claudeConsoleRelayService.js +++ b/src/services/claudeConsoleRelayService.js @@ -222,7 +222,10 @@ class ClaudeConsoleRelayService { throw new Error('Client disconnected') } - logger.error(`❌ Claude Console relay request failed (Account: ${account?.name || accountId}):`, error.message) + logger.error( + `❌ Claude Console relay request failed (Account: ${account?.name || accountId}):`, + error.message + ) // 不再因为模型不支持而block账号 @@ -297,7 +300,10 @@ class ClaudeConsoleRelayService { // 更新最后使用时间 await this._updateLastUsedTime(accountId) } catch (error) { - logger.error(`❌ Claude Console stream relay failed (Account: ${account?.name || accountId}):`, error) + logger.error( + `❌ Claude Console stream relay failed (Account: ${account?.name || accountId}):`, + error + ) throw error } } @@ -376,7 +382,9 @@ class ClaudeConsoleRelayService { // 错误响应处理 if (response.status !== 200) { - logger.error(`❌ Claude Console API returned error status: ${response.status} | Account: ${account?.name || accountId}`) + logger.error( + `❌ Claude Console API returned error status: ${response.status} | Account: ${account?.name || accountId}` + ) if (response.status === 401) { claudeConsoleAccountService.markAccountUnauthorized(accountId) @@ -528,7 +536,10 @@ class ClaudeConsoleRelayService { } } } catch (error) { - logger.error(`❌ Error processing Claude Console stream data (Account: ${account?.name || accountId}):`, error) + logger.error( + `❌ Error processing Claude Console stream data (Account: ${account?.name || accountId}):`, + error + ) if (!responseStream.destroyed) { responseStream.write('event: error\n') responseStream.write( @@ -570,7 +581,10 @@ class ClaudeConsoleRelayService { }) response.data.on('error', (error) => { - logger.error(`❌ Claude Console stream error (Account: ${account?.name || accountId}):`, error) + logger.error( + `❌ Claude Console stream error (Account: ${account?.name || accountId}):`, + error + ) if (!responseStream.destroyed) { responseStream.write('event: error\n') responseStream.write( @@ -590,7 +604,10 @@ class ClaudeConsoleRelayService { return } - logger.error(`❌ Claude Console stream request error (Account: ${account?.name || accountId}):`, error.message) + logger.error( + `❌ Claude Console stream request error (Account: ${account?.name || accountId}):`, + error.message + ) // 检查错误状态 if (error.response) { diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 577c6236..658391b8 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -1015,7 +1015,9 @@ class ClaudeRelayService { logger.error('❌ Error in stream error handler:', err) }) - logger.error(`❌ Claude API returned error status: ${res.statusCode} | Account: ${account?.name || accountId}`) + logger.error( + `❌ Claude API returned error status: ${res.statusCode} | Account: ${account?.name || accountId}` + ) let errorData = '' res.on('data', (chunk) => { @@ -1024,7 +1026,10 @@ class ClaudeRelayService { res.on('end', () => { console.error(': ❌ ', errorData) - logger.error(`❌ Claude API error response (Account: ${account?.name || accountId}):`, errorData) + logger.error( + `❌ Claude API error response (Account: ${account?.name || accountId}):`, + errorData + ) if (!responseStream.destroyed) { // 发送错误事件 responseStream.write('event: error\n') @@ -1343,11 +1348,15 @@ class ClaudeRelayService { }) req.on('error', async (error) => { - logger.error(`❌ Claude stream request error (Account: ${account?.name || accountId}):`, error.message, { - code: error.code, - errno: error.errno, - syscall: error.syscall - }) + logger.error( + `❌ Claude stream request error (Account: ${account?.name || accountId}):`, + error.message, + { + code: error.code, + errno: error.errno, + syscall: error.syscall + } + ) // 根据错误类型提供更具体的错误信息 let errorMessage = 'Upstream request failed' @@ -1493,11 +1502,15 @@ class ClaudeRelayService { }) req.on('error', async (error) => { - logger.error(`❌ Claude stream request error (Account: ${account?.name || accountId}):`, error.message, { - code: error.code, - errno: error.errno, - syscall: error.syscall - }) + logger.error( + `❌ Claude stream request error (Account: ${account?.name || accountId}):`, + error.message, + { + code: error.code, + errno: error.errno, + syscall: error.syscall + } + ) // 根据错误类型提供更具体的错误信息 let errorMessage = 'Upstream request failed' From f375f9f841ebae9cb1867bd92c669c70619e7001 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 11:10:27 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ESLint=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF=20-=20=E8=A7=A3=E5=86=B3=E6=9C=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=8F=98=E9=87=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - claudeConsoleRelayService.js: 将 account 变量声明提到更高作用域 - claudeRelayService.js: 移除 _makeClaudeStreamRequest 函数中的未定义变量引用 --- src/services/claudeConsoleRelayService.js | 6 ++++-- src/services/claudeRelayService.js | 16 ++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/services/claudeConsoleRelayService.js b/src/services/claudeConsoleRelayService.js index 6a88166d..6b97c881 100644 --- a/src/services/claudeConsoleRelayService.js +++ b/src/services/claudeConsoleRelayService.js @@ -19,10 +19,11 @@ class ClaudeConsoleRelayService { options = {} ) { let abortController = null + let account = null try { // 获取账户信息 - const account = await claudeConsoleAccountService.getAccount(accountId) + account = await claudeConsoleAccountService.getAccount(accountId) if (!account) { throw new Error('Claude Console Claude account not found') } @@ -244,9 +245,10 @@ class ClaudeConsoleRelayService { streamTransformer = null, options = {} ) { + let account = null try { // 获取账户信息 - const account = await claudeConsoleAccountService.getAccount(accountId) + account = await claudeConsoleAccountService.getAccount(accountId) if (!account) { throw new Error('Claude Console Claude account not found') } diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 658391b8..ac80cc6a 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -1502,15 +1502,11 @@ class ClaudeRelayService { }) req.on('error', async (error) => { - logger.error( - `❌ Claude stream request error (Account: ${account?.name || accountId}):`, - error.message, - { - code: error.code, - errno: error.errno, - syscall: error.syscall - } - ) + logger.error(`❌ Claude stream request error:`, error.message, { + code: error.code, + errno: error.errno, + syscall: error.syscall + }) // 根据错误类型提供更具体的错误信息 let errorMessage = 'Upstream request failed' @@ -1554,7 +1550,7 @@ class ClaudeRelayService { req.on('timeout', async () => { req.destroy() - logger.error(`❌ Claude stream request timeout | Account: ${account?.name || accountId}`) + logger.error(`❌ Claude stream request timeout`) if (!responseStream.headersSent) { responseStream.writeHead(504, { From 4ee9e0b546e7bf2a48a9b3fd55f558e0ed45a31e Mon Sep 17 00:00:00 2001 From: Feng Yue <2525275@gmail.com> Date: Tue, 9 Sep 2025 12:52:34 +0800 Subject: [PATCH 10/11] API Keys created by users have all permissions by default --- src/routes/userRoutes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js index f4f995c1..4952dd5e 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/userRoutes.js @@ -315,7 +315,8 @@ router.post('/api-keys', authenticateUser, async (req, res) => { expiresAt: expiresAt || null, dailyCostLimit: dailyCostLimit || null, createdBy: 'user', - permissions: ['messages'] // 用户创建的API Key默认只有messages权限 + // 设置服务权限为全部服务,确保前端显示“服务权限”为“全部服务”且具备完整访问权限 + permissions: 'all' } const newApiKey = await apiKeyService.createApiKey(apiKeyData) From 929852a881313cda478518cacdbf09890aad2f31 Mon Sep 17 00:00:00 2001 From: iaineng Date: Tue, 9 Sep 2025 13:06:25 +0800 Subject: [PATCH 11/11] fix(ui): correct table row striping in API Keys view Fixed second row appearing white due to invalid Tailwind CSS class bg-gray-850. Replaced with valid bg-gray-700/30 for odd rows and kept bg-gray-800/40 for even rows to enhance dark mode contrast. --- web/admin-spa/src/views/ApiKeysView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index c9bfab01..9a2da27a 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -404,7 +404,7 @@ 'table-row transition-all duration-150', index % 2 === 0 ? 'bg-white dark:bg-gray-800/40' - : 'dark:bg-gray-850/40 bg-gray-50/70', + : 'bg-gray-50/70 dark:bg-gray-700/30', 'border-b-2 border-gray-200/80 dark:border-gray-700/50', 'hover:bg-blue-50/60 hover:shadow-sm dark:hover:bg-blue-900/20' ]"