---
name: book-ingest
description: >-
  把用户提供的书源文件(epub / mobi / azw / azw3 / prc / md / markdown / 文字型 pdf)提取并按章切分,
  在 echo-reading 项目里生成 books/书名/chNN/raw.md(**只灌纯原文**)和 progress.md,并把源文件留档进
  books/书名/。当用户给出电子书文件、Markdown、文字型 PDF、下载链接,或说「导入这本书 / 加一本书 /
  把这本书切好 / 读这本新书 / 这个文件能不能拆成章」时,务必使用本 skill。PDF 必须先探测;扫描型/图片型
  PDF 暂不支持,需停止并告知用户。入库只到原文就位为止。
---

# book-ingest - 书源提取并切分入库

把一个书源文件变成 `books/<书名>/` 下一章一个目录 `chNN/`,原文落进 `chNN/raw.md`,
**只灌纯原文**(H1 标题 + 正文),供 echo-reading 逐段深读。本 skill 的职责到原文就位为止。
9 段笔记骨架由 `chapter-split` 在用户读该章时铺进同一个 `chNN/`。`raw.md` 是这章的源头底本,
split 只读它、不改它。

## 支持范围

- `epub`
- `mobi / azw / azw3 / prc`
- `md / markdown / txt`
- 文字型 `pdf`

PDF 必须先跑 `inspect_book.py`。如果报告显示疑似扫描型/图片型 PDF,停止入库,告诉用户当前暂不支持,
需要先 OCR 或换文字版 PDF/epub/md。

## 最重要的一条:切分策略不写死

不同书结构天差地别。没有一套正则能通吃,所以本 skill 不提供"万能切分器"。流程是:

```text
inspect 看结构 -> agent 判断切法 -> 写这本书专用的一次性切分脚本 ->
agent 自检 chapters.json -> verify_chapters.py 机器校验 ->
校验不通过就修正并重跑 -> 校验通过后正式写 chNN/raw.md + progress.md
```

固化下来的只有三段确定性工作:

- `scripts/inspect_book.py`:探测格式、结构、PDF 是否文字型。
- `scripts/verify_chapters.py`:校验切分结果是否覆盖源文本。
- `scripts/write_chapters.py`:写 `books/<书名>/chNN/raw.md` 和 `progress.md`。

中间"怎么切"仍然是判断。每本书写一个专用的一次性切分脚本,输出 `[{"title","body"}, ...]` JSON。

## 依赖

需要一个装好依赖的 python3:

```bash
pip install EbookLib beautifulsoup4 mobi pypdf
```

`EbookLib` 用于 epub,`mobi` 用于 mobi/azw,`pypdf` 用于文字型 PDF。Markdown 不需要额外依赖。
脚本文件名 `inspect_book.py` 不要改成 `inspect.py`,否则会盖掉标准库 `inspect`。

## 工作流

记 `SKILL` 为本 skill 目录,`ROOT` 为 echo-reading 项目根。

### 临时工作目录

每次入库先创建唯一临时目录,一次性脚本和中间产物都放这里,不要放进 `SKILL/scripts/` 或项目根目录:

```bash
WORK=$(mktemp -d "${TMPDIR:-/tmp}/book-ingest-<书名slug>.XXXXXX")
```

固定文件名:

- `$WORK/source.<ext>`:下载来的书源文件;用户给本地文件时可不复制,直接记录原路径。
- `$WORK/inspect.log`:探测报告。
- `$WORK/split_this_book.py`:这本书专用的一次性切分脚本。
- `$WORK/chapters.json`:切分脚本输出。
- `$WORK/exclude_patterns.txt`:可选,声明性排除规则。
- `$WORK/verify.log`:完整性校验输出。

清理规则:

- 成功写入 `books/<书名>/` 且源文件已 `--save-source` 留档后,删除整个 `$WORK`:
  `rm -rf "$WORK"`。
- 校验或写入失败时,不要写入最终产物;如果已经写了一部分新书目录,删除这次新建的 `books/<书名>/`。
- 失败需要排查时可以临时保留 `$WORK`,但必须在回报里写明路径;问题处理完后删除。

### 第 0 步:拿到本地文件

- 用户直接给文件:记下路径,直接进第 1 步。
- 用户给下载链接:先下到 `$WORK/source.<ext>`。GitHub `blob` 链接要转成 raw:
  `https://github.com/<u>/<repo>/blob/<branch>/<path>` -> `https://github.com/<u>/<repo>/raw/<branch>/<path>`。
- 下载后用 `file <path>` 确认不是 HTML 错页。
- 最终源文件必须通过 `--save-source` 留档进 `books/<书名>/`。

### 第 1 步:探测格式和结构

```bash
python3 SKILL/scripts/inspect_book.py <书源文件> | tee "$WORK/inspect.log"
```

读报告后判断:

