mslxdff 0.1.231 → 0.1.232

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 (104) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +254 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +145 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/models.js +225 -225
  35. package/src/peers.js +345 -345
  36. package/src/providers/agentrouter.js +238 -238
  37. package/src/providers/base.js +164 -164
  38. package/src/providers/cline/auth.js +228 -228
  39. package/src/providers/cline/chat.js +307 -307
  40. package/src/providers/cline.js +2 -2
  41. package/src/providers/codearts/chat.js +207 -207
  42. package/src/providers/dispatcher.js +147 -147
  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/const.js +202 -202
  67. package/src/providers/raccoon/credits.js +285 -285
  68. package/src/providers/registry.js +89 -89
  69. package/src/providers/workbuddy/auth.js +16 -0
  70. package/src/providers/workbuddy/chat.js +298 -264
  71. package/src/providers/workbuddy/client-headers.js +148 -0
  72. package/src/providers/workbuddy/desktop-info.js +17 -1
  73. package/src/providers/workbuddy/models.js +8 -20
  74. package/src/providers/workbuddy/reshape.js +152 -152
  75. package/src/providers/workbuddy.js +2 -2
  76. package/src/reasoning.js +32 -32
  77. package/src/routes/chat/broadband-handler.js +80 -80
  78. package/src/routes/chat/gateway.js +46 -46
  79. package/src/routes/chat/relay-pipeline.js +282 -282
  80. package/src/routes/chat/via-route-handler.js +156 -156
  81. package/src/routes/hedge.js +260 -260
  82. package/src/routes/models-route.js +167 -167
  83. package/src/routes/peers.js +373 -373
  84. package/src/routes/stream.js +347 -347
  85. package/src/runtime/bootstrap.js +75 -75
  86. package/src/runtime/keep-warm.js +135 -135
  87. package/src/runtime/provider-gate.js +75 -75
  88. package/src/runtime/providers-setup.js +187 -187
  89. package/src/runtime/raccoon-checkin.js +175 -175
  90. package/src/server.js +85 -85
  91. package/src/state/schemas/allowlist.js +92 -92
  92. package/src/sync-opencode.js +280 -280
  93. package/src/transport/index.js +244 -244
  94. package/src/transport/pool.js +56 -56
  95. package/src/transport/retry.js +24 -24
  96. package/src/transport/sse.js +93 -93
  97. package/src/upstream-probe/display.js +52 -52
  98. package/src/upstream-probe/probe.js +49 -49
  99. package/src/upstream-probe/rotate.js +110 -110
  100. package/src/upstream-probe/start.js +45 -45
  101. package/src/upstream.js +355 -355
  102. package/docs/cli_help_mini.md +0 -148
  103. package/src/providers/AGENTS.md +0 -49
  104. package/src/routes/AGENTS.md +0 -37
