@chbo297/infoflow 2026.3.6 → 2026.3.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.8
4
+
5
+ ### 新功能
6
+
7
+ #### watchRegex(正则匹配群消息)
8
+
9
+ - 支持 `watchRegex` 配置:按正则表达式匹配群内聊天内容,命中时触发机器人参与并回复
10
+ - 可在顶层、账号级别或按群(`groups.<groupId>.watchRegex`)单独配置
11
+ - 需配合 `replyMode` 为 `mention-and-watch` 或 `proactive` 使用
12
+
13
+ #### 撤回消息
14
+
15
+ - 支持私聊消息撤回能力,需在配置中填写如流企业后台的 `appAgentId`(应用 ID)
16
+
17
+ ### Bug 修复
18
+
19
+ - 修复了一些消息回复失败的问题
20
+
21
+ ---
22
+
3
23
  ## 2026.2.28
4
24
 
5
25
  ### 新功能
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - **私聊 & 群聊**消息接收与回复
14
14
  - 群内 **@机器人** 检测,被 @提及 时自动回复
15
15
  - **watchMentions(关注提及)**:监控指定人员被 @ 时,机器人作为其助手判断是否代为回复
16
+ - **watchRegex(正则匹配)**:按正则匹配群内聊天内容,命中时触发机器人回复
16
17
  - **followUp(跟进回复)**:机器人回复后,在时间窗口内智能判断后续消息是否为追问,无需再次 @
17
18
  - 五种 **replyMode(回复模式)**:从完全忽略到主动参与,灵活控制群内行为
18
19
  - **按群独立配置**:每个群可设置不同的回复策略和系统提示词
@@ -49,7 +50,7 @@ openclaw plugins install ./path/to/openclaw-infoflow
49
50
  encodingAESKey: "your-encoding-aes-key",
50
51
  appKey: "your-app-key",
51
52
  appSecret: "your-app-secret",
52
- robotName: "MyBot", // 必填:用于群内 @提及 检测
53
+ robotName: "MyBot", // 用于群内 @提及 检测
53
54
  },
54
55
  },
55
56
  }
@@ -99,6 +100,27 @@ https://your-domain/webhook/infoflow
99
100
  - 如果有把握 → 直接回复
100
101
  - 如果无法帮助 → 静默不回复
101
102
 
103
+ ## 正则匹配 (watchRegex)
104
+
105
+ 通过正则表达式匹配群内聊天内容,当消息文本命中正则时,会触发机器人参与并回复(需配合 `replyMode` 为 `mention-and-watch` 或 `proactive`)。可在顶层、账号或按群单独配置。
106
+
107
+ ```json5
108
+ {
109
+ channels: {
110
+ infoflow: {
111
+ watchRegex: "^(帮忙|请帮我|求助)", // 顶层:匹配以这些词开头的消息
112
+ groups: {
113
+ "123456": {
114
+ watchRegex: "\\?$|怎么|如何", // 该群:匹配以问号结尾或含「怎么」「如何」的消息
115
+ },
116
+ },
117
+ },
118
+ },
119
+ }
120
+ ```
121
+
122
+ **说明**:正则采用 JavaScript 标准语法;与 watchMentions、@提及 等条件并列,任一满足即可触发回复判断。
123
+
102
124
  ## 跟进回复 (followUp)
103
125
 
104
126
  机器人回复后,在 `followUpWindow` 时间窗口内(默认 300 秒),后续消息即使没有 @机器人 也会触发智能判断:
