foliko 1.1.70 → 1.1.72

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.
@@ -59,7 +59,8 @@ const markdownTheme = {
59
59
  strikethrough: (text) => chalk.strikethrough(text),
60
60
  underline: (text) => chalk.underline(text),
61
61
  highlightCode: (code, lang) => {
62
- const highlighted = hl.highlight(code, { language: lang || 'text', theme: 'nord' });
62
+ const language = lang && hl.supportsLanguage(lang) ? lang : 'text';
63
+ const highlighted = hl.highlight(code, { language, theme: 'nord' });
63
64
  return highlighted.split('\n');
64
65
  },
65
66
  codeBlockIndent: "",
@@ -90,28 +91,25 @@ class ChatUI {
90
91
  this.sessionId = options.sessionId || 'cli_default';
91
92
  this.sessionPlugin = null;
92
93
 
93
- // 获取 skill 命令列表
94
- const skillCommands = [];
95
- if (agent.framework) {
96
- const extExecutor = agent.framework.pluginManager?.get('extension-executor');
97
- if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
98
- skillCommands.push(...extExecutor.getSkillCommands());
99
- }
100
- }
94
+ // 获取 skill 命令列表(保存到 this,供后续刷新)
95
+ this.skillCommands = [];
96
+ this._refreshSkillCommands(agent.framework);
101
97
 
102
- const baseCommands = [
98
+ const baseCommands = this.baseCommands = [
103
99
  { name: "compress", description: "压缩记录" },
104
100
  { name: "clear", description: "清除记录" },
105
101
  { name: "exit", description: "退出" },
102
+ { name: "help", description: "显示帮助信息" },
103
+ { name: "reload", description: "重载技能" },
106
104
  ];
107
105
 
108
- const autocompleteProvider = new CombinedAutocompleteProvider(
109
- [...baseCommands, ...skillCommands],
106
+ this.autocompleteProvider = new CombinedAutocompleteProvider(
107
+ [...baseCommands, ...this.skillCommands],
110
108
  process.cwd(),
111
109
  );
112
-
110
+
113
111
  const editor=this.editor = new Editor(this.tui, editorTheme,{paddingX:1});
114
- editor.setAutocompleteProvider(autocompleteProvider);
112
+ this.editor.setAutocompleteProvider(this.autocompleteProvider);
115
113
  this.editor.onSubmit=this.handleOnSubmit.bind(this)
116
114
  this.tui.addChild(this.editor);
117
115
  this.tui.setFocus(this.editor);
@@ -194,13 +192,13 @@ class ChatUI {
194
192
  ERROR: 'red',
195
193
  }
196
194
  // 监听错误日志,显示到 tooler
197
- this._logHandler = (data) => {
198
- if(!Object.keys(level_dict).includes(data.level))return;
199
- const level=folikoTerracotta(`[${data.level}]`)
200
- const level_key=level_dict[data.level]||'dim'
201
- this.statusBar.notifier.show(`${level} ${chalk[level_key](data.message)}`)
202
- };
203
- logger.on('log', this._logHandler);
195
+ // this._logHandler = (data) => {
196
+ // if(!Object.keys(level_dict).includes(data.level))return;
197
+ // const level=folikoTerracotta(`[${data.level}]`)
198
+ // const level_key=level_dict[data.level]||'dim'
199
+ // this.statusBar.notifier.show(`${level} ${chalk[level_key](data.message)}`)
200
+ // };
201
+ // logger.on('log', this._logHandler);
204
202
 
205
203
  // 监听通知事件,显示到通知区域
206
204
  this._notificationHandler = (data) => {
@@ -230,6 +228,13 @@ class ChatUI {
230
228
  }
231
229
  };
232
230
  agent.framework.on('agent:usage', this._usageHandler);
231
+
232
+ // 监听 skill 热重载事件,刷新 autocomplete
233
+ this._skillReloadedHandler = () => {
234
+ this._refreshSkillCommands(agent.framework);
235
+ this._refreshAutocomplete();
236
+ };
237
+ agent.framework.on('skill:reloaded', this._skillReloadedHandler);
233
238
  }
234
239
  }
235
240
 
@@ -242,10 +247,10 @@ class ChatUI {
242
247
  clearTimeout(this._streamBufferTimer);
243
248
  this._streamBufferTimer = null;
244
249
  }
245
- if (this._logHandler && this.agent.framework) {
246
- logger.off('log', this._logHandler);
247
- this._logHandler = null;
248
- }
250
+ // if (this._logHandler && this.agent.framework) {
251
+ // logger.off('log', this._logHandler);
252
+ // this._logHandler = null;
253
+ // }
249
254
  if (this._notificationHandler && this.agent.framework) {
250
255
  this.agent.framework.off('notification', this._notificationHandler);
251
256
  this._notificationHandler = null;
@@ -254,6 +259,10 @@ class ChatUI {
254
259
  this.agent.framework.off('agent:usage', this._usageHandler);
255
260
  this._usageHandler = null;
256
261
  }
262
+ if (this._skillReloadedHandler && this.agent.framework) {
263
+ this.agent.framework.off('skill:reloaded', this._skillReloadedHandler);
264
+ this._skillReloadedHandler = null;
265
+ }
257
266
  }
258
267
 
259
268
  clear_message_done(){
@@ -275,6 +284,7 @@ class ChatUI {
275
284
  queue.add(function(){
276
285
  var next = this.next;
277
286
  self.agent.sendMessage(message, { sessionId:self.sessionId }).then(next).catch(function(e){
287
+ self.create_message(chalk.red(`[错误] ${e?.message || e || '发送消息失败'}\n`), colored('● ', RED))
278
288
  self.clear_message_done();
279
289
  });
280
290
  })
@@ -305,6 +315,20 @@ class ChatUI {
305
315
  return true;
306
316
  }
307
317
 
318
+ if (cmd === '/reload') {
319
+ try {
320
+ const result = await this.agent.framework.executeTool('reloadSkills', {});
321
+ if (result.success !== false) {
322
+ this.create_message(`${colored('[提示]', CYAN)} 技能已重载\n`,chalk.dim('● '))
323
+ } else {
324
+ this.create_message(`${colored('[错误]', RED)} 重载失败: ${result.error}\n`,chalk.dim('● '))
325
+ }
326
+ } catch (err) {
327
+ this.create_message(`${colored('[错误]', RED)} 重载失败: ${err.message}\n`,chalk.dim('● '))
328
+ }
329
+ return true;
330
+ }
331
+
308
332
  // /help 命令显示所有注册的命令
309
333
  if (cmd === '/help') {
310
334
  const { getCommandRegistry } = require('../../../src/core/command-registry');
@@ -312,17 +336,18 @@ class ChatUI {
312
336
  const commands = registry.getAllCommands();
313
337
 
314
338
  let helpText = `${colored('[帮助]', CYAN)} 可用命令:\n`;
315
- helpText += ` ${colored('/clear', CYAN)} - 清除对话上下文\n`;
316
- helpText += ` ${colored('/compress', CYAN)} - 压缩上下文\n`;
317
- helpText += ` ${colored('/exit', CYAN)} - 退出\n`;
339
+ this.baseCommands.forEach(c => {
340
+ helpText += ` ${colored('/' + c.name, CYAN)} - ${c.description}\n`;
341
+ })
342
+
318
343
 
319
344
  // 添加 skill 命令
320
345
  const extExecutor = this.agent.framework?.pluginManager?.get('extension-executor');
321
- if (extExecutor && typeof extExecutor.getSkillCommandsHelp === 'function') {
322
- const skillHelp = extExecutor.getSkillCommandsHelp();
323
- if (skillHelp) {
346
+ if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
347
+ const skillCommands = extExecutor.getSkillCommands();
348
+ if (skillCommands.length > 0) {
324
349
  helpText += `\n${colored('[技能命令]', CYAN)}\n`;
325
- helpText += skillHelp.split('\n').map(line => ` ${line}`).join('\n') + '\n';
350
+ helpText += skillCommands.map(cmd => ` ${colored('/' + cmd.name, CYAN)} - ${cmd.description || '无描述'}`).join('\n') + '\n';
326
351
  }
327
352
  }
328
353
 
@@ -350,7 +375,10 @@ class ChatUI {
350
375
  args: { args: cmdArgs }
351
376
  });
352
377
  if (result.success !== false) {
353
- this.create_message(`${colored('[结果]', CYAN)} ${result.data || result}\n`, chalk.dim('● '));
378
+ this.create_message(`${colored('[指令] '+cmdName, CYAN)}\n\n ${result.data || result}\n`, chalk.green('● '));
379
+ return true;
380
+ }else{
381
+ this.create_message(`${colored('[指令] '+cmdName, RED)}\n\n ${result.error || '未知错误'}\n`, chalk.red('● '));
354
382
  return true;
355
383
  }
356
384
  } catch (err) {
@@ -371,10 +399,10 @@ class ChatUI {
371
399
  framework: this.agent.framework,
372
400
  };
373
401
  registry.execute(cmdName, cmdArgs, context).catch(err => {
374
- this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.dim('● '));
402
+ this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.red('● '));
375
403
  });
376
404
  } catch (err) {
377
- this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.dim('● '));
405
+ this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.red('● '));
378
406
  }
