@chbo297/infoflow 2026.3.18 → 2026.5.5
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 +61 -517
- 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/cli.js +157 -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/{src/targets.ts → dist/src/targets.js} +46 -65
- package/dist/src/types.js +4 -0
- package/dist/src/ws-receiver.js +378 -0
- package/openclaw.plugin.json +194 -0
- package/package.json +30 -5
- package/scripts/deploy.sh +29 -0
- package/scripts/lib/deploy-common.sh +157 -0
- package/.claude/settings.local.json +0 -9
- package/CHANGELOG.md +0 -147
- package/index.ts +0 -23
- package/src/accounts.ts +0 -148
- package/src/actions.ts +0 -452
- package/src/bot.ts +0 -1192
- package/src/channel.ts +0 -424
- package/src/infoflow-req-parse.ts +0 -488
- package/src/logging.ts +0 -123
- package/src/markdown-local-images.ts +0 -75
- package/src/media.ts +0 -405
- package/src/monitor.ts +0 -169
- package/src/reply-dispatcher.ts +0 -367
- package/src/runtime.ts +0 -14
- package/src/send.ts +0 -986
- package/src/sent-message-store.ts +0 -267
- package/src/types.ts +0 -231
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infoflow account resolution and configuration helpers.
|
|
3
|
+
* Handles multi-account support with config merging.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/core";
|
|
6
|
+
const DEFAULT_INFOFLOW_WS_GATEWAY = "infoflow-open-gateway.weiyun.baidu.com";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Config Access Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Get the raw Infoflow channel section from config.
|
|
12
|
+
*/
|
|
13
|
+
export function getChannelSection(cfg) {
|
|
14
|
+
return cfg.channels?.["infoflow"];
|
|
15
|
+
}
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Account ID Resolution
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* List all configured Infoflow account IDs.
|
|
21
|
+
* Returns [DEFAULT_ACCOUNT_ID] if no accounts are configured (backward compatibility).
|
|
22
|
+
*/
|
|
23
|
+
export function listInfoflowAccountIds(cfg) {
|
|
24
|
+
const accounts = getChannelSection(cfg)?.accounts;
|
|
25
|
+
if (!accounts || typeof accounts !== "object") {
|
|
26
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
27
|
+
}
|
|
28
|
+
const ids = Object.keys(accounts).filter(Boolean);
|
|
29
|
+
return ids.length === 0 ? [DEFAULT_ACCOUNT_ID] : ids.toSorted((a, b) => a.localeCompare(b));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the default account ID for Infoflow.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveDefaultInfoflowAccountId(cfg) {
|
|
35
|
+
const channel = getChannelSection(cfg);
|
|
36
|
+
if (channel?.defaultAccount?.trim()) {
|
|
37
|
+
return channel.defaultAccount.trim();
|
|
38
|
+
}
|
|
39
|
+
const ids = listInfoflowAccountIds(cfg);
|
|
40
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
41
|
+
return DEFAULT_ACCOUNT_ID;
|
|
42
|
+
}
|
|
43
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Config Merging
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Merge top-level Infoflow config with account-specific overrides.
|
|
50
|
+
* Account fields override base fields.
|
|
51
|
+
*/
|
|
52
|
+
function mergeInfoflowAccountConfig(cfg, accountId) {
|
|
53
|
+
const raw = getChannelSection(cfg) ?? {};
|
|
54
|
+
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
|
55
|
+
const account = raw.accounts?.[accountId] ?? {};
|
|
56
|
+
return { ...base, ...account };
|
|
57
|
+
}
|
|
58
|
+
function normalizeWatchRegex(v) {
|
|
59
|
+
if (v == null)
|
|
60
|
+
return [];
|
|
61
|
+
return Array.isArray(v) ? v : [v];
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Account Resolution
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a complete Infoflow account with merged config.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveInfoflowAccount(params) {
|
|
70
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
71
|
+
const baseEnabled = getChannelSection(params.cfg)?.enabled !== false;
|
|
72
|
+
const merged = mergeInfoflowAccountConfig(params.cfg, accountId);
|
|
73
|
+
const accountEnabled = merged.enabled !== false;
|
|
74
|
+
const enabled = baseEnabled && accountEnabled;
|
|
75
|
+
const apiHost = merged.apiHost ?? "";
|
|
76
|
+
const checkToken = merged.checkToken ?? "";
|
|
77
|
+
const encodingAESKey = merged.encodingAESKey ?? "";
|
|
78
|
+
const appKey = merged.appKey ?? "";
|
|
79
|
+
const appSecret = merged.appSecret ?? "";
|
|
80
|
+
const effectiveConnectionMode = merged.connectionMode ?? "webhook";
|
|
81
|
+
const wsGateway = merged.wsGateway?.trim() || DEFAULT_INFOFLOW_WS_GATEWAY;
|
|
82
|
+
const wsConnectDomain = merged.wsConnectDomain?.trim() || undefined;
|
|
83
|
+
const configured = effectiveConnectionMode === "websocket"
|
|
84
|
+
? Boolean(appKey) && Boolean(appSecret)
|
|
85
|
+
: Boolean(checkToken) && Boolean(encodingAESKey) && Boolean(appKey) && Boolean(appSecret);
|
|
86
|
+
return {
|
|
87
|
+
accountId,
|
|
88
|
+
name: merged.name?.trim() || undefined,
|
|
89
|
+
enabled,
|
|
90
|
+
configured,
|
|
91
|
+
config: {
|
|
92
|
+
enabled: merged.enabled,
|
|
93
|
+
name: merged.name,
|
|
94
|
+
apiHost,
|
|
95
|
+
connectionMode: effectiveConnectionMode,
|
|
96
|
+
wsGateway,
|
|
97
|
+
wsConnectDomain,
|
|
98
|
+
checkToken,
|
|
99
|
+
encodingAESKey,
|
|
100
|
+
appKey,
|
|
101
|
+
appSecret,
|
|
102
|
+
robotName: merged.robotName?.trim() || undefined,
|
|
103
|
+
robotId: merged.robotId?.trim() || undefined,
|
|
104
|
+
requireMention: merged.requireMention,
|
|
105
|
+
watchMentions: merged.watchMentions,
|
|
106
|
+
watchRegex: normalizeWatchRegex(merged.watchRegex),
|
|
107
|
+
appAgentId: merged.appAgentId,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infoflow channel message actions adapter.
|
|
3
|
+
* Intercepts the "send" action from the message tool to support
|
|
4
|
+
* @all and @user mentions in group messages.
|
|
5
|
+
*/
|
|
6
|
+
import { jsonResult, readStringParam } from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
8
|
+
import { resolveInfoflowAccount } from "./accounts.js";
|
|
9
|
+
import { logVerbose } from "./logging.js";
|
|
10
|
+
import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "./media.js";
|
|
11
|
+
import { sendInfoflowMessage, recallInfoflowGroupMessage, recallInfoflowPrivateMessage, } from "./send.js";
|
|
12
|
+
import { findSentMessage, querySentMessages, removeRecalledMessages, } from "./sent-message-store.js";
|
|
13
|
+
import { normalizeInfoflowTarget } from "./targets.js";
|
|
14
|
+
// Recall result hint constants — reused across single/batch, group/private recall paths
|
|
15
|
+
const RECALL_OK_HINT = "Recall succeeded. output only NO_REPLY with no other text.";
|
|
16
|
+
const RECALL_FAIL_HINT = "Recall failed. Send a brief reply stating only the failure reason.";
|
|
17
|
+
const RECALL_PARTIAL_HINT = "Some recalls failed. Send a brief reply stating only the failure reason(s).";
|
|
18
|
+
export const infoflowMessageActions = {
|
|
19
|
+
describeMessageTool: () => ({
|
|
20
|
+
actions: ["send", "delete"],
|
|
21
|
+
}),
|
|
22
|
+
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
23
|
+
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
24
|
+
// -----------------------------------------------------------------------
|
|
25
|
+
// delete (群消息撤回) — Mode A: by messageId, Mode B: by count
|
|
26
|
+
// -----------------------------------------------------------------------
|
|
27
|
+
if (action === "delete") {
|
|
28
|
+
const rawTo = readStringParam(params, "to", { required: true });
|
|
29
|
+
if (!rawTo) {
|
|
30
|
+
throw new Error("delete requires a target (to).");
|
|
31
|
+
}
|
|
32
|
+
const to = normalizeInfoflowTarget(rawTo) ?? rawTo;
|
|
33
|
+
const target = to.replace(/^infoflow:/i, "");
|
|
34
|
+
const account = resolveInfoflowAccount({ cfg, accountId: accountId ?? undefined });
|
|
35
|
+
if (!account.config.appKey || !account.config.appSecret) {
|
|
36
|
+
throw new Error("Infoflow appKey/appSecret not configured.");
|
|
37
|
+
}
|
|
38
|
+
const messageId = readStringParam(params, "messageId");
|
|
39
|
+
// Default to count=1 (recall latest message) when neither messageId nor count is provided
|
|
40
|
+
const countStr = readStringParam(params, "count") ?? (messageId ? undefined : "1");
|
|
41
|
+
const groupMatch = target.match(/^group:(\d+)/i);
|
|
42
|
+
if (groupMatch) {
|
|
43
|
+
// -----------------------------------------------------------------
|
|
44
|
+
// 群消息撤回
|
|
45
|
+
// -----------------------------------------------------------------
|
|
46
|
+
const groupId = Number(groupMatch[1]);
|
|
47
|
+
// Mode A: single message recall by messageId
|
|
48
|
+
if (messageId) {
|
|
49
|
+
let msgseqid = readStringParam(params, "msgseqid") ?? "";
|
|
50
|
+
if (!msgseqid) {
|
|
51
|
+
const stored = findSentMessage(account.accountId, messageId);
|
|
52
|
+
if (stored?.msgseqid) {
|
|
53
|
+
msgseqid = stored.msgseqid;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!msgseqid) {
|
|
57
|
+
throw new Error("delete requires msgseqid (not found in store; provide it explicitly or send messages first).");
|
|
58
|
+
}
|
|
59
|
+
const result = await recallInfoflowGroupMessage({
|
|
60
|
+
account,
|
|
61
|
+
groupId,
|
|
62
|
+
messageid: messageId,
|
|
63
|
+
msgseqid,
|
|
64
|
+
});
|
|
65
|
+
if (result.ok) {
|
|
66
|
+
try {
|
|
67
|
+
removeRecalledMessages(account.accountId, [messageId]);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore cleanup errors
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return jsonResult({
|
|
74
|
+
ok: result.ok,
|
|
75
|
+
channel: "infoflow",
|
|
76
|
+
to,
|
|
77
|
+
...(result.error ? { error: result.error } : {}),
|
|
78
|
+
_hint: result.ok ? RECALL_OK_HINT : RECALL_FAIL_HINT,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Mode B: batch recall by count
|
|
82
|
+
if (countStr) {
|
|
83
|
+
const count = Number(countStr);
|
|
84
|
+
if (!Number.isFinite(count) || count < 1) {
|
|
85
|
+
throw new Error("count must be a positive integer.");
|
|
86
|
+
}
|
|
87
|
+
const records = querySentMessages(account.accountId, {
|
|
88
|
+
target: `group:${groupId}`,
|
|
89
|
+
count,
|
|
90
|
+
});
|
|
91
|
+
// Filter to records that have msgseqid (required for group recall)
|
|
92
|
+
const recallable = records.filter((r) => r.msgseqid);
|
|
93
|
+
if (recallable.length === 0) {
|
|
94
|
+
return jsonResult({
|
|
95
|
+
ok: true,
|
|
96
|
+
channel: "infoflow",
|
|
97
|
+
to,
|
|
98
|
+
recalled: 0,
|
|
99
|
+
message: "No recallable messages found in store.",
|
|
100
|
+
_hint: "No messages found to recall. Briefly inform the user.",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let succeeded = 0;
|
|
104
|
+
let failed = 0;
|
|
105
|
+
const recalledIds = [];
|
|
106
|
+
const details = [];
|
|
107
|
+
for (const record of recallable) {
|
|
108
|
+
const result = await recallInfoflowGroupMessage({
|
|
109
|
+
account,
|
|
110
|
+
groupId,
|
|
111
|
+
messageid: record.messageid,
|
|
112
|
+
msgseqid: record.msgseqid,
|
|
113
|
+
});
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
succeeded++;
|
|
116
|
+
recalledIds.push(record.messageid);
|
|
117
|
+
details.push({ messageid: record.messageid, digest: record.digest, ok: true });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
failed++;
|
|
121
|
+
details.push({
|
|
122
|
+
messageid: record.messageid,
|
|
123
|
+
digest: record.digest,
|
|
124
|
+
ok: false,
|
|
125
|
+
error: result.error,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (recalledIds.length > 0) {
|
|
130
|
+
try {
|
|
131
|
+
removeRecalledMessages(account.accountId, recalledIds);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// ignore cleanup errors
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return jsonResult({
|
|
138
|
+
ok: failed === 0,
|
|
139
|
+
channel: "infoflow",
|
|
140
|
+
to,
|
|
141
|
+
recalled: succeeded,
|
|
142
|
+
failed,
|
|
143
|
+
total: recallable.length,
|
|
144
|
+
details,
|
|
145
|
+
_hint: failed === 0 ? RECALL_OK_HINT : RECALL_PARTIAL_HINT,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// -----------------------------------------------------------------
|
|
151
|
+
// 私聊消息撤回
|
|
152
|
+
// -----------------------------------------------------------------
|
|
153
|
+
const appAgentId = account.config.appAgentId;
|
|
154
|
+
if (!appAgentId) {
|
|
155
|
+
throw new Error("Infoflow private message recall requires appAgentId configuration. " +
|
|
156
|
+
"Set channels.infoflow.appAgentId to your application ID (如流企业后台的应用ID).");
|
|
157
|
+
}
|
|
158
|
+
// Mode A: single message recall by messageId (msgkey)
|
|
159
|
+
if (messageId) {
|
|
160
|
+
const result = await recallInfoflowPrivateMessage({
|
|
161
|
+
account,
|
|
162
|
+
msgkey: messageId,
|
|
163
|
+
appAgentId,
|
|
164
|
+
});
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
try {
|
|
167
|
+
removeRecalledMessages(account.accountId, [messageId]);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// ignore cleanup errors
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return jsonResult({
|
|
174
|
+
ok: result.ok,
|
|
175
|
+
channel: "infoflow",
|
|
176
|
+
to,
|
|
177
|
+
...(result.error ? { error: result.error } : {}),
|
|
178
|
+
_hint: result.ok ? RECALL_OK_HINT : RECALL_FAIL_HINT,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// Mode B: batch recall by count
|
|
182
|
+
if (countStr) {
|
|
183
|
+
const count = Number(countStr);
|
|
184
|
+
if (!Number.isFinite(count) || count < 1) {
|
|
185
|
+
throw new Error("count must be a positive integer.");
|
|
186
|
+
}
|
|
187
|
+
const records = querySentMessages(account.accountId, { target, count });
|
|
188
|
+
// 私聊消息的 msgseqid 为空,只需要有 messageid (即 msgkey) 即可撤回
|
|
189
|
+
const recallable = records.filter((r) => r.messageid);
|
|
190
|
+
if (recallable.length === 0) {
|
|
191
|
+
return jsonResult({
|
|
192
|
+
ok: true,
|
|
193
|
+
channel: "infoflow",
|
|
194
|
+
to,
|
|
195
|
+
recalled: 0,
|
|
196
|
+
message: "No recallable messages found in store.",
|
|
197
|
+
_hint: "No messages found to recall. Briefly inform the user.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
let succeeded = 0;
|
|
201
|
+
let failed = 0;
|
|
202
|
+
const recalledIds = [];
|
|
203
|
+
const details = [];
|
|
204
|
+
for (const record of recallable) {
|
|
205
|
+
const result = await recallInfoflowPrivateMessage({
|
|
206
|
+
account,
|
|
207
|
+
msgkey: record.messageid,
|
|
208
|
+
appAgentId,
|
|
209
|
+
});
|
|
210
|
+
if (result.ok) {
|
|
211
|
+
succeeded++;
|
|
212
|
+
recalledIds.push(record.messageid);
|
|
213
|
+
details.push({ messageid: record.messageid, digest: record.digest, ok: true });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
failed++;
|
|
217
|
+
details.push({
|
|
218
|
+
messageid: record.messageid,
|
|
219
|
+
digest: record.digest,
|
|
220
|
+
ok: false,
|
|
221
|
+
error: result.error,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (recalledIds.length > 0) {
|
|
226
|
+
try {
|
|
227
|
+
removeRecalledMessages(account.accountId, recalledIds);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// ignore cleanup errors
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return jsonResult({
|
|
234
|
+
ok: failed === 0,
|
|
235
|
+
channel: "infoflow",
|
|
236
|
+
to,
|
|
237
|
+
recalled: succeeded,
|
|
238
|
+
failed,
|
|
239
|
+
total: recallable.length,
|
|
240
|
+
details,
|
|
241
|
+
_hint: failed === 0 ? RECALL_OK_HINT : RECALL_PARTIAL_HINT,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// -----------------------------------------------------------------------
|
|
247
|
+
// send
|
|
248
|
+
// -----------------------------------------------------------------------
|
|
249
|
+
if (action !== "send") {
|
|
250
|
+
throw new Error(`Action "${action}" is not supported for Infoflow.`);
|
|
251
|
+
}
|
|
252
|
+
const account = resolveInfoflowAccount({ cfg, accountId: accountId ?? undefined });
|
|
253
|
+
if (!account.config.appKey || !account.config.appSecret) {
|
|
254
|
+
throw new Error("Infoflow appKey/appSecret not configured.");
|
|
255
|
+
}
|
|
256
|
+
const rawTo = readStringParam(params, "to", { required: true });
|
|
257
|
+
if (!rawTo) {
|
|
258
|
+
throw new Error("send requires a target (to).");
|
|
259
|
+
}
|
|
260
|
+
const to = normalizeInfoflowTarget(rawTo) ?? rawTo;
|
|
261
|
+
const message = readStringParam(params, "message", { required: false, allowEmpty: true }) ?? "";
|
|
262
|
+
const mediaUrl = readStringParam(params, "media", { trim: false });
|
|
263
|
+
// Infoflow-specific mention params
|
|
264
|
+
const atAll = params.atAll === true || params.atAll === "true";
|
|
265
|
+
const mentionUserIdsRaw = readStringParam(params, "mentionUserIds");
|
|
266
|
+
const isGroup = /^group:\d+$/i.test(to);
|
|
267
|
+
const contents = [];
|
|
268
|
+
// Infoflow reply-to params (group only)
|
|
269
|
+
const replyToMessageId = readStringParam(params, "replyToMessageId");
|
|
270
|
+
const replyToPreview = readStringParam(params, "replyToPreview");
|
|
271
|
+
const replyTypeRaw = readStringParam(params, "replyType");
|
|
272
|
+
const replyTo = replyToMessageId && isGroup
|
|
273
|
+
? {
|
|
274
|
+
messageid: replyToMessageId,
|
|
275
|
+
preview: replyToPreview ?? undefined,
|
|
276
|
+
replytype: replyTypeRaw === "2" ? "2" : "1",
|
|
277
|
+
}
|
|
278
|
+
: undefined;
|
|
279
|
+
// Build AT content nodes (group messages only)
|
|
280
|
+
if (isGroup) {
|
|
281
|
+
if (atAll) {
|
|
282
|
+
contents.push({ type: "at", content: "all" });
|
|
283
|
+
}
|
|
284
|
+
else if (mentionUserIdsRaw) {
|
|
285
|
+
const userIds = mentionUserIdsRaw
|
|
286
|
+
.split(",")
|
|
287
|
+
.map((s) => s.trim())
|
|
288
|
+
.filter(Boolean);
|
|
289
|
+
if (userIds.length > 0) {
|
|
290
|
+
contents.push({ type: "at", content: userIds.join(",") });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Prepend @all/@user prefix to display text (same pattern as reply-dispatcher.ts)
|
|
295
|
+
let messageText = message;
|
|
296
|
+
if (isGroup) {
|
|
297
|
+
if (atAll) {
|
|
298
|
+
messageText = `@all ${message}`;
|
|
299
|
+
}
|
|
300
|
+
else if (mentionUserIdsRaw) {
|
|
301
|
+
const userIds = mentionUserIdsRaw
|
|
302
|
+
.split(",")
|
|
303
|
+
.map((s) => s.trim())
|
|
304
|
+
.filter(Boolean);
|
|
305
|
+
if (userIds.length > 0) {
|
|
306
|
+
const prefix = userIds.map((id) => `@${id}`).join(" ");
|
|
307
|
+
messageText = `${prefix} ${message}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (messageText.trim()) {
|
|
312
|
+
contents.push({ type: "markdown", content: messageText });
|
|
313
|
+
}
|
|
314
|
+
if (mediaUrl) {
|
|
315
|
+
logVerbose(`[infoflow:action:send] to=${to}, atAll=${atAll}, mentionUserIds=${mentionUserIdsRaw ?? "none"}`);
|
|
316
|
+
// b-mode: fire text first (if any), then image/link, then await all
|
|
317
|
+
const p1 = contents.length > 0
|
|
318
|
+
? sendInfoflowMessage({
|
|
319
|
+
cfg,
|
|
320
|
+
to,
|
|
321
|
+
contents,
|
|
322
|
+
accountId: accountId ?? undefined,
|
|
323
|
+
replyTo,
|
|
324
|
+
})
|
|
325
|
+
: null;
|
|
326
|
+
let p2;
|
|
327
|
+
try {
|
|
328
|
+
const prepared = await prepareInfoflowImageBase64({ mediaUrl });
|
|
329
|
+
if (prepared.isImage) {
|
|
330
|
+
p2 = sendInfoflowImageMessage({
|
|
331
|
+
cfg,
|
|
332
|
+
to,
|
|
333
|
+
base64Image: prepared.base64,
|
|
334
|
+
accountId: accountId ?? undefined,
|
|
335
|
+
replyTo: contents.length > 0 ? undefined : replyTo,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
p2 = sendInfoflowMessage({
|
|
340
|
+
cfg,
|
|
341
|
+
to,
|
|
342
|
+
contents: [{ type: "link", content: mediaUrl }],
|
|
343
|
+
accountId: accountId ?? undefined,
|
|
344
|
+
replyTo: contents.length > 0 ? undefined : replyTo,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
p2 = sendInfoflowMessage({
|
|
350
|
+
cfg,
|
|
351
|
+
to,
|
|
352
|
+
contents: [{ type: "link", content: mediaUrl }],
|
|
353
|
+
accountId: accountId ?? undefined,
|
|
354
|
+
replyTo: contents.length > 0 ? undefined : replyTo,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const results = await Promise.all([p1, p2].filter(Boolean));
|
|
358
|
+
const last = results.at(-1);
|
|
359
|
+
return jsonResult({
|
|
360
|
+
ok: last?.ok ?? false,
|
|
361
|
+
channel: "infoflow",
|
|
362
|
+
to,
|
|
363
|
+
messageId: last?.messageId ?? (last?.ok ? "sent" : "failed"),
|
|
364
|
+
...(last?.error ? { error: last.error } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (contents.length === 0) {
|
|
368
|
+
throw new Error("send requires text or media");
|
|
369
|
+
}
|
|
370
|
+
logVerbose(`[infoflow:action:send] to=${to}, atAll=${atAll}, mentionUserIds=${mentionUserIdsRaw ?? "none"}`);
|
|
371
|
+
const result = await sendInfoflowMessage({
|
|
372
|
+
cfg,
|
|
373
|
+
to,
|
|
374
|
+
contents,
|
|
375
|
+
accountId: accountId ?? undefined,
|
|
376
|
+
replyTo,
|
|
377
|
+
});
|
|
378
|
+
return jsonResult({
|
|
379
|
+
ok: result.ok,
|
|
380
|
+
channel: "infoflow",
|
|
381
|
+
to,
|
|
382
|
+
messageId: result.messageId ?? (result.ok ? "sent" : "failed"),
|
|
383
|
+
...(result.error ? { error: result.error } : {}),
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
};
|