@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/LICENSE +21 -0
- package/README.md +74 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +41 -0
- package/src/accounts.ts +131 -0
- package/src/bot.ts +338 -0
- package/src/channel.ts +307 -0
- package/src/infoflow-req-parse.ts +451 -0
- package/src/logging.ts +141 -0
- package/src/monitor.ts +177 -0
- package/src/reply-dispatcher.ts +88 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +527 -0
- package/src/targets.ts +130 -0
- package/src/types.ts +127 -0
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
|
+
};
|