场景很朴素:桌面上跑着 OpenCode 桌面版(本地项目 + 本地 LLM 授权),我希望躺在沙发上用手机查看会话、下发任务、审批权限。难点在于桌面 app 的内嵌 sidecar 端口随机、密码随机且不落盘——每次重启全部轮换,手机上不可能填一个会变的地址。
一、要解决什么问题
- 地址稳定性:早期用 Quick Tunnel,URL 每次启动都变,手机端等于不可用
- 凭据管理:sidecar 密码是
randomUUID(),存在进程内存里,无法写进静态配置 - 进程一致性:
opencode serve起的独立服务和桌面 app 是两个进程,手机下发的命令桌面 UI 看不到实时回显 - 安全性:入口暴露在公网,需要边缘鉴权,且用户凭据不应穿透到内网链路
二、方案演进:三条弯路与终态
- 旧方案一:服务器直部署 opencode — 控制的是服务器项目,不是本地桌面,方向就不对
- 旧方案二:Quick Tunnel — 零配置但 URL 每次变,pass
- 方案 B:隧道直达
opencode serve:4096— 地址稳了,但 serve 与桌面 app 双进程割裂,手机命令桌面不实时 - 方案 A(终态):relay 挂在桌面 sidecar 前面,读它的进程内存拿密码,命令天然跑在桌面进程里
三、三层链路与三个关键设计
设计一:固定 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/provider | cache 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/favicon | 1d | 消除重复 401 |
隧道流量降约 15 倍;SSE 切后台被 WebView 杀掉属于客户端行为,接受之——边缘缓存让「回到视图重拉」变快,体感问题消失。
4.1 proxy_pass 的前缀替换陷阱
缓存 location 的 proxy_pass 带 URI 后缀时按「前缀替换」工作(/oc/provider → /provider)。重构时丢掉 URI 后缀,请求打到上游 /,返回的 SPA HTML 被当响应缓存——症状是「缓存上线后返回一堆 HTML」。教训:改路径必查 proxy_pass 语义,污染后清 proxy_cache_dir。
五、踩坑精选
| 症状 | 根因 | 修复 |
|---|---|---|
| 鉴权全 500 | htpasswd 父目录无 x 位,www 无法穿越 | chgrp www + chmod 750 目录,文件 root:www 640 |
| 所有 POST 无响应 | relay 首包用 split 丢了请求体 | 改用 partition 完整转发 |
| SSE 十几秒被掐 | relay 对上游连接保留了超时 | 建连后关闭 socket 超时 |
| 手机历史永不更新 | 照片站 Service Worker(作用域 /)缓存优先吞掉了 /oc/* GET | SW 放行 /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、鉴权、缓存)放在服务器边缘——手机端因此可以像用一个正经产品一样「填一次,一直用」。