Angiin

feat: 登录时把二维码图片直接发到对话窗口

- 登录时不再只给文件路径,二维码图片会直接显示在对话里,扫一下就能登录
- 服务器无界面环境下同样支持扫码登录,扫对话里的图片即可
- 无界面服务器也可选择手机验证码登录,两种方式都支持
- 有桌面的设备会同时弹出浏览器窗口,浏览器和对话里的二维码都能扫
- 扫码后自动等待登录完成,不再需要反复查询登录状态
- 二维码图片自带白色边框,更容易被手机识别
@@ -111,21 +111,25 @@ def cmd_check_login(args: argparse.Namespace) -> None: @@ -111,21 +111,25 @@ def cmd_check_login(args: argparse.Namespace) -> None:
111 if logged_in: 111 if logged_in:
112 _output({"logged_in": True}, exit_code=0) 112 _output({"logged_in": True}, exit_code=0)
113 else: 113 else:
  114 + import platform
114 from chrome_launcher import has_display 115 from chrome_launcher import has_display
  116 + system = platform.system()
  117 +
115 if has_display(): 118 if has_display():
  119 + # 所有有界面环境(macOS/Windows/Linux 桌面):二维码显示在对话窗口
116 _output({ 120 _output({
117 "logged_in": False, 121 "logged_in": False,
118 "login_method": "qrcode", 122 "login_method": "qrcode",
119 - "hint": "请运行 login,Chrome 窗口会弹出二维码,扫码后自动完成登录", 123 + "hint": "请运行 get-qrcode 获取二维码,扫码后运行 wait-login 等待登录结果",
120 }, exit_code=1) 124 }, exit_code=1)
121 else: 125 else:
122 - # 无界面环境:二维码(扫对话窗口中的图片)和手机验证码均可 126 + # 无界面服务器:二维码或手机验证码均可
123 _output({ 127 _output({
124 "logged_in": False, 128 "logged_in": False,
125 "login_method": "both", 129 "login_method": "both",
126 "hint": ( 130 "hint": (
127 - "方式A: get-qrcode(二维码将显示在对话窗口,扫码即可);"  
128 - "方式B: send-code --phone <手机号>(手机验证码)" 131 + "方式A: get-qrcode + wait-login(二维码显示在对话窗口);"
  132 + "方式B: send-code --phone <手机号> + verify-code(手机验证码)"
129 ), 133 ),
130 }, exit_code=1) 134 }, exit_code=1)
131 finally: 135 finally:
@@ -214,7 +218,8 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None: @@ -214,7 +218,8 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None:
214 218
215 从登录弹窗的二维码 img 元素读取图片(data URL 或网络 URL), 219 从登录弹窗的二维码 img 元素读取图片(data URL 或网络 URL),
216 保存为本地 PNG 文件后立即退出。Chrome tab 保持打开,QR 会话继续有效。 220 保存为本地 PNG 文件后立即退出。Chrome tab 保持打开,QR 会话继续有效。
217 - 调用方收到 qrcode_path 后用 Read 工具将图片显示在对话窗口,再轮询 check-login。 221 + 调用方收到 qrcode_data_url 后直接内嵌到对话窗口显示;同时浏览器窗口(GUI 环境)
  222 + 也会显示二维码,用户可选择扫任意一个。
218 """ 223 """
219 from xhs.login import fetch_qrcode, save_qrcode_to_file 224 from xhs.login import fetch_qrcode, save_qrcode_to_file
220 225
@@ -238,6 +243,27 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None: @@ -238,6 +243,27 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None:
238 }) 243 })
239 244
240 245
  246 +def cmd_wait_login(args: argparse.Namespace) -> None:
  247 + """等待扫码登录完成(配合 get-qrcode 使用)。
  248 +
  249 + 连接已有 Chrome tab,内部轮询直到登录成功或超时,替代 Skill 层的多次 check-login 轮询。
  250 + """
  251 + from xhs.login import wait_for_login
  252 +
  253 + browser, page = _connect_existing(args)
  254 + try:
  255 + success = wait_for_login(page, timeout=args.timeout)
  256 + _output(
  257 + {
  258 + "logged_in": success,
  259 + "message": "登录成功" if success else "等待超时,请重新运行 get-qrcode 获取新二维码",
  260 + },
  261 + exit_code=0 if success else 2,
  262 + )
  263 + finally:
  264 + browser.close()
  265 +
  266 +
241 def cmd_send_code(args: argparse.Namespace) -> None: 267 def cmd_send_code(args: argparse.Namespace) -> None:
242 """分步登录第一步:填写手机号并发送验证码,保持页面不关闭。""" 268 """分步登录第一步:填写手机号并发送验证码,保持页面不关闭。"""
243 from chrome_launcher import has_display, restart_chrome 269 from chrome_launcher import has_display, restart_chrome
@@ -727,6 +753,11 @@ def build_parser() -> argparse.ArgumentParser: @@ -727,6 +753,11 @@ def build_parser() -> argparse.ArgumentParser:
727 sub = subparsers.add_parser("get-qrcode", help="获取登录二维码截图并立即返回(非阻塞)") 753 sub = subparsers.add_parser("get-qrcode", help="获取登录二维码截图并立即返回(非阻塞)")
728 sub.set_defaults(func=cmd_get_qrcode) 754 sub.set_defaults(func=cmd_get_qrcode)
729 755
  756 + # wait-login(配合 get-qrcode,阻塞等待登录完成)
  757 + sub = subparsers.add_parser("wait-login", help="等待扫码登录完成(配合 get-qrcode 使用)")
  758 + sub.add_argument("--timeout", type=float, default=120.0, help="等待超时秒数 (default: 120)")
  759 + sub.set_defaults(func=cmd_wait_login)
  760 +
