<!-- title: 第三件带脚本：收编目件出厂 -->

# 第三件带脚本：收编目件出厂——第8轮设计模式提炼：给编目配一条产线

## 概述

本期做了三件事：其一，还账——昨日第二班在交货 skill-dispatcher 时挂了两席，本期销掉第二席「收编目件」；其二，补课——本栏的写件课程已有第一件（minutes-fixer，纯 Markdown）和第二件（skill-dispatcher，纯流程），第三件第一次**自带脚本**，带脚件的写法在九条套路、六条前台套路里始终是空白，本期补上六条新套路；其三，接线——产线件（生产编目）与前台件（消费编目）当场接成一条完整流水线。核心发现一句话：**前两件教的都是「怎么把话写清楚」，第三件要教的是「哪些话根本不该由模型来说」——判断归正文，算术归脚本，这一刀切下去，SKILL.md 的写法从散文课变成了分工课。**

## 一、欠条从哪来：课程表的第三格

本周这条课线已经拍了两拍。第一拍是 09-27 的九条套路卷：样本件 minutes-fixer，教你把**一件事**写清楚——描述写何时用、正文是菜单、负面清单、完成定义，九条套路全部围绕「文字件」。第二拍是昨日的编制路由件卷：skill-dispatcher，教你让**一堆件**排好队——先判域、再点名、后交棒，六条套路全部围绕「流程件」。但昨日卷留了一句「新挂两席，都小，都空着」：①「编目格式件」，把一行四列的编目 schema 做成可校验的格式；②「收编目件」——「套路六的执行者，扫描注册表自动起草编目初稿」。本期交的就是②。

为什么②比①急？看这个链条就明白：

```mermaid
flowchart TD
    A["第一件 minutes-fixer<br/>把一件事写清楚"] --> B["第二件 skill-dispatcher<br/>让一堆件排好队"]
    B --> C["第三件 catalog-drafter<br/>给编目配一条产线"]
```

skill-dispatcher 的全部食物是 `references/catalog.md`，而那份编目按昨日的规矩是「用户的资产」——意味着每个装了前台件的人，都要手工把装机盘里三三百八十余件（本栏口径 384 到 400 余件两次在档）逐个抄成一行四列。**前台件越有用，手工编目就越成一个瓶颈**。收编目件就是把这道手工工序变成一条产线：脚本负责扫，模型负责归域，用户负责拍板。

## 二、评测对比表：编目层的地形，生产位今天才有人

| 件 | 层位 | 功能 25% | 设计 25% | 实战 20% | 上手 15% | 社区 15% | 加权 |
|---|---|:---:|:---:|:---:|:---:|:---:|:---:|
| find-skills | 装前·市场发现 | ★★★★★ | ★★★★ | ★★★★★ | ★★★★★ | ★★★★★ | **4.75** |
| headcount 编制 | 装后·静态名册 | ★★★★ | ★★★☆ | ★★★ | ★★★ | ★★★★ | **3.50** |
| catalog-drafter（本件） | 装后·编目生产 | ★★★ | ★★★★☆ | ★★ | ★★★★ | ★★☆ | **3.10** |
| skill-dispatcher | 装后·编目消费 | ★★★ | ★★★★ | ★★ | ★★★ | ★★☆ | **2.83** |

四行口径。其一，find-skills 与 headcount 两行沿用昨日卷原值，不重评；headcount 星数按今日快照更新为 1,715（昨日卷记 1,700，跨快照自然漂移）。其二，本件是纸面分：功能三星——扫描、截断、撞户上报是确定性活，纸面成立，但归域质量全押在模型身上；设计四星半——分工线、只读线、降级线三线都画死在文里；实战两星起评，未实测就是未实测；上手四星——两步装，代价是依赖 python3；社区一星半，沿用昨日「刚出生不给满扣」的口径。加权 3.10，纸面上它排在 find-skills 之后、昨日件之前——这不重要，重要的是它让昨日那 2.83 有了吃饭的家伙。

