openclaw-wechat-channel 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,32 +27,57 @@ openclaw plugins install openclaw-wechat-channel
27
27
  ### Upgrade
28
28
 
29
29
  ```bash
30
- openclaw plugins update wechat
30
+ openclaw plugins update openclaw-wechat-channel
31
31
  ```
32
32
 
33
33
  ### Configuration
34
34
 
35
35
  ```bash
36
+ # Set Wechatify Server proxy URL (required)
37
+ openclaw config set channels.wechat.proxyUrl "http://your-proxy-server:19088"
38
+
36
39
  # Set API Key (required)
37
40
  openclaw config set channels.wechat.apiKey "your-api-key"
38
41
 
42
+ # Set webhook host — your OpenClaw server's public IP or domain (optional, auto-detected if omitted)
43
+ openclaw config set channels.wechat.webhookHost "your-public-ip"
44
+
39
45
  # Set webhook port (optional, default: 18790)
40
46
  openclaw config set channels.wechat.webhookPort 18790
41
47
 
48
+ # Set webhook path (optional, default: /webhook/wechat)
49
+ openclaw config set channels.wechat.webhookPath "/webhook/wechat"
50
+
42
51
  # Enable the channel
43
52
  openclaw config set channels.wechat.enabled true
44
53
  ```
45
54
 
46
- ### Configuration Options
47
-
48
- ```yaml
49
- # ~/.openclaw/openclaw.json
50
- channels:
51
- wechat:
52
- enabled: true
53
- apiKey: your-api-key # Required Wechatify Server API Key
54
- webhookPort: 18790 # Optional Webhook listen port (default: 18790)
55
- webhookUrl: "" # Optional Custom webhook callback URL
55
+ ### Configuration Reference
56
+
57
+ | Parameter | Required | Default | Description |
58
+ |---|---|---|---|
59
+ | `apiKey` | Yes | — | Wechatify Server API Key |
60
+ | `proxyUrl` | Yes | — | Wechatify Server address, e.g. `http://your-server:19088` |
61
+ | `webhookHost` | No | auto-detect | Public IP or domain of the OpenClaw server for receiving callbacks |
62
+ | `webhookPort` | No | `18790` | Port for the webhook listener |
63
+ | `webhookPath` | No | `/webhook/wechat` | Path for the webhook endpoint |
64
+ | `enabled` | No | `true` | Enable/disable the channel |
65
+
66
+ Example (`~/.openclaw/openclaw.json`):
67
+
68
+ ```json
69
+ {
70
+ "channels": {
71
+ "wechat": {
72
+ "enabled": true,
73
+ "apiKey": "your-api-key",
74
+ "proxyUrl": "http://your-proxy-server:19088",
75
+ "webhookHost": "your-public-ip",
76
+ "webhookPort": 18790,
77
+ "webhookPath": "/webhook/wechat"
78
+ }
79
+ }
80
+ }
56
81
  ```
57
82
 
58
83
  ### Features
@@ -64,16 +89,25 @@ channels:
64
89
 
65
90
  ### Multi-Account
66
91
 
