feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# Standalone Char-Level Tracked Changes + Comments (non-workflow)
|
||||
|
||||
When modifying contracts **outside** the Doro/邱律师 workflow (e.g. Maggie directly asks to revise a client's agreement), the full `ContractEditor` library + review-rules machinery is overkill. Use this lightweight pattern instead.
|
||||
|
||||
## When to use
|
||||
- Maggie sends a contract and says "帮我改一下" / "修改这份协议"
|
||||
- No workflow, no reviewer, no deliverer — just direct revision
|
||||
- Still must produce Word-native tracked changes (del/ins) + comments
|
||||
|
||||
## Core technique: `difflib.SequenceMatcher` char-level diff
|
||||
|
||||
```python
|
||||
import difflib
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
def char_level_replace(para, new_text, author="WB", date="2026-07-07T10:00:00Z"):
|
||||
"""Replace paragraph text with char-level tracked changes.
|
||||
Unchanged chars → normal w:r (preserved).
|
||||
Deleted chars → w:del + w:delText.
|
||||
Inserted chars → w:ins + w:t.
|
||||
"""
|
||||
p = para._element
|
||||
old_text = para.text
|
||||
if old_text == new_text:
|
||||
return
|
||||
|
||||
# Remove existing runs (preserve pPr)
|
||||
for child in list(p):
|
||||
tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
|
||||
if tag in ('r', 'ins', 'del', 'hyperlink'):
|
||||
p.remove(child)
|
||||
|
||||
sm = difflib.SequenceMatcher(None, old_text, new_text)
|
||||
for op, i1, i2, j1, j2 in sm.get_opcodes():
|
||||
if op == 'equal':
|
||||
p.append(make_run(old_text[i1:i2]))
|
||||
elif op == 'delete':
|
||||
p.append(make_del_run(old_text[i1:i2], author, date))
|
||||
elif op == 'insert':
|
||||
p.append(make_ins_run(new_text[j1:j2], author, date))
|
||||
elif op == 'replace':
|
||||
p.append(make_del_run(old_text[i1:i2], author, date))
|
||||
p.append(make_ins_run(new_text[j1:j2], author, date))
|
||||
```
|
||||
|
||||
## Comments injection (bypassing python-docx limitations)
|
||||
|
||||
python-docx has no native comment support. Inject manually:
|
||||
|
||||
1. Add `commentRangeStart` + `commentRangeEnd` + `commentReference` run to target paragraph
|
||||
2. Build `word/comments.xml` as a plain string (proper namespace, no lxml serialization quirks)
|
||||
3. Inject into the docx ZIP: update `[Content_Types].xml` + `word/_rels/document.xml.rels`
|
||||
|
||||
### Critical: comments.xml namespace
|
||||
|
||||
**Wrong** (causes "reuse of xmlns" error):
|
||||
```python
|
||||
comments_xml = etree.Element(qn('w:comments'))
|
||||
comments_xml.set(qn('xmlns:w'), WNS) # ❌ double declaration
|
||||
```
|
||||
|
||||
**Right** (build as plain string):
|
||||
```python
|
||||
def build_comments_xml(comments_list):
|
||||
lines = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
||||
lines.append('<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
|
||||
' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">')
|
||||
for cid, text in comments_list:
|
||||
safe = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
lines.append(f' <w:comment w:id="{cid}" w:author="WB" w:date="..." w:initials="WB">')
|
||||
lines.append(f' <w:p><w:r><w:t>{safe}</w:t></w:r></w:p>')
|
||||
lines.append(f' </w:comment>')
|
||||
lines.append('</w:comments>')
|
||||
return "\n".join(lines)
|
||||
```
|
||||
|
||||
## Pitfalls learned (2026-07-07 退休返聘案)
|
||||
|
||||
1. **lxml etree serialization breaks Word**: `etree.tostring()` produces `xmlns:ns0=...` prefix notation that Word/OnlyOffice cannot parse. Always build comments.xml as a plain string.
|
||||
2. **Entire-paragraph del+ins is unacceptable**: Maggie and Doro both require char-level precision. "原文相同的部分保留,不一样的用修订" — this is non-negotiable.
|
||||
3. **New paragraphs (fully inserted)**: Use `pPr/rPr/ins` mark to flag the ¶ itself as inserted, plus `w:ins` wrapping the text run. Both are needed for Word to show the full paragraph as tracked insertion.
|
||||
4. **Verify files open correctly**: After save, always `Document(path)` to confirm no XML parse errors.
|
||||
|
||||
## Template (full working script structure)
|
||||
|
||||
See `/tmp/modify_v3_charlevel.py` from the 2026-07-07 session — processes two contracts (full-time + part-time) with char-level diff + comments injection. Pattern: `process_contract(input, output, is_fulltime=bool)`.
|
||||
Reference in New Issue
Block a user