2026-09-23 22:35:06 +08:00

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 / …)已合并删除,功能全部并入本菜单

等价命令行(免菜单直传,写脚本时用):

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 互通

# 列出 WSL OC 的 skill(直读 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

# 看两端目录情况(路径 + 存在性 + skill 数量 + 名字摘要;不探测 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\<name>\, OC 会 watch 这个目录(约 100ms 去抖后自动 rescan 并推 skill.reload),不用重启。

常用参数:

参数 默认 说明
--distro Ubuntu-24.04 WSL 发行版名

2026-09-24 起 ocskill.py 不再探测 OC API(--url/--port/--timeout/--password 已移除):读 skill 一律直读目录,连上 WSL 就查目录,本机的就查本机 OC 目录。 mcpctl.py(配置对调 / 热重载)仍走 API,其 --port/--password 等参数不受影响。

2. mcpctl.py — MCP 配置对调开关

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 一样生效。

前置探活:没有运行中的 OC 就不重载、也不算失败(2026-09-25 用户反馈): 对调完成后先 _live_oc() —— WSL 目标先查 VM 是否在跑(没跑直接跳过、不发网络请求), 再端口发现 + 探活找运行中的 OC。都没有 → 打印「无需热重载:… 配置已对调完成, 等 OC 下次启动时会自动加载新配置」并按成功返回,不再报 「热重载失败:… WinError 10061 连接拒绝」(WSL/OC 都没启动时冷启动会自己加载)。 探活命中的 host 直接复用给推送兜底与 reload(不重复探测);只有 「OC 在跑但 reload 返回 401/404/错误」才照旧算真实失败。 措辞按状态说(2026-09-25 Round 9):连接被拒绝(WinError 10061)报 「无进程监听(OC 未启动)」、超时报「连接超时(OC 未启动或不可达)」, skip 整句不再出现「连不上(urlopen error…)」被读成 API 报错; 未知网络错误仍保留原文不吞。

开启方向多了「推送兜底」(双保险):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 永不返回
UNC 半死时 mcp 查看 / skill dirs 无限挂起(Path.exists()/read_text 阻塞在系统调用层,Ctrl+C 都打不断) 只读链路统一走 _guard():fn 放进 daemon 工作线程,主线程 queue.get(timeout) 限时等(本机 8s / WSL UNC 20s)。超时打印「读取超时(>Ns)」并返回菜单,绝不再挂死;主线程是 Python 层等待,Ctrl+C 随时可打断。覆盖 status / mode 查看 / backup / on·off 初始读 / dirs 两端目录探查
唤醒 WSL 的十几秒里屏幕无输出,看着像卡死 唤醒统一包 _run_with_beats():每 5 秒打一个 .,配合「正在唤醒(可能需要十几秒,期间每 5 秒打一个点)」提示,等待过程可见
菜单被子命令 raise SystemExit 杀死 oc.py 的 run_safe() 护栏:KeyboardInterrupt → 已取消回菜单;SystemExit → 报告信息回菜单;异常 → 打印错误回菜单,菜单本体永不退出

list / dirs 连接 WSL 前会先打印「正在唤醒以便读配置…」(VM 没跑时,每 5 秒 打一个点),几秒无响应时一眼看出是在唤醒而不是死了;list 唤醒失败绝不回落到 UNC 目录扫描(那会触发冷启动卡死),直接报错返回。 dirs 只展示两端目录信息(路径 + 存在性 + skill 数量 + 名字摘要,限时读取防卡死), 不再探测 OC API(2026-09-24 用户反馈:读 skill 目录不需要 API,是画蛇添足)。

缺 SKILL.md 的软链接诊断(2026-09-24 新增):oc / wb 里的 skill 有些是软链接 形式(不是实体拷贝),目录列得出来却找不到 SKILL.md。现在:

  • list 表格与菜单翻页里顶层缺 SKILL.md 的条目直接标 ⚠缺 SKILL.md (与推送同名检查同一数据源 find_skill_md,选中前就能看出空壳 / 断链);
  • 编号选中(二次确认)时打印软链接诊断:目录/SKILL.md/子项是不是 symlink 或 junction、目标指向哪、解出的真实位置有没有 SKILL.md; UNC 下 POSIX 目标换算回 \\wsl.localhost\<distro> 视图,本地解不出还会进 WSL 用 readlink / find -L 再查一层;
    • WSL 内脚本走 base64 传输(wsl.exe 会把原始 commandline 交给 distro shell 二次解析,script 双引号里的 $var 会被外层提前展开 —— 实测 $1/$p 全变空、 rc=0 却零输出);
    • WSL 未运行时不自动冷启动,明确提示「跳过二层诊断,唤醒 WSL 后重试」;
    • 目标读不出来时不再用 realpath 硬解(那只会解出链接自身、误报断链), 直接标注「疑似 WSL 内软链接,交由二层诊断」;
  • 推送失败(缺 SKILL.md)的报错里同样附带这份诊断,回答「真实文件在哪」。
  • 跨根兜底:空壳目录直接找出「真身在别处」(2026-09-25 用户反馈)—— 实测来源目录(.config/opencode/skills/<name>,两端)是真实空目录、不是链接, 但大模型照样能调用:真身在另一个根 ~/.workbuddy/skills/<name>/SKILL.md (即 wb 终端)。结论行不再止步「仍未找到」,而是跨已知根找同名 SKILL.md: 本机 ~/.workbuddy/skills、~/.config/opencode/skills(含 .trash 回收站)、 当前目录 .workbuddy/skills、WSL 端对应两根(folder 是 WSL UNC 直查, 否则 VM 在跑才查),并可用环境变量 OC_SKILL_ALT_ROOTS(os.pathsep 分隔) 追加自定义根;找到就列出真身路径(2026-09-25 Round 9 起菜单推送自动切到真身, 不再要求手动换来源,见下条)。 另外 WSL 二层查完空结果也会说话(「ℹ WSL 内二层已查:没有 SKILL.md、也没有链接」), 不再静默——静默会被误读成二层没跑。
  • 推送自动切真身:源是软链接 / 空壳直接换路径,不提醒手动切(2026-09-25 Round 9)—— ocskill.real_src() 两级解析:链接(symlink / junction)先解链接目标(目标里有 SKILL.md 才切),空壳再走上面的跨根真身列表;oc.py 选完源就地切换并打印 「原 / 新」两行,随后按真身路径 probe + push。有效目录 / zip / SKILL.md 文件输入 不动(返回 None)。实测:.config/opencode/skills/memory-manager-v2__skillhub 是指向 .cc-switch/skills/... 的 junction,选它推送时自动推 cc-switch 真身。 解不出真身时才落回原诊断(「自动切换也没找到真身,无法推送」)。

