L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Données et recherche · SKILL.md

skill-maintenance

skill-maintenance

维护技能库整洁,系统性整理、归类、查重、清理废弃技能(含项目源码误入 skills/ 目录的情况),**以及在 install 前对 skill 做安全审计(调用 skillspector)**。**新能力(v2.2)**:对指定 skill 进行合规/规范检查(YAML frontmatter / description 规范 / 多余文件 / 目录结构),并与 viceroy-skills 云端同名 skill 比对,**自动三向 merge**,保证本地和云端都是最新文件。**新能力(v2.7)**:分析技能使用情况——直接查询 SQLite 数据库 session 历史,区分「mtime 活跃」和「实际被调用过」。触发词:「检查 skill 规范」「合规检查」「跟云端比对」「merge skill」「…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: skill-maintenance description: "维护技能库整洁,系统性整理、归类、查重、清理废弃技能(含项目源码误入 skills/ 目录的情况),以及在 install 前对 skill 做安全审计(调用 skillspector)。新能力(v2.2):对指定 skill 进行合规/规范检查(YAML frontmatter / description 规范 / 多余文件 / 目录结构),并与 viceroy-skills 云端同名 skill 比对,自动三向 merge,保证本地和云端都是最新文件。新能力(v2.7):分析技能使用情况——直接查询 SQLite 数据库 session 历史,区分「mtime 活跃」和「实际被调用过」。触发词:「检查 skill 规范」「合规检查」「跟云端比对」「merge skill」「同步 skill 到云端」「检查并更新 skill」「哪些 skill 没用过」「清理未使用技能」「检查技能使用情况」。"

Skill Maintenance

按 viceroy-skills 仓库的 6 分类标准维护本地技能库,识别重复/低频/废弃技能,保持 6 大分类整洁有序。配套 skill-github-sync 形成"本地整理 → 推到 GitHub"闭环。

v2.1(2026-06-21 升级):新增第 7 节「安装前安全审计」—— 在 install 新 skill 之前,自动调用 skillspector 做 64 pattern / 16 分类的静态扫描 + 风险分评估,避免装入含 prompt injection / data exfiltration / supply chain attack 的恶意 skill。

v2.2(2026-06-28 升级):新增第 8 节「合规检查 + 云端 merge」—— 对指定 skill 进行规范审计(YAML / description / 文件结构),与 viceroy-skills 云端同名 skill 三向 merge,保证本地和 GitHub 两端文件同步最新。

6 个固定分类(viceroy-skills 标准,2026-06 同步)

英文分类中文用途路径前缀
aiAI模型LLM/agent runtime~/.openclaw/skills/ai/
assistant助理通用工具/笔记/存储/陪伴~/.openclaw/skills/assistant/
creative创意写作/排版/设计/视频~/.openclaw/skills/creative/
developer开发工程化/调试/部署/skill 自身工具~/.openclaw/skills/developer/
operations运营数据采集/SEO/GitHub 项目雷达~/.openclaw/skills/operations/
product产品调研/分析/竞品/标签打分~/.openclaw/skills/product/

铁律:

  • 不新增分类(与 skill-github-sync 一致)
  • 同名 skill 只能存在一份
  • 「工作区 skills/」是试装区,不是常驻位 —— 试跑通后必须推系统级

Skill 来源优先级(OpenClaw 加载顺序)

OpenClaw 加载 skill 时按以下顺序,高优先级覆盖低优先级同名 skill:

#来源路径可见性
1Workspace skills<workspace>/skills仅该 agent
2Project agent skills<workspace>/.agents/skills仅该工作区 agent
3Personal agent skills~/.agents/skills全机器所有 agent
4Managed/local skills~/.openclaw/skills全机器所有 agent
5Bundled skills安装自带全机器所有 agent
6Extra dirsskills.load.extraDirs全机器所有 agent

维护原则:

  • 正式 skill 全部进系统级(~/.openclaw/skills/<cat>/),让所有 agent 共享
  • 工作区 skills/ 只用于临时试装(跑通再推系统级)
  • 优先级越高的位置越"私有",越低的越"共享" —— 共享价值高的放系统级

维护流程

1. 每周/每两周检查清单

按顺序执行:

  1. 盘点所有 skill:

    # 系统级(含嵌套分类目录)
    find ~/.openclaw/skills -name "SKILL.md" -type f
    # 工作区(试装区)
    find ~/.openclaw/workspace/skills -name "SKILL.md" -type f 2>/dev/null
    # 内置
    find ~/.hermes/skills -name "SKILL.md" -type f 2>/dev/null
    
  2. 识别非 skill 项(关键!实战中最常踩坑):

    • 无 SKILL.md 的目录 —— 通常是项目源码误入(如 Rust 源码带 Cargo.toml/.cargo/crates)→ 移出 skills/,建议去 ~/.openclaw/workspace/projects/<name>/
    • 只有 README.md 无 SKILL.md —— 同上
    • 目录前缀误建(如 skills/developer/ 是 skill,但 5-27 误建时把 skill-maintenance 嵌进去形成空顶层)→ 整目录审计
  3. 识别重复 / 旧版:

    • 同分类下目录名相同(如 ~/.openclaw/skills/developer/skill-maintenance/ 和 ~/.openclaw/skills/skill-maintenance/)→ 留新删旧,对比 mtime 或仓库引用
    • 描述相似度 >70% 的两个 skill → 标记合并或删除
    • 指向旧仓库的 SKILL.md(如 jacardl/skillshub → jacardl/viceroy-skills)→ 必删
    • Triplicate skills(三处重复):同 skill 在顶层 + 分类 + workspace 三处同时存在 → 执行 1.x 节检测脚本,保留分类版,删顶层和 workspace 版
  4. 归类新 skill:对照 6 分类表,关键词匹配:

    • 含「github」「trending」「seo」「data collection」→ operations/
    • 含「chat」「情感」「陪伴」「笔记」「flowus」「菜谱」→ assistant/
    • 含「标签」「kano」「调研」「竞品」→ product/
    • 含「writing」「publish」「article」「github 项目」→ creative/
    • 含「skill」「debug」「code」「github-pr」→ developer/
    • 含「llm」「model」「claude-api」「vllm」→ ai/
  5. 检查空目录 stub:

    find ~/.openclaw/skills -maxdepth 2 -name "DESCRIPTION.md" | while read f; do
      dir=$(dirname "$f")
      [ ! -f "$dir/SKILL.md" ] && echo "STUB: $dir"
    done
    
  6. 验证关键引用:例如 zhiligithub 引用 zhili-publish/scripts/publish_zhili.py,分类嵌套后路径变成 skills/creative/zhili-publish/scripts/ —— 跑一次 publish_zhili.py --help 验证

2. 删除/清理标准

满足以下任一条件建议清理(删除前必须向用户确认):

