mslxdff 0.1.238 → 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.238",
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");
@@ -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
+ }
@@ -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
+ }