skill.json 注册表:推拉遇空壳/链接的分层扫盘落库(2026-09-25 Round 10)—— 不一上来就全盘忙扫,按三层走,且推拉永远优先用户选定的目录(选定目录有效就直接用, 注册表只在空壳/链接时兜底解析真身):

层 时机 做什么
① 内存注册表 启动即读 oc.py / ocskill.py 启动时把 skill.json 读进内存(load_registry()),后续查询零扫盘
② 内嵌常见位置快扫 空壳/链接、内存没命中 builtin_skill_roots():oc(.config/opencode/skills)、wb(.workbuddy/skills)、cc-switch、claude code + agents.md 终端表 + OC_SKILL_ALT_ROOTS + WSL 两根(VM 在跑才带,绝不为快扫冷启动),秒级、命中即落库
③ 后台扫盘 上面都没中 start_background_scan():daemon 线程扫内嵌根 + 扩展浅层根(home / .config / .local/share / cwd 下一级的 skills/,不递归不全盘),不阻塞当前推拉;单飞(运行中不叠加线程);UNC 根 6s 限时防半死;扫完落库

落库结构就是「名字 -> 位置」的一堆键值(skills.<name> = [路径…],只增不删, 查询端做存在性 + SKILL.md 校验,过期条目只是查不到、不误导):

{ "version": 1, "updated": "2026-09-25 21:10:00",
  "skills": { "java-expert": ["C:\\Users\\…\\.workbuddy\\skills\\java-expert", "…"] } }
  • 文件位置:本目录 skill.json(本机运行数据,已 gitignore,不入库);
  • 推送(菜单 _flow_push + CLI cmd_push)、拉取(cmd_pull)两端对称接入: 源是空壳/链接就走上面三层自动切真身并打印「原 / 新」,没找到真身则启动后台扫盘 并提示「稍后重试即可自动切」;zip / SKILL.md / base64 输入不参与切换;
  • 扫描结果落库后,下次同名直接走 ① 内存命中(实测首次 0.49s、重启后 0.02s)。

回归单测:python _test_diag.py(空壳 / 正常 skill / junction 链接 / 断链 / 大小写 / WSL UNC 真链接 / 跨根兜底 / real_src 自动切换等 20+ 项断言,2 个文件 symlink 场景在拦截 symlink 的 机器上自动 SKIP;含 WSL 场景,首次跑会拉起 VM); python _test_mcpctl.py(热重载前置探活 + 探活措辞 10 项:未运行 skip 不探测、 失败仍报错、host 复用、10061/10060 措辞、skip 整句不含「连不上」等); python _test_registry.py(skill.json 注册表 34 项:内存加载 / 落库原子写 / 损坏文件 不炸 / 内嵌 oc·wb·cc-switch·claude 根 / 快扫命中即落库 / 分层查找顺序 / 后台扫盘单飞 与完成落库 / 点前缀与无 SKILL.md 不登记 / 用户选定目录优先 / 扩展根浅层扫描)。


三、连不上 OC 怎么办

本节只影响 mcpctl.py(配置对调 / 热重载走 API)。 ocskill.py 的 list / pull / push / delete 已全部直读 skill 目录,不连 OC API, OC 没在运行也能列 / 推 / 拉(写完 OC 起来后目录 watch 会自动热加载)。

先看这一条(最常见):有 HTTP 代理时会假报 502。

mcpctl 默认绕过系统代理直连(ProxyHandler({}))。 如果环境里有 HTTP_PROXY / HTTPS_PROXY,urllib 默认会把 localhost 请求也丢给代理, 结果是代理回 502,看起来像 OC 挂了、其实是被劫走了。 确实需要走代理时,设环境变量:MCPCTL_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. 全都失败 → skill 读写不受影响: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

S
Description
No description provided
Readme
16 MiB
Languages
JavaScript 57.9%
Python 35.9%
TypeScript 6.2%