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,114 @@
#!/usr/bin/env python3
"""编号链诊断探针 — 一次性看清 docx 的自动编号/手动编号全貌。
用途:合同编号疑似错乱(重复/跳号/双号)时,动手改之前必跑此脚本。
它把三件事一次性摊开,让你判断「是我们WB改错的 / 他人修订重排的 / 还是源文件自带的潜伏自动编号」:
1. 每个段落:是否带 <w:numPr>(自动编号)、numId、ilvl、是否整段ins/del
2. numbering.xml 解析:numId→abstractNum→(numFmt, lvlText, start) ——
⚠️ start≠1 的 decimal 列表会渲染出「6、」之类的可见编号,但 run 里没有这个字!
这是最隐蔽的坑:源文件起草人给某段挂了 numId(start=6),OnlyOffice 自动显示「6、售后服务」,
而你在末尾新增条款时只数了手打的「1 2 3 4 5」,顺手编成「6」→ 与潜伏的自动6撞号。
3. 每段「接受所有修订后」的可见文本(去w:del、保w:ins),近似 OnlyOffice 接受后视图
用法: python numbering-diagnose.py <contract.docx>
.doc 先转换: soffice --headless --convert-to docx <file>.doc
"""
import sys, zipfile
from lxml import etree
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
def text_mode(p, mode):
"""mode='final': 接受所有修订后(去del,保ins). mode='orig': 修订前(去ins,保del)."""
parts = []
for node in p.iter():
if node.tag == W + 't':
anc, skip = node, False
while anc is not None:
if mode == 'final' and anc.tag == W + 'del':
skip = True; break
if mode == 'orig' and anc.tag == W + 'ins':
skip = True; break
anc = anc.getparent()
if not skip:
parts.append(node.text or '')
elif node.tag == W + 'delText' and mode == 'orig':
parts.append(node.text or '')
return ''.join(parts).strip()
def parse_numbering(z):
"""返回 numId -> (numFmt, lvlText, start) 仅 lvl0(够用于条款标题层)."""
out = {}
if 'word/numbering.xml' not in z.namelist():
return out
num = etree.fromstring(z.read('word/numbering.xml'))
n2a = {}
for n in num.findall(W + 'num'):
ab = n.find(W + 'abstractNumId')
if ab is not None:
n2a[n.get(W + 'numId')] = ab.get(W + 'val')
a2fmt = {}
for ab in num.findall(W + 'abstractNum'):
l0 = ab.find(W + 'lvl')
if l0 is not None:
fmt = l0.find(W + 'numFmt')
txt = l0.find(W + 'lvlText')
st = l0.find(W + 'start')
a2fmt[ab.get(W + 'abstractNumId')] = (
fmt.get(W + 'val') if fmt is not None else '?',
txt.get(W + 'val') if txt is not None else '',
st.get(W + 'val') if st is not None else '1',
)
for nid, aid in n2a.items():
out[nid] = a2fmt.get(aid, ('?', '', '1'))
return out
def main(path):
z = zipfile.ZipFile(path)
root = etree.fromstring(z.read('word/document.xml'))
numinfo = parse_numbering(z)
print(f"### {path}\n")
print("=== numbering.xml: numId -> (numFmt, lvlText, start) ===")
if not numinfo:
print(" (无 numbering.xml — 全文应为手动文本编号)")
for nid, (fmt, txt, st) in sorted(numinfo.items()):
warn = ' ⚠️start≠1 会渲染潜伏编号!' if (fmt == 'decimal' and st != '1') else ''
print(f" numId={nid}: fmt={fmt}, lvlText='{txt}', start={st}{warn}")
print()
print("idx | numPr(自动) | rendered | ins/del | 文本(接受修订后)")
print("-" * 92)
for i, p in enumerate(root.findall('.//' + W + 'p')):
tf = text_mode(p, 'final')
if not tf:
continue
npr = p.find('.//' + W + 'numPr')
npinfo, rendered = '', ''
if npr is not None:
nid_el = npr.find(W + 'numId')
il_el = npr.find(W + 'ilvl')
nid = nid_el.get(W + 'val') if nid_el is not None else '?'
il = il_el.get(W + 'val') if il_el is not None else '0'
npinfo = f"numId={nid},lvl={il}"
fmt, txt, st = numinfo.get(nid, ('?', '', '1'))
if fmt == 'decimal':
rendered = (txt or '%1、').replace('%1', st) # 该项首个渲染值(近似)
elif fmt == 'bullet':
rendered = ''
elif fmt == 'none':
rendered = '(无)'
has_ins = p.find('.//' + W + 'ins') is not None
has_del = p.find('.//' + W + 'del') is not None
mk = ('INS' if has_ins else '') + ('/' if has_ins and has_del else '') + ('DEL' if has_del else '')
print(f"{i:3d} | {npinfo:18s} | {rendered:8s} | {mk:7s} | {tf[:46]}")
print()
print("判读要点:")
print(" - rendered 列非空 = OnlyOffice 会自动加这个编号(run里没有这串字)")
print(" - 手动编号: rendered='' 且文本以「N、」开头 = 编号是写死的文字")
print(" - 若末尾新增条款(INS)的手打编号 与 上方某段 rendered 自动编号 相同 → 撞号")
print(" 正确做法: 新增手打编号应接续【rendered 自动值】往下编, 不是接续最后一个手打数字")
if __name__ == '__main__':
if len(sys.argv) < 2:
print(__doc__); sys.exit(1)
main(sys.argv[1])