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/.claude/settings.local.json +148 -145
- package/examples/basic.js +110 -110
- package/examples/mcp-example.js +53 -53
- package/examples/skill-example.js +49 -49
- package/examples/test-mcp.js +79 -79
- package/examples/test-reload.js +61 -61
- package/news-20260329-1774794949179.html +39 -0
- package/news-20260329-1774794970785.html +39 -0
- package/news-20260329-1774797491928.html +39 -0
- package/package.json +1 -1
- package/plugins/ambient-agent-plugin.js +333 -22
- package/plugins/email.js +106 -20
- package/plugins/file-system-plugin.js +76 -30
- package/plugins/python-executor-plugin.js +41 -8
- package/plugins/scheduler-plugin.js +6 -10
- package/plugins/web-plugin.js +8 -6
- package/skills/ambient-agent/SKILL.md +84 -14
- package/skills/workflow-guide/SKILL.md +214 -2
- package/skills/workflow-troubleshooting/DEBUGGING.md +182 -0
- package/skills/workflow-troubleshooting/SKILL.md +314 -0
- package/src/capabilities/workflow-engine.js +367 -22
- package/src/core/agent-chat.js +106 -14
- package/src/core/framework.js +81 -1
- package/src/executors/executor-base.js +58 -58
- package/test-server.js +0 -25
- package/test.txt +0 -3
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 格式: {
|
|
259
|
+
// _event 格式: { event: 'email:received', data: { uid, messageId, from, to, subject, text, ... }, timestamp }
|
|
230
260
|
if (!to && !subject && !body && _event) {
|
|
231
|
-
const email = _event.
|
|
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
|
-
//
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
309
|
+
setTimeout(() => reject(new Error(`AI回复生成超时(${timeoutMs/1000}秒)`)), timeoutMs)
|
|
271
310
|
})
|
|
272
311
|
|
|
273
|
-
const replyPromise =
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
}
|
|
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:
|
|
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:
|
|
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
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
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 返回的消息
|
package/plugins/web-plugin.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
-
|
|
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`
|
|
176
|
-
- `
|
|
177
|
-
- `
|
|
178
|
-
- `
|
|
179
|
-
- `
|
|
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: '{
|
|
196
|
-
subject: '{
|
|
197
|
-
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: '{
|
|
215
|
-
subject: '{
|
|
216
|
-
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` 三个必需参数,必须从
|
|
291
|
+
**注意**:`email_auto_reply` 需要 `to`、`subject`、`body` 三个必需参数,必须从 `${event.xxx}` 提取。由于 `email:received` 事件已携带邮件数据,不需要额外的 `email_read` 步骤来读取邮件。
|
|
222
292
|
|
|
223
293
|
## 生命周期
|
|
224
294
|
|