@@ -0,0 +1,148 @@
1
+ // WorkBuddy 出站请求头——照抄本机官方桌面端 5.8.3 的组装真值(buildVlmGatewayHeaders +
2
+ // applyGatewayHeaders + applyTraceHeaders 三函数,逆向自 app.asar @89622109,见 .scratch/wb-chat-headers.txt)。
3
+ // 为什么要它:2026-10-21 三号集体撞 `403 11140 账号存在异常行为` 封号,且换 UA 到官方完整口径也照样 11140
4
+ // ——判的是账号本体,但旧出站指纹是**自造杂交串**:UA `CLI/2.115.0 WorkBuddy/2.115.0`(真机是
5
+ // `WorkBuddy/5.8.3 WorkBuddy/5.8.3`)、缺 X-IDE-*/X-Private-Data/x-codebuddy-request/X-Conversation-*/
6
+ // traceparent 全链追踪头、还多带真机 chat 明确排除的 Origin/Referer。裸指纹 + 桌面登录号 = 风控画像。
7
+ // 对齐口径降低「登新号又被带进沟」的概率。⚠ 对齐是降风险非保命,封号是账号级、refresh 已证实无效。
8
+ // 版本随客户端自动更新会漂移:MSLXDFF_WORKBUDDY_APP_VERSION 覆盖(缺省 5.8.3);
9
+ // MSLXDFF_WORKBUDDY_CLI_VERSION 非空才追加 ` CLI/<v>` 段(真机 buildVlmGatewayHeaders 的 i 参数可为空)。
10
+ // 一键回退旧口径:MSLXDFF_WORKBUDDY_LEGACY_HEADERS=1。
11
+ import { randomBytes } from "node:crypto";
12
+
13
+ const APP_VERSION = () => (process.env.MSLXDFF_WORKBUDDY_APP_VERSION || "5.8.3").trim();
14
+ // CLI 段版本取自本机活体日志(a06ef5f8 实时 chat 请求 UA `WorkBuddy/5.8.3 WorkBuddy/5.8.3 CLI/2.161.0`)。
15
+ const CLI_VERSION = () => {
16
+ const v = (process.env.MSLXDFF_WORKBUDDY_CLI_VERSION ?? "").trim();
17
+ return v === "" ? "2.161.0" : v; // 缺省跟本机真值;显式空串仍可关掉该段
18
+ };
19
+ const isLegacy = () => process.env.MSLXDFF_WORKBUDDY_LEGACY_HEADERS === "1";
20
+
21
+ function hex(n) { return randomBytes(n).toString("hex"); }
22
+
23
+ // 真机 X-Request-ID 家族是 UUIDv7(毫秒时间戳打头 48bit + 版本位 7 + 变体位 10xx),
24
+ // 活体日志证实:`01a15dcaf9a97a86a215ac50f6bcdfff` 第 13 位恒为 '7'。裸 hex 版本位 15/16 概率错、
25
+ // 时间戳前缀是乱码——风控一眼假。按 https://www.rfc-editor.org/rfc/rfc9562 组真 v7。
26
+ function uuidv7hex() {
27
+ const b = Buffer.from(randomBytes(16));
28
+ const ts = Date.now();
29
+ b[0] = Math.floor(ts / 2 ** 40) & 0xff;
30
+ b[1] = Math.floor(ts / 2 ** 32) & 0xff;
31
+ b[2] = Math.floor(ts / 2 ** 24) & 0xff;
32
+ b[3] = Math.floor(ts / 2 ** 16) & 0xff;
33
+ b[4] = Math.floor(ts / 2 ** 8) & 0xff;
34
+ b[5] = ts & 0xff;
35
+ b[6] = (b[6] & 0x0f) | 0x70; // version 7
36
+ b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
37
+ return b.toString("hex");
38
+ }
39
+ // 真机 X-Conversation-ID 家族是 UUIDv4(带横线,版本位 4 / 变体位 8~b),活体:`b84c6a36-bd21-461a-b9be-…`。
40
+ function uuidv4dashed() {
41
+ const b = Buffer.from(randomBytes(16));
42
+ b[6] = (b[6] & 0x0f) | 0x40;
43
+ b[8] = (b[8] & 0x3f) | 0x80;
44
+ const h = b.toString("hex");
45
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
46
+ }
47
+
48
+ // 真机 CLI trace 头(bundle @4733511 trace_start_hook,**不是** Electron 代理链的 applyTraceHeaders):
49
+ // 只设 X-Trace-ID/X-B3-TraceId(=同 traceId 32hex)/X-B3-SpanId(16hex)/X-B3-Sampled=1/b3(<trace32>-<span16>-1 三段)。
50
+ // 无 traceparent、无 X-B3-ParentSpanId——那两个是 Electron web 转发链的形状,chat 实发者是 CLI 子进程,别抄错链路。
51
+ function traceHeaders() {
52
+ const n = hex(16), r = hex(8);
53
+ return {
54
+ "X-Trace-ID": n,
55
+ "X-B3-TraceId": n,
56
+ "X-B3-SpanId": r,
57
+ "X-B3-Sampled": "1",
58
+ b3: `${n}-${r}-1`,
59
+ };
60
+ }
61
+
62
+ // 网关态:每次调用是一条新逻辑会话。id 形状取自活体日志(2026-10-21 a06ef5f8 正常发 200 的 CLI 请求):
63
+ // 会话 id = **UUIDv4 带横线**(`b84c6a36-bd21-461a-b9be-…`);请求 id = **UUIDv7 去横线**(第 13 位恒 '7')。
64
+ // bundle 逐字(@5997642 buildConversationHeaders):X-Conversation-ID=会话 id;X-Request-ID=X-Conversation-Message-ID
65
+ // =同一 uuidv7 双写;X-Conversation-Request-ID=**另一个** uuidv7(turn 级生成器)。网关一调用=新会话=新 turn,
66
+ // 两值各生成一份即可(真机一 turn 多请求时才复用 Conversation-Request-ID)。
67
+ // ⚠ X-Root-Request-ID / X-Parent-Conversation-ID 真机是**条件头**(meta 有值才设——sub-agent/续聊轮才带)。
68
+ // 网关永远首请求,带上=把每条请求伪造成续聊,画像反常 → 不带。X-Session-ID 保留(活体 config/日志有同值头)。
69
+ function conversationHeaders() {
70
+ const conv = uuidv4dashed();
71
+ const req = uuidv7hex();
72
+ const turn = uuidv7hex();
73
+ return {
74
+ "X-Conversation-ID": conv,
75
+ "X-Session-ID": conv,
76
+ "X-Request-ID": req,
77
+ "X-Conversation-Request-ID": turn,
78
+ "X-Conversation-Message-ID": req,
79
+ "X-Agent-Type": "main",
80
+ "acp-connection-id": req,
81
+ };
82
+ }
83
+
84
+ function legacyHeaders(key, auth, accept) {
85
+ const h = {
86
+ "Content-Type": "application/json",
87
+ Accept: accept,
88
+ "User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
89
+ Origin: "https://www.codebuddy.cn",
90
+ Referer: "https://www.codebuddy.cn/",
91
+ "X-Product": "SaaS",
92
+ };
93
+ if (key) h["Authorization"] = `Bearer ${key}`;
94
+ if (auth?.uid) h["X-User-Id"] = auth.uid;
95
+ h["X-Domain"] = auth?.domain || "www.codebuddy.cn";
96
+ if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
97
+ return h;
98
+ }
99
+
100
+ // 官方桌面端完整口径。三处出口(chat / refresh / config)共用,保证**一个号一张脸**:
101
+ // chat 带每请求 conversation/trace id;refresh、config 无会话 → withConversation:false。
102
+ // 真机 buildVlmGatewayHeaders 的 Accept 是 application/json(SSE 靠 body.stream 协商),故 accept 默认即此。
103
+ export function buildWorkbuddyHeaders(key, auth, { accept = "application/json", withConversation = true } = {}) {
104
+ if (isLegacy()) return legacyHeaders(key, auth, accept);
105
+ const app = APP_VERSION(), cli = CLI_VERSION();
106
+ const ua = [`WorkBuddy/${app}`, `WorkBuddy/${app}`, ...(cli ? [`CLI/${cli}`] : [])].join(" ");
107
+ const h = {
108
+ "Content-Type": "application/json",
109
+ Accept: accept,
110
+ "Accept-Encoding": "identity",
111
+ "User-Agent": ua,
112
+ "X-IDE-Type": "WorkBuddy",
113
+ "X-IDE-Name": "WorkBuddy",
114
+ "X-IDE-Version": app,
115
+ "X-Private-Data": process.env.MSLXDFF_WORKBUDDY_PRIVATE_DATA || "false",
116
+ "X-Product": "SaaS",
117
+ "x-requested-with": "XMLHttpRequest",
118
+ "x-codebuddy-request": "1",
119
+ "X-Agent-Intent": "craft",
120
+ "X-Agent-Purpose": "conversation",
121
+ ...(withConversation ? conversationHeaders() : {}),
122
+ ...(withConversation ? traceHeaders() : {}),
123
+ };
124
+ if (key) h["Authorization"] = `Bearer ${key}`;
125
+ if (auth?.uid) h["X-User-Id"] = auth.uid;
126
+ h["X-Domain"] = auth?.domain || "www.codebuddy.cn";
127
+ if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
128
+ return h;
129
+ }
130
+
131
+ // GET /v3/config 专用:活体真值=本机 CLI 子进程(好号 a06ef5f8 正常工作中)2026-10-21 17:29 实发的 6 头集
132
+ //(客户端日志逐字:authorization/user-agent(WorkBuddy/5.8.3 WorkBuddy/5.8.3 CLI/2.161.0)/x-domain/x-product/x-request-id/x-user-id)。
133
+ // ⚠ config 的 x-request-id=`b1f4cb939a57486d86d16b74bec13511` 第13位='4'、第17位='8' → 是**去横线 UUIDv4**,
134
+ // 与 chat 的 UUIDv7 生成器不同(两条链路各自的 id 策略)。**无 Content-Type(GET)、无 X-IDE-*、无 trace、无 Origin/Referer**——
135
+ // 别拿 chat 全量头套它(2026-10-21 曾 over-align,按活体回退)。
136
+ export function buildWorkbuddyConfigHeaders(key, auth) {
137
+ const app = APP_VERSION(), cli = CLI_VERSION();
138
+ const ua = [`WorkBuddy/${app}`, `WorkBuddy/${app}`, ...(cli ? [`CLI/${cli}`] : [])].join(" ");
139
+ const h = {
140
+ "User-Agent": ua,
141
+ "X-Domain": auth?.domain || "www.codebuddy.cn",
142
+ "X-Product": "SaaS",
143
+ "X-Request-ID": uuidv4dashed().replace(/-/g, ""),
144
+ };
145
+ if (key) h["Authorization"] = `Bearer ${key}`;
146
+ if (auth?.uid) h["X-User-Id"] = auth.uid;
147
+ return h;
148
+ }
@@ -73,6 +73,22 @@ export function readDesktopAccount(fp) {
73
73
  accessToken: j.auth.accessToken,
74
74
  refreshToken: j.auth.refreshToken || "",
75
75
  expiresAt: exp,
76
- domain: j.auth.domain || "www.codebuddy.cn",
76
+ // domain 以 token 的 iss realm 为准(比 auth.domain 更贴近 cop 网关鉴权域);auth.domain 兜底。
77
+ // 注:从桌面 info 导入不等于能对话——2026-10-21 一台桌面态 token 对网关直连仍 401(疑似内嵌 CLI 与
78
+ // 桌面 Electron 双鉴权);入池正解是设备授权流 `-provider workbuddy login`,本函数只负责解析登录态字段。
79
+ domain: issDomain(j.auth.accessToken) || j.auth.domain || "www.codebuddy.cn",
77
80
  };
