Skip to content

Commit 40feb93

Browse files
hiwepyclaude
andcommitted
merge: release/20260519 → main (wecom v2026.5.12)
Integrates @wecom/wecom-openclaw-plugin webhook module + QR login support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents 41bdfa1 + cd51a44 commit 40feb93

129 files changed

Lines changed: 17533 additions & 8344 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
node_modules
2+
dist/
3+
*.tgz
4+
*.srcbak

CLAUDE.md

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
This is an **OpenClaw Channel Plugin** for WeCom (企业微信 / WeChat Work). It enables AI bot integration with enterprise WeChat through a dual-mode architecture.
7+
This is an **OpenClaw Channel Plugin** for WeCom (企业微信 / WeChat Work). It enables AI bot integration with enterprise WeChat through a multi-mode architecture.
88

9-
- **Package**: `@mocrane/wecom`
9+
- **Package**: `@partme.ai/wecom`
1010
- **Type**: ES Module (NodeNext)
1111
- **Entry**: `index.ts`
1212

1313
## Architecture
1414

15-
### Dual-Mode Design (Bot + Agent)
15+
### Multi-Mode Design (WebSocket + Webhook Bot + Agent)
1616

17-
The plugin implements a unique dual-mode architecture:
17+
The plugin implements three connection modes:
1818

1919
| Mode | Purpose | Webhook Path | Capabilities |
2020
|------|---------|--------------|--------------|
21-
| **Bot** (智能体) | Real-time streaming chat | `/wecom`, `/wecom/bot` | Streaming responses, low latency, text/image only |
22-
| **Agent** (自建应用) | Fallback & broadcast | `/wecom/agent` | File sending, broadcasts, long tasks (>6min) |
21+
| **WebSocket** (Bot 长连接) | Real-time streaming chat | N/A (WS) | Streaming responses, low latency |
22+
| **Webhook** (Bot URL 回调) | HTTP callback for restricted networks | `/wecom`, `/wecom/bot`, `/plugins/wecom/bot` | Streaming via `response_url`, 6min window, Agent fallback |
23+
| **Agent** (自建应用) | Fallback & broadcast | `/wecom/agent`, `/plugins/wecom/agent` | File sending, broadcasts, long tasks (>6min) |
2324

2425
**Key Design Principle**: Bot is preferred for conversations; Agent is used as fallback when Bot cannot deliver (files, timeouts) or for proactive broadcasts.
2526

