mslxdff 0.1.87 → 0.1.89

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.
@@ -316,6 +316,44 @@ export async function startDaemonMain(VERSION) {
316
316
 
317
317
  const broadbandGroups = () => loadGroupsJoined().filter((g) => g.kind === "broadband" && g.leaderUrl);
318
318
  if (broadbandGroups().length) {
319
+ const streamEnabled = (() => {
320
+ const v = process.env.MSLXDFF_BROADBAND_STREAM;
321
+ if (v === "0" || v === "false" || v === "off") return false;
322
+ return true;
323
+ })();
324
+ const execAndPost = async (g, reqId, body) => {
325
+ let result;
326
+ try {
327
+ const upRes = await upstream.chat(body);
328
+ const ct = upRes.headers.get("content-type") || "";
329
+ const isStream = Boolean(body?.stream) || ct.includes("text/event-stream");
330
+ if (isStream && upRes.body) {
331
+ let collected = "";
332
+ for await (const chunk of upRes.body) {
333
+ if (typeof chunk === "string") collected += chunk;
334
+ else if (Buffer.isBuffer(chunk)) collected += chunk.toString("utf8");
335
+ else if (chunk instanceof Uint8Array) collected += Buffer.from(chunk).toString("utf8");
336
+ else collected += String(chunk);
337
+ }
338
+ result = { status: upRes.status, headers: { "Content-Type": "text/event-stream" }, body: collected };
339
+ } else {
340
+ const txt = await upRes.text();
341
+ let parsed;
342
+ try { parsed = JSON.parse(txt); } catch { parsed = txt; }
343
+ result = { status: upRes.status, headers: { "Content-Type": upRes.headers.get("content-type") || "application/json" }, body: typeof parsed === "string" ? parsed : JSON.stringify(parsed) };
344
+ }
345
+ } catch (err) {
346
+ result = { status: 502, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: errMsg(err) }) };
347
+ }
348
+ try {
349
+ await fetch(`${g.leaderUrl}/v1/groups/relay/result`, {
350
+ method: "POST",
351
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
352
+ body: JSON.stringify({ name: g.name, group: g.name, reqId, result }),
353
+ signal: AbortSignal.timeout(5000),
354
+ });
355
+ } catch {}
356
+ };
319
357
  const doHeartbeat = async () => {
320
358
  for (const g of broadbandGroups()) {
321
359
  try {
@@ -348,44 +386,88 @@ export async function startDaemonMain(VERSION) {
348
386
  const items = data.data || [];
349
387
  for (const item of items) {
350
388
  const { reqId, body } = item;
351
- let result;
352
- try {
353
- const upRes = await upstream.chat(body);
354
- const ct = upRes.headers.get("content-type") || "";
355
- const isStream = Boolean(body?.stream) || ct.includes("text/event-stream");
356
- if (isStream && upRes.body) {
357
- let collected = "";
358
- for await (const chunk of upRes.body) {
359
- collected += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
360
- }
361
- result = { status: upRes.status, headers: { "Content-Type": "text/event-stream" }, body: collected };
362
- } else {
363
- const txt = await upRes.text();
364
- let parsed;
365
- try { parsed = JSON.parse(txt); } catch { parsed = txt; }
366
- result = { status: upRes.status, headers: { "Content-Type": upRes.headers.get("content-type") || "application/json" }, body: typeof parsed === "string" ? parsed : JSON.stringify(parsed) };
367
- }
368
- } catch (err) {
369
- result = { status: 502, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: errMsg(err) }) };
370
- }
371
- try {
372
- await fetch(`${g.leaderUrl}/v1/groups/relay/result`, {
373
- method: "POST",
374
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
375
- body: JSON.stringify({ name: g.name, group: g.name, reqId, result }),
376
- signal: AbortSignal.timeout(5000),
377
- });
378
- } catch {}
389
+ await execAndPost(g, reqId, body);
379
390
  }
380
391
  } catch {}
381
392
  }
382
393
  };
