mslxdff 0.1.57 → 0.1.60

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 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 { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroupsJoined, loadModelErrors, savePreferredModel, loadPreferredModel, loadModelPicks, saveModelPicks, loadProviderKey, loadProviderKeys, loadProviderAuths, loadProviderConfigs as loadProviderConfigsState } from "../src/state.js";
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
- for (const id of ids) console.log(` ${mark(id)} ${id}`);
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 = "missing baseUrl";
600
- else if (!keys.length && !allowed.length) note = "no keys";
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=allow all)");
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
- keys = [...new Set([...(cur.keys || []), String(gKey).trim()].filter(Boolean))];
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 — allow all, set via: mslxdff -provider ${nid} allowlist set <model...>)`);
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
- // allowlist 管理:白名单为空 = 不限;非空 = 仅名单内可用(防昂贵模型)
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
- console.log(` allowedModels: (none — allow all)`);
825
- console.log(` set via: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
826
- console.log(` add: mslxdff -provider ${id} allowlist add <model>`);
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 = allow all (hot-reloaded, no restart needed)`);
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
- console.log(`cleared ${id} allowlist (now allow all) — takes effect immediately (hot-reloaded)`);
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
- console.log(`removed ${cur.length - next.length} model${cur.length - next.length !== 1 ? "s" : ""} from ${id} allowlist (now ${next.length ? next.join(", ") : "(allow all)"}) — takes effect immediately (hot-reloaded)`);
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 || []), String(key).trim()].filter(Boolean))];
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
- console.log(`endpoint: http://localhost:${port}/v1`);
2115
- console.log(`log dir: ${logDir()}`);
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
- const modelsFile = join(logDir(), "models.json");
2195
- const statuses = loadModelErrors();
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
- console.log(` ${fmtTs(c.ts)} ${c.model || "-"} ${c.status} ${c.durationMs ?? "?"}ms${c.auto ? " auto" : ""}`);
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}]`;