wecom-aibot-python-sdk 1.0.0__tar.gz
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.
- wecom_aibot_python_sdk-1.0.0/.env.example +8 -0
- wecom_aibot_python_sdk-1.0.0/COMPARISON.md +232 -0
- wecom_aibot_python_sdk-1.0.0/LICENSE +21 -0
- wecom_aibot_python_sdk-1.0.0/MANIFEST.in +6 -0
- wecom_aibot_python_sdk-1.0.0/PKG-INFO +365 -0
- wecom_aibot_python_sdk-1.0.0/README.md +331 -0
- wecom_aibot_python_sdk-1.0.0/aibot/__init__.py +50 -0
- wecom_aibot_python_sdk-1.0.0/aibot/api.py +74 -0
- wecom_aibot_python_sdk-1.0.0/aibot/client.py +362 -0
- wecom_aibot_python_sdk-1.0.0/aibot/crypto_utils.py +73 -0
- wecom_aibot_python_sdk-1.0.0/aibot/logger.py +47 -0
- wecom_aibot_python_sdk-1.0.0/aibot/message_handler.py +89 -0
- wecom_aibot_python_sdk-1.0.0/aibot/types.py +170 -0
- wecom_aibot_python_sdk-1.0.0/aibot/utils.py +32 -0
- wecom_aibot_python_sdk-1.0.0/aibot/ws.py +574 -0
- wecom_aibot_python_sdk-1.0.0/examples/basic.py +261 -0
- wecom_aibot_python_sdk-1.0.0/examples/comparison_test.py +469 -0
- wecom_aibot_python_sdk-1.0.0/pyproject.toml +50 -0
- wecom_aibot_python_sdk-1.0.0/requirements.txt +6 -0
- wecom_aibot_python_sdk-1.0.0/setup.cfg +4 -0
- wecom_aibot_python_sdk-1.0.0/wecom_aibot_python_sdk.egg-info/PKG-INFO +365 -0
- wecom_aibot_python_sdk-1.0.0/wecom_aibot_python_sdk.egg-info/SOURCES.txt +23 -0
- wecom_aibot_python_sdk-1.0.0/wecom_aibot_python_sdk.egg-info/dependency_links.txt +1 -0
- wecom_aibot_python_sdk-1.0.0/wecom_aibot_python_sdk.egg-info/requires.txt +8 -0
- wecom_aibot_python_sdk-1.0.0/wecom_aibot_python_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Node.js vs Python SDK 对比文档
|
|
2
|
+
|
|
3
|
+
本文档对比 `@wecom/aibot-node-sdk`(Node.js)与 `wecom-aibot-sdk`(Python)两个版本的实现差异。
|
|
4
|
+
|
|
5
|
+
## 📁 代码结构映射
|
|
6
|
+
|
|
7
|
+
| Node.js (src/) | Python (aibot/) | 说明 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `index.ts` | `__init__.py` | SDK 入口,统一导出 |
|
|
10
|
+
| `client.ts` | `client.py` | 核心客户端 WSClient |
|
|
11
|
+
| `ws.ts` | `ws.py` | WebSocket 长连接管理器 |
|
|
12
|
+
| `message-handler.ts` | `message_handler.py` | 消息解析与事件分发 |
|
|
13
|
+
| `api.ts` | `api.py` | HTTP API 客户端(文件下载) |
|
|
14
|
+
| `crypto.ts` | `crypto_utils.py` | AES-256-CBC 文件解密 |
|
|
15
|
+
| `logger.ts` | `logger.py` | 默认日志实现 |
|
|
16
|
+
| `utils.ts` | `utils.py` | 工具方法 |
|
|
17
|
+
| `types/` (6 个文件) | `types.py` (1 个文件) | 类型定义 |
|
|
18
|
+
| `examples/basic.ts` | `examples/basic.py` | 基础使用示例 |
|
|
19
|
+
|
|
20
|
+
**行数对比:**
|
|
21
|
+
|
|
22
|
+
| 模块 | Node.js | Python | 说明 |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| 核心客户端 | ~384 行 | ~280 行 | Python 更简洁(无显式类型标注冗余) |
|
|
25
|
+
| WS 管理器 | ~512 行 | ~380 行 | asyncio 原语减少了回调嵌套 |
|
|
26
|
+
| 消息处理器 | ~105 行 | ~80 行 | 逻辑一致 |
|
|
27
|
+
| API 客户端 | ~59 行 | ~60 行 | 基本等量 |
|
|
28
|
+
| 加解密 | ~59 行 | ~55 行 | 基本等量 |
|
|
29
|
+
| 类型定义 | ~820 行 (6文件) | ~160 行 (1文件) | Python 使用 dict 替代复杂接口 |
|
|
30
|
+
| 日志 | ~34 行 | ~45 行 | Python 略多(Protocol 定义) |
|
|
31
|
+
| 工具函数 | ~30 行 | ~30 行 | 等量 |
|
|
32
|
+
|
|
33
|
+
## 📚 依赖库对标
|
|
34
|
+
|
|
35
|
+
| 功能 | Node.js 依赖 | Python 依赖 | 说明 |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| WebSocket 客户端 | `ws` (^8.16.0) | `websockets` (>=12.0) | 均为各语言最流行的 WS 库 |
|
|
38
|
+
| HTTP 客户端 | `axios` (^1.6.7) | `aiohttp` (>=3.9) | Python 版使用异步 HTTP |
|
|
39
|
+
| 事件发射器 | `eventemitter3` (^5.0.1) | `pyee` (>=11.0) | API 风格高度一致(on/emit/once) |
|
|
40
|
+
| 加解密 | `crypto` (Node.js 内置) | `cryptography` (>=42.0) | Python 版需额外安装 |
|
|
41
|
+
| 类型系统 | TypeScript | `dataclasses` + `typing` + `enum` | 均为语言内置 |
|
|
42
|
+
| 构建工具 | Rollup + TypeScript | setuptools + pyproject.toml | 各语言标准构建链 |
|
|
43
|
+
|
|
44
|
+
## 🔄 异步编程模型差异
|
|
45
|
+
|
|
46
|
+
### Node.js: 事件循环 + 回调/Promise
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Node.js 天然单线程事件循环
|
|
50
|
+
wsClient.connect(); // 同步启动,内部异步
|
|
51
|
+
|
|
52
|
+
// 事件回调
|
|
53
|
+
wsClient.on('message.text', (frame) => {
|
|
54
|
+
// 同步回调,返回 Promise
|
|
55
|
+
wsClient.replyStream(frame, streamId, content, true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Promise 链式调用
|
|
59
|
+
wsClient.replyStream(frame, streamId, '内容', true)
|
|
60
|
+
.then(ack => console.log('回执:', ack))
|
|
61
|
+
.catch(err => console.error(err));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Python: asyncio + async/await
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# Python 需要显式管理事件循环
|
|
68
|
+
await ws_client.connect() # async 方法
|
|
69
|
+
|
|
70
|
+
# 事件回调支持协程
|
|
71
|
+
@ws_client.on('message.text')
|
|
72
|
+
async def on_text(frame):
|
|
73
|
+
# async 回调,使用 await
|
|
74
|
+
await ws_client.reply_stream(frame, stream_id, content, True)
|
|
75
|
+
|
|
76
|
+
# async/await 风格
|
|
77
|
+
try:
|
|
78
|
+
ack = await ws_client.reply_stream(frame, stream_id, '内容', True)
|
|
79
|
+
print('回执:', ack)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f'错误: {e}')
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 关键差异
|
|
85
|
+
|
|
86
|
+
| 方面 | Node.js | Python |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 事件循环 | 内置,自动启动 | 需要 `asyncio.run()` 或 `loop.run_forever()` |
|
|
89
|
+
| 连接方法 | `connect()` — 同步调用 | `await connect()` — 必须在 async 上下文 |
|
|
90
|
+
| 回复方法 | 返回 `Promise<WsFrame>` | 返回 `Coroutine → WsFrame`(需 await) |
|
|
91
|
+
| 定时器 | `setInterval` / `setTimeout` | `asyncio.create_task` + `asyncio.sleep` |
|
|
92
|
+
| 回调风格 | `(frame) => { ... }` | `async def handler(frame): ...` |
|
|
93
|
+
| 便捷启动 | 直接运行 | `ws_client.run()` 或手动管理循环 |
|
|
94
|
+
| 断开连接 | `disconnect()` — 同步 | `disconnect()` — 同步(内部调度异步关闭) |
|
|
95
|
+
|
|
96
|
+
## 🏷️ 类型系统差异
|
|
97
|
+
|
|
98
|
+
### Node.js: TypeScript 接口
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// 丰富的接口定义(编译期类型安全)
|
|
102
|
+
interface WsFrame<T = any> {
|
|
103
|
+
cmd?: string;
|
|
104
|
+
headers: { req_id: string; [key: string]: any };
|
|
105
|
+
body?: T;
|
|
106
|
+
errcode?: number;
|
|
107
|
+
errmsg?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 泛型消息帧
|
|
111
|
+
type TextFrame = WsFrame<TextMessage>;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Python: dataclass + dict
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# 配置类使用 dataclass(运行时类型安全)
|
|
118
|
+
@dataclass
|
|
119
|
+
class WSClientOptions:
|
|
120
|
+
bot_id: str
|
|
121
|
+
secret: str
|
|
122
|
+
reconnect_interval: int = 1000
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
# 消息帧直接使用 dict(运行时灵活)
|
|
126
|
+
WsFrame = Dict[str, Any]
|
|
127
|
+
# 访问:frame.get('body', {}).get('text', {}).get('content')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 类型策略对比
|
|
131
|
+
|
|
132
|
+
| 方面 | Node.js (TypeScript) | Python |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| 帧结构 | `WsFrame<T>` 泛型接口 | `Dict[str, Any]` 类型别名 |
|
|
135
|
+
| 消息类型 | 独立接口(TextMessage 等) | dict 访问,无独立类型 |
|
|
136
|
+
| 配置选项 | `WSClientOptions` 接口 | `WSClientOptions` dataclass |
|
|
137
|
+
| 枚举 | TypeScript `enum` | Python `enum.Enum`(str 继承) |
|
|
138
|
+
| 模板卡片 | 20+ 子接口精确定义 | `Dict[str, Any]`(运行时灵活) |
|
|
139
|
+
| 类型检查 | 编译期(tsc) | 运行时 + 可选静态检查(mypy) |
|
|
140
|
+
|
|
141
|
+
**设计选择说明**:Python 版本选择使用 `dict` 而非为每种消息定义 TypedDict,原因是:
|
|
142
|
+
1. 企业微信协议字段较多,JSON 结构直接映射为 dict 更自然
|
|
143
|
+
2. 减少了大量样板类型定义代码
|
|
144
|
+
3. Python 生态中 dict 访问模式更为常见和惯用
|
|
145
|
+
4. 保持 SDK 轻量,降低维护成本
|
|
146
|
+
|
|
147
|
+
## 📛 命名规范转换
|
|
148
|
+
|
|
149
|
+
| Node.js (camelCase) | Python (snake_case) | 说明 |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `replyStream()` | `reply_stream()` | 方法名 |
|
|
152
|
+
| `replyWelcome()` | `reply_welcome()` | 方法名 |
|
|
153
|
+
| `replyTemplateCard()` | `reply_template_card()` | 方法名 |
|
|
154
|
+
| `replyStreamWithCard()` | `reply_stream_with_card()` | 方法名 |
|
|
155
|
+
| `updateTemplateCard()` | `update_template_card()` | 方法名 |
|
|
156
|
+
| `sendMessage()` | `send_message()` | 方法名 |
|
|
157
|
+
| `downloadFile()` | `download_file()` | 方法名 |
|
|
158
|
+
| `generateReqId()` | `generate_req_id()` | 工具函数 |
|
|
159
|
+
| `generateRandomString()` | `generate_random_string()` | 工具函数 |
|
|
160
|
+
| `isConnected` | `is_connected` | 属性 |
|
|
161
|
+
| `botId` | `bot_id` | 配置项 |
|
|
162
|
+
| `heartbeatInterval` | `heartbeat_interval` | 配置项 |
|
|
163
|
+
| `maxReconnectAttempts` | `max_reconnect_attempts` | 配置项 |
|
|
164
|
+
| `reconnectInterval` | `reconnect_interval` | 配置项 |
|
|
165
|
+
| `requestTimeout` | `request_timeout` | 配置项 |
|
|
166
|
+
| `wsUrl` | `ws_url` | 配置项 |
|
|
167
|
+
| `WSClient` | `WSClient` | 类名保持 PascalCase |
|
|
168
|
+
| `WsCmd.SUBSCRIBE` | `WsCmd.SUBSCRIBE` | 常量保持 UPPER_SNAKE_CASE |
|
|
169
|
+
|
|
170
|
+
## 📊 API 方法对照表
|
|
171
|
+
|
|
172
|
+
| 功能 | Node.js | Python |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| 创建实例 | `new WSClient(options)` | `WSClient(options)` |
|
|
175
|
+
| 建立连接 | `wsClient.connect()` → `this` | `await ws_client.connect()` → `WSClient` |
|
|
176
|
+
| 断开连接 | `wsClient.disconnect()` | `ws_client.disconnect()` |
|
|
177
|
+
| 通用回复 | `wsClient.reply(frame, body, cmd?)` → `Promise<WsFrame>` | `await ws_client.reply(frame, body, cmd?)` → `WsFrame` |
|
|
178
|
+
| 流式回复 | `wsClient.replyStream(frame, id, content, finish?, items?, fb?)` → `Promise<WsFrame>` | `await ws_client.reply_stream(frame, id, content, finish?, items?, fb?)` → `WsFrame` |
|
|
179
|
+
| 欢迎语 | `wsClient.replyWelcome(frame, body)` → `Promise<WsFrame>` | `await ws_client.reply_welcome(frame, body)` → `WsFrame` |
|
|
180
|
+
| 模板卡片 | `wsClient.replyTemplateCard(frame, card, fb?)` → `Promise<WsFrame>` | `await ws_client.reply_template_card(frame, card, fb?)` → `WsFrame` |
|
|
181
|
+
| 流式+卡片 | `wsClient.replyStreamWithCard(frame, id, content, finish?, opts?)` → `Promise<WsFrame>` | `await ws_client.reply_stream_with_card(frame, id, content, finish?, ...)` → `WsFrame` |
|
|
182
|
+
| 更新卡片 | `wsClient.updateTemplateCard(frame, card, userids?)` → `Promise<WsFrame>` | `await ws_client.update_template_card(frame, card, userids?)` → `WsFrame` |
|
|
183
|
+
| 主动发送 | `wsClient.sendMessage(chatid, body)` → `Promise<WsFrame>` | `await ws_client.send_message(chatid, body)` → `WsFrame` |
|
|
184
|
+
| 下载文件 | `wsClient.downloadFile(url, aesKey?)` → `Promise<{buffer, filename?}>` | `await ws_client.download_file(url, aes_key?)` → `tuple[bytes, str\|None]` |
|
|
185
|
+
| 监听事件 | `wsClient.on('event', handler)` | `@ws_client.on('event')` 或 `ws_client.on('event', handler)` |
|
|
186
|
+
|
|
187
|
+
## 🔧 内部实现差异
|
|
188
|
+
|
|
189
|
+
### 心跳机制
|
|
190
|
+
|
|
191
|
+
| 方面 | Node.js | Python |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| 定时器 | `setInterval()` | `asyncio.create_task()` + `while` + `asyncio.sleep()` |
|
|
194
|
+
| 停止 | `clearInterval()` | `task.cancel()` |
|
|
195
|
+
| 超时判定 | 连续 2 次无 pong → `terminate()` | 连续 2 次无 pong → `ws.close()` |
|
|
196
|
+
|
|
197
|
+
### 串行回复队列
|
|
198
|
+
|
|
199
|
+
| 方面 | Node.js | Python |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
| 队列存储 | `Map<string, ReplyQueueItem[]>` | `dict[str, list[_ReplyQueueItem]]` |
|
|
202
|
+
| 等待回执 | `Promise` + `resolve/reject` | `asyncio.Future` + `set_result/set_exception` |
|
|
203
|
+
| 超时 | `setTimeout()` | `loop.call_later()` |
|
|
204
|
+
| 处理方式 | 递归调用 `processReplyQueue()` | `async while` 循环 |
|
|
205
|
+
|
|
206
|
+
### 重连策略
|
|
207
|
+
|
|
208
|
+
两个版本完全一致:
|
|
209
|
+
- 指数退避:1s → 2s → 4s → 8s → 16s → 30s(上限)
|
|
210
|
+
- 最大重连次数:默认 10,-1 表示无限重连
|
|
211
|
+
- 手动断开后不自动重连
|
|
212
|
+
|
|
213
|
+
### AES 解密
|
|
214
|
+
|
|
215
|
+
| 方面 | Node.js | Python |
|
|
216
|
+
| --- | --- | --- |
|
|
217
|
+
| 库 | `crypto`(内置) | `cryptography`(需安装) |
|
|
218
|
+
| 算法 | `createDecipheriv('aes-256-cbc', key, iv)` | `Cipher(algorithms.AES(key), modes.CBC(iv))` |
|
|
219
|
+
| Padding | 手动 PKCS#7(32 字节 block) | 手动 PKCS#7(32 字节 block) |
|
|
220
|
+
| IV | `key.subarray(0, 16)` | `key[:16]` |
|
|
221
|
+
|
|
222
|
+
## 🏗️ 构建与发布
|
|
223
|
+
|
|
224
|
+
| 方面 | Node.js | Python |
|
|
225
|
+
| --- | --- | --- |
|
|
226
|
+
| 包名 | `@wecom/aibot-node-sdk` | `wecom-aibot-sdk` |
|
|
227
|
+
| 包管理 | npm / yarn | pip |
|
|
228
|
+
| 构建工具 | Rollup | setuptools |
|
|
229
|
+
| 配置文件 | `package.json` + `rollup.config.mjs` + `tsconfig.json` | `pyproject.toml` |
|
|
230
|
+
| 输出格式 | CJS + ESM + .d.ts | 源码直接分发(.py) |
|
|
231
|
+
| 类型声明 | `.d.ts` 文件 | 内联类型注解 + `py.typed` |
|
|
232
|
+
| 注册中心 | npmjs.com | pypi.org |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WeCom
|
|
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.
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wecom-aibot-python-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 企业微信智能机器人 Python SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
|
|
5
|
+
Author-email: WeCom <wecom@tencent.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nicognaW/aibot-node-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/nicognaW/aibot-python-sdk
|
|
9
|
+
Project-URL: Documentation, https://github.com/nicognaW/aibot-python-sdk/blob/main/README.md
|
|
10
|
+
Project-URL: Issues, https://github.com/nicognaW/aibot-python-sdk/issues
|
|
11
|
+
Keywords: wecom,wechat-work,aibot,websocket,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications :: Chat
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: websockets>=12.0
|
|
27
|
+
Requires-Dist: aiohttp>=3.9
|
|
28
|
+
Requires-Dist: pyee>=11.0
|
|
29
|
+
Requires-Dist: cryptography>=42.0
|
|
30
|
+
Requires-Dist: certifi>=2023.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: python-dotenv>=1.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# wecom-openws-aibot-sdk (Python)
|
|
36
|
+
|
|
37
|
+
企业微信智能机器人 Python SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
|
|
38
|
+
|
|
39
|
+
> 本项目是 [@wecom/aibot-node-sdk](https://www.npmjs.com/package/@wecom/aibot-node-sdk)(Node.js 版)的 Python 等价实现。
|
|
40
|
+
|
|
41
|
+
## ✨ 特性
|
|
42
|
+
|
|
43
|
+
- 🔗 **WebSocket 长连接** — 基于 `wss://openws.work.weixin.qq.com` 内置默认地址,开箱即用
|
|
44
|
+
- 🔐 **自动认证** — 连接建立后自动发送认证帧(bot_id + secret)
|
|
45
|
+
- 💓 **心跳保活** — 自动维护心跳,连续未收到 ack 时自动判定连接异常
|
|
46
|
+
- 🔄 **断线重连** — 指数退避重连策略(1s → 2s → 4s → ... → 30s 上限),支持自定义最大重连次数
|
|
47
|
+
- 📨 **消息分发** — 自动解析消息类型并触发对应事件(text / image / mixed / voice / file)
|
|
48
|
+
- 🌊 **流式回复** — 内置流式回复方法,支持 Markdown 和图文混排
|
|
49
|
+
- 🃏 **模板卡片** — 支持回复模板卡片消息、流式+卡片组合回复、更新卡片
|
|
50
|
+
- 📤 **主动推送** — 支持向指定会话主动发送 Markdown 或模板卡片消息,无需依赖回调帧
|
|
51
|
+
- 📡 **事件回调** — 支持进入会话、模板卡片按钮点击、用户反馈等事件
|
|
52
|
+
- ⏩ **串行回复队列** — 同一 req_id 的回复消息串行发送,自动等待回执
|
|
53
|
+
- 🔑 **文件下载解密** — 内置 AES-256-CBC 文件解密,每个图片/文件消息自带独立的 aeskey
|
|
54
|
+
- 🪵 **可插拔日志** — 支持自定义 Logger,内置带时间戳的 DefaultLogger
|
|
55
|
+
- 🐍 **asyncio 原生** — 基于 Python asyncio 异步架构,支持 async/await
|
|
56
|
+
|
|
57
|
+
## 📦 安装
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install wecom-aibot-python-sdk
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**依赖:**
|
|
64
|
+
- Python >= 3.8
|
|
65
|
+
- websockets >= 12.0
|
|
66
|
+
- aiohttp >= 3.9
|
|
67
|
+
- pyee >= 11.0
|
|
68
|
+
- cryptography >= 42.0
|
|
69
|
+
- certifi>=2023.0
|
|
70
|
+
- python-dotenv>=1.0 (可选,用于加载 .env 文件)
|
|
71
|
+
|
|
72
|
+
## ⚙️ 配置
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 复制示例配置文件
|
|
76
|
+
cp .env.example .env
|
|
77
|
+
|
|
78
|
+
# 编辑 .env 文件,填入真实配置
|
|
79
|
+
# WECHAT_BOT_ID=your-bot-id
|
|
80
|
+
# WECHAT_BOT_SECRET=your-bot-secret
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## 🚀 快速开始
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
import os
|
|
89
|
+
from dotenv import load_dotenv
|
|
90
|
+
from aibot import WSClient, WSClientOptions, generate_req_id
|
|
91
|
+
|
|
92
|
+
# 加载 .env 文件中的环境变量
|
|
93
|
+
load_dotenv()
|
|
94
|
+
|
|
95
|
+
# 1. 创建客户端实例
|
|
96
|
+
ws_client = WSClient(
|
|
97
|
+
WSClientOptions(
|
|
98
|
+
bot_id=os.getenv('WECHAT_BOT_ID'), # 企业微信后台获取的机器人 ID
|
|
99
|
+
secret=os.getenv('WECHAT_BOT_SECRET'), # 企业微信后台获取的机器人 Secret
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# 2. 监听认证成功
|
|
104
|
+
@ws_client.on('authenticated')
|
|
105
|
+
def on_authenticated():
|
|
106
|
+
print('🔐 认证成功')
|
|
107
|
+
|
|
108
|
+
# 3. 监听文本消息并进行流式回复
|
|
109
|
+
@ws_client.on('message.text')
|
|
110
|
+
async def on_text(frame):
|
|
111
|
+
content = frame.get('body', {}).get('text', {}).get('content', '')
|
|
112
|
+
print(f'收到文本: {content}')
|
|
113
|
+
|
|
114
|
+
stream_id = generate_req_id('stream')
|
|
115
|
+
|
|
116
|
+
# 发送流式中间内容
|
|
117
|
+
await ws_client.reply_stream(frame, stream_id, '正在思考中...', False)
|
|
118
|
+
|
|
119
|
+
# 发送最终结果
|
|
120
|
+
await asyncio.sleep(1)
|
|
121
|
+
await ws_client.reply_stream(frame, stream_id, f'你好!你说的是: "{content}"', True)
|
|
122
|
+
|
|
123
|
+
# 4. 监听进入会话事件(发送欢迎语)
|
|
124
|
+
@ws_client.on('event.enter_chat')
|
|
125
|
+
async def on_enter_chat(frame):
|
|
126
|
+
await ws_client.reply_welcome(frame, {
|
|
127
|
+
'msgtype': 'text',
|
|
128
|
+
'text': {'content': '您好!我是智能助手,有什么可以帮您的吗?'},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
# 5. 启动(便捷方法,内部管理事件循环)
|
|
132
|
+
ws_client.run()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
或者手动管理事件循环:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
async def main():
|
|
139
|
+
await ws_client.connect()
|
|
140
|
+
# 保持运行
|
|
141
|
+
await asyncio.Event().wait()
|
|
142
|
+
|
|
143
|
+
asyncio.run(main())
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 📖 API 文档
|
|
147
|
+
|
|
148
|
+
### `WSClient`
|
|
149
|
+
|
|
150
|
+
核心客户端类,继承自 `pyee.AsyncIOEventEmitter`,提供连接管理、消息收发等功能。
|
|
151
|
+
|
|
152
|
+
#### 构造函数
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
ws_client = WSClient(options: WSClientOptions)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### 方法
|
|
159
|
+
|
|
160
|
+
| 方法 | 说明 | 返回值 |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `await connect()` | 建立 WebSocket 连接,连接后自动认证 | `WSClient`(支持链式调用) |
|
|
163
|
+
| `disconnect()` | 主动断开连接 | `None` |
|
|
164
|
+
| `await reply(frame, body, cmd?)` | 通过 WebSocket 通道发送回复消息(通用方法) | `WsFrame` |
|
|
165
|
+
| `await reply_stream(frame, stream_id, content, finish?, msg_item?, feedback?)` | 发送流式文本回复(便捷方法,支持 Markdown) | `WsFrame` |
|
|
166
|
+
| `await reply_welcome(frame, body)` | 发送欢迎语回复(支持文本或模板卡片),需在收到事件 5s 内调用 | `WsFrame` |
|
|
167
|
+
| `await reply_template_card(frame, template_card, feedback?)` | 回复模板卡片消息 | `WsFrame` |
|
|
168
|
+
| `await reply_stream_with_card(frame, stream_id, content, finish?, ...)` | 发送流式消息 + 模板卡片组合回复 | `WsFrame` |
|
|
169
|
+
| `await update_template_card(frame, template_card, userids?)` | 更新模板卡片,需在收到事件 5s 内调用 | `WsFrame` |
|
|
170
|
+
| `await send_message(chatid, body)` | 主动发送消息(支持 Markdown 或模板卡片),无需依赖回调帧 | `WsFrame` |
|
|
171
|
+
| `await download_file(url, aes_key?)` | 下载文件并使用 AES 密钥解密 | `tuple[bytes, str \| None]` |
|
|
172
|
+
| `run()` | 便捷启动方法(创建事件循环并连接) | `None` |
|
|
173
|
+
|
|
174
|
+
#### 属性
|
|
175
|
+
|
|
176
|
+
| 属性 | 说明 | 类型 |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `is_connected` | 当前 WebSocket 连接状态 | `bool` |
|
|
179
|
+
| `api` | 内部 API 客户端实例(高级用途) | `WeComApiClient` |
|
|
180
|
+
|
|
181
|
+
### `reply_stream` 详细说明
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
await ws_client.reply_stream(
|
|
185
|
+
frame, # 收到的原始 WebSocket 帧(透传 req_id)
|
|
186
|
+
stream_id: str, # 流式消息 ID(使用 generate_req_id('stream') 生成)
|
|
187
|
+
content: str, # 回复内容(支持 Markdown)
|
|
188
|
+
finish: bool = False, # 是否结束流式消息
|
|
189
|
+
msg_item: list = None, # 图文混排项(仅 finish=True 时有效)
|
|
190
|
+
feedback: dict = None, # 反馈信息(仅首次回复时设置)
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `reply_welcome` 详细说明
|
|
195
|
+
|
|
196
|
+
发送欢迎语回复,需在收到 `event.enter_chat` 事件 5 秒内调用。
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
# 文本欢迎语
|
|
200
|
+
await ws_client.reply_welcome(frame, {
|
|
201
|
+
'msgtype': 'text',
|
|
202
|
+
'text': {'content': '欢迎!'},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
# 模板卡片欢迎语
|
|
206
|
+
await ws_client.reply_welcome(frame, {
|
|
207
|
+
'msgtype': 'template_card',
|
|
208
|
+
'template_card': {'card_type': 'text_notice', 'main_title': {'title': '欢迎'}},
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `reply_stream_with_card` 详细说明
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
await ws_client.reply_stream_with_card(
|
|
216
|
+
frame, # 收到的原始 WebSocket 帧
|
|
217
|
+
stream_id: str, # 流式消息 ID
|
|
218
|
+
content: str, # 回复内容(支持 Markdown)
|
|
219
|
+
finish: bool = False, # 是否结束流式消息
|
|
220
|
+
msg_item: list = None, # 图文混排项(仅 finish=True 时有效)
|
|
221
|
+
stream_feedback: dict = None, # 流式消息反馈信息(首次回复时设置)
|
|
222
|
+
template_card: dict = None, # 模板卡片内容(同一消息只能回复一次)
|
|
223
|
+
card_feedback: dict = None, # 模板卡片反馈信息
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `send_message` 详细说明
|
|
228
|
+
|
|
229
|
+
主动向指定会话推送消息,无需依赖收到的回调帧。
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# 发送 Markdown 消息
|
|
233
|
+
await ws_client.send_message('userid_or_chatid', {
|
|
234
|
+
'msgtype': 'markdown',
|
|
235
|
+
'markdown': {'content': '这是一条**主动推送**的消息'},
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
# 发送模板卡片消息
|
|
239
|
+
await ws_client.send_message('userid_or_chatid', {
|
|
240
|
+
'msgtype': 'template_card',
|
|
241
|
+
'template_card': {'card_type': 'text_notice', 'main_title': {'title': '通知'}},
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `download_file` 使用示例
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# aes_key 取自消息体中的 image.aeskey 或 file.aeskey
|
|
249
|
+
@ws_client.on('message.image')
|
|
250
|
+
async def on_image(frame):
|
|
251
|
+
body = frame.get('body', {})
|
|
252
|
+
image = body.get('image', {})
|
|
253
|
+
buffer, filename = await ws_client.download_file(image.get('url'), image.get('aeskey'))
|
|
254
|
+
print(f'文件名: {filename}, 大小: {len(buffer)} bytes')
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## ⚙️ 配置选项
|
|
258
|
+
|
|
259
|
+
`WSClientOptions` 完整配置:
|
|
260
|
+
|
|
261
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
262
|
+
| --- | --- | --- | --- | --- |
|
|
263
|
+
| `bot_id` | `str` | ✅ | — | 机器人 ID(企业微信后台获取) |
|
|
264
|
+
| `secret` | `str` | ✅ | — | 机器人 Secret(企业微信后台获取) |
|
|
265
|
+
| `reconnect_interval` | `int` | — | `1000` | 重连基础延迟(毫秒),实际延迟按指数退避递增 |
|
|
266
|
+
| `max_reconnect_attempts` | `int` | — | `10` | 最大重连次数(`-1` 表示无限重连) |
|
|
267
|
+
| `heartbeat_interval` | `int` | — | `30000` | 心跳间隔(毫秒) |
|
|
268
|
+
| `request_timeout` | `int` | — | `10000` | HTTP 请求超时时间(毫秒) |
|
|
269
|
+
| `ws_url` | `str` | — | `wss://openws.work.weixin.qq.com` | 自定义 WebSocket 连接地址 |
|
|
270
|
+
| `logger` | `Logger` | — | `DefaultLogger` | 自定义日志实例 |
|
|
271
|
+
|
|
272
|
+
## 📡 事件列表
|
|
273
|
+
|
|
274
|
+
所有事件均通过 `@ws_client.on(event)` 装饰器或 `ws_client.on(event, handler)` 监听:
|
|
275
|
+
|
|
276
|
+
| 事件 | 回调参数 | 说明 |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `connected` | — | WebSocket 连接建立 |
|
|
279
|
+
| `authenticated` | — | 认证成功 |
|
|
280
|
+
| `disconnected` | `reason: str` | 连接断开 |
|
|
281
|
+
| `reconnecting` | `attempt: int` | 正在重连(第 N 次) |
|
|
282
|
+
| `error` | `error: Exception` | 发生错误 |
|
|
283
|
+
| `message` | `frame: WsFrame` | 收到消息(所有类型) |
|
|
284
|
+
| `message.text` | `frame: WsFrame` | 收到文本消息 |
|
|
285
|
+
| `message.image` | `frame: WsFrame` | 收到图片消息 |
|
|
286
|
+
| `message.mixed` | `frame: WsFrame` | 收到图文混排消息 |
|
|
287
|
+
| `message.voice` | `frame: WsFrame` | 收到语音消息 |
|
|
288
|
+
| `message.file` | `frame: WsFrame` | 收到文件消息 |
|
|
289
|
+
| `event` | `frame: WsFrame` | 收到事件回调(所有事件类型) |
|
|
290
|
+
| `event.enter_chat` | `frame: WsFrame` | 收到进入会话事件 |
|
|
291
|
+
| `event.template_card_event` | `frame: WsFrame` | 收到模板卡片事件 |
|
|
292
|
+
| `event.feedback_event` | `frame: WsFrame` | 收到用户反馈事件 |
|
|
293
|
+
|
|
294
|
+
## 📋 消息类型
|
|
295
|
+
|
|
296
|
+
SDK 支持以下消息类型(`MessageType` 枚举):
|
|
297
|
+
|
|
298
|
+
| 类型 | 值 | 说明 |
|
|
299
|
+
| --- | --- | --- |
|
|
300
|
+
| `MessageType.Text` | `'text'` | 文本消息 |
|
|
301
|
+
| `MessageType.Image` | `'image'` | 图片消息 |
|
|
302
|
+
| `MessageType.Mixed` | `'mixed'` | 图文混排消息 |
|
|
303
|
+
| `MessageType.Voice` | `'voice'` | 语音消息 |
|
|
304
|
+
| `MessageType.File` | `'file'` | 文件消息 |
|
|
305
|
+
|
|
306
|
+
SDK 支持以下事件类型(`EventType` 枚举):
|
|
307
|
+
|
|
308
|
+
| 类型 | 值 | 说明 |
|
|
309
|
+
| --- | --- | --- |
|
|
310
|
+
| `EventType.EnterChat` | `'enter_chat'` | 进入会话事件 |
|
|
311
|
+
| `EventType.TemplateCardEvent` | `'template_card_event'` | 模板卡片事件 |
|
|
312
|
+
| `EventType.FeedbackEvent` | `'feedback_event'` | 用户反馈事件 |
|
|
313
|
+
|
|
314
|
+
## 🪵 自定义日志
|
|
315
|
+
|
|
316
|
+
实现 `Logger` Protocol 接口即可自定义日志输出:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
class MyLogger:
|
|
320
|
+
def debug(self, message: str, *args) -> None:
|
|
321
|
+
pass # 静默 debug 日志
|
|
322
|
+
|
|
323
|
+
def info(self, message: str, *args) -> None:
|
|
324
|
+
print(f"[INFO] {message}", *args)
|
|
325
|
+
|
|
326
|
+
def warn(self, message: str, *args) -> None:
|
|
327
|
+
print(f"[WARN] {message}", *args)
|
|
328
|
+
|
|
329
|
+
def error(self, message: str, *args) -> None:
|
|
330
|
+
print(f"[ERROR] {message}", *args)
|
|
331
|
+
|
|
332
|
+
ws_client = WSClient(
|
|
333
|
+
WSClientOptions(
|
|
334
|
+
bot_id='your-bot-id',
|
|
335
|
+
secret='your-bot-secret',
|
|
336
|
+
logger=MyLogger(),
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## 📂 项目结构
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
aibot-python-sdk/
|
|
345
|
+
├── aibot/
|
|
346
|
+
│ ├── __init__.py # 入口文件,统一导出
|
|
347
|
+
│ ├── client.py # WSClient 核心客户端
|
|
348
|
+
│ ├── ws.py # WebSocket 长连接管理器
|
|
349
|
+
│ ├── message_handler.py # 消息解析与事件分发
|
|
350
|
+
│ ├── api.py # HTTP API 客户端(文件下载)
|
|
351
|
+
│ ├── crypto_utils.py # AES-256-CBC 文件解密
|
|
352
|
+
│ ├── logger.py # 默认日志实现
|
|
353
|
+
│ ├── utils.py # 工具方法(generate_req_id 等)
|
|
354
|
+
│ └── types.py # 类型定义(枚举、常量、dataclass)
|
|
355
|
+
├── examples/
|
|
356
|
+
│ └── basic.py # 基础使用示例
|
|
357
|
+
├── pyproject.toml # 项目配置
|
|
358
|
+
├── requirements.txt # 依赖清单
|
|
359
|
+
├── README.md # 本文件
|
|
360
|
+
└── COMPARISON.md # Node.js 与 Python 版本对比
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## 📄 License
|
|
364
|
+
|
|
365
|
+
MIT
|