feat: 添加统一Claude Code User-Agent支持及缓存管理功能

### **核心功能**
*   **自动更新**:自动获取并使用最新的 “Claude Code” 客户端版本号。
*   **智能缓存**:获取到的版本会缓存25小时,然后自动刷新。
*   **独立开关**:每个账户都可以单独设置是否启用此功能。

### **前端界面**
*   **新增开关**:账户设置里增加了“使用统一版本”的选项。
*   **信息显示**:能直接看到当前正在使用的版本号。
*   **手动刷新**:提供“清除缓存”按钮,可手动强制更新。

### **后端技术**
*   **核心方法**:开发了新的后台功能,用于捕获、比较和管理版本号。
*   **管理接口**:为管理员提供了新的API (`/admin/claude-code-version`),方便查询和刷新。
This commit is contained in:
sczheng189
2025-09-03 20:14:58 +08:00
parent 9c7ec8758d
commit 39c49fe2bb
4 changed files with 341 additions and 11 deletions

View File

@@ -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

View File

@@ -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 }

View File

@@ -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 < v20 表示相等
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 {