diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b0db2..c3a47a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 本文件对应技能市场 **「更新日志」** Tab。 +## 1.1.22 + +- **登录流程接入 sandbox 自动登录**:root 登录页(https://sandbox.jc2009.com/)的 #jc-user / #jc-pass 默认有 demo/123456 预填 + - 新增 `_try_auto_sign_in(page, wait_input_sec=2)`:等 input 出现 2 秒 → 检查是否有值 → 有则自动点 Sign in + - 新增 `_wait_for_login_inputs_filled(page, wait_sec)`:input 为空时等用户输入 → 两个 input 都有值后自动点 Sign in + - 已登录态(profile cookie 复用)直接跳过 + - 输入超时 fallback 到 `_wait_for_manual_login`(多信号判定) + ## 1.1.21 - **修复登录完成检测盲区**:`_wait_for_manual_login` 现在监听 3 个信号判断「登录已完成」,任一触发即继续 diff --git a/SKILL.md b/SKILL.md index 87ba10e..8cbb078 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 合同到期智能预警与续聘辅助 description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。" -version: 1.1.21 +version: 1.1.22 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/scripts/service/platform_collector.py b/scripts/service/platform_collector.py index f52ee76..c788526 100644 --- a/scripts/service/platform_collector.py +++ b/scripts/service/platform_collector.py @@ -167,8 +167,127 @@ def _is_logged_in(page) -> bool: ) +def _try_auto_sign_in(page, wait_input_sec: int = 2) -> bool: + """v1.1.21:检测登录页 demo/123456 是否已预填,有则自动点 Sign in。 + + 适用场景:sandbox root 登录页(https://sandbox.jc2009.com/) + 默认有 demo + 123456 预填在 #jc-user / #jc-pass,脚本无需 fill 直接点。 + + 行为: + 1. 等 #jc-user 出现,最长 wait_input_sec 秒(默认 2s) + 2. 检查 #jc-user 和 #jc-pass 是否都有值 + 3. 有值 → 点 Sign in → return True + 4. 无值 → return False(交给 _wait_for_manual_login 接管) + + 不会自动 fill(如果没值就放弃,让用户自己输)。 + """ + try: + page.wait_for_selector("#jc-user, #jc-pass", timeout=wait_input_sec * 1000) + except Exception: + logger.info( + "platform_collector: %ds 内未出现登录 input,跳过自动登录", + wait_input_sec, + ) + return False + + user_input = page.locator("#jc-user").first + pass_input = page.locator("#jc-pass").first + if user_input.count() == 0 or pass_input.count() == 0: + return False + + user_val = (user_input.input_value() or "").strip() + pass_val = (pass_input.input_value() or "").strip() + + if not user_val or not pass_val: + logger.info( + "platform_collector: 登录页 input 为空(user=%r, pass=%r)," + "等用户输入", + bool(user_val), bool(pass_val), + ) + return False + + logger.info( + "platform_collector: 登录页已预填账号(user=%r),自动点 Sign in", + user_val, + ) + btn = page.locator('button[type="submit"]:has-text("Sign in")').first + if btn.count() == 0: + logger.warning("platform_collector: 未找到 Sign in 按钮") + return False + btn.click() + return True + + +def _wait_for_login_inputs_filled(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> bool: + """v1.1.21:等用户输入完账号密码,然后自动点 Sign in。 + + - 每 HUMAN_POLL_INTERVAL 秒检查 #jc-user / #jc-pass 是否有值 + - 两个都有值 → 自动点 Sign in → return True + - 超时未填完 → return False + """ + deadline = time.monotonic() + wait_sec + last_prompt_at = time.monotonic() + last_state_log = time.monotonic() + + while time.monotonic() < deadline: + cur_url = page.url + user_input = page.locator("#jc-user").first + pass_input = page.locator("#jc-pass").first + + # 已不在登录页(用户可能手动点了别的) + if not _is_login_page_url_for_user(cur_url): + logger.info( + "platform_collector: URL 已离开登录页(%s),停止等输入", cur_url, + ) + return True + + if user_input.count() > 0 and pass_input.count() > 0: + user_val = (user_input.input_value() or "").strip() + pass_val = (pass_input.input_value() or "").strip() + now = time.monotonic() + if now - last_state_log >= HUMAN_PROMPT_INTERVAL: + last_state_log = now + logger.info( + "platform_collector: input 状态 user=%r pass=%r", + bool(user_val), bool(pass_val), + ) + if user_val and pass_val: + logger.info( + "platform_collector: ✅ 用户输入完成(user=%r),自动点 Sign in", + user_val, + ) + btn = page.locator('button[type="submit"]:has-text("Sign in")').first + if btn.count() > 0: + btn.click() + return True + + # 提醒用户 + now = time.monotonic() + if now - last_prompt_at >= HUMAN_PROMPT_INTERVAL: + last_prompt_at = now + remaining = int(deadline - now) + print(f"[登录] 等待用户输入账号密码... 剩余 {remaining}s") + + time.sleep(HUMAN_POLL_INTERVAL) + + logger.warning( + "platform_collector: %ds 内用户未完成输入", wait_sec, + ) + return False + + +def _is_login_page_url_for_user(url: str) -> bool: + """判断 URL 是否还在登录页(用于 _wait_for_login_inputs_filled 退出条件)。""" + u = url.lower() + if "/login" in u or "/signin" in u or "/auth/" in u: + return True + # 根域本身也是登录页(demo 数据未提交前的初始态) + return u.rstrip("/").endswith("sandbox.jc2009.com") + + def _wait_for_manual_login(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> bool: - """手工登录 fallback(v1.1.19 重写,v1.1.20 加强):等用户在浏览器完成登录。 + """手工登录 fallback(v1.1.19 重写,v1.1.20 加强,v1.1.21 修订): + 等用户在浏览器完成登录。 判定「登录完成」的多个信号(满足任一即视为完成): 1. URL 含 `/app/` 且稳定 URL_STABLE_REQUIRED_S 秒(v1.1.19 旧逻辑) @@ -493,19 +612,14 @@ def collect_all( # 所有 wait_for_selector 默认 30s,调试时不直观) page.set_default_timeout(DEFAULT_PAGE_TIMEOUT_MS) - # 1. v1.1.19:先 goto **根域**(用户最初看到的页面),**等 URL 稳定**, - # 避免被 sandbox 中间页误导或打断已登录态。 - # 已登录:根域会自动跳到 /app/home(满足 _is_logged_in) - # 未登录:根域就是登录页,用户在浏览器输入账号密码 + # 1. v1.1.21:先 goto **根域**(sandbox 登录页),让 SPA hydrate 2 秒 logger.info( "platform_collector: 启动,goto ROOT_URL=%s (当前 URL=%s)", ROOT_URL, page.url, ) _goto(page, ROOT_URL) - page.wait_for_timeout(SPA_HYDRATION_MS) - # 2. 等 URL 稳定:已登录→/app/xxx,未登录→根域或 /login - # **不做任何操作**,避免打断浏览器 session - _wait_for_url_stable(page, "/", max_wait_sec=10) + # 给 React/Vue 框架 2 秒 hydrate 时间,看登录 input 是否就绪 + page.wait_for_timeout(2_000) if _is_logged_in(page): logger.info( @@ -513,20 +627,41 @@ def collect_all( page.url, ) else: - logger.info( - "platform_collector: 未登录态(URL=%s),等用户在浏览器输入账号密码", - page.url, - ) - # 3. v1.1.19 用户反馈:不要自动 fill demo/123456,**纯等用户手工输入**。 - # 不强制 goto SPA_LOGIN_URL(避免打断根域的输入流程) - if not headless: - _wait_for_manual_login(page, wait_sec=human_wait_timeout) + # 2. 等 #jc-user / #jc-pass 出现(最多 2 秒) + # 若 sandbox 已预填 demo/123456 → 自动点 Sign in + # 若 input 为空 → 等用户输入完成后再自动点 Sign in + auto_clicked = _try_auto_sign_in(page, wait_input_sec=2) + if auto_clicked: + logger.info( + "platform_collector: ✅ 自动登录成功(预填 demo),等 URL 跳到 /app/" + ) + elif not headless: + # 3. input 为空:等用户输入 → 自动点 Sign in + if _wait_for_login_inputs_filled(page, wait_sec=human_wait_timeout): + logger.info( + "platform_collector: ✅ 用户输入完成并点击,等 URL 跳到 /app/" + ) + else: + # 超时未输入 → fallback 到原来的多信号等待 + logger.warning( + "platform_collector: 用户未输入完成,fallback 到 _wait_for_manual_login" + ) + _wait_for_manual_login(page, wait_sec=human_wait_timeout) else: raise LoginTimeoutError( f"headless 模式下未登录,且无法手工登录" f"(请用 --show-browser 重试)" ) + # 4. 登录完成 → 等 URL 真正稳定到 /app/(最多 30s,**不做任何操作**) + # 之前 retry loop 会强制 goto home / fill login,**打断用户刚登录态**。 + if not _wait_for_url_stable(page, "/app/", max_wait_sec=30): + logger.warning( + "platform_collector: 登录后 30s 内 URL 未稳定到 /app/(当前 URL=%s)", + page.url, + ) + # 不强行 reload —— 让用户决定是否手工刷新 + # 5. 登录完成 → 等 URL 真正稳定到 /app/(最多 30s,**不做任何操作**) # 之前 retry loop 会强制 goto home / fill login,**打断用户刚登录态**。 if not _wait_for_url_stable(page, "/app/", max_wait_sec=30): diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 154bcfd..78bb7bf 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -6,7 +6,7 @@ """ SKILL_SLUG = "alert-contract-expiring-hr" -SKILL_VERSION = "1.1.21" +SKILL_VERSION = "1.1.22" LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr" PLATFORM_KIT_MIN_VERSION = "1.2.11"