379
407
  return true;
380
408
  }
@@ -557,6 +585,32 @@ class ChatUI {
557
585
  this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
558
586
  }
559
587
  }
588
+ /**
589
+ * 刷新 skill 命令列表(用于热重载)
590
+ */
591
+ _refreshSkillCommands(framework) {
592
+ this.skillCommands = [];
593
+ if (!framework) return;
594
+ const extExecutor = framework.pluginManager?.get('extension-executor');
595
+ if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
596
+ this.skillCommands.push(...extExecutor.getSkillCommands());
597
+ }
598
+ }
599
+
600
+ /**
601
+ * 刷新自动补全提供者(skill 命令热重载时调用)
602
+ */
603
+ _refreshAutocomplete() {
604
+ if (!this.editor) return;
605
+ ;
606
+ // 重新构建 CombinedAutocompleteProvider 并更新引用
607
+ const { CombinedAutocompleteProvider } = require('@earendil-works/pi-tui');
608
+ this.autocompleteProvider = new CombinedAutocompleteProvider(
609
+ [...this.baseCommands, ...this.skillCommands],
610
+ process.cwd()
611
+ );
612
+ this.editor.setAutocompleteProvider(this.autocompleteProvider);
613
+ }
560
614
  /**
561
615
  * 刷新流式输出缓冲区:合并高频 chunk 后统一渲染,减少 Markdown 重复解析
562
616
  */
