<!-- title: 造规范提交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 设计的最佳素材。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常运行）
- 一台装有 `git`、`python3`（≥3.8）、`bash` 的 Linux/macOS 机器
- 一个可以随便提交的测试仓库（教程后半段会用到一个真实示例）
- 大致了解 Conventional Commits：提交头格式为 `<type>[scope]: <description>`

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

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

```mermaid
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/`。本教程用项目级，方便随仓库分发。

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

写入 `.claude/skills/conv-commit/SKILL.md`：

```markdown
---
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`：

```python
#!/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% 可复现的，写成脚本；需要上下文理解的，留给模型**。

验证一下（在一个有暂存改动的仓库里执行）：

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

**预期输出**（示意）：

```json
{
  "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`：

```bash
#!/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"
```

赋权：

```bash
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`：

```markdown
# 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，留空更诚实
```

## 第六步：验证与触发测试

完整的目录结构应该是：

```mermaid
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 项目：

```bash
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 空字符串也能通过"的逻辑：

```bash
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 检查，不合规就拒绝提交、把错误回吐给模型让它修正：

```bash
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 更合适。本案例之所以适合，是因为"提交"是每个仓库、每一天都会重复的高频操作。

## 延伸阅读

- **skill-creator 元技能**：不想手写骨架，可以直接对 Claude Code 说"用 skill-creator 帮我创建一个 commit 助手"，它会通过对话采访你、自动生成上述结构。安装方式：`/plugin marketplace add anthropics/skills` 后 `/plugin install example-skills@anthropic-agent-skills`。
- **Conventional Commits 官方站**：[conventionalcommits.org](https://www.conventionalcommits.org/en/v1.0.0/)，规范的权威来源，含完整示例。
- **Angular 提交规范**：被 `@commitlint/config-angular` 实现，是业界事实标准，`type_mapping.md` 的类型清单即源自此处。
- **语义化版本联动**：feat→MINOR、fix→PATCH、BREAKING CHANGE→MAJOR，理解这条映射才能体会规范提交对自动 changelog/版本号的全部价值。

## 训练营适用性评估

- **适合训练营**：开发者效能训练营、Git 工程化训练营、AI 辅助编程训练营的中级模块。
- **教学时长**：约 90 分钟。其中 20 分钟讲三层结构与渐进式披露原理，30 分钟手写脚本与 SKILL.md，20 分钟实战跑通，20 分钟触发调优与扩展。
- **先修内容**：学员需先完成"零基础装通 Claude Code"类入门轮次。
- **难度定位**：中等。核心难点不在写代码，而在理解"哪些交给脚本、哪些交给模型、description 怎么写才能稳定触发"——这正是 Skill 设计思维的精髓。

## 参考来源

1. [Anthropic Skills 官方仓库](https://github.com/anthropics/skills) — Skill 文件夹结构、SKILL.md 模板与 frontmatter 必填字段（name/description）的权威定义。
2. [Conventional Commits 1.0.0 规范](https://www.conventionalcommits.org/en/v1.0.0/) — 提交头格式、type 体系、BREAKING CHANGE 语义与 SemVer 联动的官方说明。
3. [skill-creator Complete Guide（SmartScope）](https://smartscope.blog/en/ai-development/skills/skill-creator-complete-guide/) — 详述渐进式披露、确定性脚本化、自由度分级等 Skill 设计原则与目录约定。
4. [How to create custom skills（Claude Help Center）](https://support.claude.com/en/articles/12512198-creating-custom-skills) — name/description 字段约束、保留字与字符限制的官方指引。
5. [@commitlint/config-angular（npm）](https://www.npmjs.com/package/@commitlint/config-angular) — 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
