mslxdff 0.1.237 → 0.1.239

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.237",
3
+ "version": "0.1.239",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,123 @@
1
+ // mslxdff -provider codearts checkin|quota —— 每日签到领积分(snap-access 的 `/v1/ops/*`)。
2
+ //
3
+ // 子命令:
4
+ // checkin [--json] [--dry] 逐号补领今日积分(幂等:已到账零写操作;到账只认复查)
5
+ // quota|credits|balance [--json] 只读查积分余额(`/snap-manager/v1/statistics/plugin`)
6
+ //
7
+ // ⚠ 与 `-provider codearts models` 那条「福利」**不是一回事**:福利走 opengw 的 benefit/claim(模型 Token 账),
8
+ // 积分走 snap-access 的 `/v1/ops/claim`(本文件)。两本账互不影响。
9
+ // ⚠ 判据口径:到账只认「领取后重读活动列表」的状态,不信 claim 回执
10
+ // (raccoon 踩过「只信回执就谎报到账」,traework 同款纪律)。
11
+ import { normalizeProviderId } from "../../../providers/model-id.js";
12
+
13
+ const CHECKIN_SUBS = new Set(["checkin", "signin"]);
14
+ const QUOTA_SUBS = new Set(["quota", "credits", "balance"]);
15
+
16
+ function isCodearts(id) {
17
+ return (normalizeProviderId(id) || String(id || "").toLowerCase()) === "codearts";
18
+ }
19
+ const uid8 = (account) => String(account?.userId || "(无 uid)").slice(0, 8);
20
+
21
+ /** 取号:deps.accounts 优先(测试注入,零网络);否则 keys → provider 的 authPool(带刷新写回)。 */
22
+ async function loadAccounts(deps) {
23
+ if (Array.isArray(deps.accounts)) return { accounts: deps.accounts, close: null };
24
+ const { createCodeartsProvider } = await import("../../../providers/codearts/index.js");
25
+ const provider = createCodeartsProvider({ fetchImpl: deps.fetchImpl });
26
+ const pool = provider._pool;
27
+ const accounts = [];
28
+ for (const cand of pool.aliveAccounts()) {
29
+ try { accounts.push(await pool.getCredential(cand.blob)); } catch { /* 死号跳过 */ }
30
+ }
31
+ return { accounts, close: () => { try { provider.close?.(); } catch { /* ignore */ } } };
32
+ }
33
+
34
+ /** 逐号补领今日积分。退出码:一个都没成 → 1(旧写法恒 0 会让人以为签好了)。 */
35
+ export async function handleCodeartsCheckin(id, sub, rest = [], deps = {}) {
36
+ if (!isCodearts(id) || !CHECKIN_SUBS.has(String(sub || "").toLowerCase())) return false;
37
+ const log = deps.log || ((m) => console.log(m));
38
+ const exit = deps.exit || ((code) => process.exit(code));
39
+ const wantsJson = rest.includes("--json") || rest.includes("-json");
40
+ const dry = rest.includes("--dry") || rest.includes("--probe");
41
+
42
+ const { accounts, close } = await loadAccounts(deps);
43
+ try {
44
+ if (!accounts.length) {
45
+ if (wantsJson) log(JSON.stringify({ provider: "codearts", error: "no_accounts", hint: "mslxdff -provider codearts login" }));
46
+ else log("没有 codearts 账号,请先 mslxdff -provider codearts login");
47
+ exit(1);
48
+ return true;
49
+ }
50
+ const { runCheckin } = deps.checkinModule || (await import("../../../providers/codearts/checkin.js"));
51
+ const rows = [];
52
+ for (const account of accounts) {
53
+ let r;
54
+ try {
55
+ r = await runCheckin({ account, fetchImpl: deps.fetchImpl, dry });
56
+ } catch (e) {
57
+ r = { ok: false, status: "error", claimed: 0, credits: 0, message: String(e?.message || e).slice(0, 140) };
58
+ }
59
+ const row = { uid: uid8(account), ok: r.ok, status: r.status, claimed: r.claimed, credits: r.credits, msg: String(r.message || "").slice(0, 140) };
60
+ rows.push(row);
61
+ if (!wantsJson) {
62
+ const mark = r.ok ? (r.status === "already" ? "✓ 今日已领取" : r.status === "nothing" ? "- 无可用签到活动" : `✓ ${r.message}`) : `✗ ${r.message}`;
63
+ log(` [${row.uid}] ${mark}`);
64
+ }
65
+ }
66
+ const okCount = rows.filter((r) => r.ok).length;
67
+ const credits = rows.reduce((s, r) => s + (r.credits || 0), 0);
68
+ if (wantsJson) {
69
+ log(JSON.stringify({ provider: "codearts", dry: Boolean(dry), ok: okCount, total: rows.length, credits, accounts: rows }));
70
+ } else {
71
+ log("");
72
+ log(`codearts 签到:${okCount}/${rows.length} 号完成,本次到账 ${credits} 积分`);
73
+ if (dry) log(" (--dry 干跑:未发任何写请求)");
74
+ }
75
+ exit(okCount > 0 ? 0 : 1);
76
+ return true;
77
+ } finally {
78
+ try { close?.(); } catch { /* ignore */ }
79
+ }
80
+ }
81
+
82
+ /** 只读:逐号查积分余额(`usageTotalPackageCredit` 口径,分类明细单列不累加)。 */
83
+ export async function handleCodeartsQuota(id, sub, rest = [], deps = {}) {
84
+ if (!isCodearts(id) || !QUOTA_SUBS.has(String(sub || "").toLowerCase())) return false;
85
+ const log = deps.log || ((m) => console.log(m));
86
+ const exit = deps.exit || ((code) => process.exit(code));
87
+ const wantsJson = rest.includes("--json") || rest.includes("-json");
88
+
89
+ const { accounts, close } = await loadAccounts(deps);
90
+ try {
91
+ if (!accounts.length) {
92
+ if (wantsJson) log(JSON.stringify({ provider: "codearts", error: "no_accounts", hint: "mslxdff -provider codearts login" }));
93
+ else log("没有 codearts 账号,请先 mslxdff -provider codearts login");
94
+ exit(1);
95
+ return true;
96
+ }
97
+ const { fetchCreditBalance } = deps.checkinModule || (await import("../../../providers/codearts/checkin.js"));
98
+ const rows = [];
99
+ for (const account of accounts) {
100
+ let r;
101
+ try { r = await fetchCreditBalance({ account, fetchImpl: deps.fetchImpl }); } catch (e) { r = { ok: false, error: String(e?.message || e).slice(0, 140) }; }
102
+ rows.push({ uid: uid8(account), ok: r.ok, total: r.total, used: r.used, remain: r.remain, tokenUsage: r.tokenUsage, error: r.ok ? "" : String(r.error || `HTTP ${r.status || "-"}`).slice(0, 140) });
103
+ }
104
+ const failed = rows.filter((r) => !r.ok).length;
105
+ const allBad = rows.length > 0 && failed === rows.length;
106
+ if (wantsJson) {
107
+ log(JSON.stringify({ provider: "codearts", ok: rows.length - failed, total: rows.length, note: "remain=余额;total 是总额非上限", accounts: rows }));
108
+ exit(allBad ? 1 : 0);
109
+ return true;
110
+ }
111
+ log(`codearts 积分 · 共 ${rows.length} 个号`);
112
+ for (const r of rows) {
113
+ if (!r.ok) { log(` [${r.uid}] ❌ ${r.error}`); continue; }
114
+ log(` [${r.uid}] 剩余 ${r.remain} / 共 ${r.total}(已用 ${r.used})· token 用量 ${r.tokenUsage}`);
115
+ }
116
+ log("");
117
+ log(" 每日签到: mslxdff -provider codearts checkin");
118
+ exit(allBad ? 1 : 0);
119
+ return true;
120
+ } finally {
121
+ try { close?.(); } catch { /* ignore */ }
122
+ }
123
+ }
@@ -42,6 +42,7 @@ export async function handleProvider(args) {
42
42
  console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
43
43
  console.error(" mslxdff -provider openrouter interactive hidden input (append)");
44
44
  console.error(" mslxdff -provider codearts login 华为云 CodeArts PKCE 授权(浏览器登录 → 凭证 blob 落盘,ADR-0027)");
45
+ console.error(" mslxdff -provider codearts checkin|quota [--json] [--dry] 每日签到领积分 / 查积分余额(snap-access /v1/ops/*;与 models 那条福利是两本账)");
45
46
  console.error(" mslxdff -provider qoder login [--region cn|global] Qoder 设备授权(国际站默认,国内站 --region cn)");
46
47
  console.error(" mslxdff -provider qoder checkin [--json] [--region cn|global] [--any] [--dry] 每日签到领积分(cn=daily-check-in,global=campaigns)");
47
48
  console.error(" mslxdff -provider zcode login [--bigmodel] ZCode 授权(智谱免费额度,Start/Coding Plan)");
@@ -80,6 +81,9 @@ export async function handleProvider(args) {
80
81
  if (await handleWorkbuddyQuota(id, sub, rest)) return true;
81
82
  const { handleCodeartsLogin } = await import("./codearts-login.js");
82
83
  if (await handleCodeartsLogin(id, sub, rest)) return true;
84
+ const { handleCodeartsCheckin, handleCodeartsQuota } = await import("./codearts-checkin.js");
85
+ if (await handleCodeartsCheckin(id, sub, rest)) return true;
86
+ if (await handleCodeartsQuota(id, sub, rest)) return true;
83
87
  const { handleTraeworkLogin } = await import("./traework-login.js");
84
88
  if (await handleTraeworkLogin(id, sub, rest)) return true;
85
89
  const { handleTraeworkCheckin, handleTraeworkQuota } = await import("./traework-checkin.js");
@@ -10,6 +10,8 @@ import { AUTH_DOC_PROVIDER_IDS } from "../../../runtime/provider-gate.js";
10
10
  // 保留子命令:这些词归 provider/ 下别的 handler 管,掉到 keys 兜底 = 该 provider 没接线。
11
11
  // 名单 = 各 handler 实际比对的 sub 值全集(keys.js 自己认的 list/status/add/remove/clear 不在内)。
12
12
  // 新增 handler 时同步这里;漏收的后果是那个词能被当 key 写盘。
13
+ /** 覆盖式写 keys 时,单个候选 token 的最小可信长度(闸 3 判据:真 key 都远长于此)。 */
14
+ export const MIN_KEY_CHARS = 11;
13
15
  export const RESERVED_PROVIDER_SUBS = new Set([
14
16
  "login", "auth", "oauth", "checkin", "signin", "quota", "credits", "balance",
15
17
  "migrate", "free", "bench", "benchmark", "eval", "test",
@@ -17,6 +19,7 @@ export const RESERVED_PROVIDER_SUBS = new Set([
17
19
  "set-url", "set-models-path", "set-chat-path", "url", "seturl", "setmodelspath", "setchatpath",
18
20
  "allowlist", "allow", "allowed", "allowany", "allow-any", "allow_any", "whitelist",
19
21
  "del", "delete", "rm", "remove-provider", "del-provider", "import", "import-desktop", "from-desktop",
22
+ "help", "usage", "usage-all",
20
23
  ]);
21
24
 
22
25
  /**
@@ -46,5 +49,22 @@ export function keysWriteDenial(id, sub, rest = []) {
46
49
  ` 确知自己在做什么 → 末尾加 --force 重跑`,
47
50
  ];
48
51
  }
52
+ // 闸 3:形状闸。2026-10-24 复现同类事故 —— `mslxdff -provider codearts help` 里 `help` 既不是
53
+ // 保留 verb(闸 1 名单里没有它),codearts 又不属于 auth 号型六家(闸 2 不管它),于是 `help` 一路
54
+ // 掉进「未知即当 key」的兜底分支,把 providerConfigs.codearts.keys 的 2 条真凭据整组覆盖成 "help"。
55
+ // 判据:真 key 的长度都远超 10 字符(openrouter ~73、traework accessToken 实测 1004、codearts blob
56
+ // 上千),而 ≤10 字符的裸词几乎必然是打错位置的子命令/模型名/文件名 —— 覆盖式 set 写进去就是当场废号。
57
+ // 逃生阀:`--force`,或改走只追加、不覆盖的 `add`。
58
+ const shortTokens = rest
59
+ .map((k) => String(k || "").trim())
60
+ .filter((k) => k && !k.startsWith("-") && k.length < MIN_KEY_CHARS);
61
+ if (shortTokens.length && !rest.includes("--force")) {
62
+ return [
63
+ `✗ -provider ${id} ${sub}:疑似把「${shortTokens.join("、")}」当 API key 覆盖 ${id} 现有 keys —— 已拒绝,配置未改动`,
64
+ ` 拦截原因:key 长度不足 ${MIN_KEY_CHARS} 字符(真 key 都远长于此),多半是子命令/模型名打错了位置。`,
65
+ ` ${id} 支持什么:mslxdff -provider ${id} list`,
66
+ ` 确知是短 key(自建桩服务)→ 末尾加 --force 重跑,或走只追加的 mslxdff -provider ${id} add <key>`,
67
+ ];
68
+ }
49
69
  return null;
50
70
  }
@@ -0,0 +1,198 @@
1
+ // codearts 每日签到领积分(`/v1/ops/*`)—— 与 `claimBenefit` 的「模型 Token 福利」是**两本账**。
2
+ // 协议三方交叉验证:HITZY2002/codearts2api `internal/upstream/checkin.go`(Go)、
3
+ // L0NE-6/CodeArts-Daily `codearts_daily.py`(青龙)、anye-99·gurio-wine `src/codearts-credits.ts`(TS)。
4
+ // 现网取证 2026-10-24:`GET /v1/ops/delivery?channel=IDE` → 4 个活动,`#1 USER_LOGIN` 每日 1000 积分。
5
+ // 字段坑:`campaignId` 是**数字**、积分字段叫 **`benefitAmount`**、不可领取时 `status` 是 **`null`**。
6
+ import { SNAP_BASE } from "./const.js";
7
+ import { compatFetch, timeoutSignal } from "../../compat.js";
8
+ import { signRequest } from "./sign.js";
9
+
10
+ /** 活动列表/领取/确认的渠道标识(声明请求来自 IDE 形态;官方文档与两个参考实现同值)。 */
11
+ export const OPS_CHANNEL = "IDE";
12
+ /** ops 路由需要的 Agent-Type;值错会 `TM.00001001`(空→未找到合法可路由 api)。 */
13
+ export const OPS_AGENT_TYPE = "PromptCenter";
14
+ export const EP_OPS_DELIVERY = "/v1/ops/delivery";
15
+ export const EP_OPS_CLAIM = "/v1/ops/claim";
16
+ export const EP_OPS_CONFIRM = "/v1/ops/confirm";
17
+ export const EP_PACKAGE_INFO = "/snap-manager/v1/statistics/plugin";
18
+ export const snapBase = () => SNAP_BASE;
19
+
20
+ /** 只领这两类:每日签到 + 新用户注册礼(INVITE_USER / STUDENT_CERTIFIED 不是每日任务,不碰)。 */
21
+ const DAILY_TYPES = new Set(["USER_LOGIN", "NEW_USER_REGISTER"]);
22
+ /** 已到账态:这几个状态下绝不 claim(幂等只靠活动列表预检,上游没有业务码兜底)。 */
23
+ const DONE_STATUSES = new Set(["CONFIRMED", "CONSUMED"]);
24
+
25
+ /**
26
+ * 解析活动列表。业务码非 0 或缺 `data.items` → 空表(不臆造契约)。
27
+ * @returns {Array<{id:number,type:string,title:string,credit:number,unit:string,claimable:boolean,status:string}>}
28
+ */
29
+ export function parseCreditCampaigns(payload) {
30
+ const items = payload?.data?.items;
31
+ if (!Array.isArray(items)) return [];
32
+ const out = [];
33
+ for (const it of items) {
34
+ const id = Number(it?.campaignId);
35
+ if (!Number.isFinite(id)) continue; // campaignId 缺失的条目没法回传,丢掉
36
+ out.push({
37
+ id,
38
+ type: String(it?.type || ""),
39
+ title: String(it?.title || ""),
40
+ // 对齐参考实现:`res.Credits = pendingTotalAmount || benefitAmount`(待结算优先,缺则取活动额)
41
+ credit: Number(it?.pendingTotalAmount) || Number(it?.benefitAmount) || 0,
42
+ unit: String(it?.benefitUnit || ""),
43
+ claimable: it?.claimable === true,
44
+ status: it?.status == null ? "" : String(it.status),
45
+ });
46
+ }
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * 由活动列表推出本次要做的写操作(纯函数,便于单测)。
52
+ * 判据:只有 `claimable===true` 才 claim;`status==="CLAIMED"` 只补 confirm;已到账态零动作。
53
+ * @returns {Array<{kind:"claim"|"confirm", id:number, credit:number, title:string}>}
54
+ */
55
+ export function planCreditActions(campaigns) {
56
+ const out = [];
57
+ for (const c of Array.isArray(campaigns) ? campaigns : []) {
58
+ if (!DAILY_TYPES.has(c?.type)) continue;
59
+ if (c.unit && c.unit !== "CREDIT") continue; // 非积分账本不碰
60
+ if (!Number.isFinite(c.id) || c.id <= 0) continue;
61
+ if (c.status === "CLAIMED") { out.push({ kind: "confirm", id: c.id, credit: c.credit, title: c.title }); continue; }
62
+ if (DONE_STATUSES.has(c.status)) continue;
63
+ // 只有 ELIGIBLE + claimable 才 claim(参考实现:status 空 = 未获得活动资格;未知状态不臆造)
64
+ if (c.status === "ELIGIBLE" && c.claimable) out.push({ kind: "claim", id: c.id, credit: c.credit, title: c.title });
65
+ }
66
+ return out;
67
+ }
68
+
69
+ // ─────────────────────────── HTTP 层(签名 + 写操作) ───────────────────────────
70
+
71
+ function credOf(account) {
72
+ return { accessKeyId: account?.accessKeyId, secretAccessKey: account?.secretAccessKey, securityToken: account?.securityToken };
73
+ }
74
+
75
+ /**
76
+ * 签名 ops 请求。只把 `content-type`(+ `sign.js` 自己加的 host/x-sdk-*)送进 canonical;
77
+ * `agent-type` 与 `x-language` 必须**签名后追加**(参考实现口径:进 canonical 会 401)。
78
+ */
79
+ async function opsRequest({ account, path, method = "GET", body, fetchImpl, baseUrl, timeoutMs = 20_000 } = {}) {
80
+ const url = `${baseUrl || snapBase()}${path}`;
81
+ const payload = body === undefined ? "" : JSON.stringify(body);
82
+ const { headers: signedHeaders } = signRequest({
83
+ method, url, headers: { "content-type": "application/json" }, body: payload, cred: credOf(account),
84
+ });
85
+ signedHeaders["agent-type"] = OPS_AGENT_TYPE;
86
+ signedHeaders["x-language"] = "zh-cn";
87
+ const doFetch = fetchImpl || compatFetch;
88
+ let res;
89
+ try {
90
+ res = await doFetch(url, { method, headers: signedHeaders, ...(body === undefined ? {} : { body: payload }), signal: timeoutSignal(timeoutMs) });
91
+ } catch (e) {
92
+ return { status: 0, json: null, text: "", error: String(e?.message || e).slice(0, 200) };
93
+ }
94
+ const text = await res.text().catch(() => "");
95
+ let json = null;
96
+ if (text) { try { json = JSON.parse(text); } catch { /* 非 JSON:沿用 text 给调用方看 */ } }
97
+ return { status: res.status, json, text };
98
+ }
99
+
100
+ /** 上游业务码非 0 即失败(`{code:4000,message:"invalid channel, group not found"}` 这种)。 */
101
+ const bizFailed = (r) => Boolean(r?.json) && r.json.code != null && Number(r.json.code) !== 0;
102
+ const errOf = (r) => r.error
103
+ || (bizFailed(r) ? String(r.json.message || `code=${r.json.code}`).slice(0, 180)
104
+ : (r.status === 200 ? "" : String(r.text || "").slice(0, 180)));
105
+
106
+ /** 读活动列表(只读)。 */
107
+ export async function fetchCreditCampaigns(opts = {}) {
108
+ const r = await opsRequest({ ...opts, path: `${EP_OPS_DELIVERY}?channel=${OPS_CHANNEL}` });
109
+ if (r.status !== 200) return { ok: false, status: r.status, campaigns: [], error: errOf(r), code: r.json?.code };
110
+ if (bizFailed(r)) return { ok: false, status: 200, campaigns: [], error: errOf(r), code: r.json.code };
111
+ return { ok: true, status: 200, campaigns: parseCreditCampaigns(r.json), error: "" };
112
+ }
113
+
114
+ /** 领取:`{campaignId, channel, idempotentKey}`(幂等键带毫秒时间戳,与参考实现同形状)。 */
115
+ export async function claimCreditCampaign({ campaignId, now = Date.now, ...opts } = {}) {
116
+ const body = { campaignId, channel: OPS_CHANNEL, idempotentKey: `claim_${campaignId}_${now()}` };
117
+ const r = await opsRequest({ ...opts, path: EP_OPS_CLAIM, method: "POST", body });
118
+ return { ok: r.status === 200 && !bizFailed(r), status: r.status, json: r.json, error: errOf(r) };
119
+ }
120
+
121
+ /** 确认到账:`{campaignId}`。claim 之后无条件补这一步,否则状态停在 CLAIMED 不算到账。 */
122
+ export async function confirmCreditCampaign({ campaignId, ...opts } = {}) {
123
+ const r = await opsRequest({ ...opts, path: EP_OPS_CONFIRM, method: "POST", body: { campaignId } });
124
+ return { ok: r.status === 200 && !bizFailed(r), status: r.status, json: r.json, error: errOf(r) };
125
+ }
126
+
127
+ /**
128
+ * 单账号签到:列表 → (claim → confirm) → **重读列表复查**。
129
+ * 到账只认复查后的状态(raccoon/traework 同款纪律:只信回执就会谎报到账)。
130
+ */
131
+ export async function runCheckin({ account, fetchImpl, baseUrl, timeoutMs, dry = false, now = Date.now } = {}) {
132
+ const common = { account, fetchImpl, baseUrl, timeoutMs };
133
+ const r = { account: account?.userId || null, ok: false, status: "error", dry: false, claimed: 0, credits: 0, actions: [], writes: [], campaigns: [], message: "" };
134
+ const first = await fetchCreditCampaigns(common);
135
+ r.campaigns = first.campaigns;
136
+ if (!first.ok) { r.message = `查询活动失败 HTTP ${first.status || "-"}${first.error ? `:${first.error}` : ""}`; return r; }
137
+
138
+ const actions = planCreditActions(first.campaigns);
139
+ r.actions = actions;
140
+ if (!actions.length) {
141
+ const done = first.campaigns.some((c) => DAILY_TYPES.has(c.type) && DONE_STATUSES.has(c.status));
142
+ r.ok = true;
143
+ r.status = done ? "already" : "nothing";
144
+ r.message = done ? "今日已领取(零写操作)" : "无可用签到活动";
145
+ return r;
146
+ }
147
+ if (dry) {
148
+ r.ok = true; r.dry = true; r.status = "claimable";
149
+ r.message = `干跑:${actions.length} 项待领,未发写请求`;
150
+ return r;
151
+ }
152
+
153
+ for (const a of actions) {
154
+ if (a.kind === "claim") {
155
+ const c1 = await claimCreditCampaign({ ...common, campaignId: a.id, now });
156
+ r.writes.push({ kind: "claim", id: a.id, ok: c1.ok, status: c1.status, error: c1.error });
157
+ if (!c1.ok) continue; // claim 失败则不 confirm(对齐参考实现:claim 错即返回)
158
+ }
159
+ const c2 = await confirmCreditCampaign({ ...common, campaignId: a.id });
160
+ r.writes.push({ kind: "confirm", id: a.id, ok: c2.ok, status: c2.status, error: c2.error });
161
+ }
162
+
163
+ const after = await fetchCreditCampaigns(common);
164
+ if (after.ok) r.campaigns = after.campaigns;
165
+ const landed = (after.campaigns || []).filter((c) => actions.some((a) => a.id === c.id) && DONE_STATUSES.has(c.status));
166
+ r.claimed = landed.length;
167
+ r.credits = landed.reduce((s, c) => s + (c.credit || 0), 0);
168
+ if (landed.length) {
169
+ r.ok = true; r.status = "claimed";
170
+ r.message = `签到到账 +${r.credits} 积分(${landed.length} 项:${landed.map((c) => `#${c.id}`).join(" ")})`;
171
+ } else {
172
+ const bad = r.writes.find((w) => !w.ok);
173
+ r.status = "not-landed";
174
+ r.message = `写请求已发但复查仍未到账(不许当今天签过):${bad ? `${bad.kind} HTTP ${bad.status || "-"} ${bad.error || ""}` : "重读活动列表后状态未变"}`.slice(0, 220);
175
+ }
176
+ return r;
177
+ }
178
+
179
+ /** 积分账本:取 `usageTotalPackageCredit` 的总额/已用/剩余(分类明细单列,**不累加**)。 */
180
+ export async function fetchCreditBalance(opts = {}) {
181
+ const r = await opsRequest({ ...opts, path: EP_PACKAGE_INFO });
182
+ if (r.status !== 200) return { ok: false, status: r.status, error: errOf(r) };
183
+ const metrics = Array.isArray(r.json?.metrics) ? r.json.metrics : [];
184
+ const num = (v) => Number(v) || 0;
185
+ const pick = (name) => metrics.find((m) => m?.name === name);
186
+ const creditOf = (m) => (m ? { total: num(m.package_credit_amount), used: num(m.package_credit_used), remain: num(m.package_credit_remain) } : null);
187
+ const totalM = pick("usageTotalPackageCredit");
188
+ const tokenM = pick("usageTokenChatMessages");
189
+ return {
190
+ ok: true, status: 200,
191
+ total: num(totalM?.package_credit_amount), used: num(totalM?.package_credit_used), remain: num(totalM?.package_credit_remain),
192
+ basic: creditOf(pick("usageBasicPackageCredit")),
193
+ bonus: creditOf(pick("usageBonusPackageCredit")),
194
+ onDemand: creditOf(pick("useOnDemandPackageCredit")) || creditOf(pick("usageOnDemandPackageCredit")),
195
+ tokenUsage: num(tokenM?.usage_token_num), tokenPackages: num(tokenM?.package_token_amount),
196
+ isCreditPackage: Boolean(totalM),
197
+ };
198
+ }
@@ -44,12 +44,19 @@ export const MAAS_TYPE_HEADER = "maas_type";
44
44
  export const MAAS_BENEFIT = "benefit";
45
45
 
46
46
  // 冷启动种子:福利套餐轮换后新模型在首次发现前也按福利路由(宁多带不少带,少带即 404)。
47
- // 现网取证 2026-10-16(福利网关 /api/v1/gateway/config,error_code=0000)共 4 位:下列 4 个 id 逐字对齐上游。
48
- export const SEED_BENEFIT_MODELS = ["deepseek-v4-flash-0731", "deepseek-v4-pro-0813", "deepseek-v4.1-flash", "glm-5.3-flash"];
49
- // 内置模型种子(仅用于大小写归一,不标福利)。
47
+ // 现网取证 2026-10-16(福利网关 /api/v1/gateway/config,error_code=0000)共 4 位:下列 id 逐字对齐上游。
48
+ // 2026-10-24 再取证:`deepseek-v4.1-flash` 已转 builtin 正式模型(福利网关该号已空)。它留在福利种子里
49
+ // 是有害的 —— preheat 未回来时带福利头 → InferHub.4004.200 benefit not found,不带才 200。故移出、转入正式表。
50
+ export const SEED_BENEFIT_MODELS = ["deepseek-v4-flash-0731", "deepseek-v4-pro-0813", "glm-5.3-flash"];
51
+ // 内置模型种子(仅用于大小写归一,不标福利)。id 一律写上游真正的 model_id 大小写:
52
+ // `deepseek-v4.1-flash` 是全小写,`GLM-5.2` 就是混合大小写——猜错即 InferHub.002002009.404。
50
53
  export const SEED_KNOWN_MODELS = [
51
- "GLM-5.2", "GLM-5.2-ArkTS-SPARK", "GLM-5.1", "GLM-4.7",
52
- "OpenPangu-2.0-Pro", "OpenPangu-2.0-Flash",
54
+ // 现网取证 2026-10-24(builtin 路真 model_id 逐字):这些就是全小写,展示名(OpenPangu-2.0-Pro /
55
+ // GLM-5.3 / Kimi-K3)出自 agent-center 的 model_name,当 id 发上去必 InferHub.002002009.404。
56
+ // 同名漂移记在 name 字段,不占用 id:GLM-5.2-ArkTS-SPARK 的真 id 是 glm-5.2-sft-harmony。
57
+ "deepseek-v4.1-flash", "glm-5.3", "kimi-k3", "openpangu-2.0-pro", "openpangu-2.0-flash",
58
+ "glm-5.2-sft-harmony", "kimi-k2.6-vl",
59
+ "GLM-5.2", "GLM-5.1", "GLM-4.7",
53
60
  "Qwen3-VL-235B", "Qwen3.5-397B-A17B-VL", "Qwen3.6-27B-VL",
54
61
  ];
55
62
 
@@ -14,8 +14,19 @@ const lower = (s) => String(s || "").trim().toLowerCase();
14
14
 
15
15
  export function createCatalog() {
16
16
  const known = new Map(); // lower → 精确 ID(种子 + 已发现,大小写归一单点)
17
+ const knownReal = new Set(); // 该精确 ID 出自上游真正的 model_id(不是展示名兜底)
18
+
19
+ // id 权威性:真正的 model_id > 展示名(agent-center 无 model_id 时拿 model_name 顶包,
20
+ // 发上去必 InferHub.002002009.404);同级才用历史规则「非小写的精确大小写覆盖小写」。
21
+ function noteKnown(key, id, byModelId) {
22
+ if (!known.has(key)) { known.set(key, id); if (byModelId) knownReal.add(key); return; }
23
+ if (byModelId && !knownReal.has(key)) { known.set(key, id); knownReal.add(key); return; }
24
+ if (!!byModelId === knownReal.has(key) && id !== key && known.get(key) === key) known.set(key, id);
25
+ }
26
+
17
27
  for (const id of [...SEED_KNOWN_MODELS, ...SEED_BENEFIT_MODELS]) {
18
- if (!known.has(lower(id))) known.set(lower(id), id);
28
+ const key = lower(id);
29
+ if (key) noteKnown(key, id, false); // 种子按展示名记账:真 model_id 一发现就覆盖它
19
30
  }
20
31
  const accounts = new Map(); // userId → {models, benefit:Set, knows:Set, lastBenefit:Map, fetched}
21
32
 
@@ -28,7 +39,7 @@ export function createCatalog() {
28
39
  const key = lower(m.id);
29
40
  ac.knows.add(key);
30
41
  if (m.benefit) { ac.benefit.add(key); ac.lastBenefit.set(key, m); }
31
- if (!known.has(key) || m.id !== key) known.set(key, m.id);
42
+ noteKnown(key, m.id, m.byModelId);
32
43
  ac.models.push(m);
33
44
  }
34
45
  if (keepBenefit) {
@@ -38,7 +49,7 @@ export function createCatalog() {
38
49
  ac.benefit.add(key);
39
50
  ac.knows.add(key);
40
51
  ac.models.push(mi);
41
- if (!known.has(key) || mi.id !== key) known.set(key, mi.id);
52
+ noteKnown(key, mi.id, mi.byModelId);
42
53
  }
43
54
  ac.models.sort((a, b) => (lower(a.id) < lower(b.id) ? -1 : lower(a.id) > lower(b.id) ? 1 : (a.id < b.id ? -1 : 1)));
44
55
  }
@@ -72,7 +83,7 @@ export function createCatalog() {
72
83
  return { setAccount, isBenefit, canonical, accountModels, isStale };
73
84
  }
74
85
 
75
- /** 多路合并:按小写 ID 去重,福利标记取或,参数更全/精确大小写优先,按小写 ID 排序。 */
86
+ /** 多路合并:按小写 ID 去重,福利标记取或,真 model_id 优先 > 参数更全 > 精确大小写,按小写 ID 排序。 */
76
87
  export function mergeModels(sets) {
77
88
  const merged = new Map();
78
89
  for (const set of sets || []) {
@@ -81,7 +92,11 @@ export function mergeModels(sets) {
81
92
  const key = lower(m.id);
82
93
  const cur = merged.get(key);
83
94
  if (!cur) { merged.set(key, { ...m }); continue; }
84
- const keep = (m.contextWindow > 0 && !cur.contextWindow) || (m.id !== lower(m.id) && cur.id === lower(cur.id)) ? m : cur;
95
+ // 上游真正的 model_id 优先于展示名:agent-center 那份只有 model_name(DeepSeek-V4.1-Flash),
96
+ // builtin 那份才是可请求的 id(deepseek-v4.1-flash);发错大小写 = InferHub.002002009.404。
97
+ const keep = !!m.byModelId !== !!cur.byModelId
98
+ ? (m.byModelId ? m : cur)
99
+ : ((m.contextWindow > 0 && !cur.contextWindow) || (m.id !== lower(m.id) && cur.id === lower(cur.id)) ? m : cur);
85
100
  const drop = keep === m ? cur : m;
86
101
  merged.set(key, {
87
102
  ...keep,
@@ -130,6 +145,9 @@ const toModelInfo = (m, { benefit = false } = {}) => {
130
145
  if (!id) return null;
131
146
  return {
132
147
  id: String(id),
148
+ // 出处记账:只有真正的 model_id 字段才敢当「发给上游的 id」;靠 model_name/modelAlias
149
+ // 兜底出来的是展示名,上游 InferHub 不认(现网 2026-10-24:DeepSeek-V4.1-Flash 必 404)。
150
+ byModelId: !!m?.model_id,
133
151
  name: String(m?.model_name || id),
134
152
  contextWindow: Number(m?.context_window) || 0,
135
153
  maxTokens: Number(m?.max_tokens) || 0,
@@ -68,6 +68,11 @@ export async function startDaemonMain(VERSION) {
68
68
  try { logs.appendEvent({ ts: Date.now(), type: "raccoon-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
69
69
  });
70
70
 
71
+ const { setupCodeartsCheckin } = await import("./codearts-checkin.js");
72
+ void setupCodeartsCheckin({ bus, logs }).catch((e) => {
73
+ try { logs.appendEvent({ ts: Date.now(), type: "codearts-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
74
+ });
75
+
71
76
  const { setupWorkbuddyGrowth } = await import("./workbuddy-growth.js");
72
77
  void setupWorkbuddyGrowth({ bus, logs }).catch((e) => {
73
78
  try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-growth-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * codearts 每日签到领积分(daemon 内置),仿 src/runtime/qoder-checkin.js。
3
+ * 开关:MSLXDFF_CODEARTS_CHECKIN=0 关闭(默认开);时间:MSLXDFF_CODEARTS_CHECKIN_HOUR(默认 9 点)。
4
+ * 账号源:providerConfigs.codearts.keys 的凭据 blob —— 复用 createCodeartsProvider 的 authPool,
5
+ * 刷新轮转换号**原位写回**(自己写 saveFn 会与 chat 路径的写回逻辑分叉)。
6
+ * 该上游对并发极敏感 → 逐号**串行**;ops 是独立端点,不占 chat 的并发闸(3 会话上限不受影响)。
7
+ * 记账:state.codeartsCheckin {date, at, ok, total, credits, accounts:[…]}(qoder/workbuddy 同款)。
8
+ */
9
+ import { todayKey, nextRunDelayMs, shouldCatchUp } from "./workbuddy-checkin.js";
10
+
11
+ export function isCheckinEnabled(env = process.env) {
12
+ const v = String(env.MSLXDFF_CODEARTS_CHECKIN ?? "1").trim().toLowerCase();
13
+ return !["0", "false", "off", "no"].includes(v);
14
+ }
15
+
16
+ export function getCheckinHour(env = process.env) {
17
+ const h = Number(env.MSLXDFF_CODEARTS_CHECKIN_HOUR);
18
+ return Number.isInteger(h) && h >= 0 && h <= 23 ? h : 9;
19
+ }
20
+
21
+ export async function setupCodeartsCheckin({ bus, logs, deps = {} } = {}) {
22
+ const env = deps.env || process.env;
23
+ function emit(type, data = {}) {
24
+ const entry = { ts: Date.now(), type, ...data };
25
+ try { bus?.emit(entry); } catch { /* 事件不阻断签到 */ }
26
+ try { logs?.appendEvent?.(entry); } catch { /* 同上 */ }
27
+ try { console.log(`[codearts-checkin] ${type} ${JSON.stringify(data)}`.slice(0, 500)); } catch { /* 同上 */ }
28
+ }
29
+ if (!isCheckinEnabled(env)) {
30
+ console.log("[codearts-checkin] disabled (set MSLXDFF_CODEARTS_CHECKIN=1 to enable)");
31
+ emit("codearts-checkin-disabled");
32
+ return { enabled: false };
33
+ }
34
+ const hour = getCheckinHour(env);
35
+ console.log(`[codearts-checkin] enabled: daily ${String(hour).padStart(2, "0")}:00 local, multi-account serial`);
36
+ emit("codearts-checkin-enabled", { hour });
37
+
38
+ const stateMod = await import("../state.js");
39
+
40
+ /** 取号:deps.accounts 优先(测试注入,零网络);否则走 provider 的 authPool(缺省带写回)。 */
41
+ async function loadAccounts() {
42
+ if (Array.isArray(deps.accounts)) return { accounts: deps.accounts, close: null };
43
+ const { createCodeartsProvider } = await import("../providers/codearts/index.js");
44
+ const provider = deps.provider || createCodeartsProvider({ fetchImpl: deps.fetchImpl });
45
+ const pool = provider._pool;
46
+ const accounts = [];
47
+ let candidates = [];
48
+ try { candidates = pool.aliveAccounts() || []; } catch { candidates = []; }
49
+ for (const cand of candidates) {
50
+ try { accounts.push(await pool.getCredential(cand.blob)); } catch { /* 死号跳过,别拖垮整轮 */ }
51
+ }
52
+ return { accounts, close: () => { try { provider.close?.(); } catch { /* ignore */ } } };
53
+ }
54
+
55
+ let running = false;
56
+ async function runOnce(reason) {
57
+ if (running) return { skipped: "already-running" };
58
+ running = true;
59
+ let cleanup = null;
60
+ try {
61
+ const loaded = await loadAccounts();
62
+ cleanup = loaded.close;
63
+ const accounts = loaded.accounts;
64
+ if (!accounts.length) {
65
+ emit("codearts-checkin-no-accounts", { reason });
66
+ return { ok: false, reason: "no-accounts" };
67
+ }
68
+ const { runCheckin } = await import("../providers/codearts/checkin.js");
69
+ const rows = [];
70
+ for (const account of accounts) { // 逐号串行:该上游对并发极敏感
71
+ const uid = String(account?.userId || "").slice(0, 8);
72
+ try {
73
+ const r = await runCheckin({ account, fetchImpl: deps.fetchImpl });
74
+ rows.push({ uid, ok: r.ok, status: r.status, claimed: r.claimed, credits: r.credits, msg: String(r.message || "").slice(0, 140) });
75
+ } catch (e) {
76
+ rows.push({ uid, ok: false, status: "error", claimed: 0, credits: 0, msg: String(e?.message || e).slice(0, 140) });
77
+ }
78
+ }
79
+ const okCount = rows.filter((r) => r.ok).length;
80
+ const credits = rows.reduce((s, r) => s + (r.credits || 0), 0);
81
+ const date = todayKey();
82
+ try {
83
+ stateMod.writeStateImmediate(stateMod.defaultStateFile(), {
84
+ codeartsCheckin: { date, at: Date.now(), ok: okCount, total: rows.length, credits, accounts: rows },
85
+ });
86
+ } catch { /* 落盘失败不影响本次结果 */ }
87
+ for (const r of rows) emit("codearts-checkin-account", { ...r, reason });
88
+ emit("codearts-checkin-done", { date, ok: okCount, total: rows.length, credits, reason });
89
+ return { ok: okCount > 0, okCount, total: rows.length, rows };
90
+ } catch (e) {
91
+ emit("codearts-checkin-failed", { error: String(e?.message || e).slice(0, 200), reason });
92
+ return { ok: false, error: String(e?.message || e).slice(0, 200) };
93
+ } finally {
94
+ try { cleanup?.(); } catch { /* ignore */ }
95
+ running = false;
96
+ }
97
+ }
98
+
99
+ // 启动补跑:已过今日计划点、且(从没签过 或 上次不是今天)
100
+ try {
101
+ const lastDate = stateMod.readState(stateMod.defaultStateFile())?.codeartsCheckin?.date || "";
102
+ if (shouldCatchUp({ lastDate, hour })) {
103
+ setTimeout(() => { runOnce("catch-up").catch(() => {}); }, 90_000).unref?.();
104
+ console.log(`[codearts-checkin] catch-up scheduled (last=${lastDate || "never"})`);
105
+ }
106
+ } catch { /* 读 state 失败就不补跑,等定时 */ }
107
+ function arm() {
108
+ const delay = nextRunDelayMs(new Date(), hour);
109
+ setTimeout(() => {
110
+ runOnce("daily").catch(() => {}).finally(arm);
111
+ }, delay).unref?.();
112
+ }
113
+ arm();
114
+ return { enabled: true, hour, runOnce };
115
+ }