foliko 1.1.20 → 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.
package/nul ADDED
@@ -0,0 +1,3 @@
1
+ dir: cannot access '/s': No such file or directory
2
+ dir: cannot access '/b': No such file or directory
3
+ dir: cannot access '*.png': No such file or directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -4,7 +4,6 @@
4
4
 
5
5
  const { Plugin } = require('../../src/core/plugin-base')
6
6
  const { logger } = require('../../src/utils/logger')
7
- const { Agent } = require('../../src/core/agent')
8
7
  const { StepExecutor } = require('../../src/capabilities/workflow-engine')
9
8
  const { SystemPrompts, ThinkModePrompts, FreeThinkPrompts } = require('./constants')
10
9
 
@@ -37,13 +36,19 @@ class ExplorerLoop {
37
36
  const aiPlugin = this._framework.pluginManager?.get('ai')
38
37
  const llmConfig = aiPlugin ? aiPlugin.getConfig() : {}
39
38
 
40
- this._ambientAgent = new Agent(this._framework, {
39
+ // 使用 Subagent 代替 Agent,轻量化处理后台任务
40
+ this._ambientAgent = this._framework.createSubAgent({
41
41
  name: 'ambient-worker',
42
+ role: '后台任务执行助手',
43
+ description: '执行需要 AI 能力的后台任务',
42
44
  systemPrompt: SystemPrompts.ambientWorker,
43
- model: llmConfig.model,
44
- provider: llmConfig.provider,
45
- apiKey: llmConfig.apiKey,
46
- baseURL: llmConfig.baseURL
45
+ isAll:true,
46
+ llmConfig: {
47
+ model: llmConfig.model,
48
+ provider: llmConfig.provider,
49
+ apiKey: llmConfig.apiKey,
50
+ baseURL: llmConfig.baseURL
51
+ }
47
52
  })
48
53
 
49
54
  // 注册到主 agent(如果主 agent 已存在)
@@ -451,10 +456,22 @@ class ExplorerLoop {
451
456
  if (typeof value === 'string') {
452
457
  // 处理 ${actionId.output} 和 ${actionId.output.field} 格式
453
458
  const result = value.replace(/\$\{([^}]+)\}/g, (match, path) => {
459
+ // 特殊处理:${result.xxx} 引用上一步结果中的字段
460
+ if (path.startsWith('result.')) {
461
+ const fieldPath = path.replace(/^result\./, '')
462
+ const lastResult = stepOutputs._lastResult
463
+ if (lastResult) {
464
+ const val = this._getNestedValue(lastResult, fieldPath)
465
+ if (val !== undefined) return val
466
+ }
467
+ return match
468
+ }
469
+
454
470
  if (path === 'output' || path === 'result') {
455
- // {{result}} 引用上一步结果
471
+ // ${result} 引用上一步结果
456
472
  return stepOutputs._lastResult ?? match
457
473
  }
474
+
458
475
  if (path.startsWith('event.')) {
459
476
  // ${event.xxx} 引用事件数据
460
477
  let eventPath = path.replace(/^event\./, '')
@@ -478,10 +495,12 @@ class ExplorerLoop {
478
495
  }
479
496
  return match
480
497
  }
498
+
481
499
  if (path === 'event') {
482
500
  // ${event} 引用整个事件数据对象
483
501
  return eventData ?? match
484
502
  }
503
+
485
504
  if (path.includes('.')) {
486
505
  // ${actionId.output.field} 格式
487
506
  const [actionId, ...fieldParts] = path.split('.')
@@ -526,6 +545,66 @@ class ExplorerLoop {
526
545
  return current
527
546
  }
528
547
 
548
+ /**
549
+ * 判断 LLM 回复是否应该发送通知
550
+ * 识别模式:
551
+ * - 包含 "【通知】" 或 "[通知]" 开头的回复
552
+ * - JSON 中包含 notification_send 调用
553
+ * - 包含 "已发送通知"、"发送通知" 等关键词
554
+ */
555
+ _shouldSendNotification(resultText) {
556
+ if (!resultText) return false
557
+
558
+ // 检查通知标记
559
+ const notifyPatterns = [
560
+ /【通知】/,
561
+ /\[通知\]/,
562
+ /notification_send/,
563
+ /已发送通知/,
564
+ /发送通知成功/,
565
+ /通知已发送/,
566
+ /【提醒】/,
567
+ /\[提醒\]/,
568
+ /⚠️|🚨|🔔|📢/ // 常见通知 emoji
569
+ ]
570
+
571
+ return notifyPatterns.some(pattern => pattern.test(resultText))
572
+ }
573
+
574
+ /**
575
+ * 从 LLM 回复中提取通知内容
576
+ */
577
+ _extractNotification(resultText) {
578
+ if (!resultText) return null
579
+
580
+ // 尝试提取 【通知】... 内容
581
+ const zhMatch = resultText.match(/【通知】([\s\S]*?)(?:```|$)/)
582
+ if (zhMatch) {
583
+ const content = zhMatch[1].trim()
584
+ // 尝试分离标题和内容
585
+ const lines = content.split('\n').filter(l => l.trim())
586
+ if (lines.length >= 2) {
587
+ return { title: lines[0], message: lines.slice(1).join('\n') }
588
+ } else if (lines.length === 1) {
589
+ return { title: '通知提醒', message: lines[0] }
590
+ }
591
+ }
592
+
593
+ // 尝试提取 [通知]... 内容
594
+ const enMatch = resultText.match(/\[通知\]([\s\S]*?)(?:```|$)/)
595
+ if (enMatch) {
596
+ const content = enMatch[1].trim()
597
+ const lines = content.split('\n').filter(l => l.trim())
598
+ if (lines.length >= 1) {
599
+ return { title: lines[0], message: lines.slice(1).join('\n') || lines[0] }
600
+ }
601
+ }
602
+
603
+ // 默认:从结果中提取前100字符作为消息
604
+ const shortContent = resultText.substring(0, 200).replace(/```[\s\S]*?```/g, '').trim()
605
+ return { title: 'Ambient Agent 通知', message: shortContent }
606
+ }
607
+
529
608
  /**
530
609
  * 按顺序执行所有 actions
531
610
  * @param {Array} actions - action 数组
@@ -607,9 +686,36 @@ class ExplorerLoop {
607
686
  content = `${content}\n\n[事件上下文: ${JSON.stringify(context.variables._event)}]`
608
687
  }
609
688
 
689
+ // 添加通知能力提示
690
+ const notifyPrompt = `\n\n## 通知能力\n如果你需要发送通知,可以使用 notification_send 工具:\n- plugin: "notification"\n- tool: "notification_send" \n- args: { "title": "通知标题", "message": "通知内容" }\n\n**重要**:当判断结果满足条件时,你应该调用 notification_send 发送通知。不满足条件时,静默结束即可。`
691
+
610
692
  try {
611
- const result = await this._ambientAgent.pushMessage(content)
612
- return { success: true, result }
693
+ const result = await this._ambientAgent.chat(content + notifyPrompt, { maxRetries: 2 })
694
+
695
+ // Subagent 返回 { success, message, steps, error }
696
+ if (!result.success) {
697
+ return { success: false, error: result.error }
698
+ }
699
+
700
+ const resultText = result.message || ''
701
+
702
+ // 如果 result 中包含通知相关的内容,自动发送通知
703
+ if (this._shouldSendNotification(resultText)) {
704
+ const notification = this._extractNotification(resultText)
705
+ if (notification) {
706
+ // 通过事件系统发送通知
707
+ this._framework.emit('notification', {
708
+ title: notification.title || 'Ambient Agent 通知',
709
+ message: notification.message,
710
+ source: 'ambient',
711
+ level: 'info',
712
+ timestamp: new Date()
713
+ })
714
+ return { success: true, result: resultText, notified: true, notification }
715
+ }
716
+ }
717
+
718
+ return { success: true, result: resultText }
613
719
  } catch (err) {
614
720
  return { success: false, error: err.message }
615
721
  }
@@ -701,8 +807,12 @@ ${extensionsInfo}
701
807
  请直接返回 JSON 代码块。`
702
808
 
703
809
  try {
704
- const response = await this._ambientAgent.pushMessage(prompt)
705
- const responseText = typeof response === 'string' ? response : JSON.stringify(response)
810
+ const result = await this._ambientAgent.chat(prompt, { maxRetries: 2 })
811
+ if (!result.success) {
812
+ log.warn(`[ExplorerLoop] Subagent 调用失败: ${result.error}`)
813
+ return { success: false, error: result.error }
814
+ }
815
+ const responseText = result.message || ''
706
816
 
707
817
  // 尝试解析 LLM 返回的 JSON
708
818
  let toolChoice = null
@@ -183,7 +183,7 @@ class GoalManager {
183
183
  e.event === event.type && (now - new Date(e.timestamp).getTime()) < 2000
184
184
  )
185
185
  if (recentSameEvent) {
186
- log.info(`跳过重复事件: ${event.type} (2秒内不重复添加)`)
186
+ //log.info(`跳过重复事件: ${event.type} (2秒内不重复添加)`)
187
187
  return
188
188
  }
189
189
 
@@ -136,7 +136,7 @@ class ExtensionExecutorPlugin extends Plugin {
136
136
  }),
137
137
  execute: async (args) => {
138
138
  const { plugin, tool, args: toolArgs = {} } = args;
139
- log.info(` ext_call: plugin=${plugin}, tool=${tool}, args=${JSON.stringify(toolArgs)}`);
139
+ log.info(`[Extension] ext_call: plugin=${plugin}, tool=${tool}`);
140
140
 
141
141
  // MCP 服务器工具(已注册为 server_toolname 格式,如 github_search)
142
142
  if (plugin === 'mcp' && this._mcpExecutor) {
@@ -463,8 +463,11 @@ class WeixinPlugin extends Plugin {
463
463
  [用户发送了${type_dict[msg.type]}消息]\n
464
464
  文件列表:\n
465
465
  ${JSON.stringify(files,null,2)} \n
466
-
467
- *重要:* 不要试图读取文件内容!
466
+
467
+ **重要提醒:**
468
+ - 禁止读取任何文件内容!
469
+ - 禁止调用任何读取工具!
470
+ - 只告知用户文件保存路径
468
471
  `
469
472
  , msg)
470
473
  break
@@ -606,6 +606,8 @@ class AgentChatHandler extends EventEmitter {
606
606
  */
607
607
  async _prepareSession(message, sessionId) {
608
608
  const messageStore = this._getSessionMessageStore(sessionId);
609
+ // 先加载 session 文件中的历史消息
610
+ this._chatSession.loadHistory(sessionId);
609
611
  const messages = messageStore.messages;
610
612
 
611
613
  // 刷新系统提示词
@@ -621,11 +623,17 @@ class AgentChatHandler extends EventEmitter {
621
623
  const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
622
624
  const limit = this._maxContextTokens * 0.5;
623
625
 
626
+ console.log(
627
+ `[_prepareSession] BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}`
628
+ );
629
+
624
630
  if (totalTokens > limit) {
631
+ console.log(`[_prepareSession] Compressing context...`);
625
632
  logger.info(
626
633
  `Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
627
634
  );
628
635
  await this._compressContext(sessionId, messages, messageStore);
636
+ console.log(`[_prepareSession] AFTER compress: messages=${messages.length}`);
629
637
  }
630
638
 
631
639
  // 最终 token 检查
@@ -743,26 +751,31 @@ class AgentChatHandler extends EventEmitter {
743
751
  try {
744
752
  const tokenCount = this._countMessagesTokens(inputMessages);
745
753
  const tokenLimit = this._maxContextTokens * 0.75;
746
- logger.debug(`prepareStep: messages=${inputMessages.length}, tokens=${tokenCount}`);
747
754
 
755
+ // 超过限制时,保留三分之一的消息
748
756
  if (tokenCount > tokenLimit) {
749
- logger.info(
750
- `Context compress triggered: messages=${inputMessages.length}, tokens=${tokenCount}/${tokenLimit}`
757
+ const keepCount = Math.max(10, Math.floor(inputMessages.length / 3));
758
+ console.log(
759
+ `[PrepareStep] Trimming: ${inputMessages.length} msgs -> ${keepCount}, tokens=${tokenCount}`
751
760
  );
752
- const pairedMessages = await this._compressContextForPrepare(inputMessages);
753
- if (pairedMessages.length !== inputMessages.length) {
754
- logger.info(`Compressed ${inputMessages.length} -> ${pairedMessages.length} messages`);
755
- inputMessages.length = 0;
756
- inputMessages.push(...pairedMessages);
757
- }
761
+ const trimmed = inputMessages.slice(-keepCount);
762
+ inputMessages.length = 0;
763
+ inputMessages.push(...trimmed);
758
764
  }
759
765
 
766
+ // 验证 tool-call 和 tool-result 配对
760
767
  const validated = this._validateMessagesPairing(inputMessages);
761
768
  if (validated.length !== inputMessages.length) {
769
+ console.log(
770
+ `[PrepareStep] After pairing validation: ${inputMessages.length} -> ${validated.length}`
771
+ );
762
772
  inputMessages.length = 0;
763
773
  inputMessages.push(...validated);
764
774
  }
775
+
776
+ // 验证 tool-call 格式
765
777
  this._validateToolCalls(inputMessages);
778
+
766
779
  return { messages: inputMessages };
767
780
  } catch (err) {
768
781
  logger.error('prepareStep error:', err.message, err.stack);
@@ -212,9 +212,14 @@ class ChatSession extends EventEmitter {
212
212
  return messageStore.messages;
213
213
  }
214
214
 
215
- // SessionContext 加载
215
+ // 确保 SessionContext 已初始化(会从文件加载或创建新的)
216
216
  if (this.agent?.framework) {
217
- const sessionCtx = this.agent.framework._sessionContexts?.get(sessionId);
217
+ // 先尝试获取已存在的 SessionContext
218
+ let sessionCtx = this.agent.framework._sessionContexts?.get(sessionId);
219
+ // 如果不存在,调用 getOrCreateSessionContext 来加载或创建
220
+ if (!sessionCtx) {
221
+ sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);
222
+ }
218
223
  if (sessionCtx) {
219
224
  const messages = sessionCtx.getMessages();
220
225
  if (messages && messages.length > 0) {
@@ -15,7 +15,7 @@ const MODEL_CONTEXT_LIMITS = {
15
15
  'deepseek-chat': 128000,
16
16
  'deepseek-coder': 128000,
17
17
  'deepseek-reasoner': 128000,
18
- 'MiniMax-M2.7': 110000,
18
+ 'MiniMax-M2.7': 128000,
19
19
  'gpt-4': 100000,
20
20
  'gpt-4o': 100000,
21
21
  'gpt-4o-mini': 100000,
@@ -136,14 +136,14 @@ class ContextCompressor {
136
136
  const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);
137
137
 
138
138
  const compressedCount = messagesToSummarize.length;
139
+
139
140
  let summaryContent = '';
140
141
 
141
142
  // 使用 AI 生成摘要
142
143
  if (this._enableSmartCompress && this.agent?._chatHandler?._aiClient) {
143
144
  try {
144
145
  const summaryText = await this._summarizeMessages(messagesToSummarize);
145
- summaryContent = `[早期对话摘要]: ${summaryText}`;
146
- logger.info(`AI summary generated (${summaryText.length} chars)`);
146
+ summaryContent = `[早期对话摘要]: ${summaryText || '(无内容)'}`;
147
147
  } catch (err) {
148
148
  logger.warn('AI summary failed, using simple compression:', err.message);
149
149
  summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;
@@ -409,23 +409,36 @@ class ContextCompressor {
409
409
  * @private
410
410
  */
411
411
  async _summarizeMessages(messages) {
412
- if (!this.agent?._chatHandler?._aiClient) {
412
+ const chatHandler = this.agent?._chatHandler;
413
+ if (!chatHandler?._aiClient) {
413
414
  throw new Error('AI client not available');
414
415
  }
415
416
 
416
417
  const { generateText } = await import('ai');
417
418
 
418
- const prompt = `请简要总结以下对话的主要内容,用 100 字以内描述:
419
-
420
- ${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n')}
421
-
422
- 摘要:`;
419
+ // 使用 AI SDK 消息格式
420
+ const chatMessages = [
421
+ {
422
+ role: 'user',
423
+ content:
424
+ '请简要总结以下对话的主要内容,用1000字以内描述:\n' +
425
+ messages
426
+ .map((m) => {
427
+ const content =
428
+ typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
429
+ return `${m.role}: ${content}`;
430
+ })
431
+ .join('\n'),
432
+ },
433
+ ];
423
434
 
424
435
  try {
436
+ // _aiClient 已经是 model 对象,直接使用
437
+ const model = chatHandler._aiClient;
425
438
  const result = await generateText({
426
- model: this.agent._chatHandler._aiClient,
427
- prompt: prompt,
428
- maxTokens: 200,
439
+ model: model,
440
+ messages: chatMessages,
441
+ maxTokens: 1000,
429
442
  });
430
443
  return result.text || '';
431
444
  } catch (err) {
@@ -458,6 +471,10 @@ ${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content :
458
471
 
459
472
  let total = 4; // 角色开销
460
473
 
474
+ if (!msg.content) {
475
+ return total;
476
+ }
477
+
461
478
  if (typeof msg.content === 'string') {
462
479
  total += Math.ceil(Buffer.byteLength(msg.content, 'utf8') / 4);
463
480
  } else if (Array.isArray(msg.content)) {
@@ -470,7 +487,11 @@ ${messages.map((m) => `${m.role}: ${typeof m.content === 'string' ? m.content :
470
487
  }
471
488
  } else if (block.type === 'tool-result' || block.type === 'tool_result') {
472
489
  const content =
473
- typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
490
+ block.content == null
491
+ ? ''
492
+ : typeof block.content === 'string'
493
+ ? block.content
494
+ : JSON.stringify(block.content);
474
495
  total += Math.ceil(Buffer.byteLength(content, 'utf8') / 4);
475
496
  }
476
497
  }
@@ -50,7 +50,7 @@ class Subagent extends EventEmitter {
50
50
  };
51
51
  // 如果提供了 systemPrompt 则使用,否则标记为需要动态构建
52
52
  this._customSystemPrompt = config.systemPrompt || null;
53
- this.parentTools = config?.parentTools || [];
53
+ this.parentTools = config?.parentTools;
54
54
  // 工具管理
55
55
  this._tools = new Map();
56
56
  this._registerTools(config.tools || []);
@@ -121,20 +121,31 @@ class Subagent extends EventEmitter {
121
121
  const tools = {};
122
122
  // 从父Agent继承工具
123
123
  const all_tools = this.framework.getTools();
124
- this.parentTools = this.parentTools.map((key) => {
125
- return this.bindTools[key.toLocaleLowerCase()] || key;
126
- });
127
- for (const toolName of this.parentTools) {
128
- const toolDef = all_tools.find((t) => t.name === toolName);
129
- if (toolDef) {
130
- 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
+ }
131
146
  }
132
147
  }
133
- const defaulTools = all_tools.filter((a) => this.defaulTools.includes(a.name));
134
148
 
135
- defaulTools.map((tool) => {
136
- tools[tool.name] = tool;
137
- });
138
149
  return { ...tools, ...this._tools };
139
150
  }
140
151
 
@@ -119,7 +119,12 @@ class ChatQueueManager extends EventEmitter {
119
119
 
120
120
  // 检查是否有错误(通过返回的 result.error)
121
121
  if (result.error) {
122
- console.log('[ChatQueue] executeWithRetry: attempt', attempt, 'got error:', result.error.message);
122
+ console.log(
123
+ '[ChatQueue] executeWithRetry: attempt',
124
+ attempt,
125
+ 'got error:',
126
+ result.error.message
127
+ );
123
128
  lastError = result.error;
124
129
  if (attempt < this.retryAttempts && this.isRetryableError(lastError)) {
125
130
  await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
@@ -132,7 +137,12 @@ class ChatQueueManager extends EventEmitter {
132
137
 
133
138
  return result;
134
139
  } catch (error) {
135
- console.log('[ChatQueue] executeWithRetry: attempt', attempt, 'threw error:', error.message);
140
+ console.log(
141
+ '[ChatQueue] executeWithRetry: attempt',
142
+ attempt,
143
+ 'threw error:',
144
+ error.message
145
+ );
136
146
  lastError = error;
137
147
  if (attempt < this.retryAttempts && this.isRetryableError(error)) {
138
148
  await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
@@ -186,7 +196,10 @@ class ChatQueueManager extends EventEmitter {
186
196
  ? 'AI 服务暂时不可用,请稍后重试'
187
197
  : (err.message || err.toString()).split('\n')[0];
188
198
 
189
- console.log('[ChatQueue] executeStream caught error, converting to friendly:', friendlyMessage);
199
+ console.log(
200
+ '[ChatQueue] executeStream caught error, converting to friendly:',
201
+ friendlyMessage
202
+ );
190
203
  chunks.push({ type: 'error', error: friendlyMessage });
191
204
  this.emit('stream:chunk', {
192
205
  requestId: item.id,
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
  - 用户海报风格偏好: ## 用户海报设计风格偏好