mslxdff 0.1.90 → 0.1.92

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.90",
3
+ "version": "0.1.92",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,9 +25,37 @@ export function buildHeadersForProvider(providerId, apiKey, auth) {
25
25
  return h;
26
26
  }
27
27
 
28
- export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, loadKeys, loadAllowed, loadBaseUrl }) {
28
+ // bench 取数口径:只测(allowlist 全局 picks)交集。
29
+ // 两套勾选会分叉(allowlist 管上游放行,picks 是用户 curated 集),直接测 allowlist 会打到未勾选模型、漏掉已勾选模型。
30
+ // picks 为空时原样通过(兼容纯 allowlist 用户)。
31
+ export function filterBenchModels({ providerId, allowed, picks, allowAny = false } = {}) {
32
+ const pid = String(providerId ?? "");
33
+ const isOpen = pid.toLowerCase() === "opencode";
34
+ let list = [...new Set((Array.isArray(allowed) ? allowed : []).map((m) => String(m ?? "").trim()).filter(Boolean))];
35
+ const all = [...new Set((Array.isArray(picks) ? picks : []).map((p) => String(p ?? "").trim()).filter(Boolean))];
36
+ // opencode 默认 allowAny 且 allowlist 常空:候选即裸 picks
37
+ if (!list.length && allowAny && isOpen && all.length) {
38
+ list = [];
39
+ for (const p of all) {
40
+ if (!p.includes("/")) list.push(p);
41
+ else if (p.toLowerCase().startsWith("opencode/")) list.push(p.slice(9));
42
+ }
43
+ list = [...new Set(list.map((m) => m.trim()).filter(Boolean))];
44
+ }
45
+ if (!all.length) return { models: list, skippedUnpicked: 0, pickedBlocked: [] };
46
+ const hit = (m) => all.includes(`${pid}/${m}`) || (isOpen && (all.includes(m) || all.includes(`opencode/${m}`)));
47
+ const models = list.filter(hit);
48
+ const inAllowed = new Set(list);
49
+ const pickedBlocked = all.filter((p) => {
50
+ if (isOpen) return !p.includes("/") && !inAllowed.has(p);
51
+ return p.startsWith(`${pid}/`) && !inAllowed.has(p.slice(pid.length + 1));
52
+ });
53
+ return { models, skippedUnpicked: list.length - models.length, pickedBlocked };
54
+ }
55
+
56
+ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, loadKeys, loadAllowed, loadBaseUrl, loadAllowAny, loadModelPicks, getOnlinePeersFn }) {
29
57
  const { getOnlinePeers, orchestrateVia, resolveIncludeOpencode } = await import("../../../bench/via.js");
30
- const peers = await getOnlinePeers();
58
+ const peers = await (typeof getOnlinePeersFn === "function" ? getOnlinePeersFn() : getOnlinePeers());
31
59
  if (!peers.length) {
32
60
  const msg = "未加入组或无在线 peer,--via 无意义。先 mslxdff -group list / -addtogroup";
33
61
  if (opts.json) console.log(JSON.stringify({ meta: { at: new Date().toISOString(), samples: opts.samples, timeout: opts.timeoutMs, includeOpencode: false, peers: [], opencodeSkipped: true }, results: [], advice: msg }, null, 2));
@@ -47,9 +75,23 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
47
75
  const log = (s) => (opts.json ? console.error(s) : console.log(s));
48
76
  includeOpencode = await resolveIncludeOpencode({ includeOpencode, isTTY, confirmFn, log });
49
77
  }
50
- const { loadToken } = await import("../../../state.js");
78
+ const stateMod = await import("../../../state.js");
79
+ const { loadToken } = stateMod;
51
80
  let token = "";
52
81
  try { token = (await loadToken()).token || ""; } catch {}
82
+ let picks = [];
83
+ try {
84
+ picks = typeof loadModelPicks === "function"
85
+ ? (loadModelPicks() || [])
86
+ : (typeof stateMod.loadModelPicks === "function" ? stateMod.loadModelPicks() || [] : []);
87
+ } catch { picks = []; }
88
+ const allowAnyOf = (pid) => {
89
+ try {
90
+ if (typeof loadAllowAny === "function") return loadAllowAny(pid);
91
+ if (typeof stateMod.loadProviderAllowAnyModels === "function") return stateMod.loadProviderAllowAnyModels(pid);
92
+ } catch {}
93
+ return String(pid || "").toLowerCase() === "opencode";
94
+ };
53
95
  let targetIds = [];
54
96
  const isAll = providerId === "bench" || providerId === "all";
55
97
  if (isAll) {
@@ -68,6 +110,10 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
68
110
  for (const k of Object.keys(raw.providerKeys || {})) if (!targetIds.includes(k) && (loadAllowed(k) || []).length) targetIds.push(k);
69
111
  } catch {}
70
112
  if (!targetIds.length) targetIds = ["openrouter", "workbuddy", "clinebot"].filter((p) => (loadAllowed(p) || []).length);
113
+ // opencode 默认 allowAny 但 allowlist 常空:有裸 picks 且确认包含 opencode 时纳入
114
+ if (includeOpencode && !targetIds.includes("opencode") && picks.some((p) => !String(p).includes("/") || String(p).toLowerCase().startsWith("opencode/"))) {
115
+ targetIds.push("opencode");
116
+ }
71
117
  } else {
72
118
  targetIds = [providerId];
73
119
  }