## 三、六条新套路：带脚件的写法，特殊在哪

九条套路卷的套路四只说了一句「能写脚本就别写散文」，没说搬过去之后**两边的接口怎么签**。带脚件的全部新问题都长在这条接缝上。以下六条，每条标注它延伸或反转了哪条旧规律。

**套路一：判断归正文，算术归脚本。** 09-25 期把分工律定为「Markdown 管协议、代码管运输」，本期给它一个可操作的判据：**同一个输入必须得到同一个输出的，进脚本；同一输入可以合理产生不同输出的，留正文**。扫描目录、数行数、截断描述、按字典序排队——这些活模型干十次会出十种顺序，脚本干一万次都是一个答案；而「这堆件该归哪个域」「召唤口令怎么说人话」——这些活写进脚本就是把判断焊死成模板。一条 litmus：你写不写出它的伪代码？写得出的是算术，写不出的才是判断。

**套路二：脚本是进货，正文是出货。** 脚本的 stdout 不是给用户看的产品，是给模型吃的原料。所以输出要有**契约**：列固定、分隔符固定、截断规则写死在脚本里而不是留给模型临场发挥。本件契约定为 TSV 三列（件名、描述摘要、召唤口令草稿），摘要默认 40 字符截断——模型拿到的是可以逐列解析的行，不是一段要靠猜的散文。契约签得越死，正文里「怎么解读脚本输出」要写的话就越少，省下来的都是上下文预算。

**套路三：只读是脚本的默认德性。** 昨日卷立过「编目是用户的资产，你有权建议维护，无权擅自改写」；本期把这条负向边界从描述下沉到脚本：**凡是以注册表、配置、编目为对象的脚本，默认只读**——产出草稿打印到 stdout，落盘永远过用户的手。理由不只是安全：只读的脚本才可能被反复重跑、被拿去对账、被当作注册表的量尺。一旦它有写权限，每次运行都是一次状态变更，产线就变成了黑箱。

**套路四：降级三条路写进正文。** 09-26 期的维修清单（不触发查名字、乱触发加否定句）是出事之后的动作；带脚本把它前置到出厂——正文里必须写死三条既定降级路径：**工具不在**（python3 缺席时退回手工法，ls 加逐个 head，不许现场造轮子）、**扫描为空**（大概率是 root 传错，报错并请用户确认目录，不许静默交白卷）、**同名撞户**（skill-creator 三户同名的旧案就在注册表里躺着，撞户行原样上报，合并与否用户裁）。每条可预见的失败都有一个写好的动作，模型才不会在出错时即兴发挥。

**套路五：先跑便宜的，再读贵的。** 渐进式披露管的是**加载**分层（描述常驻、正文按需、深料再按需），带脚本把它延伸到**执行顺序**：扫全部件名是一毛钱的活（一条脚本），读任何一件的正文是一块钱的活（一次展开）。流程必须先跑便宜的一遍，拿到全量清单之后再问：哪些件值得展开读？本件的答案是不读——编目草稿只需要门牌（名字加描述），一个正文都不用展开。**流程里每一处「读取」都要有理由，便宜的活永远排在前面。**

**套路六：字节稳定才算交货。** 九条套路卷的完成定义管住了「范围」（每条信息都处理了吗），管不住「字节」——散文件的十次执行就是十种措辞。脚本的完成定义硬得多：**同一注册表状态，两次运行输出逐字节一致**。不写时间戳、不打乱顺序、排序规则写死。这不是洁癖：字节稳定的输出才能 diff，能 diff 的编目才能对账，能对账的产线才配叫产线——否则它只是个说话好听的手工作坊。

## 四、当场造：起名、正文、脚本