@@ -130,6 +152,7 @@ https://your-domain/webhook/infoflow
130
152
  "123456": {
131
153
  replyMode: "mention-and-watch",
132
154
  watchMentions: ["team-lead01"],
155
+ watchRegex: "^(帮忙|求助)",
133
156
  followUp: true,
134
157
  followUpWindow: 600,
135
158
  systemPrompt: "你是这个项目组的技术助手。",
@@ -206,10 +229,12 @@ https://your-domain/webhook/infoflow
206
229
  | `appKey` | `string` | — | 应用 Key **(必填)** |
207
230
  | `appSecret` | `string` | — | 应用 Secret **(必填)** |
208
231
  | `robotName` | `string` | — | 机器人名称,用于 @提及 检测 |
232
+ | `appAgentId` | `number` | — | 如流企业后台的应用 ID,私聊消息撤回依赖此字段 |
209
233
  | `replyMode` | `string` | `"mention-and-watch"` | 回复模式 |
210
234
  | `followUp` | `boolean` | `true` | 是否启用跟进回复 |
211
235
  | `followUpWindow` | `number` | `300` | 跟进窗口(秒) |
212
236
  | `watchMentions` | `string[]` | `[]` | 关注提及的人员列表 |
237
+ | `watchRegex` | `string` | — | 正则表达式,匹配群消息内容时触发回复 |
213
238
  | `dmPolicy` | `string` | `"open"` | 私聊策略 |
214
239
  | `allowFrom` | `string[]` | `[]` | 私聊白名单 |
215
240
  | `groupPolicy` | `string` | `"open"` | 群聊策略 |
@@ -224,6 +249,7 @@ https://your-domain/webhook/infoflow
224
249
  |------|------|------|
225
250
  | `replyMode` | `string` | 覆盖该群的回复模式 |
226
251
  | `watchMentions` | `string[]` | 覆盖该群的关注列表 |
252
+ | `watchRegex` | `string` | 覆盖该群的正则匹配规则,匹配群消息内容时触发回复 |
227
253
  | `followUp` | `boolean` | 覆盖该群的跟进开关 |
228
254
  | `followUpWindow` | `number` | 覆盖该群的跟进窗口 |
229
255
  | `systemPrompt` | `string` | 该群专属系统提示词 |
@@ -257,6 +283,7 @@ Baidu Infoflow (如流) enterprise messaging platform — OpenClaw channel plugi
257
283
  - **Direct & group** message receiving and replying
258
284
  - **@mention detection** in groups — auto-reply when the bot is @mentioned
259
285
  - **watchMentions**: monitor specified people; when they are @mentioned, the bot acts as their assistant and decides whether to reply on their behalf
286
+ - **watchRegex**: match group chat content by regex; when a message matches, trigger the bot to reply
260
287
  - **followUp**: after the bot replies, intelligently judge whether subsequent messages are follow-up questions within a time window — no need to @mention again
261
288
  - Five **replyMode** levels: from fully ignoring to proactively engaging, flexibly control group behavior
262
289
  - **Per-group config**: each group can have its own reply strategy and system prompt
@@ -343,6 +370,27 @@ Configure a list of people to watch. When someone in the group @mentions a perso
343
370
  - Confident it can help → replies directly
344
371
  - Cannot help → stays silent (NO_REPLY)
345
372
 
373
+ ## Regex Match (watchRegex)
374
+
375
+ Match group chat content with a regular expression; when a message matches, the bot is triggered to participate and reply (requires `replyMode` `mention-and-watch` or `proactive`). Can be set at top level, per account, or per group.
376
+
377
+ ```json5
378
+ {
379
+ channels: {
380
+ infoflow: {
381
+ watchRegex: "^(help|please|urgent)", // Top-level: match messages starting with these
382
+ groups: {
383
+ "123456": {
384
+ watchRegex: "\\?$|how to|what is", // This group: match messages ending with ? or containing "how to"/"what is"
385
+ },
386
+ },
387
+ },
388
+ },
389
+ }
390
+ ```
391
+
392
+ **Note**: Regex uses standard JavaScript syntax. It works alongside watchMentions and @mention; any condition can trigger reply evaluation.
393
+
346
394
  ## Follow-Up (followUp)
347
395
 
348
396
  After the bot replies, any subsequent message within the `followUpWindow` (default 300 seconds) triggers intelligent judgment — even without @mentioning the bot:
@@ -374,6 +422,7 @@ Set independent reply strategies for each group, overriding the global defaults.
374
422
  "123456": {
375
423
  replyMode: "mention-and-watch",
376
424
  watchMentions: ["team-lead01"],
425
+ watchRegex: "^(help|urgent)",
377
426
  followUp: true,
378
427
  followUpWindow: 600,
379
428
  systemPrompt: "You are the tech assistant for this project team.",
@@ -450,10 +499,12 @@ Set independent reply strategies for each group, overriding the global defaults.
450
499
  | `appKey` | `string` | — | Application key **(required)** |
451
500
  | `appSecret` | `string` | — | Application secret **(required)** |
452
501
  | `robotName` | `string` | — | Bot name for @mention detection |
502
+ | `appAgentId` | `number` | — | Infoflow app ID (enterprise console); required for DM message recall |
453
503
  | `replyMode` | `string` | `"mention-and-watch"` | Reply mode |
454
504
  | `followUp` | `boolean` | `true` | Enable follow-up replies |
455
505
  | `followUpWindow` | `number` | `300` | Follow-up window (seconds) |
456
506
  | `watchMentions` | `string[]` | `[]` | List of people to watch for @mentions |
507
+ | `watchRegex` | `string` | — | Regex to match group message content; when matched, trigger reply |
457
508
  | `dmPolicy` | `string` | `"open"` | DM access policy |
458
509
  | `allowFrom` | `string[]` | `[]` | DM allowlist |
459
510
  | `groupPolicy` | `string` | `"open"` | Group access policy |
@@ -468,6 +519,7 @@ Set independent reply strategies for each group, overriding the global defaults.
468
519
  |-------|------|-------------|
469
520
  | `replyMode` | `string` | Override reply mode for this group |
470
521
  | `watchMentions` | `string[]` | Override watch list for this group |
522
+ | `watchRegex` | `string` | Override regex for this group; match group content to trigger reply |
471
523
  | `followUp` | `boolean` | Override follow-up toggle for this group |
472
524
  | `followUpWindow` | `number` | Override follow-up window for this group |
473
525
  | `systemPrompt` | `string` | Custom system prompt for this group |
@@ -30,7 +30,7 @@
30
30
  "followUp": { "type": "boolean", "default": true },
31
31
  "followUpWindow": { "type": "number", "default": 300 },
32
32
  "watchMentions": { "type": "array", "items": { "type": "string" } },
33
- "thinkingIndicator": { "type": "boolean", "default": true },
33
+ "watchRegex": { "type": "string" },
34
34
  "groups": {
35
35
  "type": "object",
36
36
  "additionalProperties": {
@@ -41,10 +41,10 @@
41
41
  "enum": ["ignore", "record", "mention-only", "mention-and-watch", "proactive"]
42
42
  },
43
43
  "watchMentions": { "type": "array", "items": { "type": "string" } },
44
+ "watchRegex": { "type": "string" },
44
45
  "followUp": { "type": "boolean" },
45
46
  "followUpWindow": { "type": "number" },
46
- "systemPrompt": { "type": "string" },
47
- "thinkingIndicator": { "type": "boolean" }
47
+ "systemPrompt": { "type": "string" }
48
48
  },
49
49
  "additionalProperties": false
50
50
  }
@@ -77,7 +77,7 @@
77
77
  "followUp": { "type": "boolean" },
78
78
  "followUpWindow": { "type": "number" },
79
79
  "watchMentions": { "type": "array", "items": { "type": "string" } },
80
- "thinkingIndicator": { "type": "boolean" },
80
+ "watchRegex": { "type": "string" },
81
81
  "groups": {
82
82
  "type": "object",
83
83
  "additionalProperties": {
@@ -88,10 +88,10 @@
88
88
  "enum": ["ignore", "record", "mention-only", "mention-and-watch", "proactive"]
89
89
  },
90
90
  "watchMentions": { "type": "array", "items": { "type": "string" } },
91
+ "watchRegex": { "type": "string" },
91
92
  "followUp": { "type": "boolean" },
92
93
  "followUpWindow": { "type": "number" },
93
- "systemPrompt": { "type": "string" },
94
- "thinkingIndicator": { "type": "boolean" }
94
+ "systemPrompt": { "type": "string" }
95
95
  },
96
96
  "additionalProperties": false
97
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chbo297/infoflow",
3
- "version": "2026.3.6",
3
+ "version": "2026.3.8",
4
4
  "description": "OpenClaw Infoflow (如流) channel plugin for Baidu enterprise messaging",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/accounts.ts CHANGED
@@ -71,6 +71,7 @@ function mergeInfoflowAccountConfig(
71
71
  robotName?: string;
72
72
  requireMention?: boolean;
73
73
  watchMentions?: string[];
74
+ watchRegex?: string;
74
75
  appAgentId?: number;
75
76
  } {
76
77
  const raw = getChannelSection(cfg) ?? {};
@@ -87,6 +88,7 @@ function mergeInfoflowAccountConfig(
87
88
  robotName?: string;
88
89
  requireMention?: boolean;
89
90
  watchMentions?: string[];
91
+ watchRegex?: string;
90
92
  appAgentId?: number;
91
93
  };
92
94
  }
@@ -131,6 +133,7 @@ export function resolveInfoflowAccount(params: {
131
133
  robotName: merged.robotName?.trim() || undefined,
132
134
  requireMention: merged.requireMention,
133
135
  watchMentions: merged.watchMentions,
136
+ watchRegex: merged.watchRegex,
134
137
  appAgentId: merged.appAgentId,
135
138
  },
136
139
  };
package/src/actions.ts CHANGED
@@ -22,6 +22,12 @@ import {
22
22
  import { normalizeInfoflowTarget } from "./targets.js";
23
23
  import type { InfoflowMessageContentItem, InfoflowOutboundReply } from "./types.js";
24
24
 
25
+ // Recall result hint constants — reused across single/batch, group/private recall paths
26
+ const RECALL_OK_HINT = "Recall succeeded. output only NO_REPLY with no other text.";
27
+ const RECALL_FAIL_HINT = "Recall failed. Send a brief reply stating only the failure reason.";
28
+ const RECALL_PARTIAL_HINT =
29
+ "Some recalls failed. Send a brief reply stating only the failure reason(s).";
30
+
25
31
  export const infoflowMessageActions: ChannelMessageActionAdapter = {
26
32
  listActions: (): ChannelMessageActionName[] => ["send", "delete"],
27
33
 
@@ -91,9 +97,7 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
91
97
  channel: "infoflow",
92
98
  to,
93
99
  ...(result.error ? { error: result.error } : {}),
94
- _hint: result.ok
95
- ? "Recall succeeded. Do NOT send any follow-up reply message to the user."
96
- : "Recall failed. Send a brief reply stating only the failure reason.",
100
+ _hint: result.ok ? RECALL_OK_HINT : RECALL_FAIL_HINT,
97
101
  });
98
102
  }
