<!-- title: 定时巡检舰队：Cron聚总报告 -->

## 概述

本教程带你用 Claude Code 的 `-p` 无头模式构建一支**定时巡检舰队**：cron 按计划触发，多个 Agent 并行扫描代码库的不同健康维度（安全、技术债、性能、依赖、覆盖），每个 Agent 以 `stream-json` / `json` 流式结构化输出，最后由一个**聚合 Agent** 把散落的 JSON 合并成一份统一健康报告并做日环比。适合已掌握交互式 Claude Code、想把 Agent 从「随用随起」升级为「无人值守定期巡逻」的后端工程师与 DevOps 人员。

## 前置条件

- 已安装 Claude Code（终端执行 `claude --version` 可输出版本号）
- Node.js 18+（Claude Code 运行依赖）
- `jq`（结构化解析必备，`sudo apt install jq` 或 `brew install jq`）
- bash 4+ 或 zsh
- 一个可访问的代码仓库（作为被巡检目标，建议先拿非生产的小仓库练手）
- 非交互鉴权：`ANTHROPIC_API_KEY` 环境变量，或 `claude setup-token` 生成的长期 OAuth token（cron 无法弹出浏览器登录）
- 基础 shell、管道、cron 知识

---

## 正文

### 步骤一：吃透三种输出格式，这是自动化的命脉

交互式 Claude Code 给你的是彩色 TUI；无头模式下，你拿到的是**可被程序解析的数据流**。`--output-format` 有三个取值，理解它们的差异决定了你的脚本能否稳定工作：

| 取值 | 输出形态 | 适用场景 | 解析难度 |
|------|----------|----------|----------|
| `text`（默认） | 纯文本最终回复 | 人工阅读、简单管道 | 低（但有围栏/噪声） |
| `json` | **单个** JSON 对象，含 `result`、`cost_usd`、`duration_ms`、`num_turns`、`session_id` | 一次性任务、要成本/耗时统计 | 低 |
| `stream-json` | **换行分隔**的多个 JSON 事件（每条消息一个对象） | 实时监控、自定义 UI、抽取中间工具调用 | 中（需 `jq` 过滤事件类型） |

关键事实：`stream-json` 是「**每个事件一个 JSON 对象、逐行输出**」的流，事件类型包括 `system`（会话初始化）、`assistant`（含文本与 tool_use）、`user`（tool_result）、以及最终的 `result`。这意味着你能在 Agent 思考的**同时**就抓到它读了哪些文件、跑了什么命令——这正是「巡检舰队」实时监控的基础。

```bash
# 看一眼 json 模式返回的结构
claude -p "用一句话总结当前目录是什么项目" --output-format json | jq .
```

**预期结果**：输出一个对象，关键字段形如：

```json
{
  "type": "result",
  "subtype": "success",
  "is_error": false,
  "duration_ms": 4231,
  "num_turns": 2,
  "cost_usd": 0.0123,
  "result": "这是一个用 Node.js 编写的 TODO CLI 工具项目……",
  "session_id": "a1b2c3d4-..."
}
```

**常见问题**：

- *报错 `--output-format requires --verbose`*？部分版本对 `stream-json` 要求同时加 `--verbose` 才会吐出完整事件流。加上即可：`--output-format stream-json --verbose`。
- *`json` 模式拿到的 `result` 里混了 markdown 围栏*？这是模型把 JSON 包进 ` ```json ... ``` ` 导致的，下一步专门解决。

### 步骤二：用 `jq` 从 `stream-json` 精准抽取结果

巡检舰队的每个「哨兵」都要输出机器可读的结果。先掌握从流里捞「最终结论」与「工具调用记录」的固定写法：

```bash
# 只取最终 result 事件
claude -p "列出本仓库的入口文件，用一句话回答" \
  --output-format stream-json --verbose \
  | jq -c 'select(.type=="result")'

# 取整个过程中 Agent 读取过的文件（来自 tool_use 事件）
claude -p "浏览 src 目录结构" \
  --output-format stream-json --verbose \
  | jq -r 'select(.type=="assistant") 
          | .message.content[]? 
          | select(.type=="tool_use" and .name=="Read") 
          | .input.file_path'
```

