@marshulll/openclaw-wecom 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -99,3 +99,5 @@ Install guide: `docs/INSTALL.md`
99
99
  - Dev doc: `docs/TECHNICAL.md`
100
100
  - Install: `docs/INSTALL.md`
101
101
  - Examples: `docs/wecom.config.example.json` / `docs/wecom.config.full.example.json`
102
+
103
+ Recommendation: use **separate webhookPath** for Bot and App (e.g. `/wecom/bot` and `/wecom/app`) for clearer debugging and fewer callback mix-ups.
package/README.md CHANGED
@@ -39,6 +39,8 @@ openclaw gateway restart
39
39
  全量示例:`docs/wecom.config.full.example.json`
40
40
  安装与配置说明:`docs/INSTALL.md`
41
41
 
42
+ 建议:Bot 与 App 使用**不同的 webhookPath**(如 `/wecom/bot` 与 `/wecom/app`),便于排障并避免回调混淆。
43
+
42
44
  ### 最小配置示例
43
45
  ```json5
44
46
  {
package/README.zh.md CHANGED
@@ -39,6 +39,8 @@ openclaw gateway restart
39
39
  全量示例:`docs/wecom.config.full.example.json`
40
40
  安装与配置说明:`docs/INSTALL.md`
41
41
 
42
+ 建议:Bot 与 App 使用**不同的 webhookPath**(如 `/wecom/bot` 与 `/wecom/app`),便于排障并避免回调混淆。
43
+
42
44
  ### 最小配置示例
43
45
  ```json5
44
46
  {
package/docs/INSTALL.md CHANGED
@@ -50,6 +50,37 @@ openclaw gateway restart
50
50
  }
