Compare commits

...

4 Commits

Author SHA1 Message Date
7584e69714 chore: auto release commit (2026-09-15 01:39:08)
Some checks failed
技能自动化发布 / release (push) Failing after 6s
2026-09-15 01:39:08 +08:00
d3fee3f46c chore: auto release commit (2026-09-15 01:30:18)
Some checks failed
技能自动化发布 / release (push) Failing after 4s
2026-09-15 01:30:18 +08:00
ebea77b5f2 chore: auto release commit (2026-09-15 01:27:04)
Some checks failed
技能自动化发布 / release (push) Failing after 4s
2026-09-15 01:27:05 +08:00
430274dbf9 chore: auto release commit (2026-09-15 01:23:04)
Some checks failed
技能自动化发布 / release (push) Failing after 4s
2026-09-15 01:23:04 +08:00
5 changed files with 396 additions and 68 deletions

View File

@@ -2,6 +2,39 @@
本文件对应技能市场 **「更新日志」** 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
- **按用户要求修正登录页等待逻辑**:

View File

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

View File

@@ -263,14 +263,17 @@ def _try_auto_sign_in(page, wait_input_sec: int = 2) -> bool:
def _wait_for_login_inputs_filled(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> bool:
"""v1.1.21:等用户输入完账号密码。
v1.1.24:**不再自动点 Sign in**——按用户要求:「等待用户输入信息后跳转页面」,
按钮交给用户自己点。脚本只负责等 URL 跳离登录页。
"""v1.1.21:等用户输入完账号密码,然后自动点登录按钮。
- 每 HUMAN_POLL_INTERVAL 秒检查 #jc-user / #jc-pass 是否有值(仅做状态日志)
- URL 已离开登录页 → return True(用户可能自己点登录完成)
- input 有值 → 提示「请点登录按钮」(**脚本不抢按钮**)
- 超时未跳离登录页 → 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()
@@ -286,21 +289,21 @@ def _wait_for_login_inputs_filled(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> b
)
return True
# 探测 input 是否就绪 + 是否已有值(仅做日志,不主动 click)
# 探测 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
else:
user_val = ""
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
else:
pass_val = ""
now = time.monotonic()
if now - last_state_log >= HUMAN_PROMPT_INTERVAL:
@@ -310,11 +313,34 @@ def _wait_for_login_inputs_filled(page, wait_sec: int = HUMAN_WAIT_TIMEOUT) -> b
bool(user_val), bool(pass_val), cur_url,
)
# 提醒用户:input 有值时明确告诉用户「点登录按钮」
# 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: ✅ 账号密码已填好(user=%r),自动点 %s 按钮",
user_val, btn_text_used,
)
btn.click()
return True
logger.warning("platform_collector: input 有值但找不到登录按钮")
# 提醒用户
if now - last_prompt_at >= HUMAN_PROMPT_INTERVAL:
last_prompt_at = now
remaining = int(deadline - now)
if user_val and pass_val:
# 上面 click 应该已经 return,不会到这里
print(
f"[登录] ✅ 账号密码已填好(user={user_val[:3]}***),"
f"请点「登录」按钮,剩余 {remaining}s..."
@@ -503,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
@@ -717,44 +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.24:sandbox 可能登录后把人踢回 SPA 二级登录页
# (/industries/campus/smartcampus/login)。如果 URL 落在
# 这个二级登录页,再走一次 _try_auto_sign_in(已经扩展支持
# 中文「登录」按钮);若 SPA 登录页 input 有预填账号 → 自动点;
# 若 input 空 → 等用户输入并**自己点登录**(脚本不抢按钮)。
# 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: 登录后仍落在登录页(%s),"
"v1.1.24 触发 SPA 二级登录兜底",
"platform_collector: 登录后落在 SPA 二级登录页(%s),"
"v1.1.28 触发 SPA 登录兜底(等 input hydrate + fill demo/123456)",
page.url,
)
spa_auto = _try_auto_sign_in(page, wait_input_sec=2)
if spa_auto:
# 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 二级登录页已自动登录,"
"等 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)
# 等 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:
raise LoginTimeoutError(
f"SPA 二级登录页 input 空且 headless 无法手工登录(URL={page.url})"
# 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 找不到
@@ -1220,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:

View File

@@ -6,7 +6,7 @@
"""
SKILL_SLUG = "alert-contract-expiring-hr"
SKILL_VERSION = "1.1.25"
SKILL_VERSION = "1.1.29"
LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr"
PLATFORM_KIT_MIN_VERSION = "1.2.11"

View File

@@ -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 ----------
@@ -1408,6 +1482,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 的路由器。
@@ -1552,51 +1630,97 @@ def test_try_auto_sign_in_selector_timeout_returns_false() -> tuple[bool, str]:
return True, "无登录 input 超时返回 False OK"
def test_wait_for_login_inputs_filled_no_auto_click_v1_24() -> tuple[bool, str]:
"""v1.1.24:`_wait_for_login_inputs_filled` **不再自动点登录按钮**——
用户输入完成(input 有值)后等 URL 跳离登录页,**按钮交给用户自己点**。
"""
import threading
import time as _time
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
# 缩短轮询周期:HUMAN_POLL_INTERVAL 默认 5s,太长测试慢
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:
page = _FakePage(
"https://sandbox.jc2009.com/industries/campus/smartcampus/login",
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),
},
)
# 0.6s 后 URL 跳到 /app/(模拟用户点登录按钮后跳转)
def fake_user_jump():
_time.sleep(0.6)
page.url = "https://sandbox.jc2009.com/industries/campus/smartcampus/app/home"
threading.Thread(target=fake_user_jump, daemon=True).start()
result = pc._wait_for_login_inputs_filled(page, wait_sec=5)
if not result:
return False, "URL 已跳到 /app/ 应返回 True"
return False, "input 有值应自动点登录 → return True"
if page.goto_calls:
return False, f"等待期间不应 goto: {page.goto_calls}"
if page.reload_calls:
return False, f"等待期间不应 reload: {page.reload_calls}"
if page.click_calls:
return False, (
f"v1.1.24 修复:等用户输入期间**不应自动点登录**按钮,"
f"但 click_calls={page.click_calls}"
)
return True, "v1.1.24 等用户跳离登录页(不抢按钮)OK"
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 ----------
@@ -1866,6 +1990,7 @@ 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)
@@ -1874,7 +1999,10 @@ def main() -> int:
_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_no_auto_click_v1_24", test_wait_for_login_inputs_filled_no_auto_click_v1_24)
_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)