@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
@@ -0,0 +1,80 @@
1
+ /**
2
+ * /infoflow-changelog command
3
+ *
4
+ * 读取 CHANGELOG.md,展示最近几次更新内容。
5
+ *
6
+ * 用法:
7
+ * /infoflow-changelog — 最近 5 次更新
8
+ * /infoflow-changelog all — 全部更新记录
9
+ */
10
+
11
+ import { readFileSync, existsSync } from "node:fs";
12
+ import { resolve, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
15
+ import { sendInfoflowMessage } from "../channel/outbound.js";
16
+ import { getPluginVersion } from "./version.js";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const CHANGELOG_PATH = resolve(__dirname, "../../CHANGELOG.md");
20
+ const DEFAULT_SHOW = 5;
21
+
22
+ export async function runChangelogCommand(ctx: {
23
+ args?: string;
24
+ config?: OpenClawConfig;
25
+ to?: string;
26
+ accountId?: string;
27
+ }): Promise<{ text: string }> {
28
+ const { args, config, to, accountId } = ctx;
29
+ const showAll = args?.trim().toLowerCase() === "all";
30
+
31
+ if (!existsSync(CHANGELOG_PATH)) {
32
+ return { text: "**更新日志**\n\n暂无更新记录。" };
33
+ }
34
+
35
+ let content: string;
36
+ try {
37
+ content = readFileSync(CHANGELOG_PATH, "utf-8");
38
+ } catch (err: unknown) {
39
+ return {
40
+ text: `**更新日志**\n\n读取更新日志失败:${err instanceof Error ? err.message : String(err)}`,
41
+ };
42
+ }
43
+
44
+ // 去掉 HTML 注释(开发规范说明,不展示给用户)
45
+ const stripped = content.replace(/<!--[\s\S]*?-->/g, "").trim();
46
+ // 按 "## " 版本标题分割,过滤掉文件标题行(# 更新日志)和"开发中"(未发布,不对外展示)
47
+ const sections = stripped
48
+ .split(/^## /m)
49
+ .filter((s) => Boolean(s) && !s.startsWith("# ") && !s.startsWith("开发中"));
50
+
51
+ if (sections.length === 0) {
52
+ return { text: "**Infoflow 更新日志**\n\n暂无更新记录。" };
53
+ }
54
+
55
+ const version = getPluginVersion();
56
+ const title = `**Infoflow 更新日志** v${version}`;
57
+
58
+ const toShow = showAll ? sections : sections.slice(0, DEFAULT_SHOW);
59
+ const body = toShow.map((s) => `## ${s.trimEnd()}`).join("\n\n");
60
+
61
+ const hint =
62
+ !showAll && sections.length > DEFAULT_SHOW
63
+ ? `\n\n> 仅显示最近 ${toShow.length} 次更新,发送 \`/infoflow-changelog all\` 查看全部 ${sections.length} 次更新记录。`
64
+ : "";
65
+
66
+ const reportText = `${title}\n\n${body}${hint}`;
67
+
68
+ // 用 markdown 格式直接发送,保证标题/加粗等格式正常渲染
69
+ if (config && to) {
70
+ await sendInfoflowMessage({
71
+ cfg: config,
72
+ to,
73
+ contents: [{ type: "markdown", content: reportText }],
74
+ accountId: accountId ?? undefined,
75
+ });
76
+ return { text: "" };
77
+ }
78
+
79
+ return { text: reportText };
80
+ }
@@ -0,0 +1,545 @@
1
+ /**
2
+ * /infoflow-doctor command
3
+ *
4
+ * 自检命令:逐项检查 Infoflow 插件的配置和连通性,
5
+ * 帮助用户快速定位"为什么群聊/私聊收不到回复"的问题。
6
+ *
7
+ * 用法:/infoflow-doctor
8
+ */
9
+
10
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
11
+ import {
12
+ DEFAULT_INFOFLOW_API_HOST,
13
+ DEFAULT_INFOFLOW_WS_GATEWAY,
14
+ listInfoflowAccountIds,
15
+ resolveInfoflowAccount,
16
+ } from "../channel/accounts.js";
17
+ import { sendInfoflowImageMessage } from "../channel/media.js";
18
+ import { sendInfoflowMessage } from "../channel/outbound.js";
19
+ import { getOrCreateAdapter } from "../utils/token-adapter.js";
20
+ import { getPluginVersion } from "./version.js";
21
+
22
+ // 48x48 绿色圆形+白色对勾,用于发图片自检(可视化验证)
23
+ const TEST_IMAGE_BASE64 =
24
+ "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAA8klEQVR42u3ZSw6DMAwE0DkUN+kle0vaJRIkGtvx2KmKWAblASH+gLPZgZ8FHe9XJeg7PXOmg0hHRIZsipUFmYY0QalhTFBSGBZKNBPTJiCBZmRCoebRhFrN3aQDTaYYgmTvZTJAAWKWqQ5EfsUPoELNdTD0MasG5AgAiSBfOMoCuYOjFMRfCP5h8nfp3v2pJ2RdlRGNBzQaGU9j/KD74CVJlWFRz+dblXNGQfPy2f15wrRrpVaV5mgv0JjTD0HxD99+urxWCeXUeRp/kr+2NFhTl2VoNqlc/82GTftDHTtoTXuMHbuwHfvUTTv5ff915B0fHgSvRnMJWVsAAAAASUVORK5CYII=";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ type CheckResult = {
31
+ name: string;
32
+ ok: boolean;
33
+ detail: string;
34
+ skip?: boolean;
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Individual checks
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function checkAccountConfig(cfg: OpenClawConfig): CheckResult[] {
42
+ const results: CheckResult[] = [];
43
+ const accountIds = listInfoflowAccountIds(cfg);
44
+
45
+ for (const accountId of accountIds) {
46
+ const prefix = accountIds.length > 1 ? `[${accountId}] ` : "";
47
+ let account: ReturnType<typeof resolveInfoflowAccount>;
48
+ try {
49
+ account = resolveInfoflowAccount({ cfg, accountId });
50
+ } catch {
51
+ results.push({
52
+ name: `${prefix}账号解析`,
53
+ ok: false,
54
+ detail: "resolveInfoflowAccount 抛出异常",
55
+ });
56
+ continue;
57
+ }
58
+
59
+ const c = account.config;
60
+
61
+ results.push({
62
+ name: `${prefix}appKey`,
63
+ ok: !!c.appKey,
64
+ detail: c.appKey ? `已配置 (${c.appKey.slice(0, 8)}...)` : "未配置",
65
+ });
66
+ results.push({
67
+ name: `${prefix}appSecret`,
68
+ ok: !!c.appSecret,
69
+ detail: c.appSecret ? "已配置" : "未配置",
70
+ });
71
+ results.push({
72
+ name: `${prefix}apiHost`,
73
+ ok: !!c.apiHost,
74
+ detail: c.apiHost ?? "未配置(将使用默认值)",
75
+ });
76
+ results.push({
77
+ name: `${prefix}connectionMode`,
78
+ ok: true,
79
+ detail: c.connectionMode ?? "websocket(默认)",
80
+ });
81
+ if ((c.connectionMode ?? "websocket") === "websocket") {
82
+ results.push({
83
+ name: `${prefix}wsGateway`,
84
+ ok: true,
85
+ detail: c.wsGateway ?? "未配置(将使用默认值 infoflow-open-gateway.baidu.com)",
86
+ });
87
+ } else {
88
+ results.push({
89
+ name: `${prefix}checkToken`,
90
+ ok: !!c.checkToken,
91
+ detail: c.checkToken ? "已配置" : "未配置(webhook 签名验证将失败)",
92
+ });
93
+ results.push({
94
+ name: `${prefix}encodingAESKey`,
95
+ ok: !!c.encodingAESKey,
96
+ detail: c.encodingAESKey ? "已配置" : "未配置(消息解密将失败)",
97
+ });
98
+ }
99
+ results.push({
100
+ name: `${prefix}dmPolicy`,
101
+ ok: true,
102
+ detail: c.dmPolicy ?? "open(默认)",
103
+ });
104
+ // dmPolicy=allowlist 但 allowFrom 为空 → 所有私聊静默拒绝
105
+ if ((c.dmPolicy ?? "open") === "allowlist") {
106
+ const allowFrom = c.allowFrom ?? [];
107
+ results.push({
108
+ name: `${prefix}allowFrom`,
109
+ ok: allowFrom.length > 0,
110
+ detail:
111
+ allowFrom.length > 0
112
+ ? `已配置 ${allowFrom.length} 个用户`
113
+ : "未配置!dmPolicy=allowlist 时必须填写 allowFrom,否则所有私聊都将被拒绝",
114
+ });
115
+ }
116
+ results.push({
117
+ name: `${prefix}groupPolicy`,
118
+ ok: true,
119
+ detail: c.groupPolicy ?? "open(默认)",
120
+ });
121
+ if (c.groupPolicy === "disabled") {
122
+ results.push({
123
+ name: `${prefix}群聊提醒`,
124
+ ok: false,
125
+ detail: "groupPolicy=disabled,群聊消息不会被处理!如需群聊支持请设置为 open 或 allowlist",
126
+ });
127
+ }
128
+ // groupPolicy=allowlist 但 groupAllowFrom 为空 → 所有群静默拒绝
129
+ if (c.groupPolicy === "allowlist") {
130
+ const groupAllowFrom = c.groupAllowFrom ?? [];
131
+ results.push({
132
+ name: `${prefix}groupAllowFrom`,
133
+ ok: groupAllowFrom.length > 0,
134
+ detail:
135
+ groupAllowFrom.length > 0
136
+ ? `已配置 ${groupAllowFrom.length} 个群`
137
+ : "未配置!groupPolicy=allowlist 时必须填写 groupAllowFrom,否则所有群消息都将被拒绝",
138
+ });
139
+ }
140
+ results.push({
141
+ name: `${prefix}replyMode`,
142
+ ok: true,
143
+ detail: c.replyMode ?? "mention-only(默认)",
144
+ });
145
+ }
146
+
147
+ return results;
148
+ }
149
+
150
+ async function checkTokenFetch(cfg: OpenClawConfig): Promise<CheckResult[]> {
151
+ const results: CheckResult[] = [];
152
+ const accountIds = listInfoflowAccountIds(cfg);
153
+
154
+ for (const accountId of accountIds) {
155
+ const prefix = accountIds.length > 1 ? `[${accountId}] ` : "";
156
+ let account: ReturnType<typeof resolveInfoflowAccount>;
157
+ try {
158
+ account = resolveInfoflowAccount({ cfg, accountId });
159
+ } catch {
160
+ continue;
161
+ }
162
+ if (!account.config.appKey || !account.config.appSecret) {
163
+ results.push({
164
+ name: `${prefix}access_token 获取`,
165
+ ok: false,
166
+ detail: "appKey/appSecret 未配置,跳过",
167
+ });
168
+ continue;
169
+ }
170
+
171
+ try {
172
+ const adapter = getOrCreateAdapter({
173
+ appKey: account.config.appKey,
174
+ appSecret: account.config.appSecret,
175
+ apiHost: account.config.apiHost ?? DEFAULT_INFOFLOW_API_HOST,
176
+ });
177
+ const token = await adapter.getToken();
178
+ results.push({
179
+ name: `${prefix}access_token 获取`,
180
+ ok: !!token,
181
+ detail: token ? `成功 (${String(token).slice(0, 12)}...)` : "返回为空",
182
+ });
183
+ } catch (err: any) {
184
+ results.push({
185
+ name: `${prefix}access_token 获取`,
186
+ ok: false,
187
+ detail: `失败: ${err?.message ?? String(err)}`,
188
+ });
189
+ }
190
+ }
191
+
192
+ return results;
193
+ }
194
+
195
+ const WS_CONNECT_TIMEOUT_MS = 8000;
196
+
197
+ async function checkWsConnectivity(cfg: OpenClawConfig): Promise<CheckResult[]> {
198
+ const results: CheckResult[] = [];
199
+ const accountIds = listInfoflowAccountIds(cfg);
200
+
201
+ for (const accountId of accountIds) {
202
+ const prefix = accountIds.length > 1 ? `[${accountId}] ` : "";
203
+ let account: ReturnType<typeof resolveInfoflowAccount>;
204
+ try {
205
+ account = resolveInfoflowAccount({ cfg, accountId });
206
+ } catch {
207
+ continue;
208
+ }
209
+
210
+ const c = account.config;
211
+ if ((c.connectionMode ?? "websocket") !== "websocket") continue;
212
+ if (!c.appKey || !c.appSecret) {
213
+ results.push({
214
+ name: `${prefix}WebSocket 连通性`,
215
+ ok: false,
216
+ detail: "appKey/appSecret 未配置,跳过",
217
+ });
218
+ continue;
219
+ }
220
+
221
+ const wsGateway = c.wsGateway ?? DEFAULT_INFOFLOW_WS_GATEWAY;
222
+
223
+ try {
224
+ const { WSClient } = await import("@core-workspace/infoflow-sdk-nodejs");
225
+ const client = new WSClient({ appId: c.appKey, appSecret: c.appSecret, wsGateway });
226
+
227
+ await new Promise<void>((resolve, reject) => {
228
+ const timer = setTimeout(() => {
229
+ reject(new Error(`连接超时(>${WS_CONNECT_TIMEOUT_MS}ms),网关: ${wsGateway}`));
230
+ }, WS_CONNECT_TIMEOUT_MS);
231
+
232
+ client.on("connected" as any, () => {
233
+ clearTimeout(timer);
234
+ try {
235
+ (client as any).disconnect?.();
236
+ } catch {
237
+ /* ignore */
238
+ }
239
+ resolve();
240
+ });
241
+ client.on("error" as any, (err: any) => {
242
+ clearTimeout(timer);
243
+ reject(new Error(err?.message ?? String(err)));
244
+ });
245
+
246
+ client.connect().catch((err: any) => {
247
+ clearTimeout(timer);
248
+ reject(err);
249
+ });
250
+ });
251
+
252
+ results.push({
253
+ name: `${prefix}WebSocket 连通性`,
254
+ ok: true,
255
+ detail: `已连接,网关: ${wsGateway}`,
256
+ });
257
+ } catch (err: any) {
258
+ results.push({
259
+ name: `${prefix}WebSocket 连通性`,
260
+ ok: false,
261
+ detail: `失败: ${err?.message ?? String(err)}`,
262
+ });
263
+ }
264
+ }
265
+
266
+ return results;
267
+ }
268
+
269
+ async function checkSendMessage(cfg: OpenClawConfig, senderId: string): Promise<CheckResult[]> {
270
+ const results: CheckResult[] = [];
271
+ if (!senderId) {
272
+ results.push({ name: "发消息自检", ok: false, detail: "无法确定发送目标(senderId 为空)" });
273
+ return results;
274
+ }
275
+
276
+ const accountIds = listInfoflowAccountIds(cfg);
277
+ for (const accountId of accountIds) {
278
+ const prefix = accountIds.length > 1 ? `[${accountId}] ` : "";
279
+ let account: ReturnType<typeof resolveInfoflowAccount>;
280
+ try {
281
+ account = resolveInfoflowAccount({ cfg, accountId });
282
+ } catch {
283
+ continue;
284
+ }
285
+ if (!account.config.appKey || !account.config.appSecret) continue;
286
+
287
+ try {
288
+ const result = await sendInfoflowMessage({
289
+ cfg,
290
+ to: senderId,
291
+ contents: [{ type: "text", content: "✅ [infoflow-doctor] 发消息自检通过" }],
292
+ accountId: account.accountId,
293
+ });
294
+ results.push({
295
+ name: `${prefix}发消息到 ${senderId}`,
296
+ ok: result.ok,
297
+ detail: result.ok
298
+ ? `成功 (messageId: ${result.messageId ?? "N/A"})`
299
+ : `失败: ${result.error ?? "unknown"}`,
300
+ });
301
+ } catch (err: any) {
302
+ results.push({
303
+ name: `${prefix}发消息到 ${senderId}`,
304
+ ok: false,
305
+ detail: `异常: ${err?.message ?? String(err)}`,
306
+ });
307
+ }
308
+ }
309
+
310
+ return results;
311
+ }
312
+
313
+ async function checkSendImage(cfg: OpenClawConfig, to: string): Promise<CheckResult[]> {
314
+ const results: CheckResult[] = [];
315
+ const accountIds = listInfoflowAccountIds(cfg);
316
+
317
+ for (const accountId of accountIds) {
318
+ const prefix = accountIds.length > 1 ? `[${accountId}] ` : "";
319
+ let account: ReturnType<typeof resolveInfoflowAccount>;
320
+ try {
321
+ account = resolveInfoflowAccount({ cfg, accountId });
322
+ } catch {
323
+ continue;
324
+ }
325
+ if (!account.config.appKey || !account.config.appSecret) continue;
326
+
327
+ const isGroup = to.includes(":group:");
328
+ const target = to.replace(/^.*?:group:/, "group:");
329
+ const label = isGroup ? `群 ${target}` : to;
330
+
331
+ try {
332
+ const result = await sendInfoflowImageMessage({
333
+ cfg,
334
+ to: target,
335
+ base64Image: TEST_IMAGE_BASE64,
336
+ accountId: account.accountId,
337
+ });
338
+ results.push({
339
+ name: `${prefix}发图片到 ${label}`,
340
+ ok: result.ok,
341
+ detail: result.ok
342
+ ? `成功 (messageId: ${result.messageId ?? "N/A"})`
343
+ : `失败: ${result.error ?? "unknown"}`,
344
+ });
345
+ } catch (err: any) {
346
+ results.push({
347
+ name: `${prefix}发图片到 ${label}`,
348
+ ok: false,
349
+ detail: `异常: ${err?.message ?? String(err)}`,
350
+ });
351
+ }
352
+ }
353
+
354
+ return results;
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Format output
359
+ // ---------------------------------------------------------------------------
360
+
361
+ // 将技术字段名映射为用户友好的中文描述
362
+ const FIELD_LABELS: Record<string, string> = {
363
+ appKey: "应用 Key",
364
+ appSecret: "应用密钥",
365
+ apiHost: "API 地址",
366
+ connectionMode: "连接方式",
367
+ wsGateway: "WebSocket 网关地址",
368
+ checkToken: "Webhook 验签 Token",
369
+ encodingAESKey: "消息加密密钥",
370
+ dmPolicy: "私聊权限策略",
371
+ allowFrom: "私聊白名单",
372
+ groupPolicy: "群聊权限策略",
373
+ groupAllowFrom: "群聊白名单",
374
+ replyMode: "群聊回复模式",
375
+ 群聊提醒: "群聊权限警告",
376
+ 账号解析: "账号配置读取",
377
+ "access_token 获取": "API 登录验证",
378
+ "WebSocket 连通性": "WebSocket 连通性",
379
+ 发消息自检: "发消息测试",
380
+ };
381
+
382
+ function friendlyName(name: string): string {
383
+ // 处理带账号前缀的名字,如 "[main] appKey"
384
+ const match = name.match(/^(\[.+?\] )(.+)$/);
385
+ if (match) {
386
+ const prefix = match[1];
387
+ const field = match[2];
388
+ return `${prefix}${FIELD_LABELS[field] ?? field}`;
389
+ }
390
+ // 处理 "发消息到 xxx" / "发图片到 xxx"
391
+ if (name.startsWith("发消息到 ") || name.startsWith("发图片到 ")) return name;
392
+ return FIELD_LABELS[name] ?? name;
393
+ }
394
+
395
+ function friendlyDetail(name: string, detail: string): string {
396
+ // 连接方式特殊处理
397
+ if (name.endsWith("connectionMode")) {
398
+ if (detail.includes("websocket")) return "WebSocket 长连接(无需公网域名)";
399
+ return "Webhook 回调(需配置公网地址)";
400
+ }
401
+ // dmPolicy
402
+ if (name.endsWith("dmPolicy")) {
403
+ if (detail.includes("open")) return "开放(任何人可私聊)";
404
+ if (detail.includes("allowlist")) return "白名单(仅允许名单内用户)";
405
+ if (detail.includes("pairing")) return "配对授权";
406
+ return detail;
407
+ }
408
+ // groupPolicy
409
+ if (name.endsWith("groupPolicy")) {
410
+ if (detail.includes("disabled")) return "已关闭群聊(所有群消息不处理)";
411
+ if (detail.includes("allowlist")) return "白名单(仅允许名单内的群)";
412
+ if (detail.includes("open")) return "开放(所有群可触发)";
413
+ return detail;
414
+ }
415
+ // replyMode
416
+ if (name.endsWith("replyMode")) {
417
+ if (detail.includes("mention-only")) return "仅响应 @机器人(默认)";
418
+ if (detail.includes("mention-and-watch")) return "响应 @机器人 + 关键词监听";
419
+ if (detail.includes("proactive")) return "主动响应所有群消息";
420
+ if (detail.includes("record")) return "仅记录,不回复";
421
+ if (detail.includes("ignore")) return "完全忽略群消息";
422
+ return detail;
423
+ }
424
+ // wsGateway 默认值
425
+ if (detail.includes("infoflow-open-gateway.baidu.com")) {
426
+ return detail.includes("未配置") ? "使用默认网关" : detail;
427
+ }
428
+ // apiHost 默认值
429
+ if (detail.includes("将使用默认值")) return "使用默认地址";
430
+ // groupPolicy=disabled 警告
431
+ if (detail.includes("groupPolicy=disabled")) {
432
+ return "群聊功能已关闭,机器人不会响应任何群消息。如需开启,请将 groupPolicy 设置为 open";
433
+ }
434
+ return detail;
435
+ }
436
+
437
+ function formatResults(results: CheckResult[]): string {
438
+ const lines: string[] = [`**如流机器人自检报告** v${getPluginVersion()}`, ""];
439
+
440
+ const failed = results.filter((r) => !r.ok && !r.skip);
441
+ const checked = results.filter((r) => !r.skip);
442
+
443
+ for (const r of results) {
444
+ const icon = r.skip ? "⏭️" : r.ok ? "✅" : "❌";
445
+ const label = friendlyName(r.name);
446
+ const detail = friendlyDetail(r.name, r.detail);
447
+ lines.push(`${icon} **${label}**:${detail}`);
448
+ }
449
+
450
+ lines.push("");
451
+ lines.push("---");
452
+
453
+ if (failed.length === 0) {
454
+ lines.push(`✅ 自检全部通过(共 ${checked.length} 项),机器人运行正常!`);
455
+ } else {
456
+ lines.push(`⚠️ 发现 ${failed.length} 个问题,请根据上方提示修复后重试`);
457
+ }
458
+
459
+ lines.push("");
460
+ lines.push("📋 **排查工具**");
461
+ lines.push("- `/infoflow-logs` — 查看最近 50 条收发消息日志");
462
+ lines.push("- `/infoflow-logs 100` — 查看最近 100 条");
463
+ lines.push("- `/infoflow-logs error` — 只看错误日志");
464
+ lines.push("- `/infoflow-logs warn 100` — 最近 100 条警告及以上");
465
+ lines.push("- `/infoflow-changelog` — 查看插件更新记录");
466
+
467
+ return lines.join("\n");
468
+ }
469
+
470
+ // ---------------------------------------------------------------------------
471
+ // Command handler
472
+ // ---------------------------------------------------------------------------
473
+
474
+ export async function runDoctorCommand(ctx: {
475
+ config: OpenClawConfig;
476
+ senderId?: string;
477
+ /** 消息目标:群聊时为 "group:<groupId>",私聊时与 senderId 相同 */
478
+ to?: string;
479
+ /** 账号 ID,用于多账号模式下选择发送账号 */
480
+ accountId?: string;
481
+ /** 命令参数,支持 /infoflow-doctor <userId> 指定发送目标 */
482
+ args?: string;
483
+ }): Promise<{ text: string }> {
484
+ const { config, senderId, to, accountId, args } = ctx;
485
+ const results: CheckResult[] = [];
486
+
487
+ // 1. 配置检查(同步)
488
+ results.push(...checkAccountConfig(config));
489
+
490
+ // 2. Token 获取(异步)
491
+ results.push(...(await checkTokenFetch(config)));
492
+
493
+ // 3. WebSocket 连通性(仅 websocket 模式)
494
+ results.push(...(await checkWsConnectivity(config)));
495
+
496
+ // 网页 UI 触发时 senderId 为系统账号,无法发消息,除非通过参数指定了目标
497
+ const isWebUI =
498
+ !senderId || senderId === "openclaw-control-ui" || senderId.startsWith("openclaw-");
499
+ const argTarget = args?.trim() || undefined;
500
+ const realSenderId = isWebUI ? argTarget : senderId;
501
+
502
+ if (isWebUI && !argTarget) {
503
+ results.push({
504
+ name: "发消息自检",
505
+ ok: true,
506
+ skip: true,
507
+ detail: "网页 UI 触发,跳过发送自检(可用 /infoflow-doctor <userId> 指定接收人)",
508
+ });
509
+ }
510
+
511
+ // 3. 发私聊消息自检(发给触发命令的用户)
512
+ if (realSenderId) {
513
+ results.push(...(await checkSendMessage(config, realSenderId)));
514
+ }
515
+
516
+ // 4. 发群消息自检(仅群聊中执行命令时)
517
+ const groupTo = to?.includes(":group:") ? to.replace(/^.*?:group:/, "group:") : undefined;
518
+ if (groupTo) {
519
+ results.push(...(await checkSendMessage(config, groupTo)));
520
+ }
521
+
522
+ // 5. 发图片自检(群聊发到群,私聊发给用户)
523
+ const imageTarget = groupTo ?? realSenderId;
524
+ if (imageTarget) {
525
+ results.push(...(await checkSendImage(config, imageTarget)));
526
+ }
527
+
528
+ const reportText = formatResults(results);
529
+
530
+ // 自检报告用 markdown 格式直接发送,保证加粗/表情符号等格式正常渲染
531
+ const replyTarget = groupTo ?? realSenderId;
532
+ if (replyTarget) {
533
+ await sendInfoflowMessage({
534
+ cfg: config,
535
+ to: replyTarget,
536
+ contents: [{ type: "markdown", content: reportText }],
537
+ accountId: accountId ?? undefined,
538
+ });
539
+ // 报告已自行发出,返回空 text 避免框架重复发送
540
+ return { text: "" };
541
+ }
542
+
543
+ // 无法确定发送目标(Web UI 场景),降级为纯文本回传给框架
544
+ return { text: reportText };
545
+ }