mslxdff 0.1.41 → 0.1.42

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
- import { readFileSync, existsSync, statSync, rmSync } from "node:fs";
3
+ import { readFileSync, existsSync, statSync, rmSync, writeFileSync } 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";
@@ -192,14 +192,24 @@ if (args.includes("-model") || args.includes("-models")) {
192
192
  // event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
193
193
  // the background, then exits. See the daemon body below for the stream wiring.
194
194
  if (args.includes("-debug") || args.includes("--debug")) {
195
- const recent = recentEvents(100);
196
- if (recent.length) {
197
- console.log(`--- last ${recent.length} event(s) ---`);
198
- for (const e of recent) console.log(fmtEvent(e));
199
- }
200
- console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
195
+ // 先停旧 daemon,再清日志,保持 debug 输出干净(仅本次会话)
201
196
  const { stopped, pid } = stopDaemon();
202
197
  if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
198
+ try {
199
+ const dir = logDir();
200
+ const toClear = [eventsFile(), callsFile(), errorsFile(), logFile()];
201
+ let cleared = 0;
202
+ for (const f of toClear) {
203
+ try {
204
+ if (existsSync(f)) {
205
+ writeFileSync(f, "");
206
+ cleared++;
207
+ }
208
+ } catch {}
209
+ }
210
+ console.log(`[debug] 已清理旧日志 ${cleared} 个文件 (${dir}),本次会话干净输出`);
211
+ } catch {}
212
+ console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
203
213
  process.env.MSLXDFF_DEBUG = "1";
204
214
  process.env.MSLXDFF_DAEMON = "1";
205
215
  // fall through to the daemon body below — no process.exit() here
@@ -692,6 +702,19 @@ if (isDebug) {
692
702
  }
693
703
 
694
704
  await srv.ready();
705
+
706
+ // 上游 Keep-Alive 预热:首条 TCP+TLS 暖好,100ms 后异步触发,不阻塞 ready
707
+ setTimeout(() => {
708
+ upstream.preheat().then((r) => {
709
+ const entry = { ts: Date.now(), type: "upstream-preheat", ...r, baseUrl };
710
+ try { bus.emit(entry); } catch {}
711
+ try { logs.appendEvent(entry); } catch {}
712
+ if (r.skipped) console.log(`[preheat] skipped (MSLXDFF_PREHEAT disabled)`);
713
+ else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
714
+ else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
715
+ }).catch(() => {});
716
+ }, 100).unref?.();
717
+
695
718
  models.startAutoRefresh();
696
719
  if (process.env.MSLXDFF_DAEMON) {
697
720
  writePid(process.pid, VERSION);
@@ -1195,6 +1218,9 @@ function fmtEvent(e) {
1195
1218
  return `${head} upstream ok ${m(e.model)} HTTP ${e.status}${e.timing ? ` total=${e.timing.totalMs}ms` : ""}`;
1196
1219
  case "upstream-error":
1197
1220
  return `${head} upstream err ${m(e.model)} ${e.status ? `HTTP ${e.status}` : "network"}: ${m(e.message)}`;
1221
+ case "upstream-preheat":
1222
+ if (e.skipped) return `${head} preheat 跳过 (MSLXDFF_PREHEAT disabled)`;
1223
+ return `${head} preheat 预热 opencode models ${e.ok ? "ok" : "fail"} ${e.status ? `HTTP ${e.status}` : e.error || ""} ${e.ms ? `${e.ms}ms` : ""}`;
1198
1224
  case "peer-race-start":
1199
1225
  return `${head} peer race 开始并发给组员 model=${m(e.model)} peers=${e.peers}`;
1200
1226
  case "peer-health":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,5 +23,8 @@
23
23
  "demo",
24
24
  "placeholder"
25
25
  ],
26
- "license": "MIT"
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "undici": "^8.10.0"
29
+ }
27
30
  }
package/src/upstream.js CHANGED
@@ -1,9 +1,31 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
+ let UndiciAgent = null;
4
+ let UndiciFetch = null;
5
+ try {
6
+ const mod = await import("undici");
7
+ UndiciAgent = mod.Agent;
8
+ UndiciFetch = mod.fetch;
9
+ } catch {
10
+ // undici not installed — fallback to no dispatcher (still works, just no keep-alive tuning)
11
+ }
12
+
3
13
  function genId(prefix) {
4
14
  return `${prefix}${crypto.randomUUID().replace(/-/g, "")}`;
5
15
  }
6
16
 
17
+ function envInt(name, fallback) {
18
+ const v = Number(process.env[name]);
19
+ return Number.isInteger(v) && v > 0 ? v : fallback;
20
+ }
21
+
22
+ function isPreheatDisabled() {
23
+ const raw = process.env.MSLXDFF_PREHEAT;
24
+ if (raw === undefined || raw === null || raw === "") return false;
25
+ const s = String(raw).trim().toLowerCase();
26
+ return s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled";
27
+ }
28
+
7
29
  export function createUpstreamClient({
8
30
  baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
9
31
  authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
@@ -15,8 +37,10 @@ export function createUpstreamClient({
15
37
  503: { attempts: 1, delayMs: 500 },
16
38
  504: { attempts: 1, delayMs: 500 },
17
39
  },
18
- fetchImpl = fetch,
40
+ fetchImpl,
19
41
  } = {}) {
42
+ // 使用 undici 的 fetch 与 Agent 配对,避免 global fetch 与 npm undici Agent 不兼容
43
+ if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
20
44
  const baseHeaders = {
21
45
  "Content-Type": "application/json",
22
46
  "Authorization": `Bearer ${authToken}`,
@@ -35,6 +59,27 @@ export function createUpstreamClient({
35
59
  };
36
60
  }
37
61
 
62
+ // Keep-Alive Agent(显式复用 TCP+TLS)
63
+ const keepAliveTimeout = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_TIMEOUT", 30_000);
64
+ const keepAliveMaxTimeout = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_MAX_TIMEOUT", 60_000);
65
+ const keepAliveConnections = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_CONNECTIONS", 20);
66
+ let dispatcher = null;
67
+ let agent = null;
68
+ if (UndiciAgent) {
69
+ try {
70
+ agent = new UndiciAgent({
71
+ keepAliveTimeout,
72
+ keepAliveMaxTimeout,
73
+ connections: keepAliveConnections,
74
+ pipelining: 1,
75
+ });
76
+ dispatcher = agent;
77
+ } catch {
78
+ agent = null;
79
+ dispatcher = null;
80
+ }
81
+ }
82
+
38
83
  async function chat(body) {
39
84
  const url = `${baseUrl}/zen/v1/chat/completions`;
40
85
  const t0 = performance.now();
@@ -85,12 +130,14 @@ export function createUpstreamClient({
85
130
  );
86
131
  try {
87
132
  const headers = buildHeaders(body);
88
- const res = await fetchImpl(url, {
133
+ const opts = {
89
134
  method: "POST",
90
135
  headers,
91
136
  body: JSON.stringify(body),
92
137
  signal: controller.signal,
93
- });
138
+ };
139
+ if (dispatcher) opts.dispatcher = dispatcher;
140
+ const res = await fetchImpl(url, opts);
94
141
  return res;
95
142
  } catch (err) {
96
143
  return err;
@@ -99,9 +146,46 @@ export function createUpstreamClient({
99
146
  }
100
147
  }
101
148
 
149
+ async function preheat() {
150
+ if (isPreheatDisabled()) return { ok: false, skipped: true, reason: "disabled" };
151
+ const url = `${baseUrl}/zen/v1/models`;
152
+ const controller = new AbortController();
153
+ const timer = setTimeout(() => controller.abort(new Error("preheat timed out after 3000ms")), 3000);
154
+ const t0 = performance.now();
155
+ try {
156
+ const headers = buildHeaders({ stream: false });
157
+ const opts = {
158
+ method: "GET",
159
+ headers,
160
+ signal: controller.signal,
161
+ };
162
+ if (dispatcher) opts.dispatcher = dispatcher;
163
+ const res = await fetchImpl(url, opts);
164
+ const ms = Math.round(performance.now() - t0);
165
+ // 消耗 body 以释放连接回池(即使不需要内容)
166
+ try { if (res.body) await res.text().catch(() => {}); } catch {}
167
+ return { ok: res.ok, status: res.status, ms };
168
+ } catch (err) {
169
+ return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
170
+ } finally {
171
+ clearTimeout(timer);
172
+ }
173
+ }
174
+
175
+ let closed = false;
176
+ async function close() {
177
+ if (closed) return;
178
+ closed = true;
179
+ if (agent && typeof agent.close === "function") {
180
+ try { await agent.close(); } catch {}
181
+ } else if (dispatcher && typeof dispatcher.close === "function" && dispatcher !== agent) {
182
+ try { await dispatcher.close(); } catch {}
183
+ }
184
+ }
185
+
102
186
  // 兼容旧调用:headers 为动态生成,暴露 getter 快照(用于测试/展示)
103
187
  const headers = buildHeaders({});
104
- return { chat, headers, buildHeaders };
188
+ return { chat, preheat, close, headers, buildHeaders, dispatcher, agent, [Symbol.asyncDispose]: close };
105
189
  }
106
190
 
107
191
  function sleep(ms) {