mslxdff 0.1.56 → 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,10 +8,12 @@ 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";
16
+ import { syncToOpencode, opencodeConfigPath, toExternalAlias, toInternalId } from "../src/sync-opencode.js";
15
17
  import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
16
18
  import { createAutoSelector } from "../src/auto.js";
17
19
  import { createPeersService } from "../src/peers.js";
@@ -304,7 +306,36 @@ if (args.includes("-model") || args.includes("-models")) {
304
306
  const pickedIds = loadModelPicks();
305
307
  const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
306
308
  console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
307
- 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
+ }
308
339
  console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
309
340
  } catch (err) {
310
341
  console.error(`could not fetch models: ${String(err?.message || err)}`);
@@ -313,14 +344,85 @@ if (args.includes("-model") || args.includes("-models")) {
313
344
  process.exit(0);
314
345
  }
315
346
 
316
- // -setto workbuddy [modelId]: set default model and sync to WorkBuddy models.json
347
+ // -setto workbuddy|opencode [modelId]: sync to WorkBuddy / opencode
317
348
  if (args.includes("-setto") || args.includes("--setto")) {
318
349
  const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
319
350
  const target = args[idx + 1];
320
- if (target !== "workbuddy") {
321
- console.error("usage: mslxdff -setto workbuddy [modelId]");
351
+ if (!["workbuddy", "opencode"].includes(target)) {
352
+ console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
322
353
  process.exit(1);
323
354
  }
355
+ if (target === "opencode") {
356
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
357
+ let id;
358
+ let internal;
359
+ if (raw) {
360
+ if (raw === "auto" || !raw) {
361
+ console.error("modelId 不能为 auto 或空");
362
+ process.exit(1);
363
+ }
364
+ const norm = normalizeModel(raw);
365
+ if (!norm) {
366
+ console.error("modelId 不能为空");
367
+ process.exit(1);
368
+ }
369
+ savePreferredModel(norm);
370
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
371
+ internal = toInternalId(norm);
372
+ id = toExternalAlias(internal);
373
+ } else {
374
+ const pref = loadPreferredModel() || getPreferredModel();
375
+ if (!pref) {
376
+ console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
377
+ process.exit(1);
378
+ }
379
+ const norm = normalizeModel(pref);
380
+ if (!norm) {
381
+ console.error("modelId 不能为空");
382
+ process.exit(1);
383
+ }
384
+ internal = toInternalId(norm);
385
+ id = toExternalAlias(internal);
386
+ }
387
+ try {
388
+ const cacheFile = join(logDir(), "models.json");
389
+ const models = createModelsService({
390
+ baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
391
+ headers: createUpstreamClient({}).headers,
392
+ refreshMs: 0,
393
+ cacheFile,
394
+ });
395
+ const fresh = await Promise.race([
396
+ models.get(),
397
+ new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
398
+ ]);
399
+ if (fresh?.data?.length) {
400
+ const ids = fresh.data.map((m) => m.id);
401
+ if (!ids.includes(internal) && !ids.includes(id)) {
402
+ console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
403
+ }
404
+ }
405
+ } catch {
406
+ // refresh failed, still proceed
407
+ }
408
+ try {
409
+ const { token } = await loadToken();
410
+ const persisted = getPort();
411
+ const envPort = Number(process.env.MSLXDFF_PORT);
412
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
413
+ const file = opencodeConfigPath();
414
+ const r = await syncToOpencode({ id, token, port, file });
415
+ const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
416
+ console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
417
+ console.log(` url: http://127.0.0.1:${port}/v1`);
418
+ if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
419
+ else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
420
+ } catch (err) {
421
+ console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
422
+ process.exit(1);
423
+ }
424
+ process.exit(0);
425
+ }
324
426
  const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
325
427
  let id;
326
428
  if (raw) {
@@ -471,6 +573,71 @@ if (args.includes("-workbuddy") || args.includes("--workbuddy") || args.includes
471
573
  }
472
574
  }
473
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
+
474
641
  // -providers list : list all deployed upstream providers (opencode + openrouter + generic)
475
642
  if (args.includes("-providers") || args.includes("--providers")) {
476
643
  const idx = args.findIndex((x) => x === "-providers" || x === "--providers");
@@ -512,34 +679,40 @@ if (args.includes("-providers") || args.includes("--providers")) {
512
679
  }
513
680
  } catch {}
514
681
  const list = [];
682
+ const { loadProviderAllowAnyModels: _la0 } = await import("../src/state.js");
683
+ const opAllowAny = _la0("opencode");
684
+ const orAllowAny = _la0("openrouter");
515
685
  const opAllowed = loadProviderAllowedModels("opencode");
516
- list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, note: "built-in, no key, cannot share" });
517
- 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" });
518
688
  for (const gid of [...genericIds].sort()) {
519
689
  const cfg = configs[gid];
520
690
  const keys = loadProviderKeys(gid);
521
691
  const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
522
692
  const share = loadProviderShareKeys(gid);
523
693
  const allowed = loadProviderAllowedModels(gid);
694
+ const allowAny = _la0(gid);
524
695
  const enabled = Boolean(baseUrl && keys.length);
525
696
  let note = "";
526
- if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
527
- else if (!baseUrl && !allowed.length) note = "missing baseUrl";
528
- 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";
529
701
  else if (!baseUrl) note = "missing baseUrl";
530
702
  else if (!keys.length) note = "no keys";
531
- 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 });
532
704
  }
