mslxdff 0.1.10 → 0.1.12

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
@@ -4,6 +4,7 @@ import { readFileSync, existsSync, statSync, watch, openSync, closeSync, mkdirSy
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,6 +12,7 @@ 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
17
  import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, eventsFile, recentEvents } from "../src/logs.js";
16
18
 
@@ -405,7 +407,8 @@ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHea
405
407
  const groups = createGroupsService({});
406
408
  const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
407
409
 
408
- const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans });
410
+ const bus = createEventBus();
411
+ const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus });
409
412
  const srv = startServer({ router });
410
413
 
411
414
  await srv.ready();
@@ -706,70 +709,132 @@ function fmtEvent(e) {
706
709
  }
707
710
  }
708
711
 
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).
712
+ // Live-follow the daemon: replay the file backlog, then stream events over
713
+ // HTTP (SSE) which the daemon pushes from memory — no filesystem watch/poll
714
+ // involved on the live path. Falls back to file polling if the stream fails.
711
715
  async function liveDebug() {
712
716
  const file = eventsFile();
713
- // ensure the event file exists so watch() doesn't fail on a fresh daemon dir
717
+ const dir = dirname(file);
718
+ const recent = recentEvents(100);
719
+ if (recent.length) {
720
+ console.log(`--- last ${recent.length} event(s) ---`);
721
+ for (const e of recent) console.log(fmtEvent(e));
722
+ }
723
+ console.log("--- live (Ctrl+C to exit) ---");
724
+
725
+ try {
726
+ await streamEventsHttp();
727
+ return; // stream kept running until Ctrl+C
728
+ } catch (err) {
729
+ console.error(`[debug] streaming failed (${err.message}) — falling back to file polling`);
730
+ }
731
+
732
+ // File-poll fallback (daemon too old to expose the SSE endpoint).
714
733
  if (!existsSync(file)) {
715
734
  try {
716
- mkdirSync(dirname(file), { recursive: true });
735
+ mkdirSync(dir, { recursive: true });
717
736
  closeSync(openSync(file, "a"));
718
737
  } catch {
719
738
  console.error(`cannot create event file: ${file}`);
720
739
  process.exit(1);
721
740
  }
722
741
  }
723
- const recent = recentEvents(100);
724
- if (recent.length) {
725
- console.log(`--- last ${recent.length} event(s) ---`);
726
- for (const e of recent) console.log(fmtEvent(e));
727
- }
728
- console.log("--- live (Ctrl+C to exit) ---");
729
- if (!existsSync(file)) console.log("(no events yet — trigger a chat request to see the flow)");
730
-
731
742
  let pos = existsSync(file) ? statSync(file).size : 0;
732
743
  let buf = "";
733
744
  const pump = () => {
734
- if (!existsSync(file)) {
735
- pos = 0;
736
- buf = "";
737
- return;
745
+ try {
746
+ if (!existsSync(file)) {
747
+ pos = 0;
748
+ buf = "";
749
+ return;
750
+ }
751
+ const size = statSync(file).size;
752
+ if (size < pos) {
753
+ pos = 0;
754
+ buf = "";
755
+ }
756
+ if (size === pos) return;
757
+ buf += readFileSync(file, "utf8").slice(pos);
758
+ pos = size;
759
+ const lines = buf.split("\n");
760
+ buf = lines.pop() || "";
761
+ for (const l of lines) {
762
+ if (!l.trim()) continue;
763
+ try {
764
+ console.log(fmtEvent(JSON.parse(l)));
765
+ } catch {
766
+ // partial/corrupt line while trimming — skip
767
+ }
768
+ }
769
+ } catch (err) {
770
+ console.error(`[debug] poll error: ${err.message}`);
738
771
  }
739
- const size = statSync(file).size;
740
- if (size < pos) {
741
- pos = 0; // file trimmed/rewritten: re-follow from the start of what remains
742
- buf = "";
772
+ };
773
+ setInterval(pump, 500);
774
+ pump();
775
+ await new Promise(() => {}); // run until Ctrl+C
776
+ }
777
+
778
+ // Connect to the daemon's SSE stream (authenticated), print each event.
779
+ // Resolves when the connection ends; throws on connect failure.
780
+ async function streamEventsHttp() {
781
+ const { token } = await loadToken();
782
+ const port = getPort() || DEFAULT_PORT;
783
+ const url = `http://127.0.0.1:${port}/v1/_debug/stream`;
784
+ // daemon may still be starting when -debug is launched right after it —
785
+ // retry a few times before giving up on the stream.
786
+ let res = null;
787
+ let lastErr = null;
788
+ for (let attempt = 1; attempt <= 10 && !res; attempt++) {
789
+ try {
790
+ res = await fetch(url, {
791
+ headers: { Authorization: `Bearer ${token}` },
792
+ signal: AbortSignal.timeout(10_000),
793
+ });
794
+ } catch (err) {
795
+ lastErr = err;
796
+ await new Promise((r) => setTimeout(r, 500));
743
797
  }
744
- if (size === pos) return;
745
- buf += readFileSync(file, "utf8").slice(pos);
746
- pos = size;
747
- const lines = buf.split("\n");
748
- buf = lines.pop() || "";
749
- for (const l of lines) {
750
- if (!l.trim()) continue;
798
+ }
799
+ if (!res) throw lastErr || new Error("cannot reach daemon");
800
+ if (res.status === 401) throw new Error("unauthorized (token mismatch?)");
801
+ if (res.status === 404) throw new Error("daemon too old (no stream endpoint)");
802
+ if (!res.ok) throw new Error(`daemon responded ${res.status}`);
803
+ const reader = res.body.getReader();
804
+ const decoder = new TextDecoder();
805
+ let chunk = "";
806
+ const first = await Promise.race([
807
+ reader.read(),
808
+ new Promise((_, reject) => setTimeout(() => reject(new Error("no data from stream")), 15_000)),
809
+ ]);
810
+ if (first.done) return;
811
+ console.log(`streaming from http://127.0.0.1:${port} (Ctrl+C to exit)`);
812
+ chunk += decoder.decode(first.value, { stream: true });
813
+ const lines = chunk.split("\n");
814
+ chunk = lines.pop() || "";
815
+ for (const l of lines) {
816
+ if (!l.startsWith("data: ")) continue;
817
+ try {
818
+ console.log(fmtEvent(JSON.parse(l.slice(6))));
819
+ } catch {
820
+ // skip malformed frames
821
+ }
822
+ }
823
+ while (true) {
824
+ const { done, value } = await reader.read();
825
+ if (done) break;
826
+ chunk += decoder.decode(value, { stream: true });
827
+ const frameLines = chunk.split("\n");
828
+ chunk = frameLines.pop() || "";
829
+ for (const l of frameLines) {
830
+ if (!l.startsWith("data: ")) continue;
751
831
  try {
752
- console.log(fmtEvent(JSON.parse(l)));
832
+ console.log(fmtEvent(JSON.parse(l.slice(6))));
753
833
  } catch {
754
- // partial/corrupt line while trimming — skip
834
+ // skip malformed frames
755
835
  }
756
836
  }
757
- };
758
- try {
759
- mkdirSync(dirname(file), { recursive: true });
760
- watch(dirname(file), (_evt, filename) => {
761
- if (!filename || basename(String(filename)) !== basename(file)) return;
762
- pump();
763
- });
764
- } catch {
765
- console.error(`cannot watch event dir: ${dirname(file)}`);
766
- process.exit(1);
767
837
  }
768
- // fs.watch is unreliable for freshly-created files on Windows — poll as a
769
- // safety net so events are never missed.
770
- setInterval(pump, 250);
771
- pump();
772
- await new Promise(() => {}); // run until Ctrl+C
773
838
  }
774
839
 
775
840
  function fmtTs(iso) { if (!iso) return "-";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
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,23 @@ 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
+
16
+ if (bus && path === "/v1/_debug/stream") {
17
+ // live debug stream (SSE): auth REQUIRED, then replay backlog + push
18
+ if (!authorized(req, token)) {
19
+ res.statusCode = 401;
20
+ res.setHeader("WWW-Authenticate", "Bearer");
21
+ return json(res, 401, { error: "Unauthorized" });
22
+ }
23
+ return streamEvents(req, res, bus);
24
+ }
25
+
15
26
  if (!route) return notFound(res);
16
27
 
17
28
  if (route.requiresAuth && !authorized(req, token)) {
@@ -20,10 +31,39 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
20
31
  return json(res, 401, { error: "Unauthorized" });
21
32
  }
22
33
 
23
- await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token });
34
+ await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token, bus });
24
35
  };
