12 KiB
Raw Blame History

name description category version
webapp-deploy 所有 Web 应用通用部署规范:build.sh 标准、目录结构、测试环境、venv、git 仓库管理 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 标准流程

  1. 创建独立 venv: python3 -m venv py3
  2. clone 基础共享包到 pkgs/(apppublic, sqlor, ahserver, rbac, xls2ddl, appbase, bricks-for-python)
  3. 构建 bricks 前端: mkdir -p bricks/dist → cd bricks/bricks && bash build.sh → ln -sf ../pkgs/bricks/dist wwwroot/bricks
  4. 本地业务模块 mv 到 pkgs/
  5. clone 外部业务模块到 pkgs/
  6. pip install 所有 pkgs/ 模块
  7. xls2ui 从 json/ 生成 CRUD
  8. 模块 wwwroot 软链接(非 cp): ln -sf ../pkgs/$mod/wwwroot wwwroot/$mod
  9. 下划线→连字符软链接: ln -sf pipeline-sdlc wwwroot/pipeline_sdlc
  10. 修复 created_by: sed 注入 ns['created_by'] = userorgid
  11. 创建 runtime 目录

部署前置

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

config.json 配置

  • SAGE_RBAC_DB: 直接设为应用的数据库名(如 rag),不通过 module_dbname 映射
  • databases: 应用独立库
  • password_key: 应用自身的加密密钥(独立应用不共享 Sage key)
  • session 持久化需要 Redis 配置: 无 session_max_time 和 session_issue_time 时 session 仅存内存,重启即丢失用户登录态
"session_max_time": 3000,
"session_issue_time": 2500,
"session_redis": {"host": "127.0.0.1", "port": 6379, "db": 1}
  • 不设 module_dbname:get_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)。同机多个相似应用监听不同端口时(ragserver:9180 别的应用,域名实际转 9181),curl 本地端口正常 ≠ 域名链路正常。

测试环境

  • 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+R(2026-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.json 的 website.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.json(driver/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 自动赋 userorgid(xls2ui 会覆盖,build.sh 中 sed 修复)
  • bricks 链接: ln -sf pkgs/bricks/dist wwwroot/bricks
  • bricks build 前置: mkdir -p ../dist/i18n
  • menu 点击嵌套全页: wwwroot 子目录缺失→fallback 根 index.ui,需 ln -sf pkgs/*/wwwroot wwwroot/
  • pipeline_sdlc 下划线: ln -sf pipeline-sdlc wwwroot/pipeline_sdlc
  • config.json 只同步 password_key,不覆盖 databases(应用有独立 DB);DB 密码必须 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