pbls/wwwroot/healthz.dspy
agent.develop 658e0b7cd8 fix(pbls): 修复 Bug WIu0Rkn0KwB0XTjYZz0qk —— 补齐 /healthz 探针端点 + build.sh 部署链路四缺口 + RBAC 宿主路径注册 + 旧口径 9300→9380 全量清理
第4轮复测三门禁全红的 develop 侧根因(实测核验,非照抄报单结论):
1. /healthz 端点根本不存在(grep -rn healthz apps/pbls 零命中),而复测门禁①判据正是
   curl http://127.0.0.1:9380/healthz;build.sh 自检却走另一条 /api/public/health(同样无实现)
   → 进程活着也必然 HTTP!=200,且存在「自检绿/复测红」假通过隐患。
2. build.sh 从不调 scripts/load_path.py(grep 零命中)→ 新端点无 RBAC 登记即 401/403。
3. build.sh 读出 DEPLOY_PATH 后只用于 echo(L33/L34/L260),从未 mkdir/rsync
   → 门禁④ /d/pbl/pbls 恒不存在;env start_cmd 指向的解释器与入口均不存在 → 门禁②③不可能成立。
4. logs/ 目录到步骤13才 mkdir,而步骤11已 tee logs/self_check.log;set -euo pipefail 下
   全新克隆必 die,部署在「建表已提交、自检未跑」处中断(实测 ls -d apps/pbls/logs 不存在)。

修复(同类问题全量排查,不只修报出来的那一个):
- 新增 wwwroot/healthz/index.dspy、wwwroot/healthz.dspy、wwwroot/api/public/health/index.dspy
  三个探针(零 import / 每分支显式 return json.dumps / dict 内不用 f-string / try-except 兜底
  不抛500 / default=str / SQL 走 $ + get_sor_context 不硬编码库名 / 不返回敏感字段);
  status=ok 只表进程存活,DB 表计数为附加诊断不降级 status。
- build.sh 新增步骤 10b(mkdir logs run 前置)、12.5(load_path.py --check 门禁 + 正式注册,
  失败明确 die 不静默)、12.8(mkdir DEPLOY_PATH + rsync -a --delete 同步 app/conf/wwwroot/scripts,
  排除 venv/logs/run/.git;venv 软链复用因 shebang 是绝对路径;断言 python/入口/config 三者就位);
  步骤13 改为 cd DEPLOY_PATH 后启动(website.root=wwwroot 是相对路径,从别处启动全量404)
  + pid/pkill 双口径停旧进程保幂等 + 进程数自证为0时打印日志尾部60行再 die;
  步骤14 双口径健康检查(/healthz + /api/public/health + 外网口 SSH_HOST)+ 终态表数断言 37/37。