383
- doHeartbeat().catch(() => {});
384
- const hbTimer = setInterval(doHeartbeat, 30_000);
385
- hbTimer.unref();
386
- const pollTimer = setInterval(doPoll, 1000);
387
- pollTimer.unref();
388
- console.log(`broadband relay: heartbeat 30s + poll 1s for ${broadbandGroups().length} group(s)`);
394
+ if (!streamEnabled) {
395
+ doHeartbeat().catch(() => {});
396
+ const hbTimer = setInterval(doHeartbeat, 30_000);
397
+ hbTimer.unref();
398
+ const pollTimer = setInterval(doPoll, 1000);
399
+ pollTimer.unref();
400
+ console.log(`broadband relay: heartbeat 30s + poll 1s for ${broadbandGroups().length} group(s) [poll mode]`);
401
+ } else {
402
+ const streamManagers = new Map();
403
+ const startStreamForGroup = (g) => {
404
+ if (streamManagers.has(g.name)) return;
405
+ let attempts = 0;
406
+ let abort = null;
407
+ let stopped = false;
408
+ const connect = async () => {
409
+ if (stopped) return;
410
+ const url = `${g.leaderUrl}/v1/groups/relay/stream?name=${encodeURIComponent(g.name)}`;
411
+ const controller = new AbortController();
412
+ abort = controller;
413
+ try {
414
+ const res = await fetch(url, {
415
+ headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
416
+ signal: controller.signal,
417
+ });
418
+ if (!res.ok) throw new Error(`stream ${res.status}`);
419
+ if (!res.body) throw new Error("no body");
420
+ attempts = 0;
421
+ let buf = "";
422
+ const decodeChunk = (c) => {
423
+ if (typeof c === "string") return c;
424
+ if (Buffer.isBuffer(c)) return c.toString("utf8");
425
+ if (c instanceof Uint8Array) return Buffer.from(c).toString("utf8");
426
+ return String(c);
427
+ };
428
+ for await (const chunk of res.body) {
429
+ buf += decodeChunk(chunk);
430
+ let idx;
431
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
432
+ const raw = buf.slice(0, idx);
433
+ buf = buf.slice(idx + 2);
434
+ if (!raw || raw.startsWith(":")) continue;
435
+ let event = "message";
436
+ let data = "";
437
+ for (const line of raw.split("\n")) {
438
+ if (line.startsWith("event:")) event = line.slice(6).trim();
439
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
440
+ }
441
+ if (event === "relay" && data) {
442
+ try {
443
+ const parsed = JSON.parse(data);
444
+ const { reqId, body } = parsed;
445
+ if (reqId && body) execAndPost(g, reqId, body).catch(() => {});
446
+ } catch {}
447
+ }
448
+ }
449
+ }
450
+ throw new Error("stream ended");
451
+ } catch (err) {
452
+ if (stopped) return;
453
+ const msg = errMsg(err);
454
+ if (!String(msg).includes("abort") && !String(msg).includes("Abort")) console.log(`broadband stream ${g.name}: ${msg} — reconnecting`);
455
+ attempts++;
456
+ const delay = Math.min(30_000, 1000 * Math.pow(2, attempts - 1) + Math.random() * 500);
457
+ await new Promise((r) => setTimeout(r, delay));
458
+ connect();
459
+ }
460
+ };
461
+ streamManagers.set(g.name, { stop: () => { stopped = true; abort?.abort(); } });
462
+ connect();
463
+ };
464
+ for (const g of broadbandGroups()) startStreamForGroup(g);
465
+ const ensureTimer = setInterval(() => {
466
+ for (const g of broadbandGroups()) if (!streamManagers.has(g.name)) startStreamForGroup(g);
467
+ }, 10_000);
468
+ ensureTimer.unref();
469
+ console.log(`broadband relay: stream (SSE) + ping 25s for ${broadbandGroups().length} group(s)`);
470
+ }
389
471
  }
390
472
 
391
473
  const { setupAutoUpdate } = await import("./auto-update.js");
