@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.
- package/.claude/settings.local.json +9 -0
- package/CHANGELOG.md +82 -0
- package/openclaw.plugin.json +86 -2
- package/package.json +1 -1
- package/src/accounts.ts +3 -0
- package/src/actions.ts +108 -0
- package/src/bot.ts +377 -50
- package/src/channel.ts +10 -11
- package/src/infoflow-req-parse.ts +17 -35
- package/src/logging.ts +10 -28
- package/src/monitor.ts +7 -15
- package/src/reply-dispatcher.ts +81 -12
- package/src/send.ts +25 -7
- package/src/targets.ts +6 -27
- package/src/types.ts +72 -2
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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: ${
|
|
333
|
+
`[infoflow] ${chatType} handler error: ${formatInfoflowError(err)}`,
|
|
342
334
|
);
|
|
343
335
|
});
|
|
344
336
|
|
|
345
|
-
|
|
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(
|
|
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
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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("{");
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -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(
|
|
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[] = [
|
|
111
|
+
const contents: InfoflowMessageContentItem[] = [];
|
|
53
112
|
|
|
54
|
-
// Add AT content for group messages (first chunk only)
|
|
55
|
-
if (isFirstChunk && isGroup
|
|
56
|
-
if (
|
|
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 (
|
|
59
|
-
contents.push({ type: "at", content:
|
|
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(
|
|
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(
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|