scense/modules/scense/wwwroot/api/list_scense.dspy
ymq 0cfeb45a3c 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 保持正常
2026-10-28 15:45:30 +08:00

349 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

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

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