15 KiB
Raw Blame History

name description category version
webapp-deploy 部署 Web 应用(应用脚手架部署到测试/生产机时必读——build.sh 标准、目录结构、测试环境、venv、git 仓库管理。不加载会漏 build.sh 标准步骤、部署目录不规范。业务模块开发用 module-development-spec部署环境信息从 env/{test,prod}.json 读。 devops 1.0.0

Web 应用部署规范

核心原则

  1. 所有代码提交到远端仓库,测试环境只能通过 build.sh 部署
  2. 所有模块从 git clone 到 pkgs/,禁止手动复制源码
  3. 禁止在测试环境直接修改代码,必须在本地开发 → git push → 测试环境 git pull + build.sh
  4. 应用有独立 venv,不共享其他应用的 Python 环境
  5. 部署前先确认运行进程的真实工作目录readlink /proc/$(pgrep -f <app>.py | head -1)/cwd。同一台服务器上可能存在同一应用的多份 checkout不同用户/不同目录home 软链也可能误导git pull 和重启必须落在运行进程实际加载代码的那份上,否则"部署成功"但行为完全不变2026-08 ragserver 事故:在 apitest 的 ~/ragserver 上验证"HEAD=修复版",实际进程跑的是 rag 用户 /d/rag/ragserver 的旧代码)

目录结构

~/<app>/                  # 应用根目录
├── app/                  # 入口代码
├── build.sh              # 一键构建脚本
├── start.sh / stop.sh    # 启停
├── conf/config.json      # 应用配置
├── pkgs/                 # 所有依赖模块 clone 到此
│   ├── apppublic/        # git clone
│   ├── sqlor/
│   ├── ahserver/
│   ├── rbac/
│   ├── bricks/
│   ├── <业务模块>/       # git clone
│   └── ...
├── py3/                  # 独立 venv
├── wwwroot/              # 前端文件
├── logs/
├── files/
└── scripts/