533
705
  console.log(`providers (${list.length}):`);
534
706
  for (const p of list) {
535
707
  const state = p.enabled ? "enabled " : "disabled";
536
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";
537
709
  const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
538
- 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)");
539
711
  const note = p.note ? ` (${p.note})` : "";
540
712
  console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} ${allowInfo.padEnd(22)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
541
713
  }
542
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)`);
543
716
  process.exit(0);
544
717
  }
545
718
  console.error("usage: mslxdff -providers list");
@@ -554,14 +727,15 @@ if (args.includes("-provider") || args.includes("--provider")) {
554
727
  const sub = args[idx + 2];
555
728
  const rest = args.slice(idx + 2);
556
729
  if (!id) {
557
- 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]");
558
731
  console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
559
732
  console.error(" mslxdff -provider openrouter add sk-4 append one key");
560
733
  console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
561
734
  console.error(" mslxdff -provider openrouter list list all keys (masked)");
562
735
  console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
563
736
  console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
564
- 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)");
565
739
  console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx add generic OpenAI-compatible provider");
566
740
  console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
567
741
  console.error(" mslxdff -provider openrouter interactive hidden input (append)");
@@ -677,7 +851,14 @@ if (args.includes("-provider") || args.includes("--provider")) {
677
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 }); }
678
852
  } catch {}
679
853
  } else {
680
- 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))];
681
862
  auths = undefined;
682
863
  saveProviderConfig(nid, { baseUrl, keys, allowedModels });
683
864
  }
@@ -685,12 +866,14 @@ if (args.includes("-provider") || args.includes("--provider")) {
685
866
  console.log(` baseUrl: ${String(gBase).trim().replace(/\/+$/, "")}`);
686
867
  console.log(` keys: ${keys.length} (${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")})`);
687
868
  if (allowedModels.length) console.log(` allowedModels: ${allowedModels.length} (${allowedModels.join(", ")})`);
688
- 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)`);
689
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`);
690
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`);
691
874
  process.exit(0);
692
875
  }
693
- 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") {
694
877
  console.log("opencode is the default (bare) provider — it needs no API key and can never be shared with peers");
695
878
  console.log("(its IP-based rate limit is spread by peer forwarding itself)");
696
879
  console.log(` allowlist: mslxdff -provider opencode allowlist [list|set|add|remove|clear] — restrict models (empty=allow all)`);
@@ -737,9 +920,28 @@ if (args.includes("-provider") || args.includes("--provider")) {
737
920
  console.log(`share keys to peers: ${state ? "ON" : "off"} — restart daemon to activate`);
738
921
  process.exit(0);
739
922
  }
740
- // 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 才放行);非空 = 仅名单内可用(防昂贵模型)
741
943
  if (sub === "allowlist" || sub === "allow" || sub === "allowed" || sub === "whitelist") {
742
- const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig } = await import("../src/state.js");
944
+ const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig, loadProviderAllowAnyModels } = await import("../src/state.js");
743
945
  const action = rest[1];
744
946
  const rawTargets = rest.slice(2);
745
947
  // 兼容:mslxdff -provider <id> allowlist (无 action)→ list
@@ -747,22 +949,30 @@ if (args.includes("-provider") || args.includes("--provider")) {
747
949
  const list = loadProviderAllowedModels(id);
748
950
  const cfg = loadProviderConfig(id);
749
951
  const baseUrl = cfg?.baseUrl || "";
952
+ const allowAny = loadProviderAllowAnyModels(id);
750
953
  console.log(`provider: ${id}${baseUrl ? ` baseUrl: ${baseUrl}` : ""}`);
751
954
  if (!list.length) {
752
- console.log(` allowedModels: (none — allow all)`);
753
- console.log(` set via: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
754
- 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
+ }
755
963
  } else {
756
964
  console.log(` allowedModels: ${list.length} model${list.length > 1 ? "s" : ""} (only these can be used)`);
757
965
  list.forEach((m, i) => console.log(` [${i + 1}] ${m}`));
758
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`);
759
968
  }
760
- 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)`);
761
970
  process.exit(0);
