mslxdff 0.1.91 → 0.1.93

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.91",
3
+ "version": "0.1.93",
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;
@@ -65,6 +65,8 @@ export async function handleProvider(args) {
65
65
  }
66
66
  const { handleClineLogin } = await import("./cline-login.js");
67
67
  if (await handleClineLogin(id, sub)) return true;
68
+ const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
69
+ if (await handleWorkbuddyLogin(id, sub, rest)) return true;
68
70
  const { handleProviderConfig } = await import("./config.js");
69
71
  if (await handleProviderConfig(id, sub, rest)) return true;
70
72
  const { handleProviderAllowlist } = await import("./allowlist.js");
@@ -0,0 +1,152 @@
1
+ // mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 clinebot login)。
2
+ // 学自 Sliverkiss/workbuddy2api(cmd/login):POST /v2/plugin/auth/state 拿 state+authUrl →
3
+ // 浏览器登录 → GET /v2/plugin/auth/token?state= 轮询 → GET /v2/plugin/login/account?state= 拿 uid。
4
+ // 不走 whistle/MITM,不依赖 codebuddy CLI,自然支持多账号追加。
5
+
6
+ const BASE = "https://copilot.tencent.com";
7
+ // UA 是上游指纹:集中一处,版本过期时改这里或用 MSLXDFF_WORKBUDDY_UA 覆盖。
8
+ const UA = process.env.MSLXDFF_WORKBUDDY_UA || "CLI/2.63.2 CodeBuddy/2.63.2";
9
+ const ORIGIN = "https://www.codebuddy.cn";
10
+
11
+ function baseHeaders() {
12
+ return {
13
+ "Content-Type": "application/json",
14
+ Accept: "application/json, text/plain, */*",
15
+ "X-Requested-With": "XMLHttpRequest",
16
+ Origin: ORIGIN,
17
+ Referer: `${ORIGIN}/`,
18
+ "User-Agent": UA,
19
+ };
20
+ }
21
+
22
+ async function readEnvelope(res) {
23
+ const txt = await res.text();
24
+ let j;
25
+ try { j = JSON.parse(txt); } catch { throw new Error(`WorkBuddy 返回非 JSON: ${txt.slice(0, 200)}`); }
26
+ return j;
27
+ }
28
+
29
+ export async function requestDeviceState(fetchImpl = globalThis.fetch) {
30
+ const res = await fetchImpl(`${BASE}/v2/plugin/auth/state?platform=CLI`, {
31
+ method: "POST",
32
+ headers: baseHeaders(),
33
+ body: "{}",
34
+ signal: AbortSignal.timeout(20000),
35
+ });
36
+ const j = await readEnvelope(res);
37
+ if (j.code !== 0 || !j.data?.state || !j.data?.authUrl) {
38
+ throw new Error(`获取授权失败: code=${j.code} ${j.msg || ""}`.trim());
39
+ }
40
+ return { state: j.data.state, authUrl: j.data.authUrl };
41
+ }
42
+
43
+ // 单次轮询:pending(业务 code 非 0)返回 null;HTTP 5xx 抛错;成功返回 token bundle。
44
+ export async function pollDeviceToken(fetchImpl = globalThis.fetch, state) {
45
+ let res;
46
+ try {
47
+ res = await fetchImpl(`${BASE}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, {
48
+ headers: baseHeaders(),
49
+ signal: AbortSignal.timeout(20000),
50
+ });
51
+ } catch (e) { throw new Error(`轮询失败: ${e.message}`); }
52
+ if (res.status >= 500) throw new Error(`token 端点故障: HTTP ${res.status}`);
53
+ const j = await readEnvelope(res);
54
+ if (j.code !== 0 || !j.data?.accessToken) return null; // pending: "login ing"
55
+ return {
56
+ accessToken: j.data.accessToken,
57
+ refreshToken: j.data.refreshToken || "",
58
+ expiresIn: Number(j.data.expiresIn) || 5184000,
59
+ domain: j.data.domain || "www.codebuddy.cn",
60
+ };
61
+ }
62
+
63
+ export async function fetchDeviceAccount(fetchImpl = globalThis.fetch, state, accessToken) {
64
+ const res = await fetchImpl(`${BASE}/v2/plugin/login/account?state=${encodeURIComponent(state)}`, {
65
+ headers: { ...baseHeaders(), Authorization: `Bearer ${accessToken}` },
66
+ signal: AbortSignal.timeout(20000),
67
+ });
68
+ const j = await readEnvelope(res);
69
+ if (j.code !== 0 || !j.data?.uid) throw new Error(`获取账号失败: code=${j.code} ${j.msg || ""}`.trim());
70
+ return { uid: j.data.uid, enterpriseId: j.data.enterpriseId || "", nickname: j.data.nickname || "" };
71
+ }
72
+
73
+ // 从本机桌面端登录态直接导入(学自 xiaofan6ya/workbuddy2api):
74
+ // 桌面端明文存着当前登录号的 token,定位逻辑在 desktop-info.js(跨平台发现,不硬编码盘符/厂商目录)。
75
+ // 桌面已登录新号时用这个,无需浏览器、无需抓包。
76
+ export async function importDesktopAccount({ file, localAppData, roots, save } = {}) {
77
+ const { resolveDesktopInfoPath, readDesktopAccount } = await import("../../../providers/workbuddy/desktop-info.js");
78
+ const { homedir } = await import("node:os");
79
+ const { path: fp, via } = resolveDesktopInfoPath({ file, roots, extraRoots: localAppData ? [localAppData] : [], homedir: homedir() });
80
+ const saveFn = save || (await import("../../../providers/workbuddy/account-store.js")).saveWorkbuddyAccount;
81
+ const saved = await saveFn(readDesktopAccount(fp));
82
+ return { ...saved, desktopFile: fp, via };
83
+ }
84
+
85
+ export async function handleWorkbuddyLogin(id, sub, rest = [], deps = {}) {
86
+ if (id !== "workbuddy" && id !== "wb") return false;
87
+ if (sub === "import" || sub === "import-desktop" || sub === "from-desktop") {
88
+ const fileArg = rest.find((x) => String(x).startsWith("--file="))?.slice("--file=".length);
89
+ try {
90
+ const saved = await importDesktopAccount({ file: fileArg, save: deps.saveAccount });
91
+ console.log(`✅ 已从桌面端导入账号(现 ${saved.accounts} 个账号):${saved.file}`);
92
+ console.log("下一步:mslxdff -workbuddy balance 验证余额");
93
+ } catch (e) {
94
+ console.error(`❌ ${e.message}`);
95
+ process.exit(1);
96
+ }
97
+ process.exit(0);
98
+ }
99
+ if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
100
+ const fetchImpl = deps.fetchImpl || globalThis.fetch;
101
+
102
+ console.log("🚀 启动 WorkBuddy 设备授权流程...\n");
103
+ let dev;
104
+ try {
105
+ dev = await requestDeviceState(fetchImpl);
106
+ } catch (e) {
107
+ console.error(`❌ 获取授权失败: ${e.message}`);
108
+ process.exit(1);
109
+ }
110
+ console.log("=".repeat(60));
111
+ console.log("1️⃣ 在浏览器打开下面这个链接(用要添加的账号登录):");
112
+ console.log(` ${dev.authUrl}`);
113
+ console.log("2️⃣ 登录成功后回到这里,无需回车,自动继续");
114
+ console.log("=".repeat(60));
115
+ console.log("\n🔄 等待你登录(自动轮询,每 5s,最多 5 分钟)...\n");
116
+
117
+ const deadline = Date.now() + 5 * 60 * 1000;
118
+ let bundle = null;
119
+ while (Date.now() < deadline) {
120
+ await new Promise((r) => setTimeout(r, 5000));
121
+ try {
122
+ bundle = await pollDeviceToken(fetchImpl, dev.state);
123
+ if (bundle) break;
124
+ process.stdout.write(".");
125
+ } catch (e) {
126
+ console.log(`\n 轮询出错: ${e.message}`);
127
+ }
128
+ }
129
+ if (!bundle) {
130
+ console.error("\n❌ 登录超时,请确认浏览器已完成登录后重跑 mslxdff -provider workbuddy login");
131
+ process.exit(1);
132
+ }
133
+ console.log("\n✅ 授权成功!获取账号信息...");
134
+ let acct;
135
+ try {
136
+ acct = await fetchDeviceAccount(fetchImpl, dev.state, bundle.accessToken);
137
+ } catch (e) {
138
+ console.error(`❌ ${e.message}`);
139
+ process.exit(1);
140
+ }
141
+ const expiresAt = Math.floor(Date.now() / 1000) + bundle.expiresIn;
142
+ const save = deps.saveAccount || (await import("../../../providers/workbuddy/account-store.js")).saveWorkbuddyAccount;
143
+ const saved = await save({ uid: acct.uid, enterpriseId: acct.enterpriseId, nickname: acct.nickname, accessToken: bundle.accessToken, refreshToken: bundle.refreshToken, expiresAt, domain: bundle.domain });
144
+ console.log("=".repeat(60));
145
+ console.log(`✅ 登录成功! 账号: ${acct.nickname || acct.uid} (${acct.uid.slice(0, 8)}…,现 ${saved.accounts} 个账号${saved.updated ? "·旧号凭证已更新" : ""})`);
146
+ console.log("=".repeat(60));
147
+ console.log("\n下一步:");
148
+ console.log(" mslxdff -restart 重启网关使新账号生效");
149
+ console.log(" mslxdff -workbuddy balance 看多号余额(新号次日自动纳入每日签到)");
150
+ console.log("\n多账号:重复 login 追加,每号独立 uid,对话自动轮换");
151
+ process.exit(0);
152
+ }
@@ -0,0 +1,41 @@
1
+ // WorkBuddy 账号落盘:auths/workbuddy-<uid>.json(0600 tmp+rename)+ state.json keys/auths 按 uid 去重追加。
2
+ // token-auto 与 device-login 共用,保证两种入口写出完全一致。
3
+ import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ export function resolveAuthDir() {
8
+ if (process.env.WORKBUDDY_AUTH_DIR) return process.env.WORKBUDDY_AUTH_DIR;
9
+ const sf = process.env.MSLXDFF_STATE_FILE || "";
10
+ if (sf.includes("mslxdff-test") || process.env.NODE_ENV === "test") return join(tmpdir(), "mslxdff-test-auths");
11
+ if (sf && sf.includes("mslxdff-")) return join(dirname(sf), "auths");
12
+ return join(process.cwd(), "auths");
13
+ }
14
+
15
+ export async function saveWorkbuddyAccount({ uid, enterpriseId = "", nickname = "", accessToken, refreshToken = "", expiresAt = 0, domain = "www.codebuddy.cn", file } = {}) {
16
+ if (!uid || !accessToken) throw new Error("saveWorkbuddyAccount: 缺少 uid/accessToken");
17
+ const authDir = resolveAuthDir();
18
+ mkdirSync(authDir, { recursive: true });
19
+ const exp = expiresAt || Math.floor(Date.now() / 1000) + 5184000;
20
+ const doc = { account: { uid, enterpriseId, nickname }, auth: { accessToken, refreshToken, expiresAt: exp, domain } };
21
+ const fp = join(authDir, `workbuddy-${uid}.json`);
22
+ const tmp = `${fp}.tmp`;
23
+ writeFileSync(tmp, JSON.stringify(doc, null, 2), { mode: 0o600 });
24
+ try {
25
+ if (existsSync(fp)) unlinkSync(fp);
26
+ renameSync(tmp, fp);
27
+ } catch {
28
+ writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 });
29
+ }
30
+ // 同步 state(对话轮换只读 state)
31
+ const { loadProviderConfigs, saveProviderConfig } = await import("../../state.js");
32
+ const cfg = (loadProviderConfigs(file ? { file } : {})?.workbuddy) || {};
33
+ const keys = [...(Array.isArray(cfg.keys) ? cfg.keys : [])];
34
+ const auths = [...(Array.isArray(cfg.auths) ? cfg.auths : [])];
35
+ const row = { uid, domain, enterpriseId, refreshToken };
36
+ const atIdx = auths.findIndex((a) => a?.uid === uid);
37
+ if (atIdx >= 0) { keys[atIdx] = accessToken; auths[atIdx] = row; }
38
+ else { keys.push(accessToken); auths.push(row); }
39
+ saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl || "https://copilot.tencent.com", keys, auths }, file ? { file } : {});
40
+ return { file: fp, accounts: keys.length, updated: atIdx >= 0 };
41
+ }
@@ -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
+ }
@@ -0,0 +1,78 @@
1
+ // 桌面端登录态定位:跨机器可用,不硬编码盘符/厂商目录名。
2
+ // 优先级:显式 file > MSLXDFF_WORKBUDDY_DESKTOP_INFO > 已知后缀快查 > 按文件名 BFS(depth≤6,多命中取 mtime 最新)。
3
+ import { existsSync, readdirSync, statSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ export const DESKTOP_INFO_NAME = "workbuddy-desktop.info";
7
+ const KNOWN_SUFFIX = join("CodeBuddyExtension", "Data", "Public", "auth", DESKTOP_INFO_NAME);
8
+ const MAX_DEPTH = 6;
9
+ const MAX_ENTRIES = 30000;
10
+
11
+ export function candidateRoots({ platform = process.platform, env = process.env, homedir = "" } = {}) {
12
+ if (platform === "win32") return [env.LOCALAPPDATA, env.APPDATA].filter(Boolean);
13
+ if (platform === "darwin") return [join(homedir, "Library", "Application Support")].filter(Boolean);
14
+ return [join(homedir, ".config"), join(homedir, ".local", "share")].filter(Boolean);
15
+ }
16
+
17
+ function searchByName(roots) {
18
+ const hits = [];
19
+ let seen = 0;
20
+ for (const root of roots) {
21
+ const queue = [{ dir: root, depth: 0 }];
22
+ while (queue.length && seen < MAX_ENTRIES) {
23
+ const { dir, depth } = queue.shift();
24
+ let names;
25
+ try { names = readdirSync(dir); } catch { continue; }
26
+ for (const n of names) {
27
+ if (++seen > MAX_ENTRIES) break;
28
+ const fp = join(dir, n);
29
+ let st;
30
+ try { st = statSync(fp); } catch { continue; }
31
+ if (st.isFile() && n === DESKTOP_INFO_NAME) hits.push({ path: fp, mtimeMs: st.mtimeMs });
32
+ else if (st.isDirectory() && depth < MAX_DEPTH) queue.push({ dir: fp, depth: depth + 1 });
33
+ }
34
+ }
35
+ }
36
+ hits.sort((a, b) => b.mtimeMs - a.mtimeMs);
37
+ return hits;
38
+ }
39
+
40
+ export function resolveDesktopInfoPath({ file, env = process.env, platform = process.platform, homedir = "", extraRoots = [], roots: rootsArg = null } = {}) {
41
+ const explicit = file || env.MSLXDFF_WORKBUDDY_DESKTOP_INFO;
42
+ if (explicit) {
43
+ if (!existsSync(explicit)) throw new Error(`桌面登录态文件不存在: ${explicit}`);
44
+ return { path: explicit, via: explicit === file ? "file" : "env" };
45
+ }
46
+ const roots = rootsArg ?? [...extraRoots, ...candidateRoots({ platform, env, homedir })];
47
+ for (const r of roots) {
48
+ const fast = join(r, KNOWN_SUFFIX);
49
+ if (existsSync(fast)) return { path: fast, via: "known-path" };
50
+ }
51
+ const hits = searchByName(roots);
52
+ if (hits.length) return { path: hits[0].path, via: "search" };
53
+ throw new Error(
54
+ `找不到桌面端登录态(已搜: ${roots.join("; ") || "无可用根目录"})。` +
55
+ "确认桌面端已登录,或用 --file=路径 / MSLXDFF_WORKBUDDY_DESKTOP_INFO 显式指定 workbuddy-desktop.info"
56
+ );
57
+ }
58
+
59
+ export function readDesktopAccount(fp) {
60
+ let j;
61
+ try {
62
+ j = JSON.parse(readFileSync(fp, "utf8"));
63
+ } catch (e) {
64
+ throw new Error(`读桌面登录态失败: ${fp}(${e.message},确认桌面端已登录)`);
65
+ }
66
+ if (!j?.account?.uid || !j?.auth?.accessToken) throw new Error(`桌面登录态不完整(无 uid/accessToken): ${fp}`);
67
+ let exp = Number(j.auth.expiresAt) || 0;
68
+ if (exp > 1e12) exp = Math.floor(exp / 1000);
69
+ return {
70
+ uid: j.account.uid,
71
+ enterpriseId: "",
72
+ nickname: j.account.nickname || j.account.phoneNumber || "",
73
+ accessToken: j.auth.accessToken,
74
+ refreshToken: j.auth.refreshToken || "",
75
+ expiresAt: exp,
76
+ domain: j.auth.domain || "www.codebuddy.cn",
77
+ };
78
+ }
@@ -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
+ }