Files

108 lines
4.3 KiB
Python

#!/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()