foliko 1.1.13 → 1.1.14

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 (246) 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/poster-expert.md +135 -196
  36. package/.agent/agents/python-developer.md +60 -0
  37. package/.agent/agents/scheduler.md +59 -0
  38. package/.agent/agents/web-developer.md +45 -0
  39. package/.agent/data/default.json +404 -9
  40. package/.agent/data/plugins-state.json +172 -173
  41. package/.agent/data/puppeteer-sessions/undefined.json +6 -0
  42. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  43. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  44. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  45. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  46. package/.agent/mcp_config.json +21 -0
  47. package/.agent/memory/feedback/mnygjgox-ualjip.md +11 -0
  48. package/.agent/memory/project/mnqx54u5-loqtoe.md +9 -0
  49. package/.agent/memory/project/mnqx84cv-mx6dmd.md +9 -0
  50. package/.agent/memory/project/mnsacuyr-hgtk5n.md +20 -0
  51. package/.agent/memory/project/mnu5hy2x-bjsg7u.md +9 -0
  52. package/.agent/memory/project/mny28ot4-8qe9au.md +9 -0
  53. package/.agent/memory/reference/mnre3cww-penbo1.md +9 -0
  54. package/.agent/memory/reference/mns9wn48-luerua.md +14 -0
  55. package/.agent/memory/reference/mns9yz5c-thc2s0.md +16 -0
  56. package/.agent/memory/reference/mnsfy4um-910f1o.md +23 -0
  57. package/.agent/memory/reference/mnsg37dp-lmfj18.md +32 -0
  58. package/.agent/memory/reference/mnsll60q-0j911u.md +36 -0
  59. package/.agent/memory/reference/mnsmlb5y-nej31u.md +16 -0
  60. package/.agent/memory/reference/mnssle72-yrot96.md +9 -0
  61. package/.agent/memory/reference/mnygj8nb-bjthmc.md +20 -0
  62. package/.agent/memory/user/mnsfuon6-l416q1.md +21 -0
  63. package/.agent/memory/user/mnsg9kut-95m7rf.md +20 -0
  64. package/.agent/memory/user/mnu2eo1v-yy6fhe.md +9 -0
  65. package/.agent/memory/user/mnu2etuo-8u8jk8.md +9 -0
  66. package/.agent/memory/user/mnx0rk6g-gsznjj.md +9 -0
  67. package/.agent/memory/user/mnyf1riz-4yo5yz.md +9 -0
  68. package/.agent/plugins/puppeteer-plugin/README.md +147 -0
  69. package/.agent/plugins/puppeteer-plugin/index.js +1422 -0
  70. package/.agent/plugins/puppeteer-plugin/package.json +9 -0
  71. package/.agent/plugins.json +5 -11
  72. package/.agent/rules/GEMINI.md +273 -0
  73. package/.agent/rules/allow-rule.md +77 -0
  74. package/.agent/rules/log-rule.md +83 -0
  75. package/.agent/rules/security-rule.md +93 -0
  76. package/.agent/scripts/auto_preview.py +148 -0
  77. package/.agent/scripts/checklist.py +217 -0
  78. package/.agent/scripts/session_manager.py +120 -0
  79. package/.agent/scripts/verify_all.py +327 -0
  80. package/.agent/sessions/cli_default.json +11 -641
  81. package/.agent/skills/api-patterns/SKILL.md +81 -0
  82. package/.agent/skills/api-patterns/api-style.md +42 -0
  83. package/.agent/skills/api-patterns/auth.md +24 -0
  84. package/.agent/skills/api-patterns/documentation.md +26 -0
  85. package/.agent/skills/api-patterns/graphql.md +41 -0
  86. package/.agent/skills/api-patterns/rate-limiting.md +31 -0
  87. package/.agent/skills/api-patterns/response.md +37 -0
  88. package/.agent/skills/api-patterns/rest.md +40 -0
  89. package/.agent/skills/api-patterns/scripts/api_validator.py +211 -0
  90. package/.agent/skills/api-patterns/security-testing.md +122 -0
  91. package/.agent/skills/api-patterns/trpc.md +41 -0
  92. package/.agent/skills/api-patterns/versioning.md +22 -0
  93. package/.agent/skills/app-builder/SKILL.md +75 -0
  94. package/.agent/skills/app-builder/agent-coordination.md +71 -0
  95. package/.agent/skills/app-builder/feature-building.md +53 -0
  96. package/.agent/skills/app-builder/project-detection.md +34 -0
  97. package/.agent/skills/app-builder/scaffolding.md +118 -0
  98. package/.agent/skills/app-builder/tech-stack.md +40 -0
  99. package/.agent/skills/app-builder/templates/SKILL.md +39 -0
  100. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  101. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  102. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  103. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  104. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  105. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  106. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  107. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -0
  108. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -0
  109. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -0
  110. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -0
  111. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  112. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -0
  113. package/.agent/skills/architecture/SKILL.md +55 -0
  114. package/.agent/skills/architecture/context-discovery.md +43 -0
  115. package/.agent/skills/architecture/examples.md +94 -0
  116. package/.agent/skills/architecture/pattern-selection.md +68 -0
  117. package/.agent/skills/architecture/patterns-reference.md +50 -0
  118. package/.agent/skills/architecture/trade-off-analysis.md +77 -0
  119. package/.agent/skills/clean-code/SKILL.md +201 -0
  120. package/.agent/skills/doc.md +177 -0
  121. package/.agent/skills/frontend-design/SKILL.md +418 -0
  122. package/.agent/skills/frontend-design/animation-guide.md +331 -0
  123. package/.agent/skills/frontend-design/color-system.md +311 -0
  124. package/.agent/skills/frontend-design/decision-trees.md +418 -0
  125. package/.agent/skills/frontend-design/motion-graphics.md +306 -0
  126. package/.agent/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  127. package/.agent/skills/frontend-design/scripts/ux_audit.py +722 -0
  128. package/.agent/skills/frontend-design/typography-system.md +345 -0
  129. package/.agent/skills/frontend-design/ux-psychology.md +1116 -0
  130. package/.agent/skills/frontend-design/visual-effects.md +383 -0
  131. package/.agent/skills/i18n-localization/SKILL.md +154 -0
  132. package/.agent/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  133. package/.agent/skills/mcp-builder/SKILL.md +176 -0
  134. package/.agent/skills/poster-design/SKILL.md +385 -0
  135. package/.agent/skills/web-design-guidelines/SKILL.md +57 -0
  136. package/.agent/workflows/brainstorm.md +113 -0
  137. package/.agent/workflows/create.md +59 -0
  138. package/.agent/workflows/debug.md +103 -0
  139. package/.agent/workflows/deploy.md +176 -0
  140. package/.agent/workflows/enhance.md +63 -0
  141. package/.agent/workflows/orchestrate.md +237 -0
  142. package/.agent/workflows/plan.md +89 -0
  143. package/.agent/workflows/preview.md +81 -0
  144. package/.agent/workflows/simple-test.md +42 -0
  145. package/.agent/workflows/status.md +86 -0
  146. package/.agent/workflows/structured-orchestrate.md +180 -0
  147. package/.agent/workflows/test.md +144 -0
  148. package/.agent/workflows/ui-ux-pro-max.md +296 -0
  149. package/.claude/settings.local.json +23 -1
  150. package/.env.example +56 -56
  151. package/README.md +441 -441
  152. package/cli/src/commands/chat.js +9 -15
  153. package/cli/src/ui/chat-ui.js +41 -71
  154. package/package.json +1 -1
  155. package/plugins/default-plugins.js +5 -5
  156. package/plugins/file-system-plugin.js +1 -1
  157. package/plugins/memory-plugin.js +12 -12
  158. package/plugins/plugin-manager-plugin.js +1 -0
  159. package/plugins/subagent-plugin.js +55 -1
  160. package/plugins/telegram-plugin.js +9 -6
  161. package/plugins/weixin-plugin.js +50 -34
  162. package/skills/find-skills/AGENTS.md +162 -162
  163. package/skills/find-skills/SKILL.md +133 -133
  164. package/src/core/agent-chat.js +460 -1612
  165. package/src/core/agent.js +53 -134
  166. package/src/core/chat-session.js +423 -0
  167. package/src/core/context-compressor.js +473 -0
  168. package/src/core/context-manager.js +0 -48
  169. package/src/core/framework.js +95 -68
  170. package/src/core/index.js +11 -0
  171. package/src/core/notification-manager.js +125 -0
  172. package/src/core/subagent.js +295 -0
  173. package/src/core/token-counter.js +190 -0
  174. package/src/core/tool-executor.js +270 -0
  175. package/src/executors/mcp-executor.js +14 -1
  176. package/system.md +312 -2373
  177. package/.agent/agents/code-assistant.json +0 -17
  178. package/.agent/agents/email-assistant.json +0 -14
  179. package/.agent/agents/file-assistant.json +0 -18
  180. package/.agent/agents/orchestrator-demo.md +0 -53
  181. package/.agent/agents/orchestrator.json +0 -7
  182. package/.agent/agents/system-assistant.json +0 -15
  183. package/.agent/agents/web-assistant.json +0 -12
  184. package/.agent/data/email/processed-emails.json +0 -1
  185. package/.agent/data/scheduler/tasks.json +0 -1
  186. package/.agent/data/web/web-config.json +0 -5
  187. package/.agent/memory/feedback/mnv3nu27-3o15pf.md +0 -9
  188. package/.agent/memory/feedback/mnv3o078-b959yj.md +0 -9
  189. package/.agent/memory/feedback/mnv3o6ej-u0fif5.md +0 -9
  190. package/.agent/memory/feedback/mnv3obgl-bkkjoj.md +0 -9
  191. package/.agent/memory/feedback/mnv4a3js-dv6onx.md +0 -9
  192. package/.agent/memory/feedback/mnv4aacm-sxxowp.md +0 -9
  193. package/.agent/memory/feedback/mnv4ahto-w40ffm.md +0 -9
  194. package/.agent/memory/feedback/mnv4anvp-3cs06y.md +0 -9
  195. package/.agent/memory/feedback/mnvzgvtd-0o2900.md +0 -9
  196. package/.agent/memory/feedback/mnvzhajn-swbx61.md +0 -15
  197. package/.agent/memory/feedback/mnvzhgsp-p5vog3.md +0 -9
  198. package/.agent/memory/feedback/mnvzho0c-fgql7q.md +0 -14
  199. package/.agent/memory/feedback/mnvzhtzq-ufr5at.md +0 -9
  200. package/.agent/memory/feedback/mnvzhyb3-9byq2z.md +0 -9
  201. package/.agent/memory/feedback/mnvzi7hp-hyeafp.md +0 -9
  202. package/.agent/memory/feedback/mnvzibph-z7rwp5.md +0 -9
  203. package/.agent/memory/feedback/mnvzilys-7h176w.md +0 -14
  204. package/.agent/memory/feedback/mnvziuh5-zjshci.md +0 -9
  205. package/.agent/memory/feedback/mnw07wde-6zqsc8.md +0 -9
  206. package/.agent/memory/feedback/mnw084bp-j0ba2a.md +0 -9
  207. package/.agent/memory/user/mnv3n62r-y0h79j.md +0 -21
  208. package/.agent/memory/user/mnv3n9yf-ead4g8.md +0 -13
  209. package/.agent/memory/user/mnv3ne3j-82tq1k.md +0 -19
  210. package/.agent/memory/user/mnv3nhgm-g2s2us.md +0 -11
  211. package/.agent/memory/user/mnv3nl9u-ejd998.md +0 -16
  212. package/.agent/memory/user/mnv3nofp-ya5szl.md +0 -10
  213. package/.agent/memory/user/mnv49qne-bhk0ki.md +0 -9
  214. package/.agent/memory/user/mnv49w3y-rzr8ju.md +0 -13
  215. package/.agent/package.json +0 -8
  216. package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
  217. package/.agent/plugins/daytona/README.md +0 -89
  218. package/.agent/plugins/daytona/index.js +0 -377
  219. package/.agent/plugins/daytona/package.json +0 -12
  220. package/.agent/plugins/marknative/README.md +0 -134
  221. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  222. package/.agent/plugins/marknative/fonts.zip +0 -0
  223. package/.agent/plugins/marknative/index.js +0 -256
  224. package/.agent/plugins/marknative/package.json +0 -12
  225. package/.agent/plugins/system-info/index.js +0 -387
  226. package/.agent/plugins/system-info/package.json +0 -4
  227. package/.agent/plugins/system-info/test.js +0 -40
  228. package/.agent/plugins/test-plugin.py +0 -123
  229. package/.agent/plugins/test_nested_plugin.py +0 -85
  230. package/.agent/python-scripts/test_sample.py +0 -24
  231. package/.agent/sessions/test.json +0 -16
  232. package/.agent/skills/agent-browser/SKILL.md +0 -311
  233. package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
  234. package/.agent/skills/sysinfo/SKILL.md +0 -38
  235. package/.agent/skills/sysinfo/system-info.sh +0 -130
  236. package/.agent/skills/workflow/SKILL.md +0 -324
  237. package/.agent/test-agent.js +0 -35
  238. package/.agent/weixin.json +0 -6
  239. package/.agent/workflows/email-digest.json +0 -50
  240. package/.agent/workflows/file-backup.json +0 -21
  241. package/.agent/workflows/get-ip-notify.json +0 -32
  242. package/.agent/workflows/news-aggregator.json +0 -93
  243. package/.agent/workflows/news-dashboard-v2.json +0 -94
  244. package/.agent/workflows/notification-batch.json +0 -32
  245. package/plugins/python-plugin-loader.js.bak +0 -856
  246. package/src/core/agent-context.js +0 -188
