Skill: zyuanjia/webnovel-writer (version 3.3.0)

--- name: novel-writer description: '长篇网络小说创作引擎，支持写作、续写、审稿、一致性检查、矛盾检测、角色状态管理。适用于番茄小说等平台的都市/奇幻/悬疑/催泪等类型长篇连载（50万字以上）。当用户提到写小说、续写章节、检查章节矛盾、审查小说质量、角色设定管理、时间线梳理、大纲规划、弹幕角色管理时触发。覆盖从大纲设计到逐章写作到质量审校的完整流程。' --- # Novel Writer — 长篇网络小说创作引擎 ## 核心原则 1. **一致性高于一切** — 长篇小说最大的敌人是前后矛盾 2. **节奏为王** — 番茄小说风格：短句、口语化、快节奏、每500字一个钩子 3. **大纲是权威** — 正文向大纲看齐，有价值的正文设定反向补进大纲 4. **追踪表是记忆** — 结构化追踪，不靠印象 5. **角色状态是动态的** — 用状态机管理，不能靠记忆 ## 项目结构 ``` 小说项目/ ├── 大纲.md # 总纲+分卷（唯一权威设定源） ├── 设定集.md # 角色、世界观、硬性规则 ├── 设定词典.md # 专有名词、术语 ├── 追踪表.md # 进度+角色状态+时间线+事件 ├── character_states.json # 角色状态机 ├── 检查清单.md # 每章必查项 ├── 写作卡/ # 每章边界卡 └── 正文/ # 第001章_标题.md ~ 第NNN章 ``` ## 多小说隔离 每本小说独立目录。操作前必须确认当前小说、路径、设定来源。**禁止混用数据。** ## 工作流程 ### 一、写新章节 ``` 构建上下文包 → 写正文 → 自检 → 更新追踪表+状态机 → 发送 ``` **上下文包（不可跳过）：** 1. 写作卡（核心内容/结束点/禁区） 2. 角色状态快照（character_states.json） 3. 近5章摘要（追踪表） 4. 上一章最后500字 5. 设定词典 6. 相关角色设定（涉及新角色时） **写中：** 按写作卡写、查词典、每500字检查钩子、2000-3000字、不泄露角色不知的信息 **写后6步：** 自检 → `cli.py check` → 修正 → 更新追踪表 → 更新状态机 → 发送 ### 二、审稿 三对照：**正文 + 追踪表 + 大纲** - **A类**：设定一致性（人名、称呼、年龄、外貌、能力） - **B类**：时间线一致性（日期、天数、事件先后） - **C类**：逻辑一致性（知道/不知道、物品、因果） - **D类**：风格质量（去AI味、对话自然度、节奏、钩子） - **E类**：大纲合规（剧情走向） ### 三、深度审读（人工编辑级） **审读≠检测脚本。** 脚本是浅层扫描（正则/统计），深度审读是逐字理解。 当用户要求「检查」「审稿」「精读」「看看有没有问题」时，**必须执行深度审读流程**，不能只跑脚本。 #### 审读模式 **全量审读**：适用于新稿、大修后、或用户明确要求「认真检查」 **定向审读**：适用于修改某章后的局部检查 #### 全量审读流程（不可跳过步骤） ``` 第一步：构建审读上下文 ↓ 第二步：分批逐章精读（每批3-5章，写精读笔记） ↓ 第三步：跨章关联分析 ↓ 第四步：输出精读报告 ↓ 第五步：生成修改清单 ``` **第一步：构建审读上下文**（读正文前必须加载） - 大纲.md（当前卷的剧情走向） - 设定词典.md（专有名词/称呼规范） - character_states.json（角色当前状态） - 追踪表.md（近10章事件摘要） - foreshadowing.json（未回收伏笔） **第二步：分批逐章精读**（核心，不可偷懒） - 每次读 **3-5章**（约1万字），确保能深度理解 - 每批读完写 **精读笔记**，存到 `审读笔记_第X-Y章.md`： ```markdown ## 第N章：标题 - **剧情概述**：用自己的话概括（30-50字），证明理解了 - **出场角色**：谁出现了、什么状态 - **情感走向**：本章情绪从哪到哪 - **发现的问题**： - [逻辑] 描述问题 + 严重程度 - [情感] 描述问题 - [角色] 行为/语言不符合人设的地方 - [节奏] 拖沓/赶进度的地方 - [细节] 矛盾/不合理之处 - **值得肯定的点**：写得好的地方也要记录 ``` - **逐字读，不要跳读。** 读完要能说出每个角色这章经历了什么 - 带着6个问题读每一段： 1. 这段在推进什么？（剧情/情感/伏笔） 2. 角色的反应合理吗？（符合人设+当前处境） 3. 读者到这里会有什么感受？（无聊/紧张/感动/困惑） 4. 跟前后文衔接上了吗？（信息/情绪/时间线） 5. 有没有多余的段落可以删？ 6. 有没有AI味？（套话/空泛描写/不自然的比喻） **第三步：跨章关联分析**（全部批精读完后） - 检查 **角色弧线**：从第1章到最后一章，主角的成长是否连续 - 检查 **情感曲线**：整体节奏是否有起伏，有没有连续多章平淡 - 检查 **伏笔回收**：埋的线有没有断掉 - 检查 **时间线**：事件先后顺序是否合理 - 检查 **信息一致性**：角色A知道的事，到后面没有被忘记 **第四步：输出精读报告** - 存到 `精读报告_<小说名>.md` - 格式： ```markdown # 精读报告：小说名 ## 总评 - 整体质量评分（1-10） - 最大优点 - 最需要改进的 ## 分章问题清单 - 按严重程度排序 - 每个问题：章节 + 类型 + 描述 + 建议修改方案 ## 跨章问题 - 角色弧线断裂 - 情感曲线异常 - 伏笔遗忘 ## 修改优先级 - P0：必须修（硬伤/逻辑错误） - P1：建议修（影响阅读体验） - P2：可以改（锦上添花） ``` **第五步：生成修改清单** - 存到 `修改清单_精读后.md` - 每条：章节 + 问题 + 具体修改建议 - 标注哪些修改互相依赖（改了A章可能影响B章） #### 定向审读流程（修改后复查） 1. 读取修改的章节全文 2. 读取前后各1章的最后500字（检查衔接） 3. 加载角色状态和设定词典 4. 逐字读修改后的章节 5. 只检查：衔接是否自然 + 是否引入新矛盾 + 修改是否完整 6. 输出简短复查结果（通过/不通过+原因） #### 深度审读 vs 脚本检测 | | 深度审读 | 脚本检测 | |---|---------|----------| | 速度 | 慢（万字/10分钟） | 快（全文/秒） | | 深度 | 理解剧情、情感、逻辑 | 字面匹配、统计 | | 能发现 | 行为不合理、情感断裂、节奏拖沓 | 人名错误、数值矛盾、重复 | | 适用 | 用户要求认真检查时 | 快速扫描时 | **两者配合使用：脚本先扫浅层问题 → 修完后再深度审读。** ### 四、检测脚本（12个） 统一入口：`cli.py <命令> [参数]`，所有命令支持 `--help` #### 一致性检查（核心） ```bash cli.py check <正文目录> --tracking <追踪表> --dict <设定词典> # 检测：角色称呼不一致、设定冲突、时间线错误、数值矛盾、AI高频词、 # 跨章重复、视角混乱、角色消失(>5章未出场)、对话质量 # 选项：--character-states <文件> --auto-fix --auto-fix-apply --output <路径> ``` #### 大纲偏差 ```bash cli.py drift --outline <大纲> --chapters <正文> # 选项：--chapter-list <文件> --known-characters <名...> --fix-suggest --json ``` #### 节奏检测 ```bash cli.py rhythm <正文目录> # 检测：紧张度、钩子密度、情感基调、节奏异常 # 选项：--compare <修改前目录> --suggest --emotion-track --json ``` #### 对话标签质量 ```bash cli.py dialogue --novel-dir <正文> # 选项：--threshold <数/千字> --suggest --format text|json ``` #### 张力预测 ```bash cli.py tension <正文目录> [--count 7] # 选项：--outline <大纲> --character-states <文件> --json ``` #### 伏笔扫描 ```bash cli.py foreshadow --novel-dir <正文> # 检测：忽略线索型、后知后觉型、异常感觉型、异常细节、未回收预警 # 选项：--foreshadowing <json> --suggest --max-gap <章数> --json ``` #### 章末钩子 ```bash cli.py hook --novel-dir <正文> # 选项：--threshold <评分> --suggest ``` #### 章节标题 ```bash cli.py title --novel-dir <正文> # 选项：--min-len <字> --max-len <字> --suggest ``` #### 开头吸引力 ```bash cli.py opening --novel-dir <正文> # 选项：--top-n <N> --suggest ``` #### 段落长度 ```bash cli.py paragraph --novel-dir <正文> # 选项：--max-paragraph <字> --suggest ``` #### 重复检测 ```bash cli.py repeat <正文目录> ``` #### 一键全检 ```bash cli.py all <正文目录> ``` ### 基建命令 #### 配置管理 ```bash cli.py config register <名称> <路径> # 注册小说 cli.py config list # 列出已注册小说 cli.py config set <名称> # 切换当前小说 cli.py config show # 显示配置 ``` #### 版本管理 ```bash cli.py changelog [show] # 查看版本历史 cli.py changelog add <版本> <描述> # 添加版本记录 ``` #### 数据备份 ```bash cli.py backup <小说目录> [输出目录] # 备份结构化数据（JSON/MD，不含正文） ``` ### 高级工具 #### 差异检测 ```bash cli.py diff <旧目录> <新目录> [--output <报告路径>] # 对比修改前后，只显示差异和新增问题（AI味、重复、标签单调） # 生成 Markdown 报告，支持 --output 指定路径 ``` #### HTML 可视化报告 ```bash cli.py report <正文目录> [--output <路径>] [--title <标题>] # 一键生成 6 维度检测的 HTML 可视化报告 # 暗色主题，交互式折叠，自动打开 ``` #### 性能基准测试 ```bash cli.py bench <正文目录> # 测量每个脚本的执行时间 + 并行加速比 ``` #### 语义重复检测 ```bash cli.py semantic <正文目录> [--threshold 0.7] # jieba 分词语义重复 + 命名实体识别（需 pip install jieba） ``` #### 并行全检 ```bash cli.py parallel <正文目录> [--workers 4] [--no-cache] # 并行跑所有脚本 + SQLite 缓存 + 增量检测 ``` ### 五、大纲设计 详见 `references/outline_design.md`。多卷衔接见 `references/volume_transition.md`。 ## 角色状态机 详见 `references/character_state_machine.md`。每章写完更新所有出场角色 snapshot。 弧线追踪见 `references/character_arc.md`。 ## 写作约束 - 番茄风格（短句、口语、少形容词、节奏快、对话多） - 2000-3000字/章，章末强钩子 - 去AI味（禁"不禁""缓缓""淡淡""微微"等，详见 `references/writing_tips.md`） - 弹幕管理见 `references/danmaku_management.md` ## 伏笔管理 埋设登记 → 5步检查回收 → 20/50/100章过期预警。详见 `references/foreshadowing.md`。 ## 参考文档索引 | 文档 | 主题 | |------|------| | checklist.md | 每章必查清单（A-E五类） | | hook_design.md | 钩子设计技巧与模板 | | writing_tips.md | 新手避坑 TOP 20 | | dialogue_quality.md | 对话去AI味指南 | | foreshadowing.md | 伏笔管理完整流程 | | character_state_machine.md | 角色状态机规范 | | character_arc.md | 角色弧线四阶段模型 | | outline_design.md | 大纲设计方法论 | | continuity.md | 续写衔接机制 | | emotion_arc_guide.md | 情感弧线设计 | | reader_experience.md | 读者体验管理 | | scene_transition.md | 场景转换技巧 | | volume_transition.md | 多卷衔接实操 | | writing_rhythm_guide.md | 5种节奏模板 + 10章周期 | | style_consistency.md | 文风一致性检测 | | smart_context.md | 智能上下文路由 | | context_loader.md | 上下文加载器 | | danmaku_management.md | 弹幕角色管理 | | usage_examples.md | 完整使用示例 | | deep_reading_guide.md | 深度审读指南（逐字精读流程） | | iteration_report.md | 迭代报告 | ## 测试 ```bash cd novel-writer && python3 -m pytest tests/ -v # 284 passed ``` ## 关键教训 1. 章节必须顺序写 2. 更新追踪表时逐章核对 3. 大纲是活的，可调整但要双向同步 4. 角色状态机比文本快照可靠 5. 近5章摘要比只读上一章更有效

path: README.md
content: <div align="center">

# 📖 Novel Writer

**Enterprise-grade Long-form Web Novel Engine for AI Agents**

