mslxdff 0.1.224 → 0.1.226

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.
@@ -80,7 +80,10 @@
80
80
  | raccoon 接入 | `-provider raccoon login` | Raccoon(商汤小浣熊)扫码登录:生成 `qrcode_code`→终端二维码 + `login/mp?code=` 链接→**用微信「扫一扫」扫**并确认(浏览器直接打开链接会 404,站点 SPA 无该路由)→轮询拿 token,落盘 `auths/raccoon-<uid>.json`+state(`keys` 一律以文档为准,续期后自愈)并把兜底 6 模型并入 allowlist;`raccoon/<modelId>` 前缀路由(**OpenAI 兼容网关**,**免验证码**);多号自动轮换:请求内撞积分不足/限流就当场换下一个号,**暂不支持手动指定用哪个号**;积分不足长冷 1h、限流短冷 30s;恒 local-only 不借出 |
81
81
  | raccoon 密码登录 | `-provider raccoon login --password` | Raccoon **手机号+密码登录**(多号免扫码):走官方同款 `POST /api/web/auth/v1/login_with_password`,**phone/password 须 AES-128-CFB 加密**(key 硬编码于前端,见 `crypto.js`;发明文会报「手机号或密码不正确」);TTY 下密码**关回显**(逐键 `*`),非 TTY 须 `--password=<密码>`;默认 `--nation 86`;失败给人话(「手机号或密码不正确」),回执回显手机号但**打码**、**不回显密码**;落盘/确认/领奖与扫码登录完全同路 |
82
82
  | raccoon 积分 | `-provider raccoon quota [--json]` / `checkin` | 逐号查积分五分项 / 领「桌面端登录奖励」——**这家没有每日签到**:每日 300 与注册礼包 3000 由服务端自动发(无端点,每日 300 当天 23:59:59 清零),唯一要领的登录奖励 3000 是**每号一次性**且 `login` 时已自动领掉,`checkin` 只是补领兜底;判据只认账单、不认上游 `code:0`(重复领会虚回成功);未登录给 login 指引;daemon 每日 09:00 兜底扫描未领过的号(`MSLXDFF_RACCOON_CHECKIN=0` 关,`_CHECKIN_HOUR` 改点) |
83
- | WorkBuddy 成长任务 | `-workbuddy growth [--json]` / `-wb growth` | 成长任务全自动(拉列表→参与→触发→领奖,串行 1.2s/1s,已领幂等跳过);可自动 `chat_5`/`automation_1`/`skill_1`/`Model_chat_GLM5.2`,需客户端任务标 MANUAL 不发包;daemon 每日 09:30 自动(`MSLXDFF_WORKBUDDY_GROWTH=0` 关,`_GROWTH_HOUR`/`_GROWTH_MODEL` 可配) |
83
+ | WorkBuddy 成长任务 | `-workbuddy growth [--json]` / `-wb growth` | 成长任务全自动(拉列表→参与→触发→领奖,串行 1.2s/1s,已领幂等跳过);三种触发通道分派——`growthEvent` 对话(`chat_5`/`automation_1`/`skill_1`/`Model_chat_GLM5.2`)、桌面指纹事件链(`RichMeow_Chat`/`Buddy_App`/`Buddy_App_QQ`/`create_canvas`/`template_5`/`playbook_prompt`)、专家真实对话回执(`expert_5`/`Expert_team_use_3`);**任务列表与领奖只在 `www.workbuddy.cn` 命中**,accept 前上游不下发 target 须 accept 后重读;仍 MANUAL 的 5 项(`Library_read`/`Expert_lighthouse`/`Hp_Appearance`/`WorkBuddy_Valorant`/`wb_wechat_oa_subscribe_task`)只认客户端真实行为,不空烧对话;daemon 每日 09:30 自动(`MSLXDFF_WORKBUDDY_GROWTH=0` 关,`_GROWTH_HOUR`/`_GROWTH_MODEL` 可配) |
84
+ | WorkBuddy 积分盘点 | `-provider workbuddy quota [--json]` / `credits` / `balance` | **只读**逐号列未领任务(含积分/进度/状态)、连登天数、可兑档位、抽奖次数、未领合计;不发任何写请求 |
85
+ | WorkBuddy 补领 | `-provider workbuddy checkin [--json]` / `claim` | 补领闭环:活跃上报点亮连登 → 兑换 7/14/28 档位 → 抽完全部次数 → 成长任务全量(含桌面/专家通道);输出到账项数/积分/跳过项/未计分告警 |
86
+ | WorkBuddy 连登 | daemon 每日随成长排程 | **签到不点亮连登**——`streak.days` 只认 `/v2/report` 的 `chat_request_send` 事件;不发则 7/14/28 档位积分与抽奖次数永远拿不到(2026-10-20 实测签到成功但 streak 恒 0) |
84
87
  | WorkBuddy 猫猫旅行 | `-workbuddy travel [--json]` / `-wb travel` | 无猫自动同意协议+领养(+300,门槛未达自动补一次对话解锁);到站领奖 / 空闲派出(location 4)/ 旅行中跳过 |
