# 自动编号 → 手动固定编号修复(删段重排根治) ## 适用场景 他人(如屠佳青)用修订模式整段删除了一个**自动编号列表项**(段落 pPr 含 ``,且段落标记 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 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 必须包进【他人的】(复制其 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 内容(id 不变)一字未动。 ## 上传 `docker cp` 覆盖 `任务交付/` 同名文件 → `chown www-data` → `occ files:scan --path` → 清 OnlyOffice 缓存(`rm -rf .../App_Data/cache/files/*`)让 Maggie 打开看到新版。`md5sum` 比对容器内==本地确认上传成功。