762
971
  }
763
972
  if (action === "clear") {
764
973
  saveProviderAllowedModels(id, []);
765
- 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)`);
766
976
  process.exit(0);
767
977
  }
768
978
  if (action === "set") {
@@ -801,17 +1011,21 @@ if (args.includes("-provider") || args.includes("--provider")) {
801
1011
  const set = new Set(flat);
802
1012
  const next = cur.filter((m) => !set.has(m));
803
1013
  saveProviderAllowedModels(id, next);
804
- 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)`);
805
1017
  process.exit(0);
806
1018
  }
807
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)`);
808
1021
  process.exit(1);
809
1022
  }
810
1023
  if (sub === "list" || sub === "status") {
811
1024
  const keys = loadProviderKeys(id);
812
1025
  const cfg = loadProviderConfig(id);
813
- const { loadProviderAllowedModels } = await import("../src/state.js");
1026
+ const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("../src/state.js");
814
1027
  const allowed = loadProviderAllowedModels(id);
1028
+ const allowAny = loadProviderAllowAnyModels(id);
815
1029
  const baseUrl = cfg?.baseUrl || "";
816
1030
  if (baseUrl) console.log(`provider: ${id} baseUrl: ${baseUrl}`);
817
1031
  else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
@@ -823,8 +1037,9 @@ if (args.includes("-provider") || args.includes("--provider")) {
823
1037
  console.log(` keys: (no keys configured)`);
824
1038
  }
825
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)`);
826
1041
  if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
827
- 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)`);
828
1043
  if (baseUrl) console.log(` set url: mslxdff -provider ${id} set-url <baseUrl>`);
829
1044
  console.log(` NOTE: opencode is the default provider and can never be shared`);
830
1045
  process.exit(0);
@@ -835,11 +1050,18 @@ if (args.includes("-provider") || args.includes("--provider")) {
835
1050
  console.error("usage: mslxdff -provider openrouter add <key>");
836
1051
  process.exit(1);
837
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
+ }
838
1060
  // 通用供应商:keys 存 providerConfigs,需保留 baseUrl
839
1061
  const configs = loadProviderConfigs();
840
1062
  if (configs[id]) {
841
1063
  const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
842
- const keys = [...new Set([...(cur.keys || []), String(key).trim()].filter(Boolean))];
1064
+ const keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
843
1065
  saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
844
1066
  console.log(`added ${id} API key (now ${keys.length} total) — restart daemon to activate`);
845
1067
  } else {
@@ -1610,6 +1832,13 @@ const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshol
1610
1832
 
1611
1833
  const isDebug = process.env.MSLXDFF_DEBUG === "1";
1612
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
+
1613
1842
  const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus, plugins: loadedPlugins });
1614
1843
  const listenHost = effectiveHost();
1615
1844
  const srv = startServer({
@@ -1979,7 +2208,7 @@ function printHelp() {
1979
2208
  Usage:
1980
2209
  mslxdff start as a background daemon and exit (status + help if one is already running)
1981
2210
  mslxdff -d start as a background daemon
1982
- 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)
1983
2212
  mslxdff -log [N] show last N events (default 10, e.g. -log 100)
1984
2213
  mslxdff -models interactive picker: ↑/↓ select a model, Enter sets it as the default (non-TTY: plain list)
1985
2214
  mslxdff -model list list the free models this proxy serves (cached)
@@ -2008,6 +2237,11 @@ Usage:
2008
2237
  mslxdff -group remove <seq> leader only: kick a member by its list sequence number
2009
2238
  mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
2010
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 查看自启状态
2011
2245
  mslxdff -chat ["prompt"] chat REPL(mimo-v2.5-free 优先/big-pickle 兜底,自然语言转命令,模糊匹配由模型完成,历史持久化,超长自动压缩,仅拦 -uninstall,daemon 重启不影响)
2012
2246
  mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
2013
2247
  mslxdff -help show this help
@@ -2036,11 +2270,210 @@ Environment:
2036
2270
 
2037
2271
  async function printStatus() {
2038
2272
  const daemon = readPid();
2273
+ const alive = daemon ? isPidAlive(daemon) : false;
2039
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)` : ""); })() : "";
2040
2289
  console.log(`mslxdff v${VERSION}`);
