wecom-aibot-py-sdk 2.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.
@@ -0,0 +1,5 @@
1
+ #企业微信后台获取的机器人 ID
2
+ WECHAT_BOT_ID=your-bot-id
3
+
4
+ #企业微信后台获取的机器人 Secret
5
+ WECHAT_BOT_SECRET=your-bot-secret
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual Environment
27
+ venv/
28
+ ENV/
29
+ env/
30
+ .venv
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+
39
+ # Environment variables
40
+ .env
41
+ .env.local
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # Test
48
+ .pytest_cache/
49
+ .coverage
50
+ htmlcov/
51
+ tmp/
@@ -0,0 +1 @@
1
+ 3.15
@@ -0,0 +1,39 @@
1
+ # Node.js 与 Python SDK 功能对照
2
+
3
+ 以本目录中的 Node.js 1.0.6 为对齐基准;Python 已补齐此前缺失的运行时功能。
4
+
5
+ | 能力 | Node.js | Python |
6
+ | --- | --- | --- |
7
+ | WebSocket、认证、心跳、指数退避重连 | 支持 | 支持 |
8
+ | 文本/图片/混排/语音/文件/视频事件 | 支持 | 支持 |
9
+ | 流式回复、欢迎语、模板卡片、卡片更新 | 支持 | 支持 |
10
+ | 主动发送 Markdown 和卡片 | 支持 | 支持 |
11
+ | 素材分片上传、动态并发、每片最多重试两次 | `uploadMedia` | `upload_media` |
12
+ | 被动/主动发送媒体 | `replyMedia` / `sendMediaMessage` | `reply_media` / `send_media_message` |
13
+ | 流式中间帧防积压 | `replyStreamNonBlocking` | `reply_stream_non_blocking` |
14
+ | 检查未确认回执 | `hasPendingReplyAck` | `has_pending_reply_ack` |
15
+ | 回复队列上限(默认 500) | `maxReplyQueueSize` | `max_reply_queue_size` |
16
+ | 独立认证失败重试上限(默认 5) | `maxAuthFailureAttempts` | `max_auth_failure_attempts` |
17
+ | 被踢下线时清理并停止重连 | 支持 | 支持 |
18
+ | 认证附加字段 | `scene`, `plug_version` | `scene`, `plug_version` |
19
+ | 自定义连接/TLS 参数 | `wsOptions`(ws 参数) | `ws_options`(websockets 参数) |
20
+ | 下载文件并 AES 解密 | `downloadFile` | `download_file` |
21
+ | 通用消息加解密、签名和验签 | `WecomCrypto` | `WecomCrypto` |
22
+
23
+ ## 语言与接口差异
24
+
25
+ - Node.js 使用 Promise;Python 使用 asyncio,异步接口必须 `await`,另有 `run()` 启动方法。
26
+ - Python 方法和配置使用 snake_case;协议中的 JSON 字段仍保留原名称。
27
+ - Node.js 有细粒度消息与卡片接口;Python 继续使用字典表示协议体,素材上传提供 TypedDict。类型注解不执行运行时类型校验。
28
+ - Node.js 下载返回 `{buffer, filename}`;Python 返回 `(bytes, filename)`。
29
+ - `reply_stream_non_blocking` 在前一帧等待 ACK 时返回 `"skipped"`;最终帧总是走正常队列,不因防积压逻辑被跳过,仍可能因连接、队列上限或服务端错误而失败。
30
+ - `ws_options` 直接传给 Python `websockets.connect`,不能照搬 Node.js 的 `ca`、`cert` 等键。自定义证书使用 `ssl.SSLContext`。
31
+ - Python 拒绝空素材和超过 50 MiB 的素材;企业微信服务端还可能按媒体类型施加更严格限制。
32
+
33
+ ## 验证
34
+
35
+ `tests/test_features.py` 覆盖上传、媒体、流式、视频、回执、重试、连接配置和加解密,并包含基于本机临时端口的 WebSocket 集成测试。真实企业微信服务端验收需使用有效机器人凭证。
36
+
37
+ ## Python 媒体便捷扩展
38
+
39
+ 提供 `MediaType` 枚举及 `reply_image`、`reply_file`、`reply_voice`、`reply_video`,后者支持 `title` 和 `description`。`upload_media` 增加可选 `md5` 参数,继续使用 options 字典与结构化返回值,保留并发及重试。`examples/media_echo.py` 演示图片回显和文本转文件。心跳失联通过独立且去重的任务触发重连,不依赖底层关闭先完成。
@@ -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,474 @@
1
+ Metadata-Version: 2.5
2
+ Name: wecom-aibot-py-sdk
3
+ Version: 2.0.0
4
+ Summary: 企业微信智能机器人 Python SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
5
+ Project-URL: Homepage, https://github.com/WecomTeam/wecom-aibot-python-sdk
6
+ Project-URL: Repository, https://github.com/WecomTeam/wecom-aibot-python-sdk
7
+ Author-email: WeCom <wecom@tencent.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: aibot,sdk,websocket,wechat-work,wecom
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.15
15
+ Classifier: Topic :: Communications :: Chat
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.15
18
+ Requires-Dist: aiohttp>=3.9
19
+ Requires-Dist: certifi>=2023.0
20
+ Requires-Dist: cryptography>=42.0
21
+ Requires-Dist: pyee>=11.0
22
+ Requires-Dist: python-socks[asyncio]>=2.4.3
23
+ Requires-Dist: websockets>=12.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # wecom-aibot-py-sdk (Python)
27
+
28
+ 企业微信智能机器人 Python SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
29
+
30
+ > 本项目是 [@wecom/aibot-node-sdk](https://www.npmjs.com/package/@wecom/aibot-node-sdk)(Node.js 版)的 Python 等价实现。
31
+ > 补齐了官方NodeSDK更新未同步的功能。
32
+ > 借鉴了[@ticoAg/wecom-aibot-python-sdk](https://github.com/ticoAg/wecom-aibot-python-sdk)的扩展功能。
33
+
34
+ ## 新增功能:素材上传、媒体消息和连接配置
35
+
36
+ ```python
37
+ from pathlib import Path
38
+
39
+ # 上传并回复文件;type 支持 file / image / voice / video
40
+ media = await ws_client.upload_media(
41
+ Path("document.pdf").read_bytes(),
42
+ {"type": "file", "filename": "document.pdf"},
43
+ )
44
+ await ws_client.reply_media(frame, "file", media["media_id"])
45
+ await ws_client.send_media_message("target_userid", "file", media["media_id"])
46
+
47
+ # 视频可附带标题和描述
48
+ await ws_client.reply_media(frame, "video", video_media_id,
49
+ {"title": "演示", "description": "操作说明"})
50
+
51
+ # 前一帧未收到 ACK 时跳过当前中间帧,返回 "skipped";最终帧仍进入队列
52
+ await ws_client.reply_stream_non_blocking(frame, stream_id, "中间内容", False)
53
+ await ws_client.reply_stream_non_blocking(frame, stream_id, "最终完整内容", True)
54
+ ```
55
+
56
+ 上传按 512 KiB 分片,最多 100 片(50 MiB);1~4 片全部并发,5~10 片并发 3,更多片并发 2。每片失败后最多重试两次,全部成功后才完成上传。空素材会被拒绝;服务端仍可能有各媒体类型自己的大小和格式限制。
57
+
58
+ 新增 `message.video` 事件。`event.disconnected_event` 表示被新连接踢下线,此时 SDK 清理等待中的请求、关闭连接并停止自动重连;可显式调用 `await connect()` 重新连接。
59
+
60
+ 新增配置:
61
+
62
+ | 参数 | 默认值 | 说明 |
63
+ | --- | --- | --- |
64
+ | `scene` | `None` | 可选认证场景值 |
65
+ | `plug_version` | `None` | 可选插件版本 |
66
+ | `max_auth_failure_attempts` | `5` | 首次认证失败后允许的重试次数,`-1` 为无限 |
67
+ | `max_reply_queue_size` | `500` | 单个 req_id 的回复队列上限,超过则拒绝新消息 |
68
+ | `ws_options` | `{}` | 传入 `websockets.connect` 的 Python 连接参数 |
69
+
70
+ 认证失败会主动关闭连接并指数退避重试;认证成功后重置认证与网络重试计数。耗尽后分别触发 `WSAuthFailureError`、`WSReconnectExhaustedError`,可在 `error` 事件中按异常类型或 `code` 判断。
71
+
72
+ 自定义 CA 示例:
73
+
74
+ ```python
75
+ import ssl
76
+
77
+ context = ssl.create_default_context(cafile="company-ca.pem")
78
+ options = WSClientOptions(
79
+ bot_id="bot-id", secret="secret",
80
+ ws_url="wss://your-server.example",
81
+ ws_options={"ssl": context},
82
+ )
83
+ ```
84
+
85
+ 通用消息加解密(独立于文件解密):
86
+
87
+ ```python
88
+ from aibot import WecomCrypto
89
+
90
+ crypto = WecomCrypto("token", "your_encoding_aes_key", receive_id="your_corp_or_bot_id")
91
+ result = crypto.encrypt("消息正文", "1234567890", "nonce")
92
+ assert crypto.verify_signature(result["signature"], "1234567890", "nonce", result["encrypt"])
93
+ assert crypto.decrypt(result["encrypt"]) == "消息正文"
94
+ ```
95
+
96
+ 在 SDK 根目录运行测试(集成测试需要允许监听本机临时端口):
97
+
98
+ ```bash
99
+ uv run python -m unittest discover -s tests -v
100
+ ```
101
+
102
+ ### 媒体便捷接口
103
+
104
+ 支持 `MediaType.File/Image/Voice/Video` 枚举和对应字符串。上传保留原有 options 参数与字典返回值,也可传入预计算的 MD5:
105
+
106
+ ```python
107
+ from aibot import MediaType
108
+
109
+ media = await ws_client.upload_media(
110
+ image_bytes, {"type": MediaType.Image, "filename": "image.png"},
111
+ # md5="预先计算的 MD5", # 可选;省略时自动计算
112
+ )
113
+ await ws_client.reply_image(frame, media["media_id"])
114
+ await ws_client.reply_file(frame, file_media_id)
115
+ await ws_client.reply_voice(frame, voice_media_id)
116
+ await ws_client.reply_video(frame, video_media_id, title="演示", description="操作说明")
117
+ ```
118
+
119
+ 以上便捷方法复用 `reply_media`。分片并发与重试保持不变。心跳超时会独立调度重连,避免等待底层关闭阻塞恢复;复用现有重连任务去重与手动断开取消逻辑。
120
+
121
+ 多媒体回显示例:`uv run examples/media_echo.py`(使用 `.env` 中的 `WECHAT_BOT_ID` 和 `WECHAT_BOT_SECRET`)。支持图片原样回复、文本转为 txt 文件回复。
122
+
123
+ ## 发布到 PyPI
124
+
125
+ 工作流位于 `.github/workflows/publish.yml`,发布 GitHub Release 时自动构建并通过 PyPI Trusted Publishing 上传,无需配置 API Token。普通 push 不会发布。
126
+
127
+ 首次使用需要完成账户侧配置:
128
+
129
+ 1. 在 GitHub 仓库 Settings → Environments 创建 `pypi` 环境。
130
+ 2. 在 PyPI 的项目管理 → Publishing 添加 GitHub Trusted Publisher;如果是尚未创建的新包,在 <https://pypi.org/manage/account/publishing/> 添加 Pending Publisher。
131
+ 3. 按当前 Git remote 填写:Owner 为 `oxy-projects`,Repository 为 `wecom-aibot-python-sdk`,Workflow 为 `publish.yml`,Environment 为 `pypi`。PyPI 项目名为 `wecom-aibot-py-sdk`。仓库转移后需同步更新这些配置。
132
+
133
+ 已有 PyPI 同名项目必须拥有维护权限;若属于其他维护者,需要先更换为可用的包名,并同步修改 `pyproject.toml` 和工作流中的 PyPI 链接。
134
+
135
+ 每次发布先同步修改 `pyproject.toml` 的 `version` 和 `src/aibot/__init__.py` 的 `__version__`,提交后针对该提交发布 `v版本号` 的 GitHub Release(当前为 `v2.0.0`)。工作流会检查这三个版本一致。已上传到 PyPI 的版本不能覆盖,需要增加版本号。
136
+
137
+ 当前包要求 Python >=3.15,工作流也使用 Python 3.15。标准 Ubuntu 运行器在公开仓库中运行免费;构建产物仅保留一天。
138
+
139
+ ## ✨ 特性
140
+
141
+ - 🔗 **WebSocket 长连接** — 基于 `wss://openws.work.weixin.qq.com` 内置默认地址,开箱即用
142
+ - 🔐 **自动认证** — 连接建立后自动发送认证帧(bot_id + secret)
143
+ - 💓 **心跳保活** — 自动维护心跳,连续未收到 ack 时自动判定连接异常
144
+ - 🔄 **断线重连** — 指数退避重连策略(1s → 2s → 4s → ... → 30s 上限),支持自定义最大重连次数
145
+ - 📨 **消息分发** — 自动解析消息类型并触发对应事件(text / image / mixed / voice / file)
146
+ - 🌊 **流式回复** — 内置流式回复方法,支持 Markdown 和图文混排
147
+ - 🃏 **模板卡片** — 支持回复模板卡片消息、流式+卡片组合回复、更新卡片
148
+ - 📤 **主动推送** — 支持向指定会话主动发送 Markdown 或模板卡片消息,无需依赖回调帧
149
+ - 📡 **事件回调** — 支持进入会话、模板卡片按钮点击、用户反馈等事件
150
+ - ⏩ **串行回复队列** — 同一 req_id 的回复消息串行发送,自动等待回执
151
+ - 🔑 **文件下载解密** — 内置 AES-256-CBC 文件解密,每个图片/文件消息自带独立的 aeskey
152
+ - 🪵 **可插拔日志** — 支持自定义 Logger,内置带时间戳的 DefaultLogger
153
+ - 🐍 **asyncio 原生** — 基于 Python asyncio 异步架构,支持 async/await
154
+
155
+ ## 📦 安装
156
+
157
+ ```bash
158
+ uv add wecom-aibot-py-sdk
159
+ ```
160
+
161
+ **依赖:**
162
+ - Python >= 3.15
163
+ - websockets >= 12.0
164
+ - aiohttp >= 3.9
165
+ - pyee >= 11.0
166
+ - cryptography >= 42.0
167
+ - certifi>=2023.0
168
+ - python-dotenv>=1.0 (可选,用于加载 .env 文件)
169
+
170
+ ## 本地开发与构建
171
+
172
+ 在本项目根目录执行(需预先安装 uv):
173
+
174
+ ```bash
175
+ uv python install 3.15
176
+ uv sync --locked
177
+ cp .env.example .env
178
+ uv run examples/basic.py
179
+ uv run python -m unittest discover -s tests -v
180
+ uv build
181
+ ```
182
+
183
+ `uv sync` 自动创建 `.venv`,以可编辑模式安装 SDK,并默认安装 `dev` 依赖组中的 `python-dotenv`。示例通过 `uv run` 使用已安装的 SDK。仅安装运行依赖时使用 `uv sync --locked --no-dev`。
184
+
185
+ 依赖统一在 `pyproject.toml` 中维护:`uv add 包名` 添加运行依赖,`uv add --dev 包名` 添加开发依赖;变更后提交 `pyproject.toml` 与 `uv.lock`。`uv build` 使用 hatchling 生成 wheel 和源码包,输出到 `dist/`。
186
+
187
+ ## ⚙️ 配置
188
+
189
+ ```bash
190
+ # 复制示例配置文件
191
+ cp .env.example .env
192
+
193
+ # 编辑 .env 文件,填入真实配置
194
+ # WECHAT_BOT_ID=your-bot-id
195
+ # WECHAT_BOT_SECRET=your-bot-secret
196
+ ```
197
+
198
+
199
+ ## 🚀 快速开始
200
+
201
+ ```python
202
+ import asyncio
203
+ import os
204
+ from dotenv import load_dotenv
205
+ from aibot import WSClient, WSClientOptions, generate_req_id
206
+
207
+ # 加载 .env 文件中的环境变量
208
+ load_dotenv()
209
+
210
+ # 1. 创建客户端实例
211
+ ws_client = WSClient(
212
+ WSClientOptions(
213
+ bot_id=os.getenv('WECHAT_BOT_ID'), # 企业微信后台获取的机器人 ID
214
+ secret=os.getenv('WECHAT_BOT_SECRET'), # 企业微信后台获取的机器人 Secret
215
+ )
216
+ )
217
+
218
+ # 2. 监听认证成功
219
+ @ws_client.on('authenticated')
220
+ def on_authenticated():
221
+ print('🔐 认证成功')
222
+
223
+ # 3. 监听文本消息并进行流式回复
224
+ @ws_client.on('message.text')
225
+ async def on_text(frame):
226
+ content = frame.get('body', {}).get('text', {}).get('content', '')
227
+ print(f'收到文本: {content}')
228
+
229
+ stream_id = generate_req_id('stream')
230
+
231
+ # 发送流式中间内容
232
+ await ws_client.reply_stream(frame, stream_id, '正在思考中...', False)
233
+
234
+ # 发送最终结果
235
+ await asyncio.sleep(1)
236
+ await ws_client.reply_stream(frame, stream_id, f'你好!你说的是: "{content}"', True)
237
+
238
+ # 4. 监听进入会话事件(发送欢迎语)
239
+ @ws_client.on('event.enter_chat')
240
+ async def on_enter_chat(frame):
241
+ await ws_client.reply_welcome(frame, {
242
+ 'msgtype': 'text',
243
+ 'text': {'content': '您好!我是智能助手,有什么可以帮您的吗?'},
244
+ })
245
+
246
+ # 5. 启动(便捷方法,内部管理事件循环)
247
+ ws_client.run()
248
+ ```
249
+
250
+ 或者手动管理事件循环:
251
+
252
+ ```python
253
+ async def main():
254
+ await ws_client.connect()
255
+ # 保持运行
256
+ await asyncio.Event().wait()
257
+
258
+ asyncio.run(main())
259
+ ```
260
+
261
+ ## 📖 API 文档
262
+
263
+ ### `WSClient`
264
+
265
+ 核心客户端类,继承自 `pyee.AsyncIOEventEmitter`,提供连接管理、消息收发等功能。
266
+
267
+ #### 构造函数
268
+
269
+ ```python
270
+ ws_client = WSClient(options: WSClientOptions)
271
+ ```
272
+
273
+ #### 方法
274
+
275
+ | 方法 | 说明 | 返回值 |
276
+ | --- | --- | --- |
277
+ | `await connect()` | 建立 WebSocket 连接,连接后自动认证 | `WSClient`(支持链式调用) |
278
+ | `disconnect()` | 主动断开连接 | `None` |
279
+ | `await reply(frame, body, cmd?)` | 通过 WebSocket 通道发送回复消息(通用方法) | `WsFrame` |
280
+ | `await reply_stream(frame, stream_id, content, finish?, msg_item?, feedback?)` | 发送流式文本回复(便捷方法,支持 Markdown) | `WsFrame` |
281
+ | `await reply_welcome(frame, body)` | 发送欢迎语回复(支持文本或模板卡片),需在收到事件 5s 内调用 | `WsFrame` |
282
+ | `await reply_template_card(frame, template_card, feedback?)` | 回复模板卡片消息 | `WsFrame` |
283
+ | `await reply_stream_with_card(frame, stream_id, content, finish?, ...)` | 发送流式消息 + 模板卡片组合回复 | `WsFrame` |
284
+ | `await update_template_card(frame, template_card, userids?)` | 更新模板卡片,需在收到事件 5s 内调用 | `WsFrame` |
285
+ | `await send_message(chatid, body)` | 主动发送消息(支持 Markdown 或模板卡片),无需依赖回调帧 | `WsFrame` |
286
+ | `await download_file(url, aes_key?)` | 下载文件并使用 AES 密钥解密 | `tuple[bytes, str \| None]` |
287
+ | `run()` | 便捷启动方法(创建事件循环并连接) | `None` |
288
+
289
+ #### 属性
290
+
291
+ | 属性 | 说明 | 类型 |
292
+ | --- | --- | --- |
293
+ | `is_connected` | 当前 WebSocket 连接状态 | `bool` |
294
+ | `api` | 内部 API 客户端实例(高级用途) | `WeComApiClient` |
295
+
296
+ ### `reply_stream` 详细说明
297
+
298
+ ```python
299
+ await ws_client.reply_stream(
300
+ frame, # 收到的原始 WebSocket 帧(透传 req_id)
301
+ stream_id: str, # 流式消息 ID(使用 generate_req_id('stream') 生成)
302
+ content: str, # 回复内容(支持 Markdown)
303
+ finish: bool = False, # 是否结束流式消息
304
+ msg_item: list = None, # 图文混排项(仅 finish=True 时有效)
305
+ feedback: dict = None, # 反馈信息(仅首次回复时设置)
306
+ )
307
+ ```
308
+
309
+ ### `reply_welcome` 详细说明
310
+
311
+ 发送欢迎语回复,需在收到 `event.enter_chat` 事件 5 秒内调用。
312
+
313
+ ```python
314
+ # 文本欢迎语
315
+ await ws_client.reply_welcome(frame, {
316
+ 'msgtype': 'text',
317
+ 'text': {'content': '欢迎!'},
318
+ })
319
+
320
+ # 模板卡片欢迎语
321
+ await ws_client.reply_welcome(frame, {
322
+ 'msgtype': 'template_card',
323
+ 'template_card': {'card_type': 'text_notice', 'main_title': {'title': '欢迎'}},
324
+ })
325
+ ```
326
+
327
+ ### `reply_stream_with_card` 详细说明
328
+
329
+ ```python
330
+ await ws_client.reply_stream_with_card(
331
+ frame, # 收到的原始 WebSocket 帧
332
+ stream_id: str, # 流式消息 ID
333
+ content: str, # 回复内容(支持 Markdown)
334
+ finish: bool = False, # 是否结束流式消息
335
+ msg_item: list = None, # 图文混排项(仅 finish=True 时有效)
336
+ stream_feedback: dict = None, # 流式消息反馈信息(首次回复时设置)
337
+ template_card: dict = None, # 模板卡片内容(同一消息只能回复一次)
338
+ card_feedback: dict = None, # 模板卡片反馈信息
339
+ )
340
+ ```
341
+
342
+ ### `send_message` 详细说明
343
+
344
+ 主动向指定会话推送消息,无需依赖收到的回调帧。
345
+
346
+ ```python
347
+ # 发送 Markdown 消息
348
+ await ws_client.send_message('userid_or_chatid', {
349
+ 'msgtype': 'markdown',
350
+ 'markdown': {'content': '这是一条**主动推送**的消息'},
351
+ })
352
+
353
+ # 发送模板卡片消息
354
+ await ws_client.send_message('userid_or_chatid', {
355
+ 'msgtype': 'template_card',
356
+ 'template_card': {'card_type': 'text_notice', 'main_title': {'title': '通知'}},
357
+ })
358
+ ```
359
+
360
+ ### `download_file` 使用示例
361
+
362
+ ```python
363
+ # aes_key 取自消息体中的 image.aeskey 或 file.aeskey
364
+ @ws_client.on('message.image')
365
+ async def on_image(frame):
366
+ body = frame.get('body', {})
367
+ image = body.get('image', {})
368
+ buffer, filename = await ws_client.download_file(image.get('url'), image.get('aeskey'))
369
+ print(f'文件名: {filename}, 大小: {len(buffer)} bytes')
370
+ ```
371
+
372
+ ## ⚙️ 配置选项
373
+
374
+ `WSClientOptions` 完整配置:
375
+
376
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
377
+ | --- | --- | --- | --- | --- |
378
+ | `bot_id` | `str` | ✅ | — | 机器人 ID(企业微信后台获取) |
379
+ | `secret` | `str` | ✅ | — | 机器人 Secret(企业微信后台获取) |
380
+ | `reconnect_interval` | `int` | — | `1000` | 重连基础延迟(毫秒),实际延迟按指数退避递增 |
381
+ | `max_reconnect_attempts` | `int` | — | `10` | 最大重连次数(`-1` 表示无限重连) |
382
+ | `heartbeat_interval` | `int` | — | `30000` | 心跳间隔(毫秒) |
383
+ | `request_timeout` | `int` | — | `10000` | HTTP 请求超时时间(毫秒) |
384
+ | `ws_url` | `str` | — | `wss://openws.work.weixin.qq.com` | 自定义 WebSocket 连接地址 |
385
+ | `logger` | `Logger` | — | `DefaultLogger` | 自定义日志实例 |
386
+
387
+ ## 📡 事件列表
388
+
389
+ 所有事件均通过 `@ws_client.on(event)` 装饰器或 `ws_client.on(event, handler)` 监听:
390
+
391
+ | 事件 | 回调参数 | 说明 |
392
+ | --- | --- | --- |
393
+ | `connected` | — | WebSocket 连接建立 |
394
+ | `authenticated` | — | 认证成功 |
395
+ | `disconnected` | `reason: str` | 连接断开 |
396
+ | `reconnecting` | `attempt: int` | 正在重连(第 N 次) |
397
+ | `error` | `error: Exception` | 发生错误 |
398
+ | `message` | `frame: WsFrame` | 收到消息(所有类型) |
399
+ | `message.text` | `frame: WsFrame` | 收到文本消息 |
400
+ | `message.image` | `frame: WsFrame` | 收到图片消息 |
401
+ | `message.mixed` | `frame: WsFrame` | 收到图文混排消息 |
402
+ | `message.voice` | `frame: WsFrame` | 收到语音消息 |
403
+ | `message.file` | `frame: WsFrame` | 收到文件消息 |
404
+ | `event` | `frame: WsFrame` | 收到事件回调(所有事件类型) |
405
+ | `event.enter_chat` | `frame: WsFrame` | 收到进入会话事件 |
406
+ | `event.template_card_event` | `frame: WsFrame` | 收到模板卡片事件 |
407
+ | `event.feedback_event` | `frame: WsFrame` | 收到用户反馈事件 |
408
+
409
+ ## 📋 消息类型
410
+
411
+ SDK 支持以下消息类型(`MessageType` 枚举):
412
+
413
+ | 类型 | 值 | 说明 |
414
+ | --- | --- | --- |
415
+ | `MessageType.Text` | `'text'` | 文本消息 |
416
+ | `MessageType.Image` | `'image'` | 图片消息 |
417
+ | `MessageType.Mixed` | `'mixed'` | 图文混排消息 |
418
+ | `MessageType.Voice` | `'voice'` | 语音消息 |
419
+ | `MessageType.File` | `'file'` | 文件消息 |
420
+
421
+ SDK 支持以下事件类型(`EventType` 枚举):
422
+
423
+ | 类型 | 值 | 说明 |
424
+ | --- | --- | --- |
425
+ | `EventType.EnterChat` | `'enter_chat'` | 进入会话事件 |
426
+ | `EventType.TemplateCardEvent` | `'template_card_event'` | 模板卡片事件 |
427
+ | `EventType.FeedbackEvent` | `'feedback_event'` | 用户反馈事件 |
428
+
429
+ ## 🪵 自定义日志
430
+
431
+ 实现 `Logger` Protocol 接口即可自定义日志输出:
432
+
433
+ ```python
434
+ class MyLogger:
435
+ def debug(self, message: str, *args) -> None:
436
+ pass # 静默 debug 日志
437
+
438
+ def info(self, message: str, *args) -> None:
439
+ print(f"[INFO] {message}", *args)
440
+
441
+ def warn(self, message: str, *args) -> None:
442
+ print(f"[WARN] {message}", *args)
443
+
444
+ def error(self, message: str, *args) -> None:
445
+ print(f"[ERROR] {message}", *args)
446
+
447
+ ws_client = WSClient(
448
+ WSClientOptions(
449
+ bot_id='your-bot-id',
450
+ secret='your-bot-secret',
451
+ logger=MyLogger(),
452
+ )
453
+ )
454
+ ```
455
+
456
+ ## 📂 项目结构
457
+
458
+ ```text
459
+ wecom-aibot-python-sdk/
460
+ ├── src/aibot/ # SDK 源码,安装后仍使用 import aibot
461
+ ├── tests/ # unittest 单元与本地集成测试
462
+ ├── examples/ # basic.py / media_echo.py / comparison_test.py
463
+ ├── pyproject.toml # 元数据、依赖、开发依赖与 hatchling 构建配置
464
+ ├── uv.lock # uv 依赖锁文件,纳入版本控制
465
+ ├── .python-version # Python 3.15
466
+ ├── .env.example # 示例环境变量
467
+ ├── README.md
468
+ ├── COMPARISON.md
469
+ └── LICENSE
470
+ ```
471
+
472
+ ## 📄 License
473
+
474
+ MIT