**预期结果**：第一条命令打印一行压缩 JSON（含成本与耗时）；第二条逐行打印 Agent 实际打开的每个文件路径。后者对「审计 Agent 到底看了什么」极有价值——巡检报告里附上「本次扫描读取的文件清单」能显著提升可信度。

**常见问题**：流式输出在管道下游被缓冲、`jq` 收不到数据。解决：用 `stdbuf -oL` 或 `unbuffer` 关闭缓冲——`stdbuf -oL claude -p ... --output-format stream-json --verbose | jq ...`。

### 步骤三：强制结构化输出——让哨兵吐干净 JSON

聚合的前提是每个哨兵的输出**结构一致**。靠 `--append-system-prompt` 注入硬约束，并在出口处剥离可能的 markdown 围栏：

```bash
#!/usr/bin/env bash
# run-one.sh <dimension> —— 跑单个维度的哨兵，输出干净 JSON
set -euo pipefail
DIM="$1"
TARGET_REPO="${2:-$HOME/projects/sample-repo}"

SYS='你是代码健康巡检哨兵。只输出一个合法 JSON 对象，禁止任何 markdown 围栏、禁止任何散文解释。JSON schema 固定为：
{"dimension": string, "score": number(0-100), "severity": "high"|"medium"|"low", "findings": [{"file": string, "issue": string, "suggestion": string}], "summary": string}'

RAW=$(claude -p "维度=$DIM。扫描仓库 $TARGET_REPO，按 schema 输出 JSON。" \
  --output-format json \
  --append-system-prompt "$SYS" \
  --max-turns 6 \
  --allowedTools "Read,Grep,Glob" \
  | jq -r '.result')

# 防御性：剥离可能的 ```json 围栏后再次校验
echo "$RAW" | sed 's/^```json//I; s/^```//; s/```$//' | jq .
```

四个细节决定了这段的健壮性：

1. `--max-turns 6`：给巡检哨兵一个**回合上限**，防止它在超大仓库里无限读文件把预算烧光。这是舰队自动化的成本护栏。
2. `--allowedTools "Read,Grep,Glob"`：**把哨兵锁定为只读**。巡检不该改代码，白名单从根上杜绝了误删文件。
3. `--append-system-prompt`：不改 Agent 底层行为，只叠加一条「输出契约」，比改系统提示更安全、可复用。
4. `sed` 剥围栏 + `jq .`：双重保险。若模型仍偷偷加围栏，`sed` 削掉；若仍非法 JSON，`jq .` 会非零退出，被 `set -e` 捕获。

**预期结果**：脚本退出码 0，stdout 是一个被 `jq` 美化、结构合法的 JSON 对象。

**常见问题**：

- *`jq: parse error`*：模型没遵守契约。对策：把 schema 再写细、把「禁止围栏」移到 `--append-system-prompt` 最前面；或对 `result` 先 `jq -R` 取原文、用 `sed`/`awk` 截取首个 `{` 到末个 `}`。
- *退出码非零导致整条管道崩*：在舰队的编排层用「容错聚合」（下一步）而非 `set -e` 一刀切。

### 步骤四：并行扇出——让 N 个哨兵同时巡逻

五个维度串行跑会浪费等待时间。用 `xargs -P` 做并发扇出，每个维度一个进程：

```bash
#!/usr/bin/env bash
# fan-out.sh —— 并行派出五个哨兵
set -uo pipefail   # 注意：去掉 -e，让单哨兵失败不连累全局
DATE=$(date +%Y%m%d-%H%M)
OUTDIR="reports/$DATE"
mkdir -p "$OUTDIR"

DIMENSIONS="security techdebt performance dependencies coverage"
TARGET="$HOME/projects/sample-repo"