@@ -29,32 +30,67 @@ The plugin implements a unique dual-mode architecture:
2930
index.ts # Plugin entry - registers channel and HTTP handlers
3031
src/
3132
channel.ts # ChannelPlugin implementation, lifecycle management
32-
monitor.ts # Core webhook handler, message flow, stream state
33+
monitor.ts # WebSocket message processing + backward-compat webhook handler
3334
runtime.ts # Runtime state singleton
3435
http.ts # HTTP client with undici + proxy support
3536
crypto.ts # AES-CBC encryption/decryption for webhooks
3637
media.ts # Media file download/decryption
3738
outbound.ts # Outbound message adapter
3839
target.ts # Target resolution (user/party/tag/chat)
3940
dynamic-agent.ts # Dynamic agent routing (per-user/per-group isolation)
41+
gateway-monitor.ts # Account lifecycle: dispatch WS / webhook gateway / agent registration
42+
ws-adapter.ts # WebSocket client adapter (@wecom/aibot-node-sdk)
43+
44+
# ── Webhook mode (integrated from @wecom/wecom-openclaw-plugin) ──
45+
webhook/
46+
index.ts # Re-exports: handler, gateway, state, helpers, types
47+
handler.ts # HTTP GET/POST handler with multi-account signature matching
48+
gateway.ts # Lifecycle: start/stop webhook targets, prune timer
49+
monitor.ts # startAgentForStream() — message processing, Agent dispatch, deliver
50+
state.ts # StreamStore + ActiveReplyStore + WebhookMonitorState (singleton)
51+
helpers.ts # buildInboundBody, processInboundMessage, buildFallbackPrompt, MIME detect
52+
types.ts # WebhookInboundMessage, StreamState, PendingInbound, WecomWebhookTarget
53+
target.ts # Path-indexed target registry (register/unregister/resolve)
54+
http.ts # undici fetch wrapper with ProxyAgent
55+
media.ts # AES-256-CBC media decryption (decryptWecomMediaWithMeta)
56+
command-auth.ts # DM policy + command authorization
57+
video-frame.ts # ffmpeg first-frame extraction for video messages
58+
4059
agent/
4160
api-client.ts # WeCom API client with AccessToken caching
4261
handler.ts # XML webhook handler for Agent mode
62+
webhook.ts # Agent HTTP handler (GET echostr verify, POST XML decrypt)
4363
config/
4464
schema.ts # Zod schemas for configuration
65+
accounts.ts # Account resolution, mode detection, conflict checking
66+
network.ts # Proxy resolution chain
67+
routing.ts # Fail-closed routing policy
4568
monitor/
46-
state.ts # StreamStore and ActiveReplyStore with TTL pruning
47-
types/constants.ts # API endpoints and limits
69+
state.ts # StreamStore and ActiveReplyStore (WebSocket mode)
70+
types.ts # StreamState, PendingInbound types
71+
mcp/ # wecom_mcp tool: JSON-RPC over Streamable HTTP
72+
crypto/ # AES-256-CBC, SHA1 signature, XML encrypt/decrypt
73+
media/ # Media uploader, constants
74+
shared/ # XML parser, command auth utilities
75+
types/ # TypeScript types: config, account, message, constants
76+
compat/ # SDK version compatibility shim
4877
```
4978

5079
### Stream State Management
5180

52-
The plugin uses a sophisticated stream state system (`src/monitor/state.ts`):
81+
The plugin uses sophisticated stream state systems for both modes:
5382

83+
**WebSocket mode** (`src/monitor/state.ts`):
5484
- **StreamStore**: Manages message streams with 6-minute timeout window
5585
- **ActiveReplyStore**: Tracks `response_url` for proactive pushes
5686
- **Pending Queue**: Debounces rapid messages (500ms default)
5787
- **Message Deduplication**: Uses `msgid` to prevent duplicate processing
88+
- Exports `getSessionChatInfo()` for MCP tool context (preserves original-case chatId)
89+
90+
**Webhook mode** (`src/webhook/state.ts`):
91+
- Separate singleton (`WebhookMonitorState`) with same StreamStore + ActiveReplyStore pattern
92+
- Additional `conversationState`/`batchKey`/`ackStream` queue semantics for multi-message merge
93+
- Used by `webhook/gateway.ts` and `webhook/monitor.ts`
5894

5995
### Token Management
6096

@@ -67,23 +103,23 @@ Agent mode uses automatic AccessToken caching (`src/agent/api-client.ts`):
67103

68104
### Testing
69105

70-
This project uses **Vitest**. Tests extend from a base config at `../../vitest.config.ts`:
106+
This project uses **Vitest**:
71107

72108
```bash
73109
# Run all tests
74-
npx vitest --config vitest.config.ts
110+
npx vitest --config vitest.config.ts run
75111

76112
# Run specific test file
77-
npx vitest --config vitest.config.ts src/crypto.test.ts
113+
npx vitest --config vitest.config.ts run src/crypto.test.ts
78114

79115
# Run tests matching pattern
80-
npx vitest --config vitest.config.ts --testNamePattern="should encrypt"
116+
npx vitest --config vitest.config.ts run -t "should encrypt"
81117

