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,避免缓存翻转 瞬间头体版本不一致被判非正版客户端 - 管理后台设置页新增配置区块,含中英文案与「当前同步到」只读展示
71 lines
2.2 KiB
Go
71 lines
2.2 KiB
Go
package claude
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// fable51MinCLIVersion 是上游对 claude-fable-5-1 的客户端版本闸门下限。
|
||
// 低于该版本时上游直接 400:
|
||
//
|
||
// claude_code_version_too_old
|
||
// "Claude Code <ver> does not support this model; version 2.1.251 or newer is required."
|
||
const fable51MinCLIVersion = "2.1.251"
|
||
|
||
// TestCLICurrentVersionMatchesDefaultUserAgent 锁定"常量与 UA 同源"这条约束。
|
||
// 两者不一致会被 Anthropic 判为第三方客户端,且只改一处是历史上的常见疏漏,
|
||
// 所以这里从 UA 里反解版本号来比对,而不是并列两个字面量。
|
||
func TestCLICurrentVersionMatchesDefaultUserAgent(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
ua := DefaultHeaders()["User-Agent"]
|
||
const prefix = "claude-cli/"
|
||
require.True(t, strings.HasPrefix(ua, prefix), "unexpected User-Agent: %q", ua)
|
||
|
||
rest := strings.TrimPrefix(ua, prefix)
|
||
version := rest
|
||
if idx := strings.IndexByte(rest, ' '); idx >= 0 {
|
||
version = rest[:idx]
|
||
}
|
||
require.Equal(t, CLICurrentVersion, version,
|
||
"DefaultHeaders User-Agent version must match CLICurrentVersion")
|
||
}
|
||
|
||
// TestCLICurrentVersionSatisfiesFable51Gate 锁定伪装版本不低于 claude-fable-5-1
|
||
// 的闸门下限。DefaultModels 里登记了该模型却发着更旧的版本号,等于模型清单可见
|
||
// 但一调就 400。
|
||
func TestCLICurrentVersionSatisfiesFable51Gate(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
require.GreaterOrEqual(t, compareSemver(t, CLICurrentVersion, fable51MinCLIVersion), 0,
|
||
"CLICurrentVersion %s is below the claude-fable-5-1 gate %s",
|
||
CLICurrentVersion, fable51MinCLIVersion)
|
||
}
|
||
|
||
// compareSemver 比较两个三段 semver,返回 -1 / 0 / 1。
|
||
func compareSemver(t *testing.T, a, b string) int {
|
||
t.Helper()
|
||
|
||
as, bs := strings.Split(a, "."), strings.Split(b, ".")
|
||
require.Len(t, as, 3, "not a three-part semver: %q", a)
|
||
require.Len(t, bs, 3, "not a three-part semver: %q", b)
|
||
|
||
for i := range as {
|
||
ai, err := strconv.Atoi(as[i])
|
||
require.NoError(t, err, "non-numeric segment in %q", a)
|
||
bi, err := strconv.Atoi(bs[i])
|
||
require.NoError(t, err, "non-numeric segment in %q", b)
|
||
|
||
switch {
|
||
case ai > bi:
|
||
return 1
|
||
case ai < bi:
|
||
return -1
|
||
}
|
||
}
|
||
return 0
|
||
}
|