@sunnoy/wecom 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - 🌊 **Streaming Output**: Built on WeCom's latest AI bot streaming mechanism for smooth typewriter-style responses.
10
10
  - 🤖 **Dynamic Agent Management**: Automatically creates isolated agents per direct message user or group chat, with independent workspaces and conversation contexts.
11
11
  - 👥 **Deep Group Chat Integration**: Supports group message parsing with @mention triggering.
12
+ - 🎤 **Voice Message Support**: Automatically processes voice messages transcribed by WeCom into text for AI interaction (direct messages only).
12
13
  - 🖼️ **Image Support**: Automatic base64 encoding and sending of local images (screenshots, generated images) without requiring additional configuration.
13
14
  - 🛠️ **Command Enhancement**: Built-in commands (e.g., `/new` for new sessions, `/status` for status) with allowlist configuration.
14
15
  - 🔒 **Security & Authentication**: Full support for WeCom message encryption/decryption, URL verification, and sender validation.
@@ -216,6 +217,10 @@ AI: [Takes screenshot] → Image displays properly in WeCom ✅
216
217
 
217
218
  If an image fails to process (size limit, invalid format), the text response will still be delivered and an error will be logged.
218
219
 
220
+ ### Q: Does the bot support voice messages?
221
+
222
+ **A:** Yes! Voice messages in direct chats are automatically transcribed by WeCom and processed as text. No additional configuration needed.
223
+
219
224
  ### Q: How to configure auth token for public-facing OpenClaw with WeCom callbacks?
220
225
 
221
226
  **A:** WeCom bot **does not need** OpenClaw's Gateway Auth Token.
package/README_ZH.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - 🌊 **流式输出 (Streaming)**: 基于企业微信最新的 AI 机器人流式分片机制,实现流畅的打字机式回复体验。
10
10
  - 🤖 **动态 Agent 管理**: 默认按"每个私聊用户 / 每个群聊"自动创建独立 Agent。每个 Agent 拥有独立的工作区与对话上下文,实现更强的数据隔离。
11
11
  - 👥 **群聊深度集成**: 支持群聊消息解析,可通过 @提及(At-mention)精准触发机器人响应。
12
+ - 🎤 **语音消息支持**: 自动处理企业微信转录后的语音消息,转换为文本进行 AI 交互(仅限私聊)。
12
13
  - 🖼️ **图片支持**: 自动将本地图片(截图、生成的图像)进行 base64 编码并发送,无需额外配置。
13
14
  - 🛠️ **指令增强**: 内置常用指令支持(如 `/new` 开启新会话、`/status` 查看状态等),并提供指令白名单配置功能。
14
15
  - 🔒 **安全与认证**: 完整支持企业微信消息加解密、URL 验证及发送者身份校验。
@@ -216,6 +217,10 @@ AI:[执行截图] → 图片在企业微信中正常显示 ✅
216
217
 
217
218
  如果图片处理失败(超出大小限制、格式不支持等),文本回复仍会正常发送,错误信息会记录在日志中。
218
219
 
220
+ ### Q: 机器人支持语音消息吗?
221
+
222
+ **A:** 支持!私聊中的语音消息会被企业微信自动转录为文字并作为文本处理,无需额外配置。
223
+
219
224
  ### Q: OpenClaw 开放公网需要 auth token,企业微信回调如何配置?
220
225
 
221
226
  **A:** 企业微信机器人**不需要**配置 OpenClaw 的 Gateway Auth Token。
package/index.js CHANGED
@@ -805,9 +805,45 @@ async function deliverWecomReply({ payload, account, responseUrl, senderId, stre
805
805
  senderId,
806
806
  });
807
807
 