build.sh 标准流程

  • 硬性约束build.sh 必须 fail-fast——脚本开头 set -e(或每步显式检查返回码),任何一步失败(建表/插种子/pip install/xls2ui立即 exit 1 终止,禁止报错后继续执行后续步骤或假装成功。特别init_data.py 插种子失败JSON 解析错误/表缺失)时,若 build.sh 仍继续 start app,会导致应用带缺表/缺种子启动、/healthz 失败且部署日志里同时出现报错和「done」造成「看似成功」的假象。build.sh 末尾的 start 必须只在前面所有步骤都成功后才执行。
  1. 创建独立 venv: python3 -m venv py3
  2. build.sh 创建 pkgs/ 目录并 cd pkgs将所有模块 git clone 进来
    • 基础共享包apppublic, sqlor, ahserver, rbac, xls2ddl, appbase, bricks-for-python, bricks
    • 本项目业务模块:必须先提交到远端仓库再 clone禁止 mv/复制源码进 pkgs/(模块开发完成即须推送远端,部署端只认远端仓库)
  3. 构建 bricks 前端: cd pkgs/bricks/bricks && bash build.sh(生成 ../dist→ 应用目录 ln -sf ../pkgs/bricks/dist wwwroot/bricks
  4. pip install 所有 pkgs/ 模块xls2ddl 提供 json2ddl/xls2ui/dbloader 三个 CLI装进 venv 后 ${VENV}/bin/ 下可用)
  5. 逐模块执行四步安装(见下节「模块安装四步」,每个业务模块都走一遍,顺序不可跳)
  6. 模块 wwwroot 软链接(非 cp四步③已含: ln -sf ../pkgs/$mod/wwwroot wwwroot/$mod
  7. 下划线→连字符软链接: ln -sf pipeline-sdlc wwwroot/pipeline_sdlc
  8. 修复 created_by: sed 注入 ns['created_by'] = userorgid
  9. 创建 runtime 目录

模块安装四步(每个业务模块必走,一键部署代码必须包含)

模块代码到位后,逐个模块按以下四步安装,任何一步失败即终止(配合上方 fail-fast 约束):

MOD=<模块名>   # 如 world
# ① 建表:进 models 执行 json2ddl 生成 DDL → 建表
cd "pkgs/${MOD}/models"
"${VENV}/bin/json2ddl" mysql . > "/tmp/${MOD}_ddl.sql"
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" < "/tmp/${MOD}_ddl.sql"
cd - >/dev/null

# ② CRUD 页面:进 json 执行 build.sh标准内容一行
#    xls2ui -m ../models -o ../wwwroot <模块名> *.json
cd "pkgs/${MOD}/json"
bash build.sh
cd - >/dev/null

# ③ wwwroot 软链(非 cpln -sf ../pkgs/${MOD}/wwwroot wwwroot/${MOD}
ln -sf "../pkgs/${MOD}/wwwroot" "wwwroot/${MOD}"

# ④ 初始化数据:模块有 data/*.xlsx 时用 dbloader 导入(无则跳过)
if [ -d "pkgs/${MOD}/data" ] && ls pkgs/${MOD}/data/*.xlsx >/dev/null 2>&1; then
  cd "pkgs/${MOD}/data"
  for f in *.xlsx; do "${VENV}/bin/dbloader" "${DEPLOY_DIR}" "${DB_NAME}" "$f"; done
  cd - >/dev/null
fi

要点:

  • ①先于②xls2ui 生成页面时会读 models 定义,建表失败不拦住会让应用带缺表启动
  • ②的 json/build.sh 是模块仓库自带文件(标准内容就一行 xls2ui -m ../models -o ../wwwroot <模块名> *.json),模块开发时就必须生成它,部署端只执行不现编
  • ④的 dbloader 参数dbloader <应用部署目录> <库名> <xlsx文件>,在模块 data/ 目录下逐个执行
  • 基础框架表users/organization/role/permission 等)不在此四步内——它们在应用自己的 scripts/ddl.sql 里一次建好(全新库必备,否则 rbac 查询报错)

部署前置

  • 手工创建应用独立数据库(如 CREATE DATABASE pipeline CHARACTER SET utf8mb4
  • 写入 conf/config.jsondatabases.<app_db> 配置
  • sage 库仅用于 rbac/session 共享服务

config.json 配置

  • SAGE_RBAC_DB: 直接设为应用的数据库名(如 rag),不通过 module_dbname 映射
  • databases: 应用独立库
  • password_key: 应用自身的加密密钥(独立应用不共享 Sage key
  • session 持久化需要 Redis 配置: 无 session_max_timesession_issue_time 时 session 仅存内存,重启即丢失用户登录态
"session_max_time": 3000,
"session_issue_time": 2500,
"session_redis": {"host": "127.0.0.1", "port": 6379, "db": 1}
  • 不设 module_dbnameget_module_dbname(m) 全部返回应用数据库名
  • DB 密码必须 AES 加密 — sqlor 的 SQLor.__init__ 始终调用 aes_decode_b64(key, password),明文密码会抛 binascii.Error: Incorrect padding。用 aes_encode_b64(password_key, plaintext) 生成密文再写入 config
  • sqlor 配置细节 → 详见 references/sqlor-config-pitfalls.md
  • RBAC 表迁移 → 详见 references/rbac-migration.md

初始化

source py3/bin/activate
python scripts/load_path.py    # RBAC 权限注册
python scripts/init_data.py     # 初始数据appcodes 等)

部署验证(必须)

部署后不可只检查语法或 API 返回码即宣布成功:

  1. 验证必须针对运行进程实际加载的那份 checkout(先 readlink /proc/<pid>/cwd 确认),不要在任何"看起来像"的目录上验证后就声称部署成功——子代理报告"HEAD=修复版"不可直接采信hub 必须亲自在运行目录复核
  2. curl 逐路径测 HTTP 码
  3. 浏览器登录后逐菜单点击,确认每个页面正常加载
  4. 新增/编辑表单每个字段测试可操作性
  5. 下拉选项确认显示正确文本(非 undefined/乱码)
  6. 验证权限:登录用户应能访问所有已注册路径
  7. 修 bug 类部署:用触发 bug 的完整请求端到端复测(登录→参数→断言返回内容),而不是只测 HTTP 200
  8. 用户仍报 bug 时,先确认他看到的是不是已部署版本grep 用户页面文案里的字面量(按钮文字、提示语)在服务器 .ui/.js 文件中是否存在。grep 不到 → 用户浏览器加载的是缓存旧前端或另一份部署后端再修也没用2026-08 ragserver用户页面有 "Search/Clear/drop in or click to choose file" 文案,而已部署 search.ui 里完全没有这些文本 → 旧缓存 UI
  9. 后端 curl 正常但用户页面异常dspy 顶部加 info(params_kw) 日志(无需 import部署重启后让用户复现一次再 grep 日志。用户操作完全没进日志 → 请求没到这个实例:cat /etc/nginx/sites-enabled/<app> 看 proxy_pass 端口 vs 运行进程监听端口(ss -ltnp。同机多个相似应用监听不同端口时ragserver9180 别的应用,域名实际转 9181curl 本地端口正常 ≠ 域名链路正常。

测试环境

  • SSH 到测试服务器
  • 应用源码在 ~/<app>/
  • 执行 git pull && bash build.sh && ./stop.sh && ./start.sh
  • 禁止在测试机上手动修改文件

常见排查和修复

app.py 入口初始化顺序

ServerEnv 必须在 load_rbac / load_appbase 之前设置,否则 rbac 内部调用 env.get_module_dbname('rbac') 时 env 还是空的,抛 TypeError: 'NoneType' object is not callable

正确顺序:

def init():
    env = ServerEnv()
    env.get_module_dbname = get_module_dbname   # ← 最先
    env.password_encode = password_encode
    load_appbase()
    load_rbac()
    # ... 其他初始化

venv 缺少 wheel → pip install -e 失败

错误 invalid command 'bdist_wheel' → venv 未安装 wheel。修复pip install wheel 再重试。

bricks 软链接路径

bricks 软链接从 wwwroot 目录创建,必须用相对路径:

ln -sf ../pkgs/bricks/dist wwwroot/bricks   # ✅ 正确
ln -sf pkgs/bricks/dist wwwroot/bricks       # ❌ 从 wwwroot 看找不到

⚠️ wwwroot/bricks 必须是软链接 → pkgs/bricks/dist,绝不能是实体目录build.sh 标准:rm -rf wwwroot/bricks && ln -sf pkgs/bricks/dist wwwroot/bricks)。手动"修复"时若创建实体目录并复制/链接了 pkgs/bricks/bricks/ 的源文件 bricks.js~23KB前端立即崩溃bricks.js:684 Class extends value undefined + bricks.App is not a constructor → 页面空白。诊断:ls -la wwwroot/ | grep bricks(有无 ->+ wc -c wwwroot/bricks/bricks.js(编译版 ~424KB源版 ~23KB。修好后用户仍报同样错误 → 对比服务器报错行号内容与浏览器报错是否吻合,不吻合就是浏览器缓存,让用户 Ctrl+Shift+R2026-08 pipeline-app 事故:反复改服务器无果,根因是实体目录+源版 bricks.js

非 pip 包的模块:用 PYTHONPATH 不用 pip install -e

如果模块没有 setup.py / pyproject.toml(如 rag-pipeline 是纯 ahserver 应用),不能 pip install。改为在 start.sh 的 PYTHONPATH 中加入该模块路径:

export PYTHONPATH=$cdir:$cdir/pkgs/rag-pipeline:$PYTHONPATH

PYTHONPATH 必须包含应用根目录

入口 app/ragserver.py 中有 from init import xxx,需要应用根目录在 PYTHONPATH 中,否则 ModuleNotFoundError: No module named 'init'

陈旧 pip 进程清理

多次后台 build 尝试可能残留旧 pip 进程,占用资源并导致后续安装冲突:

pkill -9 -f 'pip install' 2>/dev/null

BricksUIProcessor 需要 .tmpl 处理器

ahserver 的 BricksUIProcessor(处理 .ui 文件)内部会加载 /bricks/header.tmpl 等模板文件。如果 config.jsonwebsite.processors 缺少 [".tmpl", "tmpl"],会报 AttributeError: 'NoneType' object has no attribute 'be_call'/ 和 /index.ui 返回 500

必须包含:

"processors": [
    [".tmpl", "tmpl"],
    [".ui", "bui"],
    [".dspy", "dspy"],
    ...
]

sqlor 数据库 driver

  • MySQL 必须用 "driver": "mysql",不能用 "mysql+aiomysql"。sqlor 的 MySqlor.isMe() 只匹配 mysql / aiosqlor / tidb,其他值导致 Not Implemented
  • kwargs 中禁止 minsize / maxsize — aiomysql.connect 不接受这些参数

dspy 文件返回格式

  • ahserver 将 dspy 代码包装为 async def myfunc(request, **ns):await 执行
  • 必须用 return 语句显式返回值 — 最后一行裸 result 在 async def 中返回 None
  • async with db.sqlorContext() 可在 dspy 中正常工作(因为外层 wraper 是 await 的)
  • fallback 模式: async with 内 return 成功值,外 return 错误值
async with db.sqlorContext(dbname) as sor:
    ...
    return {"status": "SUCCEEDED"}
return {"error": "failed"}

app.py 必须 import bricks_for_python 并调用 load_pybricks

ahserver 的 bui 处理器由 bricks_for_python 包注册。入口文件需:

import bricks_for_python
from bricks_for_python.init import load_pybricks

def init():
    ...
    load_pybricks()  # 注册 UiWindow, UiError 等工具函数

build.sh 同步 password_key 时仅改单个字段

python3 -c "c=json.load(...); c['password_key']=k; json.dump(c,...)" 是安全的——只改 key 不覆盖 processors/databases。切勿用 sed 替换整行,会破坏 JSON 结构。

SSH key 部署到新测试机

# 生成或复制 key
ssh-keyscan -H git.opencomputing.cn >> ~/.ssh/known_hosts
scp ~/.ssh/id_rsa user@test-host:~/.ssh/id_rsa

git pull 因 config.json 本地修改冲突

每次部署时手动修改 config.jsondriver/password 等),导致下次 git pull 冲突。标准流程:

git checkout conf/config.json && git pull
# 然后 python3 脚本重新修正 driver/password

常见检查项

  • appcodes_kv 表需 utf8mb4 字符集
  • CRUD 下拉 valueField="value" textField="text" 匹配 dspy 返回
  • 新 dspy 必须注册 RBAC 权限
  • created_by 自动赋 userorgidxls2ui 会覆盖build.sh 中 sed 修复)
  • bricks 链接: ln -sf pkgs/bricks/dist wwwroot/bricks
  • bricks build 前置: mkdir -p ../dist/i18n
  • menu 点击嵌套全页: wwwroot 子目录缺失→fallback 根 index.uiln -sf pkgs/*/wwwroot wwwroot/
  • pipeline_sdlc 下划线: ln -sf pipeline-sdlc wwwroot/pipeline_sdlc
  • config.json 只同步 password_key不覆盖 databases应用有独立 DBDB 密码必须 AES 加密
  • get_module_dbname(m): return 'pipeline' 全模块统一
  • password_encode(s): if s is None: return '' 兼容 login form 首次加载
  • RBAC 通配符: /**/*
  • 部署后验证: 逐菜单点击+curl HTTP 码双重确认
  • 详细 CRUD 坑汇总: 见 references/bricks-crud-pitfalls.md
  • pipeline-app 实战经验: 见 references/pipeline-app-pitfalls.md
  • RBAC 数据库迁移: 见 references/rbac-migration.md
  • sqlor 配置 (driver/密码/RBAC列名): 见 references/sqlor-config-pitfalls.md
  • 用户密码必须 RC4 加密 (与 DB 的 AES 不同): 见 references/rbac-user-auth.md
  • Bricks UI 布局/按钮/菜单约定: 见 references/bricks-ui-conventions.md
  • RAG 应用部署记录: 见 references/rag-deployment.md
  • RAG Server 验证工作流 (RBAC session认证 + smoke test): 见 references/ragserver-verification.md
  • GPU 模型下载: 见 references/gpu-model-download.md