foliko 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +4 -1
- package/.cli_default_systemPrompt.md +291 -0
- package/CLAUDE.md +3 -0
- package/README.md +20 -3
- package/docs/architecture.md +34 -2
- package/docs/extensions.md +199 -0
- package/docs/migration.md +100 -0
- package/docs/public-api.md +1180 -6
- package/docs/usage.md +122 -30
- package/package.json +1 -1
- package/plugins/core/audit/index.js +1 -1
- package/plugins/core/default/bootstrap.js +65 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +538 -93
- package/plugins/core/sub-agent/PROMPT.md +10 -0
- package/plugins/core/sub-agent/index.js +36 -3
- package/plugins/core/think/index.js +1 -0
- package/plugins/core/workflow/index.js +106 -22
- package/plugins/executors/data-splitter/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +5 -4
- package/plugins/executors/extension/extension-registry.js +145 -0
- package/plugins/executors/extension/index.js +405 -437
- package/plugins/executors/extension/prompt-builder.js +359 -0
- package/plugins/executors/extension/skill-helper.js +143 -0
- package/plugins/messaging/feishu/index.js +5 -3
- package/plugins/messaging/qq/index.js +6 -4
- package/plugins/messaging/telegram/index.js +6 -3
- package/plugins/messaging/weixin/index.js +5 -3
- package/plugins/tools/PROMPT.md +26 -0
- package/plugins/tools/index.js +6 -5
- package/skills/foliko/AGENTS.md +196 -43
- package/skills/foliko/SKILL.md +157 -28
- package/skills/mcp/SKILL.md +77 -118
- package/skills/plugins/SKILL.md +89 -3
- package/skills/python/SKILL.md +57 -39
- package/skills/skill-guide/SKILL.md +42 -34
- package/skills/workflows/SKILL.md +224 -9
- package/skills/workflows/workflow-troubleshooting/SKILL.md +221 -281
- package/src/agent/chat.js +48 -27
- package/src/agent/main.js +34 -13
- package/src/agent/prompt-registry.js +133 -87
- package/src/agent/prompts/PROMPT.md +3 -0
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui-old.js +5 -2
- package/src/cli/ui/chat-ui.js +5 -2
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/logger.js +2 -2
- package/src/context/compressor.js +6 -2
- package/src/executors/mcp-executor.js +105 -125
- package/src/framework/framework.js +632 -6
- package/src/index.js +4 -0
- package/src/plugin/base.js +913 -10
- package/src/plugin/manager.js +29 -8
- package/src/tool/schema.js +32 -9
- package/tests/core/plugin-prompts.test.js +13 -13
- package/tests/core/prompt-registry.test.js +59 -51
- package/website/index.html +821 -0
|
@@ -130,6 +130,205 @@ class SkillMetadata {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* 从 commander.js 的 flags 字符串中提取字段名
|
|
135
|
+
* "--video-id <id>" → "videoId"
|
|
136
|
+
* "-v, --video-id <id>" → "videoId" (优先用长名)
|
|
137
|
+
* "--flag" → "flag" (布尔)
|
|
138
|
+
* "-v" → "v" (布尔)
|
|
139
|
+
*/
|
|
140
|
+
function extractOptionName(flags) {
|
|
141
|
+
if (!flags) return null;
|
|
142
|
+
// 提取所有 long 选项(--xxx)和 short 选项(-x)
|
|
143
|
+
const tokens = flags.split(/[,\s]+/).filter(t => t && !t.includes('<'));
|
|
144
|
+
// 优先选 long 选项(--开头)
|
|
145
|
+
const longOpt = tokens.find(t => t.startsWith('--'));
|
|
146
|
+
const target = longOpt || tokens[0];
|
|
147
|
+
if (!target) return null;
|
|
148
|
+
// 去掉前导 -,转 camelCase
|
|
149
|
+
const stripped = target.replace(/^-+/, '');
|
|
150
|
+
return stripped.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 判断 option 是否需要值(flags 包含 `<...>`)
|
|
155
|
+
*/
|
|
156
|
+
function optionHasValue(flags) {
|
|
157
|
+
return /<[^>]+>/.test(flags || '');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 将 cmd.options 数组转换为 Zod 对象 schema
|
|
162
|
+
*
|
|
163
|
+
* 支持两种类型声明方式(优先级从高到低):
|
|
164
|
+
* 1. schema — 直接写 Zod 实例,如 { flags: '-n <value>', schema: z.number(), description: '...' }
|
|
165
|
+
* 2. type — 字符串类型,如 { flags: '-n <value>', type: 'number', description: '...' }
|
|
166
|
+
* 3. fallback — 无 schema/type,根据 hasValue 推断(string / boolean)
|
|
167
|
+
*
|
|
168
|
+
* type 可选值:'string' | 'number' | 'boolean' | 'array' | 'object'
|
|
169
|
+
*/
|
|
170
|
+
function buildZodSchemaFromOptions(options, commandDescription) {
|
|
171
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
172
|
+
return z.object({}).passthrough();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const shape = {};
|
|
176
|
+
for (const opt of options) {
|
|
177
|
+
const name = extractOptionName(opt.flags);
|
|
178
|
+
if (!name) continue;
|
|
179
|
+
|
|
180
|
+
const hasValue = optionHasValue(opt.flags);
|
|
181
|
+
const isRequired = opt.required === true && hasValue;
|
|
182
|
+
let desc = opt.description || '';
|
|
183
|
+
if (opt.defaultValue !== undefined && opt.defaultValue !== '') {
|
|
184
|
+
desc += ` (默认: ${opt.defaultValue})`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let fieldSchema;
|
|
188
|
+
|
|
189
|
+
// 优先用 schema(Zod 实例)
|
|
190
|
+
if (opt.schema && typeof opt.schema.parse === 'function') {
|
|
191
|
+
fieldSchema = opt.schema.describe(desc);
|
|
192
|
+
if (opt.defaultValue !== undefined && opt.defaultValue !== null) {
|
|
193
|
+
fieldSchema = fieldSchema.default(opt.defaultValue);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// 其次用 type(字符串类型)
|
|
197
|
+
else if (opt.type) {
|
|
198
|
+
fieldSchema = buildSchemaFromType(opt.type, desc, opt.defaultValue);
|
|
199
|
+
}
|
|
200
|
+
// 默认行为:带 <value> 为 string,否则 boolean
|
|
201
|
+
else if (hasValue) {
|
|
202
|
+
fieldSchema = z.string().describe(desc);
|
|
203
|
+
if (opt.defaultValue !== undefined && opt.defaultValue !== null && opt.defaultValue !== '') {
|
|
204
|
+
fieldSchema = fieldSchema.default(String(opt.defaultValue));
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
fieldSchema = z.boolean().describe(desc || '布尔标志');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!isRequired) {
|
|
211
|
+
fieldSchema = fieldSchema.optional();
|
|
212
|
+
}
|
|
213
|
+
shape[name] = fieldSchema;
|
|
214
|
+
}
|
|
215
|
+
return z.object(shape).passthrough();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 根据 type 字符串构建 Zod Schema
|
|
220
|
+
*/
|
|
221
|
+
function buildSchemaFromType(type, desc, defaultValue) {
|
|
222
|
+
const LOWER = type.toLowerCase();
|
|
223
|
+
let schema;
|
|
224
|
+
|
|
225
|
+
switch (LOWER) {
|
|
226
|
+
case 'number':
|
|
227
|
+
case 'int':
|
|
228
|
+
case 'integer':
|
|
229
|
+
schema = z.number().describe(desc);
|
|
230
|
+
if (defaultValue !== undefined && defaultValue !== null) {
|
|
231
|
+
schema = schema.default(Number(defaultValue));
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
case 'boolean':
|
|
235
|
+
case 'bool':
|
|
236
|
+
schema = z.boolean().describe(desc || '布尔标志');
|
|
237
|
+
if (defaultValue !== undefined && defaultValue !== null) {
|
|
238
|
+
schema = schema.default(Boolean(defaultValue));
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case 'array':
|
|
242
|
+
schema = z.array(z.string()).describe(desc);
|
|
243
|
+
if (defaultValue !== undefined && defaultValue !== null) {
|
|
244
|
+
const arr = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
|
|
245
|
+
schema = schema.default(arr.map(String));
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case 'object':
|
|
249
|
+
schema = z.record(z.any()).describe(desc);
|
|
250
|
+
if (defaultValue !== undefined && defaultValue !== null && typeof defaultValue === 'object') {
|
|
251
|
+
schema = schema.default(defaultValue);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case 'string':
|
|
255
|
+
default:
|
|
256
|
+
schema = z.string().describe(desc);
|
|
257
|
+
if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '') {
|
|
258
|
+
schema = schema.default(String(defaultValue));
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return schema;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 生成等效的 JSON schema(用于 _rawSchema,让 ext_skill 渲染更清晰)
|
|
268
|
+
*/
|
|
269
|
+
function buildRawJsonSchema(options) {
|
|
270
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
271
|
+
return { type: 'object', properties: {}, required: [] };
|
|
272
|
+
}
|
|
273
|
+
const properties = {};
|
|
274
|
+
const required = [];
|
|
275
|
+
for (const opt of options) {
|
|
276
|
+
const name = extractOptionName(opt.flags);
|
|
277
|
+
if (!name) continue;
|
|
278
|
+
const hasValue = optionHasValue(opt.flags);
|
|
279
|
+
const isRequired = opt.required === true && hasValue;
|
|
280
|
+
const desc = opt.description || '';
|
|
281
|
+
|
|
282
|
+
let jsonType = 'string';
|
|
283
|
+
if (opt.schema && typeof opt.schema.parse === 'function') {
|
|
284
|
+
// 从 Zod 实例猜测类型
|
|
285
|
+
const s = opt.schema;
|
|
286
|
+
if (s._def.typeName === 'ZodNumber' || s._def.typeName === 'ZodInt') jsonType = 'number';
|
|
287
|
+
else if (s._def.typeName === 'ZodBoolean') jsonType = 'boolean';
|
|
288
|
+
else if (s._def.typeName === 'ZodArray') jsonType = 'array';
|
|
289
|
+
else if (s._def.typeName === 'ZodObject' || s._def.typeName === 'ZodRecord') jsonType = 'object';
|
|
290
|
+
} else if (opt.type) {
|
|
291
|
+
const L = opt.type.toLowerCase();
|
|
292
|
+
if (L === 'number' || L === 'int' || L === 'integer') jsonType = 'number';
|
|
293
|
+
else if (L === 'boolean' || L === 'bool') jsonType = 'boolean';
|
|
294
|
+
else if (L === 'array') jsonType = 'array';
|
|
295
|
+
else if (L === 'object') jsonType = 'object';
|
|
296
|
+
} else {
|
|
297
|
+
jsonType = hasValue ? 'string' : 'boolean';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
properties[name] = { type: jsonType, description: desc };
|
|
301
|
+
if (isRequired) required.push(name);
|
|
302
|
+
}
|
|
303
|
+
return { type: 'object', properties, required };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 从 options 构建示例命令行字符串
|
|
308
|
+
*/
|
|
309
|
+
function buildExampleFromOptions(options) {
|
|
310
|
+
if (!Array.isArray(options) || options.length === 0) return '';
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const opt of options) {
|
|
313
|
+
const flags = opt.flags || '';
|
|
314
|
+
// 提取短选项和长选项
|
|
315
|
+
const tokens = flags.split(/[,\s]+/).filter(t => t && !t.includes('<'));
|
|
316
|
+
const shortOpt = tokens.find(t => /^-[a-zA-Z]$/.test(t));
|
|
317
|
+
const longOpt = tokens.find(t => t.startsWith('--'));
|
|
318
|
+
const flag = shortOpt || longOpt || tokens[0];
|
|
319
|
+
if (!flag) continue;
|
|
320
|
+
|
|
321
|
+
const hasValue = /<[^>]+>/.test(flags);
|
|
322
|
+
if (hasValue) {
|
|
323
|
+
const defaultVal = opt.defaultValue !== undefined && opt.defaultValue !== null ? String(opt.defaultValue) : '<value>';
|
|
324
|
+
parts.push(`${flag} ${defaultVal}`);
|
|
325
|
+
} else {
|
|
326
|
+
parts.push(flag);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return parts.join(' ');
|
|
330
|
+
}
|
|
331
|
+
|
|
133
332
|
/**
|
|
134
333
|
* Skill 类
|
|
135
334
|
*/
|
|
@@ -183,7 +382,7 @@ class Skill {
|
|
|
183
382
|
|
|
184
383
|
const skillInfo = {
|
|
185
384
|
name: skillName,
|
|
186
|
-
description: this.metadata?.description ||
|
|
385
|
+
description: this.metadata?.description || '',
|
|
187
386
|
version: this.metadata?.compatibility || '1.0.0',
|
|
188
387
|
};
|
|
189
388
|
|
|
@@ -232,40 +431,71 @@ class Skill {
|
|
|
232
431
|
return { ...program.opts(), args: program.args };
|
|
233
432
|
} catch (err) {
|
|
234
433
|
log.warn(`[SkillManager] Command parse error: ${err.message}`);
|
|
235
|
-
|
|
434
|
+
// Commander 解析失败时,用简单方式从 rawArgs 提取选项值
|
|
435
|
+
const result = {};
|
|
436
|
+
const tokens = parseShellArgs(rawArgs);
|
|
437
|
+
let i = 0;
|
|
438
|
+
while (i < tokens.length) {
|
|
439
|
+
const token = tokens[i];
|
|
440
|
+
// 匹配选项(-x 或 --xxx)
|
|
441
|
+
const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
|
|
442
|
+
if (optMatch) {
|
|
443
|
+
const optKey = optMatch[1];
|
|
444
|
+
// 找对应的 option
|
|
445
|
+
const opt = cmd.options.find(o => {
|
|
446
|
+
const flags = o.flags || '';
|
|
447
|
+
return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
|
|
448
|
+
});
|
|
449
|
+
if (opt) {
|
|
450
|
+
const name = extractOptionName(opt.flags);
|
|
451
|
+
// 判断是否有值(下一个 token 不是选项)
|
|
452
|
+
const hasValue = /<[^>]+>/.test(opt.flags);
|
|
453
|
+
if (hasValue && i + 1 < tokens.length) {
|
|
454
|
+
const next = tokens[i + 1];
|
|
455
|
+
if (!next.match(/^-/)) {
|
|
456
|
+
result[name] = next;
|
|
457
|
+
i += 2;
|
|
458
|
+
continue;
|
|
459
|
+
} else {
|
|
460
|
+
// 下个 token 是另一个选项,但当前选项需要值,用默认值
|
|
461
|
+
result[name] = opt.defaultValue ?? undefined;
|
|
462
|
+
i += 1;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
} else if (!hasValue) {
|
|
466
|
+
result[name] = true;
|
|
467
|
+
i += 1;
|
|
468
|
+
continue;
|
|
469
|
+
} else if (hasValue && i + 1 >= tokens.length) {
|
|
470
|
+
// 当前选项需要值但没有更多 token,用默认值
|
|
471
|
+
result[name] = opt.defaultValue ?? undefined;
|
|
472
|
+
i += 1;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
i += 1;
|
|
478
|
+
}
|
|
479
|
+
// 剩余的 token 当 args
|
|
480
|
+
result.args = tokens.slice(i);
|
|
481
|
+
return result;
|
|
236
482
|
}
|
|
237
483
|
};
|
|
238
484
|
}
|
|
239
485
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const optDescs = cmd.options.map(formatOptionForHelp);
|
|
244
|
-
commandHint = `\n 可用选项:\n ${optDescs.join('\n ')}`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 生成示例命令
|
|
248
|
-
const exampleCmd = cmd.options && cmd.options.length > 0
|
|
249
|
-
? cmd.options.map(o => {
|
|
250
|
-
const flag = (o.flags || '').split(/[,\s]+/).filter(p => p && !p.includes('<') && !p.includes(','))[0]?.replace(/^-*/, '') || '';
|
|
251
|
-
const hasValue = (o.flags || '').includes('<');
|
|
252
|
-
const def = o.defaultValue !== undefined && o.defaultValue !== '' ? String(o.defaultValue) : '';
|
|
253
|
-
const val = def || '<值>';
|
|
254
|
-
return hasValue ? `-${flag} ${val}` : `-${flag}`;
|
|
255
|
-
}).join(' ')
|
|
486
|
+
// 直接使用 command 字符串的 schema,describe 说明命令行格式
|
|
487
|
+
const exampleArgs = cmd.options && cmd.options.length > 0
|
|
488
|
+
? buildExampleFromOptions(cmd.options)
|
|
256
489
|
: '';
|
|
257
|
-
|
|
258
490
|
const toolDef = {
|
|
259
|
-
name:
|
|
260
|
-
description:
|
|
491
|
+
name: cmd.name,
|
|
492
|
+
description: cmd.description || 'Skill command',
|
|
261
493
|
inputSchema: z.object({
|
|
262
|
-
command: z.string().optional().describe(
|
|
263
|
-
cmd.options && cmd.options.length > 0
|
|
264
|
-
? `**字符串**,空格分隔,如: ${cmd.options.map(formatOptionForExample).join(' ')}`
|
|
265
|
-
: `**字符串**,命令行参数`
|
|
266
|
-
),
|
|
494
|
+
command: z.string().optional().describe(exampleArgs || '命令行字符串,如:-n value1 -v'),
|
|
267
495
|
}),
|
|
268
496
|
_options: cmd.options || null,
|
|
497
|
+
// 把原始 options 数组也保存下来,便于 ext_skill 显示
|
|
498
|
+
_rawSchema: buildRawJsonSchema(cmd.options),
|
|
269
499
|
execute: async (toolArgs, framework) => {
|
|
270
500
|
const ctx = {
|
|
271
501
|
sessionId: framework?._currentSessionId || 'unknown',
|
|
@@ -273,16 +503,27 @@ class Skill {
|
|
|
273
503
|
framework: framework,
|
|
274
504
|
cwd: framework?.getCwd?.() ?? process.cwd(),
|
|
275
505
|
};
|
|
276
|
-
|
|
506
|
+
// 直接使用 command 字符串
|
|
507
|
+
const commandString = toolArgs?.command || '';
|
|
508
|
+
let parsedArgs = commandString;
|
|
277
509
|
if (typeof argumentParser === 'function') {
|
|
278
|
-
parsedArgs = argumentParser(
|
|
510
|
+
parsedArgs = argumentParser(commandString);
|
|
279
511
|
}
|
|
280
512
|
return await handler(parsedArgs, ctx);
|
|
281
513
|
},
|
|
282
514
|
};
|
|
283
515
|
|
|
284
|
-
// 注册到 extension
|
|
285
|
-
|
|
516
|
+
// 注册到 extension 系统:每个 skill 注册为独立扩展 (skill:<skillname>)
|
|
517
|
+
const skillExtName = `skill:${skillName}`;
|
|
518
|
+
// 优先用 SKILL.md frontmatter 的 description,回退到空(提示词会隐藏)
|
|
519
|
+
const extDescription = skillInfo.description
|
|
520
|
+
? skillInfo.description
|
|
521
|
+
: `Skill ${skillName}`;
|
|
522
|
+
extExecutor.registerTool(skillExtName, {
|
|
523
|
+
name: skillExtName,
|
|
524
|
+
description: extDescription,
|
|
525
|
+
version: skillInfo.version || '1.0.0',
|
|
526
|
+
}, toolDef);
|
|
286
527
|
|
|
287
528
|
// 同时添加到 this.tools,供 extension-executor 的 _scanPluginTools 扫描
|
|
288
529
|
this.tools[fullName] = toolDef;
|
|
@@ -298,15 +539,20 @@ class Skill {
|
|
|
298
539
|
/**
|
|
299
540
|
* SkillManager 插件
|
|
300
541
|
*/
|
|
542
|
+
const PROMPT_DIR = __dirname;
|
|
543
|
+
|
|
301
544
|
class SkillManagerPlugin extends Plugin {
|
|
302
545
|
// 声明式 prompt parts
|
|
303
546
|
prompts = [
|
|
304
547
|
{
|
|
305
548
|
name: 'skills',
|
|
306
|
-
|
|
549
|
+
file: path.join(PROMPT_DIR, 'PROMPT.md'),
|
|
550
|
+
scope: 'global',
|
|
551
|
+
priority: 60,
|
|
307
552
|
description: '可用技能列表',
|
|
308
|
-
|
|
309
|
-
|
|
553
|
+
interpolate: (content) => {
|
|
554
|
+
const list = this._buildSkillsList();
|
|
555
|
+
return content.replace('{{skills}}', list || '(暂无技能)');
|
|
310
556
|
},
|
|
311
557
|
},
|
|
312
558
|
];
|
|
@@ -325,6 +571,9 @@ class SkillManagerPlugin extends Plugin {
|
|
|
325
571
|
: [config.skillsDir || '.foliko/skills', 'skills'];
|
|
326
572
|
this._skills = new Map();
|
|
327
573
|
this._loaded = false;
|
|
574
|
+
// _loadReady 暴露给外部等待首加载完成(bootstrap 在创建 MainAgent 前 await 它,
|
|
575
|
+
// 确保系统提示词第一次构建就能看到所有 skill 命令)
|
|
576
|
+
this._loadReady = Promise.resolve();
|
|
328
577
|
this.tools = {}; // 用于 extension-executor 扫描(由 Skill.registerCommands 填充)
|
|
329
578
|
}
|
|
330
579
|
|
|
@@ -335,7 +584,9 @@ class SkillManagerPlugin extends Plugin {
|
|
|
335
584
|
|
|
336
585
|
start(framework) {
|
|
337
586
|
super.start(framework);
|
|
338
|
-
this._loadAllSkills().catch(err =>
|
|
587
|
+
this._loadReady = this._loadAllSkills().catch(err => {
|
|
588
|
+
log.error('_loadAllSkills failed:', err.message);
|
|
589
|
+
});
|
|
339
590
|
|
|
340
591
|
// 注册 skill_load 工具
|
|
341
592
|
framework.registerTool({
|
|
@@ -372,7 +623,7 @@ class SkillManagerPlugin extends Plugin {
|
|
|
372
623
|
}
|
|
373
624
|
cmdLines.push('');
|
|
374
625
|
}
|
|
375
|
-
cmdLines.push('调用:`ext_call({ plugin: "skill
|
|
626
|
+
cmdLines.push('调用:`ext_call({ plugin: "skill:' + skillName + '", tool: "<command>", args: { /* Zod 结构化参数 */ } })`');
|
|
376
627
|
commandDocs = cmdLines.join('\n');
|
|
377
628
|
}
|
|
378
629
|
return {
|
|
@@ -490,70 +741,23 @@ class SkillManagerPlugin extends Plugin {
|
|
|
490
741
|
}
|
|
491
742
|
|
|
492
743
|
/**
|
|
493
|
-
*
|
|
744
|
+
* 构建技能列表(简版,仅显示技能名 + 描述)
|
|
745
|
+
* 命令详情在【Extensions】扩展插件段落有完整展示(含 Zod 参数)
|
|
746
|
+
* 详细使用指南用 skill_load("技能名") 加载
|
|
494
747
|
*/
|
|
495
|
-
|
|
748
|
+
_buildSkillsList() {
|
|
496
749
|
const skills = this.getAllSkills();
|
|
497
750
|
if (!skills || skills.length === 0) {
|
|
498
751
|
return '';
|
|
499
752
|
}
|
|
500
753
|
|
|
501
|
-
const lines = [
|
|
502
|
-
'## 可用技能',
|
|
503
|
-
'命令调用(仅对上方标注了「命令:」的技能有效):`ext_call({ plugin: "skill", tool: "<技能名>:<命令>", args: { command: "参数" } })`',
|
|
504
|
-
'',
|
|
505
|
-
];
|
|
506
|
-
|
|
507
|
-
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
508
|
-
const skillCommands = extExecutor?.getSkillCommands?.() || [];
|
|
509
|
-
|
|
510
|
-
// 按技能名分组命令(保留完整信息以渲染选项)
|
|
511
|
-
const toolsBySkill = {};
|
|
512
|
-
for (const tool of skillCommands) {
|
|
513
|
-
if (!tool || !tool.name) continue;
|
|
514
|
-
const parts = tool.name.split(':');
|
|
515
|
-
if (parts.length < 2) continue;
|
|
516
|
-
const skillName = parts[0];
|
|
517
|
-
const cmdName = parts.slice(1).join(':');
|
|
518
|
-
if (!toolsBySkill[skillName]) toolsBySkill[skillName] = [];
|
|
519
|
-
toolsBySkill[skillName].push({ name: cmdName, description: tool.description || '', options: tool.options || null });
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// 一列一技能,命令含选项时显示参数
|
|
754
|
+
const lines = [];
|
|
523
755
|
for (const skill of skills) {
|
|
524
756
|
const name = skill.metadata?.name || skill.name || 'unknown';
|
|
525
757
|
const descText = skill.metadata?.description || '';
|
|
526
|
-
if (!descText
|
|
527
|
-
|
|
528
|
-
const parts = [];
|
|
529
|
-
if (descText) parts.push(descText);
|
|
530
|
-
|
|
531
|
-
if (toolsBySkill[name]) {
|
|
532
|
-
const cmdStrs = toolsBySkill[name].map(cmd => {
|
|
533
|
-
if (cmd.options && cmd.options.length > 0) {
|
|
534
|
-
const optStrs = cmd.options.map(o => {
|
|
535
|
-
// 提取长选项名(--xxx)作为参数名,形如 "--tokens <值>"
|
|
536
|
-
const flag = o.flags || '';
|
|
537
|
-
const longOpt = flag.split(/\s+/).filter(f => f.startsWith('--'))[0];
|
|
538
|
-
const valPart = flag.includes('<') ? flag.match(/<[^>]+>/)?.[0] || '' : '';
|
|
539
|
-
if (longOpt && valPart) return `${longOpt} ${valPart}`;
|
|
540
|
-
if (longOpt) return longOpt;
|
|
541
|
-
return flag.split(/\s+/).filter(f => f && !f.includes('<')).map(f => f.replace(/,$/, ''))[0] || flag;
|
|
542
|
-
});
|
|
543
|
-
return `${cmd.name}(${optStrs.join(', ')})`;
|
|
544
|
-
}
|
|
545
|
-
return cmd.name;
|
|
546
|
-
});
|
|
547
|
-
parts.push(`命令: ${cmdStrs.join(', ')}`);
|
|
548
|
-
}
|
|
549
|
-
lines.push(`- **${name}**: ${parts.join(' | ')}`);
|
|
758
|
+
if (!descText) continue;
|
|
759
|
+
lines.push(`- **${name}**: ${descText}`);
|
|
550
760
|
}
|
|
551
|
-
|
|
552
|
-
lines.push(
|
|
553
|
-
'',
|
|
554
|
-
'> **提示**:使用 `skill_load("${skill名}")` 加载技能获取详细使用指南。'
|
|
555
|
-
);
|
|
556
|
-
|
|
557
761
|
return lines.join('\n');
|
|
558
762
|
}
|
|
559
763
|
|
|
@@ -631,6 +835,19 @@ class SkillManagerPlugin extends Plugin {
|
|
|
631
835
|
|
|
632
836
|
// log.info(` Loaded ${this._skills.size} skills`);
|
|
633
837
|
this._loaded = true;
|
|
838
|
+
|
|
839
|
+
// 通知 extension-executor 失效 prompt 缓存,让 AI 看到新注册的 skill 命令
|
|
840
|
+
// 原因: _loadAllSkills 是异步的(_loadSkill 中 await skill.registerCommands() 会动态 import ESM 包),
|
|
841
|
+
// 但 framework:ready 在所有 plugin loadPlugin() 之后立即触发,那时 skill 命令可能还没注册。
|
|
842
|
+
// 不刷缓存的话,首启动后模型看不到这些命令,必须 reload 一次才出现。
|
|
843
|
+
if (this._framework) {
|
|
844
|
+
try {
|
|
845
|
+
const extExec = this._framework.pluginManager?.get('extension-executor');
|
|
846
|
+
if (extExec && typeof extExec._invalidatePromptCaches === 'function') {
|
|
847
|
+
extExec._invalidatePromptCaches();
|
|
848
|
+
}
|
|
849
|
+
} catch (_) { /* 失效失败不影响 skill 加载 */ }
|
|
850
|
+
}
|
|
634
851
|
}
|
|
635
852
|
|
|
636
853
|
/**
|
|
@@ -1006,6 +1223,233 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1006
1223
|
return this._skills.has(name);
|
|
1007
1224
|
}
|
|
1008
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* 获取 skill 详情(结构化信息)
|
|
1228
|
+
*/
|
|
1229
|
+
getSkillDetails(name) {
|
|
1230
|
+
const skill = this._skills.get(name);
|
|
1231
|
+
if (!skill) return null;
|
|
1232
|
+
return {
|
|
1233
|
+
name: skill.name,
|
|
1234
|
+
description: skill.metadata?.description || '',
|
|
1235
|
+
content: skill.content || '',
|
|
1236
|
+
path: skill.path || null,
|
|
1237
|
+
metadata: skill.metadata ? {
|
|
1238
|
+
name: skill.metadata.name,
|
|
1239
|
+
description: skill.metadata.description,
|
|
1240
|
+
license: skill.metadata.license,
|
|
1241
|
+
compatibility: skill.metadata.compatibility,
|
|
1242
|
+
allowedTools: skill.metadata.allowedTools,
|
|
1243
|
+
} : null,
|
|
1244
|
+
commands: skill.instance?._commands || [],
|
|
1245
|
+
references: skill.references ? Array.from(skill.references.keys()) : [],
|
|
1246
|
+
scripts: skill.scripts ? Array.from(skill.scripts.keys()) : [],
|
|
1247
|
+
fromPlugin: skill.fromPlugin || false,
|
|
1248
|
+
registered: skill.registered || false,
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* 注册一个新 skill(程序化注册,不依赖文件)
|
|
1254
|
+
* @param {string} name - 技能名称
|
|
1255
|
+
* @param {string} content - 技能内容(markdown)
|
|
1256
|
+
* @param {Object} [metadata] - 技能元数据
|
|
1257
|
+
* @param {Object} [options] - 选项
|
|
1258
|
+
* @param {Function[]} [options.commands] - 命令定义数组 [{ name, description, options, execute }]
|
|
1259
|
+
* @param {string} [options.path] - 技能路径(用于 references/scripts 解析)
|
|
1260
|
+
* @returns {Object} 注册的 skill 对象
|
|
1261
|
+
*/
|
|
1262
|
+
register(name, content, metadata = {}, options = {}) {
|
|
1263
|
+
const skillMetadata = new SkillMetadata({
|
|
1264
|
+
name: name,
|
|
1265
|
+
description: metadata.description || '',
|
|
1266
|
+
license: metadata.license || null,
|
|
1267
|
+
compatibility: metadata.compatibility || null,
|
|
1268
|
+
metadata: metadata.metadata || {},
|
|
1269
|
+
allowedTools: metadata['allowed-tools'] || metadata.allowedTools || [],
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
const skill = new Skill(skillMetadata, content);
|
|
1273
|
+
skill._skillPath = options.path || null;
|
|
1274
|
+
skill.install(this._framework);
|
|
1275
|
+
|
|
1276
|
+
// 注册命令
|
|
1277
|
+
if (Array.isArray(options.commands) && options.commands.length > 0) {
|
|
1278
|
+
for (const cmd of options.commands) {
|
|
1279
|
+
if (!cmd || !cmd.name) continue;
|
|
1280
|
+
|
|
1281
|
+
const handler = typeof cmd.execute === 'function' ? cmd.execute : null;
|
|
1282
|
+
if (!handler) continue;
|
|
1283
|
+
|
|
1284
|
+
const fullName = `${name}:${cmd.name}`;
|
|
1285
|
+
|
|
1286
|
+
// 获取参数解析器
|
|
1287
|
+
let argumentParser = cmd.argumentParser;
|
|
1288
|
+
if (!argumentParser && Array.isArray(cmd.options) && cmd.options.length > 0) {
|
|
1289
|
+
argumentParser = (rawArgs) => {
|
|
1290
|
+
try {
|
|
1291
|
+
const program = new Command();
|
|
1292
|
+
program.exitOverride();
|
|
1293
|
+
program.configureOutput({ writeErr: () => {} });
|
|
1294
|
+
for (const opt of cmd.options) {
|
|
1295
|
+
program.option(opt.flags, opt.description, opt.defaultValue);
|
|
1296
|
+
}
|
|
1297
|
+
program.arguments('[args...]');
|
|
1298
|
+
program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
|
|
1299
|
+
return { ...program.opts(), args: program.args };
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
log.warn(`[SkillManager] Command parse error: ${err.message}`);
|
|
1302
|
+
// Commander 解析失败时,用简单方式从 rawArgs 提取选项值
|
|
1303
|
+
const result = {};
|
|
1304
|
+
const tokens = parseShellArgs(rawArgs);
|
|
1305
|
+
let i = 0;
|
|
1306
|
+
while (i < tokens.length) {
|
|
1307
|
+
const token = tokens[i];
|
|
1308
|
+
const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
|
|
1309
|
+
if (optMatch) {
|
|
1310
|
+
const optKey = optMatch[1];
|
|
1311
|
+
const opt = cmd.options.find(o => {
|
|
1312
|
+
const flags = o.flags || '';
|
|
1313
|
+
return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
|
|
1314
|
+
});
|
|
1315
|
+
if (opt) {
|
|
1316
|
+
const name = extractOptionName(opt.flags);
|
|
1317
|
+
const hasValue = /<[^>]+>/.test(opt.flags);
|
|
1318
|
+
if (hasValue && i + 1 < tokens.length) {
|
|
1319
|
+
const next = tokens[i + 1];
|
|
1320
|
+
if (!next.match(/^-/)) {
|
|
1321
|
+
result[name] = next;
|
|
1322
|
+
i += 2;
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
} else if (!hasValue) {
|
|
1326
|
+
result[name] = true;
|
|
1327
|
+
i += 1;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
i += 1;
|
|
1333
|
+
}
|
|
1334
|
+
result.args = tokens.slice(i);
|
|
1335
|
+
return result;
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// 转为 Zod schema(统一风格)
|
|
1341
|
+
const exampleArgs2 = cmd.options && cmd.options.length > 0
|
|
1342
|
+
? buildExampleFromOptions(cmd.options)
|
|
1343
|
+
: '';
|
|
1344
|
+
const toolDef = {
|
|
1345
|
+
name: cmd.name, // 只用命令名
|
|
1346
|
+
description: cmd.description || 'Skill command',
|
|
1347
|
+
inputSchema: z.object({
|
|
1348
|
+
command: z.string().optional().describe(exampleArgs2 || '命令行字符串'),
|
|
1349
|
+
}),
|
|
1350
|
+
_options: cmd.options || null,
|
|
1351
|
+
_rawSchema: buildRawJsonSchema(cmd.options),
|
|
1352
|
+
execute: async (toolArgs, framework) => {
|
|
1353
|
+
const ctx = {
|
|
1354
|
+
sessionId: framework?._currentSessionId || 'unknown',
|
|
1355
|
+
agent: null,
|
|
1356
|
+
framework: framework,
|
|
1357
|
+
cwd: framework?.getCwd?.() ?? process.cwd(),
|
|
1358
|
+
};
|
|
1359
|
+
const commandString = toolArgs?.command || '';
|
|
1360
|
+
let parsedArgs = commandString;
|
|
1361
|
+
if (typeof argumentParser === 'function') {
|
|
1362
|
+
parsedArgs = argumentParser(commandString);
|
|
1363
|
+
}
|
|
1364
|
+
return await handler(parsedArgs, ctx);
|
|
1365
|
+
},
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
1369
|
+
if (extExecutor) {
|
|
1370
|
+
// 每个 skill 注册为独立扩展 (skill:<name>)
|
|
1371
|
+
// 优先用 skillMetadata.description,回退到占位文本
|
|
1372
|
+
const extDescription = skillMetadata.description
|
|
1373
|
+
? skillMetadata.description
|
|
1374
|
+
: `Skill ${name}`;
|
|
1375
|
+
extExecutor.registerTool(`skill:${name}`, {
|
|
1376
|
+
name: `skill:${name}`,
|
|
1377
|
+
description: extDescription,
|
|
1378
|
+
version: skillMetadata.compatibility || '1.0.0',
|
|
1379
|
+
}, toolDef);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
skill.tools[fullName] = toolDef;
|
|
1383
|
+
skill._commands.push(fullName);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// 扫描 references 和 scripts
|
|
1388
|
+
const references = skill._skillPath ? this._scanReferences(skill._skillPath) : new Map();
|
|
1389
|
+
const scripts = skill._skillPath ? this._scanScripts(skill._skillPath) : new Map();
|
|
1390
|
+
|
|
1391
|
+
const skillData = {
|
|
1392
|
+
name: name,
|
|
1393
|
+
metadata: skillMetadata,
|
|
1394
|
+
content: content,
|
|
1395
|
+
instance: skill,
|
|
1396
|
+
path: skill._skillPath,
|
|
1397
|
+
references,
|
|
1398
|
+
scripts,
|
|
1399
|
+
fromPlugin: false,
|
|
1400
|
+
registered: true, // 标记为程序化注册
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
this._skills.set(name, skillData);
|
|
1404
|
+
|
|
1405
|
+
// 更新 SkillManagerPlugin.tools
|
|
1406
|
+
Object.assign(this.tools, skill.tools);
|
|
1407
|
+
|
|
1408
|
+
return skillData;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* 移除 skill
|
|
1413
|
+
* @param {string} name 技能名称
|
|
1414
|
+
* @returns {boolean} 是否成功移除
|
|
1415
|
+
*/
|
|
1416
|
+
remove(name) {
|
|
1417
|
+
if (!this._skills.has(name)) {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const skill = this._skills.get(name);
|
|
1422
|
+
|
|
1423
|
+
// 清理命令(从 extension-executor 移除)
|
|
1424
|
+
// 新结构:每个 skill 注册为 `skill:<name>` 扩展,命令是该扩展下的 tool
|
|
1425
|
+
if (skill.instance?._commands) {
|
|
1426
|
+
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
1427
|
+
if (extExecutor) {
|
|
1428
|
+
const extName = `skill:${name}`;
|
|
1429
|
+
for (const cmdFullName of skill.instance._commands) {
|
|
1430
|
+
// cmdFullName 格式: `<skill>:<command>`,如 'demo-skill:hello'
|
|
1431
|
+
const cmdName = cmdFullName.includes(':') ? cmdFullName.split(':').slice(1).join(':') : cmdFullName;
|
|
1432
|
+
extExecutor._unregisterTool?.(extName, cmdName);
|
|
1433
|
+
}
|
|
1434
|
+
// 如果该扩展下没有工具了,移除整个扩展
|
|
1435
|
+
const ext = extExecutor._registry?.getPlugin?.(extName);
|
|
1436
|
+
if (ext && (!ext.tools || ext.tools.length === 0)) {
|
|
1437
|
+
extExecutor._registry.clearPlugin?.(extName);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// 从 SkillManagerPlugin.tools 中移除
|
|
1443
|
+
if (skill.instance?.tools) {
|
|
1444
|
+
for (const toolName of Object.keys(skill.instance.tools)) {
|
|
1445
|
+
delete this.tools[toolName];
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
this._skills.delete(name);
|
|
1450
|
+
return true;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1009
1453
|
/**
|
|
1010
1454
|
* 获取 skill 数量
|
|
1011
1455
|
*/
|
|
@@ -1020,9 +1464,10 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1020
1464
|
this._framework = framework;
|
|
1021
1465
|
|
|
1022
1466
|
// 清理 extension-executor 中的旧 skill 命令,避免重载后残留
|
|
1467
|
+
// 注意: extExecutor 的 Map 在 _registry._extensions 上,_extensions 自身是 undefined
|
|
1023
1468
|
const extExecutor = framework.pluginManager?.get('extension-executor');
|
|
1024
|
-
if (extExecutor) {
|
|
1025
|
-
extExecutor.
|
|
1469
|
+
if (extExecutor?._registry?.clearPlugin) {
|
|
1470
|
+
extExecutor._registry.clearPlugin('skill');
|
|
1026
1471
|
}
|
|
1027
1472
|
|
|
1028
1473
|
await this._loadAllSkills();
|
|
@@ -1034,8 +1479,8 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1034
1479
|
async onCwdChanged(oldCwd, newCwd, framework) {
|
|
1035
1480
|
// 切换工作目录前清理旧 skill 命令
|
|
1036
1481
|
const extExecutor = framework.pluginManager?.get('extension-executor');
|
|
1037
|
-
if (extExecutor) {
|
|
1038
|
-
extExecutor.
|
|
1482
|
+
if (extExecutor?._registry?.clearPlugin) {
|
|
1483
|
+
extExecutor._registry.clearPlugin('skill');
|
|
1039
1484
|
}
|
|
1040
1485
|
}
|
|
1041
1486
|
|