mslxdff 0.1.231 → 0.1.232
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.
- package/bin/mslxdff.js +3 -3
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +145 -145
- package/src/chat-pipeline/policy.js +84 -84
- package/src/cli/commands/group.js +266 -266
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +279 -279
- package/src/daemon.js +246 -246
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +225 -225
- package/src/peers.js +345 -345
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +203 -203
- package/src/providers/qoder/index.js +249 -249
- package/src/providers/qoder/sse.js +145 -145
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/const.js +202 -202
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/auth.js +16 -0
- package/src/providers/workbuddy/chat.js +298 -264
- package/src/providers/workbuddy/client-headers.js +148 -0
- package/src/providers/workbuddy/desktop-info.js +17 -1
- package/src/providers/workbuddy/models.js +8 -20
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +156 -156
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +347 -347
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/server.js +85 -85
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +355 -355
- package/docs/cli_help_mini.md +0 -148
- package/src/providers/AGENTS.md +0 -49
- 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
|
+
}
|