Files
bid-doctor/server/src/bid_doctor/check/findings.py
YANG JIANKUAN e499de7486 feat: 查标第 1 步——上传投标分册 → 检查 → 逐条处理 → 复查沿用
后端
- check 模块:检查预览、发起、进度、取消、记录;六个阶段(准备投标文件、响应匹配、规则、证据校验、
  状态沿用、汇总);问题列表、处理、撤销、批量处理(只限提示级)。
- 每条清单项的检查结论 check_item_result(C9,迁移 0005):「已响应」也带投标原文出处;
  没有校验通过的证据不能判已响应(C3)。级别从清单项算,不让模型定;说明不写整改建议。
- 响应匹配先只有第三层:内存 BM25 检索候选句 + check.match v1 提示词,检索不到不调模型,
  漏答记 missing_answer,模型临时错误只影响那一批。
- 计费:免费版不记流水;专业版 24 小时内复查免扣(记一行 0);expected_charge 不一致时拒绝不扣点;
  失败、取消退回;投标文件读不出文字按输入问题失败、不扣点。
- 投标分册:类型(volume_kind)和版本;电子投标平台加密格式在上传时拦下。
- 解析流水线前几个阶段拆到 document/pipeline/normalize.py,解析和检查共用。
- ops 模块:反馈接口;致命问题确认无误的说明自动进误报反馈(U4)。
- SK1:Valkey 客户端短超时、SSE 连接池上限。SK2:CI 加 Gotenberg、直接跑 init-roles.sh。

前端
- 上传投标文件并检查页、查标页(进度、覆盖范围、按级别筛选、逐条处理和行内说明、撤销、原文抽屉、
  已处理、本次已解决);项目头抽成公共组件,加「查标」标签页;工作台「下一步」覆盖查标阶段。

契约、文档:openapi.yaml 加 item-results、volume_kind、coverage 字段;data-model、api-design §10、
架构 §8 / §13(SK14)、AGENTS.md 同步。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:48:10 +08:00

86 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""由每条清单项的检查结论生成问题(FR-M3-03、m3-plan §2.3)。
- 级别从清单项算,不让模型定(参考项目让模型定级别,模型可以把高危项降成低危);
- 说明只写事实和出处,不写整改建议(报价类问题的「建议」会写出数字,红线 6);
- 「已响应」不生成问题,结论留在 check_item_result 里。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from bid_doctor.check.match import Result
FATAL_CATEGORIES = {"qualification", "invalidity"}
REASONS = {
"evidence_missing": "没有给出投标原文",
"sid_format": "引用的句子编号写法不对",
"sid_unknown": "引用的句子不在候选范围内",
"quote_not_original": "引文和投标原文对不上",
"quote_too_short": "引文太短,无法核对",
"missing_answer": "这一条没有得到判断结果",
"model_error": "模型服务暂时不可用",
}
@dataclass(frozen=True, slots=True)
class Spec:
type: str
severity: str
title: str
explanation: str
detail: dict[str, Any]
def severity(item: dict[str, Any]) -> str:
"""★ 条款、法规列明或原文写明无效的、资格条件 → 致命;▲ 参数、评分项(缺证明材料)→ 重要;其余 → 提示。"""
cats = set(item.get("categories") or [])
if item.get("importance") == "substantive" or cats & FATAL_CATEGORIES:
return "fatal"
if item.get("importance") == "important" or "scoring" in cats:
return "important"
return "hint"
def _pages(pages: list[int]) -> str:
return "、".join(f"第 {p} 页" for p in pages)
def spec(result: Result, item: dict[str, Any]) -> Spec | None:
sev = severity(item)
name = str(item.get("title") or "")
base = {"importance_basis": item.get("importance_basis"), **result.detail}
scope = "、".join(result.detail.get("search_scope") or [])
if result.verdict == "responded":
return None
if result.verdict == "not_found" and result.evidence_status == "unverified":
claimed = result.detail.get("claimed_pages") or []
where = f"{_pages(claimed)}有响应" if claimed else "有响应"
why = REASONS.get(result.reason_code or "", result.reason_code or "")
text = f"模型认为投标文件{where},但出处没能核对({why}),按「未找到响应」处理。请人工核对。"
return Spec("unresponded", sev, f"未找到响应:{name}", text, {**base, "evidence_status": "unverified"})
if result.verdict == "not_found":
text = f"在投标文件({scope})中没有找到对这条要求的响应。" if scope else "没有找到对这条要求的响应。"
return Spec("unresponded", sev, f"未找到响应:{name}", text, base)
if result.verdict == "negative_deviation":
return Spec(
"negative_deviation", sev, f"负偏离未说明:{name}", "投标文件写明了偏离或不满足,没有找到说明。", base
)
if result.verdict == "suspected_unmet":
value = result.detail.get("response_value")
text = (
f"投标文件给出的值({value})按要求值判断可能不满足。"
if value
else "投标文件给出的值按要求值判断可能不满足。"
)
return Spec("suspected_unmet", sev, f"疑似不满足:{name}", text, base)
if result.verdict == "no_value":
text = "投标文件只写了满足或响应,没有给出要求里要的具体值或材料。"
return Spec("suspected_unmet", "hint", f"未给出具体值:{name}", text, {**base, "kind": "no_value"})
if result.verdict == "scanned":
text = "候选内容所在的页面是扫描件,系统读不出文字,请人工核对。"
return Spec("manual_review", sev, f"请人工核对:{name}", text, base)
why = REASONS.get(result.reason_code or "", result.reason_code or "")
return Spec("manual_review", sev, f"未判定:{name}", f"这条要求本次没有完成判断({why}),请人工核对。", base)