@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.
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 +16 -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
@@ -1,18 +1,19 @@
1
- import {
2
- createReplyPrefixOptions,
3
- type OpenClawConfig,
4
- type ReplyPayload,
5
- } from "openclaw/plugin-sdk";
6
- import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "../../logging.js";
1
+ import { createReplyPrefixOptions } from "../../compat/openclaw-sdk.js";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
3
+ import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
7
4
  import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "../../channel/media.js";
8
- import { getInfoflowRuntime } from "../../runtime.js";
9
5
  import { isLikelyLocalPath, sendInfoflowMessage } from "../../channel/outbound.js";
6
+ import type { InfoflowStreamingCardSession } from "../../channel/streaming.js";
7
+ import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "../../logging.js";
8
+ import { getInfoflowRuntime } from "../../runtime.js";
10
9
  import type {
11
10
  InfoflowAtOptions,
12
11
  InfoflowMentionIds,
13
12
  InfoflowMessageContentItem,
13
+ InfoflowMessageFormat,
14
14
  InfoflowOutboundReply,
15
15
  } from "../../types.js";
16
+ import { parseMarkdownForLocalImages } from "./markdown-local-images.js";
16
17
 
17
18
  const PREVIEW_MAX_LENGTH = 100;
18
19
 
@@ -33,18 +34,25 @@ export type CreateInfoflowReplyDispatcherParams = {
33
34
  atOptions?: InfoflowAtOptions;
34
35
  /** Mention IDs from inbound message for resolving @id in LLM output */
35
36
  mentionIds?: InfoflowMentionIds;
36
- /** Inbound message ID for outbound reply-to (group only) */
37
+ /** Inbound message ID for outbound reply-to */
37
38
  replyToMessageId?: string;
38
39
  /** Preview text of the inbound message for reply context */
39
40
  replyToPreview?: string;
40
41
  /** IMID of the user who sent the message being replied to */
41
42
  replyToImid?: string;
43
+ /** Secondary message ID for private reply (maps to API field "msgid2") */
44
+ replyToMsgid2?: string;
42
45
  /**
43
- * Message format for outbound text: "text" or "markdown".
46
+ * Message format for outbound text.
44
47
  * Note: "markdown" does not support reply-to (quote) — replyTo will be ignored when using markdown.
45
- * Default: "text"
46
48
  */
47
- messageFormat?: "text" | "markdown";
49
+ messageFormat?: InfoflowMessageFormat;
50
+ streamingCard?: InfoflowStreamingCardSession;
51
+ /**
52
+ * Maximum character limit per outbound message chunk.
53
+ * Default: 1800
54
+ */
55
+ textChunkLimit?: number;
48
56
  };
49
57
 
