@huyooo/ai-chat-core 0.2.45 → 0.3.3

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 (247) hide show
  1. package/dist/adapter/index.d.ts +11 -0
  2. package/dist/adapter/index.d.ts.map +1 -0
  3. package/dist/adapter/model-adapter.d.ts +25 -0
  4. package/dist/adapter/model-adapter.d.ts.map +1 -0
  5. package/dist/adapter/model-options.d.ts +53 -0
  6. package/dist/adapter/model-options.d.ts.map +1 -0
  7. package/dist/adapter/types.d.ts +28 -0
  8. package/dist/adapter/types.d.ts.map +1 -0
  9. package/dist/chat-runtime.d.ts +96 -0
  10. package/dist/chat-runtime.d.ts.map +1 -0
  11. package/dist/constants.d.ts +12 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/events.d.ts +605 -1
  14. package/dist/events.d.ts.map +1 -0
  15. package/dist/events.js +1 -1
  16. package/dist/extension/index.d.ts +9 -0
  17. package/dist/extension/index.d.ts.map +1 -0
  18. package/dist/extension/types.d.ts +46 -0
  19. package/dist/extension/types.d.ts.map +1 -0
  20. package/dist/families/index.d.ts +11 -0
  21. package/dist/families/index.d.ts.map +1 -0
  22. package/dist/families/presets.d.ts +31 -0
  23. package/dist/families/presets.d.ts.map +1 -0
  24. package/dist/families/resolver.d.ts +11 -0
  25. package/dist/families/resolver.d.ts.map +1 -0
  26. package/dist/families/types.d.ts +29 -0
  27. package/dist/families/types.d.ts.map +1 -0
  28. package/dist/governance/command-safety.d.ts +34 -0
  29. package/dist/governance/command-safety.d.ts.map +1 -0
  30. package/dist/governance/governance.d.ts +19 -0
  31. package/dist/governance/governance.d.ts.map +1 -0
  32. package/dist/governance/index.d.ts +12 -0
  33. package/dist/governance/index.d.ts.map +1 -0
  34. package/dist/governance/types.d.ts +29 -0
  35. package/dist/governance/types.d.ts.map +1 -0
  36. package/dist/index.d.ts +72 -804
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +51 -1
  39. package/dist/internal/management-args.d.ts +13 -0
  40. package/dist/internal/management-args.d.ts.map +1 -0
  41. package/dist/internal/management-results.d.ts +21 -0
  42. package/dist/internal/management-results.d.ts.map +1 -0
  43. package/dist/llm-config.d.ts +108 -0
  44. package/dist/llm-config.d.ts.map +1 -0
  45. package/dist/logger/core.d.ts +31 -0
  46. package/dist/logger/core.d.ts.map +1 -0
  47. package/dist/logger/index.d.ts +9 -0
  48. package/dist/logger/index.d.ts.map +1 -0
  49. package/dist/orchestrator/compression-handler.d.ts +29 -0
  50. package/dist/orchestrator/compression-handler.d.ts.map +1 -0
  51. package/dist/orchestrator/context-compressor.d.ts +51 -0
  52. package/dist/orchestrator/context-compressor.d.ts.map +1 -0
  53. package/dist/orchestrator/context-summarizer.d.ts +41 -0
  54. package/dist/orchestrator/context-summarizer.d.ts.map +1 -0
  55. package/dist/orchestrator/index.d.ts +12 -0
  56. package/dist/orchestrator/index.d.ts.map +1 -0
  57. package/dist/orchestrator/orchestrator.d.ts +46 -0
  58. package/dist/orchestrator/orchestrator.d.ts.map +1 -0
  59. package/dist/orchestrator/types.d.ts +58 -0
  60. package/dist/orchestrator/types.d.ts.map +1 -0
  61. package/dist/parts/index.d.ts +13 -0
  62. package/dist/parts/index.d.ts.map +1 -0
  63. package/dist/parts/registry.d.ts +11 -0
  64. package/dist/parts/registry.d.ts.map +1 -0
  65. package/dist/parts/summaries.d.ts +9 -0
  66. package/dist/parts/summaries.d.ts.map +1 -0
  67. package/dist/parts/types.d.ts +61 -0
  68. package/dist/parts/types.d.ts.map +1 -0
  69. package/dist/platform.d.ts +17 -0
  70. package/dist/platform.d.ts.map +1 -0
  71. package/dist/platform.js +1 -0
  72. package/dist/protocols/anthropic.d.ts +20 -0
  73. package/dist/protocols/anthropic.d.ts.map +1 -0
  74. package/dist/protocols/ark.d.ts +36 -0
  75. package/dist/protocols/ark.d.ts.map +1 -0
  76. package/dist/protocols/deepseek.d.ts +24 -0
  77. package/dist/protocols/deepseek.d.ts.map +1 -0
  78. package/dist/protocols/error-utils.d.ts +14 -0
  79. package/dist/protocols/error-utils.d.ts.map +1 -0
  80. package/dist/protocols/gemini.d.ts +24 -0
  81. package/dist/protocols/gemini.d.ts.map +1 -0
  82. package/dist/protocols/glm.d.ts +20 -0
  83. package/dist/protocols/glm.d.ts.map +1 -0
  84. package/dist/protocols/grok.d.ts +20 -0
  85. package/dist/protocols/grok.d.ts.map +1 -0
  86. package/dist/protocols/index.d.ts +31 -0
  87. package/dist/protocols/index.d.ts.map +1 -0
  88. package/dist/protocols/minimax.d.ts +38 -0
  89. package/dist/protocols/minimax.d.ts.map +1 -0
  90. package/dist/protocols/moonshot.d.ts +20 -0
  91. package/dist/protocols/moonshot.d.ts.map +1 -0
  92. package/dist/protocols/openai-sse.d.ts +33 -0
  93. package/dist/protocols/openai-sse.d.ts.map +1 -0
  94. package/dist/protocols/openai.d.ts +19 -0
  95. package/dist/protocols/openai.d.ts.map +1 -0
  96. package/dist/protocols/qwen.d.ts +26 -0
  97. package/dist/protocols/qwen.d.ts.map +1 -0
  98. package/dist/protocols/responses-sse.d.ts +30 -0
  99. package/dist/protocols/responses-sse.d.ts.map +1 -0
  100. package/dist/protocols/sse-reader.d.ts +23 -0
  101. package/dist/protocols/sse-reader.d.ts.map +1 -0
  102. package/dist/protocols/tool-arguments.d.ts +8 -0
  103. package/dist/protocols/tool-arguments.d.ts.map +1 -0
  104. package/dist/protocols/types.d.ts +148 -0
  105. package/dist/protocols/types.d.ts.map +1 -0
  106. package/dist/protocols/vercel-gateway.d.ts +15 -0
  107. package/dist/protocols/vercel-gateway.d.ts.map +1 -0
  108. package/dist/runtime.d.ts +151 -0
  109. package/dist/runtime.d.ts.map +1 -0
  110. package/dist/runtime.js +1 -0
  111. package/dist/skills/index.d.ts +14 -0
  112. package/dist/skills/index.d.ts.map +1 -0
  113. package/dist/skills/management/admin.d.ts +10 -0
  114. package/dist/skills/management/admin.d.ts.map +1 -0
  115. package/dist/skills/management/index.d.ts +11 -0
  116. package/dist/skills/management/index.d.ts.map +1 -0
  117. package/dist/skills/management/inputs.d.ts +44 -0
  118. package/dist/skills/management/inputs.d.ts.map +1 -0
  119. package/dist/skills/management/operations.d.ts +78 -0
  120. package/dist/skills/management/operations.d.ts.map +1 -0
  121. package/dist/skills/management/types.d.ts +70 -0
  122. package/dist/skills/management/types.d.ts.map +1 -0
  123. package/dist/skills/registry.d.ts +37 -0
  124. package/dist/skills/registry.d.ts.map +1 -0
  125. package/dist/skills/summaries.d.ts +9 -0
  126. package/dist/skills/summaries.d.ts.map +1 -0
  127. package/dist/skills/types.d.ts +61 -0
  128. package/dist/skills/types.d.ts.map +1 -0
  129. package/dist/test-utils/mock-sse.d.ts +13 -0
  130. package/dist/test-utils/mock-sse.d.ts.map +1 -0
  131. package/dist/tool-manager/define-tool.d.ts +35 -0
  132. package/dist/tool-manager/define-tool.d.ts.map +1 -0
  133. package/dist/tool-manager/formats.d.ts +46 -0
  134. package/dist/tool-manager/formats.d.ts.map +1 -0
  135. package/dist/tool-manager/identity.d.ts +18 -0
  136. package/dist/tool-manager/identity.d.ts.map +1 -0
  137. package/dist/tool-manager/in-process-provider.d.ts +15 -0
  138. package/dist/tool-manager/in-process-provider.d.ts.map +1 -0
  139. package/dist/tool-manager/index.d.ts +18 -0
  140. package/dist/tool-manager/index.d.ts.map +1 -0
  141. package/dist/tool-manager/manager.d.ts +18 -0
  142. package/dist/tool-manager/manager.d.ts.map +1 -0
  143. package/dist/tool-manager/mcp-provider.d.ts +21 -0
  144. package/dist/tool-manager/mcp-provider.d.ts.map +1 -0
  145. package/dist/tool-manager/summaries.d.ts +39 -0
  146. package/dist/tool-manager/summaries.d.ts.map +1 -0
  147. package/dist/tool-manager/types.d.ts +314 -0
  148. package/dist/tool-manager/types.d.ts.map +1 -0
  149. package/dist/types.d.ts +663 -0
  150. package/dist/types.d.ts.map +1 -0
  151. package/package.json +26 -15
  152. package/src/adapter/index.ts +25 -0
  153. package/src/adapter/model-adapter.ts +196 -0
  154. package/src/adapter/model-options.ts +143 -0
  155. package/src/adapter/types.ts +41 -0
  156. package/src/chat-runtime.ts +515 -0
  157. package/src/constants.ts +9 -102
  158. package/src/events.ts +364 -150
  159. package/src/extension/index.ts +24 -0
  160. package/src/extension/types.ts +49 -0
  161. package/src/families/index.ts +28 -0
  162. package/src/families/presets.ts +124 -0
  163. package/src/families/resolver.ts +22 -0
  164. package/src/families/types.ts +55 -0
  165. package/src/governance/command-safety.ts +224 -0
  166. package/src/governance/governance.ts +125 -0
  167. package/src/governance/index.ts +38 -0
  168. package/src/governance/types.ts +44 -0
  169. package/src/index.ts +250 -145
  170. package/src/internal/management-args.ts +39 -0
  171. package/src/internal/management-results.ts +60 -0
  172. package/src/llm-config.ts +137 -0
  173. package/src/logger/core.ts +96 -0
  174. package/src/logger/index.ts +8 -0
  175. package/src/orchestrator/compression-handler.ts +137 -0
  176. package/src/{providers → orchestrator}/context-compressor.ts +79 -47
  177. package/src/orchestrator/context-summarizer.ts +123 -0
  178. package/src/orchestrator/index.ts +20 -0
  179. package/src/orchestrator/orchestrator.ts +1002 -0
  180. package/src/orchestrator/types.ts +70 -0
  181. package/src/parts/index.ts +20 -0
  182. package/src/parts/registry.ts +95 -0
  183. package/src/parts/summaries.ts +40 -0
  184. package/src/parts/types.ts +63 -0
  185. package/src/platform.ts +73 -0
  186. package/src/protocols/anthropic.ts +377 -0
  187. package/src/protocols/ark.ts +300 -0
  188. package/src/protocols/deepseek.ts +192 -0
  189. package/src/{providers/protocols → protocols}/error-utils.ts +17 -20
  190. package/src/protocols/gemini.ts +352 -0
  191. package/src/protocols/glm.ts +212 -0
  192. package/src/protocols/grok.ts +98 -0
  193. package/src/protocols/index.ts +48 -0
  194. package/src/protocols/minimax.ts +308 -0
  195. package/src/protocols/moonshot.ts +186 -0
  196. package/src/protocols/openai-sse.ts +156 -0
  197. package/src/protocols/openai.ts +97 -0
  198. package/src/protocols/qwen.ts +358 -0
  199. package/src/protocols/responses-sse.ts +224 -0
  200. package/src/protocols/sse-reader.ts +54 -0
  201. package/src/protocols/tool-arguments.ts +32 -0
  202. package/src/{providers/protocols → protocols}/types.ts +46 -37
  203. package/src/protocols/vercel-gateway.ts +391 -0
  204. package/src/runtime.ts +167 -0
  205. package/src/skills/index.ts +29 -0
  206. package/src/skills/management/admin.ts +170 -0
  207. package/src/skills/management/index.ts +27 -0
  208. package/src/skills/management/inputs.ts +79 -0
  209. package/src/skills/management/operations.ts +256 -0
  210. package/src/skills/management/types.ts +57 -0
  211. package/src/skills/registry.ts +120 -0
  212. package/src/skills/summaries.ts +48 -0
  213. package/src/skills/types.ts +65 -0
  214. package/src/test-utils/mock-sse.ts +3 -3
  215. package/src/tool-manager/define-tool.ts +201 -0
  216. package/src/tool-manager/formats.ts +146 -0
  217. package/src/tool-manager/identity.ts +80 -0
  218. package/src/tool-manager/in-process-provider.ts +164 -0
  219. package/src/tool-manager/index.ts +63 -0
  220. package/src/tool-manager/manager.ts +562 -0
  221. package/src/tool-manager/mcp-provider.ts +509 -0
  222. package/src/tool-manager/summaries.ts +136 -0
  223. package/src/tool-manager/types.ts +389 -0
  224. package/src/types.ts +750 -191
  225. package/dist/events-CU5D5ray.d.ts +0 -1128
  226. package/src/agent.ts +0 -409
  227. package/src/internal/update-plan.ts +0 -2
  228. package/src/internal/web-search.ts +0 -77
  229. package/src/mcp/client-manager.ts +0 -302
  230. package/src/mcp/index.ts +0 -2
  231. package/src/mcp/types.ts +0 -43
  232. package/src/providers/context-summarizer.ts +0 -70
  233. package/src/providers/index.ts +0 -125
  234. package/src/providers/model-registry.ts +0 -466
  235. package/src/providers/orchestrator.ts +0 -839
  236. package/src/providers/protocols/anthropic.ts +0 -406
  237. package/src/providers/protocols/ark.ts +0 -362
  238. package/src/providers/protocols/deepseek.ts +0 -344
  239. package/src/providers/protocols/gemini.ts +0 -350
  240. package/src/providers/protocols/index.ts +0 -36
  241. package/src/providers/protocols/openai.ts +0 -420
  242. package/src/providers/protocols/qwen.ts +0 -315
  243. package/src/providers/types.ts +0 -264
  244. package/src/providers/unified-adapter.ts +0 -367
  245. package/src/router.ts +0 -72
  246. package/src/tools.ts +0 -162
  247. package/src/utils.ts +0 -86
