mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-05 16:47:26 +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
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
@@ -119,6 +120,19 @@ const openAICodexClientVersionDBTimeout = 5 * time.Second
|
||||
// openAICodexClientVersionSFKey singleflight 键。
|
||||
const openAICodexClientVersionSFKey = "openai_codex_client_version"
|
||||
|
||||
// cachedClaudeCodeClientVersion 缓存出站 Claude Code 客户端版本号(进程内缓存,60s TTL)
|
||||
type cachedClaudeCodeClientVersion struct {
|
||||
version string
|
||||
expiresAt int64 // unix nano
|
||||
}
|
||||
|
||||
const claudeCodeClientVersionCacheTTL = 60 * time.Second
|
||||
const claudeCodeClientVersionErrorTTL = 5 * time.Second
|
||||
const claudeCodeClientVersionDBTimeout = 5 * time.Second
|
||||
|
||||
// claudeCodeClientVersionSFKey singleflight 键。
|
||||
const claudeCodeClientVersionSFKey = "claude_code_client_version"
|
||||
|
||||
type cachedOpenAIQuotaAutoPauseSettings struct {
|
||||
settings OpsOpenAIAccountQuotaAutoPauseSettings
|
||||
expiresAt int64
|
||||
@@ -378,6 +392,101 @@ func (s *SettingService) InvalidateOpenAICodexClientVersionCache() {
|
||||
s.openAICodexVersionCache.Store((*cachedOpenAICodexClientVersion)(nil))
|
||||
}
|
||||
|
||||
// NormalizeClaudeCodeClientVersion 校验并归一化 Claude Code 客户端版本号,非法值返回空串。
|
||||
// 容忍前导 "v" 与首尾空白;合法性复用 claude.IsSupportedCLIVersion(严格三段纯数字 semver、
|
||||
// 无预发布/构建后缀、且不低于内置基线)。
|
||||
func NormalizeClaudeCodeClientVersion(version string) string {
|
||||
normalized := strings.TrimSpace(version)
|
||||
normalized = strings.TrimPrefix(normalized, "v")
|
||||
if normalized == "" {
|
||||
return ""
|
||||
}
|
||||
if !claude.IsSupportedCLIVersion(normalized) {
|
||||
return ""
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// GetClaudeCodeClientVersion 返回出站声明的 Claude Code CLI 客户端版本号。
|
||||
// 优先级:管理员在面板覆写的版本 → 自动同步到的官方最新版本 → claude.CLIVersion()
|
||||
// (环境变量 SUB2API_CLAUDE_CLI_VERSION 覆盖 + 内置基线)。
|
||||
// 版本太旧会被 Anthropic 拒绝(HTTP 400 claude_code_version_too_old),故该值需保持跟随官方发布。
|
||||
//
|
||||
// ⚠️ 一致性约束:同一次请求里,拼 User-Agent(claude-cli/<版本>)的版本号和用于
|
||||
// billing attribution 的版本号必须是同一个值,否则 Anthropic 侧对不上、判为非正版客户端。
|
||||
// 因此调用点必须在一次请求内只取一次本函数并复用;本缓存的唯一主动变更点是
|
||||
// 同步任务写入后调用 InvalidateClaudeCodeClientVersionCache,60s TTL 不会在极短时间内抖动。
|
||||
func (s *SettingService) GetClaudeCodeClientVersion(ctx context.Context) string {
|
||||
fallback := claude.CLIVersion()
|
||||
if s == nil || s.settingRepo == nil {
|
||||
return fallback
|
||||
}
|
||||
if cached, ok := s.claudeCodeVersionCache.Load().(*cachedClaudeCodeClientVersion); ok && cached != nil {
|
||||
if time.Now().UnixNano() < cached.expiresAt {
|
||||
return cached.version
|
||||
}
|
||||
}
|
||||
|
||||
result, _, _ := s.claudeCodeVersionSF.Do(claudeCodeClientVersionSFKey, func() (any, error) {
|
||||
if cached, ok := s.claudeCodeVersionCache.Load().(*cachedClaudeCodeClientVersion); ok && cached != nil {
|
||||
if time.Now().UnixNano() < cached.expiresAt {
|
||||
return cached.version, nil
|
||||
}
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), claudeCodeClientVersionDBTimeout)
|
||||
defer cancel()
|
||||
values, err := s.settingRepo.GetMultiple(dbCtx, []string{
|
||||
SettingKeyClaudeCodeClientVersion,
|
||||
SettingKeyClaudeCodeClientVersionSynced,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("failed to get claude code client version setting", "error", err)
|
||||
s.claudeCodeVersionCache.Store(&cachedClaudeCodeClientVersion{
|
||||
version: fallback,
|
||||
expiresAt: time.Now().Add(claudeCodeClientVersionErrorTTL).UnixNano(),
|
||||
})
|
||||
return fallback, nil
|
||||
}
|
||||
version := NormalizeClaudeCodeClientVersion(values[SettingKeyClaudeCodeClientVersion])
|
||||
if version == "" {
|
||||
if raw := values[SettingKeyClaudeCodeClientVersion]; strings.TrimSpace(raw) != "" {
|
||||
slog.Warn("ignoring invalid claude_code_client_version setting; falling back to the next layer",
|
||||
"value", raw)
|
||||
}
|
||||
version = NormalizeClaudeCodeClientVersion(values[SettingKeyClaudeCodeClientVersionSynced])
|
||||
if version == "" && strings.TrimSpace(values[SettingKeyClaudeCodeClientVersionSynced]) != "" {
|
||||
slog.Warn("ignoring invalid claude_code_client_version_synced setting; falling back to the built-in pin",
|
||||
"value", values[SettingKeyClaudeCodeClientVersionSynced])
|
||||
}
|
||||
}
|
||||
if version == "" {
|
||||
version = fallback
|
||||
}
|
||||
s.claudeCodeVersionCache.Store(&cachedClaudeCodeClientVersion{
|
||||
version: version,
|
||||
expiresAt: time.Now().Add(claudeCodeClientVersionCacheTTL).UnixNano(),
|
||||
})
|
||||
return version, nil
|
||||
})
|
||||
if version, ok := result.(string); ok && version != "" {
|
||||
return version
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// InvalidateClaudeCodeClientVersionCache 丢弃版本号缓存,下次读取回源。
|
||||
// 面板保存与自动同步写入后调用。
|
||||
func (s *SettingService) InvalidateClaudeCodeClientVersionCache() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.claudeCodeVersionSF.Forget(claudeCodeClientVersionSFKey)
|
||||
s.claudeCodeVersionCache.Store((*cachedClaudeCodeClientVersion)(nil))
|
||||
}
|
||||
|
||||
// GetOpenAICodexCanonicalUserAgent 返回出站规范 Codex User-Agent。
|
||||
// 未填面板 UA 时按当前生效的客户端版本号拼出标准 Codex TUI UA。
|
||||
//
|
||||
|
||||
Reference in new issue
Block a user