@core-workspace/infoflow-openclaw-plugin 2026.3.8 → 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 +20 -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/channel/outbound.ts
CHANGED
|
@@ -4,18 +4,23 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
8
|
-
import { resolveInfoflowAccount } from "./accounts.js";
|
|
7
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
9
8
|
import { recordSentMessageId } from "../adapter/inbound/webhook-parser.js";
|
|
10
|
-
import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "../logging.js";
|
|
11
|
-
import { getOrCreateAdapter, _resetAdapters } from "../utils/token-adapter.js";
|
|
12
9
|
import { coreEvents } from "../events.js";
|
|
10
|
+
import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "../logging.js";
|
|
13
11
|
import type {
|
|
12
|
+
InfoflowGroupAgentInfo,
|
|
14
13
|
InfoflowGroupMessageBodyItem,
|
|
15
14
|
InfoflowMessageContentItem,
|
|
16
15
|
InfoflowOutboundReply,
|
|
17
16
|
ResolvedInfoflowAccount,
|
|
18
17
|
} from "../types.js";
|
|
18
|
+
import {
|
|
19
|
+
registerGroupMemberListFetcher,
|
|
20
|
+
replaceAgentNameMentions,
|
|
21
|
+
} from "../utils/group-agent-cache.js";
|
|
22
|
+
import { getOrCreateAdapter, _resetAdapters } from "../utils/token-adapter.js";
|
|
23
|
+
import { resolveInfoflowAccount } from "./accounts.js";
|
|
19
24
|
|
|
20
25
|
export const DEFAULT_TIMEOUT_MS = 30_000; // 30 seconds
|
|
21
26
|
|
|
@@ -39,6 +44,8 @@ export const INFOFLOW_PRIVATE_SEND_PATH = "/api/v1/app/message/send";
|
|
|
39
44
|
export const INFOFLOW_GROUP_SEND_PATH = "/api/v1/robot/msg/groupmsgsend";
|
|
40
45
|
export const INFOFLOW_GROUP_RECALL_PATH = "/api/v1/robot/group/msgRecall";
|
|
41
46
|
export const INFOFLOW_PRIVATE_RECALL_PATH = "/api/v1/app/message/revoke";
|
|
47
|
+
export const INFOFLOW_ASR_QUERY_PATH = "/api/v1/va/queryASRResult";
|
|
48
|
+
export const INFOFLOW_GROUP_MEMBER_LIST_PATH = "/api/v1/robot/group/memberList";
|
|
42
49
|
|
|
43
50
|
// ---------------------------------------------------------------------------
|
|
44
51
|
// Helper Functions
|
|
@@ -148,6 +155,60 @@ export function extractMsgSeqId(data: Record<string, unknown>): string | undefin
|
|
|
148
155
|
return undefined;
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
function buildGroupAtDisplayPrefix(items: InfoflowGroupMessageBodyItem[]): string | undefined {
|
|
159
|
+
const parts: string[] = [];
|
|
160
|
+
for (const item of items) {
|
|
161
|
+
if (item.type !== "AT") {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (item.atall) {
|
|
165
|
+
parts.push("@all");
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const userId of item.atuserids ?? []) {
|
|
169
|
+
const normalized = userId.trim();
|
|
170
|
+
if (normalized) {
|
|
171
|
+
parts.push(`@${normalized}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const agentId of item.atagentids ?? []) {
|
|
175
|
+
parts.push(`@${agentId}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyGroupAtDisplayPrefix(
|
|
182
|
+
items: InfoflowGroupMessageBodyItem[],
|
|
183
|
+
): InfoflowGroupMessageBodyItem[] {
|
|
184
|
+
const prefix = buildGroupAtDisplayPrefix(items);
|
|
185
|
+
if (!prefix) {
|
|
186
|
+
return items;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const next = [...items];
|
|
190
|
+
const textIndex = next.findIndex((item) => item.type === "MD");
|
|
191
|
+
if (textIndex < 0) {
|
|
192
|
+
return next;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const item = next[textIndex];
|
|
196
|
+
if (item.type !== "MD") {
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const content = item.content ?? "";
|
|
201
|
+
if (content.trimStart().startsWith(prefix)) {
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
next[textIndex] = {
|
|
206
|
+
...item,
|
|
207
|
+
content: content.trim().length > 0 ? `${prefix} ${content}` : prefix,
|
|
208
|
+
};
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
|
|
151
212
|
// ---------------------------------------------------------------------------
|
|
152
213
|
// Token Management
|
|
153
214
|
// ---------------------------------------------------------------------------
|
|
@@ -164,6 +225,9 @@ export async function getAppAccessToken(params: {
|
|
|
164
225
|
timeoutMs?: number;
|
|
165
226
|
}): Promise<{ ok: boolean; token?: string; error?: string }> {
|
|
166
227
|
try {
|
|
228
|
+
getInfoflowSendLog().info(
|
|
229
|
+
`[token] getAppAccessToken: apiHost=${params.apiHost}, appKey=${params.appKey.slice(0, 4)}***`,
|
|
230
|
+
);
|
|
167
231
|
const adapter = getOrCreateAdapter({
|
|
168
232
|
apiHost: params.apiHost,
|
|
169
233
|
appKey: params.appKey,
|
|
@@ -172,7 +236,11 @@ export async function getAppAccessToken(params: {
|
|
|
172
236
|
const token = await adapter.getToken();
|
|
173
237
|
return { ok: true, token };
|
|
174
238
|
} catch (err) {
|
|
175
|
-
|
|
239
|
+
const error = formatInfoflowError(err);
|
|
240
|
+
getInfoflowSendLog().error(
|
|
241
|
+
`[token] getAppAccessToken failed: apiHost=${params.apiHost}, appKey=${params.appKey.slice(0, 4)}***, error=${error}`,
|
|
242
|
+
);
|
|
243
|
+
return { ok: false, error };
|
|
176
244
|
}
|
|
177
245
|
}
|
|
178
246
|
|
|
@@ -190,9 +258,10 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
190
258
|
account: ResolvedInfoflowAccount;
|
|
191
259
|
toUser: string;
|
|
192
260
|
contents: InfoflowMessageContentItem[];
|
|
261
|
+
replyTo?: InfoflowOutboundReply;
|
|
193
262
|
timeoutMs?: number;
|
|
194
263
|
}): Promise<{ ok: boolean; error?: string; invaliduser?: string; msgkey?: string }> {
|
|
195
|
-
const { account, toUser, contents, timeoutMs = DEFAULT_TIMEOUT_MS } = params;
|
|
264
|
+
const { account, toUser, contents, replyTo, timeoutMs = DEFAULT_TIMEOUT_MS } = params;
|
|
196
265
|
const { apiHost, appKey, appSecret } = account.config;
|
|
197
266
|
|
|
198
267
|
// Validate account config
|
|
@@ -275,6 +344,23 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
275
344
|
}
|
|
276
345
|
}
|
|
277
346
|
|
|
347
|
+
// Inject reply fields for private message quote/reply
|
|
348
|
+
// API format: reply is an array of { content, uid, msgid, msgid2 }
|
|
349
|
+
if (replyTo && payload.msgtype !== "md") {
|
|
350
|
+
payload.reply = [
|
|
351
|
+
{
|
|
352
|
+
content: replyTo.preview ?? "",
|
|
353
|
+
uid: replyTo.imid ?? "0",
|
|
354
|
+
msgid: String(replyTo.messageid),
|
|
355
|
+
...(replyTo.msgid2 ? { msgid2: replyTo.msgid2 } : {}),
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
// Include agentid if available
|
|
359
|
+
if (account.config.appAgentId != null) {
|
|
360
|
+
payload.agentid = String(account.config.appAgentId);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
278
364
|
const headers = {
|
|
279
365
|
Authorization: `Bearer-${tokenResult.token}`,
|
|
280
366
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -283,8 +369,11 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
283
369
|
|
|
284
370
|
const bodyStr = JSON.stringify(payload);
|
|
285
371
|
|
|
286
|
-
// Log request
|
|
287
|
-
|
|
372
|
+
// Log request
|
|
373
|
+
getInfoflowSendLog().info(
|
|
374
|
+
`[outbound:dm] to=${toUser}, msgtype=${payload.msgtype}, bodyLen=${bodyStr.length}, hasReply=${!!replyTo}`,
|
|
375
|
+
);
|
|
376
|
+
logVerbose(`[outbound:dm] POST body: ${bodyStr}`);
|
|
288
377
|
|
|
289
378
|
const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_PRIVATE_SEND_PATH}`, {
|
|
290
379
|
method: "POST",
|
|
@@ -294,8 +383,16 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
294
383
|
});
|
|
295
384
|
|
|
296
385
|
const responseText = await res.text();
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
getInfoflowSendLog().error(
|
|
388
|
+
`[outbound:dm] HTTP error: status=${res.status}, body=${responseText.slice(0, 200)}`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
297
391
|
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
298
|
-
|
|
392
|
+
getInfoflowSendLog().info(
|
|
393
|
+
`[outbound:dm] response: status=${res.status}, code=${data.code ?? "?"}, msgkey=${(data.data as any)?.msgkey ?? "?"}`,
|
|
394
|
+
);
|
|
395
|
+
logVerbose(`[outbound:dm] response body: ${responseText}`);
|
|
299
396
|
|
|
300
397
|
// Check outer code first
|
|
301
398
|
const code = typeof data.code === "string" ? data.code : "";
|
|
@@ -328,7 +425,10 @@ export async function sendInfoflowPrivateMessage(params: {
|
|
|
328
425
|
coreEvents.emit("message:sent", {
|
|
329
426
|
accountId: account.accountId,
|
|
330
427
|
target: toUser,
|
|
331
|
-
from:
|
|
428
|
+
from:
|
|
429
|
+
account.config.appAgentId != null
|
|
430
|
+
? `agent:${account.config.appAgentId}`
|
|
431
|
+
: "agent:unknown",
|
|
332
432
|
messageid: msgkey,
|
|
333
433
|
msgseqid: "",
|
|
334
434
|
contents,
|
|
@@ -389,7 +489,7 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
389
489
|
} else if (type === "at") {
|
|
390
490
|
// Parse AT content: "all" means atall, otherwise comma-separated user IDs
|
|
391
491
|
if (item.content === "all") {
|
|
392
|
-
body.push({ type: "AT", atall: true
|
|
492
|
+
body.push({ type: "AT", atall: true });
|
|
393
493
|
} else {
|
|
394
494
|
const userIds = item.content
|
|
395
495
|
.split(",")
|
|
@@ -412,7 +512,7 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
412
512
|
.map((s) => Number(s.trim()))
|
|
413
513
|
.filter(Number.isFinite);
|
|
414
514
|
if (agentIds.length > 0) {
|
|
415
|
-
body.push({ type: "AT",
|
|
515
|
+
body.push({ type: "AT", atagentids: agentIds });
|
|
416
516
|
}
|
|
417
517
|
} else if (type === "image") {
|
|
418
518
|
body.push({ type: "IMAGE", content: item.content });
|
|
@@ -422,7 +522,9 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
422
522
|
// Split body: LINK and IMAGE must be sent as individual messages
|
|
423
523
|
const linkItems = body.filter((b) => b.type === "LINK");
|
|
424
524
|
const imageItems = body.filter((b) => b.type === "IMAGE");
|
|
425
|
-
const textItems =
|
|
525
|
+
const textItems = applyGroupAtDisplayPrefix(
|
|
526
|
+
body.filter((b) => b.type !== "LINK" && b.type !== "IMAGE"),
|
|
527
|
+
);
|
|
426
528
|
|
|
427
529
|
// Get token first (shared by all sends)
|
|
428
530
|
const tokenResult = await getAppAccessToken({ apiHost, appKey, appSecret, timeoutMs });
|
|
@@ -465,10 +567,10 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
465
567
|
...(replyTo
|
|
466
568
|
? {
|
|
467
569
|
reply: {
|
|
468
|
-
messageid: String(replyTo.messageid),
|
|
570
|
+
messageid: String(replyTo.messageid),
|
|
469
571
|
preview: replyTo.preview ?? "",
|
|
470
|
-
...(replyTo.imid ? { imid: replyTo.imid } : {}),
|
|
471
|
-
|
|
572
|
+
...(replyTo.imid ? { imid: replyTo.imid } : {}),
|
|
573
|
+
replytype: replyTo.replytype ?? "1",
|
|
472
574
|
},
|
|
473
575
|
}
|
|
474
576
|
: {}),
|
|
@@ -478,6 +580,11 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
478
580
|
// Build request body
|
|
479
581
|
const bodyStr = JSON.stringify(payload);
|
|
480
582
|
|
|
583
|
+
getInfoflowSendLog().info(
|
|
584
|
+
`[outbound:group] groupId=${groupId}, msgtype=${msgtype}, bodyLen=${bodyStr.length}, hasReply=${!!replyTo}`,
|
|
585
|
+
);
|
|
586
|
+
logVerbose(`[outbound:group] POST body: ${bodyStr}`);
|
|
587
|
+
|
|
481
588
|
const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_GROUP_SEND_PATH}`, {
|
|
482
589
|
method: "POST",
|
|
483
590
|
headers,
|
|
@@ -486,8 +593,16 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
486
593
|
});
|
|
487
594
|
|
|
488
595
|
const responseText = await res.text();
|
|
596
|
+
if (!res.ok) {
|
|
597
|
+
getInfoflowSendLog().error(
|
|
598
|
+
`[outbound:group] HTTP error: status=${res.status}, body=${responseText.slice(0, 200)}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
489
601
|
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
490
|
-
|
|
602
|
+
getInfoflowSendLog().info(
|
|
603
|
+
`[outbound:group] response: status=${res.status}, code=${data.code ?? "?"}, messageid=${extractIdFromRawJson(responseText, "messageid") ?? "?"}`,
|
|
604
|
+
);
|
|
605
|
+
logVerbose(`[outbound:group] response body: ${responseText}`);
|
|
491
606
|
|
|
492
607
|
const code = typeof data.code === "string" ? data.code : "";
|
|
493
608
|
if (code !== "ok") {
|
|
@@ -532,7 +647,8 @@ export async function sendInfoflowGroupMessage(params: {
|
|
|
532
647
|
coreEvents.emit("message:sent", {
|
|
533
648
|
accountId: account.accountId,
|
|
534
649
|
target: `group:${groupId}`,
|
|
535
|
-
from:
|
|
650
|
+
from:
|
|
651
|
+
account.config.appAgentId != null ? `agent:${account.config.appAgentId}` : "agent:unknown",
|
|
536
652
|
messageid: result.messageid,
|
|
537
653
|
msgseqid: result.msgseqid ?? "",
|
|
538
654
|
contents: digestContents,
|
|
@@ -805,7 +921,10 @@ async function resolveLocalImageLinks(
|
|
|
805
921
|
|
|
806
922
|
// Attempt image detection for local path
|
|
807
923
|
try {
|
|
808
|
-
const prepared = await prepareInfoflowImageBase64({
|
|
924
|
+
const prepared = await prepareInfoflowImageBase64({
|
|
925
|
+
mediaUrl: href,
|
|
926
|
+
mediaLocalRoots: [href],
|
|
927
|
+
});
|
|
809
928
|
if (prepared.isImage) {
|
|
810
929
|
resolved.push({ type: "image", content: prepared.base64 });
|
|
811
930
|
continue;
|
|
@@ -861,15 +980,90 @@ export async function sendInfoflowMessage(params: {
|
|
|
861
980
|
// Parse target: remove "infoflow:" prefix if present
|
|
862
981
|
const target = to.replace(/^infoflow:/i, "");
|
|
863
982
|
|
|
983
|
+
getInfoflowSendLog().info(
|
|
984
|
+
`[outbound] sendMessage: to=${target}, items=${resolvedContents.length}, types=[${resolvedContents.map((c) => c.type).join(",")}]`,
|
|
985
|
+
);
|
|
986
|
+
|
|
864
987
|
// Check if target is a group (format: group:123)
|
|
865
988
|
const groupMatch = target.match(/^group:(\d+)/i);
|
|
866
989
|
if (groupMatch) {
|
|
867
|
-
// Group path: sendInfoflowGroupMessage already handles IMAGE items
|
|
868
990
|
const groupId = Number(groupMatch[1]);
|
|
991
|
+
|
|
992
|
+
// Resolve @robotName → @agentId in text/md content items using the group member cache.
|
|
993
|
+
// Also collect resolved agentIds to create at-agent content items.
|
|
994
|
+
// Quick skip: only scan if any text/md item contains '@'
|
|
995
|
+
const finalContents = [...resolvedContents];
|
|
996
|
+
const resolvedAgentIds: number[] = [];
|
|
997
|
+
const hasAtMention = finalContents.some((item) => {
|
|
998
|
+
const t = item.type.toLowerCase();
|
|
999
|
+
return (t === "text" || t === "md" || t === "markdown") && item.content.includes("@");
|
|
1000
|
+
});
|
|
1001
|
+
if (hasAtMention) {
|
|
1002
|
+
for (let i = 0; i < finalContents.length; i++) {
|
|
1003
|
+
const item = finalContents[i];
|
|
1004
|
+
const t = item.type.toLowerCase();
|
|
1005
|
+
if (t === "text" || t === "md" || t === "markdown") {
|
|
1006
|
+
try {
|
|
1007
|
+
const replaced = await replaceAgentNameMentions({
|
|
1008
|
+
text: item.content,
|
|
1009
|
+
account,
|
|
1010
|
+
groupId,
|
|
1011
|
+
});
|
|
1012
|
+
if (replaced.text !== item.content) {
|
|
1013
|
+
finalContents[i] = { ...item, content: replaced.text };
|
|
1014
|
+
for (const id of replaced.newAgentIds) {
|
|
1015
|
+
if (!resolvedAgentIds.includes(id)) {
|
|
1016
|
+
resolvedAgentIds.push(id);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
// Non-fatal: continue with original text
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// Strip pure numeric IDs from type: "at" content items — they are not valid
|
|
1027
|
+
// uuapNames (likely robot imids placed by LLM via mentionUserIds). Robot @mentions
|
|
1028
|
+
// are handled by replaceAgentNameMentions resolving @name → at-agent with correct agentId.
|
|
1029
|
+
for (let i = finalContents.length - 1; i >= 0; i--) {
|
|
1030
|
+
const item = finalContents[i];
|
|
1031
|
+
if (item.type !== "at" || item.content === "all") continue;
|
|
1032
|
+
|
|
1033
|
+
const ids = item.content
|
|
1034
|
+
.split(",")
|
|
1035
|
+
.map((s) => s.trim())
|
|
1036
|
+
.filter(Boolean);
|
|
1037
|
+
const userIds = ids.filter((id) => !/^\d+$/.test(id));
|
|
1038
|
+
|
|
1039
|
+
if (userIds.length > 0) {
|
|
1040
|
+
finalContents[i] = { ...item, content: userIds.join(",") };
|
|
1041
|
+
} else {
|
|
1042
|
+
finalContents.splice(i, 1);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Add at-agent content item for resolved robot agentIds
|
|
1047
|
+
if (resolvedAgentIds.length > 0) {
|
|
1048
|
+
finalContents.unshift({ type: "at-agent", content: resolvedAgentIds.join(",") });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Strip bodyForAgent annotations like "(robotid:4105000875)" from outbound text
|
|
1052
|
+
for (let i = 0; i < finalContents.length; i++) {
|
|
1053
|
+
const item = finalContents[i];
|
|
1054
|
+
const t = item.type.toLowerCase();
|
|
1055
|
+
if (t === "text" || t === "md" || t === "markdown") {
|
|
1056
|
+
const cleaned = item.content.replace(/\s*\(robotid:\d+\)/g, "");
|
|
1057
|
+
if (cleaned !== item.content) {
|
|
1058
|
+
finalContents[i] = { ...item, content: cleaned };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
869
1063
|
const result = await sendInfoflowGroupMessage({
|
|
870
1064
|
account,
|
|
871
1065
|
groupId,
|
|
872
|
-
contents:
|
|
1066
|
+
contents: finalContents,
|
|
873
1067
|
replyTo: params.replyTo,
|
|
874
1068
|
});
|
|
875
1069
|
return {
|
|
@@ -893,6 +1087,7 @@ export async function sendInfoflowMessage(params: {
|
|
|
893
1087
|
account,
|
|
894
1088
|
toUser: target,
|
|
895
1089
|
contents: nonImageContents,
|
|
1090
|
+
replyTo: params.replyTo,
|
|
896
1091
|
});
|
|
897
1092
|
if (result.ok) {
|
|
898
1093
|
lastMessageId = result.msgkey;
|
|
@@ -924,6 +1119,148 @@ export async function sendInfoflowMessage(params: {
|
|
|
924
1119
|
return { ok: true, messageId: lastMessageId };
|
|
925
1120
|
}
|
|
926
1121
|
|
|
1122
|
+
// ---------------------------------------------------------------------------
|
|
1123
|
+
// Voice ASR Query (语音识别)
|
|
1124
|
+
// ---------------------------------------------------------------------------
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Queries the Infoflow ASR (Automatic Speech Recognition) service
|
|
1128
|
+
* to convert a voice message's audio into text.
|
|
1129
|
+
*
|
|
1130
|
+
* @param account - Resolved Infoflow account with config (for token)
|
|
1131
|
+
* @param md5 - MD5 hash from the VoiceUrl fileid parameter
|
|
1132
|
+
* @returns ASR text content, or undefined if recognition failed
|
|
1133
|
+
*/
|
|
1134
|
+
export async function queryASRResult(params: {
|
|
1135
|
+
account: ResolvedInfoflowAccount;
|
|
1136
|
+
md5: string;
|
|
1137
|
+
timeoutMs?: number;
|
|
1138
|
+
}): Promise<{ ok: boolean; content?: string; error?: string }> {
|
|
1139
|
+
const { account, md5, timeoutMs = DEFAULT_TIMEOUT_MS } = params;
|
|
1140
|
+
const { apiHost, appKey, appSecret } = account.config;
|
|
1141
|
+
|
|
1142
|
+
const tokenResult = await getAppAccessToken({ apiHost, appKey, appSecret, timeoutMs });
|
|
1143
|
+
if (!tokenResult.ok || !tokenResult.token) {
|
|
1144
|
+
getInfoflowSendLog().error(`[infoflow:asr] token error: ${tokenResult.error}`);
|
|
1145
|
+
return { ok: false, error: tokenResult.error ?? "failed to get token" };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1149
|
+
try {
|
|
1150
|
+
const controller = new AbortController();
|
|
1151
|
+
timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1152
|
+
|
|
1153
|
+
const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_ASR_QUERY_PATH}`, {
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
headers: {
|
|
1156
|
+
Authorization: `Bearer-${tokenResult.token}`,
|
|
1157
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1158
|
+
},
|
|
1159
|
+
body: `md5=${encodeURIComponent(md5)}`,
|
|
1160
|
+
signal: controller.signal,
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
const responseText = await res.text();
|
|
1164
|
+
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
1165
|
+
getInfoflowSendLog().info(
|
|
1166
|
+
`[infoflow:asr] response: status=${res.status}, data=${responseText}`,
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const code = typeof data.code === "string" ? data.code : "";
|
|
1170
|
+
if (code !== "ok") {
|
|
1171
|
+
const msg = String(data.message ?? data.msg ?? "");
|
|
1172
|
+
const errMsg = msg ? `code=${code}, ${msg}` : `code=${code || "unknown"}`;
|
|
1173
|
+
getInfoflowSendLog().error(`[infoflow:asr] failed: ${errMsg}`);
|
|
1174
|
+
return { ok: false, error: errMsg };
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const innerData = data.data as Record<string, unknown> | undefined;
|
|
1178
|
+
const innerCode = innerData?.code;
|
|
1179
|
+
if (innerCode !== 200) {
|
|
1180
|
+
const errMsg = `ASR code=${innerCode}`;
|
|
1181
|
+
getInfoflowSendLog().error(`[infoflow:asr] recognition failed: ${errMsg}`);
|
|
1182
|
+
return { ok: false, error: errMsg };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const content = String(innerData?.content ?? "").trim();
|
|
1186
|
+
return { ok: true, content: content || undefined };
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
const errMsg = formatInfoflowError(err);
|
|
1189
|
+
getInfoflowSendLog().error(`[infoflow:asr] exception: ${errMsg}`);
|
|
1190
|
+
return { ok: false, error: errMsg };
|
|
1191
|
+
} finally {
|
|
1192
|
+
clearTimeout(timeout);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Fetches the group member list to get robot name→agentId mappings.
|
|
1198
|
+
*/
|
|
1199
|
+
export async function fetchGroupMemberList(params: {
|
|
1200
|
+
account: ResolvedInfoflowAccount;
|
|
1201
|
+
groupId: number;
|
|
1202
|
+
timeoutMs?: number;
|
|
1203
|
+
}): Promise<{ ok: boolean; agents?: InfoflowGroupAgentInfo[]; error?: string }> {
|
|
1204
|
+
const { account, groupId, timeoutMs = DEFAULT_TIMEOUT_MS } = params;
|
|
1205
|
+
const { apiHost, appKey, appSecret } = account.config;
|
|
1206
|
+
|
|
1207
|
+
const tokenResult = await getAppAccessToken({ apiHost, appKey, appSecret, timeoutMs });
|
|
1208
|
+
if (!tokenResult.ok || !tokenResult.token) {
|
|
1209
|
+
getInfoflowSendLog().error(`[infoflow:memberList] token error: ${tokenResult.error}`);
|
|
1210
|
+
return { ok: false, error: tokenResult.error ?? "failed to get token" };
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1214
|
+
try {
|
|
1215
|
+
const controller = new AbortController();
|
|
1216
|
+
timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1217
|
+
|
|
1218
|
+
const res = await fetch(`${ensureHttps(apiHost)}${INFOFLOW_GROUP_MEMBER_LIST_PATH}`, {
|
|
1219
|
+
method: "POST",
|
|
1220
|
+
headers: {
|
|
1221
|
+
Authorization: `Bearer-${tokenResult.token}`,
|
|
1222
|
+
"Content-Type": "application/json",
|
|
1223
|
+
},
|
|
1224
|
+
body: JSON.stringify({ groupId, recallType: 0 }),
|
|
1225
|
+
signal: controller.signal,
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
const responseText = await res.text();
|
|
1229
|
+
logVerbose(
|
|
1230
|
+
`[infoflow:memberList] response: status=${res.status}, groupId=${groupId}, body=${responseText.slice(0, 500)}`,
|
|
1231
|
+
);
|
|
1232
|
+
const data = JSON.parse(responseText) as Record<string, unknown>;
|
|
1233
|
+
|
|
1234
|
+
// Response structure: { code: "ok", data: { errcode: 0, errmsg: "ok", data: { agentInfoList: [...] } } }
|
|
1235
|
+
const code = typeof data.code === "string" ? data.code : "";
|
|
1236
|
+
if (code !== "ok") {
|
|
1237
|
+
getInfoflowSendLog().error(`[infoflow:memberList] failed: code=${code}`);
|
|
1238
|
+
return { ok: false, error: `code=${code}` };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const outerData = data.data as Record<string, unknown> | undefined;
|
|
1242
|
+
const errcode = outerData?.errcode as number | undefined;
|
|
1243
|
+
if (errcode != null && errcode !== 0) {
|
|
1244
|
+
const errmsg = String(outerData?.errmsg ?? `errcode=${errcode}`);
|
|
1245
|
+
getInfoflowSendLog().error(`[infoflow:memberList] failed: ${errmsg}`);
|
|
1246
|
+
return { ok: false, error: errmsg };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const innerData = outerData?.data as { agentInfoList?: InfoflowGroupAgentInfo[] } | undefined;
|
|
1250
|
+
const agents = innerData?.agentInfoList ?? [];
|
|
1251
|
+
return { ok: true, agents };
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
const errMsg = formatInfoflowError(err);
|
|
1254
|
+
getInfoflowSendLog().error(`[infoflow:memberList] exception: ${errMsg}`);
|
|
1255
|
+
return { ok: false, error: errMsg };
|
|
1256
|
+
} finally {
|
|
1257
|
+
clearTimeout(timeout);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Wire up the group agent cache so it can call fetchGroupMemberList without circular imports
|
|
1262
|
+
registerGroupMemberListFetcher(fetchGroupMemberList);
|
|
1263
|
+
|
|
927
1264
|
// ---------------------------------------------------------------------------
|
|
928
1265
|
// Test-only exports (@internal — not part of the public API)
|
|
929
1266
|
// ---------------------------------------------------------------------------
|