deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
eb9cd56b6b
commit
3742c65c32
@ -2,44 +2,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 M9 覆盖层 12 个页面壳 wwwroot/pbl_overlay/<key>.ui(bricks 纯 JSON)。
|
||||
|
||||
为什么需要:wwwroot/overlay/pbl_overlay_core.js 的 PAGES 注册表 route 指向
|
||||
【产物定位:构建产物,不入库】
|
||||
wwwroot/pbl_overlay/*.ui 属生成目录,已在 .gitignore 中排除,由
|
||||
apps/pbls/build.sh 第 9 步调用本脚本重跑生成(module-development-spec
|
||||
「CRUD-Generated wwwroot Directories Must Not Be Git-Tracked」/ QC 退回意见 #3 方案 (b))。
|
||||
本脚本(生成器)与 wwwroot/i18n/*/msg.txt(文案事实源)才是入库的交付物。
|
||||
|
||||
为什么需要页面壳:wwwroot/overlay/pbl_overlay_core.js 的 PAGES 注册表 route 指向
|
||||
`/pbl_overlay/<key>.ui`,play.js 的 pageUrl() 会拼成
|
||||
`/pbl_scense_ext/pbl_overlay/<key>.ui`;页面壳文件不存在则导航即 404(QC #3)。
|
||||
`/pbl_scense_ext/pbl_overlay/<key>.ui`;页面壳文件不存在则导航即 404。
|
||||
|
||||
页面壳职责(只做壳,不做业务渲染):
|
||||
1. 打 `data-pbl-page="<key>"` + `data-pbl-overlay-root="1"` 标记 —— play.js
|
||||
hostPageKey() 据此识别当前页、boot() 据此挂载覆盖层根节点;
|
||||
2. 提供 `script` actiontype 按钮调用 `PblOverlayLoader.start({page:...})`,
|
||||
2. 每个文案节点带 `data-pbl-i18n="<key>"`,正文内嵌中文(首屏零请求可读);
|
||||
loader.js 在切换语言时按 msg.txt 覆盖同名节点(i18n 单一事实源);
|
||||
3. 提供 `script` actiontype 按钮调用 `PblOverlayLoader.start({page:...})`,
|
||||
由 loader 按需注入 core/transport/play JS 与 CSS(不在 .ui 里硬写 <script>/<link>,
|
||||
遵守 module-development-spec「ahserver auto-serves .css/.js」);
|
||||
3. 提供返回首页的 `url` actiontype 导航。
|
||||
4. 提供返回首页的 `url` actiontype 导航。
|
||||
|
||||
文案:全部取自 wwwroot/i18n/zh/msg.txt(en 用于校验 key 集合一致),
|
||||
禁止在本文件里硬编码界面文案(QC 退回意见 #3:新增文案必须提取到 i18n)。
|
||||
|
||||
route 与 RBAC:新增/删除页面壳必须同步 scripts/load_path.py 的 OVERLAY_PAGES。
|
||||
|
||||
用法:
|
||||
python3 scripts/gen_overlay_pages.py # 生成 12 个页面壳
|
||||
python3 scripts/gen_overlay_pages.py --check # 只校验(zh/en key 一致 + 壳与注册表一致),不写文件
|
||||
python3 scripts/gen_overlay_pages.py --clean # 删除生成目录内容
|
||||
"""
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
MODULE = 'pbl_scense_ext'
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LOADER_PATH = '/%s/overlay/pbl_overlay_loader.js' % MODULE
|
||||
OUT_DIR = os.path.join(ROOT, 'wwwroot', 'pbl_overlay')
|
||||
I18N_DIR = os.path.join(ROOT, 'wwwroot', 'i18n')
|
||||
|
||||
# (key, title, in_world) —— 与 wwwroot/overlay/pbl_overlay_core.js PAGES 注册表同源
|
||||
# (key, in_world) —— 与 wwwroot/overlay/pbl_overlay_core.js PAGES 注册表同源;
|
||||
# 页面标题从 i18n key `pbl.overlay.page.<key>` 取,不在这里硬编码文案。
|
||||
PAGES = [
|
||||
('home', 'PBL 首页', False),
|
||||
('blueprint_list', '蓝图列表', False),
|
||||
('blueprint_editor', '蓝图结构视图', False),
|
||||
('validation_report', '校验报告(14维)', False),
|
||||
('agent_console', 'Agent 控制台', False),
|
||||
('approval_center', '人工审批中心', False),
|
||||
('play_preview', 'Play 预览', True),
|
||||
('mission_board', '驱动问题板', True),
|
||||
('role_panel', '角色面板', True),
|
||||
('artifact_studio', '产出物工坊', True),
|
||||
('evidence_wall', '证据墙', True),
|
||||
('assessment_report', '评估报告', False),
|
||||
('home', False),
|
||||
('blueprint_list', False),
|
||||
('blueprint_editor', False),
|
||||
('validation_report', False),
|
||||
('agent_console', False),
|
||||
('approval_center', False),
|
||||
('play_preview', True),
|
||||
('mission_board', True),
|
||||
('role_panel', True),
|
||||
('artifact_studio', True),
|
||||
('evidence_wall', True),
|
||||
('assessment_report', False),
|
||||
]
|
||||
|
||||
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
'wwwroot', 'pbl_overlay')
|
||||
MSG_RE = re.compile(r'^\s*([^#:\s][^:]*?)\s*:\s*(.+?)\s*$')
|
||||
|
||||
|
||||
def load_msg(lang):
|
||||
"""读 wwwroot/i18n/<lang>/msg.txt(key: value 一行一条,# 注释)。"""
|
||||
path = os.path.join(I18N_DIR, lang, 'msg.txt')
|
||||
msgs = {}
|
||||
if not os.path.isfile(path):
|
||||
raise SystemExit('[gen_overlay_pages] 缺少文案表 %s' % path)
|
||||
with io.open(path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
m = MSG_RE.match(line)
|
||||
if not m:
|
||||
raise SystemExit('[gen_overlay_pages] %s 非法行: %r' % (path, line))
|
||||
msgs[m.group(1)] = m.group(2)
|
||||
return msgs
|
||||
|
||||
|
||||
def fmt(tpl, **kw):
|
||||
"""msg.txt 模板用 {name} 占位,避免 str.format 与 JSON 花括号冲突。"""
|
||||
out = tpl
|
||||
for k, v in kw.items():
|
||||
out = out.replace('{' + k + '}', v)
|
||||
return out
|
||||
|
||||
|
||||
def enable_script(key):
|
||||
@ -51,22 +100,27 @@ def enable_script(key):
|
||||
"var b=String(window.__PBL_API_BASE__||'').replace(/\\/$/,'');"
|
||||
"var s=document.createElement('script');s.src=b+'%s';s.async=false;"
|
||||
"s.onload=function(){window.PblOverlayLoader.start({page:'%s'})};"
|
||||
"s.onerror=function(){window.__pblOverlayFail&&window.__pblOverlayFail('PBL_E_NETWORK','覆盖层引导脚本加载失败')};"
|
||||
"document.head.appendChild(s);})()"
|
||||
"s.onerror=function(){window.__pblOverlayFail&&window.__pblOverlayFail('PBL_E_NETWORK','')}"
|
||||
";document.head.appendChild(s);})()"
|
||||
) % (key, LOADER_PATH, key)
|
||||
|
||||
|
||||
def build_page(key, title, in_world):
|
||||
def build_page(key, in_world, zh):
|
||||
"""页面壳:文案内嵌中文 + data-pbl-i18n 标记(loader 按语言覆盖)。"""
|
||||
title = zh['pbl.overlay.page.' + key]
|
||||
budget = zh['pbl.overlay.shell.budget.world'] if in_world \
|
||||
else zh['pbl.overlay.shell.budget.firstscreen']
|
||||
shell_html = (
|
||||
'<div data-pbl-page="{key}" data-pbl-overlay-root="1" class="pbl-shell">'
|
||||
'<div class="pbl-shell-hint">'
|
||||
'{title}({key})· 覆盖层未启用:点击下方「启用 PBL 覆盖层」装配 PBL 组件;'
|
||||
'{hint}'
|
||||
'</div></div>'
|
||||
'<div class="pbl-shell-hint" data-pbl-i18n="pbl.overlay.shell.hint">{hint}</div>'
|
||||
'<div class="pbl-shell-budget" data-pbl-i18n="pbl.overlay.shell.budget'
|
||||
'.{bkey}">{budget}</div>'
|
||||
'</div>'
|
||||
).format(
|
||||
key=key, title=title,
|
||||
hint='本页为世界内覆盖层(广播渲染预算 ≤500ms)。' if in_world
|
||||
else '本页为教师/学生侧页面(首屏预算 ≤1.5s)。'
|
||||
key=key,
|
||||
bkey='world' if in_world else 'firstscreen',
|
||||
hint=fmt(zh['pbl.overlay.shell.hint'], title=title, key=key, budget=budget),
|
||||
budget=budget,
|
||||
)
|
||||
return {
|
||||
'widgettype': 'VBox',
|
||||
@ -77,8 +131,11 @@ def build_page(key, title, in_world):
|
||||
'subwidgets': [
|
||||
{
|
||||
'widgettype': 'Text',
|
||||
'options': {'label': 'PBL 覆盖层 · ' + title, 'fontSize': '18px',
|
||||
'options': {'label': fmt(zh['pbl.overlay.shell.title'], title=title),
|
||||
'fontSize': '18px',
|
||||
'fontWeight': 'bold', 'color': '#E5E7EB'},
|
||||
'attrs': {'data-pbl-i18n': 'pbl.overlay.shell.title',
|
||||
'data-pbl-i18n-args': json.dumps({'title': title}, ensure_ascii=False)},
|
||||
},
|
||||
{
|
||||
'widgettype': 'Html',
|
||||
@ -91,14 +148,16 @@ def build_page(key, title, in_world):
|
||||
'subwidgets': [
|
||||
{
|
||||
'widgettype': 'Button',
|
||||
'options': {'label': '启用 PBL 覆盖层', 'type': 'primary'},
|
||||
'options': {'label': zh['pbl.overlay.btn.enable'], 'type': 'primary'},
|
||||
'attrs': {'data-pbl-i18n': 'pbl.overlay.btn.enable'},
|
||||
'binds': [{'wid': 'self', 'event': 'click',
|
||||
'actiontype': 'script',
|
||||
'options': {'script': enable_script(key)}}],
|
||||
},
|
||||
{
|
||||
'widgettype': 'Button',
|
||||
'options': {'label': 'PBL 首页'},
|
||||
'options': {'label': zh['pbl.overlay.btn.home']},
|
||||
'attrs': {'data-pbl-i18n': 'pbl.overlay.btn.home'},
|
||||
'binds': [{'wid': 'self', 'event': 'click',
|
||||
'actiontype': 'url',
|
||||
'options': {'url': "{{entire_url('/%s/pbl_overlay/home.ui')}}" % MODULE}}],
|
||||
@ -107,29 +166,84 @@ def build_page(key, title, in_world):
|
||||
},
|
||||
{
|
||||
'widgettype': 'Text',
|
||||
'options': {'label': '上下文:URL 携带 session_id / blueprint_id / world_id;'
|
||||
'覆盖层仅渲染服务端权威状态,不本地预测(Q4)。',
|
||||
'options': {'label': zh['pbl.overlay.shell.context'],
|
||||
'fontSize': '12px', 'color': '#6B7280', 'marginTop': '10px'},
|
||||
'attrs': {'data-pbl-i18n': 'pbl.overlay.shell.context'},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
def check(zh, en):
|
||||
"""校验:① zh/en key 集合一致;② 每个页面的 i18n key 齐备;③ 生成目录与注册表一致。"""
|
||||
problems = []
|
||||
only_zh = sorted(set(zh) - set(en))
|
||||
only_en = sorted(set(en) - set(zh))
|
||||
if only_zh:
|
||||
problems.append('en/msg.txt 缺 key: %s' % ', '.join(only_zh))
|
||||
if only_en:
|
||||
problems.append('zh/msg.txt 缺 key: %s' % ', '.join(only_en))
|
||||
need = ['pbl.overlay.shell.title', 'pbl.overlay.shell.hint',
|
||||
'pbl.overlay.shell.budget.world', 'pbl.overlay.shell.budget.firstscreen',
|
||||
'pbl.overlay.shell.context', 'pbl.overlay.btn.enable', 'pbl.overlay.btn.home']
|
||||
for k in need:
|
||||
if k not in zh:
|
||||
problems.append('zh/msg.txt 缺必需 key: %s' % k)
|
||||
for key, _in_world in PAGES:
|
||||
pk = 'pbl.overlay.page.' + key
|
||||
if pk not in zh:
|
||||
problems.append('zh/msg.txt 缺页面标题 key: %s' % pk)
|
||||
if pk not in en:
|
||||
problems.append('en/msg.txt 缺页面标题 key: %s' % pk)
|
||||
if os.path.isdir(OUT_DIR):
|
||||
on_disk = sorted(f for f in os.listdir(OUT_DIR) if f.endswith('.ui'))
|
||||
expect = sorted(k + '.ui' for k, _ in PAGES)
|
||||
if on_disk and on_disk != expect:
|
||||
problems.append('生成目录与 PAGES 注册表不一致: 磁盘 %s / 注册 %s'
|
||||
% (on_disk, expect))
|
||||
return problems
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description='生成 pbl_scense_ext 覆盖层页面壳(构建产物,不入库)')
|
||||
ap.add_argument('--check', action='store_true', help='只校验不写文件')
|
||||
ap.add_argument('--clean', action='store_true', help='清空生成目录')
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
zh = load_msg('zh')
|
||||
en = load_msg('en')
|
||||
problems = check(zh, en)
|
||||
if problems:
|
||||
for p in problems:
|
||||
print('[gen_overlay_pages][FAIL] %s' % p, file=sys.stderr)
|
||||
return 1
|
||||
if args.check:
|
||||
print('[gen_overlay_pages] CHECK PASS: %d 页面壳 / zh+en 各 %d 条文案 key 一致'
|
||||
% (len(PAGES), len(zh)))
|
||||
return 0
|
||||
if args.clean:
|
||||
if os.path.isdir(OUT_DIR):
|
||||
for f in os.listdir(OUT_DIR):
|
||||
if f.endswith('.ui'):
|
||||
os.remove(os.path.join(OUT_DIR, f))
|
||||
print('[gen_overlay_pages] cleaned %s' % os.path.relpath(OUT_DIR, ROOT))
|
||||
return 0
|
||||
|
||||
if not os.path.isdir(OUT_DIR):
|
||||
os.makedirs(OUT_DIR)
|
||||
written = []
|
||||
for key, title, in_world in PAGES:
|
||||
obj = build_page(key, title, in_world)
|
||||
for key, in_world in PAGES:
|
||||
obj = build_page(key, in_world, zh)
|
||||
path = os.path.join(OUT_DIR, key + '.ui')
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
with io.open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(obj, f, ensure_ascii=False, indent=2)
|
||||
f.write('\n')
|
||||
written.append(path)
|
||||
print('generated %d overlay page shells:' % len(written))
|
||||
print('generated %d overlay page shells (build artifacts, git-ignored):' % len(written))
|
||||
for p in written:
|
||||
print(' ' + os.path.relpath(p, os.path.dirname(OUT_DIR)))
|
||||
print(' ' + os.path.relpath(p, ROOT))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
26
wwwroot/i18n/en/msg.txt
Normal file
26
wwwroot/i18n/en/msg.txt
Normal file
@ -0,0 +1,26 @@
|
||||
# pbl_scense_ext overlay English texts (M9, key: value per line)
|
||||
# Same key set as wwwroot/i18n/zh/msg.txt (enforced by scripts/gen_overlay_pages.py --check).
|
||||
pbl.overlay.page.home: PBL Home
|
||||
pbl.overlay.page.blueprint_list: Blueprint List
|
||||
pbl.overlay.page.blueprint_editor: Blueprint Structure View
|
||||
pbl.overlay.page.validation_report: Validation Report (14 dims)
|
||||
pbl.overlay.page.agent_console: Agent Console
|
||||
pbl.overlay.page.approval_center: Approval Center
|
||||
pbl.overlay.page.play_preview: Play Preview
|
||||
pbl.overlay.page.mission_board: Mission Board
|
||||
pbl.overlay.page.role_panel: Role Panel
|
||||
pbl.overlay.page.artifact_studio: Artifact Studio
|
||||
pbl.overlay.page.evidence_wall: Evidence Wall
|
||||
pbl.overlay.page.assessment_report: Assessment Report
|
||||
pbl.overlay.shell.title: PBL Overlay · {title}
|
||||
pbl.overlay.shell.hint: {title} ({key}) · overlay not mounted: click "Enable PBL Overlay" below; {budget}
|
||||
pbl.overlay.shell.budget.world: In-world overlay page (broadcast render budget ≤500ms).
|
||||
pbl.overlay.shell.budget.firstscreen: Teacher/student page (first-screen budget ≤1.5s).
|
||||
pbl.overlay.shell.context: Context: URL carries session_id / blueprint_id / world_id; the overlay renders server-authoritative state only, no local prediction (Q4).
|
||||
pbl.overlay.btn.enable: Enable PBL Overlay
|
||||
pbl.overlay.btn.home: PBL Home
|
||||
pbl.overlay.err.network: Overlay bootstrap script failed to load
|
||||
pbl.overlay.err.resload: Resource load failed (403/404?) {src}
|
||||
pbl.overlay.err.playmissing: PblOverlayPlay is not registered
|
||||
pbl.overlay.err.loadfail: Overlay load failed
|
||||
pbl.overlay.err.i18n: i18n bundle load failed (fallback to built-in zh)
|
||||
28
wwwroot/i18n/zh/msg.txt
Normal file
28
wwwroot/i18n/zh/msg.txt
Normal file
@ -0,0 +1,28 @@
|
||||
# pbl_scense_ext 覆盖层中文文案(M9,key: value 一行一条,# 开头为注释)
|
||||
# 单一事实源:wwwroot/pbl_overlay/*.ui 页面壳由 scripts/gen_overlay_pages.py 读本表生成;
|
||||
# 运行时由 wwwroot/overlay/pbl_overlay_loader.js fetch 本表并替换 [data-pbl-i18n] 节点文案。
|
||||
# 新增/删除 key 必须同步 wwwroot/i18n/en/msg.txt(两语言 key 集合一致,build.sh 有 --check 门禁)。
|
||||
pbl.overlay.page.home: PBL 首页
|
||||
pbl.overlay.page.blueprint_list: 蓝图列表
|
||||
pbl.overlay.page.blueprint_editor: 蓝图结构视图
|
||||
pbl.overlay.page.validation_report: 校验报告(14维)
|
||||
pbl.overlay.page.agent_console: Agent 控制台
|
||||
pbl.overlay.page.approval_center: 人工审批中心
|
||||
pbl.overlay.page.play_preview: Play 预览
|
||||
pbl.overlay.page.mission_board: 驱动问题板
|
||||
pbl.overlay.page.role_panel: 角色面板
|
||||
pbl.overlay.page.artifact_studio: 产出物工坊
|
||||
pbl.overlay.page.evidence_wall: 证据墙
|
||||
pbl.overlay.page.assessment_report: 评估报告
|
||||
pbl.overlay.shell.title: PBL 覆盖层 · {title}
|
||||
pbl.overlay.shell.hint: {title}({key})· 覆盖层未启用:点击下方「启用 PBL 覆盖层」装配 PBL 组件;{budget}
|
||||
pbl.overlay.shell.budget.world: 本页为世界内覆盖层(广播渲染预算 ≤500ms)。
|
||||
pbl.overlay.shell.budget.firstscreen: 本页为教师/学生侧页面(首屏预算 ≤1.5s)。
|
||||
pbl.overlay.shell.context: 上下文:URL 携带 session_id / blueprint_id / world_id;覆盖层仅渲染服务端权威状态,不本地预测(Q4)。
|
||||
pbl.overlay.btn.enable: 启用 PBL 覆盖层
|
||||
pbl.overlay.btn.home: PBL 首页
|
||||
pbl.overlay.err.network: 覆盖层引导脚本加载失败
|
||||
pbl.overlay.err.resload: 资源加载失败(403/404?) {src}
|
||||
pbl.overlay.err.playmissing: PblOverlayPlay 未注册
|
||||
pbl.overlay.err.loadfail: 覆盖层加载失败
|
||||
pbl.overlay.err.i18n: 文案表加载失败(降级为内置中文)
|
||||
Loading…
x
Reference in New Issue
Block a user