Files
dsh/MOBILE_APP.md
T

137 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# dsh Mobile App 设计文档(手机远程连接 dsh)
> 结论先行:**做 PWA(移动 Web App),由 dsh 服务端自带、零额外部署**,已实现并通过手机视口实测。
> 团队私有工具不值得走应用商店分发;等需求稳定后再考虑用同一套 API 套原生壳。
---
## 1. 方案选型
| 方案 | 评价 |
|---|---|
| **A. PWA 移动 Web App(已实现)** | 服务端直接在 `GET /app` 托管页面,手机浏览器打开即用、"添加到主屏幕"后全屏运行。零安装、零上架、更新即刷新。单文件无外部依赖(LAN 内网也能用,不依赖 CDN) |
| B. Flutter / React Native 原生 App | 体验和推送能力更好,但要维护双端构建与分发(MDM/内测分发),对"管理设置和插件"这类低频管理场景过度 |
| C. 套壳 WebView(如 Capacitor) | 折中:复用 PWA 代码换原生分发与推送。作为二期可选 |
**PWA 与未来原生不冲突**:所有功能都走 §3 的 REST API,原生 App 只是换一个 UI 客户端。
---
## 2. 信息架构(已实现)
```
登录页
├─ 服务端地址(自动预填当前域名)
├─ 方式一:邀请码注册(新成员,POST /v1/tokens)
└─ 方式二:粘贴已有 Token(admin 发放 / /v1/tokens/self 加签)
主界面(底部 4 Tab)
⚙ 设置 生效配置列表(团队/个人来源与版本号标签)、编辑值、新增项;
顶部「📱 连接手机」卡片(admin):展示连接二维码、邀请码复制、多网卡地址候选切换
⬇ 插件 插件列表(版本/渠道/sha256/大小)、下载制品、admin 发布与删除
🛡 管理 仅 admin:创建成员(Token 只显示一次)、审计日志
👤 我的 身份信息、Token 列表与撤销、退出登录
登录页 服务端地址 + 邀请码/Token;「📷 扫码连接」按钮(HTTPS 下可应用内扫码);
支持 dsh 桌面端生成的二维码深链 /app#invite=… 自动预填
```
交互细节:
- **冲突处理**:编辑保存带 `base_version`,遇 409 弹确认框展示"服务器已被他人更新",用户可选强制覆盖(LWW)或放弃——与桌面客户端同一语义。
- **值即 JSON**:输入框接受 JSON 或普通文本,与服务端 `value_json` 模型一致。
- 权限自适应:非 admin 隐藏"管理"Tab、团队设置选项与插件删除按钮(服务端仍兜底校验)。
## 3. API 映射(全部复用现有服务端,零后端新增)
| App 功能 | API |
|---|---|
| 邀请码登录 | `POST /v1/tokens` |
| 身份 / 会话校验 | `GET /v1/me`(401 即回登录页) |
| 设置列表(合并视图 + ETag) | `GET /v1/settings` |
| 编辑/新增设置(乐观并发) | `PUT /v1/settings/{scope}/{key}` |
| 插件列表 / 制品下载 | `GET /v1/plugins`、`GET /v1/plugins/{id}/{ver}/download` |
| 发布 / 删除插件(admin) | `POST /v1/plugins/{id}`、`DELETE /v1/plugins/{id}/{ver}` |
| 成员管理 / 审计 / Token 撤销 | `POST /v1/users`、`GET /v1/audit`、`DELETE /v1/tokens/{hash}` |
## 4. 手机如何连上(按推荐顺序)
0. **扫码连接(最省事,已实现)**:桌面浏览器打开 `/app` 并用 admin 登录 → 设置 Tab → 「📱 连接手机」→ 手机**系统相机**对准二维码 → 自动打开本页并填好服务端地址与邀请码 → 输入用户名 → 连接。二维码内容由服务端 `GET /v1/connect/qr` 生成(深链 `http(s)://<地址>/app#invite=<邀请码>`,segno 渲染 SVG);多网卡机器会在卡片里列出全部候选地址,点选即切换二维码。应用内扫码按钮仅在 HTTPS(或 localhost)下可用——LAN HTTP 环境浏览器禁止网页调用摄像头,系统相机是唯一稳定通道。
1. **同一 Wi-Fi/局域网(当前运行模式)**:手机浏览器打开 `http://<服务机IP>:8020/app(8000/8010 已被本机 Django 开发占用,dsh 固定用 8020)`。Windows 首次启动 uvicorn 会弹防火墙授权,勾选"专用网络"允许。
2. **异地团队 → Tailscale**:服务机与手机都装 Tailscale,手机访问 `http://<tailscale-ip>:8020/app`。零公网暴露,最推荐。
3. **公网 + HTTPS**:用仓库里的 docker compose(Caddy 自动证书绑定 `DSH_SYNC_DOMAIN`)。**PWA 的"添加到主屏幕"与 Token 存储在 HTTPS 下体验最稳**,纯 HTTP 下 iOS Safari 会限制部分能力。
连接后:iOS Safari → 分享 → "添加到主屏幕";Android Chrome → 菜单 → "添加到主屏幕",之后从桌面图标全屏进入。
## 5. 安全设计
- Token 只存手机 `localStorage`,随请求 Bearer 头发送;401 自动清会话回登录页。
- "我的"Tab 可随时**远程撤销**任意设备 Token(丢手机立刻处置)。
- admin 创建成员的 Token 只显示一次,提示立即复制。
- 传输安全依赖网络形态(§4):公网必须走 compose + Caddy 的 HTTPS;LAN 内网 HTTP 可接受(团队私有数据 + 可随时撤销的 Token)。
- **DeepSeek API Key 依然只在各设备本地**,App 与服务端都不存(见 ARCHITECTURE.md §7)。
## 6. 后续路线(按需做,不建议一次性)
| 优先级 | 能力 | 说明 |
|---|---|---|
| ✅ 已完成 | 扫码连接 | 服务端生成含地址+邀请码的二维码(`/v1/connect/qr`,segno SVG),PWA 设置 Tab「连接手机」卡片展示;登录页支持 `#invite=` 深链自动预填 + 应用内扫码(BarcodeDetector,需 HTTPS) |
| ✅ 已完成 | 安卓 APK | `android/` 原生 WebView 壳,`bash android/build.sh` 构建;`/apk` 免登录安装页 + 下载二维码。见 ARCHITECTURE.md §12 |
| P1 | Service Worker 离线缓存 | 断网时仍可查看上次拉到的设置/插件清单 |
| P2 | Capacitor 套壳 | 复用本 PWA 换原生分发,获得推送能力 |
| P2 | 插件发布审核流 | 发布走"待审核"状态,admin 在手机上批准 |
| P3 | 推送通知(插件更新/审计告警) | 需要原生或统一推送服务,PWA Web Push 在国内生态不可靠 |
## 7. 已实现与已验证
- ✅ `GET /app`:服务端自带单文件 PWA(`server/dsh_sync/static/mobile.html`,无外部依赖)
- ✅ 扫码连接:`connect.py`(内网地址探测,RFC1918 优先)+ `GET /v1/connect/info` / `GET /v1/connect/qr`(admin-only)+ 设置 Tab「连接手机」卡片 + 登录页深链预填/应用内扫码
- ✅ 手机视口(390×844)实测:Token 登录 → 新增团队设置(写入 PG 成功)→ 插件列表/下载/发布 → 管理页审计日志渲染;连接二维码卡片渲染、候选地址切换、`#invite=` 深链预填实测通过
- ✅ 截图:`dsh-app-settings.png`、`dsh-app-plugins.png`、`dsh-app-admin.png`、`dsh-app-connect-qr.png`、`dsh-app-connect-phone.png`
## 8. 运维坑(2026-09-10 实测踩到并修复)
### 8.1 pytest 会洗掉生产库(已修)
`tests/conftest.py` 原先默认 `DSH_TEST_DATABASE_URL=postgresql://dsh@127.0.0.1:15433/postgres`
—— 与 `server-worker.cmd` 的生产 DSN **完全同一库**,且每个用例调 `db.reset_schema()`
执行 `DROP SCHEMA public CASCADE`。**跑一次 pytest,生产数据全没。**
现在的行为:
- 默认测试库 = 生产实例的同名库加 `_test` 后缀(`postgres_test`),不存在则自动 `CREATE DATABASE`;
- 若 `DSH_TEST_DATABASE_URL` 指向生产库,**直接拒绝运行**(`pytest.exit`,退出码 4),不靠注释约定;
- 测试库独立,`reset_schema` 只影响它自己。
数据被洗后用 `python data/restore_state.py` 恢复(幂等):重建 bootstrap admin token、
按磁盘上的制品重算 sha256 重建 demo 插件行、清理测试残留用户。
### 8.2 重复 worker 自旋刷日志(已修)
`server-worker.cmd` 有个 `:loop` + `timeout /t 3 /nobreak` 的自动重启循环。两个问题叠在一起:
1. **`timeout` 在无控制台的分离进程里不等待** —— 实测 `timeout /t 3` 约 1.2s 就返回,
循环基本没有节流;
2. **没有单例守卫** —— 第二个 worker 启动 uvicorn 绑定失败、立即退出、马上重来,
每次迭代写两行日志,实测 **~1760 行/秒**,足以吃满磁盘。
修复:worker 启动前用 `netstat` 检查 `192.168.5.2:8020` 是否已被监听,是则 sleep 20s 空转;
用 `Start-Sleep` 替代 `timeout`;连续 5 次退出后退避 60s;启动时轮转超过 5MB 的日志
(`server.log` → `server.1.log`)。
同时 `start-server.bat` 也加了同样的前置检查并拒绝重复启动。注意两处的匹配串都必须是
**`192.168.5.2:8020`** 而不是 `:8020` —— Django 开发栈占着 `127.0.0.1:8020`,
只匹配端口号会让 dsh 误判"已在运行"而永远不启动。
### 8.3 验收要点(真机复测用)
`dsh` 固定绑 LAN IP,**本机回环访问不到**(`127.0.0.1:8020` 永远是 Django):
| 检查项 | 命令 | 期望 |
|---|---|---|
| 服务健康 | `curl http://192.168.5.2:8020/healthz` | `{"status":"ok",...}` |
| PWA 可达 | `curl -o /dev/null -w "%{http_code}" http://192.168.5.2:8020/app` | `200` |
| 连接信息 | `curl -H "Authorization: Bearer <admin-token>" .../v1/connect/info` | `base_urls` + `invite_code` |
| 二维码 | `.../v1/connect/qr?host=192.168.5.2` | 2069 字节 `image/svg+xml` |
| 日志不再暴走 | 隔 5s 对比 `logs/server.log` 大小 | 增长 ≈0 |
| 单一实例 | `netstat -ano \| findstr "192.168.5.2:8020"` | 只有 1 条 LISTENING |