foliko 1.1.19 → 1.1.21

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.
@@ -6,8 +6,9 @@
6
6
 
7
7
  const { EventEmitter } = require('../utils/event-emitter');
8
8
  const { cleanResponse } = require('../utils');
9
- const { generateText, tool, stepCountIs, isLoopFinished } = require('ai');
9
+ const { generateText, tool, stepCountIs, isLoopFinished, RetryError } = require('ai');
10
10
  const { z } = require('zod');
11
+ const { logger } = require('../utils/logger');
11
12
 
12
13
  class Subagent extends EventEmitter {
13
14
  /**
@@ -49,7 +50,7 @@ class Subagent extends EventEmitter {
49
50
  };
50
51
  // 如果提供了 systemPrompt 则使用,否则标记为需要动态构建
51
52
  this._customSystemPrompt = config.systemPrompt || null;
52
- this.parentTools = config?.parentTools || [];
53
+ this.parentTools = config?.parentTools;
53
54
  // 工具管理
54
55
  this._tools = new Map();
55
56
  this._registerTools(config.tools || []);
@@ -120,20 +121,31 @@ class Subagent extends EventEmitter {
120
121
  const tools = {};
121
122
  // 从父Agent继承工具
122
123
  const all_tools = this.framework.getTools();
123
- this.parentTools = this.parentTools.map((key) => {
124
- return this.bindTools[key.toLocaleLowerCase()] || key;
125
- });
126
- for (const toolName of this.parentTools) {
127
- const toolDef = all_tools.find((t) => t.name === toolName);
128
- if (toolDef) {
129
- tools[toolDef.name] = toolDef;
124
+ let parentTools = [];
125
+ if (Array.isArray(this.parentTools)) {
126
+ parentTools = this.parentTools.map((key) => {
127
+ return this.bindTools[key.toLocaleLowerCase()] || key;
128
+ });
129
+ for (const toolName of parentTools) {
130
+ const toolDef = all_tools.find((t) => t.name === toolName);
131
+ if (toolDef) {
132
+ tools[toolDef.name] = toolDef;
133
+ }
134
+ }
135
+ const defaulTools = all_tools.filter((a) => this.defaulTools.includes(a.name));
136
+
137
+ defaulTools.map((tool) => {
138
+ tools[tool.name] = tool;
139
+ });
140
+ } else {
141
+ for (const toolName of all_tools) {
142
+ const toolDef = all_tools.find((t) => t.name === toolName);
143
+ if (toolDef) {
144
+ tools[toolDef.name] = toolDef;
145
+ }
130
146
  }
131
147
  }
132
- const defaulTools = all_tools.filter((a) => this.defaulTools.includes(a.name));
133
148
 
134
- defaulTools.map((tool) => {
135
- tools[tool.name] = tool;
136
- });
137
149
  return { ...tools, ...this._tools };
138
150
  }
139
151
 
@@ -217,47 +229,90 @@ class Subagent extends EventEmitter {
217
229
  * @param {Object} [options] - 选项
218
230
  * @param {number} [options.maxSteps] - 最大步数
219
231
  * @param {AbortSignal} [options.signal] - 中止信号
232
+ * @param {number} [options.maxRetries] - 最大重试次数
220
233
  * @returns {Promise<{success: boolean, message: string, steps: number}>}
221
234
  */
222
235
  async chat(task, options = {}) {
223
236
  const maxSteps = options?.maxSteps || 30;
237
+ const maxRetries = options?.maxRetries ?? 2;
224
238
  const aiProvider = this._getAIProvider();
225
239
  const messages = [];
226
240
  messages.push({ role: 'user', content: task });
227
- try {
228
- const tools = this._buildAITools();
229
- const systemPrompt = this._buildSystemPrompt();
230
- const result = await generateText({
231
- model: aiProvider(this.model),
232
- system: systemPrompt,
233
- messages: messages,
234
- tools: tools,
235
- stopWhen: stepCountIs(maxSteps),
236
- ...this.providerOptions,
237
- abortSignal: options.signal,
238
- onChunk: (chunk) => {
239
- this.emit('chunk', chunk);
240
- },
241
- });
242
- messages.push(...result.response.messages);
243
- const full_text = cleanResponse(result.text);
244
- this.emit('complete', { message: full_text, steps: result.steps?.length || 0 });
245
- return {
246
- success: true,
247
- message: full_text,
248
- steps: result.steps?.length || 0,
249
- };
250
- } catch (err) {
251
- this.emit('error', { error: err.message });
252
- return {
253
- success: false,
254
- message: '',
255
- error: err.message,
256
- steps: 0,
257
- };
241
+
242
+ let lastError;
243
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
244
+ try {
245
+ const tools = this._buildAITools();
246
+ const systemPrompt = this._buildSystemPrompt();
247
+ const result = await generateText({
248
+ model: aiProvider(this.model),
249
+ system: systemPrompt,
250
+ messages: messages,
251
+ tools: tools,
252
+ stopWhen: stepCountIs(maxSteps),
253
+ ...this.providerOptions,
254
+ abortSignal: options.signal,
255
+ onChunk: (chunk) => {
256
+ this.emit('chunk', chunk);
257
+ },
258
+ });
259
+ messages.push(...result.response.messages);
260
+ const full_text = cleanResponse(result.text);
261
+ this.emit('complete', { message: full_text, steps: result.steps?.length || 0 });
262
+ return {
263
+ success: true,
264
+ message: full_text,
265
+ steps: result.steps?.length || 0,
266
+ };
267
+ } catch (err) {
268
+ lastError = err;
269
+ const errName = err?.name || '';
270
+
271
+ // 判断是否是重试错误
272
+ const isRetryError =
273
+ RetryError.isInstance?.(err) ||
274
+ errName === 'AI_RetryError' ||
275
+ errName === 'RetryError' ||
276
+ err?.reason === 'maxRetriesExceeded';
277
+
278
+ if (isRetryError && attempt < maxRetries) {
279
+ logger.warn(`[Subagent:${this.name}] AI 服务不可用,${attempt + 1}/${maxRetries} 次重试`);
280
+ await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
281
+ continue;
282
+ }
283
+
284
+ // 最终失败,转换为友好消息
285
+ const friendlyMessage = this._getFriendlyError(err);
286
+ this.emit('error', { error: friendlyMessage });
287
+ return {
288
+ success: false,
289
+ message: '',
290
+ error: friendlyMessage,
291
+ steps: 0,
292
+ };
293
+ }
258
294
  }
259
295
  }
260
296
 
297
+ /**
298
+ * 获取友好错误消息
299
+ * @private
300
+ */
301
+ _getFriendlyError(err) {
302
+ const errName = err?.name || '';
303
+ const errorMessages = {
304
+ AI_RetryError: 'AI 服务暂时不可用,请稍后重试',
305
+ AI_APICallError: err?.isRetryable
306
+ ? 'AI 服务暂时不可用,请稍后重试'
307
+ : err.message?.split('\n')[0] || 'AI 请求失败',
308
+ AI_NoContentGeneratedError: 'AI 未生成有效内容,请重试',
309
+ AI_NoSuchModelError: '指定的 AI 模型不存在',
310
+ AI_NoSuchProviderError: 'AI 提供商配置错误',
311
+ AI_LoadAPIKeyError: 'AI API 密钥配置错误',
312
+ };
313
+ return errorMessages[errName] || err.message?.split('\n')[0] || '未知错误';
314
+ }
315
+
261
316
  /**
262
317
  * 设置角色(会清除自定义 systemPrompt)
263
318
  * @param {string} role - 角色描述
@@ -70,13 +70,25 @@ class ChatQueueManager extends EventEmitter {
70
70
 
71
71
  try {
72
72
  const result = await this.executeWithRetry(item);
73
- item.resolve(result);
74
- this.emit('queue:completed', {
75
- requestId: item.id,
76
- sessionId: item.sessionId,
77
- duration: Date.now() - item.timestamp,
78
- });
73
+ // 检查 result 是否有错误(而不是通过 try/catch)
74
+ if (result.error) {
75
+ console.log('[ChatQueue] Rejecting with error from result:', result.error.message);
76
+ item.reject(result.error);
77
+ this.emit('queue:failed', {
78
+ requestId: item.id,
79
+ sessionId: item.sessionId,
80
+ error: result.error.message,
81
+ });
82
+ } else {
83
+ item.resolve(result);
84
+ this.emit('queue:completed', {
85
+ requestId: item.id,
86
+ sessionId: item.sessionId,
87
+ duration: Date.now() - item.timestamp,
88
+ });
89
+ }
79
90
  } catch (error) {
91
+ console.log('[ChatQueue] Rejecting with thrown error:', error.message);
80
92
  item.reject(error);
81
93
  this.emit('queue:failed', {
82
94
  requestId: item.id,
@@ -103,8 +115,34 @@ class ChatQueueManager extends EventEmitter {
103
115
  maxRetries: this.retryAttempts,
104
116
  });
105
117
 
106
- return await this.executeStream(item);
118
+ const result = await this.executeStream(item);
119
+
120
+ // 检查是否有错误(通过返回的 result.error)
121
+ if (result.error) {
122
+ console.log(
123
+ '[ChatQueue] executeWithRetry: attempt',
124
+ attempt,
125
+ 'got error:',
126
+ result.error.message
127
+ );
128
+ lastError = result.error;
129
+ if (attempt < this.retryAttempts && this.isRetryableError(lastError)) {
130
+ await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
131
+ continue;
132
+ }
133
+ // 返回 result,让 processQueue 处理错误
134
+ console.log('[ChatQueue] executeWithRetry: returning result with error, no more retries');
135
+ return result;
136
+ }
137
+
138
+ return result;
107
139
  } catch (error) {
140
+ console.log(
141
+ '[ChatQueue] executeWithRetry: attempt',
142
+ attempt,
143
+ 'threw error:',
144
+ error.message
145
+ );
108
146
  lastError = error;
109
147
  if (attempt < this.retryAttempts && this.isRetryableError(error)) {
110
148
  await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
@@ -113,7 +151,18 @@ class ChatQueueManager extends EventEmitter {
113
151
  break;
114
152
  }
115
153
  }
116
- throw lastError;
154
+
155
+ // 将最后的错误转换为友好消息
156
+ const errName = lastError?.name || '';
157
+ const isRetryError = errName === 'AI_RetryError' || errName === 'RetryError';
158
+ const friendlyMessage = isRetryError
159
+ ? 'AI 服务暂时不可用,请稍后重试'
160
+ : (lastError?.message || String(lastError)).split('\n')[0];
161
+
162
+ const friendlyError = new Error(friendlyMessage);
163
+ friendlyError.originalError = lastError;
164
+ console.log('[ChatQueue] executeWithRetry: throwing friendly error:', friendlyMessage);
165
+ throw friendlyError;
117
166
  }
118
167
 
119
168
  /**
@@ -128,12 +177,34 @@ class ChatQueueManager extends EventEmitter {
128
177
 
129
178
  const stream = item.executeFunction(item.message, item.options);
130
179
 
131
- for await (const chunk of stream) {
132
- chunks.push(chunk);
180
+ try {
181
+ for await (const chunk of stream) {
182
+ chunks.push(chunk);
183
+ this.emit('stream:chunk', {
184
+ requestId: item.id,
185
+ sessionId: item.sessionId,
186
+ chunk,
187
+ accumulated: chunks.length,
188
+ });
189
+ }
190
+ } catch (err) {
191
+ // SDK 直接抛出错误(没有通过 chunk 传递)
192
+ // 转换为友好错误消息
193
+ const errName = err?.name || '';
194
+ const isRetryError = errName === 'AI_RetryError' || errName === 'RetryError';
195
+ const friendlyMessage = isRetryError
196
+ ? 'AI 服务暂时不可用,请稍后重试'
197
+ : (err.message || err.toString()).split('\n')[0];
198
+
199
+ console.log(
200
+ '[ChatQueue] executeStream caught error, converting to friendly:',
201
+ friendlyMessage
202
+ );
203
+ chunks.push({ type: 'error', error: friendlyMessage });
133
204
  this.emit('stream:chunk', {
134
205
  requestId: item.id,
135
206
  sessionId: item.sessionId,
136
- chunk,
207
+ chunk: chunks[chunks.length - 1],
137
208
  accumulated: chunks.length,
138
209
  });
139
210
  }
@@ -141,9 +212,19 @@ class ChatQueueManager extends EventEmitter {
141
212
  // 检查是否有错误 chunk
142
213
  const errorChunk = chunks.find((c) => c.type === 'error');
143
214
  if (errorChunk) {
215
+ // 不再抛出错误,而是返回包含错误的 result
216
+ // 让调用者通过 result 判断是否有错误
144
217
  const error = new Error(errorChunk.error || 'Stream error');
145
218
  error.chunks = chunks;
146
- throw error;
219
+ error.isStreamError = true;
220
+ console.log('[ChatQueue] executeStream returning result with error:', error.message);
221
+ return {
222
+ chunks,
223
+ content: cleanResponse(''),
224
+ sessionId: item.sessionId,
225
+ requestId: item.id,
226
+ error,
227
+ };
147
228
  }
148
229
 
149
230
  const fullText = chunks
package/system.md CHANGED
@@ -5,6 +5,85 @@
5
5
  【元数据】
6
6
  - WORK_DIR: D:\code\vb-agent
7
7
 
8
+ ## 【Ambient Agent 监控任务指南】
9
+
10
+ 当用户请求创建条件监控任务(如"价格高于X时通知我"、"收到某邮件时自动回复")时,使用以下模式:
11
+
12
+ ### 标准条件监控配置
13
+
14
+ ```javascript
15
+ {
16
+ "title": "监控任务标题",
17
+ "description": "监控条件描述(包含阈值等信息)",
18
+ "conditions": { "events": ["scheduler:reminder"] },
19
+ "persistent": true,
20
+ "actions": [
21
+ {
22
+ "id": "get_data",
23
+ "type": "tool",
24
+ "plugin": "gate-trading", // 或 email, web 等
25
+ "name": "gate_get_market_info",
26
+ "args": { "currency_pair": "BTC_USDT", "info_type": "tickers" }
27
+ },
28
+ {
29
+ "id": "judge",
30
+ "type": "message",
31
+ "content": "你是一个条件判断助手。\n\n上一步获取到了数据:${result.last}\n\n判断规则:\n- 如果数据满足条件,调用 notification_send 发送通知\n- 如果不满足条件,静默结束(不要发送任何通知)\n\n你的任务是分析数据,判断是否需要通知用户。"
32
+ }
33
+ ]
34
+ }
35
+ ```
36
+
37
+ ### 关键要点
38
+
39
+ 1. **actions 必须包含两个步骤**:
40
+ - 第一步:用 `type: "tool"` 获取数据
41
+ - 第二步:用 `type: "message"` 让 LLM 判断是否发送通知
42
+
43
+ 2. **message content 中的判断提示词**:
44
+ - 明确告诉 LLM 阈值是多少
45
+ - 说明满足条件时应该做什么
46
+ - 说明不满足条件时应该静默
47
+
48
+ 3. **使用 ${result.xxx} 引用上一步结果**:
49
+ - `${result.last}` - 获取价格数据
50
+ - `${result.from}` - 获取发件人
51
+ - `${result.subject}` - 获取邮件主题
52
+
53
+ ### 示例:比特币价格监控
54
+
55
+ ```javascript
56
+ {
57
+ "title": "比特币价格监控 (BTC > $73000)",
58
+ "description": "每2分钟检查BTC价格,高于73000美元时通知",
59
+ "conditions": { "events": ["scheduler:reminder"] },
60
+ "persistent": true,
61
+ "actions": [
62
+ {
63
+ "type": "tool",
64
+ "plugin": "gate-trading",
65
+ "name": "gate_get_market_info",
66
+ "args": { "currency_pair": "BTC_USDT", "info_type": "tickers" }
67
+ },
68
+ {
69
+ "type": "message",
70
+ "content": "你是条件判断助手。\n\n上一步获取到 BTC 价格数据:${result.last} USDT\n\n监控条件:价格 > 73000 USDT\n\n判断规则:\n- 如果 ${result.last} > 73000,调用 notification_send 发送通知\n- 通知内容:当前 BTC 价格 ${result.last} USDT,已超过 $73000 阈值\n- 如果价格 <= 73000,静默结束,不要发送任何通知"
71
+ }
72
+ ]
73
+ }
74
+ ```
75
+
76
+ ### 常见监控场景配置
77
+
78
+ | 场景 | 工具 | 判断逻辑 |
79
+ |------|------|----------|
80
+ | 价格监控 | gate_get_market_info | 价格 > 阈值 |
81
+ | 邮件监控 | email_read | 主题/发件人匹配 |
82
+ | 余额监控 | gate_get_account_balance | 余额变化 |
83
+ | 网页监控 | web_request | 内容包含某字符串 |
84
+
85
+ ---
86
+
8
87
  ## 【记忆上下文】
9
88
  ### 【用户偏好】
10
89
  - 用户海报风格偏好: ## 用户海报设计风格偏好