@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
@@ -6,23 +6,38 @@ import {
6
6
  migrateBaseNameToDefaultAccount,
7
7
  normalizeAccountId,
8
8
  setAccountEnabledInConfigSection,
9
- type ChannelPlugin,
10
- type OpenClawConfig,
11
- } from "openclaw/plugin-sdk";
9
+ } from "../compat/openclaw-sdk.js";
10
+ import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
11
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
12
12
  import {
13
+ normalizeInfoflowTarget,
14
+ looksLikeInfoflowId,
15
+ } from "../adapter/outbound/target-resolver.js";
16
+ import { logVerbose } from "../logging.js";
17
+ import { getInfoflowRuntime } from "../runtime.js";
18
+ import { infoflowMessageActions } from "../tools/actions/index.js";
19
+ import type { ResolvedInfoflowAccount } from "../types.js";
20
+ import {
21
+ DEFAULT_INFOFLOW_API_HOST,
22
+ DEFAULT_INFOFLOW_CONNECTION_MODE,
23
+ DEFAULT_INFOFLOW_CRON_RELAY_POLL_INTERVAL_MS,
24
+ DEFAULT_INFOFLOW_CRON_RELAY_PREFIX,
25
+ DEFAULT_INFOFLOW_DM_MESSAGE_FORMAT,
26
+ DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW,
27
+ DEFAULT_INFOFLOW_GROUP_MESSAGE_FORMAT,
28
+ DEFAULT_INFOFLOW_GROUP_SESSION_MODE,
29
+ DEFAULT_INFOFLOW_PRIVATE_DATA_DIR,
30
+ DEFAULT_INFOFLOW_PROCESSING_HINT_DELAY,
31
+ DEFAULT_INFOFLOW_TEXT_CHUNK_LIMIT,
32
+ DEFAULT_INFOFLOW_WS_GATEWAY,
13
33
  getChannelSection,
14
34
  listInfoflowAccountIds,
15
35
  resolveDefaultInfoflowAccountId,
16
36
  resolveInfoflowAccount,
17
37
  } from "./accounts.js";
18
- import { infoflowMessageActions } from "../tools/actions/index.js";
19
- import { logVerbose } from "../logging.js";
20
38
  import { prepareInfoflowImageBase64, sendInfoflowImageMessage } from "./media.js";
21
39
  import { startInfoflowMonitor, startInfoflowWSMonitor } from "./monitor.js";
22
- import { getInfoflowRuntime } from "../runtime.js";
23
40
  import { sendInfoflowMessage } from "./outbound.js";
24
- import { normalizeInfoflowTarget, looksLikeInfoflowId } from "../adapter/outbound/target-resolver.js";
25
- import type { ResolvedInfoflowAccount } from "../types.js";
26
41
 
27
42
  // Re-export types and account functions for external consumers
28
43
  export type { InfoflowAccountConfig, ResolvedInfoflowAccount } from "../types.js";
@@ -32,6 +47,434 @@ export { resolveInfoflowAccount } from "./accounts.js";
32
47
  // Channel plugin
33
48
  // ---------------------------------------------------------------------------
34
49
 
