137 lines
9.3 KiB
Markdown
137 lines
9.3 KiB
Markdown
# 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 |
|