wechat-ilink-sdk 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.
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ .idea/
4
+ __pycache__/
5
+ dist/
6
+ *.py[cod]
7
+ *.pyo
8
+ *.pyd
9
+ *.egg-info/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .DS_Store
16
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pengjingbo
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,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: wechat-ilink-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for WeChat iLink Bot protocol
5
+ Project-URL: Homepage, https://github.com/pengjingbo/wechat-ilink-python-sdk
6
+ Project-URL: Repository, https://github.com/pengjingbo/wechat-ilink-python-sdk
7
+ Project-URL: Issues, https://github.com/pengjingbo/wechat-ilink-python-sdk/issues
8
+ Author: pengjingbo
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 pengjingbo
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: ilink,python,sdk,wechat
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: Implementation :: CPython
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.10
46
+ Requires-Dist: httpx>=0.27
47
+ Requires-Dist: pydantic>=2.0
48
+ Requires-Dist: qrcode>=8.0
49
+ Provides-Extra: dev
50
+ Requires-Dist: build>=1.2.2; extra == 'dev'
51
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
52
+ Requires-Dist: pytest>=8.0; extra == 'dev'
53
+ Requires-Dist: respx>=0.22; extra == 'dev'
54
+ Requires-Dist: twine>=6.1.0; extra == 'dev'
55
+ Provides-Extra: release
56
+ Requires-Dist: build>=1.2.2; extra == 'release'
57
+ Requires-Dist: twine>=6.1.0; extra == 'release'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # wechat-ilink-python-sdk
61
+
62
+ Python SDK for the WeChat iLink Bot protocol.
63
+
64
+ ## Features
65
+
66
+ - QR-code login flow for iLink bot accounts
67
+ - Async HTTP client for iLink APIs
68
+ - Long-polling message loop and dispatch
69
+ - Local credential and state persistence
70
+ - Example echo bot for quick start
71
+
72
+ ## Requirements
73
+
74
+ - Python 3.10+
75
+
76
+ ## Installation
77
+
78
+ Install the published package from PyPI:
79
+
80
+ ```bash
81
+ pip install wechat-ilink-sdk
82
+ ```
83
+
84
+ ## Quick Start
85
+
86
+ Import the public SDK entry points from the `ilink` package:
87
+
88
+ ```python
89
+ from ilink import ILinkClient, login_with_qr
90
+
91
+ print(ILinkClient)
92
+ print(login_with_qr)
93
+ ```
94
+
95
+ Run the bundled example from a cloned repository:
96
+
97
+ ```bash
98
+ python example.py
99
+ ```
100
+
101
+ On first run, the bot will ask for QR-code login and cache credentials locally.
102
+
103
+ ## Main Modules
104
+
105
+ - `ilink.auth`: QR-code login flow
106
+ - `ilink.client`: async API client and polling loop
107
+ - `ilink.store`: local persistence for credentials and cursors
108
+ - `ilink.types`: protocol models and enums
109
+ - `ilink.utils`: helper utilities
110
+
111
+ ## Development
112
+
113
+ Set up a local development environment with test dependencies:
114
+
115
+ ```bash
116
+ uv sync --extra dev
117
+ ```
118
+
119
+ Run the test suite through the installed package:
120
+
121
+ ```bash
122
+ uv run pytest
123
+ ```
124
+
125
+ Build release artifacts locally:
126
+
127
+ ```bash
128
+ uv sync --extra release
129
+ uv run python -m build
130
+ uv run twine check dist/*
131
+ ```
132
+
133
+ The manual PyPI release checklist lives in `docs/releasing.md`.
@@ -0,0 +1,74 @@
1
+ # wechat-ilink-python-sdk
2
+
3
+ Python SDK for the WeChat iLink Bot protocol.
4
+
5
+ ## Features
6
+
7
+ - QR-code login flow for iLink bot accounts
8
+ - Async HTTP client for iLink APIs
9
+ - Long-polling message loop and dispatch
10
+ - Local credential and state persistence
11
+ - Example echo bot for quick start
12
+
13
+ ## Requirements
14
+
15
+ - Python 3.10+
16
+
17
+ ## Installation
18
+
19
+ Install the published package from PyPI:
20
+
21
+ ```bash
22
+ pip install wechat-ilink-sdk
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Import the public SDK entry points from the `ilink` package:
28
+
29
+ ```python
30
+ from ilink import ILinkClient, login_with_qr
31
+
32
+ print(ILinkClient)
33
+ print(login_with_qr)
34
+ ```
35
+
36
+ Run the bundled example from a cloned repository:
37
+
38
+ ```bash
39
+ python example.py
40
+ ```
41
+
42
+ On first run, the bot will ask for QR-code login and cache credentials locally.
43
+
44
+ ## Main Modules
45
+
46
+ - `ilink.auth`: QR-code login flow
47
+ - `ilink.client`: async API client and polling loop
48
+ - `ilink.store`: local persistence for credentials and cursors
49
+ - `ilink.types`: protocol models and enums
50
+ - `ilink.utils`: helper utilities
51
+
52
+ ## Development
53
+
54
+ Set up a local development environment with test dependencies:
55
+
56
+ ```bash
57
+ uv sync --extra dev
58
+ ```
59
+
60
+ Run the test suite through the installed package:
61
+
62
+ ```bash
63
+ uv run pytest
64
+ ```
65
+
66
+ Build release artifacts locally:
67
+
68
+ ```bash
69
+ uv sync --extra release
70
+ uv run python -m build
71
+ uv run twine check dist/*
72
+ ```
73
+
74
+ The manual PyPI release checklist lives in `docs/releasing.md`.
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "wechat-ilink-sdk"
3
+ version = "0.1.0"
4
+ description = "Python SDK for WeChat iLink Bot protocol"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [
9
+ { name = "pengjingbo" },
10
+ ]
11
+ keywords = ["ilink", "python", "sdk", "wechat"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: Implementation :: CPython",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.27",
29
+ "pydantic>=2.0",
30
+ "qrcode>=8.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/pengjingbo/wechat-ilink-python-sdk"
35
+ Repository = "https://github.com/pengjingbo/wechat-ilink-python-sdk"
36
+ Issues = "https://github.com/pengjingbo/wechat-ilink-python-sdk/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "build>=1.2.2",
41
+ "pytest>=8.0",
42
+ "pytest-asyncio>=0.24",
43
+ "respx>=0.22",
44
+ "twine>=6.1.0",
45
+ ]
46
+ release = [
47
+ "build>=1.2.2",
48
+ "twine>=6.1.0",
49
+ ]
50
+
51
+ [build-system]
52
+ requires = ["hatchling"]
53
+ build-backend = "hatchling.build"
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ include = [
57
+ "/LICENSE",
58
+ "/README.md",
59
+ "/pyproject.toml",
60
+ "/src/ilink",
61
+ "/tests",
62
+ ]
63
+
64
+ [tool.hatch.build.targets.wheel]
65
+ packages = ["src/ilink"]
66
+
67
+ [tool.pytest.ini_options]
68
+ asyncio_mode = "auto"
69
+ testpaths = ["tests"]
@@ -0,0 +1,63 @@
1
+ """Public package exports for the iLink SDK."""
2
+
3
+ from .auth import login_with_qr
4
+ from .client import (
5
+ CHANNEL_VERSION,
6
+ DEFAULT_API_TIMEOUT_S,
7
+ DEFAULT_LONG_POLL_TIMEOUT_S,
8
+ ILinkClient,
9
+ ILinkError,
10
+ OnMessageCallback,
11
+ PollError,
12
+ SessionExpiredError,
13
+ )
14
+ from .store import (
15
+ ContextTokenStore,
16
+ Credentials,
17
+ SyncBufStore,
18
+ default_sync_buf_store,
19
+ default_token_store,
20
+ load_credentials,
21
+ save_credentials,
22
+ )
23
+ from .types import LoginResult, QRCodeResponse, QRStatus, StatusResponse
24
+ from .utils import (
25
+ MAX_MSG_LEN,
26
+ build_headers,
27
+ generate_client_id,
28
+ random_wechat_uin,
29
+ )
30
+
31
+ __all__ = [
32
+ # auth
33
+ "login_with_qr",
34
+ # client
35
+ "CHANNEL_VERSION",
36
+ "DEFAULT_API_TIMEOUT_S",
37
+ "DEFAULT_LONG_POLL_TIMEOUT_S",
38
+ "ILinkClient",
39
+ "ILinkError",
40
+ "OnMessageCallback",
41
+ "PollError",
42
+ "SessionExpiredError",
43
+ # store — models & classes
44
+ "ContextTokenStore",
45
+ "Credentials",
46
+ "SyncBufStore",
47
+ # store — public singletons
48
+ "default_sync_buf_store",
49
+ "default_token_store",
50
+ # store — functions
51
+ "load_credentials",
52
+ "save_credentials",
53
+ # types
54
+ "LoginResult",
55
+ "QRCodeResponse",
56
+ "QRStatus",
57
+ "StatusResponse",
58
+ # utils
59
+ "MAX_MSG_LEN",
60
+ "build_headers",
61
+ "generate_client_id",
62
+ "random_wechat_uin",
63
+ ]
@@ -0,0 +1,290 @@
1
+ """QR code login flow for iLink.
2
+
3
+ iLink 二维码登录流程实现模块
4
+
5
+ 完整登录流程:
6
+ 1. GET get_bot_qrcode → 获取二维码字符串 + 图片URL
7
+ 2. Long-poll get_qrcode_status → 轮询状态(等待/已扫描/已确认/已过期)
8
+ 3. 当状态为confirmed → 接收 bot_token + ilink_bot_id + baseurl
9
+
10
+ 模块设计原则:
11
+ - 异步优先:使用 asyncio 和 httpx 实现非阻塞IO
12
+ - 错误处理:完善的超时、重试和错误反馈机制
13
+ - 用户体验:终端友好,提供清晰的扫码指导和状态反馈
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import sys
20
+ import time
21
+
22
+ import httpx
23
+ import qrcode
24
+
25
+ from .types import (
26
+ DEFAULT_BASE_URL,
27
+ LOGIN_TIMEOUT_S,
28
+ MAX_QR_REFRESH,
29
+ QR_POLL_TIMEOUT_S,
30
+ LoginResult,
31
+ QRCodeResponse,
32
+ QRStatus,
33
+ StatusResponse,
34
+ )
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Internal helpers
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ async def _fetch_qrcode(api_base_url: str) -> QRCodeResponse:
42
+ """获取新的机器人登录二维码
43
+
44
+ 向 iLink API 服务器发起 HTTP GET 请求,获取用于微信扫码登录的二维码。
45
+ 二维码包含唯一标识符和图片数据,用于后续状态轮询。
46
+
47
+ Args:
48
+ api_base_url: iLink API 服务器的基础URL地址
49
+
50
+ Returns:
51
+ QRCodeResponse: 包含二维码标识符和图片URL/数据的响应对象
52
+ - qrcode: 二维码唯一标识符,用于状态轮询
53
+ - qrcode_img_content: 二维码图片的URL或base64编码数据
54
+
55
+ Raises:
56
+ httpx.HTTPStatusError: 当服务器返回非2xx状态码时
57
+ - 401: 认证失败
58
+ - 404: API端点不存在
59
+ - 500: 服务器内部错误
60
+ httpx.RequestError: 网络连接错误、DNS解析失败等
61
+ ValueError: 服务器响应格式不符合预期
62
+ """
63
+ # 规范化URL,确保没有多余的斜杠
64
+ base: str = api_base_url.rstrip("/")
65
+ # 构建完整的API端点URL
66
+ url: str = f"{base}/ilink/bot/get_bot_qrcode"
67
+
68
+ # 使用httpx异步客户端发起请求
69
+ try:
70
+ async with httpx.AsyncClient() as client:
71
+ # 发送GET请求,附带必要的查询参数
72
+ # bot_type=3 表示特定的机器人类型配置
73
+ resp: httpx.Response = await client.get(url, params={"bot_type": "3"})
74
+
75
+ # 如果HTTP状态码不是2xx,抛出异常
76
+ resp.raise_for_status()
77
+
78
+ # 将JSON响应解析为Pydantic模型
79
+ # model_validate()会自动进行数据验证和类型转换
80
+ return QRCodeResponse.model_validate(resp.json())
81
+ except Exception as exc:
82
+ raise RuntimeError(f"获取二维码失败: {exc}") from exc
83
+
84
+
85
+ async def _poll_status(api_base_url: str, qrcode_id: str) -> StatusResponse:
86
+ """轮询二维码扫描状态(长轮询)
87
+
88
+ 向服务器查询指定二维码的扫描状态,使用长轮询机制减少频繁请求。
89
+ 当二维码被扫描、用户确认或过期时,服务器会立即返回状态。
90
+ 如果超时未扫描,客户端会收到"wait"状态并继续轮询。
91
+
92
+ Args:
93
+ api_base_url: iLink API 服务器的基础URL地址
94
+ qrcode_id: 二维码唯一标识符,从 _fetch_qrcode() 返回的 qrcode 字段获取
95
+
96
+ Returns:
97
+ StatusResponse: 状态响应对象
98
+ - status: 二维码当前状态 (WAIT/SCANNED/CONFIRMED/EXPIRED)
99
+ - bot_token: 登录成功后的令牌(仅当status=CONFIRMED)
100
+ - ilink_bot_id: 机器人ID
101
+ - baseurl: API基础URL
102
+ - ilink_user_id: 用户ID
103
+
104
+ Raises:
105
+ httpx.HTTPStatusError: 服务器返回非2xx状态码
106
+ httpx.TimeoutException: 客户端设置的超时时间到达(正常情况,返回WAIT状态)
107
+ """
108
+ base: str = api_base_url.rstrip("/")
109
+ url: str = f"{base}/ilink/bot/get_qrcode_status"
110
+
111
+ # 创建异步HTTP客户端,设置合理的超时时间(35s)
112
+ async with httpx.AsyncClient(timeout=QR_POLL_TIMEOUT_S) as client:
113
+ try:
114
+ # 发送GET请求查询二维码状态
115
+ resp: httpx.Response = await client.get(
116
+ url,
117
+ params={"qrcode": qrcode_id}, # 二维码标识符
118
+ headers={"iLink-App-ClientVersion": "1"}, # 客户端版本标识
119
+ )
120
+ resp.raise_for_status() # 检查HTTP状态码
121
+
122
+ # 解析JSON响应为StatusResponse模型
123
+ return StatusResponse.model_validate(resp.json())
124
+
125
+ except httpx.TimeoutException:
126
+ # 长轮询超时是正常情况,表示在此期间用户未扫码
127
+ # 返回WAIT状态让调用方继续轮询
128
+ return StatusResponse(status=QRStatus.WAIT)
129
+
130
+
131
+ def _display_qr(qr_img_url: str) -> None:
132
+ """在终端显示二维码并打印备用URL
133
+
134
+ 将二维码URL转换为ASCII艺术形式在终端显示,方便用户在命令行环境中扫码。
135
+ 如果终端不支持或显示不清,同时提供原始URL作为备用方案。
136
+
137
+ 实现原理:
138
+ 1. 使用qrcode库生成二维码矩阵
139
+ 2. 将矩阵转换为ASCII字符(黑色块用██,白色块用空格)
140
+ 3. 通过invert=True在深色终端上提供更好的可视性
141
+
142
+ Args:
143
+ qr_img_url: 要编码为二维码的URL字符串
144
+ 通常是微信登录页面的URL
145
+
146
+ Raises:
147
+ DataOverflowError: 当URL过长超过二维码容量时
148
+ qrcode.exceptions.DataOverflowError: 数据超出二维码编码容量
149
+ """
150
+ # 创建QRCode对象,设置错误纠正级别为L(7%错误恢复)
151
+ # 错误纠正允许二维码部分损坏仍可读取
152
+ qr: qrcode.QRCode = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L)
153
+
154
+ # 将URL添加到二维码数据中
155
+ qr.add_data(qr_img_url)
156
+
157
+ # 自动确定最小版本和最佳拟合
158
+ qr.make(fit=True)
159
+
160
+ # 在终端打印ASCII格式的二维码
161
+ # invert=True: 在深色背景终端上反色显示,提高可读性
162
+ qr.print_ascii(invert=True)
163
+
164
+ # 打印备用URL,以防二维码无法显示或扫描
165
+ print(f"\n如无法显示,请在浏览器打开: {qr_img_url}\n")
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Public API
170
+ # ---------------------------------------------------------------------------
171
+
172
+
173
+ async def login_with_qr(
174
+ api_base_url: str = DEFAULT_BASE_URL,
175
+ ) -> LoginResult:
176
+ """交互式二维码登录 - 在终端打印二维码并等待用户扫码登录
177
+
178
+ 完整的登录流程主函数:
179
+ 1. 获取初始二维码并显示
180
+ 2. 进入轮询循环,定期检查扫码状态
181
+ 3. 处理各种状态(等待、已扫描、已确认、已过期)
182
+ 4. 返回登录凭证或抛出异常
183
+
184
+ 状态机转换:
185
+ [初始] → 获取二维码 → WAIT(等待扫码)
186
+ WAIT → SCANNED(已扫码,等待确认)
187
+ SCANNED → CONFIRMED(已确认,登录成功)
188
+ WAIT/SCANNED → EXPIRED(二维码过期,需刷新)
189
+
190
+ Args:
191
+ api_base_url: iLink API 服务器的基础URL地址
192
+ 默认使用 DEFAULT_BASE_URL
193
+
194
+ Returns:
195
+ LoginResult: 登录成功返回的结果对象,包含:
196
+ - bot_token: 用于后续API调用的认证令牌
197
+ - account_id: 机器人账户ID
198
+ - base_url: API基础URL(可能与传入的不同)
199
+ - user_id: 用户ID(可选)
200
+
201
+ Raises:
202
+ RuntimeError: 在以下情况抛出:
203
+ 1. 登录总超时(LOGIN_TIMEOUT_S秒内未完成)
204
+ 2. 二维码多次过期(超过MAX_QR_REFRESH次)
205
+ 3. 服务器返回CONFIRMED但缺少必要字段
206
+ httpx.HTTPStatusError: API调用HTTP错误
207
+ asyncio.TimeoutError: 异步操作超时
208
+ """
209
+ # 步骤1: 获取初始二维码
210
+ qr: QRCodeResponse = await _fetch_qrcode(api_base_url)
211
+ refresh_count: int = 1 # 二维码刷新计数器
212
+
213
+ # 打印扫码提示
214
+ print("\n请使用微信扫描以下二维码:\n")
215
+
216
+ # 在终端显示二维码
217
+ _display_qr(qr.qrcode_img_content)
218
+
219
+ # 设置登录截止时间,防止无限等待(8分钟)
220
+ deadline: float = time.monotonic() + LOGIN_TIMEOUT_S
221
+
222
+ # 标记是否已记录"已扫码"状态,避免重复输出
223
+ scanned_logged: bool = False
224
+
225
+ # 步骤2: 进入状态轮询循环
226
+ while time.monotonic() < deadline:
227
+ # 查询当前二维码状态
228
+ status: StatusResponse = await _poll_status(api_base_url, qr.qrcode)
229
+
230
+ # 状态处理:WAIT - 等待扫码
231
+ if status.status == QRStatus.WAIT:
232
+ # 打印进度点,让用户知道程序仍在运行
233
+ sys.stdout.write(".")
234
+ sys.stdout.flush() # 立即刷新输出缓冲区
235
+
236
+ # 状态处理:SCANNED - 二维码已扫描
237
+ elif status.status == QRStatus.SCANNED:
238
+ if not scanned_logged:
239
+ # 首次进入SCANNED状态,提示用户在手机上确认
240
+ print("\n\n已扫码,请在微信上确认...")
241
+ scanned_logged = True
242
+
243
+ # 状态处理:EXPIRED - 二维码已过期
244
+ elif status.status == QRStatus.EXPIRED:
245
+ refresh_count += 1
246
+
247
+ # 检查是否超过最大刷新次数
248
+ if refresh_count > MAX_QR_REFRESH:
249
+ raise RuntimeError("二维码多次过期,登录超时")
250
+
251
+ # 刷新二维码
252
+ print(f"\n二维码已过期,正在刷新... ({refresh_count}/{MAX_QR_REFRESH})")
253
+ qr = await _fetch_qrcode(api_base_url)
254
+ scanned_logged = False
255
+
256
+ # 显示新的二维码
257
+ _display_qr(qr.qrcode_img_content)
258
+
259
+ # 状态处理:CONFIRMED - 登录已确认
260
+ elif status.status == QRStatus.CONFIRMED:
261
+ # 验证服务器返回的必要字段
262
+ if not status.ilink_bot_id or not status.bot_token:
263
+ raise RuntimeError("登录失败:服务器未返回必要信息")
264
+
265
+ # 登录成功
266
+ print("\n\n✅ 微信连接成功!")
267
+
268
+ # 返回登录结果
269
+ return LoginResult(
270
+ bot_token=status.bot_token,
271
+ account_id=status.ilink_bot_id,
272
+ base_url=status.baseurl
273
+ or api_base_url, # 使用服务器返回的baseurl或默认值
274
+ user_id=status.ilink_user_id,
275
+ )
276
+
277
+ # 状态处理:未知状态(防御性编程)
278
+ else:
279
+ # 记录警告但继续轮询
280
+ print(f"\n警告:收到未知状态: {status.status}")
281
+
282
+ # 每次轮询后等待1秒,避免过于频繁的请求
283
+ await asyncio.sleep(1)
284
+
285
+ # 循环正常结束(未在deadline前返回)表示超时
286
+ raise RuntimeError("登录超时,请重试")
287
+
288
+
289
+ if __name__ == "__main__":
290
+ login_with_qr()