From beca611f29781de51c56dce6fd364643017d179a Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 17 Sep 2026 10:20:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(revise):=20revise=5Fchapter=5Ftext?= =?UTF-8?q?=E5=AE=9A=E7=82=B9=E6=9B=BF=E6=8D=A2=E5=B7=B2=E5=AE=9A=E7=A8=BF?= =?UTF-8?q?=E7=AB=A0=E8=8A=82=E5=B1=80=E9=83=A8=E6=96=87=E6=9C=AC(2026-09-?= =?UTF-8?q?16=E7=94=98=E8=82=83=E5=88=A01.9=E7=AB=A0=E5=90=8E=E6=82=AC?= =?UTF-8?q?=E7=A9=BA=E5=BC=95=E7=94=A8=E5=AE=9E=E6=B5=8B=E9=9C=80=E8=A6=81?= =?UTF-8?q?)=E2=80=94=E2=80=94approved=E7=AB=A0=E8=8A=82write=5Fchapter?= =?UTF-8?q?=E6=8B=92=E5=86=99,=E5=90=88=E8=A7=84=E6=80=A7=E5=B1=80?= =?UTF-8?q?=E9=83=A8=E4=BF=AE=E6=AD=A3(=E6=82=AC=E7=A9=BA=E4=BA=A4?= =?UTF-8?q?=E5=8F=89=E5=BC=95=E7=94=A8/=E8=BF=9D=E5=8F=8D=E6=8B=9B?= =?UTF-8?q?=E6=A0=87=E4=B9=A6=E6=98=8E=E6=96=87/=E8=B7=A8=E7=AB=A0?= =?UTF-8?q?=E7=BC=96=E5=8F=B7=E9=94=99=E4=BD=8D)=E6=97=A0=E5=A4=84?= =?UTF-8?q?=E8=90=BD=E8=84=9A;=E6=96=AD=E8=A8=80=E5=BC=8Ffind=E9=A1=BB?= =?UTF-8?q?=E6=81=B0=E5=A5=BD=E5=87=BA=E7=8E=B01=E6=AC=A1=E5=90=A6?= =?UTF-8?q?=E5=88=99=E6=95=B4=E6=89=B9=E6=8B=92=E7=BB=9D,=E7=89=88?= =?UTF-8?q?=E6=9C=AC+1+=E5=AE=A1=E8=AE=A1+=E9=95=9C=E5=83=8F=E5=90=8C?= =?UTF-8?q?=E6=AD=A5,approved=E4=B8=8D=E8=A7=A6=E5=8F=91=E9=87=8D=E5=AE=A1?= =?UTF-8?q?(=E8=AF=84=E5=AE=A1=E8=AF=AD=E4=B9=89=E6=9C=AA=E5=8F=98),?= =?UTF-8?q?=E6=89=A9=E5=86=99=E4=BB=8D=E8=B5=B0=E8=AF=84=E5=AE=A1=E9=80=80?= =?UTF-8?q?=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_bidding/bid_write_capability.py | 77 ++++++++++++++++++++++++ pipeline_bidding/role_tool_schemas.py | 7 +++ 2 files changed, 84 insertions(+) diff --git a/pipeline_bidding/bid_write_capability.py b/pipeline_bidding/bid_write_capability.py index 209ebc9..0910451 100644 --- a/pipeline_bidding/bid_write_capability.py +++ b/pipeline_bidding/bid_write_capability.py @@ -225,6 +225,83 @@ async def write_chapter(chapter_id, content, summary="", mode="replace", % (d.get("title"), len(new_txt), ver + 1, _gap_note)) +async def revise_chapter_text(chapter_id, edits="", who=None, agent_id=None): + """定点替换已定稿章节正文的局部文本(2026-09-16 甘肃 1.9 删章后实测需要)。 + + write_chapter 对 approved 章节拒写(防误覆盖),但存在必须修正的合规性 + 局部改动(悬空交叉引用/违反招标书明文),整章重写既昂贵又有丢内容风险。 + 本工具做「断言式」定点替换: + - edits = [{"find": 原文片段, "replace": 新文本}],find 必须在正文中 + **恰好出现一次**(0 次或多次都拒绝整个操作,防误伤/防歧义); + - 全部命中才落库(先校验后写,单条不合法整批拒绝); + - 版本 +1、审计记录、镜像文件同步;approved 状态保持不变(局部合规修正 + 不触发整章重审——评审语义与分数未变)。 + + 用于:删章后的悬空引用清理、与招标书明文冲突的表述纠正、跨章编号引用 + 错位修正。不用于内容扩写(那走评审退回重写)。 + """ + import json as _json + if not chapter_id: + return False, "缺少 chapter_id" + if isinstance(edits, str): + try: + edits = _json.loads(edits) + except Exception as e: # noqa: BLE001 + return False, "edits 解析失败(须为 JSON 数组):%s" % str(e)[:120] + if not isinstance(edits, list) or not edits: + return False, "edits 必须是非空数组:[{find, replace}]" + db, dbname = get_db() + async with db.sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT id, project_id, chapter_no, title, content, version, status " + "FROM " + TABLE + " WHERE id=${cid}$", {"cid": chapter_id}) + await sor.sqlExe("COMMIT", {}) + if not recs: + return False, "章节不存在: %s" % chapter_id + d = rec_to_dict(recs[0]) + txt = d.get("content") or "" + norm, applied = [], [] + for i, e in enumerate(edits): + if not isinstance(e, dict): + return False, "edits[%d] 必须是对象 {find, replace}" % i + find = e.get("find") + rep = e.get("replace") + if not isinstance(find, str) or not find.strip(): + return False, "edits[%d].find 必须是非空字符串" % i + if rep is None or not isinstance(rep, str): + return False, "edits[%d].replace 必须是字符串(可为空串=删除)" % i + n = txt.count(find) + if n != 1: + return False, ("edits[%d] 不满足唯一性断言:find 在章节「%s」中出现 %d 次" + "(要求恰好 1 次)。请扩大上下文片段使其唯一。find 前80字=%s" + % (i, d.get("title"), n, find[:80])) + norm.append((find, rep)) + # 全部断言通过才落库 + new_txt = txt + for find, rep in norm: + new_txt = new_txt.replace(find, rep, 1) + applied.append(find[:40]) + ver = to_int(d.get("version"), 1) + await sor.sqlExe( + "UPDATE " + TABLE + " SET content=${c}$, word_count=${w}$, " + "version=${v}$, updated_at=NOW() WHERE id=${cid}$", + {"c": new_txt, "w": len(new_txt), "v": ver + 1, "cid": chapter_id}) + await sor.sqlExe("COMMIT", {}) + await record(sor, d.get("project_id"), TABLE, chapter_id, "revise_text", + from_state=d.get("status"), to_state=d.get("status"), + who=who, agent_id=agent_id, + detail="定点替换 %d 处:%s" % (len(norm), "|".join(applied)[:400])) + _mir = "" + try: + from .bid_chapter_mirror import sync_chapter_mirror + _mir = await sync_chapter_mirror(sor, d.get("project_id"), chapter_id) + except Exception as _e: # noqa: BLE001 + logger.warning("chapter mirror failed: %s", str(_e)[:120]) + return True, ("章节「%s」定点替换 %d 处(v%d,%d 字符,状态 %s 不变)%s" + % (d.get("title"), len(norm), ver + 1, len(new_txt), + d.get("status"), ";镜像 %s" % _mir if _mir else "")) + + async def submit_chapter(chapter_id, note="", who=None, agent_id=None): """提交章节评审:writing → written(reconciler 会自动派发评审任务)。""" db, dbname = get_db() diff --git a/pipeline_bidding/role_tool_schemas.py b/pipeline_bidding/role_tool_schemas.py index 0b1c8da..5833afb 100644 --- a/pipeline_bidding/role_tool_schemas.py +++ b/pipeline_bidding/role_tool_schemas.py @@ -235,6 +235,13 @@ BID_ROLE_TOOL_SCHEMAS = { "params": {"chapter_id": "章节ID", "note": "说明(可选)"}, "required": ["chapter_id"], }, + "revise_chapter_text": { + "module": f"{_M}.bid_write_capability", + "description": "定点替换章节正文局部文本(含已 approved 章节):edits=[{find,replace}],find 必须在正文中恰好出现一次否则整批拒绝。仅用于合规性局部修正(悬空交叉引用/违反招标书明文/跨章编号错位),内容扩写走评审退回重写。版本+1留审计,approved 状态不触发重审", + "params": {"chapter_id": "章节ID", + "edits": "JSON数组 [{find:原文片段(须唯一), replace:新文本(空串=删除)}]"}, + "required": ["chapter_id", "edits"], + }, # ── bid_review_capability:标书评审员 ── "review_chapter": { "module": f"{_M}.bid_review_capability",