foliko 1.1.63 → 1.1.65

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 (48) hide show
  1. package/.agent/data/plugins-state.json +8 -0
  2. package/.agent/sessions/cli_default.json +258 -260
  3. package/cli/bin/foliko.js +2 -2
  4. package/cli/src/commands/chat.js +15 -26
  5. package/cli/src/ui/chat-ui.js +102 -165
  6. package/cli/src/ui/footer-bar.js +7 -32
  7. package/cli/src/ui/message-bubble.js +24 -2
  8. package/cli/src/ui/status-bar.js +177 -0
  9. package/package.json +1 -2
  10. package/plugins/audit-plugin.js +11 -7
  11. package/plugins/coordinator-plugin.js +14 -12
  12. package/plugins/data-splitter-plugin.js +323 -0
  13. package/plugins/default-plugins.js +12 -1
  14. package/plugins/extension-executor-plugin.js +2 -2
  15. package/plugins/file-system-plugin.js +68 -50
  16. package/plugins/gate-trading.js +10 -10
  17. package/plugins/install-plugin.js +3 -3
  18. package/plugins/memory-plugin.js +8 -11
  19. package/plugins/plugin-manager-plugin.js +9 -11
  20. package/plugins/qq-plugin.js +9 -9
  21. package/plugins/rules-plugin.js +7 -7
  22. package/plugins/scheduler-plugin.js +22 -18
  23. package/plugins/session-plugin.js +14 -14
  24. package/plugins/storage-plugin.js +11 -10
  25. package/plugins/subagent-plugin.js +13 -9
  26. package/plugins/think-plugin.js +63 -59
  27. package/plugins/tools-plugin.js +8 -8
  28. package/plugins/weixin-plugin.js +5 -5
  29. package/src/capabilities/skill-manager.js +23 -15
  30. package/src/capabilities/workflow-engine.js +2 -2
  31. package/src/core/agent-chat.js +70 -26
  32. package/src/core/agent.js +17 -27
  33. package/src/core/chat-session.js +7 -161
  34. package/src/core/constants.js +198 -0
  35. package/src/core/context-compressor.js +6 -181
  36. package/src/core/framework.js +125 -6
  37. package/src/core/plugin-base.js +7 -5
  38. package/src/core/provider.js +6 -0
  39. package/src/core/subagent.js +16 -135
  40. package/src/core/tool-executor.js +2 -70
  41. package/src/executors/mcp-executor.js +12 -10
  42. package/src/utils/chat-queue.js +11 -22
  43. package/src/utils/data-splitter.js +345 -0
  44. package/src/utils/download.js +5 -4
  45. package/src/utils/message-validator.js +283 -0
  46. package/src/utils/retry.js +168 -22
  47. package/src/utils/sandbox.js +60 -207
  48. package/cli/src/utils/debounce.js +0 -106
