wecom-aibot-sdk-python 0.1.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_sdk_python-0.1.0/.github/workflows/publish.yml +48 -0
- wecom_aibot_sdk_python-0.1.0/.gitignore +30 -0
- wecom_aibot_sdk_python-0.1.0/.python-version +1 -0
- wecom_aibot_sdk_python-0.1.0/PKG-INFO +176 -0
- wecom_aibot_sdk_python-0.1.0/README.md +162 -0
- wecom_aibot_sdk_python-0.1.0/docs/README_zh.md +162 -0
- wecom_aibot_sdk_python-0.1.0/examples/basic.py +105 -0
- wecom_aibot_sdk_python-0.1.0/main.py +5 -0
- wecom_aibot_sdk_python-0.1.0/pyproject.toml +32 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/__init__.py +38 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/api.py +59 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/client.py +425 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/crypto.py +50 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/logger.py +34 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/message_handler.py +94 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/types/__init__.py +33 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/types/api.py +38 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/types/common.py +12 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/types/config.py +21 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/types/event.py +11 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/types/message.py +189 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/utils.py +20 -0
- wecom_aibot_sdk_python-0.1.0/src/wecom_aibot_sdk/ws.py +329 -0
- wecom_aibot_sdk_python-0.1.0/tests/test_basic.py +179 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
test:
|
|
9
|
+
description: 'Publish to TestPyPI'
|
|
10
|
+
required: false
|
|
11
|
+
default: 'false'
|
|
12
|
+
type: boolean
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build-and-publish:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
permissions:
|
|
21
|
+
id-token: write
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout code
|
|
25
|
+
uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.10"
|
|
31
|
+
|
|
32
|
+
- name: Install build dependencies
|
|
33
|
+
run: |
|
|
34
|
+
python -m pip install --upgrade pip
|
|
35
|
+
pip install build
|
|
36
|
+
|
|
37
|
+
- name: Build package
|
|
38
|
+
run: python -m build
|
|
39
|
+
|
|
40
|
+
- name: Publish to TestPyPI
|
|
41
|
+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.test == true }}
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
43
|
+
with:
|
|
44
|
+
repository-url: https://test.pypi.org/legacy/
|
|
45
|
+
|
|
46
|
+
- name: Publish to PyPI
|
|
47
|
+
if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.test != true) }}
|
|
48
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# IDE
|
|
15
|
+
.idea/
|
|
16
|
+
.vscode/
|
|
17
|
+
*.swp
|
|
18
|
+
*.swo
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
22
|
+
Thumbs.db
|
|
23
|
+
|
|
24
|
+
# Test/coverage
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.coverage
|
|
27
|
+
htmlcov/
|
|
28
|
+
|
|
29
|
+
# Logs
|
|
30
|
+
*.log
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wecom-aibot-sdk-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WeCom AI Bot Python SDK - Based on WebSocket long connection, provides core capabilities including message sending/receiving, streaming replies, template cards, event callbacks, and file download decryption
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
7
|
+
Requires-Dist: pycryptodome>=3.20.0
|
|
8
|
+
Requires-Dist: websockets>=12.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# WeCom AI Bot Python SDK
|
|
16
|
+
|
|
17
|
+
Enterprise WeChat AI Bot Python SDK - Based on WebSocket long connection, providing message sending/receiving, streaming replies, template cards, event callbacks, file download/decryption and other core capabilities.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **WebSocket Long Connection** - Built-in default address `wss://openws.work.weixin.qq.com`, ready to use
|
|
22
|
+
- **Auto Authentication** - Automatically sends authentication frame after connection (botId + secret)
|
|
23
|
+
- **Heartbeat Keep-Alive** - Automatic heartbeat maintenance, auto-detects connection issues when ACKs are missing
|
|
24
|
+
- **Auto Reconnect** - Exponential backoff reconnection strategy (1s → 2s → 4s → ... → 30s max)
|
|
25
|
+
- **Message Dispatch** - Auto-parses message types and triggers corresponding events (text/image/mixed/voice/file)
|
|
26
|
+
- **Streaming Reply** - Built-in streaming reply methods, supports Markdown and mixed content
|
|
27
|
+
- **Template Cards** - Supports replying with template card messages, stream+card combo replies, card updates
|
|
28
|
+
- **Proactive Push** - Proactively send Markdown or template card messages to specified chats
|
|
29
|
+
- **Event Callbacks** - Supports enter_chat, template_card_event, feedback_event
|
|
30
|
+
- **Serial Reply Queue** - Replies with same req_id are sent serially, auto-waits for receipt
|
|
31
|
+
- **File Download & Decryption** - Built-in AES-256-CBC file decryption, each image/file message has its own aeskey
|
|
32
|
+
- **Pluggable Logging** - Supports custom Logger, includes DefaultLogger with timestamps
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install wecom-aibot-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from wecom_aibot_sdk import WSClient, generate_req_id
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
# 1. Create client instance
|
|
48
|
+
client = WSClient({
|
|
49
|
+
"bot_id": "your-bot-id",
|
|
50
|
+
"secret": "your-bot-secret",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# 2. Listen for text messages and reply with streaming
|
|
54
|
+
async def on_text(frame):
|
|
55
|
+
content = frame.body.get("text", {}).get("content", "")
|
|
56
|
+
stream_id = generate_req_id("stream")
|
|
57
|
+
|
|
58
|
+
# Send intermediate content
|
|
59
|
+
await client.reply_stream(frame, stream_id, "Thinking...", finish=False)
|
|
60
|
+
|
|
61
|
+
# Send final result
|
|
62
|
+
await client.reply_stream(frame, stream_id, f'You said: "{content}"', finish=True)
|
|
63
|
+
|
|
64
|
+
client.on("message.text", on_text)
|
|
65
|
+
|
|
66
|
+
# 3. Listen for enter_chat event (send welcome)
|
|
67
|
+
async def on_enter(frame):
|
|
68
|
+
await client.reply_welcome(frame, {
|
|
69
|
+
"msgtype": "text",
|
|
70
|
+
"text": {"content": "Hello! How can I help you?"},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
client.on("event.enter_chat", on_enter)
|
|
74
|
+
|
|
75
|
+
# 4. Connect
|
|
76
|
+
await client.connect_async()
|
|
77
|
+
|
|
78
|
+
# Keep running
|
|
79
|
+
while client.is_connected:
|
|
80
|
+
await asyncio.sleep(1)
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### WSClient
|
|
88
|
+
|
|
89
|
+
Core client class providing connection management, message sending/receiving.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client = WSClient({
|
|
93
|
+
"bot_id": "your-bot-id",
|
|
94
|
+
"secret": "your-bot-secret",
|
|
95
|
+
# Optional:
|
|
96
|
+
"reconnect_interval": 1000, # Reconnect base delay (ms)
|
|
97
|
+
"max_reconnect_attempts": 10, # Max reconnect attempts (-1 for infinite)
|
|
98
|
+
"heartbeat_interval": 30000, # Heartbeat interval (ms)
|
|
99
|
+
"request_timeout": 10000, # HTTP request timeout (ms)
|
|
100
|
+
"ws_url": "wss://...", # Custom WebSocket URL
|
|
101
|
+
"logger": custom_logger, # Custom logger instance
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Methods
|
|
106
|
+
|
|
107
|
+
| Method | Description | Returns |
|
|
108
|
+
|--------|-------------|---------|
|
|
109
|
+
| `connect_async()` | Establish WebSocket connection | `None` |
|
|
110
|
+
| `disconnect()` | Disconnect | `None` |
|
|
111
|
+
| `reply(frame, body, cmd?)` | Send reply message (generic) | `None` |
|
|
112
|
+
| `reply_stream(frame, stream_id, content, finish?, msg_item?, feedback?)` | Send streaming reply | `None` |
|
|
113
|
+
| `reply_welcome(frame, body)` | Send welcome reply (within 5s of event) | `None` |
|
|
114
|
+
| `reply_template_card(frame, template_card, feedback?)` | Reply with template card | `None` |
|
|
115
|
+
| `reply_stream_with_card(frame, stream_id, content, finish?, options?)` | Send stream + card combo | `None` |
|
|
116
|
+
| `update_template_card(frame, template_card, userids?)` | Update template card (within 5s) | `None` |
|
|
117
|
+
| `send_message(chatid, body)` | Proactively send message | `None` |
|
|
118
|
+
| `download_file(url, aes_key)` | Download and decrypt file | `tuple[bytes, str?]` |
|
|
119
|
+
|
|
120
|
+
#### Events
|
|
121
|
+
|
|
122
|
+
| Event | Callback | Description |
|
|
123
|
+
|-------|----------|-------------|
|
|
124
|
+
| `connected` | `()` | WebSocket connected |
|
|
125
|
+
| `authenticated` | `()` | Authentication successful |
|
|
126
|
+
| `disconnected` | `(reason)` | Connection lost |
|
|
127
|
+
| `reconnecting` | `(attempt)` | Reconnecting (attempt N) |
|
|
128
|
+
| `error` | `(frame)` | Error occurred |
|
|
129
|
+
| `message` | `(frame)` | Any message received |
|
|
130
|
+
| `message.text` | `(frame)` | Text message |
|
|
131
|
+
| `message.image` | `(frame)` | Image message |
|
|
132
|
+
| `message.mixed` | `(frame)` | Mixed content message |
|
|
133
|
+
| `message.voice` | `(frame)` | Voice message |
|
|
134
|
+
| `message.file` | `(frame)` | File message |
|
|
135
|
+
| `event` | `(frame)` | Any event |
|
|
136
|
+
| `event.enter_chat` | `(frame)` | User entered chat |
|
|
137
|
+
| `event.template_card_event` | `(frame)` | Card button clicked |
|
|
138
|
+
| `event.feedback_event` | `(frame)` | User feedback |
|
|
139
|
+
|
|
140
|
+
## Project Structure
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
wecom_aibot_sdk/
|
|
144
|
+
├── __init__.py # Package entry, exports
|
|
145
|
+
├── client.py # WSClient core client
|
|
146
|
+
├── ws.py # WebSocket connection manager
|
|
147
|
+
├── message_handler.py # Message parsing and event dispatch
|
|
148
|
+
├── api.py # HTTP API client (file download)
|
|
149
|
+
├── crypto.py # AES-256-CBC file decryption
|
|
150
|
+
├── logger.py # Default logger implementation
|
|
151
|
+
├── utils.py # Utility functions (generate_req_id, etc.)
|
|
152
|
+
└── types/ # Type definitions
|
|
153
|
+
├── __init__.py
|
|
154
|
+
├── config.py # Configuration types
|
|
155
|
+
├── event.py # Event types
|
|
156
|
+
├── message.py # Message types
|
|
157
|
+
├── api.py # API/WebSocket frame types
|
|
158
|
+
└── common.py # Common types (Logger)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Install dev dependencies
|
|
165
|
+
pip install -e ".[dev]"
|
|
166
|
+
|
|
167
|
+
# Run tests
|
|
168
|
+
pytest
|
|
169
|
+
|
|
170
|
+
# Format code
|
|
171
|
+
ruff format .
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# WeCom AI Bot Python SDK
|
|
2
|
+
|
|
3
|
+
Enterprise WeChat AI Bot Python SDK - Based on WebSocket long connection, providing message sending/receiving, streaming replies, template cards, event callbacks, file download/decryption and other core capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **WebSocket Long Connection** - Built-in default address `wss://openws.work.weixin.qq.com`, ready to use
|
|
8
|
+
- **Auto Authentication** - Automatically sends authentication frame after connection (botId + secret)
|
|
9
|
+
- **Heartbeat Keep-Alive** - Automatic heartbeat maintenance, auto-detects connection issues when ACKs are missing
|
|
10
|
+
- **Auto Reconnect** - Exponential backoff reconnection strategy (1s → 2s → 4s → ... → 30s max)
|
|
11
|
+
- **Message Dispatch** - Auto-parses message types and triggers corresponding events (text/image/mixed/voice/file)
|
|
12
|
+
- **Streaming Reply** - Built-in streaming reply methods, supports Markdown and mixed content
|
|
13
|
+
- **Template Cards** - Supports replying with template card messages, stream+card combo replies, card updates
|
|
14
|
+
- **Proactive Push** - Proactively send Markdown or template card messages to specified chats
|
|
15
|
+
- **Event Callbacks** - Supports enter_chat, template_card_event, feedback_event
|
|
16
|
+
- **Serial Reply Queue** - Replies with same req_id are sent serially, auto-waits for receipt
|
|
17
|
+
- **File Download & Decryption** - Built-in AES-256-CBC file decryption, each image/file message has its own aeskey
|
|
18
|
+
- **Pluggable Logging** - Supports custom Logger, includes DefaultLogger with timestamps
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install wecom-aibot-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import asyncio
|
|
30
|
+
from wecom_aibot_sdk import WSClient, generate_req_id
|
|
31
|
+
|
|
32
|
+
async def main():
|
|
33
|
+
# 1. Create client instance
|
|
34
|
+
client = WSClient({
|
|
35
|
+
"bot_id": "your-bot-id",
|
|
36
|
+
"secret": "your-bot-secret",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
# 2. Listen for text messages and reply with streaming
|
|
40
|
+
async def on_text(frame):
|
|
41
|
+
content = frame.body.get("text", {}).get("content", "")
|
|
42
|
+
stream_id = generate_req_id("stream")
|
|
43
|
+
|
|
44
|
+
# Send intermediate content
|
|
45
|
+
await client.reply_stream(frame, stream_id, "Thinking...", finish=False)
|
|
46
|
+
|
|
47
|
+
# Send final result
|
|
48
|
+
await client.reply_stream(frame, stream_id, f'You said: "{content}"', finish=True)
|
|
49
|
+
|
|
50
|
+
client.on("message.text", on_text)
|
|
51
|
+
|
|
52
|
+
# 3. Listen for enter_chat event (send welcome)
|
|
53
|
+
async def on_enter(frame):
|
|
54
|
+
await client.reply_welcome(frame, {
|
|
55
|
+
"msgtype": "text",
|
|
56
|
+
"text": {"content": "Hello! How can I help you?"},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
client.on("event.enter_chat", on_enter)
|
|
60
|
+
|
|
61
|
+
# 4. Connect
|
|
62
|
+
await client.connect_async()
|
|
63
|
+
|
|
64
|
+
# Keep running
|
|
65
|
+
while client.is_connected:
|
|
66
|
+
await asyncio.sleep(1)
|
|
67
|
+
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### WSClient
|
|
74
|
+
|
|
75
|
+
Core client class providing connection management, message sending/receiving.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
client = WSClient({
|
|
79
|
+
"bot_id": "your-bot-id",
|
|
80
|
+
"secret": "your-bot-secret",
|
|
81
|
+
# Optional:
|
|
82
|
+
"reconnect_interval": 1000, # Reconnect base delay (ms)
|
|
83
|
+
"max_reconnect_attempts": 10, # Max reconnect attempts (-1 for infinite)
|
|
84
|
+
"heartbeat_interval": 30000, # Heartbeat interval (ms)
|
|
85
|
+
"request_timeout": 10000, # HTTP request timeout (ms)
|
|
86
|
+
"ws_url": "wss://...", # Custom WebSocket URL
|
|
87
|
+
"logger": custom_logger, # Custom logger instance
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Methods
|
|
92
|
+
|
|
93
|
+
| Method | Description | Returns |
|
|
94
|
+
|--------|-------------|---------|
|
|
95
|
+
| `connect_async()` | Establish WebSocket connection | `None` |
|
|
96
|
+
| `disconnect()` | Disconnect | `None` |
|
|
97
|
+
| `reply(frame, body, cmd?)` | Send reply message (generic) | `None` |
|
|
98
|
+
| `reply_stream(frame, stream_id, content, finish?, msg_item?, feedback?)` | Send streaming reply | `None` |
|
|
99
|
+
| `reply_welcome(frame, body)` | Send welcome reply (within 5s of event) | `None` |
|
|
100
|
+
| `reply_template_card(frame, template_card, feedback?)` | Reply with template card | `None` |
|
|
101
|
+
| `reply_stream_with_card(frame, stream_id, content, finish?, options?)` | Send stream + card combo | `None` |
|
|
102
|
+
| `update_template_card(frame, template_card, userids?)` | Update template card (within 5s) | `None` |
|
|
103
|
+
| `send_message(chatid, body)` | Proactively send message | `None` |
|
|
104
|
+
| `download_file(url, aes_key)` | Download and decrypt file | `tuple[bytes, str?]` |
|
|
105
|
+
|
|
106
|
+
#### Events
|
|
107
|
+
|
|
108
|
+
| Event | Callback | Description |
|
|
109
|
+
|-------|----------|-------------|
|
|
110
|
+
| `connected` | `()` | WebSocket connected |
|
|
111
|
+
| `authenticated` | `()` | Authentication successful |
|
|
112
|
+
| `disconnected` | `(reason)` | Connection lost |
|
|
113
|
+
| `reconnecting` | `(attempt)` | Reconnecting (attempt N) |
|
|
114
|
+
| `error` | `(frame)` | Error occurred |
|
|
115
|
+
| `message` | `(frame)` | Any message received |
|
|
116
|
+
| `message.text` | `(frame)` | Text message |
|
|
117
|
+
| `message.image` | `(frame)` | Image message |
|
|
118
|
+
| `message.mixed` | `(frame)` | Mixed content message |
|
|
119
|
+
| `message.voice` | `(frame)` | Voice message |
|
|
120
|
+
| `message.file` | `(frame)` | File message |
|
|
121
|
+
| `event` | `(frame)` | Any event |
|
|
122
|
+
| `event.enter_chat` | `(frame)` | User entered chat |
|
|
123
|
+
| `event.template_card_event` | `(frame)` | Card button clicked |
|
|
124
|
+
| `event.feedback_event` | `(frame)` | User feedback |
|
|
125
|
+
|
|
126
|
+
## Project Structure
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
wecom_aibot_sdk/
|
|
130
|
+
├── __init__.py # Package entry, exports
|
|
131
|
+
├── client.py # WSClient core client
|
|
132
|
+
├── ws.py # WebSocket connection manager
|
|
133
|
+
├── message_handler.py # Message parsing and event dispatch
|
|
134
|
+
├── api.py # HTTP API client (file download)
|
|
135
|
+
├── crypto.py # AES-256-CBC file decryption
|
|
136
|
+
├── logger.py # Default logger implementation
|
|
137
|
+
├── utils.py # Utility functions (generate_req_id, etc.)
|
|
138
|
+
└── types/ # Type definitions
|
|
139
|
+
├── __init__.py
|
|
140
|
+
├── config.py # Configuration types
|
|
141
|
+
├── event.py # Event types
|
|
142
|
+
├── message.py # Message types
|
|
143
|
+
├── api.py # API/WebSocket frame types
|
|
144
|
+
└── common.py # Common types (Logger)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Install dev dependencies
|
|
151
|
+
pip install -e ".[dev]"
|
|
152
|
+
|
|
153
|
+
# Run tests
|
|
154
|
+
pytest
|
|
155
|
+
|
|
156
|
+
# Format code
|
|
157
|
+
ruff format .
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# 企业微信智能机器人 Python SDK
|
|
2
|
+
|
|
3
|
+
基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密等核心能力。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **WebSocket 长连接** - 内置默认地址 `wss://openws.work.weixin.qq.com`,开箱即用
|
|
8
|
+
- **自动认证** - 连接后自动发送认证帧(botId + secret)
|
|
9
|
+
- **心跳保活** - 自动维护心跳,ACK 缺失时自动检测连接问题
|
|
10
|
+
- **自动重连** - 指数退避重连策略(1s → 2s → 4s → ... → 最大 30s)
|
|
11
|
+
- **消息分发** - 自动解析消息类型并触发相应事件(text/image/mixed/voice/file)
|
|
12
|
+
- **流式回复** - 内置流式回复方法,支持 Markdown 和混合内容
|
|
13
|
+
- **模板卡片** - 支持回复模板卡片消息、流式+卡片组合回复、卡片更新
|
|
14
|
+
- **主动推送** - 主动向指定会话发送 Markdown 或模板卡片消息
|
|
15
|
+
- **事件回调** - 支持 enter_chat、template_card_event、feedback_event
|
|
16
|
+
- **串行回复队列** - 相同 req_id 的回复串行发送,自动等待回执
|
|
17
|
+
- **文件下载解密** - 内置 AES-256-CBC 文件解密,每条图片/文件消息有独立 aeskey
|
|
18
|
+
- **可插拔日志** - 支持自定义 Logger,内置带时间戳的 DefaultLogger
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install wecom-aibot-sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import asyncio
|
|
30
|
+
from wecom_aibot_sdk import WSClient, generate_req_id
|
|
31
|
+
|
|
32
|
+
async def main():
|
|
33
|
+
# 1. 创建客户端实例
|
|
34
|
+
client = WSClient({
|
|
35
|
+
"bot_id": "your-bot-id",
|
|
36
|
+
"secret": "your-bot-secret",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
# 2. 监听文本消息并以流式方式回复
|
|
40
|
+
async def on_text(frame):
|
|
41
|
+
content = frame.body.get("text", {}).get("content", "")
|
|
42
|
+
stream_id = generate_req_id("stream")
|
|
43
|
+
|
|
44
|
+
# 发送中间内容
|
|
45
|
+
await client.reply_stream(frame, stream_id, "正在思考...", finish=False)
|
|
46
|
+
|
|
47
|
+
# 发送最终结果
|
|
48
|
+
await client.reply_stream(frame, stream_id, f'你说:"{content}"', finish=True)
|
|
49
|
+
|
|
50
|
+
client.on("message.text", on_text)
|
|
51
|
+
|
|
52
|
+
# 3. 监听进入会话事件(发送欢迎语)
|
|
53
|
+
async def on_enter(frame):
|
|
54
|
+
await client.reply_welcome(frame, {
|
|
55
|
+
"msgtype": "text",
|
|
56
|
+
"text": {"content": "你好!有什么可以帮助你的吗?"},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
client.on("event.enter_chat", on_enter)
|
|
60
|
+
|
|
61
|
+
# 4. 建立连接
|
|
62
|
+
await client.connect_async()
|
|
63
|
+
|
|
64
|
+
# 保持运行
|
|
65
|
+
while client.is_connected:
|
|
66
|
+
await asyncio.sleep(1)
|
|
67
|
+
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API 参考
|
|
72
|
+
|
|
73
|
+
### WSClient
|
|
74
|
+
|
|
75
|
+
核心客户端类,提供连接管理、消息收发。
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
client = WSClient({
|
|
79
|
+
"bot_id": "your-bot-id",
|
|
80
|
+
"secret": "your-bot-secret",
|
|
81
|
+
# 可选配置:
|
|
82
|
+
"reconnect_interval": 1000, # 重连基础延迟(毫秒)
|
|
83
|
+
"max_reconnect_attempts": 10, # 最大重连次数(-1 为无限)
|
|
84
|
+
"heartbeat_interval": 30000, # 心跳间隔(毫秒)
|
|
85
|
+
"request_timeout": 10000, # HTTP 请求超时(毫秒)
|
|
86
|
+
"ws_url": "wss://...", # 自定义 WebSocket URL
|
|
87
|
+
"logger": custom_logger, # 自定义日志实例
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### 方法
|
|
92
|
+
|
|
93
|
+
| 方法 | 描述 | 返回值 |
|
|
94
|
+
|------|------|--------|
|
|
95
|
+
| `connect_async()` | 建立 WebSocket 连接 | `None` |
|
|
96
|
+
| `disconnect()` | 断开连接 | `None` |
|
|
97
|
+
| `reply(frame, body, cmd?)` | 发送回复消息(通用) | `None` |
|
|
98
|
+
| `reply_stream(frame, stream_id, content, finish?, msg_item?, feedback?)` | 发送流式回复 | `None` |
|
|
99
|
+
| `reply_welcome(frame, body)` | 发送欢迎回复(事件后 5 秒内) | `None` |
|
|
100
|
+
| `reply_template_card(frame, template_card, feedback?)` | 回复模板卡片 | `None` |
|
|
101
|
+
| `reply_stream_with_card(frame, stream_id, content, finish?, options?)` | 发送流式 + 卡片组合 | `None` |
|
|
102
|
+
| `update_template_card(frame, template_card, userids?)` | 更新模板卡片(5 秒内) | `None` |
|
|
103
|
+
| `send_message(chatid, body)` | 主动发送消息 | `None` |
|
|
104
|
+
| `download_file(url, aes_key)` | 下载并解密文件 | `tuple[bytes, str?]` |
|
|
105
|
+
|
|
106
|
+
#### 事件
|
|
107
|
+
|
|
108
|
+
| 事件 | 回调 | 描述 |
|
|
109
|
+
|------|------|------|
|
|
110
|
+
| `connected` | `()` | WebSocket 已连接 |
|
|
111
|
+
| `authenticated` | `()` | 认证成功 |
|
|
112
|
+
| `disconnected` | `(reason)` | 连接断开 |
|
|
113
|
+
| `reconnecting` | `(attempt)` | 正在重连(第 N 次) |
|
|
114
|
+
| `error` | `(frame)` | 发生错误 |
|
|
115
|
+
| `message` | `(frame)` | 收到任意消息 |
|
|
116
|
+
| `message.text` | `(frame)` | 文本消息 |
|
|
117
|
+
| `message.image` | `(frame)` | 图片消息 |
|
|
118
|
+
| `message.mixed` | `(frame)` | 混合内容消息 |
|
|
119
|
+
| `message.voice` | `(frame)` | 语音消息 |
|
|
120
|
+
| `message.file` | `(frame)` | 文件消息 |
|
|
121
|
+
| `event` | `(frame)` | 任意事件 |
|
|
122
|
+
| `event.enter_chat` | `(frame)` | 用户进入会话 |
|
|
123
|
+
| `event.template_card_event` | `(frame)` | 卡片按钮点击 |
|
|
124
|
+
| `event.feedback_event` | `(frame)` | 用户反馈 |
|
|
125
|
+
|
|
126
|
+
## 项目结构
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
wecom_aibot_sdk/
|
|
130
|
+
├── __init__.py # 包入口,导出
|
|
131
|
+
├── client.py # WSClient 核心客户端
|
|
132
|
+
├── ws.py # WebSocket 连接管理器
|
|
133
|
+
├── message_handler.py # 消息解析和事件分发
|
|
134
|
+
├── api.py # HTTP API 客户端(文件下载)
|
|
135
|
+
├── crypto.py # AES-256-CBC 文件解密
|
|
136
|
+
├── logger.py # 默认日志实现
|
|
137
|
+
├── utils.py # 工具函数(generate_req_id 等)
|
|
138
|
+
└── types/ # 类型定义
|
|
139
|
+
├── __init__.py
|
|
140
|
+
├── config.py # 配置类型
|
|
141
|
+
├── event.py # 事件类型
|
|
142
|
+
├── message.py # 消息类型
|
|
143
|
+
├── api.py # API/WebSocket 帧类型
|
|
144
|
+
└── common.py # 通用类型(Logger)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 开发
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# 安装开发依赖
|
|
151
|
+
pip install -e ".[dev]"
|
|
152
|
+
|
|
153
|
+
# 运行测试
|
|
154
|
+
pytest
|
|
155
|
+
|
|
156
|
+
# 格式化代码
|
|
157
|
+
ruff format .
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 许可证
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic usage example for WeCom AI Bot SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import signal
|
|
7
|
+
|
|
8
|
+
from wecom_aibot_sdk import WSClient, generate_req_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
# 1. Create client instance
|
|
13
|
+
client = WSClient({
|
|
14
|
+
"bot_id": "your-bot-id", # Get from WeCom admin console
|
|
15
|
+
"secret": "your-bot-secret", # Get from WeCom admin console
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
# 2. Register event handlers
|
|
19
|
+
|
|
20
|
+
# Handle authentication success
|
|
21
|
+
async def on_authenticated(frame):
|
|
22
|
+
print("Authenticated successfully")
|
|
23
|
+
|
|
24
|
+
client.on("authenticated", on_authenticated)
|
|
25
|
+
|
|
26
|
+
# Handle text messages with streaming reply
|
|
27
|
+
async def on_text_message(frame):
|
|
28
|
+
content = frame.body.get("text", {}).get("content", "")
|
|
29
|
+
print(f"Received text: {content}")
|
|
30
|
+
|
|
31
|
+
stream_id = generate_req_id("stream")
|
|
32
|
+
|
|
33
|
+
# Send streaming intermediate content
|
|
34
|
+
await client.reply_stream(frame, stream_id, "Thinking...", finish=False)
|
|
35
|
+
|
|
36
|
+
# Send final result
|
|
37
|
+
await asyncio.sleep(1)
|
|
38
|
+
await client.reply_stream(
|
|
39
|
+
frame,
|
|
40
|
+
stream_id,
|
|
41
|
+
f'Hello! You said: "{content}"',
|
|
42
|
+
finish=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
client.on("message.text", on_text_message)
|
|
46
|
+
|
|
47
|
+
# Handle enter chat event (send welcome message)
|
|
48
|
+
async def on_enter_chat(frame):
|
|
49
|
+
await client.reply_welcome(frame, {
|
|
50
|
+
"msgtype": "text",
|
|
51
|
+
"text": {"content": "Hello! I'm your AI assistant. How can I help you?"},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
client.on("event.enter_chat", on_enter_chat)
|
|
55
|
+
|
|
56
|
+
# Handle image messages
|
|
57
|
+
async def on_image_message(frame):
|
|
58
|
+
body = frame.body
|
|
59
|
+
image = body.get("image", {})
|
|
60
|
+
url = image.get("url", "")
|
|
61
|
+
aeskey = image.get("aeskey", "")
|
|
62
|
+
|
|
63
|
+
if url and aeskey:
|
|
64
|
+
buffer, filename = await client.download_file(url, aeskey)
|
|
65
|
+
print(f"Downloaded image: {filename}, size: {len(buffer)} bytes")
|
|
66
|
+
|
|
67
|
+
client.on("message.image", on_image_message)
|
|
68
|
+
|
|
69
|
+
# Handle errors
|
|
70
|
+
async def on_error(frame):
|
|
71
|
+
print(f"Error: {frame.body}")
|
|
72
|
+
|
|
73
|
+
client.on("error", on_error)
|
|
74
|
+
|
|
75
|
+
# 3. Connect
|
|
76
|
+
print("Connecting...")
|
|
77
|
+
await client.connect_async()
|
|
78
|
+
|
|
79
|
+
# 4. Graceful shutdown
|
|
80
|
+
loop = asyncio.get_event_loop()
|
|
81
|
+
|
|
82
|
+
def signal_handler():
|
|
83
|
+
print("\nShutting down...")
|
|
84
|
+
asyncio.create_task(client.disconnect())
|
|
85
|
+
|
|
86
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
87
|
+
try:
|
|
88
|
+
loop.add_signal_handler(sig, signal_handler)
|
|
89
|
+
except NotImplementedError:
|
|
90
|
+
# Windows doesn't support add_signal_handler
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
# Keep running
|
|
94
|
+
print("Client running. Press Ctrl+C to exit.")
|
|
95
|
+
try:
|
|
96
|
+
while client.is_connected:
|
|
97
|
+
await asyncio.sleep(1)
|
|
98
|
+
except asyncio.CancelledError:
|
|
99
|
+
pass
|
|
100
|
+
finally:
|
|
101
|
+
await client.disconnect()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
asyncio.run(main())
|