@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/CLAUDE.md +135 -0
  3. package/COLLABORATION_REPORT.md +209 -0
  4. package/PROJECT_GUIDE.md +355 -0
  5. package/README.md +158 -66
  6. package/docs/dev-guide.md +63 -50
  7. package/docs/qa-feature-list.md +452 -0
  8. package/docs/webhook-guide.md +178 -0
  9. package/index.ts +28 -2
  10. package/openclaw.plugin.json +131 -21
  11. package/package.json +20 -3
  12. package/scripts/deploy.sh +66 -7
  13. package/scripts/postinstall.cjs +80 -0
  14. package/skills/infoflow-dev/SKILL.md +2 -2
  15. package/skills/infoflow-dev/references/api.md +1 -1
  16. package/src/adapter/inbound/webhook-parser.ts +27 -5
  17. package/src/adapter/inbound/ws-receiver.ts +304 -43
  18. package/src/adapter/outbound/markdown-local-images.ts +80 -0
  19. package/src/adapter/outbound/reply-dispatcher.ts +146 -65
  20. package/src/adapter/outbound/target-resolver.ts +4 -3
  21. package/src/channel/accounts.ts +97 -22
  22. package/src/channel/channel.ts +456 -12
  23. package/src/channel/media.ts +20 -6
  24. package/src/channel/monitor.ts +8 -3
  25. package/src/channel/outbound.ts +358 -21
  26. package/src/channel/streaming.ts +740 -0
  27. package/src/commands/changelog.ts +80 -0
  28. package/src/commands/doctor.ts +545 -0
  29. package/src/commands/logs.ts +449 -0
  30. package/src/commands/version.ts +20 -0
  31. package/src/compat/openclaw-sdk.ts +218 -0
  32. package/src/handler/message-handler.ts +673 -166
  33. package/src/logging.ts +1 -1
  34. package/src/runtime.ts +1 -1
  35. package/src/security/dm-policy.ts +1 -4
  36. package/src/security/group-policy.ts +174 -51
  37. package/src/tools/actions/index.ts +15 -13
  38. package/src/tools/cron/relay.ts +1154 -0
  39. package/src/tools/hooks/index.ts +13 -1
  40. package/src/tools/index.ts +714 -32
  41. package/src/types.ts +144 -25
  42. package/src/utils/audio/g722/dct_tables.ts +381 -0
  43. package/src/utils/audio/g722/decoder.ts +919 -0
  44. package/src/utils/audio/g722/defs.ts +105 -0
  45. package/src/utils/audio/g722/hd-parser.ts +247 -0
  46. package/src/utils/audio/g722/huff_tables.ts +240 -0
  47. package/src/utils/audio/g722/index.ts +78 -0
  48. package/src/utils/audio/g722/output_decoded.pcm +0 -0
  49. package/src/utils/audio/g722/output_decoded.wav +0 -0
  50. package/src/utils/audio/g722/tables.ts +173 -0
  51. package/src/utils/audio/g722/test_api.ts +31 -0
  52. package/src/utils/audio/g722/test_voice.hd +0 -0
  53. package/src/utils/bos/im-bos-client.ts +219 -0
  54. package/src/utils/group-agent-cache.ts +142 -0
  55. package/src/utils/token-adapter.ts +120 -51
@@ -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
- return { ok: false, error: formatInfoflowError(err) };
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 URL and body when verbose logging is enabled
287
- logVerbose(`[infoflow:sendPrivate] POST body: ${bodyStr}`);
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
- logVerbose(`[infoflow:sendPrivate] response: status=${res.status}, data=${responseText}`);
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: account.config.appAgentId != null ? `agent:${account.config.appAgentId}` : "agent:unknown",
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, atuserids: [] });
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", atuserids: [], atagentids: agentIds });
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 = body.filter((b) => b.type !== "LINK" && b.type !== "IMAGE");
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), // messageid应该是字符串
570
+ messageid: String(replyTo.messageid),
469
571
  preview: replyTo.preview ?? "",
470
- ...(replyTo.imid ? { imid: replyTo.imid } : {}), // 如果有 imid 则添加
471
- replyType: replyTo.replytype ?? "1", // 注意是replyType,不是replytype
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
- logVerbose(`[infoflow:sendGroup] response: status=${res.status}, data=${responseText}`);
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: account.config.appAgentId != null ? `agent:${account.config.appAgentId}` : "agent:unknown",
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({ mediaUrl: href, mediaLocalRoots:[href] });
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: resolvedContents,
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
  // ---------------------------------------------------------------------------