@core-workspace/infoflow-openclaw-plugin 2026.3.9 → 2026.3.27-beta.0
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/CHANGELOG.md +91 -0
- package/CLAUDE.md +135 -0
- package/COLLABORATION_REPORT.md +209 -0
- package/PROJECT_GUIDE.md +355 -0
- package/README.md +158 -66
- package/docs/dev-guide.md +63 -50
- package/docs/qa-feature-list.md +452 -0
- package/docs/webhook-guide.md +178 -0
- package/index.ts +28 -2
- package/openclaw.plugin.json +131 -21
- package/package.json +16 -3
- package/scripts/deploy.sh +66 -7
- package/scripts/postinstall.cjs +80 -0
- package/skills/infoflow-dev/SKILL.md +2 -2
- package/skills/infoflow-dev/references/api.md +1 -1
- package/src/adapter/inbound/webhook-parser.ts +27 -5
- package/src/adapter/inbound/ws-receiver.ts +304 -43
- package/src/adapter/outbound/markdown-local-images.ts +80 -0
- package/src/adapter/outbound/reply-dispatcher.ts +146 -65
- package/src/adapter/outbound/target-resolver.ts +4 -3
- package/src/channel/accounts.ts +97 -22
- package/src/channel/channel.ts +456 -12
- package/src/channel/media.ts +20 -6
- package/src/channel/monitor.ts +8 -3
- package/src/channel/outbound.ts +358 -21
- package/src/channel/streaming.ts +740 -0
- package/src/commands/changelog.ts +80 -0
- package/src/commands/doctor.ts +545 -0
- package/src/commands/logs.ts +449 -0
- package/src/commands/version.ts +20 -0
- package/src/compat/openclaw-sdk.ts +218 -0
- package/src/handler/message-handler.ts +673 -166
- package/src/logging.ts +1 -1
- package/src/runtime.ts +1 -1
- package/src/security/dm-policy.ts +1 -4
- package/src/security/group-policy.ts +174 -51
- package/src/tools/actions/index.ts +15 -13
- package/src/tools/cron/relay.ts +1154 -0
- package/src/tools/hooks/index.ts +13 -1
- package/src/tools/index.ts +714 -32
- package/src/types.ts +144 -25
- package/src/utils/audio/g722/dct_tables.ts +381 -0
- package/src/utils/audio/g722/decoder.ts +919 -0
- package/src/utils/audio/g722/defs.ts +105 -0
- package/src/utils/audio/g722/hd-parser.ts +247 -0
- package/src/utils/audio/g722/huff_tables.ts +240 -0
- package/src/utils/audio/g722/index.ts +78 -0
- package/src/utils/audio/g722/output_decoded.pcm +0 -0
- package/src/utils/audio/g722/output_decoded.wav +0 -0
- package/src/utils/audio/g722/tables.ts +173 -0
- package/src/utils/audio/g722/test_api.ts +31 -0
- package/src/utils/audio/g722/test_voice.hd +0 -0
- package/src/utils/bos/im-bos-client.ts +219 -0
- package/src/utils/group-agent-cache.ts +142 -0
- package/src/utils/token-adapter.ts +120 -51
package/src/tools/index.ts
CHANGED
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Infoflow Agent Tools(Agent 工具注册)
|
|
3
3
|
*
|
|
4
|
-
* 向 OpenClaw
|
|
4
|
+
* 向 OpenClaw 注册供 LLM function calling 使用的工具:
|
|
5
5
|
* - infoflow_send: 主动向用户或群发送消息
|
|
6
6
|
* - infoflow_recall: 撤回近期发送的消息
|
|
7
|
+
* - infoflow_cron: 创建强绑定当前如流会话目标的定时消息
|
|
7
8
|
*
|
|
8
9
|
* 这两个工具封装了 outbound/send.ts 中的底层发送/撤回逻辑,
|
|
9
10
|
* 让 Agent 能够主动发消息,而不仅限于被动回复。
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
12
17
|
import { Type, type Static } from "@sinclair/typebox";
|
|
13
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
18
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
19
|
+
import { normalizeInfoflowTarget } from "../adapter/outbound/target-resolver.js";
|
|
14
20
|
import { resolveInfoflowAccount } from "../channel/accounts.js";
|
|
21
|
+
import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "../channel/media.js";
|
|
22
|
+
import {
|
|
23
|
+
sendInfoflowMessage,
|
|
24
|
+
recallInfoflowGroupMessage,
|
|
25
|
+
recallInfoflowPrivateMessage,
|
|
26
|
+
isLikelyLocalPath,
|
|
27
|
+
} from "../channel/outbound.js";
|
|
15
28
|
import { logVerbose } from "../logging.js";
|
|
16
|
-
import {
|
|
29
|
+
import type { InfoflowMessageContentItem } from "../types.js";
|
|
30
|
+
import { imBosUpload, imBosGetUrl } from "../utils/bos/im-bos-client.js";
|
|
17
31
|
import { querySentMessages, removeRecalledMessages } from "../utils/store/message-store.js";
|
|
18
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
encodeInfoflowCronJobDescription,
|
|
34
|
+
resolveInfoflowTargetFromSessionEntry,
|
|
35
|
+
resolveInfoflowTargetFromSessionKey,
|
|
36
|
+
type InfoflowCronJobMetadata,
|
|
37
|
+
} from "./cron/relay.js";
|
|
19
38
|
|
|
20
39
|
// ---------------------------------------------------------------------------
|
|
21
40
|
// infoflow_send 参数 Schema
|
|
@@ -45,6 +64,12 @@ const InfoflowSendSchema = Type.Object({
|
|
|
45
64
|
description: "多账号模式下指定账号 ID,不填则使用默认账号。",
|
|
46
65
|
}),
|
|
47
66
|
),
|
|
67
|
+
imageUrl: Type.Optional(
|
|
68
|
+
Type.String({
|
|
69
|
+
description:
|
|
70
|
+
"要发送的图片 URL 或本地文件路径。提供此参数时会发送图片消息;可与 message 同时使用(先发文字再发图片)。",
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
48
73
|
});
|
|
49
74
|
|
|
50
75
|
type InfoflowSendParams = Static<typeof InfoflowSendSchema>;
|
|
@@ -78,6 +103,217 @@ const InfoflowRecallSchema = Type.Object({
|
|
|
78
103
|
|
|
79
104
|
type InfoflowRecallParams = Static<typeof InfoflowRecallSchema>;
|
|
80
105
|
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// infoflow_cron 参数 Schema
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
const InfoflowCronSchema = Type.Object({
|
|
111
|
+
name: Type.String({
|
|
112
|
+
description: "定时任务名称,建议简短明确,例如“日报提醒”。",
|
|
113
|
+
}),
|
|
114
|
+
message: Type.String({
|
|
115
|
+
description: "定时发送的消息内容,支持 Markdown。",
|
|
116
|
+
}),
|
|
117
|
+
atAll: Type.Optional(
|
|
118
|
+
Type.Boolean({
|
|
119
|
+
description: "群聊定时消息是否 @全体成员。仅群聊生效,单聊忽略。",
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
mentionUserIds: Type.Optional(
|
|
123
|
+
Type.String({
|
|
124
|
+
description:
|
|
125
|
+
'群聊定时消息需要 @ 的成员列表,逗号分隔的 uuapName(例如 "zhangsan,lisi")。仅群聊生效,atAll 为 true 时忽略。',
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
at: Type.Optional(
|
|
129
|
+
Type.String({
|
|
130
|
+
description: '一次性任务的触发时间。支持 ISO 时间,例如 "2026-03-17T18:00:00+08:00"。',
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
every: Type.Optional(
|
|
134
|
+
Type.String({
|
|
135
|
+
description: '按固定间隔重复执行,例如 "30m"、"1h"。',
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
cron: Type.Optional(
|
|
139
|
+
Type.String({
|
|
140
|
+
description: 'Cron 表达式,例如 "0 9 * * 1-5"。',
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
tz: Type.Optional(
|
|
144
|
+
Type.String({
|
|
145
|
+
description: 'Cron 表达式对应的 IANA 时区,例如 "Asia/Shanghai"。',
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
description: Type.Optional(
|
|
149
|
+
Type.String({
|
|
150
|
+
description: "可选的任务备注。",
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
keepAfterRun: Type.Optional(
|
|
154
|
+
Type.Boolean({
|
|
155
|
+
description: "一次性任务执行后是否保留。默认执行后删除。",
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
type InfoflowCronParams = Static<typeof InfoflowCronSchema>;
|
|
161
|
+
type InfoflowToolExecutionContext = {
|
|
162
|
+
messageChannel?: string;
|
|
163
|
+
requesterSenderId?: string;
|
|
164
|
+
sessionKey?: string;
|
|
165
|
+
agentDir?: string;
|
|
166
|
+
agentId?: string;
|
|
167
|
+
workspaceDir?: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function json(payload: unknown) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
173
|
+
details: payload,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function expandHomePrefix(filePath: string): string {
|
|
178
|
+
if (filePath === "~") {
|
|
179
|
+
return process.env.HOME ?? "~";
|
|
180
|
+
}
|
|
181
|
+
if (filePath.startsWith("~/")) {
|
|
182
|
+
return path.join(process.env.HOME ?? "~", filePath.slice(2));
|
|
183
|
+
}
|
|
184
|
+
return filePath;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toObject(value: unknown): Record<string, unknown> | null {
|
|
188
|
+
if (!value || typeof value !== "object") {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return value as Record<string, unknown>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function toTrimmedString(value: unknown): string | undefined {
|
|
195
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildInfoflowCronSessionKey(): string {
|
|
199
|
+
return `agent:main:cron:infoflow:${randomUUID()}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveToolSessionStorePath(
|
|
203
|
+
api: OpenClawPluginApi,
|
|
204
|
+
ctx: InfoflowToolExecutionContext,
|
|
205
|
+
): string | null {
|
|
206
|
+
const configured = toObject(api.config.session)?.store;
|
|
207
|
+
if (typeof configured === "string" && configured.trim()) {
|
|
208
|
+
const agentId = ctx.agentId?.trim() || "main";
|
|
209
|
+
return path.resolve(expandHomePrefix(configured.trim().replaceAll("{agentId}", agentId)));
|
|
210
|
+
}
|
|
211
|
+
if (ctx.agentDir?.trim()) {
|
|
212
|
+
return path.join(ctx.agentDir, "sessions", "sessions.json");
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function resolveInfoflowCronTargetForTool(
|
|
218
|
+
api: OpenClawPluginApi,
|
|
219
|
+
ctx: InfoflowToolExecutionContext,
|
|
220
|
+
): Promise<{ target: string; creatorSenderId?: string } | null> {
|
|
221
|
+
if (ctx.messageChannel !== "infoflow") {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const requesterSenderId = ctx.requesterSenderId?.trim() || undefined;
|
|
226
|
+
const storePath = resolveToolSessionStorePath(api, ctx);
|
|
227
|
+
if (storePath && ctx.sessionKey?.trim()) {
|
|
228
|
+
try {
|
|
229
|
+
const raw = await fs.readFile(storePath, "utf8");
|
|
230
|
+
const parsed = toObject(JSON.parse(raw));
|
|
231
|
+
const sessionTarget = resolveInfoflowTargetFromSessionEntry({
|
|
232
|
+
entry: parsed?.[ctx.sessionKey],
|
|
233
|
+
sessionKey: ctx.sessionKey,
|
|
234
|
+
});
|
|
235
|
+
if (sessionTarget) {
|
|
236
|
+
return {
|
|
237
|
+
target: sessionTarget,
|
|
238
|
+
creatorSenderId: requesterSenderId,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Ignore session store read errors and fall through to sessionKey / FromUserId resolution.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const sessionTarget = resolveInfoflowTargetFromSessionKey(ctx.sessionKey);
|
|
247
|
+
if (sessionTarget) {
|
|
248
|
+
return {
|
|
249
|
+
target: sessionTarget,
|
|
250
|
+
creatorSenderId: requesterSenderId,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (requesterSenderId && !ctx.sessionKey?.toLowerCase().includes(":group:")) {
|
|
255
|
+
return {
|
|
256
|
+
target: `infoflow:${requesterSenderId}`,
|
|
257
|
+
creatorSenderId: requesterSenderId,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function runOpenClaw(
|
|
265
|
+
args: string[],
|
|
266
|
+
cwd: string | undefined,
|
|
267
|
+
): Promise<{
|
|
268
|
+
exitCode: number;
|
|
269
|
+
stdout: string;
|
|
270
|
+
stderr: string;
|
|
271
|
+
}> {
|
|
272
|
+
return await new Promise((resolve, reject) => {
|
|
273
|
+
const child = spawn("openclaw", args, {
|
|
274
|
+
cwd,
|
|
275
|
+
env: process.env,
|
|
276
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
277
|
+
});
|
|
278
|
+
let stdout = "";
|
|
279
|
+
let stderr = "";
|
|
280
|
+
child.stdout.on("data", (chunk) => {
|
|
281
|
+
stdout += String(chunk);
|
|
282
|
+
});
|
|
283
|
+
child.stderr.on("data", (chunk) => {
|
|
284
|
+
stderr += String(chunk);
|
|
285
|
+
});
|
|
286
|
+
child.on("error", reject);
|
|
287
|
+
child.on("close", (exitCode) => {
|
|
288
|
+
resolve({
|
|
289
|
+
exitCode: exitCode ?? 1,
|
|
290
|
+
stdout,
|
|
291
|
+
stderr,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseJsonFromMixedOutput(raw: string): unknown {
|
|
298
|
+
const trimmed = raw.trim();
|
|
299
|
+
if (!trimmed) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(trimmed);
|
|
304
|
+
} catch {
|
|
305
|
+
const objectStart = trimmed.indexOf("{");
|
|
306
|
+
if (objectStart >= 0) {
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(trimmed.slice(objectStart));
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
81
317
|
// ---------------------------------------------------------------------------
|
|
82
318
|
// 注册函数
|
|
83
319
|
// ---------------------------------------------------------------------------
|
|
@@ -96,22 +332,23 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
96
332
|
api.registerTool(
|
|
97
333
|
{
|
|
98
334
|
name: "infoflow_send",
|
|
335
|
+
label: "Infoflow Send",
|
|
99
336
|
description:
|
|
100
|
-
"向如流(Infoflow
|
|
337
|
+
"向如流(Infoflow)的用户或群发送消息。支持纯文本、Markdown 格式和图片(URL 或本地路径)。" +
|
|
101
338
|
"群消息可以 @指定成员 或 @全体成员。" +
|
|
102
|
-
"
|
|
339
|
+
"当需要主动发送通知、提醒、消息或图片时使用此工具。",
|
|
103
340
|
parameters: InfoflowSendSchema,
|
|
104
341
|
async execute(_toolCallId, rawParams) {
|
|
105
342
|
const params = rawParams as InfoflowSendParams;
|
|
106
|
-
const { to: rawTo, message, atAll, mentionUserIds, accountId } = params;
|
|
343
|
+
const { to: rawTo, message, atAll, mentionUserIds, accountId, imageUrl } = params;
|
|
107
344
|
|
|
108
345
|
// 标准化目标格式(去掉可能多余的前缀)
|
|
109
346
|
const to = normalizeInfoflowTarget(rawTo) ?? rawTo;
|
|
110
347
|
const isGroup = /^group:\d+$/i.test(to);
|
|
111
348
|
|
|
112
|
-
logVerbose(`[infoflow_send] to=${to}, isGroup=${isGroup}`);
|
|
349
|
+
logVerbose(`[infoflow_send] to=${to}, isGroup=${isGroup}, imageUrl=${imageUrl ?? "none"}`);
|
|
113
350
|
|
|
114
|
-
const contents:
|
|
351
|
+
const contents: InfoflowMessageContentItem[] = [];
|
|
115
352
|
|
|
116
353
|
// 群消息:先拼 @mention 块,再拼正文
|
|
117
354
|
if (isGroup) {
|
|
@@ -128,13 +365,80 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
128
365
|
}
|
|
129
366
|
}
|
|
130
367
|
|
|
131
|
-
if (message
|
|
368
|
+
if (message?.trim()) {
|
|
132
369
|
contents.push({ type: "markdown", content: message });
|
|
133
370
|
}
|
|
134
371
|
|
|
372
|
+
// 有图片时:先发文字(如有),再发图片
|
|
373
|
+
if (imageUrl?.trim()) {
|
|
374
|
+
// 先发文字+@
|
|
375
|
+
if (contents.length > 0) {
|
|
376
|
+
await sendInfoflowMessage({
|
|
377
|
+
cfg: api.config!,
|
|
378
|
+
to,
|
|
379
|
+
contents,
|
|
380
|
+
accountId: accountId ?? undefined,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 尝试原生图片发送,失败则降级为链接
|
|
385
|
+
try {
|
|
386
|
+
const localRoots = isLikelyLocalPath(imageUrl) ? [imageUrl] : undefined;
|
|
387
|
+
const prepared = await prepareInfoflowImageBase64({
|
|
388
|
+
mediaUrl: imageUrl,
|
|
389
|
+
mediaLocalRoots: localRoots,
|
|
390
|
+
});
|
|
391
|
+
if (prepared.isImage) {
|
|
392
|
+
const imgResult = await sendInfoflowImageMessage({
|
|
393
|
+
cfg: api.config!,
|
|
394
|
+
to,
|
|
395
|
+
base64Image: prepared.base64,
|
|
396
|
+
accountId: accountId ?? undefined,
|
|
397
|
+
});
|
|
398
|
+
const payload = {
|
|
399
|
+
ok: imgResult.ok,
|
|
400
|
+
channel: "infoflow",
|
|
401
|
+
to,
|
|
402
|
+
...(imgResult.messageId ? { messageId: imgResult.messageId } : {}),
|
|
403
|
+
...(imgResult.error ? { error: imgResult.error } : {}),
|
|
404
|
+
};
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
407
|
+
details: payload,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
logVerbose(`[infoflow_send] prepareInfoflowImageBase64 error: ${e}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 降级:发链接
|
|
415
|
+
const linkResult = await sendInfoflowMessage({
|
|
416
|
+
cfg: api.config!,
|
|
417
|
+
to,
|
|
418
|
+
contents: [{ type: "link", content: imageUrl }],
|
|
419
|
+
accountId: accountId ?? undefined,
|
|
420
|
+
});
|
|
421
|
+
const payload = {
|
|
422
|
+
ok: linkResult.ok,
|
|
423
|
+
channel: "infoflow",
|
|
424
|
+
to,
|
|
425
|
+
...(linkResult.messageId ? { messageId: linkResult.messageId } : {}),
|
|
426
|
+
...(linkResult.error ? { error: linkResult.error } : {}),
|
|
427
|
+
};
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
430
|
+
details: payload,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
135
434
|
if (contents.length === 0) {
|
|
136
435
|
return {
|
|
137
|
-
content: [
|
|
436
|
+
content: [
|
|
437
|
+
{
|
|
438
|
+
type: "text" as const,
|
|
439
|
+
text: JSON.stringify({ ok: false, error: "message is empty" }),
|
|
440
|
+
},
|
|
441
|
+
],
|
|
138
442
|
details: { ok: false, error: "message is empty" },
|
|
139
443
|
};
|
|
140
444
|
}
|
|
@@ -167,6 +471,7 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
167
471
|
api.registerTool(
|
|
168
472
|
{
|
|
169
473
|
name: "infoflow_recall",
|
|
474
|
+
label: "Infoflow Recall",
|
|
170
475
|
description:
|
|
171
476
|
"撤回机器人在如流(Infoflow)发送的消息。" +
|
|
172
477
|
"可按消息 ID 撤回指定消息,也可按数量撤回最近 N 条(默认 1 条)。" +
|
|
@@ -180,7 +485,10 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
180
485
|
const target = to.replace(/^infoflow:/i, "");
|
|
181
486
|
|
|
182
487
|
// 校验账号配置
|
|
183
|
-
const account = resolveInfoflowAccount({
|
|
488
|
+
const account = resolveInfoflowAccount({
|
|
489
|
+
cfg: api.config!,
|
|
490
|
+
accountId: accountId ?? undefined,
|
|
491
|
+
});
|
|
184
492
|
if (!account.config.appKey || !account.config.appSecret) {
|
|
185
493
|
const err = { ok: false, error: "Infoflow appKey/appSecret not configured." };
|
|
186
494
|
return {
|
|
@@ -203,35 +511,67 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
203
511
|
// 模式 A:按 messageId 撤回单条
|
|
204
512
|
if (messageId) {
|
|
205
513
|
// 群消息撤回需要同时提供 messageid + msgseqid,从本地消息记录中查找 msgseqid
|
|
206
|
-
const stored = querySentMessages(account.accountId, {
|
|
207
|
-
|
|
514
|
+
const stored = querySentMessages(account.accountId, {
|
|
515
|
+
target: `group:${groupId}`,
|
|
516
|
+
count: 1,
|
|
517
|
+
}).find((r) => r.messageid === messageId);
|
|
208
518
|
const msgseqid = stored?.msgseqid ?? "";
|
|
209
519
|
if (!msgseqid) {
|
|
210
|
-
return json({
|
|
520
|
+
return json({
|
|
521
|
+
ok: false,
|
|
522
|
+
error:
|
|
523
|
+
"msgseqid not found for this messageId. It may have already been recalled or not found in the message store.",
|
|
524
|
+
});
|
|
211
525
|
}
|
|
212
|
-
const result = await recallInfoflowGroupMessage({
|
|
526
|
+
const result = await recallInfoflowGroupMessage({
|
|
527
|
+
account,
|
|
528
|
+
groupId,
|
|
529
|
+
messageid: messageId,
|
|
530
|
+
msgseqid,
|
|
531
|
+
});
|
|
213
532
|
if (result.ok) {
|
|
214
|
-
try {
|
|
533
|
+
try {
|
|
534
|
+
removeRecalledMessages(account.accountId, [messageId]);
|
|
535
|
+
} catch {
|
|
536
|
+
/* ignore */
|
|
537
|
+
}
|
|
215
538
|
}
|
|
216
539
|
return json({ ok: result.ok, to, ...(result.error ? { error: result.error } : {}) });
|
|
217
540
|
}
|
|
218
541
|
|
|
219
542
|
// 模式 B:按 count 批量撤回最近 N 条
|
|
220
|
-
const records = querySentMessages(account.accountId, {
|
|
221
|
-
|
|
543
|
+
const records = querySentMessages(account.accountId, {
|
|
544
|
+
target: `group:${groupId}`,
|
|
545
|
+
count,
|
|
546
|
+
}).filter((r) => r.msgseqid); // 只撤回有 msgseqid 的记录
|
|
222
547
|
|
|
223
548
|
if (records.length === 0) {
|
|
224
549
|
return json({ ok: true, to, recalled: 0, message: "No recallable messages found." });
|
|
225
550
|
}
|
|
226
551
|
|
|
227
|
-
let succeeded = 0,
|
|
552
|
+
let succeeded = 0,
|
|
553
|
+
failed = 0;
|
|
228
554
|
const recalledIds: string[] = [];
|
|
229
555
|
for (const r of records) {
|
|
230
|
-
const res = await recallInfoflowGroupMessage({
|
|
231
|
-
|
|
556
|
+
const res = await recallInfoflowGroupMessage({
|
|
557
|
+
account,
|
|
558
|
+
groupId,
|
|
559
|
+
messageid: r.messageid,
|
|
560
|
+
msgseqid: r.msgseqid,
|
|
561
|
+
});
|
|
562
|
+
if (res.ok) {
|
|
563
|
+
succeeded++;
|
|
564
|
+
recalledIds.push(r.messageid);
|
|
565
|
+
} else {
|
|
566
|
+
failed++;
|
|
567
|
+
}
|
|
232
568
|
}
|
|
233
569
|
if (recalledIds.length > 0) {
|
|
234
|
-
try {
|
|
570
|
+
try {
|
|
571
|
+
removeRecalledMessages(account.accountId, recalledIds);
|
|
572
|
+
} catch {
|
|
573
|
+
/* ignore */
|
|
574
|
+
}
|
|
235
575
|
}
|
|
236
576
|
return json({ ok: failed === 0, to, recalled: succeeded, failed, total: records.length });
|
|
237
577
|
}
|
|
@@ -240,32 +580,59 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
240
580
|
// 私聊撤回需要 appAgentId(企业后台的应用 ID)
|
|
241
581
|
const appAgentId = account.config.appAgentId;
|
|
242
582
|
if (!appAgentId) {
|
|
243
|
-
return json({
|
|
583
|
+
return json({
|
|
584
|
+
ok: false,
|
|
585
|
+
error: "Private message recall requires appAgentId configuration.",
|
|
586
|
+
});
|
|
244
587
|
}
|
|
245
588
|
|
|
246
589
|
// 模式 A:按 messageId(即 msgkey)撤回单条
|
|
247
590
|
if (messageId) {
|
|
248
|
-
const result = await recallInfoflowPrivateMessage({
|
|
591
|
+
const result = await recallInfoflowPrivateMessage({
|
|
592
|
+
account,
|
|
593
|
+
msgkey: messageId,
|
|
594
|
+
appAgentId,
|
|
595
|
+
});
|
|
249
596
|
if (result.ok) {
|
|
250
|
-
try {
|
|
597
|
+
try {
|
|
598
|
+
removeRecalledMessages(account.accountId, [messageId]);
|
|
599
|
+
} catch {
|
|
600
|
+
/* ignore */
|
|
601
|
+
}
|
|
251
602
|
}
|
|
252
603
|
return json({ ok: result.ok, to, ...(result.error ? { error: result.error } : {}) });
|
|
253
604
|
}
|
|
254
605
|
|
|
255
606
|
// 模式 B:批量撤回最近 N 条
|
|
256
|
-
const records = querySentMessages(account.accountId, { target, count }).filter(
|
|
607
|
+
const records = querySentMessages(account.accountId, { target, count }).filter(
|
|
608
|
+
(r) => r.messageid,
|
|
609
|
+
);
|
|
257
610
|
if (records.length === 0) {
|
|
258
611
|
return json({ ok: true, to, recalled: 0, message: "No recallable messages found." });
|
|
259
612
|
}
|
|
260
613
|
|
|
261
|
-
let succeeded = 0,
|
|
614
|
+
let succeeded = 0,
|
|
615
|
+
failed = 0;
|
|
262
616
|
const recalledIds: string[] = [];
|
|
263
617
|
for (const r of records) {
|
|
264
|
-
const res = await recallInfoflowPrivateMessage({
|
|
265
|
-
|
|
618
|
+
const res = await recallInfoflowPrivateMessage({
|
|
619
|
+
account,
|
|
620
|
+
msgkey: r.messageid,
|
|
621
|
+
appAgentId,
|
|
622
|
+
});
|
|
623
|
+
if (res.ok) {
|
|
624
|
+
succeeded++;
|
|
625
|
+
recalledIds.push(r.messageid);
|
|
626
|
+
} else {
|
|
627
|
+
failed++;
|
|
628
|
+
}
|
|
266
629
|
}
|
|
267
630
|
if (recalledIds.length > 0) {
|
|
268
|
-
try {
|
|
631
|
+
try {
|
|
632
|
+
removeRecalledMessages(account.accountId, recalledIds);
|
|
633
|
+
} catch {
|
|
634
|
+
/* ignore */
|
|
635
|
+
}
|
|
269
636
|
}
|
|
270
637
|
return json({ ok: failed === 0, to, recalled: succeeded, failed, total: records.length });
|
|
271
638
|
},
|
|
@@ -273,5 +640,320 @@ export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
|
273
640
|
{ name: "infoflow_recall" },
|
|
274
641
|
);
|
|
275
642
|
|
|
276
|
-
api.
|
|
643
|
+
api.registerTool(
|
|
644
|
+
(ctx) => {
|
|
645
|
+
if (ctx.messageChannel !== "infoflow") {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
name: "infoflow_cron",
|
|
651
|
+
label: "Infoflow Cron",
|
|
652
|
+
description:
|
|
653
|
+
"在当前如流会话里创建定时消息。目标会从可信的 FromUserId / groupId 上下文自动绑定,不允许模型自行猜测收件人。",
|
|
654
|
+
parameters: InfoflowCronSchema,
|
|
655
|
+
async execute(_toolCallId, rawParams) {
|
|
656
|
+
const params = rawParams as InfoflowCronParams;
|
|
657
|
+
const scheduleCount = [params.at, params.every, params.cron].filter(
|
|
658
|
+
(value) => typeof value === "string" && value.trim(),
|
|
659
|
+
).length;
|
|
660
|
+
if (scheduleCount !== 1) {
|
|
661
|
+
return json({
|
|
662
|
+
ok: false,
|
|
663
|
+
error: "Exactly one of at / every / cron must be provided.",
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const message = params.message.trim();
|
|
668
|
+
const name = params.name.trim();
|
|
669
|
+
if (!name || !message) {
|
|
670
|
+
return json({
|
|
671
|
+
ok: false,
|
|
672
|
+
error: "name and message are required.",
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const targetContext = await resolveInfoflowCronTargetForTool(api, ctx);
|
|
677
|
+
if (!targetContext) {
|
|
678
|
+
return json({
|
|
679
|
+
ok: false,
|
|
680
|
+
error:
|
|
681
|
+
"Unable to resolve the current Infoflow target from trusted session context. In private chat this requires FromUserId; in group chat this requires current groupId/session routing.",
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const isGroupTarget = /^group:\d+$/i.test(targetContext.target);
|
|
685
|
+
const mentionUserIds = isGroupTarget
|
|
686
|
+
? (params.mentionUserIds ?? "")
|
|
687
|
+
.split(",")
|
|
688
|
+
.map((value) => value.trim())
|
|
689
|
+
.filter(Boolean)
|
|
690
|
+
: [];
|
|
691
|
+
|
|
692
|
+
const metadata: InfoflowCronJobMetadata = {
|
|
693
|
+
version: 1,
|
|
694
|
+
mode: "direct",
|
|
695
|
+
target: targetContext.target,
|
|
696
|
+
creatorSenderId: targetContext.creatorSenderId,
|
|
697
|
+
channel: "infoflow",
|
|
698
|
+
source: "infoflow_cron",
|
|
699
|
+
atAll: isGroupTarget ? params.atAll === true : undefined,
|
|
700
|
+
mentionUserIds:
|
|
701
|
+
isGroupTarget && params.atAll !== true && mentionUserIds.length > 0
|
|
702
|
+
? mentionUserIds
|
|
703
|
+
: undefined,
|
|
704
|
+
};
|
|
705
|
+
const description = encodeInfoflowCronJobDescription({
|
|
706
|
+
metadata,
|
|
707
|
+
userDescription: params.description,
|
|
708
|
+
});
|
|
709
|
+
const sessionKey = buildInfoflowCronSessionKey();
|
|
710
|
+
|
|
711
|
+
const args = [
|
|
712
|
+
"cron",
|
|
713
|
+
"add",
|
|
714
|
+
"--json",
|
|
715
|
+
"--name",
|
|
716
|
+
name,
|
|
717
|
+
"--session",
|
|
718
|
+
"main",
|
|
719
|
+
"--session-key",
|
|
720
|
+
sessionKey,
|
|
721
|
+
"--wake",
|
|
722
|
+
"now",
|
|
723
|
+
"--system-event",
|
|
724
|
+
message,
|
|
725
|
+
"--description",
|
|
726
|
+
description,
|
|
727
|
+
];
|
|
728
|
+
|
|
729
|
+
if (params.at?.trim()) {
|
|
730
|
+
args.push("--at", params.at.trim());
|
|
731
|
+
if (params.keepAfterRun) {
|
|
732
|
+
args.push("--keep-after-run");
|
|
733
|
+
} else {
|
|
734
|
+
args.push("--delete-after-run");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (params.every?.trim()) {
|
|
738
|
+
args.push("--every", params.every.trim());
|
|
739
|
+
}
|
|
740
|
+
if (params.cron?.trim()) {
|
|
741
|
+
args.push("--cron", params.cron.trim());
|
|
742
|
+
}
|
|
743
|
+
if (params.tz?.trim()) {
|
|
744
|
+
args.push("--tz", params.tz.trim());
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const result = await runOpenClaw(args, ctx.workspaceDir);
|
|
748
|
+
const parsed = parseJsonFromMixedOutput(`${result.stdout}\n${result.stderr}`);
|
|
749
|
+
const parsedObject = toObject(parsed);
|
|
750
|
+
const jobObject = toObject(parsedObject?.job);
|
|
751
|
+
const jobId = toTrimmedString(parsedObject?.id) ?? toTrimmedString(jobObject?.id);
|
|
752
|
+
const account = resolveInfoflowAccount({ cfg: api.config! });
|
|
753
|
+
if (result.exitCode !== 0) {
|
|
754
|
+
return json({
|
|
755
|
+
ok: false,
|
|
756
|
+
error: result.stderr.trim() || result.stdout.trim() || "openclaw cron add failed",
|
|
757
|
+
target: targetContext.target,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return json({
|
|
762
|
+
ok: true,
|
|
763
|
+
target: targetContext.target,
|
|
764
|
+
creatorSenderId: targetContext.creatorSenderId,
|
|
765
|
+
observe: {
|
|
766
|
+
channel: "infoflow",
|
|
767
|
+
jobId,
|
|
768
|
+
sessionKey,
|
|
769
|
+
jobsPath: "~/.openclaw/cron/jobs.json",
|
|
770
|
+
runsPath: jobId
|
|
771
|
+
? `~/.openclaw/cron/runs/${jobId}.jsonl`
|
|
772
|
+
: "~/.openclaw/cron/runs/<jobId>.jsonl",
|
|
773
|
+
relayStatusPath: `<OpenClaw stateDir>/${account.config.privateDataDir}/cron-relay-status.md`,
|
|
774
|
+
relayStatePath: `<OpenClaw stateDir>/${account.config.privateDataDir}/cron-relay-state.json`,
|
|
775
|
+
},
|
|
776
|
+
job: parsed ?? result.stdout.trim(),
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
},
|
|
781
|
+
{ name: "infoflow_cron" },
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// ---- infoflow_bos_upload:上传文件到 BOS 并返回下载链接 ----
|
|
785
|
+
|
|
786
|
+
const InfoflowBosUploadSchema = Type.Object({
|
|
787
|
+
content: Type.String({
|
|
788
|
+
description: "要上传的文件内容(文本)。对于纯文本文件直接传入内容;对于代码文件传入源代码。",
|
|
789
|
+
}),
|
|
790
|
+
fileName: Type.String({
|
|
791
|
+
description: '文件名(含扩展名),例如 "report.md"、"data.csv"、"script.py"。',
|
|
792
|
+
}),
|
|
793
|
+
objectKey: Type.Optional(
|
|
794
|
+
Type.String({
|
|
795
|
+
description:
|
|
796
|
+
'可选的完整存储路径(目录 + 文件名 + 扩展名),例如 "reports/2026/Q1.md"、"exports/data.csv",系统会自动添加 openclaw/ 前缀。不填则自动生成唯一路径(格式:openclaw/uploads/{yyyyMMdd}/{uuid}-{fileName}),避免不同用户上传同名文件时互相覆盖。仅在需要固定路径或主动覆盖更新时才填此字段。',
|
|
797
|
+
}),
|
|
798
|
+
),
|
|
799
|
+
accountId: Type.Optional(
|
|
800
|
+
Type.String({
|
|
801
|
+
description: "多账号模式下指定账号 ID,不填则使用默认账号。",
|
|
802
|
+
}),
|
|
803
|
+
),
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
type InfoflowBosUploadParams = Static<typeof InfoflowBosUploadSchema>;
|
|
807
|
+
|
|
808
|
+
api.registerTool(
|
|
809
|
+
{
|
|
810
|
+
name: "infoflow_bos_upload",
|
|
811
|
+
label: "Infoflow BOS Upload",
|
|
812
|
+
description:
|
|
813
|
+
"将生成的文件内容(文本、代码、CSV、Markdown 等)上传到 BOS 云存储,并自动返回带有效期的预签名下载链接。" +
|
|
814
|
+
"当用户请求生成文件、导出数据、保存代码等场景时使用此工具。" +
|
|
815
|
+
"上传成功后会同时返回 objectKey 和可直接点击的下载 URL。",
|
|
816
|
+
parameters: InfoflowBosUploadSchema,
|
|
817
|
+
async execute(_toolCallId, rawParams) {
|
|
818
|
+
const params = rawParams as InfoflowBosUploadParams;
|
|
819
|
+
const { content, fileName, objectKey, accountId } = params;
|
|
820
|
+
|
|
821
|
+
if (!content?.trim()) {
|
|
822
|
+
return json({ ok: false, error: "content is empty" });
|
|
823
|
+
}
|
|
824
|
+
if (!fileName?.trim()) {
|
|
825
|
+
return json({ ok: false, error: "fileName is required" });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const account = resolveInfoflowAccount({
|
|
829
|
+
cfg: api.config!,
|
|
830
|
+
accountId: accountId ?? undefined,
|
|
831
|
+
});
|
|
832
|
+
const { apiHost, appKey, appSecret } = account.config;
|
|
833
|
+
if (!appKey || !appSecret) {
|
|
834
|
+
return json({ ok: false, error: "Infoflow appKey/appSecret not configured." });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// 若调用方未指定 objectKey,则自动生成唯一路径,防止不同用户/会话上传同名文件时互相覆盖。
|
|
838
|
+
// 格式:openclaw/uploads/{yyyyMMdd}/{uuid}-{fileName}
|
|
839
|
+
const resolvedObjectKey = objectKey?.trim()
|
|
840
|
+
? `openclaw/${objectKey.trim()}`
|
|
841
|
+
: (() => {
|
|
842
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
843
|
+
return `openclaw/uploads/${today}/${randomUUID()}-${fileName.trim()}`;
|
|
844
|
+
})();
|
|
845
|
+
|
|
846
|
+
logVerbose(
|
|
847
|
+
`[infoflow_bos_upload] fileName=${fileName}, objectKey=${resolvedObjectKey}, contentLen=${content.length}`,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// 上传文件
|
|
851
|
+
const fileBuffer = Buffer.from(content, "utf-8");
|
|
852
|
+
const uploadResult = await imBosUpload({
|
|
853
|
+
apiHost,
|
|
854
|
+
appKey,
|
|
855
|
+
appSecret,
|
|
856
|
+
fileContent: fileBuffer,
|
|
857
|
+
fileName: fileName.trim(),
|
|
858
|
+
objectKey: resolvedObjectKey,
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (!uploadResult.ok) {
|
|
862
|
+
return json({ ok: false, error: uploadResult.error ?? "upload failed" });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 上传成功后自动获取预签名下载 URL
|
|
866
|
+
const resultObjectKey = uploadResult.objectKey ?? resolvedObjectKey;
|
|
867
|
+
const urlResult = await imBosGetUrl({
|
|
868
|
+
apiHost,
|
|
869
|
+
appKey,
|
|
870
|
+
appSecret,
|
|
871
|
+
objectKey: resultObjectKey,
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return json({
|
|
875
|
+
ok: true,
|
|
876
|
+
objectKey: resultObjectKey,
|
|
877
|
+
eTag: uploadResult.eTag,
|
|
878
|
+
downloadUrl: urlResult.ok ? urlResult.url : undefined,
|
|
879
|
+
expirationSeconds: urlResult.ok ? urlResult.expirationSeconds : undefined,
|
|
880
|
+
...(urlResult.ok ? {} : { urlError: urlResult.error }),
|
|
881
|
+
});
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
{ name: "infoflow_bos_upload" },
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
// ---- infoflow_bos_get_url:获取/刷新 BOS 文件预签名下载 URL ----
|
|
888
|
+
|
|
889
|
+
const InfoflowBosGetUrlSchema = Type.Object({
|
|
890
|
+
objectKey: Type.String({
|
|
891
|
+
description: "BOS 对象的存储路径(object_key),由上传时返回。",
|
|
892
|
+
}),
|
|
893
|
+
expirationSeconds: Type.Optional(
|
|
894
|
+
Type.Number({
|
|
895
|
+
description: "预签名 URL 的有效期(秒),默认 3600(1 小时),最大 86400(24 小时)。",
|
|
896
|
+
}),
|
|
897
|
+
),
|
|
898
|
+
accountId: Type.Optional(
|
|
899
|
+
Type.String({
|
|
900
|
+
description: "多账号模式下指定账号 ID,不填则使用默认账号。",
|
|
901
|
+
}),
|
|
902
|
+
),
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
type InfoflowBosGetUrlParams = Static<typeof InfoflowBosGetUrlSchema>;
|
|
906
|
+
|
|
907
|
+
api.registerTool(
|
|
908
|
+
{
|
|
909
|
+
name: "infoflow_bos_get_url",
|
|
910
|
+
label: "Infoflow BOS Get URL",
|
|
911
|
+
description:
|
|
912
|
+
"为已存在的 BOS 文件获取或刷新预签名下载 URL。" +
|
|
913
|
+
"当用户需要重新获取之前上传文件的下载链接(例如链接已过期)时使用此工具。" +
|
|
914
|
+
"需要提供上传时返回的 objectKey。",
|
|
915
|
+
parameters: InfoflowBosGetUrlSchema,
|
|
916
|
+
async execute(_toolCallId, rawParams) {
|
|
917
|
+
const params = rawParams as InfoflowBosGetUrlParams;
|
|
918
|
+
const { objectKey, expirationSeconds, accountId } = params;
|
|
919
|
+
|
|
920
|
+
if (!objectKey?.trim()) {
|
|
921
|
+
return json({ ok: false, error: "objectKey is required" });
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const account = resolveInfoflowAccount({
|
|
925
|
+
cfg: api.config!,
|
|
926
|
+
accountId: accountId ?? undefined,
|
|
927
|
+
});
|
|
928
|
+
const { apiHost, appKey, appSecret } = account.config;
|
|
929
|
+
if (!appKey || !appSecret) {
|
|
930
|
+
return json({ ok: false, error: "Infoflow appKey/appSecret not configured." });
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
logVerbose(
|
|
934
|
+
`[infoflow_bos_get_url] objectKey=${objectKey}, expirationSeconds=${expirationSeconds ?? 3600}`,
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
const urlResult = await imBosGetUrl({
|
|
938
|
+
apiHost,
|
|
939
|
+
appKey,
|
|
940
|
+
appSecret,
|
|
941
|
+
objectKey: objectKey.trim(),
|
|
942
|
+
expirationSeconds: expirationSeconds ?? undefined,
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
return json({
|
|
946
|
+
ok: urlResult.ok,
|
|
947
|
+
url: urlResult.url,
|
|
948
|
+
expirationSeconds: urlResult.expirationSeconds,
|
|
949
|
+
...(urlResult.error ? { error: urlResult.error } : {}),
|
|
950
|
+
});
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
{ name: "infoflow_bos_get_url" },
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
api.logger.info?.(
|
|
957
|
+
"infoflow_tools: Registered infoflow_send, infoflow_recall, infoflow_cron, infoflow_bos_upload and infoflow_bos_get_url tools",
|
|
958
|
+
);
|
|
277
959
|
}
|