@@ -80,13 +126,20 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
80
126
  const keys = loadKeys(pid) || [];
81
127
  const allowed = loadAllowed(pid) || [];
82
128
  const baseUrl = (loadBaseUrl(pid) || cfg.baseUrl || "").trim();
83
- if (!allowed.length) { viaLog(`provider ${pid}: 无勾选模型,跳过`); continue; }
129
+ const picked = filterBenchModels({ providerId: pid, allowed, picks, allowAny: allowAnyOf(pid) });
130
+ if (!picked.models.length) {
131
+ if (!allowed.length && !(pid === "opencode" && allowAnyOf(pid))) viaLog(`provider ${pid}: 无勾选模型,跳过`);
132
+ else viaLog(`provider ${pid}: allowlist ${allowed.length} 个模型都不在全局 picks 中,跳过(如需测请 -model pick)`);
133
+ if (picked.pickedBlocked.length) viaLog(` 已勾选但未进 allowlist(先 allowlist set):${picked.pickedBlocked.join(" ")}`);
134
+ continue;
135
+ }
136
+ if (picked.skippedUnpicked) viaLog(`provider ${pid}: 跳过 ${picked.skippedUnpicked} 个未勾选模型,只测 ${picked.models.length} 个已勾选`);
84
137
  if (!baseUrl && pid !== "opencode") { viaLog(`provider ${pid}: missing baseUrl 跳过`); continue; }
85
138
  if (!keys.length && pid !== "opencode") { viaLog(`provider ${pid}: 未配置 Key 跳过`); continue; }
86
139
  const chatPath = cfg.chatPath || defaultChatPath(pid);
87
140
  let auths = [];
88
141
  try { const m = await import("../../../state.js"); auths = m.loadProviderAuths ? m.loadProviderAuths(pid) : []; } catch {}
