mslxdff 0.1.116 → 0.1.118

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.116",
3
+ "version": "0.1.118",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,7 @@ import { appendRotationLog as defaultAppend } from "./rotation-log.js";
4
4
  import { createTransport } from "../../transport/index.js";
5
5
  import { reshapeWorkbuddySse } from "./reshape.js";
6
6
  import { attemptOnceSdk } from "./sdk-chat.js";
7
+ import { sanitizeToolSequence } from "./sanitize-tools.js";
7
8
  import { resolveEngineMode } from "../../upstream-engine/mode.js";
8
9
 
9
10
  function buildAuthHeaders(key, auth) {
@@ -35,6 +36,19 @@ function withUidHeader(res, uid) {
35
36
  }
36
37
  }
37
38
 
39
+ // 400 判型需读 body 文本;读过的 Response body 不可再读(下游转发报 "Body is unusable"),
40
+ // 用文本重建一个等价响应(去掉 content-length 防长度不符)。
41
+ function rewrapBody(res, txt) {
42
+ try { console.error(`[workbuddy] upstream ${res.status} body: ${String(txt).slice(0, 300)}`); } catch {}
43
+ try {
44
+ const h = new Headers(res.headers);
45
+ h.delete("content-length");
46
+ const r = new Response(txt, { status: res.status, statusText: res.statusText, headers: h });
47
+ try { r._t = res._t; } catch {}
48
+ return r;
49
+ } catch { return res; }
50
+ }
51
+
38
52
  function nowMs(clock) { return typeof performance !== "undefined" && performance.now ? performance.now() : clock(); }
39
53
 
40
54
  function errRes(status, msg, reason, uid, t0, clock) {
@@ -109,7 +123,7 @@ export function createChatService({
109
123
  if (res.status < 400) return reshapeWorkbuddySse(res);
110
124
  let txt = "";
111
125
  try { txt = await res.text(); } catch {}
112
- if (!isAuthError(res.status, txt)) return res;
126
+ if (!isAuthError(res.status, txt)) return rewrapBody(res, txt);
113
127
  const newKey = await authService?.refreshTokenFor?.(key, auth);
114
128
  if (!newKey) throw failErr(`workbuddy refresh failed for ${auth?.uid || ""}: ${txt.slice(0, 120)}`, _t0(), clock);
115
129
  const auth2 = authForKey(newKey);
@@ -118,6 +132,7 @@ export function createChatService({
118
132
  catch (e2) { throw e2; }
119
133
  let stillTxt = "";
120
134
  if (res2.status >= 400) { try { stillTxt = await res2.text(); } catch {} }
135
+ if (stillTxt) res2 = rewrapBody(res2, stillTxt);
121
136
  const stillAuth = isAuthError(res2.status, stillTxt);
122
137
  const uid2 = auth2.uid || auth.uid;
123
138
  res2 = withUidHeader(res2, uid2);
@@ -136,6 +151,11 @@ export function createChatService({
136
151
  async function runChat(body, activeRing, opts = {}) {
137
152
  const url = joinUrl(baseUrl, chatPath);
138
153
  const t0 = nowMs(clock);
154
+ const clean = sanitizeToolSequence(body?.messages);
155
+ if (clean.droppedCalls || clean.droppedResults) {
156
+ try { console.error(`[workbuddy] tool-sequence sanitized: dropped ${clean.droppedCalls} call(s), ${clean.droppedResults} result(s), model=${body?.model || ""}`); } catch {}
157
+ body = { ...body, messages: clean.messages };
158
+ }
139
159
  _t0Val = t0;
140
160
  _modelVal = body?.model || "";
141
161
  const preferredUid = opts?.workbuddyUid ? String(opts.workbuddyUid).trim() : "";
@@ -203,6 +223,7 @@ export function createChatService({
203
223
  lastErr = failErr(`workbuddy insufficient for ${uid}: ${txt.slice(0, 120)}`, t0, clock);
204
224
  continue;
205
225
  }
226
+ res = rewrapBody(res, txt);
206
227
  }
207
228
  if (res.status === 401 || res.status === 403 || res.status === 429 || res.status >= 500) try { activeRing.onError(key); } catch {}
208
229
  res = withUidHeader(res, uid);
@@ -0,0 +1,55 @@
1
+ // workbuddy 上游对 tool 序列严格校验(400 code 11148 tool_call_sequence_broken):
2
+ // assistant.tool_calls 与后续 tool 结果必须按 id 一一配对,否则整单拒绝并提示开新会话。
3
+ // 历史上下文经压缩/编辑/跨模型切换后可能断裂(有调用无结果、结果孤立、id 漂移),
4
+ // AI SDK 转换层对缺失字段用占位(toolName "tool" / toolCallId "")进一步错配。
5
+ // 这里在 workbuddy 出口做防御性清洗:仅保留能成对出现的调用与结果,避免整单 400。
6
+ // 其他上游(opencode 等)宽容,不受影响——只挂在 workbuddy 链路。
7
+ export function sanitizeToolSequence(messages) {
8
+ const list = Array.isArray(messages) ? messages : [];
9
+
10
+ const callIds = new Set();
11
+ for (const m of list) {
12
+ if (m?.role === "assistant" && Array.isArray(m.tool_calls)) {
13
+ for (const tc of m.tool_calls) {
14
+ const id = String(tc?.id || "");
15
+ if (id) callIds.add(id);
16
+ }
17
+ }
18
+ }
19
+
20
+ const resultIds = new Set();
21
+ for (const m of list) {
22
+ if (m?.role === "tool") {
23
+ const id = String(m.tool_call_id || "");
24
+ if (id) resultIds.add(id);
25
+ }
26
+ }
27
+
28
+ const paired = new Set();
29
+ for (const id of callIds) if (resultIds.has(id)) paired.add(id);
30
+
31
+ let droppedCalls = 0;
32
+ let droppedResults = 0;
33
+ const out = [];
34
+ for (const m of list) {
35
+ if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length) {
36
+ const kept = m.tool_calls.filter((tc) => paired.has(String(tc?.id || "")));
37
+ droppedCalls += m.tool_calls.length - kept.length;
38
+ if (kept.length) {
39
+ out.push({ ...m, tool_calls: kept });
40
+ } else if (typeof m.content === "string" && m.content) {
41
+ const { tool_calls: _dropped, ...rest } = m;
42
+ out.push(rest);
43
+ }
44
+ continue;
45
+ }
46
+ if (m?.role === "tool") {
47
+ if (paired.has(String(m.tool_call_id || ""))) out.push(m);
48
+ else droppedResults++;
49
+ continue;
50
+ }
51
+ out.push(m);
52
+ }
53
+
54
+ return { messages: out, droppedCalls, droppedResults };
55
+ }