mslxdff 0.1.188 → 0.1.197
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 +3 -3
- package/package.json +1 -1
- package/src/anthropic/count-tokens.js +10 -3
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +173 -173
- package/src/chat-pipeline/policy.js +84 -84
- package/src/chat-pipeline/terminal-verdict.js +15 -6
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +136 -136
- package/src/cli/commands/provider/models.js +145 -145
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-login.js +37 -16
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +279 -279
- package/src/daemon.js +232 -232
- package/src/logs.js +11 -1
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +8 -1
- package/src/models.js +225 -225
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/chat.js +182 -182
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +203 -203
- package/src/providers/qoder/index.js +249 -249
- package/src/providers/qoder/sse.js +145 -145
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/chat.js +28 -12
- package/src/providers/raccoon/const.js +35 -6
- package/src/providers/raccoon/credits.js +26 -12
- package/src/providers/raccoon/envelope.js +10 -2
- package/src/providers/raccoon/login.js +114 -30
- package/src/providers/registry.js +79 -79
- package/src/providers/workbuddy/auth.js +26 -1
- package/src/providers/workbuddy/chat.js +264 -248
- package/src/providers/workbuddy/index.js +1 -1
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy/rotation-log.js +30 -2
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/responses/translate.js +5 -1
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +146 -146
- package/src/routes/hedge.js +260 -253
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +335 -335
- package/src/routes/stream-hold.js +66 -15
- package/src/routes/stream-scan.js +24 -4
- package/src/routes/stream.js +347 -343
- package/src/runtime/bootstrap.js +67 -67
- package/src/runtime/provider-gate.js +36 -36
- package/src/runtime/providers-setup.js +168 -168
- package/src/server.js +78 -78
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-engine/sdk/sse.js +15 -1
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +293 -293
- package/docs/cli_help_mini.md +0 -145
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
// workbuddy 流式 reasoning_content 整形:上游思考模型(如 glm-5.3-flash)把思考过程一词一 chunk
|
|
2
|
-
// 地放进 delta.reasoning_content,客户端会把每个增量渲染成独立 "- Thought" 条目。
|
|
3
|
-
// 旧版:连续 reasoning 增量缓冲到首个 content/finish 才一次性 flush → 思考期 10~30s 界面完全无变化(用户看到 workbuddy 后台流量走但 opencode 界面卡住)
|
|
4
|
-
// 新版:增量聚合 + 阈值分片:缓冲 reasoning,达到 REASONING_CHUNK_SIZE(默认 100 字符)即分片 flush,
|
|
5
|
-
// 期间仅首个 role 帧透传保持 TTFB,其余纯 reasoning 空帧吞掉(之前会透传 800+ 空 role 帧);
|
|
6
|
-
// 尾段不足阈值的在 content/finish/流结束时兜底 flush。恒空 reasoning(deepseek-v4-flash)零改写。
|
|
7
|
-
// 仅包 workbuddy 出口的 SSE 响应,不影响其他供应商与全局聚合器。
|
|
8
|
-
|
|
9
|
-
const REASONING_CHUNK_SIZE = 150;
|
|
10
|
-
|
|
11
|
-
export function reshapeWorkbuddySse(res) {
|
|
12
|
-
try {
|
|
13
|
-
const ct = res.headers?.get?.("content-type") || "";
|
|
14
|
-
if (res.status !== 200 || !ct.includes("text/event-stream") || !res.body) return res;
|
|
15
|
-
} catch { return res; }
|
|
16
|
-
|
|
17
|
-
const reader = res.body.getReader();
|
|
18
|
-
const decoder = new TextDecoder();
|
|
19
|
-
const encoder = new TextEncoder();
|
|
20
|
-
let buf = "";
|
|
21
|
-
let reasoningBuf = "";
|
|
22
|
-
let lastMeta = null;
|
|
23
|
-
let closed = false;
|
|
24
|
-
let hasSentRole = false;
|
|
25
|
-
let lastFlushAt = Date.now();
|
|
26
|
-
|
|
27
|
-
function flushReasoningInto(out) {
|
|
28
|
-
if (!reasoningBuf) return;
|
|
29
|
-
const meta = lastMeta || {};
|
|
30
|
-
out.push(`data: ${JSON.stringify({
|
|
31
|
-
id: meta.id || "chatcmpl-workbuddy-reshape",
|
|
32
|
-
object: meta.object || "chat.completion.chunk",
|
|
33
|
-
created: meta.created || Math.floor(Date.now() / 1000),
|
|
34
|
-
model: meta.model || "",
|
|
35
|
-
choices: [{ index: 0, delta: { reasoning_content: reasoningBuf }, finish_reason: null }],
|
|
36
|
-
})}\n\n`);
|
|
37
|
-
reasoningBuf = "";
|
|
38
|
-
lastFlushAt = Date.now();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let blankStreak = false;
|
|
42
|
-
// 处理一段完整行文本(以 \n 结尾),产出的行/帧推进 out(连续空行合并为单个事件分隔)
|
|
43
|
-
function processInto(text, out) {
|
|
44
|
-
for (const line of text.split("\n")) {
|
|
45
|
-
if (!line) { if (!blankStreak) out.push("\n"); blankStreak = true; continue; }
|
|
46
|
-
blankStreak = false;
|
|
47
|
-
if (!line.startsWith("data:")) { out.push(line + "\n"); continue; }
|
|
48
|
-
const payload = line.slice(5).trim();
|
|
49
|
-
if (!payload || payload === "[DONE]") { out.push(line + "\n"); continue; }
|
|
50
|
-
let obj;
|
|
51
|
-
try { obj = JSON.parse(payload); } catch { out.push(line + "\n"); continue; }
|
|
52
|
-
const ch = obj.choices?.[0];
|
|
53
|
-
if (ch && typeof ch === "object") {
|
|
54
|
-
if (obj.id) lastMeta = { id: obj.id, model: obj.model, object: obj.object, created: obj.created };
|
|
55
|
-
const d = ch.delta && typeof ch.delta === "object" ? ch.delta : null;
|
|
56
|
-
const rc = d ? d.reasoning_content : undefined;
|
|
57
|
-
if (typeof rc === "string" && rc) {
|
|
58
|
-
reasoningBuf += rc;
|
|
59
|
-
d.reasoning_content = "";
|
|
60
|
-
const isBoundary = (typeof d.content === "string" && d.content) || !!ch.finish_reason;
|
|
61
|
-
// 首个 reasoning 帧的 role 透传一次,保 TTFB;后续纯 reasoning 的 role 重复帧直接吞掉
|
|
62
|
-
if (!hasSentRole && d.role !== undefined) {
|
|
63
|
-
hasSentRole = true;
|
|
64
|
-
const rm = lastMeta || obj;
|
|
65
|
-
out.push(`data: ${JSON.stringify({
|
|
66
|
-
id: rm.id || obj.id || "chatcmpl-workbuddy-reshape",
|
|
67
|
-
object: rm.object || obj.object || "chat.completion.chunk",
|
|
68
|
-
created: rm.created || obj.created || Math.floor(Date.now() / 1000),
|
|
69
|
-
model: rm.model || obj.model || "",
|
|
70
|
-
choices: [{ index: 0, delta: { role: d.role }, finish_reason: null }],
|
|
71
|
-
})}\n\n`);
|
|
72
|
-
} else if (d.role !== undefined) {
|
|
73
|
-
// 后续重复 role 帧不再透传,避免 800+ 空帧洪泛
|
|
74
|
-
// 标记已发送过,避免后续非 reasoning 帧再误判为首次
|
|
75
|
-
hasSentRole = true;
|
|
76
|
-
}
|
|
77
|
-
// 阈值分片:攒够 100 字符即向前吐一块,期间界面可见增量而非全程静默
|
|
78
|
-
// 时间阈值:若距上次 flush 已超 700ms 且已攒 20+ 字符,也吐一块(避免小尾巴长时间不更新)
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
const shouldChunk = reasoningBuf.length >= REASONING_CHUNK_SIZE
|
|
81
|
-
|| (reasoningBuf.length >= 50 && now - lastFlushAt > 1500);
|
|
82
|
-
if (shouldChunk) flushReasoningInto(out);
|
|
83
|
-
if (isBoundary) {
|
|
84
|
-
// 兜底:content/finish 前把剩余思考一次性吐出,再透传本帧
|
|
85
|
-
flushReasoningInto(out);
|
|
86
|
-
const hasPassthrough = isBoundary || (Array.isArray(d.tool_calls) && d.tool_calls.length);
|
|
87
|
-
if (hasPassthrough) out.push(`data: ${JSON.stringify(obj)}\n\n`);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
// 纯 reasoning 碎片帧已缓冲/或已分片吐出,原空帧吞掉
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
// 非 reasoning 帧:若有 content/finish 先兜底 flush
|
|
94
|
-
if ((typeof d?.content === "string" && d.content) || ch.finish_reason) flushReasoningInto(out);
|
|
95
|
-
if (!hasSentRole && d?.role !== undefined) {
|
|
96
|
-
hasSentRole = true;
|
|
97
|
-
} else if (hasSentRole && d?.role !== undefined) {
|
|
98
|
-
// 去重:首个 role 后续重复 role 去掉,避免下游每帧都带 role
|
|
99
|
-
try { const clone = JSON.parse(payload); delete clone.choices[0].delta.role; out.push(`data: ${JSON.stringify(clone)}\n\n`); continue; } catch { /* fallback透传 */ }
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
out.push(line + "\n");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const body = new ReadableStream({
|
|
107
|
-
async pull(controller) {
|
|
108
|
-
if (closed) { try { controller.close(); } catch {} return; }
|
|
109
|
-
try {
|
|
110
|
-
// 循环读源,直到"有输出 / 流结束 / 出错"才返回。
|
|
111
|
-
// WHATWG 流规范:pull 的 promise 解析后若未 enqueue,规范不会再自动调度 pull
|
|
112
|
-
// (仅当解析期间有新读请求触发 pullAgain 才重调)——消费者只挂一个待处理 read 时,
|
|
113
|
-
// 任何一次"只吞帧不出数据"的拉取都会让整流永久停摆。workbuddy 思考流按帧到达
|
|
114
|
-
// (role 帧后每个 reasoning 碎片都不足 150 字符阈值)时必现,即"首帧后卡死"根因。
|
|
115
|
-
for (;;) {
|
|
116
|
-
const { done, value } = await reader.read();
|
|
117
|
-
if (done) {
|
|
118
|
-
closed = true;
|
|
119
|
-
const out = [];
|
|
120
|
-
if (buf) { processInto(buf + "\n", out); buf = ""; }
|
|
121
|
-
flushReasoningInto(out);
|
|
122
|
-
if (out.length) controller.enqueue(encoder.encode(out.join("")));
|
|
123
|
-
controller.close();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
buf += decoder.decode(value, { stream: true });
|
|
127
|
-
const idx = buf.lastIndexOf("\n");
|
|
128
|
-
if (idx < 0) continue;
|
|
129
|
-
const complete = buf.slice(0, idx + 1);
|
|
130
|
-
buf = buf.slice(idx + 1);
|
|
131
|
-
const out = [];
|
|
132
|
-
processInto(complete, out);
|
|
133
|
-
if (out.length) { controller.enqueue(encoder.encode(out.join(""))); return; }
|
|
134
|
-
}
|
|
135
|
-
} catch {
|
|
136
|
-
closed = true;
|
|
137
|
-
try { controller.close(); } catch {}
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
cancel() {
|
|
141
|
-
closed = true;
|
|
142
|
-
try {
|
|
143
|
-
const p = reader.cancel();
|
|
144
|
-
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
145
|
-
} catch {}
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const out = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
150
|
-
try { out._t = res._t; } catch {}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
1
|
+
// workbuddy 流式 reasoning_content 整形:上游思考模型(如 glm-5.3-flash)把思考过程一词一 chunk
|
|
2
|
+
// 地放进 delta.reasoning_content,客户端会把每个增量渲染成独立 "- Thought" 条目。
|
|
3
|
+
// 旧版:连续 reasoning 增量缓冲到首个 content/finish 才一次性 flush → 思考期 10~30s 界面完全无变化(用户看到 workbuddy 后台流量走但 opencode 界面卡住)
|
|
4
|
+
// 新版:增量聚合 + 阈值分片:缓冲 reasoning,达到 REASONING_CHUNK_SIZE(默认 100 字符)即分片 flush,
|
|
5
|
+
// 期间仅首个 role 帧透传保持 TTFB,其余纯 reasoning 空帧吞掉(之前会透传 800+ 空 role 帧);
|
|
6
|
+
// 尾段不足阈值的在 content/finish/流结束时兜底 flush。恒空 reasoning(deepseek-v4-flash)零改写。
|
|
7
|
+
// 仅包 workbuddy 出口的 SSE 响应,不影响其他供应商与全局聚合器。
|
|
8
|
+
|
|
9
|
+
const REASONING_CHUNK_SIZE = 150;
|
|
10
|
+
|
|
11
|
+
export function reshapeWorkbuddySse(res) {
|
|
12
|
+
try {
|
|
13
|
+
const ct = res.headers?.get?.("content-type") || "";
|
|
14
|
+
if (res.status !== 200 || !ct.includes("text/event-stream") || !res.body) return res;
|
|
15
|
+
} catch { return res; }
|
|
16
|
+
|
|
17
|
+
const reader = res.body.getReader();
|
|
18
|
+
const decoder = new TextDecoder();
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
let buf = "";
|
|
21
|
+
let reasoningBuf = "";
|
|
22
|
+
let lastMeta = null;
|
|
23
|
+
let closed = false;
|
|
24
|
+
let hasSentRole = false;
|
|
25
|
+
let lastFlushAt = Date.now();
|
|
26
|
+
|
|
27
|
+
function flushReasoningInto(out) {
|
|
28
|
+
if (!reasoningBuf) return;
|
|
29
|
+
const meta = lastMeta || {};
|
|
30
|
+
out.push(`data: ${JSON.stringify({
|
|
31
|
+
id: meta.id || "chatcmpl-workbuddy-reshape",
|
|
32
|
+
object: meta.object || "chat.completion.chunk",
|
|
33
|
+
created: meta.created || Math.floor(Date.now() / 1000),
|
|
34
|
+
model: meta.model || "",
|
|
35
|
+
choices: [{ index: 0, delta: { reasoning_content: reasoningBuf }, finish_reason: null }],
|
|
36
|
+
})}\n\n`);
|
|
37
|
+
reasoningBuf = "";
|
|
38
|
+
lastFlushAt = Date.now();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let blankStreak = false;
|
|
42
|
+
// 处理一段完整行文本(以 \n 结尾),产出的行/帧推进 out(连续空行合并为单个事件分隔)
|
|
43
|
+
function processInto(text, out) {
|
|
44
|
+
for (const line of text.split("\n")) {
|
|
45
|
+
if (!line) { if (!blankStreak) out.push("\n"); blankStreak = true; continue; }
|
|
46
|
+
blankStreak = false;
|
|
47
|
+
if (!line.startsWith("data:")) { out.push(line + "\n"); continue; }
|
|
48
|
+
const payload = line.slice(5).trim();
|
|
49
|
+
if (!payload || payload === "[DONE]") { out.push(line + "\n"); continue; }
|
|
50
|
+
let obj;
|
|
51
|
+
try { obj = JSON.parse(payload); } catch { out.push(line + "\n"); continue; }
|
|
52
|
+
const ch = obj.choices?.[0];
|
|
53
|
+
if (ch && typeof ch === "object") {
|
|
54
|
+
if (obj.id) lastMeta = { id: obj.id, model: obj.model, object: obj.object, created: obj.created };
|
|
55
|
+
const d = ch.delta && typeof ch.delta === "object" ? ch.delta : null;
|
|
56
|
+
const rc = d ? d.reasoning_content : undefined;
|
|
57
|
+
if (typeof rc === "string" && rc) {
|
|
58
|
+
reasoningBuf += rc;
|
|
59
|
+
d.reasoning_content = "";
|
|
60
|
+
const isBoundary = (typeof d.content === "string" && d.content) || !!ch.finish_reason;
|
|
61
|
+
// 首个 reasoning 帧的 role 透传一次,保 TTFB;后续纯 reasoning 的 role 重复帧直接吞掉
|
|
62
|
+
if (!hasSentRole && d.role !== undefined) {
|
|
63
|
+
hasSentRole = true;
|
|
64
|
+
const rm = lastMeta || obj;
|
|
65
|
+
out.push(`data: ${JSON.stringify({
|
|
66
|
+
id: rm.id || obj.id || "chatcmpl-workbuddy-reshape",
|
|
67
|
+
object: rm.object || obj.object || "chat.completion.chunk",
|
|
68
|
+
created: rm.created || obj.created || Math.floor(Date.now() / 1000),
|
|
69
|
+
model: rm.model || obj.model || "",
|
|
70
|
+
choices: [{ index: 0, delta: { role: d.role }, finish_reason: null }],
|
|
71
|
+
})}\n\n`);
|
|
72
|
+
} else if (d.role !== undefined) {
|
|
73
|
+
// 后续重复 role 帧不再透传,避免 800+ 空帧洪泛
|
|
74
|
+
// 标记已发送过,避免后续非 reasoning 帧再误判为首次
|
|
75
|
+
hasSentRole = true;
|
|
76
|
+
}
|
|
77
|
+
// 阈值分片:攒够 100 字符即向前吐一块,期间界面可见增量而非全程静默
|
|
78
|
+
// 时间阈值:若距上次 flush 已超 700ms 且已攒 20+ 字符,也吐一块(避免小尾巴长时间不更新)
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const shouldChunk = reasoningBuf.length >= REASONING_CHUNK_SIZE
|
|
81
|
+
|| (reasoningBuf.length >= 50 && now - lastFlushAt > 1500);
|
|
82
|
+
if (shouldChunk) flushReasoningInto(out);
|
|
83
|
+
if (isBoundary) {
|
|
84
|
+
// 兜底:content/finish 前把剩余思考一次性吐出,再透传本帧
|
|
85
|
+
flushReasoningInto(out);
|
|
86
|
+
const hasPassthrough = isBoundary || (Array.isArray(d.tool_calls) && d.tool_calls.length);
|
|
87
|
+
if (hasPassthrough) out.push(`data: ${JSON.stringify(obj)}\n\n`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// 纯 reasoning 碎片帧已缓冲/或已分片吐出,原空帧吞掉
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// 非 reasoning 帧:若有 content/finish 先兜底 flush
|
|
94
|
+
if ((typeof d?.content === "string" && d.content) || ch.finish_reason) flushReasoningInto(out);
|
|
95
|
+
if (!hasSentRole && d?.role !== undefined) {
|
|
96
|
+
hasSentRole = true;
|
|
97
|
+
} else if (hasSentRole && d?.role !== undefined) {
|
|
98
|
+
// 去重:首个 role 后续重复 role 去掉,避免下游每帧都带 role
|
|
99
|
+
try { const clone = JSON.parse(payload); delete clone.choices[0].delta.role; out.push(`data: ${JSON.stringify(clone)}\n\n`); continue; } catch { /* fallback透传 */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
out.push(line + "\n");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = new ReadableStream({
|
|
107
|
+
async pull(controller) {
|
|
108
|
+
if (closed) { try { controller.close(); } catch {} return; }
|
|
109
|
+
try {
|
|
110
|
+
// 循环读源,直到"有输出 / 流结束 / 出错"才返回。
|
|
111
|
+
// WHATWG 流规范:pull 的 promise 解析后若未 enqueue,规范不会再自动调度 pull
|
|
112
|
+
// (仅当解析期间有新读请求触发 pullAgain 才重调)——消费者只挂一个待处理 read 时,
|
|
113
|
+
// 任何一次"只吞帧不出数据"的拉取都会让整流永久停摆。workbuddy 思考流按帧到达
|
|
114
|
+
// (role 帧后每个 reasoning 碎片都不足 150 字符阈值)时必现,即"首帧后卡死"根因。
|
|
115
|
+
for (;;) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) {
|
|
118
|
+
closed = true;
|
|
119
|
+
const out = [];
|
|
120
|
+
if (buf) { processInto(buf + "\n", out); buf = ""; }
|
|
121
|
+
flushReasoningInto(out);
|
|
122
|
+
if (out.length) controller.enqueue(encoder.encode(out.join("")));
|
|
123
|
+
controller.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
buf += decoder.decode(value, { stream: true });
|
|
127
|
+
const idx = buf.lastIndexOf("\n");
|
|
128
|
+
if (idx < 0) continue;
|
|
129
|
+
const complete = buf.slice(0, idx + 1);
|
|
130
|
+
buf = buf.slice(idx + 1);
|
|
131
|
+
const out = [];
|
|
132
|
+
processInto(complete, out);
|
|
133
|
+
if (out.length) { controller.enqueue(encoder.encode(out.join(""))); return; }
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
closed = true;
|
|
137
|
+
try { controller.close(); } catch {}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
cancel() {
|
|
141
|
+
closed = true;
|
|
142
|
+
try {
|
|
143
|
+
const p = reader.cancel();
|
|
144
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
145
|
+
} catch {}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const out = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
150
|
+
try { out._t = res._t; } catch {}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -18,6 +18,33 @@ function defaultDirs() {
|
|
|
18
18
|
return [...dirs];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// 裁剪保留比例(按字节)。阈值 `st.size` 是**字节**,原先的回退 `content.slice(-512*1024)` 是**字符** ——
|
|
22
|
+
// 量纲不匹配:中文(3 字节/字符)下保留后字节数浮动,且 slice 按字符切会**切在半行**上
|
|
23
|
+
// (实测开头得到 `"el=workbuddy/deepseek totalMs="`,下一行接上去就把日志结构毁了)。
|
|
24
|
+
// 与 logs.js / model-trace.js 同口径:按字节留 3/4,且不截半行。
|
|
25
|
+
const KEEP_RATIO = 3 / 4;
|
|
26
|
+
|
|
27
|
+
function keepTailByBytes(text, maxBytes) {
|
|
28
|
+
const limit = Math.max(1, Math.floor(maxBytes * KEEP_RATIO));
|
|
29
|
+
if (Buffer.byteLength(text) <= limit) return text;
|
|
30
|
+
const lines = text.split("\n");
|
|
31
|
+
let bytes = 0;
|
|
32
|
+
let i = lines.length - 1;
|
|
33
|
+
for (; i >= 0; i--) {
|
|
34
|
+
const b = Buffer.byteLength(lines[i]) + 1;
|
|
35
|
+
if (bytes + b > limit) break;
|
|
36
|
+
bytes += b;
|
|
37
|
+
}
|
|
38
|
+
// 至少保留最后一条:单行超 limit 时否则返回空串 = 清空整个轮转日志。
|
|
39
|
+
// 判据是「结果为空」——文件尾有换行时 split 的最后一个元素是空串,`i === lines.length-1` 判不到内容行。
|
|
40
|
+
const kept = lines.slice(i + 1).join("\n");
|
|
41
|
+
if (!kept) {
|
|
42
|
+
const nonEmpty = lines.filter((l) => l !== "");
|
|
43
|
+
if (nonEmpty.length) return nonEmpty[nonEmpty.length - 1];
|
|
44
|
+
}
|
|
45
|
+
return kept;
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
export function appendRotationLog({ uid, model, totalMs, balanceHit, error, clock = Date.now, fs: fsOverride, dirs: dirsOverride, maxBytes = 1024 * 1024 } = {}) {
|
|
22
49
|
// 兼容旧调用:appendRotationLog({uid,model,totalMs,balanceHit,error}) 形式,clock/fs 可选
|
|
23
50
|
const useFs = fsOverride || { appendFileSync, mkdirSync, readFileSync, writeFileSync, statSync, join };
|
|
@@ -32,8 +59,9 @@ export function appendRotationLog({ uid, model, totalMs, balanceHit, error, cloc
|
|
|
32
59
|
try {
|
|
33
60
|
const st = useFs.statSync(file);
|
|
34
61
|
if (st.size > maxBytes) {
|
|
35
|
-
|
|
36
|
-
useFs.
|
|
62
|
+
// 按字节回退(不截半行),与 logs.js / model-trace.js 同口径。
|
|
63
|
+
const kept = keepTailByBytes(useFs.readFileSync(file, "utf8"), maxBytes);
|
|
64
|
+
useFs.writeFileSync(file, kept.endsWith("\n") || !kept ? kept : `${kept}\n`);
|
|
37
65
|
}
|
|
38
66
|
} catch {}
|
|
39
67
|
} catch {}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Shim — 保持历史导入路径兼容,真实实现下沉至 ./workbuddy/ 深模块
|
|
2
|
-
export * from "./workbuddy/index.js";
|
|
1
|
+
// Shim — 保持历史导入路径兼容,真实实现下沉至 ./workbuddy/ 深模块
|
|
2
|
+
export * from "./workbuddy/index.js";
|
package/src/reasoning.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
const PLACEHOLDER = " ";
|
|
2
|
-
|
|
3
|
-
const MODEL_RULES = [
|
|
4
|
-
{ match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
|
|
5
|
-
{ match: (m) => /deepseek/i.test(m || ""), scope: "all" },
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
export function normalizeModel(model) {
|
|
9
|
-
return model.startsWith("oc/") ? model.slice(3) : model;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function shouldInject(message, scope) {
|
|
13
|
-
if (message?.role !== "assistant") return false;
|
|
14
|
-
const rc = message.reasoning_content;
|
|
15
|
-
if (typeof rc === "string" && rc.length > 0) return false;
|
|
16
|
-
if (scope === "toolCalls") {
|
|
17
|
-
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
18
|
-
}
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function applyRule(body, rule) {
|
|
23
|
-
if (!rule || !body?.messages) return body;
|
|
24
|
-
const messages = body.messages.map((m) =>
|
|
25
|
-
shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
|
|
26
|
-
);
|
|
27
|
-
return { ...body, messages };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function injectReasoningContent(model, body) {
|
|
31
|
-
const rule = MODEL_RULES.find((r) => r.match(model));
|
|
32
|
-
return applyRule(body, rule);
|
|
1
|
+
const PLACEHOLDER = " ";
|
|
2
|
+
|
|
3
|
+
const MODEL_RULES = [
|
|
4
|
+
{ match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
|
|
5
|
+
{ match: (m) => /deepseek/i.test(m || ""), scope: "all" },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function normalizeModel(model) {
|
|
9
|
+
return model.startsWith("oc/") ? model.slice(3) : model;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldInject(message, scope) {
|
|
13
|
+
if (message?.role !== "assistant") return false;
|
|
14
|
+
const rc = message.reasoning_content;
|
|
15
|
+
if (typeof rc === "string" && rc.length > 0) return false;
|
|
16
|
+
if (scope === "toolCalls") {
|
|
17
|
+
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applyRule(body, rule) {
|
|
23
|
+
if (!rule || !body?.messages) return body;
|
|
24
|
+
const messages = body.messages.map((m) =>
|
|
25
|
+
shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
|
|
26
|
+
);
|
|
27
|
+
return { ...body, messages };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function injectReasoningContent(model, body) {
|
|
31
|
+
const rule = MODEL_RULES.find((r) => r.match(model));
|
|
32
|
+
return applyRule(body, rule);
|
|
33
33
|
}
|
|
@@ -89,7 +89,11 @@ export function responsesToChatBody(req = {}) {
|
|
|
89
89
|
messages.push({ role: "tool", tool_call_id: it.call_id || "", content: typeof it.output === "string" ? it.output : JSON.stringify(it.output ?? "") });
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
// 空会话直接抛错,**绝不伪造一句 "hi" 打上游**:那会把假提问写进用户上下文并白烧一发额度。
|
|
93
|
+
// 口径与 anthropic/request-body.js:83-84 一致(同一仓库同一类场景不该有两套策略)。
|
|
94
|
+
// 触发面比"空 input"更广:input 里全是未知 item 类型(如 `{type:"unknown_item"}`)时也走这里
|
|
95
|
+
// —— 静默发一句 hi 会让客户端拿到一个与它请求毫无关系的回答。
|
|
96
|
+
if (!messages.length) throw new Error("responses: 缺少用户消息");
|
|
93
97
|
const body = { model, messages, stream: Boolean(req.stream) };
|
|
94
98
|
if (Array.isArray(req.tools) && req.tools.length) {
|
|
95
99
|
body.tools = req.tools.map((t) => (t?.type === "function"
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { clientIp, json, readBody, parseHops } from "../helpers.js";
|
|
2
|
-
import { runHook } from "../../plugins.js";
|
|
3
|
-
import { createChatPipeline } from "../../chat-pipeline/index.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* ChatGateway 薄适配层:仅负责 HTTP I/O(readBody + request:received hook)→ 委托 ChatPipeline.execute
|
|
7
|
-
* Policy/Order/AutoRace/hedge/peer/broadband 全部在 chat-pipeline 深模块内。
|
|
8
|
-
*/
|
|
9
|
-
export function createChatGateway(deps = {}) {
|
|
10
|
-
const pipeline = createChatPipeline(deps);
|
|
11
|
-
|
|
12
|
-
async function handle({ req, res }) {
|
|
13
|
-
let body;
|
|
14
|
-
try {
|
|
15
|
-
body = await readBody(req);
|
|
16
|
-
} catch {
|
|
17
|
-
return json(res, 400, { error: "Invalid JSON body" });
|
|
18
|
-
}
|
|
19
|
-
if (deps.plugins?.length) {
|
|
20
|
-
const rc = await runHook(deps.plugins, "request:received", {
|
|
21
|
-
ip: clientIp(req),
|
|
22
|
-
hops: parseHops(req.headers["x-mslxdff-hops"]),
|
|
23
|
-
headers: { "content-type": req.headers["content-type"] },
|
|
24
|
-
body,
|
|
25
|
-
});
|
|
26
|
-
for (const e of rc.errors) deps.logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
|
|
27
|
-
const respond = rc.value?.respond;
|
|
28
|
-
if (respond && typeof respond === "object") return json(res, respond.status || 200, respond.body ?? {});
|
|
29
|
-
}
|
|
30
|
-
req.body = body;
|
|
31
|
-
try {
|
|
32
|
-
await pipeline.execute({ req, res });
|
|
33
|
-
} catch (err) {
|
|
34
|
-
const msg = err?.message || String(err);
|
|
35
|
-
if (!res.headersSent) return json(res, 502, { error: msg });
|
|
36
|
-
try { res.end(); } catch {}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { handle };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 薄适配:保持原 chatHandler 签名兼容
|
|
44
|
-
export async function chatHandler(ctx) {
|
|
45
|
-
const gw = createChatGateway(ctx);
|
|
46
|
-
return gw.handle(ctx);
|
|
1
|
+
import { clientIp, json, readBody, parseHops } from "../helpers.js";
|
|
2
|
+
import { runHook } from "../../plugins.js";
|
|
3
|
+
import { createChatPipeline } from "../../chat-pipeline/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ChatGateway 薄适配层:仅负责 HTTP I/O(readBody + request:received hook)→ 委托 ChatPipeline.execute
|
|
7
|
+
* Policy/Order/AutoRace/hedge/peer/broadband 全部在 chat-pipeline 深模块内。
|
|
8
|
+
*/
|
|
9
|
+
export function createChatGateway(deps = {}) {
|
|
10
|
+
const pipeline = createChatPipeline(deps);
|
|
11
|
+
|
|
12
|
+
async function handle({ req, res }) {
|
|
13
|
+
let body;
|
|
14
|
+
try {
|
|
15
|
+
body = await readBody(req);
|
|
16
|
+
} catch {
|
|
17
|
+
return json(res, 400, { error: "Invalid JSON body" });
|
|
18
|
+
}
|
|
19
|
+
if (deps.plugins?.length) {
|
|
20
|
+
const rc = await runHook(deps.plugins, "request:received", {
|
|
21
|
+
ip: clientIp(req),
|
|
22
|
+
hops: parseHops(req.headers["x-mslxdff-hops"]),
|
|
23
|
+
headers: { "content-type": req.headers["content-type"] },
|
|
24
|
+
body,
|
|
25
|
+
});
|
|
26
|
+
for (const e of rc.errors) deps.logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
|
|
27
|
+
const respond = rc.value?.respond;
|
|
28
|
+
if (respond && typeof respond === "object") return json(res, respond.status || 200, respond.body ?? {});
|
|
29
|
+
}
|
|
30
|
+
req.body = body;
|
|
31
|
+
try {
|
|
32
|
+
await pipeline.execute({ req, res });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const msg = err?.message || String(err);
|
|
35
|
+
if (!res.headersSent) return json(res, 502, { error: msg });
|
|
36
|
+
try { res.end(); } catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { handle };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 薄适配:保持原 chatHandler 签名兼容
|
|
44
|
+
export async function chatHandler(ctx) {
|
|
45
|
+
const gw = createChatGateway(ctx);
|
|
46
|
+
return gw.handle(ctx);
|
|
47
47
|
}
|