mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-14 06:47:19 +00:00
feat(claude): 自动同步 Claude Code 客户端版本号
出站伪装用的 Claude Code 版本号此前硬编码在 claude.CLICurrentVersion, 只能靠环境变量 SUB2API_CLAUDE_CLI_VERSION 覆盖且需重启;Anthropic 抬高 版本下限时(如 Opus 5.5 要求 >= 2.1.280)必须改代码发版才能跟上。 改为每小时从官方 GitHub Releases 同步最新稳定版,无需重启也无需发版。 - 新增 ClaudeCodeVersionSyncService:1 小时间隔,数据源 anthropics/claude-code 的 releases/latest,失败回退最近 30 条列表;过滤 draft/prerelease; 只向前推进,拉取失败保留上次值不降级;面板可关闭 - 新增三个设置项:claude_code_client_version(管理员手动固定)、 claude_code_client_version_synced(同步服务独占写)、 claude_code_version_auto_sync_enabled(开关,默认开) - 取值优先级:面板手动值 -> 同步值 -> SUB2API_CLAUDE_CLI_VERSION -> 内置基线, 每层经 IsSupportedCLIVersion 校验,含 60s 缓存与 singleflight - 版本号运行期化:claude.DefaultHeaders 由包级 var 改为函数,新增 EffectiveCLIVersion/DefaultUserAgent 与 SetCLIVersionResolver(未注入时 与 CLIVersion() 等价);identity 的 defaultFingerprint 不再在包 init 固化, 指纹下限与主版本超前校验改用运行期值 - 守住一致性铁律:buildUpstreamRequest 与 count_tokens 各取一次 UA 字符串, 同时喂给出站 User-Agent 头与请求体 billing 的 cc_version,避免缓存翻转 瞬间头体版本不一致被判非正版客户端 - 管理后台设置页新增配置区块,含中英文案与「当前同步到」只读展示
This commit is contained in:
1 parent
5e244e7382
commit
ca08825935
37 files changed
+1208
-70
No files matched your search
@@ -72,15 +72,17 @@ func isAcceptableFingerprintUserAgent(ua string) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
currentMajor, _, _, currentOK := parseUserAgentVersion(claudeCLIUserAgentProduct + "/" + claude.CLIVersion())
|
||||
// 超前检查以运行期生效版本为基准(EffectiveCLIVersion):同步跨大版本后,
|
||||
// 若仍与静态基线比较会误拒客户端上报的新版本。
|
||||
currentMajor, _, _, currentOK := parseUserAgentVersion(claudeCLIUserAgentProduct + "/" + claude.EffectiveCLIVersion())
|
||||
if !currentOK {
|
||||
return true
|
||||
}
|
||||
return major <= currentMajor+maxClaudeCLIMajorVersionSkew
|
||||
}
|
||||
|
||||
// floorClaudeCLIUserAgentVersion 把 claude-cli UA 中的版本号抬升到 claude.CLICurrentVersion
|
||||
// 下限:低于下限时就地升到下限并返回 changed=true,否则原样返回。
|
||||
// floorClaudeCLIUserAgentVersion 把 claude-cli UA 中的版本号抬升到运行期生效版本
|
||||
// (claude.EffectiveCLIVersion())下限:低于下限时就地升到下限并返回 changed=true,否则原样返回。
|
||||
//
|
||||
// 为什么需要这条:账号级指纹"只升不降"、活跃账号懒续期后近乎永不过期,且系统内没有重置
|
||||
// 入口,defaultFingerprint 只在首次创建指纹时使用。存量账号缓存里的 claude-cli 版本停留在
|
||||
@@ -98,28 +100,36 @@ func floorClaudeCLIUserAgentVersion(ua string) (string, bool) {
|
||||
if extractProduct(ua) != claudeCLIUserAgentProduct {
|
||||
return ua, false
|
||||
}
|
||||
floorUA := claudeCLIUserAgentProduct + "/" + claude.CLICurrentVersion
|
||||
// 用 EffectiveCLIVersion()(面板/同步的运行期值,经 IsSupportedCLIVersion
|
||||
// 校验、恒 >= 内置基线)而非静态基线 CLICurrentVersion:
|
||||
// 运行期同步到更高版本后,存量低版本指纹才能被抬到新版本;"只升不降"语义不变。
|
||||
floor := claude.EffectiveCLIVersion()
|
||||
floorUA := claudeCLIUserAgentProduct + "/" + floor
|
||||
// isNewerVersion(floor, ua) 为 true 当且仅当下限版本严格高于 ua:
|
||||
// ua 等于或高于下限、产品名不一致、或版本无法解析时都不做改动。
|
||||
if !isNewerVersion(floorUA, ua) {
|
||||
return ua, false
|
||||
}
|
||||
floored := claudeCLIUAVersionPrefixRegex.ReplaceAllString(ua, "${1}/"+claude.CLICurrentVersion)
|
||||
floored := claudeCLIUAVersionPrefixRegex.ReplaceAllString(ua, "${1}/"+floor)
|
||||
if floored == ua {
|
||||
return ua, false
|
||||
}
|
||||
return floored, true
|
||||
}
|
||||
|
||||
// 默认指纹值(当客户端未提供时使用)
|
||||
var defaultFingerprint = Fingerprint{
|
||||
UserAgent: "claude-cli/" + claude.CLIVersion() + " (external, cli)",
|
||||
StainlessLang: "js",
|
||||
StainlessPackageVersion: "0.94.0",
|
||||
StainlessOS: "Linux",
|
||||
StainlessArch: "arm64",
|
||||
StainlessRuntime: "node",
|
||||
StainlessRuntimeVersion: "v24.3.0",
|
||||
// defaultFingerprint 返回默认指纹值(当客户端未提供时使用)。
|
||||
// UserAgent 不能在包 init 时固化:必须在每次使用时经 EffectiveCLIVersion 现取,
|
||||
// 否则运行期同步到新版本后,新建账号会写入过期版本作为持久身份。
|
||||
func defaultFingerprint() Fingerprint {
|
||||
return Fingerprint{
|
||||
UserAgent: claude.DefaultUserAgent(),
|
||||
StainlessLang: "js",
|
||||
StainlessPackageVersion: "0.94.0",
|
||||
StainlessOS: "Linux",
|
||||
StainlessArch: "arm64",
|
||||
StainlessRuntime: "node",
|
||||
StainlessRuntimeVersion: "v24.3.0",
|
||||
}
|
||||
}
|
||||
|
||||
// Fingerprint represents account fingerprint data
|
||||
@@ -187,7 +197,7 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
|
||||
if uaAcceptable {
|
||||
mergeHeadersIntoFingerprint(cached, headers)
|
||||
} else {
|
||||
cached.UserAgent = defaultFingerprint.UserAgent
|
||||
cached.UserAgent = defaultFingerprint().UserAgent
|
||||
}
|
||||
needWrite = true
|
||||
logger.LegacyPrintf("service.identity",
|
||||
@@ -262,19 +272,20 @@ func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fin
|
||||
// 首次创建同样是持久化写入,必须与升级路径共用同一套校验。
|
||||
if ua := strings.TrimSpace(headers.Get("User-Agent")); isAcceptableFingerprintUserAgent(ua) {
|
||||
// 首次创建与缓存命中路径共用同一个版本下限:合法但过旧的 claude-cli UA
|
||||
// 落库时同样不能低于 CLICurrentVersion,否则新账号一开始就带着过旧的持久身份。
|
||||
// 落库时同样不能低于运行期生效版本(EffectiveCLIVersion),否则新账号一开始就带着过旧的持久身份。
|
||||
fp.UserAgent, _ = floorClaudeCLIUserAgentVersion(ua)
|
||||
} else {
|
||||
fp.UserAgent = defaultFingerprint.UserAgent
|
||||
fp.UserAgent = defaultFingerprint().UserAgent
|
||||
}
|
||||
|
||||
// 获取x-stainless-*头,如果没有则使用默认值
|
||||
fp.StainlessLang = getHeaderOrDefault(headers, "X-Stainless-Lang", defaultFingerprint.StainlessLang)
|
||||
fp.StainlessPackageVersion = getHeaderOrDefault(headers, "X-Stainless-Package-Version", defaultFingerprint.StainlessPackageVersion)
|
||||
fp.StainlessOS = getHeaderOrDefault(headers, "X-Stainless-OS", defaultFingerprint.StainlessOS)
|
||||
fp.StainlessArch = getHeaderOrDefault(headers, "X-Stainless-Arch", defaultFingerprint.StainlessArch)
|
||||
fp.StainlessRuntime = getHeaderOrDefault(headers, "X-Stainless-Runtime", defaultFingerprint.StainlessRuntime)
|
||||
fp.StainlessRuntimeVersion = getHeaderOrDefault(headers, "X-Stainless-Runtime-Version", defaultFingerprint.StainlessRuntimeVersion)
|
||||
df := defaultFingerprint()
|
||||
fp.StainlessLang = getHeaderOrDefault(headers, "X-Stainless-Lang", df.StainlessLang)
|
||||
fp.StainlessPackageVersion = getHeaderOrDefault(headers, "X-Stainless-Package-Version", df.StainlessPackageVersion)
|
||||
fp.StainlessOS = getHeaderOrDefault(headers, "X-Stainless-OS", df.StainlessOS)
|
||||
fp.StainlessArch = getHeaderOrDefault(headers, "X-Stainless-Arch", df.StainlessArch)
|
||||
fp.StainlessRuntime = getHeaderOrDefault(headers, "X-Stainless-Runtime", df.StainlessRuntime)
|
||||
fp.StainlessRuntimeVersion = getHeaderOrDefault(headers, "X-Stainless-Runtime-Version", df.StainlessRuntimeVersion)
|
||||
|
||||
return fp
|
||||
}
|
||||
@@ -324,7 +335,7 @@ func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) {
|
||||
setHeaderRaw(req.Header, "User-Agent", fp.UserAgent)
|
||||
}
|
||||
|
||||
// 设置x-stainless-*头(保持与 claude.DefaultHeaders 一致的大小写)
|
||||
// 设置x-stainless-*头(保持与 claude.DefaultHeaders() 一致的大小写)
|
||||
if fp.StainlessLang != "" {
|
||||
setHeaderRaw(req.Header, "X-Stainless-Lang", fp.StainlessLang)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user