@core-workspace/infoflow-openclaw-plugin 2026.3.8

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/src/logging.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Structured logging module for Infoflow extension.
3
+ * Provides consistent logging interface across all Infoflow modules.
4
+ */
5
+
6
+ import type { RuntimeLogger } from "openclaw/plugin-sdk";
7
+ import { getInfoflowRuntime } from "./runtime.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Logger Factory
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Creates a child logger with infoflow-specific bindings.
15
+ * Uses the PluginRuntime logging system for structured output.
16
+ */
17
+ function createInfoflowLogger(module?: string): RuntimeLogger {
18
+ const runtime = getInfoflowRuntime();
19
+ const bindings: Record<string, unknown> = { subsystem: "gateway/channels/infoflow" };
20
+ if (module) {
21
+ bindings.module = module;
22
+ }
23
+ return runtime.logging.getChildLogger(bindings);
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Module-specific Loggers (lazy initialization)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ let _sendLog: RuntimeLogger | null = null;
31
+ let _webhookLog: RuntimeLogger | null = null;
32
+ let _botLog: RuntimeLogger | null = null;
33
+ let _parseLog: RuntimeLogger | null = null;
34
+
35
+ /**
36
+ * Logger for send operations (private/group message sending).
37
+ */
38
+ export function getInfoflowSendLog(): RuntimeLogger {
39
+ if (!_sendLog) {
40
+ _sendLog = createInfoflowLogger("send");
41
+ }
42
+ return _sendLog;
43
+ }
44
+
45
+ /**
46
+ * Logger for webhook/monitor operations.
47
+ */
48
+ export function getInfoflowWebhookLog(): RuntimeLogger {
49
+ if (!_webhookLog) {
50
+ _webhookLog = createInfoflowLogger("webhook");
51
+ }
52
+ return _webhookLog;
53
+ }
54
+
55
+ /**
56
+ * Logger for bot/message processing operations.
57
+ */
58
+ export function getInfoflowBotLog(): RuntimeLogger {
59
+ if (!_botLog) {
60
+ _botLog = createInfoflowLogger("bot");
61
+ }
62
+ return _botLog;
63
+ }
64
+
65
+ /**
66
+ * Logger for request parsing operations.
67
+ */
68
+ export function getInfoflowParseLog(): RuntimeLogger {
69
+ if (!_parseLog) {
70
+ _parseLog = createInfoflowLogger("parse");
71
+ }
72
+ return _parseLog;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Utility Functions
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export type FormatErrorOptions = {
80
+ /** Include stack trace in the output (default: false) */
81
+ includeStack?: boolean;
82
+ };
83
+
84
+ /**
85
+ * Format error message for logging.
86
+ * @param err - The error to format
87
+ * @param options - Formatting options
88
+ */
89
+ export function formatInfoflowError(err: unknown, options?: FormatErrorOptions): string {
90
+ if (err instanceof Error) {
91
+ if (options?.includeStack && err.stack) {
92
+ return err.stack;
93
+ }
94
+ return err.message;
95
+ }
96
+ return String(err);
97
+ }
98
+
99
+ /**
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.
103
+ */
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
+ }
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Test-only exports (@internal)
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /** @internal — Reset all cached loggers. Only use in tests. */
118
+ export function _resetLoggers(): void {
119
+ _sendLog = null;
120
+ _webhookLog = null;
121
+ _botLog = null;
122
+ _parseLog = null;
123
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+
3
+ let runtime: PluginRuntime | null = null;
4
+
5
+ export function setInfoflowRuntime(next: PluginRuntime) {
6
+ runtime = next;
7
+ }
8
+
9
+ export function getInfoflowRuntime(): PluginRuntime {
10
+ if (!runtime) {
11
+ throw new Error("Infoflow runtime not initialized");
12
+ }
13
+ return runtime;
14
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Private chat (DM) security policy.
3
+ * Handles dmPolicy access control (open / pairing / allowlist).
4
+ */
5
+
6
+ import type { ResolvedInfoflowAccount } from "../types.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // DM policy check
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export type DmPolicyResult =
13
+ | { allowed: true }
14
+ | { allowed: false; reason: "allowlist-rejected" }
15
+ | { allowed: true; note: "pairing" };
16
+
17
+ /**
18
+ * Check if the sender is allowed to send private messages based on dmPolicy.
19
+ *
20
+ * - "open": all senders allowed
21
+ * - "pairing": allowed (pairing handled by the framework), note returned
22
+ * - "allowlist": only senders in allowFrom are allowed
23
+ */
24
+ export function checkDmPolicy(
25
+ account: ResolvedInfoflowAccount,
26
+ fromuser: string,
27
+ ): DmPolicyResult {
28
+ const dmPolicy = account.config.dmPolicy ?? "open";
29
+
30
+ if (dmPolicy === "allowlist") {
31
+ const allowFrom = account.config.allowFrom ?? [];
32
+ if (!allowFrom.includes(fromuser)) {
33
+ return { allowed: false, reason: "allowlist-rejected" };
34
+ }
35
+ }
36
+
37
+ if (dmPolicy === "pairing") {
38
+ return { allowed: true, note: "pairing" };
39
+ }
40
+
41
+ return { allowed: true };
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Group policy check
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export type GroupPolicyResult =
49
+ | { allowed: true }
50
+ | { allowed: false; reason: "disabled" }
51
+ | { allowed: false; reason: "allowlist-rejected"; groupIdStr: string; wasMentioned: boolean };
52
+
53
+ /**
54
+ * Check if the group is allowed to receive bot responses based on groupPolicy.
55
+ *
56
+ * - "open": all groups allowed
57
+ * - "disabled": no groups allowed
58
+ * - "allowlist": only groups in groupAllowFrom are allowed
59
+ */
60
+ export function checkGroupPolicy(
61
+ account: ResolvedInfoflowAccount,
62
+ groupId: number | undefined,
63
+ wasMentioned: boolean,
64
+ ): GroupPolicyResult {
65
+ const groupPolicy = account.config.groupPolicy ?? "open";
66
+
67
+ if (groupPolicy === "disabled") {
68
+ return { allowed: false, reason: "disabled" };
69
+ }
70
+
71
+ if (groupPolicy === "allowlist") {
72
+ const groupAllowFrom = account.config.groupAllowFrom ?? [];
73
+ const groupIdStr = groupId != null ? String(groupId) : "";
74
+ if (!groupAllowFrom.includes(groupIdStr)) {
75
+ return { allowed: false, reason: "allowlist-rejected", groupIdStr, wasMentioned };
76
+ }
77
+ }
78
+
79
+ return { allowed: true };
80
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Group chat security policy.
3
+ * Handles @mention detection, group access control, replyMode gating,
4
+ * follow-up window tracking, and conditional-reply prompt builders.
5
+ */
6
+
7
+ import type {
8
+ InfoflowReplyMode,
9
+ InfoflowGroupConfig,
10
+ InfoflowMentionIds,
11
+ ResolvedInfoflowAccount,
12
+ } from "../types.js";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Body item type (inbound group messages)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export type InfoflowBodyItem = {
19
+ type?: string;
20
+ content?: string;
21
+ label?: string;
22
+ /** 机器人 AT 时有此字段(数字),与 userid 互斥 */
23
+ robotid?: number;
24
+ /** AT 元素的显示名称 */
25
+ name?: string;
26
+ /** 人类用户 AT 时有此字段(uuap name),与 robotid 互斥 */
27
+ userid?: string;
28
+ /** IMAGE 类型 body item 的图片下载地址 */
29
+ downloadurl?: string;
30
+ };
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // @mention detection
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Check if the bot was @mentioned in the message body.
38
+ * Matches by robotName against the AT item's display name (case-insensitive).
39
+ */
40
+ export function checkBotMentioned(bodyItems: InfoflowBodyItem[], robotName?: string): boolean {
41
+ if (!robotName) return false;
42
+ const normalizedRobotName = robotName.toLowerCase();
43
+ for (const item of bodyItems) {
44
+ if (item.type !== "AT") continue;
45
+ if (item.name?.toLowerCase() === normalizedRobotName) return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Check if any entry in the watchlist was @mentioned in the message body.
52
+ * Matching priority: userid > robotid (parsed as number) > name (fallback).
53
+ * Returns the matched ID (from watchMentions), or undefined if none matched.
54
+ */
55
+ export function checkWatchMentioned(
56
+ bodyItems: InfoflowBodyItem[],
57
+ watchMentions: string[],
58
+ ): string | undefined {
59
+ if (!watchMentions.length) return undefined;
60
+ const normalizedIds = watchMentions.map((n) => n.toLowerCase());
61
+ const numericIds = watchMentions.map((n) => {
62
+ const num = Number(n);
63
+ return Number.isFinite(num) ? num : null;
64
+ });
65
+
66
+ for (const item of bodyItems) {
67
+ if (item.type !== "AT") continue;
68
+
69
+ if (item.userid) {
70
+ const idx = normalizedIds.indexOf(item.userid.toLowerCase());
71
+ if (idx !== -1) return watchMentions[idx];
72
+ }
73
+ if (item.robotid != null) {
74
+ const idx = numericIds.indexOf(item.robotid);
75
+ if (idx !== -1) return watchMentions[idx];
76
+ }
77
+ if (item.name) {
78
+ const idx = normalizedIds.indexOf(item.name.toLowerCase());
79
+ if (idx !== -1) return watchMentions[idx];
80
+ }
81
+ }
82
+ return undefined;
83
+ }
84
+
85
+ /** Check if message content matches the configured watchRegex regex pattern */
86
+ export function checkWatchRegex(mes: string, pattern: string): boolean {
87
+ try {
88
+ return new RegExp(pattern, "i").test(mes);
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Extract non-bot mention IDs from inbound group message body items.
96
+ * Returns human userIds and robot agentIds (excluding the bot itself).
97
+ */
98
+ export function extractMentionIds(
99
+ bodyItems: InfoflowBodyItem[],
100
+ robotName?: string,
101
+ ): InfoflowMentionIds {
102
+ const normalizedRobotName = robotName?.toLowerCase();
103
+ const userIds: string[] = [];
104
+ const agentIds: number[] = [];
105
+ const seenUsers = new Set<string>();
106
+ const seenAgents = new Set<number>();
107
+
108
+ for (const item of bodyItems) {
109
+ if (item.type !== "AT") continue;
110
+
111
+ if (item.robotid != null) {
112
+ if (normalizedRobotName && item.name?.toLowerCase() === normalizedRobotName) continue;
113
+ if (!seenAgents.has(item.robotid)) {
114
+ seenAgents.add(item.robotid);
115
+ agentIds.push(item.robotid);
116
+ }
117
+ } else if (item.userid) {
118
+ const key = item.userid.toLowerCase();
119
+ if (!seenUsers.has(key)) {
120
+ seenUsers.add(key);
121
+ userIds.push(item.userid);
122
+ }
123
+ }
124
+ }
125
+ return { userIds, agentIds };
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Follow-up window tracking (in-memory)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /** In-memory map tracking bot's last reply timestamp per group */
133
+ const groupLastReplyMap = new Map<string, number>();
134
+
135
+ /** Record that the bot replied to a group */
136
+ export function recordGroupReply(groupId: string): void {
137
+ groupLastReplyMap.set(groupId, Date.now());
138
+ }
139
+
140
+ /** Check if a group is within the follow-up window */
141
+ export function isWithinFollowUpWindow(groupId: string, windowSeconds: number): boolean {
142
+ const lastReply = groupLastReplyMap.get(groupId);
143
+ if (!lastReply) return false;
144
+ return Date.now() - lastReply < windowSeconds * 1000;
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Group config resolution
149
+ // ---------------------------------------------------------------------------
150
+
151
+ export type ResolvedGroupConfig = {
152
+ replyMode: InfoflowReplyMode;
153
+ followUp: boolean;
154
+ followUpWindow: number;
155
+ watchMentions: string[];
156
+ watchRegex?: string;
157
+ systemPrompt?: string;
158
+ };
159
+
160
+ /** Infer replyMode from legacy requireMention + watchMentions fields */
161
+ export function inferLegacyReplyMode(account: ResolvedInfoflowAccount): InfoflowReplyMode {
162
+ const requireMention = account.config.requireMention !== false;
163
+ const hasWatch = (account.config.watchMentions ?? []).length > 0;
164
+ if (!requireMention) return "proactive";
165
+ if (hasWatch) return "mention-and-watch";
166
+ return "mention-only";
167
+ }
168
+
169
+ /** Resolve effective group config by merging group-level → account-level → legacy defaults */
170
+ export function resolveGroupConfig(
171
+ account: ResolvedInfoflowAccount,
172
+ groupId?: number,
173
+ ): ResolvedGroupConfig {
174
+ const groupCfg: InfoflowGroupConfig | undefined =
175
+ groupId != null ? account.config.groups?.[String(groupId)] : undefined;
176
+ return {
177
+ replyMode: groupCfg?.replyMode ?? account.config.replyMode ?? inferLegacyReplyMode(account),
178
+ followUp: groupCfg?.followUp ?? account.config.followUp ?? true,
179
+ followUpWindow: groupCfg?.followUpWindow ?? account.config.followUpWindow ?? 300,
180
+ watchMentions: groupCfg?.watchMentions ?? account.config.watchMentions ?? [],
181
+ watchRegex: groupCfg?.watchRegex ?? account.config.watchRegex,
182
+ systemPrompt: groupCfg?.systemPrompt,
183
+ };
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Conditional-reply prompt builders
188
+ // ---------------------------------------------------------------------------
189
+
190
+ function buildReplyJudgmentRules(): string {
191
+ return [
192
+ "# Rules",
193
+ "",
194
+ "## Can answer or help → Reply directly",
195
+ "",
196
+ "Reply if ANY of these apply:",
197
+ "- The question can be answered through common sense or logical reasoning (e.g. math, general knowledge)",
198
+ "- You can find relevant clues or content in your knowledge base, documentation, or code",
199
+ "- You have sufficient domain expertise to provide a valuable reference",
200
+ "",
201
+ "## Cannot answer → Reply with NO_REPLY only",
202
+ "",
203
+ "Do NOT reply if ANY of these apply:",
204
+ "- The message contains no clear question or request (e.g. casual chat, meaningless content)",
205
+ "- The question involves private information or context you have no knowledge of",
206
+ "- You cannot understand the core intent of the message",
207
+ "",
208
+ "# Response format",
209
+ "",
210
+ "- When you can answer: give a direct, concise answer. Do not explain why you chose to answer.",
211
+ "- When you cannot answer: output only NO_REPLY with no other text.",
212
+ ].join("\n");
213
+ }
214
+
215
+ /** GroupSystemPrompt for watch-mention triggered messages */
216
+ export function buildWatchMentionPrompt(mentionedId: string): string {
217
+ return [
218
+ `Someone in the group @mentioned ${mentionedId}. As ${mentionedId}'s assistant, you observed this message.`,
219
+ "Decide whether you can answer on their behalf or provide help.",
220
+ "",
221
+ buildReplyJudgmentRules(),
222
+ "",
223
+ "# Examples",
224
+ "",
225
+ 'Message: "What is 1+1?"',
226
+ "→ 2",
227
+ "",
228
+ 'Message: "What is the qt parameter for search requests in the client code?"',
229
+ "(Assuming documentation records qt=s)",
230
+ "→ According to the documentation, the qt parameter for search requests is qt=s",
231
+ "",
232
+ 'Message: "asdfghjkl random gibberish"',
233
+ "→ NO_REPLY",
234
+ "",
235
+ 'Message: "Can you check today\'s release progress?"',
236
+ "(Assuming no relevant information available)",
237
+ "→ NO_REPLY",
238
+ ].join("\n");
239
+ }
240
+
241
+ /** GroupSystemPrompt for watch-content (regex) triggered messages */
242
+ export function buildWatchRegexPrompt(pattern: string): string {
243
+ return [
244
+ `The message content matched the configured watch pattern (${pattern}).`,
245
+ "As the group assistant, you observed this message. Decide whether you can provide help or a valuable reply.",
246
+ "",
247
+ buildReplyJudgmentRules(),
248
+ ].join("\n");
249
+ }
250
+
251
+ /** GroupSystemPrompt for follow-up replies after bot's last response */
252
+ export function buildFollowUpPrompt(): string {
253
+ return [
254
+ "You just replied to a message in this group. Someone has now sent a new message.",
255
+ "First determine if this message is a follow-up or continuation of the same topic you previously replied to, then decide if you can continue to help.",
256
+ "",
257
+ "Note: If this message is clearly a new topic or unrelated to your previous reply, respond with NO_REPLY.",
258
+ "",
259
+ buildReplyJudgmentRules(),
260
+ ].join("\n");
261
+ }
262
+
263
+ /** GroupSystemPrompt for proactive mode */
264
+ export function buildProactivePrompt(): string {
265
+ return [
266
+ "You observed this message in the group. Decide whether you can provide help or a valuable reply.",
267
+ "If you need more context or clarification, you may ask follow-up questions.",
268
+ "",
269
+ buildReplyJudgmentRules(),
270
+ ].join("\n");
271
+ }