85
88
  | WorkBuddy 余额 | `-workbuddy balance [--json]` / `-wb balance` | 查多号余额(`total/dailyPacks/nextExpire`,TTL 5min) |
86
89
  | WorkBuddy 列表 | `-workbuddy list` / `-wb list` | 列账号(`uid/domain/enterpriseId`) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.224",
3
+ "version": "0.1.226",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,55 @@
1
+ // 转发阶段的上下文闸门编排(从 serial-trial.js 拆出,控单文件 ≤20KB 硬门)。
2
+ //
3
+ // 分工:context-guard.js 做纯判决(估算 token、比上限),本文件做**编排**——把「组员/宽带转发」
4
+ // 整段包起来,闸门一触发就收场,不再落到后面的换路逻辑。
5
+ //
6
+ // 为什么必须在这里停:闸门拦下**不是**上游失败。同一份大上下文换个目标(组员→宽带成员)照样撑爆,
7
+ // 只会白烧另一台机器的额度。故直接以标准 `context_length_exceeded` 收场,让客户端压缩后重试。
8
+ import { json } from "../routes/helpers.js";
9
+
10
+ /** 闸门产出的失败(finishContextGuard 与各 handler 共用同一个标记)。 */
11
+ export function isContextGuardErr(e) {
12
+ return Boolean(e && typeof e === "object" && e.contextGuard);
13
+ }
14
+
15
+ /**
16
+ * 收场:未 flush 时回标准错误体(客户端据 code 触发压缩);已 flush(流式已开始)交给既有出口写 in-band 帧。
17
+ */
18
+ export async function finishContextGuard({
19
+ res, body, lastErr, order, handlerCtx, evt, logCall, logError, mark, perf0, stages, done,
20
+ requested, useAuto, model, exhaustedLocal,
21
+ }) {
22
+ const g = lastErr?.contextGuard || {};
23
+ evt("context-guard", {
24
+ reqId: handlerCtx.reqId, model, estimated: g.estimated, limit: g.limit, over: g.over,
25
+ action: "stop-forwarding",
26
+ });
27
+ try { logError(model, 400, lastErr.message); } catch {}
28
+ if (!res?.headersSent) {
29
+ return json(res, 400, {
30
+ error: { type: "invalid_request_error", code: "context_length_exceeded", message: lastErr.message },
31
+ });
32
+ }
33
+ await exhaustedLocal({
34
+ res, body, lastErr, order,
35
+ handlerCtx: { ...handlerCtx, model, reqId: handlerCtx.reqId },
36
+ evt, logCall, mark, perf0, stages, done, requested, useAuto,
37
+ });
38
+ return { done: true };
39
+ }
40
+
41
+ /**
42
+ * 跑一段转发并在闸门触发时立即收场。
43
+ * @param {Function} run 真正发起转发的函数(应返回 {handled, lastErr})
44
+ * @param {object} ctx 收场所需的上下文(res/body/order/handlerCtx/evt/...)
45
+ * @returns {Promise<{done:boolean, lastErr?:object}>}
46
+ */
47
+ export async function forwardOrGuard(run, ctx) {
48
+ const r = await run();
49
+ if (r?.handled) return { done: true };
50
+ if (isContextGuardErr(r?.lastErr)) {
51
+ await finishContextGuard({ ...ctx, lastErr: r.lastErr });
52
+ return { done: true };
53
+ }
54
+ return { done: false, lastErr: r?.lastErr };
55
+ }
@@ -0,0 +1,101 @@
1
+ // 转发前上下文预检:本地额度用完要转给组员时,先把「上下文会不会撑爆对方」判掉。
2
+ //
3
+ // 为什么需要(用户定的口径):各家 free 模型的上下文窗口各不相同、大多在 200k 以下,而网关**没有**
4
+ // 一个可靠的上游窗口数据源 —— 靠模型自报会拿到 1M 这类不实的值(见 context-lookup 的 L4 兜底)。
5
+ // 于是本地出口还有额度时一切正常(本地模型窗口往往更大),一旦额度用完转给组员,同一份大上下文
6
+ // 在对方那里直接 `400 context_length_exceeded`。现网实证:errors.log 里 376 条超限,其中 274 条
7
+ // 来自 broadband(组员执行)、93 条来自 messages、仅 9 条 chat;模型集中在 deepseek-v4.1-flash-free(323)。
8
+ //
9
+ // 设计取舍:**不猜每个模型的确切窗口**,只设一个保守的统一上限(默认 200k)。超了就**不转发**,
10
+ // 让客户端按标准 `context_length_exceeded` 触发自己的压缩流程 —— 这正是「在转发之前触发压缩」。
11
+ // 宁可让客户端多压一次,也不要把一个大上下文白转给组员、烧掉对方额度再撞墙。
12
+ //
13
+ // 与 count-tokens 的关系:估算公式**同源**(../text-tokens.js),不另造第二个读数来源(ADR-0055)。
14
+ // 但计量对象不同:那边是 Anthropic 请求体(system/messages blocks),这边是**已翻译好的 chat body**
15
+ // (messages 数组 + tools),是真正要发给上游的那份,所以单独走一遍计数。
16
+
17
+ import { createTokenCounter } from "../text-tokens.js";
18
+
19
+ // 统一保守窗口。取值依据:现网 free 层主流 200k,且错误集中在 -free 模型。
20
+ // 未自报窗口的模型按这个值对外宣称(见 context-lookup 的 L4);转发前也按它判。
21
+ export const DEFAULT_FORWARD_MAX_CONTEXT_TOKENS = 200_000;
22
+
23
+ /** 读取转发前上限;`0`/负数 = 关闭预检(退回旧行为:无脑转发)。非法值按默认。 */
24
+ export function forwardMaxContextTokens(env = process.env) {
25
+ const raw = String(env.MSLXDFF_FORWARD_MAX_CONTEXT_TOKENS ?? "").trim();
26
+ if (raw === "") return DEFAULT_FORWARD_MAX_CONTEXT_TOKENS;
27
+ const n = Number(raw);
28
+ if (!Number.isFinite(n)) return DEFAULT_FORWARD_MAX_CONTEXT_TOKENS;
29
+ if (n <= 0) return 0;
30
+ const f = Math.floor(n);
31
+ // (0,1) 会被 floor 成 0 —— 那等于悄悄关掉闸门,按非法值回默认(同 context-lookup 的取舍)
32
+ return f > 0 ? f : DEFAULT_FORWARD_MAX_CONTEXT_TOKENS;
33
+ }
34
+
35
+ // 单条消息的文本面(role 名与 tool_call 参数也算:它们同样占上游上下文)
36
+ function addMessage(counter, m) {
37
+ if (!m || typeof m !== "object") return;
38
+ const c = m.content;
39
+ if (typeof c === "string") counter.add(c);
40
+ else if (Array.isArray(c)) {
41
+ for (const p of c) {
42
+ if (!p || typeof p !== "object") continue;
43
+ if (typeof p.text === "string") counter.add(p.text);
44
+ else if (p.type === "image") counter.add("x".repeat(4000)); // 图片按固定面值近似(不吃字符但占真实上下文)
45
+ }
46
+ } else if (c != null) counter.add(String(c));
47
+ if (Array.isArray(m.tool_calls)) {
48
+ for (const tc of m.tool_calls) {
49
+ counter.add(String(tc?.function?.name || ""));
50
+ counter.add(String(tc?.function?.arguments || ""));
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 估算**已翻译的 chat body** 的输入 token 数。
57
+ * 逐段喂进同一个计数器再一次性套公式(与 text-tokens 的累计口径一致:逐段各套会乘上「非空至少 1」下限)。
58
+ */
59
+ export function estimateChatBodyTokens(body = {}) {
60
+ const counter = createTokenCounter();
61
+ const messages = Array.isArray(body?.messages) ? body.messages : [];
62
+ for (const m of messages) addMessage(counter, m);
63
+ if (Array.isArray(body?.tools) && body.tools.length) {
64
+ try { counter.add(JSON.stringify(body.tools)); } catch { /* 循环引用等异常:不计工具面 */ }
65
+ }
66
+ // tools 的 JSON 里已含 name/description/schema;system 已在 messages 首条里
67
+ return counter.tokens();
68
+ }
69
+
70
+ // 同一次请求的 body 对象会在多个出站口被反复判(via-route → peer → broadband),
71
+ // 而大上下文扫描是 O(字符数)。按对象身份缓存,保证「每请求只算一次」(同 ADR-0055 决策 4 的口径)。
72
+ const estimateCache = new WeakMap();
73
+
74
+ function estimateOnce(body) {
75
+ if (!body || typeof body !== "object") return estimateChatBodyTokens(body);
76
+ const hit = estimateCache.get(body);
77
+ if (hit !== undefined) return hit;
78
+ const n = estimateChatBodyTokens(body);
79
+ estimateCache.set(body, n);
80
+ return n;
81
+ }
82
+
83
+ /**
84
+ * 转发前判决。返回 null = 放行;否则给出超限信息(调用方据此短路,**不转发**)。
85
+ * @param {object} body 已翻译的 chat body
86
+ * @param {{limit?:number}} opts limit 缺省读 env
87
+ * @returns {{estimated:number, limit:number, over:number}|null}
88
+ */
89
+ export function overForwardLimit(body, opts = {}) {
90
+ const limit = opts.limit === undefined ? forwardMaxContextTokens() : Number(opts.limit) || 0;
91
+ if (!limit || limit <= 0) return null;
92
+ const estimated = estimateOnce(body);
93
+ if (estimated <= limit) return null;
94
+ return { estimated, limit, over: estimated - limit };
95
+ }
96
+
97
+ /** 给客户端看的文案:必须含 `context_length_exceeded` 语义,客户端据此触发压缩。 */
98
+ export function contextGuardMessage({ estimated, limit } = {}) {
99
+ return `context_length_exceeded: 请求约 ${estimated ?? "?"} tokens,超过网关注入的转发上限 ${limit ?? "?"};`
100
+ + "本地节点额度已用尽,为避免把同一份上下文转发给组员后再次超限,请先运行 /compact 压缩上下文再重试。";
101
+ }
@@ -14,6 +14,7 @@ import { summarizeRequest, upstreamEcho } from "../model-trace.js";
14
14
  import { isEmptyTurnError } from "../routes/chat/relay-pipeline.js";
15
15
  import { emptyRetryCfg, emptyRaiseCap, withRaisedMaxTokens, withOutputFloor, emptyNudgeCfg, withEmptyNudge, computeNextDelay, emptyTurnBudgetMs, emptyTurnMinRaiseTo } from "./empty-turn.js";
16
16
  import { classifyTerminalUpstream, readVerdictText, terminalShortcircuitOff } from "./terminal-verdict.js";
17
+ import { forwardOrGuard } from "./context-forward.js";
17
18
 
18
19
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
19
20
 
@@ -253,21 +254,24 @@ export async function runSerialTrial(ctx, deps = {}) {
253
254
  }
254
255
  break;
255
256
  }
257
+ // 转发闸门(详见 context-forward.js):被拦即收场,不许换路重试——同一份大上下文换个目标照样撑爆。
258
+ const gc = { res, body, order, handlerCtx, evt, logCall, logError, mark, perf0, stages, done, requested, useAuto, model, exhaustedLocal };
256
259
  if (canForwardPeers) {
257
260
  if (!shouldUseGroupForModel(model)) {
258
261
  evt("group-skip", { reqId, model, reason: `${groupSkipReason(model)} (peer)` });
259
262
  } else {
260
- const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, attemptStartMs: performance.now(), stages, startedAt, plugins, res });
261
- if (pr.handled) return { done: true };
262
- if (pr.lastErr) lastErr = pr.lastErr;
263
+ const fr = await forwardOrGuard(() => peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, attemptStartMs: performance.now(), stages, startedAt, plugins, res }), gc);
264
+ if (fr.done) return fr;
265
+ lastErr = fr.lastErr ?? lastErr;
263
266
  }
