pbls/app/pbls.py
2026-09-16 15:32:49 +08:00

126 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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)