v1.1.6: 按 skill-template human_verification 设计模式重构 RPA
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
按 skill-template examples/real_browser_rpa 设计模式重构 platform_collector,
把 v1.1.5 修复的硬编码等待时间统一抽到 constants.py,并补上手工登录 fallback
和结构化错误码。
核心改动:
- constants.py 新增 24 个 RPA 常量(HUMAN_WAIT_TIMEOUT=180 / HUMAN_POLL_INTERVAL=2.0 /
DEFAULT_PAGE_TIMEOUT_MS=15s / GOTO_TIMEOUT_MS=60s / SPA_LOGIN_* / TABLE_* /
SIDEBAR_LINK_TIMEOUT_MS 等),与 skill-template human_verification 同名常量对齐
- platform_collector 重构:
* 新增 RPAError / LoginTimeoutError (ERROR:LOGIN_TIMEOUT) /
MissingBrowserError (ERROR:MISSING_BROWSER) / TableNotFoundError (ERROR:TABLE_NOT_FOUND)
* 新增 _is_logged_in / _wait_for_manual_login(轮询 + 180s 兜底,与
skill-template task_rpa.py::_ensure_logged_in 对齐)
* _fill_spa_login 自动 fill 失败时 → 自动 fallback _wait_for_manual_login
(仅 --show-browser 模式;headless 模式抛 LoginTimeoutError 退出码 2)
* 所有 wait_for_timeout / wait_for_selector timeout 替换为常量
* collect_all() 加 human_wait_timeout 参数(CLI 透传)
* page.set_default_timeout(DEFAULT_PAGE_TIMEOUT_MS) 覆盖 Playwright 默认 15s
- app.py cmd_collect_platform:
* 捕获 RPAError → JSON 输出 {error: e.code, detail: ...} + exit 2
* 新增 --human-wait-timeout N flag
- requirements.txt:删除违规的 playwright>=1.40.0(playwright 由宿主 jiangchang_skill_core
runtime 提供,文件顶部注释已声明)
- .env.example:新增 HUMAN_WAIT_TIMEOUT / RPA_PAGE_TIMEOUT_MS / RPA_GOTO_TIMEOUT_MS
覆盖项
- tests/run_tests.py:新增 7 个 platform_collector 纯函数测试
(_gen_contract_id × 2 / _normalize_date × 2 / _parse_days × 2 / _parse_alert)
- SKILL.md version 1.1.5 → 1.1.6;CHANGELOG.md 加 1.1.6 条目
测试:28/28 通过(21 原有 + 7 新增)
POLICY-RELEASE-001:SKILL.md=1.1.6 == CHANGELOG=1.1.6 == SKILL_VERSION=1.1.6
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -558,6 +558,131 @@ def test_html_render_unknown_task_raises() -> tuple[bool, str]:
|
||||
return False, "应抛 ValueError,实际未抛"
|
||||
|
||||
|
||||
# ---------- v1.1.6:service.platform_collector 纯函数 ----------
|
||||
|
||||
def test_gen_contract_id_emp_id() -> tuple[bool, str]:
|
||||
"""_gen_contract_id 工号派生(v1.1.1 起用工号作主键)"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
cases = [
|
||||
(("T20190033", "2026-04-15"), "RPA-T20190033-20260415"),
|
||||
(("T20200055", "2026-06-30"), "RPA-T20200055-20260630"),
|
||||
(("T20180021", "2026-08-31"), "RPA-T20180021-20260831"),
|
||||
]
|
||||
for (emp_id, end_date), expected in cases:
|
||||
got = pc._gen_contract_id(emp_id, end_date)
|
||||
if got != expected:
|
||||
return False, f"({emp_id!r}, {end_date!r}) → {got!r},want {expected!r}"
|
||||
return True, f"3 工号派生正确"
|
||||
|
||||
|
||||
def test_gen_contract_id_special_chars() -> tuple[bool, str]:
|
||||
"""_gen_contract_id 工号含特殊字符时正确清洗(保留前 10 字符)"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
# 工号含非 \w 字符 → 清洗;超过 10 字符截断
|
||||
got = pc._gen_contract_id("T-2019.0033", "2026-04-15")
|
||||
# 清洗后保留 \w,"T-2019.0033" → "T20190033"(8 字符,未截断)
|
||||
if "RPA-" not in got or "20260415" not in got:
|
||||
return False, f"清洗后格式错:{got!r}"
|
||||
# 长工号 → 截断到 10 字符
|
||||
got2 = pc._gen_contract_id("ABCDEFGHIJKLMNO", "2026-01-01")
|
||||
# "ABCDEFGHIJ"(10 字符)
|
||||
if not got2.startswith("RPA-ABCDEFGHIJ-"):
|
||||
return False, f"长工号未截断到 10 字符:{got2!r}"
|
||||
return True, "清洗 + 截断 OK"
|
||||
|
||||
|
||||
def test_normalize_date_iso() -> tuple[bool, str]:
|
||||
"""_normalize_date ISO 日期直通"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
cases = [
|
||||
("2026-04-15", "2026-04-15"),
|
||||
("2024-01-01", "2024-01-01"),
|
||||
]
|
||||
for inp, want in cases:
|
||||
got = pc._normalize_date(inp)
|
||||
if got != want:
|
||||
return False, f"{inp!r} → {got!r},want {want!r}"
|
||||
return True, "ISO 日期直通"
|
||||
|
||||
|
||||
def test_normalize_date_fallback() -> tuple[bool, str]:
|
||||
"""_normalize_date 非标准格式截前 10 字符"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
# 非标准但带 ISO 前缀
|
||||
got = pc._normalize_date("2026-04-15T00:00:00")
|
||||
if got != "2026-04-15":
|
||||
return False, f"ISO 前缀应保留前 10 字符 → {got!r}"
|
||||
# 完全非日期 → 截前 10 字符
|
||||
got2 = pc._normalize_date("20260415X")
|
||||
if got2 != "20260415X": # 10 字符
|
||||
return False, f"非日期应截前 10 字符 → {got2!r}"
|
||||
# 带空白
|
||||
got3 = pc._normalize_date(" 2026-04-15 ")
|
||||
if got3 != "2026-04-15":
|
||||
return False, f"带空白应去除 → {got3!r}"
|
||||
return True, "非标准格式 fallback OK"
|
||||
|
||||
|
||||
def test_parse_days_positive() -> tuple[bool, str]:
|
||||
"""_parse_days 正天数"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
cases = [
|
||||
("30", 30),
|
||||
("90", 90),
|
||||
("还剩 45 天", 45),
|
||||
("0", 0),
|
||||
]
|
||||
for inp, want in cases:
|
||||
got = pc._parse_days(inp)
|
||||
if got != want:
|
||||
return False, f"{inp!r} → {got!r},want {want!r}"
|
||||
return True, "正天数解析 OK"
|
||||
|
||||
|
||||
def test_parse_days_negative() -> tuple[bool, str]:
|
||||
"""_parse_days 负数 / 兜底"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
# 含负号
|
||||
if pc._parse_days("-15") != -15:
|
||||
return False, "负天数未解析"
|
||||
# 已过期(带 "已过期" 文本)
|
||||
if pc._parse_days("已过期 7 天") != 7:
|
||||
return False, "已过期文本未解析数字部分"
|
||||
# 无法解析
|
||||
if pc._parse_days("") is not None:
|
||||
return False, "空字符串应返回 None"
|
||||
if pc._parse_days("无数字") is not None:
|
||||
return False, "纯文本应返回 None"
|
||||
return True, "负数 + 兜底 OK"
|
||||
|
||||
|
||||
def test_parse_alert_red_orange_yellow_unknown() -> tuple[bool, str]:
|
||||
"""_parse_alert 红/橙/黄/未知四级映射"""
|
||||
from service import platform_collector as pc
|
||||
|
||||
cases = [
|
||||
("红色预警", "red"),
|
||||
("红牌", "red"),
|
||||
("橙色预警", "orange"),
|
||||
("黄灯", "yellow"),
|
||||
("黄色预警", "yellow"),
|
||||
("无", "unknown"),
|
||||
("", "unknown"),
|
||||
("blue", "unknown"),
|
||||
]
|
||||
for inp, want in cases:
|
||||
got = pc._parse_alert(inp)
|
||||
if got != want:
|
||||
return False, f"{inp!r} → {got!r},want {want!r}"
|
||||
return True, "四级映射 OK"
|
||||
|
||||
|
||||
# ---------- 入口 ----------
|
||||
|
||||
def main() -> int:
|
||||
@@ -592,6 +717,13 @@ def main() -> int:
|
||||
_run("scanner/skips_non_active", test_scanner_skips_non_active_contracts)
|
||||
_run("html_renderer/basic", test_html_render_basic)
|
||||
_run("html_renderer/unknown_task_raises", test_html_render_unknown_task_raises)
|
||||
_run("platform_collector/gen_contract_id_emp_id", test_gen_contract_id_emp_id)
|
||||
_run("platform_collector/gen_contract_id_special", test_gen_contract_id_special_chars)
|
||||
_run("platform_collector/normalize_date_iso", test_normalize_date_iso)
|
||||
_run("platform_collector/normalize_date_fallback", test_normalize_date_fallback)
|
||||
_run("platform_collector/parse_days_positive", test_parse_days_positive)
|
||||
_run("platform_collector/parse_days_negative", test_parse_days_negative)
|
||||
_run("platform_collector/parse_alert_levels", test_parse_alert_red_orange_yellow_unknown)
|
||||
|
||||
total = len(_RESULTS)
|
||||
passed = sum(1 for _, ok, _ in _RESULTS if ok)
|
||||
|
||||
Reference in New Issue
Block a user