foliko 1.1.69 → 1.1.71
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/cli/src/ui/chat-ui.js +75 -23
- package/package.json +1 -1
- package/plugins/extension-executor-plugin.js +50 -23
- package/plugins/feishu-plugin.js +9 -1
- package/plugins/qq-plugin.js +9 -1
- package/plugins/subagent-plugin.js +1 -1
- package/plugins/telegram-plugin.js +9 -1
- package/plugins/weixin-plugin.js +9 -1
- package/skills/skill-guide/SKILL.md +141 -2
- package/src/capabilities/skill-manager.js +20 -3
- package/src/core/subagent.js +2 -2
- package/subagent.md +2 -2
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -90,28 +90,25 @@ class ChatUI {
|
|
|
90
90
|
this.sessionId = options.sessionId || 'cli_default';
|
|
91
91
|
this.sessionPlugin = null;
|
|
92
92
|
|
|
93
|
-
// 获取 skill
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const extExecutor = agent.framework.pluginManager?.get('extension-executor');
|
|
97
|
-
if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
|
|
98
|
-
skillCommands.push(...extExecutor.getSkillCommands());
|
|
99
|
-
}
|
|
100
|
-
}
|
|
93
|
+
// 获取 skill 命令列表(保存到 this,供后续刷新)
|
|
94
|
+
this.skillCommands = [];
|
|
95
|
+
this._refreshSkillCommands(agent.framework);
|
|
101
96
|
|
|
102
|
-
const baseCommands = [
|
|
97
|
+
const baseCommands = this.baseCommands = [
|
|
103
98
|
{ name: "compress", description: "压缩记录" },
|
|
104
99
|
{ name: "clear", description: "清除记录" },
|
|
105
100
|
{ name: "exit", description: "退出" },
|
|
101
|
+
{ name: "help", description: "显示帮助信息" },
|
|
102
|
+
{ name: "reload", description: "重载技能" },
|
|
106
103
|
];
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
[...baseCommands, ...skillCommands],
|
|
105
|
+
this.autocompleteProvider = new CombinedAutocompleteProvider(
|
|
106
|
+
[...baseCommands, ...this.skillCommands],
|
|
110
107
|
process.cwd(),
|
|
111
108
|
);
|
|
112
|
-
|
|
109
|
+
|
|
113
110
|
const editor=this.editor = new Editor(this.tui, editorTheme,{paddingX:1});
|
|
114
|
-
editor.setAutocompleteProvider(autocompleteProvider);
|
|
111
|
+
this.editor.setAutocompleteProvider(this.autocompleteProvider);
|
|
115
112
|
this.editor.onSubmit=this.handleOnSubmit.bind(this)
|
|
116
113
|
this.tui.addChild(this.editor);
|
|
117
114
|
this.tui.setFocus(this.editor);
|
|
@@ -230,6 +227,13 @@ class ChatUI {
|
|
|
230
227
|
}
|
|
231
228
|
};
|
|
232
229
|
agent.framework.on('agent:usage', this._usageHandler);
|
|
230
|
+
|
|
231
|
+
// 监听 skill 热重载事件,刷新 autocomplete
|
|
232
|
+
this._skillReloadedHandler = () => {
|
|
233
|
+
this._refreshSkillCommands(agent.framework);
|
|
234
|
+
this._refreshAutocomplete();
|
|
235
|
+
};
|
|
236
|
+
agent.framework.on('skill:reloaded', this._skillReloadedHandler);
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
|
|
@@ -254,6 +258,10 @@ class ChatUI {
|
|
|
254
258
|
this.agent.framework.off('agent:usage', this._usageHandler);
|
|
255
259
|
this._usageHandler = null;
|
|
256
260
|
}
|
|
261
|
+
if (this._skillReloadedHandler && this.agent.framework) {
|
|
262
|
+
this.agent.framework.off('skill:reloaded', this._skillReloadedHandler);
|
|
263
|
+
this._skillReloadedHandler = null;
|
|
264
|
+
}
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
clear_message_done(){
|
|
@@ -305,6 +313,20 @@ class ChatUI {
|
|
|
305
313
|
return true;
|
|
306
314
|
}
|
|
307
315
|
|
|
316
|
+
if (cmd === '/reload') {
|
|
317
|
+
try {
|
|
318
|
+
const result = await this.agent.framework.executeTool('reloadSkills', {});
|
|
319
|
+
if (result.success !== false) {
|
|
320
|
+
this.create_message(`${colored('[提示]', CYAN)} 技能已重载\n`,chalk.dim('● '))
|
|
321
|
+
} else {
|
|
322
|
+
this.create_message(`${colored('[错误]', RED)} 重载失败: ${result.error}\n`,chalk.dim('● '))
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
this.create_message(`${colored('[错误]', RED)} 重载失败: ${err.message}\n`,chalk.dim('● '))
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
308
330
|
// /help 命令显示所有注册的命令
|
|
309
331
|
if (cmd === '/help') {
|
|
310
332
|
const { getCommandRegistry } = require('../../../src/core/command-registry');
|
|
@@ -312,17 +334,18 @@ class ChatUI {
|
|
|
312
334
|
const commands = registry.getAllCommands();
|
|
313
335
|
|
|
314
336
|
let helpText = `${colored('[帮助]', CYAN)} 可用命令:\n`;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
this.baseCommands.forEach(c => {
|
|
338
|
+
helpText += ` ${colored('/' + c.name, CYAN)} - ${c.description}\n`;
|
|
339
|
+
})
|
|
340
|
+
|
|
318
341
|
|
|
319
342
|
// 添加 skill 命令
|
|
320
343
|
const extExecutor = this.agent.framework?.pluginManager?.get('extension-executor');
|
|
321
|
-
if (extExecutor && typeof extExecutor.
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
344
|
+
if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
|
|
345
|
+
const skillCommands = extExecutor.getSkillCommands();
|
|
346
|
+
if (skillCommands.length > 0) {
|
|
324
347
|
helpText += `\n${colored('[技能命令]', CYAN)}\n`;
|
|
325
|
-
helpText +=
|
|
348
|
+
helpText += skillCommands.map(cmd => ` ${colored('/' + cmd.name, CYAN)} - ${cmd.description || '无描述'}`).join('\n') + '\n';
|
|
326
349
|
}
|
|
327
350
|
}
|
|
328
351
|
|
|
@@ -350,7 +373,10 @@ class ChatUI {
|
|
|
350
373
|
args: { args: cmdArgs }
|
|
351
374
|
});
|
|
352
375
|
if (result.success !== false) {
|
|
353
|
-
this.create_message(`${colored('[
|
|
376
|
+
this.create_message(`${colored('[指令] '+cmdName, CYAN)}\n\n ${result.data || result}\n`, chalk.green('● '));
|
|
377
|
+
return true;
|
|
378
|
+
}else{
|
|
379
|
+
this.create_message(`${colored('[指令] '+cmdName, RED)}\n\n ${result.error || '未知错误'}\n`, chalk.red('● '));
|
|
354
380
|
return true;
|
|
355
381
|
}
|
|
356
382
|
} catch (err) {
|
|
@@ -371,10 +397,10 @@ class ChatUI {
|
|
|
371
397
|
framework: this.agent.framework,
|
|
372
398
|
};
|
|
373
399
|
registry.execute(cmdName, cmdArgs, context).catch(err => {
|
|
374
|
-
this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.
|
|
400
|
+
this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.red('● '));
|
|
375
401
|
});
|
|
376
402
|
} catch (err) {
|
|
377
|
-
this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.
|
|
403
|
+
this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.red('● '));
|
|
378
404
|
}
|
|
379
405
|
return true;
|
|
380
406
|
}
|
|
@@ -557,6 +583,32 @@ class ChatUI {
|
|
|
557
583
|
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
|
|
558
584
|
}
|
|
559
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* 刷新 skill 命令列表(用于热重载)
|
|
588
|
+
*/
|
|
589
|
+
_refreshSkillCommands(framework) {
|
|
590
|
+
this.skillCommands = [];
|
|
591
|
+
if (!framework) return;
|
|
592
|
+
const extExecutor = framework.pluginManager?.get('extension-executor');
|
|
593
|
+
if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
|
|
594
|
+
this.skillCommands.push(...extExecutor.getSkillCommands());
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 刷新自动补全提供者(skill 命令热重载时调用)
|
|
600
|
+
*/
|
|
601
|
+
_refreshAutocomplete() {
|
|
602
|
+
if (!this.editor) return;
|
|
603
|
+
;
|
|
604
|
+
// 重新构建 CombinedAutocompleteProvider 并更新引用
|
|
605
|
+
const { CombinedAutocompleteProvider } = require('@earendil-works/pi-tui');
|
|
606
|
+
this.autocompleteProvider = new CombinedAutocompleteProvider(
|
|
607
|
+
[...this.baseCommands, ...this.skillCommands],
|
|
608
|
+
process.cwd()
|
|
609
|
+
);
|
|
610
|
+
this.editor.setAutocompleteProvider(this.autocompleteProvider);
|
|
611
|
+
}
|
|
560
612
|
/**
|
|
561
613
|
* 刷新流式输出缓冲区:合并高频 chunk 后统一渲染,减少 Markdown 重复解析
|
|
562
614
|
*/
|
package/package.json
CHANGED
|
@@ -42,6 +42,11 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
42
42
|
log.debug('Framework ready, rescanning all plugin tools...');
|
|
43
43
|
const plugins = framework.pluginManager.getAll();
|
|
44
44
|
for (const { instance: plugin } of plugins) {
|
|
45
|
+
// skill-manager 插件的工具已通过 Skill.registerCommands 直接注册,
|
|
46
|
+
// 不需要再通过 _scanPluginTools 扫描,避免重复注册
|
|
47
|
+
if (plugin.name === 'skill-manager') {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
45
50
|
this._scanPluginTools(plugin);
|
|
46
51
|
}
|
|
47
52
|
// 刷新所有 Agent 的扩展提示词
|
|
@@ -53,8 +58,10 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
53
58
|
|
|
54
59
|
/**
|
|
55
60
|
* 扫描插件的 this.tools 并注册到扩展系统
|
|
61
|
+
* @param {Object} plugin - 插件实例
|
|
62
|
+
* @param {string} [extName] - 可选的扩展名(用于统一命名)
|
|
56
63
|
*/
|
|
57
|
-
_scanPluginTools(plugin) {
|
|
64
|
+
_scanPluginTools(plugin, extName) {
|
|
58
65
|
if (!plugin || !plugin.tools || typeof plugin.tools !== 'object') {
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
@@ -62,9 +69,12 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
62
69
|
const pluginName = plugin.name;
|
|
63
70
|
if (!pluginName) return;
|
|
64
71
|
|
|
72
|
+
// 确定扩展名:优先使用 extName,否则使用 pluginName
|
|
73
|
+
const useExtName = extName || pluginName;
|
|
74
|
+
|
|
65
75
|
// 获取插件信息
|
|
66
76
|
const pluginInfo = {
|
|
67
|
-
name:
|
|
77
|
+
name: useExtName,
|
|
68
78
|
description: plugin.description || '',
|
|
69
79
|
version: plugin.version || '1.0.0',
|
|
70
80
|
};
|
|
@@ -73,7 +83,7 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
73
83
|
for (const [toolName, toolDef] of Object.entries(plugin.tools)) {
|
|
74
84
|
if (!toolDef || typeof toolDef !== 'object') continue;
|
|
75
85
|
|
|
76
|
-
this.registerTool(
|
|
86
|
+
this.registerTool(useExtName, pluginInfo, {
|
|
77
87
|
name: toolName,
|
|
78
88
|
description: toolDef.description || '',
|
|
79
89
|
inputSchema: toolDef.inputSchema,
|
|
@@ -81,7 +91,7 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
81
91
|
});
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
log.debug(` Scanned ${Object.keys(plugin.tools).length} tools from plugin '${pluginName}'`);
|
|
94
|
+
log.debug(` Scanned ${Object.keys(plugin.tools).length} tools from plugin '${pluginName}' (as '${useExtName}')`);
|
|
85
95
|
|
|
86
96
|
// 刷新所有 Agent 的扩展提示词
|
|
87
97
|
if (this._framework && this._framework._ready) {
|
|
@@ -100,21 +110,27 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
100
110
|
const pluginName = plugin.name;
|
|
101
111
|
if (!pluginName) return;
|
|
102
112
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
// skill-manager 插件使用 'skill' 作为扩展名(保持与 Skill.registerCommands 一致)
|
|
114
|
+
const extName = pluginName === 'skill-manager' ? 'skill' : pluginName;
|
|
115
|
+
|
|
116
|
+
// 先移除旧工具(同时检查 'skill' 和 'skill-manager' 两个扩展名)
|
|
117
|
+
const keysToRemove = [extName, pluginName];
|
|
118
|
+
for (const key of keysToRemove) {
|
|
119
|
+
if (this._extensions.has(key)) {
|
|
120
|
+
const ext = this._extensions.get(key);
|
|
121
|
+
for (const tool of ext.tools || []) {
|
|
122
|
+
// 从 toolRouter 移除
|
|
123
|
+
if (this._framework?.toolRouter) {
|
|
124
|
+
this._framework.toolRouter.unregister(`ext_${tool.name}`);
|
|
125
|
+
}
|
|
110
126
|
}
|
|
127
|
+
this._extensions.delete(key);
|
|
111
128
|
}
|
|
112
|
-
this._extensions.delete(pluginName);
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
//
|
|
116
|
-
this._scanPluginTools(plugin);
|
|
117
|
-
log.info(` Rescanned tools from plugin '${pluginName}'`);
|
|
131
|
+
// 重新扫描工具,使用正确的扩展名
|
|
132
|
+
this._scanPluginTools(plugin, extName);
|
|
133
|
+
log.info(` Rescanned tools from plugin '${pluginName}' (as '${extName}')`);
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
async start(framework) {
|
|
@@ -237,7 +253,7 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
237
253
|
name,
|
|
238
254
|
description: ext.description,
|
|
239
255
|
version: ext.version,
|
|
240
|
-
tools: ext.tools.map((t) => ({ name: t.name, description: t.description })),
|
|
256
|
+
tools: ext.tools.filter(t => t && t.name).map((t) => ({ name: t.name, description: t.description })),
|
|
241
257
|
});
|
|
242
258
|
}
|
|
243
259
|
return { success: true, data: extensions };
|
|
@@ -275,6 +291,11 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
275
291
|
}
|
|
276
292
|
|
|
277
293
|
registerTool(pluginName, pluginInfo, toolDef) {
|
|
294
|
+
if (!toolDef || !toolDef.name) {
|
|
295
|
+
log.warn(`[ExtensionExecutor] Invalid tool definition for '${pluginName}'`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
278
299
|
if (!this._extensions.has(pluginName)) {
|
|
279
300
|
this._extensions.set(pluginName, {
|
|
280
301
|
name: pluginInfo.name || pluginName,
|
|
@@ -392,7 +413,7 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
392
413
|
if (this._extensions.size > 0 || (this._mcpExecutor && Object.keys(this._mcpExecutor.tools || {}).length > 0)) {
|
|
393
414
|
desc += '## 【Extensions】扩展插件\n\n';
|
|
394
415
|
desc += '**使用流程(必须按顺序执行):**\n';
|
|
395
|
-
desc += '1. 调用 `ext_skill({ plugin: "skill" })` 获取技能命令的详细参数\n';
|
|
416
|
+
desc += '1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `loadSkill({ skill: "<skill_name>" })` 获取技能命令的详细参数\n';
|
|
396
417
|
desc += '2. 根据返回的参数定义,使用 `ext_call({ plugin: "skill", tool: "技能名:命令名", args: {...} })` 调用\n\n';
|
|
397
418
|
desc += '> **警告**:禁止在未执行第1步获取参数的情况下直接调用 `ext_call`!\n\n';
|
|
398
419
|
|
|
@@ -401,6 +422,8 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
401
422
|
// Skill 命令按技能名分组显示
|
|
402
423
|
const toolsBySkill = {};
|
|
403
424
|
for (const tool of ext.tools) {
|
|
425
|
+
// 跳过无效工具
|
|
426
|
+
if (!tool || !tool.name) continue;
|
|
404
427
|
// 命令格式: skillname:cmdname
|
|
405
428
|
const parts = tool.name.split(':');
|
|
406
429
|
const skillName = parts[0];
|
|
@@ -425,7 +448,8 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
425
448
|
} else {
|
|
426
449
|
desc += `### ${ext.name || name}\n`;
|
|
427
450
|
desc += `${ext.description || '无描述'}\n`;
|
|
428
|
-
|
|
451
|
+
const validTools = ext.tools.filter(t => t && t.name);
|
|
452
|
+
desc += `**工具:** ${validTools.map(t => `\`${t.name}\``).join(', ')}\n\n`;
|
|
429
453
|
}
|
|
430
454
|
}
|
|
431
455
|
|
|
@@ -437,7 +461,7 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
437
461
|
}
|
|
438
462
|
|
|
439
463
|
desc += '\n## 禁止事项\n';
|
|
440
|
-
desc += '- 不先调用 `ext_skill` 获取参数就直接使用 `ext_call`\n';
|
|
464
|
+
desc += '- 不先调用 `ext_skill/loadSkill` 获取参数就直接使用 `ext_call`\n';
|
|
441
465
|
}
|
|
442
466
|
|
|
443
467
|
if (!desc) {
|
|
@@ -568,10 +592,12 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
568
592
|
getSkillCommands() {
|
|
569
593
|
const ext = this._extensions.get('skill');
|
|
570
594
|
if (!ext) return [];
|
|
571
|
-
return ext.tools
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
595
|
+
return ext.tools
|
|
596
|
+
.filter(t => t && t.name)
|
|
597
|
+
.map(t => ({
|
|
598
|
+
name: t.name.replace(/^([^:]+):(.*)$/, '$1:$2'),
|
|
599
|
+
description: t.description || t.name,
|
|
600
|
+
}));
|
|
575
601
|
}
|
|
576
602
|
|
|
577
603
|
/**
|
|
@@ -583,6 +609,7 @@ class ExtensionExecutorPlugin extends Plugin {
|
|
|
583
609
|
|
|
584
610
|
const lines = [];
|
|
585
611
|
for (const tool of ext.tools) {
|
|
612
|
+
if (!tool || !tool.name) continue;
|
|
586
613
|
const parts = tool.name.split(':');
|
|
587
614
|
if (parts.length === 2) {
|
|
588
615
|
lines.push(`/${parts[0]}:${parts[1]} - ${tool.description || '无描述'}`);
|
package/plugins/feishu-plugin.js
CHANGED
|
@@ -210,6 +210,14 @@ class FeishuPlugin extends Plugin {
|
|
|
210
210
|
case 'compress':
|
|
211
211
|
await this._compressSessionContext(openId)
|
|
212
212
|
break
|
|
213
|
+
case 'reload':
|
|
214
|
+
try {
|
|
215
|
+
await this._framework.executeTool('reloadSkills', {})
|
|
216
|
+
await this._sendMessage(openId, '✅ 技能已重载', originalMsg)
|
|
217
|
+
} catch (err) {
|
|
218
|
+
await this._sendMessage(openId, '❌ 重载失败: ' + err.message, originalMsg)
|
|
219
|
+
}
|
|
220
|
+
break
|
|
213
221
|
case 'history':
|
|
214
222
|
if (this._sessionPlugin) {
|
|
215
223
|
const session = this._sessionPlugin.getSession(`feishu_${openId}`)
|
|
@@ -512,7 +520,7 @@ class FeishuPlugin extends Plugin {
|
|
|
512
520
|
const extExecutor = this._framework?.pluginManager?.get('extension-executor')
|
|
513
521
|
const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
|
|
514
522
|
|
|
515
|
-
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
|
|
523
|
+
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
|
|
516
524
|
|
|
517
525
|
if (skillHelp) {
|
|
518
526
|
helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
|
package/plugins/qq-plugin.js
CHANGED
|
@@ -701,6 +701,14 @@ class QQPlugin extends Plugin {
|
|
|
701
701
|
case 'compress':
|
|
702
702
|
await this._compressSessionContext(openid)
|
|
703
703
|
break
|
|
704
|
+
case 'reload':
|
|
705
|
+
try {
|
|
706
|
+
await this._framework.executeTool('reloadSkills', {})
|
|
707
|
+
await this._sendMessage(openid, '✅ 技能已重载')
|
|
708
|
+
} catch (err) {
|
|
709
|
+
await this._sendMessage(openid, '❌ 重载失败: ' + err.message)
|
|
710
|
+
}
|
|
711
|
+
break
|
|
704
712
|
case 'history':
|
|
705
713
|
const sessionInfo = this._getSessionAgent(openid)
|
|
706
714
|
if (sessionInfo && this._framework) {
|
|
@@ -752,7 +760,7 @@ class QQPlugin extends Plugin {
|
|
|
752
760
|
const extExecutor = this._framework?.pluginManager?.get('extension-executor')
|
|
753
761
|
const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
|
|
754
762
|
|
|
755
|
-
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
|
|
763
|
+
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
|
|
756
764
|
|
|
757
765
|
if (skillHelp) {
|
|
758
766
|
helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
|
|
@@ -322,6 +322,14 @@ class TelegramPlugin extends Plugin {
|
|
|
322
322
|
case 'compress':
|
|
323
323
|
await this._compressSessionContext(chatId, msg.message_id)
|
|
324
324
|
break
|
|
325
|
+
case 'reload':
|
|
326
|
+
try {
|
|
327
|
+
await this._framework.executeTool('reloadSkills', {})
|
|
328
|
+
await this._sendMessage(chatId, '✅ 技能已重载', msg.message_id)
|
|
329
|
+
} catch (err) {
|
|
330
|
+
await this._sendMessage(chatId, '❌ 重载失败: ' + err.message, msg.message_id)
|
|
331
|
+
}
|
|
332
|
+
break
|
|
325
333
|
case 'history':
|
|
326
334
|
const sessionInfo = this._getSessionAgent(chatId)
|
|
327
335
|
if (sessionInfo && this._framework) {
|
|
@@ -525,7 +533,7 @@ class TelegramPlugin extends Plugin {
|
|
|
525
533
|
const extExecutor = this._framework?.pluginManager?.get('extension-executor')
|
|
526
534
|
const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
|
|
527
535
|
|
|
528
|
-
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
|
|
536
|
+
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
|
|
529
537
|
|
|
530
538
|
if (skillHelp) {
|
|
531
539
|
helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
|
package/plugins/weixin-plugin.js
CHANGED
|
@@ -601,6 +601,14 @@ class WeixinPlugin extends Plugin {
|
|
|
601
601
|
case 'compress':
|
|
602
602
|
await this._compressSessionContext(userId)
|
|
603
603
|
break
|
|
604
|
+
case 'reload':
|
|
605
|
+
try {
|
|
606
|
+
await this._framework.executeTool('reloadSkills', {})
|
|
607
|
+
await this._sendMessageBatch(originalMsg, userId, '✅ 技能已重载', true)
|
|
608
|
+
} catch (err) {
|
|
609
|
+
await this._sendMessageBatch(originalMsg, userId, '❌ 重载失败: ' + err.message, true)
|
|
610
|
+
}
|
|
611
|
+
break
|
|
604
612
|
case 'history':
|
|
605
613
|
const sessionInfo = this._getSessionAgent(userId)
|
|
606
614
|
if (sessionInfo && this._framework) {
|
|
@@ -634,7 +642,7 @@ class WeixinPlugin extends Plugin {
|
|
|
634
642
|
const extExecutor = this._framework?.pluginManager?.get('extension-executor')
|
|
635
643
|
const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
|
|
636
644
|
|
|
637
|
-
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
|
|
645
|
+
let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
|
|
638
646
|
|
|
639
647
|
if (skillHelp) {
|
|
640
648
|
helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skill-guide
|
|
3
|
-
description: 技能安装与开发指南。当用户询问"如何添加技能"、"自定义技能"、"安装技能"
|
|
3
|
+
description: 技能安装与开发指南。当用户询问"如何添加技能"、"自定义技能"、"安装技能"、"创建技能"时立即调用。如果技能有命令,必须在 SKILL.md 中添加 ## 命令 章节详细说明。
|
|
4
4
|
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -84,7 +84,146 @@ frontmatter 之后是技能的正文内容,支持 Markdown 格式,包含:
|
|
|
84
84
|
1. 在 `.foliko/skills/` 下创建技能文件夹
|
|
85
85
|
2. 创建 `SKILL.md` 文件,包含完整的 frontmatter
|
|
86
86
|
3. 在正文中详细描述技能的功能和使用方法
|
|
87
|
-
4.
|
|
87
|
+
4. **如有命令**,在 SKILL.md 正文中添加 `## 命令` 章节,详细说明每个命令的用法
|
|
88
|
+
5. 创建 `index.js` 文件导出命令(可选)
|
|
89
|
+
6. 调用 `reloadSkills` 工具重载所有技能
|
|
90
|
+
|
|
91
|
+
### SKILL.md 命令章节示例
|
|
92
|
+
|
|
93
|
+
如果技能有命令,必须在 SKILL.md 正文添加 `## 命令` 章节:
|
|
94
|
+
|
|
95
|
+
```markdown
|
|
96
|
+
## 命令
|
|
97
|
+
|
|
98
|
+
### /my-skill:greet
|
|
99
|
+
|
|
100
|
+
打招呼命令。
|
|
101
|
+
|
|
102
|
+
**参数:**
|
|
103
|
+
- `args`(可选):名字,如 `/my-skill:greet Alice`
|
|
104
|
+
|
|
105
|
+
**示例:**
|
|
106
|
+
- `/my-skill:greet` → "Hello World"
|
|
107
|
+
- `/my-skill:greet Alice` → "Hello Alice"
|
|
108
|
+
|
|
109
|
+
### /my-skill:split
|
|
110
|
+
|
|
111
|
+
切割会话命令。
|
|
112
|
+
|
|
113
|
+
**参数:**
|
|
114
|
+
- `-t, --tokens <value>`:保留 token 数(默认 4000)
|
|
115
|
+
- `-k, --keep-recent <value>`:保留最近 token(默认 1000)
|
|
116
|
+
- `[args...]`:位置参数
|
|
117
|
+
|
|
118
|
+
**示例:**
|
|
119
|
+
- `/my-skill:split` → 使用默认参数
|
|
120
|
+
- `/my-skill:split -t 8000 extra` → tokens=8000,extra 作为位置参数
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### index.js 格式
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
module.exports = [
|
|
127
|
+
{
|
|
128
|
+
name: 'greet', // 命令名
|
|
129
|
+
description: '打招呼', // 命令描述
|
|
130
|
+
execute: async (args, ctx) => {
|
|
131
|
+
// args: 传入的参数(字符串)
|
|
132
|
+
// ctx: 上下文对象 { sessionId, agent, framework }
|
|
133
|
+
return 'Hello ' + (args || 'World');
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'split',
|
|
138
|
+
description: '切割会话',
|
|
139
|
+
// 简洁方式:定义选项数组,自动使用 Commander.js 解析
|
|
140
|
+
options: [
|
|
141
|
+
{ flags: '-t, --tokens <value>', description: '保留 token 数', defaultValue: '4000' },
|
|
142
|
+
{ flags: '-k, --keep-recent <value>', description: '保留最近 token', defaultValue: '1000' }
|
|
143
|
+
],
|
|
144
|
+
execute: async (parsedArgs, ctx) => {
|
|
145
|
+
// parsedArgs: { tokens: "4000", keepRecent: "1000", args: [...] }
|
|
146
|
+
return `Split: tokens=${parsedArgs.tokens}`;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'status',
|
|
151
|
+
description: '显示状态',
|
|
152
|
+
execute: async (args, ctx) => {
|
|
153
|
+
return JSON.stringify({ session: ctx.sessionId });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**参数解析说明:**
|
|
160
|
+
|
|
161
|
+
- 不提供 `options` 时,`args` 是原始参数字符串
|
|
162
|
+
- 提供 `options` 数组时,自动使用 Commander.js 解析,`execute` 收到解析后的对象
|
|
163
|
+
- 对象格式:`{ tokens, keepRecent, args: [...] }`
|
|
164
|
+
|
|
165
|
+
### 命令调用方式
|
|
166
|
+
|
|
167
|
+
技能命令通过 `ext_call` 工具调用,格式为 `/skillname:commandname`:
|
|
168
|
+
|
|
169
|
+
| 渠道 | 调用方式 |
|
|
170
|
+
|------|---------|
|
|
171
|
+
| CLI | 输入 `/skillname:cmdname` |
|
|
172
|
+
| 飞书 | 发送 `/skillname:cmdname` |
|
|
173
|
+
| 微信 | 发送 `/skillname:cmdname` |
|
|
174
|
+
| Telegram | 发送 `/skillname:cmdname` |
|
|
175
|
+
| QQ | 发送 `/skillname:cmdname` |
|
|
176
|
+
|
|
177
|
+
### 使用流程
|
|
178
|
+
|
|
179
|
+
1. **查看可用命令**:发送 `/help` 或 `/start` 查看帮助,其中列出所有技能命令
|
|
180
|
+
2. **执行命令**:发送 `/skillname:cmdname` 即可执行命令
|
|
181
|
+
|
|
182
|
+
### 示例:test-skill
|
|
183
|
+
|
|
184
|
+
假设有 `test-skill` 技能,其 `index.js` 导出:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
module.exports = [
|
|
188
|
+
{ name: 'greet', description: '打招呼', execute: async (args) => 'Hello ' + (args || 'World') },
|
|
189
|
+
{ name: 'echo', description: '回声命令', execute: async (args) => 'Echo: ' + args }
|
|
190
|
+
];
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
对应的 SKILL.md 应包含:
|
|
194
|
+
|
|
195
|
+
```markdown
|
|
196
|
+
## 命令
|
|
197
|
+
|
|
198
|
+
### /test-skill:greet
|
|
199
|
+
|
|
200
|
+
打招呼命令。
|
|
201
|
+
|
|
202
|
+
**参数:**
|
|
203
|
+
- `args`(可选):名字
|
|
204
|
+
|
|
205
|
+
**示例:**
|
|
206
|
+
- `/test-skill:greet` → "Hello World"
|
|
207
|
+
- `/test-skill:greet Alice` → "Hello Alice"
|
|
208
|
+
|
|
209
|
+
### /test-skill:echo
|
|
210
|
+
|
|
211
|
+
回声命令,将输入原样返回。
|
|
212
|
+
|
|
213
|
+
**参数:**
|
|
214
|
+
- `args`:要回显的文本
|
|
215
|
+
|
|
216
|
+
**示例:**
|
|
217
|
+
- `/test-skill:echo Hello` → "Echo: Hello"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 自动注册
|
|
221
|
+
|
|
222
|
+
技能命令会在以下时机自动注册:
|
|
223
|
+
- 框架启动时加载技能
|
|
224
|
+
- 调用 `reloadSkills` 工具重载后
|
|
225
|
+
|
|
226
|
+
注册后即可在所有渠道使用 `/技能名:命令名` 格式调用。
|
|
88
227
|
|
|
89
228
|
## 技能重载
|
|
90
229
|
|
|
@@ -115,6 +115,7 @@ class Skill {
|
|
|
115
115
|
this.content = content;
|
|
116
116
|
this._framework = null;
|
|
117
117
|
this._commands = [];
|
|
118
|
+
this.tools = {}; // 用于 extension-executor 扫描
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
/**
|
|
@@ -195,8 +196,7 @@ class Skill {
|
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
extExecutor.registerTool('skill', skillInfo, {
|
|
199
|
+
const toolDef = {
|
|
200
200
|
name: fullName,
|
|
201
201
|
description: cmd.description || `Skill command: ${fullName}`,
|
|
202
202
|
inputSchema: z.object({
|
|
@@ -215,7 +215,13 @@ class Skill {
|
|
|
215
215
|
}
|
|
216
216
|
return await handler(parsedArgs, ctx);
|
|
217
217
|
},
|
|
218
|
-
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// 注册到 extension 系统,通过 ext_call 调用
|
|
221
|
+
extExecutor.registerTool('skill', skillInfo, toolDef);
|
|
222
|
+
|
|
223
|
+
// 同时添加到 this.tools,供 extension-executor 的 _scanPluginTools 扫描
|
|
224
|
+
this.tools[fullName] = toolDef;
|
|
219
225
|
|
|
220
226
|
this._commands.push(fullName);
|
|
221
227
|
}
|
|
@@ -247,6 +253,7 @@ class SkillManagerPlugin extends Plugin {
|
|
|
247
253
|
: [config.skillsDir || '.foliko/skills', 'skills'];
|
|
248
254
|
this._skills = new Map();
|
|
249
255
|
this._loaded = false;
|
|
256
|
+
this.tools = {}; // 用于 extension-executor 扫描(由 Skill.registerCommands 填充)
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
install(framework) {
|
|
@@ -626,6 +633,9 @@ class SkillManagerPlugin extends Plugin {
|
|
|
626
633
|
skill.install(this._framework);
|
|
627
634
|
skill.registerCommands(); // 注册 skill 的命令
|
|
628
635
|
|
|
636
|
+
// 将 skill 的命令添加到 SkillManagerPlugin.tools,供 extension-executor 扫描
|
|
637
|
+
Object.assign(this.tools, skill.tools);
|
|
638
|
+
|
|
629
639
|
// 扫描 references 子目录(按需加载的附加文档)
|
|
630
640
|
const references = this._scanReferences(skillPath);
|
|
631
641
|
|
|
@@ -822,6 +832,13 @@ class SkillManagerPlugin extends Plugin {
|
|
|
822
832
|
this._loaded = false;
|
|
823
833
|
this._framework = framework;
|
|
824
834
|
this._loadAllSkills();
|
|
835
|
+
|
|
836
|
+
// 通知 extension-executor 重载 skill 命令(通过监听器自动处理)
|
|
837
|
+
// 注意:skill 命令已经在 registerCommands() 中直接注册到 extension-executor,
|
|
838
|
+
// 不需要再调用 _rescanPluginTools
|
|
839
|
+
|
|
840
|
+
// 通知 UI 刷新 skill 命令(用于 autocomplete 热重载)
|
|
841
|
+
framework.emit('skill:reloaded', { skills: Array.from(this._skills.keys()) });
|
|
825
842
|
}
|
|
826
843
|
|
|
827
844
|
uninstall(framework) {
|
package/src/core/subagent.js
CHANGED
|
@@ -40,7 +40,7 @@ class Subagent extends EventEmitter {
|
|
|
40
40
|
this.providerOptions = config.providerOptions || {};
|
|
41
41
|
this.framework = config.framework || null;
|
|
42
42
|
|
|
43
|
-
this.defaulTools = ['ext_skill','ext_call','subagent_call','mcp_call','mcp_tool_schema'];
|
|
43
|
+
this.defaulTools = ['ext_skill','loadSkill','ext_call','subagent_call','mcp_call','mcp_tool_schema'];
|
|
44
44
|
|
|
45
45
|
this.bindTools = {
|
|
46
46
|
read: 'read_file',
|
|
@@ -282,7 +282,7 @@ class Subagent extends EventEmitter {
|
|
|
282
282
|
lines.push(`- 处理本职外的任务`);
|
|
283
283
|
lines.push(`- 代替其他子Agent执行操作`);
|
|
284
284
|
lines.push(`- 在回复中透露你的内部实现细节`);
|
|
285
|
-
lines.push(`- 不先调用 ext_skill 就直接使用 ext_call`);
|
|
285
|
+
lines.push(`- 不先调用 ext_skill/loadSkill 就直接使用 ext_call`);
|
|
286
286
|
|
|
287
287
|
return lines.join('\n');
|
|
288
288
|
}
|
package/subagent.md
CHANGED
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
## 【Extensions】扩展插件
|
|
112
112
|
|
|
113
113
|
**使用流程(必须按顺序执行):**
|
|
114
|
-
1. 调用
|
|
114
|
+
1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `loadSkill({ 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` 获取参数就直接使用 `ext_call`
|
|
170
|
+
- 不先调用 `ext_skill/loadSkill` 获取参数就直接使用 `ext_call`
|