@yanhaidao/wecom 2.3.150 → 2.3.160

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.md CHANGED
@@ -17,6 +17,10 @@
17
17
  <strong>🚀 深度适配企业微信原生文档(WeCom Doc):将对话沉淀为企业数字资产,并补齐写入稳定性 [v2.3.15]</strong>
18
18
  </p>
19
19
 
20
+ <p align="center">
21
+ <strong>🆕 Bot WS 混合消息附件解析修复:图片和文字一起发时,AI 终于能看到真实媒体内容 [v2.3.16]</strong>
22
+ </p>
23
+
20
24
  <p align="center">
21
25
  <a href="#sec-1">💡 核心价值</a> •
22
26
  <a href="#sec-2">📊 模式对比</a> •
@@ -72,6 +76,7 @@
72
76
 
73
77
  #### 📎 **全模态支持 (Multi-Modal)**
74
78
  * **发什么都能看**:支持接收图片、文件 (PDF/Doc/Zip)、语音 (自动转文字)、视频。
79
+ * **混合消息也不丢附件**:从 `v2.3.16` 起,`Bot WS` 可正确解析“图片/文件 + 文本”混合消息,AI 不再只看到腾讯 COS 临时签名链接文本。
75
80
  * **要什么都能给**:AI 生成的图表、代码文件、语音回复,均可自动上传并推送到企微。
76
81
 
77
82
  #### 📝 **深度适配企业微信“协作文档” (WeCom Doc)**
@@ -114,6 +119,16 @@
114
119
 
115
120
  > 项目保持高频迭代,核心改进一览:
116
121
 
122
+ #### v2.3.16(2026-03-16)
123
+
124
+ - 🛠 **[重要修复]** 补齐 `Bot WS` 对 `mixed` 结构消息的附件提取逻辑。现在用户发送“图片/文件 + 文本”的混合消息时,插件会自动遍历媒体节点并提取 URL 与 `aeskey`,确保核心处理链路能正常下载、解密并交给 AI 分析真实媒体内容。
125
+ - 🖼 修复此前 AI 只能读到腾讯 COS 临时签名链接文本、无法真正查看图片本体的问题,尤其适合“发一张截图再补一句说明”的常见企业微信使用场景。
126
+
127
+ **升级指引:**
128
+ ```bash
129
+ openclaw plugins update wecom
130
+ ```
131
+
117
132
  #### v2.3.150(2026-03-15)
118
133
 
119
134
  - 🛠 **[重要修复]** 创建企微文档时,`init_content` 现在会按官方 Wedoc 流程执行,图片会先上传再插入,减少标题正文错位、图片不显示、内容插入到错误位置的问题。
@@ -463,6 +478,8 @@ Agent 输出 `{"template_card": ...}` 时自动渲染为交互卡片:
463
478
  ### 4.4 📝 Docs 极客级协作资产管控
464
479
  从 v2.3.14 起,OpenClaw WeCom 插件已深度集成企微原生协作文档;在 v2.3.15 中,又重点补上了 `init_content`、图片插入、批量更新索引与群聊目标解析的稳定性问题。现在你既可以让 AI 建档,也更适合直接拿来做真实写入与协作:
465
480
 
481
+ 另外,从 v2.3.16 起,`Bot WS` 对“图片/文件 + 说明文字”这类混合消息的附件解析也已补齐,更适合把截图、报表和文字说明一起发给 AI 做联合分析。
482
+
466
483
  **【典型赋能场景】**
467
484
  1. **自动化建档**:对机器人说:“建一个名为『Q1需求追踪』的表格,并把群里的人都加上可写权限。”它将自动调用 `create_doc` 并生成带权限的企微链接。
468
485
  2. **移动端碎片修改**:在地铁上吩咐:“把「第二周面试记录」里的 B2 到 B5 单元格全部更新为‘二面通过’。” AI 会精准直击数据块,不干扰他人协同(`spreadsheet.edit_data`)。
