foliko 2.0.3 → 2.0.5

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.
@@ -3,40 +3,29 @@
3
3
  /**
4
4
  * PromptRegistry — 全局系统提示词注册表
5
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 完全切换到本注册表
6
+ * 简化设计:
7
+ * - priority: 数字越小越靠前(默认 100)
8
+ * - scope: 'global'(公用)| 'main'(主 agent)| agentId(特定子 agent)
9
+ * - 去掉槽位概念
21
10
  */
22
11
 
23
12
  const { EventEmitter } = require('../common/events');
24
- const { PROMPT_SLOT } = require('../common/constants');
13
+ const fs = require('fs');
14
+ const path = require('path');
25
15
 
26
16
  /**
27
17
  * 单个 prompt part
28
- * - provider 是 () => string|null,返回 null/空串时被 build 跳过
29
- * - 内部带缓存,失效后才重新计算
30
18
  */
31
19
  class PromptPart {
32
20
  constructor(owner, name, provider, options = {}) {
33
21
  this.owner = owner;
34
22
  this.name = name;
35
23
  this.provider = provider;
36
- this.slot = options.slot || 'CUSTOM';
37
- this.order = options.order ?? null; // 显式 order 覆盖 slot 默认
24
+ // scope: 'global' | 'main' | string (agentId)
25
+ this.scope = options.scope || 'global';
26
+ // priority: 数字越小越靠前,默认 100
27
+ this.priority = options.priority ?? 100;
38
28
  this.description = options.description || '';
39
- this.metadata = options.metadata || {};
40
29
  this._cached = null;
41
30
  this._dirty = true;
42
31
  this._error = null;
@@ -53,8 +42,6 @@ class PromptPart {
53
42
  this._error = e;
54
43
  this._cached = null;
55
44
  this._dirty = false;
56
- // 警告但不抛错 — 避免单个 part 失败导致整个 prompt 渲染失败
57
- // eslint-disable-next-line no-console
58
45
  console.warn(`[PromptRegistry:${this.owner}::${this.name}] provider failed:`, e.message);
59
46
  }
60
47
  }
@@ -70,61 +57,25 @@ class PromptPart {
70
57
  * PromptRegistry 主类
71
58
  */
72
59
  class PromptRegistry extends EventEmitter {
73
- constructor(options = {}) {
60
+ constructor() {
74
61
  super();
75
-
76
62
  // key = "owner::name"
77
63
  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 时检测循环依赖
64
+ // 用于 build 时检测循环依赖
93
65
  this._building = false;
94
66
  }
95
67
 
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
68
  // ==================== 注册/注销 ====================
116
69
 
117
70
  /**
118
71
  * 注册一个 prompt part
119
- * @param {string} owner 拥有者标识(通常是 plugin name 或 '_main')
72
+ * @param {string} owner 拥有者标识
120
73
  * @param {string} name part 名(在 owner 内唯一)
121
74
  * @param {Function} provider () => string|null
122
75
  * @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}
76
+ * @param {string} [options.scope='global'] 作用域: 'global'|'main'|agentId
77
+ * @param {number} [options.priority=100] 优先级(越小越靠前)
78
+ * @param {string} [options.description] 描述
128
79
  */
129
80
  register(owner, name, provider, options = {}) {
130
81
  if (!owner || typeof owner !== 'string') {
@@ -140,13 +91,53 @@ class PromptRegistry extends EventEmitter {
140
91
  const key = `${owner}::${name}`;
141
92
  const part = new PromptPart(owner, name, provider, options);
142
93
  this._parts.set(key, part);
143
- this.emit('register', { owner, name, slot: part.slot });
94
+ this.emit('register', { owner, name, scope: part.scope, priority: part.priority });
144
95
  return part;
145
96
  }
146
97
 
98
+ /**
99
+ * 注册一个 prompt part(从文件读取,支持变量插值)
100
+ * @param {string} owner 拥有者标识
101
+ * @param {string} filePath 文件路径
102
+ * @param {Object} [vars] 变量对象,模板中用 {{varName}} 引用
103
+ * @param {Object} [options] 同 register 选项
104
+ */
105
+ registerFile(owner, filePath, vars = {}, options = {}) {
106
+ if (!owner || typeof owner !== 'string') {
107
+ throw new Error('PromptRegistry.registerFile: owner is required');
108
+ }
109
+ if (!filePath || typeof filePath !== 'string') {
110
+ throw new Error('PromptRegistry.registerFile: filePath is required');
111
+ }
112
+
113
+ // 读取文件内容
114
+ const fullPath = path.resolve(filePath);
115
+ if (!fs.existsSync(fullPath)) {
116
+ throw new Error(`PromptRegistry.registerFile: file not found: ${fullPath}`);
117
+ }
118
+ let content = fs.readFileSync(fullPath, 'utf-8');
119
+
120
+ // 变量插值
121
+ if (vars && typeof vars === 'object') {
122
+ content = content.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
123
+ if (Object.prototype.hasOwnProperty.call(vars, varName)) {
124
+ return String(vars[varName]);
125
+ }
126
+ return match; // 未找到的变量保留原样
127
+ });
128
+ }
129
+
130
+ // 转为 provider
131
+ const provider = () => content;
132
+
133
+ // 从文件名生成 name
134
+ const name = options.name || path.basename(filePath, path.extname(filePath));
135
+
136
+ return this.register(owner, name, provider, options);
137
+ }
138
+
147
139
  /**
148
140
  * 注销一个 part
149
- * @returns {boolean} 是否真的删除了
150
141
  */
151
142
  unregister(owner, name) {
152
143
  const key = `${owner}::${name}`;
@@ -157,7 +148,6 @@ class PromptRegistry extends EventEmitter {
157
148
 
158
149
  /**
159
150
  * 清空指定 owner 的所有 parts
160
- * @returns {number} 删除的 part 数
161
151
  */
162
152
  clearOwner(owner) {
163
153
  const prefix = `${owner}::`;
@@ -173,7 +163,7 @@ class PromptRegistry extends EventEmitter {
173
163
  }
174
164
 
175
165
  /**
176
- * 失效指定 part(下次 build 时重新计算)
166
+ * 失效指定 part
177
167
  */
178
168
  invalidate(owner, name) {
179
169
  const part = this._parts.get(`${owner}::${name}`);
@@ -210,10 +200,14 @@ class PromptRegistry extends EventEmitter {
210
200
  }
211
201
 
212
202
  /**
213
- * 列出所有 parts(按 order 排序)
203
+ * 列出所有 parts(按 priority 排序)
214
204
  */
215
205
  list() {
216
- return Array.from(this._parts.values()).sort((a, b) => this._compareOrder(a, b));
206
+ return Array.from(this._parts.values()).sort((a, b) => {
207
+ if (a.priority !== b.priority) return a.priority - b.priority;
208
+ if (a.owner !== b.owner) return a.owner.localeCompare(b.owner);
209
+ return a.name.localeCompare(b.name);
210
+ });
217
211
  }
218
212
 
219
213
  /**
@@ -224,19 +218,24 @@ class PromptRegistry extends EventEmitter {
224
218
  }
225
219
 
226
220
  /**
227
- * 按 slot 过滤
221
+ * 按 scope 过滤
228
222
  */
229
- listBySlot(slot) {
230
- return this.list().filter((p) => p.slot === slot);
223
+ listByScope(scope) {
224
+ return this.list().filter((p) => p.scope === scope);
231
225
  }
232
226
 
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);
227
+ /**
228
+ * 获取适用于指定 agent parts
229
+ * @param {string|null} agentId agent ID,null 表示主 agent
230
+ */
231
+ listForAgent(agentId = null) {
232
+ const isMain = agentId === null || agentId === 'main';
233
+ return this.list().filter((p) => {
234
+ if (p.scope === 'global') return true;
235
+ if (p.scope === 'main' && isMain) return true;
236
+ if (p.scope === agentId) return true;
237
+ return false;
238
+ });
240
239
  }
241
240
 
242
241
  count() {
@@ -246,16 +245,24 @@ class PromptRegistry extends EventEmitter {
246
245
  // ==================== 渲染 ====================
247
246
 
248
247
  /**
249
- * 渲染完整 prompt 文本(按 slot order 拼接)
248
+ * 渲染完整 prompt(按 priority 拼接)
250
249
  */
251
250
  build() {
251
+ return this.buildForAgent(null);
252
+ }
253
+
254
+ /**
255
+ * 渲染适用于指定 agent 的 prompt
256
+ * @param {string|null} agentId agent ID
257
+ */
258
+ buildForAgent(agentId = null) {
252
259
  if (this._building) {
253
260
  throw new Error('PromptRegistry.build() re-entered — check provider for circular calls');
254
261
  }
255
262
  this._building = true;
256
263
  try {
257
264
  const sections = [];
258
- for (const part of this.list()) {
265
+ for (const part of this.listForAgent(agentId)) {
259
266
  const content = part.get();
260
267
  if (content && content.trim()) sections.push(content.trim());
261
268
  }
@@ -266,15 +273,14 @@ class PromptRegistry extends EventEmitter {
266
273
  }
267
274
 
268
275
  /**
269
- * build() 的语义化别名,调试用
276
+ * build() 的别名
270
277
  */
271
278
  preview() {
272
279
  return this.build();
273
280
  }
274
281
 
275
282
  /**
276
- * 渲染结构化清单(调试 / 文档生成)
277
- * 返回 [{ owner, name, slot, order, length, contentPreview }]
283
+ * 渲染结构化清单(调试用)
278
284
  */
279
285
  inspect() {
280
286
  return this.list().map((part) => {
@@ -282,8 +288,8 @@ class PromptRegistry extends EventEmitter {
282
288
  return {
283
289
  owner: part.owner,
284
290
  name: part.name,
285
- slot: part.slot,
286
- order: part.order != null ? part.order : this._slots.get(part.slot)?.order ?? 999,
291
+ scope: part.scope,
292
+ priority: part.priority,
287
293
  description: part.description,
288
294
  hasContent: Boolean(content && content.trim()),
289
295
  length: content ? content.length : 0,
@@ -293,4 +299,4 @@ class PromptRegistry extends EventEmitter {
293
299
  }
294
300
  }
295
301
 
296
- module.exports = { PromptRegistry, PromptPart };
302
+ module.exports = { PromptRegistry, PromptPart };
package/src/agent/sub.js CHANGED
@@ -157,7 +157,7 @@ class SubAgent extends BaseAgent {
157
157
  } catch (err) {
158
158
  lastError = err;
159
159
  if (isNetworkError(err) && attempt < maxRetries) {
160
- logger.warn(`[SubAgent:${this.name}] AI unavailable, retrying (${attempt + 1}/${maxRetries})`);
160
+ logger.warn(`[SubAgent:${this.name}] AI 服务不可用,重试中 (${attempt + 1}/${maxRetries})`);
161
161
  await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
162
162
  continue;
163
163
  }
@@ -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
 
@@ -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
 
@@ -260,7 +260,7 @@ class ChatQueueManager extends EventEmitter {
260
260
  this.activeCount = 0;
261
261
 
262
262
  pendingItems.forEach((item) => {
263
- item.reject(new Error('Queue cleared by user'));
263
+ item.reject(new Error('队列已清空'));
264
264
  });
265
265
 
266
266
  this.emit('queue:cleared', { count: pendingItems.length });