echo "$DIMENSIONS" | tr ' ' '\n' \
  | xargs -P 5 -I {} bash -c '
      dim="{}"
      ./run-one.sh "$dim" "'"$TARGET"'" > "'"$OUTDIR"'/$dim.json" 2> "'"$OUTDIR"'/$dim.err"
      echo "[done] $dim -> $?"
    '

echo "--- 扇出完成，成功 $(ls "$OUTDIR"/*.json 2>/dev/null | wc -l) 份 ---"
```

**预期结果**：约等于「最慢那个哨兵」的耗时（而非五个之和）。`reports/20260710-0217/` 下出现 `security.json`、`techdebt.json` 等文件，每个都是合法 JSON。

**常见问题**：

- *并发太高触发 429 限流*：把 `-P 5` 降到 `-P 3`，或在 `run-one.sh` 顶部加 `sleep $((RANDOM % 5))` 错峰。
- *某个哨兵崩了、`.json` 是空文件*：因为去掉了 `set -e`，崩的那份 `.err` 里有原因、其它四份照常产出。下一步聚合时跳过非法文件即可。

### 步骤五：cron 定时 + 非交互鉴权（舰队的心跳）

交互登录靠浏览器 OAuth，cron 里没人点「允许」。两条路：

```bash
# 路线 A：API Key（CI/调度最省事）
export ANTHROPIC_API_KEY="sk-ant-..."
# 写进 ~/.bashrc 不够——cron 环境极简，要在 crontab 顶部或脚本内显式 export

# 路线 B：长期 OAuth token
claude setup-token   # 交互式跑一次，输出一行 token
# 然后 export CLAUDE_CODE_OAUTH_TOKEN="那串token"
```

cron 的两个工程要点：**避开整点**（所有定时任务挤在 `0 2 * * *` 会撞车）和**显式补全环境**（cron 的 `PATH` 往往不含 `node`/`claude`）：

```cron
# crontab -e
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/home/gprshome/.npm-global/bin
ANTHROPIC_API_KEY=sk-ant-xxxxxxxx

# 每天凌晨 2:17 跑巡检（错峰，非整点）
17 2 * * * cd /home/gprshome/patrol-fleet && ./run-patrol.sh >> logs/cron.log 2>&1
# 每周一 7:23 跑一次「依赖升级评估」长任务
23 7 * * 1 cd /home/gprshome/patrol-fleet && ./run-one.sh dependencies >> logs/deps-weekly.log 2>&1
```

`run-patrol.sh` 是舰队总指挥，串起扇出与聚合：

```bash
#!/usr/bin/env bash
# run-patrol.sh
set -uo pipefail
cd "$(dirname "$0")"
DATE=$(date +%Y%m%d-%H%M)
echo "[$(date -Iseconds)] 巡检开始 run=$DATE"
./fan-out.sh "$DATE"
./aggregate.sh "$DATE"
echo "[$(date -Iseconds)] 巡检结束 run=$DATE"
```

**预期结果**：次日凌晨 2:17，`logs/cron.log` 出现「巡检开始/结束」记录，`reports/` 多一个带时间戳的目录。

**常见问题**：

- *cron 到点不跑、日志为空*：九成是 `PATH` 问题，`claude`/`node` 找不到。先在终端 `which claude` 拿绝对路径，写进 crontab 的 `PATH` 或脚本里用绝对路径调用。
- *鉴权失败 `401`*：`ANTHROPIC_API_KEY` 没被 cron 读到。确认它写在 crontab 顶部或 `run-patrol.sh` 内部 `export`，而非只写在 `~/.bashrc`（cron 不加载它）。

### 步骤六：聚合 Agent——把五份 JSON 炼成一份报告

五个哨兵各自只看一个面，需要有人「俯瞰全局」。再用一个 `-p` Agent 读全部 JSON、产出统一报告：

```bash
#!/usr/bin/env bash
# aggregate.sh <DATE>
set -euo pipefail
DATE="$1"
OUTDIR="reports/$DATE"

