diff --git a/app/ns_wiring.py b/app/ns_wiring.py new file mode 100644 index 0000000..5fce697 --- /dev/null +++ b/app/ns_wiring.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""宿主侧命名空间注入接线(T4 dspy 命名空间符号注入族)。 + +【为什么在宿主侧还有一层】ahserver 的 .dspy exec 命名空间由框架在每次请求构造, +模块 init() 的 `env.xxx = xxx` 只有在**宿主已调用 load_{模块}()** 时才生效;基础模块 +(rbac / appbase / bricks)是 referenced_modules(只读,禁改源码),其端点若依赖 +`env.` 而该符号未挂/为 None,表现就是 `TypeError: 'NoneType' object is not callable` +(R42 集群⑤)。宿主侧做两件确定性的事: + + 1) `install_module_shim(env)` —— 把业务模块的统一 dispatch 入口挂到 ServerEnv + (scense.ns_shim.install;scense_demo 复用同一入口,不重复实现)。 + 2) `backfill_base_handlers(env)` —— 对只读基础模块,用包内真实存在且可调用的函数 + 补齐 `env.`(**只 setattr 运行时命名空间,不改基础模块源码**); + 包内也不存在的符号不伪造,交由端点侧 ns-shim 给 `symbol-missing:` 结构化 503。 + +接线点:应用入口 init() 在全部 load_*() 之后调用本模块 `wire_ns_shim(env)`,并把 +`env.NS_SHIM_WIRING` 落一条自证字符串,供部署态核验。 +""" +import importlib + +NS_WIRING_VERSION = "t4-host-wiring-1" + +# 只读基础模块的 handler 候选(包内存在才补;不存在不伪造) +BASE_HANDLER_CANDIDATES = ( + ("rbac", ("find_unauth_files", "list_path_roles", "list_roles", "list_permissions")), + ("appbase", ("get_code", "get_appcodes_kv", "get_appcodes", "get_svgicon")), + ("bricks", ("list_tables", "tableinfo", "channels")), +) + + +def _get(env, name): + try: + return getattr(env, name, None) + except Exception: + return None + + +def _set(env, name, value): + try: + setattr(env, name, value) + return True + except Exception: + pass + try: + env[name] = value + return True + except Exception: + return False + + +def install_module_shim(env): + """业务模块统一 dispatch 入口(scense 域)。返回已挂载入口名清单。""" + installed = [] + if _get(env, "dspy_ns_call") is not None: + installed.append("dspy_ns_call(pre-existing)") + try: + from scense.ns_shim import install as _install_scense + _install_scense(env) + installed.append("scense.ns_shim.install") + except Exception as exc: # 模块未装/未加载都不是致命错误,但要留痕 + installed.append("scense.ns_shim.install:FAIL:%s" % exc) + return installed + + +def backfill_base_handlers(env): + """只读基础模块 handler 补齐。返回 {module: [补齐符号]}。""" + report = {} + for modpath, names in BASE_HANDLER_CANDIDATES: + try: + mod = importlib.import_module(modpath) + except Exception: + report[modpath] = [] + continue + fixed = [] + for name in names: + fn = getattr(mod, name, None) + if fn is None or not callable(fn): + continue + if _get(env, name) is None: + if _set(env, name, fn): + fixed.append(name) + report[modpath] = fixed + return report + + +def matrix(env): + """注入生效矩阵(部署态可经 /scense/api 侧或脚本读取)。""" + rows = [] + try: + from scense.ns_shim import ns_shim_matrix + rows = ns_shim_matrix(env).get("rows", []) + except Exception: + rows = [] + base = {} + for modpath, names in BASE_HANDLER_CANDIDATES: + base[modpath] = { + name: { + "env_has": _get(env, name) is not None, + "module_has": _module_has(modpath, name), + } for name in names + } + return {"version": NS_WIRING_VERSION, "ops": rows, "base_handlers": base} + + +def _module_has(modpath, name): + try: + mod = importlib.import_module(modpath) + except Exception: + return False + return callable(getattr(mod, name, None)) + + +def wire_ns_shim(env): + """入口接线:install + backfill + 自证。永不抛异常(启动期不能让接线阻断服务)。""" + out = {"version": NS_WIRING_VERSION, "installed": [], "backfilled": {}, "error": None} + try: + out["installed"] = install_module_shim(env) + except Exception as exc: + out["error"] = "install:" + str(exc) + try: + out["backfilled"] = backfill_base_handlers(env) + except Exception as exc: + out["error"] = (out["error"] or "") + "|backfill:" + str(exc) + summary = "; ".join( + ["installed=" + ",".join(out["installed"])] + + ["%s=%s" % (k, ",".join(v) or "-") for k, v in out["backfilled"].items()] + ) + try: + env.NS_SHIM_WIRING = NS_WIRING_VERSION + " " + summary + env.ns_shim_matrix = lambda: matrix(env) # noqa: E731 部署态自检入口 + except Exception: + pass + return out diff --git a/app/scense.py b/app/scense.py index 92fe8c1..a344a07 100644 --- a/app/scense.py +++ b/app/scense.py @@ -105,6 +105,17 @@ def init(): load_drag() load_runtime(env) + # ── 命名空间接线(业务模块 dispatch + 只读基础模块 handler 补齐)── + # rbac/appbase/bricks 是只读基础模块,其 dspy 依赖 env.; + # 未挂载时表现为 TypeError: 'NoneType' object is not callable。 + # 只在运行时 setattr,不改基础模块源码。 + try: + from app.ns_wiring import wire_ns_shim + wire_ns_shim(env) + except Exception as _exc: + import logging as _logging + _logging.getLogger(__name__).error("[ns-wiring] wire_ns_shim failed: %s", _exc) + # ── 标准函数处理器注册(配合 conf/config.json website.startswiths)── from appPublic.registerfunction import RegisterFunction RegisterFunction().register("healthz", _healthz)