50
58
  /**
@@ -63,7 +71,10 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
63
71
  replyToMessageId,
64
72
  replyToPreview,
65
73
  replyToImid,
74
+ replyToMsgid2,
66
75
  messageFormat = "text",
76
+ textChunkLimit = 1800,
77
+ streamingCard,
67
78
  } = params;
68
79
  const core = getInfoflowRuntime();
69
80
 
@@ -78,7 +89,10 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
78
89
  const isGroup = /^group:\d+$/i.test(to);
79
90
 
80
91
  // markdown format does not support reply-to; fall back to no replyTo when using markdown
81
- const effectiveReplyToMessageId = messageFormat === "markdown" ? undefined : replyToMessageId;
92
+ const effectiveReplyToMessageId =
93
+ messageFormat === "markdown" || messageFormat === "streaming-card"
94
+ ? undefined
95
+ : replyToMessageId;
82
96
 
83
97
  // Build id→type map for resolving @id in LLM output (distinguishes user vs agent)
84
98
  const mentionIdMap = new Map<string, "user" | "agent">();
@@ -92,31 +106,21 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
92
106
  }
93
107
 
94
108
  // Build replyTo context (only used for the first outbound message)
95
- // Note: replyTo is suppressed when messageFormat is "markdown" (not supported by API)
96
- logVerbose(`[DEBUG reply-dispatcher] isGroup=${isGroup}, replyToMessageId=${replyToMessageId}, replyToImid=${replyToImid}, replyToPreview=${replyToPreview?.slice(0, 50)}`);
97
-
109
+ // Note: replyTo is suppressed when messageFormat is "markdown" or "streaming-card" (not supported by API)
98
110
  const replyTo: InfoflowOutboundReply | undefined =
99
- isGroup && effectiveReplyToMessageId
111
+ effectiveReplyToMessageId
100
112
  ? {
101
113
  messageid: effectiveReplyToMessageId,
102
114
  preview: truncatePreview(replyToPreview),
103
115
  ...(replyToImid ? { imid: replyToImid } : {}),
104
- replytype: "2" // "2" = 引用模式
116
+ replytype: "2", // "2" = 引用模式
117
+ ...(replyToMsgid2 ? { msgid2: replyToMsgid2 } : {}),
105
118
  }
106
119
  : undefined;
107
120
  let replyApplied = false;
108
121
 
109
- // Debug: Log constructed replyTo
110
- logVerbose(`[DEBUG reply-dispatcher] replyTo constructed: ${JSON.stringify(replyTo)}`);
111
- if (replyTo) {
112
- logVerbose(`[DEBUG reply-dispatcher] Creating reply context with messageid=${replyTo.messageid}`);
113
- } else {
114
- logVerbose(`[DEBUG reply-dispatcher] replyTo is undefined - not creating reply context`);
115
- }
116
-
117
- const deliver = async (payload: ReplyPayload) => {
122
+ const deliverStandard = async (payload: ReplyPayload) => {
118
123
  const text = payload.text ?? "";
119
- logVerbose(`[infoflow] deliver called: to=${to}, text=${text}`);
120
124
 
121
125
  // Normalize media URL list (same pattern as Feishu reply-dispatcher)
122
126
  const mediaList =
@@ -126,7 +130,6 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
126
130
  ? [payload.mediaUrl]
127
131
  : [];
128
132
 
129
- logVerbose(`[infoflow] deliver called: to=${to}, text=${text}, mediaList=${JSON.stringify(mediaList)}`);
130
133
  if (!text.trim() && mediaList.length === 0) {
131
134
  return;
132
135
  }
@@ -169,52 +172,113 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
169
172
  // AT nodes will be added to contents for notification, but text stays clean
170
173
  let messageText = text;
171
174
 
172
- // Chunk text to 4000 chars max (Infoflow limit)
173
- const chunks = core.channel.text.chunkText(messageText, 4000);
175
+ if (isGroup && atOptions && messageFormat == "markdown") {
176
+ let atPrefix = "";
177
+ if (hasAtAll) {
178
+ atPrefix = "@all ";
179
+ } else if (atOptions.atUserIds?.length) {
180
+ atPrefix = atOptions.atUserIds.map((id) => `@${id}`).join(" ") + " ";
181
+ }
182
+ if (hasAtAgents) {
183
+ atPrefix += resolvedAgentIds.map((id) => `@${id}`).join(" ") + " ";
184
+ }
185
+ messageText = atPrefix + text;
186
+ }
187
+
188
+ // Chunk text using markdown-aware chunker with configured limit
189
+ const chunks = core.channel.text.chunkMarkdownText(messageText, textChunkLimit);
174
190
  // Only include @mentions in the first chunk (avoid duplicate @s)
175
191
  let isFirstChunk = true;
176
192
 
177
193
  for (const chunk of chunks) {
178
- const contents: InfoflowMessageContentItem[] = [];
194
+ // Parse chunk for embedded local images (e.g. ![alt](/path/to/image.png))
195
+ const segments = parseMarkdownForLocalImages(chunk);
179
196
 
180
- // Add AT content nodes for group messages (first chunk only)
181
- if (isFirstChunk && isGroup) {
182
- if (hasAtAll) {
183
- contents.push({ type: "at", content: "all" });
184
- } else if (hasAtUsers) {
185
- contents.push({ type: "at", content: allAtUserIds.join(",") });
186
- }
187
- if (hasAtAgents) {
188
- contents.push({ type: "at-agent", content: resolvedAgentIds.join(",") });
197
+ for (const segment of segments) {
198
+ if (segment.kind === "image") {
199
+ // Send local image as native image message
200
+ const imgReplyTo = !replyApplied ? replyTo : undefined;
201
+ try {
202
+ const prepared = await prepareInfoflowImageBase64({
203
+ mediaUrl: segment.path,
204
+ mediaLocalRoots: [segment.path],
205
+ });
206
+ if (prepared.isImage) {
207
+ const result = await sendInfoflowImageMessage({
208
+ cfg,
209
+ to,
210
+ base64Image: prepared.base64,
211
+ accountId,
212
+ replyTo: imgReplyTo,
213
+ });
214
+ if (result.ok) {
215
+ if (imgReplyTo) replyApplied = true;
216
+ statusSink?.({ lastOutboundAt: Date.now() });
217
+ // AT already consumed on first text segment; skip AT for images
218
+ isFirstChunk = false;
219
+ continue;
220
+ }
221
+ logVerbose(
222
+ `[infoflow] inline image send failed: ${result.error}, falling back to link`,
223
+ );
224
+ }
225
+ } catch (err) {
226
+ logVerbose(`[infoflow] inline image prep failed: ${err}`);
227
+ }
228
+ // Fallback: send image path as link
229
+ await sendInfoflowMessage({
230
+ cfg,
231
+ to,
232
+ contents: [{ type: "link", content: segment.path }],
233
+ accountId,
234
+ replyTo: imgReplyTo,
235
+ });
236
+ if (imgReplyTo) replyApplied = true;
237
+ isFirstChunk = false;
238
+ continue;
189
239
  }
190
- }
191
- isFirstChunk = false;
192
240
 
193
- // Add text content using configured format
194
- contents.push({ type: messageFormat, content: chunk });
241
+ // Text segment send as before
242
+ const contents: InfoflowMessageContentItem[] = [];
195
243
 
196
- // Only include replyTo on the first outbound message
197
- const chunkReplyTo = !replyApplied ? replyTo : undefined;
244
+ // Add AT content nodes for group messages (first chunk only)
245
+ if (isFirstChunk && isGroup) {
246
+ if (hasAtAll) {
247
+ contents.push({ type: "at", content: "all" });
248
+ } else if (hasAtUsers) {
249
+ contents.push({ type: "at", content: allAtUserIds.join(",") });
250
+ }
251
+ if (hasAtAgents) {
252
+ contents.push({ type: "at-agent", content: resolvedAgentIds.join(",") });
253
+ }
254
+ }
255
+ isFirstChunk = false;
198
256
 
199
- // Debug: Log when sending with replyTo
200
- logVerbose(`[DEBUG deliver] chunkReplyTo: ${JSON.stringify(chunkReplyTo)}`);
201
- logVerbose(`[DEBUG deliver] replyApplied=${replyApplied}, replyTo exists=${!!replyTo}`);
257
+ // Add text content using configured format
258
+ contents.push({ type: messageFormat, content: segment.content });
202
259
 
203
- const result = await sendInfoflowMessage({
204
- cfg,
205
- to,
206
- contents,
207
- accountId,
208
- replyTo: chunkReplyTo,
209
- });
210
- if (chunkReplyTo) replyApplied = true;
260
+ // Only include replyTo on the first outbound message
261
+ const chunkReplyTo = !replyApplied ? replyTo : undefined;
211
262
 
212
- if (result.ok) {
213
- statusSink?.({ lastOutboundAt: Date.now() });
214
- } else if (result.error) {
215
- getInfoflowSendLog().error(
216
- `[infoflow] reply failed to=${to}, accountId=${accountId}: ${result.error}`,
217
- );
263
+ const result = await sendInfoflowMessage({
264
+ cfg,
265
+ to,
266
+ contents,
267
+ accountId,
268
+ replyTo: chunkReplyTo,
269
+ });
270
+ if (chunkReplyTo) replyApplied = true;
271
+
272
+ if (result.ok) {
273
+ getInfoflowSendLog().info(
274
+ `[outbound] to=${to}, len=${segment.content.length}, msgId=${result.messageId ?? "N/A"}, replyTo=${chunkReplyTo?.messageid ?? "none"}`,
275
+ );
276
+ statusSink?.({ lastOutboundAt: Date.now() });
277
+ } else if (result.error) {
278
+ getInfoflowSendLog().error(
279
+ `[outbound:error] to=${to}, accountId=${accountId}: ${result.error}`,
280
+ );
281
+ }
218
282
  }
219
283
  }
220
284
  }
@@ -223,8 +287,8 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
223
287
  for (const mediaUrl of mediaList) {
224
288
  const mediaReplyTo = !replyApplied ? replyTo : undefined;
225
289
  try {
226
- const paths = isLikelyLocalPath(mediaUrl)?[mediaUrl]:undefined;
227
- const prepared = await prepareInfoflowImageBase64({ mediaUrl , mediaLocalRoots: paths });
290
+ const paths = isLikelyLocalPath(mediaUrl) ? [mediaUrl] : undefined;
291
+ const prepared = await prepareInfoflowImageBase64({ mediaUrl, mediaLocalRoots: paths });
228
292
  if (prepared.isImage) {
229
293
  const result = await sendInfoflowImageMessage({
230
294
  cfg,
@@ -255,10 +319,27 @@ export function createInfoflowReplyDispatcher(params: CreateInfoflowReplyDispatc
255
319
  }
256
320
  };
257
321
 
322
+ const deliver = async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
323
+ if (messageFormat === "streaming-card" && streamingCard?.isUsable()) {
324
+ const handled = await streamingCard.handleDeliveredPayload({
325
+ kind: info?.kind ?? "final",
326
+ payload,
327
+ });
328
+ if (handled) {
329
+ return;
330
+ }
331
+ }
332
+ return deliverStandard(payload);
333
+ };
334
+
258
335
  const onError = (err: unknown) => {
259
336
  getInfoflowSendLog().error(
260
337
  `[infoflow] reply error to=${to}, accountId=${accountId}: ${formatInfoflowError(err)}`,
261
338
  );
339
+ if (messageFormat === "streaming-card" && streamingCard?.isUsable()) {
340
+ void streamingCard.failWithMessage("处理出错,请稍后重试");
341
+ return;
342
+ }
262
343
  // Send error hint so users know the bot didn't silently fail
263
344
  sendInfoflowMessage({
264
345
  cfg,
@@ -32,14 +32,15 @@ const USER_PREFIX = "user:";
32
32
  * "123456" -> "group:123456" (pure digits treated as group)
33
33
  */
