Files
hermes-skills/skills/legal/contract-editor/references/auto-number-to-manual-fix.md
T

99 lines
5.2 KiB
Markdown

# 自动编号 → 手动固定编号修复(删段重排根治)
## 适用场景
他人(如屠佳青)用修订模式整段删除了一个**自动编号列表项**(段落 pPr 含 `<w:numPr>`,且段落标记 del=True),导致 OnlyOffice **markup 修订视图**把后续列表项渲染成"旧号新号"双编号:
```
(1)报名服务 ← 正常
(2)笔试服务[删除线] ← 被删,仍占编号位
(3)(2)面试服务 ← 双号!自动引擎按"接受后会变(2)"提前显示
(4)(3)项目管理 ← 双号!
```
Maggie/Doro 平时看 markup 视图,要求"修订视图下编号稳定显示 (1)(2)(3)(4)"。
根治办法:把这一组列表项从自动编号转成**手动文本编号**——文本是字面量,渲染器原样输出,不再经过自动编号引擎重排。
## 关键认知(动手前必须确认)
1. **被删项的删除是他人修订 → 绝不动其正文**(只在段首加编号 run,不碰 del 内容)。
2. **全文先确认没有对这些子项编号的交叉引用**(如"按上述第3项""见(4)")。本例"具体服务内容详见 附件一:服务报价单"是文字列举,非编号引用 → 安全。若存在引用,转手动编号后引用文字需同步核对。
3.`docker exec <nc容器> find ... ` 从 Nextcloud **拉当前交付版**作修复源,`md5sum` 比对确认本地副本没过时。
## 可复用代码(2026-06-16 验证通过)
```python
import zipfile, shutil, os
from lxml import etree
NS='http://schemas.openxmlformats.org/wordprocessingml/2006/main'
def q(t): return f'{{{NS}}}{t}'
def ln(el): return etree.QName(el).localname
src='交付版.docx'; out='FIXED.docx'
work='work.docx'; shutil.copy(src, work)
root=etree.fromstring(zipfile.ZipFile(work).read('word/document.xml'))
# 1. 按正文开头定位目标段(按你的合同改这些前缀)
segs={}
for p in root.iter(q('p')):
t=''.join((x.text or '') for x in p.iter() if ln(x) in ('t','delText'))
if t.startswith('报名服务:'): segs['报名']=p
elif t.startswith('笔试服务:提供'): segs['笔试']=p # 被删的那项
elif t.startswith('面试服务:'): segs['面试']=p
elif t.startswith('项目管理:整个项目'): segs['项目管理']=p
def make_rpr(): # 字体/字号照搬本段原文(本例宋体sz=24)。务必与目标段一致
rpr=etree.SubElement(etree.Element(q('tmp')), q('rPr'))
rf=etree.SubElement(rpr, q('rFonts'))
for a in ('ascii','hAnsi','cs'): rf.set(q(a),'宋体;SimSun')
etree.SubElement(rpr, q('sz')).set(q('val'),'24')
etree.SubElement(rpr, q('szCs')).set(q('val'),'24')
return rpr
def make_run(text):
r=etree.Element(q('r')); r.append(make_rpr())
t=etree.SubElement(r, q('t')); t.text=text
t.set('{http://www.w3.org/XML/1998/namespace}space','preserve')
return r
def remove_numpr(p):
ppr=p.find(q('pPr'))
if ppr is not None:
np=ppr.find(q('numPr'))
if np is not None: ppr.remove(np)
def insert_first(p, node): # 插到 pPr 之后、第一个内容元素之前
idx=len(p)
for i,c in enumerate(p):
if ln(c) in ('r','ins','del','hyperlink'): idx=i; break
p.insert(idx, node)
# 2. 普通项:明文编号 run
for key,label in [('报名','(1)'),('面试','(3)'),('项目管理','(4)')]:
remove_numpr(segs[key]); insert_first(segs[key], make_run(label))
# 3. 被删项:编号 run 必须包进【他人的】<w:del>(复制其 author/date)
p=segs['笔试']; remove_numpr(p)
ex=p.find(q('del')) # 已有的他人 del(屠佳青)
nd=etree.Element(q('del'))
nd.set(q('author'), ex.get(q('author'))) # 照抄他人 author
nd.set(q('date'), ex.get(q('date')))
nd.set(q('id'),'99001') # 不冲突的大 id
r=etree.SubElement(nd, q('r')); r.append(make_rpr())
dt=etree.SubElement(r, q('delText')); dt.text='(2)'
dt.set('{http://www.w3.org/XML/1998/namespace}space','preserve')
insert_first(p, nd)
# 4. 写回(只换 document.xml,其余条目原样复制)
new_doc=etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True)
with zipfile.ZipFile(work) as zin, zipfile.ZipFile(out,'w',zipfile.ZIP_DEFLATED) as zout:
for it in zin.namelist():
zout.writestr(it, new_doc if it=='word/document.xml' else zin.read(it))
```
## 交付前必验(vision 不可用时降级四查,缺一不可)
1. **逐段 markup diff vs 交付源** → 只有目标 N 段不同,其余零改动(本例 346 段只动 4 段)。
2. **pdftotext 渲染层数编号链**`onlyoffice-render.sh out.docx && pdftotext -f1 -l1 out.pdf -` 确认 1.2 下严格 (1)(2)(3)(4) 无双号。
3. **编号 run rPr == 本段正文 run rPr**(字体宋体、sz=24 一致)。
4. **接受修订后视图编号链连续** + `python-docx Document(out)` 可打开(XML 合法)。验证被删项的编号确实在 `<del author=他人>` 里、他人原 del 内容(id 不变)一字未动。
## 上传
`docker cp` 覆盖 `任务交付/` 同名文件 → `chown www-data``occ files:scan --path` → 清 OnlyOffice 缓存(`rm -rf .../App_Data/cache/files/*`)让 Maggie 打开看到新版。`md5sum` 比对容器内==本地确认上传成功。