51
51
  ```
52
52
 
53
+ 推荐示例(Bot/App 独立路径):
54
+ ```json5
55
+ {
56
+ "channels": {
57
+ "wecom": {
58
+ "enabled": true,
59
+ "mode": "both",
60
+ "defaultAccount": "bot",
61
+ "accounts": {
62
+ "bot": {
63
+ "mode": "bot",
64
+ "webhookPath": "/wecom/bot",
65
+ "token": "BOT_TOKEN",
66
+ "encodingAESKey": "BOT_AES",
67
+ "receiveId": "BOT_ID"
68
+ },
69
+ "app": {
70
+ "mode": "app",
71
+ "webhookPath": "/wecom/app",
72
+ "corpId": "CORP_ID",
73
+ "corpSecret": "CORP_SECRET",
74
+ "agentId": 1000001,
75
+ "callbackToken": "CALLBACK_TOKEN",
76
+ "callbackAesKey": "CALLBACK_AES"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
53
84
  ## 环境变量示例(可选)
54
85
 
55
86
  如果你更希望用环境变量,也可以写在 `~/.openclaw/openclaw.json` 的 `env.vars` 中(优先级低于 `channels.wecom` 配置):
@@ -99,6 +130,7 @@ openclaw gateway restart
99
130
  ## Webhook 验证
100
131
  - Bot 模式与 App 模式都要求公网 HTTPS。
101
132
  - 在企业微信后台配置回调 URL。
133
+ - 建议 Bot 与 App 使用不同 `webhookPath`,便于排障与避免回调混淆。
102
134
 
103
135
  ## 常见问题
104
136
  - 回调验证失败:检查 Token / AESKey / URL 是否一致
@@ -3,17 +3,6 @@
3
3
  "wecom": {
4
4
  "enabled": true,
5
5
  "mode": "both",
6
- "webhookPath": "/wecom",
7
-
8
- "token": "BOT_TOKEN",
9
- "encodingAESKey": "BOT_AES",
10
- "receiveId": "BOT_ID",
11
-
12
- "corpId": "CORP_ID",
13
- "corpSecret": "CORP_SECRET",
14
- "agentId": 1000001,
15
- "callbackToken": "CALLBACK_TOKEN",
16
- "callbackAesKey": "CALLBACK_AES",
17
6
 
18
7
  "media": {
19
8
  "tempDir": "/tmp/openclaw-wecom",
@@ -28,26 +17,23 @@
28
17
  "allowFrom": []
29
18
  },
30
19
 
31
- "defaultAccount": "default",
20
+ "defaultAccount": "bot",
32
21
  "accounts": {
33
- "default": {
34
- "mode": "both",
35
- "webhookPath": "/wecom",
22
+ "bot": {
23
+ "mode": "bot",
24
+ "webhookPath": "/wecom/bot",
36
25
  "token": "BOT_TOKEN",
37
26
  "encodingAESKey": "BOT_AES",
38
- "receiveId": "BOT_ID",
27
+ "receiveId": "BOT_ID"
28
+ },
29
+ "app": {
30
+ "mode": "app",
31
+ "webhookPath": "/wecom/app",
39
32
  "corpId": "CORP_ID",
40
33
  "corpSecret": "CORP_SECRET",
41
34
  "agentId": 1000001,
42
35
  "callbackToken": "CALLBACK_TOKEN",
43
- "callbackAesKey": "CALLBACK_AES",
44
- "media": {
45
- "tempDir": "/tmp/openclaw-wecom",
46
- "retentionHours": 72,
47
- "cleanupOnStart": true,
48
- "maxBytes": 10485760
49
- },
50
- "botMediaBridge": true
36
+ "callbackAesKey": "CALLBACK_AES"
51
37
  }
52
38
  }
53
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshulll/openclaw-wecom",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "description": "OpenClaw WeCom channel plugin (intelligent bot + internal app)",
6
6
  "author": "OpenClaw",
@@ -31,6 +31,17 @@ type StreamState = {
31
31
  content: string;
32
32
  };
33
33
 
34
+ type InboundMedia = {
35
+ path: string;
36
+ type: string;
37
+ url?: string;
38
+ };
39
+
40
+ type InboundBody = {
41
+ text: string;
42
+ media?: InboundMedia;
43
+ };
44
+
34
45
  const streams = new Map<string, StreamState>();
35
46
  const msgidToStreamId = new Map<string, string>();
36
47
  const recentEncrypts = new Map<string, { ts: number; streamId?: string }>();
@@ -320,7 +331,8 @@ async function startAgentForStream(params: {
320
331
  const userid = msg.from?.userid?.trim() || "unknown";
321
332
  const chatType = msg.chattype === "group" ? "group" : "direct";
322
333
  const chatId = msg.chattype === "group" ? (msg.chatid?.trim() || "unknown") : userid;
323
- const rawBody = await buildInboundBody({ target, msg });
334
+ const inbound = await buildInboundBody({ target, msg });
335
+ const rawBody = inbound.text;
324
336
 
325
337
  const route = core.channel.routing.resolveAgentRoute({
326
338
  cfg: config,
@@ -367,6 +379,14 @@ async function startAgentForStream(params: {
367
379
  OriginatingTo: `wecom:${chatId}`,
368
380
  });
369
381
 
382
+ if (inbound.media) {
383
+ ctxPayload.MediaPath = inbound.media.path;
384
+ ctxPayload.MediaType = inbound.media.type;
385
+ if (inbound.media.url) {
386
+ ctxPayload.MediaUrl = inbound.media.url;
387
+ }
388
+ }
389
+
370
390
  await core.channel.session.recordInboundSession({
371
391
  storePath,
372
392
  sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
@@ -550,7 +570,7 @@ async function buildBotMediaMessage(params: {
550
570
  url?: string;
551
571
  base64?: string;
552
572
  filename?: string;
553
- }): Promise<string> {
573
+ }): Promise<InboundBody> {
554
574
  const { target, msgtype, url, base64, filename } = params;
555
575
 
556
576
  const fallbackLabel = msgtype === "image"
@@ -561,28 +581,31 @@ async function buildBotMediaMessage(params: {
561
581
  ? "[video]"
562
582
  : "[file]";
563
583
 
564
- if (!url && !base64) return fallbackLabel;
584
+ if (!url && !base64) return { text: fallbackLabel };
565
585
 
566
586
  try {
567
587
  let buffer: Buffer | null = null;
568
588
  let contentType = "";
569
589
  if (base64) {
570
590
  buffer = Buffer.from(base64, "base64");
571
- contentType = msgtype === "image" ? "image/jpeg" : "application/octet-stream";
591
+ if (msgtype === "image") contentType = "image/jpeg";
592
+ else if (msgtype === "voice") contentType = "audio/amr";
593
+ else if (msgtype === "video") contentType = "video/mp4";
594
+ else contentType = "application/octet-stream";
572
595
  } else if (url) {
573
596
  const media = await fetchMediaFromUrl(url, target.account);
574
597
  buffer = media.buffer;
575
598
  contentType = media.contentType;
576
599
  }
577
600
 
578
- if (!buffer) return fallbackLabel;
601
+ if (!buffer) return { text: fallbackLabel };
579
602
 
580
603
  const maxBytes = resolveMediaMaxBytes(target);
581
604
  if (maxBytes && buffer.length > maxBytes) {
582
- if (msgtype === "image") return "[图片过大,未处理]\n\n请发送更小的图片。";
583
- if (msgtype === "voice") return "[语音消息过大,未处理]\n\n请发送更短的语音消息。";
584
- if (msgtype === "video") return "[视频过大,未处理]\n\n请发送更小的视频。";
585
- return "[文件过大,未处理]\n\n请发送更小的文件。";
605
+ if (msgtype === "image") return { text: "[图片过大,未处理]\n\n请发送更小的图片。" };
606
+ if (msgtype === "voice") return { text: "[语音消息过大,未处理]\n\n请发送更短的语音消息。" };
607
+ if (msgtype === "video") return { text: "[视频过大,未处理]\n\n请发送更小的视频。" };
608
+ return { text: "[文件过大,未处理]\n\n请发送更小的文件。" };
586
609
  }
587
610
 
588
611
  const tempDir = resolveMediaTempDir(target);
@@ -606,7 +629,10 @@ async function buildBotMediaMessage(params: {
606
629
  const safeName = sanitizeFilename(filename || "", `file-${Date.now()}.${ext}`);
607
630
  const tempFilePath = join(tempDir, safeName);
608
631
  await writeFile(tempFilePath, buffer);
609
- return `[用户发送了一个文件: ${safeName},已保存到: ${tempFilePath}]\n\n请根据文件内容回复用户。`;
632
+ return {
633
+ text: `[用户发送了一个文件: ${safeName},已保存到: ${tempFilePath}]\n\n请根据文件内容回复用户。`,
634
+ media: { path: tempFilePath, type: contentType || "application/octet-stream", url },
635
+ };
610
636
  }
611
637
 
612
638
  const tempPath = join(
@@ -616,34 +642,43 @@ async function buildBotMediaMessage(params: {
616
642
  await writeFile(tempPath, buffer);
617
643
 
618
644
  if (msgtype === "image") {
619
- return `[用户发送了一张图片,已保存到: ${tempPath}]\n\n请使用 Read 工具查看这张图片并描述内容。`;
645
+ return {
646
+ text: `[用户发送了一张图片,已保存到: ${tempPath}]\n\n请使用 Read 工具查看这张图片并描述内容。`,
647
+ media: { path: tempPath, type: contentType || "image/jpeg", url },
648
+ };
620
649
  }
621
650
  if (msgtype === "voice") {
622
- return `[用户发送了一条语音消息,已保存到: ${tempPath}]\n\n请根据语音内容回复用户。`;
651
+ return {
652
+ text: `[用户发送了一条语音消息,已保存到: ${tempPath}]\n\n请根据语音内容回复用户。`,
653
+ media: { path: tempPath, type: contentType || "audio/amr", url },
654
+ };
623
655
  }
624
656
  if (msgtype === "video") {
625
- return `[用户发送了一个视频文件,已保存到: ${tempPath}]\n\n请根据视频内容回复用户。`;
657
+ return {
658
+ text: `[用户发送了一个视频文件,已保存到: ${tempPath}]\n\n请根据视频内容回复用户。`,
659
+ media: { path: tempPath, type: contentType || "video/mp4", url },
660
+ };
626
661
  }
627
- return fallbackLabel;
662
+ return { text: fallbackLabel };
628
663
  } catch (err) {
629
664
  target.runtime.error?.(`wecom bot ${msgtype} download failed: ${String(err)}`);
630
- if (msgtype === "image") return "[用户发送了一张图片,但下载失败]\n\n请告诉用户图片处理暂时不可用。";
631
- if (msgtype === "voice") return "[用户发送了一条语音消息,但下载失败]\n\n请告诉用户语音处理暂时不可用。";
632
- if (msgtype === "video") return "[用户发送了一个视频,但下载失败]\n\n请告诉用户视频处理暂时不可用。";
633
- return "[用户发送了一个文件,但下载失败]\n\n请告诉用户文件处理暂时不可用。";
665
+ if (msgtype === "image") return { text: "[用户发送了一张图片,但下载失败]\n\n请告诉用户图片处理暂时不可用。" };
666
+ if (msgtype === "voice") return { text: "[用户发送了一条语音消息,但下载失败]\n\n请告诉用户语音处理暂时不可用。" };
667
+ if (msgtype === "video") return { text: "[用户发送了一个视频,但下载失败]\n\n请告诉用户视频处理暂时不可用。" };
668
+ return { text: "[用户发送了一个文件,但下载失败]\n\n请告诉用户文件处理暂时不可用。" };
634
669
  }
635
670
  }
636
671
 
637
- async function buildInboundBody(params: { target: WecomWebhookTarget; msg: WecomInboundMessage }): Promise<string> {
672
+ async function buildInboundBody(params: { target: WecomWebhookTarget; msg: WecomInboundMessage }): Promise<InboundBody> {
638
673
  const { target, msg } = params;
639
674
  const msgtype = String(msg.msgtype ?? "").toLowerCase();
640
675
  if (msgtype === "text") {
641
676
  const content = (msg as any).text?.content;
642
- return typeof content === "string" ? content : "";
677
+ return { text: typeof content === "string" ? content : "" };
643
678
  }
644
679
  if (msgtype === "voice") {
645
680
  const content = (msg as any).voice?.content;
646
- if (typeof content === "string" && content.trim()) return content;
681
+ if (typeof content === "string" && content.trim()) return { text: content.trim() };
647
682
  const url = resolveBotMediaUrl(msg as any, "voice");
648
683
  const base64 = resolveBotMediaBase64(msg as any, "voice");
649
684
  return await buildBotMediaMessage({ target, msgtype: "voice", url, base64 });
@@ -651,7 +686,7 @@ async function buildInboundBody(params: { target: WecomWebhookTarget; msg: Wecom
651
686
  if (msgtype === "mixed") {
652
687
  const items = (msg as any).mixed?.msg_item;
653
688
  if (Array.isArray(items)) {
654
- return items
689
+ const text = items
655
690
  .map((item: any) => {
656
691
  const t = String(item?.msgtype ?? "").toLowerCase();
657
692
  if (t === "text") return String(item?.text?.content ?? "");
@@ -660,8 +695,9 @@ async function buildInboundBody(params: { target: WecomWebhookTarget; msg: Wecom
660
695
  })
661
696
  .filter((part: string) => Boolean(part && part.trim()))
662
697
  .join("\n");
698
+ return { text };
663
699
  }
664
- return "[mixed]";
700
+ return { text: "[mixed]" };
665
701
  }
666
702
  if (msgtype === "image") {
667
703
  const url = resolveBotMediaUrl(msg as any, "image");
@@ -681,13 +717,13 @@ async function buildInboundBody(params: { target: WecomWebhookTarget; msg: Wecom
681
717
  }
682
718
  if (msgtype === "event") {
683
719
  const eventtype = String((msg as any).event?.eventtype ?? "").trim();
684
- return eventtype ? `[event] ${eventtype}` : "[event]";
720
+ return { text: eventtype ? `[event] ${eventtype}` : "[event]" };
685
721
  }
686
722
  if (msgtype === "stream") {
687
723
  const id = String((msg as any).stream?.id ?? "").trim();
688
- return id ? `[stream_refresh] ${id}` : "[stream_refresh]";
724
+ return { text: id ? `[stream_refresh] ${id}` : "[stream_refresh]" };
689
725
  }
690
- return msgtype ? `[${msgtype}]` : "";
726
+ return { text: msgtype ? `[${msgtype}]` : "" };
691
727
  }
692
728
 
693
729
  function normalizeMediaType(raw?: string): "image" | "voice" | "video" | "file" | null {