feat(revise): revise_chapter_text定点替换已定稿章节局部文本(2026-09-16甘肃删1.9章后悬空引用实测需要)——approved章节write_chapter拒写,合规性局部修正(悬空交叉引用/违反招标书明文/跨章编号错位)无处落脚;断言式find须恰好出现1次否则整批拒绝,版本+1+审计+镜像同步,approved不触发重审(评审语义未变),扩写仍走评审退回

This commit is contained in:
yumoqing 2026-09-17 10:20:14 +08:00
parent 227471d514
commit beca611f29
2 changed files with 84 additions and 0 deletions

View File

@ -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()

View File

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