Files
sub2api/backend/internal/pkg/claude/cli_version_runtime_test.go
T
alfadb ca08825935 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,避免缓存翻转
  瞬间头体版本不一致被判非正版客户端
- 管理后台设置页新增配置区块,含中英文案与「当前同步到」只读展示
2026-09-23 13:35:44 +08:00

74 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package claude
import (
"strings"
"testing"
)
// withCLIVersionResolver 注入测试解析器并在用例结束时还原,避免用例间污染。
func withCLIVersionResolver(t *testing.T, resolver func() string) {
t.Helper()
SetCLIVersionResolver(resolver)
t.Cleanup(func() { SetCLIVersionResolver(nil) })
}
// a. resolver 未注入时,EffectiveCLIVersion() 必须等于 CLIVersion(),
// 且 DefaultHeaders()["User-Agent"] 与改造前的 init 固化值完全一致。
func TestEffectiveCLIVersionDefaultsToCLIVersion(t *testing.T) {
SetCLIVersionResolver(nil)
if got := EffectiveCLIVersion(); got != CLIVersion() {
t.Fatalf("EffectiveCLIVersion() = %q, want CLIVersion() = %q", got, CLIVersion())
}
wantUA := "claude-cli/" + CLIVersion() + " (external, cli)"
if got := DefaultHeaders()["User-Agent"]; got != wantUA {
t.Fatalf("DefaultHeaders()[User-Agent] = %q, want %q", got, wantUA)
}
}
// b. 注入返回更高版本的 resolver 后,DefaultHeaders()["User-Agent"] 跟随变化。
func TestEffectiveCLIVersionFollowsResolver(t *testing.T) {
const upgraded = "9.9.9"
withCLIVersionResolver(t, func() string { return upgraded })
if got := EffectiveCLIVersion(); got != upgraded {
t.Fatalf("EffectiveCLIVersion() = %q, want %q", got, upgraded)
}
wantUA := "claude-cli/" + upgraded + " (external, cli)"
if got := DefaultHeaders()["User-Agent"]; got != wantUA {
t.Fatalf("DefaultHeaders()[User-Agent] = %q, want %q", got, wantUA)
}
if got := DefaultHeaders()["X-App"]; got != "cli" {
t.Fatalf("DefaultHeaders()[X-App] = %q, want cli", got)
}
}
// c. resolver 返回非法值(空串、非 semver、低于内置基线)时回退 CLIVersion()。
func TestEffectiveCLIVersionFallsBackOnInvalidResolverValues(t *testing.T) {
for name, invalid := range map[string]string{
"empty": "",
"not semver": "abc",
"below baseline": "1.0.0",
"whitespace": " ",
} {
t.Run(name, func(t *testing.T) {
withCLIVersionResolver(t, func() string { return invalid })
if got := EffectiveCLIVersion(); got != CLIVersion() {
t.Fatalf("resolver %q: EffectiveCLIVersion() = %q, want fallback %q", invalid, got, CLIVersion())
}
ua := DefaultHeaders()["User-Agent"]
if !strings.HasPrefix(ua, "claude-cli/"+CLIVersion()+" ") {
t.Fatalf("UA %q does not carry fallback version %q", ua, CLIVersion())
}
})
}
}
// 撤销注入(传 nil)后回到 CLIVersion()。
func TestSetCLIVersionResolverNilRestoresFallback(t *testing.T) {
withCLIVersionResolver(t, func() string { return "9.9.9" })
SetCLIVersionResolver(nil)
if got := EffectiveCLIVersion(); got != CLIVersion() {
t.Fatalf("after nil resolver: EffectiveCLIVersion() = %q, want %q", got, CLIVersion())
}
}