foliko 1.0.74 → 1.0.76

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 (238) hide show
  1. package/.agent/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  2. package/.agent/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  3. package/.agent/.shared/ui-ux-pro-max/data/icons.csv +101 -0
  4. package/.agent/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  5. package/.agent/.shared/ui-ux-pro-max/data/products.csv +97 -0
  6. package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  7. package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +45 -0
  8. package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  9. package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  10. package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  11. package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  12. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  13. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  14. package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  15. package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  16. package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  17. package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  18. package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  19. package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  20. package/.agent/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  21. package/.agent/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  22. package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  23. package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +31 -0
  25. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
  26. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
  27. package/.agent/.shared/ui-ux-pro-max/scripts/core.py +258 -0
  28. package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +1067 -0
  29. package/.agent/.shared/ui-ux-pro-max/scripts/search.py +106 -0
  30. package/.agent/ARCHITECTURE.md +288 -0
  31. package/.agent/agents/ambient-agent.md +57 -0
  32. package/.agent/agents/debugger.md +55 -0
  33. package/.agent/agents/email-assistant.md +49 -0
  34. package/.agent/agents/file-manager.md +42 -0
  35. package/.agent/agents/python-developer.md +60 -0
  36. package/.agent/agents/scheduler.md +59 -0
  37. package/.agent/agents/web-developer.md +45 -0
  38. package/.agent/data/default.json +29 -0
  39. package/.agent/data/plugins-state.json +255 -0
  40. package/.agent/mcp_config.json +4 -0
  41. package/.agent/mcp_config_updated.json +12 -0
  42. package/.agent/plugins.json +5 -0
  43. package/.agent/rules/GEMINI.md +273 -0
  44. package/.agent/rules/allow-rule.md +77 -0
  45. package/.agent/rules/log-rule.md +83 -0
  46. package/.agent/rules/security-rule.md +93 -0
  47. package/.agent/scripts/auto_preview.py +148 -0
  48. package/.agent/scripts/checklist.py +217 -0
  49. package/.agent/scripts/session_manager.py +120 -0
  50. package/.agent/scripts/verify_all.py +327 -0
  51. package/.agent/skills/api-patterns/SKILL.md +81 -0
  52. package/.agent/skills/api-patterns/api-style.md +42 -0
  53. package/.agent/skills/api-patterns/auth.md +24 -0
  54. package/.agent/skills/api-patterns/documentation.md +26 -0
  55. package/.agent/skills/api-patterns/graphql.md +41 -0
  56. package/.agent/skills/api-patterns/rate-limiting.md +31 -0
  57. package/.agent/skills/api-patterns/response.md +37 -0
  58. package/.agent/skills/api-patterns/rest.md +40 -0
  59. package/.agent/skills/api-patterns/scripts/api_validator.py +211 -0
  60. package/.agent/skills/api-patterns/security-testing.md +122 -0
  61. package/.agent/skills/api-patterns/trpc.md +41 -0
  62. package/.agent/skills/api-patterns/versioning.md +22 -0
  63. package/.agent/skills/app-builder/SKILL.md +75 -0
  64. package/.agent/skills/app-builder/agent-coordination.md +71 -0
  65. package/.agent/skills/app-builder/feature-building.md +53 -0
  66. package/.agent/skills/app-builder/project-detection.md +34 -0
  67. package/.agent/skills/app-builder/scaffolding.md +118 -0
  68. package/.agent/skills/app-builder/tech-stack.md +40 -0
  69. package/.agent/skills/app-builder/templates/SKILL.md +39 -0
  70. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  71. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  72. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  73. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  74. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  75. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  76. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  77. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -0
  78. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -0
  79. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -0
  80. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -0
  81. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  82. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -0
  83. package/.agent/skills/architecture/SKILL.md +55 -0
  84. package/.agent/skills/architecture/context-discovery.md +43 -0
  85. package/.agent/skills/architecture/examples.md +94 -0
  86. package/.agent/skills/architecture/pattern-selection.md +68 -0
  87. package/.agent/skills/architecture/patterns-reference.md +50 -0
  88. package/.agent/skills/architecture/trade-off-analysis.md +77 -0
  89. package/.agent/skills/clean-code/SKILL.md +201 -0
  90. package/.agent/skills/doc.md +177 -0
  91. package/.agent/skills/frontend-design/SKILL.md +418 -0
  92. package/.agent/skills/frontend-design/animation-guide.md +331 -0
  93. package/.agent/skills/frontend-design/color-system.md +311 -0
  94. package/.agent/skills/frontend-design/decision-trees.md +418 -0
  95. package/.agent/skills/frontend-design/motion-graphics.md +306 -0
  96. package/.agent/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  97. package/.agent/skills/frontend-design/scripts/ux_audit.py +722 -0
  98. package/.agent/skills/frontend-design/typography-system.md +345 -0
  99. package/.agent/skills/frontend-design/ux-psychology.md +1116 -0
  100. package/.agent/skills/frontend-design/visual-effects.md +383 -0
  101. package/.agent/skills/i18n-localization/SKILL.md +154 -0
  102. package/.agent/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  103. package/.agent/skills/mcp-builder/SKILL.md +176 -0
  104. package/.agent/skills/web-design-guidelines/SKILL.md +57 -0
  105. package/.agent/workflows/brainstorm.md +113 -0
  106. package/.agent/workflows/create.md +59 -0
  107. package/.agent/workflows/debug.md +103 -0
  108. package/.agent/workflows/deploy.md +176 -0
  109. package/.agent/workflows/enhance.md +63 -0
  110. package/.agent/workflows/orchestrate.md +237 -0
  111. package/.agent/workflows/plan.md +89 -0
  112. package/.agent/workflows/preview.md +81 -0
  113. package/.agent/workflows/simple-test.md +42 -0
  114. package/.agent/workflows/status.md +86 -0
  115. package/.agent/workflows/structured-orchestrate.md +180 -0
  116. package/.agent/workflows/test.md +144 -0
  117. package/.agent/workflows/ui-ux-pro-max.md +296 -0
  118. package/.claude/settings.local.json +11 -1
  119. package/.editorconfig +56 -0
  120. package/.husky/pre-commit +4 -0
  121. package/.lintstagedrc +7 -0
  122. package/.prettierignore +29 -0
  123. package/.prettierrc +11 -0
  124. package/CLAUDE.md +2 -0
  125. package/README.md +64 -55
  126. package/SPEC.md +102 -61
  127. package/cli/bin/foliko.js +11 -11
  128. package/cli/src/commands/chat.js +143 -141
  129. package/cli/src/commands/list.js +93 -90
  130. package/cli/src/index.js +75 -75
  131. package/cli/src/ui/chat-ui.js +201 -199
  132. package/cli/src/utils/ansi.js +40 -40
  133. package/cli/src/utils/markdown.js +292 -296
  134. package/docker-compose.yml +1 -1
  135. package/docs/ai-sdk-optimization.md +655 -643
  136. package/docs/features.md +80 -80
  137. package/docs/quick-reference.md +49 -46
  138. package/docs/user-manual.md +411 -380
  139. package/examples/ambient-example.js +194 -196
  140. package/examples/basic.js +50 -45
  141. package/examples/bootstrap.js +121 -112
  142. package/examples/mcp-example.js +19 -16
  143. package/examples/skill-example.js +20 -20
  144. package/examples/test-chat.js +137 -135
  145. package/examples/test-mcp.js +85 -79
  146. package/examples/test-reload.js +59 -61
  147. package/examples/test-telegram.js +50 -50
  148. package/examples/test-tg-bot.js +45 -42
  149. package/examples/test-tg-simple.js +47 -46
  150. package/examples/test-tg.js +62 -62
  151. package/examples/test-think.js +43 -37
  152. package/examples/test-web-plugin.js +103 -98
  153. package/examples/test-weixin-feishu.js +103 -100
  154. package/examples/workflow.js +158 -158
  155. package/package.json +37 -3
  156. package/plugins/ai-plugin.js +102 -100
  157. package/plugins/ambient-agent/EventWatcher.js +113 -0
  158. package/plugins/ambient-agent/ExplorerLoop.js +640 -0
  159. package/plugins/ambient-agent/GoalManager.js +197 -0
  160. package/plugins/ambient-agent/Reflector.js +95 -0
  161. package/plugins/ambient-agent/StateStore.js +90 -0
  162. package/plugins/ambient-agent/constants.js +101 -0
  163. package/plugins/ambient-agent/index.js +579 -0
  164. package/plugins/audit-plugin.js +187 -187
  165. package/plugins/default-plugins.js +662 -649
  166. package/plugins/email/constants.js +64 -0
  167. package/plugins/email/handlers.js +461 -0
  168. package/plugins/email/index.js +278 -0
  169. package/plugins/email/monitor.js +269 -0
  170. package/plugins/email/parser.js +138 -0
  171. package/plugins/email/reply.js +151 -0
  172. package/plugins/email/utils.js +124 -0
  173. package/plugins/feishu-plugin.js +481 -477
  174. package/plugins/file-system-plugin.js +826 -476
  175. package/plugins/install-plugin.js +199 -197
  176. package/plugins/python-executor-plugin.js +367 -365
  177. package/plugins/python-plugin-loader.js +481 -479
  178. package/plugins/rules-plugin.js +294 -292
  179. package/plugins/scheduler-plugin.js +691 -689
  180. package/plugins/session-plugin.js +369 -367
  181. package/plugins/shell-executor-plugin.js +197 -197
  182. package/plugins/storage-plugin.js +240 -238
  183. package/plugins/subagent-plugin.js +845 -785
  184. package/plugins/telegram-plugin.js +482 -475
  185. package/plugins/think-plugin.js +345 -343
  186. package/plugins/tools-plugin.js +196 -194
  187. package/plugins/web-plugin.js +606 -604
  188. package/plugins/weixin-plugin.js +545 -538
  189. package/reports/system-health-report-20260401.md +79 -0
  190. package/skills/ambient-agent/SKILL.md +49 -39
  191. package/skills/foliko-dev/AGENTS.md +64 -61
  192. package/skills/foliko-dev/SKILL.md +125 -119
  193. package/skills/mcp-usage/SKILL.md +19 -17
  194. package/skills/python-plugin-dev/SKILL.md +16 -15
  195. package/skills/skill-guide/SKILL.md +12 -12
  196. package/skills/subagent-guide/SKILL.md +237 -0
  197. package/skills/workflow-guide/SKILL.md +90 -45
  198. package/skills/workflow-troubleshooting/DEBUGGING.md +36 -21
  199. package/skills/workflow-troubleshooting/SKILL.md +156 -79
  200. package/src/capabilities/index.js +11 -11
  201. package/src/capabilities/skill-manager.js +609 -595
  202. package/src/capabilities/workflow-engine.js +1109 -1195
  203. package/src/core/agent-chat.js +882 -735
  204. package/src/core/agent.js +892 -688
  205. package/src/core/framework.js +465 -431
  206. package/src/core/index.js +19 -19
  207. package/src/core/plugin-base.js +219 -219
  208. package/src/core/plugin-manager.js +863 -767
  209. package/src/core/provider.js +114 -111
  210. package/src/core/sub-agent-config.js +264 -0
  211. package/src/core/system-prompt-builder.js +120 -0
  212. package/src/core/tool-registry.js +517 -134
  213. package/src/core/tool-router.js +297 -216
  214. package/src/executors/executor-base.js +12 -12
  215. package/src/executors/mcp-executor.js +741 -729
  216. package/src/index.js +25 -37
  217. package/src/utils/circuit-breaker.js +301 -0
  218. package/src/utils/error-boundary.js +363 -0
  219. package/src/utils/error.js +374 -0
  220. package/src/utils/event-emitter.js +97 -97
  221. package/src/utils/id.js +133 -0
  222. package/src/utils/index.js +217 -3
  223. package/src/utils/logger.js +181 -0
  224. package/src/utils/plugin-helpers.js +90 -0
  225. package/src/utils/retry.js +122 -0
  226. package/src/utils/sandbox.js +292 -0
  227. package/test/tool-registry-validation.test.js +218 -0
  228. package/test_report.md +70 -0
  229. package/website/docs/api.html +169 -107
  230. package/website/docs/configuration.html +296 -144
  231. package/website/docs/plugin-development.html +154 -85
  232. package/website/docs/project-structure.html +110 -109
  233. package/website/docs/skill-development.html +117 -61
  234. package/website/index.html +209 -205
  235. package/website/script.js +136 -133
  236. package/website/styles.css +1 -1
  237. package/plugins/ambient-agent-plugin.js +0 -1565
  238. package/plugins/email.js +0 -1142
