@pawastation/wechat-kf 0.1.2 → 0.2.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 +34 -28
- package/README.zh-CN.md +34 -28
- package/dist/index.d.ts +5 -15
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/src/accounts.d.ts +2 -1
- package/dist/src/accounts.js +61 -19
- package/dist/src/accounts.js.map +1 -1
- package/dist/src/api.d.ts +31 -2
- package/dist/src/api.js +41 -13
- package/dist/src/api.js.map +1 -1
- package/dist/src/bot.d.ts +10 -8
- package/dist/src/bot.js +231 -78
- package/dist/src/bot.js.map +1 -1
- package/dist/src/channel.d.ts +7 -106
- package/dist/src/channel.js +208 -71
- package/dist/src/channel.js.map +1 -1
- package/dist/src/config-schema.d.ts +0 -6
- package/dist/src/config-schema.js +2 -7
- package/dist/src/config-schema.js.map +1 -1
- package/dist/src/constants.d.ts +20 -0
- package/dist/src/constants.js +29 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/crypto.js +7 -6
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/monitor.d.ts +27 -14
- package/dist/src/monitor.js +67 -120
- package/dist/src/monitor.js.map +1 -1
- package/dist/src/outbound.d.ts +10 -44
- package/dist/src/outbound.js +277 -92
- package/dist/src/outbound.js.map +1 -1
- package/dist/src/reply-dispatcher.d.ts +2 -6
- package/dist/src/reply-dispatcher.js +131 -32
- package/dist/src/reply-dispatcher.js.map +1 -1
- package/dist/src/runtime.d.ts +1 -119
- package/dist/src/runtime.js +2 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/send-utils.d.ts +13 -0
- package/dist/src/send-utils.js +56 -4
- package/dist/src/send-utils.js.map +1 -1
- package/dist/src/token.js +7 -3
- package/dist/src/token.js.map +1 -1
- package/dist/src/types.d.ts +68 -6
- package/dist/src/webhook.d.ts +16 -16
- package/dist/src/webhook.js +92 -75
- package/dist/src/webhook.js.map +1 -1
- package/dist/src/wechat-kf-directives.d.ts +132 -9
- package/dist/src/wechat-kf-directives.js +535 -24
- package/dist/src/wechat-kf-directives.js.map +1 -1
- package/index.ts +22 -12
- package/openclaw.plugin.json +1 -3
- package/package.json +3 -2
- package/dist/src/chunk-utils.d.ts +0 -18
- package/dist/src/chunk-utils.js +0 -58
- package/dist/src/chunk-utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -12,29 +12,29 @@
|
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
- **Inbound message handling** — receive text, image, voice, video, file, location, link, mini-program, channels, business card, and forwarded chat history from WeChat users (
|
|
15
|
+
- **Inbound message handling** — receive text, image, voice, video, file, location, link, mini-program, channels, channels shop product, channels shop order, note, business card, and forwarded chat history from WeChat users (14+ message types)
|
|
16
16
|
- **Event handling** — processes enter_session, msg_send_fail, and servicer_status_change events
|
|
17
|
-
- **Rich outbound messaging** — send text, image, voice, video, file, and
|
|
18
|
-
- **Media upload & download** — automatically downloads inbound media and uploads outbound media via the WeCom temporary media API; supports
|
|
17
|
+
- **Rich outbound messaging** — send text, image, voice, video, file, link, location, mini-program, menu, business card, and channel article messages back to users
|
|
18
|
+
- **Media upload & download** — automatically downloads inbound media and uploads outbound media via the WeCom temporary media API; supports all URL formats (HTTP, file://, local paths) for outbound media via framework loadWebMedia
|
|
19
19
|
- **Markdown to Unicode formatting** — converts markdown bold/italic/headings/lists to Unicode Mathematical Alphanumeric symbols for styled plain-text display in WeChat
|
|
20
20
|
- **AES-256-CBC encryption** — full WeChat callback encryption/decryption with SHA-1 signature verification and PKCS#7 padding validation
|
|
21
|
-
- **Webhook + polling fallback** —
|
|
21
|
+
- **Webhook + polling fallback** — webhook handler registered on framework's shared gateway for real-time callbacks, with automatic 30-second polling fallback for reliability
|
|
22
22
|
- **Dynamic KF account discovery** — KF account IDs (open_kfid) are automatically discovered from webhook callbacks with enable/disable/delete lifecycle management
|
|
23
23
|
- **Cursor-based incremental sync** — persists sync cursors per KF account with atomic file writes for crash safety
|
|
24
24
|
- **Access token auto-caching** — tokens cached in memory with hashed keys, automatic refresh 5 minutes before expiry, and auto-retry on token expiry
|
|
25
25
|
- **Multi-KF-account isolation** — each KF account gets its own session, cursor, and routing context with per-kfId processing mutex
|
|
26
|
-
- **DM policy control** — configurable access control: `open` or `
|
|
26
|
+
- **DM policy control** — configurable access control: `open`, `allowlist`, or `pairing` with security adapter (resolveDmPolicy, collectWarnings)
|
|
27
27
|
- **Text chunking** — automatically splits long replies to respect WeChat's 2000-character message size limit, with chunker declaration for framework integration
|
|
28
28
|
- **Session limit awareness** — detects and gracefully handles WeChat's 48-hour reply window and 5-message-per-window limits
|
|
29
29
|
- **Race condition safety** — per-kfId mutex and msgid deduplication prevent duplicate message processing
|
|
30
30
|
- **Human-like reply delays** — configurable typing delay simulation for natural conversation pacing
|
|
31
|
-
- **Graceful shutdown** — responds to abort signals with pre-check guards, cleanly stopping
|
|
31
|
+
- **Graceful shutdown** — responds to abort signals with pre-check guards, cleanly stopping polling loops
|
|
32
32
|
|
|
33
33
|
## Prerequisites
|
|
34
34
|
|
|
35
35
|
1. A **WeCom account** (企业微信) with admin privileges — [Register here](https://work.weixin.qq.com/)
|
|
36
36
|
2. At least one **Customer Service account** (客服账号) created in WeCom's WeChat Customer Service module
|
|
37
|
-
3. A **publicly accessible URL** for receiving callbacks — you can use [ngrok](https://ngrok.com/), [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), or a server with a public IP
|
|
37
|
+
3. A **publicly accessible URL** for receiving callbacks — you can use [Tailscale Funnel](https://docs.openclaw.ai/gateway/tailscale#tailscale) (recommended, built-in to OpenClaw Gateway), [ngrok](https://ngrok.com/), [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), or a server with a public IP
|
|
38
38
|
4. **OpenClaw Gateway** installed and running (`openclaw gateway start`)
|
|
39
39
|
|
|
40
40
|
## Installation
|
|
@@ -94,9 +94,8 @@ channels:
|
|
|
94
94
|
appSecret: "your-app-secret-here" # App Secret (self-built app or WeChat KF secret)
|
|
95
95
|
token: "your-callback-token" # Callback Token
|
|
96
96
|
encodingAESKey: "your-43-char-key" # Callback EncodingAESKey (43 characters)
|
|
97
|
-
webhookPort: 9999 # Local port for webhook server (default: 9999)
|
|
98
97
|
webhookPath: "/wechat-kf" # URL path for webhook (default: /wechat-kf)
|
|
99
|
-
dmPolicy: "open" # Access control: open | allowlist
|
|
98
|
+
dmPolicy: "open" # Access control: open | allowlist | pairing | disabled
|
|
100
99
|
# allowFrom: # Only used with dmPolicy: allowlist
|
|
101
100
|
# - "external_userid_1"
|
|
102
101
|
# - "external_userid_2"
|
|
@@ -111,9 +110,8 @@ channels:
|
|
|
111
110
|
| `appSecret` | string | **Yes** | — | Self-built app secret or WeChat KF secret |
|
|
112
111
|
| `token` | string | **Yes** | — | Webhook callback token |
|
|
113
112
|
| `encodingAESKey` | string | **Yes** | — | 43-char AES key for message encryption |
|
|
114
|
-
| `webhookPort` | integer | No | `9999` | Port for the HTTP webhook server |
|
|
115
113
|
| `webhookPath` | string | No | `/wechat-kf` | URL path for webhook callbacks |
|
|
116
|
-
| `dmPolicy` | string | No | `"open"` | `open` / `allowlist`
|
|
114
|
+
| `dmPolicy` | string | No | `"open"` | `open` / `allowlist` / `pairing` / `disabled` |
|
|
117
115
|
| `allowFrom` | string[] | No | `[]` | Allowed external_userids (when dmPolicy is `allowlist`) |
|
|
118
116
|
|
|
119
117
|
## Verification
|
|
@@ -122,13 +120,17 @@ channels:
|
|
|
122
120
|
```bash
|
|
123
121
|
openclaw gateway start
|
|
124
122
|
```
|
|
125
|
-
2. Expose the
|
|
123
|
+
2. Expose the gateway to the public internet (if not on a public server). Option A — Tailscale Funnel (built-in):
|
|
126
124
|
```bash
|
|
127
|
-
|
|
125
|
+
openclaw gateway --tailscale funnel --auth password
|
|
128
126
|
```
|
|
129
|
-
|
|
127
|
+
Option B — ngrok:
|
|
128
|
+
```bash
|
|
129
|
+
ngrok http <gateway-port>
|
|
130
|
+
```
|
|
131
|
+
3. Copy the HTTPS URL (e.g. `https://your-machine.tail1234.ts.net` or `https://xxxx.ngrok-free.app`) and set the callback URL in WeCom:
|
|
130
132
|
```
|
|
131
|
-
https
|
|
133
|
+
https://<your-public-host>/wechat-kf
|
|
132
134
|
```
|
|
133
135
|
4. WeCom sends a GET verification request — the plugin decrypts the `echostr` and responds automatically
|
|
134
136
|
5. Send a test message from WeChat (via the KF link) and confirm the agent responds
|
|
@@ -160,15 +162,18 @@ The agent can use the `message` tool to send messages:
|
|
|
160
162
|
| Video | Downloaded as MP4, saved as media attachment |
|
|
161
163
|
| File | Downloaded, saved as media attachment |
|
|
162
164
|
| Location | Converted to text: `[Location: name address]` |
|
|
163
|
-
| Link | Converted to text: `[Link: title url]`
|
|
164
|
-
| Mini Program | Converted to text with title and
|
|
165
|
+
| Link | Converted to text: `[Link: title url]` (with desc, pic_url) |
|
|
166
|
+
| Mini Program | Converted to text with title, appid, and pagepath |
|
|
165
167
|
| Channels (Video Account) | Converted to text with type, nickname, title |
|
|
168
|
+
| Channels Shop Product | Converted to text with product info |
|
|
169
|
+
| Channels Shop Order | Converted to text with order info |
|
|
170
|
+
| Note | Converted to text with note content |
|
|
166
171
|
| Business Card | Converted to text with userid |
|
|
167
172
|
| Forwarded Messages | Parsed and expanded into readable text |
|
|
168
173
|
|
|
169
174
|
### Supported outbound message types
|
|
170
175
|
|
|
171
|
-
Text, image, voice, video, file, and
|
|
176
|
+
Text, image, voice, video, file, link, location, mini-program, menu, business card, channel article, and raw JSON messages (`[[wechat_raw:...]]`). Rich message types are sent via `[[wechat_*:...]]` text directives. Media from any source (local files, HTTP URLs, file:// URIs) is loaded via the framework's loadWebMedia and uploaded to WeChat's temporary media storage before sending.
|
|
172
177
|
|
|
173
178
|
## Architecture
|
|
174
179
|
|
|
@@ -201,9 +206,9 @@ WeCom Server (Tencent)
|
|
|
201
206
|
| +-----------+-----------+
|
|
202
207
|
| v
|
|
203
208
|
| send-utils.ts
|
|
204
|
-
| formatText,
|
|
205
|
-
| uploadAndSendMedia
|
|
206
|
-
|
|
|
209
|
+
| formatText, mediaKindToWechatType
|
|
210
|
+
| detectMediaType, uploadAndSendMedia
|
|
211
|
+
| resolveThumbMediaId
|
|
207
212
|
| v
|
|
208
213
|
+--- send_msg API <--- api.ts
|
|
209
214
|
(JSON)
|
|
@@ -213,21 +218,22 @@ WeCom Server (Tencent)
|
|
|
213
218
|
|
|
214
219
|
| Module | Role |
|
|
215
220
|
| --------------------- | ------------------------------------------------------------------------------------------------- |
|
|
216
|
-
| `webhook.ts` | HTTP
|
|
221
|
+
| `webhook.ts` | HTTP handler (framework gateway) — GET verification, POST event handling, size/method guards |
|
|
217
222
|
| `crypto.ts` | AES-256-CBC encrypt/decrypt, SHA-1 signature, full PKCS#7 validation |
|
|
218
223
|
| `token.ts` | Access token cache with hashed key and auto-refresh |
|
|
219
|
-
| `api.ts` | WeCom API client (sync_msg, send_msg, media upload/download) with token auto-retry
|
|
224
|
+
| `api.ts` | WeCom API client (sync_msg, send_msg, sendRawMessage, media upload/download) with token auto-retry |
|
|
220
225
|
| `accounts.ts` | Dynamic KF account discovery, resolution, enable/disable/delete lifecycle |
|
|
221
226
|
| `bot.ts` | Message sync with mutex + dedup, DM policy check, event handling, agent dispatch |
|
|
222
|
-
| `monitor.ts` |
|
|
227
|
+
| `monitor.ts` | Shared context manager (setSharedContext/getSharedContext/waitForSharedContext/clearSharedContext) |
|
|
223
228
|
| `reply-dispatcher.ts` | Plugin-internal streaming reply delivery with chunking, formatting, delays |
|
|
224
229
|
| `outbound.ts` | Framework-driven outbound adapter with chunker declaration |
|
|
225
|
-
| `send-utils.ts` | Shared outbound utilities (formatText, detectMediaType, uploadAndSendMedia,
|
|
226
|
-
| `
|
|
230
|
+
| `send-utils.ts` | Shared outbound utilities (formatText, mediaKindToWechatType, detectMediaType, uploadAndSendMedia, resolveThumbMediaId) |
|
|
231
|
+
| `wechat-kf-directives.ts` | `[[wechat_*:...]]` directive parser for rich message types in agent replies |
|
|
227
232
|
| `constants.ts` | Shared constants (WECHAT_TEXT_CHUNK_LIMIT, timeouts, error codes) |
|
|
228
233
|
| `fs-utils.ts` | Atomic file operations (temp file + rename) |
|
|
229
234
|
| `unicode-format.ts` | Markdown to Unicode Mathematical styled text |
|
|
230
235
|
| `channel.ts` | ChannelPlugin interface with security adapter (resolveDmPolicy, collectWarnings) |
|
|
236
|
+
| `config-schema.ts` | JSON Schema for wechat-kf channel config validation |
|
|
231
237
|
| `runtime.ts` | OpenClaw runtime reference holder |
|
|
232
238
|
|
|
233
239
|
### State persistence
|
|
@@ -243,7 +249,7 @@ WeCom Server (Tencent)
|
|
|
243
249
|
- **5 messages per window** — you can send at most 5 replies before the user sends another message. The plugin detects this limit and logs accordingly.
|
|
244
250
|
- **Voice format** — inbound voice messages are AMR format; transcription depends on the OpenClaw agent's media processing capabilities.
|
|
245
251
|
- **Temporary media only** — uploaded media uses WeChat's temporary media API (3-day expiry). Permanent media upload is not implemented.
|
|
246
|
-
- **Single webhook endpoint** — all KF accounts share the same webhook
|
|
252
|
+
- **Single webhook endpoint** — all KF accounts share the same webhook path. This is by design (WeCom sends all callbacks to one URL per enterprise).
|
|
247
253
|
- **No group chat** — WeChat KF is direct messaging only. The plugin only supports `direct` chat type.
|
|
248
254
|
- **IP whitelist drift** — if your server's public IP changes, API calls will fail silently. Monitor your IP or use a static IP.
|
|
249
255
|
|
|
@@ -259,7 +265,7 @@ pnpm run build
|
|
|
259
265
|
# Type check
|
|
260
266
|
pnpm run typecheck
|
|
261
267
|
|
|
262
|
-
# Run tests (
|
|
268
|
+
# Run tests (~600 tests across 17 files)
|
|
263
269
|
pnpm test
|
|
264
270
|
|
|
265
271
|
# Watch mode
|
package/README.zh-CN.md
CHANGED
|
@@ -12,29 +12,29 @@
|
|
|
12
12
|
|
|
13
13
|
## 功能特性
|
|
14
14
|
|
|
15
|
-
- **入站消息处理** —
|
|
15
|
+
- **入站消息处理** — 接收文本、图片、语音、视频、文件、位置、链接、小程序、视频号、视频号商品、视频号订单、笔记、名片、合并转发消息等 14+ 种消息类型
|
|
16
16
|
- **事件处理** — 处理 enter_session(用户进入会话)、msg_send_fail(消息发送失败)、servicer_status_change(接待人员状态变更)事件
|
|
17
|
-
- **丰富的出站消息** —
|
|
18
|
-
- **媒体上传与下载** — 自动下载入站媒体(图片、语音、视频、文件),通过企业微信临时素材 API
|
|
17
|
+
- **丰富的出站消息** — 发送文本、图片、语音、视频、文件、链接、位置、小程序、菜单、名片和视频号文章消息
|
|
18
|
+
- **媒体上传与下载** — 自动下载入站媒体(图片、语音、视频、文件),通过企业微信临时素材 API 上传出站媒体;通过框架 loadWebMedia 支持所有 URL 格式(HTTP、file://、本地路径)
|
|
19
19
|
- **Markdown 转 Unicode 格式化** — 将 Markdown 粗体/斜体/标题/列表转换为 Unicode 数学字母符号,在微信中实现富文本效果
|
|
20
20
|
- **AES-256-CBC 加密** — 完整的微信回调加密/解密,包含 SHA-1 签名验证和 PKCS#7 填充校验
|
|
21
|
-
- **Webhook + 轮询兜底** —
|
|
21
|
+
- **Webhook + 轮询兜底** — webhook 处理器注册在框架共享网关上接收实时回调,同时提供 30 秒轮询兜底机制保证可靠性
|
|
22
22
|
- **动态客服账号发现** — 客服账号 ID(open_kfid)从 webhook 回调中自动发现,支持启用/禁用/删除生命周期管理
|
|
23
23
|
- **基于游标的增量同步** — 每个客服账号独立持久化同步游标,使用原子文件写入保证崩溃安全
|
|
24
24
|
- **Access Token 自动缓存** — Token 在内存中以哈希键缓存,过期前 5 分钟自动刷新,Token 过期时自动重试
|
|
25
25
|
- **多客服账号隔离** — 每个客服账号拥有独立的会话、游标和路由上下文,通过 per-kfId 互斥锁隔离处理
|
|
26
|
-
- **DM 策略控制** — 可配置的访问控制模式:`open`(开放)、`allowlist
|
|
26
|
+
- **DM 策略控制** — 可配置的访问控制模式:`open`(开放)、`allowlist`(白名单)或 `pairing`(配对),包含安全适配器
|
|
27
27
|
- **文本分块** — 自动按微信 2000 字符消息限制拆分长回复,并声明 chunker 供框架集成
|
|
28
28
|
- **会话限制感知** — 检测并优雅处理微信 48 小时回复窗口和 5 条消息限制
|
|
29
29
|
- **竞态条件安全** — per-kfId 互斥锁和 msgid 去重,防止消息重复处理
|
|
30
30
|
- **仿真回复延迟** — 可配置的打字延迟模拟,营造自然的对话节奏
|
|
31
|
-
- **优雅关停** —
|
|
31
|
+
- **优雅关停** — 响应中止信号,带前置检查守卫,干净地停止轮询循环
|
|
32
32
|
|
|
33
33
|
## 前提条件
|
|
34
34
|
|
|
35
35
|
1. 一个**企业微信账号**,且拥有管理员权限 — [注册地址](https://work.weixin.qq.com/)
|
|
36
36
|
2. 至少一个**客服账号**(在企业微信的「微信客服」模块中创建)
|
|
37
|
-
3. 一个**公网可访问的 URL**,用于接收回调 — 可使用 [ngrok](https://ngrok.com/)、[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) 或有公网 IP 的服务器
|
|
37
|
+
3. 一个**公网可访问的 URL**,用于接收回调 — 可使用 [Tailscale Funnel](https://docs.openclaw.ai/gateway/tailscale#tailscale)(推荐,OpenClaw Gateway 内置支持)、[ngrok](https://ngrok.com/)、[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) 或有公网 IP 的服务器
|
|
38
38
|
4. 已安装并运行 **OpenClaw Gateway**(`openclaw gateway start`)
|
|
39
39
|
|
|
40
40
|
微信客服 API 有**两种接入方式**,请根据实际情况选择:
|
|
@@ -204,9 +204,8 @@ channels:
|
|
|
204
204
|
appSecret: "your-app-secret-here" # 应用密钥(自建应用 Secret 或微信客服 Secret)
|
|
205
205
|
token: "your-callback-token" # 回调 Token
|
|
206
206
|
encodingAESKey: "your-43-char-key" # 回调 EncodingAESKey(43 位字符)
|
|
207
|
-
webhookPort: 9999 # Webhook 服务端口(默认:9999)
|
|
208
207
|
webhookPath: "/wechat-kf" # Webhook URL 路径(默认:/wechat-kf)
|
|
209
|
-
dmPolicy: "open" # 访问控制:open | allowlist
|
|
208
|
+
dmPolicy: "open" # 访问控制:open | allowlist | pairing | disabled
|
|
210
209
|
# allowFrom: # 仅在 dmPolicy 为 allowlist 时使用
|
|
211
210
|
# - "external_userid_1"
|
|
212
211
|
# - "external_userid_2"
|
|
@@ -221,9 +220,8 @@ channels:
|
|
|
221
220
|
| `appSecret` | string | **是** | — | 自建应用密钥或微信客服 Secret |
|
|
222
221
|
| `token` | string | **是** | — | Webhook 回调 Token |
|
|
223
222
|
| `encodingAESKey` | string | **是** | — | 43 位 AES 加密密钥 |
|
|
224
|
-
| `webhookPort` | integer | 否 | `9999` | Webhook HTTP 服务端口 |
|
|
225
223
|
| `webhookPath` | string | 否 | `/wechat-kf` | Webhook 回调 URL 路径 |
|
|
226
|
-
| `dmPolicy` | string | 否 | `"open"` | `open`(开放)/ `allowlist
|
|
224
|
+
| `dmPolicy` | string | 否 | `"open"` | `open`(开放)/ `allowlist`(白名单)/ `pairing`(配对)/ `disabled`(禁用) |
|
|
227
225
|
| `allowFrom` | string[] | 否 | `[]` | 允许的 external_userid 列表(dmPolicy 为 `allowlist` 时使用) |
|
|
228
226
|
|
|
229
227
|
## 验证
|
|
@@ -232,13 +230,17 @@ channels:
|
|
|
232
230
|
```bash
|
|
233
231
|
openclaw gateway start
|
|
234
232
|
```
|
|
235
|
-
2.
|
|
233
|
+
2. 将 Gateway 暴露到公网(如果不在公网服务器上)。方式 A — Tailscale Funnel(内置支持):
|
|
236
234
|
```bash
|
|
237
|
-
|
|
235
|
+
openclaw gateway --tailscale funnel --auth password
|
|
238
236
|
```
|
|
239
|
-
|
|
237
|
+
方式 B — ngrok:
|
|
238
|
+
```bash
|
|
239
|
+
ngrok http <gateway-port>
|
|
240
|
+
```
|
|
241
|
+
3. 复制 HTTPS URL(如 `https://your-machine.tail1234.ts.net` 或 `https://xxxx.ngrok-free.app`),在企业微信中设置回调地址:
|
|
240
242
|
```
|
|
241
|
-
https
|
|
243
|
+
https://<your-public-host>/wechat-kf
|
|
242
244
|
```
|
|
243
245
|
4. 企业微信发送 GET 验证请求 — 插件自动解密 `echostr` 并响应
|
|
244
246
|
5. 从微信中通过客服链接发送测试消息,确认 Agent 正常回复
|
|
@@ -270,15 +272,18 @@ Agent 可以使用 `message` 工具发送消息:
|
|
|
270
272
|
| 视频 | 下载为 MP4 格式,保存为媒体附件 |
|
|
271
273
|
| 文件 | 下载保存为媒体附件 |
|
|
272
274
|
| 位置 | 转换为文本:`[位置: 名称 地址]` |
|
|
273
|
-
| 链接 | 转换为文本:`[链接: 标题 URL]
|
|
274
|
-
| 小程序 |
|
|
275
|
+
| 链接 | 转换为文本:`[链接: 标题 URL]`(含 desc、pic_url)|
|
|
276
|
+
| 小程序 | 转换为文本,包含标题、appid 和 pagepath |
|
|
275
277
|
| 视频号 | 转换为文本,包含类型、昵称、标题 |
|
|
278
|
+
| 视频号商品 | 转换为文本,包含商品信息 |
|
|
279
|
+
| 视频号订单 | 转换为文本,包含订单信息 |
|
|
280
|
+
| 笔记 | 转换为文本,包含笔记内容 |
|
|
276
281
|
| 名片 | 转换为文本,包含 userid |
|
|
277
282
|
| 合并转发消息 | 解析并展开为可读文本 |
|
|
278
283
|
|
|
279
284
|
### 支持的出站消息类型
|
|
280
285
|
|
|
281
|
-
|
|
286
|
+
文本、图片、语音、视频、文件、链接、位置、小程序、菜单、名片、视频号文章和原始 JSON 消息(`[[wechat_raw:...]]`)。富消息类型通过 `[[wechat_*:...]]` 文本指令发送。所有来源的媒体(本地文件、HTTP URL、file:// URI)通过框架 loadWebMedia 加载后自动上传到微信临时素材存储。
|
|
282
287
|
|
|
283
288
|
## 架构
|
|
284
289
|
|
|
@@ -311,9 +316,9 @@ Agent 可以使用 `message` 工具发送消息:
|
|
|
311
316
|
| +-----------+-----------+
|
|
312
317
|
| v
|
|
313
318
|
| send-utils.ts
|
|
314
|
-
| formatText,
|
|
315
|
-
| uploadAndSendMedia
|
|
316
|
-
|
|
|
319
|
+
| formatText, mediaKindToWechatType
|
|
320
|
+
| detectMediaType, uploadAndSendMedia
|
|
321
|
+
| resolveThumbMediaId
|
|
317
322
|
| v
|
|
318
323
|
+--- send_msg API <-- api.ts
|
|
319
324
|
(JSON)
|
|
@@ -323,21 +328,22 @@ Agent 可以使用 `message` 工具发送消息:
|
|
|
323
328
|
|
|
324
329
|
| 模块 | 职责 |
|
|
325
330
|
| --------------------- | ------------------------------------------------------------------------------------- |
|
|
326
|
-
| `webhook.ts` | HTTP
|
|
331
|
+
| `webhook.ts` | HTTP 处理器(框架网关)— GET 验证、POST 事件处理、大小/方法守卫 |
|
|
327
332
|
| `crypto.ts` | AES-256-CBC 加密/解密、SHA-1 签名验证、PKCS#7 填充校验 |
|
|
328
333
|
| `token.ts` | Access Token 缓存,哈希键存储,自动刷新 |
|
|
329
|
-
| `api.ts` | 企业微信 API 客户端(sync_msg、send_msg、媒体上传/下载),Token 过期自动重试
|
|
334
|
+
| `api.ts` | 企业微信 API 客户端(sync_msg、send_msg、sendRawMessage、媒体上传/下载),Token 过期自动重试 |
|
|
330
335
|
| `accounts.ts` | 动态客服账号发现、解析、启用/禁用/删除生命周期 |
|
|
331
336
|
| `bot.ts` | 消息同步(互斥锁 + 去重)、DM 策略检查、事件处理、Agent 分发 |
|
|
332
|
-
| `monitor.ts` |
|
|
337
|
+
| `monitor.ts` | 共享上下文管理器(setSharedContext/getSharedContext/waitForSharedContext/clearSharedContext)|
|
|
333
338
|
| `reply-dispatcher.ts` | 插件内部流式回复投递,包含分块、格式化、延迟 |
|
|
334
339
|
| `outbound.ts` | 框架驱动的出站适配器,声明 chunker |
|
|
335
|
-
| `send-utils.ts` | 共享出站工具(formatText、detectMediaType、uploadAndSendMedia、
|
|
336
|
-
| `
|
|
340
|
+
| `send-utils.ts` | 共享出站工具(formatText、mediaKindToWechatType、detectMediaType、uploadAndSendMedia、resolveThumbMediaId) |
|
|
341
|
+
| `wechat-kf-directives.ts` | `[[wechat_*:...]]` 指令解析器,用于 agent 回复中的富文本消息类型 |
|
|
337
342
|
| `constants.ts` | 共享常量(WECHAT_TEXT_CHUNK_LIMIT、超时、错误码) |
|
|
338
343
|
| `fs-utils.ts` | 原子文件操作(临时文件 + 重命名) |
|
|
339
344
|
| `unicode-format.ts` | Markdown 转 Unicode 数学字母符号格式化 |
|
|
340
345
|
| `channel.ts` | ChannelPlugin 接口,包含安全适配器(resolveDmPolicy、collectWarnings) |
|
|
346
|
+
| `config-schema.ts` | wechat-kf 渠道配置的 JSON Schema 校验 |
|
|
341
347
|
| `runtime.ts` | OpenClaw 运行时引用持有 |
|
|
342
348
|
|
|
343
349
|
### 状态持久化
|
|
@@ -353,7 +359,7 @@ Agent 可以使用 `message` 工具发送消息:
|
|
|
353
359
|
- **5 条消息限制** — 在用户发送下一条消息前,最多只能发送 5 条回复。插件检测此限制并相应记录日志。
|
|
354
360
|
- **语音格式** — 入站语音消息为 AMR 格式;转录取决于 OpenClaw Agent 的媒体处理能力。
|
|
355
361
|
- **仅临时素材** — 上传的媒体使用微信临时素材 API(3 天有效期)。未实现永久素材上传。
|
|
356
|
-
- **单一 webhook 端点** — 所有客服账号共享同一个 webhook
|
|
362
|
+
- **单一 webhook 端点** — 所有客服账号共享同一个 webhook 路径。这是设计如此(企业微信为每个企业发送所有回调到同一个 URL)。
|
|
357
363
|
- **不支持群聊** — 微信客服仅支持一对一会话。插件仅支持 `direct` 聊天类型。
|
|
358
364
|
- **IP 白名单漂移** — 如果服务器公网 IP 变化,API 调用将静默失败。请监控 IP 或使用静态 IP。
|
|
359
365
|
|
|
@@ -369,7 +375,7 @@ pnpm run build
|
|
|
369
375
|
# 类型检查
|
|
370
376
|
pnpm run typecheck
|
|
371
377
|
|
|
372
|
-
# 运行测试(
|
|
378
|
+
# 运行测试(17 个文件,约 600 个测试)
|
|
373
379
|
pnpm test
|
|
374
380
|
|
|
375
381
|
# 监听模式
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WeChat KF (微信客服) OpenClaw Channel Plugin
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
export { sendTextMessage, syncMessages } from "./src/api.js";
|
|
4
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
6
|
+
export { sendBusinessCardMessage, sendCaLinkMessage, sendLocationMessage, sendMiniprogramMessage, sendMsgMenuMessage, sendTextMessage, syncMessages, } from "./src/api.js";
|
|
7
7
|
export { wechatKfPlugin } from "./src/channel.js";
|
|
8
8
|
export { computeSignature, decrypt, encrypt, verifySignature } from "./src/crypto.js";
|
|
9
9
|
export { getAccessToken } from "./src/token.js";
|
|
10
|
-
type OpenClawPluginApi = {
|
|
11
|
-
runtime: PluginRuntime;
|
|
12
|
-
registerChannel: (opts: {
|
|
13
|
-
plugin: typeof wechatKfPlugin;
|
|
14
|
-
}) => void;
|
|
15
|
-
};
|
|
16
10
|
declare const plugin: {
|
|
17
11
|
id: string;
|
|
18
12
|
name: string;
|
|
19
13
|
description: string;
|
|
20
|
-
configSchema:
|
|
21
|
-
|
|
22
|
-
additionalProperties: boolean;
|
|
23
|
-
properties: {};
|
|
24
|
-
};
|
|
25
|
-
register(api: OpenClawPluginApi): void;
|
|
14
|
+
configSchema: ReturnType<typeof emptyPluginConfigSchema>;
|
|
15
|
+
register: (api: OpenClawPluginApi) => void;
|
|
26
16
|
};
|
|
27
17
|
export default plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WeChat KF (微信客服) OpenClaw Channel Plugin
|
|
3
3
|
*/
|
|
4
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
4
5
|
import { wechatKfPlugin } from "./src/channel.js";
|
|
5
6
|
import { setRuntime } from "./src/runtime.js";
|
|
6
|
-
|
|
7
|
+
import { handleWechatKfWebhook } from "./src/webhook.js";
|
|
8
|
+
export { sendBusinessCardMessage, sendCaLinkMessage, sendLocationMessage, sendMiniprogramMessage, sendMsgMenuMessage, sendTextMessage, syncMessages, } from "./src/api.js";
|
|
7
9
|
export { wechatKfPlugin } from "./src/channel.js";
|
|
8
10
|
export { computeSignature, decrypt, encrypt, verifySignature } from "./src/crypto.js";
|
|
9
11
|
export { getAccessToken } from "./src/token.js";
|
|
@@ -11,13 +13,11 @@ const plugin = {
|
|
|
11
13
|
id: "wechat-kf",
|
|
12
14
|
name: "WeChat KF",
|
|
13
15
|
description: "WeChat Customer Service (企业微信客服) channel plugin",
|
|
14
|
-
|
|
15
|
-
// Channel config is handled via openclaw.plugin.json configSchema
|
|
16
|
-
// and the runtime schema in channel.ts → configSchema.
|
|
17
|
-
configSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
16
|
+
configSchema: emptyPluginConfigSchema(),
|
|
18
17
|
register(api) {
|
|
19
18
|
setRuntime(api.runtime);
|
|
20
19
|
api.registerChannel({ plugin: wechatKfPlugin });
|
|
20
|
+
api.registerHttpHandler(handleWechatKfWebhook);
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
export default plugin;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,kBAAkB,EAClB,eAAe,EACf,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACtF,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,MAAM,GAMR;IACF,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,iDAAiD;IAC9D,YAAY,EAAE,uBAAuB,EAAE;IACvC,QAAQ,CAAC,GAAsB;QAC7B,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,GAAG,CAAC,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;IACjD,CAAC;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/src/accounts.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Each openKfId becomes an independent accountId (like Telegram chat groups).
|
|
6
6
|
* Enterprise credentials (corpId, appSecret, token, encodingAESKey) are shared.
|
|
7
7
|
*/
|
|
8
|
-
import type { OpenClawConfig
|
|
8
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
9
|
+
import type { ResolvedWechatKfAccount, WechatKfConfig } from "./types.js";
|
|
9
10
|
export declare function setStateDir(dir: string): void;
|
|
10
11
|
export declare function getChannelConfig(cfg: OpenClawConfig): WechatKfConfig;
|
|
11
12
|
/** Register a dynamically discovered kfid */
|
package/dist/src/accounts.js
CHANGED
|
@@ -5,21 +5,56 @@
|
|
|
5
5
|
* Each openKfId becomes an independent accountId (like Telegram chat groups).
|
|
6
6
|
* Enterprise credentials (corpId, appSecret, token, encodingAESKey) are shared.
|
|
7
7
|
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
8
9
|
import { mkdir, readFile } from "node:fs/promises";
|
|
9
10
|
import { join } from "node:path";
|
|
11
|
+
import { CHANNEL_ID, DEFAULT_WEBHOOK_PATH, DISABLED_KFIDS_FILE, defaultStateDir, formatError, KFIDS_FILE, logTag, } from "./constants.js";
|
|
10
12
|
import { atomicWriteFile } from "./fs-utils.js";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_PATH = "/wechat-kf";
|
|
13
|
+
import { getSharedContext } from "./monitor.js";
|
|
13
14
|
/** In-memory set of discovered kfids */
|
|
14
15
|
const discoveredKfIds = new Set();
|
|
15
16
|
/** In-memory set of disabled kfids (persisted to disk) */
|
|
16
17
|
const disabledKfIds = new Set();
|
|
17
18
|
let stateDir = null;
|
|
19
|
+
let kfIdsPreloaded = false;
|
|
20
|
+
/** Synchronously preload persisted kfIds so listAccountIds returns them before loadKfIds runs */
|
|
21
|
+
function preloadKfIdsSync() {
|
|
22
|
+
if (kfIdsPreloaded)
|
|
23
|
+
return;
|
|
24
|
+
kfIdsPreloaded = true;
|
|
25
|
+
const dir = stateDir ?? defaultStateDir();
|
|
26
|
+
try {
|
|
27
|
+
const data = readFileSync(join(dir, KFIDS_FILE), "utf8");
|
|
28
|
+
const ids = JSON.parse(data);
|
|
29
|
+
if (Array.isArray(ids)) {
|
|
30
|
+
for (const id of ids)
|
|
31
|
+
discoveredKfIds.add(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
|
|
36
|
+
console.warn(`${logTag()} failed to preload kfids: ${formatError(err)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const data = readFileSync(join(dir, DISABLED_KFIDS_FILE), "utf8");
|
|
41
|
+
const ids = JSON.parse(data);
|
|
42
|
+
if (Array.isArray(ids)) {
|
|
43
|
+
for (const id of ids)
|
|
44
|
+
disabledKfIds.add(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
|
|
49
|
+
console.warn(`${logTag()} failed to preload disabled kfids: ${formatError(err)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
18
53
|
export function setStateDir(dir) {
|
|
19
54
|
stateDir = dir;
|
|
20
55
|
}
|
|
21
56
|
export function getChannelConfig(cfg) {
|
|
22
|
-
return (cfg.channels?.[
|
|
57
|
+
return (cfg.channels?.[CHANNEL_ID] ?? {});
|
|
23
58
|
}
|
|
24
59
|
/** Register a dynamically discovered kfid */
|
|
25
60
|
export async function registerKfId(kfId) {
|
|
@@ -101,27 +136,32 @@ function resolveKfId(kfId) {
|
|
|
101
136
|
/** Load persisted kfids from state dir */
|
|
102
137
|
export async function loadKfIds(dir) {
|
|
103
138
|
stateDir = dir;
|
|
139
|
+
kfIdsPreloaded = true;
|
|
104
140
|
try {
|
|
105
|
-
const data = await readFile(join(dir,
|
|
141
|
+
const data = await readFile(join(dir, KFIDS_FILE), "utf8");
|
|
106
142
|
const ids = JSON.parse(data);
|
|
107
143
|
if (Array.isArray(ids)) {
|
|
108
144
|
for (const id of ids)
|
|
109
145
|
discoveredKfIds.add(id);
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
|
-
catch {
|
|
113
|
-
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
|
|
150
|
+
console.warn(`${logTag()} failed to load kfids: ${formatError(err)}`);
|
|
151
|
+
}
|
|
114
152
|
}
|
|
115
153
|
try {
|
|
116
|
-
const data = await readFile(join(dir,
|
|
154
|
+
const data = await readFile(join(dir, DISABLED_KFIDS_FILE), "utf8");
|
|
117
155
|
const ids = JSON.parse(data);
|
|
118
156
|
if (Array.isArray(ids)) {
|
|
119
157
|
for (const id of ids)
|
|
120
158
|
disabledKfIds.add(id);
|
|
121
159
|
}
|
|
122
160
|
}
|
|
123
|
-
catch {
|
|
124
|
-
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
|
|
163
|
+
console.warn(`${logTag()} failed to load disabled kfids: ${formatError(err)}`);
|
|
164
|
+
}
|
|
125
165
|
}
|
|
126
166
|
}
|
|
127
167
|
/** Persist kfids to state dir */
|
|
@@ -130,10 +170,10 @@ async function persistKfIds() {
|
|
|
130
170
|
return;
|
|
131
171
|
try {
|
|
132
172
|
await mkdir(stateDir, { recursive: true });
|
|
133
|
-
await atomicWriteFile(join(stateDir,
|
|
173
|
+
await atomicWriteFile(join(stateDir, KFIDS_FILE), JSON.stringify(Array.from(discoveredKfIds)));
|
|
134
174
|
}
|
|
135
|
-
catch {
|
|
136
|
-
|
|
175
|
+
catch (err) {
|
|
176
|
+
getSharedContext()?.botCtx.log?.warn(`${logTag()} failed to persist kfids: ${formatError(err)}`);
|
|
137
177
|
}
|
|
138
178
|
}
|
|
139
179
|
/** Persist disabled kfids to state dir */
|
|
@@ -142,16 +182,18 @@ async function persistDisabledKfIds() {
|
|
|
142
182
|
return;
|
|
143
183
|
try {
|
|
144
184
|
await mkdir(stateDir, { recursive: true });
|
|
145
|
-
await atomicWriteFile(join(stateDir,
|
|
185
|
+
await atomicWriteFile(join(stateDir, DISABLED_KFIDS_FILE), JSON.stringify(Array.from(disabledKfIds)));
|
|
146
186
|
}
|
|
147
|
-
catch {
|
|
148
|
-
|
|
187
|
+
catch (err) {
|
|
188
|
+
getSharedContext()?.botCtx.log?.warn(`${logTag()} failed to persist disabled kfids: ${formatError(err)}`);
|
|
149
189
|
}
|
|
150
190
|
}
|
|
151
191
|
export function listAccountIds(_cfg) {
|
|
152
|
-
//
|
|
192
|
+
// "default" is always first — represents enterprise-level shared infrastructure.
|
|
193
|
+
// Real kfIds follow. When no kfIds are discovered yet, returns ["default"].
|
|
194
|
+
preloadKfIdsSync();
|
|
153
195
|
const ids = getEnabledKfIds();
|
|
154
|
-
return
|
|
196
|
+
return ["default", ...ids];
|
|
155
197
|
}
|
|
156
198
|
/**
|
|
157
199
|
* Recover the original case-sensitive kfId from the normalized (lowercased) accountId.
|
|
@@ -177,6 +219,7 @@ export function _reset() {
|
|
|
177
219
|
discoveredKfIds.clear();
|
|
178
220
|
disabledKfIds.clear();
|
|
179
221
|
stateDir = null;
|
|
222
|
+
kfIdsPreloaded = false;
|
|
180
223
|
}
|
|
181
224
|
export function resolveAccount(cfg, accountId) {
|
|
182
225
|
const config = getChannelConfig(cfg);
|
|
@@ -197,8 +240,7 @@ export function resolveAccount(cfg, accountId) {
|
|
|
197
240
|
token,
|
|
198
241
|
encodingAESKey,
|
|
199
242
|
openKfId: recoverOriginalKfId(id),
|
|
200
|
-
|
|
201
|
-
webhookPath: config.webhookPath ?? DEFAULT_PATH,
|
|
243
|
+
webhookPath: config.webhookPath ?? DEFAULT_WEBHOOK_PATH,
|
|
202
244
|
config,
|
|
203
245
|
};
|
|
204
246
|
}
|
package/dist/src/accounts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"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"}
|