99
103
 
@@ -171,10 +175,7 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
171
175
  failed,
172
176
  total: recallable.length,
173
177
  details,
174
- _hint:
175
- failed === 0
176
- ? "Recall succeeded. Do NOT send any follow-up reply message to the user."
177
- : "Some recalls failed. Send a brief reply stating only the failure reason(s).",
178
+ _hint: failed === 0 ? RECALL_OK_HINT : RECALL_PARTIAL_HINT,
178
179
  });
179
180
  }
180
181
  } else {
@@ -210,9 +211,7 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
210
211
  channel: "infoflow",
211
212
  to,
212
213
  ...(result.error ? { error: result.error } : {}),
213
- _hint: result.ok
214
- ? "Recall succeeded. Do NOT send any follow-up reply message to the user."
215
- : "Recall failed. Send a brief reply stating only the failure reason.",
214
+ _hint: result.ok ? RECALL_OK_HINT : RECALL_FAIL_HINT,
216
215
  });
217
216
  }
218
217
 
@@ -286,10 +285,7 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
286
285
  failed,
287
286
  total: recallable.length,
288
287
  details,
289
- _hint:
290
- failed === 0
291
- ? "Recall succeeded. Do NOT send any follow-up reply message to the user."
292
- : "Some recalls failed. Send a brief reply stating only the failure reason(s).",
288
+ _hint: failed === 0 ? RECALL_OK_HINT : RECALL_PARTIAL_HINT,
293
289
  });
294
290
  }
295
291
  }
package/src/bot.ts CHANGED
@@ -5,23 +5,15 @@ import {
5
5
  type HistoryEntry,
6
6
  recordPendingHistoryEntryIfEnabled,
7
7
  buildAgentMediaPayload,
8
- type OpenClawConfig,
9
- type ReplyPayload,
10
8
  } from "openclaw/plugin-sdk";
11
9
  import { resolveInfoflowAccount } from "./accounts.js";
12
10
  import { getInfoflowBotLog, formatInfoflowError, logVerbose } from "./logging.js";
13
11
  import { createInfoflowReplyDispatcher } from "./reply-dispatcher.js";
14
12
  import { getInfoflowRuntime } from "./runtime.js";