条件处理方式
与现有 skill 功能完全重复(描述重叠 >70%)删旧留新(或合并)
长期未使用(边缘领域如音乐/游戏/家居控制)删
依赖的外部服务已失效删
描述过时且无法修复删
旧版(指向已废弃仓库)删
嵌套在错误位置的空顶层目录删整目录
项目源码误入 skills/移到 ~/.openclaw/workspace/projects/ 或 ~/.openclaw/workspace/<name>/

3. 分类调整规则

  • 新 skill 归入现有 6 分类,不新建分类
  • 公众号相关(zhili*)按功能分散:
    • zhili-publish → creative/(内容生产)
    • zhiligithub → operations/(GitHub 项目雷达,类比 github-daily-trending)
    • 未来若加 zhilicomments → creative/(短评)
  • 工作流类(neat-freak、flowus-crud)→ assistant/
  • skill 自身管理类(skill-creator、skill-maintenance、skill-github-sync、skillspector)→ developer/
  • 灰区归属判断用关键词匹配,匹配不上时主动问用户(不要硬塞)

4. 删除/移动流程(强制备份)

执行前必须:

  1. 列出待处理项及理由
  2. 等用户确认
  3. 先备份到 /tmp/skill-mv-backup/ 或 ~/.openclaw/workspace/.backup-pre-skill-cleanup/
  4. 执行 mv(不要用 rm —— mv 至少可逆;SOUL.md 红线:"trash > rm")
  5. 验证:跑一遍相关 skill 的 --help 或最简单命令
  6. 更新引用(SKILL.md 内的路径、MEMORY.md、脚本里的硬编码路径)

5. 验证步骤

维护完成后输出:

  • 当前 skill 总数(系统级 + 工作区 + 内置分别统计)
  • 各分类 skill 数量 + 与 viceroy-skills 仓库的对照(diff)
  • 本次新增/删除/调整列表(含备份位置)
  • 遗留待处理项

1.x Triplicate Skill Detection(v2.6 新增)

问题:同一 skill 在 3 个位置同时存在(内容完全相同)—— openclaw/skills/{name}/、openclaw/workspace/skills/{name}/、openclaw/skills/{cat}/{name}/。常见于法律类 skill 批量安装后。

检测脚本:

#!/usr/bin/env python3
"""检测 triplicate skills(三处重复的 skill)"""
import os
from pathlib import Path

def scan_all(base):
    results = {}
    for root, dirs, files in os.walk(base):
        if 'SKILL.md' not in files: continue
        rel = Path(root).relative_to(base)
        results[str(rel)] = str(root)
    return results

oc_sys = scan_all(Path.home() / ".openclaw/skills")
oc_ws  = scan_all(Path.home() / ".openclaw/workspace/skills")

cats_map = {}
top_level = []
for k, v in oc_sys.items():
    parts = k.split('/')
    if len(parts) == 1:
        top_level.append(k)
    else:
        cat = parts[0]
        name = '/'.join(parts[1:])
        cats_map.setdefault(cat, {})[name] = k

ws_names = {}
for k, v in oc_ws.items():
    parts = k.split('/')
    name = parts[0]
    ws_names[name] = k

# 检测 triplicate
for cat, skills in cats_map.items():
    if cat == 'legal-skills-chinese':
        for name, cat_path in skills.items():
            top_path = Path.home() / ".openclaw/skills" / name
            ws_path = Path.home() / ".openclaw/workspace/skills" / cat / name
            locations = []
            if top_path.exists(): locations.append("top-level")
            if ws_path.exists(): locations.append("workspace/" + cat)
            if len(locations) >= 2:
                print(f"TRIPLICATE: {name} in {locations}")

清理原则:

  • openclaw/skills/legal-skills-chinese/{name}/ — 保留(已归类)
  • openclaw/skills/{name}/ — 删除(顶层重复)
  • openclaw/workspace/skills/legal-skills-chinese/{name}/ — 删除(试装区过期)

实战案例(2026-07-03):35 个法律技能 triplicate,同时存在于顶层 + legal-skills-chinese 分类 + workspace 三处。清理后系统级从 87 → 51 个。

6. 跟 skill-github-sync 配合

skill-maintenance 不止是本地整理,最终目的是跟 viceroy-skills 仓库同步:

本地整理(skill-maintenance)→ 跟 GitHub 对照 → 推差异(skill-github-sync)
         ↑                                                  |
         └──────────────── 验证一致性 ←─────────────────────┘

联动步骤:

  1. skill-maintenance 整理完本地结构
  2. skill-github-sync 拉取 GitHub 端最新 skill 列表
  3. 对比:哪些本地有但 GitHub 没有(push)、哪些 GitHub 有但本地没有(pull 候选)
  4. 用 skill-github-sync 推本地独有 + 有价值的 skill
  5. 跟仓库分类标准对齐(参考本 skill 第 1 节的 6 分类表)

7. 安装前安全审计(v2.1 新增)

核心原则:装新 skill 之前必须先用 skillspector 扫一遍。skill 装到系统级后就默认 agent 全部信任它 —— 一旦含 prompt injection / data exfiltration / 提权代码,影响范围是全机器所有 agent。

7.1 触发条件

场景用户可能的表达动作
install 前"我要装 X,先扫一下" / "scan https://github.com/xxx/skill"强制先 scan,风险分 > MEDIUM 需用户确认
合规检查 + 云端 merge"检查 skill 规范" / "合规检查 X" / "跟云端比对" / "merge skill" / "同步 skill 到云端" / "检查并更新 skill"第 8 节三向 merge
已装 skill 体检"扫一下 skill-maintenance" / "audit my skills"跑 scan,按风险分排序
批量体检"把 ~/.openclaw/skills/ 全扫一遍"跑下面 7.4 的批量命令
看报告"显示上次扫描的 json 报告"用 --format json 重跑指定 skill

铁律:install 类操作(clone 到 skills/、推送 GitHub、覆盖 SKILL.md)必须在 scan 通过后执行。HIGH/CRITICAL 风险的 skill 不装,MEDIUM 风险的 skill 列 issues 让用户决定。

7.2 工具:skillspector

  • 位置:~/.openclaw/skills/developer/skillspector/(同级)
  • 底层 CLI:/tmp/skillspector-probe/SkillSpector/.venv/bin/skillspector(NVIDIA SkillSpector v2.2.3)
  • 扫描能力:64 pattern × 16 分类(prompt injection / data exfiltration / privilege escalation / supply chain / excessive agency / output handling / system prompt leakage / memory poisoning / tool misuse / rogue agent / trigger abuse / dangerous code via AST / taint tracking / YARA signatures / MCP least privilege / MCP tool poisoning)
  • 输出:风险分 0-100 + severity 标签(LOW/MEDIUM/HIGH/CRITICAL)

7.3 风险分解读

