diff --git a/oc.py b/oc.py index 835485e..7f74277 100644 --- a/oc.py +++ b/oc.py @@ -194,6 +194,32 @@ def _gate_unc(root: str, action: str) -> bool: 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"]) @@ -216,7 +242,9 @@ def _pull_one(name: str, src: dict) -> None: def _flow_push() -> None: - """推送 / 覆盖更新(宝宝级):选目标终端 -> 输入 -> 校验规范 -> 探查同名 + """推送 / 覆盖更新(宝宝级):选目标终端 -> 打印本机 skill 编号列表 -> + 输编号选技能(1 / 1. 二次确认打印名字+简介,confirm 执行 / cancel 回退; + 1 -confirm 跳过二次确认)或直接贴路径 / zip / SKILL.md -> 校验规范 -> 探查同名 -> 确有同名才问一次覆盖 -> 写入。 目标没有同名文件夹时直接写(没有目录和内容,覆盖无从谈起,问了也是白问)。 """ @@ -225,9 +253,31 @@ def _flow_push() -> None: return if not _gate_unc(tgt["dir"], "push"): return - src = ocskill.clean_input(ask(" 技能输入(路径 / zip / SKILL.md,可带引号,回车取消): ")) + # 选完目标就打本机 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"): @@ -256,8 +306,10 @@ def _flow_push() -> None: def _flow_pull() -> None: - """拉取(宝宝级二选一):选来源 -> 记得名字直接拉;不记得回车进分页列表 - (每页 15 个,全局序号跨页连续)—— 翻页命令 / 序号 / 直接名字 / q 退出。 + """拉取(宝宝级):选来源 -> 立即打印带编号的 skill 列表(每页 15 个,全局序号 + 跨页连续)—— 记得名字直接输名字;不记得输编号:1 / 1. 进二次确认(打印 skill + 名字 + 简介,confirm 执行 / cancel 回退列表),1 -confirm 跳过二次确认; + 翻页:下一页/上一页/第N页/next/last;q 退出。 输出固定为本机 OC skill 目录;列表直读文件夹,不启动 OC 服务、不发请求; 覆盖只在确有同名目录时问一次。 """ @@ -266,23 +318,25 @@ def _flow_pull() -> None: return if not _gate_unc(src["dir"], "pull"): return - name = ocskill.clean_input(ask(" 技能名(记得就直接输;不记得回车看列表): ")) - if name: - _pull_one(name, src) - return - # ---- 不记得名字:分页列表循环(直读文件夹,不启动 OC 服务)---- 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 退出: ")) + " 拉取:输名字或序号;翻页:下一页/上一页/第N页/next/last;q 退出: ")) if not cmd: - print(" (空输入。序号或名字 = 拉取,下一页/上一页 = 翻页,q = 退出)") + print(" (空输入。名字/序号 = 拉取,下一页/上一页 = 翻页,q = 退出)") continue if _norm(cmd) in ("q", "quit", "退出", "取消", "0", "b", "back", "返回"): print(" 已退出列表,未拉取。") @@ -292,15 +346,19 @@ def _flow_pull() -> None: if np is not None: page = np continue - # 2) 纯数字 = 全局序号(跨页连续) - if cmd.isdigit(): - idx = int(cmd) - if 1 <= idx <= len(names): + # 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 - print(" 序号超出范围(1-%d),重输。" % len(names)) + # cancel -> 回退,留在列表重选 continue - # 3) 直接输名字 + # 3) 直接输名字(记得名字不需确认,直接拉) if ocskill.NAME_RE.match(cmd): if ocskill.probe_pull(cmd, root=root).get("ok"): _pull_one(cmd, src) @@ -343,8 +401,10 @@ def menu_skill(shared: dict) -> None: banner("技能管理(skill)") print(" 0. 返回上层") print(" 1. 查看技能列表") - print(" 2. 推送 / 覆盖更新技能到目标终端(回车=wsl,简称 wb / oc,或贴目录)") - print(" 3. 拉取技能到本机 OC(选来源;不记得名字回车看分页列表,每页 15 个)") + print(" 2. 推送 / 覆盖更新技能到目标终端(选完列本机编号:1 / 1. 二次确认," + "1 -confirm 跳过;也可贴路径/zip)") + print(" 3. 拉取技能到本机 OC(选来源即出编号列表:名字直拉;1 / 1. 二次确认," + "1 -confirm 跳过确认)") print(" 4. 查看技能目录与连接状态") print(" 5. 删除技能(先挑 .trash/ 备份,可恢复)") print(" 提示:任意位置输入 /home 或「返回首页」直接回顶层菜单") diff --git a/ocskill.py b/ocskill.py index dc600e7..6d784e0 100644 --- a/ocskill.py +++ b/ocskill.py @@ -565,6 +565,28 @@ def list_skill_names(root) -> list[str]: return [] +def parse_sel(text) -> tuple[int, bool] | None: + """编号选择解析(确认协议)-> (全局序号, 是否跳过二次确认);不是编号输入返回 None。 + + 协议(推进去 / 取回来两端通用): + '1' / '1.' -> (1, False) 数字后带点号 = 确认选择它,进二次确认 + (打印 skill 名字 + 简介,confirm 执行 / cancel 回退) + '1 -confirm' -> (1, True) 跳过二次确认,直接执行('-confirm' 大小写不敏感, + 也认 '1.-confirm' / '1-confirm' / '--confirm') + 名字 / 翻页 / 路径 -> None(交给调用方的其它分支) + 纯 '0' 也命中 (0, False) —— 调用方若把 0 当退出,必须在本函数之前先拦。 + 序号范围由调用方按列表长度校验。 + """ + s = (text or "").strip() + if not s: + return None + m = re.fullmatch(r"(\d+)\s*\.?\s*(?:-{1,2}confirm)?", s, re.IGNORECASE) + if not m: + return None + skip = re.search(r"-{1,2}confirm", s, re.IGNORECASE) is not None + return int(m.group(1)), skip + + def parse_page_cmd(text, page: int, total: int) -> int | None: """翻页命令 -> 目标页码(已钳制 [1, total]);不是翻页命令返回 None。 @@ -669,6 +691,34 @@ def probe_push(src: str, distro: str = DEFAULT_DISTRO, b64: bool = False, return {"ok": True, "name": name, "dest": str(dest), "exists": exists, "description": meta.get("description", "")} +def skill_brief(name: str, root) -> dict: + """读一个 skill 的名字 + 简介 + 路径(只读,供编号选择的二次确认打印)。 + + 简介取 SKILL.md frontmatter 的 description(用现成的 find_skill_md + _frontmatter, + 兼容 SKILL.md 在子层 / 块标量描述);目录不存在或名字非法返回 ok=False + err。 + """ + name = clean_input(name or "") + if not name: + return {"ok": False, "err": "技能名为空"} + if not NAME_RE.match(name): + return {"ok": False, "err": f"非法 skill 名称「{name}」"} + folder = Path(root) / name + try: + exists = folder.is_dir() + except OSError: + exists = False + if not exists: + return {"ok": False, "err": f"不存在该 skill:{folder}"} + desc = "" + md = find_skill_md(folder) + if md is not None: + try: + desc = _frontmatter(md.read_text(encoding="utf-8", errors="replace")).get("description", "") + except OSError: + desc = "" + return {"ok": True, "name": name, "description": desc, "path": str(folder)} + + def probe_pull(name: str, distro: str = DEFAULT_DISTRO, root: str | None = None) -> dict: """拉取前探查(只读):来源端是否存在该 skill + 本机是否已有同名目录。