# 部署(生产 · openclaw-01) > 适用于当前唯一的线上环境:`openclaw-01`(192.168.8.188),访问入口 `http://192.168.8.188:8200`。 > 本地开发见 [`DEVELOPMENT.md`](DEVELOPMENT.md)。部署铁律见仓库根 [`AGENTS.md`](../AGENTS.md) 第 2 节。 ## 1. 生产拓扑 ``` 浏览器 ──HTTP──▶ openclaw-01:8200 └ gunicorn(2 × UvicornWorker) ── app.main:app ├ /api/v1/** 业务 API └ / 前端产物 web/dist(单页应用,404 回退 index.html) │ ┌────────────────────────┼────────────────────────┐ ▼ ▼ ▼ fnsvr-01:8432 PG 17.6 fnsvr-01:6379 Redis 本机 backend/var/uploads 库 dadaotask_prod db 6(独立库号) 附件文件 角色 dadao_prod_app(RLS) 角色 dadao_prod_owner(迁移) ``` 为什么是这个形状: - **生产库和开发库在同一个 PG 实例**(fnsvr-01),所以生产用**独立的库 + 独立的角色**。复用 `dadao_app` 等于拿到开发口令就能连生产。迁移里的 `GRANT`/`DEFAULT PRIVILEGES` 按 `APP_DB_ROLE` / `OWNER_DB_ROLE` 生成,靠 `backend/app/roles.py` 注入。 - **Redis 用 6 号库**隔离:共享实例没有密码,靠库号避免和别的项目撞键(验证码、幂等键)。 - **没有 nginx**:openclaw 账号无 sudo 密码,改不了系统 nginx,所以后端单端口同时托管 API 与前端产物(同源,免 CORS)。root 愿意配合时按 `deploy/prod/nginx.dadaotask.conf.example` 加反代,不用改应用。 - **用户级 systemd**:openclaw-01 上所有服务(easymemo、portal-site…)都是 `systemctl --user` 起的,本方案沿用,全程不需要 root。 ## 2. 首次部署(一次性) 前置:能 SSH 到 openclaw-01;fnsvr-01 的 root 权限(只在建库这一步需要)。 ### 2.1 建生产库与角色(在 fnsvr-01 上,root) ```bash # 口令自己生成,别用示例里的 OWNER_PW='' APP_PW='' \ bash deploy/prod/provision-db.sh ``` 会创建:库 `dadaotask_prod`(owner `dadao_prod_owner`)、角色 `dadao_prod_owner` / `dadao_prod_app`(`NOSUPERUSER NOBYPASSRLS`,否则 RLS 会被跳过)、`public` schema 归 owner、`REVOKE CREATE ... FROM PUBLIC`。建表与 RLS 策略由 alembic 负责(2.4)。 ### 2.2 代码落地(在 openclaw-01 上,openclaw 身份) ```bash git clone --branch main --single-branch http://gitea.tatta.cn/Tatta/DadaoTask.git ~/app/dadaotask ``` ### 2.3 配置(不进仓库) ```bash mkdir -p ~/.config/dadaotask && chmod 700 ~/.config/dadaotask cp ~/app/dadaotask/deploy/prod/env.production.example ~/.config/dadaotask/env.production chmod 600 ~/.config/dadaotask/env.production vim ~/.config/dadaotask/env.production # 替换全部 __CHANGE_ME__ ``` 必填项:`DATABASE_URL` / `MIGRATION_DATABASE_URL`(2.1 的口令)、`JWT_SECRET`(≥32 位随机串)、`REDIS_URL`、`WEB_DIST_DIR`、`UPLOAD_DIR`。 两个踩过的坑: - **`CORS_ORIGINS` 必须写成 JSON 数组**(`'["http://...","http://..."]'`)。它是 list 字段,逗号分隔的字符串会让 pydantic-settings 当 JSON 解析并抛 `SettingsError`,alembic 这一步就挂。 - **Redis 有口令**:fnsvr-01 的 redis 开了 `requirepass`(口令看 `docker inspect redis`),写进 URL 时要百分号编码(`#` → `%23`),否则连不上、发码/登录直接 500。 **当前状态是内测模式**:短信通道还没接(非 dev provider 直接 501),所以 `SMS_PROVIDER=dev` + `INTERNAL_TEST_LOGIN=true`,固定验证码 `123456`。启动日志会打 WARNING。**只允许内网访问**;短信通道接好后删掉 `INTERNAL_TEST_LOGIN` 并把 `SMS_PROVIDER` 换成真实供应商。 ### 2.4 装服务单元 ```bash mkdir -p ~/.config/systemd/user cp ~/app/dadaotask/deploy/prod/dadaotask-api.service ~/.config/systemd/user/ systemctl --user daemon-reload ``` ### 2.5 首次上线 ```bash bash ~/app/dadaotask/deploy/prod/deploy.sh ``` ## 3. 日常发布(每次上线) ```bash # 本地:提交并 push 到 main(生产只跑仓库 HEAD) # 服务器: bash ~/app/dadaotask/deploy/prod/deploy.sh ``` 脚本做六件事,任何一步失败即中止:代码对齐 `origin/main`(`git reset --hard`,禁止服务器手工改)→ 装 Python 依赖 → `alembic upgrade head`(用迁移角色)→ **在服务器上** `npm ci && npm run build` → 写 `env.build` 并重启 → 校验 `/health` 与 `/version` 的 commit 等于 HEAD。 前端产物只在服务器上构建,**不允许**本地上传 dist(AGENTS.md 铁律 3)。 ## 4. 部署后校验 ```bash curl -s http://192.168.8.188:8200/health # {"status":"ok","db":true,...} curl -s http://192.168.8.188:8200/version # commit 必须等于 git rev-parse --short HEAD curl -sI http://192.168.8.188:8200/ | head -1 # 200,text/html(前端产物) ``` 浏览器验收:打开 `http://192.168.8.188:8200` → 手机号任意 + 验证码 `123456` 登录 → 任务/工单/知识各点一遍。 ## 5. 安卓安装包发布(OTA) APK 不进版本库,也不走 `deploy.sh`:代码发布和"发一个安装包"是两件事, 前者每次上线都做,后者只在安卓端有改动时做。 ### 5.1 出包(本机) ```bash cd android export JAVA_HOME= # AGP 8.7 不认 JDK 25 export GRADLE_USER_HOME=<可写的 gradle 目录> gradle -Dorg.gradle.jvmargs="-Djava.io.tmpdir=<可写 tmp>" assembleRelease # 产物:android/app/build/outputs/apk/release/app-release.apk ``` - 版本号在 `android/app/build.gradle.kts`:`versionCode` 是 OTA 的唯一判据,**每次发版必须 +1**, 否则客户端查不到更新;`versionName` 只是给人看的。 - 签名证书在 `android/keystore.properties`(本机文件,`.gitignore` 已排除)指向的 `.jks`。 **换证书等于换应用**:装了旧包的手机再也装不上新包,只能卸载重装。 - `dadao.apiBase`(`android/local.properties`)决定包里写死的后端地址。正式包指向 `http://192.168.8.188:8200`,模拟器调试包指向 `http://10.0.2.2:8100`。 ### 5.2 发布清单 ```bash python backend/scripts/publish_apk.py android/app/build/outputs/apk/release/app-release.apk \ --version-code 2 --version-name 0.2.0 --notes "新增应用内升级;图标与 Web 一致" ``` 脚本把 APK 复制成 `backend/static/apk/dadaotask-<版本>.apk`,并写 `latest.json` (含 `sha256`、`size`、`published_at`)。接口读的就是这份清单。 ### 5.3 传到服务器 ```bash scp backend/static/apk/* openclaw-01:~/app/dadaotask/backend/static/apk/ ``` `deploy.sh` 的 `git reset --hard` 不会删未跟踪文件,所以已发布的 APK 在服务器上能留住; 但换了机器或清了目录就要重新传一次。**不需要重启服务**:接口每次请求都现读清单与文件。 ### 5.4 验证 ```bash curl -s "http://192.168.8.188:8200/api/v1/app/release/latest?version_code=1" # data.version_code 为新版号、has_update=true、download_url 可下载 curl -sI http://192.168.8.188:8200/api/v1/app/release/download/dadaotask-0.2.0.apk | head -1 # 200 ``` 手机端:装上新包 → 下次启动自动查一次;也可以在首页底部点「检查更新」。 装完再启动,`version_code` 已是最新,接口返回 `has_update=false`,不会再弹。 ## 6. Web 装到桌面(PWA) 前端已经打成 PWA:`/manifest.webmanifest` 与 `/sw.js` 由后端静态层直接返回(`_SPAStaticFiles` 挂在 `/`, 不需要额外配置),`node_modules/.bin` 之外没有额外依赖。发布流程不变。 **装的条件是安全上下文**——浏览器只允许 `https` 或 `localhost` 注册 Service Worker,也就是说: | 入口 | 能不能装 | |---|---| | `https://<域名>/` | 能,手机和电脑都会出现安装入口 | | `http://127.0.0.1:8200/`(本机打开) | 能,仅限这台机器 | | `http://192.168.8.188:8200/` | **不能**,非安全上下文:不弹安装、SW 也不注册 | 内网 IP 目前就是这种情况,所以真要在手机上装到桌面,得给入口套一层 HTTPS: 在能装 nginx 的机器上(或 fnsvr-01 的 1Panel OpenResty)加一个 `443 → 127.0.0.1:8200` 的反代就好, 应用本身不用改;示例见 `deploy/prod/nginx.dadaotask.conf.example`。也可以用 Cloudflare Tunnel 之类的 隧道拿一个 https 域名,同样不用碰应用代码。 装好之后的两个行为: - **图标**:来自 `web/public/icons/`(any + maskable 两套,iOS 吃 `apple-touch-icon.png`), 与 Web 页头、安卓桌面图标是同一枚。 - **发版提示**:SW 采用 `registerType: "prompt"`,检测到新版本只在页面底部提示「有新版了」, 用户点「刷新」才切换。**故意不自动刷新**:Vditor、图表是懒加载分片,后台把 chunk 换掉之后, 用户下一次点开面板就是 404(编辑器白屏)。 - **离线**:只预缓存页面骨架(构建产物 + 图标,约 0.8MB),`/api/` 一律 NetworkOnly—— 宁可报错,也不拿旧数据糊弄人。 ## 7. 运维 ```bash systemctl --user status dadaotask-api # 状态 systemctl --user restart dadaotask-api # 重启 journalctl --user -u dadaotask-api -f # 日志(gunicorn 输出到 stdout) ``` - 附件落盘:`backend/var/uploads`(按工作区分子目录),删库不删文件,备份时一起打包 - 回滚:`cd ~/app/dadaotask && git reset --hard <旧 commit> && bash deploy/prod/deploy.sh`;**数据库迁移不回滚**,回滚代码前确认旧代码兼容新表结构 - 端口:`8200`(systemd 单元与 `deploy.sh` 的 `PORT` 默认值,两处要一致) ## 8. 已知限制(当前阶段) | 限制 | 影响 | 解除方式 | |---|---|---| | 没有 HTTPS / 域名 | 只能内网 IP 访问,令牌明文传输;**PWA 装不上**(非安全上下文) | 域名 + nginx + 证书(需 root) | | 内测固定验证码 | 谁都能用任意手机号登录 | 接短信通道,关 `INTERNAL_TEST_LOGIN` | | 无 pgvector | 语义检索走 `real[]` 全表扫描,数据量大了会慢 | 上独立 pgvector 实例(`deploy/docker-compose.vector.yml`) | | 上传目录在本机磁盘 | 机器坏了附件就没了 | 备份 `backend/var/uploads`,或将来到对象存储 | | 共享 PG / Redis | 别人占满连接或内存会波及本站 | 独立实例 |