feat: export core Hermes skills

This commit is contained in:
2026-07-15 02:45:56 +00:00
parent a028b63eda
commit 54711fee2a
308 changed files with 41310 additions and 1 deletions
@@ -0,0 +1,189 @@
# Same-Template Revision Transfer (同模板修订参照)
When Doro says "参照X合同的修订进行修订" — apply the same WB revisions from a reference contract to another contract using the same template.
## ⚠️ Doro 强制验证纪律(2026-07-08 明确要求)
Doro 明确要求做同模板修订参照时必须走完以下步骤,缺一不可:
1. **先确认模板一致性**:逐段对比两份合同原文(去掉修订后的文本),确认段落数一致、差异仅限业务内容(项目名称、单价等),其余结构完全相同。打印差异段数/总段数(如"9/62段有差异")。
2. **参照修订**:提取参照合同的WB修订→适配目标合同业务语境→应用
3. **格式/字体/编号全检**:所有INS的rPr必须与前后邻居run一致(逐个检查sz/rFonts/bold)。遵守workflow规则(author=WB、精准到字、不整段del+ins)
4. **全文阅读审查合理性**:渲染accept后全文,逐段通读确认修订逻辑合理、不破坏上下文语义
5. **交付前检查**:python-docx可打开、无异常字符、修订数与参照合同一致、他人修订保持不动
**不能跳步直接做修订然后上传。** Doro原话:"你先确认:两个合同是不是模板一样,内容一样;如果一样,参照修改;你修改的格式、字体、编号等,都要遵守workflow的规则;全文阅读,修订是否合理。最后检查交付。"
## Workflow
### Step 1: Extract revisions from reference contract
```python
import zipfile
from lxml import etree
ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
def extract_wb_revisions(filepath):
"""Extract all WB-authored INS and DEL from a contract."""
with zipfile.ZipFile(filepath, 'r') as z:
content = z.read('word/document.xml')
tree = etree.fromstring(content)
revisions = []
for ins in tree.iter(f'{{{ns}}}ins'):
if ins.get(f'{{{ns}}}author') != 'WB':
continue
texts = [t.text for t in ins.iter(f'{{{ns}}}t') if t.text]
# Get parent paragraph for context
parent_p = ins
while parent_p is not None and parent_p.tag != f'{{{ns}}}p':
parent_p = parent_p.getparent()
p_texts = [t.text for t in parent_p.iter(f'{{{ns}}}t') if t.text] if parent_p is not None else []
revisions.append({
'type': 'INS', 'text': ''.join(texts),
'para_context': ''.join(p_texts)[:120]
})
for d in tree.iter(f'{{{ns}}}del'):
if d.get(f'{{{ns}}}author') != 'WB':
continue
texts = [t.text for t in d.iter(f'{{{ns}}}delText') if t.text]
parent_p = d
while parent_p is not None and parent_p.tag != f'{{{ns}}}p':
parent_p = parent_p.getparent()
p_texts = [t.text for t in parent_p.iter(f'{{{ns}}}t') if t.text] if parent_p is not None else []
revisions.append({
'type': 'DEL', 'text': ''.join(texts),
'para_context': ''.join(p_texts)[:120]
})
return revisions
```
### Step 2: Identify modification patterns
Group INS/DEL pairs by paragraph context to understand what was changed:
- Simple text replacement: DEL "协议" + INS "合同" in same paragraph
- Text insertion: INS without corresponding DEL (e.g., data ownership sentence)
- Prefix insertion: INS "上海市" before existing text
### Step 3: Context adaptation
When the template is shared but service content differs, adapt context-specific terms:
- "体检服务" → "口腔检查服务"
- "学生个人信息、健康检查结果" → "个人信息、检查结果"
- Keep legal boilerplate identical (e.g., "归甲方所有", "合同期满")
### Step 4: Apply to target contract (zipfile+lxml)
Use three operations:
#### A. Tracked replace (DEL old + INS new)
```python
def do_tracked_replace(para, find_text, replace_text):
"""Find text in paragraph runs, create DEL + INS."""
# Build character map from runs (skip ins/del elements)
char_map = []
for elem in para:
if elem.tag == f'{{{ns}}}r':
t = elem.find(f'{{{ns}}}t')
if t is not None and t.text:
for ci in range(len(t.text)):
char_map.append((elem, t, ci))
full_text = ''.join(cm[1].text[cm[2]] for cm in char_map)
pos = full_text.find(find_text)
if pos == -1:
return False
# Verify single-run containment, then split into before/DEL/INS/after
# ... (see session code for full implementation)
```
#### B. Insert before text
```python
def do_tracked_insert_before(para, anchor_text, insert_text):
"""Insert INS element right before anchor_text."""
for elem in para:
if elem.tag == f'{{{ns}}}r':
t = elem.find(f'{{{ns}}}t')
if t is not None and t.text and anchor_text in t.text:
# Split run, insert INS before anchor portion
...
```
#### C. Insert after text
```python
def do_tracked_insert_after(para, anchor_text, insert_text):
"""Insert INS element right after anchor_text."""
for elem in para:
if elem.tag == f'{{{ns}}}r':
t = elem.find(f'{{{ns}}}t')
if t is not None and t.text and anchor_text in t.text:
# Split run, insert INS after anchor portion
...
```
## Data Attribution Rule (2026-07-08 Doro clarification)
When writing data ownership/attribution clauses across same-template contracts:
**LOCKED (identical across all contracts)**: 归属表述 = "归甲方或相关权利方所有"
- NOT "归甲方所有" (excludes data subjects' rights under PIPL)
- The phrase "甲方或相关权利方" covers both: data甲方 owns (aggregated stats, service outputs) AND personal info that belongs to data subjects
**NOT LOCKED (varies by contract)**: The descriptive content before the attribution phrase
- 体检合同: "乙方在提供体检服务过程中获取和产生的全部数据(包括但不限于学生个人信息、健康检查结果等)"
- 口腔检查合同: "乙方在提供口腔检查服务过程中获取和产生的全部数据(包括但不限于个人信息、检查结果等)"
- Other contracts: adapt to the specific service/data context
**Doro原话**: "我只需要涉及到数据权利的归属时,把归属谁改成'归甲方或相关权利方所有',其他的内容不同合同会不同,所以你不能写死。"
**Rule source**: `review-rules.md` §4 保密/数据 (updated 2026-07-08)
## Pitfalls
### 1. `<w:proofErr>` splits runs
Text like "青浦区练塘镇" may be split into multiple runs separated by `<w:proofErr>` elements:
```xml
<w:r><w:t>青浦区练塘</w:t></w:r>
<w:proofErr w:type="gramStart"/>
<w:r><w:t>镇社区卫生服务中心</w:t></w:r>
```
**Fix**: Search at individual run level (`for elem in para: if elem.tag == w:r`), not at full paragraph text level. Insert INS before the run containing the anchor, not at a text position within full paragraph text.
### 2. "达成如下协议" — not all "协议" should be replaced
In the reference contract, "达成如下协议:" was NOT changed (it's a formulaic expression meaning "reached the following agreement"). Only contextual uses of "协议" meaning "this agreement/contract" were changed to "合同"/"本合同".
**Rule**: Compare reference contract's accepted text to determine which instances were changed and which were left alone.
### 3. INS rPr must clone from target run (not reference)
The target contract's runs may have different formatting than the reference. Always clone rPr from the **target paragraph's existing run**, not from the reference contract.
### 4. Order of operations matters
Do replacements BEFORE insertions. Insertions change paragraph structure and character positions, which can break subsequent text searches.
Recommended order:
1. All `do_tracked_replace` calls (these only split existing runs)
2. All `do_tracked_insert_after` / `do_tracked_insert_before` calls (these add new elements)
### 5. Verify with accept-all view
After applying all revisions, verify by building accepted text (skip DEL, include INS) for key paragraphs and comparing against the reference contract's accepted text.
## 2026-07-08 实证:练塘口腔检查合同
Reference: 【修】2026年学生体检外包合同--练塘(1).docx
Target: 2026年口腔检查外包合同--练塘.docx
Modifications applied:
| # | Type | Content | Adaptation |
|---|------|---------|------------|
| 1 | INS before "青浦区" | "上海市" | None (identical) |
| 2a | INS after "保密义务。" | Data ownership sentence | "体检服务"→"口腔检查服务", "学生个人信息、健康检查结果"→"个人信息、检查结果" |
| 2b | DEL "协议" + INS "合同" | "协议期满"→"合同期满" | None |
| 2c | DEL "服务协议" + INS "本合同" | "服务协议解除"→"本合同解除" | None |
| 2d | DEL "本协议" + INS "本合同" | "本协议的履行"→"本合同的履行" | None |
| 3 | DEL "本协议" + INS "本合同" | "本协议一式"→"本合同一式" | None |
proofErr pitfall encountered: "青浦区练塘" split by `<w:proofErr>` — had to insert at run level rather than text-position level.