chore: auto release commit (2026-09-15 01:27:04)
Some checks failed
技能自动化发布 / release (push) Failing after 4s

This commit is contained in:
2026-09-15 01:27:05 +08:00
parent 430274dbf9
commit ebea77b5f2
5 changed files with 176 additions and 27 deletions

View File

@@ -2,6 +2,15 @@
本文件对应技能市场 **「更新日志」** Tab。 本文件对应技能市场 **「更新日志」** 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 ## 1.1.26
- **按用户最新要求改回「自动点登录」行为**:「到了 /industries/campus/smartcampus/login 整个页面,有账号和密码可用直接点登录跳转」 - **按用户最新要求改回「自动点登录」行为**:「到了 /industries/campus/smartcampus/login 整个页面,有账号和密码可用直接点登录跳转」

View File

@@ -1,7 +1,7 @@
--- ---
name: 合同到期智能预警与续聘辅助 name: 合同到期智能预警与续聘辅助
description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。" description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。"
version: 1.1.26 version: 1.1.27
author: 深圳匠厂科技有限公司 author: 深圳匠厂科技有限公司
metadata: metadata:
openclaw: openclaw:

View File

@@ -529,6 +529,76 @@ def _fill_root_login(page) -> bool:
return True 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: def _click_campus_link(page) -> bool:
"""在 /select 页面点智慧校园 tile,返回是否点中。""" """在 /select 页面点智慧校园 tile,返回是否点中。"""
link = page.locator("a[href*='/industries/campus']").first link = page.locator("a[href*='/industries/campus']").first
@@ -743,44 +813,52 @@ def collect_all(
f"(请用 --show-browser 重试)" f"(请用 --show-browser 重试)"
) )
# 4. 登录完成 → 等 URL 真正稳定到 /app/ 或 /select(最多 30s,**不做任何操作**) # 4. v1.1.27:等 URL 跳到任一「SPA 已路由」状态(最多 30s,**不做任何操作**)
# v1.1.22:sandbox 登录后会先跳 /select,再自动跳 /app/home, # - /app/ = 已到 SPA 主页
# 所以两个 URL 都算"SPA 已路由" # - /select = sandbox 中间页
if not _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30): # - /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( logger.warning(
"platform_collector: 登录后 30s 内 URL 未稳定到 /app/或 /select(当前 URL=%s)", "platform_collector: 登录后 30s 内 URL 未稳定到 /app/、/select、"
"/login 或 /signin(当前 URL=%s)",
page.url, page.url,
) )
# 不强行 reload —— 让用户决定是否手工刷新
# 4.5. v1.1.24:sandbox 可能登录后把人踢回 SPA 二级登录页 # 4.5. v1.1.24/27:URL 落在 SPA 二级登录页 → 自动填 demo/123456 再登录
# (/industries/campus/smartcampus/login)。如果 URL 落在 # sandbox SPA 登录页 input 默认**不预填**(与根域不同),
# 这个二级登录页,再走一次 _try_auto_sign_in(已经扩展支持 # 所以 _try_auto_sign_in 不会自动点 —— 必须显式 fill demo/123456。
# 中文「登录」按钮);若 SPA 登录页 input 有预填账号 → 自动点;
# 若 input 空 → 等用户输入并**自己点登录**(脚本不抢按钮)。
cur_url_after_stable = page.url.lower() cur_url_after_stable = page.url.lower()
if "/login" in cur_url_after_stable or "/signin" in cur_url_after_stable: if "/login" in cur_url_after_stable or "/signin" in cur_url_after_stable:
logger.warning( logger.warning(
"platform_collector: 登录后仍落在登录页(%s)," "platform_collector: 登录后落在 SPA 二级登录页(%s),"
"v1.1.24 触发 SPA 二级登录兜底", "v1.1.27 触发 demo/123456 自动 fill",
page.url, page.url,
) )
spa_auto = _try_auto_sign_in(page, wait_input_sec=2) spa_filled = _fill_demo_on_spa_login(page)
if spa_auto: if spa_filled:
logger.info( logger.info(
"platform_collector: ✅ SPA 二级登录页已自动登录," "platform_collector: ✅ SPA 二级登录页 demo/123456 自动填好,"
"等 URL 跳到 /app/ 或 /select" "已点登录,等 URL 跳到 /app/ 或 /select"
) )
_wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30) _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: else:
raise LoginTimeoutError( # demo fill 失败(input 不存在)→ 等用户手工输入
f"SPA 二级登录页 input 空且 headless 无法手工登录(URL={page.url})" 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 出现 # 5. v1.1.22:显式跳到 SPA_HOME_URL(/app/home),等侧边栏 link 出现
# 避免 sandbox 在 /select 中间页时直接找 sidebar link 找不到 # 避免 sandbox 在 /select 中间页时直接找 sidebar link 找不到

View File

@@ -6,7 +6,7 @@
""" """
SKILL_SLUG = "alert-contract-expiring-hr" 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" LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr"
PLATFORM_KIT_MIN_VERSION = "1.2.11" PLATFORM_KIT_MIN_VERSION = "1.2.11"

View File

@@ -1408,6 +1408,10 @@ class _FakeLocator:
def click(self) -> None: def click(self) -> None:
self._click_log.append(self._value or "clicked") self._click_log.append(self._value or "clicked")
def fill(self, value: str) -> None:
"""v1.1.27:fill 后 value 立即更新。"""
self._value = value
class _FakeLocatorRouter: class _FakeLocatorRouter:
"""根据 selector 返回对应 _FakeLocator 的路由器。 """根据 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 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 ---------- # ---------- 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_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/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/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/masks_tokens", test_show_config_masks_tokens)
_run("show_config/skips_empty", test_show_config_skips_empty) _run("show_config/skips_empty", test_show_config_skips_empty)
_run("show_config/skips_comments", test_show_config_skips_comments) _run("show_config/skips_comments", test_show_config_skips_comments)