feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
## 批注注入(comments.xml 机制)
|
||||
|
||||
ContractEditor 库**不支持批注**。批注需要直接操作 docx 的 OOXML 批注三件套。验证可用(2026-06-17 培训合同实战)。
|
||||
|
||||
### 批注的三个组成部分
|
||||
1. **word/comments.xml**:批注内容本体(每条 `<w:comment>`,含 id/author/date/initials)。
|
||||
2. **word/document.xml**:在被批注的段落里插入锚点:
|
||||
- `<w:commentRangeStart w:id="X"/>` —— 放在段落第一个 run/ins 之前
|
||||
- `<w:commentRangeEnd w:id="X"/>` —— 放在段落末尾
|
||||
- 一个含 `<w:commentReference w:id="X"/>` 的 run —— 放在 rangeEnd 之后
|
||||
3. **关系声明**:
|
||||
- `[Content_Types].xml` 加 Override(comments.xml 的 ContentType)
|
||||
- `word/_rels/document.xml.rels` 加 Relationship(指向 comments.xml)
|
||||
|
||||
### 可复用代码(在 ContractEditor 修订并 save 之后,对成品 docx 注入批注)
|
||||
|
||||
```python
|
||||
import zipfile, io
|
||||
from lxml import etree
|
||||
|
||||
W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||
Wq = '{' + W + '}'
|
||||
|
||||
# 批注清单:anchor 是"接受修订后能精确匹配(唯一)"的段落片段
|
||||
comments = [
|
||||
{"id": "201", "anchor": "甲方扣除相应服务费后", "text": "建议……"},
|
||||
{"id": "202", "anchor": "向甲方住所地人民法院提起诉讼", "text": "建议……"},
|
||||
]
|
||||
DATE = "2026-06-17T10:00:00Z" # ISO格式
|
||||
|
||||
def build_comments_xml(comments):
|
||||
parts = ['<?xml version="1.0" encoding="UTF-8" standalone="yes"?>']
|
||||
parts.append(f'<w:comments xmlns:w="{W}" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">')
|
||||
for c in comments:
|
||||
parts.append(f'<w:comment w:id="{c["id"]}" w:author="WB" w:date="{DATE}" w:initials="WB">')
|
||||
# 批注文字字号建议比正文小(sz=18=9pt),字体跟随文档(宋体+hint=eastAsia)
|
||||
parts.append('<w:p><w:r><w:rPr><w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:eastAsia="宋体" w:cs="宋体" w:hint="eastAsia"/><w:sz w:val="18"/><w:szCs w:val="18"/></w:rPr>')
|
||||
parts.append(f'<w:t xml:space="preserve">{c["text"]}</w:t></w:r></w:p>')
|
||||
parts.append('</w:comment>')
|
||||
parts.append('</w:comments>')
|
||||
return ''.join(parts)
|
||||
|
||||
def inject_comments(in_path, out_path, comments):
|
||||
comments_xml = build_comments_xml(comments)
|
||||
with open(in_path, 'rb') as f:
|
||||
data = f.read()
|
||||
buf_in, buf_out = io.BytesIO(data), io.BytesIO()
|
||||
inserted = {c["id"]: False for c in comments}
|
||||
with zipfile.ZipFile(buf_in, 'r') as zin, zipfile.ZipFile(buf_out, 'w', zipfile.ZIP_DEFLATED) as zout:
|
||||
for item in zin.infolist():
|
||||
raw = zin.read(item.filename)
|
||||
if item.filename == 'word/document.xml':
|
||||
tree = etree.fromstring(raw)
|
||||
body = tree.find(f'{Wq}body')
|
||||
for para in body.findall(f'.//{Wq}p'):
|
||||
# 接受修订后文本(跳过 w:del 内的 t)做锚点匹配
|
||||
ptext = ''
|
||||
for t in para.findall(f'.//{Wq}t'):
|
||||
if not any(a.tag == f'{Wq}del' for a in t.iterancestors()):
|
||||
ptext += (t.text or '')
|
||||
for c in comments:
|
||||
if not inserted[c["id"]] and c["anchor"] in ptext:
|
||||
cid = c["id"]
|
||||
# 第一个挂靠点:段落第一个 r 或 ins
|
||||
first_child = None
|
||||
for child in para:
|
||||
if child.tag in (f'{Wq}r', f'{Wq}ins'):
|
||||
first_child = child; break
|
||||
if first_child is None:
|
||||
continue
|
||||
crs = etree.Element(f'{Wq}commentRangeStart'); crs.set(f'{Wq}id', cid)
|
||||
first_child.addprevious(crs)
|
||||
cre = etree.Element(f'{Wq}commentRangeEnd'); cre.set(f'{Wq}id', cid)
|
||||
para.append(cre)
|
||||
rr = etree.SubElement(para, f'{Wq}r')
|
||||
rrp = etree.SubElement(rr, f'{Wq}rPr')
|
||||
rs = etree.SubElement(rrp, f'{Wq}rStyle'); rs.set(f'{Wq}val', 'CommentReference')
|
||||
cref = etree.SubElement(rr, f'{Wq}commentReference'); cref.set(f'{Wq}id', cid)
|
||||
inserted[cid] = True
|
||||
raw = etree.tostring(tree, xml_declaration=True, encoding='UTF-8', standalone=True)
|
||||
elif item.filename == '[Content_Types].xml':
|
||||
ct = etree.fromstring(raw)
|
||||
NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
|
||||
ov = etree.SubElement(ct, f'{{{NS}}}Override')
|
||||
ov.set('PartName', '/word/comments.xml')
|
||||
ov.set('ContentType', 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml')
|
||||
raw = etree.tostring(ct, xml_declaration=True, encoding='UTF-8', standalone=True)
|
||||
elif item.filename == 'word/_rels/document.xml.rels':
|
||||
rt = etree.fromstring(raw)
|
||||
RNS = 'http://schemas.openxmlformats.org/package/2006/relationships'
|
||||
r = etree.SubElement(rt, f'{{{RNS}}}Relationship')
|
||||
r.set('Id', 'rIdComments1')
|
||||
r.set('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments')
|
||||
r.set('Target', 'comments.xml')
|
||||
raw = etree.tostring(rt, xml_declaration=True, encoding='UTF-8', standalone=True)
|
||||
zout.writestr(item, raw)
|
||||
zout.writestr('word/comments.xml', comments_xml.encode('utf-8'))
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(buf_out.getvalue())
|
||||
return inserted # 检查是否全部 True
|
||||
```
|
||||
|
||||
### 锚点选择要点
|
||||
- anchor 必须是**接受修订后文本里唯一**的片段(先用脚本验证 hits==1,多处命中会挂错段)。
|
||||
- 避免选被修订(w:del/w:ins)切割的文字做anchor——优先选未被改动的稳定片段。
|
||||
- 若 anchor 落在被修订段,匹配用"接受修订后文本"(跳过 w:del),与上面代码一致。
|
||||
|
||||
### 验证(终审必做)
|
||||
```python
|
||||
# commentRangeStart/End/Reference 与 comments.xml 的 id 必须全部对应
|
||||
crs = set(e.get(Wq+'id') for e in doc_root.iter(Wq+'commentRangeStart'))
|
||||
cre = set(e.get(Wq+'id') for e in doc_root.iter(Wq+'commentRangeEnd'))
|
||||
cref = set(e.get(Wq+'id') for e in doc_root.iter(Wq+'commentReference'))
|
||||
com = set(c.get(Wq+'id') for c in com_root.iter(Wq+'comment'))
|
||||
assert crs == cre == cref == com
|
||||
```
|
||||
末了用 python-docx `Document(out)` 能打开(XML合法)+ OnlyOffice 渲染确认批注气泡显示。
|
||||
@@ -0,0 +1,86 @@
|
||||
# 维修催告/通知函起草规范
|
||||
|
||||
## 适用场景
|
||||
南通新东方作为承租方(乙方),需要催促出租方/物业管理公司履行维修义务时。
|
||||
|
||||
## 起草前置步骤
|
||||
|
||||
### 1. 盘点合同关系
|
||||
- 确认该校区有几份合同(租赁+物业,通常成对出现)
|
||||
- 🔴 **配对铁律**:每份租赁合同找对应物业合同(世茂教训:2份租赁→2份物业,漏了1份被纠正)
|
||||
- 合同编号对照表示例:
|
||||
- 青少:租赁 217L0363a-1 ↔ 物业 217L0363b
|
||||
- 高中:租赁 217L0457a ↔ 物业 217L0457b
|
||||
|
||||
### 2. 条款查找
|
||||
- **租赁合同**:找"商铺修缮"/"维修"条款(世茂=第八条)
|
||||
- **物业合同**:找"商铺修缮"条款(世茂=第五条)+ "甲方义务"条款(世茂=第七条)
|
||||
- 对比多份合同条款是否实质一致,一致可合并引用,有差异需分别列明
|
||||
|
||||
### 3. 确认维修义务链
|
||||
- 出租方:结构/屋顶/主体维修义务(租赁合同)
|
||||
- 管理方:公共部位/设施维修义务(物业合同)
|
||||
- 法定义务:民法典第713条(承租人代修权)
|
||||
|
||||
## 通知函格式规范
|
||||
|
||||
### 标题
|
||||
```
|
||||
关于[具体事项]要求维修的通知函
|
||||
```
|
||||
|
||||
### 文号
|
||||
```
|
||||
新东方南通函〔年份〕第 号
|
||||
```
|
||||
|
||||
### 致送对象
|
||||
```
|
||||
致:[出租方全称]
|
||||
([管理方全称]) ← 如同时致送管理方
|
||||
```
|
||||
- 不用"尊敬的XX:"格式
|
||||
|
||||
### 正文结构
|
||||
1. **合同背景段**:列明所有相关合同(含编号、标的、面积),表述"合同均在履行期内"
|
||||
2. **问题描述段**:客观描述问题、影响、已有投诉
|
||||
3. **合同依据段**:引用维修条款(简洁总结+条款号,不全文抄录)
|
||||
- 格式:`(一)租赁合同(编号)第X条第Y款约定:[一句话总结]。`
|
||||
- 格式:`(二)物业管理服务合同(编号)第X条约定:[一句话总结]。`
|
||||
4. **定性段**:明确非承租方原因 + 属对方合同义务
|
||||
5. **请求段**:分项列明具体要求(回复时限、完成时限、损害赔偿)
|
||||
6. **后果段**:逾期未修的法律后果(代修权 + 费用追偿 + 民法典713条)
|
||||
7. **结尾段**:请予重视
|
||||
|
||||
### 落款
|
||||
```
|
||||
此致
|
||||
|
||||
[发函方全称]
|
||||
[日期]
|
||||
|
||||
附:[渗漏现场照片/其他证据](另附)
|
||||
```
|
||||
|
||||
## 条款引用风格
|
||||
|
||||
### ❌ 不推荐(全文引用,冗长)
|
||||
> 依据合同第八条第1款之约定:"如非乙方原因,该商铺及甲方或管理公司提供的设施出现妨碍安全、正常使用的损坏时,乙方应及时通知甲方或管理公司,并采取有效措施防止损失扩大。甲方或管理公司方应在接到乙方通知后,尽快安排维修工作。"
|
||||
|
||||
### ✅ 推荐(简洁总结+条款号)
|
||||
> (一)租赁合同(217L0363a-1、217L0457a)第八条第1款约定:非因乙方原因致商铺及设施损坏的,乙方应及时通知甲方或管理公司并防止损失扩大,甲方或管理公司应尽快安排维修;逾期未修的,乙方可自行维修,费用由甲方承担。
|
||||
|
||||
**原则**:Maggie要求"简洁起见不需要全文引用,进行内容总结标注条款号即可"。
|
||||
|
||||
## 修订版docx制作技术要点
|
||||
- 用修订模式(author=WB, date=当天)标记所有改动
|
||||
- 原文删除用 `<w:del>`,新增用 `<w:ins>`
|
||||
- 字体统一仿宋_GB2312,字号32(小二号)
|
||||
- 标题可用方正小标宋简体,字号44,加粗
|
||||
- 首行缩进 firstLine=482(两字符)
|
||||
- 居右对齐落款区
|
||||
|
||||
## 法律依据速查
|
||||
- 民法典第713条:承租人代修权(出租人不履行维修义务→承租人可自行维修,费用由出租人负担)
|
||||
- 民法典第714条:承租人妥善保管义务
|
||||
- 民法典第710条:正常使用导致的自然损耗,承租人不承担赔偿责任
|
||||
Reference in New Issue
Block a user