mslxdff 0.1.203 → 0.1.207

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 (330) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +146 -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/workbuddy.js +189 -189
  85. package/src/cli/format.js +141 -141
  86. package/src/cli/group-helpers.js +70 -70
  87. package/src/cli/help.js +80 -80
  88. package/src/cli/index.js +70 -70
  89. package/src/cli/interactive.js +121 -121
  90. package/src/cli/join-core.js +99 -99
  91. package/src/cli/join-wizard.js +175 -175
  92. package/src/cli/policy.js +136 -136
  93. package/src/cli/provider-row.js +126 -126
  94. package/src/cli/util.js +24 -24
  95. package/src/compat.js +53 -53
  96. package/src/events.js +38 -38
  97. package/src/free-lane.js +131 -131
  98. package/src/free-watcher.js +63 -63
  99. package/src/groups.js +262 -262
  100. package/src/logs.js +195 -195
  101. package/src/metrics.js +71 -62
  102. package/src/model-capabilities/context-lookup.js +65 -65
  103. package/src/model-capabilities/merge.js +142 -142
  104. package/src/model-trace.js +187 -187
  105. package/src/models.js +1 -1
  106. package/src/opencode-identity.js +72 -72
  107. package/src/peers.js +254 -254
  108. package/src/plugins.js +107 -107
  109. package/src/providers/AGENTS.md +49 -0
  110. package/src/providers/base.js +164 -164
  111. package/src/providers/classify.js +22 -22
  112. package/src/providers/cline/allowlist-sync.js +93 -93
  113. package/src/providers/cline/free-catalog.js +71 -71
  114. package/src/providers/cline/headers.js +37 -37
  115. package/src/providers/cline/index.js +130 -130
  116. package/src/providers/cline/models.js +150 -150
  117. package/src/providers/cline/usage.js +274 -274
  118. package/src/providers/codearts/auth-pool.js +148 -148
  119. package/src/providers/codearts/concurrency.js +34 -34
  120. package/src/providers/codearts/const.js +65 -65
  121. package/src/providers/codearts/dpop.js +95 -95
  122. package/src/providers/codearts/index.js +175 -175
  123. package/src/providers/codearts/login.js +214 -214
  124. package/src/providers/codearts/models.js +182 -182
  125. package/src/providers/codearts/sign.js +96 -96
  126. package/src/providers/codearts/sse.js +204 -204
  127. package/src/providers/codearts/stream.js +209 -209
  128. package/src/providers/codearts/sts.js +124 -124
  129. package/src/providers/codearts.js +1 -1
  130. package/src/providers/dispatcher.js +146 -146
  131. package/src/providers/generic.js +156 -156
  132. package/src/providers/globalqwenwork/account-store.js +141 -141
  133. package/src/providers/globalqwenwork/constants.js +73 -73
  134. package/src/providers/globalqwenwork/crypto.js +2 -2
  135. package/src/providers/globalqwenwork/http.js +2 -2
  136. package/src/providers/globalqwenwork/index.js +331 -331
  137. package/src/providers/globalqwenwork/payload.js +147 -147
  138. package/src/providers/globalqwenwork/rsa.js +2 -2
  139. package/src/providers/globalqwenwork/sse.js +2 -2
  140. package/src/providers/globalqwenwork/stream.js +2 -2
  141. package/src/providers/globalqwenwork.js +1 -1
  142. package/src/providers/globalworkbuddy/account-store.js +198 -198
  143. package/src/providers/globalworkbuddy/auth.js +97 -97
  144. package/src/providers/globalworkbuddy/catalog.js +138 -138
  145. package/src/providers/globalworkbuddy/constants.js +73 -73
  146. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  147. package/src/providers/globalworkbuddy/errors.js +98 -98
  148. package/src/providers/globalworkbuddy/headers.js +89 -89
  149. package/src/providers/globalworkbuddy/models.js +96 -96
  150. package/src/providers/globalworkbuddy/payload.js +80 -80
  151. package/src/providers/index.js +5 -5
  152. package/src/providers/model-id.js +172 -172
  153. package/src/providers/opencode.js +19 -19
  154. package/src/providers/openrouter.js +31 -31
  155. package/src/providers/qoder/account-store.js +106 -106
  156. package/src/providers/qoder/aggregate.js +60 -60
  157. package/src/providers/qoder/baseprompt.json +591 -591
  158. package/src/providers/qoder/checkin.js +160 -160
  159. package/src/providers/qoder/constants.js +44 -44
  160. package/src/providers/qoder/encode.js +41 -41
  161. package/src/providers/qoder/fingerprint.js +38 -38
  162. package/src/providers/qoder/models.js +77 -77
  163. package/src/providers/qoder/oauth.js +84 -84
  164. package/src/providers/qoder/payload.js +162 -162
  165. package/src/providers/qoder/request.js +27 -27
  166. package/src/providers/qoder/session.js +134 -134
  167. package/src/providers/qoder/sticky.js +38 -38
  168. package/src/providers/qoder/stream.js +116 -116
  169. package/src/providers/qoder.js +1 -1
  170. package/src/providers/raccoon/account-store.js +216 -216
  171. package/src/providers/raccoon/auth.js +67 -67
  172. package/src/providers/raccoon/chat.js +228 -228
  173. package/src/providers/raccoon/const.js +180 -180
  174. package/src/providers/raccoon/credits.js +185 -185
  175. package/src/providers/raccoon/crypto.js +43 -43
  176. package/src/providers/raccoon/envelope.js +31 -31
  177. package/src/providers/raccoon/headers.js +45 -45
  178. package/src/providers/raccoon/index.js +216 -216
  179. package/src/providers/raccoon/login.js +279 -279
  180. package/src/providers/raccoon/models.js +160 -160
  181. package/src/providers/raccoon/password-login.js +102 -102
  182. package/src/providers/raccoon/qr-codewords.js +115 -115
  183. package/src/providers/raccoon/qr.js +236 -236
  184. package/src/providers/raccoon/sse.js +135 -135
  185. package/src/providers/responses-channel.js +120 -120
  186. package/src/providers/share-keys.js +53 -53
  187. package/src/providers/traework/account-store.js +234 -234
  188. package/src/providers/traework/chat.js +217 -217
  189. package/src/providers/traework/checkin.js +77 -77
  190. package/src/providers/traework/constants.js +41 -41
  191. package/src/providers/traework/errors.js +52 -52
  192. package/src/providers/traework/headers.js +79 -79
  193. package/src/providers/traework/index.js +132 -132
  194. package/src/providers/traework/market-identity.js +116 -116
  195. package/src/providers/traework/models.js +98 -98
  196. package/src/providers/traework/payload.js +86 -86
  197. package/src/providers/traework/sse.js +184 -184
  198. package/src/providers/traework/token.js +52 -52
  199. package/src/providers/traework.js +2 -2
  200. package/src/providers/workbuddy/account-store.js +186 -186
  201. package/src/providers/workbuddy/auth.js +158 -158
  202. package/src/providers/workbuddy/balance.js +86 -86
  203. package/src/providers/workbuddy/cat-travel.js +125 -125
  204. package/src/providers/workbuddy/checkin.js +75 -75
  205. package/src/providers/workbuddy/desktop-info.js +78 -78
  206. package/src/providers/workbuddy/growth-api.js +67 -67
  207. package/src/providers/workbuddy/growth-plans.js +63 -63
  208. package/src/providers/workbuddy/growth.js +181 -181
  209. package/src/providers/workbuddy/index.js +217 -217
  210. package/src/providers/workbuddy/models.js +126 -126
  211. package/src/providers/workbuddy/payload.js +141 -141
  212. package/src/providers/workbuddy/rotation-log.js +83 -83
  213. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  214. package/src/providers/workbuddy/sdk-chat.js +26 -26
  215. package/src/providers/workbuddy-balance.js +78 -78
  216. package/src/providers/zcode/account-store.js +129 -129
  217. package/src/providers/zcode/auth.js +28 -28
  218. package/src/providers/zcode/chat.js +179 -179
  219. package/src/providers/zcode/const.js +54 -54
  220. package/src/providers/zcode/context-shape.js +162 -162
  221. package/src/providers/zcode/headers.js +93 -93
  222. package/src/providers/zcode/index.js +124 -124
  223. package/src/providers/zcode/models.js +65 -65
  224. package/src/providers/zcode/oauth.js +120 -120
  225. package/src/providers/zcode/quota.js +176 -176
  226. package/src/providers/zcode/sse.js +196 -196
  227. package/src/readline-compat.js +25 -25
  228. package/src/responses/translate.js +311 -311
  229. package/src/routes/AGENTS.md +37 -0
  230. package/src/routes/chat/broadband-handler.js +72 -72
  231. package/src/routes/chat/empty-turn-report.js +31 -31
  232. package/src/routes/chat/exhausted-handler.js +87 -87
  233. package/src/routes/chat/hedge-handler.js +130 -130
  234. package/src/routes/chat/index.js +1 -1
  235. package/src/routes/chat/local-handler.js +63 -63
  236. package/src/routes/chat/peer-handler.js +81 -81
  237. package/src/routes/chat.js +3 -3
  238. package/src/routes/fallback.js +88 -88
  239. package/src/routes/groups-relay.js +214 -214
  240. package/src/routes/groups.js +122 -122
  241. package/src/routes/helpers.js +93 -83
  242. package/src/routes/index.js +147 -147
  243. package/src/routes/messages-route.js +375 -365
  244. package/src/routes/peers.js +10 -2
  245. package/src/routes/relay-queue.js +181 -181
  246. package/src/routes/relay.js +60 -60
  247. package/src/routes/responses-route.js +213 -213
  248. package/src/routes/stream-body.js +84 -84
  249. package/src/routes/stream-hold.js +337 -337
  250. package/src/routes/stream-scan.js +366 -366
  251. package/src/routes.js +3 -3
  252. package/src/runtime/auto-update.js +113 -113
  253. package/src/runtime/broadband-stream.js +79 -79
  254. package/src/runtime/broadband.js +103 -103
  255. package/src/runtime/group-sync.js +81 -81
  256. package/src/runtime/keep-warm.js +120 -120
  257. package/src/runtime/lifecycle-forensics.js +116 -116
  258. package/src/runtime/lifecycle-log.js +87 -87
  259. package/src/runtime/log-stamp.js +73 -73
  260. package/src/runtime/proc-info.js +137 -137
  261. package/src/runtime/qoder-checkin.js +93 -93
  262. package/src/runtime/raccoon-checkin.js +126 -126
  263. package/src/runtime/server-lifecycle.js +189 -189
  264. package/src/runtime/traework-checkin.js +118 -118
  265. package/src/runtime/workbuddy-checkin.js +120 -120
  266. package/src/runtime/workbuddy-growth.js +133 -133
  267. package/src/state/facade.js +58 -58
  268. package/src/state/memory.js +162 -162
  269. package/src/state/merge.js +25 -25
  270. package/src/state/migrations/cline-unify.js +94 -94
  271. package/src/state/migrations.js +33 -33
  272. package/src/state/persist.js +42 -42
  273. package/src/state/provider-config.js +142 -142
  274. package/src/state/schemas/group.js +31 -31
  275. package/src/state/schemas/model.js +94 -94
  276. package/src/state/schemas/peer.js +31 -31
  277. package/src/state/schemas/port.js +10 -10
  278. package/src/state/schemas/provider.js +202 -202
  279. package/src/state/schemas/timezone.js +70 -70
  280. package/src/state/schemas/token.js +48 -48
  281. package/src/state/schemas/use-group.js +102 -102
  282. package/src/state/store.js +176 -176
  283. package/src/state.js +1 -1
  284. package/src/sync-claude-desktop.js +234 -234
  285. package/src/sync-claude.js +152 -152
  286. package/src/sync-codex.js +116 -116
  287. package/src/sync-workbuddy.js +158 -158
  288. package/src/talk-full-redact.js +129 -129
  289. package/src/talk-full.js +131 -131
  290. package/src/talk-log.js +257 -257
  291. package/src/text-tokens.js +52 -52
  292. package/src/thinking-effort.js +67 -67
  293. package/src/time.js +106 -106
  294. package/src/timeline.js +26 -26
  295. package/src/transport/index.js +1 -1
  296. package/src/upstream-engine/index.js +74 -74
  297. package/src/upstream-engine/mode.js +15 -15
  298. package/src/upstream-engine/sdk/attempt.js +221 -221
  299. package/src/upstream-engine/sdk/chat.js +28 -28
  300. package/src/upstream-engine/sdk/convert.js +192 -192
  301. package/src/upstream-engine/sdk/diagnose.js +62 -62
  302. package/src/upstream-engine/sdk/dispatch.js +55 -55
  303. package/src/upstream-engine/sdk/responses.js +176 -176
  304. package/src/upstream-engine/sdk/sse.js +166 -166
  305. package/src/upstream-responses.js +305 -305
  306. package/src/usage/cache-fields.js +58 -0
  307. package/src/usage/record.js +109 -106
  308. package/src/usage/report.js +209 -195
  309. package/src/v6pool/claim.js +157 -157
  310. package/src/v6pool/config.js +65 -65
  311. package/src/v6pool/detect.js +130 -130
  312. package/src/v6pool/index.js +152 -152
  313. package/src/v6pool/prefix.js +140 -140
  314. package/src/v6pool/probe.js +40 -40
  315. package/src/v6pool/report.js +104 -104
  316. package/src/v6pool/rotate.js +263 -263
  317. package/src/warp-pool/config.js +74 -74
  318. package/src/warp-pool/health.js +106 -106
  319. package/src/warp-pool/identity.js +141 -141
  320. package/src/warp-pool/index.js +358 -358
  321. package/src/warp-pool/keys.js +29 -29
  322. package/src/warp-pool/nodes.js +95 -95
  323. package/src/warp-pool/ports.js +160 -160
  324. package/src/warp-pool/register.js +113 -113
  325. package/src/warp-pool/report.js +232 -232
  326. package/src/warp-pool/revive.js +168 -168
  327. package/src/warp-pool/rotate.js +214 -214
  328. package/src/warp-pool/strategy.js +43 -43
  329. package/src/warp-pool/verdict.js +69 -69
  330. 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";