808
+ // 处理绝对路径的 MEDIA: 行(OpenClaw 会拒绝它们,所以我们需要手动处理)
809
+ const mediaRegex = /^MEDIA:\s*(.+)$/gm;
810
+ const mediaMatches = [];
811
+ let match;
812
+ while ((match = mediaRegex.exec(text)) !== null) {
813
+ const mediaPath = match[1].trim();
814
+ // 检查是否是绝对路径(以 / 开头)
815
+ if (mediaPath.startsWith("/")) {
816
+ mediaMatches.push({
817
+ fullMatch: match[0],
818
+ path: mediaPath
819
+ });
820
+ logger.debug("Detected absolute path MEDIA line", {
821
+ streamId,
822
+ mediaPath,
823
+ line: match[0]
824
+ });
825
+ }
826
+ }
827
+
828
+ // 如果检测到绝对路径的 MEDIA 行,将图片加入队列并从文本中移除
829
+ let processedText = text;
830
+ if (mediaMatches.length > 0 && streamId) {
831
+ for (const media of mediaMatches) {
832
+ const queued = streamManager.queueImage(streamId, media.path);
833
+ if (queued) {
834
+ // 从文本中移除这行
835
+ processedText = processedText.replace(media.fullMatch, "").trim();
836
+ logger.info("Queued absolute path image for stream", {
837
+ streamId,
838
+ imagePath: media.path
839
+ });
840
+ }
841
+ }
842
+ }
843
+
808
844
  // 所有消息都通过流式发送
809
- if (!text.trim()) {
810
- logger.debug("WeCom: empty block, skipping stream update");
845
+ if (!processedText.trim()) {
846
+ logger.debug("WeCom: empty block after processing, skipping stream update");
811
847
  return;
812
848
  }
813
849
 
@@ -834,10 +870,10 @@ async function deliverWecomReply({ payload, account, responseUrl, senderId, stre
834
870
  // 尝试从 activeStreams 获取
835
871
  const activeStreamId = activeStreams.get(senderId);
836
872
  if (activeStreamId && streamManager.hasStream(activeStreamId)) {
837
- appendToStream(activeStreamId, text);
873
+ appendToStream(activeStreamId, processedText);
838
874
  logger.debug("WeCom stream appended (via activeStreams)", {
839
875
  streamId: activeStreamId,
840
- contentLength: text.length,
876
+ contentLength: processedText.length,
841
877
  });
842
878
  return;
843
879
  }
@@ -850,10 +886,10 @@ async function deliverWecomReply({ payload, account, responseUrl, senderId, stre
850
886
  return;
851
887
  }
852
888
 
853
- appendToStream(streamId, text);
889
+ appendToStream(streamId, processedText);
854
890
  logger.debug("WeCom stream appended", {
855
891
  streamId,
856
- contentLength: text.length,
892
+ contentLength: processedText.length,
857
893
  to: senderId
858
894
  });
859
895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunnoy/wecom",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Enterprise WeChat AI Bot channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/webhook.js CHANGED
@@ -209,6 +209,50 @@ export class WecomWebhook {
209
209
  query: { timestamp, nonce },
210
210
  };
211
211
  }
212
+ else if (msgtype === "voice") {
213
+ // Voice message (single chat only) - WeCom automatically transcribes to text
214
+ const content = data.voice?.content || "";
215
+ const msgId = data.msgid || `msg_${Date.now()}`;
216
+ const fromUser = data.from?.userid || "";
217
+ const responseUrl = data.response_url || "";
218
+ const chatType = data.chattype || "single";
219
+ const chatId = data.chatid || "";
220
+
221
+ // Check for duplicates
222
+ if (this.deduplicator.isDuplicate(msgId)) {
223
+ logger.debug("Duplicate voice message ignored", { msgId });
224
+ return null;
225
+ }
226
+
227
+ // Validate content
228
+ if (!content.trim()) {
229
+ logger.warn("Empty voice message received", { msgId, fromUser });
230
+ return null;
231
+ }
232
+
233
+ logger.info("Received voice message (auto-transcribed by WeCom)", {
234
+ fromUser,
235
+ chatType,
236
+ chatId: chatId || "(private)",
237
+ originalType: "voice",
238
+ transcribedLength: content.length,
239
+ preview: content.substring(0, 50)
240
+ });
241
+
242
+ // Treat voice as text since WeCom already transcribed it
243
+ return {
244
+ message: {
245
+ msgId,
246
+ msgType: "text",
247
+ content,
248
+ fromUser,
249
+ chatType,
250
+ chatId,
251
+ responseUrl,
252
+ },
253
+ query: { timestamp, nonce },
254
+ };
255
+ }
212
256
  else if (msgtype === "event") {
213
257
  logger.info("Received event", { event: data.event });
214
258
  return {