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