542 lines
22 KiB
Python
542 lines
22 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",
|
||
"覆盖更新": "push", "更新": "push", "覆盖": "push",
|
||
"3": "pull", "3.": "pull", "pull": "pull", "拉取": "pull", "下载": "pull", "拉": "pull",
|
||
"4": "dirs", "4.": "dirs", "dirs": "dirs", "目录": "dirs", "状态": "dirs",
|
||
"技能目录": "dirs",
|
||
"5": "delete", "5.": "delete", "delete": "delete", "del": "delete",
|
||
"删除": "delete", "删": "delete", "移除": "delete", "删掉": "delete",
|
||
}
|
||
|
||
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",
|
||
}
|
||
|
||
|
||
# /home 别名集:任意子菜单输入即直接跳回首页(顶层菜单),不再一层层按 0 返回
|
||
HOME_KEYS = {"/home", "home", "首页", "返回首页", "回首页", "回主页", "主菜单", "主页"}
|
||
|
||
|
||
class GoHome(Exception):
|
||
"""在任意位置输入 /home(或「首页」等)—— 一路抛到顶层菜单重绘首页。"""
|
||
|
||
|
||
def ask(prompt: str) -> str:
|
||
try:
|
||
raw = input(prompt)
|
||
except (EOFError, KeyboardInterrupt):
|
||
print()
|
||
return "0"
|
||
if _norm(raw) in HOME_KEYS:
|
||
raise GoHome()
|
||
return raw
|
||
|
||
|
||
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:
|
||
raw = input(msg)
|
||
except (EOFError, KeyboardInterrupt):
|
||
print()
|
||
return
|
||
if _norm(raw) in HOME_KEYS:
|
||
raise GoHome()
|
||
|
||
|
||
def banner(title: str) -> None:
|
||
print()
|
||
print("=" * 44)
|
||
print(" " + title)
|
||
print("=" * 44)
|
||
|
||
|
||
def run_safe(fn, argv: list[str]) -> int:
|
||
"""菜单内调用 ocskill/mcpctl 的统一护栏:
|
||
- KeyboardInterrupt -> 打印“已取消”,回到菜单(不杀进程)
|
||
- SystemExit(push/pull 里的 raise SystemExit)-> 报告信息,
|
||
绝不让它穿透把整个菜单进程结束掉
|
||
- 其它异常 -> 打印错误,回到菜单
|
||
返回子命令退出码(仅直传模式会用到)。"""
|
||
try:
|
||
return fn(argv)
|
||
except KeyboardInterrupt:
|
||
print("\n已取消当前操作,返回菜单。")
|
||
return 130
|
||
except SystemExit as exc:
|
||
code = exc.code
|
||
if code is None:
|
||
return 0
|
||
if isinstance(code, int):
|
||
if code != 0:
|
||
print(f"操作未完成(退出码 {code}),返回菜单。")
|
||
return code
|
||
print(f"操作未完成:{code},返回菜单。")
|
||
return 1
|
||
except Exception as exc:
|
||
print(f"操作出错:{exc},返回菜单。")
|
||
return 1
|
||
|
||
|
||
# ---------------------------------------------------------------- 子菜单:技能管理
|
||
def _yes(prompt: str) -> bool:
|
||
"""统一的 y/N 判定(默认 N,回车即取消)。"""
|
||
return match(ask(prompt), {"y": "y", "yes": "y", "是": "y"}, None) == "y"
|
||
|
||
def _pick_agent(prompt: str, word: str) -> dict | None:
|
||
"""选来源/目标终端(宝宝级):先列可用终端,再收输入。
|
||
回车 = wsl(内置默认);简称前缀(wb…);列表序号(1./2.);或直接贴一整个 skill 目录。
|
||
输入 0/q/取消 返回 None。返回 ocskill.resolve_agent 的解析结果(kind/label/dir)。
|
||
"""
|
||
table = ocskill.load_agent_table()
|
||
print(" 可选终端(agents.md 每行一个,新增一行 = 新增一个终端):")
|
||
print(" · wsl(内置默认,回车即选)")
|
||
for i, t in enumerate(table, 1):
|
||
print(" %d. %s(%s) %s" % (i, t["key"], t["display"], t["dir"]))
|
||
raw = ocskill.clean_input(ask(prompt))
|
||
if _norm(raw) in ("0", "q", "quit", "取消", "back", "返回", "b"):
|
||
print(" 已取消。")
|
||
return None
|
||
if raw.isdigit() and table and 1 <= int(raw) <= len(table):
|
||
raw = table[int(raw) - 1]["key"]
|
||
res = ocskill.resolve_agent(raw)
|
||
if not res.get("ok"):
|
||
print(" %s" % res.get("err"))
|
||
return None
|
||
print(" %s:%s -> %s" % (word, res["label"], res["dir"]))
|
||
return res
|
||
|
||
|
||
def _gate_unc(root: str, action: str) -> bool:
|
||
"""来源/目标是 WSL UNC 时先唤醒 VM(本机目录完全不碰 WSL)。
|
||
返回 False = 唤醒失败(错误已打印)。"""
|
||
if not ocskill._is_unc_root(root):
|
||
return True
|
||
err = ocskill._wsl_ensure_running(ocskill.DEFAULT_DISTRO, action)
|
||
if err:
|
||
print(" %s" % err)
|
||
return False
|
||
return True
|
||
|
||
|
||
def _confirm_sel(idx: int, names: list[str], root: str, extra: str = "") -> bool:
|
||
"""编号选择的二次确认:打印 skill 名字 + 简介,confirm 执行 / cancel 回退。
|
||
(`N -confirm` 已在解析层跳过本步;这里只处理 `N` / `N.` 的常规确认。)
|
||
"""
|
||
name = names[idx - 1]
|
||
print(" 已选 %d:%s" % (idx, name))
|
||
brief = ocskill.skill_brief(name, root)
|
||
if brief.get("ok"):
|
||
desc = (brief.get("description") or "").replace("\n", " ").strip()
|
||
if desc:
|
||
print(" 简介:%s" % (desc[:100] + ("..." if len(desc) > 100 else "")))
|
||
print(" 路径:%s" % brief.get("path"))
|
||
else:
|
||
print(" %s" % brief.get("err"))
|
||
if extra:
|
||
print(" %s" % extra)
|
||
while True:
|
||
ans = _norm(ask(" 输入 confirm 执行 / cancel 回退: "))
|
||
if ans in ("confirm", "确认", "确定", "yes", "y"):
|
||
return True
|
||
if ans in ("cancel", "取消", "0", "返回", "回", "b", "back", ""):
|
||
print(" 已回退。")
|
||
return False
|
||
print(" 没看懂「%s」:confirm 执行,cancel 回退。" % ans)
|
||
|
||
|
||
def _pull_one(name: str, src: dict) -> None:
|
||
"""按已解析来源拉取一个 skill:探查 -> 无同名直接拉;确有同名才问一次覆盖。"""
|
||
probe = ocskill.probe_pull(name, root=src["dir"])
|
||
if not probe.get("ok"):
|
||
print(" %s" % probe.get("err"))
|
||
return
|
||
print(" 输出:%s(固定,不需选择)" % probe["dest"])
|
||
argv = ["pull", name]
|
||
if src["kind"] != "default":
|
||
argv += ["--from", src["label"]]
|
||
if not probe.get("exists"):
|
||
print(" 本机无同名目录,直接拉取。")
|
||
run_safe(ocskill.main, argv)
|
||
return
|
||
if _yes(" 本机已存在同名 skill「%s」,覆盖?(y/N): " % name):
|
||
print(" 覆盖更新(旧版本先挑进 .trash/ 备份)。")
|
||
run_safe(ocskill.main, argv + ["--force"])
|
||
else:
|
||
print(" 已跳过覆盖,未改动任何文件。")
|
||
|
||
|
||
def _flow_push() -> None:
|
||
"""推送 / 覆盖更新(宝宝级):选目标终端 -> 打印本机 skill 编号列表 ->
|
||
输编号选技能(1 / 1. 二次确认打印名字+简介,confirm 执行 / cancel 回退;
|
||
1 -confirm 跳过二次确认)或直接贴路径 / zip / SKILL.md -> 校验规范 -> 探查同名
|
||
-> 确有同名才问一次覆盖 -> 写入。
|
||
目标没有同名文件夹时直接写(没有目录和内容,覆盖无从谈起,问了也是白问)。
|
||
"""
|
||
tgt = _pick_agent(" 目标终端(回车=wsl,简称如 wb / oc,或贴完整目录,0/q 取消): ", "目标")
|
||
if tgt is None:
|
||
return
|
||
if not _gate_unc(tgt["dir"], "push"):
|
||
return
|
||
# 选完目标就打本机 skill 列表(带编号)——push 要推的是本机技能
|
||
local_names = ocskill.list_skill_names(ocskill.WIN_LOCAL_ROOT)
|
||
if local_names:
|
||
ocskill.render_page(local_names, 1, ocskill.WIN_LOCAL_ROOT)
|
||
print(" 提示:编号 1 / 1. 会打印名字+简介二次确认(confirm 执行 / cancel 回退),"
|
||
"1 -confirm 跳过确认;也可以直接贴路径 / zip / SKILL.md")
|
||
src = ocskill.clean_input(ask(" 技能输入(编号 / 路径 / zip / SKILL.md,可带引号,回车取消): "))
|
||
if not src:
|
||
return
|
||
# 0) 编号选择本机 skill(协议:1 / 1. 二次确认,1 -confirm 跳过)
|
||
sel = ocskill.parse_sel(src)
|
||
if sel is not None:
|
||
if not local_names:
|
||
print(" 本机 skill 目录为空,编号没得选;请直接贴路径 / zip。")
|
||
return
|
||
idx, skip = sel
|
||
if not (1 <= idx <= len(local_names)):
|
||
print(" 序号超出范围(1-%d)。" % len(local_names))
|
||
return
|
||
if not (skip or _confirm_sel(idx, local_names, ocskill.WIN_LOCAL_ROOT,
|
||
extra="目标:%s" % tgt["dir"])):
|
||
print(" 已取消推送。")
|
||
return
|
||
src = os.path.join(ocskill.WIN_LOCAL_ROOT, local_names[idx - 1])
|
||
print(" 选定:「%s」 -> %s" % (local_names[idx - 1], src))
|
||
# 1) 先过一遍规范校验(只读探查,不写盘)
|
||
probe = ocskill.probe_push(src, root=tgt["dir"])
|
||
if not probe.get("ok"):
|
||
print(" 校验未通过:%s" % probe.get("err"))
|
||
return
|
||
name = probe["name"]
|
||
desc = (probe.get("description") or "").replace("\n", " ")
|
||
print(" 校验通过:「%s」" % name)
|
||
if desc:
|
||
print(" 描述:%s" % (desc[:90] + ("..." if len(desc) > 90 else "")))
|
||
print(" 输出:%s" % probe["dest"])
|
||
argv = ["push", src]
|
||
if tgt["kind"] != "default":
|
||
argv += ["--to", tgt["label"]]
|
||
# 2) 探查同名:不存在 -> 直接放,不问覆盖
|
||
if not probe.get("exists"):
|
||
print(" 目标无同名目录,直接推送。")
|
||
run_safe(ocskill.main, argv)
|
||
return
|
||
# 3) 确有同名文件夹 -> 这才有资格问一次覆盖
|
||
if _yes(" 已存在同名 skill「%s」,覆盖?(y/N): " % name):
|
||
print(" 覆盖更新(旧版本先挑进 .trash/ 备份)。")
|
||
run_safe(ocskill.main, argv + ["--force"])
|
||
else:
|
||
print(" 已跳过覆盖,未改动任何文件。")
|
||
|
||
|
||
def _flow_pull() -> None:
|
||
"""拉取(宝宝级):选来源 -> 立即打印带编号的 skill 列表(每页 15 个,全局序号
|
||
跨页连续)—— 记得名字直接输名字;不记得输编号:1 / 1. 进二次确认(打印 skill
|
||
名字 + 简介,confirm 执行 / cancel 回退列表),1 -confirm 跳过二次确认;
|
||
翻页:下一页/上一页/第N页/next/last;q 退出。
|
||
输出固定为本机 OC skill 目录;列表直读文件夹,不启动 OC 服务、不发请求;
|
||
覆盖只在确有同名目录时问一次。
|
||
"""
|
||
src = _pick_agent(" 来源终端(回车=wsl,简称如 wb / oc,或贴完整目录,0/q 取消): ", "来源")
|
||
if src is None:
|
||
return
|
||
if not _gate_unc(src["dir"], "pull"):
|
||
return
|
||
root = src["dir"]
|
||
names = ocskill.list_skill_names(root)
|
||
if not names:
|
||
# 目录为空:没得列,退化成纯名字输入(名字直拉仍然有效)
|
||
print(" 这个来源下还没有 skill:%s" % root)
|
||
name = ocskill.clean_input(ask(" 技能名(直接输名字试试,回车取消): "))
|
||
if name:
|
||
_pull_one(name, src)
|
||
return
|
||
print(" 提示:名字直接拉;编号 1 / 1. 会打印名字+简介二次确认(confirm 执行 / "
|
||
"cancel 回退),1 -confirm 跳过确认")
|
||
page = 1
|
||
while True:
|
||
# 选完来源第一次进循环就把列表(带编号)打出来
|
||
page, pages = ocskill.render_page(names, page, root)
|
||
cmd = ocskill.clean_input(ask(
|
||
" 拉取:输名字或序号;翻页:下一页/上一页/第N页/next/last;q 退出: "))
|
||
if not cmd:
|
||
print(" (空输入。名字/序号 = 拉取,下一页/上一页 = 翻页,q = 退出)")
|
||
continue
|
||
if _norm(cmd) in ("q", "quit", "退出", "取消", "0", "b", "back", "返回"):
|
||
print(" 已退出列表,未拉取。")
|
||
return
|
||
# 1) 翻页命令(中英都认:下一页/第N页/上一页、next/last/N page)
|
||
np = ocskill.parse_page_cmd(cmd, page, pages)
|
||
if np is not None:
|
||
page = np
|
||
continue
|
||
# 2) 编号选择(协议:1 / 1. 二次确认;1 -confirm 跳过确认)
|
||
sel = ocskill.parse_sel(cmd)
|
||
if sel is not None:
|
||
idx, skip = sel
|
||
if not (1 <= idx <= len(names)):
|
||
print(" 序号超出范围(1-%d),重输。" % len(names))
|
||
continue
|
||
if skip or _confirm_sel(idx, names, root):
|
||
_pull_one(names[idx - 1], src)
|
||
return
|
||
# cancel -> 回退,留在列表重选
|
||
continue
|
||
# 3) 直接输名字(记得名字不需确认,直接拉)
|
||
if ocskill.NAME_RE.match(cmd):
|
||
if ocskill.probe_pull(cmd, root=root).get("ok"):
|
||
_pull_one(cmd, src)
|
||
return
|
||
print(" 来源里没有「%s」,留在列表重输(序号或名字)。" % cmd)
|
||
continue
|
||
print(" 没看懂「%s」:序号/名字拉取,下一页/上一页翻页,q 退出。" % cmd)
|
||
|
||
def _flow_delete() -> None:
|
||
"""删除:输入技能名 -> 选端 -> 展示将删路径 -> 确认 -> 删除(先挑 .trash/ 可恢复)。"""
|
||
name = ocskill.clean_input(ask(" 要删除的技能名(可带引号,回车取消): "))
|
||
if not name:
|
||
return
|
||
side = match(
|
||
ask(" 删哪端?(1=Wsl / 2=本机,回车取消): "),
|
||
{"1": "wsl", "1.": "wsl", "wsl": "wsl", "ws": "wsl", "远程": "wsl", "wsl端": "wsl",
|
||
"2": "local", "2.": "local", "local": "local", "本机": "local", "本地": "local", "本机端": "local"},
|
||
None,
|
||
)
|
||
if side is None:
|
||
print(" 已取消。")
|
||
return
|
||
local = side == "local"
|
||
probe = ocskill.probe_delete(name, ocskill.DEFAULT_DISTRO, local)
|
||
if not probe.get("ok"):
|
||
print(" %s" % probe.get("err"))
|
||
return
|
||
print(" 将删除%s:%s" % (probe["side"], probe["dest"]))
|
||
print(" (会先挑进 .trash/ 备份,删错了可移回)")
|
||
if not _yes(" 确认删除?(y/N): "):
|
||
print(" 已取消删除。")
|
||
return
|
||
argv = ["delete", name, "--yes"]
|
||
if local:
|
||
argv.append("--local")
|
||
run_safe(ocskill.main, argv)
|
||
|
||
def menu_skill(shared: dict) -> None:
|
||
while True:
|
||
banner("技能管理(skill)")
|
||
print(" 0. 返回上层")
|
||
print(" 1. 查看技能列表")
|
||
print(" 2. 推送 / 覆盖更新技能到目标终端(选完列本机编号:1 / 1. 二次确认,"
|
||
"1 -confirm 跳过;也可贴路径/zip)")
|
||
print(" 3. 拉取技能到本机 OC(选来源即出编号列表:名字直拉;1 / 1. 二次确认,"
|
||
"1 -confirm 跳过确认)")
|
||
print(" 4. 查看技能目录与连接状态")
|
||
print(" 5. 删除技能(先挑 .trash/ 备份,可恢复)")
|
||
print(" 提示:任意位置输入 /home 或「返回首页」直接回顶层菜单")
|
||
print(" 覆盖只在确有同名文件夹时才会问你一次")
|
||
raw = ask("请选择(编号或中文,/home 回首页): ")
|
||
act = match(raw, SKILL_ALIASES)
|
||
if act is None:
|
||
print(" 没看懂「%s」,输入 0-5 或中文,如:列表 / 推送 / 拉取 / 删除" % raw.strip())
|
||
continue
|
||
if act == "back":
|
||
return
|
||
|
||
if act == "list":
|
||
run_safe(ocskill.main, ["list"])
|
||
elif act == "dirs":
|
||
run_safe(ocskill.main, ["dirs"])
|
||
elif act == "push":
|
||
_flow_push()
|
||
elif act == "pull":
|
||
_flow_pull()
|
||
elif act == "delete":
|
||
_flow_delete()
|
||
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)")
|
||
print(" 提示:任意位置输入 /home 或「返回首页」直接回顶层菜单")
|
||
raw = ask("请选择(编号或中文,/home 回首页): ")
|
||
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":
|
||
run_safe(mcpctl.main, base + ["status"])
|
||
elif act == "on":
|
||
run_safe(mcpctl.main, base + ["on"])
|
||
elif act == "off":
|
||
run_safe(mcpctl.main, base + ["off"])
|
||
elif act == "backup":
|
||
run_safe(mcpctl.main, base + ["backup"])
|
||
elif act == "mode":
|
||
val = ask(" 新模式(自动 / 手动 / 关闭,回车只查看): ").strip()
|
||
run_safe(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 开关 / 模式)")
|
||
print(" 提示:子菜单任意位置输入 /home 或「返回首页」可直接回到本菜单")
|
||
try:
|
||
raw = ask("请选择: ")
|
||
except GoHome:
|
||
print(" 已经在首页了")
|
||
continue
|
||
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":
|
||
try:
|
||
menu_skill(shared)
|
||
except GoHome:
|
||
print(" 已返回首页")
|
||
elif act == "mcp":
|
||
try:
|
||
menu_mcp(shared)
|
||
except GoHome:
|
||
print(" 已返回首页")
|
||
|
||
|
||
# ---------------------------------------------------------------- 直传模式
|
||
SKILL_CMDS = {"list", "push", "pull", "dirs", "delete"}
|
||
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())
|