mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
出站伪装用的 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,避免缓存翻转 瞬间头体版本不一致被判非正版客户端 - 管理后台设置页新增配置区块,含中英文案与「当前同步到」只读展示
74 lines
2.7 KiB
Go
74 lines
2.7 KiB
Go
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())
|
||
}
|
||
}
|