50
+ const INFOFLOW_REPLY_MODE_ENUM = [
51
+ "ignore",
52
+ "record",
53
+ "mention-only",
54
+ "mention-and-watch",
55
+ "proactive",
56
+ ] as const;
57
+
58
+ const infoflowGroupConfigSchemaProperties = {
59
+ replyMode: {
60
+ type: "string",
61
+ enum: [...INFOFLOW_REPLY_MODE_ENUM],
62
+ description: "群级回复模式覆盖",
63
+ },
64
+ groupSessionMode: {
65
+ type: "string",
66
+ enum: ["group", "user"],
67
+ default: DEFAULT_INFOFLOW_GROUP_SESSION_MODE,
68
+ description: "群聊会话拆分模式:group=按群,user=按群+人",
69
+ },
70
+ watchMentions: {
71
+ type: "array",
72
+ items: { type: "string" },
73
+ description: "当这些人被 @ 时触发观察模式",
74
+ },
75
+ watchRegex: {
76
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
77
+ description: "群消息匹配任一正则时触发观察模式(兼容单个字符串或数组)",
78
+ },
79
+ followUp: {
80
+ type: "boolean",
81
+ default: true,
82
+ description: "机器人回复后是否允许一段时间内连续追问",
83
+ },
84
+ followUpWindow: {
85
+ type: "integer",
86
+ minimum: 0,
87
+ default: DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW,
88
+ description: "追问窗口时长(秒)",
89
+ },
90
+ systemPrompt: {
91
+ type: "string",
92
+ description: "该群专属的附加系统提示词",
93
+ },
94
+ } as const;
95
+
96
+ const infoflowCronRelaySchema = {
97
+ type: "object",
98
+ additionalProperties: false,
99
+ properties: {
100
+ enabled: {
101
+ type: "boolean",
102
+ default: true,
103
+ description: "是否把 OpenClaw cron 运行结果转发到如流",
104
+ },
105
+ includeAlreadyDelivered: {
106
+ type: "boolean",
107
+ default: false,
108
+ description: "是否也转发已由 OpenClaw delivery 投递过的任务",
109
+ },
110
+ prefix: {
111
+ type: "string",
112
+ default: DEFAULT_INFOFLOW_CRON_RELAY_PREFIX,
113
+ description: "定时任务转发前缀",
114
+ },
115
+ pollIntervalMs: {
116
+ type: "integer",
117
+ minimum: 500,
118
+ default: DEFAULT_INFOFLOW_CRON_RELAY_POLL_INTERVAL_MS,
119
+ description: "cron 运行日志轮询间隔(毫秒)",
120
+ },
121
+ },
122
+ } as const;
123
+
124
+ const infoflowAccountConfigSchemaProperties = {
125
+ enabled: {
126
+ type: "boolean",
127
+ default: true,
128
+ description: "是否启用当前 Infoflow 账号",
129
+ },
130
+ name: {
131
+ type: "string",
132
+ description: "账号展示名称,仅用于控制面板区分账号",
133
+ },
134
+ apiHost: {
135
+ type: "string",
136
+ default: DEFAULT_INFOFLOW_API_HOST,
137
+ description: "如流发送 API 域名",
138
+ },
139
+ connectionMode: {
140
+ type: "string",
141
+ enum: ["webhook", "websocket"],
142
+ default: DEFAULT_INFOFLOW_CONNECTION_MODE,
143
+ description: "消息接收模式,默认 websocket",
144
+ },
145
+ wsGateway: {
146
+ type: "string",
147
+ default: DEFAULT_INFOFLOW_WS_GATEWAY,
148
+ description: "WebSocket 网关域名,仅 websocket 模式使用",
149
+ },
150
+ checkToken: {
151
+ type: "string",
152
+ description: "Webhook 校验 token,仅 webhook 模式必填",
153
+ },
154
+ encodingAESKey: {
155
+ type: "string",
156
+ description: "Webhook 加密密钥,仅 webhook 模式必填",
157
+ },
158
+ appKey: {
159
+ type: "string",
160
+ description: "如流应用 AppKey",
161
+ },
162
+ appSecret: {
163
+ type: "string",
164
+ description: "如流应用 AppSecret",
165
+ },
166
+ robotName: {
167
+ type: "string",
168
+ description: "群聊中用于识别 @机器人的显示名",
169
+ },
170
+ appAgentId: {
171
+ type: "integer",
172
+ minimum: 1,
173
+ description: "如流应用 ID,私聊撤回依赖该字段",
174
+ },
175
+ dmPolicy: {
176
+ type: "string",
177
+ enum: ["open", "pairing", "allowlist"],
178
+ default: "open",
179
+ description: "单聊访问策略",
180
+ },
181
+ allowFrom: {
182
+ type: "array",
183
+ items: { type: "string" },
184
+ description: "单聊白名单,dmPolicy=allowlist 时生效",
185
+ },
186
+ groupPolicy: {
187
+ type: "string",
188
+ enum: ["open", "allowlist", "disabled"],
189
+ default: "open",
190
+ description: "群聊访问策略",
191
+ },
192
+ groupAllowFrom: {
193
+ type: "array",
194
+ items: { type: "string" },
195
+ description: "群聊白名单,groupPolicy=allowlist 时生效",
196
+ },
197
+ requireMention: {
198
+ type: "boolean",
199
+ default: true,
200
+ description: "未显式设置 replyMode 时,是否要求先 @机器人",
201
+ },
202
+ replyMode: {
203
+ type: "string",
204
+ enum: [...INFOFLOW_REPLY_MODE_ENUM],
205
+ description: "群聊回复模式;不填时按 requireMention/watch 配置推导",
206
+ },
207
+ groupSessionMode: {
208
+ type: "string",
209
+ enum: ["group", "user"],
210
+ default: DEFAULT_INFOFLOW_GROUP_SESSION_MODE,
211
+ description: "群聊会话拆分模式:group=按群,user=按群+人",
212
+ },
213
+ watchMentions: {
214
+ type: "array",
215
+ items: { type: "string" },
216
+ description: "当这些人被 @ 时触发观察模式",
217
+ },
218
+ watchRegex: {
219
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
220
+ description: "群消息匹配任一正则时触发观察模式(兼容单个字符串或数组)",
221
+ },
222
+ followUp: {
223
+ type: "boolean",
224
+ default: true,
225
+ description: "机器人回复后是否允许一段时间内连续追问",
226
+ },
227
+ followUpWindow: {
228
+ type: "integer",
229
+ minimum: 0,
230
+ default: DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW,
231
+ description: "追问窗口时长(秒)",
232
+ },
233
+ dmMessageFormat: {
234
+ type: "string",
235
+ enum: ["text", "markdown", "streaming-card"],
236
+ default: DEFAULT_INFOFLOW_DM_MESSAGE_FORMAT,
237
+ description: "单聊回复格式",
238
+ },
239
+ groupMessageFormat: {
240
+ type: "string",
241
+ enum: ["text", "markdown", "streaming-card"],
242
+ default: DEFAULT_INFOFLOW_GROUP_MESSAGE_FORMAT,
243
+ description: "群聊回复格式;markdown 和 streaming-card 不支持引用回复",
244
+ },
245
+ processingHint: {
246
+ type: "boolean",
247
+ default: true,
248
+ description: "处理较慢时先发送一条“收到啦”提示",
249
+ },
250
+ processingHintDelay: {
251
+ type: "integer",
252
+ minimum: 0,
253
+ default: DEFAULT_INFOFLOW_PROCESSING_HINT_DELAY,
254
+ description: "发送处理中提示前的等待秒数",
255
+ },
256
+ textChunkLimit: {
257
+ type: "integer",
258
+ minimum: 1,
259
+ default: DEFAULT_INFOFLOW_TEXT_CHUNK_LIMIT,
260
+ description: "长消息拆分的单段字符上限",
261
+ },
262
+ defaultTo: {
263
+ type: "string",
264
+ description: "主动发送或兼容场景的默认目标,例如 alice 或 group:12345",
265
+ },
266
+ privateDataDir: {
267
+ type: "string",
268
+ default: DEFAULT_INFOFLOW_PRIVATE_DATA_DIR,
269
+ description: "插件私有数据目录,相对 OpenClaw stateDir",
270
+ },
271
+ cronRelay: infoflowCronRelaySchema,
272
+ } as const;
273
+
274
+ const infoflowChannelConfigSchema = {
275
+ schema: {
276
+ type: "object",
277
+ additionalProperties: false,
278
+ properties: {
279
+ ...infoflowAccountConfigSchemaProperties,
280
+ defaultAccount: {
281
+ type: "string",
282
+ description: "多账号模式下的默认账号 ID",
283
+ },
284
+ groups: {
285
+ type: "object",
286
+ additionalProperties: {
287
+ type: "object",
288
+ additionalProperties: false,
289
+ properties: infoflowGroupConfigSchemaProperties,
290
+ },
291
+ },
292
+ accounts: {
293
+ type: "object",
294
+ additionalProperties: {
295
+ type: "object",
296
+ additionalProperties: false,
297
+ properties: infoflowAccountConfigSchemaProperties,
298
+ },
299
+ },
300
+ },
301
+ },
302
+ uiHints: {
303
+ appKey: {
304
+ label: "AppKey",
305
+ help: "如流应用的 AppKey。",
306
+ },
307
+ appSecret: {
308
+ label: "AppSecret",
309
+ help: "如流应用的 AppSecret。",
310
+ sensitive: true,
311
+ },
312
+ connectionMode: {
313
+ label: "接收模式",
314
+ help: "默认 websocket,本地开发无需公网域名。",
315
+ },
316
+ wsGateway: {
317
+ label: "WS 网关",
318
+ help: "仅 websocket 模式使用。",
319
+ advanced: true,
320
+ },
321
+ apiHost: {
322
+ label: "API 域名",
323
+ help: "发送消息使用的 REST API 域名。",
324
+ advanced: true,
325
+ },
326
+ checkToken: {
327
+ label: "Webhook Token",
328
+ help: "仅 webhook 模式需要。",
329
+ sensitive: true,
330
+ advanced: true,
331
+ },
332
+ encodingAESKey: {
333
+ label: "Webhook AES Key",
334
+ help: "仅 webhook 模式需要。",
335
+ sensitive: true,
336
+ advanced: true,
337
+ },
338
+ enabled: {
339
+ label: "启用通道",
340
+ help: "关闭后停止该账号的收发。",
341
+ },
342
+ name: {
343
+ label: "账号名称",
344
+ help: "仅用于控制台展示。",
345
+ },
346
+ robotName: {
347
+ label: "机器人名称",
348
+ help: "用于识别群里是否 @到了机器人。",
349
+ },
350
+ appAgentId: {
351
+ label: "应用 ID",
352
+ help: "私聊撤回消息需要该值。",
353
+ },
354
+ dmPolicy: {
355
+ label: "单聊策略",
356
+ },
357
+ allowFrom: {
358
+ label: "单聊白名单",
359
+ placeholder: "alice",
360
+ },
361
+ groupPolicy: {
362
+ label: "群聊策略",
363
+ },
364
+ groupAllowFrom: {
365
+ label: "群聊白名单",
366
+ placeholder: "12345678",
367
+ },
368
+ requireMention: {
369
+ label: "要求先 @机器人",
370
+ help: "只在未显式设置 replyMode 时作为推导条件。",
371
+ },
372
+ replyMode: {
373
+ label: "回复模式",
374
+ help: "不填则沿用现有兼容推导逻辑。",
375
+ },
376
+ groupSessionMode: {
377
+ label: "群聊会话模式",
378
+ help: "group=整个群共用 session,user=群内每人独立 session。",
379
+ },
380
+ watchMentions: {
381
+ label: "观察 @名单",
382
+ placeholder: "alice",
383
+ },
384
+ watchRegex: {
385
+ label: "观察正则",
386
+ placeholder: "incident|alert, error.*timeout",
387
+ },
388
+ followUp: {
389
+ label: "允许追问窗口",
390
+ },
391
+ followUpWindow: {
392
+ label: "追问窗口秒数",
393
+ },
394
+ dmMessageFormat: {
395
+ label: "单聊消息格式",
396
+ help: "支持普通文本、Markdown,或如流流式卡片。",
397
+ },
398
+ groupMessageFormat: {
399
+ label: "群聊消息格式",
400
+ help: "支持普通文本、Markdown,或如流流式卡片;markdown 和 streaming-card 不带引用回复。",
401
+ },
402
+ processingHint: {
403
+ label: "处理中提示",
404
+ },
405
+ processingHintDelay: {
406
+ label: "提示延迟秒数",
407
+ },
408
+ textChunkLimit: {
409
+ label: "拆分长度上限",
410
+ advanced: true,
411
+ },
412
+ defaultTo: {
413
+ label: "默认目标",
414
+ help: "仅主动发送和兼容场景使用;infoflow_cron 不依赖该值。",
415
+ advanced: true,
416
+ placeholder: "alice 或 group:12345",
417
+ },
418
+ privateDataDir: {
419
+ label: "私有数据目录",
420
+ help: "cron 可观测文件会写到该目录下,例如 cron-relay-status.md 和 cron-relay-state.json。",
421
+ advanced: true,
422
+ },
423
+ "cronRelay.enabled": {
424
+ label: "启用定时转发",
425
+ help: "开启后会把状态摘要写入私有数据目录,便于直接查看执行结果。",
426
+ advanced: true,
427
+ },
428
+ "cronRelay.includeAlreadyDelivered": {
429
+ label: "包含已投递任务",
430
+ advanced: true,
431
+ },
432
+ "cronRelay.prefix": {
433
+ label: "定时前缀",
434
+ advanced: true,
435
+ },
436
+ "cronRelay.pollIntervalMs": {
437
+ label: "定时轮询间隔",
438
+ help: "影响 relay 扫描 cron runs 和刷新状态摘要文件的频率。",
439
+ advanced: true,
440
+ },
441
+ defaultAccount: {
442
+ label: "默认账号",
443
+ advanced: true,
444
+ },
445
+ groups: {
446
+ label: "群级覆盖",
447
+ help: "按 groupId 配置群级策略覆盖。",
448
+ advanced: true,
449
+ itemTemplate: {
450
+ replyMode: "mention-only",
451
+ groupSessionMode: DEFAULT_INFOFLOW_GROUP_SESSION_MODE,
452
+ followUp: true,
453
+ followUpWindow: DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW,
454
+ },
455
+ },
456
+ accounts: {
457
+ label: "多账号配置",
458
+ help: "按账号 ID 配置额外机器人。",
459
+ advanced: true,
460
+ itemTemplate: {
461
+ enabled: true,
462
+ connectionMode: DEFAULT_INFOFLOW_CONNECTION_MODE,
463
+ apiHost: DEFAULT_INFOFLOW_API_HOST,
464
+ wsGateway: DEFAULT_INFOFLOW_WS_GATEWAY,
465
+ groupSessionMode: DEFAULT_INFOFLOW_GROUP_SESSION_MODE,
466
+ followUp: true,
467
+ followUpWindow: DEFAULT_INFOFLOW_FOLLOW_UP_WINDOW,
468
+ processingHint: true,
469
+ processingHintDelay: DEFAULT_INFOFLOW_PROCESSING_HINT_DELAY,
470
+ dmMessageFormat: DEFAULT_INFOFLOW_DM_MESSAGE_FORMAT,
471
+ groupMessageFormat: DEFAULT_INFOFLOW_GROUP_MESSAGE_FORMAT,
472
+ textChunkLimit: DEFAULT_INFOFLOW_TEXT_CHUNK_LIMIT,
473
+ },
474
+ },
475
+ },
476
+ } as const;
477
+
35
478
  export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
