@chbo297/infoflow 2026.3.6 → 2026.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chbo297/infoflow",
3
- "version": "2026.3.6",
3
+ "version": "2026.3.7",
4
4
  "description": "OpenClaw Infoflow (如流) channel plugin for Baidu enterprise messaging",
5
5
  "type": "module",
6
6
  "main": "index.ts",
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,
@@ -262,7 +254,6 @@ type ResolvedGroupConfig = {
262
254
  followUpWindow: number;
263
255
  watchMentions: string[];
264
256
  systemPrompt?: string;
265
- thinkingIndicator: boolean;
266
257
  };
267
258
 
268
259
  /** Infer replyMode from legacy requireMention + watchMentions fields */
@@ -287,104 +278,9 @@ function resolveGroupConfig(
287
278
  followUpWindow: groupCfg?.followUpWindow ?? account.config.followUpWindow ?? 300,
288
279
  watchMentions: groupCfg?.watchMentions ?? account.config.watchMentions ?? [],
289
280
  systemPrompt: groupCfg?.systemPrompt,
290
- thinkingIndicator: groupCfg?.thinkingIndicator ?? account.config.thinkingIndicator ?? true,
291
281
  };
292
282
  }
293
283
 
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
284
  /**
389
285
  * Handles an incoming private chat message from Infoflow.
390
286
  * Receives the raw decrypted message data and dispatches to the agent.
@@ -886,21 +782,6 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
886
782
  // Build unified target: "group:<id>" for group chat, username for private chat
887
783
  const to = isGroup && groupId !== undefined ? `group:${groupId}` : fromuser;
888
784
 
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
785
  // Provide mention context to the LLM so it can decide who to @mention
905
786
  if (isGroup && event.mentionIds) {
906
787
  const parts: string[] = [];
@@ -930,48 +811,12 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
930
811
  replyToPreview: isGroup ? mes : undefined,
931
812
  });
932
813
 
933
- // Wrap dispatcher to recall thinking indicator before first delivery
934
- const canRecallThinking = Boolean(thinkingHandle);
935
- let thinkingRecalled = false;
936
- const doRecallThinking = async () => {
937
- if (thinkingRecalled || !canRecallThinking) return;
938
- thinkingRecalled = true;
939
- await recallThinkingIndicator({
940
- cfg,
941
- accountId: account.accountId,
942
- handle: thinkingHandle!,
943
- groupId: isGroup ? groupId : undefined,
944
- isPrivate: !isGroup,
945
- });
946
- };
947
-
948
- const originalDeliver = dispatcherOptions.deliver;
949
- const wrappedDispatcherOptions = {
950
- ...dispatcherOptions,
951
- deliver: async (payload: ReplyPayload) => {
952
- await doRecallThinking();
953
- return originalDeliver(payload);
954
- },
955
- onCleanup: () => {
956
- void doRecallThinking();
957
- },
958
- };
959
-
960
- // Wrap dispatch in try/finally to guarantee the thinking indicator bound to
961
- // this message is always recalled — even when queue policy drops/enqueues the
962
- // message before typing activates (typing.cleanup skips onCleanup when inactive).
963
- // doRecallThinking is idempotent (thinkingRecalled flag), so duplicate calls are no-ops.
964
- let dispatchResult;
965
- try {
966
- dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
967
- ctx: ctxPayload,
968
- cfg,
969
- dispatcherOptions: wrappedDispatcherOptions,
970
- replyOptions,
971
- });
972
- } finally {
973
- await doRecallThinking();
974
- }
814
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
815
+ ctx: ctxPayload,
816
+ cfg,
817
+ dispatcherOptions,
818
+ replyOptions,
819
+ });
975
820
 
976
821
  const didReply = dispatchResult?.queuedFinal ?? false;
977
822
 
package/src/channel.ts CHANGED
@@ -52,6 +52,7 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
52
52
  agentPrompt: {
53
53
  messageToolHints: () => [
54
54
  'Infoflow group @mentions: set atAll=true to @all members, or mentionUserIds="user1,user2" (comma-separated uuapName) to @mention specific users. Only effective for group targets (group:<id>).',
55
+ 'Infoflow supports message recall (撤回): use action="delete" to recall the most recent message, or specify messageId to recall a specific message. Works for both private and group messages.',
55
56
  ],
56
57
  },
57
58
  config: {
@@ -61,9 +61,9 @@ function isDuplicateMessage(msgData: Record<string, unknown>): boolean {
61
61
  * Called after successfully sending a message to prevent
62
62
  * the bot from processing its own outbound messages as inbound.
63
63
  */
64
- export function recordSentMessageId(messageId: string | number): void {
64
+ export function recordSentMessageId(messageId: string | null): void {
65
65
  if (messageId == null) return;
66
- messageCache.check(String(messageId)); // Will record if not duplicate
66
+ messageCache.check(messageId);
67
67
  }
68
68
 
69
69
  // ---------------------------------------------------------------------------
package/src/media.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  INFOFLOW_PRIVATE_SEND_PATH,
16
16
  INFOFLOW_GROUP_SEND_PATH,
17
17
  } from "./send.js";
