mslxdff 0.1.232 → 0.1.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/docs/cli_help_mini.md +148 -0
  3. package/package.json +1 -1
  4. package/src/auto.js +264 -254
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +155 -145
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/cli/commands/group.js +266 -266
  22. package/src/cli/commands/model/picks.js +50 -50
  23. package/src/cli/commands/provider/bench-via.js +247 -247
  24. package/src/cli/commands/provider/bench.js +141 -141
  25. package/src/cli/commands/provider/index.js +139 -139
  26. package/src/cli/commands/provider/models.js +149 -149
  27. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  28. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  29. package/src/cli/commands/sync.js +310 -310
  30. package/src/cli/status.js +279 -279
  31. package/src/daemon.js +246 -246
  32. package/src/model-capabilities/enrich.js +86 -86
  33. package/src/model-capabilities/index.js +183 -183
  34. package/src/model-capabilities/parse.js +70 -70
  35. package/src/model-trace.js +10 -2
  36. package/src/models.js +293 -226
  37. package/src/peers.js +345 -345
  38. package/src/providers/AGENTS.md +49 -0
  39. package/src/providers/agentrouter.js +238 -238
  40. package/src/providers/base.js +164 -164
  41. package/src/providers/cline/auth.js +228 -228
  42. package/src/providers/cline/chat.js +307 -307
  43. package/src/providers/cline.js +2 -2
  44. package/src/providers/codearts/chat.js +207 -207
  45. package/src/providers/dispatcher.js +147 -147
  46. package/src/providers/globalqwenwork/cosy.js +123 -123
  47. package/src/providers/globalqwenwork/crypto.js +218 -218
  48. package/src/providers/globalqwenwork/http.js +20 -20
  49. package/src/providers/globalqwenwork/rsa.js +54 -54
  50. package/src/providers/globalqwenwork/sse.js +268 -268
  51. package/src/providers/globalqwenwork/stream.js +130 -130
  52. package/src/providers/globalqwenwork/upstream.js +122 -122
  53. package/src/providers/keyring.js +60 -60
  54. package/src/providers/qoder/account-store.js +18 -0
  55. package/src/providers/qoder/chat.js +292 -203
  56. package/src/providers/qoder/checkin.js +25 -12
  57. package/src/providers/qoder/index.js +311 -249
  58. package/src/providers/qoder/models.js +64 -2
  59. package/src/providers/qoder/oauth.js +42 -1
  60. package/src/providers/qoder/payload.js +116 -80
  61. package/src/providers/qoder/request.js +7 -2
  62. package/src/providers/qoder/session.js +26 -10
  63. package/src/providers/qoder/sse.js +233 -145
  64. package/src/providers/qoder/sticky.js +5 -2
  65. package/src/providers/qoder/wire-messages.js +242 -0
  66. package/src/providers/qwenwork/account-store.js +133 -133
  67. package/src/providers/qwenwork/constants.js +67 -67
  68. package/src/providers/qwenwork/cosy.js +120 -120
  69. package/src/providers/qwenwork/crypto.js +218 -218
  70. package/src/providers/qwenwork/http.js +20 -20
  71. package/src/providers/qwenwork/index.js +337 -337
  72. package/src/providers/qwenwork/payload.js +144 -144
  73. package/src/providers/qwenwork/rsa.js +54 -54
  74. package/src/providers/qwenwork/sse.js +268 -268
  75. package/src/providers/qwenwork/stream.js +130 -130
  76. package/src/providers/qwenwork/upstream.js +120 -120
  77. package/src/providers/qwenwork.js +1 -1
  78. package/src/providers/raccoon/chat.js +1 -1
  79. package/src/providers/raccoon/const.js +219 -202
  80. package/src/providers/raccoon/credits.js +285 -285
  81. package/src/providers/raccoon/index.js +51 -6
  82. package/src/providers/raccoon/models.js +68 -17
  83. package/src/providers/raccoon/sticky.js +46 -0
  84. package/src/providers/registry.js +89 -89
  85. package/src/providers/workbuddy/chat.js +298 -298
  86. package/src/providers/workbuddy/reshape.js +152 -152
  87. package/src/providers/workbuddy.js +2 -2
  88. package/src/reasoning.js +32 -32
  89. package/src/routes/AGENTS.md +37 -0
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
package/src/cli/status.js CHANGED
@@ -1,279 +1,279 @@
1
- import { readFileSync, existsSync, statSync } from "node:fs";
2
- import { join, dirname } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { readPid, readPidVersion, isPidAlive, pidFile } from "../daemon.js";
5
- import { defaultStateFile, getPort, loadGroupsJoined, loadModelErrors, loadModelStats, loadModelPicks } from "../state.js";
6
- import { resolvePort } from "../server.js";
7
- import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderBaseUrl } from "../state.js";
8
- import { loadToken } from "../state.js";
9
- import { logDir } from "../logs.js";
10
- import { createGroupsService } from "../groups.js";
11
- import { createPeersService } from "../peers.js";
12
- import { refreshGroupMembers } from "../groups.js";
13
- import { fmtShanghaiYMDHM } from "../time.js";
14
- import { fmtStatus, fmtUptime, fmtTs } from "./format.js";
15
- import { compareSemver } from "./policy.js";
16
- import { buildProviderRows, formatProviderRow, formatProviderSection } from "./provider-row.js";
17
- import { compatFetch, timeoutSignal } from "../compat.js";
18
-
19
- export async function printStatus(VERSION) {
20
- const daemon = readPid();
21
- const alive = daemon ? isPidAlive(daemon) : false;
22
- const port = getPort() || resolvePort();
23
- const persisted = getPort();
24
- const portSrc = persisted != null ? "persisted" : (process.env.MSLXDFF_PORT ? "env MSLXDFF_PORT" : "default 8989");
25
- const stateFile = defaultStateFile();
26
- const dir = logDir();
27
- let upStr = "";
28
- if (daemon && alive) {
29
- try {
30
- const st = statSync(pidFile());
31
- const ms = Date.now() - st.mtimeMs;
32
- upStr = `, up ${fmtUptime(ms)}`;
33
- } catch {}
34
- }
35
- const verNote = daemon && alive ? (() => { const v = readPidVersion(); if (!v) return ""; if (v === VERSION) return "version ok"; const cmp = compareSemver(VERSION, v); if (cmp > 0) return `version ${v} → ${VERSION} (upgrade pending)`; if (cmp < 0) return `version ${v} (newer than local ${VERSION}, keeping)`; return "version ok"; })() : "";
36
- console.log(`mslxdff v${VERSION}`);
37
- console.log(`daemon: ${daemon ? (alive ? `running (pid ${daemon}${upStr})` : `stale pid ${daemon} (not alive)`) : "not running"}${verNote ? ` [${verNote}]` : ""}`);
38
- let healthLine = "";
39
- try {
40
- const t0 = Date.now();
41
- const r = await compatFetch(`http://127.0.0.1:${port}/health`, { signal: timeoutSignal(1200) });
42
- const ms = Date.now() - t0;
43
- healthLine = r.ok ? `health ok ${ms}ms` : `health HTTP ${r.status} ${ms}ms`;
44
- } catch (e) {
45
- healthLine = daemon && alive ? `health fail (${String(e?.message || e).slice(0, 60)})` : "health — (daemon not running)";
46
- }
47
- console.log(`endpoint: http://localhost:${port}/v1 · ${healthLine}`);
48
- console.log(`config: port ${port} (${portSrc}) · state ${stateFile} · log ${dir}`);
49
- try {
50
- const host = process.env.MSLXDFF_HOST || process.env.MSLXDFF_BIND_HOST || "0.0.0.0";
51
- if (host !== "0.0.0.0") console.log(`bind: ${host}`);
52
- } catch {}
53
-
54
- try {
55
- const providerRows = buildProviderRows({});
56
- const enabledCount = providerRows.filter((r) => r.enabled).length;
57
- console.log(`\nupstream providers ${providerRows.length} 个 · ${enabledCount} 已启用 — mslxdff -providers list 查看详情`);
58
- console.log(formatProviderSection(providerRows));
59
- if (enabledCount === 1 && providerRows.length === 1) {
60
- console.log(`\n 空状态:仅 opencode 内置免费通道`);
61
- console.log(` → 加一个私有上游:mslxdff -provider add bai https://api.b.ai/v1 <key>`);
62
- console.log(` → 或 WorkBuddy 一键接入:node workbuddy-token-auto.js`);
63
- }
64
- } catch (e) {
65
- console.log(`\nupstream providers: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
66
- }
67
-
68
- try {
69
- const statuses = loadModelErrors();
70
- const stats = loadModelStats();
71
- const picks = loadModelPicks();
72
- const { getPreferredModel: gpm } = await import("../auto.js");
73
- const preferred = gpm();
74
- const modelsFile = join(dir, "models.json");
75
- let freeIds = [];
76
- let cachedAt = null;
77
- let cacheErr = null;
78
- if (existsSync(modelsFile)) {
79
- try {
80
- const cached = JSON.parse(readFileSync(modelsFile, "utf8"));
81
- freeIds = (cached.data || []).map((m) => m.id).filter(Boolean);
82
- cachedAt = cached.cachedAt || null;
83
- } catch (err) { cacheErr = String(err?.message || err).slice(0, 60); }
84
- }
85
- 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`; })() : "";
86
- const cacheLine = freeIds.length ? `${freeIds.length} free${cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)} · ${ageStr})` : ""}` : (cacheErr ? `cache unreadable (${cacheErr})` : "not cached yet (daemon 拉取后出现)");
87
- const prefStat = preferred ? (stats[preferred] || stats[`opencode/${preferred}`] || null) : null;
88
- const prefErr = preferred ? statuses[preferred] : null;
89
- const prefStatus = prefErr ? (typeof prefErr === "number" ? "error" : (prefErr?.status || "error")) : (prefStat ? "normal" : "");
90
- const prefLine = preferred ? `${preferred}${prefStatus ? ` [${prefStatus}]` : ""}` : "(none)";
91
- const picksLine = picks.length ? `${picks.length} (*${picks.slice(0, 4).join(", *")}${picks.length > 4 ? ` …+${picks.length - 4}` : ""})` : "(空=全量 auto)";
92
- console.log(`\nmodels: ${cacheLine} — mslxdff -model status 查看详情`);
93
- const prefTtfbDisp = prefStat ? (() => { const v = prefStat.avgTtfbMs ?? prefStat.emaTtfbMs; return v != null && v >= 10 ? `首字 ${v}ms` : (v != null && v < 10 ? "首字 — (测试数据)" : ""); })() : "";
94
- const prefTotDisp = prefStat?.avgTotalMs && prefStat.avgTotalMs >= 10 ? `总 ${prefStat.avgTotalMs}ms` : "";
95
- const prefTpsDisp = prefStat?.avgTps && prefStat.avgTps >= 1 ? `${prefStat.avgTps} tok/s` : "";
96
- const prefExtra = prefStat ? ` ${[prefTtfbDisp, prefTotDisp, prefTpsDisp].filter(Boolean).join(" ")} ${prefStat.count || 0}次` : "";
97
- console.log(` preferred: ${prefLine}${prefExtra}`);
98
- console.log(` picks: ${picksLine} ${picks.length ? "auto 仅在勾选内" : "auto 用全量免费池"}`);
99
- if (freeIds.length) {
100
- const ids = freeIds;
101
- console.log(` free list:`);
102
- for (const id of ids) {
103
- const st = fmtStatus(id, statuses);
104
- const full = id.includes("/") ? id : `opencode/${id}`;
105
- const ms = stats[full] || stats[id];
106
- let extra = "";
107
- if (ms) {
108
- const v = ms.avgTtfbMs ?? ms.emaTtfbMs;
109
- const vStr = v != null ? (v >= 10 ? `avg首字 ${v}ms` : (v < 10 ? "avg首字 —" : "")) : "";
110
- const tpsStr = ms.avgTps && ms.avgTps >= 1 ? ` ${ms.avgTps} tok/s` : "";
111
- const cntStr = ms.count ? ` ${ms.count}次` : "";
112
- extra = ` ${vStr}${tpsStr}${cntStr}`;
113
- }
114
- console.log(` ${id}${st ? ` [${st}]` : ""}${extra}`);
115
- }
116
- }
117
- const allStatIds = Object.keys(stats);
118
- if (allStatIds.length) {
119
- 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);
120
- if (sorted.length) {
121
- console.log(` 模型体检 Top${sorted.length}(样本>0, 按次数):`);
122
- console.log(` ${"模型".padEnd(28)} ${"首字".padEnd(7)} ${"总耗时".padEnd(7)} ${"速度".padEnd(10)} ${"啰嗦".padEnd(7)} ${"样本".padEnd(5)} 状态`);
123
- for (const { full, s } of sorted) {
124
- const ttfb = s.avgTtfbMs ?? s.emaTtfbMs;
125
- const total = s.avgTotalMs ?? s.emaTotalMs;
126
- const tps = s.avgTps ?? s.emaTps;
127
- const verbose = s.avgCompTok != null ? `${s.avgCompTok}tok` : "—";
128
- const st = statuses[full] || statuses[full.split("/").slice(1).join("/")] || null;
129
- const statusStr = st ? (typeof st === "number" ? "error" : (st.status || "normal")) : "normal";
130
- const p95 = s.p95Ttfb && s.p95Ttfb >= 10 ? ` p95:${s.p95Ttfb}ms` : "";
131
- const ttfbStr = ttfb != null ? (ttfb >= 10 ? ttfb + "ms" : "—") : "—";
132
- const totalStr = total != null ? (total >= 10 ? total + "ms" : "—") : "—";
133
- const tpsStr = tps != null && tps >= 1 ? tps + " tok/s" : "—";
134
- console.log(` ${full.padEnd(28)} ${ttfbStr.padEnd(7)} ${totalStr.padEnd(7)} ${tpsStr.padEnd(10)} ${verbose.padEnd(7)} ${String(s.count).padEnd(5)} ${statusStr}${p95}`);
135
- }
136
- }
137
- } else if (!freeIds.length) {
138
- console.log(` (暂无模型样本 — 发一次 mslxdff -chat 后出现,100次后均值更稳)`);
139
- }
140
- } catch (e) {
141
- console.log(`\nmodels: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
142
- }
143
-
144
- try {
145
- const { getAutostartStatus } = await import("../autostart.js");
146
- const a = await getAutostartStatus();
147
- const detail = a.detail || (a.enabled ? "已启用" : "未启用");
148
- const label = detail.startsWith("已启用") || detail.startsWith("未启用") ? detail : `${a.enabled ? "已启用" : "未启用"} · ${detail}`;
149
- console.log(`\nautostart: ${label} — mslxdff -autostart status`);
150
- if (a.taskToRun) console.log(` task: ${a.taskToRun.slice(0, 120)}`);
151
- if (a.unit) console.log(` unit: ${a.unit}`);
152
- } catch {}
153
- try {
154
- const { resolvePluginDirs, loadPlugins } = await import("../plugins.js");
155
- const pkgRoot2 = dirname(fileURLToPath(import.meta.url)) + "/../..";
156
- const dirs = resolvePluginDirs({ pkgRoot: pkgRoot2 });
157
- const { plugins, errors } = await loadPlugins({ dirs });
158
- if (plugins.length || errors.length) {
159
- console.log(`plugins: ${plugins.length} loaded${errors.length ? `, ${errors.length} error(s)` : ""} — mslxdff -plugins`);
160
- for (const p of plugins) console.log(` ${p.name}${p.version ? `@${p.version}` : ""} [${Object.keys(p.hooks || {}).join(", ") || "no hooks"}]`);
161
- for (const e of errors) console.log(` ! ${e.file} — ${e.error}`);
162
- } else {
163
- console.log(`plugins: (none) — 放 *.mjs 到 ${dirs[dirs.length - 1] || "~/.config/mslxdff/plugins"} 启用`);
164
- }
165
- } catch {}
166
-
167
- const groups = createGroupsService({});
168
- const joined = loadGroupsJoined();
169
- if (joined.length) {
170
- console.log(`\njoined groups (${joined.length}):`);
171
- const { token } = await loadToken();
172
- for (const g of joined) {
173
- const isLeader = !g.leaderUrl;
174
- console.log(` ${g.name} ${isLeader ? "(this node is the leader)" : `leader ${g.leaderUrl}`}`);
175
- let members = null;
176
- if (isLeader) {
177
- members = groups.list()[g.name]?.members ?? {};
178
- } else {
179
- const fetchImpl = (url, opts) => compatFetch(url, { ...opts, signal: timeoutSignal(1500) });
180
- try {
181
- members = await refreshGroupMembers(g.name, {
182
- leaderUrl: g.leaderUrl,
183
- memberName: g.memberName,
184
- url: g.myUrl,
185
- token,
186
- fetchImpl,
187
- });
188
- } catch {
189
- members = null;
190
- }
191
- }
192
- if (members === null) {
193
- console.log(" members: (unavailable — leader unreachable)");
194
- } else {
195
- const ids = Object.keys(members);
196
- if (!ids.length) {
197
- console.log(" members: (none yet)");
198
- } else {
199
- console.log(` members (${ids.length}):`);
200
- for (const id of ids) {
201
- const m = members[id];
202
- const tags = [];
203
- if (id === "leader" && isLeader) tags.push("leader");
204
- if (m?.url && m.url === g.myUrl) tags.push("this node");
205
- if (m?.kind === "broadband") tags.push("broadband");
206
- if (m?.kind === "broadband" && m?.publicIp) tags.push(`ip=${m.publicIp}`);
207
- if (m?.kind === "broadband" && m?.lastSeen) {
208
- const ago = Math.round((Date.now() - m.lastSeen) / 1000);
209
- tags.push(ago < 90 ? `via leader ${ago}s ago` : "stale");
210
- }
211
- const tag = tags.length ? ` [${tags.join(", ")}]` : "";
212
- console.log(` ${m?.url || id}${tag}`);
213
- }
214
- }
215
- }
216
- }
217
- } else {
218
- console.log("\njoined groups: (none — use -creategroup or -addtogroup)");
219
- }
220
-
221
- const peers = createPeersService({});
222
- const allPeers = peers.all();
223
- if (allPeers.length) {
224
- console.log(`\nfailover targets (${allPeers.length}):`);
225
- for (const p of allPeers) {
226
- const tags = [];
227
- if (peers.isCooling(p.url)) tags.push("cooling");
228
- if (peers.isHot(p.url)) tags.push("hot");
229
- const s = peers.stat(p.url);
230
- if (s?.latencyMs != null) tags.push(`${s.latencyMs}ms`);
231
- if (s?.fails) tags.push(`${s.fails} fail(s)`);
232
- const tag = tags.length ? ` [${tags.join(", ")}]` : "";
233
- console.log(` ${p.name || p.url} ${p.url}${tag}`);
234
- }
235
- } else {
236
- console.log(`\nfailover: (none — 加组后自动出现,mslxdff -group list)`);
237
- }
238
-
239
- const groupNames = Object.keys(groups.list());
240
- if (groupNames.length) {
241
- console.log(`\ngroups on this node (${groupNames.length}):`);
242
- for (const n of groupNames) console.log(` ${n}`);
243
- }
244
-
245
- const { recentCalls, lastError } = await import("../logs.js");
246
- console.log("\nrecent calls: (gateway 持久化,最近5条;token/速度看 mslxdff -stats)");
247
- const calls = recentCalls(5);
248
- if (calls.length) {
249
- let sumDur = 0;
250
- for (const c of calls) {
251
- if (Number.isFinite(c.durationMs)) sumDur += c.durationMs;
252
- else if (Number.isFinite(c.totalMs)) sumDur += c.totalMs;
253
- }
254
- const avgDur = calls.length ? Math.round(sumDur / calls.length) : null;
255
- console.log(` avg ${avgDur ? avgDur + "ms" : "—"} — mslxdff -log 20 查看详情`);
256
- for (const c of calls) {
257
- const dur = c.totalMs ?? c.durationMs;
258
- const tm = fmtTs(c.ts);
259
- console.log(` ${tm} ${(c.model || "-").padEnd(28)} ${String(c.status || "-").padEnd(4)} ${dur ? dur + "ms" : ""}${c.stream ? " stream" : ""}${c.auto ? " auto" : ""}`);
260
- }
261
- } else {
262
- console.log(" (none yet — 发一次请求后出现,mslxdff -chat hi)");
263
- }
264
-
265
- console.log("\nlast error:");
266
- const err = lastError();
267
- if (err) {
268
- console.log(` ${fmtTs(err.ts)} ${err.model || "-"} ${err.status} ${err.message || ""}`);
269
- if (err.stack) console.log(` ${String(err.stack).slice(0, 200)}`);
270
- } else {
271
- console.log(" (none — 暂无错误,挺好)");
272
- }
273
-
274
- const daemon2 = readPid();
275
- const alive2 = daemon2 ? isPidAlive(daemon2) : false;
276
- if (daemon2 && alive2) console.log(`\nauth token: use \`mslxdff -showtoken\` · health: http://127.0.0.1:${port}/health`);
277
- else console.log(`\nnot running — start with: mslxdff -d · 查看日志 mslxdff -log 20`);
278
- console.log(`hints: mslxdff -providers list · mslxdff -model status · mslxdff -group list · mslxdff -autostart status`);
279
- }
1
+ import { readFileSync, existsSync, statSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readPid, readPidVersion, isPidAlive, pidFile } from "../daemon.js";
5
+ import { defaultStateFile, getPort, loadGroupsJoined, loadModelErrors, loadModelStats, loadModelPicks } from "../state.js";
6
+ import { resolvePort } from "../server.js";
7
+ import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderBaseUrl } from "../state.js";
8
+ import { loadToken } from "../state.js";
9
+ import { logDir } from "../logs.js";
10
+ import { createGroupsService } from "../groups.js";
11
+ import { createPeersService } from "../peers.js";
12
+ import { refreshGroupMembers } from "../groups.js";
13
+ import { fmtShanghaiYMDHM } from "../time.js";
14
+ import { fmtStatus, fmtUptime, fmtTs } from "./format.js";
15
+ import { compareSemver } from "./policy.js";
16
+ import { buildProviderRows, formatProviderRow, formatProviderSection } from "./provider-row.js";
17
+ import { compatFetch, timeoutSignal } from "../compat.js";
18
+
19
+ export async function printStatus(VERSION) {
20
+ const daemon = readPid();
21
+ const alive = daemon ? isPidAlive(daemon) : false;
22
+ const port = getPort() || resolvePort();
23
+ const persisted = getPort();
24
+ const portSrc = persisted != null ? "persisted" : (process.env.MSLXDFF_PORT ? "env MSLXDFF_PORT" : "default 8989");
25
+ const stateFile = defaultStateFile();
26
+ const dir = logDir();
27
+ let upStr = "";
28
+ if (daemon && alive) {
29
+ try {
30
+ const st = statSync(pidFile());
31
+ const ms = Date.now() - st.mtimeMs;
32
+ upStr = `, up ${fmtUptime(ms)}`;
33
+ } catch {}
34
+ }
35
+ const verNote = daemon && alive ? (() => { const v = readPidVersion(); if (!v) return ""; if (v === VERSION) return "version ok"; const cmp = compareSemver(VERSION, v); if (cmp > 0) return `version ${v} → ${VERSION} (upgrade pending)`; if (cmp < 0) return `version ${v} (newer than local ${VERSION}, keeping)`; return "version ok"; })() : "";
36
+ console.log(`mslxdff v${VERSION}`);
37
+ console.log(`daemon: ${daemon ? (alive ? `running (pid ${daemon}${upStr})` : `stale pid ${daemon} (not alive)`) : "not running"}${verNote ? ` [${verNote}]` : ""}`);
38
+ let healthLine = "";
39
+ try {
40
+ const t0 = Date.now();
41
+ const r = await compatFetch(`http://127.0.0.1:${port}/health`, { signal: timeoutSignal(1200) });
42
+ const ms = Date.now() - t0;
43
+ healthLine = r.ok ? `health ok ${ms}ms` : `health HTTP ${r.status} ${ms}ms`;
44
+ } catch (e) {
45
+ healthLine = daemon && alive ? `health fail (${String(e?.message || e).slice(0, 60)})` : "health — (daemon not running)";
46
+ }
47
+ console.log(`endpoint: http://localhost:${port}/v1 · ${healthLine}`);
48
+ console.log(`config: port ${port} (${portSrc}) · state ${stateFile} · log ${dir}`);
49
+ try {
50
+ const host = process.env.MSLXDFF_HOST || process.env.MSLXDFF_BIND_HOST || "0.0.0.0";
51
+ if (host !== "0.0.0.0") console.log(`bind: ${host}`);
52
+ } catch {}
53
+
54
+ try {
55
+ const providerRows = buildProviderRows({});
56
+ const enabledCount = providerRows.filter((r) => r.enabled).length;
57
+ console.log(`\nupstream providers ${providerRows.length} 个 · ${enabledCount} 已启用 — mslxdff -providers list 查看详情`);
58
+ console.log(formatProviderSection(providerRows));
59
+ if (enabledCount === 1 && providerRows.length === 1) {
60
+ console.log(`\n 空状态:仅 opencode 内置免费通道`);
61
+ console.log(` → 加一个私有上游:mslxdff -provider add bai https://api.b.ai/v1 <key>`);
62
+ console.log(` → 或 WorkBuddy 一键接入:node workbuddy-token-auto.js`);
63
+ }
64
+ } catch (e) {
65
+ console.log(`\nupstream providers: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
66
+ }
67
+
68
+ try {
69
+ const statuses = loadModelErrors();
70
+ const stats = loadModelStats();
71
+ const picks = loadModelPicks();
72
+ const { getPreferredModel: gpm } = await import("../auto.js");
73
+ const preferred = gpm();
74
+ const modelsFile = join(dir, "models.json");
75
+ let freeIds = [];
76
+ let cachedAt = null;
77
+ let cacheErr = null;
78
+ if (existsSync(modelsFile)) {
79
+ try {
80
+ const cached = JSON.parse(readFileSync(modelsFile, "utf8"));
81
+ freeIds = (cached.data || []).map((m) => m.id).filter(Boolean);
82
+ cachedAt = cached.cachedAt || null;
83
+ } catch (err) { cacheErr = String(err?.message || err).slice(0, 60); }
84
+ }
85
+ 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`; })() : "";
86
+ const cacheLine = freeIds.length ? `${freeIds.length} free${cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)} · ${ageStr})` : ""}` : (cacheErr ? `cache unreadable (${cacheErr})` : "not cached yet (daemon 拉取后出现)");
87
+ const prefStat = preferred ? (stats[preferred] || stats[`opencode/${preferred}`] || null) : null;
88
+ const prefErr = preferred ? statuses[preferred] : null;
89
+ const prefStatus = prefErr ? (typeof prefErr === "number" ? "error" : (prefErr?.status || "error")) : (prefStat ? "normal" : "");
90
+ const prefLine = preferred ? `${preferred}${prefStatus ? ` [${prefStatus}]` : ""}` : "(none)";
91
+ const picksLine = picks.length ? `${picks.length} (*${picks.slice(0, 4).join(", *")}${picks.length > 4 ? ` …+${picks.length - 4}` : ""})` : "(空=全量 auto)";
92
+ console.log(`\nmodels: ${cacheLine} — mslxdff -model status 查看详情`);
93
+ const prefTtfbDisp = prefStat ? (() => { const v = prefStat.avgTtfbMs ?? prefStat.emaTtfbMs; return v != null && v >= 10 ? `首字 ${v}ms` : (v != null && v < 10 ? "首字 — (测试数据)" : ""); })() : "";
94
+ const prefTotDisp = prefStat?.avgTotalMs && prefStat.avgTotalMs >= 10 ? `总 ${prefStat.avgTotalMs}ms` : "";
95
+ const prefTpsDisp = prefStat?.avgTps && prefStat.avgTps >= 1 ? `${prefStat.avgTps} tok/s` : "";
96
+ const prefExtra = prefStat ? ` ${[prefTtfbDisp, prefTotDisp, prefTpsDisp].filter(Boolean).join(" ")} ${prefStat.count || 0}次` : "";
97
+ console.log(` preferred: ${prefLine}${prefExtra}`);
98
+ console.log(` picks: ${picksLine} ${picks.length ? "auto 仅在勾选内" : "auto 用全量免费池"}`);
99
+ if (freeIds.length) {
100
+ const ids = freeIds;
101
+ console.log(` free list:`);
102
+ for (const id of ids) {
103
+ const st = fmtStatus(id, statuses);
104
+ const full = id.includes("/") ? id : `opencode/${id}`;
105
+ const ms = stats[full] || stats[id];
106
+ let extra = "";
107
+ if (ms) {
108
+ const v = ms.avgTtfbMs ?? ms.emaTtfbMs;
109
+ const vStr = v != null ? (v >= 10 ? `avg首字 ${v}ms` : (v < 10 ? "avg首字 —" : "")) : "";
110
+ const tpsStr = ms.avgTps && ms.avgTps >= 1 ? ` ${ms.avgTps} tok/s` : "";
111
+ const cntStr = ms.count ? ` ${ms.count}次` : "";
112
+ extra = ` ${vStr}${tpsStr}${cntStr}`;
113
+ }
114
+ console.log(` ${id}${st ? ` [${st}]` : ""}${extra}`);
115
+ }
116
+ }
117
+ const allStatIds = Object.keys(stats);
118
+ if (allStatIds.length) {
119
+ 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);
120
+ if (sorted.length) {
121
+ console.log(` 模型体检 Top${sorted.length}(样本>0, 按次数):`);
122
+ console.log(` ${"模型".padEnd(28)} ${"首字".padEnd(7)} ${"总耗时".padEnd(7)} ${"速度".padEnd(10)} ${"啰嗦".padEnd(7)} ${"样本".padEnd(5)} 状态`);
123
+ for (const { full, s } of sorted) {
124
+ const ttfb = s.avgTtfbMs ?? s.emaTtfbMs;
125
+ const total = s.avgTotalMs ?? s.emaTotalMs;
126
+ const tps = s.avgTps ?? s.emaTps;
127
+ const verbose = s.avgCompTok != null ? `${s.avgCompTok}tok` : "—";
128
+ const st = statuses[full] || statuses[full.split("/").slice(1).join("/")] || null;
129
+ const statusStr = st ? (typeof st === "number" ? "error" : (st.status || "normal")) : "normal";
130
+ const p95 = s.p95Ttfb && s.p95Ttfb >= 10 ? ` p95:${s.p95Ttfb}ms` : "";
131
+ const ttfbStr = ttfb != null ? (ttfb >= 10 ? ttfb + "ms" : "—") : "—";
132
+ const totalStr = total != null ? (total >= 10 ? total + "ms" : "—") : "—";
133
+ const tpsStr = tps != null && tps >= 1 ? tps + " tok/s" : "—";
134
+ console.log(` ${full.padEnd(28)} ${ttfbStr.padEnd(7)} ${totalStr.padEnd(7)} ${tpsStr.padEnd(10)} ${verbose.padEnd(7)} ${String(s.count).padEnd(5)} ${statusStr}${p95}`);
135
+ }
136
+ }
137
+ } else if (!freeIds.length) {
138
+ console.log(` (暂无模型样本 — 发一次 mslxdff -chat 后出现,100次后均值更稳)`);
139
+ }
140
+ } catch (e) {
141
+ console.log(`\nmodels: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
142
+ }
143
+
144
+ try {
145
+ const { getAutostartStatus } = await import("../autostart.js");
146
+ const a = await getAutostartStatus();
147
+ const detail = a.detail || (a.enabled ? "已启用" : "未启用");
148
+ const label = detail.startsWith("已启用") || detail.startsWith("未启用") ? detail : `${a.enabled ? "已启用" : "未启用"} · ${detail}`;
149
+ console.log(`\nautostart: ${label} — mslxdff -autostart status`);
150
+ if (a.taskToRun) console.log(` task: ${a.taskToRun.slice(0, 120)}`);
151
+ if (a.unit) console.log(` unit: ${a.unit}`);
152
+ } catch {}
153
+ try {
154
+ const { resolvePluginDirs, loadPlugins } = await import("../plugins.js");
155
+ const pkgRoot2 = dirname(fileURLToPath(import.meta.url)) + "/../..";
156
+ const dirs = resolvePluginDirs({ pkgRoot: pkgRoot2 });
157
+ const { plugins, errors } = await loadPlugins({ dirs });
158
+ if (plugins.length || errors.length) {
159
+ console.log(`plugins: ${plugins.length} loaded${errors.length ? `, ${errors.length} error(s)` : ""} — mslxdff -plugins`);
160
+ for (const p of plugins) console.log(` ${p.name}${p.version ? `@${p.version}` : ""} [${Object.keys(p.hooks || {}).join(", ") || "no hooks"}]`);
161
+ for (const e of errors) console.log(` ! ${e.file} — ${e.error}`);
162
+ } else {
163
+ console.log(`plugins: (none) — 放 *.mjs 到 ${dirs[dirs.length - 1] || "~/.config/mslxdff/plugins"} 启用`);
164
+ }
165
+ } catch {}
166
+
167
+ const groups = createGroupsService({});
168
+ const joined = loadGroupsJoined();
169
+ if (joined.length) {
170
+ console.log(`\njoined groups (${joined.length}):`);
171
+ const { token } = await loadToken();
172
+ for (const g of joined) {
173
+ const isLeader = !g.leaderUrl;
174
+ console.log(` ${g.name} ${isLeader ? "(this node is the leader)" : `leader ${g.leaderUrl}`}`);
175
+ let members = null;
176
+ if (isLeader) {
177
+ members = groups.list()[g.name]?.members ?? {};
178
+ } else {
179
+ const fetchImpl = (url, opts) => compatFetch(url, { ...opts, signal: timeoutSignal(1500) });
180
+ try {
181
+ members = await refreshGroupMembers(g.name, {
182
+ leaderUrl: g.leaderUrl,
183
+ memberName: g.memberName,
184
+ url: g.myUrl,
185
+ token,
186
+ fetchImpl,
187
+ });
188
+ } catch {
189
+ members = null;
190
+ }
191
+ }
192
+ if (members === null) {
193
+ console.log(" members: (unavailable — leader unreachable)");
194
+ } else {
195
+ const ids = Object.keys(members);
196
+ if (!ids.length) {
197
+ console.log(" members: (none yet)");
198
+ } else {
199
+ console.log(` members (${ids.length}):`);
200
+ for (const id of ids) {
201
+ const m = members[id];
202
+ const tags = [];
203
+ if (id === "leader" && isLeader) tags.push("leader");
204
+ if (m?.url && m.url === g.myUrl) tags.push("this node");
205
+ if (m?.kind === "broadband") tags.push("broadband");
206
+ if (m?.kind === "broadband" && m?.publicIp) tags.push(`ip=${m.publicIp}`);
207
+ if (m?.kind === "broadband" && m?.lastSeen) {
208
+ const ago = Math.round((Date.now() - m.lastSeen) / 1000);
209
+ tags.push(ago < 90 ? `via leader ${ago}s ago` : "stale");
210
+ }
211
+ const tag = tags.length ? ` [${tags.join(", ")}]` : "";
212
+ console.log(` ${m?.url || id}${tag}`);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ } else {
218
+ console.log("\njoined groups: (none — use -creategroup or -addtogroup)");
219
+ }
220
+
221
+ const peers = createPeersService({});
222
+ const allPeers = peers.all();
223
+ if (allPeers.length) {
224
+ console.log(`\nfailover targets (${allPeers.length}):`);
225
+ for (const p of allPeers) {
226
+ const tags = [];
227
+ if (peers.isCooling(p.url)) tags.push("cooling");
228
+ if (peers.isHot(p.url)) tags.push("hot");
229
+ const s = peers.stat(p.url);
230
+ if (s?.latencyMs != null) tags.push(`${s.latencyMs}ms`);
231
+ if (s?.fails) tags.push(`${s.fails} fail(s)`);
232
+ const tag = tags.length ? ` [${tags.join(", ")}]` : "";
233
+ console.log(` ${p.name || p.url} ${p.url}${tag}`);
234
+ }
235
+ } else {
236
+ console.log(`\nfailover: (none — 加组后自动出现,mslxdff -group list)`);
237
+ }
238
+
239
+ const groupNames = Object.keys(groups.list());
240
+ if (groupNames.length) {
241
+ console.log(`\ngroups on this node (${groupNames.length}):`);
242
+ for (const n of groupNames) console.log(` ${n}`);
243
+ }
244
+
245
+ const { recentCalls, lastError } = await import("../logs.js");
246
+ console.log("\nrecent calls: (gateway 持久化,最近5条;token/速度看 mslxdff -stats)");
247
+ const calls = recentCalls(5);
248
+ if (calls.length) {
249
+ let sumDur = 0;
250
+ for (const c of calls) {
251
+ if (Number.isFinite(c.durationMs)) sumDur += c.durationMs;
252
+ else if (Number.isFinite(c.totalMs)) sumDur += c.totalMs;
253
+ }
254
+ const avgDur = calls.length ? Math.round(sumDur / calls.length) : null;
255
+ console.log(` avg ${avgDur ? avgDur + "ms" : "—"} — mslxdff -log 20 查看详情`);
256
+ for (const c of calls) {
257
+ const dur = c.totalMs ?? c.durationMs;
258
+ const tm = fmtTs(c.ts);
259
+ console.log(` ${tm} ${(c.model || "-").padEnd(28)} ${String(c.status || "-").padEnd(4)} ${dur ? dur + "ms" : ""}${c.stream ? " stream" : ""}${c.auto ? " auto" : ""}`);
260
+ }
261
+ } else {
262
+ console.log(" (none yet — 发一次请求后出现,mslxdff -chat hi)");
263
+ }
264
+
265
+ console.log("\nlast error:");
266
+ const err = lastError();
267
+ if (err) {
268
+ console.log(` ${fmtTs(err.ts)} ${err.model || "-"} ${err.status} ${err.message || ""}`);
269
+ if (err.stack) console.log(` ${String(err.stack).slice(0, 200)}`);
270
+ } else {
271
+ console.log(" (none — 暂无错误,挺好)");
272
+ }
273
+
274
+ const daemon2 = readPid();
275
+ const alive2 = daemon2 ? isPidAlive(daemon2) : false;
276
+ if (daemon2 && alive2) console.log(`\nauth token: use \`mslxdff -showtoken\` · health: http://127.0.0.1:${port}/health`);
277
+ else console.log(`\nnot running — start with: mslxdff -d · 查看日志 mslxdff -log 20`);
278
+ console.log(`hints: mslxdff -providers list · mslxdff -model status · mslxdff -group list · mslxdff -autostart status`);
279
+ }