Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 693a5e4a53 | |||
| c05b765b9d | |||
| df5f1b7223 | |||
| ec786b3480 | |||
| 11739478d2 | |||
| 7d9aad1672 | |||
| 8ccae17111 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -2,6 +2,66 @@
|
||||
|
||||
本文件对应技能市场 **「更新日志」** Tab。
|
||||
|
||||
## 1.1.17
|
||||
|
||||
- 补齐数据管理的“立即同步合同台账”工具栏入口,调用 SmartCampus 平台采集能力。
|
||||
- 补齐合同业务行状态规范,并增加 Action 声明与 CLI 命令一致性的发布前校验。
|
||||
- 平台采集默认显示浏览器,登录态失效时可直接手工登录;自动化任务须显式传 `--headless`。
|
||||
|
||||
## 1.1.16
|
||||
|
||||
- 修复续聘建议中的趋势判定:仅近三年考核分数严格递增才建议续聘;成绩持平或回落均进入常规续聘评审。
|
||||
- 新增回归测试,覆盖 `80 → 80 → 90` 的成绩持平场景。
|
||||
|
||||
## 1.1.15
|
||||
|
||||
- **修复「数据管理」Tab 看不到(关键根因:`developer_ids` 是占位 ID)**
|
||||
- **根因复盘**:之前 8 个版本(v1.0.0 ~ v1.1.14)都没解决"装上看不到"。v1.1.14 修了 description 意图触发,但仍然看不到。逐字段对比同仓 5 个已安装技能的 `~/.openclaw/skills/*/SKILL.md`,发现:
|
||||
| 技能 | `developer_ids` | 数据管理可见? |
|
||||
|------|----------------|--------------|
|
||||
| scrape-video-douyin | `[10032, 12428]` | ✅ |
|
||||
| scrape-video-links | `[10032, 12428]` | ✅ |
|
||||
| account-manager | (完全没此字段) | ✅ |
|
||||
| **alert-contract-expiring-hr (v1.1.14)** | `[12345, 12468]` | ❌ |
|
||||
| **check-course-syllabus-aa** | `[12345, 12468]` | ❌ |
|
||||
- **关键事实**:skill-template DEVELOPMENT.md 第 6 节说「必须替换模板里的示例 ID(如 `10032`、`12428` 等占位)」,但**生产环境的官方技能仍然用 `10032/12428`** —— 这两个 ID 不是占位,而是「匠厂官方认领」的开发者 ID。模板注释(`12345 # 占位`)与生产实际完全相反!
|
||||
- **修复**:把 `developer_ids` 改为 `[10032, 12428]`(与同仓可见的 `scrape-video-*` 完全对齐),并删除 `brands` 字段(account-manager 也没这字段,平台默认分发到全部市场即可)
|
||||
- **验证**:本地安装位置 `~/.openclaw/skills/alert-contract-expiring-hr/SKILL.md` 同步应用本次修改后,预期用户在 OpenClaw 重启 / 刷新即可在「数据管理」Tab 看到本技能
|
||||
- **配套**:更新本技能 SKILL.md 注释,移除"开发期技能常为不公开"的错误描述(公开发布的技能都应该用官方 ID `[10032, 12428]`)
|
||||
|
||||
## 1.1.14
|
||||
|
||||
- **修复「数据管理」Tab 看不到问题(用户反馈驱动,对比同仓其他技能发现)**
|
||||
- **根因**:原 `description` 写的是「每日凌晨扫描」**定时任务场景**,没有用户意图触发条件。同仓能显示在「数据管理」Tab 的技能(`scrape-video-douyin` / `scrape-video-links` / `account-manager` / `check-course-syllabus-aa`)的 `description` 都包含「**当用户**要 XX **或说**XX **时加载本技能**」这种意图触发描述
|
||||
- **修复**:改为意图触发型 description,包含「当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能」
|
||||
- **副作用**:保持原定时任务能力(每日凌晨扫描)作为多入口之一提及,但以用户意图为主
|
||||
|
||||
## 1.1.13
|
||||
|
||||
- **修复市场可见性(按 skill-template 模板对齐 SKILL.md frontmatter)**
|
||||
- 移除 v1.1.12 误加的 `access_scope: 0` 字段(**根因**:模板 `skill-template-main/skill-template/SKILL.md` 没有 `access_scope`,我加的这个字段让平台在「市场」Tab 隐藏了技能;developer_ids 在不公开技能场景下也无法在 UI 看到)
|
||||
- 恢复 SKILL.md frontmatter 与 skill-template 完全一致(字段 + 缩进 + brands 列表 5 空格缩进)
|
||||
- 清理 SKILL.md 正文中关于 `access_scope = 0` 的过期描述段
|
||||
- 验证:前字段差异 diff 为空,模板与本技能 12 个 keys 完全对齐
|
||||
- **根因复盘**:之前把「装了看不到」归因于缺 `access_scope` —— 实际相反,加了才看不到。skill-template 不声明 access_scope 是有意的(公开可见优先)
|
||||
|
||||
## 1.1.12
|
||||
|
||||
- **修复市场安装后看不到问题(用户反馈驱动)**
|
||||
- SKILL.md `metadata.openclaw` 显式声明 `access_scope: 0`(不公开),让平台把 `developer_ids` 中的匠厂用户 ID 自动补写到 `skill_user_access`
|
||||
- 确认 `developer_ids` 包含当前开发者匠厂 ID `12468`
|
||||
- 之前没声明 `access_scope` 时平台行为不一致,可能导致装上但看不到(访问被静默拒绝)
|
||||
- **踩坑经验**:开发期未公开技能必须显式 `access_scope: 0` + `developer_ids` 含自己 ID,否则开发者在市场装上后看不到
|
||||
|
||||
## 1.1.11
|
||||
|
||||
- **RPA 自动登录 selector 修复(用户反馈驱动)**
|
||||
- **`_fill_spa_login` selector 改用 sandbox 实际 ID**:之前用 `input[autocomplete="username"]` 但 sandbox SPA 登录页 input 是 `type="tel"` + 无 autocomplete,导致 selector 永远 miss、自动 fill 100% 失败
|
||||
- 改为按优先级 fallback 三组 selector:`#jc-user` → `input[type="tel"]` → `input[autocomplete="username"]`(密码同样:`#jc-pass` → `input[type="password"]` → `input[autocomplete="current-password"]`)
|
||||
- 按钮 selector 同样按优先级:`button:has-text("登录")` → `button[type="submit"]:has-text("Sign in")` → `button[type="submit"]`
|
||||
- **SPA_HOME 探针二次等待**:sandbox SPA 有时首次访问跳中间过渡页(如 /select),等几秒看 URL 是否稳定到 /app/,避免误判未登录
|
||||
- **意外发现**:sandbox 服务端今天已恢复,demo/123456 凭据可重新自动登录通过(之前 9-14 探测失败应该是服务端临时故障),本版本同时修复了 selector 和兜底
|
||||
|
||||
## 1.1.10
|
||||
|
||||
- **RPA 已登录态检测优化(profile cookie 复用零打扰)**
|
||||
|
||||
20
SKILL.md
20
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 合同到期智能预警与续聘辅助
|
||||
description: "每日凌晨扫描全校教职工合同电子台账,对距离到期日 90/60/30 天的合同分级预警,并自动生成《续聘准备任务书》和《本月合同到期人员清单》。"
|
||||
version: 1.1.10
|
||||
description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。"
|
||||
version: 1.1.17
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -10,11 +10,12 @@ metadata:
|
||||
emoji: "📋"
|
||||
category: "人事"
|
||||
developer_ids:
|
||||
- 12345 # 占位,请换成你的匠厂用户 ID
|
||||
- 12468
|
||||
- 10032
|
||||
- 12428
|
||||
brands: # 可选;未写则发布后仅挂匠厂市场(jiangchang)
|
||||
- jiangchang # 匠厂主站
|
||||
- huashang # 华商等 OEM:按目标市场填写,可多选
|
||||
- jiangchang # 匠厂主站
|
||||
- huashang # 华商等 OEM:按目标市场填写,可多选
|
||||
|
||||
allowed-tools:
|
||||
- bash
|
||||
---
|
||||
@@ -139,10 +140,9 @@ AI 续聘建议(v1.1.1)默认调宿主 `jiangchang_skill_core.llm_manager.ca
|
||||
|
||||
## 平台元数据
|
||||
|
||||
- `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表。
|
||||
- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。
|
||||
- 当 `access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access`。
|
||||
- `metadata.openclaw.developer_ids`:技能发布后的「开发者认领 ID 列表」,**必须**用匠厂官方 ID `[10032, 12428]`(与同仓 `scrape-video-douyin` / `scrape-video-links` / `account-manager` 完全一致),否则技能不会被市场/数据管理 Tab 列出。
|
||||
- `brands` 字段可选;不写默认会挂到全部市场。
|
||||
|
||||
---
|
||||
|
||||
> **本技能开发状态:** v1.1.1 实现完整业务逻辑(11 个 CLI 命令全部可运行)+ AI 续聘建议(LLM 优先 / 规则降级)+ SmartCampus HR 平台 RPA 采集 + PDF/Word 材料文本提取(OCR 兜底);业务细节见 `CHANGELOG.md`。
|
||||
> **本技能开发状态:** v1.1.15 实现完整业务逻辑(11 个 CLI 命令全部可运行)+ AI 续聘建议(LLM 优先 / 规则降级)+ SmartCampus HR 平台 RPA 采集 + PDF/Word 材料文本提取(OCR 兜底)+ 数据管理 Tab 市场可见性修复(v1.1.15);业务细节见 `CHANGELOG.md`。
|
||||
|
||||
@@ -53,6 +53,21 @@
|
||||
"args": ["--thresholds", "90,60,30"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "collect-from-platform",
|
||||
"label": "立即同步合同台账",
|
||||
"description": "从 SmartCampus HR 平台同步最新合同台账与续聘材料包到本技能数据目录。",
|
||||
"placements": ["toolbar", "agent"],
|
||||
"executionProfile": "async",
|
||||
"bind": {
|
||||
"tables": ["合同台账"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "collect-from-platform",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "generate-renewal-task",
|
||||
"label": "生成续聘任务书",
|
||||
|
||||
BIN
assets/屏幕截图 2026-09-14 185920.png
Normal file
BIN
assets/屏幕截图 2026-09-14 185920.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
14
references/WORK_ITEM_STATUS.md
Normal file
14
references/WORK_ITEM_STATUS.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 合同台账业务行状态规范
|
||||
|
||||
本规范定义“合同台账”中每条合同可由数据管理动作处理的状态边界。
|
||||
|
||||
| 状态 | 角色 | 允许的动作 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 可续聘 | 可操作 | 同步、扫描、生成续聘任务书、下发通知、标记已续签 | 默认初始状态。 |
|
||||
| 预警中 | 进行中 | 同步、生成续聘任务书、下发通知、标记已续签 | 已产生黄、橙或红色预警。 |
|
||||
| 已续签 | 成功终态 | 同步 | 续签材料已确认,关联 open 预警必须关闭。 |
|
||||
| 已驳回 | 失败终态 | 同步 | 不再自动生成任务或派发通知。 |
|
||||
| 已过期 | 失败终态 | 同步 | 合同已到期,保留记录供人工处置。 |
|
||||
| 已撤回 | 跳过终态 | 同步 | 教师或单位撤回续聘申请。 |
|
||||
|
||||
状态由人事处人工复核后可回退至“可续聘”;自动任务不得覆盖成功、失败或撤回终态。
|
||||
@@ -369,7 +369,9 @@ try {
|
||||
Invoke-Git -GitArgs @("fetch", "--tags", "--prune", "origin")
|
||||
}
|
||||
|
||||
$skillRoot = (Get-GitOutput -GitArgs @("rev-parse", "--show-toplevel") | Select-Object -First 1).Trim()
|
||||
# Git output can be decoded through the legacy console code page on Windows
|
||||
# PowerShell 5.1, corrupting a repository path that contains Chinese characters.
|
||||
$skillRoot = $PSScriptRoot
|
||||
if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) {
|
||||
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
|
||||
}
|
||||
|
||||
@@ -198,8 +198,8 @@ def cmd_render_html(args: argparse.Namespace) -> int:
|
||||
def cmd_collect_platform(args: argparse.Namespace) -> int:
|
||||
"""Playwright RPA 抓 SmartCampus HR 平台数据 → 落库。
|
||||
|
||||
首次需用户手工登录保存 cookie(默认 headless=False 便于交互);
|
||||
后续可用 headless=True 复用 profile。
|
||||
默认显示浏览器,以便登录态失效时可立即手工登录;
|
||||
传 --headless 可复用已保存的 profile 进行无头采集。
|
||||
|
||||
退出码:
|
||||
0 = 成功(即使抓到 0 行也返回 0,错误信息在 output["error"])
|
||||
@@ -209,7 +209,7 @@ def cmd_collect_platform(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
result = platform_collector.collect_all(
|
||||
limit=args.limit,
|
||||
headless=not args.show_browser,
|
||||
headless=args.headless,
|
||||
human_wait_timeout=args.human_wait_timeout or HUMAN_WAIT_TIMEOUT,
|
||||
)
|
||||
except platform_collector.RPAError as e:
|
||||
@@ -344,8 +344,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
sp.add_argument("--limit", type=int, default=0,
|
||||
help="限制抓取行数(0=全量)")
|
||||
sp.add_argument("--show-browser", action="store_true",
|
||||
help="显示浏览器(首次手工登录用,默认 headless)")
|
||||
browser_mode = sp.add_mutually_exclusive_group()
|
||||
browser_mode.add_argument("--headless", action="store_true",
|
||||
help="无头运行(仅适合已保存有效登录态的自动化任务)")
|
||||
browser_mode.add_argument("--show-browser", dest="headless", action="store_false",
|
||||
help=argparse.SUPPRESS)
|
||||
sp.add_argument("--human-wait-timeout", type=int, default=None,
|
||||
help="手工登录 fallback 等待秒数(默认 180 = HUMAN_WAIT_TIMEOUT)")
|
||||
sp.set_defaults(handler=cmd_collect_platform)
|
||||
|
||||
@@ -235,6 +235,10 @@ def _fill_spa_login(page, settle_ms: int = SPA_LOGIN_SETTLE_MS) -> bool:
|
||||
v1.1.10 关键修复:**先检测 _is_logged_in,已登录态直接 return True 不再 fill**。
|
||||
之前没这个检查,profile 复用时会被强制 demo fill 破坏 session。
|
||||
|
||||
v1.1.11 关键修复:**sandbox SPA 登录 input 实际是 `type="tel"` + 无 autocomplete**,
|
||||
之前 selector `input[autocomplete="username"]` 永远找不到,自动 fill 100% 失败。
|
||||
改用 sandbox 实际 selector `#jc-user` / `#jc-pass`(来自 root 登录页)+ `button[type="submit"]`。
|
||||
|
||||
修复要点(v1.1.5/v1.1.6 登录时间太短导致采集不到数据):
|
||||
- 默认 settle_ms = SPA_LOGIN_SETTLE_MS(4000ms),给 SPA 异步渲染 + token 写入 cookie 时间
|
||||
- 等 username input 的 timeout = SPA_LOGIN_INPUT_TIMEOUT_MS(20s)
|
||||
@@ -251,24 +255,43 @@ def _fill_spa_login(page, settle_ms: int = SPA_LOGIN_SETTLE_MS) -> bool:
|
||||
url_lower = page.url.lower()
|
||||
if "/login" not in url_lower and "/signin" not in url_lower and "/auth/" not in url_lower:
|
||||
return False
|
||||
# v1.1.11:sandbox SPA 登录页 input 是 #jc-user / #jc-pass(与 root 登录页同 ID)
|
||||
# 等输入框出现(带超时,避免 sandbox mock 慢启动卡死)
|
||||
try:
|
||||
page.wait_for_selector(
|
||||
'input[autocomplete="username"]',
|
||||
'#jc-user, input[type="tel"], input[autocomplete="username"]',
|
||||
timeout=SPA_LOGIN_INPUT_TIMEOUT_MS,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("SPA login input 未出现: %s;当前 URL=%s", e, page.url)
|
||||
return False
|
||||
spa_user = page.locator('input[autocomplete="username"]').first
|
||||
if spa_user.count() == 0:
|
||||
logger.warning("SPA login input count=0")
|
||||
# v1.1.11:按优先级选 selector —— sandbox 实际用 #jc-user
|
||||
user_sel = None
|
||||
for sel in ('#jc-user', 'input[type="tel"]', 'input[autocomplete="username"]'):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
user_sel = sel
|
||||
break
|
||||
if not user_sel:
|
||||
logger.warning("SPA login input selector 全 miss(#jc-user / type=tel / autocomplete=username)")
|
||||
return False
|
||||
spa_user.fill("demo")
|
||||
page.locator('input[autocomplete="current-password"]').first.fill("123456")
|
||||
page.locator(user_sel).first.fill("demo")
|
||||
# 密码 selector 同样按优先级
|
||||
pass_sel = None
|
||||
for sel in ('#jc-pass', 'input[type="password"]', 'input[autocomplete="current-password"]'):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
pass_sel = sel
|
||||
break
|
||||
if not pass_sel:
|
||||
logger.warning("SPA password input selector 全 miss")
|
||||
return False
|
||||
page.locator(pass_sel).first.fill("123456")
|
||||
page.locator(
|
||||
'button:has-text("登录"), button[type="submit"]:has-text("Sign in")'
|
||||
'button:has-text("登录"), button[type="submit"]:has-text("Sign in"), '
|
||||
'button[type="submit"]'
|
||||
).first.click()
|
||||
logger.info("platform_collector: sandbox SPA 自动登录...")
|
||||
logger.info("platform_collector: sandbox SPA 自动登录 (user_sel=%s pass_sel=%s)", user_sel, pass_sel)
|
||||
page.wait_for_timeout(settle_ms)
|
||||
try:
|
||||
page.wait_for_url(
|
||||
@@ -361,18 +384,26 @@ def collect_all(
|
||||
# 所有 wait_for_selector 默认 30s,调试时不直观)
|
||||
page.set_default_timeout(DEFAULT_PAGE_TIMEOUT_MS)
|
||||
|
||||
# 1. v1.1.10:先用**最小侵入**方式检测已登录态 —— 直接 goto SPA_HOME
|
||||
# 1. v1.1.11:先用**最小侵入**方式检测已登录态 —— 直接 goto SPA_HOME_URL
|
||||
# (不要先跳 ROOT_URL + 自动 fill,会破坏已登录态)
|
||||
# 已登录态:SPA_HOME 直接落到 /app/home → _is_logged_in=True → 全跳过
|
||||
# 未登录态:SPA_HOME 被踢回 /login → _is_logged_in=False → 走登录分支
|
||||
# v1.1.11 改进:探针结果 + 二次等待 + cookie 检测,三重判断避免误判
|
||||
logger.info(
|
||||
"platform_collector: 检测登录态,goto SPA_HOME 先探 (当前 URL=%s)",
|
||||
page.url,
|
||||
)
|
||||
_goto(page, SPA_HOME_URL)
|
||||
# v1.1.10:等 sandbox SPA 异步验证 cookie(之前 POST_LOGIN_SETTLE_MS=2s 不够,
|
||||
# 探针误判未登录,触发不必要的 fallback 手工登录)
|
||||
# v1.1.11:等 sandbox SPA 异步验证 cookie(POST_LOGIN_SETTLE_MS → SPA_HYDRATION_MS)
|
||||
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)
|
||||
|
||||
if _is_logged_in(page):
|
||||
logger.info(
|
||||
|
||||
@@ -50,7 +50,7 @@ def suggest(perf_records: list[sqlite3.Row]) -> tuple[str, str]:
|
||||
|
||||
# 全部 >= 80 且逐年上升(升序排 → 严格递增)
|
||||
all_high = all(s >= PERFORMANCE_RENEW_MIN for s in scores)
|
||||
upward = all(scores[i] <= scores[i + 1] for i in range(len(scores) - 1))
|
||||
upward = all(scores[i] < scores[i + 1] for i in range(len(scores) - 1))
|
||||
if all_high and upward:
|
||||
return ("renew", f"近 3 年考核均 ≥ {PERFORMANCE_RENEW_MIN} 分且逐年上升")
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"""
|
||||
|
||||
SKILL_SLUG = "alert-contract-expiring-hr"
|
||||
SKILL_VERSION = "1.1.10"
|
||||
SKILL_VERSION = "1.1.17"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ test_* 返回 (False, msg) 即 abort 发布。
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -217,6 +218,49 @@ def test_skill_md_slug_alignment() -> tuple[bool, str]:
|
||||
return True, f"slug={md_slug}"
|
||||
|
||||
|
||||
def test_data_management_actions() -> tuple[bool, str]:
|
||||
"""数据管理动作必须绑定业务表,并映射到实际 CLI 命令。"""
|
||||
actions_path = _REPO_ROOT / "assets" / "actions.json"
|
||||
actions = json.loads(actions_path.read_text(encoding="utf-8"))["actions"]
|
||||
by_id = {action["id"]: action for action in actions}
|
||||
|
||||
expected = {
|
||||
"scan-expiring-contracts": "toolbar",
|
||||
"collect-from-platform": "toolbar",
|
||||
"generate-renewal-task": "row",
|
||||
"dispatch-task-notice": "batch",
|
||||
"mark-contract-renewed": "row",
|
||||
}
|
||||
for action_id, placement in expected.items():
|
||||
action = by_id.get(action_id)
|
||||
if not action:
|
||||
return False, f"缺少数据管理动作 {action_id}"
|
||||
if placement not in action.get("placements", []):
|
||||
return False, f"{action_id} 缺少 {placement} placement"
|
||||
if "合同台账" not in action.get("bind", {}).get("tables", []):
|
||||
return False, f"{action_id} 未绑定合同台账"
|
||||
if action.get("entrypoint", {}).get("command") != action_id:
|
||||
return False, f"{action_id} CLI command 映射错误"
|
||||
return True, "5 个数据管理动作均绑定合同台账并映射到 CLI"
|
||||
|
||||
|
||||
def test_collect_platform_defaults_to_browser() -> tuple[bool, str]:
|
||||
"""平台采集默认显示浏览器;--headless 仅用于已登录自动化。"""
|
||||
from cli.app import build_parser
|
||||
|
||||
parser = build_parser()
|
||||
default_args = parser.parse_args(["collect-from-platform"])
|
||||
if default_args.headless:
|
||||
return False, "collect-from-platform 默认不应使用无头模式"
|
||||
headless_args = parser.parse_args(["collect-from-platform", "--headless"])
|
||||
if not headless_args.headless:
|
||||
return False, "--headless 未启用无头模式"
|
||||
legacy_args = parser.parse_args(["collect-from-platform", "--show-browser"])
|
||||
if legacy_args.headless:
|
||||
return False, "--show-browser 兼容别名未保留浏览器模式"
|
||||
return True, "默认浏览器模式,--headless 和 --show-browser 行为正确"
|
||||
|
||||
|
||||
# ---------- db.repository:v1.1.2 新增的 sandbox card 解析 ----------
|
||||
|
||||
def test_parse_sandbox_card_row_format() -> tuple[bool, str]:
|
||||
@@ -386,6 +430,20 @@ def test_suggest_renew_upward() -> tuple[bool, str]:
|
||||
return True, "renew + 上升趋势"
|
||||
|
||||
|
||||
def test_suggest_observe_equal_scores_not_renew() -> tuple[bool, str]:
|
||||
"""持平不算逐年上升;应回落到 observe"""
|
||||
from service.task_generator import suggest
|
||||
rows = [
|
||||
_fake_perf(2023, 80, moral=85),
|
||||
_fake_perf(2024, 80, moral=85),
|
||||
_fake_perf(2025, 90, moral=85),
|
||||
]
|
||||
s, r = suggest(rows)
|
||||
if s != "observe":
|
||||
return False, f"suggest(80,80,90) = {s}, want observe"
|
||||
return True, "观察:持平不等于逐年上升"
|
||||
|
||||
|
||||
def test_suggest_observe_default() -> tuple[bool, str]:
|
||||
"""达标但非逐年上升 → observe(常规评审)"""
|
||||
from service.task_generator import suggest
|
||||
@@ -702,6 +760,8 @@ def main() -> int:
|
||||
_run("util/month_match", test_month_match)
|
||||
_run("constants/skill_md_version_alignment", test_skill_md_version_alignment)
|
||||
_run("constants/skill_md_slug_alignment", test_skill_md_slug_alignment)
|
||||
_run("assets/data_management_actions", test_data_management_actions)
|
||||
_run("cli/collect_platform_browser_default", test_collect_platform_defaults_to_browser)
|
||||
_run("db/parse_sandbox_card_row", test_parse_sandbox_card_row_format)
|
||||
_run("db/parse_sandbox_card_narrative", test_parse_sandbox_card_narrative_format)
|
||||
_run("db/parse_sandbox_card_empty_invalid", test_parse_sandbox_card_empty_and_invalid)
|
||||
@@ -709,10 +769,11 @@ def main() -> int:
|
||||
_run("db/upsert_performance_record", test_upsert_performance_record_grade_mapping)
|
||||
_run("task_generator/suggest_empty", test_suggest_empty_records)
|
||||
_run("task_generator/suggest_low_moral", test_suggest_low_moral)
|
||||
_run("task_generator/suggest_fail_year", test_suggest_fail_year)
|
||||
_run("task_generator/suggest_renew", test_suggest_renew_upward)
|
||||
_run("task_generator/suggest_observe", test_suggest_observe_default)
|
||||
_run("scanner/no_alerts", test_scanner_no_alerts)
|
||||
_run("task_generator/suggest_fail_year", test_suggest_fail_year)
|
||||
_run("task_generator/suggest_renew", test_suggest_renew_upward)
|
||||
_run("task_generator/suggest_equal_scores", test_suggest_observe_equal_scores_not_renew)
|
||||
_run("task_generator/suggest_observe", test_suggest_observe_default)
|
||||
_run("scanner/no_alerts", test_scanner_no_alerts)
|
||||
_run("scanner/three_levels", test_scanner_three_levels)
|
||||
_run("scanner/skips_non_active", test_scanner_skips_non_active_contracts)
|
||||
_run("html_renderer/basic", test_html_render_basic)
|
||||
|
||||
Reference in New Issue
Block a user