106 lines
4.1 KiB
Python
Executable File
106 lines
4.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""skill-find —— 任务→技能 检索器(2026-09-11 建,牧尘"技能本来都有你却不用"的机械解法)
|
||
|
||
为什么存在:规则写了 N 遍我没执行,因为"扫技能库"这件事在脑子里是**可选动作**。
|
||
把它变成一条命令:**任何任务动手前先跑它**,命中即 skill_view 加载。
|
||
|
||
用法:
|
||
skill-find.py "obsidian 同步 配置" # 关键词
|
||
skill-find.py "密钥泄露了怎么清历史" # 整句任务描述也行
|
||
skill-find.py --top 15 "整理笔记库"
|
||
skill-find.py --list-cats # 看技能分类
|
||
"""
|
||
import os, re, sys, glob
|
||
|
||
SKILLS = os.path.expanduser("~/.hermes/skills")
|
||
QSTOP = set("的了和与及怎么如何要要不要是否有我你他她它把被给对为在从到就都也很呢吗个这那注意事项问题办法方法请问帮我一下做个")
|
||
|
||
|
||
def load_skills():
|
||
out = []
|
||
for p in glob.glob(os.path.join(SKILLS, "**", "SKILL.md"), recursive=True):
|
||
try:
|
||
t = open(p, encoding="utf-8", errors="ignore").read()
|
||
except Exception:
|
||
continue
|
||
fm = t.split("---", 2)
|
||
fm = fm[1] if len(fm) > 2 else t[:2000]
|
||
def get(key):
|
||
m = re.search(rf'^{key}:\s*(.+?)(?=\n[a-z_]+:|\Z)', fm, re.M | re.S)
|
||
return m.group(1).strip().strip('"').replace("\n", " ") if m else ""
|
||
name = get("name") or os.path.basename(os.path.dirname(p))
|
||
desc = re.sub(r"\s+", " ", get("description"))
|
||
trig = get("triggers") + " " + get("trigger")
|
||
cat = p.replace(SKILLS + "/", "").rsplit("/", 2)[0] if "/" in p.replace(SKILLS + "/", "") else ""
|
||
out.append({"name": name, "desc": desc, "trig": trig, "path": p, "cat": cat})
|
||
return out
|
||
|
||
|
||
def tokens(q):
|
||
q = q.strip()
|
||
parts = [p for p in re.split(r"[\s,,、/|]+", q) if p]
|
||
toks = set(parts)
|
||
# 中文长词再切 2-3 字滑窗,提高召回("密钥泄露了怎么清历史" → 密钥/泄露/清历史…)
|
||
for p in parts:
|
||
cjk = re.findall(r"[\u4e00-\u9fff]+", p)
|
||
for c in cjk:
|
||
for n in (2, 3, 4):
|
||
for i in range(0, max(1, len(c) - n + 1)):
|
||
toks.add(c[i:i + n])
|
||
return {t for t in toks if t and t not in QSTOP and len(t) >= 2}
|
||
|
||
|
||
def main():
|
||
argv = sys.argv[1:]
|
||
top = 8
|
||
if "--top" in argv:
|
||
i = argv.index("--top")
|
||
top = int(argv[i + 1])
|
||
del argv[i:i + 2]
|
||
args = [a for a in argv if not a.startswith("--")]
|
||
sk = load_skills()
|
||
if "--list-cats" in sys.argv:
|
||
cats = {}
|
||
for s in sk:
|
||
cats[s["cat"]] = cats.get(s["cat"], 0) + 1
|
||
for c, n in sorted(cats.items(), key=lambda x: -x[1]):
|
||
print(f" {c:44s} {n}")
|
||
return
|
||
if not args:
|
||
print(__doc__); return
|
||
q = " ".join(args)
|
||
toks = tokens(q)
|
||
scored = []
|
||
for s in sk:
|
||
sc, why = 0, []
|
||
blob = (s["name"] + " " + s["desc"] + " " + s["trig"]).lower()
|
||
for t in toks:
|
||
tl = t.lower()
|
||
if tl in s["name"].lower():
|
||
sc += 10; why.append(f"name:{t}")
|
||
if tl in s["trig"].lower():
|
||
sc += 6; why.append(f"trigger:{t}")
|
||
if tl in s["desc"].lower():
|
||
sc += 4
|
||
if q.lower() in blob:
|
||
sc += 8; why.append("整句命中")
|
||
if sc:
|
||
scored.append((sc, s, why))
|
||
scored.sort(key=lambda x: -x[0])
|
||
if not scored:
|
||
print(f"❌ 技能库无匹配:「{q}」")
|
||
print(" → 下一步:web_search 找现成技能库 → 装(skill-library-porting);都没有 → 按方法论自建(hermes-self-improvement)")
|
||
return
|
||
print(f"🔍 「{q}」→ {len(scored)} 个候选,取前 {min(top,len(scored))}:\n")
|
||
for sc, s, why in scored[:top]:
|
||
flag = "★" if sc >= 12 else " "
|
||
print(f"{flag} [{sc:>3}] {s['name']} ({s['cat']})")
|
||
print(f" {s['desc'][:110]}")
|
||
if why:
|
||
print(f" 命中: {', '.join(sorted(set(why))[:5])}")
|
||
print(f"\n▶ 加载:skill_view(name='{scored[0][1]['name']}') (可组合多个)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|