mslxdff 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/mslxdff.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
- import { readFileSync, existsSync, statSync, watch, openSync, closeSync, mkdirSync } from "node:fs";
3
+ import { readFileSync, existsSync, statSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join, basename } from "node:path";
6
6
  import { startServer, resolvePort } from "../src/server.js";
7
+ import { DEFAULT_PORT } from "../src/state.js";
7
8
  import { createRouter } from "../src/routes.js";
8
9
  import { createUpstreamClient } from "../src/upstream.js";
9
10
  import { createModelsService } from "../src/models.js";
@@ -11,8 +12,9 @@ import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroups
11
12
  import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
12
13
  import { createAutoSelector } from "../src/auto.js";
13
14
  import { createPeersService } from "../src/peers.js";
15
+ import { createEventBus } from "../src/events.js";
14
16
  import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
15
- import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, eventsFile, recentEvents } from "../src/logs.js";
17
+ import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, recentEvents } from "../src/logs.js";
16
18
 
17
19
  const logs = { appendCall, appendError, appendEvent };
18
20
 
@@ -130,11 +132,22 @@ if (args.includes("-model") || args.includes("-models")) {
130
132
  process.exit(0);
131
133
  }
132
134
 
133
- // -debug: live-follow the daemon's event stream (requests, upstream errors,
134
- // peer forwards, peer errors, results) for investigation
135
+ // -debug: stop the background daemon and run the server in THIS terminal
136
+ // (foreground), printing every event to stdout in real time via the in-memory
137
+ // event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
138
+ // the background, then exits. See the daemon body below for the stream wiring.
135
139
  if (args.includes("-debug") || args.includes("--debug")) {
136
- await liveDebug();
137
- process.exit(0);
140
+ const recent = recentEvents(100);
141
+ if (recent.length) {
142
+ console.log(`--- last ${recent.length} event(s) ---`);
143
+ for (const e of recent) console.log(fmtEvent(e));
144
+ }
145
+ console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
146
+ const { stopped, pid } = stopDaemon();
147
+ if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
148
+ process.env.MSLXDFF_DEBUG = "1";
149
+ process.env.MSLXDFF_DAEMON = "1";
150
+ // fall through to the daemon body below — no process.exit() here
138
151
  }
139
152
 
140
153
  // -creategroup <name> | -group create <name> | -group sync | -group leave <name> | -group list |
@@ -405,8 +418,34 @@ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHea
405
418
  const groups = createGroupsService({});
406
419
  const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
407
420
 
408
- const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans });
409
- const srv = startServer({ router });
421
+ const isDebug = process.env.MSLXDFF_DEBUG === "1";
422
+ const bus = createEventBus();
423
+ const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus });
424
+ const srv = startServer({ router, signals: !isDebug });
425
+
426
+ // -debug: push every event straight to this terminal.
427
+ if (isDebug) {
428
+ bus.subscribe((e) => {
429
+ try {
430
+ console.log(fmtEvent(e));
431
+ } catch {
432
+ // malformed event — skip
433
+ }
434
+ });
435
+ // Ctrl+C / SIGTERM: restore the background daemon, then exit.
436
+ const restore = () => {
437
+ console.log("\n[debug] restoring background daemon...");
438
+ try {
439
+ const restoredPid = startDaemon([]);
440
+ console.log(`[debug] daemon restored (pid ${restoredPid})`);
441
+ } catch (err) {
442
+ console.error(`[debug] could not restore daemon: ${err.message}`);
443
+ }
444
+ setTimeout(() => process.exit(0), 300);
445
+ };
446
+ process.on("SIGINT", restore);
447
+ process.on("SIGTERM", restore);
448
+ }
410
449
 
411
450
  await srv.ready();
412
451
  models.startAutoRefresh();
@@ -706,80 +745,8 @@ function fmtEvent(e) {
706
745
  }
707
746
  }
708
747
 