@@ -0,0 +1,283 @@
1
+ /**
2
+ * 消息验证工具
3
+ * 统一管理 tool-call / tool-result 配对验证
4
+ *
5
+ * 解决的问题:
6
+ * - 消除 4 处重复的消息配对验证逻辑
7
+ * - 统一 tool-call 输入格式验证
8
+ * - 减少消息数组多次遍历
9
+ */
10
+
11
+ const { logger } = require('./logger');
12
+
13
+ /**
14
+ * 收集所有 assistant 消息中的 tool-call ID
15
+ * @param {Array} messages - 消息数组
16
+ * @returns {Set<string>} tool-call ID 集合
17
+ */
18
+ function collectToolCallIds(messages) {
19
+ const ids = new Set();
20
+ for (const msg of messages) {
21
+ if (msg.role !== 'assistant') continue;
22
+
23
+ // 格式1: msg.content 数组中的 tool-call 块 (AI SDK 格式)
24
+ if (Array.isArray(msg.content)) {
25
+ for (const item of msg.content) {
26
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
27
+ ids.add(item.toolCallId);
28
+ }
29
+ }
30
+ }
31
+
32
+ // 格式2: msg.tool_calls 数组 (OpenAI 兼容格式)
33
+ if (Array.isArray(msg.tool_calls)) {
34
+ for (const tc of msg.tool_calls) {
35
+ if (tc.id) ids.add(tc.id);
36
+ }
37
+ }
38
+ }
39
+ return ids;
40
+ }
41
+
42
+ /**
43
+ * 验证消息配对 — 移除 orphaned tool-result(没有对应 tool-call 的)
44
+ * @param {Array} messages - 消息数组(会被修改)
45
+ * @param {Object} [options]
46
+ * @param {boolean} [options.log=true] - 是否记录日志
47
+ * @returns {Array} 过滤后的消息数组
48
+ */
49
+ function validateMessagesPairing(messages, options = {}) {
50
+ const { log: shouldLog = true } = options;
51
+ const validToolCallIds = collectToolCallIds(messages);
52
+
53
+ let removedToolResultCount = 0;
54
+
55
+ for (const msg of messages) {
56
+ if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
57
+
58
+ const originalLength = msg.content.length;
59
+ msg.content = msg.content.filter((item) => {
60
+ if (
61
+ item &&
62
+ (item.type === 'tool-result' || item.type === 'tool_result') &&
63
+ item.toolCallId &&
64
+ !validToolCallIds.has(item.toolCallId)
65
+ ) {
66
+ removedToolResultCount++;
67
+ return false;
68
+ }
69
+ return true;
70
+ });
71
+
72
+ // content 全被删除了,标记整个消息待删除
73
+ if (msg.content.length === 0 && originalLength > 0) {
74
+ msg._orphaned = true;
75
+ }
76
+ }
77
+
78
+ const originalLen = messages.length;
79
+ const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
80
+
81
+ if (shouldLog && (removedToolResultCount > 0 || filtered.length !== originalLen)) {
82
+ logger.debug(
83
+ `[validateMessagesPairing] removed ${removedToolResultCount} orphaned tool-results,` +
84
+ ` ${originalLen - filtered.length} orphaned tool messages`
85
+ );
86
+ }
87
+
88
+ return filtered;
89
+ }
90
+
91
+ /**
92
+ * 验证 tool-call 格式 — 修复不完整的 JSON 输入
93
+ * @param {Array} messages - 消息数组(会被修改)
94
+ * @param {Object} [options]
95
+ * @param {boolean} [options.log=true] - 是否记录日志
96
+ * @returns {number} 修复/移除的数量
97
+ */
98
+ function validateToolCalls(messages, options = {}) {
99
+ const { log: shouldLog = true } = options;
100
+ let fixedCount = 0;
101
+ const invalidatedToolCallIds = new Set();
102
+
103
+ for (const msg of messages) {
104
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
105
+
106
+ for (const item of msg.content) {
107
+ if (item.type !== 'tool-call' && item.type !== 'tool-use') continue;
108
+
109
+ const input = item.input;
110
+ if (typeof input !== 'string') continue;
111
+
112
+ const trimmed = input.trim();
113
+ if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
114
+ if (item.toolCallId) {
115
+ invalidatedToolCallIds.add(item.toolCallId);
116
+ }
117
+ if (shouldLog) {
118
+ logger.warn(
119
+ `[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId ||
120
+ '?'}, converting to text`
121
+ );
122
+ }
123
+ // 把无效的 tool-call 转换成 text 文本
124
+ item.type = 'text';
125
+ item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
126
+ delete item.toolCallId;
127
+ delete item.toolName;
128
+ delete item.input;
129
+ fixedCount++;
130
+ }
131
+ }
132
+ }
133
+
134
+ // 清理引用了无效 toolCallId 的 tool-result
135
+ if (invalidatedToolCallIds.size > 0) {
136
+ for (const msg of messages) {
137
+ if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
138
+
139
+ msg.content = msg.content.filter((item) => {
140
+ if (
141
+ item &&
142
+ (item.type === 'tool-result' || item.type === 'tool_result') &&
143
+ item.toolCallId &&
144
+ invalidatedToolCallIds.has(item.toolCallId)
145
+ ) {
146
+ fixedCount++;
147
+ return false;
148
+ }
149
+ return true;
150
+ });
151
+ }
152
+ }
153
+
154
+ if (shouldLog && fixedCount > 0) {
155
+ logger.info(`[validateToolCalls] Fixed ${fixedCount} incomplete tool calls/results`);
156
+ }
157
+
158
+ return fixedCount;
159
+ }
160
+
161
+ /**
162
+ * 一次遍历完成配对验证 + tool-call 格式验证
163
+ * 相比分别调用减少消息数组迭代次数
164
+ * @param {Array} messages - 消息数组(会被修改)
165
+ * @returns {Array} 过滤后的消息数组
166
+ */
167
+ function validateAll(messages) {
168
+ // 1. 先修复 tool-call 格式(避免无效 ID 干扰后续配对验证)
169
+ validateToolCalls(messages);
170
+ // 2. 再移除 orphaned tool-result
171
+ return validateMessagesPairing(messages);
172
+ }
173
+
174
+ /**
175
+ * 过滤消息,保留配对的 tool-call → tool-result 链条
176
+ * 用于上下文压缩后保留完整的工具调用链路
177
+ * @param {Array} messages - 消息数组
178
+ * @param {number} [keepRecentAssistant=3] - 保留最近几条无工具调用的 assistant 消息
179
+ * @returns {Array} 过滤后的消息数组
180
+ */
181
+ function filterPairedMessages(messages, keepRecentAssistant = 3) {
182
+ // 第一遍:收集所有 tool-call 及对应的 result
183
+ const assistantToolCalls = new Map(); // toolCallId → assistant index
184
+ const toolResults = new Map(); // toolCallId → [result indices]
185
+
186
+ for (let i = 0; i < messages.length; i++) {
187
+ const msg = messages[i];
188
+ if (msg.role === 'assistant') {
189
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
190
+ for (const item of content) {
191
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
192
+ assistantToolCalls.set(item.toolCallId, i);
193
+ }
194
+ }
195
+ if (Array.isArray(msg.tool_calls)) {
196
+ for (const tc of msg.tool_calls) {
197
+ if (tc.id) assistantToolCalls.set(tc.id, i);
198
+ }
199
+ }
200
+ }
201
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
202
+ for (const item of msg.content) {
203
+ if ((item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId) {
204
+ if (!toolResults.has(item.toolCallId)) {
205
+ toolResults.set(item.toolCallId, []);
206
+ }
207
+ toolResults.get(item.toolCallId).push(i);
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // 第二遍:决定哪些 assistant 消息需要保留
214
+ const assistantIndicesToKeep = new Set();
215
+ for (let i = 0; i < messages.length; i++) {
216
+ const msg = messages[i];
217
+ if (msg.role !== 'assistant') continue;
218
+
219
+ // 检查是否有 tool-call
220
+ let hasToolCall = false;
221
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
222
+ for (const item of content) {
223
+ if (item.type === 'tool-call' || item.type === 'tool-use') {
224
+ hasToolCall = true;
225
+ break;
226
+ }
227
+ }
228
+ if (!hasToolCall && Array.isArray(msg.tool_calls)) {
229
+ hasToolCall = msg.tool_calls.some((tc) => tc.id);
230
+ }
231
+
232
+ if (hasToolCall) {
233
+ assistantIndicesToKeep.add(i);
234
+ } else if (i >= messages.length - keepRecentAssistant) {
235
+ // 保留最近几条非工具调用的 assistant 消息
236
+ assistantIndicesToKeep.add(i);
237
+ }
238
+ }
239
+
240
+ // 保存有 tool-call 但没有 result 的 assistant(防止切掉半截链条)
241
+ for (const [toolCallId, assistantIdx] of assistantToolCalls) {
242
+ if (!toolResults.has(toolCallId)) {
243
+ assistantIndicesToKeep.add(assistantIdx);
244
+ }
245
+ }
246
+
247
+ // 第三遍:决定哪些 tool-result 要保留(只保留有对应 assistant 的)
248
+ const indicesToKeep = new Set();
249
+ for (let i = 0; i < messages.length; i++) {
250
+ const msg = messages[i];
251
+ if (msg.role === 'tool') {
252
+ const content = Array.isArray(msg.content) ? msg.content : [msg.content];
253
+ const hasPairedAssistant = content.some(
254
+ (item) =>
255
+ (item.type === 'tool-result' || item.type === 'tool_result') &&
256
+ item.toolCallId &&
257
+ assistantToolCalls.has(item.toolCallId) &&
258
+ assistantIndicesToKeep.has(assistantToolCalls.get(item.toolCallId))
259
+ );
260
+ if (hasPairedAssistant) {
261
+ indicesToKeep.add(i);
262
+ }
263
+ } else if (msg.role === 'assistant') {
264
+ if (assistantIndicesToKeep.has(i)) {
265
+ indicesToKeep.add(i);
266
+ }
267
+ } else {
268
+ // user, system 等角色默认保留
269
+ indicesToKeep.add(i);
270
+ }
271
+ }
272
+
273
+ return Array.from(indicesToKeep)
274
+ .sort((a, b) => a - b)
275
+ .map((i) => messages[i]);
276
+ }
277
+
278
+ module.exports = {
279
+ validateMessagesPairing,
280
+ validateToolCalls,
281
+ validateAll,
282
+ filterPairedMessages,
283
+ };
@@ -1,12 +1,42 @@
1
1
  /**
2
2
  * Foliko Retry Strategy - 重试策略系统
3
3
  * 提供多种重试策略,支持指数退避、抖动等
4
+ *
5
+ * 统一的重试入口,取代以下分散的重试逻辑:
6
+ * - ChatQueueManager.executeWithRetry() (utils/chat-queue.js)
7
+ * - Subagent.chat() 中的重试循环 (core/subagent.js)
8
+ * - utils/index.js 中的 retry() 函数
4
9
  */
5
10
 
6
11
  const { logger } = require('./logger');
7
12
 
8
13
  const log = logger.child('RetryStrategy');
9
14
 
15
+ /**
16
+ * 网络错误码列表
17
+ */
18
+ const NETWORK_ERROR_CODES = [
19
+ 'ECONNREFUSED',
20
+ 'ECONNRESET',
21
+ 'ETIMEDOUT',
22
+ 'ENOTFOUND',
23
+ 'EAI_AGAIN',
24
+ 'EPIPE',
25
+ 'ERR_CONNECTION_REFUSED',
26
+ 'ERR_CONNECTION_RESET',
27
+ 'ERR_CONNECTION_TIMEOUT',
28
+ ];
29
+
30
+ /**
31
+ * API 可重试的 HTTP 状态码
32
+ */
33
+ const RETRYABLE_HTTP_CODES = [429, 500, 502, 503, 504];
34
+
35
+ /**
36
+ * AI SDK 错误名称(可重试)
37
+ */
38
+ const AI_RETRYABLE_ERRORS = ['AI_RetryError', 'RetryError', 'AI_APICallError'];
39
+
10
40
  /**
11
41
  * 重试策略配置
12
42
  * @typedef {Object} RetryConfig
@@ -63,16 +93,32 @@ const PRESETS = {
63
93
  factor: 1.5,
64
94
  jitter: 0.05,
65
95
  },
66
- /** 空闲连接保活(适合长时间运行的连接检查) */
67
- idle: {
96
+ /** AI API 重试(包含 AI SDK 特定错误判断) */
97
+ ai: {
68
98
  maxAttempts: 3,
69
- baseDelay: 5000,
99
+ baseDelay: 2000,
70
100
  maxDelay: 15000,
71
- factor: 1.5,
72
- jitter: 0.1,
101
+ factor: 2,
102
+ jitter: 0.2,
103
+ shouldRetry: (error) => {
104
+ if (!error) return false;
105
+ const name = error.name || '';
106
+ if (AI_RETRYABLE_ERRORS.includes(name)) return true;
107
+ // 也检查标准网络错误
108
+ return isNetworkError(error);
109
+ },
110
+ },
111
+ /** API 调用重试(适合第三方API) */
112
+ api: {
113
+ maxAttempts: 4,
114
+ baseDelay: 500,
115
+ maxDelay: 8000,
116
+ factor: 2,
117
+ jitter: 0.15,
73
118
  shouldRetry: (error) => {
74
- const networkCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
75
- return error.code && networkCodes.includes(error.code);
119
+ const status = error.status || error.statusCode;
120
+ if (status && (status >= 500 || status === 429)) return true;
121
+ return isNetworkError(error);
76
122
  },
77
123
  },
78
124
  /** 数据库重试(适合数据库连接) */
@@ -104,19 +150,119 @@ const PRESETS = {
104
150
  return dbKeywords.some((k) => msg.includes(k));
105
151
  },
106
152
  },
107
- /** API调用重试(适合第三方API) */
108
- api: {
109
- maxAttempts: 4,
110
- baseDelay: 500,
111
- maxDelay: 8000,
112
- factor: 2,
113
- jitter: 0.15,
114
- shouldRetry: (error) => {
115
- const status = error.status || error.statusCode;
116
- if (status && (status >= 500 || status === 429)) return true;
117
- const networkCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
118
- if (error.code && networkCodes.includes(error.code)) return true;
119
- return false;
120
- },
121
- },
153
+ };
154
+
155
+ /**
156
+ * 判断错误是否由网络/AI服务问题引起(统一入口)
157
+ * 替代 ChatQueueManager.isRetryableError() 和 Subagent 中的分类逻辑
158
+ * @param {Error} err
159
+ * @returns {boolean}
160
+ */
161
+ function isNetworkError(err) {
162
+ if (!err) return false;
163
+
164
+ const code = err.code || err.status || err.statusCode;
165
+ const message = (err.message || '').toLowerCase();
166
+ const name = err.name || '';
167
+
168
+ // 检查网络错误码
169
+ if (code && NETWORK_ERROR_CODES.includes(code)) return true;
170
+
171
+ // 检查可重试 HTTP 状态码
172
+ if (typeof code === 'number' && RETRYABLE_HTTP_CODES.includes(code)) return true;
173
+
174
+ // 检查 AI SDK 重试错误
175
+ if (AI_RETRYABLE_ERRORS.includes(name)) return true;
176
+
177
+ // 检查消息关键词
178
+ const retryableKeywords = [
179
+ 'timeout', 'network', 'econnrefused', 'econnreset', 'etimedout',
180
+ 'enotfound', 'rate limit', '负载较高', '暂时不可用',
181
+ '429', '500', '502', '503', '504',
182
+ ];
183
+ return retryableKeywords.some((kw) => message.includes(kw));
184
+ }
185
+
186
+ /**
187
+ * 计算下次重试延迟(指数退避 + 随机抖动)
188
+ * @param {number} attempt - 当前尝试次数(从 1 开始)
189
+ * @param {RetryConfig} config - 重试配置
190
+ * @returns {number} 延迟毫秒数
191
+ */
192
+ function calculateDelay(attempt, config = {}) {
193
+ const factor = config.factor || 2;
194
+ const baseDelay = config.baseDelay || 1000;
195
+ const maxDelay = config.maxDelay || 30000;
196
+ const jitter = config.jitter ?? 0.2;
197
+
198
+ // 指数退避: baseDelay * factor ^ (attempt - 1)
199
+ const exponentialDelay = baseDelay * Math.pow(factor, attempt - 1);
200
+
201
+ // 加上抖动: delay * (1 + random(-jitter, +jitter))
202
+ const jitterAmount = exponentialDelay * jitter * (Math.random() * 2 - 1);
203
+ const delay = Math.min(exponentialDelay + jitterAmount, maxDelay);
204
+
205
+ return Math.max(delay, 0);
206
+ }
207
+
208
+ /**
209
+ * 带重试策略的函数执行
210
+ * @param {Function} fn - 要执行的异步函数
211
+ * @param {RetryConfig} [config] - 重试配置(默认 standard)
212
+ * @returns {Promise<any>}
213
+ * @throws {Error} 所有重试耗尽后抛出最后一次错误
214
+ */
215
+ async function withRetry(fn, config = {}) {
216
+ const merged = { ...PRESETS.standard, ...config };
217
+ const { maxAttempts, onRetry } = merged;
218
+
219
+ let lastError;
220
+
221
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
222
+ try {
223
+ return await fn();
224
+ } catch (err) {
225
+ lastError = err;
226
+
227
+ // 判断是否应重试
228
+ const shouldRetry = merged.shouldRetry || isNetworkError;
229
+ if (!shouldRetry(err)) {
230
+ throw err; // 不可重试的错误,立即抛出
231
+ }
232
+
233
+ if (attempt < maxAttempts) {
234
+ const delay = calculateDelay(attempt, merged);
235
+ if (onRetry) {
236
+ try { onRetry(err, attempt, delay); } catch { /* ignore */ }
237
+ }
238
+ await new Promise((resolve) => setTimeout(resolve, delay));
239
+ }
240
+ }
241
+ }
242
+
243
+ throw lastError;
244
+ }
245
+
246
+ /**
247
+ * 装饰器:使函数可重试
248
+ * @param {RetryConfig} [config]
249
+ * @returns {Function} 装饰后的函数
250
+ */
251
+ function retryable(config = {}) {
252
+ return function (fn) {
253
+ return async (...args) => {
254
+ return withRetry(() => fn(...args), config);
255
+ };
256
+ };
257
+ }
258
+
259
+ module.exports = {
260
+ withRetry,
261
+ retryable,
262
+ isNetworkError,
263
+ calculateDelay,
264
+ PRESETS,
265
+ NETWORK_ERROR_CODES,
266
+ RETRYABLE_HTTP_CODES,
267
+ AI_RETRYABLE_ERRORS,
122
268
  };