--- name: webapp-deploy description: 部署 Web 应用(应用脚手架部署到测试/生产机)时必读——build.sh 标准、目录结构、测试环境、venv、git 仓库管理。不加载会漏 build.sh 标准步骤、部署目录不规范。业务模块开发用 module-development-spec,部署环境信息从 env/{test,prod}.json 读。 category: devops version: 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 .py | head -1)/cwd`。同一台服务器上可能存在同一应用的多份 checkout(不同用户/不同目录,home 软链也可能误导),git pull 和重启必须落在运行进程实际加载代码的那份上,否则"部署成功"但行为完全不变(2026-08 ragserver 事故:在 apitest 的 ~/ragserver 上验证"HEAD=修复版",实际进程跑的是 rag 用户 /d/rag/ragserver 的旧代码) ## 目录结构 ``` ~// # 应用根目录 ├── 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 约束): ```bash 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 软链(非 cp):ln -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 <应用部署目录> <库名> `,在模块 data/ 目录下逐个执行 - 基础框架表(users/organization/role/permission 等)不在此四步内——它们在应用自己的 `scripts/ddl.sql` 里一次建好(全新库必备,否则 rbac 查询报错) ## 部署前置 - 手工创建应用独立数据库(如 `CREATE DATABASE pipeline CHARACTER SET utf8mb4`) - 写入 `conf/config.json` 的 `databases.` 配置 - `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 仅存内存,重启即丢失用户登录态 ```json "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` ## 初始化 ```bash source py3/bin/activate python scripts/load_path.py # RBAC 权限注册 python scripts/init_data.py # 初始数据(appcodes 等) ``` ## 部署验证(必须) 部署后不可只检查语法或 API 返回码即宣布成功: 1. **验证必须针对运行进程实际加载的那份 checkout**(先 `readlink /proc//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/` 看 proxy_pass 端口 vs 运行进程监听端口(`ss -ltnp`)。同机多个相似应用监听不同端口时(ragserver:9180 别的应用,域名实际转 9181),curl 本地端口正常 ≠ 域名链路正常。 ## 测试环境 - SSH 到测试服务器 - 应用源码在 `~//` - 执行 `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`。 正确顺序: ```python 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 目录创建,必须用相对路径: ```bash 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 中加入该模块路径: ```bash 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 进程,占用资源并导致后续安装冲突: ```bash 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)。 **必须包含**: ```json "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 错误值 ```python 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` 包注册。入口文件需: ```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 部署到新测试机 ```bash # 生成或复制 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` 冲突。标准流程: ```bash 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`