openilink-sdk-python 0.1.0__tar.gz → 0.1.1__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.
- openilink_sdk_python-0.1.1/PKG-INFO +376 -0
- openilink_sdk_python-0.1.1/README.md +365 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/__init__.py +5 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/client.py +9 -10
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/helpers.py +14 -2
- openilink_sdk_python-0.1.1/openilink/http.py +78 -0
- openilink_sdk_python-0.1.1/openilink_sdk_python.egg-info/PKG-INFO +376 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink_sdk_python.egg-info/SOURCES.txt +1 -0
- openilink_sdk_python-0.1.1/openilink_sdk_python.egg-info/requires.txt +3 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/pyproject.toml +6 -6
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/setup.cfg +4 -4
- openilink_sdk_python-0.1.0/PKG-INFO +0 -60
- openilink_sdk_python-0.1.0/README.md +0 -49
- openilink_sdk_python-0.1.0/openilink_sdk_python.egg-info/PKG-INFO +0 -60
- openilink_sdk_python-0.1.0/openilink_sdk_python.egg-info/requires.txt +0 -2
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/auth.py +0 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/errors.py +0 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/monitor.py +0 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink/types.py +0 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink_sdk_python.egg-info/dependency_links.txt +0 -0
- {openilink_sdk_python-0.1.0 → openilink_sdk_python-0.1.1}/openilink_sdk_python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openilink-sdk-python
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python client for the Weixin iLink Bot API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/openilink/openilink-sdk-python
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: qrcode
|
|
10
|
+
Requires-Dist: qrcode>=7.0; extra == "qrcode"
|
|
11
|
+
|
|
12
|
+
# openilink-sdk-python
|
|
13
|
+
|
|
14
|
+
微信 iLink Bot API 的 Python SDK,覆盖完整生命周期:扫码登录、消息监听、文本发送、输入状态指示、主动推送。
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 从源码安装
|
|
20
|
+
git clone https://github.com/openilink/openilink-sdk-python.git
|
|
21
|
+
cd openilink-sdk-python
|
|
22
|
+
pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
### 最小 Echo Bot
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from openilink import Client, LoginCallbacks, MonitorOptions, extract_text, print_qrcode
|
|
31
|
+
|
|
32
|
+
client = Client()
|
|
33
|
+
|
|
34
|
+
# 扫码登录
|
|
35
|
+
result = client.login_with_qr(
|
|
36
|
+
callbacks=LoginCallbacks(on_qrcode=print_qrcode)
|
|
37
|
+
)
|
|
38
|
+
print(f"登录成功! BotID={result.bot_id}")
|
|
39
|
+
|
|
40
|
+
# 收到什么就回复什么
|
|
41
|
+
client.monitor(
|
|
42
|
+
lambda msg: client.push(msg.from_user_id, "echo: " + extract_text(msg)),
|
|
43
|
+
opts=MonitorOptions(on_error=lambda e: print(f"错误: {e}")),
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
运行后终端会显示二维码,用微信扫描即可登录。
|
|
48
|
+
|
|
49
|
+
## 核心概念
|
|
50
|
+
|
|
51
|
+
### 1. 创建客户端
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from openilink import Client
|
|
55
|
+
|
|
56
|
+
# 首次使用,需要扫码登录
|
|
57
|
+
client = Client()
|
|
58
|
+
|
|
59
|
+
# 已有 token,跳过扫码直接使用
|
|
60
|
+
client = Client(token="你保存的bot_token")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
支持的参数:
|
|
64
|
+
|
|
65
|
+
| 参数 | 默认值 | 说明 |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `token` | `""` | Bot Token,为空则需要扫码登录 |
|
|
68
|
+
| `base_url` | `https://ilinkai.weixin.qq.com` | API 地址 |
|
|
69
|
+
| `cdn_base_url` | `https://novac2c.cdn.weixin.qq.com/c2c` | CDN 地址 |
|
|
70
|
+
| `bot_type` | `"3"` | Bot 类型 |
|
|
71
|
+
| `version` | `"1.0.0"` | 客户端版本号 |
|
|
72
|
+
| `session` | `None` | 自定义 `requests.Session` |
|
|
73
|
+
|
|
74
|
+
### 2. 扫码登录
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from openilink import Client, LoginCallbacks, print_qrcode
|
|
78
|
+
|
|
79
|
+
client = Client()
|
|
80
|
+
|
|
81
|
+
result = client.login_with_qr(
|
|
82
|
+
callbacks=LoginCallbacks(
|
|
83
|
+
on_qrcode=print_qrcode, # 二维码就绪
|
|
84
|
+
on_scanned=lambda: print("已扫码,请在手机上确认..."), # 用户已扫码
|
|
85
|
+
on_expired=lambda n, mx: print(f"二维码已过期,正在刷新 ({n}/{mx})..."),
|
|
86
|
+
),
|
|
87
|
+
timeout=480, # 登录超时时间(秒),默认 8 分钟
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if result.connected:
|
|
91
|
+
print(f"登录成功! BotID={result.bot_id} UserID={result.user_id}")
|
|
92
|
+
# 保存 token,下次启动可以跳过扫码
|
|
93
|
+
save_token(result.bot_token)
|
|
94
|
+
else:
|
|
95
|
+
print(f"登录失败: {result.message}")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`LoginResult` 字段:
|
|
99
|
+
|
|
100
|
+
| 字段 | 类型 | 说明 |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `connected` | `bool` | 是否登录成功 |
|
|
103
|
+
| `bot_token` | `str` | Bot Token,保存后下次可直接使用 |
|
|
104
|
+
| `bot_id` | `str` | Bot ID |
|
|
105
|
+
| `base_url` | `str` | 服务端返回的 API 地址 |
|
|
106
|
+
| `user_id` | `str` | 用户 ID |
|
|
107
|
+
| `message` | `str` | 状态消息 |
|
|
108
|
+
|
|
109
|
+
### 3. 监听消息
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from openilink import MonitorOptions
|
|
113
|
+
|
|
114
|
+
def handler(msg):
|
|
115
|
+
text = extract_text(msg)
|
|
116
|
+
if not text:
|
|
117
|
+
return
|
|
118
|
+
print(f"收到来自 {msg.from_user_id} 的消息: {text}")
|
|
119
|
+
|
|
120
|
+
# 回复消息
|
|
121
|
+
client.push(msg.from_user_id, f"你说了: {text}")
|
|
122
|
+
|
|
123
|
+
client.monitor(
|
|
124
|
+
handler,
|
|
125
|
+
opts=MonitorOptions(
|
|
126
|
+
initial_buf="", # 断点续传游标,空则从头开始
|
|
127
|
+
on_buf_update=lambda buf: save_to_file(buf), # 游标更新回调,持久化后可断点续传
|
|
128
|
+
on_error=lambda e: print(f"错误: {e}"), # 非致命错误回调
|
|
129
|
+
on_session_expired=lambda: print("会话已过期!"), # 会话过期回调
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`monitor()` 会阻塞当前线程,自动处理重试和退避:
|
|
135
|
+
- 连续失败 3 次后退避 30 秒
|
|
136
|
+
- 会话过期(errcode -14)后等待 5 分钟
|
|
137
|
+
- 自动缓存每条消息的 `context_token`,供 `push()` 使用
|
|
138
|
+
|
|
139
|
+
### 4. 发送消息
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# 方式一:push(推荐) —— 使用自动缓存的 context_token
|
|
143
|
+
client.push(user_id, "你好!")
|
|
144
|
+
|
|
145
|
+
# 方式二:send_text —— 手动传入 context_token
|
|
146
|
+
client.send_text(user_id, "你好!", context_token)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> `push()` 要求目标用户之前发过消息(SDK 会自动缓存其 context_token)。
|
|
150
|
+
> 如果用户从未发过消息,会抛出 `NoContextTokenError`。
|
|
151
|
+
|
|
152
|
+
### 5. 输入状态指示
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# 获取 typing_ticket
|
|
156
|
+
config = client.get_config(user_id, context_token)
|
|
157
|
+
|
|
158
|
+
# 显示"正在输入..."
|
|
159
|
+
client.send_typing(user_id, config.typing_ticket)
|
|
160
|
+
|
|
161
|
+
# 取消输入状态
|
|
162
|
+
from openilink import TypingStatus
|
|
163
|
+
client.send_typing(user_id, config.typing_ticket, TypingStatus.CANCEL)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 6. 获取 CDN 上传地址
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
resp = client.get_upload_url({
|
|
170
|
+
"filekey": "my-file-key",
|
|
171
|
+
"media_type": 1, # 1=图片 2=视频 3=文件 4=语音
|
|
172
|
+
"to_user_id": user_id,
|
|
173
|
+
"rawsize": file_size,
|
|
174
|
+
"rawfilemd5": file_md5,
|
|
175
|
+
"filesize": file_size,
|
|
176
|
+
})
|
|
177
|
+
print(resp.upload_param)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 7. 优雅停止
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import signal
|
|
184
|
+
|
|
185
|
+
def on_signal(sig, frame):
|
|
186
|
+
client.stop() # 通知 monitor 停止
|
|
187
|
+
|
|
188
|
+
signal.signal(signal.SIGINT, on_signal)
|
|
189
|
+
signal.signal(signal.SIGTERM, on_signal)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 消息结构
|
|
193
|
+
|
|
194
|
+
收到的每条消息是 `WeixinMessage` 对象:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
msg.from_user_id # 发送者 ID
|
|
198
|
+
msg.to_user_id # 接收者 ID
|
|
199
|
+
msg.message_id # 消息 ID
|
|
200
|
+
msg.message_type # MessageType.USER(1) 或 MessageType.BOT(2)
|
|
201
|
+
msg.message_state # MessageState.NEW(0) / GENERATING(1) / FINISH(2)
|
|
202
|
+
msg.context_token # 上下文 token,回复时需要
|
|
203
|
+
msg.session_id # 会话 ID
|
|
204
|
+
msg.group_id # 群 ID(私聊为空)
|
|
205
|
+
msg.item_list # 消息内容列表 [MessageItem, ...]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
每个 `MessageItem` 包含一种内容类型:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
item.type # MessageItemType: TEXT(1) IMAGE(2) VOICE(3) FILE(4) VIDEO(5)
|
|
212
|
+
item.text_item # TextItem: .text
|
|
213
|
+
item.image_item # ImageItem: .url, .media, .thumb_media, ...
|
|
214
|
+
item.voice_item # VoiceItem: .media, .playtime, .text(语音转文字), ...
|
|
215
|
+
item.file_item # FileItem: .file_name, .media, .md5, ...
|
|
216
|
+
item.video_item # VideoItem: .media, .play_length, .thumb_media, ...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
使用 `extract_text(msg)` 可以快速提取第一条文本内容。
|
|
220
|
+
|
|
221
|
+
## 完整示例:命令机器人
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import signal
|
|
225
|
+
import sys
|
|
226
|
+
from pathlib import Path
|
|
227
|
+
from openilink import (
|
|
228
|
+
Client, LoginCallbacks, MonitorOptions,
|
|
229
|
+
extract_text, print_qrcode,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
BUF_FILE = Path("sync_buf.dat")
|
|
233
|
+
TOKEN_FILE = Path("bot_token.dat")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def load_file(path: Path) -> str:
|
|
237
|
+
try:
|
|
238
|
+
return path.read_text().strip()
|
|
239
|
+
except FileNotFoundError:
|
|
240
|
+
return ""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main():
|
|
244
|
+
token = load_file(TOKEN_FILE)
|
|
245
|
+
client = Client(token=token)
|
|
246
|
+
|
|
247
|
+
# 没有 token 则扫码登录
|
|
248
|
+
if not token:
|
|
249
|
+
result = client.login_with_qr(
|
|
250
|
+
callbacks=LoginCallbacks(
|
|
251
|
+
on_qrcode=print_qrcode,
|
|
252
|
+
on_scanned=lambda: print("已扫码,请确认..."),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
if not result.connected:
|
|
256
|
+
print(f"登录失败: {result.message}", file=sys.stderr)
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
TOKEN_FILE.write_text(result.bot_token)
|
|
259
|
+
print(f"登录成功! BotID={result.bot_id}")
|
|
260
|
+
|
|
261
|
+
# Ctrl+C 优雅退出
|
|
262
|
+
signal.signal(signal.SIGINT, lambda *_: client.stop())
|
|
263
|
+
|
|
264
|
+
def handler(msg):
|
|
265
|
+
text = extract_text(msg)
|
|
266
|
+
if not text:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
user = msg.from_user_id
|
|
270
|
+
print(f"[{user}] {text}")
|
|
271
|
+
|
|
272
|
+
if text == "/help":
|
|
273
|
+
client.push(user, "支持的命令:\n/help - 帮助\n/ping - 测试\n/time - 当前时间")
|
|
274
|
+
elif text == "/ping":
|
|
275
|
+
client.push(user, "pong!")
|
|
276
|
+
elif text == "/time":
|
|
277
|
+
from datetime import datetime
|
|
278
|
+
client.push(user, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
279
|
+
else:
|
|
280
|
+
client.push(user, f"你说了: {text}")
|
|
281
|
+
|
|
282
|
+
print("开始监听消息... (Ctrl+C 退出)")
|
|
283
|
+
client.monitor(handler, opts=MonitorOptions(
|
|
284
|
+
initial_buf=load_file(BUF_FILE),
|
|
285
|
+
on_buf_update=lambda buf: BUF_FILE.write_text(buf),
|
|
286
|
+
on_error=lambda e: print(f"错误: {e}", file=sys.stderr),
|
|
287
|
+
on_session_expired=lambda: print("会话过期,需要重新登录", file=sys.stderr),
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == "__main__":
|
|
292
|
+
main()
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## 错误处理
|
|
296
|
+
|
|
297
|
+
| 异常 | 场景 |
|
|
298
|
+
|---|---|
|
|
299
|
+
| `APIError` | API 返回错误(`ret != 0`),可通过 `.is_session_expired()` 判断会话是否过期 |
|
|
300
|
+
| `HTTPError` | HTTP 状态码 >= 400 |
|
|
301
|
+
| `NoContextTokenError` | 调用 `push()` 时目标用户没有缓存的 context token |
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from openilink import NoContextTokenError, APIError
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
client.push(user_id, "你好")
|
|
308
|
+
except NoContextTokenError:
|
|
309
|
+
print("该用户还没有发过消息,无法主动推送")
|
|
310
|
+
except APIError as e:
|
|
311
|
+
if e.is_session_expired():
|
|
312
|
+
print("会话过期,请重新登录")
|
|
313
|
+
else:
|
|
314
|
+
print(f"API 错误: {e}")
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## 断点续传
|
|
318
|
+
|
|
319
|
+
`monitor()` 通过 `get_updates_buf` 游标实现增量拉取。持久化这个游标,重启后可以从断点继续:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
client.monitor(handler, opts=MonitorOptions(
|
|
323
|
+
initial_buf=Path("sync_buf.dat").read_text(), # 启动时读取
|
|
324
|
+
on_buf_update=lambda buf: Path("sync_buf.dat").write_text(buf), # 实时保存
|
|
325
|
+
))
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## API 参考
|
|
329
|
+
|
|
330
|
+
| 方法 | 说明 |
|
|
331
|
+
|---|---|
|
|
332
|
+
| `Client(token, base_url, ...)` | 创建客户端 |
|
|
333
|
+
| `client.login_with_qr(callbacks, timeout)` | 扫码登录 |
|
|
334
|
+
| `client.fetch_qr_code()` | 单独获取二维码 |
|
|
335
|
+
| `client.poll_qr_status(qrcode)` | 轮询扫码状态 |
|
|
336
|
+
| `client.monitor(handler, opts)` | 长轮询消息监听(阻塞) |
|
|
337
|
+
| `client.get_updates(buf)` | 单次拉取更新 |
|
|
338
|
+
| `client.send_message(msg)` | 发送原始消息 |
|
|
339
|
+
| `client.send_text(to, text, context_token)` | 发送文本消息 |
|
|
340
|
+
| `client.push(to, text)` | 使用缓存 token 主动推送 |
|
|
341
|
+
| `client.get_config(user_id, context_token)` | 获取 bot 配置 |
|
|
342
|
+
| `client.send_typing(user_id, ticket, status)` | 发送输入状态 |
|
|
343
|
+
| `client.get_upload_url(req)` | 获取 CDN 上传地址 |
|
|
344
|
+
| `client.set_context_token(user_id, token)` | 手动缓存 context token |
|
|
345
|
+
| `client.get_context_token(user_id)` | 获取缓存的 context token |
|
|
346
|
+
| `client.stop()` | 停止监听循环 |
|
|
347
|
+
| `extract_text(msg)` | 提取消息中第一条文本 |
|
|
348
|
+
| `print_qrcode(url)` | 在终端打印二维码 |
|
|
349
|
+
|
|
350
|
+
## 项目结构
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
openilink-sdk-python/
|
|
354
|
+
├── openilink/
|
|
355
|
+
│ ├── __init__.py # 包入口,统一导出
|
|
356
|
+
│ ├── client.py # 核心客户端,HTTP 请求封装和 API 方法
|
|
357
|
+
│ ├── types.py # 数据类型定义(dataclass + enum)
|
|
358
|
+
│ ├── auth.py # 扫码登录流程
|
|
359
|
+
│ ├── monitor.py # 长轮询消息监听
|
|
360
|
+
│ ├── errors.py # 异常类型
|
|
361
|
+
│ └── helpers.py # 工具函数
|
|
362
|
+
├── examples/
|
|
363
|
+
│ └── echo_bot.py # Echo 机器人示例
|
|
364
|
+
├── pyproject.toml
|
|
365
|
+
└── README.md
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## 依赖
|
|
369
|
+
|
|
370
|
+
- Python >= 3.10
|
|
371
|
+
- [requests](https://pypi.org/project/requests/) >= 2.28
|
|
372
|
+
- [qrcode](https://pypi.org/project/qrcode/) >= 7.0
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
MIT
|