34
34
  export function normalizeInfoflowTarget(raw: string): string | undefined {
35
- logVerbose(`[infoflow:normalizeTarget] input: "${raw}"`);
36
-
37
35
  const trimmed = raw.trim();
38
36
  if (!trimmed) {
39
- logVerbose(`[infoflow:normalizeTarget] empty input, returning undefined`);
37
+ // Empty targets are common on cron-relay ticks when metadata/payload/session
38
+ // have not yet supplied a `to`; avoid verbose spam every poll interval.
40
39
  return undefined;
41
40
  }
42
41
 
42
+ logVerbose(`[infoflow:normalizeTarget] input: "${raw}"`);
43
+
43
44
  // Strip infoflow: prefix
44
45
  let target = trimmed.replace(/^infoflow:/i, "");
45
46
 
@@ -3,8 +3,28 @@
3
3
  * Handles multi-account support with config merging.
4
4
  */
5
5
 
6
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId, type OpenClawConfig } from "openclaw/plugin-sdk";
7
- import type { InfoflowAccountConfig, InfoflowConnectionMode, ResolvedInfoflowAccount } from "../types.js";
6
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../compat/openclaw-sdk.js";
7
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
8
+ import { normalizeInfoflowTarget } from "../adapter/outbound/target-resolver.js";
9
+ import type {
10
+ InfoflowAccountConfig,
11
+ InfoflowConnectionMode,
12
+ InfoflowMessageFormat,
13
+ ResolvedInfoflowAccount,
14
+ } from "../types.js";
15
+
16
+ export const DEFAULT_INFOFLOW_API_HOST = "https://api.im.baidu.com";
17
+ export const DEFAULT_INFOFLOW_CONNECTION_MODE: InfoflowConnectionMode = "websocket";
18
+ export const DEFAULT_INFOFLOW_WS_GATEWAY = "infoflow-open-gateway.baidu.com";
19
+ export const DEFAULT_INFOFLOW_GROUP_SESSION_MODE = "group";
20
+ export const DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW = 300;
21
+ export const DEFAULT_INFOFLOW_PROCESSING_HINT_DELAY = 5;
22
+ export const DEFAULT_INFOFLOW_TEXT_CHUNK_LIMIT = 1800;
23
+ export const DEFAULT_INFOFLOW_DM_MESSAGE_FORMAT = "markdown";
24
+ export const DEFAULT_INFOFLOW_GROUP_MESSAGE_FORMAT = "markdown";
25
+ export const DEFAULT_INFOFLOW_PRIVATE_DATA_DIR = "plugins/infoflow-private";
26
+ export const DEFAULT_INFOFLOW_CRON_RELAY_PREFIX = "【定时任务】";
27
+ export const DEFAULT_INFOFLOW_CRON_RELAY_POLL_INTERVAL_MS = 2000;
8
28
 
