foliko 1.0.68 → 1.0.69

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.
package/plugins/email.js CHANGED
@@ -29,6 +29,9 @@ class EmailPlugin extends Plugin {
29
29
  this._lastSeenUid = null
30
30
  this._watchConfig = null
31
31
  this._watchEnabled = false
32
+ this._checkInProgress = false // 防止并发检查
33
+ this._recentlyEmailed = new Set() // 去重:最近已处理的 messageId(短TTL,用于快速去重)
34
+ this._processedEmails = new Set() // 已处理(已回复)的邮件,不会重复处理(长TTL)
32
35
  }
33
36
 
34
37
  install(framework) {
@@ -64,12 +67,36 @@ class EmailPlugin extends Plugin {
64
67
  * 发送邮件事件
65
68
  */
66
69
  _emitEmailReceived(email) {
67
- if (this._framework) {
68
- this._framework.emit('email:received', {
69
- email,
70
- timestamp: new Date()
71
- })
70
+ if (!this._framework) return
71
+ // 去重:如果最近已处理过这个 messageId,跳过
72
+ const msgId = email.messageId || email.uid
73
+ //console.log(`[Email] _emitEmailReceived: msgId=${msgId}, messageId=${email.messageId}, uid=${email.uid}`)
74
+
75
+ // 优先检查是否已处理过(长TTL,防止服务器标记失败导致重复处理)
76
+ if (msgId && this._processedEmails.has(msgId)) {
77
+ console.log(`[Email] 跳过已处理的邮件: ${email.subject} (${msgId})`)
78
+ return
79
+ }
80
+
81
+ // 快速去重(短TTL,防止同一批次重复触发)
82
+ if (msgId && this._recentlyEmailed.has(msgId)) {
83
+ //console.log(`[Email] 跳过重复邮件: ${email.subject} (${msgId})`)
84
+ return
72
85
  }
86
+
87
+ // 标记为已处理
88
+ if (msgId) {
89
+ this._recentlyEmailed.add(msgId)
90
+ // 5分钟后从 Set 中移除,允许重新处理(适用于邮件被删除后重新收到的极端情况)
91
+ setTimeout(() => {
92
+ this._recentlyEmailed.delete(msgId)
93
+ }, 5 * 60 * 1000)
94
+ }
95
+
96
+ this._framework.emit('email:received', {
97
+ email,
98
+ timestamp: new Date()
99
+ })
73
100
  }
74
101
 
75
102
  _registerTools() {
@@ -211,9 +238,12 @@ class EmailPlugin extends Plugin {
211
238
  subject: z.string().describe('原始邮件主题'),
212
239
  body: z.string().describe('原始邮件内容'),
213
240
  from: z.string().optional().describe('发件人邮箱地址(可选)'),
214
- prompt: z.string().optional().describe('自定义提示词,用于指导AI生成回复内容')
241
+ prompt: z.string().optional().describe('自定义提示词,用于指导AI生成回复内容'),
242
+ timeout: z.number().optional().describe('AI生成回复超时时间(毫秒),默认60000'),
243
+ messageId: z.string().optional().describe('原始邮件的Message-ID,用于在邮件客户端中创建回复线程')
215
244
  }),
216
245
  execute: async (args) => {
246
+ console.log('[Email] Auto-reply tool invoked with args:', args)
217
247
  return this._handleAutoReply(args)
218
248
  }
219
249
  })
@@ -223,18 +253,25 @@ class EmailPlugin extends Plugin {
223
253
  * 处理自动回复
224
254
  */
225
255
  async _handleAutoReply(args) {
226
- let { to, subject, body, from, _event, prompt } = args
256
+ let { to, subject, body, from, _event, prompt, messageId, inReplyTo } = args
227
257
 
228
258
  // 如果没有直接参数,尝试从 _event 提取
229
- // _event 格式: { type: 'email:received', email: {...}, timestamp: ... }
259
+ // _event 格式: { event: 'email:received', data: { uid, messageId, from, to, subject, text, ... }, timestamp }
230
260
  if (!to && !subject && !body && _event) {
231
- const email = _event.email || _event.data?.email || {}
261
+ const email = _event.data || _event.email || {}
232
262
  from = email.from?.text || email.from || ''
233
263
  to = email.to?.text || email.to || ''
234
264
  subject = email.subject || ''
235
265
  body = email.text || email.body || ''
266
+ // 从事件中提取 messageId 作为 inReplyTo
267
+ if (!messageId) {
268
+ messageId = email.messageId || email.uid
269
+ }
236
270
  }
237
271
 
272
+ // 优先使用传入的 inReplyTo,否则用 messageId
273
+ const replyTo = inReplyTo || messageId
274
+
238
275
  // 检查必要参数
239
276
  if (!from && !to) {
240
277
  return { success: false, error: '缺少收件人地址' }
@@ -244,11 +281,12 @@ class EmailPlugin extends Plugin {
244
281
  }
245
282
 
246
283
  try {
247
- // 获取活跃的 Agent
248
- const agent = this._getActiveAgent()
249
- if (!agent) {
250
- return { success: false, error: 'No active agent found' }
251
- }
284
+ // 使用子Agent生成回复
285
+ const replyAgent = this._framework.createSubAgent({
286
+ name: 'email_replier',
287
+ role: '邮件自动回复助手,专注于生成专业、礼貌、简洁的邮件回复',
288
+ parentTools: []
289
+ })
252
290
 
253
291
  // 构建提示让 LLM 生成回复
254
292
  const finalPrompt = prompt || `你是一封邮件自动回复助手。请根据以下邮件内容,生成一封专业的回复邮件。
@@ -265,14 +303,14 @@ ${body}
265
303
  3. 只输出邮件正文内容,不要额外解释
266
304
  4. 回复语言应与原邮件一致(如果原邮件是中文,则用中文回复)`
267
305
 
268
- // 等待 Agent 生成回复(带超时保护)
306
+ // 等待 Agent 生成回复(带超时保护,默认60秒)
307
+ const timeoutMs = args.timeout || 60000
269
308
  const timeoutPromise = new Promise((_, reject) => {
270
- setTimeout(() => reject(new Error('AI回复生成超时(30秒)')), 30000)
309
+ setTimeout(() => reject(new Error(`AI回复生成超时(${timeoutMs/1000}秒)`)), timeoutMs)
271
310
  })
272
311
 
273
- const replyPromise = agent.pushMessage(finalPrompt, { maxSteps: 3 })
312
+ const replyPromise = replyAgent.chat(finalPrompt, { maxSteps: 3 })
274
313
  const replyResult = await Promise.race([replyPromise, timeoutPromise])
275
-
276
314
  // 提取回复内容
277
315
  let replyContent = ''
278
316
  if (typeof replyResult === 'string') {
@@ -296,10 +334,33 @@ ${body}
296
334
  const sendResult = await this._sendEmail({
297
335
  to: from || to,
298
336
  subject: `Re: ${subject}`,
299
- body: replyContent
337
+ body: replyContent,
338
+ inReplyTo: replyTo,
339
+ references: replyTo ? [replyTo] : undefined
300
340
  })
301
341
 
302
342
  if (sendResult.success) {
343
+ // 发送成功后,标记为已处理(本地记录,防止服务器标记失败导致重复处理)
344
+ const msgId = messageId || _event?.data?.messageId || _event?.email?.messageId || uid
345
+ if (msgId) {
346
+ this._processedEmails.add(msgId)
347
+ console.log(`[Email] 邮件已处理: ${msgId}`)
348
+ // 24小时后从已处理列表中移除(防止永久记住)
349
+ setTimeout(() => {
350
+ this._processedEmails.delete(msgId)
351
+ }, 24 * 60 * 60 * 1000)
352
+ }
353
+
354
+ // 自动标记原邮件为已读(服务器端)
355
+ const uid = _event?.data?.uid || _event?.email?.uid
356
+ if (uid) {
357
+ try {
358
+ await this._markAsRead({ messageId: String(uid) })
359
+ } catch (err) {
360
+ console.warn(`[Email] 自动标记已读失败: ${err.message}`)
361
+ }
362
+ }
363
+
303
364
  return {
304
365
  success: true,
305
366
  message: `自动回复已发送至 ${from || to}`,
@@ -473,6 +534,17 @@ ${body}
473
534
  async _checkNewEmails() {
474
535
  if (!this._watchConfig) return
475
536
 
537
+ const callId = `${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
538
+ console.log(`[Email] [${callId}] _checkNewEmails called, _checkInProgress=${this._checkInProgress}`)
539
+
540
+ // 防止并发检查
541
+ if (this._checkInProgress) {
542
+ console.log(`[Email] [${callId}] Skipping - already in progress`)
543
+ return
544
+ }
545
+ this._checkInProgress = true
546
+ console.log(`[Email] [${callId}] Started, _checkInProgress=${this._checkInProgress}`)
547
+
476
548
  const Imap = require('imap-mkl')
477
549
  const { simpleParser } = require('mailparser')
478
550
 
@@ -495,6 +567,8 @@ ${body}
495
567
 
496
568
  const cleanup = () => {
497
569
  try { imap.end() } catch (e) {}
570
+ this._checkInProgress = false
571
+ console.log(`[Email] ${Date.now()} cleanup called, _checkInProgress=false`)
498
572
  }
499
573
 
500
574
  imap.on('ready', () => {
@@ -525,6 +599,9 @@ ${body}
525
599
  const f = imap.fetch(latestUid, { bodies: '' })
526
600
 
527
601
  f.on('message', (msg) => {
602
+ let msgCount = 0
603
+ msgCount++
604
+ console.log(`[Email] f.on('message') called count=${msgCount}, uid=${latestUid}`)
528
605
  let email = {}
529
606
  let bodyParsed = false
530
607
 
@@ -532,6 +609,7 @@ ${body}
532
609
  simpleParser(stream).then(mail => {
533
610
  email = {
534
611
  uid: mail.uid || latestUid,
612
+ messageId: mail.messageId,
535
613
  subject: mail.subject,
536
614
  from: mail.from?.text || '',
537
615
  to: mail.to?.text || '',
@@ -551,7 +629,13 @@ ${body}
551
629
  })
552
630
 
553
631
  msg.on('end', () => {
632
+ console.log(`[Email] msg.on('end') called for uid=${latestUid}`)
633
+ let checkCount = 0
554
634
  const checkDone = () => {
635
+ checkCount++
636
+ if (checkCount > 1) {
637
+ console.log(`[Email] checkDone called ${checkCount} times, bodyParsed=${bodyParsed}`)
638
+ }
555
639
  if (bodyParsed) {
556
640
  // 更新最后看到的 UID
557
641
  this._lastSeenUid = latestUid
@@ -620,7 +704,9 @@ ${body}
620
704
  text: args.isHtml ? undefined : args.body,
621
705
  html: args.isHtml ? args.body : undefined,
622
706
  cc: args.cc,
623
- bcc: args.bcc
707
+ bcc: args.bcc,
708
+ inReplyTo: args.inReplyTo,
709
+ references: args.references
624
710
  }
625
711
 
626
712
  // 处理附件
@@ -221,56 +221,102 @@ class FileSystemPlugin extends Plugin {
221
221
  // 搜索文件
222
222
  framework.registerTool({
223
223
  name: 'search_file',
224
- description: '在文件中搜索文本',
224
+ description: '在文件或目录中搜索文本',
225
225
  inputSchema: z.object({
226
226
  query: z.string().optional().describe('搜索关键词'),
227
- pattern: z.string().optional().describe('搜索模式'),
227
+ pattern: z.string().optional().describe('搜索模式(支持正则)'),
228
228
  searchText: z.string().optional().describe('搜索文本'),
229
229
  path: z.string().optional().describe('搜索目录'),
230
230
  dirPath: z.string().optional().describe('搜索目录(同path)'),
231
+ file: z.string().optional().describe('搜索指定文件'),
231
232
  fileType: z.string().optional().describe('文件类型过滤'),
232
- maxResults: z.number().optional().describe('最大结果数')
233
+ maxResults: z.number().optional().describe('最大结果数'),
234
+ contextLines: z.number().optional().describe('匹配行的上下文行数')
233
235
  }),
234
236
  execute: async (args, framework) => {
235
237
  const pattern = args.query || args.pattern || args.searchText || ''
236
238
  const dirPath = args.path || args.dirPath || '.'
239
+ const targetFile = args.file
237
240
  const fileType = args.fileType
238
241
  const maxResults = args.maxResults || 50
242
+ const contextLines = args.contextLines || 0
239
243
  try {
240
244
  const results = []
241
245
  const regex = new RegExp(pattern, 'gi')
242
- const searchDir = (currentPath, depth = 0) => {
243
- if (depth > 5 || results.length >= maxResults) return
244
- const entries = fs.readdirSync(currentPath, { withFileTypes: true })
245
- for (const entry of entries) {
246
- if (results.length >= maxResults) break
247
- const fullPath = path.join(currentPath, entry.name)
248
- const relativePath = path.relative(process.cwd(), fullPath)
249
- if (relativePath.includes('node_modules') || relativePath.includes('.git') ||
250
- relativePath.includes('dist') || relativePath.includes('build')) {
251
- continue
246
+
247
+ // 搜索单个文件
248
+ if (targetFile) {
249
+ const fullPath = path.resolve(targetFile)
250
+ if (!fs.existsSync(fullPath)) {
251
+ return { success: false, error: `文件不存在: ${targetFile}` }
252
+ }
253
+ try {
254
+ const content = fs.readFileSync(fullPath, 'utf8')
255
+ const lines = content.split('\n')
256
+ for (let i = 0; i < lines.length; i++) {
257
+ if (regex.test(lines[i])) {
258
+ let matchInfo = { file: targetFile, line: i + 1, content: lines[i].substring(0, 200) }
259
+ // 添加上下文行
260
+ if (contextLines > 0) {
261
+ const start = Math.max(0, i - contextLines)
262
+ const end = Math.min(lines.length, i + contextLines + 1)
263
+ matchInfo.context = lines.slice(start, end).map((l, idx) => ({
264
+ line: start + idx + 1,
265
+ content: l
266
+ }))
267
+ }
268
+ results.push(matchInfo)
269
+ if (results.length >= maxResults) break
270
+ }
252
271
  }
253
- if (entry.isDirectory()) {
254
- searchDir(fullPath, depth + 1)
255
- } else if (entry.isFile()) {
256
- if (fileType && !entry.name.endsWith(fileType)) continue
257
- const ext = path.extname(entry.name)
258
- if (['.jpg', '.png', '.gif', '.exe', '.dll', '.zip'].includes(ext)) continue
259
- try {
260
- const content = fs.readFileSync(fullPath, 'utf8')
261
- const lines = content.split('\n')
262
- for (let i = 0; i < lines.length; i++) {
263
- if (regex.test(lines[i])) {
264
- results.push({ file: relativePath, line: i + 1, content: lines[i].substring(0, 100) })
265
- if (results.length >= maxResults) break
272
+ regex.lastIndex = 0
273
+ } catch (e) {
274
+ return { success: false, error: `读取文件失败: ${e.message}` }
275
+ }
276
+ } else {
277
+ // 搜索目录
278
+ const searchDir = (currentPath, depth = 0) => {
279
+ if (depth > 5 || results.length >= maxResults) return
280
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true })
281
+ for (const entry of entries) {
282
+ if (results.length >= maxResults) break
283
+ const fullPath = path.join(currentPath, entry.name)
284
+ const relativePath = path.relative(process.cwd(), fullPath)
285
+ if (relativePath.includes('node_modules') || relativePath.includes('.git') ||
286
+ relativePath.includes('dist') || relativePath.includes('build')) {
287
+ continue
288
+ }
289
+ if (entry.isDirectory()) {
290
+ searchDir(fullPath, depth + 1)
291
+ } else if (entry.isFile()) {
292
+ if (fileType && !entry.name.endsWith(fileType)) continue
293
+ const ext = path.extname(entry.name)
294
+ if (['.jpg', '.png', '.gif', '.exe', '.dll', '.zip'].includes(ext)) continue
295
+ try {
296
+ const content = fs.readFileSync(fullPath, 'utf8')
297
+ const lines = content.split('\n')
298
+ for (let i = 0; i < lines.length; i++) {
299
+ if (regex.test(lines[i])) {
300
+ let matchInfo = { file: relativePath, line: i + 1, content: lines[i].substring(0, 200) }
301
+ if (contextLines > 0) {
302
+ const start = Math.max(0, i - contextLines)
303
+ const end = Math.min(lines.length, i + contextLines + 1)
304
+ matchInfo.context = lines.slice(start, end).map((l, idx) => ({
305
+ line: start + idx + 1,
306
+ content: l
307
+ }))
308
+ }
309
+ results.push(matchInfo)
310
+ if (results.length >= maxResults) break
311
+ }
266
312
  }
267
- }
268
- regex.lastIndex = 0
269
- } catch (e) { }
313
+ regex.lastIndex = 0
314
+ } catch (e) { }
315
+ }
270
316
  }
271
317
  }
318
+ searchDir(dirPath)
272
319
  }
273
- searchDir(dirPath)
274
320
  return { success: true, pattern, results: results.slice(0, maxResults), total: results.length }
275
321
  } catch (error) {
276
322
  return { success: false, error: error.message }
@@ -50,10 +50,11 @@ class PythonExecutorPlugin extends Plugin {
50
50
  description: '执行 Python 代码,禁止传入无用的emoji',
51
51
  inputSchema: z.object({
52
52
  code: z.string().describe('Python 代码'),
53
- timeout: z.number().optional().describe('超时时间(毫秒),默认 120000')
53
+ timeout: z.number().optional().describe('超时时间(毫秒),默认 120000'),
54
+ input_data: z.record(z.any()).optional().describe('输入数据对象,可在代码中通过 input_data.get("key") 访问')
54
55
  }),
55
56
  execute: async (args) => {
56
- return this._executePython(args.code, args.timeout || this.config.timeout)
57
+ return this._executePython(args.code, args.timeout || this.config.timeout, args.input_data)
57
58
  }
58
59
  })
59
60
 
@@ -92,7 +93,7 @@ class PythonExecutorPlugin extends Plugin {
92
93
  * 执行 Python 代码
93
94
  * @private
94
95
  */
95
- _executePython(code, timeout) {
96
+ _executePython(code, timeout, inputData) {
96
97
  return new Promise((resolve) => {
97
98
  const startTime = Date.now()
98
99
  let output = ''
@@ -101,10 +102,20 @@ class PythonExecutorPlugin extends Plugin {
101
102
  // 创建临时脚本文件
102
103
  const tempFile = path.join(this._tempDir, `temp_${Date.now()}.py`)
103
104
 
105
+ // 注入 input_data 变量
106
+ let inputDataSetup = ''
107
+ if (inputData) {
108
+ const inputDataJson = JSON.stringify(inputData).replace(/'/g, "\\'")
109
+ inputDataSetup = `import json\ninput_data = json.loads('${inputDataJson}')\n`
110
+ } else {
111
+ inputDataSetup = `input_data = {}\n`
112
+ }
113
+
104
114
  // 包装代码,添加错误处理
105
115
  const wrappedCode = `
106
116
  import sys
107
117
  import traceback
118
+ ${inputDataSetup}
108
119
  try:
109
120
  ${code.split('\n').map(line => ' ' + line).join('\n')}
110
121
  except SystemExit:
@@ -136,13 +147,26 @@ except Exception:
136
147
  } catch (e) { }
137
148
 
138
149
  const elapsed = Date.now() - startTime
150
+
151
+ // 限制返回数据大小,避免上下文超限(限制为 5000 字符)
152
+ const MAX_OUTPUT_LENGTH = 5000
153
+ let truncatedOutput = output
154
+ let truncated = false
155
+ if (output.length > MAX_OUTPUT_LENGTH) {
156
+ truncatedOutput = output.substring(0, MAX_OUTPUT_LENGTH) + `\n... [输出已截断,总长度 ${output.length} 字符]`
157
+ truncated = true
158
+ }
159
+
139
160
  resolve({
140
161
  success: code === 0,
141
162
  exitCode: code,
142
- stdout: output,
143
- stderr: errorOutput,
163
+ stdout: truncatedOutput,
164
+ stderr: errorOutput.length > MAX_OUTPUT_LENGTH
165
+ ? errorOutput.substring(0, MAX_OUTPUT_LENGTH) + `\n... [错误输出已截断]`
166
+ : errorOutput,
144
167
  elapsed,
145
- timedOut: code === null
168
+ timedOut: code === null,
169
+ truncated
146
170
  })
147
171
  })
148
172
 
@@ -164,11 +188,20 @@ except Exception:
164
188
  try {
165
189
  fs.unlinkSync(tempFile)
166
190
  } catch (e) { }
191
+
192
+ // 限制返回数据大小
193
+ const MAX_OUTPUT_LENGTH = 5000
194
+ const truncatedOutput = output.length > MAX_OUTPUT_LENGTH
195
+ ? output.substring(0, MAX_OUTPUT_LENGTH) + `\n... [输出已截断,总长度 ${output.length} 字符]`
196
+ : output
197
+
167
198
  resolve({
168
199
  success: false,
169
200
  error: `Python execution timed out after ${timeout}ms`,
170
- stdout: output,
171
- stderr: errorOutput,
201
+ stdout: truncatedOutput,
202
+ stderr: errorOutput.length > MAX_OUTPUT_LENGTH
203
+ ? errorOutput.substring(0, MAX_OUTPUT_LENGTH) + `\n... [错误输出已截断]`
204
+ : errorOutput,
172
205
  timedOut: true
173
206
  })
174
207
  }
@@ -554,18 +554,14 @@ class SchedulerPlugin extends Plugin {
554
554
 
555
555
  try {
556
556
  if (task.llm) {
557
- // LLM 模式:调用 Agent 处理
558
- const agent = this._getAgent()
559
- if (!agent) {
560
- console.error('[Scheduler] Agent not available')
561
- return
562
- }
563
-
564
- const result = await agent.pushMessage(task.message, {
565
- isScheduledTask: true,
566
- sessionId: task.sessionId
557
+ // LLM 模式:使用子Agent处理
558
+ const schedulerAgent = this._framework.createSubAgent({
559
+ name: 'scheduler_task',
560
+ role: '定时任务执行助手,专注于处理定时提醒和任务执行'
567
561
  })
568
562
 
563
+ const result = await schedulerAgent.chat(task.message)
564
+
569
565
  this._taskStats.completed++
570
566
 
571
567
  // 获取 LLM 返回的消息
@@ -312,11 +312,15 @@ class WebPlugin extends Plugin {
312
312
  // 触发 webhook 接收事件
313
313
  this._framework.emit('webhook:received', { webhook, data: webhookData, sessionId: finalSessionId })
314
314
 
315
+ // 使用子Agent处理 webhook
316
+ const webhookAgent = this._framework.createSubAgent({
317
+ name: 'webhook_handler',
318
+ role: 'Webhook处理助手,专注于处理webhook数据并生成适当响应'
319
+ })
320
+
315
321
  if (!webhook.awaitResponse) {
316
322
  // 不等待,立即返回
317
- agent.pushMessage(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`, {
318
- sessionId: finalSessionId
319
- }).then(result => {
323
+ webhookAgent.chat(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`).then(result => {
320
324
  const responseText = result.message || result.text || ''
321
325
  console.log(`[Web] Webhook processed (${webhook.path}), LLM response (${responseText.length} chars)`)
322
326
 
@@ -340,9 +344,7 @@ class WebPlugin extends Plugin {
340
344
 
341
345
  // 等待 LLM 处理完成
342
346
  try {
343
- const result = await agent.pushMessage(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`, {
344
- sessionId: finalSessionId
345
- })
347
+ const result = await webhookAgent.chat(`${prompt}\n\n数据:\n${JSON.stringify(webhookData, null, 2)}`)
346
348
  const responseText = result.message || result.text || ''
347
349
  console.log(`[Web] Webhook processed (${webhook.path}), LLM response (${responseText.length} chars)`)
348
350
 
@@ -144,7 +144,7 @@ think → think
144
144
  ```
145
145
 
146
146
  这意味着:
147
- - 变量引用语法一致:`{{variableName}}`
147
+ - 变量引用语法一致:`${variableName}`(Ambient Agent 使用 `${}`)
148
148
  - sessionId 传递机制一致
149
149
  - 错误处理方式一致
150
150
 
@@ -162,6 +162,66 @@ context.variables.loopIndex // 循环索引(如果被 loop 执行)
162
162
  context.input // 工作流/目标输入参数
163
163
  ```
164
164
 
165
+ ## 变量引用语法
166
+
167
+ Ambient Agent 使用 `${}` 语法在 action 参数中引用变量:
168
+
169
+ ### 1. 事件数据引用 `${event.xxx}`
170
+
171
+ 当 goal 监听事件时,事件数据可通过 `${event.xxx}` 直接引用:
172
+
173
+ ```javascript
174
+ {
175
+ id: 'auto-reply',
176
+ type: 'tool',
177
+ name: 'email_auto_reply',
178
+ args: {
179
+ to: '${event.from}',
180
+ subject: '${event.subject}',
181
+ body: '${event.text}',
182
+ from: ''
183
+ }
184
+ }
185
+ ```
186
+
187
+ **注意**:`email:received` 事件的数据结构:
188
+ - `event.from` - 发件人地址
189
+ - `event.subject` - 邮件主题
190
+ - `event.text` - 邮件正文(纯文本)
191
+ - `event.body` - 邮件正文(可能含 HTML)
192
+ - `event.messageId` - 邮件消息 ID
193
+
194
+ ### 2. 多步骤引用 `${actionId.output.field}`
195
+
196
+ 当有多个顺序执行的 action 时,后续 action 可以引用前一个 action 的输出:
197
+
198
+ ```javascript
199
+ actions: [
200
+ {
201
+ id: 'read-email',
202
+ type: 'tool',
203
+ name: 'email_read',
204
+ args: { limit: 1, unreadOnly: true }
205
+ },
206
+ {
207
+ id: 'process',
208
+ type: 'tool',
209
+ name: 'some_processor',
210
+ args: {
211
+ // 引用 read-email 输出的 emails 数组的第一个元素的 from 字段
212
+ sender: '${read-email.output.emails[0].from}'
213
+ }
214
+ }
215
+ ]
216
+ ```
217
+
218
+ ### 3. 变量解析规则
219
+
220
+ `${}` 语法支持嵌套属性访问:
221
+ - `${event.from}` - 直接字段
222
+ - `${actionId.output.field}` - 嵌套字段
223
+ - `${event.data.xxx}` - data 对象内的字段(兼容性别名)
224
+
165
225
  ## 创建目标的最佳实践
166
226
 
167
227
  1. **明确目标标题** - 让用户清楚知道目标意图
@@ -172,13 +232,13 @@ context.input // 工作流/目标输入参数
172
232
 
173
233
  ## 示例:创建邮件自动回复目标
174
234
 
175
- **重要**:`email:received` 事件的 `email` 对象包含:
176
- - `email.from` - 发件人地址
177
- - `email.to` - 收件人地址
178
- - `email.subject` - 邮件主题
179
- - `email.body` - 邮件正文
235
+ **重要**:`email:received` 事件的数据结构:
236
+ - `event.from` - 发件人地址
237
+ - `event.subject` - 邮件主题
238
+ - `event.text` - 邮件正文(纯文本)
239
+ - `event.messageId` - 邮件消息 ID
180
240
 
181
- 使用 `{{}}` 语法从事件中提取参数:
241
+ 使用 `${}` 语法从事件中提取参数:
182
242
 
183
243
  ```javascript
184
244
  await ambient_goals({
@@ -192,9 +252,18 @@ await ambient_goals({
192
252
  type: 'tool',
193
253
  name: 'email_auto_reply',
194
254
  args: {
195
- to: '{{_event.email.from}}',
196
- subject: '{{_event.email.subject}}',
197
- body: '{{_event.email.body}}'
255
+ to: '${event.from}',
256
+ subject: '${event.subject}',
257
+ body: '${event.text}',
258
+ from: ''
259
+ }
260
+ },
261
+ {
262
+ id: 'mark-read',
263
+ type: 'tool',
264
+ name: 'email_mark_read',
265
+ args: {
266
+ messageId: '${event.messageId}'
198
267
  }
199
268
  }
200
269
  ],
@@ -211,14 +280,15 @@ await ambient_goals({
211
280
 
212
281
  // ✅ 正确 - 从事件中提取参数
213
282
  { type: 'tool', name: 'email_auto_reply', args: {
214
- to: '{{_event.email.from}}',
215
- subject: '{{_event.email.subject}}',
216
- body: '{{_event.email.body}}'
283
+ to: '${event.from}',
284
+ subject: '${event.subject}',
285
+ body: '${event.text}',
286
+ from: ''
217
287
  }
218
288
  }
219
289
  ```
220
290
 
221
- **注意**:`email_auto_reply` 需要 `to`、`subject`、`body` 三个必需参数,必须从 `{{_event.email.xxx}}` 提取。
291
+ **注意**:`email_auto_reply` 需要 `to`、`subject`、`body` 三个必需参数,必须从 `${event.xxx}` 提取。由于 `email:received` 事件已携带邮件数据,不需要额外的 `email_read` 步骤来读取邮件。
222
292
 
223
293
  ## 生命周期
224
294