@@ -1,735 +1,882 @@
1
- /**
2
- * AgentChatHandler 聊天处理器
3
- * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环
4
- */
5
-
6
- const { EventEmitter } = require('../utils/event-emitter')
7
- const tiktoken = require('tiktoken')
8
-
9
- // 模型上下文限制表(留 15-20% 余量给 system prompt 和输出)
10
- const MODEL_CONTEXT_LIMITS = {
11
- // DeepSeek
12
- 'deepseek-chat': 28000,
13
- 'deepseek-coder': 28000,
14
- // MiniMax
15
- 'MiniMax-M2.7': 90000,
16
- // OpenAI
17
- 'gpt-4': 100000,
18
- 'gpt-4o': 100000,
19
- 'gpt-4o-mini': 100000,
20
- 'gpt-4-turbo': 100000,
21
- // Anthropic
22
- 'claude-3-5-sonnet': 150000,
23
- 'claude-3-opus': 150000,
24
- 'claude-3-sonnet': 150000,
25
- }
26
-
27
- class AgentChatHandler extends EventEmitter {
28
- /**
29
- * @param {Agent} agent - Agent 实例
30
- * @param {Object} config - 配置
31
- */
32
- constructor(agent, config = {}) {
33
- super()
34
-
35
- this.agent = agent
36
- this.config = config
37
-
38
- this.model = config.model || 'deepseek-chat'
39
- this.provider = config.provider || 'deepseek'
40
- this.apiKey = config.apiKey
41
- this.baseURL = config.baseURL
42
- this.providerOptions = config.providerOptions || {}
43
-
44
- this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.'
45
- this._messages = []
46
- this._tools = new Map()
47
- this._maxSteps = 5 // 降低默认步骤数,减少上下文消耗
48
-
49
- // 上下文压缩配置:根据模型自动设置限制
50
- const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find(k =>
51
- this.model.toLowerCase().includes(k.toLowerCase())
52
- )
53
- const defaultLimit = modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000
54
- this._maxContextTokens = config.maxContextTokens || defaultLimit
55
- this._compressionThreshold = config.compressionThreshold || 0.6 // 60% 就压缩,早触发
56
- this._keepRecentMessages = config.keepRecentMessages || 20 // 保留最近 20 条
57
- this._enableSmartCompress = config.enableSmartCompress !== false // 默认开启智能摘要
58
- this._encoder = null
59
- this._compressionCount = 0 // 压缩次数统计
60
-
61
- // 工具结果压缩配置
62
- this._maxToolResultSize = config.maxToolResultSize || 4000 // 工具结果超过此大小则压缩(字节)
63
-
64
- // 初始化编码器
65
- this._initEncoder()
66
- }
67
-
68
- /**
69
- * 初始化 tiktoken 编码器
70
- * @private
71
- */
72
- _initEncoder() {
73
- try {
74
- // cl100k_base GPT-4/ChatGPT 使用的编码
75
- this._encoder = tiktoken.get_encoding('cl100k_base')
76
- } catch (err) {
77
- console.warn('[AgentChat] Failed to initialize tiktoken encoder:', err.message)
78
- }
79
- }
80
-
81
- /**
82
- * 计算文本的 token
83
- * @param {string} text
84
- * @returns {number}
85
- * @private
86
- */
87
- _countTokens(text) {
88
- if (!this._encoder || !text) return 0
89
- try {
90
- return this._encoder.encode(text).length
91
- } catch (err) {
92
- // 粗略估算:约 4 字符 = 1 token
93
- return Math.ceil(text.length / 4)
94
- }
95
- }
96
-
97
- /**
98
- * 计算消息列表的总 token 数
99
- * @param {Array} messages
100
- * @returns {number}
101
- * @private
102
- */
103
- _countMessagesTokens(messages) {
104
- let total = 0
105
- for (const msg of messages) {
106
- total += 4 // role 标记
107
- if (typeof msg.content === 'string') {
108
- total += this._countTokens(msg.content)
109
- } else if (Array.isArray(msg.content)) {
110
- for (const part of msg.content) {
111
- if (part.text) {
112
- total += this._countTokens(part.text)
113
- }
114
- }
115
- }
116
- }
117
- total += 4 // 结尾标记
118
- return total
119
- }
120
-
121
- /**
122
- * 检查是否需要压缩上下文
123
- * @returns {boolean}
124
- * @private
125
- */
126
- _shouldCompress() {
127
- const totalTokens = this._countMessagesTokens(this._messages)
128
- return totalTokens > this._maxContextTokens * this._compressionThreshold
129
- }
130
-
131
- /**
132
- * 计算工具定义的 token 数(估算)
133
- * @returns {number}
134
- * @private
135
- */
136
- _countToolsTokens() {
137
- let total = 0
138
- for (const toolDef of this._tools.values()) {
139
- // 工具名 + 描述
140
- total += this._countTokens(toolDef.name || '')
141
- total += this._countTokens(toolDef.description || '')
142
-
143
- // 工具参数 schema
144
- if (toolDef.inputSchema) {
145
- const schemaStr = typeof toolDef.inputSchema === 'string'
146
- ? toolDef.inputSchema
147
- : JSON.stringify(toolDef.inputSchema)
148
- total += this._countTokens(schemaStr)
149
- }
150
- }
151
- return total
152
- }
153
-
154
- /**
155
- * 检查是否需要压缩(包括工具定义)
156
- * @returns {boolean}
157
- * @private
158
- */
159
- _shouldCompressWithTools() {
160
- const messagesTokens = this._countMessagesTokens(this._messages)
161
- const toolsTokens = this._countToolsTokens()
162
- const systemPromptTokens = this._countTokens(this._systemPrompt)
163
- const total = messagesTokens + toolsTokens + systemPromptTokens
164
-
165
- // 如果总token数超过上下文限制的 85%,就压缩
166
- return total > this._maxContextTokens * 0.85
167
- }
168
-
169
- /**
170
- * 压缩上下文消息(智能摘要模式)
171
- * 策略:
172
- * 1. 如果启用了智能摘要且有 AI 客户端,对早期消息进行 AI 总结
173
- * 2. 否则使用简单裁剪 + 标记
174
- * @private
175
- */
176
- async _compressContext() {
177
- if (this._messages.length <= this._keepRecentMessages) {
178
- return
179
- }
180
-
181
- const systemMessages = this._messages.filter(m => m.role === 'system')
182
- const otherMessages = this._messages.filter(m => m.role !== 'system')
183
-
184
- // 保留最近的 N 条非系统消息
185
- const recentMessages = otherMessages.slice(-this._keepRecentMessages)
186
- const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages)
187
-
188
- const compressedCount = messagesToSummarize.length
189
- let summaryContent = ''
190
-
191
- // 尝试使用 AI 总结
192
- if (this._enableSmartCompress && this._aiClient) {
193
- try {
194
- const summaryText = await this._summarizeMessages(messagesToSummarize)
195
- summaryContent = `[早期对话摘要]: ${summaryText}`
196
- console.log(`[AgentChat] AI 摘要生成成功 (${summaryText.length} chars)`)
197
- } catch (err) {
198
- console.warn('[AgentChat] AI 摘要失败,使用简单压缩:', err.message)
199
- summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`
200
- }
201
- } else {
202
- summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`
203
- }
204
-
205
- const summary = {
206
- role: 'system',
207
- content: summaryContent
208
- }
209
-
210
- this._messages = [...systemMessages, summary, ...recentMessages]
211
- this._compressionCount++
212
-
213
- const totalTokens = this._countMessagesTokens(this._messages)
214
- console.log(`[AgentChat] Context compressed (${this._compressionCount} times). Messages: ${this._messages.length}, Est. tokens: ${totalTokens}`)
215
- }
216
-
217
- /**
218
- * 使用 AI 对消息进行总结
219
- * @param {Array} messages - 要总结的消息
220
- * @returns {Promise<string>} 总结文本
221
- * @private
222
- */
223
- async _summarizeMessages(messages) {
224
- if (!this._aiClient || messages.length === 0) {
225
- return '(无早期对话)'
226
- }
227
-
228
- // 构建总结提示
229
- const conversationText = messages.map(m => {
230
- const role = m.role === 'user' ? '用户' : '助手'
231
- const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
232
- return `${role}: ${content}`
233
- }).join('\n')
234
-
235
- const summarizePrompt = `请简洁地总结以下对话的要点,保留关键信息和用户需求:
236
-
237
- ${conversationText}
238
-
239
- 总结要求:
240
- 1. 提取用户的主要需求和意图
241
- 2. 保留关键的技术细节或决策
242
- 3. 不要超过 1000 字
243
- 4. 用中文回复`
244
-
245
- // 使用 AI SDK 6.x 的 generateText
246
- const { generateText } = require('ai')
247
- const { text } = await generateText({
248
- model: this._aiClient,
249
- prompt: summarizePrompt,
250
- ...this.providerOptions
251
- })
252
-
253
- return text || '(总结生成失败)'
254
- }
255
-
256
- /**
257
- * 检查工具结果是否需要压缩
258
- * @param {any} result - 工具返回结果
259
- * @returns {boolean}
260
- * @private
261
- */
262
- _shouldCompressToolResult(result) {
263
- if (!result || this._maxToolResultSize <= 0) return false
264
-
265
- // 计算结果的大小
266
- let size = 0
267
- if (typeof result === 'string') {
268
- size = result.length
269
- } else if (typeof result === 'object') {
270
- try {
271
- size = JSON.stringify(result).length
272
- } catch {
273
- size = String(result).length
274
- }
275
- }
276
-
277
- return size > this._maxToolResultSize
278
- }
279
-
280
- /**
281
- * 压缩工具结果
282
- * @param {any} result - 工具返回结果
283
- * @returns {Promise<any>} 压缩后的结果
284
- * @private
285
- */
286
- async _compressToolResult(result) {
287
- if (!this._shouldCompressToolResult(result)) {
288
- return result
289
- }
290
-
291
- if (!this._aiClient) {
292
- console.warn('[AgentChat] Cannot compress tool result: no AI client')
293
- return this._fallbackCompress(result)
294
- }
295
-
296
- try {
297
- const originalSize = typeof result === 'string' ? result.length : JSON.stringify(result).length
298
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
299
-
300
- // 对于超大型内容(如网页),采用更好的截断策略
301
- const maxInputSize = 6000 // 保留给 AI 处理的输入大小
302
- const shouldTruncate = resultStr.length > maxInputSize
303
- const truncatedContent = resultStr.substring(0, maxInputSize)
304
- const truncatedNote = shouldTruncate ? `\n\n[内容已截断,原始长度 ${originalSize} 字符]` : ''
305
-
306
- // 检测内容类型
307
- const isHTML = resultStr.startsWith('<') || resultStr.includes('<html') || resultStr.includes('<!DOCTYPE')
308
- const isJSON = !isHTML && (resultStr.startsWith('{') || resultStr.startsWith('['))
309
- const contentTypeHint = isHTML ? '(HTML 网页内容)' : isJSON ? '(JSON 数据)' : ''
310
-
311
- // 构建压缩提示
312
- const compressPrompt = `以下是一个工具执行结果${contentTypeHint},长度 ${originalSize} 字符。请简洁地总结其核心内容:
313
-
314
- ${truncatedContent}${truncatedNote}
315
-
316
- 请提取并保留:
317
- 1. 主要标题和主题
318
- 2. 关键信息点(不超过 5 个)
319
- 3. 重要数据或结论
320
-
321
- 用简洁的中文总结,不超过 400 字:`
322
-
323
- // 使用 AI SDK 6.x 的 generateText
324
- const { generateText } = require('ai')
325
- const { text } = await generateText({
326
- model: this._aiClient,
327
- prompt: compressPrompt,
328
- ...this.providerOptions,
329
- maxTokens: 500
330
- })
331
-
332
- const summary = text || '(总结生成失败)'
333
- const compressed = `[工具结果已压缩${contentTypeHint}: ${originalSize} → ${summary.length} 字符]\n\n${summary}`
334
-
335
- console.log(`[AgentChat] Tool result compressed: ${originalSize} → ${summary.length} chars`)
336
- return compressed
337
- } catch (err) {
338
- console.warn('[AgentChat] Tool result compression failed:', err.message)
339
- return this._fallbackCompress(result)
340
- }
341
- }
342
-
343
- /**
344
- * 回退压缩方法(当 AI 客户端不可用时)
345
- * @param {any} result - 工具返回结果
346
- * @returns {string} 压缩后的结果
347
- * @private
348
- */
349
- _fallbackCompress(result) {
350
- const originalSize = typeof result === 'string' ? result.length : JSON.stringify(result).length
351
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
352
-
353
- // 简单截断策略:保留前 2000 字符
354
- const maxSize = 2000
355
- if (resultStr.length <= maxSize) {
356
- return result
357
- }
358
-
359
- const compressed = `[工具结果已压缩(简单截断): ${originalSize} → ${maxSize} 字符]\n\n${resultStr.substring(0, maxSize)}\n\n...[内容已截断,原文 ${originalSize} 字符]`
360
- console.log(`[AgentChat] Tool result fallback compressed: ${originalSize} → ${maxSize} chars`)
361
- return compressed
362
- }
363
-
364
- /**
365
- * 设置 AI 客户端
366
- * @param {Object} client - AI 模型客户端
367
- */
368
- setAIClient(client) {
369
- this._aiClient = client
370
- return this
371
- }
372
-
373
- /**
374
- * 设置系统提示
375
- * @param {string} prompt
376
- */
377
- setSystemPrompt(prompt) {
378
- this._systemPrompt = prompt
379
- return this
380
- }
381
-
382
- /**
383
- * 注册工具
384
- * @param {Object} toolDef - 工具定义
385
- */
386
- registerTool(toolDef) {
387
- this._tools.set(toolDef.name, toolDef)
388
- return this
389
- }
390
-
391
- /**
392
- * 清空对话历史
393
- */
394
- clearHistory() {
395
- this._messages = []
396
- this._compressionCount = 0
397
- return this
398
- }
399
-
400
- /**
401
- * 获取已注册的工具
402
- * @returns {Array}
403
- */
404
- getTools() {
405
- return Array.from(this._tools.values())
406
- }
407
-
408
- /**
409
- * 导入 AI SDK(动态导入)
410
- * @private
411
- */
412
- async _importAI() {
413
- try {
414
- const ai = require('ai')
415
- return {
416
- tool: ai.tool,
417
- ToolLoopAgent: ai.ToolLoopAgent
418
- }
419
- } catch (err) {
420
- throw new Error('AI SDK not found. Please install: npm install ai')
421
- }
422
- }
423
-
424
- /**
425
- * 发送消息(非流式)
426
- * @param {string|Object} message - 消息
427
- * @param {Object} options - 选项
428
- */
429
- async chat(message, options = {}) {
430
- const context = { sessionId: options.sessionId || null, isStream: false }
431
- const framework = this.agent.framework
432
- const self = this // 保存引用用于回调
433
-
434
- // 动态导入 AI SDK
435
- const { tool, ToolLoopAgent } = await this._importAI()
436
-
437
- const userMessage = typeof message === 'string'
438
- ? { role: 'user', content: message }
439
- : message
440
-
441
- this._messages.push(userMessage)
442
-
443
- // 检查是否需要压缩上下文(包括工具定义)
444
- const messagesTokens = this._countMessagesTokens(this._messages)
445
- const toolsTokens = this._countToolsTokens()
446
- const systemPromptTokens = this._countTokens(this._systemPrompt)
447
- const totalTokens = messagesTokens + toolsTokens + systemPromptTokens
448
- const limit = this._maxContextTokens * 0.7 // 降低到 70%,更早压缩
449
-
450
- if (totalTokens > limit) {
451
- console.log(`[AgentChat] Context large (${totalTokens}/${this._maxContextTokens} tokens = msgs:${messagesTokens} + tools:${toolsTokens} + sys:${systemPromptTokens}), compressing...`)
452
- await this._compressContext()
453
- } else {
454
- console.log(`[AgentChat] Context OK: ${totalTokens}/${this._maxContextTokens} tokens`)
455
- }
456
-
457
- const maxSteps = options.maxSteps || this._maxSteps
458
- const tools = this._getAITools(tool)
459
-
460
- if (!this._aiClient) {
461
- throw new Error('AI client not configured.')
462
- }
463
-
464
- const agent = new ToolLoopAgent({
465
- model: this._aiClient,
466
- instructions: this._systemPrompt,
467
- tools: tools,
468
- stopWhen: (step) => step.stepCount >= maxSteps
469
- })
470
-
471
- const messages = [
472
- ...this._cleanMessages(this._messages)
473
- ]
474
-
475
- try {
476
- // 使用 runWithContext 让工具执行时能获取 sessionId
477
- const result = await framework.runWithContext(context, async () => {
478
- return agent.generate({ messages, ...this.providerOptions })
479
- })
480
-
481
- // 处理返回的消息历史:只保留 user 和 assistant 消息,让 SDK 自动处理 tool 消息
482
- if (result.messages && Array.isArray(result.messages)) {
483
- // 只保留 user 和 assistant 消息,SDK 会自动维护 tool 消息
484
- this._messages = result.messages.filter(m => m.role === 'user' || m.role === 'assistant')
485
- } else if (result.text) {
486
- this._messages.push({
487
- role: 'assistant',
488
- content: result.text
489
- })
490
- }
491
-
492
- // 生成后检查:如果消息太长,下次需要压缩
493
- const afterTokens = this._countMessagesTokens(this._messages)
494
- if (afterTokens > this._maxContextTokens * 0.8) {
495
- console.log(`[AgentChat] After generation: ${afterTokens} tokens, will compress on next turn`)
496
- }
497
-
498
- return {
499
- success: true,
500
- message: result.text || '',
501
- stepCount: result.stepCount || 1
502
- }
503
- } catch (err) {
504
- this.emit('error', { error: err.message })
505
- // 抛出错误而不是返回错误响应,让 Agent 能捕获
506
- throw err
507
- }
508
- }
509
-
510
- /**
511
- * 发送消息(流式)
512
- * @param {string|Object} message - 消息
513
- * @param {Object} options - 选项
514
- */
515
- async *chatStream(message, options = {}) {
516
- const context = { sessionId: options.sessionId || null, isStream: true }
517
- const framework = this.agent.framework
518
-
519
- // 动态导入 AI SDK
520
- const { tool, ToolLoopAgent } = await this._importAI()
521
-
522
- const userMessage = typeof message === 'string'
523
- ? { role: 'user', content: message }
524
- : message
525
-
526
- this._messages.push(userMessage)
527
-
528
- // 检查是否需要压缩上下文(包括工具定义)
529
- const messagesTokens = this._countMessagesTokens(this._messages)
530
- const toolsTokens = this._countToolsTokens()
531
- const systemPromptTokens = this._countTokens(this._systemPrompt)
532
- const totalTokens = messagesTokens + toolsTokens + systemPromptTokens
533
- const limit = this._maxContextTokens * 0.7 // 降低到 70%
534
-
535
- // 对于流式调用,如果上下文太大,先压缩再开始
536
- if (totalTokens > limit) {
537
- console.log(`[AgentChat] Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`)
538
- // 流式调用时等待压缩完成
539
- await this._compressContext()
540
- }
541
-
542
- const maxSteps = options.maxSteps || this._maxSteps
543
- const tools = this._getAITools(tool)
544
- const self = this // 保存引用用于回调
545
-
546
- if (!this._aiClient) {
547
- throw new Error('AI client not configured.')
548
- }
549
-
550
- const agent = new ToolLoopAgent({
551
- model: this._aiClient,
552
- instructions: this._systemPrompt,
553
- tools: tools,
554
- stopWhen: (step) => step.stepCount >= maxSteps
555
- })
556
-
557
- const messages = [
558
- ...this._cleanMessages(this._messages)
559
- ]
560
-
561
- try {
562
- // 使用 runWithContext 让工具执行时能获取 sessionId(支持并行)
563
- const result = await framework.runWithContext(context, async () => {
564
- return agent.stream({ messages, ...this.providerOptions })
565
- })
566
-
567
- const stream = result.fullStream
568
- let fullText = ''
569
-
570
- // 流式迭代器
571
- const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream
572
- const finalMessages = []
573
-
574
- for await (const part of (iterator || stream)) {
575
- if (part.type === 'text-delta') {
576
- const text = part.text || part.textDelta || ''
577
- fullText += text
578
- yield { type: 'text', text }
579
- } else if (part.type === 'reasoning') {
580
- this.emit('thinking', { content: part.text })
581
- } else if (part.type === 'tool-call') {
582
- yield { type: 'tool-call', toolName: part.toolName, args: part.input }
583
- } else if (part.type === 'tool-result') {
584
- // 保存到临时消息列表
585
- finalMessages.push({ role: 'tool', content: part.output, toolName: part.toolName })
586
- yield { type: 'tool-result', toolName: part.toolName, result: part.output }
587
- } else if (part.type === 'error') {
588
- yield { type: 'error', error: part.error }
589
- }
590
- }
591
-
592
- // 暂时禁用压缩以调试 schema 问题
593
- // for (const msg of finalMessages) {
594
- // if (msg.content) {
595
- // const contentSize = typeof msg.content === 'string' ? msg.content.length : JSON.stringify(msg.content).length
596
- // if (contentSize > self._maxToolResultSize) {
597
- // msg.content = await self._compressToolResult(msg.content)
598
- // }
599
- // }
600
- // }
601
-
602
- // 只保留 user 和 assistant 消息,让 SDK 自动处理 tool 消息
603
- const assistantMsg = { role: 'assistant', content: fullText }
604
- this._messages.push(assistantMsg)
605
- } catch (err) {
606
- this.emit('error', { error: err.message })
607
- yield { type: 'error', error: err.message }
608
- }
609
- }
610
-
611
- /**
612
- * 获取 AI SDK 格式的工具
613
- * AI SDK 6.x 需要对象形式 { toolName: tool }
614
- * @param {Function} toolFn - AI SDK 的 tool 函数
615
- * @private
616
- */
617
- _getAITools(toolFn) {
618
- const tools = {}
619
-
620
- for (const [name, toolDef] of this._tools) {
621
- const toolName = toolDef.name || name
622
-
623
- // 使用 AI SDK tool() 格式
624
- // 支持 inputSchema (zod schema) parameters (旧格式)
625
- const toolConfig = {
626
- name: toolName,
627
- description: toolDef.description || '',
628
- execute: async (args) => {
629
- // 清理参数:移除 undefined、function 等无效值
630
- const cleanedArgs = this._cleanToolArgs(args)
631
-
632
- // 执行工具
633
- this.emit('tool-call', { name: toolName, args: cleanedArgs })
634
- try {
635
- const result = await toolDef.execute(cleanedArgs, this.agent.framework)
636
- this.emit('tool-result', { name: toolName, args: cleanedArgs, result })
637
- return result
638
- } catch (err) {
639
- this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message })
640
- return { error: err.message }
641
- }
642
- }
643
- }
644
-
645
- // 支持 inputSchema (zod) 或 parameters (旧格式)
646
- if (toolDef.inputSchema) {
647
- toolConfig.inputSchema = toolDef.inputSchema
648
- } else if (toolDef.parameters) {
649
- toolConfig.parameters = toolDef.parameters
650
- }
651
-
652
- // AI SDK 6.x 使用对象形式,键为工具名
653
- tools[toolName] = toolFn(toolConfig)
654
- }
655
-
656
- return tools
657
- }
658
-
659
- /**
660
- * 清理工具参数,移除无效值
661
- * @param {Object} args - 原始参数
662
- * @returns {Object} 清理后的参数
663
- * @private
664
- */
665
- _cleanToolArgs(args) {
666
- if (!args || typeof args !== 'object') {
667
- return {}
668
- }
669
-
670
- const cleaned = {}
671
- for (const [key, value] of Object.entries(args)) {
672
- // 跳过 undefined、function、symbol 等无效值
673
- if (value === undefined || value === null) {
674
- continue
675
- }
676
- if (typeof value === 'function' || typeof value === 'symbol') {
677
- continue
678
- }
679
- // 递归清理嵌套对象
680
- if (typeof value === 'object' && !Array.isArray(value)) {
681
- cleaned[key] = this._cleanToolArgs(value)
682
- } else if (Array.isArray(value)) {
683
- cleaned[key] = value.map(item =>
684
- typeof item === 'object' && item !== null
685
- ? this._cleanToolArgs(item)
686
- : item
687
- ).filter(item => item !== undefined && typeof item !== 'function')
688
- } else {
689
- cleaned[key] = value
690
- }
691
- }
692
- return cleaned
693
- }
694
-
695
- /**
696
- * 清理消息格式
697
- * @private
698
- */
699
- _cleanMessages(messages) {
700
- return messages.map(msg => {
701
- if (!msg || typeof msg !== 'object') {
702
- return { role: 'user', content: '' }
703
- }
704
-
705
- const cleaned = {
706
- role: msg.role || 'user'
707
- }
708
-
709
- if (Array.isArray(msg.content)) {
710
- cleaned.content = msg.content
711
- } else if (msg.content !== undefined) {
712
- cleaned.content = msg.content
713
- } else {
714
- cleaned.content = msg.text || msg.input || ''
715
- }
716
-
717
- return cleaned
718
- })
719
- }
720
-
721
- /**
722
- * 销毁
723
- */
724
- destroy() {
725
- this._messages = []
726
- this._tools.clear()
727
- if (this._encoder) {
728
- this._encoder.free()
729
- this._encoder = null
730
- }
731
- this.removeAllListeners()
732
- }
733
- }
734
-
735
- module.exports = { AgentChatHandler }
1
+ /**
2
+ * AgentChatHandler 聊天处理器
3
+ * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环
4
+ */
5
+
6
+ const { EventEmitter } = require('../utils/event-emitter');
7
+ const { logger } = require('../utils/logger');
8
+ const { generateText, pruneMessages } = require('ai');
9
+
10
+ // 模型上下文限制表(留 15-20% 余量给 system prompt 和输出)
11
+ const MODEL_CONTEXT_LIMITS = {
12
+ // DeepSeek
13
+ 'deepseek-chat': 28000,
14
+ 'deepseek-coder': 28000,
15
+ // MiniMax
16
+ 'MiniMax-M2.7': 90000,
17
+ // OpenAI
18
+ 'gpt-4': 100000,
19
+ 'gpt-4o': 100000,
20
+ 'gpt-4o-mini': 100000,
21
+ 'gpt-4-turbo': 100000,
22
+ // Anthropic
23
+ 'claude-3-5-sonnet': 150000,
24
+ 'claude-3-opus': 150000,
25
+ 'claude-3-sonnet': 150000,
26
+ };
27
+
28
+ /**
29
+ * JavaScript token 计数器
30
+ * 基于字符数估算,兼容中英文
31
+ * 估算规则:
32
+ * - 英文:约 4 字符 = 1 token
33
+ * - 中文:约 2 字符 = 1 token
34
+ * - 混合文本取加权平均
35
+ */
36
+ class SimpleTokenizer {
37
+ constructor() {
38
+ // 无需初始化
39
+ }
40
+
41
+ /**
42
+ * 估算文本的 token
43
+ * @param {string} text
44
+ * @returns {number}
45
+ */
46
+ encode(text) {
47
+ if (!text || typeof text !== 'string') {
48
+ return 0;
49
+ }
50
+
51
+ // 清理文本
52
+ const cleanText = text
53
+ .replace(/\0+/g, '')
54
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
55
+ .slice(0, 100000);
56
+
57
+ if (!cleanText) {
58
+ return 0;
59
+ }
60
+
61
+ // 分离中英文分别计数
62
+ const chineseChars = (cleanText.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
63
+ const englishChars = cleanText.length - chineseChars;
64
+
65
+ // 中英文分开估算后相加
66
+ // 中文约 2 字符/token,英文约 4 字符/token
67
+ const chineseTokens = chineseChars / 2;
68
+ const englishTokens = englishChars / 4;
69
+
70
+ return Math.ceil(chineseTokens + englishTokens);
71
+ }
72
+ }
73
+
74
+ // 全局 tokenizer 实例
75
+ const _globalTokenizer = new SimpleTokenizer();
76
+
77
+ // 压缩超时时间(毫秒)
78
+ const COMPRESSION_TIMEOUT = 30000;
79
+
80
+ class AgentChatHandler extends EventEmitter {
81
+ /**
82
+ * @param {Agent} agent - Agent 实例
83
+ * @param {Object} config - 配置
84
+ */
85
+ constructor(agent, config = {}) {
86
+ super();
87
+
88
+ this.agent = agent;
89
+ this.config = config;
90
+
91
+ this.model = config.model || 'deepseek-chat';
92
+ this.provider = config.provider || 'deepseek';
93
+ this.apiKey = config.apiKey;
94
+ this.baseURL = config.baseURL;
95
+ this.providerOptions = config.providerOptions || {};
96
+
97
+ this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';
98
+ this._messages = [];
99
+ this._tools = new Map();
100
+ this._maxSteps = 5; // 降低默认步骤数,减少上下文消耗
101
+
102
+ // 上下文压缩配置:根据模型自动设置限制
103
+ const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find((k) =>
104
+ this.model.toLowerCase().includes(k.toLowerCase())
105
+ );
106
+ const defaultLimit = modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;
107
+ this._maxContextTokens = config.maxContextTokens || defaultLimit;
108
+ this._compressionThreshold = config.compressionThreshold || 0.6; // 60% 就压缩,早触发
109
+ this._keepRecentMessages = config.keepRecentMessages || 20; // 保留最近 20 条
110
+ this._enableSmartCompress = config.enableSmartCompress !== false; // 默认开启智能摘要
111
+ this._encoder = null;
112
+ this._compressionCount = 0; // 压缩次数统计
113
+ this._compressionInProgress = false; // 压缩锁,防止并发压缩
114
+ this._compressionPromise = null; // 正在进行的压缩 Promise
115
+
116
+ // 工具结果压缩配置
117
+ this._maxToolResultSize = config.maxToolResultSize || 4000; // 工具结果超过此大小则压缩(字节)
118
+
119
+ // 初始化编码器
120
+ // 使用纯 JS tokenizer
121
+ this._encoder = _globalTokenizer;
122
+ }
123
+
124
+ /**
125
+ * 静态方法:保留接口兼容性(无实际作用)
126
+ */
127
+ static clearEncoderCache() {
128
+ // 不再需要清理
129
+ }
130
+
131
+ /**
132
+ * 计算文本的 token
133
+ * @param {string} text
134
+ * @returns {number}
135
+ * @private
136
+ */
137
+ _countTokens(text) {
138
+ if (!text || typeof text !== 'string') return 0;
139
+ // SimpleTokenizer.encode 已经处理了清理逻辑
140
+ try {
141
+ return this._encoder.encode(text);
142
+ } catch (err) {
143
+ // 估算失败时使用字符计数
144
+ return Math.ceil(text.length / 4);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 计算消息列表的总 token 数
150
+ * @param {Array} messages
151
+ * @returns {number}
152
+ * @private
153
+ */
154
+ _countMessagesTokens(messages) {
155
+ let total = 0;
156
+ for (const msg of messages) {
157
+ if (!msg) continue;
158
+ total += 4; // role 标记
159
+ if (typeof msg.content === 'string') {
160
+ total += this._countTokens(msg.content);
161
+ } else if (Array.isArray(msg.content)) {
162
+ for (const part of msg.content) {
163
+ if (part && typeof part === 'object' && part.text) {
164
+ total += this._countTokens(String(part.text));
165
+ }
166
+ }
167
+ } else if (msg.content && typeof msg.content === 'object') {
168
+ // 处理工具结果等对象类型
169
+ try {
170
+ const str = JSON.stringify(msg.content);
171
+ total += this._countTokens(str);
172
+ } catch (e) {
173
+ // 无法序列化的内容,忽略
174
+ }
175
+ }
176
+ }
177
+ total += 4; // 结尾标记
178
+ return total;
179
+ }
180
+
181
+ /**
182
+ * 检查是否需要压缩上下文
183
+ * @returns {boolean}
184
+ * @private
185
+ */
186
+ _shouldCompress() {
187
+ const totalTokens = this._countMessagesTokens(this._messages);
188
+ return totalTokens > this._maxContextTokens * this._compressionThreshold;
189
+ }
190
+
191
+ /**
192
+ * 计算工具定义的 token 数(估算)
193
+ * @returns {number}
194
+ * @private
195
+ */
196
+ _countToolsTokens() {
197
+ let total = 0;
198
+ for (const toolDef of this._tools.values()) {
199
+ // 工具名 + 描述
200
+ total += this._countTokens(String(toolDef.name || ''));
201
+ total += this._countTokens(String(toolDef.description || ''));
202
+
203
+ // 工具参数 schema
204
+ if (toolDef.inputSchema) {
205
+ const schemaStr =
206
+ typeof toolDef.inputSchema === 'string'
207
+ ? toolDef.inputSchema
208
+ : JSON.stringify(toolDef.inputSchema);
209
+ total += this._countTokens(schemaStr);
210
+ }
211
+ }
212
+ return total;
213
+ }
214
+
215
+ /**
216
+ * 检查是否需要压缩(包括工具定义)
217
+ * @returns {boolean}
218
+ * @private
219
+ */
220
+ _shouldCompressWithTools() {
221
+ const messagesTokens = this._countMessagesTokens(this._messages);
222
+ const toolsTokens = this._countToolsTokens();
223
+ const systemPromptTokens = this._countTokens(this._systemPrompt);
224
+ const total = messagesTokens + toolsTokens + systemPromptTokens;
225
+
226
+ // 如果总token数超过上下文限制的 85%,就压缩
227
+ return total > this._maxContextTokens * 0.85;
228
+ }
229
+
230
+ /**
231
+ * 压缩上下文消息(智能摘要模式,支持超时控制)
232
+ * 策略:
233
+ * 1. 如果启用了智能摘要且有 AI 客户端,对早期消息进行 AI 总结
234
+ * 2. 否则使用简单裁剪 + 标记
235
+ * 3. 支持超时控制,防止压缩阻塞太久
236
+ * @private
237
+ */
238
+ async _compressContext() {
239
+ // 如果已经有压缩在进行,返回现有的 Promise
240
+ if (this._compressionInProgress && this._compressionPromise) {
241
+ logger.debug('Compression already in progress, waiting for it...');
242
+ return this._compressionPromise;
243
+ }
244
+
245
+ if (this._messages.length <= this._keepRecentMessages) {
246
+ return;
247
+ }
248
+
249
+ this._compressionInProgress = true;
250
+
251
+ // 创建压缩 Promise,带超时控制
252
+ this._compressionPromise = this._executeCompressionWithTimeout().finally(() => {
253
+ this._compressionInProgress = false;
254
+ this._compressionPromise = null;
255
+ });
256
+
257
+ return this._compressionPromise;
258
+ }
259
+
260
+ /**
261
+ * 执行压缩(带超时)
262
+ * @private
263
+ */
264
+ async _executeCompressionWithTimeout() {
265
+ try {
266
+ return await Promise.race([this._doCompress(), this._createTimeoutPromise()]);
267
+ } catch (err) {
268
+ logger.warn('Compression failed:', err.message);
269
+ // 压缩失败时使用简单的截断策略
270
+ this._simpleCompress();
271
+ }
272
+ }
273
+
274
+ /**
275
+ * 创建超时 Promise
276
+ * @private
277
+ */
278
+ _createTimeoutPromise() {
279
+ return new Promise((_, reject) => {
280
+ setTimeout(() => {
281
+ reject(new Error('Compression timeout'));
282
+ }, COMPRESSION_TIMEOUT);
283
+ });
284
+ }
285
+
286
+ /**
287
+ * 执行实际压缩逻辑
288
+ * @private
289
+ */
290
+ async _doCompress() {
291
+ const systemMessages = this._messages.filter((m) => m.role === 'system');
292
+ const otherMessages = this._messages.filter((m) => m.role !== 'system');
293
+
294
+ // 保留最近的 N 条非系统消息
295
+ const recentMessages = otherMessages.slice(-this._keepRecentMessages);
296
+ const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
297
+
298
+ const compressedCount = messagesToSummarize.length;
299
+ let summaryContent = '';
300
+
301
+ // 尝试使用 AI 总结
302
+ if (this._enableSmartCompress && this._aiClient) {
303
+ try {
304
+ const summaryText = await this._summarizeMessages(messagesToSummarize);
305
+ summaryContent = `[早期对话摘要]: ${summaryText}`;
306
+ logger.info(`AI summary generated (${summaryText.length} chars)`);
307
+ } catch (err) {
308
+ logger.warn('AI summary failed, using simple compression:', err.message);
309
+ summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
310
+ }
311
+ } else {
312
+ summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
313
+ }
314
+
315
+ const summary = {
316
+ role: 'system',
317
+ content: summaryContent,
318
+ };
319
+
320
+ this._messages = [...systemMessages, summary, ...recentMessages];
321
+ this._compressionCount++;
322
+
323
+ const totalTokens = this._countMessagesTokens(this._messages);
324
+ logger.info(
325
+ `Context compressed (${this._compressionCount} times). Messages: ${this._messages.length}, Est. tokens: ${totalTokens}`
326
+ );
327
+ }
328
+
329
+ /**
330
+ * 简单压缩(当 AI 压缩失败时使用)
331
+ * @private
332
+ */
333
+ _simpleCompress() {
334
+ const systemMessages = this._messages.filter((m) => m.role === 'system');
335
+ const otherMessages = this._messages.filter((m) => m.role !== 'system');
336
+ const recentMessages = otherMessages.slice(-this._keepRecentMessages);
337
+ const compressedCount = otherMessages.length - this._keepRecentMessages;
338
+
339
+ const summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
340
+
341
+ const summary = {
342
+ role: 'system',
343
+ content: summaryContent,
344
+ };
345
+
346
+ this._messages = [...systemMessages, summary, ...recentMessages];
347
+ this._compressionCount++;
348
+
349
+ logger.info(
350
+ `Context simple compressed (${this._compressionCount} times). Messages: ${this._messages.length}`
351
+ );
352
+ }
353
+
354
+ /**
355
+ * 使用 AI 对消息进行总结
356
+ * @param {Array} messages - 要总结的消息
357
+ * @returns {Promise<string>} 总结文本
358
+ * @private
359
+ */
360
+ async _summarizeMessages(messages) {
361
+ if (!this._aiClient || messages.length === 0) {
362
+ return '(无早期对话)';
363
+ }
364
+
365
+ // 构建总结提示
366
+ const conversationText = messages
367
+ .map((m) => {
368
+ const role = m.role === 'user' ? '用户' : '助手';
369
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
370
+ return `${role}: ${content}`;
371
+ })
372
+ .join('\n');
373
+
374
+ const summarizePrompt = `请简洁地总结以下对话的要点,保留关键信息和用户需求:
375
+
376
+ ${conversationText}
377
+
378
+ 总结要求:
379
+ 1. 提取用户的主要需求和意图
380
+ 2. 保留关键的技术细节或决策
381
+ 3. 不要超过 1000 字
382
+ 4. 用中文回复`;
383
+
384
+ // 使用 AI SDK 6.x 的 generateText
385
+ const { text } = await generateText({
386
+ model: this._aiClient,
387
+ prompt: summarizePrompt,
388
+ ...this.providerOptions,
389
+ });
390
+
391
+ return text || '(总结生成失败)';
392
+ }
393
+
394
+ /**
395
+ * 检查工具结果是否需要压缩
396
+ * @param {any} result - 工具返回结果
397
+ * @returns {boolean}
398
+ * @private
399
+ */
400
+ _shouldCompressToolResult(result) {
401
+ if (!result || this._maxToolResultSize <= 0) return false;
402
+
403
+ // 计算结果的大小
404
+ let size = 0;
405
+ if (typeof result === 'string') {
406
+ size = result.length;
407
+ } else if (typeof result === 'object') {
408
+ try {
409
+ size = JSON.stringify(result).length;
410
+ } catch {
411
+ size = String(result).length;
412
+ }
413
+ }
414
+
415
+ return size > this._maxToolResultSize;
416
+ }
417
+
418
+ /**
419
+ * 压缩工具结果
420
+ * @param {any} result - 工具返回结果
421
+ * @returns {Promise<any>} 压缩后的结果
422
+ * @private
423
+ */
424
+ async _compressToolResult(result) {
425
+ if (!this._shouldCompressToolResult(result)) {
426
+ return result;
427
+ }
428
+
429
+ if (!this._aiClient) {
430
+ logger.warn('Cannot compress tool result: no AI client');
431
+ return this._fallbackCompress(result);
432
+ }
433
+
434
+ try {
435
+ const originalSize =
436
+ typeof result === 'string' ? result.length : JSON.stringify(result).length;
437
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
438
+
439
+ // 对于超大型内容(如网页),采用更好的截断策略
440
+ const maxInputSize = 6000; // 保留给 AI 处理的输入大小
441
+ const shouldTruncate = resultStr.length > maxInputSize;
442
+ const truncatedContent = resultStr.substring(0, maxInputSize);
443
+ const truncatedNote = shouldTruncate ? `\n\n[内容已截断,原始长度 ${originalSize} 字符]` : '';
444
+
445
+ // 检测内容类型
446
+ const isHTML =
447
+ resultStr.startsWith('<') || resultStr.includes('<html') || resultStr.includes('<!DOCTYPE');
448
+ const isJSON = !isHTML && (resultStr.startsWith('{') || resultStr.startsWith('['));
449
+ const contentTypeHint = isHTML ? '(HTML 网页内容)' : isJSON ? '(JSON 数据)' : '';
450
+
451
+ // 构建压缩提示
452
+ const compressPrompt = `以下是一个工具执行结果${contentTypeHint},长度 ${originalSize} 字符。请简洁地总结其核心内容:
453
+
454
+ ${truncatedContent}${truncatedNote}
455
+
456
+ 请提取并保留:
457
+ 1. 主要标题和主题
458
+ 2. 关键信息点(不超过 5 个)
459
+ 3. 重要数据或结论
460
+
461
+ 用简洁的中文总结,不超过 400 字:`;
462
+
463
+ // 使用 AI SDK 6.x 的 generateText
464
+ const { text } = await generateText({
465
+ model: this._aiClient,
466
+ prompt: compressPrompt,
467
+ ...this.providerOptions,
468
+ maxTokens: 500,
469
+ });
470
+
471
+ const summary = text || '(总结生成失败)';
472
+ const compressed = `[工具结果已压缩${contentTypeHint}: ${originalSize} -> ${summary.length} 字符]\n\n${summary}`;
473
+
474
+ logger.info(`Tool result compressed: ${originalSize} -> ${summary.length} chars`);
475
+ return compressed;
476
+ } catch (err) {
477
+ logger.warn('Tool result compression failed:', err.message);
478
+ return this._fallbackCompress(result);
479
+ }
480
+ }
481
+
482
+ /**
483
+ * 回退压缩方法(当 AI 客户端不可用时)
484
+ * @param {any} result - 工具返回结果
485
+ * @returns {string} 压缩后的结果
486
+ * @private
487
+ */
488
+ _fallbackCompress(result) {
489
+ const originalSize = typeof result === 'string' ? result.length : JSON.stringify(result).length;
490
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
491
+
492
+ // 简单截断策略:保留前 2000 字符
493
+ const maxSize = 2000;
494
+ if (resultStr.length <= maxSize) {
495
+ return result;
496
+ }
497
+
498
+ const compressed = `[工具结果已压缩(简单截断): ${originalSize} -> ${maxSize} 字符]\n\n${resultStr.substring(0, maxSize)}\n\n...[内容已截断,原文 ${originalSize} 字符]`;
499
+ logger.info(`Tool result fallback compressed: ${originalSize} -> ${maxSize} chars`);
500
+ return compressed;
501
+ }
502
+
503
+ /**
504
+ * 设置 AI 客户端
505
+ * @param {Object} client - AI 模型客户端
506
+ */
507
+ setAIClient(client) {
508
+ this._aiClient = client;
509
+ return this;
510
+ }
511
+
512
+ /**
513
+ * 设置系统提示
514
+ * @param {string} prompt
515
+ */
516
+ setSystemPrompt(prompt) {
517
+ this._systemPrompt = prompt;
518
+ return this;
519
+ }
520
+
521
+ /**
522
+ * 注册工具
523
+ * @param {Object} toolDef - 工具定义
524
+ */
525
+ registerTool(toolDef) {
526
+ this._tools.set(toolDef.name, toolDef);
527
+ return this;
528
+ }
529
+
530
+ /**
531
+ * 清空对话历史
532
+ */
533
+ clearHistory() {
534
+ this._messages = [];
535
+ this._compressionCount = 0;
536
+ return this;
537
+ }
538
+
539
+ /**
540
+ * 获取已注册的工具
541
+ * @returns {Array}
542
+ */
543
+ getTools() {
544
+ return Array.from(this._tools.values());
545
+ }
546
+
547
+ /**
548
+ * 导入 AI SDK(动态导入)
549
+ * @private
550
+ */
551
+ async _importAI() {
552
+ try {
553
+ const ai = require('ai');
554
+ return {
555
+ tool: ai.tool,
556
+ ToolLoopAgent: ai.ToolLoopAgent,
557
+ };
558
+ } catch (err) {
559
+ throw new Error('AI SDK not found. Please install: npm install ai');
560
+ }
561
+ }
562
+
563
+ /**
564
+ * 发送消息(非流式)
565
+ * @param {string|Object} message - 消息
566
+ * @param {Object} options - 选项
567
+ */
568
+ async chat(message, options = {}) {
569
+ const context = { sessionId: options.sessionId || null, isStream: false };
570
+ const framework = this.agent.framework;
571
+ const self = this; // 保存引用用于回调
572
+
573
+ // 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
574
+ // 解决上下文过长时 LLM 不调用工具的问题
575
+ this._systemPrompt = this.agent._buildSystemPrompt();
576
+ // 动态导入 AI SDK
577
+ const { tool, ToolLoopAgent } = await this._importAI();
578
+
579
+ const userMessage = typeof message === 'string' ? { role: 'user', content: message } : message;
580
+
581
+ this._messages.push(userMessage);
582
+
583
+ // 检查是否需要压缩上下文(包括工具定义)
584
+ const messagesTokens = this._countMessagesTokens(this._messages);
585
+ const toolsTokens = this._countToolsTokens();
586
+ const systemPromptTokens = this._countTokens(this._systemPrompt);
587
+ const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
588
+ const limit = this._maxContextTokens * 0.7; // 降低到 70%,更早压缩
589
+
590
+ if (totalTokens > limit) {
591
+ logger.info(
592
+ `Context large (${totalTokens}/${this._maxContextTokens} tokens = msgs:${messagesTokens} + tools:${toolsTokens} + sys:${systemPromptTokens}), compressing...`
593
+ );
594
+ // 使用带超时控制的压缩
595
+ await this._compressContext();
596
+ } else {
597
+ logger.info(`Context OK: ${totalTokens}/${this._maxContextTokens} tokens`);
598
+ }
599
+
600
+ const maxSteps = options.maxSteps || this._maxSteps;
601
+ const tools = this._getAITools(tool);
602
+
603
+ if (!this._aiClient) {
604
+ throw new Error('AI client not configured.');
605
+ }
606
+
607
+ const agent = new ToolLoopAgent({
608
+ model: this._aiClient,
609
+ instructions: this._systemPrompt,
610
+ tools: tools,
611
+ stopWhen: (step) => step.stepCount >= maxSteps,
612
+ });
613
+
614
+ const messages = this._cleanMessages(this._messages);
615
+ const prunedMessages = pruneMessages({
616
+ messages,
617
+ reasoning: 'all', // Remove all reasoning parts
618
+ toolCalls: 'before-last-2-messages', // Remove tool calls except those in the last message
619
+ });
620
+
621
+ try {
622
+ // 使用 runWithContext 让工具执行时能获取 sessionId
623
+ const result = await framework.runWithContext(context, async () => {
624
+ return agent.generate({ messages: prunedMessages, ...this.providerOptions });
625
+ });
626
+
627
+ // 处理返回的消息历史:只保留 user 和 assistant 消息,让 SDK 自动处理 tool 消息
628
+ if (result.messages && Array.isArray(result.messages)) {
629
+ // 只保留 user 和 assistant 消息,SDK 会自动维护 tool 消息
630
+ this._messages = result.messages.filter((m) => m.role === 'user' || m.role === 'assistant');
631
+ } else if (result.text) {
632
+ this._messages.push({
633
+ role: 'assistant',
634
+ content: result.text,
635
+ });
636
+ }
637
+
638
+ // 生成后检查:如果消息太长,下次需要压缩
639
+ const afterTokens = this._countMessagesTokens(this._messages);
640
+ if (afterTokens > this._maxContextTokens * 0.8) {
641
+ logger.info(`After generation: ${afterTokens} tokens, will compress on next turn`);
642
+ }
643
+
644
+ return {
645
+ success: true,
646
+ message: result.text || '',
647
+ stepCount: result.stepCount || 1,
648
+ };
649
+ } catch (err) {
650
+ this.emit('error', { error: err.message });
651
+ // 抛出错误而不是返回错误响应,让 Agent 能捕获
652
+ throw err;
653
+ }
654
+ }
655
+
656
+ /**
657
+ * 发送消息(流式)
658
+ * @param {string|Object} message - 消息
659
+ * @param {Object} options - 选项
660
+ */
661
+ async *chatStream(message, options = {}) {
662
+ const context = { sessionId: options.sessionId || null, isStream: true };
663
+ const framework = this.agent.framework;
664
+
665
+ // 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
666
+ // 解决上下文过长时 LLM 不调用工具的问题
667
+ this._systemPrompt = this.agent._buildSystemPrompt();
668
+ // 动态导入 AI SDK
669
+ const { tool, ToolLoopAgent } = await this._importAI();
670
+
671
+ const userMessage = typeof message === 'string' ? { role: 'user', content: message } : message;
672
+ // console.log('System Prompt:', this._systemPrompt);
673
+ this._messages.push(userMessage);
674
+ // 检查是否需要压缩上下文(包括工具定义)
675
+ const messagesTokens = this._countMessagesTokens(this._messages);
676
+ const toolsTokens = this._countToolsTokens();
677
+ const systemPromptTokens = this._countTokens(this._systemPrompt);
678
+ const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
679
+ const limit = this._maxContextTokens * 0.7; // 降低到 70%
680
+
681
+ // 对于流式调用,如果上下文太大,先压缩再开始
682
+ if (totalTokens > limit) {
683
+ logger.info(
684
+ `Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
685
+ );
686
+ // 流式调用时等待压缩完成(使用带超时控制的压缩)
687
+ await this._compressContext();
688
+ }
689
+
690
+ const maxSteps = options.maxSteps || this._maxSteps;
691
+ const tools = this._getAITools(tool);
692
+ const self = this; // 保存引用用于回调
693
+
694
+ if (!this._aiClient) {
695
+ throw new Error('AI client not configured.');
696
+ }
697
+
698
+ const agent = new ToolLoopAgent({
699
+ model: this._aiClient,
700
+ instructions: this._systemPrompt,
701
+ tools: tools,
702
+ stopWhen: (step) => step.stepCount >= maxSteps,
703
+ });
704
+
705
+ const messages = this._cleanMessages(this._messages);
706
+ const prunedMessages = pruneMessages({
707
+ messages,
708
+ reasoning: 'all', // Remove all reasoning parts
709
+ toolCalls: 'before-last-2-messages', // Remove tool calls except those in the last message
710
+ });
711
+ try {
712
+ // 使用 runWithContext 让工具执行时能获取 sessionId(支持并行)
713
+ const result = await framework.runWithContext(context, async () => {
714
+ return agent.stream({ messages: prunedMessages, ...this.providerOptions });
715
+ });
716
+
717
+ const stream = result.fullStream;
718
+ let fullText = '';
719
+
720
+ // 流式迭代器
721
+ const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;
722
+ const finalMessages = [];
723
+
724
+ for await (const part of iterator || stream) {
725
+ if (part.type === 'text-delta') {
726
+ const text = part.text || part.textDelta || '';
727
+ fullText += text;
728
+ yield { type: 'text', text };
729
+ } else if (part.type === 'reasoning') {
730
+ finalMessages.push(part);
731
+ this.emit('thinking', { content: part.text });
732
+ } else if (part.type === 'tool-call') {
733
+ finalMessages.push(part);
734
+ yield { type: 'tool-call', toolName: part.toolName, args: part.input };
735
+ } else if (part.type === 'tool-result') {
736
+ // AI SDK 6.x 要求 tool 消息格式为:
737
+ // { role: 'tool', content: [{ type: 'tool-result', toolCallId, toolName, output }] }
738
+ finalMessages.push(part); // 先保存到 finalMessages,等生成结束后统一添加到历史
739
+ yield { type: 'tool-result', toolName: part.toolName, result: part.output };
740
+ } else if (part.type === 'error') {
741
+ yield { type: 'error', error: part.error };
742
+ }
743
+ }
744
+
745
+ this._messages.push({ role: 'tool', content: finalMessages });
746
+ // 添加 assistant 消息
747
+ const assistantMsg = { role: 'assistant', content: fullText };
748
+ this._messages.push(assistantMsg);
749
+ } catch (err) {
750
+ this.emit('error', { error: err.message });
751
+ yield { type: 'error', error: err.message };
752
+ }
753
+ }
754
+
755
+ /**
756
+ * 获取 AI SDK 格式的工具
757
+ * AI SDK 6.x 需要对象形式 { toolName: tool }
758
+ * @param {Function} toolFn - AI SDK 的 tool 函数
759
+ * @private
760
+ */
761
+ _getAITools(toolFn) {
762
+ const tools = {};
763
+
764
+ for (const [name, toolDef] of this._tools) {
765
+ const toolName = toolDef.name || name;
766
+
767
+ // 使用 AI SDK 的 tool() 格式
768
+ // 支持 inputSchema (zod schema) 或 parameters (旧格式)
769
+ const toolConfig = {
770
+ name: toolName,
771
+ description: toolDef.description || '',
772
+ execute: async (args) => {
773
+ // 清理参数:移除 undefined、function 等无效值
774
+ const cleanedArgs = this._cleanToolArgs(args);
775
+
776
+ // 执行工具
777
+ this.emit('tool-call', { name: toolName, args: cleanedArgs });
778
+ try {
779
+ const result = await toolDef.execute(cleanedArgs, this.agent.framework);
780
+ this.emit('tool-result', { name: toolName, args: cleanedArgs, result });
781
+ return result;
782
+ } catch (err) {
783
+ this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
784
+ return { error: err.message };
785
+ }
786
+ },
787
+ };
788
+
789
+ // 支持 inputSchema (zod) 或 parameters (旧格式)
790
+ if (toolDef.inputSchema) {
791
+ toolConfig.inputSchema = toolDef.inputSchema;
792
+ } else if (toolDef.parameters) {
793
+ toolConfig.parameters = toolDef.parameters;
794
+ }
795
+
796
+ // AI SDK 6.x 使用对象形式,键为工具名
797
+ tools[toolName] = toolFn(toolConfig);
798
+ }
799
+
800
+ return tools;
801
+ }
802
+
803
+ /**
804
+ * 清理工具参数,移除无效值
805
+ * @param {Object} args - 原始参数
806
+ * @returns {Object} 清理后的参数
807
+ * @private
808
+ */
809
+ _cleanToolArgs(args) {
810
+ if (!args || typeof args !== 'object') {
811
+ return {};
812
+ }
813
+
814
+ const cleaned = {};
815
+ for (const [key, value] of Object.entries(args)) {
816
+ // 跳过 undefined、function、symbol 等无效值
817
+ if (value === undefined || value === null) {
818
+ continue;
819
+ }
820
+ if (typeof value === 'function' || typeof value === 'symbol') {
821
+ continue;
822
+ }
823
+ // 递归清理嵌套对象
824
+ if (typeof value === 'object' && !Array.isArray(value)) {
825
+ cleaned[key] = this._cleanToolArgs(value);
826
+ } else if (Array.isArray(value)) {
827
+ cleaned[key] = value
828
+ .map((item) =>
829
+ typeof item === 'object' && item !== null ? this._cleanToolArgs(item) : item
830
+ )
831
+ .filter((item) => item !== undefined && typeof item !== 'function');
832
+ } else {
833
+ cleaned[key] = value;
834
+ }
835
+ }
836
+ return cleaned;
837
+ }
838
+
839
+ /**
840
+ * 清理消息格式
841
+ * @private
842
+ */
843
+ _cleanMessages(messages) {
844
+ return messages.map((msg) => {
845
+ if (!msg || typeof msg !== 'object') {
846
+ return { role: 'user', content: '' };
847
+ }
848
+
849
+ const cleaned = {
850
+ role: msg.role || 'user',
851
+ };
852
+
853
+ if (Array.isArray(msg.content)) {
854
+ cleaned.content = msg.content;
855
+ } else if (typeof msg.content === 'string') {
856
+ cleaned.content = msg.content;
857
+ } else if (typeof msg.content === 'object' && msg.content !== null) {
858
+ // 对象类型的 content(如 tool result),转为字符串
859
+ cleaned.content =
860
+ typeof msg.content === 'object' && msg.content.content !== undefined
861
+ ? String(msg.content.content)
862
+ : JSON.stringify(msg.content);
863
+ } else {
864
+ cleaned.content = msg.text || msg.input || '';
865
+ }
866
+
867
+ return cleaned;
868
+ });
869
+ }
870
+
871
+ /**
872
+ * 销毁
873
+ */
874
+ destroy() {
875
+ this._messages = [];
876
+ this._tools.clear();
877
+ this._encoder = null;
878
+ this.removeAllListeners();
879
+ }
880
+ }
881
+
882
+ module.exports = { AgentChatHandler };