67
- ```yaml
68
- channels:
69
- wechat:
70
- accounts:
71
- work:
72
- apiKey: work-api-key
73
- webhookPort: 18790
74
- personal:
75
- apiKey: personal-api-key
76
- webhookPort: 18791
92
+ ```json
93
+ {
94
+ "channels": {
95
+ "wechat": {
96
+ "accounts": {
97
+ "work": {
98
+ "apiKey": "work-api-key",
99
+ "proxyUrl": "http://proxy-1:19088",
100
+ "webhookPort": 18790
101
+ },
102
+ "personal": {
103
+ "apiKey": "personal-api-key",
104
+ "proxyUrl": "http://proxy-2:19088",
105
+ "webhookPort": 18791
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
77
111
  ```
78
112
 
79
113
  ---
@@ -99,32 +133,57 @@ openclaw plugins install openclaw-wechat-channel
99
133
  ### 升级
100
134
 
101
135
  ```bash
102
- openclaw plugins update wechat
136
+ openclaw plugins update openclaw-wechat-channel
103
137
  ```
104
138
 
105
139
  ### 配置
106
140
 
107
141
  ```bash
142
+ # 设置 Wechatify Server 代理地址(必填)
143
+ openclaw config set channels.wechat.proxyUrl "http://your-proxy-server:19088"
144
+
108
145
  # 设置 API Key(必填)
109
146
  openclaw config set channels.wechat.apiKey "your-api-key"
110
147
 
111
- # 设置 webhook 端口(可选,默认 18790)
148
+ # 设置 Webhook 公网地址 — OpenClaw 服务器的公网 IP 或域名(可选,不填则自动检测)
149
+ openclaw config set channels.wechat.webhookHost "your-public-ip"
150
+
151
+ # 设置 Webhook 端口(可选,默认 18790)
112
152
  openclaw config set channels.wechat.webhookPort 18790
113
153
 
154
+ # 设置 Webhook 路径(可选,默认 /webhook/wechat)
155
+ openclaw config set channels.wechat.webhookPath "/webhook/wechat"
156
+
114
157
  # 启用通道
115
158
  openclaw config set channels.wechat.enabled true
116
159
  ```
117
160
 
118
- ### 配置选项
119
-
120
- ```yaml
121
- # ~/.openclaw/openclaw.json
122
- channels:
123
- wechat:
124
- enabled: true
125
- apiKey: your-api-key # 必填 Wechatify Server API Key
126
- webhookPort: 18790 # 可选 Webhook 监听端口(默认 18790)
127
- webhookUrl: "" # 可选 自定义 Webhook 回调地址
161
+ ### 配置参考
162
+
163
+ | 参数 | 必填 | 默认值 | 说明 |
164
+ |---|---|---|---|
165
+ | `apiKey` | 是 | — | Wechatify Server 的 API Key |
166
+ | `proxyUrl` | 是 | — | Wechatify Server 地址,如 `http://your-server:19088` |
167
+ | `webhookHost` | 否 | 自动检测 | OpenClaw 服务器的公网 IP 或域名,用于接收消息回调 |
168
+ | `webhookPort` | | `18790` | Webhook 监听端口 |
169
+ | `webhookPath` | | `/webhook/wechat` | Webhook 路径 |
170
+ | `enabled` | | `true` | 是否启用通道 |
171
+
172
+ 配置示例(`~/.openclaw/openclaw.json`):
173
+
174
+ ```json
175
+ {
176
+ "channels": {
177
+ "wechat": {
178
+ "enabled": true,
179
+ "apiKey": "your-api-key",
180
+ "proxyUrl": "http://your-proxy-server:19088",
181
+ "webhookHost": "your-public-ip",
182
+ "webhookPort": 18790,
183
+ "webhookPath": "/webhook/wechat"
184
+ }
185
+ }
186
+ }
128
187
  ```
129
188
 
130
189
  ### 功能
@@ -136,16 +195,25 @@ channels:
136
195
 
137
196
  ### 多账号配置
138
197
 
139
- ```yaml
140
- channels:
141
- wechat:
142
- accounts:
143
- work:
144
- apiKey: work-api-key
145
- webhookPort: 18790
146
- personal:
147
- apiKey: personal-api-key
148
- webhookPort: 18791
198
+ ```json
199
+ {
200
+ "channels": {
201
+ "wechat": {
202
+ "accounts": {
203
+ "work": {
204
+ "apiKey": "work-api-key",
205
+ "proxyUrl": "http://proxy-1:19088",
206
+ "webhookPort": 18790
207
+ },
208
+ "personal": {
209
+ "apiKey": "personal-api-key",
210
+ "proxyUrl": "http://proxy-2:19088",
211
+ "webhookPort": 18791
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
149
217
  ```
150
218
 
151
219
  ---
package/dist/index.d.mts CHANGED
@@ -17,6 +17,7 @@ interface WechatAccountConfig {
17
17
  wcId?: string;
18
18
  nickName?: string;
19
19
  configured?: boolean;
20
+ allowUsers?: string[];
20
21
  }
21
22
  interface WechatConfig {
22
23
  enabled?: boolean;
@@ -25,6 +26,7 @@ interface WechatConfig {
25
26
  webhookHost?: string;
26
27
  webhookPort?: number;
27
28
  webhookPath?: string;
29
+ allowUsers?: string[];
28
30
  accounts?: Record<string, WechatAccountConfig | undefined>;
29
31
  }
30
32
  //#endregion
@@ -43,6 +45,7 @@ interface ResolvedWeChatAccount {
43
45
  webhookHost?: string;
44
46
  webhookPort: number;
45
47
  webhookPath: string;
48
+ allowUsers?: string[];
46
49
  config: WechatAccountConfig;
47
50
  }
48
51
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  import { DEFAULT_ACCOUNT_ID, createReplyPrefixContext, emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
2
  import http from "node:http";
3
+ import { H3, defineEventHandler, readBody } from "h3";
4
+ import { toNodeHandler } from "h3/node";
3
5
 
4
6
  //#region src/proxy-client.ts
5
7
  var ProxyClient = class {
@@ -105,14 +107,17 @@ function getWeChatRuntime() {
105
107
  //#region src/reply-dispatcher.ts
106
108
  function createWeChatReplyDispatcher(params) {
107
109
  const core = getWeChatRuntime();
108
- const { cfg, agentId, runtime, apiKey, replyTo, accountId } = params;
110
+ const { cfg, agentId, runtime, apiKey, proxyUrl, replyTo, accountId } = params;
109
111
  const prefixContext = createReplyPrefixContext({
110
112
  cfg,
111
113
  agentId
112
114
  });
113
115
  const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "wechat", accountId, { fallbackLimit: 2e3 });
114
116
  const chunkMode = core.channel.text.resolveChunkMode(cfg, "wechat");
115
- const client = new ProxyClient({ apiKey });
117
+ const client = new ProxyClient({
118
+ apiKey,
119
+ baseUrl: proxyUrl
120
+ });
116
121
  const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
117
122
  responsePrefix: prefixContext.responsePrefix,
118
123
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
@@ -155,6 +160,7 @@ const DEDUP_WINDOW_MS = 1800 * 1e3;
155
160
  const DEDUP_MAX_SIZE = 1e3;
156
161
  const DEDUP_CLEANUP_INTERVAL_MS = 300 * 1e3;
157
162
  let lastCleanup = Date.now();
163
+ const notifiedNonWhitelistUsers = /* @__PURE__ */ new Set();
158
164
  function tryRecordMessage(messageId) {
159
165
  const now = Date.now();
160
166
  if (now - lastCleanup > DEDUP_CLEANUP_INTERVAL_MS) {
@@ -177,6 +183,18 @@ async function handleWeChatMessage(params) {
177
183
  log(`wechat: skipping duplicate message ${message.id}`);
178
184
  return;
179
185
  }
186
+ const allowUsers = account.allowUsers;
187
+ if (allowUsers && allowUsers.length > 0 && message.sender.id !== account.wcId && !allowUsers.includes(message.sender.id)) {
188
+ log(`wechat[${accountId}]: ignoring message from ${message.sender.id} (not in allowUsers)`);
189
+ if (account.wcId && !notifiedNonWhitelistUsers.has(message.sender.id)) {
190
+ notifiedNonWhitelistUsers.add(message.sender.id);
191
+ await new ProxyClient({
192
+ apiKey: account.apiKey,
193
+ baseUrl: account.proxyUrl
194
+ }).sendText(account.wcId, `‼️ ${message.sender.id} 不在白名单中,配置 allowUsers 后即可回复`).catch(() => {});
195
+ }
196
+ return;
197
+ }
180
198
  const isGroup = !!message.group;
181
199
  log(`wechat[${accountId}]: received ${message.type} from ${message.sender.id}${isGroup ? ` in group ${message.group.id}` : ""}`);
182
200
  if (message.type !== "text") {
@@ -240,6 +258,7 @@ async function handleWeChatMessage(params) {
240
258
  agentId: route.agentId,
241
259
  runtime,
242
260
  apiKey: account.apiKey,
261
+ proxyUrl: account.proxyUrl,
243
262
  replyTo,
244
263
  accountId: account.accountId
245
264
  });
@@ -260,23 +279,17 @@ async function handleWeChatMessage(params) {
260
279
  //#endregion
261
280
  //#region src/callback-server.ts
262
281
  async function startCallbackServer(options) {
263
- const { port, onMessage, abortSignal } = options;
264
- const server = http.createServer((req, res) => {
265
- if ((req.url?.split("?")[0] || "") === "/webhook/wechat" && req.method === "POST") {
266
- let body = "";
267
- req.on("data", (chunk) => body += chunk);
268
- req.on("end", async () => {
269
- try {
270
- const message = convertToMessageContext(JSON.parse(body));
271
- if (message) onMessage(message);
272
- res.writeHead(200).end("OK");
273
- } catch (err) {
274
- console.error("Failed to process webhook:", err);
275
- res.writeHead(400).end("Bad Request");
276
- }
277
- });
278
- } else res.writeHead(404).end("Not Found");
279
- });
282
+ const { port, apiKey, onMessage, abortSignal } = options;
283
+ const app = new H3();
284
+ app.post("/webhook/wechat", defineEventHandler(async (event) => {
285
+ const incomingKey = event.req.headers.get("x-api-key");
286
+ if (apiKey && incomingKey !== apiKey) throw createHttpError(401, "Unauthorized");
287
+ const message = convertToMessageContext(await readBody(event));
288
+ if (message) onMessage(message);
289
+ return "OK";
290
+ }));
291
+ const handler = toNodeHandler(app);
292
+ const server = http.createServer(handler);
280
293
  return new Promise((resolve, reject) => {
281
294
  server.listen(port, "0.0.0.0", () => {
282
295
  const addr = server.address();
@@ -299,6 +312,11 @@ async function startCallbackServer(options) {
299
312
  });
300
313
  });
301
314
  }
315
+ function createHttpError(statusCode, message) {
316
+ const error = new Error(message);
317
+ error.statusCode = statusCode;
318
+ return error;
319
+ }
302
320
  function normalizePayload(payload) {
303
321
  const { messageType, wcId } = payload;
304
322
  if (payload.fromUser) return {
@@ -402,6 +420,7 @@ function displayLoginSuccess(nickName, wcId) {
402
420
 
403
421
  //#endregion
404
422
  //#region src/channel.ts
423
+ const runningServers = /* @__PURE__ */ new Map();
405
424
  const PLUGIN_META = {
406
425
  id: "wechat",
407
426
  label: "WeChat",
@@ -426,7 +445,8 @@ function resolveWeChatAccount({ cfg, accountId }) {
426
445
  proxyUrl: wechatCfg?.proxyUrl,
427
446
  webhookHost: wechatCfg?.webhookHost,
428
447
  webhookPort: wechatCfg?.webhookPort,
429
- webhookPath: wechatCfg?.webhookPath
448
+ webhookPath: wechatCfg?.webhookPath,
449
+ allowUsers: wechatCfg?.allowUsers
430
450
  };
431
451
  const defaultAccount = wechatCfg?.accounts?.default;
432
452
  accountCfg = {
@@ -454,6 +474,7 @@ function resolveWeChatAccount({ cfg, accountId }) {
454
474
  webhookHost: accountCfg.webhookHost,
455
475
  webhookPort: accountCfg.webhookPort || 18790,
456
476
  webhookPath: accountCfg.webhookPath || "/webhook/wechat",
477
+ allowUsers: accountCfg.allowUsers,
457
478
  config: accountCfg
458
479
  };
459
480
  }
@@ -491,6 +512,10 @@ const wechatPlugin = {
491
512
  webhookHost: { type: "string" },
492
513
  webhookPort: { type: "integer" },
493
514
  webhookPath: { type: "string" },
515
+ allowUsers: {
516
+ type: "array",
517
+ items: { type: "string" }
518
+ },
494
519
  accounts: {
495
520
  type: "object",
496
521
  additionalProperties: {
@@ -505,7 +530,11 @@ const wechatPlugin = {
505
530
  webhookPort: { type: "integer" },
506
531
  webhookPath: { type: "string" },
507
532
  wcId: { type: "string" },
508
- nickName: { type: "string" }
533
+ nickName: { type: "string" },
534
+ allowUsers: {
535
+ type: "array",
536
+ items: { type: "string" }
537
+ }
509
538
  },
510
539
  required: ["apiKey"]
511
540
  }
@@ -584,15 +613,8 @@ const wechatPlugin = {
584
613
  name: account.name || account.nickName || account.accountId,
585
614
  wcId: account.wcId,
586
615
  isLoggedIn: account.isLoggedIn
587
- }),
588
- resolveAllowFrom: ({ cfg, accountId }) => {
589
- return [];
590
- },
591
- formatAllowFrom: ({ allowFrom }) => allowFrom.map(String)
616
+ })
592
617
  },
593
- security: { collectWarnings: ({ cfg, accountId }) => {
594
- return [];
595
- } },
596
618
  setup: {
597
619
  resolveAccountId: () => DEFAULT_ACCOUNT_ID,
598
620
  applyAccountConfig: ({ cfg, accountId }) => {
@@ -640,7 +662,7 @@ const wechatPlugin = {
640
662
  },
641
663
  directory: {
642
664
  self: async () => null,
643
- listPeers: async ({ cfg, query, limit, accountId }) => {
665
+ listPeers: async ({ cfg, limit, accountId }) => {
644
666
  const account = resolveWeChatAccount({
645
667
  cfg,
646
668
  accountId: accountId || DEFAULT_ACCOUNT_ID
@@ -655,7 +677,7 @@ const wechatPlugin = {
655
677
  name: id
656
678
  }));
657
679
  },
658
- listGroups: async ({ cfg, query, limit, accountId }) => {
680
+ listGroups: async ({ cfg, limit, accountId }) => {
659
681
  const account = resolveWeChatAccount({
660
682
  cfg,
661
683
  accountId: accountId || DEFAULT_ACCOUNT_ID
@@ -688,7 +710,7 @@ const wechatPlugin = {
688
710
  lastError: snapshot.lastError ?? null,
689
711
  port: snapshot.port ?? null
690
712
  }),
691
- probeAccount: async ({ account, cfg }) => {
713
+ probeAccount: async ({ account }) => {
692
714
  const client = new ProxyClient({
693
715
  apiKey: account.apiKey,
694
716
  baseUrl: account.proxyUrl
@@ -790,6 +812,12 @@ const wechatPlugin = {
790
812
  log?.info(`Using webhook URL: ${webhookUrl}`);
791
813
  log?.info(`Registering webhook with proxy service for wcId: ${account.wcId}`);
792
814
  await client.registerWebhook(account.wcId, webhookUrl);
815
+ const prevStop = runningServers.get(accountId);
816
+ if (prevStop) {
817
+ prevStop();
818
+ runningServers.delete(accountId);
819
+ await new Promise((r) => setTimeout(r, 500));
820
+ }
793
821
  const { stop } = await startCallbackServer({
794
822
  port,
795
823
  apiKey: account.apiKey,
@@ -807,10 +835,12 @@ const wechatPlugin = {
807
835
  abortSignal
808
836
  });
809
837
  abortSignal?.addEventListener("abort", stop);
838
+ runningServers.set(accountId, stop);
810
839
  log?.info(`WeChat account ${accountId} started successfully on port ${port}`);
811
840
  log?.info(`Webhook URL: ${webhookUrl}`);
812
841
  return { async stop() {
813
842
  stop();
843
+ runningServers.delete(accountId);
814
844
  setStatus({
815
845
  accountId,
816
846
  port,
@@ -3,7 +3,14 @@
3
3
  "channels": ["wechat"],
4
4
  "configSchema": {
5
5
  "type": "object",
6
+ "additionalProperties": false,
6
7
  "properties": {
8
+ "enabled": { "type": "boolean" },
9
+ "apiKey": { "type": "string" },
10
+ "proxyUrl": { "type": "string" },
11
+ "webhookHost": { "type": "string" },
12
+ "webhookPort": { "type": "integer" },
13
+ "webhookPath": { "type": "string" },
7
14
  "accounts": {
8
15
  "type": "object",
9
16
  "additionalProperties": {
@@ -13,8 +20,10 @@
13
20
  "enabled": { "type": "boolean" },
14
21
  "name": { "type": "string" },
15
22
  "apiKey": { "type": "string" },
23
+ "proxyUrl": { "type": "string" },
24
+ "webhookHost": { "type": "string" },
16
25
  "webhookPort": { "type": "integer" },
17
- "webhookUrl": { "type": "string" },
26
+ "webhookPath": { "type": "string" },
18
27
  "wcId": { "type": "string" },
19
28
  "nickName": { "type": "string" }
20
29
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openclaw-wechat-channel",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.3.0",
5
5
  "packageManager": "pnpm@10.30.0",
6
6
  "description": "OpenClaw WeChat channel plugin via Proxy API",
7
7
  "license": "MIT",
@@ -63,6 +63,9 @@
63
63
  "peerDependencies": {
64
64
  "openclaw": ">=2026.2.9"
65
65
  },
66
+ "dependencies": {
67
+ "h3": "2.0.1-rc.14"
68
+ },
66
69
  "devDependencies": {
67
70
  "@antfu/eslint-config": "^7.4.3",
68
71
  "@types/node": "^25.3.0",