foliko 2.0.2 → 2.0.4

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 (80) hide show
  1. package/README.md +6 -6
  2. package/docs/public-api.md +91 -0
  3. package/docs/system-prompt.md +219 -0
  4. package/docs/usage.md +6 -6
  5. package/package.json +1 -1
  6. package/plugins/ambient/ExplorerLoop.js +1 -0
  7. package/plugins/ambient/index.js +1 -0
  8. package/plugins/core/coordinator/index.js +10 -5
  9. package/plugins/core/default/bootstrap.js +21 -3
  10. package/plugins/core/default/config.js +12 -3
  11. package/plugins/core/python-loader/index.js +3 -3
  12. package/plugins/core/scheduler/index.js +26 -2
  13. package/plugins/core/skill-manager/index.js +198 -151
  14. package/plugins/core/sub-agent/index.js +34 -15
  15. package/plugins/core/think/index.js +1 -0
  16. package/plugins/core/workflow/index.js +5 -4
  17. package/plugins/executors/data-splitter/index.js +14 -1
  18. package/plugins/executors/extension/index.js +52 -38
  19. package/plugins/executors/python/index.js +3 -3
  20. package/plugins/executors/shell/index.js +6 -4
  21. package/plugins/io/web/index.js +2 -1
  22. package/plugins/memory/index.js +29 -74
  23. package/plugins/messaging/email/handlers.js +1 -19
  24. package/plugins/messaging/email/monitor.js +2 -17
  25. package/plugins/messaging/email/reply.js +2 -1
  26. package/plugins/messaging/email/utils.js +20 -1
  27. package/plugins/messaging/feishu/index.js +1 -1
  28. package/plugins/messaging/qq/index.js +1 -1
  29. package/plugins/messaging/telegram/index.js +1 -1
  30. package/plugins/messaging/weixin/index.js +1 -1
  31. package/plugins/plugin-manager/index.js +1 -33
  32. package/plugins/tools/index.js +14 -1
  33. package/skills/plugins/SKILL.md +316 -0
  34. package/skills/skill-guide/SKILL.md +5 -5
  35. package/skills/{subagent-guide → subagents}/SKILL.md +1 -1
  36. package/skills/{workflow-guide → workflows}/SKILL.md +3 -3
  37. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/DEBUGGING.md +197 -197
  38. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/SKILL.md +391 -391
  39. package/src/agent/base.js +5 -20
  40. package/src/agent/chat.js +67 -5
  41. package/src/agent/index.js +20 -8
  42. package/src/agent/main.js +100 -23
  43. package/src/agent/prompt-registry.js +296 -0
  44. package/src/agent/sub-config.js +1 -78
  45. package/src/agent/sub.js +20 -25
  46. package/src/cli/commands/plugin.js +1 -60
  47. package/src/cli/ui/chat-ui-old.js +1 -1
  48. package/src/cli/ui/chat-ui.js +21 -17
  49. package/src/cli/ui/components/agent-mention-provider.js +1 -1
  50. package/src/common/constants.js +42 -0
  51. package/src/common/id.js +13 -0
  52. package/src/common/queue.js +3 -3
  53. package/src/context/compressor.js +17 -9
  54. package/src/framework/framework.js +218 -0
  55. package/src/framework/index.js +1 -2
  56. package/src/framework/lifecycle.js +102 -1
  57. package/src/framework/loader.js +1 -55
  58. package/src/index.js +11 -2
  59. package/src/plugin/base.js +69 -55
  60. package/src/plugin/loader.js +1 -57
  61. package/src/session/chat.js +1 -1
  62. package/src/session/entry.js +1 -11
  63. package/src/storage/manager.js +1 -12
  64. package/src/tool/executor.js +3 -2
  65. package/src/tool/router.js +5 -5
  66. package/src/utils/data-splitter.js +2 -1
  67. package/src/utils/index.js +150 -0
  68. package/src/utils/message-validator.js +21 -17
  69. package/src/utils/plugin-helpers.js +19 -5
  70. package/subagent.md +2 -2
  71. package/tests/core/plugin-prompts.test.js +219 -0
  72. package/tests/core/prompt-registry.test.js +209 -0
  73. package/src/cli/utils/plugin-config.js +0 -50
  74. package/src/config/plugin-config.js +0 -50
  75. /package/skills/{ambient-agent → ambient}/SKILL.md +0 -0
  76. /package/skills/{foliko-dev → foliko}/AGENTS.md +0 -0
  77. /package/skills/{foliko-dev → foliko}/SKILL.md +0 -0
  78. /package/skills/{mcp-usage → mcp}/SKILL.md +0 -0
  79. /package/skills/{plugin-guide → plugins-guide}/SKILL.md +0 -0
  80. /package/skills/{python-plugin-dev → python}/SKILL.md +0 -0