- epub/mobi 多 spine 文档:多半一文档一章,但仍要看 TOC 和正文。
- epub/mobi 单文档 + TOC 锚点:可按锚点位置切。
- epub/mobi 单文档 + 正文标记:可按 `第N章`、`Chapter N` 等正则切。
- Markdown:优先按稳定标题层级切,常见是 H1=书名、H2=章。
- TXT/纯文本:先找目录、作品分界、章名序列;如果混入多部作品/导读/附录,识别深读主体并声明性排除非主体内容。
- 文字型 PDF + TOC/书签:优先按 TOC 页码范围切,再检查正文是否错页。
- 文字型 PDF 无可靠 TOC:按正文里的章节标记切,不要默认一页一章;特别注意页眉页脚、页码、断行、脚注错位。
- 扫描型 PDF:停止,告知用户暂不支持。
- 都不干净:agent 可列章节边界或混合策略;只有无法判断版本/粒度时才问用户。

### 第 2 步:定书名、版本和深读粒度

- 书名别盲信元数据。从正文、封面、TOC 判断;无法可靠判断时问用户。
- 如果同一文件里有多个版本、译本、前言、附录、版权页、目录页、广告页、其他作品等,先判断哪些是深读正文。
- 无法从文本本身判断用户要读哪个版本/粒度时才问用户。
- 可排除非深读内容,但必须在 `verify_chapters.py` 命令里用声明性排除项登记,不能在切分脚本里静默吞掉。

### 第 3 步:写一次性切分脚本

读 `SKILL/references/split-examples.md`,挑最接近的范例改成这本书专用脚本,保存到
`$WORK/split_this_book.py`。脚本只做一件事:

```text
load/source_text 出文本 -> 按这本书的结构切 -> 输出 [{"title","body"}, ...] JSON 到 stdout
```

优先从 `source_lib.py` 导入:

```python
import sys
sys.path.insert(0, "SKILL/scripts")
from source_lib import load, plain, source_text
```

旧脚本 `from ebook_lib import load, plain` 仍可用,但新脚本优先用 `source_lib`。

### 第 4 步:生成 chapters.json 并由 agent 自检

先把一次性脚本输出成 JSON,不要直接写入:

```bash
python3 "$WORK/split_this_book.py" > "$WORK/chapters.json"
```

agent 自行检查,不向用户停顿确认:

- 章节数是否合理。
- 首章、尾章、异常章节是否有标题和正文。
- 是否有空章节、目录残留、页眉页脚、HTML 标签碎片。
- 标题序号是否断裂或重复。
- PDF/Markdown 是否有明显断行、页码、脚注错位。

发现问题就改脚本重跑。只有版本/粒度无法判断时才问用户。

### 第 5 步:机器完整性校验,硬门槛

必须运行:

```bash
python3 SKILL/scripts/verify_chapters.py <书源文件> --chapters "$WORK/chapters.json" | tee "$WORK/verify.log"
```

校验不通过时,不得写入 `books/<书名>/`。agent 必须查看缺失/重复片段,判断原因:

- 切分脚本漏掉正文。
- 清洗过度。
- PDF 抽取导致断裂。
- 标题/正文顺序错乱。
- 缺失片段其实是版权页、目录页、广告页、译者说明等非深读内容。

如果是正文问题,修正切分脚本后重跑。如果是合理非深读内容,用声明性排除项后重跑:

```bash
python3 SKILL/scripts/verify_chapters.py <书源文件> --chapters "$WORK/chapters.json" \
  --exclude-regex "版权声明.*?版权所有" \
  --exclude-between "目录" "第一章"
```

允许的声明性排除:

- `--exclude-literal "<完整文本>"`
- `--exclude-between "<起点文本>" "<终点文本>"`
- `--exclude-regex "<正则>"`
- `--exclude-file <一行一个正则的文件>`

一直修正并重跑,直到 `verify_chapters.py` 通过。不得用降低阈值掩盖明显漏文;PDF 因抽取噪声需要调阈值时,
必须在回报里说明。

### 第 6 步:正式写原文

校验通过后再写:

```bash
python3 SKILL/scripts/write_chapters.py \
  --book <书名> --root ROOT \
  --base "<底本说明>" --save-source <源文件路径> \
  --in "$WORK/chapters.json"
```

`write_chapters.py` 会:

- 建 `ROOT/books/<书名>/ch01/ ... chNN/`。
- 写每章 `raw.md`:H1 标题 + 纯原文正文。
- 写 `progress.md`。
- 通过 `--save-source` 保存源文件。
- 遇到空正文会直接失败;不得写入占位原文。
- 默认不覆盖已存在的 `chNN/raw.md`;重灌才加 `--force`。

### 第 7 步:回报

简短告诉用户:

- 书名和格式。
- 共几章。
- 写入路径。
- 源文件留档路径。
- 校验结果和覆盖率。
- 声明排除了哪些非深读内容。
- 可以开始读第几章。
