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:
ymq 2026-10-28 15:45:30 +08:00
parent 702f27407b
commit 0cfeb45a3c
6 changed files with 1669 additions and 0 deletions

View 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"
}
}

View 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)}

View 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)}

View 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)}

View 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)}

View 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)