82118
# Watch mode
83119
npx vitest --config vitest.config.ts --watch
84120
```
85121

86-
Test files are located alongside source files with `.test.ts` suffix:
122+
Test files are located alongside source files with `.test.ts` suffix (16 test files total):
87123
- `src/crypto.test.ts`
88124
- `src/monitor.integration.test.ts`
89125
- `src/monitor/state.queue.test.ts`
@@ -97,7 +133,7 @@ npx tsc --noEmit
97133

98134
### Build
99135

100-
The plugin is loaded directly as TypeScript by OpenClaw. No build step is required for development, but type checking is recommended.
136+
The plugin is loaded directly as TypeScript by OpenClaw. No build step is required for development, but type checking is recommended. For distribution, use `npm pack`.
101137

102138
## Configuration Schema
103139

@@ -107,6 +143,11 @@ Configuration is validated via Zod (`src/config/schema.ts`):
107143
{
108144
enabled: boolean,
109145
bot: {
146+
connectionMode: 'websocket' | 'webhook',
147+
// WebSocket mode:
148+
botId: string,
149+
secret: string,
150+
// Webhook mode:
110151
token: string, // Bot webhook token
111152
encodingAESKey: string, // AES encryption key
112153
receiveId: string?, // Optional receive ID
@@ -123,11 +164,14 @@ Configuration is validated via Zod (`src/config/schema.ts`):
123164
welcomeText: string?,
124165
dm: { policy, allowFrom }
125166
},
167+
accounts: { // Multi-account (matrix mode)
168+
main: { bot: {...}, agent: {...} }
169+
},
126170
network: {
127171
egressProxyUrl: string? // For dynamic IP scenarios
128172
},
129173
media: {
130-
maxBytes: number? // Default 25MB
174+
maxBytes: number? // Default 20MB
131175
},
132176
dynamicAgents: {
133177
enabled: boolean? // Enable per-user/per-group agents
@@ -160,23 +204,23 @@ Dynamic agents are automatically added to `agents.list` in the config file.
160204
161205
### Webhook Security
162206
163-
- **Signature Verification**: HMAC-SHA256 with token
164-
- **Encryption**: AES-CBC with PKCS#7 padding (32-byte blocks)
165-
- **Paths**: `/wecom` (legacy), `/wecom/bot`, `/wecom/agent`
207+
- **Signature Verification**: SHA1(token, timestamp, nonce, encrypt) via `@wecom/aibot-node-sdk` WecomCrypto
208+
- **Encryption**: AES-256-CBC with PKCS#7 padding (32-byte blocks)
209+
- **Paths**: `/wecom` (legacy), `/wecom/bot` (bot), `/wecom/agent` (agent), `/plugins/wecom/bot/*`, `/plugins/wecom/agent/*`
166210
167211
### Timeout Handling
168212
169-
Bot mode has a 6-minute window (360s) for streaming responses. The plugin:
213+
Bot webhook mode has a 6-minute window (360s) for streaming responses. The plugin:
170214
- Tracks deadline: `createdAt + 6 * 60 * 1000`
171215
- Switches to Agent fallback at `deadline - 30s` margin
172216
- Sends DM via Agent for remaining content
173217
174218
### Media Handling
175219
176-
- **Inbound**: Decrypts WeCom encrypted media URLs
220+
- **Inbound**: Decrypts WeCom encrypted media URLs (AES-256-CBC)
177221
- **Outbound Images**: Base64 encoded via `msg_item` in stream
178222
- **Outbound Files**: Requires Agent mode, sent via `media/upload` + `message/send`
179-
- **Max Size**: 25MB default (configurable via `channels.wecom.media.maxBytes`)
223+
- **Max Size**: 20MB default (configurable via `channels.wecom.media.maxBytes`)
180224
181225
### Proxy Support
182226
@@ -186,9 +230,13 @@ For servers with dynamic IPs (common error: `60020 not allow to access from your
186230
openclaw config set channels.wecom.network.egressProxyUrl "http://proxy.company.local:3128"
187231
```
188232
233+
### message tool denial
234+
235+
`buildCfgForDispatch()` in `webhook/helpers.ts` adds `"message"` to `tools.deny` to prevent Agent from bypassing Bot delivery via the message tool.
236+
189237
## Testing Notes
190238
191-
- Tests use Vitest with `../../vitest.config.ts` as base
239+
- Tests use Vitest with co-located test files
192240
- Integration tests mock WeCom API responses
193241
- Crypto tests verify AES encryption round-trips
194242
- Monitor tests cover stream state transitions and queue behavior
@@ -197,7 +245,7 @@ openclaw config set channels.wecom.network.egressProxyUrl "http://proxy.company.
197245
198246
### Adding a New Message Type Handler
199247
200-
1. Update `buildInboundBody()` in `src/monitor.ts` to parse the message
248+
1. Update `buildInboundBody()` in `src/webhook/helpers.ts` or `src/monitor.ts` to parse the message
201249
2. Add type definitions in `src/types/message.ts`
202250
3. Update `processInboundMessage()` if media handling is needed
203251
@@ -225,14 +273,17 @@ streamStore.updateStream(streamId, (state) => {
225273
226274
## Dependencies
227275
276+
- `@wecom/aibot-node-sdk`: Official WeCom Bot WebSocket SDK + crypto
228277
- `undici`: HTTP client with proxy support
229278
- `fast-xml-parser`: XML parsing for Agent callbacks
279+
- `file-type`: MIME type detection from file buffers
230280
- `zod`: Configuration validation
231281
- `openclaw`: Peer dependency (>=2026.2.24)
232282
233283
## WeCom API Endpoints Used
234284
235285
- `GET_TOKEN`: `https://qyapi.weixin.qq.com/cgi-bin/gettoken`
236286
- `SEND_MESSAGE`: `https://qyapi.weixin.qq.com/cgi-bin/message/send`
287+
- `SEND_APPCHAT`: `https://qyapi.weixin.qq.com/cgi-bin/appchat/send`
237288
- `UPLOAD_MEDIA`: `https://qyapi.weixin.qq.com/cgi-bin/media/upload`
238289
- `DOWNLOAD_MEDIA`: `https://qyapi.weixin.qq.com/cgi-bin/media/get`

MIGRATION_SCOPE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 背景
44

5-
| | 官方插件 `@tencent/wecom-openclaw-plugin` | 本项目 `@mocrane/wecom` |
5+
| | 官方插件 `@tencent/wecom-openclaw-plugin` | 本项目 `@partme.ai/wecom` |
66
|---|---|---|
77
| **支持的 Bot 模式** | WebSocket 长连接(唯一) | WebSocket + **Webhook (URL 回调)** |
88
| **支持 Agent 模式** || ****(自建应用,XML 回调 + API 回复) |

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@
8181
### 1.1 安装插件
8282

8383
```bash
84-
openclaw plugins install @mocrane/wecom
84+
openclaw plugins install @partme.ai/wecom
8585
openclaw plugins enable wecom
8686
```
8787

88+
89+
8890
也可以通过命令行向导快速配置:
8991

9092
```bash

index.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2+
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
23
import { emptyPluginConfigSchema, ensureConfigHelpers } from "./src/compat/plugin-sdk-shim.js";
34

4-
import { handleWecomWebhookRequest } from "./src/monitor.js";
5-
import { setWecomRuntime } from "./src/runtime.js";
5+
import { handleWeComWebhookRequest } from "./src/monitor.js";
6+
import { handleWecomWebhookRequest as handleWecomBotWebhook } from "./src/webhook/index.js";
7+
import { setWeComRuntime } from "./src/runtime.js";
68
import { wecomPlugin } from "./src/channel.js";
79
import { createWeComMcpTool } from "./src/mcp/index.js";
10+
import { getSessionChatInfo } from "./src/monitor/state.js";
11+
import { createWeComAgentWebhookHandler } from "./src/agent/webhook.js";
12+
import { CHANNEL_ID, WEBHOOK_PATHS } from "./src/types/constants.js";
813

914
const plugin = {
1015
id: "wecom",
@@ -27,20 +32,61 @@ const plugin = {
2732
// 有充足的异步间隙)。
2833
void ensureConfigHelpers();
2934

30-
setWecomRuntime(api.runtime);
35+
setWeComRuntime(api.runtime);
3136
api.registerChannel({ plugin: wecomPlugin });
37+
38+
// ── Agent webhook HTTP 路由 ────────────────────────────────────────
39+
const agentWebhookHandler = createWeComAgentWebhookHandler(api.runtime);
40+
const agentRoutes = [WEBHOOK_PATHS.AGENT_PLUGIN, WEBHOOK_PATHS.AGENT];
41+
for (const path of agentRoutes) {
42+
api.registerHttpRoute({
43+
path,
44+
handler: agentWebhookHandler,
45+
auth: "plugin",
46+
match: "prefix",
47+
});
48+
}
49+
50+
// ── Bot webhook HTTP 路由(新 webhook 模块,支持多账号签名匹配) ──
51+
const botWebhookRoutes = [WEBHOOK_PATHS.BOT_PLUGIN, WEBHOOK_PATHS.BOT_ALT, WEBHOOK_PATHS.BOT];
52+
for (const routePath of botWebhookRoutes) {
53+
api.registerHttpRoute({
54+
path: routePath,
55+
handler: handleWecomBotWebhook,
56+
auth: "plugin",
57+
match: "prefix",
58+
});
59+
}
60+
61+
// ── 历史兼容路由(保留 monitor.ts 统一入口) ─────────────────────
3262
const routes = ["/plugins/wecom", "/wecom"];
3363
for (const path of routes) {
3464
api.registerHttpRoute({
3565
path,
36-
handler: handleWecomWebhookRequest,
66+
handler: handleWeComWebhookRequest,
3767
auth: "plugin",
3868
match: "prefix",
3969
});
4070
}
4171

4272
// 注册 wecom_mcp:通过 HTTP 直接调用企业微信 MCP Server
43-
api.registerTool(createWeComMcpTool(), { name: "wecom_mcp" });
73+
// 使用 factory 函数,每次调用时从 sessionKey 获取原始大小写的 chatId/chatType,
74+
// 避免 OpenClaw core 小写化 sessionKey 导致企业微信 API 报 invalid chatid
75+
api.registerTool(
76+
(ctx: OpenClawPluginToolContext) => {
77+
const trustedRequesterUserId =
78+
ctx.messageChannel === CHANNEL_ID ? ctx.requesterSenderId?.trim() ?? undefined : undefined;
79+
80+
const sessionChat = getSessionChatInfo(ctx.sessionKey);
81+
return createWeComMcpTool({
82+
requesterUserId: trustedRequesterUserId,
83+
accountId: ctx.agentAccountId,
84+
chatId: sessionChat?.chatId,
85+
chatType: sessionChat?.chatType,
86+
});
87+
},
88+
{ name: "wecom_mcp" },
89+
);
4490

4591
// 注入媒体发送指令和文件大小限制提示词(与官方 @wecom/wecom-openclaw-plugin 保持一致)。
4692
// 仅 wecom 通道注入,避免污染其他通道(如 Telegram/Discord)的 system prompt。
@@ -69,6 +115,9 @@ const plugin = {
69115
"- 语音消息仅支持 AMR 格式(.amr),如需发送语音请确保文件为 AMR 格式",
70116
"- 超过大小限制的图片/视频/语音会被自动转为文件格式发送",
71117
"- 如果文件超过 20MB,将无法发送,请提前告知用户并尝试缩减文件大小",
118+
"",
119+
"【发送模板卡片消息】",
120+
"当需要向用户发送结构化卡片消息(如通知、投票、按钮选择等)时,请在回复中直接输出 JSON 代码块(```json ... ```),其中 card_type 字段标明卡片类型。详见 wecom-send-template-card 技能。",
72121
].join("\n"),
73122
};
74123
});

openclaw.plugin.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66
"type": "object",
77
"additionalProperties": false,
88
"properties": {}
9+
},
10+
"channelConfigs": {
11+
"wecom": {
12+
"configSchema": {
13+
"type": "object",
14+
"additionalProperties": true,
15+
"properties": {}
16+
}
17+
}
918
}
1019
}
1120

0 commit comments

Comments
 (0)