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.
@@ -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
- - 普通扩展/MCP/Python:`args` 是结构化对象;Skill 命令:`args: { command: "..." }` 命令行字符串
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: { command: "..." } })`
394
+ > - Skill 命令:`ext_call({ plugin: "skill:<skillname>", tool: "<command>", args: { name: "...", lang: "..." } })`
395
395
  > - 调用前先 `ext_skill({ plugin: "<name>" })` 查看参数结构
396
- > - 普通扩展/MCP/Python:`args` 是结构化对象;Skill 命令:`args: { command: "..." }`
396
+ > - 扩展调用:`args` 是结构化对象,参数名对应工具定义的字段(camelCase)
397
397
 
398
398
  ### 子 Agent 管理工具
399
399
 
@@ -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: { command: "-n Claude -l en -s" }
120
+ args: { name: "Claude", lang: "en", shout: true }
122
121
  })
123
122
  ```
124
123
 
125
- **args 传 `command` 字符串**,commander.js 解析后 handler 收到 `{ name: 'Claude', lang: 'en', shout: true }`。
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: { command: "-n x -v" }
87
+ args: { name: "x", verbose: true }
88
88
  })
89
89
  ```
90
90
 
91
91
  **要点**:
92
92
  - 每个 skill 是独立扩展 `skill:<name>`
93
93
  - 命令是该扩展下的 tool
94
- - `args: { command: "..." }` 传命令行字符串
95
- - Commander.js 解析后传给 handler
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: { command: "-n Claude -l en --shout" },
205
+ args: { name: "Claude", lang: "en", shout: true },
206
206
  })
207
207
  ```
208
208
 
209
209
  #### 参数传递
210
210
 
211
- Skill 命令参数通过 `args: { command: "..." }` 传递命令行字符串,由 Commander.js 解析后传给 handler:
211
+ Skill 命令参数直接通过 `args` 对象传递,无需解析命令行字符串:
212
212
 
213
213
  | flags | 解析后 args 字段 |
214
214
  |---|---|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -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
- return {
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
- if (!task.enabled) return;
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
- if (!savedTasks || savedTasks.length === 0) return;
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) continue;
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) continue;
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(` 已恢复 ${this._tasks.size} 个定时任务`);
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
- // 获取参数解析器(优先使用 argumentParser,否则用 options 构建)
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
- inputSchema: z.object({
494
- command: z.string().optional().describe(exampleArgs || '命令行字符串,如:-n value1 -v'),
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
- // 直接使用 command 字符串
507
- const commandString = toolArgs?.command || '';
508
- let parsedArgs = commandString;
509
- if (typeof argumentParser === 'function') {
510
- parsedArgs = argumentParser(commandString);
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(parsedArgs, ctx);
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: z.object({
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
- const commandString = toolArgs?.command || '';
1360
- let parsedArgs = commandString;
1361
- if (typeof argumentParser === 'function') {
1362
- parsedArgs = argumentParser(commandString);
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(parsedArgs, ctx);
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
- const md = zodSchemaToMarkdown(schema);
349
- if (md && !/Zod\w+/.test(md)) return md;
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
- return zodSchemaToTable(schema) || '无';
353
- } catch {
354
- return '无';
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
- - `args`(可选):名字,如 `/my-skill:greet Alice`
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
- - `-t, --tokens <value>`:保留 token 数(默认 4000
115
- - `-k, --keep-recent <value>`:保留最近 token(默认 1000
116
- - `[args...]`:位置参数
115
+ - `tokens`(可选):保留 token 数,默认 4000
116
+ - `keepRecent`(可选):保留最近 token,默认 1000
117
117
 
118
118
  **示例:**
119
119
  - `/my-skill:split` → 使用默认参数
120
- - `/my-skill:split -t 8000 extra` → tokens=8000,extra 作为位置参数
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 'Hello ' + (args || 'World');
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: '4000' },
142
- { flags: '-k, --keep-recent <value>', description: '保留最近 token', defaultValue: '1000' }
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 (parsedArgs, ctx) => {
145
- // parsedArgs: { tokens: "4000", keepRecent: "1000", args: [...] }
146
- return `Split: tokens=${parsedArgs.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
- - 不提供 `options` 时,`args` 是原始参数字符串
162
- - 提供 `options` 数组时,自动使用 Commander.js 解析,`execute` 收到解析后的对象
163
- - 对象格式:`{ tokens, keepRecent, args: [...] }`
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: { command: "-n Alice -l en" }
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 | 固定为 `{ command: "命令行字符串" }` |
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: { command: "-n Alice" }
212
+ args: { name: "Alice", lang: "en" }
209
213
  });
210
214
 
211
- // 调用 test-skill 的 greet 命令,带可选 lang
215
+ // 调用 test-skill 的 greet 命令(使用默认参数)
212
216
  ext_call({
213
217
  plugin: "skill:test-skill",
214
218
  tool: "greet",
215
- args: { command: "-n Alice -l en" }
219
+ args: {}
216
220
  });
217
221
 
218
- // 调用 test-skill 的 echo 命令
222
+ // 调用 test-skill 的 split 命令
219
223
  ext_call({
220
224
  plugin: "skill:test-skill",
221
- tool: "echo",
222
- args: { command: "Hello World" }
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
- { name: 'greet', description: '打招呼', execute: async (args) => 'Hello ' + (args || 'World') },
238
- { name: 'echo', description: '回声命令', execute: async (args) => 'Echo: ' + args }
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
- - `args`(可选):名字
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
- - `args`:要回显的文本
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 'Hello ' + (args || 'World');
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: '4000' },
353
- { flags: '-k, --keep-recent <value>', description: '保留最近 token', defaultValue: '1000' }
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 (parsedArgs, ctx) => {
356
- // parsedArgs: { tokens: "4000", keepRecent: "1000", args: [...] }
357
- return `Split: tokens=${parsedArgs.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
- - 不提供 `options` 时,`args` 是原始参数字符串
373
- - 提供 `options` 数组时,自动使用 Commander.js 解析,`execute` 收到解析后的对象
374
- - 对象格式:`{ tokens, keepRecent, args: [...] }`
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: { command: "-n Alice -l en" }
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 | 固定为 `{ command: "命令行字符串" }` |
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: { command: "-n Alice" }
443
+ args: { name: "Alice", lang: "en" }
420
444
  });
421
445
 
422
- // 调用 test-skill 的 greet 命令,带可选 lang
446
+ // 调用 test-skill 的 greet 命令(使用默认参数)
423
447
  ext_call({
424
448
  plugin: "skill:test-skill",
425
449
  tool: "greet",
426
- args: { command: "-n Alice -l en" }
450
+ args: {}
427
451
  });
428
452
 
429
- // 调用 test-skill 的 echo 命令
453
+ // 调用 test-skill 的 split 命令
430
454
  ext_call({
431
455
  plugin: "skill:test-skill",
432
- tool: "echo",
433
- args: { command: "Hello World" }
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
- { name: 'greet', description: '打招呼', execute: async (args) => 'Hello ' + (args || 'World') },
449
- { name: 'echo', description: '回声命令', execute: async (args) => 'Echo: ' + args }
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