mslxdff 0.1.231 → 0.1.233

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 (117) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -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 +155 -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/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  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 +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -1,160 +1,160 @@
1
- import { performance } from "node:perf_hooks";
2
- import { estimateChars, needsCompress } from "./store.js";
3
- import { CHAT_KEEP_RECENT, CHAT_MAX_TOOL_LOOPS } from "./config.js";
4
- import { buildDedupKey, runTool } from "./tool-handlers.js";
5
-
6
- // 纯引擎:无 readline/ANSI/spinner,单一 runTurn 可测
7
- export function createEngine({
8
- chatWithFallback,
9
- summarizeHistory,
10
- getToolDefs = () => [],
11
- execCommand,
12
- readFileTool,
13
- curlTool,
14
- onTrace = () => {},
15
- config = {},
16
- } = {}) {
17
- const CWF = chatWithFallback || (async () => ({ ok: false, error: "no chat" }));
18
- const SUM = summarizeHistory || (async () => null);
19
- const GTOOLS = getToolDefs;
20
- const EXEC = execCommand || (async () => ({ ok: false, output: "no exec" }));
21
- const READ = readFileTool || (async () => ({ ok: false, output: "no read" }));
22
- const CURL = curlTool || (async () => ({ ok: false, output: "no curl" }));
23
-
24
- async function maybeCompress(messages) {
25
- if (!needsCompress(messages)) return [...messages];
26
- const sys = messages[0];
27
- const rest = messages.slice(1);
28
- if (rest.length <= CHAT_KEEP_RECENT + 2) return [...messages];
29
- const t0 = performance.now();
30
- const toSummarize = rest.slice(0, -CHAT_KEEP_RECENT);
31
- const keep = rest.slice(-CHAT_KEEP_RECENT);
32
- const chars = estimateChars(messages);
33
- onTrace(`[压缩] 触发 ${chars}字 > 400000阈值 · 待压 ${toSummarize.length}条 保留 ${keep.length}条`);
34
- const summary = await SUM(toSummarize);
35
- const dt = Math.round(performance.now() - t0);
36
- if (!summary) {
37
- onTrace(`[压缩] 失败/空 · ${dt}ms → 截断`);
38
- return [sys, ...keep];
39
- }
40
- const summaryMsg = { role: "system", content: summary };
41
- const next = [sys, summaryMsg, ...keep];
42
- onTrace(`[压缩] 完成 ${toSummarize.length}条→${summary.length}字 · ${dt}ms · 新总量约 ${estimateChars(next)}字`);
43
- return next;
44
- }
45
-
46
- async function runTurn(userText, messages, modelOverride) {
47
- const tools = GTOOLS();
48
- messages.push({ role: "user", content: userText });
49
- let loops = 0;
50
- let lastModel = null;
51
- let lastProvider = null;
52
- let lastUsage = null;
53
- let lastFallback = false;
54
- let lastFallbackGateway = false;
55
- let lastLatency = 0;
56
- const t0 = performance.now();
57
- const turnStart = performance.now();
58
- const seenCalls = new Map();
59
- let duplicateStrikes = 0;
60
- let forceNoTools = false;
61
- onTrace(`[turn] 开始 "${userText.slice(0, 60)}${userText.length > 60 ? "…" : ""}" · 历史 ${messages.length}条 约 ${estimateChars(messages)}字`);
62
- while (loops < CHAT_MAX_TOOL_LOOPS) {
63
- const tLoop = performance.now();
64
- const tComp = performance.now();
65
- const cur = await maybeCompress(messages);
66
- const compressMs = Math.round(performance.now() - tComp);
67
- if (compressMs > 50) onTrace(`[loop ${loops}] 压缩耗时 ${compressMs}ms`);
68
- messages.length = 0;
69
- for (const m of cur) messages.push(m);
70
- const tCall = performance.now();
71
- let res;
72
- const activeTools = forceNoTools ? [] : tools;
73
- res = await CWF({ messages, tools: activeTools, model: modelOverride || undefined });
74
- if (forceNoTools && res.ok && res.message?.tool_calls?.length) {
75
- onTrace(`[guard] 禁工具模式下仍收到 tool_calls,已拦截`);
76
- res.message.tool_calls = [];
77
- if (!res.message.content) res.message.content = "(已拦截违规工具调用,请基于已有结果直接回答)";
78
- }
79
- const llmMs = Math.round(performance.now() - tCall);
80
- onTrace(`[loop ${loops}] LLM ${llmMs}ms${compressMs > 50 ? ` (含压缩 ${compressMs}ms)` : ""} · ${estimateChars(messages)}字上下文`);
81
- lastLatency = llmMs;
82
- if (!res.ok) {
83
- const err = `大模型暂不可用:${res.error}`;
84
- messages.push({ role: "assistant", content: err });
85
- return { text: err, model: null, latency: lastLatency, usage: null, fallback: false, ok: false };
86
- }
87
- lastModel = res.model;
88
- lastProvider = res.provider || null;
89
- lastUsage = res.usage || null;
90
- lastFallback = !!res.fallback;
91
- lastFallbackGateway = !!res.fallbackGateway || !!res.viaGateway;
92
- const msg = res.message;
93
- const toolCalls = msg.tool_calls || [];
94
- let fallbackCmd = null;
95
- if (!toolCalls.length && msg.content) {
96
- const m = String(msg.content).match(/\{[^}]*"command"\s*:\s*"([^"]+)"[^}]*\}/);
97
- if (m) fallbackCmd = m[1];
98
- }
99
- if (!toolCalls.length && !fallbackCmd) {
100
- const text = String(msg.content || "").trim() || "(空回复)";
101
- messages.push({ role: "assistant", content: text });
102
- const totalMs = Math.round(performance.now() - t0);
103
- let note = "";
104
- if (lastFallbackGateway) note = "\n[注:mimo/big-pickle 均不可用,已自动切本地网关 auto(:8989)]";
105
- else if (lastFallback) note = "\n[注:mimo 不可用,已用 big-pickle]";
106
- onTrace(`[turn] 完成 总计 ${totalMs}ms · LLM ${lastLatency}ms · 0 工具${lastFallbackGateway ? " · gateway-fallback" : ""}`);
107
- return { text: text + note, model: lastModel, provider: lastProvider, latency: lastLatency, usage: lastUsage, fallback: lastFallback, fallbackGateway: lastFallbackGateway, ok: true, totalMs };
108
- }
109
- const calls = toolCalls.length ? toolCalls : [{ id: "fallback-1", function: { name: "run_command", arguments: JSON.stringify({ command: fallbackCmd }) } }];
110
- messages.push({ role: "assistant", content: msg.content || "", tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: c.function })) });
111
- onTrace(`[tools] 本轮 ${calls.length} 个调用 ${calls.map((c) => c.function?.name).join(",")} · 顺序执行`);
112
- const tTools = performance.now();
113
- const toolResults = [];
114
- const toolDeps = { execCommand: EXEC, readFileTool: READ, curlTool: CURL, onTrace };
115
- for (const c of calls) {
116
- const name = c.function?.name;
117
- let args = {};
118
- try { args = JSON.parse(c.function?.arguments || "{}"); } catch {}
119
- const t1 = performance.now();
120
- const dedupKey = buildDedupKey(name, args);
121
- const seen = seenCalls.get(dedupKey);
122
- if (seen) {
123
- const dt = Math.round(performance.now() - t1);
124
- onTrace(`[tool] ${name} 重复调用已跳过 · ${dt}ms · 之前 ${seen.count} 次`);
125
- toolResults.push({
126
- id: c.id,
127
- content: `SKIPPED_DUP: 此工具调用在本轮已执行过 ${seen.count} 次,结果相同请直接基于已有信息回答用户,不要再重复调用。\n--- 首次结果复用 ---\n${seen.firstResult.slice(0, 6000)}`,
128
- });
129
- continue;
130
- }
131
- const result = await runTool({ name, args, userText, ...toolDeps });
132
- if (!seenCalls.has(dedupKey)) seenCalls.set(dedupKey, { count: 1, firstResult: result });
133
- else seenCalls.get(dedupKey).count++;
134
- toolResults.push({ id: c.id, content: result });
135
- }
136
- for (const tr of toolResults) messages.push({ role: "tool", tool_call_id: tr.id, content: tr.content });
137
- if (toolResults.some((tr) => String(tr.content).startsWith("SKIPPED_DUP"))) {
138
- duplicateStrikes++;
139
- forceNoTools = true;
140
- messages.push({ role: "system", content: "系统提示:你已重复调用相同工具,工具侧已复用首次结果并跳过执行。你已被禁止再调用任何工具,必须立即基于以上工具结果用中文直接回答用户,0 工具调用。" });
141
- onTrace(`[dup] 检测到重复调用 ${duplicateStrikes} 次,已禁用后续工具调用`);
142
- if (duplicateStrikes >= 2) {
143
- const seen = [...seenCalls.values()].map((v) => v.firstResult).join("\n---\n").slice(0, 6000);
144
- const synth = `检测到重复调用已达 ${duplicateStrikes} 次,为避免空转,直接基于已有结果回答:\n\n${seen}`;
145
- messages.push({ role: "assistant", content: synth });
146
- const totalMs = Math.round(performance.now() - t0);
147
- onTrace(`[turn] 提前结束(重复阈值) 总计 ${totalMs}ms`);
148
- return { text: synth, model: lastModel || "local", provider: lastProvider, latency: lastLatency, usage: lastUsage, fallback: lastFallback, ok: true, totalMs };
149
- }
150
- }
151
- const toolsMs = Math.round(performance.now() - tTools);
152
- const loopMs = Math.round(performance.now() - tLoop);
153
- onTrace(`[loop ${loops}] 工具 ${toolsMs}ms · 本轮总 ${loopMs}ms · 累计 ${Math.round(performance.now() - turnStart)}ms`);
154
- loops++;
155
- }
156
- return { text: "(工具调用次数已达上限,已停止)", model: lastModel, latency: lastLatency, usage: lastUsage, fallback: lastFallback, fallbackGateway: lastFallbackGateway, ok: false };
157
- }
158
-
159
- return { runTurn, maybeCompress };
160
- }
1
+ import { performance } from "node:perf_hooks";
2
+ import { estimateChars, needsCompress } from "./store.js";
3
+ import { CHAT_KEEP_RECENT, CHAT_MAX_TOOL_LOOPS } from "./config.js";
4
+ import { buildDedupKey, runTool } from "./tool-handlers.js";
5
+
6
+ // 纯引擎:无 readline/ANSI/spinner,单一 runTurn 可测
7
+ export function createEngine({
8
+ chatWithFallback,
9
+ summarizeHistory,
10
+ getToolDefs = () => [],
11
+ execCommand,
12
+ readFileTool,
13
+ curlTool,
14
+ onTrace = () => {},
15
+ config = {},
16
+ } = {}) {
17
+ const CWF = chatWithFallback || (async () => ({ ok: false, error: "no chat" }));
18
+ const SUM = summarizeHistory || (async () => null);
19
+ const GTOOLS = getToolDefs;
20
+ const EXEC = execCommand || (async () => ({ ok: false, output: "no exec" }));
21
+ const READ = readFileTool || (async () => ({ ok: false, output: "no read" }));
22
+ const CURL = curlTool || (async () => ({ ok: false, output: "no curl" }));
23
+
24
+ async function maybeCompress(messages) {
25
+ if (!needsCompress(messages)) return [...messages];
26
+ const sys = messages[0];
27
+ const rest = messages.slice(1);
28
+ if (rest.length <= CHAT_KEEP_RECENT + 2) return [...messages];
29
+ const t0 = performance.now();
30
+ const toSummarize = rest.slice(0, -CHAT_KEEP_RECENT);
31
+ const keep = rest.slice(-CHAT_KEEP_RECENT);
32
+ const chars = estimateChars(messages);
33
+ onTrace(`[压缩] 触发 ${chars}字 > 400000阈值 · 待压 ${toSummarize.length}条 保留 ${keep.length}条`);
34
+ const summary = await SUM(toSummarize);
35
+ const dt = Math.round(performance.now() - t0);
36
+ if (!summary) {
37
+ onTrace(`[压缩] 失败/空 · ${dt}ms → 截断`);
38
+ return [sys, ...keep];
39
+ }
40
+ const summaryMsg = { role: "system", content: summary };
41
+ const next = [sys, summaryMsg, ...keep];
42
+ onTrace(`[压缩] 完成 ${toSummarize.length}条→${summary.length}字 · ${dt}ms · 新总量约 ${estimateChars(next)}字`);
43
+ return next;
44
+ }
45
+
46
+ async function runTurn(userText, messages, modelOverride) {
47
+ const tools = GTOOLS();
48
+ messages.push({ role: "user", content: userText });
49
+ let loops = 0;
50
+ let lastModel = null;
51
+ let lastProvider = null;
52
+ let lastUsage = null;
53
+ let lastFallback = false;
54
+ let lastFallbackGateway = false;
55
+ let lastLatency = 0;
56
+ const t0 = performance.now();
57
+ const turnStart = performance.now();
58
+ const seenCalls = new Map();
59
+ let duplicateStrikes = 0;
60
+ let forceNoTools = false;
61
+ onTrace(`[turn] 开始 "${userText.slice(0, 60)}${userText.length > 60 ? "…" : ""}" · 历史 ${messages.length}条 约 ${estimateChars(messages)}字`);
62
+ while (loops < CHAT_MAX_TOOL_LOOPS) {
63
+ const tLoop = performance.now();
64
+ const tComp = performance.now();
65
+ const cur = await maybeCompress(messages);
66
+ const compressMs = Math.round(performance.now() - tComp);
67
+ if (compressMs > 50) onTrace(`[loop ${loops}] 压缩耗时 ${compressMs}ms`);
68
+ messages.length = 0;
69
+ for (const m of cur) messages.push(m);
70
+ const tCall = performance.now();
71
+ let res;
72
+ const activeTools = forceNoTools ? [] : tools;
73
+ res = await CWF({ messages, tools: activeTools, model: modelOverride || undefined });
74
+ if (forceNoTools && res.ok && res.message?.tool_calls?.length) {
75
+ onTrace(`[guard] 禁工具模式下仍收到 tool_calls,已拦截`);
76
+ res.message.tool_calls = [];
77
+ if (!res.message.content) res.message.content = "(已拦截违规工具调用,请基于已有结果直接回答)";
78
+ }
79
+ const llmMs = Math.round(performance.now() - tCall);
80
+ onTrace(`[loop ${loops}] LLM ${llmMs}ms${compressMs > 50 ? ` (含压缩 ${compressMs}ms)` : ""} · ${estimateChars(messages)}字上下文`);
81
+ lastLatency = llmMs;
82
+ if (!res.ok) {
83
+ const err = `大模型暂不可用:${res.error}`;
84
+ messages.push({ role: "assistant", content: err });
85
+ return { text: err, model: null, latency: lastLatency, usage: null, fallback: false, ok: false };
86
+ }
87
+ lastModel = res.model;
88
+ lastProvider = res.provider || null;
89
+ lastUsage = res.usage || null;
90
+ lastFallback = !!res.fallback;
91
+ lastFallbackGateway = !!res.fallbackGateway || !!res.viaGateway;
92
+ const msg = res.message;
93
+ const toolCalls = msg.tool_calls || [];
94
+ let fallbackCmd = null;
95
+ if (!toolCalls.length && msg.content) {
96
+ const m = String(msg.content).match(/\{[^}]*"command"\s*:\s*"([^"]+)"[^}]*\}/);
97
+ if (m) fallbackCmd = m[1];
98
+ }
99
+ if (!toolCalls.length && !fallbackCmd) {
100
+ const text = String(msg.content || "").trim() || "(空回复)";
101
+ messages.push({ role: "assistant", content: text });
102
+ const totalMs = Math.round(performance.now() - t0);
103
+ let note = "";
104
+ if (lastFallbackGateway) note = "\n[注:mimo/big-pickle 均不可用,已自动切本地网关 auto(:8989)]";
105
+ else if (lastFallback) note = "\n[注:mimo 不可用,已用 big-pickle]";
106
+ onTrace(`[turn] 完成 总计 ${totalMs}ms · LLM ${lastLatency}ms · 0 工具${lastFallbackGateway ? " · gateway-fallback" : ""}`);
107
+ return { text: text + note, model: lastModel, provider: lastProvider, latency: lastLatency, usage: lastUsage, fallback: lastFallback, fallbackGateway: lastFallbackGateway, ok: true, totalMs };
108
+ }
109
+ const calls = toolCalls.length ? toolCalls : [{ id: "fallback-1", function: { name: "run_command", arguments: JSON.stringify({ command: fallbackCmd }) } }];
110
+ messages.push({ role: "assistant", content: msg.content || "", tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: c.function })) });
111
+ onTrace(`[tools] 本轮 ${calls.length} 个调用 ${calls.map((c) => c.function?.name).join(",")} · 顺序执行`);
112
+ const tTools = performance.now();
113
+ const toolResults = [];
114
+ const toolDeps = { execCommand: EXEC, readFileTool: READ, curlTool: CURL, onTrace };
115
+ for (const c of calls) {
116
+ const name = c.function?.name;
117
+ let args = {};
118
+ try { args = JSON.parse(c.function?.arguments || "{}"); } catch {}
119
+ const t1 = performance.now();
120
+ const dedupKey = buildDedupKey(name, args);
121
+ const seen = seenCalls.get(dedupKey);
122
+ if (seen) {
123
+ const dt = Math.round(performance.now() - t1);
124
+ onTrace(`[tool] ${name} 重复调用已跳过 · ${dt}ms · 之前 ${seen.count} 次`);
125
+ toolResults.push({
126
+ id: c.id,
127
+ content: `SKIPPED_DUP: 此工具调用在本轮已执行过 ${seen.count} 次,结果相同请直接基于已有信息回答用户,不要再重复调用。\n--- 首次结果复用 ---\n${seen.firstResult.slice(0, 6000)}`,
128
+ });
129
+ continue;
130
+ }
131
+ const result = await runTool({ name, args, userText, ...toolDeps });
132
+ if (!seenCalls.has(dedupKey)) seenCalls.set(dedupKey, { count: 1, firstResult: result });
133
+ else seenCalls.get(dedupKey).count++;
134
+ toolResults.push({ id: c.id, content: result });
135
+ }
136
+ for (const tr of toolResults) messages.push({ role: "tool", tool_call_id: tr.id, content: tr.content });
137
+ if (toolResults.some((tr) => String(tr.content).startsWith("SKIPPED_DUP"))) {
138
+ duplicateStrikes++;
139
+ forceNoTools = true;
140
+ messages.push({ role: "system", content: "系统提示:你已重复调用相同工具,工具侧已复用首次结果并跳过执行。你已被禁止再调用任何工具,必须立即基于以上工具结果用中文直接回答用户,0 工具调用。" });
141
+ onTrace(`[dup] 检测到重复调用 ${duplicateStrikes} 次,已禁用后续工具调用`);
142
+ if (duplicateStrikes >= 2) {
143
+ const seen = [...seenCalls.values()].map((v) => v.firstResult).join("\n---\n").slice(0, 6000);
144
+ const synth = `检测到重复调用已达 ${duplicateStrikes} 次,为避免空转,直接基于已有结果回答:\n\n${seen}`;
145
+ messages.push({ role: "assistant", content: synth });
146
+ const totalMs = Math.round(performance.now() - t0);
147
+ onTrace(`[turn] 提前结束(重复阈值) 总计 ${totalMs}ms`);
148
+ return { text: synth, model: lastModel || "local", provider: lastProvider, latency: lastLatency, usage: lastUsage, fallback: lastFallback, ok: true, totalMs };
149
+ }
150
+ }
151
+ const toolsMs = Math.round(performance.now() - tTools);
152
+ const loopMs = Math.round(performance.now() - tLoop);
153
+ onTrace(`[loop ${loops}] 工具 ${toolsMs}ms · 本轮总 ${loopMs}ms · 累计 ${Math.round(performance.now() - turnStart)}ms`);
154
+ loops++;
155
+ }
156
+ return { text: "(工具调用次数已达上限,已停止)", model: lastModel, latency: lastLatency, usage: lastUsage, fallback: lastFallback, fallbackGateway: lastFallbackGateway, ok: false };
157
+ }
158
+
159
+ return { runTurn, maybeCompress };
160
+ }