Skip to content

Commit 1ccd617

Browse files
xp0228EchoPing07
andauthored
fix: 签到在无原生桌面指纹时不再发送派生机器头(修复 Linux/Docker 恒为 skipped) (#17)
* fix: 签到在 Linux/无桌面指纹时不再发送派生机器头 无原生 runtime-info 指纹时,服务端把全套派生 cosy-machine* 六头判定为 非官方客户端,把 CLAIMABLE 的「每日领 100 Credits」整条过滤掉,导致签到 恒为 skipped。改为仅有真实桌面身份时发送六头,否则一个都不发。 同时把 CN 区域的 OpenAPI bearer 统一走 jobToken/exchange:COSY securityOauthToken 在 campaigns 端点返回 401 TOKEN_INVALID。 * fix: CN 额度恢复使用 COSY 令牌,仅签到 campaigns 端点统一走 jt- 交换 PR #17 把 quotaBearer 的 CN 分支一并删掉,使 CN 额度刷新新增了对 jobToken/exchange 的硬依赖:exchange 一旦失败,quota 拉取被整体跳过, 快照归零(#6 同类问题)。PR 的论据只需要 campaigns 端点拒绝 COSY token,quota/usage 在 CN 用 COSY token 一直是可用的。 拆分为两个 bearer: - quotaBearer:CN 直接返回 securityOauthToken(旧行为),global 走交换 - openAPIBearer:campaigns 专用,两区域统一 jt- 交换 修复 3 个被破坏的 bridge 测试,新增 TestCNBearerSplit 锁定两个 bearer 的分工。 * refactor: 签到移除派生机器头死代码,测试断言新头部行为 无指纹时不再发送任何机器头后,deriveMachineCode 及 sess 的 MachineToken/Type 派生值成为死代码(发送时全部被 risk 值覆盖), 连同 Checkin/CheckinWithRisk/CheckinWithBearer 的 pat 参数一并移除。 同步修正与实现相反的注释(文件头、三个入口的 doc),并把两个 断言旧行为的测试改为断言新行为:nil risk 时六个机器头一个不发; risk 有效时发送 risk 三值 + session MachineId。 * docs: 更新 README 签到说明(无指纹也可领取,两区域统一 jt- 令牌) --------- Co-authored-by: xp0228 <xp0228@users.noreply.github.com> Co-authored-by: EchoPing <EchoPing007@gmail.com>
1 parent 65a5fff commit 1ccd617

8 files changed

Lines changed: 152 additions & 103 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,12 @@ curl http://localhost:10081/v1/chat/completions \
237237

238238
- **结果三态**:`success`(本次领到积分)/ `already`(今日已签)/ `skipped`(今天没有可签到活动)。真正的失败单独记为 `error`,并把原因写入 `last_checkin_msg`(悬停在「最近结果」上可见)。
239239
- **调度**:后台每分钟检查一次,到点后按账号 ID 抖动几秒,避免整池同时打接口。同一天已成功 / 已签 / 无活动的不再重试(`skipped` 属于有意不重试:活动未开放时反复请求没有意义);失败的在 30 分钟内不重试,之后每轮检查都会再试一次(仍在当天内)。
240-
- **区域**:国内版用 COSY 会话令牌作 Bearer,国际版用 OpenAPI 令牌(自动换取并刷新);遇到 401 会强制刷新后重试一次。
241-
- **设备指纹**:签到接口只对携带**真实桌面客户端指纹**的请求返回「每天领 100 Credits」活动,程序因此会从已安装的 Qoder 桌面 App 里读取 `umid/runtime-info` 生成的指纹(Windows:`%LOCALAPPDATA%\Programs\Qoder[ CN]\resources\umid\runtime-info.exe`;macOS:`/Applications/Qoder[ CN].app/Contents/Resources/umid/runtime-info`)。
240+
- **区域**:两区域的签到端点都使用 OpenAPI 令牌(`jt-`,自动换取并刷新);遇到 401 会强制刷新后重试一次。
241+
- **设备指纹**:上游对机器头很敏感——携带一整套**派生**(非真实)机器头会被当作非官方客户端,「每天领 100 Credits」活动被静默过滤。因此程序只在能读到已安装 Qoder 桌面 App 的真实指纹时才发送六个机器头(Windows:`%LOCALAPPDATA%\Programs\Qoder[ CN]\resources\umid\runtime-info.exe`;macOS:`/Applications/Qoder[ CN].app/Contents/Resources/umid/runtime-info`);读不到时一个机器头都不发,同样能领到每日积分。
242242

243243
> 🕒 **时区**:签到时间按**服务器本地时间**解释,Docker 镜像默认 `TZ=Asia/Shanghai`。国际版账号若希望按本地「新的一天」签到,请把容器的 `TZ` 改成自己的时区。
244244
245-
> ⚠️ **已知限制**:未安装 Qoder 桌面 App 的机器(典型是 Docker / Linux 服务器,目前没有 Linux 候选路径)拿不到真实指纹,退化为 PAT 派生指纹后,上游会把签到请求当作非官方客户端,活动被静默丢弃,结果通常恒为 `already` / `skipped`。此时可用 `QODER_RUNTIME_INFO` 指向一个可执行 helper,其标准输出为 `{"machineToken":"…","machineType":"…","machineCode":"…"}`。
245+
> ⚠️ **已知限制**:未安装 Qoder 桌面 App 的机器(典型是 Docker / Linux 服务器,目前没有 Linux 候选路径)拿不到真实指纹,此时签到不发送任何机器头,通用「每日领 100 Credits」活动可正常领取;仅设备定向活动是否受影响未验证。如需真实指纹,可用 `QODER_RUNTIME_INFO` 指向一个可执行 helper,其标准输出为 `{"machineToken":"…","machineType":"…","machineCode":"…"}`。
246246
247247
### 5. 思考强度与参数透传(verbatim 原则)
248248

‎auth/checkin.go‎

Lines changed: 43 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ package auth
99
//
1010
// A campaign is claimable when actionType == "CLAIM_BENEFIT" and
1111
// claimStatus == "CLAIMABLE"; an already-CLAIMED campaign means "今日已签到".
12-
// The request authenticates with the session's `securityOauthToken` as a Bearer
13-
// token (not the COSY-signed bearer used by the chat path) plus the machine
14-
// identity headers. Cosy-MachineToken/Type/Code come from the genuine desktop
15-
// fingerprint when available; Cosy-MachineId stays the session's own random
16-
// uuid, so the header set is NOT byte-identical to the desktop client's.
12+
// The request authenticates with a "jt-" OpenAPI bearer (minted via
13+
// /api/v1/jobToken/exchange in both regions) — the COSY securityOauthToken is
14+
// rejected on this endpoint. The six Cosy-Machine* headers are sent only when
15+
// the genuine desktop fingerprint is available: a full set of derived
16+
// (non-native) values marks the caller as an unofficial client and the
17+
// claimable activity is filtered out, so without a fingerprint none of them
18+
// are sent.
1719

1820
import (
1921
"context"
20-
"crypto/md5"
2122
"crypto/sha256"
2223
"encoding/hex"
2324
"encoding/json"
@@ -104,34 +105,33 @@ func checkinEndpoint(region *RegionConfig) (base, origin string) {
104105
// caller is responsible for ensuring the session is fresh; a 401 returned here
105106
// is surfaced as *CheckinHTTPError so the bridge can force-refresh and retry.
106107
//
107-
// This variant sends the PAT-derived device fingerprint. The bridge uses
108-
// CheckinWithBearer to send the genuine desktop fingerprint, which the campaign
109-
// service requires to expose the daily credit activity.
110-
func Checkin(ctx context.Context, sess *SessionContext, pat string, region *RegionConfig) (CheckinResult, error) {
111-
return CheckinWithRisk(ctx, sess, pat, region, nil)
108+
// This variant sends no machine headers (no genuine desktop fingerprint is
109+
// available). The bridge uses CheckinWithBearer with the desktop fingerprint
110+
// and the jt- OpenAPI bearer.
111+
func Checkin(ctx context.Context, sess *SessionContext, region *RegionConfig) (CheckinResult, error) {
112+
return CheckinWithRisk(ctx, sess, region, nil)
112113
}
113114

114115
// CheckinWithRisk is Checkin with an optional genuine device fingerprint. When
115-
// risk is valid, its machineToken/Type/Code replace the PAT-derived values, so
116-
// the campaign service treats the request as the official desktop client and
117-
// includes the claimable daily credit activity. When risk is nil or partial,
118-
// the derived identity is used instead.
116+
// risk is valid, the six machine headers carry its machineToken/Type/Code plus
117+
// the session's MachineId; when risk is nil or partial, no machine headers are
118+
// sent at all — a full set of derived values would be filtered as an
119+
// unofficial client.
119120
//
120-
// The Bearer token defaults to the session's securityOauthToken. Global
121-
// (qoder.sh) rejects that token on the campaign endpoint (401 TOKEN_EXPIRE) and
122-
// requires the OpenAPI bearer instead; callers there should use
123-
// CheckinWithBearer with the OpenAPI token.
124-
func CheckinWithRisk(ctx context.Context, sess *SessionContext, pat string, region *RegionConfig, risk *RiskIdentity) (CheckinResult, error) {
121+
// The Bearer token defaults to the session's securityOauthToken; the
122+
// campaigns endpoint rejects that in both regions, so production callers use
123+
// CheckinWithBearer with the jt- OpenAPI token.
124+
func CheckinWithRisk(ctx context.Context, sess *SessionContext, region *RegionConfig, risk *RiskIdentity) (CheckinResult, error) {
125125
if sess == nil {
126126
return CheckinResult{}, errors.New("qoder check-in: no session")
127127
}
128-
return CheckinWithBearer(ctx, sess, pat, region, sess.Identity.SecurityOauthToken, risk)
128+
return CheckinWithBearer(ctx, sess, region, sess.Identity.SecurityOauthToken, risk)
129129
}
130130

131131
// CheckinWithBearer is the low-level entry: it uses the supplied bearer token
132-
// (CN: securityOauthToken; global: OpenAPI token) plus the optional genuine
133-
// device fingerprint.
134-
func CheckinWithBearer(ctx context.Context, sess *SessionContext, pat string, region *RegionConfig, bearer string, risk *RiskIdentity) (CheckinResult, error) {
132+
// (both regions: the jt- OpenAPI token) plus the optional genuine device
133+
// fingerprint.
134+
func CheckinWithBearer(ctx context.Context, sess *SessionContext, region *RegionConfig, bearer string, risk *RiskIdentity) (CheckinResult, error) {
135135
if sess == nil {
136136
return CheckinResult{}, errors.New("qoder check-in: no session")
137137
}
@@ -142,20 +142,14 @@ func CheckinWithBearer(ctx context.Context, sess *SessionContext, pat string, re
142142

143143
base, origin := checkinEndpoint(region)
144144

145-
// Prefer the genuine desktop fingerprint when available: the campaign
146-
// service only exposes the claimable daily-credit activity to the official
147-
// client's machine identity. Fall back to the PAT-derived identity otherwise.
148-
machineID := sess.MachineID
149-
machineToken := sess.MachineToken
150-
machineType := sess.MachineType
151-
machineCode := deriveMachineCode(pat)
152-
if risk.Valid() {
153-
machineToken = risk.MachineToken
154-
machineType = risk.MachineType
155-
machineCode = risk.MachineCode
156-
}
157-
machineOS := machineOSString()
158-
machineHostname := machineHostnameString()
145+
// Machine headers: real desktop identity => send all six; no native
146+
// identity (typical on Linux/Docker, where runtime-info is unavailable)
147+
// => send none. A full set of derived (non-native) values marks the caller
148+
// as an unofficial client and the claimable daily-credit activity is
149+
// filtered out of the list. UA / Cosy-ClientType / Cosy-Version must still
150+
// be present or the endpoint returns an empty campaign list without any
151+
// error.
152+
sendMachineHeaders := risk.Valid()
159153

160154
doRequest := func(path, method string) (map[string]any, int, error) {
161155
reqCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
@@ -170,15 +164,17 @@ func CheckinWithBearer(ctx context.Context, sess *SessionContext, pat string, re
170164
req.Header.Set("User-Agent", "Qoder")
171165
req.Header.Set("Cosy-ClientType", "10")
172166
req.Header.Set("Cosy-Version", "0.3.4")
173-
req.Header.Set("Cosy-MachineId", machineID)
174-
req.Header.Set("Cosy-MachineToken", machineToken)
175-
req.Header.Set("Cosy-MachineType", machineType)
176-
req.Header.Set("Cosy-MachineCode", machineCode)
177-
if machineOS != "" {
178-
req.Header.Set("Cosy-MachineOS", machineOS)
179-
}
180-
if machineHostname != "" {
181-
req.Header.Set("Cosy-MachineHostname", machineHostname)
167+
if sendMachineHeaders {
168+
req.Header.Set("Cosy-MachineId", sess.MachineID)
169+
req.Header.Set("Cosy-MachineToken", risk.MachineToken)
170+
req.Header.Set("Cosy-MachineType", risk.MachineType)
171+
req.Header.Set("Cosy-MachineCode", risk.MachineCode)
172+
if machineOS := machineOSString(); machineOS != "" {
173+
req.Header.Set("Cosy-MachineOS", machineOS)
174+
}
175+
if host := machineHostnameString(); host != "" {
176+
req.Header.Set("Cosy-MachineHostname", host)
177+
}
182178
}
183179
req.Header.Set("Origin", origin)
184180
req.Header.Set("Referer", base+"/growth-page/activity-iframe")
@@ -397,17 +393,6 @@ func truncateOneLine(s string) string {
397393
return s
398394
}
399395

400-
// deriveMachineCode is the fourth machine-identity header the OpenAPI campaigns
401-
// endpoint expects (Cosy-MachineCode), alongside Id/Token/Type. The CLI computes
402-
// it from real hardware; we derive a stable value from the PAT instead, mirroring
403-
// the other derived identities. A partial identity (missing any of the four)
404-
// causes the campaigns endpoint to silently drop the claimable credit campaign,
405-
// so this MUST be non-empty.
406-
func deriveMachineCode(pat string) string {
407-
sum := md5.Sum([]byte("machinecode:" + pat))
408-
return hex.EncodeToString(sum[:])
409-
}
410-
411396
// machineOSString mirrors the desktop client's machineOS(): "<arch>_<platform>"
412397
// (e.g. "aarch64_darwin").
413398
func machineOSString() string {

‎auth/checkin_test.go‎

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ func newCheckinTestSession() *SessionContext {
2020
}
2121
}
2222

23+
// machineHeaderNames is the full six-header set; without a genuine desktop
24+
// fingerprint none of them may be sent.
25+
var machineHeaderNames = []string{
26+
"Cosy-MachineId", "Cosy-MachineToken", "Cosy-MachineType",
27+
"Cosy-MachineCode", "Cosy-MachineOS", "Cosy-MachineHostname",
28+
}
29+
2330
// withCheckinServer points the endpoint hook at srv for the test duration.
2431
func withCheckinServer(t *testing.T, srv *httptest.Server) {
2532
t.Helper()
@@ -39,8 +46,12 @@ func TestCheckinSuccess(t *testing.T) {
3946
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
4047
t.Errorf("missing bearer: %q", r.Header.Get("Authorization"))
4148
}
42-
if r.Header.Get("Cosy-MachineCode") == "" {
43-
t.Error("missing Cosy-MachineCode")
49+
// No genuine fingerprint (nil risk) => no machine headers at all: a
50+
// full set of derived ones would filter the claimable activity.
51+
for _, h := range machineHeaderNames {
52+
if got := r.Header.Get(h); got != "" {
53+
t.Errorf("%s = %q, want no machine headers without a desktop fingerprint", h, got)
54+
}
4455
}
4556
switch {
4657
case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/campaigns"):
@@ -63,7 +74,7 @@ func TestCheckinSuccess(t *testing.T) {
6374
defer srv.Close()
6475
withCheckinServer(t, srv)
6576

66-
res, err := Checkin(context.Background(), newCheckinTestSession(), "pat-1", CN)
77+
res, err := Checkin(context.Background(), newCheckinTestSession(), CN)
6778
if err != nil {
6879
t.Fatalf("Checkin: %v", err)
6980
}
@@ -85,7 +96,7 @@ func TestCheckinAlready(t *testing.T) {
8596
defer srv.Close()
8697
withCheckinServer(t, srv)
8798

88-
res, err := Checkin(context.Background(), newCheckinTestSession(), "pat-1", CN)
99+
res, err := Checkin(context.Background(), newCheckinTestSession(), CN)
89100
if err != nil {
90101
t.Fatalf("Checkin: %v", err)
91102
}
@@ -101,7 +112,7 @@ func TestCheckinSkippedOn404(t *testing.T) {
101112
defer srv.Close()
102113
withCheckinServer(t, srv)
103114

104-
res, err := Checkin(context.Background(), newCheckinTestSession(), "pat-1", CN)
115+
res, err := Checkin(context.Background(), newCheckinTestSession(), CN)
105116
if err != nil {
106117
t.Fatalf("Checkin: %v", err)
107118
}
@@ -123,7 +134,7 @@ func TestCheckinNonCreditClaimIsSkipped(t *testing.T) {
123134
defer srv.Close()
124135
withCheckinServer(t, srv)
125136

126-
res, err := Checkin(context.Background(), newCheckinTestSession(), "pat-1", CN)
137+
res, err := Checkin(context.Background(), newCheckinTestSession(), CN)
127138
if err != nil {
128139
t.Fatalf("Checkin: %v", err)
129140
}
@@ -156,7 +167,7 @@ func TestCheckinClaimRecovered(t *testing.T) {
156167
defer srv.Close()
157168
withCheckinServer(t, srv)
158169

159-
res, err := Checkin(context.Background(), newCheckinTestSession(), "pat-1", CN)
170+
res, err := Checkin(context.Background(), newCheckinTestSession(), CN)
160171
if err != nil {
161172
t.Fatalf("Checkin: %v", err)
162173
}
@@ -166,10 +177,11 @@ func TestCheckinClaimRecovered(t *testing.T) {
166177
}
167178

168179
// CheckinWithRisk must send the supplied genuine device fingerprint headers
169-
// instead of the PAT-derived ones.
180+
// (plus the session's MachineId) instead of any derived ones.
170181
func TestCheckinWithRiskSendsRiskHeaders(t *testing.T) {
171-
var gotToken, gotType, gotCode string
182+
var gotID, gotToken, gotType, gotCode string
172183
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
184+
gotID = r.Header.Get("Cosy-MachineId")
173185
gotToken = r.Header.Get("Cosy-MachineToken")
174186
gotType = r.Header.Get("Cosy-MachineType")
175187
gotCode = r.Header.Get("Cosy-MachineCode")
@@ -182,16 +194,18 @@ func TestCheckinWithRiskSendsRiskHeaders(t *testing.T) {
182194
withCheckinServer(t, srv)
183195

184196
risk := &RiskIdentity{MachineToken: "real-token-88", MachineType: "real-type", MachineCode: "real-code"}
185-
if _, err := CheckinWithRisk(context.Background(), newCheckinTestSession(), "pat-1", CN, risk); err != nil {
197+
if _, err := CheckinWithRisk(context.Background(), newCheckinTestSession(), CN, risk); err != nil {
186198
t.Fatalf("CheckinWithRisk: %v", err)
187199
}
188-
if gotToken != "real-token-88" || gotType != "real-type" || gotCode != "real-code" {
189-
t.Errorf("risk headers not sent: token=%q type=%q code=%q", gotToken, gotType, gotCode)
200+
if gotID != "mid" || gotToken != "real-token-88" || gotType != "real-type" || gotCode != "real-code" {
201+
t.Errorf("risk headers not sent: id=%q token=%q type=%q code=%q", gotID, gotToken, gotType, gotCode)
190202
}
191203
}
192204

193-
// A nil risk identity falls back to the derived headers (non-empty token).
194-
func TestCheckinWithRiskNilFallsBack(t *testing.T) {
205+
// A nil risk identity sends no machine headers: the derived fallback was
206+
// removed because a full set of derived headers marks the caller as an
207+
// unofficial client and the claimable activity is filtered out.
208+
func TestCheckinWithRiskNilSendsNoMachineHeaders(t *testing.T) {
195209
var gotToken string
196210
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
197211
gotToken = r.Header.Get("Cosy-MachineToken")
@@ -203,11 +217,11 @@ func TestCheckinWithRiskNilFallsBack(t *testing.T) {
203217
defer srv.Close()
204218
withCheckinServer(t, srv)
205219

206-
if _, err := CheckinWithRisk(context.Background(), newCheckinTestSession(), "pat-1", CN, nil); err != nil {
220+
if _, err := CheckinWithRisk(context.Background(), newCheckinTestSession(), CN, nil); err != nil {
207221
t.Fatalf("CheckinWithRisk: %v", err)
208222
}
209-
if gotToken != "mtok" { // newCheckinTestSession's MachineToken
210-
t.Errorf("expected fallback to session machine token, got %q", gotToken)
223+
if gotToken != "" {
224+
t.Errorf("expected no machine headers without a fingerprint, got Cosy-MachineToken=%q", gotToken)
211225
}
212226
}
213227

@@ -226,7 +240,7 @@ func TestCheckinWithBearerUsesSuppliedToken(t *testing.T) {
226240
withCheckinServer(t, srv)
227241

228242
sess := newCheckinTestSession() // securityOauthToken = "sot-token"
229-
if _, err := CheckinWithBearer(context.Background(), sess, "pat-1", CN, "openapi-bearer-xyz", nil); err != nil {
243+
if _, err := CheckinWithBearer(context.Background(), sess, CN, "openapi-bearer-xyz", nil); err != nil {
230244
t.Fatalf("CheckinWithBearer: %v", err)
231245
}
232246
if gotAuth != "Bearer openapi-bearer-xyz" {

‎auth/risk.go‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ package auth
1111
// JSON object with machineToken/machineType/machineCode.
1212
//
1313
// We therefore locate that helper in the installed app and run it. When it is
14-
// unavailable (headless server, different OS, etc.) we fall back to the derived
15-
// identity so the call still works — it just may not see the daily campaign.
14+
// unavailable (headless server, different OS, etc.) no machine headers are sent
15+
// at all: a full set of DERIVED (non-native) values marks the caller as an
16+
// unofficial client and the daily-credit campaign is silently dropped, while
17+
// sending none keeps the generic daily activity visible.
1618

1719
import (
1820
"context"

‎bridge/bridge_test.go‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,6 +2134,43 @@ func TestCNQuotaUsesCOSYToken(t *testing.T) {
21342134
}
21352135
}
21362136

2137+
// TestCNBearerSplit 国内版的两个 bearer 必须分开解析:额度用 COSY 令牌(不触发
2138+
// exchange),而签到 campaigns 端点只认 jt- OpenAPI 令牌,必须走 exchange。
2139+
func TestCNBearerSplit(t *testing.T) {
2140+
var exchangeHits atomic.Int32
2141+
mux := http.NewServeMux()
2142+
mux.HandleFunc("/algo/api/v3/user/jobToken", func(w http.ResponseWriter, r *http.Request) {
2143+
fmt.Fprintf(w, `{"name":"tester","id":"uid1","refreshToken":"rt","securityOauthToken":"sot","expireTime":%d}`, time.Now().Add(6*time.Hour).UnixMilli())
2144+
})
2145+
mux.HandleFunc("/api/v1/jobToken/exchange", func(w http.ResponseWriter, r *http.Request) {
2146+
exchangeHits.Add(1)
2147+
io.WriteString(w, `{"token":"jt-cn","refresh_token":"jrt","expires_at":"2099-01-01T00:00:00Z"}`)
2148+
})
2149+
srv := httptest.NewServer(mux)
2150+
t.Cleanup(srv.Close)
2151+
b := NewOpenAiBridge("pt-test", &auth.RegionConfig{Name: "test", AuthBase: srv.URL, ChatBase: srv.URL, OpenAPIBase: srv.URL})
2152+
if err := b.EnsureFreshSession(context.Background()); err != nil {
2153+
t.Fatalf("EnsureFreshSession: %v", err)
2154+
}
2155+
2156+
// Quota keeps using the COSY session token; no exchange is triggered.
2157+
qtok, err := b.quotaBearer(context.Background())
2158+
if err != nil {
2159+
t.Fatalf("quotaBearer: %v", err)
2160+
}
2161+
if qtok != "sot" || exchangeHits.Load() != 0 {
2162+
t.Errorf("CN quota bearer = %q (exchange hits %d), want the COSY token without exchange", qtok, exchangeHits.Load())
2163+
}
2164+
// The campaigns bearer must be the exchanged jt- token in both regions.
2165+
ctok, err := b.openAPIBearer(context.Background())
2166+
if err != nil {
2167+
t.Fatalf("openAPIBearer: %v", err)
2168+
}
2169+
if ctok != "jt-cn" || exchangeHits.Load() != 1 {
2170+
t.Errorf("campaigns bearer = %q (exchange hits %d), want the exchanged jt- token", ctok, exchangeHits.Load())
2171+
}
2172+
}
2173+
21372174
// TestChatHandlerStripsRegionPrefix 模型名区域前缀在进入 bridge 前被剥离,
21382175
// 统计标签用裸模型名;解析出的 region 传给 resolver。
21392176
func TestChatHandlerStripsRegionPrefix(t *testing.T) {
@@ -2225,8 +2262,8 @@ func TestInvalidateOpenAPIBearer(t *testing.T) {
22252262
}
22262263
}
22272264

2228-
// Checkin 取不到 bearer 时必须直接失败,而不是静默退回 COSY 令牌(global 只
2229-
// 会拒绝那个令牌,退回只会掩盖真实原因并多打一次必然失败的请求)。
2265+
// Checkin 取不到 bearer 时必须直接失败,而不是静默退回 COSY 令牌(两区域的
2266+
// campaigns 端点都会拒绝那个令牌,退回只会掩盖真实原因并多打一次必然失败的请求)。
22302267
func TestCheckinFailsWhenBearerUnavailable(t *testing.T) {
22312268
mux := http.NewServeMux()
22322269
mux.HandleFunc("/algo/api/v3/user/jobToken", func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)