feat(a3): 标准流程技术标模版裁剪链——bid_standard内置tech_analysis→tech_qc→template_confirm三段(与纯技术方案流程共用对账段,逻辑只此一份)
- bid_flow A3段: 链激活五条件(有招标文件+两阶段启用+骨架QC已过+technical章全pending+无kind='summary'摘要行),任一不满足整链跳过=存量行为(甘肃等已开写项目零影响);完成判定用摘要行(换代闭环:新招标文件reset清bid_tech_items+作废pending确认待办) - C段门禁_tech_gate_on: 链激活只挡technical章,商务/报价章照常流转(HT_TEMPLATE_CONFIRM移出BLOCKING_HT_TYPES降级章节级) - confirm_tech_template双模式分流: 有非technical章=标准流程只删建technical章+全序列重排order_no(走查修复:原min+idx顺延在新骨架章数多于原技术章时与后置商务/报价章撞号,合成ORDER BY会把报价章插进技术章中间);无=纯技术流程整表清落;重确认守卫收窄到待替换范围 - record_tech_summary: 裁剪摘要落bid_tech_items kind='summary'(六类覆盖+模版来源+章数),幂等覆盖,失败不阻塞确认 - _reset_analysis_for_new_file: bid_tech_items入清空表清单+作废pending骨架确认待办 - chapter_detail: technical章注入tech_summary+list_tech_items提示(老库无表静默降级) - BID_FLOW_STAGES 14阶段: +tech_analysis(yes)/tech_qc(warn)/template_confirm(warn,dep=tech_analysis,裁头段必连带裁确认段) - bid_task_tree: analyst加tech维度组/QC加tech_items组/DIM_QCS补tech - README+DESIGN v1.6同步(3.7节A3设计+门禁表tech_template_confirm行)
This commit is contained in:
parent
6ef74388c1
commit
2fe7c784be
30
README.md
30
README.md
@ -12,10 +12,26 @@
|
||||
**产线起点 = 上传招标文件**(`import_tender_file` 无 project_id 时自动立项)。阶段链:
|
||||
|
||||
> 收到招标文件(自动立项)→ 解析(评分项/资质/投标文件要求/商务要素/章节骨架)
|
||||
> → QC 契合度审核(四类逐类,10 分制,>9.5 放行)→ 资料准备 → 分角色并发编写
|
||||
> → QC 契合度审核(四类逐类,10 分制,>9.5 放行)→ **技术方案部分:模版裁剪链(A3 段,
|
||||
> 2026-09-17 起)** → 资料准备 → 分角色并发编写
|
||||
> (business→bid_biz_writer / technical→bid_writer)→ 章节评审(含篇幅门禁)→ 合成
|
||||
> → 整书评分 → 交付确认。
|
||||
|
||||
**A3 段:标准流程技术标模版裁剪链(v1.6,2026-09-17)**:标准投标流程内置与纯技术方案
|
||||
流程同源的三段链 `tech_analysis(六类技术评估项,对标招标文件技术需求部分)→ tech_qc
|
||||
(qc_type=tech_items)→ template_confirm(PM 检索模版按评估项裁剪骨架 → 用户确认待办)`。
|
||||
对账器共用段 `_reconcile_tech_analysis_qc` / `_reconcile_template_confirm`(两条流程逻辑
|
||||
只此一份,flow_key 写进任务 params 区分来源)。**链激活五条件**:有招标文件 +
|
||||
tech_analysis/template_confirm 两阶段启用 + 骨架维度 QC 已过 + 存在 technical 章且全部
|
||||
pending + 无裁剪摘要行(`bid_tech_items kind='summary'`)——任一不满足整链跳过,技术章
|
||||
直接按招标骨架编写(存量项目零影响)。链激活期间 C 段只挡 technical 章(`_tech_gate_on`),
|
||||
商务/报价章照常流转;用户确认后 `confirm_tech_template` **标准模式只替换 technical 章**
|
||||
(新骨架落原技术章 min(order_no) 位置,商务/报价章不动),并落 kind='summary' 裁剪摘要行
|
||||
作为链完成的持久证据(confirm 落库、新招标文件 reset 清除;不用 done 待办/章节状态判完成
|
||||
——换代后会锁死新链或复活死循环)。BID_FLOW_STAGES 扩为 14 阶段(+tech_analysis
|
||||
trim=yes / tech_qc trim=warn / template_confirm trim=warn;裁 tech_analysis 必连带裁
|
||||
template_confirm)。tech 维度不进 ANALYSIS_DIMS、不走 PM 编排,由对账器直接派发。
|
||||
|
||||
**多流程模板(2026-09-15 批2)**:本产线声明两条流程(`BID_FLOW_TEMPLATES`,引擎
|
||||
FlowTemplate 机制首个接入)——`bid_standard`(上述标准链)与 `bid_tech_proposal`
|
||||
(**纯技术方案流程**:输入=技术需求书而非招标文件,产出=技术方案书):
|
||||
@ -68,7 +84,7 @@ FlowTemplate 机制首个接入)——`bid_standard`(上述标准链)与 `
|
||||
| `bid_qc_reviews` | 解析产出 QC 审核记录 | project_id, qc_type, round, reviewer, review_type, fit_score, pass_score |
|
||||
| `bid_analysis_history` | QC 分析历史快照(每轮 QC 逐审核记录归档;2026-09-10 归籍补 models 定义,此前是孤儿表) | project_id, qc_type, round, task_id, fit_score, improvement, payload |
|
||||
| `bid_cost_benefit` | 成本收益分析(分析维度4,单项+summary 结论行;m0006 建表未进 models,2026-09-10 归籍) | project_id, kind, category, item_name, amount, confidence |
|
||||
| `bid_tech_items` | 技术评估项(纯技术方案流程 tech_analysis 产出;kind=item/summary,category 六类;m0034 建表) | project_id, kind, category, item_name, requirement, mandatory, source_ref |
|
||||
| `bid_tech_items` | 技术评估项(纯技术方案流程与标准流程 A3 裁剪链 tech_analysis 产出;kind=item/summary,summary=骨架裁剪摘要行,category 六类;m0034 建表) | project_id, kind, category, item_name, requirement, mandatory, source_ref |
|
||||
| `bid_kb_docs` | 公司投标知识库 | org_id, doc_type(sample_bid 等), doc_name, doc_no, tags, file_path, content_text |
|
||||
| `bid_material_gaps` | 投标材料缺口清单(占位符规范:正文 `【材料待补:…】` 自动登记,合成后出清单,人工/产线补齐;m0035 建表) | project_id, chapter_no, material_name, placeholder_text, fingerprint, status(pending/filled/waived) |
|
||||
| `bid_members` | 投标项目参与人员 | project_id, user_id, user_name, member_role, duty, status |
|
||||
@ -89,10 +105,12 @@ bid_chapter_status / bid_review_scope / bid_kb_doc_type / bid_doc_status)。
|
||||
最近 in_progress 项目。
|
||||
- **api/bid_task_io.dspy** — 任务树右侧详情面板:输入 = 任务描述 + QC 改进意见,
|
||||
执行步骤,输出 = 交付件(复用 `pipeline_service/task_io_render.py` 统一渲染规范)。
|
||||
- **api/tech_template_confirm.dspy** — 技术方案骨架用户确认/驳回端点(2026-09-15)。
|
||||
入参 human_task_id/decision(confirm|reject)/comment(驳回必填)。用户专属:同机构+
|
||||
超管/创建者校验复用引擎 `_check_confirm_operator`;agent 无对应工具无法代确认。
|
||||
确认后骨架落 bid_chapters(section=technical) 开写;驳回意见由对账器注入重派模版任务。
|
||||
- **api/tech_template_confirm.dspy** — 技术方案骨架用户确认/驳回端点(2026-09-15;
|
||||
2026-09-17 双模式分流)。入参 human_task_id/decision(confirm|reject)/comment(驳回必填)。
|
||||
用户专属:同机构+超管/创建者校验复用引擎 `_check_confirm_operator`;agent 无对应工具无法代确认。
|
||||
确认后骨架落 bid_chapters(section=technical) 开写——**标准流程(项目存在非 technical 章)
|
||||
只替换技术章并落 kind='summary' 裁剪摘要行,商务/报价章原样保留;纯技术方案流程整表清落**;
|
||||
驳回意见由对账器注入重派模版任务。
|
||||
- **bid_task_tree_popup.ui** — ResourceBrowser 复合 widget 弹窗(左树右详情联动)。
|
||||
- **agent/index.ui** — 驾驶舱入口页(顶栏含任务树按钮)。
|
||||
- **12 个 CRUD 目录**(bid_tenders / bid_tender_files / bid_scoring_items /
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
> 对照开发产线(`sdlc_general`)设计,复用平台通用内核(项目/会话/任务链/人类任务/交付件/问答冒泡/技能分层),
|
||||
> 只增量定义投标产线自己的 **阶段、角色、工具、技能、任务流转规则、交付件**。
|
||||
> 版本:v1.2.0 创建:2026-08-26 修订:2026-08-31(v1.1 上传招标文件起点+QC 门禁;v1.2 编写分商务/技术角色+篇幅门禁+样板标书规范+并发编写)
|
||||
> 版本:v1.6.0 创建:2026-08-26 修订:2026-09-17(v1.1 上传招标文件起点+QC 门禁;v1.2 编写分商务/技术角色+篇幅门禁+样板标书规范+并发编写;v1.6 标准流程技术标模版裁剪链 A3 段,见 3.7)
|
||||
|
||||
---
|
||||
|
||||
@ -130,6 +130,7 @@ agent.bid_scorer → 整书评分
|
||||
| `tender_file_upload` | 等待上传招标文件(上传后自动完成) | 项目全部角色任务 |
|
||||
| `human_docs_request` | 人类应提供的文件 | 项目全部角色任务 |
|
||||
| `qc_escalation` | 解析产出 QC 连续不达标 | 项目全部角色任务 |
|
||||
| `tech_template_confirm` | 技术方案骨架用户确认(标准流程 A3 段链激活时;2026-09-17 起从全局阻塞降级为**章节级**:只挡 technical 章,商务/报价章照常) | technical 章节编写 |
|
||||
| `bid_delivery_confirm` | 最终标书交付确认 | 项目收尾(不阻塞派发) |
|
||||
|
||||
**门禁的守门位置:投标产线的角色任务唯一创建者是 `bid_flow` 对账器**(每 15 秒状态对账)。
|
||||
@ -162,7 +163,47 @@ agent.bid_scorer → 整书评分
|
||||
**并发编写**:对账器 C 块单轮最多派 `bid_write_concurrency`(默认 4)个编写任务;
|
||||
引擎 agent poller 另有每项目并发上限 `max_agents_per_project`(默认 4)兜底,任务间 asyncio 并发执行。
|
||||
|
||||
### 3.7 硬规则
|
||||
### 3.7 标准流程技术标模版裁剪链(A3 段,v1.6 新增)
|
||||
|
||||
标准投标流程内置与纯技术方案流程**同源**的三段链,产出标书「技术方案部分」:
|
||||
|
||||
```
|
||||
tech_analysis(agent.tender_analyst,analysis_dim=tech,六类技术评估项落 bid_tech_items,
|
||||
对标招标文件技术需求部分)
|
||||
↓
|
||||
tech_qc(agent.qc,qc_type=tech_items,对标招标原文,10 分制同款门禁)
|
||||
↓
|
||||
template_confirm(agent.pm,task_kind=tech_template:find_template_candidates 三级检索
|
||||
→ propose_tech_template 骨架六类覆盖硬校验 → tech_template_confirm 用户确认待办)
|
||||
↓ 用户确认(confirm_tech_template 标准模式)
|
||||
只替换 technical 章骨架(新章落原技术章 min(order_no) 位置,商务/报价章不动)
|
||||
+ 落 kind='summary' 裁剪摘要行 → 技术章解禁开写
|
||||
```
|
||||
|
||||
**链激活五条件**(bid_flow A3 段,任一不满足整链跳过 = 存量行为,技术章直接按招标骨架编写):
|
||||
1. 有招标文件(n_files > 0)且章节骨架已生成;
|
||||
2. `tech_analysis` + `template_confirm` 两阶段都启用(BID_FLOW_STAGES 14 阶段,
|
||||
裁 tech_analysis 必连带裁 template_confirm——依赖断裂校验拒绝单裁);
|
||||
3. 骨架维度 QC 已通过(QC_TYPE_OUTLINE ∈ _qc_passed_eff——outline 重做会清空
|
||||
bid_chapters,防把已确认模版骨架连锅端);
|
||||
4. 项目存在 technical 章节且全部 pending(任一 writing/written/approved = 已按原骨架
|
||||
推进或存量项目,不激活);
|
||||
5. 无裁剪摘要行(`bid_tech_items kind='summary'`,confirm 时落库、新招标文件 reset 时清除)。
|
||||
|
||||
**完成判定定案用摘要行**,不用 done 待办或章节状态:换代后旧 done 待办仍在会永久锁死
|
||||
新代链;替换后的新骨架也是 pending,靠章节状态判完成会复活死循环。
|
||||
|
||||
**门禁语义**:链激活时 C 段只挡 technical 章(`_tech_gate_on`),商务/报价章照常流转——
|
||||
`tech_template_confirm` 待办已从全局阻塞降级为章节级依赖(bid_common BLOCKING/HT 定义同步)。
|
||||
|
||||
**共用段实现**:`_reconcile_tech_analysis_qc`(T1/T2:评估项派发+QC+升级)与
|
||||
`_reconcile_template_confirm`(T3:模版任务派发+确认待办补发+驳回重派)——纯技术方案分支
|
||||
`_reconcile_tech_proposal` 重构为调共用段,逻辑只此一份;`flow_key` 写进任务 params 区分来源。
|
||||
`confirm_tech_template` 按「项目是否存在非 technical 章」自动分流标准/纯技术方案双模式,
|
||||
重确认守卫收窄到待替换范围。**tech 维度不进 ANALYSIS_DIMS、不走 PM 编排**(对账器直接派发,
|
||||
避免编排误派/掉链误判)。新招标文件导入 reset:清 bid_tech_items + 作废 pending 骨架确认待办。
|
||||
|
||||
### 3.8 硬规则
|
||||
|
||||
- 章节写作任务与章节一对一(`params.chapter_id`),禁止一个任务写多章。
|
||||
- 评审/评分只由 `agent.bid_reviewer` / `agent.bid_scorer` 执行,写作角色不得自评;
|
||||
|
||||
@ -44,6 +44,22 @@ BID_FLOW_STAGES = [
|
||||
description="QC 对照招标文件原文逐类审核解析产出(10分制,不过退回重做)",
|
||||
trim="warn", warn_note="裁掉解析QC后,评分项/资质/骨架的抽取错误将不经核对直接进入编写阶段,可能整章返工",
|
||||
roles=["agent.qc"], deps=["analysis_scoring", "analysis_reqs_outline"]),
|
||||
# ── 技术标模版裁剪链(2026-09-17 挂进标准流程)──
|
||||
# 标准标书的技术方案部分不再直接按招标骨架开写:先从招标文件技术部分抽取
|
||||
# 六类技术评估项 → QC 对标 → PM 检索技术标模版并按评估项裁剪出技术章骨架 →
|
||||
# 用户确认后只替换 technical 章节(商务/报价章原样保留)。与纯技术方案流程
|
||||
# 同源(bid_tech_capability),确认落库按「项目是否有非 technical 章节」自动分流。
|
||||
FlowStage(key="tech_analysis", label="技术评估项抽取(技术方案裁剪依据)",
|
||||
description="从招标文件技术部分抽取六类技术评估项(功能清单/功能点/硬件配置/技术架构/功能架构/部署架构)落库 bid_tech_items——技术章骨架裁剪与编写的对标依据",
|
||||
trim="yes", roles=["agent.tender_analyst"], deps=["analysis_reqs_outline"]),
|
||||
FlowStage(key="tech_qc", label="技术评估项QC契合度审核",
|
||||
description="QC 对照招标文件技术需求原文审核评估项(10分制,不过退回重做)",
|
||||
trim="warn", warn_note="裁掉技术评估项QC后,抽取错误不经核对直接进入骨架裁剪,技术方案可能偏离招标要求",
|
||||
roles=["agent.qc"], deps=["tech_analysis"]),
|
||||
FlowStage(key="template_confirm", label="技术标模版裁剪+用户确认骨架",
|
||||
description="预定模版(知识库)→ 无则 RAG → 无则网络检索,按评估项裁剪技术章骨架,发待办由用户确认后替换技术章(商务/报价章不受影响)",
|
||||
trim="warn", warn_note="裁掉模版确认后,技术章直接按招标书骨架编写,跳过技术标模版裁剪,技术方案结构可能不符合行业惯例",
|
||||
roles=["agent.pm"], deps=["tech_analysis"]),
|
||||
FlowStage(key="prep", label="投标资料准备",
|
||||
description="知识库资质匹配 + 缺失资质的人类文件清单(只阻塞商务章)",
|
||||
trim="yes", roles=["agent.bid_prep"], deps=["analysis_quals"]),
|
||||
|
||||
@ -31,7 +31,7 @@ from .bid_common import (
|
||||
QC_OUTPUT_TABLE, DIM_TO_STAGE,
|
||||
STAGE_ANALYSIS, STAGE_QUALS, STAGE_REQS_OUTLINE, STAGE_COST_BENEFIT,
|
||||
STAGE_QC, STAGE_PREP, STAGE_CH_REVIEW, STAGE_WHOLE_SCORE, STAGE_DELIVERY,
|
||||
FLOW_KEY_TECH_PROPOSAL, STAGE_TECH_ANALYSIS, STAGE_TECH_QC,
|
||||
FLOW_KEY_TECH_PROPOSAL, FLOW_KEY_STANDARD, STAGE_TECH_ANALYSIS, STAGE_TECH_QC,
|
||||
STAGE_TEMPLATE_CONFIRM, STAGE_TECH_CH_WRITE, STAGE_TECH_CH_QC,
|
||||
STAGE_TECH_COMPOSE, STAGE_TECH_DELIVERY,
|
||||
)
|
||||
@ -377,7 +377,9 @@ async def _reset_analysis_for_new_file(sor, project_id, since_ts):
|
||||
if not recs:
|
||||
return False
|
||||
for tbl in ("bid_scoring_items", "bid_qualifications", "bid_doc_requirements",
|
||||
"bid_chapters", "bid_cost_benefit"):
|
||||
"bid_chapters", "bid_cost_benefit",
|
||||
# 2026-09-17:技术评估项/裁剪摘要同为旧文件产物,新文件重抽
|
||||
"bid_tech_items"):
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM %s WHERE project_id=${p}$" % tbl, {"p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
@ -385,6 +387,13 @@ async def _reset_analysis_for_new_file(sor, project_id, since_ts):
|
||||
"DELETE FROM bid_qc_reviews WHERE project_id=${p}$ AND created_at < ${ts}$",
|
||||
{"p": project_id, "ts": since_ts})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
# pending 的技术章骨架确认待办作废(骨架依据旧文件评估项,新文件要重裁剪)
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_human_tasks SET status='cancelled', "
|
||||
"qc_comment='已上传新招标文件,骨架需按新技术评估项重新裁剪,本待办自动作废' "
|
||||
"WHERE project_id=${p}$ AND task_type=${tt}$ AND status='pending'",
|
||||
{"p": project_id, "tt": HT_TEMPLATE_CONFIRM})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||
if ht and "解析" in (ht.get("title") or ""):
|
||||
await sor.sqlExe(
|
||||
@ -1038,11 +1047,58 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
# 不提前 return:继续往下走章节级派发(依赖已满足的章节立即启动)
|
||||
|
||||
# ── A3. 技术方案部分:模版裁剪链(2026-09-17 挂进标准流程)──
|
||||
# 用户要求:标准投标项目必须产出标书「技术方案部分」,技术章不再直接按招标
|
||||
# 骨架开写,而是走与纯技术方案流程同源的裁剪链(T1/T2/T3 共用段):
|
||||
# tech_analysis(六类技术评估项)→ tech_qc(对标招标技术需求原文)→
|
||||
# template_confirm(PM 检索模版按评估项裁剪 → 用户确认)→
|
||||
# confirm_tech_template 标准模式(只替换 technical 章,商务/报价章原样保留)。
|
||||
# 链激活判定(存量兼容,甘肃等已开写项目零影响):
|
||||
# - tech_analysis + template_confirm 阶段都启用(任一被裁 → 整链跳过,
|
||||
# 评估项只为裁剪服务,裁了确认段还抽评估项是白烧 LLM);
|
||||
# - 骨架维度 QC 已通过(outline 重做会清空 bid_chapters——防把已确认的
|
||||
# 模版骨架连锅端);
|
||||
# - 项目存在 technical 章节且全部未开写(任一技术章 writing/written/approved
|
||||
# = 已按原骨架推进或存量项目,链不激活);
|
||||
# - 无「本文件代裁剪摘要」(bid_tech_items kind='summary',confirm 时落库、
|
||||
# 新招标文件 reset 时清除——摘要存在 = 当代链已走完。不用 done 待办判完成:
|
||||
# 换代后旧 done 待办仍在,会把新文件代的链永久锁死;替换后的新骨架也是
|
||||
# pending,靠章节状态判完成会复活死循环)。
|
||||
# 门禁语义:链激活时 C 段只挡 technical 章(_tech_gate_on),商务/报价章照常
|
||||
# 流转——HT_TEMPLATE_CONFIRM 已从全局阻塞降级为章节级依赖(bid_common)。
|
||||
_tech_over_active = False
|
||||
_tech_gate_on = False
|
||||
if n_files > 0 and chapters and _stage_on(STAGE_TECH_ANALYSIS) \
|
||||
and _stage_on(STAGE_TEMPLATE_CONFIRM):
|
||||
_tech_chs = [c for c in chapters
|
||||
if (c.get("section") or "technical") == "technical"]
|
||||
_n_summary = await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM bid_tech_items "
|
||||
"WHERE project_id=${p}$ AND kind='summary'", {"p": project_id})
|
||||
_qc_eff = await _qc_passed_eff()
|
||||
_chain_open = bool(_tech_chs) and _n_summary == 0 \
|
||||
and all(c.get("status") == CH_PENDING for c in _tech_chs) \
|
||||
and QC_TYPE_OUTLINE in _qc_eff
|
||||
if _chain_open:
|
||||
_tech_gate_on = True # C 段技术章门禁:等模版确认完成
|
||||
_n_ti = await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM bid_tech_items "
|
||||
"WHERE project_id=${p}$ AND kind='item'", {"p": project_id})
|
||||
_ti_qc = await _tech_qc_latest(sor, project_id)
|
||||
_tq_passed, _tech_over_active, _tblocked = await _reconcile_tech_analysis_qc(
|
||||
sor, project_id, pname, _n_ti, _ti_qc, th, _stage_on,
|
||||
FLOW_KEY_STANDARD, acts)
|
||||
if _tq_passed and not _tblocked:
|
||||
await _reconcile_template_confirm(
|
||||
sor, project_id, pname, FLOW_KEY_STANDARD, acts)
|
||||
|
||||
# ── A2b. qc_escalation 待办自动关闭(2026-09-16 甘肃项目实测 bug)──
|
||||
# 升级待办发出后,人工修产出/记录页强制放行/重做通过 → 轮次用尽状态消失
|
||||
# (_over_active=False),但 pending 的 qc_escalation 待办无人关闭就永远挂着,
|
||||
# 角标常驻误导「还有事没办」。逃逸阀闭环的另一半:问题解决 → 待办自动收。
|
||||
if not _over_active:
|
||||
# 2026-09-17:tech_items 轮次用尽(A3 段)同样算「仍活跃」——本段只遍历
|
||||
# QC_TYPES 五类,缺 _tech_over_active 守卫会把技术评估项的升级待办秒关。
|
||||
if not _over_active and not _tech_over_active:
|
||||
_ht_esc = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||
if _ht_esc:
|
||||
await sor.sqlExe(
|
||||
@ -1199,6 +1255,12 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
role = R_BIZ_WRITER if section == "business" else R_WRITER
|
||||
if await count_open_tasks(sor, project_id, role=role, chapter_id=c["id"]) > 0:
|
||||
continue
|
||||
# 技术章模版裁剪门禁(2026-09-17):A3 段链激活时,技术章等「模版确认
|
||||
# 完成、骨架已替换」才开写(confirm_tech_template 标准模式会替换 technical
|
||||
# 章——提前开写的内容会被替换毁掉);商务/报价章不受影响照常流转。
|
||||
if section == "technical" and _tech_gate_on:
|
||||
waiting_deps.setdefault("tech_template_confirm", []).append(str(c.get("chapter_no")))
|
||||
continue
|
||||
# 上游依赖门禁:该 section 依赖的所有产出类型最新一轮必须已通过契合度审核
|
||||
deps = CH_SECTION_DEPS.get(section, CH_SECTION_DEPS["technical"])
|
||||
unmet = [t for t in deps if t not in qc_passed]
|
||||
@ -1448,6 +1510,120 @@ async def _open_tech_tasks(sor, project_id, task_kind):
|
||||
return to_int(getattr(recs[0], "c", 0) if recs else 0)
|
||||
|
||||
|
||||
async def _reconcile_tech_analysis_qc(sor, project_id, pname, n_items, latest_qc,
|
||||
th, stage_on, flow_key, acts):
|
||||
"""技术评估项抽取(S1)+ QC 审核(S2)共用段。
|
||||
|
||||
2026-09-17 起两条流程同源:纯技术方案分支 T1/T2 与标准流程 A3 段都调本函数,
|
||||
逻辑只此一份(flow_key 写进任务 params 供任务树/审计区分来源)。
|
||||
返回 (qc_passed, over_active, blocked):
|
||||
- qc_passed:tech_items 最新一轮 QC 通过,或 tech_qc 阶段被裁(视为通过);
|
||||
- over_active:轮次用尽等人工(标准流程 A2b 自动关闭 qc_escalation 待办的守卫,
|
||||
缺它会把技术评估项的升级待办秒关——A2b 只看 QC_TYPES 五类);
|
||||
- blocked:本轮链路已有动作(派发/在办/升级),调用方不得继续模版段。
|
||||
"""
|
||||
if n_items == 0:
|
||||
if not stage_on(STAGE_TECH_ANALYSIS):
|
||||
return False, False, False # 抽取被裁且无产出:链路无依据,静默跳过
|
||||
if await _open_tech_tasks(sor, project_id, "bid_tech_analysis") > 0:
|
||||
acts.append("waiting: 技术评估项抽取任务在办")
|
||||
return False, False, True
|
||||
qc_imp = (latest_qc.get("improvement") or "") if (
|
||||
latest_qc and str(latest_qc.get("passed")) != "1") else ""
|
||||
attempts = await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM pipeline_tasks WHERE tenant_id=${p}$ "
|
||||
"AND pipeline_id='role_task' AND role=${r}$ AND state IN "
|
||||
"('approved','completed','qc_rejected') AND params LIKE ${kw}$",
|
||||
{"p": project_id, "r": R_ANALYST, "kw": '%bid_tech_analysis%'})
|
||||
if attempts >= MAX_ANALYST_ATTEMPT + th["qc_max_round"]:
|
||||
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, "general",
|
||||
"技术评估项解析多次未产出,需人工介入:%s" % pname,
|
||||
("技术评估项解析任务已执行 %d 次,产出仍为空。\n"
|
||||
"请检查招标文件技术部分是否可读(bid_tender_files.content_text),"
|
||||
"或用 list_tech_items/add_tech_item 人工补录评估项。" % attempts),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("escalated: 技术评估项解析重试超限 → 人工介入")
|
||||
return False, False, True
|
||||
tparams = {"stage": "tech_analysis", "task_kind": "bid_tech_analysis",
|
||||
"analysis_dim": TECH_KIND, "flow_key": flow_key}
|
||||
if qc_imp:
|
||||
tparams["qc_redo"] = 1
|
||||
tparams["qc_improvements"] = ["[tech_items] %s" % qc_imp[:2000]]
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_ANALYST,
|
||||
"%s 技术评估项抽取(六类%s)" % (pname, ",QC退回重做" if qc_imp else ""),
|
||||
tparams)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("created: 技术评估项抽取任务 %s" % tid)
|
||||
return False, False, True
|
||||
|
||||
if not stage_on(STAGE_TECH_QC):
|
||||
return True, False, False # QC 被裁 → 视为通过(用户已确认放弃该门禁)
|
||||
if latest_qc and str(latest_qc.get("passed")) == "1":
|
||||
return True, False, False
|
||||
|
||||
rnd = to_int(latest_qc.get("round"), 0) if latest_qc else 0
|
||||
if latest_qc and rnd >= th["qc_max_round"]:
|
||||
# 轮次用尽:产出未更新则不空转,抛人工(同标准流程逃逸阀)
|
||||
ht = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, HT_QC_ESCALATION,
|
||||
"技术评估项 QC 连续不达标需人工介入",
|
||||
("「tech_items」已审核 %d 轮仍未达标(最近改进意见:%s)。\n\n"
|
||||
"请人工核对招标文件技术需求修正评估项(list_tech_items/add_tech_item),"
|
||||
"或在 QC 审核记录页把该记录 passed 改为 1 强制放行。"
|
||||
% (rnd, (latest_qc.get("improvement") or "")[:1500])),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("escalated: 技术评估项 QC 轮次用尽 → 人工介入")
|
||||
return False, True, True
|
||||
if await _open_tech_tasks(sor, project_id, "bid_qc") > 0:
|
||||
acts.append("waiting: 技术评估项 QC 任务在办")
|
||||
return False, False, True
|
||||
# 竞态守卫:抽取/重做任务在办 → 不派审核(防对半成品打分)
|
||||
if await _open_tech_tasks(sor, project_id, "bid_tech_analysis") > 0:
|
||||
acts.append("waiting: 技术评估项抽取任务在办,暂不派 tech QC")
|
||||
return False, False, True
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_QC,
|
||||
"%s 技术评估项契合度审核(对标招标技术需求原文)" % pname,
|
||||
{"stage": "tech_qc", "task_kind": "bid_qc", "qc_types": [QC_TYPE_TECH],
|
||||
"flow_key": flow_key})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("created: 技术评估项 QC 任务 %s" % tid)
|
||||
return False, False, True
|
||||
|
||||
|
||||
async def _reconcile_template_confirm(sor, project_id, pname, flow_key, acts):
|
||||
"""模版裁剪提案段(S3,两条流程同源):无 pending 待办且无在办任务时派 PM
|
||||
检索模版→裁剪技术章骨架→propose_tech_template 发用户确认待办(带驳回意见修订)。
|
||||
返回 blocked(本轮有动作/等待中)。确认动作是用户专属(tech_template_confirm.dspy)。
|
||||
"""
|
||||
ht_pend = await find_human_task(sor, project_id, HT_TEMPLATE_CONFIRM, status="pending")
|
||||
if ht_pend:
|
||||
acts.append("waiting: 技术章骨架待用户确认")
|
||||
return True
|
||||
if await _open_tech_tasks(sor, project_id, "tech_template") > 0:
|
||||
acts.append("waiting: 模版/骨架拟定任务在办")
|
||||
return True
|
||||
from .bid_tech_capability import get_template_reject_comment
|
||||
rej = await get_template_reject_comment(sor, project_id)
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_PM,
|
||||
"%s 拟定技术方案模版骨架%s" % (pname, "(按驳回意见修订)" if rej else ""),
|
||||
{"stage": "template_confirm", "task_kind": "tech_template",
|
||||
"flow_key": flow_key,
|
||||
**({"reject_comment": rej[:2000]} if rej else {})})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("created: 模版骨架任务 %s(PM 检索模版→按评估项裁剪→发用户确认)" % tid)
|
||||
return True
|
||||
|
||||
|
||||
async def _reconcile_tech_proposal(sor, project_id, pname, stage_table):
|
||||
"""纯技术方案流程对账(幂等)。stage_table=已确认计划的阶段启用表。"""
|
||||
acts = []
|
||||
@ -1474,97 +1650,23 @@ async def _reconcile_tech_proposal(sor, project_id, pname, stage_table):
|
||||
# tech_qc 被裁 → T2 段的 _on(STAGE_TECH_QC) 判定即视为通过(用户已在确认待办里放弃该门禁),
|
||||
# 不再单独算 qc_eff 死变量(2026-09-15 清理:原变量算了未被引用)。
|
||||
|
||||
# ── T1. 技术需求解析(六类评估项)──
|
||||
if n_items == 0 and _on(STAGE_TECH_ANALYSIS):
|
||||
if await _open_tech_tasks(sor, project_id, "bid_tech_analysis") > 0:
|
||||
acts.append("waiting: 技术需求解析任务在办")
|
||||
else:
|
||||
qc_imp = (latest_qc.get("improvement") or "") if (latest_qc and str(latest_qc.get("passed")) != "1") else ""
|
||||
attempts = await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM pipeline_tasks WHERE tenant_id=${p}$ "
|
||||
"AND pipeline_id='role_task' AND role=${r}$ AND state IN "
|
||||
"('approved','completed','qc_rejected') AND params LIKE ${kw}$",
|
||||
{"p": project_id, "r": R_ANALYST, "kw": '%bid_tech_analysis%'})
|
||||
if attempts >= MAX_ANALYST_ATTEMPT + th["qc_max_round"]:
|
||||
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, "general",
|
||||
"技术需求解析多次未产出,需人工介入:%s" % pname,
|
||||
("技术评估项解析任务已执行 %d 次,产出仍为空。\n"
|
||||
"请检查需求书是否可读(bid_tender_files.content_text),"
|
||||
"或用 list_tech_items/add_tech_item 人工补录评估项。" % attempts),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("escalated: 技术需求解析重试超限 → 人工介入")
|
||||
else:
|
||||
tparams = {"stage": "tech_analysis", "task_kind": "bid_tech_analysis",
|
||||
"analysis_dim": TECH_KIND, "flow_key": FLOW_KEY_TECH_PROPOSAL}
|
||||
if qc_imp:
|
||||
tparams["qc_redo"] = 1
|
||||
tparams["qc_improvements"] = ["[tech_items] %s" % qc_imp[:2000]]
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_ANALYST,
|
||||
"%s 技术需求解析(六类评估项%s)" % (pname, ",QC退回重做" if qc_imp else ""),
|
||||
tparams)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("created: 技术需求解析任务 %s" % tid)
|
||||
return acts or ["idle: 技术需求解析中"]
|
||||
# ── T1/T2. 技术需求解析 + QC(共用段,2026-09-17 与标准流程 A3 同源)──
|
||||
qc_passed, _tech_over, _blocked = await _reconcile_tech_analysis_qc(
|
||||
sor, project_id, pname, n_items, latest_qc, th, _on,
|
||||
FLOW_KEY_TECH_PROPOSAL, acts)
|
||||
if not qc_passed:
|
||||
return acts or ["idle: 技术需求解析/审核中"]
|
||||
if _blocked:
|
||||
return acts
|
||||
|
||||
# ── T2. 解析产出 QC(对标需求书原文,qc_type=tech_items)──
|
||||
if _on(STAGE_TECH_QC) and n_items > 0 and not qc_passed:
|
||||
rnd = to_int(latest_qc.get("round"), 0) if latest_qc else 0
|
||||
if latest_qc and rnd >= th["qc_max_round"] and str(latest_qc.get("passed")) != "1":
|
||||
# 轮次用尽:产出未更新则不空转,抛人工(同标准流程逃逸阀)
|
||||
ht = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, HT_QC_ESCALATION,
|
||||
"技术评估项 QC 连续不达标需人工介入",
|
||||
("「tech_items」已审核 %d 轮仍未达标(最近改进意见:%s)。\n\n"
|
||||
"请人工核对技术需求书修正评估项(list_tech_items/add_tech_item),"
|
||||
"或在 QC 审核记录页把该记录 passed 改为 1 强制放行。"
|
||||
% (rnd, (latest_qc.get("improvement") or "")[:1500])),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("escalated: 技术评估项 QC 轮次用尽 → 人工介入")
|
||||
return acts
|
||||
if await _open_tech_tasks(sor, project_id, "bid_qc") > 0:
|
||||
acts.append("waiting: 技术评估项 QC 任务在办")
|
||||
return acts
|
||||
# 竞态守卫:解析/重做任务在办 → 不派审核(防对半成品打分)
|
||||
if await _open_tech_tasks(sor, project_id, "bid_tech_analysis") > 0:
|
||||
acts.append("waiting: 解析任务在办,暂不派 tech QC")
|
||||
return acts
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_QC,
|
||||
"%s 技术评估项契合度审核(对标需求书原文)" % pname,
|
||||
{"stage": "tech_qc", "task_kind": "bid_qc", "qc_types": [QC_TYPE_TECH],
|
||||
"flow_key": FLOW_KEY_TECH_PROPOSAL})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 技术评估项 QC 任务 %s" % tid]
|
||||
|
||||
# ── T3. 模版确定 + 用户确认骨架 ──
|
||||
# ── T3. 模版确定 + 用户确认骨架(共用段)──
|
||||
if not chapters:
|
||||
if not _on(STAGE_TEMPLATE_CONFIRM):
|
||||
# 被裁(trim=no 理论上裁不掉,防御):无人确认不得开写
|
||||
return acts + ["blocked: 模版确认阶段被裁但骨架缺失,无法开写章节"]
|
||||
# 阻塞门禁:pending 的模版确认待办在 0b 已全局阻塞,走到这里只处理「未发/被驳回」
|
||||
ht_pend = await find_human_task(sor, project_id, HT_TEMPLATE_CONFIRM, status="pending")
|
||||
if ht_pend:
|
||||
return acts + ["waiting: 技术方案骨架待用户确认"]
|
||||
if await _open_tech_tasks(sor, project_id, "tech_template") > 0:
|
||||
return acts + ["waiting: 模版/骨架拟定任务在办"]
|
||||
from .bid_tech_capability import get_template_reject_comment
|
||||
rej = await get_template_reject_comment(sor, project_id)
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_PM,
|
||||
"%s 拟定技术方案模版骨架%s" % (pname, "(按驳回意见修订)" if rej else ""),
|
||||
{"stage": "template_confirm", "task_kind": "tech_template",
|
||||
"flow_key": FLOW_KEY_TECH_PROPOSAL,
|
||||
**({"reject_comment": rej[:2000]} if rej else {})})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 模版骨架任务 %s(PM 检索模版→拟骨架→发用户确认)" % tid]
|
||||
await _reconcile_template_confirm(sor, project_id, pname,
|
||||
FLOW_KEY_TECH_PROPOSAL, acts)
|
||||
return acts or ["idle: 模版裁剪提案中"]
|
||||
|
||||
# ── T4. 分章编写(骨架已确认;一章一任务并发派发)──
|
||||
created_w = 0
|
||||
|
||||
@ -472,26 +472,72 @@ async def confirm_tech_template(human_task_id, decision, comment="",
|
||||
chapters = schema.get("chapters") or []
|
||||
if not chapters:
|
||||
return False, "待办里没有骨架数据(form_schema.chapters 空),无法落库"
|
||||
# 幂等重落守卫:仅当既有章节全部未开写(pending)时清旧骨架——
|
||||
# 标准/纯技术方案双模式分流(2026-09-17 裁剪链挂标准流程):
|
||||
# 项目存在非 technical 章节(商务/报价章)= 标准投标流程 → 只替换
|
||||
# technical 章(新骨架落原位 order_no,商务/报价章原样保留);
|
||||
# 纯技术方案流程无其他章节 → 整表清落(原语义不变)。
|
||||
_all = await sor.sqlExe(
|
||||
"SELECT id, section, status, order_no FROM bid_chapters "
|
||||
"WHERE project_id=${p}$", {"p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
_rows = rows_to_dicts(_all, limit=500)
|
||||
_tech_rows = [r for r in _rows
|
||||
if (r.get("section") or "technical") == "technical"]
|
||||
_other_rows = [r for r in _rows if r not in _tech_rows]
|
||||
std_mode = bool(_other_rows)
|
||||
# 幂等重落守卫:仅当待替换章节全部未开写(pending)时清旧骨架——
|
||||
# 已有 writing/written/approved 章节说明流程已推进,重新确认会毁掉
|
||||
# 已写内容,拒绝并提示走驳回/人工处置(2026-09-15 加固)。
|
||||
busy = await sor.sqlExe(
|
||||
"SELECT COUNT(*) AS c FROM bid_chapters WHERE project_id=${p}$ "
|
||||
"AND status<>${s}$", {"p": project_id, "s": CH_PENDING})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if to_int(getattr(busy[0], "c", 0) if busy else 0) > 0:
|
||||
return False, ("项目已有开写/已完成的章节,不能重新确认骨架(会毁掉已写内容)。"
|
||||
"如需换骨架请先驳回或在章节管理页人工处置既有章节。")
|
||||
_scope = _tech_rows if std_mode else _rows
|
||||
if any(str(r.get("status")) != CH_PENDING for r in _scope):
|
||||
return False, ("项目已有开写/已完成的%s章节,不能重新确认骨架(会毁掉已写内容)。"
|
||||
"如需换骨架请先驳回或在章节管理页人工处置既有章节。"
|
||||
% ("技术" if std_mode else ""))
|
||||
# 幂等:清掉此前确认落库但尚未开写的旧骨架(驳回重提后再确认的场景)
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM bid_chapters WHERE project_id=${p}$", {"p": project_id})
|
||||
if std_mode:
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM bid_chapters WHERE project_id=${p}$ "
|
||||
"AND (section='technical' OR section IS NULL OR section='')",
|
||||
{"p": project_id})
|
||||
else:
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM bid_chapters WHERE project_id=${p}$", {"p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
# 标准模式:新骨架插回原技术章位置,全序列重排 order_no=1..N——
|
||||
# 不能只按 min(order_no)+idx 顺延:新骨架章数多于原技术章时会与排在
|
||||
# 技术部分之后的商务/报价章撞号,合成 ORDER BY order_no 会把报价章
|
||||
# 插进技术章中间(2026-09-17 状态机走查发现的边界缺陷)。
|
||||
_seq = [] # [(row_or_None, new_order)],None=新章占位
|
||||
if std_mode:
|
||||
_others = sorted(_other_rows, key=lambda r: to_int(r.get("order_no"), 0))
|
||||
_tnums = [to_int(r.get("order_no"), 0) for r in _tech_rows
|
||||
if to_int(r.get("order_no"), 0) > 0]
|
||||
_ins = len(_others)
|
||||
if _tnums:
|
||||
_tmin = min(_tnums)
|
||||
_ins = sum(1 for r in _others
|
||||
if to_int(r.get("order_no"), 0) < _tmin)
|
||||
_new_chs = [ch for ch in chapters if isinstance(ch, dict)]
|
||||
_seq = ([(r, None) for r in _others[:_ins]]
|
||||
+ [(None, ch) for ch in _new_chs]
|
||||
+ [(r, None) for r in _others[_ins:]])
|
||||
else:
|
||||
_seq = [(None, ch) for ch in chapters if isinstance(ch, dict)]
|
||||
n = 0
|
||||
for idx, ch in enumerate(chapters):
|
||||
for idx, ent in enumerate(_seq):
|
||||
row, ch = ent
|
||||
if row is not None:
|
||||
_no = idx + 1
|
||||
if to_int(row.get("order_no"), 0) != _no:
|
||||
await sor.sqlExe(
|
||||
"UPDATE bid_chapters SET order_no=${o}$ WHERE id=${i}$",
|
||||
{"o": _no, "i": row.get("id")})
|
||||
continue
|
||||
if not isinstance(ch, dict):
|
||||
continue
|
||||
await sor.C("bid_chapters", {
|
||||
"id": new_id(), "project_id": project_id,
|
||||
"chapter_no": str(ch.get("chapter_no") or (idx + 1))[:32],
|
||||
"chapter_no": str(ch.get("chapter_no") or (n + 1))[:32],
|
||||
"title": str(ch.get("title") or "")[:255],
|
||||
"section": "technical",
|
||||
"outline": str(ch.get("outline") or "")[:60000],
|
||||
@ -499,20 +545,36 @@ async def confirm_tech_template(human_task_id, decision, comment="",
|
||||
"order_no": idx + 1,
|
||||
})
|
||||
n += 1
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
# 裁剪摘要落库(kind='summary',list_tech_items 默认 kind='item' 不污染
|
||||
# 评估项清单):写者/QC 可追溯「技术章骨架按哪六类评估项裁剪、模版来源」,
|
||||
# 也是 A3 段链激活过的持久证据(audit_log 之外的产出物侧记录)。
|
||||
try:
|
||||
await record_tech_summary(
|
||||
sor, project_id, n,
|
||||
template_source=str(schema.get("template_source") or ""),
|
||||
template_ref=str(schema.get("template_ref") or ""))
|
||||
except Exception as _e: # 摘要失败不阻塞确认主流程
|
||||
logger.warning("record_tech_summary failed project=%s: %s",
|
||||
project_id, str(_e)[:160])
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_human_tasks SET status='done', qc_status='passed', "
|
||||
"qc_comment='用户已确认技术方案骨架', result_data=${rd}$, "
|
||||
"qc_comment=${cm}$, result_data=${rd}$, "
|
||||
"submitted_by=${u}$, submitted_at=NOW() WHERE id=${h}$",
|
||||
{"rd": json.dumps({"decision": "confirm", "chapters": n}, ensure_ascii=False),
|
||||
{"cm": "用户已确认技术方案骨架(标准流程:只替换技术章)" if std_mode
|
||||
else "用户已确认技术方案骨架",
|
||||
"rd": json.dumps({"decision": "confirm", "chapters": n,
|
||||
"std_mode": std_mode}, ensure_ascii=False),
|
||||
"u": operator_id, "h": human_task_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, project_id, "bid_chapters", project_id, "template_confirmed",
|
||||
to_state="pending", who=operator_id,
|
||||
detail="骨架落库 %d 章" % n)
|
||||
detail="骨架落库 %d 章%s" % (n, "(标准流程,替换技术章)" if std_mode else ""))
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
logger.info("tech_template confirmed: project=%s chapters=%d by=%s",
|
||||
project_id, n, operator_id)
|
||||
return True, "骨架已确认并落库(%d 章),系统将自动开始分章编写。" % n
|
||||
logger.info("tech_template confirmed: project=%s chapters=%d std_mode=%s by=%s",
|
||||
project_id, n, std_mode, operator_id)
|
||||
return True, ("骨架已确认并落库(%d 章技术章%s),系统将自动开始分章编写。"
|
||||
% (n, ",商务/报价章未动" if std_mode else ""))
|
||||
|
||||
# reject
|
||||
await sor.sqlExe(
|
||||
@ -538,3 +600,44 @@ async def get_template_reject_comment(sor, project_id):
|
||||
if not isinstance(rd, dict):
|
||||
return ""
|
||||
return str(rd.get("comment") or "")
|
||||
|
||||
|
||||
async def record_tech_summary(sor, project_id, n_chapters, template_source="",
|
||||
template_ref=""):
|
||||
"""技术章骨架裁剪摘要落库(bid_tech_items kind='summary',幂等覆盖)。
|
||||
|
||||
2026-09-17 裁剪链挂标准流程:用户确认骨架后把「按哪六类评估项裁剪、模版来源、
|
||||
骨架章数」写成摘要行——写者/QC/审计可追溯技术方案部分的裁剪依据(list_tech_items
|
||||
默认 kind='item',摘要不污染评估项清单)。失败由调用方捕获,不阻塞确认主流程。
|
||||
"""
|
||||
crecs = await sor.sqlExe(
|
||||
"SELECT category, COUNT(*) AS c FROM " + TABLE +
|
||||
" WHERE project_id=${p}$ AND kind='item' GROUP BY category", {"p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
cats = {str(getattr(r, "category", "") or ""): to_int(getattr(r, "c", 0), 0)
|
||||
for r in (crecs or [])}
|
||||
lines = ["六类评估项覆盖:" + ("、".join(
|
||||
"%s%d项" % (TECH_CATEGORY_LABELS.get(k, k), v)
|
||||
for k, v in sorted(cats.items()) if v) or "(无)")]
|
||||
if template_source:
|
||||
lines.append("模版来源:%s%s" % (
|
||||
{"kb": "公司知识库预定模版", "rag": "RAG 知识库检索",
|
||||
"web": "网络检索", "manual": "按行业惯例自拟"}.get(
|
||||
template_source, template_source),
|
||||
"(%s)" % template_ref[:200] if template_ref else ""))
|
||||
lines.append("用户确认骨架:%d 章技术章(section=technical),裁剪链完成。" % n_chapters)
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM " + TABLE + " WHERE project_id=${p}$ AND kind='summary'",
|
||||
{"p": project_id})
|
||||
sid = new_id()
|
||||
await sor.C(TABLE, {
|
||||
"id": sid, "project_id": project_id, "kind": "summary",
|
||||
"category": "", "item_name": "技术章骨架裁剪摘要",
|
||||
"requirement": "\n".join(lines)[:20000],
|
||||
"confidence": "high", "source_ref": "template_confirm", "order_no": 0,
|
||||
})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, project_id, TABLE, sid, "record_tech_summary",
|
||||
who="system", detail="%d章 source=%s" % (n_chapters, template_source or "-"))
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return sid
|
||||
|
||||
@ -130,6 +130,24 @@ async def chapter_detail(chapter_id, with_content="1", length=12000):
|
||||
"doc_requirements": reqs,
|
||||
"doc_requirements_binding": "explicit" if rq_bound else "fallback_by_no",
|
||||
}
|
||||
# ── 技术方案部分:裁剪摘要注入(2026-09-17 裁剪链挂标准流程)──
|
||||
# 技术章的骨架是按六类技术评估项裁剪出来的——写者必须知道裁剪依据才能
|
||||
# 对标编写(评估项=招标技术需求原文摘录,是本章内容的第一基准)。
|
||||
if (d.get("section") or "technical") == "technical":
|
||||
try:
|
||||
_sm = await sor.sqlExe(
|
||||
"SELECT requirement FROM bid_tech_items WHERE project_id=${pid}$ "
|
||||
"AND kind='summary' ORDER BY created_at DESC LIMIT 1",
|
||||
{"pid": d.get("project_id")})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if _sm:
|
||||
out["tech_summary"] = str(getattr(_sm[0], "requirement", "") or "")[:2000]
|
||||
out["tech_items_hint"] = ("本章骨架由技术标模版按招标技术需求裁剪而来——"
|
||||
"编写时用 list_tech_items 查六类技术评估项"
|
||||
"(功能清单/功能点/硬件配置/技术架构/功能架构/"
|
||||
"部署架构),逐项对标响应,漏项=方案书必漏需求")
|
||||
except Exception:
|
||||
pass # 表不存在(老库未迁移)等场景静默降级,不阻塞写者
|
||||
if rq_warn:
|
||||
out["doc_requirements_warning"] = rq_warn
|
||||
if str(with_content) in ("1", "true", "True"):
|
||||
|
||||
@ -181,9 +181,10 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
dim_labels = {
|
||||
'scoring': '评分项与得分规则', 'quals': '资质清单与要求',
|
||||
'reqs_outline': '投标文件要求与章节骨架', 'cost_benefit': '成本收益分析',
|
||||
'tech': '技术评估项(技术方案裁剪依据)',
|
||||
'__legacy__': '全量解析(维度拆分前)',
|
||||
}
|
||||
order = ['scoring', 'quals', 'reqs_outline', 'cost_benefit', '__legacy__']
|
||||
order = ['scoring', 'quals', 'reqs_outline', 'cost_benefit', 'tech', '__legacy__']
|
||||
nodes = []
|
||||
for key in order + [k for k in groups if k not in order]:
|
||||
if key not in groups:
|
||||
@ -262,11 +263,12 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
qc_labels = {
|
||||
'scoring_items': '评分项 QC', 'qualifications': '资质 QC',
|
||||
'doc_requirements': '投标文件要求 QC', 'chapter_outline': '章节骨架 QC',
|
||||
'cost_benefit': '成本收益分析 QC',
|
||||
'cost_benefit': '成本收益分析 QC', 'tech_items': '技术评估项 QC',
|
||||
'__none__': 'QC 审核', '__legacy__': 'QC 审核(早期)',
|
||||
}
|
||||
order = ['scoring_items', 'qualifications', 'doc_requirements',
|
||||
'chapter_outline', 'cost_benefit', '__none__', '__legacy__']
|
||||
'chapter_outline', 'cost_benefit', 'tech_items',
|
||||
'__none__', '__legacy__']
|
||||
nodes = []
|
||||
for key in order + [k for k in groups if k not in order]:
|
||||
if key not in groups:
|
||||
@ -428,7 +430,7 @@ async with DBPools().sqlorContext(dbname) as sor:
|
||||
# reqs_outline 维度含两类审核记录,合并取每轮最大
|
||||
DIM_QCS = {'scoring': ('scoring_items',), 'quals': ('qualifications',),
|
||||
'reqs_outline': ('doc_requirements', 'chapter_outline'),
|
||||
'cost_benefit': ('cost_benefit',)}
|
||||
'cost_benefit': ('cost_benefit',), 'tech': ('tech_items',)}
|
||||
rev_by_round = {}
|
||||
for qt in DIM_QCS.get(key, ()):
|
||||
for rv in await sor.sqlExe(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user