首页 › tutorials › 造规范提交Skill:diff秒变提交信息

概述

本教程带你从零构建一个生成式自定义 Skill —— conv-commit:它读取暂存区的 git diff,自动产出符合 Conventional Commits 规范的提交信息并完成提交。读完你将掌握 Skill 的三层渐进式披露结构、SKILL.md frontmatter 写法、确定性脚本与模型判断的职责切分,以及触发器描述的调优技巧。适合已经能用 Claude Code 跑通基础任务、想把重复工作流沉淀为可复用 Skill 的开发者。

为什么是"提交助手"这个 Skill

先明确一个判断标准:"这个任务我会重复 3 次以上吗?" 如果会,就值得做成 Skill。

写提交信息是高频痛点。每次 git commit 都要:看 diff、归类改动、回忆规范(是 feat 还是 fix?要不要 scope?有没有 BREAKING CHANGE)、再手工敲一行。同一个改动,团队成员写出来的提交信息五花八门,后续无法自动生成 changelog、无法用语义化版本号。

把它做成 Skill 的好处在于:diff 是客观事实,提交规范是确定性规则,只有"归类"这一步需要模型的语义判断。这三者的切分,恰好是练习 Skill 设计的最佳素材。

前置条件

第一步:先理解 Skill 的三层结构

动手之前,先在脑子里建好模型。Skill 不是一个大文件,而是一个文件夹,按"加载成本"分三层:

graph LR
    A["第1层:frontmatter<br/>~100 tokens · 始终常驻"] -->|触发匹配成功| B["第2层:SKILL.md 正文<br/>建议 < 5000 tokens · 触发时加载"]
    B -->|需要细则时| C["第3层:scripts/ references/<br/>无上限 · 按需读取"]
    style A fill:#e1f5fe
    style B fill:#fff3e0
    style C fill:#e8f5e9

关键认知:第 1 层永远在上下文里,哪怕你不触发这个 Skill,它也在消耗 token。所以 description 是整个 Skill 最重要的一段——它决定 Claude 何时调用。第 2 层正文只在被触发后才加载,因此"何时使用"必须写在 description 里,写在正文里 Claude 根本看不到。

第 3 层是重资料:大段规则、API 文档、脚本,都放这里,按需读取,不占常驻预算。这就是渐进式披露(Progressive Disclosure),是 Skill 设计的灵魂。

第二步:创建目录与 SKILL.md 骨架

在任意工作目录下创建 Skill 文件夹。Claude Code 的个人级 Skill 默认放在 ~/.claude/skills/,项目级放在 <repo>/.claude/skills/。本教程用项目级,方便随仓库分发。

mkdir -p .claude/skills/conv-commit/scripts
mkdir -p .claude/skills/conv-commit/references

写入 .claude/skills/conv-commit/SKILL.md:

---
name: conv-commit
description: Analyzes the staged git diff and produces a Conventional Commits message (feat/fix/docs/refactor/perf/test/build/ci/chore), then commits. Use when the user wants to commit changes, says "提交" / "commit" / "写提交信息" / "整理这次的改动", or asks for a 规范化的 commit message.
---

# Conventional Commits 提交助手

## 工作流

1. 运行 `python3 scripts/analyze_diff.py`,拿到暂存区变更的结构化事实(JSON)。
   - 若 `has_staged` 为 false,先提示用户 `git add` 暂存相关文件,不要自动全量 `git add -A`。
2. 读取 `references/type_mapping.md`,依据事实判定 `type` / `scope` / `breaking` / `description`。
3. 组装提交信息并调用 `scripts/render_commit.sh <type> <scope> <breaking> <description> [body_file]` 完成提交。

## description 写作规则

- 主体 ≤ 50 字符,祈使句,小写开头,句末不加句号。
- 仅当改动确实聚焦于一个模块时才写 scope。
- breaking=true 时给 header 加 `!`,并在 body 中以 `BREAKING CHANGE: <原因>` 说明。

## 自由度

本 Skill 的语义判断(选 type、写描述)交给模型;格式化、行数统计、文件归类交给脚本——后者必须 100% 可复现。

预期结果:保存后用 /skills 命令应能看到 conv-commit 出现在列表里,状态为可用。

常见问题:

现象 原因 解决
/skills 看不到该 Skill frontmatter YAML 缩进/冒号错误 用 python3 -c "import yaml,sys;print(yaml.safe_load(open('.claude/skills/conv-commit/SKILL.md').read().split('---')[1]))" 验证可解析
触发了但行为不对 description 没写清触发词 在 description 里补足用户可能的自然语言表达(中英文都要)
报 "name 含保留字" name 写了 claude 或 anthropic 改名,这两个词是保留字

第三步:编写确定性脚本 analyze_diff.py

这是本 Skill 的核心设计点:把所有可机械判定的事实交给脚本,把需要语义理解的归类交给模型。脚本只回答"改了什么",模型回答"算哪类"。

写入 .claude/skills/conv-commit/scripts/analyze_diff.py:

