feat: 章节级依赖门禁——每章只等自己section依赖的QC类型通过即派发(矩阵:技术=评分项+文件要求+骨架/报价=评分项+成本收益+骨架/商务=资质+文件要求+骨架+资料准备);A2全局门禁拆为推进QC不挡下游;create_role_task统一注入skip_generic_qc豁免通用门禁
This commit is contained in:
parent
6c4aaa79a2
commit
d66207032c
@ -38,7 +38,10 @@ HT_HUMAN_DOCS = "human_docs_request"
|
|||||||
HT_QC_ESCALATION = "qc_escalation"
|
HT_QC_ESCALATION = "qc_escalation"
|
||||||
HT_DELIVERY_CONFIRM = "bid_delivery_confirm"
|
HT_DELIVERY_CONFIRM = "bid_delivery_confirm"
|
||||||
|
|
||||||
BLOCKING_HT_TYPES = (HT_TENDER_FILE, HT_HUMAN_DOCS, HT_QC_ESCALATION)
|
BLOCKING_HT_TYPES = (HT_TENDER_FILE,)
|
||||||
|
# 2026-09-03 起只保留「等招标文件」为全局阻塞:
|
||||||
|
# - HT_HUMAN_DOCS(等人类资质文件)降级为章节级依赖——只阻塞商务章(等资料准备),技术/报价章不受影响;
|
||||||
|
# - HT_QC_ESCALATION(QC 轮次用尽)降级为章节级依赖——只阻塞依赖该产出类型的章节,其余章节照常流转。
|
||||||
|
|
||||||
# ── QC 契合度审核类型(解析产出逐类审核)──
|
# ── QC 契合度审核类型(解析产出逐类审核)──
|
||||||
QC_TYPE_SCORING = "scoring_items" # 评分项 + 得分规则
|
QC_TYPE_SCORING = "scoring_items" # 评分项 + 得分规则
|
||||||
@ -249,13 +252,17 @@ async def create_role_task(sor, project_id, role, title, params=None, owner_id="
|
|||||||
与开发产线共用同一套任务表和 poller —— 投标产线不另建任务机制。
|
与开发产线共用同一套任务表和 poller —— 投标产线不另建任务机制。
|
||||||
"""
|
"""
|
||||||
tid = new_id()
|
tid = new_id()
|
||||||
|
params = dict(params or {})
|
||||||
|
# 豁免声明(2026-09-03):投标产线所有角色任务自带质量门禁
|
||||||
|
# (解析产出契合度审核 / 章节评审打分),交付后不走引擎通用 QC/PM 门禁。
|
||||||
|
params.setdefault("skip_generic_qc", True)
|
||||||
await sor.C("pipeline_tasks", {
|
await sor.C("pipeline_tasks", {
|
||||||
"id": tid,
|
"id": tid,
|
||||||
"tenant_id": project_id,
|
"tenant_id": project_id,
|
||||||
"pipeline_id": "role_task",
|
"pipeline_id": "role_task",
|
||||||
"owner_id": owner_id,
|
"owner_id": owner_id,
|
||||||
"title": (title or "")[:250],
|
"title": (title or "")[:250],
|
||||||
"params": json.dumps(params or {}, ensure_ascii=False),
|
"params": json.dumps(params, ensure_ascii=False),
|
||||||
"role": normalize_role(role),
|
"role": normalize_role(role),
|
||||||
"state": "submitted",
|
"state": "submitted",
|
||||||
"claimed_by": None,
|
"claimed_by": None,
|
||||||
|
|||||||
@ -88,6 +88,33 @@ ANALYSIS_DIMS = (
|
|||||||
DIM_QC_TYPES = {d: ts for d, _, ts in ANALYSIS_DIMS}
|
DIM_QC_TYPES = {d: ts for d, _, ts in ANALYSIS_DIMS}
|
||||||
QC_TYPE_TO_DIM = {q: d for d, _, ts in ANALYSIS_DIMS for q in ts}
|
QC_TYPE_TO_DIM = {q: d for d, _, ts in ANALYSIS_DIMS for q in ts}
|
||||||
|
|
||||||
|
# 章节级上游依赖矩阵(2026-09-03):每种章节 section 只依赖自己必需的分析产出
|
||||||
|
# 通过 QC——某一维度 QC 卡住只阻塞依赖它的章节,其余章节照常流转。
|
||||||
|
# 公共事实:章节骨架(bid_chapters)本身就是章节,OUTLINE 过了章节才可信;
|
||||||
|
# REQS(投标文件格式要求)决定章节怎么写,所有章节都要。
|
||||||
|
CH_SECTION_DEPS = {
|
||||||
|
"technical": (QC_TYPE_SCORING, QC_TYPE_REQS, QC_TYPE_OUTLINE),
|
||||||
|
"price": (QC_TYPE_SCORING, QC_TYPE_COST, QC_TYPE_OUTLINE),
|
||||||
|
"business": (QC_TYPE_QUALS, QC_TYPE_REQS, QC_TYPE_OUTLINE),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _qc_passed_types(sor, project_id):
|
||||||
|
"""最新一轮审核已通过(或人工强制放行)的 QC 类型集合。章节级依赖判定用。
|
||||||
|
|
||||||
|
逃逸阀闭环:QC 轮次用尽冒泡人工后,未通过类型不在集合里 → 依赖它的章节被挡;
|
||||||
|
人工在记录页把 passed 改为 1 强制放行后,该类型进集合 → 依赖章节自动解锁,无需改码。
|
||||||
|
"""
|
||||||
|
passed = set()
|
||||||
|
for t in QC_TYPES:
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT passed 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", {})
|
||||||
|
if recs and str(rec_to_dict(recs[0]).get("passed")) == "1":
|
||||||
|
passed.add(t)
|
||||||
|
return passed
|
||||||
|
|
||||||
|
|
||||||
async def _missing_dims(n_items, n_struct, n_quals, n_cost, chapters):
|
async def _missing_dims(n_items, n_struct, n_quals, n_cost, chapters):
|
||||||
"""返回缺失的分析维度 [(dim, label)]。各维度独立判定,缺几个并行派几个。"""
|
"""返回缺失的分析维度 [(dim, label)]。各维度独立判定,缺几个并行派几个。"""
|
||||||
@ -491,16 +518,21 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
created_dims.append(dim)
|
created_dims.append(dim)
|
||||||
if created_dims:
|
if created_dims:
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return acts
|
# 2026-09-03:不再提前 return——同轮继续章节级派发(依赖已满足的章节立即启动)
|
||||||
|
|
||||||
# ── A2. 分析产出 QC 门禁(五个类型逐一审核,每类一个审核任务并行;阈值见 bid_qc_pass_score)──
|
# ── A2. 分析产出 QC 门禁(五个类型逐一审核,每类一个审核任务并行;阈值见 bid_qc_pass_score)──
|
||||||
|
# 2026-09-03 章节级门禁:本段只负责「推进 QC」(重做/审核/冒泡),不再提前 return
|
||||||
|
# 阻塞全局流转。哪些章节可以开写,由 C 段按章节依赖矩阵(CH_SECTION_DEPS)逐章判定——
|
||||||
|
# 某一维度卡住只阻塞依赖它的章节,其余章节照常流转。
|
||||||
qc_pending = await _qc_pending_types(sor, project_id)
|
qc_pending = await _qc_pending_types(sor, project_id)
|
||||||
|
over_types = set()
|
||||||
if qc_pending:
|
if qc_pending:
|
||||||
# 轮次用尽守卫:最新审核已达上限仍未通过 → 不再空转派审核任务,
|
# 轮次用尽守卫:最新审核已达上限仍未通过 → 不再空转派审核任务,
|
||||||
# 确保有阻塞人工任务(逃逸阀:人工修产出物或在记录页强制放行后流程自恢复)。
|
# 确保有阻塞人工任务(逃逸阀:人工修产出物或在记录页强制放行后流程自恢复)。
|
||||||
over = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending
|
over = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending
|
||||||
if s == "rework" and rnd >= th["qc_max_round"]]
|
if s == "rework" and rnd >= th["qc_max_round"]]
|
||||||
if over:
|
if over:
|
||||||
|
over_types = set(t for t, _, _, _ in over)
|
||||||
ht = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
ht = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||||
if not ht:
|
if not ht:
|
||||||
await create_human_task(
|
await create_human_task(
|
||||||
@ -514,8 +546,8 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
for t, s, imp, rnd in over))),
|
for t, s, imp, rnd in over))),
|
||||||
assignee_role="owner.superuser")
|
assignee_role="owner.superuser")
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return acts + ["blocked: QC 审核轮次用尽(%s),等人工介入"
|
acts.append("escalated: QC 审核轮次用尽(%s),等人工介入;不依赖它的章节照常流转"
|
||||||
% "/".join(t for t, _, _, _ in over)]
|
% "/".join(sorted(over_types)))
|
||||||
# awaiting_redo:产出已被 QC 清空 → 重派对应维度的重做任务,不能对空产出审核
|
# awaiting_redo:产出已被 QC 清空 → 重派对应维度的重做任务,不能对空产出审核
|
||||||
needs_redo = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending if s == "awaiting_redo"]
|
needs_redo = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending if s == "awaiting_redo"]
|
||||||
if needs_redo:
|
if needs_redo:
|
||||||
@ -549,10 +581,11 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
acts.append("created: 分析重做任务(%s,产出被 QC 清空)" % dim)
|
acts.append("created: 分析重做任务(%s,产出被 QC 清空)" % dim)
|
||||||
if redo_dims:
|
if redo_dims:
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return acts
|
else:
|
||||||
return acts + ["waiting: 分析重做任务在办(QC 退回产出已清空)"]
|
acts.append("waiting: 分析重做任务在办(QC 退回产出已清空)")
|
||||||
# 逐类型并行审核:每类一个审核任务(有在办的类型跳过)
|
# 逐类型并行审核:每类一个审核任务(跳过:在办的类型 / 空产出待重做 / 轮次用尽等人工)
|
||||||
needs_qc = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending]
|
needs_qc = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending
|
||||||
|
if s == "unaudited" or (s == "rework" and t not in over_types)]
|
||||||
created_qc = []
|
created_qc = []
|
||||||
for t, _, _, _ in needs_qc:
|
for t, _, _, _ in needs_qc:
|
||||||
if await _open_qc_type_tasks(sor, project_id, t) > 0:
|
if await _open_qc_type_tasks(sor, project_id, t) > 0:
|
||||||
@ -572,20 +605,22 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
created_qc.append(t)
|
created_qc.append(t)
|
||||||
if created_qc:
|
if created_qc:
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return acts
|
# 不提前 return:继续往下走章节级派发(依赖已满足的章节立即启动)
|
||||||
return acts + ["waiting: 解析产出 QC 审核任务在办"]
|
|
||||||
|
|
||||||
# ── B. 资料准备(知识库资质匹配 + 人类文件清单)──
|
# ── B. 资料准备(知识库资质匹配 + 人类文件清单)──
|
||||||
|
# 2026-09-03 章节级依赖:资料准备是商务章(business)的上游依赖,
|
||||||
|
# 不再阻塞全局流转——技术/报价章在上游 QC 通过后立即开写。
|
||||||
prep_done = await has_done_task(sor, project_id, R_PREP)
|
prep_done = await has_done_task(sor, project_id, R_PREP)
|
||||||
if n_quals > 0 and not prep_done:
|
if n_quals > 0 and not prep_done:
|
||||||
if await count_open_tasks(sor, project_id, role=R_PREP) > 0:
|
if await count_open_tasks(sor, project_id, role=R_PREP) > 0:
|
||||||
return acts + ["waiting: 资料准备任务在办"]
|
acts.append("waiting: 资料准备任务在办(只阻塞商务章)")
|
||||||
tid = await create_role_task(
|
else:
|
||||||
sor, project_id, R_PREP,
|
tid = await create_role_task(
|
||||||
"%s 投标资料准备(资质匹配 + 人类文件清单)" % pname,
|
sor, project_id, R_PREP,
|
||||||
{"stage": "prep", "task_kind": "bid_prep", "qual_pending": n_quals_pending})
|
"%s 投标资料准备(资质匹配 + 人类文件清单)" % pname,
|
||||||
await sor.sqlExe("COMMIT", {})
|
{"stage": "prep", "task_kind": "bid_prep", "qual_pending": n_quals_pending})
|
||||||
return acts + ["created: 资料准备任务 %s" % tid]
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
acts.append("created: 资料准备任务 %s" % tid)
|
||||||
|
|
||||||
if not chapters:
|
if not chapters:
|
||||||
return acts + ["idle: 无章节,等待解析产出章节骨架"]
|
return acts + ["idle: 无章节,等待解析产出章节骨架"]
|
||||||
@ -593,7 +628,15 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
# ── C. 章节编写(一章一任务、并发派发;pending/rejected 且无在办任务)──
|
# ── C. 章节编写(一章一任务、并发派发;pending/rejected 且无在办任务)──
|
||||||
# 商务/技术分角色:section=business → 商务标写者;其余(technical/price)→ 技术标写者。
|
# 商务/技术分角色:section=business → 商务标写者;其余(technical/price)→ 技术标写者。
|
||||||
# 并发:单轮最多派 write_concurrency 个编写任务(引擎另有每项目并发上限兜底)。
|
# 并发:单轮最多派 write_concurrency 个编写任务(引擎另有每项目并发上限兜底)。
|
||||||
|
# 章节级上游依赖门禁(2026-09-03):每章只等自己 section 依赖的上游产出通过 QC
|
||||||
|
# (CH_SECTION_DEPS),依赖满足立即派发——某一维度卡住只阻塞依赖它的章节,
|
||||||
|
# 其余章节照常流转(根治:此前五类 QC 全过才放行,cost_benefit 一个维度慢
|
||||||
|
# 导致全部章节冻结)。商务章额外等资料准备完成。
|
||||||
|
qc_passed = await _qc_passed_types(sor, project_id)
|
||||||
|
hd_pending = await find_human_task(sor, project_id, HT_HUMAN_DOCS, status="pending")
|
||||||
|
biz_ok = (prep_done or n_quals == 0) and not hd_pending
|
||||||
created_w = 0
|
created_w = 0
|
||||||
|
waiting_deps = {}
|
||||||
for c in chapters:
|
for c in chapters:
|
||||||
if created_w >= th["write_concurrency"]:
|
if created_w >= th["write_concurrency"]:
|
||||||
break
|
break
|
||||||
@ -601,9 +644,19 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
continue
|
continue
|
||||||
if to_int(c.get("revise_count"), 0) > th["max_revise"]:
|
if to_int(c.get("revise_count"), 0) > th["max_revise"]:
|
||||||
continue # 已超重做上限,等人工介入(review 阶段已抛任务)
|
continue # 已超重做上限,等人工介入(review 阶段已抛任务)
|
||||||
role = R_BIZ_WRITER if (c.get("section") or "technical") == "business" else R_WRITER
|
section = c.get("section") or "technical"
|
||||||
|
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:
|
if await count_open_tasks(sor, project_id, role=role, chapter_id=c["id"]) > 0:
|
||||||
continue
|
continue
|
||||||
|
# 上游依赖门禁:该 section 依赖的所有产出类型最新一轮必须已通过契合度审核
|
||||||
|
deps = CH_SECTION_DEPS.get(section, CH_SECTION_DEPS["technical"])
|
||||||
|
unmet = [t for t in deps if t not in qc_passed]
|
||||||
|
if unmet:
|
||||||
|
waiting_deps.setdefault("/".join(sorted(unmet)), []).append(str(c.get("chapter_no")))
|
||||||
|
continue
|
||||||
|
if section == "business" and not biz_ok:
|
||||||
|
waiting_deps.setdefault("bid_prep(资料准备)", []).append(str(c.get("chapter_no")))
|
||||||
|
continue
|
||||||
stage = "revise" if c.get("status") == CH_REJECTED else "write"
|
stage = "revise" if c.get("status") == CH_REJECTED else "write"
|
||||||
tid = await create_role_task(
|
tid = await create_role_task(
|
||||||
sor, project_id, role,
|
sor, project_id, role,
|
||||||
@ -611,10 +664,14 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
"修改重写" if stage == "revise" else
|
"修改重写" if stage == "revise" else
|
||||||
("编写(商务标)" if role == R_BIZ_WRITER else "编写(技术标)")),
|
("编写(商务标)" if role == R_BIZ_WRITER else "编写(技术标)")),
|
||||||
{"stage": stage, "task_kind": "bid_write", "chapter_id": c["id"],
|
{"stage": stage, "task_kind": "bid_write", "chapter_id": c["id"],
|
||||||
"chapter_no": c.get("chapter_no"), "section": c.get("section") or "technical",
|
"chapter_no": c.get("chapter_no"), "section": section,
|
||||||
"revise_count": c.get("revise_count")})
|
"revise_count": c.get("revise_count")})
|
||||||
created_w += 1
|
created_w += 1
|
||||||
acts.append("created: 章节编写任务 %s (%s/%s)" % (tid, c.get("chapter_no"), role))
|
acts.append("created: 章节编写任务 %s (%s/%s)" % (tid, c.get("chapter_no"), role))
|
||||||
|
if waiting_deps:
|
||||||
|
acts.append("waiting: 章节上游依赖未满足:" + "; ".join(
|
||||||
|
"章[%s]等(%s)" % ("/".join(nums), deps)
|
||||||
|
for deps, nums in sorted(waiting_deps.items())))
|
||||||
if created_w:
|
if created_w:
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user