@chbo297/infoflow 2026.2.23

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/channel.ts ADDED
@@ -0,0 +1,307 @@
1
+ import {
2
+ applyAccountNameToChannelSection,
3
+ DEFAULT_ACCOUNT_ID,
4
+ deleteAccountFromConfigSection,
5
+ formatPairingApproveHint,
6
+ migrateBaseNameToDefaultAccount,
7
+ normalizeAccountId,
8
+ setAccountEnabledInConfigSection,
9
+ type ChannelPlugin,
10
+ type OpenClawConfig,
11
+ } from "openclaw/plugin-sdk";
12
+ import {
13
+ getChannelSection,
14
+ listInfoflowAccountIds,
15
+ resolveDefaultInfoflowAccountId,
16
+ resolveInfoflowAccount,
17
+ } from "./accounts.js";
18
+ import { getInfoflowSendLog } from "./logging.js";
19
+ import { startInfoflowMonitor } from "./monitor.js";
20
+ import { getInfoflowRuntime } from "./runtime.js";
21
+ import { sendInfoflowMessage } from "./send.js";
22
+ import { normalizeInfoflowTarget, looksLikeInfoflowId } from "./targets.js";
23
+ import type { InfoflowMessageContentItem, ResolvedInfoflowAccount } from "./types.js";
24
+
25
+ // Re-export types and account functions for external consumers
26
+ export type { InfoflowAccountConfig, ResolvedInfoflowAccount } from "./types.js";
27
+ export { resolveInfoflowAccount } from "./accounts.js";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Channel plugin
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
34
+ id: "infoflow",
35
+ meta: {
36
+ id: "infoflow",
37
+ label: "Infoflow",
38
+ selectionLabel: "Infoflow (如流)",
39
+ docsPath: "/channels/infoflow",
40
+ blurb: "Baidu Infoflow enterprise messaging platform.",
41
+ showConfigured: true,
42
+ },
43
+ capabilities: {
44
+ chatTypes: ["direct", "group"],
45
+ nativeCommands: true,
46
+ },
47
+ reload: { configPrefixes: ["channels.infoflow"] },
48
+ config: {
49
+ listAccountIds: (cfg) => listInfoflowAccountIds(cfg),
50
+ resolveAccount: (cfg, accountId) => resolveInfoflowAccount({ cfg, accountId }),
51
+ defaultAccountId: (cfg) => resolveDefaultInfoflowAccountId(cfg),
52
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
53
+ setAccountEnabledInConfigSection({
54
+ cfg,
55
+ sectionKey: "infoflow",
56
+ accountId,
57
+ enabled,
58
+ allowTopLevel: true,
59
+ }),
60
+ deleteAccount: ({ cfg, accountId }) =>
61
+ deleteAccountFromConfigSection({
62
+ cfg,
63
+ sectionKey: "infoflow",
64
+ accountId,
65
+ clearBaseFields: ["checkToken", "encodingAESKey", "appKey", "appSecret", "name"],
66
+ }),
67
+ isConfigured: (account) => account.configured,
68
+ describeAccount: (account) => ({
69
+ accountId: account.accountId,
70
+ name: account.name,
71
+ enabled: account.enabled,
72
+ configured: account.configured,
73
+ }),
74
+ },
75
+ security: {
76
+ resolveDmPolicy: ({ cfg, accountId, account }) => {
77
+ const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
78
+ const channelCfg = getChannelSection(cfg);
79
+ const useAccountPath = Boolean(channelCfg?.accounts?.[resolvedAccountId]);
80
+ const basePath = useAccountPath
81
+ ? `channels.infoflow.accounts.${resolvedAccountId}.`
82
+ : "channels.infoflow.";
83
+
84
+ return {
85
+ policy: ((account.config as Record<string, unknown>).dmPolicy as string) ?? "open",
86
+ allowFrom: ((account.config as Record<string, unknown>).allowFrom as string[]) ?? [],
87
+ policyPath: `${basePath}dmPolicy`,
88
+ allowFromPath: basePath,
89
+ approveHint: formatPairingApproveHint("infoflow"),
90
+ normalizeEntry: (raw: string) => raw.replace(/^infoflow:/i, ""),
91
+ };
92
+ },
93
+ collectWarnings: ({ account, cfg }) => {
94
+ const warnings: string[] = [];
95
+ const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
96
+ const groupPolicy =
97
+ ((account.config as Record<string, unknown>).groupPolicy as string) ??
98
+ defaultGroupPolicy ??
99
+ "open";
100
+
101
+ if (groupPolicy === "open") {
102
+ warnings.push(
103
+ `- Infoflow groups: groupPolicy="open" allows any group to trigger. Consider setting channels.infoflow.groupPolicy="allowlist".`,
104
+ );
105
+ }
106
+ return warnings;
107
+ },
108
+ },
109
+ groups: {
110
+ resolveRequireMention: ({ cfg, accountId }) => {
111
+ const channelCfg = getChannelSection(cfg);
112
+ const accountCfg =
113
+ accountId && accountId !== DEFAULT_ACCOUNT_ID
114
+ ? channelCfg?.accounts?.[accountId]
115
+ : channelCfg;
116
+ return (accountCfg as Record<string, unknown> | undefined)?.requireMention !== false;
117
+ },
118
+ resolveToolPolicy: () => {
119
+ // Return undefined to use global policy
120
+ return undefined;
121
+ },
122
+ },
123
+ messaging: {
124
+ normalizeTarget: (raw) => normalizeInfoflowTarget(raw),
125
+ targetResolver: {
126
+ looksLikeId: looksLikeInfoflowId,
127
+ hint: "<username|group:groupId>",
128
+ },
129
+ },
130
+ setup: {
131
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
132
+ applyAccountName: ({ cfg, accountId, name }) =>
133
+ applyAccountNameToChannelSection({
134
+ cfg,
135
+ channelKey: "infoflow",
136
+ accountId,
137
+ name,
138
+ }),
139
+ validateInput: ({ input }) => {
140
+ if (!input.token) {
141
+ return "Infoflow requires --token (checkToken).";
142
+ }
143
+ return null;
144
+ },
145
+ applyAccountConfig: ({ cfg, accountId, input }) => {
146
+ const namedConfig = applyAccountNameToChannelSection({
147
+ cfg,
148
+ channelKey: "infoflow",
149
+ accountId,
150
+ name: input.name,
151
+ });
152
+ const next =
153
+ accountId !== DEFAULT_ACCOUNT_ID
154
+ ? migrateBaseNameToDefaultAccount({ cfg: namedConfig, channelKey: "infoflow" })
155
+ : namedConfig;
156
+
157
+ const patch: Record<string, unknown> = {};
158
+ if (input.token) {
159
+ patch.checkToken = input.token;
160
+ }
161
+
162
+ const existing = (next.channels?.["infoflow"] ?? {}) as Record<string, unknown>;
163
+ if (accountId === DEFAULT_ACCOUNT_ID) {
164
+ return {
165
+ ...next,
166
+ channels: {
167
+ ...next.channels,
168
+ infoflow: {
169
+ ...existing,
170
+ enabled: true,
171
+ ...patch,
172
+ },
173
+ },
174
+ } as OpenClawConfig;
175
+ }
176
+ const existingAccounts = (existing.accounts ?? {}) as Record<string, Record<string, unknown>>;
177
+ return {
178
+ ...next,
179
+ channels: {
180
+ ...next.channels,
181
+ infoflow: {
182
+ ...existing,
183
+ enabled: true,
184
+ accounts: {
185
+ ...existingAccounts,
186
+ [accountId]: {
187
+ ...existingAccounts[accountId],
188
+ enabled: true,
189
+ ...patch,
190
+ },
191
+ },
192
+ },
193
+ },
194
+ } as OpenClawConfig;
195
+ },
196
+ },
197
+ outbound: {
198
+ deliveryMode: "direct",
199
+ chunkerMode: "markdown",
200
+ textChunkLimit: 4000,
201
+ chunker: (text, limit) => getInfoflowRuntime().channel.text.chunkText(text, limit),
202
+ sendText: async ({ cfg, to, text, accountId }) => {
203
+ const verbose = getInfoflowRuntime().logging.shouldLogVerbose();
204
+ if (verbose) {
205
+ getInfoflowSendLog().debug?.(`[infoflow:sendText] to=${to}, accountId=${accountId}`);
206
+ }
207
+ // Use "markdown" type even though param is named `text`: LLM outputs are often markdown,
208
+ // and Infoflow's markdown type handles both plain text and markdown seamlessly.
209
+ const result = await sendInfoflowMessage({
210
+ cfg,
211
+ to,
212
+ contents: [{ type: "markdown", content: text }],
213
+ accountId: accountId ?? undefined,
214
+ });
215
+ return {
216
+ channel: "infoflow",
217
+ messageId: result.ok ? (result.messageId ?? "sent") : "failed",
218
+ };
219
+ },
220
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
221
+ const verbose = getInfoflowRuntime().logging.shouldLogVerbose();
222
+ if (verbose) {
223
+ getInfoflowSendLog().debug?.(
224
+ `[infoflow:sendMedia] to=${to}, accountId=${accountId}, mediaUrl=${mediaUrl}`,
225
+ );
226
+ }
227
+
228
+ // Build contents array: text (if provided) + link for media URL
229
+ const contents: InfoflowMessageContentItem[] = [];
230
+ const trimmedText = text?.trim();
231
+ if (trimmedText) {
232
+ // Use "markdown" type even though param is named `text`: LLM outputs are often markdown,
233
+ // and Infoflow's markdown type handles both plain text and markdown seamlessly.
234
+ contents.push({ type: "markdown", content: trimmedText });
235
+ }
236
+ if (mediaUrl) {
237
+ contents.push({ type: "link", content: mediaUrl });
238
+ }
239
+
240
+ // Fallback: if no valid content, return early
241
+ if (contents.length === 0) {
242
+ return {
243
+ channel: "infoflow",
244
+ messageId: "failed",
245
+ };
246
+ }
247
+
248
+ const result = await sendInfoflowMessage({
249
+ cfg,
250
+ to,
251
+ contents,
252
+ accountId: accountId ?? undefined,
253
+ });
254
+ return {
255
+ channel: "infoflow",
256
+ messageId: result.ok ? (result.messageId ?? "sent") : "failed",
257
+ };
258
+ },
259
+ },
260
+ status: {
261
+ defaultRuntime: {
262
+ accountId: DEFAULT_ACCOUNT_ID,
263
+ running: false,
264
+ lastStartAt: null,
265
+ lastStopAt: null,
266
+ lastError: null,
267
+ },
268
+ buildAccountSnapshot: ({ account, runtime }) => ({
269
+ accountId: account.accountId,
270
+ name: account.name,
271
+ enabled: account.enabled,
272
+ configured: account.configured,
273
+ running: runtime?.running ?? false,
274
+ lastStartAt: runtime?.lastStartAt ?? null,
275
+ lastStopAt: runtime?.lastStopAt ?? null,
276
+ lastError: runtime?.lastError ?? null,
277
+ lastInboundAt: runtime?.lastInboundAt ?? null,
278
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
279
+ }),
280
+ },
281
+ gateway: {
282
+ startAccount: async (ctx) => {
283
+ const account = ctx.account;
284
+ ctx.log?.info(`[${account.accountId}] starting Infoflow webhook`);
285
+ ctx.setStatus({
286
+ accountId: account.accountId,
287
+ running: true,
288
+ lastStartAt: Date.now(),
289
+ });
290
+ const unregister = await startInfoflowMonitor({
291
+ account,
292
+ config: ctx.cfg,
293
+ runtime: ctx.runtime,
294
+ abortSignal: ctx.abortSignal,
295
+ statusSink: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }),
296
+ });
297
+ return () => {
298
+ unregister?.();
299
+ ctx.setStatus({
300
+ accountId: account.accountId,
301
+ running: false,
302
+ lastStopAt: Date.now(),
303
+ });
304
+ };
305
+ },
306
+ },
307
+ };