2041
- console.log(`daemon: ${daemon ? `running (pid ${daemon})` : "not running"}`);
2042
- console.log(`endpoint: http://localhost:${port}/v1`);
2043
- 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 {}
2044
2477
 
2045
2478
  const groups = createGroupsService({});
2046
2479
  const joined = loadGroupsJoined();
@@ -2111,6 +2544,8 @@ async function printStatus() {
2111
2544
  const tag = tags.length ? ` [${tags.join(", ")}]` : "";
2112
2545
  console.log(` ${p.name || p.url} ${p.url}${tag}`);
2113
2546
  }
2547
+ } else {
2548
+ console.log(`\nfailover: (none — 加组后自动出现,mslxdff -group list)`);
2114
2549
  }
2115
2550
 
2116
2551
  const groupNames = Object.keys(groups.list());
@@ -2119,44 +2554,43 @@ async function printStatus() {
2119
2554
  for (const n of groupNames) console.log(` ${n}`);
2120
2555
  }
2121
2556
 
2122
- const modelsFile = join(logDir(), "models.json");
2123
- const statuses = loadModelErrors();
2124
- if (existsSync(modelsFile)) {
2125
- try {
2126
- const cached = JSON.parse(readFileSync(modelsFile, "utf8"));
2127
- const ids = (cached.data || []).map((m) => m.id).filter(Boolean);
2128
- console.log(`\nmodels (${ids.length} free):`);
2129
- for (const id of ids) {
2130
- const st = fmtStatus(id, statuses);
2131
- console.log(` ${id}${st ? ` [${st}]` : ""}`);
2132
- }
2133
- } catch {
2134
- console.log("\nmodels: cache unreadable");
2135
- }
2136
- } else {
2137
- console.log("\nmodels: not cached yet (runs once the server has fetched the upstream list)");
2138
- }
2139
-
2140
- console.log("\nrecent calls:");
2557
+ // recent calls 补首字/tok/s 信息,原来只看 duration
2558
+ console.log("\nrecent calls: (gateway 持久化,最近5条,含首字/tok/s)");
2141
2559
  const calls = recentCalls(5);
2142
2560
  if (calls.length) {
2561
+ let sumDur = 0, sumTps = 0, tpsN = 0;
2143
2562
  for (const c of calls) {
2144
- 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" : ""}`);
2145
2577
  }
2146
2578
  } else {
2147
- console.log(" (none yet)");
2579
+ console.log(" (none yet — 发一次请求后出现,mslxdff -chat hi)");
2148
2580
  }
2149
2581
 
2150
2582
  console.log("\nlast error:");
2151
2583
  const err = lastError();
2152
2584
  if (err) {
2153
2585
  console.log(` ${fmtTs(err.ts)} ${err.model || "-"} ${err.status} ${err.message || ""}`);
2586
+ if (err.stack) console.log(` ${String(err.stack).slice(0, 200)}`);
2154
2587
  } else {
2155
- console.log(" (none)");
2588
+ console.log(" (none — 暂无错误,挺好)");
2156
2589
  }
2157
2590
 
2158
- if (daemon) console.log(`\nauth token: use \`mslxdff -showtoken\``);
2159
- 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`);
2160
2594
  }
2161
2595
 
2162
2596
  function fmtStatus(id, statuses) {
@@ -2173,6 +2607,18 @@ function fmtDur(ms) {
2173
2607
  return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
2174
2608
  }
2175
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
+
2176
2622
  function fmtEvent(e) {
2177
2623
  const t = e?.ts ? fmtShanghaiHMS(e.ts) : "--:--:--";
2178
2624
  const head = `[${t}]`;