foliko 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/agents/code-assistant.json +14 -0
- package/.agent/agents/email-assistant.json +14 -0
- package/.agent/agents/file-assistant.json +15 -0
- package/.agent/agents/system-assistant.json +15 -0
- package/.agent/agents/web-assistant.json +12 -0
- package/.agent/data/ambient/goals.json +50 -0
- package/.agent/data/ambient/memories.json +7 -0
- package/.agent/data/default.json +3 -412
- package/.agent/data/plugins-state.json +174 -173
- package/.agent/data/scheduler/tasks.json +1 -0
- package/.agent/memory/core.md +1 -0
- package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
- package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
- package/.agent/memory/reference/mnq3oenw-46haj6.md +63 -0
- package/.agent/memory/reference/mnq5qxm2-mjoooh.md +116 -0
- package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
- package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
- package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
- package/.agent/package.json +8 -0
- package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
- package/.agent/plugins/daytona/README.md +89 -0
- package/.agent/plugins/daytona/index.js +377 -0
- package/.agent/plugins/daytona/package.json +12 -0
- package/.agent/plugins/marknative/README.md +134 -0
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/index.js +256 -0
- package/.agent/plugins/marknative/package.json +12 -0
- package/.agent/plugins/marknative/update-readme.js +134 -0
- package/.agent/plugins/poster-plugin/emojis/rocket.png +1 -0
- package/.agent/plugins/poster-plugin/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/poster-plugin/src/elements/text.js +3 -1
- package/.agent/plugins/poster-plugin/src/fonts.js +10 -0
- package/.agent/plugins/poster-plugin/yarn.lock +1007 -0
- package/.agent/plugins/system-info/index.js +387 -0
- package/.agent/plugins/system-info/package.json +4 -0
- package/.agent/plugins/system-info/test.js +40 -0
- package/.agent/plugins.json +11 -5
- package/.agent/python-scripts/test_sample.py +24 -0
- package/.agent/sessions/cli_default.json +1869 -691
- package/.agent/skills/agent-browser/SKILL.md +311 -0
- package/.agent/skills/agent-browser/TEST_PLAN.md +200 -0
- package/.agent/skills/sysinfo/SKILL.md +38 -0
- package/.agent/skills/sysinfo/system-info.sh +130 -0
- package/.agent/skills/workflow/SKILL.md +324 -0
- package/.agent/weixin.json +6 -0
- package/.agent/workflows/email-digest.json +50 -0
- package/.agent/workflows/file-backup.json +21 -0
- package/.agent/workflows/get-ip-notify.json +32 -0
- package/.agent/workflows/news-aggregator.json +93 -0
- package/.agent/workflows/news-dashboard-v2.json +94 -0
- package/.agent/workflows/notification-batch.json +32 -0
- package/.claude/settings.local.json +1 -20
- package/.env.example +56 -56
- package/README.md +441 -441
- package/cli/src/commands/chat.js +22 -13
- package/cli/src/ui/chat-ui.js +50 -37
- package/output/emoji-segoe-test-v2.png +0 -0
- package/output/emoji-segoe-test.png +0 -0
- package/output/emoji-test.png +0 -0
- package/output/emoji-windows-test.png +0 -0
- package/output/foliko-emoji-poster.png +0 -0
- package/output/foliko-muji-poster-final.png +0 -0
- package/output/foliko-muji-poster-v2.png +0 -0
- package/output/foliko-muji-poster.png +0 -0
- package/output/foliko-share.png +0 -0
- package/output/progress-circle-test.png +0 -0
- package/output/vb-agent-poster.png +0 -0
- package/package.json +1 -2
- package/plugins/default-plugins.js +4 -3
- package/plugins/extension-executor-plugin.js +12 -91
- package/plugins/file-system-plugin.js +19 -4
- package/plugins/memory-plugin.js +33 -4
- package/plugins/subagent-plugin.js +14 -37
- package/plugins/weixin-plugin.js +40 -168
- package/skills/find-skills/AGENTS.md +162 -162
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/poster-guide/SKILL.md +669 -1426
- package/src/core/agent-chat.js +439 -269
- package/src/core/agent.js +3 -6
- package/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
- package/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
- package/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
- package/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
- package/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
- package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
- package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
- package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
- package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
- package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
- package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
- package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
- package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
- package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
- package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
- package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
- package/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
- package/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
- package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
- package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
- package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
- package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
- package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
- package/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
- package/.agent/ARCHITECTURE.md +0 -288
- package/.agent/agents/ambient-agent.md +0 -57
- package/.agent/agents/debugger.md +0 -55
- package/.agent/agents/email-assistant.md +0 -49
- package/.agent/agents/file-manager.md +0 -42
- package/.agent/agents/python-developer.md +0 -60
- package/.agent/agents/scheduler.md +0 -59
- package/.agent/agents/web-developer.md +0 -45
- package/.agent/data/puppeteer-sessions/undefined.json +0 -6
- package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
- package/.agent/mcp_config_updated.json +0 -12
- package/.agent/plugins/poster-plugin/fonts/NotoColorEmoji-Regular.ttf +0 -0
- package/.agent/plugins/puppeteer-plugin/README.md +0 -147
- package/.agent/plugins/puppeteer-plugin/index.js +0 -1418
- package/.agent/plugins/puppeteer-plugin/package.json +0 -9
- package/.agent/rules/GEMINI.md +0 -273
- package/.agent/rules/allow-rule.md +0 -77
- package/.agent/rules/log-rule.md +0 -83
- package/.agent/rules/security-rule.md +0 -93
- package/.agent/scripts/auto_preview.py +0 -148
- package/.agent/scripts/checklist.py +0 -217
- package/.agent/scripts/session_manager.py +0 -120
- package/.agent/scripts/verify_all.py +0 -327
- package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +0 -11097
- package/.agent/skills/api-patterns/SKILL.md +0 -81
- package/.agent/skills/api-patterns/api-style.md +0 -42
- package/.agent/skills/api-patterns/auth.md +0 -24
- package/.agent/skills/api-patterns/documentation.md +0 -26
- package/.agent/skills/api-patterns/graphql.md +0 -41
- package/.agent/skills/api-patterns/rate-limiting.md +0 -31
- package/.agent/skills/api-patterns/response.md +0 -37
- package/.agent/skills/api-patterns/rest.md +0 -40
- package/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
- package/.agent/skills/api-patterns/security-testing.md +0 -122
- package/.agent/skills/api-patterns/trpc.md +0 -41
- package/.agent/skills/api-patterns/versioning.md +0 -22
- package/.agent/skills/app-builder/SKILL.md +0 -75
- package/.agent/skills/app-builder/agent-coordination.md +0 -71
- package/.agent/skills/app-builder/feature-building.md +0 -53
- package/.agent/skills/app-builder/project-detection.md +0 -34
- package/.agent/skills/app-builder/scaffolding.md +0 -118
- package/.agent/skills/app-builder/tech-stack.md +0 -40
- package/.agent/skills/app-builder/templates/SKILL.md +0 -39
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -122
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -122
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -169
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -134
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -119
- package/.agent/skills/architecture/SKILL.md +0 -55
- package/.agent/skills/architecture/context-discovery.md +0 -43
- package/.agent/skills/architecture/examples.md +0 -94
- package/.agent/skills/architecture/pattern-selection.md +0 -68
- package/.agent/skills/architecture/patterns-reference.md +0 -50
- package/.agent/skills/architecture/trade-off-analysis.md +0 -77
- package/.agent/skills/clean-code/SKILL.md +0 -201
- package/.agent/skills/doc.md +0 -177
- package/.agent/skills/frontend-design/SKILL.md +0 -418
- package/.agent/skills/frontend-design/animation-guide.md +0 -331
- package/.agent/skills/frontend-design/color-system.md +0 -311
- package/.agent/skills/frontend-design/decision-trees.md +0 -418
- package/.agent/skills/frontend-design/motion-graphics.md +0 -306
- package/.agent/skills/frontend-design/scripts/accessibility_checker.py +0 -183
- package/.agent/skills/frontend-design/scripts/ux_audit.py +0 -722
- package/.agent/skills/frontend-design/typography-system.md +0 -345
- package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
- package/.agent/skills/frontend-design/visual-effects.md +0 -383
- package/.agent/skills/i18n-localization/SKILL.md +0 -154
- package/.agent/skills/i18n-localization/scripts/i18n_checker.py +0 -241
- package/.agent/skills/mcp-builder/SKILL.md +0 -176
- package/.agent/skills/web-design-guidelines/SKILL.md +0 -57
- package/.agent/workflows/brainstorm.md +0 -113
- package/.agent/workflows/create.md +0 -59
- package/.agent/workflows/debug.md +0 -103
- package/.agent/workflows/deploy.md +0 -176
- package/.agent/workflows/enhance.md +0 -63
- package/.agent/workflows/orchestrate.md +0 -237
- package/.agent/workflows/plan.md +0 -89
- package/.agent/workflows/preview.md +0 -81
- package/.agent/workflows/simple-test.md +0 -42
- package/.agent/workflows/status.md +0 -86
- package/.agent/workflows/structured-orchestrate.md +0 -180
- package/.agent/workflows/test.md +0 -144
- package/.agent/workflows/ui-ux-pro-max.md +0 -296
- package/output/beef-love-poster.png +0 -0
- package/output/international-news-daily.png +0 -0
- package/poster-test-2.png +0 -0
package/src/core/agent-chat.js
CHANGED
|
@@ -31,7 +31,7 @@ const MODEL_CONTEXT_LIMITS = {
|
|
|
31
31
|
'deepseek-chat': 28000,
|
|
32
32
|
'deepseek-coder': 28000,
|
|
33
33
|
// MiniMax
|
|
34
|
-
'MiniMax-M2.7':
|
|
34
|
+
'MiniMax-M2.7': 90000,
|
|
35
35
|
// OpenAI
|
|
36
36
|
'gpt-4': 100000,
|
|
37
37
|
'gpt-4o': 100000,
|
|
@@ -45,11 +45,9 @@ const MODEL_CONTEXT_LIMITS = {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* 纯 JavaScript token 计数器
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
* - 中文:约 2 字符 = 1 token
|
|
52
|
-
* - 混合文本取加权平均
|
|
48
|
+
* 参考 Claude Code 的实现,使用 bytes/token 比率估算
|
|
49
|
+
* - 普通文本:4 bytes ≈ 1 token
|
|
50
|
+
* - JSON 等密集文本:2 bytes ≈ 1 token
|
|
53
51
|
*/
|
|
54
52
|
class SimpleTokenizer {
|
|
55
53
|
constructor() {
|
|
@@ -59,9 +57,10 @@ class SimpleTokenizer {
|
|
|
59
57
|
/**
|
|
60
58
|
* 估算文本的 token 数
|
|
61
59
|
* @param {string} text
|
|
60
|
+
* @param {number} [bytesPerToken=4] - bytes per token 比率
|
|
62
61
|
* @returns {number}
|
|
63
62
|
*/
|
|
64
|
-
encode(text) {
|
|
63
|
+
encode(text, bytesPerToken = 4) {
|
|
65
64
|
if (!text || typeof text !== 'string') {
|
|
66
65
|
return 0;
|
|
67
66
|
}
|
|
@@ -76,16 +75,16 @@ class SimpleTokenizer {
|
|
|
76
75
|
return 0;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const englishChars = cleanText.length - chineseChars;
|
|
82
|
-
|
|
83
|
-
// 中英文分开估算后相加
|
|
84
|
-
// 中文约 2 字符/token,英文约 4 字符/token
|
|
85
|
-
const chineseTokens = chineseChars / 2;
|
|
86
|
-
const englishTokens = englishChars / 4;
|
|
78
|
+
return Math.ceil(cleanText.length / bytesPerToken);
|
|
79
|
+
}
|
|
87
80
|
|
|
88
|
-
|
|
81
|
+
/**
|
|
82
|
+
* 估算 JSON 文本的 token 数(更密集)
|
|
83
|
+
* @param {string} text
|
|
84
|
+
* @returns {number}
|
|
85
|
+
*/
|
|
86
|
+
encodeForJSON(text) {
|
|
87
|
+
return this.encode(text, 2);
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
90
|
|
|
@@ -93,7 +92,7 @@ class SimpleTokenizer {
|
|
|
93
92
|
const _globalTokenizer = new SimpleTokenizer();
|
|
94
93
|
|
|
95
94
|
// 压缩超时时间(毫秒)
|
|
96
|
-
const COMPRESSION_TIMEOUT =
|
|
95
|
+
const COMPRESSION_TIMEOUT = 30000;
|
|
97
96
|
|
|
98
97
|
class AgentChatHandler extends EventEmitter {
|
|
99
98
|
/**
|
|
@@ -166,7 +165,13 @@ class AgentChatHandler extends EventEmitter {
|
|
|
166
165
|
_getSessionMessageStore(sessionId) {
|
|
167
166
|
if (!sessionId) {
|
|
168
167
|
// 无 session 的单次请求,使用临时存储
|
|
169
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
messages: [],
|
|
170
|
+
historyLoaded: false,
|
|
171
|
+
compressionState: {},
|
|
172
|
+
lastUsage: null, // API 返回的真实 usage
|
|
173
|
+
save: () => {},
|
|
174
|
+
};
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
if (!this._sessionMessageStores.has(sessionId)) {
|
|
@@ -181,6 +186,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
181
186
|
lastTokenCount: 0,
|
|
182
187
|
count: 0,
|
|
183
188
|
},
|
|
189
|
+
lastUsage: null, // API 返回的真实 usage
|
|
184
190
|
save: () => {
|
|
185
191
|
// 简洁的保存方法
|
|
186
192
|
if (this.agent?.framework) {
|
|
@@ -305,22 +311,24 @@ class AgentChatHandler extends EventEmitter {
|
|
|
305
311
|
/**
|
|
306
312
|
* 计算文本的 token 数
|
|
307
313
|
* @param {string} text
|
|
314
|
+
* @param {number} [bytesPerToken=4] - bytes per token 比率
|
|
308
315
|
* @returns {number}
|
|
309
316
|
* @private
|
|
310
317
|
*/
|
|
311
|
-
_countTokens(text) {
|
|
318
|
+
_countTokens(text, bytesPerToken = 4) {
|
|
312
319
|
if (!text || typeof text !== 'string') return 0;
|
|
313
320
|
// SimpleTokenizer.encode 已经处理了清理逻辑
|
|
314
321
|
try {
|
|
315
|
-
return this._encoder.encode(text);
|
|
322
|
+
return this._encoder.encode(text, bytesPerToken);
|
|
316
323
|
} catch (err) {
|
|
317
324
|
// 估算失败时使用字符计数
|
|
318
|
-
return Math.ceil(text.length /
|
|
325
|
+
return Math.ceil(text.length / bytesPerToken);
|
|
319
326
|
}
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
/**
|
|
323
330
|
* 计算消息列表的总 token 数
|
|
331
|
+
* 参考 Claude Code 的分块计数逻辑
|
|
324
332
|
* @param {Array} messages
|
|
325
333
|
* @returns {number}
|
|
326
334
|
* @private
|
|
@@ -329,68 +337,184 @@ class AgentChatHandler extends EventEmitter {
|
|
|
329
337
|
let total = 0;
|
|
330
338
|
for (const msg of messages) {
|
|
331
339
|
if (!msg) continue;
|
|
332
|
-
total +=
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for (const part of msg.content) {
|
|
337
|
-
if (!part || typeof part !== 'object') continue;
|
|
340
|
+
total += this._countMessageTokens(msg);
|
|
341
|
+
}
|
|
342
|
+
return total;
|
|
343
|
+
}
|
|
338
344
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
345
|
+
/**
|
|
346
|
+
* 计算单条消息的 token 数
|
|
347
|
+
* @param {Object} msg - 消息对象
|
|
348
|
+
* @returns {number}
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
_countMessageTokens(msg) {
|
|
352
|
+
let total = 0;
|
|
353
|
+
total += 4; // role 标记
|
|
343
354
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (Object.keys(others).length > 0) {
|
|
363
|
-
total += this._countTokens(JSON.stringify(others));
|
|
364
|
-
}
|
|
365
|
-
} catch (e) {
|
|
366
|
-
// 忽略
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
355
|
+
if (typeof msg.content === 'string') {
|
|
356
|
+
total += this._countTokens(msg.content);
|
|
357
|
+
} else if (Array.isArray(msg.content)) {
|
|
358
|
+
for (const part of msg.content) {
|
|
359
|
+
total += this._countContentBlockTokens(part);
|
|
360
|
+
}
|
|
361
|
+
} else if (msg.content && typeof msg.content === 'object') {
|
|
362
|
+
// 处理工具结果等对象类型
|
|
363
|
+
try {
|
|
364
|
+
const str = JSON.stringify(msg.content);
|
|
365
|
+
total += this._countTokens(str, 2); // JSON 用更密集的比率
|
|
366
|
+
} catch (e) {
|
|
367
|
+
// 无法序列化的内容,忽略
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
total += 4; // 结尾标记
|
|
371
|
+
return total;
|
|
372
|
+
}
|
|
370
373
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
/**
|
|
375
|
+
* 计算单个内容块的 token 数
|
|
376
|
+
* 参考 Claude Code 的实现
|
|
377
|
+
* @param {Object} block - 内容块
|
|
378
|
+
* @returns {number}
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
_countContentBlockTokens(block) {
|
|
382
|
+
if (!block || typeof block !== 'object') {
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const tokenizer = this._encoder;
|
|
387
|
+
|
|
388
|
+
// 文本块
|
|
389
|
+
if (block.type === 'text') {
|
|
390
|
+
return tokenizer.encode(block.text || '');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 工具调用块 - 工具名 + 输入参数(JSON 更密集)
|
|
394
|
+
if (block.type === 'tool-use' || block.type === 'tool_call') {
|
|
395
|
+
let count = 0;
|
|
396
|
+
if (block.name || block.toolName) {
|
|
397
|
+
count += tokenizer.encode(String(block.name || block.toolName || ''));
|
|
398
|
+
}
|
|
399
|
+
if (block.input) {
|
|
400
|
+
try {
|
|
401
|
+
count += tokenizer.encode(JSON.stringify(block.input), 2);
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
}
|
|
404
|
+
return count;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 工具结果块
|
|
408
|
+
if (block.type === 'tool-result' || block.type === 'tool_result') {
|
|
409
|
+
let count = 0;
|
|
410
|
+
if (block.content) {
|
|
411
|
+
if (typeof block.content === 'string') {
|
|
412
|
+
count += tokenizer.encode(block.content);
|
|
413
|
+
} else if (Array.isArray(block.content)) {
|
|
414
|
+
for (const c of block.content) {
|
|
415
|
+
if (c && c.type === 'text') {
|
|
416
|
+
count += tokenizer.encode(c.text || '');
|
|
377
417
|
}
|
|
378
418
|
}
|
|
379
419
|
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
420
|
+
}
|
|
421
|
+
if (block.toolUseId || block.tool_call_id) {
|
|
422
|
+
count += 10; // 工具结果标识
|
|
423
|
+
}
|
|
424
|
+
return count;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 输入块 (tool_input)
|
|
428
|
+
if (block.type === 'input') {
|
|
429
|
+
try {
|
|
430
|
+
return tokenizer.encode(JSON.stringify(block), 2);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 其他类型,尝试 JSON 序列化
|
|
437
|
+
try {
|
|
438
|
+
return tokenizer.encode(JSON.stringify(block), 2);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 估算消息列表的 token 数(不带消息结构的简单估算)
|
|
446
|
+
* 用于快速检查
|
|
447
|
+
* @param {Array} messages
|
|
448
|
+
* @returns {number}
|
|
449
|
+
* @private
|
|
450
|
+
*/
|
|
451
|
+
_roughEstimateMessagesTokens(messages) {
|
|
452
|
+
let total = 0;
|
|
453
|
+
for (const msg of messages) {
|
|
454
|
+
if (!msg) continue;
|
|
455
|
+
if (typeof msg.content === 'string') {
|
|
456
|
+
total += this._countTokens(msg.content);
|
|
457
|
+
} else if (Array.isArray(msg.content)) {
|
|
458
|
+
for (const part of msg.content) {
|
|
459
|
+
if (part && part.text) {
|
|
460
|
+
total += this._countTokens(String(part.text));
|
|
461
|
+
}
|
|
387
462
|
}
|
|
388
463
|
}
|
|
389
464
|
}
|
|
390
|
-
total += 4; // 结尾标记
|
|
391
465
|
return total;
|
|
392
466
|
}
|
|
393
467
|
|
|
468
|
+
/**
|
|
469
|
+
* 基于 API usage 和新消息估算计算当前上下文 token 数
|
|
470
|
+
* 参考 Claude Code 的 tokenCountWithEstimation 实现
|
|
471
|
+
* @param {Array} messages - 消息数组
|
|
472
|
+
* @param {Object} lastUsage - 上一次 API 返回的 usage
|
|
473
|
+
* @param {number} baseMessageCount - 基准消息数量(usage 对应时)
|
|
474
|
+
* @returns {number} 估算的总 token 数
|
|
475
|
+
* @private
|
|
476
|
+
*/
|
|
477
|
+
_tokenCountWithEstimation(messages, lastUsage, baseMessageCount = 0) {
|
|
478
|
+
// 如果有真实的 usage 数据,使用它作为基准
|
|
479
|
+
if (lastUsage && typeof lastUsage === 'object') {
|
|
480
|
+
const inputTokens = lastUsage.inputTokens || 0;
|
|
481
|
+
const outputTokens = lastUsage.outputTokens || 0;
|
|
482
|
+
const cacheReadTokens = lastUsage.inputTokenDetails?.cacheReadTokens || 0;
|
|
483
|
+
const cacheWriteTokens = lastUsage.inputTokenDetails?.cacheWriteTokens || 0;
|
|
484
|
+
|
|
485
|
+
// 计算已有消息的真实 token 数
|
|
486
|
+
const baseTokens = inputTokens + cacheReadTokens + outputTokens;
|
|
487
|
+
|
|
488
|
+
// 估算新增消息的 token 数
|
|
489
|
+
if (messages.length > baseMessageCount) {
|
|
490
|
+
const newMessages = messages.slice(baseMessageCount);
|
|
491
|
+
const newMessagesTokens = this._countMessagesTokens(newMessages);
|
|
492
|
+
return baseTokens + newMessagesTokens;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return baseTokens;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 没有 usage 数据,使用纯估算
|
|
499
|
+
return this._countMessagesTokens(messages);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 更新 messageStore 的 lastUsage
|
|
504
|
+
* @param {Object} messageStore
|
|
505
|
+
* @param {Object} usage - API 返回的 usage
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
_updateMessageStoreUsage(messageStore, usage) {
|
|
509
|
+
if (messageStore && usage) {
|
|
510
|
+
messageStore.lastUsage = {
|
|
511
|
+
inputTokens: usage.inputTokens,
|
|
512
|
+
outputTokens: usage.outputTokens,
|
|
513
|
+
inputTokenDetails: usage.inputTokenDetails || {},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
394
518
|
/**
|
|
395
519
|
* 计算工具定义的 token 数(估算)
|
|
396
520
|
* @returns {number}
|
|
@@ -577,13 +701,11 @@ class AgentChatHandler extends EventEmitter {
|
|
|
577
701
|
}
|
|
578
702
|
|
|
579
703
|
// 过滤时保留:有 result 的 tool call,以及没有 result 的 tool call(但要附带其 result)
|
|
580
|
-
|
|
704
|
+
// 第一遍:先确定哪些 assistant 消息要保留
|
|
705
|
+
const assistantIndicesToKeep = new Set();
|
|
581
706
|
for (let i = 0; i < recentMessages.length; i++) {
|
|
582
707
|
const msg = recentMessages[i];
|
|
583
|
-
if (msg.role === '
|
|
584
|
-
// 保留所有 tool result(它们是必要的)
|
|
585
|
-
indicesToKeep.add(i);
|
|
586
|
-
} else if (msg.role === 'assistant') {
|
|
708
|
+
if (msg.role === 'assistant') {
|
|
587
709
|
// 检查这个 assistant 消息是否有 tool call
|
|
588
710
|
let hasToolCall = false;
|
|
589
711
|
if (msg.content) {
|
|
@@ -596,21 +718,53 @@ class AgentChatHandler extends EventEmitter {
|
|
|
596
718
|
}
|
|
597
719
|
}
|
|
598
720
|
if (hasToolCall) {
|
|
599
|
-
|
|
721
|
+
assistantIndicesToKeep.add(i);
|
|
600
722
|
} else if (i >= recentMessages.length - 3) {
|
|
601
723
|
// 保留最近几条 assistant 消息(它们可能包含重要上下文)
|
|
602
|
-
|
|
724
|
+
assistantIndicesToKeep.add(i);
|
|
603
725
|
}
|
|
604
|
-
} else {
|
|
605
|
-
// user, system 等角色默认保留
|
|
606
|
-
indicesToKeep.add(i);
|
|
607
726
|
}
|
|
608
727
|
}
|
|
609
728
|
|
|
610
729
|
// 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
|
|
611
730
|
for (const [toolCallId, assistantIdx] of assistantToolCalls) {
|
|
612
731
|
if (!toolResults.has(toolCallId)) {
|
|
613
|
-
|
|
732
|
+
assistantIndicesToKeep.add(assistantIdx);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// 第二遍:基于 assistant 的保留情况,确定哪些 tool result 要保留
|
|
737
|
+
const indicesToKeep = new Set();
|
|
738
|
+
for (let i = 0; i < recentMessages.length; i++) {
|
|
739
|
+
const msg = recentMessages[i];
|
|
740
|
+
if (msg.role === 'tool') {
|
|
741
|
+
// 检查这个 tool result 是否对应一个被保留的 assistant tool_call
|
|
742
|
+
// 如果对应的 assistant 消息被删除了,这个 tool result 也应该被删除(否则会报 orphaned error)
|
|
743
|
+
let hasPairedAssistant = false;
|
|
744
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
745
|
+
for (const item of content) {
|
|
746
|
+
if (item && item.type === 'tool-result' && item.toolCallId) {
|
|
747
|
+
// 检查这个 toolCallId 对应的 assistant 消息是否被保留
|
|
748
|
+
const assistantIdx = assistantToolCalls.get(item.toolCallId);
|
|
749
|
+
if (assistantIdx !== undefined && assistantIndicesToKeep.has(assistantIdx)) {
|
|
750
|
+
hasPairedAssistant = true;
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// 只有有配对的 assistant 消息被保留时,才保留这个 tool result
|
|
756
|
+
if (hasPairedAssistant) {
|
|
757
|
+
indicesToKeep.add(i);
|
|
758
|
+
} else {
|
|
759
|
+
logger.debug(`Dropping orphaned tool result at index ${i}`);
|
|
760
|
+
}
|
|
761
|
+
} else if (msg.role === 'assistant') {
|
|
762
|
+
if (assistantIndicesToKeep.has(i)) {
|
|
763
|
+
indicesToKeep.add(i);
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
// user, system 等角色默认保留
|
|
767
|
+
indicesToKeep.add(i);
|
|
614
768
|
}
|
|
615
769
|
}
|
|
616
770
|
|
|
@@ -618,9 +772,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
618
772
|
const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
|
|
619
773
|
const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
|
|
620
774
|
|
|
621
|
-
// 清理孤立 tool results(tool call 已被总结,但 result 还在)
|
|
622
|
-
this._cleanOrphanedToolResults(filteredRecentMessages);
|
|
623
|
-
|
|
624
775
|
// 直接修改传入的 messages 数组
|
|
625
776
|
messages.length = 0;
|
|
626
777
|
messages.push(...systemMessages, summary, ...filteredRecentMessages);
|
|
@@ -741,9 +892,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
741
892
|
const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
|
|
742
893
|
const filteredRecentMessages = sortedIndices.map((i) => recentMessages[i]);
|
|
743
894
|
|
|
744
|
-
// 清理孤立 tool results(tool call 已被总结,但 result 还在)
|
|
745
|
-
this._cleanOrphanedToolResults(filteredRecentMessages);
|
|
746
|
-
|
|
747
895
|
// 直接修改传入的 messages 数组
|
|
748
896
|
messages.length = 0;
|
|
749
897
|
messages.push(...systemMessages, summary, ...filteredRecentMessages);
|
|
@@ -766,38 +914,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
766
914
|
}
|
|
767
915
|
}
|
|
768
916
|
|
|
769
|
-
/**
|
|
770
|
-
* 清理孤立的 tool results(对应的 tool call 已被压缩删除)
|
|
771
|
-
* @param {Array} messages - 消息数组
|
|
772
|
-
* @private
|
|
773
|
-
*/
|
|
774
|
-
_cleanOrphanedToolResults(messages) {
|
|
775
|
-
// 构建当前消息中存在的 toolCallId 集合
|
|
776
|
-
const validToolCallIds = new Set();
|
|
777
|
-
for (const msg of messages) {
|
|
778
|
-
if (msg.role === 'assistant' && msg.content) {
|
|
779
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
780
|
-
for (const item of content) {
|
|
781
|
-
if (item.type === 'tool-call' && item.toolCallId) {
|
|
782
|
-
validToolCallIds.add(item.toolCallId);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// 过滤掉孤立 tool results
|
|
789
|
-
for (const msg of messages) {
|
|
790
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
791
|
-
msg.content = msg.content.filter((item) => {
|
|
792
|
-
if (item && item.type === 'tool-result' && item.toolCallId) {
|
|
793
|
-
return validToolCallIds.has(item.toolCallId);
|
|
794
|
-
}
|
|
795
|
-
return true;
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
917
|
/**
|
|
802
918
|
* 使用 AI 对消息进行总结
|
|
803
919
|
* @param {Array} messages - 要总结的消息
|
|
@@ -1074,7 +1190,7 @@ ${truncatedContent}${truncatedNote}
|
|
|
1074
1190
|
const toolsTokens = this._countToolsTokens();
|
|
1075
1191
|
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1076
1192
|
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
1077
|
-
const limit = this._maxContextTokens * 0.
|
|
1193
|
+
const limit = this._maxContextTokens * 0.5; // 降低到 50%,更早压缩,防止 context window 超限
|
|
1078
1194
|
|
|
1079
1195
|
if (totalTokens > limit) {
|
|
1080
1196
|
logger.info(
|
|
@@ -1082,11 +1198,48 @@ ${truncatedContent}${truncatedNote}
|
|
|
1082
1198
|
);
|
|
1083
1199
|
// 使用带超时控制的压缩(传入 messages 引用)
|
|
1084
1200
|
await this._compressContext(sessionId, messages, messageStore);
|
|
1201
|
+
// 压缩后验证消息配对(防止 orphaned tool result)
|
|
1202
|
+
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1203
|
+
if (validatedMessages.length !== messages.length) {
|
|
1204
|
+
messages.length = 0;
|
|
1205
|
+
messages.push(...validatedMessages);
|
|
1206
|
+
}
|
|
1085
1207
|
}
|
|
1086
1208
|
|
|
1087
1209
|
const maxSteps = options.maxSteps || this._maxSteps;
|
|
1088
1210
|
const tools = this._getAITools(tool);
|
|
1089
1211
|
|
|
1212
|
+
// 验证消息配对(防止 API 调用时报 orphaned tool result 错误)
|
|
1213
|
+
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1214
|
+
if (validatedMessages.length !== messages.length) {
|
|
1215
|
+
messages.length = 0;
|
|
1216
|
+
messages.push(...validatedMessages);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// 最终检查:在 API 调用前再次验证 token 数
|
|
1220
|
+
const finalMessagesTokens = this._countMessagesTokens(messages);
|
|
1221
|
+
const finalToolsTokens = this._countToolsTokens();
|
|
1222
|
+
const finalSystemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1223
|
+
const finalTotalTokens = finalMessagesTokens + finalToolsTokens + finalSystemPromptTokens;
|
|
1224
|
+
const finalLimit = this._maxContextTokens * 0.5;
|
|
1225
|
+
|
|
1226
|
+
logger.info(
|
|
1227
|
+
`[API Call Check] messages=${messages.length}, tokens=(${finalMessagesTokens}+${finalToolsTokens}+${finalSystemPromptTokens}=${finalTotalTokens}) vs limit=${finalLimit}`
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
// 如果仍然超过限制,强制压缩
|
|
1231
|
+
if (finalTotalTokens > finalLimit) {
|
|
1232
|
+
logger.warn(`[API Call Check] Still over limit after validation, forcing compression`);
|
|
1233
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
1234
|
+
const compressedMessages = this._validateMessagesPairing(messages);
|
|
1235
|
+
messages.length = 0;
|
|
1236
|
+
messages.push(...compressedMessages);
|
|
1237
|
+
const afterTokens = this._countMessagesTokens(messages);
|
|
1238
|
+
logger.info(
|
|
1239
|
+
`[After Forced Compression] messages=${messages.length}, tokens=${afterTokens}`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1090
1243
|
// 准备传给 agent 的消息
|
|
1091
1244
|
const prepareStepChainStream = async ({ stepNumber, messages }) => {
|
|
1092
1245
|
try {
|
|
@@ -1096,18 +1249,12 @@ ${truncatedContent}${truncatedNote}
|
|
|
1096
1249
|
return messages;
|
|
1097
1250
|
}
|
|
1098
1251
|
|
|
1099
|
-
// 2.
|
|
1100
|
-
const validated = this._validateToolCallsForPrepare(messages);
|
|
1101
|
-
if (validated !== messages) {
|
|
1102
|
-
return validated;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// 3. 消息数量超过阈值才修剪
|
|
1252
|
+
// 2. 消息数量超过阈值才修剪
|
|
1106
1253
|
if (messages.length <= 50) {
|
|
1107
1254
|
return messages;
|
|
1108
1255
|
}
|
|
1109
1256
|
|
|
1110
|
-
//
|
|
1257
|
+
// 3. 保留配对完整的消息
|
|
1111
1258
|
const pruned = this._prepareMessagesForAI(messages);
|
|
1112
1259
|
return pruned;
|
|
1113
1260
|
} catch (err) {
|
|
@@ -1125,7 +1272,6 @@ ${truncatedContent}${truncatedNote}
|
|
|
1125
1272
|
model: this._aiClient,
|
|
1126
1273
|
instructions: this._systemPrompt,
|
|
1127
1274
|
tools: tools,
|
|
1128
|
-
stopWhen: stepCountIs(30),
|
|
1129
1275
|
prepareStep: prepareStepChainStream,
|
|
1130
1276
|
});
|
|
1131
1277
|
|
|
@@ -1134,6 +1280,14 @@ ${truncatedContent}${truncatedNote}
|
|
|
1134
1280
|
return agent.generate({ messages, ...this.providerOptions });
|
|
1135
1281
|
});
|
|
1136
1282
|
|
|
1283
|
+
// 捕获 API 返回的 usage 用于更准确的 token 估算
|
|
1284
|
+
if (result.usage) {
|
|
1285
|
+
this._updateMessageStoreUsage(messageStore, result.usage);
|
|
1286
|
+
logger.debug(
|
|
1287
|
+
`API usage: input=${result.usage.inputTokens}, output=${result.usage.outputTokens}`
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1137
1291
|
messages.push(...result.response.messages);
|
|
1138
1292
|
|
|
1139
1293
|
// 触发 agent:message 事件,让 memory 插件可以自动提取记忆
|
|
@@ -1142,7 +1296,7 @@ ${truncatedContent}${truncatedNote}
|
|
|
1142
1296
|
|
|
1143
1297
|
// 生成后检查:如果消息太长,下次需要压缩
|
|
1144
1298
|
const afterTokens = this._countMessagesTokens(messages);
|
|
1145
|
-
if (afterTokens > this._maxContextTokens * 0.
|
|
1299
|
+
if (afterTokens > this._maxContextTokens * 0.5) {
|
|
1146
1300
|
logger.info(`After generation: ${afterTokens} tokens, will compress on next turn`);
|
|
1147
1301
|
}
|
|
1148
1302
|
|
|
@@ -1152,6 +1306,8 @@ ${truncatedContent}${truncatedNote}
|
|
|
1152
1306
|
stepCount: result.stepCount || 1,
|
|
1153
1307
|
};
|
|
1154
1308
|
} finally {
|
|
1309
|
+
// 校验并修复消息中的不完整工具调用
|
|
1310
|
+
this._validateToolCalls(messages);
|
|
1155
1311
|
// 确保保存聊天历史到 session(无论成功还是失败)
|
|
1156
1312
|
messageStore.save();
|
|
1157
1313
|
}
|
|
@@ -1176,7 +1332,7 @@ ${truncatedContent}${truncatedNote}
|
|
|
1176
1332
|
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
1177
1333
|
// 动态导入 AI SDK
|
|
1178
1334
|
const { tool, ToolLoopAgent } = await this._importAI();
|
|
1179
|
-
|
|
1335
|
+
//await fs.writeFile('system.md',this._systemPrompt)
|
|
1180
1336
|
const userMessage =
|
|
1181
1337
|
typeof message === 'string' ? { role: 'user', content: message } : message;
|
|
1182
1338
|
messages.push(userMessage);
|
|
@@ -1186,7 +1342,7 @@ ${truncatedContent}${truncatedNote}
|
|
|
1186
1342
|
const toolsTokens = this._countToolsTokens();
|
|
1187
1343
|
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1188
1344
|
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
1189
|
-
const limit = this._maxContextTokens * 0.
|
|
1345
|
+
const limit = this._maxContextTokens * 0.5; // 降低到 50%,更早压缩,防止 context window 超限
|
|
1190
1346
|
// 对于流式调用,如果上下文太大,先压缩再开始
|
|
1191
1347
|
if (totalTokens > limit) {
|
|
1192
1348
|
logger.info(
|
|
@@ -1194,6 +1350,12 @@ ${truncatedContent}${truncatedNote}
|
|
|
1194
1350
|
);
|
|
1195
1351
|
// 流式调用时等待压缩完成(使用带超时控制的压缩)
|
|
1196
1352
|
await this._compressContext(sessionId, messages, messageStore);
|
|
1353
|
+
// 压缩后验证消息配对(防止 orphaned tool result)
|
|
1354
|
+
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1355
|
+
if (validatedMessages.length !== messages.length) {
|
|
1356
|
+
messages.length = 0;
|
|
1357
|
+
messages.push(...validatedMessages);
|
|
1358
|
+
}
|
|
1197
1359
|
}
|
|
1198
1360
|
|
|
1199
1361
|
const maxSteps = options.maxSteps || this._maxSteps;
|
|
@@ -1202,6 +1364,37 @@ ${truncatedContent}${truncatedNote}
|
|
|
1202
1364
|
throw new Error('AI client not configured.');
|
|
1203
1365
|
}
|
|
1204
1366
|
|
|
1367
|
+
// 验证消息配对(防止 API 调用时报 orphaned tool result 错误)
|
|
1368
|
+
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1369
|
+
if (validatedMessages.length !== messages.length) {
|
|
1370
|
+
messages.length = 0;
|
|
1371
|
+
messages.push(...validatedMessages);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// 最终检查:在 API 调用前再次验证 token 数
|
|
1375
|
+
const finalMessagesTokens = this._countMessagesTokens(messages);
|
|
1376
|
+
const finalToolsTokens = this._countToolsTokens();
|
|
1377
|
+
const finalSystemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1378
|
+
const finalTotalTokens = finalMessagesTokens + finalToolsTokens + finalSystemPromptTokens;
|
|
1379
|
+
const finalLimit = this._maxContextTokens * 0.5;
|
|
1380
|
+
|
|
1381
|
+
logger.info(
|
|
1382
|
+
`[API Call Check (stream)] messages=${messages.length}, tokens=(${finalMessagesTokens}+${finalToolsTokens}+${finalSystemPromptTokens}=${finalTotalTokens}) vs limit=${finalLimit}`
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
// 如果仍然超过限制,强制压缩
|
|
1386
|
+
if (finalTotalTokens > finalLimit) {
|
|
1387
|
+
logger.warn(`[API Call Check (stream)] Still over limit, forcing compression`);
|
|
1388
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
1389
|
+
const compressedMessages = this._validateMessagesPairing(messages);
|
|
1390
|
+
messages.length = 0;
|
|
1391
|
+
messages.push(...compressedMessages);
|
|
1392
|
+
const afterTokens = this._countMessagesTokens(messages);
|
|
1393
|
+
logger.info(
|
|
1394
|
+
`[After Forced Compression (stream)] messages=${messages.length}, tokens=${afterTokens}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1205
1398
|
// 准备传给 agent 的消息
|
|
1206
1399
|
const prepareStepChainStream = async ({ stepNumber, messages }) => {
|
|
1207
1400
|
try {
|
|
@@ -1211,18 +1404,12 @@ ${truncatedContent}${truncatedNote}
|
|
|
1211
1404
|
return messages;
|
|
1212
1405
|
}
|
|
1213
1406
|
|
|
1214
|
-
// 2.
|
|
1215
|
-
const validated = this._validateToolCallsForPrepare(messages);
|
|
1216
|
-
if (validated !== messages) {
|
|
1217
|
-
return validated;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// 3. 消息数量超过阈值才修剪
|
|
1407
|
+
// 2. 消息数量超过阈值才修剪
|
|
1221
1408
|
if (messages.length <= 50) {
|
|
1222
1409
|
return messages;
|
|
1223
1410
|
}
|
|
1224
1411
|
|
|
1225
|
-
//
|
|
1412
|
+
// 3. 保留配对完整的消息
|
|
1226
1413
|
const pruned = this._prepareMessagesForAI(messages);
|
|
1227
1414
|
return pruned;
|
|
1228
1415
|
} catch (err) {
|
|
@@ -1235,7 +1422,6 @@ ${truncatedContent}${truncatedNote}
|
|
|
1235
1422
|
model: this._aiClient,
|
|
1236
1423
|
instructions: this._systemPrompt,
|
|
1237
1424
|
tools: tools,
|
|
1238
|
-
stopWhen: stepCountIs(30),
|
|
1239
1425
|
prepareStep: prepareStepChainStream,
|
|
1240
1426
|
});
|
|
1241
1427
|
|
|
@@ -1271,6 +1457,18 @@ ${truncatedContent}${truncatedNote}
|
|
|
1271
1457
|
|
|
1272
1458
|
const finishMessages = (await result.response).messages;
|
|
1273
1459
|
messages.push(...finishMessages);
|
|
1460
|
+
// 捕获 API 返回的 usage 用于更准确的 token 估算
|
|
1461
|
+
const usage = await result.totalUsage;
|
|
1462
|
+
if (usage) {
|
|
1463
|
+
try {
|
|
1464
|
+
this._updateMessageStoreUsage(messageStore, usage);
|
|
1465
|
+
logger.debug(
|
|
1466
|
+
`API usage (stream): input=${usage.inputTokens}, output=${usage.outputTokens}`
|
|
1467
|
+
);
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
logger.debug('Failed to capture stream usage:', e.message);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1274
1472
|
|
|
1275
1473
|
// 触发 agent:message 事件,让 memory 插件可以自动提取记忆
|
|
1276
1474
|
const userMsg = messages[messages.length - finishMessages.length - 1];
|
|
@@ -1279,6 +1477,8 @@ ${truncatedContent}${truncatedNote}
|
|
|
1279
1477
|
this.emit('error', { error: err.message });
|
|
1280
1478
|
yield { type: 'error', error: err.message };
|
|
1281
1479
|
} finally {
|
|
1480
|
+
// 校验并修复消息中的不完整工具调用
|
|
1481
|
+
this._validateToolCalls(messages);
|
|
1282
1482
|
// 确保保存聊天历史到 session(无论成功还是失败)
|
|
1283
1483
|
messageStore.save();
|
|
1284
1484
|
}
|
|
@@ -1437,171 +1637,141 @@ ${truncatedContent}${truncatedNote}
|
|
|
1437
1637
|
}
|
|
1438
1638
|
|
|
1439
1639
|
/**
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1640
|
+
* 验证并修复消息中的 tool call/result 配对
|
|
1641
|
+
* 删除没有对应 assistant tool_call 的 tool result(会导致 API 报错)
|
|
1442
1642
|
* @param {Array} messages - 消息列表
|
|
1443
1643
|
* @private
|
|
1444
1644
|
*/
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1645
|
+
_validateMessagesPairing(messages) {
|
|
1646
|
+
// 收集所有 assistant 的 tool-call IDs
|
|
1647
|
+
const assistantToolCallIds = new Set();
|
|
1448
1648
|
for (const msg of messages) {
|
|
1449
|
-
if (msg.role
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
if (item.type !== 'tool-call') {
|
|
1455
|
-
continue;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
const input = item.input;
|
|
1459
|
-
if (typeof input !== 'string') {
|
|
1460
|
-
continue;
|
|
1649
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
1650
|
+
for (const item of msg.content) {
|
|
1651
|
+
if (item.type === 'tool-call' && item.toolCallId) {
|
|
1652
|
+
assistantToolCallIds.add(item.toolCallId);
|
|
1653
|
+
}
|
|
1461
1654
|
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1462
1657
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
// 检查 JSON 是否能完整解析
|
|
1475
|
-
try {
|
|
1476
|
-
JSON.parse(trimmed);
|
|
1477
|
-
} catch (e) {
|
|
1478
|
-
// JSON 解析失败,说明是不完整的 JSON
|
|
1479
|
-
item.type = 'text';
|
|
1480
|
-
item.text = `(工具调用 ${item.toolName} 参数不完整(${e.message}),已跳过)`;
|
|
1481
|
-
delete item.toolCallId;
|
|
1482
|
-
delete item.toolName;
|
|
1483
|
-
delete item.input;
|
|
1484
|
-
fixedCount++;
|
|
1658
|
+
// 检查并删除没有配对的 tool result
|
|
1659
|
+
let removedCount = 0;
|
|
1660
|
+
for (const msg of messages) {
|
|
1661
|
+
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
1662
|
+
const originalLength = msg.content.length;
|
|
1663
|
+
msg.content = msg.content.filter((item) => {
|
|
1664
|
+
if (item && item.type === 'tool-result' && item.toolCallId) {
|
|
1665
|
+
if (!assistantToolCallIds.has(item.toolCallId)) {
|
|
1666
|
+
removedCount++;
|
|
1667
|
+
return false; // 删除没有配对的 tool result
|
|
1668
|
+
}
|
|
1485
1669
|
}
|
|
1670
|
+
return true;
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
// 如果所有 content 都被删除了,标记整个消息待删除
|
|
1674
|
+
if (msg.content.length === 0 && originalLength > 0) {
|
|
1675
|
+
msg._orphaned = true;
|
|
1486
1676
|
}
|
|
1487
1677
|
}
|
|
1488
1678
|
}
|
|
1489
1679
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1680
|
+
// 移除被标记为 orphaned 的 tool 消息
|
|
1681
|
+
const originalLength = messages.length;
|
|
1682
|
+
const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
|
|
1683
|
+
|
|
1684
|
+
if (removedCount > 0 || filtered.length !== originalLength) {
|
|
1685
|
+
logger.debug(
|
|
1686
|
+
`Removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
|
|
1687
|
+
);
|
|
1492
1688
|
}
|
|
1689
|
+
|
|
1690
|
+
return filtered;
|
|
1493
1691
|
}
|
|
1494
1692
|
|
|
1495
1693
|
/**
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1694
|
+
* 校验并修复消息中的工具调用参数
|
|
1695
|
+
* 移除不完整的 JSON(如只有 "{" )的工具调用
|
|
1696
|
+
* 同时清理 tool result 中的错误信息
|
|
1498
1697
|
* @param {Array} messages - 消息列表
|
|
1499
|
-
* @returns {Array} 修改后的消息数组或原数组
|
|
1500
1698
|
* @private
|
|
1501
1699
|
*/
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
const invalidToolCallIds = new Set();
|
|
1700
|
+
_validateToolCalls(messages) {
|
|
1701
|
+
let fixedCount = 0;
|
|
1505
1702
|
|
|
1506
1703
|
for (const msg of messages) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
continue;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
const input = item.input;
|
|
1517
|
-
if (typeof input !== 'string') {
|
|
1518
|
-
continue;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// 检查 input 是否是有效的 JSON
|
|
1522
|
-
const trimmed = input.trim();
|
|
1523
|
-
let isInvalid = false;
|
|
1704
|
+
// 清理 assistant 消息中的不完整 tool-call
|
|
1705
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
1706
|
+
for (const item of msg.content) {
|
|
1707
|
+
if (item.type !== 'tool-call') {
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1524
1710
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
try {
|
|
1529
|
-
JSON.parse(trimmed);
|
|
1530
|
-
} catch (e) {
|
|
1531
|
-
isInvalid = true;
|
|
1711
|
+
const input = item.input;
|
|
1712
|
+
if (typeof input !== 'string') {
|
|
1713
|
+
continue;
|
|
1532
1714
|
}
|
|
1533
|
-
}
|
|
1534
1715
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1716
|
+
// 检查 input 是否是有效的 JSON(不是不完整的)
|
|
1717
|
+
const trimmed = input.trim();
|
|
1718
|
+
if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
|
|
1719
|
+
// 不完整的 JSON,移除这个 tool-call
|
|
1720
|
+
item.type = 'text';
|
|
1721
|
+
item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
|
|
1722
|
+
delete item.toolCallId;
|
|
1723
|
+
delete item.toolName;
|
|
1724
|
+
delete item.input;
|
|
1725
|
+
fixedCount++;
|
|
1726
|
+
}
|
|
1538
1727
|
}
|
|
1539
1728
|
}
|
|
1540
|
-
}
|
|
1541
1729
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1730
|
+
// 清理 tool result 中的无效 JSON 错误信息
|
|
1731
|
+
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
1732
|
+
for (const item of msg.content) {
|
|
1733
|
+
if (item.type !== 'tool-result') {
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1546
1736
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1737
|
+
const output = item.output;
|
|
1738
|
+
let errorText = null;
|
|
1549
1739
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
item.type === 'tool-result' &&
|
|
1558
|
-
item.toolCallId &&
|
|
1559
|
-
invalidToolCallIds.has(item.toolCallId)
|
|
1560
|
-
) {
|
|
1561
|
-
// 跳过这个 tool result
|
|
1562
|
-
continue;
|
|
1740
|
+
// 处理字符串类型 output
|
|
1741
|
+
if (typeof output === 'string') {
|
|
1742
|
+
errorText = output;
|
|
1743
|
+
} else if (typeof output === 'object' && output !== null) {
|
|
1744
|
+
// 处理 { type: 'error-text', value: '...' } 结构
|
|
1745
|
+
if (output.value && typeof output.value === 'string') {
|
|
1746
|
+
errorText = output.value;
|
|
1563
1747
|
}
|
|
1564
|
-
newContent.push(item);
|
|
1565
1748
|
}
|
|
1566
|
-
|
|
1567
|
-
if (
|
|
1749
|
+
|
|
1750
|
+
if (!errorText) {
|
|
1568
1751
|
continue;
|
|
1569
1752
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
item.toolCallId &&
|
|
1583
|
-
invalidToolCallIds.has(item.toolCallId)
|
|
1584
|
-
) {
|
|
1585
|
-
// 跳过不完整的 tool-call
|
|
1586
|
-
continue;
|
|
1753
|
+
|
|
1754
|
+
// 检查是否包含 "JSON parsing failed" 或类似的不完整 JSON 错误
|
|
1755
|
+
if (errorText.includes('JSON parsing failed') || errorText.includes('Invalid input')) {
|
|
1756
|
+
const toolName = item.toolName || 'unknown';
|
|
1757
|
+
// 替换为更简洁的错误信息
|
|
1758
|
+
if (typeof output === 'string') {
|
|
1759
|
+
item.output = `(工具 ${toolName} 参数不完整,已跳过)`;
|
|
1760
|
+
} else {
|
|
1761
|
+
item.output = {
|
|
1762
|
+
type: 'error-text',
|
|
1763
|
+
value: `(工具 ${toolName} 参数不完整,已跳过)`,
|
|
1764
|
+
};
|
|
1587
1765
|
}
|
|
1588
|
-
|
|
1589
|
-
}
|
|
1590
|
-
// 如果所有 content 都被移除了,跳过这条消息
|
|
1591
|
-
if (newContent.length === 0) {
|
|
1592
|
-
continue;
|
|
1766
|
+
fixedCount++;
|
|
1593
1767
|
}
|
|
1594
|
-
result.push({ ...msg, content: newContent });
|
|
1595
|
-
} else {
|
|
1596
|
-
result.push(msg);
|
|
1597
1768
|
}
|
|
1598
|
-
} else {
|
|
1599
|
-
result.push(msg);
|
|
1600
1769
|
}
|
|
1601
1770
|
}
|
|
1602
1771
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1772
|
+
if (fixedCount > 0) {
|
|
1773
|
+
logger.info(`Fixed ${fixedCount} incomplete tool calls/results`);
|
|
1774
|
+
}
|
|
1605
1775
|
}
|
|
1606
1776
|
|
|
1607
1777
|
/**
|