feat(reentry): 阶段重做钩子三件套(2026-09-17流程修订/定向重做机制,引擎PipelineAbility钩子接入)——bid_stage_reentry按注入阶段执行下游重置:分析维度清产出表+QC记录(reqs_outline连带毁章节,确认待办如实预告);qc_analysis单独注入只清五类QC记录产出保留自动重派复审;tech链重置(甘肃场景:补技术方案部分)=清bid_tech_items含summary+tech QC记录+技术章置pending保正文(删章则A3链激活条件bool(_tech_chs)不成立永不激活;正文保留至模版确认时整体替换)+作废模版确认待办+技术章在办编写任务清场(LIKE走参数绑定防%格式化坑)+标书rejected+交付待办cancelled→A3裁剪链重新激活:抽取→techQC→PM模版裁剪→用户确认骨架→技术章重写,商务/报价章零接触;chapter_write清正文全复位/chapter_review复位written保正文重审;compose/score/delivery标书+待办作废;统一收尾:affected阶段角色在办任务取消(角色集运行时从阶段声明取)+材料缺口pending记录waived防孤儿;bid_reentry_describe影响说明(破坏性预告进确认待办);bid_progress_summary进度摘要(章节分布/标书版本/tech_items含summary判定/QC记录/在办待办,detect_reentry_stage的LLM上下文)

This commit is contained in:
yumoqing 2026-09-18 00:06:49 +08:00
parent 051eae7a25
commit dc77748a09
2 changed files with 401 additions and 0 deletions

View File

