diff --git a/build.sh b/build.sh index 1d7eb1c..3bfc927 100644 --- a/build.sh +++ b/build.sh @@ -113,7 +113,19 @@ if [ ! -x "${PY}" ]; then python3 -m venv "${VENV_DIR}" fi "${PY}" -m pip install --upgrade pip wheel setuptools -"${PY}" -m pip install -r "${APP_DIR}/requirements.txt" + +# [deploy_test R5 修复 #1] 平台基础包(ahserver/sqlor/apppublic/appbase/rbac/bricks_for_python/accounting) +# 原走内网私有源 http://10.20.30.41:8081 —— 目标机 192.168.16.3 实测 connect timeout(5 次重试全失败), +# build.sh 在此处 BUILD_RC=1 终止(R4 部署失败根因,见 evidence/r4_build.log)。 +# 改为:机构 wheelhouse 离线供给(--find-links,绝对路径,不依赖 cwd)+ 公网 pypi.org 取第三方包 +# (目标机实测 https://pypi.org/simple/pymysql/ HTTP=200)。 +WHEELHOUSE="${WS_DIR}/wheelhouse" +[ -d "${WHEELHOUSE}" ] || die "缺少机构 wheelhouse ${WHEELHOUSE}(平台基础包离线源,内网 pypi 在目标机不可达)" +for _w in ahserver sqlor apppublic appbase rbac bricks_for_python accounting; do + ls "${WHEELHOUSE}"/${_w}-*.whl >/dev/null 2>&1 || die "wheelhouse 缺 ${_w} 的 whl(${WHEELHOUSE})" +done +log "wheelhouse 就位: $(ls "${WHEELHOUSE}"/*.whl | wc -l) 个 whl @ ${WHEELHOUSE}" +"${PY}" -m pip install --find-links "${WHEELHOUSE}" -r "${APP_DIR}/requirements.txt" # ---- 7b. 测试前置依赖(DISC.2)---- # 运行时依赖(requirements.txt)刻意不含 pytest;测试依赖单列 requirements-test.txt, @@ -137,17 +149,57 @@ else fi # ---- 8. 本地安装 15 个 pbl_* 模块 + 复用模块(可编辑模式)---- +# [deploy_test R5 修复 #2] 原循环只匹配 modules/pbl_*,但入口 app/pbls.py 还显式 import +# world / scene / entity / script_engine / scense_runtime / scense / scense_game +# 这 7 个复用域模块(pbls_spec.json referenced_modules)。漏装 → 入口 ImportError → +# 进程起不来(端口无 LISTEN、healthz=000)。此处按 spec.json referenced_modules 补装, +# 并做「入口 import 全部可解析」自证门禁,装不上就 die,不留到启动阶段才炸。 +REUSE_MODULES="$(python3 -c "import json;print(' '.join(json.load(open('${WS_DIR}/projects/pbls/pbls_spec.json'))['referenced_modules']))")" +INSTALL_DIRS="" for mod_dir in "${WS_DIR}"/modules/pbl_*; do [ -d "${mod_dir}" ] || continue + INSTALL_DIRS="${INSTALL_DIRS} ${mod_dir}" +done +for rm in ${REUSE_MODULES}; do + # 平台基础包(apppublic/sqlor/ahserver/appbase/rbac/accounting)已由 wheelhouse 装好,跳过 + case " ${rm} " in *" apppublic "*|*" sqlor "*|*" ahserver "*|*" appbase "*|*" rbac "*|*" accounting "*) continue;; esac + [ -d "${WS_DIR}/modules/${rm}" ] || die "spec.json referenced_modules 声明 ${rm},但 ${WS_DIR}/modules/${rm} 不存在" + INSTALL_DIRS="${INSTALL_DIRS} ${WS_DIR}/modules/${rm}" +done +for mod_dir in ${INSTALL_DIRS}; do if [ -f "${mod_dir}/pyproject.toml" ] || [ -f "${mod_dir}/setup.py" ]; then log "安装模块 $(basename "${mod_dir}")" - "${PY}" -m pip install -e "${mod_dir}" + "${PY}" -m pip install --find-links "${WHEELHOUSE}" -e "${mod_dir}" else export PYTHONPATH="${mod_dir}:${PYTHONPATH:-}" log "模块 $(basename "${mod_dir}") 无打包描述,加入 PYTHONPATH" fi done +# ---- 8a. 入口 import 可解析性自证(部署前拦截 ImportError,不留到启动阶段)---- +log "入口 import 自证:$(basename "${ENTRY}") 全部 import 必须可解析" +IMPORT_RC=0 +"${PY}" - <<'PYIMP' || IMPORT_RC=$? +import ast, importlib.util, sys +src = open('app/pbls.py', encoding='utf-8').read() +mods = set() +for node in ast.walk(ast.parse(src)): + if isinstance(node, ast.ImportFrom) and node.module: + mods.add(node.module.split('.')[0]) + elif isinstance(node, ast.Import): + for a in node.names: + mods.add(a.name.split('.')[0]) +mods -= set(sys.stdlib_module_names) +mods -= {'json', 'os', 'sys', 'ast'} +missing = sorted(m for m in mods if importlib.util.find_spec(m) is None) +print('[import-check] 需解析顶层模块 %d 个: %s' % (len(mods), ' '.join(sorted(mods)))) +if missing: + print('[import-check][FAIL] 无法解析: %s' % ' '.join(missing)) + sys.exit(1) +print('[import-check] PASS 全部可解析') +PYIMP +[ "${IMPORT_RC}" = "0" ] || die "入口 import 自证失败(rc=${IMPORT_RC}):上列模块在 venv 内不可解析,应用必然启动失败,终止部署" + # ---- 8b. M9 覆盖层页面壳生成(构建产物,不入库)+ 一致性门禁 ---- # 页面壳由 pbl_scense_ext 的生成器产出;产物目录 wwwroot/pbl_overlay/ 在模块仓 # .gitignore 中被排除,因此必须在构建期生成,否则新克隆/新部署环境该目录为空、 diff --git a/build.sh.r4bak b/build.sh.r4bak new file mode 100644 index 0000000..1d7eb1c --- /dev/null +++ b/build.sh.r4bak @@ -0,0 +1,367 @@ +#!/usr/bin/env bash +# ===================================================================== +# PBL Agent OS 验证原型(Phase 0/1)—— 一键部署脚本(test 环境) +# 端口唯一事实源:projects/pbls/env/test.json(9380);本脚本不硬编码端口,从 env 读取 +# 门禁:建表 37 张 + 9 项种子全绿 + self_check PASS 74/74 + py_compile 全通过 +# ===================================================================== +set -euo pipefail + +APP_NAME="pbls" +APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WS_DIR="$(cd "${APP_DIR}/../.." && pwd)" +ENV_FILE="${WS_DIR}/projects/pbls/env/test.json" +DDL_FILE="${APP_DIR}/scripts/ddl/pbls_tables.sql" +VENV_DIR="${APP_DIR}/venv" +PY="${VENV_DIR}/bin/python" + +log() { echo "[build.sh][$(date '+%F %T')] $*"; } +die() { echo "[build.sh][FATAL] $*" >&2; exit 1; } + +# ---- 0. 前置检查:env 唯一事实源必须存在且无占位 ---- +[ -f "${ENV_FILE}" ] || die "缺少环境事实源 ${ENV_FILE}" +if grep -Eq '待明确|TBD|占位|CHANGE_ME|xxx' "${ENV_FILE}"; then + die "${ENV_FILE} 含占位/待明确字段,禁止部署" +fi + +read_env() { python3 -c "import json,sys;print(json.load(open('${ENV_FILE}'))$1)"; } +PORT="$(read_env "['port']")" +DB_HOST="$(read_env "['db']['host']")" +DB_PORT="$(read_env "['db']['port']")" +DB_USER="$(read_env "['db']['user']")" +DB_PASS="$(read_env "['db']['password']")" +DB_NAME="$(read_env "['db']['dbname']")" +DEPLOY_PATH="$(read_env "['deploy']['path']")" +# 外网口自检用(复测门禁 ① 同时 curl 127.0.0.1 与 192.168.16.3 两个地址); +# env 无 ssh.host 时回退 127.0.0.1,不让 read_env 抛 KeyError 中断部署。 +SSH_HOST="$(python3 -c "import json;print(json.load(open('${ENV_FILE}')).get('ssh',{}).get('host') or '127.0.0.1')")" +log "env 事实源: port=${PORT} db=${DB_NAME}@${DB_HOST}:${DB_PORT} deploy=${DEPLOY_PATH}" + +# ---- 1. 端口一致性核验(conf/config.json 必须与 env 同值)---- +CONF_PORT="$(python3 -c "import json;print(json.load(open('${APP_DIR}/conf/config.json'))['port'])")" +[ "${CONF_PORT}" = "${PORT}" ] || die "端口不一致: conf=${CONF_PORT} env=${PORT}" +log "端口一致性核验通过: ${PORT}" + +# ---- 2. 应用仓库不得另存 env/(端口唯一事实源在项目侧)---- +[ ! -d "${APP_DIR}/env" ] || die "应用仓库存在 env/ 目录,违反端口唯一事实源规则" +log "应用仓库无 env/ 目录,核验通过" + +# ---- 3. 入口硬门禁核验(6.1 / 6.2)---- +ENTRY="${APP_DIR}/app/${APP_NAME}.py" +[ -f "${ENTRY}" ] || die "缺少入口 ${ENTRY}" +grep -q 'from ahserver.webapp import webapp' "${ENTRY}" || die "入口未使用 ahserver.webapp.webapp" +grep -q 'webapp(init)' "${ENTRY}" || die "入口未调用 webapp(init)" +if grep -Eq 'Flask|FastAPI|Django|http\.server|jsonify|@app\.route' "${ENTRY}"; then + die "入口含禁用框架关键字(Flask/FastAPI/Django/http.server/jsonify/@app.route)" +fi +log "硬门禁 6.1 通过: ahserver.webapp + webapp(init),无禁用框架" + +# ServerEnv() 行号必须早于 load_rbac() 行号 +ENV_LINE="$(grep -n 'ServerEnv()' "${ENTRY}" | head -1 | cut -d: -f1)" +RBAC_LINE="$(grep -n 'load_rbac()' "${ENTRY}" | head -1 | cut -d: -f1)" +[ -n "${ENV_LINE}" ] && [ -n "${RBAC_LINE}" ] || die "入口缺 ServerEnv() 或 load_rbac() 显式调用" +[ "${ENV_LINE}" -lt "${RBAC_LINE}" ] || die "ServerEnv() 必须早于 load_rbac()(env 行 ${ENV_LINE} >= rbac 行 ${RBAC_LINE})" +grep -q 'load_appbase()' "${ENTRY}" || die "入口缺 load_appbase() 显式调用" +grep -q 'load_pybricks()' "${ENTRY}" || die "入口缺 load_pybricks() 显式调用" +log "硬门禁 6.2 通过: ServerEnv()(L${ENV_LINE}) < load_rbac()(L${RBAC_LINE}),load_appbase/load_rbac/load_pybricks 均显式调用" + +# ---- 4. DDL 方言核验(6.4:mariadb,BIGINT AUTO_INCREMENT,零 SERIAL/FK/ENUM/TIMESTAMP)---- +# 【Q4 修复 / deploy_test 第 3 轮实测】旧版直接对 DDL **原文** grep,存在两类假阳性: +# (1) DDL 文末「契约自检」注释块为说明"零命中项",逐字写出了 +# FOREIGN KEY / REFERENCES / ENUM( / SERIAL / BIGSERIAL / nextval / TIMESTAMP +# (实测原文命中 3 行:L6 头部说明、L220 COMMENT 串内的 references、L891 契约自检块), +# 旧 `grep -Eiq` 一律触发 die —— 注释/COMMENT 串被当成真实 DDL,门禁必然误杀; +# (2) `CREATE TABLE` / `AUTO_INCREMENT` 计数同理会把注释行与 COMMENT '...' 串内字样算进去, +# 表数不可信(且旧断言 36 与 DDL 实际 37 张不符,新增 pbl_compile_task 后未同步)。 +# 修复:改为「先剥 SQL 注释 + 字符串字面量(含 COMMENT 串),再在纯 DDL 正文上扫描」, +# 由 scripts/ddl_gate_check.py 单遍字符扫描实现(非逐行正则,串内 -- / # 不会误判为注释), +# 期望值与 DDL 实际表数对齐为 37(表数 / BIGINT AUTO_INCREMENT 主键数同为 37)。 +[ -f "${DDL_FILE}" ] || die "缺少 DDL ${DDL_FILE}" +DDL_GATE="${APP_DIR}/scripts/ddl_gate_check.py" +[ -f "${DDL_GATE}" ] || die "缺少 DDL 门禁扫描器 ${DDL_GATE}(6.4 依赖,禁止回退到原文 grep)" + +# 4a. 扫描器自身可信度:先跑内置最小单测(剥注释/剥串/幂等/真实违规不漏报) +SELFTEST_RC=0 +python3 "${DDL_GATE}" --selftest || SELFTEST_RC=$? +[ "${SELFTEST_RC}" = "0" ] || die "DDL 门禁扫描器自检未通过(--selftest rc=${SELFTEST_RC}),扫描结果不可信,终止部署" + +# 4b. 在降噪后的纯 DDL 正文上核验:方言零命中 + 表数 37 + BIGINT AUTO_INCREMENT 37 +GATE_RC=0 +python3 "${DDL_GATE}" --ddl "${DDL_FILE}" --expect-tables 37 --expect-autoinc 37 || GATE_RC=$? +[ "${GATE_RC}" = "0" ] || die "硬门禁 6.4 未通过(ddl_gate_check.py rc=${GATE_RC}):非 mariadb 方言关键字命中 / 表数≠37 / BIGINT AUTO_INCREMENT≠37,详见上方回显" +log "硬门禁 6.4 通过: mariadb 方言(剥注释+COMMENT 串后零命中),37 表 / 37 处 BIGINT AUTO_INCREMENT" + +# ---- 5. 依赖声明核验(6.5)---- +for dep in ahserver sqlor apppublic appbase rbac PyMySQL; do + grep -qi "^${dep}" "${APP_DIR}/requirements.txt" || die "requirements.txt 缺依赖 ${dep}" +done +log "硬门禁 6.5 通过: ahserver/sqlor/apppublic/appbase/rbac/PyMySQL 均已声明" + +# ---- 6. website.processors / indexes 核验(缺一会导致 / 返回 500)---- +python3 - <<'PYEOF' +import json, sys +conf = json.load(open('conf/config.json')) +procs = dict((p[0], p[1]) for p in conf['website']['processors']) +for ext, name in (('.tmpl', 'tmpl'), ('.ui', 'bui'), ('.dspy', 'dspy')): + assert procs.get(ext) == name, 'processors 缺 %s->%s,实际 %r' % (ext, name, procs.get(ext)) +assert 'index.ui' in conf['website']['indexes'], 'indexes 缺 index.ui' +print('[build.sh] website 核验通过: processors=%s indexes=%s' % (procs, conf['website']['indexes'])) +PYEOF + +# ---- 7. venv + 依赖安装 ---- +if [ ! -x "${PY}" ]; then + log "创建 venv: ${VENV_DIR}" + python3 -m venv "${VENV_DIR}" +fi +"${PY}" -m pip install --upgrade pip wheel setuptools +"${PY}" -m pip install -r "${APP_DIR}/requirements.txt" + +# ---- 7b. 测试前置依赖(DISC.2)---- +# 运行时依赖(requirements.txt)刻意不含 pytest;测试依赖单列 requirements-test.txt, +# 由本步装进同一个 venv,使 +# source venv/bin/activate && python3 -m pytest tests/test_m5a_idempotency.py -v +# 在**不手工注入 PYTHONPATH** 的前提下可直接执行(此前靠 PYTHONPATH=/.pylibs +# 临时注入跑通,换机器/换角色即失效,test 角色会误判为环境不可用)。 +# 只要运行时依赖的场景(如生产镜像)用 PBL_SKIP_TEST_DEPS=1 显式跳过。 +REQ_TEST="${APP_DIR}/requirements-test.txt" +if [ "${PBL_SKIP_TEST_DEPS:-0}" = "1" ]; then + log "PBL_SKIP_TEST_DEPS=1:跳过测试前置依赖(venv 内将没有 pytest,测试角色需自行引导)" +elif [ ! -f "${REQ_TEST}" ]; then + die "缺测试依赖清单 ${REQ_TEST}(DISC.2 要求显式声明;确要跳过请设 PBL_SKIP_TEST_DEPS=1)" +else + log "安装测试前置依赖: ${REQ_TEST}" + "${PY}" -m pip install -r "${REQ_TEST}" + # 自证:装完必须真能 import + 跑 --version,否则本步等于没做(不让注释与事实矛盾) + "${PY}" -c 'import pytest' >/dev/null 2>&1 \ + || die "安装后 venv 内仍无法 import pytest(${PY})" + log "7b 自证通过: $("${PY}" -m pytest --version 2>&1 | head -1) @ ${PY}" +fi + +# ---- 8. 本地安装 15 个 pbl_* 模块 + 复用模块(可编辑模式)---- +for mod_dir in "${WS_DIR}"/modules/pbl_*; do + [ -d "${mod_dir}" ] || continue + if [ -f "${mod_dir}/pyproject.toml" ] || [ -f "${mod_dir}/setup.py" ]; then + log "安装模块 $(basename "${mod_dir}")" + "${PY}" -m pip install -e "${mod_dir}" + else + export PYTHONPATH="${mod_dir}:${PYTHONPATH:-}" + log "模块 $(basename "${mod_dir}") 无打包描述,加入 PYTHONPATH" + fi +done + +# ---- 8b. M9 覆盖层页面壳生成(构建产物,不入库)+ 一致性门禁 ---- +# 页面壳由 pbl_scense_ext 的生成器产出;产物目录 wwwroot/pbl_overlay/ 在模块仓 +# .gitignore 中被排除,因此必须在构建期生成,否则新克隆/新部署环境该目录为空、 +# /pbl_overlay/.ui 导航全量 404。位置:模块安装之后、模块 wwwroot 软链(紧随其后的 +# 「8c. 模块 wwwroot 挂载」段)之前——生成物落在模块仓,必须再经 8c 挂载到应用 wwwroot +# 才能在应用侧 URL /pbl_scense_ext/pbl_overlay/.ui 上取到。 +OVERLAY_GEN="${WS_DIR}/modules/pbl_scense_ext/scripts/gen_overlay_pages.py" +[ -f "${OVERLAY_GEN}" ] || die "缺少覆盖层页面壳生成器: ${OVERLAY_GEN}(modules/pbl_scense_ext 未就位)" +log "生成 PBL 覆盖层页面壳: ${OVERLAY_GEN}" +# 注意:不能用 `if ! cmd; then die "... rc=$?"` —— 进入分支时 $? 已被 if 取反为 0, +# 打印出的 rc 恒为 0 会误导排障;改为显式捕获真实返回码。 +GEN_RC=0 +"${PY}" "${OVERLAY_GEN}" || GEN_RC=$? +if [ "${GEN_RC}" -ne 0 ]; then + die "覆盖层页面壳生成失败(gen_overlay_pages.py rc=${GEN_RC}):wwwroot/pbl_overlay/ 将为空,/pbl_overlay/.ui 全量 404,终止部署" +fi +log "覆盖层页面壳门禁校验(gen_overlay_pages.py --check)" +CHK_RC=0 +"${PY}" "${OVERLAY_GEN}" --check || CHK_RC=$? +if [ "${CHK_RC}" -ne 0 ]; then + die "覆盖层页面壳门禁校验未通过(--check rc=${CHK_RC}):i18n zh/en key 不一致,或页面壳与 PAGES 注册表不一致,终止部署" +fi +log "覆盖层页面壳生成 + 门禁校验通过(12 个页面壳 / i18n key 一致)" + +# ---- 8c. 模块 wwwroot 挂载(软链)+ 应用侧可达性门禁(QC 退回意见 #1/#2)---- +# 上一版只做了 8b「生成」,生成物落在模块仓 modules//wwwroot/ 下,但本脚本从未把 +# 模块 wwwroot 挂进应用 wwwroot —— 应用 website.root=wwwroot 只指向应用自身目录, +# 因此 build.sh 跑完后应用侧依然取不到 /pbl_scense_ext/pbl_overlay/.ui(可达链路 +# 断在挂载环节,全量 404 并未消除)。此处按 web-application-spec「挂载模块 9 步流程」 +# 第 5 步(wwwroot 软链,非 cp,保持与模块仓单一副本同步)补齐,并立即以应用侧路径 +# 实测 12 个页面壳数量作为门禁,rc≠0 终止部署。 +LINKED_MODULES="" +for mod_dir in "${WS_DIR}"/modules/pbl_*; do + [ -d "${mod_dir}/wwwroot" ] || continue + mod_name="$(basename "${mod_dir}")" + www_link="${APP_DIR}/wwwroot/${mod_name}" + # 已存在但不是软链(应用仓自带同名真实目录)时不覆盖,直接终止让人工处置, + # 避免 ln -sfn 把软链建进既有目录里、或静默删掉应用仓跟踪文件。 + if [ -e "${www_link}" ] && [ ! -L "${www_link}" ]; then + die "wwwroot 挂载冲突:${www_link} 已存在且不是软链(应用仓自带目录?),请人工处置后重试" + fi + ln -sfn "${mod_dir}/wwwroot" "${www_link}" + LINKED_MODULES="${LINKED_MODULES} ${mod_name}" +done +[ -n "${LINKED_MODULES}" ] || die "未发现任何含 wwwroot/ 的 pbl_* 模块,模块 wwwroot 挂载为空" +log "模块 wwwroot 软链挂载完成:${LINKED_MODULES}" + +# 挂载后应用侧可达性门禁:覆盖层页面壳必须能通过应用 wwwroot 路径取到 +APP_OVERLAY_DIR="${APP_DIR}/wwwroot/pbl_scense_ext/pbl_overlay" +APP_UI_COUNT=0 +if [ -d "${APP_OVERLAY_DIR}" ]; then + APP_UI_COUNT="$(find -L "${APP_OVERLAY_DIR}" -maxdepth 1 -name '*.ui' | wc -l | tr -d '[:space:]')" +fi +if [ "${APP_UI_COUNT}" != "12" ]; then + die "应用侧覆盖层页面壳应为 12 个,实际 ${APP_UI_COUNT}(${APP_OVERLAY_DIR}):模块 wwwroot 未挂载成功或生成器未产出,/pbl_scense_ext/pbl_overlay/.ui 将全量 404,终止部署" +fi +log "应用侧可达性核验通过: ${APP_OVERLAY_DIR}/*.ui = ${APP_UI_COUNT}" + +# ---- 9. 语法验证:全部 .py 跑 py_compile(QC 退回意见 #16)---- +log "py_compile 全量语法验证" +FAIL_SYNTAX=0 +while IFS= read -r pyfile; do + if ! "${PY}" -m py_compile "${pyfile}"; then + echo "[build.sh][SYNTAX FAIL] ${pyfile}" + FAIL_SYNTAX=1 + fi +done < <(find "${APP_DIR}/app" "${WS_DIR}/modules/pbl_common" "${WS_DIR}/modules/pbl_appcodes" -name '*.py' -not -path '*/venv/*' 2>/dev/null) +[ "${FAIL_SYNTAX}" = "0" ] || die "存在语法错误文件,终止部署" +log "py_compile 全部通过" + +# ---- 10. 建表(mariadb,显式 TCP,禁 socket)---- +log "执行 DDL: ${DDL_FILE}" +mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \ + --default-character-set=utf8mb4 "${DB_NAME}" < "${DDL_FILE}" +ACTUAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \ + -N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE 'pbl\\_%'" "${DB_NAME}")" +# Q4 修复:与 DDL 实际表数对齐(36 → 37,新增 pbl_compile_task) +[ "${ACTUAL_TABLES}" = "37" ] || die "建表数应为 37,实际 ${ACTUAL_TABLES}" +log "建表完成: pbl_* 共 ${ACTUAL_TABLES} 张" + +# ---- 10b. 运行目录前置创建(Bug WIu0Rkn0KwB0XTjYZz0qk 修复)---- +# 原脚本到步骤 13 才 mkdir logs/run,而步骤 11 已经 `tee ${APP_DIR}/logs/self_check.log`。 +# 全新克隆(仓库不含 logs/,.gitignore 也排除 *.log)时 tee 打不开文件 → +# set -euo pipefail 下整条管道非零 → die,部署在「建表已成功、自检未跑」处中断, +# 表现为门禁 ③ 表数 0/37(DDL 事务已提交但脚本没走到断言)与门禁 ②③ 全红。 +mkdir -p "${APP_DIR}/logs" "${APP_DIR}/run" +log "运行目录就位: ${APP_DIR}/logs ${APP_DIR}/run" + +# ---- 11. 枚举数量契约自检(QC 退回意见 #15:必须真实可运行输出)---- +"${PY}" -m pbl_appcodes.self_check | tee "${APP_DIR}/logs/self_check.log" +grep -q 'SELF_CHECK pbl_appcodes: PASS 74/74' "${APP_DIR}/logs/self_check.log" \ + || die "pbl_appcodes 数量契约自检未通过" + +# ---- 12. 9 项种子数据幂等注入(重复执行零差异)---- +log "种子注入(第 1 轮)" +"${PY}" - <<'PYEOF' +import json +from pbl_appcodes.seed import seed_all +s1 = seed_all() +print('[seed round1]', json.dumps(s1, ensure_ascii=False, sort_keys=True)) +s2 = seed_all() +print('[seed round2]', json.dumps(s2, ensure_ascii=False, sort_keys=True)) +assert s2['groups_inserted'] == 0 and s2['groups_updated'] == 0, '重复执行组有差异' +assert s2['items_inserted'] == 0 and s2['items_updated'] == 0, '重复执行项有差异' +assert s2['idempotent'] is True, '幂等校验失败' +print('[seed] 幂等零差异校验通过: 8 组 74 项') +PYEOF + +# ---- 12.5 RBAC 路径注册(Bug WIu0Rkn0KwB0XTjYZz0qk 修复:原脚本从不注册)---- +# 中央聚合器 apps/pbls/scripts/load_path.py 把各模块 + 宿主自有端点(含本次新增的 +# /healthz、/healthz.dspy、/api/public/health)逐条写入 permission/rolepermission。 +# 不注册的后果:进程起来了、端口监听了,但 curl 一律 401/403 → 冒烟门禁 ① HTTP!=200。 +# 先跑 --check 做通配符 + 聚合完整性门禁(rc!=0 即终止,禁止带缺口部署)。 +LOAD_PATH="${APP_DIR}/scripts/load_path.py" +[ -f "${LOAD_PATH}" ] || die "缺少 RBAC 注册入口 ${LOAD_PATH}" +log "RBAC 一致性门禁: load_path.py --check" +CHK_RBAC=0 +WS_DIR="${WS_DIR}" "${PY}" "${LOAD_PATH}" --check || CHK_RBAC=$? +[ "${CHK_RBAC}" = "0" ] || die "RBAC 聚合门禁未通过(rc=${CHK_RBAC}):存在通配符或整模块聚合失败,终止部署" +log "RBAC 注册: load_path.py(逐条显式,无通配符)" +REG_RC=0 +WS_DIR="${WS_DIR}" "${PY}" "${LOAD_PATH}" || REG_RC=$? +if [ "${REG_RC}" != "0" ]; then + # set_role_perm.py 不在本机时 register() 返回 PENDING 清单且 rc=1; + # 这不是代码缺陷而是部署机缺工具,明确回显后终止(不静默放过 → 否则又是 403)。 + die "RBAC 注册未成功(rc=${REG_RC}):set_role_perm.py 不可用或写库失败,端点将 401/403,终止部署" +fi +log "RBAC 注册完成" + +# ---- 12.8 落地部署目录 DEPLOY_PATH(Bug WIu0Rkn0KwB0XTjYZz0qk 修复)---- +# 原脚本把 DEPLOY_PATH 读出来只用于最后一行日志 echo,从未创建/同步任何内容, +# 于是复测门禁 ④「ls -la /d/pbl/pbls」恒为 No such file or directory, +# 且 env/test.json 的 start_cmd(${DEPLOY_PATH}/venv/bin/python ${DEPLOY_PATH}/app/pbls.py) +# 指向的解释器与入口都不存在 → 门禁 ②③(进程数 0 / 端口未监听)不可能转绿。 +# 处置:把应用树同步到 DEPLOY_PATH,venv 以软链复用(venv 内 shebang/符号链接是 +# 绝对路径,复制换目录必坏,软链才安全),随后从 DEPLOY_PATH 启动。 +if [ "${DEPLOY_PATH}" = "${APP_DIR}" ]; then + log "DEPLOY_PATH 与仓库目录同址(${DEPLOY_PATH}),跳过同步" + RUN_DIR="${APP_DIR}" + RUN_PY="${PY}" + RUN_ENTRY="${ENTRY}" +else + log "同步应用树到部署目录: ${DEPLOY_PATH}" + mkdir -p "${DEPLOY_PATH}" + # -a 保留权限/时间并**保留软链本身**(8c 建的模块 wwwroot 软链是绝对路径,换机仍可解析); + # 排除 venv/logs/run/.git:venv 不可搬移,logs/run 由目标侧自建,.git 不属于运行态。 + rsync -a --delete \ + --exclude 'venv/' --exclude '.venv/' --exclude 'logs/' --exclude 'run/' \ + --exclude '.git/' --exclude '__pycache__/' --exclude '*.pyc' \ + "${APP_DIR}/" "${DEPLOY_PATH}/" + mkdir -p "${DEPLOY_PATH}/logs" "${DEPLOY_PATH}/run" + # venv 软链复用(目标侧 start_cmd 要求 ${DEPLOY_PATH}/venv/bin/python 可用) + if [ ! -e "${DEPLOY_PATH}/venv" ]; then + ln -sfn "${VENV_DIR}" "${DEPLOY_PATH}/venv" + fi + [ -x "${DEPLOY_PATH}/venv/bin/python" ] || die "部署目录 venv 不可用: ${DEPLOY_PATH}/venv/bin/python" + [ -f "${DEPLOY_PATH}/app/${APP_NAME}.py" ] || die "部署目录缺入口: ${DEPLOY_PATH}/app/${APP_NAME}.py" + [ -f "${DEPLOY_PATH}/conf/config.json" ] || die "部署目录缺配置: ${DEPLOY_PATH}/conf/config.json" + log "部署目录就位: ${DEPLOY_PATH}(app/conf/wwwroot/scripts + venv 软链 + logs/run)" + RUN_DIR="${DEPLOY_PATH}" + RUN_PY="${DEPLOY_PATH}/venv/bin/python" + RUN_ENTRY="${DEPLOY_PATH}/app/${APP_NAME}.py" +fi + +# ---- 13. 启动应用(唯一入口 + 唯一端口,从部署目录启动)---- +# 先停旧进程(幂等重跑不撞端口):按 pid 文件 + 入口路径双口径清理。 +if [ -f "${RUN_DIR}/run/${APP_NAME}.pid" ]; then + OLD_PID="$(cat "${RUN_DIR}/run/${APP_NAME}.pid" 2>/dev/null || true)" + if [ -n "${OLD_PID:-}" ] && kill -0 "${OLD_PID}" 2>/dev/null; then + log "停止旧进程 pid=${OLD_PID}" + kill "${OLD_PID}" 2>/dev/null || true + sleep 2 + fi +fi +pkill -f "${RUN_ENTRY}" 2>/dev/null || true +sleep 1 + +log "启动 ${APP_NAME} 于端口 ${PORT}(cwd=${RUN_DIR})" +# cwd 必须是 RUN_DIR:conf/config.json 的 website.root="wwwroot" 是相对路径, +# 从别处启动会解析不到 wwwroot → 所有 .ui/.dspy 404(含 /healthz)。 +( cd "${RUN_DIR}" && nohup "${RUN_PY}" "${RUN_ENTRY}" \ + > "${RUN_DIR}/logs/${APP_NAME}.out" 2>&1 & echo $! > "${RUN_DIR}/run/${APP_NAME}.pid" ) +sleep 6 + +# 进程 + 端口双自证(对应复测门禁 ②):起不来就把日志尾部打出来再 die, +# 避免「HTTP=000 但没人知道为什么」的四轮同构复测。 +PROC_N="$(ps -ef | grep -F "${APP_NAME}.py" | grep -v grep | wc -l | tr -d '[:space:]')" +log "进程数=${PROC_N}" +if [ "${PROC_N}" = "0" ]; then + echo "----- ${RUN_DIR}/logs/${APP_NAME}.out (tail 60) -----" + tail -60 "${RUN_DIR}/logs/${APP_NAME}.out" 2>/dev/null || true + echo "----------------------------------------------------" + die "应用进程未存活(ps 计数 0),门禁 ② 不成立,详见上方日志尾部" +fi + +# ---- 14. 健康检查(两条口径都验:/healthz = 复测门禁 ①,/api/public/health = 原自检口径)---- +check_http() { + curl -s -m 8 -o /dev/null -w '%{http_code}' "$1" 2>/dev/null || echo 000 +} +HZ_CODE="$(check_http "http://127.0.0.1:${PORT}/healthz")" +AH_CODE="$(check_http "http://127.0.0.1:${PORT}/api/public/health")" +IP_CODE="$(check_http "http://${SSH_HOST:-127.0.0.1}:${PORT}/healthz")" +log "健康检查: /healthz=${HZ_CODE} /api/public/health=${AH_CODE} 外网口 /healthz=${IP_CODE}" +if [ "${HZ_CODE}" != "200" ] || [ "${AH_CODE}" != "200" ]; then + echo "----- ${RUN_DIR}/logs/${APP_NAME}.out (tail 60) -----" + tail -60 "${RUN_DIR}/logs/${APP_NAME}.out" 2>/dev/null || true + echo "----------------------------------------------------" + die "健康检查失败(/healthz=${HZ_CODE}, /api/public/health=${AH_CODE}),门禁 ① 不成立" +fi + +# 表数终态复述(对应复测门禁 ③):部署完成后 pbl 库必须有 37 张 pbl_* 表。 +FINAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \ + -N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE 'pbl\\_%'" "${DB_NAME}")" +log "终态表数: ${DB_NAME}.pbl_* = ${FINAL_TABLES}/37" +[ "${FINAL_TABLES}" = "37" ] || die "终态表数 ${FINAL_TABLES} != 37,门禁 ③ 不成立" + +log "BUILD SUCCESS: ${APP_NAME} 已运行于 http://127.0.0.1:${PORT}/ (部署路径 ${DEPLOY_PATH},/healthz=200,表数 ${FINAL_TABLES}/37)" diff --git a/requirements.txt b/requirements.txt index 8fe78ef..772718f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,27 @@ # PBL Agent OS 验证原型(Phase 0/1)—— 应用依赖声明 # 平台基础依赖(ahserver / sqlor / apppublic / appbase / rbac)由机构私有源提供, # 通过 --index-url 指向内网 pypi;缺失时 build.sh 会 fail-fast 报错而非静默跳过。 ---extra-index-url http://10.20.30.41:8081/repository/pypi-hosted/simple ---trusted-host 10.20.30.41 +# [deploy_test R5 修复] 原 --extra-index-url http://10.20.30.41:8081/... 在目标机 192.168.16.3 +# 实测 connect timeout(5 次重试全失败 → build.sh BUILD_RC=1,部署卡在依赖安装)。 +# 平台基础包改由机构 wheelhouse 离线供给(--find-links 相对本文件定位 WS/wheelhouse), +# 第三方包走公网 pypi.org(目标机实测 HTTP=200 可达)。 +--find-links ../../wheelhouse # ---- 平台基础框架(必需,硬门禁 6.5)---- -ahserver>=1.4.0 -sqlor>=1.3.0 -apppublic>=1.2.0 -appbase>=1.3.0 -rbac>=1.2.0 -bricks_for_python>=1.2.0 # 前端积木(.ui/.bui 渲染 + load_pybricks) -pybricks>=1.1.0 -accounting>=1.0.0 +ahserver>=1.2.0 +sqlor>=2.1.2 +apppublic>=5.3.2 +appbase>=1.0.0 +rbac>=1.0.0 +bricks_for_python>=0.0.5 # 前端积木(.ui/.bui 渲染 + load_pybricks) +# [deploy_test R5 修复] 移除 pybricks>=1.1.0:load_pybricks 实际由 bricks_for_python.init 提供, +# wheelhouse 无此包,公网同名包为 LEGO 机器人库(误装风险)——幻影依赖。 +accounting>=0.1.0 # ---- DB 驱动(mariadb 方言,显式 TCP,禁 socket)---- PyMySQL>=1.1.0 -mysqlclient>=2.2.0 +# [deploy_test R5 修复] 移除 mysqlclient>=2.2.0:需 mysql_config(目标机实测 which mysql_config 无输出, +# pbl 无 sudo 装不了 mariadb-devel)。DB 驱动由 PyMySQL + aiomysql 承担(sqlor/ahserver 传递依赖)。 SQLAlchemy>=2.0.0 # ---- 业务模块(本项目新建 15 个 pbl_*,本地路径安装)---- diff --git a/requirements.txt.r4bak b/requirements.txt.r4bak new file mode 100644 index 0000000..8fe78ef --- /dev/null +++ b/requirements.txt.r4bak @@ -0,0 +1,43 @@ +# PBL Agent OS 验证原型(Phase 0/1)—— 应用依赖声明 +# 平台基础依赖(ahserver / sqlor / apppublic / appbase / rbac)由机构私有源提供, +# 通过 --index-url 指向内网 pypi;缺失时 build.sh 会 fail-fast 报错而非静默跳过。 +--extra-index-url http://10.20.30.41:8081/repository/pypi-hosted/simple +--trusted-host 10.20.30.41 + +# ---- 平台基础框架(必需,硬门禁 6.5)---- +ahserver>=1.4.0 +sqlor>=1.3.0 +apppublic>=1.2.0 +appbase>=1.3.0 +rbac>=1.2.0 +bricks_for_python>=1.2.0 # 前端积木(.ui/.bui 渲染 + load_pybricks) +pybricks>=1.1.0 +accounting>=1.0.0 + +# ---- DB 驱动(mariadb 方言,显式 TCP,禁 socket)---- +PyMySQL>=1.1.0 +mysqlclient>=2.2.0 +SQLAlchemy>=2.0.0 + +# ---- 业务模块(本项目新建 15 个 pbl_*,本地路径安装)---- +# 由 build.sh 以 pip install -e modules/{模块} 方式安装,不发布到公共源 +# pbl_common / pbl_appcodes / pbl_governance / pbl_blueprint / pbl_template +# pbl_validation / pbl_compiler / pbl_agent_runtime / pbl_evidence / pbl_assessment +# pbl_domain_ext / pbl_scense_ext / pbl_runtime_ext / pbl_kdb_ext / pbl_analytics + +# ---- 通用运行时依赖 ---- +requests>=2.31.0 +python-dateutil>=2.8.2 +pytz>=2024.1 +jsonschema>=4.21.0 +PyYAML>=6.0.1 +hashlib-additional>=1.0.1 ; python_version < "3.0" + +# ---- 前端/模板处理(website.processors: .tmpl/.ui/.dspy)---- +Mako>=1.3.0 +Jinja2>=3.1.0 + +# ---- 部署与自检 ---- +psutil>=5.9.0 +setuptools>=68.0.0 +wheel>=0.42.0