@@ -1,85 +1,31 @@
1
1
  /**
2
2
  * AgentChatHandler 聊天处理器
3
3
  * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环
4
+ *
5
+ * 职责委托:
6
+ * - ChatSession: 会话管理、消息历史、队列
7
+ * - ToolExecutor: 工具发现、执行、验证
8
+ * - ContextCompressor: 上下文压缩
4
9
  */
5
10
 
6
11
  const { EventEmitter } = require('../utils/event-emitter');
7
12
  const { logger } = require('../utils/logger');
8
- const { generateText, stepCountIs, isLoopFinished } = require('ai');
13
+ const {
14
+ tool: aiTool,
15
+ ToolLoopAgent,
16
+ isLoopFinished,
17
+ generateText,
18
+ streamText,
19
+ RetryError,
20
+ APICallError,
21
+ } = require('ai');
9
22
  const { prepareMessagesForAPI, cleanResponse } = require('../utils');
10
23
  const { ChatQueueManager } = require('../utils/chat-queue');
11
24
  const fs = require('fs/promises');
12
-
13
- // 模型上下文限制表(留 15-20% 余量给 system prompt 和输出)
14
- const MODEL_CONTEXT_LIMITS = {
15
- // DeepSeek
16
- 'deepseek-chat': 128000,
17
- 'deepseek-coder': 128000,
18
- 'deepseek-reasoner': 128000,
19
- // MiniMax
20
- 'MiniMax-M2.7': 110000,
21
- // OpenAI
22
- 'gpt-4': 100000,
23
- 'gpt-4o': 100000,
24
- 'gpt-4o-mini': 100000,
25
- 'gpt-4-turbo': 100000,
26
- // Anthropic
27
- 'claude-3-5-sonnet': 150000,
28
- 'claude-3-opus': 150000,
29
- 'claude-3-sonnet': 150000,
30
- 'glm-5.1': 200000,
31
- };
32
-
33
- /**
34
- * 纯 JavaScript token 计数器
35
- * 参考 Claude Code 的实现,使用 bytes/token 比率估算
36
- * - 普通文本:4 bytes ≈ 1 token
37
- * - JSON 等密集文本:2 bytes ≈ 1 token
38
- */
39
- class SimpleTokenizer {
40
- constructor() {
41
- // 无需初始化
42
- }
43
-
44
- /**
45
- * 估算文本的 token 数
46
- * @param {string} text
47
- * @param {number} [bytesPerToken=4] - bytes per token 比率
48
- * @returns {number}
49
- */
50
- encode(text, bytesPerToken = 4) {
51
- if (!text || typeof text !== 'string') {
52
- return 0;
53
- }
54
-
55
- // 清理文本
56
- const cleanText = text
57
- .replace(/\0+/g, '')
58
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
59
- .slice(0, 100000);
60
-
61
- if (!cleanText) {
62
- return 0;
63
- }
64
-
65
- return Math.ceil(cleanText.length / bytesPerToken);
66
- }
67
-
68
- /**
69
- * 估算 JSON 文本的 token 数(更密集)
70
- * @param {string} text
71
- * @returns {number}
72
- */
73
- encodeForJSON(text) {
74
- return this.encode(text, 2);
75
- }
76
- }
77
-
78
- // 全局 tokenizer 实例
79
- const _globalTokenizer = new SimpleTokenizer();
80
-
81
- // 压缩超时时间(毫秒)
82
- const COMPRESSION_TIMEOUT = 120000;
25
+ // 新模块
26
+ const { ChatSession } = require('./chat-session');
27
+ const { ToolExecutor } = require('./tool-executor');
28
+ const { ContextCompressor } = require('./context-compressor');
83
29
 
