@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 +20 -0
- package/README.md +53 -1
- package/openclaw.plugin.json +6 -6
- package/package.json +1 -1
- package/src/accounts.ts +3 -0
- package/src/actions.ts +10 -14
- package/src/bot.ts +62 -164
- package/src/channel.ts +1 -0
- package/src/infoflow-req-parse.ts +2 -2
- package/src/media.ts +3 -1
- package/src/send.ts +15 -14
- package/src/sent-message-store.ts +34 -5
- package/src/types.ts +5 -6
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 |
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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 |
|
|
64
|
+
export function recordSentMessageId(messageId: string | null): void {
|
|
65
65
|
if (messageId == null) return;
|
|
66
|
-
messageCache.check(
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
|
|
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
|
-
| {
|
|
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 */
|