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:
parent
051eae7a25
commit
dc77748a09
@ -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},
|
||||
|
||||
393
pipeline_bidding/bid_stage_reentry.py
Normal file
393
pipeline_bidding/bid_stage_reentry.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user