mslxdff 0.1.174 → 0.1.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +254 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/auto-race.js +26 -1
  19. package/src/chat-pipeline/index.js +131 -131
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/chat-pipeline/serial-trial.js +35 -8
  22. package/src/chat-pipeline/terminal-verdict.js +109 -0
  23. package/src/cli/commands/group.js +249 -249
  24. package/src/cli/commands/model/picks.js +50 -50
  25. package/src/cli/commands/provider/bench-via.js +247 -247
  26. package/src/cli/commands/provider/bench.js +141 -141
  27. package/src/cli/commands/provider/index.js +136 -133
  28. package/src/cli/commands/provider/keys-guard.js +50 -0
  29. package/src/cli/commands/provider/keys.js +7 -0
  30. package/src/cli/commands/provider/models.js +145 -145
  31. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  32. package/src/cli/commands/provider/traework-checkin.js +144 -0
  33. package/src/cli/commands/provider/traework-login.js +7 -45
  34. package/src/cli/commands/sync.js +310 -310
  35. package/src/cli/help.js +3 -0
  36. package/src/cli/status.js +279 -279
  37. package/src/daemon.js +112 -112
  38. package/src/model-capabilities/enrich.js +86 -86
  39. package/src/model-capabilities/index.js +183 -183
  40. package/src/model-capabilities/parse.js +70 -70
  41. package/src/model-trace.js +2 -0
  42. package/src/models.js +225 -225
  43. package/src/providers/cline/auth.js +228 -228
  44. package/src/providers/cline/chat.js +307 -307
  45. package/src/providers/cline.js +2 -2
  46. package/src/providers/codearts/chat.js +7 -1
  47. package/src/providers/codearts/const.js +6 -1
  48. package/src/providers/codearts/index.js +18 -11
  49. package/src/providers/globalqwenwork/cosy.js +123 -123
  50. package/src/providers/globalqwenwork/crypto.js +218 -218
  51. package/src/providers/globalqwenwork/http.js +20 -20
  52. package/src/providers/globalqwenwork/rsa.js +54 -54
  53. package/src/providers/globalqwenwork/sse.js +268 -268
  54. package/src/providers/globalqwenwork/stream.js +130 -130
  55. package/src/providers/globalqwenwork/upstream.js +122 -122
  56. package/src/providers/keyring.js +60 -60
  57. package/src/providers/qoder/chat.js +203 -203
  58. package/src/providers/qoder/index.js +249 -249
  59. package/src/providers/qoder/sse.js +145 -145
  60. package/src/providers/qwenwork/account-store.js +133 -133
  61. package/src/providers/qwenwork/constants.js +67 -67
  62. package/src/providers/qwenwork/cosy.js +120 -120
  63. package/src/providers/qwenwork/crypto.js +218 -218
  64. package/src/providers/qwenwork/http.js +20 -20
  65. package/src/providers/qwenwork/index.js +327 -327
  66. package/src/providers/qwenwork/payload.js +144 -144
  67. package/src/providers/qwenwork/rsa.js +54 -54
  68. package/src/providers/qwenwork/sse.js +268 -268
  69. package/src/providers/qwenwork/stream.js +130 -130
  70. package/src/providers/qwenwork/upstream.js +120 -120
  71. package/src/providers/qwenwork.js +1 -1
  72. package/src/providers/registry.js +79 -79
  73. package/src/providers/traework/account-store.js +87 -2
  74. package/src/providers/traework/checkin.js +51 -9
  75. package/src/providers/traework/constants.js +20 -0
  76. package/src/providers/traework/headers.js +42 -10
  77. package/src/providers/traework/index.js +14 -4
  78. package/src/providers/traework/market-identity.js +116 -0
  79. package/src/providers/workbuddy/chat.js +248 -248
  80. package/src/providers/workbuddy/reshape.js +152 -152
  81. package/src/providers/workbuddy.js +2 -2
  82. package/src/reasoning.js +32 -32
  83. package/src/routes/chat/gateway.js +46 -46
  84. package/src/routes/chat/relay-pipeline.js +294 -294
  85. package/src/routes/chat/via-route-handler.js +146 -146
  86. package/src/routes/hedge.js +255 -255
  87. package/src/routes/models-route.js +167 -167
  88. package/src/routes/peers.js +273 -273
  89. package/src/routes/stream.js +385 -385
  90. package/src/runtime/bootstrap.js +50 -50
  91. package/src/runtime/provider-gate.js +36 -36
  92. package/src/runtime/providers-setup.js +168 -165
  93. package/src/runtime/traework-checkin.js +26 -11
  94. package/src/server.js +69 -69
  95. package/src/state/schemas/allowlist.js +92 -92
  96. package/src/sync-opencode.js +280 -280
  97. package/src/transport/index.js +244 -244
  98. package/src/transport/pool.js +56 -56
  99. package/src/transport/retry.js +24 -24
  100. package/src/transport/sse.js +93 -93
  101. package/src/upstream-probe/display.js +52 -52
  102. package/src/upstream-probe/probe.js +49 -49
  103. package/src/upstream-probe/rotate.js +110 -110
  104. package/src/upstream-probe/start.js +45 -45
  105. package/src/upstream.js +291 -289
  106. package/src/warp-pool/config.js +71 -0
  107. package/src/warp-pool/health.js +65 -0
  108. package/src/warp-pool/index.js +261 -0
  109. package/src/warp-pool/keys.js +29 -0
  110. package/src/warp-pool/nodes.js +94 -0
  111. package/src/warp-pool/register.js +113 -0
  112. package/src/warp-pool/rotate.js +150 -0
  113. package/src/warp-pool/strategy.js +43 -0
  114. package/src/warp-pool/wireproxy.js +153 -0
  115. package/docs/cli_help_mini.md +0 -143
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -4,6 +4,8 @@ import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, readdirSy
4
4
  import { join, dirname } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { isTestEnv, defaultStateFile } from "../../state/store.js";
