From 39c49fe2bb8f27ef6cb67673f679bb6887e0c1df Mon Sep 17 00:00:00 2001 From: sczheng189 <724100151@qq.com> Date: Wed, 3 Sep 2025 20:14:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BB=9F=E4=B8=80Cla?= =?UTF-8?q?ude=20Code=20User-Agent=E6=94=AF=E6=8C=81=E5=8F=8A=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=20###=20**?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=8A=9F=E8=83=BD**=20*=20=20=20**=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=9B=B4=E6=96=B0**=EF=BC=9A=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=B9=B6=E4=BD=BF=E7=94=A8=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=9A=84=20=E2=80=9CClaude=20Code=E2=80=9D=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E7=89=88=E6=9C=AC=E5=8F=B7=E3=80=82=20*=20?= =?UTF-8?q?=20=20**=E6=99=BA=E8=83=BD=E7=BC=93=E5=AD=98**=EF=BC=9A?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=88=B0=E7=9A=84=E7=89=88=E6=9C=AC=E4=BC=9A?= =?UTF-8?q?=E7=BC=93=E5=AD=9825=E5=B0=8F=E6=97=B6=EF=BC=8C=E7=84=B6?= =?UTF-8?q?=E5=90=8E=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0=E3=80=82=20*=20?= =?UTF-8?q?=20=20**=E7=8B=AC=E7=AB=8B=E5=BC=80=E5=85=B3**=EF=BC=9A?= =?UTF-8?q?=E6=AF=8F=E4=B8=AA=E8=B4=A6=E6=88=B7=E9=83=BD=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E5=8D=95=E7=8B=AC=E8=AE=BE=E7=BD=AE=E6=98=AF=E5=90=A6=E5=90=AF?= =?UTF-8?q?=E7=94=A8=E6=AD=A4=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### **前端界面** * **新增开关**:账户设置里增加了“使用统一版本”的选项。 * **信息显示**:能直接看到当前正在使用的版本号。 * **手动刷新**:提供“清除缓存”按钮,可手动强制更新。 ### **后端技术** * **核心方法**:开发了新的后台功能,用于捕获、比较和管理版本号。 * **管理接口**:为管理员提供了新的API (`/admin/claude-code-version`),方便查询和刷新。 --- src/routes/admin.js | 50 ++++++ src/services/claudeAccountService.js | 10 +- src/services/claudeRelayService.js | 141 +++++++++++++++- .../src/components/accounts/AccountForm.vue | 151 +++++++++++++++++- 4 files changed, 341 insertions(+), 11 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 49d43fb2..a37a0942 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -5961,4 +5961,54 @@ router.post('/migrate-api-keys-azure', authenticateAdmin, async (req, res) => { } }) +// 📋 获取统一Claude Code User-Agent信息 +router.get('/claude-code-version', authenticateAdmin, async (req, res) => { + try { + const CACHE_KEY = 'claude_code_user_agent:daily' + + // 获取缓存的统一User-Agent + const unifiedUserAgent = await redis.client.get(CACHE_KEY) + const ttl = unifiedUserAgent ? await redis.client.ttl(CACHE_KEY) : 0 + + res.json({ + success: true, + userAgent: unifiedUserAgent, + isActive: !!unifiedUserAgent, + ttlSeconds: ttl, + lastUpdated: unifiedUserAgent ? new Date().toISOString() : null + }) + } catch (error) { + logger.error('❌ Get unified Claude Code User-Agent error:', error) + res.status(500).json({ + success: false, + message: 'Failed to get User-Agent information', + error: error.message + }) + } +}) + +// 🗑️ 清除统一Claude Code User-Agent缓存 +router.post('/claude-code-version/clear', authenticateAdmin, async (req, res) => { + try { + const CACHE_KEY = 'claude_code_user_agent:daily' + + // 删除缓存的统一User-Agent + await redis.client.del(CACHE_KEY) + + logger.info(`🗑️ Admin manually cleared unified Claude Code User-Agent cache`) + + res.json({ + success: true, + message: 'Unified User-Agent cache cleared successfully' + }) + } catch (error) { + logger.error('❌ Clear unified User-Agent cache error:', error) + res.status(500).json({ + success: false, + message: 'Failed to clear cache', + error: error.message + }) + } +}) + module.exports = router diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 1540b74b..08606550 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -59,7 +59,8 @@ class ClaudeAccountService { priority = 50, // 调度优先级 (1-100,数字越小优先级越高) schedulable = true, // 是否可被调度 subscriptionInfo = null, // 手动设置的订阅信息 - autoStopOnWarning = false // 5小时使用量接近限制时自动停止调度 + autoStopOnWarning = false, // 5小时使用量接近限制时自动停止调度 + useUnifiedUserAgent = false // 是否使用统一Claude Code版本的User-Agent } = options const accountId = uuidv4() @@ -91,6 +92,7 @@ class ClaudeAccountService { errorMessage: '', schedulable: schedulable.toString(), // 是否可被调度 autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度 + useUnifiedUserAgent: useUnifiedUserAgent.toString(), // 是否使用统一Claude Code版本的User-Agent // 优先使用手动设置的订阅信息,否则使用OAuth数据中的,否则默认为空 subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) @@ -122,6 +124,7 @@ class ClaudeAccountService { errorMessage: '', schedulable: schedulable.toString(), // 是否可被调度 autoStopOnWarning: autoStopOnWarning.toString(), // 5小时使用量接近限制时自动停止调度 + useUnifiedUserAgent: useUnifiedUserAgent.toString(), // 是否使用统一Claude Code版本的User-Agent // 手动设置的订阅信息 subscriptionInfo: subscriptionInfo ? JSON.stringify(subscriptionInfo) : '' } @@ -487,6 +490,8 @@ class ClaudeAccountService { schedulable: account.schedulable !== 'false', // 默认为true,兼容历史数据 // 添加自动停止调度设置 autoStopOnWarning: account.autoStopOnWarning === 'true', // 默认为false + // 添加统一User-Agent设置 + useUnifiedUserAgent: account.useUnifiedUserAgent === 'true', // 默认为false // 添加停止原因 stoppedReason: account.stoppedReason || null } @@ -522,7 +527,8 @@ class ClaudeAccountService { 'priority', 'schedulable', 'subscriptionInfo', - 'autoStopOnWarning' + 'autoStopOnWarning', + 'useUnifiedUserAgent' ] const updatedData = { ...accountData } diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index e285dea8..4a0f48da 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -9,6 +9,7 @@ const sessionHelper = require('../utils/sessionHelper') const logger = require('../utils/logger') const config = require('../../config/config') const claudeCodeHeadersService = require('./claudeCodeHeadersService') +const redis = require('../models/redis') class ClaudeRelayService { constructor() { @@ -610,6 +611,12 @@ class ClaudeRelayService { ) { const url = new URL(this.claudeApiUrl) + // 获取账户信息用于统一 User-Agent + const account = await claudeAccountService.getAccount(accountId) + + // 获取统一的 User-Agent + const unifiedUA = await this.captureAndGetUnifiedUserAgent(clientHeaders, account) + // 获取过滤后的客户端 headers const filteredHeaders = this._filterClientHeaders(clientHeaders) @@ -656,11 +663,19 @@ class ClaudeRelayService { timeout: config.proxy.timeout } - // 如果客户端没有提供 User-Agent,使用默认值 + // 使用统一 User-Agent 或客户端提供的,最后使用默认值 if (!options.headers['User-Agent'] && !options.headers['user-agent']) { - options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)' + const userAgent = + unifiedUA || + clientHeaders?.['user-agent'] || + clientHeaders?.['User-Agent'] || + 'claude-cli/1.0.102 (external, cli)' + options.headers['User-Agent'] = userAgent } + logger.info(`🔗 指纹是这个: ${options.headers['User-Agent']}`) + logger.info(`🔗 指纹是这个: ${options.headers['user-agent']}`) + // 使用自定义的 betaHeader 或默认值 const betaHeader = requestOptions?.betaHeader !== undefined ? requestOptions.betaHeader : this.betaHeader @@ -868,6 +883,12 @@ class ClaudeRelayService { streamTransformer = null, requestOptions = {} ) { + // 获取账户信息用于统一 User-Agent + const account = await claudeAccountService.getAccount(accountId) + + // 获取统一的 User-Agent + const unifiedUA = await this.captureAndGetUnifiedUserAgent(clientHeaders, account) + // 获取过滤后的客户端 headers const filteredHeaders = this._filterClientHeaders(clientHeaders) @@ -908,9 +929,14 @@ class ClaudeRelayService { timeout: config.proxy.timeout } - // 如果客户端没有提供 User-Agent,使用默认值 + // 使用统一 User-Agent 或客户端提供的,最后使用默认值 if (!options.headers['User-Agent'] && !options.headers['user-agent']) { - options.headers['User-Agent'] = 'claude-cli/1.0.57 (external, cli)' + const userAgent = + unifiedUA || + clientHeaders?.['user-agent'] || + clientHeaders?.['User-Agent'] || + 'claude-cli/1.0.102 (external, cli)' + options.headers['User-Agent'] = userAgent } // 使用自定义的 betaHeader 或默认值 @@ -1398,7 +1424,12 @@ class ClaudeRelayService { // 如果客户端没有提供 User-Agent,使用默认值 if (!filteredHeaders['User-Agent'] && !filteredHeaders['user-agent']) { - options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)' + // 第三个方法不支持统一 User-Agent,使用简化逻辑 + const userAgent = + clientHeaders?.['user-agent'] || + clientHeaders?.['User-Agent'] || + 'claude-cli/1.0.102 (external, cli)' + options.headers['User-Agent'] = userAgent } // 使用自定义的 betaHeader 或默认值 @@ -1535,7 +1566,6 @@ class ClaudeRelayService { async recordUnauthorizedError(accountId) { try { const key = `claude_account:${accountId}:401_errors` - const redis = require('../models/redis') // 增加错误计数,设置5分钟过期时间 await redis.client.incr(key) @@ -1551,7 +1581,6 @@ class ClaudeRelayService { async getUnauthorizedErrorCount(accountId) { try { const key = `claude_account:${accountId}:401_errors` - const redis = require('../models/redis') const count = await redis.client.get(key) return parseInt(count) || 0 @@ -1565,7 +1594,6 @@ class ClaudeRelayService { async clearUnauthorizedErrors(accountId) { try { const key = `claude_account:${accountId}:401_errors` - const redis = require('../models/redis') await redis.client.del(key) logger.info(`✅ Cleared 401 error count for account ${accountId}`) @@ -1574,6 +1602,103 @@ class ClaudeRelayService { } } + // 🔧 动态捕获并获取统一的 User-Agent + async captureAndGetUnifiedUserAgent(clientHeaders, account) { + if (account.useUnifiedUserAgent !== 'true') { + return null + } + + const CACHE_KEY = 'claude_code_user_agent:daily' + const TTL = 90000 // 25小时 + + // ⚠️ 重要:这里通过 'claude-cli/' 判断是否为 Claude Code 客户端 + // 如果未来 Claude Code 的 User-Agent 格式发生变化(不再包含 'claude-cli/'), + // 需要更新这个判断条件! + // 当前已知格式:claude-cli/1.0.102 (external, cli) + const CLAUDE_CODE_UA_IDENTIFIER = 'claude-cli/' + + const clientUA = clientHeaders?.['user-agent'] || clientHeaders?.['User-Agent'] + let cachedUA = await redis.client.get(CACHE_KEY) + + if (clientUA?.includes(CLAUDE_CODE_UA_IDENTIFIER)) { + if (!cachedUA) { + // 没有缓存,直接存储 + await redis.client.setex(CACHE_KEY, TTL, clientUA) + logger.info(`📱 Captured unified Claude Code User-Agent: ${clientUA}`) + cachedUA = clientUA + } else { + // 有缓存,比较版本号,保存更新的版本 + const shouldUpdate = this.compareClaudeCodeVersions(clientUA, cachedUA) + if (shouldUpdate) { + await redis.client.setex(CACHE_KEY, TTL, clientUA) + logger.info(`🔄 Updated to newer Claude Code User-Agent: ${clientUA} (was: ${cachedUA})`) + cachedUA = clientUA + } else { + // 当前版本不比缓存版本新,仅刷新TTL + await redis.client.expire(CACHE_KEY, TTL) + } + } + } + + return cachedUA // 没有缓存返回 null + } + + // 🔄 比较Claude Code版本号,判断是否需要更新 + // 返回 true 表示 newUA 版本更新,需要更新缓存 + compareClaudeCodeVersions(newUA, cachedUA) { + try { + // 提取版本号:claude-cli/1.0.102 (external, cli) -> 1.0.102 + const newVersionMatch = newUA.match(/claude-cli\/([0-9]+\.[0-9]+\.[0-9]+)/) + const cachedVersionMatch = cachedUA.match(/claude-cli\/([0-9]+\.[0-9]+\.[0-9]+)/) + + if (!newVersionMatch || !cachedVersionMatch) { + // 无法解析版本号,优先使用新的 + logger.warn(`⚠️ Unable to parse Claude Code versions: new=${newUA}, cached=${cachedUA}`) + return true + } + + const newVersion = newVersionMatch[1] + const cachedVersion = cachedVersionMatch[1] + + // 比较版本号 (semantic version) + const compareResult = this.compareSemanticVersions(newVersion, cachedVersion) + + logger.debug(`🔍 Version comparison: ${newVersion} vs ${cachedVersion} = ${compareResult}`) + + return compareResult > 0 // 新版本更大则返回 true + } catch (error) { + logger.warn(`⚠️ Error comparing Claude Code versions, defaulting to update: ${error.message}`) + return true // 出错时优先使用新的 + } + } + + // 🔢 比较版本号 + // 返回:1 表示 v1 > v2,-1 表示 v1 < v2,0 表示相等 + compareSemanticVersions(version1, version2) { + // 将版本号字符串按"."分割成数字数组 + const arr1 = version1.split('.') + const arr2 = version2.split('.') + + // 获取两个版本号数组中的最大长度 + const maxLength = Math.max(arr1.length, arr2.length) + + // 循环遍历,逐段比较版本号 + for (let i = 0; i < maxLength; i++) { + // 如果某个版本号的某一段不存在,则视为0 + const num1 = parseInt(arr1[i] || 0, 10) + const num2 = parseInt(arr2[i] || 0, 10) + + if (num1 > num2) { + return 1 // version1 大于 version2 + } + if (num1 < num2) { + return -1 // version1 小于 version2 + } + } + + return 0 // 两个版本号相等 + } + // 🎯 健康检查 async healthCheck() { try { diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index b7055c1e..19182c8b 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -854,6 +854,51 @@ + +
+ +
+
+ +
+ +
+