foliko 2.0.5 → 2.0.7

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 (93) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.cli_default_systemPrompt.md +291 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +20 -3
  5. package/docs/architecture.md +34 -2
  6. package/docs/extensions.md +199 -0
  7. package/docs/migration.md +100 -0
  8. package/docs/public-api.md +280 -24
  9. package/docs/usage.md +122 -30
  10. package/package.json +1 -1
  11. package/plugins/core/audit/index.js +1 -1
  12. package/plugins/core/default/bootstrap.js +44 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/scheduler/index.js +1 -0
  15. package/plugins/core/skill-manager/PROMPT.md +6 -0
  16. package/plugins/core/skill-manager/index.js +402 -115
  17. package/plugins/core/sub-agent/PROMPT.md +10 -0
  18. package/plugins/core/sub-agent/index.js +36 -3
  19. package/plugins/core/think/index.js +1 -0
  20. package/plugins/core/workflow/context.js +941 -0
  21. package/plugins/core/workflow/engine.js +66 -0
  22. package/plugins/core/workflow/examples/01-basic.js +42 -0
  23. package/plugins/core/workflow/examples/01-basic.json +30 -0
  24. package/plugins/core/workflow/examples/02-choice.js +75 -0
  25. package/plugins/core/workflow/examples/02-choice.json +59 -0
  26. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  27. package/plugins/core/workflow/examples/03-each.json +41 -0
  28. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  29. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  30. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  31. package/plugins/core/workflow/examples/05-each.js +65 -0
  32. package/plugins/core/workflow/examples/06-script.js +82 -0
  33. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  34. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  35. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  36. package/plugins/core/workflow/examples/10-logger.js +34 -0
  37. package/plugins/core/workflow/examples/11-storage.js +68 -0
  38. package/plugins/core/workflow/examples/simple.js +77 -0
  39. package/plugins/core/workflow/examples/simple.json +75 -0
  40. package/plugins/core/workflow/index.js +204 -78
  41. package/plugins/core/workflow/js-runner.js +318 -0
  42. package/plugins/core/workflow/json-runner.js +323 -0
  43. package/plugins/core/workflow/stages/action.js +211 -0
  44. package/plugins/core/workflow/stages/choice.js +74 -0
  45. package/plugins/core/workflow/stages/delay.js +73 -0
  46. package/plugins/core/workflow/stages/each.js +123 -0
  47. package/plugins/core/workflow/stages/parallel.js +69 -0
  48. package/plugins/core/workflow/stages/try.js +142 -0
  49. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  50. package/plugins/executors/data-splitter/index.js +8 -6
  51. package/plugins/executors/extension/extension-registry.js +145 -0
  52. package/plugins/executors/extension/index.js +405 -437
  53. package/plugins/executors/extension/prompt-builder.js +359 -0
  54. package/plugins/executors/extension/skill-helper.js +143 -0
  55. package/plugins/messaging/feishu/index.js +5 -3
  56. package/plugins/messaging/qq/index.js +6 -4
  57. package/plugins/messaging/telegram/index.js +6 -3
  58. package/plugins/messaging/weixin/index.js +5 -3
  59. package/plugins/tools/PROMPT.md +26 -0
  60. package/plugins/tools/index.js +6 -5
  61. package/sandbox/check-context.js +5 -0
  62. package/sandbox/test-context.js +27 -0
  63. package/sandbox/test-fixes.js +40 -0
  64. package/sandbox/test-hello-js.js +46 -0
  65. package/skills/foliko/AGENTS.md +196 -43
  66. package/skills/foliko/SKILL.md +157 -28
  67. package/skills/mcp/SKILL.md +77 -118
  68. package/skills/plugins/SKILL.md +89 -3
  69. package/skills/python/SKILL.md +57 -39
  70. package/skills/skill-guide/SKILL.md +42 -34
  71. package/skills/workflows/SKILL.md +753 -436
  72. package/src/agent/chat.js +56 -28
  73. package/src/agent/main.js +39 -14
  74. package/src/agent/prompt-registry.js +56 -16
  75. package/src/agent/prompts/PROMPT.md +3 -0
  76. package/src/agent/sub.js +1 -1
  77. package/src/cli/ui/chat-ui-old.js +5 -2
  78. package/src/cli/ui/chat-ui.js +9 -5
  79. package/src/common/constants.js +12 -0
  80. package/src/common/error-capture.js +91 -0
  81. package/src/common/json-safe.js +20 -0
  82. package/src/common/logger.js +2 -2
  83. package/src/context/compressor.js +6 -2
  84. package/src/executors/mcp-executor.js +105 -125
  85. package/src/framework/framework.js +78 -12
  86. package/src/index.js +4 -0
  87. package/src/plugin/base.js +908 -5
  88. package/src/plugin/manager.js +124 -9
  89. package/src/tool/schema.js +32 -9
  90. package/src/utils/sandbox.js +1 -1
  91. package/website/index.html +821 -0
  92. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  93. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
@@ -1,25 +1,34 @@
1
1
  /**
2
- * ExtensionExecutorPlugin 扩展插件执行器
2
+ * ExtensionExecutorPlugin - 扩展插件执行器
3
3
  * 统一管理扩展插件(如 gate-trading)的工具,通过 ext_call 调用
4
+ *
5
+ * 模块拆分:
6
+ * - extension-registry.js: 扩展注册表(数据管理)
7
+ * - prompt-builder.js: Prompt 构建器(提示词生成)
8
+ * - skill-helper.js: Skill 命令辅助
9
+ * - index.js (本文件): 主入口,组合各模块
4
10
  */
5
11
 
6
12
  const { Plugin } = require('../../../src/plugin/base');
7
13
  const { logger } = require('../../../src/common/logger');
8
14
  const { z } = require('zod');