@@ -0,0 +1,11 @@
1
+ # OpenClaw WeCom 插件 v2.3.16 变更简报
2
+
3
+ > [!TIP]
4
+ > **企业微信 Bot-WS 混合消息附件解析修复版本**:`v2.3.16` 重点解决了在 WebSocket 模式下,企业微信机器人接收到的混合消息(如同时包含图片和文字)由于解析遗漏导致附件丢失、AI 只能看到带签名的临时链接文本而无法查看真正图片内容的问题。
5
+
6
+ ## 2026-03-16(v2.3.16)
7
+ - 【混合消息媒体解析修复】🛠 **[重要修复]** 补齐了 Bot WebSocket 传输通道(`bot-ws`)下对 `mixed` 结构消息的附件提取逻辑。现在,当用户在企微发出一条包含图片/文件与文字的混合消息时,底层框架会自动遍历并提取各个媒体节点的 URL 和 AES Key,确保核心处理管线能进行正常下载与安全解密。
8
+
9
+ ## 升级提示
10
+ - 执行 `openclaw plugins update wecom` 即可升级到 `v2.3.16`。
11
+ - 如果你之前在企微发出“带图的一段话”时,曾遇到 AI 回复说“这是一个腾讯 COS 的临时签名链接”,本次升级后,此问题将被修复,AI 将可以直接分析图片内容本身。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanhaidao/wecom",
3
- "version": "2.3.150",
3
+ "version": "2.3.160",
4
4
  "type": "module",
5
5
  "description": "OpenClaw 企业微信(WeCom)插件,默认 Bot WebSocket,支持加密媒体解密、Agent 主动发消息与多账号接入",
