CLI合并为oc.py/oc.bat菜单入口(数字+中文),mcpctl支持键值mcpcfg与on/off标记;删除6个分散bat

This commit is contained in:
ypshidifu
2026-09-23 13:47:10 +08:00
parent 35513f1cdf
commit d56cc01058
10 changed files with 417 additions and 148 deletions
+39 -16
View File
@@ -7,16 +7,31 @@
## 一、双击就能用(小白版)
把整个 `wsl-round-mcp` 文件夹拷到任意位置,双击对应的 bat:
把整个 `wsl-round-mcp` 文件夹拷到任意位置,双击 **`oc.bat`** —— 只有这一个入口,
启动打印数字菜单,**输编号或中文都能进**:
| 文件 | 作用 |
| --- | --- |
| `skill-list.bat` | 连接 WSL 的 OC,列出它当前有哪些 skill |
| `skill-push.bat` | 会让你粘贴一个 skill 路径(zip / 目录 / SKILL.md / base64),校验格式后写进 WSL 的 skill 目录 |
| `skill-pull.bat` | 会让你输入 skill 名,把它从 WSL 拷回本机 `skills-backup\` |
| `mcp-status.bat` | 查看 MCP 当前是开是关、第1行是什么模式 |
| `mcp-on.bat` | 把配置对调为「含 MCP」并热重载 |
| `mcp-off.bat` | 把配置对调为「安静版(无 MCP)」并热重载 |
```
============================================
OC 工具箱(输入编号或中文,0 退出)
============================================
0. 退出
1. 技能管理(skill 拉取 / 推送)
2. 连接器管理(MCP 开关 / 模式)
请选择:
```
- `1` / `技能管理` → 列表(1)、推送(2)、拉取(3)、目录(4);每层 `0` 返回
- `2` / `连接器管理` → 状态(1)、开启(2)、关闭(3)、模式(4)、备份(5)、`t` 切换本机/WSL
- 菜单内所有操作会逐项问参数(路径、是否覆盖…),回车即用默认值
- 旧的 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`。
@@ -84,22 +99,30 @@ python ocskill.py dirs
python mcpctl.py status # 看状态(只读)
python mcpctl.py on # 对调成含 MCP + 热重载
python mcpctl.py off # 对调成安静版 + 热重载
python mcpctl.py mode # 看第1行
python mcpctl.py mode 手动 # 改第1行(自动/手动/关闭,或 auto/manual/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` 那三行:
它读的正是 `mcpcfg.txt`(键值格式,小白直接改值即可):
```
自动 ← 第1行:模式
C:\...\opencode.json.1 ← 第2行:含 MCP 的原始配置
C:\...\opencode.json ← 第3行:删了 MCP 的安静配置
# MCP 开关配置:改完保存,重启 OC 生效
模式: 自动 ← 自动 / 手动 / 关闭
含MCP的配置: C:\...\opencode.json
安静配置: C:\...\opencode.json.2
```
兼容旧「裸三行」格式(第1行模式、第2行含MCP、第3行安静),
插件/CLI 读到旧格式会**自动迁移写回**成上面的键值格式;
两行路径指向的文件不存在时会**自动扫同目录 `opencode.json.*` 自愈**并写回,
避免路径写错把 `/mcpon` `/mcpoff` 锁死。
**状态判定只看活文件**(文件名为 `opencode.json` 的那份)里 `mcp.servers` 是否非空,
所以对调之后第2/3行的身份怎么漂移都不影响判断——这与 TUI 插件、server 插件完全一致。
所以对调之后两行的身份怎么漂移都不影响判断——这与 TUI 插件、server 插件完全一致。
另外 `on`/`off` 成功后会维护 `mcp.on.flag`(开写、关切):
自动模式的「启动归位」见到标记就跳过,不会把刚开启的状态又换回去。
模式语义(三个入口共用同一套规则):
-15
View File
@@ -1,15 +0,0 @@
@echo off
chcp 65001 >nul
title MCP off (swap config + reload)
set "HERE=%~dp0"
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python not found in PATH.
pause
exit /b 1
)
python "%HERE%mcpctl.py" off
echo.
pause
-15
View File
@@ -1,15 +0,0 @@
@echo off
chcp 65001 >nul
title MCP on (swap config + reload)
set "HERE=%~dp0"
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python not found in PATH.
pause
exit /b 1
)
python "%HERE%mcpctl.py" on
echo.
pause
-15
View File
@@ -1,15 +0,0 @@
@echo off
chcp 65001 >nul
title MCP status
set "HERE=%~dp0"
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python not found in PATH.
pause
exit /b 1
)
python "%HERE%mcpctl.py" status
echo.
pause
+104 -23
View File
@@ -7,7 +7,7 @@ mcpctl —— 命令行版的 MCP 配置对调开关(与 TUI 插件 / server
status 查看当前模式与 MCP 开/关状态(只读,不改任何文件)
on 把配置对调为「含 MCP」的原始版,并调用 reload 热重载
off 把配置对调为「不含 MCP」的安静版,并调用 reload 热重载
mode 查看或修改 mcpcfg.txt 第1行(自动 / 手动 / 关闭)
mode 查看或修改 mcpcfg.txt 模式键(自动 / 手动 / 关闭)
模式语义(与插件完全一致):
自动 大模型可通过 server 插件切换;/mcpon /mcpoff 与本工具视为手动切换一次
@@ -15,7 +15,7 @@ mcpctl —— 命令行版的 MCP 配置对调开关(与 TUI 插件 / server
关闭 功能整体禁用,on/off 都会被拒绝
状态判定只看活文件(basename 为 opencode.json 的那份)里 mcp.servers 是否非空,
与 mcpcfg.txt 第2/3行的行语义无关,因此对调之后身份漂移也不影响判断。
与 mcpcfg.txt「含MCP的配置」/「安静配置」两键的行语义无关,因此对调之后身份漂移也不影响判断。
"""
import argparse
@@ -82,6 +82,9 @@ def resolve_cfg_file(target: str, distro: str) -> Path:
# ---------------------------------------------------------------- mcpcfg 解析
# 与 tui.js / ai-mcp.ts 的 readMcpCfg 保持一致:
# 优先按键值解析(模式: / 含MCP的配置: / 安静配置:,认中英文冒号),
# 解析不到再回退旧「裸三行」(第1行模式、第2行含MCP、第3行安静)。
def read_cfg(path: Path) -> dict:
if not path.exists():
return {
@@ -89,15 +92,35 @@ def read_cfg(path: Path) -> dict:
"error": f"缺少配置文件:{path}(配置缺失时功能视为关闭)",
}
try:
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
raw_text = path.read_text(encoding="utf-8", errors="replace")
except Exception as exc:
return {"mode": "off", "error": f"读取 {path} 失败:{exc}"}
key = (lines[0] if lines else "").strip().lower()
mode = LABEL_MODE.get(key, "auto" if key == "" else None) or "auto"
lines = raw_text.splitlines()
on_path = (lines[1] if len(lines) > 1 else "").strip()
quiet_path = (lines[2] if len(lines) > 2 else "").strip()
kv: dict[str, str] = {}
for ln in lines:
line = ln.strip()
if not line or line.startswith("#") or line.startswith("//"):
continue
m = re.match(r"^([^::]+)[::]\s*(.*)$", line)
if m:
kv[m.group(1).strip().lower()] = m.group(2).strip()
mode_val = kv.get("模式", kv.get("mode", kv.get("第1行")))
on_val = kv.get("含mcp的配置", kv.get("含mcp配置", kv.get("含mcp", kv.get("on", kv.get("原始配置", kv.get("第2行"))))))
quiet_val = kv.get("安静配置", kv.get("安静", kv.get("quiet", kv.get("备份", kv.get("backup", kv.get("第3行"))))))
is_legacy = mode_val is None and on_val is None and quiet_val is None
if not is_legacy:
mode = LABEL_MODE.get((mode_val or "自动").strip().lower(), "auto")
on_path = (on_val or "").strip()
quiet_path = (quiet_val or "").strip()
else:
bare = [l.strip() for l in lines if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("//")]
mode = LABEL_MODE.get((bare[0] if bare else "").strip().lower(), "auto" if not bare or bare[0] == "" else "auto")
on_path = (bare[1] if len(bare) > 1 else "").strip()
quiet_path = (bare[2] if len(bare) > 2 else "").strip()
active = shadow = None
if _basename(on_path) == "opencode.json":
@@ -111,15 +134,72 @@ def read_cfg(path: Path) -> dict:
"quiet_path": quiet_path,
"active": active,
"shadow": shadow,
"legacy": is_legacy,
"error": None,
}
if mode != "off" and not active:
result["error"] = "mcpcfg.txt 第2/3行必须有一个是 opencode.json 的完整路径"
result["error"] = "mcpcfg.txt 两行路径都不是 opencode.json(「含MCP的配置」或「安静配置」需指向 opencode.json)"
elif mode != "off" and (not Path(on_path).exists() or not Path(quiet_path).exists()):
result["error"] = "mcpcfg.txt 第2/3行指向的文件不存在(请检查路径)"
result["error"] = "mcpcfg.txt 指向的配置文件不存在(请检查「含MCP的配置」/「安静配置」两键的路径)"
return result
def write_cfg(path: Path, mode: str, on_path: str, quiet_path: str) -> None:
"""按键值格式写回(模式/含MCP的配置/安静配置),带注释头。"""
mode_cn = {"auto": "自动", "manual": "手动", "off": "关闭"}.get(mode, "自动")
body = "\n".join([
"# MCP 开关配置:改完保存,重启 OC 生效(第1个键=模式)",
"# 模式可填:自动(大模型可切换) / 手动(仅 /mcpon /mcpoff) / 关闭(禁用本功能)",
f"模式: {mode_cn}",
f"含MCP的配置: {on_path}",
f"安静配置: {quiet_path}",
"",
])
path.write_text(body, encoding="utf-8")
def set_mode_in_file(path: Path, mode: str) -> None:
"""只改模式键:键值文件按键替换;旧三行文件替换第1行(插件下次读会自动迁移)。"""
raw = path.read_text(encoding="utf-8", errors="replace")
lines = raw.splitlines()
mode_cn = {"auto": "自动", "manual": "手动", "off": "关闭"}.get(mode, "自动")
kv_pattern = re.compile(r"^(模式|mode)\s*[::]\s*.*$", re.IGNORECASE)
hit = False
for i, ln in enumerate(lines):
if kv_pattern.match(ln.strip()):
prefix = ":" if "模式" in ln.split(":")[0] or ":" in ln else ":"
lines[i] = f"模式{prefix} {mode_cn}"
hit = True
break
if not hit:
# 旧三行(或键缺失):第1行位放模式;若本来是键值缺模式键,插到最前面
if any(re.match(r"^([^::]+)[::]", l.strip()) for l in lines if l.strip() and not l.strip().startswith("#")):
lines.insert(0, f"模式: {mode_cn}")
else:
while len(lines) < 3:
lines.append("")
lines[0] = mode_cn
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def flag_path(cfg_dir_target: str, distro: str) -> Path:
"""显式开启标记 mcp.on.flag 的落点(与插件共用;wsl 目标落在 WSL 目录)。"""
base = wsl_config_dir(distro) if cfg_dir_target == "wsl" else _home_config()
return base / "mcp.on.flag"
def set_on_flag(cfg_dir_target: str, distro: str, on: bool) -> None:
try:
p = flag_path(cfg_dir_target, distro)
if on:
p.write_text(str(os.getpid()), encoding="utf-8")
elif p.exists():
p.unlink()
except Exception:
pass
def active_has_mcp(active: str | None) -> bool:
try:
if not active:
@@ -233,7 +313,7 @@ def _stamp() -> str:
def print_state(cfg: dict) -> bool:
has = active_has_mcp(cfg.get("active"))
print(f"模式:{MODE_LABEL.get(cfg['mode'], cfg['mode'])}(mcpcfg.txt 第1行)")
print(f"模式:{MODE_LABEL.get(cfg['mode'], cfg['mode'])}(mcpcfg.txt「模式」键)")
print(f"MCP :{'已开启(配置含 mcp.servers)' if has else '已关闭(安静,无 mcp 配置)'}")
print(f"活配置:{cfg.get('active') or '(未识别)'}")
if cfg.get("error"):
@@ -259,8 +339,8 @@ def _switch(args, want_on: bool) -> int:
action = "开启" if want_on else "关闭"
if cfg["mode"] == "off" and not args.ignore_mode:
print(f"拒绝:MCP 开关功能已关闭(mcpcfg.txt 第1行=关闭)。")
print("如需使用,请把第1行改成「自动」或「手动」。")
print("拒绝:MCP 开关功能已关闭(mcpcfg.txt「模式: 关闭」)。")
print("如需使用,请把模式改成「自动」或「手动」。")
return 3
has = active_has_mcp(cfg["active"])
@@ -272,9 +352,13 @@ def _switch(args, want_on: bool) -> int:
swap_files(cfg["on_path"], cfg["quiet_path"])
after = active_has_mcp(cfg["active"])
if after != want_on:
print("失败:配置对调后状态校验不符,请检查 mcpcfg.txt 第2/3行路径。")
print("失败:配置对调后状态校验不符,请检查 mcpcfg.txt「含MCP的配置」/「安静配置」路径。")
return 4
# 显式意图落标记:开写 mcp.on.flag、关删标记(与 tui.js / ai-mcp 共用,
# 避免自动模式的“启动归位”把刚开启的状态又换回去)
set_on_flag(args.target, args.distro, want_on)
print(f"配置已对调,MCP 现在是「{'开启' if after else '关闭'}」。")
if args.no_reload:
@@ -303,11 +387,11 @@ def cmd_mode(args) -> int:
if not path.exists():
print(f"配置文件不存在:{path}")
return 2
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
if not args.value:
cur = (lines[0] if lines else "").strip()
print(f"当前第1行:{cur or '(空,默认自动)'}")
cfg = read_cfg(path)
cur = MODE_LABEL.get(cfg.get("mode", "auto"), cfg.get("mode", "auto"))
print(f"当前模式:{cur}")
print("可选值:自动 / 手动 / 关闭(也支持 auto / manual / off)")
return 0
@@ -316,11 +400,8 @@ def cmd_mode(args) -> int:
print(f"无法识别的模式:{args.value}(可选:自动 / 手动 / 关闭 / auto / manual / off)")
return 2
while len(lines) < 3:
lines.append("")
lines[0] = LABEL_MODE[key]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"已把第1行设为:{MODE_LABEL[LABEL_MODE[key]]}({path})")
set_mode_in_file(path, LABEL_MODE[key])
print(f"已把模式设为:{MODE_LABEL[LABEL_MODE[key]]}({path})")
return 0
@@ -356,7 +437,7 @@ def build_parser() -> argparse.ArgumentParser:
p.add_argument("--timeout", type=float, default=5.0, help="HTTP 超时秒数")
p.add_argument("--password", default="", help="OC server.password(默认自动从 opencode.json 读)")
p.add_argument("--no-reload", action="store_true", help="只对调文件,不调用热重载")
p.add_argument("--ignore-mode", action="store_true", help="忽略第1行的「关闭」限制(慎用)")
p.add_argument("--ignore-mode", action="store_true", help="忽略「模式: 关闭」限制强制切换(慎用)")
sub = p.add_subparsers(dest="cmd")
@@ -369,7 +450,7 @@ def build_parser() -> argparse.ArgumentParser:
sp = sub.add_parser("off", help="对调为安静配置(删 MCP)并热重载")
sp.set_defaults(func=cmd_off)
sp = sub.add_parser("mode", help="查看或修改 mcpcfg.txt 第1行的模式")
sp = sub.add_parser("mode", help="查看或修改 mcpcfg.txt「模式」键(自动/手动/关闭)")
sp.add_argument("value", nargs="?", help="要设置的模式:自动 / 手动 / 关闭(不传则只查看)")
sp.set_defaults(func=cmd_mode)
+21
View File
@@ -0,0 +1,21 @@
@echo off
rem OC toolbox single entry: menu mode when no args, passthrough otherwise.
rem UTF-8 console codepage so the Python menu (Chinese) prints correctly.
chcp 65001 >nul
setlocal
cd /d "%~dp0"
set "PY="
where py >nul 2>nul && set "PY=py -3"
if not defined PY where python >nul 2>nul && set "PY=python"
if not defined PY where python3 >nul 2>nul && set "PY=python3"
if not defined PY (
echo [ERROR] Python not found in PATH. Install Python 3.10+ first.
pause
exit /b 1
)
%PY% oc.py %*
set "RC=%ERRORLEVEL%"
if not "%~1"=="" pause
exit /b %RC%
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
oc.py —— OC 工具箱统一入口(数字 / 中文菜单)
启动即打印菜单,输入编号或中文都能进:
0 / 返回 / 退出 / q 退出
1 / 技能 / 技能管理 / skill 进入技能管理(列表 / 推送 / 拉取 / 目录)
2 / 连接器 / 连接器管理 / mcp 进入连接器管理(状态 / 开关 / 模式 / 备份)
也支持免菜单直传子命令(等价旧的分散入口):
python oc.py skill list
python oc.py mcp status
python oc.py list (自动识别归属到技能管理)
底层实现复用同目录 ocskill.py / mcpctl.py,本文件只负责交互编排。
"""
import os
import sys
# 保证同目录模块可导入(无论从哪个工作目录启动)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mcpctl # noqa: E402
import ocskill # noqa: E402
# ---------------------------------------------------------------- 控制台
def fix_console() -> None:
"""Windows 控制台中文输出兜底(UTF-8 重配)。"""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
reconf = getattr(stream, "reconfigure", None)
if callable(reconf):
try:
reconf(encoding="utf-8", errors="replace")
except Exception:
pass
# ---------------------------------------------------------------- 输入解析
def _norm(text: str) -> str:
return (text or "").strip().lower().replace(" ", "")
# 顶层别名:值 -> 传给对应函数的“行为键”
TOP_ALIASES = {
"0": "quit", "0.": "quit", "q": "quit", "quit": "quit", "exit": "quit",
"退出": "quit", "返回": "quit", "回": "quit",
"1": "skill", "1.": "skill", "skill": "skill", "skills": "skill",
"技能": "skill", "技能管理": "skill", "管理技能": "skill", "技": "skill",
"2": "mcp", "2.": "mcp", "mcp": "mcp", "连接器": "mcp",
"连接器管理": "mcp", "管理连接器": "mcp", "连接": "mcp", "连": "mcp",
"connector": "mcp", "connectors": "mcp",
}
SKILL_ALIASES = {
"0": "back", "0.": "back", "b": "back", "back": "back", "返回": "back", "回": "back",
"1": "list", "1.": "list", "list": "list", "列表": "list", "查看": "list",
"技能列表": "list", "看列表": "list",
"2": "push", "2.": "push", "push": "push", "推送": "push", "上传": "push", "推": "push",
"3": "pull", "3.": "pull", "pull": "pull", "拉取": "pull", "下载": "pull", "拉": "pull",
"4": "dirs", "4.": "dirs", "dirs": "dirs", "目录": "dirs", "状态": "dirs",
"技能目录": "dirs",
}
MCP_ALIASES = {
"0": "back", "0.": "back", "b": "back", "back": "back", "返回": "back", "回": "back",
"1": "status", "1.": "status", "status": "status", "状态": "status", "查看": "status",
"连接器状态": "status",
"2": "on", "2.": "on", "on": "on", "开": "on", "开启": "on", "打开": "on",
"3": "off", "3.": "off", "off": "off", "关": "off", "关闭": "off",
"4": "mode", "4.": "mode", "mode": "mode", "模式": "mode",
"5": "backup", "5.": "backup", "backup": "backup", "备份": "backup",
}
def ask(prompt: str) -> str:
try:
return input(prompt)
except (EOFError, KeyboardInterrupt):
print()
return "0"
def match(raw: str, aliases: dict, default: str | None = None) -> str | None:
key = _norm(raw)
if key in aliases:
return aliases[key]
# 中文模糊包含:输入“技能管理菜单”也能命中“技能管理”
for k, v in aliases.items():
if k and k in key and not k.isdigit():
return v
return default
def pause(msg: str = "按回车返回…") -> None:
try:
input(msg)
except (EOFError, KeyboardInterrupt):
print()
def banner(title: str) -> None:
print()
print("=" * 44)
print(" " + title)
print("=" * 44)
# ---------------------------------------------------------------- 子菜单:技能管理
def menu_skill(shared: dict) -> None:
while True:
banner("技能管理(skill)")
print(" 0. 返回上层")
print(" 1. 查看技能列表")
print(" 2. 推送技能到 OC(zip / 目录 / SKILL.md / base64)")
print(" 3. 拉取技能到本机")
print(" 4. 查看技能目录与连接状态")
raw = ask("请选择(编号或中文): ")
act = match(raw, SKILL_ALIASES)
if act is None:
print(" 没看懂「%s」,输入 0-4 或中文,如:列表 / 推送 / 拉取" % raw.strip())
continue
if act == "back":
return
if act == "list":
ocskill.main(["list"])
elif act == "dirs":
ocskill.main(["dirs"])
elif act == "push":
src = ask(" 技能路径(文件 / 目录 / zip,回车取消): ").strip()
if not src:
continue
argv = ["push", src]
if match(ask(" 同名覆盖?(y/N): "), {"y": "y", "yes": "y", "是": "y"}, None) == "y":
argv.append("--force")
if match(ask(" 只校验不写入?(y/N): "), {"y": "y", "yes": "y", "是": "y"}, None) == "y":
argv.append("--dry-run")
ocskill.main(argv)
elif act == "pull":
name = ask(" 要拉取的技能名(回车取消): ").strip()
if not name:
continue
argv = ["pull", name]
out = ask(" 输出目录(回车=当前目录): ").strip()
if out:
argv += ["--out", out]
if match(ask(" 覆盖本地同名目录?(y/N): "), {"y": "y", "yes": "y", "是": "y"}, None) == "y":
argv.append("--force")
ocskill.main(argv)
pause()
# ---------------------------------------------------------------- 子菜单:连接器管理
def menu_mcp(shared: dict) -> None:
# 目标(本机 / WSL)本次会话记住,子菜单里随时可换
target = shared.get("target", "local")
while True:
banner("连接器管理(MCP,wb 语境的“连接器”)")
print(" 当前目标: %s" % ("本机" if target == "local" else "WSL"))
print(" 0. 返回上层")
print(" 1. 查看状态(模式 + 开关)")
print(" 2. 开启连接器(对调为含 MCP 配置 + 热重载)")
print(" 3. 关闭连接器(对调为安静配置 + 热重载)")
print(" 4. 查看 / 修改模式(自动 / 手动 / 关闭)")
print(" 5. 备份当前 mcpcfg.txt")
print(" t. 切换目标(本机 <-> WSL)")
raw = ask("请选择(编号或中文): ")
key = _norm(raw)
if key in ("t", "切换", "target", "目标"):
target = "wsl" if target == "local" else "local"
shared["target"] = target
print(" 已切换目标 -> %s" % ("本机" if target == "local" else "WSL"))
continue
act = match(raw, MCP_ALIASES)
if act is None:
print(" 没看懂「%s」,输入 0-5 / t 或中文,如:状态 / 开启 / 关闭 / 模式" % raw.strip())
continue
if act == "back":
return
base = ["--target", target]
if act == "status":
mcpctl.main(base + ["status"])
elif act == "on":
mcpctl.main(base + ["on"])
elif act == "off":
mcpctl.main(base + ["off"])
elif act == "backup":
mcpctl.main(base + ["backup"])
elif act == "mode":
val = ask(" 新模式(自动 / 手动 / 关闭,回车只查看): ").strip()
mcpctl.main(base + ["mode"] + ([val] if val else []))
pause()
# ---------------------------------------------------------------- 顶层菜单
def menu_top() -> None:
shared: dict = {"target": "local"}
while True:
banner("OC 工具箱(输入编号或中文,0 退出)")
print(" 0. 退出")
print(" 1. 技能管理(skill 拉取 / 推送)")
print(" 2. 连接器管理(MCP 开关 / 模式)")
raw = ask("请选择: ")
act = match(raw, TOP_ALIASES)
if act is None:
print(" 没看懂「%s」,输入 0 / 1 / 2,或:退出 / 技能管理 / 连接器管理" % raw.strip())
continue
if act == "quit":
print("再见。")
return
if act == "skill":
menu_skill(shared)
elif act == "mcp":
menu_mcp(shared)
# ---------------------------------------------------------------- 直传模式
SKILL_CMDS = {"list", "push", "pull", "dirs"}
MCP_CMDS = {"status", "on", "off", "mode", "backup"}
def dispatch(argv: list[str]) -> int:
"""有参数时免菜单直传(兼容旧分散入口的用法)。"""
head = _norm(argv[0]) if argv else ""
rest = argv[1:]
if head in ("skill", "skills", "技能", "技能管理"):
return ocskill.main(rest)
if head in ("mcp", "连接器", "连接器管理"):
return mcpctl.main(rest)
if head in SKILL_CMDS:
return ocskill.main(argv)
if head in MCP_CMDS:
return mcpctl.main(argv)
print("未知命令: %s(技能: %s;连接器: %s)" % (
argv[0], "/".join(sorted(SKILL_CMDS)), "/".join(sorted(MCP_CMDS))))
return 2
def main() -> int:
fix_console()
if len(sys.argv) > 1:
return dispatch(sys.argv[1:])
menu_top()
return 0
if __name__ == "__main__":
sys.exit(main())
-16
View File
@@ -1,16 +0,0 @@
@echo off
chcp 65001 >nul
title List WSL OpenCode skills
set "HERE=%~dp0"
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python not found in PATH.
echo Install Python 3 and tick "Add to PATH", then run this again.
pause
exit /b 1
)
python "%HERE%ocskill.py" list
echo.
pause
-23
View File
@@ -1,23 +0,0 @@
@echo off
chcp 65001 >nul
title Pull a skill from WSL OpenCode
set "HERE=%~dp0"
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python not found in PATH.
pause
exit /b 1
)
set /p "NAME=Skill name: "
if "%NAME%"=="" (
echo Nothing entered, cancelled.
pause
exit /b 0
)
set "OUT=%HERE%skills-backup"
python "%HERE%ocskill.py" pull "%NAME%" --out "%OUT%"
echo.
pause
-25
View File
@@ -1,25 +0,0 @@
@echo off
chcp 65001 >nul
title Push a skill to WSL OpenCode
set "HERE=%~dp0"
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python not found in PATH.
pause
exit /b 1
)
echo Accepts: a .zip file, a skill folder, a SKILL.md file, or base64 text.
echo Example: D:\skills\my-skill.zip
echo.
set /p "SRC=Path or content: "
if "%SRC%"=="" (
echo Nothing entered, cancelled.
pause
exit /b 0
)
python "%HERE%ocskill.py" push "%SRC%"
echo.
pause