mslxdff 0.1.11 → 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,12 +709,27 @@ 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
714
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).
715
733
  if (!existsSync(file)) {
716
734
  try {
717
735
  mkdirSync(dir, { recursive: true });
@@ -721,15 +739,6 @@ async function liveDebug() {
721
739
  process.exit(1);
722
740
  }
723
741
  }
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
742
  let pos = existsSync(file) ? statSync(file).size : 0;
734
743
  let buf = "";
735
744
  const pump = () => {
@@ -741,7 +750,7 @@ async function liveDebug() {
741
750
  }
742
751
  const size = statSync(file).size;
743
752
  if (size < pos) {
744
- pos = 0; // file trimmed/rewritten: re-follow from the start of what remains
753
+ pos = 0;
745
754
  buf = "";
746
755
  }
747
756
  if (size === pos) return;
@@ -761,24 +770,73 @@ async function liveDebug() {
761
770
  console.error(`[debug] poll error: ${err.message}`);
762
771
  }
763
772
  };
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);
773
+ setInterval(pump, 500);
778
774
  pump();
779
775
  await new Promise(() => {}); // run until Ctrl+C
780
776
  }
781
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));
797
+ }
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;
831
+ try {
832
+ console.log(fmtEvent(JSON.parse(l.slice(6))));
833
+ } catch {
834
+ // skip malformed frames
835
+ }
836
+ }
837
+ }
838
+ }
839
+
782
840
  function fmtTs(iso) { if (!iso) return "-";
783
841
  try {
784
842
  return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.11",
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);