From fa7e8166fc6fbcebd650aa64e6ddc576bb9498b6 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Tue, 22 Sep 2026 13:48:36 +0800 Subject: [PATCH] =?UTF-8?q?build(pbls):=20=E6=81=A2=E5=A4=8D=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=20build.sh=20=E5=B9=B6=E6=8E=A5=E5=85=A5=20M9=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=B1=82=E9=A1=B5=E9=9D=A2=E5=A3=B3=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=AD=A5=E9=AA=A4=EF=BC=888b=20=E6=AE=B5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 f9b1f55 恢复 182 行完整一键部署脚本正文(此前收口提交误将 5 行索引占位 作为交付事实,导致 die/log/${WS_DIR}/${PY} 等符号无定义、bash build.sh rc=127) - 在模块安装循环(pip install -e modules/pbl_*)之后、py_compile/wwwroot 软链 之前新增 8b 段:调用 modules/pbl_scense_ext/scripts/gen_overlay_pages.py 生成 12 个覆盖层页面壳,并紧随其后以 --check 作为部署门禁,rc!=0 终止并打印原因 - 返回码用 GEN_RC/CHK_RC 显式捕获(不能用 if ! cmd 后取 $?,会被取反成 0) --- build.sh | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 186 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index 623bd4a..f49dc8d 100644 --- a/build.sh +++ b/build.sh @@ -1,5 +1,188 @@ -# 见仓库文件:新增 8b 段(16 行)——覆盖层页面壳生成 + --check 门禁 +#!/usr/bin/env bash +# ===================================================================== +# PBL Agent OS 验证原型(Phase 0/1)—— 一键部署脚本(test 环境) +# 端口唯一事实源:projects/pbls/env/test.json(9300);本脚本不硬编码端口,从 env 读取 +# 门禁:建表 36 张 + 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']")" +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)---- +[ -f "${DDL_FILE}" ] || die "缺少 DDL ${DDL_FILE}" +if grep -Eiq 'BIGSERIAL|SERIAL|nextval|FOREIGN KEY|REFERENCES|ENUM\(|TIMESTAMP' "${DDL_FILE}"; then + grep -Ein 'BIGSERIAL|SERIAL|nextval|FOREIGN KEY|REFERENCES|ENUM\(|TIMESTAMP' "${DDL_FILE}" | head -20 + die "DDL 含非 mariadb 方言关键字(应为零命中)" +fi +TABLE_COUNT="$(grep -c 'CREATE TABLE IF NOT EXISTS' "${DDL_FILE}")" +[ "${TABLE_COUNT}" = "36" ] || die "DDL 表数应为 36,实际 ${TABLE_COUNT}" +AUTOINC_COUNT="$(grep -c 'BIGINT NOT NULL AUTO_INCREMENT' "${DDL_FILE}")" +[ "${AUTOINC_COUNT}" = "36" ] || die "BIGINT AUTO_INCREMENT 主键应为 36 处,实际 ${AUTOINC_COUNT}" +log "硬门禁 6.4 通过: mariadb 方言,36 表 / 36 处 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" + +# ---- 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 软链之前。 OVERLAY_GEN="${WS_DIR}/modules/pbl_scense_ext/scripts/gen_overlay_pages.py" [ -f "${OVERLAY_GEN}" ] || die "缺少覆盖层页面壳生成器: ${OVERLAY_GEN}(modules/pbl_scense_ext 未就位)" -if ! "${PY}" "${OVERLAY_GEN}"; then die "覆盖层页面壳生成失败"; fi -if ! "${PY}" "${OVERLAY_GEN}" --check; then die "覆盖层页面壳门禁校验未通过"; fi +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 一致)" + +# ---- 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}")" +[ "${ACTUAL_TABLES}" = "36" ] || die "建表数应为 36,实际 ${ACTUAL_TABLES}" +log "建表完成: pbl_* 共 ${ACTUAL_TABLES} 张" + +# ---- 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 + +# ---- 13. 启动应用(唯一入口 + 唯一端口)---- +log "启动 ${APP_NAME} 于端口 ${PORT}" +mkdir -p "${APP_DIR}/logs" "${APP_DIR}/run" +nohup "${PY}" "${ENTRY}" > "${APP_DIR}/logs/${APP_NAME}.out" 2>&1 & +echo $! > "${APP_DIR}/run/${APP_NAME}.pid" +sleep 5 + +# ---- 14. 健康检查 ---- +HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/api/public/health" || echo 000)" +log "健康检查 HTTP ${HTTP_CODE}" +[ "${HTTP_CODE}" = "200" ] || die "健康检查失败(HTTP ${HTTP_CODE}),查看 ${APP_DIR}/logs/${APP_NAME}.out" + +log "BUILD SUCCESS: ${APP_NAME} 已运行于 http://127.0.0.1:${PORT}/ (部署路径 ${DEPLOY_PATH})"