perf: 二维码登录流程优化,3步减为2步 + 频率限制自动降级
- fetch_qrcode() 跳过重复导航(页面已在 explore 时省 5-15s) - fetch_qrcode() 合并 _wait_for_auth_ui + wait_for_element 为单次等待 - check-login 未登录时自动返回二维码(qrcode_data_url + qrcode_path) - get-qrcode 增加 qrcode_data_url 字段,AI 可直接内嵌 markdown - send-code 频率限制时自动切换二维码登录(_qrcode_fallback) - wait_for_login 轮询间隔 0.5s → 0.3s - 新增 _open_file_if_display:桌面环境自动打开二维码图片 - SKILL.md 方式A 从3步简化为2步(check-login → wait-login)
Showing
3 changed files
with
163 additions
and
74 deletions
| @@ -81,6 +81,28 @@ def _output(data: dict, exit_code: int = 0) -> None: | @@ -81,6 +81,28 @@ def _output(data: dict, exit_code: int = 0) -> None: | ||
| 81 | sys.exit(exit_code) | 81 | sys.exit(exit_code) |
| 82 | 82 | ||
| 83 | 83 | ||
| 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 | + | ||
| 84 | def _update_account_nickname(args: argparse.Namespace, page) -> None: | 106 | def _update_account_nickname(args: argparse.Namespace, page) -> None: |
| 85 | """登录成功后,将平台昵称写入账号描述(best-effort,失败不影响登录结果)。""" | 107 | """登录成功后,将平台昵称写入账号描述(best-effort,失败不影响登录结果)。""" |
| 86 | if not getattr(args, "account", ""): | 108 | if not getattr(args, "account", ""): |
| @@ -229,43 +251,103 @@ def _headless_fallback(port: int) -> None: | @@ -229,43 +251,103 @@ def _headless_fallback(port: int) -> None: | ||
| 229 | exit_code=1, | 251 | exit_code=1, |
| 230 | ) | 252 | ) |
| 231 | 253 | ||
| 254 | +def _qrcode_fallback(browser, page, args: argparse.Namespace) -> None: | ||
| 255 | + """频率限制时刷新页面返回二维码,让 AI 直接展示给用户扫码。""" | ||
| 256 | + from xhs.login import fetch_qrcode, save_qrcode_to_file | ||
| 257 | + from xhs.urls import EXPLORE_URL | ||
| 258 | + | ||
| 259 | + # 刷新页面使登录弹窗回到默认的二维码 tab | ||
| 260 | + page.navigate(EXPLORE_URL) | ||
| 261 | + page.wait_for_load() | ||
| 262 | + | ||
| 263 | + png_bytes, already = fetch_qrcode(page) | ||
| 264 | + if already: | ||
| 265 | + browser.close() | ||
| 266 | + _output({"logged_in": True, "message": "已登录"}) | ||
| 267 | + return | ||
| 268 | + | ||
| 269 | + qrcode_path = save_qrcode_to_file(png_bytes) | ||
| 270 | + | ||
| 271 | + import base64 as _b64 | ||
| 272 | + qrcode_data_url = ( | ||
| 273 | + "data:image/png;base64," | ||
| 274 | + + _b64.b64encode(png_bytes).decode() | ||
| 275 | + ) | ||
| 276 | + | ||
| 277 | + _open_file_if_display(qrcode_path) | ||
| 278 | + | ||
| 279 | + _save_login_tab(page.target_id, args.port) | ||
| 280 | + _clear_session_tab(args.port) | ||
| 281 | + browser.close() | ||
| 282 | + _output({ | ||
| 283 | + "logged_in": False, | ||
| 284 | + "login_method": "qrcode", | ||
| 285 | + "qrcode_path": qrcode_path, | ||
| 286 | + "qrcode_data_url": qrcode_data_url, | ||
| 287 | + "message": ( | ||
| 288 | + "验证码发送受限,已切换为二维码登录,请扫码。" | ||
| 289 | + "扫码后运行 wait-login 等待登录结果。" | ||
| 290 | + ), | ||
| 291 | + }, exit_code=1) | ||
| 292 | + | ||
| 232 | 293 | ||
| 233 | # ========== 子命令实现 ========== | 294 | # ========== 子命令实现 ========== |
| 234 | 295 | ||
| 235 | 296 | ||
| 236 | def cmd_check_login(args: argparse.Namespace) -> None: | 297 | def cmd_check_login(args: argparse.Namespace) -> None: |
| 237 | - """检查登录状态。""" | ||
| 238 | - from xhs.login import check_login_status | 298 | + """检查登录状态。未登录时自动获取二维码,省去单独调 get-qrcode 的一轮通信。""" |
| 299 | + from xhs.login import check_login_status, fetch_qrcode, save_qrcode_to_file | ||
| 239 | 300 | ||
| 240 | browser, page = _connect(args) | 301 | browser, page = _connect(args) |
| 241 | try: | 302 | try: |
| 242 | logged_in = check_login_status(page) | 303 | logged_in = check_login_status(page) |
| 243 | if logged_in: | 304 | if logged_in: |
| 244 | _output({"logged_in": True}, exit_code=0) | 305 | _output({"logged_in": True}, exit_code=0) |
| 306 | + return | ||
| 307 | + | ||
| 308 | + # 未登录——当前页面已在登录弹窗,复用页面直接获取二维码 | ||
| 309 | + png_bytes, already = fetch_qrcode(page) | ||
| 310 | + if already: | ||
| 311 | + _output({"logged_in": True}, exit_code=0) | ||
| 312 | + return | ||
| 313 | + | ||
| 314 | + qrcode_path = save_qrcode_to_file(png_bytes) | ||
| 315 | + | ||
| 316 | + # 生成 data URL,AI 可直接内嵌到 markdown 图片 | ||
| 317 | + import base64 as _b64 | ||
| 318 | + qrcode_data_url = "data:image/png;base64," + _b64.b64encode(png_bytes).decode() | ||
| 319 | + | ||
| 320 | + # 记录 login tab + 清除 session tab(与 cmd_get_qrcode 一致) | ||
| 321 | + _save_login_tab(page.target_id, args.port) | ||
| 322 | + _clear_session_tab(args.port) | ||
| 323 | + | ||
| 324 | + # CLI 终端有桌面时自动打开二维码图片 | ||
| 325 | + _open_file_if_display(qrcode_path) | ||
| 326 | + | ||
| 327 | + from chrome_launcher import has_display | ||
| 328 | + | ||
| 329 | + if has_display(): | ||
| 330 | + _output({ | ||
| 331 | + "logged_in": False, | ||
| 332 | + "login_method": "qrcode", | ||
| 333 | + "qrcode_path": qrcode_path, | ||
| 334 | + "qrcode_data_url": qrcode_data_url, | ||
| 335 | + "hint": "未登录,二维码已自动生成。扫码后运行 wait-login 等待登录结果", | ||
| 336 | + }, exit_code=1) | ||
| 245 | else: | 337 | 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) | 338 | + _output({ |
| 339 | + "logged_in": False, | ||
| 340 | + "login_method": "both", | ||
| 341 | + "qrcode_path": qrcode_path, | ||
| 342 | + "qrcode_data_url": qrcode_data_url, | ||
| 343 | + "hint": ( | ||
| 344 | + "未登录,二维码已自动生成。" | ||
| 345 | + "方式A: 直接扫码 + wait-login;" | ||
| 346 | + "方式B: send-code --phone <手机号> + verify-code(手机验证码)" | ||
| 347 | + ), | ||
| 348 | + }, exit_code=1) | ||
| 267 | finally: | 349 | finally: |
| 268 | - # 不关闭 tab,保留页面供下次命令复用(_SESSION_TAB_FILE) | 350 | + # 只断开 CDP 连接,不关闭 tab——保留登录页面 |
| 269 | browser.close() | 351 | browser.close() |
| 270 | 352 | ||
| 271 | 353 | ||
| @@ -281,6 +363,7 @@ def cmd_login(args: argparse.Namespace) -> None: | @@ -281,6 +363,7 @@ def cmd_login(args: argparse.Namespace) -> None: | ||
| 281 | return | 363 | return |
| 282 | 364 | ||
| 283 | qrcode_path = save_qrcode_to_file(png_bytes) | 365 | qrcode_path = save_qrcode_to_file(png_bytes) |
| 366 | + _open_file_if_display(qrcode_path) | ||
| 284 | print( | 367 | print( |
| 285 | json.dumps( | 368 | json.dumps( |
| 286 | {"qrcode_path": qrcode_path, "message": "请扫码登录,二维码已保存到文件"}, | 369 | {"qrcode_path": qrcode_path, "message": "请扫码登录,二维码已保存到文件"}, |
| @@ -364,6 +447,13 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None: | @@ -364,6 +447,13 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None: | ||
| 364 | 447 | ||
| 365 | qrcode_path = save_qrcode_to_file(png_bytes) | 448 | qrcode_path = save_qrcode_to_file(png_bytes) |
| 366 | 449 | ||
| 450 | + # 生成 data URL,AI 可直接内嵌到 markdown 图片 | ||
| 451 | + import base64 as _b64 | ||
| 452 | + qrcode_data_url = "data:image/png;base64," + _b64.b64encode(png_bytes).decode() | ||
| 453 | + | ||
| 454 | + # CLI 终端有桌面时自动打开二维码图片 | ||
| 455 | + _open_file_if_display(qrcode_path) | ||
| 456 | + | ||
| 367 | # 记录 login tab,供 wait-login 精确 reconnect | 457 | # 记录 login tab,供 wait-login 精确 reconnect |
| 368 | _save_login_tab(page.target_id, args.port) | 458 | _save_login_tab(page.target_id, args.port) |
| 369 | # 清除 session tab 引用——隔离登录表单,防止其他命令复用并关闭/导航该 tab | 459 | # 清除 session tab 引用——隔离登录表单,防止其他命令复用并关闭/导航该 tab |
| @@ -373,7 +463,8 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None: | @@ -373,7 +463,8 @@ def cmd_get_qrcode(args: argparse.Namespace) -> None: | ||
| 373 | browser.close() | 463 | browser.close() |
| 374 | _output({ | 464 | _output({ |
| 375 | "qrcode_path": qrcode_path, | 465 | "qrcode_path": qrcode_path, |
| 376 | - "message": "二维码已生成,请扫码登录。扫码后运行 check-login 确认登录状态。", | 466 | + "qrcode_data_url": qrcode_data_url, |
| 467 | + "message": "二维码已生成,请扫码登录。扫码后运行 wait-login 等待登录结果。", | ||
| 377 | }) | 468 | }) |
| 378 | 469 | ||
| 379 | 470 | ||
| @@ -402,39 +493,39 @@ def cmd_wait_login(args: argparse.Namespace) -> None: | @@ -402,39 +493,39 @@ def cmd_wait_login(args: argparse.Namespace) -> None: | ||
| 402 | 493 | ||
| 403 | 494 | ||
| 404 | def cmd_send_code(args: argparse.Namespace) -> None: | 495 | def cmd_send_code(args: argparse.Namespace) -> None: |
| 405 | - """分步登录第一步:填写手机号并发送验证码,保持页面不关闭。""" | ||
| 406 | - from chrome_launcher import has_display, restart_chrome | 496 | + """分步登录第一步:填写手机号并发送验证码,保持页面不关闭。 |
| 497 | + | ||
| 498 | + 频率限制时返回错误信息和建议,由 AI 告知用户选择。 | ||
| 499 | + """ | ||
| 407 | from xhs.errors import RateLimitError | 500 | from xhs.errors import RateLimitError |
| 408 | from xhs.login import send_phone_code | 501 | from xhs.login import send_phone_code |
| 409 | 502 | ||
| 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() | 503 | + browser, page = _connect(args) |
| 504 | + try: | ||
| 505 | + sent = send_phone_code(page, args.phone) | ||
| 506 | + if not sent: | ||
| 507 | + _output({"logged_in": True, "message": "已登录,无需重新登录"}) | ||
| 436 | return | 508 | return |
| 437 | 509 | ||
| 510 | + # 记录 login tab,供 verify-code 精确 reconnect | ||
| 511 | + _save_login_tab(page.target_id, args.port) | ||
| 512 | + # 清除 session tab 引用——隔离登录表单,防止其他命令复用并关闭/导航该 tab | ||
| 513 | + _clear_session_tab(args.port) | ||
| 514 | + _output({ | ||
| 515 | + "status": "code_sent", | ||
| 516 | + "message": ( | ||
| 517 | + f"验证码已发送至 {args.phone[:3]}****{args.phone[-4:]}," | ||
| 518 | + "请运行 verify-code --code <验证码>" | ||
| 519 | + ), | ||
| 520 | + }) | ||
| 521 | + except RateLimitError: | ||
| 522 | + # 频率限制——直接切换二维码登录 | ||
| 523 | + logger.info("验证码发送受限,切换为二维码登录") | ||
| 524 | + _qrcode_fallback(browser, page, args) | ||
| 525 | + else: | ||
| 526 | + # 只断开控制连接,不关闭页面——tab 保持打开,verify-code 继续复用 | ||
| 527 | + browser.close() | ||
| 528 | + | ||
| 438 | 529 | ||
| 439 | def cmd_verify_code(args: argparse.Namespace) -> None: | 530 | def cmd_verify_code(args: argparse.Namespace) -> None: |
| 440 | """分步登录第二步:在已有页面上填写验证码并提交。""" | 531 | """分步登录第二步:在已有页面上填写验证码并提交。""" |
| @@ -118,15 +118,18 @@ def fetch_qrcode(page: Page) -> tuple[bytes, bool]: | @@ -118,15 +118,18 @@ def fetch_qrcode(page: Page) -> tuple[bytes, bool]: | ||
| 118 | - 如果已登录,返回 (b"", True) | 118 | - 如果已登录,返回 (b"", True) |
| 119 | - 如果未登录,返回 (png_bytes, False) | 119 | - 如果未登录,返回 (png_bytes, False) |
| 120 | """ | 120 | """ |
| 121 | - page.navigate(EXPLORE_URL) | ||
| 122 | - page.wait_for_load() | ||
| 123 | - _wait_for_auth_ui(page) | 121 | + # 如果当前页面已在 explore(如 check-login 刚导航过),跳过重复导航 |
| 122 | + current_url = page.evaluate("location.href") or "" | ||
| 123 | + if "explore" not in current_url: | ||
| 124 | + page.navigate(EXPLORE_URL) | ||
| 125 | + page.wait_for_load() | ||
| 124 | 126 | ||
| 127 | + # 快速检查是否已登录,避免无谓等待二维码 | ||
| 125 | if page.has_element(LOGIN_STATUS): | 128 | if page.has_element(LOGIN_STATUS): |
| 126 | return b"", True | 129 | return b"", True |
| 127 | 130 | ||
| 128 | - # 等待 img.qrcode-img 出现,用浏览器 Canvas 加白边后导出 PNG base64 | ||
| 129 | - page.wait_for_element(QRCODE_IMG, timeout=10.0) | 131 | + # 直接等待二维码元素出现,合并了 _wait_for_auth_ui 的逻辑 |
| 132 | + page.wait_for_element(QRCODE_IMG, timeout=15.0) | ||
| 130 | b64 = page.evaluate( | 133 | b64 = page.evaluate( |
| 131 | f""" | 134 | f""" |
| 132 | (() => {{ | 135 | (() => {{ |
| @@ -307,5 +310,5 @@ def wait_for_login(page: Page, timeout: float = 120.0) -> bool: | @@ -307,5 +310,5 @@ def wait_for_login(page: Page, timeout: float = 120.0) -> bool: | ||
| 307 | if page.has_element(LOGIN_STATUS): | 310 | if page.has_element(LOGIN_STATUS): |
| 308 | logger.info("登录成功") | 311 | logger.info("登录成功") |
| 309 | return True | 312 | return True |
| 310 | - time.sleep(0.5) | 313 | + time.sleep(0.3) |
| 311 | return False | 314 | return False |
| @@ -89,24 +89,16 @@ python scripts/cli.py check-login | @@ -89,24 +89,16 @@ python scripts/cli.py check-login | ||
| 89 | 89 | ||
| 90 | 输出解读: | 90 | 输出解读: |
| 91 | - `"logged_in": true` → 已登录,可执行后续操作。 | 91 | - `"logged_in": true` → 已登录,可执行后续操作。 |
| 92 | -- `"logged_in": false` + `"login_method": "qrcode"` → 有界面环境,走方式 A(二维码)。 | ||
| 93 | -- `"logged_in": false` + `"login_method": "both"` → 无界面服务器,**询问用户选方式 A(二维码)或方式 B(手机验证码)**。 | 92 | +- `"logged_in": false` + `"login_method": "qrcode"` → 有界面环境,走方式 A(二维码)。输出自动包含 `qrcode_data_url` 和 `qrcode_path`。 |
| 93 | +- `"logged_in": false` + `"login_method": "both"` → 无界面服务器,输出自动包含二维码,**询问用户选方式 A(二维码)或方式 B(手机验证码)**。 | ||
| 94 | 94 | ||
| 95 | ### 第二步:根据输出选择登录方式 | 95 | ### 第二步:根据输出选择登录方式 |
| 96 | 96 | ||
| 97 | #### 方式 A:二维码登录(所有平台通用) | 97 | #### 方式 A:二维码登录(所有平台通用) |
| 98 | 98 | ||
| 99 | -**第一步** — 获取二维码(非阻塞,立即返回): | 99 | +> `check-login` 未登录时会自动返回二维码(`qrcode_data_url` + `qrcode_path`),无需单独调 `get-qrcode`。 |
| 100 | 100 | ||
| 101 | -```bash | ||
| 102 | -python scripts/cli.py get-qrcode | ||
| 103 | -``` | ||
| 104 | - | ||
| 105 | -- Chrome 正常启动,从登录弹窗 `img` 元素读取二维码(相当于右键另存为)。 | ||
| 106 | -- 命令立即退出,Chrome tab 保持打开(QR 会话继续有效)。 | ||
| 107 | -- 输出:`{"qrcode_path": "...", "qrcode_data_url": "data:image/png;base64,...", "message": "..."}` | ||
| 108 | - | ||
| 109 | -**第二步** — 从 JSON 取 `qrcode_data_url`,在回复中直接写出: | 101 | +**第一步** — 从 `check-login` 返回的 JSON 取 `qrcode_data_url`,在回复中直接写出: |
| 110 | 102 | ||
| 111 | ``` | 103 | ``` |
| 112 |  | 104 |  |
| @@ -114,14 +106,16 @@ python scripts/cli.py get-qrcode | @@ -114,14 +106,16 @@ python scripts/cli.py get-qrcode | ||
| 114 | 106 | ||
| 115 | 图片内嵌在对话窗口,用户用小红书 App 扫对话里的二维码。 | 107 | 图片内嵌在对话窗口,用户用小红书 App 扫对话里的二维码。 |
| 116 | 108 | ||
| 117 | -**第三步** — 等待登录完成(**单次调用,无需轮询**): | 109 | +**第二步** — 等待登录完成(**单次调用,无需轮询**): |
| 118 | 110 | ||
| 119 | ```bash | 111 | ```bash |
| 120 | python scripts/cli.py wait-login | 112 | python scripts/cli.py wait-login |
| 121 | ``` | 113 | ``` |
| 122 | 114 | ||
| 123 | - 连接已有 Chrome tab,内部阻塞等待(最多 120 秒)。 | 115 | - 连接已有 Chrome tab,内部阻塞等待(最多 120 秒)。 |
| 124 | -- 输出 `{"logged_in": true}` 则完成;超时则提示用户重新运行 `get-qrcode`。 | 116 | +- 输出 `{"logged_in": true}` 则完成;超时则提示用户重新运行 `get-qrcode` 刷新二维码。 |
| 117 | + | ||
| 118 | +> **二维码过期刷新**:如需单独刷新二维码(如超时后),可运行 `get-qrcode`,它仍作为独立命令保留。 | ||
| 125 | 119 | ||
| 126 | #### 方式 B:手机验证码登录(无界面服务器,分两步) | 120 | #### 方式 B:手机验证码登录(无界面服务器,分两步) |
| 127 | 121 | ||
| @@ -140,7 +134,8 @@ python scripts/cli.py send-code --phone <用户确认的手机号> | @@ -140,7 +134,8 @@ python scripts/cli.py send-code --phone <用户确认的手机号> | ||
| 140 | ``` | 134 | ``` |
| 141 | - 自动填写手机号、勾选用户协议、点击"获取验证码"。 | 135 | - 自动填写手机号、勾选用户协议、点击"获取验证码"。 |
| 142 | - Chrome 页面保持打开,等待下一步。 | 136 | - Chrome 页面保持打开,等待下一步。 |
| 143 | -- 输出:`{"status": "code_sent", "message": "验证码已发送至 138****0000,请运行 verify-code --code <验证码>"}` | 137 | +- 正常输出:`{"status": "code_sent", "message": "..."}` |
| 138 | +- **频率限制**:自动切换为二维码登录,输出含 `qrcode_data_url`。告知用户"验证码发送受限,已切换为二维码登录",展示二维码,然后运行 `wait-login`。 | ||
| 144 | 139 | ||
| 145 | **第二步** — 向用户询问验证码,然后提交登录: | 140 | **第二步** — 向用户询问验证码,然后提交登录: |
| 146 | 141 |
-
Please register or login to post a comment