#!/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> [本地核验件.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()