feat: 读标补齐五类清单与项目基本信息,修复国动样本暴露的 4 个问题

- 文字层 v2:页眉页脚带单独成句;写到页尾的句子和下一页首句接续(出处两页各一条)
- 结构识别(最小版):十进制编号、冒号引出的列表找上级条款;上级条款写明无效的,条目照样认定
- 抽取:资格、评分、日期与递交、技术参数、项目基本信息各一份提示词;实质性条款出 v2
  (认上级条款、补「做无效响应处理」等写法);各类取值核对;按首句合并;
  拆开被合成一条的列举条款;描述标注方式的句子和表头碎片不当条款;* 号标注
- 项目:解析出的名称、编号、采购人、预算、截止时间、分包写回项目,用户改过的不覆盖
- 前端:清单项显示分值、参数要求、日期、证明材料
- 测试 40 个,新增两页虚构样例

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 19:32:25 +08:00
parent ee4bb23488
commit a95e27a290
29 changed files with 1971 additions and 230 deletions

View File

@@ -159,3 +159,44 @@ async def test_objects_are_encrypted_at_rest(user: User) -> None:
assert files and not any(p.read_bytes().startswith(b"%PDF") for p in files)
plain = (FIXTURES / "tender-mini.pdf").read_bytes()[:64]
assert not any(plain in p.read_bytes() for p in files)
async def test_all_categories_and_project_info(user: User) -> None:
"""虚构的两页样例:项目基本信息、截止时间、资格、评分、技术参数、上级标题下的无效情形、* 号兜底、页脚不进清单。"""
project_id, doc_id = await project_with_tender(user, "tender-full.pdf")
job = await run_parse(user, doc_id)
assert job["status"] == "succeeded", job
project = (await user.get(f"/projects/{project_id}")).json()
assert project["name"] == "某市智慧政务服务平台建设项目" # 建项目时没填名称:按招标文件自动填
assert project["code"] == "TEST-2026-001"
assert project["purchaser"] == "某市行政审批局"
assert project["legal_regime"] == "gov_procurement"
assert project["deadline_at"].startswith("2026-12-01T01:30") # 北京时间 9:30
fields = {f["key"]: f for f in project["fields"]}
assert fields["budget_total"]["value"] == "450000.00"
assert fields["budget_total"]["citation"]["page_no"] == 1
items = (await user.get(f"/projects/{project_id}/checklist/items")).json()["items"]
by_text = {i["requirement_text"]: i for i in items}
def item(prefix: str) -> dict[str, object]:
return next(v for k, v in by_text.items() if k and k.startswith(prefix))
for clause in ("25.3.1", "25.3.2"): # 本身没写「无效」,靠上级标题认定
it = item(clause)
assert it["zone"] == "formal" and "invalidity" in it["categories"], it
assert item("资格要求")["categories"] == ["qualification"]
assert item("技术方案")["spec"]["scoring"]["points"] == "20"
assert item("系统支持")["spec"]["tech_param"] == {"param": "系统支持", "op": ">=", "value": "200", "unit": "个"}
deadline = item("响应文件递交截止时间")
assert deadline["spec"]["schedule"]["date_kind"] == "bid_deadline"
star = item("*供应商须提供原厂授权书") # 假模型不认 * 号:规则兜底进待确认
assert (star["zone"], star["pending_reason"], star["importance"]) == ("pending", "marker_uncovered", "substantive")
assert not any(k and ("—1—" in k or "—2—" in k) for k in by_text) # 页码不混进条款
assert not any(k and "斜体且有下划线" in k for k in by_text) # 规则说明本身不是条款
checklist = (await user.get(f"/projects/{project_id}/checklist")).json()
assert {r["marker"] for r in checklist["marking_rules"]} == {"italic_underline", "asterisk"}
sentences = (await user.get(f"/documents/{doc_id}/pages/1/sentences")).json()["items"]
assert [s["location"] for s in sentences if s["text"] == "—1—"] == ["header_footer"]