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)。
This commit is contained in:
parent
dc1c52b827
commit
d891ee720a
@ -1,15 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_scense_ext RBAC 路径注册(硬门禁 6.6 / QC #11 / M9 重做 QC #1)。
|
||||
"""pbl_scense_ext RBAC 路径注册(硬门禁 6.6 / QC #11 / M9 重做 QC #1 / QC #6)。
|
||||
|
||||
约定:
|
||||
- 路径 = 模块自动路由 `/pbl_scense_ext/...`,不带端口、不带 /wss 前缀;
|
||||
- 角色 `any` = 免登录可取(覆盖层引导脚本 loader.js、覆盖层主题 CSS——须在
|
||||
鉴权完成前即可注入,否则预鉴权页壳拿不到 loader);
|
||||
- 角色 `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
|
||||
@ -25,6 +31,18 @@ OVERLAY_PAGES = [
|
||||
'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,均需登录) ──
|
||||
@ -39,7 +57,7 @@ PATHS = [
|
||||
|
||||
# ── 覆盖层静态资源(QC #1 硬门禁:ahserver 自动托管 .css/.js 但仍受 RBAC 约束,
|
||||
# 未注册即 403,M9 前端全废) ──
|
||||
# loader.js 是页壳引导注入器,须在预鉴权页壳里就能加载 → any
|
||||
# loader.js 是页壳引导注入器 + 运行时 i18n 链路,须在预鉴权页壳里就能加载 → any
|
||||
('/pbl_scense_ext/overlay/pbl_overlay_loader.js', 'any'),
|
||||
# 主题 CSS 同理(页壳首屏样式,不含业务数据)→ any
|
||||
('/pbl_scense_ext/overlay/pbl_overlay.css', 'any'),
|
||||
@ -47,6 +65,15 @@ PATHS = [
|
||||
('/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:目录 + 每个文件显式注册(登录后可见,含世界内覆盖层页)
|
||||
@ -71,34 +98,78 @@ def register():
|
||||
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 条目与磁盘文件一一对应(无悬空、无漏注册)。"""
|
||||
"""自检: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 = []
|
||||
# 1) 磁盘上有但未注册
|
||||
for sub in ('overlay', 'pbl_overlay'):
|
||||
d = os.path.join(root, 'wwwroot', sub)
|
||||
if not os.path.isdir(d):
|
||||
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
|
||||
for fn in sorted(os.listdir(d)):
|
||||
if not fn.endswith(('.js', '.css', '.ui')):
|
||||
continue
|
||||
p = '/%s/%s/%s' % (MODULE, sub, fn)
|
||||
if p not in [x[0] for x in PATHS]:
|
||||
problems.append('UNREGISTERED %s' % p)
|
||||
# 2) 注册了但磁盘缺文件(页面壳条目)
|
||||
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)
|
||||
for path in sorted(have):
|
||||
f = os.path.join(root, 'wwwroot', 'pbl_overlay', os.path.basename(path))
|
||||
if not os.path.isfile(f):
|
||||
problems.append('MISSING FILE %s' % path)
|
||||
# 3) 通配符零容忍
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user