@chbo297/infoflow 2026.3.16 → 2026.3.18

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.17
4
+
5
+ ### 修复与优化
6
+
7
+ #### follow-up(跟进回复)策略
8
+
9
+ - **他人被 @ 时仅记录不派发**:在 follow-up 时间窗口内,若新消息 @ 了其他人或机器人(而非本 bot),则仅写入会话历史、不派发 LLM,避免误判为对机器人的追问。
10
+ - **LLM 可见 body 含 robotid**:群消息中 @ 提及会以「@名称 (robotid:N)」形式呈现给模型,便于区分不同机器人并做出更准确的回复判断。
11
+
12
+ ---
13
+
3
14
  ## 2026.3.15
4
15
 
5
16
  ### 新功能
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  百度如流 (Infoflow) 企业消息平台 — OpenClaw 频道插件。
10
10
 
11
+ 📦 **[npm](https://www.npmjs.com/package/@chbo297/infoflow)**
12
+
11
13
  ## 特性
12
14
 
13
15
  - **私聊 & 群聊**消息接收与回复
@@ -279,6 +281,8 @@ MIT
279
281
 
280
282
  Baidu Infoflow (如流) enterprise messaging platform — OpenClaw channel plugin.
281
283
 
284
+ 📦 **[npm](https://www.npmjs.com/package/@chbo297/infoflow)**
285
+
282
286
  ## Features
283
287
 
284
288
  - **Direct & group** message receiving and replying
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chbo297/infoflow",
3
- "version": "2026.3.16",
3
+ "version": "2026.3.18",
4
4
  "description": "OpenClaw Infoflow (如流) channel plugin for Baidu enterprise messaging",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -194,6 +194,38 @@ function hasOtherMentions(mentionIds?: InfoflowMentionIds): boolean {
194
194
  return mentionIds.userIds.length > 0 || mentionIds.agentIds.length > 0;
195
195
  }
196
196
 
197
+ /**
198
+ * When in follow-up window and message has other @mentions (not this bot): record only and
199
+ * return "record_only" (no LLM dispatch). Otherwise return "dispatch".
200
+ */
201
+ function resolveFollowUpOtherMentioned(params: {
202
+ mentionIds: InfoflowMentionIds | undefined;
203
+ groupId: number | undefined;
204
+ bodyForAgent: string;
205
+ senderName: string;
206
+ fromuser: string;
207
+ }): "record_only" | "dispatch" {
208
+ const { mentionIds, groupId, bodyForAgent, senderName, fromuser } = params;
209
+ if (!hasOtherMentions(mentionIds)) return "dispatch";
210
+ const groupIdStr = groupId != null ? String(groupId) : undefined;
211
+ if (groupIdStr) {
212
+ recordPendingHistoryEntryIfEnabled({
213
+ historyMap: chatHistories,
214
+ historyKey: groupIdStr,
215
+ entry: {
216
+ sender: senderName || fromuser,
217
+ body: bodyForAgent,
218
+ timestamp: Date.now(),
219
+ },
220
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
221
+ });
222
+ }
223
+ logVerbose(
224
+ `[infoflow:bot] skip dispatch: from=${fromuser}, group=${groupId}, reason=followUp-other-mentioned (record only, no LLM)`,
225
+ );
226
+ return "record_only";
227
+ }
228
+
197
229
  // ---------------------------------------------------------------------------
198
230
  // Reply-to-bot detection (引用回复机器人消息)
199
231
  // ---------------------------------------------------------------------------
@@ -347,18 +379,6 @@ function buildFollowUpPrompt(isReplyToBot: boolean): string {
347
379
  return lines.join("\n");
348
380
  }
349
381
 
350
- /**
351
- * Build a GroupSystemPrompt for follow-up messages that @mention another person or bot.
352
- * Uses the conservative ReplyJudgmentRules since the message is likely directed at someone else.
353
- */
354
- function buildFollowUpOtherMentionedPrompt(): string {
355
- return [
356
- "You recently replied in this group. A new message has arrived, but it @mentions another person or bot — it is likely directed at them, not at you.",
357
- "",
358
- buildReplyJudgmentRules(),
359
- ].join("\n");
360
- }
361
-
362
382
  /**
363
383
  * Build a GroupSystemPrompt for proactive mode.
364
384
  * Instructs the agent to think about the message and reply when helpful.
@@ -585,9 +605,11 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
585
605
  // Extract non-bot mention IDs (userIds + agentIds) for LLM-driven @mentions
586
606
  const mentionIds = extractMentionIds(bodyItems, robotName);
587
607
 
588
- // Build two versions: mes (for CommandBody, no @xxx) and rawMes (for RawBody, with @xxx)
608
+ // Build three versions: mes (for CommandBody, no @xxx), rawMes (for RawBody, with @xxx),
609
+ // and bodyForAgent (for LLM: @name with robotid when present so model sees "@地图不打烊 (robotid:N)")
589
610
  let textContent = "";
590
611
  let rawTextContent = "";
612
+ let agentVisibleText = "";
591
613
  const replyContextItems: string[] = [];
592
614
  const imageUrls: string[] = [];
593
615
  if (Array.isArray(bodyItems)) {
@@ -601,17 +623,21 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
601
623
  } else if (item.type === "TEXT" || item.type === "MD") {
602
624
  textContent += item.content ?? "";
603
625
  rawTextContent += item.content ?? "";
626
+ agentVisibleText += item.content ?? "";
604
627
  } else if (item.type === "LINK") {
605
628
  const label = item.label ?? "";
606
629
  if (label) {
607
630
  textContent += ` ${label} `;
608
631
  rawTextContent += ` ${label} `;
632
+ agentVisibleText += ` ${label} `;
609
633
  }
610
634
  } else if (item.type === "AT") {
611
- // AT elements only go into rawTextContent, not textContent
635
+ // AT elements only go into rawTextContent and agentVisibleText, not textContent
612
636
  const name = item.name ?? "";
613
637
  if (name) {
614
638
  rawTextContent += `@${name} `;
639
+ agentVisibleText +=
640
+ item.robotid != null ? `@${name} (robotid:${item.robotid}) ` : `@${name} `;
615
641
  }
616
642
  } else if (item.type === "IMAGE") {
617
643
  // 提取图片下载地址
@@ -623,6 +649,7 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
623
649
  // Fallback: for any other item types with string content, treat content as text.
624
650
  textContent += item.content;
625
651
  rawTextContent += item.content;
652
+ agentVisibleText += item.content;
626
653
  }
627
654
  }
628
655
  }
@@ -643,6 +670,8 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
643
670
  if (!mes && replyContext) {
644
671
  mes = "(引用回复)";
645
672
  }
673
+ // Body for LLM: include @mentions with robotid so model sees e.g. "@地图不打烊 (robotid:N)"
674
+ const bodyForAgent = agentVisibleText.trim() || rawMes || mes;
646
675
 
647
676
  // Extract sender name from header or fallback to fromuser
648
677
  const senderName = String(header?.username ?? header?.nickname ?? msgData.username ?? fromuser);
@@ -657,6 +686,7 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
657
686
  fromuser,
658
687
  mes,
659
688
  rawMes,
689
+ bodyForAgent,
660
690
  chatType: "group",
661
691
  groupId: groupid,
662
692
  senderName,
@@ -682,6 +712,8 @@ export async function handleGroupChatMessage(params: HandleGroupChatParams): Pro
682
712
  export async function handleInfoflowMessage(params: HandleInfoflowMessageParams): Promise<void> {
683
713
  const { cfg, event, accountId, statusSink } = params;
684
714
  const { fromuser, mes, chatType, groupId, senderName } = event;
715
+ // Single source for "body shown to LLM": already computed in group handler (line ~666)
716
+ const bodyForAgent = event.bodyForAgent ?? mes;
685
717
 
686
718
  const account = resolveInfoflowAccount({ cfg, accountId });
687
719
  const core = getInfoflowRuntime();
@@ -729,7 +761,7 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
729
761
  timestamp: Date.now(),
730
762
  previousTimestamp,
731
763
  envelope: envelopeOptions,
732
- body: mes,
764
+ body: bodyForAgent,
733
765
  });
734
766
 
735
767
  // Inject accumulated group chat history into the body for context
@@ -844,6 +876,7 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
844
876
  Body: combinedBody,
845
877
  RawBody: event.rawMes ?? mes,
846
878
  CommandBody: mes,
879
+ BodyForAgent: bodyForAgent,
847
880
  From: fromAddress,
848
881
  To: toAddress,
849
882
  SessionKey: route.sessionKey,
@@ -866,6 +899,14 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
866
899
  ...mediaPayload,
867
900
  });
868
901
 
902
+ // Ensure BodyForAgent stays set for group messages (with @ and robotid) so the LLM sees full context
903
+ if (isGroup && bodyForAgent !== mes) {
904
+ (ctxPayload as Record<string, unknown>).BodyForAgent = bodyForAgent;
905
+ logVerbose(
906
+ `[infoflow] group: BodyForAgent set for LLM (${bodyForAgent.length} chars, includes @/robotid)`,
907
+ );
908
+ }
909
+
869
910
  // Record session using recordInboundSession for proper session tracking
870
911
  await core.channel.session.recordInboundSession({
871
912
  storePath,
@@ -894,7 +935,11 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
894
935
  recordPendingHistoryEntryIfEnabled({
895
936
  historyMap: chatHistories,
896
937
  historyKey: groupIdStr,
897
- entry: { sender: senderName || fromuser, body: mes, timestamp: Date.now() },
938
+ entry: {
939
+ sender: senderName || fromuser,
940
+ body: bodyForAgent,
941
+ timestamp: Date.now(),
942
+ },
898
943
  limit: DEFAULT_GROUP_HISTORY_LIMIT,
899
944
  });
900
945
  }
@@ -917,8 +962,17 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
917
962
  isWithinFollowUpWindow(groupIdStr, groupCfg.followUpWindow)
918
963
  ) {
919
964
  if (hasOtherMentions(event.mentionIds)) {
920
- triggerReason = "followUp-other-mentioned";
921
- ctxPayload.GroupSystemPrompt = buildFollowUpOtherMentionedPrompt();
965
+ if (
966
+ resolveFollowUpOtherMentioned({
967
+ mentionIds: event.mentionIds,
968
+ groupId,
969
+ bodyForAgent,
970
+ senderName: senderName || fromuser,
971
+ fromuser,
972
+ }) === "record_only"
973
+ ) {
974
+ return;
975
+ }
922
976
  } else {
923
977
  triggerReason = "followUp";
924
978
  ctxPayload.GroupSystemPrompt = buildFollowUpPrompt(event.isReplyToBot === true);
@@ -931,7 +985,11 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
931
985
  recordPendingHistoryEntryIfEnabled({
932
986
  historyMap: chatHistories,
933
987
  historyKey: groupIdStr,
934
- entry: { sender: senderName || fromuser, body: mes, timestamp: Date.now() },
988
+ entry: {
989
+ sender: senderName || fromuser,
990
+ body: bodyForAgent,
991
+ timestamp: Date.now(),
992
+ },
935
993
  limit: DEFAULT_GROUP_HISTORY_LIMIT,
936
994
  });
937
995
  }
@@ -969,8 +1027,17 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
969
1027
  isWithinFollowUpWindow(groupIdStr, groupCfg.followUpWindow)
970
1028
  ) {
971
1029
  if (hasOtherMentions(event.mentionIds)) {
972
- triggerReason = "followUp-other-mentioned";
973
- ctxPayload.GroupSystemPrompt = buildFollowUpOtherMentionedPrompt();
1030
+ if (
1031
+ resolveFollowUpOtherMentioned({
1032
+ mentionIds: event.mentionIds,
1033
+ groupId,
1034
+ bodyForAgent,
1035
+ senderName: senderName || fromuser,
1036
+ fromuser,
1037
+ }) === "record_only"
1038
+ ) {
1039
+ return;
1040
+ }
974
1041
  } else {
975
1042
  triggerReason = "followUp";
976
1043
  ctxPayload.GroupSystemPrompt = buildFollowUpPrompt(event.isReplyToBot === true);
@@ -983,7 +1050,11 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
983
1050
  recordPendingHistoryEntryIfEnabled({
984
1051
  historyMap: chatHistories,
985
1052
  historyKey: groupIdStr,
986
- entry: { sender: senderName || fromuser, body: mes, timestamp: Date.now() },
1053
+ entry: {
1054
+ sender: senderName || fromuser,
1055
+ body: bodyForAgent,
1056
+ timestamp: Date.now(),
1057
+ },
987
1058
  limit: DEFAULT_GROUP_HISTORY_LIMIT,
988
1059
  });
989
1060
  }
@@ -1038,8 +1109,22 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
1038
1109
  }
1039
1110
  }
1040
1111
 
1112
+ const mentionIdsLog =
1113
+ isGroup && event.mentionIds
1114
+ ? `, mentionIds={userIds:[${event.mentionIds.userIds.join(",")}], agentIds:[${event.mentionIds.agentIds.join(",")}]}`
1115
+ : "";
1116
+ const bodyPreview =
1117
+ (ctxPayload as Record<string, unknown>).Body != null
1118
+ ? String((ctxPayload as Record<string, unknown>).Body)
1119
+ : "";
1120
+ const bodyLog = `bodyLen=${bodyPreview.length} bodyPreview=${bodyPreview.length > 5000 ? bodyPreview.slice(0, 5000) + "..." : bodyPreview}`;
1121
+ const sysPrompt =
1122
+ (ctxPayload as Record<string, unknown>).GroupSystemPrompt != null
1123
+ ? String((ctxPayload as Record<string, unknown>).GroupSystemPrompt)
1124
+ : "";
1125
+ const sysPromptLog = `groupSystemPromptLen=${sysPrompt.length} groupSystemPromptPreview=${sysPrompt.length > 5000 ? sysPrompt.slice(0, 5000) + "..." : sysPrompt}`;
1041
1126
  logVerbose(
1042
- `[infoflow:bot] dispatching to LLM: from=${fromuser}, group=${groupId ?? "N/A"}, trigger=${triggerReason}, replyMode=${groupCfg?.replyMode ?? "N/A"}`,
1127
+ `[infoflow:bot] dispatching to LLM: from=${fromuser}, group=${groupId ?? "N/A"}, trigger=${triggerReason}, replyMode=${groupCfg?.replyMode ?? "N/A"}${mentionIdsLog} | ${bodyLog} | ${sysPromptLog}`,
1043
1128
  );
1044
1129
 
1045
1130
  const { dispatcherOptions, replyOptions } = createInfoflowReplyDispatcher({
@@ -1054,7 +1139,7 @@ export async function handleInfoflowMessage(params: HandleInfoflowMessageParams)
1054
1139
  mentionIds: isGroup ? event.mentionIds : undefined,
1055
1140
  // Pass inbound messageId for outbound reply-to (group only)
1056
1141
  replyToMessageId: isGroup ? event.messageId : undefined,
1057
- replyToPreview: isGroup ? mes : undefined,
1142
+ replyToPreview: isGroup ? bodyForAgent : undefined,
1058
1143
  mediaLocalRoots: getAgentScopedMediaLocalRoots(cfg, route.agentId),
1059
1144
  });
1060
1145
 
@@ -9,6 +9,55 @@ import { handlePrivateChatMessage, handleGroupChatMessage } from "./bot.js";
9
9
  import type { ResolvedInfoflowAccount } from "./channel.js";
10
10
  import { getInfoflowParseLog, formatInfoflowError, logVerbose } from "./logging.js";
11
11
 
12
+ // ---------------------------------------------------------------------------
13
+ // Large-integer precision protection
14
+ // ---------------------------------------------------------------------------
15
+
16
+ // Infoflow message IDs (e.g. 1859713223686736431) exceed Number.MAX_SAFE_INTEGER (2^53-1).
17
+ // JSON.parse silently truncates these to imprecise values.
18
+ // We extract precise strings from raw JSON text via regex and patch the parsed object.
19
+ const ID_KEYS = ["messageid", "msgid", "MsgId", "msgkey"] as const;
20
+
21
+ /**
22
+ * Patches large integer ID fields in `obj` with precise string values
23
+ * extracted from `rawText` via regex, bypassing JSON.parse precision loss.
24
+ * Only patches values with 16+ digits (smaller integers are safe).
25
+ */
26
+ function patchPreciseIds(rawText: string, obj: Record<string, unknown>): void {
27
+ for (const key of ID_KEYS) {
28
+ const re = new RegExp(`"${key}"\\s*:\\s*(\\d{16,})`, "g");
29
+ const preciseValues: string[] = [];
30
+ let m;
31
+ while ((m = re.exec(rawText)) !== null) {
32
+ preciseValues.push(m[1]);
33
+ }
34
+ if (preciseValues.length > 0) {
35
+ patchField(obj, key, preciseValues, 0);
36
+ }
37
+ }
38
+ }
39
+
40
+ /** Recursively walks `obj` and replaces numeric fields named `key` with precise strings (in order). */
41
+ function patchField(obj: unknown, key: string, values: string[], idx: number): number {
42
+ if (obj == null || typeof obj !== "object") return idx;
43
+ if (Array.isArray(obj)) {
44
+ for (const item of obj) {
45
+ idx = patchField(item, key, values, idx);
46
+ }
47
+ return idx;
48
+ }
49
+ const rec = obj as Record<string, unknown>;
50
+ if (key in rec && typeof rec[key] === "number" && idx < values.length) {
51
+ rec[key] = values[idx++];
52
+ }
53
+ for (const v of Object.values(rec)) {
54
+ if (v != null && typeof v === "object") {
55
+ idx = patchField(v, key, values, idx);
56
+ }
57
+ }
58
+ return idx;
59
+ }
60
+
12
61
  const DEDUP_TTL_MS = 5 * 60 * 1000; // 5 minutes
13
62
  const DEDUP_MAX_SIZE = 1000;
14
63
 
@@ -309,10 +358,13 @@ function tryDecryptAndDispatch(params: DecryptDispatchParams): ParseResult {
309
358
  continue; // Try next account
310
359
  }
311
360
 
361
+ logVerbose(`[infoflow] ${chatType}: decryptedContent=(${decryptedContent})`);
362
+
312
363
  // Parse as JSON first, then try fallback parser (XML for private)
313
364
  let msgData: Record<string, unknown> | null = null;
314
365
  try {
315
366
  msgData = JSON.parse(decryptedContent) as Record<string, unknown>;
367
+ patchPreciseIds(decryptedContent, msgData);
316
368
  } catch {
317
369
  if (fallbackParser) {
318
370
  msgData = fallbackParser(decryptedContent);
@@ -357,6 +409,7 @@ function handlePrivateMessage(messageJsonStr: string, targets: WebhookTarget[]):
357
409
  let messageJson: Record<string, unknown>;
358
410
  try {
359
411
  messageJson = JSON.parse(messageJsonStr) as Record<string, unknown>;
412
+ patchPreciseIds(messageJsonStr, messageJson);
360
413
  } catch {
361
414
  getInfoflowParseLog().error(`[infoflow] private: invalid messageJson`);
362
415
  return { handled: true, statusCode: 400, body: "invalid messageJson" };
@@ -430,3 +483,6 @@ export const _parseXmlMessage = parseXmlMessage;
430
483
  export function _resetMessageCache(): void {
431
484
  messageCache.clear();
432
485
  }
486
+
487
+ /** @internal */
488
+ export const _patchPreciseIds = patchPreciseIds;
package/src/types.ts CHANGED
@@ -191,6 +191,8 @@ export type InfoflowMessageEvent = {
191
191
  timestamp?: number;
192
192
  /** Raw message text preserving @mentions (for RawBody) */
193
193
  rawMes?: string;
194
+ /** Message text for the LLM including @mentions and robotid (BodyForAgent) */
195
+ bodyForAgent?: string;
194
196
  /** Raw body items from group message (for watch-mention detection) */
195
197
  bodyItems?: InfoflowInboundBodyItem[];
196
198
  /** Non-bot mention IDs extracted from AT items in group messages (excluding bot itself) */