15
- import {
16
- sendInfoflowMessage,
17
- recallInfoflowGroupMessage,
18
- recallInfoflowPrivateMessage,
19
- } from "./send.js";
20
13
  import type {
21
14
  InfoflowChatType,
22
15
  InfoflowMessageEvent,
23
16
  InfoflowMentionIds,
24
- InfoflowOutboundReply,
25
17
  InfoflowReplyMode,
26
18
  InfoflowGroupConfig,
27
19
  HandleInfoflowMessageParams,
@@ -111,6 +103,15 @@ function checkWatchMentioned(
111
103
  return undefined;
112
104
  }
113
105
 
106
+ /** Check if message content matches the configured watchRegex regex pattern */
107
+ function checkWatchRegex(mes: string, pattern: string): boolean {
108
+ try {
109
+ return new RegExp(pattern, "i").test(mes);
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
114
115
  /**
115
116
  * Extract non-bot mention IDs from inbound group message body items.
116
117
  * Returns human userIds and robot agentIds (excluding the bot itself, matched by robotName).
@@ -202,6 +203,19 @@ function buildWatchMentionPrompt(mentionedId: string): string {
202
203
  ].join("\n");
203
204
  }
204
205
 
206
+ /**
207
+ * Build a GroupSystemPrompt for watch-content triggered messages.
208
+ * Instructs the agent to reply only when confident, otherwise use NO_REPLY.
209
+ */
210
+ function buildWatchRegexPrompt(pattern: string): string {
211
+ return [
212
+ `The message content matched the configured watch pattern (${pattern}).`,
213
+ "As the group assistant, you observed this message. Decide whether you can provide help or a valuable reply.",
214
+ "",
215
+ buildReplyJudgmentRules(),
216
+ ].join("\n");
217
+ }
218
+
205
219
  /**
206
220
  * Build a GroupSystemPrompt for follow-up replies after bot's last response.
207
221
  * Instructs the agent to reply only if the message is a follow-up on the same topic.
@@ -261,8 +275,8 @@ type ResolvedGroupConfig = {
261
275
  followUp: boolean;
262
276
  followUpWindow: number;
263
277
  watchMentions: string[];
278
+ watchRegex?: string;
264
279
  systemPrompt?: string;
265
- thinkingIndicator: boolean;
266
280
  };
267
281
 
268
282
  /** Infer replyMode from legacy requireMention + watchMentions fields */
@@ -286,105 +300,11 @@ function resolveGroupConfig(
286
300
  followUp: groupCfg?.followUp ?? account.config.followUp ?? true,
287
301
  followUpWindow: groupCfg?.followUpWindow ?? account.config.followUpWindow ?? 300,
288
302
  watchMentions: groupCfg?.watchMentions ?? account.config.watchMentions ?? [],
303
+ watchRegex: groupCfg?.watchRegex ?? account.config.watchRegex,
289
304
  systemPrompt: groupCfg?.systemPrompt,
290
- thinkingIndicator: groupCfg?.thinkingIndicator ?? account.config.thinkingIndicator ?? true,
291
305
  };
292
306
  }
293
307
 
294
- // ---------------------------------------------------------------------------
295
- // Thinking indicator (收到🤔...)
296
- // ---------------------------------------------------------------------------
297
-
298
- type ThinkingIndicatorHandle = {
299
- messageid: string;
300
- msgseqid: string;
301
- };
302
-
303
- /**
304
- * Sends a "收到🤔..." thinking indicator message.
305
- * Returns message IDs needed for recall, or undefined on failure.
306
- */
307
- async function sendThinkingIndicator(params: {
308
- cfg: OpenClawConfig;
309
- to: string;
310
- accountId: string;
311
- replyTo?: InfoflowOutboundReply;
312
- }): Promise<ThinkingIndicatorHandle | undefined> {
313
- const { cfg, to, accountId, replyTo } = params;
314
- try {
315
- const result = await sendInfoflowMessage({
316
- cfg,
317
- to,
318
- contents: [{ type: "text", content: "收到🤔..." }],
319
- accountId,
320
- replyTo,
321
- });
322
- if (result.ok && result.messageId) {
323
- logVerbose(
324
- `[infoflow] thinking indicator sent: to=${to}, messageId=${result.messageId}, msgseqid=${result.msgseqid ?? "n/a"}`,
325
- );
326
- return { messageid: result.messageId, msgseqid: result.msgseqid ?? "" };
327
- }
328
- if (!result.ok) {
329
- logVerbose(`[infoflow] thinking indicator send failed: ${result.error}`);
330
- }
331
- return undefined;
332
- } catch (err) {
333
- logVerbose(`[infoflow] thinking indicator exception: ${formatInfoflowError(err)}`);
334
- return undefined;
335
- }
336
- }
337
-
338
- /**
339
- * Recalls a previously sent thinking indicator (group or private).
340
- * Silently swallows errors to avoid disrupting the reply flow.
341
- */
342
- async function recallThinkingIndicator(params: {
343
- cfg: OpenClawConfig;
344
- accountId: string;
345
- handle: ThinkingIndicatorHandle;
346
- groupId?: number;
347
- isPrivate?: boolean;
348
- }): Promise<void> {
349
- const { cfg, accountId, handle, groupId, isPrivate } = params;
350
- try {
351
- const account = resolveInfoflowAccount({ cfg, accountId });
352
- if (isPrivate) {
353
- const appAgentId = account.config.appAgentId;
354
- if (!appAgentId) {
355
- logVerbose(
356
- `[infoflow] thinking indicator private recall skipped: appAgentId not configured`,
357
- );
358
- return;
359
- }
360
- const result = await recallInfoflowPrivateMessage({
361
- account,
362
- msgkey: handle.messageid,
363
- appAgentId,
364
- });
365
- if (result.ok) {
366
- logVerbose(`[infoflow] thinking indicator recalled (private)`);
367
- } else {
368
- logVerbose(`[infoflow] thinking indicator private recall failed: ${result.error}`);
369
- }
370
- } else if (groupId !== undefined) {
371
- const result = await recallInfoflowGroupMessage({
372
- account,
373
- groupId,
374
- messageid: handle.messageid,
375
- msgseqid: handle.msgseqid,
376
- });
377
- if (result.ok) {
378
- logVerbose(`[infoflow] thinking indicator recalled: groupId=${groupId}`);
379
- } else {
380
- logVerbose(`[infoflow] thinking indicator recall failed: ${result.error}`);
381
- }
382
- }
383
- } catch (err) {
384
- logVerbose(`[infoflow] thinking indicator recall exception: ${formatInfoflowError(err)}`);
385
- }
386
- }
387
-
388
308
  /**
389
309
  * Handles an incoming private chat message from Infoflow.
390
310
  * Receives the raw decrypted message data and dispatches to the agent.
@@ -780,6 +700,7 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
780
700
 
781
701
  // Reply mode gating for group messages
782
702
  // Session is already recorded above for context history
703
+ let triggerReason = "direct-message";
783
704
  if (isGroup && groupCfg) {
784
705
  const { replyMode } = groupCfg;
785
706
  const groupIdStr = groupId !== undefined ? String(groupId) : undefined;
@@ -787,6 +708,9 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
787
708
  // "record" mode: save to session only, no think, no reply
788
709
  if (replyMode === "record") {
789
710
  if (groupIdStr) {
711
+ logVerbose(
712
+ `[infoflow:bot] pending: from=${fromuser}, group=${groupId}, reason=record-mode`,
713
+ );
790
714
  recordPendingHistoryEntryIfEnabled({
791
715
  historyMap: chatHistories,
792
716
  historyKey: groupIdStr,
@@ -803,16 +727,22 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
803
727
  if (replyMode === "mention-only") {
804
728
  // Only reply if bot was @mentioned
805
729
  const shouldReply = canDetectMention && wasMentioned;
806
- if (!shouldReply) {
730
+ if (shouldReply) {
731
+ triggerReason = "bot-mentioned";
732
+ } else {
807
733
  // Check follow-up window: if bot recently replied, allow LLM to decide
808
734
  if (
809
735
  groupCfg.followUp &&
810
736
  groupIdStr &&
811
737
  isWithinFollowUpWindow(groupIdStr, groupCfg.followUpWindow)
812
738
  ) {
739
+ triggerReason = "followUp";
813
740
  ctxPayload.GroupSystemPrompt = buildFollowUpPrompt();
814
741
  } else {
815
742
  if (groupIdStr) {
743
+ logVerbose(
744
+ `[infoflow:bot] pending: from=${fromuser}, group=${groupId}, reason=mention-only-not-mentioned`,
745
+ );
816
746
  recordPendingHistoryEntryIfEnabled({
817
747
  historyMap: chatHistories,
818
748
  historyKey: groupIdStr,
@@ -826,7 +756,9 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
826
756
  } else if (replyMode === "mention-and-watch") {
827
757
  // Reply if bot @mentioned, or if watched person @mentioned, or follow-up
828
758
  const botMentioned = canDetectMention && wasMentioned;
829
- if (!botMentioned) {
759
+ if (botMentioned) {
760
+ triggerReason = "bot-mentioned";
761
+ } else {
830
762
  // Check watch-mention
831
763
  const watchMentions = groupCfg.watchMentions;
832
764
  const matchedWatchId =
@@ -835,17 +767,26 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
835
767
  : undefined;
836
768
 
837
769
  if (matchedWatchId) {
770
+ triggerReason = `watchMentions(${matchedWatchId})`;
838
771
  // Watch-mention triggered: instruct agent to reply only if confident
839
772
  ctxPayload.GroupSystemPrompt = buildWatchMentionPrompt(matchedWatchId);
773
+ } else if (groupCfg.watchRegex && checkWatchRegex(mes, groupCfg.watchRegex)) {
774
+ triggerReason = `watchRegex(${groupCfg.watchRegex})`;
775
+ // Watch-content triggered: message matched configured regex pattern
776
+ ctxPayload.GroupSystemPrompt = buildWatchRegexPrompt(groupCfg.watchRegex);
840
777
  } else if (
841
778
  groupCfg.followUp &&
842
779
  groupIdStr &&
843
780
  isWithinFollowUpWindow(groupIdStr, groupCfg.followUpWindow)
844
781
  ) {
782
+ triggerReason = "followUp";
845
783
  // Follow-up window: let LLM decide if this is a follow-up
846
784
  ctxPayload.GroupSystemPrompt = buildFollowUpPrompt();
847
785
  } else {
848
786
  if (groupIdStr) {
787
+ logVerbose(
788
+ `[infoflow:bot] pending: from=${fromuser}, group=${groupId}, reason=mention-and-watch-no-trigger`,
789
+ );
849
790
  recordPendingHistoryEntryIfEnabled({
850
791
  historyMap: chatHistories,
851
792
  historyKey: groupIdStr,
@@ -859,7 +800,9 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
859
800
  } else if (replyMode === "proactive") {
860
801
  // Always think and potentially reply
861
802
  const botMentioned = canDetectMention && wasMentioned;
862
- if (!botMentioned) {
803
+ if (botMentioned) {
804
+ triggerReason = "bot-mentioned";
805
+ } else {
863
806
  // Check watch-mention first (higher priority prompt)
864
807
  const watchMentions = groupCfg.watchMentions;
865
808
  const matchedWatchId =
@@ -867,8 +810,10 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
867
810
  ? checkWatchMentioned(event.bodyItems, watchMentions)
868
811
  : undefined;
869
812
  if (matchedWatchId) {
813
+ triggerReason = `watchMentions(${matchedWatchId})`;
870
814
  ctxPayload.GroupSystemPrompt = buildWatchMentionPrompt(matchedWatchId);
871
815
  } else {
816
+ triggerReason = "proactive";
872
817
  ctxPayload.GroupSystemPrompt = buildProactivePrompt();
873
818
  }
874
819
  }
@@ -886,21 +831,6 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
886
831
  // Build unified target: "group:<id>" for group chat, username for private chat
887
832
  const to = isGroup && groupId !== undefined ? `group:${groupId}` : fromuser;
888
833
 
889
- // --- Thinking indicator ("收到🤔...") ---
890
- const thinkingEnabled = groupCfg?.thinkingIndicator ?? account.config.thinkingIndicator ?? true;
891
- let thinkingHandle: ThinkingIndicatorHandle | undefined;
892
- if (thinkingEnabled) {
893
- thinkingHandle = await sendThinkingIndicator({
894
- cfg,
895
- to,
896
- accountId: account.accountId,
897
- replyTo:
898
- isGroup && event.messageId
899
- ? { messageid: event.messageId, preview: mes.slice(0, 100) }
900
- : undefined,
901
- });
902
- }
903
-
904
834
  // Provide mention context to the LLM so it can decide who to @mention
905
835
  if (isGroup && event.mentionIds) {
906
836
  const parts: string[] = [];
@@ -915,6 +845,10 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
915
845
  }
916
846
  }
917
847
 
848
+ logVerbose(
849
+ `[infoflow:bot] dispatching to LLM: from=${fromuser}, group=${groupId ?? "N/A"}, trigger=${triggerReason}, replyMode=${groupCfg?.replyMode ?? "N/A"}`,
850
+ );
851
+
918
852
  const { dispatcherOptions, replyOptions } = createInfoflowReplyDispatcher({
919
853
  cfg,
920
854
  agentId: route.agentId,
@@ -930,48 +864,12 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
930
864
  replyToPreview: isGroup ? mes : undefined,
931
865
  });
932
866
 
933
- // Wrap dispatcher to recall thinking indicator before first delivery
934
- const canRecallThinking = Boolean(thinkingHandle);
935
- let thinkingRecalled = false;
936
- const doRecallThinking = async () => {
937
- if (thinkingRecalled || !canRecallThinking) return;
938
- thinkingRecalled = true;
939
- await recallThinkingIndicator({
940
- cfg,
941
- accountId: account.accountId,
942
- handle: thinkingHandle!,
943
- groupId: isGroup ? groupId : undefined,
944
- isPrivate: !isGroup,
945
- });
946
- };
947
-
948
- const originalDeliver = dispatcherOptions.deliver;
949
- const wrappedDispatcherOptions = {
950
- ...dispatcherOptions,
951
- deliver: async (payload: ReplyPayload) => {
952
- await doRecallThinking();
953
- return originalDeliver(payload);
954
- },
955
- onCleanup: () => {
956
- void doRecallThinking();
957
- },
958
- };
959
-
960
- // Wrap dispatch in try/finally to guarantee the thinking indicator bound to
961
- // this message is always recalled — even when queue policy drops/enqueues the
962
- // message before typing activates (typing.cleanup skips onCleanup when inactive).
963
- // doRecallThinking is idempotent (thinkingRecalled flag), so duplicate calls are no-ops.
964
- let dispatchResult;
965
- try {
966
- dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
967
- ctx: ctxPayload,
968
- cfg,
969
- dispatcherOptions: wrappedDispatcherOptions,
970
- replyOptions,
971
- });
972
- } finally {
973
- await doRecallThinking();
974
- }
867
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
868
+ ctx: ctxPayload,
869
+ cfg,
870
+ dispatcherOptions,
871
+ replyOptions,
872
+ });
975
873
 
976
874
  const didReply = dispatchResult?.queuedFinal ?? false;
977
875
 
package/src/channel.ts CHANGED
@@ -52,6 +52,7 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
52
52
  agentPrompt: {
53
53
  messageToolHints: () => [
54
54
  'Infoflow group @mentions: set atAll=true to @all members, or mentionUserIds="user1,user2" (comma-separated uuapName) to @mention specific users. Only effective for group targets (group:<id>).',
55
+ 'Infoflow supports message recall (撤回): use action="delete" to recall the most recent message, or specify messageId to recall a specific message. Works for both private and group messages.',
55
56
  ],
56
57
  },
57
58
  config: {
@@ -61,9 +61,9 @@ function isDuplicateMessage(msgData: Record<string, unknown>): boolean {
61
61
  * Called after successfully sending a message to prevent
62
62
  * the bot from processing its own outbound messages as inbound.
63
63
  */
64
- export function recordSentMessageId(messageId: string | number): void {
64
+ export function recordSentMessageId(messageId: string | null): void {
65
65
  if (messageId == null) return;
66
- messageCache.check(String(messageId)); // Will record if not duplicate
66
+ messageCache.check(messageId);
67
67
  }
68
68
 
69
69
  // ---------------------------------------------------------------------------
package/src/media.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  INFOFLOW_PRIVATE_SEND_PATH,
16
16
  INFOFLOW_GROUP_SEND_PATH,
17
17
  } from "./send.js";
18
- import { recordSentMessage } from "./sent-message-store.js";
18
+ import { recordSentMessage, buildAgentFrom } from "./sent-message-store.js";
19
19
  import type { ResolvedInfoflowAccount, InfoflowOutboundReply } from "./types.js";
20
20
 
21
21
  /** Infoflow API image size limit: 1MB raw bytes */
@@ -225,6 +225,7 @@ export async function sendInfoflowGroupImage(params: {
225
225
  try {
226
226
  recordSentMessage(account.accountId, {
227
227
  target: `group:${groupId}`,
228
+ from: buildAgentFrom(account.config.appAgentId),
228
229
  messageid,
229
230
  msgseqid: msgseqid ?? "",
230
231
  digest: "image",
@@ -313,6 +314,7 @@ export async function sendInfoflowPrivateImage(params: {
313
314
  try {
314
315
  recordSentMessage(account.accountId, {
315
316
  target: toUser,
317
+ from: buildAgentFrom(account.config.appAgentId),
316
318
  messageid: msgkey,
317
319
  msgseqid: "",
318
320
  digest: "image",
package/src/send.ts CHANGED
@@ -8,7 +8,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
8
8
  import { resolveInfoflowAccount } from "./accounts.js";
9
9
  import { recordSentMessageId } from "./infoflow-req-parse.js";
10
10
  import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "./logging.js";
11
- import { recordSentMessage, buildMessageDigest } from "./sent-message-store.js";
11
+ import { recordSentMessage, buildMessageDigest, buildAgentFrom } from "./sent-message-store.js";
12
12
  import type {
13
13
  InfoflowGroupMessageBodyItem,
14
14
  InfoflowMessageContentItem,
@@ -375,6 +375,7 @@ export async function sendInfoflowPrivateMessage(params: {
375
375
  try {
376
376
  recordSentMessage(account.accountId, {
377
377
  target: toUser,
378
+ from: buildAgentFrom(account.config.appAgentId),
378
379
  messageid: msgkey,
379
380
  msgseqid: "",
380
381
  digest: buildMessageDigest(contents),
@@ -576,18 +577,18 @@ export async function sendInfoflowGroupMessage(params: {
576
577
  result: { messageid?: string; msgseqid?: string },
577
578
  digestContents: InfoflowMessageContentItem[],
578
579
  ) => {
579
- if (result.messageid) {
580
- try {
581
- recordSentMessage(account.accountId, {
582
- target: `group:${groupId}`,
583
- messageid: result.messageid,
584
- msgseqid: result.msgseqid ?? "",
585
- digest: buildMessageDigest(digestContents),
586
- sentAt: Date.now(),
587
- });
588
- } catch {
589
- // Do not block sending
590
- }
580
+ if (!result.messageid) return;
581
+ try {
582
+ recordSentMessage(account.accountId, {
583
+ target: `group:${groupId}`,
584
+ from: buildAgentFrom(account.config.appAgentId),
585
+ messageid: result.messageid,
586
+ msgseqid: result.msgseqid ?? "",
587
+ digest: buildMessageDigest(digestContents),
588
+ sentAt: Date.now(),
589
+ });
590
+ } catch {
591
+ // Do not block sending
591
592
  }
592
593
  };
593
594
 
@@ -774,7 +775,7 @@ export async function recallInfoflowPrivateMessage(params: {
774
775
 
775
776
  const bodyStr = JSON.stringify({ msgkey, agentid: appAgentId });
776
777
 
777
- logVerbose(`[infoflow:recallPrivate] POST body: ${bodyStr}`);
778
+ logVerbose(`[infoflow:recallPrivate] POST auth: ${tokenResult.token} body: ${bodyStr}`);
778
779
 
779
780
  const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_PRIVATE_RECALL_PATH}`, {
780
781
  method: "POST",
@@ -51,6 +51,7 @@ function getDb(): DatabaseSync {
51
51
  id INTEGER PRIMARY KEY AUTOINCREMENT,
52
52
  account_id TEXT NOT NULL,
53
53
  target TEXT NOT NULL,
54
+ from_id TEXT NOT NULL DEFAULT '',
54
55
  messageid TEXT NOT NULL,
55
56
  msgseqid TEXT NOT NULL DEFAULT '',
56
57
  digest TEXT NOT NULL DEFAULT '',
@@ -62,6 +63,13 @@ function getDb(): DatabaseSync {
62
63
  ON sent_messages(account_id, target, sent_at DESC);
63
64
  `);
64
65
 
66
+ // Migration: add from_id column to existing databases
67
+ try {
68
+ db.exec(`ALTER TABLE sent_messages ADD COLUMN from_id TEXT NOT NULL DEFAULT ''`);
69
+ } catch {
70
+ // Column already exists — ignore
71
+ }
72
+
65
73
  return db;
66
74
  }
67
75
 
@@ -71,6 +79,7 @@ function getDb(): DatabaseSync {
71
79
 
72
80
  export type SentMessageRecord = {
73
81
  target: string;
82
+ from: string;
74
83
  messageid: string;
75
84
  msgseqid: string;
76
85
  digest: string;
@@ -85,11 +94,12 @@ export function recordSentMessage(accountId: string, record: SentMessageRecord):
85
94
  try {
86
95
  const d = getDb();
87
96
  d.prepare(
88
- `INSERT INTO sent_messages (account_id, target, messageid, msgseqid, digest, sent_at)
89
- VALUES (?, ?, ?, ?, ?, ?)`,
97
+ `INSERT INTO sent_messages (account_id, target, from_id, messageid, msgseqid, digest, sent_at)
98
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
90
99
  ).run(
91
100
  accountId,
92
101
  record.target,
102
+ record.from,
93
103
  record.messageid,
94
104
  record.msgseqid,
95
105
  record.digest,
@@ -117,7 +127,7 @@ export function querySentMessages(
117
127
  const d = getDb();
118
128
  const rows = d
119
129
  .prepare(
120
- `SELECT target, messageid, msgseqid, digest, sent_at
130
+ `SELECT target, from_id, messageid, msgseqid, digest, sent_at
121
131
  FROM sent_messages
122
132
  WHERE account_id = ? AND target = ?
123
133
  ORDER BY sent_at DESC
@@ -125,6 +135,7 @@ export function querySentMessages(
125
135
  )
126
136
  .all(accountId, params.target, params.count) as Array<{
127
137
  target: string;
138
+ from_id: string;
128
139
  messageid: string;
129
140
  msgseqid: string;
130
141
  digest: string;
@@ -133,6 +144,7 @@ export function querySentMessages(
133
144
 
134
145
  return rows.map((r) => ({
135
146
  target: r.target,
147
+ from: r.from_id,
136
148
  messageid: r.messageid,
137
149
  msgseqid: r.msgseqid,
138
150
  digest: r.digest,
@@ -150,18 +162,26 @@ export function findSentMessage(
150
162
  const d = getDb();
151
163
  const row = d
152
164
  .prepare(
153
- `SELECT target, messageid, msgseqid, digest, sent_at
165
+ `SELECT target, from_id, messageid, msgseqid, digest, sent_at
154
166
  FROM sent_messages
155
167
  WHERE account_id = ? AND messageid = ?
156
168
  LIMIT 1`,
157
169
  )
158
170
  .get(accountId, messageid) as
159
- | { target: string; messageid: string; msgseqid: string; digest: string; sent_at: number }
171
+ | {
172
+ target: string;
173
+ from_id: string;
174
+ messageid: string;
175
+ msgseqid: string;
176
+ digest: string;
177
+ sent_at: number;
178
+ }
160
179
  | undefined;
161
180
 
162
181
  if (!row) return undefined;
163
182
  return {
164
183
  target: row.target,
184
+ from: row.from_id,
165
185
  messageid: row.messageid,
166
186
  msgseqid: row.msgseqid,
167
187
  digest: row.digest,
@@ -181,6 +201,15 @@ export function removeRecalledMessages(accountId: string, messageids: string[]):
181
201
  ).run(accountId, ...messageids);
182
202
  }
183
203
 
204
+ // ---------------------------------------------------------------------------
205
+ // From-ID builder
206
+ // ---------------------------------------------------------------------------
207
+
208
+ /** Builds a `from` identifier for self-sent (agent) messages. */
209
+ export function buildAgentFrom(appAgentId: number | undefined): string {
210
+ return appAgentId != null ? `agent:${appAgentId}` : "agent:unknown";
211
+ }
212
+
184
213
  // ---------------------------------------------------------------------------
185
214
  // Digest builder
186
215
  // ---------------------------------------------------------------------------
package/src/types.ts CHANGED
@@ -22,11 +22,10 @@ export type InfoflowReplyMode =
22
22
  export type InfoflowGroupConfig = {
23
23
  replyMode?: InfoflowReplyMode;
24
24
  watchMentions?: string[];
25
+ watchRegex?: string;
25
26
  followUp?: boolean;
26
27
  followUpWindow?: number;
27
28
  systemPrompt?: string;
28
- /** Enable thinking indicator ("收到🤔...") before processing (default: true) */
29
- thinkingIndicator?: boolean;
30
29
  };
31
30
 
32
31
  // ---------------------------------------------------------------------------
@@ -114,14 +113,14 @@ export type InfoflowAccountConfig = {
114
113
  /** Names to watch for @mentions; when someone @mentions a person in this list,
115
114
  * the bot analyzes the message and replies only if confident. */
116
115
  watchMentions?: string[];
116
+ /** Regex pattern to watch for in message content; triggers bot activation when matched */
117
+ watchRegex?: string;
117
118
  /** Reply mode controlling bot engagement level in groups */
118
119
  replyMode?: InfoflowReplyMode;
119
120
  /** Enable follow-up replies after bot responds to a mention (default: true) */
120
121
  followUp?: boolean;
121
122
  /** Follow-up window in seconds after last bot reply (default: 300) */
122
123
  followUpWindow?: number;
123
- /** Enable thinking indicator ("收到🤔...") before processing (default: true) */
124
- thinkingIndicator?: boolean;
125
124
  /** 如流企业后台的应用ID(私聊消息撤回依赖此字段) */
126
125
  appAgentId?: number;
127
126
  /** Per-group configuration overrides, keyed by group ID */
@@ -153,14 +152,14 @@ export type ResolvedInfoflowAccount = {
153
152
  /** Names to watch for @mentions; when someone @mentions a person in this list,
154
153
  * the bot analyzes the message and replies only if confident. */
155
154
  watchMentions?: string[];
155
+ /** Regex pattern to watch for in message content; triggers bot activation when matched */
156
+ watchRegex?: string;
156
157
  /** Reply mode controlling bot engagement level in groups */
157
158
  replyMode?: InfoflowReplyMode;
158
159
  /** Enable follow-up replies after bot responds to a mention (default: true) */
159
160
  followUp?: boolean;
160
161
  /** Follow-up window in seconds after last bot reply (default: 300) */
161
162
  followUpWindow?: number;
162
- /** Enable thinking indicator ("收到🤔...") before processing (default: true) */
163
- thinkingIndicator?: boolean;
164
163
  /** 如流企业后台的应用ID(私聊消息撤回依赖此字段) */
165
164
  appAgentId?: number;
166
165
  /** Per-group configuration overrides, keyed by group ID */