mslxdff 0.1.57 → 0.1.62
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/bin/mslxdff.js +427 -53
- package/package.json +2 -2
- package/src/auto.js +1 -1
- package/src/chat/prompt.js +3 -1
- package/src/chat/repl.js +131 -7
- package/src/chat/stats.js +83 -30
- package/src/chat/tools.js +3 -3
- package/src/free-watcher.js +62 -0
- package/src/providers/dispatcher.js +4 -1
- package/src/providers/model-id.js +98 -0
- package/src/providers/workbuddy.js +11 -2
- package/src/routes/chat/index.js +9 -2
- package/src/routes/stream.js +58 -1
- package/src/state.js +93 -7
- package/src/sync-workbuddy.js +38 -6
package/bin/mslxdff.js
CHANGED
|
@@ -8,7 +8,8 @@ import { DEFAULT_PORT, defaultStateFile } from "../src/state.js";
|
|
|
8
8
|
import { createRouter } from "../src/routes.js";
|
|
9
9
|
import { createUpstreamClient } from "../src/upstream.js";
|
|
10
10
|
import { createModelsService } from "../src/models.js";
|
|
11
|
-
import {
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroupsJoined, loadModelErrors, savePreferredModel, loadPreferredModel, loadModelPicks, saveModelPicks, loadProviderKey, loadProviderKeys, loadProviderAuths, loadProviderConfigs as loadProviderConfigsState, loadModelStats, loadModelLatencies, loadProviderConfigs, loadProviderAllowedModels, loadProviderShareKeys, loadProviderBaseUrl } from "../src/state.js";
|
|
12
13
|
import { getPreferredModel } from "../src/auto.js";
|
|
13
14
|
import { normalizeModel } from "../src/reasoning.js";
|
|
14
15
|
import { syncToWorkbuddy, workbuddyModelsPath } from "../src/sync-workbuddy.js";
|
|
@@ -305,7 +306,36 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
305
306
|
const pickedIds = loadModelPicks();
|
|
306
307
|
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
307
308
|
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
308
|
-
|
|
309
|
+
// 加载别名映射,显示原始名+别名
|
|
310
|
+
let aliasMap = {};
|
|
311
|
+
let fullAliases = {};
|
|
312
|
+
try {
|
|
313
|
+
const { loadModelAliases, getAliasForModel } = await import("../src/providers/model-id.js");
|
|
314
|
+
loadModelAliases();
|
|
315
|
+
for (const id of ids) {
|
|
316
|
+
const alias = getAliasForModel(id);
|
|
317
|
+
if (alias) aliasMap[id] = alias;
|
|
318
|
+
}
|
|
319
|
+
// 读取完整 alias 表(用于展示本地别名,如 clinebot/*)
|
|
320
|
+
try {
|
|
321
|
+
const aliasesFile = join(homedir(), ".config", "mslxdff", "model-aliases.json");
|
|
322
|
+
const raw = JSON.parse(readFileSync(aliasesFile, "utf8"));
|
|
323
|
+
if (raw && typeof raw === "object") fullAliases = raw;
|
|
324
|
+
} catch {}
|
|
325
|
+
} catch {}
|
|
326
|
+
for (const id of ids) {
|
|
327
|
+
const alias = aliasMap[id];
|
|
328
|
+
const aliasStr = alias ? ` (alias: ${alias})` : "";
|
|
329
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
330
|
+
}
|
|
331
|
+
// 额外展示本地别名(不在上游列表中的,如 clinebot/*)
|
|
332
|
+
const aliasEntries = Object.entries(fullAliases).filter(([alias, canonical]) => !ids.includes(canonical));
|
|
333
|
+
if (aliasEntries.length) {
|
|
334
|
+
console.log(`\nlocal aliases (${aliasEntries.length}):`);
|
|
335
|
+
for (const [alias, canonical] of aliasEntries) {
|
|
336
|
+
console.log(` ${canonical} => ${alias}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
309
339
|
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
310
340
|
} catch (err) {
|
|
311
341
|
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
@@ -543,6 +573,71 @@ if (args.includes("-workbuddy") || args.includes("--workbuddy") || args.includes
|
|
|
543
573
|
}
|
|
544
574
|
}
|
|
545
575
|
|
|
576
|
+
// -enable-autostart / -disable-autostart / -autostart status : 开机自启(Windows schtasks / Linux systemd user)
|
|
577
|
+
if (args.includes("-enable-autostart") || args.includes("--enable-autostart") || args.includes("-disable-autostart") || args.includes("--disable-autostart") || args.includes("-autostart") || args.includes("--autostart")) {
|
|
578
|
+
const { enableAutostart, disableAutostart, getAutostartStatus, autostartHelp } = await import("../src/autostart.js");
|
|
579
|
+
if (args.includes("-enable-autostart") || args.includes("--enable-autostart")) {
|
|
580
|
+
const r = await enableAutostart();
|
|
581
|
+
if (r.ok) {
|
|
582
|
+
console.log(`autostart 已启用 · ${r.method || autostartHelp()}`);
|
|
583
|
+
console.log(`验证: mslxdff -autostart status`);
|
|
584
|
+
} else {
|
|
585
|
+
console.error(`启用自启失败: ${r.error || "unknown"}`);
|
|
586
|
+
console.error(`提示: Windows 需允许任务计划,Linux 需 systemd --user`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
process.exit(0);
|
|
590
|
+
}
|
|
591
|
+
if (args.includes("-disable-autostart") || args.includes("--disable-autostart")) {
|
|
592
|
+
const r = await disableAutostart();
|
|
593
|
+
if (r.ok) console.log(`autostart 已禁用`);
|
|
594
|
+
else { console.error(`禁用失败: ${r.error}`); process.exit(1); }
|
|
595
|
+
process.exit(0);
|
|
596
|
+
}
|
|
597
|
+
// -autostart / -autostart status
|
|
598
|
+
const idx = args.findIndex((x) => x === "-autostart" || x === "--autostart");
|
|
599
|
+
const sub = args[idx + 1];
|
|
600
|
+
if (!sub || sub === "status" || sub === "list") {
|
|
601
|
+
const s = await getAutostartStatus();
|
|
602
|
+
console.log(`autostart: ${s.enabled ? "已启用" : "未启用"} · ${s.detail || autostartHelp()}`);
|
|
603
|
+
if (s.taskToRun) console.log(` task: ${s.taskToRun}`);
|
|
604
|
+
if (s.unit) console.log(` unit: ${s.unit}`);
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
console.error("usage: mslxdff -enable-autostart | mslxdff -disable-autostart | mslxdff -autostart status");
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// -free / -free-check / --free / -free-watch : V2EX 限免白嫖雷达(仅 V2EX 单源)
|
|
612
|
+
if (args.includes("-free") || args.includes("--free") || args.includes("-free-check") || args.includes("--free-check") || args.includes("-free-watch") || args.includes("--free-watch")) {
|
|
613
|
+
const isWatch = args.includes("-free-watch") || args.includes("--free-watch");
|
|
614
|
+
const { fetchV2exFree } = await import("../src/free-watcher.js");
|
|
615
|
+
const show = async () => {
|
|
616
|
+
const hits = await fetchV2exFree({ timeoutMs: 6000 });
|
|
617
|
+
const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
618
|
+
console.log(`[V2EX] free check @ ${ts} — ${hits.length} hit(s)`);
|
|
619
|
+
if (!hits.length) {
|
|
620
|
+
console.log("(暂无命中 — 关键词:白嫖|限免|免费额度|注册送|羊毛,来源:/api/topics/latest.json + hot.json)");
|
|
621
|
+
} else {
|
|
622
|
+
for (const h of hits) console.log(` ${h.title} | ${h.url} | ${h.node} ${h.replies}回复`);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
if (!isWatch) {
|
|
626
|
+
try { await show(); } catch (err) { console.error(`V2EX 拉取失败: ${err?.message || err}`); process.exit(1); }
|
|
627
|
+
process.exit(0);
|
|
628
|
+
}
|
|
629
|
+
console.log("V2EX 白嫖雷达 watch 模式 — 每 5 分钟拉一次 Ctrl+C 退出");
|
|
630
|
+
const run = async () => {
|
|
631
|
+
try { await show(); } catch (err) { console.error(`[${new Date().toISOString().slice(11,19)}] 拉取失败: ${err?.message || err}`); }
|
|
632
|
+
console.log("---");
|
|
633
|
+
};
|
|
634
|
+
await run();
|
|
635
|
+
const timer = setInterval(run, 5 * 60 * 1000);
|
|
636
|
+
timer.unref?.();
|
|
637
|
+
// 保持前台常驻
|
|
638
|
+
await new Promise(() => {});
|
|
639
|
+
}
|
|
640
|
+
|
|
546
641
|
// -providers list : list all deployed upstream providers (opencode + openrouter + generic)
|
|
547
642
|
if (args.includes("-providers") || args.includes("--providers")) {
|
|
548
643
|
const idx = args.findIndex((x) => x === "-providers" || x === "--providers");
|
|
@@ -584,34 +679,40 @@ if (args.includes("-providers") || args.includes("--providers")) {
|
|
|
584
679
|
}
|
|
585
680
|
} catch {}
|
|
586
681
|
const list = [];
|
|
682
|
+
const { loadProviderAllowAnyModels: _la0 } = await import("../src/state.js");
|
|
683
|
+
const opAllowAny = _la0("opencode");
|
|
684
|
+
const orAllowAny = _la0("openrouter");
|
|
587
685
|
const opAllowed = loadProviderAllowedModels("opencode");
|
|
588
|
-
list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, note: "built-in, no key, cannot share" });
|
|
589
|
-
list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, note: orKeys.length ? "" : "no keys" });
|
|
686
|
+
list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
|
|
687
|
+
list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
|
|
590
688
|
for (const gid of [...genericIds].sort()) {
|
|
591
689
|
const cfg = configs[gid];
|
|
592
690
|
const keys = loadProviderKeys(gid);
|
|
593
691
|
const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
|
|
594
692
|
const share = loadProviderShareKeys(gid);
|
|
595
693
|
const allowed = loadProviderAllowedModels(gid);
|
|
694
|
+
const allowAny = _la0(gid);
|
|
596
695
|
const enabled = Boolean(baseUrl && keys.length);
|
|
597
696
|
let note = "";
|
|
598
|
-
if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
|
|
599
|
-
else if (!baseUrl && !allowed.length) note = "
|
|
600
|
-
else if (!
|
|
697
|
+
if (!baseUrl && !keys.length && !allowed.length && allowAny === false) note = "no baseUrl, no keys, BLOCKED (allowAny OFF)";
|
|
698
|
+
else if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
|
|
699
|
+
else if (!baseUrl && !allowed.length && !allowAny) note = "missing baseUrl, BLOCKED";
|
|
700
|
+
else if (!keys.length && !allowed.length && !allowAny) note = "no keys, BLOCKED";
|
|
601
701
|
else if (!baseUrl) note = "missing baseUrl";
|
|
602
702
|
else if (!keys.length) note = "no keys";
|
|
603
|
-
list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, allowed, note });
|
|
703
|
+
list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, allowed, allowAny, note });
|
|
604
704
|
}
|
|
605
705
|
console.log(`providers (${list.length}):`);
|
|
606
706
|
for (const p of list) {
|
|
607
707
|
const state = p.enabled ? "enabled " : "disabled";
|
|
608
708
|
const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
|
|
609
709
|
const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
|
|
610
|
-
const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 3).join(",")}${p.allowed.length > 3 ? "..." : ""})` : "allow=all";
|
|
710
|
+
const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 3).join(",")}${p.allowed.length > 3 ? "..." : ""})` : (p.allowAny ? "allow=all" : "allow=none(BLOCKED)");
|
|
611
711
|
const note = p.note ? ` (${p.note})` : "";
|
|
612
712
|
console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} ${allowInfo.padEnd(22)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
|
|
613
713
|
}
|
|
614
714
|
console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider <id> allowlist set <model...> to restrict`);
|
|
715
|
+
console.log(` mslxdff -provider <id> allowAny on|off (empty allowlist = block or allow all)`);
|
|
615
716
|
process.exit(0);
|
|
616
717
|
}
|
|
617
718
|
console.error("usage: mslxdff -providers list");
|
|
@@ -626,14 +727,15 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
626
727
|
const sub = args[idx + 2];
|
|
627
728
|
const rest = args.slice(idx + 2);
|
|
628
729
|
if (!id) {
|
|
629
|
-
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|clear|share|set-url|allowlist]");
|
|
730
|
+
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|clear|share|set-url|allowlist|allowAny]");
|
|
630
731
|
console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
|
|
631
732
|
console.error(" mslxdff -provider openrouter add sk-4 append one key");
|
|
632
733
|
console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
|
|
633
734
|
console.error(" mslxdff -provider openrouter list list all keys (masked)");
|
|
634
735
|
console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
|
|
635
736
|
console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
|
|
636
|
-
console.error(" mslxdff -provider openrouter allowlist set gpt-4 gpt-3.5 manage allowed models (empty=
|
|
737
|
+
console.error(" mslxdff -provider openrouter allowlist set gpt-4 gpt-3.5 manage allowed models (empty=block unless allowAny ON)");
|
|
738
|
+
console.error(" mslxdff -provider openrouter allowAny on|off empty allowlist = allow all or block all (default OFF, secure)");
|
|
637
739
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx add generic OpenAI-compatible provider");
|
|
638
740
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
|
|
639
741
|
console.error(" mslxdff -provider openrouter interactive hidden input (append)");
|
|
@@ -749,7 +851,14 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
749
851
|
try { const { renameSync, unlinkSync, existsSync } = await import("node:fs"); if (existsSync(fp)) unlinkSync(fp); renameSync(fp + ".tmp", fp); } catch { writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 }); }
|
|
750
852
|
} catch {}
|
|
751
853
|
} else {
|
|
752
|
-
|
|
854
|
+
const trimmed = String(gKey).trim();
|
|
855
|
+
const already = (cur.keys || []).some((k) => String(k).trim() === trimmed);
|
|
856
|
+
if (already) {
|
|
857
|
+
console.log(`key already exists for ${nid} (${trimmed.slice(0, 4)}…${trimmed.slice(-4)}), skipped — still ${cur.keys.length} key(s)`);
|
|
858
|
+
console.log(` use: mslxdff -provider ${nid} list to see keys`);
|
|
859
|
+
process.exit(0);
|
|
860
|
+
}
|
|
861
|
+
keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
|
|
753
862
|
auths = undefined;
|
|
754
863
|
saveProviderConfig(nid, { baseUrl, keys, allowedModels });
|
|
755
864
|
}
|
|
@@ -757,12 +866,14 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
757
866
|
console.log(` baseUrl: ${String(gBase).trim().replace(/\/+$/, "")}`);
|
|
758
867
|
console.log(` keys: ${keys.length} (${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")})`);
|
|
759
868
|
if (allowedModels.length) console.log(` allowedModels: ${allowedModels.length} (${allowedModels.join(", ")})`);
|
|
760
|
-
else console.log(` allowedModels: (none —
|
|
869
|
+
else console.log(` allowedModels: (none — BLOCKED, otherwise unusable) → mslxdff -provider ${nid} allowlist set <model...> OR mslxdff -provider ${nid} allowAny on (allow all)`);
|
|
761
870
|
console.log(` share: ${loadProviderShareKeys(nid) ? "ON" : "off"} (mslxdff -provider ${nid} share on|off)`);
|
|
871
|
+
console.log(` allowAny: OFF (secure, empty allowlist = 403 block before upstream) — enable via: mslxdff -provider ${nid} allowAny on`);
|
|
762
872
|
console.log(` use as: ${nid}/<model-id> — restart daemon to activate`);
|
|
873
|
+
console.log(` NOTE: empty allowlist = 403 before upstream, no cost — must set allowlist to use`);
|
|
763
874
|
process.exit(0);
|
|
764
875
|
}
|
|
765
|
-
if ((id === "opencode" || id === "oc") && sub !== "allowlist" && sub !== "allow" && sub !== "allowed" && sub !== "whitelist" && sub !== "list" && sub !== "status") {
|
|
876
|
+
if ((id === "opencode" || id === "oc") && sub !== "allowlist" && sub !== "allow" && sub !== "allowed" && sub !== "whitelist" && sub !== "list" && sub !== "status" && sub !== "allowAny" && sub !== "allow-any" && sub !== "allow_any" && sub !== "allowany") {
|
|
766
877
|
console.log("opencode is the default (bare) provider — it needs no API key and can never be shared with peers");
|
|
767
878
|
console.log("(its IP-based rate limit is spread by peer forwarding itself)");
|
|
768
879
|
console.log(` allowlist: mslxdff -provider opencode allowlist [list|set|add|remove|clear] — restrict models (empty=allow all)`);
|
|
@@ -809,9 +920,28 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
809
920
|
console.log(`share keys to peers: ${state ? "ON" : "off"} — restart daemon to activate`);
|
|
810
921
|
process.exit(0);
|
|
811
922
|
}
|
|
812
|
-
//
|
|
923
|
+
// allowAnyModels 开关:默认关闭(安全),空 allowlist 时直接 403(防扣费)
|
|
924
|
+
if (sub === "allowAny" || sub === "allow-any" || sub === "allow_any" || sub === "allowany") {
|
|
925
|
+
const { loadProviderAllowAnyModels, saveProviderAllowAnyModels } = await import("../src/state.js");
|
|
926
|
+
const on = rest[1];
|
|
927
|
+
if (!on) {
|
|
928
|
+
const cur = loadProviderAllowAnyModels(id);
|
|
929
|
+
console.log(`allowAnyModels: ${cur ? "ON (allow all when allowlist empty)" : "OFF (empty allowlist = block all)"}`);
|
|
930
|
+
console.log(` set: mslxdff -provider ${id} allowAny on|off`);
|
|
931
|
+
process.exit(0);
|
|
932
|
+
}
|
|
933
|
+
if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
|
|
934
|
+
console.error(`usage: mslxdff -provider ${id} allowAny on|off`);
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
const state = ["on", "1", "true"].includes(String(on).toLowerCase());
|
|
938
|
+
saveProviderAllowAnyModels(id, state);
|
|
939
|
+
console.log(`allowAnyModels: ${state ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = block all)"} — takes effect immediately (hot-reloaded)`);
|
|
940
|
+
process.exit(0);
|
|
941
|
+
}
|
|
942
|
+
// allowlist 管理:默认关闭,白名单为空 = 禁止(需显式 allowAny:true 才放行);非空 = 仅名单内可用(防昂贵模型)
|
|
813
943
|
if (sub === "allowlist" || sub === "allow" || sub === "allowed" || sub === "whitelist") {
|
|
814
|
-
const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig } = await import("../src/state.js");
|
|
944
|
+
const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig, loadProviderAllowAnyModels } = await import("../src/state.js");
|
|
815
945
|
const action = rest[1];
|
|
816
946
|
const rawTargets = rest.slice(2);
|
|
817
947
|
// 兼容:mslxdff -provider <id> allowlist (无 action)→ list
|
|
@@ -819,22 +949,30 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
819
949
|
const list = loadProviderAllowedModels(id);
|
|
820
950
|
const cfg = loadProviderConfig(id);
|
|
821
951
|
const baseUrl = cfg?.baseUrl || "";
|
|
952
|
+
const allowAny = loadProviderAllowAnyModels(id);
|
|
822
953
|
console.log(`provider: ${id}${baseUrl ? ` baseUrl: ${baseUrl}` : ""}`);
|
|
823
954
|
if (!list.length) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
955
|
+
if (allowAny) {
|
|
956
|
+
console.log(` allowedModels: (none — allow all, because allowAny=ON)`);
|
|
957
|
+
console.log(` to secure: mslxdff -provider ${id} allowAny off or mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
958
|
+
} else {
|
|
959
|
+
console.log(` allowedModels: (none — BLOCK ALL, provider disabled until allowlist set or allowAny ON)`);
|
|
960
|
+
console.log(` set via: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
961
|
+
console.log(` or: mslxdff -provider ${id} allowAny on (allow all when allowlist empty)`);
|
|
962
|
+
}
|
|
827
963
|
} else {
|
|
828
964
|
console.log(` allowedModels: ${list.length} model${list.length > 1 ? "s" : ""} (only these can be used)`);
|
|
829
965
|
list.forEach((m, i) => console.log(` [${i + 1}] ${m}`));
|
|
830
966
|
console.log(` manage: mslxdff -provider ${id} allowlist add <model> | remove <model> | set <m1> <m2> ... | clear`);
|
|
967
|
+
console.log(` allowAny: ${allowAny ? "ON" : "OFF"} (empty list behavior) — mslxdff -provider ${id} allowAny on|off`);
|
|
831
968
|
}
|
|
832
|
-
console.log(` NOTE: empty allowlist =
|
|
969
|
+
console.log(` NOTE: empty allowlist + allowAny OFF = 403 block (hot-reloaded, no restart needed)`);
|
|
833
970
|
process.exit(0);
|
|
834
971
|
}
|
|
835
972
|
if (action === "clear") {
|
|
836
973
|
saveProviderAllowedModels(id, []);
|
|
837
|
-
|
|
974
|
+
const allowAny = loadProviderAllowAnyModels(id);
|
|
975
|
+
console.log(`cleared ${id} allowlist (now ${allowAny ? "allow all (allowAny ON)" : "BLOCK ALL (allowAny OFF)"}) — takes effect immediately (hot-reloaded)`);
|
|
838
976
|
process.exit(0);
|
|
839
977
|
}
|
|
840
978
|
if (action === "set") {
|
|
@@ -873,17 +1011,21 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
873
1011
|
const set = new Set(flat);
|
|
874
1012
|
const next = cur.filter((m) => !set.has(m));
|
|
875
1013
|
saveProviderAllowedModels(id, next);
|
|
876
|
-
|
|
1014
|
+
const { loadProviderAllowAnyModels: _la } = await import("../src/state.js");
|
|
1015
|
+
const _allowAny = _la(id);
|
|
1016
|
+
console.log(`removed ${cur.length - next.length} model${cur.length - next.length !== 1 ? "s" : ""} from ${id} allowlist (now ${next.length ? next.join(", ") : (_allowAny ? "(allow all)" : "(BLOCK ALL)")}) — takes effect immediately (hot-reloaded)`);
|
|
877
1017
|
process.exit(0);
|
|
878
1018
|
}
|
|
879
1019
|
console.error(`usage: mslxdff -provider ${id} allowlist [list|set|add|remove|clear] [models...]`);
|
|
1020
|
+
console.error(` mslxdff -provider ${id} allowAny on|off (empty allowlist = block or allow all)`);
|
|
880
1021
|
process.exit(1);
|
|
881
1022
|
}
|
|
882
1023
|
if (sub === "list" || sub === "status") {
|
|
883
1024
|
const keys = loadProviderKeys(id);
|
|
884
1025
|
const cfg = loadProviderConfig(id);
|
|
885
|
-
const { loadProviderAllowedModels } = await import("../src/state.js");
|
|
1026
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("../src/state.js");
|
|
886
1027
|
const allowed = loadProviderAllowedModels(id);
|
|
1028
|
+
const allowAny = loadProviderAllowAnyModels(id);
|
|
887
1029
|
const baseUrl = cfg?.baseUrl || "";
|
|
888
1030
|
if (baseUrl) console.log(`provider: ${id} baseUrl: ${baseUrl}`);
|
|
889
1031
|
else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
|
|
@@ -895,8 +1037,9 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
895
1037
|
console.log(` keys: (no keys configured)`);
|
|
896
1038
|
}
|
|
897
1039
|
console.log(` share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"} (mslxdff -provider ${id} share on|off)`);
|
|
1040
|
+
console.log(` allowAnyModels: ${allowAny ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = BLOCK ALL)"} (mslxdff -provider ${id} allowAny on|off)`);
|
|
898
1041
|
if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
|
|
899
|
-
else console.log(` allowedModels: (none — allow all) (mslxdff -provider ${id} allowlist set <model...>)`);
|
|
1042
|
+
else console.log(` allowedModels: (none — ${allowAny ? "allow all" : "BLOCK ALL"}) (mslxdff -provider ${id} allowlist set <model...> or allowAny on)`);
|
|
900
1043
|
if (baseUrl) console.log(` set url: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
901
1044
|
console.log(` NOTE: opencode is the default provider and can never be shared`);
|
|
902
1045
|
process.exit(0);
|
|
@@ -907,11 +1050,18 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
907
1050
|
console.error("usage: mslxdff -provider openrouter add <key>");
|
|
908
1051
|
process.exit(1);
|
|
909
1052
|
}
|
|
1053
|
+
const trimmed = String(key).trim();
|
|
1054
|
+
const curKeys = loadProviderKeys(id);
|
|
1055
|
+
if (curKeys.some((k) => String(k).trim() === trimmed)) {
|
|
1056
|
+
console.log(`key already exists for ${id} (${trimmed.slice(0, 4)}…${trimmed.slice(-4)}), skipped — still ${curKeys.length} key(s)`);
|
|
1057
|
+
console.log(` use: mslxdff -provider ${id} list to see keys`);
|
|
1058
|
+
process.exit(0);
|
|
1059
|
+
}
|
|
910
1060
|
// 通用供应商:keys 存 providerConfigs,需保留 baseUrl
|
|
911
1061
|
const configs = loadProviderConfigs();
|
|
912
1062
|
if (configs[id]) {
|
|
913
1063
|
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
914
|
-
const keys = [...new Set([...(cur.keys || []),
|
|
1064
|
+
const keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
|
|
915
1065
|
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
|
|
916
1066
|
console.log(`added ${id} API key (now ${keys.length} total) — restart daemon to activate`);
|
|
917
1067
|
} else {
|
|
@@ -1682,6 +1832,13 @@ const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshol
|
|
|
1682
1832
|
|
|
1683
1833
|
const isDebug = process.env.MSLXDFF_DEBUG === "1";
|
|
1684
1834
|
const bus = createEventBus();
|
|
1835
|
+
|
|
1836
|
+
// 加载 WorkBuddy 别名映射(clinebot-z-ai → clinebot/z-ai)
|
|
1837
|
+
try {
|
|
1838
|
+
const { loadModelAliases } = await import("../src/providers/model-id.js");
|
|
1839
|
+
loadModelAliases();
|
|
1840
|
+
} catch {}
|
|
1841
|
+
|
|
1685
1842
|
const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus, plugins: loadedPlugins });
|
|
1686
1843
|
const listenHost = effectiveHost();
|
|
1687
1844
|
const srv = startServer({
|
|
@@ -2051,7 +2208,7 @@ function printHelp() {
|
|
|
2051
2208
|
Usage:
|
|
2052
2209
|
mslxdff start as a background daemon and exit (status + help if one is already running)
|
|
2053
2210
|
mslxdff -d start as a background daemon
|
|
2054
|
-
mslxdff -status show current status (daemon, models, recent calls, last error)
|
|
2211
|
+
mslxdff -status show current status (daemon/health/port/config, upstream providers, models + metrics/体检表, autostart/plugins, groups/peers, recent calls with ttfb/tps, last error)
|
|
2055
2212
|
mslxdff -log [N] show last N events (default 10, e.g. -log 100)
|
|
2056
2213
|
mslxdff -models interactive picker: ↑/↓ select a model, Enter sets it as the default (non-TTY: plain list)
|
|
2057
2214
|
mslxdff -model list list the free models this proxy serves (cached)
|
|
@@ -2080,6 +2237,11 @@ Usage:
|
|
|
2080
2237
|
mslxdff -group remove <seq> leader only: kick a member by its list sequence number
|
|
2081
2238
|
mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
|
|
2082
2239
|
mslxdff -delgroup <name> disband a group this node leads (deletes it and its members)
|
|
2240
|
+
mslxdff -free V2EX 白嫖雷达(仅 V2EX 单源:latest+hot 按 白嫖|限免|免费额度|注册送|羊毛 过滤)
|
|
2241
|
+
mslxdff -free-watch V2EX 白嫖雷达 watch 模式(每 5 分钟轮询)
|
|
2242
|
+
mslxdff -enable-autostart 开机自启:注册 Windows 任务计划 / Linux systemd user(重启后自动拉起)
|
|
2243
|
+
mslxdff -disable-autostart 关闭开机自启
|
|
2244
|
+
mslxdff -autostart status 查看自启状态
|
|
2083
2245
|
mslxdff -chat ["prompt"] chat REPL(mimo-v2.5-free 优先/big-pickle 兜底,自然语言转命令,模糊匹配由模型完成,历史持久化,超长自动压缩,仅拦 -uninstall,daemon 重启不影响)
|
|
2084
2246
|
mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
|
|
2085
2247
|
mslxdff -help show this help
|
|
@@ -2108,11 +2270,210 @@ Environment:
|
|
|
2108
2270
|
|
|
2109
2271
|
async function printStatus() {
|
|
2110
2272
|
const daemon = readPid();
|
|
2273
|
+
const alive = daemon ? isPidAlive(daemon) : false;
|
|
2111
2274
|
const port = getPort() || resolvePort();
|
|
2275
|
+
const persisted = getPort();
|
|
2276
|
+
const portSrc = persisted != null ? "persisted" : (process.env.MSLXDFF_PORT ? "env MSLXDFF_PORT" : "default 8989");
|
|
2277
|
+
const stateFile = defaultStateFile();
|
|
2278
|
+
const dir = logDir();
|
|
2279
|
+
// daemon uptime from pid file mtime
|
|
2280
|
+
let upStr = "";
|
|
2281
|
+
if (daemon && alive) {
|
|
2282
|
+
try {
|
|
2283
|
+
const st = statSync(pidFile());
|
|
2284
|
+
const ms = Date.now() - st.mtimeMs;
|
|
2285
|
+
upStr = `, up ${fmtUptime(ms)}`;
|
|
2286
|
+
} catch {}
|
|
2287
|
+
}
|
|
2288
|
+
const verNote = daemon && alive ? (() => { const v = readPidVersion(); return v === VERSION ? "version ok" : (v ? `version ${v} → ${VERSION} (restart pending)` : ""); })() : "";
|
|
2112
2289
|
console.log(`mslxdff v${VERSION}`);
|
|
2113
|
-
console.log(`daemon: ${daemon ? `running (pid ${daemon})` : "not running"}`);
|
|
2114
|
-
|
|
2115
|
-
|
|
2290
|
+
console.log(`daemon: ${daemon ? (alive ? `running (pid ${daemon}${upStr})` : `stale pid ${daemon} (not alive)`) : "not running"}${verNote ? ` [${verNote}]` : ""}`);
|
|
2291
|
+
// local health probe — 1.2s timeout, 显示端到端可用性
|
|
2292
|
+
let healthLine = "";
|
|
2293
|
+
try {
|
|
2294
|
+
const t0 = Date.now();
|
|
2295
|
+
const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1200) });
|
|
2296
|
+
const ms = Date.now() - t0;
|
|
2297
|
+
healthLine = r.ok ? `health ok ${ms}ms` : `health HTTP ${r.status} ${ms}ms`;
|
|
2298
|
+
} catch (e) {
|
|
2299
|
+
healthLine = daemon && alive ? `health fail (${String(e?.message || e).slice(0, 60)})` : "health — (daemon not running)";
|
|
2300
|
+
}
|
|
2301
|
+
console.log(`endpoint: http://localhost:${port}/v1 · ${healthLine}`);
|
|
2302
|
+
console.log(`config: port ${port} (${portSrc}) · state ${stateFile} · log ${dir}`);
|
|
2303
|
+
try {
|
|
2304
|
+
const host = process.env.MSLXDFF_HOST || process.env.MSLXDFF_BIND_HOST || "0.0.0.0";
|
|
2305
|
+
if (host !== "0.0.0.0") console.log(`bind: ${host}`);
|
|
2306
|
+
} catch {}
|
|
2307
|
+
|
|
2308
|
+
// ---- upstream providers(用户最想要的一块)----
|
|
2309
|
+
try {
|
|
2310
|
+
const configs = loadProviderConfigs();
|
|
2311
|
+
const upstreamBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
2312
|
+
const providerRows = [];
|
|
2313
|
+
// opencode 恒 enabled
|
|
2314
|
+
const opAllowed = loadProviderAllowedModels("opencode");
|
|
2315
|
+
providerRows.push({ id: "opencode", enabled: true, baseUrl: upstreamBase, keys: [], allowed: opAllowed, share: false, note: "built-in, no key, cannot share" });
|
|
2316
|
+
// collect generic ids (configs + legacy providerKeys + env)
|
|
2317
|
+
const genericIds = new Set(Object.keys(configs).filter((id) => id !== "opencode"));
|
|
2318
|
+
try {
|
|
2319
|
+
const raw = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
2320
|
+
const pk = raw.providerKeys || {};
|
|
2321
|
+
for (const id of Object.keys(pk)) if (id !== "opencode") genericIds.add(id);
|
|
2322
|
+
const cfgRaw = raw.providerConfigs || {};
|
|
2323
|
+
for (const id of Object.keys(cfgRaw)) if (id !== "opencode") genericIds.add(id);
|
|
2324
|
+
} catch {}
|
|
2325
|
+
for (const k of Object.keys(process.env)) {
|
|
2326
|
+
const m = k.match(/^MSLXDFF_(.+)_KEY$/);
|
|
2327
|
+
if (m) {
|
|
2328
|
+
const id = m[1].toLowerCase().replace(/__/g, "-");
|
|
2329
|
+
if (id !== "opencode") genericIds.add(id);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
for (const gid of [...genericIds].sort()) {
|
|
2333
|
+
const cfg = configs[gid];
|
|
2334
|
+
const keys = loadProviderKeys(gid);
|
|
2335
|
+
const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || (gid === "openrouter" ? "https://openrouter.ai/api/v1" : gid === "workbuddy" ? "https://copilot.tencent.com" : "");
|
|
2336
|
+
const share = loadProviderShareKeys(gid);
|
|
2337
|
+
const allowed = loadProviderAllowedModels(gid);
|
|
2338
|
+
let enabled = Boolean(baseUrl && keys.length) || (gid === "openrouter" && keys.length > 0);
|
|
2339
|
+
const auths = gid === "workbuddy" ? (loadProviderAuths(gid) || []) : [];
|
|
2340
|
+
let note = "";
|
|
2341
|
+
// 检测测试桩:workbuddy 的 k-new / 127.0.0.1 / 短 key 均视为未真实配置
|
|
2342
|
+
const isWorkbuddyStub = gid === "workbuddy" && (keys.includes("k-new") || baseUrl.includes("127.0.0.1") || (keys.length === 1 && keys[0].length < 20));
|
|
2343
|
+
if (isWorkbuddyStub) {
|
|
2344
|
+
enabled = false;
|
|
2345
|
+
note = "测试桩 (key=k-new, baseUrl=127.0.0.1) — 请重跑 node workbuddy-token-auto.js 写入真实 JWT";
|
|
2346
|
+
} else if (!enabled) {
|
|
2347
|
+
if (!baseUrl && !keys.length) note = "no baseUrl, no keys";
|
|
2348
|
+
else if (!baseUrl) note = "missing baseUrl";
|
|
2349
|
+
else if (!keys.length) note = "no keys";
|
|
2350
|
+
} else if (gid === "workbuddy" && auths.length && auths.length !== keys.length) {
|
|
2351
|
+
note = `${auths.length} auth(s) / ${keys.length} key(s) — 数量不一致请重跑 workbuddy-token-auto.js`;
|
|
2352
|
+
}
|
|
2353
|
+
providerRows.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, allowed, share, note, authCount: auths.length });
|
|
2354
|
+
}
|
|
2355
|
+
const enabledCount = providerRows.filter((r) => r.enabled).length;
|
|
2356
|
+
console.log(`\nupstream providers (${providerRows.length}, ${enabledCount} enabled) — mslxdff -providers list 查看详情`);
|
|
2357
|
+
for (const p of providerRows) {
|
|
2358
|
+
const dot = p.enabled ? "●" : "○";
|
|
2359
|
+
const state = p.enabled ? "enabled " : "disabled";
|
|
2360
|
+
let keysInfo;
|
|
2361
|
+
if (p.id === "opencode") keysInfo = "无需 key (内置)";
|
|
2362
|
+
else keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 3)}…${k.slice(-3)}`).join(", ")}` : "0 keys";
|
|
2363
|
+
const authInfo = p.authCount ? ` ${p.authCount} acc` : "";
|
|
2364
|
+
const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 2).join(",")}${p.allowed.length > 2 ? "…" : ""})` : "allow=all";
|
|
2365
|
+
const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
|
|
2366
|
+
const note = p.note ? ` (${p.note})` : "";
|
|
2367
|
+
console.log(` ${dot} ${p.id.padEnd(12)} ${state} ${keysInfo}${authInfo} ${allowInfo.padEnd(18)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
|
|
2368
|
+
}
|
|
2369
|
+
if (enabledCount === 1 && providerRows.length === 1) {
|
|
2370
|
+
console.log(` (仅 opencode 内置免费通道;按需加:mslxdff -provider add bai https://api.b.ai/v1 <key> 或 node workbuddy-token-auto.js)`);
|
|
2371
|
+
}
|
|
2372
|
+
} catch (e) {
|
|
2373
|
+
console.log(`\nupstream providers: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// ---- models:preferred + picks + 免费缓存 + 体检表 ----
|
|
2377
|
+
try {
|
|
2378
|
+
const statuses = loadModelErrors();
|
|
2379
|
+
const stats = loadModelStats();
|
|
2380
|
+
const picks = loadModelPicks();
|
|
2381
|
+
const preferred = getPreferredModel();
|
|
2382
|
+
const modelsFile = join(dir, "models.json");
|
|
2383
|
+
let freeIds = [];
|
|
2384
|
+
let cachedAt = null;
|
|
2385
|
+
let cacheErr = null;
|
|
2386
|
+
if (existsSync(modelsFile)) {
|
|
2387
|
+
try {
|
|
2388
|
+
const cached = JSON.parse(readFileSync(modelsFile, "utf8"));
|
|
2389
|
+
freeIds = (cached.data || []).map((m) => m.id).filter(Boolean);
|
|
2390
|
+
cachedAt = cached.cachedAt || null;
|
|
2391
|
+
} catch (err) { cacheErr = String(err?.message || err).slice(0, 60); }
|
|
2392
|
+
}
|
|
2393
|
+
const ageStr = cachedAt ? (() => { const ms = Date.now() - cachedAt; const h = Math.floor(ms / 3600000); const m = Math.floor((ms % 3600000) / 60000); return h ? `${h}h${m}m ago` : `${m}m ago`; })() : "";
|
|
2394
|
+
const cacheLine = freeIds.length ? `${freeIds.length} free${cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)} · ${ageStr})` : ""}` : (cacheErr ? `cache unreadable (${cacheErr})` : "not cached yet (daemon 拉取后出现)");
|
|
2395
|
+
const prefStat = preferred ? (stats[preferred] || stats[`opencode/${preferred}`] || null) : null;
|
|
2396
|
+
const prefErr = preferred ? statuses[preferred] : null;
|
|
2397
|
+
const prefStatus = prefErr ? (typeof prefErr === "number" ? "error" : (prefErr?.status || "error")) : (prefStat ? "normal" : "");
|
|
2398
|
+
const prefLine = preferred ? `${preferred}${prefStatus ? ` [${prefStatus}]` : ""}` : "(none)";
|
|
2399
|
+
const picksLine = picks.length ? `${picks.length} (*${picks.slice(0, 4).join(", *")}${picks.length > 4 ? ` …+${picks.length - 4}` : ""})` : "(空=全量 auto)";
|
|
2400
|
+
console.log(`\nmodels: ${cacheLine} — mslxdff -model status 查看详情`);
|
|
2401
|
+
const prefTtfbDisp = prefStat ? (() => { const v = prefStat.avgTtfbMs ?? prefStat.emaTtfbMs; return v != null && v >= 10 ? `首字 ${v}ms` : (v != null && v < 10 ? "首字 — (测试数据)" : ""); })() : "";
|
|
2402
|
+
const prefTotDisp = prefStat?.avgTotalMs && prefStat.avgTotalMs >= 10 ? `总 ${prefStat.avgTotalMs}ms` : "";
|
|
2403
|
+
const prefTpsDisp = prefStat?.avgTps && prefStat.avgTps >= 1 ? `${prefStat.avgTps} tok/s` : "";
|
|
2404
|
+
const prefExtra = prefStat ? ` ${[prefTtfbDisp, prefTotDisp, prefTpsDisp].filter(Boolean).join(" ")} ${prefStat.count || 0}次` : "";
|
|
2405
|
+
console.log(` preferred: ${prefLine}${prefExtra}`);
|
|
2406
|
+
console.log(` picks: ${picksLine} ${picks.length ? "auto 仅在勾选内" : "auto 用全量免费池"}`);
|
|
2407
|
+
if (freeIds.length) {
|
|
2408
|
+
const ids = freeIds;
|
|
2409
|
+
console.log(` free list:`);
|
|
2410
|
+
for (const id of ids) {
|
|
2411
|
+
const st = fmtStatus(id, statuses);
|
|
2412
|
+
const full = id.includes("/") ? id : `opencode/${id}`;
|
|
2413
|
+
const ms = stats[full] || stats[id];
|
|
2414
|
+
let extra = "";
|
|
2415
|
+
if (ms) {
|
|
2416
|
+
const v = ms.avgTtfbMs ?? ms.emaTtfbMs;
|
|
2417
|
+
const vStr = v != null ? (v >= 10 ? `avg首字 ${v}ms` : (v < 10 ? "avg首字 —" : "")) : "";
|
|
2418
|
+
const tpsStr = ms.avgTps && ms.avgTps >= 1 ? ` ${ms.avgTps} tok/s` : "";
|
|
2419
|
+
const cntStr = ms.count ? ` ${ms.count}次` : "";
|
|
2420
|
+
extra = ` ${vStr}${tpsStr}${cntStr}`;
|
|
2421
|
+
}
|
|
2422
|
+
console.log(` ${id}${st ? ` [${st}]` : ""}${extra}`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
// 体检表 Top(按样本数)—— 来自 modelStats,更能反映“跑得快不快/啰不啰嗦”
|
|
2426
|
+
const allStatIds = Object.keys(stats);
|
|
2427
|
+
if (allStatIds.length) {
|
|
2428
|
+
const sorted = allStatIds.map((full) => ({ full, s: stats[full] })).filter((x) => x.s && x.s.count).sort((a, b) => (b.s.count - a.s.count) || ((b.s.avgTps || 0) - (a.s.avgTps || 0))).slice(0, 7);
|
|
2429
|
+
if (sorted.length) {
|
|
2430
|
+
console.log(` 模型体检 Top${sorted.length}(样本>0, 按次数):`);
|
|
2431
|
+
console.log(` ${"模型".padEnd(28)} ${"首字".padEnd(7)} ${"总耗时".padEnd(7)} ${"速度".padEnd(10)} ${"啰嗦".padEnd(7)} ${"样本".padEnd(5)} 状态`);
|
|
2432
|
+
for (const { full, s } of sorted) {
|
|
2433
|
+
const ttfb = s.avgTtfbMs ?? s.emaTtfbMs;
|
|
2434
|
+
const total = s.avgTotalMs ?? s.emaTotalMs;
|
|
2435
|
+
const tps = s.avgTps ?? s.emaTps;
|
|
2436
|
+
const verbose = s.avgCompTok != null ? `${s.avgCompTok}tok` : "—";
|
|
2437
|
+
const st = statuses[full] || statuses[full.split("/").slice(1).join("/")] || null;
|
|
2438
|
+
const statusStr = st ? (typeof st === "number" ? "error" : (st.status || "normal")) : "normal";
|
|
2439
|
+
const p95 = s.p95Ttfb && s.p95Ttfb >= 10 ? ` p95:${s.p95Ttfb}ms` : "";
|
|
2440
|
+
const ttfbStr = ttfb != null ? (ttfb >= 10 ? ttfb + "ms" : "—") : "—";
|
|
2441
|
+
const totalStr = total != null ? (total >= 10 ? total + "ms" : "—") : "—";
|
|
2442
|
+
const tpsStr = tps != null && tps >= 1 ? tps + " tok/s" : "—";
|
|
2443
|
+
console.log(` ${full.padEnd(28)} ${ttfbStr.padEnd(7)} ${totalStr.padEnd(7)} ${tpsStr.padEnd(10)} ${verbose.padEnd(7)} ${String(s.count).padEnd(5)} ${statusStr}${p95}`);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
} else if (!freeIds.length) {
|
|
2447
|
+
console.log(` (暂无模型样本 — 发一次 mslxdff -chat 后出现,100次后均值更稳)`);
|
|
2448
|
+
}
|
|
2449
|
+
} catch (e) {
|
|
2450
|
+
console.log(`\nmodels: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// ---- autostart + plugins(运维一眼可见)----
|
|
2454
|
+
try {
|
|
2455
|
+
const { getAutostartStatus } = await import("../src/autostart.js");
|
|
2456
|
+
const a = await getAutostartStatus();
|
|
2457
|
+
const detail = a.detail || (a.enabled ? "已启用" : "未启用");
|
|
2458
|
+
// detail 已含“已启用/未启用”,避免重复
|
|
2459
|
+
const label = detail.startsWith("已启用") || detail.startsWith("未启用") ? detail : `${a.enabled ? "已启用" : "未启用"} · ${detail}`;
|
|
2460
|
+
console.log(`\nautostart: ${label} — mslxdff -autostart status`);
|
|
2461
|
+
if (a.taskToRun) console.log(` task: ${a.taskToRun.slice(0, 120)}`);
|
|
2462
|
+
if (a.unit) console.log(` unit: ${a.unit}`);
|
|
2463
|
+
} catch {}
|
|
2464
|
+
try {
|
|
2465
|
+
const { resolvePluginDirs, loadPlugins } = await import("../src/plugins.js");
|
|
2466
|
+
const pkgRoot = dirname(fileURLToPath(import.meta.url)) + "/..";
|
|
2467
|
+
const dirs = resolvePluginDirs({ pkgRoot });
|
|
2468
|
+
const { plugins, errors } = await loadPlugins({ dirs });
|
|
2469
|
+
if (plugins.length || errors.length) {
|
|
2470
|
+
console.log(`plugins: ${plugins.length} loaded${errors.length ? `, ${errors.length} error(s)` : ""} — mslxdff -plugins`);
|
|
2471
|
+
for (const p of plugins) console.log(` ${p.name}${p.version ? `@${p.version}` : ""} [${Object.keys(p.hooks || {}).join(", ") || "no hooks"}]`);
|
|
2472
|
+
for (const e of errors) console.log(` ! ${e.file} — ${e.error}`);
|
|
2473
|
+
} else {
|
|
2474
|
+
console.log(`plugins: (none) — 放 *.mjs 到 ${dirs[dirs.length - 1] || "~/.config/mslxdff/plugins"} 启用`);
|
|
2475
|
+
}
|
|
2476
|
+
} catch {}
|
|
2116
2477
|
|
|
2117
2478
|
const groups = createGroupsService({});
|
|
2118
2479
|
const joined = loadGroupsJoined();
|
|
@@ -2183,6 +2544,8 @@ async function printStatus() {
|
|
|
2183
2544
|
const tag = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
2184
2545
|
console.log(` ${p.name || p.url} ${p.url}${tag}`);
|
|
2185
2546
|
}
|
|
2547
|
+
} else {
|
|
2548
|
+
console.log(`\nfailover: (none — 加组后自动出现,mslxdff -group list)`);
|
|
2186
2549
|
}
|
|
2187
2550
|
|
|
2188
2551
|
const groupNames = Object.keys(groups.list());
|
|
@@ -2191,44 +2554,43 @@ async function printStatus() {
|
|
|
2191
2554
|
for (const n of groupNames) console.log(` ${n}`);
|
|
2192
2555
|
}
|
|
2193
2556
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
if (existsSync(modelsFile)) {
|
|
2197
|
-
try {
|
|
2198
|
-
const cached = JSON.parse(readFileSync(modelsFile, "utf8"));
|
|
2199
|
-
const ids = (cached.data || []).map((m) => m.id).filter(Boolean);
|
|
2200
|
-
console.log(`\nmodels (${ids.length} free):`);
|
|
2201
|
-
for (const id of ids) {
|
|
2202
|
-
const st = fmtStatus(id, statuses);
|
|
2203
|
-
console.log(` ${id}${st ? ` [${st}]` : ""}`);
|
|
2204
|
-
}
|
|
2205
|
-
} catch {
|
|
2206
|
-
console.log("\nmodels: cache unreadable");
|
|
2207
|
-
}
|
|
2208
|
-
} else {
|
|
2209
|
-
console.log("\nmodels: not cached yet (runs once the server has fetched the upstream list)");
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
console.log("\nrecent calls:");
|
|
2557
|
+
// recent calls — 补首字/tok/s 信息,原来只看 duration
|
|
2558
|
+
console.log("\nrecent calls: (gateway 持久化,最近5条,含首字/tok/s)");
|
|
2213
2559
|
const calls = recentCalls(5);
|
|
2214
2560
|
if (calls.length) {
|
|
2561
|
+
let sumDur = 0, sumTps = 0, tpsN = 0;
|
|
2215
2562
|
for (const c of calls) {
|
|
2216
|
-
|
|
2563
|
+
if (Number.isFinite(c.durationMs)) sumDur += c.durationMs;
|
|
2564
|
+
else if (Number.isFinite(c.totalMs)) sumDur += c.totalMs;
|
|
2565
|
+
if (Number.isFinite(c.tps)) { sumTps += c.tps; tpsN++; } else if (Number.isFinite(c.charsPerSec)) { sumTps += c.charsPerSec; tpsN++; }
|
|
2566
|
+
}
|
|
2567
|
+
const avgDur = calls.length ? Math.round(sumDur / calls.length) : null;
|
|
2568
|
+
const avgTps = tpsN ? Math.round(sumTps / tpsN) : null;
|
|
2569
|
+
console.log(` avg ${avgDur ? avgDur + "ms" : "—"}${avgTps ? ` · ${avgTps} tok/s` : ""} — mslxdff -log 20 查看详情`);
|
|
2570
|
+
for (const c of calls) {
|
|
2571
|
+
const dur = c.totalMs ?? c.durationMs;
|
|
2572
|
+
const ttfb = c.ttfbMs != null ? ` 首字${c.ttfbMs}ms` : "";
|
|
2573
|
+
const tps = c.tps != null ? ` ${c.tps}tok/s` : (c.charsPerSec ? ` ${c.charsPerSec}ch/s` : "");
|
|
2574
|
+
const tok = c.usage?.completion_tokens != null ? ` tok${c.usage.completion_tokens}` : (c.chars ? ` ch${c.chars}` : "");
|
|
2575
|
+
const tm = fmtTs(c.ts);
|
|
2576
|
+
console.log(` ${tm} ${(c.model || "-").padEnd(28)} ${String(c.status || "-").padEnd(4)} ${dur ? dur + "ms" : ""}${ttfb}${tps}${tok}${c.auto ? " auto" : ""}`);
|
|
2217
2577
|
}
|
|
2218
2578
|
} else {
|
|
2219
|
-
console.log(" (none yet)");
|
|
2579
|
+
console.log(" (none yet — 发一次请求后出现,mslxdff -chat hi)");
|
|
2220
2580
|
}
|
|
2221
2581
|
|
|
2222
2582
|
console.log("\nlast error:");
|
|
2223
2583
|
const err = lastError();
|
|
2224
2584
|
if (err) {
|
|
2225
2585
|
console.log(` ${fmtTs(err.ts)} ${err.model || "-"} ${err.status} ${err.message || ""}`);
|
|
2586
|
+
if (err.stack) console.log(` ${String(err.stack).slice(0, 200)}`);
|
|
2226
2587
|
} else {
|
|
2227
|
-
console.log(" (none)");
|
|
2588
|
+
console.log(" (none — 暂无错误,挺好)");
|
|
2228
2589
|
}
|
|
2229
2590
|
|
|
2230
|
-
if (daemon) console.log(`\nauth token: use \`mslxdff -showtoken
|
|
2231
|
-
else console.log(`\nnot running — start with: mslxdff -d`);
|
|
2591
|
+
if (daemon && alive) console.log(`\nauth token: use \`mslxdff -showtoken\` · health: http://127.0.0.1:${port}/health`);
|
|
2592
|
+
else console.log(`\nnot running — start with: mslxdff -d · 查看日志 mslxdff -log 20`);
|
|
2593
|
+
console.log(`hints: mslxdff -providers list · mslxdff -model status · mslxdff -group list · mslxdff -autostart status`);
|
|
2232
2594
|
}
|
|
2233
2595
|
|
|
2234
2596
|
function fmtStatus(id, statuses) {
|
|
@@ -2245,6 +2607,18 @@ function fmtDur(ms) {
|
|
|
2245
2607
|
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
2246
2608
|
}
|
|
2247
2609
|
|
|
2610
|
+
function fmtUptime(ms) {
|
|
2611
|
+
if (!Number.isFinite(ms) || ms < 0) return "?";
|
|
2612
|
+
const s = Math.floor(ms / 1000);
|
|
2613
|
+
if (s < 60) return `${s}s`;
|
|
2614
|
+
const m = Math.floor(s / 60);
|
|
2615
|
+
const h = Math.floor(m / 60);
|
|
2616
|
+
const d = Math.floor(h / 24);
|
|
2617
|
+
if (d) return `${d}d${h % 24}h`;
|
|
2618
|
+
if (h) return `${h}h${m % 60}m`;
|
|
2619
|
+
return `${m}m${s % 60}s`;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2248
2622
|
function fmtEvent(e) {
|
|
2249
2623
|
const t = e?.ts ? fmtShanghaiHMS(e.ts) : "--:--:--";
|
|
2250
2624
|
const head = `[${t}]`;
|