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

@@ -14,7 +14,8 @@ from bid_doctor.core.authz import Permission, require
from bid_doctor.core.db import TenantContext
from bid_doctor.core.errors import AppError
from bid_doctor.core.ids import uuid7
from bid_doctor.project.models import Lot, Project, ProjectMember
from bid_doctor.document import citations
from bid_doctor.project.models import Lot, Project, ProjectField, ProjectMember
UNNAMED = "未命名项目"
@@ -83,3 +84,46 @@ async def list_projects(s: AsyncSession, ctx: TenantContext, limit: int) -> list
).tuples():
lots.setdefault(pid, []).append(lot_no)
return [(p, lots.get(p.id, [])) for p in projects]
FIELD_ORDER = (
"name",
"code",
"purchaser",
"agency",
"procurement_method",
"legal_regime",
"budget_total",
"ceiling_total",
"eval_method",
"accept_consortium",
"accept_imported",
"sme_only",
"deadline_at",
)
async def fields(s: AsyncSession, project_id: UUID) -> list[dict[str, Any]]:
"""项目基本信息字段(FR-M2-03):当前值、来源(AI / 人工)、AI 原值、出处、补遗带来的待确认值。"""
rows = (await s.execute(select(ProjectField).where(ProjectField.project_id == project_id))).scalars().all()
cites = await citations.to_api_many(s, [c for r in rows for c in (r.ai_citation_id, r.pending_citation_id) if c])
out = []
for r in sorted(rows, key=lambda r: FIELD_ORDER.index(r.field_key)):
human = r.human_value is not None
out.append(
{
"key": r.field_key,
"value": r.human_value if human else r.ai_value,
"source": "human" if human else "ai" if r.ai_value is not None else "none",
"ai_value": r.ai_value,
"citation": cites.get(r.ai_citation_id) if r.ai_citation_id else None,
"updated_by": None,
"pending": {
"value": r.pending_value,
"citation": cites.get(r.pending_citation_id) if r.pending_citation_id else None,
}
if r.pending_value is not None
else None,
}
)
return out