709
- // Live-follow the daemon's event stream: print the recent tail, then stream
710
- // new lines as they are appended (Ctrl+C to exit).
711
- async function liveDebug() {
712
- const file = eventsFile();
713
- // ensure the event file exists so watch() doesn't fail on a fresh daemon dir
714
- const dir = dirname(file);
715
- if (!existsSync(file)) {
716
- try {
717
- mkdirSync(dir, { recursive: true });
718
- closeSync(openSync(file, "a"));
719
- } catch {
720
- console.error(`cannot create event file: ${file}`);
721
- process.exit(1);
722
- }
723
- }
724
- const recent = recentEvents(100);
725
- if (recent.length) {
726
- console.log(`--- last ${recent.length} event(s) ---`);
727
- for (const e of recent) console.log(fmtEvent(e));
728
- }
729
- console.log("--- live (Ctrl+C to exit) ---");
730
- console.log(`watching: ${file}`);
731
- if (!existsSync(file)) console.log("(no events yet — trigger a chat request to see the flow)");
732
-
733
- let pos = existsSync(file) ? statSync(file).size : 0;
734
- let buf = "";
735
- const pump = () => {
736
- try {
737
- if (!existsSync(file)) {
738
- pos = 0;
739
- buf = "";
740
- return;
741
- }
742
- const size = statSync(file).size;
743
- if (size < pos) {
744
- pos = 0; // file trimmed/rewritten: re-follow from the start of what remains
745
- buf = "";
746
- }
747
- if (size === pos) return;
748
- buf += readFileSync(file, "utf8").slice(pos);
749
- pos = size;
750
- const lines = buf.split("\n");
751
- buf = lines.pop() || "";
752
- for (const l of lines) {
753
- if (!l.trim()) continue;
754
- try {
755
- console.log(fmtEvent(JSON.parse(l)));
756
- } catch {
757
- // partial/corrupt line while trimming — skip
758
- }
759
- }
760
- } catch (err) {
761
- console.error(`[debug] poll error: ${err.message}`);
762
- }
763
- };
764
- // fs.watch is unreliable across platforms for freshly-created/rotated files,
765
- // so polling is the source of truth (200ms); the directory watch just adds
766
- // low-latency wake-ups and silently degrades when it misbehaves.
767
- try {
768
- mkdirSync(dir, { recursive: true });
769
- watch(dir, (_evt, filename) => {
770
- if (!filename || basename(String(filename)) !== basename(file)) return;
771
- pump();
772
- });
773
- } catch {
774
- console.error(`cannot watch event dir: ${dir} (falling back to polling only)`);
775
- }
776
- // Poll is the source of truth; 200ms keeps human-perceived latency negligible.
777
- setInterval(pump, 200);
778
- pump();
779
- await new Promise(() => {}); // run until Ctrl+C
780
- }
781
-
782
- function fmtTs(iso) { if (!iso) return "-";
748
+ function fmtTs(iso) {
749
+ if (!iso) return "-";
783
750
  try {
784
751
  return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
785
752
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/daemon.js CHANGED
@@ -24,10 +24,14 @@ export function startDaemon(args = []) {
24
24
  const dir = daemonDir();
25
25
  mkdirSync(dir, { recursive: true });
26
26
  const logFd = openSync(logFile(), "a", 0o600);
27
+ const env = { ...process.env, MSLXDFF_DAEMON: "1" };
28
+ // a -debug foreground session wouldn't pass MSLXDFF_DEBUG to the
29
+ // background daemon it restores (that flag means "print events to stdout")
30
+ delete env.MSLXDFF_DEBUG;
27
31
  const child = spawn(process.execPath, [entry, ...args, "--daemon"], {
28
32
  detached: true,
29
33
  stdio: ["ignore", logFd, logFd],
30
- env: { ...process.env, MSLXDFF_DAEMON: "1" },
34
+ env,
31
35
  });
32
36
  child.unref();
33
37
  return child.pid;
package/src/events.js ADDED
@@ -0,0 +1,39 @@
1
+ // In-memory event bus for live debugging: routes.js emits, -debug streams
2
+ // over HTTP. Keeps a small ring buffer so late subscribers can catch up.
3
+
4
+ const MAX_BUFFERED = 500;
5
+
6
+ export function createEventBus() {
7
+ let buffer = [];
8
+ const listeners = new Set();
9
+
10
+ return {
11
+ // emit an event to every current subscriber (sync, never throws)
12
+ emit(event) {
13
+ buffer.push(event);
14
+ if (buffer.length > MAX_BUFFERED) buffer.splice(0, buffer.length - MAX_BUFFERED);
15
+ for (const fn of listeners) {
16
+ try {
17
+ fn(event);
18
+ } catch {
19
+ // subscriber errors must never break the request path
20
+ }
21
+ }
22
+ },
23
+ // subscribe; returns an unsubscribe fn
24
+ subscribe(fn) {
25
+ listeners.add(fn);
26
+ return () => listeners.delete(fn);
27
+ },
28
+ // buffered events since start (or from offset), consumed left-to-right
29
+ replay({ since = 0 } = {}) {
30
+ return buffer.slice(since);
31
+ },
32
+ replayAll() {
33
+ return buffer.slice();
34
+ },
35
+ size() {
36
+ return buffer.length;
37
+ },
38
+ };
39
+ }
package/src/routes.js CHANGED
@@ -6,12 +6,13 @@ import { DEFAULT_MAX_HOPS } from "./peers.js";
6
6
 
7
7
  export const errMsg = (err) => String(err?.message || err);
8
8
 
9
- export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans }) {
9
+ export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans, bus }) {
10
10
  return async function router(req, res) {
11
11
  const method = req.method || "GET";
12
12
  const path = (req.url || "").split("?")[0];
13
13
 
14
14
  const route = ROUTES.find((r) => r.method === method && r.path === path);
15
+
15
16
  if (!route) return notFound(res);
16
17
 
17
18
  if (route.requiresAuth && !authorized(req, token)) {
@@ -20,7 +21,7 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
20
21
  return json(res, 401, { error: "Unauthorized" });
21
22
  }
22
23
 
23
- await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token });
24
+ await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token, bus });
24
25
  };
