#!/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 编号列表 (列表跟随所选目录:选 wb 列 wb,2026-09-24 起不再硬编码本机 OC 目录;带翻页)-> 翻页(下一页/上一页/第N页/next/last,与拉取端同一条 parse_page_cmd,越界拒绝跳转)、 输编号选技能(1 / 1. 二次确认打印名字+简介,confirm 执行 / cancel 回列表; 1 -confirm 跳过二次确认)或直接贴路径 / zip / SKILL.md -> 选【目标】终端 -> 校验规范 -> 探查同名 -> 确有同名才问一次覆盖 -> 写入。 目标没有同名文件夹时直接写(没有目录和内容,覆盖无从谈起,问了也是白问)。 来源与目标同目录时拦掉(自己推自己没有意义)。 """ root_pick = _pick_agent(" 来源终端(列它的 skill,回车=wsl,简称如 wb / oc,或贴完整目录,0/q 取消): ", "来源") if root_pick is None: return if not _gate_unc(root_pick["dir"], "push"): return # 列表 = 所选来源目录的 skill(选 wb 就列 wb —— 用户协议:选了目录就把 skill 打出来带编号) root = root_pick["dir"] local_names = ocskill.list_skill_names(root) src = None if not local_names: # 所选目录没列表可打:退化成单次输入(没有编号/翻页可言) src = ocskill.clean_input(ask(" 技能输入(路径 / zip / SKILL.md,可带引号,回车取消): ")) if not src: return else: print(" 提示:编号 1 / 1. 会打印名字+简介二次确认(confirm 执行 / cancel 回列表)," "1 -confirm 跳过确认;翻页:下一页/上一页/第N页/next/last/one page;" "也可直接贴路径 / zip / SKILL.md") page = 1 while True: # 每次进循环先把当前页(带编号)打出来 —— 含首次进入(目录 = 所选来源) page, pages = ocskill.render_page(local_names, page, root) cmd = ocskill.clean_input(ask( " 技能输入:翻页(下一页/上一页/第N页/next/last);编号选来源技能;" "或贴路径 / zip / SKILL.md;q 取消: ")) if not cmd: print(" 已取消推送(回车=取消;翻页 / 编号 / 路径都能输)。") return if _norm(cmd) in ("q", "quit", "退出", "取消", "0", "b", "back", "返回"): print(" 已取消推送。") return # 1) 翻页命令(与拉取端同一条 parse_page_cmd:中英双语全支持;越界拒绝跳转) np = ocskill.parse_page_cmd(cmd, page, pages) if np is not None: if not (1 <= np <= pages): print(" 共 %d 页,没有第 %d 页(当前停在第 %d 页)。" % (pages, np, page)) continue page = np continue # 2) 编号选择来源 skill(协议:1 / 1. 二次确认,1 -confirm 跳过) sel = ocskill.parse_sel(cmd) if sel is not None: idx, skip = sel if not (1 <= idx <= len(local_names)): print(" 序号超出范围(1-%d),重输。" % len(local_names)) continue if not (skip or _confirm_sel(idx, local_names, root, extra="来源:%s" % root)): print(" 已回退到列表,未推送。") continue src = os.path.join(root, local_names[idx - 1]) print(" 选定:「%s」 -> %s" % (local_names[idx - 1], src)) break # 3) 其余输入 = 路径 / zip / SKILL.md,交给下面的规范校验去判 src = cmd break if not src: return # 技能选定后才选目标终端(列表已按所选来源打出) tgt = _pick_agent(" 目标终端(推送到哪,回车=wsl,简称如 wb / oc,或贴完整目录,0/q 取消): ", "目标") if tgt is None: print(" 已取消推送(技能已选好,未写入任何文件)。") return if not _gate_unc(tgt["dir"], "push"): return # 来源与目标同目录 = 自己推自己:拦掉(推了等于没推,-f 还可能自覆盖搞坏) if os.path.isdir(src) and os.path.normcase( os.path.abspath(os.path.dirname(os.path.abspath(src)))) == \ os.path.normcase(os.path.abspath(tgt["dir"])): print(" 来源与目标是同一个目录(%s),自己推自己没有意义,已取消。" % os.path.abspath(tgt["dir"])) return # 1) 先过一遍规范校验(只读探查,不写盘) probe = ocskill.probe_push(src, root=tgt["dir"]) if not probe.get("ok"): err = probe.get("err") or "" if os.path.isdir(src) and ocskill.find_skill_md(ocskill.Path(src)) is None: print(" 该目录缺 SKILL.md(空目录或不完整 skill),无法推送:%s" % src) print(" (提示:回列表换一个,或直接贴完整 skill 的路径。)") else: print(" 校验未通过:%s" % 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: if not (1 <= np <= pages): print(" 共 %d 页,没有第 %d 页(当前停在第 %d 页)。" % (pages, np, page)) continue 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 _delete_one(name: str, root: str) -> None: """按编号/名字删除一个 skill:先打印将删路径,再走 CLI 删除(自动挑 .trash/ 备份)。""" probe = ocskill.probe_delete(name, ocskill.DEFAULT_DISTRO, root=root) if not probe.get("ok"): print(" %s" % probe.get("err")) return print(" 将删除%s:%s" % (probe["side"], probe["dest"])) print(" (会先挑进 .trash/ 备份,删错了可移回)") run_safe(ocskill.main, ["delete", name, "--yes", "--dir", root]) def _flow_delete() -> None: """删除(宝宝级,与推拉同一套编号协议):选【目标】终端 -> 立即打印该目录 skill 编号列表(每页 15 个、序号跨页连续,翻页绝对页码越界拒绝)-> 输编号选技能: `1 -confirm` 跳过二次确认直接删除(仍先挑 .trash/ 备份,可恢复); `1` / `1.` 打印 skill 名字 + 描述 + 路径,confirm 确认删除 / cancel 取消不动; 也可直接输名字(删除是危险操作,同样走二次确认)。q 退出。 """ tgt = _pick_agent(" 删哪端(回车=wsl,简称如 wb / oc,或贴完整目录,0/q 取消): ", "目标") if tgt is None: return if not _gate_unc(tgt["dir"], "delete"): return root = tgt["dir"] names = ocskill.list_skill_names(root) if not names: print(" 这个目录下还没有 skill:%s" % root) return print(" 提示:编号 1 / 1. 会打印名字+简介二次确认(confirm 删除 / cancel 回列表)," "1 -confirm 直接删除;翻页:下一页/上一页/第N页/next/last;" "也可直接输名字;删除先挑 .trash/ 备份,可恢复") page = 1 while True: # 每次进循环先把当前页(带编号)打出来 —— 含首次进入(目录 = 所选终端) page, pages = ocskill.render_page(names, page, root) cmd = ocskill.clean_input(ask( " 删除:输编号(1 / 1. 确认,1 -confirm 直删)或名字;" "翻页:下一页/上一页/第N页/next/last;q 取消: ")) if not cmd: print(" (空输入。编号/名字 = 删除,下一页/上一页 = 翻页,q = 退出)") continue if _norm(cmd) in ("q", "quit", "退出", "取消", "0", "b", "back", "返回"): print(" 已退出列表,未删除任何 skill。") return # 1) 翻页命令(与推拉同一条 parse_page_cmd:中英双语;绝对页码越界拒绝跳转) np = ocskill.parse_page_cmd(cmd, page, pages) if np is not None: if not (1 <= np <= pages): print(" 共 %d 页,没有第 %d 页(当前停在第 %d 页)。" % (pages, np, page)) continue page = np continue # 2) 编号选择(协议:1 -confirm 直删;1 / 1. 打印名字+简介+路径二次确认) 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 name = names[idx - 1] if not (skip or _confirm_sel(idx, names, root, extra="删除会先挑进 .trash/ 备份,可恢复")): print(" 已取消,这个 skill 没动。") continue _delete_one(name, root) return # 3) 直接输名字(删除是危险操作,同样走二次确认,不给静默直删) if ocskill.NAME_RE.match(cmd): brief = ocskill.skill_brief(cmd, root) if not brief.get("ok"): print(" %s" % brief.get("err")) continue print(" 已选:%s" % cmd) desc = (brief.get("description") or "").replace("\n", " ").strip() if desc: print(" 简介:%s" % (desc[:100] + ("..." if len(desc) > 100 else ""))) print(" 路径:%s" % brief.get("path")) print(" 删除会先挑进 .trash/ 备份,可恢复") ans = _norm(ask(" 输入 confirm 删除 / cancel 取消: ")) if ans not in ("confirm", "确认", "确定", "yes", "y"): print(" 已取消,这个 skill 没动。") continue _delete_one(cmd, root) return print(" 没看懂「%s」:编号/名字删除,下一页/上一页翻页,q 退出。" % cmd) 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. 查看两端 skill 目录情况(路径 + skill 数量 + 本机/WSL API 分开探测)") print(" 5. 删除技能(选端即出编号列表:1 -confirm 直删;1 / 1. 打印名字+简介" "二次确认,confirm 删 / cancel 不动;先挑 .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())