@pawastation/wechat-kf 0.2.1 → 0.2.2

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
@@ -6,36 +6,65 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![OpenClaw](https://img.shields.io/badge/OpenClaw-channel%20plugin-blue.svg)](https://openclaw.dev)
8
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.
9
+ Let WeChat users chat with your OpenClaw AI agent via **WeChat Customer Service**.
10
10
 
11
11
  ---
12
12
 
13
13
  ## Features
14
14
 
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
- - **Event handling** — processes enter_session, msg_send_fail, and servicer_status_change events
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
- - **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** — webhook handler registered on framework's shared gateway for real-time callbacks, with automatic 30-second polling fallback for reliability
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`, `allowlist`, or `pairing` with security adapter (resolveDmPolicy, collectWarnings)
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 polling loops
15
+ - **No follow required** — users tap a link to start chatting, no need to follow any account first
16
+ - **Rich message types** — send and receive text, images, voice, video, files, link cards, mini-program cards, menus, and more
17
+ - **Markdown styling** — bold, lists, and headings render as Unicode-styled text in WeChat
18
+ - **Free to use** — the WeChat KF API itself is free; no enterprise verification required
19
+ - **Easy setup** — no domain verification, no IP whitelist; a free Cloudflare Tunnel is enough
20
+
21
+ ### Inbound message types
22
+
23
+ | Type | Notes |
24
+ | ----------------------- | ----------------------------------------------------------------------------------------- |
25
+ | Text | Plain text, including menu callbacks |
26
+ | Image | Image attachments |
27
+ | Voice | AMR-format voice messages |
28
+ | Video | Video attachments |
29
+ | File | Any file attachment |
30
+ | Link | Shared link cards |
31
+ | Mini-program | Mini-program cards |
32
+ | Location | Geographic location with coordinates |
33
+ | Merged messages | Forwarded message bundles |
34
+ | Channels shop product | Video Channel product cards |
35
+ | Channels shop order | Video Channel order messages |
36
+ | Channels post/live/card | Video Channel post, live, or profile card; only partial fields returned (nickname, title) |
37
+ | User notes | Type detected only; API does not expose note content |
38
+
39
+ ### Outbound message types
40
+
41
+ | Type | Notes |
42
+ | ----------------- | ------------------------------ |
43
+ | Text | Plain text |
44
+ | Image | Image attachments |
45
+ | Voice | AMR voice |
46
+ | Video | Video attachments |
47
+ | File | Any file |
48
+ | Link card | Rich link with thumbnail |
49
+ | Mini-program card | Mini-program jump card |
50
+ | Menu | Quick-reply menu buttons |
51
+ | Business card | Employee contact card |
52
+ | Location | Geographic location |
53
+ | Acquisition link | Customer acquisition link card |
54
+
55
+ ### WeChat-specific features
56
+
57
+ - **Markdown → Unicode styling** — bold, lists, and headings in agent replies are converted to Unicode-styled characters that render visually in WeChat (e.g., 𝗯𝗼𝗹𝗱 text, bullet symbols)
58
+ - **Message debounce** (`debounceMs`) — when a user sends multiple messages in rapid succession, the plugin waits until no new message arrives within the window, then delivers them all together to the agent as a single turn
59
+
60
+ ---
32
61
 
33
62
  ## Prerequisites
34
63
 
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 [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
- 4. **OpenClaw Gateway** installed and running (`openclaw gateway start`)
64
+ 1. A **WeCom account** (企业微信) — register with the WeCom app (personal accounts work; no real company required)
65
+ 2. **OpenClaw** installed and running see [OpenClaw docs](https://docs.openclaw.ai/)
66
+
67
+ ---
39
68
 
40
69
  ## Installation
41
70
 
@@ -43,254 +72,110 @@
43
72
  openclaw plugins install @pawastation/wechat-kf
44
73
  ```
45
74
 
46
- ## WeCom Setup Guide
75
+ ---
47
76
 
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.
77
+ ## Setup guide
49
78
 
50
- ### Method comparison
79
+ > The WeCom and WeChat KF admin consoles are in Chinese. For detailed step-by-step screenshots, see the [Chinese setup guide](./README.zh-CN.md#快速开始).
51
80
 
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 |
81
+ **Step 1 Install a tunnel**
63
82
 
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.
83
+ WeChat KF requires a public callback URL. Start a Cloudflare Tunnel:
65
84
 
66
- ### Required credentials
85
+ ```bash
86
+ cloudflared tunnel --url http://localhost:7860 # replace with your gateway port
87
+ ```
67
88
 
68
- Regardless of which method you choose, you need these four values for the plugin configuration:
89
+ Note the `https://xxxx.trycloudflare.com` URL it prints.
69
90
 
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) |
91
+ **Step 2 Get your Corp ID**
76
92
 
77
- ### Detailed setup instructions
93
+ In the [WeCom Admin console](https://work.weixin.qq.com/wework_admin/frame), go to **My Enterprise** and copy the **Enterprise ID** (format: `wwXXXXXXXXXXXXXXXX`).
78
94
 
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:
95
+ **Step 3 Create a KF account**
80
96
 
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
97
+ Open [kf.weixin.qq.com](https://kf.weixin.qq.com/) (scan QR with WeCom app), create a customer service account.
84
98
 
85
- ## Configuration
99
+ **Step 4 — Configure callback in WeChat KF admin**
86
100
 
87
- Add the following to your OpenClaw config (`~/.openclaw/openclaw.yaml` or via `openclaw config`):
101
+ In [kf.weixin.qq.com](https://kf.weixin.qq.com/) **Dev Config** **Get Started**:
102
+
103
+ 1. Set callback URL to `https://xxxx.trycloudflare.com/wechat-kf`
104
+ 2. Click **Random** to generate Token and EncodingAESKey
105
+
106
+ **Copy the Token and EncodingAESKey — do not click Save yet.** Configure OpenClaw first (next step), then come back to save.
107
+
108
+ **Step 5 — Configure OpenClaw (use a placeholder secret)**
109
+
110
+ Add the channel to OpenClaw config and enable it. Fill in the Token and EncodingAESKey from step 4; use a placeholder for `appSecret` for now. **Save the config.**
88
111
 
89
112
  ```yaml
90
113
  channels:
91
114
  wechat-kf:
92
115
  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
- webhookPath: "/wechat-kf" # URL path for webhook (default: /wechat-kf)
98
- dmPolicy: "open" # Access control: open | allowlist | pairing | disabled
99
- # allowFrom: # Only used with dmPolicy: allowlist
100
- # - "external_userid_1"
101
- # - "external_userid_2"
116
+ corpId: "wwXXXXXXXXXXXXXXXX"
117
+ appSecret: "placeholder" # replace in step 7
118
+ token: "" # from step 4
119
+ encodingAESKey: "" # from step 4
102
120
  ```
103
121
 
104
- ### Configuration reference
105
-
106
- | Field | Type | Required | Default | Description |
107
- | ---------------- | -------- | -------- | ------------ | ------------------------------------------------------- |
108
- | `enabled` | boolean | No | `false` | Enable the channel |
109
- | `corpId` | string | **Yes** | — | WeCom Corp ID |
110
- | `appSecret` | string | **Yes** | — | Self-built app secret or WeChat KF secret |
111
- | `token` | string | **Yes** | — | Webhook callback token |
112
- | `encodingAESKey` | string | **Yes** | — | 43-char AES key for message encryption |
113
- | `webhookPath` | string | No | `/wechat-kf` | URL path for webhook callbacks |
114
- | `dmPolicy` | string | No | `"open"` | `open` / `allowlist` / `pairing` / `disabled` |
115
- | `allowFrom` | string[] | No | `[]` | Allowed external_userids (when dmPolicy is `allowlist`) |
116
-
117
- ## Verification
118
-
119
- 1. Start the gateway:
120
- ```bash
121
- openclaw gateway start
122
- ```
123
- 2. Expose the gateway to the public internet (if not on a public server). Option A — Tailscale Funnel (built-in):
124
- ```bash
125
- openclaw gateway --tailscale funnel --auth password
126
- ```
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:
132
- ```
133
- https://<your-public-host>/wechat-kf
134
- ```
135
- 4. WeCom sends a GET verification request — the plugin decrypts the `echostr` and responds automatically
136
- 5. Send a test message from WeChat (via the KF link) and confirm the agent responds
137
-
138
- ## Usage
139
-
140
- Once configured and running, the plugin works automatically:
141
-
142
- 1. **Users** tap your Customer Service link in WeChat to start a conversation
143
- 2. **Inbound messages** arrive via webhook — the plugin decrypts, syncs messages via `sync_msg`, downloads any media, and dispatches to your OpenClaw agent
144
- 3. **The agent** processes the message and generates a reply
145
- 4. **Outbound replies** are sent back via the WeCom `send_msg` API, with markdown automatically converted to Unicode-styled plain text
146
-
147
- ### Sending messages from the agent
148
-
149
- The agent can use the `message` tool to send messages:
150
-
151
- - **Reply to current conversation** — omit `target`; the reply goes to whoever messaged
152
- - **Send to a specific user** — set `target` to the user's `external_userid`
153
- - **Send media** — use `filePath` or `media` to attach images, voice, video, or files
154
-
155
- ### Supported inbound message types
156
-
157
- | WeChat Type | How it's handled |
158
- | ------------------------ | --------------------------------------------------------------------- |
159
- | Text | Passed as-is to the agent |
160
- | Image | Downloaded, saved as media attachment, placeholder text sent to agent |
161
- | Voice | Downloaded as AMR, saved as media attachment |
162
- | Video | Downloaded as MP4, saved as media attachment |
163
- | File | Downloaded, saved as media attachment |
164
- | Location | Converted to text: `[Location: name address]` |
165
- | Link | Converted to text: `[Link: title url]` (with desc, pic_url) |
166
- | Mini Program | Converted to text with title, appid, and pagepath |
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 |
171
- | Business Card | Converted to text with userid |
172
- | Forwarded Messages | Parsed and expanded into readable text |
173
-
174
- ### Supported outbound message types
175
-
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.
177
-
178
- ## Architecture
122
+ Once saved, OpenClaw starts listening on the callback URL, so WeChat's verification request in the next step can succeed.
179
123
 
180
- ```
181
- WeChat User
182
- |
183
- v
184
- WeCom Server (Tencent)
185
- |
186
- |--- POST callback ---> webhook.ts ---> verify signature + size/method guards
187
- | (encrypted XML) | decrypt AES-256-CBC
188
- | | extract OpenKfId + Token
189
- | v
190
- | bot.ts ---> DM policy check
191
- | | per-kfId mutex + msgid dedup
192
- | | sync_msg API (pull messages)
193
- | | cursor-based incremental sync
194
- | | handle events (enter_session, etc.)
195
- | | download media attachments
196
- | v
197
- | OpenClaw Agent (dispatch via runtime)
198
- | |
199
- | +-----------+-----------+
200
- | v v
201
- | outbound.ts reply-dispatcher.ts
202
- | (framework-driven) (plugin-internal streaming)
203
- | chunker declaration markdown -> unicode
204
- | sendText / sendMedia text chunking + delay
205
- | | |
206
- | +-----------+-----------+
207
- | v
208
- | send-utils.ts
209
- | formatText, mediaKindToWechatType
210
- | detectMediaType, uploadAndSendMedia
211
- | resolveThumbMediaId
212
- | v
213
- +--- send_msg API <--- api.ts
214
- (JSON)
215
- ```
124
+ **Step 6 — Back to WeChat KF admin: verify and copy Secret**
216
125
 
217
- ### Key modules
218
-
219
- | Module | Role |
220
- | --------------------- | ------------------------------------------------------------------------------------------------- |
221
- | `webhook.ts` | HTTP handler (framework gateway) GET verification, POST event handling, size/method guards |
222
- | `crypto.ts` | AES-256-CBC encrypt/decrypt, SHA-1 signature, full PKCS#7 validation |
223
- | `token.ts` | Access token cache with hashed key and auto-refresh |
224
- | `api.ts` | WeCom API client (sync_msg, send_msg, sendRawMessage, media upload/download) with token auto-retry |
225
- | `accounts.ts` | Dynamic KF account discovery, resolution, enable/disable/delete lifecycle |
226
- | `bot.ts` | Message sync with mutex + dedup, DM policy check, event handling, agent dispatch |
227
- | `monitor.ts` | Shared context manager (setSharedContext/getSharedContext/waitForSharedContext/clearSharedContext) |
228
- | `reply-dispatcher.ts` | Plugin-internal streaming reply delivery with chunking, formatting, delays |
229
- | `outbound.ts` | Framework-driven outbound adapter with chunker declaration |
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 |
232
- | `constants.ts` | Shared constants (WECHAT_TEXT_CHUNK_LIMIT, timeouts, error codes) |
233
- | `fs-utils.ts` | Atomic file operations (temp file + rename) |
234
- | `unicode-format.ts` | Markdown to Unicode Mathematical styled text |
235
- | `channel.ts` | ChannelPlugin interface with security adapter (resolveDmPolicy, collectWarnings) |
236
- | `config-schema.ts` | JSON Schema for wechat-kf channel config validation |
237
- | `runtime.ts` | OpenClaw runtime reference holder |
238
-
239
- ### State persistence
240
-
241
- - **Sync cursors** — saved per KF account in `~/.openclaw/state/wechat-kf/wechat-kf-cursor-{kfid}.txt` (atomic writes)
242
- - **Discovered KF IDs** — saved in `~/.openclaw/state/wechat-kf/wechat-kf-kfids.json` (atomic writes)
243
- - **Access tokens** — in-memory only with hashed cache key (re-fetched on restart)
244
-
245
- ## Limitations / Known Issues
246
-
247
- - **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.
248
- - **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.
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.
250
- - **Voice format** — inbound voice messages are AMR format; transcription depends on the OpenClaw agent's media processing capabilities.
251
- - **Temporary media only** — uploaded media uses WeChat's temporary media API (3-day expiry). Permanent media upload is not implemented.
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).
253
- - **No group chat** — WeChat KF is direct messaging only. The plugin only supports `direct` chat type.
254
- - **IP whitelist drift** — if your server's public IP changes, API calls will fail silently. Monitor your IP or use a static IP.
255
-
256
- ## Development
126
+ Back in [kf.weixin.qq.com](https://kf.weixin.qq.com/) → **Dev Config**, click **Save** — WeChat sends a verification request; OpenClaw responds automatically and the config takes effect.
127
+
128
+ On the same page, copy the **App Secret**.
129
+
130
+ **Step 7Replace placeholder with real Secret**
131
+
132
+ Replace the placeholder `appSecret` in your OpenClaw config with the value copied in step 6. Save the config.
133
+
134
+ **Step 8 Get the contact link and test**
135
+
136
+ In the WeChat KF admin, copy the **contact link** for your KF account and open it in WeChat to start chatting with your agent.
137
+
138
+ **Step 9 (Recommended) Enable pairing mode**
139
+
140
+ To restrict access, set `dmPolicy: "pairing"`. New users receive a pairing code; approve with:
257
141
 
258
142
  ```bash
259
- # Install dependencies
260
- pnpm install
143
+ openclaw pairing approve wechat-kf <code>
144
+ ```
261
145
 
262
- # Build
263
- pnpm run build
146
+ ---
264
147
 
265
- # Type check
266
- pnpm run typecheck
148
+ ## Configuration reference
267
149
 
268
- # Run tests (~600 tests across 17 files)
269
- pnpm test
150
+ | Field | Type | Required | Default | Description |
151
+ | ---------------- | -------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------- |
152
+ | `enabled` | boolean | No | `false` | Enable the channel |
153
+ | `corpId` | string | **Yes** | — | WeCom Corp ID |
154
+ | `appSecret` | string | **Yes** | — | WeChat KF secret (from WeChat KF Admin > Dev Config) |
155
+ | `token` | string | **Yes** | — | Webhook callback token |
156
+ | `encodingAESKey` | string | **Yes** | — | 43-char AES key for message encryption |
157
+ | `webhookPath` | string | No | `/wechat-kf` | URL path for webhook callbacks |
158
+ | `dmPolicy` | string | No | `"open"` | `open` / `allowlist` / `pairing` / `disabled` |
159
+ | `allowFrom` | string[] | No | `[]` | Allowed external_userids (when `dmPolicy` is `allowlist`) |
160
+ | `debounceMs` | number | No | `2000` | Debounce window in ms (0–10000): waits until no new message in window, then dispatches all to agent; set to `0` to disable |
270
161
 
271
- # Watch mode
272
- pnpm run test:watch
162
+ ---
273
163
 
274
- # Lint (Biome)
275
- pnpm run lint
164
+ ## Limitations
276
165
 
277
- # Lint + auto-fix (Biome)
278
- pnpm run lint:fix
166
+ - **Public by design** — anyone with the contact link can send messages; this cannot be prevented at the platform level. Use `dmPolicy: "pairing"` or `"allowlist"` to control who the agent responds to.
167
+ - **48-hour reply window** — WeChat only allows replies within 48 hours of the user's last message.
168
+ - **5 messages per window** — at most 5 replies before the user sends another message.
169
+ - **Voice format** — inbound voice is AMR; transcription depends on your agent's media capabilities.
170
+ - **Tunnel URL changes** — free Cloudflare Tunnel URLs change on restart. Use a custom domain pointed to your server, a paid tunnel (e.g. Cloudflare Zero Trust), or a static IP for production.
279
171
 
280
- # Format (Biome)
281
- pnpm run format
172
+ ---
282
173
 
283
- # Combined Biome check (lint + format)
284
- pnpm run check
285
- ```
174
+ ## Developer docs
286
175
 
287
- ## Contributing
176
+ For architecture, module descriptions, development commands, and contributing workflow, see [CONTRIBUTING.md](./CONTRIBUTING.md).
288
177
 
289
- 1. Fork the repository
290
- 2. Create a feature branch (`git checkout -b feature/my-feature`)
291
- 3. Make your changes and add tests
292
- 4. Run `pnpm run check && pnpm run typecheck && pnpm test` to verify
293
- 5. Submit a pull request
178
+ ---
294
179
 
295
180
  ## License
296
181