CLI合并为oc.py/oc.bat菜单入口(数字+中文),mcpctl支持键值mcpcfg与on/off标记;删除6个分散bat
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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%
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user