fix(dspy): 修复 5 个 dspy 的 await-outside-async 语法错误 + 归档死文件
背景:移除 24 个自造鉴权中间件后(上一条 commit 702f274),原本被中间件
提前拦截的请求真正走到 dspy 执行层,暴露出存量语法错误——这些端点此前
一律被中间件返回 401/405,从未真正执行过。
根因:agent 往 dspy 头部注入 ns-shim 段时,把含 await 的辅助函数
(_auth_resolve_uid / _ns_export)写成同步 def,而 ahserver 的
PythonScriptProcessor 只把整个文件包进 async myfunc,函数体内的 await
成为编译期 SyntaxError。
修复(5 个文件,模式一致:同步 def -> async def + 调用点加 await):
- runtime/api/runtime_status.dspy (_auth_resolve_uid, _ns_export)
- runtime/api/build_entity_tree.dspy (同上)
- scense/api/list_scense.dspy (同上)
- scene/api/get_search_scene_type.dspy (同上)
- scene/api/get_search_status.dspy (同上)
另归档 _archive_dead_dspy/add_reseller.dspy:该文件未跟踪、非 rbac 自带
(宿主 rbac 无此文件),且依赖 rbac 中不存在的 openCustomerAccounts,
纯属自造死文件,保留只会持续 500。
验证:
- 全量扫描 184 个 dspy(按 ahserver 包装方式 compile):语法错误 0
- 带 cookie 实测 6 个端点全部 200:healthz / runtime_status /
build_entity_tree / list_scense / get_search_status / get_search_scene_type
- login 200 + cookie、匿名 401 保持正常
This commit is contained in:
parent
702f27407b
commit
0cfeb45a3c
26
_archive_dead_dspy/add_reseller.dspy.20261028_154412
Normal file
26
_archive_dead_dspy/add_reseller.dspy.20261028_154412
Normal file
@ -0,0 +1,26 @@
|
||||
ns = params_kw.copy()
|
||||
id = params_kw.id
|
||||
if not id or len(id) > 32:
|
||||
id = uuid()
|
||||
ns['id'] = id
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('rbac')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ownerid = await get_userorgid()
|
||||
await create_org(sor, ns, orgtypes=['customer', 'reseller'])
|
||||
if openCustomerAccounts:
|
||||
await openCustomerAccounts(sor, ownerid, orgid)
|
||||
if openResellerAccounts:
|
||||
await openResellerAccounts(sor, ownerid, id)
|
||||
return UiMessage(title="Success", message="add reseller success")
|
||||
|
||||
return {
|
||||
"widgettype":"Error",
|
||||
"options":{
|
||||
"title":"Add reseller Error",
|
||||
"cwidth":16,
|
||||
"cheight":9,
|
||||
"timeout":3,
|
||||
"message":"failed"
|
||||
}
|
||||
}
|
||||
281
modules/runtime/wwwroot/api/build_entity_tree.dspy
Normal file
281
modules/runtime/wwwroot/api/build_entity_tree.dspy
Normal file
@ -0,0 +1,281 @@
|
||||
# [fix-iter3 strict-auth v2] fail-closed 门禁(Bug 1BoEurcDgf4HHRqBMmJt8 / R-01-neg,P0)——勿删
|
||||
# 本段取代「globals().get("get_user") 判空」的旧判据:ahserver 的 dspy 预加载全局经
|
||||
# builtins 解析、不在 exec-globals 里,globals().get 恒为 None → 判据失真 → 匿名 200。
|
||||
# 口径:裸名取值 → 双重判空(get_user + get_userinfo,且必须取到有效用户标识)
|
||||
# → 未登录返回真 HTTP 401(Response 解析不到时 set_status(401)+JSON,绝不返回业务数据)。
|
||||
_auth_uid = ""
|
||||
|
||||
|
||||
def _auth_uid_of(_v):
|
||||
"""从 get_user()/get_userinfo() 的返回值里提取有效用户标识;取不到返回 ''。"""
|
||||
if not _v:
|
||||
return ""
|
||||
if isinstance(_v, str):
|
||||
return _v
|
||||
if isinstance(_v, dict):
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
|
||||
|
||||
def _auth_call(_name):
|
||||
"""裸名优先取预加载全局(builtins 解析),退 globals 映射。取不到返回 None。"""
|
||||
_f = None
|
||||
try:
|
||||
_f = globals().get(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
if not callable(_f):
|
||||
try:
|
||||
_f = _auth_bare(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
return _f if callable(_f) else None
|
||||
|
||||
|
||||
def _auth_bare(_name):
|
||||
"""裸名解析(ahserver 预加载全局在 builtins 中);未注入时抛 NameError。"""
|
||||
if _name == "get_user":
|
||||
return get_user
|
||||
if _name == "get_userinfo":
|
||||
return get_userinfo
|
||||
if _name == "get_username":
|
||||
return get_username
|
||||
raise NameError(_name)
|
||||
|
||||
|
||||
async def _auth_resolve_uid():
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
_f = _auth_call(_n)
|
||||
if _f is None:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
_r = None
|
||||
_u = _auth_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
try:
|
||||
_auth_uid = await _auth_resolve_uid()
|
||||
except Exception:
|
||||
_auth_uid = ""
|
||||
|
||||
if not _auth_uid:
|
||||
_auth_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"reason": "login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_auth_resp_cls = None
|
||||
try:
|
||||
_auth_proc = globals().get("processor")
|
||||
if _auth_proc is None:
|
||||
_auth_proc = processor
|
||||
_auth_resp_cls = _auth_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_auth_resp_cls = None
|
||||
if _auth_resp_cls is not None:
|
||||
return _auth_resp_cls(
|
||||
body=json.dumps(_auth_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
request.set_status(401)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return json.dumps(_auth_deny, ensure_ascii=False)
|
||||
except Exception:
|
||||
return _auth_deny
|
||||
# [fix-iter3 strict-auth v2] end
|
||||
# [ns-shim] 统一注入 dspy 执行命名空间(Bug qVMyATuR2Z7UuN2P9pbzp / Ua43R1DMO8-7tCb9ybwo8 / j_rE23TAyqp0OV20OTDwv / iFsP7Yklktq_AixTnEWYi)
|
||||
# 根因:ahserver 的 dspy exec 命名空间不保证注入 `env`(ServerEnv 实例)与模块函数
|
||||
# (create_entity / list_entities / *_import …),直写裸名会 NameError,或把 None 拼进 SQL
|
||||
# ('select None as None from None')→ 泛化 500。
|
||||
# 注入方式(QC#1 整改):ServerEnv 继承 appPublic.dictObject.DictObject(dict 子类 Singleton),
|
||||
# `vars(_ns)` 返回实例 __dict__(实测为空)→ 旧写法 globals().update(vars(_ns)) 实际注入 0 项,
|
||||
# shim 形同虚设(5cffb7b 实测记载)。现统一改为**映射取值解析**:
|
||||
# `_resolve_delegate(name)` 依次 globals().get(name) → env.get(name) → getattr(env, name, None);
|
||||
# 并在 env 为映射时按 `env.items()`(不是 vars())批量导出到 globals。全 74 个端点共用同一段。
|
||||
# Bug Dsgk7Hflcyyf_p9ILXYHg 部署态实测:ahserver 预加载全局(request 等)经 builtins 解析,
|
||||
# 不在 exec-globals 里 —— `globals().get("request")` 恒为 None,旧写法让 5 条契约端点全返 503。
|
||||
# 改为裸名优先(ahserver 语义),globals 取值仅作兼容兜底。
|
||||
_ns = None
|
||||
try:
|
||||
_ns = request._run_ns
|
||||
except Exception:
|
||||
_ns = None
|
||||
if _ns is None:
|
||||
_ns = getattr(globals().get("request"), "_run_ns", None)
|
||||
if _ns is None:
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[ns-shim] dspy 执行命名空间缺少 request._run_ns,`env` 与模块函数无法解析;"
|
||||
"请检查应用 init() 是否完成 ServerEnv 装配并逐个调用 load_{模块}()"}
|
||||
env = _ns
|
||||
|
||||
|
||||
def _resolve_delegate(_name):
|
||||
"""三级映射取值解析委托对象(QC#1 统一内置实现)。
|
||||
|
||||
解析顺序:globals().get(name) → env.get(name) → getattr(env, name, None)。
|
||||
全部未命中返回 None,调用方据此走 503 可行动兜底(不退化为泛化 500)。
|
||||
"""
|
||||
_v = globals().get(_name)
|
||||
if _v is not None:
|
||||
return _v
|
||||
try:
|
||||
_v = env.get(_name)
|
||||
except Exception:
|
||||
_v = None
|
||||
if _v is not None:
|
||||
return _v
|
||||
return getattr(env, _name, None)
|
||||
|
||||
|
||||
def _ns_export():
|
||||
"""把 env 中的名字导出到 dspy globals。
|
||||
|
||||
主路径:映射取值 `env.items()`——ServerEnv 继承 appPublic.dictObject.DictObject(dict 子类),
|
||||
这是唯一可靠注入源(实例 __dict__ 实测为空,故不得只用 vars())。
|
||||
回退路径:env 不是映射时(无 items()),退回 `vars(env)` 读实例 __dict__,
|
||||
以覆盖属性式挂载的命名空间(不回退会把可用命名空间误判成"未注册"→假 503)。
|
||||
"""
|
||||
_items = None
|
||||
try:
|
||||
_items = env.items()
|
||||
except Exception:
|
||||
_items = None
|
||||
if _items is None:
|
||||
try:
|
||||
_items = vars(env).items()
|
||||
except Exception:
|
||||
_items = None
|
||||
if _items is None:
|
||||
return
|
||||
for _k, _v in list(_items):
|
||||
if not isinstance(_k, str) or _k.startswith("__") or _k == "params_kw":
|
||||
continue
|
||||
globals()[_k] = _v
|
||||
|
||||
|
||||
_ns_export()
|
||||
for _ldrname in ("load_entity", "load_scene", "load_scense", "load_scense_game",
|
||||
"load_world", "load_world_sync", "load_world_snapshot",
|
||||
"load_runtime", "load_script_engine", "load_sys_ops"):
|
||||
_ldr = _resolve_delegate(_ldrname)
|
||||
if _ldr is None:
|
||||
continue
|
||||
try:
|
||||
_loaded = _ldr()
|
||||
if hasattr(_loaded, "__await__"):
|
||||
await _loaded
|
||||
except Exception:
|
||||
pass
|
||||
_ns_export()
|
||||
|
||||
|
||||
def _dspy_missing(_name):
|
||||
"""未注册模块函数的统一兜底:503 + 可行动提示(不要退化成泛化 500)。"""
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[ns-shim] 模块函数 %s 未注入 dspy 命名空间;"
|
||||
"请确认该模块 init() 已 env.%s = %s 且应用已 load_{模块}() 挂载" % (_name, _name, _name)}
|
||||
# [/ns-shim]
|
||||
# -*- coding: utf-8 -*-
|
||||
# runtime/api/build_entity_tree.dspy —— W-10a 运行态实体树构建
|
||||
# 入参(二选一,可同时给):
|
||||
# entities : 实体数组(对象数组或 JSON 字符串)——纯函数路径,无 DB 依赖
|
||||
# world_id : 世界 id —— 服务端路径:先按真实 world_id 读实体,再构树
|
||||
# 出参:{ok, roots, index, count};任何异常降级返回,绝不 500
|
||||
# (Bug aylOIQTBs7w5MpuSCxn7I:旧版 build_entity_tree 在空输入时返回 list,
|
||||
# 端点 result['ok']=True 触发 "list indices must be integers or slices, not str",
|
||||
# 被降级兜底吞掉 → roots 恒为空。本版强制 dict 口径并支持 world_id 直读。)
|
||||
|
||||
|
||||
def _norm_tree(v):
|
||||
"""把任意形态的构树结果(None / list / dict)统一成 {roots,index,count}。"""
|
||||
if isinstance(v, dict):
|
||||
roots = v.get('roots')
|
||||
index = v.get('index')
|
||||
count = v.get('count')
|
||||
if not isinstance(roots, list):
|
||||
roots = []
|
||||
if not isinstance(index, dict):
|
||||
index = {}
|
||||
try:
|
||||
count = int(count)
|
||||
except Exception:
|
||||
count = len(index)
|
||||
return {'roots': roots, 'index': index, 'count': count}
|
||||
if isinstance(v, (list, tuple)):
|
||||
# 兼容旧版返回值:list 即根节点数组
|
||||
return {'roots': list(v), 'index': {}, 'count': len(v)}
|
||||
return {'roots': [], 'index': {}, 'count': 0}
|
||||
|
||||
|
||||
try:
|
||||
entities = params_kw.get('entities')
|
||||
if entities is None:
|
||||
entities = params_kw.get('entity_json')
|
||||
if entities is None:
|
||||
entities = params_kw.get('data')
|
||||
if isinstance(entities, str):
|
||||
try:
|
||||
entities = json.loads(entities)
|
||||
except Exception:
|
||||
entities = []
|
||||
if entities is not None and not isinstance(entities, list):
|
||||
entities = []
|
||||
|
||||
world_id = params_kw.get('world_id') or params_kw.get('id') or ''
|
||||
loaded = None
|
||||
|
||||
# 未显式给 entities 且给了 world_id -> 按真实 world_id 从库里读实体(服务端路径)
|
||||
if not entities and world_id:
|
||||
loaded = await load_world_runtime(world_id=str(world_id))
|
||||
if isinstance(loaded, dict):
|
||||
got = loaded.get('entities')
|
||||
if isinstance(got, list):
|
||||
entities = got
|
||||
|
||||
tree = _norm_tree(build_entity_tree(entities or []))
|
||||
tree['ok'] = True
|
||||
tree['degraded'] = False
|
||||
tree['count'] = int(tree.get('count') or 0)
|
||||
if world_id:
|
||||
tree['world_id'] = str(world_id)
|
||||
if isinstance(loaded, dict):
|
||||
if loaded.get('degraded'):
|
||||
tree['degraded'] = True
|
||||
tree['reason'] = 'entity source degraded: ' + str(loaded.get('message') or '')
|
||||
tree['entity_count'] = len(entities or [])
|
||||
return tree
|
||||
except Exception as e:
|
||||
return {'ok': True, 'degraded': True, 'roots': [], 'index': {}, 'count': 0,
|
||||
'message': 'build_entity_tree degraded: ' + str(e)}
|
||||
221
modules/runtime/wwwroot/api/runtime_status.dspy
Normal file
221
modules/runtime/wwwroot/api/runtime_status.dspy
Normal file
@ -0,0 +1,221 @@
|
||||
# [fix-iter3 strict-auth v2] fail-closed 门禁(Bug 1BoEurcDgf4HHRqBMmJt8 / R-01-neg,P0)——勿删
|
||||
# 本段取代「globals().get("get_user") 判空」的旧判据:ahserver 的 dspy 预加载全局经
|
||||
# builtins 解析、不在 exec-globals 里,globals().get 恒为 None → 判据失真 → 匿名 200。
|
||||
# 口径:裸名取值 → 双重判空(get_user + get_userinfo,且必须取到有效用户标识)
|
||||
# → 未登录返回真 HTTP 401(Response 解析不到时 set_status(401)+JSON,绝不返回业务数据)。
|
||||
_auth_uid = ""
|
||||
|
||||
|
||||
def _auth_uid_of(_v):
|
||||
"""从 get_user()/get_userinfo() 的返回值里提取有效用户标识;取不到返回 ''。"""
|
||||
if not _v:
|
||||
return ""
|
||||
if isinstance(_v, str):
|
||||
return _v
|
||||
if isinstance(_v, dict):
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
|
||||
|
||||
def _auth_call(_name):
|
||||
"""裸名优先取预加载全局(builtins 解析),退 globals 映射。取不到返回 None。"""
|
||||
_f = None
|
||||
try:
|
||||
_f = globals().get(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
if not callable(_f):
|
||||
try:
|
||||
_f = _auth_bare(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
return _f if callable(_f) else None
|
||||
|
||||
|
||||
def _auth_bare(_name):
|
||||
"""裸名解析(ahserver 预加载全局在 builtins 中);未注入时抛 NameError。"""
|
||||
if _name == "get_user":
|
||||
return get_user
|
||||
if _name == "get_userinfo":
|
||||
return get_userinfo
|
||||
if _name == "get_username":
|
||||
return get_username
|
||||
raise NameError(_name)
|
||||
|
||||
|
||||
async def _auth_resolve_uid():
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
_f = _auth_call(_n)
|
||||
if _f is None:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
_r = None
|
||||
_u = _auth_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
try:
|
||||
_auth_uid = await _auth_resolve_uid()
|
||||
except Exception:
|
||||
_auth_uid = ""
|
||||
|
||||
if not _auth_uid:
|
||||
_auth_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"reason": "login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_auth_resp_cls = None
|
||||
try:
|
||||
_auth_proc = globals().get("processor")
|
||||
if _auth_proc is None:
|
||||
_auth_proc = processor
|
||||
_auth_resp_cls = _auth_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_auth_resp_cls = None
|
||||
if _auth_resp_cls is not None:
|
||||
return _auth_resp_cls(
|
||||
body=json.dumps(_auth_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
request.set_status(401)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return json.dumps(_auth_deny, ensure_ascii=False)
|
||||
except Exception:
|
||||
return _auth_deny
|
||||
# [fix-iter3 strict-auth v2] end
|
||||
# [ns-shim] 统一注入 dspy 执行命名空间(Bug qVMyATuR2Z7UuN2P9pbzp / Ua43R1DMO8-7tCb9ybwo8 / j_rE23TAyqp0OV20OTDwv / iFsP7Yklktq_AixTnEWYi)
|
||||
# 根因:ahserver 的 dspy exec 命名空间不保证注入 `env`(ServerEnv 实例)与模块函数
|
||||
# (create_entity / list_entities / *_import …),直写裸名会 NameError,或把 None 拼进 SQL
|
||||
# ('select None as None from None')→ 泛化 500。
|
||||
# 注入方式(QC#1 整改):ServerEnv 继承 appPublic.dictObject.DictObject(dict 子类 Singleton),
|
||||
# `vars(_ns)` 返回实例 __dict__(实测为空)→ 旧写法 globals().update(vars(_ns)) 实际注入 0 项,
|
||||
# shim 形同虚设(5cffb7b 实测记载)。现统一改为**映射取值解析**:
|
||||
# `_resolve_delegate(name)` 依次 globals().get(name) → env.get(name) → getattr(env, name, None);
|
||||
# 并在 env 为映射时按 `env.items()`(不是 vars())批量导出到 globals。全 74 个端点共用同一段。
|
||||
# Bug Dsgk7Hflcyyf_p9ILXYHg 部署态实测:ahserver 预加载全局(request 等)经 builtins 解析,
|
||||
# 不在 exec-globals 里 —— `globals().get("request")` 恒为 None,旧写法让 5 条契约端点全返 503。
|
||||
# 改为裸名优先(ahserver 语义),globals 取值仅作兼容兜底。
|
||||
_ns = None
|
||||
try:
|
||||
_ns = request._run_ns
|
||||
except Exception:
|
||||
_ns = None
|
||||
if _ns is None:
|
||||
_ns = getattr(globals().get("request"), "_run_ns", None)
|
||||
if _ns is None:
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[ns-shim] dspy 执行命名空间缺少 request._run_ns,`env` 与模块函数无法解析;"
|
||||
"请检查应用 init() 是否完成 ServerEnv 装配并逐个调用 load_{模块}()"}
|
||||
env = _ns
|
||||
|
||||
|
||||
def _resolve_delegate(_name):
|
||||
"""三级映射取值解析委托对象(QC#1 统一内置实现)。
|
||||
|
||||
解析顺序:globals().get(name) → env.get(name) → getattr(env, name, None)。
|
||||
全部未命中返回 None,调用方据此走 503 可行动兜底(不退化为泛化 500)。
|
||||
"""
|
||||
_v = globals().get(_name)
|
||||
if _v is not None:
|
||||
return _v
|
||||
try:
|
||||
_v = env.get(_name)
|
||||
except Exception:
|
||||
_v = None
|
||||
if _v is not None:
|
||||
return _v
|
||||
return getattr(env, _name, None)
|
||||
|
||||
|
||||
def _ns_export():
|
||||
"""把 env 中的名字导出到 dspy globals。
|
||||
|
||||
主路径:映射取值 `env.items()`——ServerEnv 继承 appPublic.dictObject.DictObject(dict 子类),
|
||||
这是唯一可靠注入源(实例 __dict__ 实测为空,故不得只用 vars())。
|
||||
回退路径:env 不是映射时(无 items()),退回 `vars(env)` 读实例 __dict__,
|
||||
以覆盖属性式挂载的命名空间(不回退会把可用命名空间误判成"未注册"→假 503)。
|
||||
"""
|
||||
_items = None
|
||||
try:
|
||||
_items = env.items()
|
||||
except Exception:
|
||||
_items = None
|
||||
if _items is None:
|
||||
try:
|
||||
_items = vars(env).items()
|
||||
except Exception:
|
||||
_items = None
|
||||
if _items is None:
|
||||
return
|
||||
for _k, _v in list(_items):
|
||||
if not isinstance(_k, str) or _k.startswith("__") or _k == "params_kw":
|
||||
continue
|
||||
globals()[_k] = _v
|
||||
|
||||
|
||||
_ns_export()
|
||||
for _ldrname in ("load_entity", "load_scene", "load_scense", "load_scense_game",
|
||||
"load_world", "load_world_sync", "load_world_snapshot",
|
||||
"load_runtime", "load_script_engine", "load_sys_ops"):
|
||||
_ldr = _resolve_delegate(_ldrname)
|
||||
if _ldr is None:
|
||||
continue
|
||||
try:
|
||||
_loaded = _ldr()
|
||||
if hasattr(_loaded, "__await__"):
|
||||
await _loaded
|
||||
except Exception:
|
||||
pass
|
||||
_ns_export()
|
||||
|
||||
|
||||
def _dspy_missing(_name):
|
||||
"""未注册模块函数的统一兜底:503 + 可行动提示(不要退化成泛化 500)。"""
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[ns-shim] 模块函数 %s 未注入 dspy 命名空间;"
|
||||
"请确认该模块 init() 已 env.%s = %s 且应用已 load_{模块}() 挂载" % (_name, _name, _name)}
|
||||
# [/ns-shim]
|
||||
# -*- coding: utf-8 -*-
|
||||
# runtime/api/runtime_status.dspy — W-10g 运行时状态查询(服务端辅助,可选)
|
||||
try:
|
||||
runtime_id = params_kw.get('runtime_id') or params_kw.get('world_id') or 'local'
|
||||
result = await runtime_status(runtime_id=runtime_id)
|
||||
if result is None:
|
||||
result = {'ok': True, 'runtime_id': runtime_id, 'running': False,
|
||||
'paused': False, 'mode': 'independent', 'reason': 'empty result'}
|
||||
return result
|
||||
except Exception as e:
|
||||
return {'ok': True, 'degraded': True, 'runtime_id': 'local', 'running': False, 'paused': False,
|
||||
'mode': 'independent', 'message': 'status degraded: ' + str(e)}
|
||||
391
modules/scene/wwwroot/api/get_search_scene_type.dspy
Normal file
391
modules/scene/wwwroot/api/get_search_scene_type.dspy
Normal file
@ -0,0 +1,391 @@
|
||||
# [fix-iter17 auth-gate v3] fail-closed 登录态门禁 —— 勿删
|
||||
# 归属 Bug:OmWRbpwPZRpNe9r2hkKWC / uabpjOGelxJTknG9vqO9V / G-l57of8enoipnJDg17Uz
|
||||
# 口径:本块必须是文件第一条可执行语句(在命名空间 shim / 业务分支之前),任何匿名请求
|
||||
# (无有效会话)一律返回真 HTTP 401,**不返回任何业务数据**;Response 类解析不到时
|
||||
# raise(→500)硬失败,绝不降级为 200——R26~R27 的两次放行均源于「判据失真 + 降级返回」。
|
||||
def _r17_uid_of(_v):
|
||||
if not _v:
|
||||
return ""
|
||||
_BAD = ("", "none", "null", "anonymous", "anonymoususer", "anon", "0")
|
||||
if isinstance(_v, str):
|
||||
_s = _v.strip()
|
||||
return "" if _s.lower() in _BAD else _s
|
||||
if isinstance(_v, (list, tuple, set, bytes, bytearray)):
|
||||
return ""
|
||||
_KEYS = ("id", "userid", "user_id", "user", "username", "name", "uid", "account")
|
||||
if isinstance(_v, dict):
|
||||
for _k in _KEYS:
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
_s = str(_x).strip()
|
||||
if _s.lower() not in _BAD:
|
||||
return _s
|
||||
return ""
|
||||
for _k in _KEYS:
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
_s = str(_x).strip()
|
||||
if _s.lower() not in _BAD:
|
||||
return _s
|
||||
return ""
|
||||
|
||||
|
||||
async def _r17_uid():
|
||||
"""取登录用户标识;取不到返回 ''。裸名解析(ahserver 预加载全局在 builtins)。"""
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
try:
|
||||
if _n == "get_userinfo":
|
||||
_f = get_userinfo
|
||||
elif _n == "get_user":
|
||||
_f = get_user
|
||||
else:
|
||||
_f = get_username
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
continue
|
||||
_u = _r17_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
_r17_uid_val = ""
|
||||
try:
|
||||
_r17_uid_val = await _r17_uid()
|
||||
except Exception:
|
||||
_r17_uid_val = ""
|
||||
|
||||
if not _r17_uid_val:
|
||||
_r17_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_r17_proc = None
|
||||
try:
|
||||
_r17_proc = processor
|
||||
except Exception:
|
||||
_r17_proc = globals().get("processor")
|
||||
_r17_rc = None
|
||||
try:
|
||||
if _r17_proc is not None:
|
||||
_r17_rc = _r17_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_r17_rc = None
|
||||
if _r17_rc is None:
|
||||
raise RuntimeError(
|
||||
"[fix-iter17 auth-gate v3] 未登录且无法解析 aiohttp Response:门禁 hard-fail"
|
||||
"(宁 500 不放行,绝不返回业务数据)"
|
||||
)
|
||||
return _r17_rc(
|
||||
body=json.dumps(_r17_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store", "X-Scene-Auth": "denied"},
|
||||
content_type="application/json",
|
||||
)
|
||||
# [fix-iter17 auth-gate v3] end
|
||||
|
||||
# [fix-iter3 strict-auth v2] fail-closed 门禁(Bug 1BoEurcDgf4HHRqBMmJt8 / R-01-neg,P0)——勿删
|
||||
# 本段取代「globals().get("get_user") 判空」的旧判据:ahserver 的 dspy 预加载全局经
|
||||
# builtins 解析、不在 exec-globals 里,globals().get 恒为 None → 判据失真 → 匿名 200。
|
||||
# 口径:裸名取值 → 双重判空(get_user + get_userinfo,且必须取到有效用户标识)
|
||||
# → 未登录返回真 HTTP 401(Response 解析不到时 set_status(401)+JSON,绝不返回业务数据)。
|
||||
_auth_uid = ""
|
||||
|
||||
|
||||
def _auth_uid_of(_v):
|
||||
"""从 get_user()/get_userinfo() 的返回值里提取有效用户标识;取不到返回 ''。"""
|
||||
if not _v:
|
||||
return ""
|
||||
if isinstance(_v, str):
|
||||
return _v
|
||||
if isinstance(_v, dict):
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
|
||||
|
||||
def _auth_call(_name):
|
||||
"""裸名优先取预加载全局(builtins 解析),退 globals 映射。取不到返回 None。"""
|
||||
_f = None
|
||||
try:
|
||||
_f = globals().get(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
if not callable(_f):
|
||||
try:
|
||||
_f = _auth_bare(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
return _f if callable(_f) else None
|
||||
|
||||
|
||||
def _auth_bare(_name):
|
||||
"""裸名解析(ahserver 预加载全局在 builtins 中);未注入时抛 NameError。"""
|
||||
if _name == "get_user":
|
||||
return get_user
|
||||
if _name == "get_userinfo":
|
||||
return get_userinfo
|
||||
if _name == "get_username":
|
||||
return get_username
|
||||
raise NameError(_name)
|
||||
|
||||
|
||||
async def _auth_resolve_uid():
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
_f = _auth_call(_n)
|
||||
if _f is None:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
_r = None
|
||||
_u = _auth_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
try:
|
||||
_auth_uid = await _auth_resolve_uid()
|
||||
except Exception:
|
||||
_auth_uid = ""
|
||||
|
||||
if not _auth_uid:
|
||||
_auth_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"reason": "login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_auth_resp_cls = None
|
||||
try:
|
||||
_auth_proc = globals().get("processor")
|
||||
if _auth_proc is None:
|
||||
_auth_proc = processor
|
||||
_auth_resp_cls = _auth_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_auth_resp_cls = None
|
||||
if _auth_resp_cls is not None:
|
||||
return _auth_resp_cls(
|
||||
body=json.dumps(_auth_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
request.set_status(401)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return json.dumps(_auth_deny, ensure_ascii=False)
|
||||
except Exception:
|
||||
return _auth_deny
|
||||
# [fix-iter3 strict-auth v2] end
|
||||
# [scene-auth-guard] R26-05/06/07/09 —— scene 端点登录态门禁(fail-closed,勿删)
|
||||
# 背景:R26 复测(plan 0wtjrw6Gn3WG5ewb-BaDR)实测**匿名(不带会话 cookie)**请求本模块
|
||||
# api/*.dspy 返回 HTTP 200 + 业务数据(get_search_status / get_search_scene_type /
|
||||
# get_search_world_id 三个读类端点,以及 scene_import 写入口)。
|
||||
# 根因:scene 的 RBAC 路径已在 scripts/load_path.py 逐条登记(10 个 .dspy 全登记、无通配符),
|
||||
# 但 ahserver 的 dspy 处理器(PythonScriptProcessor)只注入 get_user 等工具函数、不做鉴权;
|
||||
# 权限表登记 != 端点有登录态校验 → 未登录仍可直连 dspy 拿到业务数据。故补**入口侧**门禁(纵深防御)。
|
||||
# 口径(QC #12 收口):
|
||||
# * await get_user()(ahserver 注入,async;无会话时返回 None/空)判登录态;
|
||||
# 取不到再退 get_username();两者都空 → 拒绝;
|
||||
# * 拒绝时返回**真 HTTP 401**(aiohttp Response;BaseProcessor.handle 对 Response 原样透传),
|
||||
# body 只含 status/success/need_login/message 与空 rows/data,**不含任何业务数据**;
|
||||
# * dspy 禁 import,Response 类从已注入的 processor 的对象模块 globals 解析(ahserver
|
||||
# processorResource 模块级已 from aiohttp.web_response import Response);
|
||||
# * **无降级分支**:Response 解析失败一律 `raise RuntimeError` 硬失败(宁 500 不放行)。
|
||||
# 旧版曾降级为返回 dict {"status":401,...},ahserver 会把 dict 包成 HTTP 200 的 JSON body
|
||||
# ("200 + 语义 401"),鉴权强度落空,且离线 harness 覆盖不到该路径 —— 已按 QC #12 方案(b) 删除。
|
||||
try:
|
||||
_scene_au = None
|
||||
_scene_gu = globals().get("get_user")
|
||||
if callable(_scene_gu):
|
||||
_scene_au = _scene_gu()
|
||||
if hasattr(_scene_au, "__await__"):
|
||||
_scene_au = await _scene_au
|
||||
if not _scene_au:
|
||||
_scene_gn = globals().get("get_username")
|
||||
if callable(_scene_gn):
|
||||
_scene_au = _scene_gn()
|
||||
if hasattr(_scene_au, "__await__"):
|
||||
_scene_au = await _scene_au
|
||||
except Exception:
|
||||
_scene_au = None
|
||||
|
||||
if not _scene_au:
|
||||
_scene_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"need_login": True,
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
"message": "[scene] 未登录或会话已失效:本接口需要登录态,请先登录后访问",
|
||||
}
|
||||
_scene_resp_cls = None
|
||||
try:
|
||||
_scene_proc = globals().get("processor")
|
||||
_scene_resp_cls = _scene_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_scene_resp_cls = None
|
||||
if _scene_resp_cls is None:
|
||||
raise RuntimeError(
|
||||
"[scene-auth-guard] 未登录且无法解析 aiohttp Response:门禁 hard-fail"
|
||||
"(宁 500 不放行,绝不返回业务数据)"
|
||||
)
|
||||
return _scene_resp_cls(
|
||||
body=json.dumps(_scene_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"X-Scene-Auth": "denied", "Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
# [/scene-auth-guard]
|
||||
|
||||
# [scene/options] 场景类型下拉端点 get_search_scene_type.dspy —— 读类 500 归零修复版(R27)
|
||||
# Bug _ViEgLj09USW3JU_79r5a(与 get_search_status 同根因)/ 3x-k0LCAk32WcM4ao5xQo
|
||||
# 根因:旧实现委托 scene/init.py::get_scene_type_options(),对 sor.R(..., {"page":1,"rows":1000})
|
||||
# 返回的分页信封 {'total','rows'} 直接 for 迭代 → 迭代出 str 键 → 'str' has no attribute 'k';
|
||||
# params_kw 为 None/str 时取属性同样抛 AttributeError → 框架级 500(无 JSON 体)。
|
||||
# 修复:端点自包含 + 取参归一 _as_params() + 行归一 _rows_of()(分页信封只取 rows)
|
||||
# + 列取值兼容 k/v|value/text + 全路径异常返回可行动 JSON(400/503,绝不上抛)。
|
||||
# 库名:get_sor_context(env, "scene"),运行时由 env.get_module_dbname 解析(无硬编码 DBNAME)。
|
||||
try:
|
||||
_env = request._run_ns
|
||||
except Exception:
|
||||
_env = None
|
||||
if _env is None:
|
||||
try:
|
||||
_env = getattr(globals().get("request"), "_run_ns", None)
|
||||
except Exception:
|
||||
_env = None
|
||||
if _env is None:
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[scene/get_search_scene_type] dspy 执行命名空间缺少 request._run_ns;"
|
||||
"请确认应用 init() 已完成 ServerEnv 装配并 load_scene()"}
|
||||
|
||||
|
||||
def _as_params(_raw):
|
||||
if _raw is None:
|
||||
return {}
|
||||
if isinstance(_raw, dict):
|
||||
return dict(_raw)
|
||||
if isinstance(_raw, (bytes, bytearray)):
|
||||
try:
|
||||
_raw = _raw.decode("utf-8", "replace")
|
||||
except Exception:
|
||||
return None
|
||||
if isinstance(_raw, str):
|
||||
_t = _raw.strip()
|
||||
if not _t:
|
||||
return {}
|
||||
try:
|
||||
_v = json.loads(_t)
|
||||
except Exception:
|
||||
return None
|
||||
return dict(_v) if isinstance(_v, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _rows_of(_res):
|
||||
if _res is None:
|
||||
return []
|
||||
if isinstance(_res, dict):
|
||||
_r = _res.get("rows")
|
||||
if isinstance(_r, list):
|
||||
return _r
|
||||
if _r is not None:
|
||||
return [_r]
|
||||
return [_res] if _res else []
|
||||
if isinstance(_res, (list, tuple)):
|
||||
return list(_res)
|
||||
try:
|
||||
return list(_res)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _get(_row, _name):
|
||||
try:
|
||||
if isinstance(_row, dict):
|
||||
return _row.get(_name)
|
||||
if hasattr(_row, _name):
|
||||
return getattr(_row, _name)
|
||||
return _row[_name]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _pick(_row, _names):
|
||||
for _n in _names:
|
||||
_v = _get(_row, _n)
|
||||
if _v is not None and str(_v).strip() != "":
|
||||
return _v
|
||||
return None
|
||||
|
||||
|
||||
async def _load_options(_parentid):
|
||||
async with get_sor_context(_env, "scene") as _sor:
|
||||
_res = await _sor.sqlExe(
|
||||
"SELECT k, v FROM appcodes_kv WHERE parentid=${parentid}$ ORDER BY v",
|
||||
{"parentid": _parentid})
|
||||
_out = []
|
||||
for _r in _rows_of(_res):
|
||||
_val = _pick(_r, ("k", "value", "id"))
|
||||
_txt = _pick(_r, ("v", "text", "name"))
|
||||
if _val is None:
|
||||
continue
|
||||
_out.append({"value": _val, "text": _txt if _txt is not None else _val})
|
||||
return _out
|
||||
|
||||
|
||||
_params = _as_params(params_kw)
|
||||
if _params is None:
|
||||
return {"status": 400, "success": False, "rows": [],
|
||||
"message": "[scene/get_search_scene_type] 参数类型非法:params 必须是 JSON 对象"}
|
||||
|
||||
try:
|
||||
_opts = await _load_options("scene_type")
|
||||
return {"status": 200, "success": True, "total": len(_opts), "rows": _opts}
|
||||
except Exception as _e:
|
||||
return {"status": 503, "success": False, "rows": [],
|
||||
"message": "[scene/get_search_scene_type] 读取 appcodes_kv(parentid=scene_type) 失败: "
|
||||
+ str(_e)}
|
||||
402
modules/scene/wwwroot/api/get_search_status.dspy
Normal file
402
modules/scene/wwwroot/api/get_search_status.dspy
Normal file
@ -0,0 +1,402 @@
|
||||
# [fix-iter17 auth-gate v3] fail-closed 登录态门禁 —— 勿删
|
||||
# 归属 Bug:OmWRbpwPZRpNe9r2hkKWC / uabpjOGelxJTknG9vqO9V / G-l57of8enoipnJDg17Uz
|
||||
# 口径:本块必须是文件第一条可执行语句(在命名空间 shim / 业务分支之前),任何匿名请求
|
||||
# (无有效会话)一律返回真 HTTP 401,**不返回任何业务数据**;Response 类解析不到时
|
||||
# raise(→500)硬失败,绝不降级为 200——R26~R27 的两次放行均源于「判据失真 + 降级返回」。
|
||||
def _r17_uid_of(_v):
|
||||
if not _v:
|
||||
return ""
|
||||
_BAD = ("", "none", "null", "anonymous", "anonymoususer", "anon", "0")
|
||||
if isinstance(_v, str):
|
||||
_s = _v.strip()
|
||||
return "" if _s.lower() in _BAD else _s
|
||||
if isinstance(_v, (list, tuple, set, bytes, bytearray)):
|
||||
return ""
|
||||
_KEYS = ("id", "userid", "user_id", "user", "username", "name", "uid", "account")
|
||||
if isinstance(_v, dict):
|
||||
for _k in _KEYS:
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
_s = str(_x).strip()
|
||||
if _s.lower() not in _BAD:
|
||||
return _s
|
||||
return ""
|
||||
for _k in _KEYS:
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
_s = str(_x).strip()
|
||||
if _s.lower() not in _BAD:
|
||||
return _s
|
||||
return ""
|
||||
|
||||
|
||||
async def _r17_uid():
|
||||
"""取登录用户标识;取不到返回 ''。裸名解析(ahserver 预加载全局在 builtins)。"""
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
try:
|
||||
if _n == "get_userinfo":
|
||||
_f = get_userinfo
|
||||
elif _n == "get_user":
|
||||
_f = get_user
|
||||
else:
|
||||
_f = get_username
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
continue
|
||||
_u = _r17_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
_r17_uid_val = ""
|
||||
try:
|
||||
_r17_uid_val = await _r17_uid()
|
||||
except Exception:
|
||||
_r17_uid_val = ""
|
||||
|
||||
if not _r17_uid_val:
|
||||
_r17_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_r17_proc = None
|
||||
try:
|
||||
_r17_proc = processor
|
||||
except Exception:
|
||||
_r17_proc = globals().get("processor")
|
||||
_r17_rc = None
|
||||
try:
|
||||
if _r17_proc is not None:
|
||||
_r17_rc = _r17_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_r17_rc = None
|
||||
if _r17_rc is None:
|
||||
raise RuntimeError(
|
||||
"[fix-iter17 auth-gate v3] 未登录且无法解析 aiohttp Response:门禁 hard-fail"
|
||||
"(宁 500 不放行,绝不返回业务数据)"
|
||||
)
|
||||
return _r17_rc(
|
||||
body=json.dumps(_r17_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store", "X-Scene-Auth": "denied"},
|
||||
content_type="application/json",
|
||||
)
|
||||
# [fix-iter17 auth-gate v3] end
|
||||
|
||||
# [fix-iter3 strict-auth v2] fail-closed 门禁(Bug 1BoEurcDgf4HHRqBMmJt8 / R-01-neg,P0)——勿删
|
||||
# 本段取代「globals().get("get_user") 判空」的旧判据:ahserver 的 dspy 预加载全局经
|
||||
# builtins 解析、不在 exec-globals 里,globals().get 恒为 None → 判据失真 → 匿名 200。
|
||||
# 口径:裸名取值 → 双重判空(get_user + get_userinfo,且必须取到有效用户标识)
|
||||
# → 未登录返回真 HTTP 401(Response 解析不到时 set_status(401)+JSON,绝不返回业务数据)。
|
||||
_auth_uid = ""
|
||||
|
||||
|
||||
def _auth_uid_of(_v):
|
||||
"""从 get_user()/get_userinfo() 的返回值里提取有效用户标识;取不到返回 ''。"""
|
||||
if not _v:
|
||||
return ""
|
||||
if isinstance(_v, str):
|
||||
return _v
|
||||
if isinstance(_v, dict):
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
|
||||
|
||||
def _auth_call(_name):
|
||||
"""裸名优先取预加载全局(builtins 解析),退 globals 映射。取不到返回 None。"""
|
||||
_f = None
|
||||
try:
|
||||
_f = globals().get(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
if not callable(_f):
|
||||
try:
|
||||
_f = _auth_bare(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
return _f if callable(_f) else None
|
||||
|
||||
|
||||
def _auth_bare(_name):
|
||||
"""裸名解析(ahserver 预加载全局在 builtins 中);未注入时抛 NameError。"""
|
||||
if _name == "get_user":
|
||||
return get_user
|
||||
if _name == "get_userinfo":
|
||||
return get_userinfo
|
||||
if _name == "get_username":
|
||||
return get_username
|
||||
raise NameError(_name)
|
||||
|
||||
|
||||
async def _auth_resolve_uid():
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
_f = _auth_call(_n)
|
||||
if _f is None:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
_r = None
|
||||
_u = _auth_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
try:
|
||||
_auth_uid = await _auth_resolve_uid()
|
||||
except Exception:
|
||||
_auth_uid = ""
|
||||
|
||||
if not _auth_uid:
|
||||
_auth_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"reason": "login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_auth_resp_cls = None
|
||||
try:
|
||||
_auth_proc = globals().get("processor")
|
||||
if _auth_proc is None:
|
||||
_auth_proc = processor
|
||||
_auth_resp_cls = _auth_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_auth_resp_cls = None
|
||||
if _auth_resp_cls is not None:
|
||||
return _auth_resp_cls(
|
||||
body=json.dumps(_auth_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
request.set_status(401)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return json.dumps(_auth_deny, ensure_ascii=False)
|
||||
except Exception:
|
||||
return _auth_deny
|
||||
# [fix-iter3 strict-auth v2] end
|
||||
# [scene-auth-guard] R26-05/06/07/09 —— scene 端点登录态门禁(fail-closed,勿删)
|
||||
# 背景:R26 复测(plan 0wtjrw6Gn3WG5ewb-BaDR)实测**匿名(不带会话 cookie)**请求本模块
|
||||
# api/*.dspy 返回 HTTP 200 + 业务数据(get_search_status / get_search_scene_type /
|
||||
# get_search_world_id 三个读类端点,以及 scene_import 写入口)。
|
||||
# 根因:scene 的 RBAC 路径已在 scripts/load_path.py 逐条登记(10 个 .dspy 全登记、无通配符),
|
||||
# 但 ahserver 的 dspy 处理器(PythonScriptProcessor)只注入 get_user 等工具函数、不做鉴权;
|
||||
# 权限表登记 != 端点有登录态校验 → 未登录仍可直连 dspy 拿到业务数据。故补**入口侧**门禁(纵深防御)。
|
||||
# 口径(QC #12 收口):
|
||||
# * await get_user()(ahserver 注入,async;无会话时返回 None/空)判登录态;
|
||||
# 取不到再退 get_username();两者都空 → 拒绝;
|
||||
# * 拒绝时返回**真 HTTP 401**(aiohttp Response;BaseProcessor.handle 对 Response 原样透传),
|
||||
# body 只含 status/success/need_login/message 与空 rows/data,**不含任何业务数据**;
|
||||
# * dspy 禁 import,Response 类从已注入的 processor 的对象模块 globals 解析(ahserver
|
||||
# processorResource 模块级已 from aiohttp.web_response import Response);
|
||||
# * **无降级分支**:Response 解析失败一律 `raise RuntimeError` 硬失败(宁 500 不放行)。
|
||||
# 旧版曾降级为返回 dict {"status":401,...},ahserver 会把 dict 包成 HTTP 200 的 JSON body
|
||||
# ("200 + 语义 401"),鉴权强度落空,且离线 harness 覆盖不到该路径 —— 已按 QC #12 方案(b) 删除。
|
||||
try:
|
||||
_scene_au = None
|
||||
_scene_gu = globals().get("get_user")
|
||||
if callable(_scene_gu):
|
||||
_scene_au = _scene_gu()
|
||||
if hasattr(_scene_au, "__await__"):
|
||||
_scene_au = await _scene_au
|
||||
if not _scene_au:
|
||||
_scene_gn = globals().get("get_username")
|
||||
if callable(_scene_gn):
|
||||
_scene_au = _scene_gn()
|
||||
if hasattr(_scene_au, "__await__"):
|
||||
_scene_au = await _scene_au
|
||||
except Exception:
|
||||
_scene_au = None
|
||||
|
||||
if not _scene_au:
|
||||
_scene_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"need_login": True,
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
"message": "[scene] 未登录或会话已失效:本接口需要登录态,请先登录后访问",
|
||||
}
|
||||
_scene_resp_cls = None
|
||||
try:
|
||||
_scene_proc = globals().get("processor")
|
||||
_scene_resp_cls = _scene_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_scene_resp_cls = None
|
||||
if _scene_resp_cls is None:
|
||||
raise RuntimeError(
|
||||
"[scene-auth-guard] 未登录且无法解析 aiohttp Response:门禁 hard-fail"
|
||||
"(宁 500 不放行,绝不返回业务数据)"
|
||||
)
|
||||
return _scene_resp_cls(
|
||||
body=json.dumps(_scene_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"X-Scene-Auth": "denied", "Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
# [/scene-auth-guard]
|
||||
|
||||
# [scene/options] 场景状态下拉端点 get_search_status.dspy —— 读类 500 归零修复版(R27)
|
||||
# Bug _ViEgLj09USW3JU_79r5a:GET /scene/api/get_search_status.dspy → 500(无 JSON 体)
|
||||
# Bug 3x-k0LCAk32WcM4ao5xQo:同端点列/取参根因同族
|
||||
# 根因(旧实现):
|
||||
# 1) 委托 scene/init.py::get_scene_status_options(),其内部
|
||||
# recs = await sor.R("appcodes_kv", {"parentid":"scene_status","page":1,"rows":1000})
|
||||
# 带 page/rows 的查询返回 {'total':N,'rows':[...]},代码直接 for 迭代 dict → 得到 str 键
|
||||
# [{"value": r.k, ...} for r in recs] → AttributeError: 'str' object has no attribute 'k'
|
||||
# 2) params_kw 为 None/str 时对参数取属性 → 'NoneType' object has no attribute 'get'
|
||||
# 3) 旧实现把异常上抛给框架 → 响应体退化为 "Server got itself in trouble"
|
||||
# 修复:
|
||||
# - 端点自包含,不再依赖模块侧委托函数(避免 init.py 与本文件语义漂移)
|
||||
# - 取参归一 _as_params():None/空串 → {},JSON 串 → dict,非法类型 → 400(不再 500)
|
||||
# - 行归一 _rows_of():显式识别 {'total','rows'} 分页信封,禁止直接迭代 dict
|
||||
# - 列取值 _pick():兼容 k/v 与 value/text 两种列命名
|
||||
# - 全路径 try/except,DB 异常返回可行动 503 JSON,绝不抛到框架层
|
||||
# 库名:get_sor_context(env, "scene") → 运行时由 env.get_module_dbname 解析,无硬编码 DBNAME。
|
||||
try:
|
||||
_env = request._run_ns
|
||||
except Exception:
|
||||
_env = None
|
||||
if _env is None:
|
||||
try:
|
||||
_env = getattr(globals().get("request"), "_run_ns", None)
|
||||
except Exception:
|
||||
_env = None
|
||||
if _env is None:
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[scene/get_search_status] dspy 执行命名空间缺少 request._run_ns;"
|
||||
"请确认应用 init() 已完成 ServerEnv 装配并 load_scene()"}
|
||||
|
||||
|
||||
def _as_params(_raw):
|
||||
"""取参归一:None → {};dict 复制;JSON/空串 → dict/{};其它 → None(非法)。"""
|
||||
if _raw is None:
|
||||
return {}
|
||||
if isinstance(_raw, dict):
|
||||
return dict(_raw)
|
||||
if isinstance(_raw, (bytes, bytearray)):
|
||||
try:
|
||||
_raw = _raw.decode("utf-8", "replace")
|
||||
except Exception:
|
||||
return None
|
||||
if isinstance(_raw, str):
|
||||
_t = _raw.strip()
|
||||
if not _t:
|
||||
return {}
|
||||
try:
|
||||
_v = json.loads(_t)
|
||||
except Exception:
|
||||
return None
|
||||
return dict(_v) if isinstance(_v, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _rows_of(_res):
|
||||
"""把 sor.R / sqlExe 返回值归一为行列表(分页信封只取 rows)。"""
|
||||
if _res is None:
|
||||
return []
|
||||
if isinstance(_res, dict):
|
||||
_r = _res.get("rows")
|
||||
if isinstance(_r, list):
|
||||
return _r
|
||||
if _r is not None:
|
||||
return [_r]
|
||||
return [_res] if _res else []
|
||||
if isinstance(_res, (list, tuple)):
|
||||
return list(_res)
|
||||
try:
|
||||
return list(_res)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _get(_row, _name):
|
||||
try:
|
||||
if isinstance(_row, dict):
|
||||
return _row.get(_name)
|
||||
if hasattr(_row, _name):
|
||||
return getattr(_row, _name)
|
||||
return _row[_name]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _pick(_row, _names):
|
||||
for _n in _names:
|
||||
_v = _get(_row, _n)
|
||||
if _v is not None and str(_v).strip() != "":
|
||||
return _v
|
||||
return None
|
||||
|
||||
|
||||
async def _load_options(_parentid):
|
||||
async with get_sor_context(_env, "scene") as _sor:
|
||||
_res = await _sor.sqlExe(
|
||||
"SELECT k, v FROM appcodes_kv WHERE parentid=${parentid}$ ORDER BY v",
|
||||
{"parentid": _parentid})
|
||||
_out = []
|
||||
for _r in _rows_of(_res):
|
||||
_val = _pick(_r, ("k", "value", "id"))
|
||||
_txt = _pick(_r, ("v", "text", "name"))
|
||||
if _val is None:
|
||||
continue
|
||||
_out.append({"value": _val, "text": _txt if _txt is not None else _val})
|
||||
return _out
|
||||
|
||||
|
||||
_params = _as_params(params_kw)
|
||||
if _params is None:
|
||||
return {"status": 400, "success": False, "rows": [],
|
||||
"message": "[scene/get_search_status] 参数类型非法:params 必须是 JSON 对象"}
|
||||
|
||||
try:
|
||||
_opts = await _load_options("scene_status")
|
||||
return {"status": 200, "success": True, "total": len(_opts), "rows": _opts}
|
||||
except Exception as _e:
|
||||
return {"status": 503, "success": False, "rows": [],
|
||||
"message": "[scene/get_search_status] 读取 appcodes_kv(parentid=scene_status) 失败: "
|
||||
+ str(_e)}
|
||||
348
modules/scense/wwwroot/api/list_scense.dspy
Normal file
348
modules/scense/wwwroot/api/list_scense.dspy
Normal file
@ -0,0 +1,348 @@
|
||||
# [fix-iter17 auth-gate v3] fail-closed 登录态门禁 —— 勿删
|
||||
# 归属 Bug:OmWRbpwPZRpNe9r2hkKWC / uabpjOGelxJTknG9vqO9V / G-l57of8enoipnJDg17Uz
|
||||
# 口径:本块必须是文件第一条可执行语句(在命名空间 shim / 业务分支之前),任何匿名请求
|
||||
# (无有效会话)一律返回真 HTTP 401,**不返回任何业务数据**;Response 类解析不到时
|
||||
# raise(→500)硬失败,绝不降级为 200——R26~R27 的两次放行均源于「判据失真 + 降级返回」。
|
||||
def _r17_uid_of(_v):
|
||||
if not _v:
|
||||
return ""
|
||||
_BAD = ("", "none", "null", "anonymous", "anonymoususer", "anon", "0")
|
||||
if isinstance(_v, str):
|
||||
_s = _v.strip()
|
||||
return "" if _s.lower() in _BAD else _s
|
||||
if isinstance(_v, (list, tuple, set, bytes, bytearray)):
|
||||
return ""
|
||||
_KEYS = ("id", "userid", "user_id", "user", "username", "name", "uid", "account")
|
||||
if isinstance(_v, dict):
|
||||
for _k in _KEYS:
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
_s = str(_x).strip()
|
||||
if _s.lower() not in _BAD:
|
||||
return _s
|
||||
return ""
|
||||
for _k in _KEYS:
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
_s = str(_x).strip()
|
||||
if _s.lower() not in _BAD:
|
||||
return _s
|
||||
return ""
|
||||
|
||||
|
||||
async def _r17_uid():
|
||||
"""取登录用户标识;取不到返回 ''。裸名解析(ahserver 预加载全局在 builtins)。"""
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
try:
|
||||
if _n == "get_userinfo":
|
||||
_f = get_userinfo
|
||||
elif _n == "get_user":
|
||||
_f = get_user
|
||||
else:
|
||||
_f = get_username
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
continue
|
||||
_u = _r17_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
_r17_uid_val = ""
|
||||
try:
|
||||
_r17_uid_val = await _r17_uid()
|
||||
except Exception:
|
||||
_r17_uid_val = ""
|
||||
|
||||
if not _r17_uid_val:
|
||||
_r17_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_r17_proc = None
|
||||
try:
|
||||
_r17_proc = processor
|
||||
except Exception:
|
||||
_r17_proc = globals().get("processor")
|
||||
_r17_rc = None
|
||||
try:
|
||||
if _r17_proc is not None:
|
||||
_r17_rc = _r17_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_r17_rc = None
|
||||
if _r17_rc is None:
|
||||
raise RuntimeError(
|
||||
"[fix-iter17 auth-gate v3] 未登录且无法解析 aiohttp Response:门禁 hard-fail"
|
||||
"(宁 500 不放行,绝不返回业务数据)"
|
||||
)
|
||||
return _r17_rc(
|
||||
body=json.dumps(_r17_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store", "X-Scene-Auth": "denied"},
|
||||
content_type="application/json",
|
||||
)
|
||||
# [fix-iter17 auth-gate v3] end
|
||||
|
||||
# [fix-iter3 strict-auth v2] fail-closed 门禁(Bug 1BoEurcDgf4HHRqBMmJt8 / R-01-neg,P0)——勿删
|
||||
# 本段取代「globals().get("get_user") 判空」的旧判据:ahserver 的 dspy 预加载全局经
|
||||
# builtins 解析、不在 exec-globals 里,globals().get 恒为 None → 判据失真 → 匿名 200。
|
||||
# 口径:裸名取值 → 双重判空(get_user + get_userinfo,且必须取到有效用户标识)
|
||||
# → 未登录返回真 HTTP 401(Response 解析不到时 set_status(401)+JSON,绝不返回业务数据)。
|
||||
_auth_uid = ""
|
||||
|
||||
|
||||
def _auth_uid_of(_v):
|
||||
"""从 get_user()/get_userinfo() 的返回值里提取有效用户标识;取不到返回 ''。"""
|
||||
if not _v:
|
||||
return ""
|
||||
if isinstance(_v, str):
|
||||
return _v
|
||||
if isinstance(_v, dict):
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = _v.get(_k)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"):
|
||||
try:
|
||||
_x = getattr(_v, _k, None)
|
||||
except Exception:
|
||||
_x = None
|
||||
if _x:
|
||||
return str(_x)
|
||||
return ""
|
||||
|
||||
|
||||
def _auth_call(_name):
|
||||
"""裸名优先取预加载全局(builtins 解析),退 globals 映射。取不到返回 None。"""
|
||||
_f = None
|
||||
try:
|
||||
_f = globals().get(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
if not callable(_f):
|
||||
try:
|
||||
_f = _auth_bare(_name)
|
||||
except Exception:
|
||||
_f = None
|
||||
return _f if callable(_f) else None
|
||||
|
||||
|
||||
def _auth_bare(_name):
|
||||
"""裸名解析(ahserver 预加载全局在 builtins 中);未注入时抛 NameError。"""
|
||||
if _name == "get_user":
|
||||
return get_user
|
||||
if _name == "get_userinfo":
|
||||
return get_userinfo
|
||||
if _name == "get_username":
|
||||
return get_username
|
||||
raise NameError(_name)
|
||||
|
||||
|
||||
async def _auth_resolve_uid():
|
||||
for _n in ("get_userinfo", "get_user", "get_username"):
|
||||
_f = _auth_call(_n)
|
||||
if _f is None:
|
||||
continue
|
||||
try:
|
||||
_r = _f()
|
||||
if hasattr(_r, "__await__"):
|
||||
_r = await _r
|
||||
except Exception:
|
||||
_r = None
|
||||
_u = _auth_uid_of(_r)
|
||||
if _u:
|
||||
return _u
|
||||
return ""
|
||||
|
||||
|
||||
try:
|
||||
_auth_uid = await _auth_resolve_uid()
|
||||
except Exception:
|
||||
_auth_uid = ""
|
||||
|
||||
if not _auth_uid:
|
||||
_auth_deny = {
|
||||
"status": 401,
|
||||
"success": False,
|
||||
"ok": False,
|
||||
"need_login": True,
|
||||
"message": "unauthorized: login required",
|
||||
"reason": "login required",
|
||||
"rows": [],
|
||||
"data": [],
|
||||
"total": 0,
|
||||
}
|
||||
_auth_resp_cls = None
|
||||
try:
|
||||
_auth_proc = globals().get("processor")
|
||||
if _auth_proc is None:
|
||||
_auth_proc = processor
|
||||
_auth_resp_cls = _auth_proc.__class__.__init__.__globals__.get("Response")
|
||||
except Exception:
|
||||
_auth_resp_cls = None
|
||||
if _auth_resp_cls is not None:
|
||||
return _auth_resp_cls(
|
||||
body=json.dumps(_auth_deny, ensure_ascii=False).encode("utf-8"),
|
||||
status=401,
|
||||
headers={"Cache-Control": "no-store"},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
request.set_status(401)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return json.dumps(_auth_deny, ensure_ascii=False)
|
||||
except Exception:
|
||||
return _auth_deny
|
||||
# [fix-iter3 strict-auth v2] end
|
||||
# [ns-shim] 统一注入 dspy 执行命名空间(Bug qVMyATuR2Z7UuN2P9pbzp / Ua43R1DMO8-7tCb9ybwo8 / j_rE23TAyqp0OV20OTDwv / iFsP7Yklktq_AixTnEWYi)
|
||||
# 根因:ahserver 的 dspy exec 命名空间不保证注入 `env`(ServerEnv 实例)与模块函数
|
||||
# (create_entity / list_entities / *_import …),直写裸名会 NameError,或把 None 拼进 SQL
|
||||
# ('select None as None from None')→ 泛化 500。
|
||||
# 注入方式(QC#1 整改):ServerEnv 继承 appPublic.dictObject.DictObject(dict 子类 Singleton),
|
||||
# `vars(_ns)` 返回实例 __dict__(实测为空)→ 旧写法 globals().update(vars(_ns)) 实际注入 0 项,
|
||||
# shim 形同虚设(5cffb7b 实测记载)。现统一改为**映射取值解析**:
|
||||
# `_resolve_delegate(name)` 依次 globals().get(name) → env.get(name) → getattr(env, name, None);
|
||||
# 并在 env 为映射时按 `env.items()`(不是 vars())批量导出到 globals。全 74 个端点共用同一段。
|
||||
# Bug Dsgk7Hflcyyf_p9ILXYHg 部署态实测:ahserver 预加载全局(request 等)经 builtins 解析,
|
||||
# 不在 exec-globals 里 —— `globals().get("request")` 恒为 None,旧写法让 5 条契约端点全返 503。
|
||||
# 改为裸名优先(ahserver 语义),globals 取值仅作兼容兜底。
|
||||
_ns = None
|
||||
try:
|
||||
_ns = request._run_ns
|
||||
except Exception:
|
||||
_ns = None
|
||||
if _ns is None:
|
||||
_ns = getattr(globals().get("request"), "_run_ns", None)
|
||||
if _ns is None:
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[ns-shim] dspy 执行命名空间缺少 request._run_ns,`env` 与模块函数无法解析;"
|
||||
"请检查应用 init() 是否完成 ServerEnv 装配并逐个调用 load_{模块}()"}
|
||||
env = _ns
|
||||
|
||||
|
||||
def _resolve_delegate(_name):
|
||||
"""三级映射取值解析委托对象(QC#1 统一内置实现)。
|
||||
|
||||
解析顺序:globals().get(name) → env.get(name) → getattr(env, name, None)。
|
||||
全部未命中返回 None,调用方据此走 503 可行动兜底(不退化为泛化 500)。
|
||||
"""
|
||||
_v = globals().get(_name)
|
||||
if _v is not None:
|
||||
return _v
|
||||
try:
|
||||
_v = env.get(_name)
|
||||
except Exception:
|
||||
_v = None
|
||||
if _v is not None:
|
||||
return _v
|
||||
return getattr(env, _name, None)
|
||||
|
||||
|
||||
def _ns_export():
|
||||
"""把 env 中的名字导出到 dspy globals。
|
||||
|
||||
主路径:映射取值 `env.items()`——ServerEnv 继承 appPublic.dictObject.DictObject(dict 子类),
|
||||
这是唯一可靠注入源(实例 __dict__ 实测为空,故不得只用 vars())。
|
||||
回退路径:env 不是映射时(无 items()),退回 `vars(env)` 读实例 __dict__,
|
||||
以覆盖属性式挂载的命名空间(不回退会把可用命名空间误判成"未注册"→假 503)。
|
||||
"""
|
||||
_items = None
|
||||
try:
|
||||
_items = env.items()
|
||||
except Exception:
|
||||
_items = None
|
||||
if _items is None:
|
||||
try:
|
||||
_items = vars(env).items()
|
||||
except Exception:
|
||||
_items = None
|
||||
if _items is None:
|
||||
return
|
||||
for _k, _v in list(_items):
|
||||
if not isinstance(_k, str) or _k.startswith("__") or _k == "params_kw":
|
||||
continue
|
||||
globals()[_k] = _v
|
||||
|
||||
|
||||
_ns_export()
|
||||
for _ldrname in ("load_entity", "load_scene", "load_scense", "load_scense_game",
|
||||
"load_world", "load_world_sync", "load_world_snapshot",
|
||||
"load_runtime", "load_script_engine", "load_sys_ops"):
|
||||
_ldr = _resolve_delegate(_ldrname)
|
||||
if _ldr is None:
|
||||
continue
|
||||
try:
|
||||
_loaded = _ldr()
|
||||
if hasattr(_loaded, "__await__"):
|
||||
await _loaded
|
||||
except Exception:
|
||||
pass
|
||||
_ns_export()
|
||||
|
||||
|
||||
def _dspy_missing(_name):
|
||||
"""未注册模块函数的统一兜底:503 + 可行动提示(不要退化成泛化 500)。"""
|
||||
return {"status": 503, "success": False,
|
||||
"message": "[ns-shim] 模块函数 %s 未注入 dspy 命名空间;"
|
||||
"请确认该模块 init() 已 env.%s = %s 且应用已 load_{模块}() 挂载" % (_name, _name, _name)}
|
||||
# [/ns-shim]
|
||||
"""GET /scense/api/list_scense.dspy —— 展示会话列表(验收契约路径)。
|
||||
|
||||
QC#4 整改(硬编码空数组桩):原实现是一个**恒返回常量空结果的桩**
|
||||
(`return json.dumps({'ok': True, 'total': 0, 'rows': [], 'note': ...})`),
|
||||
不读任何数据源,任何调用方拿到的都是空列表——它让 `/scense/api/list_scense.dspy`
|
||||
从「500 fpath is None」变成了「200 但永远空」,属于用桩掩盖契约缺失,不可接受。
|
||||
|
||||
scense 是游戏**展示前端**模块,自身不持有数据表(无 models/ 无 json/),
|
||||
会话数据的权威持有者是 scense_game 模块的 `game_session` 表。
|
||||
scense 模块自己的同源 list 实现是 `scense.core.game_list_sessions(user_id)`:
|
||||
经 `scense/init.py` 的 `load_scense()` 注册到 ServerEnv(`env.game_list_sessions = ...`),
|
||||
内部走 `_get_dbname()` 取库名 + `sor.R('game_session', {...})` 按 updated_at 倒序取最近 20 条。
|
||||
|
||||
故本文件改为**委托该同源实现**,并按登录用户隔离(多用户不串数据),
|
||||
不再返回任何硬编码常量。
|
||||
"""
|
||||
try:
|
||||
uid = ''
|
||||
try:
|
||||
info = get_userinfo()
|
||||
except Exception:
|
||||
info = None
|
||||
if isinstance(info, dict):
|
||||
uid = info.get('id') or info.get('userid') or info.get('user') or ''
|
||||
if not uid:
|
||||
try:
|
||||
uid = get_user() or ''
|
||||
except Exception:
|
||||
uid = ''
|
||||
rows = await game_list_sessions(uid)
|
||||
rows = rows or []
|
||||
return json.dumps({'ok': True, 'total': len(rows), 'rows': rows},
|
||||
ensure_ascii=False, default=str)
|
||||
except Exception as e:
|
||||
return json.dumps({'ok': False, 'total': 0, 'rows': [], 'error': str(e)},
|
||||
ensure_ascii=False)
|
||||
Loading…
x
Reference in New Issue
Block a user