第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 节。
59 lines
3.4 KiB
Plaintext
59 lines
3.4 KiB
Plaintext
# apps/pbls/wwwroot/api/public/health/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/api/public/health/index.dspy -> GET /healthz
|
||
# 另在 wwwroot/healthz.dspy 放同逻辑副本 -> GET /api/public/health(兼容带扩展名探测)。
|
||
#
|
||
# 【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': '/api/public/health',
|
||
'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': '/api/public/health',
|
||
'error': str(e)}, ensure_ascii=False, default=str)
|