@@ -81,6 +81,28 @@ def _output(data: dict, exit_code: int = 0) -> None:
8181 sys .exit (exit_code )
8282
8383
84+ def _open_file_if_display (path : str ) -> None :
85+ """有桌面环境时用系统默认程序打开文件,无界面环境静默跳过。"""
86+ from chrome_launcher import has_display
87+
88+ if not has_display ():
89+ return
90+
91+ import platform
92+ import subprocess
93+
94+ try :
95+ system = platform .system ()
96+ if system == "Windows" :
97+ os .startfile (path )
98+ elif system == "Darwin" :
99+ subprocess .Popen (["open" , path ])
100+ else :
101+ subprocess .Popen (["xdg-open" , path ])
102+ except Exception :
103+ logger .debug ("无法自动打开文件: %s" , path )
104+
105+
84106def _update_account_nickname (args : argparse .Namespace , page ) -> None :
85107 """登录成功后,将平台昵称写入账号描述(best-effort,失败不影响登录结果)。"""
86108 if not getattr (args , "account" , "" ):
@@ -229,43 +251,105 @@ def _headless_fallback(port: int) -> None:
229251 exit_code = 1 ,
230252 )
231253
254+ def _qrcode_fallback (browser , page , args : argparse .Namespace ) -> None :
255+ """频率限制时刷新页面返回二维码,让 AI 直接展示给用户扫码。"""
256+ from xhs .login import (
257+ fetch_qrcode ,
258+ make_qrcode_url ,
259+ save_qrcode_to_file ,
260+ )
261+ from xhs .urls import EXPLORE_URL
262+
263+ # 刷新页面使登录弹窗回到默认的二维码 tab
264+ page .navigate (EXPLORE_URL )
265+ page .wait_for_load ()
266+
267+ png_bytes , _b64_orig , already = fetch_qrcode (page )
268+ if already :
269+ browser .close ()
270+ _output ({"logged_in" : True , "message" : "已登录" })
271+ return
272+
273+ qrcode_path = save_qrcode_to_file (png_bytes )
274+ image_url , login_url = make_qrcode_url (png_bytes )
275+
276+ _open_file_if_display (qrcode_path )
277+
278+ _save_login_tab (page .target_id , args .port )
279+ _clear_session_tab (args .port )
280+ browser .close ()
281+ result : dict = {
282+ "logged_in" : False ,
283+ "login_method" : "qrcode" ,
284+ "qrcode_path" : qrcode_path ,
285+ "qrcode_image_url" : image_url ,
286+ "message" : (
287+ "验证码发送受限,已切换为二维码登录,请扫码。"
288+ "扫码后运行 wait-login 等待登录结果。"
289+ ),
290+ }
291+ if login_url :
292+ result ["qr_login_url" ] = login_url
293+ _output (result , exit_code = 1 )
294+
232295
233296# ========== 子命令实现 ==========
234297
235298
236299def cmd_check_login (args : argparse .Namespace ) -> None :
237- """检查登录状态。"""
238- from xhs .login import check_login_status
300+ """检查登录状态。未登录时自动获取二维码,省去单独调 get-qrcode 的一轮通信。
301+
302+ 直接调 fetch_qrcode 一步完成:导航 + 登录检查 + 二维码获取,
303+ 不再经过 check_login_status 避免重复导航和等待。
304+ """
305+ from xhs .login import (
306+ fetch_qrcode ,
307+ make_qrcode_url ,
308+ save_qrcode_to_file ,
309+ )
239310
240311 browser , page = _connect (args )
241312 try :
242- logged_in = check_login_status (page )
243- if logged_in :
313+ png_bytes , _b64_orig , already = fetch_qrcode (page )
314+ if already :
244315 _output ({"logged_in" : True }, exit_code = 0 )
316+ return
317+
318+ qrcode_path = save_qrcode_to_file (png_bytes )
319+ image_url , login_url = make_qrcode_url (png_bytes )
320+
321+ # 记录 login tab + 清除 session tab
322+ _save_login_tab (page .target_id , args .port )
323+ _clear_session_tab (args .port )
324+
325+ _open_file_if_display (qrcode_path )
326+
327+ from chrome_launcher import has_display
328+
329+ result : dict = {
330+ "logged_in" : False ,
331+ "qrcode_path" : qrcode_path ,
332+ "qrcode_image_url" : image_url ,
333+ }
334+ if login_url :
335+ result ["qr_login_url" ] = login_url
336+ if has_display ():
337+ result ["login_method" ] = "qrcode"
338+ result ["hint" ] = (
339+ "未登录,二维码已自动生成。"
340+ "扫码后运行 wait-login 等待登录结果"
341+ )
245342 else :
246- import platform
247- from chrome_launcher import has_display
248- system = platform .system ()
249-
250- if has_display ():
251- # 所有有界面环境(macOS/Windows/Linux 桌面):二维码显示在对话窗口
252- _output ({
253- "logged_in" : False ,
254- "login_method" : "qrcode" ,
255- "hint" : "请运行 get-qrcode 获取二维码,扫码后运行 wait-login 等待登录结果" ,
256- }, exit_code = 1 )
257- else :
258- # 无界面服务器:二维码或手机验证码均可
259- _output ({
260- "logged_in" : False ,
261- "login_method" : "both" ,
262- "hint" : (
263- "方式A: get-qrcode + wait-login(二维码显示在对话窗口);"
264- "方式B: send-code --phone <手机号> + verify-code(手机验证码)"
265- ),
266- }, exit_code = 1 )
343+ result ["login_method" ] = "both"
344+ result ["hint" ] = (
345+ "未登录,二维码已自动生成。"
346+ "方式A: 直接扫码 + wait-login;"
347+ "方式B: send-code --phone <手机号>"
348+ " + verify-code(手机验证码)"
349+ )
350+ _output (result , exit_code = 1 )
267351 finally :
268- # 不关闭 tab,保留页面供下次命令复用(_SESSION_TAB_FILE)
352+ # 只断开 CDP 连接, 不关闭 tab——保留登录页面
269353 browser .close ()
270354
271355
@@ -275,15 +359,16 @@ def cmd_login(args: argparse.Namespace) -> None:
275359
276360 browser , page = _connect (args )
277361 try :
278- png_bytes , already = fetch_qrcode (page )
362+ png_bytes , _b64 , already = fetch_qrcode (page )
279363 if already :
280364 _output ({"logged_in" : True , "message" : "已登录" })
281365 return
282366
283367 qrcode_path = save_qrcode_to_file (png_bytes )
368+ _open_file_if_display (qrcode_path )
284369 print (
285370 json .dumps (
286- {"qrcode_path" : qrcode_path , "message" : "请扫码登录,二维码已保存到文件 " },
371+ {"qrcode_path" : qrcode_path , "message" : "请扫码登录" },
287372 ensure_ascii = False ,
288373 )
289374 )
@@ -301,19 +386,33 @@ def cmd_login(args: argparse.Namespace) -> None:
301386
302387def cmd_phone_login (args : argparse .Namespace ) -> None :
303388 """手机号+验证码登录(适用于无界面服务器)。"""
389+ from xhs .errors import RateLimitError
304390 from xhs .login import send_phone_code , submit_phone_code
305391
306392 browser , page = _connect (args )
307393 try :
308394 sent = send_phone_code (page , args .phone )
395+ except RateLimitError :
396+ # 频率限制——直接切换二维码登录
397+ logger .info ("验证码发送受限,切换为二维码登录" )
398+ _qrcode_fallback (browser , page , args )
399+ return
400+
401+ try :
309402 if not sent :
310403 _output ({"logged_in" : True , "message" : "已登录,无需重新登录" })
311404 return
312405
313406 # 输出提示,等待用户在终端输入验证码
314407 print (
315408 json .dumps (
316- {"status" : "code_sent" , "message" : f"验证码已发送至 { args .phone [:3 ]} ****{ args .phone [- 4 :]} " },
409+ {
410+ "status" : "code_sent" ,
411+ "message" : (
412+ f"验证码已发送至 "
413+ f"{ args .phone [:3 ]} ****{ args .phone [- 4 :]} "
414+ ),
415+ },
317416 ensure_ascii = False ,
318417 ),
319418 flush = True ,
@@ -326,16 +425,25 @@ def cmd_phone_login(args: argparse.Namespace) -> None:
326425 try :
327426 code = input ("请输入验证码: " ).strip ()
328427 except EOFError :
329- _output ({"success" : False , "error" : "未收到验证码输入" }, exit_code = 2 )
428+ _output (
429+ {"success" : False , "error" : "未收到验证码输入" },
430+ exit_code = 2 ,
431+ )
330432 return
331433
332434 if not code :
333- _output ({"success" : False , "error" : "验证码不能为空" }, exit_code = 2 )
435+ _output (
436+ {"success" : False , "error" : "验证码不能为空" },
437+ exit_code = 2 ,
438+ )
334439 return
335440
336441 success = submit_phone_code (page , code )
337442 _output (
338- {"logged_in" : success , "message" : "登录成功" if success else "验证码错误或超时" },
443+ {
444+ "logged_in" : success ,
445+ "message" : "登录成功" if success else "验证码错误或超时" ,
446+ },
339447 exit_code = 0 if success else 2 ,
340448 )
341449 finally :
@@ -351,30 +459,42 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None:
351459 调用方收到 qrcode_data_url 后直接内嵌到对话窗口显示;同时浏览器窗口(GUI 环境)
352460 也会显示二维码,用户可选择扫任意一个。
353461 """
354- from xhs .login import fetch_qrcode , save_qrcode_to_file
462+ from xhs .login import (
463+ fetch_qrcode ,
464+ make_qrcode_url ,
465+ save_qrcode_to_file ,
466+ )
355467
356468 browser , page = _connect (args )
357469
358- png_bytes , already = fetch_qrcode (page )
470+ png_bytes , _b64_orig , already = fetch_qrcode (page )
359471 if already :
360472 browser .close_page (page )
361473 browser .close ()
362474 _output ({"logged_in" : True , "message" : "已登录" })
363475 return
364476
365477 qrcode_path = save_qrcode_to_file (png_bytes )
478+ image_url , login_url = make_qrcode_url (png_bytes )
479+
480+ _open_file_if_display (qrcode_path )
366481
367482 # 记录 login tab,供 wait-login 精确 reconnect
368483 _save_login_tab (page .target_id , args .port )
369- # 清除 session tab 引用——隔离登录表单,防止其他命令复用并关闭/导航该 tab
484+ # 清除 session tab 引用——隔离登录表单,防止其他命令复用
370485 _clear_session_tab (args .port )
371486
372- # 只断开 CDP 连接,不关闭 tab——QR 会话保持,用户可继续扫码
487+ # 只断开 CDP 连接,不关闭 tab——QR 会话保持
373488 browser .close ()
374- _output ( {
489+ result : dict = {
375490 "qrcode_path" : qrcode_path ,
376- "message" : "二维码已生成,请扫码登录。扫码后运行 check-login 确认登录状态。" ,
377- })
491+ "qrcode_image_url" : image_url ,
492+ "message" : "二维码已生成,请扫码登录。"
493+ "扫码后运行 wait-login 等待登录结果。" ,
494+ }
495+ if login_url :
496+ result ["qr_login_url" ] = login_url
497+ _output (result )
378498
379499
380500def cmd_wait_login (args : argparse .Namespace ) -> None :
@@ -402,39 +522,39 @@ def cmd_wait_login(args: argparse.Namespace) -> None:
402522
403523
404524def cmd_send_code (args : argparse .Namespace ) -> None :
405- """分步登录第一步:填写手机号并发送验证码,保持页面不关闭。"""
406- from chrome_launcher import has_display , restart_chrome
525+ """分步登录第一步:填写手机号并发送验证码,保持页面不关闭。
526+
527+ 频率限制时返回错误信息和建议,由 AI 告知用户选择。
528+ """
407529 from xhs .errors import RateLimitError
408530 from xhs .login import send_phone_code
409531
410- for attempt in range (2 ):
411- browser , page = _connect (args )
412- try :
413- sent = send_phone_code (page , args .phone )
414- if not sent :
415- _output ({"logged_in" : True , "message" : "已登录,无需重新登录" })
416- return
417-
418- # 记录 login tab,供 verify-code 精确 reconnect
419- _save_login_tab (page .target_id , args .port )
420- # 清除 session tab 引用——隔离登录表单,防止其他命令复用并关闭/导航该 tab
421- _clear_session_tab (args .port )
422- _output ({
423- "status" : "code_sent" ,
424- "message" : f"验证码已发送至 { args .phone [:3 ]} ****{ args .phone [- 4 :]} ,请运行 verify-code --code <验证码>" ,
425- })
426- except RateLimitError :
427- browser .close ()
428- if attempt == 0 :
429- logger .info ("请求频率限制,重启 Chrome 后重试..." )
430- restart_chrome (port = args .port , headless = not has_display ())
431- continue
432- _output ({"success" : False , "error" : "请求太频繁,重启后仍失败,请稍后再试" }, exit_code = 2 )
433- else :
434- # 只断开控制连接,不关闭页面——tab 保持打开,verify-code 继续复用
435- browser .close ()
532+ browser , page = _connect (args )
533+ try :
534+ sent = send_phone_code (page , args .phone )
535+ if not sent :
536+ _output ({"logged_in" : True , "message" : "已登录,无需重新登录" })
436537 return
437538
539+ # 记录 login tab,供 verify-code 精确 reconnect
540+ _save_login_tab (page .target_id , args .port )
541+ # 清除 session tab 引用——隔离登录表单,防止其他命令复用并关闭/导航该 tab
542+ _clear_session_tab (args .port )
543+ _output ({
544+ "status" : "code_sent" ,
545+ "message" : (
546+ f"验证码已发送至 { args .phone [:3 ]} ****{ args .phone [- 4 :]} ,"
547+ "请运行 verify-code --code <验证码>"
548+ ),
549+ })
550+ except RateLimitError :
551+ # 频率限制——直接切换二维码登录
552+ logger .info ("验证码发送受限,切换为二维码登录" )
553+ _qrcode_fallback (browser , page , args )
554+ else :
555+ # 只断开控制连接,不关闭页面——tab 保持打开,verify-code 继续复用
556+ browser .close ()
557+
438558
439559def cmd_verify_code (args : argparse .Namespace ) -> None :
440560 """分步登录第二步:在已有页面上填写验证码并提交。"""
0 commit comments