mslxdff 0.1.203 → 0.1.208

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 (332) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +147 -0
  4. package/package.json +38 -38
  5. package/plugins/README.md +14 -14
  6. package/plugins/prefer-model.mjs.example +14 -14
  7. package/src/anthropic/canonical-json.js +55 -0
  8. package/src/anthropic/count-tokens.js +61 -61
  9. package/src/anthropic/request-body.js +183 -126
  10. package/src/anthropic/request-shape.js +60 -0
  11. package/src/anthropic/session-id.js +58 -0
  12. package/src/anthropic/shared.js +18 -18
  13. package/src/anthropic/sse-aggregate.js +56 -56
  14. package/src/anthropic/tool-media.js +75 -0
  15. package/src/anthropic/translate.js +296 -287
  16. package/src/anthropic/web-search.js +361 -361
  17. package/src/api-kind.js +31 -31
  18. package/src/autostart.js +288 -288
  19. package/src/bench/probe.js +1 -1
  20. package/src/bench/runner.js +1 -1
  21. package/src/bench/via-probe.js +1 -1
  22. package/src/bench/via.js +144 -144
  23. package/src/bench/workbuddy-bench.js +1 -1
  24. package/src/chat/config.js +11 -11
  25. package/src/chat/cooling.js +81 -81
  26. package/src/chat/direct.js +58 -58
  27. package/src/chat/index.js +6 -6
  28. package/src/chat/spinner.js +38 -38
  29. package/src/chat/sse.js +69 -69
  30. package/src/chat/stats.js +247 -247
  31. package/src/chat/store.js +48 -48
  32. package/src/chat/tool-handlers.js +67 -67
  33. package/src/chat/tools.js +1 -1
  34. package/src/chat/upstream.js +59 -59
  35. package/src/chat-pipeline/auto-race.js +158 -158
  36. package/src/chat-pipeline/empty-turn.js +147 -147
  37. package/src/chat-pipeline/engine.js +17 -17
  38. package/src/chat-pipeline/index.js +8 -40
  39. package/src/chat-pipeline/serial-trial.js +279 -283
  40. package/src/chat-pipeline/terminal-verdict.js +132 -147
  41. package/src/chooser.js +54 -54
  42. package/src/claude-desktop/apply.js +101 -101
  43. package/src/claude-desktop/context1m.js +38 -38
  44. package/src/claude-desktop/doctor.js +146 -146
  45. package/src/claude-desktop/slots.js +152 -152
  46. package/src/cli/bootstrap.js +1 -1
  47. package/src/cli/commands/claude-desktop.js +284 -284
  48. package/src/cli/commands/daemon.js +231 -231
  49. package/src/cli/commands/model/list-live.js +22 -22
  50. package/src/cli/commands/model/list-providers.js +76 -76
  51. package/src/cli/commands/model/list-render.js +77 -77
  52. package/src/cli/commands/model/list-sort.js +52 -52
  53. package/src/cli/commands/model/list-tty.js +76 -76
  54. package/src/cli/commands/model/list.js +166 -166
  55. package/src/cli/commands/model/live-models.js +24 -24
  56. package/src/cli/commands/model/stats.js +43 -43
  57. package/src/cli/commands/model/status.js +47 -47
  58. package/src/cli/commands/model.js +51 -51
  59. package/src/cli/commands/provider/add.js +108 -108
  60. package/src/cli/commands/provider/allowlist.js +99 -99
  61. package/src/cli/commands/provider/cline-free.js +99 -99
  62. package/src/cli/commands/provider/cline-login.js +218 -218
  63. package/src/cli/commands/provider/cline-quota.js +65 -65
  64. package/src/cli/commands/provider/codearts-login.js +45 -45
  65. package/src/cli/commands/provider/config.js +119 -119
  66. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  67. package/src/cli/commands/provider/keys-guard.js +50 -50
  68. package/src/cli/commands/provider/keys.js +161 -161
  69. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  70. package/src/cli/commands/provider/qoder-login.js +102 -102
  71. package/src/cli/commands/provider/raccoon-login.js +180 -180
  72. package/src/cli/commands/provider/raccoon-password.js +162 -162
  73. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  74. package/src/cli/commands/provider/traework-checkin.js +144 -144
  75. package/src/cli/commands/provider/traework-login.js +164 -164
  76. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  77. package/src/cli/commands/provider/zcode-login.js +77 -77
  78. package/src/cli/commands/provider/zcode-quota.js +55 -55
  79. package/src/cli/commands/provider.js +1 -1
  80. package/src/cli/commands/stats.js +158 -149
  81. package/src/cli/commands/system.js +261 -261
  82. package/src/cli/commands/timezone.js +47 -47
  83. package/src/cli/commands/use-group.js +60 -60
  84. package/src/cli/commands/v6-pool.js +123 -0
  85. package/src/cli/commands/workbuddy.js +189 -189
  86. package/src/cli/format.js +141 -141
  87. package/src/cli/group-helpers.js +70 -70
  88. package/src/cli/help.js +80 -80
  89. package/src/cli/index.js +73 -70
  90. package/src/cli/interactive.js +121 -121
  91. package/src/cli/join-core.js +99 -99
  92. package/src/cli/join-wizard.js +175 -175
  93. package/src/cli/policy.js +136 -136
  94. package/src/cli/provider-row.js +126 -126
  95. package/src/cli/util.js +24 -24
  96. package/src/compat.js +53 -53
  97. package/src/events.js +38 -38
  98. package/src/free-lane.js +131 -131
  99. package/src/free-watcher.js +63 -63
  100. package/src/groups.js +262 -262
  101. package/src/logs.js +195 -195
  102. package/src/metrics.js +71 -62
  103. package/src/model-capabilities/context-lookup.js +65 -65
  104. package/src/model-capabilities/merge.js +142 -142
  105. package/src/model-trace.js +187 -187
  106. package/src/models.js +1 -1
  107. package/src/opencode-identity.js +72 -72
  108. package/src/peers.js +254 -254
  109. package/src/plugins.js +107 -107
  110. package/src/providers/AGENTS.md +49 -0
  111. package/src/providers/base.js +164 -164
  112. package/src/providers/classify.js +22 -22
  113. package/src/providers/cline/allowlist-sync.js +93 -93
  114. package/src/providers/cline/free-catalog.js +71 -71
  115. package/src/providers/cline/headers.js +37 -37
  116. package/src/providers/cline/index.js +130 -130
  117. package/src/providers/cline/models.js +150 -150
  118. package/src/providers/cline/usage.js +274 -274
  119. package/src/providers/codearts/auth-pool.js +148 -148
  120. package/src/providers/codearts/concurrency.js +34 -34
  121. package/src/providers/codearts/const.js +65 -65
  122. package/src/providers/codearts/dpop.js +95 -95
  123. package/src/providers/codearts/index.js +175 -175
  124. package/src/providers/codearts/login.js +214 -214
  125. package/src/providers/codearts/models.js +182 -182
  126. package/src/providers/codearts/sign.js +96 -96
  127. package/src/providers/codearts/sse.js +204 -204
  128. package/src/providers/codearts/stream.js +209 -209
  129. package/src/providers/codearts/sts.js +124 -124
  130. package/src/providers/codearts.js +1 -1
  131. package/src/providers/dispatcher.js +146 -146
  132. package/src/providers/generic.js +156 -156
  133. package/src/providers/globalqwenwork/account-store.js +141 -141
  134. package/src/providers/globalqwenwork/constants.js +73 -73
  135. package/src/providers/globalqwenwork/crypto.js +2 -2
  136. package/src/providers/globalqwenwork/http.js +2 -2
  137. package/src/providers/globalqwenwork/index.js +331 -331
  138. package/src/providers/globalqwenwork/payload.js +147 -147
  139. package/src/providers/globalqwenwork/rsa.js +2 -2
  140. package/src/providers/globalqwenwork/sse.js +2 -2
  141. package/src/providers/globalqwenwork/stream.js +2 -2
  142. package/src/providers/globalqwenwork.js +1 -1
  143. package/src/providers/globalworkbuddy/account-store.js +198 -198
  144. package/src/providers/globalworkbuddy/auth.js +97 -97
  145. package/src/providers/globalworkbuddy/catalog.js +138 -138
  146. package/src/providers/globalworkbuddy/constants.js +73 -73
  147. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  148. package/src/providers/globalworkbuddy/errors.js +98 -98
  149. package/src/providers/globalworkbuddy/headers.js +89 -89
  150. package/src/providers/globalworkbuddy/models.js +96 -96
  151. package/src/providers/globalworkbuddy/payload.js +80 -80
  152. package/src/providers/index.js +5 -5
  153. package/src/providers/model-id.js +172 -172
  154. package/src/providers/opencode.js +19 -19
  155. package/src/providers/openrouter.js +31 -31
  156. package/src/providers/qoder/account-store.js +106 -106
  157. package/src/providers/qoder/aggregate.js +60 -60
  158. package/src/providers/qoder/baseprompt.json +591 -591
  159. package/src/providers/qoder/checkin.js +160 -160
  160. package/src/providers/qoder/constants.js +44 -44
  161. package/src/providers/qoder/encode.js +41 -41
  162. package/src/providers/qoder/fingerprint.js +38 -38
  163. package/src/providers/qoder/models.js +77 -77
  164. package/src/providers/qoder/oauth.js +84 -84
  165. package/src/providers/qoder/payload.js +162 -162
  166. package/src/providers/qoder/request.js +27 -27
  167. package/src/providers/qoder/session.js +134 -134
  168. package/src/providers/qoder/sticky.js +38 -38
  169. package/src/providers/qoder/stream.js +116 -116
  170. package/src/providers/qoder.js +1 -1
  171. package/src/providers/raccoon/account-store.js +216 -216
  172. package/src/providers/raccoon/auth.js +67 -67
  173. package/src/providers/raccoon/chat.js +228 -228
  174. package/src/providers/raccoon/const.js +180 -180
  175. package/src/providers/raccoon/credits.js +185 -185
  176. package/src/providers/raccoon/crypto.js +43 -43
  177. package/src/providers/raccoon/envelope.js +31 -31
  178. package/src/providers/raccoon/headers.js +45 -45
  179. package/src/providers/raccoon/index.js +216 -216
  180. package/src/providers/raccoon/login.js +279 -279
  181. package/src/providers/raccoon/models.js +160 -160
  182. package/src/providers/raccoon/password-login.js +102 -102
  183. package/src/providers/raccoon/qr-codewords.js +115 -115
  184. package/src/providers/raccoon/qr.js +236 -236
  185. package/src/providers/raccoon/sse.js +135 -135
  186. package/src/providers/responses-channel.js +120 -120
  187. package/src/providers/share-keys.js +53 -53
  188. package/src/providers/traework/account-store.js +234 -234
  189. package/src/providers/traework/chat.js +217 -217
  190. package/src/providers/traework/checkin.js +77 -77
  191. package/src/providers/traework/constants.js +41 -41
  192. package/src/providers/traework/errors.js +52 -52
  193. package/src/providers/traework/headers.js +79 -79
  194. package/src/providers/traework/index.js +132 -132
  195. package/src/providers/traework/market-identity.js +116 -116
  196. package/src/providers/traework/models.js +98 -98
  197. package/src/providers/traework/payload.js +86 -86
  198. package/src/providers/traework/sse.js +184 -184
  199. package/src/providers/traework/token.js +52 -52
  200. package/src/providers/traework.js +2 -2
  201. package/src/providers/workbuddy/account-store.js +186 -186
  202. package/src/providers/workbuddy/auth.js +158 -158
  203. package/src/providers/workbuddy/balance.js +86 -86
  204. package/src/providers/workbuddy/cat-travel.js +125 -125
  205. package/src/providers/workbuddy/checkin.js +75 -75
  206. package/src/providers/workbuddy/desktop-info.js +78 -78
  207. package/src/providers/workbuddy/growth-api.js +67 -67
  208. package/src/providers/workbuddy/growth-plans.js +63 -63
  209. package/src/providers/workbuddy/growth.js +181 -181
  210. package/src/providers/workbuddy/index.js +217 -217
  211. package/src/providers/workbuddy/models.js +126 -126
  212. package/src/providers/workbuddy/payload.js +141 -141
  213. package/src/providers/workbuddy/rotation-log.js +83 -83
  214. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  215. package/src/providers/workbuddy/sdk-chat.js +26 -26
  216. package/src/providers/workbuddy-balance.js +78 -78
  217. package/src/providers/zcode/account-store.js +129 -129
  218. package/src/providers/zcode/auth.js +28 -28
  219. package/src/providers/zcode/chat.js +179 -179
  220. package/src/providers/zcode/const.js +54 -54
  221. package/src/providers/zcode/context-shape.js +162 -162
  222. package/src/providers/zcode/headers.js +93 -93
  223. package/src/providers/zcode/index.js +124 -124
  224. package/src/providers/zcode/models.js +65 -65
  225. package/src/providers/zcode/oauth.js +120 -120
  226. package/src/providers/zcode/quota.js +176 -176
  227. package/src/providers/zcode/sse.js +196 -196
  228. package/src/readline-compat.js +25 -25
  229. package/src/responses/translate.js +311 -311
  230. package/src/routes/AGENTS.md +37 -0
  231. package/src/routes/chat/broadband-handler.js +72 -72
  232. package/src/routes/chat/empty-turn-report.js +31 -31
  233. package/src/routes/chat/exhausted-handler.js +87 -87
  234. package/src/routes/chat/hedge-handler.js +130 -130
  235. package/src/routes/chat/index.js +1 -1
  236. package/src/routes/chat/local-handler.js +63 -63
  237. package/src/routes/chat/peer-handler.js +81 -81
  238. package/src/routes/chat.js +3 -3
  239. package/src/routes/fallback.js +88 -88
  240. package/src/routes/groups-relay.js +214 -214
  241. package/src/routes/groups.js +122 -122
  242. package/src/routes/helpers.js +93 -83
  243. package/src/routes/index.js +147 -147
  244. package/src/routes/messages-route.js +375 -365
  245. package/src/routes/peers.js +10 -2
  246. package/src/routes/relay-queue.js +181 -181
  247. package/src/routes/relay.js +60 -60
  248. package/src/routes/responses-route.js +213 -213
  249. package/src/routes/stream-body.js +84 -84
  250. package/src/routes/stream-hold.js +337 -337
  251. package/src/routes/stream-scan.js +366 -366
  252. package/src/routes.js +3 -3
  253. package/src/runtime/auto-update.js +113 -113
  254. package/src/runtime/broadband-stream.js +79 -79
  255. package/src/runtime/broadband.js +103 -103
  256. package/src/runtime/group-sync.js +81 -81
  257. package/src/runtime/keep-warm.js +120 -120
  258. package/src/runtime/lifecycle-forensics.js +116 -116
  259. package/src/runtime/lifecycle-log.js +87 -87
  260. package/src/runtime/log-stamp.js +73 -73
  261. package/src/runtime/proc-info.js +137 -137
  262. package/src/runtime/qoder-checkin.js +93 -93
  263. package/src/runtime/raccoon-checkin.js +126 -126
  264. package/src/runtime/server-lifecycle.js +189 -189
  265. package/src/runtime/traework-checkin.js +118 -118
  266. package/src/runtime/workbuddy-checkin.js +120 -120
  267. package/src/runtime/workbuddy-growth.js +133 -133
  268. package/src/state/facade.js +58 -58
  269. package/src/state/memory.js +162 -162
  270. package/src/state/merge.js +25 -25
  271. package/src/state/migrations/cline-unify.js +94 -94
  272. package/src/state/migrations.js +33 -33
  273. package/src/state/persist.js +42 -42
  274. package/src/state/provider-config.js +142 -142
  275. package/src/state/schemas/group.js +31 -31
  276. package/src/state/schemas/model.js +94 -94
  277. package/src/state/schemas/peer.js +31 -31
  278. package/src/state/schemas/port.js +10 -10
  279. package/src/state/schemas/provider.js +202 -202
  280. package/src/state/schemas/timezone.js +70 -70
  281. package/src/state/schemas/token.js +48 -48
  282. package/src/state/schemas/use-group.js +102 -102
  283. package/src/state/store.js +176 -176
  284. package/src/state.js +1 -1
  285. package/src/sync-claude-desktop.js +234 -234
  286. package/src/sync-claude.js +152 -152
  287. package/src/sync-codex.js +116 -116
  288. package/src/sync-workbuddy.js +158 -158
  289. package/src/talk-full-redact.js +129 -129
  290. package/src/talk-full.js +131 -131
  291. package/src/talk-log.js +257 -257
  292. package/src/text-tokens.js +52 -52
  293. package/src/thinking-effort.js +67 -67
  294. package/src/time.js +106 -106
  295. package/src/timeline.js +26 -26
  296. package/src/transport/index.js +1 -1
  297. package/src/upstream-engine/index.js +74 -74
  298. package/src/upstream-engine/mode.js +15 -15
  299. package/src/upstream-engine/sdk/attempt.js +221 -221
  300. package/src/upstream-engine/sdk/chat.js +28 -28
  301. package/src/upstream-engine/sdk/convert.js +192 -192
  302. package/src/upstream-engine/sdk/diagnose.js +62 -62
  303. package/src/upstream-engine/sdk/dispatch.js +55 -55
  304. package/src/upstream-engine/sdk/responses.js +176 -176
  305. package/src/upstream-engine/sdk/sse.js +166 -166
  306. package/src/upstream-responses.js +305 -305
  307. package/src/usage/cache-fields.js +58 -0
  308. package/src/usage/record.js +109 -106
  309. package/src/usage/report.js +209 -195
  310. package/src/v6pool/claim.js +157 -157
  311. package/src/v6pool/config.js +65 -65
  312. package/src/v6pool/detect.js +130 -130
  313. package/src/v6pool/index.js +152 -152
  314. package/src/v6pool/prefix.js +140 -140
  315. package/src/v6pool/probe.js +40 -40
  316. package/src/v6pool/report.js +104 -104
  317. package/src/v6pool/rotate.js +263 -263
  318. package/src/v6pool/selftest.js +154 -0
  319. package/src/warp-pool/config.js +74 -74
  320. package/src/warp-pool/health.js +106 -106
  321. package/src/warp-pool/identity.js +141 -141
  322. package/src/warp-pool/index.js +358 -358
  323. package/src/warp-pool/keys.js +29 -29
  324. package/src/warp-pool/nodes.js +95 -95
  325. package/src/warp-pool/ports.js +160 -160
  326. package/src/warp-pool/register.js +113 -113
  327. package/src/warp-pool/report.js +232 -232
  328. package/src/warp-pool/revive.js +168 -168
  329. package/src/warp-pool/rotate.js +214 -214
  330. package/src/warp-pool/strategy.js +43 -43
  331. package/src/warp-pool/verdict.js +69 -69
  332. package/src/warp-pool/wireproxy.js +162 -162
