@marshulll/openclaw-wecom 0.1.25 → 0.1.27
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/LICENSE +21 -0
- package/README.en.md +23 -3
- package/README.md +23 -3
- package/README.zh.md +23 -3
- package/docs/INSTALL.md +26 -0
- package/package.json +2 -2
- package/wecom/src/wecom-bot.ts +67 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenClaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
CHANGED
|
@@ -10,7 +10,7 @@ OpenClaw WeCom plugin supporting **Bot API mode** and **Internal App mode** with
|
|
|
10
10
|
- Dual mode: Bot API (JSON callback + stream) / App (XML callback + ACK + proactive send)
|
|
11
11
|
- Multi-account: `channels.wecom.accounts`
|
|
12
12
|
- Message types: text / image / voice / video / file (send & receive)
|
|
13
|
-
- Commands: `/help`, `/status`, `/clear`, `/sendfile`
|
|
13
|
+
- Commands (App mode): `/help`, `/status`, `/clear`, `/sendfile`
|
|
14
14
|
- Stability: signature verification, AES decrypt, token cache, rate limit & retries
|
|
15
15
|
- Group chat: uses `appchat/send` when `chatId` is present
|
|
16
16
|
- Advanced: folder zip sending, send queue, operation logs, media auto recognition
|
|
@@ -88,18 +88,38 @@ Install guide: `docs/INSTALL.md`
|
|
|
88
88
|
- App mode: downloads inbound media to local temp dir (`media.tempDir`)
|
|
89
89
|
- Bot mode media bridge: if reply payload includes `mediaUrl + mediaType`,
|
|
90
90
|
and App credentials are present, media will be uploaded and sent
|
|
91
|
+
> Bot-only config cannot send/receive media (image/voice/video/file). Full media support requires App credentials.
|
|
91
92
|
|
|
92
|
-
## Extra commands
|
|
93
|
+
## Extra commands (App mode)
|
|
93
94
|
- `/sendfile`: send files from server (multiple absolute paths)
|
|
94
95
|
- Directories are zipped automatically
|
|
95
96
|
- Example: `/sendfile /tmp/openclaw-wecom /home/shu/Desktop/report.pdf`
|
|
96
97
|
- Natural language also works: "send me this file image-xxx.jpg" (default match in `media.tempDir`)
|
|
98
|
+
- Search scope keywords: `桌面` → `~/Desktop`, `下载` → `~/Downloads`, `临时` → `media.tempDir`
|
|
97
99
|
- If multiple matches are found, a list will be returned for confirmation
|
|
98
100
|
|
|
101
|
+
## Proactive send (App mode)
|
|
102
|
+
Push endpoint path: `{webhookPath}/push` (e.g. `/wecom/app/push`).
|
|
103
|
+
|
|
104
|
+
- Method: `POST`
|
|
105
|
+
- Auth: `pushToken` (optional but recommended)
|
|
106
|
+
- Accepts `Authorization: Bearer <token>`, `x-openclaw-token`, query/body `token`
|
|
107
|
+
- Target: `toUser` (DM) or `chatId` (group)
|
|
108
|
+
|
|
109
|
+
Minimal example (text):
|
|
110
|
+
```bash
|
|
111
|
+
curl -X POST "https://your-domain/wecom/app/push" \
|
|
112
|
+
-H "Content-Type: application/json" \
|
|
113
|
+
-H "Authorization: Bearer PUSH_TOKEN" \
|
|
114
|
+
-d '{"toUser":"WenShuJun","text":"Hello"}'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Media (file/image/voice/video): use `mediaUrl` or `mediaBase64`. You can also send text together.
|
|
118
|
+
|
|
99
119
|
## Media auto recognition (optional)
|
|
100
120
|
- **Voice send/receive does NOT require API**; only auto transcription needs an OpenAI-compatible API
|
|
101
121
|
- **Video recognition requires ffmpeg** (install on server, then set `media.auto.video.enabled = true`)
|
|
102
|
-
- Video recognition supports **light / full** modes (default: light)
|
|
122
|
+
- Video recognition supports **light / full** modes (default: light) via `media.auto.video.mode`
|
|
103
123
|
- Small text files can be previewed automatically
|
|
104
124
|
|
|
105
125
|
## Send queue & operation logs (optional)
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ OpenClaw WeCom 插件,支持 **智能机器人 API 模式** 与 **自建应用
|
|
|
10
10
|
- 双模式:Bot API(JSON 回调 + stream)/ App(XML 回调 + ACK + 主动发送)
|
|
11
11
|
- 多账户:`channels.wecom.accounts`
|
|
12
12
|
- 消息类型:文本 / 图片 / 语音 / 视频 / 文件(收发均支持)
|
|
13
|
-
-
|
|
13
|
+
- 机器人命令(App 模式):`/help`、`/status`、`/clear`、`/sendfile`
|
|
14
14
|
- 稳定性:签名校验、AES 解密、token 缓存、限流与重试
|
|
15
15
|
- 群聊:自动识别 `chatId` 并使用 `appchat/send`
|
|
16
16
|
- 进阶:文件夹打包发送、发送队列、操作日志、多媒体自动识别
|
|
@@ -90,18 +90,38 @@ openclaw gateway restart
|
|
|
90
90
|
- App 模式:收到媒体会下载到本地临时目录(可配置 `media.tempDir`)
|
|
91
91
|
- Bot 模式媒体桥接:当 reply payload 含 `mediaUrl + mediaType` 时,
|
|
92
92
|
若已配置 App 凭据,会自动上传并发送媒体
|
|
93
|
+
> 仅配置 Bot 时,媒体(图片/语音/视频/文件)无法收发;需补齐 App 凭据才能启用完整媒体能力。
|
|
93
94
|
|
|
94
|
-
##
|
|
95
|
+
## 命令补充(App 模式)
|
|
95
96
|
- `/sendfile`:发送服务器文件(支持多个绝对路径)
|
|
96
97
|
- 支持目录:自动打包为 zip 后发送
|
|
97
98
|
- 示例:`/sendfile /tmp/openclaw-wecom /home/shu/Desktop/report.pdf`
|
|
98
99
|
- 也支持自然语言:`把这个文件发给我 image-xxx.jpg`(默认仅在 `media.tempDir` 内匹配)
|
|
100
|
+
- 搜索范围关键词:`桌面` → `~/Desktop`,`下载` → `~/Downloads`,`临时` → `media.tempDir`
|
|
99
101
|
- 多文件会先返回列表,回复“全部”或序号再发送
|
|
100
102
|
|
|
103
|
+
## 主动消息(App 模式)
|
|
104
|
+
主动推送接口路径为:`{webhookPath}/push`(例如 `/wecom/app/push`)。
|
|
105
|
+
|
|
106
|
+
- 方法:`POST`
|
|
107
|
+
- 鉴权:`pushToken`(可选,但建议开启)
|
|
108
|
+
- 可放在 `Authorization: Bearer <token>`、`x-openclaw-token`、`token` 参数或 body 的 `token` 字段
|
|
109
|
+
- 目标:`toUser`(单人)或 `chatId`(群聊),二选一
|
|
110
|
+
|
|
111
|
+
最小示例(文本):
|
|
112
|
+
```bash
|
|
113
|
+
curl -X POST "https://你的域名/wecom/app/push" \
|
|
114
|
+
-H "Content-Type: application/json" \
|
|
115
|
+
-H "Authorization: Bearer PUSH_TOKEN" \
|
|
116
|
+
-d '{"toUser":"WenShuJun","text":"你好"}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
发送媒体(file/image/voice/video):支持 `mediaUrl` 或 `mediaBase64`,可与 `text` 同时发送。
|
|
120
|
+
|
|
101
121
|
## 多媒体自动识别(可选)
|
|
102
122
|
- **语音收发不需要 API**,只有开启“语音自动转写”才需要 OpenAI 兼容接口
|
|
103
123
|
- **视频识别需要 ffmpeg**(服务器已安装后,将 `media.auto.video.enabled` 设为 `true`)
|
|
104
|
-
- 视频识别支持 **light / full** 两种模式(默认 light
|
|
124
|
+
- 视频识别支持 **light / full** 两种模式(默认 light),可通过 `media.auto.video.mode` 切换
|
|
105
125
|
- 文本文件可自动预览(小文件直接读入)
|
|
106
126
|
|
|
107
127
|
## 发送队列与操作日志(可选)
|
package/README.zh.md
CHANGED
|
@@ -10,7 +10,7 @@ OpenClaw WeCom 插件,支持 **智能机器人 API 模式** 与 **自建应用
|
|
|
10
10
|
- 双模式:Bot API(JSON 回调 + stream)/ App(XML 回调 + ACK + 主动发送)
|
|
11
11
|
- 多账户:`channels.wecom.accounts`
|
|
12
12
|
- 消息类型:文本 / 图片 / 语音 / 视频 / 文件(收发均支持)
|
|
13
|
-
-
|
|
13
|
+
- 机器人命令(App 模式):`/help`、`/status`、`/clear`、`/sendfile`
|
|
14
14
|
- 稳定性:签名校验、AES 解密、token 缓存、限流与重试
|
|
15
15
|
- 群聊:自动识别 `chatId` 并使用 `appchat/send`
|
|
16
16
|
- 进阶:文件夹打包发送、发送队列、操作日志、多媒体自动识别
|
|
@@ -90,18 +90,38 @@ openclaw gateway restart
|
|
|
90
90
|
- App 模式:收到媒体会下载到本地临时目录(可配置 `media.tempDir`)
|
|
91
91
|
- Bot 模式媒体桥接:当 reply payload 含 `mediaUrl + mediaType` 时,
|
|
92
92
|
若已配置 App 凭据,会自动上传并发送媒体
|
|
93
|
+
> 仅配置 Bot 时,媒体(图片/语音/视频/文件)无法收发;需补齐 App 凭据才能启用完整媒体能力。
|
|
93
94
|
|
|
94
|
-
##
|
|
95
|
+
## 命令补充(App 模式)
|
|
95
96
|
- `/sendfile`:发送服务器文件(支持多个绝对路径)
|
|
96
97
|
- 支持目录:自动打包为 zip 后发送
|
|
97
98
|
- 示例:`/sendfile /tmp/openclaw-wecom /home/shu/Desktop/report.pdf`
|
|
98
99
|
- 也支持自然语言:`把这个文件发给我 image-xxx.jpg`(默认仅在 `media.tempDir` 内匹配)
|
|
100
|
+
- 搜索范围关键词:`桌面` → `~/Desktop`,`下载` → `~/Downloads`,`临时` → `media.tempDir`
|
|
99
101
|
- 多文件会先返回列表,回复“全部”或序号再发送
|
|
100
102
|
|
|
103
|
+
## 主动消息(App 模式)
|
|
104
|
+
主动推送接口路径为:`{webhookPath}/push`(例如 `/wecom/app/push`)。
|
|
105
|
+
|
|
106
|
+
- 方法:`POST`
|
|
107
|
+
- 鉴权:`pushToken`(可选,但建议开启)
|
|
108
|
+
- 可放在 `Authorization: Bearer <token>`、`x-openclaw-token`、`token` 参数或 body 的 `token` 字段
|
|
109
|
+
- 目标:`toUser`(单人)或 `chatId`(群聊),二选一
|
|
110
|
+
|
|
111
|
+
最小示例(文本):
|
|
112
|
+
```bash
|
|
113
|
+
curl -X POST "https://你的域名/wecom/app/push" \
|
|
114
|
+
-H "Content-Type: application/json" \
|
|
115
|
+
-H "Authorization: Bearer PUSH_TOKEN" \
|
|
116
|
+
-d '{"toUser":"WenShuJun","text":"你好"}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
发送媒体(file/image/voice/video):支持 `mediaUrl` 或 `mediaBase64`,可与 `text` 同时发送。
|
|
120
|
+
|
|
101
121
|
## 多媒体自动识别(可选)
|
|
102
122
|
- **语音收发不需要 API**,只有开启“语音自动转写”才需要 OpenAI 兼容接口
|
|
103
123
|
- **视频识别需要 ffmpeg**(服务器已安装后,将 `media.auto.video.enabled` 设为 `true`)
|
|
104
|
-
- 视频识别支持 **light / full** 两种模式(默认 light
|
|
124
|
+
- 视频识别支持 **light / full** 两种模式(默认 light),可通过 `media.auto.video.mode` 切换
|
|
105
125
|
- 文本文件可自动预览(小文件直接读入)
|
|
106
126
|
|
|
107
127
|
## 发送队列与操作日志(可选)
|
package/docs/INSTALL.md
CHANGED
|
@@ -129,9 +129,11 @@ openclaw gateway restart
|
|
|
129
129
|
|
|
130
130
|
## 高级能力(可选)
|
|
131
131
|
### /sendfile(文件与文件夹)
|
|
132
|
+
- 仅 **App 模式** 支持 `/sendfile`
|
|
132
133
|
- `/sendfile` 仅支持 **服务器绝对路径**
|
|
133
134
|
- 目录会自动打包为 zip 再发送
|
|
134
135
|
- 自然语言也可触发:`把这个文件发给我 image-xxx.jpg`(默认仅在 `media.tempDir` 内匹配)
|
|
136
|
+
- 搜索范围关键词:`桌面` → `~/Desktop`,`下载` → `~/Downloads`,`临时` → `media.tempDir`
|
|
135
137
|
- 如匹配多个文件,会返回列表让你确认(回复“全部”或序号)
|
|
136
138
|
|
|
137
139
|
示例:
|
|
@@ -142,8 +144,14 @@ openclaw gateway restart
|
|
|
142
144
|
### 多媒体自动识别
|
|
143
145
|
- **语音收发不需要 API**;只有开启“语音自动转写”才需要 OpenAI 兼容接口
|
|
144
146
|
- **视频识别需要 ffmpeg**(服务器安装后将 `media.auto.video.enabled=true`)
|
|
147
|
+
- **视频识别 light/full 模式**:`media.auto.video.mode`(默认 `light`)
|
|
145
148
|
- 文本文件可自动预览(小文件直接读入)
|
|
146
149
|
|
|
150
|
+
建议安装 ffmpeg(Ubuntu):
|
|
151
|
+
```bash
|
|
152
|
+
sudo apt-get update && sudo apt-get install -y ffmpeg
|
|
153
|
+
```
|
|
154
|
+
|
|
147
155
|
### 发送队列与操作日志
|
|
148
156
|
- `sendQueue.intervalMs`:/sendfile 多文件发送间隔
|
|
149
157
|
- `operations.logPath`:JSONL 日志,记录发送文件与主动推送
|
|
@@ -153,6 +161,24 @@ openclaw gateway restart
|
|
|
153
161
|
- 在企业微信后台配置回调 URL。
|
|
154
162
|
- 建议 Bot 与 App 使用不同 `webhookPath`,便于排障与避免回调混淆。
|
|
155
163
|
|
|
164
|
+
## 主动推送(App 模式)
|
|
165
|
+
主动推送接口路径:`{webhookPath}/push`(例如 `/wecom/app/push`)。
|
|
166
|
+
|
|
167
|
+
- 方法:`POST`
|
|
168
|
+
- 鉴权:`pushToken`(可选,但建议开启)
|
|
169
|
+
- `Authorization: Bearer <token>`、`x-openclaw-token`、`token` 参数或 body `token`
|
|
170
|
+
- 目标:`toUser`(单人)或 `chatId`(群聊),二选一
|
|
171
|
+
|
|
172
|
+
最小示例(文本):
|
|
173
|
+
```bash
|
|
174
|
+
curl -X POST "https://你的域名/wecom/app/push" \
|
|
175
|
+
-H "Content-Type: application/json" \
|
|
176
|
+
-H "Authorization: Bearer PUSH_TOKEN" \
|
|
177
|
+
-d '{"toUser":"WenShuJun","text":"你好"}'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
媒体发送(file/image/voice/video):使用 `mediaUrl` 或 `mediaBase64`,可与 `text` 同时发送。
|
|
181
|
+
|
|
156
182
|
## 常见问题
|
|
157
183
|
- 回调验证失败:检查 Token / AESKey / URL 是否一致
|
|
158
184
|
- 没有回复:检查 OpenClaw 是否已启用插件并重启 gateway
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marshulll/openclaw-wecom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw WeCom channel plugin (intelligent bot + internal app)",
|
|
6
6
|
"author": "OpenClaw",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"channel",
|
|
53
53
|
"plugin"
|
|
54
54
|
],
|
|
55
|
-
"license": "
|
|
55
|
+
"license": "MIT",
|
|
56
56
|
"files": [
|
|
57
57
|
"wecom/**",
|
|
58
58
|
"docs/INSTALL.md",
|
package/wecom/src/wecom-bot.ts
CHANGED
|
@@ -9,7 +9,15 @@ import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
|
9
9
|
import type { WecomWebhookTarget } from "./monitor.js";
|
|
10
10
|
import type { ResolvedWecomAccount, WecomInboundMessage } from "./types.js";
|
|
11
11
|
import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext, verifyWecomSignature } from "./crypto.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
downloadWecomMedia,
|
|
14
|
+
fetchMediaFromUrl,
|
|
15
|
+
sendWecomFile,
|
|
16
|
+
sendWecomImage,
|
|
17
|
+
sendWecomVideo,
|
|
18
|
+
sendWecomVoice,
|
|
19
|
+
uploadWecomMedia,
|
|
20
|
+
} from "./wecom-api.js";
|
|
13
21
|
import { getWecomRuntime } from "./runtime.js";
|
|
14
22
|
import { describeImageWithVision, resolveVisionConfig } from "./media-vision.js";
|
|
15
23
|
import {
|
|
@@ -501,9 +509,13 @@ function resolveBotMediaUrl(msg: any, msgtype: "image" | "voice" | "video" | "fi
|
|
|
501
509
|
if (msgtype === "image") {
|
|
502
510
|
return pickString(
|
|
503
511
|
block.url,
|
|
512
|
+
block.imageUrl,
|
|
513
|
+
block.image_url,
|
|
504
514
|
block.picurl,
|
|
505
515
|
block.picUrl,
|
|
506
516
|
block.pic_url,
|
|
517
|
+
msg.imageUrl,
|
|
518
|
+
msg.image_url,
|
|
507
519
|
msg.picurl,
|
|
508
520
|
msg.picUrl,
|
|
509
521
|
msg.pic_url,
|
|
@@ -516,6 +528,8 @@ function resolveBotMediaUrl(msg: any, msgtype: "image" | "voice" | "video" | "fi
|
|
|
516
528
|
block.fileurl,
|
|
517
529
|
block.fileUrl,
|
|
518
530
|
block.file_url,
|
|
531
|
+
block.downloadUrl,
|
|
532
|
+
block.download_url,
|
|
519
533
|
block.mediaUrl,
|
|
520
534
|
block.media_url,
|
|
521
535
|
msg.voiceUrl,
|
|
@@ -529,6 +543,8 @@ function resolveBotMediaUrl(msg: any, msgtype: "image" | "voice" | "video" | "fi
|
|
|
529
543
|
block.fileurl,
|
|
530
544
|
block.fileUrl,
|
|
531
545
|
block.file_url,
|
|
546
|
+
block.downloadUrl,
|
|
547
|
+
block.download_url,
|
|
532
548
|
block.mediaUrl,
|
|
533
549
|
block.media_url,
|
|
534
550
|
msg.videoUrl,
|
|
@@ -541,6 +557,8 @@ function resolveBotMediaUrl(msg: any, msgtype: "image" | "voice" | "video" | "fi
|
|
|
541
557
|
block.fileurl,
|
|
542
558
|
block.fileUrl,
|
|
543
559
|
block.file_url,
|
|
560
|
+
block.downloadUrl,
|
|
561
|
+
block.download_url,
|
|
544
562
|
block.mediaUrl,
|
|
545
563
|
block.media_url,
|
|
546
564
|
msg.fileUrl,
|
|
@@ -554,12 +572,28 @@ function resolveBotMediaBase64(msg: any, msgtype: "image" | "voice" | "video" |
|
|
|
554
572
|
const block = msg[msgtype] ?? {};
|
|
555
573
|
return pickString(
|
|
556
574
|
block.base64,
|
|
575
|
+
block.base64Data,
|
|
557
576
|
block.data,
|
|
558
577
|
msg.base64,
|
|
559
578
|
msg.data,
|
|
560
579
|
);
|
|
561
580
|
}
|
|
562
581
|
|
|
582
|
+
function resolveBotMediaId(msg: any, msgtype: "image" | "voice" | "video" | "file"): string {
|
|
583
|
+
if (!msg || typeof msg !== "object") return "";
|
|
584
|
+
const block = msg[msgtype] ?? {};
|
|
585
|
+
return pickString(
|
|
586
|
+
block.media_id,
|
|
587
|
+
block.mediaId,
|
|
588
|
+
block.mediaid,
|
|
589
|
+
block.mediaID,
|
|
590
|
+
msg.media_id,
|
|
591
|
+
msg.mediaId,
|
|
592
|
+
msg.mediaid,
|
|
593
|
+
msg.mediaID,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
563
597
|
function resolveBotMediaFilename(msg: any): string {
|
|
564
598
|
if (!msg || typeof msg !== "object") return "";
|
|
565
599
|
const block = msg.file ?? {};
|
|
@@ -568,6 +602,7 @@ function resolveBotMediaFilename(msg: any): string {
|
|
|
568
602
|
block.fileName,
|
|
569
603
|
block.name,
|
|
570
604
|
block.file_name,
|
|
605
|
+
block.file,
|
|
571
606
|
msg.filename,
|
|
572
607
|
msg.fileName,
|
|
573
608
|
msg.name,
|
|
@@ -579,9 +614,10 @@ async function buildBotMediaMessage(params: {
|
|
|
579
614
|
msgtype: "image" | "voice" | "video" | "file";
|
|
580
615
|
url?: string;
|
|
581
616
|
base64?: string;
|
|
617
|
+
mediaId?: string;
|
|
582
618
|
filename?: string;
|
|
583
619
|
}): Promise<InboundBody> {
|
|
584
|
-
const { target, msgtype, url, base64, filename } = params;
|
|
620
|
+
const { target, msgtype, url, base64, mediaId, filename } = params;
|
|
585
621
|
|
|
586
622
|
const fallbackLabel = msgtype === "image"
|
|
587
623
|
? "[image]"
|
|
@@ -591,7 +627,7 @@ async function buildBotMediaMessage(params: {
|
|
|
591
627
|
? "[video]"
|
|
592
628
|
: "[file]";
|
|
593
629
|
|
|
594
|
-
if (!url && !base64) return { text: fallbackLabel };
|
|
630
|
+
if (!url && !base64 && !mediaId) return { text: fallbackLabel };
|
|
595
631
|
|
|
596
632
|
try {
|
|
597
633
|
const cacheKey = buildMediaCacheKey({ url, base64 });
|
|
@@ -625,15 +661,23 @@ async function buildBotMediaMessage(params: {
|
|
|
625
661
|
let buffer: Buffer | null = null;
|
|
626
662
|
let contentType = "";
|
|
627
663
|
if (base64) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
664
|
+
const parsed = parseBase64Input(base64);
|
|
665
|
+
buffer = Buffer.from(parsed.data, "base64");
|
|
666
|
+
if (parsed.mimeType) contentType = parsed.mimeType;
|
|
667
|
+
if (!contentType) {
|
|
668
|
+
if (msgtype === "image") contentType = "image/jpeg";
|
|
669
|
+
else if (msgtype === "voice") contentType = "audio/amr";
|
|
670
|
+
else if (msgtype === "video") contentType = "video/mp4";
|
|
671
|
+
else contentType = "application/octet-stream";
|
|
672
|
+
}
|
|
633
673
|
} else if (url) {
|
|
634
674
|
const media = await fetchMediaFromUrl(url, target.account);
|
|
635
675
|
buffer = media.buffer;
|
|
636
676
|
contentType = media.contentType;
|
|
677
|
+
} else if (mediaId && target.account.corpId && target.account.corpSecret && target.account.agentId) {
|
|
678
|
+
const media = await downloadWecomMedia({ account: target.account, mediaId });
|
|
679
|
+
buffer = media.buffer;
|
|
680
|
+
contentType = media.contentType;
|
|
637
681
|
}
|
|
638
682
|
|
|
639
683
|
if (!buffer) return { text: fallbackLabel };
|
|
@@ -771,9 +815,17 @@ async function buildInboundBody(params: { target: WecomWebhookTarget; msg: Wecom
|
|
|
771
815
|
if (msgtype === "voice") {
|
|
772
816
|
const content = (msg as any).voice?.content;
|
|
773
817
|
if (typeof content === "string" && content.trim()) return { text: content.trim() };
|
|
818
|
+
const recognition = pickString(
|
|
819
|
+
(msg as any).voice?.recognition,
|
|
820
|
+
(msg as any).voice?.text,
|
|
821
|
+
(msg as any).voice?.transcript,
|
|
822
|
+
(msg as any).recognition,
|
|
823
|
+
);
|
|
824
|
+
if (recognition) return { text: recognition };
|
|
774
825
|
const url = resolveBotMediaUrl(msg as any, "voice");
|
|
775
826
|
const base64 = resolveBotMediaBase64(msg as any, "voice");
|
|
776
|
-
|
|
827
|
+
const mediaId = resolveBotMediaId(msg as any, "voice");
|
|
828
|
+
return await buildBotMediaMessage({ target, msgtype: "voice", url, base64, mediaId });
|
|
777
829
|
}
|
|
778
830
|
if (msgtype === "mixed") {
|
|
779
831
|
const items = (msg as any).mixed?.msg_item;
|
|
@@ -794,18 +846,21 @@ async function buildInboundBody(params: { target: WecomWebhookTarget; msg: Wecom
|
|
|
794
846
|
if (msgtype === "image") {
|
|
795
847
|
const url = resolveBotMediaUrl(msg as any, "image");
|
|
796
848
|
const base64 = resolveBotMediaBase64(msg as any, "image");
|
|
797
|
-
|
|
849
|
+
const mediaId = resolveBotMediaId(msg as any, "image");
|
|
850
|
+
return await buildBotMediaMessage({ target, msgtype: "image", url, base64, mediaId });
|
|
798
851
|
}
|
|
799
852
|
if (msgtype === "file") {
|
|
800
853
|
const url = resolveBotMediaUrl(msg as any, "file");
|
|
801
854
|
const base64 = resolveBotMediaBase64(msg as any, "file");
|
|
855
|
+
const mediaId = resolveBotMediaId(msg as any, "file");
|
|
802
856
|
const filename = resolveBotMediaFilename(msg as any);
|
|
803
|
-
return await buildBotMediaMessage({ target, msgtype: "file", url, base64, filename });
|
|
857
|
+
return await buildBotMediaMessage({ target, msgtype: "file", url, base64, mediaId, filename });
|
|
804
858
|
}
|
|
805
859
|
if (msgtype === "video") {
|
|
806
860
|
const url = resolveBotMediaUrl(msg as any, "video");
|
|
807
861
|
const base64 = resolveBotMediaBase64(msg as any, "video");
|
|
808
|
-
|
|
862
|
+
const mediaId = resolveBotMediaId(msg as any, "video");
|
|
863
|
+
return await buildBotMediaMessage({ target, msgtype: "video", url, base64, mediaId });
|
|
809
864
|
}
|
|
810
865
|
if (msgtype === "event") {
|
|
811
866
|
const eventtype = String((msg as any).event?.eventtype ?? "").trim();
|