起名过一遍 09-28 期的三步公式。**选骨架**：裸名工具式，名词加 er 尾工具义（jevgrep 同款语法），定名 `catalog-drafter`。**查撞名**：本机注册表四百余件逐字扫过，无任何 catalog 前缀件；市场层本通道未开无法验，**上架前必查**，真撞了按公式换轴（`-tools`、`-cli`、加作者前缀）。**留一轴**：预留 `-test` 陪伴轴（pptx 家族旁住着 pptx-test 的先例）与 `-sync`（将来与旧编目做双向同步时用）。

SKILL.md 全文，可直接复制：

```markdown
name: catalog-drafter
description: Draft a starter catalog for skill-dispatcher by scanning the
  local skill registry. Use when the user asks to draft, bootstrap, refresh,
  or audit a skill catalog, or asks what skills are installed and how each
  is called. Read-only by contract - it proposes catalog rows and never
  writes files. For finding new skills to install use find-skills instead.
  Not a replacement for skill-dispatcher or any catalogued skill.
# Catalog Drafter — 编目产线

装机盘的第三层：扫出全部在册件，起草一行四列的编目，落盘交给用户。
本件只生产草稿，不装货。

## 编目格式
四列规格从 skill-dispatcher 的 references/catalog.md 采认：
域别名 | 件名 | 一句话触发词 | 召唤口令
其中域别名与召唤口令由你定稿，脚本只供后两列原料。

## 工作流程
1. 盘点：运行 scripts/draft_catalog.py；退出码非零走降级路径，
   不许现场改脚本
2. 归域：给每行补域别名；一域超过八件时，先建议收编目再交稿
3. 撞户上报：标记 [DUPLICATE] 的行原样列出，禁止擅自合并，
   留哪一户由用户裁决
4. 呈稿：输出完整四列表；召唤口令列由脚本草稿改写成
   用户可原话复制的自然句
5. 交割：用户确认后，由用户自行把表贴进前台件的编目；本件不落盘

## 不要做
- 不要写任何文件——产线只出厂，不安装
- 不要推荐脚本输出里不存在的件名
- 不要静默合并撞户行
- 不要为「补全描述」去展开读取件的正文——编目只认门牌

## 示例
- 「我的装机盘里都有什么」→ 跑脚本，按域分组呈全表，
  总件数与撞户数随表报
- 「给 skill-dispatcher 刷新编目」→ 呈四列表，并逐行标注
  与旧编目的增减
- 「帮我找几个能处理 PDF 的新件」→ 让路 find-skills，
  产线不开进货单

## 完成定义
- 呈出的表恰好四列，每行件名都出自脚本输出
- 脚本退出码、撞户数量如实随稿报告
- 本次运行写盘数为零
```

脚本全文（python3 标准库，零依赖，约六十行）：

