@chbo297/infoflow 2026.2.27 → 2026.3.2

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.
@@ -7,7 +7,7 @@ import { createDedupeCache } from "openclaw/plugin-sdk";
7
7
  // ---------------------------------------------------------------------------
8
8
  import { handlePrivateChatMessage, handleGroupChatMessage } from "./bot.js";
9
9
  import type { ResolvedInfoflowAccount } from "./channel.js";
10
- import { getInfoflowParseLog } from "./logging.js";
10
+ import { getInfoflowParseLog, formatInfoflowError, logVerbose } from "./logging.js";
11
11
 
12
12
  const DEDUP_TTL_MS = 5 * 60 * 1000; // 5 minutes
13
13
  const DEDUP_MAX_SIZE = 1000;
@@ -138,9 +138,7 @@ function parseXmlMessage(xmlString: string): Record<string, string> | null {
138
138
  // Types
139
139
  // ---------------------------------------------------------------------------
140
140
 
141
- type InfoflowCoreRuntime = {
142
- logging: { shouldLogVerbose(): boolean };
143
- };
141
+ type InfoflowCoreRuntime = Record<string, unknown>;
144
142
 
145
143
  export type WebhookTarget = {
146
144
  account: ResolvedInfoflowAccount;
@@ -216,14 +214,9 @@ export async function parseAndDispatchInfoflowRequest(
216
214
  rawBody: string,
217
215
  targets: WebhookTarget[],
218
216
  ): Promise<ParseResult> {
219
- const verbose = targets[0]?.core?.logging?.shouldLogVerbose?.() ?? false;
220
217
  const contentType = String(req.headers["content-type"] ?? "").toLowerCase();
221
218
 
222
- if (verbose) {
223
- getInfoflowParseLog().debug?.(
224
- `[infoflow] parseAndDispatch: contentType=${contentType}, bodyLen=${rawBody.length}`,
225
- );
226
- }
219
+ logVerbose(`[infoflow] parseAndDispatch: contentType=${contentType}, bodyLen=${rawBody.length}`);
227
220
 
228
221
  // --- form-urlencoded: echostr verification + private chat ---
229
222
  if (contentType.startsWith("application/x-www-form-urlencoded")) {
@@ -248,14 +241,16 @@ export async function parseAndDispatchInfoflowRequest(
248
241
  return { handled: true, statusCode: 200, body: echostr };
249
242
  }
250
243
  }
251
- getInfoflowParseLog().error(`[infoflow] echostr signature mismatch`);
244
+ getInfoflowParseLog().error(
245
+ `[infoflow] echostr signature mismatch (tried ${targets.length} account(s))`,
246
+ );
252
247
  return { handled: true, statusCode: 403, body: "Invalid signature" };
253
248
  }
254
249
 
255
250
  // private chat message (messageJson field in form)
256
251
  const messageJsonStr = form.get("messageJson") ?? "";
257
252
  if (messageJsonStr) {
258
- return handlePrivateMessage(messageJsonStr, targets, verbose);
253
+ return handlePrivateMessage(messageJsonStr, targets);
259
254
  }
260
255
 
261
256
  getInfoflowParseLog().error(`[infoflow] form-urlencoded but missing echostr or messageJson`);
@@ -264,7 +259,7 @@ export async function parseAndDispatchInfoflowRequest(
264
259
 
265
260
  // --- text/plain: group chat ---
266
261
  if (contentType.startsWith("text/plain")) {
267
- return handleGroupMessage(rawBody, targets, verbose);
262
+ return handleGroupMessage(rawBody, targets);
268
263
  }
269
264
 
270
265
  // --- unsupported Content-Type ---
@@ -280,7 +275,6 @@ type DecryptDispatchParams = {
280
275
  encryptedContent: string;
281
276
  targets: WebhookTarget[];
282
277
  chatType: "direct" | "group";
283
- verbose: boolean;
284
278
  fallbackParser?: (content: string) => Record<string, unknown> | null;
285
279
  /** Async handler to process the decrypted message. Errors are caught internally. */
286
280
  dispatchFn: (target: WebhookTarget, msgData: Record<string, unknown>) => Promise<void>;
@@ -292,7 +286,7 @@ type DecryptDispatchParams = {
292
286
  * Dispatches asynchronously (fire-and-forget) with centralized error logging.
293
287
  */
294
288
  function tryDecryptAndDispatch(params: DecryptDispatchParams): ParseResult {
295
- const { encryptedContent, targets, chatType, verbose, fallbackParser, dispatchFn } = params;
289
+ const { encryptedContent, targets, chatType, fallbackParser, dispatchFn } = params;
296
290
 
297
291
  if (targets.length === 0) {
298
292
  getInfoflowParseLog().error(`[infoflow] ${chatType}: no target configured`);
@@ -327,9 +321,7 @@ function tryDecryptAndDispatch(params: DecryptDispatchParams): ParseResult {
327
321
 
328
322
  if (msgData && Object.keys(msgData).length > 0) {
329
323
  if (isDuplicateMessage(msgData)) {
330
- if (verbose) {
331
- getInfoflowParseLog().debug?.(`[infoflow] ${chatType}: duplicate message, skipping`);
332
- }
324
+ logVerbose(`[infoflow] ${chatType}: duplicate message, skipping`);
333
325
  return { handled: true, statusCode: 200, body: "success" };
334
326
  }
335
327
 
@@ -338,18 +330,18 @@ function tryDecryptAndDispatch(params: DecryptDispatchParams): ParseResult {
338
330
  // Fire-and-forget with centralized error handling
339
331
  void dispatchFn(target, msgData).catch((err) => {
340
332
  getInfoflowParseLog().error(
341
- `[infoflow] ${chatType} handler error: ${err instanceof Error ? err.message : String(err)}`,
333
+ `[infoflow] ${chatType} handler error: ${formatInfoflowError(err)}`,
342
334
  );
343
335
  });
344
336
 
345
- if (verbose) {
346
- getInfoflowParseLog().debug?.(`[infoflow] ${chatType}: message dispatched successfully`);
347
- }
337
+ logVerbose(`[infoflow] ${chatType}: message dispatched successfully`);
348
338
  return { handled: true, statusCode: 200, body: "success" };
349
339
  }
350
340
  }
351
341
 
352
- getInfoflowParseLog().error(`[infoflow] ${chatType}: decryption failed for all accounts`);
342
+ getInfoflowParseLog().error(
343
+ `[infoflow] ${chatType}: decryption failed for all ${targets.length} account(s)`,
344
+ );
353
345
  return { handled: true, statusCode: 500, body: "decryption failed for all accounts" };
354
346
  }
355
347
 
@@ -362,11 +354,7 @@ function tryDecryptAndDispatch(params: DecryptDispatchParams): ParseResult {
362
354
  * Decrypts the Encrypt field with encodingAESKey (AES-ECB),
363
355
  * parses the decrypted content, then dispatches to bot.ts.
364
356
  */
365
- function handlePrivateMessage(
366
- messageJsonStr: string,
367
- targets: WebhookTarget[],
368
- verbose: boolean,
369
- ): ParseResult {
357
+ function handlePrivateMessage(messageJsonStr: string, targets: WebhookTarget[]): ParseResult {
370
358
  let messageJson: Record<string, unknown>;
371
359
  try {
372
360
  messageJson = JSON.parse(messageJsonStr) as Record<string, unknown>;
@@ -385,7 +373,6 @@ function handlePrivateMessage(
385
373
  encryptedContent: encrypt,
386
374
  targets,
387
375
  chatType: "direct",
388
- verbose,
389
376
  fallbackParser: parseXmlMessage,
390
377
  dispatchFn: (target, msgData) =>
391
378
  handlePrivateChatMessage({
@@ -406,16 +393,11 @@ function handlePrivateMessage(
406
393
  * The rawBody itself is an AES-encrypted ciphertext (Base64URLSafe encoded).
407
394
  * Decrypts and dispatches to bot.ts.
408
395
  */
409
- function handleGroupMessage(
410
- rawBody: string,
411
- targets: WebhookTarget[],
412
- verbose: boolean,
413
- ): ParseResult {
396
+ function handleGroupMessage(rawBody: string, targets: WebhookTarget[]): ParseResult {
414
397
  return tryDecryptAndDispatch({
415
398
  encryptedContent: rawBody,
416
399
  targets,
417
400
  chatType: "group",
418
- verbose,
419
401
  dispatchFn: (target, msgData) =>
420
402
  handleGroupChatMessage({
421
403
  cfg: target.config,
package/src/logging.ts CHANGED
@@ -96,36 +96,18 @@ export function formatInfoflowError(err: unknown, options?: FormatErrorOptions):
96
96
  return String(err);
97
97
  }
98
98
 
99
- export type LogApiErrorOptions = {
100
- /** Logger to use (defaults to send logger) */
101
- logger?: RuntimeLogger;
102
- /** Include stack trace in the log (default: false) */
103
- includeStack?: boolean;
104
- };
105
-
106
99
  /**
107
- * Log an API error with operation context and structured metadata.
108
- * @param operation - The API operation name (e.g., "sendPrivate", "getToken")
109
- * @param error - The error to log
110
- * @param options - Logging options
100
+ * Log a message when verbose mode is enabled.
101
+ * Checks shouldLogVerbose() via PluginRuntime, then writes to console for
102
+ * --verbose terminal output. Safe to call before runtime is initialized.
111
103
  */
112
- export function logInfoflowApiError(
113
- operation: string,
114
- error: unknown,
115
- options?: LogApiErrorOptions | RuntimeLogger,
116
- ): void {
117
- // Support legacy signature: logInfoflowApiError(op, err, logger)
118
- const opts: LogApiErrorOptions =
119
- options && "error" in options ? { logger: options as RuntimeLogger } : (options ?? {});
120
-
121
- const log = opts.logger ?? getInfoflowSendLog();
122
- const errMsg = formatInfoflowError(error, { includeStack: opts.includeStack });
123
-
124
- // Use structured meta for better log aggregation and filtering
125
- log.error(`[infoflow:${operation}] ${errMsg}`, {
126
- operation,
127
- errorType: error instanceof Error ? error.constructor.name : typeof error,
128
- });
104
+ export function logVerbose(message: string): void {
105
+ try {
106
+ if (!getInfoflowRuntime().logging.shouldLogVerbose()) return;
107
+ console.log(message);
108
+ } catch {
109
+ // runtime not available, skip verbose logging
110
+ }
129
111
  }
130
112
 
131
113
  // ---------------------------------------------------------------------------
package/src/monitor.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  loadRawBody,
7
7
  type WebhookTarget,
8
8
  } from "./infoflow-req-parse.js";
9
- import { getInfoflowWebhookLog } from "./logging.js";
9
+ import { getInfoflowWebhookLog, formatInfoflowError, logVerbose } from "./logging.js";
10
10
  import { getInfoflowRuntime } from "./runtime.js";
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -88,16 +88,11 @@ export async function handleInfoflowWebhookRequest(
88
88
  req: IncomingMessage,
89
89
  res: ServerResponse,
90
90
  ): Promise<boolean> {
91
- const core = getInfoflowRuntime();
92
- const verbose = core.logging.shouldLogVerbose();
93
-
94
91
  const url = new URL(req.url ?? "/", "http://localhost");
95
92
  const requestPath = normalizeWebhookPath(url.pathname);
96
93
 
97
- if (verbose) {
98
- // Log the full request URL
99
- getInfoflowWebhookLog().debug?.(`[infoflow] request: url=${url}`);
100
- }
94
+ // Log the full request URL
95
+ logVerbose(`[infoflow] request: url=${url}`);
101
96
 
102
97
  // Check if path matches Infoflow webhook pattern
103
98
  if (!isInfoflowPath(requestPath)) {
@@ -130,18 +125,15 @@ export async function handleInfoflowWebhookRequest(
130
125
  try {
131
126
  result = await parseAndDispatchInfoflowRequest(req, bodyResult.raw, targets);
132
127
  } catch (err) {
133
- const errMsg = err instanceof Error ? err.message : String(err);
134
- getInfoflowWebhookLog().error(`[infoflow] webhook handler error: ${errMsg}`);
128
+ getInfoflowWebhookLog().error(`[infoflow] webhook handler error: ${formatInfoflowError(err)}`);
135
129
  res.statusCode = 500;
136
130
  res.end("internal error");
137
131
  return true;
138
132
  }
139
133
 
140
- if (verbose) {
141
- getInfoflowWebhookLog().debug?.(
142
- `[infoflow] dispatch result: handled=${result.handled}, status=${result.handled ? result.statusCode : "N/A"}`,
143
- );
144
- }
134
+ logVerbose(
135
+ `[infoflow] dispatch result: handled=${result.handled}, status=${result.handled ? result.statusCode : "N/A"}`,
136
+ );
145
137
 
146
138
  if (result.handled) {
147
139
  const looksLikeJson = result.body.startsWith("{");
@@ -3,10 +3,10 @@ import {
3
3
  type OpenClawConfig,
4
4
  type ReplyPayload,
5
5
  } from "openclaw/plugin-sdk";
6
- import { getInfoflowSendLog } from "./logging.js";
6
+ import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "./logging.js";
7
7
  import { getInfoflowRuntime } from "./runtime.js";
8
8
  import { sendInfoflowMessage } from "./send.js";
9
- import type { InfoflowAtOptions, InfoflowMessageContentItem } from "./types.js";
9
+ import type { InfoflowAtOptions, InfoflowMentionIds, InfoflowMessageContentItem } from "./types.js";
10
10
 
11
11
  export type CreateInfoflowReplyDispatcherParams = {
12
12
  cfg: OpenClawConfig;
@@ -17,6 +17,8 @@ export type CreateInfoflowReplyDispatcherParams = {
17
17
  statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
18
18
  /** AT options for @mentioning members in group messages */
19
19
  atOptions?: InfoflowAtOptions;
20
+ /** Mention IDs from inbound message for resolving @id in LLM output */
21
+ mentionIds?: InfoflowMentionIds;
20
22
  };
21
23
 
22
24
  /**
@@ -24,7 +26,7 @@ export type CreateInfoflowReplyDispatcherParams = {
24
26
  * Encapsulates prefix options, chunked deliver (send via Infoflow API + statusSink), and onError.
25
27
  */
26
28
  export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatcherParams) {
27
- const { cfg, agentId, accountId, to, statusSink, atOptions } = params;
29
+ const { cfg, agentId, accountId, to, statusSink, atOptions, mentionIds } = params;
28
30
  const core = getInfoflowRuntime();
29
31
 
30
32
  const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
@@ -37,42 +39,109 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
37
39
  // Check if target is a group (format: group:<id>)
38
40
  const isGroup = /^group:\d+$/i.test(to);
39
41
 
42
+ // Build id→type map for resolving @id in LLM output (distinguishes user vs agent)
43
+ const mentionIdMap = new Map<string, "user" | "agent">();
44
+ if (mentionIds) {
45
+ for (const id of mentionIds.userIds) {
46
+ mentionIdMap.set(id.toLowerCase(), "user");
47
+ }
48
+ for (const id of mentionIds.agentIds) {
49
+ mentionIdMap.set(String(id).toLowerCase(), "agent");
50
+ }
51
+ }
52
+
40
53
  const deliver = async (payload: ReplyPayload) => {
41
54
  const text = payload.text ?? "";
55
+ logVerbose(`[infoflow] deliver called: to=${to}, text=${text}`);
42
56
  if (!text.trim()) {
43
57
  return;
44
58
  }
45
59
 
60
+ // Resolve @id patterns in LLM output text to user/agent IDs
61
+ const resolvedUserIds: string[] = [];
62
+ const resolvedAgentIds: number[] = [];
63
+ if (isGroup && mentionIdMap.size > 0) {
64
+ const mentionPattern = /@([\w.]+)/g;
65
+ let match: RegExpExecArray | null;
66
+ while ((match = mentionPattern.exec(text)) !== null) {
67
+ const id = match[1];
68
+ const type = mentionIdMap.get(id.toLowerCase());
69
+ if (type === "user" && !resolvedUserIds.includes(id)) {
70
+ resolvedUserIds.push(id);
71
+ } else if (type === "agent") {
72
+ const numId = Number(id);
73
+ if (Number.isFinite(numId) && !resolvedAgentIds.includes(numId)) {
74
+ resolvedAgentIds.push(numId);
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ // Merge atOptions user IDs (sender echo-back) with LLM-resolved user IDs
81
+ const atOptionIds = atOptions?.atAll ? [] : (atOptions?.atUserIds ?? []);
82
+ const allAtUserIds = [...atOptionIds];
83
+ for (const id of resolvedUserIds) {
84
+ if (!allAtUserIds.includes(id)) {
85
+ allAtUserIds.push(id);
86
+ }
87
+ }
88
+ const hasAtAll = atOptions?.atAll === true;
89
+ const hasAtUsers = allAtUserIds.length > 0;
90
+ const hasAtAgents = resolvedAgentIds.length > 0;
91
+
92
+ // Prepend AT mentions to the text if needed (group messages only)
93
+ // Only prepend for atOptions IDs; LLM text already contains @id for resolved mentions
94
+ let messageText = text;
95
+ if (isGroup && atOptions) {
96
+ let atPrefix = "";
97
+ if (hasAtAll) {
98
+ atPrefix = "@all ";
99
+ } else if (atOptions.atUserIds?.length) {
100
+ atPrefix = atOptions.atUserIds.map((id) => `@${id}`).join(" ") + " ";
101
+ }
102
+ messageText = atPrefix + text;
103
+ }
104
+
46
105
  // Chunk text to 4000 chars max (Infoflow limit)
47
- const chunks = core.channel.text.chunkText(text, 4000);
106
+ const chunks = core.channel.text.chunkText(messageText, 4000);
48
107
  // Only include @mentions in the first chunk (avoid duplicate @s)
49
108
  let isFirstChunk = true;
50
109
 
51
110
  for (const chunk of chunks) {
52
- const contents: InfoflowMessageContentItem[] = [{ type: "markdown", content: chunk }];
111
+ const contents: InfoflowMessageContentItem[] = [];
53
112
 
54
- // Add AT content for group messages (first chunk only)
55
- if (isFirstChunk && isGroup && atOptions) {
56
- if (atOptions.atAll) {
113
+ // Add AT content nodes for group messages (first chunk only)
114
+ if (isFirstChunk && isGroup) {
115
+ if (hasAtAll) {
57
116
  contents.push({ type: "at", content: "all" });
58
- } else if (atOptions.atUserIds?.length) {
59
- contents.push({ type: "at", content: atOptions.atUserIds.join(",") });
117
+ } else if (hasAtUsers) {
118
+ contents.push({ type: "at", content: allAtUserIds.join(",") });
119
+ }
120
+ if (hasAtAgents) {
121
+ contents.push({ type: "at-agent", content: resolvedAgentIds.join(",") });
60
122
  }
61
123
  }
62
124
  isFirstChunk = false;
63
125
 
126
+ // Add markdown content
127
+ contents.push({ type: "markdown", content: chunk });
128
+
64
129
  const result = await sendInfoflowMessage({ cfg, to, contents, accountId });
65
130
 
66
131
  if (result.ok) {
67
132
  statusSink?.({ lastOutboundAt: Date.now() });
68
133
  } else if (result.error) {
69
- getInfoflowSendLog().error(`[infoflow] Failed to send message: ${result.error}`);
134
+ getInfoflowSendLog().error(
135
+ `[infoflow] reply failed to=${to}, accountId=${accountId}: ${result.error}`,
136
+ );
70
137
  }
71
138
  }
72
139
  };
73
140
 
74
141
  const onError = (err: unknown) => {
75
- getInfoflowSendLog().error(`[infoflow] reply failed: ${String(err)}`);
142
+ getInfoflowSendLog().error(
143
+ `[infoflow] reply error to=${to}, accountId=${accountId}: ${formatInfoflowError(err)}`,
144
+ );
76
145
  };
77
146
 
78
147
  return {
package/src/send.ts CHANGED
@@ -7,8 +7,7 @@ import { createHash, randomUUID } from "node:crypto";
7
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
- import { getInfoflowSendLog } from "./logging.js";
11
- import { getInfoflowRuntime } from "./runtime.js";
10
+ import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "./logging.js";
12
11
  import type {
13
12
  InfoflowGroupMessageBodyItem,
14
13
  InfoflowMessageContentItem,
@@ -163,7 +162,7 @@ export async function getAppAccessToken(params: {
163
162
 
164
163
  return { ok: true, token };
165
164
  } catch (err) {
166
- const errMsg = err instanceof Error ? err.message : String(err);
165
+ const errMsg = formatInfoflowError(err);
167
166
  return { ok: false, error: errMsg };
168
167
  } finally {
169
168
  clearTimeout(timeout);
@@ -275,10 +274,15 @@ export async function sendInfoflowPrivateMessage(params: {
275
274
  LOGID: randomUUID(),
276
275
  };
277
276
 
277
+ const bodyStr = JSON.stringify(payload);
278
+
279
+ // Log request body when verbose logging is enabled
280
+ logVerbose(`[infoflow:sendPrivate] POST body: ${bodyStr}`);
281
+
278
282
  const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_PRIVATE_SEND_PATH}`, {
279
283
  method: "POST",
280
284
  headers,
281
- body: JSON.stringify(payload),
285
+ body: bodyStr,
282
286
  signal: controller.signal,
283
287
  });
284
288
 
@@ -313,7 +317,7 @@ export async function sendInfoflowPrivateMessage(params: {
313
317
 
314
318
  return { ok: true, invaliduser: innerData?.invaliduser as string | undefined, msgkey };
315
319
  } catch (err) {
316
- const errMsg = err instanceof Error ? err.message : String(err);
320
+ const errMsg = formatInfoflowError(err);
317
321
  getInfoflowSendLog().error(`[infoflow:sendPrivate] exception: ${errMsg}`);
318
322
  return { ok: false, error: errMsg };
319
323
  } finally {
@@ -379,6 +383,15 @@ export async function sendInfoflowGroupMessage(params: {
379
383
  const { href } = parseLinkContent(item.content);
380
384
  body.push({ type: "LINK", href });
381
385
  }
386
+ } else if (type === "at-agent") {
387
+ // Robot AT: parse comma-separated numeric IDs into atagentids
388
+ const agentIds = item.content
389
+ .split(",")
390
+ .map((s) => Number(s.trim()))
391
+ .filter(Number.isFinite);
392
+ if (agentIds.length > 0) {
393
+ body.push({ type: "AT", atuserids: [], atagentids: agentIds });
394
+ }
382
395
  }
383
396
  }
384
397
 
@@ -417,10 +430,15 @@ export async function sendInfoflowGroupMessage(params: {
417
430
  "Content-Type": "application/json",
418
431
  };
419
432
 
433
+ const bodyStr = JSON.stringify(payload);
434
+
435
+ // Log request body when verbose logging is enabled
436
+ logVerbose(`[infoflow:sendGroup] POST body: ${bodyStr}`);
437
+
420
438
  const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_GROUP_SEND_PATH}`, {
421
439
  method: "POST",
422
440
  headers,
423
- body: JSON.stringify(payload),
441
+ body: bodyStr,
424
442
  signal: controller.signal,
425
443
  });
426
444
 
@@ -452,7 +470,7 @@ export async function sendInfoflowGroupMessage(params: {
452
470
 
453
471
  return { ok: true, messageid };
454
472
  } catch (err) {
455
- const errMsg = err instanceof Error ? err.message : String(err);
473
+ const errMsg = formatInfoflowError(err);
456
474
  getInfoflowSendLog().error(`[infoflow:sendGroup] exception: ${errMsg}`);
457
475
  return { ok: false, error: errMsg };
458
476
  } finally {
package/src/targets.ts CHANGED
@@ -3,8 +3,7 @@
3
3
  * Handles user and group ID formats for message targeting.
4
4
  */
5
5
 
6
- import { getInfoflowSendLog } from "./logging.js";
7
- import { getInfoflowRuntime } from "./runtime.js";
6
+ import { logVerbose } from "./logging.js";
8
7
 
9
8
  // ---------------------------------------------------------------------------
10
9
  // Target Format Constants
@@ -33,23 +32,11 @@ const USER_PREFIX = "user:";
33
32
  * "123456" -> "group:123456" (pure digits treated as group)
34
33
  */
35
34
  export function normalizeInfoflowTarget(raw: string): string | undefined {
36
- // Get verbose state once at start
37
- let verbose = false;
38
- try {
39
- verbose = getInfoflowRuntime().logging.shouldLogVerbose();
40
- } catch {
41
- // runtime not available, keep verbose = false
42
- }
43
-
44
- if (verbose) {
45
- getInfoflowSendLog().debug?.(`[infoflow:normalizeTarget] input: "${raw}"`);
46
- }
35
+ logVerbose(`[infoflow:normalizeTarget] input: "${raw}"`);
47
36
 
48
37
  const trimmed = raw.trim();
49
38
  if (!trimmed) {
50
- if (verbose) {
51
- getInfoflowSendLog().debug?.(`[infoflow:normalizeTarget] empty input, returning undefined`);
52
- }
39
+ logVerbose(`[infoflow:normalizeTarget] empty input, returning undefined`);
53
40
  return undefined;
54
41
  }
55
42
 
@@ -63,27 +50,19 @@ export function normalizeInfoflowTarget(raw: string): string | undefined {
63
50
 
64
51
  // Keep group: prefix as-is
65
52
  if (target.toLowerCase().startsWith(GROUP_PREFIX)) {
66
- if (verbose) {
67
- getInfoflowSendLog().debug?.(`[infoflow:normalizeTarget] output: "${target}" (group)`);
68
- }
53
+ logVerbose(`[infoflow:normalizeTarget] output: "${target}" (group)`);
69
54
  return target;
70
55
  }
71
56
 
72
57
  // Pure digits -> treat as group ID
73
58
  if (/^\d+$/.test(target)) {
74
59
  const result = `${GROUP_PREFIX}${target}`;
75
- if (verbose) {
76
- getInfoflowSendLog().debug?.(
77
- `[infoflow:normalizeTarget] output: "${result}" (digits -> group)`,
78
- );
79
- }
60
+ logVerbose(`[infoflow:normalizeTarget] output: "${result}" (digits -> group)`);
80
61
  return result;
81
62
  }
82
63
 
83
64
  // Otherwise it's a username
84
- if (verbose) {
85
- getInfoflowSendLog().debug?.(`[infoflow:normalizeTarget] output: "${target}" (username)`);
86
- }
65
+ logVerbose(`[infoflow:normalizeTarget] output: "${target}" (username)`);
87
66
  return target;
88
67
  }
89
68
 
package/src/types.ts CHANGED
@@ -10,6 +10,48 @@ export type InfoflowDmPolicy = "open" | "pairing" | "allowlist";
10
10
  export type InfoflowGroupPolicy = "open" | "allowlist" | "disabled";
11
11
  export type InfoflowChatType = "direct" | "group";
12
12
 
13
+ /** Reply mode controlling bot behavior per group */
14
+ export type InfoflowReplyMode =
15
+ | "ignore"
16
+ | "record"
17
+ | "mention-only"
18
+ | "mention-and-watch"
19
+ | "proactive";
20
+
21
+ /** Per-group configuration overrides */
22
+ export type InfoflowGroupConfig = {
23
+ replyMode?: InfoflowReplyMode;
24
+ watchMentions?: string[];
25
+ followUp?: boolean;
26
+ followUpWindow?: number;
27
+ systemPrompt?: string;
28
+ };
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Inbound body item (for @mention detection in received messages)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Inbound body item from group messages (for @mention detection) */
35
+ export type InfoflowInboundBodyItem = {
36
+ type?: string;
37
+ content?: string;
38
+ label?: string;
39
+ /** 机器人 AT 时有此字段(数字),与 userid 互斥 */
40
+ robotid?: number;
41
+ /** AT 元素的显示名称 */
42
+ name?: string;
43
+ /** 人类用户 AT 时有此字段(uuap name),与 robotid 互斥 */
44
+ userid?: string;
45
+ };
46
+
47
+ /** Mention IDs extracted from inbound group AT items (excluding the bot itself) */
48
+ export type InfoflowMentionIds = {
49
+ /** Human user userid list */
50
+ userIds: string[];
51
+ /** Robot robotid list (numbers, corresponding to outbound atagentids) */
52
+ agentIds: number[];
53
+ };
54
+
13
55
  // ---------------------------------------------------------------------------
14
56
  // AT mention types
15
57
  // ---------------------------------------------------------------------------
@@ -26,12 +68,12 @@ export type InfoflowAtOptions = {
26
68
  export type InfoflowGroupMessageBodyItem =
27
69
  | { type: "TEXT"; content: string }
28
70
  | { type: "MD"; content: string }
29
- | { type: "AT"; atall?: boolean; atuserids: string[] }
71
+ | { type: "AT"; atall?: boolean; atuserids: string[]; atagentids?: number[] }
30
72
  | { type: "LINK"; href: string };
31
73
 
32
74
  /** Content item for sendInfoflowMessage */
33
75
  export type InfoflowMessageContentItem = {
34
- type: "text" | "markdown" | "at" | "link";
76
+ type: "text" | "markdown" | "at" | "at-agent" | "link";
35
77
  content: string;
36
78
  };
37
79
 
@@ -54,6 +96,17 @@ export type InfoflowAccountConfig = {
54
96
  requireMention?: boolean;
55
97
  /** Robot name for matching @mentions in group messages */
56
98
  robotName?: string;
99
+ /** Names to watch for @mentions; when someone @mentions a person in this list,
100
+ * the bot analyzes the message and replies only if confident. */
101
+ watchMentions?: string[];
102
+ /** Reply mode controlling bot engagement level in groups */
103
+ replyMode?: InfoflowReplyMode;
104
+ /** Enable follow-up replies after bot responds to a mention (default: true) */
105
+ followUp?: boolean;
106
+ /** Follow-up window in seconds after last bot reply (default: 300) */
107
+ followUpWindow?: number;
108
+ /** Per-group configuration overrides, keyed by group ID */
109
+ groups?: Record<string, InfoflowGroupConfig>;
57
110
  accounts?: Record<string, InfoflowAccountConfig>;
58
111
  defaultAccount?: string;
59
112
  };
@@ -78,6 +131,17 @@ export type ResolvedInfoflowAccount = {
78
131
  requireMention?: boolean;
79
132
  /** Robot name for matching @mentions in group messages */
80
133
  robotName?: string;
134
+ /** Names to watch for @mentions; when someone @mentions a person in this list,
135
+ * the bot analyzes the message and replies only if confident. */
136
+ watchMentions?: string[];
137
+ /** Reply mode controlling bot engagement level in groups */
138
+ replyMode?: InfoflowReplyMode;
139
+ /** Enable follow-up replies after bot responds to a mention (default: true) */
140
+ followUp?: boolean;
141
+ /** Follow-up window in seconds after last bot reply (default: 300) */
142
+ followUpWindow?: number;
143
+ /** Per-group configuration overrides, keyed by group ID */
144
+ groups?: Record<string, InfoflowGroupConfig>;
81
145
  };
82
146
  };
83
147
 
@@ -99,6 +163,12 @@ export type InfoflowMessageEvent = {
99
163
  timestamp?: number;
100
164
  /** Raw message text preserving @mentions (for RawBody) */
101
165
  rawMes?: string;
166
+ /** Raw body items from group message (for watch-mention detection) */
167
+ bodyItems?: InfoflowInboundBodyItem[];
168
+ /** Non-bot mention IDs extracted from AT items in group messages (excluding bot itself) */
169
+ mentionIds?: InfoflowMentionIds;
170
+ /** Reply/quote context extracted from replyData body items (supports multiple quotes) */
171
+ replyContext?: string[];
102
172
  };
103
173
 
104
174
  // ---------------------------------------------------------------------------