Risk ScoreSeverity含义建议
0-19LOW安全直接装
20-49MEDIUM提示看 issues 列表,人工 review 后再装
50-79HIGH警告必须人工 review 全部 issues 后再装
80-100CRITICAL危险不装,告警用户

⚠️ 误报注意:很多合法 skill 会被标 MEDIUM(典型如 "External Transmission" 因为 skill 文档里有 API URL,或 "File System Enumeration" 因为 skill 需要读 ~/.openclaw/)。真问题要看 HIGH/CRITICAL + Confidence ≥ 70% 的 issue。

7.3.1 skillspector HIGH 误报的标准响应:permissions 字段

skillspector 对本地网关调用和** shell 工具脚本会持续报 HIGH,即使加了 --no-llm 也无法消除。这类不是漏洞,是设计行为。标准响应是在 YAML frontmatter 中声明 permissions 字段**,让 skillspector 知道这是已知的合法能力。

典型误报场景:

Pattern IDPattern Name触发原因是误报的条件
SC2External Transmissioncurl HTTP 请求目标地址是 127.0.0.1 或 localhost(本地网关)
LP3Shell Script Presence存在 .sh 脚本文件脚本只含 curl / base64 / jq 等系统工具,无外部脚本下载
LP1Tool Spawningskill 调用 shell 脚本脚本是静态工具(本地网关调用),非动态恶意代码
E1Excessive File Access访问 ~/.hermes/ 或 ~/.openclaw/skill 自身配置文件,属于正常工作范围

判断流程:

  1. 扫出 HIGH → 看 Pattern ID + Confidence
  2. 若 Confidence < 70% → 直接忽略
  3. 若 Confidence ≥ 70% → 核查目标地址:
    • 127.0.0.1 / localhost → 误报,加 permissions
    • 外部域名(api.openai.com 等)→ 核查是否 skill 自身必需调用
  4. 加 permissions 后重扫仍有残留 HIGH → 标注为已知设计行为,在审计报告中说明,跳过该 skill

permissions 字段格式(加在 YAML frontmatter,与 name/description 同级):

---
name: example-skill
description: "描述(description 值含中文/冒号/括号必须用双引号包裹)"
permissions:
  - http://127.0.0.1:20128/   # 9Router 本地网关调用
  - http://localhost/          # 本地服务调用
  - filesystem:read:~/.openclaw/   # OpenClaw 配置读取
  - filesystem:read:~/.hermes/    # Hermes 配置读取
---

实战案例(9router-image,2026-06-28):

  • skillspector 初始扫描:64/100,4× HIGH(SC2 External Transmission curl 127.0.0.1:20128)
  • 加 permissions: - http://127.0.0.1:20128/ 后重扫:仍有 4× SC2 HIGH
  • 原因:permissions 字段告知安装者该 skill 需要这些权限,但不改变 skillspector 静态扫描结果;静态分析无法理解运行时上下文,必然对本地网关调用报警
  • 结论:permissions 字段的作用是消除后续安装警告,让 skill 作者对已知能力负责;skillspector 扫描本身保持原样
  • 处置:标注为已知设计行为,报告输出 ⚠️ N HIGH (known design: local gateway curl + shell script),skill 可正常安装

注意:description 值含特殊字符时必须用双引号包裹,否则 YAML 解析器会警告:

  • 含中文括号 () → 必须加 "..." 包裹
  • 含冒号 : → 冒号后有空格时无需引号,但冒号紧跟文字时需要
  • 含反斜杠 \ → 在 YAML 双引号字符串中必须写成 \\

7.4 批量体检命令

定期(如每月 / 装完一波新 skill 后)跑一次,扫整个 skills/ 目录:

SS=/tmp/skillspector-probe/SkillSpector/.venv/bin/skillspector

# 批量扫描:所有系统级 skill
for d in ~/.openclaw/skills/*/*/; do
  [ -d "$d" ] || continue
  skill_name=$(basename "$d")
  # 跳过 skillspector 自己(避免循环)
  [ "$skill_name" = "skillspector" ] && continue
  # 跳过软链
  [ -L "$d" ] && continue
  # 只扫有 SKILL.md 的
  [ -f "$d/SKILL.md" ] || continue
  echo "=== $skill_name ==="
  $SS scan "$d" --no-llm --format terminal 2>&1 | grep -E '(Risk Score|Severity|CRITICAL|HIGH)' | head -10
  echo
done

输出排序建议:把扫描结果按风险分倒序排列,先处理 CRITICAL > HIGH > MEDIUM。

7.5 快速命令(一行版)

# 扫描单个 skill(装前必跑)
SS=/tmp/skillspector-probe/SkillSpector/.venv/bin/skillspector
$SS scan <path-or-git-url> --no-llm --format terminal

# JSON 报告(机器可读)
$SS scan <path> --no-llm --format json --output /tmp/scan-report.json

# Markdown 报告(贴飞书/Notion)
$SS scan <path> --no-llm --format markdown --output /tmp/scan-report.md

# SARIF 报告(GitHub Code Scanning 集成)
$SS scan <path> --no-llm --format sarif --output /tmp/scan.sarif

--no-llm 跳过 LLM 语义分析(快、准、免 API key)。有 LLM key 想去掉 --no-llm 改用:

  • SKILLSPECTOR_PROVIDER=openai + OPENAI_API_KEY
  • SKILLSPECTOR_PROVIDER=anthropic + ANTHROPIC_API_KEY
  • SKILLSPECTOR_PROVIDER=nv_inference + NVIDIA_INFERENCE_KEY

8. 合规检查 + 云端三向 merge(新功能 v2.2)

当用户要求对指定 skill 做合规/规范检查并与云端同步时,执行以下流程。

8.1 三向 merge 流程

输入:用户指定 cat/skill(如 operations/radar-daily-report)

⚠️ 凭证文件保护规则(硬性约束)

以下文件在 merge 时永远使用 CLOUD 版本(不覆盖 LOCAL 的云端脱敏版):

  • 含 APPID、APPSECRET、access_token、API_KEY 字面量的 .md / .py / .json / .yaml / .txt 文件
  • 路径含 config、credentials、secret、token 的文件

判断标准:LOCAL 有真实凭证(32+ 字符的 hex/base64,或 wx 开头的 16 位十六进制 appid 搭配明文 secret)vs CLOUD 只有 ***REDACTED*** 占位符。

操作:在 merge 前对这些文件单独做 git checkout HEAD -- <file>,保留云端脱敏版,不做任何合并。

Step 0:凭证预检(必须先做)

import re
from pathlib import Path

CRED_PATTERNS = [
    r"['\"][0-9a-f]{32,}['\"]",     # 32+ hex (APPSECRET)
    r"['\"][A-Za-z0-9+/]{40,}={0,2}['\"]",  # 40+ base64
    r"wx[0-9a-f]{16}",              # WeChat APPID
]

