技能编号确认协议:1/1.进二次确认打印名字简介(confirm执行/cancel回退),1 -confirm跳过;选来源/目标即出带编号列表,推送支持编号选本机技能

This commit is contained in:
ypshidifu
2026-09-24 09:53:23 +08:00
parent 2f653473ea
commit 8203076851
2 changed files with 129 additions and 19 deletions
+79 -19
View File
@@ -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 或「返回首页」直接回顶层菜单")
+50
View File
@@ -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 + 本机是否已有同名目录。