@chbo297/infoflow 2026.3.18 → 2026.5.4
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/README.md +24 -528
- package/dist/index.js +21 -0
- package/dist/src/accounts.js +110 -0
- package/dist/src/actions.js +386 -0
- package/dist/src/bot.js +1010 -0
- package/dist/src/channel.js +385 -0
- package/dist/src/infoflow-req-parse.js +394 -0
- package/dist/src/logging.js +102 -0
- package/dist/src/markdown-local-images.js +65 -0
- package/dist/src/media.js +318 -0
- package/dist/src/monitor.js +145 -0
- package/dist/src/reply-dispatcher.js +301 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/send.js +820 -0
- package/dist/src/sent-message-store.js +190 -0
- package/dist/src/targets.js +90 -0
- package/dist/src/types.js +4 -0
- package/dist/src/ws-receiver.js +378 -0
- package/openclaw.plugin.json +194 -0
- package/package.json +18 -3
- package/scripts/deploy.sh +215 -0
- package/src/accounts.ts +25 -3
- package/src/actions.ts +9 -3
- package/src/bot.ts +63 -20
- package/src/channel.ts +64 -45
- package/src/infoflow-req-parse.ts +2 -2
- package/src/infoflow-sdk.d.ts +12 -0
- package/src/monitor.ts +21 -2
- package/src/reply-dispatcher.ts +2 -5
- package/src/types.ts +11 -0
- package/src/ws-receiver.ts +482 -0
- package/tsconfig.build.json +6 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
2
|
+
import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "./logging.js";
|
|
3
|
+
import { parseMarkdownForLocalImages } from "./markdown-local-images.js";
|
|
4
|
+
import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "./media.js";
|
|
5
|
+
import { getInfoflowRuntime } from "./runtime.js";
|
|
6
|
+
import { sendInfoflowMessage } from "./send.js";
|
|
7
|
+
const PREVIEW_MAX_LENGTH = 100;
|
|
8
|
+
function truncatePreview(text) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return "";
|
|
11
|
+
if (text.length <= PREVIEW_MAX_LENGTH)
|
|
12
|
+
return text;
|
|
13
|
+
return text.slice(0, PREVIEW_MAX_LENGTH) + "...";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds dispatcherOptions and replyOptions for dispatchReplyWithBufferedBlockDispatcher.
|
|
17
|
+
* Encapsulates prefix options, chunked deliver (send via Infoflow API + statusSink), and onError.
|
|
18
|
+
*/
|
|
19
|
+
export function createInfoflowReplyDispatcher(params) {
|
|
20
|
+
const { cfg, agentId, accountId, to, statusSink, atOptions, mentionIds, replyToMessageId, replyToPreview, mediaLocalRoots, } = params;
|
|
21
|
+
const core = getInfoflowRuntime();
|
|
22
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
23
|
+
cfg,
|
|
24
|
+
agentId,
|
|
25
|
+
channel: "infoflow",
|
|
26
|
+
accountId,
|
|
27
|
+
});
|
|
28
|
+
// Check if target is a group (format: group:<id>)
|
|
29
|
+
const isGroup = /^group:\d+$/i.test(to);
|
|
30
|
+
// Build id→type map for resolving @id in LLM output (distinguishes user vs agent)
|
|
31
|
+
const mentionIdMap = new Map();
|
|
32
|
+
if (mentionIds) {
|
|
33
|
+
for (const id of mentionIds.userIds) {
|
|
34
|
+
mentionIdMap.set(id.toLowerCase(), "user");
|
|
35
|
+
}
|
|
36
|
+
for (const id of mentionIds.agentIds) {
|
|
37
|
+
mentionIdMap.set(String(id).toLowerCase(), "agent");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Build replyTo context (only used for the first outbound message)
|
|
41
|
+
const replyTo = isGroup && replyToMessageId
|
|
42
|
+
? { messageid: replyToMessageId, preview: truncatePreview(replyToPreview) }
|
|
43
|
+
: undefined;
|
|
44
|
+
let replyApplied = false;
|
|
45
|
+
const deliver = async (payload) => {
|
|
46
|
+
const text = payload.text ?? "";
|
|
47
|
+
logVerbose(`[infoflow] deliver called: to=${to}, text=${text}`);
|
|
48
|
+
// Normalize media URL list (same pattern as Feishu reply-dispatcher)
|
|
49
|
+
const mediaList = payload.mediaUrls && payload.mediaUrls.length > 0
|
|
50
|
+
? payload.mediaUrls
|
|
51
|
+
: payload.mediaUrl
|
|
52
|
+
? [payload.mediaUrl]
|
|
53
|
+
: [];
|
|
54
|
+
if (!text.trim() && mediaList.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// --- Text handling (existing logic) ---
|
|
58
|
+
if (text.trim()) {
|
|
59
|
+
// Resolve @id patterns in LLM output text to user/agent IDs
|
|
60
|
+
const resolvedUserIds = [];
|
|
61
|
+
const resolvedAgentIds = [];
|
|
62
|
+
if (isGroup && mentionIdMap.size > 0) {
|
|
63
|
+
const mentionPattern = /@([\w.]+)/g;
|
|
64
|
+
let match;
|
|
65
|
+
while ((match = mentionPattern.exec(text)) !== null) {
|
|
66
|
+
const id = match[1];
|
|
67
|
+
const type = mentionIdMap.get(id.toLowerCase());
|
|
68
|
+
if (type === "user" && !resolvedUserIds.includes(id)) {
|
|
69
|
+
resolvedUserIds.push(id);
|
|
70
|
+
}
|
|
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
|
+
// Merge atOptions user IDs (sender echo-back) with LLM-resolved user IDs
|
|
80
|
+
const atOptionIds = atOptions?.atAll ? [] : (atOptions?.atUserIds ?? []);
|
|
81
|
+
const allAtUserIds = [...atOptionIds];
|
|
82
|
+
for (const id of resolvedUserIds) {
|
|
83
|
+
if (!allAtUserIds.includes(id)) {
|
|
84
|
+
allAtUserIds.push(id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const hasAtAll = atOptions?.atAll === true;
|
|
88
|
+
const hasAtUsers = allAtUserIds.length > 0;
|
|
89
|
+
const hasAtAgents = resolvedAgentIds.length > 0;
|
|
90
|
+
// Prepend AT mentions to the text if needed (group messages only)
|
|
91
|
+
// Only prepend for atOptions IDs; LLM text already contains @id for resolved mentions
|
|
92
|
+
let messageText = text;
|
|
93
|
+
if (isGroup && atOptions) {
|
|
94
|
+
let atPrefix = "";
|
|
95
|
+
if (hasAtAll) {
|
|
96
|
+
atPrefix = "@all ";
|
|
97
|
+
}
|
|
98
|
+
else if (atOptions.atUserIds?.length) {
|
|
99
|
+
atPrefix = atOptions.atUserIds.map((id) => `@${id}`).join(" ") + " ";
|
|
100
|
+
}
|
|
101
|
+
messageText = atPrefix + text;
|
|
102
|
+
}
|
|
103
|
+
// Chunk text to 2048 chars max (Infoflow limit)
|
|
104
|
+
const chunks = core.channel.text.chunkText(messageText, 2048);
|
|
105
|
+
let isFirstChunk = true;
|
|
106
|
+
const textPromises = [];
|
|
107
|
+
for (const chunk of chunks) {
|
|
108
|
+
const segments = parseMarkdownForLocalImages(chunk);
|
|
109
|
+
for (const segment of segments) {
|
|
110
|
+
const chunkReplyTo = !replyApplied ? replyTo : undefined;
|
|
111
|
+
if (segment.type === "text") {
|
|
112
|
+
const contents = [];
|
|
113
|
+
if (isFirstChunk && isGroup) {
|
|
114
|
+
if (hasAtAll) {
|
|
115
|
+
contents.push({ type: "at", content: "all" });
|
|
116
|
+
}
|
|
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(",") });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const trimmed = segment.content.trim();
|
|
125
|
+
if (contents.length > 0 || trimmed) {
|
|
126
|
+
contents.push({ type: "markdown", content: segment.content });
|
|
127
|
+
textPromises.push(sendInfoflowMessage({
|
|
128
|
+
cfg,
|
|
129
|
+
to,
|
|
130
|
+
contents,
|
|
131
|
+
accountId,
|
|
132
|
+
replyTo: chunkReplyTo,
|
|
133
|
+
}));
|
|
134
|
+
if (chunkReplyTo)
|
|
135
|
+
replyApplied = true;
|
|
136
|
+
}
|
|
137
|
+
isFirstChunk = false;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// segment.type === "image"
|
|
141
|
+
if (isFirstChunk && isGroup && (hasAtAll || hasAtUsers || hasAtAgents)) {
|
|
142
|
+
const atContents = [];
|
|
143
|
+
if (hasAtAll)
|
|
144
|
+
atContents.push({ type: "at", content: "all" });
|
|
145
|
+
else if (hasAtUsers)
|
|
146
|
+
atContents.push({ type: "at", content: allAtUserIds.join(",") });
|
|
147
|
+
if (hasAtAgents)
|
|
148
|
+
atContents.push({ type: "at-agent", content: resolvedAgentIds.join(",") });
|
|
149
|
+
atContents.push({ type: "markdown", content: "" });
|
|
150
|
+
textPromises.push(sendInfoflowMessage({
|
|
151
|
+
cfg,
|
|
152
|
+
to,
|
|
153
|
+
contents: atContents,
|
|
154
|
+
accountId,
|
|
155
|
+
replyTo: chunkReplyTo,
|
|
156
|
+
}));
|
|
157
|
+
if (chunkReplyTo)
|
|
158
|
+
replyApplied = true;
|
|
159
|
+
}
|
|
160
|
+
isFirstChunk = false;
|
|
161
|
+
try {
|
|
162
|
+
const prepared = await prepareInfoflowImageBase64({
|
|
163
|
+
mediaUrl: segment.content,
|
|
164
|
+
mediaLocalRoots: mediaLocalRoots ?? undefined,
|
|
165
|
+
});
|
|
166
|
+
if (prepared.isImage) {
|
|
167
|
+
const segmentReplyTo = !replyApplied ? replyTo : undefined;
|
|
168
|
+
textPromises.push(sendInfoflowImageMessage({
|
|
169
|
+
cfg,
|
|
170
|
+
to,
|
|
171
|
+
base64Image: prepared.base64,
|
|
172
|
+
accountId,
|
|
173
|
+
replyTo: segmentReplyTo,
|
|
174
|
+
}).then((r) => {
|
|
175
|
+
if (r.ok)
|
|
176
|
+
return r;
|
|
177
|
+
logVerbose(`[infoflow] native image send failed: ${r.error}, falling back to link`);
|
|
178
|
+
return sendInfoflowMessage({
|
|
179
|
+
cfg,
|
|
180
|
+
to,
|
|
181
|
+
contents: [{ type: "link", content: segment.content }],
|
|
182
|
+
accountId,
|
|
183
|
+
replyTo: segmentReplyTo,
|
|
184
|
+
});
|
|
185
|
+
}));
|
|
186
|
+
if (!replyApplied)
|
|
187
|
+
replyApplied = true;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
textPromises.push(sendInfoflowMessage({
|
|
191
|
+
cfg,
|
|
192
|
+
to,
|
|
193
|
+
contents: [{ type: "link", content: segment.content }],
|
|
194
|
+
accountId,
|
|
195
|
+
replyTo: !replyApplied ? replyTo : undefined,
|
|
196
|
+
}));
|
|
197
|
+
if (!replyApplied)
|
|
198
|
+
replyApplied = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
logVerbose(`[infoflow] image prep failed in text segment, falling back to link: ${err}`);
|
|
203
|
+
textPromises.push(sendInfoflowMessage({
|
|
204
|
+
cfg,
|
|
205
|
+
to,
|
|
206
|
+
contents: [{ type: "link", content: segment.content }],
|
|
207
|
+
accountId,
|
|
208
|
+
replyTo: !replyApplied ? replyTo : undefined,
|
|
209
|
+
}));
|
|
210
|
+
if (!replyApplied)
|
|
211
|
+
replyApplied = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (textPromises.length > 0) {
|
|
216
|
+
const results = await Promise.all(textPromises);
|
|
217
|
+
for (const result of results) {
|
|
218
|
+
if (result?.ok) {
|
|
219
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
220
|
+
}
|
|
221
|
+
else if (result?.error) {
|
|
222
|
+
getInfoflowSendLog().error(`[infoflow] reply failed to=${to}, accountId=${accountId}: ${result.error}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// --- Media handling: send each media item as native image or fallback link (b-mode: collect then await) ---
|
|
228
|
+
const mediaPromises = [];
|
|
229
|
+
for (const mediaUrl of mediaList) {
|
|
230
|
+
const mediaReplyTo = !replyApplied ? replyTo : undefined;
|
|
231
|
+
try {
|
|
232
|
+
const prepared = await prepareInfoflowImageBase64({ mediaUrl });
|
|
233
|
+
if (prepared.isImage) {
|
|
234
|
+
mediaPromises.push(sendInfoflowImageMessage({
|
|
235
|
+
cfg,
|
|
236
|
+
to,
|
|
237
|
+
base64Image: prepared.base64,
|
|
238
|
+
accountId,
|
|
239
|
+
replyTo: mediaReplyTo,
|
|
240
|
+
}).then((r) => {
|
|
241
|
+
if (r.ok)
|
|
242
|
+
return r;
|
|
243
|
+
logVerbose(`[infoflow] native image send failed: ${r.error}, falling back to link`);
|
|
244
|
+
return sendInfoflowMessage({
|
|
245
|
+
cfg,
|
|
246
|
+
to,
|
|
247
|
+
contents: [{ type: "link", content: mediaUrl }],
|
|
248
|
+
accountId,
|
|
249
|
+
replyTo: mediaReplyTo,
|
|
250
|
+
});
|
|
251
|
+
}));
|
|
252
|
+
if (mediaReplyTo)
|
|
253
|
+
replyApplied = true;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
mediaPromises.push(sendInfoflowMessage({
|
|
257
|
+
cfg,
|
|
258
|
+
to,
|
|
259
|
+
contents: [{ type: "link", content: mediaUrl }],
|
|
260
|
+
accountId,
|
|
261
|
+
replyTo: mediaReplyTo,
|
|
262
|
+
}));
|
|
263
|
+
if (mediaReplyTo)
|
|
264
|
+
replyApplied = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
logVerbose(`[infoflow] image prep failed, falling back to link: ${err}`);
|
|
269
|
+
mediaPromises.push(sendInfoflowMessage({
|
|
270
|
+
cfg,
|
|
271
|
+
to,
|
|
272
|
+
contents: [{ type: "link", content: mediaUrl }],
|
|
273
|
+
accountId,
|
|
274
|
+
replyTo: mediaReplyTo,
|
|
275
|
+
}));
|
|
276
|
+
if (mediaReplyTo)
|
|
277
|
+
replyApplied = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (mediaPromises.length > 0) {
|
|
281
|
+
const results = await Promise.all(mediaPromises);
|
|
282
|
+
for (const result of results) {
|
|
283
|
+
if (result?.ok)
|
|
284
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const onError = (err) => {
|
|
289
|
+
getInfoflowSendLog().error(`[infoflow] reply error to=${to}, accountId=${accountId}: ${formatInfoflowError(err)}`);
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
dispatcherOptions: {
|
|
293
|
+
...prefixOptions,
|
|
294
|
+
deliver,
|
|
295
|
+
onError,
|
|
296
|
+
},
|
|
297
|
+
replyOptions: {
|
|
298
|
+
onModelSelected,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|