mslxdff 0.1.65 → 0.1.67

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.
Files changed (65) hide show
  1. package/bin/mslxdff.js +3 -3159
  2. package/package.json +1 -1
  3. package/src/chat/cooling.js +99 -0
  4. package/src/chat/direct.js +57 -0
  5. package/src/chat/gateway.js +148 -0
  6. package/src/chat/orchestrator.js +218 -0
  7. package/src/chat/sse.js +69 -0
  8. package/src/chat/upstream.js +59 -501
  9. package/src/cli/bootstrap.js +1 -0
  10. package/src/cli/commands/daemon.js +151 -0
  11. package/src/cli/commands/group.js +272 -0
  12. package/src/cli/commands/model.js +405 -0
  13. package/src/cli/commands/provider/add.js +106 -0
  14. package/src/cli/commands/provider/allowlist.js +99 -0
  15. package/src/cli/commands/provider/config.js +82 -0
  16. package/src/cli/commands/provider/index.js +193 -0
  17. package/src/cli/commands/provider/keys.js +135 -0
  18. package/src/cli/commands/provider/models.js +99 -0
  19. package/src/cli/commands/provider.js +1 -0
  20. package/src/cli/commands/sync.js +143 -0
  21. package/src/cli/commands/system.js +236 -0
  22. package/src/cli/commands/workbuddy.js +91 -0
  23. package/src/cli/format.js +119 -0
  24. package/src/cli/group-helpers.js +69 -0
  25. package/src/cli/help.js +66 -0
  26. package/src/cli/index.js +59 -0
  27. package/src/cli/interactive.js +84 -0
  28. package/src/cli/policy.js +119 -0
  29. package/src/cli/provider-row.js +73 -0
  30. package/src/cli/status.js +283 -0
  31. package/src/cli/util.js +24 -0
  32. package/src/providers/base.js +159 -0
  33. package/src/providers/dispatcher.js +18 -6
  34. package/src/providers/generic.js +28 -166
  35. package/src/providers/openrouter.js +19 -205
  36. package/src/providers/workbuddy/auth.js +175 -0
  37. package/src/providers/workbuddy/balance.js +84 -0
  38. package/src/providers/workbuddy/chat.js +310 -0
  39. package/src/providers/workbuddy/index.js +263 -0
  40. package/src/providers/workbuddy/models.js +111 -0
  41. package/src/providers/workbuddy/rotation-log.js +54 -0
  42. package/src/providers/workbuddy.js +2 -677
  43. package/src/routes/chat/broadband-handler.js +25 -46
  44. package/src/routes/chat/exhausted-handler.js +2 -2
  45. package/src/routes/chat/gateway.js +301 -0
  46. package/src/routes/chat/hedge-handler.js +65 -83
  47. package/src/routes/chat/index.js +1 -384
  48. package/src/routes/chat/local-handler.js +32 -61
  49. package/src/routes/chat/peer-handler.js +32 -23
  50. package/src/routes/chat/relay-pipeline.js +151 -0
  51. package/src/runtime/bootstrap.js +408 -0
  52. package/src/state/facade.js +57 -0
  53. package/src/state/memory.js +161 -0
  54. package/src/state/merge.js +26 -0
  55. package/src/state/persist.js +42 -0
  56. package/src/state/provider-config.js +143 -0
  57. package/src/state/schemas/allowlist.js +87 -0
  58. package/src/state/schemas/group.js +31 -0
  59. package/src/state/schemas/model.js +53 -0
  60. package/src/state/schemas/peer.js +31 -0
  61. package/src/state/schemas/port.js +10 -0
  62. package/src/state/schemas/provider.js +204 -0
  63. package/src/state/schemas/token.js +43 -0
  64. package/src/state/store.js +176 -0
  65. package/src/state.js +1 -711
