v1.1.2: sandbox 卡卡片文本采集 + 解析为 performance 记录

sandbox 行为新发现:
1. 「续聘材料包」按钮不是下载文件,而是触发页面下方显示「续聘准备任务书」
   +「单位清单」卡片(含 checklist + 近三年考核表)
2. 进入 hr/contracts 页面后需要刷新(page.reload)才能看到表格行
3. 表格列序扩展为 10 列:工号 / 姓名 / 单位 / 岗位 / 合同类型 / 起止日期 / 剩余天数 / 预警 / 建议 / 材料包按钮
4. contract_id 改用工号(稳定,不随姓名变化)

改动:
- platform_collector.collect_all:
  * 进入 hr/contracts 后 page.reload 刷新触发 SPA mock 数据加载
  * 新列序映射:r[0]=工号 r[1]=姓名 r[2]=单位 r[3]=岗位 r[4]=合同类型
    r[5]=起止日期 r[6]=剩余天数 r[7]=预警 r[8]=建议
  * 合同 end_date 从「2024-03-01 ~ 2026-04-15」截取 ~ 后的部分
  * 点「续聘材料包」按钮 → 抓下方卡片 inner_text(含 checklist + 考核等次)
  * 把卡片文本存入 materials_content(parse_method='sandbox_card')
- _persist_contracts 增加 sandbox_card 路径分支:写 materials_content 后同步调
  repo.parse_sandbox_card_performance → 抽 year + 等次 → 写 performance_records
- repository.parse_sandbox_card_performance:双模式正则匹配
  * 行格式 "2024\t称职"
  * 叙述式 "2024年:等次「称职」"
- repository.upsert_performance_record + 等次 → 分数映射
  (优秀=90 称职=75 基本称职=60 不称职=30)

实测(sandbox 3 合同):
- RPA-T20190033-20260415 钱实验 → 解析 4 条 performance
- RPA-T20200055-20260630 孙助理 → 解析 6 条
- RPA-T20180021-20260831 王明远 → 解析 6 条
- generate-renewal-task 全部输出 observe + decision_source=rules

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-14 16:01:20 +08:00
parent 2f06291270
commit 4af2b4fbf3
2 changed files with 214 additions and 48 deletions

View File

@@ -59,6 +59,82 @@ def list_performance_by_contract(
).fetchall()
# 等次 → 分数映射(v1.1.1 sandbox card 解析)
_GRADE_TO_SCORE = {
"优秀": 90.0,
"称职": 75.0,
"基本称职": 60.0,
"不称职": 30.0,
"不合格": 30.0,
}
def upsert_performance_record(
conn: sqlite3.Connection,
contract_id: str,
year: int,
grade: str,
) -> bool:
"""upsert 一条业绩记录(用 grade 映射分数)。
sandbox card 文本不含真分数时使用。返回是否实际写入。
"""
score = _GRADE_TO_SCORE.get(grade)
if score is None:
return False
record_id = f"P-{contract_id}-{year}"
conn.execute(
"""
INSERT INTO performance_records
(record_id, contract_id, year, performance_score,
moral_teaching_score, research_score, teaching_score)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(record_id) DO UPDATE SET
performance_score = excluded.performance_score
""",
(record_id, contract_id, year, score, score, score, score),
)
return True
def parse_sandbox_card_performance(
conn: sqlite3.Connection,
contract_id: str,
card_text: str,
) -> int:
"""从 sandbox 卡片文本抽 (year, 等次) → upsert 到 performance_records。
文本片段形如:
year result
2024 称职
2025 优秀
2024年:等次「称职」。
返回写入条数。
"""
import re
if not card_text:
return 0
written = 0
# 模式 1:行格式 "2024\t称职"
for m in re.finditer(r"^(\d{4})\s*[ \t]+(优秀|称职|基本称职|不称职|不合格)\s*$",
card_text, re.MULTILINE):
year = int(m.group(1))
grade = m.group(2)
if upsert_performance_record(conn, contract_id, year, grade):
written += 1
# 模式 2:叙述式 "2024年:等次「称职」"
for m in re.finditer(r"(\d{4})\s*年[::]\s*等次[「\"']\s*(优秀|称职|基本称职|不称职|不合格)\s*[」\"']",
card_text):
year = int(m.group(1))
grade = m.group(2)
if upsert_performance_record(conn, contract_id, year, grade):
written += 1
return written
# ---------- alerts ----------
def insert_alert(