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.
Files changed (107) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/anthropic/count-tokens.js +10 -3
  4. package/src/auto.js +254 -254
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +173 -173
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/chat-pipeline/terminal-verdict.js +15 -6
  22. package/src/cli/commands/group.js +249 -249
  23. package/src/cli/commands/model/picks.js +50 -50
  24. package/src/cli/commands/provider/bench-via.js +247 -247
  25. package/src/cli/commands/provider/bench.js +141 -141
  26. package/src/cli/commands/provider/index.js +136 -136
  27. package/src/cli/commands/provider/models.js +145 -145
  28. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  29. package/src/cli/commands/provider/raccoon-login.js +37 -16
  30. package/src/cli/commands/sync.js +310 -310
  31. package/src/cli/status.js +279 -279
  32. package/src/daemon.js +232 -232
  33. package/src/logs.js +11 -1
  34. package/src/model-capabilities/enrich.js +86 -86
  35. package/src/model-capabilities/index.js +183 -183
  36. package/src/model-capabilities/parse.js +70 -70
  37. package/src/model-trace.js +8 -1
  38. package/src/models.js +225 -225
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +182 -182
  43. package/src/providers/globalqwenwork/cosy.js +123 -123
  44. package/src/providers/globalqwenwork/crypto.js +218 -218
  45. package/src/providers/globalqwenwork/http.js +20 -20
  46. package/src/providers/globalqwenwork/rsa.js +54 -54
  47. package/src/providers/globalqwenwork/sse.js +268 -268
  48. package/src/providers/globalqwenwork/stream.js +130 -130
  49. package/src/providers/globalqwenwork/upstream.js +122 -122
  50. package/src/providers/keyring.js +60 -60
  51. package/src/providers/qoder/chat.js +203 -203
  52. package/src/providers/qoder/index.js +249 -249
  53. package/src/providers/qoder/sse.js +145 -145
  54. package/src/providers/qwenwork/account-store.js +133 -133
  55. package/src/providers/qwenwork/constants.js +67 -67
  56. package/src/providers/qwenwork/cosy.js +120 -120
  57. package/src/providers/qwenwork/crypto.js +218 -218
  58. package/src/providers/qwenwork/http.js +20 -20
  59. package/src/providers/qwenwork/index.js +337 -337
  60. package/src/providers/qwenwork/payload.js +144 -144
  61. package/src/providers/qwenwork/rsa.js +54 -54
  62. package/src/providers/qwenwork/sse.js +268 -268
  63. package/src/providers/qwenwork/stream.js +130 -130
  64. package/src/providers/qwenwork/upstream.js +120 -120
  65. package/src/providers/qwenwork.js +1 -1
  66. package/src/providers/raccoon/chat.js +28 -12
  67. package/src/providers/raccoon/const.js +35 -6
  68. package/src/providers/raccoon/credits.js +26 -12
  69. package/src/providers/raccoon/envelope.js +10 -2
  70. package/src/providers/raccoon/login.js +114 -30
  71. package/src/providers/registry.js +79 -79
  72. package/src/providers/workbuddy/auth.js +26 -1
  73. package/src/providers/workbuddy/chat.js +264 -248
  74. package/src/providers/workbuddy/index.js +1 -1
  75. package/src/providers/workbuddy/reshape.js +152 -152
  76. package/src/providers/workbuddy/rotation-log.js +30 -2
  77. package/src/providers/workbuddy.js +2 -2
  78. package/src/reasoning.js +32 -32
  79. package/src/responses/translate.js +5 -1
  80. package/src/routes/chat/gateway.js +46 -46
  81. package/src/routes/chat/relay-pipeline.js +282 -282
  82. package/src/routes/chat/via-route-handler.js +146 -146
  83. package/src/routes/hedge.js +260 -253
  84. package/src/routes/models-route.js +167 -167
  85. package/src/routes/peers.js +335 -335
  86. package/src/routes/stream-hold.js +66 -15
  87. package/src/routes/stream-scan.js +24 -4
  88. package/src/routes/stream.js +347 -343
  89. package/src/runtime/bootstrap.js +67 -67
  90. package/src/runtime/provider-gate.js +36 -36
  91. package/src/runtime/providers-setup.js +168 -168
  92. package/src/server.js +78 -78
  93. package/src/state/schemas/allowlist.js +92 -92
  94. package/src/sync-opencode.js +280 -280
  95. package/src/transport/index.js +244 -244
  96. package/src/transport/pool.js +56 -56
  97. package/src/transport/retry.js +24 -24
  98. package/src/transport/sse.js +93 -93
  99. package/src/upstream-engine/sdk/sse.js +15 -1
  100. package/src/upstream-probe/display.js +52 -52
  101. package/src/upstream-probe/probe.js +49 -49
  102. package/src/upstream-probe/rotate.js +110 -110
  103. package/src/upstream-probe/start.js +45 -45
  104. package/src/upstream.js +293 -293
  105. package/docs/cli_help_mini.md +0 -145
  106. package/src/providers/AGENTS.md +0 -49
  107. 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
- const content = useFs.readFileSync(file, "utf8");
36
- useFs.writeFileSync(file, content.slice(-512 * 1024));
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
- if (!messages.length) messages.push({ role: "user", content: "hi" });
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
  }