samememory.life
项目/ 技术博客/ AI 工程

手机远控桌面 AI:一条永不换 URL 的隧道链

AI 工程 OpenCode SSH 反向隧道 nginx Basic Auth SSE
2026-10-15 · 1647 字 · 约 5 分钟
本文目录(7 节)
  1. 要解决什么问题
  2. 方案演进:三条弯路与终态
  3. 三层链路与三个关键设计
  4. 性能优化:8.8s → 0.34s
  5. proxy_pass 的前缀替换陷阱
  6. 踩坑精选
  7. 验证清单与结语

场景很朴素:桌面上跑着 OpenCode 桌面版(本地项目 + 本地 LLM 授权),我希望躺在沙发上用手机查看会话、下发任务、审批权限。难点在于桌面 app 的内嵌 sidecar 端口随机、密码随机且不落盘——每次重启全部轮换,手机上不可能填一个会变的地址。

💡 一句话定位: 手机 → 固定域名(nginx 边缘鉴权)→ SSH 反向隧道 → 本地 relay(实时注入 sidecar 密码)→ 桌面 sidecar。URL 永不变,app 重启自愈。

一、要解决什么问题

  • 地址稳定性:早期用 Quick Tunnel,URL 每次启动都变,手机端等于不可用
  • 凭据管理:sidecar 密码是 randomUUID(),存在进程内存里,无法写进静态配置
  • 进程一致性:opencode serve 起的独立服务和桌面 app 是两个进程,手机下发的命令桌面 UI 看不到实时回显
  • 安全性:入口暴露在公网,需要边缘鉴权,且用户凭据不应穿透到内网链路

二、方案演进:三条弯路与终态

  • 旧方案一:服务器直部署 opencode — 控制的是服务器项目,不是本地桌面,方向就不对
  • 旧方案二:Quick Tunnel — 零配置但 URL 每次变,pass
  • 方案 B:隧道直达 opencode serve:4096 — 地址稳了,但 serve 与桌面 app 双进程割裂,手机命令桌面不实时
  • 方案 A(终态):relay 挂在桌面 sidecar 前面,读它的进程内存拿密码,命令天然跑在桌面进程里

三、三层链路与三个关键设计

图 1 · 手机到桌面的五级链路(每级只解决一个问题)
图 1 · 手机到桌面的五级链路 手机 WebView固定 URL + Basic Auth nginx 边缘鉴权后剥掉凭据 SSH 反向隧道-R 14096,断线自重连 oc-relay.pyPEB 读密码注入,3s TTL 桌面 sidecar随机端口/密码 服务器只做边缘,隧道断 = 502,URL 与凭据永不改变

设计一:固定 URL 是产品需求,不是偏好。 域名和 TLS 在服务器上,隧道只是服务器 14096 端口的「按需出现」——ssh -R 由服务端自动监听,断了只是 502,手机里存的 URL 一辈子不变。

设计二:双层鉴权 + 凭据换手。 用户只记一套 admin 凭据,在 nginx 边缘校验后立即 proxy_set_header Authorization "" 剥掉;请求到达本地 relay 后,由它从 sidecar 进程的 PEB 里读出真密码(3 秒缓存)注入转发。真密码不落盘、不进配置、不出本机。

设计三:自愈。 桌面 app 重启 → sidecar 端口/密码轮换 → relay 3 秒内重新解析适配 → 隧道和 nginx 全程无感。启动脚本负责 relay + serve 兜底 + 隧道守护(断线 5 秒重连),计划任务登录自启。

四、性能优化:8.8s → 0.34s

上线后最先暴露的不是功能而是速度:切一次会话要等 8.8 秒。量化定位后发现单次 /oc/provider JSON 高达 7MB,开一次 app 三个 URI 变体裸过隧道 ≈ 21MB。

解法是在 nginx 边缘做选择性缓存——只缓存体积大、变化少的资源,动态数据(session / message / event)永不缓存:

location策略效果
/oc/providercache 120s + lock + background_update,强制 gzip 过隧道(7MB→470KB)8.8s → MISS 1.85s / HIT 0.34s
/oc/assets/7d,保持未压缩(sub_filter 需要明文 JS)HIT 0.26s
/oc/favicon1d消除重复 401

隧道流量降约 15 倍;SSE 切后台被 WebView 杀掉属于客户端行为,接受之——边缘缓存让「回到视图重拉」变快,体感问题消失。

4.1 proxy_pass 的前缀替换陷阱

缓存 location 的 proxy_pass 带 URI 后缀时按「前缀替换」工作(/oc/provider → /provider)。重构时丢掉 URI 后缀,请求打到上游 /,返回的 SPA HTML 被当响应缓存——症状是「缓存上线后返回一堆 HTML」。教训:改路径必查 proxy_pass 语义,污染后清 proxy_cache_dir。

五、踩坑精选

症状根因修复
鉴权全 500htpasswd 父目录无 x 位,www 无法穿越chgrp www + chmod 750 目录,文件 root:www 640
所有 POST 无响应relay 首包用 split 丢了请求体改用 partition 完整转发
SSE 十几秒被掐relay 对上游连接保留了超时建连后关闭 socket 超时
手机历史永不更新照片站 Service Worker(作用域 /)缓存优先吞掉了 /oc/* GETSW 放行 /oc、/server、/global,CACHE_NAME 升版清旧缓存
sw.js 自己也不更新proxy_cache 按上游 max-age 缓存 30 天对 = /sw.js 关缓存 + 剥缓存头
启动脚本静默退出PowerShell 启动含空格路径未加内层引号统一走 bat 包装启动

其中 Service Worker 那条最隐蔽:跨项目抢占作用域,access log 里完全没有请求记录——「日志里没有」本身就是「请求没发出」的证据。

六、验证清单与结语

  • 无凭据访问 → 401;带凭据 → 200 且 HTML 资源路径已改写为 /oc/...
  • curl -N 挂住 /global/event(SSE 不断流);缓存命中看 X-Cache: MISS→HIT
  • 手机发一条命令,本地 opencode 日志应出现 loop/process/stream(方案 A 下跑在桌面进程)

这套链路的本质是把「易变的东西」(sidecar 端口/密码、隧道状态)关进本机自治域,把「稳定的东西」(域名、TLS、鉴权、缓存)放在服务器边缘——手机端因此可以像用一个正经产品一样「填一次,一直用」。