730 # phone-login(单命令交互式) 761 # phone-login(单命令交互式)
731 sub = subparsers.add_parser("phone-login", help="手机号+验证码登录(交互式,适合本地终端)") 762 sub = subparsers.add_parser("phone-login", help="手机号+验证码登录(交互式,适合本地终端)")
732 sub.add_argument("--phone", required=True, help="手机号(不含国家码,如 13800138000)") 763 sub.add_argument("--phone", required=True, help="手机号(不含国家码,如 13800138000)")
@@ -33,23 +33,12 @@ python scripts/cli.py check-login @@ -33,23 +33,12 @@ python scripts/cli.py check-login
33 33
34 输出解读: 34 输出解读:
35 - `"logged_in": true` → 已登录,可执行后续操作。 35 - `"logged_in": true` → 已登录,可执行后续操作。
36 -- `"logged_in": false` + `"login_method": "qrcode"` → 有界面环境,直接走二维码登录。  
37 -- `"logged_in": false` + `"login_method": "both"` → 无界面服务器,**询问用户选择方式 A(二维码)或方式 B(手机验证码)** 36 +- `"logged_in": false` + `"login_method": "qrcode"` → 有界面环境,走方式 A(二维码)。
  37 +- `"logged_in": false` + `"login_method": "both"` → 无界面服务器,**询问用户选方式 A(二维码)或方式 B(手机验证码)**
38 38
39 -### 第二步:根据 login_method 选择登录方式 39 +### 第二步:根据输出选择登录方式
40 40
41 -#### 方式 A1:二维码登录 — 有界面(GUI)设备  
42 -  
43 -```bash  
44 -python scripts/cli.py login  
45 -```  
46 -  
47 -- Chrome **有窗口**弹出,二维码直接显示在浏览器窗口中。  
48 -- 告知用户用小红书 App 扫屏幕上的二维码。  
49 -- 命令阻塞等待(最多 120 秒),扫码成功后输出 `{"logged_in": true}`  
50 -- 无需在对话窗口显示图片。  
51 -  
52 -#### 方式 A2:二维码登录 — 无界面(headless)服务器 41 +#### 方式 A:二维码登录(所有平台通用)
53 42
54 **第一步** — 获取二维码(非阻塞,立即返回): 43 **第一步** — 获取二维码(非阻塞,立即返回):
55 44
@@ -57,7 +46,7 @@ python scripts/cli.py login @@ -57,7 +46,7 @@ python scripts/cli.py login
57 python scripts/cli.py get-qrcode 46 python scripts/cli.py get-qrcode
58 ``` 47 ```
59 48
60 -- headless Chrome 加载登录页,从 `img` 元素读取二维码图片(相当于右键另存为)。 49 +- Chrome 正常启动,从登录弹窗 `img` 元素读取二维码(相当于右键另存为)。
61 - 命令立即退出,Chrome tab 保持打开(QR 会话继续有效)。 50 - 命令立即退出,Chrome tab 保持打开(QR 会话继续有效)。
62 - 输出:`{"qrcode_path": "...", "qrcode_data_url": "data:image/png;base64,...", "message": "..."}` 51 - 输出:`{"qrcode_path": "...", "qrcode_data_url": "data:image/png;base64,...", "message": "..."}`
63 52
@@ -67,16 +56,16 @@ python scripts/cli.py get-qrcode @@ -67,16 +56,16 @@ python scripts/cli.py get-qrcode
67 ![小红书登录二维码]({qrcode_data_url}) 56 ![小红书登录二维码]({qrcode_data_url})
68 ``` 57 ```
69 58
70 -图片本身已含 16px 白色边框,内嵌在对话窗口,用户用小红书 App 扫对话里的二维码即可 59 +图片含 16px 白色边框,内嵌在对话窗口,用户用小红书 App 扫对话里的二维码
71 60
72 -**第三步**轮询登录状态(每 10 秒一次,最多 12 次): 61 +**第三步**等待登录完成(**单次调用,无需轮询**):
73 62
74 ```bash 63 ```bash
75 -python scripts/cli.py check-login 64 +python scripts/cli.py wait-login
76 ``` 65 ```
77 66
78 -- `"logged_in": true` 则完成。  
79 -- 2 分钟内未登录,提示用户重新执行第一步。 67 +- 连接已有 Chrome tab,内部阻塞等待(最多 120 秒)。
  68 +- 输出 `{"logged_in": true}` 则完成;超时则提示用户重新运行 `get-qrcode`
80 69
81 #### 方式 B:手机验证码登录(无界面服务器,分两步) 70 #### 方式 B:手机验证码登录(无界面服务器,分两步)
82 71