feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
# Multi-Version Creation Pattern
|
||||
|
||||
## When to Use
|
||||
When creating multiple versions of the same contract (e.g., 版本1法定安排 vs 版本2反委托保护) or when needing to redo a version from scratch.
|
||||
|
||||
## Critical Rule
|
||||
**ALWAYS start from the original source file for each version. Never modify a previously modified version.**
|
||||
|
||||
## Step-by-Step Pattern
|
||||
|
||||
### 1. Preserve Original Source
|
||||
```python
|
||||
# First time: copy original to safe location
|
||||
shutil.copy('/path/to/original.docx', '/tmp/original_backup.docx')
|
||||
```
|
||||
|
||||
### 2. For Each Version, Start Fresh
|
||||
```python
|
||||
# Always reload from original
|
||||
with zipfile.ZipFile('/tmp/original_backup.docx', 'r') as zin:
|
||||
all_data = {n: zin.read(n) for n in zin.namelist()}
|
||||
|
||||
doc_xml = all_data['word/document.xml']
|
||||
root = etree.fromstring(doc_xml)
|
||||
body = root.find(f'{W}body')
|
||||
paras = body.findall(f'{W}p')
|
||||
|
||||
# Get font template from original
|
||||
rpr_template = None
|
||||
for p in paras:
|
||||
for r in p.findall(f'{W}r'):
|
||||
t = r.find(f'{W}t')
|
||||
if t is not None and t.text and t.text.strip():
|
||||
rpr_elem = r.find(f'{W}rPr')
|
||||
if rpr_elem is not None:
|
||||
rpr_template = copy.deepcopy(rpr_elem)
|
||||
break
|
||||
if rpr_template:
|
||||
break
|
||||
```
|
||||
|
||||
### 3. Apply All Modifications in One Pass
|
||||
```python
|
||||
rev_id = 1000 # Start fresh revision ID counter
|
||||
|
||||
# Batch all replacements
|
||||
replacements = [
|
||||
(0, "old text", "new text"),
|
||||
(5, "old text", "new text"),
|
||||
# ... more replacements
|
||||
]
|
||||
|
||||
for idx, old_text, new_text in replacements:
|
||||
p = paras[idx]
|
||||
# Clear runs (but NOT comment anchors!)
|
||||
for child in list(p):
|
||||
tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
||||
if tag == 'r': # Only remove regular runs
|
||||
p.remove(child)
|
||||
d, i = make_tracked_replace(old_text, new_text, rpr_template, rev_id)
|
||||
rev_id += 2
|
||||
p.append(d)
|
||||
p.append(i)
|
||||
|
||||
# Insert new clauses
|
||||
insert_after = paras[10]
|
||||
for clause_text in new_clauses:
|
||||
new_p = make_ins_paragraph(clause_text, rpr_template, rev_id)
|
||||
rev_id += 1
|
||||
insert_after.addnext(new_p)
|
||||
insert_after = new_p
|
||||
|
||||
# Mark deletions (e.g., 承诺书)
|
||||
for idx in range(21, 30):
|
||||
p = paras[idx]
|
||||
text_parts = []
|
||||
for r in p.findall(f'{W}r'):
|
||||
t = r.find(f'{W}t')
|
||||
if t is not None and t.text:
|
||||
text_parts.append(t.text)
|
||||
full_text = ''.join(text_parts)
|
||||
if not full_text.strip():
|
||||
continue
|
||||
for child in list(p):
|
||||
tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
||||
if tag == 'r':
|
||||
p.remove(child)
|
||||
del_elem = make_tracked_delete(full_text, rpr_template, rev_id)
|
||||
rev_id += 1
|
||||
p.append(del_elem)
|
||||
|
||||
# Add comments LAST (after all structural changes)
|
||||
# Comment anchors are fragile - add them at the end
|
||||
```
|
||||
|
||||
### 4. Add Comments Carefully
|
||||
```python
|
||||
# Check if paragraph already has comment anchors
|
||||
existing = p.find(f'{W}commentRangeStart')
|
||||
if existing is None:
|
||||
# Add new comment anchors
|
||||
comment_start = etree.Element(f'{W}commentRangeStart')
|
||||
comment_start.set(f'{W}id', str(comment_id))
|
||||
p.insert(0, comment_start)
|
||||
|
||||
comment_end = etree.Element(f'{W}commentRangeEnd')
|
||||
comment_end.set(f'{W}id', str(comment_id))
|
||||
p.append(comment_end)
|
||||
|
||||
comment_ref_run = etree.SubElement(p, f'{W}r')
|
||||
comment_ref = etree.SubElement(comment_ref_run, f'{W}commentReference')
|
||||
comment_ref.set(f'{W}id', str(comment_id))
|
||||
|
||||
# Update comments.xml
|
||||
if 'word/comments.xml' in all_data:
|
||||
croot = etree.fromstring(all_data['word/comments.xml'])
|
||||
else:
|
||||
croot = etree.Element(f'{W}comments', nsmap={'w': W_NS})
|
||||
|
||||
# Add or update comment
|
||||
new_comment = etree.SubElement(croot, f'{W}comment')
|
||||
new_comment.set(f'{W}id', str(comment_id))
|
||||
new_comment.set(f'{W}author', author)
|
||||
new_comment.set(f'{W}date', datetime.now().isoformat())
|
||||
|
||||
p = etree.SubElement(new_comment, f'{W}p')
|
||||
r = etree.SubElement(p, f'{W}r')
|
||||
t = etree.SubElement(r, f'{W}t')
|
||||
t.set(XML_SPACE, 'preserve')
|
||||
t.text = comment_text
|
||||
```
|
||||
|
||||
### 5. Save and Verify
|
||||
```python
|
||||
all_data['word/document.xml'] = etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True)
|
||||
all_data['word/comments.xml'] = etree.tostring(croot, xml_declaration=True, encoding='UTF-8', standalone=True)
|
||||
|
||||
with zipfile.ZipFile('/tmp/version1.docx', 'w', zipfile.ZIP_DEFLATED) as zout:
|
||||
for name, data in all_data.items():
|
||||
zout.writestr(name, data)
|
||||
|
||||
# Verify immediately
|
||||
doc = Document('/tmp/version1.docx')
|
||||
print(f"OK: {len(doc.paragraphs)} paragraphs")
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Don't Do This
|
||||
```python
|
||||
# WRONG: Modifying v1 to create v2
|
||||
shutil.copy('/tmp/v1.docx', '/tmp/v2.docx')
|
||||
with zipfile.ZipFile('/tmp/v2.docx', 'r') as zin:
|
||||
# ... load v1's modified structure
|
||||
# This will have v1's tracked changes, comments, etc.
|
||||
```
|
||||
|
||||
### ❌ Don't Clear Everything When Modifying
|
||||
```python
|
||||
# WRONG: Clears comment anchors too!
|
||||
for child in list(p):
|
||||
if child.tag not in (f'{W}pPr',):
|
||||
p.remove(child) # Removes commentRangeStart/End!
|
||||
```
|
||||
|
||||
### ✅ Do This Instead
|
||||
```python
|
||||
# RIGHT: Only clear regular runs, preserve comment anchors
|
||||
for child in list(p):
|
||||
tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
||||
if tag == 'r': # Only regular runs
|
||||
p.remove(child)
|
||||
# commentRangeStart, commentRangeEnd are preserved
|
||||
```
|
||||
|
||||
## Preserving Original Comments
|
||||
When the original document has comments (e.g., Alice, 法务, 杜律), the workflow must:
|
||||
1. Read original comments.xml to get all comment IDs and content
|
||||
2. Check which paragraphs have comment anchors (commentRangeStart/End)
|
||||
3. When clearing runs, preserve comment anchors (they're not `w:r` elements)
|
||||
4. Add new comments with NEW IDs (don't reuse original IDs)
|
||||
5. Original comments remain unchanged in comments.xml
|
||||
|
||||
## Preserving Third-Party Tracked Changes (2026-07-01 华诚-Z案)
|
||||
|
||||
When a contract file contains tracked changes from someone other than WB (e.g., 华诚-Z, Crystall, or any third-party reviewer), **those files must never be overwritten**. The tracked changes represent real editorial work that cannot be reconstructed from session notes alone.
|
||||
|
||||
### Backup Protocol
|
||||
```python
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# BEFORE any modification to a file with third-party tracked changes:
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_path = f'/tmp/{filename}.bak_{ts}'
|
||||
shutil.copy(source_path, backup_path)
|
||||
print(f"Backed up to {backup_path}")
|
||||
```
|
||||
|
||||
### Detection: Does This File Have Third-Party Changes?
|
||||
```python
|
||||
import zipfile, re
|
||||
with zipfile.ZipFile(filepath) as z:
|
||||
content = z.read('word/document.xml').decode('utf-8', errors='ignore')
|
||||
authors = set(re.findall(r'w:author="([^"]+)"', content))
|
||||
third_party = authors - {'WB'}
|
||||
if third_party:
|
||||
print(f"⚠️ Third-party authors found: {third_party} — BACKUP REQUIRED")
|
||||
```
|
||||
|
||||
### Multi-Version with Third-Party Edits
|
||||
When creating v1 and v2 from a file that has both original content AND third-party edits:
|
||||
|
||||
1. **Backup the file with third-party edits** (e.g., `华诚-Z版.bak_20260701`)
|
||||
2. **Backup the pristine original** (no tracked changes at all)
|
||||
3. **For each version**: start from the pristine original, then layer on:
|
||||
- WB's own tracked changes
|
||||
- Third-party's tracked changes (with author renamed to WB)
|
||||
4. **Never modify the backup files** — they are your insurance
|
||||
|
||||
### What Was Lost (华诚-Z案)
|
||||
- 华诚-Z made 3 tracked changes in OnlyOffice: 第六条 (removed specific legal citations), 第七条 (simplified correction process), 第八条 (added employee return placement clause)
|
||||
- These intermediate files in /tmp/ were overwritten during v1/v2 creation
|
||||
- Only session notes preserved the *content* of changes, not the actual tracked change markup (ids, timestamps, exact XML positions)
|
||||
- **Recovery was impossible** — Doro had to accept reconstructed versions
|
||||
|
||||
## Real Example from This Session
|
||||
- Original: 4 comments (Alice×2, 法务, 杜律)
|
||||
- Version 1: 5 comments (original 4 + WB legal risk)
|
||||
- Version 2: 5 comments (original 4 + WB legal risk)
|
||||
|
||||
Both versions created independently from original, each with their own WB comment (different content for each version).
|
||||
Reference in New Issue
Block a user