@@ -1,185 +1,185 @@
1
- // raccoon 积分:余额五分项查询 + 登录奖励领取(幂等,判据只认账单)。
2
- // 这家是**积分制**,三种积分来源的性质(2026-10-10 账单流水实测 + 参考实现双向取证):
3
- // · 新人注册礼包 3000 —— 注册时服务端自动发,**无端点可领**
4
- // · 每日积分发放 300 —— 每天服务端自动发,**当天 23:59:59 清零**(账单里有 `daily_expire`),也**无端点可领**
5
- // · 桌面端登录奖励 3000 —— **每号一次性**,走 `POST /desktop/v1/login/points/grant`,唯一需要领的
6
- // ⚠ 上游对「已经领过」的重复请求**照样回 code:0**,但积分一分不到账 —— 所以 `code:0` 绝不能当成功凭据,
7
- // 必须用账单流水裁决(见 claimRaccoonLoginReward)。谎报成功会让 daemon/CLI 输出假到账,比报错更坏。
8
- import {
9
- RACCOON_LOGIN_REWARD_EVENT_NAME,
10
- RACCOON_LOGIN_REWARD_POINTS,
11
- RACCOON_REQUEST_TIMEOUT_MS,
12
- raccoonBalanceUrl,
13
- raccoonBillsUrl,
14
- raccoonLoginGrantUrl,
15
- } from "./const.js";
16
- import { raccoonAuthHeaders } from "./headers.js";
17
- import { confirmRaccoonSession } from "./login.js";
18
- import { parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
19
-
20
- const num = (v) => {
21
- if (typeof v === "number" && Number.isFinite(v)) return v;
22
- if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim())) return Number(v.trim());
23
- return undefined;
24
- };
25
-
26
- /** 上游「新会话未确认」闸门码:先 POST /iam/confirm 再重试一次(与 chat.js 同款兜底)。 */
27
- const IAM_CONFIRM_REQUIRED = 200901;
28
-
29
- async function request(url, { credential, fetchImpl = fetch, env = process.env, method = "GET", body, timeoutMs = RACCOON_REQUEST_TIMEOUT_MS } = {}) {
30
- const once = async () => {
31
- const res = await fetchImpl(url, {
32
- method,
33
- headers: raccoonAuthHeaders(credential, { env }),
34
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
35
- signal: AbortSignal.timeout(timeoutMs),
36
- });
37
- let parsed;
38
- try {
39
- parsed = await res.json();
40
- } catch {
41
- throw new Error(`raccoon: 响应不是 JSON(HTTP ${res.status})`);
42
- }
43
- return { res, envelope: parseRaccoonEnvelope(parsed, res.status) };
44
- };
45
- let { res, envelope } = await once();
46
- // 未确认会话闸门:确认一次后重试(老号 / CLI 直连都可能撞上,登录时已确认的号不会走到这)
47
- if (res.status === 400 && Number(envelope.code) === IAM_CONFIRM_REQUIRED) {
48
- if (await confirmRaccoonSession(credential, { fetchImpl, env })) {
49
- ({ res, envelope } = await once());
50
- }
51
- }
52
- return envelope;
53
- }
54
-
55
- /** 余额五分项(缺项即省略,合计优先取上游的 available_points)。 */
56
- export function parseRaccoonBalance(data) {
57
- const src = typeof data === "object" && data !== null ? data : {};
58
- const available = num(src.available_points);
59
- const reward = num(src.reward_points);
60
- const daily = num(src.daily_points);
61
- const topup = num(src.topup_points);
62
- const monthly = num(src.monthly_points);
63
- const items = [];
64
- if (reward !== undefined) items.push({ key: "reward", label: "奖励积分", value: reward });
65
- if (daily !== undefined) items.push({ key: "daily", label: "每日积分", value: daily });
66
- if (monthly !== undefined) items.push({ key: "monthly", label: "月度积分", value: monthly });
67
- if (topup !== undefined) items.push({ key: "topup", label: "充值积分", value: topup });
68
- const total = available !== undefined ? available : items.reduce((s, i) => s + i.value, 0);
69
- return { total, items };
70
- }
71
-
72
- export async function fetchRaccoonBalance(opts = {}) {
73
- const envelope = await request(raccoonBalanceUrl(), opts);
74
- if (envelope.code !== 0) {
75
- const err = new Error(`raccoon: 查询积分失败:${raccoonEnvelopeText(envelope)}`);
76
- err.code = envelope.code;
77
- err.status = envelope.status;
78
- throw err;
79
- }
80
- return parseRaccoonBalance(envelope.data);
81
- }
82
-
83
- /** 账单流水数组(上游把 list/items 两种形状都回过,容错取)。 */
84
- function billsRows(data) {
85
- const list = Array.isArray(data?.list) ? data.list : Array.isArray(data?.items) ? data.items : Array.isArray(data) ? data : null;
86
- return list === null ? undefined : list.filter((x) => typeof x === "object" && x !== null);
87
- }
88
-
89
- /** 上游的时间字段可能是秒/毫秒数字或 ISO 串,统一成毫秒。 */
90
- function billTimeMs(item) {
91
- const at = item.created_at ?? item.createdAt ?? item.time;
92
- if (typeof at === "number" && Number.isFinite(at)) return at > 1e12 ? at : at * 1000;
93
- if (typeof at === "string") {
94
- const ms = Date.parse(at);
95
- if (Number.isFinite(ms)) return ms;
96
- }
97
- return undefined;
98
- }
99
-
100
- /**
101
- * 从账单里找「这个号历史上领过登录奖励没有」,返回 `{ points, at }` 或 undefined。
102
- * ⚠ 判据是**全历史**,不看日期窗口:登录奖励是每号一次性,用「今天」当窗口会让已领的号每天重新发请求,
103
- * 还会把"昨天领的"判成"没领过"。注册礼包同样是 `reward_grant`,所以 `event_name` 必须逐字比对。
104
- */
105
- export function findLoginRewardInBills(data) {
106
- const rows = billsRows(data) || [];
107
- for (const item of rows) {
108
- if (item.biz_type !== "reward_grant") continue;
109
- if (item.event_name !== RACCOON_LOGIN_REWARD_EVENT_NAME) continue;
110
- const at = billTimeMs(item);
111
- return { points: num(item.points) ?? RACCOON_LOGIN_REWARD_POINTS, ...(at === undefined ? {} : { at }) };
112
- }
113
- return undefined;
114
- }
115
-
116
- /** 拉一次账单流水;网络/解析任何失败都返回 undefined(判定降级,不抛)。 */
117
- async function readBills(opts) {
118
- try {
119
- const envelope = await request(raccoonBillsUrl(), opts);
120
- return envelope?.code === 0 ? billsRows(envelope.data) : undefined;
121
- } catch {
122
- return undefined;
123
- }
124
- }
125
-
126
- /**
127
- * 领取「桌面端登录奖励」(每号一次性)。返回三种非抛错结果之一:
128
- * - `{ claimed: true, points }` 本次真到账(账单里出现了登录奖励那笔)
129
- * - `{ claimed: false, alreadyClaimed: true, points }` 历史已领(账单查到,或上游回 `granted:false`)
130
- * - `{ claimed: false, phantom: true, points: 0 }` 上游回了 code:0 但账单没有这笔 —— 虚回,如实报未到账
131
- * - `{ claimed: false, unverified: true, points: 0 }` 账单查不动,无法确认到账(不猜、不谎报)
132
- * grant 请求本身失败(业务码非 0)且账单也查不到已领 → 抛错(如实透出)。
133
- */
134
- export async function claimRaccoonLoginReward({ credential, fetchImpl = fetch, env = process.env, timeoutMs } = {}) {
135
- const opts = { credential, fetchImpl, env, timeoutMs };
136
-
137
- const before = await readBills(opts);
138
- const already = before === undefined ? undefined : findLoginRewardInBills(before);
139
- if (already) return { claimed: false, alreadyClaimed: true, points: already.points };
140
-
141
- let envelope;
142
- try {
143
- envelope = await request(raccoonLoginGrantUrl(), { ...opts, method: "POST", body: {} });
144
- } catch (error) {
145
- envelope = { code: -1, message: error?.message ?? String(error), status: 0, data: undefined };
146
- }
147
-
148
- // 上游用 `data.granted` 直接回答「这次到底发没发」—— 这是比账单更权威的判据,优先采信。
149
- // `granted === false` ⇒ 本号此前已发过(一次性礼包),**不是「未到账」**。
150
- // 为什么不能只靠账单:重度使用的号(如 daemon 正在跑的那个)账单会被「对话消耗」刷满,
151
- // 分页窗口里根本翻不到那条陈年 reward_grant —— 此时只信账单就会把「早就领过」
152
- // 误报成「上游回执成功但账单查不到 → 未到账」(实测踩到,教训)。
153
- // 官方前端同款判据:granted 为真才弹「积分已到账」,为假就静默(见 app.asar grantDesktopLoginPoints 调用点)。
154
- if (envelope?.code === 0 && envelope.data?.granted === false) {
155
- return { claimed: false, alreadyClaimed: true, points: RACCOON_LOGIN_REWARD_POINTS };
156
- }
157
-
158
- // 未给 granted 判据(如上游老版本/测试桩)时回退账单复查 —— 到账凭据仍以账单为准。
159
- const after = await readBills(opts);
160
- const landed = after === undefined ? undefined : findLoginRewardInBills(after);
161
- if (landed) {
162
- const points = num(envelope?.data?.points) ?? num(envelope?.data?.popup?.points) ?? landed.points ?? RACCOON_LOGIN_REWARD_POINTS;
163
- return { claimed: true, points };
164
- }
165
- if (after === undefined) return { claimed: false, unverified: true, points: 0 };
166
- if (envelope?.code === 0) return { claimed: false, phantom: true, points: 0 };
167
-
168
- const err = new Error(`raccoon: 领取登录积分失败:${raccoonEnvelopeText(envelope, "上游未返回原因")}`);
169
- err.code = envelope.code;
170
- err.status = envelope.status;
171
- throw err;
172
- }
173
-
174
- /**
175
- * claimRaccoonLoginReward 的结果 → 统一状态字。daemon 与 CLI 共用,避免两处各写一份分支后漂移。
176
- * claimed=真到账 · already=一次性礼包此前已领过(正常态) · phantom=上游回执成功但账单没这笔 ·
177
- * unverified=账单查不动,不下结论 · error=真失败。
178
- */
179
- export function raccoonClaimStatus(r) {
180
- if (r?.claimed) return "claimed";
181
- if (r?.alreadyClaimed) return "already";
182
- if (r?.phantom) return "phantom";
183
- if (r?.unverified) return "unverified";
184
- return "error";
185
- }
1
+ // raccoon 积分:余额五分项查询 + 登录奖励领取(幂等,判据只认账单)。
2
+ // 这家是**积分制**,三种积分来源的性质(2026-10-10 账单流水实测 + 参考实现双向取证):
3
+ // · 新人注册礼包 3000 —— 注册时服务端自动发,**无端点可领**
4
+ // · 每日积分发放 300 —— 每天服务端自动发,**当天 23:59:59 清零**(账单里有 `daily_expire`),也**无端点可领**
5
+ // · 桌面端登录奖励 3000 —— **每号一次性**,走 `POST /desktop/v1/login/points/grant`,唯一需要领的
6
+ // ⚠ 上游对「已经领过」的重复请求**照样回 code:0**,但积分一分不到账 —— 所以 `code:0` 绝不能当成功凭据,
7
+ // 必须用账单流水裁决(见 claimRaccoonLoginReward)。谎报成功会让 daemon/CLI 输出假到账,比报错更坏。
8
+ import {
9
+ RACCOON_LOGIN_REWARD_EVENT_NAME,
10
+ RACCOON_LOGIN_REWARD_POINTS,
11
+ RACCOON_REQUEST_TIMEOUT_MS,
12
+ raccoonBalanceUrl,
13
+ raccoonBillsUrl,
14
+ raccoonLoginGrantUrl,
15
+ } from "./const.js";
16
+ import { raccoonAuthHeaders } from "./headers.js";
17
+ import { confirmRaccoonSession } from "./login.js";
18
+ import { parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
19
+
20
+ const num = (v) => {
21
+ if (typeof v === "number" && Number.isFinite(v)) return v;
22
+ if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim())) return Number(v.trim());
23
+ return undefined;
24
+ };
25
+
26
+ /** 上游「新会话未确认」闸门码:先 POST /iam/confirm 再重试一次(与 chat.js 同款兜底)。 */
27
+ const IAM_CONFIRM_REQUIRED = 200901;
28
+
29
+ async function request(url, { credential, fetchImpl = fetch, env = process.env, method = "GET", body, timeoutMs = RACCOON_REQUEST_TIMEOUT_MS } = {}) {
30
+ const once = async () => {
31
+ const res = await fetchImpl(url, {
32
+ method,
33
+ headers: raccoonAuthHeaders(credential, { env }),
34
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
35
+ signal: AbortSignal.timeout(timeoutMs),
36
+ });
37
+ let parsed;
38
+ try {
39
+ parsed = await res.json();
40
+ } catch {
41
+ throw new Error(`raccoon: 响应不是 JSON(HTTP ${res.status})`);
42
+ }
43
+ return { res, envelope: parseRaccoonEnvelope(parsed, res.status) };
44
+ };
45
+ let { res, envelope } = await once();
46
+ // 未确认会话闸门:确认一次后重试(老号 / CLI 直连都可能撞上,登录时已确认的号不会走到这)
47
+ if (res.status === 400 && Number(envelope.code) === IAM_CONFIRM_REQUIRED) {
48
+ if (await confirmRaccoonSession(credential, { fetchImpl, env })) {
49
+ ({ res, envelope } = await once());
50
+ }
51
+ }
52
+ return envelope;
53
+ }
54
+
55
+ /** 余额五分项(缺项即省略,合计优先取上游的 available_points)。 */
56
+ export function parseRaccoonBalance(data) {
57
+ const src = typeof data === "object" && data !== null ? data : {};
58
+ const available = num(src.available_points);
59
+ const reward = num(src.reward_points);
60
+ const daily = num(src.daily_points);
61
+ const topup = num(src.topup_points);
62
+ const monthly = num(src.monthly_points);
63
+ const items = [];
64
+ if (reward !== undefined) items.push({ key: "reward", label: "奖励积分", value: reward });
65
+ if (daily !== undefined) items.push({ key: "daily", label: "每日积分", value: daily });
66
+ if (monthly !== undefined) items.push({ key: "monthly", label: "月度积分", value: monthly });
67
+ if (topup !== undefined) items.push({ key: "topup", label: "充值积分", value: topup });
68
+ const total = available !== undefined ? available : items.reduce((s, i) => s + i.value, 0);
69
+ return { total, items };
70
+ }
71
+
72
+ export async function fetchRaccoonBalance(opts = {}) {
73
+ const envelope = await request(raccoonBalanceUrl(), opts);
74
+ if (envelope.code !== 0) {
75
+ const err = new Error(`raccoon: 查询积分失败:${raccoonEnvelopeText(envelope)}`);
76
+ err.code = envelope.code;
77
+ err.status = envelope.status;
78
+ throw err;
79
+ }
80
+ return parseRaccoonBalance(envelope.data);
81
+ }
82
+
83
+ /** 账单流水数组(上游把 list/items 两种形状都回过,容错取)。 */
84
+ function billsRows(data) {
85
+ const list = Array.isArray(data?.list) ? data.list : Array.isArray(data?.items) ? data.items : Array.isArray(data) ? data : null;
86
+ return list === null ? undefined : list.filter((x) => typeof x === "object" && x !== null);
87
+ }
88
+
89
+ /** 上游的时间字段可能是秒/毫秒数字或 ISO 串,统一成毫秒。 */
90
+ function billTimeMs(item) {
91
+ const at = item.created_at ?? item.createdAt ?? item.time;
92
+ if (typeof at === "number" && Number.isFinite(at)) return at > 1e12 ? at : at * 1000;
93
+ if (typeof at === "string") {
94
+ const ms = Date.parse(at);
95
+ if (Number.isFinite(ms)) return ms;
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ /**
101
+ * 从账单里找「这个号历史上领过登录奖励没有」,返回 `{ points, at }` 或 undefined。
102
+ * ⚠ 判据是**全历史**,不看日期窗口:登录奖励是每号一次性,用「今天」当窗口会让已领的号每天重新发请求,
103
+ * 还会把"昨天领的"判成"没领过"。注册礼包同样是 `reward_grant`,所以 `event_name` 必须逐字比对。
104
+ */
105
+ export function findLoginRewardInBills(data) {
106
+ const rows = billsRows(data) || [];
107
+ for (const item of rows) {
108
+ if (item.biz_type !== "reward_grant") continue;
109
+ if (item.event_name !== RACCOON_LOGIN_REWARD_EVENT_NAME) continue;
110
+ const at = billTimeMs(item);
111
+ return { points: num(item.points) ?? RACCOON_LOGIN_REWARD_POINTS, ...(at === undefined ? {} : { at }) };
112
+ }
113
+ return undefined;
114
+ }
115
+
116
+ /** 拉一次账单流水;网络/解析任何失败都返回 undefined(判定降级,不抛)。 */
117
+ async function readBills(opts) {
118
+ try {
119
+ const envelope = await request(raccoonBillsUrl(), opts);
120
+ return envelope?.code === 0 ? billsRows(envelope.data) : undefined;
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * 领取「桌面端登录奖励」(每号一次性)。返回三种非抛错结果之一:
128
+ * - `{ claimed: true, points }` 本次真到账(账单里出现了登录奖励那笔)
129
+ * - `{ claimed: false, alreadyClaimed: true, points }` 历史已领(账单查到,或上游回 `granted:false`)
130
+ * - `{ claimed: false, phantom: true, points: 0 }` 上游回了 code:0 但账单没有这笔 —— 虚回,如实报未到账
131
+ * - `{ claimed: false, unverified: true, points: 0 }` 账单查不动,无法确认到账(不猜、不谎报)
132
+ * grant 请求本身失败(业务码非 0)且账单也查不到已领 → 抛错(如实透出)。
133
+ */
134
+ export async function claimRaccoonLoginReward({ credential, fetchImpl = fetch, env = process.env, timeoutMs } = {}) {
135
+ const opts = { credential, fetchImpl, env, timeoutMs };
136
+
137
+ const before = await readBills(opts);
138
+ const already = before === undefined ? undefined : findLoginRewardInBills(before);
139
+ if (already) return { claimed: false, alreadyClaimed: true, points: already.points };
140
+
141
+ let envelope;
142
+ try {
143
+ envelope = await request(raccoonLoginGrantUrl(), { ...opts, method: "POST", body: {} });
144
+ } catch (error) {
145
+ envelope = { code: -1, message: error?.message ?? String(error), status: 0, data: undefined };
146
+ }
147
+
148
+ // 上游用 `data.granted` 直接回答「这次到底发没发」—— 这是比账单更权威的判据,优先采信。
149
+ // `granted === false` ⇒ 本号此前已发过(一次性礼包),**不是「未到账」**。
150
+ // 为什么不能只靠账单:重度使用的号(如 daemon 正在跑的那个)账单会被「对话消耗」刷满,
151
+ // 分页窗口里根本翻不到那条陈年 reward_grant —— 此时只信账单就会把「早就领过」
152
+ // 误报成「上游回执成功但账单查不到 → 未到账」(实测踩到,教训)。
153
+ // 官方前端同款判据:granted 为真才弹「积分已到账」,为假就静默(见 app.asar grantDesktopLoginPoints 调用点)。
154
+ if (envelope?.code === 0 && envelope.data?.granted === false) {
155
+ return { claimed: false, alreadyClaimed: true, points: RACCOON_LOGIN_REWARD_POINTS };
156
+ }
157
+
158
+ // 未给 granted 判据(如上游老版本/测试桩)时回退账单复查 —— 到账凭据仍以账单为准。
159
+ const after = await readBills(opts);
160
+ const landed = after === undefined ? undefined : findLoginRewardInBills(after);
161
+ if (landed) {
162
+ const points = num(envelope?.data?.points) ?? num(envelope?.data?.popup?.points) ?? landed.points ?? RACCOON_LOGIN_REWARD_POINTS;
163
+ return { claimed: true, points };
164
+ }
165
+ if (after === undefined) return { claimed: false, unverified: true, points: 0 };
166
+ if (envelope?.code === 0) return { claimed: false, phantom: true, points: 0 };
167
+
168
+ const err = new Error(`raccoon: 领取登录积分失败:${raccoonEnvelopeText(envelope, "上游未返回原因")}`);
169
+ err.code = envelope.code;
170
+ err.status = envelope.status;
171
+ throw err;
172
+ }
173
+
174
+ /**
175
+ * claimRaccoonLoginReward 的结果 → 统一状态字。daemon 与 CLI 共用,避免两处各写一份分支后漂移。
176
+ * claimed=真到账 · already=一次性礼包此前已领过(正常态) · phantom=上游回执成功但账单没这笔 ·
177
+ * unverified=账单查不动,不下结论 · error=真失败。
178
+ */
179
+ export function raccoonClaimStatus(r) {
180
+ if (r?.claimed) return "claimed";
181
+ if (r?.alreadyClaimed) return "already";
182
+ if (r?.phantom) return "phantom";
183
+ if (r?.unverified) return "unverified";
184
+ return "error";
185
+ }
@@ -1,43 +1,43 @@
1
- // raccoon 登录参数加密(手机号 / 密码)—— 单一源。
2
- //
3
- // 取证(2026-10-18:app.asar webpack module 68284 原文 + 本机真实账号实测):
4
- // 官方登录页拿到输入后先调 `U.yv()` 再拼 body,而 `yv` 的真身就是这段:
5
- // function c(e, t = "senseraccoon2023") {
6
- // const a = CryptoJS.enc.Utf8.parse(t);
7
- // const i = CryptoJS.lib.WordArray.random(16); // 随机 IV
8
- // const r = CryptoJS.AES.encrypt(e, a, { iv: i, mode: CFB, padding: NoPadding });
9
- // return i.concat(r.ciphertext).toString(CryptoJS.enc.Base64);
10
- // }
11
- // 即 **key 与模式都硬编码在前端**:AES-128-CFB、key = "senseraccoon2023"、无 padding,
12
- // 输出 Base64(IV(16 字节) ‖ 密文)。手机号与密码走的是同一个函数 —— 两个字段都要加密。
13
- //
14
- // 为什么必须加密(否则极难自查):明文发送时服务端解得空串再去比对,一律回
15
- // `AUTH_ACCOUNT_OR_PASSWORD_INVALID`("手机号或密码不正确")—— 用「密码错」的口径
16
- // 掩盖了「格式错」。本机用真实账号取证:明文 → 401;按本算法加密 → 200。
17
- // 且只加密手机号、密码留明文也是 401,可证**两个字段都加密**。
18
- //
19
- // ⚠ 这个 key 是**上游公开常量**(打包进任意客户端),不是本项目的机密;照抄它只为对齐协议,
20
- // 不代表传输具备保密性 —— 真正的保护来自 HTTPS,以及本项目「密码不落盘/不打印」的纪律。
21
- import { createCipheriv, randomBytes } from "node:crypto";
22
- import { RACCOON_PASSWORD_ENC_KEY } from "./const.js";
23
-
24
- const IV_BYTES = 16;
25
-
26
- /**
27
- * 加密单个登录字段(手机号或密码)→ Base64(IV ‖ AES-128-CFB 密文)。
28
- *
29
- * 与上游前端**逐位对齐**:同 key、同模式(CFB128,对齐 CryptoJS 的 `mode.CFB`)、
30
- * 同 padding(NoPadding —— CFB 是流模式,密文长度恒等于明文长度)、同拼接顺序。
31
- * 每次调用生成新的随机 IV(上游亦然):同一输入两次调用密文不同,这是正确行为。
32
- */
33
- export function raccoonEncryptField(plain, keyStr = RACCOON_PASSWORD_ENC_KEY) {
34
- const key = Buffer.from(String(keyStr ?? ""), "utf8");
35
- if (key.length !== 16) throw new Error("raccoon: 登录加密 key 必须是 16 字节(AES-128)");
36
- const iv = randomBytes(IV_BYTES);
37
- const cipher = createCipheriv("aes-128-cfb", key, iv);
38
- const body = Buffer.concat([
39
- cipher.update(Buffer.from(String(plain ?? ""), "utf8")),
40
- cipher.final(),
41
- ]);
42
- return Buffer.concat([iv, body]).toString("base64");
43
- }
1
+ // raccoon 登录参数加密(手机号 / 密码)—— 单一源。
2
+ //
3
+ // 取证(2026-10-18:app.asar webpack module 68284 原文 + 本机真实账号实测):
4
+ // 官方登录页拿到输入后先调 `U.yv()` 再拼 body,而 `yv` 的真身就是这段:
5
+ // function c(e, t = "senseraccoon2023") {
6
+ // const a = CryptoJS.enc.Utf8.parse(t);
7
+ // const i = CryptoJS.lib.WordArray.random(16); // 随机 IV
8
+ // const r = CryptoJS.AES.encrypt(e, a, { iv: i, mode: CFB, padding: NoPadding });
9
+ // return i.concat(r.ciphertext).toString(CryptoJS.enc.Base64);
10
+ // }
11
+ // 即 **key 与模式都硬编码在前端**:AES-128-CFB、key = "senseraccoon2023"、无 padding,
12
+ // 输出 Base64(IV(16 字节) ‖ 密文)。手机号与密码走的是同一个函数 —— 两个字段都要加密。
13
+ //
14
+ // 为什么必须加密(否则极难自查):明文发送时服务端解得空串再去比对,一律回
15
+ // `AUTH_ACCOUNT_OR_PASSWORD_INVALID`("手机号或密码不正确")—— 用「密码错」的口径
16
+ // 掩盖了「格式错」。本机用真实账号取证:明文 → 401;按本算法加密 → 200。
17
+ // 且只加密手机号、密码留明文也是 401,可证**两个字段都加密**。
18
+ //
19
+ // ⚠ 这个 key 是**上游公开常量**(打包进任意客户端),不是本项目的机密;照抄它只为对齐协议,
20
+ // 不代表传输具备保密性 —— 真正的保护来自 HTTPS,以及本项目「密码不落盘/不打印」的纪律。
21
+ import { createCipheriv, randomBytes } from "node:crypto";
22
+ import { RACCOON_PASSWORD_ENC_KEY } from "./const.js";
23
+
24
+ const IV_BYTES = 16;
25
+
26
+ /**
27
+ * 加密单个登录字段(手机号或密码)→ Base64(IV ‖ AES-128-CFB 密文)。
28
+ *
29
+ * 与上游前端**逐位对齐**:同 key、同模式(CFB128,对齐 CryptoJS 的 `mode.CFB`)、
30
+ * 同 padding(NoPadding —— CFB 是流模式,密文长度恒等于明文长度)、同拼接顺序。
31
+ * 每次调用生成新的随机 IV(上游亦然):同一输入两次调用密文不同,这是正确行为。
32
+ */
33
+ export function raccoonEncryptField(plain, keyStr = RACCOON_PASSWORD_ENC_KEY) {
34
+ const key = Buffer.from(String(keyStr ?? ""), "utf8");
35
+ if (key.length !== 16) throw new Error("raccoon: 登录加密 key 必须是 16 字节(AES-128)");
36
+ const iv = randomBytes(IV_BYTES);
37
+ const cipher = createCipheriv("aes-128-cfb", key, iv);
38
+ const body = Buffer.concat([
39
+ cipher.update(Buffer.from(String(plain ?? ""), "utf8")),
40
+ cipher.final(),
41
+ ]);
42
+ return Buffer.concat([iv, body]).toString("base64");
43
+ }
@@ -1,31 +1,31 @@
1
- // raccoon 上游统一信封:`{ code, message, details, data }`(与 pack.js 的 parseEnvelope 同形)。
2
- // ⚠ 两代通道的成功码不同(2026-10 上游改版):旧 /api/web/* 用数字 `0`,新 /api/iam/v1/* 用字符串 `"OK"`。
3
- // 故 `code` 原样保留(数字或字符串),判成功一律走 isRaccoonOk()——**不能**再用 `code === 0`:
4
- // 字符串码一律不等于 0(把 "OK" 判成失败),而把它兜底成 0 又会把 `AUTH_QRCODE_EXPIRED` 判成成功。
5
- // 登录态类业务码:200001 未带 token、200003 登录态过期 —— 见 const.js 的 RACCOON_ERROR_KINDS。
6
- export function parseRaccoonEnvelope(payload, status = 200) {
7
- const record = typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : {};
8
- const raw = record.code;
9
- const code = typeof raw === "number" || typeof raw === "string" ? raw : status >= 400 ? status : 0;
10
- const message = typeof record.message === "string" ? record.message : "";
11
- const details = typeof record.details === "string" ? record.details : "";
12
- const data = typeof record.data === "object" && record.data !== null && !Array.isArray(record.data) ? record.data : undefined;
13
- return { code, message, details, data, status };
14
- }
15
-
16
- /** 业务成功判据(两代通道通用):数字 0,或新 IAM 通道的字符串 "OK"。 */
17
- export function isRaccoonOk(envelope) {
18
- return envelope?.code === 0 || envelope?.code === "OK";
19
- }
20
-
21
- /** 人话错误文本:`message: details`,都没有时用调用方给的兜底句。 */
22
- export function raccoonEnvelopeText(envelope, fallback = "请求失败") {
23
- const parts = [envelope?.message, envelope?.details].filter((s) => typeof s === "string" && s.length > 0);
24
- return parts.length > 0 ? parts.join(":") : fallback;
25
- }
26
-
27
- /** 登录态失效判据:HTTP 401 或业务码 200001/200003。 */
28
- export function isRaccoonAuthFailure(envelope, status) {
29
- if (status === 401) return true;
30
- return envelope?.code === 200001 || envelope?.code === 200003;
31
- }
1
+ // raccoon 上游统一信封:`{ code, message, details, data }`(与 pack.js 的 parseEnvelope 同形)。
2
+ // ⚠ 两代通道的成功码不同(2026-10 上游改版):旧 /api/web/* 用数字 `0`,新 /api/iam/v1/* 用字符串 `"OK"`。
3
+ // 故 `code` 原样保留(数字或字符串),判成功一律走 isRaccoonOk()——**不能**再用 `code === 0`:
4
+ // 字符串码一律不等于 0(把 "OK" 判成失败),而把它兜底成 0 又会把 `AUTH_QRCODE_EXPIRED` 判成成功。
5
+ // 登录态类业务码:200001 未带 token、200003 登录态过期 —— 见 const.js 的 RACCOON_ERROR_KINDS。
6
+ export function parseRaccoonEnvelope(payload, status = 200) {
7
+ const record = typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : {};
8
+ const raw = record.code;
9
+ const code = typeof raw === "number" || typeof raw === "string" ? raw : status >= 400 ? status : 0;
10
+ const message = typeof record.message === "string" ? record.message : "";
11
+ const details = typeof record.details === "string" ? record.details : "";
12
+ const data = typeof record.data === "object" && record.data !== null && !Array.isArray(record.data) ? record.data : undefined;
13
+ return { code, message, details, data, status };
14
+ }
15
+
16
+ /** 业务成功判据(两代通道通用):数字 0,或新 IAM 通道的字符串 "OK"。 */
17
+ export function isRaccoonOk(envelope) {
18
+ return envelope?.code === 0 || envelope?.code === "OK";
19
+ }
20
+
21
+ /** 人话错误文本:`message: details`,都没有时用调用方给的兜底句。 */
22
+ export function raccoonEnvelopeText(envelope, fallback = "请求失败") {
23
+ const parts = [envelope?.message, envelope?.details].filter((s) => typeof s === "string" && s.length > 0);
24
+ return parts.length > 0 ? parts.join(":") : fallback;
25
+ }
26
+
27
+ /** 登录态失效判据:HTTP 401 或业务码 200001/200003。 */
28
+ export function isRaccoonAuthFailure(envelope, status) {
29
+ if (status === 401) return true;
30
+ return envelope?.code === 200001 || envelope?.code === 200003;
31
+ }
@@ -1,45 +1,45 @@
1
- // raccoon 出站请求头:与官方客户端同形(逐字段对齐 pack.js 的 raccoonHeaders)。
2
- // 风控按版本/头判断,形态错会 401/403 —— 所以这里不做「优化」,只做照抄。
3
- import {
4
- RACCOON_CLIENT_LANGUAGE,
5
- RACCOON_CLIENT_PLATFORM,
6
- raccoonClientVersion,
7
- raccoonUserAgent,
8
- } from "./const.js";
9
-
10
- /** 匿名头:登录轮询等不需要 token 的调用(官方客户端在这些路径也不带 Authorization)。 */
11
- export function raccoonAnonymousHeaders({ env = process.env, accept = "application/json" } = {}) {
12
- return {
13
- Accept: accept,
14
- "Content-Type": "application/json",
15
- "User-Agent": raccoonUserAgent(env),
16
- "X-Raccoon-Language": RACCOON_CLIENT_LANGUAGE,
17
- "X-Client-Platform": RACCOON_CLIENT_PLATFORM,
18
- "X-Client-Version": raccoonClientVersion(env),
19
- };
20
- }
21
-
22
- /**
23
- * 带 token 的出站头。
24
- * - `X-Org-Code`:个人号为空串(官方客户端总是发送该头)。
25
- * - `X-Client-Device-ID`:**为空则不发**(个人号可选;被网关拒时的退化路径)。
26
- * - `X-Client-Version` / `User-Agent` 走 env 可配(见 const.js)。
27
- */
28
- export function raccoonAuthHeaders(credential, { env = process.env, accept = "application/json", version } = {}) {
29
- const headers = {
30
- Accept: accept,
31
- "Content-Type": "application/json",
32
- Authorization: `Bearer ${String(credential?.access_token ?? "")}`,
33
- "X-Org-Code": String(credential?.office_identity ?? ""),
34
- "X-Raccoon-Language": RACCOON_CLIENT_LANGUAGE,
35
- "X-Client-Platform": RACCOON_CLIENT_PLATFORM,
36
- "X-Client-Version": version || raccoonClientVersion(env),
37
- "User-Agent": raccoonUserAgent(env),
38
- };
39
- const deviceId = String(credential?.device_id ?? "").trim();
40
- if (deviceId) headers["X-Client-Device-ID"] = deviceId;
41
- return headers;
42
- }
43
-
44
- /** 聊天的 Accept 要比 JSON 宽:上游流式与非流式共用同一端点。 */
45
- export const RACCOON_CHAT_ACCEPT = "text/event-stream, application/json";
1
+ // raccoon 出站请求头:与官方客户端同形(逐字段对齐 pack.js 的 raccoonHeaders)。
2
+ // 风控按版本/头判断,形态错会 401/403 —— 所以这里不做「优化」,只做照抄。
3
+ import {
4
+ RACCOON_CLIENT_LANGUAGE,
5
+ RACCOON_CLIENT_PLATFORM,
6
+ raccoonClientVersion,
7
+ raccoonUserAgent,
8
+ } from "./const.js";
9
+
10
+ /** 匿名头:登录轮询等不需要 token 的调用(官方客户端在这些路径也不带 Authorization)。 */
11
+ export function raccoonAnonymousHeaders({ env = process.env, accept = "application/json" } = {}) {
12
+ return {
13
+ Accept: accept,
14
+ "Content-Type": "application/json",
15
+ "User-Agent": raccoonUserAgent(env),
16
+ "X-Raccoon-Language": RACCOON_CLIENT_LANGUAGE,
17
+ "X-Client-Platform": RACCOON_CLIENT_PLATFORM,
18
+ "X-Client-Version": raccoonClientVersion(env),
19
+ };
20
+ }
21
+
22
+ /**
23
+ * 带 token 的出站头。
24
+ * - `X-Org-Code`:个人号为空串(官方客户端总是发送该头)。
25
+ * - `X-Client-Device-ID`:**为空则不发**(个人号可选;被网关拒时的退化路径)。
26
+ * - `X-Client-Version` / `User-Agent` 走 env 可配(见 const.js)。
27
+ */
28
+ export function raccoonAuthHeaders(credential, { env = process.env, accept = "application/json", version } = {}) {
29
+ const headers = {
30
+ Accept: accept,
31
+ "Content-Type": "application/json",
32
+ Authorization: `Bearer ${String(credential?.access_token ?? "")}`,
33
+ "X-Org-Code": String(credential?.office_identity ?? ""),
34
+ "X-Raccoon-Language": RACCOON_CLIENT_LANGUAGE,
35
+ "X-Client-Platform": RACCOON_CLIENT_PLATFORM,
36
+ "X-Client-Version": version || raccoonClientVersion(env),
37
+ "User-Agent": raccoonUserAgent(env),
38
+ };
39
+ const deviceId = String(credential?.device_id ?? "").trim();
40
+ if (deviceId) headers["X-Client-Device-ID"] = deviceId;
41
+ return headers;
42
+ }
43
+
44
+ /** 聊天的 Accept 要比 JSON 宽:上游流式与非流式共用同一端点。 */
45
+ export const RACCOON_CHAT_ACCEPT = "text/event-stream, application/json";