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,72 @@
# 双语合同完善 / 模板残留清理(中英对照法律服务协议等)
## 适用场景
莎莎 / Maggie / 客户直接交办一份"在现有草稿基础上完善"的中英对照合同(法律服务协议、委托代理协议等),不走 Doro 的 uwf workflow。草稿往往是从所内旧模板 / 旧案改来的,残留前案痕迹。
## 铁律1:模板残留是常态,必须逐条中英对照
律所复用双语模板时,**中文侧和英文侧可能各自残留不同旧案的内容**,且核心商业条款中英文互相矛盾。
普力克 Procore 案(2026-06-25)实例:
- 中文侧整体是"社保调查"案,英文侧整体是"喆航买卖合同纠纷"案——**两个不同旧案混在一份草稿里**
- 客户名残留旧客户(艾达 / AMOS),分布在抬头、正文、付款条款第三方代付人、签署页多处
- 费率:中文 2,500 元/小时·2-3 小时一次性;英文 2,000 元/小时·每月 5-6 小时——三项数据互相打架
- 付款:中文付全额;英文付 50% 即 72,500 元 + AMOS 新加坡公司代付
**做法**:python-docx 提全文(含 `doc.tables`),把每个条款的中文段和紧邻的英文段配对,逐条比对。重点盯:服务内容、服务范围、主要联系人、费率、工时、付款方式/金额、第三方付款人、签署日期。
## 铁律2:商业条款绝不替客户猜,先确认后动手
费率、金额、工时、付款分期、第三方代付——这些是商业条款。草稿里中英文打架时**不能自己挑一个填进去**。把所有矛盾点列成清单,让委托律师给准数,确认后再出稿。
本案先问了 7 点(真实业务事由 / 非诉 or 诉讼 / 联系人 / 费率 / 工时 / 收款方式 / 签署日),拿到 3 点核心确认(服务内容=王晓玲社保调查、费率 2,500×2-3 小时一次性全额、签署日 6/25)才动手。
## 铁律3:以中文为准 → 先定中文,再对齐英文
协议若含"如有出入以中文为准"条款(多数中英对照协议都有),完善顺序必须是:**先把中文条款定稿,再逐条把英文改成与中文一致**。用户本案也明确指示此顺序。这样既符合协议约定,也符合用户预期。该兜底条款审完保留——对己方有利,不要删。
## 技术:段落级 run 合并替换(保格式整段替换)
python-docx 里一个段落常被切成几十个碎 run(中文逐字符切、英文逐词切,bold/size 各异)。整段替换文本又要保留段落样式,用这个函数:
```python
def set_para_text(p, text):
"""整段文本写入第一个 run,清空其余 run,保留首 run 格式。"""
if not p.runs:
p.add_run(text); return
p.runs[0].text = text
for r in p.runs[1:]:
r.text = ""
```
- 表格单元格同理:`set_para_text(cell.paragraphs[idx], text)`
- 删除整个空段落(如清掉被替空的旧英文残段,避免留空行):`el = p._element; el.getparent().remove(el)`
- 替换后务必复查:遍历全文(段落 + 表格 cell)拼成大字符串,断言旧内容(艾达/AMOS/旧金额/旧费率等)`not in full`,再断言新数据出现次数正确。
## 验收:多页 OnlyOffice 渲染 → 逐页 vision 核对
单页固定版面用 `print-ready-pdf/scripts/oo_render_check.sh`。多页双语合同用以下管线(Maggie/Doro/莎莎都用 OnlyOffice 看文件,必须用 x2t 不用 LibreOffice):
```bash
docker cp file.docx nextcloud-onlyoffice-1:/tmp/_chk.docx
# 容器内写 TaskQueueDataConvert xml,m_nFormatTo=513,跑 x2t(见 print-ready-pdf)
docker cp nextcloud-onlyoffice-1:/tmp/_chk.pdf /tmp/out.pdf
pdftoppm -png -r 110 /tmp/out.pdf /tmp/pg # 每页一张 PNG
```
然后 `vision_analyze` 逐页核对:客户名、服务内容、费率、付款方式金额、签署日期、签署页主体、中英一致性。
## 铁律4:vision 报"截断/截字/多空格"先回源核实,别急着改
justified(两端对齐)渲染图里,vision 模型会把以下**正常排版现象**误报为"文字截断/排版错误"——本案一次任务里就误报了 4 次:
- 跨页连字符断词(第1页底 `accor-` → 第2页头 `ding`
- 跨页句子接续(第4页底 `Negotiations shall` → 第5页头 `commence...`
- 两端对齐的字距假象(`with` 被读成 `wit h`
- 两端对齐的自动换行断词(`aforemen` / `tioned`,且无连字符)
判断方法(任一即可证伪 vision 的"截断"):
1. 从源 docx 读该段 `p.text`,确认是完整单词 / 完整句子;
2.`word/settings.xml` 是否含 `autoHyphenation`(没开就不会真断字);
3. 查文本不含软连字符 `\u00ad`
**渲染层表象 ≠ 文档缺陷**,Word / OnlyOffice 编辑视图会正常回流。
## 铁律5:关键数据(账号/金额/信用代码)逐字符回源核对,不信 vision 读数
vision 把收款账号 `121955296610001`(15 位)读成 `12195296610001`(漏 1 位)。账号、金额、统一社会信用代码这类关键数字,**必须从源 docx 逐字符提取核对**,绝不采信 vision 对渲染图的读数。本案账号与原稿逐位一致,确认后未改。
## 顺手订正范围("条款明确得当"要求)
完善双语合同时,除主体/商业条款外,一并修掉:
- 中文病句("在上述法律服务作为客户的代理人" → "就上述法律服务事项作为客户的代理人")、错别字("疑异"→"疑义")、异体字("⻓"→"长")
- 英文逗号粘连句(comma splice,逗号改分号或加 and)、口语化用词("really" → "truthfully",与同条 "faithfully" 体系一致)
## 交付
- 命名规则:当事人名称+文件名称+版本+修改人+日期(本案:`普力克贸易(上海)有限公司-法律服务协议-v1-20260625.docx`
- 本类任务默认交"用印前清稿"(直接定稿、非修订模式)。若需**带修订痕迹**版本供对方/同事看改了哪些,另出一版 tracked changes(用 ContractEditor 库,不裸写 XML)。交付时主动告知用户这两种版本的区别,问要哪种。
@@ -0,0 +1,193 @@
# Contract Portfolio Audit (合同梳理/合规审查)
Different from individual contract review (docx track changes). This is batch reading of an existing contract collection to produce a compliance overview for the client.
## When to use
- Client wants to understand the status and risks of their existing contract portfolio
- Goal is compliance oversight, not redlining individual contracts
- Deliverables: annotated PDFs + Excel summary table
## Workflow
### 1. File inventory
- Map the full directory structure and count files
- Group by logical categories (the client's folder structure usually reflects this)
- Record in MemPalace as a long-term project if ongoing
### 2. OCR scanned PDFs
- Most contracts from clients are scanned PDFs (no text layer)
- Check with `page.get_text().strip()` — empty = scanned
- Use `deepseek-ocr` skill (page-by-page image fallback if API unstable)
- See `deepseek-ocr` skill for retry strategy
- For large batches: convert all PDFs to images first, then OCR in a single loop with `time.sleep(1-2)` between pages
### 3. Analyze and annotate PDFs
- Add sticky note annotations via pymupdf (see `ocr-and-documents` skill)
- Author field: use `annot.set_info(title="WB")` + `annot.update()`
- Annotation content: risk points, missing clauses, suggestions — no risk level tags (【高风险】etc.)
- Suggestions must be specific and actionable ("建议增加……""建议修改为……"), not vague descriptions
### 4. Excel summary table
- Use openpyxl with proper styling (微软雅黑, header fill, borders, wrap_text, freeze panes)
#### File naming for deliverables (differs from standard versioned naming):
```
批注PDF: 客户名称-文件名称-批注-YYYYMMDD.pdf
汇总表: 客户名称-XX合同汇总表-YYYYMMDD.xlsx
```
Example:
```
南通新东方-BOSS直聘服务合同-批注-20260609.pdf
南通新东方-人力资源合同汇总表-20260609.xlsx
南通新东方-租赁合同汇总表-20260609.xlsx
```
#### Standard fields (adjustable per project):
| Field | Notes |
|-------|-------|
| 序号 | Sequential |
| 合同名称 | |
| 合同编号 | "无" if not present |
| 合同类型 | 采购服务/采购货物/租赁/etc. |
| 甲方(我方主体) | May have multiple entities |
| 乙方(对方主体) | |
| 合同期限 | Start-end dates |
| 合同金额 | With currency |
| 服务内容 | Brief description |
| 合同状态 | 履行中/即将到期/已到期 |
| 主要风险点 | Numbered list |
| 建议 | Numbered list, actionable |
### 5. Grouping logic (confirmed with Maggie for 南通新东方)
- **房租物业合同**: Group by 校区/租赁物 — rental + property management together in one sheet per campus
- **非模板其他合同**: Group by 合同类型 (采购货物/采购服务/租赁/etc.)
- **人力资源**: Flat list
### 6. Excel structure for rental contract portfolios
**Multi-sheet workbook**: one Excel file for all campuses.
| Sheet | Content |
|-------|---------|
| **总览** | One row per campus: campus name, tenant entity, landlord, current area, term, quarterly rent, quarterly property fee, deposit total, key risks, notes |
| **校区A** | All contracts for that campus in detail |
| **校区B** | ... |
| ... | ... |
**Per-campus sheet structure** (sections with merged header rows):
```
一、原租赁合同系列(乙方:XXX)
[header row]
主合同 → 补充协议 → 退租 → 三方转让 → ...
二、扩租合同系列(乙方:YYY) [if applicable]
[header row]
主合同 → 补充协议 → ...
三、物业管理合同系列
[header row]
原租物业 → 补充协议 → 退租 → 扩租物业 → ...
四、校区整体风险分析与建议
[merged A:K cell — 【整体风险分析】+ numbered risks + 【建议】]
[merged A:K cell — 【文件汇总说明】+ source file ↔ table entry mapping]
```
**Section 四 has TWO merged cells** (both A:K, wrap_text=True, vertical=top):
1. **风险分析**: overall risks + recommendations (existing)
2. **文件汇总说明**: client-facing reconciliation — lists every source file by original directory, maps each to its table entry number, and explains any discrepancies (duplicates removed, entries combined, files reclassified to different sections). This lets the client cross-reference their original files against the table without guessing.
**Per-campus detail columns**:
序号, 文件名称, 合同类型, 合同当事人, 租赁标的/服务范围, 面积(㎡), 合同期限, 金额/费用, 核心内容, 当前状态, 风险点/备注
**Key points**:
- Each section has its own header row (repeated column headers after each section divider)
- Section dividers are merged cells with blue background
- Within each section, documents are listed in logical/chronological order (main contract → supplements → amendments)
- The risk analysis section at the bottom is a single large merged cell covering all columns
- Use freeze_panes on the title area so data scrolls while title stays
### 7. File count reconciliation (MANDATORY before upload)
After completing all sheets, reconcile source files against table entries:
1. **Count source files**: list all PDFs in the source directory tree
2. **Subtract known duplicates**: identical content files (same size + same date often = duplicate scan)
3. **Count table entries**: sum all numbered rows across the sheet
4. **Reconcile**: source_files - duplicates MUST equal table_entries. If not, identify missing files.
**Common miss patterns**:
- Two files with similar names/content silently merged into one row (e.g., `扩租物业补充协议.pdf` and `扩租物业补充协议(电费教育科技)1.pdf` — same theme but different signing parties, should be separate entries or explicitly noted as combined)
- A file in one subdirectory overlooked because a "similar" file in another subdirectory was already covered (e.g., `物业补充协议(电费培训学校).pdf` missed because `物业补充xiey.pdf` seemed to cover the same topic)
- Files across subdirectories not cross-checked against the master list
**Rule**: Every unique file gets its own row unless explicitly combined (in which case the combined row must list all file names and the备注 must explain why they're grouped). One-to-one is the default; combining requires justification.
**Client-facing summary**: After reconciliation, add a 【文件汇总说明】section to the Excel (inside section 四, as a separate merged A:K cell below the risk analysis). Format:
```
【文件汇总说明】
客户提供XX校区相关合同文件共N份,分布在M个文件夹中。经核对整理,去重后为X份独立文件,汇总表中归纳为Y条记录。具体对照如下:
一、文件夹A/(N份)
1. 文件名.pdf → 表第X项
2. 文件名.pdf → 表第Y项
...
二、文件夹B/(N份,含M份重复)
...
说明:
- 重复文件:XXX同时出现在A/和B/目录中,内容一致,仅计1份
- 合并记录:XXX两份内容一致,合并为表第N项
- 分类调整:A/目录下的XXX实为YYY,已归入ZZZ系列
```
This is NOT optional — every campus/category sheet must include this summary so the client can verify completeness without asking.
### 8. Upload to Nextcloud
- Place annotated PDFs alongside originals in same directory
- Place Excel in the parent category folder (e.g., 房租物业合同/ for the rental summary)
- `docker cp``chown www-data``php occ files:scan`
### 9. Sample-first validation
**Always start with a small sample** (2-3 contracts of different types):
1. Pick contracts that represent different complexity levels
2. Do full OCR → analyze → annotate → fill Excel
3. Send to Maggie for review of format and content
4. Adjust table structure based on feedback
5. Then batch process remaining contracts
This avoids rework — the table structure almost always needs adjustment after the first review.
## Key differences from individual contract review
| Aspect | Individual review | Portfolio audit |
|--------|------------------|-----------------|
| Input | Usually docx | Usually scanned PDF |
| Output | Track changes docx | Annotated PDF + Excel summary |
| Depth | Clause-by-clause redline | Risk overview and key terms extraction |
| Audience | Lawyer (for negotiation) | Client management (for compliance) |
| Volume | 1 contract at a time | Batch (tens to hundreds) |
| Workflow | uwf reviewer→editor | OCR → analyze → annotate → summarize |
| File naming | 当事人+文件+版本+修改人+日期 | 客户名+文件名+批注/汇总表+日期 |
## Related references
- `references/template-comparison-methodology.md` — when client has a standard template, how to produce the "与标准模版差异" column
## Pitfalls
- **Section 四 formatting**: The risk analysis section must use properly merged A:K cells. Common mistakes: (1) row heights set wrong (section headers should be ~30, not 80/350); (2) content cell not merged across all columns so text only shows in column A; (3) empty separator rows getting large fixed heights instead of auto. After building section 四, verify: `ws.merged_cells.ranges` includes both A:K rows, row heights are sane (None or ~30 for headers), and wrap_text=True on content cells.
- **CRITICAL: OnlyOffice does NOT reliably auto-size row heights — for merged cells OR regular cells with wrap_text.** Setting `height=None` (auto) on ANY cell with multi-line content means text WILL be cut off. This is the #1 user complaint ("文字显示不出来"). **Always set explicit heights on EVERY row with multi-line content**:
1. After populating all cells, run a height estimation pass over ALL data rows
2. For each row, find the cell with the most visual lines (accounting for CJK chars at ~2 width units each, line wrapping at `col_width * 1.2` chars)
3. Set height = max_visual_lines × 15pt + 30-50% buffer
4. For merged cells spanning A:K (~231 width), one text line ≈ one visual line (no wrapping)
5. For narrow columns (H=20, K/L=40), long CJK lines wrap heavily — a 60-char Chinese line in a width-20 column = ~4 visual lines
6. Write a reusable height-check script rather than eyeballing — the number of rows that need fixing always exceeds expectations
- **File count reconciliation**: after completing a campus/category sheet, always count source files and compare against table rows BEFORE uploading. The user should never be the one to discover a missing file. See step 7 above.
- Start with a small sample (2-3 contracts) to validate table headers with Maggie before batch processing
- Chinese text in Python strings with nested quotes (especially inside f-strings or dicts): use single quotes inside double or vice versa, or put long strings in variables
- openpyxl SyntaxError: Chinese punctuation like 、()【】 inside nested Python string literals can confuse the parser — write the script to a .py file first, then run it, rather than using inline heredocs
- For complex campuses (like 北翼玖玖 with 17 files across 3 subdirs): use delegate_task to read all OCR'd files in parallel, then synthesize the analysis yourself
- When building a campus sheet, always trace the full document chain (main contract → what amended it → what superseded it) to understand the current state
- Watch for different legal entities signing different contracts at the same location (e.g., 培训学校 vs 教育科技) — flag this as a management complexity risk
- Watch for landlord entity changes mid-lease (common in commercial real estate) — trace the transfer chain and note deposit movements
@@ -0,0 +1,101 @@
# 合同接收分流:邱律师批量文件交叉核对
当邱律师(QiuTing)一次性发送多份合同时,必须先交叉核对分流,再决定是否启动workflow。
## 分流四类
| 类别 | 判定方法 | 处理 |
|------|---------|------|
| ✅ 今日已交付 | xlsx有今天日期的对应条目 | 跳过 |
| 🔄 正在审查 | `uwf thread list`显示running | 等完成,不重复启动 |
| 📋 历史已审、今日重发 | xlsx有该文件但日期更早 | **报告Doro**:可能是修改稿需重审,也可能是误发 |
| ❌ 全新未处理 | xlsx无匹配条目 | 启动workflow |
## 核对步骤
### Step 1: 提取今日QiuTing文件清单
```bash
for f in ~/.hermes/cache/documents/*.meta; do
sender=$(python3 -c "import json; d=json.load(open('$f')); print(d.get('sender_id',''))" 2>/dev/null)
if [ "$sender" = "QiuTing" ]; then
ts=$(python3 -c "import json; d=json.load(open('$f')); print(int(d.get('timestamp',0)))" 2>/dev/null)
# 比对今日时间戳范围
filename=$(basename "$f" .meta | sed 's/^doc_[a-f0-9]*_//')
echo "$filename"
fi
done
```
按北京时间筛选当天00:00~24:00范围内的文件。
### Step 2: 查xlsx台账匹配
```bash
sudo /home/maggie/.hermes/hermes-agent/venv/bin/python3 << 'PYEOF'
import openpyxl
xlsx = "/home/maggie/nextcloud/data/data/doro/files/Doro合同审查任务/合同审查清单.xlsx"
wb = openpyxl.load_workbook(xlsx, read_only=True, data_only=True)
ws = wb.active
# 用关键词模糊匹配文件名(去掉前缀/后缀后取核心词)
keywords = ["关键词1", "关键词2"] # 从文件名提取
for row in ws.iter_rows(min_row=2, values_only=True):
row_str = str(row)
for kw in keywords:
if kw in row_str:
print(f"#{row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]}")
break
wb.close()
PYEOF
```
**匹配要点**:文件名常有微小差异(空格、下划线、版本号),用核心关键词模糊匹配,不要精确匹配全名。
### Step 3: 查uwf运行状态
```bash
/home/maggie/.hermes/node/bin/uwf thread list | grep running
```
### Step 4: 查/tmp/contract-review/和progress目录
确认是否有文件正在队列中等待。
### Step 5: 汇总报告Doro
按四类分类呈现,对「历史已审重发」类必须询问Doro:是修改稿需重审还是误发。
## 合同发送量核查协议(Doro问"发了几份/审了几份"时)
⚠️ **Doro问邱律师发了几份合同时,必须多源交叉核对,不能只查一个来源。** 2026-06-29/30教训:Doro问了三遍"邱律师发了几份",每次agent都漏东西。根因:只查了gateway.log的inbound消息,漏了document cache的meta文件(文件消息不产生inbound text log)。
### 必查5个源(缺一不可)
| # | 数据源 | 命令 | 提取信息 |
|---|--------|------|----------|
| 1 | gateway.log inbound消息 | `grep "2026-MM-DD" gateway.log \| grep "QiuTing" \| grep "inbound"` | 文字消息+指令(合同审查等) |
| 2 | document cache meta文件 | `for f in ~/.hermes/cache/documents/*.meta; do` 读sender_id+timestamp | 实际收到的文件(⚠️文件消息在gateway.log中msg为空) |
| 3 | 待审查目录 | `sudo find .../待审查/ -type f` | 已上传但未审完的合同 |
| 4 | 任务交付目录 | `sudo find .../任务交付/ -type f -name "【修】*"` | 已审查交付的合同 |
| 5 | xlsx台账 | `sudo python3 -c "openpyxl..."` | 已pass登记的合同 |
### meta文件时间戳转换
```python
from datetime import datetime, timezone, timedelta
bj = timezone(timedelta(hours=8))
# meta的timestamp是Unix秒,转北京时间
dt = datetime.fromtimestamp(ts, tz=bj)
print(f'{dt.strftime("%m-%d %H:%M")} Beijing - {filename}')
```
### 汇总格式(给Doro的报告)
按时间顺序列出每份合同:
- 文件名 | 发送时间(北京时间) | 审查状态(已审/待审/未审) | 交付文件(有/无)
- 去重:同文件名的重发标记"重发,与XX:XX相同"
- 最终统计:N份独立合同,X份已审,Y份未审
### 关键陷阱
1. **gateway.log里文件消息的msg字段为空** — QiuTing发送文件时,gateway记录`msg=''`,看不到文件名。必须查meta文件才能知道发了什么文件
2. **服务器时区是UTC,meta时间戳是Unix秒** — 必须转UTC+8才是北京时间,不能直接显示
3. **待审查目录可能有未清理的旧文件** — 不能只看待审查目录判断今天收了几份,要按meta时间戳筛选
## 关键陷阱
1. **重发≠重审**:邱律师有时重新发送已审查过的文件(今天5/12份是这种情况),不代表需要重新审查。必须报告Doro确认。
2. **auto_notify可能漏文件**`auto_notify_new_file.sh` 监控inotify有时不触发(进程挂掉、文件到达太快等),不能假设自动化已处理所有新文件。手动核对是必要的。
3. **xlsx需要sudo+venv Python**:文件权限属于www-data,必须用 `sudo /home/maggie/.hermes/hermes-agent/venv/bin/python3` 读取。
4. **同名不同版**:文件名相同但日期不同的可能是新版本(如 `购销合同 朱家角` 在6/27和6/29各有一份),需比较内容确认是否修改稿。
@@ -0,0 +1,298 @@
# Multi-Party Revision Reconciliation (多方修订对账)
## 场景
合同经多方修订(如WB、华诚-Z、Adon hase等不同修订人),需要:
1. 核对叠加修订后的最终效果是否符合协商一致的商业条件
2. 对比最终版与模板的差异
3. 评估差异对特定方权利义务的影响
4. 统一修订人署名
## 技术方法
### 1. 提取带修订标记的全文(含作者归属)
```python
from docx import Document
from lxml import etree
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
doc = Document('contract.docx')
body = doc.element.body
# 统计修订作者
authors = set()
for elem in body.iter():
author = elem.get(f'{W}author')
if author:
authors.add(author)
# 分作者统计插入/删除
ins_by_author = {}
del_by_author = {}
for ins in body.findall(f'.//{W}ins'):
a = ins.get(f'{W}author', 'unknown')
ins_by_author[a] = ins_by_author.get(a, 0) + 1
for d in body.findall(f'.//{W}del'):
a = d.get(f'{W}author', 'unknown')
del_by_author[a] = del_by_author.get(a, 0) + 1
```
### 2. 带修订标记的文本提取
格式:`[+作者: 插入文本]` / `[-作者: 删除文本]` / 普通文本
```python
def get_text_with_revisions(body):
result = []
for para in body.findall(f'.//{W}p'):
para_text = []
for elem in para.iter():
if elem.tag == f'{W}ins':
author = elem.get(f'{W}author', '?')
texts = [t.text for t in elem.findall(f'.//{W}t') if t.text]
if texts:
para_text.append(f"[+{author}: {''.join(texts)}]")
elif elem.tag == f'{W}del':
author = elem.get(f'{W}author', '?')
texts = [t.text for t in elem.findall(f'.//{W}delText') if t.text]
if texts:
para_text.append(f"[-{author}: {''.join(texts)}]")
elif elem.tag == f'{W}t':
in_revision = False
p = elem
while p is not None:
if p.tag in [f'{W}ins', f'{W}del']:
in_revision = True
break
p = p.getparent()
if not in_revision and elem.text:
para_text.append(elem.text)
if para_text:
result.append(''.join(para_text))
return result
```
### 3. 三版对比分析
对比维度:
- **模板** → 我方标准条款(基准线)
- **对方修订版** → 对方修改后接受所有修订的版本(ins=0, del=0 说明已全部接受)
- **当前叠加版** → 在WB修订基础上加入另一方修改
### 4. 协商一致核对表
按商业条件逐项核对:
| 协商条件 | 条款位置 | 当前版本内容 | 是否符合 |
|---|---|---|---|
| 费用40/60/60/80/80 | 3.3条 | 具体金额 | ✅/❌ |
| 违约责任按我方 | 第4条 | ... | ✅/⚠️ |
### 5. 模板偏差影响评估
对每处偏差评级:
- ✅ 对己方有利(扩大了己方权利/对方义务)
- ⚠️ 中等影响(条件调整但不改变核心权利义务)
- ❗ 重要(实质性削弱己方权利/扩大己方义务/给对方逃出合同的通道)
## 修订人统一规则
当需要将多个修订人统一为一个(如统一为WB):
### 处理优先级
1. A修订了B的修订 → **以A为准**(接受A对B的修改)
2. A和B各自独立修订 → 两者都保留,统一署名
### 核心技术挑战:嵌套修订
**关键结构:B(华诚-Z)在A(WB)的 `w:ins` 内部做了 `w:del`**
XML表现为:
```xml
<w:ins w:author="WB">
<w:r><w:t>个月未给乙方安排工作的,</w:t></w:r>
<w:del w:author="华诚-Z">
<w:r><w:delText>拍摄</w:delText></w:r>
</w:del>
</w:ins>
```
含义:WB插入了"个月未给乙方安排拍摄工作的,",华诚-Z在WB的插入中删除了"拍摄",最终效果="个月未给乙方安排工作的,"。
### 三步统一流程(2026-07-03 MCN模特合同实证)
**Step 1: 接受嵌套删除**(B对A修订的修改)
```python
def accept_nested_deletions(body, inner_author='华诚-Z', outer_author='WB'):
"""接受inner_author对outer_author修订的修改(删除嵌套del元素)"""
for ins_elem in body.findall(f'.//{W}ins'):
if ins_elem.get(f'{W}author') != outer_author:
continue
# 找到outer_author的ins内部,inner_author做的del
for del_elem in ins_elem.findall(f'.//{W}del'):
if del_elem.get(f'{W}author') == inner_author:
parent = del_elem.getparent()
parent.remove(del_elem)
accept_nested_deletions(body)
```
**Step 2: 清除空壳元素**
接受嵌套删除后,某些WB的ins可能变空(内容全被华诚-Z删了,华诚-Z在旁边插入了替代文本):
```python
def remove_empty_ins(body):
"""删除没有任何文本内容的ins元素"""
for ins_elem in body.findall(f'.//{W}ins'):
has_text = False
for t in ins_elem.findall(f'.//{W}t'):
if t.text and t.text.strip():
has_text = True
break
if not has_text:
parent = ins_elem.getparent()
if parent is not None:
parent.remove(ins_elem)
remove_empty_ins(body)
```
**Step 3: 统一作者名**
```python
def rename_author(body, old_author, new_author):
"""修改所有修订元素的author属性"""
count = 0
for elem in body.iter():
if elem.get(f'{W}author') == old_author:
elem.set(f'{W}author', new_author)
count += 1
return count
count = rename_author(body, '华诚-Z', 'WB')
```
### 在统一后的文件上追加模板修改
如果发现与模板有偏差需要修正(如恢复模板中的固定金额违约金选项、自动续约条件等),在已统一的文件上新增tracked changes:
```python
from copy import deepcopy
def get_rPr_from_run(run):
"""获取run的格式属性用于新增修订"""
rpr = run.find(f'{W}rPr')
return deepcopy(rpr) if rpr is not None else None
def make_ins(text, rPr=None, author='WB', date='2026-07-03T06:00:00Z'):
"""创建tracked insertion"""
ins = etree.Element(f'{W}ins')
ins.set(f'{W}id', str(abs(hash(text)) % 100000))
ins.set(f'{W}author', author)
ins.set(f'{W}date', date)
r = etree.SubElement(ins, f'{W}r')
if rPr is not None:
r.append(deepcopy(rPr))
t = etree.SubElement(r, f'{W}t')
t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
t.text = text
return ins
def make_del(text, rPr=None, author='WB', date='2026-07-03T06:00:00Z'):
"""创建tracked deletion"""
d = etree.Element(f'{W}del')
d.set(f'{W}id', str(abs(hash(text + 'del')) % 100000))
d.set(f'{W}author', author)
d.set(f'{W}date', date)
r = etree.SubElement(d, f'{W}r')
if rPr is not None:
r.append(deepcopy(rPr))
dt = etree.SubElement(r, f'{W}delText')
dt.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
dt.text = text
return d
# 修改模式:找到目标run → 替换为del(旧) + ins(新)
target_run = ... # 找到包含目标文本的w:r元素
rPr = get_rPr_from_run(target_run)
old_text = target_run.find(f'.//{W}t').text
del_elem = make_del(old_text, rPr)
ins_elem = make_ins(new_text, rPr)
idx = list(para).index(target_run)
para.remove(target_run)
para.insert(idx, del_elem)
para.insert(idx + 1, ins_elem)
```
### 恢复已被删除的内容(撤回WB之前的del)
当需要把WB之前删除的内容恢复回来(因为模板中有这个内容):
```python
# 找到WB的del元素
for child in para:
if child.tag == f'{W}del' and child.get(f'{W}author') == 'WB':
del_texts = ''.join(dt.text for dt in child.findall(f'.//{W}delText') if dt.text)
if '目标文本' in del_texts:
# 策略:删除这个del,插入一个ins代替(因为原文已经"被删"了)
inner_r = child.find(f'.//{W}r')
inner_rPr = get_rPr_from_run(inner_r)
ins_restore = make_ins(del_texts, inner_rPr)
idx = list(para).index(child)
para.remove(child)
para.insert(idx, ins_restore)
break
```
## 验证步骤
统一修订人后必须验证:
```python
# 1. 确认只剩一个作者
authors = set()
for elem in body.iter():
a = elem.get(f'{W}author')
if a:
authors.add(a)
assert authors == {'WB'}, f"Unexpected authors: {authors}"
# 2. 获取接受所有修订后的最终文本
def get_accepted_text(para):
texts = []
for elem in para.iter():
if elem.tag == f'{W}t':
in_del = False
p = elem
while p is not None:
if p.tag == f'{W}del':
in_del = True
break
p = p.getparent()
if not in_del and elem.text:
texts.append(elem.text)
return ''.join(texts)
# 3. 逐条核对关键条款的最终文本
```
## 输出格式
报告分三部分:
1. **协商一致核对** — 逐项确认商业条件是否落实(✅/⚠️/❌)
2. **模板差异表** — 与标准模板的偏差点 + 对己方权利义务的影响评估
3. **结论与建议** — 哪些已符合、哪些需关注、是否需要进一步协商
## 注意事项
- 对方修订版如果修订已全部接受(ins=0, del=0),说明是"接受所有修订后"的干净版本
- 比较时需要分别提取:(1)当前版本接受所有修订后的最终文本;(2)带修订标记的过程文本
- 对方修改了措辞但实质不变的情况(如"整容"→"形象变化"),需评估措辞变化是否改变法律效果——措辞更宽泛时分析对哪方有利
- 费用条款的结构性改写(如合并年度/拆分年度)要验证数学正确性(年总额×期数=合同总额)
- 处理段落29-31连读场景:华诚-Z可能将原来的多个年度段落(第三、四年/第五年分开写)合并为一个(第四、第五年),导致中间段落被del清空,需要把前后段落连起来读才能看到完整的费用结构
- **修订人统一后的检查清单**:作者唯一性、ins/del总数合理、关键条款最终文本正确、文件可正常打开
@@ -0,0 +1,76 @@
# 屋顶光伏租赁合同审查要点(出租方/甲方视角)
> 来源:2026-06-15 莎莎委托审查"全额上网 租赁合同"的实践总结
> 适用:代表屋顶业主(出租方)审查光伏投资方提供的格式合同
## 合同性质
- 司法实践认定为**综合性无名合同**(最高院2020年判决),包含:
- 租赁关系 → 适用《民法典》合同编租赁合同章
- 电力供应关系 → 适用供用电合同相关规定
- **没有专门立法**,多部法律法规叠加适用
## 三种上网模式
| 模式 | 含义 | 政策限制 |
|------|------|---------|
| 全额上网 | 发电全部卖给电网 | 2025.5.1后工商业分布式**禁止** |
| 自发自用余电上网 | 优先自用,余电卖电网 | 各省有自用比例要求(30%-80%) |
| 全部自发自用 | 全部自用,不上网 | 需防逆流装置 |
**关键政策**:《分布式光伏发电开发建设管理办法》(国能发新能规〔2025〕7号),2025年1月23日发布
## 甲方视角核心审查清单
### 一、合同有效性
- [ ] 上网模式是否符合现行政策(全额上网已禁止工商业分布式)
- [ ] 租赁期限是否超过20年(《民法典》第705条上限)
- [ ] "自动续签"条款是否可执行(≠重新签合同,超出部分可能无效)
- [ ] 建筑是否有合法手续(无建设工程规划许可证→租赁合同无效,司法解释第2条)
- [ ] 是否存在在先抵押/查封(先抵后租→买卖不破租赁不适用,司法解释第14条)
### 二、租金保障
- [ ] 首次租金支付有无明确的并网截止期限
- [ ] 是否以甲方先开票为付款前提(税务资质限制风险)
- [ ] 有无租金递增机制(20年合同无递增→购买力大幅贬值)
- [ ] 乙方逾期付租有无违约责任
- [ ] 日期是否正确(常见错误:6月31日、2月30日等)
### 三、违约责任对等性
- [ ] 甲方违约赔偿范围和计算方式(常见:剩余年限全部预期收益,金额巨大)
- [ ] 乙方违约责任是否对等(典型格式合同:甲方3条详尽、乙方仅1条极简)
- [ ] 赔偿有无上限
- [ ] 甲方有无主动解除权(格式合同常常不给甲方任何解除权)
### 四、甲方义务合理性
- [ ] 甲方能否自由进入自己的屋顶(常见:需乙方书面同意)
- [ ] 甲方对第三方行为是否承担连带责任
- [ ] 是否需免费提供大量场地设施(设备房、电缆通道等)
- [ ] 乙方转让/质押资产是否仅需"通知"甲方
- [ ] 有无开放式义务(如"甲方应积极支持")
### 五、其他风险
- [ ] 拆迁补偿分配(格式合同常约定全归乙方)
- [ ] 管辖法院(格式合同常约定乙方所在地)
- [ ] 政策变化解除时甲方有无补偿
- [ ] 有无保险要求
- [ ] 合同到期后设备拆除和屋顶恢复义务
## 关键法律依据
| 法律法规 | 条文 | 内容 |
|---------|------|------|
| 《民法典》第705条 | 租赁期限 | 不得超过20年,超过部分无效 |
| 《民法典》第725条 | 买卖不破租赁 | 所有权变动不影响租赁效力 |
| 房屋租赁司法解释第2条 | 合同无效 | 无建设工程规划许可证→租赁合同无效 |
| 房屋租赁司法解释第14条 | 不受保护 | 先抵押/查封后租赁→不受保护 |
| 《企业破产法》第18条 | 管理人选择权 | 破产后管理人可解除未完合同 |
| 分布式光伏管理办法(2025) | 上网模式 | 工商业禁止全额上网 |
## 司法实践要点
- **零租金合同风险**:法院可能不认定租赁关系→无法适用买卖不破租赁(浙06民终4075号)
- **BAPV vs BIPV**:附加式光伏通常不构成添附(浙02民终4005号),一体化光伏可能构成添附
- **无规划许可证**:直接认定租赁协议无效(京民申2129号)
- **屋顶漏水**:法院严格审查因果关系,需第三方鉴定报告
- **预期收益损失**:破产清算中难以获得支持,需合同中明确约定计算方式
@@ -0,0 +1,120 @@
# Template Comparison Methodology (标准模版对比)
## When to use
When a client has a standard template and wants to know how their signed contracts deviate from it.
This is an ADD-ON to the portfolio audit workflow — produces an extra column in the Excel summary.
## Setup
1. Locate template file(s) in the client's 参考文件/ directory
2. Read template with python-docx (if .docx) or OCR (if .pdf)
3. Map template articles to a checklist
## Comparison Dimensions
### 1. Missing clauses (模版有但合同缺失)
Check each template article against the actual contract. Flag:
- Entire articles/sections missing
- Sub-clauses within a section that were dropped
- Protective language removed (e.g., "且不承担违约责任")
- Quantitative requirements removed (e.g., time limits, penalty caps)
Risk levels:
- 🔴 高:directly impacts party's core rights (押金退还、法定解除权、发票保护)
- ⚠️ 中:reduces protection but workarounds exist
- Low: cosmetic or minor scope differences
### 2. Added clauses (合同有但模版没有)
Note whether additions are:
- ✅ Favorable to the client
- ❌ Unfavorable (e.g., additional burden of proof requirements)
- Neutral
### 3. Substantive differences (实质性差异)
Side-by-side of same clause with different wording/numbers:
- Notice periods (e.g., 1 month → 3 months)
- Penalty rates (e.g., 0.1‰ vs 0.1% — **watch for OCR errors on ‰**)
- Scope qualifiers added/removed (e.g., "严重影响" vs "影响")
- Rights downgraded (e.g., "征得同意" → "通知")
- Obligation standards changed (e.g., "按现状交付" → "装修保持原状")
## Key articles to always check (rental contracts)
| Template Article | What to check |
|-----------------|---------------|
| 发票条款 | 甲方不开票→乙方可延付不违约?税务损失由谁担? |
| 押金退还 | 仅"期满"还是"期满/解除/终止"均可退? |
| 单方解除 | 有无"除法定或本合同约定外"限定? |
| 甲方违约赔偿 | 是否含退押金+退预付款+装修损失+诉讼费律师费? |
| 不可抗力范围 | 是否含"政府政策变更"+"行业治理"?还是仅限特定事件? |
| 无法办证解除 | "房屋本身原因+政策原因"还是仅"政策原因"? |
| 续租/优先权时限 | 通知期多长?模版通常1个月 |
| 看房权 | "征得同意"还是"通知"? |
| 迁离标准 | "按现状"还是"保持原状"(暗示恢复义务)? |
| 甲方备案义务 | 有无时限?不配合有无违约后果? |
| 竞业限制范围 | "大楼"还是"大楼、商圈等"? |
| 补充条款 | 是否利用了模版预留的补充条款空间? |
## Output format for L column (与标准模版差异)
For contracts with MAJOR deviations:
```
⚠️ 与标准模版存在多处重大偏离:
1. 【条款名】模版要求XX;本合同YY
2. 【条款名】模版要求XX;本合同YY
...
✅ [any positive deviation noted]
```
For contracts largely matching template:
```
✅ 与标准模版高度一致,仅以下小偏差:
1. 【条款名】差异描述
...
其余主要条款均与模版一致
```
For contracts SAME-SOURCE as template but with substantive deviations (本合同就是 07 模版填空而成、骨架同源,但个别关键条款被改动/删除 —— 人民中路即此类,最易被误判为"独立友好范本"):
```
✅ 本合同与 07 标准模版同源(系模版填空而成,条款体系/编号/措辞逐条对应),但有以下实质偏离:
1. 🔴【条款名】模版为XX;本合同被改为YY(对乙方不利/缺失关键保护)
2. 🔴【条款名】模版有XX条款;本合同整条删除
...
其余条款与模版一致(含0.1‰逾期、含疫情/行业治理、优先权等模版标配——属同源固有,非本合同特有优势)
```
> ⚠️ **此档对治的典型错误(人民中路实证)**:把模版本身就有的标配条款(0.1‰、含疫情、优先权)当成"本合同特别友好"列为优势,却漏掉真正被改动/删除的实质偏离(抵押"不得→可"、办学许可证免责款被删)。判别口诀:**先认"是不是模版填空而成"——是 → 用本档,差异只写"被改/被删"的地方,绝不把模版标配当本合同优势。**
For non-comparable documents:
```
补充协议,非模版对比范围
```
or
```
物业服务协议,无对应标准模版
```
## Implementation pattern
Use `delegate_task` with two parallel subagents:
1. **Structural comparison subagent**: reads template + actual contracts, produces clause-by-clause diff
2. **Thematic extraction subagent**: extracts specific clause categories the client cares about (e.g., termination, modification, penalties)
Pass to each subagent:
- Template full text
- All contract .md files with their table entry numbers
- Clear focus instructions
After delegation, verify outputs exist and are coherent before incorporating into Excel.
## Thematic risk additions to K column
When the client asks for specific risk categories (e.g., 合同变更与提前解除):
- APPEND to existing K column content with a clear sub-header: `【合同变更与提前解除】`
- Include: notice period, penalty formula, deposit handling, practical implications
- Flag favorable precedents (e.g., a previously successful partial surrender with zero penalty)
- In section 四, add a comprehensive summary with cost estimates for all scenarios
## OCR artifact awareness
- ‰ (千分号) frequently OCR'd as % — flag for manual verification on originals
- Table cells may have garbled column alignment
- Stamps/seals over text cause character corruption
- Article numbering may be inconsistent in OCR output
@@ -0,0 +1,109 @@
# Workflow 系统性问题清单(2026-06-27 至 2026-07-01)
## 1. auto_notify 静默失效(模式B)
**表现**:进程在跑但 inotifywait 没有捕获事件,日志完全为空。
**根因**:inotifywait 文件描述符失效/内核事件丢失。
**现状**:watchdog cron (`63bb31d4f050`) 只能重启进程,无法修复内核级别的事件丢失。
**Fallback**:手动检查 `~/.hermes/cache/documents/` + relay 脚本串行启动 workflow。
## 2. 非合同文件误审
**表现**:香花桥家庭医生招标需求被当合同审查。
**根因**:classifier 的 `not_a_contract` 逻辑虽然写了(第13-19行),但 LLM 执行时没走到——可能因标题含"服务""项目"等关键词触发了合同判断。
**修复状态**:通知脚本已存在(`wecom_group_notify.py`),routing 已配置(`$END`)。需加强 classifier prompt 中对招标/技术需求的识别。
## 3. 审查意见不该生成
**表现**:非卫生中心合同(如生育友好-计生协会)也自动生成了审查意见。
**根因**:workflow 对所有合同统一执行 editor 生成审查意见的逻辑,未按 ruleset_type 区分。
**修复方向**:在 editor procedure 中加条件判断——只有 ruleset_type=health-centers 且 review-rules.md 中明确要求"审查意见"交付物时才生成。
## 4. 文件名 hash 前缀残留
**表现**`【修】doc_0ad4ee63bff5_印刷品制作合同2026.6(1).docx`
**根因**:deliverer 从 cache 路径取文件名时未剥离 `doc_[0-9a-f]{12}_` 前缀。
**修复状态**:editor procedure step 7 已有清理逻辑(第147-149行),但 deliverer 步骤可能绕过了 editor 的命名。
## 5. 新增条款插入位置错乱
**表现**:健康积分合同新增"数据归属""转包分包"条款标题和内容分离。
**根因**:editor 用 python-docx 插入段落时,对文档结构的理解不够(插入在错误的 anchor 位置)。
**自愈**:reviewer 第2轮检出并返回 editor 修复,第3轮通过。但多花了2轮=多花10分钟+token。
## 6. idle thread 不自动继续
**表现**:反委托代发的 thread 卡在 idle classifier。
**根因**:auto_notify 启动了 thread (`uwf thread start`) 但没有执行 (`uwf thread exec --count 20 --background`)。
**修复**:需要确保 relay/queue 脚本在 start 后立即 exec。
## 7. /tmp/contract-review 被僵尸线程污染(2026-07-01)
**表现**:新启动的 classifier 在 /tmp/contract-review/ 中看到旧线程残留的文件(其他合同的【修】文件、.py脚本、.pdf等),导致分类器混乱或长时间卡住。
**根因**
- queue-runner.sh 和手动启动并存,多个 thread exec 进程同时操作 /tmp/contract-review/
- 僵尸线程(6月26日、6月30日的stuck threads)被queue-runner定期唤醒但无法推进,占用/tmp/contract-review/
- `uwf thread show` 显示 "running" 但实际 uwf-hermes 子进程已死或卡住
### 7b. Queue-runner 自动启动重复线程(2026-07-02 恭兴合同教训)
**表现**:手动启动的 thread `06FJ1DY529R7C85BDPBQG1QBBW` 和 queue-runner 自动启动的 `06FJ1DVM3PCZYVDXAAXE1DTYBG` 同时处理恭兴合同.docx,两个 uwf-hermes reviewer 进程同时写 /tmp/contract-review/【修】恭兴合同.docx。
**根因**:queue-runner.sh 检测到 Nextcloud 待审查目录中的新文件后自动 `uwf thread start + exec`,与手动操作撞车。
**症状**`ps aux | grep uwf-hermes` 可以看到**两个**不同 thread-id 的 reviewer/editor 进程,prompt 里有相同的文件名。
**修复**:手动管理合同时,**必须先 kill queue-runner**:
```bash
kill $(ps aux | grep "[c]ontract-queue-runner" | awk '{print $2}') 2>/dev/null
```
确认只有自己的线程在跑后再继续。如果发现重复线程已经启动,cancel 它并 kill 其进程。
**教训**:仅 cancel thread 不够——对应的 uwf-hermes 子进程可能仍在运行(进程不感知 thread status 变化),必须同时 kill 进程 PID。
**清理步骤(批量开工前必做)**
```bash
# 1. 查看正在运行的uwf进程
ps aux | grep "[u]wf-hermes" | grep -v grep
ps aux | grep "[u]wf.*thread exec" | grep -v grep
# 2. 杀死所有uwf-hermes子进程
kill $(ps aux | grep "[u]wf-hermes" | grep -v grep | awk '{print $2}') 2>/dev/null
# 3. 杀死queue-runner
kill $(ps aux | grep "[c]ontract-queue-runner" | awk '{print $2}') 2>/dev/null
# 4. 取消所有非当前的stuck threads
/home/maggie/.hermes/node/bin/uwf thread list | grep running
# 对每个非当前任务的thread: uwf thread cancel <id>
# 5. 清理/tmp/contract-review(保留rules/)
cd /tmp/contract-review
rm -f *.docx *.docx.bak *.py *.pdf *.png *.yaml *.xml 【修】*.docx 2>/dev/null
# 6. 确认干净
ls /tmp/contract-review/ | grep -v rules
# 应该为空
# 7. 复制新合同文件进去,启动thread
```
**预防**:串行处理合同(一份完成再启动下一份)。`uwf thread show` 显示 end 后再清理+启动下一份。
## 8. 批量合同串行处理实操(2026-07-01 四份合同)
**场景**:邱律师一次发2-4份合同,需要逐份串行审查。
**正确流程**
1. 收到所有文件 → 上传Nextcloud待审查目录 → 复制到本地shared目录
2. 第1份:清理/tmp → 复制文件 → `uwf thread start``uwf thread exec --count 20 --background` → 等`status=end`
3. 第2份:清理/tmp → 复制文件 → 新thread → exec → 等end
4. 全部完成后:统一更新tracker、不逐份通知(通知只在全部完成后发一次)
**等待策略**
- `sleep N && uwf thread show <id>` 轮询,每轮5-10分钟
- 典型合同全程约30-60分钟(classifier 2-3min → reviewer 5-10min → editor 5-10min → reviewer复核 5-10min → 可能再editor → final_review 3-5min → deliverer 2-3min)
- 如果同一个role卡超过15分钟没变化,检查 `ps aux | grep uwf-hermes` 进程是否存活
## 待修复优先级
1. 【高】加强 classifier 对非合同文件的识别(prompt 强化)
2. 【中】审查意见按 ruleset_type 条件生成
3. 【中】/tmp/contract-review 隔离(考虑按thread_id创建子目录,避免多线程冲突)
4. 【低】auto_notify 根因(可能需要换 inotifywait 为 polling 方案)
5. 【低】新增条款插入位置的准确性(需要更结构化的段落插入逻辑)