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
|
@@ -229,7 +229,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
async start(framework) {
|
|
232
|
-
//
|
|
232
|
+
// 异步加载所有 MCP 服务器,不阻塞框架启动
|
|
233
233
|
if (this._config.servers) {
|
|
234
234
|
for (const serverConfig of this._config.servers) {
|
|
235
235
|
if (serverConfig.enabled === false) {
|
|
@@ -238,7 +238,10 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
238
238
|
this._serverConfigs.set(serverConfig.name, { ...serverConfig });
|
|
239
239
|
continue;
|
|
240
240
|
}
|
|
241
|
-
await
|
|
241
|
+
// 异步连接,不 await
|
|
242
|
+
this.addServer(serverConfig).catch(err => {
|
|
243
|
+
log.error(`[MCP] Async server connection failed for ${serverConfig.name}:`, err.message);
|
|
244
|
+
});
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -307,7 +310,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
307
310
|
|
|
308
311
|
return {
|
|
309
312
|
success: true,
|
|
310
|
-
|
|
313
|
+
data: {
|
|
311
314
|
name: toolInfo.name,
|
|
312
315
|
description: toolInfo.description,
|
|
313
316
|
required,
|
|
@@ -405,7 +408,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
405
408
|
source: 'mcp',
|
|
406
409
|
});
|
|
407
410
|
|
|
408
|
-
return { success: true,
|
|
411
|
+
return { success: true, data: execResult };
|
|
409
412
|
} catch (err) {
|
|
410
413
|
log.error(` Tool '${tool}' failed:`, err.message);
|
|
411
414
|
|
|
@@ -450,7 +453,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
450
453
|
});
|
|
451
454
|
}
|
|
452
455
|
}
|
|
453
|
-
return { success: true, servers };
|
|
456
|
+
return { success: true, data: servers };
|
|
454
457
|
},
|
|
455
458
|
});
|
|
456
459
|
|
|
@@ -458,14 +461,14 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
458
461
|
framework.registerTool({
|
|
459
462
|
name: 'mcp_reload',
|
|
460
463
|
description:
|
|
461
|
-
'重新加载 MCP 服务器配置。当配置文件 .
|
|
464
|
+
'重新加载 MCP 服务器配置。当配置文件 .foliko/mcp_config.json 修改后使用此工具重载配置',
|
|
462
465
|
inputSchema: z.object({}),
|
|
463
466
|
execute: async () => {
|
|
464
467
|
log.info(' mcp_reload called');
|
|
465
468
|
try {
|
|
466
469
|
const fs = require('fs');
|
|
467
470
|
const path = require('path');
|
|
468
|
-
const configPath = path.resolve('.
|
|
471
|
+
const configPath = path.resolve('.foliko/mcp_config.json');
|
|
469
472
|
|
|
470
473
|
log.info(' Reading config from:', configPath);
|
|
471
474
|
if (!fs.existsSync(configPath)) {
|
|
@@ -481,8 +484,10 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
481
484
|
|
|
482
485
|
return {
|
|
483
486
|
success: true,
|
|
484
|
-
|
|
485
|
-
|
|
487
|
+
data: 'MCP 配置已重载',
|
|
488
|
+
metadata: {
|
|
489
|
+
servers: Object.keys(config.mcpServers || {}),
|
|
490
|
+
},
|
|
486
491
|
};
|
|
487
492
|
} catch (err) {
|
|
488
493
|
log.error(' Reload error:', err);
|
|
@@ -507,7 +512,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
507
512
|
if (enabled) {
|
|
508
513
|
// 启用服务器
|
|
509
514
|
if (clientInfo && clientInfo.enabled) {
|
|
510
|
-
return { success: true,
|
|
515
|
+
return { success: true, data: `MCP 服务器 '${server}' 已经是开启状态` };
|
|
511
516
|
}
|
|
512
517
|
// 如果服务器从未连接过,也检查配置中的 enabled 状态
|
|
513
518
|
if (!clientInfo && serverConfig && serverConfig.enabled === false) {
|
|
@@ -527,7 +532,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
527
532
|
// 保存 enabled 状态到配置文件
|
|
528
533
|
await this._saveMCPServerEnabled(server, true);
|
|
529
534
|
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
530
|
-
return { success: true,
|
|
535
|
+
return { success: true, data: `MCP 服务器 '${server}' 已开启` };
|
|
531
536
|
} else {
|
|
532
537
|
return { success: false, error: '服务器配置不存在,需要重载配置' };
|
|
533
538
|
}
|
|
@@ -555,10 +560,10 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
555
560
|
// 保存 enabled 状态到配置文件
|
|
556
561
|
await this._saveMCPServerEnabled(server, false);
|
|
557
562
|
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
558
|
-
return { success: true,
|
|
563
|
+
return { success: true, data: `MCP 服务器 '${server}' 已关闭` };
|
|
559
564
|
}
|
|
560
565
|
|
|
561
|
-
return { success: true,
|
|
566
|
+
return { success: true, data: `MCP 服务器 '${server}' 状态未变化` };
|
|
562
567
|
},
|
|
563
568
|
});
|
|
564
569
|
|
|
@@ -610,7 +615,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
610
615
|
*/
|
|
611
616
|
_refreshAgentMCPPrompt(agent) {
|
|
612
617
|
// 检查是否已刷新过(通过检查系统提示词是否已包含 MCP 描述)
|
|
613
|
-
const existingPrompt = agent._originalPrompt || '';
|
|
618
|
+
const existingPrompt = (typeof agent.getOriginalPrompt === 'function' ? agent.getOriginalPrompt() : agent._originalPrompt) || '';
|
|
614
619
|
if (existingPrompt.includes('【MCP Servers】')) {
|
|
615
620
|
return;
|
|
616
621
|
}
|
|
@@ -1263,7 +1268,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
1263
1268
|
async _saveMCPServerEnabled(serverName, enabled) {
|
|
1264
1269
|
const fs = require('fs');
|
|
1265
1270
|
const path = require('path');
|
|
1266
|
-
const configPath = path.resolve('.
|
|
1271
|
+
const configPath = path.resolve('.foliko/mcp_config.json');
|
|
1267
1272
|
|
|
1268
1273
|
try {
|
|
1269
1274
|
if (!fs.existsSync(configPath)) {
|
|
@@ -1293,7 +1298,13 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
1293
1298
|
// 断开所有 MCP 连接
|
|
1294
1299
|
for (const [name, clientInfo] of this._clients) {
|
|
1295
1300
|
try {
|
|
1296
|
-
|
|
1301
|
+
const closePromise = clientInfo.client.close?.() || clientInfo.client.destroy?.();
|
|
1302
|
+
if (closePromise && typeof closePromise.then === 'function') {
|
|
1303
|
+
await Promise.race([
|
|
1304
|
+
closePromise,
|
|
1305
|
+
new Promise((resolve) => setTimeout(() => resolve(), 3000)),
|
|
1306
|
+
]);
|
|
1307
|
+
}
|
|
1297
1308
|
} catch (e) {
|
|
1298
1309
|
// ignore
|
|
1299
1310
|
}
|
package/src/utils/chat-queue.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { EventEmitter } = require('./event-emitter');
|
|
2
2
|
const { cleanResponse } = require('./index');
|
|
3
3
|
const { logger } = require('./logger');
|
|
4
|
+
const { isNetworkError, calculateDelay, PRESETS } = require('./retry');
|
|
4
5
|
const log = logger.child('ChatQueue');
|
|
5
6
|
// ChatQueueManager.js
|
|
6
7
|
class ChatQueueManager extends EventEmitter {
|
|
@@ -118,10 +119,9 @@ class ChatQueueManager extends EventEmitter {
|
|
|
118
119
|
if (result.error) {
|
|
119
120
|
lastError = result.error;
|
|
120
121
|
if (attempt < this.retryAttempts && this.isRetryableError(lastError)) {
|
|
121
|
-
await this.sleep(
|
|
122
|
+
await this.sleep(calculateDelay(attempt, { baseDelay: this.retryDelay }));
|
|
122
123
|
continue;
|
|
123
124
|
}
|
|
124
|
-
// 重试耗尽,直接抛出 result.error
|
|
125
125
|
throw lastError;
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -130,7 +130,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
130
130
|
log.warn('[ChatQueue] executeWithRetry: ', attempt, 'error:', error.message);
|
|
131
131
|
lastError = error;
|
|
132
132
|
if (attempt < this.retryAttempts && this.isRetryableError(error)) {
|
|
133
|
-
await this.sleep(
|
|
133
|
+
await this.sleep(calculateDelay(attempt, { baseDelay: this.retryDelay }));
|
|
134
134
|
continue;
|
|
135
135
|
}
|
|
136
136
|
break;
|
|
@@ -138,9 +138,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// 将最后的错误转换为友好消息
|
|
141
|
-
const
|
|
142
|
-
const isRetryError = errName === 'AI_RetryError' || errName === 'RetryError';
|
|
143
|
-
const friendlyMessage = isRetryError
|
|
141
|
+
const friendlyMessage = isNetworkError(lastError)
|
|
144
142
|
? 'AI 服务暂时不可用,请稍后重试'
|
|
145
143
|
: (lastError?.message || String(lastError)).split('\n')[0];
|
|
146
144
|
|
|
@@ -175,9 +173,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
175
173
|
} catch (err) {
|
|
176
174
|
// SDK 直接抛出错误(没有通过 chunk 传递)
|
|
177
175
|
// 转换为友好错误消息
|
|
178
|
-
const
|
|
179
|
-
const isRetryError = errName === 'AI_RetryError' || errName === 'RetryError';
|
|
180
|
-
const friendlyMessage = isRetryError
|
|
176
|
+
const friendlyMessage = isNetworkError(err)
|
|
181
177
|
? 'AI 服务暂时不可用,请稍后重试'
|
|
182
178
|
: (err.message || err.toString()).split('\n')[0];
|
|
183
179
|
|
|
@@ -225,20 +221,13 @@ class ChatQueueManager extends EventEmitter {
|
|
|
225
221
|
/**
|
|
226
222
|
* 判断错误是否可重试
|
|
227
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* 判断错误是否可重试(委托给统一入口)
|
|
226
|
+
* @param {Error} error
|
|
227
|
+
* @returns {boolean}
|
|
228
|
+
*/
|
|
228
229
|
isRetryableError(error) {
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
message.includes('负载较高') ||
|
|
232
|
-
message.includes('timeout') ||
|
|
233
|
-
message.includes('network') ||
|
|
234
|
-
message.includes('429') ||
|
|
235
|
-
message.includes('500') ||
|
|
236
|
-
message.includes('502') ||
|
|
237
|
-
message.includes('503') ||
|
|
238
|
-
message.includes('rate limit') ||
|
|
239
|
-
error.name === 'AI_RetryError' ||
|
|
240
|
-
error.name === 'AI_APICallError'
|
|
241
|
-
);
|
|
230
|
+
return isNetworkError(error);
|
|
242
231
|
}
|
|
243
232
|
|
|
244
233
|
/**
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataSplitter — 大数据分拆引擎
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 将大文本内容按 token 估算分块
|
|
6
|
+
* 2. 为每个分块创建子 Agent 并行处理
|
|
7
|
+
* 3. 汇总各子 Agent 的结果
|
|
8
|
+
*
|
|
9
|
+
* 使用场景:
|
|
10
|
+
* - 读取超过 200KB 的大文件
|
|
11
|
+
* - 抓取内容过多的网页
|
|
12
|
+
* - 任何超过上下文限制的工具结果
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { logger } = require('./logger');
|
|
16
|
+
|
|
17
|
+
// 默认分块大小 (字符数,约 50K tokens)
|
|
18
|
+
const DEFAULT_CHUNK_SIZE = 60000;
|
|
19
|
+
|
|
20
|
+
// 默认 token 字节换算(中英混合粗略估算:1 token ≈ 2 chars)
|
|
21
|
+
const CHARS_PER_TOKEN = 2;
|
|
22
|
+
|
|
23
|
+
// 安全阈值:超过此字符数触发自动分拆(默认 100K tokens = 200K chars)
|
|
24
|
+
const DEFAULT_SAFE_THRESHOLD = 50000;
|
|
25
|
+
|
|
26
|
+
class DataSplitter {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} framework - Framework 实例
|
|
29
|
+
* @param {Object} [options]
|
|
30
|
+
* @param {number} [options.chunkSize=60000] - 每块字符数
|
|
31
|
+
* @param {number} [options.safeThreshold=50000] - 安全阈值字符数
|
|
32
|
+
* @param {number} [options.maxConcurrent=3] - 最大并行子Agent数
|
|
33
|
+
* @param {number} [options.maxRetries=2] - 子Agent最大重试次数
|
|
34
|
+
*/
|
|
35
|
+
constructor(framework, options = {}) {
|
|
36
|
+
this.framework = framework;
|
|
37
|
+
this.chunkSize = options.chunkSize || DEFAULT_CHUNK_SIZE;
|
|
38
|
+
this.safeThreshold = options.safeThreshold || DEFAULT_SAFE_THRESHOLD;
|
|
39
|
+
this.maxConcurrent = options.maxConcurrent || 3;
|
|
40
|
+
this.maxRetries = options.maxRetries || 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 判断内容是否需要分拆
|
|
45
|
+
* @param {string} content - 大文本内容
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
needsSplit(content) {
|
|
49
|
+
if (!content || typeof content !== 'string') return false;
|
|
50
|
+
return content.length > this.safeThreshold;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 获取内容统计信息
|
|
55
|
+
* @param {string} content
|
|
56
|
+
* @returns {{ chars: number, estimatedTokens: number, chunks: number }}
|
|
57
|
+
*/
|
|
58
|
+
getContentStats(content) {
|
|
59
|
+
const chars = content.length;
|
|
60
|
+
return {
|
|
61
|
+
chars,
|
|
62
|
+
estimatedTokens: Math.ceil(chars / CHARS_PER_TOKEN),
|
|
63
|
+
chunks: Math.ceil(chars / this.chunkSize),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 将大文本按分块大小拆分成多个片段
|
|
69
|
+
* 尝试在段落边界(\n\n)切分,保持语义完整性
|
|
70
|
+
* @param {string} content - 大文本内容
|
|
71
|
+
* @param {number} [chunkSize] - 每块最大字符数,默认使用实例配置
|
|
72
|
+
* @returns {Array<{ index: number, content: string, startOffset: number, endOffset: number }>}
|
|
73
|
+
*/
|
|
74
|
+
splitContent(content, chunkSize) {
|
|
75
|
+
chunkSize = chunkSize || this.chunkSize;
|
|
76
|
+
if (!content) return [];
|
|
77
|
+
|
|
78
|
+
const chunks = [];
|
|
79
|
+
let start = 0;
|
|
80
|
+
|
|
81
|
+
while (start < content.length) {
|
|
82
|
+
// 计算当前块的理论结束位置
|
|
83
|
+
let end = Math.min(start + chunkSize, content.length);
|
|
84
|
+
|
|
85
|
+
// 如果不是最后一块,尝试在段落边界切分
|
|
86
|
+
if (end < content.length) {
|
|
87
|
+
// 从 end 向前查找段落边界(双重换行)
|
|
88
|
+
const searchStart = Math.max(start, end - Math.floor(chunkSize * 0.3));
|
|
89
|
+
const segmentToSearch = content.slice(searchStart, end);
|
|
90
|
+
|
|
91
|
+
// 优先找 \n\n(段落边界)
|
|
92
|
+
let boundary = segmentToSearch.lastIndexOf('\n\n');
|
|
93
|
+
if (boundary > 0) {
|
|
94
|
+
end = searchStart + boundary + 1; // 把 \n\n 留给下一块开头会空行,更好
|
|
95
|
+
// 实际上保留 \n\n 在末尾更好
|
|
96
|
+
end = searchStart + boundary + 2;
|
|
97
|
+
} else {
|
|
98
|
+
// 其次找 \n(行边界)
|
|
99
|
+
boundary = segmentToSearch.lastIndexOf('\n');
|
|
100
|
+
if (boundary > 0) {
|
|
101
|
+
end = searchStart + boundary + 1;
|
|
102
|
+
} else {
|
|
103
|
+
// 最后找空格
|
|
104
|
+
boundary = segmentToSearch.lastIndexOf(' ');
|
|
105
|
+
if (boundary > 0) {
|
|
106
|
+
end = searchStart + boundary + 1;
|
|
107
|
+
}
|
|
108
|
+
// 找不到合适边界就在 chunkSize 处硬切
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const chunkContent = content.slice(start, end);
|
|
114
|
+
if (chunkContent.trim()) {
|
|
115
|
+
chunks.push({
|
|
116
|
+
index: chunks.length,
|
|
117
|
+
content: chunkContent,
|
|
118
|
+
startOffset: start,
|
|
119
|
+
endOffset: end,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
start = end;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return chunks;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 为每个分块创建子 Agent 并分配处理任务
|
|
131
|
+
* 子 Agent 并行处理,结果自动汇总
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} options
|
|
134
|
+
* @param {Array<{ index: number, content: string }>} options.chunks - 分块数据
|
|
135
|
+
* @param {string} options.taskDescription - 每个子 Agent 要执行的任务描述
|
|
136
|
+
* @param {string} [options.agentName] - 子 Agent 名称前缀
|
|
137
|
+
* @param {string} [options.agentRole] - 子 Agent 角色
|
|
138
|
+
* @param {number} [options.maxConcurrent] - 并行度
|
|
139
|
+
* @param {AbortSignal} [options.signal] - 取消信号
|
|
140
|
+
* @returns {Promise<{ success: boolean, results: Array, summary: string, errors: Array }>}
|
|
141
|
+
*/
|
|
142
|
+
async dispatchToSubAgents(options) {
|
|
143
|
+
const {
|
|
144
|
+
chunks,
|
|
145
|
+
taskDescription,
|
|
146
|
+
agentName = 'data-processor',
|
|
147
|
+
agentRole = '数据处理专家',
|
|
148
|
+
maxConcurrent = this.maxConcurrent,
|
|
149
|
+
signal,
|
|
150
|
+
} = options;
|
|
151
|
+
|
|
152
|
+
if (!chunks || chunks.length === 0) {
|
|
153
|
+
return { success: true, results: [], summary: '', errors: [] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalChunks = chunks.length;
|
|
157
|
+
const results = [];
|
|
158
|
+
const errors = [];
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
`[DataSplitter] 开始分拆处理: ${totalChunks} 块, 任务="${taskDescription.slice(0, 50)}..."`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// 并行执行(受 maxConcurrent 限制)
|
|
165
|
+
const queue = [...chunks];
|
|
166
|
+
const inFlight = new Set();
|
|
167
|
+
|
|
168
|
+
const processChunk = async (chunk) => {
|
|
169
|
+
const chunkIndex = chunk.index;
|
|
170
|
+
|
|
171
|
+
// 为当前块创建子 Agent
|
|
172
|
+
const subagent = this.framework.createSubAgent({
|
|
173
|
+
name: `${agentName}-chunk-${chunkIndex}`,
|
|
174
|
+
role: agentRole,
|
|
175
|
+
systemPrompt: `你是${agentRole},负责处理大数据中的第 ${chunkIndex + 1}/${totalChunks} 块。`,
|
|
176
|
+
maxRetries: this.maxRetries,
|
|
177
|
+
disableTools: true, // 分拆处理只做文本分析,不需要额外工具
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const taskMsg = `## 任务
|
|
182
|
+
${taskDescription}
|
|
183
|
+
|
|
184
|
+
## 待处理内容(第 ${chunkIndex + 1}/${totalChunks} 块)
|
|
185
|
+
\`\`\`
|
|
186
|
+
${chunk.content}
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
## 要求
|
|
190
|
+
- 只处理这块内容
|
|
191
|
+
- 返回简洁的结果
|
|
192
|
+
- 如果这块内容没有有用信息,返回"无有效信息"`;
|
|
193
|
+
|
|
194
|
+
const result = await subagent.chat(taskMsg, { signal });
|
|
195
|
+
results.push({
|
|
196
|
+
chunkIndex,
|
|
197
|
+
success: result.success,
|
|
198
|
+
message: result.message || '',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
logger.debug(
|
|
202
|
+
`[DataSplitter] 块 ${chunkIndex + 1}/${totalChunks} 处理完成: ` +
|
|
203
|
+
`${result.success ? '成功' : '失败'}`
|
|
204
|
+
);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
errors.push({ chunkIndex, error: err.message });
|
|
207
|
+
results.push({ chunkIndex, success: false, error: err.message });
|
|
208
|
+
logger.warn(
|
|
209
|
+
`[DataSplitter] 块 ${chunkIndex + 1}/${totalChunks} 处理失败: ${err.message}`
|
|
210
|
+
);
|
|
211
|
+
} finally {
|
|
212
|
+
subagent.destroy();
|
|
213
|
+
inFlight.delete(chunk);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// 并发控制:最多 maxConcurrent 个同时运行
|
|
218
|
+
while (queue.length > 0 || inFlight.size > 0) {
|
|
219
|
+
// 填充 inFlight 到 maxConcurrent
|
|
220
|
+
while (queue.length > 0 && inFlight.size < maxConcurrent) {
|
|
221
|
+
const chunk = queue.shift();
|
|
222
|
+
inFlight.add(chunk);
|
|
223
|
+
processChunk(chunk); // 不 await,并发执行
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 等待任意一个完成
|
|
227
|
+
if (inFlight.size > 0) {
|
|
228
|
+
await Promise.race(
|
|
229
|
+
[...inFlight].map(
|
|
230
|
+
(chunk) =>
|
|
231
|
+
new Promise((resolve) => {
|
|
232
|
+
const check = () => {
|
|
233
|
+
if (!inFlight.has(chunk)) resolve();
|
|
234
|
+
else setTimeout(check, 50);
|
|
235
|
+
};
|
|
236
|
+
check();
|
|
237
|
+
})
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 按块索引排序结果
|
|
244
|
+
results.sort((a, b) => a.chunkIndex - b.chunkIndex);
|
|
245
|
+
|
|
246
|
+
// 生成汇总摘要
|
|
247
|
+
const summary = this._buildSummary(results, errors, taskDescription);
|
|
248
|
+
|
|
249
|
+
logger.info(
|
|
250
|
+
`[DataSplitter] 分拆处理完成: ` +
|
|
251
|
+
`${results.filter((r) => r.success).length}/${totalChunks} 块成功, ` +
|
|
252
|
+
`${errors.length} 个错误`
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
success: errors.length === 0,
|
|
257
|
+
results,
|
|
258
|
+
summary,
|
|
259
|
+
errors,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 一步完成:检测 → 分拆 → 派发 → 汇总
|
|
265
|
+
* 如果内容不超过阈值,直接返回原文
|
|
266
|
+
*
|
|
267
|
+
* @param {string} content - 原始内容
|
|
268
|
+
* @param {string} taskDescription - 要执行的任务
|
|
269
|
+
* @param {Object} [options]
|
|
270
|
+
* @returns {Promise<string>} 汇总结果
|
|
271
|
+
*/
|
|
272
|
+
async processLargeContent(content, taskDescription, options = {}) {
|
|
273
|
+
if (!this.needsSplit(content)) {
|
|
274
|
+
return content; // 不需要分拆
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const stats = this.getContentStats(content);
|
|
278
|
+
const chunkSize = options.chunkSize || this.chunkSize;
|
|
279
|
+
|
|
280
|
+
logger.info(
|
|
281
|
+
`[DataSplitter] 自动分拆: ${stats.chars} 字符, ` +
|
|
282
|
+
`约 ${stats.estimatedTokens} tokens, ` +
|
|
283
|
+
`分 ${stats.chunks} 块处理`
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const chunks = this.splitContent(content, chunkSize);
|
|
287
|
+
const result = await this.dispatchToSubAgents({
|
|
288
|
+
chunks,
|
|
289
|
+
taskDescription,
|
|
290
|
+
maxConcurrent: options.maxConcurrent || this.maxConcurrent,
|
|
291
|
+
signal: options.signal,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return result.summary;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 构建汇总
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
_buildSummary(results, errors, taskDescription) {
|
|
302
|
+
const successfulResults = results.filter((r) => r.success && r.message && r.message !== '无有效信息');
|
|
303
|
+
const lineCounts = results.map((r) => r.message ? r.message.split('\n').length : 0);
|
|
304
|
+
|
|
305
|
+
const totalLines = lineCounts.reduce((a, b) => a + b, 0);
|
|
306
|
+
|
|
307
|
+
const lines = [
|
|
308
|
+
`## 大数据处理汇总`,
|
|
309
|
+
``,
|
|
310
|
+
`**任务**: ${taskDescription}`,
|
|
311
|
+
`**总块数**: ${results.length}`,
|
|
312
|
+
`**成功**: ${successfulResults.length} 块`,
|
|
313
|
+
`**失败**: ${errors.length} 块`,
|
|
314
|
+
``,
|
|
315
|
+
`### 各块处理结果`,
|
|
316
|
+
``,
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
for (const r of results) {
|
|
320
|
+
const status = r.success ? '✅' : '❌';
|
|
321
|
+
const msg = r.message || r.error || '无结果';
|
|
322
|
+
// 每块只取关键信息,避免重复
|
|
323
|
+
const trimmed = msg.split('\n').slice(0, 3).join('\n');
|
|
324
|
+
lines.push(`**块 ${r.chunkIndex + 1}** ${status}`);
|
|
325
|
+
lines.push(`> ${trimmed}`);
|
|
326
|
+
lines.push('');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (errors.length > 0) {
|
|
330
|
+
lines.push(`### 错误信息`);
|
|
331
|
+
lines.push('');
|
|
332
|
+
for (const e of errors) {
|
|
333
|
+
lines.push(`- 块 ${e.chunkIndex + 1}: ${e.error}`);
|
|
334
|
+
}
|
|
335
|
+
lines.push('');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
lines.push(`---`);
|
|
339
|
+
lines.push(`*共 ${results.length} 块, ${successfulResults.length} 块成功, 汇总 ${totalLines} 行*`);
|
|
340
|
+
|
|
341
|
+
return lines.join('\n');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = { DataSplitter };
|