@@ -0,0 +1,236 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { defaultStateFile } from "../../state.js";
5
+ import { loadToken, refreshToken } from "../../state.js";
6
+ import { stopDaemon, pidFile, logFile } from "../../daemon.js";
7
+ import { logDir, eventsFile, callsFile, errorsFile, recentEvents } from "../../logs.js";
8
+ import { fmtEvent } from "../format.js";
9
+ import { printHelp } from "../help.js";
10
+ import { printStatus } from "../status.js";
11
+ import { loadPlugins, resolvePluginDirs } from "../../plugins.js";
12
+
13
+ export async function handleHelp(args, VERSION) {
14
+ if (!(args.includes("-help") || args.includes("--help") || args.includes("-h"))) return false;
15
+ printHelp(VERSION);
16
+ process.exit(0);
17
+ }
18
+
19
+ export async function handleUpdate(args, VERSION) {
20
+ if (!(args.includes("-update") || args.includes("--update"))) return false;
21
+ const { npmCmd, run } = await import("../util.js");
22
+ const { compareSemver } = await import("../policy.js");
23
+ const { resolvePort } = await import("../../server.js");
24
+ const { readPid } = await import("../../daemon.js");
25
+ const { startDaemon, stopDaemon } = await import("../../daemon.js");
26
+ const { waitForHealth } = await import("../policy.js");
27
+ console.log(`mslxdff v${VERSION} — checking for updates…`);
28
+ const info = await run(npmCmd(), ["view", "mslxdff", "dist-tags.latest", "--json"]);
29
+ if (info.err) {
30
+ console.error(`could not query npm: ${info.err.message || String(info.stderr || "").slice(0, 500)}`);
31
+ process.exit(1);
32
+ }
33
+ let latest = "";
34
+ try {
35
+ latest = JSON.parse(String(info.stdout || "").trim());
36
+ if (Array.isArray(latest)) latest = latest[latest.length - 1];
37
+ latest = String(latest || "").trim();
38
+ } catch {
39
+ const m = String(info.stdout || "").trim().match(/(\d+\.\d+\.\d+[^\s'"]*)/);
40
+ latest = m ? m[1] : "";
41
+ }
42
+ latest = String(latest || "").replace(/['"]/g, "").trim();
43
+ const version = VERSION;
44
+ console.log(` installed: ${version}`);
45
+ console.log(` latest: ${latest || "unknown"}`);
46
+ if (!latest || version === latest || compareSemver(latest, version) <= 0) {
47
+ console.log("already up to date");
48
+ process.exit(0);
49
+ }
50
+ console.log(`updating to ${latest}…`);
51
+ const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`], { stdio: "inherit" });
52
+ if (up.err) {
53
+ console.error(`update failed: ${up.err.message}`);
54
+ process.exit(1);
55
+ }
56
+ console.log(`updated to ${latest}`);
57
+ const daemon = readPid();
58
+ if (daemon) {
59
+ console.log("restarting daemon on the new version…");
60
+ stopDaemon();
61
+ startDaemon([]);
62
+ await waitForHealth(resolvePort(), 4000);
63
+ console.log(`restarted (pid ${readPid()})`);
64
+ }
65
+ process.exit(0);
66
+ }
67
+
68
+ export async function handleRefreshToken(args) {
69
+ if (!(args.includes("-refresh-token") || args.includes("--refresh-token"))) return false;
70
+ const token = await refreshToken();
71
+ console.log(token);
72
+ process.exit(0);
73
+ }
74
+
75
+ export async function handleShowToken(args) {
76
+ if (!(args.includes("-showtoken") || args.includes("--showtoken"))) return false;
77
+ const { token } = await loadToken();
78
+ console.log(token);
79
+ process.exit(0);
80
+ }
81
+
82
+ export async function handleUninstall(args) {
83
+ if (!(args.includes("-uninstall") || args.includes("--uninstall"))) return false;
84
+ const { stopped, pid } = stopDaemon();
85
+ if (stopped) console.log(`mslxdff daemon stopped (pid ${pid})`);
86
+ else console.log("mslxdff daemon not running");
87
+ const stateFile = defaultStateFile();
88
+ const dir = dirname(stateFile);
89
+ const removed = [];
90
+ for (const f of [
91
+ stateFile,
92
+ pidFile(),
93
+ logFile(),
94
+ join(dir, "calls.log"),
95
+ join(dir, "errors.log"),
96
+ join(dir, "events.log"),
97
+ ]) {
98
+ try {
99
+ rmSync(f, { force: true });
100
+ removed.push(f);
101
+ } catch {}
102
+ }
103
+ if (removed.length) console.log(`removed ${removed.length} file(s):\n ${removed.join("\n ")}`);
104
+ else console.log("no state/log files to remove");
105
+ console.log("\npackage still installed — finish with:");
106
+ console.log(" npm uninstall -g mslxdff");
107
+ process.exit(0);
108
+ }
109
+
110
+ export async function handleLog(args) {
111
+ if (!(args.includes("-log") || args.includes("--log") || args.includes("-logs") || args.includes("--logs"))) return false;
112
+ const idx = args.findIndex((x) => x === "-log" || x === "--log" || x === "-logs" || x === "--logs");
113
+ const raw = args[idx + 1];
114
+ const n = Number(raw);
115
+ const count = Number.isInteger(n) && n > 0 ? n : 10;
116
+ const file = eventsFile();
117
+ const dir = logDir();
118
+ console.log(`log dir: ${dir}`);
119
+ console.log(`events: ${file}`);
120
+ const evts = recentEvents(count);
121
+ if (!evts.length) {
122
+ console.log(`(no events yet — file empty or not found)`);
123
+ } else {
124
+ console.log(`--- last ${evts.length} event(s) ---`);
125
+ for (const e of evts) console.log(fmtEvent(e));
126
+ }
127
+ if (count <= 10) {
128
+ console.log(`\nhint: mslxdff -log 100 | calls: ${callsFile()} errors: ${errorsFile()} daemon: ${logFile()}`);
129
+ }
130
+ process.exit(0);
131
+ }
132
+
133
+ export async function handlePlugins(args) {
134
+ if (!(args.includes("-plugins") || args.includes("--plugins"))) return false;
135
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
136
+ const dirs = resolvePluginDirs({ pkgRoot: dirname(pkgRoot) });
137
+ // Actually pkgRoot is src/cli -> need to go to root: bin/mslxdff.js used dirname(dirname(...)). For src/cli/commands/system.js, need 3 levels.
138
+ // Simpler: compute from import.meta.url
139
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
140
+ const dirs2 = resolvePluginDirs({ pkgRoot: root });
141
+ const labels = ["official (bundled)", "user"];
142
+ if (dirs2.length === 1) labels[0] = "dir";
143
+ console.log(`plugin dirs:`);
144
+ dirs2.forEach((d, i) => console.log(` [${labels[i] || `dir${i + 1}`}] ${d}${existsSync(d) ? "" : " (not created yet)"}`));
145
+ const { plugins, errors } = await loadPlugins({ dirs: dirs2 });
146
+ if (!plugins.length && !errors.length) {
147
+ console.log("(no plugins — drop *.mjs files into a dir above, see docs/plugins.md)");
148
+ }
149
+ for (const p of plugins) {
150
+ const hooks = Object.keys(p.hooks || {});
151
+ const src = p.file.startsWith(root) ? "official" : "user";
152
+ console.log(` ${p.name}${p.version ? `@${p.version}` : ""} [${hooks.join(", ") || "no hooks"}] (${src})`);
153
+ if (p.description) console.log(` ${p.description}`);
154
+ }
155
+ for (const e of errors) console.log(` load error: ${e.file} — ${e.error}`);
156
+ process.exit(0);
157
+ }
158
+
159
+ export async function handleChat(args) {
160
+ if (!(args.includes("-chat") || args.includes("--chat"))) return false;
161
+ const idx = args.findIndex((x) => x === "-chat" || x === "--chat");
162
+ const rest = args.slice(idx + 1);
163
+ const singleShot = rest.length ? rest.join(" ").trim() : null;
164
+ const { startChat } = await import("../../chat/index.js");
165
+ await startChat({ singleShot: singleShot || undefined });
166
+ process.exit(0);
167
+ }
168
+
169
+ export async function handleStatus(args, VERSION) {
170
+ if (!(args.includes("-status") || args.includes("--status") || args.includes("-s"))) return false;
171
+ await printStatus(VERSION);
172
+ process.exit(0);
173
+ }
174
+
175
+ export async function handleFree(args) {
176
+ if (!(args.includes("-free") || args.includes("--free") || args.includes("-free-check") || args.includes("--free-check") || args.includes("-free-watch") || args.includes("--free-watch"))) return false;
177
+ const isWatch = args.includes("-free-watch") || args.includes("--free-watch");
178
+ const { fetchV2exFree } = await import("../../free-watcher.js");
179
+ const show = async () => {
180
+ const hits = await fetchV2exFree({ timeoutMs: 6000 });
181
+ const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
182
+ console.log(`[V2EX] free check @ ${ts} — ${hits.length} hit(s)`);
183
+ if (!hits.length) {
184
+ console.log("(暂无命中 — 关键词:白嫖|限免|免费额度|注册送|羊毛,来源:/api/topics/latest.json + hot.json)");
185
+ } else {
186
+ for (const h of hits) console.log(` ${h.title} | ${h.url} | ${h.node} ${h.replies}回复`);
187
+ }
188
+ };
189
+ if (!isWatch) {
190
+ try { await show(); } catch (err) { console.error(`V2EX 拉取失败: ${err?.message || err}`); process.exit(1); }
191
+ process.exit(0);
192
+ }
193
+ console.log("V2EX 白嫖雷达 watch 模式 — 每 5 分钟拉一次 Ctrl+C 退出");
194
+ const run = async () => {
195
+ try { await show(); } catch (err) { console.error(`[${new Date().toISOString().slice(11,19)}] 拉取失败: ${err?.message || err}`); }
196
+ console.log("---");
197
+ };
198
+ await run();
199
+ const timer = setInterval(run, 5 * 60 * 1000);
200
+ timer.unref?.();
201
+ await new Promise(() => {});
202
+ }
203
+
204
+ export async function handleAutostart(args) {
205
+ if (!(args.includes("-enable-autostart") || args.includes("--enable-autostart") || args.includes("-disable-autostart") || args.includes("--disable-autostart") || args.includes("-autostart") || args.includes("--autostart"))) return false;
206
+ const { enableAutostart, disableAutostart, getAutostartStatus, autostartHelp } = await import("../../autostart.js");
207
+ if (args.includes("-enable-autostart") || args.includes("--enable-autostart")) {
208
+ const r = await enableAutostart();
209
+ if (r.ok) {
210
+ console.log(`autostart 已启用 · ${r.method || autostartHelp()}`);
211
+ console.log(`验证: mslxdff -autostart status`);
212
+ } else {
213
+ console.error(`启用自启失败: ${r.error || "unknown"}`);
214
+ console.error(`提示: Windows 需允许任务计划,Linux 需 systemd --user`);
215
+ process.exit(1);
216
+ }
217
+ process.exit(0);
218
+ }
219
+ if (args.includes("-disable-autostart") || args.includes("--disable-autostart")) {
220
+ const r = await disableAutostart();
221
+ if (r.ok) console.log(`autostart 已禁用`);
222
+ else { console.error(`禁用失败: ${r.error}`); process.exit(1); }
223
+ process.exit(0);
224
+ }
225
+ const idx = args.findIndex((x) => x === "-autostart" || x === "--autostart");
226
+ const sub = args[idx + 1];
227
+ if (!sub || sub === "status" || sub === "list") {
228
+ const s = await getAutostartStatus();
229
+ console.log(`autostart: ${s.enabled ? "已启用" : "未启用"} · ${s.detail || autostartHelp()}`);
230
+ if (s.taskToRun) console.log(` task: ${s.taskToRun}`);
231
+ if (s.unit) console.log(` unit: ${s.unit}`);
232
+ process.exit(0);
233
+ }
234
+ console.error("usage: mslxdff -enable-autostart | mslxdff -disable-autostart | mslxdff -autostart status");
235
+ process.exit(1);
236
+ }
@@ -0,0 +1,91 @@
1
+ import { join, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export async function handleWorkbuddy(args) {
5
+ if (!(args.includes("-workbuddy") || args.includes("--workbuddy") || args.includes("-wb"))) return false;
6
+ const idx = args.findIndex((x) => x === "-workbuddy" || x === "--workbuddy" || x === "-wb");
7
+ const sub = args[idx + 1];
8
+ if (!sub || sub === "checkin" || sub === "daily-checkin" || sub === "check-in") {
9
+ const { spawn } = await import("node:child_process");
10
+ const script = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "workbuddy-checkin.js");
11
+ // for src/cli/commands/workbuddy.js -> ../../../workbuddy-checkin.js is root/workbuddy-checkin.js
12
+ const child = spawn(process.execPath, [script, ...args.slice(idx + 2)], { stdio: "inherit" });
13
+ child.on("close", (code) => process.exit(code ?? 0));
14
+ child.on("error", (err) => { console.error(`workbuddy checkin failed: ${err.message}`); process.exit(1); });
15
+ await new Promise(() => {});
16
+ } else if (sub === "balance" || sub === "balances" || sub === "credit" || sub === "credits") {
17
+ const asJson = args.includes("--json") || args.includes("-json");
18
+ const { loadProviderConfigs } = await import("../../state.js");
19
+ const { fetchBalance } = await import("../../providers/workbuddy-balance.js");
20
+ const cfg = loadProviderConfigs().workbuddy || {};
21
+ const auths = Array.isArray(cfg.auths) ? cfg.auths : [];
22
+ const keys = Array.isArray(cfg.keys) ? cfg.keys : [];
23
+ let items = auths.map((a,i)=> ({ uid:a.uid, domain:a.domain, key: keys[i]||"" , auth:a }));
24
+ if (!items.length) {
25
+ try {
26
+ const { readdirSync, readFileSync, existsSync } = await import("node:fs");
27
+ const { join } = await import("node:path");
28
+ const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
29
+ if (existsSync(dir)) {
30
+ for (const f of readdirSync(dir).filter(x=>x.startsWith("workbuddy-")&&x.endsWith(".json"))) {
31
+ try { const j=JSON.parse(readFileSync(join(dir,f),"utf8")); if(j?.account?.uid) items.push({uid:j.account.uid, domain:j.auth.domain||"www.codebuddy.cn", key:j.auth.accessToken, auth:{uid:j.account.uid, domain:j.auth.domain||"www.codebuddy.cn", enterpriseId:j.account.enterpriseId||""}}); } catch {}
32
+ }
33
+ }
34
+ } catch {}
35
+ }
36
+ if (!items.length) { console.log("no workbuddy accounts — run node workbuddy-token-auto.js"); process.exit(0); }
37
+ const results=[];
38
+ for (const it of items) {
39
+ const b = await fetchBalance({ uid: it.uid, key: it.key, auth: it.auth }).catch(()=>null);
40
+ results.push({ uid: it.uid, domain: it.domain, balance: b });
41
+ }
42
+ if (asJson) { console.log(JSON.stringify({ results }, null, 2)); process.exit(0); }
43
+ console.log(`workbuddy balances (${results.length}):`);
44
+ for (const r of results) {
45
+ const b=r.balance;
46
+ if (!b) console.log(` ${r.uid} (balance unavailable) domain=${r.domain}`);
47
+ else console.log(` ${r.uid} total=${b.totalStr||b.total} dailyPacks=${b.dailyPacks} active=${b.activeCount} nextExpire=${b.nextExpire||"-"} domain=${r.domain}`);
48
+ }
49
+ process.exit(0);
50
+ } else if (sub === "list" || sub === "ls" || sub === "status") {
51
+ const { loadProviderConfigs } = await import("../../state.js");
52
+ const cfg = loadProviderConfigs().workbuddy || {};
53
+ const auths = Array.isArray(cfg.auths) ? cfg.auths : [];
54
+ const keys = Array.isArray(cfg.keys) ? cfg.keys : [];
55
+ if (!auths.length) { console.log("no workbuddy accounts in state — check auths/workbuddy-*.json"); process.exit(0); }
56
+ console.log(`workbuddy accounts (${auths.length}):`);
57
+ auths.forEach((a,i)=> {
58
+ const k=(keys[i]||"").slice(0,4);
59
+ console.log(` [${i+1}] uid=${a.uid} domain=${a.domain||"www.codebuddy.cn"} enterprise=${a.enterpriseId||"-"} key=${k?k+"…":"(none)"} refresh=${a.refreshToken?"yes":"no"}`);
60
+ });
61
+ process.exit(0);
62
+ } else if (sub === "remove" || sub === "rm" || sub === "del" || sub === "delete") {
63
+ const target = args[idx+2];
64
+ if (!target) { console.error("usage: mslxdff -workbuddy remove <uid> [--keep-file]"); process.exit(1); }
65
+ const keep = args.includes("--keep-file");
66
+ const { loadProviderConfigs, saveProviderConfig } = await import("../../state.js");
67
+ const cfg = loadProviderConfigs().workbuddy || {};
68
+ let auths = Array.isArray(cfg.auths)? [...cfg.auths]:[];
69
+ let keys = Array.isArray(cfg.keys)? [...cfg.keys]:[];
70
+ let rmIdx = auths.findIndex(a=> a.uid===target || a.uid.startsWith(target));
71
+ if (rmIdx<0) { console.error(`uid not found: ${target}`); process.exit(1); }
72
+ const uid = auths[rmIdx].uid;
73
+ auths.splice(rmIdx,1); keys.splice(rmIdx,1);
74
+ saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl||"https://copilot.tencent.com", keys, auths });
75
+ if (!keep) {
76
+ try {
77
+ const { existsSync, unlinkSync } = await import("node:fs");
78
+ const { join } = await import("node:path");
79
+ const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
80
+ const fp = join(dir, `workbuddy-${uid}.json`);
81
+ if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
82
+ } catch {}
83
+ }
84
+ try { const { getBalanceCache } = await import("../../providers/workbuddy-balance.js"); getBalanceCache().delete(uid); } catch {}
85
+ console.log(`removed workbuddy ${uid} (now ${auths.length} account(s))`);
86
+ process.exit(0);
87
+ } else {
88
+ console.error("usage: mslxdff -workbuddy checkin | balance [--json] | list | remove <uid>");
89
+ process.exit(1);
90
+ }
91
+ }
@@ -0,0 +1,119 @@
1
+ import { fmtShanghai, fmtShanghaiHMS } from "../time.js";
2
+
3
+ export function fmtStatus(id, statuses) {
4
+ const e = statuses[id];
5
+ if (typeof e === "number") return `error ${fmtTs(e)}`;
6
+ if (!e?.status || e.status === "normal") return "";
7
+ const when = e.at ? ` ${fmtTs(e.at)}` : "";
8
+ const code = e.code ? ` HTTP ${e.code}` : "";
9
+ return `${e.status}${when}${code}`;
10
+ }
11
+
12
+ export function fmtDur(ms) {
13
+ if (!Number.isFinite(ms)) return "?";
14
+ return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
15
+ }
16
+
17
+ export function fmtUptime(ms) {
18
+ if (!Number.isFinite(ms) || ms < 0) return "?";
19
+ const s = Math.floor(ms / 1000);
20
+ if (s < 60) return `${s}s`;
21
+ const m = Math.floor(s / 60);
22
+ const h = Math.floor(m / 60);
23
+ const d = Math.floor(h / 24);
24
+ if (d) return `${d}d${h % 24}h`;
25
+ if (h) return `${h}h${m % 60}m`;
26
+ return `${m}m${s % 60}s`;
27
+ }
28
+
29
+ export function fmtEvent(e) {
30
+ const t = e?.ts ? fmtShanghaiHMS(e.ts) : "--:--:--";
31
+ const head = `[${t}]`;
32
+ const m = (x) => x || "-";
33
+ const fallbackTag = e.fallback?.fallback ? ` fallback=${e.fallback.requested_model || e.requested}->${e.fallback.actual_model || e.actual}(${e.fallback.reason || e.reason})` : "";
34
+ switch (e?.type) {
35
+ case "request":
36
+ return `${head} request client-> ${m(e.requested || e.model)}${e.auto ? " (auto)" : ""} raw=${m(e.rawModel)} lock=${m(e.lockModel)} hops=${e.hops} from ${e.ip || "?"}${e.stream ? " stream" : ""}${e.prompt ? ` content="${e.prompt}"` : ""} reqId=${e.reqId || ""}`;
37
+ case "client-request":
38
+ return `${head} client req 客户端请求 model=${m(e.requested)} raw=${m(e.rawModel)} lock=${m(e.lockModel)} ip=${e.ip || "?"}${e.stream ? " stream" : ""}`;
39
+ case "ordered":
40
+ return `${head} ordered 尝试顺序 [${(e.order||[]).join(" -> ")}] canFallback=${e.canFallback} useAuto=${e.useAuto}`;
41
+ case "model-try":
42
+ return `${head} model-try 尝试本地 model=${m(e.model)} idx=${e.idx} reqId=${e.reqId || ""}`;
43
+ case "upstream-try":
44
+ return `${head} upstream try 上游请求 model=${m(e.model)} attempt=${e.attempt || 1}`;
45
+ case "upstream-done":
46
+ return `${head} upstream ok ${m(e.model)} HTTP ${e.status}${e.timing ? ` total=${e.timing.totalMs}ms` : ""}`;
47
+ case "upstream-error":
48
+ return `${head} upstream err ${m(e.model)} ${e.status ? `HTTP ${e.status}` : "network"}: ${m(e.message)}`;
49
+ case "upstream-preheat":
50
+ if (e.skipped) return `${head} preheat 跳过 (MSLXDFF_PREHEAT disabled)`;
51
+ return `${head} preheat 预热 opencode models ${e.ok ? "ok" : "fail"} ${e.status ? `HTTP ${e.status}` : e.error || ""} ${e.ms ? `${e.ms}ms` : ""}`;
52
+ case "peer-race-start":
53
+ return `${head} peer race 开始并发给组员 model=${m(e.model)} peers=${e.peers}`;
54
+ case "peer-health":
55
+ return `${head} peer check ${e.peer} -> ${e.count ? e.healthy.join(", ") : "no healthy models"}${e.strict ? " (strict)" : ""}`;
56
+ case "peer-request":
57
+ return `${head} peer req 并发发出 peer=${e.peer} model=${m(e.model)} hops=${e.hops}`;
58
+ case "peer-forward":
59
+ return `${head} peer resp 收到响应 peer=${e.peer} model=${m(e.model)} ${e.ok ? "ok" : "fail"} ${e.latencyMs ? `${e.latencyMs}ms` : ""} hops=${e.hops}${e.retry ? " (retry)" : ""}`;
60
+ case "peer-error":
61
+ return `${head} peer err ${e.peer} ${e.status ? `HTTP ${e.status}` : "network"}: ${m(e.message)} model=${m(e.model)}`;
62
+ case "peer-race-win":
63
+ return `${head} peer win 选中 peer=${e.winPeer} model=${m(e.winTarget)} latency=${e.latencyMs}ms`;
64
+ case "peer-race-lose":
65
+ return `${head} peer lose 组员全部失败 model=${m(e.model)}`;
66
+ case "fallback":
67
+ return `${head} fallback ${m(e.from)} -> ${m(e.to)} reason=${m(e.reason)}`;
68
+ case "fallback-notice":
69
+ return `${head} fallback! 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} 原因=${m(e.reason)} via=${m(e.via)} notice=${m(e.notice)}`;
70
+ case "relay-try":
71
+ return `${head} relay try 给宽带中继请求 target=${e.target} model=${m(e.model)} via=${e.via}`;
72
+ case "relay-fail":
73
+ return `${head} relay fail ${e.target} ${e.status ? `HTTP ${e.status}` : ""} ${m(e.message)}`;
74
+ case "relay-ip-change":
75
+ return `${head} relay ip ${e.member || e.id || "?"} ${e.oldIp || "?"} -> ${e.newIp || e.publicIp || "?"} via leader`;
76
+ case "relay-forward":
77
+ return `${head} relay fwd ${e.target || e.peer || "?"} via leader model=${m(e.model)} hops=${e.hops || 0}`;
78
+ case "relay-heartbeat":
79
+ return `${head} relay hb ${e.member || "?"} ip=${e.ip || "?"} lastSeen=${e.lastSeen ? fmtShanghaiHMS(e.lastSeen) : "?"}`;
80
+ case "client-abort":
81
+ return `${head} client abort ${m(e.model)} total=${fmtDur(e.totalMs)}`;
82
+ case "result":
83
+ return `${head} result 返回客户端 status=${e.status} model=${m(e.model)} via=${e.via} 响应耗时 ${fmtDur(e.durationMs)}${fallbackTag}`;
84
+ case "client-response":
85
+ return `${head} client res 返回客户端 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} via=${m(e.via)}${e.fallback?.fallback ? ` fallback=${e.fallback.requested_model}->${e.fallback.actual_model}(${e.fallback.reason})` : " 无fallback"} status=${e.status}`;
86
+ case "auto-update-enabled":
87
+ return `${head} auto-update enabled every ${Math.round((e.intervalMs||0)/60000)}m current=${e.current}`;
88
+ case "auto-update-disabled":
89
+ return `${head} auto-update disabled current=${e.current}`;
90
+ case "auto-update-check":
91
+ return `${head} auto-update checking current=${e.current}`;
92
+ case "auto-update-query":
93
+ return `${head} auto-update querying npm current=${e.current}`;
94
+ case "auto-update-queried":
95
+ return `${head} auto-update queried current=${e.current} latest=${e.latest} raw=${(e.stdout||"").slice(0,80)}`;
96
+ case "auto-update-noop":
97
+ return `${head} auto-update noop current=${e.current} latest=${e.latest}${e.reason?` reason=${e.reason}`:""}`;
98
+ case "auto-update-found":
99
+ return `${head} auto-update NEW v${e.current} -> v${e.latest} 发现新版本`;
100
+ case "auto-update-installing":
101
+ return `${head} auto-update installing v${e.latest}...`;
102
+ case "auto-update-installed":
103
+ return `${head} auto-update installed v${e.latest}`;
104
+ case "auto-update-restarting":
105
+ return `${head} auto-update restarting daemon to v${e.latest}...`;
106
+ case "auto-update-restarted":
107
+ return `${head} auto-update restarted pid=${e.newPid} v${e.current} -> v${e.latest} 升级完成`;
108
+ case "auto-update-failed":
109
+ case "auto-update-query-failed":
110
+ case "auto-update-install-failed":
111
+ return `${head} auto-update failed ${e.type} error=${e.error||""}`;
112
+ default:
113
+ return `${head} ${e?.type || "?"} ${JSON.stringify(e || {})}`;
114
+ }
115
+ }
116
+
117
+ export function fmtTs(iso) {
118
+ return fmtShanghai(iso);
119
+ }
@@ -0,0 +1,69 @@
1
+ import { loadToken, loadGroupsJoined, saveGroupsJoined } from "../state.js";
2
+ import { refreshGroupMembers, syncPeersFromMembers } from "../groups.js";
3
+ import { errMsg } from "./util.js";
4
+
5
+ const HEALTH_TIMEOUT_MS = 4000;
6
+
7
+ export function groupIs(action, argv) {
8
+ const idx = argv.indexOf("-group");
9
+ if (idx < 0) return null;
10
+ return argv[idx + 1] === action ? argv[idx + 2] : null;
11
+ }
12
+
13
+ export function markJoined(entry) {
14
+ const { name } = entry;
15
+ if (!entry.kind) entry.kind = "static";
16
+ const list = loadGroupsJoined().filter((g) => g.name !== name);
17
+ saveGroupsJoined([...list, entry]);
18
+ }
19
+
20
+ export async function probeHealth({ id, url, kind, lastSeen, publicIp } = {}) {
21
+ if (!url) return { id, url, fail: "no url", rank: 3 };
22
+ const isBb = kind === "broadband" || String(url).startsWith("relay://");
23
+ if (isBb) {
24
+ const staleMs = Number(process.env.MSLXDFF_BROADBAND_STALE_MS) > 0 ? Number(process.env.MSLXDFF_BROADBAND_STALE_MS) : 90_000;
25
+ const stale = typeof lastSeen === "number" ? Date.now() - lastSeen > staleMs : true;
26
+ return { id, url, kind: "broadband", lastSeen, publicIp, stale, rank: stale ? 2 : 0 };
27
+ }
28
+ const base = String(url).replace(/\/+$/, "");
29
+ const startedAt = Date.now();
30
+ try {
31
+ const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });
32
+ if (!res.ok) return { id, url: base, fail: `HTTP ${res.status}`, rank: 2 };
33
+ return { id, url: base, ms: Date.now() - startedAt, rank: 0 };
34
+ } catch (err) {
35
+ return { id, url: base, fail: errMsg(err), rank: 2 };
36
+ }
37
+ }
38
+
39
+ export async function syncAllJoinedGroups({ peers, groups }) {
40
+ const joined = loadGroupsJoined();
41
+ const myToken = (await loadToken()).token;
42
+ const results = [];
43
+ for (const g of joined) {
44
+ try {
45
+ if (g.leaderUrl) {
46
+ const members = await refreshGroupMembers(g.name, {
47
+ leaderUrl: g.leaderUrl,
48
+ memberName: g.memberName,
49
+ url: g.myUrl,
50
+ token: myToken,
51
+ kind: g.kind,
52
+ });
53
+ results.push({
54
+ name: g.name,
55
+ ...syncPeersFromMembers({ peers, members, myUrl: g.myUrl, group: g.name }),
56
+ });
57
+ } else {
58
+ const members = groups.list()[g.name]?.members ?? {};
59
+ results.push({
60
+ name: g.name,
61
+ ...syncPeersFromMembers({ peers, members, myUrl: g.myUrl, group: g.name, skipIds: ["leader"] }),
62
+ });
63
+ }
64
+ } catch (err) {
65
+ results.push({ name: g.name, error: errMsg(err) });
66
+ }
67
+ }
68
+ return results;
69
+ }
@@ -0,0 +1,66 @@
1
+ export function printHelp(VERSION) {
2
+ console.log(`mslxdff v${VERSION} — OpenCode Free OpenAI-compatible proxy
3
+
4
+ Usage:
5
+ mslxdff start as a background daemon and exit (status + help if one is already running)
6
+ mslxdff -d start as a background daemon
7
+ mslxdff -status show current status (daemon/health/port/config, upstream providers, models + metrics/体检表, autostart/plugins, groups/peers, recent calls with ttfb/tps, last error)
8
+ mslxdff -log [N] show last N events (default 10, e.g. -log 100)
9
+ mslxdff -models interactive picker: ↑/↓ select a model, Enter sets it as the default (non-TTY: plain list)
10
+ mslxdff -model list list the free models this proxy serves (cached)
11
+ mslxdff -model set <id> set the default (preferred) model without the interactive picker
12
+ mslxdff -model status show per-model health status (normal/limit/error)
13
+ mslxdff -model refresh force-refresh the model cache from the upstream
14
+ mslxdff -debug live-follow the daemon event stream (requests, errors, peer forwards)
15
+ mslxdff -stop stop the running daemon
16
+ mslxdff -restart restart the daemon
17
+ mslxdff -uninstall stop the daemon and delete all state/log files
18
+ mslxdff -port N persist the listen port (restarts the daemon on it if running)
19
+ mslxdff -update update mslxdff to the latest published version
20
+ mslxdff -showtoken print the current auth token
21
+ mslxdff -refresh-token rotate the auth token (prints the new one)
22
+ mslxdff -setto workbuddy [modelId] set default model and sync to WorkBuddy models.json (insert or update 127.0.0.1/v1 entry)
23
+ mslxdff -provider add <id> <baseUrl> <key> [allowedModel...] add a generic OpenAI-compatible provider (myapi/gpt-4, baseUrl https://api.example.com/v1; extra models = allowlist)
24
+ mslxdff -provider add workbuddy https://copilot.tencent.com <key> [allow...] add WorkBuddy专用供应商(workbuddy/hy3 前缀路由,auths 与 keys 一一对应)
25
+ mslxdff -provider <id> [key...|add|remove|list|clear|share|set-url] configure provider API keys/URL (multiple keys = rotating, set-url for generic)
26
+ mslxdff -provider <id> allowlist [list|set|add|remove|clear] manage allowed models (empty=allow all, non-empty=only listed)
27
+ mslxdff -workbuddy checkin daily 100 credits(双域幂等,已签 code 10001 视为成功)
28
+ mslxdff -providers list list all configured upstream providers (opencode, openrouter, generic, workbuddy)
29
+ mslxdff -creategroup <name> create a group on this node (the group name is the password)
30
+ mslxdff -addtogroup <leader-host> <name> [--broadband] join a group via its leader host (default port 8989) — broadband: 宽带动态IP成员(经Leader中继,无需公网入站,默认127.0.0.1)
31
+ mslxdff -group sync pull the freshest member list for all joined groups
32
+ mslxdff -group leave <name> leave a group (removes its members from this node)
33
+ mslxdff -group list list groups on this node (numbered members)
34
+ mslxdff -group remove <seq> leader only: kick a member by its list sequence number
35
+ mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
36
+ mslxdff -delgroup <name> disband a group this node leads (deletes it and its members)
37
+ mslxdff -free V2EX 白嫖雷达(仅 V2EX 单源:latest+hot 按 白嫖|限免|免费额度|注册送|羊毛 过滤)
38
+ mslxdff -free-watch V2EX 白嫖雷达 watch 模式(每 5 分钟轮询)
39
+ mslxdff -enable-autostart 开机自启:注册 Windows 任务计划 / Linux systemd user(重启后自动拉起)
40
+ mslxdff -disable-autostart 关闭开机自启
41
+ mslxdff -autostart status 查看自启状态
42
+ mslxdff -chat ["prompt"] chat REPL(mimo-v2.5-free 优先/big-pickle 兜底,自然语言转命令,模糊匹配由模型完成,历史持久化,超长自动压缩,仅拦 -uninstall,daemon 重启不影响)
43
+ mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
44
+ mslxdff -help show this help
45
+
46
+ Environment:
47
+ MSLXDFF_PORT listen port (default 8989; use mslxdff -port N to persist)
48
+ MSLXDFF_STATE_FILE token/port state file
49
+ MSLXDFF_DAEMON_DIR daemon pid/log/models dir
50
+ UPSTREAM_BASE_URL upstream base (default https://opencode.ai)
51
+ UPSTREAM_AUTH_TOKEN upstream bearer value (default "public")
52
+ MSLXDFF_OPENROUTER_KEY openrouter provider API keys(s) (single env value; multiple use "mslxdff -provider openrouter k1 k2 ..." to persist)
53
+ UPSTREAM_CONNECT_TIMEOUT_MS upstream connect timeout (default 30000)
54
+ MODELS_REFRESH_MS model-list background refresh interval (default 7200000)
55
+ MSLXDFF_MODEL_COOLDOWN_MS fallback cooldown after a model error (default 60000)
56
+ MSLXDFF_PEER_COOLDOWN_MS peer failover cooldown (default 30000)
57
+ MSLXDFF_PEER_HEAT_MS how long a peer success stays hot for fast reuse (default 300000)
58
+ MSLXDFF_GROUP_SYNC_MS group membership sync interval (default 60000)
59
+ MSLXDFF_MAX_HOPS max peer-forwarding depth (default 3)
60
+ MSLXDFF_BAN_THRESHOLD failed joins before an ip is banned (default 5)
61
+ MSLXDFF_BAN_WINDOW_MS ban duration after too many failures (default 48h)
62
+ MSLXDFF_HEDGE_DELAY_MS hedge peer race when local stream first chunk slow (default 1000, 0/off to disable)
63
+ MSLXDFF_AUTO_UPDATE auto-update: hourly by default, 0/off/false to disable, 1/true or ms
64
+ MSLXDFF_AUTO_UPDATE_MS same as above, explicit ms (overrides AUTO_UPDATE)
65
+ `);
66
+ }
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8")).version;
7
+
8
+ // deep CLI entry — single inlet: run(args) — hides all command branching inside
9
+ export async function run(args = process.argv.slice(2)) {
10
+ const { handleHelp, handleUpdate, handleRefreshToken, handleShowToken, handleUninstall, handleLog, handlePlugins, handleChat, handleStatus, handleFree, handleAutostart } = await import("./commands/system.js");
11
+ if (await handleHelp(args, VERSION)) return;
12
+ if (await handleUpdate(args, VERSION)) return;
13
+ if (await handleRefreshToken(args)) return;
14
+ if (await handleShowToken(args)) return;
15
+
16
+ const { handleStop, handleRestart } = await import("./commands/daemon.js");
17
+ if (await handleStop(args)) return;
18
+ if (await handleRestart(args, VERSION)) return;
19
+ if (await handleUninstall(args)) return;
20
+ if (await handleLog(args)) return;
21
+ if (await handlePlugins(args)) return;
22
+ if (await handleChat(args)) return;
23
+ if (await handleStatus(args, VERSION)) return;
24
+
25
+ const { handleModel } = await import("./commands/model.js");
26
+ if (await handleModel(args)) return;
27
+ const { handleSetto } = await import("./commands/sync.js");
28
+ if (await handleSetto(args)) return;
29
+ const { handleWorkbuddy } = await import("./commands/workbuddy.js");
30
+ if (await handleWorkbuddy(args)) return;
31
+
32
+ if (await handleAutostart(args)) return;
33
+ if (await handleFree(args)) return;
34
+
35
+ const { handleProviders, handleProvider } = await import("./commands/provider.js");
36
+ if (await handleProviders(args)) return;
37
+ if (await handleProvider(args)) return;
38
+
39
+ const { handleDebug } = await import("./commands/daemon.js");
40
+ await handleDebug(args);
41
+
42
+ const { handleGroupCreate, handleGroupCommand, handleAddToGroup, handleResetBan, handleLeaveGroup, handleDelGroup } = await import("./commands/group.js");
43
+ if (await handleGroupCreate(args)) return;
44
+ if (await handleGroupCommand(args)) return;
45
+ if (await handleAddToGroup(args)) return;
46
+ if (await handleResetBan(args)) return;
47
+ if (await handleLeaveGroup(args)) return;
48
+ if (await handleDelGroup(args)) return;
49
+
50
+ const { handlePort, handleDaemonFlag, handleBareRun } = await import("./commands/daemon.js");
51
+ if (await handlePort(args)) return;
52
+ if (await handleDaemonFlag(args, VERSION)) return;
53
+ if (await handleBareRun(args, VERSION)) return;
54
+
55
+ const { startDaemonMain } = await import("./bootstrap.js");
56
+ await startDaemonMain(VERSION);
57
+ }
58
+
59
+ export { VERSION };