25
26
  }
26
27
 
@@ -175,7 +176,7 @@ const ROUTES = [
175
176
  method: "POST",
176
177
  path: "/v1/chat/completions",
177
178
  requiresAuth: true,
178
- handler: async ({ req, res, upstream, auto, logs, peers, maxHops }) => {
179
+ handler: async ({ req, res, upstream, auto, logs, peers, maxHops, bus }) => {
179
180
  let body;
180
181
  try {
181
182
  body = await readBody(req);
@@ -205,7 +206,11 @@ const ROUTES = [
205
206
  logs?.appendCall({ model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream) });
206
207
  const logError = (model, status, message) =>
207
208
  logs?.appendError({ model, auto: useAuto, status, message });
208
- const evt = (type, data) => logs?.appendEvent?.({ type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt });
209
+ const evt = (type, data) => {
210
+ const entry = { ts: Date.now(), type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt };
211
+ if (bus) bus.emit(entry);
212
+ logs?.appendEvent?.(entry);
213
+ };
209
214
  evt("request", { hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body) });
210
215
 
211
216
  let lastErr = null;
package/src/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer as httpCreateServer } from "node:http";
2
2
  import { DEFAULT_PORT, getPort } from "./state.js";
3
3
 
4
- export function startServer({ router }, port = resolvePort()) {
4
+ export function startServer({ router, signals = true }, port = resolvePort()) {
5
5
  const server = httpCreateServer((req, res) => {
6
6
  router(req, res).catch((err) => {
7
7
  res.statusCode = 500;
@@ -19,10 +19,15 @@ export function startServer({ router }, port = resolvePort()) {
19
19
  const close = () =>
20
20
  new Promise((resolve) => {
21
21
  server.close(resolve);
22
+ // SSE debug streams keep connections open — force-close so shutdown
23
+ // never waits on them.
24
+ server.closeAllConnections?.();
22
25
  });
23
26
 
24
- process.on("SIGINT", close);
25
- process.on("SIGTERM", close);
27
+ if (signals) {
28
+ process.on("SIGINT", close);
29
+ process.on("SIGTERM", close);
30
+ }
26
31
 
27
32
  return { server, ready, close };
28
33
  }