# 合并所有合法 JSON 为一个数组，跳过损坏的（容错聚合）
jq -s '[.[] | select(.dimension != null)]' "$OUTDIR"/*.json > "$OUTDIR/_merged.json"

claude -p "$(cat <<EOF
你是代码健康总指挥。下面是 N 个维度哨兵的 JSON 汇总（_merged.json 内容）：

$(cat "$OUTDIR/_merged.json")

请输出一份 Markdown 巡检报告，必须包含：1) 总体健康分（各维度 score 加权）；2) 按 severity 倒序排列的高危问题清单（标注文件与建议）；3) 与上一份报告 reports/ 目录中最新旧报告的「日环比」变化（新增/已解决）；4) 一段给团队负责人的三句话结论。
EOF
)" \
  --append-system-prompt "报告用简体中文，务实，不要客套。" \
  --max-turns 4 \
  --allowedTools "Read" \
  > "$OUTDIR/REPORT.md"

echo "报告已生成：$OUTDIR/REPORT.md"
```

注意聚合 Agent 的 `--allowedTools "Read"`：它只需要读旧报告做日环比，**不需要写、不需要 grep 全仓**，权限收得越紧越安全。

**预期结果**：`reports/20260710-0217/REPORT.md` 是一份带总体分、高危清单、日环比、结论的完整报告，可直接进群推送或转 PDF。

**常见问题**：

- *日环比找不到旧报告*：第一次运行时 `reports/` 里没有历史，Agent 会如实写「无历史可比」。从第二天起才有环比。
- *`_merged.json` 因某份损坏 JSON 导致 `jq -s` 报错*：`fan-out.sh` 已保证每份都过 `jq .` 校验才会落盘，损坏的不会进目录，故安全。

---

## 实战案例：patrol-fleet 完整项目

把上面六步组装成一个可克隆即用的项目。目录结构：

```
patrol-fleet/
├── run-patrol.sh          # 总指挥：扇出 → 聚合
├── fan-out.sh             # 并行派出哨兵
├── run-one.sh             # 单哨兵：输出干净 JSON
├── aggregate.sh           # 聚合 Agent：出 REPORT.md
├── dimensions/            # 每个维度的 prompt 模板（可选，让 run-one 读它）
│   ├── security.md
│   ├── techdebt.md
│   ├── performance.md
│   ├── dependencies.md
│   └── coverage.md
├── crontab                # cron 配置样例
├── logs/                  # cron.log / 各哨兵 err
└── reports/               # 按时间戳归档：REPORT.md + 各维度 json
```

`dimensions/security.md` 示例（让哨兵更聚焦，比靠系统提示更可维护）：

```markdown
你是安全哨兵。重点检查：
- 硬编码的密钥、token、密码（含 .env 误提交）
- 危险函数：eval、exec、反序列化、SSRF 入口
- SQL 拼接、命令拼接
- 依赖里的已知 CVE（若能从 lockfile 推断）
按既定 JSON schema 输出，severity 严格分级。
```

把 `run-one.sh` 改成读取模板：`PROMPT_BODY=$(cat "dimensions/$DIM.md")`，再把 `$PROMPT_BODY` 拼进 `-p`。这样**新增一个巡检维度只需丢一个 `.md` 文件**，零改代码。

一键验证（不依赖 cron，先手动跑通）：

```bash
cd ~/patrol-fleet
chmod +x *.sh
./run-patrol.sh             # 立刻跑一次，确认 reports/ 下产出 REPORT.md
cat reports/$(ls reports/ | tail -1)/REPORT.md
```

跑通后再 `crontab crontab` 装上定时，舰队就正式值班了。

**进阶扩展**：

- **结果推送**：在 `run-patrol.sh` 末尾加 `curl` 把 `REPORT.md` 发到飞书/钉钉/Slack webhook，实现「早上一条巡检播报」。
- **失败重试**：对 `run-one.sh` 套一层 `retry`（最多 2 次），提升健壮性。
- **趋势看板**：用 `jq` 每天抽出总体分写进 `trend.csv`，配 `gnuplot` 或丢给可视化 MCP 画健康分走势线。
- **分级告警**：聚合后若 `severity==high` 数量超阈值，用另一个 `-p` Agent 起草「立即处置工单」并 @ 负责人。

---

## 巡检舰队工作流程图

```mermaid
flowchart TD
    CRON["cron 定时触发<br/>每日 02:17"] --> RUN["run-patrol.sh 总指挥"]
    RUN --> FAN["fan-out.sh 并行扇出<br/>xargs -P 5"]

    FAN --> S1["security 哨兵<br/>--allowedTools Read,Grep,Glob"]
    FAN --> S2["techdebt 哨兵"]
    FAN --> S3["performance 哨兵"]
    FAN --> S4["dependencies 哨兵"]
    FAN --> S5["coverage 哨兵"]

    S1 --> J1["security.json<br/>--output-format json"]
    S2 --> J2["techdebt.json"]
    S3 --> J3["performance.json"]
    S4 --> J4["dependencies.json"]
    S5 --> J5["coverage.json"]

    J1 & J2 & J3 & J4 & J5 --> MERGE["jq -s 合并<br/>跳过损坏文件"]
    MERGE --> AGG["聚合 Agent<br/>读旧报告做日环比"]
    AGG --> REP[("REPORT.md<br/>总体分+高危+环比+结论")]
    REP --> PUSH["webhook 推送群消息"]

    style CRON fill:#fde68a
    style REP fill:#bbf7d0
    style AGG fill:#bfdbfe
```

---

## 延伸阅读

- 官方 Headless 文档 `code.claude.com/docs/en/headless` —— `-p`、`--output-format`、SDK 编程调用的权威说明。
- 官方 CLI 参考 `code.claude.com/docs/en/cli-reference` —— 全部 flag（`--max-turns`、`--allowedTools`、`--resume`、`--permission-mode`）逐条解释。
- `backgroundclaude.com/blog/stream-json` —— 专门讲 `stream-json` 第三种输出格式的事件结构与解析技巧。
- Anthropic Cookbook 里的 headless 示例 —— CI/CD 集成、多 Agent 编排的真实代码片段。

---

## 训练营适用性评估

- **适合类型**：DevOps / SRE 方向训练营、后端工程提效训练营、AI 工程化实战营。已具备 Claude Code 交互式基础的中级学员最佳。
- **教学时长**：建议 2 课时（90 分钟）+ 1 课时实操。第 1 课时讲三种输出格式与 `jq` 解析；第 2 课时讲并行扇出、cron、聚合；实操课要求学员在自己的仓库上跑通一支五维度舰队并产出 `REPORT.md`。
- **验收标准**：能独立解释 `text/json/stream-json` 差异；能用 `--allowedTools` 把哨兵锁成只读；能在 cron 里成功触发一次非交互巡检并拿到结构化报告。
- **难度拐点**：第六步的「容错聚合」与日环比逻辑对 shell/jq 不熟的学员是难点，讲师可提供 `_merged.json` 的现成片段降低门槛。

---

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方无头模式/`-p`/输出格式的权威文档
2. [CLI reference — Claude Code Docs](https://code.claude.com/docs/en/cli-reference) — 全部 CLI flag（`--output-format`、`--max-turns`、`--allowedTools`、`--append-system-prompt`）逐条说明
3. [Claude Code stream-json: the output format that changes everything — BackgroundClaude](https://backgroundclaude.com/blog/stream-json) — `stream-json` 换行分隔事件结构与 `jq` 解析技巧
4. [Claude Code Headless Mode: The Definitive Guide to claude -p — BuildThisNow](https://www.buildthisnow.com/blog/guide/development/claude-code-headless-mode) — 含 `jq` 解析、CLI-vs-SDK 决策与真实 shell 片段
5. [Using Claude Code Programmatically — GitHub Gist (JacobFV)](https://gist.github.com/JacobFV/2c4a75bc6a835d2c1f6c863cfcbdfa5a) — `--print --output-format` 绕过 TUI 的完整实践指南

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