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,113 @@
# 在「自动编号的顶层列表」中新增条款(保留numPr)
2026-06-18 华新慢病运维合同实战。一次返工换来的教训。
## 适用判定(动手前先分清两类合同)
合同的「条款」分两种承载方式,新增条款的手法完全不同:
| 类型 | 特征 | 新增条款手法 |
|------|------|-------------|
| **A. 第X条 文本标题** | 条款标题是 run 里的文字「第七条 …」/「7. …」,段落**无** numPr | `add_clause()`(库会剥 numPr,正确)|
| **B. 自动编号列表项** | 条款本身是自动编号列表项:段落 pPr 带 `<w:numPr>`,编号由 numbering.xml 的 `start`+`lvlText`(如 `一、`/`1.`/`(1)`)自动渲染,run 里**没有**编号文字 | ❌ 不能用 `add_clause()`;按下方「numbered-insert」手法 |
**判定脚本**:对要插入位置附近的条款段落跑 `numbering-diagnose.py`,或直接看锚点段 `pPr/numPr` 是否存在且其 numId 的 lvlText 是序号格式。本案锚点「五、违约责任」段 `pPr` = `pStyle=12 + numPr(numId=1,ilvl=0) + ind`,numId=1→abstractNum start=1 lvlText=`%1、`(japaneseCounting 一、二、三)。
## 为什么 add_clause 在 B 类会坏
`add_clause()`(contract_docx_lib.py 第408-411行)**无条件**剥离新段的 numPr:
```python
numpr = new_ppr.find(qn('numPr'))
if numpr is not None:
new_ppr.remove(numpr) # ← B类灾难
```
后果(本案实测):5 个新增条款全部**丢失自动编号**,且因 pPr 缺 numPr/缩进与列表项不一致,渲染时**堆到了文档最末尾**(签署页之前),既无编号又错位——违反「新增条款插在逻辑对应位置、不堆到最后」+「自动编号保留numPr」两条规则。
`add_clause` 第二个缺陷:它只把**文本** run 包进 w:ins,**没有把段落标记(¶)标记为插入**。B 类里 ¶ 承载着自动编号,¶ 不是 tracked-insert,则接受/拒绝修订时这一项的编号增减不随修订走。
## 正确手法:numbered tracked-insert 段落
克隆锚点段的 pPr(**保留** numPr,让新段成为同一自动编号序列的一员),并把**段落标记本身**也标成 w:ins:
```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)
# 1) 定位锚点段(要插在它之后的那条原文条款)
anchor = None
for p in ed.body.findall(qn('p')):
if '违约责任:按照中华人民共和国民法典' in ed.get_para_text(p):
anchor = p; break
anchor_ppr = anchor.find(qn('pPr'))
assert anchor_ppr.find(qn('numPr')) is not None, "锚点不是自动编号项,确认是否B类"
def make_numbered_ins_para(text):
new_p = etree.Element(qn('p'))
new_ppr = copy.deepcopy(anchor_ppr) # 含 pStyle + numPr(同numId/ilvl) + ind → 入同一自动编号序列
# 关键:把段落标记(¶)标成插入,整段(含自动编号)作为 tracked insertion
rpr_mark = new_ppr.find(qn('rPr'))
if rpr_mark is None:
rpr_mark = etree.SubElement(new_ppr, qn('rPr'))
ins_mark = etree.SubElement(rpr_mark, qn('ins'))
ins_mark.set(qn('id'), ed._next_id())
ins_mark.set(qn('author'), 'WB')
ins_mark.set(qn('date'), ed._revision_date)
new_p.append(new_ppr)
# 文本作为 tracked-ins run,用规范化 _body_rpr(完整rFonts四属性+hint=eastAsia+显式sz)
new_p.append(ed._mk_ins(text, ed._body_rpr))
return new_p
clauses = [ # 期望的最终正序 六~十
"保密与数据:……",
"知识产权与系统交接:……",
"转包与分包:……",
"第三方侵权:……",
"违约赔偿:……",
]
# 2) 全部插在 anchor 之后;倒序 insert 使最终正序
parent = ed.body
anchor_idx = list(parent).index(anchor)
for txt in reversed(clauses):
parent.insert(anchor_idx + 1, make_numbered_ins_para(txt))
assert ed.validate() == []
ed.save(out)
```
要点:
- **倒序插入**:每条都插在 `anchor_idx+1`,倒序遍历 → 最终正序。
- **同一 numId/ilvl**:克隆锚点 pPr 即自动继承,新条款自动续编(本案锚点是五 → 新条款渲染为六、七、八、九、十,后续原文自动顺延为十一、十二…,**无需手动改任何原文编号**)。
- **¶ 标插入** + **文本 run 标插入**,两者都要,缺一不可。
- 文本 run 用 `ed._body_rpr`(库已规整:四属性 rFonts + hint=eastAsia + 显式 sz),不要手搓 rPr。
## 交付前验证(B 类专项)
1. **接受所有修订后**渲染(删 w:del + 删带 `pPr/rPr/del` 的整段 + 解包 w:ins)→ 确认新条款编号与锚点连续、原文顺延正确、无错位到末尾。
2. **字体核对走「同段原文」标准**:本案原文正文 run = `<w:rFonts hint="eastAsia"/><w:szCs val="21"/>`(**无**显式 eastAsia 名,继承 docDefaults 宋体)。新 INS run 与之等效即合格——`ea=None hint=eastAsia` 是**正确**的,`wb-ins-font-verify.py` 若按绝对属性报 `ea=None` 是假阳性(见 contract-reviewer 的 2026-06-17 培训合同条)。唯一差异是 INS 多了显式 `<w:sz val="21">`(w:ins 必需),渲染一致。
3. **LibreOffice 渲染假象**:用 `libreoffice→pdftotext` 自查时,被顺延的自动编号项会显示 `十二、[七、]` 这种**方括号叠加**(recomputed 新号 + cached 旧号),这是 LibreOffice markup 渲染产物,**不是错误**,XML 里没有字面方括号。判真实编号一律以「接受所有修订后」或 OnlyOffice 渲染为准(OnlyOffice 是 Maggie/Doro 实际所用引擎)。
## 锚点选择铁律:插在 body text 之后,不是 heading 之后
**这是一个极易犯的错误**(2026-06-26 朱家角环保袋合同实证):
当Reviewer要求"在违约责任条款之后、争议解决条款之前新增XX条款"时,合同结构通常是:
```
P74: 八.违约责任 ← heading(numId=1)
P75: 若乙方未按本合同... ← body text(无 numPr)
P76: 九.合同金额 ← 下一个 heading(numId=1)
```
**错误做法**:锚点 = P74(heading),插入后 → 新条款夹在 heading 和它的 body text 之间,结构错乱。
**正确做法**:锚点 = P75(body text),插入后 → 新条款在 body text 之后、下一个 heading 之前,结构正确。
**判据**`numbering-diagnose.py` 确认锚点段的 `numPr` 状态——heading 有 numPr,body text 无 numPr。新条款应克隆**下一个 heading**(如 P76 合同金额)的 pPr(含 numPr),插入在**前一个 body text**(如 P75)之后。
## 一句话
锚点是自动编号列表项(pPr 有 numPr)→ 别用 add_clause,克隆锚点 pPr(留 numPr)+ ¶ 标 w:ins + 文本标 w:ins,倒序插入,新条款自动续编、原文自动顺延。**插在 body text 之后,不是 heading 之后。**