Skip to content

feat(wsl): manage opencode servers inside WSL distros (Windows desktop) - #164

Open
rayn1314 wants to merge 9 commits into
lehhair:mainfrom
rayn1314:feat/wsl-server-management
Open

feat(wsl): manage opencode servers inside WSL distros (Windows desktop)#164
rayn1314 wants to merge 9 commits into
lehhair:mainfrom
rayn1314:feat/wsl-server-management

Conversation

@rayn1314

@rayn1314 rayn1314 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

起因

我在 Windows 上主力用 WSL 跑 opencode(Linux 环境确实更顺手),但 OpenCodeUI 桌面端想连上 WSL 里的服务只能手动「添加服务器」填地址——而 opencode serve 每次启动端口和密码都会变,等于每次都要去 WSL 里查一遍再手填一遍,没法日常用(也就是 #161 描述的痛点)。

官方 OpenCode 桌面端已经有完整的一套 WSL 服务器管理,所以我对着它的实现(packages/desktop/src/main/wsl 和 packages/app/src/wsl)逐项对照,给 OpenCodeUI 补上了这块能力。这个功能的目标是让 opencode 真正跑在 WSL 里、像管理本地服务一样管理它的完整生命周期,而不是从 Windows 侧拼个地址远程连过去的包装。

这是 Windows 桌面端(Tauri)专属功能,Web / Docker 部署完全不受影响(WSL 命令只在 Windows 编译分支注册)。

现在能做到什么

设置 → 服务器里:

  • 自动探测本机 WSL 运行时,没装会引导安装(UAC 提权);装了会列出本地发行版 + 在线目录(自动过滤 docker-desktop 这类不可用的,WSL1 发行版禁用)
  • 一键把发行版添加为服务器:自动检查里面有没有 bash / curl / opencode,缺 opencode 可以直接安装或升级,添加后立即启动
  • 每张 WSL 服务器卡片(连接面板里和普通服务器同款交互):状态灯(启动中 / 就绪 / 失败 / 停止)+ 失败原因、健康检查(延迟 + opencode 版本)、一键订阅进多服务器侧边栏(带 WSL 徽标)、设为默认服务器、安装 / 更新 opencode、失败后重试启动、删除(带确认,同时清掉探测记录)
  • 连接自愈:WSL 服务器的端口和密码每次启动都会变,现在启动后自动更新地址并重连事件流;启动失败自动回退本地服务器,WSL 恢复后自动切回;重启应用也不会把过期的 WSL 地址存成「死连接」
  • 「打开终端」入口直接进对应发行版

实现方式挑重点

  • 后端(Rust / Tauri):wsl.exe 进程管理 + sidecar 启动脚本(stdin 下发 bash -se,PATH 清洗 /mnt/*、WSLENV、禁用 filewatcher,对齐官方);动态端口分配并持久化复用;UUID 密码 + Basic auth;生命周期状态经 wsl-state 事件全量推送,前端订阅渲染
  • 前端(React):settings-model.ts 纯函数层负责所有「该显示什么、下一步探测什么」的判断(对齐官方同名文件,含 fuzzysort 搜索、probe 计划、失败门控),React 组件只做渲染,方便单测
  • 添加弹窗是完整状态机:运行时探测 → 发行版列表 → 逐个探测依赖 → opencode 检查,任何一步失败都能看到原因并手动重试

截图

6ffa9866-59de-4f16-a5b0-573f083becef c3d356eb-a647-4f93-a74f-9bbdd30bd89a 3d572f93-73ef-4c0a-80a1-e6b09ea1c355

后续改进(启动性能与加载体验)

功能做完真机用了一阵,发现初始启动的加载明显变慢(侧边栏会话列表、模型选择栏都要等更久),用 DevTools Network 看了一遍请求时序,定位到三组问题,挨个修掉了:

1. WSL 探测全部改为按需触发

原来的实现是应用一启动就在后台无条件跑一轮 WSL 探测:runtime 探测、发行版列表、联网拉微软在线目录、逐个发行版查 opencode——哪怕你从没添加过 WSL 服务器也照跑,等于为用不到的功能在启动路径上付成本。现在启动只做「恢复上次状态」(读配置 + 拉起已添加的服务器),所有探测改到打开设置页时按需触发;联网的在线目录加了 24 小时缓存,先展示后刷新,点「重新检测」才强制联网。

2. 启动级联:全量拆建改增量接入

WSL 服务器就绪注册时,原实现会把所有服务器的 SSE 连接全拆全建、所有会话数据全量重拉一遍,同一份数据在启动窗口期要加载 2~3 次;另外未注册的 WSL 服务器 id 会静默回退到本地端点,把本地数据写到 WSL 的键下(数据串服)。现在未注册服务器不订阅(注册即自动接入),订阅集合按服务器增量 diff——加入只连新的,移出只拆旧的;wsl-state 全量推送的重复同步也加了短路,无变更零副作用。

3. 会话列表的加载态与错误态

网络抖动时每个文件夹的重试间隙会闪现「此文件夹中没有对话」,重试耗尽后失败还会被永久伪装成空数据。现在重试期间保持加载态,「此文件夹中没有对话」只在真的确认没有会话时出现,加载失败如实显示「加载失败,点击重试」。

提交前对这批改动做了一轮独立审查,顺手修了几个边界问题:pane 里还开着已失效服务器时 SSE 集合可能被清空(现在兜底回退 active server)、离线机器上后台刷新会反复拉起挂住的联网进程(加了在途去重)等。

踩过的坑(挑几个印象深的)

  1. Rust 和前端的字段名各说各话:serde 默认 snake_case,TS 类型写的 camelCase,前端读到的全是 undefined,UI 稳定显示「请先打开一次发行版」。现在所有跨语言结构体统一 #[serde(rename_all = "camelCase")],命名有机械保证。
  2. wsl --list --online 要联网:网络慢时超时会把已经成功的本地列表一起丢掉,还伪装成「没有发行版」。现在本地列表和在线目录解耦容错,谁失败只报谁。
  3. 重写进程封装时漏了 Stdio::piped():子进程输出全拿空串,探测「成功」但数据全空,上层误判成没数据就无限自动刷新,整个弹窗卡死。已补回归测试防再犯。
  4. Windows 分支曾另起一次 invoke_handler 注册 WSL 命令——Tauri 的 invoke_handler 是整体替换语义,第二次调用会把第一次注册的全部命令覆盖掉,安装包表现为「进程在但窗口永远不出来」。现在 WSL 命令合并进同一个 generate_handler! 列表。
  5. wsl.exe 的管道输出是 UTF-16LE(可能带 BOM)和 UTF-8 混杂,不解码就是乱码。

验证

  • typecheck 0 错误;ESLint 0 错误
  • 测试 644/644 通过(94 个文件)
  • vite 生产构建通过;cargo check / cargo test(16 个后端测试)通过
  • 实际使用体验:切换到 WSL 服务器后,会话里跑的工具(执行命令、读写文件)都是 WSL 环境内部的——路径、shell、权限都是 Linux 侧的,不是本地中转转发式的实现
  • Tauri release 构建出安装包,真机(Win11 + WSL2 Ubuntu)完整验收:添加 → 自动装 opencode → 启动 → 订阅切换 → 设默认 → 健康检查 → 重试启动 → 删除,全流程可用
  • 基于 0.6.45(e3605b57),与当前 main 仅差一个无交集的依赖 bump(fast-uri),合并无冲突

提交组织

为方便审计拆成九个提交:

  1. chore: exclude local opencode reference dir from git, eslint and vitest —— 排除我做官方源码对照用的本地目录
  2. feat(wsl): backend server management commands for Windows desktop —— src-tauri 全部(7 文件)
  3. feat(wsl): frontend server sync, settings UI and connections integration —— src 全部 + package.json(21 文件)
  4. fix(wsl): sidebar group and SSE follow WSL server registered after boot —— WSL 服务器启动后就绪后自动注册进侧边栏并接通事件流
  5. perf(wsl): 按需预热——启动零探测、设置页意图驱动、在线目录 TTL 缓存 —— 启动路径只恢复状态,探测按需
  6. perf(sessions): 修复启动级联——未注册服务器过滤、upsert 短路、订阅集合增量管理 —— 消灭启动窗口期的重复加载与数据串服
  7. fix(sidebar): 会话列表加载态与错误态——重试间隙不再闪现空态文案
  8. fix(wsl): 评审修正——在线目录 revalidate 去重、prewarm 跳过就绪服务器、测试门控简化
  9. fix(sessions): 评审修正——过滤后兜底 active server、enabled 关闭中断重试、去除空 catch

前后端拆开但没拆更细的原因:wslStore 和 serverStore 互相引用对方本次新增的方法,行级拆分会产生无法通过 typecheck 的中间提交;性能与体验修复(5-9)按主题拆分,每个提交都能独立通过校验。

Closes #161

关于 AI 辅助

这次的排查和实现过程使用了 AI 辅助(对照官方源码做差距清单、定位跨���言序列化问题等),但上面每一个功能点都是我在真机上逐一实测验收过的。

- WSL runtime probe, distro enumeration (local + online), per-distro
  probes and opencode version checks with UAC-elevated installers
- server lifecycle jobs (start/install/remove) with generation-guarded
  state, UTF-16 console decoding, full state push via wsl-state event
- deterministic server ids (wsl:<distro>), startup warm-up
- opencode command surface extended for WSL sidecar lifecycle
- wslStore syncs runtime into serverStore: ready servers register
  with auth and auto-subscribe to the multi-server sidebar (WSL badge)
- connections list renders one card per WSL server combining
  connection and lifecycle management (switch, health, default,
  install/update opencode, retry, remove); add dialog with detection
  progress state and terminal entry
- default-server preference persisted (boot auto-switch, death-
  rebirth restore after sidecar restart); WSL ids excluded from
  localStorage persistence; SSE resubscribes on same-id runtime change
- upsertServer broadcasts server-runtime-updated on any endpoint change
  (first registration included), decoupled from the active server
- SidePanel subscribed filter tracks the serverStore snapshot so the WSL
  group appears the moment the server registers, no manual re-subscribe
- per-server SSE rebuilds the single affected connection on runtime change:
  the boot-window fallback connection (WSL not yet registered) stays healthy
  on the wrong address and never self-heals via auto-reconnect
@lehhair

lehhair commented Aug 30, 2026

Copy link
Copy Markdown
Owner

感谢pr,最近工作太忙,更新比较慢,抽空我会看看的

- initialize_wsl 瘦身为仅恢复配置+拉起已有服务器,删启动全量 opencode 检查与预热块
- 新增 prewarm_wsl 命令,设置页打开时按需补齐 runtime/发行版列表/opencode 检查
- 在线目录 stale-while-revalidate 缓存(wsl-online-cache.json, TTL 24h):联网目录持久化,过期先展示后后台刷新
- 对话框「重新检测」按钮走 force=true 绕过缓存,autoProbePlan 走缓存
- 未添加过 WSL 服务器的机器启动路径零开销
- collectActiveServerIds 过滤未注册服务器,消灭 WSL 就绪前请求回退 local 的数据串服
- 注册事件订阅 serverStore,WSL 就绪时自动入集触发接入
- upsertServer 无变更短路,wsl-state 全量推送不再触发 localStorage 写入与全体重渲染
- 订阅集合改增量 diff(Map 按 serverId),集合变化不再全量拆建 SSE 与全量重拉
- useModels 按 reason 门控,非 active 服务器端点变化不再触发模型重拉
- 补增量接入与定向重建订阅的契约测试
- useSessions 重试改为显式循环,loading 生命周期=循环生命周期,重试等待期不落地
- 重试耗尽才落 error 终态,空态文案不再伪装成「没有对话」的加载失败
- FolderRecentList 空态前增加 error 分支,加载失败显示可点击重试
- 新增 locale 键 sidebar.loadFailed
- 补重试期保持 loading 与耗尽落 error 的契约测试
- 后台 revalidate 加在途标志,避免每次 ServeStale 并发拉起会挂住的 wsl --list --online
- prewarm 只补未就绪发行版的 opencode 检查,就绪服务器由 Ready 挂钩刷新,不再重复 spawn
- 单元测试去掉冗余 target_os 门控(模块本身已按 Windows 编译)
- collectActiveServerIds 过滤后为空时兜底回退 active server,避免 SSE 全灭(补回归测试)
- useSessions 重试等待期检查 enabledRef,懒加载闸门关闭时中断在途重试
- 重试终态改为 return 而非 throw 到空 catch,消除空 catch 块
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants