Files
DadaoTask/2.Design/DEPLOY.md
T
Steven 483b554c3c
CI / backend (push) Canceled after 0s
feat(web): Web 装到桌面(PWA),带安装引导与发版提示
用 vite-plugin-pwa 生成 manifest 与 Service Worker,不自己写缓存逻辑。

- 装到桌面:manifest 名字/主题色/三个图标(any + maskable + apple-touch-icon),
  图标和页头、安卓桌面是同一枚;首页底部给安装引导,能装就弹「安装」,
  iOS 给「分享 → 添加到主屏幕」,关掉记在 localStorage
- 发版提示:registerType=prompt,收到新版本只在底部提示,点「刷新」才换。
  故意不自动刷新——Vditor、图表是懒加载分片,后台换 chunk 会让面板 404
- 缓存边界:只预缓存页面骨架(约 0.8MB),/api/ 一律 NetworkOnly,
  离线宁可报错也不端旧数据;/vditor/ 走 CacheFirst,不拖慢首次安装
- 顺手修:UpdatePrompt 居中改用 flex 包一层,animate-slide-up 的 keyframes
  自带 transform,会和 -translate-x-1/2 抢属性,手机窄屏会把卡片推出屏幕

验证:web/e2e/pwa.mjs 19 项全绿(manifest/SW 激活/预缓存/安装引导/iOS 文案/
发版提示/断网仍能打开);tsc 通过、vite build 通过。

已知限制:Service Worker 只在 https 或 localhost 注册,生产入口还是 http,
所以内网 IP 上装不了——需要域名 + 证书反代或隧道,已写进 DEPLOY.md 第 6 节。
2026-10-31 04:33:43 +08:00

11 KiB
Raw Blame History

部署(生产 · openclaw-01)

适用于当前唯一的线上环境:openclaw-01(192.168.8.188),访问入口 http://192.168.8.188:8200。 本地开发见 DEVELOPMENT.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)

# 口令自己生成,别用示例里的
OWNER_PW='<dadao_prod_owner 的口令>' APP_PW='<dadao_prod_app 的口令>' \
  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 身份)

git clone --branch main --single-branch http://gitea.tatta.cn/Tatta/DadaoTask.git ~/app/dadaotask

2.3 配置(不进仓库)

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 装服务单元

mkdir -p ~/.config/systemd/user
cp ~/app/dadaotask/deploy/prod/dadaotask-api.service ~/.config/systemd/user/
systemctl --user daemon-reload

2.5 首次上线

bash ~/app/dadaotask/deploy/prod/deploy.sh

3. 日常发布(每次上线)

# 本地:提交并 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. 部署后校验

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 出包(本机)

cd android
export JAVA_HOME=<JDK 17>                 # 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 发布清单

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 传到服务器

scp backend/static/apk/* openclaw-01:~/app/dadaotask/backend/static/apk/

deploy.sh 的 git reset --hard 不会删未跟踪文件,所以已发布的 APK 在服务器上能留住; 但换了机器或清了目录就要重新传一次。不需要重启服务:接口每次请求都现读清单与文件。

5.4 验证

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. 运维

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 别人占满连接或内存会波及本站 独立实例