#!/usr/bin/env python3 # -*- coding: utf-8 -*- """pbls —— PBL Agent OS 应用唯一入口(端口 9300,见 projects/pbls/env/test.json:port)。 框架铁律(web-application-spec): - 入口必须 `from ahserver.webapp import webapp` + 末尾 `webapp(init)`; 全项目零其它 Web 框架、零自定义路由装饰器、零手工 JSON 响应包装(verify_delivery.py 扫描零命中)。 - 加载顺序固定:`ServerEnv()` → 基础模块(appbase/rbac/pybricks)→ 业务模块。 基础模块必须**显式调用 loader**(load_appbase/load_rbac/load_pybricks), 只 importlib.import_module 就标 loaded 是反模式(登录/权限链路必崩)。 - 库名由应用统一决定:get_module_dbname(m) 从 conf/config.json 的 module_dbmap 读, 模块侧禁止硬编码 DBNAME。 - 端口唯一来源 = projects/pbls/env/{test,prod}.json 的 port;应用仓库不设 env/ 目录。 挂载规模对账(QC #13):20 个 load_* 调用 = 3 平台基础模块(appbase/rbac/pybricks) + 5 复用基座模块(world/scene/entity/script_engine/scense_game) + 12 个 pbl_* 模块(见下 MODULE_LOAD_ORDER,编号 M1~M12 与 modules/*.md 一一对应)。 """ import json import os from ahserver.webapp import webapp from ahserver.serverenv import ServerEnv import bricks_for_python # registers bui processor from bricks_for_python.init import load_pybricks # registers UiWindow etc. from appbase.init import load_appbase from rbac.init import load_rbac CONF = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'conf', 'config.json') # 12 个 pbl_* 业务模块(编号 ↔ 目录,M10=pbl_appcodes 零表、M12=pbl_common 内核) MODULE_LOAD_ORDER = [ ('pbl', 'pbl_common'), # M12 公共内核(必须最先,提供租户/DB 出口) ('pbl', 'pbl_appcodes'), # M10 枚举 6+2 组幂等注入 ('pbl', 'pbl_blueprint'), # M1 蓝图聚合根 + 子对象 + 版本 + 模板 ('pbl', 'pbl_validation'), # M2 14 维校验 + 5 级质量状态 ('pbl', 'pbl_compiler'), # M3 确定性编译 + Game Definition + Capability ('pbl', 'pbl_agent_runtime'), # M4 Designer/Critic + fail-closed 工具裁决 ('pbl', 'pbl_domain_ext'), # M8 world/scene/entity 薄扩展(不改基表) ('pbl', 'pbl_runtime_ext'), # M11 单事务事件+状态写入与广播 ('pbl', 'pbl_evidence'), # M5 产出物 + 证据幂等采集 ('pbl', 'pbl_assessment'), # M6 Rubric 加权评估与报告 ('pbl', 'pbl_kdb_ext'), # M7 KDB 只读桩 + 匿名聚合 ('pbl', 'pbl_scense_ext'), # M9 前端覆盖层上下文 + 页面注册 ] # 复用基座模块(只读引用 + 薄扩展,不改基表;缺失时降级并明确回报,不静默假成功) BASE_MODULE_LOADERS = ['world', 'scene', 'entity', 'script_engine', 'scense_game'] def _conf(): with open(CONF, encoding='utf-8') as f: return json.load(f) def get_module_dbname(m): """模块 → 库名映射(来自 conf/config.json:module_dbmap,禁止写死单个库名)。""" conf = _conf() mapping = conf.get('module_dbmap') or {} if m in mapping: return mapping[m] return mapping.get('*') or conf.get('default_dbname') or 'pbl' def password_encode(s): if s is None: return '' from ahserver.globalEnv import password_encode as _orig return _orig(s) def init(): env = ServerEnv() # ← MUST be before load_rbac env.get_module_dbname = get_module_dbname env.password_encode = password_encode env.pbl_default_dbname = get_module_dbname('pbl') env.pbl_default_tenant = _conf().get('default_tenant') or 'default' # ── 基础模块:显式调用 loader(反模式禁令:不得只 import 就标 loaded)── load_appbase() load_rbac() load_pybricks() # ── 复用基座模块(world/scene/entity/script_engine/scense_game)── missing = [] for name in BASE_MODULE_LOADERS: try: pkg = __import__('importlib').import_module('%s.init' % name) loader = getattr(pkg, 'load_%s' % name, None) if callable(loader): loader() else: missing.append(name) # 无 loader 即视为未挂载,不谎报 loaded except ImportError: missing.append(name) # ── 12 个 pbl_* 业务模块(按 Wave 依赖顺序)── loaded = [] for _db, name in MODULE_LOAD_ORDER: pkg = __import__('importlib').import_module('%s.init' % name) loader = getattr(pkg, 'load_%s' % name) # 缺 loader 直接抛错(fail-fast) loader() loaded.append(name) env.pbl_modules_loaded = loaded env.pbl_base_missing = missing print('[pbls] modules loaded: %d pbl + base missing=%s' % (len(loaded), missing or 'none')) return {'loaded': len(loaded) + 3, 'base_missing': missing} if __name__ == '__main__': webapp(init)