@@ -20,10 +20,30 @@ class Plugin {
20
20
 
21
21
  _framework = null;
22
22
  _subAgents = [];
23
- _promptParts = [];
24
- _deferredPromptParts = null;
25
23
  tools = {};
26
24
 
25
+ /**
26
+ * 声明式 prompt parts(推荐使用,替代 start() 中的 registerPromptPart 调用)
27
+ *
28
+ * 用法:
29
+ * prompts = [
30
+ * {
31
+ * name: 'my-part',
32
+ * slot: 'BEHAVIOR', // 可选,PROMPT_SLOT 中的一个;默认 CUSTOM
33
+ * order: 50, // 可选,覆盖 slot 默认 order
34
+ * description: '描述', // 可选
35
+ * provider() { // 'this' 绑定到 plugin 实例;不要用箭头函数
36
+ * return this._buildSomething();
37
+ * },
38
+ * },
39
+ * ];
40
+ *
41
+ * 生命周期:
42
+ * - Plugin.start() 时自动注册到 framework.prompts
43
+ * - Plugin.uninstall() / Plugin.reload() 时自动清理(clearOwner(this.name))
44
+ */
45
+ prompts = [];
46
+
27
47
  install(framework) {
28
48
  this._framework = framework;
29
49
  return this;
@@ -56,9 +76,51 @@ class Plugin {
56
76
 
57
77
  start(framework) {
58
78
  this._autoRegisterSubAgents();
79
+ this._autoRegisterPrompts();
59
80
  return this;
60
81
  }
61
82
 
83
+ /**
84
+ * 自动注册 this.prompts 声明的所有 part 到 framework.prompts
85
+ * - 失败一个不影响其他
86
+ * - 'this' 在 provider 中绑定到 plugin 实例(必须用普通 function,箭头函数的 this 是词法的)
87
+ */
88
+ _autoRegisterPrompts() {
89
+ const reg = this._framework?.prompts;
90
+ if (!reg) return;
91
+ if (!Array.isArray(this.prompts) || this.prompts.length === 0) return;
92
+
93
+ for (const entry of this.prompts) {
94
+ if (!entry || !entry.name || typeof entry.provider !== 'function') {
95
+ log.warn(`[${this.name}] Invalid prompt entry: missing name or provider`);
96
+ continue;
97
+ }
98
+ try {
99
+ // bind 让 provider 中的 this 指向 plugin 实例
100
+ const boundProvider = entry.provider.bind(this);
101
+ reg.register(this.name, entry.name, boundProvider, {
102
+ slot: entry.slot,
103
+ order: entry.order,
104
+ description: entry.description,
105
+ });
106
+ } catch (err) {
107
+ log.error(`[${this.name}] Failed to register prompt part "${entry.name}":`, err.message);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 清理当前 plugin 在 framework.prompts 中注册的所有 part
114
+ */
115
+ _cleanupPrompts() {
116
+ const reg = this._framework?.prompts;
117
+ if (!reg) return;
118
+ const removed = reg.clearOwner(this.name);
119
+ if (removed > 0) {
120
+ log.debug(`[${this.name}] cleared ${removed} prompt part(s)`);
121
+ }
122
+ }
123
+
62
124
  _autoRegisterSubAgents() {
63
125
  const agentsToRegister = this._deferredSubAgents || this.agents;
64
126
  if (!agentsToRegister || !Array.isArray(agentsToRegister) || agentsToRegister.length === 0) {
@@ -139,15 +201,10 @@ class Plugin {
139
201
  reload(framework) {
140
202
  this._framework = framework;
141
203
  this._deferredSubAgents = null;
142
- this._deferredPromptParts = null;
143
204
 
144
- for (const part of this._promptParts) {
145
- try {
146
- framework._mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
147
- } catch (err) {
148
- log.error(` Failed to re-register prompt part ${part.name}:`, err.message);
149
- }
150
- }
205
+ // 先清理旧的,再重新注册 declarative prompts
206
+ this._cleanupPrompts();
207
+ this._autoRegisterPrompts();
151
208
 
152
209
  this._autoRegisterSubAgents();
153
210
  }
@@ -157,15 +214,8 @@ class Plugin {
157
214
  }
158
215
 
159
216
  uninstall(framework) {
160
- for (const part of this._promptParts) {
161
- try {
162
- framework._mainAgent?.unregisterPromptPart(part.name);
163
- } catch (err) {
164
- // ignore
165
- }
166
- }
167
- this._promptParts = [];
168
- this._deferredPromptParts = null;
217
+ // 清理 framework.prompts 中该插件注册的所有 part
218
+ this._cleanupPrompts();
169
219
 
170
220
  for (const plugin of this._subAgents) {
171
221
  try {
@@ -178,42 +228,6 @@ class Plugin {
178
228
  this._framework = null;
179
229
  }
180
230
 
181
- registerPromptPart(name, priority = 100, provider) {
182
- if (!this._framework) return;
183
-
184
- this._promptParts.push({ name, priority, provider });
185
-
186
- const mainAgent = this._framework.getMainAgent();
187
- if (mainAgent) {
188
- mainAgent.registerPromptPart(name, priority, provider);
189
- this._framework.syncPromptPartsToSubagents();
190
- return;
191
- }
192
-
193
- if (!this._deferredPromptParts) {
194
- this._deferredPromptParts = [];
195
- }
196
- this._deferredPromptParts.push({ name, priority, provider });
197
-
198
- const alreadyListening = this._framework._events?.['framework:ready']?.length > 0;
199
- this._framework.once('framework:ready', () => {
200
- setTimeout(() => {
201
- if (this._deferredPromptParts) {
202
- for (const part of this._deferredPromptParts) {
203
- try {
204
- const mainAgent = this._framework.getMainAgent();
205
- mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
206
- } catch (err) {
207
- log.error(` Failed to register prompt part ${part.name}:`, err.message);
208
- }
209
- }
210
- this._deferredPromptParts = null;
211
- this._framework.syncPromptPartsToSubagents();
212
- }
213
- }, 100);
214
- });
215
- }
216
-
217
231
  getInfo() {
218
232
  return {
219
233
  name: this.name,
@@ -5,66 +5,10 @@
5
5
  */
6
6
 
7
7
  const path = require('path');
8
- const fs = require('fs');
8
+ const { resolvePluginPath, scanPluginNames } = require('../utils/plugin-helpers');
9
9
  const { logger } = require('../common/logger');
10
10
  const log = logger.child('plugin-loader');
11
11
 
12
- /**
13
- * Resolve plugin path from name
14
- * @param {string} pluginName
15
- * @param {Object} options
16
- * @returns {string|null}
17
- */
18
- function resolvePluginPath(pluginName, options = {}) {
19
- const { pluginDirs = [], searchPaths = [] } = options;
20
-
21
- // Check explicit paths first
22
- const allPaths = [...pluginDirs, ...searchPaths];
23
- for (const dir of allPaths) {
24
- const fullPath = path.resolve(dir, pluginName);
25
- if (fs.existsSync(fullPath)) {
26
- return fullPath;
27
- }
28
- const indexJs = path.join(fullPath, 'index.js');
29
- if (fs.existsSync(indexJs)) {
30
- return fullPath;
31
- }
32
- }
33
-
34
- // Check node_modules
35
- try {
36
- const resolved = require.resolve(pluginName);
37
- return resolved;
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * Scan directory for plugin names
45
- * @param {string} dir
46
- * @returns {string[]}
47
- */
48
- function scanPluginNames(dir) {
49
- if (!fs.existsSync(dir)) return [];
50
-
51
- try {
52
- return fs.readdirSync(dir)
53
- .filter(f => {
54
- const fullPath = path.join(dir, f);
55
- const stat = fs.statSync(fullPath);
56
- if (stat.isDirectory()) {
57
- return fs.existsSync(path.join(fullPath, 'index.js'));
58
- }
59
- return f.endsWith('.js');
60
- })
61
- .map(f => f.replace(/\.js$/, ''));
62
- } catch (err) {
63
- log.warn(`Failed to scan plugin directory ${dir}: ${err.message}`);
64
- return [];
65
- }
66
- }
67
-
68
12
  /**
69
13
  * Load a plugin from a file path
70
14
  * @param {string} pluginPath
@@ -225,7 +225,7 @@ class ChatSession extends EventEmitter {
225
225
  messageStore.messages = messages;
226
226
  messageStore.historyLoaded = true;
227
227
  messageStore._hasMoreHistory = allMessages.length > MAX_INITIAL_LOAD;
228
- logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
228
+ //logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
229
229
  }
230
230
  }
231
231
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { randomUUID } = require('crypto');
7
+ const { generateEntryId } = require('../common/id');
7
8
 
8
9
  /**
9
10
  * Entry types for session tree entries
@@ -21,17 +22,6 @@ const EntryTypes = {
21
22
  LEAF: 'leaf'
22
23
  };
23
24
 
24
- /**
25
- * Generate a unique short ID (8 hex chars, collision-checked)
26
- */
27
- function generateEntryId(byId) {
28
- for (let i = 0; i < 100; i++) {
29
- const id = randomUUID().slice(0, 8);
30
- if (!byId.has(id)) return id;
31
- }
32
- return randomUUID();
33
- }
34
-
35
25
  /**
36
26
  * Generate UUID v7 for session IDs
37
27
  */
@@ -5,18 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
- const { randomUUID } = require('crypto');
9
-
10
- /**
11
- * Generate unique entry ID
12
- */
13
- function generateEntryId(existingIds) {
14
- for (let i = 0; i < 100; i++) {
15
- const id = randomUUID().slice(0, 8);
16
- if (!existingIds.has(id)) return id;
17
- }
18
- return randomUUID();
19
- }
8
+ const { generateEntryId } = require('../common/id');
20
9
 
21
10
  class StorageEntry {
22
11
  constructor(key, value, timestamp) {
@@ -35,7 +35,7 @@ class ToolExecutor extends EventEmitter {
35
35
  return;
36
36
  }
37
37
  this._tools.set(tool.name, tool);
38
- logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
38
+ //logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
39
39
  }
40
40
 
41
41
  unregisterTool(name) {
@@ -71,7 +71,8 @@ class ToolExecutor extends EventEmitter {
71
71
  }
72
72
 
73
73
  validateToolCalls(messages) {
74
- return validateToolCalls(messages);
74
+ const registeredTools = new Set(this._tools.keys());
75
+ return validateToolCalls(messages, { availableTools: registeredTools });
75
76
  }
76
77
 
77
78
  getStats() {
@@ -23,7 +23,7 @@ const INTENT_PATTERNS = {
23
23
  '写代码', '开发', '编程', '创建插件', '编写函数',
24
24
  'code', 'develop', 'plugin', '编程',
25
25
  ],
26
- tools: ['loadSkill', 'read_file', 'write_file', 'execute_command', 'shell'],
26
+ tools: ['skill_load', 'read_file', 'write_file', 'execute_command', 'shell_exec'],
27
27
  },
28
28
  system_info: {
29
29
  keywords: [
@@ -34,19 +34,19 @@ const INTENT_PATTERNS = {
34
34
  },
35
35
  data_analysis: {
36
36
  keywords: ['分析', '数据', '统计', '查询', 'analyze', 'data', 'statistics', 'query'],
37
- tools: ['execute_command', 'python', 'search_file'],
37
+ tools: ['execute_command', 'py_execute', 'search_file'],
38
38
  },
39
39
  plugin_management: {
40
40
  keywords: ['插件', '重载', '加载插件', 'plugin', 'reload', 'load plugin'],
41
- tools: ['reload_plugins', 'list_plugins', 'loadSkill'],
41
+ tools: ['reload_plugins', 'list_plugins', 'skill_load'],
42
42
  },
43
43
  shell_command: {
44
44
  keywords: ['命令', '终端', 'shell', '执行', 'command', 'terminal', 'bash', 'cmd'],
45
- tools: ['shell', 'powershell', 'execute_command'],
45
+ tools: ['shell_exec', 'powershell', 'execute_command'],
46
46
  },
47
47
  skill_usage: {
48
48
  keywords: ['技能', 'skill', '使用技能', '加载技能'],
49
- tools: ['loadSkill'],
49
+ tools: ['skill_load'],
50
50
  },
51
51
  scheduling: {
52
52
  keywords: [
@@ -168,13 +168,14 @@ class DataSplitter {
168
168
  const processChunk = async (chunk) => {
169
169
  const chunkIndex = chunk.index;
170
170
 
171
- // 为当前块创建子 Agent
171
+ // 为当前块创建子 Agent(隐藏,不在指令系统中显示)
172
172
  const subagent = this.framework.createSubAgent({
173
173
  name: `${agentName}-chunk-${chunkIndex}`,
174
174
  role: agentRole,
175
175
  systemPrompt: `你是${agentRole},负责处理大数据中的第 ${chunkIndex + 1}/${totalChunks} 块。`,
176
176
  maxRetries: this.maxRetries,
177
177
  disableTools: true, // 分拆处理只做文本分析,不需要额外工具
178
+ hidden: true, // 隐藏子Agent,不在指令系统中显示
178
179
  });
179
180
 
180
181
  try {
@@ -47,6 +47,80 @@ const {
47
47
  combineBoundaries,
48
48
  } = require('../common/errors');
49
49
 
50
+ /**
51
+ * 解析 YAML frontmatter
52
+ * @param {string} content
53
+ * @returns {Object|null}
54
+ */
55
+ function parseFrontmatter(content) {
56
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
57
+ if (!match) return null;
58
+
59
+ const frontmatter = {};
60
+ const lines = match[1].split('\n');
61
+ let currentKey = null;
62
+
63
+ for (const line of lines) {
64
+ // 处理 metadata 嵌套
65
+ if (currentKey === 'metadata') {
66
+ if (line.match(/^ {2}[a-zA-Z]/)) {
67
+ const colonIndex = line.indexOf(':');
68
+ if (colonIndex > 0) {
69
+ const key = line.substring(2, colonIndex).trim();
70
+ const value = line
71
+ .substring(colonIndex + 1)
72
+ .trim()
73
+ .replace(/^["']|["']$/g, '');
74
+ frontmatter[currentKey][key] = value;
75
+ continue;
76
+ }
77
+ } else if (line.match(/^[a-zA-Z]/)) {
78
+ currentKey = null;
79
+ continue;
80
+ }
81
+ }
82
+
83
+ const colonIndex = line.indexOf(':');
84
+ if (colonIndex === -1) continue;
85
+
86
+ const key = line.substring(0, colonIndex).trim();
87
+ let value = line.substring(colonIndex + 1).trim();
88
+
89
+ // 移除引号
90
+ if (
91
+ (value.startsWith('"') && value.endsWith('"')) ||
92
+ (value.startsWith("'") && value.endsWith("'"))
93
+ ) {
94
+ value = value.slice(1, -1);
95
+ }
96
+
97
+ if (key === 'metadata') {
98
+ currentKey = 'metadata';
99
+ frontmatter.metadata = {};
100
+ } else if (key === 'allowed-tools' || key === 'skills' || key === 'tools' || key === 'tags') {
101
+ // 去除首尾空白和方括号
102
+ value = value.trim().replace(/^\[|\]$/g, '');
103
+ frontmatter[key] = value
104
+ .split(',')
105
+ .map((v) => v.trim().replace(/^["']|["']$/g, ''))
106
+ .filter(Boolean);
107
+ } else {
108
+ frontmatter[key] = value;
109
+ }
110
+ }
111
+
112
+ return frontmatter;
113
+ }
114
+
115
+ /**
116
+ * 移除 frontmatter,只保留正文内容
117
+ * @param {string} content
118
+ * @returns {string}
119
+ */
120
+ function stripFrontmatter(content) {
121
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
122
+ }
123
+
50
124
  /**
51
125
  * 清理 LLM 回复中的思考标记
52
126
  * @param {string} text
@@ -530,6 +604,8 @@ module.exports = {
530
604
  // 工具函数
531
605
  cleanResponse,
532
606
  safeJsonParse,
607
+ parseFrontmatter,
608
+ stripFrontmatter,
533
609
  deepClone,
534
610
  debounce,
535
611
  throttle,
@@ -539,4 +615,78 @@ module.exports = {
539
615
 
540
616
  // Edit/Diff
541
617
  editDiff: require('../common/diff'),
618
+
619
+ // 插件发布/安装相关
620
+ DEFAULT_REPO: 'https://github.com/chnak/foliko-plugins.git',
621
+
622
+ IGNORE_PATTERNS: [
623
+ 'node_modules',
624
+ '.git',
625
+ '.env',
626
+ '.DS_Store',
627
+ 'Thumbs.db',
628
+ '*.log',
629
+ '*.lock',
630
+ '*.bak',
631
+ '.claude',
632
+ '.foliko',
633
+ 'examples',
634
+ 'dist',
635
+ 'build',
636
+ 'coverage',
637
+ 'tests',
638
+ '__tests__',
639
+ '*.test.js',
640
+ '*.spec.js',
641
+ 'package-lock.json',
642
+ 'yarn.lock',
643
+ 'pnpm-lock.yaml',
644
+ ],
645
+
646
+ shouldIgnore(name) {
647
+ return this.IGNORE_PATTERNS.some((pattern) => {
648
+ if (pattern.startsWith('*.')) {
649
+ return name.endsWith(pattern.slice(1));
650
+ }
651
+ return name === pattern;
652
+ });
653
+ },
654
+
655
+ copyDirRecursive(src, dest) {
656
+ const fs = require('fs');
657
+ if (!fs.existsSync(src)) return;
658
+ fs.mkdirSync(dest, { recursive: true });
659
+ const entries = fs.readdirSync(src, { withFileTypes: true });
660
+ for (const entry of entries) {
661
+ const srcPath = path.join(src, entry.name);
662
+ const destPath = path.join(dest, entry.name);
663
+ if (this.shouldIgnore(entry.name)) continue;
664
+ if (entry.isDirectory()) {
665
+ this.copyDirRecursive(srcPath, destPath);
666
+ } else {
667
+ fs.copyFileSync(srcPath, destPath);
668
+ }
669
+ }
670
+ },
671
+
672
+ parseGitUrl(url) {
673
+ const patterns = [
674
+ /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
675
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
676
+ ];
677
+ for (const pattern of patterns) {
678
+ const match = url.match(pattern);
679
+ if (match) return { owner: match[1], repo: match[2] };
680
+ }
681
+ return null;
682
+ },
683
+
684
+ gitCommand(args, cwd) {
685
+ const { execSync } = require('child_process');
686
+ try {
687
+ return execSync(`git ${args}`, { cwd, encoding: 'utf-8', stdio: 'pipe' });
688
+ } catch (err) {
689
+ return err.stdout || err.stderr || '';
690
+ }
691
+ },
542
692
  };
@@ -53,24 +53,28 @@ function validateMessagesPairing(messages, options = {}) {
53
53
  let removedToolResultCount = 0;
54
54
 
55
55
  for (const msg of messages) {
56
- if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
56
+ if (msg.role !== 'tool') continue;
57
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;
58
+ if (Array.isArray(msg.content)) {
59
+ const originalLength = msg.content.length;
60
+ msg.content = msg.content.filter((item) => {
61
+ if (
62
+ item &&
63
+ (item.type === 'tool-result' || item.type === 'tool_result') &&
64
+ item.toolCallId &&
65
+ !validToolCallIds.has(item.toolCallId)
66
+ ) {
67
+ removedToolResultCount++;
68
+ return false;
69
+ }
70
+ return true;
71
+ });
72
+ // content 全被删除了,标记整个消息待删除
73
+ if (msg.content.length === 0 && originalLength > 0) {
74
+ msg._orphaned = true;
68
75
  }
69
- return true;
70
- });
71
-
72
- // content 全被删除了,标记整个消息待删除
73
- if (msg.content.length === 0 && originalLength > 0) {
76
+ } else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
77
+ // OpenAI 兼容格式:整条消息无对应 tool_calls,标记为 orphaned
74
78
  msg._orphaned = true;
75
79
  }
76
80
  }
@@ -280,4 +284,4 @@ module.exports = {
280
284
  validateToolCalls,
281
285
  validateAll,
282
286
  filterPairedMessages,
283
- };
287
+ };
@@ -70,15 +70,20 @@ function resolvePluginPath(pluginsDir, name, options = {}) {
70
70
  }
71
71
 
72
72
  /**
73
- * 扫描插件目录,返回所有插件名称
73
+ * 扫描插件目录,返回所有插件名称(支持嵌套目录)
74
74
  * @param {string} pluginsDir - 插件目录
75
+ * @param {string} baseDir - 基础目录(用于计算相对路径)
75
76
  * @returns {string[]} 插件名称列表
76
77
  */
77
- function scanPluginNames(pluginsDir) {
78
+ function scanPluginNames(pluginsDir, baseDir = null) {
78
79
  if (!fs.existsSync(pluginsDir)) {
79
80
  return [];
80
81
  }
81
82
 
83
+ if (baseDir === null) {
84
+ baseDir = pluginsDir;
85
+ }
86
+
82
87
  // 忽略的目录/文件(无意义或系统文件)
83
88
  const IGNORE_PATTERNS = new Set([
84
89
  '__pycache__',
@@ -105,13 +110,22 @@ function scanPluginNames(pluginsDir) {
105
110
  const isDir = entry.isSymbolicLink() ? isDirectory(fullPath) : entry.isDirectory();
106
111
 
107
112
  if (isDir) {
108
- // 文件夹插件(包括 Junction Symbolic Link)
109
- names.add(entry.name);
113
+ const indexPath = path.join(fullPath, 'index.js');
114
+ if (fs.existsSync(indexPath)) {
115
+ // 这是一个插件目录
116
+ const relativePath = path.relative(baseDir, fullPath);
117
+ names.add(relativePath.replace(/\\/g, '/'));
118
+ } else {
119
+ // 不是插件目录,递归扫描子目录
120
+ const subNames = scanPluginNames(fullPath, baseDir);
121
+ subNames.forEach(n => names.add(n));
122
+ }
110
123
  } else if (entry.isFile() && entry.name.endsWith('.js')) {
111
124
  // 单文件插件(排除与文件夹同名的)
112
125
  const baseName = entry.name.replace(/\.js$/, '');
113
126
  if (!names.has(baseName)) {
114
- names.add(baseName);
127
+ const relativePath = path.relative(baseDir, fullPath);
128
+ names.add(relativePath.replace(/\\/g, '/').replace(/\.js$/, ''));
115
129
  }
116
130
  }
117
131
  }
package/subagent.md CHANGED
@@ -111,7 +111,7 @@
111
111
  ## 【Extensions】扩展插件
112
112
 
113
113
  **使用流程(必须按顺序执行):**
114
- 1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `loadSkill({ skill: "<skill_name>" })` 获取目标扩展的详细工具参数
114
+ 1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `skill_load({ skill: "<skill_name>" })` 获取目标扩展的详细工具参数
115
115
  2. 根据返回的参数定义,使用 `ext_call({ plugin, tool, args })` 调用扩展
116
116
 
117
117
  > **警告**:禁止在未执行第1步获取参数的情况下直接调用 `ext_call`!
@@ -167,4 +167,4 @@ MCP (Model Context Protocol) 执行器
167
167
  **工具:** `designmd_search_design_kits`, `designmd_get_design_kit`, `designmd_download_design_kit`, `designmd_upload_design_kit`, `designmd_delete_design_kit`, `designmd_list_popular_kits`, `designmd_list_tags`
168
168
 
169
169
  ## 禁止事项
170
- - 不先调用 `ext_skill/loadSkill` 获取参数就直接使用 `ext_call`
170
+ - 不先调用 `ext_skill/skill_load` 获取参数就直接使用 `ext_call`