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.
- package/.agent/data/plugins-state.json +8 -0
- package/.agent/sessions/cli_default.json +258 -260
- package/cli/bin/foliko.js +2 -2
- package/cli/src/commands/chat.js +15 -26
- package/cli/src/ui/chat-ui.js +102 -165
- package/cli/src/ui/footer-bar.js +7 -32
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/package.json +1 -2
- package/plugins/audit-plugin.js +11 -7
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +12 -1
- package/plugins/extension-executor-plugin.js +2 -2
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +3 -3
- package/plugins/memory-plugin.js +8 -11
- package/plugins/plugin-manager-plugin.js +9 -11
- package/plugins/qq-plugin.js +9 -9
- package/plugins/rules-plugin.js +7 -7
- package/plugins/scheduler-plugin.js +22 -18
- package/plugins/session-plugin.js +14 -14
- package/plugins/storage-plugin.js +11 -10
- package/plugins/subagent-plugin.js +13 -9
- package/plugins/think-plugin.js +63 -59
- package/plugins/tools-plugin.js +8 -8
- package/plugins/weixin-plugin.js +5 -5
- package/src/capabilities/skill-manager.js +23 -15
- package/src/capabilities/workflow-engine.js +2 -2
- package/src/core/agent-chat.js +70 -26
- package/src/core/agent.js +17 -27
- package/src/core/chat-session.js +7 -161
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +6 -181
- package/src/core/framework.js +125 -6
- package/src/core/plugin-base.js +7 -5
- package/src/core/provider.js +6 -0
- package/src/core/subagent.js +16 -135
- package/src/core/tool-executor.js +2 -70
- package/src/executors/mcp-executor.js +12 -10
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/download.js +5 -4
- package/src/utils/message-validator.js +283 -0
- package/src/utils/retry.js +168 -22
- package/src/utils/sandbox.js +60 -207
- package/cli/src/utils/debounce.js +0 -106
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const { logger } = require('../utils/logger');
|
|
11
11
|
const { TokenCounter } = require('./token-counter');
|
|
12
|
-
const {
|
|
12
|
+
const { validateMessagesPairing, filterPairedMessages } = require('../utils/message-validator');
|
|
13
13
|
|
|
14
14
|
// 模型上下文限制表
|
|
15
15
|
const MODEL_CONTEXT_LIMITS = {
|
|
@@ -30,7 +30,7 @@ const MODEL_CONTEXT_LIMITS = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
// 压缩超时
|
|
33
|
-
const
|
|
33
|
+
const COMPRESSION_TIMEOUT_MS = 1200000;
|
|
34
34
|
|
|
35
35
|
class ContextCompressor {
|
|
36
36
|
/**
|
|
@@ -128,8 +128,8 @@ class ContextCompressor {
|
|
|
128
128
|
return new Promise((_, reject) => {
|
|
129
129
|
this._compressionTimeoutId = setTimeout(() => {
|
|
130
130
|
this._compressionTimeoutId = null;
|
|
131
|
-
reject(new Error(`Compression timeout (${
|
|
132
|
-
},
|
|
131
|
+
reject(new Error(`Compression timeout (${COMPRESSION_TIMEOUT_MS}ms)`));
|
|
132
|
+
}, COMPRESSION_TIMEOUT_MS);
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -242,122 +242,7 @@ class ContextCompressor {
|
|
|
242
242
|
* @private
|
|
243
243
|
*/
|
|
244
244
|
_filterPairedMessages(messages) {
|
|
245
|
-
|
|
246
|
-
const isToolCall = (item) =>
|
|
247
|
-
item && (item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId;
|
|
248
|
-
const isToolResult = (item) =>
|
|
249
|
-
item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId;
|
|
250
|
-
|
|
251
|
-
// 第一遍:找出所有 tool call 和它们的 results
|
|
252
|
-
const assistantToolCalls = new Map(); // toolCallId -> assistant message index
|
|
253
|
-
const toolResults = new Map(); // toolCallId -> tool result indices
|
|
254
|
-
|
|
255
|
-
for (let i = 0; i < messages.length; i++) {
|
|
256
|
-
const msg = messages[i];
|
|
257
|
-
if (msg.role === 'assistant') {
|
|
258
|
-
// 格式1: msg.content 中的 tool-call 块
|
|
259
|
-
if (msg.content) {
|
|
260
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
261
|
-
for (const item of content) {
|
|
262
|
-
if (isToolCall(item)) {
|
|
263
|
-
assistantToolCalls.set(item.toolCallId, i);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// 格式2: msg.tool_calls 数组 (OpenAI 格式)
|
|
268
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
269
|
-
for (const tc of msg.tool_calls) {
|
|
270
|
-
if (tc.id) {
|
|
271
|
-
assistantToolCalls.set(tc.id, i);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
277
|
-
for (const item of msg.content) {
|
|
278
|
-
if (isToolResult(item)) {
|
|
279
|
-
if (!toolResults.has(item.toolCallId)) {
|
|
280
|
-
toolResults.set(item.toolCallId, []);
|
|
281
|
-
}
|
|
282
|
-
toolResults.get(item.toolCallId).push(i);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// 第二遍:找出哪些 assistant 消息需要保留
|
|
289
|
-
const assistantIndicesToKeep = new Set();
|
|
290
|
-
for (let i = 0; i < messages.length; i++) {
|
|
291
|
-
const msg = messages[i];
|
|
292
|
-
if (msg.role === 'assistant') {
|
|
293
|
-
let hasToolCall = false;
|
|
294
|
-
// 检查 msg.content 格式
|
|
295
|
-
if (msg.content) {
|
|
296
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
297
|
-
for (const item of content) {
|
|
298
|
-
if (isToolCall(item)) {
|
|
299
|
-
hasToolCall = true;
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// 检查 msg.tool_calls 格式 (OpenAI)
|
|
305
|
-
if (!hasToolCall && Array.isArray(msg.tool_calls)) {
|
|
306
|
-
for (const tc of msg.tool_calls) {
|
|
307
|
-
if (tc.id) {
|
|
308
|
-
hasToolCall = true;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (hasToolCall) {
|
|
314
|
-
assistantIndicesToKeep.add(i);
|
|
315
|
-
} else if (i >= messages.length - 3) {
|
|
316
|
-
// 保留最近几条 assistant 消息
|
|
317
|
-
assistantIndicesToKeep.add(i);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 如果有孤儿的 tool call(没有 result),保留该 assistant 消息
|
|
323
|
-
for (const [toolCallId, assistantIdx] of assistantToolCalls) {
|
|
324
|
-
if (!toolResults.has(toolCallId)) {
|
|
325
|
-
assistantIndicesToKeep.add(assistantIdx);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 第三遍:确定哪些 tool result 要保留
|
|
330
|
-
const indicesToKeep = new Set();
|
|
331
|
-
for (let i = 0; i < messages.length; i++) {
|
|
332
|
-
const msg = messages[i];
|
|
333
|
-
if (msg.role === 'tool') {
|
|
334
|
-
let hasPairedAssistant = false;
|
|
335
|
-
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
336
|
-
for (const item of content) {
|
|
337
|
-
if (isToolResult(item)) {
|
|
338
|
-
const assistantIdx = assistantToolCalls.get(item.toolCallId);
|
|
339
|
-
if (assistantIdx !== undefined && assistantIndicesToKeep.has(assistantIdx)) {
|
|
340
|
-
hasPairedAssistant = true;
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (hasPairedAssistant) {
|
|
346
|
-
indicesToKeep.add(i);
|
|
347
|
-
}
|
|
348
|
-
} else if (msg.role === 'assistant') {
|
|
349
|
-
if (assistantIndicesToKeep.has(i)) {
|
|
350
|
-
indicesToKeep.add(i);
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
// user, system 等角色默认保留
|
|
354
|
-
indicesToKeep.add(i);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 按索引排序并返回
|
|
359
|
-
const sortedIndices = Array.from(indicesToKeep).sort((a, b) => a - b);
|
|
360
|
-
return sortedIndices.map((i) => messages[i]);
|
|
245
|
+
return filterPairedMessages(messages);
|
|
361
246
|
}
|
|
362
247
|
|
|
363
248
|
/**
|
|
@@ -366,67 +251,7 @@ class ContextCompressor {
|
|
|
366
251
|
* @returns {Array} 验证后的消息
|
|
367
252
|
*/
|
|
368
253
|
validateMessagesPairing(messages) {
|
|
369
|
-
|
|
370
|
-
const assistantToolCallIds = new Set();
|
|
371
|
-
for (const msg of messages) {
|
|
372
|
-
if (msg.role === 'assistant') {
|
|
373
|
-
// 格式1: msg.content 中的 tool-call 块
|
|
374
|
-
if (Array.isArray(msg.content)) {
|
|
375
|
-
for (const item of msg.content) {
|
|
376
|
-
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
377
|
-
assistantToolCallIds.add(item.toolCallId);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// 格式2: msg.tool_calls 数组 (OpenAI 格式)
|
|
382
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
383
|
-
for (const tc of msg.tool_calls) {
|
|
384
|
-
if (tc.id) {
|
|
385
|
-
assistantToolCallIds.add(tc.id);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// 检查并删除没有配对的 tool result
|
|
393
|
-
let removedCount = 0;
|
|
394
|
-
for (const msg of messages) {
|
|
395
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
396
|
-
const originalLength = msg.content.length;
|
|
397
|
-
msg.content = msg.content.filter((item) => {
|
|
398
|
-
// 兼容 tool-result 和 tool_result 两种类型
|
|
399
|
-
if (
|
|
400
|
-
item &&
|
|
401
|
-
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
402
|
-
item.toolCallId
|
|
403
|
-
) {
|
|
404
|
-
if (!assistantToolCallIds.has(item.toolCallId)) {
|
|
405
|
-
removedCount++;
|
|
406
|
-
return false; // 删除没有配对的 tool result
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return true;
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// 如果所有 content 都被删除了,标记整个消息待删除
|
|
413
|
-
if (msg.content.length === 0 && originalLength > 0) {
|
|
414
|
-
msg._orphaned = true;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// 移除被标记为 orphaned 的 tool 消息
|
|
420
|
-
const originalLength = messages.length;
|
|
421
|
-
const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
|
|
422
|
-
|
|
423
|
-
if (removedCount > 0 || filtered.length !== originalLength) {
|
|
424
|
-
logger.debug(
|
|
425
|
-
`Removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return filtered;
|
|
254
|
+
return validateMessagesPairing(messages);
|
|
430
255
|
}
|
|
431
256
|
|
|
432
257
|
/**
|
package/src/core/framework.js
CHANGED
|
@@ -324,6 +324,55 @@ class Framework extends EventEmitter {
|
|
|
324
324
|
return this.toolRegistry.getAll();
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// 公开 API — 减少外部直接访问私有属性
|
|
329
|
+
// ============================================================================
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 获取主 Agent
|
|
333
|
+
* @returns {Agent|null}
|
|
334
|
+
*/
|
|
335
|
+
getMainAgent() {
|
|
336
|
+
return this._mainAgent;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 获取主 Agent 的 System Prompt
|
|
341
|
+
* @returns {string}
|
|
342
|
+
*/
|
|
343
|
+
getSystemPrompt() {
|
|
344
|
+
if (this._mainAgent) {
|
|
345
|
+
return this._mainAgent._buildSystemPrompt();
|
|
346
|
+
}
|
|
347
|
+
return '';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 获取插件实例(安全访问)
|
|
352
|
+
* @param {string} name - 插件名称
|
|
353
|
+
* @returns {Object|null}
|
|
354
|
+
*/
|
|
355
|
+
getPluginInstance(name) {
|
|
356
|
+
const entry = this.pluginManager.get(name);
|
|
357
|
+
return entry || null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 获取所有 Agent 列表
|
|
362
|
+
* @returns {Array<Agent>}
|
|
363
|
+
*/
|
|
364
|
+
getAgents() {
|
|
365
|
+
return [...this._agents];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 获取所有 SessionContext 的 ID 列表
|
|
370
|
+
* @returns {string[]}
|
|
371
|
+
*/
|
|
372
|
+
listSessionContexts() {
|
|
373
|
+
return Array.from(this._sessionContexts.keys());
|
|
374
|
+
}
|
|
375
|
+
|
|
327
376
|
// ============================================================================
|
|
328
377
|
// 事件描述注册(供 Ambient Agent 使用)
|
|
329
378
|
// ============================================================================
|
|
@@ -551,6 +600,68 @@ class Framework extends EventEmitter {
|
|
|
551
600
|
}
|
|
552
601
|
}
|
|
553
602
|
|
|
603
|
+
// Session TTL 清理配置
|
|
604
|
+
_sessionTTL = 30 * 60 * 1000; // 默认 30 分钟
|
|
605
|
+
_sessionCleanupInterval = null;
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* 配置 Session TTL
|
|
609
|
+
* @param {number} ttlMs - TTL 毫秒数
|
|
610
|
+
*/
|
|
611
|
+
setSessionTTL(ttlMs) {
|
|
612
|
+
this._sessionTTL = ttlMs;
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 启动 Session 自动清理
|
|
618
|
+
* @param {number} [intervalMs=60000] - 检查间隔
|
|
619
|
+
*/
|
|
620
|
+
startSessionCleanup(intervalMs = 60000) {
|
|
621
|
+
if (this._sessionCleanupInterval) {
|
|
622
|
+
clearInterval(this._sessionCleanupInterval);
|
|
623
|
+
}
|
|
624
|
+
this._sessionCleanupInterval = setInterval(() => {
|
|
625
|
+
this._cleanupStaleSessions();
|
|
626
|
+
}, intervalMs);
|
|
627
|
+
// 允许进程退出时不阻止
|
|
628
|
+
if (this._sessionCleanupInterval.unref) {
|
|
629
|
+
this._sessionCleanupInterval.unref();
|
|
630
|
+
}
|
|
631
|
+
return this;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* 停止 Session 自动清理
|
|
636
|
+
*/
|
|
637
|
+
stopSessionCleanup() {
|
|
638
|
+
if (this._sessionCleanupInterval) {
|
|
639
|
+
clearInterval(this._sessionCleanupInterval);
|
|
640
|
+
this._sessionCleanupInterval = null;
|
|
641
|
+
}
|
|
642
|
+
return this;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 清理过期的 Session
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
_cleanupStaleSessions() {
|
|
650
|
+
const now = Date.now();
|
|
651
|
+
const staleIds = [];
|
|
652
|
+
for (const [sessionId, ctx] of this._sessionContexts) {
|
|
653
|
+
if (ctx.metadata && (now - ctx.metadata.lastActive) > this._sessionTTL) {
|
|
654
|
+
staleIds.push(sessionId);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const id of staleIds) {
|
|
658
|
+
this.destroySessionContext(id);
|
|
659
|
+
}
|
|
660
|
+
if (staleIds.length > 0) {
|
|
661
|
+
this.logger.debug(`Cleaned up ${staleIds.length} stale sessions`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
554
665
|
/**
|
|
555
666
|
* 列出所有活跃的 Session ID
|
|
556
667
|
* @returns {string[]}
|
|
@@ -737,9 +848,9 @@ class Framework extends EventEmitter {
|
|
|
737
848
|
|
|
738
849
|
/**
|
|
739
850
|
* 同步主Agent的提示词部分到所有子Agent
|
|
740
|
-
* @
|
|
851
|
+
* @public — 插件可能需要调用
|
|
741
852
|
*/
|
|
742
|
-
|
|
853
|
+
syncPromptPartsToSubagents() {
|
|
743
854
|
if (!this._mainAgent || !this._mainAgent._systemPromptBuilder) return;
|
|
744
855
|
const mainParts = this._mainAgent._systemPromptBuilder._parts;
|
|
745
856
|
if (!mainParts) return;
|
|
@@ -784,8 +895,9 @@ class Framework extends EventEmitter {
|
|
|
784
895
|
lines.push('');
|
|
785
896
|
|
|
786
897
|
// 2. 主Agent的系统提示词(基础部分)
|
|
787
|
-
|
|
788
|
-
|
|
898
|
+
const mainAgent = this.getMainAgent();
|
|
899
|
+
if (mainAgent) {
|
|
900
|
+
const mainPrompt = mainAgent.getOriginalPrompt();
|
|
789
901
|
if (mainPrompt) {
|
|
790
902
|
lines.push('## 主Agent的系统提示词');
|
|
791
903
|
lines.push(mainPrompt);
|
|
@@ -988,10 +1100,17 @@ class Framework extends EventEmitter {
|
|
|
988
1100
|
this._toolRegistryListeners = [];
|
|
989
1101
|
}
|
|
990
1102
|
|
|
991
|
-
//
|
|
1103
|
+
// 停止 Session 清理
|
|
1104
|
+
this.stopSessionCleanup();
|
|
1105
|
+
|
|
1106
|
+
// 卸载所有插件(逐个保护,防止一个失败阻断后续)
|
|
992
1107
|
const plugins = this.pluginManager.getAll();
|
|
993
1108
|
for (const { name } of plugins) {
|
|
994
|
-
|
|
1109
|
+
try {
|
|
1110
|
+
await this.pluginManager.unload(name);
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
this.logger.warn(`Failed to unload plugin '${name}': ${err.message}`);
|
|
1113
|
+
}
|
|
995
1114
|
}
|
|
996
1115
|
|
|
997
1116
|
// 清空工具
|
package/src/core/plugin-base.js
CHANGED
|
@@ -286,10 +286,11 @@ class Plugin {
|
|
|
286
286
|
this._promptParts.push({ name, priority, provider });
|
|
287
287
|
|
|
288
288
|
// 如果主 agent 已存在,直接注册
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
const mainAgent = this._framework.getMainAgent();
|
|
290
|
+
if (mainAgent) {
|
|
291
|
+
mainAgent.registerPromptPart(name, priority, provider);
|
|
291
292
|
// 同步到所有子Agent
|
|
292
|
-
this._framework.
|
|
293
|
+
this._framework.syncPromptPartsToSubagents();
|
|
293
294
|
return;
|
|
294
295
|
}
|
|
295
296
|
|
|
@@ -307,14 +308,15 @@ class Plugin {
|
|
|
307
308
|
if (this._deferredPromptParts) {
|
|
308
309
|
for (const part of this._deferredPromptParts) {
|
|
309
310
|
try {
|
|
310
|
-
this._framework.
|
|
311
|
+
const mainAgent = this._framework.getMainAgent();
|
|
312
|
+
mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
|
|
311
313
|
} catch (err) {
|
|
312
314
|
log.error(` Failed to register prompt part ${part.name}:`, err.message);
|
|
313
315
|
}
|
|
314
316
|
}
|
|
315
317
|
this._deferredPromptParts = null;
|
|
316
318
|
// 同步到所有子Agent
|
|
317
|
-
this._framework.
|
|
319
|
+
this._framework.syncPromptPartsToSubagents();
|
|
318
320
|
}
|
|
319
321
|
}, 100);
|
|
320
322
|
});
|
package/src/core/provider.js
CHANGED
|
@@ -14,26 +14,32 @@ const DEFAULT_PROVIDERS = {
|
|
|
14
14
|
openai: {
|
|
15
15
|
name: 'OpenAI',
|
|
16
16
|
baseURL: 'https://api.openai.com/v1',
|
|
17
|
+
defaultModel: 'gpt-4o',
|
|
17
18
|
},
|
|
18
19
|
ollama: {
|
|
19
20
|
name: 'Ollama',
|
|
20
21
|
baseURL: 'http://localhost:11434/v1',
|
|
22
|
+
defaultModel: 'llama3',
|
|
21
23
|
},
|
|
22
24
|
lmstudio: {
|
|
23
25
|
name: 'LM Studio',
|
|
24
26
|
baseURL: 'http://localhost:1234/v1',
|
|
27
|
+
defaultModel: 'local-model',
|
|
25
28
|
},
|
|
26
29
|
deepseek: {
|
|
27
30
|
name: 'DeepSeek',
|
|
28
31
|
baseURL: 'https://api.deepseek.com/v1',
|
|
32
|
+
defaultModel: 'deepseek-chat',
|
|
29
33
|
},
|
|
30
34
|
anthropic: {
|
|
31
35
|
name: 'Anthropic',
|
|
32
36
|
baseURL: 'https://api.anthropic.com/v1',
|
|
37
|
+
defaultModel: 'claude-sonnet-4-20250514',
|
|
33
38
|
},
|
|
34
39
|
minimax: {
|
|
35
40
|
name: 'MiniMax',
|
|
36
41
|
baseURL: 'https://api.minimaxi.com/v1',
|
|
42
|
+
defaultModel: 'MiniMax-M2.7',
|
|
37
43
|
},
|
|
38
44
|
};
|
|
39
45
|
|
package/src/core/subagent.js
CHANGED
|
@@ -8,8 +8,9 @@ const { EventEmitter } = require('../utils/event-emitter');
|
|
|
8
8
|
const { cleanResponse } = require('../utils');
|
|
9
9
|
const { generateText, tool, stepCountIs, isLoopFinished, RetryError, ToolLoopAgent } = require('ai');
|
|
10
10
|
const { z } = require('zod');
|
|
11
|
-
const fs=require('fs/promises')
|
|
12
11
|
const { logger } = require('../utils/logger');
|
|
12
|
+
const { validateAll } = require('../utils/message-validator');
|
|
13
|
+
const { isNetworkError, calculateDelay } = require('../utils/retry');
|
|
13
14
|
|
|
14
15
|
class Subagent extends EventEmitter {
|
|
15
16
|
/**
|
|
@@ -94,64 +95,7 @@ class Subagent extends EventEmitter {
|
|
|
94
95
|
* @private
|
|
95
96
|
*/
|
|
96
97
|
_validateMessagesPairing(messages) {
|
|
97
|
-
|
|
98
|
-
const assistantToolCallIds = new Set();
|
|
99
|
-
for (const msg of messages) {
|
|
100
|
-
if (msg.role === 'assistant') {
|
|
101
|
-
// 格式1: msg.content 中的 tool-call 块
|
|
102
|
-
if (Array.isArray(msg.content)) {
|
|
103
|
-
for (const item of msg.content) {
|
|
104
|
-
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
105
|
-
assistantToolCallIds.add(item.toolCallId);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// 格式2: msg.tool_calls 数组 (OpenAI 格式)
|
|
110
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
111
|
-
for (const tc of msg.tool_calls) {
|
|
112
|
-
if (tc.id) {
|
|
113
|
-
assistantToolCallIds.add(tc.id);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 检查并删除没有配对的 tool result
|
|
121
|
-
let removedCount = 0;
|
|
122
|
-
for (const msg of messages) {
|
|
123
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
124
|
-
const originalLength = msg.content.length;
|
|
125
|
-
msg.content = msg.content.filter((item) => {
|
|
126
|
-
if (
|
|
127
|
-
item &&
|
|
128
|
-
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
129
|
-
item.toolCallId
|
|
130
|
-
) {
|
|
131
|
-
if (!assistantToolCallIds.has(item.toolCallId)) {
|
|
132
|
-
removedCount++;
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return true;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (msg.content.length === 0 && originalLength > 0) {
|
|
140
|
-
msg._orphaned = true;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const originalLength = messages.length;
|
|
146
|
-
const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
|
|
147
|
-
|
|
148
|
-
if (removedCount > 0 || filtered.length !== originalLength) {
|
|
149
|
-
logger.debug(
|
|
150
|
-
`[Subagent:${this.name}] _validateMessagesPairing: removed ${removedCount} orphaned tool-results, ${originalLength - filtered.length} orphaned tool messages`
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return filtered;
|
|
98
|
+
return validateAll(messages);
|
|
155
99
|
}
|
|
156
100
|
|
|
157
101
|
/**
|
|
@@ -159,64 +103,8 @@ class Subagent extends EventEmitter {
|
|
|
159
103
|
* @private
|
|
160
104
|
*/
|
|
161
105
|
_validateToolCalls(messages) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
for (const msg of messages) {
|
|
166
|
-
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
167
|
-
for (const item of msg.content) {
|
|
168
|
-
if (item.type !== 'tool-call' && item.type !== 'tool-use') {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const input = item.input;
|
|
173
|
-
if (typeof input !== 'string') {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const trimmed = input.trim();
|
|
178
|
-
if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
|
|
179
|
-
if (item.toolCallId) {
|
|
180
|
-
invalidatedToolCallIds.add(item.toolCallId);
|
|
181
|
-
}
|
|
182
|
-
logger.warn(
|
|
183
|
-
`[Subagent:${this.name}] _validateToolCalls: invalid tool-call input="${input}", toolCallId=${item.toolCallId}`
|
|
184
|
-
);
|
|
185
|
-
item.type = 'text';
|
|
186
|
-
item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
|
|
187
|
-
delete item.toolCallId;
|
|
188
|
-
delete item.toolName;
|
|
189
|
-
delete item.input;
|
|
190
|
-
fixedCount++;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (invalidatedToolCallIds.size > 0) {
|
|
197
|
-
for (const msg of messages) {
|
|
198
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
199
|
-
const oldLen = msg.content.length;
|
|
200
|
-
msg.content = msg.content.filter((item) => {
|
|
201
|
-
if (item.type !== 'tool-result' && item.type !== 'tool_result') {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
if (item.toolCallId && invalidatedToolCallIds.has(item.toolCallId)) {
|
|
205
|
-
logger.warn(
|
|
206
|
-
`[Subagent:${this.name}] _validateToolCalls: removing orphaned tool-result with toolCallId=${item.toolCallId}`
|
|
207
|
-
);
|
|
208
|
-
fixedCount++;
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
return true;
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (fixedCount > 0) {
|
|
218
|
-
logger.info(`[Subagent:${this.name}] _validateToolCalls: Fixed ${fixedCount} incomplete tool calls/results`);
|
|
219
|
-
}
|
|
106
|
+
// validateAll 内部已包含 validateMessagesPairing + validateToolCalls
|
|
107
|
+
// Subagent 的 chat() 中 _validateMessagesPairing 已调用 validateAll,所以这里不再重复
|
|
220
108
|
}
|
|
221
109
|
|
|
222
110
|
/**
|
|
@@ -327,8 +215,9 @@ class Subagent extends EventEmitter {
|
|
|
327
215
|
}
|
|
328
216
|
|
|
329
217
|
// 2. 主Agent的系统提示词
|
|
330
|
-
|
|
331
|
-
|
|
218
|
+
const mainAgent = this.framework?.getMainAgent?.();
|
|
219
|
+
if (mainAgent) {
|
|
220
|
+
const mainPrompt = mainAgent.getOriginalPrompt();
|
|
332
221
|
if (mainPrompt) {
|
|
333
222
|
lines.push('## 主Agent的系统提示词');
|
|
334
223
|
lines.push(mainPrompt);
|
|
@@ -472,18 +361,8 @@ class Subagent extends EventEmitter {
|
|
|
472
361
|
} catch (err) {
|
|
473
362
|
logger.warn(`[Subagent:${this.name}] generateText error: ${err.message}`);
|
|
474
363
|
lastError = err;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const errCode = err?.code || err?.status;
|
|
478
|
-
|
|
479
|
-
// 判断是否是重试错误
|
|
480
|
-
const isRetryError =
|
|
481
|
-
RetryError.isInstance?.(err) ||
|
|
482
|
-
errName === 'AI_RetryError' ||
|
|
483
|
-
errName === 'RetryError' ||
|
|
484
|
-
err?.reason === 'maxRetriesExceeded';
|
|
485
|
-
|
|
486
|
-
if (isRetryError && attempt < maxRetries) {
|
|
364
|
+
// 统一使用 retry.js 的错误判断
|
|
365
|
+
if (isNetworkError(err) && attempt < maxRetries) {
|
|
487
366
|
logger.warn(
|
|
488
367
|
`[Subagent:${this.name}] AI 服务暂时不可用,正在重试 (${attempt + 1}/${maxRetries})`
|
|
489
368
|
);
|
|
@@ -511,11 +390,13 @@ class Subagent extends EventEmitter {
|
|
|
511
390
|
*/
|
|
512
391
|
_getFriendlyError(err) {
|
|
513
392
|
const errName = err?.name || '';
|
|
393
|
+
|
|
394
|
+
// 先用 isNetworkError 统一判断
|
|
395
|
+
if (isNetworkError(err)) {
|
|
396
|
+
return 'AI 服务暂时不可用,请稍后重试';
|
|
397
|
+
}
|
|
398
|
+
|
|
514
399
|
const errorMessages = {
|
|
515
|
-
AI_RetryError: 'AI 服务暂时不可用,请稍后重试',
|
|
516
|
-
AI_APICallError: err?.isRetryable
|
|
517
|
-
? 'AI 服务暂时不可用,请稍后重试'
|
|
518
|
-
: err.message?.split('\n')[0] || 'AI 请求失败',
|
|
519
400
|
AI_NoContentGeneratedError: 'AI 未生成有效内容,请重试',
|
|
520
401
|
AI_NoSuchModelError: '指定的 AI 模型不存在',
|
|
521
402
|
AI_NoSuchProviderError: 'AI 提供商配置错误',
|