@@ -607,7 +661,7 @@ class ChatUI {
607
661
  this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
608
662
  }
609
663
  } else if (chunk.type === 'tool-call') {
610
- const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 50) : '';
664
+ const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 30) : '';
611
665
  this.statusBar.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
612
666
  } else if(chunk.type==='tool-result'){
613
667
  const result = chunk.result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.1.70",
3
+ "version": "1.1.72",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -42,6 +42,11 @@ class ExtensionExecutorPlugin extends Plugin {
42
42
  log.debug('Framework ready, rescanning all plugin tools...');
43
43
  const plugins = framework.pluginManager.getAll();
44
44
  for (const { instance: plugin } of plugins) {
45
+ // skill-manager 插件的工具已通过 Skill.registerCommands 直接注册,
46
+ // 不需要再通过 _scanPluginTools 扫描,避免重复注册
47
+ if (plugin.name === 'skill-manager') {
48
+ continue;
49
+ }
45
50
  this._scanPluginTools(plugin);
46
51
  }
47
52
  // 刷新所有 Agent 的扩展提示词
@@ -53,8 +58,10 @@ class ExtensionExecutorPlugin extends Plugin {
53
58
 
54
59
  /**
55
60
  * 扫描插件的 this.tools 并注册到扩展系统
61
+ * @param {Object} plugin - 插件实例
62
+ * @param {string} [extName] - 可选的扩展名(用于统一命名)
56
63
  */
57
- _scanPluginTools(plugin) {
64
+ _scanPluginTools(plugin, extName) {
58
65
  if (!plugin || !plugin.tools || typeof plugin.tools !== 'object') {
59
66
  return;
60
67
  }
@@ -62,9 +69,12 @@ class ExtensionExecutorPlugin extends Plugin {
62
69
  const pluginName = plugin.name;
63
70
  if (!pluginName) return;
64
71
 
72
+ // 确定扩展名:优先使用 extName,否则使用 pluginName
73
+ const useExtName = extName || pluginName;
74
+
65
75
  // 获取插件信息
66
76
  const pluginInfo = {
67
- name: plugin.name,
77
+ name: useExtName,
68
78
  description: plugin.description || '',
69
79
  version: plugin.version || '1.0.0',
70
80
  };
@@ -73,7 +83,7 @@ class ExtensionExecutorPlugin extends Plugin {
73
83
  for (const [toolName, toolDef] of Object.entries(plugin.tools)) {
74
84
  if (!toolDef || typeof toolDef !== 'object') continue;
75
85
 
76
- this.registerTool(pluginName, pluginInfo, {
86
+ this.registerTool(useExtName, pluginInfo, {
77
87
  name: toolName,
78
88
  description: toolDef.description || '',
79
89
  inputSchema: toolDef.inputSchema,
@@ -81,7 +91,7 @@ class ExtensionExecutorPlugin extends Plugin {
81
91
  });
82
92
  }
83
93
 
84
- log.debug(` Scanned ${Object.keys(plugin.tools).length} tools from plugin '${pluginName}'`);
94
+ log.debug(` Scanned ${Object.keys(plugin.tools).length} tools from plugin '${pluginName}' (as '${useExtName}')`);
85
95
 
86
96
  // 刷新所有 Agent 的扩展提示词
87
97
  if (this._framework && this._framework._ready) {
@@ -100,21 +110,27 @@ class ExtensionExecutorPlugin extends Plugin {
100
110
  const pluginName = plugin.name;
101
111
  if (!pluginName) return;
102
112
 
103
- // 先移除旧工具
104
- if (this._extensions.has(pluginName)) {
105
- const ext = this._extensions.get(pluginName);
106
- for (const tool of ext.tools || []) {
107
- // toolRouter 移除
108
- if (this._framework?.toolRouter) {
109
- this._framework.toolRouter.unregister(`ext_${tool.name}`);
113
+ // skill-manager 插件使用 'skill' 作为扩展名(保持与 Skill.registerCommands 一致)
114
+ const extName = pluginName === 'skill-manager' ? 'skill' : pluginName;
115
+
116
+ // 先移除旧工具(同时检查 'skill' 'skill-manager' 两个扩展名)
117
+ const keysToRemove = [extName, pluginName];
118
+ for (const key of keysToRemove) {
119
+ if (this._extensions.has(key)) {
120
+ const ext = this._extensions.get(key);
121
+ for (const tool of ext.tools || []) {
122
+ // 从 toolRouter 移除
123
+ if (this._framework?.toolRouter) {
124
+ this._framework.toolRouter.unregister(`ext_${tool.name}`);
125
+ }
110
126
  }
127
+ this._extensions.delete(key);
111
128
  }
112
- this._extensions.delete(pluginName);
113
129
  }
114
130
 
115
- // 重新扫描工具
116
- this._scanPluginTools(plugin);
117
- log.info(` Rescanned tools from plugin '${pluginName}'`);
131
+ // 重新扫描工具,使用正确的扩展名
132
+ this._scanPluginTools(plugin, extName);
133
+ log.info(` Rescanned tools from plugin '${pluginName}' (as '${extName}')`);
118
134
  }
119
135
 
120
136
  async start(framework) {
@@ -143,7 +159,7 @@ class ExtensionExecutorPlugin extends Plugin {
143
159
  if (plugin === 'mcp' && this._mcpExecutor) {
144
160
  const mcpToolDef = this._mcpExecutor.tools?.[tool];
145
161
  if (mcpToolDef && mcpToolDef.execute) {
146
- log.info(` ext_call [MCP]: tool=${tool}`);
162
+ //log.info(` ext_call [MCP]: tool=${tool}`);
147
163
  return await mcpToolDef.execute(toolArgs || {});
148
164
  }
149
165
  return { success: false, error: `MCP tool '${tool}' not found` };
@@ -237,7 +253,7 @@ class ExtensionExecutorPlugin extends Plugin {
237
253
  name,
238
254
  description: ext.description,
239
255
  version: ext.version,
240
- tools: ext.tools.map((t) => ({ name: t.name, description: t.description })),
256
+ tools: ext.tools.filter(t => t && t.name).map((t) => ({ name: t.name, description: t.description })),
241
257
  });
242
258
  }
243
259
  return { success: true, data: extensions };
@@ -275,6 +291,11 @@ class ExtensionExecutorPlugin extends Plugin {
275
291
  }
276
292
 
277
293
  registerTool(pluginName, pluginInfo, toolDef) {
294
+ if (!toolDef || !toolDef.name) {
295
+ log.warn(`[ExtensionExecutor] Invalid tool definition for '${pluginName}'`);
296
+ return;
297
+ }
298
+
278
299
  if (!this._extensions.has(pluginName)) {
279
300
  this._extensions.set(pluginName, {
280
301
  name: pluginInfo.name || pluginName,
@@ -401,6 +422,8 @@ class ExtensionExecutorPlugin extends Plugin {
401
422
  // Skill 命令按技能名分组显示
402
423
  const toolsBySkill = {};
403
424
  for (const tool of ext.tools) {
425
+ // 跳过无效工具
426
+ if (!tool || !tool.name) continue;
404
427
  // 命令格式: skillname:cmdname
405
428
  const parts = tool.name.split(':');
406
429
  const skillName = parts[0];
@@ -425,7 +448,8 @@ class ExtensionExecutorPlugin extends Plugin {
425
448
  } else {
426
449
  desc += `### ${ext.name || name}\n`;
427
450
  desc += `${ext.description || '无描述'}\n`;
428
- desc += `**工具:** ${ext.tools.map(t => `\`${t.name}\``).join(', ')}\n\n`;
451
+ const validTools = ext.tools.filter(t => t && t.name);
452
+ desc += `**工具:** ${validTools.map(t => `\`${t.name}\``).join(', ')}\n\n`;
429
453
  }
430
454
  }
431
455
 
@@ -568,10 +592,12 @@ class ExtensionExecutorPlugin extends Plugin {
568
592
  getSkillCommands() {
569
593
  const ext = this._extensions.get('skill');
570
594
  if (!ext) return [];
571
- return ext.tools.map(t => ({
572
- name: t.name.replace(/^([^:]+):(.*)$/, '$1:$2'),
573
- description: t.description || t.name,
574
- }));
595
+ return ext.tools
596
+ .filter(t => t && t.name)
597
+ .map(t => ({
598
+ name: t.name.replace(/^([^:]+):(.*)$/, '$1:$2'),
599
+ description: t.description || t.name,
600
+ }));
575
601
  }
576
602
 
577
603
  /**
@@ -583,6 +609,7 @@ class ExtensionExecutorPlugin extends Plugin {
583
609
 
584
610
  const lines = [];
585
611
  for (const tool of ext.tools) {
612
+ if (!tool || !tool.name) continue;
586
613
  const parts = tool.name.split(':');
587
614
  if (parts.length === 2) {
588
615
  lines.push(`/${parts[0]}:${parts[1]} - ${tool.description || '无描述'}`);
@@ -210,6 +210,14 @@ class FeishuPlugin extends Plugin {
210
210
  case 'compress':
211
211
  await this._compressSessionContext(openId)
212
212
  break
213
+ case 'reload':
214
+ try {
215
+ await this._framework.executeTool('reloadSkills', {})
216
+ await this._sendMessage(openId, '✅ 技能已重载', originalMsg)
217
+ } catch (err) {
218
+ await this._sendMessage(openId, '❌ 重载失败: ' + err.message, originalMsg)
219
+ }
220
+ break
213
221
  case 'history':
214
222
  if (this._sessionPlugin) {
215
223
  const session = this._sessionPlugin.getSession(`feishu_${openId}`)
@@ -512,7 +520,7 @@ class FeishuPlugin extends Plugin {
512
520
  const extExecutor = this._framework?.pluginManager?.get('extension-executor')
513
521
  const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
514
522
 
515
- let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
523
+ let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
516
524
 
517
525
  if (skillHelp) {
518
526
  helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
@@ -701,6 +701,14 @@ class QQPlugin extends Plugin {
701
701
  case 'compress':
702
702
  await this._compressSessionContext(openid)
703
703
  break
704
+ case 'reload':
705
+ try {
706
+ await this._framework.executeTool('reloadSkills', {})
707
+ await this._sendMessage(openid, '✅ 技能已重载')
708
+ } catch (err) {
709
+ await this._sendMessage(openid, '❌ 重载失败: ' + err.message)
710
+ }
711
+ break
704
712
  case 'history':
705
713
  const sessionInfo = this._getSessionAgent(openid)
706
714
  if (sessionInfo && this._framework) {
@@ -752,7 +760,7 @@ class QQPlugin extends Plugin {
752
760
  const extExecutor = this._framework?.pluginManager?.get('extension-executor')
753
761
  const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
754
762
 
755
- let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
763
+ let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
756
764
 
757
765
  if (skillHelp) {
758
766
  helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
@@ -322,6 +322,14 @@ class TelegramPlugin extends Plugin {
322
322
  case 'compress':
323
323
  await this._compressSessionContext(chatId, msg.message_id)
324
324
  break
325
+ case 'reload':
326
+ try {
327
+ await this._framework.executeTool('reloadSkills', {})
328
+ await this._sendMessage(chatId, '✅ 技能已重载', msg.message_id)
329
+ } catch (err) {
330
+ await this._sendMessage(chatId, '❌ 重载失败: ' + err.message, msg.message_id)
331
+ }
332
+ break
325
333
  case 'history':
326
334
  const sessionInfo = this._getSessionAgent(chatId)
327
335
  if (sessionInfo && this._framework) {
@@ -525,7 +533,7 @@ class TelegramPlugin extends Plugin {
525
533
  const extExecutor = this._framework?.pluginManager?.get('extension-executor')
526
534
  const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
527
535
 
528
- let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
536
+ let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
529
537
 
530
538
  if (skillHelp) {
531
539
  helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
@@ -601,6 +601,14 @@ class WeixinPlugin extends Plugin {
601
601
  case 'compress':
602
602
  await this._compressSessionContext(userId)
603
603
  break
604
+ case 'reload':
605
+ try {
606
+ await this._framework.executeTool('reloadSkills', {})
607
+ await this._sendMessageBatch(originalMsg, userId, '✅ 技能已重载', true)
608
+ } catch (err) {
609
+ await this._sendMessageBatch(originalMsg, userId, '❌ 重载失败: ' + err.message, true)
610
+ }
611
+ break
604
612
  case 'history':
605
613
  const sessionInfo = this._getSessionAgent(userId)
606
614
  if (sessionInfo && this._framework) {
@@ -634,7 +642,7 @@ class WeixinPlugin extends Plugin {
634
642
  const extExecutor = this._framework?.pluginManager?.get('extension-executor')
635
643
  const skillHelp = extExecutor?.getSkillCommandsHelp?.() || ''
636
644
 
637
- let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/history - 查看历史消息数'
645
+ let helpText = '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/compress - 压缩上下文\n/reload - 重载技能\n/history - 查看历史消息数'
638
646
 
639
647
  if (skillHelp) {
640
648
  helpText += '\n\n【技能命令】\n' + skillHelp.split('\n').map(line => line.replace(/^\//, '/')).join('\n')
@@ -10,6 +10,39 @@ const { logger } = require('../utils/logger');
10
10
  const log = logger.child('SkillManager');
11
11
  const { z } = require('zod');
12
12
  const { Command } = require('commander');
13
+
14
+ /**
15
+ * 简单的 shell 参数解析器(处理带引号的参数)
16
+ * @param {string} str - 参数字符串
17
+ * @returns {string[]} 参数数组
18
+ */
19
+ function parseShellArgs(str) {
20
+ if (!str || !str.trim()) return [];
21
+ const args = [];
22
+ let current = '';
23
+ let inQuote = false;
24
+ let quoteChar = '';
25
+
26
+ for (let i = 0; i < str.length; i++) {
27
+ const char = str[i];
28
+ if (!inQuote && (char === '"' || char === "'")) {
29
+ inQuote = true;
30
+ quoteChar = char;
31
+ } else if (inQuote && char === quoteChar) {
32
+ inQuote = false;
33
+ } else if (!inQuote && char === ' ') {
34
+ if (current.trim()) {
35
+ args.push(current.trim());
36
+ current = '';
37
+ }
38
+ } else {
39
+ current += char;
40
+ }
41
+ }
42
+ if (current.trim()) args.push(current.trim());
43
+ return args;
44
+ }
45
+
13
46
  /**
14
47
  * 验证 skill 名称
15
48
  * 1-64字符,字母数字、下划线和连字符,不能以连字符或下划线开头或结尾
@@ -185,18 +218,25 @@ class Skill {
185
218
  let argumentParser = cmd.argumentParser;
186
219
  if (!argumentParser && Array.isArray(cmd.options) && cmd.options.length > 0) {
187
220
  argumentParser = (rawArgs) => {
188
-
189
- const program = new Command();
190
- for (const opt of cmd.options) {
191
- program.option(opt.flags, opt.description, opt.defaultValue);
221
+ try {
222
+ const program = new Command();
223
+ program.exitOverride(); // 不自动退出
224
+ program.configureOutput({ writeErr: () => {} }); // 隐藏错误输出
225
+ for (const opt of cmd.options) {
226
+ program.option(opt.flags, opt.description, opt.defaultValue);
227
+ }
228
+ program.arguments('[args...]');
229
+ program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
230
+ return { ...program.opts(), args: program.args };
231
+ } catch (err) {
232
+ log.warn(`[SkillManager] Command parse error: ${err.message}`);
233
+ return { args: rawArgs };
192
234
  }
193
- program.arguments('[args...]');
194
- program.parse(['node', 'cmd', ...(rawArgs || '').split(' ').filter(Boolean)]);
195
- return { ...program.opts(), args: program.args };
196
235
  };
197
236
  }
198
237
 
199
238
  const toolDef = {
239
+ name: fullName,
200
240
  description: cmd.description || `Skill command: ${fullName}`,
201
241
  inputSchema: z.object({
202
242
  args: z.string().optional().describe('命令参数'),
@@ -832,14 +872,12 @@ class SkillManagerPlugin extends Plugin {
832
872
  this._framework = framework;
833
873
  this._loadAllSkills();
834
874
 
835
- // 通知 extension-executor 重载 skill 命令
836
- const extExecutor = framework.pluginManager?.get('extension-executor');
837
- if (extExecutor && typeof extExecutor._rescanPluginTools === 'function') {
838
- const skillPlugin = framework.pluginManager?.get('skill-manager');
839
- if (skillPlugin) {
840
- extExecutor._rescanPluginTools(skillPlugin);
841
- }
842
- }
875
+ // 通知 extension-executor 重载 skill 命令(通过监听器自动处理)
876
+ // 注意:skill 命令已经在 registerCommands() 中直接注册到 extension-executor
877
+ // 不需要再调用 _rescanPluginTools
878
+
879
+ // 通知 UI 刷新 skill 命令(用于 autocomplete 热重载)
880
+ framework.emit('skill:reloaded', { skills: Array.from(this._skills.keys()) });
843
881
  }
844
882
 
845
883
  uninstall(framework) {
@@ -500,6 +500,8 @@ class AgentChatHandler extends EventEmitter {
500
500
  const stream = result.fullStream;
501
501
  let fullText = '';
502
502
  const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;
503
+ // 追踪发送的 tool-call ID,过滤 API 返回的不匹配 tool-result
504
+ const sentToolCallIds = new Set();
503
505
 
504
506
  for await (const part of iterator || stream) {
505
507
  if (part.type === 'text-delta') {
@@ -510,8 +512,16 @@ class AgentChatHandler extends EventEmitter {
510
512
  reasoningContent += part.text || '';
511
513
  yield { type: 'thinking', text: part.text };
512
514
  } else if (part.type === 'tool-call') {
515
+ const toolCallId = part.toolCallId;
516
+ if (toolCallId) sentToolCallIds.add(toolCallId);
513
517
  yield { type: 'tool-call', toolName: part.toolName, input: part.input };
514
518
  } else if (part.type === 'tool-result') {
519
+ // 过滤 toolCallId 不匹配的 tool-result(API bug)
520
+ const resultToolCallId = part.toolCallId;
521
+ if (resultToolCallId && !sentToolCallIds.has(resultToolCallId)) {
522
+ logger.debug(`[stream]过滤不匹配的 tool-result: ${resultToolCallId}`);
523
+ continue;
524
+ }
515
525
  yield { type: 'tool-result', toolName: part.toolName, result: part.output };
516
526
  } else if (part.type === 'error') {
517
527
  // 统一错误消息