Skip to content

Commit 41c2e11

Browse files
authored
Merge pull request #23 from autoclaw-cc/perf/qrcode-login-optimization
性能优化 - 优化二维码获取流程:当页面已经在 explore 页面时跳过重复导航,二维码获取速度提升约 5–15 秒。 - 合并登录界面等待逻辑:将 `_wait_for_auth_ui` 与 `wait_for_element` 合并为一次等待,减少不必要的等待时间。 - `wait_for_login` 登录检测轮询间隔从 0.5 秒缩短到 0.3 秒,登录状态识别更快。 二维码登录体验改进 - `check-login` 在未登录时会自动返回二维码信息(`qrcode_data_url` 与 `qrcode_path`),无需额外请求。 - `get-qrcode` 新增 `qrcode_data_url` 字段,AI 或前端可以直接以内嵌 Markdown 的方式展示二维码。 - 新增 `_open_file_if_display`:在有桌面环境时自动打开二维码图片,方便直接扫码。 - QR 码生成优化:使用 goqr.me API 替代 base64 方案,并消除重复导航。 登录流程简化 - `SKILL.md` 中的方式 A 登录流程从 3 步简化为 2 步: - `check-login` - `wait-login` 自动降级机制 当手机验证码登录触发频率限制时: - `send-code` 会自动切换到二维码登录(`_qrcode_fallback`)。 - `check-login` 也会在未登录时直接返回二维码,避免额外操作。 标题长度检测强化 - 修复标题长度超过小红书限制时,发布失败但仍显示发布成功的问题。 问题修复 - 修复 `cmd_phone_login` 在验证码发送频率限制时未正确捕获异常的问题。 - 新增 `_qrcode_fallback`,在此类情况下自动降级为二维码登录。
2 parents f89499e + e62928c commit 41c2e11

File tree

4 files changed

+338
-139
lines changed

4 files changed

+338
-139
lines changed

scripts/cli.py

Lines changed: 187 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
84106
def _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

236299
def 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

302387
def 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

380500
def cmd_wait_login(args: argparse.Namespace) -> None:
@@ -402,39 +522,39 @@ def cmd_wait_login(args: argparse.Namespace) -> None:
402522

403523

404524
def 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

439559
def cmd_verify_code(args: argparse.Namespace) -> None:
440560
"""分步登录第二步:在已有页面上填写验证码并提交。"""

0 commit comments

Comments
 (0)