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:
alfadb
2026-09-23 13:35:44 +08:00
parent 5e244e7382
commit ca08825935
37 changed files with 1208 additions and 70 deletions
@@ -257,6 +257,8 @@ type UpdateSettingsRequest struct {
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
OpenAICodexClientVersion *string `json:"openai_codex_client_version"`
OpenAICodexVersionAutoSyncEnabled *bool `json:"openai_codex_version_auto_sync_enabled"`
ClaudeCodeClientVersion *string `json:"claude_code_client_version"`
ClaudeCodeVersionAutoSyncEnabled *bool `json:"claude_code_version_auto_sync_enabled"`
// codex_cli_only 加固(global-only)
MinCodexVersion string `json:"min_codex_version"`
@@ -1455,6 +1457,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
req.OpenAICodexClientVersion = &normalized
}
if req.ClaudeCodeClientVersion != nil {
// 该值会被拼进出站 User-Agent 与 billing attribution,必须是合法版本号;空串表示跟随自动同步。
normalized := strings.TrimSpace(*req.ClaudeCodeClientVersion)
if normalized != "" && service.NormalizeClaudeCodeClientVersion(normalized) == "" {
response.Error(c, http.StatusBadRequest, "claude_code_client_version must be empty or a valid version (e.g. 2.1.258)")
return
}
req.ClaudeCodeClientVersion = &normalized
}
// codex_cli_only 加固:最低/最高 Codex 版本(空=禁用,或合法 semver;max>=min)
if req.MinCodexVersion != "" && !semverPattern.MatchString(req.MinCodexVersion) {
@@ -1768,6 +1779,20 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.OpenAICodexVersionAutoSyncEnabled
}(),
ClaudeCodeClientVersion: func() string {
if req.ClaudeCodeClientVersion != nil {
return *req.ClaudeCodeClientVersion
}
return previousSettings.ClaudeCodeClientVersion
}(),
// 同步值由自动同步任务独占写入,面板保存时原样带回,避免被清空。
ClaudeCodeClientVersionSynced: previousSettings.ClaudeCodeClientVersionSynced,
ClaudeCodeVersionAutoSyncEnabled: func() bool {
if req.ClaudeCodeVersionAutoSyncEnabled != nil {
return *req.ClaudeCodeVersionAutoSyncEnabled
}
return previousSettings.ClaudeCodeVersionAutoSyncEnabled
}(),
MinCodexVersion: strings.TrimSpace(req.MinCodexVersion),
MaxCodexVersion: strings.TrimSpace(req.MaxCodexVersion),
CodexCLIOnlyBlacklist: strings.TrimSpace(req.CodexCLIOnlyBlacklist),
@@ -2311,6 +2336,9 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
OpenAICodexClientVersion: updatedSettings.OpenAICodexClientVersion,
OpenAICodexClientVersionSynced: updatedSettings.OpenAICodexClientVersionSynced,
OpenAICodexVersionAutoSyncEnabled: updatedSettings.OpenAICodexVersionAutoSyncEnabled,
ClaudeCodeClientVersion: updatedSettings.ClaudeCodeClientVersion,
ClaudeCodeClientVersionSynced: updatedSettings.ClaudeCodeClientVersionSynced,
ClaudeCodeVersionAutoSyncEnabled: updatedSettings.ClaudeCodeVersionAutoSyncEnabled,
MinCodexVersion: updatedSettings.MinCodexVersion,
MaxCodexVersion: updatedSettings.MaxCodexVersion,
CodexCLIOnlyBlacklist: updatedSettings.CodexCLIOnlyBlacklist,