def has_real_credential(path: Path) -> bool:
    """检查文件是否含真实凭证(非占位符)"""
    content = path.read_text()
    redacted = "***REDACTED***" in content or "REDACTED" in content
    for pat in CRED_PATTERNS:
        hits = re.findall(pat, content, re.I)
        for line in content.split('\n'):
            for hit in hits:
                if hit in line and not line.strip().startswith('#'):
                    return True  # 真实凭证
    return False

def scan_for_real_creds(local_dir: Path) -> list[Path]:
    """扫描 skill 目录下所有含真实凭证的文件"""
    flagged = []
    for f in local_dir.rglob("*"):
        if f.is_file() and f.suffix in ['.md', '.py', '.json', '.yaml', '.txt']:
            try:
                if has_real_credential(f):
                    flagged.append(f)
            except:
                pass
    return flagged

预检结果解读:

场景处置
LOCAL 无凭证文件正常 merge
LOCAL 有凭证,CLOUD 有脱敏版保留 CLOUD 脱敏版,不 merge 该文件
LOCAL 有凭证,CLOUD 无此文件凭证文件不 merge(用户需自行处理本地凭证)
CLOUD 有凭证(不应该)警告,阻断 merge,提示用户

Step 1:收集三方文件

import urllib.request, subprocess, json, base64
from pathlib import Path

TOKEN = open("~/.hermes/keys/github_token.txt").read().strip()
REPO = "/tmp/viceroy-skills_sync"

# LOCAL
local_text = Path("~/.openclaw/skills/{cat}/{skill}/SKILL.md").expanduser().read_text()

# BASE(git show HEAD:{path},新增文件则空)
base_text = subprocess.run(
    ["git", "show", "HEAD:{CLOUD_PATH}"],
    capture_output=True, text=True, cwd=REPO
).stdout

# CLOUD(GitHub HEAD 最新)
url = "https://api.github.com/repos/jacardl/viceroy-skills/contents/{CLOUD_PATH}"
req = urllib.request.Request(url, headers={
    "Accept": "application/vnd.github.v3+json",
    "Authorization": "token {TOKEN}"
})
resp = json.loads(urllib.request.urlopen(req).read())
cloud_text = base64.b64decode(resp["content"]).decode("utf-8", errors="replace")

Step 2:合规检查 LOCAL + CLOUD

检查项标准不合格处置
YAML frontmatter以 ---\n...\n--- 包裹且 yaml.safe_load 可解析报错,用户确认后跳过
description 值含中文/冒号/(/) 等字符必须双引号包裹自动加引号
--- 文档分隔符数量恰好 2 个(frontmatter 开始 + 结束),body 内无多余 ---用 grep -n '^---#x27; 定位多余分隔符并删除
目录结构skills/{cat}/{skill}/ 格式报错
本地 vs 云端字节级比对read_bytes() vs base64.b64decode(cloud_content) 相等相等则跳过 merge(Pitfall G)

⚠️ 字节 vs 字符陷阱(Pitfall G):stat().st_size 是字节数,len(read_text()) 是 Unicode 字符数。当文件含中文/emoji 时二者不相等,但可能实际内容相同。判断"本地 vs 云端是否相同"必须用字节级比对,不能用字符数。 | 多余文件 | 顶层不含 .git/ node_modules/ __pycache__/ | 警告,用户确认后忽略 | | 目录结构 | skills/{cat}/{skill}/ 格式 | 报错 |

Step 3:三向合并(git merge-file)

import tempfile, subprocess
from pathlib import Path

def three_way_merge(local_path, cloud_text, base_text):
    local_path = Path(local_path)
    if cloud_text.strip() == local_path.read_text().strip():
        return "identical", cloud_text

    with tempfile.TemporaryDirectory() as td:
        td = Path(td)
        (td/"BASE").write_text(base_text)
        (td/"LOCAL").write_text(local_path.read_text())
        (td/"CLOUD").write_text(cloud_text)

        cp = subprocess.run(
            ["git", "merge-file", str(td/"LOCAL"), str(td/"BASE"), str(td/"CLOUD")],
            capture_output=True, text=True, cwd=str(td)
        )
        merged = (td/"LOCAL").read_text()
        has_conflict = any(m in merged for m in ["<<<<<<<", ">>>>>>>", "======="])

        if not has_conflict:
            local_path.write_text(merged)
            status = "cloud_updated" if merged.strip() == cloud_text.strip() else "merged"
            return status, merged
        else:
            lines = [l for l in merged.splitlines()
                     if not l.startswith(("<<<<<<<", ">>>>>>>", "======="))]
            clean = "
".join(lines)
            local_path.write_text(clean)
            return "conflict_resolved", clean

Step 4:推送 GitHub

def push_github(local_path, cloud_path, commit_msg, token):
    local_path = Path(local_path)
    content_b64 = base64.b64encode(local_path.read_text().encode()).decode()
    url = "https://api.github.com/repos/jacardl/viceroy-skills/contents/{cloud_path}"
    req = urllib.request.Request(url, headers={
        "Authorization": "token {token}",
        "Accept": "application/vnd.github.v3+json"
    })
    current = json.loads(urllib.request.urlopen(req).read())
    data = json.dumps({
        "message": commit_msg,
        "content": content_b64,
        "sha": current["sha"]
    }).encode()
    blob_req = urllib.request.Request(url, data=data, headers={
        "Authorization": "token {token}",
        "Content-Type": "application/json"
    }, method="PUT")
    result = json.loads(urllib.request.urlopen(blob_req).read())
    return result["commit"]["sha"]

Step 5:输出报告

skill: {cat}/{skill}

合规检查:
  YAML frontmatter:  OK / FAIL
  description 引号: OK / 已修复
  文件结构:         OK / WARN

三向 merge 结果:
  local vs cloud: 对齐 / LOCAL新 / CLOUD新 / 冲突
  merge status:   {status}

GitHub 推送:
  commit: {sha}
  URL: https://github.com/jacardl/viceroy-skills/commit/{sha}

8.2 快速调用脚本

将下方保存为 /tmp/skill_merge.py,用法:python3 /tmp/skill_merge.py <cat> <skill>

#!/usr/bin/env python3
import sys, urllib.request, subprocess, tempfile, json, base64
from pathlib import Path
import yaml

TOKEN = open("/Users/apple/.hermes/keys/github_token.txt").read().strip()
REPO  = "/tmp/viceroy-skills_sync"

def get_cloud(path):
    url = "https://api.github.com/repos/jacardl/viceroy-skills/contents/" + path
    req = urllib.request.Request(url, headers={"Authorization": "token " + TOKEN, "Accept": "application/vnd.github.v3+json"})
    r = json.loads(urllib.request.urlopen(req).read())
    return base64.b64decode(r["content"]).decode(), r["sha"]