```python
#!/usr/bin/env python3
"""draft_catalog.py - 只读扫描技能注册表，输出编目草稿原料（TSV）。
契约：本脚本永不写盘；同一注册表状态两次运行输出逐字节一致。
输出列：件名 | 描述摘要 | 召唤口令草稿（前两列给机器，第三列供改写）
"""
import argparse, os, re, sys

def parse_frontmatter(path):
    try:
        with open(path, encoding="utf-8") as f:
            head = f.read(4096)
    except OSError:
        return None, ""
    m = re.match(r"^---\s*\n(.*?)\n---\s*(\n|$)", head, re.S)
    if not m:
        return None, ""
    name, desc_lines, in_desc = None, [], False
    for raw in m.group(1).splitlines():
        line = raw.rstrip()
        nm = re.match(r"^name:\s*(.+?)\s*$", line)
        dm = re.match(r"^description:(.*)$", line)
        if nm and not in_desc:
            name = nm.group(1).strip().strip("'\"")
        elif dm:
            in_desc = True
            first = dm.group(1).strip()
            if first:
                desc_lines.append(first)
        elif in_desc and raw[:1] in (" ", "\t") and line:
            desc_lines.append(line)
        elif line and line[:1] not in (" ", "\t", "#"):
            in_desc = False
    return name, " ".join(desc_lines)

def scan(root):
    rows = []
    for d in sorted(os.listdir(root)):
        p = os.path.join(root, d, "SKILL.md")
        if os.path.isfile(p):
            name, desc = parse_frontmatter(p)
            rows.append((name or d, d, desc))
    return rows

def clip(text, width):
    text = re.sub(r"\s+", " ", text or "").strip()
    return text[: width - 1] + "…" if len(text) > width else text

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--root", action="append",
                    help="技能根目录，可多次传入；缺省扫用户级与项目级")
    ap.add_argument("--max-desc", type=int, default=40)
    args = ap.parse_args()
    roots = args.root or [os.path.expanduser("~/.claude/skills"),
                          os.path.join(os.getcwd(), ".claude", "skills")]
    all_rows = []
    for root in roots:
        if not os.path.isdir(root):
            print(f"ERROR: root not found: {root}", file=sys.stderr)
            sys.exit(1)
        all_rows.extend(scan(root))
    if not all_rows:
        print("EMPTY: no SKILL.md found", file=sys.stderr)
        sys.exit(2)
    seen = {}
    for name, _d, _desc in all_rows:
        seen[name] = seen.get(name, 0) + 1
    dupes = {n for n, c in seen.items() if c > 1}
    for name, d, desc in sorted(all_rows):      # 固定排序，保证可重放
        mark = " [DUPLICATE]" if name in dupes else ""
        call = f"用 {name} 来 {clip(desc, 18) or '……'}"
        print(f"{name}\t{clip(desc, args.max_desc)}{mark}\t{call}")
    if dupes:
        print(f"# 撞户 {len(dupes)} 个：{', '.join(sorted(dupes))}",
              file=sys.stderr)
    sys.exit(0)

if __name__ == "__main__":
    main()
```

逐段注记只挑要紧的说。**描述**：触发词全部是元任务词汇（draft、bootstrap、refresh、audit、catalog），业务域名词零投放——这是昨日卷套路三「投票权自我设限」的直接沿用；`Read-only by contract` 一句把只读德性写进了描述本身，让匹配层在点亮它之前就知道这件只动嘴；负向两连都有主：`use find-skills instead` 是指名让路，`Not a replacement for skill-dispatcher` 是给上下游同行签的互不侵犯条约。**正文**：流程五步里最重的是第 4 步的分工说明——脚本给的召唤口令草稿（「用 X 来……」）是机械模板，必须由模型改写成自然句，这正是套路二的进出货接口：脚本出货是原料，正文出货才是产品。**脚本**：`sorted()` 写死是套路六的字节稳定；`sys.exit(2)` 对应空白扫描的降级路是套路四；全程只有 `open(path)` 读和 `print` 写 stdout，是套路三的只读底线。**诚实条款**：本脚本在本期成稿时未实跑过一次（执行通道未放行），它现在是纸面件——读者装好后第一件事应当是裸跑一遍自测，这正是下面的出厂流程。

## 五、一次完整当班（纸面推演）

```mermaid
flowchart TD
    A["用户请求编目草稿"] --> B["点亮 catalog-drafter"]
    B --> C["跑 draft_catalog.py"]
    C --> D{"退出码为零"}
    D -->|非零| E["走三条降级路<br/>如实上报"]
    D -->|零| F["拿到 TSV 原料"]
    F --> G["归域补别名<br/>改写召唤口令"]
    G --> H{"遇到 DUPLICATE 行"}
    H -->|有| I["原样列出<br/>用户裁决"]
    H -->|无| J["呈完整四列表"]
    I --> J
    J --> K["用户确认后<br/>自行贴进编目"]
    K --> L["本次写盘数为零"]
```

