@pawastation/wechat-kf 0.1.1

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +291 -0
  3. package/README.zh-CN.md +401 -0
  4. package/dist/index.d.ts +27 -0
  5. package/dist/index.js +24 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/accounts.d.ts +37 -0
  8. package/dist/src/accounts.js +205 -0
  9. package/dist/src/accounts.js.map +1 -0
  10. package/dist/src/api.d.ts +29 -0
  11. package/dist/src/api.js +172 -0
  12. package/dist/src/api.js.map +1 -0
  13. package/dist/src/bot.d.ts +35 -0
  14. package/dist/src/bot.js +379 -0
  15. package/dist/src/bot.js.map +1 -0
  16. package/dist/src/channel.d.ts +113 -0
  17. package/dist/src/channel.js +183 -0
  18. package/dist/src/channel.js.map +1 -0
  19. package/dist/src/chunk-utils.d.ts +18 -0
  20. package/dist/src/chunk-utils.js +58 -0
  21. package/dist/src/chunk-utils.js.map +1 -0
  22. package/dist/src/config-schema.d.ts +56 -0
  23. package/dist/src/config-schema.js +38 -0
  24. package/dist/src/config-schema.js.map +1 -0
  25. package/dist/src/constants.d.ts +19 -0
  26. package/dist/src/constants.js +20 -0
  27. package/dist/src/constants.js.map +1 -0
  28. package/dist/src/crypto.d.ts +18 -0
  29. package/dist/src/crypto.js +80 -0
  30. package/dist/src/crypto.js.map +1 -0
  31. package/dist/src/fs-utils.d.ts +7 -0
  32. package/dist/src/fs-utils.js +13 -0
  33. package/dist/src/fs-utils.js.map +1 -0
  34. package/dist/src/monitor.d.ts +18 -0
  35. package/dist/src/monitor.js +131 -0
  36. package/dist/src/monitor.js.map +1 -0
  37. package/dist/src/outbound.d.ts +66 -0
  38. package/dist/src/outbound.js +234 -0
  39. package/dist/src/outbound.js.map +1 -0
  40. package/dist/src/reply-dispatcher.d.ts +40 -0
  41. package/dist/src/reply-dispatcher.js +120 -0
  42. package/dist/src/reply-dispatcher.js.map +1 -0
  43. package/dist/src/runtime.d.ts +130 -0
  44. package/dist/src/runtime.js +22 -0
  45. package/dist/src/runtime.js.map +1 -0
  46. package/dist/src/send-utils.d.ts +30 -0
  47. package/dist/src/send-utils.js +89 -0
  48. package/dist/src/send-utils.js.map +1 -0
  49. package/dist/src/send.d.ts +7 -0
  50. package/dist/src/send.js +13 -0
  51. package/dist/src/send.js.map +1 -0
  52. package/dist/src/token.d.ts +8 -0
  53. package/dist/src/token.js +57 -0
  54. package/dist/src/token.js.map +1 -0
  55. package/dist/src/types.d.ts +173 -0
  56. package/dist/src/types.js +3 -0
  57. package/dist/src/types.js.map +1 -0
  58. package/dist/src/unicode-format.d.ts +26 -0
  59. package/dist/src/unicode-format.js +157 -0
  60. package/dist/src/unicode-format.js.map +1 -0
  61. package/dist/src/webhook.d.ts +22 -0
  62. package/dist/src/webhook.js +138 -0
  63. package/dist/src/webhook.js.map +1 -0
  64. package/dist/src/wechat-kf-directives.d.ts +34 -0
  65. package/dist/src/wechat-kf-directives.js +65 -0
  66. package/dist/src/wechat-kf-directives.js.map +1 -0
  67. package/index.ts +32 -0
  68. package/openclaw.plugin.json +31 -0
  69. package/package.json +91 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 pawaca
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.md ADDED
@@ -0,0 +1,291 @@
1
+ **English** | [中文](./README.zh-CN.md)
2
+
3
+ # WeChat KF for OpenClaw
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@pawastation%2Fwechat-kf.svg)](https://www.npmjs.com/package/@pawastation/wechat-kf)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![OpenClaw](https://img.shields.io/badge/OpenClaw-channel%20plugin-blue.svg)](https://openclaw.dev)
8
+
9
+ **WeChat Customer Service channel plugin for OpenClaw** — let WeChat users chat with your AI agent via the WeCom KF API. Zero runtime dependencies — uses only Node.js built-ins.
10
+
11
+ ---
12
+
13
+ ## Features
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 (11+ message types)
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 link messages back to users
18
+ - **Media upload & download** — automatically downloads inbound media and uploads outbound media via the WeCom temporary media API; supports HTTP URL download for outbound media
19
+ - **Markdown to Unicode formatting** — converts markdown bold/italic/headings/lists to Unicode Mathematical Alphanumeric symbols for styled plain-text display in WeChat
20
+ - **AES-256-CBC encryption** — full WeChat callback encryption/decryption with SHA-1 signature verification and PKCS#7 padding validation
21
+ - **Webhook + polling fallback** — HTTP webhook server for real-time callbacks, with automatic 30-second polling fallback for reliability; hardened with body size limits, method validation, and error responses
22
+ - **Dynamic KF account discovery** — KF account IDs (open_kfid) are automatically discovered from webhook callbacks with enable/disable/delete lifecycle management
23
+ - **Cursor-based incremental sync** — persists sync cursors per KF account with atomic file writes for crash safety
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
+ - **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 `allowlist` with security adapter (resolveDmPolicy, collectWarnings). `pairing` mode is not yet implemented.
27
+ - **Text chunking** — automatically splits long replies to respect WeChat's 2000-character message size limit, with chunker declaration for framework integration
28
+ - **Session limit awareness** — detects and gracefully handles WeChat's 48-hour reply window and 5-message-per-window limits
29
+ - **Race condition safety** — per-kfId mutex and msgid deduplication prevent duplicate message processing
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 the webhook server and polling
32
+
33
+ ## Prerequisites
34
+
35
+ 1. A **WeCom account** (企业微信) with admin privileges — [Register here](https://work.weixin.qq.com/)
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
38
+ 4. **OpenClaw Gateway** installed and running (`openclaw gateway start`)
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ openclaw plugins install @pawastation/wechat-kf
44
+ ```
45
+
46
+ ## WeCom Setup Guide
47
+
48
+ The WeChat KF API supports **two integration methods**. Both share the same underlying API (`sync_msg`, `send_msg`, etc.) and this plugin is fully compatible with either.
49
+
50
+ ### Method comparison
51
+
52
+ | | Method 1: WeCom Admin Self-built App | Method 2: WeChat KF Admin API Hosting |
53
+ | ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------- |
54
+ | **Admin console** | [WeCom Admin](https://work.weixin.qq.com/wework_admin/frame) | [WeChat KF Admin](https://work.weixin.qq.com/kf/) |
55
+ | **Secret source** | Self-built app secret | WeChat KF dedicated secret |
56
+ | **Callback config location** | WeCom Admin > WeChat KF > API > Callback settings | WeChat KF Admin > Dev Config > Callback settings |
57
+ | **Callback URL requirement** | Must use a verified corporate domain (trusted domain configured in WeCom Admin) | No restriction — any publicly accessible URL works |
58
+ | **Requires self-built app** | Yes — create an app and grant KF API permissions | No — configure directly in the KF admin console |
59
+ | **IP whitelist** | Required (self-built app security requirement) | Not required |
60
+ | **API scope** | Full — can call other WeCom APIs alongside KF | Limited to WeChat KF APIs only |
61
+ | **Best for** | Teams with existing WeCom integrations | Developers who only need AI customer service |
62
+ | **Complexity** | Higher — create app, grant permissions, configure IP whitelist | Lower — enable API and go |
63
+
64
+ > **Important:** The two methods are **mutually exclusive** — a KF account can only be managed through one method at a time. To switch, you must first unbind the current API integration.
65
+
66
+ ### Required credentials
67
+
68
+ Regardless of which method you choose, you need these four values for the plugin configuration:
69
+
70
+ | Credential | Where to find it |
71
+ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
72
+ | **Corp ID** (`corpId`) | WeCom Admin > My Enterprise > Enterprise ID (format: `wwXXXXXXXXXXXXXXXX`) |
73
+ | **App Secret** (`appSecret`) | Method 1: WeCom Admin > App Management > App Details > Secret; Method 2: WeChat KF Admin > Dev Config > Secret |
74
+ | **Token** (`token`) | Generated when configuring callback URL (any random string, up to 32 chars) |
75
+ | **EncodingAESKey** (`encodingAESKey`) | Generated when configuring callback URL (43-char string) |
76
+
77
+ ### Detailed setup instructions
78
+
79
+ Since the WeCom and WeChat KF admin consoles are entirely in Chinese, detailed step-by-step setup instructions are provided in the [Chinese guide](./README.zh-CN.md#企业微信客服接入指南). The guide covers:
80
+
81
+ - **Method 1** (企业微信后台自建应用): Creating a self-built app, granting KF permissions, configuring callback URL and IP whitelist (6 steps)
82
+ - **Method 2** (微信客服后台 API 托管): Enabling the API directly from the KF admin console (5 steps)
83
+ - A detailed comparison table of both methods
84
+
85
+ ## Configuration
86
+
87
+ Add the following to your OpenClaw config (`~/.openclaw/openclaw.yaml` or via `openclaw config`):
88
+
89
+ ```yaml
90
+ channels:
91
+ wechat-kf:
92
+ enabled: true
93
+ corpId: "wwXXXXXXXXXXXXXXXX" # Your Corp ID
94
+ appSecret: "your-app-secret-here" # App Secret (self-built app or WeChat KF secret)
95
+ token: "your-callback-token" # Callback Token
96
+ encodingAESKey: "your-43-char-key" # Callback EncodingAESKey (43 characters)
97
+ webhookPort: 9999 # Local port for webhook server (default: 9999)
98
+ webhookPath: "/wechat-kf" # URL path for webhook (default: /wechat-kf)
99
+ dmPolicy: "open" # Access control: open | allowlist (pairing: not yet implemented)
100
+ # allowFrom: # Only used with dmPolicy: allowlist
101
+ # - "external_userid_1"
102
+ # - "external_userid_2"
103
+ ```
104
+
105
+ ### Configuration reference
106
+
107
+ | Field | Type | Required | Default | Description |
108
+ | ---------------- | -------- | -------- | ------------ | ------------------------------------------------------- |
109
+ | `enabled` | boolean | No | `false` | Enable the channel |
110
+ | `corpId` | string | **Yes** | — | WeCom Corp ID |
111
+ | `appSecret` | string | **Yes** | — | Self-built app secret or WeChat KF secret |
112
+ | `token` | string | **Yes** | — | Webhook callback token |
113
+ | `encodingAESKey` | string | **Yes** | — | 43-char AES key for message encryption |
114
+ | `webhookPort` | integer | No | `9999` | Port for the HTTP webhook server |
115
+ | `webhookPath` | string | No | `/wechat-kf` | URL path for webhook callbacks |
116
+ | `dmPolicy` | string | No | `"open"` | `open` / `allowlist` (`pairing` not yet implemented) |
117
+ | `allowFrom` | string[] | No | `[]` | Allowed external_userids (when dmPolicy is `allowlist`) |
118
+
119
+ ## Verification
120
+
121
+ 1. Start the gateway:
122
+ ```bash
123
+ openclaw gateway start
124
+ ```
125
+ 2. Expose the webhook port (if not on a public server):
126
+ ```bash
127
+ ngrok http 9999
128
+ ```
129
+ 3. Copy the HTTPS URL (e.g. `https://xxxx.ngrok-free.app`) and set the callback URL in WeCom:
130
+ ```
131
+ https://xxxx.ngrok-free.app/wechat-kf
132
+ ```
133
+ 4. WeCom sends a GET verification request — the plugin decrypts the `echostr` and responds automatically
134
+ 5. Send a test message from WeChat (via the KF link) and confirm the agent responds
135
+
136
+ ## Usage
137
+
138
+ Once configured and running, the plugin works automatically:
139
+
140
+ 1. **Users** tap your Customer Service link in WeChat to start a conversation
141
+ 2. **Inbound messages** arrive via webhook — the plugin decrypts, syncs messages via `sync_msg`, downloads any media, and dispatches to your OpenClaw agent
142
+ 3. **The agent** processes the message and generates a reply
143
+ 4. **Outbound replies** are sent back via the WeCom `send_msg` API, with markdown automatically converted to Unicode-styled plain text
144
+
145
+ ### Sending messages from the agent
146
+
147
+ The agent can use the `message` tool to send messages:
148
+
149
+ - **Reply to current conversation** — omit `target`; the reply goes to whoever messaged
150
+ - **Send to a specific user** — set `target` to the user's `external_userid`
151
+ - **Send media** — use `filePath` or `media` to attach images, voice, video, or files
152
+
153
+ ### Supported inbound message types
154
+
155
+ | WeChat Type | How it's handled |
156
+ | ------------------------ | --------------------------------------------------------------------- |
157
+ | Text | Passed as-is to the agent |
158
+ | Image | Downloaded, saved as media attachment, placeholder text sent to agent |
159
+ | Voice | Downloaded as AMR, saved as media attachment |
160
+ | Video | Downloaded as MP4, saved as media attachment |
161
+ | File | Downloaded, saved as media attachment |
162
+ | Location | Converted to text: `[Location: name address]` |
163
+ | Link | Converted to text: `[Link: title url]` |
164
+ | Mini Program | Converted to text with title and appid |
165
+ | Channels (Video Account) | Converted to text with type, nickname, title |
166
+ | Business Card | Converted to text with userid |
167
+ | Forwarded Messages | Parsed and expanded into readable text |
168
+
169
+ ### Supported outbound message types
170
+
171
+ Text, image, voice, video, file, and link messages. Local files are automatically uploaded to WeChat's temporary media storage before sending.
172
+
173
+ ## Architecture
174
+
175
+ ```
176
+ WeChat User
177
+ |
178
+ v
179
+ WeCom Server (Tencent)
180
+ |
181
+ |--- POST callback ---> webhook.ts ---> verify signature + size/method guards
182
+ | (encrypted XML) | decrypt AES-256-CBC
183
+ | | extract OpenKfId + Token
184
+ | v
185
+ | bot.ts ---> DM policy check
186
+ | | per-kfId mutex + msgid dedup
187
+ | | sync_msg API (pull messages)
188
+ | | cursor-based incremental sync
189
+ | | handle events (enter_session, etc.)
190
+ | | download media attachments
191
+ | v
192
+ | OpenClaw Agent (dispatch via runtime)
193
+ | |
194
+ | +-----------+-----------+
195
+ | v v
196
+ | outbound.ts reply-dispatcher.ts
197
+ | (framework-driven) (plugin-internal streaming)
198
+ | chunker declaration markdown -> unicode
199
+ | sendText / sendMedia text chunking + delay
200
+ | | |
201
+ | +-----------+-----------+
202
+ | v
203
+ | send-utils.ts
204
+ | formatText, detectMediaType
205
+ | uploadAndSendMedia
206
+ | downloadMediaFromUrl
207
+ | v
208
+ +--- send_msg API <--- api.ts
209
+ (JSON)
210
+ ```
211
+
212
+ ### Key modules
213
+
214
+ | Module | Role |
215
+ | --------------------- | ------------------------------------------------------------------------------------------------- |
216
+ | `webhook.ts` | HTTP server — GET verification, POST event handling, size/method guards |
217
+ | `crypto.ts` | AES-256-CBC encrypt/decrypt, SHA-1 signature, full PKCS#7 validation |
218
+ | `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 |
220
+ | `accounts.ts` | Dynamic KF account discovery, resolution, enable/disable/delete lifecycle |
221
+ | `bot.ts` | Message sync with mutex + dedup, DM policy check, event handling, agent dispatch |
222
+ | `monitor.ts` | Webhook + polling lifecycle management with AbortSignal guards |
223
+ | `reply-dispatcher.ts` | Plugin-internal streaming reply delivery with chunking, formatting, delays |
224
+ | `outbound.ts` | Framework-driven outbound adapter with chunker declaration |
225
+ | `send-utils.ts` | Shared outbound utilities (formatText, detectMediaType, uploadAndSendMedia, downloadMediaFromUrl) |
226
+ | `chunk-utils.ts` | Text chunking with natural boundary splitting (newline, whitespace, hard-cut) |
227
+ | `constants.ts` | Shared constants (WECHAT_TEXT_CHUNK_LIMIT, timeouts, error codes) |
228
+ | `fs-utils.ts` | Atomic file operations (temp file + rename) |
229
+ | `unicode-format.ts` | Markdown to Unicode Mathematical styled text |
230
+ | `channel.ts` | ChannelPlugin interface with security adapter (resolveDmPolicy, collectWarnings) |
231
+ | `runtime.ts` | OpenClaw runtime reference holder |
232
+
233
+ ### State persistence
234
+
235
+ - **Sync cursors** — saved per KF account in `~/.openclaw/state/wechat-kf/wechat-kf-cursor-{kfid}.txt` (atomic writes)
236
+ - **Discovered KF IDs** — saved in `~/.openclaw/state/wechat-kf/wechat-kf-kfids.json` (atomic writes)
237
+ - **Access tokens** — in-memory only with hashed cache key (re-fetched on restart)
238
+
239
+ ## Limitations / Known Issues
240
+
241
+ - **Open access by design** — WeChat Customer Service is inherently a public-facing service within the WeChat ecosystem. Anyone who obtains the KF contact link (URL or QR code) can send messages to your KF account — this cannot be prevented at the WeChat platform level. The plugin's `dmPolicy: "allowlist"` mode can restrict which users the agent actually responds to (non-allowlisted messages are silently dropped), but it cannot prevent unknown users from reaching the KF entry point itself. Please be aware of this public-facing nature when deploying in production.
242
+ - **48-hour reply window** — WeChat only allows replies within 48 hours of the user's last message. The plugin detects this (errcode 95026) and logs a clear warning.
243
+ - **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
+ - **Voice format** — inbound voice messages are AMR format; transcription depends on the OpenClaw agent's media processing capabilities.
245
+ - **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 port and path. This is by design (WeCom sends all callbacks to one URL per enterprise).
247
+ - **No group chat** — WeChat KF is direct messaging only. The plugin only supports `direct` chat type.
248
+ - **IP whitelist drift** — if your server's public IP changes, API calls will fail silently. Monitor your IP or use a static IP.
249
+
250
+ ## Development
251
+
252
+ ```bash
253
+ # Install dependencies
254
+ pnpm install
255
+
256
+ # Build
257
+ pnpm run build
258
+
259
+ # Type check
260
+ pnpm run typecheck
261
+
262
+ # Run tests (363 tests across 16 files)
263
+ pnpm test
264
+
265
+ # Watch mode
266
+ pnpm run test:watch
267
+
268
+ # Lint (Biome)
269
+ pnpm run lint
270
+
271
+ # Lint + auto-fix (Biome)
272
+ pnpm run lint:fix
273
+
274
+ # Format (Biome)
275
+ pnpm run format
276
+
277
+ # Combined Biome check (lint + format)
278
+ pnpm run check
279
+ ```
280
+
281
+ ## Contributing
282
+
283
+ 1. Fork the repository
284
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
285
+ 3. Make your changes and add tests
286
+ 4. Run `pnpm run check && pnpm run typecheck && pnpm test` to verify
287
+ 5. Submit a pull request
288
+
289
+ ## License
290
+
291
+ MIT