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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Windows 桌面端支持 WSL 连接

2 participants