7
+ // 市场侧伪设备身份:派生算法在 market-identity.js(纯函数),本文件只管落盘与取用。
8
+ import { deriveMarketIdentity, marketUsable } from "./market-identity.js";
7
9
 
8
10
  export function authDirFor({ explicit = "", testEnv = false, stateFile } = {}) {
9
11
  if (explicit) return explicit;
@@ -34,8 +36,8 @@ export function resolveAuthDirs() {
34
36
  });
35
37
  }
36
38
 
37
- export function findAccountFile(uid) {
38
- for (const dir of resolveAuthDirs()) {
39
+ export function findAccountFile(uid, { dirs } = {}) {
40
+ for (const dir of dirs || resolveAuthDirs()) {
39
41
  const fp = join(dir, `trae-${uid}.json`);
40
42
  if (existsSync(fp)) return fp;
41
43
  }
@@ -147,3 +149,86 @@ export async function applyTokenRefresh({ uid, oldKey, newToken, refreshToken =
147
149
  const st = await persistAccounts(accounts, file, extraKeys);
148
150
  return { updated: matched, accounts: st.keys.length, file: fp };
149
151
  }
152
+
153
+ /**
154
+ * 按 uid 回 auth 文件取「设备身份」(machineId/deviceId/apiHost/enterpriseId/domain)。
155
+ *
156
+ * 为什么必须回文件而不是信 state 行:`src/state/provider-config.js` 的 `normalizeAuths` 是
157
+ * {uid,domain,enterpriseId,refreshToken} **四字段白名单**(为 WorkBuddy 设计),任何写 state 的路径
158
+ * 都会把 machineId/deviceId/apiHost 静默剥光 —— 于是 `persistAccounts:100` 与 `applyTokenRefresh:141`
159
+ * 精心写的这三个字段**从来没落过盘**。缺 X-Device-Id 时上游 `ug/checkin_credits/claim` 回
160
+ * `code=9004 The submitted order parameters are incorrect`(2026-10-15 实测:同一次登录用局部 deviceId
161
+ * 那一发回的是 9074「参与用户太多」,两行代码之差即因果)。
162
+ */
163
+ export function traeworkIdentityByUid(uid, { dirs } = {}) {
164
+ const blank = { machineId: "", deviceId: "", apiHost: "", domain: "", enterpriseId: "" };
165
+ const want = String(uid || "");
166
+ if (!want) return blank;
167
+ for (const { uid: u, doc } of listAccountDocs({ dirs })) {
168
+ if (String(u) !== want) continue;
169
+ const a = (doc && doc.auth) || {}, acc = (doc && doc.account) || {};
170
+ return {
171
+ machineId: String(a.machineId || ""),
172
+ deviceId: String(a.deviceId || ""),
173
+ apiHost: String(a.apiHost || ""),
174
+ domain: String(a.domain || ""),
175
+ enterpriseId: String(acc.enterpriseId || a.enterpriseId || ""),
176
+ };
177
+ }
178
+ return blank;
179
+ }
180
+
181
+ /** 批量版:一次扫目录,按 uid 把 authList 行补齐身份(state 已有的值优先,缺的才回文件取)。 */
182
+ export function withTraeworkIdentity(authList = [], { dirs } = {}) {
183
+ const byUid = new Map(listAccountDocs({ dirs }).map((d) => [String(d.uid), d.doc]));
184
+ return (Array.isArray(authList) ? authList : []).map((row) => {
185
+ const doc = byUid.get(String((row && row.uid) || ""));
186
+ if (!doc) return { ...(row || {}) };
187
+ const a = (doc && doc.auth) || {}, acc = (doc && doc.account) || {};
188
+ return {
189
+ ...(row || {}),
190
+ machineId: (row && row.machineId) || a.machineId || "",
191
+ deviceId: (row && row.deviceId) || a.deviceId || "",
192
+ apiHost: (row && row.apiHost) || a.apiHost || "",
193
+ domain: (row && row.domain) || a.domain || "trae.cn",
194
+ enterpriseId: (row && row.enterpriseId) || acc.enterpriseId || "",
195
+ // accessToken 仍以 state.keys[i] 为准(keyring 按它轮转),只在行里空时兜一份文件里的
196
+ ...(row && row.accessToken ? {} : (a.accessToken ? { accessToken: a.accessToken } : {})),
197
+ };
198
+ });
199
+ }
200
+
201
+ /**
202
+ * 取该账号的市场侧伪设备身份(UG 接口的 x-device-id / x-market-user-id / vscode-sessionid)。
203
+ *
204
+ * 真相源是 `auths/trae-<uid>.json` 的 `market` 块,**不是 state**:`normalizeAuths` 是四字段白名单
205
+ * (`src/state/provider-config.js:21`),写进 state 的 auths 行必被剥光(2026-10-15 实测)。
206
+ * 存量缺失/形状不对/gen 落后 → 按 uid 确定性重建并回写文档;同 uid 永远得到同一套身份,
207
+ * 因此重登不会换设备(换设备在上游眼里就是新设备,签到风控会跟上来,见参考实现 device.rs:108)。
208
+ */
209
+ export function ensureMarketIdentity(uid, { dirs } = {}) {
210
+ const want = String(uid || "").trim();
211
+ const derived = deriveMarketIdentity(want);
212
+ if (!want) return derived; // 无 uid:只可能拿到不可复现的兜底值,不写盘(没有文件名可写)
213
+ const fp = findAccountFile(want, { dirs });
214
+ if (!fp) return derived;
215
+ let doc = null;
216
+ try {
217
+ doc = JSON.parse(readFileSync(fp, "utf8"));
218
+ } catch {
219
+ return derived;
220
+ }
221
+ if (marketUsable(doc && doc.market)) {
222
+ const m = doc.market;
223
+ return { deviceId: String(m.deviceId), marketUserId: String(m.marketUserId), sessionId: String(m.sessionId), created: m.created || "", gen: Number(m.gen) };
224
+ }
225
+ // 就地补写:只加/换 `market` 一个键,其余字段原样保留(不碰 auth/account,防洗掉 token 与身份)
226
+ try {
227
+ const next = { ...doc, market: derived };
228
+ const tmp = `${fp}.tmp`;
229
+ writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
230
+ try { renameSync(tmp, fp); } catch { writeFileSync(fp, JSON.stringify(next, null, 2), { mode: 0o600 }); try { unlinkSync(tmp); } catch {} }
231
+ try { chmodSync(fp, 0o600); } catch {}
232
+ } catch {}
233
+ return derived;
234
+ }
@@ -2,9 +2,18 @@
2
2
  import { UG_HOST, EP_CHECKIN_STATUS, EP_CHECKIN_CLAIM, EP_ENT_USAGE } from "./constants.js";
3
3
  import { ugHeaders } from "./headers.js";
4
4
  import { compatFetch, timeoutSignal } from "../../compat.js";
5
+ import { ensureMarketIdentity } from "./account-store.js";
6
+
7
+ // 三个 UG 接口共用这一发:市场身份在此统一挂载,调用点就不必各自记得带
8
+ // (少带一件 x-device-id,claim 即回 9004;见 headers.js 的 ugHeaders 注释)。
9
+ function withUgIdentity(cred = {}) {
10
+ if (cred.market) return cred; // 调用方已备好自己的身份(含测试注入)→ 尊重它,不越权读盘
11
+ const uid = String(cred.uid || "").trim();
12
+ return { ...cred, market: ensureMarketIdentity(uid) };
13
+ }
5
14
 
6
15
  async function postJson(fetchImpl, url, cred, timeoutMs = 15000) {
7
- const res = await fetchImpl(url, { method: "POST", headers: ugHeaders(cred), body: "{}", signal: timeoutSignal(timeoutMs) });
16
+ const res = await fetchImpl(url, { method: "POST", headers: ugHeaders(withUgIdentity(cred)), body: "{}", signal: timeoutSignal(timeoutMs) });
8
17
  const txt = await res.text().catch(() => "");
9
18
  let j = null;
10
19
  try { j = JSON.parse(txt); } catch { j = { _raw: txt.slice(0, 200) }; }
@@ -17,19 +26,52 @@ export async function checkinStatus({ cred, baseUrl = UG_HOST, fetchImpl = compa
17
26
  return { checkedIn: Boolean(json?.checked_in), credits: Number(json?.credits) || 0, enable: Boolean(json?.enable) };
18
27
  }
19
28
 
29
+ // 领取每日签到积分。⚠ 别单用本函数判成败:上游对重复领取照样回 HTTP 200(raccoon 踩过同一坑,
30
+ // 判据口径见 src/cli/commands/provider/raccoon-quota.js:3-5「只信回执就会谎报到账」)。
31
+ // 本函数只做「HTTP 通了 + 回执 code 非明确失败」;到账与否请用下面 checkinClaimVerified。
32
+ // `code` 字段缺失时不判失败——claim 的响应体没实测过,不臆造上游契约。
20
33
  export async function checkinClaim({ cred, baseUrl = UG_HOST, fetchImpl = compatFetch, timeoutMs = 15000 } = {}) {
21
- const { res } = await postJson(fetchImpl, `${String(baseUrl).replace(/\/+$/, "")}${EP_CHECKIN_CLAIM}`, cred, timeoutMs);
34
+ const { res, json } = await postJson(fetchImpl, `${String(baseUrl).replace(/\/+$/, "")}${EP_CHECKIN_CLAIM}`, cred, timeoutMs);
22
35
  if (res.status >= 400) throw Object.assign(new Error(`checkin claim http ${res.status}`), { status: res.status });
23
- return { ok: true };
36
+ const code = json && typeof json === "object" && json.code !== undefined ? Number(json.code) : undefined;
37
+ return {
38
+ ok: code === undefined || code === 0,
39
+ code,
40
+ message: String(json?.message || json?.msg || "").slice(0, 120),
41
+ raw: json,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * 领取并**复核到账** —— 签到唯一该用的入口。
47
+ * `claimed` 只在「领取后复查 checkinStatus().checkedIn === true」时为真;
48
+ * 回执没报错但复查仍 false → `phantom: true`,调用方必须按**未签到**处理(绝不写进 state 当今天签过了)。
49
+ * opts.before:调用方已查过状态时传进来,省一次往返。
50
+ */
51
+ export async function checkinClaimVerified(opts = {}) {
52
+ const before = opts.before || (await checkinStatus(opts));
53
+ if (before.checkedIn) return { claimed: false, already: true, phantom: false, checkedIn: true, credits: before.credits, msg: "" };
54
+ if (!before.enable) return { claimed: false, already: false, phantom: false, checkedIn: false, credits: before.credits, msg: "checkin not enabled" };
55
+ let claim = { ok: false, code: undefined, message: "" };
56
+ try {
57
+ claim = await checkinClaim(opts);
58
+ } catch (e) {
59
+ return { claimed: false, already: false, phantom: false, checkedIn: false, credits: before.credits, msg: `claim http: ${String(e?.message || e).slice(0, 80)}` };
60
+ }
61
+ const after = await checkinStatus(opts);
62
+ if (after.checkedIn) return { claimed: true, already: false, phantom: false, checkedIn: true, credits: after.credits, msg: "" };
63
+ const why = claim.ok ? "上游回执未报错、但复查 checked_in 仍为 false(判未到账)" : `上游回执 code=${claim.code}${claim.message ? ` ${claim.message}` : ""}`;
64
+ return { claimed: false, already: false, phantom: claim.ok, checkedIn: false, credits: after.credits, msg: why.slice(0, 120) };
24
65
  }
25
66
 
26
- // 积分:ide_user_ent_usage 的 credits_limit 求和。
67
+ // 积分额度:把 ide_user_ent_usage 各 pack 的 credits_limit 求和。
68
+ // ⚠ 这是**额度上限**、不是剩余可用:实测该接口只回 credits_limit 与 enable_solo_* 开关,
69
+ // 没有任何已用/剩余字段。故模型报 4008(requests exceeded the quota)时本数照样非零 —— 别拿它当余额判生死。
27
70
  export async function entUsage({ cred, baseUrl = UG_HOST, fetchImpl = compatFetch, timeoutMs = 15000 } = {}) {
28
71
  const { res, json } = await postJson(fetchImpl, `${String(baseUrl).replace(/\/+$/, "")}${EP_ENT_USAGE}`, cred, timeoutMs);
29
72
  if (res.status >= 400) throw Object.assign(new Error(`ent usage http ${res.status}`), { status: res.status });
30
- const packs = json?.user_entitlement_pack_list;
31
- if (!Array.isArray(packs)) return { remain: 0, isCreditsBilling: Boolean(json?.is_credits_billing) };
32
- let remain = 0;
33
- for (const p of packs) remain += Number(p?.entitlement_base_info?.quota?.credits_limit) || 0;
34
- return { remain, isCreditsBilling: Boolean(json?.is_credits_billing) };
73
+ const packs = Array.isArray(json?.user_entitlement_pack_list) ? json.user_entitlement_pack_list : [];
74
+ let totalCredits = 0;
75
+ for (const p of packs) totalCredits += Number(p?.entitlement_base_info?.quota?.credits_limit) || 0;
76
+ return { totalCredits, isCreditsBilling: Boolean(json?.is_credits_billing), packCount: packs.length };
35
77
  }
@@ -19,3 +19,23 @@ export const EP_USER_INFO = "/cloudide/api/v3/trae/GetUserInfo";
19
19
  export const EP_CHECKIN_STATUS = "/trae/api/v2/ug/checkin_credits/status";
20
20
  export const EP_CHECKIN_CLAIM = "/trae/api/v2/ug/checkin_credits/claim";
21
21
  export const EP_ENT_USAGE = "/trae/api/v2/pay/ide_user_ent_usage";
22
+
23
+ // ── 市场/UG 侧线上契约(status/claim/ent_usage 共用)────────────────────────
24
+ // 出处:抓包复刻自 NextAgentX/trae-workbuddy-switch `modules/trae/credits.rs:49-99`,
25
+ // 不是从客户端源码抄的 —— 该仓自述「把 `x-market-client-id`/`x-market-user-id`/`x-lscbd-aid`
26
+ // 与整串 `VSCode 1.107.1 (TRAE SOLO CN)` 在 Trae 安装目录 5141 个文件(含 .exe/.asar)里全扫一遍,
27
+ // **一个字面量都搜不到**」,即无法从客户端侧核对,只能以「线上实测能用」为准。
28
+ // ⚠ 因此这些值是**线上契约**,不要"顺手改成看起来更对的";要改必须先拿到抓包证据。
29
+ // 上面那段纪律原文见该仓 credits.rs:53-76。
30
+ export const MARKET_UA_PRODUCT = "TRAE SOLO CN"; // UA 括号里的产品名
31
+ export const MARKET_VSCODE_VERSION = "1.107.1"; // 真机 resources/app/package.json 的 version
32
+ export const MARKET_UA = `VSCode ${MARKET_VSCODE_VERSION} (${MARKET_UA_PRODUCT})`;
33
+ export const MARKET_AID = "787976"; // x-lscbd-aid(活动/埋点侧应用标识)
34
+ export const MARKET_SDK_TYPE = "3"; // x-lgw-req-sdk-type
35
+ export const MARKET_PACKAGE_TYPE = "stable_cn"; // package-type,CN 稳定版通道
36
+ export const MARKET_USER_REGION = "CN"; // x-user-region,产品级 CN 通道,不随账号 Region 变
37
+ export const MARKET_APP_VERSION = "0.1.45"; // app-version(≠ 上面的 IDE_VERSION 0.1.43:两条通道各自取值,别合)
38
+
39
+ // 市场伪设备身份的派生算法版本(抄 device.rs:35 DEVICE_GEN)。
40
+ // auth 文档里的 `market.gen` 缺失或小于此值 → 视为旧算法产物,就地重建(自愈)。
41
+ export const MARKET_DEVICE_GEN = 2;
@@ -1,5 +1,6 @@
1
1
  // SOLO 三类请求头(照抄 traework2api internal/upstream/headers.go)。纯函数,可测。
2
- import { APP_ID, DEVICE_BRAND, IDE_VERSION, IDE_VERSION_CODE, OS_VERSION, TRAE_UA } from "./constants.js";
2
+ import { APP_ID, DEVICE_BRAND, IDE_VERSION, IDE_VERSION_CODE, OS_VERSION, TRAE_UA, MARKET_UA, MARKET_VSCODE_VERSION, MARKET_USER_REGION, MARKET_SDK_TYPE, MARKET_PACKAGE_TYPE, MARKET_AID, MARKET_APP_VERSION } from "./constants.js";
3
+ import { uuid } from "../../compat.js";
3
4
 
4
5
  // 对话/模型表(Cloud-IDE-JWT 鉴权 + IDE 指纹)。
5
6
  export function soloHeaders(cred = {}, stream = true) {
@@ -28,17 +29,48 @@ export function soloHeaders(cred = {}, stream = true) {
28
29
  return h;
29
30
  }
30
31
 
31
- // 签到/积分(api.trae.cn)。
32
+ // 签到/积分/额度(api.trae.cn 的 UG 与 pay 系接口)。
33
+ // ⚠ 这套头是**线上契约**:三件设备身份(x-device-id / x-market-user-id / vscode-sessionid)缺一或形态不对,
34
+ // `checkin_credits/claim` 就回 `code=9004 下单参数不正确`(2026-10-15 现网取证)。
35
+ // 逐字对齐 NextAgentX/trae-workbuddy-switch `modules/trae/credits.rs:116-147`,取值出处与该仓纪律见 constants.js 注释。
32
36
  export function ugHeaders(cred = {}) {
33
- const h = {
34
- "Content-Type": "application/json",
35
- Accept: "application/json",
36
- "User-Agent": TRAE_UA,
37
- Authorization: `Cloud-IDE-JWT ${cred?.accessToken || ""}`,
38
- "X-User-Region": "CN",
37
+ const m = cred?.market || {};
38
+ const at = String(cred?.accessToken || "");
39
+ return {
40
+ accept: "*/*",
41
+ "accept-language": "zh-CN",
42
+ "content-type": "application/json",
43
+ "user-agent": MARKET_UA,
44
+ // 防双前缀:传进来的可能已带 `Cloud-IDE-JWT `(口径同参考实现 jwt.rs:53-61)
45
+ authorization: at.startsWith("Cloud-IDE-JWT ") ? at : `Cloud-IDE-JWT ${at}`,
46
+ "x-market-client-id": `VSCode ${MARKET_VSCODE_VERSION}`,
47
+ "x-market-user-id": String(m.marketUserId || ""),
48
+ "x-user-region": MARKET_USER_REGION,
49
+ "x-device-id": String(m.deviceId || ""),
50
+ "x-lgw-req-sdk-type": MARKET_SDK_TYPE,
51
+ "package-type": MARKET_PACKAGE_TYPE,
52
+ "x-request-id": uuid(),
53
+ "x-lscbd-aid": MARKET_AID,
54
+ "x-lscbd-platform": ugPlatformTag(),
55
+ "app-version": MARKET_APP_VERSION,
56
+ "x-tt-trace-id": ugTraceId(),
57
+ "vscode-sessionid": String(m.sessionId || ""),
58
+ "sec-fetch-dest": "empty",
59
+ "sec-fetch-mode": "no-cors",
60
+ "sec-fetch-site": "none",
39
61
  };
40
- if (cred?.deviceId) h["X-Device-Id"] = cred.deviceId;
41
- return h;
62
+ }
63
+
64
+ /** `x-lscbd-platform`:按真实平台报,不自称 Windows(参考实现 credits.rs:150-160 的理由:减少跨平台特征矛盾)。 */
65
+ export function ugPlatformTag(p = process.platform) {
66
+ if (p === "win32") return "windows";
67
+ if (p === "darwin") return "macos";
68
+ return "linux";
69
+ }
70
+
71
+ /** `x-tt-trace-id` = `00-<uuid 去连字符前 16 位>-01`(参考实现 credits.rs:162-166)。 */
72
+ export function ugTraceId(rnd = uuid) {
73
+ return `00-${String(rnd()).replace(/-/g, "").slice(0, 16)}-01`;
42
74
  }
43
75
 
44
76
  // ExchangeToken / GetUserInfo(无签名,仅 UA)。
@@ -4,7 +4,7 @@ import { loadProviderKeys, loadProviderAuths, loadProviderBaseUrl } from "../../
4
4
  import { compatFetch, getUndici } from "../../compat.js";
5
5
  import { envInt, createAgent } from "../base.js";
6
6
  import { AGENT_HOST } from "./constants.js";
7
- import { listAccountDocs } from "./account-store.js";
7
+ import { listAccountDocs, withTraeworkIdentity } from "./account-store.js";
8
8
  import { createChatService } from "./chat.js";
9
9
  import { createModelsService } from "./models.js";
10
10
 
@@ -66,14 +66,24 @@ export function createTraeworkProvider({
66
66
  }
67
67
  } catch {}
68
68
  }
69
+ // 无论 authList 来自 state 还是刚扫的目录,都回 auth 文档补一次设备身份:
70
+ // state 的 auths 行被 `normalizeAuths` 四字段白名单剥掉了 machineId/deviceId(见 account-store.js 同段注释),
71
+ // 不补则 SOLO 对话永远不发 X-Machine-Id / X-Device-Id。
72
+ authList = withTraeworkIdentity(authList);
69
73
  // auths 行自带 refreshToken 但无 accessToken(login 后 keys 与 auths 平行);
70
- // keys 与 authList 不配对(如 state 被外部改写)→ 从 auth 目录按 uid 补齐 accessToken,防 ring 空
71
- if (authList.length && keys.length !== authList.length) {
74
+ // 补齐触发条件不能只看长度相等:2026-10-15 实测 `-provider traework <未接线子命令>` 掉进 keys 兜底分支,
75
+ // 把 keys[0] 覆盖成 5 字符垃圾串,长度仍与 authList 相等 → 旧判据放行,真拿垃圾串当 accessToken 鉴权。
76
+ // 故加形态判据:不像凭据(短、含空白)的槽位一律回 auth 目录重取。
77
+ // (判空白逐字符 trim 比对,不用 \s 正则——本仓 Edit 通道会把单反斜杠吃成两个,留可读写法防再踩)
78
+ const hasBlank = (k) => [...k].some((c) => c.trim() === "");
79
+ const looksLikeCredential = (k) => typeof k === "string" && k.trim().length >= 20 && !hasBlank(k);
80
+ const needRefill = authList.length && (keys.length !== authList.length || keys.some((k, i) => authList[i]?.uid && !looksLikeCredential(k)));
81
+ if (needRefill) {
72
82
  try {
73
83
  const docs = new Map(listAccountDocs().map((d) => [String(d.uid), d.doc]));
74
84
  authList.forEach((a, i) => {
75
85
  const at = docs.get(String(a.uid))?.auth?.accessToken;
76
- if (at && !keys.includes(at)) keys[i] = at;
86
+ if (at) keys[i] = at; // 按位序覆盖:keys 与 authList 是平行数组(account-store 同语义),去重判据会留垃圾槽位
77
87
  });
78
88
  } catch {}
79
89
  }
@@ -0,0 +1,116 @@
1
+ // traework 市场侧「伪设备身份」派生 —— 纯函数,不碰 fs/env。
2
+ //
3
+ // 为什么需要它(2026-10-15 现网取证):UG 系接口(`checkin_credits/status|claim`、`pay/ide_user_ent_usage`)
4
+ // 要一整套 VSCode/market 请求头,其中三件是**按账号派生的设备身份**:
5
+ // x-device-id = 15 位十进制数字串
6
+ // x-market-user-id = UUID v4 形态
7
+ // vscode-sessionid = 64 位十六进制串
8
+ // 我们原先只发 5 个头、且把 OAuth 那个 32 位 hex 当 x-device-id 用 → `claim` 回
9
+ // `code=9004 The submitted order parameters are incorrect`;补齐后同一发回 `9074 当前参与用户太多`
10
+ // (已过参数校验,进到了排队/风控层)。
11
+ //
12
+ // 算法逐字对齐 NextAgentX/trae-workbuddy-switch `modules/trae/device.rs:59-151`
13
+ // (salt `devid`/`sess`/`market` 与 SHA256 计数器模式都不能改 —— 改了同 uid 会派生出另一套身份,
14
+ // 在风控眼里等同换设备)。落盘与 gen 自愈见 `account-store.js` 的 ensureMarketIdentity。
15
+ import { createHash, randomBytes } from "node:crypto";
16
+ import { MARKET_DEVICE_GEN } from "./constants.js";
17
+
18
+ /** 每号 x-device-id 的位数(真机形态:15 位十进制,不是 OAuth 的 32 位 hex)。 */
19
+ export const MARKET_DEVICE_ID_DIGITS = 15;
20
+ /** vscode-sessionid 位数(64 位 hex)。 */
21
+ export const MARKET_SESSION_HEX_LEN = 64;
22
+
23
+ /**
24
+ * 确定性字节流:`SHA256("<salt>:<seed>" || u32BE(counter))` 逐块拼接后截断到 nbytes。
25
+ * seed 为空返回 null —— 调用方必须显式处理「无种子」,不许悄悄退化成不可复现的随机值
26
+ * (口径同参考实现 device.rs:59-79)。
27
+ */
28
+ export function seededStream(seed, salt, nbytes) {
29
+ const s = String(seed || "");
30
+ if (!s || !(nbytes > 0)) return null;
31
+ const prefix = Buffer.from(`${salt}:${s}`, "utf8");
32
+ const out = [];
33
+ let counter = 0;
34
+ while (out.length < nbytes) {
35
+ const h = createHash("sha256");
36
+ h.update(prefix);
37
+ const c = Buffer.alloc(4);
38
+ c.writeUInt32BE(counter >>> 0, 0);
39
+ h.update(c);
40
+ out.push(...new Uint8Array(h.digest()));
41
+ counter = (counter + 1) >>> 0;
42
+ }
43
+ return Uint8Array.from(out.slice(0, nbytes));
44
+ }
45
+
46
+ /** 无种子时的兜底字节(仅在 uid 缺失的异常路径上用到;正常流程永远走确定性派生)。 */
47
+ function fallbackBytes(nbytes) {
48
+ return new Uint8Array(randomBytes(nbytes));
49
+ }
50
+
51
+ /**
52
+ * 生成 n 位十进制数字串(x-device-id 用)。
53
+ * ⚠ 必须 `String(b % 10)` 出**单个数字字符**,不能写 `String(48 + (b % 10))` ——
54
+ * 后者在 JS 里是"数字转十进制字符串"(码点 49 → 两个字符 `"49"`),会把 n 字节撑成 2n 位、
55
+ * 且首位恒为 `4`/`5`。Rust 侧 `char::from(b'0' + b % 10)` 是码点→字符,语义即本行。
56
+ * (2026-10-15 移植时真踩过,靠形状断言 `^[0-9]{15}$` 才暴露。)
57
+ * 每字节出一位,故 seededStream(n) 取 n 字节即得 n 位、无截断。
58
+ */
59
+ export function marketDigits(seed, n = MARKET_DEVICE_ID_DIGITS) {
60
+ const bytes = seededStream(seed, "devid", n) || fallbackBytes(n);
61
+ let out = "";
62
+ for (const b of bytes) out += String(b % 10);
63
+ return out;
64
+ }
65
+
66
+ /** 按任意 salt 派生 n 位十六进制串(needed=ceil(n/2),n 为偶数时截断是 no-op,奇数 n 靠 slice 截)。 */
67
+ export function marketHexSalted(n, salt, seed) {
68
+ const needed = Math.ceil(n / 2);
69
+ const bytes = seededStream(seed, salt, needed) || fallbackBytes(needed);
70
+ let hex = "";
71
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
72
+ return hex.slice(0, n);
73
+ }
74
+
75
+ /** vscode-sessionid 专用(salt 固定 `sess`)。 */
76
+ export function marketSession(seed, n = MARKET_SESSION_HEX_LEN) {
77
+ return marketHexSalted(n, "sess", seed);
78
+ }
79
+
80
+ /** 标准 UUID v4 形态(salt 固定 `market`),并置版本位 4 与 RFC4122 变体位,使输出与 uuid4() 一致。 */
81
+ export function marketUuid(seed) {
82
+ const raw = seededStream(seed, "market", 16) || fallbackBytes(16);
83
+ const b = Array.from(raw);
84
+ b[6] = (b[6] & 0x0f) | 0x40;
85
+ b[8] = (b[8] & 0x3f) | 0x80;
86
+ const hex = b.map((x) => x.toString(16).padStart(2, "0")).join("");
87
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
88
+ }
89
+
90
+ /**
91
+ * 按 uid 派生完整市场身份(不落盘)。`created` 只在**新生成**时盖时间戳,
92
+ * 复用盘上旧值时由调用方保留原 created(换 created 不影响身份稳定性)。
93
+ */
94
+ export function deriveMarketIdentity(uid) {
95
+ const seed = String(uid || "").trim();
96
+ return {
97
+ deviceId: marketDigits(seed, MARKET_DEVICE_ID_DIGITS),
98
+ marketUserId: marketUuid(seed),
99
+ sessionId: marketSession(seed),
100
+ created: new Date().toISOString(),
101
+ gen: MARKET_DEVICE_GEN,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * 校验一份存量 market 块是否可用:三件齐、形状对、gen 不小于当前算法版本。
107
+ * 任一不满足都返回 false → 调用方重新派生并回写(对应参考实现的「gen 缺失按 1 处理,下次取用就地重建」)。
108
+ */
109
+ export function marketUsable(m) {
110
+ if (!m || typeof m !== "object") return false;
111
+ if (Number(m.gen || 0) < MARKET_DEVICE_GEN) return false;
112
+ if (!/^[0-9]{15}$/.test(String(m.deviceId || ""))) return false;
113
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(String(m.marketUserId || ""))) return false;
114
+ if (!/^[0-9a-f]{64}$/.test(String(m.sessionId || ""))) return false;
115
+ return true;
116
+ }