# -*- coding: utf-8 -*- """PBL Agent OS 验证原型(Phase 0/1)—— 唯一应用入口。 硬门禁对齐(web-application-spec): 6.1 入口只用 ahserver.webapp.webapp(init),无 Flask/FastAPI/Django/http.server/jsonify/@app.route 6.2 init() 内 ServerEnv() 先于 load_rbac();load_appbase()/load_rbac()/load_pybricks() 为显式调用 6.3 端口唯一事实源 = projects/pbls/env/test.json(9300),本文件不硬编码端口 6.4 DDL 方言 mariadb(BIGINT AUTO_INCREMENT),见 apps/pbls/scripts/ddl/pbls_tables.sql 一个应用 = 一个入口 = 一个端口;28 个模块(13 复用 + 15 新建 pbl_*)全部经 load_{module}() 挂载。 """ import json import os from ahserver.webapp import webapp from appbase.serverenv import ServerEnv APP_NAME = 'pbls' APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CONF_FILE = os.path.join(APP_DIR, 'conf', 'config.json') # 全量 28 模块严格拓扑序(被依赖者先挂载),与 projects/pbls/pbls_spec.json load_order 一致 LOAD_ORDER = [ # 框架基座 'apppublic', 'sqlor', 'ahserver', 'appbase', # 平台服务 'rbac', 'accounting', # 复用域(写保护:零改表) 'world', 'scene', 'entity', 'script_engine', 'scense_runtime', 'scense', 'scense_game', # 新建 pbl_*(Wave0 -> Wave4) 'pbl_common', 'pbl_appcodes', 'pbl_governance', 'pbl_blueprint', 'pbl_template', 'pbl_validation', 'pbl_compiler', 'pbl_agent_runtime', 'pbl_evidence', 'pbl_assessment', 'pbl_domain_ext', 'pbl_scense_ext', 'pbl_runtime_ext', 'pbl_kdb_ext', 'pbl_analytics', ] def _read_conf(): """读取应用配置(conf/config.json),失败时返回最小可用配置而非崩溃。""" try: with open(CONF_FILE, 'r', encoding='utf-8') as fp: return json.load(fp) except Exception as exc: # pragma: no cover - 部署期排障 return {'app': APP_NAME, 'error': 'conf load failed: %s' % exc} def get_module_dbname(module_name): """模块 -> 库名映射(禁止在模块内硬编码 DBNAME)。 Phase 0/1 单库部署:28 个模块全部落 pbls 库;映射优先取 conf/config.json 的 module_dbname 段,缺省回落 db.dbname,再缺省回落 'pbls'。 """ conf = _read_conf() mapping = conf.get('module_dbname') or {} if module_name in mapping: return mapping[module_name] db_conf = conf.get('db') or {} return db_conf.get('dbname') or 'pbls' def init(): """应用初始化:先挂 ServerEnv(含 get_module_dbname),再显式 load 基座,最后按拓扑序挂载业务模块。""" conf = _read_conf() # ---- 6.2 第一步:ServerEnv 必须先于 load_rbac() 建立 ---- env = ServerEnv() env.app = APP_NAME env.app_dir = APP_DIR env.conf = conf env.dbconf = conf.get('db') or {} env.get_module_dbname = get_module_dbname # ---- 6.2 第二步:基座显式调用(不是仅 import) ---- from appbase.init import load_appbase load_appbase() from rbac.init import load_rbac load_rbac() from pybricks.init import load_pybricks load_pybricks() # ---- 第三步:按 load_order 逐个挂载(跳过已显式加载的基座) ---- loaded = [] skipped = [] for module_name in LOAD_ORDER: if module_name in ('appbase', 'rbac', 'ahserver', 'apppublic', 'sqlor'): skipped.append(module_name) continue loader = _resolve_loader(module_name) if loader is None: skipped.append(module_name) continue loader() loaded.append(module_name) env.loaded_modules = loaded env.skipped_modules = skipped return env def _resolve_loader(module_name): """动态解析模块的 load_{module}() 入口;模块缺失时返回 None(记录 skipped,不阻断启动)。""" import importlib candidates = [ ('%s.init' % module_name, 'load_%s' % module_name), (module_name, 'load_%s' % module_name), ('%s.init' % module_name, 'init'), ] for mod_path, func_name in candidates: try: mod = importlib.import_module(mod_path) except Exception: continue func = getattr(mod, func_name, None) if callable(func): return func return None if __name__ == '__main__': webapp(init)