@core-workspace/infoflow-openclaw-plugin 2026.3.9 → 2026.3.27-beta.0
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 +91 -0
- package/CLAUDE.md +135 -0
- package/COLLABORATION_REPORT.md +209 -0
- package/PROJECT_GUIDE.md +355 -0
- package/README.md +158 -66
- package/docs/dev-guide.md +63 -50
- package/docs/qa-feature-list.md +452 -0
- package/docs/webhook-guide.md +178 -0
- package/index.ts +28 -2
- package/openclaw.plugin.json +131 -21
- package/package.json +16 -3
- package/scripts/deploy.sh +66 -7
- package/scripts/postinstall.cjs +80 -0
- package/skills/infoflow-dev/SKILL.md +2 -2
- package/skills/infoflow-dev/references/api.md +1 -1
- package/src/adapter/inbound/webhook-parser.ts +27 -5
- package/src/adapter/inbound/ws-receiver.ts +304 -43
- package/src/adapter/outbound/markdown-local-images.ts +80 -0
- package/src/adapter/outbound/reply-dispatcher.ts +146 -65
- package/src/adapter/outbound/target-resolver.ts +4 -3
- package/src/channel/accounts.ts +97 -22
- package/src/channel/channel.ts +456 -12
- package/src/channel/media.ts +20 -6
- package/src/channel/monitor.ts +8 -3
- package/src/channel/outbound.ts +358 -21
- package/src/channel/streaming.ts +740 -0
- package/src/commands/changelog.ts +80 -0
- package/src/commands/doctor.ts +545 -0
- package/src/commands/logs.ts +449 -0
- package/src/commands/version.ts +20 -0
- package/src/compat/openclaw-sdk.ts +218 -0
- package/src/handler/message-handler.ts +673 -166
- package/src/logging.ts +1 -1
- package/src/runtime.ts +1 -1
- package/src/security/dm-policy.ts +1 -4
- package/src/security/group-policy.ts +174 -51
- package/src/tools/actions/index.ts +15 -13
- package/src/tools/cron/relay.ts +1154 -0
- package/src/tools/hooks/index.ts +13 -1
- package/src/tools/index.ts +714 -32
- package/src/types.ts +144 -25
- package/src/utils/audio/g722/dct_tables.ts +381 -0
- package/src/utils/audio/g722/decoder.ts +919 -0
- package/src/utils/audio/g722/defs.ts +105 -0
- package/src/utils/audio/g722/hd-parser.ts +247 -0
- package/src/utils/audio/g722/huff_tables.ts +240 -0
- package/src/utils/audio/g722/index.ts +78 -0
- package/src/utils/audio/g722/output_decoded.pcm +0 -0
- package/src/utils/audio/g722/output_decoded.wav +0 -0
- package/src/utils/audio/g722/tables.ts +173 -0
- package/src/utils/audio/g722/test_api.ts +31 -0
- package/src/utils/audio/g722/test_voice.hd +0 -0
- package/src/utils/bos/im-bos-client.ts +219 -0
- package/src/utils/group-agent-cache.ts +142 -0
- package/src/utils/token-adapter.ts +120 -51
package/src/logging.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Provides consistent logging interface across all Infoflow modules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { RuntimeLogger } from "openclaw/plugin-sdk";
|
|
6
|
+
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
|
|
7
7
|
import { getInfoflowRuntime } from "./runtime.js";
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
package/src/runtime.ts
CHANGED
|
@@ -21,10 +21,7 @@ export type DmPolicyResult =
|
|
|
21
21
|
* - "pairing": allowed (pairing handled by the framework), note returned
|
|
22
22
|
* - "allowlist": only senders in allowFrom are allowed
|
|
23
23
|
*/
|
|
24
|
-
export function checkDmPolicy(
|
|
25
|
-
account: ResolvedInfoflowAccount,
|
|
26
|
-
fromuser: string,
|
|
27
|
-
): DmPolicyResult {
|
|
24
|
+
export function checkDmPolicy(account: ResolvedInfoflowAccount, fromuser: string): DmPolicyResult {
|
|
28
25
|
const dmPolicy = account.config.dmPolicy ?? "open";
|
|
29
26
|
|
|
30
27
|
if (dmPolicy === "allowlist") {
|
|
@@ -4,56 +4,62 @@
|
|
|
4
4
|
* follow-up window tracking, and conditional-reply prompt builders.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { getInfoflowBotLog } from "../logging.js";
|
|
7
8
|
import type {
|
|
9
|
+
InfoflowInboundBodyItem,
|
|
8
10
|
InfoflowReplyMode,
|
|
9
11
|
InfoflowGroupConfig,
|
|
12
|
+
InfoflowGroupSessionMode,
|
|
10
13
|
InfoflowMentionIds,
|
|
11
14
|
ResolvedInfoflowAccount,
|
|
12
15
|
} from "../types.js";
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Body item type (inbound group messages)
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export type InfoflowBodyItem = {
|
|
19
|
-
type?: string;
|
|
20
|
-
content?: string;
|
|
21
|
-
label?: string;
|
|
22
|
-
/** 机器人 AT 时有此字段(数字),与 userid 互斥 */
|
|
23
|
-
robotid?: number;
|
|
24
|
-
/** AT 元素的显示名称 */
|
|
25
|
-
name?: string;
|
|
26
|
-
/** 人类用户 AT 时有此字段(uuap name),与 robotid 互斥 */
|
|
27
|
-
userid?: string;
|
|
28
|
-
/** IMAGE 类型 body item 的图片下载地址 */
|
|
29
|
-
downloadurl?: string;
|
|
30
|
-
};
|
|
16
|
+
import { findSentMessage } from "../utils/store/message-store.js";
|
|
31
17
|
|
|
32
18
|
// ---------------------------------------------------------------------------
|
|
33
19
|
// @mention detection
|
|
34
20
|
// ---------------------------------------------------------------------------
|
|
35
21
|
|
|
36
22
|
/**
|
|
37
|
-
* Check if the bot was @mentioned in the message body.
|
|
38
|
-
*
|
|
23
|
+
* Check if the current bot was @mentioned in the message body.
|
|
24
|
+
*
|
|
25
|
+
* Priority order per AT item (only checks fields present in both config and item):
|
|
26
|
+
* 1. appAgentId vs item.robotid (string comparison)
|
|
27
|
+
* 2. robotName vs item.name (display name, case-insensitive)
|
|
28
|
+
* 3. robotId vs item.robotid (string comparison)
|
|
29
|
+
*
|
|
30
|
+
* All robotid comparisons use string form to avoid precision loss on large IDs.
|
|
39
31
|
*/
|
|
40
|
-
export function checkBotMentioned(
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
export function checkBotMentioned(
|
|
33
|
+
bodyItems: InfoflowInboundBodyItem[],
|
|
34
|
+
botIdentity: { robotName?: string; appAgentId?: number; robotId?: string },
|
|
35
|
+
): boolean {
|
|
36
|
+
const { robotName, appAgentId, robotId } = botIdentity;
|
|
37
|
+
const appAgentIdStr = appAgentId != null ? String(appAgentId) : undefined;
|
|
43
38
|
for (const item of bodyItems) {
|
|
44
39
|
if (item.type !== "AT") continue;
|
|
45
|
-
|
|
40
|
+
// 1. appAgentId vs item.robotid
|
|
41
|
+
if (appAgentIdStr && item.robotid != null && String(item.robotid) === appAgentIdStr)
|
|
42
|
+
return true;
|
|
43
|
+
// 2. robotName vs item.name (display name, case-insensitive)
|
|
44
|
+
if (robotName && item.name && item.name.toLowerCase() === robotName.toLowerCase()) return true;
|
|
45
|
+
// 3. robotId vs item.robotid
|
|
46
|
+
if (robotId && item.robotid != null && String(item.robotid) === robotId) return true;
|
|
46
47
|
}
|
|
47
48
|
return false;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/** Returns true if the message body contains an AT item for any bot. */
|
|
52
|
+
export function hasAnyRobotMention(bodyItems: InfoflowInboundBodyItem[]): boolean {
|
|
53
|
+
return bodyItems.some((item) => item.type === "AT" && item.robotid != null);
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
/**
|
|
51
57
|
* Check if any entry in the watchlist was @mentioned in the message body.
|
|
52
58
|
* Matching priority: userid > robotid (parsed as number) > name (fallback).
|
|
53
59
|
* Returns the matched ID (from watchMentions), or undefined if none matched.
|
|
54
60
|
*/
|
|
55
61
|
export function checkWatchMentioned(
|
|
56
|
-
bodyItems:
|
|
62
|
+
bodyItems: InfoflowInboundBodyItem[],
|
|
57
63
|
watchMentions: string[],
|
|
58
64
|
): string | undefined {
|
|
59
65
|
if (!watchMentions.length) return undefined;
|
|
@@ -82,13 +88,34 @@ export function checkWatchMentioned(
|
|
|
82
88
|
return undefined;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
// Cache compiled regexes to avoid recompilation on every message
|
|
92
|
+
const regexCache = new Map<string, RegExp | null>();
|
|
93
|
+
const MAX_REGEX_CACHE = 256;
|
|
94
|
+
|
|
95
|
+
function getCachedRegex(pattern: string): RegExp | null {
|
|
96
|
+
let re = regexCache.get(pattern);
|
|
97
|
+
if (re !== undefined) return re;
|
|
87
98
|
try {
|
|
88
|
-
|
|
89
|
-
} catch {
|
|
90
|
-
|
|
99
|
+
re = new RegExp(pattern, "is");
|
|
100
|
+
} catch (err) {
|
|
101
|
+
getInfoflowBotLog().warn(
|
|
102
|
+
`Invalid watchRegex pattern "${pattern}", skipping: ${err instanceof Error ? err.message : String(err)}`,
|
|
103
|
+
);
|
|
104
|
+
re = null;
|
|
105
|
+
}
|
|
106
|
+
if (regexCache.size >= MAX_REGEX_CACHE) regexCache.clear();
|
|
107
|
+
regexCache.set(pattern, re);
|
|
108
|
+
return re;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Check if message content matches any configured watchRegex pattern. Returns matched pattern or undefined. */
|
|
112
|
+
export function checkWatchRegex(mes: string, patterns: string | string[]): string | undefined {
|
|
113
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
114
|
+
for (const pattern of list) {
|
|
115
|
+
const re = getCachedRegex(pattern);
|
|
116
|
+
if (re?.test(mes)) return pattern;
|
|
91
117
|
}
|
|
118
|
+
return undefined;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
/**
|
|
@@ -96,10 +123,12 @@ export function checkWatchRegex(mes: string, pattern: string): boolean {
|
|
|
96
123
|
* Returns human userIds and robot agentIds (excluding the bot itself).
|
|
97
124
|
*/
|
|
98
125
|
export function extractMentionIds(
|
|
99
|
-
bodyItems:
|
|
126
|
+
bodyItems: InfoflowInboundBodyItem[],
|
|
100
127
|
robotName?: string,
|
|
128
|
+
robotId?: string,
|
|
101
129
|
): InfoflowMentionIds {
|
|
102
130
|
const normalizedRobotName = robotName?.toLowerCase();
|
|
131
|
+
const normalizedRobotId = robotId?.trim();
|
|
103
132
|
const userIds: string[] = [];
|
|
104
133
|
const agentIds: number[] = [];
|
|
105
134
|
const seenUsers = new Set<string>();
|
|
@@ -109,6 +138,7 @@ export function extractMentionIds(
|
|
|
109
138
|
if (item.type !== "AT") continue;
|
|
110
139
|
|
|
111
140
|
if (item.robotid != null) {
|
|
141
|
+
if (normalizedRobotId && String(item.robotid) === normalizedRobotId) continue;
|
|
112
142
|
if (normalizedRobotName && item.name?.toLowerCase() === normalizedRobotName) continue;
|
|
113
143
|
if (!seenAgents.has(item.robotid)) {
|
|
114
144
|
seenAgents.add(item.robotid);
|
|
@@ -125,6 +155,60 @@ export function extractMentionIds(
|
|
|
125
155
|
return { userIds, agentIds };
|
|
126
156
|
}
|
|
127
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Extract the bot's own robotid from AT items in the message body.
|
|
160
|
+
* When the bot is @mentioned, the AT item contains the bot's actual numeric robotid.
|
|
161
|
+
* Returns the robotid string if found, undefined otherwise.
|
|
162
|
+
*/
|
|
163
|
+
export function getBotRobotidFromBody(
|
|
164
|
+
bodyItems: InfoflowInboundBodyItem[],
|
|
165
|
+
robotName?: string,
|
|
166
|
+
robotId?: string,
|
|
167
|
+
): string | undefined {
|
|
168
|
+
const normalizedRobotName = robotName?.toLowerCase();
|
|
169
|
+
const normalizedRobotId = robotId?.trim();
|
|
170
|
+
for (const item of bodyItems) {
|
|
171
|
+
if (item.type !== "AT") continue;
|
|
172
|
+
if (normalizedRobotId && item.robotid != null && String(item.robotid) === normalizedRobotId) {
|
|
173
|
+
return normalizedRobotId;
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
normalizedRobotName &&
|
|
177
|
+
item.name?.toLowerCase() === normalizedRobotName &&
|
|
178
|
+
item.robotid != null
|
|
179
|
+
) {
|
|
180
|
+
return String(item.robotid);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if a message is a quoted reply to one of the bot's own previously sent messages.
|
|
188
|
+
* Looks up replyData body items' messageid in the sent-message store.
|
|
189
|
+
*/
|
|
190
|
+
export function checkReplyToBot(bodyItems: InfoflowInboundBodyItem[], accountId: string): boolean {
|
|
191
|
+
for (const item of bodyItems) {
|
|
192
|
+
if (item.type !== "replyData") continue;
|
|
193
|
+
const msgId = item.messageid;
|
|
194
|
+
if (msgId == null) continue;
|
|
195
|
+
try {
|
|
196
|
+
if (findSentMessage(accountId, msgId)) return true;
|
|
197
|
+
} catch {
|
|
198
|
+
// DB error — gracefully continue
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if the mention IDs contain any non-bot mentions (other users or other robots).
|
|
206
|
+
*/
|
|
207
|
+
export function hasOtherMentions(mentionIds?: InfoflowMentionIds): boolean {
|
|
208
|
+
if (!mentionIds) return false;
|
|
209
|
+
return mentionIds.userIds.length > 0 || mentionIds.agentIds.length > 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
128
212
|
// ---------------------------------------------------------------------------
|
|
129
213
|
// Follow-up window tracking (in-memory)
|
|
130
214
|
// ---------------------------------------------------------------------------
|
|
@@ -150,10 +234,11 @@ export function isWithinFollowUpWindow(groupId: string, windowSeconds: number):
|
|
|
150
234
|
|
|
151
235
|
export type ResolvedGroupConfig = {
|
|
152
236
|
replyMode: InfoflowReplyMode;
|
|
237
|
+
groupSessionMode: InfoflowGroupSessionMode;
|
|
153
238
|
followUp: boolean;
|
|
154
239
|
followUpWindow: number;
|
|
155
240
|
watchMentions: string[];
|
|
156
|
-
watchRegex?: string;
|
|
241
|
+
watchRegex?: string | string[];
|
|
157
242
|
systemPrompt?: string;
|
|
158
243
|
};
|
|
159
244
|
|
|
@@ -175,6 +260,7 @@ export function resolveGroupConfig(
|
|
|
175
260
|
groupId != null ? account.config.groups?.[String(groupId)] : undefined;
|
|
176
261
|
return {
|
|
177
262
|
replyMode: groupCfg?.replyMode ?? account.config.replyMode ?? inferLegacyReplyMode(account),
|
|
263
|
+
groupSessionMode: groupCfg?.groupSessionMode ?? account.config.groupSessionMode ?? "group",
|
|
178
264
|
followUp: groupCfg?.followUp ?? account.config.followUp ?? true,
|
|
179
265
|
followUpWindow: groupCfg?.followUpWindow ?? account.config.followUpWindow ?? 300,
|
|
180
266
|
watchMentions: groupCfg?.watchMentions ?? account.config.watchMentions ?? [],
|
|
@@ -189,26 +275,32 @@ export function resolveGroupConfig(
|
|
|
189
275
|
|
|
190
276
|
function buildReplyJudgmentRules(): string {
|
|
191
277
|
return [
|
|
192
|
-
"# Rules",
|
|
278
|
+
"# Rules for Group Message Response",
|
|
279
|
+
"",
|
|
280
|
+
"## When to Reply",
|
|
193
281
|
"",
|
|
194
|
-
"
|
|
282
|
+
"Reply if ANY of the following is true:",
|
|
283
|
+
"- The message is directed at you — either by explicit mention, or by contextual signals suggesting the user expects your response (e.g., a question following your previous reply, a topic clearly within your role, or conversational flow implying you are the intended recipient)",
|
|
284
|
+
"- The message contains a clear question or request that you can answer using your knowledge, skills, tools, or reasoning",
|
|
285
|
+
"- You have relevant domain expertise, documentation, or codebase context that adds value",
|
|
195
286
|
"",
|
|
196
|
-
"Reply
|
|
197
|
-
"- The question can be answered through common sense or logical reasoning (e.g. math, general knowledge)",
|
|
198
|
-
"- You can find relevant clues or content in your knowledge base, documentation, or code",
|
|
199
|
-
"- You have sufficient domain expertise to provide a valuable reference",
|
|
287
|
+
"## When NOT to Reply — output only `NO_REPLY`",
|
|
200
288
|
"",
|
|
201
|
-
"
|
|
289
|
+
"Do NOT reply if ANY of the following is true:",
|
|
290
|
+
"- The message is casual chatter, banter, emoji-only, or has no actionable question/request",
|
|
291
|
+
"- The user explicitly indicates they don't want your response",
|
|
292
|
+
"- The message is directed at another person, not at you",
|
|
293
|
+
"- You lack the context or knowledge to give a useful answer (e.g., private/internal info you don't have access to)",
|
|
294
|
+
"- The message intent is ambiguous and a wrong guess would be more disruptive than silence",
|
|
202
295
|
"",
|
|
203
|
-
"
|
|
204
|
-
"- The message contains no clear question or request (e.g. casual chat, meaningless content)",
|
|
205
|
-
"- The question involves private information or context you have no knowledge of",
|
|
206
|
-
"- You cannot understand the core intent of the message",
|
|
296
|
+
"## Response Format",
|
|
207
297
|
"",
|
|
208
|
-
"
|
|
298
|
+
"- If you can answer: respond directly and concisely. Do not explain why you chose to answer. Do not add filler or pleasantries.",
|
|
299
|
+
"- If you cannot answer: output exactly `NO_REPLY` — nothing else, no explanation, no apology.",
|
|
209
300
|
"",
|
|
210
|
-
"
|
|
211
|
-
"
|
|
301
|
+
"## Guiding Principle",
|
|
302
|
+
"",
|
|
303
|
+
"When in doubt, prefer silence (`NO_REPLY`). A missing reply is far less disruptive than an irrelevant or incorrect one in a group chat.",
|
|
212
304
|
].join("\n");
|
|
213
305
|
}
|
|
214
306
|
|
|
@@ -249,15 +341,46 @@ export function buildWatchRegexPrompt(pattern: string): string {
|
|
|
249
341
|
}
|
|
250
342
|
|
|
251
343
|
/** GroupSystemPrompt for follow-up replies after bot's last response */
|
|
252
|
-
export function buildFollowUpPrompt(): string {
|
|
253
|
-
|
|
344
|
+
export function buildFollowUpPrompt(isReplyToBot?: boolean): string {
|
|
345
|
+
const lines: string[] = [
|
|
254
346
|
"You just replied to a message in this group. Someone has now sent a new message.",
|
|
255
|
-
"
|
|
347
|
+
"Follow the priority rules below **in order** to decide whether to reply.",
|
|
348
|
+
"",
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
if (isReplyToBot) {
|
|
352
|
+
lines.push(
|
|
353
|
+
"**Important context: this message is a quoted reply to your previous message. This is a strong signal that the user is following up with you.**",
|
|
354
|
+
"",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
lines.push(
|
|
359
|
+
"# Priority 1: The sender intends to talk to you → MUST reply",
|
|
256
360
|
"",
|
|
257
|
-
"
|
|
361
|
+
"Based on semantic analysis, if the sender shows ANY of the following intents or expectations, you **MUST** reply (do NOT output NO_REPLY):",
|
|
362
|
+
"- Asking a follow-up question about your previous answer (e.g. 'why?', 'what else?', 'what if...?')",
|
|
363
|
+
"- Quoted/replied to your message (indicating a conversation with you)",
|
|
364
|
+
"- Addressing you by name, or using words like 'bot', 'assistant', etc.",
|
|
365
|
+
"- Requesting you to do something (e.g. 'help me...', 'explain...', 'translate...')",
|
|
366
|
+
"- Semantically expects a reply from you",
|
|
367
|
+
"",
|
|
368
|
+
"# Priority 2: Explicitly asking you to stop → MUST NOT reply",
|
|
369
|
+
"",
|
|
370
|
+
"If the message explicitly tells you to stop replying (e.g. 'shut up', 'stop', 'don't reply',",
|
|
371
|
+
"'no need for bot', or equivalent expressions in any language),",
|
|
372
|
+
"output only NO_REPLY.",
|
|
373
|
+
"",
|
|
374
|
+
"# Priority 3: No explicit intent → Judge topic continuity",
|
|
375
|
+
"",
|
|
376
|
+
"If neither Priority 1 nor Priority 2 applies:",
|
|
377
|
+
"- If the message continues the same topic you previously replied to, and you can provide valuable help → reply.",
|
|
378
|
+
"- If it is a new/unrelated topic, or you cannot add value → output only NO_REPLY.",
|
|
258
379
|
"",
|
|
259
380
|
buildReplyJudgmentRules(),
|
|
260
|
-
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return lines.join("\n");
|
|
261
384
|
}
|
|
262
385
|
|
|
263
386
|
/** GroupSystemPrompt for proactive mode */
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @all and @user mentions in group messages.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ChannelMessageActionAdapter
|
|
8
|
-
import { extractToolSend, jsonResult, readStringParam } from "openclaw
|
|
7
|
+
import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
|
|
8
|
+
import { extractToolSend, jsonResult, readStringParam } from "../../compat/openclaw-sdk.js";
|
|
9
|
+
import { normalizeInfoflowTarget } from "../../adapter/outbound/target-resolver.js";
|
|
9
10
|
import { resolveInfoflowAccount } from "../../channel/accounts.js";
|
|
10
|
-
import { logVerbose } from "../../logging.js";
|
|
11
11
|
import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "../../channel/media.js";
|
|
12
12
|
import {
|
|
13
13
|
sendInfoflowMessage,
|
|
@@ -15,13 +15,13 @@ import {
|
|
|
15
15
|
recallInfoflowPrivateMessage,
|
|
16
16
|
isLikelyLocalPath,
|
|
17
17
|
} from "../../channel/outbound.js";
|
|
18
|
+
import { logVerbose } from "../../logging.js";
|
|
19
|
+
import type { InfoflowMessageContentItem, InfoflowOutboundReply } from "../../types.js";
|
|
18
20
|
import {
|
|
19
21
|
findSentMessage,
|
|
20
22
|
querySentMessages,
|
|
21
23
|
removeRecalledMessages,
|
|
22
24
|
} from "../../utils/store/message-store.js";
|
|
23
|
-
import { normalizeInfoflowTarget } from "../../adapter/outbound/target-resolver.js";
|
|
24
|
-
import type { InfoflowMessageContentItem, InfoflowOutboundReply } from "../../types.js";
|
|
25
25
|
|
|
26
26
|
// Recall result hint constants — reused across single/batch, group/private recall paths
|
|
27
27
|
const RECALL_OK_HINT = "Recall succeeded. output only NO_REPLY with no other text.";
|
|
@@ -30,12 +30,12 @@ const RECALL_PARTIAL_HINT =
|
|
|
30
30
|
"Some recalls failed. Send a brief reply stating only the failure reason(s).";
|
|
31
31
|
|
|
32
32
|
export const infoflowMessageActions: ChannelMessageActionAdapter = {
|
|
33
|
-
|
|
33
|
+
describeMessageTool: () => ({ actions: ["send", "delete"] }),
|
|
34
34
|
|
|
35
35
|
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
38
|
-
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
39
|
// delete (群消息撤回) — Mode A: by messageId, Mode B: by count
|
|
40
40
|
// -----------------------------------------------------------------------
|
|
41
41
|
if (action === "delete") {
|
|
@@ -313,7 +313,9 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
|
|
|
313
313
|
const mediaUrl = readStringParam(params, "media", { trim: false });
|
|
314
314
|
|
|
315
315
|
// Log sendMessage action call
|
|
316
|
-
logVerbose(
|
|
316
|
+
logVerbose(
|
|
317
|
+
`[DEBUG actions:send] action=send, to=${to}, message="${message}", mediaUrl="${mediaUrl}"`,
|
|
318
|
+
);
|
|
317
319
|
|
|
318
320
|
// Infoflow-specific mention params
|
|
319
321
|
const atAll = params.atAll === true || params.atAll === "true";
|
|
@@ -322,12 +324,12 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
|
|
|
322
324
|
const isGroup = /^group:\d+$/i.test(to);
|
|
323
325
|
const contents: InfoflowMessageContentItem[] = [];
|
|
324
326
|
|
|
325
|
-
// Infoflow reply-to params
|
|
327
|
+
// Infoflow reply-to params
|
|
326
328
|
const replyToMessageId = readStringParam(params, "replyToMessageId");
|
|
327
329
|
const replyToPreview = readStringParam(params, "replyToPreview");
|
|
328
330
|
const replyTypeRaw = readStringParam(params, "replyType");
|
|
329
331
|
const replyTo: InfoflowOutboundReply | undefined =
|
|
330
|
-
replyToMessageId
|
|
332
|
+
replyToMessageId
|
|
331
333
|
? {
|
|
332
334
|
messageid: replyToMessageId,
|
|
333
335
|
preview: replyToPreview ?? undefined,
|
|
@@ -389,8 +391,8 @@ export const infoflowMessageActions: ChannelMessageActionAdapter = {
|
|
|
389
391
|
|
|
390
392
|
// Try native image send, fallback to link
|
|
391
393
|
try {
|
|
392
|
-
const paths = isLikelyLocalPath(mediaUrl)?[mediaUrl]:undefined;
|
|
393
|
-
const prepared = await prepareInfoflowImageBase64({ mediaUrl, mediaLocalRoots:paths });
|
|
394
|
+
const paths = isLikelyLocalPath(mediaUrl) ? [mediaUrl] : undefined;
|
|
395
|
+
const prepared = await prepareInfoflowImageBase64({ mediaUrl, mediaLocalRoots: paths });
|
|
394
396
|
logVerbose(`prepareInfoflowImageBase64 result: isImage=${prepared.isImage}`);
|
|
395
397
|
if (prepared.isImage) {
|
|
396
398
|
const imgResult = await sendInfoflowImageMessage({
|