mslxdff 0.1.65 → 0.1.66

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 (42) hide show
  1. package/bin/mslxdff.js +3 -3159
  2. package/package.json +1 -1
  3. package/src/cli/bootstrap.js +408 -0
  4. package/src/cli/commands/daemon.js +151 -0
  5. package/src/cli/commands/group.js +272 -0
  6. package/src/cli/commands/model.js +405 -0
  7. package/src/cli/commands/provider/add.js +106 -0
  8. package/src/cli/commands/provider/allowlist.js +99 -0
  9. package/src/cli/commands/provider/config.js +82 -0
  10. package/src/cli/commands/provider/index.js +193 -0
  11. package/src/cli/commands/provider/keys.js +135 -0
  12. package/src/cli/commands/provider/models.js +99 -0
  13. package/src/cli/commands/provider.js +1 -0
  14. package/src/cli/commands/sync.js +143 -0
  15. package/src/cli/commands/system.js +236 -0
  16. package/src/cli/commands/workbuddy.js +91 -0
  17. package/src/cli/format.js +119 -0
  18. package/src/cli/group-helpers.js +69 -0
  19. package/src/cli/help.js +66 -0
  20. package/src/cli/index.js +59 -0
  21. package/src/cli/interactive.js +84 -0
  22. package/src/cli/policy.js +119 -0
  23. package/src/cli/status.js +332 -0
  24. package/src/cli/util.js +24 -0
  25. package/src/providers/base.js +159 -0
  26. package/src/providers/generic.js +26 -166
  27. package/src/providers/openrouter.js +32 -162
  28. package/src/providers/workbuddy.js +7 -32
  29. package/src/routes/chat/gateway.js +301 -0
  30. package/src/routes/chat/index.js +1 -384
  31. package/src/state/facade.js +57 -0
  32. package/src/state/memory.js +180 -0
  33. package/src/state/persist.js +42 -0
  34. package/src/state/schemas/allowlist.js +87 -0
  35. package/src/state/schemas/group.js +31 -0
  36. package/src/state/schemas/model.js +53 -0
  37. package/src/state/schemas/peer.js +31 -0
  38. package/src/state/schemas/port.js +10 -0
  39. package/src/state/schemas/provider.js +254 -0
  40. package/src/state/schemas/token.js +43 -0
  41. package/src/state/store.js +202 -0
  42. package/src/state.js +1 -711
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,408 @@
1
+ import { join, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { startServer } from "../server.js";
4
+ import { defaultStateFile } from "../state.js";
5
+ import { createRouter } from "../routes.js";
6
+ import { createUpstreamClient } from "../upstream.js";
7
+ import { createModelsService } from "../models.js";
8
+ import { loadToken } from "../state.js";
9
+ import { createAutoSelector } from "../auto.js";
10
+ import { createPeersService } from "../peers.js";
11
+ import { createEventBus } from "../events.js";
12
+ import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../groups.js";
13
+ import { logDir, appendCall, appendError, appendEvent } from "../logs.js";
14
+ import { loadPlugins, runHook, resolvePluginDirs } from "../plugins.js";
15
+ import { createOpenCodeProvider } from "../providers/opencode.js";
16
+ import { loadProviderKeys, loadProviderAuths, loadProviderConfigs } from "../state.js";
17
+ import { effectiveHost, refreshIntervalMs, modelCooldownMs, slowCooldownMs, peerCooldownMs, peerHeatMs, maxHopsValue, groupSyncIntervalMs, autoUpdateIntervalMs, banWindowMs, banThreshold } from "./policy.js";
18
+ import { fmtEvent } from "./format.js";
19
+ import { errMsg, npmCmd, run } from "./util.js";
20
+ import { loadGroupsJoined } from "../state.js";
21
+ import { writePid } from "../daemon.js";
22
+ import { resolvePort } from "../server.js";
23
+
24
+ const logs = { appendCall, appendError, appendEvent };
25
+
26
+ export async function startDaemonMain(VERSION) {
27
+ const { token, created } = await loadToken();
28
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
29
+ // src/cli/bootstrap.js -> 2 levels up to src, then 1 to root
30
+ const pkgRoot2 = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
31
+ const pluginDirs = resolvePluginDirs({ pkgRoot: pkgRoot2 });
32
+ const { plugins: loadedPlugins, errors: pluginErrors } = await loadPlugins({ dirs: pluginDirs });
33
+ for (const e of pluginErrors) {
34
+ console.log(`plugin load failed: ${e.file} — ${e.error}`);
35
+ appendEvent({ ts: Date.now(), type: "plugin-load-error", file: e.file, error: e.error });
36
+ }
37
+ if (loadedPlugins.length) {
38
+ console.log(`plugins loaded (${loadedPlugins.length}): ${loadedPlugins.map((p) => `${p.name}${p.version ? `@${p.version}` : ""}`).join(", ")}`);
39
+ appendEvent({ ts: Date.now(), type: "plugins-loaded", plugins: loadedPlugins.map((p) => ({ name: p.name, version: p.version })) });
40
+ }
41
+ const upstreamHooks = loadedPlugins.length
42
+ ? (name, ctx) => runHook(loadedPlugins, name, ctx)
43
+ : null;
44
+ const providerPlugin = loadedPlugins.find((p) => typeof p.createUpstream === "function");
45
+ let upstream;
46
+ const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
47
+ let providers = [];
48
+ if (providerPlugin) {
49
+ if (!upstream) {
50
+ try {
51
+ upstream = await providerPlugin.createUpstream({ baseUrl, authToken: process.env.UPSTREAM_AUTH_TOKEN || "public", env: process.env });
52
+ console.log(`upstream provider replaced by plugin: ${providerPlugin.name}`);
53
+ appendEvent({ ts: Date.now(), type: "plugin-upstream-active", plugin: providerPlugin.name });
54
+ } catch (err) {
55
+ console.log(`plugin upstream (${providerPlugin.name}) failed: ${errMsg(err)} — falling back to default`);
56
+ appendEvent({ ts: Date.now(), type: "plugin-upstream-error", plugin: providerPlugin.name, error: errMsg(err) });
57
+ }
58
+ }
59
+ if (!upstream) upstream = createUpstreamClient({ hooks: upstreamHooks });
60
+ } else {
61
+ const opencodeClient = createUpstreamClient({ hooks: upstreamHooks });
62
+ const opencodeModels = createModelsService({
63
+ baseUrl,
64
+ headers: opencodeClient.headers,
65
+ refreshMs: refreshIntervalMs(),
66
+ cacheFile: join(logDir(), "models.json"),
67
+ });
68
+ providers.push(createOpenCodeProvider({ upstream: opencodeClient, modelsService: opencodeModels }));
69
+ const orKeys = loadProviderKeys("openrouter");
70
+ if (orKeys.length) {
71
+ const { createOpenRouterProvider } = await import("../providers/openrouter.js");
72
+ providers.push(createOpenRouterProvider({ apiKeys: orKeys }));
73
+ console.log(`provider enabled: openrouter (${orKeys.length} key${orKeys.length > 1 ? "s" : ""})`);
74
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: "openrouter", keys: orKeys.length });
75
+ }
76
+ const genericConfigs = loadProviderConfigs();
77
+ for (const [gid, cfg] of Object.entries(genericConfigs)) {
78
+ if (gid === "opencode" || gid === "openrouter") continue;
79
+ if (gid === "workbuddy") {
80
+ const base = String(cfg?.baseUrl || "").trim() || "https://copilot.tencent.com";
81
+ const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
82
+ const auths = Array.isArray(cfg?.auths) ? cfg.auths : [];
83
+ if (!keys.length) continue;
84
+ try {
85
+ const { createWorkbuddyProvider } = await import("../providers/workbuddy.js");
86
+ providers.push(createWorkbuddyProvider({ baseUrl: base, apiKeys: keys, auths }));
87
+ console.log(`provider enabled: workbuddy (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base}`);
88
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base });
89
+ } catch (err) {
90
+ console.log(`provider ${gid} failed: ${err?.message || err}`);
91
+ appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
92
+ }
93
+ continue;
94
+ }
95
+ const base = String(cfg?.baseUrl || "").trim();
96
+ const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
97
+ if (!base || !keys.length) continue;
98
+ try {
99
+ const { createGenericProvider } = await import("../providers/generic.js");
100
+ providers.push(createGenericProvider({ id: gid, baseUrl: base, apiKeys: keys }));
101
+ console.log(`provider enabled: ${gid} (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base}`);
102
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base });
103
+ } catch (err) {
104
+ console.log(`provider ${gid} failed: ${err?.message || err}`);
105
+ appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
106
+ }
107
+ }
108
+ if (!genericConfigs["workbuddy"]) {
109
+ const wbKeys = loadProviderKeys("workbuddy");
110
+ if (wbKeys.length) {
111
+ try {
112
+ const { createWorkbuddyProvider } = await import("../providers/workbuddy.js");
113
+ const wbAuths = loadProviderAuths("workbuddy");
114
+ providers.push(createWorkbuddyProvider({ apiKeys: wbKeys, auths: wbAuths }));
115
+ console.log(`provider enabled: workbuddy (${wbKeys.length} key${wbKeys.length > 1 ? "s" : ""}) baseUrl=https://copilot.tencent.com (env)`);
116
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: "workbuddy", keys: wbKeys.length, baseUrl: "https://copilot.tencent.com" });
117
+ } catch (err) {
118
+ console.log(`provider workbuddy failed: ${err?.message || err}`);
119
+ }
120
+ }
121
+ }
122
+ const { createProviderDispatcher } = await import("../providers/dispatcher.js");
123
+ upstream = createProviderDispatcher(providers);
124
+ appendEvent({ ts: Date.now(), type: "providers", providers: providers.map((p) => p.id) });
125
+ }
126
+ const opencodeProvider = providers.find((p) => p.id === "opencode");
127
+ const models = createModelsService({
128
+ providers: providers.length > 1 ? providers : undefined,
129
+ baseUrl,
130
+ headers: upstream.headers || opencodeProvider?.upstream?.headers,
131
+ refreshMs: refreshIntervalMs(),
132
+ cacheFile: join(logDir(), "models.json"),
133
+ });
134
+ const auto = createAutoSelector({
135
+ cooldownMs: modelCooldownMs(),
136
+ slowCooldownMs: slowCooldownMs(),
137
+ file: defaultStateFile(),
138
+ loadCandidates: async () => {
139
+ try {
140
+ return (await models.get()).data.map((m) => m.id);
141
+ } catch {
142
+ return null;
143
+ }
144
+ },
145
+ });
146
+ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHeatMs() });
147
+ const groups = createGroupsService({});
148
+ const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
149
+
150
+ const isDebug = process.env.MSLXDFF_DEBUG === "1";
151
+ const bus = createEventBus();
152
+
153
+ try {
154
+ const { loadModelAliases } = await import("../providers/model-id.js");
155
+ loadModelAliases();
156
+ } catch {}
157
+
158
+ const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus, plugins: loadedPlugins });
159
+ const listenHost = effectiveHost();
160
+ const srv = startServer({
161
+ router,
162
+ signals: !isDebug,
163
+ host: listenHost,
164
+ onBeforeClose: loadedPlugins.length
165
+ ? () => runHook(loadedPlugins, "server:stop", { version: VERSION }).then(() => {})
166
+ : undefined,
167
+ });
168
+
169
+ if (isDebug) {
170
+ bus.subscribe((e) => {
171
+ try {
172
+ console.log(fmtEvent(e));
173
+ } catch {}
174
+ });
175
+ const { startDaemon: sd } = await import("../daemon.js");
176
+ const restore2 = () => {
177
+ console.log("\n[debug] restoring background daemon...");
178
+ try {
179
+ const restoredPid = sd([]);
180
+ console.log(`[debug] daemon restored (pid ${restoredPid})`);
181
+ } catch (err) {
182
+ console.error(`[debug] could not restore daemon: ${err.message}`);
183
+ }
184
+ setTimeout(() => process.exit(0), 300);
185
+ };
186
+ process.on("SIGINT", restore2);
187
+ process.on("SIGTERM", restore2);
188
+ }
189
+
190
+ await srv.ready();
191
+
192
+ if (loadedPlugins.length) {
193
+ runHook(loadedPlugins, "server:start", { port: srv.server.address()?.port, host: listenHost, version: VERSION }).catch(() => {});
194
+ const eventPlugins = loadedPlugins.filter((p) => typeof p.onEvent === "function");
195
+ if (eventPlugins.length) {
196
+ bus.subscribe((e) => {
197
+ for (const p of eventPlugins) {
198
+ try { p.onEvent(e); } catch {}
199
+ }
200
+ });
201
+ }
202
+ }
203
+
204
+ setTimeout(() => {
205
+ upstream.preheat().then((r) => {
206
+ const entry = { ts: Date.now(), type: "upstream-preheat", ...r, baseUrl };
207
+ try { bus.emit(entry); } catch {}
208
+ try { logs.appendEvent(entry); } catch {}
209
+ if (r.skipped) console.log(`[preheat] skipped (MSLXDFF_PREHEAT disabled)`);
210
+ else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
211
+ else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
212
+ }).catch(() => {});
213
+ }, 100).unref?.();
214
+
215
+ models.startAutoRefresh();
216
+ if (process.env.MSLXDFF_DAEMON) {
217
+ writePid(process.pid, VERSION);
218
+ }
219
+ const addr = srv.server.address();
220
+ const host = addr.address === "0.0.0.0" || addr.address === "::" ? "localhost" : addr.address;
221
+ console.log(`mslxdff v${VERSION} listening on http://${host}:${addr.port}`);
222
+ if (created) {
223
+ console.log(`auth token: ${token}`);
224
+ }
225
+ console.log(`endpoint: http://${host}:${addr.port}/v1`);
226
+ try {
227
+ const { hedgeDelayMs } = await import("../routes/hedge.js");
228
+ const hd = hedgeDelayMs();
229
+ console.log(`hedge: ${hd ? `${hd}ms` : "off"} (MSLXDFF_HEDGE_DELAY_MS)`);
230
+ } catch {}
231
+
232
+ // group sync
233
+ const { syncAllJoinedGroups } = await import("./group-helpers.js");
234
+ syncAllJoinedGroups({ peers, groups })
235
+ .then((results) => {
236
+ for (const r of results) {
237
+ if (r.error) console.log(`group sync ${r.name}: failed — ${r.error}`);
238
+ else console.log(`group sync ${r.name}: ${r.total} member(s), ${r.added} peer(s)`);
239
+ }
240
+ })
241
+ .catch((err) => console.log(`group sync: ${errMsg(err)}`));
242
+ const groupSyncTimer = setInterval(() => {
243
+ syncAllJoinedGroups({ peers, groups })
244
+ .then((results) => {
245
+ for (const r of results) {
246
+ if (r.error) console.log(`group sync ${r.name}: failed — ${r.error}`);
247
+ else if (r.added) console.log(`group sync ${r.name}: ${r.total} member(s), ${r.added} peer(s)`);
248
+ }
249
+ })
250
+ .catch((err) => console.log(`group sync: ${errMsg(err)}`));
251
+ }, groupSyncIntervalMs());
252
+ groupSyncTimer.unref();
253
+
254
+ const broadbandGroups = () => loadGroupsJoined().filter((g) => g.kind === "broadband" && g.leaderUrl);
255
+ if (broadbandGroups().length) {
256
+ const doHeartbeat = async () => {
257
+ for (const g of broadbandGroups()) {
258
+ try {
259
+ const res = await fetch(`${g.leaderUrl}/v1/groups/relay/heartbeat`, {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
262
+ body: JSON.stringify({ name: g.name, group: g.name }),
263
+ signal: AbortSignal.timeout(5000),
264
+ });
265
+ if (!res.ok) {
266
+ const txt = await res.text().catch(() => "");
267
+ console.log(`broadband heartbeat ${g.name}: ${res.status} ${txt.slice(0, 100)}`);
268
+ }
269
+ } catch (err) {
270
+ console.log(`broadband heartbeat ${g.name}: failed — ${errMsg(err)}`);
271
+ }
272
+ }
273
+ };
274
+ const doPoll = async () => {
275
+ for (const g of broadbandGroups()) {
276
+ try {
277
+ const pollRes = await fetch(`${g.leaderUrl}/v1/groups/relay/poll`, {
278
+ method: "POST",
279
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
280
+ body: JSON.stringify({ name: g.name, group: g.name }),
281
+ signal: AbortSignal.timeout(8000),
282
+ });
283
+ if (!pollRes.ok) continue;
284
+ const data = await pollRes.json().catch(() => ({}));
285
+ const items = data.data || [];
286
+ for (const item of items) {
287
+ const { reqId, body } = item;
288
+ let result;
289
+ try {
290
+ const upRes = await upstream.chat(body);
291
+ const ct = upRes.headers.get("content-type") || "";
292
+ const isStream = Boolean(body?.stream) || ct.includes("text/event-stream");
293
+ if (isStream && upRes.body) {
294
+ let collected = "";
295
+ for await (const chunk of upRes.body) {
296
+ collected += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
297
+ }
298
+ result = { status: upRes.status, headers: { "Content-Type": "text/event-stream" }, body: collected };
299
+ } else {
300
+ const txt = await upRes.text();
301
+ let parsed;
302
+ try { parsed = JSON.parse(txt); } catch { parsed = txt; }
303
+ result = { status: upRes.status, headers: { "Content-Type": upRes.headers.get("content-type") || "application/json" }, body: typeof parsed === "string" ? parsed : JSON.stringify(parsed) };
304
+ }
305
+ } catch (err) {
306
+ result = { status: 502, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: errMsg(err) }) };
307
+ }
308
+ try {
309
+ await fetch(`${g.leaderUrl}/v1/groups/relay/result`, {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
312
+ body: JSON.stringify({ name: g.name, group: g.name, reqId, result }),
313
+ signal: AbortSignal.timeout(5000),
314
+ });
315
+ } catch {}
316
+ }
317
+ } catch {}
318
+ }
319
+ };
320
+ doHeartbeat().catch(() => {});
321
+ const hbTimer = setInterval(doHeartbeat, 30_000);
322
+ hbTimer.unref();
323
+ const pollTimer = setInterval(doPoll, 1000);
324
+ pollTimer.unref();
325
+ console.log(`broadband relay: heartbeat 30s + poll 1s for ${broadbandGroups().length} group(s)`);
326
+ }
327
+
328
+ const autoUpdateMs = autoUpdateIntervalMs();
329
+ function emitAutoUpdate(type, data = {}) {
330
+ const entry = { ts: Date.now(), type, ...data };
331
+ try { bus?.emit(entry); } catch {}
332
+ try { logs?.appendEvent?.(entry); } catch {}
333
+ const line = `[auto-update] ${type} ${JSON.stringify(data)}`;
334
+ console.log(line);
335
+ }
336
+ if (autoUpdateMs) {
337
+ console.log(`auto-update enabled: checking every ${Math.round(autoUpdateMs / 60000)}m`);
338
+ emitAutoUpdate("auto-update-enabled", { intervalMs: autoUpdateMs, current: VERSION });
339
+ setTimeout(() => {
340
+ emitAutoUpdate("auto-update-check", { current: VERSION });
341
+ checkAndAutoUpdate().catch((err) => {
342
+ console.log(`auto-update check failed: ${errMsg(err)}`);
343
+ emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
344
+ });
345
+ }, 30_000).unref?.();
346
+ const autoUpdateTimer = setInterval(() => {
347
+ emitAutoUpdate("auto-update-check", { current: VERSION });
348
+ checkAndAutoUpdate().catch((err) => {
349
+ console.log(`auto-update check failed: ${errMsg(err)}`);
350
+ emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
351
+ });
352
+ }, autoUpdateMs);
353
+ autoUpdateTimer.unref();
354
+ } else {
355
+ console.log(`auto-update disabled (set MSLXDFF_AUTO_UPDATE=1 to enable hourly)`);
356
+ emitAutoUpdate("auto-update-disabled", { current: VERSION });
357
+ }
358
+
359
+ async function checkAndAutoUpdate() {
360
+ emitAutoUpdate("auto-update-query", { current: VERSION });
361
+ const info = await run(npmCmd(), ["view", "mslxdff", "dist-tags.latest", "--json"]);
362
+ if (info.err) {
363
+ emitAutoUpdate("auto-update-query-failed", { error: info.err.message || String(info.stderr || "").slice(0, 500) });
364
+ throw new Error(info.err.message || String(info.stderr || "").slice(0, 500));
365
+ }
366
+ let latest = "";
367
+ try {
368
+ latest = JSON.parse(String(info.stdout || "").trim());
369
+ if (Array.isArray(latest)) latest = latest[latest.length - 1];
370
+ latest = String(latest || "").replace(/^v/, "").trim();
371
+ } catch {
372
+ const raw = String(info.stdout || "").trim();
373
+ const m = raw.match(/(\d+\.\d+\.\d+[^\s'"]*)/);
374
+ latest = m ? m[1] : raw.split(/\s+/).pop()?.replace(/['"]/g, "") || "";
375
+ }
376
+ latest = latest.replace(/['"]/g, "").trim();
377
+ emitAutoUpdate("auto-update-queried", { current: VERSION, latest, stdout: String(info.stdout || "").trim().slice(0, 200) });
378
+ if (!latest || latest === VERSION) {
379
+ emitAutoUpdate("auto-update-noop", { current: VERSION, latest });
380
+ return;
381
+ }
382
+ const { compareSemver } = await import("./policy.js");
383
+ if (compareSemver(latest, VERSION) <= 0) {
384
+ emitAutoUpdate("auto-update-noop", { current: VERSION, latest, reason: "not newer" });
385
+ return;
386
+ }
387
+ emitAutoUpdate("auto-update-found", { current: VERSION, latest });
388
+ console.log(`auto-update: v${VERSION} -> v${latest}, installing...`);
389
+ emitAutoUpdate("auto-update-installing", { current: VERSION, latest });
390
+ const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`]);
391
+ if (up.err) {
392
+ emitAutoUpdate("auto-update-install-failed", { current: VERSION, latest, error: up.err.message || String(up.stderr || "").slice(0, 500) });
393
+ throw new Error(up.err.message || String(up.stderr || "").slice(0, 500));
394
+ }
395
+ emitAutoUpdate("auto-update-installed", { current: VERSION, latest, stdout: String(up.stdout || "").slice(0, 500) });
396
+ console.log(`auto-update: installed v${latest}, restarting daemon...`);
397
+ emitAutoUpdate("auto-update-restarting", { current: VERSION, latest });
398
+ const { stopDaemon, startDaemon } = await import("../daemon.js");
399
+ try { stopDaemon(); } catch (e) { emitAutoUpdate("auto-update-stop-failed", { error: errMsg(e) }); }
400
+ const { waitForHealth } = await import("./policy.js");
401
+ const newPid = startDaemon([]);
402
+ await waitForHealth(resolvePort(), 8000);
403
+ console.log(`auto-update: restarted as v${latest} (pid ${newPid})`);
404
+ emitAutoUpdate("auto-update-restarted", { current: VERSION, latest, newPid });
405
+ }
406
+ }
407
+
408
+
@@ -0,0 +1,151 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import { startDaemon, stopDaemon, readPid, readPidVersion, isPidAlive, pidFile, logFile } from "../../daemon.js";
3
+ import { setPort } from "../../state.js";
4
+ import { logDir, eventsFile, callsFile, errorsFile } from "../../logs.js";
5
+ import { effectivePort, waitForHealth, stopDaemonIfOutdated, compareSemver, argValue } from "../policy.js";
6
+ import { printStatus } from "../status.js";
7
+
8
+ export async function handleStop(args) {
9
+ if (!(args.includes("-stop") || args.includes("--stop"))) return false;
10
+ const { stopped, pid, reason } = stopDaemon();
11
+ if (stopped) {
12
+ console.log(`mslxdff daemon stopped (pid ${pid})`);
13
+ } else {
14
+ console.log(`mslxdff daemon not running${reason ? ` (${reason})` : ""}`);
15
+ }
16
+ process.exit(0);
17
+ }
18
+
19
+ export async function handleRestart(args, VERSION) {
20
+ if (!((args.includes("-restart") || args.includes("--restart")) && !process.env.MSLXDFF_DAEMON)) return false;
21
+ const pid = readPid();
22
+ const alive = pid ? isPidAlive(pid) : false;
23
+ if (alive) {
24
+ console.log(`restarting daemon (pid ${pid})...`);
25
+ stopDaemon();
26
+ await new Promise((r) => setTimeout(r, 300));
27
+ } else if (pid) {
28
+ console.log(`daemon pid ${pid} is stale (not running) — starting fresh...`);
29
+ try { stopDaemon(); } catch {}
30
+ } else {
31
+ console.log(`daemon not running — starting...`);
32
+ }
33
+ const port = effectivePort(args);
34
+ const spawnedPid = startDaemon([]);
35
+ await waitForHealth(port, 4000);
36
+ let ok = false;
37
+ try {
38
+ const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1200) });
39
+ ok = r.ok;
40
+ } catch {}
41
+ if (ok) console.log(`mslxdff v${VERSION} restarted as a background daemon (pid ${spawnedPid})`);
42
+ else console.log(`mslxdff v${VERSION} restarted (pid ${spawnedPid}) — health check pending (http://127.0.0.1:${port}/health)`);
43
+ console.log(`endpoint: http://localhost:${port}/v1`);
44
+ console.log(`log: ${logFile()}`);
45
+ console.log(`pid: ${pidFile()}`);
46
+ process.exit(0);
47
+ }
48
+
49
+ export async function handlePort(args) {
50
+ const portArg = argValue(args, "-port", "--port");
51
+ if (!portArg || process.env.MSLXDFF_DAEMON) return false;
52
+ const port = Number(portArg);
53
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
54
+ console.error(`invalid port: ${portArg}`);
55
+ process.exit(1);
56
+ }
57
+ setPort(port);
58
+ const daemon = readPid();
59
+ if (daemon) {
60
+ stopDaemon();
61
+ startDaemon(["-port", String(port)]);
62
+ await waitForHealth(port, 4000);
63
+ console.log(`mslxdff restarted on port ${port} (pid ${readPid()})`);
64
+ console.log(`endpoint: http://localhost:${port}/v1`);
65
+ } else {
66
+ console.log(`port saved: ${port} (daemon not running; takes effect on next start)`);
67
+ }
68
+ process.exit(0);
69
+ }
70
+
71
+ export async function handleDaemonFlag(args, VERSION) {
72
+ if (!(args.includes("-d") || args.includes("--daemon"))) return false;
73
+ if (!process.env.MSLXDFF_DAEMON) {
74
+ const port = effectivePort(args);
75
+ stopDaemonIfOutdated(VERSION);
76
+ const keptPid = readPid();
77
+ if (keptPid && isPidAlive(keptPid)) {
78
+ const rv = readPidVersion();
79
+ if (rv && compareSemver(rv, VERSION) > 0) {
80
+ await printStatus(VERSION);
81
+ console.log(`daemon already running newer v${rv} — not starting v${VERSION}`);
82
+ process.exit(0);
83
+ }
84
+ }
85
+ const spawnedPid = startDaemon(args.filter((a) => a !== "-d" && a !== "--daemon"));
86
+ await waitForHealth(port, 4000);
87
+ console.log(`mslxdff daemon started (pid ${spawnedPid})`);
88
+ console.log(`log: ${logFile()}`);
89
+ console.log(`pid: ${pidFile()}`);
90
+ process.exit(0);
91
+ }
92
+ return false; // we ARE the daemon — fall through to bootstrap
93
+ }
94
+
95
+ export async function handleDebug(args) {
96
+ if (!(args.includes("-debug") || args.includes("--debug"))) return false;
97
+ const { stopped, pid } = stopDaemon();
98
+ if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
99
+ try {
100
+ const dir = logDir();
101
+ const toClear = [eventsFile(), callsFile(), errorsFile(), logFile()];
102
+ let cleared = 0;
103
+ for (const f of toClear) {
104
+ try {
105
+ if (existsSync(f)) {
106
+ writeFileSync(f, "");
107
+ cleared++;
108
+ }
109
+ } catch {}
110
+ }
111
+ console.log(`[debug] 已清理旧日志 ${cleared} 个文件 (${dir}),本次会话干净输出`);
112
+ } catch {}
113
+ console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
114
+ process.env.MSLXDFF_DEBUG = "1";
115
+ process.env.MSLXDFF_DAEMON = "1";
116
+ return false; // fall through to daemon body
117
+ }
118
+
119
+ export async function handleBareRun(args, VERSION) {
120
+ if (process.env.MSLXDFF_DAEMON) return false;
121
+ const pid = readPid();
122
+ if (pid && isPidAlive(pid)) {
123
+ const rv = readPidVersion();
124
+ if (rv && compareSemver(rv, VERSION) >= 0) {
125
+ await printStatus(VERSION);
126
+ const { printHelp } = await import("../help.js");
127
+ printHelp(VERSION);
128
+ process.exit(0);
129
+ }
130
+ }
131
+ const port = effectivePort(args);
132
+ stopDaemonIfOutdated(VERSION);
133
+ const pid2 = readPid();
134
+ if (pid2 && isPidAlive(pid2)) {
135
+ const rv2 = readPidVersion();
136
+ if (rv2 && compareSemver(rv2, VERSION) >= 0) {
137
+ await printStatus(VERSION);
138
+ const { printHelp } = await import("../help.js");
139
+ printHelp(VERSION);
140
+ process.exit(0);
141
+ }
142
+ }
143
+ const spawnedPid = startDaemon([]);
144
+ await waitForHealth(port, 4000);
145
+ console.log(`mslxdff v${VERSION} started as a background daemon (pid ${spawnedPid})`);
146
+ console.log(`endpoint: http://localhost:${port}/v1`);
147
+ console.log(`log: ${logFile()}`);
148
+ console.log(`pid: ${pidFile()}`);
149
+ console.log(`status: run \`mslxdff\` again (or \`mslxdff -status\`)`);
150
+ process.exit(0);
151
+ }