# 自动拆书流水线·三文件代码·book_splitter+notion_writer+auto_dishu·2026-05-20

```jsx
HLDP://juzi/auto-dishu/tools/2026-05-20
├── 新建文件夹: ~/chenxing-aircraft/tools/
├── 三个文件: book_splitter.py / notion_writer.py / auto_dishu.py
└── 妈妈操作: 先建文件夹，再按顺序粘贴三段代码
```

---

## 准备：建文件夹

```bash
mkdir -p ~/chenxing-aircraft/tools
touch ~/chenxing-aircraft/tools/__init__.py
echo "tools 文件夹创建完成 ✅"
```

---

## 第一段：book_[splitter.py](http://splitter.py)（切书工具）

```bash
cat > ~/chenxing-aircraft/tools/book_splitter.py << 'ENDOFFILE'
#!/usr/bin/env python3
"""
书籍分块工具 · book_splitter.py
把 txt 文件按章节切块存入知识库
用法: python3 tools/book_splitter.py --file 折春漪.txt --book 折春漪
"""
import argparse
import re
import sqlite3
import sys
from pathlib import Path

BASE_DIR = Path(__file__).parent.parent
DB_PATH = BASE_DIR / "chenxing.db"

def split_by_chapters(text: str, chapters_per_chunk: int = 2) -> list:
    """按章节标题切割"""
    patterns = [
        r'第[零一二三四五六七八九十百千0-9]+章[\s　]',
        r'第[零一二三四五六七八九十百千0-9]+章$',
        r'Chapter\s+\d+',
    ]
    combined = '|'.join(f'(?:{p})' for p in patterns)
    positions = [(m.start(), m.group().strip()) for m in re.finditer(combined, text, re.MULTILINE)]

    if not positions:
        print("⚠️  未找到章节标题，按5000字切割")
        chunks = []
        for i, start in enumerate(range(0, len(text), 5000)):
            chunks.append({"title": f"第{i+1}块", "content": text[start:start+5000]})
        return chunks

    chapters = []
    for i, (pos, title) in enumerate(positions):
        end = positions[i+1][0] if i+1 < len(positions) else len(text)
        chapters.append({"title": title, "content": text[pos:end]})

    # 合并每 chapters_per_chunk 章为一块
    chunks = []
    for i in range(0, len(chapters), chapters_per_chunk):
        batch = chapters[i:i+chapters_per_chunk]
        if len(batch) > 1:
            combined_title = f"{batch[0]['title']}-{batch[-1]['title']}"
        else:
            combined_title = batch[0]['title']
        chunks.append({
            "title": combined_title,
            "content": "\n\n".join(c["content"] for c in batch)
        })
    return chunks

def import_to_knowledge(book_name: str, chunks: list, user_id: int = 1):
    """导入到知识库"""
    conn = sqlite3.connect(str(DB_PATH))
    imported = skipped = 0
    try:
        for chunk in chunks:
            title = f"《{book_name}》{chunk['title']}"
            existing = conn.execute(
                "SELECT id FROM knowledge WHERE title=? AND user_id=?", (title, user_id)
            ).fetchone()
            if existing:
                skipped += 1
                continue
            conn.execute(
                """INSERT INTO knowledge (user_id,title,content,category,tags,source)
                   VALUES (?,?,?,'book',?,?)""",
                (user_id, title, chunk["content"], book_name, f"书籍分块·{book_name}")
            )
            imported += 1
        conn.commit()
        print(f"✅ 导入完成：新增 {imported} 块，跳过 {skipped} 块（已存在）")
        return imported
    finally:
        conn.close()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="书籍分块导入知识库")
    parser.add_argument("--file", required=True, help="书籍 txt 文件路径")
    parser.add_argument("--book", required=True, help="书名（不含《》）")
    parser.add_argument("--chunks", type=int, default=2, help="每块几章（默认2）")
    parser.add_argument("--user_id", type=int, default=1, help="用户ID（默认1）")
    args = parser.parse_args()

    fp = Path(args.file)
    if not fp.exists():
        print(f"❌ 文件不存在：{args.file}")
        sys.exit(1)

    text = fp.read_text(encoding="utf-8", errors="ignore")
    print(f"📚 书名：《{args.book}》")
    print(f"📏 总字数：{len(text):,}")

    chunks = split_by_chapters(text, args.chunks)
    print(f"✂️  共切割为 {len(chunks)} 块（每块约 {args.chunks} 章）")

    import_to_knowledge(args.book, chunks, args.user_id)
    print(f"\n下一步运行：")
    print(f"python3 tools/auto_dishu.py --book \"{args.book}\" --end_chapter {len(chunks)*args.chunks}")
ENDOFFILE
echo "book_splitter.py 创建完成 ✅"
```