89
- const models = allowed.map((id) => ({ provider: pid, model: String(id), id: String(id) }));
142
+ const models = picked.models.map((id) => ({ provider: pid, model: String(id), id: String(id) }));
90
143
  const directRunner = async ({ provider, model }) => {
91
144
  const p = provider || pid;
92
145
  const idx = models.findIndex((x) => x.model === model);
@@ -6,7 +6,7 @@ import { isRefreshToken } from "../../../providers/cline/headers.js";
6
6
  import { refreshTokenForBase } from "../../../providers/cline/auth.js";
7
7
  import { clineBenchOne } from "../../../bench/cline-bench.js";
8
8
  import { workbuddyBenchOne } from "../../../bench/workbuddy-bench.js";
9
- import { buildHeadersForProvider, handleVia } from "./bench-via.js";
9
+ import { buildHeadersForProvider, filterBenchModels, handleVia } from "./bench-via.js";
10
10
 
11
11
  function parseBenchArgs(rest) {
12
12
  const opts = { json: false, prompt: "hi", maxTokens: 32, timeoutMs: 30000, via: false, includeOpencode: false, samples: 1, apply: false };
@@ -36,13 +36,14 @@ export async function handleProviderBench(id, sub, rest, args, deps = {}) {
36
36
  if (!isBench) return false;
37
37
  const opts = parseBenchArgs(_restArr);
38
38
  if (opts.via) {
39
- const loadConfigs = deps.loadProviderConfigs || (await import("../../../state.js")).loadProviderConfigs;
40
- const loadKeys = deps.loadProviderKeys || (await import("../../../state.js")).loadProviderKeys;
41
- const loadAllowed = deps.loadProviderAllowedModels || (await import("../../../state.js")).loadProviderAllowedModels;
42
- const loadBaseUrl = deps.loadProviderBaseUrl || (await import("../../../state.js")).loadProviderBaseUrl;
39
+ const stateMod = await import("../../../state.js");
40
+ const loadConfigs = deps.loadProviderConfigs || stateMod.loadProviderConfigs;
41
+ const loadKeys = deps.loadProviderKeys || stateMod.loadProviderKeys;
42
+ const loadAllowed = deps.loadProviderAllowedModels || stateMod.loadProviderAllowedModels;
43
+ const loadBaseUrl = deps.loadProviderBaseUrl || stateMod.loadProviderBaseUrl;
43
44
  const viaPid = _isBenchViaAll ? "bench" : String(id || "").trim();
44
45
  if (!viaPid) { console.error("usage: mslxdff -provider <id> bench --via [--json] [--include-opencode]"); process.exit(1); }
45
- await handleVia({ providerId: viaPid, opts, fetchImpl: deps.fetchImpl || globalThis.fetch, loadConfigs, loadKeys, loadAllowed, loadBaseUrl });
46
+ await handleVia({ providerId: viaPid, opts, fetchImpl: deps.fetchImpl || globalThis.fetch, loadConfigs, loadKeys, loadAllowed, loadBaseUrl, loadAllowAny: deps.loadProviderAllowAnyModels || stateMod.loadProviderAllowAnyModels, loadModelPicks: deps.loadModelPicks, getOnlinePeersFn: deps.getOnlinePeers });
46
47
  return true;
47
48
  }
48
49
  const fetchImpl = deps.fetchImpl || globalThis.fetch;
@@ -58,6 +59,12 @@ export async function handleProviderBench(id, sub, rest, args, deps = {}) {
58
59
  const keys = loadKeys(providerId) || [];
59
60
  const allowed = loadAllowed(providerId) || [];
60
61
  const allowAny = loadAllowAny(providerId);
62
+ let picks = [];
63
+ try {
64
+ const m = await import("../../../state.js");
65
+ picks = typeof deps.loadModelPicks === "function" ? (deps.loadModelPicks() || []) : (typeof m.loadModelPicks === "function" ? m.loadModelPicks() || [] : []);
66
+ } catch { picks = []; }
67
+ const picked = filterBenchModels({ providerId, allowed, picks, allowAny });
61
68
  const baseUrl = (loadBaseUrl(providerId) || cfg.baseUrl || "").trim();
62
69
  let auths = [];
63
70
  try { const m = await import("../../../state.js"); auths = m.loadProviderAuths ? m.loadProviderAuths(providerId) : []; } catch {}
@@ -81,18 +88,26 @@ export async function handleProviderBench(id, sub, rest, args, deps = {}) {
81
88
  if (opts.json) console.log(JSON.stringify({ ok: true, data: list, hint: `pick then bench` }, null, 2));
82
89
  process.exit(0);
83
90
  }
91
+ if (picks.length && !picked.models.length) {
92
+ console.log(`provider ${providerId}: allowlist ${allowed.length} 个模型都不在全局 picks 中,不测速(只测勾选)。`);
93
+ if (picked.pickedBlocked.length) console.log(`已勾选但未进 allowlist:${picked.pickedBlocked.join(" ")} → mslxdff -provider ${providerId} allowlist set <model>`);
94
+ else console.log(`先勾选:mslxdff -model pick ${providerId}/<model>`);
95
+ process.exit(0);
96
+ }
84
97
  const log = (s) => (opts.json ? console.error(s) : console.log(s));
85
- log(`bench ${providerId}: 共 ${allowed.length} 个已勾选模型,逐个测速(串行,${opts.timeoutMs}ms 超时)...`);
98
+ log(`bench ${providerId}: 共 ${picked.models.length} 个已勾选模型,逐个测速(串行,${opts.timeoutMs}ms 超时)...`);
99
+ if (picked.skippedUnpicked) log(`跳过 ${picked.skippedUnpicked} 个未勾选模型`);
86
100
  if (!opts.json) console.log(`prompt="${opts.prompt}" maxTokens=${opts.maxTokens}\n`);
87
101
  const chatPath = cfg.chatPath || defaultChatPath(providerId);
88
102
  const rtKeys = keys.filter((k) => isRefreshToken(k, providerId));
89
103
  const normBase = String(baseUrl).replace(/\/+$/, "");
90
104
  const clineChatBase = normBase.endsWith("/api/v1") ? normBase.slice(0, -7) : normBase;
91
105
  const results = [];
92
- for (let i = 0; i < allowed.length; i++) {
93
- const raw = allowed[i];
106
+ const benchModels = picked.models;
107
+ for (let i = 0; i < benchModels.length; i++) {
108
+ const raw = benchModels[i];
94
109
  const model = String(raw || "").trim();
95
- if (!opts.json) process.stdout.write(` [${i + 1}/${allowed.length}] ${model} ... `);
110
+ if (!opts.json) process.stdout.write(` [${i + 1}/${benchModels.length}] ${model} ... `);
96
111
  const kIdx = i % (keys.length || 1);
97
112
  const aIdx = Math.min(kIdx, Math.max(auths.length - 1, 0));
98
113
  const key = keys[kIdx];
@@ -7,17 +7,48 @@ export async function handleClineLogin(id, sub) {
7
7
  const WORKOS_AUTH = "https://api.workos.com/user_management/authenticate";
8
8
  const CLINE_REGISTER = "https://api.cline.bot/api/v1/auth/register";
9
9
 
10
+ // 网络层:支持代理(读环境变量)+ 超时 + 人话报错。
11
+ // 直连 api.workos.com 在部分网络下 TCP 会被墙(DNS 通但 443 超时),此时必须走代理。
12
+ const PROXY_URL = (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || "").trim();
13
+ let dispatcher = null;
14
+ if (PROXY_URL) {
15
+ try {
16
+ const { ProxyAgent } = await import("undici");
17
+ dispatcher = new ProxyAgent(PROXY_URL);
18
+ } catch (e) {
19
+ console.error(`⚠️ 代理变量 ${PROXY_URL} 不可用(${e.message}),回退直连`);
20
+ }
21
+ }
22
+ function netHint(e, url) {
23
+ const host = (() => { try { return new URL(url).host; } catch { return url; } })();
24
+ const cause = e?.cause || e;
25
+ const code = cause?.code || cause?.name || "";
26
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `DNS 解析失败(${host}),检查 DNS/网络`;
27
+ if (code === "ECONNREFUSED") return `连接被拒(${host}),检查代理是否开启`;
28
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || e?.name === "TimeoutError") return `连接 ${host} 超时(直连可能被墙),请开代理后重试:set HTTPS_PROXY=http://127.0.0.1:7890 再跑 login`;
29
+ if (!PROXY_URL && /workos\.com|cline\.bot/.test(host)) return `直连 ${host} 失败(${code || e.message}),疑似被墙,请开代理后重试`;
30
+ return `${code || e.message}`;
31
+ }
32
+ // 注意:dispatcher 为 null 时不能显式传给 fetch(undici 会断言失败),有代理才带。
33
+ const extraOpts = dispatcher ? { dispatcher } : {};
10
34
  async function postForm(url, form) {
11
35
  const body = new URLSearchParams(form).toString();
12
- const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body });
36
+ let res;
37
+ try {
38
+ res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, ...extraOpts, signal: AbortSignal.timeout(20000) });
39
+ } catch (e) { throw new Error(`连不上 WorkOS:${netHint(e, url)}`); }
13
40
  const txt = await res.text();
14
41
  try { return JSON.parse(txt); } catch { throw new Error(`WorkOS 返回非 JSON: ${txt.slice(0, 200)}`); }
15
42
  }
16
43
  async function postJson(url, obj) {
17
- const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) });
44
+ let res;
45
+ try {
46
+ res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj), ...extraOpts, signal: AbortSignal.timeout(20000) });
47
+ } catch (e) { throw new Error(`连不上 Cline:${netHint(e, url)}`); }
18
48
  const txt = await res.text();