#!/usr/bin/env python3
"""analyze_diff.py — 把暂存区 diff 解析成机器可读的变更事实。
仅做确定性提取,不做 type 判断。输出 JSON 供模型消费。"""
import subprocess, json, os, sys
from collections import defaultdict

def git(*args):
    return subprocess.run(["git", *args], capture_output=True, text=True).stdout

def classify(path):
    """按路径启发式分桶,给模型一个粗粒度的归类信号。"""
    base = os.path.basename(path).lower()
    if (base.startswith("test") or ".test." in base or ".spec." in base
            or "__test__" in path or "/tests/" in path or "/test/" in path):
        return "tests"
    if base in {"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
                "go.mod", "go.sum", "requirements.txt", "pyproject.toml",
                "cargo.toml", "cargo.lock", "pom.xml", "gemfile", "gemfile.lock"}:
        return "deps"
    if base.endswith((".md", ".rst")) or "/docs/" in path or "/doc/" in path:
        return "docs"
    if base.endswith((".yml", ".yaml")) and ("/.github/" in path or "ci" in path):
        return "ci"
    if base.startswith("dockerfile") or base.endswith((".dockerfile", "makefile")) or base == "cmakelists.txt":
        return "build"
    return "source"

def main():
    name_status = git("diff", "--cached", "--name-status")
    numstat = git("diff", "--cached", "--numstat")

    added = deleted = 0
    for line in numstat.splitlines():
        p = line.split("\t")
        if len(p) == 3 and p[0].lstrip("-").isdigit():
            added += int(p[0]); deleted += int(p[1])

    files, buckets = [], defaultdict(int)
    for line in name_status.splitlines():
        if not line.strip():
            continue
        code, _, path = line.partition("\t")
        status = code[0]  # A=新增 M=修改 D=删除 R=重命名 ...
        files.append({"status": status, "path": path})
        buckets[classify(path)] += 1

    result = {
        "has_staged": len(files) > 0,
        "file_count": len(files),
        "added_lines": added,
        "deleted_lines": deleted,
        "buckets": dict(buckets),   # 例如 {"source": 2, "tests": 1}
        "files": files,
    }
    print(json.dumps(result, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

为什么用脚本而不是写进 SKILL.md 正文? 因为行数统计、文件分桶这类操作要求"每次输入相同就输出相同"。如果用自然语言让模型去数行数,不同次调用结果可能差几行——而提交信息的客观事实必须精确。这就是"确定性处理脚本化"原则:需要 100% 可复现的,写成脚本;需要上下文理解的,留给模型。

验证一下(在一个有暂存改动的仓库里执行):

python3 .claude/skills/conv-commit/scripts/analyze_diff.py

预期输出(示意):

{
  "has_staged": true,
  "file_count": 3,
  "added_lines": 47,
  "deleted_lines": 9,
  "buckets": { "source": 2, "tests": 1 },
  "files": [
    { "status": "M", "path": "src/auth/token.py" },
    { "status": "M", "path": "src/auth/session.py" },
    { "status": "A", "path": "tests/auth/test_token_expire.py" }
  ]
}

第四步:编写 render_commit.sh 格式化提交

模型负责决定字段值,脚本负责按规范拼装并真正提交——同样为了格式 100% 一致。

写入 .claude/skills/conv-commit/scripts/render_commit.sh:

#!/usr/bin/env bash
# render_commit.sh — 组装 Conventional Commits 头并提交。不做事语义判断。
set -euo pipefail

type="${1:?usage: render_commit.sh <type> <scope> <breaking> <description> [body_file]}"
scope="${2:-}"
breaking="${3:-false}"
description="${4:?description required}"
body_file="${5:-}"

scope_part=""; [[ -n "$scope" ]] && scope_part="($scope)"
bang=""; [[ "$breaking" == "true" ]] && bang="!"
header="${type}${scope_part}${bang}: ${description}"

if [[ -n "$body_file" && -f "$body_file" ]]; then
  git commit -m "$header" -F "$body_file"
else
  git commit -m "$header"
fi
echo "✓ committed: $header"

赋权:

chmod +x .claude/skills/conv-commit/scripts/render_commit.sh

第五步:编写参考文档 type_mapping.md

这是第 3 层资料——放完整的归类规则,平时不加载,模型需要时才读。把它和 SKILL.md 正文分开,是为了把正文 token 控制在预算内。

写入 .claude/skills/conv-commit/references/type_mapping.md:

# Commit Type 判定规则(基于 @commitlint/config-angular)

## 类型清单
feat 新功能(→ SemVer MINOR)
fix  修 bug(→ SemVer PATCH)
docs  仅文档
style 不影响代码含义(空格/格式)
refactor 既非修 bug 也非加功能
perf  提升性能
test  补/改测试
build 构建系统/外部依赖(webpack/npm/go.mod 等)
ci   CI 配置(.github/workflows、GitLab CI)
chore 杂务(不修 bug 也不加功能的琐事,如改 .gitignore)
revert 回滚某个提交

## 判定优先级(结合 analyze_diff.py 的 buckets)
1. buckets 只含 docs         → docs
2. buckets 只含/主导 tests    → test(除非连源码一起改,则以源码为准)
3. buckets 只含 deps/build/ci → build / ci
4. source 中含全新公开能力
   (新增导出符号、新路由、新页面、新 CLI 命令)→ feat
5. source 改动针对已知缺陷
   (加边界判断、修空指针、纠正逻辑)→ fix
6. 仅结构调整、无行为变化    → refactor
7. 命中性能优化(缓存、算法)→ perf

## Breaking Change 信号(→ 加 !,并在 body 写 BREAKING CHANGE:)
- 删除或重命名公开导出符号
- 函数签名参数变化/移除
- 配置项被移除
- 文件迁移导致 import 路径变化
- 数据库 schema 的不可逆迁移

## scope 写法
- 仅当改动聚焦单一模块时使用,取模块/包名:feat(auth):、fix(parser):
- 跨多模块的大改不要硬凑 scope,留空更诚实

第六步:验证与触发测试

完整的目录结构应该是:

graph TD
    R["conv-commit/"] --> S["SKILL.md<br/>frontmatter + 工作流"]
    R --> D["scripts/"]
    R --> E["references/"]
    D --> D1["analyze_diff.py<br/>确定性事实提取"]
    D --> D2["render_commit.sh<br/>格式化并提交"]
    E --> E1["type_mapping.md<br/>归类规则资料"]
    style S fill:#fff3e0
    style D fill:#e8f5e9
    style E fill:#e1f5fe

在测试仓库里制造一次暂存改动,然后对 Claude Code 说:

"帮我整理这次的改动并提交"

预期行为链:

  1. Claude 识别出 commit/提交 意图,触发 conv-commit(因为 description 里写了触发词)。
  2. 调用 analyze_diff.py,读到的 JSON 没有 has_staged——它会先问你"是否需要 git add 这几个文件?"。
  3. 暂存后再次运行,拿到事实;读取 type_mapping.md 判定 type/scope。
  4. 调用 render_commit.sh feat auth false "修复 token 过期判断" 完成提交。

如果没触发,十有八九是 description 不够好。调试方法:把当时的真实用户原话加进 description。比如你习惯说"commit 一下",那 description 里就必须出现 commit 一下 这几个字。

实战案例:在真实仓库跑一遍

准备一个最小 Flask 项目:

mkdir demo && cd demo && git init
cat > app.py <<'EOF'
def auth(user, token):
    return user == "admin" and token != ""
EOF
git add app.py && git commit -m "init"

现在新增一个"修复 token 空字符串也能通过"的逻辑:

cat > app.py <<'EOF'
def auth(user, token):
    if not token:
        return False
    return user == "admin"
EOF
cat > test_app.py <<'EOF'
def test_empty_token_rejected():
    assert auth("admin", "") is False
EOF
git add app.py test_app.py

对 Claude Code 说:"提交一下这次的改动"。

理想情况下,Skill 跑完 analyze_diff.py,看到 buckets={"source":1,"tests":1},test_app.py 是新增测试、app.py 改了判断逻辑(从宽松变严格),判定为 fix,scope 取 auth,最终提交:

fix(auth): reject empty token in auth check

这就是一个完整的"diff 秒变提交信息"闭环。

进阶:加 commitlint 校验与团队分发

Skill 跑通后,两个值得加的增强:

1. 提交前本地校验。在 render_commit.sh 末尾、git commit 之前插入 commitlint 检查,不合规就拒绝提交、把错误回吐给模型让它修正:

if command -v commitlint >/dev/null 2>&1; then
  echo "$header" | commitlint || { echo "✗ commitlint 拒绝,请修正"; exit 1; }
fi

2. 团队分发。把 .claude/skills/conv-commit/ 整个目录提交到仓库。团队成员 clone 后自动获得该 Skill——这是项目级 Skill 的天然优势,比口头约定"请按规范写提交"靠谱得多。如果要跨项目复用,则放进 ~/.claude/skills/(个人级)。

何时不该做成 Skill

保持诚实:一次性任务别做 Skill,维护成本会超过收益;频繁变动的流程更适合写成 Memory 或项目说明;主要在和外部 API 交互的任务,MCP 服务器比 Skill 更合适。本案例之所以适合,是因为"提交"是每个仓库、每一天都会重复的高频操作。

延伸阅读

训练营适用性评估

参考来源

  1. Anthropic Skills 官方仓库 — Skill 文件夹结构、SKILL.md 模板与 frontmatter 必填字段(name/description)的权威定义。
  2. Conventional Commits 1.0.0 规范 — 提交头格式、type 体系、BREAKING CHANGE 语义与 SemVer 联动的官方说明。
  3. skill-creator Complete Guide(SmartScope) — 详述渐进式披露、确定性脚本化、自由度分级等 Skill 设计原则与目录约定。
  4. How to create custom skills(Claude Help Center) — name/description 字段约束、保留字与字符限制的官方指引。
  5. @commitlint/config-angular(npm) — Angular 提交约定在 commitlint 中的实现,是本文 type 清单的工程化落地参考。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入