78
81
  }
82
+
83
+ // 从 JWT payload.iss(如 https://www.codebuddy.cn/auth/realms/copilot)提取主机名;解析失败返回 ""。
84
+ export function issDomain(token) {
85
+ try {
86
+ const payload = String(token || "").split(".")[1];
87
+ if (!payload) return "";
88
+ let b = payload.replace(/-/g, "+").replace(/_/g, "/");
89
+ while (b.length % 4) b += "=";
90
+ const iss = JSON.parse(Buffer.from(b, "base64").toString("utf8"))?.iss;
91
+ if (typeof iss !== "string") return "";
92
+ return new URL(iss).host || "";
93
+ } catch { return ""; }
94
+ }
@@ -1,5 +1,7 @@
1
1
  import { joinModelId } from "../model-id.js";
2
2
  import { joinUrl } from "../base.js";
3
+ import { isAccountRestricted } from "./auth.js";
4
+ import { buildWorkbuddyConfigHeaders } from "./client-headers.js";
3
5
 
4
6
  function creditsValue(c) {
5
7
  if (!c || !String(c).trim()) return 0;
@@ -28,16 +30,8 @@ export function createModelsService({
28
30
  const controller = new AbortController();
29
31
  const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
30
32
  try {
31
- const headers = {
32
- Accept: "application/json",
33
- "X-User-Id": useAuth?.uid || "",
34
- "X-Domain": useAuth?.domain || "www.codebuddy.cn",
35
- "X-Product": "SaaS",
36
- "User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
37
- Origin: "https://www.codebuddy.cn",
38
- Referer: "https://www.codebuddy.cn/",
39
- };
40
- if (useKey) headers["Authorization"] = `Bearer ${useKey}`;
33
+ // config 端点按活体真值只发 6 头(GET 简头通道,勿套 chat 全量头——2026-10-21 实测回退)。
34
+ const headers = buildWorkbuddyConfigHeaders(useKey, useAuth);
41
35
  const opts = { headers, signal: controller.signal };
42
36
  if (dispatcher) opts.dispatcher = dispatcher;
43
37
  return await fetchImpl(`${joinUrl(baseUrl, modelsPath)}`, opts);
@@ -65,6 +59,8 @@ export function createModelsService({
65
59
  if (!r.ok) {
66
60
  let t = "";
67
61
  try { t = await r.clone().text(); } catch {}
62
+ // 封号 11140:refresh 无效(判账号本体),跳过刷新直接换下一个 key,别白烧 refresh 端点。
63
+ if (isAccountRestricted(r.status, t)) continue;
68
64
  if (isAuthError?.(r.status, t)) {
69
65
  const newKey = await refreshTokenFor?.(key, auth);
70
66
  if (newKey && !tried.has(newKey)) {
@@ -98,16 +94,8 @@ export function createModelsService({
98
94
  try {
99
95
  const key = getKey ? getKey() : "";
100
96
  const auth = getAuth ? getAuth(key) : null;
101
- const headers = {
102
- Accept: "application/json",
103
- "X-User-Id": auth?.uid || "",
104
- "X-Domain": auth?.domain || "www.codebuddy.cn",
105
- "X-Product": "SaaS",
106
- "User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
107
- Origin: "https://www.codebuddy.cn",
108
- Referer: "https://www.codebuddy.cn/",
109
- };
110
- if (key) headers["Authorization"] = `Bearer ${key}`;
97
+ // 与 listModels 同一口径:config 简头集(活体真值)。
98
+ const headers = buildWorkbuddyConfigHeaders(key, auth);
111
99
  const opts = { headers };
112
100
  if (dispatcher) opts.dispatcher = dispatcher;
113
101
  const res = await fetchImpl(joinUrl(baseUrl, modelsPath), opts);
@@ -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
+ }
@@ -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
  }