chore: auto release commit (2026-09-15 00:57:09)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -36,6 +36,7 @@ from util.constants import (
|
||||
FULL_RENDER_WAIT_MS,
|
||||
GOTO_TIMEOUT_MS,
|
||||
HUMAN_POLL_INTERVAL,
|
||||
HUMAN_PROMPT_INTERVAL,
|
||||
HUMAN_WAIT_TIMEOUT,
|
||||
POST_LOGIN_SETTLE_MS,
|
||||
POST_RELOAD_LOGIN_HYDRATION_MS,
|
||||
@@ -55,6 +56,8 @@ from util.constants import (
|
||||
TABLE_POLL_INTERVAL_MS,
|
||||
TABLE_POLL_MAX_ATTEMPTS,
|
||||
TABLE_TR_TIMEOUT_MS,
|
||||
URL_STABLE_POLL_S,
|
||||
URL_STABLE_REQUIRED_S,
|
||||
URL_AFTER_LOGIN_TIMEOUT_MS,
|
||||
)
|
||||
from util.logging_setup import get_skill_logger
|
||||
@@ -165,32 +168,100 @@ def _is_logged_in(page) -> bool:
|
||||
|
||||
|
||||
def _wait_for_manual_login(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> bool:
|
||||
"""手工登录 fallback:轮询 _is_logged_in,每 HUMAN_POLL_INTERVAL 秒一次。
|
||||
"""手工登录 fallback(v1.1.19 重写):等用户在浏览器输完账号密码提交。
|
||||
|
||||
与 skill-template task_rpa.py::_ensure_logged_in 设计对齐:
|
||||
- 不自动破解,等用户在浏览器手工完成登录
|
||||
- 屏幕打印 [登录] 提示
|
||||
- 超时抛 LoginTimeoutError(ERROR:LOGIN_TIMEOUT)
|
||||
与之前轮询 _is_logged_in 不同的设计:
|
||||
- **不做任何 goto / reload / click**,避免打断用户输入
|
||||
- 每 HUMAN_PROMPT_INTERVAL 秒打印一次剩余时间(默认 15s)
|
||||
- 每 HUMAN_POLL_INTERVAL 秒(默认 5s)只检查 URL,不做任何操作
|
||||
- 用户完成登录(URL 跳到 /app/...)后 **等 URL 真正稳定**,再返回 True
|
||||
|
||||
自动 fill demo/123456 失败时调用此函数。
|
||||
超时抛 LoginTimeoutError。
|
||||
"""
|
||||
print(
|
||||
f"[登录] 自动登录失败,请在浏览器中完成登录"
|
||||
f"[登录] 请在浏览器中完成登录"
|
||||
f"(sandbox demo/123456 或真实账号),最多等待 {wait_sec} 秒..."
|
||||
)
|
||||
deadline = time.monotonic() + wait_sec
|
||||
attempt = 0
|
||||
while time.monotonic() < deadline:
|
||||
attempt += 1
|
||||
last_prompt_at = time.monotonic()
|
||||
last_url = page.url
|
||||
last_url_change_at = time.monotonic()
|
||||
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
# 用户输入完提交后,URL 会从 /login 跳到 /app/...
|
||||
cur_url = page.url
|
||||
if cur_url != last_url:
|
||||
logger.info(
|
||||
"platform_collector: URL 变化 %s → %s", last_url, cur_url,
|
||||
)
|
||||
last_url = cur_url
|
||||
last_url_change_at = time.monotonic()
|
||||
|
||||
if _is_logged_in(page):
|
||||
print(f"[登录] 检测到登录成功(attempt {attempt})")
|
||||
return True
|
||||
# URL 已跳到 /app/...:等 URL 稳定 URL_STABLE_REQUIRED_S 秒再返回
|
||||
stable_for = time.monotonic() - last_url_change_at
|
||||
if stable_for >= URL_STABLE_REQUIRED_S:
|
||||
print(f"[登录] ✅ 检测到登录完成(URL={cur_url}, 稳定 {stable_for:.1f}s)")
|
||||
return True
|
||||
logger.info(
|
||||
"platform_collector: 登录后 URL 还在过渡,等稳定 "
|
||||
"(current=%s, stable_for=%.1fs)", cur_url, stable_for,
|
||||
)
|
||||
else:
|
||||
# 还在登录页:每 HUMAN_PROMPT_INTERVAL 秒提醒用户一次
|
||||
now = time.monotonic()
|
||||
if now - last_prompt_at >= HUMAN_PROMPT_INTERVAL:
|
||||
last_prompt_at = now
|
||||
print(
|
||||
f"[登录] 等待用户输入... 剩余 {int(remaining)}s "
|
||||
f"(当前 URL={cur_url})"
|
||||
)
|
||||
|
||||
time.sleep(HUMAN_POLL_INTERVAL)
|
||||
|
||||
raise LoginTimeoutError(
|
||||
f"自动登录失败,{wait_sec}s 内用户在浏览器中也未完成登录"
|
||||
)
|
||||
|
||||
|
||||
def _wait_for_url_stable(
|
||||
page, target_substr: str, max_wait_sec: int = 30,
|
||||
) -> bool:
|
||||
"""v1.1.19:等 URL 含 target_substr,且连续 URL_STABLE_REQUIRED_S 秒不变。
|
||||
|
||||
用于「已登录态直接跳到目标页面」后等待 SPA 路由稳定。
|
||||
**不做任何 goto / reload**,只 listen URL 变化。
|
||||
|
||||
Returns:
|
||||
True = URL 已稳定到目标;False = 超时。
|
||||
"""
|
||||
deadline = time.monotonic() + max_wait_sec
|
||||
last_url = page.url
|
||||
last_url_change_at = time.monotonic()
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
cur_url = page.url
|
||||
if cur_url != last_url:
|
||||
logger.info(
|
||||
"platform_collector._wait_for_url_stable: URL 变化 %s → %s",
|
||||
last_url, cur_url,
|
||||
)
|
||||
last_url = cur_url
|
||||
last_url_change_at = time.monotonic()
|
||||
|
||||
if target_substr in cur_url.lower():
|
||||
stable_for = time.monotonic() - last_url_change_at
|
||||
if stable_for >= URL_STABLE_REQUIRED_S:
|
||||
return True
|
||||
|
||||
time.sleep(URL_STABLE_POLL_S)
|
||||
return False
|
||||
|
||||
|
||||
def _fill_root_login(page) -> bool:
|
||||
"""根域登录页(如还在):fill demo/123456 → click Sign in。返回是否执行了登录。"""
|
||||
user_input = page.locator('#jc-user').first
|
||||
@@ -384,81 +455,48 @@ def collect_all(
|
||||
# 所有 wait_for_selector 默认 30s,调试时不直观)
|
||||
page.set_default_timeout(DEFAULT_PAGE_TIMEOUT_MS)
|
||||
|
||||
# 1. v1.1.11:先用**最小侵入**方式检测已登录态 —— 直接 goto SPA_HOME_URL
|
||||
# (不要先跳 ROOT_URL + 自动 fill,会破坏已登录态)
|
||||
# v1.1.11 改进:探针结果 + 二次等待 + cookie 检测,三重判断避免误判
|
||||
# 1. v1.1.19:先 goto **根域**(用户最初看到的页面),**等 URL 稳定**,
|
||||
# 避免被 sandbox 中间页误导或打断已登录态。
|
||||
# 已登录:根域会自动跳到 /app/home(满足 _is_logged_in)
|
||||
# 未登录:根域就是登录页,用户在浏览器输入账号密码
|
||||
logger.info(
|
||||
"platform_collector: 检测登录态,goto SPA_HOME 先探 (当前 URL=%s)",
|
||||
page.url,
|
||||
"platform_collector: 启动,goto ROOT_URL=%s (当前 URL=%s)",
|
||||
ROOT_URL, page.url,
|
||||
)
|
||||
_goto(page, SPA_HOME_URL)
|
||||
# v1.1.11:等 sandbox SPA 异步验证 cookie(POST_LOGIN_SETTLE_MS → SPA_HYDRATION_MS)
|
||||
_goto(page, ROOT_URL)
|
||||
page.wait_for_timeout(SPA_HYDRATION_MS)
|
||||
# v1.1.11:二次等待 —— sandbox SPA 有时首次访问会先跳一个中间过渡页
|
||||
# (如 /select),等几秒看 URL 是否稳定到 /app/
|
||||
first_url = page.url
|
||||
page.wait_for_timeout(SPA_HOME_RETRY_DELAY_MS)
|
||||
second_url = page.url
|
||||
if first_url != second_url:
|
||||
logger.info(
|
||||
"platform_collector: 探针 URL 过渡 %s → %s,等稳定", first_url, second_url,
|
||||
)
|
||||
page.wait_for_timeout(SPA_HOME_RETRY_DELAY_MS)
|
||||
# 2. 等 URL 稳定:已登录→/app/xxx,未登录→根域或 /login
|
||||
# **不做任何操作**,避免打断浏览器 session
|
||||
_wait_for_url_stable(page, "/", max_wait_sec=10)
|
||||
|
||||
if _is_logged_in(page):
|
||||
logger.info(
|
||||
"platform_collector: ✅ 已登录态,**完全跳过登录流程**(不弹窗、不自动 fill、不打扰)"
|
||||
",URL=%s", page.url,
|
||||
"platform_collector: ✅ 已登录态(URL=%s),跳过登录流程",
|
||||
page.url,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"platform_collector: 未登录态(SPA_HOME 跳回 login, URL=%s),走登录分支",
|
||||
"platform_collector: 未登录态(URL=%s),等用户在浏览器输入账号密码",
|
||||
page.url,
|
||||
)
|
||||
# 2. 未登录:直接 goto sandbox SPA login(绕过 /select 和 root 跳转)
|
||||
_goto(page, SPA_LOGIN_URL)
|
||||
# SPA hydration + framework 初始化时间(修复登录时间太短的核心点)
|
||||
page.wait_for_timeout(SPA_HYDRATION_MS)
|
||||
|
||||
# 3. sandbox SPA 自动登录(demo/123456 已知可用)
|
||||
# 失败 fallback 手工登录等待(轮询 _is_logged_in + 180s 兜底)
|
||||
if not _fill_spa_login(page):
|
||||
logger.warning(
|
||||
"platform_collector: 自动 fill 失败(URL=%s),fallback 手工登录等待",
|
||||
page.url,
|
||||
)
|
||||
if not headless:
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
# headless 模式无法手工登录:抛 LoginTimeoutError 让 CLI 决定
|
||||
raise LoginTimeoutError(
|
||||
f"headless 模式下自动 fill 失败,且无法手工登录"
|
||||
f"(请用 --show-browser 重试)"
|
||||
)
|
||||
# 3. v1.1.19 用户反馈:不要自动 fill demo/123456,**纯等用户手工输入**。
|
||||
# 不强制 goto SPA_LOGIN_URL(避免打断根域的输入流程)
|
||||
if not headless:
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
# 等 SPA 路由稳定(之前这里没有 wait,直接 goto home 会打断 pending 跳转)
|
||||
page.wait_for_timeout(POST_LOGIN_SETTLE_MS)
|
||||
|
||||
# 4. 强制 goto SPA home(如果还停在 login,重试登录一次)
|
||||
for retry in range(SPA_HOME_RETRY_MAX):
|
||||
if _is_logged_in(page):
|
||||
break
|
||||
logger.info(
|
||||
"platform_collector: 强制 goto SPA_HOME (retry=%d), 当前 URL=%s",
|
||||
retry, page.url,
|
||||
)
|
||||
_goto(page, SPA_HOME_URL)
|
||||
page.wait_for_timeout(SPA_HOME_RETRY_DELAY_MS)
|
||||
if _fill_spa_login(page):
|
||||
page.wait_for_timeout(POST_LOGIN_SETTLE_MS)
|
||||
|
||||
# 4.5. retry 后若仍未登录,headless 模式抛超时
|
||||
if not _is_logged_in(page):
|
||||
if headless:
|
||||
raise LoginTimeoutError(
|
||||
"SPA_HOME retry 全部失败,URL 仍未离开 login 页"
|
||||
f"headless 模式下未登录,且无法手工登录"
|
||||
f"(请用 --show-browser 重试)"
|
||||
)
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
|
||||
# 5. 登录完成 → 等 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. 等 SPA 渲染 + click 侧边栏 hr/contracts(SPA 路由,避免 goto 触发整页 reload 丢 session)
|
||||
try:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"""
|
||||
|
||||
SKILL_SLUG = "alert-contract-expiring-hr"
|
||||
SKILL_VERSION = "1.1.19"
|
||||
SKILL_VERSION = "1.1.20"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
@@ -48,7 +48,10 @@ PERFORMANCE_RENEW_MIN = 80 # 全部 >= 80 且逐年上升 → renew
|
||||
# ----- v1.1.6:RPA 超时常量(与 skill-template human_verification.py 对齐)-----
|
||||
# 手工登录等待(HITL fallback)
|
||||
HUMAN_WAIT_TIMEOUT = 180 # 手工登录等待超时(秒)—— skill-template 同名常量
|
||||
HUMAN_POLL_INTERVAL = 2.0 # 轮询间隔(秒)—— skill-template 同名常量
|
||||
HUMAN_POLL_INTERVAL = 5.0 # 轮询间隔(秒)—— v1.1.19 调长:避免打断用户输入
|
||||
HUMAN_PROMPT_INTERVAL = 15.0 # 剩余时间提示间隔(秒)—— v1.1.19 新增
|
||||
URL_STABLE_REQUIRED_S = 2.0 # URL 连续 N 秒不变才算稳定 —— v1.1.19 新增
|
||||
URL_STABLE_POLL_S = 0.5 # URL 稳定探测间隔(秒)
|
||||
|
||||
# Playwright 操作超时(毫秒)
|
||||
DEFAULT_PAGE_TIMEOUT_MS = 15_000 # Playwright 默认操作超时 —— skill-template 同名常量
|
||||
|
||||
Reference in New Issue
Block a user