foliko 2.0.12 → 2.0.13
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 +3 -1
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/docs/extensions.md +3 -4
- package/docs/migration.md +3 -3
- package/docs/usage.md +3 -3
- package/package.json +1 -1
- package/plugins/core/scheduler/index.js +40 -9
- package/plugins/core/skill-manager/index.js +125 -151
- package/plugins/executors/extension/prompt-builder.js +32 -5
- package/plugins/executors/shell/index.js +28 -0
- package/skills/skill-guide/SKILL.md +99 -60
|
@@ -239,7 +239,9 @@
|
|
|
239
239
|
"Bash(npm start *)",
|
|
240
240
|
"Bash(xxd)",
|
|
241
241
|
"Bash(python fix_ext2.py)",
|
|
242
|
-
"Bash(npx vitest *)"
|
|
242
|
+
"Bash(npx vitest *)",
|
|
243
|
+
"Bash(ls node_modules | wc -l && ls -d node_modules/@* 2>/dev/null | head -5)",
|
|
244
|
+
"Bash(node -e \"console.log\\(require.resolve\\('croner'\\)\\)\")"
|
|
243
245
|
]
|
|
244
246
|
}
|
|
245
247
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -147,7 +147,7 @@ Provider selection via `config.provider` and AI settings in `.foliko/ai.json`.
|
|
|
147
147
|
5. **Per-Session Message Storage**: `AgentChatHandler` uses `_sessionMessageStores` Map for Per-Session 消息隔离
|
|
148
148
|
6. **Unified Extension Call**: 扩展工具(普通扩展 / `mcp:<server>` / `skill:<name>`)通过 `ext_skill` + `ext_call` 统一调度
|
|
149
149
|
- `ext_skill({ plugin })` 查询参数 → `ext_call({ plugin, tool, args })` 调用
|
|
150
|
-
-
|
|
150
|
+
- 扩展调用:`args` 是结构化对象,参数名对应工具定义的字段(camelCase)
|
|
151
151
|
|
|
152
152
|
### Built-in Plugins
|
|
153
153
|
|
package/README.md
CHANGED
|
@@ -391,9 +391,9 @@ allowed-tools: tool1,tool2
|
|
|
391
391
|
> **统一扩展调用模式**:
|
|
392
392
|
> - 普通扩展:`ext_call({ plugin: "<name>", tool: "<tool>", args: {...} })`
|
|
393
393
|
> - MCP 服务器:`ext_call({ plugin: "mcp:<servername>", tool: "<tool>", args: {...} })`
|
|
394
|
-
> - Skill 命令:`ext_call({ plugin: "skill:<skillname>", tool: "<command>", args: {
|
|
394
|
+
> - Skill 命令:`ext_call({ plugin: "skill:<skillname>", tool: "<command>", args: { name: "...", lang: "..." } })`
|
|
395
395
|
> - 调用前先 `ext_skill({ plugin: "<name>" })` 查看参数结构
|
|
396
|
-
> -
|
|
396
|
+
> - 扩展调用:`args` 是结构化对象,参数名对应工具定义的字段(camelCase)
|
|
397
397
|
|
|
398
398
|
### 子 Agent 管理工具
|
|
399
399
|
|
package/docs/extensions.md
CHANGED
|
@@ -45,8 +45,7 @@ ext_call({
|
|
|
45
45
|
|
|
46
46
|
| 扩展类型 | args 格式 |
|
|
47
47
|
|---|---|
|
|
48
|
-
| 普通扩展 / MCP / Python | `args: {...}`
|
|
49
|
-
| Skill 命令 | `args: { command: "..." }` 命令行字符串 |
|
|
48
|
+
| 普通扩展 / MCP / Python / Skill | `args: {...}` 结构化对象,参数名对应工具定义的字段(camelCase) |
|
|
50
49
|
|
|
51
50
|
## 3. MCP 服务器自动注册
|
|
52
51
|
|
|
@@ -118,11 +117,11 @@ module.exports = {
|
|
|
118
117
|
ext_call({
|
|
119
118
|
plugin: 'skill:my-skill',
|
|
120
119
|
tool: 'greet',
|
|
121
|
-
args: {
|
|
120
|
+
args: { name: "Claude", lang: "en", shout: true }
|
|
122
121
|
})
|
|
123
122
|
```
|
|
124
123
|
|
|
125
|
-
**args
|
|
124
|
+
**args 直接传结构化对象**,handler 收到 `{ name: 'Claude', lang: 'en', shout: true }`。
|
|
126
125
|
|
|
127
126
|
## 5. Plugin 基类:`this.skill` accessor
|
|
128
127
|
|
package/docs/migration.md
CHANGED
|
@@ -84,15 +84,15 @@ Skill 命令通过 `skill:<name>` 命名空间调用:
|
|
|
84
84
|
ext_call({
|
|
85
85
|
plugin: "skill:my-skill", // skill:<name>
|
|
86
86
|
tool: "cmd", // 命令名(不是 <skill>:<cmd>)
|
|
87
|
-
args: {
|
|
87
|
+
args: { name: "x", verbose: true }
|
|
88
88
|
})
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
**要点**:
|
|
92
92
|
- 每个 skill 是独立扩展 `skill:<name>`
|
|
93
93
|
- 命令是该扩展下的 tool
|
|
94
|
-
- `args
|
|
95
|
-
-
|
|
94
|
+
- `args` 直接传结构化对象,参数名对应 `options` 中定义的字段(camelCase)
|
|
95
|
+
- 不再需要命令行字符串解析
|
|
96
96
|
|
|
97
97
|
#### 3. ext_skill 返回内容
|
|
98
98
|
|
package/docs/usage.md
CHANGED
|
@@ -198,17 +198,17 @@ module.exports = {
|
|
|
198
198
|
// 1. 查询参数
|
|
199
199
|
ext_skill({ plugin: "skill:<skillname>" })
|
|
200
200
|
|
|
201
|
-
// 2.
|
|
201
|
+
// 2. 调用(传结构化参数)
|
|
202
202
|
ext_call({
|
|
203
203
|
plugin: "skill:<skillname>",
|
|
204
204
|
tool: "greet",
|
|
205
|
-
args: {
|
|
205
|
+
args: { name: "Claude", lang: "en", shout: true },
|
|
206
206
|
})
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
#### 参数传递
|
|
210
210
|
|
|
211
|
-
Skill
|
|
211
|
+
Skill 命令参数直接通过 `args` 对象传递,无需解析命令行字符串:
|
|
212
212
|
|
|
213
213
|
| flags | 解析后 args 字段 |
|
|
214
214
|
|---|---|
|
package/package.json
CHANGED
|
@@ -340,6 +340,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
340
340
|
}),
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
log.info(` [start] Scheduler 插件启动,当前时间: ${new Date().toISOString()} timezone=${this.config.timezone}`);
|
|
343
344
|
this._loadPersistedTasks();
|
|
344
345
|
return this;
|
|
345
346
|
}
|
|
@@ -432,7 +433,11 @@ class SchedulerPlugin extends Plugin {
|
|
|
432
433
|
// once:startAt 限定未来触发
|
|
433
434
|
task.scheduleHandle = new Cron(runAt, this._cronOptions(task), () => this._onFire(task));
|
|
434
435
|
}
|
|
436
|
+
// 打印下次执行时间便于调试
|
|
437
|
+
const nextRun = task.scheduleHandle.nextRun();
|
|
438
|
+
log.info(` [_createTask] 创建任务成功: ${task.name} cron="${cron || ''}" 下次执行: ${nextRun ? nextRun.toISOString() : 'once-立即?'} timezone=${task.timezone}`);
|
|
435
439
|
} catch (err) {
|
|
440
|
+
log.error(` [_createTask] 创建调度失败: ${err.message}`);
|
|
436
441
|
return { success: false, error: `调度创建失败: ${err.message}` };
|
|
437
442
|
}
|
|
438
443
|
|
|
@@ -465,30 +470,39 @@ class SchedulerPlugin extends Plugin {
|
|
|
465
470
|
}
|
|
466
471
|
|
|
467
472
|
_cronOptions(task) {
|
|
468
|
-
|
|
473
|
+
const opts = {
|
|
469
474
|
timezone: this.config.timezone,
|
|
470
475
|
protect: true, // 防重叠:上一次还没跑完,跳过本次
|
|
471
476
|
catch: true, // catch-up:进程恢复后补触发错过的触发
|
|
472
477
|
name: `scheduler:${task.id}`,
|
|
473
478
|
unref: true, // 不阻止进程退出
|
|
474
479
|
};
|
|
480
|
+
log.debug(` [_cronOptions] task=${task.name} cron=${task.cronExpression || 'once'} timezone=${opts.timezone}`);
|
|
481
|
+
return opts;
|
|
475
482
|
}
|
|
476
483
|
|
|
477
484
|
// ============================================================
|
|
478
485
|
// 触发回调
|
|
479
486
|
// ============================================================
|
|
480
487
|
async _onFire(task) {
|
|
481
|
-
|
|
488
|
+
log.info(` [_onFire] >>> 任务触发: ${task.name} (id=${task.id}) cron=${task.cronExpression || 'once'} timezone=${task.timezone}`);
|
|
489
|
+
|
|
490
|
+
if (!task.enabled) {
|
|
491
|
+
log.warn(` [_onFire] 任务 ${task.name} 已禁用,跳过`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
482
494
|
if (task.running) {
|
|
483
495
|
// 理论上 protect:true 已经处理过;这里再保险一次
|
|
484
|
-
log.warn(` 任务 ${task.name} 仍在执行中,跳过本次触发`);
|
|
496
|
+
log.warn(` [_onFire] 任务 ${task.name} 仍在执行中,跳过本次触发`);
|
|
485
497
|
return;
|
|
486
498
|
}
|
|
487
499
|
task.running = true;
|
|
488
500
|
task.lastRun = new Date();
|
|
489
501
|
task.runCount++;
|
|
502
|
+
log.info(` [_onFire] 开始执行任务: ${task.name} (第${task.runCount}次)`);
|
|
490
503
|
try {
|
|
491
504
|
await this._executeTask(task);
|
|
505
|
+
log.info(` [_onFire] <<< 任务执行完成: ${task.name}`);
|
|
492
506
|
} finally {
|
|
493
507
|
task.running = false;
|
|
494
508
|
}
|
|
@@ -890,12 +904,20 @@ class SchedulerPlugin extends Plugin {
|
|
|
890
904
|
// ============================================================
|
|
891
905
|
_loadPersistedTasks() {
|
|
892
906
|
const savedTasks = this._taskStore.loadTasks();
|
|
893
|
-
|
|
907
|
+
log.info(` [_loadPersistedTasks] 开始加载任务,当前时间: ${new Date().toISOString()}`);
|
|
908
|
+
if (!savedTasks || savedTasks.length === 0) {
|
|
909
|
+
log.info(` [_loadPersistedTasks] 没有已保存的任务`);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
log.info(` [_loadPersistedTasks] 从磁盘加载了 ${savedTasks.length} 个任务`);
|
|
894
913
|
const now = Date.now();
|
|
895
914
|
|
|
896
915
|
for (const saved of savedTasks) {
|
|
897
916
|
// 已完成的 once 任务不再恢复
|
|
898
|
-
if (saved.type === 'once' && (saved.runCount || 0) > 0)
|
|
917
|
+
if (saved.type === 'once' && (saved.runCount || 0) > 0) {
|
|
918
|
+
log.debug(` [_loadPersistedTasks] 跳过已完成的 once 任务: ${saved.name}`);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
899
921
|
|
|
900
922
|
const task = {
|
|
901
923
|
...saved,
|
|
@@ -910,19 +932,25 @@ class SchedulerPlugin extends Plugin {
|
|
|
910
932
|
try {
|
|
911
933
|
if (task.type === 'cron' && task.enabled && task.cronExpression) {
|
|
912
934
|
task.scheduleHandle = new Cron(task.cronExpression, this._cronOptions(task), () => this._onFire(task));
|
|
935
|
+
const nextRun = task.scheduleHandle.nextRun();
|
|
936
|
+
log.info(` [_loadPersistedTasks] 恢复 cron 任务: ${task.name} cron="${task.cronExpression}" 下次执行: ${nextRun ? nextRun.toISOString() : 'N/A'}`);
|
|
913
937
|
this._tasks.set(task.id, task);
|
|
914
938
|
} else if (task.type === 'once' && task.enabled) {
|
|
915
|
-
if (!task.runAt)
|
|
939
|
+
if (!task.runAt) {
|
|
940
|
+
log.warn(` [_loadPersistedTasks] 跳过没有 runAt 的 once 任务: ${task.name}`);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
916
943
|
const fireAt = task.runAt.getTime();
|
|
917
944
|
if (fireAt <= now) {
|
|
918
945
|
// 已过期:超过 grace period 就丢弃
|
|
919
946
|
if (now - fireAt > ONCE_EXPIRY_GRACE_MS) {
|
|
920
|
-
log.warn(` 丢弃过期任务 ${task.name}(已过期 ${Math.round((now - fireAt) / 1000)}s,超出 24h 容忍期)`);
|
|
947
|
+
log.warn(` [_loadPersistedTasks] 丢弃过期任务 ${task.name}(已过期 ${Math.round((now - fireAt) / 1000)}s,超出 24h 容忍期)`);
|
|
921
948
|
continue;
|
|
922
949
|
}
|
|
923
950
|
// B4/B5 修复:catchup 也用 croner 调度过去时间,croner 会立即触发;
|
|
924
951
|
// stop() 能真正取消,update 走 stop()+new Cron() 不会双触发
|
|
925
952
|
// protect:false 是因为"补触发"不应被 startup 期间的并发阻塞
|
|
953
|
+
log.info(` [_loadPersistedTasks] 恢复过期的 once 任务(立即触发): ${task.name} 原定执行: ${task.runAt.toISOString()}`);
|
|
926
954
|
task.scheduleHandle = new Cron(task.runAt, {
|
|
927
955
|
...this._cronOptions(task),
|
|
928
956
|
protect: false,
|
|
@@ -930,15 +958,18 @@ class SchedulerPlugin extends Plugin {
|
|
|
930
958
|
this._tasks.set(task.id, task);
|
|
931
959
|
} else {
|
|
932
960
|
// 未来触发
|
|
961
|
+
log.info(` [_loadPersistedTasks] 恢复 future once 任务: ${task.name} 执行时间: ${task.runAt.toISOString()}`);
|
|
933
962
|
task.scheduleHandle = new Cron(task.runAt, this._cronOptions(task), () => this._onFire(task));
|
|
934
963
|
this._tasks.set(task.id, task);
|
|
935
964
|
}
|
|
965
|
+
} else {
|
|
966
|
+
log.debug(` [_loadPersistedTasks] 跳过任务: ${task.name} type=${task.type} enabled=${task.enabled}`);
|
|
936
967
|
}
|
|
937
968
|
} catch (err) {
|
|
938
|
-
log.error(` 恢复任务失败: ${task.name}`, err.message);
|
|
969
|
+
log.error(` [_loadPersistedTasks] 恢复任务失败: ${task.name}`, err.message);
|
|
939
970
|
}
|
|
940
971
|
}
|
|
941
|
-
log.info(`
|
|
972
|
+
log.info(` [_loadPersistedTasks] 完成,共恢复 ${this._tasks.size} 个定时任务`);
|
|
942
973
|
}
|
|
943
974
|
|
|
944
975
|
// ============================================================
|
|
@@ -263,6 +263,59 @@ function buildSchemaFromType(type, desc, defaultValue) {
|
|
|
263
263
|
return schema;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* 从 options 构建 JSON Schema(供 AI SDK 直接使用,不走 Zod)
|
|
268
|
+
* 用于替换旧的 CLI 字符串模式,让 LLM 能用结构化参数调用
|
|
269
|
+
*
|
|
270
|
+
* type 可选值:'string' | 'number' | 'boolean' | 'array' | 'object'
|
|
271
|
+
* (对应 JS 原生类型 String, Boolean, Number, Array, Object)
|
|
272
|
+
*/
|
|
273
|
+
function buildInputSchemaFromOptions(options) {
|
|
274
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
275
|
+
return { type: 'object', properties: {}, required: [] };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const properties = {};
|
|
279
|
+
const required = [];
|
|
280
|
+
|
|
281
|
+
for (const opt of options) {
|
|
282
|
+
const name = extractOptionName(opt.flags);
|
|
283
|
+
if (!name) continue;
|
|
284
|
+
|
|
285
|
+
const hasValue = optionHasValue(opt.flags);
|
|
286
|
+
const isRequired = opt.required === true && hasValue;
|
|
287
|
+
const desc = opt.description || '';
|
|
288
|
+
const defaultVal = opt.defaultValue;
|
|
289
|
+
|
|
290
|
+
let jsonType = 'string';
|
|
291
|
+
if (opt.schema && typeof opt.schema.parse === 'function') {
|
|
292
|
+
// 从 Zod 实例猜测类型
|
|
293
|
+
const s = opt.schema;
|
|
294
|
+
if (s._def.typeName === 'ZodNumber' || s._def.typeName === 'ZodInt') jsonType = 'number';
|
|
295
|
+
else if (s._def.typeName === 'ZodBoolean') jsonType = 'boolean';
|
|
296
|
+
else if (s._def.typeName === 'ZodArray') jsonType = 'array';
|
|
297
|
+
else if (s._def.typeName === 'ZodObject' || s._def.typeName === 'ZodRecord') jsonType = 'object';
|
|
298
|
+
} else if (opt.type) {
|
|
299
|
+
const L = opt.type.toLowerCase();
|
|
300
|
+
if (L === 'number' || L === 'int' || L === 'integer') jsonType = 'number';
|
|
301
|
+
else if (L === 'boolean' || L === 'bool') jsonType = 'boolean';
|
|
302
|
+
else if (L === 'array') jsonType = 'array';
|
|
303
|
+
else if (L === 'object') jsonType = 'object';
|
|
304
|
+
} else {
|
|
305
|
+
jsonType = hasValue ? 'string' : 'boolean';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const prop = { type: jsonType, description: desc };
|
|
309
|
+
if (defaultVal !== undefined && defaultVal !== null) {
|
|
310
|
+
prop.default = defaultVal;
|
|
311
|
+
}
|
|
312
|
+
properties[name] = prop;
|
|
313
|
+
if (isRequired) required.push(name);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { type: 'object', properties, required };
|
|
317
|
+
}
|
|
318
|
+
|
|
266
319
|
/**
|
|
267
320
|
* 生成等效的 JSON schema(用于 _rawSchema,让 ext_skill 渲染更清晰)
|
|
268
321
|
*/
|
|
@@ -329,6 +382,50 @@ function buildExampleFromOptions(options) {
|
|
|
329
382
|
return parts.join(' ');
|
|
330
383
|
}
|
|
331
384
|
|
|
385
|
+
/**
|
|
386
|
+
* 解析命令行参数字符串为结构化对象(供 messaging 插件 backward compatible 使用)
|
|
387
|
+
* 支持:--name value, -n value, --flag, -f
|
|
388
|
+
* @param {string} str - 参数字符串
|
|
389
|
+
* @param {Array} options - 选项定义数组
|
|
390
|
+
* @returns {Object} 解析后的参数对象
|
|
391
|
+
*/
|
|
392
|
+
function parseCommandArgs(str, options) {
|
|
393
|
+
if (typeof str !== 'string' || !str.trim()) return {};
|
|
394
|
+
const result = {};
|
|
395
|
+
const tokens = parseShellArgs(str);
|
|
396
|
+
let i = 0;
|
|
397
|
+
while (i < tokens.length) {
|
|
398
|
+
const token = tokens[i];
|
|
399
|
+
const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
|
|
400
|
+
if (optMatch) {
|
|
401
|
+
const optKey = optMatch[1];
|
|
402
|
+
const opt = options.find(o => {
|
|
403
|
+
const flags = o.flags || '';
|
|
404
|
+
return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
|
|
405
|
+
});
|
|
406
|
+
if (opt) {
|
|
407
|
+
const name = extractOptionName(opt.flags);
|
|
408
|
+
const hasValue = optionHasValue(opt.flags);
|
|
409
|
+
if (hasValue && i + 1 < tokens.length) {
|
|
410
|
+
const next = tokens[i + 1];
|
|
411
|
+
if (!next.match(/^-/)) {
|
|
412
|
+
result[name] = next;
|
|
413
|
+
i += 2;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
} else if (!hasValue) {
|
|
417
|
+
result[name] = true;
|
|
418
|
+
i += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
i += 1;
|
|
424
|
+
}
|
|
425
|
+
if (i < tokens.length) result._ = tokens.slice(i);
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
332
429
|
/**
|
|
333
430
|
* Skill 类
|
|
334
431
|
*/
|
|
@@ -356,6 +453,17 @@ class Skill {
|
|
|
356
453
|
return [];
|
|
357
454
|
}
|
|
358
455
|
|
|
456
|
+
/**
|
|
457
|
+
* 简单命令行字符串解析器(backward compatible,供 messaging 插件使用)
|
|
458
|
+
* 支持:--name value, -n value, --flag, -f, positional args
|
|
459
|
+
* @param {string} str - 参数字符串
|
|
460
|
+
* @param {Array} options - 选项定义数组
|
|
461
|
+
* @returns {Object} 解析后的参数对象
|
|
462
|
+
*/
|
|
463
|
+
_parseCommandString(str, options) {
|
|
464
|
+
return parseCommandArgs(str, options);
|
|
465
|
+
}
|
|
466
|
+
|
|
359
467
|
/**
|
|
360
468
|
* 注册 skill 的命令到 extension 系统
|
|
361
469
|
* 异步版本:用动态 import() 替代 require(),支持 ESM 包
|
|
@@ -415,87 +523,13 @@ class Skill {
|
|
|
415
523
|
// 命令名格式: {skill_name}:{cmd_name}
|
|
416
524
|
const fullName = `${skillName}:${cmd.name}`;
|
|
417
525
|
|
|
418
|
-
//
|
|
419
|
-
let argumentParser = cmd.argumentParser;
|
|
420
|
-
if (!argumentParser && Array.isArray(cmd.options) && cmd.options.length > 0) {
|
|
421
|
-
argumentParser = (rawArgs) => {
|
|
422
|
-
try {
|
|
423
|
-
const program = new Command();
|
|
424
|
-
program.exitOverride(); // 不自动退出
|
|
425
|
-
program.configureOutput({ writeErr: () => {} }); // 隐藏错误输出
|
|
426
|
-
for (const opt of cmd.options) {
|
|
427
|
-
program.option(opt.flags, opt.description, opt.defaultValue);
|
|
428
|
-
}
|
|
429
|
-
program.arguments('[args...]');
|
|
430
|
-
program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
|
|
431
|
-
return { ...program.opts(), args: program.args };
|
|
432
|
-
} catch (err) {
|
|
433
|
-
log.warn(`[SkillManager] Command parse error: ${err.message}`);
|
|
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;
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// 直接使用 command 字符串的 schema,describe 说明命令行格式
|
|
487
|
-
const exampleArgs = cmd.options && cmd.options.length > 0
|
|
488
|
-
? buildExampleFromOptions(cmd.options)
|
|
489
|
-
: '';
|
|
526
|
+
// 新模式:结构化参数 schema,让 LLM 直接用 JSON 对象调用
|
|
490
527
|
const toolDef = {
|
|
491
528
|
name: cmd.name,
|
|
492
529
|
description: cmd.description || 'Skill command',
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}),
|
|
530
|
+
// 直接用 JSON Schema,不走 Zod(让 LLM 看到清晰的结构化参数)
|
|
531
|
+
inputSchema: buildInputSchemaFromOptions(cmd.options),
|
|
496
532
|
_options: cmd.options || null,
|
|
497
|
-
// 把原始 options 数组也保存下来,便于 ext_skill 显示
|
|
498
|
-
_rawSchema: buildRawJsonSchema(cmd.options),
|
|
499
533
|
execute: async (toolArgs, framework) => {
|
|
500
534
|
const ctx = {
|
|
501
535
|
sessionId: framework?._currentSessionId || 'unknown',
|
|
@@ -503,13 +537,13 @@ class Skill {
|
|
|
503
537
|
framework: framework,
|
|
504
538
|
cwd: framework?.getCwd?.() ?? process.cwd(),
|
|
505
539
|
};
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
let
|
|
509
|
-
if (typeof
|
|
510
|
-
|
|
540
|
+
// backward compatible:如果 args.command 是字符串(messaging 插件传来),
|
|
541
|
+
// 用 options 做简单解析;否则直接传递结构化参数(AI SDK / ext_call 传来)
|
|
542
|
+
let args = toolArgs || {};
|
|
543
|
+
if (typeof args.command === 'string' && Array.isArray(cmd.options) && cmd.options.length > 0) {
|
|
544
|
+
args = parseCommandArgs(args.command, cmd.options);
|
|
511
545
|
}
|
|
512
|
-
return await handler(
|
|
546
|
+
return await handler(args, ctx);
|
|
513
547
|
},
|
|
514
548
|
};
|
|
515
549
|
|
|
@@ -1283,72 +1317,12 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1283
1317
|
|
|
1284
1318
|
const fullName = `${name}:${cmd.name}`;
|
|
1285
1319
|
|
|
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
|
-
: '';
|
|
1320
|
+
// 新模式:结构化参数 schema
|
|
1344
1321
|
const toolDef = {
|
|
1345
|
-
name: cmd.name,
|
|
1322
|
+
name: cmd.name,
|
|
1346
1323
|
description: cmd.description || 'Skill command',
|
|
1347
|
-
inputSchema:
|
|
1348
|
-
command: z.string().optional().describe(exampleArgs2 || '命令行字符串'),
|
|
1349
|
-
}),
|
|
1324
|
+
inputSchema: buildInputSchemaFromOptions(cmd.options),
|
|
1350
1325
|
_options: cmd.options || null,
|
|
1351
|
-
_rawSchema: buildRawJsonSchema(cmd.options),
|
|
1352
1326
|
execute: async (toolArgs, framework) => {
|
|
1353
1327
|
const ctx = {
|
|
1354
1328
|
sessionId: framework?._currentSessionId || 'unknown',
|
|
@@ -1356,12 +1330,12 @@ class SkillManagerPlugin extends Plugin {
|
|
|
1356
1330
|
framework: framework,
|
|
1357
1331
|
cwd: framework?.getCwd?.() ?? process.cwd(),
|
|
1358
1332
|
};
|
|
1359
|
-
|
|
1360
|
-
let
|
|
1361
|
-
if (typeof
|
|
1362
|
-
|
|
1333
|
+
// backward compatible:如果 args.command 是字符串,解析为结构化对象
|
|
1334
|
+
let args = toolArgs || {};
|
|
1335
|
+
if (typeof args.command === 'string' && Array.isArray(cmd.options) && cmd.options.length > 0) {
|
|
1336
|
+
args = parseCommandArgs(args.command, cmd.options);
|
|
1363
1337
|
}
|
|
1364
|
-
return await handler(
|
|
1338
|
+
return await handler(args, ctx);
|
|
1365
1339
|
},
|
|
1366
1340
|
};
|
|
1367
1341
|
|
|
@@ -345,14 +345,41 @@ class ExtensionPromptBuilder {
|
|
|
345
345
|
_formatSchema(schema, rawSchema) {
|
|
346
346
|
// 优先用 zodSchemaToMarkdown(MCP/普通扩展都走这条路径)
|
|
347
347
|
try {
|
|
348
|
-
|
|
349
|
-
if (
|
|
348
|
+
// Zod 实例有 _def.typeName,纯 JSON Schema 没有
|
|
349
|
+
if (schema && schema._def && schema._def.typeName) {
|
|
350
|
+
const md = zodSchemaToMarkdown(schema);
|
|
351
|
+
if (md && !/Zod\w+/.test(md)) return md;
|
|
352
|
+
}
|
|
350
353
|
} catch { /* fall through */ }
|
|
351
354
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
const table = zodSchemaToTable(schema);
|
|
356
|
+
if (table) return table;
|
|
357
|
+
} catch { /* fall through */ }
|
|
358
|
+
|
|
359
|
+
// 兜底:纯 JSON Schema({type,properties,required})
|
|
360
|
+
if (schema && schema.properties) {
|
|
361
|
+
return this._jsonSchemaToMarkdown(schema);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return '无';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 将纯 JSON Schema 对象格式化为 Markdown 表格
|
|
369
|
+
*/
|
|
370
|
+
_jsonSchemaToMarkdown(schema) {
|
|
371
|
+
const { properties = {}, required = [] } = schema;
|
|
372
|
+
if (Object.keys(properties).length === 0) return '无';
|
|
373
|
+
|
|
374
|
+
const lines = ['| 参数 | 类型 | 必填 | 默认 | 描述 |', '|------|------|------|------|------|'];
|
|
375
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
376
|
+
const type = prop.type || 'any';
|
|
377
|
+
const desc = prop.description || '';
|
|
378
|
+
const isRequired = required.includes(name);
|
|
379
|
+
const def = prop.default !== undefined ? String(prop.default) : '';
|
|
380
|
+
lines.push(`| \`${name}\` | ${type} | ${isRequired ? '✅' : '❌'} | ${def} | ${desc} |`);
|
|
355
381
|
}
|
|
382
|
+
return lines.join('\n');
|
|
356
383
|
}
|
|
357
384
|
}
|
|
358
385
|
|
|
@@ -45,6 +45,34 @@ class ShellExecutorPlugin extends Plugin {
|
|
|
45
45
|
execute: async (args) => {
|
|
46
46
|
const cwd = args.cwd || this.config.workingDir
|
|
47
47
|
const timeout = args.timeout || this.config.timeout
|
|
48
|
+
const command = args.command || ''
|
|
49
|
+
|
|
50
|
+
// 检测 /skillname:cmdname args 格式,转到 skill executor
|
|
51
|
+
if (command.startsWith('/')) {
|
|
52
|
+
const colonIdx = command.indexOf(':');
|
|
53
|
+
if (colonIdx > 1) {
|
|
54
|
+
const skillCmd = command.slice(1); // 去掉前导 /
|
|
55
|
+
const spaceIdx = command.indexOf(' ');
|
|
56
|
+
const cmdPart = spaceIdx > 0 ? command.slice(1, spaceIdx) : command.slice(1);
|
|
57
|
+
const cmdArgs = spaceIdx > 0 ? command.slice(spaceIdx + 1).trim() : '';
|
|
58
|
+
|
|
59
|
+
const [skillName, ...rest] = cmdPart.split(':');
|
|
60
|
+
const cmdName = rest.join(':');
|
|
61
|
+
if (skillName && cmdName) {
|
|
62
|
+
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
63
|
+
if (extExecutor && typeof extExecutor.executeSkillCommand === 'function') {
|
|
64
|
+
try {
|
|
65
|
+
const result = await extExecutor.executeSkillCommand(`${skillName}:${cmdName}`, cmdArgs);
|
|
66
|
+
if (result) {
|
|
67
|
+
return { success: true, stdout: result.data || result, stderr: '', command, skillCommand: true };
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { success: false, error: err.message, command, skillCommand: true };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
48
76
|
|
|
49
77
|
return new Promise((resolve) => {
|
|
50
78
|
const startTime = Date.now()
|
|
@@ -100,24 +100,24 @@ frontmatter 之后是技能的正文内容,支持 Markdown 格式,包含:
|
|
|
100
100
|
打招呼命令。
|
|
101
101
|
|
|
102
102
|
**参数:**
|
|
103
|
-
- `
|
|
103
|
+
- `name`(可选):名字,默认 "World"
|
|
104
|
+
- `lang`(可选):语言,如 "en",默认 "zh"
|
|
104
105
|
|
|
105
106
|
**示例:**
|
|
106
107
|
- `/my-skill:greet` → "Hello World"
|
|
107
|
-
- `/my-skill:greet Alice` → "Hello Alice"
|
|
108
|
+
- `/my-skill:greet {"name":"Alice"}` → "Hello Alice"
|
|
108
109
|
|
|
109
110
|
### /my-skill:split
|
|
110
111
|
|
|
111
112
|
切割会话命令。
|
|
112
113
|
|
|
113
114
|
**参数:**
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
- `[args...]`:位置参数
|
|
115
|
+
- `tokens`(可选):保留 token 数,默认 4000
|
|
116
|
+
- `keepRecent`(可选):保留最近 token,默认 1000
|
|
117
117
|
|
|
118
118
|
**示例:**
|
|
119
119
|
- `/my-skill:split` → 使用默认参数
|
|
120
|
-
- `/my-skill:split
|
|
120
|
+
- `/my-skill:split {"tokens":8000}` → tokens=8000
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
### index.js 格式
|
|
@@ -127,23 +127,26 @@ module.exports = [
|
|
|
127
127
|
{
|
|
128
128
|
name: 'greet', // 命令名
|
|
129
129
|
description: '打招呼', // 命令描述
|
|
130
|
+
options: [
|
|
131
|
+
{ flags: '-n, --name <value>', description: '名字', defaultValue: 'World' },
|
|
132
|
+
{ flags: '-l, --lang <value>', description: '语言', defaultValue: 'zh' }
|
|
133
|
+
],
|
|
130
134
|
execute: async (args, ctx) => {
|
|
131
|
-
// args:
|
|
135
|
+
// args: 解析后的结构化对象,如 { name: "Alice", lang: "en" }
|
|
132
136
|
// ctx: 上下文对象 { sessionId, agent, framework }
|
|
133
|
-
return
|
|
137
|
+
return `Hello ${args.name || 'World'}`;
|
|
134
138
|
}
|
|
135
139
|
},
|
|
136
140
|
{
|
|
137
141
|
name: 'split',
|
|
138
142
|
description: '切割会话',
|
|
139
|
-
// 简洁方式:定义选项数组,自动使用 Commander.js 解析
|
|
140
143
|
options: [
|
|
141
|
-
{ flags: '-t, --tokens <value>', description: '保留 token 数', defaultValue:
|
|
142
|
-
{ flags: '-k, --keep-recent <value>', description: '保留最近 token', defaultValue:
|
|
144
|
+
{ flags: '-t, --tokens <value>', type: 'number', description: '保留 token 数', defaultValue: 4000 },
|
|
145
|
+
{ flags: '-k, --keep-recent <value>', type: 'number', description: '保留最近 token', defaultValue: 1000 }
|
|
143
146
|
],
|
|
144
|
-
execute: async (
|
|
145
|
-
//
|
|
146
|
-
return `Split: tokens=${
|
|
147
|
+
execute: async (args, ctx) => {
|
|
148
|
+
// args: { tokens: 4000, keepRecent: 1000 }
|
|
149
|
+
return `Split: tokens=${args.tokens}`;
|
|
147
150
|
}
|
|
148
151
|
},
|
|
149
152
|
{
|
|
@@ -156,11 +159,12 @@ module.exports = [
|
|
|
156
159
|
];
|
|
157
160
|
```
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
**参数类型说明:**
|
|
160
163
|
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
+
- `options[].type`:可选,声明参数类型 (`String` | `Number` | `Boolean` | `Array` | `Object`),帮助 LLM 理解参数格式
|
|
165
|
+
- 不提供 `options` 时,`args` 是空对象 `{}`
|
|
166
|
+
- 提供 `options` 数组时,`execute` 收到解析后的结构化对象
|
|
167
|
+
- 对象格式:`{ name, lang, tokens, keepRecent, ... }`(camelCase,匹配 flag 名称)
|
|
164
168
|
|
|
165
169
|
### 命令调用方式
|
|
166
170
|
|
|
@@ -182,11 +186,11 @@ module.exports = [
|
|
|
182
186
|
// 1. 先查询命令参数
|
|
183
187
|
ext_skill({ plugin: "skill:<技能名>" })
|
|
184
188
|
|
|
185
|
-
// 2.
|
|
189
|
+
// 2. 用结构化参数调用
|
|
186
190
|
ext_call({
|
|
187
191
|
plugin: "skill:<技能名>",
|
|
188
192
|
tool: "<commandname>",
|
|
189
|
-
args: {
|
|
193
|
+
args: { name: "Alice", lang: "en" }
|
|
190
194
|
});
|
|
191
195
|
```
|
|
192
196
|
|
|
@@ -196,30 +200,30 @@ ext_call({
|
|
|
196
200
|
|------|------|------|
|
|
197
201
|
| `plugin` | string | 固定为 `"skill:<技能名>"`,如 `skill:test-skill` |
|
|
198
202
|
| `tool` | string | 命令名(不带技能前缀),如 `greet` |
|
|
199
|
-
| `args` | object |
|
|
203
|
+
| `args` | object | 结构化参数对象,字段名对应 `options` 中的参数名(camelCase) |
|
|
200
204
|
|
|
201
205
|
**示例:**
|
|
202
206
|
|
|
203
207
|
```javascript
|
|
204
|
-
// 调用 test-skill 的 greet
|
|
208
|
+
// 调用 test-skill 的 greet 命令(带参数)
|
|
205
209
|
ext_call({
|
|
206
210
|
plugin: "skill:test-skill",
|
|
207
211
|
tool: "greet",
|
|
208
|
-
args: {
|
|
212
|
+
args: { name: "Alice", lang: "en" }
|
|
209
213
|
});
|
|
210
214
|
|
|
211
|
-
// 调用 test-skill 的 greet
|
|
215
|
+
// 调用 test-skill 的 greet 命令(使用默认参数)
|
|
212
216
|
ext_call({
|
|
213
217
|
plugin: "skill:test-skill",
|
|
214
218
|
tool: "greet",
|
|
215
|
-
args: {
|
|
219
|
+
args: {}
|
|
216
220
|
});
|
|
217
221
|
|
|
218
|
-
// 调用 test-skill 的
|
|
222
|
+
// 调用 test-skill 的 split 命令
|
|
219
223
|
ext_call({
|
|
220
224
|
plugin: "skill:test-skill",
|
|
221
|
-
tool: "
|
|
222
|
-
args: {
|
|
225
|
+
tool: "split",
|
|
226
|
+
args: { tokens: 8000 }
|
|
223
227
|
});
|
|
224
228
|
```
|
|
225
229
|
|
|
@@ -234,8 +238,23 @@ ext_call({
|
|
|
234
238
|
|
|
235
239
|
```javascript
|
|
236
240
|
module.exports = [
|
|
237
|
-
{
|
|
238
|
-
|
|
241
|
+
{
|
|
242
|
+
name: 'greet',
|
|
243
|
+
description: '打招呼',
|
|
244
|
+
options: [
|
|
245
|
+
{ flags: '-n, --name <value>', description: '名字', defaultValue: 'World' },
|
|
246
|
+
{ flags: '-l, --lang <value>', description: '语言', defaultValue: 'zh' }
|
|
247
|
+
],
|
|
248
|
+
execute: async (args) => `Hello ${args.name || 'World'}`
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'echo',
|
|
252
|
+
description: '回声命令',
|
|
253
|
+
options: [
|
|
254
|
+
{ flags: '-t, --text <value>', description: '回显文本', required: true }
|
|
255
|
+
],
|
|
256
|
+
execute: async (args) => 'Echo: ' + args.text
|
|
257
|
+
}
|
|
239
258
|
];
|
|
240
259
|
```
|
|
241
260
|
|
|
@@ -249,21 +268,22 @@ module.exports = [
|
|
|
249
268
|
打招呼命令。
|
|
250
269
|
|
|
251
270
|
**参数:**
|
|
252
|
-
- `
|
|
271
|
+
- `name`(可选):名字,默认 "World"
|
|
272
|
+
- `lang`(可选):语言,默认 "zh"
|
|
253
273
|
|
|
254
274
|
**示例:**
|
|
255
275
|
- `/test-skill:greet` → "Hello World"
|
|
256
|
-
- `/test-skill:greet Alice` → "Hello Alice"
|
|
276
|
+
- `/test-skill:greet {"name":"Alice"}` → "Hello Alice"
|
|
257
277
|
|
|
258
278
|
### /test-skill:echo
|
|
259
279
|
|
|
260
280
|
回声命令,将输入原样返回。
|
|
261
281
|
|
|
262
282
|
**参数:**
|
|
263
|
-
- `
|
|
283
|
+
- `text`(必填):要回显的文本
|
|
264
284
|
|
|
265
285
|
**示例:**
|
|
266
|
-
- `/test-skill:echo Hello` → "Echo: Hello"
|
|
286
|
+
- `/test-skill:echo {"text":"Hello"}` → "Echo: Hello"
|
|
267
287
|
```
|
|
268
288
|
|
|
269
289
|
### 自动注册
|
|
@@ -338,23 +358,26 @@ module.exports = [
|
|
|
338
358
|
{
|
|
339
359
|
name: 'greet', // 命令名
|
|
340
360
|
description: '打招呼', // 命令描述
|
|
361
|
+
options: [
|
|
362
|
+
{ flags: '-n, --name <value>', description: '名字', defaultValue: 'World' },
|
|
363
|
+
{ flags: '-l, --lang <value>', description: '语言', defaultValue: 'zh' }
|
|
364
|
+
],
|
|
341
365
|
execute: async (args, ctx) => {
|
|
342
|
-
// args:
|
|
366
|
+
// args: 解析后的结构化对象,如 { name: "Alice", lang: "en" }
|
|
343
367
|
// ctx: 上下文对象 { sessionId, agent, framework }
|
|
344
|
-
return
|
|
368
|
+
return `Hello ${args.name || 'World'}`;
|
|
345
369
|
}
|
|
346
370
|
},
|
|
347
371
|
{
|
|
348
372
|
name: 'split',
|
|
349
373
|
description: '切割会话',
|
|
350
|
-
// 简洁方式:定义选项数组,自动使用 Commander.js 解析
|
|
351
374
|
options: [
|
|
352
|
-
{ flags: '-t, --tokens <value>', description: '保留 token 数', defaultValue:
|
|
353
|
-
{ flags: '-k, --keep-recent <value>', description: '保留最近 token', defaultValue:
|
|
375
|
+
{ flags: '-t, --tokens <value>', type: 'number', description: '保留 token 数', defaultValue: 4000 },
|
|
376
|
+
{ flags: '-k, --keep-recent <value>', type: 'number', description: '保留最近 token', defaultValue: 1000 }
|
|
354
377
|
],
|
|
355
|
-
execute: async (
|
|
356
|
-
//
|
|
357
|
-
return `Split: tokens=${
|
|
378
|
+
execute: async (args, ctx) => {
|
|
379
|
+
// args: { tokens: 4000, keepRecent: 1000 }
|
|
380
|
+
return `Split: tokens=${args.tokens}`;
|
|
358
381
|
}
|
|
359
382
|
},
|
|
360
383
|
{
|
|
@@ -367,11 +390,12 @@ module.exports = [
|
|
|
367
390
|
];
|
|
368
391
|
```
|
|
369
392
|
|
|
370
|
-
|
|
393
|
+
**参数类型说明:**
|
|
371
394
|
|
|
372
|
-
-
|
|
373
|
-
-
|
|
374
|
-
-
|
|
395
|
+
- `options[].type`:可选,声明参数类型 (`String` | `Number` | `Boolean` | `Array` | `Object`),帮助 LLM 理解参数格式
|
|
396
|
+
- 不提供 `options` 时,`args` 是空对象 `{}`
|
|
397
|
+
- 提供 `options` 数组时,`execute` 收到解析后的结构化对象
|
|
398
|
+
- 对象格式:`{ name, lang, tokens, keepRecent, ... }`(camelCase,匹配 flag 名称)
|
|
375
399
|
|
|
376
400
|
### 命令调用方式
|
|
377
401
|
|
|
@@ -393,11 +417,11 @@ module.exports = [
|
|
|
393
417
|
// 1. 先查询命令参数
|
|
394
418
|
ext_skill({ plugin: "skill:<技能名>" })
|
|
395
419
|
|
|
396
|
-
// 2.
|
|
420
|
+
// 2. 用结构化参数调用
|
|
397
421
|
ext_call({
|
|
398
422
|
plugin: "skill:<技能名>",
|
|
399
423
|
tool: "<commandname>",
|
|
400
|
-
args: {
|
|
424
|
+
args: { name: "Alice", lang: "en" }
|
|
401
425
|
});
|
|
402
426
|
```
|
|
403
427
|
|
|
@@ -407,30 +431,30 @@ ext_call({
|
|
|
407
431
|
|------|------|------|
|
|
408
432
|
| `plugin` | string | 固定为 `"skill:<技能名>"`,如 `skill:test-skill` |
|
|
409
433
|
| `tool` | string | 命令名(不带技能前缀),如 `greet` |
|
|
410
|
-
| `args` | object |
|
|
434
|
+
| `args` | object | 结构化参数对象,字段名对应 `options` 中的参数名(camelCase) |
|
|
411
435
|
|
|
412
436
|
**示例:**
|
|
413
437
|
|
|
414
438
|
```javascript
|
|
415
|
-
// 调用 test-skill 的 greet
|
|
439
|
+
// 调用 test-skill 的 greet 命令(带参数)
|
|
416
440
|
ext_call({
|
|
417
441
|
plugin: "skill:test-skill",
|
|
418
442
|
tool: "greet",
|
|
419
|
-
args: {
|
|
443
|
+
args: { name: "Alice", lang: "en" }
|
|
420
444
|
});
|
|
421
445
|
|
|
422
|
-
// 调用 test-skill 的 greet
|
|
446
|
+
// 调用 test-skill 的 greet 命令(使用默认参数)
|
|
423
447
|
ext_call({
|
|
424
448
|
plugin: "skill:test-skill",
|
|
425
449
|
tool: "greet",
|
|
426
|
-
args: {
|
|
450
|
+
args: {}
|
|
427
451
|
});
|
|
428
452
|
|
|
429
|
-
// 调用 test-skill 的
|
|
453
|
+
// 调用 test-skill 的 split 命令
|
|
430
454
|
ext_call({
|
|
431
455
|
plugin: "skill:test-skill",
|
|
432
|
-
tool: "
|
|
433
|
-
args: {
|
|
456
|
+
tool: "split",
|
|
457
|
+
args: { tokens: 8000 }
|
|
434
458
|
});
|
|
435
459
|
```
|
|
436
460
|
|
|
@@ -445,16 +469,31 @@ ext_call({
|
|
|
445
469
|
|
|
446
470
|
```javascript
|
|
447
471
|
module.exports = [
|
|
448
|
-
{
|
|
449
|
-
|
|
472
|
+
{
|
|
473
|
+
name: 'greet',
|
|
474
|
+
description: '打招呼',
|
|
475
|
+
options: [
|
|
476
|
+
{ flags: '-n, --name <value>', description: '名字', defaultValue: 'World' },
|
|
477
|
+
{ flags: '-l, --lang <value>', description: '语言', defaultValue: 'zh' }
|
|
478
|
+
],
|
|
479
|
+
execute: async (args) => `Hello ${args.name || 'World'}`
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: 'echo',
|
|
483
|
+
description: '回声命令',
|
|
484
|
+
options: [
|
|
485
|
+
{ flags: '-t, --text <value>', description: '回显文本', required: true }
|
|
486
|
+
],
|
|
487
|
+
execute: async (args) => 'Echo: ' + args.text
|
|
488
|
+
}
|
|
450
489
|
];
|
|
451
490
|
```
|
|
452
491
|
|
|
453
492
|
**使用方法:**
|
|
454
493
|
|
|
455
494
|
- `/test-skill:greet` → 返回 "Hello World"
|
|
456
|
-
- `/test-skill:greet Alice` → 返回 "Hello Alice"
|
|
457
|
-
- `/test-skill:echo Hello` → 返回 "Echo: Hello"
|
|
495
|
+
- `/test-skill:greet {"name":"Alice"}` → 返回 "Hello Alice"
|
|
496
|
+
- `/test-skill:echo {"text":"Hello"}` → 返回 "Echo: Hello"
|
|
458
497
|
|
|
459
498
|
### 自动注册
|
|
460
499
|
|