feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,698 @@
|
||||
---
|
||||
name: contract-pass-workflow
|
||||
description: 合同审查pass后的标准操作——更新tracker、更新xlsx清单、上传Nextcloud、清缓存。Doro说pass后照此执行。
|
||||
version: 1.0.0
|
||||
tags: [合同审查, pass, tracker, xlsx]
|
||||
---
|
||||
|
||||
# 合同审查 Pass 后标准操作
|
||||
|
||||
## 交付文件位置(铁律,2026-06-29 Doro纠正)
|
||||
|
||||
所有交付文件统一放在 **`Doro合同审查任务/任务交付/`(根目录)**,不放顾问单位子文件夹。
|
||||
|
||||
- ✅ `Doro合同审查任务/任务交付/【修】合同_朱家角.docx`
|
||||
- ❌ `Doro合同审查任务/朱家角镇社区卫生服务中心/任务交付/【修】合同_朱家角.docx`
|
||||
|
||||
workflow YAML deliverer 步骤(第210行)明确写的上传路径是根目录 `任务交付/`。如果 deliverer 或 final_review 把文件放到了子文件夹,pass 步骤0核对时必须发现并移到根目录。
|
||||
|
||||
## 步骤0核对要点
|
||||
|
||||
pass 步骤0的核对流程:
|
||||
1. 取原始文件名(去掉【修】/【审】前缀)
|
||||
2. 去 `Doro合同审查任务/待审查/` 确认源文件存在
|
||||
3. **确认交付文件在根目录 `任务交付/`**(不是子文件夹)
|
||||
4. 顾问单位核对(从合同正文读甲方名称)
|
||||
5. tracker 查重
|
||||
|
||||
## 红线:验证指令 ≠ 回忆指令(2026-07-02 信任危机后确立)
|
||||
|
||||
### 根因排查铁律:抛弃旧判断,先查“发生了什么变化”,再谈原因(2026-07-14 Doro纠正)
|
||||
|
||||
当 Doro 明确说“去查原因”“不是让你猜测、推断”“完全抛弃此前的判断,重新核查情况”时,后续动作必须切换为**变化核查模式**,而不是继续打磨措辞、修补上一版归因。
|
||||
|
||||
**强制步骤:**
|
||||
1. **旧判断全部作废**:停止沿用“状态漂移”“异常”“习惯变差”“元控制失效”等任何解释性语言,除非已经有直接证据支持。
|
||||
2. **先限定时间窗**:如果用户已给出时间边界(如“发生在 7 月 10 日前”),所有核查必须围绕该边界展开;边界外的改动先排除,不得混入结论。
|
||||
3. **优先查“可观察变化”而不是“解释”**:
|
||||
- 配置文件修改时间与具体改动;
|
||||
- workflow/queue/watchdog/notify 脚本的修改时间与新增逻辑;
|
||||
- skill 规则文本的修改时间;
|
||||
- gateway / auto-notify / watchdog 的退出、重启、恢复日志;
|
||||
- tracker / queue / done / manifest 等状态文件是否出现结构变化。
|
||||
4. **汇报格式必须是“已查到的变化 / 没查到的变化”**,不能把“更可能”“像是”“说明了”写成原因结论。
|
||||
5. **只有在“变化事实”查清后,才允许进入第二步根因分析**;若变化事实尚未闭环,明确说“目前只查到这些变化,原因尚未下结论”。
|
||||
|
||||
**禁止事项:**
|
||||
- 不断修改上一版判断的措辞,假装自己在继续调查;
|
||||
- 用“异常”“偶发”“状态不好”“坏习惯”去解释持续两天、批量失效的问题;
|
||||
- 把证据和推断混写成同一层结论;
|
||||
- 用户要求查原因时,实际只做语言收缩而不做新核查。
|
||||
|
||||
**本会话教训:** Doro连续纠正“不是让你猜测、推断”“不是让你不断修改措辞”“要你完全抛弃此前判断,重新核查 7 月 10 日前发生了什么变化”。以后凡是原因排查任务,第一步不是解释,而是建立时间窗并枚举已发生变化。
|
||||
|
||||
### 先查清时间窗,再查数据(2026-07-13 本会话再犯后补丁)
|
||||
|
||||
当 Doro/Maggie 说“上周五”“今天”“昨天”“本周一”这类**相对日期**时,**第一步不是直接查数据,而是先把自然语言时间词锚定成明确的北京时间起止窗口**。没先锚时间窗,就会出现:
|
||||
- 把“上周五”理解成错误日期;
|
||||
- UTC/BJT 边界算错;
|
||||
- 用错窗口后得出“0 份”之类错误结论;
|
||||
- 之后再补查 gateway.log 才发现用户是对的,严重伤信任。
|
||||
|
||||
**强制步骤:**
|
||||
1. 先用北京时间确认当前日期和星期;
|
||||
2. 把“上周五/今天”等词转换成**北京时间明确起止时间**;
|
||||
3. 再换算成 UTC 时间戳/窗口;
|
||||
4. 把这个时间窗写出来后,才开始查 `.meta` / gateway.log / tracker / xlsx。
|
||||
|
||||
**执行纪律:**
|
||||
- 如果 `meta` 查不到,但 `gateway.log` 里已经出现该日文件消息证据,**不得继续说“0 份”或“没发”**;必须当场降级结论为“已证明确实发了,但文件清单仍在继续反查”。
|
||||
- 对周五这类历史批次,**至少要交叉两源**:`gateway.log` 文件消息 + tracker / xlsx / queue 痕迹,不能只信一侧。
|
||||
- 没闭环前,结论只能说“已查实部分”“尚未查实部分”,不能提前给“全部无遗漏”的总判断。
|
||||
|
||||
**本会话教训**:周五邱律师明明发了文件,但因先把时间窗算错、又只依赖 `.meta`,错误说成“0 份”;之后从 `gateway.log` 查到 `msg=''` 文件消息才纠正。以后凡是相对日期统计任务,**先定北京时间窗口,再查数据**。
|
||||
|
||||
## 红线:验证指令 ≠ 回忆指令(2026-07-02 信任危机后确立)
|
||||
|
||||
### 当前交付目录全量 pass 指令(2026-07-13 Doro明确授权)
|
||||
|
||||
当 Doro 明确下达类似指令:
|
||||
- "任务交付文件夹里所有的合同及companion,都做pass"
|
||||
- "现在这一刻,Nextcloud-任务交付里,所有的合同和companion,都做pass流程"
|
||||
|
||||
这属于**对当前任务交付目录的全量授权**,不再局限于单份合同或单个 companion。此时必须:
|
||||
|
||||
1. **先列出任务交付目录当前全部文件**,不要凭上一条对话里提到的那一份合同推断。
|
||||
2. **合同与 companion 都要纳入核查范围**——先查清目录里到底有哪些主合同、有哪些 companion,不能只扫主合同关键词就下结论。
|
||||
3. **主合同与 companion 的 pass 处理规则不同**:
|
||||
- **主合同**:做完整 pass——tracker 标记 `completed`,并登记到 excel。
|
||||
- **companion**:属于主合同的附属交付物,**不单独登记到 excel,不单独占 seq**。只在核查结果中确认其存在与关联关系,必要时体现在主合同的 pass 备注/关联记录中。
|
||||
4. **先查 tracker / xlsx 再写入**,但用户已经授权时,不要卡在"没有 tracker 记录所以我先不做"的保守口径上;对主合同应直接补建记录并完成 completed + excel 登记。
|
||||
5. **汇报时先给出全量清单,再区分主合同与 companion 的处理结果**,不能把“全部做pass”误写成“所有文件都单独登记 excel”。
|
||||
|
||||
### 本次会话教训
|
||||
- 错误做法:只围绕白鹤这一份合同回答"已pass",没有先把任务交付目录全量列出,遗漏了香花桥和朱家角积分项目的一整组合同及 companion。
|
||||
- 第二层错误:把 companion 当成主合同一样单独写入 tracker/xlsx,导致 seq 冲突和错误登记。
|
||||
- 正确做法:当 Doro 说"任务交付文件夹里所有的合同及companion,都做pass"时,必须把**当前任务交付目录的全部文件**作为审计范围,但**excel 只登记主合同,companion 不单独登记**。
|
||||
**凡Doro说"查""核实""核对""是不是都""有没有遗漏"→ 回复中必须先有工具调用再有结论。context记忆/session summary ≠ 查证,不可直接输出。**
|
||||
|
||||
违反后果:Doro已明确说"到了无法信任你的程度"。这不是规则问题,是行为问题——规则早就写了(见已知坑),照样违反。
|
||||
|
||||
四个具体失败模式(2026-07-02 同一轮对话全犯):
|
||||
1. **拿记忆当查证**:session context有信息→直接组织成答案→包装成"查实结果"→其实没跑工具
|
||||
2. **渠道遗漏**:只查了QiuTing私信,漏了Doro私信、Doro直接Nextcloud上传、飞书等渠道
|
||||
3. **不认识自己的输出**:之前汇报过的数据(seq 216-222含重固),被追问时说"找不到"——数据一直在,没看自己历史输出
|
||||
4. **推责给用户**:找不到时直接问Doro"你记得叫什么名字吗"——把验证责任转嫁给用户。正确做法:穷尽搜索策略(换关键词、按时间批次找、按相邻seq推断、直接读xlsx逐行扫、session_search换多个query)。数据就在tracker里,是自己没认真遍历。
|
||||
|
||||
**第4条补充(2026-07-02追加)**:被追问"你真的查了吗"→ 如果上一条回复里没有tool call,直接承认"没查,现在查"。不辩解、不包装。
|
||||
|
||||
**唯一可接受的行为**:
|
||||
- 跑工具 → 看输出 → 写结论
|
||||
- 不确定的说不确定
|
||||
- 被追问时不防御不绕,直接承认没查到
|
||||
- 对比自己之前输出过的表格/数据,确认是否已经有答案
|
||||
|
||||
## 恢复到workflow交付状态(2026-07-13 教训)
|
||||
|
||||
Doro可能要求"恢复到workflow完成的状态"——意思是撤销你的手动修改,让NC上的交付文件回到workflow原始交付版本。
|
||||
|
||||
**恢复方法(按优先级)**:
|
||||
1. **NC版本历史**:`docker exec nextcloud-nextcloud-1 ls /var/www/html/data/doro/files_versions/Doro合同审查任务/任务交付/` 找 `.v<timestamp>` 文件,最早的版本 = workflow原始交付版
|
||||
2. **`/tmp/pass_check_*` 副本**:pass流程核对时从NC拉取的副本,如果时间早于你的手动修改,就是workflow原版
|
||||
3. **练塘镇子目录副本**:部分合同在 `练塘镇社区卫生服务中心/任务交付/` 也有一份
|
||||
|
||||
**操作**:docker cp 覆盖 → chown www-data → occ files:scan。
|
||||
|
||||
**场景**:你手动修复了workflow交付的合同(补编号、修字体等),但Doro认为应该重新走workflow而非手动修补。恢复后等Doro进一步指示。
|
||||
|
||||
## 前置审查(Doro要求"审查workflow修改"时触发)
|
||||
|
||||
Doro可能在pass之前要求逐份审查workflow交付物的质量。这不是pass流程,而是**前置质量审计**,只有审计通过+手动修复后Doro才会说pass。
|
||||
|
||||
### 审计方法论
|
||||
|
||||
1. **区分原文修订 vs workflow修订**:用zipfile读XML,按`w:ins/@author`和`w:del/@author`区分。`author=WB`是workflow的,其余(WB-1/86187/杨丽等)是原文自带的→保持不动。
|
||||
2. **对照原文确认**:必须docker cp原文件(从NC待审查目录),转换后逐段对比,确认哪些修订是原文自带。
|
||||
3. **逐项检查清单**:
|
||||
- INS rFonts:WB INS的rFonts属性必须与同段原文run一致(不多不少)。常见问题:多了hAnsi/cs/hint
|
||||
- pStyle:新增条款标题的段落样式必须与原文条款标题一致(如Heading4),不能用Style15等其他样式
|
||||
- 标题空格:原文"第六条违约责任"无空格→新增也不能有空格"第七条转包与分包"
|
||||
- 子编号顺延:章节编号改了(七→八),内部子编号(7.1→8.1)也必须改
|
||||
- 赔偿上限:双向条款的赔偿上限(限制甲方获赔)按规则"能删就删"
|
||||
- 内容去重:新增保密存续等条款前,检查原文是否已有同义表述
|
||||
- 脚注"法律顾问修订版":**必须用修订格式**(w:ins, author=WB),不能是普通文本
|
||||
- 文件命名:【修】+原文件名一字不动(含扩展名变化.doc→.docx)
|
||||
4. **通读全文**:接受修订后的全文必须通读,检查WB插入的内容是否与上下文语句通顺、有无重复编号(原文问题不改,但要识别)
|
||||
5. **Doro的期望**:
|
||||
- "打开文件查清楚再回答我"→ 必须用工具完整检查后才能下结论,不能凭印象
|
||||
- "看清楚前后文再回答我"→ 报告问题前必须理解修改点的上下文语境
|
||||
- "有没有该加粗没加粗的"→ 格式检查要全面(bold/style/indent/spacing都要对比)
|
||||
- "按照workflow的规则,手动修改"→ 发现问题后直接修复,不只是报告
|
||||
|
||||
### 修复操作要点
|
||||
|
||||
- 用zipfile+lxml直接操作XML(不用python-docx修改tracked changes)
|
||||
- 修复后的INS rFonts只保留原文有的属性(通常eastAsia+ascii)
|
||||
- 子编号顺延:原文数字可能分散在多个run中(如"7"+".1 "两个run),只需DEL+INS第一个数字run
|
||||
- P49类大段文本需精确split run(保留前后文,只DEL中间要删的部分)
|
||||
- 修复后必须python-docx打开验证
|
||||
|
||||
## 触发条件
|
||||
Doro对已交付的合同**明确说"pass"**(可以是单份或批量)。
|
||||
|
||||
⚠️ **绝对前提:Doro必须亲口说"pass"才能启动此流程。** 合同交付后、workflow完成后、端午节合同end后——这些都**不是**pass。不要因为合同已交付就主动查xlsx/tracker或准备pass操作。Doro没说pass之前,对交付物的一切后续操作(更新tracker、更新xlsx、查重等)都不做。
|
||||
|
||||
⚠️ **"我满意了"≠Doro说pass(2026-07-13教训)**:即使你作为审查负责人完成了质检、修复了所有问题、对文件满意,也**绝不能自行执行pass流程**。必须等Doro明确说"pass"。2026-07-13实证:白鹤劳务派遣协议修复完毕后自行做了pass,被Doro纠正"我没说pass你做什么pass",紧急撤销(tracker回退delivered+xlsx删行)。自主质检和pass是两个完全独立的步骤——前者是你的职责,后者是Doro的权力。
|
||||
|
||||
2026-06-15教训:两次被Doro纠正("我都还没说pass呢"、"今天的合同我都没说pass呢")——agent在合同交付后主动查xlsx是否已更新、准备补写tracker,被视为越权操作。
|
||||
2026-07-13教训:白鹤劳务派遣协议自行质检完后直接执行pass,被纠正"我没说pass你做什么pass",紧急撤销。
|
||||
|
||||
**手动审查交付物的完整检查清单**见 `references/manual-review-checklist-0713.md`——当Doro要求"审查workflow修改的情况"时按此执行。核心:自主质检→发现问题直接修→报告→等Doro说pass。不问"需要修复吗",不自行pass。
|
||||
|
||||
## 特殊判定:"终身不通过"
|
||||
|
||||
Doro可能对某些合同说"终身不通过"——这意味着合同审查质量太差,**永远不会pass**。
|
||||
- **不是返工**:不是让你修了再交,而是直接否决
|
||||
- **处理方式**:在tracker中标记`status: "permanently_rejected"`,不进入pass流程
|
||||
- **反思**:必须分析为什么质量差到这个程度,是workflow哪个角色出了问题,把教训记入historical-failures
|
||||
- **不要追问Doro**:已经定性了就不要再烦,自己复盘
|
||||
|
||||
## 操作步骤(严格按顺序)
|
||||
|
||||
### 0. 交付物核对(写入前必做)
|
||||
|
||||
⚠️ **PDF 批注交付件的独立核验** → 用 `scripts/verify_pdf_annotation_deliverable.py <原件> <NC交付件> [本地核验件]`:一键查 SHA256 字节一致、页数、原文未改动、批注数/author=WB、批注格式("建议"开头无【】)、高亮几何锚定。扫描件/PDF 合同走批注模式(非 docx 修订),docx 专项检查不适用,用此脚本替代。
|
||||
|
||||
Doro说pass后,在写tracker/xlsx之前,**必须先核对交付物与源文件的匹配关系**:
|
||||
|
||||
1. **取原始文件名**:从交付文件名去掉`【修】`或`【无修改意见】`前缀 → 得到原始文件名
|
||||
2. **待审查目录核实**:去Nextcloud `Doro合同审查任务/待审查/` 确认该原始文件名存在。不存在则停下来排查,不继续写入
|
||||
3. **顾问单位核对**:确认要写入xlsx的顾问单位名称正确。⚠️ 不能盲信classifier的`our_party_name`——已有多次classifier误判案例(如智慧医院云项目合同classifier设为"朱家角"实际甲方是"卫健事业发展中心")。**必须用python-docx打开合同原文,直接读取甲方名称**
|
||||
- **⚠️ python-docx 的 `paragraph.text` 会吞掉 `w:ins`(修订插入)内容 → 读出的甲方可能是"补全前"的残缺名(2026-06-25 实证)**:当本次审查的改动之一就是"给甲方补全行政区前缀"(如 WB 修订插入「上海市青浦区」),交付件里甲方全称是「原稿可见文字 + w:ins 插入文字」拼起来的。`python-docx` 遍历 `doc.paragraphs[i].text` 时**未必包含 ins 的文字**,会让你误以为甲方还是缺前缀的简称。**核甲方全称必须把 `w:ins` 算进去**:用 zipfile 读 `word/document.xml`、对甲方那一段 `''.join(t.text for t in p.iter('{...}t'))`(`w:t` 不分 ins/非 ins,全收),或干脆遍历所有 `w:ins` 看 author=WB 插了什么。2026-06-25 项目终止协议书:`paragraph.text` 显示甲方「华新镇社区卫生服务中心」,实际交付件 w:ins 补了「上海市青浦区」,全称应是「上海市青浦区华新镇社区卫生服务中心」——只看 `.text` 就会把残缺简称写进 tracker/xlsx。**写 party 前先确认你读的是"接受修订后"的完整甲方名。**
|
||||
- **顾问单位名称空白的特殊情况(按合同来源分两路,2026-06-25 补全)**:合同正文里顾问单位名称是空白下划线(待签时填)时,无法从正文确定。**先分清这份合同是谁的任务线**,再决定问谁——别默认是邱律师:
|
||||
- **邱律师批量线**(health-centers):私信邱律师询问归属。私信用 `python3 ~/.hermes/scripts/wecom_dm.py --to qiuting --text "…"`(或 `_send_wecom(extra,'QiuTing',msg)`),**不用** `send_message(target='wecom:X')`(静默回退 home channel)。暂停 pass(不写 tracker/xlsx),邱律师回复后从步骤1继续;Doro 说过"邱律师回复了就直接补上 我不管了"——回复后自主补登记,不再找 Doro。
|
||||
- **Doro 直接指派 / 非批量线**(如幼儿园保密协议、劳动合同等非卫生中心合同):**问发起 pass 的人本人**(通常就是正在对话的 Doro),不要问邱律师,也不要自己瞎填占位符。
|
||||
- ⚠️ **绝不用泛指占位符当 party 写进 tracker/xlsx**(如"幼儿园(园方,名称空白待填)")——2026-06-25 教训:园名空白我写了这种占位符,Doro 直接纠正"顾问单位是平和学校"。空白就停下来问准确**法律主体全称**,确认后再写。
|
||||
- **同名/近名主体消歧(2026-06-25 教训,写 party 前必做)**:拿到顾问单位名后,先 `openpyxl` 扫 xlsx 第 C 列看清单里**是否已有多个相似名**的主体——它们往往是**不同法律实体**,不能混用。本会话清单里同时有「上海青浦平和**幼儿园有限公司**」(seq13/15) 和「上海青浦平和**双语学校**」(seq44/210/211),一个园、一个校,是两个主体。判别靠**合同内容性质**:这份通篇"幼儿园/幼儿就读/保教费"→幼儿园主体;但既然清单里并存多个近名实体,**最终仍用 `clarify` 让发起人拍板一个准确全称**,并复用清单里既有的写法(保持前后一致,别造新写法)。
|
||||
- **更正已写错的 party(两处同步)**:若 tracker/xlsx 已写入后才被纠正,两处都要改:tracker 用原子写回(tempfile+rename)改对应 seq 的 `party`;xlsx 改第 C 列后重新 `docker cp` 上传 + `occ files:scan` + 清 OnlyOffice 缓存重启;最后从线上重新拉 xlsx + 读 tracker **核对两处一致**才算完成。
|
||||
4. **查重**:在tracker中检查是否已有相同 `original_filename` + `party` 的completed记录。有则为重复交付,不再写入
|
||||
|
||||
以上4步全部通过,才进入步骤1写tracker。任何一步不通过,停下来排查原因。
|
||||
|
||||
**注意**:Doro批量pass时可能列出审查意见文件(如"朱家角审查意见-xxx"、"采购协议-审查意见")。这些是主合同的附属交付物,**默认不单独写tracker/xlsx条目**。只需为主合同文件(【修】前缀的)写tracker和xlsx。审查意见文件如果不在交付目录中(可能已被清理或因classifier误判未生成),不影响主合同的pass流程——跳过即可,不需要报错或追问Doro。
|
||||
|
||||
### 例外:用户明确说“任务交付文件夹里所有的合同及companion,都做pass流程”时(2026-07-13 Doro明确)
|
||||
当 Doro 用**当前任务交付目录全量授权**的口径下指示:
|
||||
- “任务交付文件夹里所有的合同及companion,都做pass”
|
||||
- “现在这一刻,Nextcloud-任务交付里,所有的合同和companion,都做pass流程”
|
||||
|
||||
此时必须把 companion **纳入 pass 核查范围**,但仍要遵守:
|
||||
- **companion 不是独立合同,不单独登记 excel,不单独占 seq**;
|
||||
- tracker/xlsx 的登记主体仍然是**主合同**;
|
||||
- companion 的处理应当体现在“该主合同已连同 companion 一并核查/补做pass”的结果里,而不是把每个 companion 当主合同单独建台账。
|
||||
|
||||
**执行顺序**:
|
||||
1. 先把当前 `任务交付/` 目录全部文件列出来,不能只围绕当前对话那一份合同;
|
||||
2. 再识别哪些是主合同、哪些是 companion;
|
||||
3. 主合同逐份做 pass / tracker / xlsx;
|
||||
4. companion 只做挂靠核查,不单独占 excel 行;
|
||||
5. 回复时明确区分“主合同已登记几份、companion 已核查几份”,不要混成一类。
|
||||
|
||||
**2026-06-11教训**:安全测试合同因跳过核对,第一次把原始文件名写进xlsx文件名列(没有【修】前缀),发现后补写但未清理错误记录,导致tracker和xlsx各多一条重复数据。
|
||||
**2026-06-12教训**:手动写xlsx时列顺序写反(日期和序号对调、文件名列写成原始文件名而非delivered_filename、顾问单位列写成"已完成"状态文字)。**写xlsx前必须先读上一行确认列顺序**,不要凭记忆。正确列顺序:A=序号, B=日期, C=顾问单位, D=合同名称, E=文件名(delivered_filename)。
|
||||
|
||||
### 1. 更新 Tracker JSON
|
||||
|
||||
路径:`~/.hermes/data/contract-tracker.json`
|
||||
|
||||
#### 状态流转(新增 `delivered` 中间态,2026-07-02 确立)
|
||||
|
||||
```
|
||||
文件到达 → workflow审查 → deliverer交付成功 → queue-runner写入 status=delivered
|
||||
↓
|
||||
Doro说pass → status=completed
|
||||
↓
|
||||
24h后 → cleanup清理
|
||||
```
|
||||
|
||||
**三道防线防重复:**
|
||||
| 检查点 | 逻辑 |
|
||||
|--------|------|
|
||||
| queue-runner 启动前 | 查 tracker,`delivered` 或 `completed` → 直接 SKIP 并移入 done/ |
|
||||
| watchdog resume 前 | 查 tracker,已交付的 thread 不恢复 |
|
||||
| deliverer 写入时 | exists 检查,同文件不重复写入 |
|
||||
|
||||
#### Pass 时的写入逻辑
|
||||
|
||||
**如果 tracker 中已有该 `original_filename` 且 `status=delivered`** → 更新该记录为 `completed`,补全 `seq`/`party`/`contract_name`/`delivered_filename`/`converted_filename`/`xlsx_updated_at`/`cleaned=false`。
|
||||
|
||||
**如果 tracker 中没有记录**(旧合同、手动审查等情况)→ 新建完整记录,直接 `status=completed`。
|
||||
|
||||
完整记录示例:
|
||||
```json
|
||||
{
|
||||
"original_filename": "消防设施检测服务合同(练塘卫生院).doc",
|
||||
"delivered_filename": "【修】消防设施检测服务合同(练塘卫生院).docx",
|
||||
"converted_filename": "消防设施检测服务合同(练塘卫生院).docx",
|
||||
"party": "上海市青浦区练塘镇社区卫生服务中心",
|
||||
"contract_name": "消防设施2026年度检测服务合同",
|
||||
"seq": 175,
|
||||
"status": "completed",
|
||||
"delivered_at": "2026-06-09T18:30:00+08:00",
|
||||
"xlsx_updated_at": "2026-06-09T20:09:00+08:00",
|
||||
"cleaned": false
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
- `original_filename`:邱律师发来的原始文件名(可能是.doc)
|
||||
- `delivered_filename`:交付到任务交付目录的文件名(【修】前缀)
|
||||
- `converted_filename`:workflow转换后的.docx文件名(原始是.doc时有值,否则空字符串)。**来源**:workflow的`converted_filename`字段会从classifier一路传递到deliverer/final_review输出。pass流程必须从workflow输出中提取并写入tracker。如果workflow输出中没有此字段(旧workflow跑的合同),则根据original_filename判断:以`.doc`结尾的,converted_filename = 同名`.docx`;以`.docx`结尾的,converted_filename = 空字符串
|
||||
- `seq`:合同审查清单中的序号
|
||||
- `delivered_at`:workflow 交付完成时间(queue-runner 写入)
|
||||
- `xlsx_updated_at`:pass 时写入,北京时间ISO格式,清理脚本据此计算24h
|
||||
- `cleaned`:清理脚本执行后改为true
|
||||
|
||||
**写入方式**:先写临时文件再rename(原子操作),防止进程中断导致JSON损坏。
|
||||
|
||||
```python
|
||||
import json, os, tempfile
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
BJT = timezone(timedelta(hours=8))
|
||||
tracker_path = os.path.expanduser('~/.hermes/data/contract-tracker.json')
|
||||
|
||||
# 读取现有tracker
|
||||
if os.path.exists(tracker_path):
|
||||
with open(tracker_path, 'r') as f:
|
||||
tracker = json.load(f)
|
||||
else:
|
||||
tracker = {"contracts": []}
|
||||
|
||||
now = datetime.now(BJT).isoformat()
|
||||
|
||||
# 查找是否已有 delivered 记录
|
||||
existing = None
|
||||
for c in tracker["contracts"]:
|
||||
if c.get("original_filename") == original_filename:
|
||||
existing = c
|
||||
break
|
||||
|
||||
if existing and existing.get("status") == "delivered":
|
||||
# 已有 delivered → 更新为 completed(补全字段)
|
||||
existing["status"] = "completed"
|
||||
existing["delivered_filename"] = delivered_filename
|
||||
existing["converted_filename"] = converted_filename
|
||||
existing["party"] = party
|
||||
existing["contract_name"] = contract_name
|
||||
existing["seq"] = seq
|
||||
existing["xlsx_updated_at"] = now
|
||||
existing["cleaned"] = False
|
||||
elif existing and existing.get("status") == "completed":
|
||||
# 已经 completed → 查重命中,不重复写入
|
||||
pass
|
||||
else:
|
||||
# 无记录 → 新建(旧合同/手动审查走这条路)
|
||||
tracker["contracts"].append({
|
||||
"original_filename": original_filename,
|
||||
"delivered_filename": delivered_filename,
|
||||
"converted_filename": converted_filename,
|
||||
"party": party,
|
||||
"contract_name": contract_name,
|
||||
"seq": seq,
|
||||
"status": "completed",
|
||||
"xlsx_updated_at": now,
|
||||
"cleaned": False
|
||||
})
|
||||
|
||||
# 原子写入
|
||||
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(tracker_path), suffix='.json')
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
json.dump(tracker, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, tracker_path)
|
||||
```
|
||||
|
||||
### 2. 更新合同审查清单 xlsx
|
||||
|
||||
路径(Nextcloud):`Doro合同审查任务/合同审查清单.xlsx`
|
||||
|
||||
⚠️ **铁律(2026-07-03覆盖事故后确立):写xlsx前必须从Nextcloud实时拉取最新版本,禁止使用/tmp或本地任何已有的xlsx文件。** 违反此规则会用旧版覆盖新版,导致其他session已登记的记录丢失(2026-07-03实证:用240行旧文件覆盖了252行最新版,丢失12条记录)。
|
||||
|
||||
**强制检查流程**:
|
||||
1. `docker cp` 从NC拉取 → 保存到 `/tmp/合同审查清单_LIVE.xlsx`(带LIVE后缀避免与残留文件混淆)
|
||||
2. 打开后先读 `ws.max_row` 和最后一行的seq → 打印确认
|
||||
3. 如果发现本地已有同名文件,**必须删除后重新拉取**,不得复用
|
||||
4. 追加新行后上传
|
||||
|
||||
步骤:
|
||||
1. **从Nextcloud下载最新xlsx(必须实时拉取,禁止复用本地文件)**
|
||||
2. 用openpyxl追加行(序号、日期、顾问单位全称、合同名称、文件名)
|
||||
3. 从上一行复制字体和对齐样式(用copy())
|
||||
4. border用Side对象重建(不用ref_cell.border直接赋值,会报unhashable错误)
|
||||
5. 保存后上传回Nextcloud
|
||||
6. 执行files:scan + 清OnlyOffice缓存
|
||||
|
||||
```bash
|
||||
# 下载(铁律:必须每次实时拉取,rm掉旧文件防止复用)
|
||||
rm -f /tmp/合同审查清单_LIVE.xlsx
|
||||
docker cp nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx /tmp/合同审查清单_LIVE.xlsx
|
||||
sudo chown maggie:maggie /tmp/合同审查清单_LIVE.xlsx
|
||||
|
||||
# 上传
|
||||
docker cp /tmp/合同审查清单_LIVE.xlsx nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
|
||||
# 上传
|
||||
docker cp /tmp/合同审查清单_LIVE.xlsx nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
|
||||
python3 -c "import openpyxl; ws=openpyxl.load_workbook('/tmp/合同审查清单_LIVE.xlsx').active; print(f'NC当前版本: {ws.max_row}行, 最后seq={ws.cell(ws.max_row,1).value}')"
|
||||
|
||||
# 上传
|
||||
docker cp /tmp/合同审查清单_LIVE.xlsx nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
|
||||
docker exec nextcloud-nextcloud-1 chown www-data:www-data /var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx
|
||||
docker exec -u www-data nextcloud-nextcloud-1 php occ files:scan --path="doro/files/Doro合同审查任务/合同审查清单.xlsx"
|
||||
|
||||
# 上传后验证(write-read-verify,防止覆盖事故)
|
||||
rm -f /tmp/合同审查清单_VERIFY.xlsx
|
||||
docker cp nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx /tmp/合同审查清单_VERIFY.xlsx
|
||||
python3 -c "import openpyxl; ws=openpyxl.load_workbook('/tmp/合同审查清单_VERIFY.xlsx').active; print(f'上传后验证: {ws.max_row}行, 最后seq={ws.cell(ws.max_row,1).value}')"
|
||||
|
||||
# 清OnlyOffice缓存
|
||||
docker exec nextcloud-onlyoffice-1 bash -c 'rm -rf /var/lib/onlyoffice/documentserver/App_Data/cache/files/data/*'
|
||||
docker restart nextcloud-onlyoffice-1
|
||||
```
|
||||
|
||||
### 3. 确认并汇报
|
||||
|
||||
向Doro确认已标记completed,告知清单已更新到第几条。
|
||||
|
||||
### 批量pass后的登记完整性审计(2026-06-15教训)
|
||||
|
||||
⚠️ **Doro说\"都pass了\"或\"看下是否都走了pass流程\"时,必须把每一份pass的合同逐一对照 tracker JSON 和 xlsx 两个存储,找出缺漏——不能假设\"workflow跑完了/交付了\"就等于\"登记完整\"。**
|
||||
|
||||
2026-06-15教训:6份合同队列全部跑完并交付,Doro说6份都pass。实际核对发现只有2份(端午节、医疗急救招聘)走了完整pass流程,另外4份(金泽蛋糕、赵巷消防、练塘健康积分、徐泾维保)tracker和xlsx**全都没登记**。Doro主动提醒\"excel登记是不够的\"。原因:交付由workflow的final_review完成,但pass流程(写tracker+xlsx)是独立的人工步骤,前面几份漏做了。
|
||||
|
||||
**审计脚本逻辑**(用openpyxl+json对照):
|
||||
1. 列出本批次所有pass合同的 `delivered_filename`(从任务交付目录或Doro的pass消息提取)
|
||||
2. 读 xlsx 第E列(文件名列)建一个 set
|
||||
3. 读 tracker.json 的 `contracts[].delivered_filename` 建一个 set
|
||||
4. 逐份合同检查:`in xlsx?` + `in tracker?`,打印缺失矩阵
|
||||
5. 对缺失的合同,**完整补走步骤0(待审查核实+正文读甲方+查重)→ 步骤1(tracker)→ 步骤2(xlsx)**,不能只补一个存储
|
||||
6. 补完后重新跑一遍审计脚本,确认6份全部 ✅ xlsx + ✅ tracker
|
||||
|
||||
**xlsx与tracker必须成对存在**:xlsx是给人看的登记,tracker是给cleanup cron用的。只有xlsx没tracker→文件永远不被自动清理;只有tracker没xlsx→Doro的清单缺条目。审计时两个都要查。
|
||||
|
||||
**openpyxl写xlsx的权限陷阱**:从Nextcloud用`sudo cp`下载的xlsx属主是root,openpyxl保存时报`PermissionError`。先`sudo chown maggie:maggie`改属主到当前用户的可写副本再操作。
|
||||
|
||||
## 触发模式
|
||||
|
||||
Doro会**回复引用**某条交付通知消息并说"pass"。可能是单份也可能批量("四份都pass")。
|
||||
- 引用的消息中包含交付文件名,从中提取合同信息
|
||||
- 批量pass时逐份处理,每份都写tracker+xlsx
|
||||
|
||||
### 审查意见文件的处理
|
||||
Doro可能把审查意见文件(如"朱家角审查意见-XXX.docx"、"采购协议-审查意见.docx")也列入pass清单。这些是合同的伴随交付物,**不需要单独的tracker条目和xlsx行**——它们跟随主合同的tracker记录:
|
||||
- 主合同"【修】XXX.docx" → 写tracker + 写xlsx
|
||||
- 伴随审查意见文件 → 不写tracker,不写xlsx
|
||||
- 清理时:审查意见文件跟随主合同一起清理
|
||||
|
||||
### Classifier甲方误判的pass处理(2026-06-12教训)
|
||||
当workflow classifier误判了顾问单位(如把"卫健事业发展中心"误判为"朱家角"),pass流程写tracker/xlsx时必须用**合同正文中的真实甲方名称**,不能用classifier的`our_party_name`。
|
||||
- 步骤0核对时用python-docx读取合同正文前30段,找甲方全称
|
||||
- tracker的`party`字段和xlsx的"顾问单位"列写真实甲方
|
||||
|
||||
## 清理cron配套信息
|
||||
- Cron job name: `contract-cleanup`,job_id: `4636467b715d`
|
||||
- 脚本路径: `~/.hermes/scripts/contract-cleanup.py`
|
||||
- 调度: 每小时,no_agent静默模式
|
||||
- 逻辑: 读tracker → 找completed + xlsx_updated_at超24h + cleaned=false → docker exec删Nextcloud待审查/和任务交付/中的文件 → 标记cleaned=true → files:scan
|
||||
- 安全: 只删tracker中精确记录的文件名,不通配。xlsx在上一级目录碰不到。JSON损坏时静默不操作。原子写入(tempfile+rename)
|
||||
- **.doc→.docx转换残留(2026-06-11发现+修复)**:原始文件为`.doc`时,workflow会用libreoffice转换为`.docx`副本放在待审查目录。cleanup脚本已有`converted_filename`字段的读取逻辑(第113-116行),但旧workflow跑的合同tracker中缺少此字段导致转换文件残留。**治本修复**(2026-06-11已实施):在review-contract.yaml的classifier procedure中添加`converted_filename`记录步骤,从classifier→reviewer→editor→deliverer→final_review全链路传递该字段。pass流程写tracker时从workflow输出中提取。cleanup脚本自动生效。
|
||||
- **手动批量清理(Doro授权模式)**:Doro可能要求按日期批量清除旧文件(不走tracker逻辑),此时按修改时间(stat %Y)过滤,在Nextcloud容器内执行rm,完成后files:scan+清OO缓存。2026-06-11执行过一次:保留6月10日及之后的文件,删除之前的全部(待审查11个+任务交付179个)。
|
||||
|
||||
## 已知坑
|
||||
|
||||
- **时间窗不能先拍脑袋,必须先用工具确认当天与"上周五"的北京时间边界(2026-07-13 再犯后补强)**:用户要求"查看北京时间上周五及今天,邱律师(查sender id)发了多少合同"时,不能直接按自己理解硬算时间窗,更不能在没交叉验证前就下"周五=0份"结论。必须至少两步核对:①先用 `TZ='Asia/Shanghai' date` 确认当前北京时间与星期;②把北京时间窗口换算成 UTC 后,再同时查 `.meta` 和 `gateway.log`。**只要 gateway.log 已出现 QiuTing 的空消息(msg='')文件记录,就不能再说该时段 0 份。**
|
||||
|
||||
- **查"邱律师发了多少合同"必须双源交叉,不可只靠 cache meta(2026-07-13 教训)**:仅查 `~/.hermes/cache/documents/*.meta` 可能漏掉周五文件或误判为 0。标准做法:
|
||||
1. `.meta`:按 `sender_id == QiuTing` + 北京时间窗口换算后的 UTC 区间统计
|
||||
2. `gateway.log`:按同一时间窗 grep `platform=wecom user=QiuTing chat=QiuTing msg=''`,这是文件消息的一手证据
|
||||
3. tracker + xlsx:核对这些文件是否都 workflow 完成、是否都 pass、是否都登记 excel
|
||||
4. **没有把两源对齐前,不得下"都完成了/没有遗漏"结论**
|
||||
|
||||
- **被 Doro 说"胡说八道,重新查,查明情况再说"时,表示你刚才的结论缺乏证据链**:此时必须回退到原始数据源重查,不得只改措辞继续硬答。正确做法:先承认上一条没查明,再用不同数据源(如从 meta 转到 gateway.log)交叉验证。
|
||||
- **审查workflow交付物时必须区分原文自带修订 vs WB修订(2026-07-13 Doro指示)**:Doro要求"对照原文件,WB-1如果是原文自带修订,保持不动。你只需要看workflow是不是准确遵守了workflow的规则,包括命名规则"。具体方法:①用zipfile读原文件(待审查目录)的revision authors,确认哪些author是原文已有的 ②读交付件,按author过滤只看WB的修订 ③逐条核对WB修订是否符合审查规则 ④原文自带的修订(其他author)不评价、不报告为问题。**常见误判**:把原文WB-1的修订当成workflow的WB修订来审查——必须先确认author再下结论。
|
||||
|
||||
- **核查汇报必须先用工具验证再说(2026-07-01+07-02 升级为顶部红线)**:详见本skill顶部「红线:验证指令 ≠ 回忆指令」章节 + `references/audit-methodology.md`。2026-07-02 同一轮对话中此规则被违反4次,导致Doro信任破裂("到了无法信任你的程度")。不再重复列举——执行时加载顶部红线即可。
|
||||
|
||||
- **"确定吗?再仔细查实"(2026-07-02 追加)**:Doro说"确定吗""再仔细认真查实"时,意思是**上一条回复的结论不够可信/不够深入**。正确做法:不要只是重复上一轮的输出,而是用不同角度/更深层的工具去交叉验证。具体:①查session_search看是否有其他session做过相同操作 ②查uwf step list确认每个thread走到了哪一步 ③确认通知确实被发出(session中有_send_wecom的tool call记录)而非假设。核心原则:**每一条"确认X发生了"的结论,都必须有对应的工具调用输出作为证据。**
|
||||
|
||||
- **文件"消失"的排查思路(2026-07-01 教训)**:Doro发现待审查/交付目录文件不在时,不要急着说"被误删"。先排查:①tracker 的 `cleaned` 字段是否已为true(cron清理了)→ 不可能不到24h;②auto_notify 是否正常工作(文件可能从来没上传到待审查目录,workflow直接从cache路径读文件完成审查,但Doro在Nextcloud看不到);③是否是手动操作中 `docker exec rm` 删了。根因很可能是"从来没上传到待审查"而不是"上传后被删了"。
|
||||
|
||||
- **交付件被 Doro 手动编辑后大小/内容会变 —— pass 时不覆盖、只登记(2026-06-23 实证)**:Doro 收到交付件后可能自己在 OnlyOffice 里编辑(删掉部分批注内容、改条款等),导致 Nextcloud 上的交付文件大小/字节/md5 与 workflow 原始交付件不同;且 OnlyOffice 后台处理期间文件**大小会持续变化**,docker cp 出来用 pymupdf/python-docx 读可能是 0 页/0 批注的中间态。**看到交付件与你核验时不一致,先确认是不是 Doro 自己改的,绝不要当成文件损坏去"修复"或用本地完整版覆盖。** Doro 说 pass 后:以 Doro 改后的版本为准,只写 tracker+xlsx 登记,**不触碰交付目录里的文件**。2026-06-23 教训:交付 PDF 从 20MB 变 3.5MB 且持续变化,我误判损坏、准备用本地版覆盖,Doro 说"是我删除了一些内容,你直接做 pass 就可以"。
|
||||
|
||||
- **PDF 扫描件合同走批注模式,不 OCR 转 docx(2026-06-23 Doro 明确)**:收到扫描件 PDF 合同(无文字层)时,**不要纠结"OCR 转 docx 才能修订"**——workflow 对 PDF 用**批注模式**审查(高亮 Highlight + 批注气泡 Text 配对,author=WB),原文零污染,这是正常流程(seq=201 朱家角.pdf、seq=207 阳澄湖团建.pdf 都是 PDF 批注交付)。Doro 原话"PDF 的修改使用批注,这是 workflow 的正常流程"。PDF 交付件**核验/pass 时**:核批注数/author/高亮锚定位置/原文页数不变,**不套用** INS/DEL/字号/numPr 那套 docx 专项检查(final_review 对 PDF 会自动判定这些不适用)。
|
||||
|
||||
- **交付文件位置:根目录 `任务交付/` 是正确的(2026-06-29 确认)**:workflow deliverer 明确写着上传到 `Doro合同审查任务/任务交付/`(根目录)。不要把文件移到顾问单位子文件夹——那里不是交付目的地。pass 流程步骤0核对时,交付文件应该在根目录 `任务交付/` 中。如果 deliverer 同时上传到了子文件夹(如 final_review 越权重复上传),子文件夹里的是多余的,应清理。
|
||||
|
||||
- **final_review 越权重复上传(2026-06-29 教训)**:workflow 中上传是 deliverer 的职责,final_review 只负责质量检查和通知 Doro。但 LLM 执行 final_review 时可能越权上传文件(到子文件夹而非根目录 `任务交付/`),且使用完全不同的命名格式。**判别**:`stat` 比较文件大小,同内容两份不同名=重复。**处理**:保留 deliverer 上传的(【修】/【审】+ 原始文件名),删除 final_review 额外上传的。**根因**:review-contract.yaml 的 final_review procedure 需修改,明确禁止上传动作(待 WeiWei 实施)。
|
||||
|
||||
- **审查意见标题空壳(2026-06-29 教训)**:workflow editor 生成的审查意见文档标题是"关于《合同》的审查意见"——"《合同》"是占位符,没有替换为实际合同名称。classifier 已正确提取了 `contract_title`,但 editor 生成审查意见时没有填入。**pass 流程必须检查审查意见标题**:如果标题是"关于《合同》的审查意见",需要手动修正为"关于《{合同实际名称}》的审查意见"。修正方法:用 zipfile + lxml 读 document.xml,找到标题段落的所有 `w:t` 节点,第一个设为完整标题,其余清空。
|
||||
|
||||
- **【审】审查意见文件缺失(2026-06-30 发现)**:deliverer 有时只上传【修】修订版而没有上传【审】审查意见文件。**排查**:`sudo find .../任务交付/ -name "【审】*"` 检查是否有对应的审查意见。**处理**:如果缺失,需要手动生成或报告Doro确认是否需要补做。审查意见是companion文件,不影响主合同的pass流程(不需要tracker/xlsx条目),但Doro可能期望看到。
|
||||
- **Workflow常见格式缺陷清单(2026-07-13 审计总结,详见 `references/pre-pass-audit-checklist-20260713.md`)**:
|
||||
1. WB INS rFonts多余属性(hAnsi/cs/hint)——每份都有,每次审计必查必清
|
||||
2. 新增条款标题pStyle错误(用了Style15而非Heading4等)
|
||||
3. 新增标题带空格("第七条 转包"应为"第七条转包")
|
||||
4. 子编号未顺延(只改了章编号,没改内部X.Y编号)
|
||||
5. 赔偿上限遗漏(双向条款的20%上限未删)
|
||||
6. 保密存续重复插入(原文已有同义表述)
|
||||
7. 脚注未用修订格式
|
||||
|
||||
- **同模板合同修订一致性(2026-07-03 Doro确认:手动调整)**:不同顾问单位提交审查的合同若为相同模板,修订需保持一致(规则9已有)。但不改workflow自动化逻辑——Doro指出不一致时手动调整即可,避免矫枉过正。不需要template_type标签或修订库。
|
||||
|
||||
- **非合同文件进入workflow审查(2026-07-01 香花桥招标需求实证)**:classifier识别出"招标需求文件"但仍走了完整review+edit流程并交付了修订版。Doro判定此类文件无需审查,直接通知邱律师。**已修auto_notify加L1文件名预筛**(关键词:招标需求/技术方案/报价单等)。但classifier兜底层未修——仍然会把非合同当合同审。根因:workflow的routing graph没有从classifier直接到$END的non-contract路径。**pass前排查**:如果交付物对应的原始文件明显不是合同(招标需求/投标文件/会议纪要等),直接删除交付物并通知邱律师,不做pass。
|
||||
|
||||
- **审查意见文件是错误交付物(2026-07-01 发现)**:workflow 有时会生成不该有的审查意见文件(如合同已被其他thread审查过、或classifier误判导致多生成)。**pass前排查**:检查任务交付目录中是否有不属于本次审查的【审】文件。如果是错误交付物(如旧版残留、重复审查产物),直接删除不pass。判断标准:审查意见的标题和内容是否对应本次审查的合同,标题是否是"关于《合同》的审查意见"(占位符)。
|
||||
- **cleanup脚本不处理tracker顶层"ready"条目 + 不清理本地目录(2026-07-03 查明根因)**:`contract-cleanup.py` 只遍历 `tracker["contracts"]` 数组中 `status=completed` + `cleaned=False` 的条目。但以下两类文件不会被清理:
|
||||
1. **Tracker顶层字典键**(status=ready的条目):这些是workflow跑完但尚未被Doro pass的合同,存储在tracker的顶层键而非contracts数组里。cleanup脚本看不到它们。Doro pass后,pass流程会把它们从顶层搬到contracts数组并标记completed——此时cleanup才能处理。**如果pass流程有bug没搬进contracts数组,文件永远不会被清理。**
|
||||
2. **本地 `~/.hermes/shared/Doro合同审查任务/待审查/` 目录**:cleanup脚本只删Nextcloud容器内的文件(`docker exec rm`),本地shared目录的副本无人管理。这些是auto_notify上传NC后留下的本地副本——NC侧被cleanup清了,本地的永远残留。
|
||||
- **诊断方法**:`ls ~/.hermes/shared/Doro合同审查任务/待审查/` 如果有文件,且对应的tracker条目已经completed/ready,就是此bug。
|
||||
- **临时清理**:确认文件在tracker中已completed后,手动 `rm` 本地副本即可。
|
||||
- **根治**:cleanup脚本需要增加两段逻辑:①遍历tracker顶层ready条目(Doro已pass但pass流程漏搬的)②清理本地shared/待审查/中已completed的文件。
|
||||
|
||||
- **沟通时间统一使用北京时间(2026-07-03 Doro多次纠正)**:所有对外沟通(包括向Doro汇报workflow状态、文件接收时间等)一律使用北京时间。服务器UTC时间仅在内部日志/脚本中使用,不对用户展示。
|
||||
|
||||
- **Doro说"查清楚"/"你去查原因"——必须用工具验证后再给结论**:Doro要求查证时,不能凭记忆或context summary回答。必须实际调用工具(session_search、terminal读文件/目录、tracker等)取得证据后再汇报。2026-07-13教训:被要求"查清楚我哪些说了pass"时,需要在本session对话记录中找到Doro的原话,不能凭印象列清单。
|
||||
|
||||
- **脚注"法律顾问修订版"必须用修订格式(2026-07-13 Doro纠正)**:添加页脚文字时,必须包裹在`w:ins`元素中(author=WB),不能作为普通文本直接写入。OnlyOffice/Word中显示为带修订标记的新增内容。实现方式:用python-docx添加footer文本后,再用zipfile+lxml找到footer XML中的对应run,包裹进`w:ins`元素。
|
||||
|
||||
- **交付件字体大小不一致(2026-07-01 Doro纠正,根因细化)**:有两种常见成因:
|
||||
1. **段内混用**:同一段落内不同run使用不同字号(如sz=21和sz=24混用)。修复:找dominant size统一。
|
||||
2. **INS-only新段落缺sz**(更隐蔽):`add_clause`插入的全新段落,原文docDefaults无sz定义或sz≠邻居段落的显式sz。新段落INS run不写sz→继承docDefaults→与显式sz=24的邻居段落不一致。**诊断**:找所有INS-only段落(整段只有w:ins),检查其run的rPr是否有sz,再对比前后段落的sz。缺sz且邻居有显式sz=补上。
|
||||
3. **numPr叠加**:加了手动编号但没去掉原自动编号→显示"1. 第一条"。修法见contract-editor skill的"strip numPr"规则。
|
||||
|
||||
- **deliverer 重复上传(同内容两份不同名)(2026-06-29 教训)**:deliverer 可能对同一合同上传两次,文件大小完全一致说明内容相同。**判别**:`stat` 比较文件大小。**处理**:保留命名规范的那份(【修】/【审】+ 原始文件名),删除另一份。
|
||||
|
||||
- **原文件自带【修】前缀时命名错误(2026-07-13 璞石合同实证)**:邱律师发来的文件本身已有【修】前缀(如`【修】练塘-硬件购销合同-璞石医疗2026.7.13.wps`),workflow按规则应生成`【修】【修】练塘-...docx`(原文件名一字不动,前面再加【修】),但实际只生成了`【修】练塘-...docx`(吞掉了原文的【修】前缀)。**审查时判别**:原文件名含【修】时,交付文件名应出现两个【修】。只有一个=workflow命名错误。**根因**:workflow的命名逻辑strip了原文件名中已有的【修】前缀后再加自己的。
|
||||
|
||||
- **Cache hash前缀混入交付文件名(2026-06-30 实证)**:deliverer 有时把 cache 文件名直接当交付文件名上传,产生类似 `【修】doc_0ad4ee63bff5_印刷品制作合同2026.6(1).docx` 的文件名。**判别**:文件名含 `doc_[a-f0-9]{12}_` 模式。**处理**:删除带 hash 前缀的那份,保留正确命名的(`【修】印刷品制作合同2026.6(1).docx`)。**根因**:deliverer 从 cache 复制文件时没有去掉 cache hash 前缀重命名。
|
||||
|
||||
- **companion 文件(审查意见/流程单)不会被 cleanup 自动清理 → 永久残留孤儿(2026-06-21/29 实证)**:`contract-cleanup.py` 只删 tracker 里 `original_filename`/`converted_filename`/`delivered_filename` 三个字段精确匹配的文件。companion 从不进 tracker → 主合同被清理后 companion 永远残留。**排查**:运行 `references/cleanup-audit.py` 审计脚本,列出所有 not-in-tracker 的文件。**应急清理(Doro 授权后)**:
|
||||
```bash
|
||||
# 必须搜索所有任务交付路径(根目录+顾问单位子目录都可能有)
|
||||
sudo docker exec nextcloud-nextcloud-1 find /var/www/html/data/doro/files/Doro合同审查任务/ -path "*任务交付*" -name "*审*意见*"
|
||||
# 确认后逐个删除
|
||||
sudo docker exec nextcloud-nextcloud-1 rm -f "<path1>" "<path2>" ...
|
||||
# 扫描+清缓存
|
||||
sudo docker exec -u www-data nextcloud-nextcloud-1 php occ files:scan doro --path="/doro/files/Doro合同审查任务/"
|
||||
docker exec nextcloud-onlyoffice-1 bash -c 'rm -rf /var/lib/onlyoffice/documentserver/App_Data/cache/files/data/*'
|
||||
docker restart nextcloud-onlyoffice-1
|
||||
```
|
||||
⚠️ Companion 可能同时存在于根目录 `任务交付/` 和顾问单位子目录(如 `朱家角镇社区卫生服务中心/任务交付/`)——find 命令用 `-path "*任务交付*"` 通配搜索确保不遗漏(2026-07-06 实证:肃言/恭兴审查意见各在两个位置共4份)。
|
||||
**治本方案见 `references/companion-cleanup-proposal.md`**,需 WeiWei 决策后实施。
|
||||
|
||||
- **Companion 判定不能靠想当然命名(2026-07-13 白鹤劳务派遣协议教训)**:Doro说“交付文件夹里的合同及companion全部做pass”时,**先查清交付文件夹里到底有哪些相关文件,再决定有没有 companion**。不要因为很多合同通常会带审查意见,就默认本案也有;也不要只看主文件名一次就下结论。正确顺序:①先在 `任务交付/` 中做**宽匹配扫描**(合同名关键词、当事人名关键词、业务关键词都要扫)②再把整个 `Doro合同审查任务/` 目录补扫一遍,确认没有藏在别的子目录里的 companion ③最后再对 tracker+xlsx 核对是否已有 completed 记录。**如果实扫结果只有主合同一份,就要明确汇报“合同1份、companion 0份”,而不是笼统说“都pass了”。**
|
||||
- **Companion 命名规则(2026-07-01 Doro确认)**:交付物=`【修】{原文件名}`,companion=`【审】{原文件名去扩展名} 审查意见.docx`。pass skill 扫描时按此规则拼确定性文件名+`nc_file_exists()`查询,查到就写入 tracker 的 `companion_files` 字段。cleanup 脚本读 `companion_files` 一并删除。两头(pass skill + cleanup script)必须同步改才有效果。workflow editor/deliverer 生成审查意见时也必须强制用此命名规则。
|
||||
|
||||
- **手动启动workflow前必须查实auto_notify是否已处理(2026-07-03 教训)**:发现cache中有新文件时,不要直接`uwf thread start`。必须先:①查`/tmp/auto_notify_new_file.log`确认auto_notify是否已检测到并启动了workflow ②`uwf thread list | grep running\|idle`看是否已有对应thread在跑。auto_notify正常工作时会自动上传NC+启动queue runner+排队执行,手动启动会导致重复审查。2026-07-03实证:邱律师发了文件,auto_notify 30秒内就启动了workflow(thread 06FJDADW),我没查就又手动启动了第三个重复thread,被Doro纠正。
|
||||
|
||||
- **不判断文件是否相同/重复(2026-07-03 Doro纠正)**:邱律师发了几次、发什么文件,不是我该判断的。不要说"同上""同名文件""是重复的"——即使md5一致也不做这个判断。auto_notify和workflow自己处理,我不干涉。待审查目录里有几份就是几份,workflow跑几个就是几个。
|
||||
|
||||
- **沟通必须使用北京时间(多次纠正)**:所有与Doro/Maggie的时间沟通一律用北京时间,包括表格、汇报、日志引用。服务器是UTC,必须+8转换后再输出。不写UTC时间。
|
||||
|
||||
- **"删掉批注"指令——先验证再操作(2026-07-08)**:Doro可能要求"删掉批注,提交后做pass"。操作前必须先用zipfile检查comments.xml是否存在+commentRangeStart数量。如果文件无批注(comments.xml不存在且无comment引用元素),直接汇报"文件无批注"然后继续后续操作(提交/pass),不需要强行"删除"不存在的东西。
|
||||
|
||||
- **邱律师同名文件不自行判断是否重复(2026-07-03+07-09 加强)**:邱律师发的多次同名文件,不要自己判断"重复发送"。同名文件可能:①不同顾问单位②修改版(字节不同=内容变了)③同一份发了两次。**只要字节大小不同,就必须视为不同合同独立审查**。2026-07-09教训:字节差407的两份合同有3处实质性条款差异(项目名称、支付方式、合同期限),不是"重复发送"。正确做法:检查字节是否相同→不同→独立审查或私信邱律师确认。
|
||||
|
||||
- **交付件大小/内容突变,先确认是不是用户手动编辑了,别当损坏(2026-06-22 实证)**:pass 前看到 Nextcloud 上的交付 docx/pdf 大小骤变(如 20MB→3.5MB 还在持续变、md5 对不上本地核验版),第一反应**别**判为"文件损坏/被进程写坏"。先确认是不是 Doro 自己在 OnlyOffice 里改了(删批注、调内容)。确认是用户编辑后:**不覆盖、不动交付目录的文件,以用户改后版本为准**,直接走 pass 登记(tracker+xlsx)。**铁律:pass 登记只动台账(tracker/xlsx),交付目录里的成品文件除非用户明确要求重做,否则只读不写。**
|
||||
|
||||
- **`.doc` 原始文件残留(2026-06-29 实证)**:tracker 的 `original_filename` 有时记录了 `.docx`(转换后文件名)而非 `.doc`(真正的原始文件),cleanup 按 `.docx` 去删找不到 `.doc` → 残留。**排查**:cleanup-audit.py 会标记为 NOT_IN_TRACKER。**预防**:pass 写 tracker 时确保 `original_filename` 是邱律师发来的真实文件名(含扩展名),不要写转换后的文件名。
|
||||
|
||||
- **编号"错误"误判——markup视图双编号是正常现象(2026-06-16教训)**:OnlyOffice/Word修订视图(markup)下,自动编号列表项被删除(含他人如屠佳青的删除)或新增条款插入后,后续项会显示"(3)(2)""(4)(3)"这类双编号——前一个是删除/插入前的旧编号,后一个是接受修订后的新编号。这是track changes的正常渲染,**不是编号错误**。Doro/Maggie说某份合同"编号修改错误"时,先别急着改:
|
||||
1. 用execute_code生成"接受所有修订后"的版本:删除所有`w:del`元素 + 删除带段落标记删除(`pPr/rPr/del`)的整段 + 解包所有`w:ins`(把ins的子元素提到父级再删ins壳)
|
||||
2. 用OnlyOffice x2t渲染accept版PDF,pdftotext看**最终编号**是否连续正确
|
||||
3. 若accept后编号正确 → workflow没改错,markup双编号是正常的,**恢复workflow原版即可,不要擅改**
|
||||
4. 若要改,必须先问清Doro/Maggie指的是accept后哪一处,他人(屠佳青等)的修订按铁律不能动
|
||||
- 2026-06-16教训:把端午节合同(转包6误改成7)、医疗急救招聘合同的正常编号重排误判为错误并擅自改动,被Maggie两次纠正"workflow改得都没错,恢复成workflow修订版本"。x2t accept渲染命令见本skill的"用OnlyOffice渲染自查"或contract-editor skill。
|
||||
|
||||
- **但确有真编号重复时要修(2026-06-15端午节实测,与上条不冲突)**:自动编号列表项(numbering.xml中`numId`对应的`abstractNum`有`start=N`、`lvlText=%1、`)渲染出的编号,与后续**手动键入数字**的tracked插入条款(`w:ins`里`w:t`文本直接以"6、""7、"开头)会真冲突。本案"售后服务"是auto-number(numId=3, start=6 → 渲染为6),紧跟的WB插入条款手动写了"6、转包限制" → 出现两个6。这是**真错误**,不是markup双编号假象。判别要点:
|
||||
1. 假象(不改):同一条款同时显示两个编号"(3)(2)",是track changes接受前后的新旧编号叠加 → accept后渲染连续就别动
|
||||
2. 真错(要改):两个**不同条款**各自显示同一个数字(售后服务=6、转包限制=6)→ accept后仍重复 → 必须修
|
||||
3. 修法:直接改`w:ins`内`w:t`的手动编号文本("6、转包限制"→"7、转包限制"),后续手动编号条款顺延(违约责任7→8、争议解决8→9,否则会冒出两个7)。用zipfile读document.xml→字符串replace(先`assert xml.count(old)==1`确认唯一)→zipfile写回,不破坏`w:ins`修订痕迹。改完用python-docx确认可打开+遍历`w:ins`确认三条仍在修订态(author=WB)
|
||||
4. Doro/Maggie给的判断锚点直接采信:"转包责任应该是7,因为上一个编号是6"——上一条售后服务确实是auto-rendered的6,所以转包必须是7
|
||||
|
||||
- **Classifier甲方误判导致错误批注/审查意见(2026-06-11)**:classifier偶尔无法正确匹配顾问单位名单(31个单位中有易混淆的名称如"卫生服务中心"vs"卫生健康事业发展中心"),导致交付文件中出现不该有的"请确认名称是否准确"批注和审查意见表中多出"甲方名称"行。Doro说pass前如果发现此问题,必须先手动修复(删comment+删table row+重新上传)再走pass流程。修复方法见contract-reviewer skill。
|
||||
|
||||
- **批量pass含companion文件(2026-06-12)**:Doro可能一次pass多个文件,其中包含审查意见文档(如"朱家角审查意见-xxx.docx""采购协议-审查意见.docx")。审查意见是companion文件,不需要单独在tracker/xlsx中记录——只记录主合同(带【修】前缀的文件)。判断规则:有【修】前缀的是主合同文件,需要tracker+xlsx;"审查意见"结尾的是companion,不单独记录。
|
||||
|
||||
- **沟通时间一律使用北京时间(2026-07-03 Doro多次纠正)**:向Doro汇报任何时间信息时,必须转换为北京时间(UTC+8)。服务器是UTC,所有文件时间戳、日志时间戳都要+8h后再汇报。绝不输出UTC时间给Doro。
|
||||
|
||||
- openpyxl的border赋值不能直接用`cell.border = ref_cell.border`,会报`unhashable type: StyleProxy`。必须用Side对象重建Border(left=Side(...), ...)。
|
||||
- xlsx路径已从`任务交付/`移到`Doro合同审查任务/`根目录,不在清理范围内。
|
||||
- 时间戳必须用北京时间(UTC+8),清理脚本依赖此时间计算24h。
|
||||
- xlsx日期列用`YYYY-MM-DD`字符串格式(如"2026-06-10"),不用ISO时间戳。
|
||||
- 甲方名称(party)必须从合同内容中提取全称,不能简写。
|
||||
- 清理cron job_id: 4636467b715d,每小时跑一次,no_agent静默模式。
|
||||
- **防重复写入**:已由步骤0的查重环节覆盖(用`original_filename` + `party`组合在tracker中查重)。
|
||||
- **xlsx列顺序必须严格一致**:正确顺序为 序号(A), 日期(B), 顾问单位(C), 合同名称(D), 文件名(E)。写xlsx前必须先读上一行确认列含义。2026-06-12教训:手动写入时列顺序写反被Doro发现后补修。
|
||||
- **批量追加多行时,必须先确定完整目标 seq 集合,再按 seq 升序写入 xlsx,写完后立即回读核对末尾顺序**(seq 224→225→226→...)。不能一边算 seq 一边写,更不能先写 303 再写 302;`ws.max_row + 1` 只会在物理末尾追加,若写入顺序错了,就会出现 xlsx 中 303 排在 302 前面的台账乱序。**硬性补丁(2026-07-14)**:凡一次 pass 涉及 2 份及以上合同,先在内存中生成 `[{seq, 日期, 顾问单位, 合同名称, 文件名}]` 全部待写行,按 `seq` 升序排序后再统一 append;保存上传后,必须回读最后 N 行,逐行确认 A 列 seq 单调递增且文件名与本批次一一对应。若发现顺序错误,立即重拉 LIVE xlsx 重写,不得带错交付。
|
||||
- **按“北京时间今天/昨天/上周五”统计邱律师发了多少合同时,先定时间窗,再查 meta,再对照审查状态(2026-07-14 再次实证)**:这类问题不能直接凭印象回答“几份、都做完了吗”。标准顺序必须是:①先用 `TZ='Asia/Shanghai' date` 锚定今天的北京时间窗口;②查 `~/.hermes/cache/documents/*.meta` 中 `sender_id=QiuTing` 且落在该窗口内的文件,得到**今天实际收到的文件清单**;③再去 tracker 中逐份核对这些文件对应的 `status/seq/delivered_filename/xlsx_updated_at`;④最后才汇总结论。**重点**:meta 统计的是“今天收到几份”,tracker 统计的是“这些文件审查到了哪一步”,两者不能互相替代。
|
||||
- **同名不同来源/不同顾问单位的合同,统计和汇报时必须拆开,不得混成一句“劳务派遣协议已完成”(2026-07-14)**:如果今天收到的是 `劳务派遣协议.doc`,但 tracker/任务交付里同时还存在 `【修】白鹤--劳务派遣协议.docx` 等近名文件,汇报时必须明确区分“今天收到的这份”与“其他同类/同模板/同主题文件”。不能把别的合同的 completed 记录拿来替代今天这份的审查状态。标准动作:按 `original_filename` 精确对 tracker 命中,再补充说明是否另有近名已完成文件。
|
||||
- **被用户要求“再核查一次”时,必须升级为四路交叉核查,不得只复述上一轮结论(2026-07-14)**:对“是不是同名合同但顾问单位不同”“今天收到的到底是哪几份”这类追问,重查时至少同时核:①Nextcloud 全目录文件扫描(不只根目录任务交付)②tracker 命中记录 ③xlsx 命中记录 ④cache meta/原件名称与正文中的甲方或项目编号。回复必须把四路结果并列写出,再下结论。不能只说“我刚才已经看过了,结论不变”。
|
||||
- **自动 cleanup 依赖 tracker 文件名与 Nextcloud 实际文件名精确一致(2026-07-14)**:若发现“历史台账文件名”和“当前 NC 实际文件名”不一致,即使该条记录已经 completed,仍要同步修正 `tracker.original_filename` / `tracker.delivered_filename`,必要时同步修正 xlsx 第 E 列文件名,确保三者一致:①tracker ②xlsx ③NC 实际文件。否则 cleanup 按精确文件名匹配时会漏删或误判。修正后必须再次核对目标待审查文件与任务交付文件在 NC 中确实存在。
|
||||
- **用户只要结果,不要过程解释时,汇报必须收敛成一句结论(2026-07-14 Doro纠正)**:当用户明确说“我就要一个结果”时,禁止继续附加过程说明、背景铺垫、风险解释或“补一句严谨的”。此类场景只回复最终结论,例如“能。”、“已完成。”、“不能,需要X条件。”;如确需补充,等用户追问后再展开。
|
||||
- **统计“今天收到的合同”时,先答收到清单,再答审查状态,别把两件事揉成一句笼统结论(2026-07-14)**:推荐输出结构固定为:①今天收到几份;②逐份列出收到时间;③逐份列出审查状态(未启动 / 审查中 / delivered / completed);④如有同主题近名文件,单列“另有近名已完成文件,不等于今天这份”。这样可以避免把“收到数量”和“已完成数量”说串。
|
||||
- **乙方空白时的处理**:合同乙方名称空白→不问Doro"该问谁",直接查文件来源(cache/documents时间戳+session_search找发送人),确定是邱律师发的就直接私信邱律师询问。Doro可能说"邱律师回复了就直接补上 我不管了"——意思是邱律师回复后自主完成tracker+xlsx更新,不再找Doro确认。
|
||||
- **私信邱律师的send_message路由问题**(2026-06-12):`send_message(target='wecom:QiuTing')`会路由到home channel而非QiuTing私信。必须用`_send_wecom(extra, 'QiuTing', msg)`直接发送才能到私信。
|
||||
- **私信任意企微用户的首选方法 = `wecom_dm.py` 脚本(2026-06-21 再犯后确立)**:`send_message(target='wecom:<user>')` 对企微用户名会**静默回退到 home channel**(JiaQian),既发不到目标、又可能违反信息隔离(把 Doro 团队内容发进 Maggie 渠道)。`_send_wecom(extra, ...)` 需要 gateway 的 `extra` 上下文,在 terminal/execute_code 里拿不到。**最稳的通用方法**是独立脚本:`python3 ~/.hermes/scripts/wecom_dm.py --to <别名> --text "内容"`——自己开 WebSocket + `aibot_send_msg` + `chat_type=1` 发主动私信,永不串号、目标唯一确定。白名单别名:doro / jiaqian(贾茜Maggie) / qiuting(邱律师) / weiwei(技术支持) / shasha(苌莎莎) / yangayi(颜伽艺) / xiaonan。先 `wecom_dm.py --list` 核对别名→userid 再发。**铁律:私信非 home 的企微用户,一律走 `wecom_dm.py`,绝不用 `send_message(target='wecom:X')`。** 2026-06-21 教训:给魏玮发技术讨论用了 `send_message(target='wecom:WeiWei')`,静默落到 JiaQian home channel,既没到魏玮又把 Doro 团队的脚本细节泄露进 Maggie 渠道,改用 `wecom_dm.py --to WeiWei` 才真正送达。
|
||||
- **不干涉workflow铁律(2026-07-03 Doro纠正)**:发现邱律师发了新文件时,**禁止直接手动启动workflow**。必须先查 `/tmp/auto_notify_new_file.log` 和 `uwf thread list` 确认auto_notify是否已经处理。不判断文件是否重复("你也不要去判断是不是同一份文件")——让系统自己处理。2026-07-03教训:auto_notify已正常工作并启动了workflow,我没查就手动又启动了一个重复的。
|
||||
- **手动启动workflow前必须查实auto_notify是否已处理(2026-07-03 Doro纠正)**:发现邱律师发了新合同时,**第一步不是手动启动workflow**,而是:①查`/tmp/auto_notify_new_file.log`确认auto_notify是否已检测并处理 ②查`uwf thread list | grep running\|idle`确认是否已有对应thread在跑。只有确认auto_notify没处理(日志无记录+无相关thread)才手动启动。2026-07-03教训:auto_notify已正常触发并启动了workflow,我没查就又手动启动了一个重复的。**"收到即启动"的前提是"确认没有被自动处理"。**
|
||||
|
||||
- **不判断邱律师发的文件是否重复/相同(2026-07-03 Doro纠正,2026-07-08 再次验证)**:邱律师多次发送同名文件时,不要自行判断"是同一份发重了"。存在同文件名但不同版本、不同条款内容的可能性。2026-07-08实证:同日发两份"2026年华新镇公立中小学生健康体检服务合同.docx"(11:05和16:04),文件大小不同(27889 vs 27482 bytes),实际条款差异显著(项目内容、付款方式、合同起始日均不同)——完全是两份实质不同的合同版本。queue-runner因同名文件已在done/直接SKIP导致第二份没审查。**核查方法**:用python比对两文件文本差异(zipfile提取全文+difflib对比),有实质差异则为不同合同/不同版本,须独立审查。**报告时绝不说"重复发送"**——Doro问"如何判断是重复发送"即提醒不该自行判断。
|
||||
|
||||
- **恢复文件到workflow原始交付状态(2026-07-13 实证)**:Doro要求撤销手动修改、恢复到workflow交付版本时,三个来源按优先级尝试:①NC版本历史(`files_versions/`目录下的`.vXXXXXXXXXX`文件,时间戳最早的=workflow首次上传版本)②`/tmp/pass_check_*`文件(之前做pass检查时从NC拉取的快照,如果当时还没手动修改则=workflow版本)③重新跑workflow(最后手段)。恢复后必须`docker cp`回NC + `chown www-data` + `occ files:scan`。
|
||||
|
||||
- **Tracker重复条目(同一合同不同文件名)**:workflow跑多轮或auto_notify重复触发时,tracker可能出现同一合同的多条delivered记录(文件名带版本后缀如`_v0113.doc`vs不带的)。批量pass时需注意:只pass实际在任务交付目录中的那份(【修】前缀的),旧的重复delivered条目如果文件名与NC上的交付物不匹配,不影响pass流程但会残留在tracker中(cleanup会因找不到文件而跳过)。
|
||||
|
||||
- **手动启动前必须先验证auto_notify是否已处理(2026-07-03铁律)**:发现邱律师新文件后,**禁止**直接手动启动workflow。必须先:①查`/tmp/auto_notify_new_file.log`确认auto_notify是否已检测并处理该文件 ②`uwf thread list | grep running`确认是否已有workflow在跑。只有确认auto_notify确实没工作(进程死亡或模式B静默失效)才手动介入。2026-07-03教训:auto_notify已正常触发workflow,但我没查就手动又启动了一个重复的,被Doro纠正"不要去干涉workflow"。**邱律师发多次同名文件不自行判断是否相同**——让系统处理,同时私信邱律师确认。
|
||||
|
||||
- **交付通知丢失的诊断与补发(2026-07-09 实证)**:Doro说"有几份合同我没有收到交付通知"时,按以下流程排查:①检查tracker中status=delivered的记录 ②查gateway.log在对应delivered_at时间前后是否有846609/Timeout/WS closed错误 ③确认是WS断连导致fire-and-forget通知丢失 ④用`wecom_dm.py --to doro`补发。**根因是企微WS凌晨不稳定+通知无重试机制,详见 `references/notification-ws-failure-pattern-20260709.md`。** 临时止血=手动补发;根治=watchdog增加notification_sent检查和补发逻辑(方案待实施)。
|
||||
|
||||
- **通知静默丢失——workflow完成但Doro没收到通知(2026-07-09 实证)**:workflow全流程跑完(thread status=end, tracker=delivered),但Doro说没收到交付通知。根因:`final_review`步骤通过`_send_wecom`发通知时,企微WebSocket已断连(errcode 846609: aibot websocket not subscribed),发送静默失败,无重试机制。**症状**:Doro说"没收到通知" + tracker有delivered记录 + `grep '846609' ~/.hermes/logs/gateway.log`在final_review执行时间段有报错。**诊断**:①查tracker找delivered_at时间 ②查gateway.log该时间±5分钟有无846609/WebSocket error ③确认final_review步骤确实跑完了(`uwf step list <thread>`有final_review且thread status=end)。**补救**:用`python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "合同审查完成通知(补发):..."`手动补发。**预防**:目前无自动重试机制。如果发现当天有WebSocket中断记录,主动检查该时段内完成的所有workflow是否通知成功——以gateway.log中有对应的"Sending response...to doro"记录(不含后续846609 error)为准。
|
||||
|
||||
- **"delivered但Doro没收到通知"诊断(2026-07-09 实证)**:Doro说"有几份没收到交付通知"时,根因通常是final_review发通知时WebSocket已断(846609错误)。Workflow全程跑完(classifier→reviewer→editor→reviewer→deliverer→final_review),queue-runner标记delivered,但final_review内的`_send_wecom`因WS断连静默失败。**诊断步骤**:①tracker找status=delivered的合同 ②gateway.log搜846609和"WebSocket error"确认断连时段 ③`uwf step list <thread_id>`确认final_review确实执行过(有时长=执行了) ④确认交付文件在任务交付目录存在。**补救**:用`wecom_dm.py --to doro --text "合同审查完成通知(补发):..."`补发。补发内容含:合同名、顾问单位、修订数(ins/del计数)、主要修订摘要。注明"因XX时段企微WebSocket中断未实时送达,现补发"。
|
||||
|
||||
- **Watchdog "suspended + already delivered" 死循环(2026-07-08 实证)**:当 workflow 在 `final_review` 阶段因 HTTP 500 suspended,但 deliverer 已经成功交付(tracker=delivered)时,watchdog 会无限循环 BLOCK resume 且不归档文件,同时阻止 runner 重启。表现:Doro 没收到通知但文件已在任务交付目录。**诊断**:`tail /tmp/contract-queue/watchdog.log | grep "BLOCK resume"`。**止血三步**:①`uwf thread cancel <thread_id>` 取消所有suspended thread ②`mv /tmp/contract-queue/<files> /tmp/contract-queue/done/` 清空queue ③对已delivered的合同直接做pass(tracker delivered→completed + xlsx追加)。**注意**:可能影响一批合同(2026-07-08是5份同时卡住),需全部处理。详见 `references/watchdog-suspended-delivered-deadlock-20260708.md`。
|
||||
|
||||
- **同名文件判重铁律(2026-07-08 华新镇体检合同教训,Doro明确要求)**:判断两份合同是否为"同一份",**绝不能只看文件名**。邱律师经常对同一份合同发送修改版(文件名不变但内容已改),也可能不同顾问单位使用相同文件名。**判断标准——必须比对合同实质内容**:
|
||||
1. 甲方(顾问单位)名称
|
||||
2. 金额/费用条款(总价、单价、费用上限等关键数字)
|
||||
3. 合同期限(起止日期)
|
||||
4. 项目内容描述
|
||||
5. 字节大小
|
||||
|
||||
**以上任何一项不同 → 不同合同/新版本,必须独立审查。全部相同 → 重复发送,可跳过。**
|
||||
|
||||
此规则适用于所有环节:auto_notify入队、queue-runner启动前、手动操作、以及任何需要判断"是否已审查过"的场景。auto_notify已通过 `~/.hermes/scripts/contract_content_compare.py` 自动执行内容比对。
|
||||
|
||||
**2026-07-08实证**:华新镇体检合同同日发送两版(11:05和16:04),文件名完全相同,但第二版增加了费用上限17万元、项目名称加了"华新镇"、起始日期从9月10日改为9月1日——是实质不同的合同版本。因系统只按文件名判重,第二版被跳过未审查。详见 `references/queue-runner-same-name-different-content-20260708.md`。
|
||||
|
||||
- **auto_notify脚本依赖与失败处理**:如果邱律师的合同没有自动启动workflow,先检查`auto_notify_new_file.sh`是否还活着(`ps aux | grep inotifywait`)。该脚本没有守护机制,会静默死亡。**两种失败模式**:
|
||||
- **模式A:进程死亡** — inotifywait进程不存在。watchdog cron (`63bb31d4f050`) 会自动重启。
|
||||
- **模式B:进程活着但事件静默丢失(更隐蔽)** — 进程在跑但inotifywait没有捕获到任何事件(日志完全为空)。原因可能是:inotifywait的文件描述符失效、文件系统事件被内核丢弃、脚本启动时文件已经到达(race condition)。watchdog无法修复此模式,必须人工介入。
|
||||
- **诊断方法**:`cat ~/.hermes/logs/auto_notify.log | grep 日期` — 如果日志为空但meta文件有当天文件,就是模式B。
|
||||
|
||||
**fallback流程**:
|
||||
1. 检查 `~/.hermes/cache/documents/` 中是否有未处理的文件(按meta时间戳筛选今天邱律师发的)
|
||||
2. 手动复制到 `Doro合同审查任务/待审查/`(用 `sudo cp` + `sudo chown www-data:www-data`)
|
||||
3. 逐个启动 workflow(`uwf thread start review-contract -p "..."`)
|
||||
|
||||
**⚠️ 主动发现铁律(2026-07-03教训)**:任何操作过程中(查私信状态、查文件、核对数据等),如果发现cache/documents/中有未处理的邱律师文件(meta显示sender_id=QiuTing但对应文件不在待审查目录),必须**立即中断当前任务**,先上传+启动workflow,再继续原任务。"收到即启动"不只是auto_notify的职责——手动发现的文件也必须立即处理,不能"等会再说"。2026-07-03实证:邱律师14:27发了新合同,我在14:30+检查私信时看到了meta记录但没有立即启动workflow,直到Doro追问才处理。
|
||||
4. **多份合同时串行执行**:写 relay 脚本(等当前 thread `status=end` 后再启动下一个),因为所有合同共用 `/tmp/contract-review/` 工作目录,并行会互相覆盖
|
||||
5. **后台执行+通知**:`uwf thread exec <thread_id> --count 20 --background`,配合 `notify_on_complete=true`
|
||||
6. 可选:设 cron job 每15分钟检查 relay 进程是否存活,挂了自动重启
|
||||
|
||||
**2026-06-29 实证**:邱律师发了12个文件,auto_notify 没运行,只有4个自动进了 workflow。手动把剩余4个从 cache 移到待审查,写 relay 脚本串行执行,成功完成。
|
||||
**2026-06-30 实证(模式B)**:邱律师发了4份合同,auto_notify进程在跑但日志完全为空(inotifywait静默失效)。手动处理全部4份。
|
||||
- **Subagent/delegate_task 的清理禁令**:给 subagent 的 context 中必须明确写入"禁止删除 Nextcloud 待审查/和任务交付/目录中的任何文件。只做被要求的操作(写tracker/写xlsx/上传),不做任何清理"。2026-07-01教训:subagent 在执行 pass 操作时可能做了多余的清理动作导致文件丢失。
|
||||
- **xlsx文件名列必须统一用delivered_filename**(带【修】或【无修改意见】前缀),不能写原始文件名。
|
||||
|
||||
## 自动清理架构(2026-06-11确认,2026-07-01补充)
|
||||
|
||||
活跃 cron 列表(截至 2026-07-02):
|
||||
- **`contract-cleanup`**(job_id: `4636467b715d`):每小时,no_agent,deliver=local。清理 pass 超 24h 的文件。
|
||||
- **`contract-queue-watchdog`**(job_id: `3174518affda`):每20分钟,no_agent,deliver=local。巡检 queue-runner 是否存活,必要时重启。⚠️ 此 cron 有已知 bug:当 queue/ 中有文件未移入 done/ 时会反复重启 runner 导致重复审查(详见 `references/queue-runner-duplicate-bug-20260702.md`)。**修复前需确保 runner 加了 tracker 查重逻辑。**
|
||||
- **`auto-notify-watchdog`**(job_id: `63bb31d4f050`):每5分钟,no_agent。守护 auto_notify_new_file.sh 的 inotifywait 进程。
|
||||
- 旧版`清理已交付合同`(agent模式/每6h/deliver到wecom:doro)和`合同审查调度`(每15分钟轮询)已于2026-06-11删除,与现有机制功能重复
|
||||
- 新文件监控由 `auto_notify_new_file.sh`(inotifywait实时事件驱动)独立承担,不再有cron轮询
|
||||
|
||||
### ⚠️ 文件删除权限铁律(2026-07-01 Doro纠正)
|
||||
|
||||
**只有 cleanup cron 有权删除待审查/和任务交付/目录中的文件。** 手动操作(包括小Maggie和subagent)不得直接 `docker exec ... rm` 删除这两个目录的文件。
|
||||
|
||||
唯一例外:Doro 明确指令删除特定文件(如"删掉这个招标需求")。
|
||||
|
||||
2026-07-01教训:小Maggie在执行替换/清理操作时,`docker exec rm` 误删了4份已pass但未满24小时的合同原始文件(徐泾北大居、印刷品、健康积分华新、银发健康包)。文件不在trashbin(docker exec rm绕过trashbin),cleanup cron全天silent确认没动,是手动操作误删。
|
||||
|
||||
### Companion 文件清理方案(2026-07-01 确认)
|
||||
|
||||
**Companion 类型因顾问单位而异**(不统一为"审查意见"):
|
||||
- 朱家角:审查意见文档(从模板生成)
|
||||
- 爱卫中心:合同流程单(xlsx)
|
||||
- 练塘:脚注(加在合同里,非独立文件,不需清理)
|
||||
- 其他:无
|
||||
|
||||
**方案**:deliverer 上传后写 manifest → pass skill 读取 manifest 写入 tracker `companion_files` → cleanup 读取并一并删除。
|
||||
|
||||
**manifest 位置**:Nextcloud 任务交付目录下 `.delivery-manifest-{原文件名去扩展名}.json`(隐藏文件)。
|
||||
|
||||
**待落地**:cleanup 脚本需增加读取 `companion_files` 字段的逻辑;deliverer YAML 需增加写 manifest 步骤。
|
||||
|
||||
### 清理时机明确定义
|
||||
- **触发条件**:Doro 说 pass → 写入 tracker(`xlsx_updated_at` 记录当前北京时间)
|
||||
- **清理时机**:cleanup cron 每小时检查 tracker,找 `status=completed` + `xlsx_updated_at` 超过24小时 + `cleaned=false` 的记录
|
||||
- **即:pass 后 24 小时清理**,不是交付后24小时、不是workflow结束后24小时
|
||||
- **清理范围**:精确删除 tracker 中记录的 `original_filename`(待审查/)+ `delivered_filename`(任务交付/)+ `converted_filename`(待审查/,.doc转.docx时有值)
|
||||
- **不清理的**:companion 文件(审查意见)不在 tracker 中,不会被自动清理;xlsx 在上一级目录不受影响
|
||||
|
||||
## 防重复审查(2026-07-01 朱家角标识牌 + 2026-07-02 夏阳/家庭医生签约)
|
||||
|
||||
**已pass合同被重复审查交付的根因**:
|
||||
|
||||
1. **relay-runner/auto_notify 不查 tracker**(2026-07-01):启动 workflow 前不检查 tracker 是否已有 completed 记录。manifest 文件残留已 pass 合同的文件名,被重新捡起来跑了一遍。
|
||||
2. **queue-runner + watchdog 交互 bug**(2026-07-02,详见 `references/queue-runner-duplicate-bug-20260702.md`):watchdog 每20分钟重启 runner(因 done/ 不满 manifest 行数),runner 的 SKIP 逻辑只查文件是否在 queue/ 目录,**不查 tracker**。一天内 watchdog 重启 runner 41次,导致夏阳和家庭医生签约被重复审查并重复通知 Doro。
|
||||
|
||||
**铁律**:任何触发 workflow 的流程(auto_notify / relay-runner / queue-runner / 手动启动),**启动前必须检查 contract-tracker.json**:
|
||||
|
||||
**查错后否认(2026-07-01)**:Doro问朱家角标识牌怎么重复了,回答说"你没pass"——实际查 tracker 发现 seq=229 早在6/27就pass了。**被问任何合同状态时,先查 tracker/xlsx 用工具验证再回答,不凭"印象"。**
|
||||
|
||||
**Queue-runner + Watchdog 重复审查(2026-07-02 实证,详见 `references/queue-runner-duplicate-review-20260702.md`)**:runner等worker时异常退出→worker独立完成(含通知)→文件没移入done/→watchdog重启runner→重新审查→重复通知。**止血**:杀重复进程→清queue→移文件到done/。**Doro说收到重复通知时,按reference文件中的止血SOP执行。**
|
||||
|
||||
```bash
|
||||
# 在 uwf thread start 之前(精确版,匹配 original_filename + status)
|
||||
if python3 -c "
|
||||
import json, sys
|
||||
t = json.load(open('$HOME/.hermes/data/contract-tracker.json'))
|
||||
completed = [c['original_filename'] for c in t['contracts'] if c['status']=='completed']
|
||||
sys.exit(0 if '${FILENAME}' in completed else 1)
|
||||
" 2>/dev/null; then
|
||||
echo "SKIP: already completed in tracker"
|
||||
# 移入 done/ 防止 watchdog 下次重启时再次尝试
|
||||
mv "$QUEUE_DIR/$FILENAME" "$QUEUE_DIR/done/" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
**手动启动时的检查**:用 `python3 -c "import json; ..."` 精确检查 original_filename + status=completed 组合。
|
||||
|
||||
**queue-runner 止血方法**(重复通知正在发生时):
|
||||
1. `kill` runner 进程和 background-worker 进程
|
||||
2. 将已完成的文件全部移入 `done/`:确保 `done/` 数量 ≥ manifest 行数
|
||||
3. 验证后 watchdog 自然停止重启(进度检查通过)
|
||||
|
||||
## 铁律
|
||||
|
||||
- **待审查目录原文件:Doro说pass之前严禁删除(2026-07-01 Doro纠正)**:无论审查了多少轮、出了多少个修订版本,原文件必须留在待审查目录,直到Doro明确说pass。违反此规则等于丢失原始文件。2026-07-01教训:重新审查反委托代发工资和生育友好合同时,把原文件从待审查删了(以为已经审查完),Doro发现后要求恢复。恢复方法:从cache/documents/复制原文件回到待审查目录。
|
||||
- **Nextcloud 待审查/和任务交付/目录文件:只有 cleanup cron 有权删除(2026-07-01确立)**:手动操作(包括小Maggie本人、subagent/delegate_task)不得使用 docker exec rm 删除这两个目录的文件。唯一例外:Doro 明确指令删除特定文件(如"删掉这个招标需求")。2026-07-01教训:今天pass的4份合同(徐泾北大居、印刷品、健康积分、银发健康包)原始文件和交付文件在pass后不到24小时即被删除,tracker显示cleaned=False,cleanup cron全天silent——是手动操作误删。根因无法追溯。
|
||||
- **手动操作时 workflow 规则同样适用(2026-07-01确立)**:手动执行合同审查相关操作(修订、交付、清理)时,必须遵守 workflow YAML 中各角色的职责边界和规则约束。不能因为"我知道怎么做"就跳过规则。
|
||||
- **不主动生成审查意见文档(2026-07-01 Doro纠正)**:除非Doro明确要求,否则pass流程只处理【修】修订版,不主动生成【审】审查意见文档。审查意见是额外交付物。
|
||||
- **方案不等于授权执行**
|
||||
- 新方案提出后,Doro会问"会有什么影响吗"——必须主动分析潜在影响(token消耗、误判风险、时区问题、单点故障、竞态条件等),不能只说好处。被要求"再想一想不要有漏洞"时,逐一列出漏洞清单+解法,不能遗漏。复杂度过高时Doro会直接砍掉——接受并简化。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 合同审查完整性审计方法 (Audit Methodology)
|
||||
|
||||
## 触发条件
|
||||
|
||||
Doro说"查一查""核查""核实""是不是都审查了/pass了/登记了"→ 这是**验证指令**。
|
||||
|
||||
## 铁律
|
||||
|
||||
**回复中必须先有工具调用再有结论。context记忆≠查证,不可直接输出。**
|
||||
|
||||
## 时区转换(铁律)
|
||||
|
||||
服务器时区UTC,Maggie/Doro/邱律师北京时间(UTC+8)。当问"今天发了多少"时:
|
||||
- **北京时间7月3日** = UTC 7月2日 16:00 ~ 7月3日 16:00
|
||||
- `find` 命令用 `-newermt "2026-07-02 16:00:00" ! -newermt "2026-07-03 16:00:00"`
|
||||
- 或 `TZ='Asia/Shanghai' date` 确认当前北京时间
|
||||
|
||||
**典型错误**:用UTC当天(00:00-24:00)筛选→会把北京时间前一天下午的文件算进来、漏掉当天上午的文件。2026-07-03教训:初始查询用 `-mtime -1` 返回了UTC时间范围的文件(含前一天的6份),Maggie追问"北京时间7/3的"后改用精确UTC窗口,确认只有1份。
|
||||
|
||||
## 必须覆盖的数据源(缺一不可)
|
||||
|
||||
### 1. Tracker JSON
|
||||
```bash
|
||||
cat ~/.hermes/data/contract-tracker.json
|
||||
```
|
||||
- 按seq范围筛选
|
||||
- 逐条列出original_filename, party, status, xlsx_updated_at
|
||||
|
||||
### 2. xlsx(与tracker交叉比对)
|
||||
```bash
|
||||
sudo docker cp nextcloud-nextcloud-1:/var/www/html/data/doro/files/Doro合同审查任务/合同审查清单.xlsx /tmp/
|
||||
```
|
||||
- openpyxl读取,逐行比对tracker
|
||||
|
||||
### 3. Gateway log - 全部接收渠道
|
||||
```bash
|
||||
# QiuTing私信文件(空消息=文件附件)
|
||||
grep 'user=QiuTing.*chat=QiuTing' gateway.log | grep "msg=''"
|
||||
|
||||
# Doro私信文件
|
||||
grep 'user=doro.*chat=doro' gateway.log | grep "msg=''"
|
||||
|
||||
# Doro群文件
|
||||
grep 'user=doro.*chat=wrbAFkXAAAiWC3styKqNj0bZyH6BbJ_Q' gateway.log | grep "msg=''"
|
||||
|
||||
# Doro "待审查上传"指令(表示Doro直接往Nextcloud上传了文件)
|
||||
grep 'user=doro' gateway.log | grep -i '待审查.*上传\|上传.*新.*合同'
|
||||
|
||||
# 飞书渠道
|
||||
grep 'feishu.*ou_757f053c9d7aff6c73b18aa60c337756' gateway.log | grep 'media='
|
||||
```
|
||||
|
||||
### 4. Nextcloud目录实时状态
|
||||
```bash
|
||||
# 待审查(原文件仍在=未pass或等cleanup)
|
||||
sudo docker exec nextcloud-nextcloud-1 ls Doro合同审查任务/待审查/
|
||||
|
||||
# 任务交付(交付物)
|
||||
sudo docker exec nextcloud-nextcloud-1 ls Doro合同审查任务/任务交付/
|
||||
```
|
||||
|
||||
### 5. 交叉比对
|
||||
- 接收总数(各渠道文件消息数之和)
|
||||
- 处理总数(tracker completed + 待pass + 跳过 + 排除)
|
||||
- 差值 = 可能遗漏
|
||||
|
||||
## 汇报格式
|
||||
|
||||
```
|
||||
=== 查证方法 ===
|
||||
1. 读了什么(tracker/xlsx/gateway log哪些渠道/Nextcloud哪些目录)
|
||||
2. 每个数据源的结果数
|
||||
|
||||
=== 查证结果 ===
|
||||
- 已pass登记:X份(seq范围)
|
||||
- 已交付未pass:X份(列出文件名)
|
||||
- 已排除:X份(原因)
|
||||
- 差异/存疑:X份(说明)
|
||||
|
||||
=== 无法确认的 ===
|
||||
- 明确说"这些我查不到/确认不了"
|
||||
- 说明已尝试的搜索策略
|
||||
```
|
||||
|
||||
## 反面教材(2026-07-02)
|
||||
|
||||
❌ "查证属实,无遗漏" → 实际没跑任何工具
|
||||
❌ "找不到第2份" → 实际数据在tracker里,自己之前还列过表
|
||||
❌ "你记得叫什么名字吗?" → 把验证责任转嫁用户
|
||||
|
||||
✅ 正确做法:跑完全部5个数据源 → 列出原始数据 → 标注不确定项 → 再给结论
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit 待审查 and 任务交付 directories against tracker.
|
||||
|
||||
Usage:
|
||||
python3 ~/.hermes/skills/legal/contract-pass-workflow/references/cleanup-audit.py
|
||||
|
||||
Prints a matrix showing which files are:
|
||||
- In tracker (and their status/age/cleaned flag)
|
||||
- NOT in tracker (orphans that will never be auto-cleaned)
|
||||
- Should be cleaned (>24h + completed + cleaned=false)
|
||||
|
||||
Does NOT delete anything. Pure diagnostic.
|
||||
"""
|
||||
import json, os, subprocess
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
BJT = timezone(timedelta(hours=8))
|
||||
now = datetime.now(BJT)
|
||||
|
||||
TRACKER = os.path.expanduser('~/.hermes/data/contract-tracker.json')
|
||||
BASE = os.path.expanduser('~/nextcloud/data/data/doro/files/Doro合同审查任务')
|
||||
待审查 = os.path.join(BASE, '待审查')
|
||||
任务交付 = os.path.join(BASE, '任务交付')
|
||||
|
||||
def nc_ls(path):
|
||||
"""List files in a Nextcloud-managed directory (needs sudo)."""
|
||||
r = subprocess.run(['sudo', 'ls', path], capture_output=True, text=True)
|
||||
return [f for f in r.stdout.strip().split('\n') if f] if r.stdout.strip() else []
|
||||
|
||||
def file_age_hours(path):
|
||||
"""Get file age in hours from mtime."""
|
||||
r = subprocess.run(['sudo', 'stat', '-c', '%Y', path], capture_output=True, text=True)
|
||||
if r.stdout.strip():
|
||||
mtime = int(r.stdout.strip())
|
||||
return (now - datetime.fromtimestamp(mtime, tz=BJT)).total_seconds() / 3600
|
||||
return -1
|
||||
|
||||
def main():
|
||||
with open(TRACKER) as f:
|
||||
tracker = json.load(f)
|
||||
|
||||
# Build lookup: filename -> list of tracker records
|
||||
lookup = {}
|
||||
for c in tracker['contracts']:
|
||||
for key in ['delivered_filename', 'original_filename', 'converted_filename']:
|
||||
fn = c.get(key, '')
|
||||
if fn:
|
||||
lookup.setdefault(fn, []).append(c)
|
||||
|
||||
orphans = []
|
||||
|
||||
for label, directory in [('待审查', 待审查), ('任务交付', 任务交付)]:
|
||||
print(f"\n{'='*70}")
|
||||
print(f" {label} ({directory})")
|
||||
print(f"{'='*70}")
|
||||
files = nc_ls(directory)
|
||||
if not files:
|
||||
print(" (empty)")
|
||||
continue
|
||||
|
||||
for fn in sorted(files):
|
||||
records = lookup.get(fn, [])
|
||||
age = file_age_hours(os.path.join(directory, fn))
|
||||
is_companion = any(k in fn for k in ['审查意见', '合同流程单'])
|
||||
|
||||
if records:
|
||||
for r in records:
|
||||
ts = r.get('xlsx_updated_at', '')
|
||||
h = (now - datetime.fromisoformat(ts)).total_seconds() / 3600 if ts else -1
|
||||
should = r['status'] == 'completed' and h > 24
|
||||
flag = '🔴 SHOULD_CLEAN' if should else '⏳ waiting'
|
||||
print(f" {fn}")
|
||||
print(f" seq={r['seq']} | {h:.0f}h | cleaned={r.get('cleaned')} | {flag}")
|
||||
else:
|
||||
tag = '📋 COMPANION_ORPHAN' if is_companion else '⚠️ NOT_IN_TRACKER'
|
||||
print(f" {fn}")
|
||||
print(f" {tag} | age={age:.0f}h")
|
||||
orphans.append((label, fn, age))
|
||||
|
||||
if orphans:
|
||||
print(f"\n{'='*70}")
|
||||
print(f" ORPHANS SUMMARY: {len(orphans)} files not tracked")
|
||||
print(f"{'='*70}")
|
||||
for label, fn, age in orphans:
|
||||
print(f" [{label}] {fn} ({age:.0f}h old)")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
# Companion 文件清理修复方案(待 WeiWei 实施)
|
||||
|
||||
## 问题
|
||||
cleanup cron (`contract-cleanup.py`) 只删 tracker 里有记录的文件。审查意见、合同流程单等 companion 文件按规则不单独写 tracker,主合同被清理后 companion 成为孤儿,永远留在 `任务交付/`。
|
||||
|
||||
## 2026-06-29 实证
|
||||
清理了 11 个 orphan companion 文件(6个合同流程单 xlsx、5个审查意见 docx),最老的残留 91 小时。
|
||||
|
||||
## 修复方案(方案 A,推荐)
|
||||
|
||||
### 1. pass workflow (skill) 改动
|
||||
步骤1写 tracker 时,扫描 `任务交付/` 目录,找到与主合同同名的 companion 文件,写入 `companion_files` 字段。
|
||||
|
||||
### 2. cleanup 脚本改动
|
||||
删主合同时,读 `companion_files` 字段,一并删除。
|
||||
|
||||
## .doc 原始文件残留修复
|
||||
|
||||
cleanup 删 `original_filename` 时,如果文件不存在,尝试同名但换扩展名(.doc 换 .docx 或反之)。
|
||||
|
||||
## 状态
|
||||
- 2026-06-29:方案已提交给 WeiWei 讨论
|
||||
- 待 WeiWei 确认后实施代码改动
|
||||
@@ -0,0 +1,36 @@
|
||||
# Companion 文件清理方案(待 WeiWei 决策)
|
||||
|
||||
## 问题
|
||||
`contract-cleanup.py` 只删 tracker 里 `original_filename` / `converted_filename` / `delivered_filename` 三个字段精确匹配的文件。companion 文件(审查意见、合同流程单)从不进 tracker → 主合同被清理后 companion 成孤儿,永远残留。
|
||||
|
||||
## 方案 A:tracker 增加 companion_files 字段(推荐)
|
||||
|
||||
**改动1:pass workflow(skill contract-pass-workflow)**
|
||||
步骤1写 tracker 时,扫描 `任务交付/` 目录,找到与主合同同目录且包含"审查意见"/"合同流程单"的文件,写入:
|
||||
```json
|
||||
"companion_files": ["【审】购销合同 审查意见.docx", "【审】合同流程单-xxx.xlsx"]
|
||||
```
|
||||
|
||||
**改动2:cleanup 脚本(~/.hermes/scripts/contract-cleanup.py)**
|
||||
删主合同时,读 `companion_files` 字段,逐个 `docker exec rm`。
|
||||
|
||||
**优点**:精确匹配,不误删。
|
||||
**缺点**:需改两处(skill + 脚本)。旧记录无此字段,但不影响——旧 companion 已手动清理或无价值。
|
||||
|
||||
## 方案 B:cleanup 按 stem 模糊匹配
|
||||
|
||||
**改动:仅 cleanup 脚本**
|
||||
删主合同时,取 `delivered_filename` 的 stem(去扩展名),在 `任务交付/` 目录找 `*{stem}*审查意见*` / `*{stem}*合同流程单*` 一并删除。
|
||||
|
||||
**优点**:只改一处。
|
||||
**缺点**:模糊匹配有误删风险(如两个合同名相近)。
|
||||
|
||||
## 附加修复:.doc 原始文件残留
|
||||
|
||||
**问题**:tracker 的 `original_filename` 有时记录了 `.docx`(转换后文件名)而非 `.doc`(真正的原始文件),cleanup 按 `.docx` 去删找不到 `.doc` → 残留。
|
||||
|
||||
**修复**:cleanup 脚本删 `original_filename` 时,如果文件不存在,尝试换扩展名(`.doc` ↔ `.docx`)再找一次。兜底逻辑,不影响正常流程。
|
||||
|
||||
## 状态
|
||||
- 2026-06-29:方案已提出,Doro 未授权执行
|
||||
- 需 WeiWei 决策后实施
|
||||
@@ -0,0 +1,36 @@
|
||||
# Contract Processing Completeness Audit
|
||||
|
||||
When asked "have all contracts been reviewed/registered" for a date range, follow this exhaustive verification method. Do NOT answer from memory — every claim must be tool-verified.
|
||||
|
||||
## Input Channels to Check (ALL of these)
|
||||
|
||||
1. **QiuTing private messages** — gateway.log `user=QiuTing chat=QiuTing msg=''` (empty msg = file)
|
||||
2. **Doro private messages** — gateway.log `user=doro chat=doro msg=''` (empty msg = file)
|
||||
3. **Doro group messages** — gateway.log `user=doro chat=wrbAFkXAAAiWC3styKqNj0bZyH6BbJ_Q msg=''`
|
||||
4. **Feishu messages** — gateway.log feishu platform entries with `media=` indicators
|
||||
5. **Direct Nextcloud uploads** — Doro uploads directly to 待审查/ without going through gateway (indicated by Doro saying "待审查里上传了新合同" without a preceding file message)
|
||||
|
||||
## Cross-Reference Procedure
|
||||
|
||||
```
|
||||
Step 1: Count ALL file-receive events per channel in date range (gateway.log grep)
|
||||
Step 2: Read tracker JSON — list all entries in date range by seq
|
||||
Step 3: Read xlsx — verify 1:1 match with tracker
|
||||
Step 4: Check 待审查/ directory for unprocessed files
|
||||
Step 5: Check 任务交付/ for delivered but un-tracked files
|
||||
Step 6: Reconcile: total received (Step 1) vs total tracked (Step 2)
|
||||
Account for: duplicates (Doro said "重复了 不用审了"), non-contracts (excluded),
|
||||
multi-file batches, files awaiting pass
|
||||
```
|
||||
|
||||
## Critical Rule
|
||||
|
||||
**Never say "查证属实无遗漏" unless ALL channels have been checked and reconciled.** If a channel cannot be fully verified (e.g., log doesn't record filenames for empty messages), state the uncertainty explicitly.
|
||||
|
||||
## Pitfalls (from 2026-06-29 incident)
|
||||
|
||||
- Doro sends files via private chat AND uploads directly to Nextcloud — both channels must be checked
|
||||
- Gateway log records `msg=''` for file messages but does NOT record the filename — you cannot map file→contract from log alone
|
||||
- When Doro says "2份 顾问单位是X", the number must be verified against tracker entries matching that party
|
||||
- A contract may be tracked under a different party name than expected (e.g., "重固卫生服务中心" contract could be filed under the actual contract title without "重固" in it)
|
||||
- Session memory is NOT verification — "I remember processing it" is not evidence
|
||||
@@ -0,0 +1,51 @@
|
||||
# Manual Review Checklist (2026-07-13 session)
|
||||
|
||||
When Doro asks to "审查workflow修改的情况" on delivered contracts, follow this checklist:
|
||||
|
||||
## Process
|
||||
1. **Read the审查规则 first** — full text of review-rules-root.md
|
||||
2. **Get both files**: original from 待审查/ + delivered from 任务交付/
|
||||
3. **Identify ALL WB modifications** — list every WB INS/DEL with paragraph number
|
||||
4. **Distinguish WB from original revisions** — other authors (WB-1, 86187, etc.) are original, don't touch
|
||||
5. **Check each WB modification against rules** — one by one
|
||||
6. **Read full accepted text** — check for语句不通顺, especially at INS boundaries
|
||||
7. **Fix problems directly** — don't ask Doro if you should fix. Just fix.
|
||||
8. **Report findings** — list what's correct and what's wrong
|
||||
9. **Wait for Doro to say pass** — never self-initiate pass
|
||||
|
||||
## Common Workflow Issues Found (2026-07-13)
|
||||
|
||||
### 1. INS rFonts多余属性
|
||||
Workflow consistently adds `hAnsi`, `cs`, `hint` to WB INS runs even when original runs only have `eastAsia` + `ascii`. Fix: strip these three attributes from all WB INS rPr/rFonts.
|
||||
|
||||
### 2. Sub-numbering not updated
|
||||
When workflow inserts a new chapter (e.g. 第七条转包), it changes chapter headings (七→八, 八→九) but does NOT change sub-clause numbering (7.1→8.1, 8.1→9.1, 9.1→10.1). Fix: add DEL old number + INS new number for each sub-clause.
|
||||
|
||||
### 3. New clause heading format mismatch
|
||||
- Wrong pStyle (e.g. Style15 instead of Heading4)
|
||||
- Extra space in heading text (e.g. "第七条 转包" vs original "第六条违约责任" no space)
|
||||
- Missing paragraph properties (spacing, ind) that originals have
|
||||
|
||||
### 4. Missing "法律顾问修订版" footer
|
||||
Workflow sometimes doesn't add the footer. Fix: add via python-docx, then wrap the run in `w:ins author=WB` (must be tracked change format).
|
||||
|
||||
### 5. Duplicate content insertion
|
||||
P32 example: original already had "保密义务不因合同解除...而免除", but WB added another "本条保密义务不因本合同的终止或解除而终止" — semantic duplicate. Fix: remove WB's duplicate.
|
||||
|
||||
### 6. Sentence flow at INS boundaries
|
||||
WB appends保密/数据归属 text directly after original sentence without transition. If the original sentence's context doesn't naturally lead into the INS content (e.g. "遵守保密规范。保密义务不因..."), add a proper subject/definition sentence as transition.
|
||||
|
||||
### 7. 赔偿上限未删
|
||||
Rule says "赔偿上限能删就删". Watch for bilateral clauses with caps (e.g. "违约金额为合同总金额的20%") — if it limits what our client can claim, delete it.
|
||||
|
||||
### 8. File naming with pre-existing【修】prefix
|
||||
If the original file already has 【修】prefix (e.g. from previous editor), the delivered should technically be 【修】【修】... per strict rules. Record as known workflow defect.
|
||||
|
||||
## Self-check before reporting "满意"
|
||||
- [ ] Every WB INS/DEL reviewed against rules
|
||||
- [ ] Full accepted text read for fluency (especially INS boundaries)
|
||||
- [ ] rFonts cleaned (no hAnsi/cs/hint extras)
|
||||
- [ ] Sub-numbering顺延 complete (not just chapter headings)
|
||||
- [ ] Footer "法律顾问修订版" present in tracked change format
|
||||
- [ ] No duplicate semantic content
|
||||
- [ ] Heading style/format matches originals
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# 交付通知未送达诊断(2026-07-09 职业卫生+舜珙血压计)
|
||||
|
||||
## 现象
|
||||
Doro说"有几份合同没收到交付通知"。任务交付目录有文件,tracker状态=delivered。
|
||||
|
||||
## 根因
|
||||
今日凌晨01:27-02:15企微WebSocket连接中断(errcode 846609: aibot websocket not subscribed)。
|
||||
Workflow的final_review步骤内部调用`_send_wecom(extra, 'doro', msg)`发送通知,但此时WS已断,通知静默失败。
|
||||
|
||||
## 时间线
|
||||
- 01:27:23 — 首次846609错误
|
||||
- 01:28:01 — 职业卫生合同thread end(final_review完成,通知发送失败)
|
||||
- 01:30:41 — WebSocket closed (attempt 6)
|
||||
- 02:12:29 — 舜珙血压计thread end(final_review完成,通知发送失败)
|
||||
- 02:15:50 — WebSocket closed (attempt 7)
|
||||
- 02:18:30 — Doro发"hi",WS恢复
|
||||
|
||||
## 诊断命令
|
||||
```bash
|
||||
# 1. 找tracker中delivered状态的合同
|
||||
python3 -c "import json; t=json.load(open('~/.hermes/data/contract-tracker.json')); [print(c['delivered_filename']) for c in t['contracts'] if c.get('status')=='delivered']"
|
||||
|
||||
# 2. 查gateway.log确认WS断连时段
|
||||
grep '846609\|WebSocket error\|WebSocket closed' ~/.hermes/logs/gateway.log | grep '2026-07-09'
|
||||
|
||||
# 3. 确认thread完成了final_review
|
||||
uwf step list <thread_id> # 看是否有final_revi步骤且有时长
|
||||
|
||||
# 4. 确认文件在任务交付目录
|
||||
sudo docker exec nextcloud-nextcloud-1 stat "/var/www/html/data/doro/files/Doro合同审查任务/任务交付/【修】XXX.docx"
|
||||
```
|
||||
|
||||
## 补发方法
|
||||
```bash
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "合同审查完成通知(补发):
|
||||
|
||||
1️⃣ 合同名称
|
||||
顾问单位: XXX
|
||||
修订: N处插入、M处删除
|
||||
主要修订: ...
|
||||
|
||||
(此通知因XX时段企微WebSocket连接中断未能实时送达,现补发)"
|
||||
```
|
||||
|
||||
## 修订内容提取方法(用于补发摘要)
|
||||
```python
|
||||
import zipfile
|
||||
from lxml import etree
|
||||
W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||
|
||||
with zipfile.ZipFile(path) as z:
|
||||
xml = z.read('word/document.xml')
|
||||
root = etree.fromstring(xml)
|
||||
|
||||
# Count WB modifications
|
||||
wb_ins = [ins for ins in root.findall(f'.//{{{W}}}ins') if ins.get(f'{{{W}}}author') == 'WB']
|
||||
wb_del = [d for d in root.findall(f'.//{{{W}}}del') if d.get(f'{{{W}}}author') == 'WB']
|
||||
print(f"WB修订: {len(wb_ins)} ins, {len(wb_del)} del")
|
||||
|
||||
# Get INS text summaries
|
||||
for ins in wb_ins[:10]:
|
||||
texts = [t.text for t in ins.iter(f'{{{W}}}t') if t.text]
|
||||
text = ''.join(texts).strip()
|
||||
if text and len(text) > 2:
|
||||
print(f" + {text[:80]}")
|
||||
```
|
||||
|
||||
## 预防措施
|
||||
- auto_notify_watchdog cron每5分钟检查WS连接
|
||||
- gateway WS重连机制(attempt 6-8自动重连)
|
||||
- 但final_review内的`_send_wecom`调用没有重试机制——WS断了就直接失败
|
||||
- **待改进**:final_review的通知步骤应增加重试逻辑或失败后写入pending_notifications队列
|
||||
@@ -0,0 +1,64 @@
|
||||
# Notification Silent Failure — WeChat 846609 WebSocket Disconnection
|
||||
|
||||
## Incident: 2026-07-09
|
||||
|
||||
### Timeline
|
||||
- 01:27 UTC — Gateway errcode 846609 first appears (WebSocket not subscribed)
|
||||
- 01:28 — 职业卫生监督 workflow final_review completes, notification fails silently
|
||||
- 01:30 — WebSocket error attempt 6
|
||||
- 02:12 — 舜珙血压计 workflow final_review completes, notification fails silently
|
||||
- 02:15 — WebSocket error attempt 7
|
||||
- 02:18 — Doro sends "hi", WebSocket recovers (inbound works before outbound stabilizes)
|
||||
|
||||
### Root Cause
|
||||
`final_review` calls `_send_wecom(extra, 'doro', msg)` in a subprocess. When the WeCom WebSocket is disconnected (846609), the send fails but:
|
||||
1. The uwf thread still ends successfully (status=end)
|
||||
2. The queue-runner marks the contract as `delivered` in tracker
|
||||
3. No retry mechanism exists for failed notifications
|
||||
4. No alarm fires for silent notification failures
|
||||
|
||||
### Diagnostic Commands
|
||||
|
||||
```bash
|
||||
# 1. Find delivered contracts that may have missed notifications
|
||||
python3 -c "
|
||||
import json
|
||||
t = json.load(open('$HOME/.hermes/data/contract-tracker.json'))
|
||||
for c in t['contracts']:
|
||||
if c.get('status') == 'delivered':
|
||||
print(f\" {c['original_filename']} delivered_at={c.get('delivered_at','?')}\")
|
||||
"
|
||||
|
||||
# 2. Check for 846609 errors in the time window
|
||||
grep '846609' ~/.hermes/logs/gateway.log | grep "$(date +%Y-%m-%d)"
|
||||
|
||||
# 3. Check WebSocket disconnection periods
|
||||
grep 'WebSocket error\|websocket closed' ~/.hermes/logs/gateway.log | grep "$(date +%Y-%m-%d)"
|
||||
|
||||
# 4. Verify if notification was actually sent (look for successful send around delivered_at)
|
||||
# A successful notification looks like:
|
||||
# INFO gateway.platforms.base: [Wecom] Sending response (XXX chars) to doro
|
||||
# WITHOUT a subsequent 846609 error in the same second
|
||||
|
||||
# 5. Check which threads completed during outage
|
||||
grep 'DONE.*status=end' /tmp/contract-queue/queue.log | grep "TIME_RANGE"
|
||||
```
|
||||
|
||||
### Remediation
|
||||
```bash
|
||||
# Manually re-send notification for affected contracts
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "合同审查完成通知(补发):
|
||||
|
||||
文件名: 【修】XXX.docx
|
||||
顾问单位: XXX
|
||||
修订摘要: X处插入、Y处删除
|
||||
主要修订: ...
|
||||
|
||||
已上传至Nextcloud任务交付目录。
|
||||
(因企微连接中断未能实时送达,现补发)"
|
||||
```
|
||||
|
||||
### Prevention (not yet implemented)
|
||||
- `final_review` should check send result and retry 3x with backoff
|
||||
- Queue-runner should distinguish "delivered + notified" from "delivered + notification failed"
|
||||
- Watchdog could audit: for each `delivered` record older than 30 min, verify gateway.log has a matching successful send
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# 交付通知丢失:企微WS凌晨断连 + fire-and-forget架构
|
||||
|
||||
## 事件:2026-07-09 职业卫生+舜珙血压计两份合同交付无通知
|
||||
|
||||
### 时间线
|
||||
- 01:23:06 — 最后一条成功发送(gateway → doro)
|
||||
- 01:25~01:27 — WS断连(原因未知,WeCom服务端)
|
||||
- 01:27:23 — 首次846609 "aibot websocket not subscribed"
|
||||
- 01:28:01 — 职业卫生监督thread=end,final_review尝试通知→失败
|
||||
- 01:30:44 — WS reconnected
|
||||
- 02:04:16 — 成功发送(QiuTing),说明短暂恢复
|
||||
- 02:12:29 — 舜珙thread=end,通知可能在02:05-02:12期间尝试
|
||||
- 02:15:50 — WS再次断开
|
||||
- 持续不稳定直到 06:36
|
||||
|
||||
### 根因链
|
||||
1. WeCom WS凌晨不稳定(每30-60min断一次,服务端维护/长连接超时)
|
||||
2. Gateway自动重连成功但846609持续("not subscribed"是服务端状态滞后)
|
||||
3. workflow 24/7运行,final_review完成时间不可控
|
||||
4. final_review通知是fire-and-forget:`_send_wecom(extra, 'doro', msg)` 调一次,失败即丢弃
|
||||
|
||||
### 通知机制分析
|
||||
```
|
||||
final_review procedure step 3:
|
||||
cd ~/.hermes/hermes-agent && source venv/bin/activate && python -c "
|
||||
from tools.send_message_tool import _send_wecom
|
||||
...asyncio.run(_send_wecom(extra, 'doro', msg))..."
|
||||
```
|
||||
|
||||
`_send_wecom`实现:
|
||||
- 创建**新的** WeComAdapter实例
|
||||
- connect() → send() → disconnect()
|
||||
- 独立WS连接,不依赖gateway的WS
|
||||
- 但用的是同一个WeCom API,846609是服务端状态,新连接一样受影响
|
||||
- 失败返回 `{"error": "..."}` 给LLM,LLM可能仍标记notification_sent=true
|
||||
|
||||
### 7月8日也有相同模式
|
||||
- 01:13 Timeout → 03:07 reconnect失败 → 06:04 gateway重启才恢复
|
||||
- 约5小时不可用窗口
|
||||
|
||||
### 解决方案(待实施)
|
||||
|
||||
**推荐方案B:watchdog补发**
|
||||
- watchdog cron(每20min)增加逻辑:
|
||||
1. 扫描tracker中 `status=delivered` + `delivered_at > 30min前` + `notification_sent != true`
|
||||
2. 用 `wecom_dm.py --to doro` 补发通知(独立WS连接)
|
||||
3. 成功后写 `notification_sent=true` + `notification_at=timestamp`
|
||||
4. 失败则 `notification_attempts += 1`,下次tick继续重试
|
||||
5. attempts > 6(即2小时)仍失败→日志告警不再重试
|
||||
|
||||
**tracker字段扩展**:
|
||||
```json
|
||||
{
|
||||
"notification_sent": false,
|
||||
"notification_at": null,
|
||||
"notification_attempts": 0
|
||||
}
|
||||
```
|
||||
|
||||
**wecom_dm.py优势**:
|
||||
- 独立WS连接,不受gateway状态影响
|
||||
- 有明确的返回值(success/fail)
|
||||
- 凌晨WS虽然不稳定但有恢复窗口(如02:04成功发送)
|
||||
- 20min tick间隔 × 多次重试,大概率能命中一个可用窗口
|
||||
|
||||
### 临时止血
|
||||
当发现合同delivered但Doro没收到通知时:
|
||||
```bash
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "合同审查完成通知(补发): ..."
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# Pre-Pass Audit Checklist (2026-07-13 练塘璞石+环保袋实战)
|
||||
|
||||
When Doro asks to "审查workflow修改" before pass, use this checklist.
|
||||
|
||||
## Step 1: Identify WB vs Original Revisions
|
||||
|
||||
```python
|
||||
# In delivered docx:
|
||||
for ins in root.iter(f'{{{W}}}ins'):
|
||||
author = ins.get(f'{{{W}}}author')
|
||||
# WB = workflow's modifications (audit these)
|
||||
# Others (WB-1, 86187, 杨丽, etc.) = original file revisions (leave alone)
|
||||
```
|
||||
|
||||
## Step 2: Format Audit (per WB INS run)
|
||||
|
||||
| Check | How | Common Fail |
|
||||
|-------|-----|-------------|
|
||||
| rFonts extra attrs | Compare WB INS rFonts with same-para orig run | hAnsi/cs/hint added by workflow |
|
||||
| sz mismatch | Compare sz values | Usually OK if same as orig |
|
||||
| pStyle on new headings | Compare with adjacent original headings | Style15 instead of Heading4 |
|
||||
| Heading spacing/ind | Must match original heading paragraphs | Missing before/after=0, ind |
|
||||
| Bold | If orig headings not bold, new ones shouldn't be | Usually OK |
|
||||
| Title space | "第七条转包" vs "第七条 转包" | Workflow adds space |
|
||||
|
||||
## Step 3: Content/Numbering Audit
|
||||
|
||||
| Check | How | Common Fail |
|
||||
|-------|-----|-------------|
|
||||
| Sub-numbering顺延 | If 第七条→第八条, check 7.1→8.1 etc. | Workflow only changes chapter heading, forgets sub-numbers |
|
||||
| 赔偿上限20% | Bilateral caps limit our client's recovery | Workflow misses bilateral cap deletion |
|
||||
| Content dedup | Check if INS保密存续 duplicates existing text | Original may already have "义务不因...终止而免除" |
|
||||
| 编号冲突 | Original may already have duplicate numbers | Don't fix original numbering bugs (per rules) |
|
||||
|
||||
## Step 4: Global Checks
|
||||
|
||||
- [ ] 脚注"法律顾问修订版" exists AND is in tracked change format (w:ins author=WB)
|
||||
- [ ] File naming: 【修】+ original filename unchanged
|
||||
- [ ] Read full accepted text for WB-introduced grammar issues
|
||||
- [ ] Original revisions (other authors) untouched
|
||||
|
||||
## Fix Patterns
|
||||
|
||||
### Sub-numbering (split across runs: "7" + ".1 ")
|
||||
Only need to DEL/INS the first digit run. Don't touch ".1 " run.
|
||||
|
||||
### Precise text deletion (P49 pattern)
|
||||
When deleting middle of a single large run:
|
||||
1. Split run into: before_text | DEL_text | after_text
|
||||
2. Create 3 elements: normal_run(before) + del_elem(middle) + normal_run(after)
|
||||
3. Insert at original position
|
||||
|
||||
### Footer tracked change
|
||||
1. Add footer text via python-docx
|
||||
2. Re-open with zipfile, find footer XML
|
||||
3. Wrap the text run in `<w:ins id="..." author="WB" date="...">`
|
||||
@@ -0,0 +1,91 @@
|
||||
# Queue-Runner / Watchdog 重复审查 Bug(2026-07-02 确诊)
|
||||
|
||||
## 症状
|
||||
|
||||
Doro 不断收到同一份合同的重复"审查完毕"通知。同一天内夏阳合同被审查交付2次,家庭医生签约合同被审查交付后又有一个 thread 在跑。
|
||||
|
||||
## 根因
|
||||
|
||||
`contract-queue-watchdog`(cron `3174518affda`,每20分钟)与 `contract-queue-runner.sh` 的交互存在逻辑缺陷:
|
||||
|
||||
### 时间线
|
||||
|
||||
1. Runner 启动,从 manifest.txt 读取待处理文件列表
|
||||
2. Runner 对文件A启动 `uwf thread start` + `uwf thread exec --background`
|
||||
3. 背景 worker(node进程)开始跑 workflow,耗时 1-2 小时
|
||||
4. Runner 自身在 `wait for worker PID` 循环中——但如果 runner 自己因某种原因退出(进程被杀、OOM、超时),只剩 worker 在跑
|
||||
5. **关键 bug**:workflow 跑完后 deliverer 交付文件 + final_review 通知 Doro,但 runner 已经不在了——无法把文件从 queue/ 移入 done/
|
||||
6. 20分钟后 watchdog tick:发现 `RUNNER_PID` 为空 + `DONE_CNT < TOTAL` + 无活跃 worker → **重启 runner**
|
||||
7. 新 runner 读 manifest,发现文件还在 queue/(因为没被移到 done/)→ **再次启动 workflow** → 重复审查 → 重复通知
|
||||
|
||||
### 更隐蔽的变体(本次实证)
|
||||
|
||||
即使 runner 没死,也会出问题:
|
||||
- Runner 在等 worker exit,worker 正常完成 → runner 移文件到 done/ → 进入下一份
|
||||
- 但 runner 处理完 manifest 所有文件后正常退出
|
||||
- 新文件在 runner 退出后被追加到 manifest(如 auto_notify 追加)
|
||||
- Watchdog 重启 runner → runner 从头读 manifest → 前面的文件已在 done/ 会被 SKIP
|
||||
- **但如果某份文件在 queue/ 中仍然存在**(不在 done/)→ 又跑一遍
|
||||
|
||||
### 为什么文件会在 queue/ 而不在 done/
|
||||
|
||||
1. Runner 异常退出,文件从未被移到 done/
|
||||
2. 新追加的文件,上一轮 runner 没跑到就退出了
|
||||
3. 文件被 auto_notify 或手动操作重新放回 queue/(不太可能但理论上存在)
|
||||
|
||||
## 缺失的防线
|
||||
|
||||
Runner 的 SKIP 逻辑只有一层:
|
||||
|
||||
```bash
|
||||
[ -e "$FILE" ] || { log "SKIP (not found / already done): $BASENAME"; continue; }
|
||||
```
|
||||
|
||||
只看文件是否还在 queue/ 目录。**完全不查 contract-tracker.json**。
|
||||
|
||||
## 修复方案
|
||||
|
||||
在 runner 的 `=== START:` 之前加 tracker 查重:
|
||||
|
||||
```bash
|
||||
# === BEFORE START: check tracker for already-completed ===
|
||||
if python3 -c "
|
||||
import json, sys
|
||||
t = json.load(open('$HOME/.hermes/data/contract-tracker.json'))
|
||||
completed = [c['original_filename'] for c in t['contracts'] if c['status']=='completed']
|
||||
sys.exit(0 if '$BASENAME' in completed else 1)
|
||||
" 2>/dev/null; then
|
||||
log "SKIP (already completed in tracker): $BASENAME"
|
||||
mv "$FILE" "$QUEUE_DIR/done/"
|
||||
continue
|
||||
fi
|
||||
```
|
||||
|
||||
## 止血操作(已执行)
|
||||
|
||||
1. ✅ kill 了正在重复跑的 reviewer thread (06FJ5NPHT9XX63HW0WDKXV3ZSR) 的 worker
|
||||
2. ✅ kill 了 queue-runner 进程 (PID 1907973)
|
||||
3. ✅ 将所有已处理文件移入 done/(家庭医生签约、朱家角标识标牌、计划生育协议)
|
||||
4. ✅ 验证 done/ 数量 ≥ manifest 行数 → watchdog 不会再重启 runner
|
||||
|
||||
## 今天的重复统计
|
||||
|
||||
| 合同 | 正常审查 | 重复审查 | 影响 |
|
||||
|------|----------|----------|------|
|
||||
| 恭兴 | 05:00 (end) | 02:45被cancel了不算 | 无重复 |
|
||||
| 肃言 | 06:25 (end) | — | 无重复 |
|
||||
| 卫健委 | 07:28 (end) | — | 无重复 |
|
||||
| 夏阳 | 08:42 (end) | 11:40 再跑一遍 (end) | ⚠️ 重复通知 |
|
||||
| 家庭医生签约 | 12:38→19:21交付 | 19:40又重启→21:16重复交付 | ⚠️ 重复通知 |
|
||||
|
||||
## Watchdog 今天重启 runner 的次数
|
||||
|
||||
今天 watchdog tick 64次,其中触发 runner 重启 **41次**(00:00-10:40每20分钟都重启一次!)。大多数重启只是空跑(文件都在 done/ 了),但恭兴和夏阳那两次重启时文件还没进 done/,导致重复审查。
|
||||
|
||||
## 相关组件
|
||||
|
||||
- Runner 脚本:`~/.hermes/skills/devops/uwf/scripts/contract-queue-runner.sh`
|
||||
- Watchdog 脚本:`~/.hermes/scripts/contract-queue-watchdog.sh`
|
||||
- Watchdog cron:`contract-queue-watchdog` (job_id: `3174518affda`),每20分钟
|
||||
- Queue 目录:`/tmp/contract-queue/`(manifest.txt + done/)
|
||||
- Tracker:`~/.hermes/data/contract-tracker.json`
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
# Queue-Runner + Watchdog 重复审查Bug(2026-07-02 实证)
|
||||
|
||||
## 现象
|
||||
Doro反复收到同一份合同的"审查完毕"通知。今天受影响的合同:
|
||||
- 夏阳合同:被通知至少2次(可能3次)
|
||||
- 家庭医生签约合同:被通知2次(第3次在final_review被手动杀掉)
|
||||
|
||||
## Bug链条(已验证)
|
||||
|
||||
```
|
||||
1. Queue-runner 启动 → START 合同X → 创建 worker PID → "Waiting for worker..."
|
||||
2. Runner 进程异常退出(OOM/信号/shell被杀),但 worker 子进程继续运行
|
||||
3. Worker 独立完成整个 workflow(包括 final_review = 私信通知 Doro)
|
||||
4. Runner 已死 → 没有执行 "mv $FILE done/" → 文件仍在 queue 目录
|
||||
5. Watchdog(20min cron)检测到:runner不在 + done/ < manifest → 重启 runner
|
||||
6. 新 runner 看到文件还在 queue → SKIP逻辑只检查 `[ -e "$FILE" ]` → 认为未处理
|
||||
7. 新 runner 启动第二个 thread → 从头审查 → final_review 又通知 Doro
|
||||
```
|
||||
|
||||
## 额外失败模式(watchdog恢复旧thread)
|
||||
|
||||
```
|
||||
watchdog.log:
|
||||
[2026-07-02 18:40:14] suspended 06FJ42KNSK1ZA767W8AZ6MXJ6C → exec 恢复
|
||||
[2026-07-02 18:40:15] idle 06FJ3ZJXR5WHJNPDHF22YYMGCM → exec 续跑
|
||||
```
|
||||
|
||||
Watchdog 恢复处于 idle/suspended 状态的旧 thread,这些 thread 的同名合同可能已被新 runner 完成。
|
||||
旧 thread 被唤醒后接着跑完 final_review → 又一次通知。
|
||||
|
||||
## 今天的实际时间线
|
||||
|
||||
| 时间(BJT) | 事件 |
|
||||
|-----------|------|
|
||||
| 02:45 | 第一个runner启动,处理恭兴(cancelled) |
|
||||
| 05:00 | Watchdog重启runner → 恭兴(成功) |
|
||||
| 06:25 | 肃言完成 |
|
||||
| 07:28 | 卫健委完成 |
|
||||
| 08:42 | 夏阳 thread#1 启动 (06FJ3ZJXR) |
|
||||
| ~11:00 | 夏阳#1 完成(含final_review通知);但runner已死,文件没进done/ |
|
||||
| 11:40 | Watchdog重启runner → 夏阳 thread#2 启动 (06FJ58AD) |
|
||||
| 12:38 | 夏阳#2 完成(第二次通知);家庭医生签约 thread 启动 (06FJ5NPHT) |
|
||||
| 18:40 | Watchdog恢复旧idle thread 06FJ3ZJXR5WHJNPDHF22YYMGCM (夏阳#1) + 06FJ42KNSK (家庭医生#?) |
|
||||
| 19:21 | 家庭医生签约交付到Nextcloud |
|
||||
| ~21:00 | 06FJ42KNSK 完成 final_review(第2次家庭医生通知) |
|
||||
| 21:16 | Watchdog再次重启runner → 家庭医生 thread#2 (06FJ5NPHT) 进入 final_review |
|
||||
| 21:30 | 手动 kill -9 杀掉 06FJ5NPHT 的 final_review → 阻止第3次通知 |
|
||||
|
||||
## 止血SOP
|
||||
|
||||
当 Doro 报告收到重复通知时:
|
||||
|
||||
1. **找重复进程**:`ps aux | grep -E "(background-worker|uwf-hermes)" | grep -v grep`
|
||||
2. **杀掉重复进程**:`kill -9 <worker-PID> <uwf-hermes-PID>`
|
||||
3. **杀掉queue-runner**:`kill <queue-runner-PID>`
|
||||
4. **清理queue**:把所有tracker中已completed的文件移入done/
|
||||
```python
|
||||
import json, os, shutil
|
||||
tracker = json.load(open(os.path.expanduser('~/.hermes/data/contract-tracker.json')))
|
||||
completed = {c['original_filename'] for c in tracker['contracts'] if c['status'] == 'completed'}
|
||||
queue_dir = '/tmp/contract-queue'
|
||||
for f in os.listdir(queue_dir):
|
||||
if f.endswith(('.doc', '.docx', '.pdf')) and f in completed:
|
||||
shutil.move(f'{queue_dir}/{f}', f'{queue_dir}/done/{f}')
|
||||
```
|
||||
5. **验证**:`find /tmp/contract-queue/ -maxdepth 1 -name '*.doc*'` 应为空
|
||||
6. **确认watchdog不会重启**:done/ 文件数 ≥ manifest.txt 行数
|
||||
|
||||
## 缺失防线(待修复)
|
||||
|
||||
| 位置 | 应加的检查 |
|
||||
|------|-----------|
|
||||
| queue-runner START 逻辑 | 启动workflow前查tracker:已completed直接mv到done/ |
|
||||
| watchdog 恢复thread逻辑 | resume前查:同filename的tracker记录是否already completed |
|
||||
| final_review | 发通知前查:是否24h内已有同合同的通知(防御性去重) |
|
||||
|
||||
## 关键文件路径
|
||||
|
||||
- Queue runner: `/home/maggie/.hermes/skills/devops/uwf/scripts/contract-queue-runner.sh`
|
||||
- Watchdog: `~/.hermes/scripts/contract-queue-watchdog.sh`
|
||||
- Queue dir: `/tmp/contract-queue/` (manifest.txt + done/)
|
||||
- Queue log: `/tmp/contract-queue/queue.log`
|
||||
- Watchdog log: `/tmp/contract-queue/watchdog.log`
|
||||
- Tracker: `~/.hermes/data/contract-tracker.json`
|
||||
- Cron jobs: `contract-queue-watchdog` (*/20, job_id:3174518affda), `auto-notify-watchdog` (5min, job_id:63bb31d4f050)
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# Queue-Runner: Same-Name Different-Content File Silently Dropped (2026-07-08)
|
||||
|
||||
## Incident
|
||||
|
||||
邱律师 sent two versions of "2026年华新镇公立中小学生健康体检服务合同.docx" on the same day:
|
||||
- 11:05 BJT (27889 bytes): generic version without fee cap
|
||||
- 16:04 BJT (27482 bytes): specific version with 华新镇 in project name, ¥170,000 fee cap, different start date
|
||||
|
||||
Only the first was reviewed. The second was silently dropped.
|
||||
|
||||
## Root Cause Chain (3 components)
|
||||
|
||||
### 1. auto_notify manifest dedup (`grep -qFx`)
|
||||
|
||||
```bash
|
||||
# In auto_notify_new_file.sh:
|
||||
if ! grep -qFx "$orig_name" "$QUEUE_DIR/manifest.txt" 2>/dev/null; then
|
||||
echo "$orig_name" >> "$QUEUE_DIR/manifest.txt"
|
||||
fi
|
||||
```
|
||||
|
||||
The filename was already in manifest.txt from the first file → second file NOT appended → manifest has only ONE entry for this filename.
|
||||
|
||||
### 2. Runner single-pass no-backtrack
|
||||
|
||||
The runner reads manifest top-to-bottom in one pass. By 08:00:09 UTC it had already passed the 华新镇 line (first version was in done/ → "SKIP not found"). When auto_notify wrote the second file to queue/ at 08:04, the runner was already past that line processing later files. It never goes back.
|
||||
|
||||
### 3. done/ presence satisfies watchdog progress check
|
||||
|
||||
`[ -e "$QUEUE_DIR/done/$f" ]` — the first version in done/ counts as "complete" for this manifest line.
|
||||
|
||||
## Key Principle (Doro 2026-07-08 铁律)
|
||||
|
||||
**判断是否为相同文件不能只看文件名。** 必须检查合同实质内容:
|
||||
- 顾问单位(甲方)名称
|
||||
- 金额/费用上限
|
||||
- 合同期限(起止日期)
|
||||
- 项目内容描述
|
||||
- 字节大小
|
||||
|
||||
以上任何一项不同 → 视为新版本/不同合同,正常入队审查。
|
||||
全部相同 → 视为重复发送,可跳过。
|
||||
|
||||
## Fix Implemented (2026-07-09)
|
||||
|
||||
### 1. Content comparison script: `~/.hermes/scripts/contract_content_compare.py`
|
||||
|
||||
```
|
||||
Usage: python3 contract_content_compare.py <file_a> <file_b>
|
||||
Exit 0 = same content (duplicate)
|
||||
Exit 1 = different content (new version / different contract)
|
||||
Exit 2 = cannot read (treat as different, err on safe side)
|
||||
```
|
||||
|
||||
Compares:
|
||||
- File size (bytes)
|
||||
- 甲方 name (regex extraction from first 2000 chars)
|
||||
- All amounts (阿拉伯数字 ≥4 digits + 元/万, 人民币XXX, percentages)
|
||||
- All dates (YYYY年M月D日 format)
|
||||
- Project summary (first 500 chars normalized)
|
||||
|
||||
### 2. auto_notify_new_file.sh modification
|
||||
|
||||
Replaced the simple `grep -qFx` manifest dedup with content-level comparison:
|
||||
|
||||
```bash
|
||||
if grep -qFx "$orig_name" "$QUEUE_DIR/manifest.txt" 2>/dev/null; then
|
||||
# Same filename exists in manifest — compare content
|
||||
EXISTING="" # find in done/ or queue/
|
||||
COMPARE_RESULT=$(python3 contract_content_compare.py "$EXISTING" "$filepath")
|
||||
if [ $? -eq 0 ]; then
|
||||
# Content identical → true duplicate, skip
|
||||
log "SKIP duplicate (content identical): $orig_name"
|
||||
return
|
||||
else
|
||||
# Content different → new version, rename with timestamp suffix and queue
|
||||
RENAMED="${orig_name%.*}_v${TS}.${orig_name##*.}"
|
||||
cp "$filepath" "$QUEUE_DIR/$RENAMED"
|
||||
echo "$RENAMED" >> "$QUEUE_DIR/manifest.txt"
|
||||
log "SAME NAME DIFFERENT CONTENT — queued as new: $RENAMED"
|
||||
fi
|
||||
else
|
||||
# Brand new filename, normal flow
|
||||
cp "$filepath" "$QUEUE_DIR/${orig_name}"
|
||||
echo "$orig_name" >> "$QUEUE_DIR/manifest.txt"
|
||||
fi
|
||||
```
|
||||
|
||||
### 3. Verification
|
||||
|
||||
Tested with the actual 华新镇 two files:
|
||||
```
|
||||
$ python3 contract_content_compare.py file1.docx file2.docx
|
||||
DIFFERENT: 两份文件内容不同(新版本/不同合同)
|
||||
- 字节大小不同: 27889 vs 27482
|
||||
- 金额不同: A多set(), B多{'170000'}
|
||||
- 日期不同: A多{'2026年9月10日'}, B多{'2026年9月1日'}
|
||||
- 内容不同(第212字起): '...年公立中小学生健康检查...' vs '...年华新镇公立中小学生健康体检...'
|
||||
```
|
||||
|
||||
## Key Lesson
|
||||
|
||||
The initial diagnosis went through multiple wrong iterations:
|
||||
1. First said "queue-runner only checks filename" — wrong (tracker guard also exists)
|
||||
2. Then said "tracker guard is the one that skipped" — wrong (no tracker skip in logs)
|
||||
3. Then said "整个链路没有内容比对" — wrong (Doro corrected: the system should have had it)
|
||||
|
||||
**Correct diagnosis process**: trace the actual log timestamps precisely, verify each claim with tool output, don't assume mechanisms exist or don't exist without reading the actual code.
|
||||
|
||||
**Doro's requirement**: "不能用文件名判断是否为相同文件,需要查看合同内容(包括顾问单位名称、金额、日期等)以及字节大小,要全面判断。" This is a capability requirement, not just a bug fix — the system must understand what makes two contracts "the same" at a business level.
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Watchdog "suspended + delivered" Deadlock (2026-07-08)
|
||||
|
||||
## Symptom
|
||||
- Doro reports not receiving delivery notification for completed contracts
|
||||
- `tail watchdog.log` shows repeated lines every 20 minutes:
|
||||
```
|
||||
BLOCK resume <thread_id>: tracker shows <filename> already delivered
|
||||
runner 退出但有 worker/卡住thread在处理,暂不重启(避免撞车)
|
||||
```
|
||||
- Files ARE in 任务交付/ directory (deliverer succeeded), but notification was never sent
|
||||
- Queue runner has exited, watchdog won't restart it
|
||||
|
||||
## Root Cause Chain
|
||||
1. Workflow reaches `final_review` step (responsible for quality check + notification)
|
||||
2. `final_review` encounters HTTP 500 / API failure → thread suspends
|
||||
3. But `deliverer` already succeeded earlier → tracker shows `status=delivered`
|
||||
4. Queue-runner sees `status=suspended` (not `end`) → marks as "needs inspection", does NOT archive to done/
|
||||
5. Queue-runner continues to next file, eventually exits
|
||||
6. Watchdog checks: finds suspended thread → tries to resume → checks tracker → sees "already delivered" → BLOCK
|
||||
7. File still in queue/ (not in done/) → watchdog thinks work remains → won't restart runner for other files
|
||||
8. **Infinite loop**: every 20 min watchdog ticks, hits same BLOCK, same "暂不重启"
|
||||
|
||||
## Impact Scope (2026-07-08 batch)
|
||||
All 5 contracts from the 16:00 batch were affected (all hit HTTP 500 at final_review):
|
||||
- 【修】华新慢病支持中心运维合同(1)_2.docx
|
||||
- 赵巷合同1.doc
|
||||
- 疾控中心(卫监所)实习人员宿舍改造工程.docx
|
||||
- 2026年爱在党群·沟通有方家长沟通之道青春健康教育项目合作协议.docx
|
||||
- 合同.docx
|
||||
|
||||
## Resolution (止血 SOP)
|
||||
|
||||
### Step 1: Cancel suspended threads
|
||||
```bash
|
||||
/home/maggie/.hermes/node/bin/uwf thread cancel <thread_id>
|
||||
# Repeat for all suspended threads
|
||||
```
|
||||
|
||||
### Step 2: Move stuck files to done/
|
||||
```bash
|
||||
cd /tmp/contract-queue
|
||||
mv "filename1.docx" done/
|
||||
mv "filename2.doc" done/
|
||||
# Move ALL files that are in tracker as delivered/completed
|
||||
```
|
||||
|
||||
### Step 3: Verify queue is clear
|
||||
```bash
|
||||
ls /tmp/contract-queue/*.doc* 2>/dev/null # Should be empty
|
||||
```
|
||||
|
||||
### Step 4: Do pass for delivered files
|
||||
Since files are already in 任务交付/ and tracker shows delivered, proceed with normal pass flow (update tracker to completed + write xlsx).
|
||||
|
||||
## Prevention
|
||||
- The watchdog should detect "suspended + already delivered" as a terminal state and auto-archive to done/ (currently it only BLOCKs resume without archiving)
|
||||
- The final_review step should have retry logic for transient HTTP 500 errors
|
||||
- Queue-runner should archive files to done/ even when thread status=suspended IF tracker shows delivered (the work is done, only notification failed)
|
||||
|
||||
## Diagnosis Commands
|
||||
```bash
|
||||
# Check for deadlock pattern
|
||||
tail -20 /tmp/contract-queue/watchdog.log | grep "BLOCK resume"
|
||||
|
||||
# Check queue-runner status
|
||||
ps aux | grep contract-queue-runner | grep -v grep
|
||||
|
||||
# Check what's stuck in queue
|
||||
ls /tmp/contract-queue/*.doc* 2>/dev/null
|
||||
|
||||
# Check tracker for delivered status
|
||||
python3 -c "
|
||||
import json
|
||||
t = json.load(open('$HOME/.hermes/data/contract-tracker.json'))
|
||||
for c in t['contracts']:
|
||||
if c.get('status') == 'delivered':
|
||||
print(f\" {c['original_filename']} → delivered\")
|
||||
"
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Workflow Issues Report 2026-07-01 (Summary)
|
||||
|
||||
Full report at: Nextcloud 小Maggie协作区/workflow-issues-report-20260701.md
|
||||
|
||||
## Key Findings
|
||||
|
||||
### File Disappearance Root Cause
|
||||
Files "missing" from 待审查 were likely **never uploaded there**. auto_notify failed silently (Mode B),
|
||||
workflow read directly from cache path, audit completed, but Nextcloud 待审查 directory was never populated.
|
||||
Cleanup cron confirmed NOT responsible (all cleaned records show >24h gap).
|
||||
|
||||
### Duplicate Review (朱家角标识牌)
|
||||
Root cause: `/tmp/contract-queue/manifest` retained filename after pass+clean.
|
||||
relay-runner reads manifest without checking contract-tracker.json for completed status.
|
||||
Fix: relay-runner must `grep original_filename tracker.json` before `uwf thread start`.
|
||||
|
||||
### Companion Cleanup
|
||||
Confirmed approach (Doro 2026-07-01):
|
||||
- Naming rule: `【审】{原文件名去扩展名} 审查意见.docx`
|
||||
- Pass skill writes `companion_files` field to tracker
|
||||
- Cleanup script reads and deletes alongside main contract
|
||||
- Both sides must be modified simultaneously
|
||||
|
||||
### Private Message Routing
|
||||
`wecom_group_notify.py` (default=group) was used when `wecom_dm.py --to qiuting` (DM) was required.
|
||||
Workflow YAML line 16-17 also incorrectly references group notify script.
|
||||
@@ -0,0 +1,31 @@
|
||||
# xlsx覆盖事故 2026-07-03
|
||||
|
||||
## 事故
|
||||
|
||||
pass流程中使用了/tmp残留的旧xlsx(240行,截止6月30日),覆盖了Nextcloud上的最新版(252行,截止7月3日),导致seq 241-251共11条记录丢失。
|
||||
|
||||
## 根因
|
||||
|
||||
没有按skill规定从Nextcloud实时拉取最新xlsx,直接用了本地旧文件。
|
||||
|
||||
## 恢复
|
||||
|
||||
/tmp下恰好有另一份正确副本(今早其他session拉取的),用它恢复+追加新行。
|
||||
|
||||
## 铁律
|
||||
|
||||
1. 写xlsx前必须 `rm -f → docker cp从NC拉取 → 读max_row打印seq确认 → 追加 → 上传`
|
||||
2. 禁止使用/tmp中任何已存在的xlsx文件
|
||||
3. 上传后重新拉取验证行数
|
||||
|
||||
## Doro原话
|
||||
|
||||
"你这个错误太可怕了"
|
||||
"不仅要写入,避免下次发生,还得写入铁律"
|
||||
|
||||
## 连带错误
|
||||
|
||||
同一session中还犯了:
|
||||
- 查到邱律师新文件后没查auto_notify日志就手动启动workflow → 重复
|
||||
- 主观判断两份文件"相同" → 被Doro纠正"你也不要去判断是不是同一份文件"
|
||||
- 时间用UTC而非北京时间 → 多次纠正
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
独立终审/pass 前核验 PDF 批注交付件(2026-06-22 阳澄湖团建实证)。
|
||||
|
||||
用于扫描件/PDF 合同走"批注模式"交付后,小Maggie作为总负责人独立亲验——
|
||||
不只信 workflow final_review 自报。docx 专项检查(编号/INS字体/numPr)对 PDF 不适用,
|
||||
PDF 批注交付改查以下项:
|
||||
|
||||
1. Nextcloud 交付件 ↔ 本地核验件 SHA256 字节一致(防同步/编码改坏)
|
||||
2. 页数:原件 == 交付件
|
||||
3. 原文完整性:原件批注数应为 0(确认原文未被改动)
|
||||
4. 批注数 + author:高亮(Highlight)与批注气泡(Text)应配对,全部 author=WB
|
||||
5. 批注格式合规:内容全以"建议"开头,无【】标签、无理由/原因解释
|
||||
6. 高亮几何锚定:落在正文区(非页边/空白),宽度横跨条款行
|
||||
|
||||
⚠️ 若交付件大小/页数异常(如读出 0 页、大小骤变),先排查是不是用户在 OnlyOffice
|
||||
手动编辑——别急着判定损坏并用本地副本覆盖。见 SKILL.md "已知坑"第一条。
|
||||
|
||||
用法:
|
||||
python3 verify_pdf_annotation_deliverable.py <原件.pdf> <Nextcloud交付件.pdf> [本地核验件.pdf]
|
||||
# 第三个参数可选;给了就做 SHA256 字节一致性比对
|
||||
"""
|
||||
import sys, os, hashlib
|
||||
|
||||
try:
|
||||
import pymupdf
|
||||
except ImportError:
|
||||
import fitz as pymupdf
|
||||
|
||||
FORBIDDEN = ['【', '】', '原因', '理由', '因为', '风险'] # 批注禁用 token(保密条款本体"因乙方原因"需人工甄别)
|
||||
|
||||
def sha256(p):
|
||||
h = hashlib.sha256()
|
||||
with open(p, 'rb') as f:
|
||||
h.update(f.read())
|
||||
return h.hexdigest()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__); sys.exit(1)
|
||||
orig, deliv = sys.argv[1], sys.argv[2]
|
||||
local = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
ok = True
|
||||
|
||||
# 1. SHA256 字节一致
|
||||
if local:
|
||||
s_d, s_l = sha256(deliv), sha256(local)
|
||||
same = s_d == s_l
|
||||
print(f"[1] SHA256 NC↔本地 {'✅一致' if same else '❌不一致'} NC={s_d[:16]} 本地={s_l[:16]}")
|
||||
ok &= same
|
||||
|
||||
do, dd = pymupdf.open(orig), pymupdf.open(deliv)
|
||||
|
||||
# 2. 页数
|
||||
p_ok = len(do) == len(dd)
|
||||
print(f"[2] 页数 原件{len(do)}==交付{len(dd)} {'✅' if p_ok else '❌'}")
|
||||
ok &= p_ok
|
||||
if len(dd) == 0:
|
||||
print(" ⚠️ 交付件读出 0 页!先查是不是用户在 OnlyOffice 改动/后台编码,别判定损坏就覆盖。")
|
||||
ok = False
|
||||
|
||||
# 3. 原文完整性
|
||||
orig_annots = sum(len(list(p.annots() or [])) for p in do)
|
||||
print(f"[3] 原件批注数={orig_annots} {'✅原文未改动' if orig_annots == 0 else '⚠️原件本身带批注'}")
|
||||
|
||||
# 4 & 5. 批注数/author/格式
|
||||
total = 0; authors = set(); types = {}; bad_fmt = []
|
||||
for pi, page in enumerate(dd):
|
||||
for a in page.annots() or []:
|
||||
total += 1
|
||||
info = a.info
|
||||
authors.add(info.get('title', ''))
|
||||
t = a.type[1]; types[t] = types.get(t, 0) + 1
|
||||
c = (info.get('content', '') or '').strip()
|
||||
if c:
|
||||
if not c.startswith('建议'):
|
||||
bad_fmt.append(f"P{pi+1}: 非'建议'开头: {c[:30]}")
|
||||
for tok in FORBIDDEN:
|
||||
if tok in c:
|
||||
bad_fmt.append(f"P{pi+1}: 含禁用token[{tok}]: {c[:30]}")
|
||||
a_ok = authors <= {'WB'} and total > 0
|
||||
print(f"[4] 批注总数={total} 类型={types} author={authors} {'✅' if a_ok else '❌'}")
|
||||
ok &= a_ok
|
||||
if bad_fmt:
|
||||
print(f"[5] ❌批注格式问题{len(bad_fmt)}处:")
|
||||
for b in bad_fmt[:10]:
|
||||
print(f" {b}")
|
||||
ok = False
|
||||
else:
|
||||
print(f"[5] 批注格式 ✅全以'建议'开头、无【】/理由")
|
||||
|
||||
# 6. 高亮几何锚定(落正文非页边)
|
||||
print(f"[6] 高亮锚定位置:")
|
||||
for pi, page in enumerate(dd):
|
||||
ph = page.rect.height
|
||||
for a in page.annots() or []:
|
||||
if a.type[1] == 'Highlight':
|
||||
r = a.rect
|
||||
yp = r.y0 / ph * 100
|
||||
flag = '✅' if (5 < yp < 95 and r.width > 200) else '⚠️疑似页边/过窄'
|
||||
print(f" P{pi+1} y={yp:.0f}%高 宽{r.width:.0f}px {flag}")
|
||||
|
||||
print(f"\n{'='*40}\n总核验: {'✅ 全部通过,可交付/登记' if ok else '❌ 有问题,先排查再说'}")
|
||||
sys.exit(0 if ok else 2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user