foliko 1.1.67 → 1.1.69
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/.claude/settings.local.json +19 -10
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/CLAUDE.md +2 -2
- package/README.md +13 -13
- package/SPEC.md +3 -3
- package/cli/src/commands/chat.js +2 -20
- package/cli/src/commands/list.js +7 -6
- package/cli/src/commands/plugin.js +3 -2
- package/cli/src/daemon.js +2 -2
- package/cli/src/ui/chat-ui-old.js +15 -4
- package/cli/src/ui/chat-ui.js +236 -203
- package/cli/src/ui/footer-bar.js +20 -46
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/cli/src/utils/config.js +29 -0
- package/cli/src/utils/plugin-config.js +1 -1
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/docs/user-manual.md +1391 -1391
- package/examples/ambient-example.js +2 -2
- package/examples/bootstrap.js +3 -3
- package/examples/test-chat.js +1 -1
- package/examples/test-reload.js +1 -1
- package/examples/test-telegram.js +1 -1
- package/examples/test-tg-bot.js +1 -1
- package/examples/test-tg-simple.js +2 -2
- package/examples/test-tg.js +1 -1
- package/examples/test-think.js +1 -1
- package/examples/test-weixin-feishu.js +3 -3
- package/package.json +3 -1
- package/plugins/ambient-agent/index.js +1 -1
- package/plugins/audit-plugin.js +84 -29
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +23 -12
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +87 -9
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +7 -7
- package/plugins/memory-plugin.js +9 -12
- package/plugins/plugin-manager-plugin.js +12 -14
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +151 -24
- package/plugins/rules-plugin.js +8 -8
- package/plugins/scheduler-plugin.js +24 -20
- package/plugins/session-plugin.js +313 -397
- package/plugins/storage-plugin.js +235 -175
- package/plugins/subagent-plugin.js +17 -13
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +64 -60
- package/plugins/tools-plugin.js +8 -8
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +107 -24
- package/skills/find-skills/AGENTS.md +2 -2
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/foliko-dev/SKILL.md +19 -19
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/plugin-guide/SKILL.md +4 -4
- package/skills/python-plugin-dev/SKILL.md +5 -5
- package/skills/skill-guide/SKILL.md +104 -6
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +124 -17
- package/src/capabilities/workflow-engine.js +3 -3
- package/src/core/agent-chat.js +72 -26
- package/src/core/agent.js +17 -27
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +45 -169
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +702 -326
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +260 -84
- package/src/core/jsonl-storage.js +253 -0
- package/src/core/plugin-base.js +7 -5
- package/src/core/plugin-manager.js +15 -10
- package/src/core/provider-registry.js +159 -0
- package/src/core/provider.js +2 -0
- package/src/core/session-entry.js +225 -0
- package/src/core/session-manager.js +701 -0
- package/src/core/storage-manager.js +494 -0
- package/src/core/sub-agent-config.js +1 -1
- package/src/core/subagent.js +16 -135
- package/src/core/token-counter.js +177 -58
- package/src/core/tool-executor.js +2 -70
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +27 -16
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/logger.js +152 -180
- package/src/utils/message-validator.js +283 -0
- package/src/utils/plugin-helpers.js +2 -2
- package/src/utils/retry.js +168 -22
- package/website_v2/docs/api.html +1 -1
- package/website_v2/docs/configuration.html +2 -2
- package/website_v2/docs/plugin-development.html +4 -4
- package/website_v2/docs/project-structure.html +2 -2
- package/website_v2/docs/skill-development.html +2 -2
- package/website_v2/index.html +1 -1
- package/website_v2/styles/animations.css +7 -7
- package/.agent/agents/backend-dev.md +0 -102
- package/.agent/agents/data-analyst.md +0 -117
- package/.agent/agents/devops.md +0 -115
- package/.agent/agents/frontend-dev.md +0 -94
- package/.agent/agents/network-requester.md +0 -44
- package/.agent/agents/poster-designer.md +0 -52
- package/.agent/agents/product-manager.md +0 -85
- package/.agent/agents/qa-engineer.md +0 -100
- package/.agent/agents/security-engineer.md +0 -99
- package/.agent/agents/team-lead.md +0 -137
- package/.agent/agents/ui-designer.md +0 -116
- package/.agent/data/default.json +0 -58
- package/.agent/data/email/processed-emails.json +0 -1
- package/.agent/data/plugins-state.json +0 -199
- package/.agent/data/scheduler/tasks.json +0 -1
- package/.agent/data/web/web-config.json +0 -5
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/data/weixin.json +0 -6
- package/.agent/mcp_config.json +0 -14
- package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
- package/.agent/package.json +0 -8
- package/.agent/plugins/marknative/README.md +0 -134
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +0 -256
- package/.agent/plugins/marknative/package.json +0 -12
- package/.agent/plugins/test-plugin.py +0 -99
- package/.agent/plugins.json +0 -14
- package/.agent/python-scripts/test_sample.py +0 -24
- package/.agent/sessions/cli_default.json +0 -247
- package/.agent/skills/agent-browser/SKILL.md +0 -311
- package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
- package/.agent/skills/sysinfo/SKILL.md +0 -38
- package/.agent/skills/sysinfo/system-info.sh +0 -130
- package/.agent/skills/workflow/SKILL.md +0 -324
- package/.agent/test-agent.js +0 -35
- package/.agent/weixin.json +0 -6
- package/.agent/workflows/email-digest.json +0 -50
- package/.agent/workflows/file-backup.json +0 -21
- package/.agent/workflows/get-ip-notify.json +0 -32
- package/.agent/workflows/news-aggregator.json +0 -93
- package/.agent/workflows/news-dashboard-v2.json +0 -94
- package/.agent/workflows/notification-batch.json +0 -32
- package/src/core/session-context.js +0 -346
- package/src/core/session-storage.js +0 -295
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TokenCounter - Token 计算工具
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* 1. 计算文本的 token 数量
|
|
6
|
-
* 2. 计算消息数组的 token 总数
|
|
7
|
-
* 3. 计算工具定义的 token 总数
|
|
3
|
+
* Ported from pi's compaction/compaction.ts with enhancements
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
/**
|
|
@@ -22,56 +18,191 @@ function encode(text, bytesPerToken = 4) {
|
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
20
|
* 计算 JSON 字符串的 token 数量
|
|
25
|
-
* @param {string} text - JSON 字符串
|
|
26
|
-
* @returns {number} token 数量
|
|
27
21
|
*/
|
|
28
22
|
function encodeForJSON(text) {
|
|
29
23
|
if (!text) return 0;
|
|
30
|
-
// JSON 字符串需要额外计算引号和转义
|
|
31
24
|
const encoded = encode(text);
|
|
32
25
|
return encoded + Math.ceil(Buffer.byteLength(JSON.stringify(text), 'utf8') / 100);
|
|
33
26
|
}
|
|
34
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Safe JSON stringify that handles unserializable values
|
|
30
|
+
*/
|
|
31
|
+
function safeJsonStringify(value) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
34
|
+
} catch {
|
|
35
|
+
return '[unserializable]';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Estimate token count for one message using a conservative character heuristic
|
|
41
|
+
* Ported from pi
|
|
42
|
+
*/
|
|
43
|
+
function estimateTokens(message) {
|
|
44
|
+
let chars = 0;
|
|
45
|
+
|
|
46
|
+
switch (message.role) {
|
|
47
|
+
case 'user': {
|
|
48
|
+
const content = message.content;
|
|
49
|
+
if (typeof content === 'string') {
|
|
50
|
+
chars = content.length;
|
|
51
|
+
} else if (Array.isArray(content)) {
|
|
52
|
+
for (const block of content) {
|
|
53
|
+
if (block.type === 'text' && block.text) {
|
|
54
|
+
chars += block.text.length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Math.ceil(chars / 4);
|
|
59
|
+
}
|
|
60
|
+
case 'assistant': {
|
|
61
|
+
const assistant = message;
|
|
62
|
+
for (const block of assistant.content || []) {
|
|
63
|
+
if (block.type === 'text') {
|
|
64
|
+
chars += block.text.length;
|
|
65
|
+
} else if (block.type === 'thinking') {
|
|
66
|
+
chars += block.thinking.length;
|
|
67
|
+
} else if (block.type === 'toolCall') {
|
|
68
|
+
chars += block.name.length + safeJsonStringify(block.arguments).length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Math.ceil(chars / 4);
|
|
72
|
+
}
|
|
73
|
+
case 'custom':
|
|
74
|
+
case 'toolResult': {
|
|
75
|
+
if (typeof message.content === 'string') {
|
|
76
|
+
chars = message.content.length;
|
|
77
|
+
} else {
|
|
78
|
+
for (const block of message.content || []) {
|
|
79
|
+
if (block.type === 'text' && block.text) {
|
|
80
|
+
chars += block.text.length;
|
|
81
|
+
}
|
|
82
|
+
if (block.type === 'image') {
|
|
83
|
+
chars += 4800;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return Math.ceil(chars / 4);
|
|
88
|
+
}
|
|
89
|
+
case 'bashExecution': {
|
|
90
|
+
chars = message.command.length + (message.output?.length || 0);
|
|
91
|
+
return Math.ceil(chars / 4);
|
|
92
|
+
}
|
|
93
|
+
case 'branchSummary':
|
|
94
|
+
case 'compactionSummary': {
|
|
95
|
+
chars = message.summary.length;
|
|
96
|
+
return Math.ceil(chars / 4);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Calculate total context tokens from provider usage
|
|
105
|
+
*/
|
|
106
|
+
function calculateContextTokens(usage) {
|
|
107
|
+
return usage.totalTokens || (usage.input || 0) + (usage.output || 0) + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get assistant usage from message
|
|
112
|
+
*/
|
|
113
|
+
function getAssistantUsage(msg) {
|
|
114
|
+
if (msg.role === 'assistant' && msg.usage) {
|
|
115
|
+
if (msg.stopReason !== 'aborted' && msg.stopReason !== 'error' && msg.usage) {
|
|
116
|
+
return msg.usage;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Return usage from the last successful assistant message in session entries
|
|
124
|
+
*/
|
|
125
|
+
function getLastAssistantUsage(entries) {
|
|
126
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
127
|
+
const entry = entries[i];
|
|
128
|
+
if (entry.type === 'message') {
|
|
129
|
+
const usage = getAssistantUsage(entry.message);
|
|
130
|
+
if (usage) return usage;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get last assistant usage info from messages
|
|
138
|
+
*/
|
|
139
|
+
function getLastAssistantUsageInfo(messages) {
|
|
140
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
141
|
+
const usage = getAssistantUsage(messages[i]);
|
|
142
|
+
if (usage) return { usage, index: i };
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Estimate context tokens for messages using provider usage when available
|
|
149
|
+
*/
|
|
150
|
+
function estimateContextTokens(messages) {
|
|
151
|
+
const usageInfo = getLastAssistantUsageInfo(messages);
|
|
152
|
+
|
|
153
|
+
if (!usageInfo) {
|
|
154
|
+
let estimated = 0;
|
|
155
|
+
for (const message of messages) {
|
|
156
|
+
estimated += estimateTokens(message);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
tokens: estimated,
|
|
160
|
+
usageTokens: 0,
|
|
161
|
+
trailingTokens: estimated,
|
|
162
|
+
lastUsageIndex: null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const usageTokens = calculateContextTokens(usageInfo.usage);
|
|
167
|
+
let trailingTokens = 0;
|
|
168
|
+
for (let i = usageInfo.index + 1; i < messages.length; i++) {
|
|
169
|
+
trailingTokens += estimateTokens(messages[i]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
tokens: usageTokens + trailingTokens,
|
|
174
|
+
usageTokens,
|
|
175
|
+
trailingTokens,
|
|
176
|
+
lastUsageIndex: usageInfo.index
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Return whether context usage exceeds the configured compaction threshold
|
|
182
|
+
*/
|
|
183
|
+
function shouldCompact(contextTokens, contextWindow, settings) {
|
|
184
|
+
if (!settings.enabled) return false;
|
|
185
|
+
return contextTokens > contextWindow - settings.reserveTokens;
|
|
186
|
+
}
|
|
187
|
+
|
|
35
188
|
class TokenCounter {
|
|
36
|
-
/**
|
|
37
|
-
* @param {Object} config - 配置
|
|
38
|
-
* @param {Object} config.toolSchema - 工具 schema(用于工具 token 计算)
|
|
39
|
-
*/
|
|
40
189
|
constructor(config = {}) {
|
|
41
190
|
this.toolSchema = config.toolSchema || null;
|
|
42
191
|
}
|
|
43
192
|
|
|
44
|
-
/**
|
|
45
|
-
* 计算文本 token
|
|
46
|
-
* @param {string} text - 文本
|
|
47
|
-
* @param {number} bytesPerToken - 每 token 字节数
|
|
48
|
-
* @returns {number} token 数量
|
|
49
|
-
*/
|
|
50
193
|
countText(text, bytesPerToken = 4) {
|
|
51
194
|
return encode(text, bytesPerToken);
|
|
52
195
|
}
|
|
53
196
|
|
|
54
|
-
/**
|
|
55
|
-
* 计算消息数组的 token 总数
|
|
56
|
-
* @param {Array} messages - 消息数组
|
|
57
|
-
* @returns {number} token 总数
|
|
58
|
-
*/
|
|
59
197
|
countMessages(messages) {
|
|
60
198
|
if (!Array.isArray(messages)) return 0;
|
|
61
199
|
return messages.reduce((sum, msg) => sum + this.countMessage(msg), 0);
|
|
62
200
|
}
|
|
63
201
|
|
|
64
|
-
/**
|
|
65
|
-
* 计算单条消息的 token
|
|
66
|
-
* @param {Object} msg - 消息
|
|
67
|
-
* @returns {number} token 数量
|
|
68
|
-
*/
|
|
69
202
|
countMessage(msg) {
|
|
70
203
|
if (!msg) return 0;
|
|
71
204
|
|
|
72
205
|
let total = 0;
|
|
73
|
-
|
|
74
|
-
// 角色和格式开销
|
|
75
206
|
total += 4;
|
|
76
207
|
|
|
77
208
|
if (typeof msg.content === 'string') {
|
|
@@ -82,9 +213,8 @@ class TokenCounter {
|
|
|
82
213
|
}
|
|
83
214
|
}
|
|
84
215
|
|
|
85
|
-
// tool_calls 开销
|
|
86
216
|
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
87
|
-
total += 15;
|
|
217
|
+
total += 15;
|
|
88
218
|
for (const tc of msg.tool_calls) {
|
|
89
219
|
if (tc.function) {
|
|
90
220
|
total += this.countText(tc.function.name) + this.countText(tc.function.arguments);
|
|
@@ -92,7 +222,6 @@ class TokenCounter {
|
|
|
92
222
|
}
|
|
93
223
|
}
|
|
94
224
|
|
|
95
|
-
// tool_call_id 开销
|
|
96
225
|
if (msg.tool_call_id) {
|
|
97
226
|
total += 15;
|
|
98
227
|
}
|
|
@@ -100,11 +229,6 @@ class TokenCounter {
|
|
|
100
229
|
return total;
|
|
101
230
|
}
|
|
102
231
|
|
|
103
|
-
/**
|
|
104
|
-
* 计算 content block 的 token
|
|
105
|
-
* @param {Object} block - content block
|
|
106
|
-
* @returns {number} token 数量
|
|
107
|
-
*/
|
|
108
232
|
countContentBlock(block) {
|
|
109
233
|
if (!block) return 0;
|
|
110
234
|
|
|
@@ -126,34 +250,23 @@ class TokenCounter {
|
|
|
126
250
|
}
|
|
127
251
|
return 0;
|
|
128
252
|
case 'image':
|
|
129
|
-
// 图片按 token 估算
|
|
130
253
|
return 85;
|
|
131
254
|
default:
|
|
132
255
|
return this.countText(JSON.stringify(block));
|
|
133
256
|
}
|
|
134
257
|
}
|
|
135
258
|
|
|
136
|
-
/**
|
|
137
|
-
* 计算工具定义的 token 总数
|
|
138
|
-
* @param {Array} tools - 工具数组
|
|
139
|
-
* @returns {number} token 总数
|
|
140
|
-
*/
|
|
141
259
|
countTools(tools) {
|
|
142
260
|
if (!tools || !Array.isArray(tools)) return 0;
|
|
143
261
|
|
|
144
262
|
let total = 0;
|
|
145
263
|
for (const tool of tools) {
|
|
146
|
-
|
|
147
|
-
total += 20; // overhead
|
|
148
|
-
|
|
264
|
+
total += 20;
|
|
149
265
|
if (tool.description) {
|
|
150
266
|
total += this.countText(tool.description);
|
|
151
267
|
}
|
|
152
|
-
|
|
153
|
-
// 参数
|
|
154
268
|
if (tool.inputSchema) {
|
|
155
|
-
const schema =
|
|
156
|
-
tool.inputSchema.jsonSchema || tool.inputSchema.inputSchema || tool.inputSchema;
|
|
269
|
+
const schema = tool.inputSchema.jsonSchema || tool.inputSchema.inputSchema || tool.inputSchema;
|
|
157
270
|
if (schema.properties) {
|
|
158
271
|
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
159
272
|
total += this.countText(name) + 10;
|
|
@@ -168,11 +281,6 @@ class TokenCounter {
|
|
|
168
281
|
return total;
|
|
169
282
|
}
|
|
170
283
|
|
|
171
|
-
/**
|
|
172
|
-
* 估算完整请求的 token(消息 + 工具 + 系统提示)
|
|
173
|
-
* @param {Object} params - 请求参数
|
|
174
|
-
* @returns {Object} - { messagesTokens, toolsTokens, systemPromptTokens, total }
|
|
175
|
-
*/
|
|
176
284
|
estimateRequest({ messages, tools, systemPrompt }) {
|
|
177
285
|
const messagesTokens = this.countMessages(messages);
|
|
178
286
|
const toolsTokens = this.countTools(tools);
|
|
@@ -182,9 +290,20 @@ class TokenCounter {
|
|
|
182
290
|
messagesTokens,
|
|
183
291
|
toolsTokens,
|
|
184
292
|
systemPromptTokens,
|
|
185
|
-
total: messagesTokens + toolsTokens + systemPromptTokens
|
|
293
|
+
total: messagesTokens + toolsTokens + systemPromptTokens
|
|
186
294
|
};
|
|
187
295
|
}
|
|
188
296
|
}
|
|
189
297
|
|
|
190
|
-
module.exports = {
|
|
298
|
+
module.exports = {
|
|
299
|
+
TokenCounter,
|
|
300
|
+
encode,
|
|
301
|
+
encodeForJSON,
|
|
302
|
+
estimateTokens,
|
|
303
|
+
calculateContextTokens,
|
|
304
|
+
getAssistantUsage,
|
|
305
|
+
getLastAssistantUsage,
|
|
306
|
+
estimateContextTokens,
|
|
307
|
+
shouldCompact,
|
|
308
|
+
safeJsonStringify
|
|
309
|
+
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const { EventEmitter } = require('../utils/event-emitter');
|
|
11
11
|
const { logger } = require('../utils/logger');
|
|
12
|
+
const { validateToolCalls } = require('../utils/message-validator');
|
|
12
13
|
|
|
13
14
|
class ToolExecutor extends EventEmitter {
|
|
14
15
|
/**
|
|
@@ -183,76 +184,7 @@ class ToolExecutor extends EventEmitter {
|
|
|
183
184
|
* @returns {Array} 验证后的消息
|
|
184
185
|
*/
|
|
185
186
|
validateToolCalls(messages) {
|
|
186
|
-
|
|
187
|
-
// 收集被跳过的 toolCallId,用于清理对应的 tool-result
|
|
188
|
-
const invalidatedToolCallIds = new Set();
|
|
189
|
-
|
|
190
|
-
for (const msg of messages) {
|
|
191
|
-
// 清理 assistant 消息中的不完整 tool-call
|
|
192
|
-
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
193
|
-
for (const item of msg.content) {
|
|
194
|
-
// 兼容 tool-call 和 tool-use 两种类型
|
|
195
|
-
if (item.type !== 'tool-call' && item.type !== 'tool-use') {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const input = item.input;
|
|
200
|
-
if (typeof input !== 'string') {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 检查 input 是否是有效的 JSON(不是不完整的)
|
|
205
|
-
const trimmed = input.trim();
|
|
206
|
-
if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
|
|
207
|
-
// 不完整的 JSON,移除这个 tool-call
|
|
208
|
-
// 记录 toolCallId,以便后续清理对应的 tool-result
|
|
209
|
-
if (item.toolCallId) {
|
|
210
|
-
invalidatedToolCallIds.add(item.toolCallId);
|
|
211
|
-
}
|
|
212
|
-
logger.warn(
|
|
213
|
-
`_validateToolCalls: invalid tool-call input="${input}", toolCallId=${item.toolCallId}, converting to text`
|
|
214
|
-
);
|
|
215
|
-
item.type = 'text';
|
|
216
|
-
item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
|
|
217
|
-
delete item.toolCallId;
|
|
218
|
-
delete item.toolName;
|
|
219
|
-
delete item.input;
|
|
220
|
-
fixedCount++;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// 如果有无效的 tool-call,清理对应的 tool-result
|
|
227
|
-
if (invalidatedToolCallIds.size > 0) {
|
|
228
|
-
logger.warn(
|
|
229
|
-
`_validateToolCalls: removing ${invalidatedToolCallIds.size} tool-results with invalidated toolCallIds`
|
|
230
|
-
);
|
|
231
|
-
for (const msg of messages) {
|
|
232
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
233
|
-
// 过滤掉引用了无效 toolCallId 的 tool-result
|
|
234
|
-
const oldLen = msg.content.length;
|
|
235
|
-
msg.content = msg.content.filter((item) => {
|
|
236
|
-
if (item.type !== 'tool-result' && item.type !== 'tool_result') {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
// 如果 tool-result 引用的 toolCallId 已被标记为无效,则移除
|
|
240
|
-
if (item.toolCallId && invalidatedToolCallIds.has(item.toolCallId)) {
|
|
241
|
-
logger.warn(
|
|
242
|
-
`_validateToolCalls: removing orphaned tool-result with toolCallId=${item.toolCallId}`
|
|
243
|
-
);
|
|
244
|
-
fixedCount++;
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
return true;
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (fixedCount > 0) {
|
|
254
|
-
logger.info(`_validateToolCalls: Fixed ${fixedCount} incomplete tool calls/results`);
|
|
255
|
-
}
|
|
187
|
+
return validateToolCalls(messages);
|
|
256
188
|
}
|
|
257
189
|
|
|
258
190
|
/**
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExtensionUIContext - UI Extension Context for Plugins
|
|
3
|
+
* 允许插件扩展 TUI 界面
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { logger } = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
class ExtensionUIContext {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} chatUI - ChatUI 实例
|
|
11
|
+
* @param {string} pluginName - 插件名称
|
|
12
|
+
*/
|
|
13
|
+
constructor(chatUI, pluginName) {
|
|
14
|
+
this._ui = chatUI;
|
|
15
|
+
this._pluginName = pluginName;
|
|
16
|
+
this._components = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 显示选择对话框
|
|
21
|
+
* @param {string} title - 对话框标题
|
|
22
|
+
* @param {Array<{label: string, value: any}>} options - 选项
|
|
23
|
+
* @returns {Promise<any>} 选中的值
|
|
24
|
+
*/
|
|
25
|
+
async select(title, options) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
// 简化实现:使用 TUI 的 prompt 机制
|
|
28
|
+
if (!this._ui || !this._ui.tui) {
|
|
29
|
+
reject(new Error('ChatUI not available'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 构建选项文本
|
|
34
|
+
let prompt = `${title}\n`;
|
|
35
|
+
options.forEach((opt, i) => {
|
|
36
|
+
prompt += `${i + 1}. ${opt.label}\n`;
|
|
37
|
+
});
|
|
38
|
+
prompt += '请选择: ';
|
|
39
|
+
|
|
40
|
+
// 创建一个简单的选择机制
|
|
41
|
+
this._ui.editor.setPrompt(prompt);
|
|
42
|
+
this._ui.editor.once('submit', (value) => {
|
|
43
|
+
const idx = parseInt(value, 10) - 1;
|
|
44
|
+
if (idx >= 0 && idx < options.length) {
|
|
45
|
+
resolve(options[idx].value);
|
|
46
|
+
} else {
|
|
47
|
+
resolve(options[0]?.value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this._ui.tui.setFocus(this._ui.editor);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 显示确认对话框
|
|
57
|
+
* @param {string} message - 确认消息
|
|
58
|
+
* @returns {Promise<boolean>} 是否确认
|
|
59
|
+
*/
|
|
60
|
+
async confirm(message) {
|
|
61
|
+
const result = await this.select(message, [
|
|
62
|
+
{ label: '是', value: true },
|
|
63
|
+
{ label: '否', value: false },
|
|
64
|
+
]);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 获取文本输入
|
|
70
|
+
* @param {string} prompt - 输入提示
|
|
71
|
+
* @returns {Promise<string>} 用户输入
|
|
72
|
+
*/
|
|
73
|
+
async input(prompt) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (!this._ui || !this._ui.editor) {
|
|
76
|
+
reject(new Error('ChatUI not available'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const originalPrompt = this._ui.editor.getPrompt?.() || '';
|
|
81
|
+
this._ui.editor.setPrompt(prompt);
|
|
82
|
+
|
|
83
|
+
const handler = (value) => {
|
|
84
|
+
this._ui.editor.off('submit', handler);
|
|
85
|
+
if (originalPrompt) {
|
|
86
|
+
this._ui.editor.setPrompt(originalPrompt);
|
|
87
|
+
}
|
|
88
|
+
resolve(value);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this._ui.editor.once('submit', handler);
|
|
92
|
+
this._ui.tui.setFocus(this._ui.editor);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 显示通知
|
|
98
|
+
* @param {string} title - 通知标题
|
|
99
|
+
* @param {string} message - 通知消息
|
|
100
|
+
* @param {number} [duration=3000] - 显示时长(ms)
|
|
101
|
+
*/
|
|
102
|
+
notify(title, message, duration = 3000) {
|
|
103
|
+
if (this._ui && this._ui.statusBar) {
|
|
104
|
+
this._ui.statusBar.tooler?.show(`🔔 ${title}: ${message}`);
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
this._ui.statusBar.tooler?.hide();
|
|
107
|
+
}, duration);
|
|
108
|
+
} else {
|
|
109
|
+
logger.info(`[Notification] ${title}: ${message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 设置状态栏
|
|
115
|
+
* @param {string} status - 状态文本
|
|
116
|
+
*/
|
|
117
|
+
setStatus(status) {
|
|
118
|
+
if (this._ui && this._ui.statusBar) {
|
|
119
|
+
this._ui.statusBar.tooler?.show(status);
|
|
120
|
+
} else {
|
|
121
|
+
logger.debug(`[Status] ${status}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 添加自定义组件
|
|
127
|
+
* @param {string} id - 组件标识
|
|
128
|
+
* @param {Object} component - TUI 组件
|
|
129
|
+
*/
|
|
130
|
+
addComponent(id, component) {
|
|
131
|
+
if (this._ui && this._ui.tui) {
|
|
132
|
+
this._ui.tui.addChild(component);
|
|
133
|
+
this._components.set(id, component);
|
|
134
|
+
}
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 移除自定义组件
|
|
140
|
+
*/
|
|
141
|
+
removeComponent(id) {
|
|
142
|
+
const comp = this._components.get(id);
|
|
143
|
+
if (comp && this._ui && this._ui.tui) {
|
|
144
|
+
this._ui.tui.removeChild(comp);
|
|
145
|
+
this._components.delete(id);
|
|
146
|
+
}
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取 TUI 实例(用于更高级的定制)
|
|
152
|
+
*/
|
|
153
|
+
getTUI() {
|
|
154
|
+
return this._ui?.tui || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 在消息区域显示消息
|
|
159
|
+
*/
|
|
160
|
+
showMessage(message, type = 'info') {
|
|
161
|
+
if (this._ui) {
|
|
162
|
+
const colors = {
|
|
163
|
+
info: 'cyan',
|
|
164
|
+
success: 'green',
|
|
165
|
+
warning: 'yellow',
|
|
166
|
+
error: 'red',
|
|
167
|
+
};
|
|
168
|
+
const color = colors[type] || 'cyan';
|
|
169
|
+
this._ui.create_message(`[${this._pluginName}] ${message}\n`, chalk?.[color] || ((t) => t));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { ExtensionUIContext };
|