---

## 第二段：notion_[writer.py](http://writer.py)（Notion写入工具）

```bash
cat > ~/chenxing-aircraft/tools/notion_writer.py << 'ENDOFFILE'
#!/usr/bin/env python3
"""
Notion 写入工具 · notion_writer.py
"""
import os
import requests

NOTION_API_KEY = os.environ.get("NOTION_API_KEY", "")
NOTION_VERSION = "2022-06-28"
BASE_URL = "https://api.notion.com/v1"

def _headers():
    return {
        "Authorization": f"Bearer {NOTION_API_KEY}",
        "Content-Type": "application/json",
        "Notion-Version": NOTION_VERSION,
    }

def _md_to_blocks(text: str) -> list:
    """Markdown 转 Notion blocks（简化版）"""
    blocks = []
    for line in text.split("\n"):
        if not line.strip():
            continue
        if line.startswith("# "):
            blocks.append({"type":"heading_1","heading_1":{"rich_text":[{"type":"text","text":{"content":line[2:100]}}]}})
        elif line.startswith("## "):
            blocks.append({"type":"heading_2","heading_2":{"rich_text":[{"type":"text","text":{"content":line[3:100]}}]}})
        elif line.startswith("### "):
            blocks.append({"type":"heading_3","heading_3":{"rich_text":[{"type":"text","text":{"content":line[4:100]}}]}})
        elif line.startswith("- "):
            blocks.append({"type":"bulleted_list_item","bulleted_list_item":{"rich_text":[{"type":"text","text":{"content":line[2:2000]}}]}})
        else:
            blocks.append({"type":"paragraph","paragraph":{"rich_text":[{"type":"text","text":{"content":line[:2000]}}]}})
    return blocks

def create_notion_page(title: str, content: str, parent_page_id: str) -> bool:
    """在指定父页面下创建新页面"""
    if not NOTION_API_KEY:
        print("⚠️  未配置 NOTION_API_KEY，跳过写入Notion")
        return False

    blocks = _md_to_blocks(content)
    data = {
        "parent": {"page_id": parent_page_id},
        "properties": {"title": {"title": [{"type": "text", "text": {"content": title[:255]}}]}},
        "children": blocks[:100]
    }
    try:
        r = requests.post(f"{BASE_URL}/pages", headers=_headers(), json=data, timeout=30)
        if r.status_code in [200, 201]:
            page_id = r.json()["id"]
            # 超过100个block追加
            if len(blocks) > 100:
                for i in range(100, len(blocks), 100):
                    requests.patch(
                        f"{BASE_URL}/blocks/{page_id}/children",
                        headers=_headers(),
                        json={"children": blocks[i:i+100]},
                        timeout=30
                    )
            return True
        else:
            print(f"Notion API 错误 {r.status_code}: {r.text[:300]}")
            return False
    except Exception as e:
        print(f"请求失败: {e}")
        return False
ENDOFFILE
echo "notion_writer.py 创建完成 ✅"
```

---

## 第三段：auto_[dishu.py](http://dishu.py)（主流水线）