def merge(local_p, cloud_text, base_text):
    lp = Path(local_p)
    if cloud_text.strip() == lp.read_text().strip():
        return "identical", cloud_text
    with tempfile.TemporaryDirectory() as td:
        td = Path(td)
        (td/"B").write_text(base_text)
        (td/"L").write_text(lp.read_text())
        (td/"C").write_text(cloud_text)
        subprocess.run(["git", "merge-file", str(td/"L"), str(td/"B"), str(td/"C")],
                       capture_output=True, text=True, cwd=str(td))
        merged = (td/"L").read_text()
        if not any(m in merged for m in ["<<<<<<<",">>>>>>>","======="]):
            lp.write_text(merged)
            return ("merged", merged)
        else:
            lines = [l for l in merged.splitlines()
                     if not l.startswith(("<<<<<<<",">>>>>>>","======="))]
            lp.write_text("
".join(lines))
            return ("conflict_resolved", "
".join(lines))

def push(local_p, cloud_path, msg):
    b64 = base64.b64encode(Path(local_p).read_text().encode()).decode()
    url = "https://api.github.com/repos/jacardl/viceroy-skills/contents/" + cloud_path
    req = urllib.request.Request(url, headers={"Authorization": "token " + TOKEN, "Accept": "application/vnd.github.v3+json"})
    cur = json.loads(urllib.request.urlopen(req).read())
    data = json.dumps({"message": msg, "content": b64, "sha": cur["sha"]}).encode()
    r = urllib.request.Request(url, data=data, headers={"Authorization": "token " + TOKEN, "Content-Type": "application/json"}, method="PUT")
    return json.loads(urllib.request.urlopen(r).read())["commit"]["sha"]

def check_compliance(text, label):
    issues = []
    if not text.startswith("---
"):
        issues.append(label + ": missing YAML frontmatter")
    else:
        try:
            end = text.index("
---
")
            yaml.safe_load("---
" + text[4:end] + "
---")
        except Exception as e:
            issues.append(label + " YAML: " + str(e))
    for line in text.splitlines():
        ls = line.strip()
        if ls.startswith("description:"):
            val = ls.split(":", 1)[1].strip()
            if any(c in val for c in ["(", ")", "《", "》"]) and not val.startswith('"'):
                issues.append(label + " desc needs quotes: " + val[:60])
    return issues

if __name__ == "__main__":
    cat, skill = sys.argv[1], sys.argv[2]
    local_p = str(Path.home() / (".openclaw/skills/" + cat + "/" + skill + "/SKILL.md"))
    cloud_path = "skills/" + cat + "/" + skill + "/SKILL.md"
    local_text = Path(local_p).read_text()
    cloud_text, _ = get_cloud(cloud_path)
    base_text = subprocess.run(["git", "show", "HEAD:" + cloud_path],
                               capture_output=True, text=True, cwd=REPO).stdout
    for t, lbl in [(local_text, "LOCAL"), (cloud_text, "CLOUD")]:
        issues = check_compliance(t, lbl)
        for iss in issues:
            print("  WARN: " + iss)
        if not issues:
            print("  " + lbl + ": compliance OK")
    status, _ = merge(local_p, cloud_text, base_text)
    print("merge: " + status)
    if status != "identical":
        sha = push(local_p, cloud_path, "merge: " + cat + "/" + skill + " (" + status + ")")
        print("GitHub commit: " + sha[:7])
        print("URL: https://github.com/jacardl/viceroy-skills/commit/" + sha)
    else:
        print("No changes needed (local == cloud)")

7.6 跟其他 skill 联动

  • skillspector:本节就是 skillspector 在 skill-maintenance 下的"安装前 / 批量体检"工作流入口
  • skill-github-sync:sync 前必须先跑本节 7.4 体检(确保本地要 push 的 skill 全部 LOW/MEDIUM)
  • skill-creator:新 skill 写完后,发布前自己跑一次 7.5 自检

GitHub API 推送权限要求(陷阱)

向 jacardl/viceroy-skills 推送 skill 文件使用 GitHub REST API(blobs/trees/commits/refs)。必须使用有写入权限的 Token:

  • Classic PAT(推荐):https://github.com/settings/tokens/new,勾选 repo scope
  • Fine-grained PAT:需单独设置:
    • Repository access → 选择 jacardl/viceroy-skills
    • Repository permissions → Contents 设置为 Read and write(默认只有 Read)

验证方法(创建 blob 不报 403 即代表有写入权限):

curl -s -X POST -H "Authorization: token <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"content":"test","encoding":"utf-8"}' \
  "https://api.github.com/repos/jacardl/viceroy-skills/git/blobs"
# 成功返回 {"sha":"..."},403 = 无写入权限

Token 凭证保存路径:~/.hermes/keys/github_token.txt(兼容多环境:~/.openclaw/openclaw.json 也可)

GitHub 推送路径规范

所有 skill 推送到 jacardl/viceroy-skills 时,路径格式:

skills/{category}/{skill-name}/SKILL.md

示例:

skills/operations/aihot/SKILL.md
skills/product/tag-scoring/SKILL.md
skills/developer/skill-github-sync/SKILL.md
skills/ai/9router/SKILL.md

推送完成后验证:

curl -s -H "Authorization: token $TOKEN" \
  "https://api.github.com/repos/jacardl/viceroy-skills/git/trees/main?recursive=1" | \
  python3 -c "import sys,json; [print(f['path']) for f in json.load(sys.stdin)['tree'] if f['path'].endswith('SKILL.md')]"

快速命令

# 统计 SKILL.md 文件数(系统级)
find ~/.openclaw/skills -name "SKILL.md" -type f | wc -l

# 列出所有 skill 名称(去重)
find ~/.openclaw/skills -name "SKILL.md" -type f | xargs -I{} dirname {} | xargs -I{} basename {} | sort -u

# 按分类统计
for cat in ai assistant creative developer operations product; do
  count=$(find ~/.openclaw/skills/$cat -name "SKILL.md" -type f 2>/dev/null | wc -l)
  echo "$cat: $count"
done

# 清理空目录 stub
find ~/.openclaw/skills -maxdepth 2 -name "DESCRIPTION.md" | while read f; do
  dir=$(dirname "$f")
  [ ! -f "$dir/SKILL.md" ] && echo "STUB: $dir"
done

# 跟 GitHub 仓库对照(需要 token)
python3 << 'EOF'
import urllib.request, json, os
token = open(os.path.expanduser("~/.hermes/keys/github_token.txt")).read().strip()
github = {}
for cat in ['ai','assistant','creative','developer','operations','product']:
    url = f"https://api.github.com/repos/jacardl/viceroy-skills/contents/skills/{cat}"
    req = urllib.request.Request(url, headers={'Accept':'application/vnd.github.v3+json', 'Authorization':f'token {token}'})
    r = json.loads(urllib.request.urlopen(req, timeout=15).read())
    github.update({item['name']: cat for item in r if item.get('type')=='dir'})

import os
local = set()
for root, _, files in os.walk(os.path.expanduser("~/.openclaw/skills")):
    for f in files:
        if f == "SKILL.md":
            local.add(os.path.basename(root))

print("本地独有(候选 push):", sorted(local - set(github.keys())))
print("GitHub 独有(候选 pull):", sorted(set(github.keys()) - local))
EOF

9. 跨 skill 引用对齐检查(v2.4 新增)

安装新 skill 后,如果该 skill 在工作流中引用了其他 skill, 必须验证引用是否对齐。方法:grep 双侧 SKILL.md 提取 callee 的实际要求,对照 caller 的引用描述,以严重度表格输出。 详见 references/cross-skill-citation-check.md。

10. 技能使用情况分析(v2.7 新增)

⚠️ 文件修改时间(mtime)是误导性的——skillhub 批量安装/更新时, 大量 skill 的 mtime 会被同步更新到同一日期(如 2026-05-18), 造成"全部同时更新"的假象。判断"哪些 skill 从未被使用"必须用 session 历史查询。

10.1 正确方法:直接查询 SQLite 数据库

Hermes 将所有 session 消息存储在 ~/.hermes/state.db,其中 messages.tool_calls 字段包含每次工具调用的 JSON。 查找 skill_view 调用:

import sqlite3
import json
import re
from collections import Counter

conn = sqlite3.connect(os.path.expanduser('~/.hermes/state.db'))
cursor = conn.cursor()

cursor.execute("""
    SELECT tool_calls FROM messages WHERE tool_calls LIKE '%skill_view%'
""")

used_skills = Counter()
for row in cursor.fetchall():
    if not row[0]:
        continue
    try:
        tool_calls = json.loads(row[0])
        for tc in tool_calls:
            if tc.get('function', {}).get('name') == 'skill_view':
                args = json.loads(tc['function']['arguments'])
                name = args.get('name', '')
                if name:
                    used_skills[name] += 1
    except (json.JSONDecodeError, KeyError):
        # 备用:正则提取
        for m in re.finditer(r'"name"\s*:\s*"([^"]+)"', row[0]):
            used_skills[m.group(1)] += 1

for skill, count in used_skills.most_common():
    print(f"{count:3d}x  {skill}")

10.2 获取本地所有 skill

find ~/.hermes/skills -name "SKILL.md" | while read f; do
  skill=$(dirname "$f" | xargs basename)
  echo "$skill"
done | sort -u

10.3 对比分析脚本

#!/usr/bin/env python3
"""对比:本地 skill vs session 历史中实际调用过的 skill"""
import sqlite3, json, re, os, subprocess
from collections import Counter

# 1. 获取所有本地 skill
result = subprocess.run(
    ['find', os.path.expanduser('~/.hermes/skills'), '-name', 'SKILL.md'],
    capture_output=True, text=True
)
local_skills = set()
for line in result.stdout.strip().split('\n'):
    if line:
        skill_path = line.replace(os.path.expanduser('~/.hermes/skills/'), '').replace('/SKILL.md', '')
        local_skills.add(skill_path)

# 2. 查询 session 历史
conn = sqlite3.connect(os.path.expanduser('~/.hermes/state.db'))
cursor = conn.cursor()
cursor.execute("SELECT tool_calls FROM messages WHERE tool_calls LIKE '%skill_view%'")

used_skills = set()
for row in cursor.fetchall():
    if not row[0]:
        continue
    try:
        for tc in json.loads(row[0]):
            if tc.get('function', {}).get('name') == 'skill_view':
                args = json.loads(tc['function']['arguments'])
                name = args.get('name', '')
                if name:
                    used_skills.add(name)
    except:
        pass

# 3. 找出从未使用的 skill
never_used = local_skills - used_skills

print(f"本地 skill 总数: {len(local_skills)}")
print(f"历史调用过: {len(used_skills)}")
print(f"从未使用: {len(never_used)} ({len(never_used)/len(local_skills)*100:.0f}%)")
print("\n按从未使用分组:")
from collections import defaultdict
by_cat = defaultdict(list)
for s in sorted(never_used):
    cat = s.split('/')[0] if '/' in s else 'root'
    by_cat[cat].append(s)
for cat in sorted(by_cat):
    print(f"  {cat}: {len(by_cat[cat])} 个")

10.4 mtime 分析(仅供参考)

如果只想快速看哪些 skill 最近有活动(不能证明实际使用):

# 最近 30 天修改过的 SKILL.md
find ~/.hermes/skills -name "SKILL.md" -mtime -30 | while read f; do
  echo "$(stat -f %Sm -t %Y-%m-%d "$f") $(dirname "$f" | xargs basename)"
done | sort -r

结论:mtime 只说明"这个文件最近被touch过",不代表用户实际调用过该 skill。 区分"安装后从未被调用"和"被调用但文件没更新"的唯一方法是查 session 历史。

10.5 清理决策框架

信号含义建议
session 历史从未调用 + mtime 30天前真正的废弃 skill可安全删除
session 历史从未调用 + mtime 最近30天安装后未激活询问用户是否保留
session 历史有调用 + mtime 30天前可能在用但文件久未更新检查是否需要更新
.archive/ 或 .curator_backups/ 目录备份文件确认后可删除

10.6 完整分析脚本(2026-07-04 实测)

#!/usr/bin/env python3
"""
完整的技能使用情况分析脚本
从 SQLite session 历史中提取真实使用的技能,与本地技能对比
"""
import sqlite3, json, os, subprocess
from collections import defaultdict

SKILLS_DIR = os.path.expanduser('~/.hermes/skills')
DB_PATH = os.path.expanduser('~/.hermes/state.db')

# ── 1. 查询 session 历史中的 skill_view 调用 ──────────────────────────
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT tool_calls FROM messages WHERE tool_calls LIKE '%skill_view%'")

raw_used = []
for row in cursor.fetchall():
    if not row[0]:
        continue
    try:
        for tc in json.loads(row[0]):
            if tc.get('function', {}).get('name') == 'skill_view':
                args = json.loads(tc['function']['arguments'])
                name = args.get('name', '')
                if name:
                    raw_used.append(name)
    except:
        pass

# ── 2. 别名标准化映射表 ─────────────────────────────────────────────
ALIASES = {
    # 公众号系列大小写/路径别名
    'zhiliComments': 'zhilicomments',
    'social-media/.agents/skills/zhiliComments': 'zhilicomments',
    'zhiliGitHub': 'zhiligithub',
    'social-media/.agents/skills/zhiliGitHub': 'zhiligithub',
    'zhilicomments-publish': 'zhilicomments',
    'social-media:.agents/skills:zhilicomments': 'zhilicomments',
    'social-media/.agents/skills/zhili-publish': 'zhili-publish',
    'social-media/.agents/skills/zhiligithub': 'zhiligithub',
    # skill-github-sync 系列别名
    'skillgithub-sync': 'skill-github-sync',
    'developer/skill-github-sync': 'skill-github-sync',
    # geo 系列别名
    'geo-content-strategy': 'geo-keyword-research',
    'content-strategy/geo-keyword-research': 'geo-keyword-research',
    # hv-analysis 别名
    'research/hv-analysis': 'hv-analysis',
}

# 标准化:去重后的已使用技能集合
normalized_used = set()
for s in raw_used:
    normalized_used.add(ALIASES.get(s, s))
    # 顶级目录名也算(如 zhililong, renwei-writing)
    if '/' not in s:
        normalized_used.add(s)

print(f"Session 历史中调用过的 skill_view: {len(set(raw_used))} 种写法")
print(f"标准化后: {len(normalized_used)} 个")

# ── 3. 获取所有本地技能 ─────────────────────────────────────────────
result = subprocess.run(
    ['find', SKILLS_DIR, '-name', 'SKILL.md', '-type', 'f'],
    capture_output=True, text=True
)
all_skills = []
for line in result.stdout.strip().split('\n'):
    if line:
        path = line.replace(SKILLS_DIR + '/', '').replace('/SKILL.md', '')
        all_skills.append(path)

# ── 4. 判断技能是否被使用(关键:只看最后一级目录名) ─────────────
def is_used(skill_path):
    name = skill_path.split('/')[-1]
    if skill_path in normalized_used or name in normalized_used:
        return True
    # 别名后缀匹配
    for alias in ALIASES:
        if skill_path.endswith(alias):
            return True
    return False

# ── 5. 分类统计 ────────────────────────────────────────────────────
unused = [s for s in all_skills if not is_used(s)]
by_cat = defaultdict(list)
for s in sorted(unused):
    cat = s.split('/')[0]
    sub = '/'.join(s.split('/')[1:]) if '/' in s else s
    by_cat[cat].append((sub, s))

print(f"\n本地技能总数: {len(all_skills)}")
print(f"从未使用: {len(unused)} ({(len(unused)/len(all_skills)*100):.1f}%)")
print(f"正在使用: {len(all_skills) - len(unused)}")

# ── 6. 按目录输出未使用技能 ───────────────────────────────────────
print("\n" + "="*60)
print("从未使用的技能(按目录):")
print("="*60)

for cat in sorted(by_cat):
    items = by_cat[cat]
    result = subprocess.run(
        ['find', f'{SKILLS_DIR}/{cat}', '-name', 'SKILL.md'],
        capture_output=True, text=True
    )
    total = len([l for l in result.stdout.strip().split('\n') if l])
    unused_count = len(items)
    
    if unused_count == total:
        print(f"\n🗑️ {cat}/ (全部 {unused_count} 个未使用)")
    else:
        print(f"\n⚠️  {cat}/ ({unused_count}/{total} 个未使用)")
    
    for sub, full in items[:5]:
        print(f"   └── {sub}")
    if len(items) > 5:
        print(f"   └── ... 还有 {len(items)-5} 个")

# ── 7. 输出可安全删除的顶级目录 ────────────────────────────────────
print("\n" + "="*60)
print("可安全删除的顶级目录:")
print("="*60)
safe_dirs = []
for cat in sorted(by_cat):
    items = by_cat[cat]
    result = subprocess.run(
        ['find', f'{SKILLS_DIR}/{cat}', '-name', 'SKILL.md'],
        capture_output=True, text=True
    )
    total = len([l for l in result.stdout.strip().split('\n') if l])
    if len(items) == total:
        safe_dirs.append(cat)
        print(f"  rm -rf {cat}/")

if safe_dirs:
    print(f"\n批量命令: rm -rf {' '.join(safe_dirs)}")

10.7 批量清理执行流程(2026-07-04 实测)

Phase 1:分析确认

# 1. 跑分析脚本,确认未使用技能数量
python3 /tmp/analyze_skills.py

# 2. 输出:
# 本地技能总数: 225
# 从未使用: 192 (85.3%)
# 正在使用: 33

Phase 2:分批清理

每次清理前必须:

  1. mkdir -p ~/.hermes/skills-cleanup-backup{1,2,3...} 备份
  2. 列出待删目录让用户确认(用 clarify 工具)
  3. 逐批执行,不要一次性全删
# 第一批:备份目录 + 空目录
cd ~/.hermes/skills
mkdir -p ~/.hermes/skills-cleanup-backup
cp -r .archive .curator_backups ~/.hermes/skills-cleanup-backup/
rm -rf .archive .curator_backups

# 第二批:从未使用的顶级目录
mkdir -p ~/.hermes/skills-cleanup-backup2
for dir in blockchain communication dogfood email github migration mlops red-teaming web-development webpage-audit yuanbao; do
    [ -d "$dir" ] && cp -r "$dir" ~/.hermes/skills-cleanup-backup2/ && rm -rf "$dir"
done

# 第三批:激进清理(删除目录下未使用的子技能)
mkdir -p ~/.hermes/skills-cleanup-backup3
# 读取未使用技能列表并逐个备份删除
while IFS= read -r skill; do
    skill_dir=$(dirname "$skill")
    if [ -n "$skill_dir" ] && [ "$skill_dir" != "." ]; then
        cp -r "$skill_dir" ~/.hermes/skills-cleanup-backup3/ 2>/dev/null
    fi
    rm -rf "$skill"
done < /tmp/unused_skills.txt

Phase 3:验证统计

echo "清理前: $(cat /tmp/before_count.txt) 个技能"
echo "清理后: $(find ~/.hermes/skills -name 'SKILL.md' | wc -l) 个技能"
echo "共删除: $(($(cat /tmp/before_count.txt) - $(find ~/.hermes/skills -name 'SKILL.md' | wc -l))) 个"

# 验证保留的技能仍可用
for skill in zhililong renwei-writing zhili-publish; do
    echo "验证: $skill"
    # 调用一次 skill_view 测试
done

10.8 备份恢复命令

如需从备份恢复:

# 查看所有备份
ls -la ~/.hermes/skills-cleanup-backup*/

# 恢复指定目录
cp -r ~/.hermes/skills-cleanup-backup3/creative ~/.hermes/skills/

# 恢复后验证
find ~/.hermes/skills -name 'SKILL.md' | wc -l

# 确认无误后可删除备份(谨慎!)
rm -rf ~/.hermes/skills-cleanup-backup*

10.9 关键经验总结

为什么会漏删(已踩坑):

  1. ❌ 只看 mtime — skillhub 批量安装时会统一更新所有文件时间
  2. ❌ 精确字符串匹配 — zhiliComments 和 zhilicomments 被当作不同技能
  3. ❌ 只用 skill_path 匹配 — 忽略了顶级目录名(如 zhililong)
  4. ❌ 没查 session 历史 — 文件存在不代表被调用过

正确方法:

  1. ✅ 直接查询 ~/.hermes/state.db SQLite 数据库
  2. ✅ 提取 tool_calls 中的 skill_view 调用参数
  3. ✅ 应用别名标准化映射表
  4. ✅ 用最后一级目录名匹配(不是完整路径)
  5. ✅ 用差集运算找出真正的未使用技能

备份策略:

  • 每次清理前创建带编号的备份目录
  • 备份放在 ~/.hermes/skills-cleanup-backup{1,2,3...}/
  • 用户确认后才执行删除
  • 恢复命令随时可用

注意事项

  • 删除/移动前先备份,不破坏性操作(mv 比 rm 安全,trash > rm)
  • API keys/tokens/credentials 一律 [REDACTED],不在 SKILL.md 输出
  • 维护后主动汇报给用户(带数量、列表、备份位置),不静默完成
  • skill-maintenance 自身归入 developer/(skill 自身管理工具)
  • 跨 agent 共享的 skill 必须放系统级,不要留在工作区
  • 「项目源码 vs skill」判断标准:有 SKILL.md 才算 skill,只有 README + 源码 = 项目
  • v2.1 新增:install 类操作(clone / 推送 / 覆盖)必须先跑第 7 节的安全审计,HIGH/CRITICAL 风险不装
  • v2.4 新增:安装后跑第 9 节引用对齐检查(防止 caller 引用腐烂)

9.1 清理前的别名标准化(关键!)

⚠️ Pitfall H:别名未标准化导致漏报(2026-07-04 实测)

首次分析时用了精确路径匹配,漏掉了 ~110 个未使用技能。原因是 session 历史中的 skill_view 调用存在多种别名写法:

别名写法标准化为
zhiliCommentszhilicomments
social-media/.agents/skills/zhiliCommentszhilicomments
zhiliGitHubzhiligithub
social-media/.agents/skills/zhiliGitHubzhiligithub
skillgithub-syncskill-github-sync
developer/skill-github-syncskill-github-sync
zhilicomments-publishzhilicomments
social-media:.agents/skills:zhilicommentszhilicomments
geo-content-strategygeo-keyword-research
dogfood(根目录)实际是 dogfood/dogfood

标准化映射表(分析前必须先应用):

skill_aliases = {
    'zhiliComments': 'zhilicomments',
    'social-media/.agents/skills/zhiliComments': 'zhilicomments',
    'zhiliGitHub': 'zhiligithub',
    'social-media/.agents/skills/zhiliGitHub': 'zhiligithub',
    'skillgithub-sync': 'skill-github-sync',
    'developer/skill-github-sync': 'skill-github-sync',
    'zhilicomments-publish': 'zhilicomments',
    'social-media:.agents/skills:zhilicomments': 'zhilicomments',
    'social-media/.agents/skills/zhili-publish': 'zhili-publish',
    'social-media/.agents/skills/zhiligithub': 'zhiligithub',
    'geo-content-strategy': 'geo-keyword-research',
    'content-strategy/geo-keyword-research': 'geo-keyword-research',
}

判断函数(必须用这个,不能只做字符串精确匹配):

def is_used(skill_path):
    name = skill_path.split('/')[-1]  # 只取最后一级目录名
    if skill_path in normalized_used or name in normalized_used:
        return True
    for alias, target in skill_aliases.items():
        if alias == skill_path or skill_path.endswith(alias):
            return True
    return False

9.2 批量清理工作流

标准步骤(2026-07-04 实测有效):

  1. 查询 session 历史 → 获取真实使用的 skill 集合
  2. 标准化别名 → 映射多种写法到统一名称
  3. 获取本地所有 skill → find ~/.hermes/skills -name "SKILL.md"
  4. 对比 → 差集 = 从未使用的 skill
  5. 按目录分组 → 识别「全部未使用」vs「部分未使用」的目录
  6. 输出清理建议 → 列出可安全删除的顶级目录
  7. ⚠️ 必须确认 → 用 clarify 工具让用户明确选择
  8. 备份 → cp -r <dir> ~/.hermes/skills-cleanup-backup/
  9. 执行删除 → rm -rf <dir>
  10. 验证统计 → 清理后 skill 总数

确认命令示例:

# 备份(先做)
BACKUP=~/.hermes/skills-cleanup-backup
mkdir -p "$BACKUP"
for dir in blockchain mlops github; do
  [ -d "$dir" ] && cp -r "$dir" "$BACKUP/" && rm -rf "$dir"
done

# 验证
echo "清理后: $(find ~/.hermes/skills -name 'SKILL.md' | wc -l) 个技能"

历史优化记录

  • 2026-07-04(v2.7):新增第 10 节「技能使用情况分析」—— 直接查询 ~/.hermes/state.db SQLite 数据库的 messages.tool_calls 字段,从 session 历史中提取真实 skill_view 调用记录,比文件 mtime 更准确。固化 Python 对比脚本 + mtime 参考命令 + 清理决策框架。新增 9.1 节 Pitfall H「别名未标准化导致漏报」—— 首次分析漏掉 110 个未使用技能是因为 zhiliComments/zhiliGitHub 等多种写法未映射到统一名称;固化标准化映射表 + 判断函数。新增 9.2 节「批量清理工作流」—— 实测有效的 10 步流程,含别名标准化 → 目录分组 → 用户确认 → 备份 → 执行 → 验证。
  • 2026-07-03(v2.6):新增 Pitfall F「多 --- 文档分隔符」—— body 内多余 --- 会让 YAML 多文档解析器报错;固化诊断命令(grep -n '^---#x27;)+ Python 删除脚本。新增 Pitfall G「字节 vs 字符数差异」—— UTF-8 文件 stat().st_size ≠ len(read_text()),导致本地 vs 云端文件大小比较失真;固化正确解法(read_bytes() 字节级比对)。SKILL.md Step 2 合规检查表新增 --- 数量检查 + 字节级比对步骤。
  • 2026-07-02(v2.5):新增 Step 0「凭证预检」—— merge 前必须扫描 config/ 类文件,对含真实凭证的 LOCAL 文件强制保留 CLOUD 脱敏版,防止明文 APPSECRET 上云。
  • 2026-06-28(v2.3):更新第 7.3 节「误报注意」—— 新增发布类 skill 的 skillspector 判断逻辑;更新第 8.2 节合规检查——修正 YAML closing --- 识别(description 块内 --- 会被 yaml.safe_load 误判为第二个文档);固化 /tmp/skill_merge.py 为快速调用脚本。
  • 2026-06-04:从 7 分类改成 6 分类(去掉「研究」「运营」等冗余,对齐 viceroy-skills 实际仓库结构)
  • 2026-06-04:加入「OpenClaw 加载优先级」章节(解释系统级 vs 工作区差异)
  • 2026-06-04:加入「项目源码 vs skill」识别规则(应对 obscura 误入场景)
  • 2026-06-04:跟 skill-github-sync 联动标准化
  • 2026-05-27:初版,7 分类 + 删除流程
Skills enregistrées