264
267
  }
265
268
  if (groups) {
266
269
  if (!shouldUseGroupForModel(model)) {
267
270
  evt("group-skip", { reqId, model, reason: `${groupSkipReason(model)} (broadband)` });
268
271
  } else {
269
- const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, attemptStartMs: performance.now(), stages, res, startedAt, plugins });
270
- if (br.handled) return { done: true };
272
+ const fr = await forwardOrGuard(() => broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, attemptStartMs: performance.now(), stages, res, startedAt, plugins }), gc);
273
+ if (fr.done) return fr;
274
+ lastErr = fr.lastErr ?? lastErr;
271
275
  }
272
276
  }
273
277
  if (canFailover()) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` }); continue; }
@@ -76,6 +76,8 @@ export async function handleProvider(args) {
76
76
  if (await handleClineLogin(id, sub)) return true;
77
77
  const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
78
78
  if (await handleWorkbuddyLogin(id, sub, rest)) return true;
79
+ const { handleWorkbuddyQuota } = await import("./workbuddy-quota.js");
80
+ if (await handleWorkbuddyQuota(id, sub, rest)) return true;
79
81
  const { handleCodeartsLogin } = await import("./codearts-login.js");
80
82
  if (await handleCodeartsLogin(id, sub, rest)) return true;
81
83
  const { handleTraeworkLogin } = await import("./traework-login.js");
@@ -0,0 +1,154 @@
1
+ // mslxdff -provider workbuddy quota|checkin —— 查还漏领多少积分 / 手动补领
2
+ //
3
+ // 子命令:
4
+ // quota|credits|balance [--json] 只读:逐号列出未领任务、连登天数、可兑换档位、抽奖次数
5
+ // checkin|claim 补领:连登兑换 + 抽奖 + 成长任务闭环(含桌面/专家类)
6
+ //
7
+ // ⚠ 只读口径(2026-10-20 实测):
8
+ // - 签到每天成功(code 0)**不等于**连登点亮;连登只认 /v2/report 的
9
+ // chat_request_send 事件,所以「签到 OK + 连登 0 天」是常态,不是异常。
10
+ // - 任务列表与领奖只在 www.workbuddy.cn 命中(web 成长中心口径)。
11
+ import { normalizeProviderId } from "../../../providers/model-id.js";
12
+ import { compatFetch } from "../../../compat.js";
13
+
14
+ const QUOTA_SUBS = new Set(["quota", "credits", "balance"]);
15
+ const CLAIM_SUBS = new Set(["checkin", "claim", "signin"]);
16
+
17
+ async function loadCreds() {
18
+ const { loadProviderConfig, loadProviderKeys } = await import("../../../state.js");
19
+ const cfg = loadProviderConfig("workbuddy") || {};
20
+ const keys = loadProviderKeys("workbuddy") || [];
21
+ const authList = Array.isArray(cfg.auths) ? cfg.auths : [];
22
+ const out = [];
23
+ for (let i = 0; i < Math.max(keys.length, authList.length); i++) {
24
+ const a = authList[i] || {};
25
+ const at = keys[i] || "";
26
+ if (!a.uid || !at) continue;
27
+ out.push({ uid: a.uid, at, domain: a.domain, enterpriseId: a.enterpriseId || "" });
28
+ }
29
+ return out;
30
+ }
31
+
32
+ const short = (uid) => String(uid).slice(0, 8);
33
+
34
+ export async function handleWorkbuddyQuota(id, sub, rest = [], deps = {}) {
35
+ const pid = normalizeProviderId(id) || String(id || "").toLowerCase();
36
+ if (pid !== "workbuddy") return false;
37
+ const s = String(sub || "").toLowerCase();
38
+ const isClaim = CLAIM_SUBS.has(s);
39
+ if (!isClaim && !QUOTA_SUBS.has(s)) return false;
40
+
41
+ const log = deps.log || ((m) => console.log(m));
42
+ const exit = deps.exit || ((code) => process.exit(code));
43
+ const json = rest.includes("--json") || rest.includes("-json");
44
+ const fetchImpl = deps.fetchImpl || compatFetch;
45
+ const accounts = await loadCreds();
46
+ if (!accounts.length) {
47
+ if (json) log(JSON.stringify({ error: "not_logged_in", hint: "mslxdff -provider workbuddy login" }));
48
+ else {
49
+ log("未登录:还没有 workbuddy 账号");
50
+ log(" 先运行: mslxdff -provider workbuddy login");
51
+ }
52
+ exit(1);
53
+ return true;
54
+ }
55
+
56
+ const { growthTasks, runGrowthAccount } = await import("../../../providers/workbuddy/growth.js");
57
+ const { growthStreak, lotteryChances, reportChatActivity, runStreakBonus } = await import("../../../providers/workbuddy/activity.js");
58
+
59
+ const rows = [];
60
+ for (const a of accounts) {
61
+ const row = { uid: a.uid, ok: true };
62
+ try {
63
+ if (isClaim) {
64
+ const rep = await reportChatActivity({ ...a, fetchImpl });
65
+ const sb = await runStreakBonus({ ...a, fetchImpl });
66
+ const g = await runGrowthAccount({ ...a, fetchImpl });
67
+ row.activity = rep.ok;
68
+ row.streak = { days: sb.days, redeemed: sb.redeemed, drawn: sb.drawn, credit: sb.credit, errors: sb.errors };
69
+ const claimed = g.tasks.filter((t) => t.status === "claimed");
70
+ row.growth = {
71
+ credit: g.credit, energy: g.energy,
72
+ claimed: claimed.map((t) => t.task_code),
73
+ ignored: g.tasks.filter((t) => t.fired > 0 && !["claimed", "completed"].includes(String(t.status || ""))).map((t) => t.task_code),
74
+ skipped: g.tasks.filter((t) => t.skipped).length,
75
+ error: g.error,
76
+ };
77
+ row.ok = rep.ok || g.ok;
78
+ } else {
79
+ const listed = await growthTasks({ ...a, fetchImpl });
80
+ const tasks = listed.tasks || [];
81
+ const pending = tasks.filter((t) => t.accept_status !== "claimed");
82
+ row.tasksError = listed.ok ? null : listed.msg || "拉取失败";
83
+ row.pending = pending.map((t) => ({
84
+ code: t.task_code,
85
+ credit: Number(t.reward_credit || 0),
86
+ energy: Number(t.reward_energy || 0),
87
+ status: t.accept_status,
88
+ progress: `${t.progress?.current ?? t.current ?? 0}/${t.progress?.target ?? t.target ?? 0}`,
89
+ }));
90
+ row.pendingCredit = row.pending.reduce((n, t) => n + t.credit, 0);
91
+ row.claimed = tasks.filter((t) => t.accept_status === "claimed").length;
92
+ const st = await growthStreak({ ...a, fetchImpl });
93
+ row.streak = { days: st.days, tiers: st.tiers, cards: st.cards, error: st.ok ? undefined : st.msg };
94
+ const lot = await lotteryChances({ ...a, fetchImpl });
95
+ row.chances = lot.chances;
96
+ row.ok = !!listed.ok;
97
+ }
98
+ } catch (e) {
99
+ row.ok = false;
100
+ row.error = String(e?.message || e).slice(0, 160);
101
+ }
102
+ rows.push(row);
103
+ }
104
+
105
+ const failed = rows.filter((r) => !r.ok).length;
106
+
107
+ if (json) {
108
+ log(JSON.stringify({ provider: "workbuddy", mode: isClaim ? "claim" : "quota", accounts: rows, ok: rows.length - failed, total: rows.length }));
109
+ exit(failed === rows.length ? 1 : 0);
110
+ return true;
111
+ }
112
+
113
+ if (isClaim) {
114
+ log(`workbuddy 补领 · 共 ${rows.length} 个号`);
115
+ for (const r of rows) {
116
+ log(` [${short(r.uid)}]`);
117
+ if (r.error) { log(` ❌ ${r.error}`); continue; }
118
+ log(` 活跃上报: ${r.activity ? "✓ 已点亮连登" : "✗ 失败"}`);
119
+ const s = r.streak || {};
120
+ const tiers = (s.redeemed || []).map((x) => `${x.tier}(+${x.credit}c)`).join(" ") || "-";
121
+ log(` 连登: ${s.days ?? "?"} 天 · 兑换 ${tiers} · 抽奖 ${s.drawn ?? 0} 次 · +${s.credit ?? 0} 分`);
122
+ if (s.errors?.length) log(` ⚠ ${s.errors.join(";")}`);
123
+ const g = r.growth || {};
124
+ log(` 成长任务: 到账 ${g.claimed?.length ?? 0} 项 · +${g.credit ?? 0} 分 · 跳过 ${g.skipped ?? 0} 项`);
125
+ if (g.claimed?.length) log(` 已领: ${g.claimed.join(", ")}`);
126
+ if (g.ignored?.length) log(` ⚠ 上报未计分(该任务只认客户端真实行为): ${g.ignored.join(", ")}`);
127
+ if (g.error) log(` ⚠ ${g.error}`);
128
+ }
129
+ log("");
130
+ log(" 复核: mslxdff -provider workbuddy quota");
131
+ exit(failed === rows.length ? 1 : 0);
132
+ return true;
133
+ }
134
+
135
+ log(`workbuddy 已领 / 未领 · 共 ${rows.length} 个号`);
136
+ let totalPending = 0;
137
+ for (const r of rows) {
138
+ if (r.error) { log(` [${short(r.uid)}] ❌ ${r.error}`); continue; }
139
+ totalPending += r.pendingCredit || 0;
140
+ const st = r.streak || {};
141
+ const unlocked = (st.tiers || []).filter((t) => !["locked", "claimed"].includes(t.status));
142
+ log(` [${short(r.uid)}] 已领 ${r.claimed} 项 · 未领 ${r.pending.length} 项 合计 ${r.pendingCredit} 分`);
143
+ log(` 连登 ${st.days} 天${st.cards ? ` · 补签卡 ${st.cards}` : ""} · 可兑档位 ${unlocked.map((t) => t.tier).join(",") || "无"} · 抽奖次数 ${r.chances}`);
144
+ for (const t of r.pending) {
145
+ log(` · ${t.code.padEnd(28)} +${String(t.credit).padEnd(4)} c ${t.status} ${t.progress}`);
146
+ }
147
+ if (r.tasksError) log(` ⚠ 任务列表拉取失败:${r.tasksError}`);
148
+ }
149
+ log("");
150
+ log(` 未领合计 ${totalPending} 分(不含连登档位与抽奖)`);
151
+ log(" 补领: mslxdff -provider workbuddy checkin");
152
+ exit(failed === rows.length ? 1 : 0);
153
+ return true;
154
+ }
@@ -0,0 +1,204 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { compatFetch, timeoutSignal } from "../../compat.js";
3
+ import { growthRequest } from "./growth-api.js";
4
+ // WorkBuddy 每日活跃上报 + 连登管家(兑换档位 + 抽奖)。
5
+ //
6
+ // 为什么不靠签到:实测(2026-10-20,本机三号)签到每天成功(code 0)但
7
+ // GET /activity/growth/streak 的 days 恒为 0 —— 连登只认 /v2/report 的
8
+ // chat_request_send 事件,签到不点亮。连登档位 7/14/28 天的积分、补签卡、
9
+ // 抽奖次数全部拿不到,属于「不报错但持续漏领」。
10
+ //
11
+ // 上报形状照抄客户端(字段宁全勿简,防上游加严);userId 必填,缺失服务端
12
+ // 200 但静默丢弃。每号每天 1 次即可(按天去重,重复上报无额外收益)。
13
+
14
+ const REPORT_BASES = ["https://www.codebuddy.cn", "https://copilot.tencent.com"];
15
+
16
+ function reportHeaders({ at, uid, domain, enterpriseId }) {
17
+ return {
18
+ Authorization: `Bearer ${at}`,
19
+ "Content-Type": "application/json",
20
+ Accept: "application/json",
21
+ "X-User-Id": uid,
22
+ "X-Domain": domain || "www.codebuddy.cn",
23
+ "X-Product": "SaaS",
24
+ "User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
25
+ Origin: "https://www.codebuddy.cn",
26
+ Referer: "https://www.codebuddy.cn/",
27
+ ...(enterpriseId ? { "X-Enterprise-Id": enterpriseId } : {}),
28
+ };
29
+ }
30
+
31
+ export function chatActivityEvent({ uid, conversationId, requestId, model = "deepseek-v4-flash", modelName = "DeepSeek V4 Flash" }) {
32
+ const now = Date.now();
33
+ return {
34
+ eventCode: "chat_request_send",
35
+ timestamp: now,
36
+ reportDelay: 0,
37
+ mode: "craft",
38
+ conversationId,
39
+ requestId,
40
+ inputLength: 12,
41
+ requestModelId: model,
42
+ requestModelName: modelName,
43
+ isPlan: false,
44
+ isAutoExecuteTerminal: false,
45
+ isAutoModify: false,
46
+ codebaseEnable: false,
47
+ maxToken: 0,
48
+ maxSteps: 0,
49
+ temperature: 0,
50
+ maxRetries: 0,
51
+ mentionContexts: [],
52
+ knowledgeId: [],
53
+ knowledgeName: [],
54
+ codebaseId: "",
55
+ mentionContextCount: 0,
56
+ command: "",
57
+ expertId: "",
58
+ recommendId: "",
59
+ skillId: "",
60
+ skillCount: 0,
61
+ totalCount: 0,
62
+ fileUri: "",
63
+ presentAt: now,
64
+ traceId: "",
65
+ rootRequestId: requestId,
66
+ parentConversationId: conversationId,
67
+ agentName: "default",
68
+ agentType: "conversation",
69
+ userId: uid,
70
+ };
71
+ }
72
+
73
+ // 一条对话活跃上报:点亮当日连登 + 解锁 first_buddy(领养前置)。双域容灾,不抛异常。
74
+ export async function reportChatActivity({ uid, at, domain, enterpriseId, conversationId, requestId, model, modelName, fetchImpl = compatFetch, timeoutMs = 15000 } = {}) {
75
+ if (!uid || !at) return { ok: false, msg: "缺少 uid / accessToken" };
76
+ const conv = conversationId || `mslxdff-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
77
+ const req = requestId || conv;
78
+ const body = JSON.stringify([chatActivityEvent({ uid, conversationId: conv, requestId: req, model, modelName })]);
79
+ let last = { ok: false, msg: "无可用域" };
80
+ for (const base of REPORT_BASES) {
81
+ let res;
82
+ try {
83
+ res = await fetchImpl(`${base}/v2/report`, {
84
+ method: "POST",
85
+ headers: reportHeaders({ at, uid, domain, enterpriseId }),
86
+ body,
87
+ signal: timeoutSignal(timeoutMs),
88
+ });
89
+ } catch (e) {
90
+ last = { ok: false, status: 0, msg: `网络失败: ${String(e?.message || e).slice(0, 160)}` };
91
+ continue;
92
+ }
93
+ let j = null;
94
+ try { j = await res.json(); } catch {}
95
+ const out = { ok: res.status === 200 && j?.code === 0, status: res.status, code: j?.code, msg: j?.msg || "", base };
96
+ if (out.ok) return out;
97
+ // 401/403 是登录态问题,换域也没用
98
+ if (res.status === 401 || res.status === 403) return { ...out, needRefresh: true };
99
+ last = out;
100
+ }
101
+ return last;
102
+ }
103
+
104
+ const TIER_KEYS = ["tier_7d", "tier_14d", "tier_28d"];
105
+
106
+ // 连登完整状态(days / 档位 / 补签卡)。
107
+ export async function growthStreak(api = {}) {
108
+ const r = await growthRequest({ ...api, method: "GET", path: "/activity/growth/streak" });
109
+ if (!r.ok) return { ok: false, msg: r.msg || `HTTP ${r.status}`, days: 0, tiers: [], cards: 0 };
110
+ const data = r.data || {};
111
+ const rs = data.redemption_status || {};
112
+ const tiers = TIER_KEYS
113
+ .map((k) => ({ tier: k.slice(5), status: rs[`${k}_status`] || "", days: Number(rs[`${k}_count`] || 0) }))
114
+ .filter((t) => t.status);
115
+ return {
116
+ ok: true,
117
+ days: Number(data.streak?.days || 0),
118
+ monthTotalDays: Number(data.streak?.month_total_days || 0),
119
+ nextTier: data.streak?.next_tier || "",
120
+ nextTierRemaining: Number(data.streak?.next_tier_remaining || 0),
121
+ tiers,
122
+ cards: Number(data.makeup_cards?.balance || 0),
123
+ rewards: Array.isArray(rs.tiers) ? rs.tiers : [],
124
+ };
125
+ }
126
+
127
+ // 兑换一个已解锁档位(tier: "7d"|"14d"|"28d");未解锁上游 403,调用方按 status 跳过即可。
128
+ export async function redeemStreakTier(api = {}, tier) {
129
+ const r = await growthRequest({
130
+ ...api,
131
+ method: "POST",
132
+ path: "/activity/growth/redeem",
133
+ body: { tier, client_token: randomUUID() },
134
+ });
135
+ return { ok: r.ok, status: r.status, code: r.code, msg: r.msg || "", data: r.data || {} };
136
+ }
137
+
138
+ export async function lotteryChances(api = {}) {
139
+ const r = await growthRequest({ ...api, method: "GET", path: "/activity/growth/lottery/summary" });
140
+ if (!r.ok) return { ok: false, chances: 0, enabled: false, msg: r.msg || `HTTP ${r.status}` };
141
+ return { ok: true, chances: Number(r.data?.chances || 0), enabled: r.data?.module?.enabled !== false };
142
+ }
143
+
144
+ export async function lotteryDraw(api = {}) {
145
+ const r = await growthRequest({
146
+ ...api,
147
+ method: "POST",
148
+ path: "/activity/growth/lottery/draw",
149
+ body: { client_token: randomUUID() },
150
+ });
151
+ return { ok: r.ok, status: r.status, msg: r.msg || "", prize: r.data || null };
152
+ }
153
+
154
+ // 连登管家闭环:兑换所有已解锁未领取的档位 → 抽完全部次数。幂等,未解锁静默跳过。
155
+ export async function runStreakBonus({ uid, at, domain, enterpriseId, fetchImpl = compatFetch, onStep = null, maxDraws = 20 } = {}) {
156
+ const api = { uid, at, domain, enterpriseId, fetchImpl };
157
+ const step = (e) => { try { onStep?.(e); } catch {} };
158
+ const out = { uid, redeemed: [], drawn: 0, credit: 0, energy: 0, prizes: [], errors: [] };
159
+
160
+ const st = await growthStreak(api);
161
+ if (!st.ok) {
162
+ out.errors.push(`读连登状态失败:${st.msg}`);
163
+ return out;
164
+ }
165
+ out.days = st.days;
166
+
167
+ for (const t of st.tiers) {
168
+ // locked = 天数不够;claimed = 已领过。其余状态视为可兑换。
169
+ if (!["locked", "claimed"].includes(String(t.status))) {
170
+ const r = await redeemStreakTier(api, t.tier);
171
+ if (r.ok) {
172
+ const credit = Number(r.data?.credit || 0);
173
+ const energy = Number(r.data?.energy || 0);
174
+ out.credit += credit;
175
+ out.energy += energy;
176
+ out.redeemed.push({ tier: t.tier, credit, energy });
177
+ step({ phase: "redeem", ok: true, tier: t.tier, credit, energy });
178
+ } else {
179
+ out.errors.push(`兑换 ${t.tier} 失败:${r.msg || `HTTP ${r.status}`}`);
180
+ step({ phase: "redeem", ok: false, tier: t.tier, msg: r.msg });
181
+ }
182
+ }
183
+ }
184
+
185
+ // 每轮重读次数(兑换与抽奖都可能再发次数),抽到 0 或达上限为止。
186
+ for (;;) {
187
+ if (out.drawn >= maxDraws) break;
188
+ const left = (await lotteryChances(api)).chances;
189
+ if (left <= 0) break;
190
+ const r = await lotteryDraw(api);
191
+ if (!r.ok) {
192
+ out.errors.push(`抽奖失败:${r.msg || `HTTP ${r.status}`}`);
193
+ step({ phase: "draw", ok: false, msg: r.msg });
194
+ break;
195
+ }
196
+ out.drawn += 1;
197
+ const name = r.prize?.prize_name || r.prize?.name || r.prize?.title || "";
198
+ const credit = Number(r.prize?.credit || 0);
199
+ out.credit += credit;
200
+ out.prizes.push({ name, credit });
201
+ step({ phase: "draw", ok: true, remaining: left - 1, prize: name, credit });
202
+ }
203
+ return out;
204
+ }