```bash
cat > ~/chenxing-aircraft/tools/auto_dishu.py << 'ENDOFFILE'
#!/usr/bin/env python3
"""
自动拆书流水线 · auto_dishu.py
桔子×晨星 · 霜砚设计 · 2026-05-20

用法:
  python3 tools/auto_dishu.py --book 折春漪 --end_chapter 400
  python3 tools/auto_dishu.py --book 折春漪 --start_chapter 301 --end_chapter 400
"""
import argparse
import asyncio
import os
import sqlite3
import sys
from pathlib import Path

from openai import AsyncOpenAI

BASE_DIR = Path(__file__).parent.parent
DB_PATH = BASE_DIR / "chenxing.db"

DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
NOTION_API_KEY = os.environ.get("NOTION_API_KEY", "")
NOTION_DISHU_PAGE_ID = os.environ.get("NOTION_DISHU_PAGE_ID", "")

DISHU_SYSTEM = "你是晨星，桔子妈妈的拆书宝宝。请用场景颗粒拆书法v2.0认真分析，格式严格按三模块输出，缺一不可。"

DISHU_PROMPT = """请用「场景颗粒拆书法v2.0」分析以下章节内容。

拆书格式（三模块·缺一不可）：

【模块一·逐章场景表格】
| 场景序号 | 场景边界 | 人物动作/对话 | 情绪质地 | 读者感受 |
（每个场景一行）

【模块二·章末规律锁定】
- 本章节奏类型：
- 情绪弧线：（开头→过程→结尾）
- 卡点设计：
- 可复用规律：（至少2条）

【模块三·期待点库存】
- 新增期待点：
- 消解期待点：
- 遗留悬念：

待分析内容：
{content}"""

def db_get_chunks(book_name: str) -> list:
    """从知识库取该书所有块"""
    conn = sqlite3.connect(str(DB_PATH))
    conn.row_factory = sqlite3.Row
    try:
        rows = conn.execute(
            "SELECT id, title, content FROM knowledge WHERE tags LIKE ? AND category='book' ORDER BY id",
            (f"%{book_name}%",)
        ).fetchall()
        if not rows:
            rows = conn.execute(
                "SELECT id, title, content FROM knowledge WHERE title LIKE ? ORDER BY id",
                (f"%{book_name}%",)
            ).fetchall()
        return [{"id": r["id"], "title": r["title"], "content": r["content"]} for r in rows]
    finally:
        conn.close()

def db_update_progress(book_name: str, analyzed: int, last_chapter: str, status: str = "in_progress", total: int = 0):
    """更新 book_progress 表"""
    conn = sqlite3.connect(str(DB_PATH))
    try:
        existing = conn.execute("SELECT id FROM book_progress WHERE book_name=?", (book_name,)).fetchone()
        if existing:
            conn.execute(
                "UPDATE book_progress SET analyzed_chapters=?,last_chapter=?,status=?,updated_at=datetime('now') WHERE book_name=?",
                (analyzed, last_chapter, status, book_name)
            )
        else:
            conn.execute(
                "INSERT INTO book_progress(book_name,analyzed_chapters,last_chapter,status,total_chapters) VALUES(?,?,?,?,?)",
                (book_name, analyzed, last_chapter, status, total)
            )
        conn.commit()
    finally:
        conn.close()

def save_local(book_name: str, title: str, content: str):
    """本地备份"""
    out = BASE_DIR / "output" / book_name
    out.mkdir(parents=True, exist_ok=True)
    (out / f"{title[:80]}.md").write_text(f"# {title}\n\n{content}", encoding="utf-8")

async def analyze(content: str) -> str:
    client = AsyncOpenAI(api_key=DEEPSEEK_API_KEY, base_url="https://api.deepseek.com")
    resp = await client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": DISHU_SYSTEM},
            {"role": "user", "content": DISHU_PROMPT.format(content=content[:8000])}
        ],
        max_tokens=4096,
        temperature=0.7
    )
    return resp.choices[0].message.content

async def run(book_name: str, start_chapter: int, end_chapter: int):
    print(f"\n{'='*50}")
    print(f"🚀 自动拆书流水线")
    print(f"📚 《{book_name}》第{start_chapter}-{end_chapter}章")
    print(f"{'='*50}\n")

    chunks = db_get_chunks(book_name)
    if not chunks:
        print(f"❌ 知识库中未找到《{book_name}》")
        print("请先运行: python3 tools/book_splitter.py --file 书名.txt --book 书名")
        return

    print(f"✅ 找到 {len(chunks)} 个知识库块")
    total = len(chunks)
    analyzed = 0

    for i, chunk in enumerate(chunks):
        label = chunk["title"]
        print(f"\n[{i+1}/{total}] 分析中：{label}")
        print(f"  字数：{len(chunk['content']):,}")

        try:
            result = await analyze(chunk["content"])
            notion_title = f"《{book_name}》{label}·场景颗粒拆书"

            # 写Notion
            written = False
            if NOTION_DISHU_PAGE_ID and NOTION_API_KEY:
                from tools.notion_writer import create_notion_page
                written = create_notion_page(notion_title, result, NOTION_DISHU_PAGE_ID)
                if written:
                    print(f"  ✅ 写入Notion：{notion_title}")

            # 本地备份
            save_local(book_name, notion_title, result)
            if not written:
                print(f"  📁 已保存本地：output/{book_name}/")

            analyzed += 2
            db_update_progress(book_name, analyzed, label, "in_progress", end_chapter)

            print(f"  ⏱  等待3秒...")
            await asyncio.sleep(3)

        except Exception as e:
            print(f"  ❌ 出错：{e}")
            await asyncio.sleep(5)

    db_update_progress(book_name, analyzed, f"第{end_chapter}章", "completed", end_chapter)
    print(f"\n{'='*50}")
    print(f"🎉 全部完成！共分析 {analyzed} 章")
    if NOTION_DISHU_PAGE_ID:
        print(f"📝 结果已写入 Notion")
    print(f"📁 本地备份：output/{book_name}/")
    print(f"{'='*50}\n")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--book", required=True)
    parser.add_argument("--start_chapter", type=int, default=1)
    parser.add_argument("--end_chapter", type=int, required=True)
    args = parser.parse_args()
    asyncio.run(run(args.book, args.start_chapter, args.end_chapter))
ENDOFFILE
echo "auto_dishu.py 创建完成 ✅"
```

