391 lines
23 KiB
Python
391 lines
23 KiB
Python
"""解析产出 QC 契合度审核能力(agent.qc)。
|
||
|
||
对应业务:招标文件解析的每类产出物(评分项+得分规则 / 资质 / 投标文件要求 / 章节骨架)
|
||
必须由 QC 对照招标文件原文逐项核对契合度,按 10 分制评分;**得分高于通过分(当前 8.5)才算完成**,
|
||
否则给出改进意见 → 清空该产出物 → 对账器重派解析任务带意见重做 → 重审。超轮次上限抛人工。
|
||
|
||
机制分工(与产线既有约定一致):
|
||
- 状态流转归机制:bid_flow 对账器派 QC 任务、判门禁、清空产出物触发重做;
|
||
- 本模块只固化操作原语:开轮(start_qc)/ 落分(finish_qc)/ 查询(qc_report、list_qc)。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
|
||
from .bid_common import (
|
||
get_db, new_id, rec_to_dict, rows_to_dicts, to_float, to_int,
|
||
record, create_human_task, find_human_task, get_thresholds, resolve_project_id,
|
||
QC_TYPE_SCORING, QC_TYPE_QUALS, QC_TYPE_REQS, QC_TYPE_OUTLINE, QC_TYPE_COST,
|
||
QC_TYPE_TECH, QC_TYPES, TECH_QC_TYPES,
|
||
QC_OUTPUT_TABLE,
|
||
HT_QC_ESCALATION,
|
||
)
|
||
|
||
logger = logging.getLogger("pipeline.bidding.qc")
|
||
|
||
# 每类产出的审核清单(start_qc 时随评审记录返回,指导 LLM 对照原文核对)
|
||
QC_DIMENSIONS = {
|
||
QC_TYPE_SCORING: (
|
||
"评分项+得分规则契合度审核(对照招标文件评标办法原文):\n"
|
||
"1. 完整性:评标办法中的每个评分项是否全部录入,无漏项、无编造项;\n"
|
||
"2. 满分准确:每项满分与原文一致(商务/技术/价格分合计核对);\n"
|
||
"3. 得分规则:档位/扣分/加分细则与原文一致,无概括失真;\n"
|
||
"4. 否决项:废标/否决条款全部标 is_veto=1,无漏标;\n"
|
||
"5. 归属正确:业务/技术/价格归类与出处(source_ref)可回溯原文。"
|
||
),
|
||
QC_TYPE_QUALS: (
|
||
"所需资质契合度审核(对照招标文件投标人资格要求原文):\n"
|
||
"1. 完整性:资格要求中的每项资质/证书/业绩门槛全部录入,无漏项;\n"
|
||
"2. 原文一致:要求描述与原文一致(等级/有效期/联合体口径);\n"
|
||
"3. 必需性:强制性条款标 is_mandatory=1,加分性资质不得误标必需;\n"
|
||
"4. 无编造:招标文件未要求的资质不得出现。"
|
||
),
|
||
QC_TYPE_REQS: (
|
||
"投标文件要求契合度审核(对照招标文件投标文件格式/要求原文):\n"
|
||
"1. 章节结构(structure)完整且顺序与原文一致,章节号/标题准确;\n"
|
||
"2. 格式/密封/份数/截止时间等要求(format/seal/copies/deadline)无遗漏;\n"
|
||
"3. 页数限制与原文一致;\n"
|
||
"4. 出处(source_ref)可回溯。"
|
||
),
|
||
QC_TYPE_OUTLINE: (
|
||
"章节骨架契合度审核(对照投标文件要求与评分项):\n"
|
||
"1. 章节与 req_type=structure 的要求一一对应,无缺章、无多余章;\n"
|
||
"2. 章节号/标题/顺序与投标文件要求一致;\n"
|
||
"3. 评分项挂载正确:每个评分项能落到对应章节(target_chapter_no/名称匹配),无错挂漏挂;\n"
|
||
"4. 章节要点来自评分标准原文枚举的子维度,不是通用目录。"
|
||
),
|
||
QC_TYPE_COST: (
|
||
"投标成本收益分析契合度审核(对照招标文件商务条款与预算/保证金原文):\n"
|
||
"1. 完整性:招标文件明确的成本/收益项(保证金、预算金额、付款条件等)全部覆盖,无漏项;\n"
|
||
"2. 金额依据:招标原文明确给出的金额(预算/保证金)与原文一致,估算项标注口径与置信度;\n"
|
||
"3. 推理合理:每项 basis 含原文引用+推理过程,无凭空编造的确定性数字;\n"
|
||
"4. 结论一致:summary 的成本合计/收益合计与单项一致,毛利口径正确;算不出须说明原因,不得硬编。"
|
||
),
|
||
QC_TYPE_TECH: (
|
||
"技术评估项契合度审核(纯技术方案流程,对照【技术需求书】原文——需求书是唯一基准):\n"
|
||
"1. 完整性:六类评估项(功能清单/功能点/硬件配置/技术架构/功能架构/部署架构)需求书明确提出的条目全部录入,无漏项;\n"
|
||
"2. 原文一致:requirement 与需求书原文一致(参数/数量/指标不失真),source_ref 可回溯章节条款;\n"
|
||
"3. 硬性要求标注:需求书明确必须的条目 mandatory=1 无漏标;\n"
|
||
"4. 颗粒度:功能点细化到可评估颗粒度(不是把整个模块当一条);硬件参数逐项拆条;\n"
|
||
"5. 无编造:需求书未提出的通用最佳实践/臆测需求不得录入;qty_note 与原文数字一致。"
|
||
),
|
||
}
|
||
|
||
# 每类产出对应的落库表:统一从 bid_common.QC_OUTPUT_TABLE 导入(含 cost_benefit)
|
||
|
||
|
||
async def _qc_type_valid(sor, qc_type):
|
||
if qc_type in QC_TYPES or qc_type in TECH_QC_TYPES:
|
||
return qc_type, ""
|
||
return "", "qc_type 非法(应为 %s)" % "/".join(QC_TYPES + TECH_QC_TYPES)
|
||
|
||
|
||
async def list_qc(project_id, qc_type="", limit=50):
|
||
"""列出项目 QC 审核记录。"""
|
||
try:
|
||
project_id = await resolve_project_id(project_id)
|
||
except ValueError:
|
||
return []
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
sql = ("SELECT id, qc_type, round, reviewer, fit_score, pass_score, passed, "
|
||
"LEFT(improvement, 500) AS improvement, task_id, created_at "
|
||
"FROM bid_qc_reviews WHERE project_id=${pid}$")
|
||
p = {"pid": project_id}
|
||
if qc_type:
|
||
sql += " AND qc_type=${qt}$"
|
||
p["qt"] = qc_type
|
||
sql += " ORDER BY qc_type, round DESC, created_at DESC LIMIT " + str(to_int(limit, 50))
|
||
recs = await sor.sqlExe(sql, p)
|
||
await sor.sqlExe("COMMIT", {})
|
||
return rows_to_dicts(recs)
|
||
|
||
|
||
async def start_qc(qc_type, project_id="", who=None, agent_id=None, task_id=""):
|
||
"""开启一轮 QC 审核:创建/复用当轮评审记录,返回审核清单 + 该产出物的待核内容。
|
||
|
||
LLM 随后读招标文件原文(list_tender_files/read_tender_file)+ 查产出物表,
|
||
对照审核清单逐项核对,得出 0~10 契合度分,调 finish_qc 落分。
|
||
"""
|
||
qc_type, err = await _qc_type_valid(None, qc_type)
|
||
if err:
|
||
return False, err
|
||
try:
|
||
project_id = await resolve_project_id(project_id)
|
||
except ValueError as e:
|
||
return False, str(e)
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
th = await get_thresholds(sor)
|
||
# 轮次 = 该类型已有审核数 + 1;同轮未落分的记录复用(幂等)
|
||
rn = await sor.sqlExe(
|
||
"SELECT COUNT(*) AS c FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$",
|
||
{"p": project_id, "t": qc_type})
|
||
await sor.sqlExe("COMMIT", {})
|
||
rnd = to_int(getattr(rn[0], "c", 0) if rn else 0) + 1
|
||
open_r = await sor.sqlExe(
|
||
"SELECT id, round FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$ "
|
||
"AND fit_score IS NULL ORDER BY created_at DESC LIMIT 1",
|
||
{"p": project_id, "t": qc_type})
|
||
await sor.sqlExe("COMMIT", {})
|
||
# 已通过守卫(2026-09-02):最新一轮已放行 → 不再开新轮。
|
||
# 根治:任务被引擎通用门禁弹回重跑时再次 start_qc 会白白烧掉一轮
|
||
# (qualifications 实测:第2轮8.80已过,弹回重跑开第3轮对重写中的表打5.6)。
|
||
# 产出若被 QC 清空重做,对账器只在重做完成后才重新派审核,此处不会误挡。
|
||
last_r = await sor.sqlExe(
|
||
"SELECT round, fit_score, passed FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$ "
|
||
"ORDER BY round DESC LIMIT 1", {"p": project_id, "t": qc_type})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if last_r and not open_r and str(rec_to_dict(last_r[0]).get("passed")) == "1":
|
||
_lr = rec_to_dict(last_r[0])
|
||
return True, {"status": "already_passed", "qc_type": qc_type,
|
||
"round": _lr.get("round"), "fit_score": _lr.get("fit_score"),
|
||
"message": "该类型第 %s 轮已通过契合度审核(%.2f 分 > 通过分),无需重审。"
|
||
"直接完成任务即可。" % (_lr.get("round"), to_float(_lr.get("fit_score")))}
|
||
# 同任务重复开轮守卫(2026-09-03):本任务已有落分记录且产出未在其后更新 →
|
||
# 复用该轮,不开新轮。根治:QC agent 对同一产出自我重试连烧轮次
|
||
# (doc_requirements 实测:同一任务 18:08 开第4轮 5.5 分后 18:32 又开第5轮)。
|
||
if task_id and not open_r:
|
||
my_r = await sor.sqlExe(
|
||
"SELECT id, round, fit_score, passed, updated_at FROM bid_qc_reviews "
|
||
"WHERE project_id=${p}$ AND qc_type=${t}$ AND task_id=${tid}$ "
|
||
"AND fit_score IS NOT NULL ORDER BY round DESC LIMIT 1",
|
||
{"p": project_id, "t": qc_type, "tid": task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if my_r:
|
||
_mr = rec_to_dict(my_r[0])
|
||
rev_t = str(_mr.get("updated_at") or "")
|
||
out_rec = await sor.sqlExe(
|
||
"SELECT MAX(GREATEST(created_at, IFNULL(updated_at, created_at))) AS mt "
|
||
"FROM " + QC_OUTPUT_TABLE[qc_type] + " WHERE project_id=${p}$",
|
||
{"p": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
out_t = str(getattr(out_rec[0], 'mt', '') or '') if out_rec else ""
|
||
if not (out_t and rev_t and out_t > rev_t):
|
||
return True, {"status": "same_task_reviewed", "qc_type": qc_type,
|
||
"review_id": _mr.get("id"), "round": _mr.get("round"),
|
||
"fit_score": _mr.get("fit_score"),
|
||
"message": "本任务第 %s 轮已审核(%.2f 分)且产出未更新,"
|
||
"不重复开轮。请完成本任务。"
|
||
% (_mr.get("round"), to_float(_mr.get("fit_score")))}
|
||
# 空产出守卫(2026-09-02):产出被 QC 清空、重做未完成 → 不开新轮。
|
||
# 根治:QC 审核任务比分析重做任务先跑时对着空表开轮打 0 分,白白烧一轮
|
||
# (cost_benefit 实测:第2轮对着零记录表打 0.00)。
|
||
_cnt = await sor.sqlExe(
|
||
"SELECT COUNT(*) AS c FROM " + QC_OUTPUT_TABLE[qc_type] + " WHERE project_id=${p}$",
|
||
{"p": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not open_r and to_int(getattr(_cnt[0], "c", 0) if _cnt else 0) == 0:
|
||
return True, {"status": "no_output", "qc_type": qc_type,
|
||
"message": "%s 当前无产出记录(被 QC 清空待重做或重做未完成),"
|
||
"不开评审轮次。请等待分析重做任务落库后再审,"
|
||
"本轮任务直接交付说明等待原因即可。" % qc_type}
|
||
if open_r:
|
||
rid = getattr(open_r[0], "id", "")
|
||
rnd = to_int(getattr(open_r[0], "round", 1), 1)
|
||
else:
|
||
rid = new_id()
|
||
await sor.C("bid_qc_reviews", {
|
||
"id": rid, "project_id": project_id, "qc_type": qc_type, "round": rnd,
|
||
"reviewer": (who or "agent.qc")[:60], "review_type": "agent",
|
||
"pass_score": th["qc_pass_score"], "passed": "0",
|
||
"task_id": task_id or "",
|
||
})
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
# 待核产出物(截断防上下文爆炸)
|
||
out_rows, note = [], ""
|
||
tbl = QC_OUTPUT_TABLE[qc_type]
|
||
if qc_type == QC_TYPE_OUTLINE:
|
||
recs = await sor.sqlExe(
|
||
"SELECT chapter_no, title, scoring_item_ids, max_score, LEFT(outline, 300) AS outline "
|
||
"FROM bid_chapters WHERE project_id=${p}$ ORDER BY order_no, chapter_no LIMIT 200",
|
||
{"p": project_id})
|
||
elif qc_type == QC_TYPE_SCORING:
|
||
recs = await sor.sqlExe(
|
||
"SELECT section, item_no, item_name, max_score, is_veto, target_chapter_no, "
|
||
"LEFT(scoring_rule, 400) AS scoring_rule, source_ref "
|
||
"FROM bid_scoring_items WHERE project_id=${p}$ ORDER BY section, order_no LIMIT 300",
|
||
{"p": project_id})
|
||
elif qc_type == QC_TYPE_QUALS:
|
||
recs = await sor.sqlExe(
|
||
"SELECT qual_name, is_mandatory, LEFT(requirement, 400) AS requirement "
|
||
"FROM bid_qualifications WHERE project_id=${p}$ ORDER BY is_mandatory DESC LIMIT 300",
|
||
{"p": project_id})
|
||
elif qc_type == QC_TYPE_COST:
|
||
recs = await sor.sqlExe(
|
||
"SELECT kind, category, item_name, amount, amount_note, "
|
||
"LEFT(basis, 500) AS basis, confidence, source_ref "
|
||
"FROM bid_cost_benefit WHERE project_id=${p}$ ORDER BY kind DESC, order_no LIMIT 300",
|
||
{"p": project_id})
|
||
elif qc_type == QC_TYPE_TECH:
|
||
recs = await sor.sqlExe(
|
||
"SELECT category, item_name, LEFT(requirement, 400) AS requirement, "
|
||
"qty_note, mandatory, confidence, source_ref "
|
||
"FROM bid_tech_items WHERE project_id=${p}$ AND kind='item' "
|
||
"ORDER BY category, order_no LIMIT 400",
|
||
{"p": project_id})
|
||
else:
|
||
recs = await sor.sqlExe(
|
||
"SELECT req_type, chapter_no, chapter_title, LEFT(requirement, 400) AS requirement, "
|
||
"page_limit, source_ref FROM bid_doc_requirements WHERE project_id=${p}$ "
|
||
"ORDER BY order_no LIMIT 300", {"p": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
out_rows = rows_to_dicts(recs, limit=300)
|
||
if not out_rows:
|
||
note = "该类产出物为空,无法审核(应等解析任务产出后再开审)。"
|
||
return True, json.dumps({
|
||
"review_id": rid, "qc_type": qc_type, "round": rnd,
|
||
"pass_score": th["qc_pass_score"], "max_round": th["qc_max_round"],
|
||
"checklist": QC_DIMENSIONS[qc_type],
|
||
"outputs_table": tbl, "outputs": out_rows, "note": note,
|
||
"instruction": (("请读技术需求书原文(list_tender_files → read_tender_file 分段)并逐条对照上述清单核对,"
|
||
if qc_type == QC_TYPE_TECH else
|
||
"请读招标文件原文(list_tender_files → read_tender_file 分段)并逐条对照上述清单核对,") +
|
||
"得出 0~10 的契合度分(可带一位小数),然后调 finish_qc 落分。"
|
||
"不通过(得分未高于通过分)时 improvement 必须写清:漏了什么/错在哪/对照原文哪一处,"
|
||
"否则工具拒绝提交。"),
|
||
}, ensure_ascii=False, default=str)
|
||
|
||
|
||
async def _snapshot_outputs(sor, pid, qc_type, round_no, task_id, fit_score, improvement):
|
||
"""QC 不通过清空产出前,把本轮产出整表快照到 bid_analysis_history(幂等:同轮重复快照跳过)。
|
||
|
||
保留失败轮次的输出,供任务树/人工追溯;不清空历史。
|
||
"""
|
||
tbl = QC_OUTPUT_TABLE.get(qc_type or "", "")
|
||
if not tbl:
|
||
return 0
|
||
exist = await sor.sqlExe(
|
||
"SELECT COUNT(*) AS c FROM bid_analysis_history WHERE project_id=${p}$ "
|
||
"AND qc_type=${t}$ AND round=${r}$", {"p": pid, "t": qc_type, "r": round_no})
|
||
if exist and to_int(getattr(exist[0], 'c', 0), 0) > 0:
|
||
return 0
|
||
rows = await sor.sqlExe(
|
||
"SELECT * FROM " + tbl + " WHERE project_id=${p}$", {"p": pid}) or []
|
||
n = 0
|
||
for r in rows:
|
||
d = rec_to_dict(r)
|
||
d.pop('id', None)
|
||
await sor.sqlExe(
|
||
"INSERT INTO bid_analysis_history "
|
||
"(id, project_id, qc_type, round, task_id, fit_score, improvement, payload, created_at) "
|
||
"VALUES (${id}$, ${p}$, ${t}$, ${r}$, ${tid}$, ${sc}$, ${imp}$, ${pl}$, NOW())",
|
||
{"id": new_id(), "p": pid, "t": qc_type, "r": round_no, "tid": task_id or "",
|
||
"sc": fit_score, "imp": (improvement or "")[:20000],
|
||
"pl": json.dumps(d, ensure_ascii=False, default=str)[:200000]})
|
||
n += 1
|
||
await sor.sqlExe("COMMIT", {})
|
||
logger.info("bid_qc: snapshot %s round=%d rows=%d", qc_type, round_no, n)
|
||
return n
|
||
|
||
|
||
async def finish_qc(review_id, fit_score, improvement="", comments="",
|
||
who=None, agent_id=None, task_id=""):
|
||
"""落分并判定:得分 > 通过分 → 该类产出放行;否则清空产出物 + 触发解析重做。
|
||
|
||
fit_score:0~10(10 分制契合度分)。
|
||
"""
|
||
if not review_id:
|
||
return False, "缺少 review_id"
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rv = await sor.sqlExe("SELECT * FROM bid_qc_reviews WHERE id=${r}$", {"r": review_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not rv:
|
||
return False, "QC 评审记录不存在: %s" % review_id
|
||
review = rec_to_dict(rv[0])
|
||
pid = review.get("project_id")
|
||
qc_type = review.get("qc_type")
|
||
th = await get_thresholds(sor)
|
||
sc = to_float(fit_score, -1.0)
|
||
if sc < 0 or sc > 10:
|
||
return False, "fit_score 必须为 0~10 的数字"
|
||
passed = sc > th["qc_pass_score"] # 严格高于通过分才算完成(8.5 → 需 >8.5)
|
||
if not passed and not (improvement or "").strip():
|
||
return False, ("QC 不通过(%.2f 未高于 %.2f)必须给出 improvement:\n"
|
||
"逐项写清缺了什么、错在哪、对照招标文件原文哪一处,解析角色据此重做。"
|
||
% (sc, th["qc_pass_score"]))
|
||
|
||
summary_lines = ["契合度 %.2f/10(通过线 >%.2f)→ %s"
|
||
% (sc, th["qc_pass_score"], "通过" if passed else "不通过")]
|
||
if comments:
|
||
summary_lines.append("审核说明:" + comments)
|
||
if improvement:
|
||
summary_lines.append("改进意见:" + improvement)
|
||
await sor.sqlExe(
|
||
"UPDATE bid_qc_reviews SET fit_score=${sc}$, passed=${ps}$, improvement=${imp}$, "
|
||
"comments=${cm}$, updated_at=NOW() WHERE id=${r}$",
|
||
{"sc": sc, "ps": "1" if passed else "0", "imp": improvement or "",
|
||
"cm": "\n".join(summary_lines)[:60000], "r": review_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
await record(sor, pid, "bid_qc_reviews", review_id, "qc_" + ("pass" if passed else "reject"),
|
||
to_state="passed" if passed else "rejected", who=who, agent_id=agent_id,
|
||
detail="%s round=%s %.2f" % (qc_type, review.get("round"), sc))
|
||
|
||
if passed:
|
||
return True, ("QC 通过:%s 契合度 %.2f > %.2f,该类产出放行。"
|
||
% (qc_type, sc, th["qc_pass_score"]))
|
||
|
||
# ── 不通过:轮次门禁 ──
|
||
if to_int(review.get("round"), 1) >= th["qc_max_round"]:
|
||
exist = await find_human_task(sor, pid, HT_QC_ESCALATION, status="pending")
|
||
if not exist:
|
||
await create_human_task(
|
||
sor, pid, HT_QC_ESCALATION,
|
||
"解析产出 QC 连续不达标需人工介入(%s)" % qc_type,
|
||
("「%s」已审核 %s 轮,最近契合度 %.2f/10(通过线 >%.2f),仍未达标:\n%s\n\n"
|
||
"请人工处理:核对招标文件修正产出物(可在对应管理页直接编辑),\n"
|
||
"或在 QC 审核记录页(bid_qc_reviews)把该记录 passed 改为 1 强制放行。\n"
|
||
"处理完成本任务后流程自动恢复。"
|
||
% (qc_type, review.get("round"), sc, th["qc_pass_score"],
|
||
(improvement or "")[:3000])),
|
||
assignee_role="owner.superuser")
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, ("QC 不通过且已达审核轮次上限 %d → 已抛人工介入(阻塞门禁)。"
|
||
% th["qc_max_round"])
|
||
|
||
# ── 不通过且未达轮次上限:先快照本轮产出(保留失败轮次的输出),
|
||
# 再把该轮分析任务标 qc_rejected(任务状态与QC判定一致,不再显示误导的"已批准"),
|
||
# 最后清空产出触发重做。失败轮次的输入(qc_improvements 在重做任务 params)
|
||
# 与输出(快照表)全程可查。──
|
||
tbl = QC_OUTPUT_TABLE.get(qc_type or "", "")
|
||
if not tbl:
|
||
return False, "QC 记录类型非法: %s"
|
||
await _snapshot_outputs(sor, pid, qc_type, to_int(review.get("round"), 1),
|
||
review.get("task_id") or "", sc, improvement or "")
|
||
if review.get("task_id"):
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_tasks SET state='qc_rejected', updated_at=NOW() "
|
||
"WHERE id=${t}$", {"t": review.get("task_id")})
|
||
await sor.sqlExe("DELETE FROM " + tbl + " WHERE project_id=${p}$", {"p": pid})
|
||
await sor.sqlExe("COMMIT", {})
|
||
cleared = tbl if qc_type != QC_TYPE_OUTLINE else "bid_chapters(章节骨架)"
|
||
return True, ("QC 不通过:%s 契合度 %.2f 未高于 %.2f。本轮产出已快照保留(bid_analysis_history),"
|
||
"任务标记 qc_rejected,产出物(%s)已清空,"
|
||
"系统将在下一轮对账重派解析任务(携带改进意见)重做后重审。"
|
||
% (qc_type, sc, th["qc_pass_score"], cleared))
|
||
|
||
|
||
async def qc_report(project_id):
|
||
"""项目 QC 总览:四类产出的最新审核状态 + 未通过时的改进意见。"""
|
||
try:
|
||
project_id = await resolve_project_id(project_id)
|
||
except ValueError as e:
|
||
return str(e)
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
th = await get_thresholds(sor)
|
||
out = {}
|
||
for t in QC_TYPES:
|
||
recs = await sor.sqlExe(
|
||
"SELECT round, fit_score, passed, LEFT(improvement, 600) AS improvement, created_at "
|
||
"FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$ "
|
||
"ORDER BY round DESC LIMIT 1", {"p": project_id, "t": t})
|
||
await sor.sqlExe("COMMIT", {})
|
||
out[t] = rec_to_dict(recs[0]) if recs else {"round": 0, "note": "尚未审核"}
|
||
return json.dumps({
|
||
"project_id": project_id, "pass_score": th["qc_pass_score"],
|
||
"max_round": th["qc_max_round"], "latest": out,
|
||
}, ensure_ascii=False, default=str)
|