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",