9
29
  // ---------------------------------------------------------------------------
10
30
  // Config Access Helpers
@@ -70,22 +90,42 @@ function mergeInfoflowAccountConfig(
70
90
  name?: string;
71
91
  connectionMode?: InfoflowConnectionMode;
72
92
  wsGateway?: string;
93
+ wsConnectDomain?: string;
73
94
  robotName?: string;
95
+ robotId?: string;
74
96
  requireMention?: boolean;
75
97
  watchMentions?: string[];
76
- watchRegex?: string;
98
+ watchRegex?: string | string[];
77
99
  appAgentId?: number;
78
- dmMessageFormat?: "text" | "markdown";
79
- groupMessageFormat?: "text" | "markdown";
80
- dmPolicy?: string;
100
+ defaultTo?: string;
101
+ privateDataDir?: string;
102
+ cronRelay?: import("../types.js").InfoflowCronRelayConfig;
103
+ processingHint?: boolean;
104
+ processingHintDelay?: number;
105
+ dmMessageFormat?: InfoflowMessageFormat;
106
+ groupMessageFormat?: InfoflowMessageFormat;
107
+ replyMode?: import("../types.js").InfoflowReplyMode;
108
+ groupSessionMode?: import("../types.js").InfoflowGroupSessionMode;
109
+ followUp?: boolean;
110
+ followUpWindow?: number;
111
+ groups?: Record<string, import("../types.js").InfoflowGroupConfig>;
112
+ dmPolicy?: import("../types.js").InfoflowDmPolicy;
81
113
  allowFrom?: string[];
82
- groupPolicy?: string;
114
+ groupPolicy?: import("../types.js").InfoflowGroupPolicy;
83
115
  groupAllowFrom?: string[];
116
+ textChunkLimit?: number;
84
117
  } {
85
118
  const raw = getChannelSection(cfg) ?? {};
86
119
  const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
87
120
  const account = raw.accounts?.[accountId] ?? {};
88
- return { ...base, ...account } as {
121
+ return {
122
+ ...base,
123
+ ...account,
124
+ cronRelay: {
125
+ ...(base.cronRelay ?? {}),
126
+ ...(account.cronRelay ?? {}),
127
+ },
128
+ } as {
89
129
  apiHost: string;
90
130
  checkToken: string;
91
131
  encodingAESKey: string;
@@ -95,17 +135,30 @@ function mergeInfoflowAccountConfig(
95
135
  name?: string;
96
136
  connectionMode?: InfoflowConnectionMode;
97
137
  wsGateway?: string;
138
+ wsConnectDomain?: string;
98
139
  robotName?: string;
140
+ robotId?: string;
99
141
  requireMention?: boolean;
100
142
  watchMentions?: string[];
101
- watchRegex?: string;
143
+ watchRegex?: string | string[];
102
144
  appAgentId?: number;
103
- dmMessageFormat?: "text" | "markdown";
104
- groupMessageFormat?: "text" | "markdown";
105
- dmPolicy?: string;
145
+ defaultTo?: string;
146
+ privateDataDir?: string;
147
+ cronRelay?: import("../types.js").InfoflowCronRelayConfig;
148
+ processingHint?: boolean;
149
+ processingHintDelay?: number;
150
+ dmMessageFormat?: InfoflowMessageFormat;
151
+ groupMessageFormat?: InfoflowMessageFormat;
152
+ replyMode?: import("../types.js").InfoflowReplyMode;
153
+ groupSessionMode?: import("../types.js").InfoflowGroupSessionMode;
154
+ followUp?: boolean;
155
+ followUpWindow?: number;
156
+ groups?: Record<string, import("../types.js").InfoflowGroupConfig>;
157
+ dmPolicy?: import("../types.js").InfoflowDmPolicy;
106
158
  allowFrom?: string[];
107
- groupPolicy?: string;
159
+ groupPolicy?: import("../types.js").InfoflowGroupPolicy;
108
160
  groupAllowFrom?: string[];
161
+ textChunkLimit?: number;
109
162
  };
110
163
  }
111
164
 
@@ -125,13 +178,16 @@ export function resolveInfoflowAccount(params: {
125
178
  const merged = mergeInfoflowAccountConfig(params.cfg, accountId);
126
179
  const accountEnabled = merged.enabled !== false;
127
180
  const enabled = baseEnabled && accountEnabled;
128
- const apiHost = merged.apiHost ?? "";
181
+ const apiHost = merged.apiHost?.trim() || DEFAULT_INFOFLOW_API_HOST;
129
182
  const checkToken = merged.checkToken ?? "";
130
183
  const encodingAESKey = merged.encodingAESKey ?? "";
131
184
  const appKey = merged.appKey ?? "";
132
185
  const appSecret = merged.appSecret ?? "";
186
+ const effectiveConnectionMode = merged.connectionMode ?? DEFAULT_INFOFLOW_CONNECTION_MODE;
133
187
  const configured =
134
- Boolean(checkToken) && Boolean(encodingAESKey) && Boolean(appKey) && Boolean(appSecret);
188
+ effectiveConnectionMode === "websocket"
189
+ ? Boolean(appKey) && Boolean(appSecret)
190
+ : Boolean(checkToken) && Boolean(encodingAESKey) && Boolean(appKey) && Boolean(appSecret);
135
191
 
136
192
  return {
137
193
  accountId,
@@ -146,19 +202,38 @@ export function resolveInfoflowAccount(params: {
146
202
  encodingAESKey,
147
203
  appKey,
148
204
  appSecret,
149
- connectionMode: merged.connectionMode,
150
- wsGateway: merged.wsGateway,
205
+ connectionMode: effectiveConnectionMode,
206
+ wsGateway: merged.wsGateway?.trim() || DEFAULT_INFOFLOW_WS_GATEWAY,
207
+ wsConnectDomain: merged.wsConnectDomain?.trim() || undefined,
151
208
  robotName: merged.robotName?.trim() || undefined,
152
- requireMention: merged.requireMention,
209
+ robotId: merged.robotId?.trim() || undefined,
210
+ requireMention: merged.requireMention ?? true,
153
211
  watchMentions: merged.watchMentions,
154
212
  watchRegex: merged.watchRegex,
155
213
  appAgentId: merged.appAgentId,
156
- dmMessageFormat: merged.dmMessageFormat,
157
- groupMessageFormat: merged.groupMessageFormat,
158
- dmPolicy: merged.dmPolicy,
214
+ defaultTo: normalizeInfoflowTarget(merged.defaultTo ?? "") ?? undefined,
215
+ privateDataDir: merged.privateDataDir?.trim() || DEFAULT_INFOFLOW_PRIVATE_DATA_DIR,
216
+ cronRelay: {
217
+ enabled: merged.cronRelay?.enabled ?? true,
218
+ includeAlreadyDelivered: merged.cronRelay?.includeAlreadyDelivered ?? false,
219
+ prefix: merged.cronRelay?.prefix?.trim() || DEFAULT_INFOFLOW_CRON_RELAY_PREFIX,
220
+ pollIntervalMs:
221
+ merged.cronRelay?.pollIntervalMs ?? DEFAULT_INFOFLOW_CRON_RELAY_POLL_INTERVAL_MS,
222
+ },
223
+ processingHint: merged.processingHint ?? true,
224
+ processingHintDelay: merged.processingHintDelay ?? DEFAULT_INFOFLOW_PROCESSING_HINT_DELAY,
225
+ dmMessageFormat: merged.dmMessageFormat ?? DEFAULT_INFOFLOW_DM_MESSAGE_FORMAT,
226
+ groupMessageFormat: merged.groupMessageFormat ?? DEFAULT_INFOFLOW_GROUP_MESSAGE_FORMAT,
227
+ replyMode: merged.replyMode,
228
+ groupSessionMode: merged.groupSessionMode ?? DEFAULT_INFOFLOW_GROUP_SESSION_MODE,
229
+ followUp: merged.followUp ?? true,
230
+ followUpWindow: merged.followUpWindow ?? DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW,
231
+ groups: merged.groups,
232
+ dmPolicy: merged.dmPolicy ?? "open",
159
233
  allowFrom: merged.allowFrom,
160
- groupPolicy: merged.groupPolicy,
234
+ groupPolicy: merged.groupPolicy ?? "open",
161
235
  groupAllowFrom: merged.groupAllowFrom,
236
+ textChunkLimit: merged.textChunkLimit ?? DEFAULT_INFOFLOW_TEXT_CHUNK_LIMIT,
162
237
  },
163
238
  };
164
239
  }