feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
交付前闸门 / Delivery Gate
|
||||
=========================
|
||||
每次交付南通新东方梳理表前,必须跑这个脚本。
|
||||
不通过 = 不发文件。不通过 = 回去补 workflow。
|
||||
|
||||
用法:
|
||||
python3 delivery-gate.py <xlsx路径> <工作目录> <校区名>
|
||||
|
||||
例:
|
||||
python3 delivery-gate.py /tmp/金飞达/金飞达-梳理-MJ-20260627.xlsx /tmp/金飞达 金飞达
|
||||
|
||||
检查项:
|
||||
G1. 模版比对 subagent 是否执行过(工作目录是否有 模版比对-*.md)
|
||||
G2. K/L 分工自检是否通过
|
||||
G3. 格式自检是否通过(标题酒红底、段标题红底、列头正确)
|
||||
G4. 整体风险分析段是否存在
|
||||
G5. 数据行数是否合理(至少 文件数 行)
|
||||
G6. 文件是否已上传 Nextcloud
|
||||
G7. OCR占位符残留检测(【…】/〔待核PDF〕/OCR模糊 等,任一残留=禁止交付)
|
||||
"""
|
||||
import sys, os, subprocess, re
|
||||
import openpyxl
|
||||
|
||||
def fail(msg):
|
||||
print(f"❌ {msg}")
|
||||
return False
|
||||
|
||||
def ok(msg):
|
||||
print(f"✅ {msg}")
|
||||
return True
|
||||
|
||||
def check_g1(workdir):
|
||||
"""G1: 模版比对 subagent 是否执行过"""
|
||||
import glob
|
||||
files = glob.glob(os.path.join(workdir, "模版比对-*.md"))
|
||||
if not files:
|
||||
return fail("G1 模版比对:未找到模版比对输出文件。Step2 动作B 必须 delegate_task subagent 做模版比对。")
|
||||
f = files[0]
|
||||
size = os.path.getsize(f)
|
||||
if size < 1000:
|
||||
return fail(f"G1 模版比对:{os.path.basename(f)} 仅 {size} 字节,内容过短,可能未完整执行。")
|
||||
return ok(f"G1 模版比对:{os.path.basename(f)} ({size:,} 字节)")
|
||||
|
||||
def check_g2(xlsx_path):
|
||||
"""G2: K/L 分工自检"""
|
||||
# 直接用 kl-separation-check.py
|
||||
script = os.path.expanduser("~/.hermes/skills/legal/contract-portfolio-analysis/scripts/kl-separation-check.py")
|
||||
r = subprocess.run(["python3", script, xlsx_path], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
return fail(f"G2 K/L自检:未通过\n{r.stdout}")
|
||||
return ok("G2 K/L自检:通过")
|
||||
|
||||
def check_g3(xlsx_path):
|
||||
"""G3: 格式自检"""
|
||||
try:
|
||||
wb = openpyxl.load_workbook(xlsx_path)
|
||||
ws = wb[wb.sheetnames[0]]
|
||||
# 找到第一个列头行(含"序号"的)
|
||||
hdr_row = None
|
||||
for r in range(1, 30):
|
||||
if ws.cell(r, 1).value == '序号':
|
||||
hdr_row = r
|
||||
break
|
||||
if not hdr_row:
|
||||
return fail("G3 格式:未找到列头行")
|
||||
checks = [
|
||||
("标题酒红底", ws.cell(1,1).fill.start_color.rgb == 'FF8B1A2B'),
|
||||
("段标题(K列头)", ws.cell(hdr_row, 11).value == '法律风险(站乙方立场)'),
|
||||
("段标题(L列头)", ws.cell(hdr_row, 12).value == '与07标准模版差异'),
|
||||
]
|
||||
for name, passed in checks:
|
||||
if not passed:
|
||||
return fail(f"G3 格式:{name} 不正确")
|
||||
return ok("G3 格式自检:通过")
|
||||
except Exception as e:
|
||||
return fail(f"G3 格式:打开失败 - {e}")
|
||||
|
||||
def check_g4(xlsx_path):
|
||||
"""G4: 整体风险分析段"""
|
||||
try:
|
||||
wb = openpyxl.load_workbook(xlsx_path)
|
||||
ws = wb[wb.sheetnames[0]]
|
||||
for r in range(ws.max_row, 1, -1):
|
||||
v = ws.cell(r, 1).value
|
||||
if v and '整体风险分析' in str(v):
|
||||
return ok(f"G4 整体风险分析段:存在(行{r})")
|
||||
return fail("G4 整体风险分析段:未找到。每校区必须包含「整体风险分析与建议」段。")
|
||||
except Exception as e:
|
||||
return fail(f"G4 整体风险分析段:打开失败 - {e}")
|
||||
|
||||
def check_g5(xlsx_path, min_rows=3):
|
||||
"""G5: 数据行数"""
|
||||
try:
|
||||
wb = openpyxl.load_workbook(xlsx_path)
|
||||
ws = wb[wb.sheetnames[0]]
|
||||
data_rows = 0
|
||||
for r in range(5, ws.max_row + 1):
|
||||
v = ws.cell(r, 1).value
|
||||
if v and re.match(r'^\d+$', str(v).strip()):
|
||||
data_rows += 1
|
||||
if data_rows < min_rows:
|
||||
return fail(f"G5 数据行数:仅 {data_rows} 行,需 ≥ {min_rows}")
|
||||
return ok(f"G5 数据行数:{data_rows} 行")
|
||||
except Exception as e:
|
||||
return fail(f"G5 数据行数:打开失败 - {e}")
|
||||
|
||||
def check_g6(xlsx_path, campus):
|
||||
"""G6: 文件是否已上传 Nextcloud"""
|
||||
fname = os.path.basename(xlsx_path)
|
||||
nc_path = f"/var/www/html/data/admin/files/小Maggie协作区/南通新东方/履约期内非集采合同-综办/房租物业合同/{campus}/{fname}"
|
||||
r = subprocess.run(
|
||||
["docker", "exec", "nextcloud-nextcloud-1", "test", "-f", nc_path],
|
||||
capture_output=True
|
||||
)
|
||||
if r.returncode == 0:
|
||||
return ok(f"G6 Nextcloud:已上传 {campus}/{fname}")
|
||||
else:
|
||||
return fail(f"G6 Nextcloud:未上传到 {campus}/{fname}。请先 docker cp + files:scan。")
|
||||
|
||||
def check_g7(xlsx_path):
|
||||
"""G7: OCR占位符残留检测(【…】、〔待核PDF〕、OCR模糊 等)"""
|
||||
patterns = [
|
||||
r'【\.{1,5}】', # 【…】、【...】
|
||||
r'【…】', # 中文省略号
|
||||
r'〔待核PDF〕', # 待核标记
|
||||
r'〔待核〕',
|
||||
r'OCR模糊', # OCR模糊标记
|
||||
r'OCR无法识别',
|
||||
r'OCR乱码',
|
||||
r'待补全',
|
||||
]
|
||||
try:
|
||||
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
|
||||
hits = []
|
||||
for ws in wb.worksheets:
|
||||
for r in range(1, ws.max_row + 1):
|
||||
for c in range(1, ws.max_column + 1):
|
||||
v = ws.cell(r, c).value
|
||||
if not v:
|
||||
continue
|
||||
sv = str(v)
|
||||
for pat in patterns:
|
||||
if re.search(pat, sv):
|
||||
col_letter = openpyxl.utils.get_column_letter(c)
|
||||
m = re.search(pat, sv)
|
||||
start = max(0, m.start() - 10)
|
||||
end = min(len(sv), m.end() + 10)
|
||||
ctx = sv[start:end].replace('\n', ' ')
|
||||
hits.append(f" [{ws.title}] {col_letter}{r}: ...{ctx}...")
|
||||
if hits:
|
||||
msg = f"G7 OCR占位符残留:发现 {len(hits)} 处未补全的OCR标记\n" + "\n".join(hits[:5])
|
||||
if len(hits) > 5:
|
||||
msg += f"\n ...(共 {len(hits)} 处,仅显示前5处)"
|
||||
msg += "\n 铁律:OCR读不出的必须用vision看原图补全,不能用占位符交付。"
|
||||
return fail(msg)
|
||||
return ok("G7 OCR占位符:无残留(全表扫描通过)")
|
||||
except Exception as e:
|
||||
return fail(f"G7 OCR占位符:检查失败 - {e}")
|
||||
|
||||
def check_g8(workdir):
|
||||
"""G8: OCR完整性checkpoint(物理依赖链第一环)"""
|
||||
checkpoint = os.path.join(workdir, 'step1.verified')
|
||||
if not os.path.exists(checkpoint):
|
||||
return fail(f"G8 OCR依赖链:checkpoint不存在\n 路径: {checkpoint}\n 说明: Step1 OCR完成后必须运行 ocr-integrity-check.py 生成checkpoint\n 动作: python3 scripts/ocr-integrity-check.py {workdir}")
|
||||
# 读取checkpoint内容确认
|
||||
with open(checkpoint, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if 'OCR完整性检查通过' not in content:
|
||||
return fail(f"G8 OCR依赖链:checkpoint内容异常\n {checkpoint} 未包含'OCR完整性检查通过'")
|
||||
return ok(f"G8 OCR依赖链:checkpoint存在且有效")
|
||||
|
||||
def check_g9(workdir):
|
||||
"""G9: 模版比对checkpoint(物理依赖链第二环)"""
|
||||
checkpoint = os.path.join(workdir, 'step2b.verified')
|
||||
if not os.path.exists(checkpoint):
|
||||
return fail(f"G9 模版比对依赖链:checkpoint不存在\n 路径: {checkpoint}\n 说明: Step2 动作B必须delegate subagent做模版比对,然后运行验证脚本\n 动作: python3 scripts/template-diff-verify.py {workdir}")
|
||||
# 读取checkpoint内容确认
|
||||
with open(checkpoint, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if '模版比对验证通过' not in content:
|
||||
return fail(f"G9 模版比对依赖链:checkpoint内容异常\n {checkpoint} 未包含'模版比对验证通过'")
|
||||
return ok(f"G9 模版比对依赖链:checkpoint存在且有效")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print("用法: python3 delivery-gate.py <xlsx路径> <工作目录> <校区名>")
|
||||
print("例: python3 delivery-gate.py /tmp/金飞达/金飞达-梳理-MJ-20260627.xlsx /tmp/金飞达 金飞达")
|
||||
sys.exit(1)
|
||||
|
||||
xlsx = sys.argv[1]
|
||||
workdir = sys.argv[2]
|
||||
campus = sys.argv[3]
|
||||
|
||||
if not os.path.exists(xlsx):
|
||||
print(f"❌ 文件不存在: {xlsx}")
|
||||
sys.exit(1)
|
||||
|
||||
bar = "=" * 60
|
||||
print(bar)
|
||||
print(f" 交付闸门 · {campus}")
|
||||
print(bar)
|
||||
print()
|
||||
|
||||
results = [
|
||||
check_g1(workdir),
|
||||
check_g2(xlsx),
|
||||
check_g3(xlsx),
|
||||
check_g4(xlsx),
|
||||
check_g5(xlsx, min_rows=3),
|
||||
check_g6(xlsx, campus),
|
||||
check_g7(xlsx),
|
||||
check_g8(workdir),
|
||||
check_g9(workdir),
|
||||
]
|
||||
|
||||
print()
|
||||
print(bar)
|
||||
passed = sum(1 for r in results if r)
|
||||
total = len(results)
|
||||
if all(results):
|
||||
print(f" ✅ 全部通过 ({passed}/{total}) —— 可以交付")
|
||||
print(bar)
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f" ❌ {total - passed}/{total} 项未通过 —— 禁止交付,回去补 workflow")
|
||||
print(bar)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user