fix(mechanism): 五连修(2026-09-16甘肃项目实测)——①cost_benefit维度补齐list/update/delete三工具(analyst无查看工具→QC退回盲重抽→24行互斥堆积三轮全灭,与2026-09-03 create_chapter_outline只增不改同款缺陷漏修) ②write_chapter支持mode=append分批追加(2.2章403行响应表一次写不进,评审明确要求机制侧开放分段追加) ③bid_flow C0段writing孤儿章节回收(写者任务终态后章节卡writing永久死态,1.9/2.1.4实测卡死11小时;有正文→written派评审,无正文→rejected重派) ④升级待办标题级去重dedup_by_title+C1段三态闭环(类型级dedup吞掉第2~N章升级信号:1/2.1.5/2.2三章达上限只有2.1.7一条待办;人工done后revise_count复位自动重派;信号丢失按标题补发) ⑤review_chapter加material_missing通道(实体材料缺失如实披露后放行+material_supply信息流待办,不再烧写作轮次冻结章节,对齐2026-09-15用户裁定资质=信息流非门禁流)+qc_escalation待办自动关闭(A2b段:轮次用尽类型全部通过/放行后对账器自动收单)
This commit is contained in:
parent
0f23c0d8de
commit
1167371844
@ -113,9 +113,11 @@ load_pipeline_bidding() # import 后必须再显式调用,漏了无任何日
|
||||
|
||||
`load_pipeline_bidding()`(幂等)做三件事:
|
||||
|
||||
1. `role_tool_schemas.register_role_tools()` — 把约 55 个投标角色工具 schema
|
||||
(read_tender_file / extract_* / add_* / patch_chapters / write_chapter /
|
||||
review_chapter / start_qc / finish_qc / compose_bid / start_bid_score /
|
||||
1. `role_tool_schemas.register_role_tools()` — 把约 60 个投标角色工具 schema
|
||||
(read_tender_file / extract_* / add_* / list_cost_benefit / update_cost_benefit_item /
|
||||
delete_cost_benefit_item / patch_chapters / write_chapter(支持 mode=append 分批追加超长章节)/
|
||||
review_chapter(支持 material_missing=1 实体材料缺失如实披露放行)/ start_qc / finish_qc /
|
||||
compose_bid / start_bid_score /
|
||||
finalize_bid_score / dispatch_analysis_dim 等)注册进引擎
|
||||
`pipeline_service.capability_tools.TOOL_SCHEMAS`(引擎 `exec_capability_tool` 的
|
||||
`module` 支持全路径,裸名仍走 `pipeline_service.*`,SDLC 零变化)。
|
||||
|
||||
@ -829,6 +829,112 @@ async def add_cost_benefit_item(project_id, category, item_name, amount="", amou
|
||||
return True, "成本/收益项已录入: %s (%s)" % (item_name, category)
|
||||
|
||||
|
||||
async def list_cost_benefit(project_id):
|
||||
"""列出成本/收益分析全部条目(QC 重做前必读现状,禁止盲重抽)。
|
||||
|
||||
2026-09-16 甘肃项目实测:analyst 没有查看工具 → QC 退回后看不到旧行 →
|
||||
只能整维度重抽 → 新行换名追加、旧行无人删 → 24 行互斥重复堆积,
|
||||
QC 三轮全灭抛人工(与 2026-09-03 create_chapter_outline 只增不改同款缺陷)。
|
||||
"""
|
||||
try:
|
||||
project_id = await resolve_project_id(project_id)
|
||||
except ValueError as e:
|
||||
return "ERROR: 项目上下文缺失(%s)。请显式传 project_id 后重试。" % e
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, kind, category, item_name, amount, amount_note, confidence, "
|
||||
"source_ref, order_no, created_at FROM bid_cost_benefit "
|
||||
"WHERE project_id=${pid}$ ORDER BY kind DESC, order_no, id",
|
||||
{"pid": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out = rows_to_dicts(recs)
|
||||
for r in out:
|
||||
r["source_ref"] = (r.get("source_ref") or "")[:200]
|
||||
r["amount_note"] = (r.get("amount_note") or "")[:200]
|
||||
return json.dumps({"total": len(out), "items": out}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
async def update_cost_benefit_item(project_id, record_id, item_name="", amount="",
|
||||
amount_note="", basis="", confidence="",
|
||||
source_ref="", who=None, agent_id=None):
|
||||
"""修改一条成本/收益项(QC 改进意见要求合并口径/修正引用/补算式时用)。
|
||||
|
||||
只改传入的字段;amount 传空串不改、传 0 改为 0。改前必须先 list_cost_benefit。
|
||||
"""
|
||||
if not record_id:
|
||||
return False, "缺少 record_id(先 list_cost_benefit 查到目标行 id)"
|
||||
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:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT * FROM bid_cost_benefit WHERE id=${i}$ AND project_id=${p}$",
|
||||
{"i": record_id, "p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not recs:
|
||||
return False, "记录不存在或不属于本项目: %s" % record_id
|
||||
old = rec_to_dict(recs[0])
|
||||
upd = {}
|
||||
if item_name:
|
||||
upd["item_name"] = str(item_name)[:250]
|
||||
if str(amount) not in ("", None):
|
||||
upd["amount"] = to_float(amount, 0.0) or None
|
||||
if amount_note:
|
||||
upd["amount_note"] = str(amount_note)[:200]
|
||||
if basis:
|
||||
upd["basis"] = basis
|
||||
if confidence:
|
||||
upd["confidence"] = str(confidence)[:16]
|
||||
if source_ref:
|
||||
upd["source_ref"] = str(source_ref)[:480]
|
||||
if not upd:
|
||||
return False, "没有传入任何要修改的字段"
|
||||
sets = ", ".join("%s=${%s}$" % (k, k) for k in upd)
|
||||
p = dict(upd); p["i"] = record_id
|
||||
await sor.sqlExe(
|
||||
"UPDATE bid_cost_benefit SET " + sets + ", updated_at=NOW() WHERE id=${i}$", p)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, project_id, "bid_cost_benefit", record_id, "update",
|
||||
who=who, agent_id=agent_id,
|
||||
detail="; ".join("%s: %s→%s" % (k, str(old.get(k))[:40], str(v)[:40])
|
||||
for k, v in upd.items())[:480])
|
||||
return True, "成本/收益项已更新: %s(字段:%s)" % (record_id, ",".join(upd.keys()))
|
||||
|
||||
|
||||
async def delete_cost_benefit_item(project_id, record_id, reason="",
|
||||
who=None, agent_id=None):
|
||||
"""删除一条成本/收益项(QC 判定互斥重复/占位无效行时用,reason 必填留审计)。"""
|
||||
if not record_id:
|
||||
return False, "缺少 record_id(先 list_cost_benefit 查到目标行 id)"
|
||||
if not (reason or "").strip():
|
||||
return False, "删除必须给出 reason(对照 QC 改进意见说明为何删:重复/占位/被取代)"
|
||||
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:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT item_name, category FROM bid_cost_benefit "
|
||||
"WHERE id=${i}$ AND project_id=${p}$",
|
||||
{"i": record_id, "p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not recs:
|
||||
return False, "记录不存在或不属于本项目: %s" % record_id
|
||||
nm = getattr(recs[0], "item_name", "")
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM bid_cost_benefit WHERE id=${i}$ AND project_id=${p}$",
|
||||
{"i": record_id, "p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, project_id, "bid_cost_benefit", record_id, "delete",
|
||||
who=who, agent_id=agent_id,
|
||||
detail="%s | 理由: %s" % (str(nm)[:60], str(reason)[:200]))
|
||||
return True, "已删除成本/收益项: %s(理由已留审计)" % nm
|
||||
|
||||
|
||||
async def extract_cost_benefit(project_id, file_id="", model_name="", offset=0,
|
||||
length=30000, who=None, agent_id=None):
|
||||
"""维度4:投标成本与收益分析(落 bid_cost_benefit:单项 + summary 结论行)。"""
|
||||
|
||||
@ -369,13 +369,18 @@ async def has_pending_blocking_human_task(sor, project_id):
|
||||
|
||||
async def create_human_task(sor, project_id, task_type, title, description="",
|
||||
assignee_role="owner.superuser", assignee_id="",
|
||||
form_schema=None):
|
||||
form_schema=None, dedup_by_title=False, task_id=""):
|
||||
"""创建人类任务(pipeline_human_tasks)。iteration_id 留空 → 走项目级门禁。
|
||||
|
||||
同一事项只发一次待办(2026-09-01 机制层不变量):同项目+同类型已有
|
||||
pending 任务 → 直接返回已有 id,不重复创建。防止对账器/agent 每轮
|
||||
重复发同类待办把用户淹没。
|
||||
|
||||
dedup_by_title(2026-09-16 甘肃项目实测 bug):「事项」粒度是标题而非类型——
|
||||
多个章节各自达重做上限都发 general 类型待办时,类型级去重把第 2~N 章的
|
||||
升级信号静默吞掉(只剩最先那条,其余章节人工介入信号丢失)。
|
||||
True 时按 (task_type+title) 去重:同标题 pending 才复用,不同标题各发一条。
|
||||
|
||||
项目状态门禁(2026-09-15,与引擎 create_human_task 同款):项目已删除或
|
||||
处于 paused/archived/cancelled/completed → 不发新待办返回 ''(暂停/删除的
|
||||
项目不再产生待办;恢复推进后对账器按需补发)。
|
||||
@ -392,15 +397,28 @@ async def create_human_task(sor, project_id, task_type, title, description="",
|
||||
logger.info("bid create_human_task skipped: project %s status=%s (type=%s)",
|
||||
project_id, _st, task_type)
|
||||
return ""
|
||||
exist = await find_human_task(sor, project_id, task_type, status="pending")
|
||||
if exist:
|
||||
logger.info("bid create_human_task dedup: type=%s existing=%s project=%s",
|
||||
task_type, exist.get("id"), project_id)
|
||||
return exist.get("id")
|
||||
if dedup_by_title:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id FROM pipeline_human_tasks WHERE project_id=${pid}$ "
|
||||
"AND task_type=${tt}$ AND title=${ti}$ AND status='pending' "
|
||||
"ORDER BY created_at DESC LIMIT 1",
|
||||
{"pid": project_id, "tt": task_type, "ti": (title or "")[:480]})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if recs:
|
||||
_eid = getattr(recs[0], "id", "")
|
||||
logger.info("bid create_human_task dedup(title): type=%s title=%s existing=%s",
|
||||
task_type, (title or "")[:40], _eid)
|
||||
return _eid
|
||||
else:
|
||||
exist = await find_human_task(sor, project_id, task_type, status="pending")
|
||||
if exist:
|
||||
logger.info("bid create_human_task dedup: type=%s existing=%s project=%s",
|
||||
task_type, exist.get("id"), project_id)
|
||||
return exist.get("id")
|
||||
hid = new_id()
|
||||
await sor.C("pipeline_human_tasks", {
|
||||
"id": hid,
|
||||
"task_id": "",
|
||||
"task_id": task_id or "",
|
||||
"step_name": task_type,
|
||||
"version": 1,
|
||||
"task_type": task_type,
|
||||
|
||||
@ -18,7 +18,7 @@ import logging
|
||||
import os
|
||||
|
||||
from .bid_common import (
|
||||
get_db, rows_to_dicts, rec_to_dict, to_int, to_float, json_loads,
|
||||
get_db, rows_to_dicts, rec_to_dict, to_int, to_float, json_loads, record,
|
||||
create_role_task, count_open_tasks, has_done_task,
|
||||
has_pending_blocking_human_task, find_human_task, get_thresholds,
|
||||
create_human_task, get_project, PIPELINE_ID,
|
||||
@ -905,6 +905,7 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
_kept.append(_qp)
|
||||
qc_pending = _kept
|
||||
over_types = set()
|
||||
_over_active = False # 2026-09-16:本轮是否仍存在「轮次用尽等人工」的类型
|
||||
if qc_pending:
|
||||
# 轮次用尽守卫:最新审核已达上限仍未通过且产出未更新 → 不再空转派审核任务,
|
||||
# 确保有阻塞人工任务(逃逸阀:人工修产出物或在记录页强制放行后流程自恢复)。
|
||||
@ -914,6 +915,7 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
if s == "rework" and rnd >= th["qc_max_round"] and not on]
|
||||
if over:
|
||||
over_types = set(t for t, _, _, _, _ in over)
|
||||
_over_active = True
|
||||
ht = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
@ -1036,6 +1038,21 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
# 不提前 return:继续往下走章节级派发(依赖已满足的章节立即启动)
|
||||
|
||||
# ── A2b. qc_escalation 待办自动关闭(2026-09-16 甘肃项目实测 bug)──
|
||||
# 升级待办发出后,人工修产出/记录页强制放行/重做通过 → 轮次用尽状态消失
|
||||
# (_over_active=False),但 pending 的 qc_escalation 待办无人关闭就永远挂着,
|
||||
# 角标常驻误导「还有事没办」。逃逸阀闭环的另一半:问题解决 → 待办自动收。
|
||||
if not _over_active:
|
||||
_ht_esc = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||
if _ht_esc:
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_human_tasks SET status='done', qc_status='passed', "
|
||||
"qc_comment='对账器自动关闭:QC 轮次用尽的类型已全部通过/放行,流程已恢复', "
|
||||
"submitted_at=NOW() WHERE id=${h}$ AND status='pending'",
|
||||
{"h": _ht_esc.get("id")})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("auto-closed: qc_escalation 待办(轮次用尽类型已全部通过/放行)")
|
||||
|
||||
# ── B. 资料准备(知识库资质匹配 + 人类文件清单)──
|
||||
# 2026-09-03 章节级依赖:资料准备是商务章(business)的上游依赖,
|
||||
# 不再阻塞全局流转——技术/报价章在上游 QC 通过后立即开写。
|
||||
@ -1056,6 +1073,103 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
if not chapters:
|
||||
return acts + ["idle: 无章节,等待解析产出章节骨架"]
|
||||
|
||||
# ── C0. writing 孤儿章节回收(2026-09-16 甘肃项目实测 bug)──
|
||||
# 写者任务 write_chapter 落 writing 后进程被打断/超时终态(任务 approved/failed/
|
||||
# cancelled),章节停在 writing:C 段只扫 pending/rejected、D 段只扫 written,
|
||||
# writing+无任务 = 永久死态(1.9/2.1.4 两章卡死 11 小时无人回收)。
|
||||
# 回收规则:无任何在办任务时——有正文 → written(D 段自动派评审续走质量循环);
|
||||
# 无正文 → rejected(C 段重派编写)。revise_count 不动(评审才是计轮主体)。
|
||||
for c in chapters:
|
||||
if c.get("status") != CH_WRITING:
|
||||
continue
|
||||
if await count_open_tasks(sor, project_id, chapter_id=c["id"]) > 0:
|
||||
continue # 写者/评审任务还在办,不是孤儿
|
||||
_has_txt = await sor.sqlExe(
|
||||
"SELECT LENGTH(content) AS n FROM bid_chapters WHERE id=${i}$", {"i": c["id"]})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
_n = to_int(getattr(_has_txt[0], "n", 0) if _has_txt else 0, 0)
|
||||
_new_st = CH_WRITTEN if _n > 0 else CH_REJECTED
|
||||
await sor.sqlExe(
|
||||
"UPDATE bid_chapters SET status=${st}$, updated_at=NOW() "
|
||||
"WHERE id=${i}$ AND status=${old}$",
|
||||
{"st": _new_st, "i": c["id"], "old": CH_WRITING})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, project_id, "bid_chapters", c["id"], "orphan_recover",
|
||||
from_state=CH_WRITING, to_state=_new_st, who="bid_flow",
|
||||
detail="writing 孤儿回收:无在办任务,正文 %d 字符 → %s" % (_n, _new_st))
|
||||
c["status"] = _new_st # 本轮内存态同步,后续段直接可用
|
||||
acts.append("recovered: 章[%s] writing 孤儿回收 → %s(无在办任务)"
|
||||
% (c.get("chapter_no"), _new_st))
|
||||
if any("orphan_recover" in (a or "") or "recovered: 章" in (a or "") for a in acts):
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
# ── C1. 超重做上限章节的人工闭环(2026-09-16 甘肃项目实测 bug)──
|
||||
# 旧行为缺陷:升级待办只在 review_chapter 调用瞬间发,且 general 类型级去重
|
||||
# 会把第 2~N 章的信号吞掉;人工完成待办后章节永久冻结(revise_count 已超限,
|
||||
# 无任何复位路径)。三态闭环:
|
||||
# ① 有 pending 升级待办(标题精确匹配本章)→ 真在等人工,跳过;
|
||||
# ② 无 pending 但有 done → 人工已处理 → revise_count 复位 0,C 段自动重派
|
||||
# 写作(人工确认换一轮写作机会,留审计);
|
||||
# ③ 无 pending 也无 done(信号曾丢失/从未发过)→ 按标题补发升级待办。
|
||||
_esc_types = ("material_supply", "general")
|
||||
for c in chapters:
|
||||
if c.get("status") != CH_REJECTED:
|
||||
continue
|
||||
if to_int(c.get("revise_count"), 0) <= th["max_revise"]:
|
||||
continue
|
||||
_titles = ("章节反复不达标需人工介入:%s %s" % (c.get("chapter_no"), c.get("title")),
|
||||
"章节实体材料缺口(信息流):%s %s" % (c.get("chapter_no"), c.get("title")))
|
||||
_in = ",".join("'${t%d}$'" % i for i in range(len(_titles)))
|
||||
_p = {"pid": project_id}
|
||||
for i, t in enumerate(_titles):
|
||||
_p["t%d" % i] = t[:480]
|
||||
_recs = await sor.sqlExe(
|
||||
"SELECT id, status FROM pipeline_human_tasks WHERE project_id=${pid}$ "
|
||||
"AND task_type IN ('" + "','".join(_esc_types) + "') AND title IN (" + _in + ")",
|
||||
_p)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
_rows = rows_to_dicts(_recs)
|
||||
if any(r.get("status") == "pending" for r in _rows):
|
||||
continue # ① 等人工中
|
||||
if any(r.get("status") == "done" for r in _rows):
|
||||
# ② 人工已处理 → 复位重派
|
||||
await sor.sqlExe(
|
||||
"UPDATE bid_chapters SET revise_count=0, updated_at=NOW() "
|
||||
"WHERE id=${i}$ AND status=${old}$",
|
||||
{"i": c["id"], "old": CH_REJECTED})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, project_id, "bid_chapters", c["id"], "human_reset",
|
||||
to_state=CH_REJECTED, who="bid_flow",
|
||||
detail="人工已完成升级待办 → revise_count 复位,自动重派写作")
|
||||
c["revise_count"] = 0
|
||||
acts.append("recovered: 章[%s] 人工处理完成,revise_count 复位自动重派"
|
||||
% c.get("chapter_no"))
|
||||
else:
|
||||
# ③ 信号丢失/从未发过 → 补发(材料缺失挂起的章按评审意见判类型)
|
||||
_cmr = await sor.sqlExe(
|
||||
"SELECT LEFT(review_comment, 1600) AS rc FROM bid_chapters WHERE id=${i}$",
|
||||
{"i": c["id"]})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
_cm = (getattr(_cmr[0], "rc", "") if _cmr else "") or ""
|
||||
if "材料缺失" in _cm and "非写作缺陷" in _cm:
|
||||
_tt, _ti = "material_supply", _titles[1]
|
||||
_desc = ("章节「%s %s」评审判定实体材料缺失(补发信息流待办)。\n评审意见:\n%s\n\n"
|
||||
"如需补材料请上传后走增补轮重写该章;不补则完成本待办即可。"
|
||||
% (c.get("chapter_no"), c.get("title"), _cm[:1500]))
|
||||
else:
|
||||
_tt, _ti = "general", _titles[0]
|
||||
_desc = ("章节「%s %s」已重做 %d 次仍未达门限(补发升级待办,原信号丢失)。\n"
|
||||
"评审意见:\n%s\n\n"
|
||||
"请人工判断:补充素材/调整章节范围/降低目标分,处理完成后完成本任务。"
|
||||
% (c.get("chapter_no"), c.get("title"),
|
||||
to_int(c.get("revise_count"), 0), _cm[:1500]))
|
||||
await create_human_task(
|
||||
sor, project_id, _tt, _ti, _desc,
|
||||
assignee_role="owner.superuser", dedup_by_title=True)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
acts.append("escalated: 章[%s] 超重做上限且无待办记录,补发人工介入待办"
|
||||
% c.get("chapter_no"))
|
||||
|
||||
# ── C. 章节编写(一章一任务、并发派发;pending/rejected 且无在办任务)──
|
||||
# 商务/技术分角色:section=business → 商务标写者;其余(technical/price)→ 技术标写者。
|
||||
# 并发:单轮最多派 write_concurrency 个编写任务(引擎另有每项目并发上限兜底)。
|
||||
|
||||
@ -26,11 +26,16 @@ async def _chapter(sor, chapter_id):
|
||||
|
||||
|
||||
async def review_chapter(chapter_id, score, max_score="", comments="", detail="",
|
||||
who=None, agent_id=None, task_id=""):
|
||||
material_missing="", who=None, agent_id=None, task_id=""):
|
||||
"""章节评审落库 + 按门限判定通过/退回。
|
||||
|
||||
score / max_score:按该章节对应评分项的得分规则打分(max_score 省略时取章节 max_score)。
|
||||
comments:不通过时必须给出可执行的改进意见(缺失则拒绝,避免"退回但不说怎么改")。
|
||||
material_missing(2026-09-16 甘肃项目 2.1.5/2.1.7 实测 bug):评审判定失分全部因
|
||||
「实体材料缺失」(证书扫描件/截图/业绩证明未回传)、写作层已达可写上限时传 1——
|
||||
章节如实披露缺口后通过(不烧写作轮次、不冻结流转),同时发 material_supply
|
||||
信息流待办通知人类补料,材料到位后可走增补轮重写该章。
|
||||
与 2026-09-15 用户裁定一致:资质缺口=信息流,非门禁流。
|
||||
"""
|
||||
if not chapter_id:
|
||||
return False, "缺少 chapter_id"
|
||||
@ -109,6 +114,42 @@ async def review_chapter(chapter_id, score, max_score="", comments="", detail=""
|
||||
return True, ("章节「%s」评审通过 %.2f/%.2f(%.0f%%,门限 %.0f%%)"
|
||||
% (c.get("title"), sc, ms, ratio * 100, th["chapter_pass_ratio"] * 100))
|
||||
|
||||
# ── 实体材料缺失通道(2026-09-16):如实标注后通过 + 信息流待办 ──
|
||||
# 旧行为 bug:证书扫描件/截图未回传 → 评审每轮打低分退回 → 写者无法变出
|
||||
# 材料仍重写 → 烧光 revise_count → 章节冻结阻塞合成。与 2026-09-15 用户裁定
|
||||
# 「资质缺口=信息流待办,非门禁流」对齐:写作层已达上限、失分全因实体材料
|
||||
# 未回传 → 章节通过(真实分数+材料缺失披露注记,整书评分如实反映),
|
||||
# 同时发 material_supply 信息流待办通知人类补料,不冻结任何流转。
|
||||
_mat_missing = str(material_missing).strip().lower() in ("1", "true", "yes", "y")
|
||||
if _mat_missing:
|
||||
await sor.sqlExe(
|
||||
"UPDATE " + TABLE + " SET status=${st}$, review_score=${sc}$, max_score=${ms}$, "
|
||||
"review_comment=${cm}$, updated_at=NOW() WHERE id=${cid}$",
|
||||
{"st": CH_APPROVED, "sc": sc, "ms": ms,
|
||||
"cm": ("【材料缺失如实披露后放行(2026-09-15 用户裁定:资质缺口=信息流"
|
||||
"非门禁流)】实体材料未回传致失分 %.2f/%.2f,写作层已达可写上限,"
|
||||
"不计写作重做轮次;已发人类供料待办,材料到位后可增补。\n"
|
||||
% (sc, ms) + (comments or ""))[:60000],
|
||||
"cid": chapter_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, pid, TABLE, chapter_id, "review_material_missing_waive",
|
||||
from_state=c.get("status"), to_state=CH_APPROVED,
|
||||
who=who, agent_id=agent_id,
|
||||
detail="%.2f/%.2f 实体材料缺失如实披露放行(不计轮次)" % (sc, ms))
|
||||
title = "章节实体材料缺口(信息流):%s %s" % (c.get("chapter_no"), c.get("title"))
|
||||
await create_human_task(
|
||||
sor, pid, "material_supply", title,
|
||||
("章节「%s %s」评审判定:写作层已达可写上限,失分 %.2f/%.2f 全部因实体材料"
|
||||
"未回传。章节已如实披露缺口后放行(不阻塞标书推进)。\n\n评审意见:\n%s\n\n"
|
||||
"如需补材料(证书扫描件/截图/业绩证明),上传到项目工作空间/知识库后"
|
||||
"可在增补轮重写该章;不补则完成本待办即可。"
|
||||
% (c.get("chapter_no"), c.get("title"), sc, ms, (comments or "")[:1500])),
|
||||
assignee_role="owner.superuser", dedup_by_title=True)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return True, ("章节「%s」评审 %.2f/%.2f:失分全部因实体材料缺失(非写作缺陷),"
|
||||
"已如实披露放行 + 发信息流供料待办「%s」,不阻塞标书推进。"
|
||||
% (c.get("title"), sc, ms, title))
|
||||
|
||||
revise = to_int(c.get("revise_count"), 0) + 1
|
||||
await sor.sqlExe(
|
||||
"UPDATE " + TABLE + " SET status=${st}$, review_score=${sc}$, max_score=${ms}$, "
|
||||
@ -122,18 +163,19 @@ async def review_chapter(chapter_id, score, max_score="", comments="", detail=""
|
||||
detail="%.2f/%.2f revise=%d" % (sc, ms, revise))
|
||||
|
||||
if revise > th["max_revise"]:
|
||||
exist = await find_human_task(sor, pid, "general", status="pending")
|
||||
# 2026-09-16 标题级去重:多章各自达上限各发一条(旧代码类型级 dedup,
|
||||
# 第 2~N 章升级信号被第一条 general pending 吞掉——甘肃项目 1/2.1.5/2.2
|
||||
# 三章达上限只有 2.1.7 一条待办,其余静默丢失)。
|
||||
title = "章节反复不达标需人工介入:%s %s" % (c.get("chapter_no"), c.get("title"))
|
||||
if not (exist and exist.get("title") == title):
|
||||
await create_human_task(
|
||||
sor, pid, "general", title,
|
||||
("章节「%s %s」已重做 %d 次仍未达门限(最近得分 %.2f/%.2f,门限 %.0f%%)。\n"
|
||||
"最新改进意见:\n%s\n\n"
|
||||
"请人工判断:补充素材/调整章节范围/降低目标分,处理完成后完成本任务。"
|
||||
% (c.get("chapter_no"), c.get("title"), revise, sc, ms,
|
||||
th["chapter_pass_ratio"] * 100, (comments or "")[:1500])),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await create_human_task(
|
||||
sor, pid, "general", title,
|
||||
("章节「%s %s」已重做 %d 次仍未达门限(最近得分 %.2f/%.2f,门限 %.0f%%)。\n"
|
||||
"最新改进意见:\n%s\n\n"
|
||||
"请人工判断:补充素材/调整章节范围/降低目标分,处理完成后完成本任务。"
|
||||
% (c.get("chapter_no"), c.get("title"), revise, sc, ms,
|
||||
th["chapter_pass_ratio"] * 100, (comments or "")[:1500])),
|
||||
assignee_role="owner.superuser", dedup_by_title=True)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return True, ("章节「%s」评审不通过 %.2f/%.2f,已达重做上限 %d 次 → 已抛人工介入任务"
|
||||
% (c.get("title"), sc, ms, th["max_revise"]))
|
||||
return True, ("章节「%s」评审不通过 %.2f/%.2f(%.0f%% < 门限 %.0f%%),已退回重写"
|
||||
|
||||
@ -77,15 +77,27 @@ async def chapter_detail(chapter_id, with_content="1", length=12000):
|
||||
return json.dumps(out, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
async def write_chapter(chapter_id, content, summary="", who=None, agent_id=None):
|
||||
"""写入/覆盖章节正文:pending/rejected/writing → writing,版本+1,字数统计。"""
|
||||
async def write_chapter(chapter_id, content, summary="", mode="replace",
|
||||
who=None, agent_id=None):
|
||||
"""写入章节正文:mode=replace 覆盖 / append 追加,版本+1,字数统计。
|
||||
|
||||
append(2026-09-16 新增,甘肃项目 2.2 章实测):超长章节(如 403 行技术
|
||||
参数响应表)一次 write 超出单条工具调用可承载长度写不进去,评审明确要求
|
||||
「机制侧开放 write_chapter 分段追加,按标的分批每批 50~80 行」。
|
||||
追加时自动在两段之间补换行;篇幅门禁按累计总字数判定。
|
||||
"""
|
||||
if not chapter_id:
|
||||
return False, "缺少 chapter_id"
|
||||
txt = content or ""
|
||||
if len(txt.strip()) < MIN_CONTENT_LEN:
|
||||
mode = (mode or "replace").strip().lower()
|
||||
if mode not in ("replace", "append"):
|
||||
return False, "mode 只支持 replace(覆盖)或 append(追加)"
|
||||
if len(txt.strip()) < MIN_CONTENT_LEN and mode == "replace":
|
||||
return False, ("章节正文过短(%d 字符 < %d),拒绝写入。请按招标文件要求与评分规则"
|
||||
"写到操作层面(量化指标/流程步骤/责任人/表单),不要写空话。"
|
||||
% (len(txt.strip()), MIN_CONTENT_LEN))
|
||||
if mode == "append" and not txt.strip():
|
||||
return False, "追加内容为空"
|
||||
# ── 配图形态硬门禁(2026-09-15 用户要求:标书章节的图必须 t2i/i2i 真图)──
|
||||
# 确定性判定(复用 pipeline_service.diagram_gate),mermaid/ASCII 伪图拒绝写入;
|
||||
# 平台无图像模型时正文如实标注「配图缺失」即豁免。
|
||||
@ -108,16 +120,27 @@ async def write_chapter(chapter_id, content, summary="", who=None, agent_id=None
|
||||
if d.get("status") == CH_APPROVED:
|
||||
return False, "章节已评审通过(approved),不可覆盖;如需修改请先由评审/评分角色退回"
|
||||
ver = to_int(d.get("version"), 1)
|
||||
if mode == "append":
|
||||
old_txt = d.get("content") or ""
|
||||
new_txt = (old_txt.rstrip() + "\n\n" + txt.strip() + "\n") if old_txt.strip() \
|
||||
else (txt.strip() + "\n")
|
||||
else:
|
||||
new_txt = txt
|
||||
await sor.sqlExe(
|
||||
"UPDATE " + TABLE + " SET content=${c}$, word_count=${w}$, status=${st}$, "
|
||||
"version=${v}$, updated_at=NOW() WHERE id=${cid}$",
|
||||
{"c": txt, "w": len(txt), "st": CH_WRITING, "v": ver + 1, "cid": chapter_id})
|
||||
{"c": new_txt, "w": len(new_txt), "st": CH_WRITING, "v": ver + 1, "cid": chapter_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, d.get("project_id"), TABLE, chapter_id, "write",
|
||||
await record(sor, d.get("project_id"), TABLE, chapter_id,
|
||||
"append" if mode == "append" else "write",
|
||||
from_state=d.get("status"), to_state=CH_WRITING,
|
||||
who=who, agent_id=agent_id, detail=(summary or "")[:480])
|
||||
if mode == "append":
|
||||
return True, ("章节「%s」已追加 %d 字符(累计 %d 字符,v%d)。"
|
||||
"全部批次写完后回读核对总行数/总字数,再调 submit_chapter 提交评审。"
|
||||
% (d.get("title"), len(txt), len(new_txt), ver + 1))
|
||||
return True, ("章节「%s」正文已保存(%d 字符,v%d)。确认完成后调用 submit_chapter 提交评审。"
|
||||
% (d.get("title"), len(txt), ver + 1))
|
||||
% (d.get("title"), len(new_txt), ver + 1))
|
||||
|
||||
|
||||
async def submit_chapter(chapter_id, note="", who=None, agent_id=None):
|
||||
|
||||
@ -73,6 +73,28 @@ BID_ROLE_TOOL_SCHEMAS = {
|
||||
"confidence": "high|medium|low(可选)", "source_ref": "出处(可选)"},
|
||||
"required": ["category", "item_name"],
|
||||
},
|
||||
"list_cost_benefit": {
|
||||
"module": f"{_M}.bid_analysis_capability",
|
||||
"description": "列出成本/收益分析全部条目。QC 重做/修正前必读现状——禁止看不到旧行就盲重抽(会堆积互斥重复条)",
|
||||
"params": {"project_id": "项目ID(可选,默认当前)"},
|
||||
"required": [],
|
||||
},
|
||||
"update_cost_benefit_item": {
|
||||
"module": f"{_M}.bid_analysis_capability",
|
||||
"description": "修改一条成本/收益项(QC 意见要求合并口径/修正引用/补算式时用;只改传入字段,改前先 list_cost_benefit 查 id)",
|
||||
"params": {"project_id": "项目ID(可选,默认当前)", "record_id": "目标行id(必填)",
|
||||
"item_name": "新名称(可选)", "amount": "新金额(可选)",
|
||||
"amount_note": "新口径说明(可选)", "basis": "新推理依据(可选)",
|
||||
"confidence": "high|medium|low(可选)", "source_ref": "新出处(可选)"},
|
||||
"required": ["record_id"],
|
||||
},
|
||||
"delete_cost_benefit_item": {
|
||||
"module": f"{_M}.bid_analysis_capability",
|
||||
"description": "删除一条成本/收益项(QC 判定互斥重复/占位无效行时用;reason 必填留审计)",
|
||||
"params": {"project_id": "项目ID(可选,默认当前)", "record_id": "目标行id(必填)",
|
||||
"reason": "删除理由(必填:重复/占位/被哪条取代)"},
|
||||
"required": ["record_id", "reason"],
|
||||
},
|
||||
"add_scoring_item": {
|
||||
"module": f"{_M}.bid_analysis_capability",
|
||||
"description": "录入评分项及得分规则(抽取漏项时人工补录)",
|
||||
@ -195,8 +217,9 @@ BID_ROLE_TOOL_SCHEMAS = {
|
||||
},
|
||||
"write_chapter": {
|
||||
"module": f"{_M}.bid_write_capability",
|
||||
"description": "写入章节正文(写到操作层面,禁止空话;有最小长度门槛)",
|
||||
"params": {"chapter_id": "章节ID", "content": "章节正文", "summary": "本次写作要点(可选)"},
|
||||
"description": "写入章节正文(写到操作层面,禁止空话;有最小长度门槛)。超长章节(如几百行响应表)用 mode=append 分批追加,每批50~80行,全部写完回读核对再 submit",
|
||||
"params": {"chapter_id": "章节ID", "content": "本批正文",
|
||||
"mode": "replace覆盖(默认)|append追加", "summary": "本次写作要点(可选)"},
|
||||
"required": ["chapter_id", "content"],
|
||||
},
|
||||
"submit_chapter": {
|
||||
@ -208,9 +231,10 @@ BID_ROLE_TOOL_SCHEMAS = {
|
||||
# ── bid_review_capability:标书评审员 ──
|
||||
"review_chapter": {
|
||||
"module": f"{_M}.bid_review_capability",
|
||||
"description": "章节评审打分:对照评分项得分规则+招标文件/投标文件要求;不达标必须给改进意见",
|
||||
"description": "章节评审打分:对照评分项得分规则+招标文件/投标文件要求;不达标必须给改进意见。若写作层已达可写上限、失分全部因实体材料未回传(证书扫描件/截图等写作者变不出的东西),传material_missing=1——章节如实披露后通过不冻结流转,自动发信息流供料待办,禁止再烧写作轮次",
|
||||
"params": {"chapter_id": "章节ID", "score": "得分(数字)", "max_score": "满分(可选,默认取章节挂载满分)",
|
||||
"comments": "评审意见/改进意见(不通过必填)", "detail": "评审明细(可选)"},
|
||||
"comments": "评审意见/改进意见(不通过必填)", "detail": "评审明细(可选)",
|
||||
"material_missing": "1=失分因实体材料缺失且写作者无法闭环(可选)"},
|
||||
"required": ["chapter_id", "score"],
|
||||
},
|
||||
"approve_chapter": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user