@@ -1,839 +0,0 @@
1
- /**
2
- * Chat Orchestrator
3
- *
4
- * 统一处理:
5
- * 1. 工具调用循环 (while iterations < MAX)
6
- * 2. 消息历史维护 (assistant + tool 消息)
7
- * 3. 事件发射 (ChatEvent)
8
- * 4. 错误处理
9
- *
10
- * 所有 Provider 通过此类执行,确保行为一致
11
- */
12
-
13
- import type { ChatEvent } from '../events';
14
- import type { ChatMessage, ToolDefinition } from '../types';
15
- import { isToolError } from '../types';
16
- import type {
17
- ProviderAdapter,
18
- StandardMessage,
19
- StreamChunk,
20
- ToolCallRequest,
21
- OrchestratorConfig,
22
- OrchestratorContext,
23
- OrchestratorOptions,
24
- SearchResultItem,
25
- AutoRunConfig,
26
- } from './types';
27
- import {
28
- createTextDelta,
29
- createThinkingStart,
30
- createThinkingDelta,
31
- createThinkingEnd,
32
- createSearchStart,
33
- createSearchResult,
34
- createSearchEnd,
35
- createToolCallStart,
36
- createToolCallOutput,
37
- createToolCallResult,
38
- createToolCallRequest,
39
- createDone,
40
- createAbort,
41
- createApiError,
42
- createStepStart,
43
- createStepEnd,
44
- createAgentStatus,
45
- createCompactStart,
46
- createCompactEnd,
47
- } from '../events';
48
- import type { ToolApprovalRequestEvent, CompactStartEvent, CompactEndEvent } from '../events';
49
- import { DebugLogger } from '../utils';
50
- import {
51
- needsCompaction,
52
- estimateTotalTokens,
53
- getPromptBudget,
54
- type CompactConfig,
55
- } from './context-compressor';
56
- import { summarizeHistory } from './context-summarizer';
57
- import { getModelContextConfig } from './model-registry';
58
- import { WEB_SEARCH_TOOL_NAME } from '../internal/web-search';
59
-
60
- // 创建模块专用 logger
61
- const logger = DebugLogger.module('Orchestrator');
62
-
63
-
64
- function normalizeSearchResults(items: SearchResultItem[]): SearchResultItem[] {
65
- const byUrl = new Map<string, SearchResultItem>();
66
- for (const it of items) {
67
- const url = String(it?.url ?? '').trim();
68
- if (!url) continue;
69
-
70
- const snippet = String(it?.snippet ?? '').trim();
71
- let title = String(it?.title ?? '').trim();
72
- if (!title) {
73
- try {
74
- title = new URL(url).hostname;
75
- } catch {
76
- title = url;
77
- }
78
- }
79
-
80
- const prev = byUrl.get(url);
81
- if (!prev) {
82
- byUrl.set(url, { title, url, snippet });
83
- continue;
84
- }
85
- if (!prev.snippet && snippet) prev.snippet = snippet;
86
- if (!prev.title && title) prev.title = title;
87
- }
88
- return Array.from(byUrl.values());
89
- }
90
-
91
- /**
92
- * 默认最大迭代次数(安全兜底)
93
- *
94
- * 不是正常终止条件——模型不调工具时循环自然 break。
95
- *
96
- * 正常终止由以下机制保证:
97
- * 1. 模型不再调工具 → break
98
- * 2. 用户取消 → abort signal
99
- * 3. context window 超限 → API 报错 → error handler
100
- * 4. 重复工具调用检测 → 注入提示终止循环
101
- *
102
- * 设为 25,作为异常情况的安全兜底(正常任务极少超过 10 轮)。
103
- * 调用方可通过 OrchestratorConfig.maxIterations 覆盖。
104
- */
105
- const DEFAULT_MAX_ITERATIONS = 25;
106
-
107
- /**
108
- * 重复工具调用检测阈值
109
- *
110
- * 同名同参数的工具连续调用超过此次数时,注入提示让模型停止循环。
111
- */
112
- const DUPLICATE_TOOL_CALL_THRESHOLD = 2;
113
-
114
- /**
115
- * Chat Orchestrator
116
- *
117
- * 核心职责:统一处理工具调用循环,所有 Provider 共享相同的逻辑
118
- */
119
- export class ChatOrchestrator {
120
- private config: OrchestratorConfig;
121
-
122
- constructor(config: OrchestratorConfig) {
123
- this.config = {
124
- maxIterations: DEFAULT_MAX_ITERATIONS,
125
- ...config,
126
- };
127
- }
128
-
129
- /**
130
- * 执行聊天
131
- *
132
- * @param adapter Provider 适配器
133
- * @param message 用户消息
134
- * @param context Orchestrator 上下文
135
- * @param options 选项
136
- */
137
- async *chat(
138
- adapter: ProviderAdapter,
139
- message: string,
140
- context: OrchestratorContext,
141
- options: OrchestratorOptions
142
- ): AsyncGenerator<ChatEvent> {
143
- const startedAt = Date.now();
144
- const maxIterations = this.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
145
-
146
- // 构建标准化消息列表
147
- let messages = this.buildMessages(context, message);
148
-
149
- // 根据模型注册表获取 context 配置(模型必须已注册,否则抛错)
150
- const { contextWindowTokens, maxOutputTokens } = getModelContextConfig(options.model);
151
- const compactConfig: CompactConfig = { contextWindowTokens, maxOutputTokens };
152
-
153
- // 初始压缩:前端传入的历史可能就已经超长
154
- {
155
- const gen = this.compactIfNeeded(messages, compactConfig);
156
- let step = await gen.next();
157
- while (!step.done) {
158
- yield step.value;
159
- step = await gen.next();
160
- }
161
- messages = step.value;
162
- }
163
-
164
- // 全局状态
165
- let iterations = 0;
166
- let finalText = '';
167
- let searchResults: SearchResultItem[] = [];
168
- let searchStartedAt = 0;
169
- let searchStarted = false;
170
- let searchEnded = false;
171
-
172
- // 重复工具调用检测:记录上一轮的工具调用签名(name + args hash)
173
- let lastToolCallSignature = '';
174
- let duplicateToolCallCount = 0;
175
-
176
- // Token 使用累积(多轮工具调用时累加)
177
- let totalUsage: { promptTokens: number; completionTokens: number; totalTokens: number; reasoningTokens: number; cachedTokens: number } = {
178
- promptTokens: 0, completionTokens: 0, totalTokens: 0, reasoningTokens: 0, cachedTokens: 0,
179
- };
180
- let hasUsage = false;
181
-
182
- // 工具调用循环
183
- while (iterations < maxIterations) {
184
- if (context.signal.aborted) {
185
- yield createAbort('请求已取消');
186
- return;
187
- }
188
-
189
- iterations++;
190
-
191
- // 每轮迭代前检查是否需要压缩
192
- if (iterations > 1) {
193
- const gen = this.compactIfNeeded(messages, compactConfig);
194
- let step = await gen.next();
195
- while (!step.done) {
196
- yield step.value;
197
- step = await gen.next();
198
- }
199
- messages = step.value;
200
- }
201
-
202
- const stepStartedAt = Date.now();
203
- logger.info(`======= 第 ${iterations} 轮开始 =======`);
204
-
205
- // 发射步骤开始事件(前端可用于展示 Agent 执行进度)
206
- yield createStepStart(iterations);
207
- // 告知前端当前阶段:模型正在思考(首次请求 / 工具执行完后新一轮)
208
- yield createAgentStatus('thinking');
209
-
210
- // ========== 每轮独立的状态 ==========
211
- let thinkingText = '';
212
- let thinkingStartedAt = 0;
213
- let thinkingStarted = false;
214
- let thinkingComplete = false;
215
-
216
- // ========== 调试日志:chunk 计数 ==========
217
- const chunkCounts: Record<string, number> = {};
218
- let textStartedInOrchestrator = false;
219
- const logChunk = (type: string) => {
220
- chunkCounts[type] = (chunkCounts[type] || 0) + 1;
221
- };
222
- // ==========================================
223
-
224
- try {
225
- const pendingToolCalls: ToolCallRequest[] = [];
226
- let hasToolCalls = false;
227
-
228
- // 是否启用思考(每轮都可以启用)
229
- // 强制转为 boolean,避免上层传入 'false' / 1 等导致逻辑误判
230
- const enableThinkingThisRound = options.enableThinking === true;
231
- // Web Search:允许在多轮中发生(模型可能在后续轮次再次检索)
232
- const enableSearchThisRound = options.enableSearch === true;
233
-
234
- logger.debug('调用 adapter.streamOnce', { enableThinking: enableThinkingThisRound, enableSearch: enableSearchThisRound });
235
-
236
- const stream = adapter.streamOnce(messages, context.tools, {
237
- model: options.model,
238
- enableThinking: enableThinkingThisRound,
239
- enableSearch: enableSearchThisRound,
240
- signal: context.signal,
241
- });
242
-
243
- for await (const chunk of stream) {
244
- logChunk(chunk.type);
245
- switch (chunk.type) {
246
- case 'text':
247
- if (chunk.text) {
248
- // 首次 text,记录状态并清除 thinking loading(文字流接管)
249
- if (!textStartedInOrchestrator) {
250
- textStartedInOrchestrator = true;
251
- logger.debug('首次收到 text', { thinkingStarted, thinkingComplete });
252
- yield createAgentStatus(null);
253
- }
254
- // 如果还在思考中,先结束思考(仅在启用 thinking 时)
255
- if (enableThinkingThisRound && thinkingStarted && !thinkingComplete) {
256
- logger.debug('text 触发 thinking_end');
257
- thinkingComplete = true;
258
- yield createThinkingEnd(thinkingStartedAt);
259
- }
260
- finalText += chunk.text;
261
- yield createTextDelta(chunk.text);
262
- }
263
- break;
264
-
265
- case 'thinking':
266
- // Gate:未启用 thinking 时,忽略所有 thinking 输出(用于统一 ask/normal 体验)
267
- if (!enableThinkingThisRound) break;
268
- if (chunk.thinking) {
269
- if (thinkingComplete) {
270
- // ⚠️ 异常:thinking_done 后还收到 thinking
271
- logger.warn('⚠️ thinkingComplete=true 但收到 thinking', chunk.thinking.slice(0, 30));
272
- logger.warn('当前状态', { textStarted: textStartedInOrchestrator, chunkCounts });
273
- } else if (textStartedInOrchestrator) {
274
- // ⚠️ 异常:text 开始后还收到 thinking
275
- logger.warn('⚠️ text 开始后收到 thinking', chunk.thinking.slice(0, 30));
276
- } else {
277
- // 兼容:部分 provider 的第一段 thinking 可能以换行开头(UI 使用 pre-wrap 会显示成“顶部空一行”)
278
- // 只在“第一段 thinking”做一次性处理,避免破坏中间格式
279
- let delta = chunk.thinking;
280
- if (!thinkingStarted) {
281
- delta = delta.replace(/^(?:\r?\n)+/, '');
282
- if (!delta) break; // 第一段只有换行:等待后续真正内容再开始
283
- }
284
-
285
- // 正常处理
286
- if (!thinkingStarted) {
287
- thinkingStarted = true;
288
- thinkingStartedAt = Date.now();
289
- logger.debug('发送 thinking_start');
290
- yield createAgentStatus(null); // thinking spinner 接管,清除 loading
291
- yield createThinkingStart();
292
- }
293
- thinkingText += delta;
294
- yield createThinkingDelta(delta);
295
- }
296
- }
297
- break;
298
-
299
- case 'thinking_done':
300
- // Gate:未启用 thinking 时,忽略 thinking_done
301
- if (!enableThinkingThisRound) break;
302
- logger.info('收到 thinking_done', { thinkingStarted, thinkingComplete });
303
- if (thinkingStarted && !thinkingComplete) {
304
- thinkingComplete = true;
305
- logger.debug('发送 thinking_end');
306
- yield createThinkingEnd(thinkingStartedAt);
307
- } else if (!thinkingStarted) {
308
- logger.warn('⚠️ 收到 thinking_done 但 thinkingStarted=false');
309
- }
310
- break;
311
-
312
- case 'tool_call':
313
- if (chunk.toolCall) {
314
- // Gate:如果当前没有任何可用工具定义,忽略模型产生的工具调用(避免 ask 模式误入工具循环)
315
- if (!context.tools || context.tools.length === 0) {
316
- logger.warn('收到 tool_call 但当前未注入工具,已忽略', { toolName: chunk.toolCall.name });
317
- break;
318
- }
319
- pendingToolCalls.push(chunk.toolCall);
320
- hasToolCalls = true;
321
- }
322
- break;
323
-
324
- case 'search_result':
325
- // Gate:未启用搜索时,忽略 search_result
326
- if (!enableSearchThisRound) break;
327
- if (chunk.searchResults) {
328
- // 首次收到搜索结果时,发送 search_start 事件
329
- if (!searchStarted) {
330
- searchStarted = true;
331
- searchStartedAt = Date.now();
332
- yield createAgentStatus(null); // 搜索 spinner 接管,清除 loading
333
- yield createSearchStart(message);
334
- }
335
- searchResults = normalizeSearchResults(chunk.searchResults);
336
- yield createSearchResult(searchResults, searchStartedAt);
337
- }
338
- break;
339
-
340
- case 'done':
341
- logger.info('收到 done', { finishReason: chunk.finishReason, usage: chunk.usage });
342
- logger.info(`第 ${iterations} 轮 chunk 统计`, chunkCounts);
343
- if (chunk.finishReason === 'tool_calls') {
344
- hasToolCalls = true;
345
- }
346
- // 累积 Token 使用统计(多轮工具调用时累加)
347
- if (chunk.usage) {
348
- hasUsage = true;
349
- totalUsage.promptTokens += chunk.usage.promptTokens || 0;
350
- totalUsage.completionTokens += chunk.usage.completionTokens || 0;
351
- totalUsage.totalTokens += chunk.usage.totalTokens || 0;
352
- totalUsage.reasoningTokens += chunk.usage.reasoningTokens || 0;
353
- totalUsage.cachedTokens += chunk.usage.cachedTokens || 0;
354
- }
355
- break;
356
-
357
- case 'error':
358
- logger.error('收到 error', chunk.error);
359
- yield createApiError(chunk.error ?? '未知错误');
360
- return;
361
- }
362
- }
363
-
364
- logger.info(`第 ${iterations} 轮 for-await 循环结束`);
365
- logger.debug('状态', { thinkingStarted, thinkingComplete });
366
-
367
- // 确保思考状态完成(仅在启用 thinking 时)
368
- if (enableThinkingThisRound && thinkingStarted && !thinkingComplete) {
369
- logger.debug('补发 thinking_end');
370
- thinkingComplete = true;
371
- yield createThinkingEnd(thinkingStartedAt);
372
- }
373
-
374
- // 处理工具调用
375
- if (hasToolCalls && pendingToolCalls.length > 0) {
376
- // ========== 重复工具调用检测 ==========
377
- // 生成当前轮的工具调用签名(所有工具名 + 参数拼接后的字符串)
378
- const currentSignature = pendingToolCalls
379
- .map(tc => `${tc.name}:${tc.arguments}`)
380
- .sort()
381
- .join('|');
382
-
383
- if (currentSignature === lastToolCallSignature) {
384
- duplicateToolCallCount++;
385
- } else {
386
- duplicateToolCallCount = 0;
387
- lastToolCallSignature = currentSignature;
388
- }
389
-
390
- if (duplicateToolCallCount >= DUPLICATE_TOOL_CALL_THRESHOLD) {
391
- logger.warn('检测到重复工具调用,注入终止提示', {
392
- signature: currentSignature.slice(0, 200),
393
- count: duplicateToolCallCount + 1,
394
- });
395
-
396
- // 不再执行工具,而是注入一条提示消息让模型停止循环
397
- const toolNames = pendingToolCalls.map(tc => tc.name).join(', ');
398
- messages.push({
399
- role: 'assistant',
400
- content: finalText,
401
- toolCalls: pendingToolCalls,
402
- });
403
- for (const toolCall of pendingToolCalls) {
404
- messages.push({
405
- role: 'tool',
406
- content: `[系统提示] 你已经连续 ${duplicateToolCallCount + 1} 次使用相同参数调用 ${toolCall.name},任务应已完成。请直接回复用户执行结果,不要再调用工具。`,
407
- toolCallId: toolCall.id,
408
- toolName: toolCall.name,
409
- });
410
- }
411
-
412
- // 发射步骤结束事件,进入下一轮让模型生成总结
413
- yield createStepEnd(iterations, stepStartedAt);
414
- finalText = '';
415
- continue;
416
- }
417
- // ========== 重复检测结束 ==========
418
-
419
- // 检查是否有客户端工具调用(优先使用 context 中的配置,兼容 config)
420
- const clientToolNames = context.clientToolNames || this.config.clientToolNames;
421
- const clientToolCalls = clientToolNames
422
- ? pendingToolCalls.filter(tc => clientToolNames.has(tc.name))
423
- : [];
424
-
425
- // 如果有客户端工具调用,透传给客户端,结束本轮
426
- if (clientToolCalls.length > 0) {
427
- logger.info('检测到客户端工具调用,透传给客户端', clientToolCalls.map(tc => tc.name));
428
-
429
- // 发送所有客户端工具调用请求
430
- for (const toolCall of clientToolCalls) {
431
- let args: Record<string, unknown>;
432
- try {
433
- args = JSON.parse(toolCall.arguments || '{}');
434
- } catch {
435
- args = {};
436
- }
437
- yield createToolCallRequest(toolCall.id, toolCall.name, args);
438
- }
439
-
440
- // 结束本轮对话,让客户端执行后发新请求继续
441
- const duration = Date.now() - startedAt;
442
- yield createDone(finalText, undefined, duration);
443
- return;
444
- }
445
-
446
- // 工具执行阶段:工具卡片/搜索卡片接管进度显示,清除 loading
447
- yield createAgentStatus(null);
448
-
449
- // 1. 添加 assistant 消息(包含 tool_calls)到消息历史
450
- const assistantMessage: StandardMessage = {
451
- role: 'assistant',
452
- content: finalText,
453
- toolCalls: pendingToolCalls,
454
- };
455
- messages.push(assistantMessage);
456
-
457
- // 2. 执行云端工具并添加 tool 消息
458
- for (const toolCall of pendingToolCalls) {
459
- const toolStartedAt = Date.now();
460
-
461
- // 解析参数
462
- let args: Record<string, unknown>;
463
- try {
464
- args = JSON.parse(toolCall.arguments || '{}');
465
- } catch {
466
- // 参数解析失败,添加错误消息
467
- messages.push({
468
- role: 'tool',
469
- content: '参数解析错误',
470
- toolCallId: toolCall.id,
471
- });
472
- continue;
473
- }
474
-
475
- // web_search_ai 参数兜底:
476
- // OpenRouter 等模型有时会发起空 arguments 的 function_call(arguments: ""),导致 query 缺失。
477
- // 为保证链路可用:当 query 缺失时,默认使用本轮用户消息作为 query。
478
- if (toolCall.name === WEB_SEARCH_TOOL_NAME) {
479
- const q = typeof args.query === 'string' ? args.query : '';
480
- if (!q.trim()) {
481
- args.query = message;
482
- }
483
- }
484
-
485
- // 获取最新的自动运行配置(优先使用动态回调)
486
- const autoRunConfig = this.config.getAutoRunConfig
487
- ? await this.config.getAutoRunConfig()
488
- : (options.autoRunConfig || this.config.autoRunConfig);
489
-
490
- // 检查是否需要手动批准:全局 manual 模式 或 工具声明 requiresApproval
491
- const currentTool = this.config.tools?.get(toolCall.name);
492
- const needsApproval = (autoRunConfig?.mode === 'manual' || currentTool?.requiresApproval === true);
493
- logger.debug('检查工具批准', {
494
- toolName: toolCall.name,
495
- autoRunConfigMode: autoRunConfig?.mode,
496
- requiresApproval: currentTool?.requiresApproval,
497
- hasCallback: !!this.config.onToolApprovalRequest,
498
- });
499
- if (needsApproval && this.config.onToolApprovalRequest) {
500
- logger.info('发送工具批准请求', toolCall.name);
501
- // 发送批准请求事件
502
- const approvalRequest: ToolApprovalRequestEvent = {
503
- type: 'tool_approval_request',
504
- data: {
505
- id: toolCall.id,
506
- name: toolCall.name,
507
- args,
508
- requestedAt: Date.now(),
509
- },
510
- };
511
- yield approvalRequest;
512
-
513
- // 等待用户批准
514
- const approved = await this.config.onToolApprovalRequest({
515
- id: toolCall.id,
516
- name: toolCall.name,
517
- args,
518
- });
519
-
520
- if (!approved) {
521
- // 用户跳过执行
522
- const result = JSON.stringify({ skipped: true, message: '用户跳过了此工具' });
523
- if (toolCall.name === WEB_SEARCH_TOOL_NAME && enableSearchThisRound) {
524
- // 用 search_end 表示“跳过搜索”
525
- if (!searchStarted) {
526
- searchStarted = true;
527
- searchStartedAt = Date.now();
528
- const q = typeof args.query === 'string' ? args.query : message;
529
- yield createSearchStart(q);
530
- }
531
- searchEnded = true;
532
- yield createSearchEnd(false, searchStartedAt, `用户跳过了 ${WEB_SEARCH_TOOL_NAME}`);
533
- } else {
534
- yield createToolCallResult(toolCall.id, toolCall.name, result, false, toolStartedAt);
535
- }
536
- messages.push({
537
- role: 'tool',
538
- content: result,
539
- toolCallId: toolCall.id,
540
- toolName: toolCall.name,
541
- });
542
- continue;
543
- }
544
- }
545
-
546
- const isWebSearchTool = toolCall.name === WEB_SEARCH_TOOL_NAME;
547
- // web_search_ai 走专属事件;其他工具(含 update_plan)走通用 tool_call_start
548
- if (!isWebSearchTool) {
549
- yield createToolCallStart(toolCall.id, toolCall.name, args);
550
- } else if (enableSearchThisRound) {
551
- if (!searchStarted) {
552
- searchStarted = true;
553
- searchStartedAt = Date.now();
554
- const q = typeof args.query === 'string' ? args.query : message;
555
- yield createSearchStart(q);
556
- }
557
- }
558
-
559
- // 如果工具声明了 timeout,包装一个带超时的 AbortSignal
560
- const toolDefForTimeout = this.config.tools?.get(toolCall.name);
561
- let effectiveSignal = context.signal;
562
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
563
- if (toolDefForTimeout?.timeout) {
564
- const ac = new AbortController();
565
- timeoutId = setTimeout(() => ac.abort(), toolDefForTimeout.timeout);
566
- // 监听父信号,父取消时同步取消
567
- context.signal.addEventListener('abort', () => ac.abort(), { once: true });
568
- effectiveSignal = ac.signal;
569
- }
570
-
571
- // 执行工具
572
- let result: string;
573
- let toolErrorShape: import('../events').ToolErrorShape | undefined;
574
- let success = true;
575
- try {
576
- // stdout/stderr 流式输出:通过 hooks 推送到事件队列,再在等待工具完成期间持续 yield
577
- const eventQueue: ChatEvent[] = [];
578
- let wake: (() => void) | null = null;
579
- const notify = () => wake?.();
580
- const pushEvent = (ev: ChatEvent) => {
581
- eventQueue.push(ev);
582
- notify();
583
- };
584
-
585
- let toolDone = false;
586
- let toolValue: object | undefined;
587
- let toolError: unknown;
588
-
589
- const hooks = {
590
- toolCallId: toolCall.id,
591
- toolName: toolCall.name,
592
- onStdout: (chunk: string) => {
593
- if (chunk) pushEvent(createToolCallOutput(toolCall.id, toolCall.name, 'stdout', chunk));
594
- },
595
- onStderr: (chunk: string) => {
596
- if (chunk) pushEvent(createToolCallOutput(toolCall.id, toolCall.name, 'stderr', chunk));
597
- },
598
- };
599
-
600
- this.config.executeTool(toolCall.name, args, effectiveSignal, hooks)
601
- .then((v) => {
602
- toolValue = v;
603
- toolDone = true;
604
- notify();
605
- })
606
- .catch((e) => {
607
- toolError = e;
608
- toolDone = true;
609
- notify();
610
- });
611
-
612
- // 在工具执行期间,把 stdout/stderr 事件不断 yield 出去
613
- while (!toolDone || eventQueue.length > 0) {
614
- while (eventQueue.length > 0) {
615
- const ev = eventQueue.shift();
616
- if (ev) yield ev;
617
- }
618
- if (toolDone) break;
619
- await new Promise<void>((r) => (wake = r));
620
- wake = null;
621
- }
622
-
623
- if (toolError) throw toolError;
624
- // 工具返回 object,自动序列化
625
- result = JSON.stringify(toolValue ?? {});
626
- } catch (error) {
627
- success = false;
628
- if (isToolError(error)) {
629
- toolErrorShape = error.toolError;
630
- result = JSON.stringify({ error: error.toolError.message });
631
- } else {
632
- const msg = error instanceof Error ? error.message : String(error);
633
- toolErrorShape = { message: msg };
634
- result = JSON.stringify({ error: msg });
635
- }
636
- }
637
-
638
- // 清理超时定时器
639
- if (timeoutId !== undefined) clearTimeout(timeoutId);
640
-
641
- // 获取工具 ui 声明(成功时传递给前端)
642
- const toolDef = this.config.tools?.get(toolCall.name);
643
- const toolUI = success ? toolDef?.ui : undefined;
644
-
645
- // 发射工具结果事件
646
- if (!isWebSearchTool) {
647
- yield createToolCallResult(toolCall.id, toolCall.name, result, success, toolStartedAt, toolErrorShape, toolUI);
648
- } else if (enableSearchThisRound) {
649
- // web_search_ai:解析工具结果并产出 search_result / search_end
650
- let parsedResults: SearchResultItem[] = [];
651
- let parseError: string | undefined;
652
- try {
653
- const obj = JSON.parse(result || '{}') as any;
654
- const arr = Array.isArray(obj?.results) ? obj.results : [];
655
- parsedResults = arr
656
- .map((r: any) => ({
657
- title: typeof r?.title === 'string' ? r.title : '',
658
- url: typeof r?.url === 'string' ? r.url : '',
659
- snippet: typeof r?.snippet === 'string' ? r.snippet : '',
660
- }))
661
- .filter((r: SearchResultItem) => !!r.url);
662
- if (typeof obj?.error === 'string' && obj.error) {
663
- parseError = obj.error;
664
- success = false;
665
- }
666
- } catch {
667
- parseError = `${WEB_SEARCH_TOOL_NAME} 返回结果解析失败`;
668
- success = false;
669
- }
670
-
671
- if (parsedResults.length > 0) {
672
- searchResults = normalizeSearchResults(parsedResults);
673
- yield createSearchResult(searchResults, searchStartedAt);
674
- }
675
- searchEnded = true;
676
- yield createSearchEnd(success, searchStartedAt, parseError);
677
- }
678
-
679
- // 添加 tool 消息到历史
680
- messages.push({
681
- role: 'tool',
682
- content: result,
683
- toolCallId: toolCall.id,
684
- toolName: toolCall.name, // Gemini 需要工具名称
685
- });
686
-
687
- // 工具执行后检查取消信号:如果用户在工具执行过程中取消,立即终止
688
- if (context.signal.aborted) {
689
- yield createAbort('请求已取消');
690
- return;
691
- }
692
- }
693
-
694
- // 发射步骤结束事件
695
- yield createStepEnd(iterations, stepStartedAt);
696
-
697
- // 清空累积文本,进入下一轮
698
- finalText = '';
699
- continue;
700
- }
701
-
702
- // 没有工具调用,响应完成
703
- yield createStepEnd(iterations, stepStartedAt);
704
- break;
705
-
706
- } catch (error) {
707
- // 异常路径也要关闭 step 事件,确保前端 step 计数正确
708
- yield createStepEnd(iterations, stepStartedAt);
709
-
710
- if (context.signal.aborted) {
711
- yield createAbort('请求已取消');
712
- } else {
713
- yield createApiError(error instanceof Error ? error.message : String(error));
714
- }
715
- return;
716
- }
717
- }
718
-
719
- // 安全兜底:如果达到最大迭代次数,记录警告
720
- if (iterations >= maxIterations) {
721
- logger.warn(`达到最大迭代次数 ${maxIterations},强制结束工具调用循环`);
722
- }
723
-
724
- // 确保搜索状态完成
725
- if (searchStarted && !searchEnded) {
726
- yield createSearchEnd(true, searchStartedAt);
727
- }
728
-
729
- // 注意:不再更新 context.history
730
- // 无状态架构:前端负责保存消息到数据库,下次请求时重新构建历史
731
-
732
- // 发射完成事件(包含累积的 Token 使用统计)
733
- const duration = Date.now() - startedAt;
734
- const usage = hasUsage ? {
735
- promptTokens: totalUsage.promptTokens,
736
- completionTokens: totalUsage.completionTokens,
737
- totalTokens: totalUsage.totalTokens,
738
- ...(totalUsage.reasoningTokens > 0 ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
739
- ...(totalUsage.cachedTokens > 0 ? { cachedTokens: totalUsage.cachedTokens } : {}),
740
- } : undefined;
741
- yield createDone(finalText, usage, duration);
742
- }
743
-
744
- /**
745
- * 构建标准化消息列表
746
- */
747
- private buildMessages(context: OrchestratorContext, message: string): StandardMessage[] {
748
- const messages: StandardMessage[] = [];
749
-
750
- // 系统提示
751
- if (context.systemPrompt) {
752
- messages.push({
753
- role: 'system',
754
- content: context.systemPrompt,
755
- });
756
- }
757
-
758
- // 历史消息
759
- for (const msg of context.history) {
760
- const standardMsg: StandardMessage = {
761
- role: msg.role as 'user' | 'assistant' | 'tool',
762
- content: msg.content,
763
- };
764
-
765
- // 处理 assistant 消息的工具调用
766
- if (msg.tool_calls) {
767
- standardMsg.toolCalls = msg.tool_calls.map(tc => ({
768
- id: tc.id,
769
- name: tc.function.name,
770
- arguments: tc.function.arguments,
771
- // 保留 thought_signature(Gemini 模型需要)
772
- thought_signature: tc.thought_signature,
773
- }));
774
- }
775
-
776
- // 处理 tool 消息的 tool_call_id
777
- if (msg.role === 'tool' && msg.tool_call_id) {
778
- standardMsg.toolCallId = msg.tool_call_id;
779
- }
780
-
781
- messages.push(standardMsg);
782
- }
783
-
784
- // 当前用户消息(跳过空消息,用于工具调用后继续对话)
785
- if (message) {
786
- messages.push({
787
- role: 'user',
788
- content: message,
789
- images: context.images,
790
- });
791
- }
792
-
793
- return messages;
794
- }
795
-
796
- /** 检测并执行上下文压缩,通过注入的 summarize 回调走 ai-server */
797
- private async *compactIfNeeded(
798
- messages: StandardMessage[],
799
- compactConfig: CompactConfig,
800
- ): AsyncGenerator<CompactStartEvent | CompactEndEvent, StandardMessage[]> {
801
- if (!needsCompaction(messages, compactConfig)) {
802
- return messages;
803
- }
804
-
805
- const summarizeFn = this.config.summarize;
806
- if (!summarizeFn) {
807
- logger.warn('上下文超限但未配置 summarize 回调,跳过压缩');
808
- return messages;
809
- }
810
-
811
- const compactStartedAt = Date.now();
812
- const tokensBeforeCompact = estimateTotalTokens(messages);
813
- const budget = getPromptBudget(compactConfig);
814
- const originalCount = messages.length;
815
-
816
- yield createCompactStart(tokensBeforeCompact, budget);
817
-
818
- const { messages: result, success } = await summarizeHistory(
819
- summarizeFn, messages, compactConfig,
820
- );
821
-
822
- yield createCompactEnd(
823
- success,
824
- estimateTotalTokens(result),
825
- originalCount,
826
- result.length,
827
- compactStartedAt,
828
- );
829
-
830
- return result;
831
- }
832
- }
833
-
834
- /**
835
- * 创建 Orchestrator 实例
836
- */
837
- export function createOrchestrator(config: OrchestratorConfig): ChatOrchestrator {
838
- return new ChatOrchestrator(config);
839
- }