Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7584e69714 | |||
| d3fee3f46c | |||
| ebea77b5f2 | |||
| 430274dbf9 | |||
| 0157fde0f1 | |||
| aa9bf262fe |
49
CHANGELOG.md
49
CHANGELOG.md
@@ -2,6 +2,55 @@
|
||||
|
||||
本文件对应技能市场 **「更新日志」** Tab。
|
||||
|
||||
## 1.1.29
|
||||
|
||||
- **修复「老 DB 没 download_tasks 表导致入队失败」问题**:
|
||||
- 用户本地 `data/alert-contract-expiring-hr.db` 是 v1.1.17 之前 init 的(只有前 6 张表),没跑过 v1.1.18 的 init-db → 没 `download_tasks` 表
|
||||
- `_enqueue_download_tasks_safe` 在 `owns_conn=True` 路径下,先 idempotent `CREATE TABLE IF NOT EXISTS download_tasks ...` + 2 索引,再 INSERT
|
||||
- 解决 `OperationalError: no such table: download_tasks` + 下载队列 `failed=3`
|
||||
- 新增测试 `enqueue_safe_auto_create_table_v1_29` 模拟「老 DB 没建过 download_tasks」场景(用 monkeypatch `rp.get_db_path` + `db.connection.get_db_path`),合计 67/67
|
||||
|
||||
## 1.1.28
|
||||
|
||||
- **彻底修复「SPA 二级登录页卡顿」**:
|
||||
- v1.1.27 失败根因:URL 已稳定在 `/login` 时,React SPA input 可能还没渲染(hydrate 慢),`_fill_demo_on_spa_login` 找不到 input → False → 卡 180 秒等用户输入
|
||||
- 修复:步骤 4.5 先 `wait_for_selector(input, timeout=10s, state="attached")` 等 input 真正 hydrate + 0.5s React 稳定,再 fill demo/123456 + 点登录
|
||||
- demo fill 后如果 URL 仍卡在 login 页(demo 失效)→ headed 模式 fallback 等用户手工登录,headless 模式抛 `LoginTimeoutError`
|
||||
- 不再依赖「input 是否预填」—— sandbox SPA 二级登录页 input 默认不预填,必须强制覆盖 fill
|
||||
- 合计 66/66
|
||||
|
||||
## 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 整个页面,有账号和密码可用直接点登录跳转」
|
||||
- `_wait_for_login_inputs_filled` 改回 v1.1.21 行为:input 都有值 → 自动点 Sign in / 登录按钮 → return True
|
||||
- 与 `_try_auto_sign_in` 行为统一:登录页 = 「有数据自动点」/ 「无数据等用户输入」
|
||||
- 测试替换:`wait_login_inputs_filled_no_auto_click_v1_24` → `wait_login_inputs_filled_auto_click_v1_26`,合计 63/63
|
||||
|
||||
## 1.1.25
|
||||
|
||||
- **按用户要求修正登录页等待逻辑**:
|
||||
- 有数据(账号密码已存在)→ 自动点登录按钮(`_try_auto_sign_in`)
|
||||
- **无数据 → 等待用户输入完成(最多 180 秒)→ 等 URL 跳离登录页**(按钮交给用户自己点,脚本不抢按钮)
|
||||
- `_wait_for_login_inputs_filled` 重写为「**等待模式**」:不再自动点 Sign in / 登录按钮;只轮询 input 值 + URL,每 15 秒提示「请点登录按钮」或「等待输入」
|
||||
- 根域登录 + SPA 二级登录页统一使用同一套行为
|
||||
- 新增测试 `wait_login_inputs_filled_no_auto_click_v1_24` 验证不抢按钮,合计 63/63
|
||||
|
||||
## 1.1.24
|
||||
|
||||
- **修复 `/industries/campus/smartcampus/login` 等待过久问题**:sandbox 在用户首次登录后可能把人踢回 SPA 二级登录页(按钮中文「登录」),之前根域自动登录代码不识别这个 URL + 中文按钮,导致脚本卡死等用户输入
|
||||
- `_try_auto_sign_in` 扩展为通用版本:支持 `#jc-user` / `input[type='tel']` / `input[autocomplete='username']` 多种 user input selector;按钮文本按优先级匹配 `Sign in` > `登录` > `Login` > `signin` > `submit`
|
||||
- `collect_all` 登录后新增步骤 4.5:等 URL 稳定到 `/app/` 或 `/select` 后,若发现 URL 仍含 `/login` / `/signin`(落在 SPA 二级登录页),自动重走一次 `_try_auto_sign_in` → 再等 URL 跳到 `/app/` 或 `/select`
|
||||
- 新增 4 个测试覆盖 `_try_auto_sign_in`(SPA 中文按钮 / 根域英文按钮 / input 空 / selector 超时),合计 58/58
|
||||
|
||||
## 1.1.23
|
||||
|
||||
- **修复登录后跳 /app/home 的流程**:
|
||||
|
||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 合同到期智能预警与续聘辅助
|
||||
description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。"
|
||||
version: 1.1.23
|
||||
version: 1.1.29
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
|
||||
@@ -180,19 +180,27 @@ def _is_spa_routed(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 直接点。
|
||||
v1.1.24:扩展到支持 SPA 二级登录页(/industries/campus/smartcampus/login),
|
||||
按钮文本可能是中文「登录」而非英文「Sign in」。
|
||||
|
||||
适用场景:
|
||||
- 根域登录页(https://sandbox.jc2009.com/):按钮「Sign in」
|
||||
- SPA 二级登录页(/industries/campus/smartcampus/login):按钮「登录」
|
||||
默认有 demo/123456(或 18677308191 等真实账号)预填在 input。
|
||||
|
||||
行为:
|
||||
1. 等 #jc-user 出现,最长 wait_input_sec 秒(默认 2s)
|
||||
2. 检查 #jc-user 和 #jc-pass 是否都有值
|
||||
3. 有值 → 点 Sign in → return True
|
||||
4. 无值 → return False(交给 _wait_for_manual_login 接管)
|
||||
1. 等 input 出现,最长 wait_input_sec 秒(默认 2s)
|
||||
2. 检查 input 是否都有值
|
||||
3. 有值 → 点 Sign in / 登录 → return True
|
||||
4. 无值 → return False(交给 _wait_for_login_inputs_filled 接管)
|
||||
|
||||
不会自动 fill(如果没值就放弃,让用户自己输)。
|
||||
"""
|
||||
try:
|
||||
page.wait_for_selector("#jc-user, #jc-pass", timeout=wait_input_sec * 1000)
|
||||
page.wait_for_selector(
|
||||
"#jc-user, #jc-pass, input[type='tel'], input[type='password']",
|
||||
timeout=wait_input_sec * 1000,
|
||||
)
|
||||
except Exception:
|
||||
logger.info(
|
||||
"platform_collector: %ds 内未出现登录 input,跳过自动登录",
|
||||
@@ -200,9 +208,23 @@ def _try_auto_sign_in(page, wait_input_sec: int = 2) -> bool:
|
||||
)
|
||||
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:
|
||||
# 多种 input selector 优先级
|
||||
user_input = None
|
||||
for sel in ("#jc-user", "input[type='tel']", "input[autocomplete='username']"):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
user_input = loc
|
||||
break
|
||||
if user_input is None:
|
||||
return False
|
||||
|
||||
pass_input = None
|
||||
for sel in ("#jc-pass", "input[type='password']", "input[autocomplete='current-password']"):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
pass_input = loc
|
||||
break
|
||||
if pass_input is None:
|
||||
return False
|
||||
|
||||
user_val = (user_input.input_value() or "").strip()
|
||||
@@ -216,24 +238,42 @@ def _try_auto_sign_in(page, wait_input_sec: int = 2) -> bool:
|
||||
)
|
||||
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 按钮")
|
||||
# 多种按钮文本优先级:英文 Sign in > 中文登录 > submit
|
||||
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: 未找到登录按钮")
|
||||
return False
|
||||
|
||||
logger.info(
|
||||
"platform_collector: 登录页已预填账号(user=%r),自动点 %s 按钮",
|
||||
user_val, btn_text_used,
|
||||
)
|
||||
btn.click()
|
||||
return True
|
||||
|
||||
|
||||
def _wait_for_login_inputs_filled(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> bool:
|
||||
"""v1.1.21:等用户输入完账号密码,然后自动点 Sign in。
|
||||
"""v1.1.21:等用户输入完账号密码,然后自动点登录按钮。
|
||||
|
||||
- 每 HUMAN_POLL_INTERVAL 秒检查 #jc-user / #jc-pass 是否有值
|
||||
- 两个都有值 → 自动点 Sign in → return True
|
||||
- 超时未填完 → return False
|
||||
v1.1.26:按用户最新要求改回**自动点登录**行为——
|
||||
「到了 /industries/campus/smartcampus/login 整个页面,有账号和密码可用直接点登录跳转」,
|
||||
不再等用户自己点。
|
||||
|
||||
优先级(任一 selector 命中即视为有 input):
|
||||
- user: `#jc-user` / `input[type='tel']` / `input[autocomplete='username']`
|
||||
- pass: `#jc-pass` / `input[type='password']` / `input[autocomplete='current-password']`
|
||||
|
||||
按钮文本优先级:`Sign in` / `登录` / `Login` / `signin` / `submit`。
|
||||
"""
|
||||
deadline = time.monotonic() + wait_sec
|
||||
last_prompt_at = time.monotonic()
|
||||
@@ -241,47 +281,79 @@ def _wait_for_login_inputs_filled(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> b
|
||||
|
||||
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,
|
||||
"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
|
||||
# 探测 input(多 selector 优先级)
|
||||
user_val, pass_val = "", ""
|
||||
user_loc, pass_loc = None, None
|
||||
for sel in ("#jc-user", "input[type='tel']", "input[autocomplete='username']"):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
user_loc = loc
|
||||
user_val = (loc.input_value() or "").strip()
|
||||
break
|
||||
for sel in ("#jc-pass", "input[type='password']", "input[autocomplete='current-password']"):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
pass_loc = loc
|
||||
pass_val = (loc.input_value() or "").strip()
|
||||
break
|
||||
|
||||
now = time.monotonic()
|
||||
if now - last_state_log >= HUMAN_PROMPT_INTERVAL:
|
||||
last_state_log = now
|
||||
logger.info(
|
||||
"platform_collector: 等待用户输入 user=%r pass=%r (URL=%s)",
|
||||
bool(user_val), bool(pass_val), cur_url,
|
||||
)
|
||||
|
||||
# input 都有值 → 自动点登录按钮(v1.1.26 关键:用户最新要求自动点)
|
||||
if user_val and pass_val:
|
||||
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 not None and btn.count() > 0:
|
||||
logger.info(
|
||||
"platform_collector: input 状态 user=%r pass=%r",
|
||||
bool(user_val), bool(pass_val),
|
||||
"platform_collector: ✅ 账号密码已填好(user=%r),自动点 %s 按钮",
|
||||
user_val, btn_text_used,
|
||||
)
|
||||
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
|
||||
btn.click()
|
||||
return True
|
||||
logger.warning("platform_collector: input 有值但找不到登录按钮")
|
||||
|
||||
# 提醒用户
|
||||
now = time.monotonic()
|
||||
if now - last_prompt_at >= HUMAN_PROMPT_INTERVAL:
|
||||
last_prompt_at = now
|
||||
remaining = int(deadline - now)
|
||||
print(f"[登录] 等待用户输入账号密码... 剩余 {remaining}s")
|
||||
if user_val and pass_val:
|
||||
# 上面 click 应该已经 return,不会到这里
|
||||
print(
|
||||
f"[登录] ✅ 账号密码已填好(user={user_val[:3]}***),"
|
||||
f"请点「登录」按钮,剩余 {remaining}s..."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[登录] 等待用户输入账号密码... 剩余 {remaining}s"
|
||||
)
|
||||
|
||||
time.sleep(HUMAN_POLL_INTERVAL)
|
||||
|
||||
logger.warning(
|
||||
"platform_collector: %ds 内用户未完成输入", wait_sec,
|
||||
"platform_collector: %ds 内用户未跳转(最后 URL=%s)", wait_sec, page.url,
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -457,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
|
||||
@@ -647,22 +789,22 @@ def collect_all(
|
||||
else:
|
||||
# 2. 等 #jc-user / #jc-pass 出现(最多 2 秒)
|
||||
# 若 sandbox 已预填 demo/123456 → 自动点 Sign in
|
||||
# 若 input 为空 → 等用户输入完成后再自动点 Sign in
|
||||
# 若 input 为空 → 等用户输入(**v1.1.24 用户自己点登录按钮**)
|
||||
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
|
||||
# 3. input 为空:等用户输入 → **用户自己点 Sign in**(脚本不抢按钮)
|
||||
if _wait_for_login_inputs_filled(page, wait_sec=human_wait_timeout):
|
||||
logger.info(
|
||||
"platform_collector: ✅ 用户输入完成并点击,等 URL 跳到 /app/"
|
||||
"platform_collector: ✅ 用户完成输入并跳转,等 URL 跳到 /app/"
|
||||
)
|
||||
else:
|
||||
# 超时未输入 → fallback 到原来的多信号等待
|
||||
# 超时未跳转 → fallback 到原来的多信号等待
|
||||
logger.warning(
|
||||
"platform_collector: 用户未输入完成,fallback 到 _wait_for_manual_login"
|
||||
"platform_collector: 用户未跳转,fallback 到 _wait_for_manual_login"
|
||||
)
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
@@ -671,15 +813,86 @@ 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.28:URL 落在 SPA 二级登录页 → 主动等 input hydrate(最多 10 秒)
|
||||
# → 主动 fill demo/123456(sandbox SPA 默认 mock)→ 点登录 → 等 URL 跳离
|
||||
# v1.1.27 失败的根因:URL 已稳定在 /login 时,React SPA input 可能还没渲染,
|
||||
# _fill_demo_on_spa_login 找不到 input → False → 卡 180 秒等用户输入。
|
||||
# 修复:先等 input 真正 hydrate 出来,再 fill+click。
|
||||
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: 登录后落在 SPA 二级登录页(%s),"
|
||||
"v1.1.28 触发 SPA 登录兜底(等 input hydrate + fill demo/123456)",
|
||||
page.url,
|
||||
)
|
||||
# 1. 等 input 真正 hydrate 出来(最多 10 秒)
|
||||
try:
|
||||
page.wait_for_selector(
|
||||
"#jc-user, #jc-pass, input[type='tel'], input[type='password']",
|
||||
timeout=10_000,
|
||||
state="attached",
|
||||
)
|
||||
page.wait_for_timeout(500) # 让 React 状态稳定
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"platform_collector: SPA 登录页 10s 内未渲染 input: %s", e,
|
||||
)
|
||||
|
||||
# 2. fill demo/123456 + 点登录(不管当前 input 有没有值,强制覆盖)
|
||||
spa_filled = _fill_demo_on_spa_login(page)
|
||||
if spa_filled:
|
||||
logger.info(
|
||||
"platform_collector: ✅ SPA 二级登录页 demo/123456 自动填好,"
|
||||
"已点登录,等 URL 跳到 /app/ 或 /select"
|
||||
)
|
||||
# 等 URL 跳离 login(30 秒内跳到 /app/ 或 /select 都算成功)
|
||||
if not _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30):
|
||||
logger.warning(
|
||||
"platform_collector: SPA 登录后 30s 内 URL 未稳定到 /app/或/select"
|
||||
"(当前 URL=%s,demo 可能失效需手工登录)",
|
||||
page.url,
|
||||
)
|
||||
# demo 失效:等用户手工登录(headless 模式直接抛错)
|
||||
if not headless:
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
raise LoginTimeoutError(
|
||||
f"SPA 二级登录页 demo/123456 填了但 URL 不跳离"
|
||||
f"(可能 demo 失效或需人工登录,URL={page.url})"
|
||||
)
|
||||
else:
|
||||
# fill 失败(input 仍未渲染)→ 等用户手工输入
|
||||
logger.warning(
|
||||
"platform_collector: SPA 登录页 _fill_demo 失败"
|
||||
"(input 仍未渲染),fallback 等用户输入"
|
||||
)
|
||||
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 找不到
|
||||
@@ -1145,6 +1358,35 @@ def _enqueue_download_tasks_safe(
|
||||
owns_conn = conn is None
|
||||
if owns_conn:
|
||||
conn = get_connection()
|
||||
# v1.1.29:idempotent 表创建 —— 用户没跑 init-db 时,download_tasks 表不存在,
|
||||
# 首次采集直接建表(CREATE TABLE IF NOT EXISTS)而不是报 "no such table"
|
||||
try:
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS download_tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
contract_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL DEFAULT 'platform_button',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
error_msg TEXT,
|
||||
enqueued_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
finished_at TEXT,
|
||||
source_path TEXT,
|
||||
result_path TEXT,
|
||||
FOREIGN KEY (contract_id) REFERENCES contracts(contract_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_dlq_status ON download_tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_dlq_contract ON download_tasks(contract_id);
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as ensure_err:
|
||||
logger.warning(
|
||||
"platform_collector: ensure download_tasks 表创建失败(继续走入队流程): %s",
|
||||
ensure_err,
|
||||
)
|
||||
try:
|
||||
for cid in contract_ids:
|
||||
try:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"""
|
||||
|
||||
SKILL_SLUG = "alert-contract-expiring-hr"
|
||||
SKILL_VERSION = "1.1.23"
|
||||
SKILL_VERSION = "1.1.29"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
|
||||
@@ -1271,6 +1271,80 @@ def test_enqueue_download_tasks_safe_empty_list() -> tuple[bool, str]:
|
||||
return True, "空列表 no-op OK"
|
||||
|
||||
|
||||
def test_enqueue_download_tasks_safe_auto_create_table_v1_29() -> tuple[bool, str]:
|
||||
"""v1.1.29:用户没跑 init-db 时,首次调用 `_enqueue_download_tasks_safe`
|
||||
不传 conn(owns_conn=True),函数内部 idempotent 创建 download_tasks 表
|
||||
而非抛 "no such table"。
|
||||
"""
|
||||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from service.platform_collector import _enqueue_download_tasks_safe
|
||||
|
||||
# 1. 临时文件 DB,**只建 contracts 表**(模拟老 DB 没 download_tasks)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_db = Path(tmp) / "old.db"
|
||||
# 重定向 JIANGCHANG_DATA_ROOT 或 get_db_path 用临时路径
|
||||
# 这里用 monkeypatch get_db_path + get_connection
|
||||
from util import runtime_paths as rp
|
||||
from db import connection as dbconn
|
||||
|
||||
orig_rp_db_path = rp.get_db_path
|
||||
orig_dbconn_db_path = dbconn.get_db_path
|
||||
rp.get_db_path = lambda: tmp_db # type: ignore[assignment]
|
||||
dbconn.get_db_path = lambda: tmp_db # type: ignore[assignment]
|
||||
try:
|
||||
# 模拟老 DB:只有 contracts 表(v1.1.17 之前 init 过)
|
||||
_tmp_conn = sqlite3.connect(str(tmp_db))
|
||||
try:
|
||||
_tmp_conn.executescript(
|
||||
"""
|
||||
CREATE TABLE contracts (
|
||||
contract_id TEXT PRIMARY KEY,
|
||||
teacher_name TEXT, unit_id TEXT, unit_name TEXT,
|
||||
position TEXT, contract_type TEXT, contract_start TEXT,
|
||||
contract_end TEXT, trial_period_end TEXT,
|
||||
status TEXT DEFAULT '可续聘',
|
||||
last_material_id TEXT, renewed_at TEXT
|
||||
);
|
||||
INSERT INTO contracts
|
||||
(contract_id, teacher_name, unit_id, unit_name,
|
||||
position, contract_type, contract_start,
|
||||
contract_end, trial_period_end)
|
||||
VALUES ('ENQ-CREATE-1', '测试', 'U1', '物理学院',
|
||||
'实验师', '专任教师聘用合同',
|
||||
'2024-01-01', '2026-12-31', '2024-04-01');
|
||||
"""
|
||||
)
|
||||
_tmp_conn.commit()
|
||||
finally:
|
||||
_tmp_conn.close()
|
||||
|
||||
# 2. 不传 conn 调用(owns_conn=True,会走 get_connection → 触发 ensure 建表)
|
||||
summary = _enqueue_download_tasks_safe(["ENQ-CREATE-1"])
|
||||
if summary.get("failed", 0) > 0:
|
||||
return False, f"应自动建表后入队成功,但 failed={summary}"
|
||||
|
||||
# 3. 验证表确实被自动创建了
|
||||
conn = sqlite3.connect(str(tmp_db))
|
||||
try:
|
||||
count = conn.execute(
|
||||
"SELECT COUNT(*) FROM download_tasks"
|
||||
).fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if count != 1:
|
||||
return False, f"download_tasks 应有 1 行,实际 {count}"
|
||||
|
||||
return True, f"ensure 建表 + 入队 OK(enqueued={summary.get('enqueued')})"
|
||||
finally:
|
||||
rp.get_db_path = orig_rp_db_path # type: ignore[assignment]
|
||||
dbconn.get_db_path = orig_dbconn_db_path # type: ignore[assignment]
|
||||
|
||||
|
||||
# ---------- v1.1.19: _wait_for_url_stable + login flow ----------
|
||||
|
||||
|
||||
@@ -1279,6 +1353,8 @@ class _FakePage:
|
||||
|
||||
- .url: 字符串
|
||||
- 支持外部修改 .url 模拟用户登录跳转
|
||||
- v1.1.24:新增 .locator() 返回 _FakeLocatorNoOp,让
|
||||
_wait_for_login_inputs_filled 的 selector 探测不抛 AttributeError
|
||||
"""
|
||||
|
||||
def __init__(self, initial_url: str = "about:blank") -> None:
|
||||
@@ -1287,6 +1363,10 @@ class _FakePage:
|
||||
self.reload_calls: int = 0
|
||||
self.click_calls: int = 0
|
||||
|
||||
def locator(self, selector: str): # noqa: ARG002
|
||||
"""v1.1.24 扩展:测试 _wait_for_login_inputs_filled 用,返回空 locator。"""
|
||||
return _FakeLocator(count=0)
|
||||
|
||||
|
||||
def test_wait_for_url_stable_immediate_match() -> tuple[bool, str]:
|
||||
"""_wait_for_url_stable: URL 已含目标 + 稳定 → 立即返回 True。"""
|
||||
@@ -1366,6 +1446,281 @@ def test_wait_for_manual_login_no_goto() -> tuple[bool, str]:
|
||||
pc.URL_STABLE_REQUIRED_S = orig_stable
|
||||
|
||||
|
||||
# ---------- v1.1.24: _try_auto_sign_in SPA 二级登录页(中文「登录」按钮)----------
|
||||
|
||||
|
||||
class _FakeLocator:
|
||||
"""v1.1.24 _try_auto_sign_in 测试用的简易 Playwright Locator mock。
|
||||
|
||||
支持的 API:
|
||||
- .count() -> int
|
||||
- .input_value() -> str
|
||||
- .click() -> None
|
||||
- .first -> 自身
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
count: int = 0,
|
||||
value: str = "",
|
||||
click_log: list | None = None,
|
||||
) -> None:
|
||||
self._count = count
|
||||
self._value = value
|
||||
self._click_log = click_log if click_log is not None else []
|
||||
|
||||
@property
|
||||
def first(self) -> "_FakeLocator":
|
||||
return self
|
||||
|
||||
def count(self) -> int:
|
||||
return self._count
|
||||
|
||||
def input_value(self) -> str:
|
||||
return self._value
|
||||
|
||||
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 的路由器。
|
||||
|
||||
selectors: dict[selector_str, _FakeLocator]
|
||||
"""
|
||||
|
||||
def __init__(self, selectors: dict[str, _FakeLocator], page) -> None:
|
||||
self._selectors = selectors
|
||||
self._page = page
|
||||
|
||||
def __call__(self, sel: str):
|
||||
"""page.locator(sel) 形式调用。"""
|
||||
if sel in self._selectors:
|
||||
return self._selectors[sel]
|
||||
return _FakeLocator(count=0)
|
||||
|
||||
def __getattr__(self, sel: str):
|
||||
# loc.page 已存在则按 selector 路由
|
||||
if sel in self._selectors:
|
||||
return self._selectors[sel]
|
||||
# fallback:未注册的 selector 都返回空 locator
|
||||
return _FakeLocator(count=0)
|
||||
|
||||
|
||||
class _FakePlaywrightPage:
|
||||
"""v1.1.24 _try_auto_sign_in 测试用的 page mock。
|
||||
|
||||
usage:
|
||||
page = _FakePlaywrightPage(
|
||||
url="https://sandbox.jc2009.com/industries/campus/smartcampus/login",
|
||||
selectors={
|
||||
"input[type='tel']": _FakeLocator(count=1, value="18677308191"),
|
||||
"input[type='password']": _FakeLocator(count=1, value="***"),
|
||||
"button:has-text('登录')": _FakeLocator(count=1, click_log=click_log),
|
||||
},
|
||||
wait_for_selector_log=wait_log,
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str = "about:blank",
|
||||
selectors: dict[str, _FakeLocator] | None = None,
|
||||
wait_for_selector_log: list | None = None,
|
||||
wait_selector_should_raise: bool = False,
|
||||
) -> None:
|
||||
self.url = url
|
||||
self._selectors = selectors or {}
|
||||
self._wait_log = wait_for_selector_log if wait_for_selector_log is not None else []
|
||||
self._wait_should_raise = wait_selector_should_raise
|
||||
|
||||
def wait_for_selector(self, selector: str, timeout: int = 0) -> None:
|
||||
self._wait_log.append(selector)
|
||||
if self._wait_should_raise:
|
||||
raise Exception("fake: timeout")
|
||||
|
||||
@property
|
||||
def locator(self):
|
||||
return _FakeLocatorRouter(self._selectors, self)
|
||||
|
||||
|
||||
def test_try_auto_sign_in_spa_login_chinese_button() -> tuple[bool, str]:
|
||||
"""v1.1.24:SPA 二级登录页(/industries/campus/smartcampus/login),
|
||||
input 用 `input[type='tel']` / `input[type='password']`,
|
||||
按钮中文「登录」。预填有值 → 自动点登录。
|
||||
"""
|
||||
from service.platform_collector import _try_auto_sign_in
|
||||
|
||||
click_log: list[str] = []
|
||||
page = _FakePlaywrightPage(
|
||||
url="https://sandbox.jc2009.com/industries/campus/smartcampus/login",
|
||||
selectors={
|
||||
"input[type='tel']": _FakeLocator(count=1, value="18677308191"),
|
||||
"input[type='password']": _FakeLocator(count=1, value="dummy"),
|
||||
"button:has-text('登录')": _FakeLocator(count=1, click_log=click_log),
|
||||
},
|
||||
)
|
||||
if not _try_auto_sign_in(page, wait_input_sec=2):
|
||||
return False, "SPA 二级登录页预填应自动点 登录"
|
||||
if not click_log:
|
||||
return False, f"未触发点击: {click_log}"
|
||||
return True, f"SPA 二级登录页中文按钮自动点 OK(click_log={click_log})"
|
||||
|
||||
|
||||
def test_try_auto_sign_in_root_sign_in_english_button() -> tuple[bool, str]:
|
||||
"""v1.1.24(向后兼容):根域登录页(https://sandbox.jc2009.com/),
|
||||
input 用 `#jc-user` / `#jc-pass`,按钮英文「Sign in」。预填有值 → 自动点。
|
||||
"""
|
||||
from service.platform_collector import _try_auto_sign_in
|
||||
|
||||
click_log: list[str] = []
|
||||
page = _FakePlaywrightPage(
|
||||
url="https://sandbox.jc2009.com/",
|
||||
selectors={
|
||||
"#jc-user": _FakeLocator(count=1, value="demo"),
|
||||
"#jc-pass": _FakeLocator(count=1, value="123456"),
|
||||
"button:has-text('Sign in')": _FakeLocator(count=1, click_log=click_log),
|
||||
},
|
||||
)
|
||||
if not _try_auto_sign_in(page, wait_input_sec=2):
|
||||
return False, "根域登录页预填应自动点 Sign in"
|
||||
if not click_log:
|
||||
return False, f"未触发点击: {click_log}"
|
||||
return True, f"根域登录页 Sign in 自动点 OK(click_log={click_log})"
|
||||
|
||||
|
||||
def test_try_auto_sign_in_no_input_returns_false() -> tuple[bool, str]:
|
||||
"""v1.1.24:input 为空(用户还没输入)→ 返回 False,不点按钮。"""
|
||||
from service.platform_collector import _try_auto_sign_in
|
||||
|
||||
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="123"),
|
||||
"button:has-text('登录')": _FakeLocator(count=1, click_log=click_log),
|
||||
},
|
||||
)
|
||||
if _try_auto_sign_in(page, wait_input_sec=2):
|
||||
return False, "user 空时不应自动点"
|
||||
if click_log:
|
||||
return False, f"未填完不应点击: {click_log}"
|
||||
return True, "input 空返回 False 不点 OK"
|
||||
|
||||
|
||||
def test_try_auto_sign_in_selector_timeout_returns_false() -> tuple[bool, str]:
|
||||
"""v1.1.24:等 input 超时(如 URL 已是 SPA home,无登录 input)→ 返回 False。"""
|
||||
from service.platform_collector import _try_auto_sign_in
|
||||
|
||||
click_log: list[str] = []
|
||||
page = _FakePlaywrightPage(
|
||||
url="https://sandbox.jc2009.com/industries/campus/smartcampus/app/home",
|
||||
selectors={}, # 没有任何 input
|
||||
wait_selector_should_raise=True,
|
||||
)
|
||||
if _try_auto_sign_in(page, wait_input_sec=1):
|
||||
return False, "无登录 input 应返回 False"
|
||||
if click_log:
|
||||
return False, f"无 input 不应点击: {click_log}"
|
||||
return True, "无登录 input 超时返回 False OK"
|
||||
|
||||
|
||||
def test_wait_for_login_inputs_filled_auto_click_v1_26() -> tuple[bool, str]:
|
||||
"""v1.1.26:`_wait_for_login_inputs_filled` **自动点登录按钮**——
|
||||
按用户最新要求:「到了 /industries/campus/smartcampus/login 整个页面,
|
||||
有账号和密码可用直接点登录跳转」。
|
||||
|
||||
input 有值时点击 `button:has-text('登录')` 或 `Sign in`(多按钮文本优先级)。
|
||||
"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
orig_poll = pc.HUMAN_POLL_INTERVAL
|
||||
orig_prompt = pc.HUMAN_PROMPT_INTERVAL
|
||||
pc.HUMAN_POLL_INTERVAL = 0.2
|
||||
pc.HUMAN_PROMPT_INTERVAL = 0.2
|
||||
try:
|
||||
click_log: list[str] = []
|
||||
page = _FakePlaywrightPage(
|
||||
url="https://sandbox.jc2009.com/industries/campus/smartcampus/login",
|
||||
selectors={
|
||||
"input[type='tel']": _FakeLocator(count=1, value="18677308191"),
|
||||
"input[type='password']": _FakeLocator(count=1, value="dummy"),
|
||||
"button:has-text('登录')": _FakeLocator(count=1, click_log=click_log),
|
||||
},
|
||||
)
|
||||
|
||||
result = pc._wait_for_login_inputs_filled(page, wait_sec=5)
|
||||
if not result:
|
||||
return False, "input 有值应自动点登录 → return True"
|
||||
|
||||
if not click_log:
|
||||
return False, f"v1.1.26 应自动点登录按钮,但 click_log={click_log}"
|
||||
return True, f"v1.1.26 自动点登录按钮 OK(click_log={click_log})"
|
||||
finally:
|
||||
pc.HUMAN_POLL_INTERVAL = orig_poll
|
||||
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 ----------
|
||||
|
||||
|
||||
@@ -1635,10 +1990,19 @@ def main() -> int:
|
||||
_run("download_tasks/enqueue_safe_idempotent", test_enqueue_download_tasks_safe_idempotent)
|
||||
_run("download_tasks/enqueue_safe_fk_failure", test_enqueue_download_tasks_safe_fk_failure)
|
||||
_run("download_tasks/enqueue_safe_empty_list", test_enqueue_download_tasks_safe_empty_list)
|
||||
_run("download_tasks/enqueue_safe_auto_create_table_v1_29", test_enqueue_download_tasks_safe_auto_create_table_v1_29)
|
||||
_run("login_flow/wait_url_stable_immediate", test_wait_for_url_stable_immediate_match)
|
||||
_run("login_flow/wait_url_stable_transition", test_wait_for_url_stable_transition)
|
||||
_run("login_flow/wait_url_stable_timeout", test_wait_for_url_stable_timeout)
|
||||
_run("login_flow/wait_manual_login_no_goto", test_wait_for_manual_login_no_goto)
|
||||
_run("login_flow/auto_sign_in_spa_chinese_button", test_try_auto_sign_in_spa_login_chinese_button)
|
||||
_run("login_flow/auto_sign_in_root_english_button", test_try_auto_sign_in_root_sign_in_english_button)
|
||||
_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)
|
||||
|
||||
Reference in New Issue
Block a user