diff --git a/src/app.js b/src/app.js index 67f26bfe..88b32fad 100644 --- a/src/app.js +++ b/src/app.js @@ -34,6 +34,7 @@ const { globalRateLimit, requestSizeLimit } = require('./middleware/auth') +const { browserFallbackMiddleware } = require('./middleware/browserFallback') class Application { constructor() { @@ -108,6 +109,9 @@ class Application { } else { this.app.use(corsMiddleware) } + + // 🆕 兜底中间件:处理Chrome插件兼容性(必须在认证之前) + this.app.use(browserFallbackMiddleware) // 📦 压缩 - 排除流式响应(SSE) this.app.use( diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 4d4364ac..a3f8c32a 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -757,7 +757,7 @@ const requireAdmin = (req, res, next) => { // 注意:使用统计现在直接在/api/v1/messages路由中处理, // 以便从Claude API响应中提取真实的usage数据 -// 🚦 CORS中间件(优化版) +// 🚦 CORS中间件(优化版,支持Chrome插件) const corsMiddleware = (req, res, next) => { const { origin } = req.headers @@ -769,8 +769,11 @@ const corsMiddleware = (req, res, next) => { 'https://127.0.0.1:3000' ] + // 🆕 检查是否为Chrome插件请求 + const isChromeExtension = origin && origin.startsWith('chrome-extension://') + // 设置CORS头 - if (allowedOrigins.includes(origin) || !origin) { + if (allowedOrigins.includes(origin) || !origin || isChromeExtension) { res.header('Access-Control-Allow-Origin', origin || '*') } @@ -785,7 +788,9 @@ const corsMiddleware = (req, res, next) => { 'Authorization', 'x-api-key', 'api-key', - 'x-admin-token' + 'x-admin-token', + 'anthropic-version', + 'anthropic-dangerous-direct-browser-access' ].join(', ') ) diff --git a/src/middleware/browserFallback.js b/src/middleware/browserFallback.js new file mode 100644 index 00000000..d8b66083 --- /dev/null +++ b/src/middleware/browserFallback.js @@ -0,0 +1,50 @@ +const logger = require('../utils/logger') + +/** + * 浏览器/Chrome插件兜底中间件 + * 专门处理第三方插件的兼容性问题 + */ +const browserFallbackMiddleware = (req, res, next) => { + const userAgent = req.headers['user-agent'] || '' + const origin = req.headers['origin'] || '' + const authHeader = req.headers['authorization'] || req.headers['x-api-key'] || '' + + // 检查是否为Chrome插件或浏览器请求 + const isChromeExtension = origin.startsWith('chrome-extension://') + const isBrowserRequest = userAgent.includes('Mozilla/') && userAgent.includes('Chrome/') + const hasApiKey = authHeader.startsWith('cr_') // 我们的API Key格式 + + if ((isChromeExtension || isBrowserRequest) && hasApiKey) { + // 为Chrome插件请求添加特殊标记 + req.isBrowserFallback = true + req.originalUserAgent = userAgent + + // 🆕 关键修改:伪装成claude-cli请求以绕过客户端限制 + req.headers['user-agent'] = 'claude-cli/1.0.110 (external, cli, browser-fallback)' + + // 确保设置正确的认证头 + if (!req.headers['authorization'] && req.headers['x-api-key']) { + req.headers['authorization'] = `Bearer ${req.headers['x-api-key']}` + } + + // 添加必要的Anthropic头 + if (!req.headers['anthropic-version']) { + req.headers['anthropic-version'] = '2023-06-01' + } + + if (!req.headers['anthropic-dangerous-direct-browser-access']) { + req.headers['anthropic-dangerous-direct-browser-access'] = 'true' + } + + logger.api(`🔧 Browser fallback activated for ${isChromeExtension ? 'Chrome extension' : 'browser'} request`) + logger.api(` Original User-Agent: "${req.originalUserAgent}"`) + logger.api(` Origin: "${origin}"`) + logger.api(` Modified User-Agent: "${req.headers['user-agent']}"`) + } + + next() +} + +module.exports = { + browserFallbackMiddleware +} \ No newline at end of file diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index f4e03334..43fce0b9 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -184,22 +184,11 @@ class ClaudeRelayService { // 记录401错误 await this.recordUnauthorizedError(accountId) - // 检查是否需要标记为异常(遇到1次401就停止调度) + // 记录401错误但不停用账号(根据用户要求,401错误永远不会导致账号不可用) const errorCount = await this.getUnauthorizedErrorCount(accountId) - logger.info( - `🔐 Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes` + logger.warn( + `🔐 Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes - account remains active` ) - - if (errorCount >= 1) { - logger.error( - `❌ Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized` - ) - await unifiedClaudeScheduler.markAccountUnauthorized( - accountId, - accountType, - sessionHash - ) - } } // 检查是否为403状态码(禁止访问) else if (response.statusCode === 403) { @@ -598,8 +587,30 @@ class ClaudeRelayService { 'transfer-encoding' ] + // 🆕 需要移除的浏览器相关 headers(避免CORS问题) + const browserHeaders = [ + 'origin', + 'referer', + 'sec-fetch-mode', + 'sec-fetch-site', + 'sec-fetch-dest', + 'sec-ch-ua', + 'sec-ch-ua-mobile', + 'sec-ch-ua-platform', + 'accept-language', + 'accept-encoding', + 'accept', + 'cache-control', + 'pragma', + 'anthropic-dangerous-direct-browser-access' // 这个头可能触发CORS检查 + ] + // 应该保留的 headers(用于会话一致性和追踪) - const allowedHeaders = ['x-request-id'] + const allowedHeaders = [ + 'x-request-id', + 'anthropic-version', // 保留API版本 + 'anthropic-beta' // 保留beta功能 + ] const filteredHeaders = {} @@ -610,8 +621,8 @@ class ClaudeRelayService { if (allowedHeaders.includes(lowerKey)) { filteredHeaders[key] = clientHeaders[key] } - // 如果不在敏感列表中,也保留 - else if (!sensitiveHeaders.includes(lowerKey)) { + // 如果不在敏感列表和浏览器列表中,也保留 + else if (!sensitiveHeaders.includes(lowerKey) && !browserHeaders.includes(lowerKey)) { filteredHeaders[key] = clientHeaders[key] } }) @@ -983,20 +994,9 @@ class ClaudeRelayService { await this.recordUnauthorizedError(accountId) const errorCount = await this.getUnauthorizedErrorCount(accountId) - logger.info( - `🔐 [Stream] Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes` + logger.warn( + `🔐 [Stream] Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes - account remains active` ) - - if (errorCount >= 1) { - logger.error( - `❌ [Stream] Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized` - ) - await unifiedClaudeScheduler.markAccountUnauthorized( - accountId, - accountType, - sessionHash - ) - } } else if (res.statusCode === 403) { logger.error( `🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked`