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,95 @@
# A类(手动文本编号)新增条款:克隆"真实邻居段落"而非信任库的 _title_rpr / _body_rpr
2026-06-18 赵巷镇 X线设备采购合同实战。终审字体核验抓出"新标题不加粗",根因是库提取的标题格式丢了 bold。
## 何时用这套手法
- 合同是 **A 类**:条款标题是 run 里的**手动文本编号**(如 `8.争端的解决``第七条 索赔`),段落**无** numPr。
- 要新增一个带标题的条款(标题段 + 正文段),并希望格式与兄弟条款 100% 一致。
- (B 类自动编号列表项见 `auto-numbered-list-clause-insert.md`,手法不同。)
## 为什么不直接用库的 `_title_rpr` / `_body_rpr`
`ContractEditor._extract_formats()`(contract_docx_lib.py ~第86-175行)用启发式认"条款标题":
```python
is_clause_title = (re.match(r'^\d+[..、]\s*\S', p_text) or
re.match(r'^第[一二三四五六七八九十百千\d]+条\s*\S', p_text)) and len(p_text) < 30
# 且 _is_title_style() 要 <w:b/> 或标题字体(黑体/SimHei…) 才算 title
```
**坑**:当标题就是"8.争端的解决"(宋体 + `<w:b/>`,无特殊标题字体),若该段在扫描中**没被 `_is_title_style` 命中**(例如 b 标记在 bCs 旁、或正则边界),`clause_title_rpr` / `first_bold_rpr` 取空 → `_title_rpr` **回退到 `_body_rpr`(不含 bold)**
实测后果:新标题 `8.转包与分包` 的 INS run `bold=False`,而原文兄弟标题 `9.争端的解决` `bold=True``validate()` 查不出(它不比 bold),只有逐段 WB INS 与"同段/同级原文"对比才抓得到。
## 稳健手法:克隆紧邻同级原文段落
不取库的 `_title_rpr`/`_body_rpr`,改为**直接深拷贝隔壁真实条款段**的 pPr 和首个 run 的 rPr:
```python
import sys, copy
sys.path.insert(0, '/home/maggie/contract-work')
from contract_docx_lib import ContractEditor, qn
from lxml import etree
ed = ContractEditor(src) # 已先做完所有 tracked_replace
def find(kw):
for p in ed.body.findall(qn('p')):
if kw in ''.join(t.text or '' for t in p.findall(f'.//{qn("t")}')):
return p
return None
# 克隆来源:插入点后面那条原文条款的【标题段】和它的【正文段】
title_src = find('8.争端的解决') # 兄弟条款标题(自带 <w:b/> + 宋体四属性 + 标题pPr缩进)
body_src = find('双方如在履行合同中发生纠纷') # 兄弟条款正文(无bold + firstLine=420 缩进)
def clone_as_ins(src_para, new_text):
"""深拷贝 src_para 的 pPr + 首run rPr,替换文本,整段(含¶)标 w:ins(author=WB)"""
np = etree.Element(qn('p'))
ppr = copy.deepcopy(src_para.find(qn('pPr')))
# ¶ 段落标记标插入
rprm = ppr.find(qn('rPr'))
if rprm is None:
rprm = etree.SubElement(ppr, qn('rPr'))
insm = etree.SubElement(rprm, qn('ins'))
insm.set(qn('id'), ed._next_id()); insm.set(qn('author'), 'WB'); insm.set(qn('date'), ed._revision_date)
np.append(ppr)
# run rPr 直接克隆兄弟段首 run(bold/字体/字号全继承,不碰库的默认值)
src_r = src_para.find(qn('r'))
src_rpr = copy.deepcopy(src_r.find(qn('rPr'))) if (src_r is not None and src_r.find(qn('rPr')) is not None) else None
np.append(ed._mk_ins(new_text, src_rpr))
return np
title_p = clone_as_ins(title_src, "8.转包与分包")
body_p = clone_as_ins(body_src, "未经甲方书面同意,乙方不得将本合同项下的…连带责任。")
idx = list(ed.body).index(title_src)
ed.body.insert(idx, title_p) # 标题插在兄弟条款标题之前 → 成为新的"8.",兄弟顺延为"9."
ed.body.insert(idx + 1, body_p)
```
## A类手动编号的顺延(与 B 类自动顺延不同!)
A 类编号是 run 里的字面文字,**不会自动顺延**。插入新"8."后,必须手动把后续所有手动编号 DEL 旧号+INS 新号(用 tracked_replace):
```python
for old, new in [("8.争端的解决","9.争端的解决"), ("9.合同生效","10.合同生效"),
("9.1 本合同在…","10.1 本合同在…"), ("10.合同附件","11.合同附件"),
("10.1 配置清单","11.1 配置清单"), ..., ("12.特别约定","13.特别约定")]:
ed.tracked_replace(old, new)
```
- **子编号一并顺延**(9.1/9.2→10.1/10.2,11.1-11.7→12.1-12.7)。
- 匹配串要够长以避免短串误命中(见 SKILL.md「tracked_replace 短字符串误命中」)。
## 交付前验证(必做)
1. **bold 对照**:新标题 INS run `bold==True` 且 ==兄弟标题;新正文 INS run `bold==False` 且有正确 `firstLine` 缩进。
```python
r = p.find('.//w:ins/w:r', ns); b = r.find('w:rPr/w:b', ns)
# 标题段 b is not None == 兄弟标题段 b is not None
```
2. **WB INS 字体逐段核验(相对同段/同级原文)**:异常应为 0。原文 run 有显式宋体四属性时,克隆来的 INS 也带四属性——与原文一致即合格。
3. **接受所有修订后渲染**,确认手动编号链连续(…7、**8.转包**、9、10、10.1、10.2、11…13),无重号/跳号。
4. python-docx 能打开(XML 合法)。
## 一句话
A 类手动编号合同新增带标题条款:**别用库的 `_title_rpr`/`_body_rpr`(启发式可能丢 bold)**,直接 `copy.deepcopy` 紧邻兄弟条款的【标题段】和【正文段】的 pPr+首run rPr,文本替换+整段标 w:ins;编号不会自动顺延,手动 tracked_replace 把后续主/子编号全部 +1。