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.
Files changed (157) hide show
  1. package/.claude/settings.local.json +19 -10
  2. package/.dockerignore +45 -45
  3. package/.env.example +56 -56
  4. package/CLAUDE.md +2 -2
  5. package/README.md +13 -13
  6. package/SPEC.md +3 -3
  7. package/cli/src/commands/chat.js +2 -20
  8. package/cli/src/commands/list.js +7 -6
  9. package/cli/src/commands/plugin.js +3 -2
  10. package/cli/src/daemon.js +2 -2
  11. package/cli/src/ui/chat-ui-old.js +15 -4
  12. package/cli/src/ui/chat-ui.js +236 -203
  13. package/cli/src/ui/footer-bar.js +20 -46
  14. package/cli/src/ui/message-bubble.js +24 -2
  15. package/cli/src/ui/status-bar.js +177 -0
  16. package/cli/src/utils/config.js +29 -0
  17. package/cli/src/utils/plugin-config.js +1 -1
  18. package/docker-compose.yml +33 -33
  19. package/docs/features.md +120 -120
  20. package/docs/quick-reference.md +160 -160
  21. package/docs/user-manual.md +1391 -1391
  22. package/examples/ambient-example.js +2 -2
  23. package/examples/bootstrap.js +3 -3
  24. package/examples/test-chat.js +1 -1
  25. package/examples/test-reload.js +1 -1
  26. package/examples/test-telegram.js +1 -1
  27. package/examples/test-tg-bot.js +1 -1
  28. package/examples/test-tg-simple.js +2 -2
  29. package/examples/test-tg.js +1 -1
  30. package/examples/test-think.js +1 -1
  31. package/examples/test-weixin-feishu.js +3 -3
  32. package/package.json +3 -1
  33. package/plugins/ambient-agent/index.js +1 -1
  34. package/plugins/audit-plugin.js +84 -29
  35. package/plugins/coordinator-plugin.js +14 -12
  36. package/plugins/data-splitter-plugin.js +323 -0
  37. package/plugins/default-plugins.js +23 -12
  38. package/plugins/email/index.js +1 -1
  39. package/plugins/extension-executor-plugin.js +87 -9
  40. package/plugins/feishu-plugin.js +118 -16
  41. package/plugins/file-system-plugin.js +68 -50
  42. package/plugins/gate-trading.js +10 -10
  43. package/plugins/install-plugin.js +7 -7
  44. package/plugins/memory-plugin.js +9 -12
  45. package/plugins/plugin-manager-plugin.js +12 -14
  46. package/plugins/python-executor-plugin.js +1 -1
  47. package/plugins/python-plugin-loader.js +1 -1
  48. package/plugins/qq-plugin.js +151 -24
  49. package/plugins/rules-plugin.js +8 -8
  50. package/plugins/scheduler-plugin.js +24 -20
  51. package/plugins/session-plugin.js +313 -397
  52. package/plugins/storage-plugin.js +235 -175
  53. package/plugins/subagent-plugin.js +17 -13
  54. package/plugins/telegram-plugin.js +116 -17
  55. package/plugins/think-plugin.js +64 -60
  56. package/plugins/tools-plugin.js +8 -8
  57. package/plugins/web-plugin.js +2 -2
  58. package/plugins/weixin-plugin.js +107 -24
  59. package/skills/find-skills/AGENTS.md +2 -2
  60. package/skills/find-skills/SKILL.md +133 -133
  61. package/skills/foliko-dev/AGENTS.md +236 -236
  62. package/skills/foliko-dev/SKILL.md +19 -19
  63. package/skills/mcp-usage/SKILL.md +200 -200
  64. package/skills/plugin-guide/SKILL.md +4 -4
  65. package/skills/python-plugin-dev/SKILL.md +5 -5
  66. package/skills/skill-guide/SKILL.md +104 -6
  67. package/skills/subagent-guide/SKILL.md +237 -237
  68. package/skills/workflow-guide/SKILL.md +646 -646
  69. package/src/capabilities/skill-manager.js +124 -17
  70. package/src/capabilities/workflow-engine.js +3 -3
  71. package/src/core/agent-chat.js +72 -26
  72. package/src/core/agent.js +17 -27
  73. package/src/core/branch-summary-auto.js +206 -0
  74. package/src/core/chat-session.js +45 -169
  75. package/src/core/command-registry.js +200 -0
  76. package/src/core/constants.js +198 -0
  77. package/src/core/context-compressor.js +702 -326
  78. package/src/core/context-manager.js +0 -1
  79. package/src/core/enhanced-context-compressor.js +210 -0
  80. package/src/core/framework.js +260 -84
  81. package/src/core/jsonl-storage.js +253 -0
  82. package/src/core/plugin-base.js +7 -5
  83. package/src/core/plugin-manager.js +15 -10
  84. package/src/core/provider-registry.js +159 -0
  85. package/src/core/provider.js +2 -0
  86. package/src/core/session-entry.js +225 -0
  87. package/src/core/session-manager.js +701 -0
  88. package/src/core/storage-manager.js +494 -0
  89. package/src/core/sub-agent-config.js +1 -1
  90. package/src/core/subagent.js +16 -135
  91. package/src/core/token-counter.js +177 -58
  92. package/src/core/tool-executor.js +2 -70
  93. package/src/core/ui-extension-context.js +174 -0
  94. package/src/executors/mcp-executor.js +27 -16
  95. package/src/utils/chat-queue.js +11 -22
  96. package/src/utils/data-splitter.js +345 -0
  97. package/src/utils/logger.js +152 -180
  98. package/src/utils/message-validator.js +283 -0
  99. package/src/utils/plugin-helpers.js +2 -2
  100. package/src/utils/retry.js +168 -22
  101. package/website_v2/docs/api.html +1 -1
  102. package/website_v2/docs/configuration.html +2 -2
  103. package/website_v2/docs/plugin-development.html +4 -4
  104. package/website_v2/docs/project-structure.html +2 -2
  105. package/website_v2/docs/skill-development.html +2 -2
  106. package/website_v2/index.html +1 -1
  107. package/website_v2/styles/animations.css +7 -7
  108. package/.agent/agents/backend-dev.md +0 -102
  109. package/.agent/agents/data-analyst.md +0 -117
  110. package/.agent/agents/devops.md +0 -115
  111. package/.agent/agents/frontend-dev.md +0 -94
  112. package/.agent/agents/network-requester.md +0 -44
  113. package/.agent/agents/poster-designer.md +0 -52
  114. package/.agent/agents/product-manager.md +0 -85
  115. package/.agent/agents/qa-engineer.md +0 -100
  116. package/.agent/agents/security-engineer.md +0 -99
  117. package/.agent/agents/team-lead.md +0 -137
  118. package/.agent/agents/ui-designer.md +0 -116
  119. package/.agent/data/default.json +0 -58
  120. package/.agent/data/email/processed-emails.json +0 -1
  121. package/.agent/data/plugins-state.json +0 -199
  122. package/.agent/data/scheduler/tasks.json +0 -1
  123. package/.agent/data/web/web-config.json +0 -5
  124. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  125. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  126. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  127. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  128. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  129. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  130. package/.agent/data/weixin.json +0 -6
  131. package/.agent/mcp_config.json +0 -14
  132. package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
  133. package/.agent/package.json +0 -8
  134. package/.agent/plugins/marknative/README.md +0 -134
  135. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  136. package/.agent/plugins/marknative/fonts.zip +0 -0
  137. package/.agent/plugins/marknative/index.js +0 -256
  138. package/.agent/plugins/marknative/package.json +0 -12
  139. package/.agent/plugins/test-plugin.py +0 -99
  140. package/.agent/plugins.json +0 -14
  141. package/.agent/python-scripts/test_sample.py +0 -24
  142. package/.agent/sessions/cli_default.json +0 -247
  143. package/.agent/skills/agent-browser/SKILL.md +0 -311
  144. package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
  145. package/.agent/skills/sysinfo/SKILL.md +0 -38
  146. package/.agent/skills/sysinfo/system-info.sh +0 -130
  147. package/.agent/skills/workflow/SKILL.md +0 -324
  148. package/.agent/test-agent.js +0 -35
  149. package/.agent/weixin.json +0 -6
  150. package/.agent/workflows/email-digest.json +0 -50
  151. package/.agent/workflows/file-backup.json +0 -21
  152. package/.agent/workflows/get-ip-notify.json +0 -32
  153. package/.agent/workflows/news-aggregator.json +0 -93
  154. package/.agent/workflows/news-dashboard-v2.json +0 -94
  155. package/.agent/workflows/notification-batch.json +0 -32
  156. package/src/core/session-context.js +0 -346
  157. 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
- // 先连接所有 MCP 服务器
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 this.addServer(serverConfig);
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
- result: {
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, result: execResult };
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 服务器配置。当配置文件 .agent/mcp_config.json 修改后使用此工具重载配置',
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('.agent/mcp_config.json');
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
- message: 'MCP 配置已重载',
485
- servers: Object.keys(config.mcpServers || {}),
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, message: `MCP 服务器 '${server}' 已经是开启状态` };
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, message: `MCP 服务器 '${server}' 已开启` };
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, message: `MCP 服务器 '${server}' 已关闭` };
563
+ return { success: true, data: `MCP 服务器 '${server}' 已关闭` };
559
564
  }
560
565
 
561
- return { success: true, message: `MCP 服务器 '${server}' 状态未变化` };
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('.agent/mcp_config.json');
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
- (await clientInfo.client.close?.()) || clientInfo.client.destroy?.();
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
  }
@@ -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(this.retryDelay * Math.pow(2, attempt - 1));
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(this.retryDelay * Math.pow(2, attempt - 1));
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 errName = lastError?.name || '';
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 errName = err?.name || '';
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
- const message = error.message || String(error);
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 };