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 +502 -56
- package/package.json +2 -2
- package/src/auto.js +1 -1
- package/src/chat/prompt.js +2 -0
- package/src/chat/repl.js +62 -5
- package/src/chat/stats.js +83 -30
- package/src/chat/tools.js +3 -3
- package/src/free-watcher.js +62 -0
- package/src/providers/dispatcher.js +4 -1
- package/src/providers/model-id.js +98 -0
- package/src/routes/chat/index.js +44 -2
- package/src/routes/stream.js +58 -1
- package/src/state.js +83 -7
- package/src/sync-opencode.js +152 -0
- package/src/sync-workbuddy.js +38 -6
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 {
|
|
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
|
-
|
|
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]:
|
|
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 (
|
|
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 = "
|
|
528
|
-
else if (!
|
|
697
|
+
if (!baseUrl && !keys.length && !allowed.length && allowAny === false) note = "no baseUrl, no keys, BLOCKED (allowAny OFF)";
|
|
698
|
+
else if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
|
|
699
|
+
else if (!baseUrl && !allowed.length && !allowAny) note = "missing baseUrl, BLOCKED";
|
|
700
|
+
else if (!keys.length && !allowed.length && !allowAny) note = "no keys, BLOCKED";
|
|
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=
|
|
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
|
-
|
|
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 —
|
|
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
|
-
//
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 || []),
|
|
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
|
-
|
|
2043
|
-
|
|
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
|
-
|
|
2123
|
-
|
|
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
|
-
|
|
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}]`;
|