---

## 配置环境变量（在 .env 或 [config.py](http://config.py) 里加）

```bash
# 如果要写回Notion，需要配置这两个环境变量：
export NOTION_API_KEY="你的Notion集成Token"
export NOTION_DISHU_PAGE_ID="拆书结果要写入的Notion页面ID"

# 不配置的话结果会保存到本地 output/书名/ 文件夹，一样能用
```

---

## 完整使用流程

```bash
# 第一步：把书导入知识库
python3 tools/book_splitter.py --file ~/Downloads/折春漪.txt --book 折春漪

# 第二步：启动自动拆书（会跑完所有块）
python3 tools/auto_dishu.py --book 折春漪 --end_chapter 400

# 或者从301章开始拆到400章：
python3 tools/auto_dishu.py --book 折春漪 --start_chapter 301 --end_chapter 400

# 第三步：去睡觉 💤
# 拆完后去 Notion 或 output/折春漪/ 文件夹查看结果
```

---

## 验收清单

- [ ]  `python3 tools/book_splitter.py --file test.txt --book 测试` → 成功切块导入知识库
- [ ]  知识库面板能看到导入的条目（分类：书籍内容）
- [ ]  `python3 tools/auto_dishu.py --book 测试 --end_chapter 4` → 成功分析并保存
- [ ]  `output/测试/` 文件夹里有 .md 文件
- [ ]  拆书面板里能看到「测试」这本书的进度
- [ ]  流水线启动按钮 → 后台运行 → 拆书面板实时刷新进度

---

> 🤖 自动拆书流水线·三工具文件 · 晨星×桔子 · 霜砚出品 · 2026-05-20
> 

> 妈妈按一下启动，宝宝把整本书的记忆都装好 💛
>