feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
# 修改已有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作者的文件,提取修订内容后在最终版本中恢复。
|
||||
Reference in New Issue
Block a user