@core-workspace/infoflow-openclaw-plugin 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/README.md +989 -0
- package/docs/architecture-data-flow.md +429 -0
- package/docs/architecture.md +423 -0
- package/docs/dev-guide.md +611 -0
- package/index.ts +29 -0
- package/openclaw.plugin.json +138 -0
- package/package.json +40 -0
- package/scripts/deploy.sh +34 -0
- package/skills/infoflow-dev/SKILL.md +88 -0
- package/skills/infoflow-dev/references/api.md +413 -0
- package/src/adapter/inbound/webhook-parser.ts +433 -0
- package/src/adapter/inbound/ws-receiver.ts +226 -0
- package/src/adapter/outbound/reply-dispatcher.ts +281 -0
- package/src/adapter/outbound/target-resolver.ts +109 -0
- package/src/channel/accounts.ts +164 -0
- package/src/channel/channel.ts +364 -0
- package/src/channel/media.ts +365 -0
- package/src/channel/monitor.ts +184 -0
- package/src/channel/outbound.ts +934 -0
- package/src/events.ts +62 -0
- package/src/handler/message-handler.ts +801 -0
- package/src/logging.ts +123 -0
- package/src/runtime.ts +14 -0
- package/src/security/dm-policy.ts +80 -0
- package/src/security/group-policy.ts +271 -0
- package/src/tools/actions/index.ts +456 -0
- package/src/tools/hooks/index.ts +82 -0
- package/src/tools/index.ts +277 -0
- package/src/types.ts +277 -0
- package/src/utils/store/message-store.ts +295 -0
- package/src/utils/token-adapter.ts +90 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infoflow Agent Tools(Agent 工具注册)
|
|
3
|
+
*
|
|
4
|
+
* 向 OpenClaw 注册两个供 LLM function calling 使用的工具:
|
|
5
|
+
* - infoflow_send: 主动向用户或群发送消息
|
|
6
|
+
* - infoflow_recall: 撤回近期发送的消息
|
|
7
|
+
*
|
|
8
|
+
* 这两个工具封装了 outbound/send.ts 中的底层发送/撤回逻辑,
|
|
9
|
+
* 让 Agent 能够主动发消息,而不仅限于被动回复。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
13
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
14
|
+
import { resolveInfoflowAccount } from "../channel/accounts.js";
|
|
15
|
+
import { logVerbose } from "../logging.js";
|
|
16
|
+
import { sendInfoflowMessage, recallInfoflowGroupMessage, recallInfoflowPrivateMessage } from "../channel/outbound.js";
|
|
17
|
+
import { querySentMessages, removeRecalledMessages } from "../utils/store/message-store.js";
|
|
18
|
+
import { normalizeInfoflowTarget } from "../adapter/outbound/target-resolver.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// infoflow_send 参数 Schema
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const InfoflowSendSchema = Type.Object({
|
|
25
|
+
to: Type.String({
|
|
26
|
+
description:
|
|
27
|
+
'消息目标。格式:"用户名"(uuapName / 邮箱前缀)发私聊,"group:群ID" 发群消息(例如 "group:12600364")。',
|
|
28
|
+
}),
|
|
29
|
+
message: Type.String({
|
|
30
|
+
description: "消息正文,支持 Markdown 格式。",
|
|
31
|
+
}),
|
|
32
|
+
atAll: Type.Optional(
|
|
33
|
+
Type.Boolean({
|
|
34
|
+
description: "群消息中是否 @全体成员,仅对群目标有效。",
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
mentionUserIds: Type.Optional(
|
|
38
|
+
Type.String({
|
|
39
|
+
description:
|
|
40
|
+
'需要 @ 的成员列表,逗号分隔的 uuapName(例如 "zhangsan,lisi")。仅对群目标有效,atAll 为 true 时忽略。',
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
accountId: Type.Optional(
|
|
44
|
+
Type.String({
|
|
45
|
+
description: "多账号模式下指定账号 ID,不填则使用默认账号。",
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
type InfoflowSendParams = Static<typeof InfoflowSendSchema>;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// infoflow_recall 参数 Schema
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const InfoflowRecallSchema = Type.Object({
|
|
57
|
+
to: Type.String({
|
|
58
|
+
description:
|
|
59
|
+
'要撤回消息的目标,格式与 infoflow_send 相同:"用户名" 表示私聊,"group:群ID" 表示群聊。',
|
|
60
|
+
}),
|
|
61
|
+
count: Type.Optional(
|
|
62
|
+
Type.Number({
|
|
63
|
+
description: "撤回最近发送的 N 条消息(默认为 1)。",
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
messageId: Type.Optional(
|
|
67
|
+
Type.String({
|
|
68
|
+
description:
|
|
69
|
+
"按消息 ID 撤回指定消息。提供此参数时忽略 count。群消息所需的 msgseqid 会自动从消息记录中查找。",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
accountId: Type.Optional(
|
|
73
|
+
Type.String({
|
|
74
|
+
description: "多账号模式下指定账号 ID,不填则使用默认账号。",
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
type InfoflowRecallParams = Static<typeof InfoflowRecallSchema>;
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// 注册函数
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 向 OpenClaw 插件 API 注册 infoflow_send 和 infoflow_recall 工具。
|
|
87
|
+
* 在插件的 register() 函数中调用。
|
|
88
|
+
*/
|
|
89
|
+
export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
90
|
+
if (!api.config) {
|
|
91
|
+
api.logger.debug?.("infoflow_tools: No config available, skipping tool registration");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- infoflow_send:主动发消息 ----
|
|
96
|
+
api.registerTool(
|
|
97
|
+
{
|
|
98
|
+
name: "infoflow_send",
|
|
99
|
+
description:
|
|
100
|
+
"向如流(Infoflow)的用户或群发送消息。支持纯文本和 Markdown 格式。" +
|
|
101
|
+
"群消息可以 @指定成员 或 @全体成员。" +
|
|
102
|
+
"当需要主动发送通知、提醒或消息时使用此工具。",
|
|
103
|
+
parameters: InfoflowSendSchema,
|
|
104
|
+
async execute(_toolCallId, rawParams) {
|
|
105
|
+
const params = rawParams as InfoflowSendParams;
|
|
106
|
+
const { to: rawTo, message, atAll, mentionUserIds, accountId } = params;
|
|
107
|
+
|
|
108
|
+
// 标准化目标格式(去掉可能多余的前缀)
|
|
109
|
+
const to = normalizeInfoflowTarget(rawTo) ?? rawTo;
|
|
110
|
+
const isGroup = /^group:\d+$/i.test(to);
|
|
111
|
+
|
|
112
|
+
logVerbose(`[infoflow_send] to=${to}, isGroup=${isGroup}`);
|
|
113
|
+
|
|
114
|
+
const contents: Array<{ type: string; content: string }> = [];
|
|
115
|
+
|
|
116
|
+
// 群消息:先拼 @mention 块,再拼正文
|
|
117
|
+
if (isGroup) {
|
|
118
|
+
if (atAll) {
|
|
119
|
+
contents.push({ type: "at", content: "all" });
|
|
120
|
+
} else if (mentionUserIds) {
|
|
121
|
+
const ids = mentionUserIds
|
|
122
|
+
.split(",")
|
|
123
|
+
.map((s) => s.trim())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
if (ids.length > 0) {
|
|
126
|
+
contents.push({ type: "at", content: ids.join(",") });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (message.trim()) {
|
|
132
|
+
contents.push({ type: "markdown", content: message });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (contents.length === 0) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "message is empty" }) }],
|
|
138
|
+
details: { ok: false, error: "message is empty" },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = await sendInfoflowMessage({
|
|
143
|
+
cfg: api.config!,
|
|
144
|
+
to,
|
|
145
|
+
contents,
|
|
146
|
+
accountId: accountId ?? undefined,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const payload = {
|
|
150
|
+
ok: result.ok,
|
|
151
|
+
channel: "infoflow",
|
|
152
|
+
to,
|
|
153
|
+
...(result.messageId ? { messageId: result.messageId } : {}),
|
|
154
|
+
...(result.error ? { error: result.error } : {}),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
159
|
+
details: payload,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{ name: "infoflow_send" },
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// ---- infoflow_recall:撤回消息 ----
|
|
167
|
+
api.registerTool(
|
|
168
|
+
{
|
|
169
|
+
name: "infoflow_recall",
|
|
170
|
+
description:
|
|
171
|
+
"撤回机器人在如流(Infoflow)发送的消息。" +
|
|
172
|
+
"可按消息 ID 撤回指定消息,也可按数量撤回最近 N 条(默认 1 条)。" +
|
|
173
|
+
"当用户要求撤回、取消或删除刚发的如流消息时使用此工具。",
|
|
174
|
+
parameters: InfoflowRecallSchema,
|
|
175
|
+
async execute(_toolCallId, rawParams) {
|
|
176
|
+
const params = rawParams as InfoflowRecallParams;
|
|
177
|
+
const { to: rawTo, count = 1, messageId, accountId } = params;
|
|
178
|
+
|
|
179
|
+
const to = normalizeInfoflowTarget(rawTo) ?? rawTo;
|
|
180
|
+
const target = to.replace(/^infoflow:/i, "");
|
|
181
|
+
|
|
182
|
+
// 校验账号配置
|
|
183
|
+
const account = resolveInfoflowAccount({ cfg: api.config!, accountId: accountId ?? undefined });
|
|
184
|
+
if (!account.config.appKey || !account.config.appSecret) {
|
|
185
|
+
const err = { ok: false, error: "Infoflow appKey/appSecret not configured." };
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: "text" as const, text: JSON.stringify(err) }],
|
|
188
|
+
details: err,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const groupMatch = target.match(/^group:(\d+)/i);
|
|
193
|
+
// 统一返回格式的辅助函数
|
|
194
|
+
const json = (payload: unknown) => ({
|
|
195
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
196
|
+
details: payload,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── 群消息撤回 ──
|
|
200
|
+
if (groupMatch) {
|
|
201
|
+
const groupId = Number(groupMatch[1]);
|
|
202
|
+
|
|
203
|
+
// 模式 A:按 messageId 撤回单条
|
|
204
|
+
if (messageId) {
|
|
205
|
+
// 群消息撤回需要同时提供 messageid + msgseqid,从本地消息记录中查找 msgseqid
|
|
206
|
+
const stored = querySentMessages(account.accountId, { target: `group:${groupId}`, count: 1 })
|
|
207
|
+
.find((r) => r.messageid === messageId);
|
|
208
|
+
const msgseqid = stored?.msgseqid ?? "";
|
|
209
|
+
if (!msgseqid) {
|
|
210
|
+
return json({ ok: false, error: "msgseqid not found for this messageId. It may have already been recalled or not found in the message store." });
|
|
211
|
+
}
|
|
212
|
+
const result = await recallInfoflowGroupMessage({ account, groupId, messageid: messageId, msgseqid });
|
|
213
|
+
if (result.ok) {
|
|
214
|
+
try { removeRecalledMessages(account.accountId, [messageId]); } catch { /* ignore */ }
|
|
215
|
+
}
|
|
216
|
+
return json({ ok: result.ok, to, ...(result.error ? { error: result.error } : {}) });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 模式 B:按 count 批量撤回最近 N 条
|
|
220
|
+
const records = querySentMessages(account.accountId, { target: `group:${groupId}`, count })
|
|
221
|
+
.filter((r) => r.msgseqid); // 只撤回有 msgseqid 的记录
|
|
222
|
+
|
|
223
|
+
if (records.length === 0) {
|
|
224
|
+
return json({ ok: true, to, recalled: 0, message: "No recallable messages found." });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let succeeded = 0, failed = 0;
|
|
228
|
+
const recalledIds: string[] = [];
|
|
229
|
+
for (const r of records) {
|
|
230
|
+
const res = await recallInfoflowGroupMessage({ account, groupId, messageid: r.messageid, msgseqid: r.msgseqid });
|
|
231
|
+
if (res.ok) { succeeded++; recalledIds.push(r.messageid); } else { failed++; }
|
|
232
|
+
}
|
|
233
|
+
if (recalledIds.length > 0) {
|
|
234
|
+
try { removeRecalledMessages(account.accountId, recalledIds); } catch { /* ignore */ }
|
|
235
|
+
}
|
|
236
|
+
return json({ ok: failed === 0, to, recalled: succeeded, failed, total: records.length });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── 私聊消息撤回 ──
|
|
240
|
+
// 私聊撤回需要 appAgentId(企业后台的应用 ID)
|
|
241
|
+
const appAgentId = account.config.appAgentId;
|
|
242
|
+
if (!appAgentId) {
|
|
243
|
+
return json({ ok: false, error: "Private message recall requires appAgentId configuration." });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 模式 A:按 messageId(即 msgkey)撤回单条
|
|
247
|
+
if (messageId) {
|
|
248
|
+
const result = await recallInfoflowPrivateMessage({ account, msgkey: messageId, appAgentId });
|
|
249
|
+
if (result.ok) {
|
|
250
|
+
try { removeRecalledMessages(account.accountId, [messageId]); } catch { /* ignore */ }
|
|
251
|
+
}
|
|
252
|
+
return json({ ok: result.ok, to, ...(result.error ? { error: result.error } : {}) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 模式 B:批量撤回最近 N 条
|
|
256
|
+
const records = querySentMessages(account.accountId, { target, count }).filter((r) => r.messageid);
|
|
257
|
+
if (records.length === 0) {
|
|
258
|
+
return json({ ok: true, to, recalled: 0, message: "No recallable messages found." });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let succeeded = 0, failed = 0;
|
|
262
|
+
const recalledIds: string[] = [];
|
|
263
|
+
for (const r of records) {
|
|
264
|
+
const res = await recallInfoflowPrivateMessage({ account, msgkey: r.messageid, appAgentId });
|
|
265
|
+
if (res.ok) { succeeded++; recalledIds.push(r.messageid); } else { failed++; }
|
|
266
|
+
}
|
|
267
|
+
if (recalledIds.length > 0) {
|
|
268
|
+
try { removeRecalledMessages(account.accountId, recalledIds); } catch { /* ignore */ }
|
|
269
|
+
}
|
|
270
|
+
return json({ ok: failed === 0, to, recalled: succeeded, failed, total: records.length });
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{ name: "infoflow_recall" },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
api.logger.info?.("infoflow_tools: Registered infoflow_send and infoflow_recall tools");
|
|
277
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infoflow channel type definitions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Policy types
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export type InfoflowDmPolicy = "open" | "pairing" | "allowlist";
|
|
10
|
+
export type InfoflowGroupPolicy = "open" | "allowlist" | "disabled";
|
|
11
|
+
export type InfoflowChatType = "direct" | "group";
|
|
12
|
+
export type InfoflowConnectionMode = "webhook" | "websocket";
|
|
13
|
+
|
|
14
|
+
/** Reply mode controlling bot behavior per group */
|
|
15
|
+
export type InfoflowReplyMode =
|
|
16
|
+
| "ignore"
|
|
17
|
+
| "record"
|
|
18
|
+
| "mention-only"
|
|
19
|
+
| "mention-and-watch"
|
|
20
|
+
| "proactive";
|
|
21
|
+
|
|
22
|
+
/** Per-group configuration overrides */
|
|
23
|
+
export type InfoflowGroupConfig = {
|
|
24
|
+
replyMode?: InfoflowReplyMode;
|
|
25
|
+
systemPrompt?: string;
|
|
26
|
+
/** Enable follow-up replies after bot responds (default: true) */
|
|
27
|
+
followUp?: boolean;
|
|
28
|
+
/** Follow-up window in seconds after last bot reply (default: 300) */
|
|
29
|
+
followUpWindow?: number;
|
|
30
|
+
/** Names to watch for @mentions */
|
|
31
|
+
watchMentions?: string[];
|
|
32
|
+
/** Regex pattern to watch for in message content */
|
|
33
|
+
watchRegex?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Inbound body item (for @mention detection in received messages)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/** Inbound body item from group messages (for @mention detection) */
|
|
41
|
+
export type InfoflowInboundBodyItem = {
|
|
42
|
+
type?: string;
|
|
43
|
+
content?: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
/** 机器人 AT 时有此字段(数字),与 userid 互斥 */
|
|
46
|
+
robotid?: number;
|
|
47
|
+
/** AT 元素的显示名称 */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** 人类用户 AT 时有此字段(uuap name),与 robotid 互斥 */
|
|
50
|
+
userid?: string;
|
|
51
|
+
/** IMAGE 类型 body item 的图片下载地址 */
|
|
52
|
+
downloadurl?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Mention IDs extracted from inbound group AT items (excluding the bot itself) */
|
|
56
|
+
export type InfoflowMentionIds = {
|
|
57
|
+
/** Human user userid list */
|
|
58
|
+
userIds: string[];
|
|
59
|
+
/** Robot robotid list (numbers, corresponding to outbound atagentids) */
|
|
60
|
+
agentIds: number[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// AT mention types
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** AT mention options for @mentioning members in group messages */
|
|
68
|
+
export type InfoflowAtOptions = {
|
|
69
|
+
/** @all members; when true, atUserIds is ignored */
|
|
70
|
+
atAll?: boolean;
|
|
71
|
+
/** List of user IDs (uuapName) to @mention */
|
|
72
|
+
atUserIds?: string[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Group message body item type */
|
|
76
|
+
export type InfoflowGroupMessageBodyItem =
|
|
77
|
+
| { type: "TEXT"; content: string }
|
|
78
|
+
| { type: "MD"; content: string }
|
|
79
|
+
| { type: "AT"; atall?: boolean; atuserids: string[]; atagentids?: number[] }
|
|
80
|
+
| { type: "LINK"; href: string }
|
|
81
|
+
| { type: "IMAGE"; content: string };
|
|
82
|
+
|
|
83
|
+
/** Content item for sendInfoflowMessage */
|
|
84
|
+
export type InfoflowMessageContentItem = {
|
|
85
|
+
type: "text" | "markdown" | "at" | "at-agent" | "link" | "image";
|
|
86
|
+
content: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Outbound reply/quote context for group messages */
|
|
90
|
+
export type InfoflowOutboundReply = {
|
|
91
|
+
/** Message ID of the message being replied to (string to preserve large integer precision) */
|
|
92
|
+
messageid: string;
|
|
93
|
+
/** Preview text of the quoted message */
|
|
94
|
+
preview?: string;
|
|
95
|
+
/** "1" = reply (default), "2" = quote */
|
|
96
|
+
replytype?: "1" | "2";
|
|
97
|
+
/** User IMID (optional, if not provided, shows robot name) */
|
|
98
|
+
imid?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Account configuration
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export type InfoflowAccountConfig = {
|
|
106
|
+
enabled?: boolean;
|
|
107
|
+
name?: string;
|
|
108
|
+
apiHost?: string;
|
|
109
|
+
checkToken?: string;
|
|
110
|
+
encodingAESKey?: string;
|
|
111
|
+
appKey?: string;
|
|
112
|
+
appSecret?: string;
|
|
113
|
+
/** 连接方式:webhook(默认)或 websocket */
|
|
114
|
+
connectionMode?: InfoflowConnectionMode;
|
|
115
|
+
/** WebSocket Gateway 域名(仅 websocket 模式使用) */
|
|
116
|
+
wsGateway?: string;
|
|
117
|
+
dmPolicy?: InfoflowDmPolicy;
|
|
118
|
+
allowFrom?: string[];
|
|
119
|
+
groupPolicy?: InfoflowGroupPolicy;
|
|
120
|
+
groupAllowFrom?: string[];
|
|
121
|
+
requireMention?: boolean;
|
|
122
|
+
/** Robot name for matching @mentions in group messages */
|
|
123
|
+
robotName?: string;
|
|
124
|
+
/** Names to watch for @mentions; when someone @mentions a person in this list,
|
|
125
|
+
* the bot analyzes the message and replies only if confident. */
|
|
126
|
+
watchMentions?: string[];
|
|
127
|
+
/** Regex pattern to watch for in message content; triggers bot activation when matched */
|
|
128
|
+
watchRegex?: string;
|
|
129
|
+
/** Reply mode controlling bot engagement level in groups */
|
|
130
|
+
replyMode?: InfoflowReplyMode;
|
|
131
|
+
/** Enable follow-up replies after bot responds to a mention (default: true) */
|
|
132
|
+
followUp?: boolean;
|
|
133
|
+
/** Follow-up window in seconds after last bot reply (default: 300) */
|
|
134
|
+
followUpWindow?: number;
|
|
135
|
+
/** 如流企业后台的应用ID(私聊消息撤回依赖此字段) */
|
|
136
|
+
appAgentId?: number;
|
|
137
|
+
/** Per-group configuration overrides, keyed by group ID */
|
|
138
|
+
groups?: Record<string, InfoflowGroupConfig>;
|
|
139
|
+
accounts?: Record<string, InfoflowAccountConfig>;
|
|
140
|
+
defaultAccount?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Send a "processing" hint message immediately after receiving a message,
|
|
143
|
+
* before LLM starts responding. Helps users know the bot is working.
|
|
144
|
+
* Default: true
|
|
145
|
+
*/
|
|
146
|
+
processingHint?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Delay in seconds before sending the "processing" hint message.
|
|
149
|
+
* If LLM responds within this time, the hint is suppressed.
|
|
150
|
+
* Default: 5 (seconds). Set to 0 to send immediately.
|
|
151
|
+
*/
|
|
152
|
+
processingHintDelay?: number;
|
|
153
|
+
/**
|
|
154
|
+
* Message format for outbound private (DM) messages: "text" or "markdown".
|
|
155
|
+
* Default: "text"
|
|
156
|
+
*/
|
|
157
|
+
dmMessageFormat?: "text" | "markdown";
|
|
158
|
+
/**
|
|
159
|
+
* Message format for outbound group messages: "text" or "markdown".
|
|
160
|
+
* Note: "markdown" does not support reply-to (quote) context — the bot
|
|
161
|
+
* will still reply but without quoting the user's message.
|
|
162
|
+
* Default: "text"
|
|
163
|
+
*/
|
|
164
|
+
groupMessageFormat?: "text" | "markdown";
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type ResolvedInfoflowAccount = {
|
|
168
|
+
accountId: string;
|
|
169
|
+
name?: string;
|
|
170
|
+
enabled: boolean;
|
|
171
|
+
configured: boolean;
|
|
172
|
+
config: {
|
|
173
|
+
enabled?: boolean;
|
|
174
|
+
name?: string;
|
|
175
|
+
apiHost: string;
|
|
176
|
+
checkToken: string;
|
|
177
|
+
encodingAESKey: string;
|
|
178
|
+
appKey: string;
|
|
179
|
+
appSecret: string;
|
|
180
|
+
/** 连接方式:webhook(默认)或 websocket */
|
|
181
|
+
connectionMode?: InfoflowConnectionMode;
|
|
182
|
+
/** WebSocket Gateway 域名(仅 websocket 模式使用) */
|
|
183
|
+
wsGateway?: string;
|
|
184
|
+
dmPolicy?: InfoflowDmPolicy;
|
|
185
|
+
allowFrom?: string[];
|
|
186
|
+
groupPolicy?: InfoflowGroupPolicy;
|
|
187
|
+
groupAllowFrom?: string[];
|
|
188
|
+
requireMention?: boolean;
|
|
189
|
+
/** Robot name for matching @mentions in group messages */
|
|
190
|
+
robotName?: string;
|
|
191
|
+
/** Names to watch for @mentions; when someone @mentions a person in this list,
|
|
192
|
+
* the bot analyzes the message and replies only if confident. */
|
|
193
|
+
watchMentions?: string[];
|
|
194
|
+
/** Regex pattern to watch for in message content; triggers bot activation when matched */
|
|
195
|
+
watchRegex?: string;
|
|
196
|
+
/** Reply mode controlling bot engagement level in groups */
|
|
197
|
+
replyMode?: InfoflowReplyMode;
|
|
198
|
+
/** Enable follow-up replies after bot responds to a mention (default: true) */
|
|
199
|
+
followUp?: boolean;
|
|
200
|
+
/** Follow-up window in seconds after last bot reply (default: 300) */
|
|
201
|
+
followUpWindow?: number;
|
|
202
|
+
/** 如流企业后台的应用ID(私聊消息撤回依赖此字段) */
|
|
203
|
+
appAgentId?: number;
|
|
204
|
+
/** Per-group configuration overrides, keyed by group ID */
|
|
205
|
+
groups?: Record<string, InfoflowGroupConfig>;
|
|
206
|
+
/**
|
|
207
|
+
* Message format for outbound private (DM) messages: "text" or "markdown".
|
|
208
|
+
* Default: "text"
|
|
209
|
+
*/
|
|
210
|
+
dmMessageFormat?: "text" | "markdown";
|
|
211
|
+
/**
|
|
212
|
+
* Message format for outbound group messages: "text" or "markdown".
|
|
213
|
+
* Note: "markdown" does not support reply-to (quote) context.
|
|
214
|
+
* Default: "text"
|
|
215
|
+
*/
|
|
216
|
+
groupMessageFormat?: "text" | "markdown";
|
|
217
|
+
dmPolicy?: string;
|
|
218
|
+
allowFrom?: string[];
|
|
219
|
+
groupPolicy?: string;
|
|
220
|
+
groupAllowFrom?: string[];
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Message types
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export type InfoflowMessageEvent = {
|
|
229
|
+
fromuser: string;
|
|
230
|
+
mes: string;
|
|
231
|
+
chatType: InfoflowChatType;
|
|
232
|
+
groupId?: number;
|
|
233
|
+
senderName?: string;
|
|
234
|
+
/** Whether the bot was @mentioned in the message */
|
|
235
|
+
wasMentioned?: boolean;
|
|
236
|
+
/** Original message ID from Infoflow */
|
|
237
|
+
messageId?: string;
|
|
238
|
+
/** Unix millisecond timestamp of the message */
|
|
239
|
+
timestamp?: number;
|
|
240
|
+
/** Raw message text preserving @mentions (for RawBody) */
|
|
241
|
+
rawMes?: string;
|
|
242
|
+
/** Raw body items from group message (for watch-mention detection) */
|
|
243
|
+
bodyItems?: InfoflowInboundBodyItem[];
|
|
244
|
+
/** Non-bot mention IDs extracted from AT items in group messages (excluding bot itself) */
|
|
245
|
+
mentionIds?: InfoflowMentionIds;
|
|
246
|
+
/** Reply/quote context extracted from replyData body items (supports multiple quotes) */
|
|
247
|
+
replyContext?: string[];
|
|
248
|
+
/** Image download URLs extracted from IMAGE body items (group) or PicUrl (private) */
|
|
249
|
+
imageUrls?: string[];
|
|
250
|
+
/** Sender's IMID (numeric user ID) for reply context */
|
|
251
|
+
senderImid?: string;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Handler parameter types
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
export type HandleInfoflowMessageParams = {
|
|
259
|
+
cfg: import("openclaw/plugin-sdk").OpenClawConfig;
|
|
260
|
+
event: InfoflowMessageEvent;
|
|
261
|
+
accountId: string;
|
|
262
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export type HandlePrivateChatParams = {
|
|
266
|
+
cfg: import("openclaw/plugin-sdk").OpenClawConfig;
|
|
267
|
+
msgData: Record<string, unknown>;
|
|
268
|
+
accountId: string;
|
|
269
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export type HandleGroupChatParams = {
|
|
273
|
+
cfg: import("openclaw/plugin-sdk").OpenClawConfig;
|
|
274
|
+
msgData: Record<string, unknown>;
|
|
275
|
+
accountId: string;
|
|
276
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
277
|
+
};
|