9
- const { PROMPT_PRIORITY } = require('../../../src/common/constants');
10
- const { zodSchemaToMarkdown, zodSchemaToTable } = require('@chnak/zod-to-markdown');
15
+
16
+ const { ExtensionRegistry } = require('./extension-registry');
17
+ const { ExtensionPromptBuilder } = require('./prompt-builder');
18
+ const { SkillHelper } = require('./skill-helper');
11
19
 
12
20
  const log = logger.child('ExtensionExecutor');
13
21
 
14
22
  class ExtensionExecutorPlugin extends Plugin {
15
- // 声明式 prompt parts
23
+ // 声明式 prompt parts - 由基类自动注册和管理生命周期
16
24
  prompts = [
17
25
  {
18
26
  name: 'extension-tools',
19
- slot: 'CAPABILITY',
27
+ scope: 'global',
28
+ priority: 60,
20
29
  description: '所有扩展工具的说明和使用方式',
21
30
  provider: function () {
22
- return this._buildExtensionsDescription();
31
+ return this._promptBuilder.build();
23
32
  },
24
33
  },
25
34
  ];
@@ -33,47 +42,93 @@ class ExtensionExecutorPlugin extends Plugin {
33
42
  this.system = true;
34
43
 
35
44
  this._framework = null;
36
- this._extensions = new Map();
45
+ this._registry = new ExtensionRegistry();
46
+ this._promptBuilder = new ExtensionPromptBuilder(this._registry, this._registry);
47
+ this._skillHelper = null; // 延迟到 start 时创建(需要 framework)
37
48
  }
38
49
 
50
+ // ============ 生命周期 ============
51
+
39
52
  install(framework) {
40
53
  this._framework = framework;
54
+ this._skillHelper = new SkillHelper(this._registry, framework);
41
55
 
42
- framework.on('plugin:loaded', (plugin) => {
43
- this._scanPluginTools(plugin);
44
- });
56
+ framework.on('plugin:loaded', (plugin) => this._scanPlugin(plugin));
57
+ framework.on('plugin:reloaded', (plugin) => this._rescanPlugin(plugin));
45
58
 
46
- framework.on('plugin:reloaded', (plugin) => {
47
- this._rescanPluginTools(plugin);
59
+ // 监听 MCP 工具变化(MCP 工具是异步加载的)
60
+ // MCP 服务器现在直接注册为 mcp:<server> 扩展,
61
+ // mcp:tools-changed 事件触发时让 prompt 缓存失效,让 provider 重新渲染
62
+ framework.on('mcp:tools-changed', () => {
63
+ this._invalidatePromptCaches();
64
+ });
65
+ framework.on('mcp:reloaded', () => {
66
+ this._invalidatePromptCaches();
48
67
  });
49
-
50
- // 不监听 tool:registered — 直接 registerTool 注册的工具通过 AI SDK 暴露给 LLM,无需出现在 Extensions 中
51
68
 
52
69
  framework.on('framework:ready', () => {
53
70
  log.debug('Framework ready, rescanning all plugin tools...');
54
- const plugins = framework.pluginManager.getAll();
55
- for (const { instance: plugin } of plugins) {
56
- if (plugin.name === 'skill-manager') {
57
- continue;
58
- }
59
- this._scanPluginTools(plugin);
60
- }
61
- this._invalidatePromptCaches(framework);
71
+ this._scanAllPlugins();
72
+ this._invalidatePromptCaches();
62
73
  });
63
74
 
64
75
  return this;
65
76
  }
66
77
 
67
- _scanPluginTools(plugin, extName) {
68
- if (!plugin || !plugin.tools || typeof plugin.tools !== 'object') {
69
- return;
78
+ async start(framework) {
79
+ super.start(framework);
80
+ this._framework = framework;
81
+ this._registry._framework = framework;
82
+ this._promptBuilder = new ExtensionPromptBuilder(this._registry, framework);
83
+ this._skillHelper = new SkillHelper(this._registry, framework);
84
+
85
+ this._scanAllPlugins();
86
+ this._registerBuiltinTools(framework);
87
+
88
+ this._invalidatePromptCaches();
89
+ return this;
90
+ }
91
+
92
+ async reload(framework) {
93
+ this._framework = framework;
94
+ this._registry.clear();
95
+ this._scanAllPlugins();
96
+ this._invalidatePromptCaches();
97
+ }
98
+
99
+ uninstall(framework) {
100
+ this._registry.clear();
101
+ this._framework = null;
102
+ }
103
+
104
+ // ============ 扫描插件 ============
105
+
106
+ _scanAllPlugins() {
107
+ if (!this._framework) return;
108
+ const plugins = this._framework.pluginManager.getAll();
109
+ for (const { instance: plugin } of plugins) {
110
+ if (plugin.name === 'skill-manager') continue;
111
+ this._scanPlugin(plugin);
70
112
  }
113
+ }
71
114
 
72
- const pluginName = plugin.name;
73
- if (!pluginName) return;
115
+ /**
116
+ * 扫描单个插件的工具
117
+ * 兼容两种注册方式:
118
+ * 1. plugin.tools = { name: toolDef } - 旧的静态方式
119
+ * 2. plugin._registeredExtensions = [...] - 新的 this.extension.register() 方式
120
+ */
121
+ _scanPlugin(plugin) {
122
+ if (!plugin || !plugin.name) return;
74
123
 
75
- const useExtName = extName || pluginName;
124
+ this._scanPluginTools(plugin);
125
+ this._scanRegisteredExtensions(plugin);
126
+ }
76
127
 
128
+ _scanPluginTools(plugin) {
129
+ if (!plugin.tools || typeof plugin.tools !== 'object') return;
130
+
131
+ const useExtName = plugin.name;
77
132
  const pluginInfo = {
78
133
  name: useExtName,
79
134
  description: plugin.description || '',
@@ -82,7 +137,6 @@ class ExtensionExecutorPlugin extends Plugin {
82
137
 
83
138
  for (const [toolName, toolDef] of Object.entries(plugin.tools)) {
84
139
  if (!toolDef || typeof toolDef !== 'object') continue;
85
-
86
140
  this.registerTool(useExtName, pluginInfo, {
87
141
  name: toolName,
88
142
  description: toolDef.description || '',
@@ -91,504 +145,418 @@ class ExtensionExecutorPlugin extends Plugin {
91
145
  });
92
146
  }
93
147
 
94
- log.debug(` Scanned ${Object.keys(plugin.tools).length} tools from plugin '${pluginName}' (as '${useExtName}')`);
148
+ log.debug(` Scanned ${Object.keys(plugin.tools).length} tools from plugin '${plugin.name}'`);
149
+ }
150
+
151
+ _scanRegisteredExtensions(plugin) {
152
+ if (!plugin._registeredExtensions || !Array.isArray(plugin._registeredExtensions)) return;
95
153
 
96
- if (this._framework && this._framework._ready) {
97
- this._invalidatePromptCaches(this._framework);
154
+ const pluginInfo = {
155
+ name: plugin.name,
156
+ description: plugin.description || '',
157
+ version: plugin.version || '1.0.0',
158
+ };
159
+
160
+ for (const { extName, toolDef } of plugin._registeredExtensions) {
161
+ this.registerTool(extName || plugin.name, pluginInfo, toolDef);
98
162
  }
163
+
164
+ log.debug(` Scanned ${plugin._registeredExtensions.length} extensions from plugin '${plugin.name}'`);
165
+ }
166
+
167
+ _rescanPlugin(plugin) {
168
+ if (!plugin || !plugin.name) return;
169
+ this._registry.clearPlugin(plugin.name);
170
+ this._scanPlugin(plugin);
99
171
  }
100
172
 
101
- _rescanPluginTools(plugin) {
102
- if (!plugin || !plugin.tools || typeof plugin.tools !== 'object') {
173
+ // ============ 注册/注销工具 ============
174
+
175
+ /**
176
+ * 注册扩展工具(供 Plugin 基类 extension.register 调用)
177
+ */
178
+ registerTool(pluginName, pluginInfo, toolDef) {
179
+ if (!toolDef || !toolDef.name) {
180
+ log.warn(`[ExtensionExecutor] Invalid tool definition for '${pluginName}'`);
103
181
  return;
104
182
  }
105
183
 
106
- const pluginName = plugin.name;
107
- if (!pluginName) return;
108
-
109
- const extName = pluginName === 'skill-manager' ? 'skill' : pluginName;
110
-
111
- const keysToRemove = [extName, pluginName];
112
- for (const key of keysToRemove) {
113
- if (this._extensions.has(key)) {
114
- const ext = this._extensions.get(key);
115
- for (const tool of ext.tools || []) {
116
- if (this._framework?.toolRouter) {
117
- this._framework.toolRouter.unregister(`ext_${tool.name}`);
118
- }
119
- }
120
- this._extensions.delete(key);
121
- }
122
- }
184
+ this._registry.registerTool(pluginName, pluginInfo, toolDef);
185
+ this._invalidatePromptCaches();
186
+ }
123
187
 
124
- this._scanPluginTools(plugin, extName);
188
+ /**
189
+ * 注销扩展工具(供 Plugin 基类 extension.remove 调用)
190
+ */
191
+ _unregisterTool(pluginName, toolName) {
192
+ this._registry.unregisterTool(pluginName, toolName);
193
+ if (this._framework?.toolRouter) {
194
+ this._framework.toolRouter.unregister(`ext_${toolName}`);
195
+ }
196
+ this._invalidatePromptCaches();
125
197
  }
126
198
 
127
- async start(framework) {
128
- super.start(framework);
129
- const plugins = framework.pluginManager.getAll();
130
- for (const { instance: plugin } of plugins) {
131
- if (plugin.name === 'skill-manager') {
132
- continue;
133
- }
134
- this._scanPluginTools(plugin);
199
+ // ============ 缓存管理 ============
200
+
201
+ _invalidatePromptCaches() {
202
+ this._framework?.prompts?.invalidate(this.name, 'extension-tools');
203
+ for (const agent of this._framework?._agents || []) {
204
+ agent._invalidateSystemPromptCache?.();
135
205
  }
206
+ }
207
+
208
+ // ============ 内置工具 ============
136
209
 
137
- this._mcpExecutor = framework.pluginManager?.get('mcp') || null;
210
+ _registerBuiltinTools(framework) {
211
+ framework.registerTool(this._createExtCallTool());
212
+ framework.registerTool(this._createExtListTool());
213
+ framework.registerTool(this._createExtSkillTool());
214
+ }
138
215
 
139
- framework.registerTool({
216
+ _createExtCallTool() {
217
+ return {
140
218
  name: 'ext_call',
141
- description: '调用扩展插件的工具(包括 MCP 服务器工具)。注意:使用前必须先调用 ext_skill 获取详细参数和使用方法',
219
+ description:
220
+ '【⚡ 执行扩展工具】调用某个扩展插件的具体工具。\n' +
221
+ '\n' +
222
+ '**必传参数**:\n' +
223
+ '- `plugin`:插件名(命名空间支持 4 种:普通插件名 `email` / MCP 服务器 `mcp:<server>` / Skill `skill:<name>` / Python 插件 `python:<plugin>`)\n' +
224
+ '- `tool`:工具名(来自 ext_list 或 ext_skill 输出)\n' +
225
+ '- `args`:参数对象(每个工具的必填项见 ext_skill 输出)\n' +
226
+ '\n' +
227
+ '**推荐使用顺序**:`ext_list` 查看所有可用插件 → `ext_skill` 查看参数细节 → `ext_call` 执行\n' +
228
+ '\n' +
229
+ '**调用示例**:\n' +
230
+ '```js\n' +
231
+ 'ext_call({\n' +
232
+ ' plugin: "mcp:designmd",\n' +
233
+ ' tool: "search_design_kits",\n' +
234
+ ' args: { query: "modern" }\n' +
235
+ '})\n' +
236
+ '```\n' +
237
+ '\n' +
238
+ '**返回值**:`{success: true, data: <工具返回的数据>}` 或 `{success: false, error: "<错误信息>"}`\n' +
239
+ '**注意**:调用前必须先调 ext_skill 确认参数格式和必填项,否则可能因参数缺失失败',
142
240
  inputSchema: z.object({
143
- plugin: z.string().describe('插件名称(如 email, gate-trading, mcp)'),
144
- tool: z.string().describe('工具名称'),
145
- args: z.record(z.any()).optional().describe('工具参数'),
241
+ plugin: z.string().describe('插件名称(如 email, gate-trading, mcp:designmd, skill:my-skill, python:stock)'),
242
+ tool: z.string().describe('工具名称(来自 ext_list 输出)'),
243
+ args: z.record(z.any()).optional().describe('工具参数(对象格式,详见 ext_skill 输出)'),
146
244
  }),
147
245
  execute: async (args) => {
148
246
  const { plugin, tool, args: toolArgs = {} } = args;
149
-
150
- if (plugin === 'mcp' && this._mcpExecutor) {
151
- const mcpToolDef = this._mcpExecutor.tools?.[tool];
152
- if (mcpToolDef && mcpToolDef.execute) {
153
- return await mcpToolDef.execute(toolArgs || {});
154
- }
155
- return { success: false, error: `MCP tool '${tool}' not found` };
156
- }
157
-
158
- const ext = this._extensions.get(plugin);
159
- if (!ext) {
160
- framework.emit('tool:error', {
161
- name: `${plugin}:${tool}`,
162
- args: toolArgs,
163
- error: `扩展插件 '${plugin}' 不存在`,
164
- source: 'extension'
165
- });
166
- return { success: false, error: `扩展插件 '${plugin}' 不存在` };
167
- }
168
-
169
- const toolDef = ext.tools.find((t) => t.name === tool);
170
- if (!toolDef) {
171
- framework.emit('tool:error', {
172
- name: `${plugin}:${tool}`,
173
- args: toolArgs,
174
- error: `扩展工具 '${tool}' 不存在`,
175
- source: 'extension'
176
- });
177
- return { success: false, error: `扩展工具 '${tool}' 不存在` };
178
- }
179
-
180
- try {
181
- framework.emit('tool:call', {
182
- name: `${plugin}:${tool}`,
183
- args: toolArgs,
184
- source: 'extension'
185
- });
186
- framework.emit('tool-call', {
187
- name: `${plugin}:${tool}`,
188
- args: toolArgs,
189
- source: 'extension'
190
- });
191
-
192
- const result = await toolDef.execute(toolArgs, framework);
193
-
194
- if (result && result.success === false && result.error) {
195
- framework.emit('tool:error', {
196
- name: `${plugin}:${tool}`,
197
- args: toolArgs,
198
- error: result.error,
199
- source: 'extension'
200
- });
201
- return result;
202
- }
203
-
204
- framework.emit('tool:result', {
205
- name: `${plugin}:${tool}`,
206
- args: toolArgs,
207
- result,
208
- source: 'extension'
209
- });
210
-
211
- return { success: true, data: result };
212
- } catch (err) {
213
- log.error(` Tool '${tool}' failed:`, err.message);
214
-
215
- framework.emit('tool:error', {
216
- name: `${plugin}:${tool}`,
217
- args: toolArgs,
218
- error: err.message,
219
- source: 'extension'
220
- });
221
-
222
- return { success: false, error: err.message };
223
- }
247
+ return this._executeExtensionTool(plugin, tool, toolArgs);
224
248
  },
225
- });
249
+ };
250
+ }
226
251
 
227
- framework.registerTool({
252
+ _createExtListTool() {
253
+ return {
228
254
  name: 'ext_list',
229
- description: '列出所有可用的扩展插件及其工具',
255
+ description:
256
+ '【📋 扩展工具清单】列出所有可用的扩展插件及其工具。\n' +
257
+ '\n' +
258
+ '**用途**:作为使用扩展的第一步,先调用本工具了解可用能力。\n' +
259
+ '\n' +
260
+ '**返回格式**:\n' +
261
+ '```js\n' +
262
+ '{\n' +
263
+ ' success: true,\n' +
264
+ ' data: [\n' +
265
+ ' { name: "email", description: "...", tools: [{ name: "send_mail", description: "..." }] },\n' +
266
+ ' { name: "mcp:designmd", description: "...", tools: [...] }\n' +
267
+ ' ]\n' +
268
+ '}\n' +
269
+ '```\n' +
270
+ '\n' +
271
+ '**命名空间规则**:返回的 plugin 名支持 4 种命名空间:\n' +
272
+ '- 普通插件:`email`, `gate-trading`\n' +
273
+ '- MCP 服务器:`mcp:<servername>`(如 `mcp:designmd`)\n' +
274
+ '- Skill:`skill:<name>`(如 `skill:ambient-agent`)\n' +
275
+ '- Python 插件:`python:<plugin>`(如 `python:stock`)\n' +
276
+ '\n' +
277
+ '**推荐使用顺序**:`ext_list` → `ext_skill` → `ext_call`',
230
278
  inputSchema: z.object({}),
231
279
  execute: async () => {
232
- const extensions = [];
233
- for (const [name, ext] of this._extensions) {
234
- extensions.push({
235
- name,
236
- description: ext.description,
237
- version: ext.version,
238
- tools: ext.tools.filter(t => t && t.name).map((t) => ({ name: t.name, description: t.description })),
239
- });
240
- }
280
+ const extensions = this._registry.getAll().map((ext) => ({
281
+ name: ext.name,
282
+ description: ext.description,
283
+ version: ext.version,
284
+ tools: ext.tools
285
+ .filter((t) => t && t.name)
286
+ .map((t) => ({ name: t.name, description: t.description })),
287
+ }));
241
288
  return { success: true, data: extensions };
242
289
  },
243
- });
290
+ };
291
+ }
244
292
 
245
- framework.registerTool({
293
+ _createExtSkillTool() {
294
+ return {
246
295
  name: 'ext_skill',
247
- description: '查询扩展工具的调用参数结构',
296
+ description:
297
+ '【📖 扩展参数手册】查询某个扩展插件的详细参数结构。\n' +
298
+ '\n' +
299
+ '**必传参数**:`plugin`(支持 `mcp:<server>`, `skill:<name>`, `python:<plugin>`, 普通插件名)\n' +
300
+ '\n' +
301
+ '**返回内容**:Markdown 格式,包含:\n' +
302
+ '- 插件标题、描述、类型元数据\n' +
303
+ '- 快速调用示例(可直接复制)\n' +
304
+ '- 每个工具的参数表(含必填/可选、类型、描述)\n' +
305
+ '\n' +
306
+ '**典型使用**:找到陌生工具的必填参数 → 调用 ext_skill 看详情 → 再用 ext_call 执行\n' +
307
+ '\n' +
308
+ '**调用示例**:\n' +
309
+ '```js\n' +
310
+ 'ext_skill({ plugin: "mcp:designmd" })\n' +
311
+ '```\n' +
312
+ '\n' +
313
+ '**插件不存在时**:返回友好的错误提示(包含可能原因和排查步骤)',
248
314
  inputSchema: z.object({
249
- plugin: z.string().describe('扩展插件名称')
315
+ plugin: z.string().describe('扩展插件名称(如 mcp:designmd, skill:my-skill, python:stock, email)'),
250
316
  }),
251
317
  execute: async (args) => {
252
318
  const { plugin } = args;
253
- if (plugin === 'skill') {
254
- return "请使用 `skill_load` 获取skill的使用详情"
255
- }
256
- return this.bindAllToolSkill(plugin)
319
+ return this._promptBuilder.buildSkill(plugin);
257
320
  },
258
- });
259
-
260
- framework.on('agent:created', (agent) => {
261
- // Extension tool 描述已通过 prompts 声明式注册到 framework.prompts,
262
- // 并由 _syncPromptParts 同步到子 Agent,无需再注入 _originalPrompt
263
- });
321
+ };
322
+ }
264
323
 
265
- // Extension tool 描述已通过 prompts 声明式注册到 framework.prompts,
266
- // 不再需要 _refreshAllAgentsExtPrompt 注入 _originalPrompt
324
+ // ============ 工具执行 ============
267
325
 
268
- // prompt part 由基类 _autoRegisterPrompts 自动处理
326
+ /**
327
+ * 执行扩展工具(供 ext_call 工具调用)
328
+ */
329
+ async _executeExtensionTool(plugin, tool, toolArgs) {
330
+ return this._doExecute(plugin, tool, toolArgs, { source: 'ext_call', emitEvent: true });
331
+ }
269
332
 
270
- return this;
333
+ /**
334
+ * 执行扩展工具(供 Plugin 基类 extension.execute 调用)
335
+ */
336
+ async _executeTool(pluginName, toolName, args) {
337
+ return this._doExecute(pluginName, toolName, args, { source: 'plugin_extension', emitEvent: false });
271
338
  }
272
339
 
273
- registerTool(pluginName, pluginInfo, toolDef) {
274
- if (!toolDef || !toolDef.name) {
275
- log.warn(`[ExtensionExecutor] Invalid tool definition for '${pluginName}'`);
276
- return;
340
+ /**
341
+ * 统一的工具执行内部实现
342
+ * @private
343
+ */
344
+ async _doExecute(plugin, tool, toolArgs, { source, emitEvent }) {
345
+ // 1) 插件存在性检查 + 友好错误
346
+ const ext = this._registry.getPlugin(plugin);
347
+ if (!ext) {
348
+ return this._pluginNotFoundError(plugin);
277
349
  }
278
350
 
279
- if (!this._extensions.has(pluginName)) {
280
- this._extensions.set(pluginName, {
281
- name: pluginInfo.name || pluginName,
282
- description: pluginInfo.description || '',
283
- version: pluginInfo.version || '1.0.0',
284
- tools: [],
285
- });
351
+ // 2) 工具存在性检查 + 友好错误
352
+ const toolDef = ext.tools.find((t) => t.name === tool);
353
+ if (!toolDef) {
354
+ return this._toolNotFoundError(plugin, ext, tool);
286
355
  }
287
356
 
288
- const ext = this._extensions.get(pluginName);
289
- const existingIdx = ext.tools.findIndex((t) => t.name === toolDef.name);
290
- const toolEntry = {
291
- name: toolDef.name,
292
- description: toolDef.description || '',
293
- inputSchema: toolDef.inputSchema,
294
- execute: toolDef.execute,
295
- };
296
- if (toolDef._options) {
297
- toolEntry._options = toolDef._options;
298
- }
299
- if (existingIdx >= 0) {
300
- ext.tools[existingIdx] = toolEntry;
301
- } else {
302
- ext.tools.push(toolEntry);
357
+ // 3) 参数预校验(Zod schema)
358
+ if (toolDef.inputSchema && typeof toolDef.inputSchema.safeParse === 'function') {
359
+ const parsed = toolDef.inputSchema.safeParse(toolArgs);
360
+ if (!parsed.success) {
361
+ return this._paramValidationError(plugin, tool, parsed.error);
362
+ }
303
363
  }
304
364
 
305
- //log.debug(` Registered tool '${toolDef.name}' for extension '${pluginName}'`);
306
-
307
- if (this._framework && this._framework._ready) {
308
- this._invalidatePromptCaches(this._framework);
365
+ // 4) 触发事件(可选)
366
+ if (emitEvent) {
367
+ this._framework?.emit?.('tool-call', {
368
+ name: `${plugin}:${tool}`,
369
+ args: toolArgs,
370
+ source,
371
+ });
309
372
  }
310
- }
311
373
 
312
- bindAllToolSkill(pluginName) {
313
- const mcp = this._framework.pluginManager.get('mcp');
314
- const sections = [
315
- '你可以通过 `ext_call` 工具调用以下扩展插件的功能。',
316
- '',
317
- '### 调用规则',
318
- '',
319
- '> **重要**:',
320
- '> 1. 调用前必须指定 `plugin`(插件名)和 `tool`(工具名)',
321
- '> 2. 参数通过 `args` 对象传入,参数名和类型见各工具说明,注意引号的问题,遵守JSON的格式',
322
- '> 3. 必填参数(Required)必须提供,可选参数(Optional)可省略',
323
- '',
324
- '---',
325
- '',
326
- ];
327
-
328
- const ext = this._extensions.get(pluginName);
329
- if (!ext) return '找不到扩展!'
330
- for (const tool of ext.tools) {
331
- sections.push(`#### \`${tool.name}\``, '');
332
- sections.push(tool.description || '无描述', '');
333
- if (ext.name === 'mcp' && mcp) {
334
- const mcp_desc = mcp._bindMcpParamsDesc(tool.inputSchema);
335
- sections.push(mcp_desc, '');
336
- } else if (tool.inputSchema) {
337
- try {
338
- const schemaMarkdown = zodSchemaToMarkdown(tool.inputSchema);
339
- sections.push('**参数:**', ' ');
340
- sections.push(schemaMarkdown || '无', ' ');
341
- } catch (e) {
342
- const schemaTable = zodSchemaToTable(tool.inputSchema);
343
- sections.push('**参数:**', ' ');
344
- sections.push(schemaTable || '无', ' ');
345
- }
346
- }
374
+ // 5) 执行
375
+ try {
376
+ const result = await toolDef.execute(toolArgs, this._framework);
377
+ return { success: true, data: result };
378
+ } catch (err) {
379
+ return this._executionError(plugin, tool, err);
347
380
  }
381
+ }
348
382
 
349
- sections.push('');
350
- return sections.join('\n')
383
+ // ============ 错误处理辅助方法 ============
384
+
385
+ /** 插件不存在错误 */
386
+ _pluginNotFoundError(plugin) {
387
+ const allPlugins = this._registry.listPlugins();
388
+ const namespaces = this._groupByNamespace(allPlugins);
389
+ const similar = this._findSimilar(plugin, allPlugins);
390
+
391
+ return {
392
+ success: false,
393
+ error: `扩展插件 '${plugin}' 不存在`,
394
+ hint: similar.length > 0
395
+ ? `你可能想用:${similar.slice(0, 3).join(', ')}`
396
+ : `可用插件:\n${this._formatNamespaceList(namespaces)}`,
397
+ availableNamespaces: namespaces,
398
+ similar: similar.slice(0, 3),
399
+ };
351
400
  }
352
401
 
353
- /**
354
- * 失效所有 Agent 的延展工具 prompt 缓存
355
- * 不再注入 _originalPrompt,仅失效缓存让下次 build 时自动读取最新 _buildExtensionsDescription()
356
- */
357
- _invalidatePromptCaches(framework) {
358
- framework?.prompts?.invalidate(this.name, 'extension-tools');
359
- for (const agent of framework?._agents || []) {
360
- agent._invalidateSystemPromptCache?.();
361
- }
402
+ /** 工具不存在错误 */
403
+ _toolNotFoundError(plugin, ext, tool) {
404
+ const availableTools = ext.tools.map((t) => t.name);
405
+ const similar = this._findSimilar(tool, availableTools);
406
+
407
+ return {
408
+ success: false,
409
+ error: `扩展工具 '${tool}' 不存在(插件 '${plugin}')`,
410
+ hint: similar.length > 0
411
+ ? `你可能想用:${similar.slice(0, 3).join(', ')}`
412
+ : `可用工具(${availableTools.length} 个):${availableTools.slice(0, 10).join(', ')}${availableTools.length > 10 ? '...' : ''}`,
413
+ availableTools,
414
+ similar: similar.slice(0, 3),
415
+ };
362
416
  }
363
417
 
364
- _buildExtensionsDescription() {
365
- let desc = '';
418
+ /** 参数校验错误(Zod) */
419
+ _paramValidationError(plugin, tool, zodError) {
420
+ const issues = (zodError && zodError.issues) || [];
421
+ const formatted = issues.slice(0, 5).map((iss) => {
422
+ const path = (iss.path && iss.path.length > 0 ? iss.path.join('.') : '<root>');
423
+ return ` - ${path}: ${iss.message}`;
424
+ }).join('\n');
425
+
426
+ return {
427
+ success: false,
428
+ error: `参数校验失败:'${plugin}:${tool}'`,
429
+ details: formatted,
430
+ issuesCount: issues.length,
431
+ hint: '请使用 ext_skill({ plugin: "' + plugin + '" }) 查看完整参数说明',
432
+ };
433
+ }
366
434
 
367
- if (this._extensions.size > 0 || (this._mcpExecutor && Object.keys(this._mcpExecutor.tools || {}).length > 0)) {
368
- desc += '## 【Extensions】扩展插件\n\n';
369
- desc += '**使用流程(必须按顺序执行):**\n';
370
- desc += '1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 获取技能命令的详细参数\n';
371
- desc += '2. 根据返回的参数定义,使用 `ext_call({ plugin: "<plugin_name>", tool: "<tool_name>", args: {...} })` 调用\n\n';
372
- desc += '> **警告**:禁止在未执行第1步获取参数的情况下直接调用 `ext_call`!\n\n';
435
+ /** 执行时错误 */
436
+ _executionError(plugin, tool, err) {
437
+ const message = (err && err.message) || String(err);
438
+ const stack = (err && err.stack) ? err.stack.split('\n').slice(0, 5).join('\n') : '';
439
+ log.error(`Tool '${plugin}:${tool}' failed:`, message);
440
+ if (stack) log.debug(` Stack: ${stack}`);
441
+
442
+ return {
443
+ success: false,
444
+ error: message,
445
+ errorType: (err && err.name) || 'Error',
446
+ stack,
447
+ timeout: message.includes('timeout'),
448
+ };
449
+ }
373
450
 
374
- for (const [name, ext] of this._extensions) {
375
- if (name === 'mcp' || name === 'skill') continue;
376
- desc += `### ${ext.name || name}\n`;
377
- desc += `${ext.description || '无描述'}\n`;
378
- const validTools = ext.tools.filter(t => t && t.name);
379
- desc += `**工具:** ${validTools.map(t => `\`${t.name}\``).join(', ')}\n\n`;
451
+ // ============ 匹配/格式化辅助方法 ============
452
+
453
+ /** 模糊匹配(包含 / 编辑距离),返回相似名称列表 */
454
+ _findSimilar(target, candidates, max = 5) {
455
+ if (!target || !Array.isArray(candidates) || candidates.length === 0) return [];
456
+ const lower = String(target).toLowerCase();
457
+ const threshold = Math.max(2, Math.floor(lower.length / 3));
458
+
459
+ const scored = [];
460
+ for (const c of candidates) {
461
+ if (typeof c !== 'string') continue;
462
+ const cl = c.toLowerCase();
463
+ // 1) 包含关系
464
+ if (cl.includes(lower) || lower.includes(cl)) {
465
+ scored.push({ name: c, score: 1 });
466
+ continue;
380
467
  }
381
-
382
- if (this._mcpExecutor && Object.keys(this._mcpExecutor.tools || {}).length > 0) {
383
- desc += '### mcp (MCP 服务器工具)\n';
384
- desc += '已注册为 `服务器_工具名` 格式\n';
385
- desc += `**工具:** ${Object.keys(this._mcpExecutor.tools).map(t => `\`${t}\``).join(', ')}\n`;
468
+ // 2) 编辑距离
469
+ const dist = this._levenshtein(lower, cl);
470
+ if (dist <= threshold) {
471
+ scored.push({ name: c, score: 1 - dist / Math.max(lower.length, cl.length) });
386
472
  }
387
-
388
- desc += '\n## 禁止事项\n';
389
- desc += '- 不先调用 `ext_skill` 获取参数就直接使用 `ext_call`\n';
390
473
  }
391
474
 
392
- if (!desc) {
393
- return '';
394
- }
395
-
396
- return desc.trim();
475
+ return scored
476
+ .sort((a, b) => b.score - a.score)
477
+ .slice(0, max)
478
+ .map((s) => s.name);
397
479
  }
398
480
 
399
- _convertSchemaToMarkdown(inputSchema) {
400
- if (!inputSchema) return null;
401
-
402
- let schema = inputSchema;
403
- if (inputSchema.jsonSchema) schema = inputSchema.jsonSchema;
404
- if (inputSchema.inputSchema) schema = inputSchema.inputSchema;
405
-
406
- const isZodSchema = typeof schema.shape === 'function' || (schema._def && schema._def.typeName);
407
- if (isZodSchema) {
408
- return zodSchemaToMarkdown(schema);
409
- }
410
-
411
- if (schema.properties) {
412
- const zodSchema = this._jsonSchemaToZod(schema);
413
- if (zodSchema) {
414
- return zodSchemaToMarkdown(zodSchema);
481
+ /** Levenshtein 编辑距离(带长度预筛) */
482
+ _levenshtein(a, b) {
483
+ if (Math.abs(a.length - b.length) > 5) return 99;
484
+ const m = a.length, n = b.length;
485
+ if (m === 0) return n;
486
+ if (n === 0) return m;
487
+ const dp = Array.from({ length: m + 1 }, (_, i) => {
488
+ const row = new Array(n + 1);
489
+ row[0] = i;
490
+ return row;
491
+ });
492
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
493
+ for (let i = 1; i <= m; i++) {
494
+ for (let j = 1; j <= n; j++) {
495
+ dp[i][j] = a[i - 1] === b[j - 1]
496
+ ? dp[i - 1][j - 1]
497
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
415
498
  }
416
499
  }
417
-
418
- return this._schemaToMarkdown(schema);
500
+ return dp[m][n];
419
501
  }
420
502
 
421
- _jsonSchemaToZod(jsonSchema) {
422
- if (!jsonSchema || !jsonSchema.properties) return null;
423
-
424
- try {
425
- const shape = {};
426
- const properties = jsonSchema.properties;
427
- const required = jsonSchema.required || [];
428
-
429
- for (const [key, prop] of Object.entries(properties)) {
430
- shape[key] = this._jsonSchemaPropToZod(prop, required.includes(key));
503
+ /** 按命名空间分组插件名 */
504
+ _groupByNamespace(plugins) {
505
+ const groups = {};
506
+ for (const p of plugins) {
507
+ let ns = 'plugin';
508
+ if (typeof p === 'string') {
509
+ if (p.startsWith('mcp:')) ns = 'mcp';
510
+ else if (p.startsWith('skill:')) ns = 'skill';
511
+ else if (p.startsWith('python:')) ns = 'python';
431
512
  }
432
-
433
- return z.object(shape);
434
- } catch (e) {
435
- return null;
513
+ if (!groups[ns]) groups[ns] = [];
514
+ groups[ns].push(p);
436
515
  }
516
+ return groups;
437
517
  }
438
518
 
439
- _jsonSchemaPropToZod(prop, isRequired) {
440
- if (prop.enum) {
441
- let zodType = z.string().enum(prop.enum);
442
- if (prop.nullable) zodType = zodType.nullable();
443
- return isRequired ? zodType : zodType.optional();
444
- }
445
-
446
- const type = prop.type || 'string';
447
- switch (type) {
448
- case 'string':
449
- return isRequired ? z.string() : z.string().optional();
450
- case 'number':
451
- case 'integer':
452
- return isRequired ? z.number() : z.number().optional();
453
- case 'boolean':
454
- return isRequired ? z.boolean() : z.boolean().optional();
455
- case 'array':
456
- return isRequired ? z.array(z.any()) : z.array(z.any()).optional();
457
- case 'object':
458
- if (prop.properties) {
459
- const nested = {};
460
- for (const [k, v] of Object.entries(prop.properties)) {
461
- nested[k] = this._jsonSchemaPropToZod(v, prop.required?.includes(k) || false);
462
- }
463
- return isRequired ? z.object(nested) : z.object(nested).optional();
464
- }
465
- return isRequired ? z.record(z.any()) : z.record(z.any()).optional();
466
- default:
467
- return isRequired ? z.any() : z.any().optional();
519
+ /** 格式化命名空间列表为多行文本 */
520
+ _formatNamespaceList(namespaces) {
521
+ const lines = [];
522
+ for (const [ns, items] of Object.entries(namespaces)) {
523
+ const head = items.slice(0, 5).join(', ');
524
+ const more = items.length > 5 ? ` (+${items.length - 5} more)` : '';
525
+ lines.push(` [${ns}] ${head}${more}`);
468
526
  }
527
+ return lines.join('\n');
469
528
  }
470
529
 
471
- _schemaToMarkdown(schema) {
472
- if (!schema || !schema.properties) return null;
473
-
474
- const props = schema.properties || {};
475
- const required = schema.required || [];
476
- let md = '';
477
-
478
- for (const [key, prop] of Object.entries(props)) {
479
- const isRequired = required.includes(key);
480
- const type = prop.type || 'any';
481
- const descText = prop.description || '';
482
- md += `- \`${key}\`${isRequired ? ' (必填)' : ''}: ${type} ${descText}\n`;
483
- }
484
- return md || null;
485
- }
530
+ // ============ 公开查询 API ============
486
531
 
487
532
  getExtensions() {
488
- return Array.from(this._extensions.entries()).map(([name, ext]) => ({
489
- name,
490
- description: ext.description,
491
- version: ext.version,
492
- tools: ext.tools,
493
- }));
533
+ return this._registry.getAll();
494
534
  }
495
535
 
496
- // 获取指定插件的所有工具
497
536
  getExtensionTools(pluginName) {
498
- const ext = this._extensions.get(pluginName);
499
- return ext ? ext.tools : [];
537
+ return this._registry.listTools(pluginName);
500
538
  }
501
539
 
502
- // 获取指定插件的指定工具
503
540
  getExtensionTool(pluginName, toolName) {
504
- const ext = this._extensions.get(pluginName);
505
- if (!ext) return null;
506
- return ext.tools.find(t => t.name === toolName) || null;
541
+ return this._registry.getTool(pluginName, toolName);
507
542
  }
508
543
 
509
- // 获取所有插件名称
510
544
  getExtensionNames() {
511
- return Array.from(this._extensions.keys());
545
+ return this._registry.listPlugins();
512
546
  }
513
547
 
548
+ // Skill 命令相关
514
549
  getSkillCommands() {
515
- const ext = this._extensions.get('skill');
516
- if (!ext) return [];
517
- return ext.tools
518
- .filter(t => t && t.name)
519
- .map(t => ({
520
- name: t.name.replace(/^([^:]+):(.*)$/, '$1:$2'),
521
- description: t.description || t.name,
522
- options: t._options || null,
523
- }));
550
+ return this._skillHelper?.getCommands() || [];
524
551
  }
525
552
 
526
553
  getSkillCommandsHelp() {
527
- const ext = this._extensions.get('skill');
528
- if (!ext || ext.tools.length === 0) return '';
529
-
530
- const lines = [];
531
- for (const tool of ext.tools) {
532
- if (!tool || !tool.name) continue;
533
- const parts = tool.name.split(':');
534
- if (parts.length === 2) {
535
- lines.push(`/${parts[0]}:${parts[1]} - ${tool.description || '无描述'}`);
536
- }
537
- }
538
- return lines.join('\n');
554
+ return this._skillHelper?.getCommandsHelp() || '';
539
555
  }
540
556
 
541
557
  async executeSkillCommand(command, args) {
542
- if (!command.includes(':')) return null;
543
- try {
544
- const result = await this._framework.executeTool('ext_call', {
545
- plugin: 'skill',
546
- tool: command,
547
- args: { command: args }
548
- });
549
- return result.success !== false ? result : null;
550
- } catch (err) {
551
- log.warn('Skill command failed:', err.message);
552
- return null;
553
- }
554
- }
555
-
556
- async reload(framework) {
557
- this._framework = framework;
558
- this._extensions.clear();
559
- const plugins = framework.pluginManager.getAll();
560
- for (const { instance: plugin } of plugins) {
561
- this._rescanPluginTools(plugin);
562
- }
563
- this._mcpExecutor = framework.pluginManager?.get('mcp') || null;
564
- this._invalidatePromptCaches(framework);
565
-
566
- await this._reloadMCPConfig(framework);
567
- }
568
-
569
- async _reloadMCPConfig(framework) {
570
- const fs = require('fs');
571
- const path = require('path');
572
- const mcpExecutor = framework.pluginManager?.get('mcp');
573
- if (!mcpExecutor) return;
574
-
575
- try {
576
- const configPath = path.resolve('.foliko/mcp_config.json');
577
- if (fs.existsSync(configPath)) {
578
- const configContent = fs.readFileSync(configPath, 'utf8');
579
- const config = JSON.parse(configContent);
580
- await mcpExecutor.reloadConfig(config);
581
- logger.info(' MCP config reloaded via extension-executor');
582
- }
583
- } catch (err) {
584
- logger.warn(' Failed to reload MCP config:', err.message);
585
- }
586
- }
587
-
588
- async uninstall(framework) {
589
- this._extensions.clear();
590
- this._framework = null;
558
+ return this._skillHelper?.executeCommand(command, args) || null;
591
559
  }
592
560
  }
593
561
 
594
- module.exports = ExtensionExecutorPlugin
562
+ module.exports = ExtensionExecutorPlugin;