@ -15,6 +15,10 @@ from pipeline_core import (
SlashCommand, register_slash_command,
)
from .bid_stage_reentry import (
bid_stage_reentry, bid_reentry_describe, bid_progress_summary,
)
logger = logging.getLogger("pipeline.bidding.ability")
PIPELINE_ID = "bidding_general"
@ -636,6 +640,10 @@ def register_bid_ability():
roles=BID_ROLES,
flow_stages=BID_FLOW_STAGES,
flow_templates=BID_FLOW_TEMPLATES,
# 流程修订/定向重做钩子(2026-09-17,引擎 revise_flow_plan/confirm 回调)
stage_reentry_describe=bid_reentry_describe,
stage_reentry=bid_stage_reentry,
progress_summary=bid_progress_summary,
menus=[
{"label": "📄 招标文件", "icon": "", "url": "/pipeline-bidding/bid_tender_files/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},

View File

@ -0,0 +1,393 @@
# -*- coding: utf-8 -*-
"""投标产线阶段重做钩子(stage_reentry / describe / progress_summary)。
2026-09-17 用户裁定:用户有权调整流程,并要求从流程的任何一步按照意见重做;
或者只给意见,由 LLM 判定注入点(detect_reentry_stage,引擎层通用机制)。
本模块把「重置投标流程某阶段」的产线语义知识实现为引擎钩子:
- bid_stage_reentry(sor, project_id, stage_key, affected, user_requirements, operator)
按注入阶段执行下游重置(清产出/复位状态/作废交付物/取消在办任务),之后对账器
按既有逻辑自动从该阶段重新推进。破坏性动作已由用户在流程修订确认待办里授权
(确认时引擎回调本钩子——template_confirm 同款确认门模式)。
- bid_reentry_describe:重做影响说明(进确认待办,破坏性预告)。
- bid_progress_summary:项目进度摘要(LLM 判注入点上下文)。
重置语义(affected = 注入阶段+依赖闭包下游,引擎算好传入):
analysis_* 清对应产出表+QC记录(reqs_outline 连带清骨架=毁章节,如实预告)
qc_analysis 只清 QC 记录(产出保留,对账器自动重派复审)
tech_analysis/tech_qc/template_confirm(甘肃场景:补技术方案部分)
清 bid_tech_items(含summary)+tech QC 记录;technical 章节**置 pending
不删不清正文**(A3 链激活条件是「存在技术章且全 pending」,删章链
永不激活;正文保留到模版确认时被骨架整体替换,镜像/docx 里仍可查);
作废 pending 模版确认待办+标书+交付待办 → A3 裁剪链重新激活:
抽取→tech QC→PM 模版裁剪→用户确认骨架→技术章重写;商务/报价章不动
prep 作废 prep 完成记录(对账器重派资料准备)
chapter_write 全部章节复位 pending+清正文+revise清零 → 编写链重跑
chapter_review approved/rejected/reviewing 章节复位 written(正文保留)重审
compose/whole_score/delivery(含 tech_* 对应段)
标书全版本置 rejected+交付确认待办作废 → 对账器重派合成/评分/重发确认
统一收尾:affected 阶段角色的在办任务取消(防旧任务残留跑脏数据);凡触碰章节
或合成的重置,passed 标书一律作废(防僵尸交付指向旧版——2026-09-16 同款教训)。
"""
import json
import logging
from .bid_common import (
record,
QC_TYPE_TECH,
STAGE_ANALYSIS, STAGE_QUALS, STAGE_REQS_OUTLINE, STAGE_COST_BENEFIT,
STAGE_QC, STAGE_PREP, STAGE_CH_REVIEW, STAGE_WHOLE_SCORE, STAGE_DELIVERY,
STAGE_TECH_ANALYSIS, STAGE_TECH_QC, STAGE_TEMPLATE_CONFIRM,
STAGE_TECH_CH_WRITE, STAGE_TECH_CH_QC, STAGE_TECH_COMPOSE, STAGE_TECH_DELIVERY,
)
logger = logging.getLogger("pipeline.bidding.reentry")
PIPELINE_ID = "bidding_general"
# 分析维度 → (产出表, QC类型);reqs_outline 双表(要求+骨架)单独处理
_DIM_TABLES = {
"scoring": ("bid_scoring_items", "scoring_items"),
"quals": ("bid_qualifications", "qualifications"),
"cost_benefit": ("bid_cost_benefit", "cost_benefit"),
}
_DIM_STAGE_MAP = {
STAGE_ANALYSIS: "scoring",
STAGE_QUALS: "quals",
STAGE_REQS_OUTLINE: "reqs_outline",
STAGE_COST_BENEFIT: "cost_benefit",
}
_OPEN_STATES = ("submitted", "running", "review", "qc_review", "waiting")
# 触发「标书+交付待办作废」的注入阶段集合(触碰章节/合成/评分/交付的任何重置)
_VOID_DOC_STAGES = {"compose", "chapter_write", STAGE_CH_REVIEW, STAGE_WHOLE_SCORE,
STAGE_DELIVERY, STAGE_TECH_ANALYSIS, STAGE_TECH_QC,
STAGE_TEMPLATE_CONFIRM, STAGE_TECH_CH_WRITE, STAGE_TECH_CH_QC,
STAGE_TECH_COMPOSE, STAGE_TECH_DELIVERY, STAGE_REQS_OUTLINE}
async def _cancel_open_tasks(sor, project_id, roles, detail):
"""取消指定角色的在办任务(重做前置清场;affected 阶段角色集合由调用方算)。"""
if not roles:
return 0
states = "','".join(_OPEN_STATES)
role_in = "','".join(sorted(roles))
recs = await sor.sqlExe(
"SELECT id FROM pipeline_tasks WHERE tenant_id=${p}$ AND pipeline_id='role_task' "
"AND role IN ('" + role_in + "') AND state IN ('" + states + "')",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
n = 0
for r in (recs or []):
tid = getattr(r, "id", "")
if not tid:
continue
await sor.sqlExe(
"UPDATE pipeline_tasks SET state='cancelled', claimed_by=NULL, updated_at=NOW() "
"WHERE id=${t}$ AND state IN ('" + states + "')", {"t": tid})
n += 1
if n:
await sor.sqlExe("COMMIT", {})
return n
async def _void_docs_and_delivery(sor, project_id, detail):
"""标书全版本作废 + pending 交付确认待办作废(防僵尸交付指向旧版)。
对账器语义:最新标书 rejected → E 段全章 approved 后重派合成(v+1);
G/T7 段对 cancelled/rejected 待办视为不存在重发新版确认待办。
"""
await sor.sqlExe(
"UPDATE bid_documents SET status='rejected', updated_at=NOW() "
"WHERE project_id=${p}$ AND status IN ('draft','reviewing','passed')",
{"p": project_id})
# 注:pipeline_human_tasks 无 updated_at 列
await sor.sqlExe(
"UPDATE pipeline_human_tasks SET status='cancelled' "
"WHERE project_id=${p}$ AND task_type='bid_delivery_confirm' AND status='pending'",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_documents", project_id, "reentry_void",
to_state="rejected", who="system.flow_reentry", detail=detail[:480])
await sor.sqlExe("COMMIT", {})
async def _delete_orphan_gaps(sor, project_id, chapter_ids=None, all_pending=False):
"""清理材料缺口占位记录(章节被删/复位后 pending 缺口成孤儿;filled/waived 留审计)。
chapter_ids:删这些章节关联的 pending 缺口;all_pending:删项目全部 pending 缺口。
"""
try:
if all_pending:
await sor.sqlExe(
"UPDATE bid_material_gaps SET status='waived', "
"fill_note='流程定向重做:章节重建,旧缺口作废', updated_at=NOW() "
"WHERE project_id=${p}$ AND status='pending'", {"p": project_id})
elif chapter_ids:
for cid in chapter_ids:
await sor.sqlExe(
"UPDATE bid_material_gaps SET status='waived', "
"fill_note='流程定向重做:章节重置,旧缺口作废', updated_at=NOW() "
"WHERE project_id=${p}$ AND chapter_id=${c}$ AND status='pending'",
{"p": project_id, "c": cid})
await sor.sqlExe("COMMIT", {})
except Exception as e: # noqa: BLE001
logger.warning("reentry: 材料缺口清理失败(不阻断): %s", str(e)[:160])
async def _reset_tech_chain(sor, project_id, detail):
"""技术标裁剪链重置(tech_analysis/tech_qc/template_confirm 注入共用)。
A3 链激活五条件复原:无 summary 行 + technical 章全 pending + outline QC 仍通过
(不动五类解析 QC 记录)→ 对账器自动重派 tech_analysis → tech_qc → PM 模版任务
→ 用户确认骨架(confirm_tech_template 标准模式届时整体替换 technical 章)。
⚠️ 技术章**置 pending 不删不清正文**:删章则 `bool(_tech_chs)` 不成立,链永不
激活;正文保留供用户/写者参照,模版确认时才被新骨架替换。
"""
await sor.sqlExe("DELETE FROM bid_tech_items WHERE project_id=${p}$", {"p": project_id})
await sor.sqlExe(
"DELETE FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$",
{"p": project_id, "t": QC_TYPE_TECH})
# 技术章复位 pending(保正文);正文里的占位缺口记录同步作废(重写时会重新登记)
recs = await sor.sqlExe(
"SELECT id FROM bid_chapters WHERE project_id=${p}$ AND section='technical'",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
tech_ids = [getattr(r, "id", "") for r in (recs or []) if getattr(r, "id", "")]
await _delete_orphan_gaps(sor, project_id, chapter_ids=tech_ids)
await sor.sqlExe(
"UPDATE bid_chapters SET status='pending', revise_count=0, review_score=NULL, "
"updated_at=NOW() WHERE project_id=${p}$ AND section='technical'",
{"p": project_id})
await sor.sqlExe(
"UPDATE pipeline_human_tasks SET status='cancelled' "
"WHERE project_id=${p}$ AND task_type='tech_template_confirm' AND status='pending'",
{"p": project_id})
# 技术章在办编写任务取消(chapter_write 不在 affected 角色集里,写者任务不会被
# 统一收尾段取消——技术章已复位 pending,旧编写任务继续写会撞上模版确认的
# 「全 pending 才可替换」守卫,必须清场)
states = "','".join(_OPEN_STATES)
# LIKE 模式走参数绑定(sqlor ${kw}$ → %s):SQL 文本裸写 % 会被 pymysql
# 的 query%args 格式化吃掉(2026-09-16 踩坑同款),必须参数化
_kw = '%"section": "technical"%'
await sor.sqlExe(
"UPDATE pipeline_tasks SET state='cancelled', claimed_by=NULL, updated_at=NOW() "
"WHERE tenant_id=${p}$ AND pipeline_id='role_task' AND role='agent.bid_writer' "
"AND state IN ('" + states + "') AND params LIKE ${kw}$",
{"p": project_id, "kw": _kw})
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_tech_items", project_id, "reentry_reset",
who="system.flow_reentry", detail=detail[:480])
await sor.sqlExe("COMMIT", {})
async def bid_stage_reentry(sor, project_id, stage_key, affected_keys,
user_requirements="", operator_id=None):
"""执行阶段重置(引擎 confirm_flow_plan 回调)。返回 (ok, msg)。"""
affected = set(affected_keys or [])
if not affected:
affected = {stage_key}
detail = "定向重做注入点=%s(用户意见:%s)" % (stage_key, (user_requirements or "")[:200])
msgs = []
# ── 分析维度重置 ──
for st in (STAGE_ANALYSIS, STAGE_QUALS, STAGE_REQS_OUTLINE, STAGE_COST_BENEFIT):
if st not in affected:
continue
dim = _DIM_STAGE_MAP[st]
if dim == "reqs_outline":
# 骨架重解析 = 章节全毁(新骨架由 create_chapter_outline 重建),如实预告过
await _delete_orphan_gaps(sor, project_id, all_pending=True)
await sor.sqlExe("DELETE FROM bid_doc_requirements WHERE project_id=${p}$",
{"p": project_id})
await sor.sqlExe("DELETE FROM bid_chapters WHERE project_id=${p}$",
{"p": project_id})
await sor.sqlExe(
"DELETE FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type IN "
"('doc_requirements','chapter_outline')", {"p": project_id})
msgs.append("已清空投标文件要求+章节骨架与全部已写正文+对应QC记录(章节将重建重写)")
else:
tbl, qc = _DIM_TABLES[dim]
await sor.sqlExe("DELETE FROM %s WHERE project_id=${p}$" % tbl, {"p": project_id})
await sor.sqlExe(
"DELETE FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$",
{"p": project_id, "t": qc})
msgs.append("已清空 %s 产出+QC记录(对账器将重派解析)" % tbl)
await sor.sqlExe("COMMIT", {})
# ── QC 审核重置(qc_analysis 单独注入:只清 QC 记录,产出保留自动重派复审)──
if STAGE_QC in affected and not any(
s in affected for s in (STAGE_ANALYSIS, STAGE_QUALS, STAGE_REQS_OUTLINE,
STAGE_COST_BENEFIT)):
from .bid_common import QC_TYPES
qtypes = "','".join(QC_TYPES)
await sor.sqlExe(
"DELETE FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type IN ('" + qtypes + "')",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
msgs.append("已清空解析 QC 记录(各维度产出保留,对账器自动重派复审)")
# ── 技术标裁剪链重置 ──
if affected & {STAGE_TECH_ANALYSIS, STAGE_TECH_QC, STAGE_TEMPLATE_CONFIRM}:
await _reset_tech_chain(sor, project_id, detail)
msgs.append("已清空技术评估项+tech QC记录,技术章复位 pending(正文保留至模版确认替换),"
"A3 裁剪链将重新激活:抽取→QC→PM模版裁剪→用户确认骨架→技术章重写")
# ── 资料准备重置 ──
if STAGE_PREP in affected:
await sor.sqlExe(
"UPDATE pipeline_tasks SET state='cancelled', claimed_by=NULL, updated_at=NOW() "
"WHERE tenant_id=${p}$ AND pipeline_id='role_task' AND role='agent.bid_prep' "
"AND state IN ('approved','completed')", {"p": project_id})
await sor.sqlExe("COMMIT", {})
msgs.append("已作废资料准备完成记录(对账器将重派)")
# ── 章节编写重置(清正文全部重写)──
if "chapter_write" in affected or STAGE_TECH_CH_WRITE in affected:
await _delete_orphan_gaps(sor, project_id, all_pending=True)
await sor.sqlExe(
"UPDATE bid_chapters SET status='pending', revise_count=0, content='', "
"review_score=NULL, review_comment=NULL, updated_at=NOW() WHERE project_id=${p}$",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
msgs.append("已复位全部章节到 pending 并清空正文(编写链重跑)")
# ── 章节评审重置(正文保留,复位 written 重审;chapter_write 在 affected 时跳过
# ——编写注入已把章节打到 pending,评审自然重走)──
elif STAGE_CH_REVIEW in affected or STAGE_TECH_CH_QC in affected:
await sor.sqlExe(
"UPDATE bid_chapters SET status='written', revise_count=0, review_score=NULL, "
"review_comment=NULL, updated_at=NOW() "
"WHERE project_id=${p}$ AND status IN ('approved','reviewing','rejected')",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
msgs.append("已复位章节到 written(正文保留,评审重跑)")
# ── 标书/交付作废(凡触碰章节/合成/评分/交付的重置统一执行,防僵尸交付)──
if affected & _VOID_DOC_STAGES:
await _void_docs_and_delivery(sor, project_id, detail)
msgs.append("已作废标书(rejected)+交付确认待办(重走合成→评分→交付确认)")
# ── 统一收尾:affected 阶段角色的在办任务取消(角色集从阶段声明运行时取)──
roles = set()
try:
from pipeline_core import get_flow_stages
for fk in ("", "bid_standard", "bid_tech_proposal"):
for s in (get_flow_stages(PIPELINE_ID, flow_key=fk) or []):
if s.key in affected:
roles.update(s.roles or [])
except Exception as e: # noqa: BLE001
logger.warning("reentry: 取阶段角色失败(跳过任务清场): %s", str(e)[:120])
n_cancel = await _cancel_open_tasks(sor, project_id, roles, detail)
if n_cancel:
msgs.append("已取消 %d 个在办角色任务" % n_cancel)
await record(sor, project_id, "sd_projects", project_id, "stage_reentry",
to_state=stage_key, who=operator_id or "user",
detail=(detail + ";动作:" + ";".join(msgs))[:900])
await sor.sqlExe("COMMIT", {})
logger.info("bid stage_reentry done: project=%s stage=%s affected=%s",
project_id, stage_key, sorted(affected))
return True, ";".join(msgs) if msgs else "无需重置动作(该阶段无既有产出)"
# ══════════════════════ 影响说明(进确认待办,破坏性预告) ══════════════════════
_DESC = {
STAGE_ANALYSIS: "清空评分项与得分规则(bid_scoring_items)+对应QC记录,重新解析并复审",
STAGE_QUALS: "清空资质清单(bid_qualifications)+对应QC记录,重新解析并复审",
STAGE_REQS_OUTLINE: "清空投标文件要求(bid_doc_requirements)+**全部章节骨架与已写正文**(bid_chapters)+对应QC记录——章节将全部重建重写,**已有正文不保留**",
STAGE_COST_BENEFIT: "清空成本收益分析(bid_cost_benefit)+对应QC记录,重新解析并复审",
STAGE_QC: "清空全部解析QC记录(bid_qc_reviews),各维度产出保留并自动重派复审",
STAGE_TECH_ANALYSIS: "清空技术评估项(bid_tech_items)+tech QC记录;技术章复位待重写(现有正文保留至模版确认时替换);技术标模版裁剪链重新激活(抽取→QC→PM裁剪模版→**你重新确认骨架**→技术章重写→重合成→重评分);商务/报价章不受影响",
STAGE_TECH_QC: "清空tech QC记录+技术评估项重审,技术章复位;模版裁剪链从QC段重走;商务/报价章不受影响",
STAGE_TEMPLATE_CONFIRM: "撤销既有/待确认的技术章骨架+技术评估项+tech QC记录(裁剪链全链重走:抽取→QC→PM重新检索模版裁剪→**你重新确认骨架**);商务/报价章不受影响",
STAGE_PREP: "作废资料准备完成记录,重新执行知识库资质匹配",
"chapter_write": "**全部章节复位重写(已写正文清空)**→重审→标书作废重合成→重评分→重发交付确认",
STAGE_CH_REVIEW: "已通过/被退回章节复位重审(正文保留);不达标自动退回重写→重合成→重评分",
"compose": "标书全版本作废(rejected)+交付确认待办作废;全部章节通过后自动重合成新版→重评分→重发交付确认",
STAGE_WHOLE_SCORE: "最新标书作废+交付待办作废,自动重派整书评分;不达标按意见打回章节",
STAGE_DELIVERY: "交付确认待办作废+标书置 rejected,重评分后重发交付确认(仅重走确认环节;要改内容请注入更早阶段或用交付待办的退回通道)",
STAGE_TECH_CH_WRITE: "技术方案书全部章节复位重写(已写正文清空)→重QC→重合成→重发交付确认",
STAGE_TECH_CH_QC: "技术方案书章节复位重审(正文保留,对标需求书覆盖度)",
STAGE_TECH_COMPOSE: "技术方案书作废+交付待办作废,重合成→重发交付确认",
STAGE_TECH_DELIVERY: "技术方案书交付确认待办作废+文档置 rejected,重发交付确认",
}
async def bid_reentry_describe(sor, project_id, stage_key, affected_keys):
"""重做影响说明(确认待办正文渲染,破坏性预告)。"""
lines = []
for k in (affected_keys or []):
d = _DESC.get(k)
if d:
lines.append("- 【%s】%s" % (k, d))
if not lines:
lines.append("- 阶段 %s:按产线流转机制自动重走(无既有产出需清理)" % stage_key)
lines.append("- 通用动作:上述阶段角色的在办任务全部取消;标书/交付确认待办作废"
"(防交付物指向旧版);流程从注入阶段自动重新推进,无需人工再派任务。")
return "\n".join(lines)
async def bid_progress_summary(sor, project_id):
"""项目进度摘要(detect_reentry_stage 的 LLM 上下文)。"""
lines = []
try:
recs = await sor.sqlExe(
"SELECT status, COUNT(*) AS c FROM bid_chapters WHERE project_id=${p}$ "
"GROUP BY status", {"p": project_id})
await sor.sqlExe("COMMIT", {})
ch = {str(getattr(r, "status", "?")): getattr(r, "c", 0) for r in (recs or [])}
if ch:
lines.append("章节状态分布:%s" % json.dumps(ch, ensure_ascii=False))
recs = await sor.sqlExe(
"SELECT section, COUNT(*) AS c FROM bid_chapters WHERE project_id=${p}$ "
"GROUP BY section", {"p": project_id})
await sor.sqlExe("COMMIT", {})
sec = {str(getattr(r, "section", "?")): getattr(r, "c", 0) for r in (recs or [])}
if sec:
lines.append("章节分区分布(business商务/technical技术/price报价):%s"
% json.dumps(sec, ensure_ascii=False))
recs = await sor.sqlExe(
"SELECT version, status, chapter_count FROM bid_documents WHERE project_id=${p}$ "
"ORDER BY version DESC LIMIT 3", {"p": project_id})
await sor.sqlExe("COMMIT", {})
for r in (recs or []):
lines.append("标书 v%s:%s(%s章)" % (getattr(r, "version", "?"),
getattr(r, "status", "?"),
getattr(r, "chapter_count", "?")))
recs = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM bid_tech_items WHERE project_id=${p}$", {"p": project_id})
await sor.sqlExe("COMMIT", {})
n_ti = getattr(recs[0], "c", 0) if recs else 0
recs = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM bid_tech_items WHERE project_id=${p}$ AND kind='summary'",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
n_sum = getattr(recs[0], "c", 0) if recs else 0
lines.append("技术评估项:%s 行(模版裁剪摘要 %s 行%s)" % (
n_ti, n_sum, "——技术标模版裁剪链已走完" if n_sum else "——裁剪链未完成或未启用"))
recs = await sor.sqlExe(
"SELECT qc_type, round, fit_score, passed FROM bid_qc_reviews WHERE project_id=${p}$ "
"ORDER BY created_at DESC LIMIT 8", {"p": project_id})
await sor.sqlExe("COMMIT", {})
for r in (recs or []):
lines.append("QC %s r%s:%s分 passed=%s" % (
getattr(r, "qc_type", "?"), getattr(r, "round", "?"),
getattr(r, "fit_score", "?"), getattr(r, "passed", "?")))
recs = await sor.sqlExe(
"SELECT task_type, title, status FROM pipeline_human_tasks WHERE project_id=${p}$ "
"AND status='pending' ORDER BY created_at DESC LIMIT 8", {"p": project_id})
await sor.sqlExe("COMMIT", {})
for r in (recs or []):
lines.append("在办待办 [%s] %s" % (getattr(r, "task_type", "?"),
str(getattr(r, "title", ""))[:60]))
for tbl in ("bid_scoring_items", "bid_qualifications", "bid_doc_requirements"):
recs = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM %s WHERE project_id=${p}$" % tbl, {"p": project_id})
await sor.sqlExe("COMMIT", {})
lines.append("%s:%s 行" % (tbl, getattr(recs[0], "c", 0) if recs else 0))
except Exception as e: # noqa: BLE001
logger.warning("bid_progress_summary failed: %s", str(e)[:160])
return "\n".join(lines)