25
36
  }
26
37
 
38
+ // SSE endpoint consumed by `mslxdff -debug`: replays the buffered backlog,
39
+ // then pushes each new event as it happens. One second heartbeat keeps
40
+ // proxies/NAT from closing the connection.
41
+ function streamEvents(req, res, bus) {
42
+ res.statusCode = 200;
43
+ res.setHeader("Content-Type", "text/event-stream");
44
+ res.setHeader("Cache-Control", "no-cache");
45
+ res.setHeader("Connection", "keep-alive");
46
+ res.flushHeaders?.();
47
+
48
+ for (const e of bus.replayAll()) {
49
+ res.write(`data: ${JSON.stringify(e)}\n\n`);
50
+ }
51
+ const unsubscribe = bus.subscribe((e) => {
52
+ res.write(`data: ${JSON.stringify(e)}\n\n`);
53
+ });
54
+ const heartbeat = setInterval(() => {
55
+ res.write(`: ping\n\n`);
56
+ }, 15_000);
57
+ res.on("close", () => {
58
+ clearInterval(heartbeat);
59
+ unsubscribe();
60
+ });
61
+ req.on("close", () => {
62
+ clearInterval(heartbeat);
63
+ unsubscribe();
64
+ });
65
+ }
66
+
27
67
  function clientIp(req) {
28
68
  const fwd = req.headers["x-forwarded-for"];
29
69
  const head = typeof fwd === "string" ? fwd.split(",")[0].trim() : "";
@@ -175,7 +215,7 @@ const ROUTES = [
175
215
  method: "POST",
176
216
  path: "/v1/chat/completions",
177
217
  requiresAuth: true,
178
- handler: async ({ req, res, upstream, auto, logs, peers, maxHops }) => {
218
+ handler: async ({ req, res, upstream, auto, logs, peers, maxHops, bus }) => {
179
219
  let body;
180
220
  try {
181
221
  body = await readBody(req);
@@ -205,7 +245,11 @@ const ROUTES = [
205
245
  logs?.appendCall({ model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream) });
206
246
  const logError = (model, status, message) =>
207
247
  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 });
248
+ const evt = (type, data) => {
249
+ const entry = { ts: Date.now(), type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt };
250
+ if (bus) bus.emit(entry);
251
+ logs?.appendEvent?.(entry);
252
+ };
209
253
  evt("request", { hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body) });
210
254
 
211
255
  let lastErr = null;
package/src/server.js CHANGED
@@ -19,6 +19,9 @@ 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
27
  process.on("SIGINT", close);