Files
dsh/MOBILE_APP.md

9.3 KiB
Raw Permalink Blame History

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. 手机如何连上(按推荐顺序)

  1. 扫码连接(最省事,已实现):桌面浏览器打开 /app 并用 admin 登录 → 设置 Tab → 「📱 连接手机」→ 手机系统相机对准二维码 → 自动打开本页并填好服务端地址与邀请码 → 输入用户名 → 连接。二维码内容由服务端 GET /v1/connect/qr 生成(深链 http(s)://<地址>/app#invite=<邀请码>,segno 渲染 SVG);多网卡机器会在卡片里列出全部候选地址,点选即切换二维码。应用内扫码按钮仅在 HTTPS(或 localhost)下可用——LAN HTTP 环境浏览器禁止网页调用摄像头,系统相机是唯一稳定通道。

  2. 同一 Wi-Fi/局域网(当前运行模式):手机浏览器打开 http://<服务机IP>:8020/app(8000/8010 已被本机 Django 开发占用,dsh 固定用 8020)。Windows 首次启动 uvicorn 会弹防火墙授权,勾选"专用网络"允许。

  3. 异地团队 → Tailscale:服务机与手机都装 Tailscale,手机访问 http://<tailscale-ip>:8020/app。零公网暴露,最推荐。

  4. 公网 + 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