- scripts/load_path.py 新增 HOST_PATHS 四条(/healthz、/healthz.dspy、/api/public/health、
  /api/public/health.dspy,role=any 免鉴权,禁通配符),collect() 末尾并入(原聚合器只扫
  modules/*,宿主 wwwroot 端点无人登记);走同规则去重 + _stricter 冲突保留更严格 role。
- 旧口径 9300→9380 清理 4 处:build.sh:4 头注释、wwwroot/index.ui:179 状态栏(用户可见)、
  pbl_appcodes/__init__.py:14、pbl_appcodes/init.py:16;_patch_common_contract_symbols.py:9
  属历史修复台账,有意保留原文不改(改历史记录=伪造)。
- conf/config.json 同步新口径(port 9380 / 库 pbl / user test / module_dbname 全量 pbl),
  与 projects/pbls/env/test.json 唯一事实源同值,满足 build.sh 步骤1 端口一致性门禁。

自证(真实回显):bash -n build.sh OK;py_compile load_path.py OK;
load_path.py --check → total=284 wildcard=0 aggregation_failures=0 PASS(280→284,+4 宿主探针);
--list | grep health → 四条 any 均在册;三个 .dspy 经 ast.parse 包装校验 AST OK、
no import/no print、return×2;grep -rn 9300 于四处改动文件零命中。

边界:未执行真实部署(建表/种子/rsync/起进程属 deploy_test 范畴,需 venv+真库+占端口),
不声称 healthz 已返回 200;未改任何 referenced_modules(rbac/apppublic/sqlor/ahserver/appbase/
accounting 只读)与 world/scene/entity/script_engine/scense* 复用域;硬编码库名同类问题经
grep 核验不存在(各模块均走 get_module_dbname),无需修。
详见 projects/pbls/docs/02-develop/dev-notes.md 2026-09-25 节。
2026-09-25 11:35:23 +08:00

59 lines
3.3 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.

# apps/pbls/wwwroot/healthz/index.dspy —— 部署冒烟健康探针(无鉴权 / any 角色)
#
# 【为什么必须有这个文件】Bug WIu0Rkn0KwB0XTjYZz0qk 第 4 轮复测门禁 ① 用
# curl -s -m 8 -o /tmp/h1.txt -w 'HTTP=%{http_code}' http://127.0.0.1:9380/healthz
# 作为「应用是否活着」的判据,但仓库里此前**根本不存在 /healthz 端点**
# (grep -rn healthz apps/pbls/app apps/pbls/wwwroot 零命中),而 build.sh 步骤 14
# 自检用的却是另一条路径 /api/public/health(同样不存在)。两条探针路径都缺 →
# 即便进程起来、端口监听,门禁 ① 也必然 HTTP=000/404,冒烟永远不可能转绿。
# 本文件补齐 /healthz;同批补 wwwroot/api/public/health/index.dspy 保持 build.sh 原口径。
#
# 【URL 映射】ahserver 按 website.root=wwwroot + processors['.dspy']='dspy' 解析:
# wwwroot/healthz/index.dspy -> GET /healthz
# 另在 wwwroot/healthz.dspy 放同逻辑副本 -> GET /healthz.dspy(兼容带扩展名探测)。
#
# 【dspy 规约(dspy-file-implementation-spec 硬规则)】
# * 零 import:json / datetime 由框架预加载,直接裸用;
# * 每个分支显式 return(框架把文件包进 async def,裸表达式返回 None → 500);
# * dict 返回内**不用 f-string**(exec 误读 '}' → SyntaxError),改字符串拼接;
# * try/except 兜底:对象端点异常也返回合法 JSON({"status":"error",...}),不抛 500;
# * json.dumps(..., default=str) 防 Decimal/datetime 序列化炸;
# * 不返回任何敏感字段(库口令/密钥一律不出现在响应里)。
#
# 【语义约定】status=ok 只表示「进程活着 + HTTP 栈可服务 + 路由/RBAC 已放行」。
# DB 探测是**附加诊断信息**(db.tables / db.error),DB 不通**不**把 status 降级为
# error —— 否则库抖动会让「进程存活」探针失真,排障时分不清是进程死了还是库挂了。
# 表数门禁(37 张)由 build.sh 步骤 10 的硬断言负责,不在探针里做判定。
try:
result = {
'status': 'ok',
'app': 'pbls',
'endpoint': '/healthz',
'ts': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
}
# ---- 附加诊断:pbl_* 业务表数量(只读 information_schema,失败不影响 status)----
db_info = {'ok': False, 'tables': None, 'error': None}
try:
env = request._run_ns
async with get_sor_context(env, 'pbl_common') as sor:
rows = await sor.sqlExe(
"SELECT COUNT(*) AS cnt FROM information_schema.tables "
"WHERE table_schema = DATABASE() AND table_name LIKE ${pat}$",
{'pat': 'pbl\\_%'})
if rows:
first = rows[0]
cnt = getattr(first, 'cnt', None)
if cnt is None and hasattr(first, 'keys'):
cnt = dict(first).get('cnt')
db_info['tables'] = int(cnt) if cnt is not None else 0
db_info['ok'] = True
except Exception as dbe:
db_info['error'] = str(dbe)
result['db'] = db_info
return json.dumps(result, ensure_ascii=False, default=str)
except Exception as e:
return json.dumps({'status': 'error', 'app': 'pbls', 'endpoint': '/healthz',
'error': str(e)}, ensure_ascii=False, default=str)