feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# Comment Restoration from Original File
|
||||
|
||||
When comments are lost during docx editing (e.g., paragraph clear operations that remove commentRangeStart/End/Reference elements), restore them from the original file.
|
||||
|
||||
## Scenario
|
||||
- Original file has N comments (e.g., Alice×2, 法务, 杜律 = 4 comments, ids 0-3)
|
||||
- Edited file lost some/all original comments and may have added new ones (e.g., 华诚-Z comment id=0, Alice id=2)
|
||||
- Goal: merge all comments — original ones preserved + new ones added, with non-conflicting IDs
|
||||
|
||||
## Recovery Technique
|
||||
|
||||
### Step 1: Extract original comments
|
||||
```python
|
||||
WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
|
||||
|
||||
z_orig = zipfile.ZipFile('original.docx')
|
||||
with z_orig.open('word/comments.xml') as f:
|
||||
ctree_orig = etree.parse(f)
|
||||
orig_comments = []
|
||||
for c in ctree_orig.getroot().findall(f'{WNS}comment'):
|
||||
orig_comments.append({
|
||||
'id': c.get(f'{WNS}id'),
|
||||
'author': c.get(f'{WNS}author'),
|
||||
'date': c.get(f'{WNS}date'),
|
||||
'text': ''.join(t.text for t in c.iter(f'{WNS}t') if t.text),
|
||||
'element': copy.deepcopy(c)
|
||||
})
|
||||
z_orig.close()
|
||||
```
|
||||
|
||||
### Step 2: Identify which comments survived in the edited file
|
||||
```python
|
||||
z_edit = zipfile.ZipFile('edited.docx')
|
||||
with z_edit.open('word/comments.xml') as f:
|
||||
ctree_edit = etree.parse(f)
|
||||
edit_comment_ids = set()
|
||||
for c in ctree_edit.getroot().findall(f'{WNS}comment'):
|
||||
edit_comment_ids.add(c.get(f'{WNS}id'))
|
||||
```
|
||||
|
||||
### Step 3: Find new comments (non-original authors)
|
||||
```python
|
||||
new_comments = []
|
||||
for c in ctree_edit.getroot().findall(f'{WNS}comment'):
|
||||
if c.get(f'{WNS}author') not in [oc['author'] for oc in orig_comments]:
|
||||
new_comments.append({
|
||||
'old_id': c.get(f'{WNS}id'),
|
||||
'author': c.get(f'{WNS}author'),
|
||||
'element': copy.deepcopy(c)
|
||||
})
|
||||
```
|
||||
|
||||
### Step 4: Rebuild comments.xml with all comments
|
||||
Assign non-conflicting IDs:
|
||||
- Original comments keep their original IDs (0, 1, 2, 3)
|
||||
- New comments get IDs starting from max(original_ids) + 1
|
||||
|
||||
```python
|
||||
new_comments_xml = etree.Element(f'{WNS}comments')
|
||||
new_comments_xml.set('xmlns:w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
|
||||
# ... add other namespaces as needed
|
||||
|
||||
max_id = max(int(oc['id']) for oc in orig_comments)
|
||||
|
||||
# Add original comments
|
||||
for oc in orig_comments:
|
||||
new_comments_xml.append(oc['element'])
|
||||
|
||||
# Add new comments with renumbered IDs
|
||||
for nc in new_comments:
|
||||
max_id += 1
|
||||
nc['new_id'] = str(max_id)
|
||||
nc['element'].set(f'{WNS}id', nc['new_id'])
|
||||
new_comments_xml.append(nc['element'])
|
||||
```
|
||||
|
||||
### Step 5: Update document.xml comment references
|
||||
For each new comment, find its commentRangeStart, commentRangeEnd, and commentReference in document.xml and update the ID from old to new:
|
||||
|
||||
```python
|
||||
for nc in new_comments:
|
||||
old_id = nc['old_id']
|
||||
new_id = nc['new_id']
|
||||
|
||||
# Update commentRangeStart
|
||||
for elem in root.iter(f'{WNS}commentRangeStart'):
|
||||
if elem.get(f'{WNS}id') == old_id:
|
||||
elem.set(f'{WNS}id', new_id)
|
||||
|
||||
# Update commentRangeEnd
|
||||
for elem in root.iter(f'{WNS}commentRangeEnd'):
|
||||
if elem.get(f'{WNS}id') == old_id:
|
||||
elem.set(f'{WNS}id', new_id)
|
||||
|
||||
# Update commentReference (inside w:r)
|
||||
for elem in root.iter(f'{WNS}commentReference'):
|
||||
if elem.get(f'{WNS}id') == old_id:
|
||||
elem.set(f'{WNS}id', new_id)
|
||||
```
|
||||
|
||||
### Step 6: Write back to docx
|
||||
```python
|
||||
z_out = zipfile.ZipFile('output.docx', 'w')
|
||||
# Copy all files from edited.docx except comments.xml and document.xml
|
||||
for item in z_edit.namelist():
|
||||
if item not in ('word/comments.xml', 'word/document.xml'):
|
||||
z_out.writestr(item, z_edit.read(item))
|
||||
|
||||
# Write updated comments.xml
|
||||
z_out.writestr('word/comments.xml',
|
||||
etree.tostring(new_comments_xml, encoding='UTF-8', xml_declaration=True, standalone=True))
|
||||
|
||||
# Write updated document.xml
|
||||
z_out.writestr('word/document.xml',
|
||||
etree.tostring(tree, encoding='UTF-8', xml_declaration=True, standalone=True))
|
||||
|
||||
z_edit.close()
|
||||
z_out.close()
|
||||
```
|
||||
|
||||
## Verification
|
||||
```python
|
||||
z = zipfile.ZipFile('output.docx')
|
||||
with z.open('word/comments.xml') as f:
|
||||
ctree = etree.parse(f)
|
||||
for c in ctree.getroot().findall(f'{WNS}comment'):
|
||||
print(f" id={c.get(f'{WNS}id')} author={c.get(f'{WNS}author')}: {text[:80]}")
|
||||
|
||||
# Check all IDs referenced in document.xml exist in comments.xml
|
||||
content = z.read('word/document.xml').decode('utf-8')
|
||||
doc_ids = set(re.findall(r'commentRangeStart[^>]*w:id="(\d+)"', content))
|
||||
doc_ids |= set(re.findall(r'commentRangeEnd[^>]*w:id="(\d+)"', content))
|
||||
doc_ids |= set(re.findall(r'commentReference[^>]*w:id="(\d+)"', content))
|
||||
comment_ids = set(c.get(f'{WNS}id') for c in ctree.getroot().findall(f'{WNS}comment'))
|
||||
assert doc_ids == comment_ids, f"ID mismatch: doc={doc_ids} comments={comment_ids}"
|
||||
```
|
||||
|
||||
## Key Pitfall: Comment Text Extraction
|
||||
When extracting comment text for comparison, comments may have nested `<w:p>` elements (multi-paragraph comments). Use `.iter()` not `.findall()` to get all text nodes.
|
||||
|
||||
## Empirical Case (2026-07-01 反委托代发工资协议)
|
||||
- Original: 4 comments (Alice id=0, Alice id=1, 法务 id=2, 杜律 id=3)
|
||||
- v1_doro_updated: 2 comments (华诚-Z id=0, Alice id=2) — lost Alice id=0/1, 法务, 杜律
|
||||
- Final: 5 comments (Alice id=0, Alice id=1, 法务 id=2, 杜律 id=3, 华诚-Z id=4)
|
||||
- 华诚-Z's comment was id=0 in v1_doro_updated, renumbered to id=4 in final
|
||||
- All commentRangeStart/End/Reference IDs updated in document.xml accordingly
|
||||
Reference in New Issue
Block a user