# -*- coding: utf-8 -*- """重写 selfcheck.py 的 F 组:改为 import load_path 模块读取权威 PATHS 元组。 旧实现用正则扫描 load_path.py **源码文本**,会把生成器表达式里的模板串 "/api/pbl_learner/%s.dspy" 误当成注册路径(实际 PATHS 里是展开后的字面量), 导致「无通配符」检查假失败。改为 importlib 加载模块、直接读 PATHS 与 validate(), 以运行时真实值为准,彻底消除文本扫描误报。 """ import os import re import shutil import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SC = os.path.join(ROOT, "scripts", "selfcheck.py") src = open(SC, "r", encoding="utf-8").read() bak = SC + ".fbak" shutil.copyfile(SC, bak) NEW_F = '''# ============================================================== F. RBAC 路径 def group_f() -> None: """以 import load_path 后的运行时 PATHS 元组为权威(不扫源码文本,避免把 生成器模板串 '/api/x/%s.dspy' 误判为通配路径)。""" g = "F.RBAC路径" check(g, os.path.isfile(LOAD_PATH), "load_path.py 存在", rel(LOAD_PATH)) mod = None err = "" try: import importlib.util spec = importlib.util.spec_from_file_location("_lp_m1a", LOAD_PATH) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) except Exception as e: # noqa: BLE001 err = "%s: %s" % (type(e).__name__, e) check(g, mod is not None, "load_path.py 可导入执行", err or "导入成功") if mod is None: return paths = list(getattr(mod, "PATHS", ()) or ()) check(g, len(paths) > 0, "RBAC 注册路径显式枚举(PATHS 元组)", "路径数=%d" % len(paths)) wild = [p for p in paths if "%" in p or "*" in p] check(g, not wild, "RBAC 路径无 %/* 通配符(运行时真实值)", "通配路径=%s" % (wild[:5] or "无")) nonstd = [p for p in paths if not re.fullmatch(r"/api/[A-Za-z0-9_.-]+\\.dspy", p)] check(g, not nonstd, "RBAC 路径均为 /api/<表>/<动作>.dspy 规范字面量", "异常=%s" % (nonstd[:5] or "无")) dup = sorted({p for p in paths if paths.count(p) > 1}) check(g, not dup, "RBAC 路径无重复", "重复=%s" % (dup[:5] or "无")) # 覆盖 json/*.json editable 的 3×11 条(超集合法:支撑表含额外动作) need = set() if os.path.isdir(JSON_DIR): for fn in sorted(os.listdir(JSON_DIR)): if not fn.endswith(".json"): continue d, _e = load_json(os.path.join(JSON_DIR, fn)) for u in ((d or {}).get("editable") or []): if isinstance(u, str) and u.startswith("api/"): need.add("/" + u) missing = sorted(need - set(paths)) check(g, not missing, "load_path.py 覆盖 json/*.json editable 全部路径(超集)", "load_path=%d json_editable=%d 未覆盖=%s" % (len(paths), len(need), missing[:5] or "无")) # 孤儿路径:每条路径的表名必须归属主清单 11 表或支撑表 9 表 main_t = set(getattr(mod, "MAIN_TABLES", ()) or ()) sup_t = set(getattr(mod, "SUPPORT_TABLES", ()) or ()) known = main_t | sup_t check(g, len(main_t) == 11, "load_path.MAIN_TABLES = 设计定稿 11 表", "实际=%d" % len(main_t)) orphan = [] for p in paths: m = re.match(r"/api/([a-z0-9_]+)/", p) if not m or (known and m.group(1) not in known): orphan.append(p) check(g, not orphan, "load_path.py 无孤儿路径(均归属已知表)", "孤儿=%s" % (orphan[:5] or "无")) # 模块自带 validate() 必须通过 vfn = getattr(mod, "validate", None) if callable(vfn): verrs = vfn() or [] check(g, not verrs, "load_path.validate() 自检通过", "错误=%s" % (verrs[:3] or "无")) else: check(g, False, "load_path.validate() 存在", "未定义 validate()") ''' pat = re.compile(r"# =+ F\. RBAC 路径\n.*?(?=# =+ G\.)", re.S) if pat.search(src): src = pat.sub(NEW_F, src, count=1) open(SC, "w", encoding="utf-8").write(src) r = subprocess.run([sys.executable, "-m", "py_compile", SC], capture_output=True, text=True) if r.returncode != 0: shutil.move(bak, SC) print("[ROLLBACK] 编译失败已回滚: %s" % (r.stderr or "")[-400:]) sys.exit(1) os.remove(bak) print("[FIX ] F 组已重写为 import load_path 读运行时 PATHS") else: os.remove(bak) print("[ERR ] 未匹配到 F 组区块,未修改") sys.exit(1)