foliko 2.0.2 → 2.0.4

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.
Files changed (80) hide show
  1. package/README.md +6 -6
  2. package/docs/public-api.md +91 -0
  3. package/docs/system-prompt.md +219 -0
  4. package/docs/usage.md +6 -6
  5. package/package.json +1 -1
  6. package/plugins/ambient/ExplorerLoop.js +1 -0
  7. package/plugins/ambient/index.js +1 -0
  8. package/plugins/core/coordinator/index.js +10 -5
  9. package/plugins/core/default/bootstrap.js +21 -3
  10. package/plugins/core/default/config.js +12 -3
  11. package/plugins/core/python-loader/index.js +3 -3
  12. package/plugins/core/scheduler/index.js +26 -2
  13. package/plugins/core/skill-manager/index.js +198 -151
  14. package/plugins/core/sub-agent/index.js +34 -15
  15. package/plugins/core/think/index.js +1 -0
  16. package/plugins/core/workflow/index.js +5 -4
  17. package/plugins/executors/data-splitter/index.js +14 -1
  18. package/plugins/executors/extension/index.js +52 -38
  19. package/plugins/executors/python/index.js +3 -3
  20. package/plugins/executors/shell/index.js +6 -4
  21. package/plugins/io/web/index.js +2 -1
  22. package/plugins/memory/index.js +29 -74
  23. package/plugins/messaging/email/handlers.js +1 -19
  24. package/plugins/messaging/email/monitor.js +2 -17
  25. package/plugins/messaging/email/reply.js +2 -1
  26. package/plugins/messaging/email/utils.js +20 -1
  27. package/plugins/messaging/feishu/index.js +1 -1
  28. package/plugins/messaging/qq/index.js +1 -1
  29. package/plugins/messaging/telegram/index.js +1 -1
  30. package/plugins/messaging/weixin/index.js +1 -1
  31. package/plugins/plugin-manager/index.js +1 -33
  32. package/plugins/tools/index.js +14 -1
  33. package/skills/plugins/SKILL.md +316 -0
  34. package/skills/skill-guide/SKILL.md +5 -5
  35. package/skills/{subagent-guide → subagents}/SKILL.md +1 -1
  36. package/skills/{workflow-guide → workflows}/SKILL.md +3 -3
  37. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/DEBUGGING.md +197 -197
  38. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/SKILL.md +391 -391
  39. package/src/agent/base.js +5 -20
  40. package/src/agent/chat.js +67 -5
  41. package/src/agent/index.js +20 -8
  42. package/src/agent/main.js +100 -23
  43. package/src/agent/prompt-registry.js +296 -0
  44. package/src/agent/sub-config.js +1 -78
  45. package/src/agent/sub.js +20 -25
  46. package/src/cli/commands/plugin.js +1 -60
  47. package/src/cli/ui/chat-ui-old.js +1 -1
  48. package/src/cli/ui/chat-ui.js +21 -17
  49. package/src/cli/ui/components/agent-mention-provider.js +1 -1
  50. package/src/common/constants.js +42 -0
  51. package/src/common/id.js +13 -0
  52. package/src/common/queue.js +3 -3
  53. package/src/context/compressor.js +17 -9
  54. package/src/framework/framework.js +218 -0
  55. package/src/framework/index.js +1 -2
  56. package/src/framework/lifecycle.js +102 -1
  57. package/src/framework/loader.js +1 -55
  58. package/src/index.js +11 -2
  59. package/src/plugin/base.js +69 -55
  60. package/src/plugin/loader.js +1 -57
  61. package/src/session/chat.js +1 -1
  62. package/src/session/entry.js +1 -11
  63. package/src/storage/manager.js +1 -12
  64. package/src/tool/executor.js +3 -2
  65. package/src/tool/router.js +5 -5
  66. package/src/utils/data-splitter.js +2 -1
  67. package/src/utils/index.js +150 -0
  68. package/src/utils/message-validator.js +21 -17
  69. package/src/utils/plugin-helpers.js +19 -5
  70. package/subagent.md +2 -2
  71. package/tests/core/plugin-prompts.test.js +219 -0
  72. package/tests/core/prompt-registry.test.js +209 -0
  73. package/src/cli/utils/plugin-config.js +0 -50
  74. package/src/config/plugin-config.js +0 -50
  75. /package/skills/{ambient-agent → ambient}/SKILL.md +0 -0
  76. /package/skills/{foliko-dev → foliko}/AGENTS.md +0 -0
  77. /package/skills/{foliko-dev → foliko}/SKILL.md +0 -0
  78. /package/skills/{mcp-usage → mcp}/SKILL.md +0 -0
  79. /package/skills/{plugin-guide → plugins-guide}/SKILL.md +0 -0
  80. /package/skills/{python-plugin-dev → python}/SKILL.md +0 -0
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PromptRegistry — 全局系统提示词注册表
5
+ *
6
+ * 设计目标:
7
+ * - 单一注册入口(替代散落在各 Plugin 的 registerPromptPart 调用)
8
+ * - owner 命名空间隔离(避免 part 名冲突)
9
+ * - 用语义槽位(PROMPT_SLOT)排序,替代魔法数字
10
+ * - 可观测:list / preview / getPart / getSlots
11
+ *
12
+ * 与旧 API 的关系:
13
+ * - 完全独立的新 API,不破坏 MainAgent._systemPromptBuilder
14
+ * - 旧 registerPromptPart 调用继续工作(写入各 agent 自己的 builder)
15
+ * - 新代码推荐使用 framework.prompts.register(owner, name, provider, { slot })
16
+ *
17
+ * 渐进迁移路径:
18
+ * - P0:注册表可独立使用(plugins 主动注册,外部可预览)
19
+ * - P2:Plugin 基类支持声明式 prompts 字段
20
+ * - P5:MainAgent 完全切换到本注册表
21
+ */
22
+
23
+ const { EventEmitter } = require('../common/events');
24
+ const { PROMPT_SLOT } = require('../common/constants');
25
+
26
+ /**
27
+ * 单个 prompt part
28
+ * - provider 是 () => string|null,返回 null/空串时被 build 跳过
29
+ * - 内部带缓存,失效后才重新计算
30
+ */
31
+ class PromptPart {
32
+ constructor(owner, name, provider, options = {}) {
33
+ this.owner = owner;
34
+ this.name = name;
35
+ this.provider = provider;
36
+ this.slot = options.slot || 'CUSTOM';
37
+ this.order = options.order ?? null; // 显式 order 覆盖 slot 默认
38
+ this.description = options.description || '';
39
+ this.metadata = options.metadata || {};
40
+ this._cached = null;
41
+ this._dirty = true;
42
+ this._error = null;
43
+ }
44
+
45
+ get() {
46
+ if (this._dirty || this._cached === null) {
47
+ try {
48
+ const result = this.provider();
49
+ this._cached = result == null ? null : String(result);
50
+ this._dirty = false;
51
+ this._error = null;
52
+ } catch (e) {
53
+ this._error = e;
54
+ this._cached = null;
55
+ this._dirty = false;
56
+ // 警告但不抛错 — 避免单个 part 失败导致整个 prompt 渲染失败
57
+ // eslint-disable-next-line no-console
58
+ console.warn(`[PromptRegistry:${this.owner}::${this.name}] provider failed:`, e.message);
59
+ }
60
+ }
61
+ return this._cached;
62
+ }
63
+
64
+ invalidate() {
65
+ this._dirty = true;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * PromptRegistry 主类
71
+ */
72
+ class PromptRegistry extends EventEmitter {
73
+ constructor(options = {}) {
74
+ super();
75
+
76
+ // key = "owner::name"
77
+ this._parts = new Map();
78
+
79
+ // slot 名称 → { order, label }
80
+ this._slots = new Map();
81
+
82
+ // 注入 slots:优先用参数,否则用全局 PROMPT_SLOT
83
+ const slots = options.slots || PROMPT_SLOT;
84
+ for (const [name, def] of Object.entries(slots)) {
85
+ if (!def || typeof def.order !== 'number') continue;
86
+ this._slots.set(name, {
87
+ order: def.order,
88
+ label: def.label || name,
89
+ });
90
+ }
91
+
92
+ // 用于 render 时检测循环依赖
93
+ this._building = false;
94
+ }
95
+
96
+ // ==================== 槽位管理 ====================
97
+
98
+ /**
99
+ * 定义/覆盖一个槽位
100
+ */
101
+ defineSlot(name, order, label) {
102
+ if (typeof order !== 'number') throw new Error('slot order must be a number');
103
+ this._slots.set(name, { order, label: label || name });
104
+ this.emit('slot-defined', { name, order, label });
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * 获取所有槽位定义
110
+ */
111
+ getSlots() {
112
+ return new Map(this._slots);
113
+ }
114
+
115
+ // ==================== 注册/注销 ====================
116
+
117
+ /**
118
+ * 注册一个 prompt part
119
+ * @param {string} owner 拥有者标识(通常是 plugin name 或 '_main')
120
+ * @param {string} name part 名(在 owner 内唯一)
121
+ * @param {Function} provider () => string|null
122
+ * @param {Object} [options]
123
+ * @param {string} [options.slot='CUSTOM'] 槽位名
124
+ * @param {number} [options.order] 自定义 order(覆盖 slot 默认)
125
+ * @param {string} [options.description] 描述(用于 list/debug)
126
+ * @param {Object} [options.metadata] 额外元数据
127
+ * @returns {PromptPart}
128
+ */
129
+ register(owner, name, provider, options = {}) {
130
+ if (!owner || typeof owner !== 'string') {
131
+ throw new Error('PromptRegistry.register: owner is required');
132
+ }
133
+ if (!name || typeof name !== 'string') {
134
+ throw new Error('PromptRegistry.register: name is required');
135
+ }
136
+ if (typeof provider !== 'function') {
137
+ throw new Error('PromptRegistry.register: provider must be a function');
138
+ }
139
+
140
+ const key = `${owner}::${name}`;
141
+ const part = new PromptPart(owner, name, provider, options);
142
+ this._parts.set(key, part);
143
+ this.emit('register', { owner, name, slot: part.slot });
144
+ return part;
145
+ }
146
+
147
+ /**
148
+ * 注销一个 part
149
+ * @returns {boolean} 是否真的删除了
150
+ */
151
+ unregister(owner, name) {
152
+ const key = `${owner}::${name}`;
153
+ const existed = this._parts.delete(key);
154
+ if (existed) this.emit('unregister', { owner, name });
155
+ return existed;
156
+ }
157
+
158
+ /**
159
+ * 清空指定 owner 的所有 parts
160
+ * @returns {number} 删除的 part 数
161
+ */
162
+ clearOwner(owner) {
163
+ const prefix = `${owner}::`;
164
+ let count = 0;
165
+ for (const key of Array.from(this._parts.keys())) {
166
+ if (key.startsWith(prefix)) {
167
+ this._parts.delete(key);
168
+ count++;
169
+ }
170
+ }
171
+ if (count > 0) this.emit('clear-owner', { owner, count });
172
+ return count;
173
+ }
174
+
175
+ /**
176
+ * 失效指定 part(下次 build 时重新计算)
177
+ */
178
+ invalidate(owner, name) {
179
+ const part = this._parts.get(`${owner}::${name}`);
180
+ if (part) {
181
+ part.invalidate();
182
+ this.emit('invalidate', { owner, name });
183
+ }
184
+ return this;
185
+ }
186
+
187
+ /**
188
+ * 失效所有 parts
189
+ */
190
+ invalidateAll() {
191
+ for (const part of this._parts.values()) part.invalidate();
192
+ this.emit('invalidate-all');
193
+ return this;
194
+ }
195
+
196
+ // ==================== 查询 ====================
197
+
198
+ /**
199
+ * 获取单个 part 实例
200
+ */
201
+ getPart(owner, name) {
202
+ return this._parts.get(`${owner}::${name}`) || null;
203
+ }
204
+
205
+ /**
206
+ * 是否有指定 part
207
+ */
208
+ has(owner, name) {
209
+ return this._parts.has(`${owner}::${name}`);
210
+ }
211
+
212
+ /**
213
+ * 列出所有 parts(按 order 排序)
214
+ */
215
+ list() {
216
+ return Array.from(this._parts.values()).sort((a, b) => this._compareOrder(a, b));
217
+ }
218
+
219
+ /**
220
+ * 按 owner 过滤
221
+ */
222
+ listByOwner(owner) {
223
+ return this.list().filter((p) => p.owner === owner);
224
+ }
225
+
226
+ /**
227
+ * 按 slot 过滤
228
+ */
229
+ listBySlot(slot) {
230
+ return this.list().filter((p) => p.slot === slot);
231
+ }
232
+
233
+ _compareOrder(a, b) {
234
+ const oa = a.order != null ? a.order : this._slots.get(a.slot)?.order ?? 999;
235
+ const ob = b.order != null ? b.order : this._slots.get(b.slot)?.order ?? 999;
236
+ if (oa !== ob) return oa - ob;
237
+ // 同 order 时按 owner → name 字母序,保证稳定
238
+ if (a.owner !== b.owner) return a.owner.localeCompare(b.owner);
239
+ return a.name.localeCompare(b.name);
240
+ }
241
+
242
+ count() {
243
+ return this._parts.size;
244
+ }
245
+
246
+ // ==================== 渲染 ====================
247
+
248
+ /**
249
+ * 渲染完整 prompt 文本(按 slot order 拼接)
250
+ */
251
+ build() {
252
+ if (this._building) {
253
+ throw new Error('PromptRegistry.build() re-entered — check provider for circular calls');
254
+ }
255
+ this._building = true;
256
+ try {
257
+ const sections = [];
258
+ for (const part of this.list()) {
259
+ const content = part.get();
260
+ if (content && content.trim()) sections.push(content.trim());
261
+ }
262
+ return sections.join('\n\n');
263
+ } finally {
264
+ this._building = false;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * build() 的语义化别名,调试用
270
+ */
271
+ preview() {
272
+ return this.build();
273
+ }
274
+
275
+ /**
276
+ * 渲染结构化清单(调试 / 文档生成)
277
+ * 返回 [{ owner, name, slot, order, length, contentPreview }]
278
+ */
279
+ inspect() {
280
+ return this.list().map((part) => {
281
+ const content = part.get();
282
+ return {
283
+ owner: part.owner,
284
+ name: part.name,
285
+ slot: part.slot,
286
+ order: part.order != null ? part.order : this._slots.get(part.slot)?.order ?? 999,
287
+ description: part.description,
288
+ hasContent: Boolean(content && content.trim()),
289
+ length: content ? content.length : 0,
290
+ contentPreview: content ? content.slice(0, 80).replace(/\n/g, ' ') : null,
291
+ };
292
+ });
293
+ }
294
+ }
295
+
296
+ module.exports = { PromptRegistry, PromptPart };
@@ -6,84 +6,7 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
-
10
- /**
11
- * 解析 YAML frontmatter
12
- */
13
- function parseFrontmatter(content) {
14
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
15
- if (!match) return null;
16
-
17
- const frontmatter = {};
18
- const lines = match[1].split('\n');
19
- let currentKey = null;
20
-
21
- for (const line of lines) {
22
- // 处理 metadata 嵌套
23
- if (currentKey === 'metadata') {
24
- if (line.match(/^ {2}[a-zA-Z]/)) {
25
- const colonIndex = line.indexOf(':');
26
- if (colonIndex > 0) {
27
- const key = line.substring(2, colonIndex).trim();
28
- const value = line
29
- .substring(colonIndex + 1)
30
- .trim()
31
- .replace(/^["']|["']$/g, '');
32
- frontmatter[currentKey][key] = value;
33
- continue;
34
- }
35
- } else if (line.match(/^[a-zA-Z]/)) {
36
- currentKey = null;
37
- continue;
38
- }
39
- }
40
-
41
- const colonIndex = line.indexOf(':');
42
- if (colonIndex === -1) continue;
43
-
44
- const key = line.substring(0, colonIndex).trim();
45
- let value = line.substring(colonIndex + 1).trim();
46
-
47
- // 移除引号
48
- if (
49
- (value.startsWith('"') && value.endsWith('"')) ||
50
- (value.startsWith("'") && value.endsWith("'"))
51
- ) {
52
- value = value.slice(1, -1);
53
- }
54
-
55
- if (key === 'metadata') {
56
- currentKey = 'metadata';
57
- frontmatter.metadata = {};
58
- } else if (key === 'allowed-tools') {
59
- frontmatter[key] = value
60
- .split(',')
61
- .map((v) => v.trim())
62
- .filter(Boolean);
63
- } else if (key === 'skills') {
64
- frontmatter[key] = value
65
- .split(',')
66
- .map((v) => v.trim())
67
- .filter(Boolean);
68
- } else if (key === 'tools') {
69
- frontmatter[key] = value
70
- .split(',')
71
- .map((v) => v.trim())
72
- .filter(Boolean);
73
- } else {
74
- frontmatter[key] = value;
75
- }
76
- }
77
-
78
- return frontmatter;
79
- }
80
-
81
- /**
82
- * 移除 frontmatter,只保留正文内容
83
- */
84
- function stripFrontmatter(content) {
85
- return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
86
- }
9
+ const { parseFrontmatter, stripFrontmatter } = require('../utils');
87
10
 
88
11
  class SubAgentConfig {
89
12
  constructor(name, filePath) {
package/src/agent/sub.js CHANGED
@@ -27,10 +27,10 @@ class SubAgent extends BaseAgent {
27
27
  this.baseURL = config.baseURL;
28
28
  this.providerOptions = config.providerOptions || {};
29
29
 
30
- this.defaulTools = ['ext_skill', 'loadSkill', 'ext_call', 'subagent_call', 'mcp_call', 'mcp_tool_schema'];
30
+ this.defaulTools = ['ext_skill', 'skill_load', 'ext_call', 'subagent_call', 'mcp_call', 'mcp_tool_schema'];
31
31
  this.bindTools = {
32
32
  read: 'read_file', write: 'write_file', edit: 'modify_file',
33
- glob: 'read_directory', grep: 'search_file', bash: 'bash',
33
+ glob: 'read_directory', grep: 'search_file', bash: 'shell_exec',
34
34
  };
35
35
 
36
36
  this._customSystemPrompt = config.systemPrompt || null;
@@ -39,24 +39,13 @@ class SubAgent extends BaseAgent {
39
39
  this.maxRetries = config.maxRetries ?? 2;
40
40
  this.retryDelay = config.retryDelay ?? 5000;
41
41
  this.disableTools = config.disableTools ?? false;
42
- this._promptParts = [];
42
+ this.hidden = config.hidden || false; // 隐藏子Agent,不在指令系统中显示
43
43
  this._isSubagent = true;
44
44
 
45
45
  this.providerOptions.deepseek = this.providerOptions.deepseek || {};
46
46
  this.providerOptions.deepseek.thinking = { type: 'disabled' };
47
47
 
48
- if (this.framework) {
49
- this.framework.once('framework:ready', () => {
50
- const extExecutor = this.framework?.pluginManager?.get('extension-executor');
51
- extExecutor?._refreshAllAgentsExtPrompt?.(this.framework);
52
- });
53
- }
54
- }
55
-
56
- registerPromptPart(name, priority = 100, provider) {
57
- this._promptParts.push({ name, priority, provider });
58
- this._promptParts.sort((a, b) => a.priority - b.priority);
59
- return this;
48
+ // 扩展工具 prompt 由 _syncPromptParts 同步到 promptRegistry,无需显式刷新
60
49
  }
61
50
 
62
51
  _validateMessagesPairing(messages) { return validateAll(messages); }
@@ -74,7 +63,15 @@ class SubAgent extends BaseAgent {
74
63
 
75
64
  _getAIProvider() {
76
65
  const { createAI } = require('../llm/provider');
77
- return createAI({ provider: this.provider, model: this.model, apiKey: this.apiKey, baseURL: this.baseURL });
66
+ // 优先从 framework ai 插件拿最新配置(避免构造时拿到 stale apiKey
67
+ const aiPlugin = this.framework?.pluginManager?.get('ai');
68
+ const aiConfig = aiPlugin ? aiPlugin.getConfig() : {};
69
+ return createAI({
70
+ provider: aiConfig.provider || this.provider,
71
+ model: aiConfig.model || this.model,
72
+ apiKey: aiConfig.apiKey || this.apiKey,
73
+ baseURL: aiConfig.baseURL || this.baseURL,
74
+ });
78
75
  }
79
76
 
80
77
  _buildAITools() {
@@ -111,15 +108,13 @@ class SubAgent extends BaseAgent {
111
108
  const mainPrompt = mainAgent.getOriginalPrompt();
112
109
  if (mainPrompt) { lines.push('## 主Agent的系统提示词'); lines.push(mainPrompt); lines.push(''); }
113
110
  }
114
- const subagentManager = this.framework?.pluginManager?.get('subagent-manager');
115
- if (subagentManager?._buildDescription) lines.push(subagentManager._buildDescription());
116
111
  lines.push('');
117
- const extExecutor = this.framework?.pluginManager?.get('extension-executor');
118
- const extDesc = extExecutor?._buildExtensionsDescription?.();
119
- if (extDesc) { lines.push(extDesc); lines.push(''); }
120
- for (const part of this._promptParts) {
112
+ // 渲染通过 promptRegistry 注册的 prompt part(按 slot/order 排序)
113
+ // 扩展工具描述、子Agent 描述等都由 _syncPromptParts 同步到 promptRegistry,
114
+ // 无需再单独硬编码调用 _buildExtensionsDescription / _buildDescription
115
+ for (const part of (this.promptRegistry?.list() || [])) {
121
116
  try {
122
- const content = typeof part.provider === 'function' ? part.provider() : '';
117
+ const content = part.get();
123
118
  if (content?.trim()) lines.push('', content.trim());
124
119
  } catch (err) {
125
120
  logger.warn(`[SubAgent:${this.name}] Prompt part '${part.name}' failed:`, err.message);
@@ -127,7 +122,7 @@ class SubAgent extends BaseAgent {
127
122
  }
128
123
  lines.push('', '## 核心职责', `- **专注本职**:只完成与 ${this.role} 相关的任务`, '- **不越界**:不要代替其他子Agent完成任务', '- **返回结果**:返回简洁明确的操作结果', '');
129
124
  lines.push('当你被调用时,你应该:', '1. 仔细理解任务要求', '2. 如果任务不在职责范围内,返回"此任务不在我的职责范围内"', '3. 使用你的工具集完成任务', '4. 返回完整结果');
130
- lines.push('', '## 禁止事项', '- 处理本职外的任务', '- 代替其他子Agent执行操作', '- 在回复中透露内部实现细节', '- 不先调用 ext_skill/loadSkill 就直接使用 ext_call');
125
+ lines.push('', '## 禁止事项', '- 处理本职外的任务', '- 代替其他子Agent执行操作', '- 在回复中透露内部实现细节', '- 不先调用 ext_skill/skill_load 就直接使用 ext_call');
131
126
  return lines.join('\n');
132
127
  }
133
128
 
@@ -162,7 +157,7 @@ class SubAgent extends BaseAgent {
162
157
  } catch (err) {
163
158
  lastError = err;
164
159
  if (isNetworkError(err) && attempt < maxRetries) {
165
- logger.warn(`[SubAgent:${this.name}] AI unavailable, retrying (${attempt + 1}/${maxRetries})`);
160
+ logger.warn(`[SubAgent:${this.name}] AI 服务不可用,重试中 (${attempt + 1}/${maxRetries})`);
166
161
  await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
167
162
  continue;
168
163
  }
@@ -5,70 +5,11 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
- const { execSync } = require('child_process');
9
8
  const https = require('https');
10
9
  const http = require('http');
11
- const { DEFAULT_REPO, shouldIgnore } = require('../utils/plugin-config');
10
+ const { DEFAULT_REPO, shouldIgnore, copyDirRecursive, parseGitUrl, gitCommand } = require('../../utils');
12
11
  const { AGENT_DIR_NAME } = require('../utils/config');
13
12
 
14
- /**
15
- * 递归复制目录(带过滤)
16
- */
17
- function copyDirRecursive(src, dest, ignorePatterns = []) {
18
- if (!fs.existsSync(src)) return;
19
-
20
- fs.mkdirSync(dest, { recursive: true });
21
-
22
- const entries = fs.readdirSync(src, { withFileTypes: true });
23
-
24
- for (const entry of entries) {
25
- const srcPath = path.join(src, entry.name);
26
- const destPath = path.join(dest, entry.name);
27
-
28
- // 检查是否忽略
29
- if (shouldIgnore(entry.name)) {
30
- console.log(` Ignoring: ${entry.name}`);
31
- continue;
32
- }
33
-
34
- if (entry.isDirectory()) {
35
- copyDirRecursive(srcPath, destPath, ignorePatterns);
36
- } else {
37
- fs.copyFileSync(srcPath, destPath);
38
- }
39
- }
40
- }
41
-
42
- /**
43
- * 解析 Git URL 获取信息
44
- */
45
- function parseGitUrl(url) {
46
- // 支持多种格式
47
- const patterns = [
48
- /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
49
- /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
50
- ];
51
-
52
- for (const pattern of patterns) {
53
- const match = url.match(pattern);
54
- if (match) {
55
- return { owner: match[1], repo: match[2] };
56
- }
57
- }
58
- return null;
59
- }
60
-
61
- /**
62
- * 执行 Git 命令
63
- */
64
- function gitCommand(args, cwd) {
65
- try {
66
- return execSync(`git ${args}`, { cwd, encoding: 'utf-8', stdio: 'pipe' });
67
- } catch (err) {
68
- return err.stdout || err.stderr || '';
69
- }
70
- }
71
-
72
13
  /**
73
14
  * 下载文件
74
15
  */
@@ -168,7 +168,7 @@ class ChatUIOld {
168
168
 
169
169
  async _reloadSkills() {
170
170
  try {
171
- const result = await this.agent.framework.executeTool('reloadSkills', {});
171
+ const result = await this.agent.framework.executeTool('skill_reload', {});
172
172
  if (result.success !== false) {
173
173
  console.log(colored('[提示] 技能已重载', CYAN));
174
174
  } else {
@@ -287,9 +287,11 @@ class ChatUI {
287
287
  const self=this;
288
288
  queue.add(function(){
289
289
  var next = this.next;
290
+ self.interrupted = false; // 新消息重置中断状态,确保 stream chunks 正常处理
290
291
  self.agent.sendMessage(message, { sessionId:self.sessionId }).then(next).catch(function(e){
291
- self.create_message(chalk.red(`[错误] ${e?.message || e || '发送消息失败'}\n`), colored('● ', RED))
292
+ // 错误由 sessionScope.on('queue:failed') 统一显示,此处只需清理状态
292
293
  self.clear_message_done();
294
+ next(); // 必须调 next() 推进队列,否则后续消息卡死
293
295
  });
294
296
  })
295
297
 
@@ -321,7 +323,7 @@ class ChatUI {
321
323
 
322
324
  if (cmd === '/reload') {
323
325
  try {
324
- const result = await this.agent.framework.executeTool('reloadSkills', {});
326
+ const result = await this.agent.framework.executeTool('skill_reload', {});
325
327
  if (result.success !== false) {
326
328
  this.create_message(`${colored('[提示]', CYAN)} 技能已重载\n`,chalk.dim('● '))
327
329
  } else {
@@ -592,11 +594,11 @@ class ChatUI {
592
594
  }
593
595
  } else if (chunk.type === 'tool-call') {
594
596
  const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 30) : '';
595
- this.statusBar.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
597
+ this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(args+'...')}`)
596
598
  } else if(chunk.type==='tool-result'){
597
599
  const result = chunk.result;
598
600
  const shortResult = typeof result === 'string' ? result.slice(0, 30) : JSON.stringify(result).slice(0, 30);
599
- this.statusBar.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
601
+ this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(shortResult+'...')}`)
600
602
 
601
603
  // 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
602
604
  if (chunk.toolName === 'edit' || chunk.toolName === 'edit_file') {
@@ -610,7 +612,7 @@ class ChatUI {
610
612
  const bgColor = chalk.bgHex('#1a1a2e');
611
613
  this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
612
614
  // this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
613
- this.statusBar.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
615
+ this.statusBar.tooler.show(`${chalk.green('[差异]')} ${folikoGold(resultObj.filePath)}`);
614
616
  }
615
617
  }
616
618
  // setTimeout(() => this.tooler.hide(), 3000);
@@ -662,23 +664,25 @@ class ChatUI {
662
664
 
663
665
  this.interrupted = false;
664
666
 
665
- const interruptHandler = () => {
666
- if (!this.interrupted) {
667
- this.interrupted = true;
668
- console.log(`\n${colored('[中断]', RED)} 输出已中断`);
669
- }
670
- };
671
-
672
-
673
667
  this.tui.addInputListener((data) => {
674
668
  if (matchesKey(data, 'ctrl+c')) {
675
- this.tui.stop();
676
- process.exit(0);
669
+ if (this.isResponding) {
670
+ // 正在响应中:中断对话但不退出
671
+ this.isResponding = false;
672
+ this.interrupted = true;
673
+ this.agent?.framework?.stop();
674
+ } else {
675
+ // 空闲状态:退出
676
+ this.tui.stop();
677
+ process.exit(0);
678
+ }
677
679
  }
678
680
  });
679
681
  this.tui.start();
680
-
681
- //process.on('SIGINT', interruptHandler);
682
+
683
+ process.on('SIGINT', () => {
684
+ // 防止默认 SIGINT 退出(由 tui 的 ctrl+c 处理)
685
+ });
682
686
  }
683
687
 
684
688
 
@@ -78,7 +78,7 @@ class AgentMentionProvider {
78
78
 
79
79
  const mainAgent = this.framework._mainAgent;
80
80
  return this.framework._agents
81
- .filter(agent => agent !== mainAgent && agent.name)
81
+ .filter(agent => agent !== mainAgent && agent.name && !agent.hidden)
82
82
  .map(agent => ({
83
83
  name: agent.name.replace(/^subagent_/, '').replace(/^session_/, ''),
84
84
  description: agent._role || agent.description || agent.name,