[![Tests](https://img.shields.io/badge/tests-284%20passed-success)]()
[![Scripts](https://img.shields.io/badge/scripts-18-orange)]()
[![References](https://img.shields.io/badge/references-20-blue)]()
[![Version](https://img.shields.io/badge/version-3.1.0-blue)]()

*Consistency-first novel writing with automated quality checks, character state machines, and plot tracking.*

[English](#english) | [中文](#中文)

</div>

---

## 中文

### 为什么需要 Novel Writer？

写长篇小说（50万字+）最大的敌人是**前后矛盾**。AI 写作尤其容易犯：
- 角色名字写错
- 时间线对不上
- 角色突然知道不该知道的事
- 前面埋的伏笔忘了回收

Novel Writer 用**结构化追踪 + 自动化检测**解决这些问题。

### 核心能力

```
写作层：上下文包构建 → 正文写作 → 6步自检 → 状态更新
检测层：一致性 / 大纲偏差 / 节奏 / 对话质量 / 张力 / 伏笔 / 钩子 / 标题 / 开头 / 段落 / 重复（11维度）
追踪层：角色状态机 / 伏笔追踪 / 时间线 / 事件记录
参考层：20篇写作指南（大纲设计/钩子技巧/节奏模板/去AI味/弹幕管理...）
```

### 12个检测脚本

| 脚本 | CLI 别名 | 检测内容 |
|------|----------|----------|
| consistency_check | `check` | 角色称呼、设定冲突、时间线、AI高频词、跨章重复、角色消失 |
| outline_drift | `drift` | 标题匹配、关键词覆盖、角色出场偏差 |
| rhythm_check | `rhythm` | 紧张度、钩子密度、情感基调、节奏异常 |
| dialogue_tag_check | `dialogue` | 标签频率、单调性、副词过度、对话占比 |
| tension_forecast | `tension` | 张力分布预测、走势分析 |
| foreshadow_scan | `foreshadow` | 伏笔模式识别、未回收预警 |
| chapter_hook_check | `hook` | 钩子类型、强度评分、连续弱结尾 |
| chapter_title_check | `title` | 标题长度、平淡检测、标题党、信息过量 |
| opening_score | `opening` | 开头类型、吸引力评分、钩子检测 |
| paragraph_check | `paragraph` | 过长段落、墙文字、视觉节奏 |
| repetition_check | `repeat` | 跨章重复、连续句式 |
| run_all_checks | `all` | 一键全检 |
| config_manager | `config` | 小说注册/切换/配置管理 |
| changelog | `changelog` | 版本管理 |
| backup | `backup` | 结构化数据备份 |
| run_parallel | `parallel` | 并行检测+缓存+增量 |
| semantic_check | `semantic` | jieba语义重复+实体识别 |

### 深度审读（人工编辑级）

不只是跑脚本，而是像人类编辑一样逐字精读：
- 每批读 3-5 章，写精读笔记（证明理解了剧情）
- 每段带着 6 个问题分析（合理性/读者感受/衔接/AI味...）
- 跨章关联分析（角色弧线/情感曲线/伏笔回收）
- 输出精读报告 + 修改清单（P0/P1/P2 分级）

详细流程见 SKILL.md「三、深度审读」。

### 使用示例

```bash
# 一致性检查
python3 scripts/cli.py check 正文/ --tracking 追踪表.md --dict 设定词典.md

# 大纲偏差
python3 scripts/cli.py drift --outline 大纲.md --chapters 正文/

# 全部检测
python3 scripts/cli.py all 正文/

# 带建议输出
python3 scripts/cli.py hook --novel-dir 正文/ --suggest

# 注册小说并备份
python3 scripts/cli.py config register 我的小说 /path/to/novel
python3 scripts/cli.py backup /path/to/novel

# 并行全检（带缓存，快3-5倍）
python3 scripts/cli.py parallel 正文/ --workers 4

# 语义重复检测（需 pip install jieba）
python3 scripts/cli.py semantic 正文/ --threshold 0.7

# 缓存管理
python3 scripts/cli.py parallel --stats
python3 scripts/cli.py parallel --clear
```

### 20篇写作参考文档

| 文档 | 覆盖内容 |
|------|----------|
| checklist.md | 每章必查清单（A-E 五类） |
| hook_design.md | 钩子设计技巧与模板 |
| writing_tips.md | 新手避坑 TOP 20 |
| dialogue_quality.md | 对话去AI味指南 |
| foreshadowing.md | 伏笔管理完整流程 |
| character_state_machine.md | 角色状态机规范 |
| character_arc.md | 角色弧线四阶段模型 |
| outline_design.md | 大纲设计方法论 |
| continuity.md | 续写衔接机制 |
| emotion_arc_guide.md | 情感弧线设计 |
| reader_experience.md | 读者体验管理 |
| scene_transition.md | 场景转换技巧 |
| volume_transition.md | 多卷衔接实操 |
| writing_rhythm_guide.md | 5种节奏模板 + 10章周期 |
| style_consistency.md | 文风一致性检测 |
| smart_context.md | 智能上下文路由 |
| context_loader.md | 上下文加载器 |
| danmaku_management.md | 弹幕角色管理 |
| usage_examples.md | 完整使用示例 |
| iteration_report.md | 迭代报告 |

### 测试

```bash
cd novel-writer && python3 -m pytest scripts/tests/ -v  # 283 passed
```

## English

### Why Novel Writer?

Long-form novels (500K+ words) face one critical enemy: **inconsistency**. AI writing is especially prone to:
- Character name errors
- Timeline contradictions
- Information leaks (characters knowing what they shouldn't)
- Forgotten plot threads

Novel Writer solves this with **structured tracking + automated quality checks**.

### 12 Detection Scripts

| Script | Alias | What it Checks |
|--------|-------|---------------|
| consistency_check | `check` | Names, settings, timeline, AI words, cross-chapter repetition, missing characters |
| outline_drift | `drift` | Title match, keyword coverage, character appearance deviation |
| rhythm_check | `rhythm` | Tension, hook density, emotional tone, pacing anomalies |
| dialogue_tag_check | `dialogue` | Tag frequency, monotony, adverb overuse, dialogue ratio |
| tension_forecast | `tension` | Tension distribution prediction, trend analysis |
| foreshadow_scan | `foreshadow` | Foreshadowing pattern detection, unrecovered thread warnings |
| chapter_hook_check | `hook` | Hook types, strength scores, consecutive weak endings |
| chapter_title_check | `title` | Title length, bland detection, clickbait, info overload |
| opening_score | `opening` | Opening type, appeal score, hook detection |
| paragraph_check | `paragraph` | Long paragraphs, wall-of-text, visual pacing |
| repetition_check | `repeat` | Cross-chapter repetition, consecutive patterns |
| run_all_checks | `all` | Run everything |

### Test

```bash
cd novel-writer && python3 -m pytest scripts/tests/ -v  # 283 passed
```

### License

MIT License

### Credits

Built with ❤️ for the [OpenClaw](https://github.com/openclaw/openclaw) ecosystem.


path: references/character_arc.md
content: # 角色弧线追踪模板

角色弧线追踪与状态机互补：状态机追踪"角色在哪、知道什么、有什么"（外部状态），弧线追踪"角色变成了谁"（内部演变）。

## 一、弧线追踪文件格式

每部小说维护 `character_arcs.json`，与 `character_states.json` 同级。

```json
{
  "陆远": {
    "arc_overview": {
      "start_trait": "封闭、过度理性、不敢建立亲密关系",
      "end_trait": "学会信任、接受不完美、愿意为爱冒险",
      "core_conflict": "25年孤独vs渴望被理解",
      "total_phases": 4
    },
    "phases": [
      {
        "id": "isolation",
        "name": "孤岛期",
        "chapter_range": [1, 50],
        "trait_snapshot": "理性到冷血，拒绝所有人靠近",
        "behavior_patterns": ["遇事先计算概率", "用冷幽默回避情感", "独处时焦虑但绝不示人"],
        "trigger_to_next": "沈清的出现打破了他的计算逻辑"
      },
      {
        "id": "cracking",
        "name": "裂痕期",
        "chapter_range": [51, 120],
        "trait_snapshot": "开始犹豫、偶尔感性，但立刻自我否定",
        "behavior_patterns": ["做决定时会想'沈清会怎么看'", "第一次主动分享秘密", "噩梦增多"],
        "trigger_to_next": "刘阳之死让他质疑理性是否有意义"
      },
      {
        "id": "breaking",
        "name": "破碎期",
        "chapter_range": [121, 200],
        "trait_snapshot": "理性外壳崩塌，暴露脆弱和愤怒",
        "behavior_patterns": ["不再说'正常营业'", "主动寻求帮助", "情绪失控频率增加"],
        "trigger_to_next": "接受父亲的遗产，理解孤独不是宿命"
      },
      {
        "id": "rebuilding",
        "name": "重建期",
        "chapter_range": [201, 300],
        "trait_snapshot": "带着伤疤但选择信任，理性与感性并存",
        "behavior_patterns": ["愿意展示弱点", "为爱做不理性的决定但不后悔", "能用幽默化解而不是回避"],
        "trigger_to_next": null
      }
    ],
    "current_phase": "isolation",
    "phase_transitions": [
      {
        "from_phase": "isolation",
        "to_phase": "cracking",
        "chapter": null,
        "event": null,
        "completed": false
      }
    ]
  }
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| arc_overview | object | 弧线总览：起点性格、终点性格、核心冲突、阶段总数 |
| phases | array | 性格演变阶段列表，按顺序排列 |
| phases[].id | string | 阶段唯一标识（用于状态机关联） |
| phases[].name | string | 阶段中文名（如"孤岛期"） |
| phases[].chapter_range | [int,int] | 预计章节范围（可调整） |
| phases[].trait_snapshot | string | 该阶段性格特征一句话描述 |
| phases[].behavior_patterns | [string] | 该阶段的典型行为模式（写作时对照） |
| phases[].trigger_to_next | string/null | 进入下一阶段的触发事件描述 |
| current_phase | string | 当前所处阶段的 id |
| phase_transitions | array | 已发生的阶段转换记录 |

## 二、关系演变追踪

在 `character_arcs.json` 中为每对重要关系维护演变轨迹：

```json
{
  "relationship_arcs": {
    "陆远×沈清": {
      "trajectory": ["客户", "对手", "同谋", "决裂", "理解", "恋人", "战友"],
      "current_stage": "客户",
      "milestones": [
        {
          "stage": "对手",
          "chapter": null,
          "event": "商业谈判中的正面冲突",
          "completed": false
        },
        {
          "stage": "同谋",
          "chapter": null,
          "event": "共同发现永盛的秘密",
          "completed": false
        }
      ],
      "tension_level": "低",
      "trust_score": 1
    },
    "陆远×赵恒": {
      "trajectory": ["搭档", "怀疑", "真相", "原谅", "生死之交"],
      "current_stage": "搭档",
      "milestones": [],
      "tension_level": "低",
      "trust_score": 7
    }
  }
}
```

### 字段说明

| 字段 | 说明 |
|------|------|
| trajectory | 关系演变的完整路径（从起点到终点） |
| current_stage | 当前处于哪个阶段 |
| milestones | 每个阶段转换的具体事件记录 |
| tension_level | 当前张力等级（低/中/高/极高） |
| trust_score | 信任度 1-10（10为完全信任） |

### 常见关系演变模式

| 模式 | 轨迹示例 | 适用场景 |
|------|----------|----------|
| 缓慢建立信任 | 陌生→试探→合作→信任→生死相托 | 主角团队 |
| 先甜后苦 | 热恋→裂痕→背叛→敌对 | 反转角色 |
| 宿敌变盟友 | 敌对→理解→尊重→合作 | 对手角色 |
| 背叛与救赎 | 信任→背叛→孤立→赎罪→原谅 | 复杂配角 |

## 三、性格变化检测

写作时用以下规则检测角色行为是否符合当前阶段：

### 检测逻辑

```
IF 角色处于"孤岛期"（trait: 封闭、理性）:
  ✅ 合理行为：拒绝帮助、用数据说话、独来独往
  ⚠️ 需铺垫：偶尔流露温柔（必须有触发事件）
  ❌ 不合理：主动倾诉、轻易信任陌生人、情感失控

IF 角色处于"裂痕期"（trait: 犹豫、偶尔感性）:
  ✅ 合理行为：犹豫不决、事后后悔感性、试探性靠近
  ⚠️ 需铺垫：完全信任他人（还不够）
  ❌ 不合理：回到完全封闭（已回不去了）
```

### 检测规则模板（写入 consistency_check.py 的扩展）

```python
# 性格一致性检测规则
ARC_BEHAVIOR_RULES = {
    "isolation": {
        "allowed": ["理性决策", "冷幽默", "回避亲密", "独自行动"],
        "needs_setup": ["流露感情", "主动求助", "表达信任"],
        "forbidden": ["情感失控", "无条件信任", "主动倾诉内心"]
    },
    "cracking": {
        "allowed": ["犹豫", "矛盾", "试探性信任", "事后自我否定"],
        "needs_setup": ["完全信任", "理性决策（恢复）"],
        "forbidden": ["完全回到封闭", "无条件信任"]
    },
    "breaking": {
        "allowed": ["情绪失控", "愤怒", "主动求助", "展示脆弱"],
        "needs_setup": ["理性回归"],
        "forbidden": ["完美理性决策", "假装无事发生"]
    },
    "rebuilding": {
        "allowed": ["有限度信任", "带着伤疤前行", "理性与感性并存"],
        "needs_setup": [],
        "forbidden": ["回到完全封闭", "无条件信任所有人"]
    }
}
```

### Agent 检测指令

写完每章后，对照角色的 `current_phase`，检查：
1. 角色行为是否匹配当前阶段的 `behavior_patterns`
2. 如果角色出现了 `needs_setup` 中的行为，是否有充分的触发事件
3. 如果出现了 `forbidden` 中的行为，标记为**性格断裂bug**

## 四、与状态机的整合方式

### 架构关系

```
character_states.json（状态机）    character_arcs.json（弧线）
├── location                        ├── arc_overview
├── knows / not_knows               ├── phases + current_phase
├── has                             ├── relationship_arcs
├── emotional_state                 └── （性格阶段信息）
└── relationships（简述）
          ↓ 互相引用 ↓
    snapshots[X].arc_phase = "isolation"
    phases[X].chapter_range 覆盖 snapshot 章节
```

### 整合规则

1. **状态机 snapshot 新增 `arc_phase` 字段**：每章 snapshot 标注角色当前所处的弧线阶段
   ```json
   "snapshots": {
     "5": {
       "arc_phase": "isolation",
       "location": "...",
       ...
     }
   }
   ```

2. **弧线阶段变更时同步更新**：当角色经历触发事件进入新阶段时，同时更新两个文件：
   - `character_arcs.json`：更新 `current_phase`，记录 `phase_transitions`
   - `character_states.json`：后续 snapshot 的 `arc_phase` 字段改为新阶段

3. **关系信息双向同步**：
   - 状态机 `relationships` 记录简要关系描述（向后兼容）
   - 弧线文件 `relationship_arcs` 记录详细演变轨迹和张力

4. **一致性检查联动**：consistency_check.py 同时读取两个文件：
   - 状态机检查：位置冲突、知识泄露、物品凭空出现
   - 弧线检查：性格断裂、关系跳跃、阶段回退

### 文件读写顺序

```
每章写完后：
1. 更新 character_states.json（snapshot）
2. 检查是否有弧线阶段变更 → 有则更新 character_arcs.json
3. 检查是否有关系阶段变更 → 有则更新 relationship_arcs
4. 运行 consistency_check.py 验证一致性
```

## 五、具体示例

### 示例1：陆远（悬疑类小说主角）

```json
{
  "陆远": {
    "arc_overview": {
      "start_trait": "封闭、过度理性、25年孤独让他不相信任何人",
      "end_trait": "学会信任、接受不完美、愿意为所爱之人冒险",
      "core_conflict": "理性自保vs情感渴望",
      "total_phases": 4
    },
    "phases": [
      {
        "id": "isolation",
        "name": "孤岛期",
        "chapter_range": [1, 50],
        "trait_snapshot": "冷静到冷血，看到所有人倒计时却从不干预",
        "behavior_patterns": [
          "永远说'正常营业'",
          "用概率和数据解释一切",
          "深夜失眠但不让任何人知道",
          "8块钱牛肉面是他唯一的'人情味'"
        ],
        "trigger_to_next": "沈清让他意识到有些事算不清概率"
      },
      {
        "id": "cracking",
        "name": "裂痕期",
        "chapter_range": [51, 120],
        "trait_snapshot": "理性外壳出现裂缝，开始有计算之外的冲动",
        "behavior_patterns": [
          "第一次对别人的倒计时产生情绪反应",
          "开始主动调查而不是被动观察",
          "试探性地向赵恒透露更多信息",
          "偶尔忘了说'正常营业'"
        ],
        "trigger_to_next": "刘阳之死——他计算对了但人还是死了"
      },
      {
        "id": "breaking",
        "name": "破碎期",
        "chapter_range": [121, 200],
        "trait_snapshot": "理性完全失效，被迫面对情感和信任",
        "behavior_patterns": [
          "不再假装冷静",
          "主动寻求沈清的帮助",
          "做出'不理性'的选择但不后悔",
          "第一次为别人流泪（不是7岁的自己）"
        ],
        "trigger_to_next": "理解父亲的牺牲，接受孤独不是命运"
      },
      {
        "id": "rebuilding",
        "name": "重建期",
        "chapter_range": [201, 300],
        "trait_snapshot": "理性仍在但不再排斥感性，带着伤疤信任",
        "behavior_patterns": [
          "该算的算，该赌的赌",
          "不再回避'爱'这个字",
          "偶尔还能冷幽默，但底色是温暖的",
          "愿意展示脆弱，因为沈清接住了"
        ],
        "trigger_to_next": null
      }
    ],
    "current_phase": "isolation",
    "phase_transitions": []
  },
  "relationship_arcs": {
    "陆远×沈清": {
      "trajectory": ["客户", "对手", "同谋", "决裂", "理解", "恋人", "战友"],
      "current_stage": "客户",
      "milestones": [],
      "tension_level": "低",
      "trust_score": 2
    },
    "陆远×赵恒": {
      "trajectory": ["搭档", "怀疑", "真相", "原谅", "生死之交"],
      "current_stage": "搭档",
      "milestones": [],
      "tension_level": "低",
      "trust_score": 7
    }
  }
}
```

### 示例2：林昭（催泪类小说主角）

```json
{
  "林昭": {
    "arc_overview": {
      "start_trait": "温暖的普通人，不善社交但真诚，被推着走",
      "end_trait": "独立、有担当、带着999个前世的爱活成最好的自己",
      "core_conflict": "依赖前世vs成为自己",
      "total_phases": 6
    },
    "phases": [
      {
        "id": "passive",
        "name": "被动期",
        "chapter_range": [1, 90],
        "trait_snapshot": "被前世的价值观碰撞推着走，依赖锦囊做决定",
        "behavior_patterns": [
          "遇到困难第一反应是'锦囊怎么说'",
          "对前世言听计从，不敢质疑",
          "社交场合沉默，不敢表达自己",
          "创业决策犹豫，需要前世推一把"
        ],
        "trigger_to_next": "Ch91独立宣言——第一次违抗前世的建议"
      },
      {
        "id": "awakening",
        "name": "觉醒期",
        "chapter_range": [91, 150],
        "trait_snapshot": "开始有自己的判断，但还不坚定",
        "behavior_patterns": [
          "偶尔做出与前世建议相反的决定",
          "事后会怀疑自己是不是错了",
          "开始主动与前世对话而非被动接受",
          "第一次对前世说'不'"
        ],
        "trigger_to_next": "Ch150破茧——彻底独立"
      },
      {
        "id": "independent",
        "name": "独立期",
        "chapter_range": [151, 201],
        "trait_snapshot": "能独立判断，但锦囊能力让他过于依赖",
        "behavior_patterns": [
          "有自己的决策逻辑",
          "锦囊从依赖变成辅助工具",
          "创业开始独当一面",
          "与前世关系从'听令'变成'朋友'"
        ],
        "trigger_to_next": "Ch202锦囊反噬"
      },
      {
        "id": "backlash",
        "name": "反噬期",
        "chapter_range": [202, 219],
        "trait_snapshot": "能力失控，人格分裂征兆，跌入谷底",
        "behavior_patterns": [
          "能力失控导致无法正常工作",
          "前世记忆反噬导致身份混乱",
          "苏晚不离不弃是唯一锚点",
          "学会区分'自己的声音'和'前世的声音'"
        ],
        "trigger_to_next": "Ch218-219融合自我"
      },
      {
        "id": "fusion",
        "name": "融合期",
        "chapter_range": [220, 400],
        "trait_snapshot": "融合前世经验，形成独立人格，有温度的强者",
        "behavior_patterns": [
          "不再需要锦囊做决定，但尊重前世的智慧",
          "创业成功但不忘初心",
          "面对危机有担当，不再逃避",
          "与前世的告别是温暖的而非痛苦的"
        ],
        "trigger_to_next": "失去所有能力"
      },
      {
        "id": "ordinary",
        "name": "凡人期",
        "chapter_range": [401, 502],
        "trait_snapshot": "失去能力成为普通人，但内在强大",
        "behavior_patterns": [
          "没有能力但比有能力时更从容",
          "传承给新持有者",
          "守护空影院——999个空座位是荣耀不是悲伤",
          "和苏晚过上普通生活，珍惜每一天"
        ],
        "trigger_to_next": null
      }
    ],
    "current_phase": "passive",
    "phase_transitions": []
  },
  "relationship_arcs": {
    "林昭×苏晚": {
      "trajectory": ["前女友", "客户", "暧昧", "伴侣", "妻子", "母亲", "灵魂伴侣"],
      "current_stage": "前女友",
      "milestones": [],
      "tension_level": "中",
      "trust_score": 5
    },
    "林昭×何薇": {
      "trajectory": ["邻居", "同事", "合伙人", "铁搭档"],
      "current_stage": "邻居",
      "milestones": [],
      "tension_level": "低",
      "trust_score": 3
    },
    "林昭×001号": {
      "trajectory": ["权威", "导师", "朋友", "告别"],
      "current_stage": "权威",
      "milestones": [],
      "tension_level": "低",
      "trust_score": 6
    },
    "林昭×666号李归墟": {
      "trajectory": ["陌生", "共鸣", "理解", "互相救赎"],
      "current_stage": "陌生",
      "milestones": [],
      "tension_level": "低",
      "trust_score": 1
    }
  }
}
```

## 六、Agent 操作指令

### 初始化弧线文件

```bash
# 在小说目录下创建 character_arcs.json
# 从设定集提取主角和核心配角的信息填入
```

### 每章写完后更新

```bash
# 1. 检查本章是否触发了弧线阶段变更
# 2. 如有变更：更新 current_phase + 记录 phase_transitions + 同步 character_states.json 的 arc_phase
# 3. 检查关系是否有阶段变化 → 更新 relationship_arcs
# 4. 运行一致性检查
```

### 性格一致性检查

写完每章后，对出场角色执行：
1. 读取角色的 `current_phase` 和对应 `behavior_patterns`
2. 检查角色行为是否在 `allowed` 列表中
3. 如果行为在 `needs_setup` 中，检查是否有铺垫
4. 如果行为在 `forbidden` 中，标记为 bug

### 与伏笔管理的联动

弧线阶段转换通常是伏笔回收的好时机：
- 角色进入新阶段时，检查是否有与之相关的伏笔可以回收
- 伏笔的回收可以作为阶段转换的触发事件


path: references/character_state_machine.md
content: # 角色状态机模板

每部小说应维护 `character_states.json`，用于结构化追踪角色动态状态。
角色状态包含两层：**状态层**（位置/知识/物品）和**弧线层**（成长阶段/性格演变/关系变化）。

## 文件格式

```json
{
  "陆远": {
    "first_appearance": 1,
    "role": "主角",
    "pov": true,
    "base_info": {
      "age": 28,
      "job": "永盛集团分析师",
      "personality": "冷静、理性、好奇心强"
    },
    "snapshots": {
      "1": {
        "location": "永盛集团12层办公区",
        "knows": ["自己能看到死亡倒计时", "孙磊的倒计时异常"],
        "not_knows": ["倒计时的真正原因", "永盛的秘密"],
        "has": ["手机", "工牌"],
        "emotional_state": "震惊、恐惧",
        "relationships": {
          "孙磊": "同事，关系一般",
          "赵恒": "大学好友，远程联系"
        }
      },
      "2": {
        "location": "永盛集团食堂",
        "knows": ["孙磊的倒计时消失了"],
        "not_knows": ["为什么消失"],
        "has": ["手机", "工牌", "孙磊给的数据盘"],
        "emotional_state": "困惑、警觉",
        "key_events": ["发现孙磊余额变为空白"]
      }
    }
  },
  "孙磊": {
    "first_appearance": 1,
    "role": "关键配角",
    "pov": false,
    "base_info": {
      "age": 35,
      "job": "永盛集团高级经理"
    },
    "snapshots": {
      "1": {
        "location": "永盛集团",
        "knows": [],
        "not_knows": ["陆远能看到倒计时"],
        "has": ["数据盘"],
        "emotional_state": "正常",
        "countdown": "42年137天"
      },
      "9": {
        "countdown": "空白（已消失）",
        "emotional_state": "未知（未出场）"
      }
    }
  }
}
```

## 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| first_appearance | int | 首次出场章节 |
| role | string | 角色定位（主角/核心配角/配角/路人） |
| pov | bool | 是否有视角（能否描写内心） |
| base_info | object | 不变的基础信息 |
| snapshots | object | 每章的状态快照，key为章节号（字符串） |

### snapshot 字段

| 字段 | 必填 | 说明 |
|------|------|------|
| location | 是 | 角色当前所在位置 |
| knows | 是 | 角色目前已知道的信息列表 |
| not_knows | 是 | 角色尚不知道的信息（防剧透） |
| has | 是 | 角色持有的关键物品 |
| emotional_state | 是 | 当前情绪状态 |
| relationships | 否 | 当前与其他角色的关系 |
| key_events | 否 | 本章发生在此角色身上的关键事件 |
| countdown | 否 | 特殊机制数值（如倒计时、余额） |

## 维护规则

1. **每章写完必须更新** 所有出场角色的 snapshot
2. **只更新出场角色** — 未出场的角色保持上一章状态不变
3. **knows 只增不减** — 角色获得的信息不能"忘记"（除非有剧情原因）
4. **not_knows 要同步移除** — 角色获知信息后从 not_knows 移到 knows
5. **has 要准确** — 物品获得/丢失/转移都要记录
6. **location 每章更新** — 即使没移动也要确认

## 一致性检查要点

- 角色在A地，但同一章另一角色在同一地点没看到他 → 位置冲突
- 角色做出了基于某信息的决策，但 knows 里没有这个信息 → 知识泄露
- 角色使用了某物品，但 has 里没有 → 物品凭空出现
- 角色情绪与上一章不衔接且没有过渡事件 → 情绪断裂
- 角色行为与其当前成长阶段不符 → 弧线断裂（如处于"怀疑"阶段却主动对抗）
- 角色关系变化没有触发事件 → 关系跳跃

---

# 角色弧线追踪模板

## 概述

状态层追踪"角色在哪里、知道什么、有什么"，弧线层追踪"角色经历了什么变化、成长到哪一步"。两层共享同一个 `character_states.json`，弧线信息存放在角色的 `arc` 字段中。

## 文件格式（arc 部分新增）

```json
{
  "陆远": {
    "first_appearance": 1,
    "role": "主角",
    "pov": true,
    "base_info": {
      "age": 32,
      "job": "投行合伙人",
      "personality": "冷静、理性、好奇心强"
    },
    "arc": {
      "growth_stages": [
        {
          "stage": "隐忍",
          "chapter_range": [1, 30],
          "description": "25年来假装正常，看到余额但不敢干预，三次失败后发誓不再插手",
          "personality_traits": ["过度理性", "自我封闭", "冷幽默掩盖焦虑"],
          "trigger_events": ["发现孙磊余额消失"]
        },
        {
          "stage": "怀疑",
          "chapter_range": [31, 80],
          "description": "主动调查，不再被动接受，开始质疑系统",
          "personality_traits": ["主动", "警觉", "挣扎于理性和直觉之间"],
          "trigger_events": ["发现永盛的秘密入口", "遇到赵恒异常行为"]
        },
        {
          "stage": "确认",
          "chapter_range": [81, 150],
          "description": "完全了解系统运作，开始寻找对抗方法",
          "personality_traits": ["决断", "责任感", "第一次允许自己信任他人"],
          "trigger_events": ["父亲真相揭露"]
        },
        {
          "stage": "对抗",
          "chapter_range": [151, 250],
          "description": "正面挑战系统，承受代价",
          "personality_traits": ["勇敢但脆弱", "愿意牺牲", "与沈清并肩"],
          "trigger_events": ["沈清觉醒能力"]
        },
        {
          "stage": "解决",
          "chapter_range": [251, 372],
          "description": "找到平衡，接受自己，重建生活",
          "personality_traits": ["平和", "接纳", "不再需要伪装"],
          "trigger_events": ["系统终结"]
        }
      ],
      "personality_timeline": [
        {
          "chapter": 1,
          "trait": "冷理性",
          "evidence": "看到孙磊余额异常，第一反应是数据验证而不是恐慌"
        },
        {
          "chapter": 50,
          "trait": "开始信任",
          "evidence": "第一次向沈清透露自己的秘密"
        }
      ],
      "relationship_arcs": {
        "沈清": {
          "stages": [
            {"phase": "陌生人", "chapter_range": [1, 10], "key_event": "业务会面"},
            {"phase": "客户", "chapter_range": [11, 40], "key_event": "合作项目"},
            {"phase": "同谋", "chapter_range": [41, 100], "key_event": "共同发现系统秘密"},
            {"phase": "决裂", "chapter_range": [101, 130], "key_event": "刘阳之死"},
            {"phase": "复合", "chapter_range": [131, 180], "key_event": "沈清觉醒"},
            {"phase": "并肩", "chapter_range": [181, 350], "key_event": "对抗系统"},
            {"phase": "恋人", "chapter_range": [351, 372], "key_event": "重建生活"}
          ]
        },
        "赵恒": {
          "stages": [
            {"phase": "搭档", "chapter_range": [1, 60]},
            {"phase": "怀疑对象", "chapter_range": [61, 100]},
            {"phase": "敌人", "chapter_range": [101, 150]},
            {"phase": "理解", "chapter_range": [151, 200]},
            {"phase": "告别", "chapter_range": [201, 250]}
          ]
        }
      },
      "internal_conflicts": [
        {
          "conflict": "理性 vs 情感",
          "chapter_range": [1, 150],
          "resolution": "学会接受不理性的一面",
          "resolution_chapter": 150
        },
        {
          "conflict": "自我保护 vs 责任",
          "chapter_range": [30, 200],
          "resolution": "选择承担但不再孤军奋战",
          "resolution_chapter": 180
        }
      ],
      "key_turning_points": [
        {"chapter": 1, "event": "发现孙磊余额消失", "impact": "从被动观察转向主动调查"},
        {"chapter": 80, "event": "父亲真相", "impact": "从怀疑转向确认，找到行动方向"},
        {"chapter": 150, "event": "沈清决裂", "impact": "第一次暴露真正的情感需求"}
      ]
    },
    "snapshots": { }
  }
}
```

## arc 字段说明

### 顶层字段

| 字段 | 必填 | 说明 |
|------|------|------|
| growth_stages | 是 | 角色成长阶段列表，至少2个阶段 |
| personality_timeline | 否 | 性格特征变化时间线 |
| relationship_arcs | 否 | 与其他角色的关系演变 |
| internal_conflicts | 否 | 内心冲突及其解决 |
| key_turning_points | 否 | 关键转折点 |

### growth_stages 阶段项

| 字段 | 必填 | 说明 |
|------|------|------|
| stage | 是 | 阶段名称（简短，如"隐忍""怀疑""确认""对抗""解决"） |
| chapter_range | 是 | [起始章, 结束章]，结束章可用 null 表示尚未写到 |
| description | 是 | 该阶段角色的心理状态和行为模式 |
| personality_traits | 是 | 该阶段的3-5个性格关键词 |
| trigger_events | 是 | 推动角色进入下一阶段的关键事件 |

### personality_timeline 性格时间线项

| 字段 | 必填 | 说明 |
|------|------|------|
| chapter | 是 | 性格变化发生的章节 |
| trait | 是 | 新出现/变化的性格特征 |
| evidence | 是 | 正文中的具体表现（用于审校时对照） |

### relationship_arcs 关系弧线

key 为对方角色名，value 的 stages 为关系阶段列表：

| 字段 | 必填 | 说明 |
|------|------|------|
| phase | 是 | 关系阶段名称 |
| chapter_range | 是 | [起始章, 结束章] |
| key_event | 否 | 推动关系变化的关键事件 |

### internal_conflicts 内心冲突项

| 字段 | 必填 | 说明 |
|------|------|------|
| conflict | 是 | 冲突描述（"A vs B"格式） |
| chapter_range | 是 | 冲突持续的章节范围 |
| resolution | 否 | 冲突如何解决 |
| resolution_chapter | 否 | 解决的章节 |

### key_turning_points 转折点项

| 字段 | 必填 | 说明 |
|------|------|------|
| chapter | 是 | 转折发生的章节 |
| event | 是 | 事件描述 |
| impact | 是 | 对角色弧线的影响 |

## 常见成长阶段模板

不同类型小说可参考以下阶段模型：

### 悬疑/推理类
```
无知 → 发现异常 → 调查 → 发现真相 → 对抗 → 解决
```

### 催泪/成长类
```
被推着走 → 建立连接 → 独立判断 → 承受代价 → 自我融合 → 传承
```

### 爽文/逆袭类
```
低谷 → 觉醒 → 爆发 → 瓶颈 → 突破 → 巅峰
```

### 虐恋/感情类
```
相遇 → 确认心意 → 误会/阻碍 → 分离 → 成长 → 重逢/告别
```

## 维护规则

1. **大纲阶段预填** — 开写前根据大纲预填 growth_stages 的阶段名和 chapter_range
2. **每10章回顾** — 检查角色是否按预期推进到下一阶段，如果偏离则记录原因
3. **性格变化必记** — 只要有显著性格变化就追加 personality_timeline
4. **关系变化必记** — 每次关系阶段转换都要更新 relationship_arcs
5. **转折点即时记** — 写完转折章节立即补充 key_turning_points
6. **与 snapshots 配合** — snapshot 记录"此刻的状态"，arc 记录"为什么会变成这样"

## 与状态层的整合

| 层 | 追踪内容 | 更新频率 | 用途 |
|------|------|------|------|
| 状态层 (snapshots) | 位置/知识/物品/情绪 | 每章 | 防止硬性矛盾 |
| 弧线层 (arc) | 成长阶段/性格演变/关系变化 | 每10章+转折点 | 保证角色发展连贯 |

**交叉检查**：
- snapshot.emotional_state 应与 arc 当前阶段的 personality_traits 匹配
- snapshot.knows 的新增信息应与 arc.key_turning_points 的事件对应
- snapshot.relationships 的变化应与 arc.relationship_arcs 的阶段转换一致

## 初始化方法

对新角色或已有角色补充弧线数据：

```bash
# 从大纲提取成长阶段
grep -n "成长\|弧线\|阶段\|转变" 设定集.md

# 从正文提取性格变化证据
grep -n "性格\|变了\|不再\|第一次" chapters/*.md

# 从大纲提取关系变化
# 对照大纲中两人互动的章节，标记关系阶段转换点
```


path: references/checklist.md
content: # 章节质量检查清单

每章写完或审稿时逐项检查。

## A类：设定一致性

- [ ] A1 角色名字/称呼是否与设定集一致
- [ ] A2 角色年龄、身份、职业是否与设定一致
- [ ] A3 角色外貌描写是否与设定一致（发型、体型、特征）
- [ ] A4 角色能力/技能是否与当前进度匹配（不能提前展现）
- [ ] A5 角色关系是否正确（谁认识谁、亲疏程度）
- [ ] A6 公司/组织/地点名称是否统一
- [ ] A7 专有名词是否与设定词典一致
- [ ] A8 数值是否正确（金额、人数、距离、时间长度）

## B类：时间线一致性

- [ ] B1 故事内日期/天数是否与追踪表一致
- [ ] B2 事件先后顺序是否正确
- [ ] B3 角色行动时间是否合理（不可能同时出现在两个地方）
- [ ] B4 季节/天气是否与时间线匹配
- [ ] B5 提到的历史事件是否与世界观设定一致

## C类：逻辑一致性

- [ ] C1 角色是否知道他应该知道的信息（不泄露未来剧情）
- [ ] C2 角色是否不知道他不应该知道的信息
- [ ] C3 物品持有是否正确（不能凭空出现或消失）
- [ ] C4 因果关系是否成立（结果必须有合理原因）
- [ ] C5 角色动机是否充分（关键行为必须有铺垫）

## D类：风格质量（去AI味）

- [ ] D1 是否避免了AI高频词（不禁、缓缓、淡淡、微微、竟然、居然）
- [ ] D2 外貌描写是否与前面章节重复（不能每章用同一套描写）
- [ ] D3 章末是否用了不同的收尾方式（不能每章都是悬念结尾）
- [ ] D4 对话是否自然口语化（不能书面腔）
- [ ] D5 弹幕/群聊发言顺序是否有变化（不能每章固定顺序）
- [ ] D6 是否避免了每章相同结构（开头环境→对话→结尾）
- [ ] D7 情绪是否靠行为和细节传达（不靠形容词堆叠）
- [ ] D8 是否有足够的钩子密度（每500字一个）

## E类：大纲合规

- [ ] E1 本章内容是否在大纲规划范围内
- [ ] E2 是否提前泄露了后续章节的重大信息
- [ ] E3 是否遗漏了本章应该出现的关键事件
- [ ] E4 角色行为是否符合当前阶段的性格发展
- [ ] E5 结束点是否与写作卡一致


path: references/context_loader.md
content: # 上下文智能摘要机制

> 解决核心问题：50万字+长篇小说的设定信息无法全部塞进上下文窗口，写作时需要智能加载最相关的内容。

## 一、分层加载策略

### L0 - 始终加载（每章写作必带，约2-3KB）

这些信息量小但极重要，全部加载不会浪费上下文：

```
1. 写作硬性规则（来自设定词典/设定集的"写作规则"部分）
2. 设定词典全文（专有名词+时间线+禁用词，通常5KB以内）
3. 近5章摘要表（来自追踪表）
4. 上一章最后500字原文
5. 当前写作卡（本章大纲要点）
```

**操作方式**：写作前直接 `read` 设定词典 + 写作追踪表 + 上一章文件尾部。

### L1 - 按需加载：角色设定（约1-3KB/角色）

只加载**本章出场角色**的设定。根据写作卡判断谁出场。

**操作方式**：
1. 从写作卡提取角色名列表
2. 用 `exec` + `grep` 从设定集提取对应角色段落：
   ```bash
   grep -n "###\|####" 设定集.md
   ```
3. 用 `read` 的 offset/limit 只读目标角色的部分

**示例**：写作卡写"陆远和沈清在咖啡馆见面"→ 只加载陆远和沈清的角色档案，不加载老钱、赵恒、方哲。

### L2 - 按需加载：大纲与剧情线（按卷/章范围）

502章的大纲文件可达67KB+，绝不能全读。

**操作方式**：
1. 根据当前章节号，用 `grep` 定位当前卷的范围：
   ```bash
   grep -n "第.*卷\|Vol\|卷[0-9]" 大纲文件.md | head -30
   ```
2. 用 `read` offset/limit 只读当前卷大纲（通常2-5KB）
3. 额外读取前后各1卷的摘要标题，了解上下文

### L3 - 按需加载：场景/道具/关系（极小概率）

仅在写作卡涉及特殊设定时加载（如系统的具体运作规则、特定场景描写）。

**操作方式**：维护一个"设定索引"（见下方模板），写作时查索引决定是否需要深入读取。

---

## 二、章节相关性匹配

### 匹配流程

```
写作卡输入 → 提取关键词 → 匹配设定索引 → 决定加载内容
```

### 关键词提取规则

从写作卡中提取以下维度：

| 维度 | 示例 | 需要加载 |
|------|------|----------|
| 角色名 | "陆远""沈清""赵恒" | L1：对应角色设定 |
| 场地名 | "地下金库""投行" | L3：场景描写设定 |
| 道具/物品 | "余额""迈凯轮" | L3：相关设定条目 |
| 事件关键词 | "交易""假死""毕业" | L2：相关卷的大纲回顾 |
| 情感/关系 | "决裂""告白""信任" | L1：相关角色档案的情感线部分 |

### 匹配执行模板

写作前的标准化操作序列：

```markdown
## 写作上下文包生成步骤

1. 读取当前写作卡
2. 从写作卡提取：出场角色、涉及场景、关键事件
3. 加载 L0（始终加载项）
4. 根据角色列表加载 L1（grep 定位 + read 摘取）
5. 根据章节号加载 L2（当前卷大纲）
6. 检查 L3 索引，判断是否需要额外加载
7. 组装上下文包，开始写作
```

---

## 三、摘要压缩方法

### 3.1 角色设定压缩

完整角色档案可能1000字+，写作时只需核心要素。压缩模板：

```markdown
## [角色名] 写作摘要
- 身份：[一句话]
- 当前状态：[情感/处境，从追踪表取]
- 与本章关系：[为什么出场]
- 关键特征：[外貌/口头禅/习惯，最多3条]
- 称呼规则：[怎么称呼别人/被谁怎么称呼]
```

**压缩示例**（原文800字 → 压缩后120字）：

```markdown
## 老钱 写作摘要
- 身份：钱国明，55岁，交易部总监，陆远上司
- 当前状态：刚发现前妻被系统交易真相，情绪不稳定
- 与本章关系：向陆远透露关键线索
- 关键特征：爱喝浓茶、说话慢吞吞、住老小区三居室
- 称呼规则：陆远叫他"钱哥"，他叫陆远"小陆"
```

### 3.2 大纲压缩

502章大纲按卷压缩为"卷摘要卡"：

```markdown
## 第3卷 摘要卡（第61-90章）
- 核心事件：陆远发现孙磊余额异常 → 深入调查 → 揭示交易系统冰山一角
- 关键转折：第75章孙磊余额归零
- 情感线：陆远与沈清从合作到产生好感
- 出场主要角色：陆远、沈清、孙磊、赵恒
- 承上：第2卷沈清首次出场
- 启下：第4卷方哲登场
```

### 3.3 设定词典不压缩

设定词典本身已经是精简的条目表，全文保留。5KB在上下文中完全可接受。

---

## 四、增量更新策略

### 4.1 章节写作后的增量维护

每章写完后，agent 自动执行：

```
1. 更新写作追踪表的"近5章摘要"
2. 如果本章引入新专有名词 → 追加到设定词典
3. 如果角色状态发生变化 → 更新追踪表的角色状态列
4. 如果发现设定冲突 → 标记到待修复列表
```

### 4.2 角色状态快照（增量，不传全量）

在追踪表中维护角色状态表，每章只更新变化的角色：

```markdown
| 角色 | 最近变化章节 | 当前状态 | 关键备注 |
|------|-------------|----------|----------|
| 陆远 | Ch35 | 决定深入调查 | 从被动转主动 |
| 沈清 | Ch33 | 不知情 | 未出场2章 |
| 老钱 | Ch34 | 情绪低落 | 被王建国警告后 |
| 赵恒 | Ch33 | 远程协助 | 未到场 |
```

写作时只需读取这张表，不需要重新加载全部角色档案。

### 4.3 设定变更日志

设定文件如果被修改，维护变更记录：

```markdown
## 设定变更日志
| 日期 | 文件 | 变更内容 | 影响范围 |
|------|------|----------|----------|
| 2026-04-10 | 设定词典 | 新增"刘阳"条目 | 第8-9卷 |
| 2026-04-10 | 设定集 | 修改老钱背景：前妻张敏细节 | 第3卷起 |
```

写作前检查变更日志，只加载近期变更的相关部分。

---

## 五、写作上下文包模板

### 标准模板（写作时组装）

```markdown
# 第[NN]章 写作上下文包

## 📋 写作卡
[从大纲中提取的本章要点，200-500字]

## ⚙️ 硬性规则提醒
[3-5条最易违反的规则，从设定词典中挑选]

## 👥 本章角色摘要
### [角色A] 写作摘要
- 身份/状态/关键特征/称呼规则

### [角色B] 写作摘要
- 身份/状态/关键特征/称呼规则

## 📖 近5章摘要
| 章节 | 关键事件 | 角色变化 |
|------|----------|----------|
| ... | ... | ... |

## 🔗 上一章尾部
[上一章最后500字原文]

## 📍 当前卷方向
[当前卷的1-2句话方向概括]

## ⚠️ 特别注意
[写作卡中涉及的特殊设定/易错点]
```

### 实际示例

以《我的死亡日期消失了》第35章为例：

```markdown
# 第35章 写作上下文包

## 📋 写作卡
陆远决定不再被动等待，主动深入调查永盛财务数据。深夜独自加班，
发现第二层加密数据指向一个海外账户。赵恒打来电话提醒他小心。
陆远在回家路上经过牛肉面馆，想起老钱说的话，决定第二天找老钱
单独谈谈。章末：陆远打开家门，发现门没锁。

## ⚙️ 硬性规则提醒
- 全程第三人称"陆远"视角，禁止"我"
- 公司名"永盛集团"，禁止其他写法
- 不提前剧透余额归零等重大异常
- 每章2500字左右

## 👥 本章角色摘要
### 陆远
- 身份：32岁投行合伙人，能看到寿命余额
- 当前状态：从被动转主动，决定深入调查
- 关键特征：西装三件套、黑眼圈、口头禅"正常营业"
- 称呼：老钱叫他"小陆"

### 赵恒
- 身份：陆远搭档，实际是系统人格化（600岁）
- 当前状态：远程协助，不在江城
- 关键特征：外表35岁、永远笑眯眯、说话带哲学味
- 称呼：叫陆远"陆老弟"

## 📖 近5章摘要
| 章节 | 关键事件 | 角色变化 |
|------|----------|----------|
| Ch30 | 陆远完成永盛尽调初稿 | 无 |
| Ch31 | 发现财务系统后门 | 开始怀疑 |
| Ch32 | 找孙磊确认，孙磊否认 | 孙磊异常 |
| Ch33 | 赵恒远程分析数据 | 发现海外关联 |
| Ch34 | 王建国警告"别多管闲事" | 处境危险 |

## 🔗 上一章尾部
[第34章最后500字...]

## 📍 当前卷方向
第2卷核心：陆远在永盛项目中越陷越深，逐步发现交易系统在现实中的痕迹。

## ⚠️ 特别注意
- 赵恒的电话要用他特有的说话方式（带哲学味但不啰嗦）
- 牛肉面馆是公司楼下，8块一碗——不要写错位置
- 门没锁是悬念钩子，本章不揭晓原因
```

---

## 六、Agent 操作指令

写作前 agent 执行的标准化流程（伪代码）：

```
function build_context_packet(章节号, 写作卡):
    # L0：始终加载
    规则 = read("设定词典.md") 的"写作硬性规则"部分
    词典 = read("设定词典.md") 全文
    追踪表 = read("写作追踪表.md")
    近5章摘要 = 追踪表.近5章摘要
    上一章尾部 = read("正文/第{章节号-1}章.md", offset=尾部500字)
    
    # L1：角色
    出场角色 = extract_characters(写作卡)
    for 角色 in 出场角色:
        位置 = grep("设定集.md", "### " + 角色)
        角色摘要 = read("设定集.md", offset=位置, limit=50行)
        压缩为写作摘要格式
    
    # L2：大纲
    当前卷 = determine_volume(章节号)
    卷范围 = grep("大纲.md", "第{当前卷}卷")
    卷大纲 = read("大纲.md", offset=卷范围.start, limit=卷范围.length)
    
    # 组装上下文包
    return 组装为模板格式
```

### 具体可执行的 shell 命令

```bash
# 查找角色在设定集中的位置
grep -n "^### \|^## " 设定集.md

# 查找当前卷在大纲中的位置
grep -n "第.*卷\|卷[0-9]" 大纲.md

# 读取上一章尾部
tail -20 "正文/第${PREV_CHAPTER}章.md"

# 从设定词典提取特定条目
grep -A2 "永盛\|投行\|沈清" 设定词典.md
```

---

## 七、容量估算与限制

| 内容层 | 典型大小 | 加载频率 |
|--------|----------|----------|
| L0：硬性规则 + 设定词典 | ~5KB | 每章 |
| L0：近5章摘要 + 上一章尾部 | ~2KB | 每章 |
| L1：角色摘要（2-3人） | ~1KB | 每章 |
| L2：当前卷大纲 | ~3KB | 每章 |
| L3：特殊设定 | ~1KB | 按需 |
| **合计** | **~12KB** | - |

12KB 约等于 4000-5000 tokens，在大多数模型的上下文窗口中完全可控。对比全量加载（设定集17KB + 大纲67KB + 词典5KB = 89KB），压缩率约 86%。

---

## 八、何时触发全量加载

以下场景需要牺牲效率换取完整性：

1. **新卷开始**：加载前卷完整摘要 + 新卷完整大纲
2. **重大剧情转折**：加载所有相关角色的完整档案
3. **一致性检查时**：加载设定词典全文 + 角色状态表全文
4. **回顾性章节**（闪回、回忆）：加载相关历史时间线设定


path: references/continuity.md
content: # 续写衔接机制

长篇小说续写的最大挑战是上下文断裂。本指南确保每章与前文的衔接。

## 写前必读：近5章摘要法

不能只读上一章最后500字，需要构建"写作上下文包"。

### 上下文包的构成

```
1. 当前写作卡（本章要写什么）
2. 角色状态快照（所有出场角色当前状态）
3. 近5章关键事件摘要（每章1-2句话）
4. 上一章最后500字原文
5. 本章涉及角色的设定
6. 相关设定词典条目
```

### 近5章摘要模板

每章写完后，在追踪表中维护：

```markdown
### 近5章摘要（写作时自动生成）

| 章节 | 关键事件 | 新增信息 | 角色变化 |
|------|----------|----------|----------|
| 第31章 | 陆远发现财务系统后门 | 永盛有隐藏账户 | 陆远开始怀疑公司高层 |
| 第32章 | 陆远找孙磊确认数据 | 孙磊否认知道后门 | 孙磊态度异常 |
| 第33章 | 赵恒远程分析数据 | 数据指向海外空壳公司 | 无 |
| 第34章 | 陆远被王建国叫去谈话 | 王建国暗示"别多管闲事" | 陆远处境危险 |
| 第35章 | 陆远决定深入调查 | 发现第二层加密数据 | 陆远从被动变主动 |
```

## 衔接检查清单

写完新章节后，检查以下衔接点：

### 1. 时间衔接
- [ ] 本章开头的时间是否紧接上一章结尾
- [ ] 如果有时间跳跃，是否在文中明确交代
- [ ] 角色的行动时间是否合理（走路、吃饭、开会需要时间）

### 2. 空间衔接
- [ ] 上一章结尾角色在哪，本章开头是否一致
- [ ] 如果换了场景，是否有过渡描写
- [ ] 场景中的道具/环境是否连贯

### 3. 情绪衔接
- [ ] 上一章结尾角色的情绪，本章开头是否延续
- [ ] 情绪变化是否有事件触发，不能突兀转变
- [ ] 对话的语气是否与情绪匹配

### 4. 信息衔接
- [ ] 角者在上一章获得的信息，本章是否正确运用
- [ ] 没有在本章"遗忘"上一章的关键发现
- [ ] 角色的决策是否基于已有信息

### 5. 悬念衔接
- [ ] 上一章末尾的悬念是否在本章回应（即使不揭晓，也要推进）
- [ ] 不能完全忽略上一章的钩子
- [ ] 回应方式可以是"部分回答+新悬念"

## 常见衔接错误

| 错误类型 | 示例 | 修正方法 |
|----------|------|----------|
| 位置跳跃 | 上一章在办公室，本章突然在家，没交代怎么回去的 | 加过渡段 |
| 情绪断裂 | 上一章结尾恐惧，本章开头轻松聊天 | 加情绪过渡 |
| 信息遗忘 | 上一章发现重要线索，本章完全没提 | 自然带出"他一直在想那条线索" |
| 角色凭空出现 | 角色突然在场但没交代什么时候来的 | 补一句"他不知道什么时候站到了身后" |
| 时间重叠 | 两个事件的时间对不上 | 用追踪表的时间线核实 |

## 自动摘要生成

每章写完后，在追踪表中自动维护摘要。agent 应执行：

1. 总结本章3个关键事件
2. 记录新增的重要信息
3. 标记角色状态变化
4. 将摘要追加到"近5章摘要"表
5. 如果超过5章，删除最旧的一条


path: references/danmaku_management.md
content: # 弹幕角色管理指南

适用于有弹幕/群聊/多角色实时互动机制的小说（如《观众都是我的前世》）。

## 核心原则

1. **每个弹幕角色有独立人格** — 不能只是"第N号"的标签
2. **弹幕推动剧情** — 不是装饰，每个弹幕角色都有功能
3. **发言频率合理** — 不是每章每个角色都发言
4. **互动有化学反应** — 角色之间会互怼、共鸣、争论

## 弹幕角色档案模板

每个常驻弹幕角色需要建立档案：

```markdown
### 001号 赵承乾

**身份：** 唐朝将领，战死沙场
**性格：** 刚烈、忠义、嘴硬心软
**说话风格：** 文言底子+偶尔现代口语混搭，喜欢用军事比喻
**口头禅：** "哼""某当年……""这点小事"
**与主角关系：** 最先认可主角，像长辈
**功能定位：** 力量担当/情感锚点
**出场频率：** 每2-3章至少一次发言
**互动对象：** 经常和777号互怼，与247号有惺惺相惜

### 777号 [名字]

**身份：** 现代程序员，过劳死
**性格：** 毒舌、理性、闷骚
**说话风格：** 网络梗+冷笑话+技术术语
**口头禅：** "笑死""这波啊""寄了"
**功能定位：** 喜剧担当/解构严肃
**出场频率：** 每2章至少一次
**互动对象：** 经常怼001号，和247号组"理性派"
```

## 弹幕管理规则

### 发言顺序
- ❌ 不能每章都是 001→777→247→... 固定顺序
- ✅ 根据场景选择合适的角色发言（战斗场景001，搞笑场景777）
- ✅ 每章发言角色数控制在3-6个

### 弹幕密度
| 场景类型 | 弹幕频率 | 说明 |
|----------|----------|------|
| 日常/轻松 | 每200-300字 | 弹幕多，氛围轻松 |
| 紧张/战斗 | 每500字 | 弹幕少，不抢戏 |
| 催泪/情感 | 每400字 | 弹幕是情感放大器 |
| 揭秘/反转 | 反转前少，反转后爆发 | 制造对比 |

### 弹幕格式
- 编号+名字首次出现，后续可用编号
- 隔5章以上没出场的角色再次出现要带名字
- 弹幕用【】包裹，区分正文

### 去AI味
- ❌ 弹幕角色都一个语气（都是"有趣""有意思"）
- ❌ 每个角色发言都是"分析性"的（像AI在分析剧情）
- ✅ 弹幕要像真人在看直播时的反应（吐槽、惊叹、嘲讽、感动）
- ✅ 不同时代/背景的角色要用不同表达方式

## 弹幕与剧情的关系

### 自称一致性（铁律）
每个弹幕角色必须有固定自称，整部小说不变：
- 001号赵承乾 → "朕"（皇帝身份）
- 389号金万三 → "老夫"（商人身份）
- 666号李归墟 → 极少说话，以"算了"为主
- 520号冷月 → 惜字如金（"嗯""……""危险"）
- 888号孟长卿 → "老夫"（文臣身份）

**写作时必须检查：**
1. 每个角色第一次出场确定自称
2. 后续所有弹幕保持一致
3. 用 `consistency_check.py` 自动检测自称不一致
4. "我"是通用自称，但有个性的角色应该有特色自称

## 弹幕与剧情的关系

1. **弹幕可以给提示** — 但不能直接剧透
2. **弹幕可以误导** — 弹幕角色的猜测不等于真相
3. **弹幕反映读者情绪** — 读者可能在想什么，弹幕角色替他们说出来
4. **弹幕推动主角思考** — 主角从弹幕中获得启发
5. **毕业是弹幕角色的高光** — 毕业前要铺垫足够的情感

## 弹幕角色毕业设计

- 毕业不能太突然，至少提前3章铺垫
- 毕业的触发是角色自己主动申请
- 毕业场景是催泪高潮，要写得慢、写得细
- 毕业后座位变空/星光，留下视觉记忆
- 毕业对其他弹幕角色有影响（悲伤、感慨、珍惜当下）


path: references/deep_reading_guide.md
content: # 深度审读指南

## 原则

**审读 = 像人类编辑一样逐字读，不是用正则扫。**

脚本检测能发现字面问题（人名写错、数值矛盾），但发现不了：
- 角色行为不符合人设
- 情感断裂（上一章还在悲伤，下一章突然正常）
- 逻辑漏洞（这事在设定下不合理）
- 节奏问题（这段拖了、那段赶了）
- AI味（套话、空泛描写、不自然的比喻）

这些只能靠**理解**。

## 审读前准备

必须先加载（缺一不可）：

1. **大纲.md** — 知道剧情往哪走，才能判断正文是否跑偏
2. **设定词典.md** — 专有名词、称呼规范
3. **character_states.json** — 角色当前状态（关系、情感、能力）
4. **追踪表.md** — 近10章摘要，了解前情
5. **foreshadowing.json** — 未回收的伏笔

## 逐章精读流程

### 第一步：通读全文

- 不要跳读，不要只看开头结尾
- 读的过程中在心里回答：这章在讲什么？
- 如果读完说不出来 → 说明这章没有明确主线

### 第二步：逐段分析

对每一段问6个问题：

1. **推进什么？** — 剧情/情感/伏笔/世界观，至少要占一个
2. **角色反应合理吗？** — 符合人设 + 当前处境 + 上一章状态
3. **读者感受？** — 到这里会无聊/紧张/感动/困惑/想看下去？
4. **衔接上了吗？** — 跟上一段的信息/情绪/时间线是否连贯
5. **多余吗？** — 删掉这段，剧情是否受影响？不受影响就删
6. **AI味？** — 套话/空泛描写/不自然比喻/过度形容词

### 第三步：记录发现

每章写精读笔记，格式：

```markdown
## 第N章：标题

### 剧情概述
（30-50字，用自己话概括。如果概括不出来，说明这章有问题）

### 出场角色
- 角色A：什么状态，做了什么
- 角色B：什么状态，做了什么

### 情感走向
（本章情绪从哪到哪，用一个词概括）

### 发现的问题
- [P0/逻辑] 具体问题 + 为什么不合理
- [P1/角色] 具体问题 + 为什么不符合人设
- [P1/情感] 具体问题 + 情绪断层在哪
- [P2/节奏] 具体问题 + 哪里拖了/赶了

### 写得好的地方
（也要记录，不要只挑毛病）
```

## 跨章分析（全部精读完后）

### 角色弧线检查

1. 主角从第1章到最后一章，心态变化是否连续？
2. 有没有"突然开窍"/"突然黑化"/"突然原谅"的断裂？
3. 配角有没有工具人化（只在需要时出现）？

### 情感曲线检查

1. 整体有没有起伏？连续3章以上平淡就是问题
2. 高潮章的情感是否足够到位？有没有"高潮了但读者没感觉"的情况
3. 低谷章是否在铺垫？还是单纯的拖？

### 伏笔检查

1. 埋的伏笔有没有过期没回收的？
2. 回收伏笔时，读者是否有"啊原来如此"的感觉？
3. 有没有忘了伏笔的情况？

### 时间线检查

1. 事件发生顺序是否合理？
2. 有没有"昨天才发生的事，今天大家都忘了"的情况？
3. 跨天/跨周的时间过渡是否自然？

### 信息一致性

1. 角色A知道的事，后续有没有被"忘记"？
2. 某个设定前几章确立，后面有没有被推翻？
3. 读者的信息量和主角的信息量是否同步？

## 问题分级

| 级别 | 定义 | 示例 |
|------|------|------|
| **P0** | 硬伤，必须修 | 人名写错、时间线矛盾、设定冲突 |
| **P1** | 影响体验，建议修 | 角色行为不合理的、情感断裂、节奏拖沓 |
| **P2** | 锦上添花 | AI味、对话标签单调、段落略长 |

## 输出格式

### 精读报告

```markdown
# 精读报告：小说名

## 总评
- 质量评分：X/10
- 最大优点：...
- 最需要改进：...

## 分章问题清单
（按严重程度排序）

### P0（必须修）
| 章节 | 类型 | 问题 | 建议修改 |
|------|------|------|----------|
| 第N章 | 逻辑 | ... | ... |

### P1（建议修）
...

### P2（可以改）
...

## 跨章问题
### 角色弧线
- ...

### 情感曲线
- ...

### 伏笔
- ...

## 修改依赖
- 修改第N章可能影响第M章（原因）
```

### 修改清单

```markdown
# 修改清单_精读后

## 第N章
- [ ] P0: 修复人名错误（"张三"→"李四"）
- [ ] P1: 角色情绪调整（悲伤不应该这么快消失）

## 第M章
- [ ] P1: 补充情感过渡（修改第N章后需要检查这里是否受影响）
```

## 注意事项

1. **不要一次性读太多章** — 每批3-5章，保证深度理解
2. **不要带着先入为主的判断** — 先读完再下结论
3. **好的地方也要记录** — 审读不是挑刺
4. **问题要有具体建议** — 只说"这里不好"没用，要说"怎么改"
5. **注意修改连锁反应** — 改了一处可能影响其他章节


path: references/dialogue_quality.md
content: # 对话质量检查指南

## 一、AI味对话的典型特征

从实际正文中提取的常见问题：

### 1. 每句话都在推进剧情或交代信息
**实际例子**（《我的死亡日期消失了》第5章）：
> 赵恒的消息：【路上注意安全。到了江城先别急着开工，在酒店休息一下，养足精神。尽调是马拉松，不是百米冲刺。】

问题：赵恒的每条消息都像教科书——关心+建议+比喻，完美得不像人在微信上打字。真人会写"到了说一声"或"别搞太晚"。

**实际例子**（同书第15章）：
> 【赵恒，你在哪？】
> 赵恒秒回：【公司。你呢？】
> 【在家。明天我回公司，有些事想跟你聊聊。】
> 【什么事？】
> 【永盛的事。】
> 赵恒过了两分钟才回：【好。明天上午十点，我办公室。】

问题：这段反而不错——短、直接、有节奏。但"过了两分钟才回"是AI写作常见的"情绪标记"手法，用叙事提示代替了对话本身的张力。

### 2. 角色说话风格高度统一
**实际例子**（《我的死亡日期消失了》）：
陆远、赵恒、周雅、老钱的对话在信息密度和句式长度上非常接近——都是冷静、克制、逻辑清晰的短句。差异仅在内容，不在风格。

**实际例子**（轮回影院 ch01-ch04）：
虽然角色设定差异很大（皇帝、剑客、书生、赌徒），但实际对话的区别主要靠"口头禅"实现（"朕""可悲""稳了""算了"），而非说话节奏和思维方式的真正差异。

### 3. 对话像面试问答，缺乏自然跳跃
**实际例子**（同书第25章）：
> "什么时间点？"
> "不知道。但触发条件和余额系统有关。这不是重点。重点是——"
> "重点是2006年。"

每个问题都被完美回答，每个回答都引出下一个问题。真人对话会跑题、会答非所问、会跳过某些问题。

### 4. 缺少对话中的"废话"和"气口"
角色从不开玩笑、不说废话、不用语气词。每句话都有意义。

### 5. 情绪表达靠叙述而非对话本身
角色不在对话中展现情绪，而是靠叙述描写（"声音变了""停了两秒""语气微妙地变了"）。

---

## 二、真人对话的特征（对比分析）

| 维度 | AI味对话 | 真人对话 |
|------|---------|---------|
| 信息密度 | 每句都有用 | 大量废话、寒暄、重复 |
| 话题连贯性 | 严格问答链 | 经常跑题、岔开、忘记回答 |
| 句式长度 | 均匀（偏短偏工整） | 忽长忽短，受情绪影响 |
| 完整性 | 每个问题都回答 | 经常选择性忽略 |
| 礼貌程度 | 过度礼貌/过度直接 | 因关系而异，熟人之间很随意 |
| 情绪表达 | 靠叙述描写 | 直接在对话里体现（语气词、反问、打断） |
| 打断/重叠 | 几乎没有 | 频繁，尤其争论时 |
| 自相矛盾 | 几乎没有 | 常见，人的想法会变 |

---

## 三、对话质量检查清单

### 基础检查（必过项）
1. **□ 角色语言是否可辨识？** 遮住名字，能否通过说话方式分辨是谁？
2. **□ 是否存在"万能语调"？** 所有角色是否都是冷静克制短句风？
3. **□ 对话中是否有废话？** 如果每句话都在推进剧情，需要加入"水分"
4. **□ 是否有打断和抢话？** 争论场景中，对话不应是轮流发言
5. **□ 是否有答非所问？** 真人不会完美回答每个问题
6. **□ 口头禅是否仅靠词汇差异？** 角色差异应体现在句式节奏，不只是"朕""可悲"
7. **□ 情绪是否直接在对话中表达？** 不要全靠叙述描写转述情绪
8. **□ 微信/短信对话是否像真人？** 真人打字更短、更随意、会用语音/表情包

### 进阶检查（质量提升项）
9. **□ 不同关系的对话温度是否不同？** 上下级、朋友、陌生人的对话应有区别
10. **□ 是否有信息差？** 对话双方不应总是信息对等
11. **□ 对话是否有"气口"？** 沉默、犹豫、岔开话题都是真实对话的一部分
12. **□ 同一角色在不同场景下的语气是否一致？** 紧张时vs放松时的变化应符合人设
13. **□ 是否存在只有AI才会说的"总结性发言"？** 角色不应像在做演讲
14. **□ 对话节奏是否与场景匹配？** 危急时刻对话更短更碎，闲聊更长更散
15. **□ 是否有人说话绕弯子？** 不是所有人都直来直去

---

## 四、常见对话模式问题

### 问题1：每人都像在演讲
角色说话太有条理，每段话都是"观点+论据+结论"的三段式。

**坏例子**（第25章实际模式）：
> "我追查余额的第十年。也是我人生的转折点。那一年，我前妻的余额从32年跳到了17年。15年，一夜之间跳掉了15年。这个数字意味着——按照消耗速度——她只能活到55岁。"

**问题**：老钱在讲自己前妻的事，却用了"意味着——按照消耗速度"这种分析师口吻。

### 问题2：对话太礼貌
角色之间极少粗口、极少不耐烦、极少敷衍。即使在争吵中，也保持基本礼貌。

### 问题3：缺少打断和重叠
争论场景中角色轮流说话，像辩论赛而非真人吵架。真人在激动时会：
- 抢话说（"你先听我说——"）
- 重复对方的话表示不信（"你说完了？你确定你说完了？"）
- 直接无视对方继续说自己的

### 问题4：对话只服务于信息传递
所有对话都在交代设定或推进剧情，缺少"没有目的"的对话——闲聊、吐槽、发牢骚。

### 问题5：提问过于精准
角色提问总是恰好问到关键信息，像侦探审讯而非普通聊天。

---

## 五、对话风格差异化指南

### 按性格区分

| 性格类型 | 句式特征 | 常见表达 | 避免 |
|---------|---------|---------|------|
| 强势果断型 | 短句、祈使句、少解释 | "不用了。""就这个。""明天再说。" | 长篇解释、犹豫语气 |
| 谨慎内敛型 | 短句、多停顿、不直说 | "……不太确定。""可能吧。" | 直接下结论 |
| 话痨社交型 | 长句、跳跃、信息过量 | "诶你知道吗……""对对对！" | 简短冷淡 |
| 冷漠寡言型 | 极短、省略主语 | "嗯。""说完了？" | 热情、主动 |
| 知识分子型 | 长句、爱用比喻、好为人师 | "这个事情本质上是……" | 粗俗表达 |
| 老油条型 | 模糊、绕弯子、不说死 | "不好说。""看情况吧。" | 明确表态 |

### 按关系区分
- **上下级**：下级用省略句（"明天交。""收到。"），上级用完整句或反问句
- **亲密关系**：省略寒暄，直接说事，偶尔刻薄但不生气
- **陌生人**：多客套话、多确认（"你是说……？""不好意思请问……"）
- **对立关系**：话里有话、选择性回应、故意曲解

### 按场景区分
- **紧急场景**：句子更短、更多省略、更多打断
- **闲聊场景**：句子更长、更多跑题、更多废话
- **谈判/对抗**：每句话都有两层意思、大量沉默

---

## 六、坏对话→好对话改写对比

### 示例1：微信对话（出自《我的死亡日期消失了》第5章）

**原文（AI味重）：**
> 赵恒的消息：【路上注意安全。到了江城先别急着开工，在酒店休息一下，养足精神。尽调是马拉松，不是百米冲刺。】

**改写：**
> 赵恒的消息：【到了说一声。别一去就开会。】
> 过了几秒又发了一条：【那边的米粉不错，高铁站出来左拐。】

理由：真人微信不会一次性发这么完整的信息。关心和建议分开发、用短句、加入无关的私人建议（米粉）更自然。

### 示例2：信息交代型对话（出自第25章）

**原文（AI味重）：**
> "我追查余额的第十年。也是我人生的转折点。那一年，我前妻的余额从32年跳到了17年。15年，一夜之间跳掉了15年。这个数字意味着——按照消耗速度——她只能活到55岁。"

**改写：**
> 老钱翻开2006年的笔记本，看了很久。
> "张敏。"他说。
> 陆远等着。
> "我前妻。"老钱合上本子，"06年四月，有人告诉我——她出事了。"
> "什么事？"
> "余额。一夜之间掉了十五年。"
> 他说这话的时候没有看陆远，盯着铁盒子上的锈迹。
> "三十二变十七。她那年三十八。"

理由：把陈述句拆成对话，利用停顿和动作代替长段独白。去掉"意味着——按照消耗速度"的分析师口吻。

### 示例3：角色群戏（出自轮回影院 ch04）

**原文（差异靠口头禅）：**
> "不行！"背剑的年轻人猛地站起来，"这个女人不对劲！我之前就说了——她身上有杀气！"
> "什么杀气？"赌袍男人翻白眼，"人家就是请你吃个饭，你看出杀气来了？"
> "我的直觉不会错！"

**改写（差异靠句式节奏）：**
> "等等——"127号的手已经按在剑柄上了，"不行。这女人不对。"
> "哪里不对？"老六翻了个白眼。
> "就是不对！我说不上来，但她看他的眼神——"
> "人家看的是PPT，不是他。你是不是武侠看多了？"
> "……闭嘴。"

理由：剑客说话应该更碎片化、更直觉型（"就是不对！我说不上来"），赌徒说话更轻浮、爱用反问。不用每句话都完整。

### 示例4：老板指示型对话

**原文模式（实际正文中较常见）：**
> "陆总，赵总刚发了消息。"周雅把手机递过来。
> 赵恒的消息：【明天见孙磊的时候，注意一个问题：他说话特别有逻辑，但逻辑链条里有的时候会少一环。你要抓住那个缺环。】

**改写：**
> "陆总，赵总发的。"周雅把手机递过来。
> 赵恒：【孙磊说话很顺，太顺了。听的时候注意他跳过了什么。】

理由：指示不需要像管理学教材。用更直觉的表达（"太顺了"比"逻辑链条里有的时候会少一环"更自然）。

---

## 七、Agent使用说明

### 写作时
- 每写完一段密集对话，过一遍检查清单前8项
- 特别关注：遮住名字能否分辨是谁说话
- 如果发现所有角色都是"冷静克制短句风"，立刻调整

### 审校时
- 用 `consistency_check.py` 的AI词检测后，额外检查对话中的AI味
- 重点检查：信息交代型对话（最容易出现AI味）
- 优先修改：演讲式独白、教科书式指示、完美问答链

### 常用修改策略
1. **拆长为短**：一段长独白拆成多轮对话
2. **加入废话**：在关键信息前插入闲聊或吐槽
3. **制造停顿**：用动作描写打断对话流
4. **制造不完美**：让角色说错话、改口、没听清
5. **差异化节奏**：给每个角色不同的句子长度偏好


path: references/emotion_arc_guide.md
content: # 情感弧线实操指南

情感弧线是读者情绪随章节推进的起伏轨迹。和节奏模板（控制信息密度、句式长短）不同，情感弧线控制的是**读者心里那根弦的松紧**。

节奏是骨架，情感弧线是血肉。两者配合，章节才有呼吸感。

---

## 一、5 种情感弧线模式

### 1. 上升弧线（从平静到爆发）

**走势**：情绪起点低/平静 → 逐层加码 → 顶点爆发

**铺垫写法**（前40%章节）：
- 用日常场景建立"平静"基准线，让读者放松警惕
- 埋入2-3个轻微异常（不是悬念钩子，而是情感种子）
- 角色在此阶段处于"不知道"或"不想面对"的状态
- 用感官细节暗示暗流（如阳光很好但风有点凉）

**高潮写法**（中间40%）：
- 信息逐层揭开，每次揭开都让情绪升高一级
- 用短句加速节奏，但不超过紧张模板的密度
- 对话减少，内心独白/沉默增多
- 关键技法：**克制**——角色越克制，读者越替他难受

**收束写法**（最后20%）：
- 爆发后不拖沓，用1-2个安静的画面收尾
- 金句或沉默，二选一
- 可以留一个余韵钩子，但不要留悬念钩子（两种钩子不同）

**适用场景**：
- 揭露类情节（发现真相、揭开秘密）
- 角色觉醒（从逃避到面对）
- 悬疑小说的核心线索推进章

**实战示例**（来自《我的死亡日期消失了》第9章「没有余额的人」）：

> 铺垫（日常场景）：
> "孙磊坐在对面，正在讲银行借款的明细。'目前永盛的银行借款总计8.7亿……'"
>
> 情绪爬升（发现异常）：
> "孙磊的头顶——昨天还是42年137天的余额——什么都没有。不是零。不是在跳动。是空白。"
>
> 高潮（内心冲击+克制）：
> "他的后背开始冒冷汗。但他的表情没有任何变化——十年投行练出来的基本功，你可以在心里天崩地裂，脸上不能有任何波动。"
>
> 收束（安静画面+金句）：
> "孙磊那杯没喝完的咖啡还在桌上。水位比两个小时前低了两厘米。但陆远没有听到过他吞咽的声音。一次都没有。"

**为什么有效**：从"正常的尽调会议"到"余额空白"到"他在喝咖啡但没在吞咽"，情绪三级跳。但主角全程克制，读者的情绪无处释放，只能憋着——这就是上升弧线的核心：**替角色急**。

---

### 2. 下降弧线（从美好到坠落）

**走势**：情绪起点高/温暖 → 裂缝出现 → 坍塌

**铺垫写法**（前40%章节）：
- 建立美好/温暖的场景，越具体越好
- 用细节让读者产生"这段关系/这个人真好"的共鸣
- 关键：铺垫越甜/越暖，后面的坠落越痛
- 可以让角色在这个阶段说一些日后回想会扎心的话

**高潮写法**（中间40%）：
- 裂缝先是一个小细节，不是大事件
- 角色试图修补/忽视裂缝，但裂缝在扩大
- 用回忆穿插制造对比（"刚才还好好的"→"原来从那时候就不对了"）
- 关键技法：**不对等感**——一个角色还在笑，另一个已经知道要完了

**收束写法**（最后20%）：
- 坍塌后不要立刻进入下一个情节
- 给角色（和读者）一个"空"的时刻
- 用物品/场景承载情绪（空椅子、没关的灯、没吃完的饭）
- 结尾可以很轻——越是大事，写得越轻越扎心

**适用场景**：
- 离别/分离
- 背叛揭露
- 角色牺牲
- 回忆杀（美好回忆→残酷现实）

**实战示例**（来自《我的死亡日期消失了》第1章，外婆段落）：

> 铺垫（温暖）：
> "对外婆说'外婆你头上有个数字在变'。外婆愣了一下，然后笑着摸他的头，说小孩子不要乱讲话。"
>
> 裂缝+坠落（精确的残酷）：
> "三天后外婆的余额归零了。她停顿了0.2秒，然后继续笑着给他盛饭。但那碗饭她没有吃完。"
>
> 收束（轻到极点的悲剧）：
> "从此他再也没有对任何人说过。"

**为什么有效**：整个下降弧线只用了一个段落。但"0.2秒"这个精确的数字比任何形容词都有力。外婆"继续笑着盛饭"是角色的克制，"那碗饭她没有吃完"是事实的残酷，"从此再也没有"是永久的空洞。三层递进，每个字都在读者的伤口上按。

---

### 3. U型弧线（先抑后扬）

**走势**：低谷/困境 → 挣扎 → 转机 → 升起

**铺垫写法**（前35%章节）：
- 直接展示角色的困境/低谷，不需要先铺垫"曾经多好"
- 用具体数字/事实代替心理描写（"余额437元"比"他很穷"有力）
- 让配角也处在困境中，但各人以不同方式应对
- 可以用弹幕/旁观者视角制造苦中作乐的效果

**高潮写法**（中间40%）：
- 转机不是突然降临的，而是角色在困境中的一个小行动触发的
- 转机出现时角色不一定立刻意识到（事后回想才懂）
- 用"差点放弃→咬咬牙再撑一下"的节奏
- 关键技法：**苦中有人情**——不是一个人扛，而是有人默默帮你

**收束写法**（最后25%）：
- 升起阶段不要太快/太圆满，留一点"还没完全好"的余味
- 用一个温暖的集体场景收束（碰杯/围坐/一起笑）
- 可以在这里埋下一章的伏笔
- 结尾语气从沉重变轻盈，但不要变到"一切OK"的程度

**适用场景**：
- 创业低谷期
- 团队共渡难关
- 角色从失败中爬起来
- 日常章和催泪章之间的过渡

**实战示例**（来自《观众都是我的前世》第35章「穷日子回忆」）：

> 低谷（困境具象化）：
> "余额：437元。三个人。房租月底交，还有两千三。"
> "空调是坏的，房东说修，拖了一周也没来。八月的深圳，闷得人喘不上气。三个人光着膀子写方案，电风扇吹出来的全是热风。"
>
> 挣扎（苦中有人情）：
> "房东大姐没听懂，但也没追问。她沉默了几秒钟，摆了摆手：'月底之前给我就行，不算你违约。'林昭说了谢谢，关上门，背靠着门板滑坐下去。何薇从桌边转过头来看他，眼眶有点红。小王还在修椅子，假装什么都没听见。"
>
> 转机+升起：
> "那天晚上三个人吃了一锅泡面，加了两个鸡蛋，算是庆祝又撑过了一关。"
>
> 收束（温暖的集体场景）：
> "四个人碰了碰杯子。'敬活着。'"

**为什么有效**：U型弧线的核心不是"从穷到富"，而是"在穷的时候还有人陪你扛"。437元是数字冲击，房东大姐的宽限是人情的温暖，泡面加两个鸡蛋是苦中作乐。读者跟着角色一起经历谷底，升起的时候才有真实的感动。

---

### 4. 倒U型弧线（先扬后抑）

**走势**：美好/希望 → 顶点 → 突然坠落 → 沉入

**铺垫写法**（前35%章节）：
- 建立角色的高光时刻或美好期待
- 让读者以为"这次稳了""终于要好了"
- 用细节强化美好感（可以和后面坠落形成对照）
- 弹幕/旁观者在此阶段也要表现出乐观

**高潮写法**（中间35%）：
- 顶点之后，用一个看似不起眼的细节开启坠落
- 坠落速度要快——倒U型的核心就是"美好的东西碎得很快"
- 角色从"以为没事"到"意识到出事了"的过程要短
- 关键技法：**延迟感知**——读者先于角色意识到不对劲

**收束写法**（最后30%）：
- 坠落后的沉入阶段，用回忆和对比制造余痛
- 不要立刻给解决方案或安慰
- 用一个和开头呼应的细节（同一个场景/同一句话/同一样东西），但语境完全变了
- 结尾可以是沉默、可以是角色的一个微小动作

**适用场景**：
- 希望被打碎（以为安全了结果没有）
- 告别/送别（先回忆美好，再面对分离）
- 角色牺牲前的高光时刻
- 毕业场景（前世离开影院）

**实战示例**（来自《观众都是我的前世》第5章「第一滴泪」，777号老六毕业）：

> 顶点（美好/温暖）：
> "他笑了。不是平时那种嘻嘻哈哈的笑，而是一种很干净的、轻松的笑。'林昭，你知道吗——我活了三十八年，赌了三十八年。从十二岁第一次摸骰子开始，我就没停过。'"
> "'但今天，我想放手了。'"
>
> 坠落（毕业=永远的离开）：
> "他的身体开始化为光点。从指尖开始，被风吹散的蒲公英。一点一点，飘向穹顶。赌袍的花纹彻底消失了。老六站在那里，最后的样子，是一个穿着普通灰色布衣的年轻人。没有花哨的赌袍，没有夸张的笑声。只是一个三十八岁就死去的、赌了一辈子的普通人。"
>
> 沉入（其他角色的反应）：
> "书生低着头，折扇停在半空，嘴唇微微颤抖。他一句话都没说——对于256号沈墨白来说，这是最大的尊重。"
> "赵承乾站在原地，看着穹顶上那颗星。很久。'他是第一个。'……'不会是最后一个。'"
>
> 呼应收束：
> "林昭翻了个身，拿起手机。凌晨五点十七分。……眼眶有点湿。他说不清是因为老六的故事，还是因为别的什么。也许都有一点。"

**为什么有效**：倒U型的痛感来自"刚觉得温暖就要失去"。老六的毕业是美好的（他放下了、他自由了），但毕业意味着永远消失。读者在"为他高兴"和"不想他走"之间被撕扯——这正是倒U型弧线的杀伤力。

---

### 5. 波浪弧线（反复起伏）

**走势**：起→落→起→落→……→最终停在某个位置（不一定是起也不一定是落）

**铺垫写法**（前20%章节）：
- 快速建立情绪基准线，不需要太长的铺垫
- 确立第一个"起"或第一个"落"
- 用对比鲜明的场景/对话制造第一个波峰或波谷

**中间波动写法**（中间60%）：
- 每次起伏之间要有明确的触发事件（不是无端变化）
- 起伏幅度逐渐加大（像波浪一样越荡越高）
- 每次起伏切换时，用1-2句过渡，不要突兀跳转
- 关键技法：**情绪错位**——角色和读者对同一件事的反应不同步

**收束写法**（最后20%）：
- 最终停在哪个位置取决于下一章的需要
  - 停在"起"：给读者希望，适合接轻松/日常章
  - 停在"落"：给读者悬念/不安，适合接紧张/悬念章
- 用一个和开头呼应但情绪完全不同的细节收尾
- 不要在波浪弧线结尾做总结性旁白

**适用场景**：
- 重逢/复合场景（期待→尴尬→打开话匣子→旧伤被触→重新连接）
- 角色内心纠结（反复在两个选择间摇摆）
- 信息逐层揭开的推理章
- 紧张章和日常章之间的过渡

**实战示例**（来自《我的死亡日期消失了》第26章「因为你姓陆」）：

> 波峰1（日常对话的轻松）：
> "老钱单手握方向盘，另一只手从中央扶手箱里摸出一包烟。他抽出一根，没点，夹在手指间转了两圈。'我不抽烟了。戒了三年。但有时候还是想摸一下。'"
>
> 波谷1（信息揭露的沉重）：
> "能看到余额的人——不是随机的。……他们是被'选择'的。"
>
> 波峰2（回忆的温度）：
> "我追了三十年。……得出的结论只有一个。"（老钱在追寻真相的使命感）
>
> 波谷2（更深的残酷）：
> "2005年底。心脏衰竭。我在医院陪了最后一周。他走的那个晚上——我看到了他的余额归零。零点几秒的停顿。"
>
> 最终停在"落"（悬念+不安）：
> "永盛大楼就站在面前。他第一次来的时候，觉得这栋楼'突兀'。现在他觉得这栋楼像一扇门。一扇他必须走进去的门。"

**为什么有效**：波浪弧线让信息量大的章节不显得平。老钱的车上对话包含大量设定信息，如果用平铺直叙会变成"设定灌输"。用波浪弧线，每一段新信息都和一段情绪起伏绑定，读者在接受信息的同时也在经历情感波动。

---

## 二、情感弧线与节奏模板的配合

| 情感弧线 | 最佳配合的节奏模板 | 原因 |
|----------|-------------------|------|
| 上升弧线 | 悬念节奏 → 紧张节奏 | 前半段用悬念模板铺信息，后半段切紧张模板推高潮 |
| 下降弧线 | 日常节奏 → 催泪节奏 | 前半段用日常建立温暖，后半段切催泪节奏完成坠落 |
| U型弧线 | 催泪节奏 → 日常节奏 | 前半段催泪展示困境，后半段日常展示温暖恢复 |
| 倒U型弧线 | 日常节奏 → 催泪节奏 | 和下降弧线类似，但顶点更高、坠落更陡 |
| 波浪弧线 | 混合使用，每2-3段切换 | 波浪弧线本身就要求节奏变化，可穿插紧张/日常/悬念 |

**使用建议**：
1. 一个章节只用一种情感弧线，不要混用
2. 弧线类型由章节在全书中的位置决定（见下表）
3. 节奏模板负责"怎么写"（句式、密度），情感弧线负责"写什么情绪"
4. 两者叠加才是完整的写作方案

---

## 三、情感弧线与角色弧线的联动

角色弧线是角色从A状态成长到B状态的轨迹。情感弧线是为角色弧线服务的——不同的成长阶段，需要不同的情感弧线来放大效果。

### 角色成长阶段 × 推荐情感弧线

| 角色成长阶段 | 角色状态 | 推荐情感弧线 | 作用 |
|-------------|---------|-------------|------|
| 逃避期 | 知道问题但不想面对 | 上升弧线 | 把角色逼到不得不面对 |
| 动摇期 | 开始怀疑自己/旧信念 | 波浪弧线 | 内心反复纠结，外化为情节起伏 |
| 觉醒期 | 接受新认知/做出选择 | 倒U型弧线 | 高光之后立刻付代价，让觉醒不廉价 |
| 考验期 | 用新认知面对困难 | U型弧线 | 低谷中证明成长是真实的 |
| 新生期 | 完成蜕变 | 下降弧线（从旧自我的视角） | 用旧事物的消逝衬托新自我的诞生 |

### 两部小说实例

**《我的死亡日期消失了》·陆远的角色弧线**：
- 逃避期（Ch1-8）：用上升弧线，从"正常投行精英"到发现孙磊余额空白，被迫进入真相
- 动摇期（Ch9-20）：用波浪弧线，在"孙磊是死人"和"系统是什么"之间反复试探
- 觉醒期（Ch21-30）：用倒U型弧线，发现永盛秘密的高光之后，立刻面对"你被选择"的代价
- 考验期（Ch31-37）：用U型弧线，在食堂观察、验证中坚持追查

**《观众都是我的前世》·老六的角色弧线**（浓缩在一章内完成）：
- 逃避期→觉醒期：用倒U型弧线，从"赌了一辈子"到"我想放手"再到毕业消散
- 关键联动：老六的成长弧线越完整，毕业时的情感冲击越大——读者见证了一个人用一辈子才学会"放手"，然后他立刻就走了

---

## 四、10章周期的情感弧线安排

结合 `writing_rhythm_guide.md` 的10章周期模板，情感弧线的安排建议：

| 章节位置 | 节奏类型 | 推荐情感弧线 | 说明 |
|---------|---------|-------------|------|
| 第1章 | 日常 | 波浪 | 开篇用信息量大的波浪弧线吸引读者 |
| 第2章 | 紧张 | 上升 | 拉起紧张感 |
| 第3章 | 日常 | U型 | 紧张后给喘息，用温暖收束 |
| 第4章 | 悬念 | 波浪 | 推进信息，保持好奇 |
| 第5章 | 催泪 | 倒U型 | 情感高潮章，先暖后痛 |
| 第6章 | 日常 | U型 | 催泪后恢复，低谷中寻找温暖 |
| 第7章 | 紧张 | 上升 | 再次拉高 |
| 第8章 | 悬念 | 波浪 | 信息密集章 |
| 第9章 | 催泪 | 下降 | 第二个情感点，直接扎心 |
| 第10章 | 转折 | 上升 | 转折章用上升弧线推到顶，留悬念进下一周期 |

**核心原则**：
- 催泪章不连续（间隔≥2章）
- 上升弧线适合做章节组的收尾
- 波浪弧线适合做信息量大的中间章
- U型弧线适合做紧张章之后的过渡

---

## 五、快速决策表

写章节前问自己三个问题：

| 问题 | 判断 |
|------|------|
| 这一章结束时读者应该哭、笑、还是悬？ | 哭→下降/倒U；笑→U型；悬→上升/波浪 |
| 角色在这一章经历什么变化？ | 变好→U型/上升；变差→下降/倒U；纠结→波浪 |
| 上一章用了什么弧线？ | 不要连续用同一种 |

---

## 六、Agent 使用说明

### 写作时
1. 写章节前先确定情感弧线类型
2. 在写作提示中明确指定：`本章使用上升弧线，从日常场景切入，中段信息逐层揭开，结尾用安静画面收束`
3. 参考对应的实战示例，模仿其铺垫→高潮→收束的结构

### 审校时
1. 检查章节的情绪是否按预期弧线走（有没有"该痛的时候没痛"或"该暖的时候突然冷了"）
2. 检查连续章节是否用了同一种弧线（应该避免）
3. 检查角色成长阶段和弧线是否匹配

### 验证命令
```bash
# 检查近10章的情感弧线分布（需配合 rhythm_check.py）
python3 scripts/rhythm_check.py --chapters-dir upload/ --suggest

# 如果连续3章以上提示同一种节奏，考虑调整情感弧线
```


path: references/foreshadowing.md
content: # 伏笔管理机制

> 长篇小说伏笔追踪系统。解决核心问题：第5章埋的伏笔，第50章回收时容易忘。

---

## 一、伏笔登记模板（JSON格式）

Agent 读写伏笔表时使用以下结构，存储为 `伏笔表.json`：

```json
{
  "novel": "小说名称",
  "lastUpdated": "2026-04-10",
  "foreshadowings": [
    {
      "id": "FS-001",
      "description": "伏笔内容简述",
      "plantedChapter": 5,
      "plantedVolume": 1,
      "plantedDetail": "埋设时的具体描写/原文关键词",
      "expectedRecallChapter": 50,
      "expectedRecallVolume": 3,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "mystery",
      "recallHints": ["回收时需要的前置铺垫"],
      "relatedCharacters": ["角色名"],
      "notes": "补充说明"
    }
  ]
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 唯一标识，格式 `FS-NNN` |
| `description` | string | 伏笔内容一句话描述 |
| `plantedChapter` | number | 埋设章节号 |
| `plantedVolume` | number | 埋设卷号 |
| `plantedDetail` | string | 埋设时的原文关键词或具体描写 |
| `expectedRecallChapter` | number/null | 预计回收章节（不确定填null） |
| `expectedRecallVolume` | number/null | 预计回收卷号 |
| `actualRecallChapter` | number/null | 实际回收章节（未回收为null） |
| `status` | enum | `planted`（已埋）/ `hinted`（已铺垫）/ `recalled`（已回收）/ `abandoned`（已废弃） |
| `priority` | enum | `high`（核心伏笔）/ `medium`（重要）/ `low`（点缀） |
| `category` | enum | `mystery`（悬疑）/ `character`（角色）/ `emotion`（情感）/ `setting`（设定）/ `plot`（情节） |
| `recallHints` | string[] | 回收前需要的前置铺垫列表 |
| `relatedCharacters` | string[] | 相关角色 |
| `notes` | string | 补充说明 |

---

## 二、埋设操作规范

### 埋伏笔时必须做的事

1. **立即登记**：在 `伏笔表.json` 中新增一条记录
2. **标记原文**：在章节笔记中标注 `[FS-NNN]` 伏笔标记
3. **设定预期**：填写 `expectedRecallChapter`（至少估算一个范围）
4. **评估优先级**：核心反转用 `high`，辅助线索用 `medium`，氛围点缀用 `low`

### 埋设原则

- **自然融入**：伏笔应像正常叙事的一部分，不能像"此处有伏笔"的标注
- **可验证**：回收时读者翻回去能找到对应描写
- **密度控制**：每章新埋伏笔不超过2条，同时活跃的未回收伏笔不超过5条
- **分级管理**：`high` 级伏笔必须在大纲中标注回收节点

---

## 三、回收前检查流程

### 写每章前的标准检查

```
步骤1：读取伏笔表，筛选 status=planted 或 status=hinted 的记录
步骤2：按 expectedRecallChapter 排序，找出"本章应回收"的伏笔
步骤3：检查 recallHints，确认前置铺垫是否已到位
步骤4：决定本章操作——回收 / 提前铺垫(hinted) / 延后（更新expectedRecallChapter）
步骤5：回收后更新 actualRecallChapter 和 status
```

### 回收前的铺垫检查

回收 `high` 级伏笔前，至少需要 **2次前置暗示**（`hinted` 状态更新）：

- 第1次暗示：回收前5-10章，轻触伏笔相关元素
- 第2次暗示：回收前1-3章，营造悬念氛围
- 正式回收：本章揭示

---

## 四、过期预警机制

### 预警规则

| 条件 | 预警级别 | 处理建议 |
|------|---------|---------|
| 埋设超过 **20章** 未回收 | ⚠️ 注意 | 检查是否需要在近期铺垫 |
| 埋设超过 **50章** 未回收 | 🔴 警告 | 必须在5章内铺垫或废弃 |
| 埋设超过 **100章** 未回收 | 🚨 紧急 | 立即决定回收或废弃，不可再拖 |
| `expectedRecallChapter` 已过但未回收 | 📅 逾期 | 更新预期章节或执行回收 |

### 预警检查方式

写作每章前，Agent 自动执行：

```bash
# 伪代码逻辑
for fs in foreshadowings:
  if fs.status == "planted":
    gap = current_chapter - fs.plantedChapter
    if gap > 100: emit("🚨紧急", fs)
    elif gap > 50: emit("🔴警告", fs)
    elif gap > 20: emit("⚠️注意", fs)
  if fs.expectedRecallChapter and fs.expectedRecallChapter < current_chapter:
    emit("📅逾期", fs)
```

---

## 五、回收质量标准

### 好的回收 vs 差的回收

| 标准 | ✅ 好的回收 | ❌ 差的回收 |
|------|-----------|-----------|
| 铺垫 | 有2+次前置暗示 | 突然跳出，无任何准备 |
| 逻辑 | 读者回看能找到线索 | 强行圆场，逻辑牵强 |
| 情感 | 读者"恍然大悟" | 读者"这也行？" |
| 节奏 | 在合适的情节高潮回收 | 随便找个地方塞进去 |
| 密度 | 同一章最多回收2条伏笔 | 一章回收5条，信息过载 |

### 回收时的操作

1. 更新 `status` → `recalled`
2. 填写 `actualRecallChapter`
3. 在章节笔记中标注 `[FS-NNN 已回收]`
4. 检查是否有依赖此伏笔的后续伏笔需要激活

---

## 六、具体示例

### 示例A：《我的死亡日期消失了》（悬疑类）

悬疑类小说伏笔密集，多为 **谜团型** 伏笔（埋设→暗示→大反转回收）。

```json
{
  "novel": "我的死亡日期消失了",
  "foreshadowings": [
    {
      "id": "FS-001",
      "description": "公司里有人余额归零后还上了三个月的班——系统存在暗示",
      "plantedChapter": 1,
      "plantedVolume": 1,
      "plantedDetail": "第1章发现'死人'还在上班",
      "expectedRecallChapter": 56,
      "expectedRecallVolume": 7,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "mystery",
      "recallHints": ["赵恒暴露异常", "寿命余额交易机制"],
      "relatedCharacters": ["陆远", "赵恒"],
      "notes": "全书开篇核心悬念，第7卷赵恒摊牌时回收"
    },
    {
      "id": "FS-002",
      "description": "老钱暗中知道系统存在，从第3卷开始守护陆远",
      "plantedChapter": 10,
      "plantedVolume": 3,
      "plantedDetail": "老钱看似随意的提醒，实际是在保护",
      "expectedRecallChapter": 100,
      "expectedRecallVolume": 13,
      "actualRecallChapter": null,
      "status": "hinted",
      "priority": "high",
      "category": "character",
      "recallHints": ["老钱多次反常行为", "第13卷老钱之死前的坦白"],
      "relatedCharacters": ["老钱", "陆远"],
      "notes": "第13卷老钱牺牲时揭示——他一直知道。催泪型回收"
    },
    {
      "id": "FS-003",
      "description": "陆远父亲是系统创建者——全书最大反转",
      "plantedChapter": 5,
      "plantedVolume": 1,
      "plantedDetail": "父亲已故15年，留下的遗物/线索",
      "expectedRecallChapter": 128,
      "expectedRecallVolume": 16,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "mystery",
      "recallHints": ["父亲遗物中的异常", "系统运行600年的线索", "道印相关设定"],
      "relatedCharacters": ["陆远", "陆远父亲"],
      "notes": "第16卷核心反转，需在前10卷埋设3-5处暗示"
    },
    {
      "id": "FS-004",
      "description": "沈清第12卷开始看到余额——她有特殊体质",
      "plantedChapter": 33,
      "plantedVolume": 5,
      "plantedDetail": "沈清对陆远异常行为的好奇/直觉",
      "expectedRecallChapter": 96,
      "expectedRecallVolume": 12,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "character",
      "recallHints": ["沈清多次展现出异常直觉", "与陆远的亲密接触"],
      "relatedCharacters": ["沈清", "陆远"],
      "notes": "催泪+惊喜型回收，前5卷沈清需要'好像感觉到了什么'的暗示"
    },
    {
      "id": "FS-005",
      "description": "方哲500年前为救女儿成为执行器",
      "plantedChapter": 30,
      "plantedVolume": 4,
      "plantedDetail": "方哲'永远在笑'的异常",
      "expectedRecallChapter": 88,
      "expectedRecallVolume": 11,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "medium",
      "category": "character",
      "recallHints": ["方哲笑容下的疲惫瞬间", "与女儿相关的话题回避"],
      "relatedCharacters": ["方哲"],
      "notes": "第11卷揭示背景，让反派有人性维度"
    }
  ]
}
```

### 示例B：《观众都是我的前世》（催泪类）

催泪类小说伏笔多为 **情感型** 伏笔（前期微妙暗示→中期铺垫→毕业时集中爆发）。

```json
{
  "novel": "观众都是我的前世",
  "foreshadowings": [
    {
      "id": "FS-001",
      "description": "999号不是前世，是来世的林昭——全书最大悬念",
      "plantedChapter": 1,
      "plantedVolume": 1,
      "plantedDetail": "999号鼓掌一次，扶手微光如心跳",
      "expectedRecallChapter": 242,
      "expectedRecallVolume": 8,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "mystery",
      "recallHints": ["Ch6扶手微光", "Ch14倒计时", "Ch17'快了'", "Ch19转头朝周衍", "Ch22光团异动"],
      "relatedCharacters": ["999号", "林昭"],
      "notes": "超长线伏笔（242章），需每20章左右给一次微弱信号"
    },
    {
      "id": "FS-002",
      "description": "苏晚也能'听到声音'，疑似持有者",
      "plantedChapter": 20,
      "plantedVolume": 1,
      "plantedDetail": "苏晚重逢时对林昭异常的直觉反应",
      "expectedRecallChapter": 182,
      "expectedRecallVolume": 7,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "character",
      "recallHints": ["苏晚多次展现不合理的直觉", "与林昭的同步反应"],
      "relatedCharacters": ["苏晚", "林昭"],
      "notes": "Ch182揭示，Ch457走进影院——唯一进入影院的现世人"
    },
    {
      "id": "FS-003",
      "description": "001号赵承乾与666号李归墟的'空殿共鸣'——亡国帝王的孤独",
      "plantedChapter": 9,
      "plantedVolume": 1,
      "plantedDetail": "001号银幕上看到666号记忆，末年相似",
      "expectedRecallChapter": 189,
      "expectedRecallVolume": 7,
      "actualRecallChapter": null,
      "status": "hinted",
      "priority": "medium",
      "category": "emotion",
      "recallHints": ["001号对666号'丧'的理解", "666号毕业时001号的反应"],
      "relatedCharacters": ["赵承乾", "李归墟"],
      "notes": "情感型伏笔——两个坐过龙椅的人的共鸣，666号毕业时爆发"
    },
    {
      "id": "FS-004",
      "description": "冷月的感知信号体系——右手悬空/握刀/松开",
      "plantedChapter": 19,
      "plantedVolume": 1,
      "plantedDetail": "冷月右手摸腰间=有意外、握刀=危险、松开=安全",
      "expectedRecallChapter": 108,
      "expectedRecallVolume": 4,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "medium",
      "category": "setting",
      "recallHints": ["多次使用感知信号", "关键时刻信号与剧情呼应"],
      "relatedCharacters": ["冷月"],
      "notes": "第4卷冷月毕业时，最后一次感知——信号本身成为催泪点"
    },
    {
      "id": "FS-005",
      "description": "沈岩提到的887号——'别的持有者的记忆'作为锦囊",
      "plantedChapter": 23,
      "plantedVolume": 1,
      "plantedDetail": "沈岩说退出后887号会发别的持有者的记忆",
      "expectedRecallChapter": 180,
      "expectedRecallVolume": 6,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "medium",
      "category": "setting",
      "recallHints": ["锦囊内容的异常", "887号与999号的对应关系"],
      "relatedCharacters": ["沈岩", "999号"],
      "notes": "与999号身份伏笔联动——887号=沈岩的999号"
    },
    {
      "id": "FS-006",
      "description": "周衍名片背面'轮回计划·外联组'——全书第一次出现轮回计划",
      "plantedChapter": 26,
      "plantedVolume": 1,
      "plantedDetail": "名片背面右下角极小字",
      "expectedRecallChapter": 155,
      "expectedRecallVolume": 6,
      "actualRecallChapter": null,
      "status": "planted",
      "priority": "high",
      "category": "mystery",
      "recallHints": ["周衍的身份疑点", "轮回计划的逐步揭示"],
      "relatedCharacters": ["周衍"],
      "notes": "Ch155周衍正面登场时部分回收，但'轮回计划'全貌更晚揭示"
    }
  ]
}
```

### 两类小说的伏笔差异总结

| 维度 | 悬疑类（《死亡日期》） | 催泪类（《观众》） |
|------|---------------------|-------------------|
| 伏笔密度 | 高，几乎每章有线索 | 中，集中在情感铺垫 |
| 回收周期 | 中等（10-30章） | 长线（100-200章） |
| 回收方式 | 逻辑反转，"原来如此" | 情感爆发，"原来他一直在…" |
| 铺垫频率 | 需要多次逻辑暗示 | 需要多次情感微场景 |
| 过期风险 | 低（回收节奏快） | 高（长线容易忘） |
| 关键提醒 | 逻辑一致性是生命线 | 情感连贯性是生命线 |

---

## 七、Agent 操作指令

### 初始化伏笔表

```bash
# 在小说工作目录创建伏笔表
cat > 伏笔表.json << 'EOF'
{"novel":"小说名","lastUpdated":"日期","foreshadowings":[]}
EOF
```

### 写每章前检查

```bash
# 读取伏笔表，关注 planted 和 hinted 状态的记录
# 按当前章节号检查是否有应回收的伏笔
# 检查预警规则（20/50/100章阈值）
python3 -c "
import json
data = json.load(open('伏笔表.json'))
current = 当前章节号
for fs in data['foreshadowings']:
    if fs['status'] in ('planted','hinted'):
        gap = current - fs['plantedChapter']
        if gap > 50: print(f'🔴 {fs[\"id\"]}: {gap}章未回收 - {fs[\"description\"]}')
        elif gap > 20: print(f'⚠️ {fs[\"id\"]}: {gap}章未回收 - {fs[\"description\"]}')
        if fs.get('expectedRecallChapter') and fs['expectedRecallChapter'] <= current:
            print(f'📅 {fs[\"id\"]}: 预期{fs[\"expectedRecallChapter\"]}章回收，已到 - {fs[\"description\"]}')
"
```

### 埋设伏笔

```bash
# 追加新伏笔到表中
python3 -c "
import json
data = json.load(open('伏笔表.json'))
data['foreshadowings'].append({
    'id': 'FS-NNN',
    'description': '描述',
    'plantedChapter': 当前章节,
    'plantedVolume': 当前卷,
    'plantedDetail': '原文关键词',
    'expectedRecallChapter': 预期章节,
    'expectedRecallVolume': 预期卷,
    'actualRecallChapter': None,
    'status': 'planted',
    'priority': 'high/medium/low',
    'category': 'mystery/character/emotion/setting/plot',
    'recallHints': [],
    'relatedCharacters': [],
    'notes': ''
})
data['lastUpdated'] = '日期'
json.dump(data, open('伏笔表.json','w'), ensure_ascii=False, indent=2)
"
```

### 回收伏笔

```bash
# 更新伏笔状态为已回收
python3 -c "
import json
data = json.load(open('伏笔表.json'))
for fs in data['foreshadowings']:
    if fs['id'] == 'FS-NNN':
        fs['status'] = 'recalled'
        fs['actualRecallChapter'] = 当前章节
        break
data['lastUpdated'] = '日期'
json.dump(data, open('伏笔表.json','w'), ensure_ascii=False, indent=2)
"
```


path: references/hook_design.md
content: # 钩子设计指南

番茄小说的核心竞争力是钩子。钩子让读者忍不住点下一章。

## 钩子类型

### 1. 悬念型（最常用）
留下一个未回答的问题，让读者必须看下去。
- "陆远盯着屏幕上的数字，瞳孔骤然收缩——那个数字，变了。"
- "电话那头沉默了三秒，然后赵恒说了一句话，让陆远彻底愣住。"

### 2. 反转型
颠覆读者刚刚建立的预期。
- "他以为一切结束了。直到他看到第二条消息。"
- "所有人都以为死者是意外。但法医报告第三页上的那个数字，证明了相反的事实。"

### 3. 信息差型
读者知道但角色不知道，产生焦虑感。
- 读者已经知道反派在暗处，但主角还在毫无防备地行动。
- "陆远笑了笑，没注意到身后那双一直盯着他的眼睛。"

### 4. 金句型
一句话戳中读者情绪，产生共鸣。
- "有些真相，不知道比知道幸福。但知道了，就再也回不去了。"
- "他终于明白，这个世界给每个人的倒计时，从来不是死亡——而是选择。"

### 5. 恐怖/惊悚型
在安全氛围中突然插入不安元素。
- "走廊尽头的灯又灭了。但这次，他听到了脚步声。"
- "微信消息弹出，备注名是'孙磊'。但孙磊，已经失踪三天了。"

### 6. 催泪型（催泪向专用）
情感冲击让读者产生分享欲。
- "弹幕区安静了很久。127号说：'谢谢你，终于有人记得我活着的时候。'然后他的座位亮了一下，熄灭了。"
- "她把信折好放进口袋，抬头笑了笑：'没事，我不难过。'眼泪却掉在了手背上。"

### 7. 爽感型
主角逆袭/打脸/获利的瞬间。
- "估值从2000万直接跳到1.5亿。投资人说了一句话：'我们投的不是项目，是你。'"
- "他挂掉电话，对面十几个人全沉默了。刚才还嘲笑他的那个副总，脸色最难看。"

## 钩子密度规则

| 位置 | 要求 |
|------|------|
| 章开头（前200字） | 必须有钩子，承接上一章悬念或抛出新悬念 |
| 章中段（每500字） | 至少一个小钩子（可以是悬念句、对话伏笔、异常细节） |
| 章末（最后100字） | 必须有强钩子，让读者必须点下一章 |

## 钩子设计方法

### "然后呢？" 测试
写完每段，问自己："读者看到这里会不会想'然后呢？'"如果不想，就缺钩子。

### "我要告诉朋友" 测试
这段话有没有让读者想截图发给朋友？如果没有，还不够有冲击力。

### 节奏控制
- 连续2-3个悬念后，需要一个舒缓段落（情感/幽默/日常），否则读者疲劳
- 大悬念（跨多章）和小悬念（章内解决）交替使用
- 每10章左右一个大爆点（反转/揭秘/高潮）

### 避免的钩子写法
- ❌ "接下来发生的事，改变了一切。"（空洞，没有具体信息）
- ❌ "他不知道的是，危险正在逼近。"（用烂了）
- ❌ 每章结尾都是"他愣住了"（重复）
- ✅ 用具体细节代替抽象描述


path: references/iteration_report.md
content: # novel-writer skill 迭代报告

> 2026-04-10 迭代工程师工作总结

## 1. 总迭代轮数：3轮

由于中断和恢复，实际完成了3个完整的迭代循环。每轮聚焦一个主题。

## 2. 每轮变更摘要

### 迭代1：视角检查+数值矛盾误报修复（01:00-04:00）

**视角检查误报修复：**
- 增加跳过聊天/消息行（【】格式、**加粗**、"XX："开头行）
- 增加 `is_in_quotes()` 函数检测引号内的"我"
- 增加重复引用检测（前5行内相同短语出现在对话/消息中则跳过）
- 正则排除"自"（"自我保护"）和"们"（"我们"）中的"我"
- 增加标题行跳过（#开头）

**数值矛盾误报修复：**
- entity提取改为2-3字+非中文字符前缀
- 增加月份排除（"4月"不是数值属性）
- 缩小nearby搜索范围从30→15字符
- 改进角色名提取：无character_names时用严格2-3字规则
- 兼容性判断增加纯数字比较（"42"和"42年"兼容）

**unnumbered文件过滤增强：**
- 大幅扩展排除列表
- 增加"必须含'章'字"过滤
- 效果：upload目录从244问题→5问题

### 迭代2：弹幕自称一致性检查（04:00-05:00）

**新增 `check_danmaku_self_reference()` 函数：**
- 自动识别弹幕角色
- 只匹配主语位置的自称词（句首/逗号后）
- 排除易误匹配的"臣""学生"
- 检测到真实问题：888号孟长卿混用"老夫"(6次)/"朕"(1次)

**增强 danmaku_management.md：**
- 新增"自称一致性"章节，包含铁律和具体角色自称对照表

### 迭代3：AI高频词增强（05:00）

- AI_WORDS增加"像是"（阈值3次/章）
- 检测到6处超标：aichanpin第5章(6次)、aileader第2/5/40/41/43章

## 3. 新增检测能力

| 检测项 | 函数 | 说明 |
|--------|------|------|
| 弹幕自称一致性 | `check_danmaku_self_reference` | 检测角色自称词不一致 |
| "像是"频率 | `check_ai_words`（增强） | AI高频词列表新增"像是" |

## 4. 效果对比

### aichanpin《我的死亡日期消失了》

| 版本 | 问题数 | 严重 | 中等 | 轻微 |
|------|--------|------|------|------|
| 迭代前 | 10 | 0 | 10 | 0 |
| 迭代后 | 6 | 0 | 4 | 2 |

误报消除：
- 数值矛盾：7个→0个（全部是误报）
- 视角混乱：2个→0个（全部是误报）

新增检测：
- AI高频词"像是"：1个（第5章6次超标）

### aileader《观众都是我的前世》

| 版本 | 问题数 | 严重 | 中等 | 轻微 |
|------|--------|------|------|------|
| 迭代前 | 244 | 183 | 19 | 42 |
| 迭代后 | 11 | 0 | 6 | 5 |

大幅改善主要来自unnumbered文件过滤（82个草稿文件不再纳入检查）。

新增检测：
- 弹幕自称不一致：1个（888号孟长卿，真实问题）
- AI高频词"像是"：5个

## 5. 已知未解决问题

### 脚本层面
1. **弹幕自称检查的精确度**：目前只匹配句首/逗号后的自称词，可能遗漏句子中间的自称
2. **数值矛盾在复杂对话中的解析**：如"孙磊的余额是空白的"后面很远才出现"38年"（15字符范围内已修复，但极端情况可能遗漏）
3. **角色消失检查在混合目录下不准确**：upload目录同时包含两部小说的文件

### 架构层面
4. **上下文窗口限制**：已有 `smart_context.md` 和 `context_loader.md` 的理论方案，但尚未实现为自动化脚本
5. **大纲偏差检测**：已有 `outline_drift.py` 脚本，但需要更精确的章节-大纲对应关系
6. **伏笔追踪**：已有 `foreshadowing.md` 和 `foreshadow_scan.py`，但需要结构化的伏笔表（JSON）

### 使用层面
7. **upload目录混合问题**：aileader的章节文件和aichanpin的章节文件在同一个目录，导致章节去重丢失数据
8. **两个小说项目需要独立的正文目录**

## 6. 对未来的建议

### 优先级P0（下次迭代必做）
1. 为两部小说创建独立正文目录，分离文件
2. 实现伏笔表的JSON结构，并在每章写作后自动更新
3. 增强数值矛盾检查——支持从追踪表提取角色名列表

### 优先级P1（近期改进）
4. 实现上下文智能路由脚本——根据章节大纲自动提取需要的设定
5. 增加章节间衔接断裂检测——检查新章开头是否回应上章悬念
6. 增加"去重复描写"检查——检测跨章节的相似外貌/场景描写

### 优先级P2（长期方向）
7. 实现情感曲线追踪——自动追踪每章的情感走向，检测情感单调
8. 读者体验模拟——基于节奏、钩子密度、信息量评估"弃书风险"
9. 写作风格一致性检测——跨章节的句式/用词/节奏分析
10. 角色弧线追踪模板——自动记录角色每章的成长/变化


path: references/outline_design.md
content: # 大纲设计方法论

## 一、从零开始设计大纲

### 第1步：确立核心卖点
- 一句话概括故事（不超过30字）
- 核心冲突是什么
- 为什么读者会追下去（悬念/爽感/情感）

### 第2步：确定类型和风格
- 类型：都市/奇幻/悬疑/言情/玄幻/科幻...
- 风格：轻松/沉重/热血/催泪/恐怖...
- 平台适配：番茄小说风格（快节奏、短句、高密度钩子）

### 第3步：设计核心设定
- 主角：身份、性格、核心困境、成长弧线
- 核心机制：系统/金手指/特殊能力/核心悬念
- 世界观：背景规则、限制条件
- 核心冲突：主角vs什么（人/制度/命运/自己）

### 第4步：分卷规划
- 每卷主题（这一卷解决什么问题）
- 每卷关键事件（3-5个转折点）
- 每卷结尾高潮（让读者想继续看下一卷）
- 卷与卷之间的升级逻辑（难度递增、格局扩大）

### 第5步：角色设定
- 主角档案（背景、动机、性格、说话风格、弱点）
- 核心配角（每人的功能定位、与主角的关系弧线）
- 反派/对手（动机要合理，不能脸谱化）
- 角色关系图

### 第6步：章节列表
- 每章标题 + 一句话概述
- 标注关键节点（转折、高潮、揭秘）
- 控制每章节奏（不能连续几章都是铺垫）

## 二、分卷设计模板

```markdown
## 第X卷：卷名

- **主题：** 本卷核心主题
- **起因：** 什么事件开启了本卷
- **经过：** 3-5个关键事件
- **高潮：** 本卷最大冲突/揭秘
- **结局：** 本卷如何收尾，留什么悬念给下一卷
- **角色变化：** 主角在本卷的成长/变化
- **章节数：** XX-XX章（约XX章）

### 章节列表
| 章节 | 标题 | 核心事件 | 关键度 |
|------|------|----------|--------|
| XX1 | 标题 | 一句话描述 | ★/★★/★★★ |
```

## 三、大纲调整原则

1. **大纲是活的** — 写作中发现更好的方向可以调整
2. **调整要双向同步** — 改了大纲要更新追踪表，发现正文好想法要补进大纲
3. **重大调整要记录** — 为什么改、改了什么、影响哪些后续章节
4. **不轻易推翻核心设定** — 核心冲突和主角弧线是地基，可以改装修不能改地基

## 四、节奏设计

### 单章节奏
```
钩子开头（抓注意力）
→ 铺垫/发展（推进情节）
→ 小高潮/反转（中段爆点）
→ 收束 + 悬念结尾（让读者点下一章）
```

### 单卷节奏
```
开篇（1-2章）— 建立本卷背景和悬念
发展（占60%）— 逐层推进，每3-5章一个小高潮
高潮（2-3章）— 本卷最大冲突
收尾（1章）— 解决+留下一卷钩子
```

### 全书节奏
- 前30章：必须让读者入坑（核心机制展示+第一个大高潮）
- 中段：持续升级，不能水（每卷都要有推进）
- 后期：收线，前面埋的伏笔逐个引爆


path: references/reader_experience.md
content: # 读者体验模拟指南

> 写完一章不是结束，而是验证的开始。本文档提供系统化方法，模拟5种典型读者的反应，在发布前识别弃书风险。

---

## 一、5种读者类型画像

### 1. 猎奇型读者

**核心驱动力**：新鲜设定、猎奇概念、"没见过这种"

| 维度 | 描述 |
|------|------|
| 弃书触发点 | 设定解释完就跑（设定党，只看创意不看执行）；同类套路重复出现；金手指/系统太常规无新意 |
| 满足点 | 开篇3章内展现独特世界观；每10章左右有新设定信息揭露；"设定层层剥洋葱"的快感 |
| 黄金节奏 | 前3章抛核心设定→5-10章扩展世界观→每卷开新设定层→不要一口气倒完 |
| 弹幕特征 | "这个设定有意思""第一次见这种""等等所以XX是YY？" |

### 2. 情感型读者

**核心驱动力**：共情、角色羁绊、"哭了""心疼"

| 维度 | 描述 |
|------|------|
| 弃书触发点 | 角色行为不可理喻（为剧情强行降智）；情感铺垫不足突然爆发；主要角色长期不互动；过度虐心无回报 |
| 满足点 | 用行为/细节传达情感而非形容词；角色关系有明确的进展里程碑；虐之后有甜、哭之后有暖 |
| 黄金节奏 | 每3-5章一个情感小高潮→每卷一个催泪名场面→虐甜比约3:7→催泪后接1-2章缓冲日常 |
| 弹幕特征 | "呜呜呜""心疼XX""这段看哭了""别说了我眼泪止不住" |

### 3. 推理型读者

**核心驱动力**：猜真相、找伏笔、"我早猜到了"/"反转绝了"

| 维度 | 描述 |
|------|------|
| 弃书触发点 | 伏笔回收敷衍（"就这？"）；推理线长期无进展（超过15章没有新线索）；答案靠天降/主角光环而非推理；设定矛盾被无视 |
| 满足点 | 线索公平递呈（读者可以跟着猜）；伏笔回收有"恍然大悟"感；多层反转（至少2层）；关键信息前面确实埋过 |
| 黄金节奏 | 每5章给1条明确线索→每卷中段设1个误导→卷末反转→跨卷大伏笔不超过50章回收周期 |
| 弹幕特征 | "我就说XX有问题！""这个伏笔早就埋了""等等回到第X章再看一遍""细思极恐" |

### 4. 爽文型读者

**核心驱动力**：爽感、打脸、逆袭、"太爽了"

| 维度 | 描述 |
|------|------|
| 弃书触发点 | 主角连续3章以上被动挨打；装逼被拆台无后续反击；日常拖沓超过2章无冲突；配角戏份过多抢主角风头 |
| 满足点 | 每3章一个小爽点（小胜/小打脸）；每10章一个中爽点（阶段性胜利）；每卷一个大爽点（终极打脸/实力碾压）；爽完立刻接新危机 |
| 黄金节奏 | 短章快节奏（2000-3000字/章）；对话推进剧情不水字；打脸-装逼循环间隔不超过5章；低谷期不超过2章 |
| 弹幕特征 | "爽！""笑死""主角牛逼""前方高能""打脸来得太快" |

### 5. 细节型读者

**核心驱动力**：沉浸感、世界真实感、"像真的一样"

| 维度 | 描述 |
|------|------|
| 弃书触发点 | 明显常识错误（时间/地理/逻辑）；设定前后矛盾；环境描写空洞（"一个房间"）；配角脸谱化 |
| 满足点 | 环境描写有五感细节；角色行为符合其性格逻辑；时间线可追溯；世界观自洽 |
| 黄金节奏 | 关键场景3-5句环境锚定→日常场景1-2句点睛→不堆砌描写但每次描写都有质感→每10章补充1次世界观细节 |
| 弹幕特征 | "这里有个bug""等等时间对不上""作者功课做得好""细节好评" |

---

## 二、章节级读者体验评估流程

写完每章后，按以下5步模拟：

### 步骤1：标记本章类型

| 章节类型 | 标记 |
|----------|------|
| 日常过渡 | `type:日常` |
| 紧张推进 | `type:紧张` |
| 催泪情感 | `type:催泪` |
| 悬念铺设 | `type:悬念` |
| 高潮反转 | `type:高潮` |
| 信息揭露 | `type:揭露` |

### 步骤2：对每种读者模拟打分（1-5分）

```
读者类型  | 吸引力 | 风险项
猎奇型   | ?/5   | ?
情感型   | ?/5   | ?
推理型   | ?/5   | ?
爽文型   | ?/5   | ?
细节型   | ?/5   | ?
```

### 步骤3：判断标准

- **任意读者 ≤ 2分**：该类型读者高流失风险，需要修改
- **所有读者 ≥ 3分**：及格线，可以发布
- **核心读者群 ≥ 4分**：良好（核心读者群见第四节小说分析）

### 步骤4：连续追踪

每5章画一次雷达图趋势：

```
章节1-5：  猎奇4 情感3 推理3 爽文4 细节3
章节6-10： 猎奇3 情感4 推理4 爽文3 细节4
...
```

**趋势预警**：任何读者类型连续3个5章窗口下降 → 需要针对性补强。

### 步骤5：修改优先级

1. 核心读者群弃书风险 → 必须修
2. 次要读者群弃书风险 → 尽量修
3. 所有读者低于3分 → 必须修
4. 单读者低于3分但非核心 → 可选修

---

## 三、弃书预警信号速查表

| 章节特征 | 高流失读者类型 | 预警等级 |
|----------|---------------|----------|
| 连续2章无新设定信息 | 猎奇型 | 🟡中 |
| 连续3章无新设定信息 | 猎奇型 | 🔴高 |
| 角色行为缺乏情感铺垫 | 情感型 | 🔴高 |
| 连续2章纯打斗/纯日常无情感点 | 情感型 | 🟡中 |
| 虐心超过3章无回报 | 情感型 | 🔴高 |
| 10章以上无新推理线索 | 推理型 | 🔴高 |
| 伏笔超过50章未回收 | 推理型 | 🟡中 |
| 线索揭露靠巧合而非推理 | 推理型 | 🔴高 |
| 主角连续3章被动 | 爽文型 | 🔴高 |
| 连续2章纯日常无冲突 | 爽文型 | 🟡中 |
| 打脸后无新危机（超过3章） | 爽文型 | 🟡中 |
| 时间线矛盾 | 细节型 | 🔴高 |
| 设定前后不一致 | 细节型 | 🔴高 |
| 环境描写缺失（关键场景） | 细节型 | 🟡中 |

### 高危组合（多种读者同时流失）

| 组合 | 触发条件 | 影响范围 |
|------|----------|----------|
| "注水日常" | 连续3章日常+无设定+无冲突+无线索 | 猎奇+爽文+推理 |
| "情感真空" | 角色不互动+纯剧情推进+无细节 | 情感+细节 |
| "逻辑崩塌" | 设定矛盾+主角降智+巧合推进 | 推理+细节+爽文 |
| "设定透支" | 设定倒完+无新料+无情感+无推理 | 全类型 |

---

## 四、两部小说的读者画像分析

### 《我的死亡日期消失了》—— 悬疑/都市

**核心设定**：男主能看见人头上的死亡倒计时数字，自己的死亡日期突然消失了。

#### 读者画像

| 维度 | 分析 |
|------|------|
| 核心读者群 | **推理型（40%）+ 猎奇型（30%）** |
| 次要读者群 | 爽文型（15%）+ 情感型（10%）+ 细节型（5%） |
| 获客钩子 | "看见死亡倒计时"——猎奇型秒入；"消失"——推理型被勾住 |
| 留存关键 | 悬疑线的层层推进、伏笔的精妙回收、反转的质量 |
| 风险点 | 悬疑解密后猎奇型流失（需持续提供新谜题）；推理线拖沓导致推理型弃书 |

#### 各读者类型满足策略

- **猎奇型**：死亡倒计时的规则持续扩展（谁能看见？数字能改变吗？消失意味着什么？），每5章左右揭露一个新规则
- **推理型**：核心悬疑线（消失原因）贯穿全书，每卷设子谜题，跨卷大线索串联。公平递呈线索，让读者能参与推理
- **情感型**：陆远看到亲人/朋友头上的数字时的心痛；配角死亡时的催泪。**策略**：用"知道死期却无力改变"的绝望感制造情感钩子
- **爽文型**：利用信息差（知道别人不知道的事）制造"上帝视角爽感"；解谜成功时的智力碾压感
- **细节型**：时间线必须严格（每个死亡日期都对应得上）；世界观规则必须自洽

#### 关键节奏参数

```
悬疑解密周期：每卷一个大谜题，卷末揭晓
伏笔密度：每章至少埋1条微伏笔，每5章1条中伏笔
猎奇型喂料周期：每5-7章一个新设定信息
情感型催泪点：每卷至少1个催泪名场面
```

---

### 《观众都是我的前世》—— 催泪/都市

**核心设定**：主角的每个前世都是观众，在"轮回影院"里看自己的人生。

#### 读者画像

| 维度 | 分析 |
|------|------|
| 核心读者群 | **情感型（45%）+ 猎奇型（25%）** |
| 次要读者群 | 推理型（15%）+ 细节型（10%）+ 爽文型（5%） |
| 获客钩子 | "前世都是观众"——猎奇型+情感型双重钩子；"看自己的人生"——共情爆点 |
| 留存关键 | 角色关系的深度刻画、催泪场景的密度和质量、前世故事的多样性 |
| 风险点 | 催泪疲劳（连续虐心导致麻木）；前世故事套路化；推理线偏弱导致推理型流失 |

#### 各读者类型满足策略

- **情感型**：每3-5章一个情感高光时刻；前世故事聚焦"遗憾"主题引发共鸣；用日常细节铺情感而非直说。参考风格："温暖日常中藏锋芒、对话活泼、弹幕幽默但关键时刻安静"
- **猎奇型**：前世身份的多样性（皇帝、乞丐、杀手……）；轮回影院的规则持续揭露；每次进入新前世都是新世界
- **推理型**：为什么所有前世都是观众？轮回影院的本质是什么？——这条元悬疑线留住推理型
- **细节型**：每个前世的世界观需要自洽；历史细节（如果涉及古代前世）需要准确
- **爽文型**：利用前世记忆/能力的跨世加成制造爽感（不是核心，但是锦上添花）

#### 关键节奏参数

```
催泪点周期：每3-5章一个小催泪，每卷一个大催泪
虐甜比：3:7（虐是为了甜更甜）
前世故事更新：每卷1-2个前世完整故事
猎奇型喂料：每10章左右揭露轮回影院新规则
推理线推进：每卷给1条关于轮回影院本质的线索
```

---

## 五、Agent 操作指令

### 写完后快速自检命令

```bash
# 检查最近一章是否有弃书预警信号
# 1. 统计日常连续章数
grep -c "type:日常" 最近5章的追踪记录

# 2. 检查伏笔状态（参考 foreshadowing.json 过期预警）
cat tracking/foreshadowing.json | python3 -m json.tool

# 3. 时间线一致性（细节型读者关注）
python3 scripts/consistency_check.py novels/当前小说/ --dict 设定词典.md
```

### 读者体验评估记录模板

在每章写完后，追加到 `tracking/reader_experience.md`：

```markdown
## 第X章 读者体验评估

| 读者类型 | 吸引力 | 风险项 | 备注 |
|----------|--------|--------|------|
| 猎奇型 | /5 | | |
| 情感型 | /5 | | |
| 推理型 | /5 | | |
| 爽文型 | /5 | | |
| 细节型 | /5 | | |

核心读者群评分：X/5
是否需要修改：是/否
修改计划：
```

---

## 六、快速参考卡片

**写每章时问自己4个问题：**

1. 猎奇型读者能看到新东西吗？（新设定/新信息）
2. 情感型读者能感到什么吗？（共情/心疼/温暖）
3. 推理型读者能猜什么吗？（新线索/新谜题）
4. 爽文型读者能爽到吗？（小胜/打脸/逆袭）

**每5章回顾时问自己2个问题：**

5. 细节型读者能挑出毛病吗？（时间线/设定矛盾）
6. 哪种读者类型在最近5章的得分在持续下降？

**4个问题中至少覆盖2个 = 及格。覆盖3个 = 良好。覆盖4个 = 优秀。**

---

## 七、弃书点检测

> 什么样的段落让读者想关掉页面？以下是7类高危弃书点及其检测方法。

### 7.1 弃书点分类与识别

| 弃书点类型 | 典型表现 | 严重度 | 检测方法 |
|-----------|---------|--------|----------|
| **设定倾倒** | 连续超过300字纯世界观解释，无角色动作穿插 | 🔴高 | 统计连续无对话/动作的描写段落长度 |
| **角色降智** | 角色做明显不符合智力/性格的决定，只为推进剧情 | 🔴高 | 对照 character_states.json 的 personality 字段检查行为合理性 |
| **情感跳崖** | 没有铺垫突然大哭/暴怒/表白，前一章还在正常对话 | 🔴高 | 检查情感词密度突变（相邻章节情感基调变化>2级） |
| **重复套路** | 相似情节结构在10章内重复出现≥3次 | 🟡中 | 用相似度检测比对章节结构（打脸→被嘲讽→再打脸的循环） |
| **信息真空** | 连续2章没有新信息（新设定/新线索/新关系进展） | 🟡中 | 标记每章新增信息量，连续低值触发预警 |
| **节奏塌陷** | 连续3章以上紧张度低于3（10分制） | 🟡中 | rhythm_check.py 的连续单调检测 |
| **视角漂移** | 突然切到不相关角色的内心独白超过1段 | 🟡中 | consistency_check.py 视角混乱检测 |

### 7.2 章节开头3秒检测

读者决定是否继续读下一章，通常只看开头3行。检测清单：

- [ ] 开头3行是否有新信息或悬念？（不是上章总结）
- [ ] 开头是否有动作或对话？（不是环境描写）
- [ ] 前100字是否出现人名？（不是代词"他"）
- [ ] 开头是否与上章结尾有明确的衔接或反差？

**不通过任意2项 = 高弃书风险开头，建议重写。**

### 7.3 章末钩子有效性检测

章节末尾是决定读者是否点"下一章"的关键：

- [ ] 最后200字是否有悬念/反转/情感冲击？
- [ ] 章末是否有未解的问题？（读者想知道答案）
- [ ] 章末是否有情感留白？（不是完全收束）
- [ ] 是否避免了"总结式结尾"（把本章事件复述一遍）？

---

## 八、阅读疲劳检测

> 读者不是机器，连续相同节奏会疲劳。以下是量化检测方法。

### 8.1 疲劳阈值参考表

| 内容类型 | 连续无转折最大安全字数 | 超过后表现 |
|---------|---------------------|----------|
| 打斗场景 | 1500字 | 招式重复感、读者跳读 |
| 日常对话 | 2000字 | 水字感、"什么时候进入正题" |
| 世界观解释 | 800字 | 设定党满足但其他人流失 |
| 情感描写 | 1200字 | "矫情""行了知道你难过" |
| 悬念铺垫 | 2500字 | 期待感耗尽、"到底说不说" |
| 单一情绪状态 | 3000字（约1章） | 情感麻木、催泪失效 |

### 8.2 转折点密度公式

```
转折密度 = 关键转折点数 / (总字数 / 1000)

健康范围：
- 快节奏小说（爽文/悬疑）：2.0-3.0（每千字2-3个转折）
- 慢节奏小说（文艺/催泪）：1.0-2.0（每千字1-2个转折）
- 低于1.0 = 阅读疲劳高风险
```

**关键转折点包括：** 新信息揭露、立场转变、关系变化、意外事件、情绪翻转、伏笔回收。

### 8.3 自动化检测（rhythm_check.py 集成）

rhythm_check.py 的连续单调检测已覆盖节奏塌陷，以下是补充的手动检测项：

1. **同义词堆砌检测**：同一情绪词（如"愤怒"）在500字内出现≥3次 → 疲劳信号
2. **句式重复检测**：连续5句以上相同句式结构（"他...了。他...了。他...了。"）→ 疲劳信号
3. **对话堆砌检测**：连续8行以上纯对话无动作/心理穿插 → 跳读信号
4. **描写堆砌检测**：连续200字以上纯环境/外貌描写无剧情推进 → 跳读信号

---

## 九、情感共鸣点评估

> 好的小说不是让读者"知道"角色难过，而是让读者"跟着"难过。

### 9.1 共鸣强度评估矩阵

| 共鸣等级 | 读者反应 | 文本特征 | 示例 |
|---------|---------|---------|------|
| 5-爆哭 | 放下手机平复情绪 | 核心情感+细节铺垫+出其不意+角色成长 | 前世终于释怀、生死离别 |
| 4-泪目 | 鼻酸但能继续看 | 情感积累到临界+一个触发细节 | 发现一直被默默保护 |
| 3-触动 | 停下来回味一下 | 共情角色处境+真实细节 | 角色说出读者想说的话 |
| 2-理解 | 知道应该感动但没感觉 | 情感铺垫不足/套路化 | "他悲痛欲绝"（空喊口号） |
| 1-无感 | 跳过不看的冲动 | 强行煽情/煽情时机不对 | 没铺垫突然角色牺牲 |

### 9.2 共鸣三要素检查

每个情感场景检查是否满足以下三要素（缺一个降一级）：

1. **铺垫充分**：情感爆发前至少有2章的细节暗示（小动作、小对话、小变化）
2. **细节真实**：用具体的、感官的、独特的细节传达情感，而非形容词堆砌
3. **情感意外**：不是读者能预判的"这里该哭了"，而是出其不意的触发点

### 9.3 共鸣点分布检查

```
每卷检查：
- Level 5（爆哭）：1-2个
- Level 4（泪目）：2-3个  
- Level 3（触动）：3-5个
- Level 2 以下：0个（有的话需要重写）

警告信号：
- 连续10章无 Level 3+ 共鸣点 → 情感型读者流失风险
- 单章超过2个 Level 4+ 共鸣点 → 情感过载，催泪疲劳
```

### 9.4 反共鸣检测（必须避免）

| 反共鸣模式 | 表现 | 修复方法 |
|-----------|------|----------|
| **强行煽情** | 悲惨事件堆砌但无铺垫 | 删掉事件，先花2章建立角色关系 |
| **情绪通货膨胀** | 每章都在哭/每章都在爆发 | 降低日常章节的情感强度，留空间 |
| **说教代替共情** | 角色长篇大论讲道理 | 用行为和选择传达，不用台词 |
| **全知视角泄露** | 提前揭示角色不知道的信息 | 严格守视角，只写角色能感知的 |

---

## 十、读者体验评估速查表（新增检测项）

| 检测项 | 触发条件 | 影响 | 负责检测 |
|-------|---------|------|----------|
| 弃书点 | 7类高危段落出现 | 读者直接关闭 | 手动 + consistency_check.py |
| 开头3秒 | 开头3行不通过≥2项 | 不点下一章 | 手动 |
| 章末钩子 | 最后200字无悬念 | 不点下一章 | 手动 + hook_design.md |
| 阅读疲劳 | 连续无转折超阈值 | 跳读/弃书 | rhythm_check.py + 手动 |
| 转折密度 | 低于1.0/千字 | 整体节奏拖沓 | rhythm_check.py |
| 情感共鸣 | 10章无Level 3+ | 情感型流失 | 手动（本指南§九） |
| 反共鸣 | 4类反共鸣出现 | 出戏/反感 | 手动（本指南§九.4） |


path: references/scene_transition.md
content: # 场景转换手册

从《我的死亡日期消失了》（悬疑类）和《轮回影院》（催泪/奇幻类）正文中提取的实际场景转换手法。供写作和审校时参考。

---

## 一、场景转换分类

### 1. 时间跳

**手法A：时间锚点句**

用一句精确的时间陈述直接跳转。

> 原文范例（悬疑）：
> 「第二天早上七点半，陆远到了公司。不是因为他勤快——是因为昨晚没怎么睡好。」（第3章）

> 原文范例（催泪）：
> 「周六中午，林昭坐在一家日料店里，对面是何薇。」（第5章）

- **适用场景**：日常推进、工作日切换、约见面
- **使用频率**：高频，几乎每章至少一次
- **要点**：时间锚点后紧跟一句内心独白或补充说明，避免干巴巴的跳转

**手法B：睡前→醒来**

通过"闭眼/睡觉→睁眼"完成时间跳跃。

> 原文范例（催泪）：
> 「林昭醒来的时候，枕头是湿的。不是哭的，是口水。他迷迷糊糊地坐起来，看了一眼手机——早上七点十二分。」（第3章）

> 原文范例（催泪）：
> 「林昭猛地睁开眼。天花板还是那个发霉的天花板，手机屏幕亮着，显示凌晨两点零三分。」（第1章末→第2章）

- **适用场景**：夜晚→次日、影院→现实的往返
- **使用频率**：中频，每3-5章出现一次
- **要点**：醒来后的第一个感官细节很重要（天花板、手机屏幕、声音），制造"回到现实"的落地感

**手法C：模糊时间流逝**

不写具体时间，用状态描述暗示时间过去。

> 原文范例（悬疑）：
> 「回到上海以后，陆远没有回公司。他在家里待了一天。」（第15章）

- **适用场景**：大段时间跳跃、调查空白期
- **使用频率**：低频，仅在需要跳过较长周期时使用
- **要点**：后文必须补一句解释"这段时间做了什么"，否则读者会觉得断裂

---

### 2. 空间跳

**手法A：交通工具过渡**

通过乘车/乘电梯/坐高铁的过程完成空间转换。

> 原文范例（悬疑）：
> 「出发那天是周四。早上七点的高铁，从虹桥到江城，两个多小时。……高铁到了江城站。江城比上海热。一下车就是一股湿热的风扑面而来。」（第5章）

> 原文范例（悬疑）：
> 「陆远在电梯里数过，从B2到45楼，一共需要四分十二秒。足够他把同乘的三个人的余额全部扫一遍。」（第1章）

- **适用场景**：城市间移动、楼层转换
- **使用频率**：中频
- **要点**：交通工具上的细节（余额扫描、周雅的表情）让过渡不空洞；到达后的第一个感官冲击（热风、气味）标记新空间

**手法B：感官切换**

用一个感官描写结束旧场景，另一个感官开启新场景。

> 原文范例（悬疑）：
> 「回到酒店，陆远没有立刻上楼。他坐在大堂的沙发上，打开了今天记的笔记。」（第10章）

- **适用场景**：同一天内不同地点的切换
- **使用频率**：高频
- **要点**：一个具体的动作（坐下、打开笔记）锚定新空间

**手法C：建筑/空间描写直接切入**

对新场景的建筑或环境做1-2句描写，自然过渡。

> 原文范例（悬疑）：
> 「大堂很干净——太干净了。大理石地面光可鉴人，前台后面的logo墙一尘不染。空气里有淡淡的消毒水味。」（第5章）

> 原文范例（催泪）：
> 「红色的天鹅绒座椅一排排向两侧延伸，看不到尽头。穹顶高得离谱，像一座教堂的内部。」（第1章）

- **适用场景**：首次进入一个重要空间
- **使用频率**：低频，仅在关键空间首次出场时用
- **要点**：环境描写必须暗含情绪暗示（"太干净了"→不安；"看不到尽头"→压迫感）

---

### 3. 视角跳

**手法A：现实↔影院双线切换**

通过"闭眼→意识下沉→影院"和"意识上升→睁眼→现实"完成视角转换。

> 原文范例（催泪）：
> 「晚上十一点，林昭躺在床上，闭上了眼。意识下沉。再睁开时，他已经站在轮回影院的过道里了。」（第4章）

> 「林昭感觉到意识在上升，轮回影院正在远去。但在完全消失之前，他看到了最后排最角落的画面——」（第4章末）

- **适用场景**：双线叙事的视角切换（核心机制）
- **使用频率**：高频率（催泪类每章至少一次往返）
- **要点**：去程要快（一两句），回程可以留一个"尾巴"（最后看到的画面/感觉），为下一章埋钩子

**手法B：群像视角快速切换**

在同一个场景内，通过角色反应快速切换观察视角。

> 原文范例（催泪）：
> 老六毕业时，逐个切到赵承乾、沈墨白、孟长卿、金万三、李归墟、冷月的反应，每人1-2句。（第5章）

- **适用场景**：群像戏、重要时刻的集体反应
- **使用频率**：中频
- **要点**：每个角色的反应必须符合其人设（赵承乾→沉重点头；李归墟→"算了"；冷月→一个字"嗯"），不能千人一面

---

### 4. 情绪跳

**手法A：日常→不安（悬疑专用）**

用一个微小的异常信号打破日常氛围。

> 原文范例（悬疑）：
> 「走廊尽头，赵恒的办公室灯亮着。赵恒请假了。昨天下午发消息说家里有点事，今天不来。灯却亮着。」（第1章）

> 原文范例（悬疑）：
> 「老钱点了点头，然后像是随口说了一句，'永盛的创始人是哪年死的来着？'」（第3章）

- **适用场景**：悬疑类在正常节奏中植入不安
- **使用频率**：高频（悬疑类每章1-2次）
- **要点**：异常信号必须微小（一盏灯、一句"随口说"），主角的反应也要克制（"半秒""没有多想"），不能过度反应

**手法B：轻松→沉重（催泪专用）**

用角色的一个回忆或一句话，从喜剧氛围跌入悲伤。

> 原文范例（催泪）：
> 老六从嘻嘻哈哈讲赌钱，突然转到「那座城是我赢来的……一夜输回去」，再转到「我站在城门口，发现换主人了」。整个情绪曲线：笑→沉默→心酸。（第5章）

> 原文范例（催泪）：
> 「他低下头，看着自己那双布满老茧的手。'但今天，我想放手了。'」（第5章）

- **适用场景**：角色毕业/退场、催泪高潮
- **使用频率**：低频（仅在关键情感节点）
- **要点**：转换前必须有足够的轻松铺垫，落差越大效果越强。不要在转换处加解释性文字，让读者自己感受落差

**手法C：内心独白断句**

用一个极短的句子打断正在进行的叙事，制造情绪断裂。

> 原文范例（悬疑）：
> 「但余额停了。」（第1章）

> 原文范例（催泪）：
> 「然后——他消失了。」（第5章）

- **适用场景**：重大发现、角色消失/死亡、反转时刻
- **使用频率**：极低频（全书不超过5次）
- **要点**：单独成段。前后各空一行。越短越有冲击力。

---

## 二、转换质量标准

### 好转换 vs 差转换

| 维度 | 好转换 | 差转换 |
|------|--------|--------|
| **时间跳** | 有时间锚点+补充说明（"不是因为他勤快"） | 干巴巴写"第二天"就完了 |
| **空间跳** | 到达后有感官落地（热风、气味、声音） | 直接写"他到了XX地方" |
| **视角跳** | 双线转换用固定句式（"闭眼→意识下沉"），读者形成节奏感 | 每次转换方式不同，读者分不清在哪条线 |
| **情绪跳** | 轻重落差大，不加解释，让读者自己感受 | 作者跳出来写"他感到非常悲伤" |
| **整体节奏** | 快慢交替：日常慢→发现快→紧张更快→突然停 | 从头到尾一个速度 |

### 差转换的典型问题

1. **生硬跳转**：「第二天，陆远去了公司。」——没有内心独白补充，像流水账
2. **过度解释**：「他感到一阵复杂的情绪涌上心头，那是对过去的怀念和对未来的恐惧交织在一起的感觉」——直接告诉读者情绪，不如用一个动作或回忆暗示
3. **缺少感官落地**：到新场景后没有视觉/听觉/嗅觉描写，读者没有"到达感"
4. **转换过于频繁**：一段300字内跳3次场景，读者跟不上
5. **转换过于花哨**：每次都用不同的转换方式，没有稳定的节奏感

---

## 三、场景转换检查清单

写完一章后，用以下清单自查：

- [ ] **每章场景转换不超过3次**（不含双线往返的固定切换）
- [ ] **每次时间跳都有锚点**（具体时间词+补充说明/内心独白）
- [ ] **每次空间跳都有感官落地**（到达后的视觉/听觉/嗅觉至少一项）
- [ ] **双线切换用固定句式**（如果适用），不随意变换转换方式
- [ ] **情绪跳跃前后有足够铺垫**，不在无准备的情况下突然沉重
- [ ] **重大转折单独成段、前后留白**（"但余额停了。"式写法全年书≤5次）
- [ ] **没有"他感到XX"式直接情绪描述**，改用动作/回忆/环境暗示
- [ ] **整章的节奏有快慢变化**，不全是一个速度
- [ ] **群像场景中每个角色反应符合人设**，不千人一面
- [ ] **章末转换留有"尾巴"**（最后一个画面/感觉/悬念），引导读者翻下一章

---

## 四、不同类型小说的转换偏好

| 类型 | 时间跳偏好 | 空间跳偏好 | 视角跳偏好 | 情绪跳偏好 |
|------|-----------|-----------|-----------|-----------|
| **悬疑类** | 精确锚点（"第二天早上七点半"） | 交通工具+建筑描写 | 单视角为主，不切 | 日常→不安，克制，微小异常 |
| **催泪/奇幻类** | 睡前→醒来 | 感官切换为主 | 双线高频往返 | 轻松→沉重，大落差 |

---

*本手册基于《我的死亡日期消失了》15章、《轮回影院》7章正文提取。写作时应根据小说类型和具体场景灵活选用，不要机械套用。*


path: references/smart_context.md
content: # 上下文智能路由机制

> 在 `context_loader.md` 的分层加载基础上，增加**标签索引 + 上下文路由器**，让 agent 在写作每章时精准定位需要加载的设定条目，而不是靠 grep 模糊搜索。

## 一、问题定义

### 1.1 实际信息量（两部小说实测）

| 小说 | 设定集 | 设定词典 | 大纲 | 追踪表 | 合计 |
|------|--------|----------|------|--------|------|
| 我的死亡日期消失了 | ~17KB | ~5KB | ~20KB | ~8KB | ~50KB |
| 观众都是我的前世 | ~15KB | ~3KB | ~67KB | ~10KB | ~95KB |

即使按 context_loader.md 的压缩策略降到 12KB，仍有优化空间：很多章节不需要加载全部 L0 内容，而 grep 搜索容易遗漏。

### 1.2 核心矛盾

- **设定条目之间有隐性关联**：提到"永盛"就会涉及"孙磊""陈永生""八楼""三月周期"，但 grep "永盛" 不会自动带出这些关联
- **不同章节需要不同的设定组合**：第 14 章需要出差记录相关设定，第 35 章需要异常员工相关设定
- **角色在不同卷的设定侧重不同**：赵恒在第 1 卷只是搭档，第 10 卷揭示系统身份

## 二、设定标签索引

### 2.1 索引文件规范

每部小说维护一个 `设定索引.json`，放在项目根目录。结构：

```json
{
  "meta": {
    "novel": "我的死亡日期消失了",
    "last_updated": "2026-04-10",
    "total_entries": 42
  },
  "entries": [
    {
      "id": "E001",
      "name": "永盛集团",
      "type": "location",
      "tags": ["永盛", "公司", "江城", "孙磊", "陈永生", "十二楼", "八楼", "三月"],
      "file": "设定集.md",
      "section": "### 永盛集团",
      "line_start": 85,
      "line_end": 110,
      "volume_range": [1, 20],
      "priority": "always"
    },
    {
      "id": "C001",
      "name": "陆远",
      "type": "character",
      "tags": ["陆远", "主角", "余额", "观察者", "投行", "迈凯轮"],
      "file": "设定集.md",
      "section": "### 陆远",
      "line_start": 20,
      "line_end": 60,
      "volume_range": [1, 20],
      "priority": "always"
    },
    {
      "id": "S001",
      "name": "寿命交易系统",
      "type": "worldbuilding",
      "tags": ["系统", "余额", "交易", "归零", "暂停", "清算", "观察者", "陆承平"],
      "file": "设定集.md",
      "section": "### 寿命余额系统",
      "line_start": 200,
      "line_end": 260,
      "volume_range": [1, 20],
      "priority": "on-demand"
    },
    {
      "id": "P001",
      "name": "三月周期规律",
      "type": "plot_mechanism",
      "tags": ["三月", "周期", "入职", "陈永生", "忌日", "孙磊", "张维", "周志远"],
      "file": "写作追踪表.md",
      "section": "三个异常员工共同特征",
      "line_start": 180,
      "line_end": 190,
      "volume_range": [1, 5],
      "priority": "on-demand"
    }
  ],
  "tag_groups": {
    "永盛线": ["永盛", "孙磊", "张维", "周志远", "八楼", "十二楼", "三月", "陈永生"],
    "系统线": ["系统", "余额", "交易", "归零", "观察者", "陆承平", "赵恒"],
    "情感线": ["沈清", "告白", "决裂", "信任", "老钱", "张敏"],
    "职场线": ["投行", "尽调", "合伙人", "周雅", "王建国"]
  }
}
```

### 2.2 条目类型（type）

| type | 说明 | 压缩策略 |
|------|------|----------|
| `character` | 角色档案 | 按出场压缩为摘要 |
| `location` | 场景设定 | 按涉及场景加载描写 |
| `worldbuilding` | 世界观规则 | 按关键词触发加载 |
| `plot_mechanism` | 剧情机制/规律 | 按剧情阶段触发 |
| `relationship` | 角色关系 | 按出场角色组合加载 |
| `item` | 道具/物品 | 按提及加载 |
| `timeline` | 时间线事件 | 按章节范围加载 |

### 2.3 优先级（priority）

| priority | 含义 | 写入上下文包规则 |
|----------|------|------------------|
| `always` | 每章必带 | 全量或压缩后加载 |
| `high` | 经常需要 | 写作卡匹配到标签时加载 |
| `on-demand` | 特定场景才需要 | 写作卡精确匹配时加载 |
| `rare` | 极少需要 | 仅全量检查时加载 |

### 2.4 关联标签组（tag_groups）

`tag_groups` 定义标签之间的关联关系。当写作卡命中某标签时，同组的其他标签也会被激活。

**路由规则**：命中 tag_groups 中任一标签 → 该组所有标签对应的条目进入候选加载列表。

## 三、上下文路由器

### 3.1 路由流程

```
写作卡 → 关键词提取 → 标签匹配 → tag_groups 扩展 → 优先级过滤 → 去重 → 生成加载清单 → 按行号读取
```

### 3.2 关键词提取规则

从写作卡中提取以下维度并映射到标签：

| 提取维度 | 正则模式示例 | 映射到 tags |
|----------|-------------|-------------|
| 角色名 | `陆远\|沈清\|赵恒\|老钱\|孙磊\|方哲` | 直接匹配 |
| 场地名 | `永盛\|投行\|金库\|别墅\|面馆` | 直接匹配 |
| 机制词 | `余额\|交易\|归零\|暂停\|观察\|清算` | 直接匹配 |
| 情节词 | `调查\|发现\|见面\|决裂\|告白\|死亡` | 映射到 tag_groups |
| 物品词 | `迈凯轮\|签名\|死亡证明\|笔记本` | 直接匹配 |

### 3.3 路由算法（伪代码）

```python
def route_context(writing_card, index):
    # 1. 提取关键词
    keywords = extract_keywords(writing_card)
    
    # 2. 直接标签匹配
    matched_entries = set()
    for entry in index.entries:
        if any(kw in entry.tags for kw in keywords):
            matched_entries.add(entry.id)
    
    # 3. tag_groups 扩展
    expanded_tags = set()
    for group_name, group_tags in index.tag_groups.items():
        if any(kw in group_tags for kw in keywords):
            expanded_tags.update(group_tags)
    
    for entry in index.entries:
        if any(tag in entry.tags for tag in expanded_tags):
            matched_entries.add(entry.id)
    
    # 4. 加入 always 优先级条目
    for entry in index.entries:
        if entry.priority == "always":
            matched_entries.add(entry.id)
    
    # 5. 按 priority 排序 + 容量控制
    entries = sorted(
        [e for e in index.entries if e.id in matched_entries],
        key=lambda e: {"always": 0, "high": 1, "on-demand": 2, "rare": 3}[e.priority]
    )
    
    # 6. 容量限制（总加载量 ≤ 15KB）
    total_size = 0
    load_list = []
    for entry in entries:
        size = (entry.line_end - entry.line_start) * 30  # 估算：每行约30字节
        if total_size + size <= 15360:
            load_list.append(entry)
            total_size += size
        elif entry.priority == "always":
            # always 条目超限也要压缩加载
            load_list.append(entry)
    
    return load_list
```

### 3.4 路由结果示例

**输入**：第 35 章写作卡「分析三个异常员工共同特征→全部三月入职→3月15日关联→永盛是系统性问题」

**路由结果**：

| 条目ID | 名称 | 优先级 | 加载方式 |
|--------|------|--------|----------|
| C001 | 陆远 | always | 摘要 |
| C003 | 赵恒 | always | 摘要 |
| E001 | 永盛集团 | always | 场景描写 |
| P001 | 三月周期规律 | on-demand→命中 | 全量 |
| C010 | 孙磊 | high→命中 | 摘要 |
| C011 | 张维 | high→命中 | 摘要 |
| C012 | 周志远 | high→命中 | 摘要 |
| S001 | 寿命交易系统 | on-demand→命中 | 相关规则 |

**不加载**：方哲档案（第 4 卷才出场）、沈清档案（本章不出场）、地下金库场景（本章不去）。

## 四、索引维护规范

### 4.1 初始化

新小说项目创建时，agent 执行：

1. 读取设定集、设定词典、大纲
2. 为每个角色、场景、世界观条目创建索引条目
3. 基于大纲为每个条目分配 volume_range
4. 根据出场频率分配 priority
5. 构建 tag_groups

### 4.2 增量更新

每章写完后检查：

- **新增角色** → 添加 character 条目
- **新增场景** → 添加 location 条目
- **新揭示设定** → 添加 worldbuilding 条目 + 更新相关 tags
- **角色状态变化** → 更新 priority（如从 on-demand 升为 high）
- **新剧情线开启** → 添加 plot_mechanism 条目 + 新 tag_group

### 4.3 维护命令

```bash
# 查看索引统计
cat 设定索引.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'条目数: {len(d[\"entries\"])}')"

# 搜索某标签关联的所有条目
cat 设定索引.json | python3 -c "
import json,sys
d=json.load(sys.stdin)
tag=sys.argv[1]
for e in d['entries']:
    if tag in e['tags']:
        print(f'{e[\"id\"]} {e[\"name\"]} ({e[\"priority\"]})')
" '永盛'
```

## 五、与 context_loader.md 的关系

| 方面 | context_loader.md | 本文档（smart_context.md） |
|------|-------------------|---------------------------|
| 分层策略 | L0-L3 四层加载 | 继承分层，增加标签索引 |
| 角色加载 | grep 角色名 → read | 标签匹配 → 精确行号读取 |
| 大纲加载 | 按卷 grep | 同（大纲不适合标签化，按卷读取即可） |
| 关联发现 | 无 | tag_groups 自动扩展关联条目 |
| 容量控制 | 估算 12KB | 精确到条目级别的容量控制 |
| 维护成本 | 低（纯策略文档） | 中（需要维护设定索引文件） |

**建议**：简单项目（< 200 章）用 context_loader.md 的 grep 策略即可；复杂项目（200+ 章、多线并进）启用本文档的标签索引路由。

## 六、实战操作步骤

写作第 N 章时的完整操作序列：

```
1. 读取写作卡（从大纲提取本章要点）
2. 如果存在 设定索引.json → 执行路由算法
   如果不存在 → 回退到 context_loader.md 的 grep 策略
3. 加载 L0 必带项（硬性规则 + 设定词典 + 近5章摘要 + 上章尾部）
4. 根据路由结果加载 L1-L3 条目（按行号精确读取）
5. 组装上下文包
6. 开始写作
```

**不需要每次都运行路由算法**：如果连续几章出场角色和场景相似（常见），可以复用上一章的路由结果，只做增量调整。


path: references/style_consistency.md
content: # 文风一致性指南

> 从多卷衔接实践中提炼，适用于长篇网络小说（14卷+、500章+级别）。

---



### 1. 文风维度定义

#### 四维模型

| 维度 | 定义 | 检测指标 |
|------|------|---------|
| **句式长度** | 平均每句字数 | 长句(>30字)/中句(15-30)/短句(<15) 的比例 |
| **用词风格** | 口语化 vs 文学化程度 | 书面语词汇占比、成语密度、专业术语频率 |
| **描写密度** | 环境描写 vs 叙事推进的比重 | 每1000字中纯描写段落数量 |
| **对话比例** | 对话文本占总文本的比例 | 对话行数 / 总行数 |

#### 基准值（网络小说标准）

| 维度 | 建议范围 | 说明 |
|------|---------|------|
| 句式长度 | 短句40-50%、中句30-40%、长句10-20% | 网文以短句为主，节奏明快 |
| 用词风格 | 口语化70%+ | 网文读者不吃纯文学那套 |
| 描写密度 | 每1000字1-2段环境描写 | 点缀而非堆砌 |
| 对话比例 | 35-50% | 太少不生动，太多像剧本 |

#### 按类型的基准偏移
- **悬疑类**：短句比例可提高到55%+，对话偏少（30-40%），描写密度1段/千字
- **催泪/都市类**：中长句可增多，对话偏多（40-55%），描写密度1.5段/千字
- **战斗/动作场景**：短句可达70%，对话降到20-25%
- **情感/回忆场景**：中长句可达50%，描写密度3段/千字

### 2. 文风检测方法

#### 自动统计脚本指标

每章完成后，应统计以下基础数据（可脚本化）：

```python
# 核心检测指标
metrics = {
    "字数": total_chars,
    "段落数": total_paragraphs,
    "对话段落数": dialogue_paragraphs,
    "对话占比": dialogue_pct,           # 目标: 35-50%
    "平均句长": avg_sentence_length,     # 目标: 18-25字
    "短句占比(<15字)": short_sentence_pct, # 目标: 40-55%
    "环境描写段数": description_paragraphs,
    "描写密度": desc_per_1k,            # 目标: 1-2段/千字
    "问号数量": question_marks,          # 过多=角色总在提问
    "感叹号数量": exclamation_marks,     # 过多=情绪过激
    "省略号数量": ellipsis_count,        # 过多=犹豫不决风格
}
```

#### 偏移阈值

| 指标 | 黄灯（轻微偏移） | 红灯（需要修正） |
|------|-----------------|-----------------|
| 对话占比 | <30% 或 >55% | <20% 或 >65% |
| 平均句长 | <15字 或 >30字 | <12字 或 >40字 |
| 短句占比 | <35% 或 >60% | <25% 或 >75% |
| 描写密度 | <0.5 或 >3段/千字 | <0.2 或 >5段/千字 |
| 感叹号/千字 | >8个 | >15个 |

#### 检测频率
- 每5章做一次统计检查
- 关键章节（卷首、卷末、高潮章）逐章检查
- 发现红灯立即修正，黄灯在下章自然调整

### 3. 文风偏移的修正策略

#### 常见偏移类型及修正

| 偏移类型 | 表现 | 修正方法 |
|---------|------|---------|
| **文学化偏移** | 句子变长、描写变多、用词变雅 | 砍掉形容词、拆长句为短句、加对话打断描写 |
| **口语化过度** | 全是短句、没有描写、像聊天记录 | 每3-5段对话插入1段动作/环境描写 |
| **对话泛滥** | 连续十几行纯对话 | 用动作描写打断（"他端起杯子""她皱了皱眉"） |
| **描写泛滥** | 连续几段纯环境描写 | 用角色反应/内心想法打断，或者直接砍半 |
| **节奏断裂** | 突然从紧张跳到平淡 | 检查段落过渡，加入情绪过渡句 |
| **AI味加重** | 用词过于工整、排比过多、说教感 | 参照标杆段落，刻意制造不工整（口语打断、句式变化） |

#### 修正优先级
1. **不改动剧情**：风格修正不能改变情节走向
2. **不改动人物性格**：对话风格是角色的一部分
3. **最小修改原则**：调句式 > 调段落 > 重写段落
4. **保持节奏**：修正后重读，确保阅读节奏自然

### 4. 风格标杆段落机制

#### 机制说明
每部小说保存3-5段"标杆段落"，代表该小说的理想文风。写新章节时对照标杆，发现偏移及时调整。

#### 标杆选择标准
- **代表性**：能代表该小说的核心风格（不是最好的段落，而是最"对味"的段落）
- **多样性**：覆盖不同场景类型（日常、紧张、情感、对话、描写）
- **长度**：每段200-400字

#### 标杆存储格式

```
# 风格标杆段落

## 小说：《XXX》

### 标杆1：日常场景（来源：第X章）
[段落内容]

### 标杆2：紧张场景（来源：第X章）
[段落内容]

### 标杆3：情感场景（来源：第X章）
[段落内容]

### 标杆4：对话场景（来源：第X章）
[段落内容]

### 标杆5：描写场景（来源：第X章）
[段落内容]
```

#### 使用方法
1. **写作前**：重读标杆段落，进入正确风格频道
2. **写作中**：每写1000字，对照标杆检查句式和用词
3. **审校时**：发现偏移，用标杆段落作为"校准参照"重写

#### 标杆更新
- 每50章回顾一次标杆段落
- 如果小说风格有意转变（新卷基调变化），可以更新标杆
- 保留旧标杆作为历史参考

#### 实际标杆示例（需在实际写作中填充）

**悬疑类标杆（参考《我的死亡日期消失了》风格）：**
> 短句为主、冷静克制、恐怖感通过细节传递、不直说"害怕"但让读者自己害怕

**催泪/都市类标杆（参考《观众都是我的前世》风格）：**
> 温暖日常中藏锋芒、对话活泼、弹幕幽默但关键时刻安静、情感用行为而非形容词表达

---

## Agent 使用说明

### 初始化
每部新小说开写前：
1. 在小说工作目录创建 `style_benchmark.md`（风格标杆段落）
2. 确定该小说的四维基准值（参考上方"按类型的基准偏移"）
3. 记录在 `style_benchmark.md` 头部

### 写作时
- 跨卷章节：参照"卷首开篇模板"和"卷末收束模板"
- 新角色引入：参照"3章法则"
- 时间跳跃：参照对应跨度的处理方式

### 审校时
- 每5章运行文风统计检测
- 黄灯提醒，红灯必须修正
- 参照标杆段落校准

### Shell 命令参考

```bash
# 统计章节基础数据（需配合 consistency_check.py 或自定义脚本）
python3 scripts/consistency_check.py <正文目录> --dict 设定词典.md

# 查看风格标杆
cat <小说目录>/style_benchmark.md

# 更新标杆
# 手动编辑 style_benchmark.md，选择最具代表性的段落
```


path: references/usage_examples.md
content: # Skill 使用示例文档

本文档给出 novel-writer skill 的4个典型使用场景，包含具体命令和预期输出示例。

---

## 场景1：开始写新章节前，检查一致性

**何时使用：** 每章写完后或写下一章前，确保正文没有设定矛盾、时间线错误、AI高频词等问题。

**命令：**

```bash
python3 scripts/consistency_check.py 正文/ --tracking 追踪表.md --dict 设定词典.md --character-states character_states.json
```

**预期输出示例：**

```
正在扫描正文目录: 正文/
加载追踪表: 追踪表.md
加载设定词典: 设定词典.md
加载角色状态: character_states.json

扫描完成: 38 个文件

===== 一致性检查报告 =====

[A类-设定一致性] ✅ 通过
[B类-时间线] ⚠️ 发现 1 个问题
  - 第025章: "三天后" 与 追踪表记录的时间线偏差1天（预期: 第14天, 实际: 第15天）

[C类-逻辑一致性] ✅ 通过
[D类-风格质量] ⚠️ 发现 3 个问题
  - 第023章: AI高频词 "不禁" 出现 2 次
  - 第024章: AI高频词 "微微" 出现 3 次
  - 第025章: 对话超过150字无打断/动作，建议拆分

[E类-大纲合规] ✅ 通过
[角色状态机] ✅ 通过
[角色消失] ⚠️ "林小美" 已连续 6 章未出场
[跨章重复] ✅ 未发现重复段落
[章节字数] ✅ 所有章节在 2000-3000 字范围内

===== 总结 =====
通过: 8/11  |  警告: 3  |  错误: 0
建议修正上述 3 个警告后再继续写作。
```

**解读：** 检查通过率较高，需修正时间线偏差、替换AI高频词、拆分过长对话。修正后可安心写下一章。

---

## 场景2：写完章节后，用 rhythm_check 检查节奏

**何时使用：** 批量审校阶段，或感觉最近几章"读起来太平""节奏不对"时。

**命令：**

```bash
python3 scripts/rhythm_check.py 正文/ --suggest --emotion-track
```

**预期输出示例：**

```
节奏检测: 正文/ (38 章)

===== 章节紧张度 =====
第021章: ★★☆☆☆ (2/5) 日常过渡
第022章: ★★★☆☆ (3/5) 信息铺垫
第023章: ★★★★☆ (4/5) 矛盾升级
第024章: ★★★★★ (5/5) 高潮冲突
第025章: ★★☆☆☆ (2/5) 冷却消化
第026章: ★★★☆☆ (3/5) 新线索
第027章: ★★★☆☆ (3/5) 调查推进
第028章: ★★★☆☆ (3/5) 调查推进  ← ⚠️ 连续3章紧张度相同

===== 钩子密度 =====
第021章: ✅ 章末钩子 (悬念型)
第022章: ✅ 章末钩子 (信息型)
第023章: ❌ 缺少500字内钩子
第024章: ✅ 章末钩子 (情感型)
第025章: ✅ 章末钩子 (悬念型)
第026章: ⚠️ 钩子力度弱，建议加强
第027章: ✅ 章末钩子 (反转型)
第028章: ✅ 章末钩子 (悬念型)

===== 情绪曲线 =====
平静 ████████░░
紧张 ██████░░░░
悲伤 ████░░░░░░
愤怒 ███░░░░░░░
好奇 █████████░  ← 好奇情绪占比偏高

===== 问题摘要 =====
1. ⚠️ 第026-028章紧张度连续3章为3/5，节奏单调，建议插入一个高/低谷
2. ⚠️ 第023章缺少中段钩子
3. 💡 第028章建议调整为紧张度4/5，为下一高潮做铺垫

===== 节奏模板推荐 =====
针对第026-028章的节奏单调问题，推荐 "悬念→紧张" 模板:
  第026章: 保持3/5（新线索悬念）
  第027章: 提升至4/5（发现关键证据，危机感）
  第028章: 4/5→5/5过渡（逼近真相，章末反转）
```

**解读：** 发现连续3章节奏平坦的问题，按推荐模板调整即可恢复张力起伏。

---

## 场景3：用 tension_forecast 规划接下来的章节

**何时使用：** 大纲规划阶段或中期节奏调整，想知道接下来几章的张力应该怎么排。

**命令：**

```bash
python3 scripts/tension_forecast.py 正文/ --count 7 --outline 大纲.md --character-states character_states.json
```

**预期输出示例：**

```
张力预测: 正文/ (基于已写 38 章)

===== 已写章节张力走势 =====
[021] ██  [022] ███  [023] ████  [024] █████  [025] ██  
[026] ███  [027] ███  [028] ███

===== 未来7章张力预测 =====
第029章: █████ (5/5) 高潮  ← 大纲第29章为"真相揭露"
第030章: ████  (4/5) 余波  
第031章: ███   (3/5) 新篇铺垫
第032章: ████  (4/5) 小高潮 ← 角色弧线转折点（林远山信念动摇）
第033章: ██    (2/5) 情感沉淀
第034章: ███   (3/5) 蓄势
第035章: █████ (5/5) 大高潮 ← 大纲第35章为"终极对决"

===== 弃书风险预判 =====
第030-031章: ⚠️ 高潮后低谷连续2章，建议第031章加入小悬念维持兴趣
第033章: ⚠️ 情感沉淀章容易拖节奏，建议控制在2000字以内

===== 建议写作节奏 =====
29(爆) → 30(收) → 31(钩) → 32(升) → 33(沉) → 34(蓄) → 35(爆)
```

**解读：** 预测了未来7章的张力走势，并标记了2个弃书风险点。写第031章和第033章时需特别注意节奏控制。

---

## 场景4：用 outline_drift 检查是否偏离大纲

**何时使用：** 写了较长一段后，怀疑正文可能偏离大纲规划时。

**命令：**

```bash
python3 scripts/outline_drift.py --outline 大纲.md --chapters 正文/ --fix-suggest
```

**预期输出示例：**

```
大纲偏差检测: 正文/ vs 大纲.md

===== 标题匹配度 =====
匹配: 32/38 (84.2%)
偏离章节:
  - 第019章: 大纲标题"林远山的抉择" → 正文标题"深夜对话" (关键词偏差)
  - 第023章: 大纲标题"线索浮现" → 正文标题"意外的发现" (关键词偏差)
  - 第026章: 大纲标题"内鬼现身" → 正文标题"新的方向" (关键词偏差)

===== 关键词覆盖率 =====
第019章: 60%  ⚠️ 缺少大纲关键词: "抉择"、"挣扎"
第023章: 75%  ✅
第026章: 45%  ❌ 严重偏离，缺少: "内鬼"、"暴露"、"对峙"

===== 角色出场偏差 =====
大纲预期但未出场:
  - 第020章: "赵局长" 应出场但未出现
  - 第026章: "老钱" 应出场但未出现

正文出现但大纲未规划:
  - 第022章: "小王" 额外出场（不影响主线，可保留）

===== 综合偏差评分 =====
第019章: 3.2/10 (轻微偏离)
第023章: 2.1/10 (基本符合)
第026章: 7.8/10 ⚠️ 严重偏离

===== 修复建议 =====
[第026章 - 严重偏离]
  问题: 大纲要求"内鬼现身"，正文改为"新的方向"，核心剧情未展开
  建议: 
    1. 在第026章末尾增加内鬼线索暗示（不完全揭露，留悬念）
    2. 或在第027章尽早补上"内鬼"剧情线
    3. 如有意调整大纲，请在大纲.md中同步修改第026章规划并记录修改日志

[第019章 - 轻微偏离]  
  问题: 缺少"抉择"主题关键词
  建议: 在第019章对话中增加林远山面临两难选择的内心刻画
```

**解读：** 第026章严重偏离大纲，需要补回"内鬼"剧情线或同步修改大纲。第019章轻微偏离，补充抉择主题即可。

---

## 小结

| 场景 | 命令 | 何时用 |
|------|------|--------|
| 一致性检查 | `consistency_check.py` | 每章写完后 |
| 节奏检测 | `rhythm_check.py --suggest` | 感觉节奏不对时 |
| 张力预测 | `tension_forecast.py --count N` | 规划未来章节时 |
| 大纲偏差 | `outline_drift.py --fix-suggest` | 怀疑偏离大纲时 |


path: references/volume_transition.md
content: # 多卷衔接实操指南

> 基于《我的死亡日期消失了》（20卷悬疑）和《观众都是我的前世》（14卷催泪）两部实战经验提炼，提供可直接套用的模板和示例。

---

## 一、卷首开篇的3种模板

### 模板1：悬念式开篇

**适用**：悬疑类、紧张→紧张节奏转换、核心转折卷

**结构**：异常细节切入 → 主角注意到不对劲 → 与上卷悬念挂钩 → 新卷任务浮现

**示例（第一部 第1卷→第2卷过渡，Ch31《回到上海》）**：

> 回上海的高铁上，陆远把笔记本摊在小桌板上。
>
> 窗外是江城到上海沿途的农田和厂房，灰蒙蒙的天空，偶尔闪过一片油菜花。老钱坐在他旁边，闭着眼，像在睡觉。
>
> 但陆远知道他没睡。老钱的手指一直在扶手上轻轻敲着，像在打拍子。
>
> 陆远没有管他。他需要把这几天在永盛看到的所有东西整理一遍。

**拆解**：
- 日常场景（高铁）包裹紧张内核（整理调查笔记）
- 用配角细节暗示情绪（老钱"没睡"）
- 自然带出上卷遗留任务（永盛调查）
- 200字内完成场景+情绪+任务三重建立

**写作要点**：
- 第一个细节必须是**具体的、感官化的**（"摊开笔记本"而非"他在思考"）
- 悬念不是大喊大叫，是**安静中的不安**
- 上卷悬念用**一个动作**带出（翻笔记本），不要用内心独白复述

---

### 模板2：日常式开篇

**适用**：催泪/都市类、紧张→舒缓节奏转换、日常过渡卷

**结构**：温馨/日常场景 → 角色状态自然展示 → 细节中藏暗线 → 情感钩子

**示例（第二部 第1卷→第2卷过渡，Ch32起）**：

> 创业第70天。林昭的办公室从出租屋搬到了创业园B栋308。
>
> 新办公室有三张桌子、一台二手打印机、和一面还没来得及刷白的公司logo墙。何薇坚持要自己刷，小王说不如贴墙纸。
>
> 影院银幕变了。
>
> 从黑白的变成了彩色。
>
> 林昭坐在工位上，盯着银幕上那些鲜活的弹幕——777号老六的颜色从灰色变成了暗红，001号赵承乾从白变成了金色。

**拆解**：
- 日常场景（搬办公室）建立新环境
- 配角互动展示团队关系（何薇vs小王的细节）
- 用一个变化（银幕变彩色）悄悄暗示新卷方向
- 读者自然被吸引：为什么银幕变了？

**写作要点**：
- 日常场景要**真**——具体到桌子数量、打印机型号
- 暗线藏在变化的细节里，不要明说
- 情感温度要**暖**，让读者感到安全后再下刀

---

### 模板3：回顾式开篇

**适用**：长时间跳跃后、新阶段开始、信息量大的卷

**结构**：一个触发物（旧物/旧地/旧人）→ 回忆闪回 → 现状对比 → 新矛盾

**示例（第二部 第10卷《普通人》开篇设计）**：

> 办公桌抽屉最里面有一张旧电影票。
>
> 林昭已经不记得那部电影叫什么了。但他记得自己一个人坐在最后一排，银幕亮起来的瞬间，头顶出现了一个巨大的倒计时。
>
> 那是三年前。
>
> 现在影院空了。999个座位全是空的。银幕没亮，弹幕没了，锦囊的格子也灰了。
>
> 林昭把电影票放回抽屉，打开电脑，开始看今天的财务报表。

**拆解**：
- 用旧物（电影票）触发回忆，自然交代过去
- 回忆→现状的对比制造情感冲击
- 最后一个动作（看报表）建立新卷基调：普通人生活
- 三段时间（三年前→现在→今天）自然压缩

**写作要点**：
- 回顾不超过**3句话**，用细节而非概述
- 现状对比要制造**情感落差**（满→空、热闹→寂静）
- 最后必须落回**当下动作**，不能停在回忆里

---

## 二、卷末收束的3种模板

### 模板1：高潮式收束

**适用**：核心转折卷、boss战卷、真相揭示卷

**结构**：冲突爆发 → 关键选择/牺牲 → 短暂沉默 → 一句话炸弹

**示例（第一部 第1卷末尾，Ch30《第一次反击》）**：

> "行。"他说，"我跟你一起。"
>
> "你不用——"
>
> "三十年的调查笔记，"老钱打断他，"只记了线索，没记证据。这次我不会再犯同样的错误。"

**拆解**：
- 冲突得到阶段性推进（决定系统性调查）
- 关键角色做出承诺（老钱的转折）
- 结尾短促有力，读者想知道下一卷会发生什么

**示例（第二部 第5卷末尾，Ch154，001号大帝毕业）**：

> 影院的龙椅空了。
>
> 001号赵承乾的位置上什么都没留下——没有金色弹幕，没有帝王威压，连影子都没有。
>
> 林昭坐在空荡荡的影院里，看着那把龙椅。
>
> 从今天开始，没有大帝了。
>
> 只有他。

**写作要点**：
- 高潮后必须有**1-2段的静**（像爆炸后的耳鸣）
- 最后一句要**短**（10字以内最佳）
- 角色的选择比事件本身更有冲击力

---

### 模板2：悬念式收束

**适用**：信息揭示卷、过渡卷、铺线卷

**结构**：阶段性完成 → 新信息/新异常出现 → 主角还没反应过来 → 卷终

**示例（第二部 第3卷末尾设计，Ch94）**：

> 前世碰撞告一段落。弹幕安静了。
>
> 林昭关掉电脑，准备下班。
>
> 手机震了一下。
>
> 一条短信，号码未知：
>
> "你以为赢了？你连对手是谁都不知道。"
>
> 林昭盯着屏幕看了三秒。然后锁屏，把手机揣进口袋。

**写作要点**：
- 悬念必须在**阶段性满足之后**出现（不能一整卷都在吊胃口）
- 新信息要**具体**（短信内容）而非模糊（"一种不安的感觉"）
- 主角的反应要**克制**——看三秒就揣进口袋，比大惊失色更让读者好奇

---

### 模板3：余韵式收束

**适用**：日常过渡卷、情感沉淀卷、收尾卷

**结构**：事件自然结束 → 一个安静的细节 → 情感回响 → 留白

**示例（第二部 终卷末尾设计，Ch502）**：

> 影院的灯灭了。
>
> 最后一颗星也灭了。穹顶暗下来，像从来没有过999颗星。
>
> 林昭站起来，走到出口。
>
> 推开门。
>
> 外面是阳光。苏晚在车里等他。孩子在后座睡着了。
>
> 林昭回头看了一眼空荡荡的入口。
>
> 然后他笑了，上了车。

**写作要点**：
- 余韵靠**细节**不靠抒情（杯温、星光、后座睡着的孩子）
- 最后必须是**向前走**的姿态，不是回头看
- 控制在**200字以内**，干净利落
- 不需要解释，让读者自己体会

---

## 三、时间跳跃处理

### 短跳（几天~2周）

**方法**：场景细节自然交代，不喊"X天后"

| 交代方式 | 示例 |
|---------|------|
| 天气/环境变化 | "窗外下着雨——上周从江城回来那天还是晴天" |
| 对话中的时间词 | 老钱："这周你瘦了。" |
| 角色状态暗示 | "三天没怎么睡，眼下发青" |
| 物件变化 | "桌上多了三个空咖啡杯——从江城带回来的习惯" |

**第一部实际示例（Ch30→Ch31）**：没有明说过了几天，用"回上海的高铁"交代空间转换，用笔记本记录的日期（4月12日、4月13日）自然标明时间。

**原则**：
- 短跳不需要任何"过渡段"，直接进入新场景
- 读者自己会从细节推算时间
- **禁止**"三天后""一周过去了"这种旁白式跳转

---

### 中跳（1~6个月）

**方法**：用角色变化和环境变化双重暗示

**三部曲**：
1. **环境变了**：新办公室、新城市、新季节
2. **人变了**：新习惯、新关系状态、新能力
3. **有东西留下了**：跳过期间的事件变成悬念

**第二部实际示例（V1→V2，跳过约70天）**：
- 环境变了：出租屋→创业园B栋308
- 人变了：林昭从社畜变成创业者，有了团队
- 留下了：银幕从黑白变彩色（暗示系统升级）

**操作模板**：
```
[新环境描写1-2句]
[角色新状态1句]
[一个变化暗示时间过了多久]
[一个悬念拉回主线]
```

**原则**：
- 中跳**不要补叙**跳过期间的所有事件
- 只交代2-3个关键变化，其余让读者自己拼
- 跳过期间的事件，变成新卷的**暗线悬念**

---

### 长跳（半年~数年）

**方法**：蒙太奇快切 + 对照法

**蒙太奇法**（开篇3-5个短场景快切）：
```
场景A：一个标志性画面（1-2段）
场景B：另一个变化画面（1-2段）
场景C：回到主角当下（正常叙事）
```

**第二部实际应用（V10→V11，跳过数年）**：
- 第十卷末：影院空了，林昭是普通人
- 第十一卷初：新持有者出现，林昭从持有者变成别人的"前世"
- 时间跳跃通过"孩子出生""公司上市"等标志性事件暗示跨度

**对照法**：用一个与上卷末尾**相似但不同**的场景展示变化。

**原则**：
- 长跳后**必须重新建立情感连接**——用一个日常场景让读者重新"认识"主角
- 不要一次性倒出所有变化，分3-5章逐渐揭示
- 关键角色的状态必须**明确交代**（读者会担心他们）

---

## 四、节奏转换的具体过渡手法

### 紧张卷 → 轻松卷（减速）

**核心**：让主角和读者一起"喘气"，但暗线不能断。

**三部曲**：
1. **最后一战的后遗症**：用1-2章处理收尾（受伤恢复、证据整理、告别）
2. **日常回归**：回到熟悉环境，恢复日常节奏
3. **暗线埋雷**：在看似平静中埋入1-2个异常信号

**第一部示例（V1紧张调查→V2回到上海）**：
- Ch30：永盛反击战（紧张高潮）
- Ch31：高铁上整理笔记（减速）
- Ch32：回到上海，周雅发现异常（暗线重启）

**操作要点**：
- 前两章节奏控制在**每章1个事件**（紧张卷是每章2-3个）
- 环境描写可以增加（给读者"风景"看）
- 配角的轻松互动是天然的减速器（老钱的酒、何薇的吐槽）

---

### 轻松卷 → 紧张卷（加速）

**核心**：用"不对劲"感代替"大事件"，让紧张感自然升温。

**三部曲**：
1. **末尾埋雷**：在日常中悄悄放入1-2个不安信号（不是危机，是异常）
2. **新卷第一章小事件**：不需要大场面，一个反常行为/数据/细节就够了
3. **第二章升温**：小事件牵出更大的问题

**第二部示例（V3前世碰撞结束→V4深渊）**：
- V3末：弹幕安静了，看似和解
- V4初：锦城集团新一轮攻击+前世围攻，双线压力
- 林昭心理崩溃线开始

**操作要点**：
- **不要**用爆炸/枪战等大事件开场
- 用**细节异常**制造不安："他今天笑了两次——平时不会超过一次"
- 配角的反常行为比主角的反常更有效（读者会替主角担心）

---

### 平级节奏转换（紧张→紧张）

**核心**：换战场不换压力，避免读者疲劳。

**手法**：
- **切换调查对象/对手**：旧boss解决，新问题浮现
- **插入1章冷却**：配角视角或主角独白，给读者消化信息
- **升级规则**：同样的压力但赌注更大

---

## 五、新卷引入新角色的"三章法则"

### 核心原则

新角色在前3章内必须完成：**出场 → 建立印象 → 展现独特价值**

### 第1章：出场——先看到行为，不知道身份

- 通过**事件**自然出场（不是介绍段落）
- 读者只知道：这个人做了什么、看起来怎样
- **给一个记忆锚点**：一句口头禅/一个标志性动作/一个视觉特征

**第一部示例**：孙磊的出场
- 第1次见面就是尽调会议，行为描写（金丝眼镜、标准笑容、握手有力）
- 记忆锚点：**不吞咽**——一个看似无害但让人毛骨悚然的细节
- 读者不知道他是"死人"，只觉得"有点不对"

### 第2章：建立印象——展现性格特质

- 通过**对话和行动**建立性格，不要用叙述性介绍
- 与已有角色产生**化学反应**（冲突、互补、共鸣）
- 让读者对这个角色产生**情绪**（喜欢/讨厌/好奇）

**第二部示例**：赵鹏的出场（V2）
- 通过神秘短信暗线引入，先是痕迹而非本人
- 建立印象：商业对手的阴险感
- 读者情绪：不安和好奇

### 第3章：展现独特价值——证明存在必要性

- 新角色必须做一件**只有他/她能做的事**
- 这件事要**推动剧情**或**改变主角的认知**
- 完成后读者会说："哦，原来需要这么个人"

**禁忌**：
- **不要**同章引入超过2个新主要角色
- **不要**用"这是XX，他今年XX岁"式的介绍段落
- **不要**让新角色在前3章就变成核心（要有融入过程）
- **要**给每个新角色一个独属于自己的**说话方式**

### 角色类型与出场方式对照

| 角色类型 | 出场方式 | 第1章 | 第2章 | 第3章 |
|---------|---------|-------|-------|-------|
| **盟友型** | 事件驱动 | 在事件中自然出现 | 展现能力/资源 | 做一件帮到主角的事 |
| **对手型** | 悬念铺垫 | 先出现影响/痕迹 | 正式露面，制造冲突 | 让主角吃一次亏 |
| **导师型** | 关系引出 | 通过配角提及 | 正式见面，展现深度 | 提供关键信息/视角 |
| **谜团型** | 悬念登场 | 只出现碎片信息 | 短暂接触，留下疑问 | 展现与主线的隐秘关联 |

---

## 六、实战速查表

### 卷首开篇选择

| 卷的类型 | 推荐模板 | 节奏 |
|---------|---------|------|
| 悬疑核心卷 | 悬念式 | 中→高 |
| 日常过渡卷 | 日常式 | 低→中 |
| 长跳后的新阶段 | 回顾式 | 低→中 |
| 终章/收尾卷 | 回顾式 | 低→高→低 |

### 卷末收束选择

| 卷的类型 | 推荐模板 | 留白度 |
|---------|---------|--------|
| boss战/揭示卷 | 高潮式 | 低（爆炸性结尾） |
| 信息卷/铺线卷 | 悬念式 | 中（新问题浮出） |
| 日常卷/收尾卷 | 余韵式 | 高（让读者自己品） |

### 时间跳跃速查

| 跨度 | 方法 | 补叙量 | 暗线 |
|------|------|--------|------|
| 几天~2周 | 场景细节暗示 | 0 | 不需要 |
| 1~6个月 | 环境+人变化 | 1-2个关键事件 | 跳过事件变悬念 |
| 半年~数年 | 蒙太奇/对照 | 分3-5章揭示 | 必须有长线暗线 |

---

## Agent 使用说明

### 写作时
1. 写卷首章前，查上卷末章最后500字，确认情绪落点
2. 选择对应的开篇模板
3. 如果有时间跳跃，参照对应跨度的处理方式
4. 新角色严格遵循"三章法则"

### 审校时
1. 检查卷首前3段是否建立了场景+情绪+任务
2. 检查卷末最后200字是否有满足感+追读动力
3. 检查时间跳跃是否有自然交代（不是旁白硬跳）
4. 检查新角色是否在3章内完成了出场→印象→价值

### 验证命令

```bash
# 检查卷首章的字数和结构
python3 scripts/consistency_check.py <正文目录> --dict 设定词典.md

# 对比两卷节奏差异（text_dir 为修改后，--compare 指向修改前目录）
python3 scripts/rhythm_check.py <本卷正文目录>/ --compare <上卷正文目录>/
```

---

## 补充：卷间信息密度调整

> 来自操作级实践补充。

### 信息密度等级

| 等级 | 定义 | 适用位置 | 每章新信息量 |
|------|------|----------|------------|
| 高密度 | 每页都有新线索/新设定 | 卷首（建立新框架）、高潮前 | 3-5个新信息点 |
| 中密度 | 推进剧情但不密集 | 卷中段、过渡章节 | 1-2个新信息点 |
| 低密度 | 消化已给信息、情感沉淀 | 高潮后、卷尾、新卷前几章 | 0-1个新信息点 |

### 卷间密度曲线

```
上卷高潮（高密度）→ 上卷收尾（低密度）→ 新卷开篇（中密度）→ 新卷展开（逐步升高）
```

### 跨卷信息密度自查

- [ ] 上卷最后3章是否给了读者"消化空间"？
- [ ] 新卷前3章新设定/新角色是否不超过2个？
- [ ] 新卷是否在5章内建立了本卷的核心矛盾？
- [ ] 是否有"信息过载"风险（连续3章以上高密度）？

### 速查表

| 场景 | 选什么 | 关键动作 |
|------|--------|----------|
| 高潮后开新卷 | 日常式开篇 | 2-3章低密度呼吸 |
| 悬念未解开新卷 | 悬念式开篇 | 立刻回钩+新线索 |
| 有大段时间跳跃 | 回顾式开篇 | 重新锚定+变化对照 |
| 新卷需要大量新设定 | 回顾式开篇 | 分散在第1-5章逐步引入 |


path: references/writing_rhythm_guide.md
content: # 章节节奏实操指南

节奏是网文的脉搏。太快读者喘不过气，太慢读者弃书。本文档提供 5 种节奏模板，配合实际示例，让每个章节的节奏有据可依。

---

## 一、5 种节奏模板

### 1. 日常节奏（舒缓章）

**用途**：铺垫世界观、推进日常关系、给读者喘息空间。通常跟在紧张章或催泪章之后。

| 维度 | 建议值 |
|------|--------|
| 总字数 | 2000-3000 字 |
| 开头（前 300 字） | 日常场景切入，轻松语调，可回忆或描写 |
| 发展（300-1800 字） | 人物互动、生活细节、幽默对话，推进关系 |
| 高潮（1800-2500 字） | 一个小冲突或小发现，轻微吊胃口 |
| 结尾（最后 200 字） | 铺一个轻钩子（伏笔/异常细节），不急不缓 |
| 钩子密度 | 3-5 个/千字（以金句型和信息差型为主） |
| 对话比例 | 40-55% |
| 短句比例 | 25-35% |
| 弹幕密度建议 | 低-中（每 500 字 1-2 条弹幕，日常吐槽/角色互动为主） |

**示例段落**（来自《我的死亡日期消失了》第36章「食堂那顿午饭」）：

> 打饭的阿姨在窗口后面舀菜，不锈钢盘子磕出清脆的声响。空气里飘着酱油和葱姜的味道。
>
> 陆远端着盘子站在食堂门口扫了一圈。
>
> 第一张桌，三个穿蓝色工装的工人，余额都在三十五年以上，正常跳动。吃饭速度很快，筷子碰盘子叮叮当当，聊着上周的钓鱼比赛。

**节奏要点**：用感官细节（声响、气味）营造日常氛围，但通过"扫描余额"维持角色特质，结尾处"孙磊"的出现自然过渡到下一章紧张节奏。

---

### 2. 紧张节奏（高压章）

**用途**：危机逼近、对峙、追查关键线索。是故事的引擎，制造"不得不看下去"的紧迫感。

| 维度 | 建议值 |
|------|--------|
| 总字数 | 2500-3500 字 |
| 开头（前 200 字） | 直接进入紧张状态，不做铺垫 |
| 发展（200-2000 字） | 信息快速叠加，每段都有新发现或新危机 |
| 高潮（2000-3000 字） | 核心冲突爆发，短句密集，情绪顶点 |
| 结尾（最后 200 字） | 强悬念钩子，留一个未回答的问题 |
| 钩子密度 | 8-12 个/千字（悬念型+惊悚型为主） |
| 对话比例 | 25-40%（多短对话，少长篇大论） |
| 短句比例 | 45-60% |
| 弹幕密度建议 | 高（每 300 字 1-2 条弹幕，震惊/推理/紧张型） |

**示例段落**（来自《我的死亡日期消失了》第21章「死亡日期：三个月前」）：

> 2026年1月——什么都没有。
>
> 没有新闻。没有公告。没有变更记录。
>
> 一个月的时间，一家年营收几十亿的制造企业，零公开信息。
>
> 不是"没有新闻可报"。
>
> 是"所有信息都被按住了"。

**节奏要点**：极短句叠加制造压迫感，用否定句式层层递进（"没有…没有…没有…"），信息差让读者和主角同步焦虑。

---

### 3. 催泪节奏（情感章）

**用途**：角色牺牲、离别、回忆杀、情感揭露。制造共鸣和分享欲，是催泪向的核心。

| 维度 | 建议值 |
|------|--------|
| 总字数 | 2000-3000 字 |
| 开头（前 300 字） | 用日常或平静场景铺垫，制造反差 |
| 发展（300-1800 字） | 情感线索渐次展开，用细节而非形容词 |
| 高潮（1800-2500 字） | 情感爆发点，克制表达比煽情更有力 |
| 结尾（最后 300 字） | 金句或沉默，留白让读者自己感受 |
| 钩子密度 | 4-7 个/千字（催泪型+金句型为主） |
| 对话比例 | 20-35%（少说多做，沉默比语言更有力） |
| 短句比例 | 35-45% |
| 弹幕密度建议 | 中-高（每 400 字 1-2 条弹幕，情感共鸣型） |

**示例段落**（来自《我的死亡日期消失了》第1章，铺垫型催泪）：

> 七岁那年他第一次开口告诉别人这件事——对外婆说"外婆你头上有个数字在变"。外婆愣了一下，然后笑着摸他的头，说小孩子不要乱讲话。三天后外婆的余额归零了。她停顿了0.2秒，然后继续笑着给他盛饭。但那碗饭她没有吃完。
>
> 从此他再也没有对任何人说过。

**节奏要点**：用"0.2秒"这样精确的细节代替"她很难过"，克制表达制造更大的情感冲击。结尾的"从此再也没有"是经典的留白式催泪。

---

### 4. 悬念节奏（推理章）

**用途**：抛出谜团、发现线索、分析推理。让读者跟着主角一起思考，产生"我知道了但主角还不知道"的焦虑。

| 维度 | 建议值 |
|------|--------|
| 总字数 | 2500-3500 字 |
| 开头（前 200 字） | 抛出核心疑问或异常现象 |
| 发展（200-2200 字） | 线索逐条展开，有真有假，制造推理空间 |
| 高潮（2200-3000 字） | 关键发现或关键推理，改变认知框架 |
| 结尾（最后 200 字） | 新谜团或颠覆性信息，让读者重新思考 |
| 钩子密度 | 6-10 个/千字（悬念型+反转型为主） |
| 对话比例 | 30-45%（问答式对话推进推理） |
| 短句比例 | 35-50% |
| 弹幕密度建议 | 高（每 300 字 1-2 条弹幕，推理/讨论型） |

**示例段落**（来自《轮回影院》第17章「第二条线索」）：

> "朕以为他是所有前世里地位最低的那个——最不起眼，最没有存在感，坐在最后一排，从来不发言。"
>
> "但现在朕觉得——也许恰恰相反。"
>
> "也许他是所有前世里……最重要的那个。"
>
> 影院里没有人反驳。
>
> 因为所有人都有同样的感觉。

**节奏要点**：通过角色对话完成推理，用"也许恰恰相反"做认知翻转，结尾"所有人都有同样的感觉"暗示共识但不揭底。

---

### 5. 转折节奏（爆点章）

**用途**：真相揭露、立场反转、命运改变。是每 8-10 章一次的大爆点，让读者截图分享的瞬间。

| 维度 | 建议值 |
|------|--------|
| 总字数 | 2000-3000 字 |
| 开头（前 150 字） | 误导性铺垫，让读者以为走向A |
| 发展（150-1500 字） | 看似正常的剧情推进，暗藏伏笔 |
| 高潮（1500-2500 字） | 核心转折瞬间，短句轰炸，认知颠覆 |
| 结尾（最后 300 字） | 转折后的余波+新方向暗示 |
| 钩子密度 | 10-15 个/千字（反转型+惊悚型+爽感型叠加） |
| 对话比例 | 20-35%（多用动作和内心独白） |
| 短句比例 | 50-65% |
| 弹幕密度建议 | 极高（每 200 字 1-2 条弹幕，震惊/反转/打卡型） |

**示例段落**（来自《轮回影院》第20章「苏晚重逢」）：

> 名片上有一行小字，他之前没注意到——
>
> **天成资本·合伙人·周衍**
>
> **「每个人都有一座轮回影院。」**
>
> 林昭的瞳孔猛地缩了一下。他揉了揉眼睛，再看——
>
> 名片上那行小字，消失了。只剩名字和公司。什么都没有。

**节奏要点**：先铺误导（正常商务名片），然后用粗体+独立行做视觉冲击，"消失了"三字制造超自然恐怖感，读者必须翻下一页。

---

## 二、节奏搭配建议

### 推荐交替模式

```
日常 → 悬念 → 日常 → 紧张 → 催泪 → 日常 → 悬念 → 转折
```

**核心原则**：
- **张弛有度**：每 2-3 章高压后必须插入 1 章日常/轻松
- **情绪递进**：日常→悬念→紧张→转折，层层递进比突然爆发更有效
- **催泪需要铺垫**：催泪章前至少 1 章日常建立情感连接
- **转折需要误导**：转折章前至少 2 章铺垫让读者建立错误预期

### 10 章周期模板

| 章节 | 节奏 | 作用 |
|------|------|------|
| 第1章 | 悬念 | 开篇抓人，抛核心设定 |
| 第2章 | 日常 | 展开世界观，介绍角色 |
| 第3章 | 悬念 | 深化谜团，叠加线索 |
| 第4章 | 紧张 | 第一次冲突/危机 |
| 第5章 | 日常 | 喘息，推进关系 |
| 第6章 | 悬念 | 新线索出现 |
| 第7章 | 紧张 | 危机升级 |
| 第8章 | 催泪 | 情感冲击 |
| 第9章 | 紧张 | 最后冲刺 |
| 第10章 | 转折 | 大爆点，改变认知框架 |

---

## 三、弃书风险点

### 🔴 高危节奏组合（容易弃书）

1. **连续 4+ 章日常**：读者失去耐心，觉得"没有剧情"。日常章最多连 2 章，第 3 章必须出现悬念或冲突。
2. **连续 3+ 章紧张无喘息**：读者疲劳，紧张感钝化。高压之间必须插入至少 1 章舒缓。
3. **转折后无消化**：大反转后直接进入下一个紧张章，读者来不及消化情绪，会觉得"太假"。
4. **催泪章后接催泪章**：连续催泪会麻木，催泪章的效力在于"意料之外的动情"，连续使用适得其反。
5. **悬念长期不回收**：如果 5+ 章都在抛悬念而没有一个回收，读者会失去信任感。每 3-4 章至少回收一个小悬念。

### 🟡 中危节奏问题

6. **开篇两章都是日常**：网文黄金三章法则——前 3 章必须让读者知道核心设定+产生好奇。如果前 2 章都是日常铺垫，第 3 章必须爆点。
7. **章节长度差异过大**：相邻两章字数差超过 2 倍（如一章 3000 字，下一章 800 字），阅读体验断裂。
8. **弹幕密度与节奏不匹配**：紧张章弹幕太少（缺氛围）或日常章弹幕太多（喧宾夺主）。

### 🟢 低危但需注意

9. **同一类型钩子连续使用**：如果连续 3 章结尾都是"他愣住了"，读者会产生抗体。钩子类型要轮换。
10. **全章无钩子**：任何章节的任何 500 字段落内至少要有 1 个小钩子，否则该段落可删。

---

## 四、与节奏检测脚本配合使用

本指南与 `scripts/rhythm_check.py` 配合：

1. **写完初稿后**运行 `python3 rhythm_check.py <正文目录>`，查看节奏曲线
2. **对照紧张度评分**：
   - 日常章应在 20-40 分
   - 悬念章应在 40-60 分
   - 紧张章应在 60-80 分
   - 转折章应在 70-90 分
3. **修复报警问题**后再提交终稿

---

## 五、快速决策表

| 你要写什么？ | 用什么节奏 | 关键指标 |
|-------------|-----------|---------|
| 吃饭/聊天/日常生活 | 日常 | 对话>40%，钩子<5/千字 |
| 追查线索/推理分析 | 悬念 | 每段抛线索，结尾留疑问 |
| 被追杀/对峙/危机 | 紧张 | 短句>45%，不做铺垫 |
| 回忆/牺牲/离别 | 催泪 | 克制表达，细节>形容词 |
| 真相揭露/立场翻转 | 转折 | 先误导再颠覆，短句轰炸 |


path: references/writing_tips.md
content: # 新手常见写作错误速查手册

基于《我的死亡日期消失了》（38章）和《观众都是我的前世》（49章）实际检测结果，以及 consistency_check.py / rhythm_check.py / outline_drift.py 三脚本长期测试中发现的高频问题整理。

---

## 高频错误 TOP 20

### 1. AI高频词泛滥（最常见，占比最高）
- **问题描述**：「不禁」「缓缓」「淡淡」「微微」「竟然」「居然」「一声叹息」「嘴角微扬」等AI生成痕迹词反复出现
- **检测方法**：consistency_check.py 检查项5（AI词检测），两部小说共检出 60+ 处
- **修正方案**：用具体动作/感官替换。如「不禁笑了」→「嘴角已经压不住了」；「缓缓开口」→删掉「缓缓」，直接说话
- **正面示例**：
  - ❌ 他不禁握紧了拳头
  - ✅ 指甲掐进掌心，疼了一下

### 2. 角色对话风格雷同
- **问题描述**：所有角色说话节奏、信息密度、语气高度统一，差异仅靠口头禅区分
- **检测方法**：人工对比不同角色的对话，检查句长、语气词、信息密度（dialogue_quality.md 的15项清单）
- **修正方案**：按角色性格差异化——急性子短句多、冲动；文化人书面词多、爱引经据典；老练的人话少、点到为止
- **正面示例**：
  - ❌ 赵恒："我建议你先调查清楚再行动。" / 老钱："我建议你先调查清楚再行动。"
  - ✅ 赵恒："查。别急，把线索捋清楚再动。" / 老钱："小伙子，这水比你想象的深。"

### 3. 数值前后矛盾
- **问题描述**：余额、年龄、时间跨度等数值在不同章节不一致
- **检测方法**：consistency_check.py 检查项4（数值矛盾），实测检出「孙磊余额42年 vs 42年137天 vs 4」等矛盾
- **修正方案**：写作前查设定词典/追踪表；对关键数值建独立数值表，每章更新
- **正面示例**：第20章写「余额42年137天」，第32章引用时写「四年出头，加上之前攒的，总共四十二年挂零」——精确与口语表达一致

### 4. 角色长时间消失无交代
- **问题描述**：重要角色连续5章以上不出场且无去向交代
- **检测方法**：consistency_check.py 检查项11（角色出场追踪），检出赵恒连续7章、张维连续15章未出场
- **修正方案**：用一句话交代去向（「赵恒这周出差，消息都没回」），或安排其他角色提及
- **正面示例**：「张维？上个月调去深圳了，听说是升了。」

### 5. 重复段落/草稿残留
- **问题描述**：不同文件存在100%相似段落（通常是草稿文件未清理）
- **检测方法**：consistency_check.py 检查项9（重复段落），检出草稿与正式章节完全重复
- **修正方案**：写完后清理草稿文件；用 --skip-drafts 参数跳过草稿检测
- **正面示例**：章节目录只保留正式版本，草稿移至 `drafts/` 子目录

### 6. 字数不足的"水章"
- **问题描述**：单章字数低于1500字，内容空洞无法支撑叙事
- **检测方法**：consistency_check.py 检查项6（字数检测），检出第19章仅1419字
- **修正方案**：补充感官细节、内心活动、环境描写，或合并到相邻章节
- **正面示例**：短章不等于烂章——如果内容紧凑2000字也够，但要有足够的密度

### 7. 对话像面试问答
- **问题描述**：每个问题都被完美回答，对话一问一答像审讯，缺乏跑题、答非所问
- **检测方法**：人工检查——对话是否形成「问→答→问→答」固定模式
- **修正方案**：加入答非所问、转移话题、沉默、反问、岔开等真实对话行为
- **正面示例**：
  - ❌ "什么时间点？""不知道。但触发条件和余额有关。"
  - ✅ "什么时间点？""你吃了吗？楼下新开了家面馆。"

### 8. 对话句式过长/书面腔
- **问题描述**：角色说话像写作文，句子超过50字，使用「因此」「然而」「所以」等书面连接词
- **检测方法**：consistency_check.py 检查项12（对话质量），检测≥50字的对话句
- **修正方案**：口语化——拆长句、加语气词、用省略号表示停顿
- **正面示例**：
  - ❌ "因此我们必须在尽调完成之前，把所有可疑的财务数据都梳理一遍。"
  - ✅ "所以那些账，趁尽调还没完，得赶紧理一遍。"

### 9. 对话堆砌无动作描写
- **问题描述**：连续8行以上纯对话，无动作、表情、环境描写，像剧本不像小说
- **检测方法**：consistency_check.py 检查项12，检测连续≥8行纯对话
- **修正方案**：每3-4句对话插入一个小动作或表情描写
- **正面示例**：
  ```
  "你确定？"
  "确定。"陆远把杯子转了半圈。
  "那行。"
  ```

### 10. 连续章节节奏太平
- **问题描述**：连续3章以上缺少钩子词，紧张度曲线走平，读者容易弃书
- **检测方法**：rhythm_check.py 检查连续太平章节
- **修正方案**：参考 writing_rhythm_guide.md 的5种节奏模板，在连续2章日常后插入紧张/悬念章
- **正面示例**：日常→日常→悬念反转→日常→紧张高潮，避免日常→日常→日常→日常

### 11. 情感表达靠叙述而非行为
- **问题描述**：「声音变了」「停了两秒」「语气微妙地变了」——用叙述标签代替对话本身的张力
- **检测方法**：搜索「语气」「声音」「停了」「微妙」等叙事标签词
- **修正方案**：让对话本身传递情绪，或用角色动作传递（摔杯子、攥紧手机）
- **正面示例**：
  - ❌ 他的语气微妙地变了："是吗。"
  - ✅ "是吗。"他把筷子放下了。

### 12. 外貌描写重复
- **问题描述**：每章对同一角色的外貌描写雷同，套话化
- **检测方法**：人工对比不同章节的外貌描写段落
- **修正方案**：第一次全面描写，后续只提变化或最突出的1-2个特征
- **正面示例**：首次「浓眉，瘦高，永远穿白衬衫」；后续「白衬衫袖子挽到肘」即可唤起印象

### 13. 章节结构雷同
- **问题描述**：每章都是「环境描写→对话推进→悬念结尾」固定结构
- **检测方法**：rhythm_check.py 检查连续结构相似章节（对话比例/短句比例/段落长度三维度）
- **修正方案**：交替使用不同开篇方式（对话开篇/动作开篇/回忆开篇/悬念倒叙开篇）
- **正面示例**：第1章悬念开篇→第2章对话开篇→第3章回忆开篇

### 14. 章末钩子单调
- **问题描述**：每章结尾都是悬念式，缺乏变化
- **检测方法**：统计最近10章的结尾类型
- **修正方案**：参考 hook_design.md 的7种钩子类型，轮换使用
- **正面示例**：悬念→情感→反转→日常余韵→悬念

### 15. 弹幕/群聊发言顺序固定
- **问题描述**：弹幕角色每章按相同顺序发言，缺乏随机感
- **检测方法**：人工对比多章弹幕出场顺序
- **修正方案**：按事件相关性安排发言，不同话题由不同角色先发言
- **正面示例**：话题A由777号先开口，话题B由444号先抢答

### 16. 视角混乱
- **问题描述**：第一人称视角突然跳到第三人称，或出现主角不可能知道的信息
- **检测方法**：consistency_check.py 检查项7（视角检查）
- **修正方案**：严格限定视角人物；如需多视角，用明确的场景分隔
- **正面示例**：陆远视角的章节不能写「赵恒心里想」

### 17. 设定名词不统一
- **问题描述**：同一事物在不同章节叫法不同（如公司名、地点名、系统名称）
- **检测方法**：consistency_check.py 检查项1（角色称呼）+ 设定词典对照
- **修正方案**：维护设定词典，写作前查词典
- **正面示例**：「永盛集团」全程统一，不混用「永盛」「永盛公司」「那家集团」

### 18. 情绪曲线突变无铺垫
- **问题描述**：角色情绪突然反转（前一秒平静后一秒暴怒），缺乏心理铺垫
- **检测方法**：rhythm_check.py --emotion-track 检测情绪突变点（欧氏距离≥0.35）
- **修正方案**：情绪转折前用2-3个细节暗示（握拳、咬唇、声音变低）
- **正面示例**：暴怒前先写「他没说话。手指在桌面上敲了三下，停住了。」

### 19. 伏笔埋后遗忘
- **问题描述**：前期埋的伏笔后续完全不提及也不回收
- **检测方法**：foreshadowing.json 的过期预警机制
- **修正方案**：参考 foreshadowing.md，每10章检查伏笔状态，20/50/100章三级预警
- **正面示例**：第5章埋「匿名短信」，第15章角色偶然提及「那条短信」，第30章揭示来源

### 20. 大纲偏差无意识
- **问题描述**：正文逐渐偏离大纲，但作者未察觉
- **检测方法**：outline_drift.py 逐章偏差评分
- **修正方案**：每5章跑一次大纲偏差检测；偏差>0.4时人工决定回归大纲还是修改大纲
- **正面示例**：大纲写「第10章发现线索」，正文改为「第8章发现线索，第10章验证线索」——有意识调整

---

## 快速自检清单（写完后5分钟过一遍）

1. [ ] **AI词扫描**：搜索「不禁/缓缓/淡淡/微微/竟然/居然」，至少检查前5个结果
2. [ ] **数值核对**：本章提到的关键数值（余额/年龄/时间）是否与追踪表一致
3. [ ] **角色在场确认**：本章该出场的重要角色是否都出场了？消失的角色有交代吗？
4. [ ] **对话差异化**：把本章对话去掉角色名，还能分辨谁在说话吗？
5. [ ] **对话节奏**：有没有超过5句连续对话没有动作描写？
6. [ ] **钩子检查**：章末有没有让读者想翻下一页的理由？
7. [ ] **字数底线**：本章字数是否≥1500字？如果不够，内容密度是否足够？
8. [ ] **视角一致性**：有没有出现视角人物不可能知道的信息？
9. [ ] **结构变化**：本章的开篇方式和上一章是否不同？
10. [ ] **设定名词**：公司名/地点名/专有名词是否与设定词典一致？

---

## 进阶避坑指南（不容易被脚本检测到的深层问题）

### 1. 角色行为OOC（Out of Character）
脚本能检测名字是否出现，但无法判断角色行为是否符合人设。一个「冷静克制」的角色突然冲动行事，需要有充分的心理铺垫，否则就是OOC。
- **自检方法**：写完关键行为后问自己「这个角色之前的行为模式支持他这样做吗？」

### 2. 信息密度不均匀
前半章信息量爆炸（大量设定解说），后半章空洞。读者在前半章被信息淹没，后半章觉得无聊。
- **自检方法**：每500字检查一次——这段有新信息吗？信息量是否均衡？

### 3. 情感代币化
把情感写成交易——「痛苦了一个段落就该高兴了」。真实情感有惯性，不会精确地按段落切换。
- **自检方法**：角色的情绪变化是否有足够的过渡空间？

### 4. 反派/对手戏份不足
主角光环过强，对手/反派缺乏有效抵抗，冲突缺乏张力。
- **自检方法**：对手是否有过至少一次让主角真正陷入困境的行动？

### 5. 时间感模糊
读者无法感知故事过去了多久——是几天还是几周？节奏感丢失。
- **自检方法**：每章是否有时间标记（天气变化、日期提及、作息暗示）？

### 6. 剧情推动靠巧合而非角色行动
关键转折总是「恰好」发生，而非角色主动争取。削弱了角色的主体性。
- **自检方法**：每个关键转折点，是角色主动选择还是外部事件推动？

### 7. 世界观设定与情感脱节
世界观/系统设定很精美，但角色的情感反应像是局外人，没有真正被设定影响。
- **自检方法**：角色的日常是否受到世界观设定的真实影响？（如「余额倒计时」是否影响了角色的焦虑感）

### 8. 配角工具人化
配角只在需要推动剧情时出现，没有自己的生活和动机。
- **自检方法**：删掉这个配角，剧情会受影响吗？如果会，说明他只是工具人。

### 9. 悬念强度衰减
早期悬念很强，后续揭示前铺垫不足，导致读者对后续悬念的期待降低。
- **自检方法**：每个悬念在揭示前，最近5章是否有至少一次「提醒读者这个悬念还在」？

### 10. 节奏与读者预期错位
爽文小说的读者期待高频爽点，但作者在写慢节奏的铺垫；悬疑小说的读者期待逻辑严密，但作者在写情感戏。
- **自检方法**：找3个目标读者类型的朋友试读前10章，看他们的弃书点是否一致。

---

## 附录：检测脚本速查

| 问题类型 | 对应脚本 | 参数 |
|---------|---------|------|
| AI高频词 | consistency_check.py --auto-fix | 自动生成替换建议 |
| 数值矛盾 | consistency_check.py | 检查项4 |
| 角色消失 | consistency_check.py --character-states | 需配合 character_states.json |
| 重复段落 | consistency_check.py | 检查项9 |
| 节奏太平 | rhythm_check.py --suggest | 附带修复建议 |
| 情绪突变 | rhythm_check.py --emotion-track | 四维情绪追踪 |
| 大纲偏差 | outline_drift.py --fix-suggest | 附带修复建议 |
| 节奏预测 | tension_forecast.py | 预测未来10章节奏 |


path: scripts/backup.py
content: #!/usr/bin/env python3
"""Novel Writer 数据备份"""

import json
import shutil
import sys
from datetime import datetime
from pathlib import Path

def backup_novel(novel_dir: str, output_dir: str = None):
    """备份小说数据文件（不备份正文，只备份结构化数据）"""
    src = Path(novel_dir).resolve()
    if not src.exists():
        print(f"❌ 目录不存在: {src}")
        return False

    data_files = []
    for pattern in ["character_states.json", "foreshadowing.json", "设定词典.md",
                    "追踪表.md", "检查清单.md", "大纲.md", "设定集.md"]:
        p = src / pattern
        if p.exists():
            data_files.append(p)

    # 也扫描 数据/ 子目录
    data_sub = src / "数据"
    if data_sub.exists():
        data_files.extend(data_sub.glob("*.json"))
        data_files.extend(data_sub.glob("*.md"))

    if not data_files:
        print(f"⚠️  未找到数据文件: {src}")
        return False

    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    name = src.name
    if output_dir:
        dest = Path(output_dir) / f"{name}_backup_{ts}"
    else:
        dest = src / "backups" / f"backup_{ts}"
    dest.mkdir(parents=True, exist_ok=True)

    for f in data_files:
        rel = f.relative_to(src)
        d = dest / rel.parent
        d.mkdir(parents=True, exist_ok=True)
        shutil.copy2(f, d / f.name)

    print(f"✅ 已备份 {len(data_files)} 个文件 → {dest}")
    return True

def main():
    if len(sys.argv) < 2:
        print("📖 Novel Writer 数据备份")
        print("用法: backup <小说目录> [输出目录]")
        sys.exit(0)

    backup_novel(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)

if __name__ == "__main__":
    main()


path: scripts/benchmark.py
content: #!/usr/bin/env python3
"""性能基准测试 - 测量各脚本的执行时间和内存占用

用法:
    benchmark <正文目录>
"""

import os
import subprocess
import sys
import time
from pathlib import Path

SCRIPTS_DIR = Path(__file__).parent


def benchmark_script(name: str, args: list) -> dict:
    """测量单个脚本的执行时间"""
    script = SCRIPTS_DIR / f"{name}.py"
    if not script.exists():
        return None

    start = time.perf_counter()
    result = subprocess.run(
        [sys.executable, str(script)] + args,
        capture_output=True, text=True, timeout=120
    )
    elapsed = time.perf_counter() - start

    return {
        "script": name,
        "time_ms": round(elapsed * 1000, 1),
        "exit_code": result.returncode,
        "output_lines": len(result.stdout.strip().split("\n")) if result.stdout.strip() else 0,
    }


def list_chapters(directory: str):
    return list(Path(directory).glob("*.md"))


def main():
    if len(sys.argv) < 2:
        print("📖 性能基准测试")
        print("用法: benchmark <正文目录>")
        sys.exit(0)

    novel_dir = sys.argv[1]
    if not Path(novel_dir).is_dir():
        print(f"❌ 目录不存在: {novel_dir}")
        sys.exit(1)

    # 统计章节数和字数
    chapters = list(Path(novel_dir).glob("*.md"))
    total_chars = 0
    for c in chapters:
        total_chars += len(c.read_text(encoding="utf-8"))

    print(f"📊 基准测试: {len(chapters)} 章, {total_chars:,} 字\n")

    scripts = [
        ("consistency_check", [novel_dir]),
        ("repetition_check", [novel_dir]),
        ("dialogue_tag_check", [novel_dir]),
        ("chapter_hook_check", [novel_dir]),
        ("chapter_title_check", [novel_dir]),
        ("opening_score", [novel_dir]),
        ("paragraph_check", [novel_dir]),
        ("rhythm_check", [novel_dir]),
        ("foreshadow_scan", [novel_dir]),
        ("tension_forecast", [novel_dir]),
    ]

    results = []
    for name, args in scripts:
        print(f"  ⏳ {name}...", end=" ", flush=True)
        r = benchmark_script(name, args)
        if r:
            results.append(r)
            status = f"{r['time_ms']}ms"
            if r["exit_code"] != 0:
                status += " ❌"
            print(status)
        else:
            print("跳过")

    # 结果表格
    print(f"\n{'='*60}")
    print(f"{'脚本':<25} {'时间':>10} {'输出行':>8}")
    print(f"{'-'*60}")

    total_time = 0
    for r in sorted(results, key=lambda x: x["time_ms"], reverse=True):
        print(f"{r['script']:<25} {r['time_ms']:>8}ms {r['output_lines']:>8}")
        total_time += r["time_ms"]

    print(f"{'-'*60}")
    print(f"{'总计':<25} {total_time:>8}ms")
    print(f"\n💡 首次运行会触发 jieba 分词加载，后续会快很多")

    # 并行模式对比
    print(f"\n{'='*60}")
    print("🔄 并行模式测试...")
    parallel_script = SCRIPTS_DIR / "run_parallel.py"
    if parallel_script.exists():
        start = time.perf_counter()
        result = subprocess.run(
            [sys.executable, str(parallel_script), novel_dir, "--workers", "4"],
            capture_output=True, text=True, timeout=120
        )
        parallel_time = round((time.perf_counter() - start) * 1000)
        speedup = round(total_time / max(parallel_time, 1), 1)
        print(f"  串行: {total_time}ms")
        print(f"  并行(4线程): {parallel_time}ms")
        print(f"  加速比: {speedup}x")


if __name__ == "__main__":
    main()


path: scripts/changelog.py
content: #!/usr/bin/env python3
"""Novel Writer 版本管理 & changelog"""

from pathlib import Path
from datetime import datetime

CHANGELOG_PATH = Path(__file__).parent.parent / "CHANGELOG.md"

ENTRIES = [
    ("2.1.0", "2026-04-11", "测试迁移到标准路径 tests/，修复4个脚本路径测试，SKILL.md补全参数速查"),
    ("2.0.0", "2026-04-11", "企业级重构：SKILL.md通用化，统一CLI，移除用户数据，中英README+LICENSE"),
    ("1.0.0", "2026-04-10", "初始版本：13个检测脚本，283个测试，20篇参考文档"),
]

def show():
    for ver, date, desc in ENTRIES:
        print(f"  {ver} ({date}) — {desc}")

def add(version, description):
    date = datetime.now().strftime("%Y-%m-%d")
    ENTRIES.insert(0, (version, date, description))
    _write_changelog()
    print(f"✅ 已添加 {version}: {description}")

def _write_changelog():
    lines = ["# Changelog\n"]
    for ver, date, desc in ENTRIES:
        lines.append(f"## {ver} ({date})\n- {desc}\n")
    CHANGELOG_PATH.write_text("\n".join(lines), encoding="utf-8")

def main():
    import sys
    if len(sys.argv) < 2:
        print("📖 Novel Writer 版本管理")
        print("用法: changelog [show|add <版本> <描述>]")
        show()
        sys.exit(0)

    cmd = sys.argv[1]
    if cmd == "show":
        show()
    elif cmd == "add" and len(sys.argv) >= 4:
        add(sys.argv[2], " ".join(sys.argv[3:]))
    else:
        print(f"❌ 未知命令: {cmd}")

if __name__ == "__main__":
    main()


path: scripts/chapter_hook_check.py
content: #!/usr/bin/env python3
"""
章末钩子检测脚本 v2.2.1
扫描正文目录，检测每章最后200字是否有有效钩子，评估钩子强度和类型

用法：
  python3 chapter_hook_check.py --novel-dir <正文目录>
  python3 chapter_hook_check.py --novel-dir <正文目录> --suggest
  python3 chapter_hook_check.py --novel-dir <正文目录> --format json

检查项：
  1. 钩子类型识别（悬念/情感/反转/危机/秘密/决定/无钩子）
  2. 钩子强度评分（0-10）
  3. 连续弱结尾检测（节奏疲劳）
  4. --suggest：改善建议
"""

import os
import re
import sys
import json
import argparse
from pathlib import Path
from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple

# ============================================================
# 常量定义
# ============================================================

VERSION = "v2.2.1"

# 钩子类型关键词映射
HOOK_PATTERNS: Dict[str, Dict[str, Any]] = {
    "悬念钩子": {
        "keywords": ["？", "?", "什么", "为什么", "怎么回事", "到底", "难道", "究竟",
                      "谁", "怎么会", "怎么可能", "不知道", "猜不透", "看不透",
                      "答案", "秘密", "谜", "真相", "背后"],
        "patterns": [
            r"[^。！？\n]*[？?][^。！？\n]*$",  # 以问句结尾
            r"到底[^。！？]*[？?]",
            r"难道[^。！？]*[？?]",
            r"他(还)?不知道",
            r"一切才刚刚开始",
        ],
        "base_score": 6,
    },
    "情感钩子": {
        "keywords": ["泪", "哭", "笑", "颤抖", "心碎", "崩溃", "窒息", "绝望",
                      "痛", "心酸", "红了眼眶", "沉默", "良久", "叹息", "苦笑",
                      "后悔", "愧疚", "感动", "温暖", "冰冷", "心如刀割",
                      "再也", "再也回不去", "再也见不到"],
        "patterns": [
            r"(泪水|眼泪|红了眼眶|心碎|崩溃|心如刀割)",
            r"(沉默|良久)([^。！？]*$)",
            r"(苦笑|叹息)",
        ],
        "base_score": 5,
    },
    "反转钩子": {
        "keywords": ["但", "可是", "然而", "竟然", "居然", "没想到", "出乎意料",
                      "不对", "等等", "不可能", "怎么会这样", "完全没想到",
                      "大吃一惊", "震惊", "瞪大眼睛", "愣住", "傻了"],
        "patterns": [
            r"(但|可是|然而|不过)[^。！？]*(竟然|居然|没想到|不可能)",
            r"(不对|等等|不可能)[^。！？]*$",
            r"完全[^。！？]*意外",
        ],
        "base_score": 7,
    },
    "危机钩子": {
        "keywords": ["危险", "死", "杀", "塌", "炸", "碎", "断", "裂", "崩",
                      "逼", "逼到", "绝路", "走投无路", "倒计时", "来不及了",
                      "来不及", "完了", "糟了", "出事了", "出大问题了",
                      "黑暗", "阴影", "逼近", "靠近", "威胁"],
        "patterns": [
            r"(来不及|完了|糟了|出事了)",
            r"(死|杀|危险|威胁)[^。！？]*$",
            r"(逼近|靠近|逼近)",
        ],
        "base_score": 7,
    },
    "秘密钩子": {
        "keywords": ["秘密", "隐藏", "背后", "另有隐情", "真相", "不为人知",
                      "隐瞒", "掩盖", "从未", "一直", "其实", "原来",
                      "不简单", "没那么简单", "暗藏", "另有"],
        "patterns": [
            r"(秘密|隐情|真相|隐藏)[^。！？]*$",
            r"(其实|原来|一直)[^。！？]*(瞒|隐瞒|没说|不说)",
            r"(不简单|没那么简单)",
        ],
        "base_score": 6,
    },
    "决定钩子": {
        "keywords": ["必须", "一定", "决定", "下定决心", "非...不可", "不能",
                      "该", "要么", "只有", "唯一的", "最后的机会",
                      "是时候", "该做了", "不能再拖", "别无选择"],
        "patterns": [
            r"(必须|一定|决定|下定决心)[^。！？]*$",
            r"(是时候|别无选择|只有[^。！？]*才)",
            r"(要么[^。！？]*要么)",
        ],
        "base_score": 5,
    },
}

# 额外加分的句式模式
STRONG_ENDING_PATTERNS = [
    (r"[^。！？]*[——…]+[^。！？]*$", 2, "破折号/省略号结尾，留白感强"),
    (r"[^。！？]*[？?]\s*$", 1, "问句结尾，引发好奇"),
    (r"(突然|忽然|猛地|刹那|一瞬间)", 1, "突发感"),
    (r"(他|她|它|这|那)(还|却|竟|才|就)(没|不|要|会|能)", 1, "转折语气"),
    (r"[^\u4e00-\u9fff\w]\s*$", -1, "非中文字符结尾（可能是格式问题）"),
]

# ============================================================
# 工具函数
# ============================================================

def read_file(path: str) -> str:
    """读取文件内容"""
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        print(f"[警告] 无法读取 {path}: {e}")
        return ""


def list_chapters(dir_path: str) -> List[Tuple[int, str, str]]:
    """列出章节文件，返回 [(章节号, 路径, 文件名)]"""
    if not os.path.isdir(dir_path):
        print(f"[错误] 正文目录不存在: {dir_path}")
        return []
    chapters = []
    for f in sorted(os.listdir(dir_path)):
        if f.endswith('.md') or f.endswith('.txt'):
            match = re.search(r'第(\d+)章', f)
            if match:
                num = int(match.group(1))
                chapters.append((num, os.path.join(dir_path, f), f))
    chapters.sort(key=lambda x: x[0])
    return chapters


def extract_ending(content: str, char_count: int = 200) -> str:
    """提取正文最后N个字符"""
    # 去掉标题行
    lines = content.strip().split('\n')
    body_lines = [l for l in lines if not l.strip().startswith('#')]
    body = '\n'.join(body_lines)
    # 只取中文字符范围
    return body[-char_count:] if len(body) > char_count else body


# ============================================================
# 钩子检测核心逻辑
# ============================================================

def detect_hook_type(ending: str) -> Tuple[str, float, List[str]]:
    """
    检测章末钩子类型和强度。
    
    Args:
        ending: 章节最后200字文本
        
    Returns:
        (钩子类型, 强度评分0-10, 匹配到的关键词列表)
    """
    if not ending or len(ending.strip()) < 10:
        return ("无钩子", 1.0, [])
    
    scores: Dict[str, float] = {}
    matched_details: Dict[str, List[str]] = {}
    
    for hook_type, config in HOOK_PATTERNS.items():
        score = 0.0
        matched = []
        
        # 关键词匹配
        for kw in config["keywords"]:
            count = ending.count(kw)
            if count > 0:
                score += min(count * 0.8, 3.0)  # 每个关键词+0.8，上限3分
                matched.append(kw)
        
        # 正则模式匹配
        for pattern in config["patterns"]:
            if re.search(pattern, ending):
                score += 1.5
                matched.append(f"模式:{pattern[:20]}")
        
        scores[hook_type] = score + config["base_score"] if score > 0 else 0
        matched_details[hook_type] = matched
    
    # 附加句式加分
    bonus = 0.0
    for pattern, add_score, _ in STRONG_ENDING_PATTERNS:
        if re.search(pattern, ending):
            bonus += add_score
    
    # 找得分最高的类型
    best_type = "无钩子"
    best_score = 2.0  # 无钩子基础分
    best_matched = []
    
    for hook_type, score in scores.items():
        final_score = score + bonus
        if final_score > best_score:
            best_score = final_score
            best_type = hook_type
            best_matched = matched_details[hook_type]
    
    # 限制在0-10范围
    best_score = max(0.0, min(10.0, best_score))
    
    return (best_type, round(best_score, 1), best_matched)


def check_consecutive_weak(chapter_results: List[Dict[str, Any]], threshold: float = 4.0) -> List[Dict[str, Any]]:
    """
    检测连续弱结尾（节奏疲劳）。
    
    Args:
        chapter_results: 每章的检测结果列表
        threshold: 弱钩子阈值
        
    Returns:
        节奏疲劳警告列表
    """
    warnings = []
    streak = 0
    streak_start = None
    
    for i, r in enumerate(chapter_results):
        if r["score"] < threshold:
            if streak == 0:
                streak_start = r["chapter"]
            streak += 1
        else:
            if streak >= 3:
                end_chapter = chapter_results[i - 1]["chapter"]
                warnings.append({
                    "type": "节奏疲劳",
                    "severity": "high" if streak >= 5 else "medium",
                    "detail": f"第{streak_start}-{end_chapter}章连续{streak}章钩子评分低于{threshold}，"
                              f"读者可能失去翻页动力",
                    "suggestion": _fatigue_suggestion(streak),
                })
            streak = 0
            streak_start = None
    
    # 尾部处理
    if streak >= 3 and chapter_results:
        end_chapter = chapter_results[-1]["chapter"]
        warnings.append({
            "type": "节奏疲劳",
            "severity": "high" if streak >= 5 else "medium",
            "detail": f"第{streak_start}-{end_chapter}章连续{streak}章钩子评分低于{threshold}，"
                      f"读者可能失去翻页动力",
            "suggestion": _fatigue_suggestion(streak),
        })
    
    return warnings


def _fatigue_suggestion(streak: int) -> str:
    """生成节奏疲劳的改善建议"""
    suggestions = [
        "在平淡结尾处插入突发转折（电话响起、门外脚步、意外来客）",
        "添加悬念元素（未回答的问题、神秘暗示、角色反常行为）",
        "制造情绪冲击（意外告白、真相揭露、生死危机）",
        "以角色内心独白结尾，暗示即将做出的重大决定",
    ]
    return "建议穿插使用以下手法打破平淡：" + "；".join(suggestions[:min(3, streak)])


def generate_hook_suggestion(ending: str, hook_type: str, score: float) -> Optional[str]:
    """
    生成章末钩子改善建议。
    
    Args:
        ending: 章节最后200字
        hook_type: 检测到的钩子类型
        score: 钩子强度评分
        
    Returns:
        改善建议字符串，评分为7+时返回None
    """
    if score >= 7:
        return None
    
    suggestions_map = {
        "无钩子": [
            "结尾过于平淡。尝试以问句、省略号、突发动作、角色内心独白结尾",
            "示例：他转身要走，却发现门把手上沾着血。",
            "示例：手机亮了。一条陌生号码的消息：我知道你做了什么。",
        ],
        "悬念钩子": [
            "悬念已建立但强度不够。尝试更具体化——把模糊的疑问变成迫在眉睫的威胁",
            "或用角色视角强化紧迫感：他意识到自己只剩24小时了。",
        ],
        "情感钩子": [
            "情感冲击可以更强。用具体行为代替抽象描述",
            "示例：她把照片翻过去，发现背面有行字。字迹是她的，但她完全不记得写过。",
        ],
        "反转钩子": [
            "反转信号弱。尝试在最后一句话加入与前面所有信息矛盾的新信息",
            "示例：一切都解释得通了——除了那枚不属于任何人的戒指。",
        ],
        "危机钩子": [
            "危机感不够紧迫。加倒计时或身体反应（冷汗、心跳加速）",
            "示例：倒计时还剩三分钟，而他还不知道炸弹在哪。",
        ],
        "秘密钩子": [
            "秘密暗示太模糊。给读者一个更具体的线索片段",
            "示例：文件最后一页被人撕掉了。撕得很急，纸边还是锯齿形的。",
        ],
        "决定钩子": [
            "决定感不够强烈。强化两难困境的矛盾性",
            "示例：左边是来时的路，右边是他发誓再也不会踏足的地方。他迈出了步子。",
        ],
    }
    
    lines = suggestions_map.get(hook_type, ["尝试在结尾添加更强的悬念或情感冲击"])
    return "\n".join(lines)


# ============================================================
# 主流程
# ============================================================

def check_single_chapter(chapter_num: int, content: str, char_count: int = 200) -> Dict[str, Any]:
    """
    检查单章的章末钩子。
    
    Args:
        chapter_num: 章节号
        content: 章节全文
        char_count: 提取最后N个字符
        
    Returns:
        检测结果字典
    """
    ending = extract_ending(content, char_count)
    hook_type, score, matched = detect_hook_type(ending)
    
    # 确定严重程度
    if score < 4:
        severity = "high"
    elif score < 6:
        severity = "medium"
    else:
        severity = "low"
    
    return {
        "chapter": chapter_num,
        "hook_type": hook_type,
        "score": score,
        "severity": severity,
        "ending_preview": ending[-80:] if len(ending) > 80 else ending,
        "matched_keywords": matched,
    }


def run_check(novel_dir: str, threshold: float = 4.0, suggest: bool = False) -> Dict[str, Any]:
    """
    执行章末钩子检测。
    
    Args:
        novel_dir: 正文目录路径
        threshold: 弱钩子阈值
        suggest: 是否输出改善建议
        
    Returns:
        检测结果汇总
    """
    print("=" * 60)
    print(f"🪝 章末钩子检测 {VERSION}")
    print("=" * 60)
    
    chapters = list_chapters(novel_dir)
    if not chapters:
        print("[错误] 未找到章节文件")
        return {"error": "未找到章节文件", "chapters": [], "warnings": []}
    
    print(f"\n📖 找到 {len(chapters)} 个章节（第{chapters[0][0]}章 ~ 第{chapters[-1][0]}章）")
    
    # 逐章检测
    chapter_results = []
    for num, path, fname in chapters:
        content = read_file(path)
        result = check_single_chapter(num, content)
        chapter_results.append(result)
    
    # 连续弱结尾检测
    fatigue_warnings = check_consecutive_weak(chapter_results, threshold)
    
    # 统计
    type_dist = defaultdict(int)
    score_sum = 0.0
    weak_count = 0
    for r in chapter_results:
        type_dist[r["hook_type"]] += 1
        score_sum += r["score"]
        if r["score"] < threshold:
            weak_count += 1
    
    avg_score = score_sum / len(chapter_results) if chapter_results else 0
    
    # 输出结果
    print(f"\n{'='*60}")
    print(f"📊 检测结果")
    print(f"{'='*60}")
    print(f"\n总章数: {len(chapters)} | 平均钩子评分: {avg_score:.1f} | 弱结尾(<{threshold}分): {weak_count}章")
    print(f"类型分布: {', '.join(f'{t}×{c}' for t, c in sorted(type_dist.items(), key=lambda x: -x[1]))}")
    
    # 逐章输出
    print(f"\n{'章节':>6} | {'类型':<8} | {'评分':>4} | 预览")
    print("-" * 70)
    for r in chapter_results:
        emoji = "🔴" if r["score"] < threshold else ("🟡" if r["score"] < 6 else "🟢")
        preview = r["ending_preview"].replace("\n", " ")[:40]
        print(f"第{r['chapter']:>3}章 | {r['hook_type']:<8} | {emoji}{r['score']:>4.1f} | {preview}...")
        if suggest and r["score"] < 7:
            suggestion = generate_hook_suggestion(r["ending_preview"], r["hook_type"], r["score"])
            if suggestion:
                for line in suggestion.split("\n"):
                    print(f"       💡 {line}")
    
    # 节奏疲劳警告
    if fatigue_warnings:
        print(f"\n⚠️  节奏疲劳警告：")
        for w in fatigue_warnings:
            emoji = "🔴" if w["severity"] == "high" else "🟡"
            print(f"  {emoji} {w['detail']}")
            if suggest:
                print(f"     💡 {w['suggestion']}")
    else:
        print(f"\n✅ 未检测到连续弱结尾")
    
    result = {
        "version": VERSION,
        "total_chapters": len(chapters),
        "average_score": round(avg_score, 1),
        "weak_count": weak_count,
        "type_distribution": dict(type_dist),
        "chapters": chapter_results,
        "fatigue_warnings": fatigue_warnings,
    }
    
    return result


def main():
    parser = argparse.ArgumentParser(
        description="章末钩子检测脚本",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--novel-dir", required=True, help="正文目录路径")
    parser.add_argument("--threshold", type=float, default=4.0, help="弱钩子评分阈值（默认4）")
    parser.add_argument("--suggest", action="store_true", help="输出改善建议")
    parser.add_argument("--format", choices=["text", "json"], default="text", help="输出格式")
    parser.add_argument("--version", action="version", version=f"chapter_hook_check {VERSION}")
    
    args = parser.parse_args()
    
    result = run_check(args.novel_dir, args.threshold, args.suggest)
    
    if args.format == "json":
        # JSON输出时移除内部字段
        print("\n" + json.dumps(result, ensure_ascii=False, indent=2))
    elif "error" not in result:
        # 保存JSON报告
        report_dir = os.path.dirname(args.novel_dir.rstrip('/'))
        report_path = os.path.join(report_dir, "chapter_hook_report.json")
        try:
            with open(report_path, 'w', encoding='utf-8') as f:
                json.dump(result, f, ensure_ascii=False, indent=2)
            print(f"\n📄 报告已保存至: {report_path}")
        except Exception as e:
            print(f"\n[警告] 保存报告失败: {e}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python3 chapter_hook_check.py --novel-dir <正文目录> [--threshold 4] [--suggest] [--format text|json]")
        sys.exit(1)
    main()


path: scripts/chapter_title_check.py
content: #!/usr/bin/env python3
"""
章节标题质量检查脚本 v2.2.1
扫描正文目录的文件名，检测章节标题质量

用法：
  python3 chapter_title_check.py --novel-dir <正文目录>

检查项：
  1. 标题长度（最佳8-20字）
  2. 标题吸引力（平淡型/标题党/信息过量）
  3. 标题格式一致性（第X章格式、数字统一）
  4. 钩子检测（悬念词）
"""

import os
import re
import json
import argparse
from pathlib import Path
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple

VERSION = 'v2.2.1'

# ============================================================
# 常量定义
# ============================================================

# 平淡型标题关键词
FLAT_PATTERNS = [
    r'^第\d+章$',           # 只有"第X章"无标题
    r'^今天$', r'^昨天$', r'^明天$',
    r'^早上$', r'^中午$', r'^下午$', r'^晚上$', r'^深夜$',
    r'^第二天$', r'^第三天$',
    r'^开始$', r'^结束$', r'^结局$', r'^尾声$',
    r'^新的.*$', r'^继续$',
    r'^第\d+天$',            # "第X天"直接做标题
]

# 标题党关键词（过度使用扣分）
CLICKBAIT_WORDS = ['震惊', '竟然', '意想不到', '万万没想到', '不敢相信',
                   '所有人都', '99%的人', '绝对', '史上最']

# 悬念/钩子词
HOOK_WORDS = ['秘密', '真相', '阴谋', '命运', '背叛', '陷阱', '危机',
              '抉择', '代价', '代价', '预言', '诅咒', '禁忌', '谜团',
              '隐藏', '线索', '反转', '悬念', '暗流', '杀机', '伏笔',
              '意外', '变故', '风暴', '深渊', '迷雾', '陷阱', '暗算']

# 透露结局/信息过多的关键词
SPOILER_WORDS = ['死了', '结局', '最终', '终于', '成功', '失败',
                 '胜利', '沦陷', '覆灭', '身亡', '复活']


# ============================================================
# 工具函数
# ============================================================

def list_chapter_files(novel_dir: str) -> List[Tuple[int, str]]:
    """列出章节文件，返回 [(章节号, 文件名), ...]
    
    只处理 .md 和 .txt 文件，按章节号排序。
    """
    if not os.path.isdir(novel_dir):
        print(f"[错误] 目录不存在: {novel_dir}")
        return []
    
    chapters = []
    for fname in sorted(os.listdir(novel_dir)):
        if not (fname.endswith('.md') or fname.endswith('.txt')):
            continue
        match = re.search(r'第(\d+)章', fname)
        if match:
            num = int(match.group(1))
            chapters.append((num, fname))
    chapters.sort(key=lambda x: x[0])
    return chapters


def extract_title(fname: str) -> str:
    """从文件名提取标题部分（去掉'第X章'前缀和扩展名）"""
    # 去掉扩展名
    name = os.path.splitext(fname)[0]
    # 去掉"第X章"前缀及分隔符
    title = re.sub(r'^第\d+章[\s_\-—]*', '', name).strip()
    return title


def title_char_count(title: str) -> int:
    """统计标题中的中文字符数"""
    return len(re.findall(r'[\u4e00-\u9fff]', title))


# ============================================================
# 检查函数
# ============================================================

def check_title_length(title: str, num: int, min_len: int = 5, max_len: int = 25) -> List[Dict[str, Any]]:
    """检查标题长度"""
    issues = []
    char_count = title_char_count(title)
    
    if not title:
        issues.append({
            'type': '无标题',
            'severity': 'high',
            'chapter': num,
            'detail': f'第{num}章没有标题（只有章节号）',
            'suggestion': '添加有信息量的标题，8-20字最佳'
        })
    elif char_count < min_len:
        issues.append({
            'type': '标题过短',
            'severity': 'medium',
            'chapter': num,
            'detail': f'第{num}章标题「{title}」仅{char_count}字，可能太笼统',
            'suggestion': '补充信息量，8-20字最佳'
        })
    elif char_count > max_len:
        issues.append({
            'type': '标题过长',
            'severity': 'medium',
            'chapter': num,
            'detail': f'第{num}章标题「{title}」有{char_count}字，可能太啰嗦',
            'suggestion': '精简标题，8-20字最佳'
        })
    
    return issues


def check_title_attractiveness(title: str, num: int) -> List[Dict[str, Any]]:
    """检查标题吸引力"""
    issues = []
    if not title:
        return issues
    
    # 平淡型标题
    for pattern in FLAT_PATTERNS:
        if re.match(pattern, title):
            issues.append({
                'type': '平淡标题',
                'severity': 'high',
                'chapter': num,
                'detail': f'第{num}章标题「{title}」过于平淡，缺乏吸引力',
                'suggestion': '用具体事件或悬念替代时间/状态描述'
            })
            break
    
    # 标题党检测
    clickbait_hits = [w for w in CLICKBAIT_WORDS if w in title]
    if len(clickbait_hits) >= 2:
        issues.append({
            'type': '标题党',
            'severity': 'medium',
            'chapter': num,
            'detail': f'第{num}章标题「{title}」含{len(clickbait_hits)}个标题党词：{"、".join(clickbait_hits)}',
            'suggestion': '适度悬念可以，过度标题党会降低信任'
        })
    
    # 信息过量（透露结局）
    spoiler_hits = [w for w in SPOILER_WORDS if w in title]
    if spoiler_hits:
        issues.append({
            'type': '信息过量',
            'severity': 'medium',
            'chapter': num,
            'detail': f'第{num}章标题「{title}」可能透露剧情：{"、".join(spoiler_hits)}',
            'suggestion': '标题应引发好奇而非给出答案'
        })
    
    return issues


def check_format_consistency(filenames: List[Tuple[int, str]]) -> List[Dict[str, Any]]:
    """检查标题格式一致性"""
    issues = []
    if len(filenames) < 2:
        return issues
    
    formats = []  # 收集每章的格式特征
    for num, fname in filenames:
        fmt = {
            'num': num,
            'fname': fname,
            'has_prefix': bool(re.match(r'^第\d+章', fname)),
            'separator': '',  # 分隔符类型
            'number_padded': False,  # 数字是否补零
        }
        
        # 检测分隔符
        sep_match = re.match(r'^第\d+章([_\s\-—])', fname)
        if sep_match:
            fmt['separator'] = sep_match.group(1)
        
        # 检测数字补零
        num_match = re.match(r'^第(0*)(\d+)章', fname)
        if num_match and num_match.group(1):
            fmt['number_padded'] = True
        
        formats.append(fmt)
    
    # 检查分隔符一致性
    separators = [f['separator'] for f in formats if f['separator']]
    if separators:
        sep_counter = Counter(separators)
        main_sep = sep_counter.most_common(1)[0][0]
        inconsistent = [f for f in formats if f['separator'] and f['separator'] != main_sep]
        if inconsistent:
            for f in inconsistent:
                issues.append({
                    'type': '格式不统一',
                    'severity': 'low',
                    'chapter': f['num'],
                    'detail': f'第{f["num"]}章文件名分隔符为「{f["separator"]}」，与主流「{main_sep}」不一致',
                    'suggestion': f'统一使用「{main_sep}」作为分隔符'
                })
    
    # 检查数字补零一致性
    padded_count = sum(1 for f in formats if f['number_padded'])
    if 0 < padded_count < len(formats):
        issues.append({
            'type': '编号格式不统一',
            'severity': 'low',
            'chapter': 0,
            'detail': f'部分章节编号补零（{padded_count}个），部分未补零（{len(formats)-padded_count}个）',
            'suggestion': '统一编号格式，建议三位数补零（第001章）'
        })
    
    # 检查中英文混搭
    for num, fname in filenames:
        # 检查文件名中是否有不合理的英文（排除常见扩展名前的内容）
        name_part = os.path.splitext(fname)[0]
        english_parts = re.findall(r'[a-zA-Z]+', name_part)
        # 过滤掉合理的英文（如draft、WIP等标记）
        allowed = {'draft', 'wip', 'DRAFT', 'WIP', 'v'}
        suspicious = [e for e in english_parts if e not in allowed and len(e) > 1]
        if suspicious:
            issues.append({
                'type': '中英混搭',
                'severity': 'low',
                'chapter': num,
                'detail': f'第{num}章文件名含英文：{"、".join(suspicious)}',
                'suggestion': '文件名建议纯中文+数字'
            })
    
    return issues


def check_hooks(title: str, num: int) -> List[Dict[str, Any]]:
    """检查标题是否带有钩子/悬念"""
    issues = []
    if not title:
        return issues
    
    hook_hits = [w for w in HOOK_WORDS if w in title]
    if not hook_hits:
        # 没有任何悬念词，标记为缺少钩子（但不一定有问题）
        # 仅当标题较长（>6字）且无钩子时才提示
        if title_char_count(title) > 6:
            issues.append({
                'type': '缺少钩子',
                'severity': 'low',
                'chapter': num,
                'detail': f'第{num}章标题「{title}」无悬念词，可能难以引发读者好奇',
                'suggestion': f'考虑加入悬念元素，如：{", ".join(HOOK_WORDS[:5])}等'
            })
    
    return issues


# ============================================================
# 生成建议
# ============================================================

def generate_suggestion(title: str, num: int, issues: List[Dict[str, Any]]) -> str:
    """根据问题生成综合修改建议"""
    if not issues:
        return f'第{num}章标题「{title}」质量良好 ✓'
    
    suggestions = []
    for issue in issues:
        suggestions.append(f'  · [{issue["type"]}] {issue.get("suggestion", "")}')
    
    return f'第{num}章标题「{title}」\n' + '\n'.join(suggestions)


# ============================================================
# 主流程
# ============================================================

def run_check(novel_dir: str, min_len: int = 5, max_len: int = 25,
              suggest: bool = False, output_format: str = 'text') -> Dict[str, Any]:
    """执行章节标题质量检查
    
    Args:
        novel_dir: 章节目录路径
        min_len: 最短标题字数
        max_len: 最长标题字数
        suggest: 是否输出修改建议
        output_format: 输出格式 'text' 或 'json'
    
    Returns:
        检查结果字典
    """
    chapters = list_chapter_files(novel_dir)
    if not chapters:
        return {'error': f'未找到章节文件: {novel_dir}', 'issues': [], 'stats': {}}
    
    all_issues = []
    suggestions = []
    
    # 每章检查
    for num, fname in chapters:
        title = extract_title(fname)
        chapter_issues = []
        
        # 1. 长度检查
        chapter_issues.extend(check_title_length(title, num, min_len, max_len))
        
        # 2. 吸引力检查
        chapter_issues.extend(check_title_attractiveness(title, num))
        
        # 3. 钩子检查
        chapter_issues.extend(check_hooks(title, num))
        
        all_issues.extend(chapter_issues)
        
        if suggest:
            suggestions.append(generate_suggestion(title, num, chapter_issues))
    
    # 格式一致性检查（全局）
    all_issues.extend(check_format_consistency(chapters))
    
    # 统计
    total = len(chapters)
    no_title_count = sum(1 for _, f in chapters if not extract_title(f))
    has_hook_count = sum(1 for _, f in chapters 
                        if any(w in extract_title(f) for w in HOOK_WORDS))
    flat_count = sum(1 for _, f in chapters 
                    if any(re.match(p, extract_title(f)) for p in FLAT_PATTERNS))
    
    # 按严重程度统计
    severity_counts = Counter(i.get('severity', 'medium') for i in all_issues)
    
    stats = {
        'total_chapters': total,
        'no_title': no_title_count,
        'has_hook': has_hook_count,
        'flat_title': flat_count,
        'hook_rate': f'{has_hook_count/total:.0%}' if total else '0%',
        'total_issues': len(all_issues),
        'high': severity_counts.get('high', 0),
        'medium': severity_counts.get('medium', 0),
        'low': severity_counts.get('low', 0),
    }
    
    result = {'issues': all_issues, 'stats': stats}
    
    # 输出
    if output_format == 'json':
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        _print_text_result(result, suggest, suggestions)
    
    return result


def _print_text_result(result: Dict[str, Any], suggest: bool, suggestions: List[str]) -> None:
    """文本格式输出"""
    stats = result['stats']
    issues = result['issues']
    
    print('=' * 60)
    print(f'📖 章节标题质量检查 {VERSION}')
    print('=' * 60)
    print(f'\n📊 统计：共{stats["total_chapters"]}章')
    print(f'   无标题：{stats["no_title"]}章')
    print(f'   平淡标题：{stats["flat_title"]}章')
    print(f'   含悬念词：{stats["has_hook"]}章（{stats["hook_rate"]}）')
    print(f'   问题总数：{stats["total_issues"]}（🔴{stats["high"]} 🟡{stats["medium"]} 🟢{stats["low"]}）')
    
    if not issues:
        print('\n✅ 未发现标题质量问题')
    else:
        # 按严重程度输出
        for sev, label, emoji in [('high', '严重', '🔴'), ('medium', '中等', '🟡'), ('low', '轻微', '🟢')]:
            sev_issues = [i for i in issues if i.get('severity') == sev]
            if not sev_issues:
                continue
            print(f'\n### {emoji} {label}问题（{len(sev_issues)}个）')
            for issue in sev_issues:
                ch = issue.get('chapter', 0)
                ch_label = f'第{ch}章' if ch > 0 else '全局'
                print(f'  • [{issue["type"]}] {ch_label}：{issue["detail"]}')
                if issue.get('suggestion'):
                    print(f'    💡 {issue["suggestion"]}')
    
    if suggest and suggestions:
        print(f'\n{"="*60}')
        print('📝 修改建议')
        print('=' * 60)
        for s in suggestions:
            print(f'\n{s}')


def main():
    parser = argparse.ArgumentParser(description='章节标题质量检查')
    parser.add_argument('--novel-dir', required=True, help='章节目录路径')
    parser.add_argument('--min-len', type=int, default=5, help='最短标题字数（默认5）')
    parser.add_argument('--max-len', type=int, default=25, help='最长标题字数（默认25）')
    parser.add_argument('--suggest', action='store_true', help='输出修改建议')
    parser.add_argument('--format', choices=['text', 'json'], default='text', help='输出格式')
    parser.add_argument('--version', action='version', version=f'chapter_title_check {VERSION} (novel-writer skill)')
    args = parser.parse_args()
    
    run_check(args.novel_dir, args.min_len, args.max_len, args.suggest, args.format)


if __name__ == '__main__':
    main()


path: scripts/cli.py
content: #!/usr/bin/env python3
"""Novel Writer 统一 CLI 入口"""

import argparse
import sys
from pathlib import Path

SCRIPTS_DIR = Path(__file__).parent


def run_script(name: str, args: list[str]):
    script = SCRIPTS_DIR / f"{name}.py"
    if not script.exists():
        print(f"❌ 未知命令: {name}")
        available = sorted(p.stem for p in SCRIPTS_DIR.glob("*.py") if p.stem not in ("cli", "__init__"))
        print(f"   可用命令: {', '.join(available)}")
        sys.exit(1)
    import subprocess
    result = subprocess.run([sys.executable, str(script)] + args)
    sys.exit(result.returncode)


def main():
    p = argparse.ArgumentParser(
        prog="novel-writer",
        description="📖 长篇网络小说创作引擎 — 企业级 CLI",
    )
    p.add_argument("command", help="子命令")
    p.add_argument("args", nargs="*", help="子命令参数")

    aliases = {
        "check": "consistency_check",
        "drift": "outline_drift",
        "rhythm": "rhythm_check",
        "dialogue": "dialogue_tag_check",
        "hook": "chapter_hook_check",
        "title": "chapter_title_check",
        "opening": "opening_score",
        "paragraph": "paragraph_check",
        "foreshadow": "foreshadow_scan",
        "tension": "tension_forecast",
        "repeat": "repetition_check",
        "all": "run_all_checks",
        "config": "config_manager",
        "changelog": "changelog",
        "backup": "backup",
        "parallel": "run_parallel",
        "semantic": "semantic_check",
        "diff": "diff_check",
        "report": "html_report",
        "bench": "benchmark",
    }

    args = p.parse_args()
    cmd = aliases.get(args.command, args.command)
    run_script(cmd, args.args)


if __name__ == "__main__":
    main()


path: scripts/config_manager.py
content: #!/usr/bin/env python3
"""Novel Writer 配置管理"""

import json
from pathlib import Path

CONFIG_PATH = Path(__file__).parent.parent / "novel-writer.json"

DEFAULT_CONFIG = {
    "version": "2.1.0",
    "defaults": {
        "max_chapter_words": 3000,
        "min_chapter_words": 2000,
        "hook_interval_words": 500,
        "max_paragraph_words": 200,
        "missing_character_gap": 5,
        "foreshadow_warning_chapters": [20, 50, 100],
    },
    "novels": {},
    "active_novel": None,
}

def load_config():
    if CONFIG_PATH.exists():
        with open(CONFIG_PATH, "r", encoding="utf-8") as f:
            cfg = json.load(f)
        # 合并缺失的默认值
        for k, v in DEFAULT_CONFIG.items():
            if k not in cfg:
                cfg[k] = v
        return cfg
    return DEFAULT_CONFIG.copy()

def save_config(cfg):
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    with open(CONFIG_PATH, "w", encoding="utf-8") as f:
        json.dump(cfg, f, ensure_ascii=False, indent=2)

def register_novel(name, path):
    cfg = load_config()
    p = str(Path(path).resolve())
    if name in cfg["novels"] and cfg["novels"][name] != p:
        print(f"⚠️  '{name}' 已存在（路径: {cfg['novels'][name]}），已更新")
    cfg["novels"][name] = p
    cfg["active_novel"] = name
    save_config(cfg)
    print(f"✅ 已注册小说: {name} → {p}")

def list_novels():
    cfg = load_config()
    if not cfg["novels"]:
        print("📦 暂无注册小说。用 register <名称> <路径> 注册。")
        return
    print(f"📚 已注册小说 ({len(cfg['novels'])})\n")
    for name, path in cfg["novels"].items():
        marker = " ← 当前" if name == cfg["active_novel"] else ""
        print(f"  • {name}{marker}")
        print(f"    {path}")

def set_active(name):
    cfg = load_config()
    if name not in cfg["novels"]:
        print(f"❌ 未找到小说: {name}")
        return
    cfg["active_novel"] = name
    save_config(cfg)
    print(f"✅ 当前小说: {name}")

def get_active():
    cfg = load_config()
    name = cfg.get("active_novel")
    if not name or name not in cfg["novels"]:
        return None, None
    return name, Path(cfg["novels"][name])

def main():
    import sys
    if len(sys.argv) < 2:
        print("📖 novel-writer 配置管理")
        print("用法: config <register|list|set|show>")
        sys.exit(0)

    cmd = sys.argv[1]
    if cmd == "register" and len(sys.argv) >= 4:
        register_novel(sys.argv[2], sys.argv[3])
    elif cmd == "list":
        list_novels()
    elif cmd == "set" and len(sys.argv) >= 3:
        set_active(sys.argv[2])
    elif cmd == "show":
        cfg = load_config()
        print(json.dumps(cfg, ensure_ascii=False, indent=2))
    else:
        print(f"❌ 未知命令: {cmd}")
        sys.exit(1)

if __name__ == "__main__":
    main()


path: scripts/diff_check.py
content: #!/usr/bin/env python3
"""章节差异检测 - 对比修改前后的章节，只显示差异和新增问题

用法:
    diff_check <旧目录> <新目录> [--outline <大纲>] [--dict <设定词典>]
"""

import difflib
import json
import os
import re
import sys
from pathlib import Path
from typing import List, Tuple, Optional

SCRIPTS_DIR = Path(__file__).parent


def _read(path: str) -> str:
    with open(path, "r", encoding="utf-8") as f:
        return f.read()


def _write(path, content):
    Path(path).parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)


def pair_chapters(old_dir: str, new_dir: str) -> List[Tuple[str, str, int, Optional[str]]]:
    """配对两个目录的章节文件，返回 [(old_path, new_path, 章号, 标题), ...]"""
    old_files = {}
    new_files = {}

    for f in Path(old_dir).glob("*.md"):
        m = re.search(r"第(\d+)", f.name)
        if m:
            old_files[int(m.group(1))] = (str(f), f.name)

    for f in Path(new_dir).glob("*.md"):
        m = re.search(r"第(\d+)", f.name)
        if m:
            new_files[int(m.group(1))] = (str(f), f.name)

    all_nums = sorted(set(old_files.keys()) | set(new_files.keys()))
    pairs = []
    for num in all_nums:
        old = old_files.get(num)
        new = new_files.get(num)
        old_path = old[0] if old else None
        new_path = new[0] if new else None
        title = (new or old)[1] if (new or old) else ""
        pairs.append((old_path, new_path, num, title))

    return pairs


def text_diff(old_text: str, new_text: str) -> dict:
    """生成文本差异"""
    old_lines = old_text.splitlines(keepends=True)
    new_lines = new_text.splitlines(keepends=True)

    diff = list(difflib.unified_diff(old_lines, new_lines,
                                      fromfile="旧版", tofile="新版", n=1))

    added = sum(1 for l in diff if l.startswith("+") and not l.startswith("+++"))
    removed = sum(1 for l in diff if l.startswith("-") and not l.startswith("---"))

    return {
        "diff_lines": len(diff),
        "added_lines": added,
        "removed_lines": removed,
        "diff_text": "".join(diff),
    }


def analyze_modifications(old_text: str, new_text: str) -> List[dict]:
    """分析修改内容，找出可能引入的新问题"""
    issues = []

    diff = text_diff(old_text, new_text)
    if diff["diff_lines"] == 0:
        return issues

    # 1. 检查新增段落是否与上下文重复
    new_lines_set = set(new_text.splitlines())
    old_lines_set = set(old_text.splitlines())
    added_lines = new_lines_set - old_lines_set
    long_adds = [l for l in added_lines if len(l) >= 15]

    if long_adds:
        # 检查新增行之间是否互相重复
        from collections import Counter
        add_counts = Counter(long_adds)
        for line, count in add_counts.items():
            if count >= 2:
                issues.append({
                    "type": "新增重复",
                    "severity": "medium",
                    "detail": f"新增内容中重复出现: 「{line[:50]}…」",
                    "suggestion": "删除重复段落",
                })

    # 2. 检查新增内容的AI味
    ai_patterns = [
        r"不禁[了]?", r"缓缓[地]?", r"淡淡[地]?", r"微微[了]?",
        r"缓缓而[来]", r"不禁感慨", r"心中一[动震凛]",
        r"仿佛[被]?", r"竟然[不]?",
    ]
    for line in long_adds:
        for pat in ai_patterns:
            if re.search(pat, line):
                issues.append({
                    "type": "AI味",
                    "severity": "low",
                    "detail": f"新增内容含AI高频词: 「{line[:50]}…」",
                    "suggestion": f"替换「{pat}」相关表述",
                })
                break

    # 3. 检查新增对话标签
    added_text = "\n".join(added_lines)
    dialogue_patterns = re.findall(r'[："]\s*\w+\s*(说|道|笑|问|答|喊|叫|吼|吼)', added_text)
    if len(dialogue_patterns) > 5:
        unique_tags = set(dialogue_patterns)
        if len(unique_tags) <= 2:
            issues.append({
                "type": "对话标签单调",
                "severity": "low",
                "detail": f"新增{len(dialogue_patterns)}处对话，只用{len(unique_tags)}种标签: {unique_tags}",
                "suggestion": "丰富对话表达方式",
            })

    # 4. 检查是否引入了前后不一致（简单字面检查）
    old_names = set(re.findall(r"[\u4e00-\u9fff]{2,4}(?:说|道|想|看|走|来|去|笑|哭|叫|喊)", old_text))
    new_names = set(re.findall(r"[\u4e00-\u9fff]{2,4}(?:说|道|想|看|走|来|去|笑|哭|叫|喊)", new_text))
    removed_names = old_names - new_names
    if removed_names:
        for name in list(removed_names)[:3]:
            issues.append({
                "type": "名称变化",
                "severity": "info",
                "detail": f"旧版有「{name}」，新版消失",
                "suggestion": "确认是否为有意修改",
            })

    # 5. 字数变化过大警告
    old_len = len(old_text)
    new_len = len(new_text)
    change_pct = (new_len - old_len) / max(old_len, 1) * 100
    if abs(change_pct) > 30:
        direction = "增加" if change_pct > 0 else "减少"
        issues.append({
            "type": "字数变化",
            "severity": "info",
            "detail": f"字数{direction} {abs(change_pct):.0f}%（{old_len}→{new_len}字）",
            "suggestion": "确认修改幅度是否合理" if abs(change_pct) > 50 else "",
        })

    return issues


def generate_report(pairs: List, old_dir: str, new_dir: str, output: str = None) -> str:
    """生成完整的差异报告"""
    lines = []
    lines.append("# 章节差异检测报告\n")
    lines.append(f"旧目录: `{old_dir}`\n")
    lines.append(f"新目录: `{new_dir}`\n")

    total_issues = 0
    changed = 0
    added_chapters = 0
    removed_chapters = 0

    for old_path, new_path, num, title in pairs:
        if old_path and new_path:
            old_text = _read(old_path)
            new_text = _read(new_path)
            if old_text == new_text:
                continue

            changed += 1
            diff = text_diff(old_text, new_text)
            mods = analyze_modifications(old_text, new_text)

            lines.append(f"## 第{num:03d}章: {title}")
            lines.append(f"- 变更: +{diff['added_lines']}行 / -{diff['removed_lines']}行\n")

            if diff["diff_text"]:
                lines.append("<details><summary>查看差异</summary>\n")
                lines.append("```diff")
                lines.append(diff["diff_text"])
                lines.append("```\n")
                lines.append("</details>\n")

            if mods:
                total_issues += len(mods)
                lines.append("**发现的问题:**\n")
                for m in mods:
                    marker = "⚠️" if m["severity"] == "medium" else "💡"
                    lines.append(f"- {marker} [{m['type']}] {m['detail']}")
                    if m["suggestion"]:
                        lines.append(f"  → {m['suggestion']}")
                lines.append("")

        elif new_path and not old_path:
            added_chapters += 1
            lines.append(f"## 第{num:03d}章: {title} 🆕 新增章节\n")
        elif old_path and not new_path:
            removed_chapters += 1
            lines.append(f"## 第{num:03d}章: {title} ❌ 删除章节\n")

    # 汇总
    lines.insert(3, f"\n| 统计 | 数量 |")
    lines.insert(4, f"|------|------|")
    lines.insert(5, f"| 对比章节 | {len(pairs)} |")
    lines.insert(6, f"| 有变更 | {changed} |")
    lines.insert(7, f"| 新增章节 | {added_chapters} |")
    lines.insert(8, f"| 删除章节 | {removed_chapters} |")
    lines.insert(9, f"| 发现问题 | {total_issues} |")

    report = "\n".join(lines)

    if output:
        _write(output, report)
        print(f"✅ 报告已保存: {output}")
    else:
        print(report)

    return report


def main():
    if len(sys.argv) < 3:
        print("📖 章节差异检测")
        print("用法: diff_check <旧目录> <新目录> [--output <报告路径>]")
        print("  对比修改前后的章节，只显示差异和新增问题")
        sys.exit(0)

    old_dir = sys.argv[1]
    new_dir = sys.argv[2]
    output = None

    for i, arg in enumerate(sys.argv):
        if arg == "--output" and i + 1 < len(sys.argv):
            output = sys.argv[i + 1]

    if not Path(old_dir).is_dir():
        print(f"❌ 目录不存在: {old_dir}")
        sys.exit(1)
    if not Path(new_dir).is_dir():
        print(f"❌ 目录不存在: {new_dir}")
        sys.exit(1)

    pairs = pair_chapters(old_dir, new_dir)
    if not pairs:
        print("❌ 未找到章节文件")
        sys.exit(1)

    print(f"🔍 对比 {len(pairs)} 章...\n")
    generate_report(pairs, old_dir, new_dir, output)


if __name__ == "__main__":
    main()


path: tests/test_benchmark.py
content: """测试 benchmark.py"""
import tempfile
from pathlib import Path


def test_benchmark_script():
    from scripts import benchmark as bm
    result = bm.benchmark_script("nonexistent_script", [])
    assert result is None


def test_list_chapters(tmp_path):
    from scripts import benchmark as bm

    d = tmp_path / "chapters"
    d.mkdir()
    (d / "第001章_开始.md").write_text("内容" * 100)
    (d / "第002章_发展.md").write_text("内容" * 200)
    (d / "not_a_chapter.md").write_text("无关")

    chapters = bm.list_chapters(str(d))
    # list_chapters 返回所有 .md，不过滤
    assert len(chapters) == 3


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)