9.3 KiB
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. 手机如何连上(按推荐顺序)
-
扫码连接(最省事,已实现):桌面浏览器打开
/app并用 admin 登录 → 设置 Tab → 「📱 连接手机」→ 手机系统相机对准二维码 → 自动打开本页并填好服务端地址与邀请码 → 输入用户名 → 连接。二维码内容由服务端GET /v1/connect/qr生成(深链http(s)://<地址>/app#invite=<邀请码>,segno 渲染 SVG);多网卡机器会在卡片里列出全部候选地址,点选即切换二维码。应用内扫码按钮仅在 HTTPS(或 localhost)下可用——LAN HTTP 环境浏览器禁止网页调用摄像头,系统相机是唯一稳定通道。 -
同一 Wi-Fi/局域网(当前运行模式):手机浏览器打开
http://<服务机IP>:8020/app(8000/8010 已被本机 Django 开发占用,dsh 固定用 8020)。Windows 首次启动 uvicorn 会弹防火墙授权,勾选"专用网络"允许。 -
异地团队 → Tailscale:服务机与手机都装 Tailscale,手机访问
http://<tailscale-ip>:8020/app。零公网暴露,最推荐。 -
公网 + 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 的自动重启循环。两个问题叠在一起:
timeout在无控制台的分离进程里不等待 —— 实测timeout /t 3约 1.2s 就返回, 循环基本没有节流;- 没有单例守卫 —— 第二个 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 |