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

4.5 KiB

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被污染

诊断步骤

# 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?
# 如果是 → 被污染

修复代码模板

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有的没有