界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
数据与研究 · 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 …

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

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 分类 + 删除流程
我的收藏