@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/CLAUDE.md +135 -0
  3. package/COLLABORATION_REPORT.md +209 -0
  4. package/PROJECT_GUIDE.md +355 -0
  5. package/README.md +158 -66
  6. package/docs/dev-guide.md +63 -50
  7. package/docs/qa-feature-list.md +452 -0
  8. package/docs/webhook-guide.md +178 -0
  9. package/index.ts +28 -2
  10. package/openclaw.plugin.json +131 -21
  11. package/package.json +16 -3
  12. package/scripts/deploy.sh +66 -7
  13. package/scripts/postinstall.cjs +80 -0
  14. package/skills/infoflow-dev/SKILL.md +2 -2
  15. package/skills/infoflow-dev/references/api.md +1 -1
  16. package/src/adapter/inbound/webhook-parser.ts +27 -5
  17. package/src/adapter/inbound/ws-receiver.ts +304 -43
  18. package/src/adapter/outbound/markdown-local-images.ts +80 -0
  19. package/src/adapter/outbound/reply-dispatcher.ts +146 -65
  20. package/src/adapter/outbound/target-resolver.ts +4 -3
  21. package/src/channel/accounts.ts +97 -22
  22. package/src/channel/channel.ts +456 -12
  23. package/src/channel/media.ts +20 -6
  24. package/src/channel/monitor.ts +8 -3
  25. package/src/channel/outbound.ts +358 -21
  26. package/src/channel/streaming.ts +740 -0
  27. package/src/commands/changelog.ts +80 -0
  28. package/src/commands/doctor.ts +545 -0
  29. package/src/commands/logs.ts +449 -0
  30. package/src/commands/version.ts +20 -0
  31. package/src/compat/openclaw-sdk.ts +218 -0
  32. package/src/handler/message-handler.ts +673 -166
  33. package/src/logging.ts +1 -1
  34. package/src/runtime.ts +1 -1
  35. package/src/security/dm-policy.ts +1 -4
  36. package/src/security/group-policy.ts +174 -51
  37. package/src/tools/actions/index.ts +15 -13
  38. package/src/tools/cron/relay.ts +1154 -0
  39. package/src/tools/hooks/index.ts +13 -1
  40. package/src/tools/index.ts +714 -32
  41. package/src/types.ts +144 -25
  42. package/src/utils/audio/g722/dct_tables.ts +381 -0
  43. package/src/utils/audio/g722/decoder.ts +919 -0
  44. package/src/utils/audio/g722/defs.ts +105 -0
  45. package/src/utils/audio/g722/hd-parser.ts +247 -0
  46. package/src/utils/audio/g722/huff_tables.ts +240 -0
  47. package/src/utils/audio/g722/index.ts +78 -0
  48. package/src/utils/audio/g722/output_decoded.pcm +0 -0
  49. package/src/utils/audio/g722/output_decoded.wav +0 -0
  50. package/src/utils/audio/g722/tables.ts +173 -0
  51. package/src/utils/audio/g722/test_api.ts +31 -0
  52. package/src/utils/audio/g722/test_voice.hd +0 -0
  53. package/src/utils/bos/im-bos-client.ts +219 -0
  54. package/src/utils/group-agent-cache.ts +142 -0
  55. 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
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
@@ -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
- * Matches by robotName against the AT item's display name (case-insensitive).
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(bodyItems: InfoflowBodyItem[], robotName?: string): boolean {
41
- if (!robotName) return false;
42
- const normalizedRobotName = robotName.toLowerCase();
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
- if (item.name?.toLowerCase() === normalizedRobotName) return true;
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: InfoflowBodyItem[],
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
- /** Check if message content matches the configured watchRegex regex pattern */
86
- export function checkWatchRegex(mes: string, pattern: string): boolean {
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
- return new RegExp(pattern, "i").test(mes);
89
- } catch {
90
- return false;
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: InfoflowBodyItem[],
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
- "## Can answer or help Reply directly",
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 if ANY of these apply:",
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
- "## Cannot answer Reply with NO_REPLY only",
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
- "Do NOT reply if ANY of these apply:",
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
- "# Response format",
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
- "- When you can answer: give a direct, concise answer. Do not explain why you chose to answer.",
211
- "- When you cannot answer: output only NO_REPLY with no other text.",
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
- return [
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
- "First determine if this message is a follow-up or continuation of the same topic you previously replied to, then decide if you can continue to help.",
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
- "Note: If this message is clearly a new topic or unrelated to your previous reply, respond with NO_REPLY.",
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
- ].join("\n");
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, ChannelMessageActionName } from "openclaw/plugin-sdk";
8
- import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk";
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
- listActions: (): ChannelMessageActionName[] => ["send", "delete"],
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(`[DEBUG actions:send] action=send, to=${to}, message="${message}", mediaUrl="${mediaUrl}"`);
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 (group only)
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 && isGroup
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({