# 修改已有tracked changes的作者和文本内容

## 场景
- 合并用户在OnlyOffice中的修订（author如"华诚-Z"改为"WB"）
- 修改INS元素中的文本内容（如更新法律措辞）
- 修改批注作者（comments.xml中的w:comment author属性）

## 技术实现

### 修改tracked change作者
```python
from lxml import etree
import zipfile

WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'

# 打开docx，修改document.xml
z_in = zipfile.ZipFile('input.docx', 'r')
z_out = zipfile.ZipFile('output.docx', 'w')

# 复制非document.xml的文件
for item in z_in.namelist():
    if item != 'word/document.xml':
        z_out.writestr(item, z_in.read(item))

# 修改tracked change作者
with z_in.open('word/document.xml') as f:
    tree = etree.parse(f)
root = tree.getroot()

for elem in root.iter():
    tag = etree.QName(elem.tag).localname
    if tag in ('ins', 'del'):
        old_author = elem.get(f'{WNS}author', '')
        if old_author == '旧作者名':
            elem.set(f'{WNS}author', 'WB')

z_out.writestr('word/document.xml', etree.tostring(tree, encoding='UTF-8', xml_declaration=True, standalone=True))
z_in.close()
z_out.close()
```

### 修改INS文本内容
```python
from copy import deepcopy
from datetime import datetime

now = datetime.now().isoformat()

# 定位特定段落中的INS元素
body = root.find(f'{WNS}body')
paras = body.findall(f'{WNS}p')
target_para = paras[12]  # 按索引定位

# 删除旧的INS元素（按作者筛选）
for ins in list(target_para.findall(f'{WNS}ins')):
    author = ins.get(f'{WNS}author', '')
    if author == '目标作者':
        target_para.remove(ins)

# 添加新的INS元素
new_ins = etree.SubElement(target_para, f'{WNS}ins')
new_ins.set(f'{WNS}author', 'WB')
new_ins.set(f'{WNS}date', now)
new_r = etree.SubElement(new_ins, f'{WNS}r')

# 从同段落的原文run复制格式
orig_runs = target_para.findall(f'{WNS}r')
if orig_runs:
    orig_rpr = orig_runs[0].find(f'{WNS}rPr')
    if orig_rpr is not None:
        new_r.append(deepcopy(orig_rpr))

new_t = etree.SubElement(new_r, f'{WNS}t')
new_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
new_t.text = '新的插入文本'
```

### 修改批注作者
```python
# 修改comments.xml中的作者
if 'word/comments.xml' in z_in.namelist():
    with z_in.open('word/comments.xml') as f:
        ctree = etree.parse(f)
    croot = ctree.getroot()
    for c in croot.findall(f'{WNS}comment'):
        if c.get(f'{WNS}author', '') == '旧作者名':
            c.set(f'{WNS}author', 'WB')
    z_out.writestr('word/comments.xml', etree.tostring(ctree, encoding='UTF-8', xml_declaration=True, standalone=True))
```

### 在已有WB INS元素内修改部分文本（2026-07-01 反委托代发工资协议）

当段落文本全部是WB INS（无普通w:r），需要替换其中某一句时，**不能删除整个INS重建**（会丢失该INS中其他文本的修订标记）。正确手法：**trim原INS的w:t + addnext插入DEL/INS**。

```python
old_sentence = "退回派遣员工由乙方依法自行安置处理，与甲方无涉。"
new_sentence = "派遣员工退回后由乙方依法负责安置处理。因乙方安置不当导致甲方被追究责任的，乙方应赔偿甲方因此遭受的全部损失。"

for child in list(p):
    tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
    if tag == 'ins' and child.get(f'{WNS}author') == 'WB':
        for r in child.findall(f'{WNS}r'):
            for t in r.findall(f'{WNS}t'):
                if t.text and old_sentence in t.text:
                    rpr_copy = copy.deepcopy(r.find(f'{WNS}rPr')) if r.find(f'{WNS}rPr') is not None else None
                    
                    # 1. Trim原INS文本（去掉被替换的句子）
                    t.text = t.text.replace(old_sentence, "")
                    
                    # 2. 创建DEL
                    del_elem = etree.Element(f'{WNS}del')
                    del_elem.set(f'{WNS}id', next_id())
                    del_elem.set(f'{WNS}author', 'WB')
                    del_elem.set(f'{WNS}date', rev_date)
                    del_r = etree.SubElement(del_elem, f'{WNS}r')
                    if rpr_copy: del_r.insert(0, copy.deepcopy(rpr_copy))
                    del_r.set(f'{WNS}rsidDel', rsid)
                    del_t = etree.SubElement(del_r, f'{WNS}delText')
                    del_t.set(XML_SPACE, 'preserve')
                    del_t.text = old_sentence
                    
                    # 3. 创建INS
                    ins_elem = etree.Element(f'{WNS}ins')
                    ins_elem.set(f'{WNS}id', next_id())
                    ins_elem.set(f'{WNS}author', 'WB')
                    ins_elem.set(f'{WNS}date', rev_date)
                    ins_r = etree.SubElement(ins_elem, f'{WNS}r')
                    if rpr_copy: ins_r.insert(0, copy.deepcopy(rpr_copy))
                    ins_r.set(f'{WNS}rsidR', rsid)
                    ins_t = etree.SubElement(ins_r, f'{WNS}t')
                    ins_t.set(XML_SPACE, 'preserve')
                    ins_t.text = new_sentence
                    
                    # 4. 插入到原INS之后（addnext保证顺序）
                    child.addnext(ins_elem)   # 后插的在后面
                    child.addnext(del_elem)   # 后插的在前面 → 最终: [原INS] [DEL] [INS]
```

