# wsl-round-mcp — 本机 ⇄ WSL OpenCode 互通工具 打通**本机 OpenCode** 与 **WSL 里的 OpenCode**:skill 列表互查、skill 一键推送、MCP 配置对调开关。 全部零第三方依赖,只要装了 Python 3 就能跑。 --- ## 一、双击就能用(小白版) 把整个 `wsl-round-mcp` 文件夹拷到任意位置,双击 **`oc.bat`** —— 只有这一个入口, 启动打印数字菜单,**输编号或中文都能进**: ``` ============================================ OC 工具箱(输入编号或中文,0 退出) ============================================ 0. 退出 1. 技能管理(skill 拉取 / 推送) 2. 连接器管理(MCP 开关 / 模式) 请选择: ``` - `1` / `技能管理` → 列表(1)、推送(2)、拉取(3)、目录(4);每层 `0` 返回 - `2` / `连接器管理` → 状态(1)、开启(2)、关闭(3)、模式(4)、备份(5)、`t` 切换本机/WSL - **任意位置输入 `/home`(或「首页 / 返回首页 / 回首页 / 回主页 / 主菜单 / 主页」)直接跳回顶层菜单**—— 菜单位、参数位(路径/技能名/新模式 的输入框)、回车前都算,不用一层层按 `0`; 在顶层菜单输 `/home` 会提示「已经在首页了」 - 菜单内所有操作会逐项问参数(路径、是否覆盖…),回车即用默认值 - 旧的 6 个分散 bat(skill-list / skill-push / …)已合并删除,功能全部并入本菜单 等价命令行(免菜单直传,写脚本时用): ```bash python oc.py skill list # = 旧 skill-list.bat python oc.py mcp status # = 旧 mcp-status.bat python oc.py list # 自动识别归属 ``` > bat 内部会自动 `chcp 65001`,中文输出不会乱码。 > 没装 Python 或没加进 PATH,会提示 `Python not found in PATH`。 --- ## 二、命令行用法 ### 1. `ocskill.py` — skill 互通 ```bash # 列出 WSL OC 的 skill(API 连不上会自动退回目录扫描) python ocskill.py list # 推送:压缩包(自动解压) python ocskill.py push .\my-skill.zip # 推送:整个 skill 目录 python ocskill.py push .\my-skill # 推送:单个 SKILL.md(会自动包成以 name 命名的目录) python ocskill.py push .\SKILL.md # 推送:base64 内容 python ocskill.py push <一长串base64> --base64 # 只校验格式、不写入 python ocskill.py push .\my-skill --dry-run # 覆盖同名 skill(先自动备份成 .bak-时间戳) python ocskill.py push .\my-skill --force # 反向:把 WSL 里的 skill 拷回本机 python ocskill.py pull java-expert --out .\skills-backup # 看两端目录与 API 连接状态 python ocskill.py dirs ``` **push 的完整流程**(对应「先解码 → 确认标准 skill 格式 → 传入 skill 目录」): 1. **解码归一**:zip 就解压(带 zip-slip 目录穿越防护),目录原样用, 单个 `.md` 会包成目录,base64 字符串先解成文件再判断是 zip 还是 md。 2. **定位 `SKILL.md`**:允许压缩包里多套一层父目录(最深 3 层)。 3. **校验标准 skill 格式**,四项全过才算通过: - 找得到 `SKILL.md` - 文件以 `---` 开头,有 YAML frontmatter - frontmatter 里有 `name`,且只含 `字母/数字/._-` - frontmatter 里有 `description`(大模型靠它决定何时调用) 4. **写入** `\\wsl.localhost\<发行版>\root\.config\opencode\skills\\`, OC 会 watch 这个目录(约 100ms 去抖后自动 rescan 并推 `skill.reload`),**不用重启**。 常用参数: | 参数 | 默认 | 说明 | | --- | --- | --- | | `--distro` | `Ubuntu-24.04` | WSL 发行版名 | | `--url` | 自动探测 | 显式指定 OC 地址,如 `http://localhost:4096` | | `--port` | `4096` | OC 端口(等价于环境变量 `OC_PORT`) | | `--password` | 自动读 | OC 口令,读取链 `service.json` → 环境变量 `OPENCODE_PASSWORD`/`OPENCODE_SERVER_PASSWORD` → `opencode.json server.password`;鉴权统一 `Basic base64("opencode:<口令>")`(用户名固定 `opencode`)。端口 4096 没监听时会 `tasklist`+`netstat` 自动发现 OC 实际随机端口并探活 | ### 2. `mcpctl.py` — MCP 配置对调开关 ```bash python mcpctl.py status # 看状态(只读) python mcpctl.py on # 对调成含 MCP + 热重载 python mcpctl.py off # 对调成安静版 + 热重载 python mcpctl.py mode # 看当前模式 python mcpctl.py mode 手动 # 改模式(自动/手动/关闭,或 auto/manual/off) python mcpctl.py status --target wsl # 操作 WSL 那一份配置 python mcpctl.py off --no-reload # 只对调文件,不调用重载 ``` 它读的正是 `mcpcfg.txt`(键值格式,小白直接改值即可): ``` # MCP 开关配置:改完保存,重启 OC 生效 模式: 自动 ← 自动 / 手动 / 关闭 含MCP的配置: C:\...\opencode.json 安静配置: C:\...\opencode.json.2 ``` 兼容旧「裸三行」格式(第1行模式、第2行含MCP、第3行安静), 插件/CLI 读到旧格式会**自动迁移写回**成上面的键值格式; 两行路径指向的文件不存在时会**自动扫同目录 `opencode.json.*` 自愈**并写回, 避免路径写错把 `/mcpon` `/mcpoff` 锁死。 **状态判定只看活文件**(文件名为 `opencode.json` 的那份)里 `mcp.servers` 是否非空, 所以对调之后两行的身份怎么漂移都不影响判断——这与 TUI 插件、server 插件完全一致。 另外 `on`/`off` 成功后会维护 `mcp.on.flag`(开写、关切): 自动模式的「启动归位」见到标记就跳过,不会把刚开启的状态又换回去。 模式语义(三个入口共用同一套规则): | 模式 | `/mcpon` `/mcpoff` | 大模型(server 插件) | 本工具 `mcpctl.py` | | --- | --- | --- | --- | | 自动 | 可用(视为手动切换一次) | 可用 | 可用 | | 手动 | **仅此可用** | 拒绝 | 可用(等价于用户输入命令) | | 关闭 | 不注册 | 拒绝 | 拒绝(可用 `--ignore-mode` 强行) | 热重载打的是 `POST /api/location/reload`,会重建 location 并重读磁盘配置,MCP 立即生效; 失败时配置文件已经对调完成,重启 OC 一样生效。 **开启方向多了「推送兜底」(双保险)**:`on` 在对调+写 flag 之后、reload 之前, 先把活配置里的 `mcp` 推进**运行中的 OC**。三端同一套语义——**GET+merge 补 shell → PATCH 真实路由**,按各自运行方式实现: - **TUI 插件(v10,进程内 SDK)**:`client.config.get()` 取 shell → `config.update({config})` → `({body})` → `experimental.config.update` 三跳; - **server 插件(v3,raw HTTP)**:`GET /api/config?directory=` 取 shell → `PATCH /api/experimental/config?directory=`,口令读 `service.json` 链、 `Basic base64("opencode:<口令>")`; - **本 CLI(mcpctl)**:`tasklist`+`netstat` **端口发现**(OC 每次启动随机端口, 写死 4096 常 10061)→ `GET /api/config` 探活并取 `info.shell`(GET→活文件→null 三级兜底)→ `PATCH /api/experimental/config?directory=`,body 必含 `shell` (缺则 400 `Missing key at ["shell"]`;裸 `PATCH /config` 是 405)。 推送成功再 reload = 双保险,MCP 百分百加载;推送失败不阻断,自动降级为原 reload 单通道。 ### 稳定性设计(WSL 相关) | 问题 | 修复 | | --- | --- | | WSL 路径假错「两键路径不存在 / mcpcfg.txt 不存在」 | mcpcfg 内部是 POSIX 路径(`/root/...`),Windows 上曾被解析成 `C:\root`。现在统一过 `fs_view()` 翻译成 `\\wsl.localhost\<发行版>\...` 再访问;报错与展示仍用**原文**(你在 txt 里看到的样子) | | 参数顺序被 argparse 拒(`mcpctl status --target wsl` 报 unrecognized) | `_hoist_shared()` 在解析前把共享 flag(`--target/--distro/--port/--timeout/--password/--no-reload/--ignore-mode`)提到最前,**子命令前后都能写** | | WSL 没跑时读配置卡死/假错 | `wsl_gate` 统一门控:**读写都先唤醒**——「先给机器启动起来,再去读配置」:status / mode 查看 / backup / `list` / `dirs` 等读操作,VM 没跑同样打印「正在唤醒」拉起 VM(超时 45s)再读,绝不秒回假错;唤醒失败给清晰指引 | | `wsl.exe` 超时后进程永久卡死(必须 ctrl+C) | 所有 `wsl.exe` 调用改走 `_wsl_safe_run`:`stdin=DEVNULL` + stdout/stderr 落**临时文件**(零管道)+ 先查 `-l -q --running` 绝不无提示冷启动。根因是管道句柄被 wslrelay 等后代继承,client 被 kill 后 `communicate()` 等 EOF 永不返回 | | 菜单被子命令 `raise SystemExit` 杀死 | `oc.py` 的 `run_safe()` 护栏:`KeyboardInterrupt` → 已取消回菜单;`SystemExit` → 报告信息回菜单;异常 → 打印错误回菜单,菜单本体永不退出 | `list` / `dirs` 连接 OC 前会先打印「正在唤醒以便读配置…」(VM 没跑时)和 「正在探测 OpenCode API 连接…」进度行(flush), 几秒无响应时一眼看出是在唤醒/探测而不是死了;`list` 唤醒失败**绝不**回落到 UNC 目录扫描(那会触发冷启动卡死),只走 API 或明确报错。 --- ## 三、连不上 OC 怎么办 **先看这一条(最常见):有 HTTP 代理时会假报 502。** 两个脚本已默认**绕过系统代理**直连(`ProxyHandler({})`)。 如果环境里有 `HTTP_PROXY` / `HTTPS_PROXY`,`urllib` 默认会把 `localhost` 请求也丢给代理, 结果是**代理回 502**,看起来像 OC 挂了、其实是被劫走了。 确实需要走代理时,设环境变量再运行:`MCPCTL_USE_PROXY=1` / `OCSKILL_USE_PROXY=1`。 其余排查: 1. 确认 WSL 里的 OC 起了:`opencode serve --port 4096` 2. **端口不对不用管**:OC 每次启动端口随机(实测如 49374),工具会 `tasklist` 找 `opencode.exe` 的 PID、`netstat -ano` 找它 LISTENING 的端口, 再逐候选 `GET /api/config` 探活(200 = 命中);仍要显式指定就加 `--port` 或环境变量 `OC_PORT` 3. 提示 `401 WWW-Authenticate: Basic realm="Secure Area"` → OC 启用了服务口令。 用 `--password` 传值(默认按 `service.json` → 环境变量 `OPENCODE_PASSWORD`/`OPENCODE_SERVER_PASSWORD` → `opencode.json server.password` 的顺序读,用户名固定 `opencode`、Basic 鉴权)。注意临时 `opencode serve --port X` 实例用**每次启动随机口令且不落盘**,`service.json` 读到的口令对它必 401——要么用 TUI 启动的 OC(走 `service.json`),要么 `--password` 传启动时设的口令 4. `localhost` 不通 → 工具会自动 `wsl hostname -I` 取 WSL 真实 IP 再试 5. 全都失败 → `list` 会自动降级为**直接扫目录**,照样能看到 skill 列表 --- ## 四、和另外两个仓库的关系 本目录是独立 git 仓库,**分支名 = 文件夹名** `wsl-round-mcp`,远程与主仓库共用同一个根: | 文件夹 | 分支 | 内容 | | --- | --- | --- | | `oc_session/`(主仓库) | `main` | TUI 插件 `oc-session-all`(`/sessions-all`、`/mcpon`、`/mcpoff`) | | `oc_session/ai-mcp/` | `ai-mcp` | server 插件 `ai-mcp.ts`,给大模型用的 `mcp_switch` 工具 | | `oc_session/wsl-round-mcp/` | `wsl-round-mcp` | 本目录:skill 互通 CLI + MCP 命令行开关 | 远程:`https://git.ypshidifu.cn/youpaishidifu/oc_session.git`