6
6
  "repository": {
package/src/target.ts CHANGED
@@ -39,14 +39,14 @@ export interface ScopedWecomTarget {
39
39
  * - 纯数字 -> 默认 User ID (用户),避免误判部门导致 81013 错误
40
40
  * - 其他 -> User ID (用户)
41
41
  *
42
- * @param raw - The raw target string (e.g. "party:1", "zhangsan", "wecom:user:0404777")
42
+ * @param raw - The raw target string (e.g. "party:1", "zhangsan", "wecom:user:xxx")
43
43
  */
44
44
  export function resolveWecomTarget(raw: string | undefined, options?: { preferUserForDigits?: boolean }): WecomTarget | undefined {
45
45
  if (!raw?.trim()) return undefined;
46
46
 
47
47
  const trimmed = raw.trim();
48
48
 
49
- // 1. 先检查原始字符串中的类型前缀(处理 user:0404777 无前缀格式)
49
+ // 1. 先检查原始字符串中的类型前缀(处理 user:xxx 无前缀格式)
50
50
  // 这样即使没有 wecom: 前缀,也能正确识别类型
51
51
  if (/^user:/i.test(trimmed)) {
52
52
  return { touser: trimmed.replace(/^user:/i, "").trim() };
@@ -64,7 +64,7 @@ export function resolveWecomTarget(raw: string | undefined, options?: { preferUs
64
64
  // 2. Remove standard namespace prefixes (移除标准命名空间前缀)
65
65
  let clean = trimmed.replace(/^(wecom-agent|wecom|wechatwork|wework|qywx):/i, "");
66
66
 
67
- // 3. 再次检查类型前缀(处理 wecom:user:0404777 格式)
67
+ // 3. 再次检查类型前缀(处理 wecom:user:xxx 格式)
68
68
  if (/^user:/i.test(clean)) {
69
69
  return { touser: clean.replace(/^user:/i, "").trim() };
70
70
  }
@@ -90,8 +90,11 @@ export function resolveWecomTarget(raw: string | undefined, options?: { preferUs
90
90
  // 原因:1) 定时任务可能直接配置 to: "1" 发送给根部门
91
91
  // 2) 企业微信官方文档示例使用纯数字表示部门
92
92
  // 3) 用户 ID 应该使用显式前缀 "user:xxx"
93
- // 如果需要发送给用户,请使用 "user:0404777" 格式
93
+ // 但如果 preferUserForDigits 为 true 则视为 User ID(用于 agent scoped 场景)
94
94
  if (/^\d+$/.test(clean)) {
95
+ if (options?.preferUserForDigits) {
96
+ return { touser: clean };
97
+ }
95
98
  return { toparty: clean };
96
99
  }
97
100
 
@@ -47,4 +47,50 @@ describe("mapBotWsFrameToInboundEvent", () => {
47
47
 
48
48
  expect(event.text).toBe("@daodao 这个线索价值\n\n> 原始引用内容");
49
49
  });
50
+
51
+ it("extracts attachments from mixed events", () => {
52
+ const event = mapBotWsFrameToInboundEvent({
53
+ account: createBotAccount(),
54
+ frame: {
55
+ cmd: "aibot_msg_callback",
56
+ headers: { req_id: "req-2" },
57
+ body: {
58
+ msgid: "msg-2",
59
+ msgtype: "mixed",
60
+ chattype: "group",
61
+ chatid: "group-2",
62
+ from: { userid: "user-2" },
63
+ mixed: {
64
+ msg_item: [
65
+ {
66
+ msgtype: "text",
67
+ text: { content: "来看看这张图" },
68
+ },
69
+ {
70
+ msgtype: "image",
71
+ image: { url: "https://example.com/image.jpg", aeskey: "mock-aes-key" },
72
+ },
73
+ {
74
+ msgtype: "file",
75
+ file: { url: "https://example.com/doc.pdf", aeskey: "mock-file-key" },
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ },
81
+ });
82
+
83
+ expect(event.attachments).toBeDefined();
84
+ expect(event.attachments).toHaveLength(2);
85
+ expect(event.attachments![0]).toEqual({
86
+ name: "image",
87
+ remoteUrl: "https://example.com/image.jpg",
88
+ aesKey: "mock-aes-key",
89
+ });
90
+ expect(event.attachments![1]).toEqual({
91
+ name: "file",
92
+ remoteUrl: "https://example.com/doc.pdf",
93
+ aesKey: "mock-file-key",
94
+ });
95
+ });
50
96
  });
@@ -55,6 +55,28 @@ export function mapBotWsFrameToInboundEvent(params: {
55
55
  const peerId = peerKind === "group" ? body.chatid ?? senderId : senderId;
56
56
  const inboundKind = resolveInboundKind(body);
57
57
 
58
+ let attachments: UnifiedInboundEvent["attachments"];
59
+ if (body.msgtype === "image") {
60
+ attachments = [{ name: "image", remoteUrl: (body as any).image?.url, aesKey: (body as any).image?.aeskey }];
61
+ } else if (body.msgtype === "file") {
62
+ attachments = [{ name: "file", remoteUrl: (body as any).file?.url, aesKey: (body as any).file?.aeskey }];
63
+ } else if (body.msgtype === "mixed") {
64
+ const items = (body as any).mixed?.msg_item;
65
+ if (Array.isArray(items)) {
66
+ attachments = [];
67
+ for (const item of items) {
68
+ if (item.msgtype === "image" && item.image?.url) {
69
+ attachments.push({ name: "image", remoteUrl: item.image.url, aesKey: item.image.aeskey });
70
+ } else if (item.msgtype === "file" && item.file?.url) {
71
+ attachments.push({ name: "file", remoteUrl: item.file.url, aesKey: item.file.aeskey });
72
+ }
73
+ }
74
+ if (attachments.length === 0) {
75
+ attachments = undefined;
76
+ }
77
+ }
78
+ }
79
+
58
80
  return {
59
81
  accountId: account.accountId,
60
82
  capability: "bot",
@@ -89,10 +111,6 @@ export function mapBotWsFrameToInboundEvent(params: {
89
111
  envelopeType: "ws",
90
112
  },
91
113
  },
92
- attachments: body.msgtype === "image"
93
- ? [{ name: "image", remoteUrl: (body as any).image?.url, aesKey: (body as any).image?.aeskey }]
94
- : body.msgtype === "file"
95
- ? [{ name: "file", remoteUrl: (body as any).file?.url, aesKey: (body as any).file?.aeskey }]
96
- : undefined,
114
+ attachments,
97
115
  };
98
116
  }