84
30
  class AgentChatHandler extends EventEmitter {
85
31
  /**
@@ -92,62 +38,58 @@ class AgentChatHandler extends EventEmitter {
92
38
  this.agent = agent;
93
39
  this.config = config;
94
40
 
41
+ // AI 配置
95
42
  this.model = config.model || 'deepseek-chat';
96
43
  this.provider = config.provider || 'deepseek';
97
44
  this.apiKey = config.apiKey;
98
45
  this.baseURL = config.baseURL;
99
46
  this.providerOptions = config.providerOptions || {};
47
+ this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;
48
+ this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;
100
49
 
101
50
  this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';
102
- // this._messages = []; // 移除共享消息存储!改用 Per-Session
103
- this._sessionMessageStores = new Map(); // sessionId → { messages: [], historyLoaded: false, compressionState: {} }
104
- this._tools = new Map();
105
- this._maxSteps = 20; // 降低默认步骤数,减少上下文消耗
106
-
107
- // 上下文压缩配置:根据模型自动设置限制
108
- const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find((k) =>
109
- this.model.toLowerCase().includes(k.toLowerCase())
110
- );
111
- const defaultLimit = modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;
112
- this._maxContextTokens = config.maxContextTokens || defaultLimit;
113
- this._compressionThreshold = config.compressionThreshold || 0.6; // 60% 就压缩,早触发
114
- this._keepRecentMessages = config.keepRecentMessages || 20; // 保留最近 20 条
115
- this._enableSmartCompress = config.enableSmartCompress !== false; // 默认开启智能摘要
116
- this._encoder = null;
117
- this._compressionCount = 0; // 压缩次数统计
118
- this._compressionInProgress = false; // 压缩锁,防止并发压缩
119
- this._compressionPromise = null; // 正在进行的压缩 Promise
120
-
121
- // 工具结果压缩配置
122
- this._maxToolResultSize = config.maxToolResultSize || 4000; // 工具结果超过此大小则压缩(字节)
123
-
124
- // 初始化编码器
125
- // 使用纯 JS tokenizer
126
- this._encoder = _globalTokenizer;
127
-
128
- // Session 历史存储配置
129
- this._sessionHistoryKey = config.sessionHistoryKey || 'chat_history';
130
-
131
- // Per-Session 队列管理
132
- this._sessionQueues = new Map(); // sessionId -> Queue of {message, options, resolve, reject}
133
- this._processingSessions = new Set(); // sessionId -> processing flag
134
-
135
- // 初始化队列管理器
136
- this.queueManager = new ChatQueueManager({
51
+ this._maxSteps = config.maxSteps || 20;
52
+
53
+ // 委托给新模块
54
+ // ChatSession: 会话、消息、队列
55
+ this._chatSession = new ChatSession({
56
+ agent,
137
57
  maxConcurrent: config.maxConcurrent || 1,
138
58
  retryAttempts: config.retryAttempts || 3,
139
59
  retryDelay: config.retryDelay || 1000,
140
60
  });
141
61
 
142
- // 转发队列事件到 AgentChatHandler
143
- this.forwardQueueEvents();
62
+ // ToolExecutor: 工具管理
63
+ this._toolExecutor = new ToolExecutor({
64
+ agent,
65
+ framework: agent?.framework,
66
+ });
67
+
68
+ // ContextCompressor: 压缩
69
+ this._contextCompressor = new ContextCompressor({
70
+ agent,
71
+ framework: agent?.framework,
72
+ model: this.model,
73
+ maxContextTokens: config.maxContextTokens,
74
+ keepRecentMessages: config.keepRecentMessages || 20,
75
+ enableSmartCompress: config.enableSmartCompress !== false,
76
+ });
77
+
78
+ // 上下文限制
79
+ this._maxContextTokens = this._contextCompressor._maxContextTokens;
80
+
81
+ // ChatQueueManager: 队列管理
82
+ this.queueManager = new ChatQueueManager({
83
+ maxConcurrent: config.maxConcurrent || 1,
84
+ retryAttempts: config.retryAttempts || 3,
85
+ retryDelay: config.retryDelay || 1000,
86
+ });
144
87
 
145
- // 可选:启用队列日志
146
- // if (config.enableQueueLogging) {
147
- // this.enableQueueLogging();
148
- // }
88
+ // AI client
89
+ this._aiClient = null;
90
+ this._aiProvider = null;
149
91
 
150
- // Session Memory prepareStep(从 memory 插件获取)
92
+ // Session Memory prepareStep
151
93
  this._sessionMemoryPrepareStep = null;
152
94
  if (agent?.framework) {
153
95
  const memoryPlugin = agent.framework.pluginManager.get('memory');
@@ -156,12 +98,24 @@ class AgentChatHandler extends EventEmitter {
156
98
  logger.debug('Session memory prepareStep loaded');
157
99
  }
158
100
  }
101
+
102
+ // 转发 ChatSession 事件
103
+ this._setupEventForwarding();
104
+
105
+ // 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk)
106
+ this.queueManager.on('stream:chunk', (data) => {
107
+ this._chatSession.emit('stream:chunk', data);
108
+ });
109
+
110
+ // 设置消息处理器(使用 bind 保持 this 引用)
111
+ this._chatSession.setMessageProcessor(this._processMessage.bind(this));
159
112
  }
160
113
 
161
114
  /**
162
- * 转发队列事件到自身,方便外部监听
115
+ * 设置事件转发
116
+ * @private
163
117
  */
164
- forwardQueueEvents() {
118
+ _setupEventForwarding() {
165
119
  const events = [
166
120
  'queue:added',
167
121
  'queue:processing',
@@ -175,125 +129,97 @@ class AgentChatHandler extends EventEmitter {
175
129
  ];
176
130
 
177
131
  events.forEach((eventName) => {
178
- this.queueManager.on(eventName, (data) => {
179
- // 转发事件,保持 this 指向 AgentChatHandler
132
+ this._chatSession.on(eventName, (data) => {
180
133
  this.emit(eventName, data);
181
134
  });
182
135
  });
183
136
 
184
- // Session 事件作用域管理
185
- this._sessionScopes = new Map(); // sessionId -> Set<{event, handler}>
137
+ // 转发工具事件
138
+ this._toolExecutor.on('tool:call', (data) => this.emit('tool:call', data));
139
+ this._toolExecutor.on('tool:result', (data) => this.emit('tool:result', data));
140
+ this._toolExecutor.on('tool:error', (data) => this.emit('tool:error', data));
186
141
  }
187
142
 
188
- /**
189
- * 创建 Session 作用域的事件监听器
190
- * 所有通过返回对象注册的事件监听器都只接收属于指定 sessionId 的事件
191
- * @param {string} sessionId - Session ID
192
- * @returns {Object} { on, once, off, removeAllListeners }
193
- */
143
+ // ==================== 工具管理(委托给 ToolExecutor) ====================
144
+
145
+ registerTool(tool) {
146
+ this._toolExecutor.registerTool(tool);
147
+ return this;
148
+ }
149
+
150
+ getTool(name) {
151
+ return this._toolExecutor.getTool(name);
152
+ }
153
+
154
+ getTools() {
155
+ return this._toolExecutor.getAllTools();
156
+ }
157
+
158
+ // ==================== 会话管理(委托给 ChatSession) ====================
159
+
194
160
  createSessionScope(sessionId) {
195
- const scope = this;
196
- const handlers = new Set(); // 记录这个 scope 注册的所有 handler
197
-
198
- // 创建包装 handler,自动过滤 sessionId
199
- const wrapHandler = (handler) => {
200
- return (data) => {
201
- // 检查 data 中的 sessionId
202
- if (data && data.sessionId !== sessionId) {
203
- return; // 不是当前 scope 的 session,跳过
204
- }
205
- handler(data);
206
- };
207
- };
161
+ return this._chatSession.createSessionScope(sessionId);
162
+ }
208
163
 
209
- const wrappedHandlers = new WeakMap(); // original -> wrapped
210
-
211
- return {
212
- /**
213
- * 监听事件(自动过滤 sessionId
214
- */
215
- on(event, handler) {
216
- const wrapped = wrapHandler(handler);
217
- wrappedHandlers.set(handler, wrapped);
218
- handlers.add({ event, handler, wrapped });
219
- scope.on(event, wrapped);
220
- return this;
221
- },
222
-
223
- /**
224
- * 监听一次性事件
225
- */
226
- once(event, handler) {
227
- const wrapped = wrapHandler(handler);
228
- wrappedHandlers.set(handler, wrapped);
229
- handlers.add({ event, handler, wrapped });
230
- scope.once(event, wrapped);
231
- return this;
232
- },
233
-
234
- /**
235
- * 取消监听(精确移除)
236
- */
237
- off(event, handler) {
238
- const wrapped = wrappedHandlers.get(handler);
239
- if (wrapped) {
240
- scope.off(event, wrapped);
241
- wrappedHandlers.delete(handler);
242
- }
243
- // 从 handlers 中移除
244
- for (const item of handlers) {
245
- if (item.event === event && item.handler === handler) {
246
- handlers.delete(item);
247
- break;
248
- }
249
- }
250
- return this;
251
- },
252
-
253
- /**
254
- * 移除当前 scope 注册的所有监听器
255
- */
256
- removeAllListeners() {
257
- for (const { event, wrapped } of handlers) {
258
- scope.off(event, wrapped);
259
- }
260
- handlers.clear();
261
- return this;
262
- },
263
-
264
- /**
265
- * 获取当前绑定的 sessionId
266
- */
267
- getSessionId() {
268
- return sessionId;
269
- },
270
- };
164
+ getSessionMessageStore(sessionId) {
165
+ return this._chatSession.getSessionMessageStore(sessionId);
166
+ }
167
+
168
+ // ==================== 压缩(委托给 ContextCompressor ====================
169
+
170
+ async _compressContext(sessionId, messages, messageStore) {
171
+ return this._contextCompressor.compress(sessionId, messages, messageStore);
172
+ }
173
+
174
+ _validateMessagesPairing(messages) {
175
+ return this._contextCompressor.validateMessagesPairing(messages);
271
176
  }
272
177
 
273
178
  /**
274
- * 启用队列日志(可选)
179
+ * 验证工具调用
180
+ * @private
275
181
  */
276
- enableQueueLogging() {
277
- this.queueManager.on('queue:added', (data) => {
278
- console.log(`[Queue] Request ${data.requestId} added. Queue length: ${data.queueLength}`);
279
- });
182
+ _validateToolCalls(messages) {
183
+ return this._toolExecutor.validateToolCalls(messages);
184
+ }
280
185
 
281
- this.queueManager.on('queue:processing', (data) => {
282
- console.log(
283
- `[Queue] Processing ${data.requestId}, Active: ${data.activeCount}, Remaining: ${data.remaining}`
284
- );
285
- });
186
+ // ==================== AI 调用 ====================
286
187
 
287
- this.queueManager.on('queue:completed', (data) => {
288
- console.log(`[Queue] Request ${data.requestId} completed in ${data.duration}ms`);
289
- });
188
+ setAIClient(client) {
189
+ this._aiClient = client;
190
+ }
191
+
192
+ setSystemPrompt(prompt) {
193
+ this._systemPrompt = prompt;
194
+ }
290
195
 
291
- this.queueManager.on('queue:failed', (data) => {
292
- console.error(`[Queue] Request ${data.requestId} failed: ${data.error}`);
196
+ /**
197
+ * 获取 AI Provider 和 Model
198
+ * @private
199
+ */
200
+ _createAIProvider() {
201
+ const { createAI, createModel } = require('./provider');
202
+ const aiProvider = createAI({
203
+ provider: this.provider,
204
+ model: this.model,
205
+ apiKey: this.apiKey,
206
+ baseURL: this.baseURL,
293
207
  });
208
+ const model = createModel(this.model, aiProvider);
209
+ return { provider: aiProvider, model };
210
+ }
294
211
 
295
- this.queueManager.on('queue:empty', () => {
296
- console.log('[Queue] Queue is empty');
212
+ /**
213
+ * 创建 ToolLoopAgent
214
+ * @private
215
+ */
216
+ _createToolLoopAgent(model, tools) {
217
+ return new ToolLoopAgent({
218
+ model,
219
+ instructions: this._systemPrompt,
220
+ tools,
221
+ stopWhen: isLoopFinished(),
222
+ prepareStep: this._createPrepareStep(),
297
223
  });
298
224
  }
299
225
 
@@ -351,7 +277,6 @@ class AgentChatHandler extends EventEmitter {
351
277
  // 监听流式数据
352
278
  const chunkHandler = (data) => {
353
279
  if (data.requestId === requestId) {
354
- //chunkQueue.push(data.chunk);
355
280
  if (resolveNext) {
356
281
  resolveNext();
357
282
  resolveNext = null;
@@ -457,1012 +382,222 @@ class AgentChatHandler extends EventEmitter {
457
382
  }
458
383
 
459
384
  /**
460
- * 获取队列状态
461
- */
462
- getQueueStatus() {
463
- return this.queueManager.getStatus();
464
- }
465
-
466
- /**
467
- * 清空队列
385
+ * 获取默认 sessionId
468
386
  */
469
- clearQueue() {
470
- const count = this.queueManager.clear();
471
- this.emit('queue:cleared-manually', { count });
472
- return count;
387
+ getDefaultSessionId() {
388
+ return 'default';
473
389
  }
474
390
 
475
391
  /**
476
- * 生成请求ID
392
+ * 生成请求 ID
477
393
  */
478
394
  generateRequestId() {
479
- return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
395
+ return `req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
480
396
  }
481
397
 
482
398
  /**
483
- * 获取默认会话ID
399
+ * 聊天(直接非流式,使用 agent.generate)
484
400
  */
485
- getDefaultSessionId() {
486
- return 'default_session';
401
+ async chat(message, options = {}) {
402
+ const sessionId = options.sessionId || this.getDefaultSessionId();
403
+ const result = await this._processMessage(sessionId, message, options);
404
+ return { message: result.text, ...result };
487
405
  }
488
406
 
489
407
  /**
490
- * 获取 Per-Session 的消息存储
491
- * @param {string} sessionId - 会话 ID
492
- * @returns {Object} 消息存储对象
493
- * @private
408
+ * 流式聊天
494
409
  */
495
- _getSessionMessageStore(sessionId) {
496
- if (!sessionId) {
497
- // session 的单次请求,使用临时存储
498
- return {
499
- messages: [],
500
- historyLoaded: false,
501
- compressionState: {},
502
- lastUsage: null, // API 返回的真实 usage
503
- save: () => {},
504
- };
505
- }
410
+ async *chatStream(message, options = {}) {
411
+ const sessionId = options.sessionId || this.getDefaultSessionId();
412
+ const framework = this.agent.framework;
506
413
 
507
- if (!this._sessionMessageStores.has(sessionId)) {
508
- // 自动从 session 加载消息
509
- const savedMessages = this._loadHistoryFromSession(sessionId);
510
- this._sessionMessageStores.set(sessionId, {
511
- sessionId, // 保存 sessionId 引用
512
- messages: savedMessages,
513
- historyLoaded: true, // 已加载
514
- compressionState: {
515
- lastCompressedAt: null,
516
- lastTokenCount: 0,
517
- count: 0,
518
- },
519
- lastUsage: null, // API 返回的真实 usage
520
- save: () => {
521
- // 简洁的保存方法
522
- if (this.agent?.framework) {
523
- const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
524
- if (sessionCtx) {
525
- sessionCtx.replaceMessages(this._sessionMessageStores.get(sessionId).messages);
526
- }
527
- }
528
- },
529
- });
530
- }
531
- return this._sessionMessageStores.get(sessionId);
532
- }
414
+ try {
415
+ const { messageStore, messages } = await this._prepareSession(message, sessionId);
533
416
 
534
- /**
535
- * SessionContext 加载聊天历史
536
- * @param {string} sessionId - 会话 ID
537
- * @returns {Array} 消息数组
538
- * @private
539
- */
540
- _loadHistoryFromSession(sessionId) {
541
- if (!sessionId || !this.agent?.framework) return [];
417
+ if (!this._aiClient) {
418
+ throw new Error('AI client not configured.');
419
+ }
542
420
 
543
- try {
544
- const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
545
- if (!sessionCtx) return [];
421
+ const tools = this._getAITools(aiTool);
422
+ const agent = new ToolLoopAgent({
423
+ model: this._aiClient,
424
+ instructions: this._systemPrompt,
425
+ tools,
426
+ stopWhen: isLoopFinished(),
427
+ prepareStep: this._createPrepareStep(),
428
+ });
546
429
 
547
- const messages = sessionCtx.getMessages();
548
- return messages || [];
549
- } catch (err) {
550
- // 忽略加载错误
551
- }
552
- return [];
553
- }
430
+ const result = await framework.runInSession(sessionId, {}, async () => {
431
+ return agent.stream({ messages, ...this.providerOptions });
432
+ });
554
433
 
555
- /**
556
- * 保存聊天历史到 SessionContext
557
- * @param {string} sessionId - 会话 ID
558
- * @param {Array} messages - 消息数组
559
- * @private
560
- */
561
- _saveHistoryToSession(sessionId, messages) {
562
- if (!sessionId || !this.agent?.framework || !messages) return;
434
+ const stream = result.fullStream;
435
+ let fullText = '';
436
+ const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;
563
437
 
564
- try {
565
- const sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
566
- if (!sessionCtx) return;
438
+ for await (const part of iterator || stream) {
439
+ if (part.type === 'text-delta') {
440
+ const text = part.text || part.textDelta || '';
441
+ fullText += text;
442
+ yield { type: 'text', text };
443
+ } else if (part.type === 'reasoning') {
444
+ yield { type: 'thinking', text: part.text };
445
+ } else if (part.type === 'tool-call') {
446
+ yield { type: 'tool-call', toolName: part.toolName, input: part.input };
447
+ } else if (part.type === 'tool-result') {
448
+ yield { type: 'tool-result', toolName: part.toolName, result: part.output };
449
+ } else if (part.type === 'error') {
450
+ yield { type: 'error', error: part.error };
451
+ }
452
+ }
453
+
454
+ const finishMessages = (await result.response).messages;
455
+ messages.push(...finishMessages);
456
+ const usage = await result.totalUsage;
457
+ if (usage) {
458
+ this._updateMessageStoreUsage(messageStore, usage);
459
+ }
567
460
 
568
- // 清理消息格式后整体替换
569
- const cleanedMessages = prepareMessagesForAPI(messages);
570
- sessionCtx.replaceMessages(cleanedMessages);
461
+ const userMsg = messages[messages.length - finishMessages.length - 1];
462
+ this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });
571
463
  } catch (err) {
572
- // 忽略保存错误
573
- }
574
- }
464
+ // 使用 AI SDK 的错误类型判断
465
+ // RetryError: SDK 内部抛出的重试失败错误,name 可能是 'AI_RetryError' 或 'RetryError'
466
+ const errName = err?.name || '';
467
+ const isRetryError =
468
+ err instanceof RetryError ||
469
+ errName === 'AI_RetryError' ||
470
+ errName === 'RetryError' ||
471
+ err.reason === 'maxRetriesExceeded';
472
+
473
+ // APICallError: 可重试的 API 调用错误
474
+ const isAPICallError = err instanceof APICallError;
475
+ const isRetryable = isAPICallError && err.isRetryable === true;
575
476
 
576
- /**
577
- * 将消息加入队列(Per-Session)
578
- * @param {string} sessionId - 会话 ID
579
- * @param {string|Object} message - 消息
580
- * @param {Object} options - 选项
581
- * @returns {Promise}
582
- */
583
- enqueue(sessionId, message, options = {}) {
584
- if (!this._sessionQueues.has(sessionId)) {
585
- this._sessionQueues.set(sessionId, []);
586
- }
587
- const queue = this._sessionQueues.get(sessionId);
477
+ const isTransientError =
478
+ isRetryError ||
479
+ isRetryable ||
480
+ (err.message && err.message.includes('Invalid JSON response'));
588
481
 
589
- if (this._processingSessions.has(sessionId)) {
590
- // 已在处理中,加入队列
591
- return new Promise((resolve, reject) => {
592
- queue.push({ message, options, resolve, reject });
593
- });
482
+ if (isTransientError) {
483
+ yield { type: 'error', error: 'AI 服务暂时不可用,请稍后重试。' };
484
+ } else {
485
+ logger.error(`[AgentChat] Stream error: ${err.message}`);
486
+ this.emit('error', { error: err.message });
487
+ yield { type: 'error', error: err.message };
488
+ }
489
+ } finally {
490
+ const messageStore = this._getSessionMessageStore(sessionId);
491
+ messageStore.save();
594
492
  }
595
-
596
- // 直接处理
597
- return this._processWithSession(sessionId, message, options);
598
493
  }
599
494
 
600
495
  /**
601
- * 处理单个 session 的消息
602
- * @param {string} sessionId - 会话 ID
603
- * @param {string|Object} message - 消息
604
- * @param {Object} options - 选项
605
- * @returns {Promise}
606
- * @private
496
+ * 清除历史
607
497
  */
608
- async _processWithSession(sessionId, message, options) {
609
- this._processingSessions.add(sessionId);
610
- try {
611
- return await this._doChat(message, options);
612
- } finally {
613
- this._processingSessions.delete(sessionId);
614
- // 处理队列中的下一条
615
- setImmediate(() => this._drainQueue(sessionId));
498
+ clearHistory(sessionId) {
499
+ if (sessionId) {
500
+ const store = this._chatSession.getSessionMessageStore(sessionId);
501
+ store.messages = [];
502
+ store.historyLoaded = false;
616
503
  }
617
504
  }
618
505
 
619
506
  /**
620
- * 消费队列中的下一条消息
621
- * @param {string} sessionId - 会话 ID
622
- * @private
507
+ * 销毁
623
508
  */
624
- _drainQueue(sessionId) {
625
- const queue = this._sessionQueues.get(sessionId);
626
- if (!queue || queue.length === 0) return;
627
-
628
- const item = queue.shift();
629
- this._processWithSession(sessionId, item.message, item.options)
630
- .then(item.resolve)
631
- .catch(item.reject);
509
+ destroy() {
510
+ if (this._chatSession) {
511
+ this._chatSession.removeAllListeners();
512
+ }
513
+ this.removeAllListeners();
632
514
  }
633
515
 
634
516
  /**
635
- * 静态方法:保留接口兼容性(无实际作用)
517
+ * 入队消息
636
518
  */
637
- static clearEncoderCache() {
638
- // 不再需要清理
519
+ enqueue(sessionId, message, options = {}) {
520
+ return this._chatSession.enqueue(sessionId, message, options);
639
521
  }
640
522
 
641
523
  /**
642
- * 计算文本的 token 数
643
- * @param {string} text
644
- * @param {number} [bytesPerToken=4] - bytes per token 比率
645
- * @returns {number}
524
+ * 处理消息(队列回调)
646
525
  * @private
647
526
  */
648
- _countTokens(text, bytesPerToken = 4) {
649
- if (!text || typeof text !== 'string') return 0;
650
- // SimpleTokenizer.encode 已经处理了清理逻辑
527
+ async _processMessage(sessionId, message, options) {
528
+ const framework = this.agent.framework;
529
+
651
530
  try {
652
- return this._encoder.encode(text, bytesPerToken);
531
+ const { messageStore, messages } = await this._prepareSession(message, sessionId);
532
+
533
+ if (!this._aiClient) {
534
+ throw new Error('AI client not configured.');
535
+ }
536
+
537
+ const tools = this._getAITools(aiTool);
538
+ const agent = new ToolLoopAgent({
539
+ model: this._aiClient,
540
+ instructions: this._systemPrompt,
541
+ tools,
542
+ stopWhen: isLoopFinished(),
543
+ prepareStep: this._createPrepareStep(),
544
+ });
545
+
546
+ const result = await framework.runInSession(sessionId, {}, async () => {
547
+ return agent.generate({ messages, ...this.providerOptions });
548
+ });
549
+
550
+ if (result.usage) {
551
+ this._updateMessageStoreUsage(messageStore, result.usage);
552
+ }
553
+
554
+ messages.push(...result.response.messages);
555
+ const userMsg = messages[messages.length - result.response.messages.length - 1];
556
+ this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });
557
+
558
+ return {
559
+ success: true,
560
+ message: cleanResponse(result.text || ''),
561
+ stepCount: result.stepCount || 1,
562
+ };
653
563
  } catch (err) {
654
- // 估算失败时使用字符计数
655
- return Math.ceil(text.length / bytesPerToken);
564
+ console.log(err);
565
+ const errorMsg = err.message || String(err);
566
+
567
+ return {
568
+ success: false,
569
+ message: 'AI 服务暂时不可用,请稍后重试。',
570
+ error: errorMsg,
571
+ stepCount: 0,
572
+ };
573
+ } finally {
574
+ const messageStore = this._getSessionMessageStore(sessionId);
575
+ messageStore.save();
656
576
  }
657
577
  }
658
578
 
659
579
  /**
660
- * 计算消息列表的总 token
661
- * 参考 Claude Code 的分块计数逻辑
662
- * @param {Array} messages
663
- * @returns {number}
580
+ * 获取 Session Message Store
664
581
  * @private
665
582
  */
666
- _countMessagesTokens(messages) {
667
- let total = 0;
668
- for (const msg of messages) {
669
- if (!msg) continue;
670
- total += this._countMessageTokens(msg);
671
- }
672
- return total;
583
+ _getSessionMessageStore(sessionId) {
584
+ return this._chatSession.getSessionMessageStore(sessionId);
673
585
  }
674
586
 
675
587
  /**
676
- * 计算单条消息的 token 数
677
- * @param {Object} msg - 消息对象
678
- * @returns {number}
588
+ * 准备会话消息
679
589
  * @private
680
590
  */
681
- _countMessageTokens(msg) {
682
- let total = 0;
683
- total += 4; // role 标记
684
-
685
- if (typeof msg.content === 'string') {
686
- total += this._countTokens(msg.content);
687
- } else if (Array.isArray(msg.content)) {
688
- for (const part of msg.content) {
689
- total += this._countContentBlockTokens(part);
690
- }
691
- } else if (msg.content && typeof msg.content === 'object') {
692
- // 处理工具结果等对象类型
693
- try {
694
- const str = JSON.stringify(msg.content);
695
- total += this._countTokens(str, 2); // JSON 用更密集的比率
696
- } catch (e) {
697
- // 无法序列化的内容,忽略
698
- }
699
- }
700
- total += 4; // 结尾标记
701
- return total;
702
- }
703
-
704
- /**
705
- * 计算单个内容块的 token 数
706
- * 参考 Claude Code 的实现
707
- * 兼容 AI SDK 6.x 标准格式
708
- * @param {Object} block - 内容块
709
- * @returns {number}
710
- * @private
711
- */
712
- _countContentBlockTokens(block) {
713
- if (!block || typeof block !== 'object') {
714
- return 0;
715
- }
716
-
717
- const tokenizer = this._encoder;
718
-
719
- // 文本块
720
- if (block.type === 'text') {
721
- return tokenizer.encode(block.text || '');
722
- }
723
-
724
- // 工具调用块 - 兼容 tool-call 和 tool-use 两种类型
725
- if (block.type === 'tool-call' || block.type === 'tool-use' || block.type === 'tool_call') {
726
- let count = 0;
727
- if (block.name || block.toolName) {
728
- count += tokenizer.encode(String(block.name || block.toolName || ''));
729
- }
730
- if (block.input) {
731
- try {
732
- count += tokenizer.encode(JSON.stringify(block.input), 2);
733
- } catch (e) {}
734
- }
735
- return count;
736
- }
737
-
738
- // 工具结果块 - 兼容 tool-result 和 tool_result 两种类型
739
- if (block.type === 'tool-result' || block.type === 'tool_result') {
740
- let count = 0;
741
- if (block.content) {
742
- if (typeof block.content === 'string') {
743
- count += tokenizer.encode(block.content);
744
- } else if (Array.isArray(block.content)) {
745
- for (const c of block.content) {
746
- if (c && c.type === 'text') {
747
- count += tokenizer.encode(c.text || '');
748
- }
749
- }
750
- }
751
- }
752
- if (block.toolUseId || block.tool_call_id) {
753
- count += 10; // 工具结果标识
754
- }
755
- return count;
756
- }
757
-
758
- // 输入块 (tool_input)
759
- if (block.type === 'input') {
760
- try {
761
- return tokenizer.encode(JSON.stringify(block), 2);
762
- } catch (e) {
763
- return 0;
764
- }
765
- }
766
-
767
- // 其他类型,尝试 JSON 序列化
768
- try {
769
- return tokenizer.encode(JSON.stringify(block), 2);
770
- } catch (e) {
771
- return 0;
772
- }
773
- }
774
-
775
- /**
776
- * 更新 messageStore 的 lastUsage
777
- * @param {Object} messageStore
778
- * @param {Object} usage - API 返回的 usage
779
- * @private
780
- */
781
- _updateMessageStoreUsage(messageStore, usage) {
782
- if (messageStore && usage) {
783
- messageStore.lastUsage = {
784
- inputTokens: usage.inputTokens,
785
- outputTokens: usage.outputTokens,
786
- inputTokenDetails: usage.inputTokenDetails || {},
787
- };
788
- }
789
- }
790
-
791
- /**
792
- * 计算工具定义的 token 数(估算)
793
- * @returns {number}
794
- * @private
795
- */
796
- _countToolsTokens() {
797
- let total = 0;
798
- for (const toolDef of this._tools.values()) {
799
- // 工具名 + 描述
800
- total += this._countTokens(String(toolDef.name || ''));
801
- total += this._countTokens(String(toolDef.description || ''));
802
-
803
- // 工具参数 schema
804
- if (toolDef.inputSchema) {
805
- const schemaStr =
806
- typeof toolDef.inputSchema === 'string'
807
- ? toolDef.inputSchema
808
- : JSON.stringify(toolDef.inputSchema);
809
- total += this._countTokens(schemaStr);
810
- }
811
- }
812
- return total;
813
- }
814
-
815
- /**
816
- * 压缩上下文消息(智能摘要模式,支持超时控制)
817
- * 策略:
818
- * 1. 如果启用了智能摘要且有 AI 客户端,对早期消息进行 AI 总结
819
- * 2. 否则使用简单裁剪 + 标记
820
- * 3. 支持超时控制,防止压缩阻塞太久
821
- * @param {string} sessionId - 会话 ID(用于压缩后同步)
822
- * @param {Array} messages - 消息数组引用
823
- * @param {Object} messageStore - 消息存储对象
824
- * @private
825
- */
826
- async _compressContext(sessionId, messages, messageStore) {
827
- // 如果已经有压缩在进行,返回现有的 Promise
828
- if (this._compressionInProgress && this._compressionPromise) {
829
- logger.debug('Compression already in progress, waiting for it...');
830
- return this._compressionPromise;
831
- }
832
-
833
- if (messages.length <= this._keepRecentMessages) {
834
- return;
835
- }
836
-
837
- this._compressionInProgress = true;
838
-
839
- // 创建压缩 Promise,带超时控制
840
- this._compressionPromise = this._executeCompressionWithTimeout(
841
- sessionId,
842
- messages,
843
- messageStore
844
- ).finally(() => {
845
- this._compressionInProgress = false;
846
- this._compressionPromise = null;
847
- });
848
-
849
- return this._compressionPromise;
850
- }
851
-
852
- /**
853
- * 执行压缩(带超时)
854
- * @param {string} sessionId - 会话 ID
855
- * @param {Array} messages - 消息数组引用
856
- * @param {Object} messageStore - 消息存储对象
857
- * @private
858
- */
859
- async _executeCompressionWithTimeout(sessionId, messages, messageStore) {
860
- try {
861
- return await Promise.race([
862
- this._doCompress(sessionId, messages, messageStore),
863
- this._createTimeoutPromise(),
864
- ]);
865
- } catch (err) {
866
- logger.warn('Compression failed:', err.message);
867
- // 压缩失败时使用简单的截断策略
868
- this._simpleCompress(sessionId, messages, messageStore);
869
- }
870
- }
871
-
872
- /**
873
- * 创建超时 Promise
874
- * @private
875
- */
876
- _createTimeoutPromise() {
877
- return new Promise((_, reject) => {
878
- setTimeout(() => {
879
- reject(new Error('Compression timeout'));
880
- }, COMPRESSION_TIMEOUT);
881
- });
882
- }
883
-
884
- async compressToolResults(messages) {
885
- try {
886
- const recentMessages = prepareMessagesForAPI(messages.slice(-10));
887
- const messagesToSummarize = messages.slice(0, -10);
888
- let summaryContent = '';
889
- const summaryText = await this._summarizeMessages(messagesToSummarize);
890
- summaryContent = `[早期对话摘要]: ${summaryText}`;
891
- const summary = {
892
- role: 'assistant',
893
- content: summaryContent,
894
- };
895
- logger.info(`AI summary generated (${summaryText.length} chars)`);
896
- return [summary, ...recentMessages];
897
- } catch (err) {
898
- logger.warn('Tool result compression failed:', err.message);
899
- return prepareMessagesForAPI(messages);
900
- }
901
- }
902
-
903
- /**
904
- * 执行实际压缩逻辑
905
- * @param {string} sessionId - 会话 ID
906
- * @param {Array} messages - 消息数组引用
907
- * @param {Object} messageStore - 消息存储对象
908
- * @private
909
- */
910
- async _doCompress(sessionId, messages, messageStore) {
911
- const systemMessages = messages.filter((m) => m.role === 'system');
912
- const otherMessages = messages.filter((m) => m.role !== 'system');
913
-
914
- // 保留最近的 N 条非系统消息
915
- const recentMessages = otherMessages.slice(-this._keepRecentMessages);
916
- const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
917
-
918
- const compressedCount = messagesToSummarize.length;
919
- let summaryContent = '';
920
-
921
- // 尝试使用 AI 总结
922
- if (this._enableSmartCompress && this._aiClient) {
923
- try {
924
- const summaryText = await this._summarizeMessages(messagesToSummarize);
925
- summaryContent = `[早期对话摘要]: ${summaryText}`;
926
- logger.info(`AI summary generated (${summaryText.length} chars)`);
927
- } catch (err) {
928
- logger.warn('AI summary failed, using simple compression:', err.message);
929
- summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
930
- }
931
- } else {
932
- summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
933
- }
934
-
935
- const summary = {
936
- role: 'assistant',
937
- content: summaryContent,
938
- };
939
-
940
- // 构建保留的消息,确保 tool call 和 tool result 配对不被分离
941
- const assistantToolCalls = new Map(); // toolCallId -> assistant message index
942
- const toolResults = new Map(); // toolCallId -> tool result indices
943
-
944
- // 工具类型检查辅助函数(兼容 AI SDK 6.x)
945
- const isToolCall = (item) =>
946
- item && (item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId;
947
- const isToolResult = (item) =>
948
- item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId;
949
-
950
- // 第一遍:找出所有 tool call 和它们的 results
951
- for (let i = 0; i < recentMessages.length; i++) {
952
- const msg = recentMessages[i];
953
- if (msg.role === 'assistant' && msg.content) {
954
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
955
- for (const item of content) {
956
- if (isToolCall(item)) {
957
- assistantToolCalls.set(item.toolCallId, i);
958
- }
959
- }
960
- }
961
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
962
- // 遍历 tool message 中的所有 tool-results
963
- for (const item of msg.content) {
964
- if (isToolResult(item)) {
965
- if (!toolResults.has(item.toolCallId)) {
966
- toolResults.set(item.toolCallId, []);
967
- }
968
- toolResults.get(item.toolCallId).push(i);
969
- }
970
- }
971
- }
972
- }
973
-
974
- // 第二遍:找出哪些 tool call 没有对应的 result(需要保留)
975
- const orphanToolCalls = new Set();
976
- for (const [toolCallId, assistantIdx] of assistantToolCalls) {
977
- if (!toolResults.has(toolCallId)) {
978
- orphanToolCalls.add(toolCallId);
979
- }
980
- }
981
-
982
- // 过滤时保留:有 result 的 tool call,以及没有 result 的 tool call(但要附带其 result)
983
- // 第一遍:先确定哪些 assistant 消息要保留
984
- const assistantIndicesToKeep = new Set();
985
- for (let i = 0; i < recentMessages.length; i++) {
986
- const msg = recentMessages[i];
987
- if (msg.role === 'assistant') {
988
- // 检查这个 assistant 消息是否有 tool call
989
- let hasToolCall = false;
990
- if (msg.content) {
991
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
992
- for (const item of content) {
993
- if (isToolCall(item)) {
994
- hasToolCall = true;
995
- break;
996
- }
997
- }
998
- }
999
- if (hasToolCall) {
1000
- assistantIndicesToKeep.add(i);
1001
- } else if (i >= recentMessages.length - 3) {
1002
- // 保留最近几条 assistant 消息(它们可能包含重要上下文)
1003
- assistantIndicesToKeep.add(i);
1004
- }
1005
- }
1006
- }
1007
-
1008
- // 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
1009
- for (const [toolCallId, assistantIdx] of assistantToolCalls) {
1010
- if (!toolResults.has(toolCallId)) {
1011
- assistantIndicesToKeep.add(assistantIdx);
1012
- }
1013
- }
1014
-
1015
- // 第二遍:基于 assistant 的保留情况,确定哪些 tool result 要保留
1016
- const indicesToKeep = new Set();
1017
- for (let i = 0; i < recentMessages.length; i++) {
1018
- const msg = recentMessages[i];
1019
- if (msg.role === 'tool') {
1020
- // 检查这个 tool result 是否对应一个被保留的 assistant tool_call
1021
- // 如果对应的 assistant 消息被删除了,这个 tool result 也应该被删除(否则会报 orphaned error)
1022
- let hasPairedAssistant = false;
1023
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
1024
- for (const item of content) {
1025
- if (isToolResult(item)) {
1026
- // 检查这个 toolCallId 对应的 assistant 消息是否被保留
1027
- const assistantIdx = assistantToolCalls.get(item.toolCallId);
1028
- if (assistantIdx !== undefined && assistantIndicesToKeep.has(assistantIdx)) {
1029
- hasPairedAssistant = true;
1030
- break;
1031
- }
1032
- }
1033
- }
1034
- // 只有有配对的 assistant 消息被保留时,才保留这个 tool result
1035
- if (hasPairedAssistant) {
1036
- indicesToKeep.add(i);
1037
- } else {
1038
- logger.debug(`Dropping orphaned tool result at index ${i}`);
1039
- }
1040
- } else if (msg.role === 'assistant') {
1041
- if (assistantIndicesToKeep.has(i)) {
1042
- indicesToKeep.add(i);
1043
- }
1044
- } else {
1045
- // user, system 等角色默认保留
1046
- indicesToKeep.add(i);
1047
- }
1048
- }
1049
-
1050
- // 按索引排序并重建消息数组
1051
- const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
1052
- const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
1053
-
1054
- // 直接修改传入的 messages 数组
1055
- messages.length = 0;
1056
- messages.push(...systemMessages, summary, ...filteredRecentMessages);
1057
-
1058
- // 更新压缩状态
1059
- this._compressionCount++;
1060
- if (messageStore.compressionState) {
1061
- messageStore.compressionState.count++;
1062
- messageStore.compressionState.lastCompressedAt = Date.now();
1063
- messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);
1064
- }
1065
-
1066
- const totalTokens = this._countMessagesTokens(messages);
1067
- logger.info(
1068
- `Context compressed (${this._compressionCount} times). Messages: ${messages.length}, Est. tokens: ${totalTokens}`
1069
- );
1070
-
1071
- // 压缩后立即同步到 session
1072
- if (sessionId) {
1073
- this._saveHistoryToSession(sessionId, messages);
1074
- }
1075
- }
1076
-
1077
- /**
1078
- * 简单压缩(当 AI 压缩失败时使用)
1079
- * @param {string} sessionId - 会话 ID
1080
- * @param {Array} messages - 消息数组引用
1081
- * @param {Object} messageStore - 消息存储对象
1082
- * @private
1083
- */
1084
- _simpleCompress(sessionId, messages, messageStore) {
1085
- const systemMessages = messages.filter((m) => m.role === 'system');
1086
- const otherMessages = messages.filter((m) => m.role !== 'system');
1087
- const recentMessages = otherMessages.slice(-this._keepRecentMessages);
1088
- const compressedCount = otherMessages.length - this._keepRecentMessages;
1089
-
1090
- const summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
1091
-
1092
- const summary = {
1093
- role: 'assistant',
1094
- content: summaryContent,
1095
- };
1096
-
1097
- // 工具类型检查辅助函数(兼容 AI SDK 6.x)
1098
- const isToolCall = (item) =>
1099
- item && (item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId;
1100
- const isToolResult = (item) =>
1101
- item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId;
1102
-
1103
- // 构建保留的消息,确保 tool call 和 tool result 配对不被分离
1104
- const assistantToolCalls = new Map(); // toolCallId -> assistant message index
1105
- const toolResults = new Map(); // toolCallId -> tool result indices
1106
-
1107
- // 第一遍:找出所有 tool call 和它们的 results
1108
- for (let i = 0; i < recentMessages.length; i++) {
1109
- const msg = recentMessages[i];
1110
- if (msg.role === 'assistant' && msg.content) {
1111
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
1112
- for (const item of content) {
1113
- if (isToolCall(item)) {
1114
- assistantToolCalls.set(item.toolCallId, i);
1115
- }
1116
- }
1117
- }
1118
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
1119
- // 遍历 tool message 中的所有 tool-results
1120
- for (const item of msg.content) {
1121
- if (isToolResult(item)) {
1122
- if (!toolResults.has(item.toolCallId)) {
1123
- toolResults.set(item.toolCallId, []);
1124
- }
1125
- toolResults.get(item.toolCallId).push(i);
1126
- }
1127
- }
1128
- }
1129
- }
1130
-
1131
- // 第二遍:找出哪些 tool call 没有对应的 result(需要保留)
1132
- const orphanToolCalls = new Set();
1133
- for (const [toolCallId, assistantIdx] of assistantToolCalls) {
1134
- if (!toolResults.has(toolCallId)) {
1135
- orphanToolCalls.add(toolCallId);
1136
- }
1137
- }
1138
-
1139
- // 过滤时保留:有 result 的 tool call,以及没有 result 的 tool call
1140
- const indicesToKeep = new Set();
1141
- for (let i = 0; i < recentMessages.length; i++) {
1142
- const msg = recentMessages[i];
1143
- if (msg.role === 'tool') {
1144
- // 保留所有 tool result
1145
- indicesToKeep.add(i);
1146
- } else if (msg.role === 'assistant') {
1147
- let hasToolCall = false;
1148
- if (msg.content) {
1149
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
1150
- for (const item of content) {
1151
- if (isToolCall(item)) {
1152
- hasToolCall = true;
1153
- break;
1154
- }
1155
- }
1156
- }
1157
- if (hasToolCall) {
1158
- indicesToKeep.add(i);
1159
- } else if (i >= recentMessages.length - 3) {
1160
- // 保留最近几条 assistant 消息
1161
- indicesToKeep.add(i);
1162
- }
1163
- } else {
1164
- // user, system 等角色默认保留
1165
- indicesToKeep.add(i);
1166
- }
1167
- }
1168
-
1169
- // 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
1170
- for (const [toolCallId, assistantIdx] of assistantToolCalls) {
1171
- if (!toolResults.has(toolCallId)) {
1172
- indicesToKeep.add(assistantIdx);
1173
- }
1174
- }
1175
-
1176
- // 按索引排序并重建消息数组
1177
- const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
1178
- const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
1179
-
1180
- // 直接修改传入的 messages 数组
1181
- messages.length = 0;
1182
- messages.push(...systemMessages, summary, ...filteredRecentMessages);
1183
-
1184
- // 更新压缩状态
1185
- this._compressionCount++;
1186
- if (messageStore.compressionState) {
1187
- messageStore.compressionState.count++;
1188
- messageStore.compressionState.lastCompressedAt = Date.now();
1189
- messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);
1190
- }
1191
-
1192
- logger.info(
1193
- `Context simple compressed (${this._compressionCount} times). Messages: ${messages.length}`
1194
- );
1195
-
1196
- // 压缩后立即同步到 session
1197
- if (sessionId) {
1198
- this._saveHistoryToSession(sessionId, messages);
1199
- }
1200
- }
1201
-
1202
- /**
1203
- * 使用 AI 对消息进行总结
1204
- * @param {Array} messages - 要总结的消息
1205
- * @returns {Promise<string>} 总结文本
1206
- * @private
1207
- */
1208
- async _summarizeMessages(messages) {
1209
- if (!this._aiClient || messages.length === 0) {
1210
- return '(无早期对话)';
1211
- }
1212
-
1213
- // 构建总结提示
1214
- const conversationText = messages
1215
- .map((m) => {
1216
- const role = m.role === 'user' ? '用户' : '助手';
1217
- const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
1218
- return `${role}: ${content}`;
1219
- })
1220
- .join('\n');
1221
-
1222
- const summarizePrompt = `请简洁地总结以下对话的要点,保留关键信息和用户需求:
1223
-
1224
- ${conversationText}
1225
-
1226
- 总结要求:
1227
- 1. 提取用户的主要需求和意图
1228
- 2. 保留关键的技术细节或决策
1229
- 3. 不要超过 1000 字
1230
- 4. 用中文回复`;
1231
-
1232
- // 使用 AI SDK 6.x 的 generateText
1233
- const { text } = await generateText({
1234
- model: this._aiClient,
1235
- prompt: summarizePrompt,
1236
- ...this.providerOptions,
1237
- });
1238
-
1239
- return text || '(总结生成失败)';
1240
- }
1241
-
1242
- /**
1243
- * 检查工具结果是否需要压缩
1244
- * @param {any} result - 工具返回结果
1245
- * @returns {boolean}
1246
- * @private
1247
- */
1248
- _shouldCompressToolResult(result) {
1249
- if (!result || this._maxToolResultSize <= 0) return false;
1250
-
1251
- // 计算结果的大小
1252
- let size = 0;
1253
- if (typeof result === 'string') {
1254
- size = result.length;
1255
- } else if (typeof result === 'object') {
1256
- try {
1257
- size = JSON.stringify(result).length;
1258
- } catch {
1259
- size = String(result).length;
1260
- }
1261
- }
1262
-
1263
- return size > this._maxToolResultSize;
1264
- }
1265
-
1266
- /**
1267
- * 压缩工具结果
1268
- * @param {any} result - 工具返回结果
1269
- * @returns {Promise<any>} 压缩后的结果
1270
- * @private
1271
- */
1272
- async _compressToolResult(result) {
1273
- if (!this._shouldCompressToolResult(result)) {
1274
- return result;
1275
- }
1276
-
1277
- if (!this._aiClient) {
1278
- logger.warn('Cannot compress tool result: no AI client');
1279
- return this._fallbackCompress(result);
1280
- }
1281
-
1282
- try {
1283
- const originalSize =
1284
- typeof result === 'string' ? result.length : JSON.stringify(result).length;
1285
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1286
-
1287
- // 对于超大型内容(如网页),采用更好的截断策略
1288
- const maxInputSize = 6000; // 保留给 AI 处理的输入大小
1289
- const shouldTruncate = resultStr.length > maxInputSize;
1290
- const truncatedContent = resultStr.substring(0, maxInputSize);
1291
- const truncatedNote = shouldTruncate ? `\n\n[内容已截断,原始长度 ${originalSize} 字符]` : '';
1292
-
1293
- // 检测内容类型
1294
- const isHTML =
1295
- resultStr.startsWith('<') || resultStr.includes('<html') || resultStr.includes('<!DOCTYPE');
1296
- const isJSON = !isHTML && (resultStr.startsWith('{') || resultStr.startsWith('['));
1297
- const contentTypeHint = isHTML ? '(HTML 网页内容)' : isJSON ? '(JSON 数据)' : '';
1298
-
1299
- // 构建压缩提示
1300
- const compressPrompt = `以下是一个工具执行结果${contentTypeHint},长度 ${originalSize} 字符。请简洁地总结其核心内容:
1301
-
1302
- ${truncatedContent}${truncatedNote}
1303
-
1304
- 请提取并保留:
1305
- 1. 主要标题和主题
1306
- 2. 关键信息点(不超过 5 个)
1307
- 3. 重要数据或结论
1308
-
1309
- 用简洁的中文总结,不超过 1000 字:`;
1310
-
1311
- // 使用 AI SDK 6.x 的 generateText
1312
- const { text } = await generateText({
1313
- model: this._aiClient,
1314
- prompt: compressPrompt,
1315
- ...this.providerOptions,
1316
- maxTokens: 500,
1317
- });
1318
-
1319
- const summary = text || '(总结生成失败)';
1320
- const compressed = `[工具结果已压缩${contentTypeHint}: ${originalSize} -> ${summary.length} 字符]\n\n${summary}`;
1321
-
1322
- logger.info(`Tool result compressed: ${originalSize} -> ${summary.length} chars`);
1323
- return compressed;
1324
- } catch (err) {
1325
- logger.warn('Tool result compression failed:', err.message);
1326
- return this._fallbackCompress(result);
1327
- }
1328
- }
1329
-
1330
- /**
1331
- * 回退压缩方法(当 AI 客户端不可用时)
1332
- * @param {any} result - 工具返回结果
1333
- * @returns {string} 压缩后的结果
1334
- * @private
1335
- */
1336
- _fallbackCompress(result) {
1337
- const originalSize = typeof result === 'string' ? result.length : JSON.stringify(result).length;
1338
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1339
-
1340
- // 简单截断策略:保留前 2000 字符
1341
- const maxSize = 2000;
1342
- if (resultStr.length <= maxSize) {
1343
- return result;
1344
- }
1345
-
1346
- const compressed = `[工具结果已压缩(简单截断): ${originalSize} -> ${maxSize} 字符]\n\n${resultStr.substring(0, maxSize)}\n\n...[内容已截断,原文 ${originalSize} 字符]`;
1347
- logger.info(`Tool result fallback compressed: ${originalSize} -> ${maxSize} chars`);
1348
- return compressed;
1349
- }
1350
-
1351
- /**
1352
- * 设置 AI 客户端
1353
- * @param {Object} client - AI 模型客户端
1354
- */
1355
- setAIClient(client) {
1356
- this._aiClient = client;
1357
- return this;
1358
- }
1359
-
1360
- /**
1361
- * 设置系统提示
1362
- * @param {string} prompt
1363
- */
1364
- setSystemPrompt(prompt) {
1365
- this._systemPrompt = prompt;
1366
- return this;
1367
- }
1368
-
1369
- /**
1370
- * 注册工具
1371
- * @param {Object} toolDef - 工具定义
1372
- */
1373
- registerTool(toolDef) {
1374
- this._tools.set(toolDef.name, toolDef);
1375
- return this;
1376
- }
1377
-
1378
- /**
1379
- * 清空对话历史
1380
- * @param {string} sessionId - 可选,指定 session 则同时清除 session 中的历史
1381
- */
1382
- clearHistory(sessionId) {
1383
- if (sessionId) {
1384
- // 清除 Per-Session 消息存储
1385
- const messageStore = this._sessionMessageStores.get(sessionId);
1386
- if (messageStore) {
1387
- messageStore.messages = [];
1388
- messageStore.historyLoaded = false;
1389
- messageStore.compressionState = {
1390
- lastCompressedAt: null,
1391
- lastTokenCount: 0,
1392
- count: 0,
1393
- };
1394
- }
1395
- // 清除 SessionContext 中的历史
1396
- if (this.agent?.framework) {
1397
- try {
1398
- const sessionCtx = this.agent.framework.getSessionContext(sessionId);
1399
- if (sessionCtx) {
1400
- sessionCtx.clearMessages();
1401
- }
1402
- } catch (err) {
1403
- // 忽略错误
1404
- }
1405
- }
1406
- } else {
1407
- // 清除所有 Per-Session 消息存储
1408
- this._sessionMessageStores.clear();
1409
- }
1410
- this._compressionCount = 0;
1411
- return this;
1412
- }
1413
-
1414
- /**
1415
- * 获取已注册的工具
1416
- * @returns {Array}
1417
- */
1418
- getTools() {
1419
- return Array.from(this._tools.values());
1420
- }
1421
-
1422
- /**
1423
- * 导入 AI SDK(动态导入)
1424
- * @private
1425
- */
1426
- async _importAI() {
1427
- try {
1428
- const ai = require('ai');
1429
- return {
1430
- tool: ai.tool,
1431
- ToolLoopAgent: ai.ToolLoopAgent,
1432
- };
1433
- } catch (err) {
1434
- throw new Error('AI SDK not found. Please install: npm install ai');
1435
- }
1436
- }
1437
-
1438
- /**
1439
- * 发送消息(非流式)- 使用队列机制
1440
- * @param {string|Object} message - 消息
1441
- * @param {Object} options - 选项
1442
- */
1443
- async chat(message, options = {}) {
1444
- const sessionId = options.sessionId || null;
1445
- return this.enqueue(sessionId, message, options);
1446
- }
1447
-
1448
- /**
1449
- * 准备聊天会话:刷新系统提示词、添加用户消息、压缩上下文
1450
- * @param {string|Object} message - 消息
1451
- * @param {string} sessionId - 会话 ID
1452
- * @returns {Object} - { messageStore, messages, tools, maxSteps }
1453
- * @private
1454
- */
1455
- async _prepareSession(message, sessionId) {
1456
- const messageStore = this._getSessionMessageStore(sessionId);
1457
- const messages = messageStore.messages;
591
+ async _prepareSession(message, sessionId) {
592
+ const messageStore = this._getSessionMessageStore(sessionId);
593
+ const messages = messageStore.messages;
1458
594
 
1459
595
  // 刷新系统提示词
1460
596
  this._systemPrompt = this.agent._buildSystemPrompt();
1461
597
 
1462
598
  // 添加用户消息
1463
599
  const userMessage = typeof message === 'string' ? { role: 'user', content: message } : message;
1464
- messages.push(userMessage);
1465
- //await fs.writeFile('system.md',this._systemPrompt)
600
+
1466
601
  // 检查上下文大小,必要时压缩
1467
602
  const messagesTokens = this._countMessagesTokens(messages);
1468
603
  const toolsTokens = this._countToolsTokens();
@@ -1475,23 +610,8 @@ ${truncatedContent}${truncatedNote}
1475
610
  `Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
1476
611
  );
1477
612
  await this._compressContext(sessionId, messages, messageStore);
1478
- const validated = this._validateMessagesPairing(messages);
1479
- if (validated.length !== messages.length) {
1480
- messages.length = 0;
1481
- messages.push(...validated);
1482
- }
1483
- }
1484
-
1485
- // 验证消息配对
1486
- const validatedMessages = this._validateMessagesPairing(messages);
1487
- if (validatedMessages.length !== messages.length) {
1488
- messages.length = 0;
1489
- messages.push(...validatedMessages);
1490
613
  }
1491
614
 
1492
- // 验证工具调用
1493
- this._validateToolCalls(messages);
1494
-
1495
615
  // 最终 token 检查
1496
616
  const finalTokens =
1497
617
  this._countMessagesTokens(messages) +
@@ -1500,205 +620,146 @@ ${truncatedContent}${truncatedNote}
1500
620
  if (finalTokens > this._maxContextTokens * 0.5) {
1501
621
  logger.warn('Still over limit after validation, forcing compression');
1502
622
  await this._compressContext(sessionId, messages, messageStore);
1503
- const compressed = this._validateMessagesPairing(messages);
1504
- messages.length = 0;
1505
- messages.push(...compressed);
1506
623
  }
624
+ // 验证工具调用
1507
625
 
1508
- const maxSteps = this._maxSteps;
626
+ const validated = this._validateMessagesPairing(messages);
627
+ if (validated.length !== messages.length) {
628
+ messages.length = 0;
629
+ messages.push(...validated);
630
+ }
631
+ messages.push(userMessage);
632
+ this._validateToolCalls(messages);
1509
633
 
1510
- return { messageStore, messages, maxSteps };
634
+ return { messageStore, messages };
1511
635
  }
1512
636
 
1513
637
  /**
1514
- * 创建 prepareStep 函数(供 AI SDK 调用)
1515
- * @returns {Function}
638
+ * 更新消息存储的使用量
1516
639
  * @private
1517
640
  */
1518
- _createPrepareStep() {
1519
- return async ({ stepNumber, messages: inputMessages }) => {
1520
- try {
1521
- if (!Array.isArray(inputMessages)) {
1522
- return { messages: inputMessages };
1523
- }
1524
-
1525
- this._validateToolCalls(inputMessages);
1526
- const pairedMessages = this._validateMessagesPairing(inputMessages);
1527
- if (pairedMessages.length !== inputMessages.length) {
1528
- inputMessages.length = 0;
1529
- inputMessages.push(...pairedMessages);
1530
- }
1531
-
1532
- const tokenCount = this._countMessagesTokens(inputMessages);
1533
- const tokenLimit = this._maxContextTokens * 0.75;
1534
- if (inputMessages.length <= 30 && tokenCount <= tokenLimit) {
1535
- return { messages: inputMessages };
1536
- }
1537
-
1538
- const pruned = this._prepareMessagesForAI(inputMessages);
1539
- return { messages: pruned };
1540
- } catch (err) {
1541
- logger.error('prepareStep error:', err.message);
1542
- return { messages: inputMessages };
1543
- }
1544
- };
641
+ _updateMessageStoreUsage(messageStore, usage) {
642
+ if (messageStore && usage) {
643
+ messageStore.usage = usage;
644
+ }
1545
645
  }
1546
646
 
1547
647
  /**
1548
- * 执行实际聊天逻辑
1549
- * @param {string|Object} message - 消息
1550
- * @param {Object} options - 选项
648
+ * 获取 AI 工具格式
649
+ * 使用 AI SDK tool() 格式
1551
650
  * @private
1552
651
  */
1553
- async _doChat(message, options = {}) {
1554
- const sessionId = options.sessionId || null;
1555
- const framework = this.agent.framework;
1556
-
1557
- try {
1558
- const { tool, ToolLoopAgent } = await this._importAI();
1559
- const { messageStore, messages, maxSteps } = await this._prepareSession(message, sessionId);
1560
-
1561
- if (!this._aiClient) {
1562
- throw new Error('AI client not configured.');
1563
- }
1564
-
1565
- const tools = this._getAITools(tool);
1566
- const agent = new ToolLoopAgent({
1567
- model: this._aiClient,
1568
- instructions: this._systemPrompt,
1569
- tools: tools,
1570
- prepareStep: this._createPrepareStep(),
1571
- });
1572
-
1573
- const result = await framework.runInSession(sessionId, {}, async () => {
1574
- return agent.generate({ messages, ...this.providerOptions });
1575
- });
1576
-
1577
- if (result.usage) {
1578
- this._updateMessageStoreUsage(messageStore, result.usage);
1579
- }
652
+ _getAITools(toolFn) {
653
+ const tools = {};
654
+ const allTools = this.agent.framework.getTools();
655
+ for (const toolDef of allTools) {
656
+ const toolName = toolDef.name;
1580
657
 
1581
- messages.push(...result.response.messages);
1582
- const userMsg = messages[messages.length - result.response.messages.length - 1];
1583
- this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });
658
+ // 使用 AI SDK 的 tool() 格式
659
+ const toolConfig = {
660
+ name: toolName,
661
+ description: toolDef.description || '',
662
+ execute: async (args) => {
663
+ // 清理参数
664
+ const cleanedArgs = this._cleanToolArgs(args);
1584
665
 
1585
- return {
1586
- success: true,
1587
- message: cleanResponse(result.text || ''),
1588
- stepCount: result.stepCount || 1,
666
+ // 执行工具
667
+ this.emit('tool-call', { name: toolName, args: cleanedArgs });
668
+ try {
669
+ const result = await toolDef.execute(cleanedArgs, this.agent.framework);
670
+ this.emit('tool-result', { name: toolName, args: cleanedArgs, result });
671
+ // 确保返回字符串或可序列化的对象
672
+ if (result === null || result === undefined) {
673
+ return 'OK';
674
+ }
675
+ if (typeof result === 'object') {
676
+ return JSON.stringify(result);
677
+ }
678
+ return String(result);
679
+ } catch (err) {
680
+ this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
681
+ // 返回错误信息字符串,而不是抛出异常
682
+ return JSON.stringify({ error: err.message, success: false });
683
+ }
684
+ },
1589
685
  };
1590
- } catch (err) {
1591
- console.log(err);
1592
- const errorMsg = err.message || String(err);
1593
686
 
1594
- return {
1595
- success: false,
1596
- message: 'AI 服务暂时不可用,请稍后重试。',
1597
- error: errorMsg,
1598
- stepCount: 0,
1599
- };
1600
- } finally {
1601
- const messageStore = this._getSessionMessageStore(sessionId);
1602
- messageStore.save();
687
+ // 支持 inputSchema 或 parameters
688
+ if (toolDef.inputSchema) {
689
+ toolConfig.inputSchema = toolDef.inputSchema;
690
+ } else if (toolDef.parameters) {
691
+ toolConfig.parameters = toolDef.parameters;
692
+ }
693
+
694
+ // AI SDK 6.x 使用对象形式,键为工具名
695
+ tools[toolName] = toolFn(toolConfig);
1603
696
  }
697
+
698
+ return tools;
1604
699
  }
1605
700
 
1606
701
  /**
1607
- * 发送消息(流式)
1608
- * @param {string|Object} message - 消息
1609
- * @param {Object} options - 选项
702
+ * 清理工具参数
703
+ * @private
1610
704
  */
1611
- async *chatStream(message, options = {}) {
1612
- const sessionId = options.sessionId || null;
1613
- const framework = this.agent.framework;
1614
-
1615
- try {
1616
- const { tool, ToolLoopAgent } = await this._importAI();
1617
- const { messageStore, messages } = await this._prepareSession(message, sessionId);
705
+ _cleanToolArgs(args) {
706
+ if (!args || typeof args !== 'object') {
707
+ return {};
708
+ }
1618
709
 
1619
- if (!this._aiClient) {
1620
- throw new Error('AI client not configured.');
710
+ const cleaned = {};
711
+ for (const [key, value] of Object.entries(args)) {
712
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
713
+ continue;
1621
714
  }
715
+ cleaned[key] = value;
716
+ }
717
+ return cleaned;
718
+ }
1622
719
 
1623
- const tools = this._getAITools(tool);
1624
-
1625
- const agent = new ToolLoopAgent({
1626
- model: this._aiClient,
1627
- instructions: this._systemPrompt,
1628
- tools: tools,
1629
- stopWhen: isLoopFinished(),
1630
- prepareStep: this._createPrepareStep(),
1631
- });
1632
-
1633
- const result = await framework.runInSession(sessionId, {}, async () => {
1634
- return agent.stream({ messages, ...this.providerOptions });
1635
- });
1636
-
1637
- const stream = result.fullStream;
1638
- let fullText = '';
1639
- const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;
720
+ /**
721
+ * 为 AI SDK prepareStep 准备消息
722
+ * @private
723
+ */
724
+ _createPrepareStep() {
725
+ return async ({ stepNumber, messages: inputMessages }) => {
726
+ try {
727
+ const tokenCount = this._countMessagesTokens(inputMessages);
728
+ const tokenLimit = this._maxContextTokens * 0.75;
1640
729
 
1641
- for await (const part of iterator || stream) {
1642
- if (part.type === 'text-delta') {
1643
- const text = part.text || part.textDelta || '';
1644
- fullText += text;
1645
- yield { type: 'text', text };
1646
- } else if (part.type === 'reasoning') {
1647
- // this.emit('thinking', { content: part.text });
1648
- yield { type: 'thinking', text: part.text };
1649
- } else if (part.type === 'tool-call') {
1650
- yield { type: 'tool-call', toolName: part.toolName, input: part.input };
1651
- } else if (part.type === 'tool-result') {
1652
- yield { type: 'tool-result', toolName: part.toolName, result: part.output };
1653
- } else if (part.type === 'error') {
1654
- yield { type: 'error', error: part.error };
730
+ if (inputMessages.length > 50 || tokenCount > tokenLimit) {
731
+ const pairedMessages = this._compressContextForPrepare(inputMessages);
732
+ if (pairedMessages.length !== inputMessages.length) {
733
+ inputMessages.length = 0;
734
+ inputMessages.push(...pairedMessages);
735
+ }
1655
736
  }
1656
- }
1657
-
1658
- const finishMessages = (await result.response).messages;
1659
- messages.push(...finishMessages);
1660
- const usage = await result.totalUsage;
1661
- if (usage) {
1662
- this._updateMessageStoreUsage(messageStore, usage);
1663
- }
1664
-
1665
- const userMsg = messages[messages.length - finishMessages.length - 1];
1666
- this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });
1667
- } catch (err) {
1668
- const errorMsg = err.message || String(err);
1669
- const isTransientError =
1670
- errorMsg.includes('Invalid JSON response') ||
1671
- errorMsg.includes('负载较高') ||
1672
- err.name === 'AI_RetryError' ||
1673
- err.name === 'AI_APICallError';
1674
737
 
1675
- if (isTransientError) {
1676
- yield { type: 'error', error: 'AI 服务暂时不可用,请稍后重试。' };
1677
- } else {
1678
- logger.error(`[AgentChat] Stream error: ${err.message}`);
1679
- this.emit('error', { error: err.message });
1680
- yield { type: 'error', error: err.message };
738
+ const validated = this._validateMessagesPairing(inputMessages);
739
+ if (validated.length !== inputMessages.length) {
740
+ inputMessages.length = 0;
741
+ inputMessages.push(...validated);
742
+ }
743
+ this._validateToolCalls(inputMessages);
744
+ return { messages: inputMessages };
745
+ } catch (err) {
746
+ logger.error('prepareStep error:', err.message);
747
+ return { messages: inputMessages };
1681
748
  }
1682
- } finally {
1683
- const messageStore = this._getSessionMessageStore(sessionId);
1684
- messageStore.save();
1685
- }
749
+ };
1686
750
  }
1687
751
 
1688
752
  /**
1689
- * AI SDK prepareStep 准备消息
1690
- * 确保 tool call 和 tool result 正确配对
1691
- * @param {Array} messages - 消息列表
1692
- * @returns {Array} 过滤后的消息数组
753
+ * 压缩上下文(用于 prepareStep
1693
754
  * @private
1694
755
  */
1695
- _prepareMessagesForAI(messages) {
1696
- if (messages.length <= 15) {
756
+ async _compressContextForPrepare(messages) {
757
+ if (messages.length <= 30) {
1697
758
  return messages;
1698
759
  }
1699
760
 
1700
761
  // 1. 确定要保留的消息范围(保留最近 N 条)
1701
- const keepRecentCount = Math.min(30, Math.floor(messages.length * 0.4));
762
+ const keepRecentCount = Math.min(10, Math.floor(messages.length * 0.4));
1702
763
  const minIndexToKeep = Math.max(0, messages.length - keepRecentCount);
1703
764
 
1704
765
  // 2. 只从保留下来的消息(minIndexToKeep 之后)中收集 assistant 的 tool-call IDs
@@ -1793,317 +854,104 @@ ${truncatedContent}${truncatedNote}
1793
854
  }
1794
855
 
1795
856
  /**
1796
- * 修剪消息确保配对的 tool call/result 不被拆分
1797
- * @param {number} stepNumber - 当前步骤号
1798
- * @param {Array} messages - 消息列表
1799
- * @returns {Object} { messages } 或空对象
857
+ * 计算 token
1800
858
  * @private
1801
859
  */
1802
- _prepareStepForPruning(stepNumber, messages) {
1803
- if (messages.length <= 50) {
1804
- return {};
1805
- }
1806
-
1807
- // 工具类型检查辅助函数(兼容 AI SDK 6.x)
1808
- const isToolCall = (item) =>
1809
- item && (item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId;
1810
- const isToolResult = (item) =>
1811
- item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId;
1812
-
1813
- // 构建配对索引集合:确保 assistant 的 tool call 和 tool result 不会被单独裁剪
1814
- const assistantIndices = new Map(); // toolCallId -> assistant index
1815
- const toolResultIndices = new Map(); // toolCallId -> tool result indices
1816
-
1817
- for (let i = 0; i < messages.length; i++) {
1818
- const msg = messages[i];
1819
- if (msg.role === 'assistant' && msg.content) {
1820
- const content = Array.isArray(msg.content) ? msg.content : [msg.content];
1821
- for (const item of content) {
1822
- if (isToolCall(item)) {
1823
- assistantIndices.set(item.toolCallId, i);
1824
- }
1825
- }
1826
- }
1827
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
1828
- // 遍历 tool message 中的所有 tool-results
1829
- for (const item of msg.content) {
1830
- if (isToolResult(item)) {
1831
- if (!toolResultIndices.has(item.toolCallId)) {
1832
- toolResultIndices.set(item.toolCallId, []);
1833
- }
1834
- toolResultIndices.get(item.toolCallId).push(i);
1835
- }
1836
- }
1837
- }
1838
- }
1839
-
1840
- const recentCount = Math.max(10, messages.length - 10);
1841
- let minIndexToKeep = messages.length - recentCount;
1842
-
1843
- // 情况1:保留被裁剪范围内的 assistant 消息及其 tool result
1844
- for (const [toolCallId, assistantIdx] of assistantIndices) {
1845
- if (assistantIdx < minIndexToKeep) {
1846
- minIndexToKeep = Math.min(minIndexToKeep, assistantIdx);
1847
- }
1848
- }
1849
-
1850
- // 情况2:保留被裁剪范围内的 tool result 及其 assistant 消息
1851
- for (const [toolCallId, resultIndices] of toolResultIndices) {
1852
- for (const resultIdx of resultIndices) {
1853
- if (resultIdx >= minIndexToKeep) {
1854
- const assistantIdx = assistantIndices.get(toolCallId);
1855
- if (assistantIdx !== undefined && assistantIdx < minIndexToKeep) {
1856
- minIndexToKeep = Math.min(minIndexToKeep, assistantIdx);
1857
- }
1858
- }
1859
- }
1860
- }
1861
-
1862
- const pruned = messages.slice(minIndexToKeep);
1863
- // logger.info(
1864
- // `prepareStep pruned: ${messages.length} -> ${pruned.length} (step ${stepNumber}, kept from index ${minIndexToKeep})`
1865
- // );
1866
- return { messages: pruned };
860
+ _countTokens(text, bytesPerToken = 4) {
861
+ if (!text) return 0;
862
+ return Math.ceil(Buffer.byteLength(String(text), 'utf8') / bytesPerToken);
1867
863
  }
1868
864
 
1869
865
  /**
1870
- * 验证并修复消息中的 tool call/result 配对
1871
- * 删除没有对应 assistant tool_call 的 tool result(会导致 API 报错)
1872
- * 兼容 AI SDK 6.x 标准格式
1873
- * @param {Array} messages - 消息列表
866
+ * 计算消息 token
1874
867
  * @private
1875
868
  */
1876
- _validateMessagesPairing(messages) {
1877
- // 收集所有 assistant 的 tool-call IDs(兼容多种类型名)
1878
- const assistantToolCallIds = new Set();
1879
- for (const msg of messages) {
1880
- if (msg.role === 'assistant' && Array.isArray(msg.content)) {
1881
- for (const item of msg.content) {
1882
- // 兼容 tool-call 和 tool-use 两种类型
1883
- if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
1884
- assistantToolCallIds.add(item.toolCallId);
1885
- }
1886
- }
1887
- }
1888
- }
1889
-
1890
- // 检查并删除没有配对的 tool result
1891
- let removedCount = 0;
869
+ _countMessagesTokens(messages) {
870
+ if (!Array.isArray(messages)) return 0;
871
+ let total = 0;
1892
872
  for (const msg of messages) {
1893
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
1894
- const originalLength = msg.content.length;
1895
- msg.content = msg.content.filter((item) => {
1896
- // 兼容 tool-result 和 tool_result 两种类型
1897
- if (
1898
- item &&
1899
- (item.type === 'tool-result' || item.type === 'tool_result') &&
1900
- item.toolCallId
1901
- ) {
1902
- if (!assistantToolCallIds.has(item.toolCallId)) {
1903
- removedCount++;
1904
- return false; // 删除没有配对的 tool result
1905
- }
1906
- }
1907
- return true;
1908
- });
1909
-
1910
- // 如果所有 content 都被删除了,标记整个消息待删除
1911
- if (msg.content.length === 0 && originalLength > 0) {
1912
- msg._orphaned = true;
1913
- }
1914
- }
1915
- }
1916
-
1917
- // 移除被标记为 orphaned 的 tool 消息
1918
- const originalLength = messages.length;
1919
- const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
1920
-
1921
- if (removedCount > 0 || filtered.length !== originalLength) {
1922
- logger.debug(
1923
- `Removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
1924
- );
873
+ total += this._countMessageTokens(msg);
1925
874
  }
1926
-
1927
- return filtered;
875
+ return total;
1928
876
  }
1929
877
 
1930
878
  /**
1931
- * 校验并修复消息中的工具调用参数
1932
- * 移除不完整的 JSON(如只有 "{" )的工具调用
1933
- * 同时清理 tool result 中的错误信息
1934
- * 兼容 AI SDK 6.x 标准格式
1935
- * @param {Array} messages - 消息列表
879
+ * 计算单条消息 token
1936
880
  * @private
1937
881
  */
1938
- _validateToolCalls(messages) {
1939
- let fixedCount = 0;
1940
- // 收集被跳过的 toolCallId,用于清理对应的 tool-result
1941
- const invalidatedToolCallIds = new Set();
1942
-
1943
- for (const msg of messages) {
1944
- // 清理 assistant 消息中的不完整 tool-call
1945
- if (msg.role === 'assistant' && Array.isArray(msg.content)) {
1946
- for (const item of msg.content) {
1947
- // 兼容 tool-call 和 tool-use 两种类型
1948
- if (item.type !== 'tool-call' && item.type !== 'tool-use') {
1949
- continue;
1950
- }
1951
-
1952
- const input = item.input;
1953
- if (typeof input !== 'string') {
1954
- continue;
1955
- }
882
+ _countMessageTokens(msg) {
883
+ if (!msg) return 0;
884
+ let total = 4;
1956
885
 
1957
- // 检查 input 是否是有效的 JSON(不是不完整的)
1958
- const trimmed = input.trim();
1959
- if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
1960
- // 不完整的 JSON,移除这个 tool-call
1961
- // 记录 toolCallId,以便后续清理对应的 tool-result
1962
- if (item.toolCallId) {
1963
- invalidatedToolCallIds.add(item.toolCallId);
1964
- }
1965
- logger.warn(
1966
- `_validateToolCalls: invalid tool-call input="${input}", toolCallId=${item.toolCallId}, converting to text`
1967
- );
1968
- item.type = 'text';
1969
- item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
1970
- delete item.toolCallId;
1971
- delete item.toolName;
1972
- delete item.input;
1973
- fixedCount++;
886
+ if (typeof msg.content === 'string') {
887
+ total += this._countTokens(msg.content);
888
+ } else if (Array.isArray(msg.content)) {
889
+ for (const block of msg.content) {
890
+ if (block.type === 'text') {
891
+ total += this._countTokens(block.text || '');
892
+ } else if (block.type === 'tool-call' || block.type === 'tool-use') {
893
+ if (block.input) {
894
+ total += this._countTokens(JSON.stringify(block.input));
1974
895
  }
896
+ } else if (block.type === 'tool-result' || block.type === 'tool_result') {
897
+ const content =
898
+ typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
899
+ total += this._countTokens(content);
1975
900
  }
1976
901
  }
1977
902
  }
1978
903
 
1979
- // 如果有无效的 tool-call,清理对应的 tool-result
1980
- if (invalidatedToolCallIds.size > 0) {
1981
- logger.warn(
1982
- `_validateToolCalls: removing ${invalidatedToolCallIds.size} tool-results with invalidated toolCallIds`
1983
- );
1984
- for (const msg of messages) {
1985
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
1986
- // 过滤掉引用了无效 toolCallId 的 tool-result
1987
- const oldLen = msg.content.length;
1988
- msg.content = msg.content.filter((item) => {
1989
- if (item.type !== 'tool-result' && item.type !== 'tool_result') {
1990
- return true;
1991
- }
1992
- // 如果 tool-result 引用的 toolCallId 已被标记为无效,则移除
1993
- if (item.toolCallId && invalidatedToolCallIds.has(item.toolCallId)) {
1994
- logger.warn(
1995
- `_validateToolCalls: removing orphaned tool-result with toolCallId=${item.toolCallId}`
1996
- );
1997
- fixedCount++;
1998
- return false;
1999
- }
2000
- return true;
2001
- });
904
+ if (msg.tool_calls) {
905
+ total += 15;
906
+ for (const tc of msg.tool_calls) {
907
+ if (tc.function) {
908
+ total += this._countTokens(tc.function.name || '');
909
+ total += this._countTokens(tc.function.arguments || '{}');
2002
910
  }
2003
911
  }
2004
912
  }
2005
913
 
2006
- if (fixedCount > 0) {
2007
- logger.info(`_validateToolCalls: Fixed ${fixedCount} incomplete tool calls/results`);
2008
- }
914
+ return total;
2009
915
  }
2010
916
 
2011
917
  /**
2012
- * 获取 AI SDK 格式的工具
2013
- * AI SDK 6.x 需要对象形式 { toolName: tool }
2014
- * @param {Function} toolFn - AI SDK 的 tool 函数
918
+ * 计算工具 token
2015
919
  * @private
2016
920
  */
2017
- _getAITools(toolFn) {
2018
- const tools = {};
2019
-
2020
- for (const [name, toolDef] of this._tools) {
2021
- const toolName = toolDef.name || name;
2022
-
2023
- // 使用 AI SDK 的 tool() 格式
2024
- // 支持 inputSchema (zod schema) 或 parameters (旧格式)
2025
- const toolConfig = {
2026
- name: toolName,
2027
- description: toolDef.description || '',
2028
- execute: async (args) => {
2029
- // 清理参数:移除 undefined、function 等无效值
2030
- const cleanedArgs = this._cleanToolArgs(args);
921
+ _countToolsTokens() {
922
+ const tools = this._toolExecutor.getAllTools();
923
+ if (!tools.length) return 0;
2031
924
 
2032
- // 执行工具
2033
- this.emit('tool-call', { name: toolName, args: cleanedArgs });
2034
- try {
2035
- const result = await toolDef.execute(cleanedArgs, this.agent.framework);
2036
- this.emit('tool-result', { name: toolName, args: cleanedArgs, result });
2037
- if (result !== null && typeof result === 'object') {
2038
- return JSON.stringify(result);
2039
- }
2040
- return String(result);
2041
- } catch (err) {
2042
- this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
2043
- return { error: err.message };
925
+ let total = 0;
926
+ for (const tool of tools) {
927
+ total += 20;
928
+ if (tool.description) {
929
+ total += this._countTokens(tool.description);
930
+ }
931
+ if (tool.inputSchema) {
932
+ const schema = tool.inputSchema.jsonSchema || tool.inputSchema;
933
+ if (schema.properties) {
934
+ for (const [name, prop] of Object.entries(schema.properties)) {
935
+ total += this._countTokens(name) + 10;
2044
936
  }
2045
- },
2046
- };
2047
-
2048
- // 支持 inputSchema (zod) 或 parameters (旧格式)
2049
- if (toolDef.inputSchema) {
2050
- toolConfig.inputSchema = toolDef.inputSchema;
2051
- } else if (toolDef.parameters) {
2052
- toolConfig.parameters = toolDef.parameters;
937
+ }
2053
938
  }
2054
-
2055
- // AI SDK 6.x 使用对象形式,键为工具名
2056
- tools[toolName] = toolFn(toolConfig);
2057
939
  }
2058
-
2059
- return tools;
940
+ return total;
2060
941
  }
2061
942
 
2062
- /**
2063
- * 清理工具参数,移除无效值
2064
- * @param {Object} args - 原始参数
2065
- * @returns {Object} 清理后的参数
2066
- * @private
2067
- */
2068
- _cleanToolArgs(args) {
2069
- if (!args || typeof args !== 'object') {
2070
- return {};
2071
- }
943
+ // ==================== 兼容性方法 ====================
2072
944
 
2073
- const cleaned = {};
2074
- for (const [key, value] of Object.entries(args)) {
2075
- // 跳过 undefined、function、symbol 等无效值
2076
- if (value === undefined || value === null) {
2077
- continue;
2078
- }
2079
- if (typeof value === 'function' || typeof value === 'symbol') {
2080
- continue;
2081
- }
2082
- // 递归清理嵌套对象
2083
- if (typeof value === 'object' && !Array.isArray(value)) {
2084
- cleaned[key] = this._cleanToolArgs(value);
2085
- } else if (Array.isArray(value)) {
2086
- cleaned[key] = value
2087
- .map((item) =>
2088
- typeof item === 'object' && item !== null ? this._cleanToolArgs(item) : item
2089
- )
2090
- .filter((item) => item !== undefined && typeof item !== 'function');
2091
- } else {
2092
- cleaned[key] = value;
2093
- }
2094
- }
2095
- return cleaned;
945
+ cancelSession(sessionId) {
946
+ this._chatSession.cancelSession(sessionId);
2096
947
  }
2097
948
 
2098
- /**
2099
- * 销毁
2100
- */
2101
- destroy() {
2102
- this._sessionMessageStores.clear();
2103
- this._sessionMessageStores = new Map();
2104
- this._tools.clear();
2105
- this._encoder = null;
2106
- this.removeAllListeners();
949
+ getQueueStatus(sessionId) {
950
+ return this._chatSession.getQueueStatus(sessionId);
951
+ }
952
+
953
+ clearQueue(sessionId) {
954
+ this._chatSession.clearQueue(sessionId);
2107
955
  }
2108
956
  }
2109
957