Files
hermes-skills/skills/legal/contract-editor/references/workflow-font-contamination-repair.md
T

128 lines
4.5 KiB
Markdown

# ContractEditor 原文Run属性污染诊断与修复
## 2026-07-13 洋励合同实证
### 问题描述
ContractEditor(contract_docx_lib.py)在处理文档时,不仅给WB INS runs添加多余属性,还会**修改原文runs**的rPr——给本来靠docDefaults/style继承的orig runs添加显式eastAsia/cs/sz。
### 典型污染模式
| 属性 | 原文(待审查) | 被污染后(交付物中的orig run) | WB INS run |
|------|--------------|-------------------------------|-----------|
| eastAsia | None (继承minorEastAsia) | **宋体** (被加) | None |
| cs | None | **宋体** (被加) | None |
| sz | None (继承docDefaults=22) | **21** (被加且值错) | None |
| ascii | 宋体 | 宋体 | None |
| hint | eastAsia (部分有) | eastAsia | None |
### 后果
1. 原文所有文字从11pt(docDefaults sz=22)变成10.5pt(显式sz=21) — 整体缩小0.5pt
2. INS文字没有任何属性 → 走docDefaults 11pt → 与被改小的原文不一致
3. 字体验证脚本(wb-ins-font-verify.py)报INS缺属性,但实际问题是orig被污染
### 诊断步骤
```bash
# 1. 读原文代表性段落run rPr
python3 -c "
import zipfile
from lxml import etree
WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
with zipfile.ZipFile('原文.docx', 'r') as z:
...
# 检查: eastAsia=None? sz=None?
# 如果是 → 原文靠继承
# 2. 读交付物同段落orig run rPr
# 检查: 是否多了eastAsia/cs/sz?
# 如果是 → 被污染
```
### 修复代码模板
```python
import zipfile, tempfile, shutil
from lxml import etree
WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
with zipfile.ZipFile(filepath, 'r') as z:
all_files = {n: z.read(n) for n in z.namelist()}
tree = etree.fromstring(all_files['word/document.xml'])
body = tree.find(f'{WNS}body')
# Step 1: Strip contaminated attributes from ALL orig runs
for p in body.findall(f'{WNS}p'):
for r in p.findall(f'{WNS}r'): # Only direct child runs (not inside ins/del)
rpr = r.find(f'{WNS}rPr')
if rpr is None:
continue
rf = rpr.find(f'{WNS}rFonts')
if rf is not None:
# Strip eastAsia (original didn't have it)
if f'{WNS}eastAsia' in rf.attrib:
del rf.attrib[f'{WNS}eastAsia']
# Strip cs (original didn't have it)
if f'{WNS}cs' in rf.attrib:
del rf.attrib[f'{WNS}cs']
# Strip sz (original relies on docDefaults)
sz = rpr.find(f'{WNS}sz')
if sz is not None:
rpr.remove(sz)
# Step 2: Fix INS runs to match REAL original format
for ins in body.findall(f'.//{WNS}ins'):
if ins.get(f'{WNS}author') != 'WB':
continue
for r in ins.findall(f'{WNS}r'):
rpr = r.find(f'{WNS}rPr')
if rpr is None:
continue
rf = rpr.find(f'{WNS}rFonts')
if rf is None:
rf = etree.SubElement(rpr, f'{WNS}rFonts')
# Match real original: ascii=宋体, hAnsi=宋体, NO eastAsia
rf.set(f'{WNS}ascii', '宋体')
rf.set(f'{WNS}hAnsi', '宋体')
if f'{WNS}eastAsia' in rf.attrib:
del rf.attrib[f'{WNS}eastAsia']
# Remove sz (let it inherit)
sz = rpr.find(f'{WNS}sz')
if sz is not None:
rpr.remove(sz)
# Step 3: Per-paragraph hint matching
for p in body.findall(f'{WNS}p'):
# Get orig run's hint
orig_hint = None
for r in p.findall(f'{WNS}r'):
rpr = r.find(f'{WNS}rPr')
if rpr is not None:
rf = rpr.find(f'{WNS}rFonts')
orig_hint = rf.get(f'{WNS}hint') if rf is not None else None
break
# Apply to INS runs in same paragraph
for ins in p.findall(f'.//{WNS}ins'):
if ins.get(f'{WNS}author') != 'WB':
continue
for r in ins.findall(f'{WNS}r'):
rpr = r.find(f'{WNS}rPr')
if rpr is None: continue
rf = rpr.find(f'{WNS}rFonts')
if rf is None: continue
if orig_hint:
rf.set(f'{WNS}hint', orig_hint)
elif f'{WNS}hint' in rf.attrib:
del rf.attrib[f'{WNS}hint']
```
### 注意事项
1. **必须对比原文确定被污染了哪些属性** — 不同合同模板的原文属性不同
2. **不是所有合同都有此问题** — 取决于原文是否靠继承(有显式属性的不会被"污染",因为值相同)
3. **Step 1必须在Step 2之前** — 否则wb-ins-font-verify仍会报INS与(被污染的)orig不一致
4. **hint要逐段处理** — 同一文档不同段落的orig runs可能有的有hint有的没有