perf: 手机验证码登录流程速度优化 + 手机号确认强制化
- send_phone_code: 合并重叠的 UI 等待逻辑,固定 sleep 改为事件驱动轮询倒计时 - submit_phone_code: type_text delay_ms=0 替代 JS setValue(保证 isTrusted=true) - cmd_phone_login: 去掉 close_page 与 verify-code 保持一致 - SKILL.md: 强制每次登录必须向用户确认手机号,禁止自动填入
Showing
3 changed files
with
41 additions
and
20 deletions
| @@ -339,7 +339,7 @@ def cmd_phone_login(args: argparse.Namespace) -> None: | @@ -339,7 +339,7 @@ def cmd_phone_login(args: argparse.Namespace) -> None: | ||
| 339 | exit_code=0 if success else 2, | 339 | exit_code=0 if success else 2, |
| 340 | ) | 340 | ) |
| 341 | finally: | 341 | finally: |
| 342 | - browser.close_page(page) | 342 | + # 不关闭 tab——与 verify-code 一致,保留页面供重试 |
| 343 | browser.close() | 343 | browser.close() |
| 344 | 344 | ||
| 345 | 345 |
| @@ -36,6 +36,20 @@ from .urls import EXPLORE_URL | @@ -36,6 +36,20 @@ from .urls import EXPLORE_URL | ||
| 36 | logger = logging.getLogger(__name__) | 36 | logger = logging.getLogger(__name__) |
| 37 | 37 | ||
| 38 | 38 | ||
| 39 | +def _wait_for_countdown(page: Page, timeout: float = 5.0) -> None: | ||
| 40 | + """等待"获取验证码"按钮出现倒计时数字,确认验证码已发送。 | ||
| 41 | + | ||
| 42 | + 轮询按钮文字直到包含数字(如 "60s"),超时则抛出 RateLimitError。 | ||
| 43 | + """ | ||
| 44 | + deadline = time.monotonic() + timeout | ||
| 45 | + while time.monotonic() < deadline: | ||
| 46 | + btn_text = page.get_element_text(GET_CODE_BUTTON) or "" | ||
| 47 | + if any(ch.isdigit() for ch in btn_text): | ||
| 48 | + return | ||
| 49 | + time.sleep(0.3) | ||
| 50 | + raise RateLimitError() | ||
| 51 | + | ||
| 52 | + | ||
| 39 | def _wait_for_auth_ui(page: Page, timeout: float = 8.0) -> None: | 53 | def _wait_for_auth_ui(page: Page, timeout: float = 8.0) -> None: |
| 40 | """等待认证 UI 出现,替代固定延迟。 | 54 | """等待认证 UI 出现,替代固定延迟。 |
| 41 | 55 | ||
| @@ -171,20 +185,26 @@ def send_phone_code(page: Page, phone: str) -> bool: | @@ -171,20 +185,26 @@ def send_phone_code(page: Page, phone: str) -> bool: | ||
| 171 | """ | 185 | """ |
| 172 | page.navigate(EXPLORE_URL) | 186 | page.navigate(EXPLORE_URL) |
| 173 | page.wait_for_load() | 187 | page.wait_for_load() |
| 174 | - sleep_random(1500, 2500) | 188 | + |
| 189 | + # 直接等待登录容器出现(合并了 _wait_for_auth_ui 的逻辑,避免重复等待) | ||
| 190 | + try: | ||
| 191 | + page.wait_for_element(LOGIN_CONTAINER, timeout=10.0) | ||
| 192 | + except Exception as exc: | ||
| 193 | + # 可能已登录(没有登录容器),检查登录状态 | ||
| 194 | + if page.has_element(LOGIN_STATUS): | ||
| 195 | + return False | ||
| 196 | + raise RuntimeError("找不到登录表单") from exc | ||
| 175 | 197 | ||
| 176 | if page.has_element(LOGIN_STATUS): | 198 | if page.has_element(LOGIN_STATUS): |
| 177 | return False | 199 | return False |
| 178 | 200 | ||
| 179 | - # 等待登录弹窗出现 | ||
| 180 | - page.wait_for_element(LOGIN_CONTAINER, timeout=15.0) | ||
| 181 | - sleep_random(500, 800) | 201 | + sleep_random(200, 400) |
| 182 | 202 | ||
| 183 | # 点击手机号输入框并逐字输入 | 203 | # 点击手机号输入框并逐字输入 |
| 184 | page.click_element(PHONE_INPUT) | 204 | page.click_element(PHONE_INPUT) |
| 185 | sleep_random(200, 400) | 205 | sleep_random(200, 400) |
| 186 | page.type_text(phone, delay_ms=80) | 206 | page.type_text(phone, delay_ms=80) |
| 187 | - sleep_random(500, 800) | 207 | + sleep_random(200, 400) |
| 188 | 208 | ||
| 189 | # 先勾选用户协议,再点获取验证码 | 209 | # 先勾选用户协议,再点获取验证码 |
| 190 | if not page.has_element(AGREE_CHECKBOX_CHECKED): | 210 | if not page.has_element(AGREE_CHECKBOX_CHECKED): |
| @@ -193,12 +213,9 @@ def send_phone_code(page: Page, phone: str) -> bool: | @@ -193,12 +213,9 @@ def send_phone_code(page: Page, phone: str) -> bool: | ||
| 193 | 213 | ||
| 194 | # 点击"获取验证码" | 214 | # 点击"获取验证码" |
| 195 | page.click_element(GET_CODE_BUTTON) | 215 | page.click_element(GET_CODE_BUTTON) |
| 196 | - sleep_random(2000, 2500) | ||
| 197 | 216 | ||
| 198 | - # 检测按钮是否变为倒计时(成功发送后按钮文字会包含数字秒数) | ||
| 199 | - btn_text = page.get_element_text(GET_CODE_BUTTON) or "" | ||
| 200 | - if not any(ch.isdigit() for ch in btn_text): | ||
| 201 | - raise RateLimitError() | 217 | + # 事件驱动:轮询按钮文字直到出现倒计时数字,替代固定 2-2.5s 等待 |
| 218 | + _wait_for_countdown(page) | ||
| 202 | 219 | ||
| 203 | logger.info("验证码已发送至 %s", phone[:3] + "****" + phone[-4:]) | 220 | logger.info("验证码已发送至 %s", phone[:3] + "****" + phone[-4:]) |
| 204 | return True | 221 | return True |
| @@ -214,9 +231,9 @@ def submit_phone_code(page: Page, code: str) -> bool: | @@ -214,9 +231,9 @@ def submit_phone_code(page: Page, code: str) -> bool: | ||
| 214 | Returns: | 231 | Returns: |
| 215 | True 登录成功,False 失败(超时或验证码错误)。 | 232 | True 登录成功,False 失败(超时或验证码错误)。 |
| 216 | """ | 233 | """ |
| 217 | - # 点击验证码输入框,先清空已有内容(防止重试时追加导致验证码错误),再逐字输入 | 234 | + # 点击验证码输入框,先清空再用 CDP 键盘事件逐字输入(isTrusted=true,React 能识别) |
| 218 | page.click_element(CODE_INPUT) | 235 | page.click_element(CODE_INPUT) |
| 219 | - sleep_random(300, 500) | 236 | + sleep_random(100, 200) |
| 220 | page.evaluate( | 237 | page.evaluate( |
| 221 | f"""(() => {{ | 238 | f"""(() => {{ |
| 222 | const el = document.querySelector({json.dumps(CODE_INPUT)}); | 239 | const el = document.querySelector({json.dumps(CODE_INPUT)}); |
| @@ -229,12 +246,12 @@ def submit_phone_code(page: Page, code: str) -> bool: | @@ -229,12 +246,12 @@ def submit_phone_code(page: Page, code: str) -> bool: | ||
| 229 | }} | 246 | }} |
| 230 | }})()""" | 247 | }})()""" |
| 231 | ) | 248 | ) |
| 232 | - page.type_text(code, delay_ms=100) | ||
| 233 | - sleep_random(500, 800) | 249 | + page.type_text(code, delay_ms=0) |
| 250 | + sleep_random(100, 200) | ||
| 234 | 251 | ||
| 235 | # 点击登录按钮 | 252 | # 点击登录按钮 |
| 236 | page.click_element(PHONE_LOGIN_SUBMIT) | 253 | page.click_element(PHONE_LOGIN_SUBMIT) |
| 237 | - sleep_random(1000, 2000) | 254 | + sleep_random(500, 1000) |
| 238 | 255 | ||
| 239 | # 检查是否有错误提示 | 256 | # 检查是否有错误提示 |
| 240 | err = page.get_element_text(LOGIN_ERR_MSG) | 257 | err = page.get_element_text(LOGIN_ERR_MSG) |
| @@ -125,14 +125,18 @@ python scripts/cli.py wait-login | @@ -125,14 +125,18 @@ python scripts/cli.py wait-login | ||
| 125 | 125 | ||
| 126 | #### 方式 B:手机验证码登录(无界面服务器,分两步) | 126 | #### 方式 B:手机验证码登录(无界面服务器,分两步) |
| 127 | 127 | ||
| 128 | -**执行前必须先向用户索取手机号,不得自行假设或跳过此步。** | 128 | +**⚠️ 强制要求:必须先向用户确认手机号,即使上下文中已有手机号也不得跳过。** |
| 129 | +- 用户可能要登录不同账号,手机号可能已变更。 | ||
| 130 | +- **禁止从历史对话、记忆或上下文中自动填入手机号。** | ||
| 131 | +- **每次登录都必须明确向用户询问并得到确认后才能执行 `send-code`。** | ||
| 129 | 132 | ||
| 130 | -**第一步** — 向用户询问手机号,然后发送验证码: | 133 | +**第一步** — 向用户确认手机号,然后发送验证码: |
| 131 | 134 | ||
| 132 | -> 请先问用户:"请提供您的手机号(不含国家码,如 13800138000)",获得回复后再执行以下命令。 | 135 | +> **必须先问用户**:"请提供您要登录的手机号(不含国家码,如 13800138000)"。 |
| 136 | +> 收到用户明确回复手机号后,才能执行以下命令。**不得跳过此步。** | ||
| 133 | 137 | ||
| 134 | ```bash | 138 | ```bash |
| 135 | -python scripts/cli.py send-code --phone <用户提供的手机号> | 139 | +python scripts/cli.py send-code --phone <用户确认的手机号> |
| 136 | ``` | 140 | ``` |
| 137 | - 自动填写手机号、勾选用户协议、点击"获取验证码"。 | 141 | - 自动填写手机号、勾选用户协议、点击"获取验证码"。 |
| 138 | - Chrome 页面保持打开,等待下一步。 | 142 | - Chrome 页面保持打开,等待下一步。 |
-
Please register or login to post a comment