36
479
  id: "infoflow",
37
480
  meta: {
@@ -48,6 +491,7 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
48
491
  unsend: true,
49
492
  },
50
493
  reload: { configPrefixes: ["channels.infoflow"] },
494
+ configSchema: infoflowChannelConfigSchema,
51
495
  actions: infoflowMessageActions,
52
496
  agentPrompt: {
53
497
  messageToolHints: () => [
@@ -205,10 +649,10 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
205
649
  },
206
650
  },
207
651
  outbound: {
208
- deliveryMode: "direct",
652
+ deliveryMode: "gateway",
209
653
  chunkerMode: "markdown",
210
- textChunkLimit: 4000,
211
- chunker: (text, limit) => getInfoflowRuntime().channel.text.chunkText(text, limit),
654
+ textChunkLimit: DEFAULT_INFOFLOW_TEXT_CHUNK_LIMIT,
655
+ chunker: (text, limit) => getInfoflowRuntime().channel.text.chunkMarkdownText(text, limit),
212
656
  sendText: async ({ cfg, to, text, accountId }) => {
213
657
  logVerbose(`[infoflow:sendText] to=${to}, accountId=${accountId}`);
214
658
  // Use "markdown" type even though param is named `text`: LLM outputs are often markdown,
@@ -322,7 +766,7 @@ export const infoflowPlugin: ChannelPlugin<ResolvedInfoflowAccount> = {
322
766
  gateway: {
323
767
  startAccount: async (ctx) => {
324
768
  const account = ctx.account;
325
- const connectionMode = account.config.connectionMode ?? "webhook";
769
+ const connectionMode = account.config.connectionMode ?? "websocket";
326
770
  ctx.log?.info(`[${account.accountId}] starting Infoflow ${connectionMode}`);
327
771
  ctx.setStatus({
328
772
  accountId: account.accountId,
@@ -2,11 +2,13 @@
2
2
  * Infoflow native image sending: compress, base64-encode, and POST via Infoflow API.
3
3
  */
4
4
 
5
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
6
- import { resolveInfoflowAccount } from "./accounts.js";
5
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
7
6
  import { recordSentMessageId } from "../adapter/inbound/webhook-parser.js";
7
+ import { coreEvents } from "../events.js";
8
8
  import { getInfoflowSendLog, formatInfoflowError, logVerbose } from "../logging.js";
9
9
  import { getInfoflowRuntime } from "../runtime.js";
10
+ import type { ResolvedInfoflowAccount, InfoflowOutboundReply } from "../types.js";
11
+ import { resolveInfoflowAccount } from "./accounts.js";
10
12
  import {
11
13
  getAppAccessToken,
12
14
  ensureHttps,
@@ -15,8 +17,6 @@ import {
15
17
  INFOFLOW_PRIVATE_SEND_PATH,
16
18
  INFOFLOW_GROUP_SEND_PATH,
17
19
  } from "./outbound.js";
18
- import { coreEvents } from "../events.js";
19
- import type { ResolvedInfoflowAccount, InfoflowOutboundReply } from "../types.js";
20
20
 
21
21
  /** Infoflow API image size limit: 1MB raw bytes */
22
22
  const INFOFLOW_IMAGE_MAX_BYTES = 1 * 1024 * 1024;
@@ -227,7 +227,10 @@ export async function sendInfoflowGroupImage(params: {
227
227
  coreEvents.emit("message:sent", {
228
228
  accountId: account.accountId,
229
229
  target: `group:${groupId}`,
230
- from: account.config.appAgentId != null ? `agent:${account.config.appAgentId}` : "agent:unknown",
230
+ from:
231
+ account.config.appAgentId != null
232
+ ? `agent:${account.config.appAgentId}`
233
+ : "agent:unknown",
231
234
  messageid,
232
235
  msgseqid: msgseqid ?? "",
233
236
  contents: [{ type: "image", content: "image" }],
@@ -298,6 +301,14 @@ export async function sendInfoflowPrivateImage(params: {
298
301
  const data = JSON.parse(responseText) as Record<string, unknown>;
299
302
  logVerbose(`[infoflow:sendPrivateImage] response: status=${res.status}, data=${responseText}`);
300
303
 
304
+ // Check outer code first (same format as text message API)
305
+ const code = typeof data.code === "string" ? data.code : "";
306
+ if (code && code !== "ok") {
307
+ const errMsg = String(data.message ?? data.errmsg ?? `code=${code}`);
308
+ getInfoflowSendLog().error(`[infoflow:sendPrivateImage] failed: ${errMsg}`);
309
+ return { ok: false, error: errMsg };
310
+ }
311
+ // Also check inner errcode
301
312
  if (data.errcode && data.errcode !== 0) {
302
313
  const errMsg = String(data.errmsg ?? `errcode ${data.errcode}`);
303
314
  getInfoflowSendLog().error(`[infoflow:sendPrivateImage] failed: ${errMsg}`);
@@ -313,7 +324,10 @@ export async function sendInfoflowPrivateImage(params: {
313
324
  coreEvents.emit("message:sent", {
314
325
  accountId: account.accountId,
315
326
  target: toUser,
316
- from: account.config.appAgentId != null ? `agent:${account.config.appAgentId}` : "agent:unknown",
327
+ from:
328
+ account.config.appAgentId != null
329
+ ? `agent:${account.config.appAgentId}`
330
+ : "agent:unknown",
317
331
  messageid: msgkey,
318
332
  msgseqid: "",
319
333
  contents: [{ type: "image", content: "image" }],
@@ -1,14 +1,14 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
- import type { ResolvedInfoflowAccount } from "../types.js";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
4
3
  import {
5
4
  parseAndDispatchInfoflowRequest,
6
5
  loadRawBody,
7
6
  type WebhookTarget,
8
7
  } from "../adapter/inbound/webhook-parser.js";
8
+ import { InfoflowWSReceiver } from "../adapter/inbound/ws-receiver.js";
9
9
  import { getInfoflowWebhookLog, formatInfoflowError, logVerbose } from "../logging.js";
10
10
  import { getInfoflowRuntime } from "../runtime.js";
11
- import { InfoflowWSReceiver } from "../adapter/inbound/ws-receiver.js";
11
+ import type { ResolvedInfoflowAccount } from "../types.js";
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // Types
@@ -97,16 +97,21 @@ export async function handleInfoflowWebhookRequest(
97
97
 
98
98
  // Check if path matches Infoflow webhook pattern
99
99
  if (!isInfoflowPath(requestPath)) {
100
+ logVerbose(
101
+ `[infoflow] skipping: path=${requestPath} does not match any registered webhook path`,
102
+ );
100
103
  return false;
101
104
  }
102
105
 
103
106
  // Get registered targets for the actual request path
104
107
  const targets = webhookTargets.get(requestPath);
105
108
  if (!targets || targets.length === 0) {
109
+ logVerbose(`[infoflow] skipping: no registered targets for path=${requestPath}`);
106
110
  return false;
107
111
  }
108
112
 
109
113
  if (req.method !== "POST") {
114
+ logVerbose(`[infoflow] rejecting: method=${req.method} is not POST for path=${requestPath}`);
110
115
  res.statusCode = 405;
111
116
  res.setHeader("Allow", "POST");
112
117
  res.end("Method Not Allowed");