254 lines
9.4 KiB
Python
254 lines
9.4 KiB
Python
#!/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())
|