@@ -0,0 +1,248 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { resolveRetry, sleep, backoffDelay } from "./retry.js";
3
+ import { createSseParser } from "./sse.js";
4
+ import { createPool } from "./pool.js";
5
+
6
+ let UndiciFetch = null;
7
+ try {
8
+ const mod = await import("undici");
9
+ UndiciFetch = mod.fetch;
10
+ } catch {}
11
+
12
+ const DEFAULT_RETRY = {
13
+ network: { attempts: 2, delayMs: 300 },
14
+ 429: { attempts: 1, delayMs: 100 },
15
+ 502: { attempts: 1, delayMs: 100 },
16
+ 503: { attempts: 1, delayMs: 100 },
17
+ 504: { attempts: 1, delayMs: 100 },
18
+ };
19
+
20
+ export function createTransport({
21
+ baseUrl,
22
+ headers: baseHeaders = {},
23
+ keepAlive = true,
24
+ fetchImpl,
25
+ dispatcher: extDispatcher,
26
+ timeoutMs: defaultTimeoutMs = 30_000,
27
+ retry: defaultRetry = DEFAULT_RETRY,
28
+ hooks,
29
+ } = {}) {
30
+ if (!fetchImpl) fetchImpl = UndiciFetch || globalThis.fetch;
31
+ const pool = keepAlive && !extDispatcher ? createPool({ keepAlive }) : null;
32
+ const getDispatcher = () => extDispatcher || pool?.dispatcher || null;
33
+
34
+ let closed = false;
35
+
36
+ function resolveUrl(url) {
37
+ if (!url) return baseUrl || "";
38
+ if (/^https?:\/\//i.test(url)) return url;
39
+ if (!baseUrl) return url;
40
+ return `${String(baseUrl).replace(/\/+$/, "")}/${String(url).replace(/^\/+/, "")}`;
41
+ }
42
+
43
+ async function applyHooks(name, ctx) {
44
+ if (!hooks) return null;
45
+ try { return await hooks(name, ctx); } catch { return null; }
46
+ }
47
+
48
+ async function request({
49
+ url,
50
+ method = "POST",
51
+ headers = {},
52
+ body,
53
+ stream,
54
+ timeoutMs,
55
+ retry,
56
+ dispatcher,
57
+ } = {}) {
58
+ const finalUrl = resolveUrl(url);
59
+ const retryCfg = retry ?? defaultRetry;
60
+ const timeout = Number(timeoutMs ?? defaultTimeoutMs) || 30_000;
61
+ const disp = dispatcher ?? getDispatcher();
62
+ const t0 = performance.now();
63
+ const attempts = [];
64
+ let waitMs = 0;
65
+
66
+ // 合并 headers
67
+ const finalHeaders = { ...baseHeaders, ...headers };
68
+ if (body != null && !finalHeaders["Content-Type"] && !finalHeaders["content-type"]) {
69
+ finalHeaders["Content-Type"] = "application/json";
70
+ }
71
+ if (stream && !finalHeaders["Accept"] && !finalHeaders["accept"]) {
72
+ finalHeaders["Accept"] = "text/event-stream";
73
+ }
74
+
75
+ const bodyStr = body != null && typeof body !== "string" ? JSON.stringify(body) : body;
76
+
77
+ for (let attempt = 0; ; attempt++) {
78
+ const tAttempt = performance.now();
79
+ let res;
80
+ let err = null;
81
+ const controller = new AbortController();
82
+ const timer = setTimeout(() => controller.abort(new Error(`upstream timed out after ${timeout}ms`)), timeout);
83
+ try {
84
+ let reqUrl = finalUrl;
85
+ let reqHeaders = { ...finalHeaders };
86
+ // hooks
87
+ const hh = await applyHooks("upstream:headers", { url: reqUrl, body, headers: reqHeaders });
88
+ if (hh?.changed && hh.value?.headers) reqHeaders = hh.value.headers;
89
+ const br = await applyHooks("upstream:before-request", { url: reqUrl, method, body, headers: reqHeaders });
90
+ if (br?.changed && br.value) {
91
+ if (typeof br.value.url === "string" && br.value.url) reqUrl = br.value.url;
92
+ if (br.value.headers && typeof br.value.headers === "object") reqHeaders = br.value.headers;
93
+ }
94
+ const opts = { method, headers: reqHeaders, body: bodyStr, signal: controller.signal };
95
+ if (disp) opts.dispatcher = disp;
96
+ res = await fetchImpl(reqUrl, opts);
97
+ } catch (e) {
98
+ err = e;
99
+ } finally {
100
+ clearTimeout(timer);
101
+ }
102
+ const ms = Math.round(performance.now() - tAttempt);
103
+
104
+ if (err) {
105
+ attempts.push({ type: "network", ms });
106
+ const { shouldRetry, delayMs } = resolveRetry("network", attempt, retryCfg);
107
+ if (shouldRetry) {
108
+ await sleep(delayMs);
109
+ waitMs += delayMs;
110
+ continue;
111
+ }
112
+ err._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
113
+ throw err;
114
+ }
115
+
116
+ // http
117
+ attempts.push({ type: `http${res.status}`, ms });
118
+ const { shouldRetry, delayMs } = resolveRetry(res.status, attempt, retryCfg);
119
+ if (shouldRetry) {
120
+ // 消耗 body 以释放连接
121
+ try { if (res.body) await res.text().catch(() => {}); } catch {}
122
+ await sleep(delayMs);
123
+ waitMs += delayMs;
124
+ continue;
125
+ }
126
+
127
+ const ttfbMs = Math.round(performance.now() - t0);
128
+ const isStreamRequested = stream === true;
129
+ const contentType = res.headers.get("content-type") || "";
130
+ // 若请求为 stream 但上游返回的是 JSON(测试桩常见),则回退为非流式处理,避免 SSE 空聚合
131
+ const isStream = isStreamRequested && contentType.includes("text/event-stream");
132
+ if (!isStream) {
133
+ // 非流式:预读 body 以得 totalMs 与缓存(兼容 stream:true 但返回 JSON 的桩)
134
+ let cachedText = "";
135
+ let readMs = ttfbMs;
136
+ try {
137
+ cachedText = await res.text();
138
+ readMs = Math.round(performance.now() - t0);
139
+ } catch {
140
+ cachedText = "";
141
+ readMs = ttfbMs;
142
+ }
143
+ const totalMs = readMs;
144
+ const headers = res.headers;
145
+ return {
146
+ status: res.status,
147
+ headers,
148
+ ok: res.ok,
149
+ ttfbMs,
150
+ totalMs,
151
+ _t: { attempts, waitMs, totalMs },
152
+ async json() { try { return JSON.parse(cachedText); } catch { return cachedText; } },
153
+ async text() { return cachedText; },
154
+ stream() { throw new Error("not-streaming: call with stream:true"); },
155
+ get body() { return null; },
156
+ };
157
+ } else {
158
+ // 流式:保留原始 res 用于 stream()
159
+ const headers = res.headers;
160
+ let firstTtfb = ttfbMs; // 更新为首事件到达时刻(测得即所得)
161
+ let firstDone = false;
162
+ let lastRead = ttfbMs;
163
+ const markFirst = () => {
164
+ if (!firstDone) { firstTtfb = Math.round(performance.now() - t0); firstDone = true; }
165
+ };
166
+ let _t = { attempts, waitMs, totalMs: ttfbMs };
167
+ return {
168
+ status: res.status,
169
+ headers,
170
+ ok: res.ok,
171
+ get ttfbMs() { return firstDone ? firstTtfb : ttfbMs; },
172
+ get totalMs() { return lastRead; },
173
+ get _t() { return _t; },
174
+ set _t(v) { _t = v; },
175
+ async json() {
176
+ let acc = "";
177
+ for await (const chunk of this.stream()) acc += chunk;
178
+ try { return JSON.parse(acc); } catch { return acc; }
179
+ },
180
+ async text() {
181
+ let acc = "";
182
+ for await (const chunk of this.stream()) acc += chunk;
183
+ return acc;
184
+ },
185
+ get body() { return res.body; },
186
+ async *stream() {
187
+ const parser = createSseParser();
188
+ if (res.body && typeof res.body.getReader === "function") {
189
+ const reader = res.body.getReader();
190
+ const decoder = new TextDecoder();
191
+ while (true) {
192
+ const { done, value } = await reader.read();
193
+ if (done) break;
194
+ lastRead = Math.round(performance.now() - t0);
195
+ try { _t.totalMs = lastRead; } catch {}
196
+ const text = decoder.decode(value, { stream: true });
197
+ const evs = parser.push(text);
198
+ for (const e of evs) {
199
+ if (e === "[DONE]") return;
200
+ if (e === "") continue;
201
+ markFirst();
202
+ yield e;
203
+ }
204
+ }
205
+ } else if (typeof res.text === "function") {
206
+ const txt = await res.text();
207
+ const evs = parser.push(txt);
208
+ for (const e of evs) {
209
+ if (e === "[DONE]") return;
210
+ if (e === "") continue;
211
+ markFirst();
212
+ yield e;
213
+ }
214
+ }
215
+ },
216
+ };
217
+ }
218
+ }
219
+ }
220
+
221
+ async function preheat(url) {
222
+ const target = url || (baseUrl ? `${String(baseUrl).replace(/\/+$/, "")}/zen/v1/models` : null);
223
+ if (!target) return { ok: false, skipped: true };
224
+ const t0 = performance.now();
225
+ try {
226
+ const res = await request({ url: target, method: "GET", stream: false, timeoutMs: 3000 });
227
+ try { if (res.text) await res.text().catch(() => {}); } catch {}
228
+ return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
229
+ } catch (e) {
230
+ return { ok: false, error: String(e?.message || e), ms: Math.round(performance.now() - t0) };
231
+ }
232
+ }
233
+
234
+ async function close() {
235
+ if (closed) return;
236
+ closed = true;
237
+ if (pool) await pool.close();
238
+ }
239
+
240
+ return {
241
+ request,
242
+ preheat,
243
+ close,
244
+ get dispatcher() { return getDispatcher(); },
245
+ get agent() { return pool?.agent || null; },
246
+ [Symbol.asyncDispose]: close,
247
+ };
248
+ }
@@ -0,0 +1,60 @@
1
+ let UndiciAgent = null;
2
+ try {
3
+ const mod = await import("undici");
4
+ UndiciAgent = mod.Agent;
5
+ } catch {
6
+ UndiciAgent = null;
7
+ }
8
+
9
+ function envInt(name, fallback) {
10
+ const v = Number(process.env[name]);
11
+ return Number.isInteger(v) && v > 0 ? v : fallback;
12
+ }
13
+
14
+ export function createPool({
15
+ keepAlive = true,
16
+ keepAliveTimeout,
17
+ keepAliveMaxTimeout,
18
+ connections,
19
+ } = {}) {
20
+ const keepAliveTimeoutMs = keepAliveTimeout ?? envInt("MSLXDFF_UPSTREAM_KEEPALIVE_TIMEOUT", 30_000);
21
+ const keepAliveMaxTimeoutMs = keepAliveMaxTimeout ?? envInt("MSLXDFF_UPSTREAM_KEEPALIVE_MAX_TIMEOUT", 60_000);
22
+ const keepAliveConnections = connections ?? envInt("MSLXDFF_UPSTREAM_KEEPALIVE_CONNECTIONS", 20);
23
+
24
+ let agent = null;
25
+ let dispatcher = null;
26
+ let closed = false;
27
+
28
+ if (keepAlive && UndiciAgent) {
29
+ try {
30
+ agent = new UndiciAgent({
31
+ keepAliveTimeout: keepAliveTimeoutMs,
32
+ keepAliveMaxTimeout: keepAliveMaxTimeoutMs,
33
+ connections: keepAliveConnections,
34
+ pipelining: 1,
35
+ });
36
+ dispatcher = agent;
37
+ } catch {
38
+ agent = null;
39
+ dispatcher = null;
40
+ }
41
+ }
42
+
43
+ async function close() {
44
+ if (closed) return;
45
+ closed = true;
46
+ if (agent && typeof agent.close === "function") {
47
+ try { await agent.close(); } catch {}
48
+ } else if (dispatcher && typeof dispatcher.close === "function" && dispatcher !== agent) {
49
+ try { await dispatcher.close(); } catch {}
50
+ }
51
+ }
52
+
53
+ return {
54
+ get dispatcher() { return dispatcher; },
55
+ get agent() { return agent; },
56
+ get closed() { return closed; },
57
+ close,
58
+ [Symbol.asyncDispose]: close,
59
+ };
60
+ }
@@ -0,0 +1,24 @@
1
+ export function backoffDelay(baseMs, attempt) {
2
+ // attempt 0 => base, 1 => base*2, 2 => base*4
3
+ return baseMs * (1 << attempt);
4
+ }
5
+
6
+ export function resolveRetry(status, attempt, config) {
7
+ const key = status === "network" ? "network" : String(status);
8
+ const entry = config?.[key];
9
+ if (!entry) return { shouldRetry: false, delayMs: 0 };
10
+ const attempts = Number(entry.attempts) || 0;
11
+ const base = Number(entry.delayMs) || 0;
12
+ if (attempt < attempts) {
13
+ return { shouldRetry: true, delayMs: backoffDelay(base, attempt) };
14
+ }
15
+ return { shouldRetry: false, delayMs: 0 };
16
+ }
17
+
18
+ export function shouldRetry(status, attempt, config) {
19
+ return resolveRetry(status, attempt, config).shouldRetry;
20
+ }
21
+
22
+ export function sleep(ms) {
23
+ return new Promise((r) => setTimeout(r, ms));
24
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * SSE 行协议深模块 — 纯函数 + 流式
3
+ * 负责 `data: / : / event: / [DONE]` 的解析与粘包 remain 处理
4
+ */
5
+
6
+ export function parseSseChunk(buf) {
7
+ const events = [];
8
+ let remain = String(buf || "");
9
+ // 按 \n\n 分帧,\r\n 也兼容
10
+ // 保留最后一帧若不以 \n\n 结尾则为 remain
11
+ const normalized = remain.replace(/\r\n/g, "\n");
12
+ const parts = normalized.split("\n\n");
13
+ // 最后一部分若原串不以 \n\n 结尾则为 remain
14
+ const endsWithDelim = normalized.endsWith("\n\n") || normalized.endsWith("\n\r\n");
15
+ // parts 最后一项在非结尾时为未完成帧
16
+ const complete = endsWithDelim ? parts : parts.slice(0, -1);
17
+ remain = endsWithDelim ? "" : parts[parts.length - 1] || "";
18
+ for (const frame of complete) {
19
+ if (!frame) continue; // 空帧
20
+ const lines = frame.split("\n");
21
+ const dataLines = [];
22
+ for (const line of lines) {
23
+ if (!line) continue;
24
+ if (line.startsWith(":")) continue; // 注释/keepalive
25
+ if (line.startsWith("event:")) continue; // 事件类型忽略
26
+ if (line.startsWith("data:")) {
27
+ let v = line.slice(5);
28
+ if (v.startsWith(" ")) v = v.slice(1);
29
+ dataLines.push(v);
30
+ }
31
+ }
32
+ if (dataLines.length === 0) continue;
33
+ // 多行 data 按 \n 拼接(SSE 规范)
34
+ const payload = dataLines.join("\n");
35
+ // 空 data: 仍产生事件,由上层过滤;此处保留以便测试可见
36
+ events.push(payload);
37
+ }
38
+ return { events, remain };
39
+ }
40
+
41
+ export function createSseParser() {
42
+ let buf = "";
43
+ return {
44
+ push(chunk) {
45
+ buf += String(chunk || "");
46
+ const { events, remain } = parseSseChunk(buf);
47
+ buf = remain;
48
+ return events;
49
+ },
50
+ flush() {
51
+ // 未形成完整帧的不发(避免半包误触发)
52
+ if (!buf.trim()) { buf = ""; return []; }
53
+ // 若残留包含完整 data: 行但缺结尾,不强行发
54
+ return [];
55
+ },
56
+ getRemain() { return buf; },
57
+ };
58
+ }
59
+
60
+ export async function* streamFromResponse(res) {
61
+ if (!res || !res.body) return;
62
+ const parser = createSseParser();
63
+ const reader = res.body.getReader ? res.body.getReader() : null;
64
+ const decoder = new TextDecoder();
65
+ if (reader) {
66
+ while (true) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+ const text = decoder.decode(value, { stream: true });
70
+ const evs = parser.push(text);
71
+ for (const e of evs) {
72
+ if (e === "[DONE]") return;
73
+ if (e === "") continue;
74
+ yield e;
75
+ }
76
+ }
77
+ // flush 残留
78
+ const rest = parser.flush();
79
+ for (const e of rest) {
80
+ if (e === "[DONE]") return;
81
+ if (e) yield e;
82
+ }
83
+ } else if (typeof res.text === "function") {
84
+ // 回退:一次性 text
85
+ const txt = await res.text();
86
+ const evs = parser.push(txt);
87
+ for (const e of evs) {
88
+ if (e === "[DONE]") return;
89
+ if (e === "") continue;
90
+ yield e;
91
+ }
92
+ }
93
+ }