mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 23:53:33 +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,避免缓存翻转 瞬间头体版本不一致被判非正版客户端 - 管理后台设置页新增配置区块,含中英文案与「当前同步到」只读展示
121 lines
5.3 KiB
Go
121 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// withCLIVersionResolverForTest 注入解析器并在用例结束时还原(置 nil),避免用例间污染。
|
|
func withCLIVersionResolverForTest(t *testing.T, resolver func() string) {
|
|
t.Helper()
|
|
claude.SetCLIVersionResolver(resolver)
|
|
t.Cleanup(func() { claude.SetCLIVersionResolver(nil) })
|
|
}
|
|
|
|
// 一致性铁律(d):模拟一次 OAuth + mimic 的请求构建(messages 与 count_tokens 两条
|
|
// 出站路径),断言出站 User-Agent 头里的版本号与请求体 x-anthropic-billing-header
|
|
// 中 cc_version 的版本号相同,且都等于运行期注入的版本号。头/体不一致会被 Anthropic
|
|
// 判为非正版客户端。
|
|
func TestBuildOAuthMimicRequest_RuntimeVersionConsistentBetweenHeaderAndBilling(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
const upgraded = "9.9.9"
|
|
withCLIVersionResolverForTest(t, func() string { return upgraded })
|
|
|
|
for _, endpoint := range []string{"messages", "count_tokens"} {
|
|
t.Run(endpoint, func(t *testing.T) {
|
|
resetGatewayForwardingSettingsCacheForTest(t)
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
|
|
body := []byte(`{"model":"claude-haiku-4-5","system":[{"type":"text","text":""}],"messages":[{"role":"user","content":"hello world"}]}`)
|
|
billing, err := buildBillingAttributionText(body, "2.1.81")
|
|
require.NoError(t, err)
|
|
body, err = sjson.SetBytes(body, "system.0.text", billing)
|
|
require.NoError(t, err)
|
|
|
|
svc := &GatewayService{cfg: &config.Config{}}
|
|
account := &Account{ID: 1, Platform: PlatformAnthropic, Type: AccountTypeOAuth}
|
|
|
|
var req *http.Request
|
|
var wireBody []byte
|
|
if endpoint == "messages" {
|
|
req, wireBody, err = svc.buildUpstreamRequest(context.Background(), c, account,
|
|
body, "test-token", "oauth", "claude-haiku-4-5", false, true)
|
|
} else {
|
|
req, wireBody, err = svc.buildCountTokensRequest(context.Background(), c, account,
|
|
body, "test-token", "oauth", "claude-haiku-4-5", true)
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
wantUA := "claude-cli/" + upgraded + " (external, cli)"
|
|
outboundUA := getHeaderRaw(req.Header, "User-Agent")
|
|
require.Equal(t, wantUA, outboundUA)
|
|
|
|
billingText := gjson.GetBytes(wireBody, "system.0.text").String()
|
|
require.Contains(t, billingText, "x-anthropic-billing-header")
|
|
require.Contains(t, billingText, "cc_version="+upgraded+"."+computeClaudeCodeFingerprint(wireBody, upgraded)+";")
|
|
require.NotContains(t, billingText, "cc_version=2.1.81")
|
|
|
|
// 头/体版本号必须来自同一字符串。
|
|
headerVersion := ExtractCLIVersion(outboundUA)
|
|
require.Equal(t, upgraded, headerVersion)
|
|
require.Contains(t, billingText, "cc_version="+headerVersion+".")
|
|
})
|
|
}
|
|
}
|
|
|
|
// e. 注入更高版本后,floorClaudeCLIUserAgentVersion 把存量低版本指纹抬升到新版本;
|
|
//
|
|
// 等于或高于新版本的指纹保持不动(只升不降)。
|
|
func TestFloorClaudeCLIUserAgentVersion_UsesRuntimeVersion(t *testing.T) {
|
|
const upgraded = "9.9.9"
|
|
withCLIVersionResolverForTest(t, func() string { return upgraded })
|
|
|
|
floored, changed := floorClaudeCLIUserAgentVersion("claude-cli/2.1.100 (external, cli)")
|
|
require.True(t, changed)
|
|
require.Equal(t, "claude-cli/"+upgraded+" (external, cli)", floored)
|
|
|
|
// 等于运行期版本:不动。
|
|
same, changed := floorClaudeCLIUserAgentVersion("claude-cli/" + upgraded + " (external, cli)")
|
|
require.False(t, changed)
|
|
require.Equal(t, "claude-cli/"+upgraded+" (external, cli)", same)
|
|
|
|
// 高于运行期版本:不动。
|
|
greater, changed := floorClaudeCLIUserAgentVersion("claude-cli/99.0.0 (external, cli)")
|
|
require.False(t, changed)
|
|
require.Equal(t, "claude-cli/99.0.0 (external, cli)", greater)
|
|
|
|
// resolver 返回非法值时回退内置基线,floor 语义仍然成立。
|
|
withCLIVersionResolverForTest(t, func() string { return "abc" })
|
|
baselineFloored, changed := floorClaudeCLIUserAgentVersion("claude-cli/1.0.0 (external, cli)")
|
|
require.True(t, changed)
|
|
require.Equal(t, "claude-cli/"+claude.CLIVersion()+" (external, cli)", baselineFloored)
|
|
}
|
|
|
|
// 注入更高版本后,isAcceptableFingerprintUserAgent 以运行期版本为基准,允许客户端
|
|
// 上报的新版本。
|
|
func TestIsAcceptableFingerprintUserAgent_UsesRuntimeVersion(t *testing.T) {
|
|
const upgraded = "9.9.9"
|
|
withCLIVersionResolverForTest(t, func() string { return upgraded })
|
|
require.True(t, isAcceptableFingerprintUserAgent("claude-cli/"+upgraded+" (external, cli)"))
|
|
// 允许的最大主版本超前量是 +2(9.9.9 → 11.x)。
|
|
require.True(t, isAcceptableFingerprintUserAgent("claude-cli/11.0.0 (external, cli)"))
|
|
// 超前 +3 会被拒(与静态基线下的既有语义一致,只是基准换成运行期版本)。
|
|
require.False(t, isAcceptableFingerprintUserAgent("claude-cli/12.0.0 (external, cli)"))
|
|
}
|
|
|
|
// defaultFingerprint 现取运行期版本,不再在 init 固化。
|
|
func TestDefaultFingerprintUsesRuntimeVersion(t *testing.T) {
|
|
const upgraded = "9.9.9"
|
|
withCLIVersionResolverForTest(t, func() string { return upgraded })
|
|
require.Equal(t, "claude-cli/"+upgraded+" (external, cli)", defaultFingerprint().UserAgent)
|
|
}
|