mslxdff 0.1.231 → 0.1.233

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 +264 -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/index.js +155 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -1,285 +1,285 @@
1
- // raccoon 积分:余额五分项查询 + 每日积分结算 + 登录奖励领取(幂等,判据只认账单)。
2
- // 这家是**积分制**,四种积分来源的性质(2026-10-21 账单流水实测,此前 2026-10-10 的结论已被上游改版推翻):
3
- // · 新人注册礼包 3000 —— 注册时服务端自动发,**无端点可领**
4
- // · 每日积分 500 —— 服务端按天挂账,**当天第一次访问 `/office/v3/setting_info` 才入账**
5
- // (惰性结算,不是签到;入账即有效期到 T+6 的 23:59:59,即官方文案的「登录后延长至 7 天」)
6
- // · 桌面端登录奖励 3000 —— **每号一次性**,走 `POST /desktop/v1/login/points/grant`,唯一「领取」式接口
7
- // · 充值加油包 —— 用户自购
8
- // ⚠ 2026-10-10 的旧结论「每日 300、当天 23:59:59 清零、无端点可领」**已过时**:现为 500 分、有效期 7 天、
9
- // 有明确触发端点。别再照旧注释写代码。
10
- // ⚠ 上游对「已经领过」的重复请求**照样回 code:0**,但积分一分不到账 —— 所以 `code:0` 绝不能当成功凭据,
11
- // 必须用账单流水裁决(见 claimRaccoonLoginReward / claimRaccoonDailyPoints)。谎报成功会让 daemon/CLI 输出假到账,比报错更坏。
12
- import {
13
- RACCOON_BILLS_PAGE_LIMIT,
14
- RACCOON_DAILY_GRANT_EVENT_NAME,
15
- RACCOON_LOGIN_REWARD_EVENT_NAME,
16
- RACCOON_LOGIN_REWARD_POINTS,
17
- RACCOON_REQUEST_TIMEOUT_MS,
18
- raccoonBalanceUrl,
19
- raccoonBillsUrl,
20
- raccoonLoginGrantUrl,
21
- raccoonOfficeSettingUrl,
22
- } from "./const.js";
23
- import { raccoonAuthHeaders } from "./headers.js";
24
- import { confirmRaccoonSession } from "./login.js";
25
- import { parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
26
-
27
- const num = (v) => {
28
- if (typeof v === "number" && Number.isFinite(v)) return v;
29
- if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim())) return Number(v.trim());
30
- return undefined;
31
- };
32
-
33
- /** 上游「新会话未确认」闸门码:先 POST /iam/confirm 再重试一次(与 chat.js 同款兜底)。 */
34
- const IAM_CONFIRM_REQUIRED = 200901;
35
-
36
- async function request(url, { credential, fetchImpl = fetch, env = process.env, method = "GET", body, timeoutMs = RACCOON_REQUEST_TIMEOUT_MS } = {}) {
37
- const once = async () => {
38
- const res = await fetchImpl(url, {
39
- method,
40
- headers: raccoonAuthHeaders(credential, { env }),
41
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
42
- signal: AbortSignal.timeout(timeoutMs),
43
- });
44
- let parsed;
45
- try {
46
- parsed = await res.json();
47
- } catch {
48
- throw new Error(`raccoon: 响应不是 JSON(HTTP ${res.status})`);
49
- }
50
- return { res, envelope: parseRaccoonEnvelope(parsed, res.status) };
51
- };
52
- let { res, envelope } = await once();
53
- // 未确认会话闸门:确认一次后重试(老号 / CLI 直连都可能撞上,登录时已确认的号不会走到这)
54
- if (res.status === 400 && Number(envelope.code) === IAM_CONFIRM_REQUIRED) {
55
- if (await confirmRaccoonSession(credential, { fetchImpl, env })) {
56
- ({ res, envelope } = await once());
57
- }
58
- }
59
- return envelope;
60
- }
61
-
62
- /** 余额五分项(缺项即省略,合计优先取上游的 available_points)。 */
63
- export function parseRaccoonBalance(data) {
64
- const src = typeof data === "object" && data !== null ? data : {};
65
- const available = num(src.available_points);
66
- const reward = num(src.reward_points);
67
- const daily = num(src.daily_points);
68
- const topup = num(src.topup_points);
69
- const monthly = num(src.monthly_points);
70
- const items = [];
71
- if (reward !== undefined) items.push({ key: "reward", label: "奖励积分", value: reward });
72
- if (daily !== undefined) items.push({ key: "daily", label: "每日积分", value: daily });
73
- if (monthly !== undefined) items.push({ key: "monthly", label: "月度积分", value: monthly });
74
- if (topup !== undefined) items.push({ key: "topup", label: "充值积分", value: topup });
75
- const total = available !== undefined ? available : items.reduce((s, i) => s + i.value, 0);
76
- return { total, items };
77
- }
78
-
79
- export async function fetchRaccoonBalance(opts = {}) {
80
- const envelope = await request(raccoonBalanceUrl(), opts);
81
- if (envelope.code !== 0) {
82
- const err = new Error(`raccoon: 查询积分失败:${raccoonEnvelopeText(envelope)}`);
83
- err.code = envelope.code;
84
- err.status = envelope.status;
85
- throw err;
86
- }
87
- return parseRaccoonBalance(envelope.data);
88
- }
89
-
90
- /** 账单流水数组(上游把 list/items 两种形状都回过,容错取)。 */
91
- function billsRows(data) {
92
- const list = Array.isArray(data?.list) ? data.list : Array.isArray(data?.items) ? data.items : Array.isArray(data) ? data : null;
93
- return list === null ? undefined : list.filter((x) => typeof x === "object" && x !== null);
94
- }
95
-
96
- /** 上游的时间字段可能是秒/毫秒数字或 ISO 串,统一成毫秒。 */
97
- function billTimeMs(item) {
98
- const at = item.created_at ?? item.createdAt ?? item.time;
99
- if (typeof at === "number" && Number.isFinite(at)) return at > 1e12 ? at : at * 1000;
100
- if (typeof at === "string") {
101
- const ms = Date.parse(at);
102
- if (Number.isFinite(ms)) return ms;
103
- }
104
- return undefined;
105
- }
106
-
107
- /**
108
- * 从账单里找「这个号历史上领过登录奖励没有」,返回 `{ points, at }` 或 undefined。
109
- * ⚠ 判据是**全历史**,不看日期窗口:登录奖励是每号一次性,用「今天」当窗口会让已领的号每天重新发请求,
110
- * 还会把"昨天领的"判成"没领过"。注册礼包同样是 `reward_grant`,所以 `event_name` 必须逐字比对。
111
- */
112
- export function findLoginRewardInBills(data) {
113
- const rows = billsRows(data) || [];
114
- for (const item of rows) {
115
- if (item.biz_type !== "reward_grant") continue;
116
- if (item.event_name !== RACCOON_LOGIN_REWARD_EVENT_NAME) continue;
117
- const at = billTimeMs(item);
118
- return { points: num(item.points) ?? RACCOON_LOGIN_REWARD_POINTS, ...(at === undefined ? {} : { at }) };
119
- }
120
- return undefined;
121
- }
122
-
123
- /** 拉一次账单流水;网络/解析任何失败都返回 undefined(判定降级,不抛)。 */
124
- async function readBills(opts) {
125
- try {
126
- const envelope = await request(raccoonBillsUrl(), opts);
127
- return envelope?.code === 0 ? billsRows(envelope.data) : undefined;
128
- } catch {
129
- return undefined;
130
- }
131
- }
132
-
133
- /**
134
- * 领取「桌面端登录奖励」(每号一次性)。返回三种非抛错结果之一:
135
- * - `{ claimed: true, points }` 本次真到账(账单里出现了登录奖励那笔)
136
- * - `{ claimed: false, alreadyClaimed: true, points }` 历史已领(账单查到,或上游回 `granted:false`)
137
- * - `{ claimed: false, phantom: true, points: 0 }` 上游回了 code:0 但账单没有这笔 —— 虚回,如实报未到账
138
- * - `{ claimed: false, unverified: true, points: 0 }` 账单查不动,无法确认到账(不猜、不谎报)
139
- * grant 请求本身失败(业务码非 0)且账单也查不到已领 → 抛错(如实透出)。
140
- */
141
- export async function claimRaccoonLoginReward({ credential, fetchImpl = fetch, env = process.env, timeoutMs } = {}) {
142
- const opts = { credential, fetchImpl, env, timeoutMs };
143
-
144
- const before = await readBills(opts);
145
- const already = before === undefined ? undefined : findLoginRewardInBills(before);
146
- if (already) return { claimed: false, alreadyClaimed: true, points: already.points };
147
-
148
- let envelope;
149
- try {
150
- envelope = await request(raccoonLoginGrantUrl(), { ...opts, method: "POST", body: {} });
151
- } catch (error) {
152
- envelope = { code: -1, message: error?.message ?? String(error), status: 0, data: undefined };
153
- }
154
-
155
- // 上游用 `data.granted` 直接回答「这次到底发没发」—— 这是比账单更权威的判据,优先采信。
156
- // `granted === false` ⇒ 本号此前已发过(一次性礼包),**不是「未到账」**。
157
- // 为什么不能只靠账单:重度使用的号(如 daemon 正在跑的那个)账单会被「对话消耗」刷满,
158
- // 分页窗口里根本翻不到那条陈年 reward_grant —— 此时只信账单就会把「早就领过」
159
- // 误报成「上游回执成功但账单查不到 → 未到账」(实测踩到,教训)。
160
- // 官方前端同款判据:granted 为真才弹「积分已到账」,为假就静默(见 app.asar grantDesktopLoginPoints 调用点)。
161
- if (envelope?.code === 0 && envelope.data?.granted === false) {
162
- return { claimed: false, alreadyClaimed: true, points: RACCOON_LOGIN_REWARD_POINTS };
163
- }
164
-
165
- // 未给 granted 判据(如上游老版本/测试桩)时回退账单复查 —— 到账凭据仍以账单为准。
166
- const after = await readBills(opts);
167
- const landed = after === undefined ? undefined : findLoginRewardInBills(after);
168
- if (landed) {
169
- const points = num(envelope?.data?.points) ?? num(envelope?.data?.popup?.points) ?? landed.points ?? RACCOON_LOGIN_REWARD_POINTS;
170
- return { claimed: true, points };
171
- }
172
- if (after === undefined) return { claimed: false, unverified: true, points: 0 };
173
- if (envelope?.code === 0) return { claimed: false, phantom: true, points: 0 };
174
-
175
- const err = new Error(`raccoon: 领取登录积分失败:${raccoonEnvelopeText(envelope, "上游未返回原因")}`);
176
- err.code = envelope.code;
177
- err.status = envelope.status;
178
- throw err;
179
- }
180
-
181
- /**
182
- * claimRaccoonLoginReward 的结果 → 统一状态字。daemon 与 CLI 共用,避免两处各写一份分支后漂移。
183
- * claimed=真到账 · already=一次性礼包此前已领过(正常态) · phantom=上游回执成功但账单没这笔 ·
184
- * unverified=账单查不动,不下结论 · error=真失败。
185
- */
186
- export function raccoonClaimStatus(r) {
187
- if (r?.claimed) return "claimed";
188
- if (r?.alreadyClaimed) return "already";
189
- if (r?.phantom) return "phantom";
190
- if (r?.unverified) return "unverified";
191
- return "error";
192
- }
193
-
194
- // —— 每日积分(惰性结算,每日可重复触发;与上面的一次性登录奖励是两件事) ——
195
-
196
- /** 从账单里找「今天那笔每日积分发放」,返回 `{ points, at, expireAt }` 或 undefined。 */
197
- export function findDailyGrantInBills(data, now = Date.now()) {
198
- const rows = billsRows(data) || [];
199
- const today = localDateKey(now);
200
- for (const item of rows) {
201
- if (item.biz_type !== "daily_grant") continue;
202
- if (item.event_name && item.event_name !== RACCOON_DAILY_GRANT_EVENT_NAME) continue;
203
- // 归属日以 grant_at 为准(上游把它固定成当天 00:00),缺失时回落到 created_at
204
- const at = billTimeMs({ created_at: item.grant_at ?? item.created_at });
205
- if (at === undefined || localDateKey(at) !== today) continue;
206
- const expireAt = billTimeMs({ created_at: item.expire_at });
207
- return {
208
- points: num(item.points) ?? 0,
209
- at,
210
- ...(expireAt === undefined ? {} : { expireAt }),
211
- };
212
- }
213
- return undefined;
214
- }
215
-
216
- /** 本地日期键 YYYY-MM-DD —— 上游的日界按 +08:00 切,用本机时区判同一天即可。 */
217
- function localDateKey(ms) {
218
- const d = new Date(ms);
219
- if (!Number.isFinite(d.getTime())) return "";
220
- const p = (n) => String(n).padStart(2, "0");
221
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
222
- }
223
-
224
- /** 拉当天账单(限量 200:够覆盖一天的重度对话消耗)。失败返回 undefined(判定降级,不抛)。 */
225
- async function readBillsWide(opts) {
226
- try {
227
- const envelope = await request(raccoonBillsUrl(RACCOON_BILLS_PAGE_LIMIT), opts);
228
- return envelope?.code === 0 ? billsRows(envelope.data) : undefined;
229
- } catch {
230
- return undefined;
231
- }
232
- }
233
-
234
- /**
235
- * 结算当日每日积分(可每天重复调用,上游幂等)。
236
- *
237
- * 机制:`GET /api/web/office/v3/setting_info` 是服务端的惰性结算钩子 ——
238
- * 当天第一次访问它,服务端才把当天的每日积分真正挂账,并把有效期延到 T+6。
239
- * 不调用的话,`balance.daily_points` 一直是 0、账单里也压根没有 `daily_grant`(2026-10-21 实测)。
240
- *
241
- * 返回四种非抛错结果之一:
242
- * - `{ claimed: true, points, expireAt }` 本次真结算入账(账单里出现了今天那笔)
243
- * - `{ claimed: false, alreadyToday: true, points, expireAt }` 今天已经结算过(正常态,不是失败)
244
- * - `{ claimed: false, unverified: true }` 账单查不动,无法确认(不猜、不谎报)
245
- * - 结算请求本身失败(业务码非 0 / 网络错误)→ 抛错(如实透出)
246
- */
247
- export async function claimRaccoonDailyPoints({ credential, fetchImpl = fetch, env = process.env, timeoutMs, now = Date.now() } = {}) {
248
- const opts = { credential, fetchImpl, env, timeoutMs };
249
-
250
- // 先查账单:今天已经结算过就别再打结算端点(省一次请求,也让日志含义更准确)
251
- const before = await readBillsWide(opts);
252
- if (before !== undefined) {
253
- const hit = findDailyGrantInBills(before, now);
254
- if (hit) return { claimed: false, alreadyToday: true, points: hit.points, ...(hit.expireAt === undefined ? {} : { expireAt: hit.expireAt }) };
255
- }
256
-
257
- const envelope = await request(raccoonOfficeSettingUrl(), opts);
258
- if (envelope?.code !== 0) {
259
- const err = new Error(`raccoon: 每日积分结算失败:${raccoonEnvelopeText(envelope, "上游未返回原因")}`);
260
- err.code = envelope?.code;
261
- err.status = envelope?.status;
262
- throw err;
263
- }
264
-
265
- // 到账凭据只认账单:上游对已结算的重复请求同样回 code:0,回执不足以证明「这次发了」
266
- const after = await readBillsWide(opts);
267
- if (after === undefined) return { claimed: false, unverified: true, points: 0 };
268
- const landed = findDailyGrantInBills(after, now);
269
- if (!landed) {
270
- // 连打两次 code:0 都没在账单里看到今天这笔 → 上游改了结算规则或账单口径,如实说未确认,别谎报
271
- return { claimed: false, unverified: true, points: 0 };
272
- }
273
- return { claimed: true, points: landed.points, ...(landed.expireAt === undefined ? {} : { expireAt: landed.expireAt }) };
274
- }
275
-
276
- /**
277
- * claimRaccoonDailyPoints 的结果 → 统一状态字(daemon 与 CLI 共用)。
278
- * claimed=本次真入账 · already=今天已结算过(正常态) · unverified=账单查不动/没看到这笔,不下结论 · error=真失败。
279
- */
280
- export function raccoonDailyStatus(r) {
281
- if (r?.claimed) return "claimed";
282
- if (r?.alreadyToday) return "already";
283
- if (r?.unverified) return "unverified";
284
- return "error";
285
- }
1
+ // raccoon 积分:余额五分项查询 + 每日积分结算 + 登录奖励领取(幂等,判据只认账单)。
2
+ // 这家是**积分制**,四种积分来源的性质(2026-10-21 账单流水实测,此前 2026-10-10 的结论已被上游改版推翻):
3
+ // · 新人注册礼包 3000 —— 注册时服务端自动发,**无端点可领**
4
+ // · 每日积分 500 —— 服务端按天挂账,**当天第一次访问 `/office/v3/setting_info` 才入账**
5
+ // (惰性结算,不是签到;入账即有效期到 T+6 的 23:59:59,即官方文案的「登录后延长至 7 天」)
6
+ // · 桌面端登录奖励 3000 —— **每号一次性**,走 `POST /desktop/v1/login/points/grant`,唯一「领取」式接口
7
+ // · 充值加油包 —— 用户自购
8
+ // ⚠ 2026-10-10 的旧结论「每日 300、当天 23:59:59 清零、无端点可领」**已过时**:现为 500 分、有效期 7 天、
9
+ // 有明确触发端点。别再照旧注释写代码。
10
+ // ⚠ 上游对「已经领过」的重复请求**照样回 code:0**,但积分一分不到账 —— 所以 `code:0` 绝不能当成功凭据,
11
+ // 必须用账单流水裁决(见 claimRaccoonLoginReward / claimRaccoonDailyPoints)。谎报成功会让 daemon/CLI 输出假到账,比报错更坏。
12
+ import {
13
+ RACCOON_BILLS_PAGE_LIMIT,
14
+ RACCOON_DAILY_GRANT_EVENT_NAME,
15
+ RACCOON_LOGIN_REWARD_EVENT_NAME,
16
+ RACCOON_LOGIN_REWARD_POINTS,
17
+ RACCOON_REQUEST_TIMEOUT_MS,
18
+ raccoonBalanceUrl,
19
+ raccoonBillsUrl,
20
+ raccoonLoginGrantUrl,
21
+ raccoonOfficeSettingUrl,
22
+ } from "./const.js";
23
+ import { raccoonAuthHeaders } from "./headers.js";
24
+ import { confirmRaccoonSession } from "./login.js";
25
+ import { parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
26
+
27
+ const num = (v) => {
28
+ if (typeof v === "number" && Number.isFinite(v)) return v;
29
+ if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim())) return Number(v.trim());
30
+ return undefined;
31
+ };
32
+
33
+ /** 上游「新会话未确认」闸门码:先 POST /iam/confirm 再重试一次(与 chat.js 同款兜底)。 */
34
+ const IAM_CONFIRM_REQUIRED = 200901;
35
+
36
+ async function request(url, { credential, fetchImpl = fetch, env = process.env, method = "GET", body, timeoutMs = RACCOON_REQUEST_TIMEOUT_MS } = {}) {
37
+ const once = async () => {
38
+ const res = await fetchImpl(url, {
39
+ method,
40
+ headers: raccoonAuthHeaders(credential, { env }),
41
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
42
+ signal: AbortSignal.timeout(timeoutMs),
43
+ });
44
+ let parsed;
45
+ try {
46
+ parsed = await res.json();
47
+ } catch {
48
+ throw new Error(`raccoon: 响应不是 JSON(HTTP ${res.status})`);
49
+ }
50
+ return { res, envelope: parseRaccoonEnvelope(parsed, res.status) };
51
+ };
52
+ let { res, envelope } = await once();
53
+ // 未确认会话闸门:确认一次后重试(老号 / CLI 直连都可能撞上,登录时已确认的号不会走到这)
54
+ if (res.status === 400 && Number(envelope.code) === IAM_CONFIRM_REQUIRED) {
55
+ if (await confirmRaccoonSession(credential, { fetchImpl, env })) {
56
+ ({ res, envelope } = await once());
57
+ }
58
+ }
59
+ return envelope;
60
+ }
61
+
62
+ /** 余额五分项(缺项即省略,合计优先取上游的 available_points)。 */
63
+ export function parseRaccoonBalance(data) {
64
+ const src = typeof data === "object" && data !== null ? data : {};
65
+ const available = num(src.available_points);
66
+ const reward = num(src.reward_points);
67
+ const daily = num(src.daily_points);
68
+ const topup = num(src.topup_points);
69
+ const monthly = num(src.monthly_points);
70
+ const items = [];
71
+ if (reward !== undefined) items.push({ key: "reward", label: "奖励积分", value: reward });
72
+ if (daily !== undefined) items.push({ key: "daily", label: "每日积分", value: daily });
73
+ if (monthly !== undefined) items.push({ key: "monthly", label: "月度积分", value: monthly });
74
+ if (topup !== undefined) items.push({ key: "topup", label: "充值积分", value: topup });
75
+ const total = available !== undefined ? available : items.reduce((s, i) => s + i.value, 0);
76
+ return { total, items };
77
+ }
78
+
79
+ export async function fetchRaccoonBalance(opts = {}) {
80
+ const envelope = await request(raccoonBalanceUrl(), opts);
81
+ if (envelope.code !== 0) {
82
+ const err = new Error(`raccoon: 查询积分失败:${raccoonEnvelopeText(envelope)}`);
83
+ err.code = envelope.code;
84
+ err.status = envelope.status;
85
+ throw err;
86
+ }
87
+ return parseRaccoonBalance(envelope.data);
88
+ }
89
+
90
+ /** 账单流水数组(上游把 list/items 两种形状都回过,容错取)。 */
91
+ function billsRows(data) {
92
+ const list = Array.isArray(data?.list) ? data.list : Array.isArray(data?.items) ? data.items : Array.isArray(data) ? data : null;
93
+ return list === null ? undefined : list.filter((x) => typeof x === "object" && x !== null);
94
+ }
95
+
96
+ /** 上游的时间字段可能是秒/毫秒数字或 ISO 串,统一成毫秒。 */
97
+ function billTimeMs(item) {
98
+ const at = item.created_at ?? item.createdAt ?? item.time;
99
+ if (typeof at === "number" && Number.isFinite(at)) return at > 1e12 ? at : at * 1000;
100
+ if (typeof at === "string") {
101
+ const ms = Date.parse(at);
102
+ if (Number.isFinite(ms)) return ms;
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ /**
108
+ * 从账单里找「这个号历史上领过登录奖励没有」,返回 `{ points, at }` 或 undefined。
109
+ * ⚠ 判据是**全历史**,不看日期窗口:登录奖励是每号一次性,用「今天」当窗口会让已领的号每天重新发请求,
110
+ * 还会把"昨天领的"判成"没领过"。注册礼包同样是 `reward_grant`,所以 `event_name` 必须逐字比对。
111
+ */
112
+ export function findLoginRewardInBills(data) {
113
+ const rows = billsRows(data) || [];
114
+ for (const item of rows) {
115
+ if (item.biz_type !== "reward_grant") continue;
116
+ if (item.event_name !== RACCOON_LOGIN_REWARD_EVENT_NAME) continue;
117
+ const at = billTimeMs(item);
118
+ return { points: num(item.points) ?? RACCOON_LOGIN_REWARD_POINTS, ...(at === undefined ? {} : { at }) };
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ /** 拉一次账单流水;网络/解析任何失败都返回 undefined(判定降级,不抛)。 */
124
+ async function readBills(opts) {
125
+ try {
126
+ const envelope = await request(raccoonBillsUrl(), opts);
127
+ return envelope?.code === 0 ? billsRows(envelope.data) : undefined;
128
+ } catch {
129
+ return undefined;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * 领取「桌面端登录奖励」(每号一次性)。返回三种非抛错结果之一:
135
+ * - `{ claimed: true, points }` 本次真到账(账单里出现了登录奖励那笔)
136
+ * - `{ claimed: false, alreadyClaimed: true, points }` 历史已领(账单查到,或上游回 `granted:false`)
137
+ * - `{ claimed: false, phantom: true, points: 0 }` 上游回了 code:0 但账单没有这笔 —— 虚回,如实报未到账
138
+ * - `{ claimed: false, unverified: true, points: 0 }` 账单查不动,无法确认到账(不猜、不谎报)
139
+ * grant 请求本身失败(业务码非 0)且账单也查不到已领 → 抛错(如实透出)。
140
+ */
141
+ export async function claimRaccoonLoginReward({ credential, fetchImpl = fetch, env = process.env, timeoutMs } = {}) {
142
+ const opts = { credential, fetchImpl, env, timeoutMs };
143
+
144
+ const before = await readBills(opts);
145
+ const already = before === undefined ? undefined : findLoginRewardInBills(before);
146
+ if (already) return { claimed: false, alreadyClaimed: true, points: already.points };
147
+
148
+ let envelope;
149
+ try {
150
+ envelope = await request(raccoonLoginGrantUrl(), { ...opts, method: "POST", body: {} });
151
+ } catch (error) {
152
+ envelope = { code: -1, message: error?.message ?? String(error), status: 0, data: undefined };
153
+ }
154
+
155
+ // 上游用 `data.granted` 直接回答「这次到底发没发」—— 这是比账单更权威的判据,优先采信。
156
+ // `granted === false` ⇒ 本号此前已发过(一次性礼包),**不是「未到账」**。
157
+ // 为什么不能只靠账单:重度使用的号(如 daemon 正在跑的那个)账单会被「对话消耗」刷满,
158
+ // 分页窗口里根本翻不到那条陈年 reward_grant —— 此时只信账单就会把「早就领过」
159
+ // 误报成「上游回执成功但账单查不到 → 未到账」(实测踩到,教训)。
160
+ // 官方前端同款判据:granted 为真才弹「积分已到账」,为假就静默(见 app.asar grantDesktopLoginPoints 调用点)。
161
+ if (envelope?.code === 0 && envelope.data?.granted === false) {
162
+ return { claimed: false, alreadyClaimed: true, points: RACCOON_LOGIN_REWARD_POINTS };
163
+ }
164
+
165
+ // 未给 granted 判据(如上游老版本/测试桩)时回退账单复查 —— 到账凭据仍以账单为准。
166
+ const after = await readBills(opts);
167
+ const landed = after === undefined ? undefined : findLoginRewardInBills(after);
168
+ if (landed) {
169
+ const points = num(envelope?.data?.points) ?? num(envelope?.data?.popup?.points) ?? landed.points ?? RACCOON_LOGIN_REWARD_POINTS;
170
+ return { claimed: true, points };
171
+ }
172
+ if (after === undefined) return { claimed: false, unverified: true, points: 0 };
173
+ if (envelope?.code === 0) return { claimed: false, phantom: true, points: 0 };
174
+
175
+ const err = new Error(`raccoon: 领取登录积分失败:${raccoonEnvelopeText(envelope, "上游未返回原因")}`);
176
+ err.code = envelope.code;
177
+ err.status = envelope.status;
178
+ throw err;
179
+ }
180
+
181
+ /**
182
+ * claimRaccoonLoginReward 的结果 → 统一状态字。daemon 与 CLI 共用,避免两处各写一份分支后漂移。
183
+ * claimed=真到账 · already=一次性礼包此前已领过(正常态) · phantom=上游回执成功但账单没这笔 ·
184
+ * unverified=账单查不动,不下结论 · error=真失败。
185
+ */
186
+ export function raccoonClaimStatus(r) {
187
+ if (r?.claimed) return "claimed";
188
+ if (r?.alreadyClaimed) return "already";
189
+ if (r?.phantom) return "phantom";
190
+ if (r?.unverified) return "unverified";
191
+ return "error";
192
+ }
193
+
194
+ // —— 每日积分(惰性结算,每日可重复触发;与上面的一次性登录奖励是两件事) ——
195
+
196
+ /** 从账单里找「今天那笔每日积分发放」,返回 `{ points, at, expireAt }` 或 undefined。 */
197
+ export function findDailyGrantInBills(data, now = Date.now()) {
198
+ const rows = billsRows(data) || [];
199
+ const today = localDateKey(now);
200
+ for (const item of rows) {
201
+ if (item.biz_type !== "daily_grant") continue;
202
+ if (item.event_name && item.event_name !== RACCOON_DAILY_GRANT_EVENT_NAME) continue;
203
+ // 归属日以 grant_at 为准(上游把它固定成当天 00:00),缺失时回落到 created_at
204
+ const at = billTimeMs({ created_at: item.grant_at ?? item.created_at });
205
+ if (at === undefined || localDateKey(at) !== today) continue;
206
+ const expireAt = billTimeMs({ created_at: item.expire_at });
207
+ return {
208
+ points: num(item.points) ?? 0,
209
+ at,
210
+ ...(expireAt === undefined ? {} : { expireAt }),
211
+ };
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ /** 本地日期键 YYYY-MM-DD —— 上游的日界按 +08:00 切,用本机时区判同一天即可。 */
217
+ function localDateKey(ms) {
218
+ const d = new Date(ms);
219
+ if (!Number.isFinite(d.getTime())) return "";
220
+ const p = (n) => String(n).padStart(2, "0");
221
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
222
+ }
223
+
224
+ /** 拉当天账单(限量 200:够覆盖一天的重度对话消耗)。失败返回 undefined(判定降级,不抛)。 */
225
+ async function readBillsWide(opts) {
226
+ try {
227
+ const envelope = await request(raccoonBillsUrl(RACCOON_BILLS_PAGE_LIMIT), opts);
228
+ return envelope?.code === 0 ? billsRows(envelope.data) : undefined;
229
+ } catch {
230
+ return undefined;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 结算当日每日积分(可每天重复调用,上游幂等)。
236
+ *
237
+ * 机制:`GET /api/web/office/v3/setting_info` 是服务端的惰性结算钩子 ——
238
+ * 当天第一次访问它,服务端才把当天的每日积分真正挂账,并把有效期延到 T+6。
239
+ * 不调用的话,`balance.daily_points` 一直是 0、账单里也压根没有 `daily_grant`(2026-10-21 实测)。
240
+ *
241
+ * 返回四种非抛错结果之一:
242
+ * - `{ claimed: true, points, expireAt }` 本次真结算入账(账单里出现了今天那笔)
243
+ * - `{ claimed: false, alreadyToday: true, points, expireAt }` 今天已经结算过(正常态,不是失败)
244
+ * - `{ claimed: false, unverified: true }` 账单查不动,无法确认(不猜、不谎报)
245
+ * - 结算请求本身失败(业务码非 0 / 网络错误)→ 抛错(如实透出)
246
+ */
247
+ export async function claimRaccoonDailyPoints({ credential, fetchImpl = fetch, env = process.env, timeoutMs, now = Date.now() } = {}) {
248
+ const opts = { credential, fetchImpl, env, timeoutMs };
249
+
250
+ // 先查账单:今天已经结算过就别再打结算端点(省一次请求,也让日志含义更准确)
251
+ const before = await readBillsWide(opts);
252
+ if (before !== undefined) {
253
+ const hit = findDailyGrantInBills(before, now);
254
+ if (hit) return { claimed: false, alreadyToday: true, points: hit.points, ...(hit.expireAt === undefined ? {} : { expireAt: hit.expireAt }) };
255
+ }
256
+
257
+ const envelope = await request(raccoonOfficeSettingUrl(), opts);
258
+ if (envelope?.code !== 0) {
259
+ const err = new Error(`raccoon: 每日积分结算失败:${raccoonEnvelopeText(envelope, "上游未返回原因")}`);
260
+ err.code = envelope?.code;
261
+ err.status = envelope?.status;
262
+ throw err;
263
+ }
264
+
265
+ // 到账凭据只认账单:上游对已结算的重复请求同样回 code:0,回执不足以证明「这次发了」
266
+ const after = await readBillsWide(opts);
267
+ if (after === undefined) return { claimed: false, unverified: true, points: 0 };
268
+ const landed = findDailyGrantInBills(after, now);
269
+ if (!landed) {
270
+ // 连打两次 code:0 都没在账单里看到今天这笔 → 上游改了结算规则或账单口径,如实说未确认,别谎报
271
+ return { claimed: false, unverified: true, points: 0 };
272
+ }
273
+ return { claimed: true, points: landed.points, ...(landed.expireAt === undefined ? {} : { expireAt: landed.expireAt }) };
274
+ }
275
+
276
+ /**
277
+ * claimRaccoonDailyPoints 的结果 → 统一状态字(daemon 与 CLI 共用)。
278
+ * claimed=本次真入账 · already=今天已结算过(正常态) · unverified=账单查不动/没看到这笔,不下结论 · error=真失败。
279
+ */
280
+ export function raccoonDailyStatus(r) {
281
+ if (r?.claimed) return "claimed";
282
+ if (r?.alreadyToday) return "already";
283
+ if (r?.unverified) return "unverified";
284
+ return "error";
285
+ }
@@ -5,7 +5,7 @@ import { envInt } from "../base.js";
5
5
  import { createKeyRing } from "../keyring.js";
6
6
  import { loadProviderKeys } from "../../state.js";
7
7
  import { forwardRaccoonChat } from "./chat.js";
8
- import { buildRaccoonModelList, listRaccoonModels } from "./models.js";
8
+ import { buildRaccoonModelList, listRaccoonModels, mergeRaccoonCatalogIntoAllowlist } from "./models.js";
9
9
  import { applyRaccoonRefresh, listRaccoonAccountDocs, raccoonCredentialFromDoc, readRaccoonAccountDoc, syncRaccoonKeys, writeRaccoonAccountFile } from "./account-store.js";
10
10
  import { refreshRaccoonCredential } from "./login.js";
11
11
  import { isRaccoonExpiringSoon, raccoonTokenFingerprint } from "./auth.js";
@@ -178,12 +178,25 @@ export function createRaccoonProvider({
178
178
  return allCoolingResponse();
179
179
  }
180
180
 
181
+ let catalogMergeTried = false;
181
182
  async function listModels() {
182
183
  let token = ring.keys[0];
183
184
  // 内存号已不在文档里(重登换号)时先对齐,否则目录撞 401 后静默回落内置表,表现为「列表不是最新的」
184
185
  if (token && !accountFor(token)) token = (await healKeyFromDocs(token)) || token;
185
186
  if (!token) return [];
186
- const { models } = await listRaccoonModels({ credential: credentialFor(token), fetchImpl });
187
+ const { models, source } = await listRaccoonModels({ credential: credentialFor(token), fetchImpl });
188
+ // 上游目录是这家唯一真相(模型 id 全由上游定义、按账号下发):每进程首次成功拉到就只增不减
189
+ // 并入 allowlist,否则 dispatcher 会拿登录时播种的静态白名单把新上架模型(如 …-1017)挡掉。
190
+ // 只在 source=upstream 时做:回落兜底表时并入的仍是静态 id,没有意义还会白写盘。
191
+ if (source === "upstream" && !catalogMergeTried) {
192
+ catalogMergeTried = true;
193
+ try {
194
+ const { added } = mergeRaccoonCatalogIntoAllowlist(models, file ? { file } : {});
195
+ if (added.length) console.error(`[raccoon] 上游目录新模型并入白名单: ${added.join(", ")}`);
196
+ } catch (e) {
197
+ console.error(`[raccoon] 目录并入白名单失败(列表照常返回): ${String(e?.message || e).slice(0, 160)}`);
198
+ }
199
+ }
187
200
  return buildRaccoonModelList({ id, models });
188
201
  }
189
202