126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
# -*- 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)
|