pbl_scense_ext/scripts/load_path.py
agent.develop d891ee720a feat(pbl_scense_ext): loader 运行时 i18n 链路 + i18n RBAC 注册(QC #7 / QC #6)
#7 loader.js 新增运行时 i18n:loadLang(lang) fetch /pbl_scense_ext/i18n/<lang>/msg.txt
→ MSG_RE 解析(与 gen_overlay_pages.py 逐字一致,跳过 # 注释/空行)→ 遍历
[data-pbl-i18n] 节点,按 data-pbl-i18n-args 的 {title}/{key}/{budget}/{src} 占位
替换后写回 textContent;fetch/解析失败降级内置中文 FALLBACK_ZH 并打
degrade('PBL_E_I18N', pbl.overlay.err.i18n) 横幅;start() 在 boot() 之后调一次,
另绑语言切换钩子(pbl-lang-change/bricks-lang-change/languagechange 事件 +
__pblSetLang/setLang)+ MutationObserver 覆盖异步挂载节点。load()/start()/
degrade()/apiBase() 既有逻辑不改。

#6 load_path.py PATHS 追加 ('/pbl_scense_ext/i18n/zh/msg.txt','any') 与
('/pbl_scense_ext/i18n/en/msg.txt','any')(预鉴权页壳首屏文案,禁通配符);
audit() 扫描目录由 overlay/pbl_overlay 扩展为 AUDIT_DIRS 三类(i18n 递归含
语言子目录),新增 UNREGISTERED/MISSING FILE/语言清单一致性/BAD ENTRY/通配符
五类检查,消除漏注册假绿(实测:临时删一条 i18n 条目 → --audit rc=1)。
2026-09-21 13:19:20 +08:00

183 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_scense_ext RBAC 路径注册(硬门禁 6.6 / QC #11 / M9 重做 QC #1 / QC #6)。
约定:
- 路径 = 模块自动路由 `/pbl_scense_ext/...`,不带端口、不带 /wss 前缀;
- 角色 `any` = 免登录可取(覆盖层引导脚本 loader.js、覆盖层主题 CSS、以及
i18n 文案表 wwwroot/i18n/<lang>/msg.txt——预鉴权页壳首屏就要拿到文案,
鉴权完成前拿不到 loader/CSS/msg.txt 会导致页壳空白);
- 角色 `logined` = 登录后可访问(契约 .dspy 接口、覆盖层业务 JS、12 个页面壳 .ui);
- 禁止 `%` / `*` 通配符(module-development-spec「load_path.py Wildcards are FORBIDDEN」),
每个文件一条显式条目;新增/删除静态资源必须同步本表;
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
audit() 自检范围(QC #6 修正「假绿」):overlay/ 与 pbl_overlay/ 之外,
**必须**递归扫描 i18n/(含 zh/en 等语言子目录)——此前只扫 overlay/pbl_overlay,
新增 i18n 资源漏注册不会报错,属假绿。现改为按扩展名递归收集磁盘清单,
漏注册 / 悬空条目 / 通配符三类问题一律非零返回。
"""
import os
import subprocess
import sys
MODULE = 'pbl_scense_ext'
# 覆盖层 12 页面壳(wwwroot/pbl_overlay/*.ui)——与 wwwroot/overlay/pbl_overlay_core.js
# PAGES 注册表 route 同源(route '/pbl_overlay/<key>.ui' → 本模块目录 pbl_overlay/<key>.ui)
OVERLAY_PAGES = [
'home', 'blueprint_list', 'blueprint_editor', 'validation_report',
'agent_console', 'approval_center', 'play_preview', 'mission_board',
'role_panel', 'artifact_studio', 'evidence_wall', 'assessment_report',
]
# i18n 语言清单(wwwroot/i18n/<lang>/msg.txt)——与 scripts/gen_overlay_pages.py
# 的 load_msg('zh')/load_msg('en') 校验语言集合同源,新增语言必须同步本表 + PATHS。
I18N_LANGS = ['zh', 'en']
# audit() 递归扫描的目录(相对 wwwroot/)与其纳管扩展名。
# i18n 文案表是纯文本,必须与 .js/.css/.ui 一样显式注册(QC #6)。
AUDIT_DIRS = {
'overlay': ('.js', '.css'),
'pbl_overlay': ('.ui',),
'i18n': ('.txt',),
}
# (path, role)
PATHS = [
# ── 契约接口(4 个 .dspy,均需登录) ──
('/pbl_scense_ext/api/pbl_session_context_get.dspy', 'logined'),
('/pbl_scense_ext/api/pbl_page_registry_list.dspy', 'logined'),
('/pbl_scense_ext/api/pbl_playtest_feedback_save.dspy', 'logined'),
('/pbl_scense_ext/api/pbl_overlay_bootstrap.dspy', 'logined'),
# ── 模块入口页 ──
('/pbl_scense_ext', 'logined'),
('/pbl_scense_ext/index.ui', 'logined'),
# ── 覆盖层静态资源(QC #1 硬门禁:ahserver 自动托管 .css/.js 但仍受 RBAC 约束,
# 未注册即 403,M9 前端全废) ──
# loader.js 是页壳引导注入器 + 运行时 i18n 链路,须在预鉴权页壳里就能加载 → any
('/pbl_scense_ext/overlay/pbl_overlay_loader.js', 'any'),
# 主题 CSS 同理(页壳首屏样式,不含业务数据)→ any
('/pbl_scense_ext/overlay/pbl_overlay.css', 'any'),
# 业务 JS 只在登录后由 loader 注入 → logined
('/pbl_scense_ext/overlay/pbl_overlay_core.js', 'logined'),
('/pbl_scense_ext/overlay/pbl_overlay_transport.js', 'logined'),
('/pbl_scense_ext/overlay/pbl_overlay_play.js', 'logined'),
# ── i18n 文案表(QC #6 硬门禁)──────────────────────────────────────────
# loader.js 的 loadLang() 运行时 fetch `/pbl_scense_ext/i18n/<lang>/msg.txt`
# 覆盖 [data-pbl-i18n] 节点。页面壳是预鉴权首屏即可渲染的壳,文案表若走
# logined 则未登录/鉴权竞态下拿不到文案 → 界面只剩生成期内嵌中文快照,
# 多语言链路形同不存在。故与 loader.js / CSS 同级注册为 any(纯静态文案,
# 不含任何业务数据/用户数据)。禁止通配符:zh、en 各一条显式条目。
('/pbl_scense_ext/i18n/zh/msg.txt', 'any'),
('/pbl_scense_ext/i18n/en/msg.txt', 'any'),
]
# 覆盖层页面壳 .ui:目录 + 每个文件显式注册(登录后可见,含世界内覆盖层页)
for _key in OVERLAY_PAGES:
PATHS.append(('/pbl_scense_ext/pbl_overlay/%s.ui' % _key, 'logined'))
PATHS.append(('/pbl_scense_ext/pbl_overlay', 'logined'))
def register():
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
done, missing = 0, []
for path, role in PATHS:
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
tool, role, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
done += 1
else:
missing.append((path, role))
print('[%s] rbac paths: total=%d ok=%d pending=%d' % (MODULE, len(PATHS), done, len(missing)))
for path, role in missing:
print(' PENDING %-12s %s' % (role, path))
return len(missing) == 0
def _walk_disk(root, sub, exts):
"""递归收集 wwwroot/<sub> 下扩展名在 exts 内的文件 → (url_path, fs_path)。"""
out = []
base = os.path.join(root, 'wwwroot', sub)
if not os.path.isdir(base):
return out
for dirpath, dirnames, filenames in os.walk(base):
dirnames.sort()
for fn in sorted(filenames):
if not fn.endswith(exts):
continue
fs = os.path.join(dirpath, fn)
rel = os.path.relpath(fs, base).replace(os.sep, '/')
out.append(('/%s/%s/%s' % (MODULE, sub, rel), fs))
return out
def audit():
"""自检:PATHS 与磁盘静态资源(overlay / pbl_overlay / i18n 递归)一一对应。
三类问题(QC #6:i18n 未纳入扫描 = 假绿,现全部覆盖):
UNREGISTERED 磁盘有文件但 PATHS 缺条目(漏注册 → 线上 403)
MISSING FILE PATHS 有条目但磁盘无文件(悬空注册)
BAD ENTRY PATHS 条目落在受管目录里却不符合命名/语言清单约定
WILDCARD 任何通配符(零容忍)
"""
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
problems = []
registered = set(p for p, _r in PATHS)
# 1) 磁盘上有但未注册(含 i18n 语言子目录递归)
for sub, exts in sorted(AUDIT_DIRS.items()):
for url_path, fs_path in _walk_disk(root, sub, exts):
if url_path not in registered:
problems.append('UNREGISTERED %s' % url_path)
# 2) 注册了但磁盘缺文件(overlay / i18n / 页面壳三类都查)
for path, _role in PATHS:
marker = '/%s/' % MODULE
if marker not in path:
continue
rel = path[len(marker):]
head = rel.split('/', 1)[0]
if head not in AUDIT_DIRS:
continue
# 目录型注册条目(如 '/pbl_scense_ext/pbl_overlay',basename 无扩展名)
# 不做文件存在性检查——它注册的是目录路由,磁盘上是目录而非文件。
if '.' not in os.path.basename(rel):
continue
if not os.path.isfile(os.path.join(root, 'wwwroot', rel)):
problems.append('MISSING FILE %s' % path)
# 3) 页面壳条目必须与 OVERLAY_PAGES 注册表同源(防手滑写错页名)
have = set('/%s/pbl_overlay/%s.ui' % (MODULE, k) for k in OVERLAY_PAGES)
for path, _role in PATHS:
if '/pbl_overlay/' in path and path not in have:
problems.append('BAD ENTRY %s' % path)
# 4) i18n 语言清单与磁盘语言目录一致(新增语言目录未登记 I18N_LANGS 即报错)
i18n_base = os.path.join(root, 'wwwroot', 'i18n')
if os.path.isdir(i18n_base):
on_disk_langs = sorted(d for d in os.listdir(i18n_base)
if os.path.isfile(os.path.join(i18n_base, d, 'msg.txt')))
if on_disk_langs != sorted(I18N_LANGS):
problems.append('BAD ENTRY i18n 语言清单与磁盘不一致: 磁盘 %s / 注册 %s'
% (on_disk_langs, sorted(I18N_LANGS)))
# 5) 通配符零容忍
for path, _role in PATHS:
if '%' in path or '*' in path:
problems.append('WILDCARD FORBIDDEN %s' % path)
return problems
if __name__ == '__main__':
bad = audit()
for b in bad:
print('AUDIT %s' % b)
if '--audit' in sys.argv:
sys.exit(1 if bad else 0)
sys.exit(0 if register() else 1)