From ebea77b5f25a784d84fde990fb4841b2ff8cf2a2 Mon Sep 17 00:00:00 2001 From: taojianmin Date: Tue, 15 Sep 2026 01:27:05 +0800 Subject: [PATCH] chore: auto release commit (2026-09-15 01:27:04) --- CHANGELOG.md | 9 ++ SKILL.md | 2 +- scripts/service/platform_collector.py | 128 +++++++++++++++++++++----- scripts/util/constants.py | 2 +- tests/run_tests.py | 62 +++++++++++++ 5 files changed, 176 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ddc4e..6ba32fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 本文件对应技能市场 **「更新日志」** Tab。 +## 1.1.27 + +- **修复「到 SPA 二级登录页卡 30 秒」问题**: + - 之前步骤 4 等 URL 稳定到 `/app/` 或 `/select` 30 秒,但 sandbox 登录后会把人踢回 `/industries/campus/smartcampus/login`(URL 含 `/login`),等满 30 秒才发现要进 SPA 登录兜底 + - 步骤 4 改成等 URL 跳到 `/app/` 或 `/select` 或 `/login` 或 `/signin`(任一即可,**最多 30 秒**),URL 含 `/login` 立即退出等待 +- **新增 `_fill_demo_on_spa_login(page)`**:SPA 二级登录页 input 默认不预填(与根域不同),主动 `fill('demo')` + `fill('123456')` + 点登录按钮(Sign in / 登录 / Login / submit 任一) +- 步骤 4.5 改为:URL 落在 SPA 登录页 → 自动 `_fill_demo_on_spa_login` → 不再卡 180 秒等用户输入 +- 新增 3 个测试覆盖 `_fill_demo_on_spa_login`(basic / 中文按钮 / 无 input),合计 66/66 + ## 1.1.26 - **按用户最新要求改回「自动点登录」行为**:「到了 /industries/campus/smartcampus/login 整个页面,有账号和密码可用直接点登录跳转」 diff --git a/SKILL.md b/SKILL.md index 483d6a6..064f6a5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 合同到期智能预警与续聘辅助 description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。" -version: 1.1.26 +version: 1.1.27 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/scripts/service/platform_collector.py b/scripts/service/platform_collector.py index bbbf6b8..24b54ca 100644 --- a/scripts/service/platform_collector.py +++ b/scripts/service/platform_collector.py @@ -529,6 +529,76 @@ def _fill_root_login(page) -> bool: return True +def _fill_demo_on_spa_login(page) -> bool: + """v1.1.27:SPA 二级登录页(/industries/campus/smartcampus/login) + input 默认**不预填**(与根域不同),必须主动 fill demo/123456 再点登录。 + + selector 优先级: + - user: `#jc-user` → `input[type='tel']` → `input[autocomplete='username']` + - pass: `#jc-pass` → `input[type='password']` → `input[autocomplete='current-password']` + 按钮文本优先级:Sign in → 登录 → Login → signin → submit[type]。 + + 返回是否成功点登录按钮。 + """ + # 找 user input + user_loc = None + for sel in ("#jc-user", "input[type='tel']", "input[autocomplete='username']"): + loc = page.locator(sel).first + if loc.count() > 0: + user_loc = loc + break + if user_loc is None: + logger.warning( + "platform_collector._fill_demo_on_spa_login: 找不到 user input " + "(#jc-user / tel / autocomplete=username 全 miss)" + ) + return False + + # 找 pass input + pass_loc = None + for sel in ("#jc-pass", "input[type='password']", "input[autocomplete='current-password']"): + loc = page.locator(sel).first + if loc.count() > 0: + pass_loc = loc + break + if pass_loc is None: + logger.warning( + "platform_collector._fill_demo_on_spa_login: 找不到 pass input" + ) + return False + + user_loc.fill("demo") + pass_loc.fill("123456") + logger.info( + "platform_collector: SPA 二级登录页 fill demo/123456 完成" + ) + + # 找登录按钮 + btn = None + btn_text_used = "" + for txt in ("Sign in", "登录", "Login", "signin"): + loc = page.locator(f"button:has-text('{txt}')").first + if loc.count() > 0: + btn = loc + btn_text_used = txt + break + if btn is None: + btn = page.locator("button[type='submit']").first + btn_text_used = "submit" + if btn is None or btn.count() == 0: + logger.warning( + "platform_collector._fill_demo_on_spa_login: 找不到登录按钮" + ) + return False + + logger.info( + "platform_collector: SPA 二级登录页点 %s 按钮(demo/123456)", + btn_text_used, + ) + btn.click() + return True + + def _click_campus_link(page) -> bool: """在 /select 页面点智慧校园 tile,返回是否点中。""" link = page.locator("a[href*='/industries/campus']").first @@ -743,44 +813,52 @@ def collect_all( f"(请用 --show-browser 重试)" ) - # 4. 登录完成 → 等 URL 真正稳定到 /app/ 或 /select(最多 30s,**不做任何操作**) - # v1.1.22:sandbox 登录后会先跳 /select,再自动跳 /app/home, - # 所以两个 URL 都算"SPA 已路由" - if not _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30): + # 4. v1.1.27:等 URL 跳到任一「SPA 已路由」状态(最多 30s,**不做任何操作**) + # - /app/ = 已到 SPA 主页 + # - /select = sandbox 中间页 + # - /login 或 /signin = sandbox 又把人踢回登录页(根域登录后常见, + # sandbox 会跳到 /industries/campus/smartcampus/login SPA 二级登录页) + # - 如果 URL 跳到 login/signin,**立即**退出 30 秒等待(不再干等), + # 让 4.5 处理。否则会等满 30 秒才发现要进 SPA 登录兜底,造成卡顿。 + spa_url_seen = False + if not _wait_for_url_stable( + page, ("/app/", "/select", "/login", "/signin"), max_wait_sec=30, + ): logger.warning( - "platform_collector: 登录后 30s 内 URL 未稳定到 /app/或 /select(当前 URL=%s)", + "platform_collector: 登录后 30s 内 URL 未稳定到 /app/、/select、" + "/login 或 /signin(当前 URL=%s)", page.url, ) - # 不强行 reload —— 让用户决定是否手工刷新 - # 4.5. v1.1.24:sandbox 可能登录后把人踢回 SPA 二级登录页 - # (/industries/campus/smartcampus/login)。如果 URL 落在 - # 这个二级登录页,再走一次 _try_auto_sign_in(已经扩展支持 - # 中文「登录」按钮);若 SPA 登录页 input 有预填账号 → 自动点; - # 若 input 空 → 等用户输入并**自己点登录**(脚本不抢按钮)。 + # 4.5. v1.1.24/27:URL 落在 SPA 二级登录页 → 自动填 demo/123456 再登录 + # sandbox SPA 登录页 input 默认**不预填**(与根域不同), + # 所以 _try_auto_sign_in 不会自动点 —— 必须显式 fill demo/123456。 cur_url_after_stable = page.url.lower() if "/login" in cur_url_after_stable or "/signin" in cur_url_after_stable: logger.warning( - "platform_collector: 登录后仍落在登录页(%s)," - "v1.1.24 触发 SPA 二级登录兜底", + "platform_collector: 登录后落在 SPA 二级登录页(%s)," + "v1.1.27 触发 demo/123456 自动 fill", page.url, ) - spa_auto = _try_auto_sign_in(page, wait_input_sec=2) - if spa_auto: + spa_filled = _fill_demo_on_spa_login(page) + if spa_filled: logger.info( - "platform_collector: ✅ SPA 二级登录页已自动登录," - "等 URL 跳到 /app/ 或 /select" + "platform_collector: ✅ SPA 二级登录页 demo/123456 自动填好," + "已点登录,等 URL 跳到 /app/ 或 /select" ) _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30) - elif not headless: - if _wait_for_login_inputs_filled(page, wait_sec=human_wait_timeout): - _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30) - else: - _wait_for_manual_login(page, wait_sec=human_wait_timeout) else: - raise LoginTimeoutError( - f"SPA 二级登录页 input 空且 headless 无法手工登录(URL={page.url})" - ) + # demo fill 失败(input 不存在)→ 等用户手工输入 + if not headless: + if _wait_for_login_inputs_filled(page, wait_sec=human_wait_timeout): + _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30) + else: + _wait_for_manual_login(page, wait_sec=human_wait_timeout) + else: + raise LoginTimeoutError( + f"SPA 二级登录页 demo fill 失败且 headless 无法手工登录" + f"(URL={page.url})" + ) # 5. v1.1.22:显式跳到 SPA_HOME_URL(/app/home),等侧边栏 link 出现 # 避免 sandbox 在 /select 中间页时直接找 sidebar link 找不到 diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 4ebadce..0bc08c7 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.26" +SKILL_VERSION = "1.1.27" LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/run_tests.py b/tests/run_tests.py index cecbb16..b29f5ab 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1408,6 +1408,10 @@ class _FakeLocator: def click(self) -> None: self._click_log.append(self._value or "clicked") + def fill(self, value: str) -> None: + """v1.1.27:fill 后 value 立即更新。""" + self._value = value + class _FakeLocatorRouter: """根据 selector 返回对应 _FakeLocator 的路由器。 @@ -1588,6 +1592,61 @@ def test_wait_for_login_inputs_filled_auto_click_v1_26() -> tuple[bool, str]: pc.HUMAN_PROMPT_INTERVAL = orig_prompt +def test_fill_demo_on_spa_login_basic_v1_27() -> tuple[bool, str]: + """v1.1.27:`_fill_demo_on_spa_login` ——SPA 二级登录页 input 默认不预填, + 必须主动 fill demo/123456 再点登录按钮。 + """ + click_log: list[str] = [] + page = _FakePlaywrightPage( + url="https://sandbox.jc2009.com/industries/campus/smartcampus/login", + selectors={ + "#jc-user": _FakeLocator(count=1, value=""), # 空 → 要 fill + "#jc-pass": _FakeLocator(count=1, value=""), + "button:has-text('Sign in')": _FakeLocator(count=1, click_log=click_log), + }, + ) + from service.platform_collector import _fill_demo_on_spa_login + + if not _fill_demo_on_spa_login(page): + return False, "SPA login fill demo 应成功" + if not click_log: + return False, f"应点 Sign in,但 click_log={click_log}" + return True, f"SPA 二级登录页 demo/123456 自动 fill+点 OK(click_log={click_log})" + + +def test_fill_demo_on_spa_login_chinese_button_v1_27() -> tuple[bool, str]: + """v1.1.27:中文「登录」按钮(用户截图版本)也能 fill demo/123456 并点。""" + click_log: list[str] = [] + page = _FakePlaywrightPage( + url="https://sandbox.jc2009.com/industries/campus/smartcampus/login", + selectors={ + "input[type='tel']": _FakeLocator(count=1, value=""), + "input[type='password']": _FakeLocator(count=1, value=""), + "button:has-text('登录')": _FakeLocator(count=1, click_log=click_log), + }, + ) + from service.platform_collector import _fill_demo_on_spa_login + + if not _fill_demo_on_spa_login(page): + return False, "SPA login 中文按钮 fill demo 应成功" + if not click_log: + return False, f"应点 登录,但 click_log={click_log}" + return True, f"SPA 二级登录页中文「登录」按钮 OK(click_log={click_log})" + + +def test_fill_demo_on_spa_login_no_input_returns_false_v1_27() -> tuple[bool, str]: + """v1.1.27:找不到 user input(页面不是 login 页)→ 返回 False。""" + page = _FakePlaywrightPage( + url="https://sandbox.jc2009.com/industries/campus/smartcampus/app/home", + selectors={}, + ) + from service.platform_collector import _fill_demo_on_spa_login + + if _fill_demo_on_spa_login(page): + return False, "无 login input 应返回 False" + return True, "无 login input 返回 False OK" + + # ---------- v1.1.18: show-config / logs-list ---------- @@ -1866,6 +1925,9 @@ def main() -> int: _run("login_flow/auto_sign_in_no_input_returns_false", test_try_auto_sign_in_no_input_returns_false) _run("login_flow/auto_sign_in_selector_timeout_returns_false", test_try_auto_sign_in_selector_timeout_returns_false) _run("login_flow/wait_login_inputs_filled_auto_click_v1_26", test_wait_for_login_inputs_filled_auto_click_v1_26) + _run("login_flow/fill_demo_on_spa_login_basic_v1_27", test_fill_demo_on_spa_login_basic_v1_27) + _run("login_flow/fill_demo_on_spa_login_chinese_button_v1_27", test_fill_demo_on_spa_login_chinese_button_v1_27) + _run("login_flow/fill_demo_on_spa_login_no_input_returns_false_v1_27", test_fill_demo_on_spa_login_no_input_returns_false_v1_27) _run("show_config/masks_tokens", test_show_config_masks_tokens) _run("show_config/skips_empty", test_show_config_skips_empty) _run("show_config/skips_comments", test_show_config_skips_comments)