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.
- 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 +280 -24
- 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 +44 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/scheduler/index.js +1 -0
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +402 -115
- 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/context.js +941 -0
- package/plugins/core/workflow/engine.js +66 -0
- package/plugins/core/workflow/examples/01-basic.js +42 -0
- package/plugins/core/workflow/examples/01-basic.json +30 -0
- package/plugins/core/workflow/examples/02-choice.js +75 -0
- package/plugins/core/workflow/examples/02-choice.json +59 -0
- package/plugins/core/workflow/examples/03-chain-style.js +114 -0
- package/plugins/core/workflow/examples/03-each.json +41 -0
- package/plugins/core/workflow/examples/04-parallel.js +52 -0
- package/plugins/core/workflow/examples/04-parallel.json +51 -0
- package/plugins/core/workflow/examples/05-chain-style.json +68 -0
- package/plugins/core/workflow/examples/05-each.js +65 -0
- package/plugins/core/workflow/examples/06-script.js +82 -0
- package/plugins/core/workflow/examples/07-module-export.js +43 -0
- package/plugins/core/workflow/examples/08-default-export.js +29 -0
- package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
- package/plugins/core/workflow/examples/10-logger.js +34 -0
- package/plugins/core/workflow/examples/11-storage.js +68 -0
- package/plugins/core/workflow/examples/simple.js +77 -0
- package/plugins/core/workflow/examples/simple.json +75 -0
- package/plugins/core/workflow/index.js +204 -78
- package/plugins/core/workflow/js-runner.js +318 -0
- package/plugins/core/workflow/json-runner.js +323 -0
- package/plugins/core/workflow/stages/action.js +211 -0
- package/plugins/core/workflow/stages/choice.js +74 -0
- package/plugins/core/workflow/stages/delay.js +73 -0
- package/plugins/core/workflow/stages/each.js +123 -0
- package/plugins/core/workflow/stages/parallel.js +69 -0
- package/plugins/core/workflow/stages/try.js +142 -0
- package/plugins/executors/data-splitter/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +8 -6
- 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/sandbox/check-context.js +5 -0
- package/sandbox/test-context.js +27 -0
- package/sandbox/test-fixes.js +40 -0
- package/sandbox/test-hello-js.js +46 -0
- 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 +753 -436
- package/src/agent/chat.js +56 -28
- package/src/agent/main.js +39 -14
- package/src/agent/prompt-registry.js +56 -16
- 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 +9 -5
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/json-safe.js +20 -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 +78 -12
- package/src/index.js +4 -0
- package/src/plugin/base.js +908 -5
- package/src/plugin/manager.js +124 -9
- package/src/tool/schema.js +32 -9
- package/src/utils/sandbox.js +1 -1
- package/website/index.html +821 -0
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
|
@@ -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,32 @@ 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
|
+
|
|
1009
1252
|
/**
|
|
1010
1253
|
* 注册一个新 skill(程序化注册,不依赖文件)
|
|
1011
1254
|
* @param {string} name - 技能名称
|
|
@@ -1055,29 +1298,57 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1055
1298
|
program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
|
|
1056
1299
|
return { ...program.opts(), args: program.args };
|
|
1057
1300
|
} catch (err) {
|
|
1058
|
-
|
|
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;
|
|
1059
1336
|
}
|
|
1060
1337
|
};
|
|
1061
1338
|
}
|
|
1062
1339
|
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
commandHint = `\n 可用选项:\n ${optDescs.join('\n ')}`;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1340
|
+
// 转为 Zod schema(统一风格)
|
|
1341
|
+
const exampleArgs2 = cmd.options && cmd.options.length > 0
|
|
1342
|
+
? buildExampleFromOptions(cmd.options)
|
|
1343
|
+
: '';
|
|
1070
1344
|
const toolDef = {
|
|
1071
|
-
name:
|
|
1072
|
-
description:
|
|
1345
|
+
name: cmd.name, // 只用命令名
|
|
1346
|
+
description: cmd.description || 'Skill command',
|
|
1073
1347
|
inputSchema: z.object({
|
|
1074
|
-
command: z.string().optional().describe(
|
|
1075
|
-
cmd.options && cmd.options.length > 0
|
|
1076
|
-
? `**字符串**,空格分隔,如: ${cmd.options.map(formatOptionForExample).join(' ')}`
|
|
1077
|
-
: `**字符串**,命令行参数`
|
|
1078
|
-
),
|
|
1348
|
+
command: z.string().optional().describe(exampleArgs2 || '命令行字符串'),
|
|
1079
1349
|
}),
|
|
1080
1350
|
_options: cmd.options || null,
|
|
1351
|
+
_rawSchema: buildRawJsonSchema(cmd.options),
|
|
1081
1352
|
execute: async (toolArgs, framework) => {
|
|
1082
1353
|
const ctx = {
|
|
1083
1354
|
sessionId: framework?._currentSessionId || 'unknown',
|
|
@@ -1085,9 +1356,10 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1085
1356
|
framework: framework,
|
|
1086
1357
|
cwd: framework?.getCwd?.() ?? process.cwd(),
|
|
1087
1358
|
};
|
|
1088
|
-
|
|
1359
|
+
const commandString = toolArgs?.command || '';
|
|
1360
|
+
let parsedArgs = commandString;
|
|
1089
1361
|
if (typeof argumentParser === 'function') {
|
|
1090
|
-
parsedArgs = argumentParser(
|
|
1362
|
+
parsedArgs = argumentParser(commandString);
|
|
1091
1363
|
}
|
|
1092
1364
|
return await handler(parsedArgs, ctx);
|
|
1093
1365
|
},
|
|
@@ -1095,9 +1367,14 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1095
1367
|
|
|
1096
1368
|
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
1097
1369
|
if (extExecutor) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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,
|
|
1101
1378
|
version: skillMetadata.compatibility || '1.0.0',
|
|
1102
1379
|
}, toolDef);
|
|
1103
1380
|
}
|
|
@@ -1144,11 +1421,20 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1144
1421
|
const skill = this._skills.get(name);
|
|
1145
1422
|
|
|
1146
1423
|
// 清理命令(从 extension-executor 移除)
|
|
1424
|
+
// 新结构:每个 skill 注册为 `skill:<name>` 扩展,命令是该扩展下的 tool
|
|
1147
1425
|
if (skill.instance?._commands) {
|
|
1148
1426
|
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
1149
1427
|
if (extExecutor) {
|
|
1150
|
-
|
|
1151
|
-
|
|
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);
|
|
1152
1438
|
}
|
|
1153
1439
|
}
|
|
1154
1440
|
}
|
|
@@ -1178,9 +1464,10 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1178
1464
|
this._framework = framework;
|
|
1179
1465
|
|
|
1180
1466
|
// 清理 extension-executor 中的旧 skill 命令,避免重载后残留
|
|
1467
|
+
// 注意: extExecutor 的 Map 在 _registry._extensions 上,_extensions 自身是 undefined
|
|
1181
1468
|
const extExecutor = framework.pluginManager?.get('extension-executor');
|
|
1182
|
-
if (extExecutor) {
|
|
1183
|
-
extExecutor.
|
|
1469
|
+
if (extExecutor?._registry?.clearPlugin) {
|
|
1470
|
+
extExecutor._registry.clearPlugin('skill');
|
|
1184
1471
|
}
|
|
1185
1472
|
|
|
1186
1473
|
await this._loadAllSkills();
|
|
@@ -1192,8 +1479,8 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1192
1479
|
async onCwdChanged(oldCwd, newCwd, framework) {
|
|
1193
1480
|
// 切换工作目录前清理旧 skill 命令
|
|
1194
1481
|
const extExecutor = framework.pluginManager?.get('extension-executor');
|
|
1195
|
-
if (extExecutor) {
|
|
1196
|
-
extExecutor.
|
|
1482
|
+
if (extExecutor?._registry?.clearPlugin) {
|
|
1483
|
+
extExecutor._registry.clearPlugin('skill');
|
|
1197
1484
|
}
|
|
1198
1485
|
}
|
|
1199
1486
|
|