19
49
  try { return JSON.parse(txt); } catch { throw new Error(`Cline 返回非 JSON: ${txt.slice(0, 200)}`); }
20
50
  }
51
+ if (PROXY_URL && dispatcher) console.log(`🌐 检测到代理:${PROXY_URL}\n`);
21
52
 
22
53
  console.log("🚀 启动 Cline WorkOS 设备授权流程...\n");
23
54
  let device;
@@ -4,6 +4,16 @@ import { createTransport } from "../../transport/index.js";
4
4
 
5
5
  function genSessionId() { return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
6
6
 
7
+ // stripProviderPrefix 去掉本地供应商前缀,发上游用裸模型 id。
8
+ // 上游(对标 cline2api-workers MODELS)只认裸 id:"deepseek/deepseek-v4-flash"、
9
+ // "poolside/laguna-s-2.1:free";本地 "clinebot/deepseek/..." 需取后两段。
10
+ // 两段及以内视为已是裸 id,原样透传。
11
+ function stripProviderPrefix(id) {
12
+ const parts = String(id || "").split("/").filter(Boolean);
13
+ if (parts.length > 2) return parts.slice(1).join("/");
14
+ return String(id || "");
15
+ }
16
+
7
17
  function unwrapData(obj) {
8
18
  if (obj && obj.data && typeof obj.data === "object") {
9
19
  const d = obj.data;
@@ -144,7 +154,7 @@ export function createChatService({
144
154
  const model = body?.model || "deepseek/deepseek-v4-flash";
145
155
  const sessionId = genSessionId();
146
156
  const isStream = body?.stream === true;
147
- const upstreamModel = String(model).split("/").pop().includes(":") ? model : model;
157
+ const upstreamModel = stripProviderPrefix(model);
148
158
  const upstreamBody = {
149
159
  model: upstreamModel,
150
160
  max_tokens: body?.max_tokens || body?.max_completion_tokens || 4096,
@@ -171,7 +181,10 @@ export function createChatService({
171
181
  return new Response(JSON.stringify(ret.data), { status: 200, headers: hdrs });
172
182
  }
173
183
  const raw = await resp.json().catch(() => null);
174
- if (!raw) return resp;
184
+ if (!raw || typeof raw !== "object") {
185
+ if (!raw) return resp;
186
+ return new Response(JSON.stringify({ error: { message: "upstream returned non-JSON body", type: "api_error" } }), { status: 502, headers: { "Content-Type": "application/json" } });
187
+ }
175
188
  const normalized = unwrapData(raw);
176
189
  normalized.model = model;
177
190
  return new Response(JSON.stringify(normalized), { status: 200, headers: { "Content-Type": "application/json" } });
@@ -0,0 +1,74 @@
1
+ // WorkBuddy 每日签到 core:纯逻辑 + fetch 注入,被 daemon scheduler 与 CLI 共用。
2
+ // 接口:双域 POST /v2/billing/meter/daily-checkin,code 0=新签成功,10001=已签(幂等成功)。
3
+
4
+ export const CHECKIN_ENDPOINTS = [
5
+ "https://www.codebuddy.cn/v2/billing/meter/daily-checkin",
6
+ "https://copilot.tencent.com/v2/billing/meter/daily-checkin",
7
+ ];
8
+
9
+ function checkinHeaders({ at, uid, domain, enterpriseId }) {
10
+ return {
11
+ Authorization: `Bearer ${at}`,
12
+ "Content-Type": "application/json",
13
+ Accept: "application/json",
14
+ "X-User-Id": uid,
15
+ "X-Domain": domain || "www.codebuddy.cn",
16
+ "X-Product": "SaaS",
17
+ "User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
18
+ Origin: "https://www.codebuddy.cn",
19
+ Referer: "https://www.codebuddy.cn/",
20
+ ...(enterpriseId ? { "X-Enterprise-Id": enterpriseId } : {}),
21
+ };
22
+ }
23
+
24
+ export async function checkinOne({ uid, at, domain, enterpriseId, fetchImpl = globalThis.fetch, timeoutMs = 15000 } = {}) {
25
+ const headers = checkinHeaders({ at, uid, domain, enterpriseId });
26
+ let last = null;
27
+ for (const url of CHECKIN_ENDPOINTS) {
28
+ try {
29
+ const res = await fetchImpl(url, {
30
+ method: "POST",
31
+ headers,
32
+ body: "{}",
33
+ signal: AbortSignal.timeout(timeoutMs),
34
+ });
35
+ const text = await res.text();
36
+ let j;
37
+ try { j = JSON.parse(text); } catch { j = { code: res.status, msg: text.slice(0, 200) }; }
38
+ last = { url, status: res.status, body: j };
39
+ if (j.code === 0 || j.code === 10001 || String(j.msg || "").includes("已签到")) {
40
+ return { ok: true, already: j.code === 10001, ...last };
41
+ }
42
+ // 401/403 = token 过期:标记给上层 refresh,不浪费第二域
43
+ if (res.status === 401 || res.status === 403) return { ok: false, needRefresh: true, ...last };
44
+ } catch (e) {
45
+ last = { url, error: String(e?.message || e).slice(0, 200) };
46
+ }
47
+ }
48
+ return { ok: false, ...last };
49
+ }
50
+
51
+ // 多账号签到:逐个独立,失败不挡别人;返回按 uid 排序的 rows。
52
+ export async function checkinAll({ accounts = [], fetchImpl = globalThis.fetch, concurrency = 3, onAccount } = {}) {
53
+ const list = (Array.isArray(accounts) ? accounts : []).filter((a) => a && a.uid && a.at);
54
+ const results = new Array(list.length);
55
+ let idx = 0;
56
+ async function worker() {
57
+ while (idx < list.length) {
58
+ const i = idx++;
59
+ const a = list[i];
60
+ let row;
61
+ try {
62
+ const r = await checkinOne({ uid: a.uid, at: a.at, domain: a.domain, enterpriseId: a.enterpriseId, fetchImpl });
63
+ row = { uid: a.uid, ok: r.ok, already: !!r.already, needRefresh: !!r.needRefresh, code: r.body?.code, msg: r.body?.msg || r.error || "", url: r.url };
64
+ } catch (e) {
65
+ row = { uid: a.uid, ok: false, msg: String(e?.message || e).slice(0, 200) };
66
+ }
67
+ if (typeof onAccount === "function") { try { await onAccount(row, a); } catch {} }
68
+ results[i] = row;
69
+ }
70
+ }
71
+ await Promise.all(Array.from({ length: Math.min(Math.max(concurrency, 1), Math.max(list.length, 1)) }, () => worker()));
72
+ results.sort((a, b) => String(a.uid).localeCompare(String(b.uid)));
73
+ return results;
74
+ }
@@ -19,4 +19,9 @@ export async function startDaemonMain(VERSION) {
19
19
 
20
20
  const { setupAutoUpdate } = await import("./auto-update.js");
21
21
  setupAutoUpdate({ VERSION, bus, logs });
22
+
23
+ const { setupWorkbuddyCheckin } = await import("./workbuddy-checkin.js");
24
+ void setupWorkbuddyCheckin({ bus, logs }).catch((e) => {
25
+ try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
26
+ });
22
27
  }
@@ -0,0 +1,119 @@
1
+ // daemon 内置 WorkBuddy 每日自动签到(A 方案):多账号全签 + 启动补签 + 每日定时。
2
+ // 开关:MSLXDFF_WORKBUDDY_CHECKIN=0 关闭(默认开);时间:MSLXDFF_WORKBUDDY_CHECKIN_HOUR(默认 9 点本地时)。
3
+ // 幂等:上游 code 10001(今天已签)视为成功;落盘 workbuddyCheckin {date} 防重复。
4
+
5
+ export function isCheckinEnabled(env = process.env) {
6
+ const v = String(env.MSLXDFF_WORKBUDDY_CHECKIN ?? "1").trim().toLowerCase();
7
+ return !["0", "false", "off", "no"].includes(v);
8
+ }
9
+
10
+ export function getCheckinHour(env = process.env) {
11
+ const h = Number(env.MSLXDFF_WORKBUDDY_CHECKIN_HOUR);
12
+ return Number.isInteger(h) && h >= 0 && h <= 23 ? h : 9;
13
+ }
14
+
15
+ export function todayKey(d = new Date()) {
16
+ const m = String(d.getMonth() + 1).padStart(2, "0");
17
+ const day = String(d.getDate()).padStart(2, "0");
18
+ return `${d.getFullYear()}-${m}-${day}`;
19
+ }
20
+
21
+ // 到下一次计划点的毫秒数(今天未到点→今天,已过→明天同时)。
22
+ export function nextRunDelayMs(now = new Date(), hour = 9) {
23
+ const next = new Date(now);
24
+ next.setHours(hour, 0, 0, 0);
25
+ if (next.getTime() <= now.getTime()) next.setDate(next.getDate() + 1);
26
+ return next.getTime() - now.getTime();
27
+ }
28
+
29
+ // 启动补签:已过今日计划点、且(从没签过 或 上次不是今天)。
30
+ export function shouldCatchUp({ lastDate, now = new Date(), hour = 9 } = {}) {
31
+ const point = new Date(now);
32
+ point.setHours(hour, 0, 0, 0);
33
+ if (now.getTime() < point.getTime()) return false;
34
+ return !lastDate || lastDate !== todayKey(now);
35
+ }
36
+
37
+ export async function setupWorkbuddyCheckin({ bus, logs } = {}) {
38
+ function emit(type, data = {}) {
39
+ const entry = { ts: Date.now(), type, ...data };
40
+ try { bus?.emit(entry); } catch {}
41
+ try { logs?.appendEvent?.(entry); } catch {}
42
+ console.log(`[workbuddy-checkin] ${type} ${JSON.stringify(data)}`.slice(0, 500));
43
+ }
44
+ if (!isCheckinEnabled()) {
45
+ console.log("[workbuddy-checkin] disabled (set MSLXDFF_WORKBUDDY_CHECKIN=1 to enable)");
46
+ emit("workbuddy-checkin-disabled");
47
+ return { enabled: false };
48
+ }
49
+ const hour = getCheckinHour();
50
+ console.log(`[workbuddy-checkin] enabled: daily ${String(hour).padStart(2, "0")}:00 local, multi-account`);
51
+ emit("workbuddy-checkin-enabled", { hour });
52
+
53
+ const stateMod = await import("../state.js");
54
+ const { checkinAll } = await import("../providers/workbuddy/checkin.js");
55
+ const { createAuthService, decodeJwtExp } = await import("../providers/workbuddy/auth.js");
56
+
57
+ let running = false;
58
+ async function runOnce(reason) {
59
+ if (running) return { skipped: "already-running" };
60
+ running = true;
61
+ try {
62
+ const cfg = stateMod.loadProviderConfigs().workbuddy || {};
63
+ const keys = [...(Array.isArray(cfg.keys) ? cfg.keys : [])];
64
+ const authList = [...(Array.isArray(cfg.auths) ? cfg.auths : [])];
65
+ if (!keys.length) {
66
+ emit("workbuddy-checkin-no-accounts", { reason });
67
+ return { ok: false, reason: "no-accounts" };
68
+ }
69
+ // 先给过期 token 续期(复用 chat 同款 refresh,落盘回 state)
70
+ const authService = createAuthService({
71
+ fetchImpl: globalThis.fetch,
72
+ store: { keys, authList },
73
+ });
74
+ await Promise.all(authList.map(async (auth, i) => {
75
+ try {
76
+ const exp = typeof decodeJwtExp === "function" ? decodeJwtExp(keys[i]) : null;
77
+ if (exp && exp - Date.now() / 1000 < 3600) await authService.refreshTokenFor(keys[i], auth);
78
+ } catch {}
79
+ }));
80
+ const accounts = authList.map((a, i) => ({ uid: a.uid, at: keys[i], domain: a.domain, enterpriseId: a.enterpriseId })).filter((a) => a.uid && a.at);
81
+ const rows = await checkinAll({ accounts, concurrency: 3 });
82
+ const okCount = rows.filter((r) => r.ok).length;
83
+ const date = todayKey();
84
+ try {
85
+ stateMod.writeStateImmediate(stateMod.defaultStateFile(), {
86
+ workbuddyCheckin: { date, at: Date.now(), ok: okCount, total: rows.length, accounts: rows.map((r) => ({ uid: r.uid, ok: r.ok, already: r.already })) },
87
+ });
88
+ } catch {}
89
+ for (const r of rows) {
90
+ emit("workbuddy-checkin-account", { uid: String(r.uid).slice(0, 8), ok: r.ok, already: !!r.already, msg: String(r.msg || "").slice(0, 120), reason });
91
+ }
92
+ emit("workbuddy-checkin-done", { date, ok: okCount, total: rows.length, reason });
93
+ return { ok: okCount > 0, okCount, total: rows.length, rows };
94
+ } catch (e) {
95
+ emit("workbuddy-checkin-failed", { error: String(e?.message || e).slice(0, 200), reason });
96
+ return { ok: false, error: String(e?.message || e).slice(0, 200) };
97
+ } finally {
98
+ running = false;
99
+ }
100
+ }
101
+
102
+ // 启动补签:昨天没签且已过今天计划点,60s 后跑(等网络/上游就绪)
103
+ try {
104
+ const lastDate = stateMod.readState(stateMod.defaultStateFile())?.workbuddyCheckin?.date || "";
105
+ if (shouldCatchUp({ lastDate, hour })) {
106
+ setTimeout(() => { runOnce("catch-up").catch(() => {}); }, 60_000).unref?.();
107
+ console.log(`[workbuddy-checkin] catch-up scheduled (last=${lastDate || "never"})`);
108
+ }
109
+ } catch {}
110
+ // 每日定时:对齐到下一个计划点,跑完再约 24h 后
111
+ function arm() {
112
+ const delay = nextRunDelayMs(new Date(), hour);
113
+ setTimeout(() => {
114
+ runOnce("daily").catch(() => {}).finally(arm);
115
+ }, delay).unref?.();
116
+ }
117
+ arm();
118
+ return { enabled: true, hour, runOnce };
119
+ }