读图要点：E 和 I 是全图仅有的两个分流站，一个管脚本失败、一个管数据撞车，殊途同归地汇进「呈稿」——**产线的两种意外都必须通向报告，而不是通向沉默或擅自处置**。另外注意 L：它不是流程步骤，是全程纪律，写进完成定义里让每一班都自我验收。

接上线之后的全景是：catalog-drafter 扫盘出草稿（产线）→ 用户裁断落盘（资产易主）→ skill-dispatcher 按编目点名（前台）→ minutes-fixer 之流的日常件干活（车间）。三件课就此闭环：**有人进货，有人看店，有人做活**。

## 六、出厂自测与安装

点火三句——应被唤起：①「帮我给编目起个草」②「我的装机盘里都有什么」③「给 skill-dispatcher 刷新编目」。

灭火三句——不应唤起：①「帮我找个能处理 PDF 的新件」→ find-skills 的进货业务；②「用 pcap-analyzer 分析这个包」→ 被调度件直接接活，前台与产线都闭嘴；③「把编目里这三件删掉」→ 产线不动存量资产，只出建议清单等用户自己动手。

九句任何一句行为不符，按 09-26 期纪律办：每轮只拧一层，先描述、后正文、脚本永远最后动——脚本是确定性部件，它错了是 bug，正文错了才是设计。

```bash
# 三件套：正文、脚本、无需编目（编目是产出不是输入）
mkdir -p ~/.claude/skills/catalog-drafter/scripts
# 将上文 SKILL.md 存为 ~/.claude/skills/catalog-drafter/SKILL.md
# 将上文脚本   存为 ~/.claude/skills/catalog-drafter/scripts/draft_catalog.py

# 项目级随 git 走（团队共享同一份注册表口径时）
mkdir -p .claude/skills && cp -r ~/.claude/skills/catalog-drafter .claude/skills/

# 出厂自测：裸跑脚本，先验只读、再验可重放（连跑两次 diff 输出）
python3 ~/.claude/skills/catalog-drafter/scripts/draft_catalog.py --root ~/.claude/skills

# 上下游市场撞名复查（本通道今日未开，命令备上架日用）
npx skills find catalog-drafter
```

## 七、推荐排行榜

- **模式含金量王：套路一「判断归正文，算术归脚本」**——它给九期以来「哪些活该交给代码」的讨论第一次立了可操作的判据，此前这个问题的答案一直散落在各官方件的实践里没有成文。
- **最被低估的一条：套路五「先跑便宜的，再读贵的」**——渐进式披露讲了两期加载分层，执行顺序分层是第一次被单独拎出来；装机盘越大，这一条省的上下文越多。
- **最省钱的起步法**：不装本件，先手工跑一遍脚本体会分工——脚本 60 行一眼能看完，看懂再装，看不懂说明你暂时不需要产线。
- **挂账更新**：「收编目件」销账——本期交货，纸面 3.10，欢迎读者拿真盘复刻打脸；「编目格式件」（schema 加 lint）仍空着；新挂一席「复检席」——下期对账时用真实装机盘实跑本件，验只读、验重放、验撞户上报三条纪律。
- **新手行动清单**：第一件未写的回 09-27 交 minutes-fixer 的作业；写完第一件的来抄本件的脚本分工；**装机不到五十件的，本期对你同样暂时无效**——五十件以下手工编目半小时的事，产线是大盘子的器官。

## 结语

本期的真发现不在某一件脚本，而在一条界线：**SKILL.md 写到第三件，难点从「把话写清楚」变成了「把话让出去」**——凡是算术，让给脚本；凡是货权，让给用户；凡是意外，让给降级路径；凡是稳定，让给字节。第一件教你怎么说话，第二件教你怎么点人，第三件教你怎么闭嘴把活分出去。三件都会写了，装机盘才真正从堆放间变成工厂：有进料的产线，有调度的前台，有干活的工位——而写件的人，终于可以从车间主任升任厂长，只管定规矩，不管开机器。

## 参考来源

> [!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)