18
- import { recordSentMessage } from "./sent-message-store.js";
18
+ import { recordSentMessage, buildAgentFrom } from "./sent-message-store.js";
19
19
  import type { ResolvedInfoflowAccount, InfoflowOutboundReply } from "./types.js";
20
20
 
21
21
  /** Infoflow API image size limit: 1MB raw bytes */
@@ -225,6 +225,7 @@ export async function sendInfoflowGroupImage(params: {
225
225
  try {
226
226
  recordSentMessage(account.accountId, {
227
227
  target: `group:${groupId}`,
228
+ from: buildAgentFrom(account.config.appAgentId),
228
229
  messageid,
229
230
  msgseqid: msgseqid ?? "",
230
231
  digest: "image",
@@ -313,6 +314,7 @@ export async function sendInfoflowPrivateImage(params: {
313
314
  try {
314
315
  recordSentMessage(account.accountId, {
315
316
  target: toUser,
317
+ from: buildAgentFrom(account.config.appAgentId),
316
318
  messageid: msgkey,
317
319
  msgseqid: "",
318
320
  digest: "image",
package/src/send.ts CHANGED
@@ -8,7 +8,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
8
8
  import { resolveInfoflowAccount } from "./accounts.js";
9
9
  import { recordSentMessageId } from "./infoflow-req-parse.js";
10
10
  import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "./logging.js";
11
- import { recordSentMessage, buildMessageDigest } from "./sent-message-store.js";
11
+ import { recordSentMessage, buildMessageDigest, buildAgentFrom } from "./sent-message-store.js";
12
12
  import type {
13
13
  InfoflowGroupMessageBodyItem,
14
14
  InfoflowMessageContentItem,
@@ -375,6 +375,7 @@ export async function sendInfoflowPrivateMessage(params: {
375
375
  try {
376
376
  recordSentMessage(account.accountId, {
377
377
  target: toUser,
378
+ from: buildAgentFrom(account.config.appAgentId),
378
379
  messageid: msgkey,
379
380
  msgseqid: "",
380
381
  digest: buildMessageDigest(contents),
@@ -576,18 +577,18 @@ export async function sendInfoflowGroupMessage(params: {
576
577
  result: { messageid?: string; msgseqid?: string },
577
578
  digestContents: InfoflowMessageContentItem[],
578
579
  ) => {
579
- if (result.messageid) {
580
- try {
581
- recordSentMessage(account.accountId, {
582
- target: `group:${groupId}`,
583
- messageid: result.messageid,
584
- msgseqid: result.msgseqid ?? "",
585
- digest: buildMessageDigest(digestContents),
586
- sentAt: Date.now(),
587
- });
588
- } catch {
589
- // Do not block sending
590
- }
580
+ if (!result.messageid) return;
581
+ try {
582
+ recordSentMessage(account.accountId, {
583
+ target: `group:${groupId}`,
584
+ from: buildAgentFrom(account.config.appAgentId),
585
+ messageid: result.messageid,
586
+ msgseqid: result.msgseqid ?? "",
587
+ digest: buildMessageDigest(digestContents),
588
+ sentAt: Date.now(),
589
+ });
590
+ } catch {
591
+ // Do not block sending
591
592
  }
592
593
  };
593
594
 
@@ -774,7 +775,7 @@ export async function recallInfoflowPrivateMessage(params: {
774
775
 
775
776
  const bodyStr = JSON.stringify({ msgkey, agentid: appAgentId });
776
777
 
777
- logVerbose(`[infoflow:recallPrivate] POST body: ${bodyStr}`);
778
+ logVerbose(`[infoflow:recallPrivate] POST auth: ${tokenResult.token} body: ${bodyStr}`);
778
779
 
779
780
  const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_PRIVATE_RECALL_PATH}`, {
780
781
  method: "POST",
@@ -51,6 +51,7 @@ function getDb(): DatabaseSync {
51
51
  id INTEGER PRIMARY KEY AUTOINCREMENT,
52
52
  account_id TEXT NOT NULL,
53
53
  target TEXT NOT NULL,
54
+ from_id TEXT NOT NULL DEFAULT '',
54
55
  messageid TEXT NOT NULL,
55
56
  msgseqid TEXT NOT NULL DEFAULT '',
56
57
  digest TEXT NOT NULL DEFAULT '',
@@ -62,6 +63,13 @@ function getDb(): DatabaseSync {
62
63
  ON sent_messages(account_id, target, sent_at DESC);
63
64
  `);
64
65
 
66
+ // Migration: add from_id column to existing databases
67
+ try {
68
+ db.exec(`ALTER TABLE sent_messages ADD COLUMN from_id TEXT NOT NULL DEFAULT ''`);
69
+ } catch {
70
+ // Column already exists — ignore
71
+ }
72
+
65
73
  return db;
66
74
  }
67
75
 
@@ -71,6 +79,7 @@ function getDb(): DatabaseSync {
71
79
 
72
80
  export type SentMessageRecord = {
73
81
  target: string;
82
+ from: string;
74
83
  messageid: string;
75
84
  msgseqid: string;
76
85
  digest: string;
@@ -85,11 +94,12 @@ export function recordSentMessage(accountId: string, record: SentMessageRecord):
85
94
  try {
86
95
  const d = getDb();
87
96
  d.prepare(
88
- `INSERT INTO sent_messages (account_id, target, messageid, msgseqid, digest, sent_at)
89
- VALUES (?, ?, ?, ?, ?, ?)`,
97
+ `INSERT INTO sent_messages (account_id, target, from_id, messageid, msgseqid, digest, sent_at)
98
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
90
99
  ).run(
91
100
  accountId,
92
101
  record.target,
102
+ record.from,
93
103
  record.messageid,
94
104
  record.msgseqid,
95
105
  record.digest,
@@ -117,7 +127,7 @@ export function querySentMessages(
117
127
  const d = getDb();
118
128
  const rows = d
119
129
  .prepare(
120
- `SELECT target, messageid, msgseqid, digest, sent_at
130
+ `SELECT target, from_id, messageid, msgseqid, digest, sent_at
121
131
  FROM sent_messages
122
132
  WHERE account_id = ? AND target = ?
123
133
  ORDER BY sent_at DESC
@@ -125,6 +135,7 @@ export function querySentMessages(
125
135
  )
126
136
  .all(accountId, params.target, params.count) as Array<{
127
137
  target: string;
138
+ from_id: string;
128
139
  messageid: string;
129
140
  msgseqid: string;
130
141
  digest: string;
@@ -133,6 +144,7 @@ export function querySentMessages(
133
144
 
134
145
  return rows.map((r) => ({
135
146
  target: r.target,
147
+ from: r.from_id,
136
148
  messageid: r.messageid,
137
149
  msgseqid: r.msgseqid,
138
150
  digest: r.digest,
@@ -150,18 +162,26 @@ export function findSentMessage(
150
162
  const d = getDb();
151
163
  const row = d
152
164
  .prepare(
153
- `SELECT target, messageid, msgseqid, digest, sent_at
165
+ `SELECT target, from_id, messageid, msgseqid, digest, sent_at
154
166
  FROM sent_messages
155
167
  WHERE account_id = ? AND messageid = ?
156
168
  LIMIT 1`,
157
169
  )
158
170
  .get(accountId, messageid) as
159
- | { target: string; messageid: string; msgseqid: string; digest: string; sent_at: number }
171
+ | {
172
+ target: string;
173
+ from_id: string;
174
+ messageid: string;
175
+ msgseqid: string;
176
+ digest: string;
177
+ sent_at: number;
178
+ }
160
179
  | undefined;
161
180
 
162
181
  if (!row) return undefined;
163
182
  return {
164
183
  target: row.target,
184
+ from: row.from_id,
165
185
  messageid: row.messageid,
166
186
  msgseqid: row.msgseqid,
167
187
  digest: row.digest,
@@ -181,6 +201,15 @@ export function removeRecalledMessages(accountId: string, messageids: string[]):
181
201
  ).run(accountId, ...messageids);
182
202
  }
183
203
 
204
+ // ---------------------------------------------------------------------------
205
+ // From-ID builder
206
+ // ---------------------------------------------------------------------------
207
+
208
+ /** Builds a `from` identifier for self-sent (agent) messages. */
209
+ export function buildAgentFrom(appAgentId: number | undefined): string {
210
+ return appAgentId != null ? `agent:${appAgentId}` : "agent:unknown";
211
+ }
212
+
184
213
  // ---------------------------------------------------------------------------
185
214
  // Digest builder
186
215
  // ---------------------------------------------------------------------------
package/src/types.ts CHANGED
@@ -25,8 +25,6 @@ export type InfoflowGroupConfig = {
25
25
  followUp?: boolean;
26
26
  followUpWindow?: number;
27
27
  systemPrompt?: string;
28
- /** Enable thinking indicator ("收到🤔...") before processing (default: true) */
29
- thinkingIndicator?: boolean;
30
28
  };
31
29
 
32
30
  // ---------------------------------------------------------------------------
@@ -120,8 +118,6 @@ export type InfoflowAccountConfig = {
120
118
  followUp?: boolean;
121
119
  /** Follow-up window in seconds after last bot reply (default: 300) */
122
120
  followUpWindow?: number;
123
- /** Enable thinking indicator ("收到🤔...") before processing (default: true) */
124
- thinkingIndicator?: boolean;
125
121
  /** 如流企业后台的应用ID(私聊消息撤回依赖此字段) */
126
122
  appAgentId?: number;
127
123
  /** Per-group configuration overrides, keyed by group ID */
@@ -159,8 +155,6 @@ export type ResolvedInfoflowAccount = {
159
155
  followUp?: boolean;
160
156
  /** Follow-up window in seconds after last bot reply (default: 300) */
161
157
  followUpWindow?: number;
162
- /** Enable thinking indicator ("收到🤔...") before processing (default: true) */
163
- thinkingIndicator?: boolean;
164
158
  /** 如流企业后台的应用ID(私聊消息撤回依赖此字段) */
165
159
  appAgentId?: number;
166
160
  /** Per-group configuration overrides, keyed by group ID */