**关键点**：
- `addnext` 两次：先插INS再插DEL，后插的排前面，最终顺序：`[原INS(trimmed)] [DEL旧句] [INS新句]`
- 绝不能 `p.remove(child)` 再重建——会丢失INS中其他未改动的文本
- rPr必须从原INS的run深拷贝，不要从全文body_rpr取（字号可能不同）

### 给全INS段落补充条款编号（2026-07-01）

段落所有文本都是WB INS时，编号INS插到pPr之后：

```python
ins_num = etree.Element(f'{WNS}ins')
ins_num.set(f'{WNS}id', next_id()); ins_num.set(f'{WNS}author', 'WB'); ins_num.set(f'{WNS}date', rev_date)
ins_r = etree.SubElement(ins_num, f'{WNS}r')
ins_r.insert(0, copy.deepcopy(existing_rpr))  # 从同段落INS run深拷贝
ins_r.set(f'{WNS}rsidR', rsid)
ins_t = etree.SubElement(ins_r, f'{WNS}t')
ins_t.set(XML_SPACE, 'preserve'); ins_t.text = "第X条  "

ppr = p.find(f'{WNS}pPr')
if ppr is not None: ppr.addnext(ins_num)
else: p.insert(0, ins_num)
```

### 新INS元素eastAsia字体显式补齐

原文WB INS run可能**没有显式eastAsia属性**（靠docDefaults回退），但新INS run**必须显式设置eastAsia=宋体**，否则修订上下文中可能丢失回退。post-save sweep：

```python
for p in body.findall(f'{WNS}p'):
    for ins in p.findall(f'{WNS}ins'):
        for r in ins.findall(f'{WNS}r'):
            text = ''.join(t.text for t in r.findall(f'{WNS}t') if t.text)
            if not any('\u4e00' <= c <= '\u9fff' for c in text): continue
            rpr = r.find(f'{WNS}rPr')
            if rpr is None:
                rpr = etree.Element(f'{WNS}rPr'); r.insert(0, rpr)
            rf = rpr.find(f'{WNS}rFonts')
            if rf is None: rf = etree.SubElement(rpr, f'{WNS}rFonts')
            if not rf.get(f'{WNS}eastAsia'): rf.set(f'{WNS}eastAsia', '宋体')
            if not rf.get(f'{WNS}ascii'): rf.set(f'{WNS}ascii', '宋体')
```

## 验证方法
```python
# 验证所有作者已更改
content = z_out.read('word/document.xml').decode('utf-8', 'ignore')
authors = set(re.findall(r'w:author="([^"]+)"', content))
assert '旧作者名' not in authors, f"仍有旧作者: {authors}"

# 验证批注作者
if 'word/comments.xml' in z_out.namelist():
    with z_out.open('word/comments.xml') as f:
        ctree = etree.parse(f)
    for c in ctree.getroot().findall(f'{WNS}comment'):
        assert c.get(f'{WNS}author') != '旧作者名'
```

## ⚠️ 铁律
1. **修改前必须备份原文件**：覆盖含第三方修订的文件 = 不可逆丢失
2. **只改作者名，不改文本**：除非明确要求修改INS内容
3. **zipfile不能原地读写**：必须先读后写临时文件，再用os.replace
4. **保留comments.xml中的批注锚点**：只改author属性，不改id/content/anchor

## 实证（2026-07-01 反委托代发工资协议）
华诚-Z在OnlyOffice中做了3处修订（第六条去法条引用、第七条简化纠正流程、第八条加退回员工安置）。后续制作版本时覆盖了所有中间文件，导致华诚-Z修订痕迹丢失。最终通过系统化文件扫描在/tmp/v1_doro_updated.docx中找到仍含华诚-Z作者的文件，提取修订内容后在最终版本中恢复。
