1
0
Эх сурвалжийг харах

refactor: migrate to .claude directory structure with unified skill naming

- Move all files to .claude/ directory (Claude Code standard)
- Rename skills to webnovel-xxx format (init/plan/write/review/query/resume)
- Remove commands/ directory (skills now serve as single entry point)
- Update all internal path references
- Add README.md with v5.0 documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
lingfengQAQ 5 сар өмнө
commit
4a12623166
100 өөрчлөгдсөн 35416 нэмэгдсэн , 0 устгасан
  1. 169 0
      .claude/agents/consistency-checker.md
  2. 323 0
      .claude/agents/context-agent.md
  3. 244 0
      .claude/agents/continuity-checker.md
  4. 336 0
      .claude/agents/data-agent.md
  5. 133 0
      .claude/agents/high-point-checker.md
  6. 330 0
      .claude/agents/metadata-extractor.md
  7. 210 0
      .claude/agents/ooc-checker.md
  8. 210 0
      .claude/agents/pacing-checker.md
  9. 582 0
      .claude/genres/dog-blood-romance/character-archetypes.md
  10. 578 0
      .claude/genres/dog-blood-romance/emotional-tension.md
  11. 213 0
      .claude/genres/dog-blood-romance/plot-templates.md
  12. 574 0
      .claude/genres/dog-blood-romance/romance-pacing.md
  13. 528 0
      .claude/genres/dog-blood-romance/romance-tropes.md
  14. 620 0
      .claude/genres/dog-blood-romance/sweet-moments.md
  15. 612 0
      .claude/genres/dog-blood-romance/torture-points.md
  16. 288 0
      .claude/genres/period-drama/ancient-dialogue.md
  17. 268 0
      .claude/genres/period-drama/character-design.md
  18. 278 0
      .claude/genres/period-drama/historical-setting.md
  19. 268 0
      .claude/genres/period-drama/palace-intrigue.md
  20. 276 0
      .claude/genres/period-drama/plot-patterns.md
  21. 234 0
      .claude/genres/realistic/character-depth.md
  22. 284 0
      .claude/genres/realistic/dialogue-authenticity.md
  23. 273 0
      .claude/genres/realistic/plot-logic.md
  24. 229 0
      .claude/genres/realistic/reality-anchoring.md
  25. 232 0
      .claude/genres/realistic/social-issues.md
  26. 524 0
      .claude/genres/rules-mystery/clue-design.md
  27. 431 0
      .claude/genres/rules-mystery/core-elements.md
  28. 560 0
      .claude/genres/rules-mystery/detective-design.md
  29. 472 0
      .claude/genres/rules-mystery/revelation-design.md
  30. 520 0
      .claude/genres/rules-mystery/structure-pacing.md
  31. 497 0
      .claude/genres/rules-mystery/suspect-management.md
  32. 507 0
      .claude/genres/rules-mystery/trick-design.md
  33. 476 0
      .claude/genres/xuanhuan/cultivation-levels.md
  34. 373 0
      .claude/genres/xuanhuan/power-systems.md
  35. 672 0
      .claude/genres/xuanhuan/xuanhuan-cool-points.md
  36. 547 0
      .claude/genres/xuanhuan/xuanhuan-plot-patterns.md
  37. 190 0
      .claude/genres/zhihu-short/character-quick-build.md
  38. 236 0
      .claude/genres/zhihu-short/emotional-peaks.md
  39. 217 0
      .claude/genres/zhihu-short/ending-patterns.md
  40. 224 0
      .claude/genres/zhihu-short/genre-templates.md
  41. 151 0
      .claude/genres/zhihu-short/hook-techniques.md
  42. 190 0
      .claude/genres/zhihu-short/pacing-rhythm.md
  43. 207 0
      .claude/genres/zhihu-short/plot-compression.md
  44. 487 0
      .claude/references/entity-management-spec.md
  45. 19 0
      .claude/scripts/__init__.py
  46. 594 0
      .claude/scripts/archive_manager.py
  47. 471 0
      .claude/scripts/backup_manager.py
  48. 84 0
      .claude/scripts/chapter_paths.py
  49. 514 0
      .claude/scripts/context_pack_builder.py
  50. 50 0
      .claude/scripts/data_modules/__init__.py
  51. 424 0
      .claude/scripts/data_modules/api_client.py
  52. 208 0
      .claude/scripts/data_modules/config.py
  53. 343 0
      .claude/scripts/data_modules/entity_linker.py
  54. 471 0
      .claude/scripts/data_modules/index_manager.py
  55. 638 0
      .claude/scripts/data_modules/rag_adapter.py
  56. 1045 0
      .claude/scripts/data_modules/state_manager.py
  57. 385 0
      .claude/scripts/data_modules/style_sampler.py
  58. 1 0
      .claude/scripts/data_modules/tests/__init__.py
  59. 488 0
      .claude/scripts/data_modules/tests/test_data_modules.py
  60. 217 0
      .claude/scripts/extract_chapter_context.py
  61. 1816 0
      .claude/scripts/extract_entities.py
  62. 570 0
      .claude/scripts/golden_three_checker.py
  63. 491 0
      .claude/scripts/init_project.py
  64. 94 0
      .claude/scripts/project_locator.py
  65. 585 0
      .claude/scripts/security_utils.py
  66. 992 0
      .claude/scripts/status_reporter.py
  67. 520 0
      .claude/scripts/stress_test_500chapters.py
  68. 721 0
      .claude/scripts/stress_test_index.py
  69. 1256 0
      .claude/scripts/structured_index.py
  70. 120 0
      .claude/scripts/token_budget_report.md
  71. 638 0
      .claude/scripts/update_state.py
  72. 504 0
      .claude/scripts/workflow_manager.py
  73. 18 0
      .claude/settings.local.json
  74. 155 0
      .claude/skills/webnovel-init/SKILL.md
  75. 511 0
      .claude/skills/webnovel-init/references/creativity/creative-combination.md
  76. 298 0
      .claude/skills/webnovel-init/references/creativity/inspiration-collection.md
  77. 421 0
      .claude/skills/webnovel-init/references/creativity/market-positioning.md
  78. 670 0
      .claude/skills/webnovel-init/references/creativity/selling-points.md
  79. 115 0
      .claude/skills/webnovel-init/references/genre-tropes.md
  80. 42 0
      .claude/skills/webnovel-init/references/system-data-flow.md
  81. 89 0
      .claude/skills/webnovel-init/references/worldbuilding/character-design.md
  82. 179 0
      .claude/skills/webnovel-init/references/worldbuilding/faction-systems.md
  83. 95 0
      .claude/skills/webnovel-init/references/worldbuilding/power-systems.md
  84. 215 0
      .claude/skills/webnovel-init/references/worldbuilding/setting-consistency.md
  85. 86 0
      .claude/skills/webnovel-init/references/worldbuilding/world-rules.md
  86. 142 0
      .claude/skills/webnovel-plan/SKILL.md
  87. 96 0
      .claude/skills/webnovel-plan/references/cool-points-guide.md
  88. 295 0
      .claude/skills/webnovel-plan/references/outlining/chapter-planning.md
  89. 277 0
      .claude/skills/webnovel-plan/references/outlining/conflict-design.md
  90. 210 0
      .claude/skills/webnovel-plan/references/outlining/outline-structure.md
  91. 240 0
      .claude/skills/webnovel-plan/references/outlining/plot-frameworks.md
  92. 106 0
      .claude/skills/webnovel-plan/references/strand-weave-pattern.md
  93. 151 0
      .claude/skills/webnovel-query/SKILL.md
  94. 120 0
      .claude/skills/webnovel-query/references/advanced/foreshadowing.md
  95. 219 0
      .claude/skills/webnovel-query/references/system-data-flow.md
  96. 155 0
      .claude/skills/webnovel-query/references/tag-specification.md
  97. 165 0
      .claude/skills/webnovel-resume/SKILL.md
  98. 42 0
      .claude/skills/webnovel-resume/references/system-data-flow.md
  99. 139 0
      .claude/skills/webnovel-resume/references/workflow-resume.md
  100. 133 0
      .claude/skills/webnovel-review/SKILL.md

+ 169 - 0
.claude/agents/consistency-checker.md

@@ -0,0 +1,169 @@
+---
+name: consistency-checker
+description: 设定一致性检查,输出结构化报告供润色步骤参考
+tools: Read, Grep
+---
+
+# consistency-checker (设定一致性检查器)
+
+> **Role**: Continuity guardian enforcing the second anti-hallucination law (设定即物理 - Settings are Physics).
+
+## Scope
+
+**Input**: Chapter range (e.g., "1-2", "45-46")
+
+**Output**: Structured report on setting violations, power-level conflicts, and logical inconsistencies.
+
+## Execution Protocol
+
+### Step 1: Load Reference Materials
+
+**Parallel reads**:
+1. Target chapters from `正文/`
+2. `.webnovel/state.json` (current protagonist state)
+3. `设定集/` (world-building bible)
+4. `大纲/` (outline for context)
+
+### Step 2: Three-Tier Consistency Check
+
+#### Tier 1: Power System Consistency (战力检查)
+
+**Verify**:
+- Protagonist's current realm/level matches state.json
+- Abilities used are within realm limitations
+- Power-ups follow established progression rules
+
+**Red Flags** (POWER_CONFLICT):
+```
+❌ 主角筑基3层使用金丹期才能掌握的"破空斩"
+   → Realm: 筑基3 | Ability: 破空斩 (requires 金丹期)
+   → VIOLATION: Premature ability access
+
+❌ 上章境界淬体9层,本章突然变成凝气5层(无突破描写)
+   → Previous: 淬体9 | Current: 凝气5 | Missing: Breakthrough scene
+   → VIOLATION: Unexplained power jump
+```
+
+**Check Against**:
+- state.json: `protagonist_state.power.realm`, `protagonist_state.power.layer`
+- 设定集/修炼体系.md: Realm ability restrictions
+
+#### Tier 2: Location & Character Consistency (地点/角色检查)
+
+**Verify**:
+- Current location matches state.json or has valid travel sequence
+- Characters appearing are established in 设定集/ or tagged with `<entity/>`
+- Character attributes (appearance, personality, affiliations) match records
+
+**Red Flags** (LOCATION_ERROR / CHARACTER_CONFLICT):
+```
+❌ 上章在"天云宗",本章突然出现在"千里外的血煞秘境"(无移动描写)
+   → Previous location: 天云宗 | Current: 血煞秘境 | Distance: 1000+ li
+   → VIOLATION: Teleportation without explanation
+
+❌ 李雪上次是"筑基期修为",本章变成"练气期"(无解释)
+   → Character: 李雪 | Previous: 筑基期 | Current: 练气期
+   → VIOLATION: Power regression unexplained
+```
+
+**Check Against**:
+- state.json: `protagonist_state.location.current`
+- 设定集/角色卡/: Character profiles
+
+#### Tier 3: Timeline Consistency (时间线检查)
+
+**Verify**:
+- Event sequence is chronologically logical
+- Time-sensitive elements (deadlines, age, seasonal events) align
+- Flashbacks are clearly marked
+
+**Red Flags** (TIMELINE_ISSUE):
+```
+❌ 第10章提到"三天后的宗门大比",第11章描述大比结束(中间无时间流逝)
+   → Setup: 3 days until event | Next chapter: Event concluded
+   → VIOLATION: Missing time passage
+
+❌ 主角15岁修炼5年,推算应该10岁开始,但设定集记录"12岁入门"
+   → Age: 15 | Cultivation years: 5 | Start age: 10 | Record: 12
+   → VIOLATION: Timeline arithmetic error
+```
+
+### Step 3: Entity Consistency Check (v5.0)
+
+**For all new entities detected in chapters**:
+1. Check if they contradict existing settings
+2. Assess if their introduction is consistent with world-building
+3. Verify power levels are reasonable for the current arc
+
+**Note (v5.0)**: 不再检查 XML 标签,Data Agent 会使用 AI 自动提取实体。
+
+**Report inconsistent inventions**:
+```
+⚠️ 发现设定冲突:
+- 第46章出现"紫霄宗",与设定集中势力分布矛盾
+  → 建议: 确认是否为新势力或笔误
+```
+
+### Step 4: Generate Report
+
+```markdown
+# 设定一致性检查报告 (Consistency Review)
+
+## 覆盖范围
+Chapters {N} - {M}
+
+## 战力一致性 (Power System)
+| Chapter | Issue | Severity | Details |
+|---------|-------|----------|---------|
+| {N} | ✓ No violations | - | - |
+| {M} | ✗ POWER_CONFLICT | High | 主角筑基3层使用金丹期技能"破空斩" |
+
+**Verdict**: {X} violations found
+
+## 地点/角色一致性 (Location & Character)
+| Chapter | Type | Issue | Severity |
+|---------|------|-------|----------|
+| {M} | Location | ✗ LOCATION_ERROR | Medium | 未描述移动过程,从天云宗跳跃到血煞秘境 |
+
+**Verdict**: {Y} violations found
+
+## 时间线一致性 (Timeline)
+| Chapter | Issue | Severity | Details |
+|---------|-------|----------|---------|
+| {M} | ✗ TIMELINE_ISSUE | Low | 大比倒计时逻辑不一致 |
+
+**Verdict**: {Z} violations found
+
+## 新实体一致性检查 (Entity Consistency)
+- ✓ All new entities consistent with world-building: {count}
+- ⚠️ Inconsistent entities found: {count} (详见下方列表)
+- ❌ Contradictory entities: {count}
+
+**Inconsistent List**:
+1. 第{M}章:"紫霄宗" (势力) - 与现有势力分布矛盾
+2. 第{M}章:"天雷果" (物品) - 效果与力量体系不符
+
+## 建议 (Recommendations)
+- [For power conflicts] 润色时修改第{M}章,将"破空斩"替换为筑基期可用技能
+- [For location errors] 润色时补充移动过程描述或调整地点设定
+- [For timeline issues] 润色时统一时间线推算,修正矛盾
+- [For entity conflicts] 润色时确认是否为新设定或需要调整
+
+## 综合评分
+**Overall**: {PASS/FAIL} - {Brief summary}
+**Critical Violations**: {count} (Must fix before continuing)
+**Minor Issues**: {count} (Recommend fixing)
+```
+
+## Anti-Patterns (Forbidden)
+
+❌ Approving chapters with POWER_CONFLICT (战力崩坏)
+❌ Ignoring untagged new entities
+❌ Accepting teleportation without in-world explanation
+
+## Success Criteria
+
+- 0 critical violations (power conflicts, unexplained character changes)
+- All new entities consistent with existing world-building
+- Location and timeline transitions are logical
+- Report provides specific fix recommendations for polish step

+ 323 - 0
.claude/agents/context-agent.md

@@ -0,0 +1,323 @@
+---
+name: context-agent
+description: 智能上下文搜集Agent,为章节写作准备完整的上下文包。在写作前自动调用,负责读取大纲、状态、索引、RAG检索、设定集,并智能筛选组装上下文。
+tools: Read, Grep, Bash
+---
+
+# context-agent (上下文搜集Agent)
+
+> **Role**: 智能上下文工程师,负责为章节写作准备精准、完整的上下文信息包。
+>
+> **Philosophy**: 按需召回,智能筛选 - 不是堆砌信息,而是提供写作真正需要的上下文。
+
+## 输入
+
+```json
+{
+  "chapter": 100,
+  "project_root": "D:/wk/斗破苍穹"
+}
+```
+
+## 输出
+
+```json
+{
+  "core": {
+    "chapter_outline": "本章大纲内容",
+    "volume_outline": "本卷大纲摘要",
+    "protagonist_snapshot": {
+      "name": "萧炎",
+      "realm": "斗师",
+      "location": "天云宗",
+      "recent_events": ["突破斗师", "获得青莲地心火线索"]
+    },
+    "recent_summaries": [
+      {"chapter": 99, "summary": "..."},
+      {"chapter": 98, "summary": "..."}
+    ]
+  },
+  "scene": {
+    "location_context": {
+      "name": "天云宗",
+      "description": "...",
+      "related_chapters": [45, 67, 89]
+    },
+    "appearing_characters": [
+      {"id": "yaolao", "name": "药老", "last_state": "..."},
+      {"id": "lintian", "name": "林天", "last_state": "..."}
+    ],
+    "urgent_foreshadowing": [
+      {"id": "F001", "content": "三年之约", "urgency": "high", "planted_chapter": 1}
+    ],
+    "foreshadow_suggestions": [
+      {"id": "F002", "content": "青莲地心火", "suggestion": "可在本章埋下伏笔"}
+    ]
+  },
+  "global": {
+    "worldview_skeleton": "修炼体系简述...",
+    "power_system_skeleton": "斗气等级: 斗者→斗师→...",
+    "style_samples": [
+      {"type": "combat", "sample": "高质量战斗描写片段..."},
+      {"type": "dialogue", "sample": "高质量对话片段..."}
+    ]
+  },
+  "rag": {
+    "related_scenes": [
+      {"chapter": 45, "scene": 2, "summary": "相关场景摘要", "relevance": 0.85}
+    ]
+  },
+  "alerts": {
+    "disambiguation_warnings": [
+      {"chapter": 99, "mention": "宗主", "chosen_id": "lintian", "confidence": 0.63, "note": "中置信度匹配"}
+    ],
+    "disambiguation_pending": [
+      {"chapter": 99, "mention": "那位前辈", "suggested_id": "yaolao", "confidence": 0.42}
+    ]
+  }
+}
+```
+
+## 执行流程
+
+### Step 1: 分析本章需求
+
+**读取大纲**:
+```bash
+# 读取本章大纲
+Read: 大纲/卷N/第XXX章.md
+
+# 读取本卷大纲概述
+Read: 大纲/卷N/卷概述.md
+```
+
+**分析要点**:
+- 本章主要事件是什么?
+- 需要哪些角色出场?
+- 发生在什么地点?
+- 是否涉及战斗/突破/重要对话?
+
+### Step 2: 获取主角状态
+
+```bash
+# 读取状态文件
+Read: .webnovel/state.json
+```
+
+**提取**:
+- `progress.current_chapter` (进度)
+- `entities_v3.角色` 中主角实体的属性 (境界/位置/物品)
+- `relationships` (重要关系)
+- `state_changes` 最近变化记录
+- `disambiguation_warnings` 最近消歧警告(0.5-0.8 采用但提示风险)
+- `disambiguation_pending` 待确认消歧(<0.5 不自动采用,需人工确认)
+
+### Step 3: 查询相关实体
+
+```bash
+# 查询本章地点相关场景
+python -m data_modules.index_manager search-scenes --location "天云宗" --project-root "."
+
+# 查询出场角色历史
+python -m data_modules.index_manager entity-appearances --entity "yaolao" --project-root "."
+
+# 查询最近出场实体
+python -m data_modules.index_manager recent-appearances --limit 20 --project-root "."
+```
+
+**处理逻辑**:
+- 地点相关: 召回最近3次在该地点的场景
+- 角色相关: 召回角色最近出场状态
+- 伏笔: 筛选 urgency >= medium 的伏笔
+
+### Step 4: 语义检索 (RAG)
+
+```bash
+# 基于大纲关键词进行语义检索
+python -m data_modules.rag_adapter search --query "大纲关键事件" --mode hybrid --top-k 5 --project-root "."
+```
+
+**检索策略**:
+- 提取大纲中的关键事件/冲突
+- 检索相关历史场景
+- 优先召回高相关度 (score > 0.7) 的场景
+
+### Step 5: 搜索设定集
+
+```bash
+# 搜索相关设定
+Grep: 设定集/ "关键词"
+```
+
+**搜索内容**:
+- 修炼体系相关 (如果涉及突破)
+- 势力设定 (如果涉及新势力)
+- 角色卡片 (如果有新角色互动)
+
+### Step 6: 评估伏笔紧急度
+
+**紧急度计算**:
+```
+urgency = base_urgency + (current_chapter - planted_chapter) / expected_resolve_range
+```
+
+**分类**:
+- `critical`: urgency > 0.9,必须本章/近期回收
+- `high`: urgency > 0.7,建议近期回收
+- `medium`: urgency > 0.4,可以提及/推进
+- `low`: urgency <= 0.4,暂不处理
+
+### Step 7: 选择风格样本
+
+**选择逻辑**:
+- 根据大纲判断本章类型 (战斗/对话/过渡/描写)
+- 从风格样本库中选择匹配类型的高质量片段
+- 最多选择 2-3 个样本
+
+```bash
+# 查询风格样本
+python -m data_modules.style_sampler list --type "战斗" --limit 2 --project-root "."
+```
+
+### Step 8: 组装上下文包
+
+**智能筛选原则**:
+
+| 信息类型 | 包含条件 | Token 预算 |
+|---------|---------|-----------|
+| 本章大纲 | 必须 | ~500 |
+| 主角快照 | 必须 | ~300 |
+| 最近3章摘要 | 必须 | ~600 |
+| 地点上下文 | 如果换地点 | ~400 |
+| 出场角色 | 大纲提及的 | ~500 |
+| 紧急伏笔 | urgency >= high | ~300 |
+| 世界观骨架 | 如果涉及设定 | ~400 |
+| 风格样本 | 按场景类型 | ~600 |
+| RAG召回 | score > 0.7 | ~800 |
+
+**总预算**: ~4000-5000 tokens
+
+### Step 9: 输出上下文包 JSON
+
+将组装好的上下文包以 JSON 格式输出,供写作步骤使用。
+
+---
+
+## 智能决策点
+
+### 决策 1: 召回多少历史?
+
+| 场景复杂度 | 召回量 |
+|-----------|-------|
+| 简单过渡章 | 最近2章摘要 |
+| 普通剧情章 | 最近3章摘要 + 1-2个RAG场景 |
+| 复杂冲突章 | 最近5章摘要 + 3-5个RAG场景 |
+| 回收伏笔章 | 伏笔种下章 + 相关发展章节 |
+
+### 决策 2: 是否附带伏笔建议?
+
+- 如果有 `critical` 伏笔 → 强制附带回收建议
+- 如果有 `high` 伏笔且本章场景适合 → 附带推进建议
+- 其他情况 → 不附带
+
+### 决策 3: 选择哪些风格样本?
+
+| 本章类型 | 样本类型 |
+|---------|---------|
+| 战斗为主 | combat x2 |
+| 对话为主 | dialogue x2 |
+| 描写为主 | description x2 |
+| 混合类型 | 各取1个 |
+
+---
+
+## 错误处理
+
+### 文件不存在
+
+```
+⚠️ 大纲文件不存在: 大纲/卷3/第100章.md
+→ 尝试读取卷概述作为参考
+→ 如果卷概述也不存在,返回错误要求补充大纲
+```
+
+### 索引查询失败
+
+```
+⚠️ data_modules 查询失败
+→ 降级为 Grep 直接搜索
+→ 记录 warning 到输出
+```
+
+### RAG 服务不可用
+
+```
+⚠️ data_modules.rag_adapter 服务不可用
+→ 跳过语义检索
+→ 增加 Grep 搜索范围补偿
+→ 记录 warning 到输出
+```
+
+---
+
+## 输出示例
+
+```json
+{
+  "core": {
+    "chapter_outline": "萧炎在天云宗突破斗师,引发宗门震动...",
+    "volume_outline": "第三卷:天云宗篇,主线:萧炎加入天云宗,暗线:云韵身份...",
+    "protagonist_snapshot": {
+      "name": "萧炎",
+      "realm": "斗者九层",
+      "location": "天云宗·外门",
+      "recent_events": ["击败慕容战天", "获得地心火线索", "与药老商议突破"]
+    },
+    "recent_summaries": [
+      {"chapter": 99, "summary": "萧炎闭关准备突破,药老传授突破要诀"},
+      {"chapter": 98, "summary": "萧炎击败慕容战天,声名鹊起"},
+      {"chapter": 97, "summary": "宗门大比开始,萧炎一路碾压"}
+    ]
+  },
+  "scene": {
+    "location_context": {
+      "name": "天云宗",
+      "description": "东域中等宗门,以炼丹著称",
+      "related_chapters": [45, 67, 89]
+    },
+    "appearing_characters": [
+      {"id": "yaolao", "name": "药老", "last_state": "戒指中沉睡,偶尔指点"},
+      {"id": "yunzhi", "name": "云芝", "last_state": "宗门长老,对萧炎有好感"}
+    ],
+    "urgent_foreshadowing": [
+      {"id": "F001", "content": "三年之约", "urgency": "high", "planted_chapter": 1, "suggestion": "可在突破时内心独白提及"}
+    ],
+    "foreshadow_suggestions": []
+  },
+  "global": {
+    "worldview_skeleton": "斗气大陆,以斗气修炼为主...",
+    "power_system_skeleton": "斗者→斗师→大斗师→斗灵→斗王→斗皇→斗宗→斗尊→斗圣→斗帝",
+    "style_samples": [
+      {"type": "breakthrough", "sample": "体内斗气如潮水般涌动,经脉中传来阵阵酥麻..."}
+    ]
+  },
+  "rag": {
+    "related_scenes": [
+      {"chapter": 45, "scene": 2, "summary": "萧炎初入天云宗,被分配到外门", "relevance": 0.82}
+    ]
+  },
+  "warnings": [],
+  "token_estimate": 4200
+}
+```
+
+---
+
+## 成功标准
+
+1. ✅ 上下文包包含写作必需的所有信息
+2. ✅ Token 预算控制在 5000 以内
+3. ✅ 紧急伏笔被正确识别和附带
+4. ✅ 风格样本与本章类型匹配
+5. ✅ 错误情况有合理降级处理
+6. ✅ 输出格式为有效 JSON

+ 244 - 0
.claude/agents/continuity-checker.md

@@ -0,0 +1,244 @@
+---
+name: continuity-checker
+description: 连贯性检查,输出结构化报告供润色步骤参考
+tools: Read, Grep
+---
+
+# continuity-checker (连贯性检查器)
+
+> **Role**: Narrative flow guardian ensuring smooth transitions and logical plot progression.
+
+## Scope
+
+**Input**: Chapter range (e.g., "1-2", "45-46")
+
+**Output**: Continuity analysis covering scene transitions, plot threads, foreshadowing, and logical flow.
+
+## Execution Protocol
+
+### Step 1: Load Context
+
+**Parallel reads**:
+1. Target chapters from `正文/`
+2. Previous 2-3 chapters (for transition context)
+3. `大纲/` (to check outline adherence - 大纲即法律)
+4. `.webnovel/state.json` (plot thread tracker, if exists)
+
+### Step 2: Four-Tier Continuity Check
+
+#### Tier 1: Scene Transition Smoothness (场景转换)
+
+**Check for**:
+- Abrupt location jumps without explanation
+- Time skips without markers
+- POV changes without clear breaks
+
+**Red Flags**:
+```
+❌ Abrupt Transition:
+上一段:林天在天云宗大殿与长老对话
+下一段:林天已经在血煞秘境深处战斗
+问题:缺少移动过程/时间流逝描写
+
+✓ Smooth Transition:
+上一段:林天告别长老,离开宗门
+过渡句:"三日后,林天抵达血煞秘境入口"
+下一段:林天在秘境中遭遇妖兽
+```
+
+**Transition Quality Grades**:
+- **A**: 自然过渡 + 时间/空间标记清晰
+- **B**: 有过渡但略显生硬
+- **C**: 缺少过渡,靠读者推测
+- **F**: 完全断裂,逻辑跳跃
+
+#### Tier 2: Plot Thread Coherence (情节线连贯)
+
+**Track active plot threads**:
+- **Main Thread** (主线): 当前核心任务/目标
+- **Sub-threads** (支线): 次要任务、悬念、铺垫
+
+**Check for**:
+- Threads introduced but never resolved (烂尾)
+- Threads resolved without proper setup (突兀)
+- Threads forgotten mid-story (遗忘)
+
+**Example Analysis**:
+```
+第40章引入: "宗门大比将在10天后举行"(主线)
+第45章: 大比正在进行中 ✓
+第50章: 大比结束,主角获胜 ✓
+判定:✓ 线索完整,有始有终
+
+vs.
+
+第30章引入: "血煞门即将入侵"(支线伏笔)
+第31-50章: 完全未提及血煞门
+判定:⚠️ 线索悬空,可能遗忘或拖得太久
+```
+
+#### Tier 3: Foreshadowing Management (伏笔管理)
+
+**Classify foreshadowing**:
+| Type | Setup → Payoff Gap | Risk |
+|------|-------------------|------|
+| **Short-term** (短期) | 1-3 章 | Low |
+| **Mid-term** (中期) | 4-10 章 | Medium (容易被遗忘) |
+| **Long-term** (长期) | 10+ 章 | High (需明确标记) |
+
+**Red Flags**:
+```
+⚠️ Forgotten Foreshadowing:
+第10章: "林天发现神秘玉佩,似乎隐藏秘密"
+第11-30章: 玉佩再未提及
+判定:⚠️ 伏笔遗忘风险,建议第31章回收或再次提及
+
+✓ Proper Payoff:
+第10章: "李雪提到师父曾去过血煞秘境"
+第25章: "在秘境中发现李雪师父留下的线索"
+判定:✓ 伏笔回收合理,间隔15章属于中期伏笔
+```
+
+**Foreshadowing Checklist**:
+- [ ] 所有设置的伏笔是否在合理章节内回收?
+- [ ] 长期伏笔(10+章)是否定期提及以保持读者记忆?
+- [ ] 回收时是否自然,不生硬?
+
+#### Tier 4: Logical Flow (逻辑流畅性)
+
+**Check for plot holes and logical inconsistencies**:
+
+```
+❌ Logic Hole:
+第45章: 主角说"我从未见过这种妖兽"
+第30章: 主角曾击败同种妖兽
+判定:❌ 前后矛盾,需修正
+
+❌ Causality Break:
+第46章: 主角突然获得神秘力量
+问题: 无解释来源,违反"发明需申报"原则
+判定:❌ 缺少因果关系,需补充 `<entity/>` 或铺垫
+
+✓ Logical:
+第44章: 主角服用聚气丹(铺垫)
+第45章: 主角突破境界(因果)
+判定:✓ 因果清晰
+```
+
+### Step 3: Outline Adherence Check (大纲即法律)
+
+**Compare chapters against outline**:
+
+```
+大纲第45章: "主角参加宗门大比,对战王少,险胜"
+
+实际第45章内容:
+- ✓ 主角参加大比
+- ✓ 对战王少
+- ✗ 结果是"轻松碾压"而非"险胜"
+
+判定:⚠️ 偏离大纲(难度降低),需确认是否有意调整
+```
+
+**Deviation Handling**:
+- **Minor** (细节优化): 可接受
+- **Moderate** (情节调整): 需标记并确认
+- **Major** (核心冲突变化): 必须标记 `<deviation reason="..."/>` 并说明
+
+### Step 4: Pacing & Drag Check (拖沓检查)
+
+**Identify dragging sections**:
+```
+⚠️ Possible Drag:
+第45-46章: 两章都在描述"主角赶路"
+内容: 重复的风景描写,无关键事件
+判定:⚠️ 节奏拖沓,建议:
+- 压缩为1章
+- 或在赶路途中安排事件(遭遇/奇遇/思考)
+
+✓ Efficient Pacing:
+第47章: "三日后,主角抵达秘境"(一句带过)
+判定:✓ 有效省略无关紧要的过程
+```
+
+### Step 5: Generate Report
+
+```markdown
+# 连贯性检查报告 (Continuity Review)
+
+## 覆盖范围
+Chapters {N} - {M}
+
+## 场景转换评分 (Scene Transitions)
+| Transition | From → To | Grade | Issue |
+|------------|-----------|-------|-------|
+| Ch{N}→Ch{M} | 天云宗大殿 → 血煞秘境 | C | 缺少移动过程描写 |
+
+**Overall Transition Quality**: {Average Grade}
+
+## 情节线追踪 (Plot Threads)
+| Thread | Introduced | Last Mentioned | Status | Next Action |
+|--------|-----------|----------------|--------|-------------|
+| 宗门大比 | Ch 40 | Ch 46 (结束) | ✓ Resolved | - |
+| 血煞门入侵 | Ch 30 | Ch 30 | ⚠️ Dormant (16章未提及) | 建议Ch 47提及或回收 |
+| 神秘玉佩 | Ch 10 | Ch 10 | ⚠️ Forgotten (36章未提及) | 建议回收或删除伏笔 |
+
+**Active Threads**: {count}
+**Dormant/Forgotten**: {count}
+
+## 伏笔管理 (Foreshadowing)
+| Setup | Chapter | Type | Payoff | Gap | Status |
+|-------|---------|------|--------|-----|--------|
+| 李雪师父去过秘境 | 10 | Mid-term | Ch 25发现线索 | 15章 | ✓ Resolved |
+| 神秘玉佩 | 10 | Long-term | 未回收 | 36章+ | ❌ 遗忘风险 |
+
+**Foreshadowing Health**: {X} resolved, {Y} pending, {Z} at risk
+
+## 逻辑一致性 (Logical Flow)
+| Chapter | Issue | Type | Severity |
+|---------|-------|------|----------|
+| {M} | 前后矛盾(主角称"从未见过"但第30章遇见过) | Contradiction | High |
+| {M} | 突然获得力量无解释 | Missing Causality | Medium |
+
+**Logic Holes Found**: {count}
+
+## 大纲一致性 (Outline Adherence)
+| Chapter | Outline | Actual | Deviation Level |
+|---------|---------|--------|----------------|
+| {M} | 险胜王少 | 轻松碾压 | ⚠️ Moderate (难度调整) |
+
+**Deviations**: {count} ({X} minor, {Y} moderate, {Z} major)
+
+## 节奏拖沓检查 (Pacing Drag)
+- ⚠️ Chapters {N}-{M}: 两章赶路场景重复,建议压缩或增加事件
+
+## 建议 (Recommendations)
+1. **修复场景转换**: Ch{M}添加"三日后"等时间标记
+2. **回收遗忘伏笔**: 神秘玉佩已36章未提及,建议:
+   - Ch 47-50 安排回收场景
+   - 或回溯删除该伏笔(如不重要)
+3. **解决逻辑矛盾**: Ch{M}修改"从未见过"为"很少见到"
+4. **提及休眠线索**: 血煞门入侵线索建议Ch 47再次提及,保持读者记忆
+5. **压缩拖沓段落**: Ch{N}-{M}赶路场景合并为1章
+
+## 综合评分
+**Overall Continuity**: {SMOOTH/ACCEPTABLE/CHOPPY/BROKEN}
+**Critical Issues**: {count} (必须修复)
+**Recommendations**: {count} (建议改进)
+```
+
+## Anti-Patterns (Forbidden)
+
+❌ Approving chapters with major outline deviations without `<deviation/>` tag
+❌ Ignoring forgotten foreshadowing (10+ chapters dormant)
+❌ Accepting abrupt scene transitions (Grade F)
+❌ Overlooking plot holes and contradictions
+
+## Success Criteria
+
+- All scene transitions rated ≥ B
+- No active plot threads forgotten > 15 chapters
+- All long-term foreshadowing tracked and payoff planned
+- 0 major logic holes
+- Outline deviations properly tagged
+- Report identifies specific chapters for fixes

+ 336 - 0
.claude/agents/data-agent.md

@@ -0,0 +1,336 @@
+---
+name: data-agent
+description: 数据处理Agent (v5.0),负责AI实体提取、场景切片、索引构建。使用 entities_v3 格式和一对多别名。在章节完成后自动调用,处理数据链的写入工作。
+tools: Read, Write, Bash
+---
+
+# data-agent (数据处理Agent v5.0)
+
+> **Role**: 智能数据工程师,负责从章节正文中提取结构化信息并写入数据链。
+>
+> **Philosophy**: AI驱动提取,智能消歧 - 用语义理解替代正则匹配,用置信度控制质量。
+>
+> **v5.0 变更**:
+> - 使用 `entities_v3` 分组格式(按类型:角色/地点/物品/势力/招式)
+> - 别名索引支持一对多(同一别名可映射多个实体)
+> - `alias_index` 内嵌在 `state.json` 中,不再是独立文件
+
+## 输入
+
+```json
+{
+  "chapter": 100,
+  "chapter_file": "正文/第0100章.md",
+  "review_score": 85,
+  "project_root": "D:/wk/斗破苍穹"
+}
+```
+
+## 输出
+
+```json
+{
+  "entities_appeared": [
+    {"id": "xiaoyan", "type": "角色", "mentions": ["萧炎", "他"], "confidence": 0.95}
+  ],
+  "entities_new": [
+    {"suggested_id": "hongyi_girl", "name": "红衣女子", "type": "角色", "tier": "装饰"}
+  ],
+  "state_changes": [
+    {"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破"}
+  ],
+  "relationships_new": [
+    {"from": "xiaoyan", "to": "hongyi_girl", "type": "相识", "description": "初次见面"}
+  ],
+  "scenes_chunked": 4,
+  "uncertain": [
+    {"mention": "那位前辈", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "confidence": 0.6}
+  ],
+  "warnings": []
+}
+```
+
+## 执行流程
+
+### Step A: 加载上下文
+
+```bash
+# 读取章节正文
+Read: 正文/第0100章.md
+
+# 读取已有实体库
+Read: .webnovel/state.json → entities
+
+# 读取别名索引
+python -m data_modules.entity_linker list-aliases --entity "xiaoyan" --project-root "."
+
+# 读取最近章节出场记录
+python -m data_modules.index_manager recent-appearances --limit 20 --project-root "."
+```
+
+**准备数据**:
+- 已有实体列表 (id, name, aliases, type)
+- 别名映射表 (alias → entity_id)
+- 最近出场实体 (用于上下文推断)
+
+### Step B: AI 实体提取
+
+**Data Agent 直接执行** (无需调用外部 LLM):
+
+基于上述上下文,直接分析章节正文,输出结构化 JSON:
+
+```json
+{
+  "entities_appeared": [
+    {"id": "xiaoyan", "type": "角色", "mentions": ["萧炎", "他"], "confidence": 0.95},
+    {"id": "yaolao", "type": "角色", "mentions": ["药老"], "confidence": 0.92},
+    {"id": "NEW", "suggested_id": "hongyi_girl", "name": "红衣女子", "type": "角色", "tier": "装饰"}
+  ],
+  "state_changes": [
+    {"entity_id": "xiaoyan", "field": "realm", "old": "斗者九层", "new": "斗师一层", "reason": "闭关突破"}
+  ],
+  "relationships_new": [
+    {"from": "xiaoyan", "to": "hongyi_girl", "type": "相识", "description": "在突破时偶遇"}
+  ],
+  "uncertain": [
+    {"mention": "那位前辈", "context": "那位前辈看了他一眼", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "suggested": "yaolao", "confidence": 0.6}
+  ]
+}
+```
+
+### Step C: 实体消歧处理
+
+**置信度策略**:
+
+| 置信度范围 | 处理方式 |
+|-----------|---------|
+| > 0.8 | 自动采用,无需确认 |
+| 0.5 - 0.8 | 采用建议值,记录 warning |
+| < 0.5 | 标记待人工确认,不自动写入 |
+
+**消歧算法**:
+```python
+for uncertain_item in uncertain:
+    if uncertain_item.confidence > 0.8:
+        # 高置信度:直接采用
+        adopt(uncertain_item.suggested)
+    elif uncertain_item.confidence > 0.5:
+        # 中置信度:采用但警告
+        adopt(uncertain_item.suggested)
+        warnings.append(f"中置信度匹配: {uncertain_item.mention} → {uncertain_item.suggested}")
+    else:
+        # 低置信度:不采用,人工确认
+        pending_review.append(uncertain_item)
+        warnings.append(f"需人工确认: {uncertain_item.mention}")
+```
+
+**同名异人处理**:
+```
+"宗主" 出现在血煞秘境 → 可能是 xueshazonzhu
+"宗主" 出现在天云宗 → 可能是 lintian
+→ 根据当前地点上下文推断
+```
+
+**异名同人处理**:
+```
+"萧炎" / "小炎子" / "那小子" / "他"
+→ 根据 alias_index 映射到 xiaoyan
+→ 代词"他"需根据上下文推断
+```
+
+### Step D: 写入存储
+
+**更新 state.json (v5.0 entities_v3 格式)**:
+```bash
+python -m data_modules.state_manager process-chapter --chapter 100 --data '{...}' --project-root "."
+```
+
+写入内容:
+- 新实体添加到 `entities_v3.{类型}.{entity_id}`
+- 状态变化更新到对应实体的 `current` 字段
+- 新关系添加到 `relationships`
+- 新别名注册到 `alias_index`(一对多格式)
+- 更新 `progress.current_chapter`
+- **自动同步主角状态**:`entities_v3.角色.{主角ID}.current` → `protagonist_state`
+
+> **主角同步说明**:为避免双源不一致,`process_chapter_result()` 会自动调用 `sync_protagonist_from_entity()`,将主角实体的 realm/location 同步到 `protagonist_state`,确保 consistency-checker 等依赖 `protagonist_state` 的组件获取最新数据。
+
+**更新 index.db**:
+```bash
+python -m data_modules.index_manager process-chapter --chapter 100 --title "突破" --location "天云宗" --word-count 3500 --entities '[...]' --scenes '[...]' --project-root "."
+```
+
+写入内容:
+- 章节元数据 (location, characters, word_count)
+- 实体出场记录
+- 场景索引
+
+**注册新别名 (v5.0 一对多)**:
+```bash
+python -m data_modules.entity_linker register-alias --entity "hongyi_girl" --alias "红衣女子" --type "角色" --project-root "."
+```
+
+> 注:v5.0 别名索引支持一对多,同一别名(如"天云宗")可同时映射到地点和势力。
+
+### Step E: AI 场景切片
+
+**Data Agent 直接执行** (无需调用外部 LLM):
+
+根据以下规则切分场景:
+- 按地点变化切分
+- 按时间跳跃切分
+- 按视角变化切分
+- 每个场景生成摘要 (50-100字)
+
+**输出**:
+```json
+{
+  "scenes": [
+    {"index": 1, "start_line": 1, "end_line": 45, "location": "天云宗·闭关室", "summary": "萧炎闭关突破斗师...", "characters": ["xiaoyan"]},
+    {"index": 2, "start_line": 46, "end_line": 89, "location": "天云宗·演武场", "summary": "突破引发天象,众人围观...", "characters": ["xiaoyan", "lintian"]}
+  ]
+}
+```
+
+### Step F: 向量嵌入
+
+**调用 RAG 存储**:
+```bash
+python -m data_modules.rag_adapter index-chapter --chapter 100 --scenes '[...]' --project-root "."
+```
+
+写入内容:
+- 场景摘要向量化 (调用 Modal Embedding API)
+- 存入 SQLite 向量库
+- 更新 BM25 索引
+
+### Step G: 风格样本评估
+
+**评估条件**:
+```python
+if review_score >= 80:
+    # 高分章节,可能作为风格样本
+    extract_style_candidates(chapter_content)
+```
+
+**提取逻辑**:
+- 识别高质量片段 (战斗/对话/描写)
+- 标记片段类型
+- 存入风格样本库
+
+```bash
+python -m data_modules.style_sampler extract --chapter 100 --score 85 --scenes '[...]' --project-root "."
+```
+
+### Step H: 生成处理报告
+
+```json
+{
+  "chapter": 100,
+  "entities_appeared": 5,
+  "entities_new": 1,
+  "state_changes": 1,
+  "relationships_new": 1,
+  "scenes_chunked": 4,
+  "uncertain": [
+    {"mention": "那位前辈", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "adopted": "yaolao", "confidence": 0.6}
+  ],
+  "warnings": [
+    "中置信度匹配: 那位前辈 → yaolao (confidence: 0.6)"
+  ],
+  "errors": []
+}
+```
+
+---
+
+## 提取规则参考
+
+### 1. 实体识别
+- 识别所有出场的角色、地点、物品、势力
+- 优先匹配已有实体(通过名称或别名)
+- 新实体需要建议 entity_id(使用拼音或英文)
+
+### 2. 状态变化
+- 识别实力变化(境界突破/跌落)
+- 识别位置变化(移动到新地点)
+- 识别持有物变化(获得/失去物品)
+- 识别关系变化(结盟/敌对/师徒等)
+
+### 3. 消歧处理
+- 代词(他/她/它)需根据上下文推断指代
+- 称呼(宗主/长老/前辈)需根据场景推断
+- 不确定时标记 uncertain 并给出候选
+
+### 4. 置信度评估
+- 0.9-1.0: 明确提及名字
+- 0.7-0.9: 通过别名/称呼确定
+- 0.5-0.7: 通过上下文推断
+- <0.5: 无法确定
+
+---
+
+## 错误处理
+
+### 章节文件不存在
+```
+❌ 章节文件不存在: 正文/第0100章.md
+→ 返回错误,终止处理
+```
+
+### AI 提取失败
+```
+⚠️ AI 提取失败或返回无效 JSON
+→ 重试一次
+→ 仍失败则记录错误,跳过本章处理
+```
+
+### 向量服务不可用
+```
+⚠️ data_modules.rag_adapter 服务不可用
+→ 跳过向量嵌入步骤
+→ 记录 warning,其他步骤继续
+```
+
+### 状态文件锁定
+```
+⚠️ state.json 被锁定
+→ 等待 5 秒重试
+→ 仍失败则记录错误
+```
+
+---
+
+## 成功标准
+
+1. ✅ 所有出场实体被正确识别(准确率 > 90%)
+2. ✅ 状态变化被正确捕获(准确率 > 85%)
+3. ✅ 消歧结果合理(高置信度 > 80%)
+4. ✅ 场景切片数量合理(通常 3-6 个/章)
+5. ✅ 向量成功存入数据库
+6. ✅ 不确定项被正确标记和报告
+7. ✅ 输出格式为有效 JSON
+
+---
+
+## 与 Context Agent 的协作
+
+```
+写作前: Context Agent 读取数据 → 组装上下文包
+写作中: Writer 使用上下文包生成正文
+写作后: Data Agent 处理正文 → 写入数据链
+
+Context Agent (读) ←→ 数据存储 ←→ Data Agent (写)
+```
+
+**数据流 (v5.0)**:
+```
+章节正文 → Data Agent → state.json
+                      ├── entities_v3.{类型}.{id}
+                      ├── alias_index (一对多)
+                      ├── relationships
+                      └── state_changes
+                      → index.db
+                              ↓
+                      Context Agent → 下一章上下文
+```

+ 133 - 0
.claude/agents/high-point-checker.md

@@ -0,0 +1,133 @@
+---
+name: high-point-checker
+description: 爽点密度检查,输出结构化报告供润色步骤参考
+tools: Read, Grep
+---
+
+# high-point-checker (爽点检查器)
+
+> **Role**: Quality assurance specialist focused on reader satisfaction mechanics (爽点设计).
+
+## Scope
+
+**Input**: Chapter range (e.g., "1-2", "45-46")
+
+**Output**: Structured report on cool-point density, type coverage, and execution quality.
+
+## Execution Protocol
+
+### Step 1: Load Target Chapters
+
+Read all chapters in the specified range from `正文/` directory.
+
+### Step 2: Identify Cool-Points (爽点)
+
+Scan for the **5 standard cool-point types**:
+
+| Type | Pattern Keywords | Minimal Requirements |
+|------|-----------------|---------------------|
+| **打脸型** (Face-slapping) | 嘲讽/废物/不屑 → 反转/震惊/目瞪口呆 | Setup + Reversal + Reaction |
+| **升级型** (Level-up) | 突破/境界/瓶颈/顿悟 → 实力暴涨 | Struggle + Breakthrough + Display |
+| **收获型** (Reward) | 奖励/宝物/认可/机缘 | Effort + Acquisition + Value |
+| **扮猪吃虎** (Underdog) | 示弱/隐藏实力 → 碾压 | Concealment + Underestimation + Domination |
+| **装逼打脸** (Counter-flexing) | 对手炫耀 → 主角展示更强 | Opponent Flex + Protagonist Counter + Shock |
+
+### Step 3: Density Check
+
+**Required Baseline**:
+- **Every chapter**: ≥ 1 cool-point (任何类型)
+- **Every 5 chapters**: ≥ 1 combo cool-point (打脸+升级+收获)
+- **Every 10 chapters**: ≥ 1 protagonist power-up
+
+**Output**:
+```
+Chapter X: [✓ 2 cool-points] or [✗ 0 cool-points - VIOLATION]
+```
+
+### Step 4: Type Diversity Check
+
+**Anti-monotony requirement**: No single type should dominate 80%+ of cool-points in the review range.
+
+**Example**:
+```
+Chapters 1-2:
+- 打脸型: 3 (75%) ✓
+- 升级型: 1 (25%)
+Type diversity: Acceptable
+```
+
+vs.
+
+```
+Chapters 45-46:
+- 打脸型: 7 (87.5%) ✗ OVER-RELIANCE
+- 升级型: 1 (12.5%)
+Type diversity: Warning - Monotonous pacing
+```
+
+### Step 5: Execution Quality Assessment
+
+For each identified cool-point, check:
+
+1. **Setup sufficiency**: Was there adequate build-up (至少1-2章伏笔)?
+2. **Reversal impact**: Is the twist unexpected yet logical?
+3. **Emotional payoff**: Did it deliver catharsis (读者情绪释放)?
+
+**Quality Grades**:
+- **A (优秀)**: All 3 criteria met, strong execution
+- **B (良好)**: 2/3 criteria met
+- **C (及格)**: 1/3 criteria met
+- **F (失败)**: Sudden cool-point without setup, or logically inconsistent
+
+### Step 6: Generate Report
+
+```markdown
+# 爽点检查报告 (Cool-Point Review)
+
+## 覆盖范围
+Chapters {N} - {M}
+
+## 密度检查 (Density)
+- Chapter {N}: ✓ 2 cool-points (打脸型 + 升级型)
+- Chapter {M}: ✗ 0 cool-points **[VIOLATION - 需要补充]**
+
+**Verdict**: {PASS/FAIL} ({X}/{Y} chapters meet baseline)
+
+## 类型分布 (Type Diversity)
+- 打脸型 (Face-slapping): {count} ({percent}%)
+- 升级型 (Level-up): {count} ({percent}%)
+- 收获型 (Reward): {count} ({percent}%)
+- 扮猪吃虎 (Underdog): {count} ({percent}%)
+- 装逼打脸 (Counter-flexing): {count} ({percent}%)
+
+**Verdict**: {PASS/WARNING} (Monotony risk if one type > 80%)
+
+## 质量评级 (Quality)
+| Chapter | Cool-Point | Type | Grade | Issue (if any) |
+|---------|-----------|------|-------|----------------|
+| {N} | 主角被嘲讽后一招秒杀对手 | 打脸型 | A | - |
+| {M} | 突然顿悟突破境界 | 升级型 | C | 缺少铺垫(no prior struggle) |
+
+**Verdict**: Average grade = {X}
+
+## 建议 (Recommendations)
+- [If density violation] Chapter {M} 缺少爽点,建议添加{type}型爽点
+- [If monotony] 过度依赖{type}型,建议增加{other_types}
+- [If quality issue] Chapter {M} 的爽点执行不足,需要补充{missing_element}
+
+## 综合评分
+**Overall**: {PASS/FAIL} - {Brief summary}
+```
+
+## Anti-Patterns (Forbidden)
+
+❌ Accepting chapters with 0 cool-points without flagging
+❌ Ignoring sudden cool-points without setup
+❌ Approving 5+ consecutive chapters of the same type
+
+## Success Criteria
+
+- All chapters have ≥ 1 cool-point
+- Type distribution shows variety (no single type > 80%)
+- Average quality grade ≥ B
+- Report includes actionable recommendations

+ 330 - 0
.claude/agents/metadata-extractor.md

@@ -0,0 +1,330 @@
+---
+name: metadata-extractor
+description: "⚠️ DEPRECATED in v5.0 - 功能已合并到 data-agent。保留此文件仅供参考。"
+tools: Read, Grep
+deprecated: true
+replaced_by: data-agent
+---
+
+# Metadata Extractor Agent (已废弃)
+
+> **⚠️ 已废弃**: v5.0 起,此 Agent 的功能已完全合并到 `data-agent`。
+>
+> **替代方案**: 使用 `data-agent`,它同时负责:
+> - AI 语义实体提取(替代 XML 标签解析)
+> - 章节标题/地点推断
+> - 场景切片和索引构建
+>
+> 以下内容保留仅供历史参考。
+
+---
+
+## 🎯 Core Responsibility (历史参考)
+
+Extract **structured metadata** from webnovel chapter content to populate the structured index database, enabling:
+- Fast location-based chapter queries (O(log n) performance)
+- Character appearance tracking
+- Content change detection (via hash)
+
+**与脚本分工**:
+| 功能 | extract_entities.py | metadata-extractor |
+|------|---------------------|-------------------|
+| XML 标签提取 | ✅ 主责 | ❌ 不处理 |
+| 设定集同步 | ✅ 主责 | ❌ 不处理 |
+| 章节标题 | ❌ | ✅ 主责 |
+| 地点推断(语义) | ❌ | ✅ 主责 |
+| 角色识别(语义) | ❌ | ✅ 补充 |
+| 字数/哈希 | ❌ | ✅ 主责 |
+
+---
+
+## 📥 Input Format
+
+**Parameters**:
+- `chapter_num`: Chapter number (integer)
+- `chapter_content`: Full Markdown content of the chapter
+- `script_output` (optional): Output from extract_entities.py script
+
+**Example Input**:
+```markdown
+# 第一章 废柴少年
+
+东域,慕容家族。
+
+清晨的阳光洒在演武场上,带着几分温暖,却驱散不了林天心中的寒意。
+
+"废物!连练气期一层都突破不了,还有脸站在这里?"
+
+```
+
+**With Script Output**:
+```
+脚本已提取实体:
+- 角色: 慕容战天, 慕容虎
+- 地点: (无)
+- 技能: 吞噬 Lv1
+
+请补充语义元数据。
+```
+
+---
+
+## 📤 Output Format
+
+**CRITICAL**: Output **ONLY** a valid JSON object, no additional text or explanations.
+
+**JSON Schema**:
+```json
+{
+  "title": "string (章节标题,从第一行 # 提取)",
+  "location": "string (主要地点,从上下文推断)",
+  "characters": ["array of strings (出场角色名称,最多5个主要角色)"],
+  "word_count": "integer (总字数)",
+  "hash": "string (MD5 hash of content)",
+  "metadata_quality": "string (high/medium/low - 元数据提取置信度)",
+  "script_entities_merged": "boolean (是否已合并脚本提取的实体)"
+}
+```
+
+**角色合并规则**(当有脚本输出时):
+1. 脚本提取的角色 → 优先保留(来自 XML 标签,作者明确标记)
+2. 语义识别的角色 → 补充添加(去重后合并)
+3. 最终最多保留 5 个主要角色
+
+**Example Output (with script merge)**:
+```json
+{
+  "title": "第一章 废柴少年",
+  "location": "慕容家族",
+  "characters": ["林天", "慕容战天", "慕容虎", "云长老"],
+  "word_count": 3215,
+  "hash": "abc123def456...",
+  "metadata_quality": "high",
+  "script_entities_merged": true
+}
+```
+
+**Note**: XML 标签(`<entity>`, `<skill>`, `<foreshadow>`)由脚本处理,本代理不重复提取。
+
+---
+
+## 🔍 Extraction Guidelines
+
+### 1. Title Extraction
+
+**Strategy**:
+- Extract from first `# Heading` in content
+- Remove `#` symbols and leading/trailing whitespace
+- Format: "第N章 章节名"
+
+**Examples**:
+```markdown
+# 第一章 废柴少年           → "第一章 废柴少年"
+## 第十五章:突破!          → "第十五章:突破!"
+# Chapter 7 - The Battle    → "Chapter 7 - The Battle"
+```
+
+---
+
+### 2. Location Extraction ⭐ (Most Critical)
+
+**Strategy** (in priority order):
+
+**A) Explicit Location Markers** (Highest Priority):
+```markdown
+**地点:天云宗**           → "天云宗"
+**位置:血煞秘境**         → "血煞秘境"
+【场景:拍卖会】           → "拍卖会"
+```
+
+**B) Context Clues in First 10 Lines**:
+- Look for geographical/organizational names after chapter title
+- Common patterns:
+  - "东域,慕容家族。" → "慕容家族"
+  - "天云宗,外门演武场。" → "天云宗"
+  - "林天来到了血煞秘境入口。" → "血煞秘境"
+
+**C) Semantic Analysis**:
+- Identify most frequently mentioned location in first 500 characters
+- Prioritize:
+  - 宗门/家族/势力名称(sect/family/faction names)
+  - 地理区域名称(geographical names)
+  - 建筑/场所名称(building/venue names)
+
+**D) Default**:
+- If no clear location found: `"未知"`
+- If multiple locations: choose the **first mentioned** or **most prominent**
+
+**Examples**:
+```markdown
+# 第五章 血煞秘境
+
+林天跟随云长老来到了血煞秘境入口。这里是东域三大凶地之一...
+→ location: "血煞秘境"
+
+# 第三章 拍卖会
+
+天云城,天宝阁。今日是月度拍卖会...
+→ location: "天宝阁" (优先具体场所,而非城市)
+```
+
+**Edge Cases**:
+- Multiple locations in one chapter → pick **first major location**
+- Transition chapters → pick **destination location**
+- Flashback scenes → pick **current timeline location**, note in future if needed
+
+---
+
+### 3. Character Extraction
+
+**Strategy**:
+
+**A) Identify Named Characters**:
+- Extract names from:
+  - Dialogue attributions: `林天说道:`
+  - XML entity tags: `<entity type="角色" name="慕容战天" .../>`
+  - XML skill tags: `<skill .../>` (Protagonist learning new skills)
+  - Narrative mentions: `慕容战天冷笑一声`
+
+**B) Filter Out**:
+- Generic terms: "修士", "弟子", "长老", "众人"
+- Pronouns: "他", "她", "我", "你"
+- Unless part of a name: "云长老" is valid if it's a character identifier
+
+**C) Ranking (Select Top 5)**:
+- **Priority 1**: Protagonist (主角,usually most mentioned)
+- **Priority 2**: Characters in dialogue
+- **Priority 3**: XML-tagged characters (`<entity type="角色" .../>`)
+- **Priority 4**: Most mentioned names (by frequency)
+
+**D) Name Format**:
+- Use **full names** if available: "慕容战天" not just "战天"
+- Keep titles if they're identifiers: "云长老", "血煞门主"
+
+**Examples**:
+```markdown
+Content:
+林天看着慕容战天,心中一片平静。
+"废物,今天就是你的死期!"慕容战天冷笑。
+<entity type="角色" name="慕容虎" desc="跟班" tier="装饰"/>
+云长老在一旁观战。
+
+→ characters: ["林天", "慕容战天", "慕容虎", "云长老"]
+```
+
+---
+
+### 4. Word Count
+
+**Strategy**:
+- Count **total characters** in Markdown content (including Chinese/English/punctuation)
+- Use: `len(content)`
+- **Do NOT** exclude Markdown syntax
+
+---
+
+### 5. Content Hash
+
+**Strategy**:
+- Compute MD5 hash of the **entire content** (UTF-8 encoded)
+- Python equivalent: `hashlib.md5(content.encode('utf-8')).hexdigest()`
+- Used for detecting file changes (Self-Healing Index)
+
+---
+
+### 6. Metadata Quality Assessment
+
+**Confidence Levels**:
+
+- **high**:
+  - Title extracted successfully
+  - Location explicitly marked OR clearly inferred from context
+  - ≥3 characters identified
+
+- **medium**:
+  - Title extracted
+  - Location inferred with moderate confidence
+  - 1-2 characters identified
+
+- **low**:
+  - Missing title OR location is "未知"
+  - No named characters found
+  - Content seems incomplete
+
+---
+
+## ⚠️ Critical Rules
+
+### MUST DO:
+1. ✅ **Output ONLY JSON** - No explanations, no markdown code blocks, just the raw JSON object
+2. ✅ **Escape special characters** in JSON strings (quotes, backslashes)
+3. ✅ **Use double quotes** for JSON keys and string values
+4. ✅ **Include all 6 required fields** (title, location, characters, word_count, hash, metadata_quality)
+
+### MUST NOT:
+1. ❌ **Do NOT** output markdown code blocks (no `` ```json ``)
+2. ❌ **Do NOT** add comments or explanations outside JSON
+3. ❌ **Do NOT** guess wildly - use "未知" for location if truly uncertain
+4. ❌ **Do NOT** include generic terms in characters array
+
+---
+
+## 📋 Example Task Execution
+
+**Input**:
+```
+Chapter 7 content:
+# 第七章 突破
+
+东域,慕容家族,林天的小院。
+
+深夜,月光如水。
+
+林天盘膝而坐,运转《吞天诀》...
+```
+
+**Your Output** (raw JSON, no code block):
+```json
+{
+  "title": "第七章 突破",
+  "location": "慕容家族",
+  "characters": ["林天"],
+  "word_count": 4521,
+  "hash": "7f8a9b2c3d4e5f6a7b8c9d0e1f2a3b4c",
+  "metadata_quality": "high"
+}
+```
+
+---
+
+## 🧪 Self-Check Before Output
+
+Before outputting, verify:
+- [ ] JSON is valid (no syntax errors)
+- [ ] All 7 fields are present (including `script_entities_merged`)
+- [ ] `characters` is an array of strings (max 5 items)
+- [ ] `location` is a meaningful place name or "未知"
+- [ ] `metadata_quality` is one of: high/medium/low
+- [ ] No text outside the JSON object
+- [ ] 如有脚本输出,角色已合并去重
+
+---
+
+## 🔄 Integration Point
+
+**两阶段流水线**(webnovel-write Step 7):
+```
+Step 7.1: extract_entities.py → 设定集同步 + state.json 更新
+    ↓ (传递提取的实体列表)
+Step 7.2: metadata-extractor agent → 语义补充 + structured_index.py
+```
+
+**调用方式**:
+1. 主工作流先运行 `extract_entities.py --auto`
+2. 捕获脚本输出中的实体列表
+3. 调用本代理,传入章节内容 + 脚本输出
+4. 本代理输出 JSON → 传给 `structured_index.py --metadata-json`
+
+---
+
+**End of Specification**

+ 210 - 0
.claude/agents/ooc-checker.md

@@ -0,0 +1,210 @@
+---
+name: ooc-checker
+description: 人物OOC检查,输出结构化报告供润色步骤参考
+tools: Read, Grep
+---
+
+# ooc-checker (人物OOC检查器)
+
+> **Role**: Character integrity guardian preventing OOC (Out-Of-Character) violations.
+
+## Scope
+
+**Input**: Chapter range (e.g., "1-2", "45-46")
+
+**Output**: Character behavior analysis, OOC violations, and personality drift warnings.
+
+## Execution Protocol
+
+### Step 1: Load Character Profiles
+
+**Parallel reads**:
+1. Target chapters from `正文/`
+2. `设定集/角色卡/` (all character profiles)
+3. Previous chapters for behavioral baseline (if reviewing chapters > 10)
+
+### Step 2: Extract Character Profiles
+
+**For each major character, extract**:
+- **Personality traits** (性格): e.g., "隐忍冷静/嚣张狂妄/温柔体贴"
+- **Speech patterns** (说话风格): e.g., "言简意赅/喜欢嘲讽/礼貌用词"
+- **Core values** (价值观): e.g., "重视承诺/追求力量/保护弱者"
+- **Behavioral tendencies** (行为倾向): e.g., "三思而后行/冲动鲁莽/谨慎多疑"
+
+**Example Profile**:
+```
+角色:林天(主角)
+性格:隐忍冷静、智谋深沉、不轻易暴露实力
+说话风格:言简意赅,很少废话,语气平淡
+价值观:重视家族荣誉,保护弱者
+行为倾向:三思而后行,善于隐藏真实意图
+```
+
+### Step 3: Behavior Sampling
+
+**For each chapter, extract character actions and dialogue**:
+
+```
+第45章 - 林天行为采样:
+[对话] "你找死!" 林天怒吼一声,失去理智冲向对手
+[行动] 不顾一切地正面硬刚
+[情绪] 暴怒失控
+```
+
+### Step 4: OOC Detection (三级判定)
+
+#### Level 1: Minor Deviation (轻微偏离)
+**Definition**: Character behaves slightly differently, but has plausible in-world justification.
+
+**Examples**:
+```
+✓ Acceptable:
+角色:林天(平时冷静)
+场景:敌人威胁要杀他家人
+行为:罕见地暴怒
+判定:✓ 触及底线,情绪变化合理
+
+✓ Acceptable:
+角色:李雪(平时温柔)
+场景:主角生死关头
+行为:展现强势果断的一面
+判定:✓ 危机激发隐藏面,有前置铺垫
+```
+
+#### Level 2: Moderate OOC (中度失真)
+**Definition**: Character acts inconsistently without adequate setup or explanation.
+
+**Examples**:
+```
+⚠️ Warning:
+角色:林天(三思而后行)
+场景:普通挑衅
+行为:突然冲动鲁莽
+判定:⚠️ 缺少动机,需补充原因(如压力积累/特殊影响)
+
+⚠️ Warning:
+角色:慕容雪(高傲冷漠)
+场景:对路人甲
+行为:突然温柔体贴
+判定:⚠️ 性格转变过快,需铺垫(如特殊原因/渐进变化)
+```
+
+#### Level 3: Severe OOC (严重崩坏)
+**Definition**: Character acts completely opposite to established traits with no justification.
+
+**Examples**:
+```
+❌ Violation:
+角色:反派(嚣张狂妄、智商在线)
+场景:与主角对峙
+行为:突然智商下线,犯低级错误(故意让主角翻盘)
+判定:❌ 反派智商崩坏,纯粹为剧情服务
+
+❌ Violation:
+角色:林天(隐忍冷静)
+场景:无特殊刺激
+行为:持续多章表现为冲动易怒
+判定:❌ 性格全面改变无解释,核心人设崩塌
+```
+
+### Step 5: Speech Pattern Check
+
+**Verify dialogue consistency**:
+
+| Character Type | Expected Style | OOC Examples |
+|---------------|----------------|--------------|
+| **主角(冷静型)** | 言简意赅、语气平淡 | ❌ "哈哈哈!老子今天就让你见识见识!" (过度张扬) |
+| **反派(嚣张型)** | 嘲讽、轻蔑、自信 | ❌ "对不起...我错了..." (突然怯懦) |
+| **修仙者** | "阁下/道友/在下" | ❌ "牛逼/666/OMG" (现代网络用语) |
+
+### Step 6: Character Development vs. OOC
+
+**Distinguish legitimate growth from OOC**:
+
+```
+✓ Character Development:
+第1-10章:林天谨慎多疑(因为实力弱)
+第50章:林天开始自信果敢(实力提升+经历磨练)
+判定:✓ 合理成长,有渐进式铺垫
+
+❌ OOC:
+第10章:林天隐忍冷静
+第11章:林天突然变成话痨
+判定:❌ 无解释的性格突变,非成长而是失真
+```
+
+**Growth Checklist**:
+- [ ] 性格转变有合理触发事件?
+- [ ] 转变过程有渐进式铺垫?
+- [ ] 转变后的行为与触发事件逻辑一致?
+
+### Step 7: Generate Report
+
+```markdown
+# 人物OOC检查报告 (Character Consistency Review)
+
+## 覆盖范围
+Chapters {N} - {M}
+
+## 主要角色行为采样
+
+### 林天(主角)
+| Chapter | Action/Dialogue | Profile Match | OOC Level |
+|---------|----------------|---------------|-----------|
+| {N} | "..." 冷静观察,未轻举妄动 | ✓ 符合"隐忍冷静" | None |
+| {M} | "你找死!"暴怒冲向对手 | ✗ 不符合"三思而后行" | ⚠️ Moderate |
+
+**OOC Analysis**:
+- 第{M}章林天失去冷静,**缺少触发原因**
+- 建议补充:对手触及底线(如威胁家人)来合理化情绪爆发
+
+### 慕容雪(女配)
+| Chapter | Action/Dialogue | Profile Match | OOC Level |
+|---------|----------------|---------------|-----------|
+| {M} | 突然对路人温柔体贴 | ✗ 不符合"高傲冷漠" | ⚠️ Moderate |
+
+**OOC Analysis**:
+- 性格转变缺少铺垫,建议:
+  - 补充慕容雪性格变化的原因(如受主角影响)
+  - 或将此场景改为"表面冷漠实则关心"来保持人设
+
+## 对话风格检查
+| Character | Expected Style | Violations Found |
+|-----------|----------------|-----------------|
+| 林天 | 言简意赅 | ✓ 无违规 |
+| 反派王少 | 嚣张嘲讽 | ✗ 第{M}章突然谦逊(智商下线) |
+
+## 性格转变检查
+| Character | Previous Trait | Current Trait | Justification | Verdict |
+|-----------|---------------|---------------|---------------|---------|
+| 林天 | 谨慎 | 自信 | ✓ 实力提升+经历铺垫 | ✓ 合理成长 |
+| 慕容雪 | 高傲 | 温柔 | ✗ 无铺垫 | ❌ OOC |
+
+## 建议 (Recommendations)
+1. **修复第{M}章林天OOC**: 补充对手触及底线的情节
+2. **慕容雪性格转变**: 添加渐进式铺垫(3-5章)或调整此章表现
+3. **反派王少智商崩坏**: 修改对话,恢复嚣张狂妄但逻辑在线的人设
+
+## 综合评分
+**OOC Violations**:
+- Severe (严重): {count}
+- Moderate (中度): {count}
+- Minor (轻微): {count}
+
+**Overall**: {PASS/WARNING/FAIL}
+**Priority Fixes**: {列出必须修复的严重OOC}
+```
+
+## Anti-Patterns (Forbidden)
+
+❌ Approving severe OOC without flagging (e.g., 反派智商下线)
+❌ Ignoring character speech pattern violations
+❌ Confusing OOC with character development
+
+## Success Criteria
+
+- 0 severe OOC violations
+- Moderate OOC has plausible in-world justification
+- Character development is gradual and well-motivated
+- Speech patterns match established profiles
+- Report distinguishes between OOC and legitimate growth

+ 210 - 0
.claude/agents/pacing-checker.md

@@ -0,0 +1,210 @@
+---
+name: pacing-checker
+description: Strand Weave 节奏检查,输出结构化报告供润色步骤参考
+tools: Read, Grep
+---
+
+# pacing-checker (节奏检查器)
+
+> **Role**: Pacing analyst enforcing Strand Weave balance to prevent reader fatigue.
+
+## Scope
+
+**Input**: Chapter range (e.g., "1-2", "45-46")
+
+**Output**: Strand distribution analysis, balance warnings, and pacing recommendations.
+
+## Execution Protocol
+
+### Step 1: Load Context
+
+**Parallel reads**:
+1. Target chapters from `正文/`
+2. `.webnovel/state.json` (strand_tracker history)
+3. `大纲/` (to understand intended arc structure)
+
+**Optional: Use status_reporter for automated analysis**:
+```bash
+# 获取 Strand Weave 详细分析(推荐)
+python -m scripts.status_reporter --focus strand --project-root "${PROJECT_ROOT}"
+
+# 输出包含:
+# - Quest/Fire/Constellation 占比统计
+# - 违规检测(连续Quest>5章等)
+# - 章节列表与主导Strand
+```
+
+### Step 2: Classify Chapter Strands
+
+**For each chapter, identify the dominant strand**:
+
+| Strand | Indicators | Examples |
+|--------|-----------|----------|
+| **Quest** (任务线) | 战斗/任务/探索/升级/打怪 | 参加宗门大比、探索秘境、击败反派 |
+| **Fire** (情感线) | 情感关系/暧昧/友情/羁绊 | 与李雪的感情发展、师徒情深、兄弟义气 |
+| **Constellation** (人际线) | 势力关系/阵营/社交/揭示世界观 | 新势力登场、修仙界格局展示、宗门政治 |
+
+**Classification Rules**:
+- A chapter can have **undertones** of multiple strands, but only **one dominant**
+- Dominant =占据章节内容 ≥ 60%
+
+**Example**:
+```
+第45章:主角参加大比(Quest 80%)+ 李雪担心主角(Fire 20%)
+→ Dominant: Quest
+
+第46章:主角与李雪约会(Fire 70%)+ 揭示血煞门阴谋(Constellation 30%)
+→ Dominant: Fire
+```
+
+### Step 3: Balance Check (Strand Weave Violations)
+
+**Load strand_tracker from state.json**:
+```json
+{
+  "strand_tracker": {
+    "last_quest_chapter": 46,
+    "last_fire_chapter": 42,
+    "last_constellation_chapter": 38,
+    "history": [
+      {"chapter": 45, "dominant": "quest"},
+      {"chapter": 46, "dominant": "quest"}
+    ]
+  }
+}
+```
+
+**Apply Warning Thresholds**:
+
+| Violation | Condition | Severity | Impact |
+|-----------|-----------|----------|--------|
+| **Quest Overload** | 连续 5+ 章 Quest 主导 | High | 战斗疲劳,缺少情感深度 |
+| **Fire Drought** | 距上次 Fire > 10 章 | Medium | 人物关系停滞 |
+| **Constellation Absence** | 距上次 Constellation > 15 章 | Low | 世界观单薄 |
+
+**Example Violations**:
+```
+⚠️ Quest Overload (连续7章)
+Chapters 40-46 全部为 Quest 主导
+→ Impact: 读者疲劳,建议第47章安排感情戏或世界观扩展
+
+⚠️ Fire Drought (已12章未出现)
+Last Fire chapter: 34 | Current: 46 | Gap: 12 chapters
+→ Impact: 李雪等角色存在感降低,建议补充互动场景
+
+✓ Constellation Acceptable
+Last Constellation: 38 | Current: 46 | Gap: 8 chapters
+```
+
+### Step 4: Ideal Pacing Reference
+
+**Every 10 chapters should contain**:
+```
+Quest:         ~60% (6-7 chapters)
+Fire:          ~20% (2-3 chapters)
+Constellation: ~20% (1-2 chapters)
+```
+
+**No strand should be absent for**:
+- Quest: > 3 chapters (罕见,因为主线推进)
+- Fire: > 10 chapters
+- Constellation: > 15 chapters
+
+### Step 5: Historical Trend Analysis
+
+**If state.json contains 20+ chapters of history**:
+
+Generate strand distribution chart:
+```
+Chapters 1-20 Strand Distribution:
+Quest:         ████████████░░░░░░░░  60% (12 chapters)
+Fire:          ████░░░░░░░░░░░░░░░░  20% (4 chapters)
+Constellation: ████░░░░░░░░░░░░░░░░  20% (4 chapters)
+
+Verdict: ✓ Balanced pacing (符合理想比例)
+```
+
+vs.
+
+```
+Chapters 21-40 Strand Distribution:
+Quest:         ███████████████████░  95% (19 chapters)
+Fire:          █░░░░░░░░░░░░░░░░░░░   5% (1 chapter)
+Constellation: ░░░░░░░░░░░░░░░░░░░░   0% (0 chapters)
+
+Verdict: ✗ Severe imbalance (Quest 过载,节奏单调)
+```
+
+### Step 6: Generate Report
+
+```markdown
+# 节奏检查报告 (Pacing Review)
+
+## 覆盖范围
+Chapters {N} - {M}
+
+## 当前章节主导情节线
+| Chapter | Dominant Strand | Undertones | Intensity |
+|---------|----------------|-----------|-----------|
+| {N} | Quest | Fire (20%) | High (战斗密集) |
+| {M} | Quest | - | Medium |
+
+## Strand Balance 检查
+### Quest Strand (任务线)
+- Last appearance: Chapter {X}
+- Consecutive chapters: {count}
+- **Status**: {✓ Normal / ⚠️ Warning / ✗ Overload}
+
+### Fire Strand (情感线)
+- Last appearance: Chapter {Y}
+- Gap since last: {count} chapters
+- **Status**: {✓ Normal / ⚠️ Warning / ✗ Drought}
+
+### Constellation Strand (人际线)
+- Last appearance: Chapter {Z}
+- Gap since last: {count} chapters
+- **Status**: {✓ Normal / ⚠️ Warning}
+
+## 历史趋势 (if ≥ 20 chapters)
+Recent 20 chapters distribution:
+- Quest: {X}% ({count} chapters)
+- Fire: {Y}% ({count} chapters)
+- Constellation: {Z}% ({count} chapters)
+
+**Trend**: {Balanced / Quest-heavy / Fire-deficient / ...}
+
+## 建议 (Recommendations)
+- [If Quest Overload] 连续{count}章Quest主导,建议在第{next}章安排:
+  - 与{角色}的感情发展场景 (Fire)
+  - 或揭示{势力/世界观元素} (Constellation)
+
+- [If Fire Drought] 距上次Fire已{count}章,建议补充:
+  - 与李雪/师父/伙伴的互动
+  - 不必是专门的感情章,可作为undertone穿插
+
+- [If Constellation gap] 世界观扩展不足,建议:
+  - 揭示新势力或修仙界格局
+  - 展示新的修炼体系或设定
+
+## 下一章节奏建议
+Based on current balance, Chapter {next} should prioritize:
+**Primary**: {Strand} (因为距上次{gap}章)
+**Secondary**: {Strand} as undertone
+
+## 综合评分
+**Overall Pacing**: {HEALTHY/WARNING/CRITICAL}
+**Reader Fatigue Risk**: {Low/Medium/High}
+```
+
+## Anti-Patterns (Forbidden)
+
+❌ Approving 5+ consecutive Quest chapters without warning
+❌ Ignoring Fire drought > 10 chapters
+❌ Accepting identical pacing patterns across 20+ chapters
+
+## Success Criteria
+
+- No single strand dominates > 70% of recent 10 chapters
+- All strands appear at least once per their threshold
+- Report provides actionable next-chapter recommendation
+- Trend analysis shows balanced distribution (if sufficient history)

+ 582 - 0
.claude/genres/dog-blood-romance/character-archetypes.md

@@ -0,0 +1,582 @@
+# 言情角色人设宝典 (Romance Character Archetypes)
+
+> **核心原则**: 好的言情小说,七分靠人设,三分靠剧情。读者追的不是故事,而是"这对CP我磕定了"。
+
+---
+
+## 1. 男主人设(12 大经典类型)
+
+### 类型 1: 霸道总裁型
+
+**核心特质**:
+```
+外在: 年轻有为(28-35岁) + 身家过亿 + 颜值逆天
+内在: 外冷内热 + 占有欲强 + 只对女主温柔
+特殊标配: 洁癖/强迫症 + 不近女色 + 有童年创伤
+```
+
+**性格设定**:
+- **对外**: 冷酷无情、雷厉风行、说一不二
+- **对女主**: 宠溺包容、霸道温柔、患得患失
+- **弱点**: 怕女主离开、怕女主受伤、怕女主哭
+
+**经典台词模板**:
+```
+"你是我的,谁都抢不走。"
+"我可以给你全世界,但你不能离开我。"
+"除了你,我对所有女人都没兴趣。"(对女主说)
+"滚。"(对其他女人说)
+```
+
+**职业选择**:
+- **最常见**: CEO、集团董事长、投资公司总裁
+- **可用**: 医生(外科主任)、律师(合伙人)、军官(上校以上)
+- **禁忌**: 普通职员、销售员(破坏霸总人设)
+
+---
+
+### 类型 2: 高冷禁欲型
+
+**核心特质**:
+```
+外在: 清冷矜贵 + 不苟言笑 + 西装笔挺
+内在: 自律克制 + 理性至上 + 感情迟钝
+反差萌: 一旦动情 → 疯狂占有 + 失控偏执
+```
+
+**性格曲线**:
+```
+前期(1-30章): 冷漠疏离,对女主视而不见
+中期(30-80章): 开始注意女主,内心挣扎
+后期(80章+): 彻底沦陷,疯狂追求
+```
+
+**破防时刻**:
+```
+【第一次破防】
+他盯着女主的背影,第一次有了"留住她"的念头。
+(内心独白: 奇怪,为什么她离开时,我会觉得不安?)
+
+【彻底沦陷】
+"我以为自己不会爱上任何人。"
+他捧起她的脸,眼中满是深情。
+"直到遇见你,我才知道,我也会失控。"
+```
+
+**代表作**: 《何以笙箫默》何以琛、《微微一笑很倾城》肖奈
+
+---
+
+### 类型 3: 腹黑妖孽型
+
+**核心特质**:
+```
+外在: 妖孽俊美 + 邪魅一笑 + 行为不羁
+内在: 心机深沉 + 算无遗策 + 护短偏执
+特点: 表面玩世不恭,实则深情专一
+```
+
+**性格标签**:
+- 笑面虎(笑得越温柔,手段越狠)
+- 狐狸精(勾人不偿命)
+- 护妻狂魔(谁敢欺负女主,灭谁全家)
+
+**经典桥段**:
+```
+【腹黑设套】
+"你以为逃得掉吗?"
+他勾唇一笑,早已布下天罗地网。
+
+【护妻狂魔】
+"她是我的心尖宠,谁碰谁死。"
+他笑得温柔,眼中却是杀意。
+```
+
+---
+
+### 类型 4: 忠犬奶狗型
+
+**核心特质**:
+```
+外在: 阳光帅气 + 干净清爽 + 少年感
+内在: 忠诚专一 + 黏人撒娇 + 把女主当命
+年龄差: 比女主小 3-8 岁(姐弟恋)
+```
+
+**成长曲线**:
+```
+前期: 小奶狗(撒娇卖萌求抱抱)
+中期: 成长期(默默守护女主)
+后期: 大狼狗(占有欲爆棚,狠辣护妻)
+```
+
+**经典台词**:
+```
+"姐姐,我长大了,可以娶你了吗?"
+"我会一直等你,等到你愿意接受我。"
+"别怕,有我在。"(成长后)
+```
+
+**代表作**: 《致我们单纯的小美好》江辰
+
+---
+
+### 类型 5: 病娇偏执型(高危人设)
+
+**核心特质**:
+```
+外在: 病态俊美 + 阴郁气质 + 笑容诡异
+内在: 偏执占有 + 扭曲偏爱 + 疯狂危险
+危险指数: ★★★★★
+```
+
+**人设风险提示**:
+- ⚠️ **慎用**: 容易写成变态,劝退读者
+- ✅ **平衡**: 病娇 + 深情 = 危险的专一
+- ❌ **禁忌**: 病娇 + 暴力 = 恐怖片
+
+**可接受的病娇表现**:
+```
+【占有欲】
+"你只能看着我,只能想着我,只能属于我。"
+(锁定女主,不让她离开视线)
+
+【疯狂守护】
+"谁让你哭的?"
+他眼神阴冷,手中的刀滴着血。
+"告诉我,我帮你杀了他。"
+```
+
+**不可接受的病娇表现**(禁忌):
+```
+❌ 囚禁女主、虐待女主、强迫女主
+❌ 伤害女主身边的亲友(除非是坏人)
+❌ 以爱之名的控制与伤害
+```
+
+---
+
+### 类型 6: 温柔绅士型
+
+**核心特质**:
+```
+外在: 斯文优雅 + 温润如玉 + 举止得体
+内在: 温柔体贴 + 尊重女性 + 细心周到
+反差: 温柔外表下藏着强大内心
+```
+
+**适用场景**:
+- **前期配角 → 后期真爱**(女主先爱渣男,后遇良人)
+- **竹马竹马**(青梅竹马,默默守护)
+- **治愈系**(女主受伤后遇到的温暖)
+
+**经典桥段**:
+```
+【默默守护】
+"我会一直在这里等你,直到你愿意回头看我。"
+
+【温柔治愈】
+他轻轻擦去她的泪水:"别哭了,有我在。"
+```
+
+---
+
+### 类型 7: 禁忌系(师生/叔侄/上下级)
+
+**核心特质**:
+```
+身份差: 老师 vs 学生 / 叔叔 vs 侄女 / 上司 vs 下属
+年龄差: 8-15 岁
+禁忌感: 道德压力 + 世俗眼光 + 伦理禁忌
+```
+
+**情感线设计**:
+```
+阶段 1: 压抑克制(明知不可,却情难自禁)
+阶段 2: 挣扎矛盾(理智与感情的撕扯)
+阶段 3: 冲破禁忌(为了爱,不顾一切)
+```
+
+**经典台词**:
+```
+"我不该爱你,但我控制不住自己。"
+"就算全世界反对,我也要和你在一起。"
+```
+
+**注意事项**:
+- ✅ 女主必须成年(18岁+)
+- ✅ 感情发展必须自然,不能突兀
+- ❌ 禁止未成年恋情
+- ❌ 禁止真实血缘关系(养父女可以)
+
+---
+
+### 类型 8-12 速查表
+
+| 类型 | 核心特质 | 代表职业 | 经典台词 | 适用题材 |
+|------|---------|---------|---------|---------|
+| **军人硬汉** | 铁血柔情+保家卫国 | 特种兵/军官 | "我的命可以不要,但你必须活着" | 军旅言情 |
+| **古风王爷** | 腹黑权谋+帝王心术 | 王爷/皇帝 | "朕的江山,不如你" | 古言宫斗 |
+| **娱乐圈影帝** | 高冷禁欲+演技炸裂 | 演员/歌手 | "我的世界只有两个人,我和你" | 娱乐圈 |
+| **校园学霸** | 清冷矜贵+天才少年 | 学生 | "除了你,我对谁都没兴趣" | 校园甜文 |
+| **痞帅混混** | 痞坏不羁+护短偏执 | 街头老大 | "惹了我的人,天王老子也保不住你" | 黑道言情 |
+
+---
+
+## 2. 女主人设(10 大经典类型)
+
+### 类型 1: 傻白甜(新手最易上手)
+
+**核心特质**:
+```
+外在: 清纯可爱 + 软萌无害 + 惹人怜爱
+内在: 善良单纯 + 乐观开朗 + 没心机
+智商: 不蠢,但对感情迟钝
+```
+
+**性格标签**:
+- 路痴、吃货、怕黑、怕鬼
+- 容易被骗、容易相信人
+- 受欺负时会哭,但不记仇
+
+**经典桥段**:
+```
+【被欺负】
+她委屈地咬着唇,眼泪在眼眶里打转。
+"对不起……我不是故意的……"
+
+【被保护】
+男主一把将她揽入怀中:"别怕,有我在。"
+她终于忍不住哭出声:"呜呜呜……"
+```
+
+**适用题材**: 校园甜宠、霸总宠文、治愈系
+
+---
+
+### 类型 2: 软糯小白兔(最受欢迎)
+
+**核心特质**:
+```
+外在: 软萌柔弱 + 楚楚可怜 + 惹人疼
+内在: 坚强隐忍 + 善良温柔 + 不给人添麻烦
+特点: 受伤了也不说,默默忍受
+```
+
+**吸引男主的关键**:
+- **脆弱但坚强**: 哭着也要微笑
+- **柔软但有底线**: 可以欺负,但不能践踏尊严
+- **依赖但独立**: 需要保护,但不是废物
+
+**经典桥段**:
+```
+【隐忍】
+她躲在角落里,偷偷擦掉眼泪。
+"没事的,我可以的……"
+
+【被发现】
+男主看到她手上的伤,心疼到发疯。
+"为什么不告诉我?!"
+```
+
+---
+
+### 类型 3: 飒爽女强人(爽文必备)
+
+**核心特质**:
+```
+外在: 御姐范 + 气场强大 + 干练果决
+内在: 事业心强 + 独立自主 + 不依附男人
+反差: 对外女强人,对男主小女人
+```
+
+**职业选择**:
+- 女总裁、律师、医生、特工、设计师
+
+**情感线设计**:
+```
+前期: 事业为重,无视男主
+中期: 逐渐动心,但拒绝承认
+后期: 彻底沦陷,变成小女人
+```
+
+**经典桥段**:
+```
+【御姐范】
+她冷眼扫过众人:"不服?那就来试试。"
+
+【反差萌】
+她窝在他怀里撒娇:"老公,我想吃那个~"
+男主(震惊):"你……你刚才叫我什么?"
+```
+
+---
+
+### 类型 4: 腹黑小妖精
+
+**核心特质**:
+```
+外在: 妖娆魅惑 + 笑容狡黠 + 勾人不偿命
+内在: 聪明机智 + 不吃亏 + 护短偏执
+特点: 能屈能伸,软硬兼施
+```
+
+**性格曲线**:
+- **对敌人**: 笑面虎,笑得越甜,坑得越狠
+- **对男主**: 又甜又欲,撩完就跑
+- **对亲人**: 护犊子,谁碰谁死
+
+**经典桥段**:
+```
+【撩男主】
+她勾起唇角,凑到他耳边轻笑:"想我了吗?"
+男主(耳根泛红):"……别闹。"
+她笑着后退:"切,真无趣。"
+
+【护短】
+"你敢欺负我的人?"
+她笑得人畜无害,眼中却是杀意。
+```
+
+---
+
+### 类型 5: 重生复仇女(虐恋必备)
+
+**核心特质**:
+```
+前世: 被渣男贱女害死
+重生后: 复仇为主,爱情为辅
+性格变化: 从软弱 → 强势果敢
+```
+
+**复仇三部曲**:
+```
+阶段 1: 看清真相(前世的爱人是仇人)
+阶段 2: 步步为营(利用重生优势报仇)
+阶段 3: 大仇得报(虐死渣男贱女)
+```
+
+**与男主的关系**:
+- **前世男主**: 负心汉 → 后期追悔莫及(追妻火葬场)
+- **今生男主**: 新角色 → 帮女主复仇 → 相爱
+
+**经典台词**:
+```
+"这一世,我不会再爱你。"(对前世男主)
+"你欠我的,我要你十倍奉还!"(对仇人)
+```
+
+---
+
+### 类型 6-10 速查表
+
+| 类型 | 核心特质 | 性格标签 | 适用题材 | 代表作 |
+|------|---------|---------|---------|---------|
+| **乖巧学霸** | 成绩优异+温柔乖巧 | 邻家女孩、青梅 | 校园甜文 | 《致我们单纯的小美好》 |
+| **娇蛮千金** | 任性骄纵+刀子嘴豆腐心 | 被宠坏、有底线 | 豪门虐恋 | 《杉杉来吃》 |
+| **元气少女** | 活泼开朗+治愈系 | 小太阳、正能量 | 治愈甜宠 | 校园题材 |
+| **高冷女神** | 清冷疏离+不食人间烟火 | 高岭之花 | 破冰恋爱 | 娱乐圈题材 |
+| **逗比女神经** | 搞笑逗比+没心没肺 | 活宝、段子手 | 轻松搞笑 | 《微微一笑很倾城》 |
+
+---
+
+## 3. 人设搭配公式(黄金CP组合)
+
+### 公式 1: 强男 + 弱女(最经典)
+
+```
+霸道总裁 + 傻白甜 = 保护欲爆棚 + 宠溺
+```
+
+**优点**: 读者代入感强,满足保护欲与被保护欲  
+**缺点**: 容易写俗套  
+**创新**: 给女主隐藏实力(表面软萌,实则大佬)
+
+---
+
+### 公式 2: 强女 + 更强男(势均力敌)
+
+```
+女强人 + 高冷禁欲男 = 棋逢对手 + 互相征服
+```
+
+**优点**: CP感强,平等恋爱  
+**缺点**: 不够甜宠  
+**创新**: 加入"谁先动心谁输"的赌约
+
+---
+
+### 公式 3: 腹黑 + 腹黑(斗智斗勇)
+
+```
+腹黑男 + 腹黑女 = 尔虞我诈 + 深情虐恋
+```
+
+**优点**: 剧情精彩,虐恋刺激  
+**缺点**: 写作难度高  
+**适用**: 宫斗、权谋、商战题材
+
+---
+
+### 公式 4: 年下 + 成熟女(姐弟恋)
+
+```
+忠犬奶狗 + 御姐/软妹 = 反差萌 + 宠溺
+```
+
+**优点**: 新颖,满足姐姐粉的幻想  
+**缺点**: 受众相对小众  
+**创新**: 小奶狗成长为大狼狗
+
+---
+
+### 公式 5: 病娇 + 治愈系(高危组合)
+
+```
+病娇偏执男 + 温柔治愈女 = 危险的救赎
+```
+
+**优点**: 刺激,暗黑系读者最爱  
+**缺点**: 容易翻车,写成恐怖片  
+**平衡**: 病娇只对女主温柔,对其他人冷酷
+
+---
+
+## 4. 人设禁忌(绝不能犯)
+
+### 禁忌 1: 圣母女主
+
+**表现**:
+```
+❌ 被欺负还帮仇人说话:"他也是有苦衷的……"
+❌ 被渣男抛弃还原谅:"我相信他是爱我的……"
+❌ 被害还不报仇:"算了,过去的事就过去吧……"
+```
+
+**读者反应**: "这女主有病吧?!气死我了!"
+
+**正确做法**: 善良可以,但**必须有底线**。
+
+---
+
+### 禁忌 2: 渣男主(不洗白)
+
+**表现**:
+```
+❌ 劈腿出轨还找借口:"我只是一时糊涂……"
+❌ 虐待女主还不道歉:"你活该被我虐……"
+❌ 伤害女主后不追悔:"她离开就离开吧,无所谓……"
+```
+
+**读者反应**: "这男主活该单身一辈子!"
+
+**正确做法**: 
+- **虐可以,但必须追妻火葬场**
+- **伤害后必须疯狂追悔**
+- **最后必须跪地求原谅**
+
+---
+
+### 禁忌 3: 智障配角(工具人)
+
+**表现**:
+```
+❌ 反派智商为负,被主角轻松碾压
+❌ 配角只会喊"你好厉害啊!"
+❌ 所有人都围着主角转
+```
+
+**读者反应**: "配角都是智障吗?"
+
+**正确做法**: **配角也要有智商、有动机、有血有肉**
+
+---
+
+## 5. 人设创新技巧
+
+### 技巧 1: 反转经典人设
+
+```
+霸道总裁 → 其实是演的,真实性格是宅男
+傻白甜 → 其实是装的,真实身份是特工
+高冷男主 → 其实是社恐,不会和人相处
+```
+
+---
+
+### 技巧 2: 叠加多重身份
+
+```
+女主 = 学霸 + 黑客 + 格斗高手 + 隐藏大小姐
+男主 = 霸总 + 杀手 + 豪门继承人 + 前特种兵
+```
+
+**注意**: 不要堆砌太多,3-4 个身份即可
+
+---
+
+### 技巧 3: 给经典人设加缺陷
+
+```
+霸道总裁 + 社恐(只对女主不社恐)
+傻白甜 + 路痴(严重到离谱)
+高冷男主 + 洁癖(只让女主碰)
+```
+
+**作用**: 增加反差萌,更真实
+
+---
+
+## 6. 人设塑造自检清单
+
+在设计人设时,逐项检查:
+
+- [ ] **有吸引力吗**: 读者会喜欢这个角色吗?
+- [ ] **有反差吗**: 性格是否有多面性?
+- [ ] **有成长吗**: 角色是否会随剧情成长?
+- [ ] **有底线吗**: 善良但不圣母,腹黑但不变态?
+- [ ] **符合逻辑吗**: 性格、行为、职业是否一致?
+- [ ] **够独特吗**: 和其他小说的角色有区别吗?
+
+---
+
+## 🛠️ 人设速查表
+
+| 男主类型 | 核心特质 | 代表台词 | 适配女主 | 适用题材 |
+|---------|---------|---------|---------|---------|
+| **霸道总裁** | 强势宠溺 | "你是我的" | 傻白甜/软糯 | 现言宠文 |
+| **高冷禁欲** | 理性克制 | "我不该爱你" | 主动热情型 | 破冰恋爱 |
+| **腹黑妖孽** | 心机深沉 | "你逃不掉的" | 腹黑/软萌 | 宫斗权谋 |
+| **忠犬奶狗** | 专一黏人 | "姐姐,我长大了" | 御姐/成熟 | 姐弟恋 |
+| **病娇偏执** | 疯狂占有 | "你只能是我的" | 治愈温柔 | 暗黑虐恋 |
+
+| 女主类型 | 核心特质 | 性格标签 | 适配男主 | 适用题材 |
+|---------|---------|---------|---------|---------|
+| **傻白甜** | 单纯善良 | 软萌可爱 | 霸道总裁 | 校园甜宠 |
+| **软糯小白兔** | 坚强隐忍 | 楚楚可怜 | 强势保护型 | 治愈宠文 |
+| **飒爽女强人** | 独立自主 | 事业为重 | 高冷禁欲 | 职场言情 |
+| **腹黑小妖精** | 聪明狡黠 | 不吃亏 | 腹黑/霸道 | 爽文虐恋 |
+| **重生复仇** | 果敢狠辣 | 复仇为先 | 忠犬/腹黑 | 重生虐渣 |
+
+---
+
+## 附录:人设案例库
+
+### 案例 1: 《何以笙箫默》何以琛
+
+**人设**: 高冷禁欲型 + 专一深情  
+**经典台词**: "如果世界上曾经有那个人出现过,其他人都会变成将就。"  
+**成功原因**: 反差萌(外表高冷,内心深情)+ 漫长等待(7年)
+
+---
+
+### 案例 2: 反面教材(某扑街文)
+
+**人设失败**:
+```
+男主: 霸道总裁,但没钱没势,只会骂女主
+女主: 傻白甜,但蠢到无药可救,被骗 800 次还不长记性
+配角: 都是脸谱化工具人,智商为负
+```
+
+**问题**: 人设空洞、不合逻辑、惹人厌烦

+ 578 - 0
.claude/genres/dog-blood-romance/emotional-tension.md

@@ -0,0 +1,578 @@
+# 情感张力技巧 (Emotional Tension)
+
+> **核心原则**: 言情小说的灵魂在于情感张力。没有张力 = 没有吸引力。读者追文的动力来自"想知道他们什么时候在一起"。
+
+---
+
+## 1. 情感张力的四大来源
+
+### 来源 1: 身份差距(地位悬殊)
+
+**核心公式**:
+```
+巨大身份差 → 不对等关系 → 女主自卑/男主傲慢 → 情感张力
+```
+
+**经典身份差组合**:
+| 男主身份 | 女主身份 | 张力点 |
+|---------|---------|--------|
+| 亿万总裁 | 普通职员 | 金钱/地位差距 |
+| 娱乐圈影帝 | 十八线小透明 | 名气/资源差距 |
+| 豪门继承人 | 灰姑娘 | 阶级/家世差距 |
+| 古代王爷 | 平民女子 | 权力/身份差距 |
+| 天才学霸 | 学渣 | 智商/成就差距 |
+
+**示例场景**:
+```
+【身份差导致的卑微】
+"林先生,您的咖啡。"苏念低着头,不敢看他。
+她只是一个普通秘书,而他是商业帝国的太子爷。
+她连仰望的资格都没有……
+
+【男主因身份差的强势】
+"苏念,你以为你配得上我?"
+林慕寒冷笑,眼中满是轻蔑。
+```
+
+**张力爆发点**: 当女主因身份差距想要退缩,男主霸道表白:"我不在乎你是谁,我只在乎你是你。"
+
+---
+
+### 来源 2: 误会隔阂(信息差)
+
+**核心公式**:
+```
+关键误会 → 双方都不解释 → 越陷越深 → 爆发冲突 → 真相揭开
+```
+
+**经典误会类型**:
+
+#### 类型 A: 出轨误会
+```
+【表象】女主看到男主和别的女人暧昧
+【真相】那女人是男主妹妹/表妹/救命恩人
+【冲突】女主伤心欲绝,提出分手
+【爆发】真相大白,男主追妻火葬场
+```
+
+#### 类型 B: 背叛误会
+```
+【表象】女主被陷害,男主以为她背叛了他
+【真相】女主是被迫/被陷害的
+【冲突】男主冷暴力/报复女主
+【爆发】真相揭露,男主悔恨万分
+```
+
+#### 类型 C: 身份误会
+```
+【表象】女主以为男主是穷小子,男主以为女主是拜金女
+【真相】双方都隐瞒了真实身份
+【冲突】身份曝光后的尴尬与试探
+【爆发】坦诚相待,感情升华
+```
+
+**示例场景**:
+```
+【误会产生】
+苏念看到林慕寒和一个漂亮女人亲密地走在一起。
+那女人挽着他的手臂,笑得甜蜜。
+她的心,碎了一地。
+
+【误会加深】
+"林慕寒,我们分手吧。"
+"为什么?"
+"因为……我配不上你。"(真实原因:以为他有别的女人)
+
+【真相揭晓】
+"她是我妹妹!我唯一的妹妹!"
+林慕寒抓住她的肩膀,眼中满是急切。
+"苏念,你怎么能因为这个误会就要离开我?"
+```
+
+---
+
+### 来源 3: 禁忌关系(道德压力)
+
+**核心公式**:
+```
+禁忌身份 → 不能在一起 → 压抑克制 → 情难自禁 → 突破禁忌
+```
+
+**经典禁忌类型**:
+| 禁忌类型 | 关系设定 | 张力来源 |
+|---------|---------|---------|
+| **师生恋** | 老师 × 学生 | 伦理道德/年龄差 |
+| **上下级** | 老板 × 员工 | 职场规则/权力不对等 |
+| **世仇家族** | 罗密欧与朱丽叶式 | 家族仇恨/外界压力 |
+| **姐弟恋** | 姐姐 × 弟弟 | 社会偏见/年龄差 |
+| **医患** | 医生 × 病人 | 职业道德 |
+
+**示例场景**:
+```
+【禁忌压抑】
+"不行,我是你的老师……"
+他推开她,眼中满是挣扎。
+"这违背师德,我不能……"
+
+【情难自禁】
+但她靠近,他的理智就崩塌了。
+"苏念……别这样……"
+他的声音颤抖,手却紧紧抱住了她。
+
+【突破禁忌】
+"管他什么师德!我只知道我爱你!"
+他俯身吻住她的唇。
+```
+
+---
+
+### 来源 4: 第三者插足(外部威胁)
+
+**核心公式**:
+```
+第三者出现 → 威胁女主地位 → 女主危机感 → 男主表态 → 感情稳固
+```
+
+**第三者类型**:
+| 类型 | 设定 | 作用 |
+|------|------|------|
+| **白月光** | 男主初恋/前女友 | 制造女主的不安全感 |
+| **门当户对** | 豪门千金 | 家族施压,逼男主娶她 |
+| **青梅竹马** | 从小一起长大 | 有共同回忆,女主吃醋 |
+| **暗恋者** | 默默守护女主的人 | 让男主吃醋,意识到女主的好 |
+
+**示例场景**:
+```
+【第三者登场】
+"慕寒哥哥,好久不见~"
+一个精致的女人走进来,亲昵地挽住林慕寒的手臂。
+苏念的心,揪了起来。
+
+【女主危机感】
+"她是你的初恋吧?"
+"……是。"
+"那你们……"
+"过去的事了。"林慕寒淡淡地说。
+但苏念还是不安……
+
+【男主表态】
+林慕寒当着所有人的面,牵起苏念的手。
+"我只爱她。过去的,就让它过去吧。"
+```
+
+---
+
+## 2. 情感张力的强度分级
+
+### 1级张力(日常小摩擦)
+
+**特点**: 轻微吃醋、小矛盾、小误会
+
+**示例**:
+```
+"你为什么要和那个女生说话?"苏念噘嘴。
+"她问我路……"林慕寒无奈。
+"哼,不许和别的女生说话!"
+"好好好,我的错。"他宠溺地揉她的头。
+```
+
+**作用**: 调节气氛,展现男主宠溺。
+
+---
+
+### 2级张力(明显冲突)
+
+**特点**: 冷战、赌气、小分手
+
+**示例**:
+```
+"苏念,你别无理取闹!"
+"我无理取闹?那你去找你的白月光吧!"
+"你……!"
+苏念摔门而出。
+林慕寒站在原地,懊恼地扯了扯领带。
+```
+
+**作用**: 制造波折,让感情升温。
+
+---
+
+### 3级张力(严重危机)
+
+**特点**: 分手、背叛、误会爆发
+
+**示例**:
+```
+"林慕寒,我们分手吧。"
+"为什么?!"
+"因为……我们不合适。"
+"不合适?你昨天还说爱我!"
+"我……我不爱了。"
+她转身离去,留下他一人站在雨中。
+```
+
+**作用**: 情节高潮,虐心虐肺。
+
+---
+
+### 4级张力(生死相隔)
+
+**特点**: 意外、疾病、生离死别
+
+**示例**:
+```
+"林慕寒……如果我死了……你会想我吗?"
+"别说傻话!你不会有事的!"
+他紧紧抱住她,声音颤抖。
+"医生!医生!求你救救她!"
+```
+
+**作用**: 极致虐点,情感爆发。
+
+---
+
+## 3. 情感张力的节奏控制
+
+### 基础节奏公式
+
+```
+甜(5章)→ 虐(3章)→ 甜(5章)→ 虐(5章)→ 甜(10章)
+```
+
+**原则**: 甜虐交替,虐完必甜,虐不过三。
+
+---
+
+### 错误节奏(禁忌)
+
+#### 禁忌 1: 一直甜(太腻)
+```
+第1-50章: 男主对女主各种宠
+问题: 读者会腻,没有追文动力
+```
+
+#### 禁忌 2: 一直虐(太苦)
+```
+第1-50章: 误会、背叛、分手、虐心
+问题: 读者会弃文,"看不下去了"
+```
+
+#### 禁忌 3: 虐而不解(憋屈)
+```
+第10章: 男主误会女主
+第20章: 还在误会
+第30章: 继续误会
+问题: 读者会骂:"真相什么时候大白?!"
+```
+
+**正确做法**: 虐点不要拖太久,最多10-15章就要解开。
+
+---
+
+## 4. 制造情感张力的具体技巧
+
+### 技巧 1: 欲擒故纵(拉扯感)
+
+**公式**:
+```
+男主靠近 → 女主退后 → 男主追 → 女主逃 → 男主霸道:"别跑!"
+```
+
+**示例**:
+```
+【女主躲避】
+苏念看到林慕寒,转身就走。
+"苏念!"他叫住她。
+"林先生,有事吗?"她冷淡地问。
+
+【男主追击】
+林慕寒大步走过来,将她困在墙角。
+"躲什么?我又不会吃了你。"
+"请你自重……"
+"自重?"他冷笑,"你以为你逃得掉?"
+```
+
+---
+
+### 技巧 2: 若即若离(暧昧感)
+
+**公式**:
+```
+不是恋人 → 但亲密接触 → 双方心动 → 但都不表白 → 读者着急
+```
+
+**示例**:
+```
+【亲密但不表白】
+林慕寒帮她擦掉嘴角的酱汁。
+"吃慢点,又没人跟你抢。"
+苏念的脸红了。
+他……这是什么意思?
+但他什么都没说,只是笑了笑。
+
+【暧昧气氛】
+"苏念,你……有男朋友吗?"
+"没……没有……"
+"那就好。"
+"什么?"
+"没什么。"他转身离开。
+```
+
+**读者反应**: "啊啊啊快表白啊!急死我了!"
+
+---
+
+### 技巧 3: 先抑后扬(反差感)
+
+**公式**:
+```
+前期虐 → 女主绝望 → 男主突然温柔 → 情感爆发
+```
+
+**示例**:
+```
+【前期冷漠】
+"林慕寒,你能不能对我好一点……"
+"为什么?"他冷冷地问。
+"因为……算了,当我没说。"
+
+【突然温柔】
+那天她发高烧,他整夜守在床边。
+"傻瓜……"他低声说,"以后别让我担心了。"
+苏念睁开眼,看到的是他红肿的眼睛。
+她的泪,滑落了。
+```
+
+---
+
+### 技巧 4: 借第三者(吃醋感)
+
+**公式**:
+```
+第三者出现 → 男主/女主吃醋 → 争风吃醋 → 确认感情
+```
+
+**示例**:
+```
+【女主吃醋】
+"慕寒,这是我特意为你做的便当~"
+那女人笑得甜蜜,将便当递给林慕寒。
+苏念在一旁,心如刀割。
+
+【男主表态】
+林慕寒看都没看那便当一眼,转身走向苏念。
+"我只吃她做的。"
+他拿起苏念的便当,当着所有人的面吃了起来。
+```
+
+---
+
+## 5. 情感爆发点设计
+
+### 爆发点 1: 表白(甜)
+
+**时机**: 经历误会/磨难后,男主终于说出心里话
+
+**示例**:
+```
+"苏念,我爱你。"
+"什么……?"
+"从见到你的第一天起,我就爱上你了。"
+他将她拥入怀中。
+"对不起,让你受委屈了。"
+```
+
+---
+
+### 爆发点 2: 分手(虐)
+
+**时机**: 误会达到顶峰,女主心灰意冷
+
+**示例**:
+```
+"林慕寒,我们分手吧。"
+她的声音很平静,但眼中已无光。
+"不……不要……"他慌了,"我可以解释……"
+"不必了。我累了。"
+她转身离开,头也不回。
+```
+
+---
+
+### 爆发点 3: 生死关头(极致虐)
+
+**时机**: 意外/疾病,面临生死抉择
+
+**示例**:
+```
+"林慕寒……我可能……活不了了……"
+"别说傻话!你一定会好起来的!"
+"如果……我死了……你会记得我吗?"
+"我会!我会一辈子记得你!"
+他的泪水滴在她脸上。
+"所以……你一定要活下去……一定要……"
+```
+
+---
+
+### 爆发点 4: 追妻火葬场(反转甜)
+
+**时机**: 男主意识到错误,疯狂追回女主
+
+**示例**:
+```
+"苏念,求你原谅我……"
+林慕寒跪在雨中,眼中满是悔恨。
+"当初是我瞎了眼,是我不信你……"
+"求你……再给我一次机会……"
+苏念转过身,泪流满面。
+"林慕寒……我恨你……"
+"我知道……所以让我用一辈子来赎罪……"
+```
+
+---
+
+## 6. 情感张力的对话技巧
+
+### 技巧 A: 欲言又止(暧昧)
+
+```
+"苏念,我……"
+"嗯?"
+"……没什么。"
+他到嘴边的话,又咽了回去。
+```
+
+**作用**: 制造悬念,让读者猜测他想说什么。
+
+---
+
+### 技巧 B: 话中有话(试探)
+
+```
+"苏念,你会离开我吗?"
+"为什么这么问?"
+"如果……有一天我什么都没有了……你还会爱我吗?"
+"……会。"
+"真的?"
+"真的。"
+他紧紧抱住她,眼中闪过一丝不易察觉的痛苦。
+```
+
+**作用**: 暗示危机,埋伏笔。
+
+---
+
+### 技巧 C: 反问回怼(拉扯)
+
+```
+"你是不是喜欢我?"
+"你说呢?"
+"我问你呢!"
+"那你呢?你喜欢我吗?"
+"我……"
+"不说就算了。"
+"喂!你别走啊!"
+```
+
+**作用**: 制造拉扯感,增加互动趣味。
+
+---
+
+## 7. 避免情感张力的常见错误
+
+### 错误 1: 无病呻吟
+
+**表现**:
+```
+"我好难过……"
+"为什么难过?"
+"就是难过……"
+"到底为什么?"
+"说不清楚……"
+```
+
+**问题**: 没有明确的冲突点,读者不知道为什么难过。
+
+**正确做法**: 给出明确的原因(误会/背叛/分离等)。
+
+---
+
+### 错误 2: 为虐而虐
+
+**表现**:
+```
+第10章: 男主误会女主出轨 → 冷暴力
+第15章: 女主证明清白 → 和好
+第20章: 又误会女主背叛 → 又冷暴力
+第25章: 又证明清白 → 又和好
+```
+
+**问题**: 重复套路,读者会腻。
+
+**正确做法**: 每次虐点要有新意,不要重复。
+
+---
+
+### 错误 3: 男主渣而不洗白
+
+**表现**:
+```
+男主一直对女主冷暴力/出轨/家暴
+直到最后都没有真心悔改
+```
+
+**问题**: 读者会恨男主,弃文。
+
+**正确做法**: 虐可以,但男主必须悔改,追妻火葬场。
+
+---
+
+## 8. 情感张力自检清单
+
+- [ ] **有明确的冲突点**: 身份差/误会/禁忌/第三者?
+- [ ] **节奏合理**: 甜虐交替,虐不过三?
+- [ ] **张力递进**: 从小摩擦到大危机?
+- [ ] **有爆发点**: 表白/分手/生死/追妻?
+- [ ] **符合人设**: 角色的反应符合其性格?
+- [ ] **不重复**: 每次虐点有新意?
+
+---
+
+## 🛠️ 情感张力速查表
+
+| 张力类型 | 核心矛盾 | 持续时长 | 解决方式 | 爽点 |
+|---------|---------|---------|---------|------|
+| **身份差** | 地位悬殊 | 长期 | 男主表态"我不在乎" | 霸道宠溺 |
+| **误会** | 信息差 | 5-15章 | 真相揭晓 | 追妻火葬场 |
+| **禁忌** | 道德压力 | 长期 | 突破禁忌 | 禁忌之恋 |
+| **第三者** | 外部威胁 | 3-10章 | 男主表态 | 吃醋撒娇 |
+| **冷战** | 小矛盾 | 1-3章 | 男主主动和好 | 宠溺哄人 |
+| **分手** | 严重误会 | 10-20章 | 追妻火葬场 | 虐后反转 |
+
+---
+
+## 附录:经典情感张力案例
+
+### 案例 1: 《何以笙箫默》身份差 + 误会
+
+**张力点**:
+- 身份差: 律师精英 vs 普通女孩
+- 误会: 女主以为男主不爱她,主动离开
+- 分离: 7年后重逢
+
+**高潮**:
+"等了七年,你终于回来了。"
+何以琛紧紧抱住赵默笙。
+
+---
+
+### 案例 2: 反面教材(某扑街文)
+
+```
+男主一直虐女主,从头虐到尾。
+女主被虐得死去活来,男主还是不悔改。
+最后女主原谅了男主,两人在一起了。
+```
+
+**问题**: 男主没有成长,女主太圣母,读者不买账。

+ 213 - 0
.claude/genres/dog-blood-romance/plot-templates.md

@@ -0,0 +1,213 @@
+# 狗血言情剧情模板库
+
+> 本文档提供经过验证的狗血言情剧情模板,涵盖长篇、中篇、短篇不同体量,可直接套用或组合使用。
+
+---
+
+## 一、长篇模板(80-150万字)
+
+### 模板1:霸总追妻火葬场
+
+**核心公式**:`前期虐女主 + 误会分离 + 男主追悔 + 漫长追妻 + HE`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 卷一:甜蜜假象 | 15% | 契约/利益结合,女主隐忍,男主冷漠 |
+| 卷二:虐心深渊 | 25% | 白月光出现,误会加深,女主心死 |
+| 卷三:决裂离开 | 15% | 真相部分揭露,女主离开,男主震惊 |
+| 卷四:追妻之路 | 30% | 男主追悔,女主成长,反复拉扯 |
+| 卷五:破镜重圆 | 15% | 真相大白,男主赎罪,HE |
+
+**关键节点**:
+```
+开篇钩子:女主签下契约/协议结婚(展示不平等地位)
+第一转折:白月光归来,男主态度骤变
+虐心高潮:女主小产/重病/被陷害,男主不信任
+决裂点:女主心死离开(带球跑/净身出户/假死)
+追妻起点:男主发现真相,开始追悔
+追妻高潮:女主事业有成/有新欢,男主卑微求复合
+黑暗时刻:旧敌反扑/新危机,男主舍命相护
+大结局:真相彻底揭露,男主完成救赎,HE
+```
+
+---
+
+### 模板2:先婚后爱
+
+**核心公式**:`利益联姻 + 冷漠同居 + 日久生情 + 误会危机 + 甜蜜HE`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 卷一:陌生夫妻 | 20% | 联姻原因,同居磨合,互相试探 |
+| 卷二:暗生情愫 | 25% | 日常相处,渐生好感,不自知的在意 |
+| 卷三:情感确认 | 20% | 一方先动心,试探表白,关系升温 |
+| 卷四:危机考验 | 20% | 外部威胁/误会,感情受考验 |
+| 卷五:甜蜜日常 | 15% | 危机解除,撒糖日常,HE |
+
+**关键节点**:
+```
+开篇钩子:联姻原因揭示(家族利益/报恩/意外)
+破冰时刻:一次意外合作/共同经历
+心动瞬间:不经意的关心被对方察觉
+嫉妒桥段:第三者出现,引发占有欲
+表白时刻:一方坦诚心意
+甜蜜期:婚后恋爱,补办仪式感
+危机点:旧事重提/家族反对/误会
+大结局:携手面对,感情升华
+```
+
+---
+
+### 模板3:重生复仇
+
+**核心公式**:`惨死重生 + 步步为营 + 手撕仇人 + 收获真爱 + 圆满结局`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 卷一:浴火重生 | 15% | 前世惨状回顾,重生节点,复仇规划 |
+| 卷二:布局落子 | 25% | 改变命运,积蓄力量,小试牛刀 |
+| 卷三:步步紧逼 | 25% | 逐个击破仇人,真相逐渐揭露 |
+| 卷四:终极对决 | 20% | 大Boss现身,最终决战 |
+| 卷五:新生圆满 | 15% | 仇人伏法,收获幸福 |
+
+**关键节点**:
+```
+开篇钩子:前世惨死场景(震撼开局)
+重生时刻:回到关键节点,立下复仇誓言
+第一次反击:小胜仇人,展示重生优势
+遇见男主:命运改变,新的羁绊
+中期高潮:揭露一个重大真相
+黑暗时刻:仇人反扑,陷入危机
+最终决战:手撕大Boss
+大结局:仇人伏法,与男主HE
+```
+
+---
+
+## 二、中篇模板(30-60万字)
+
+### 模板4:娱乐圈甜宠
+
+**核心公式**:`隐婚设定 + 事业线交织 + 撒糖日常 + 公开撒狗粮`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 卷一:秘密关系 | 30% | 隐婚原因,偷偷约会,差点暴露 |
+| 卷二:事业危机 | 35% | 绯闻/黑料,携手应对,感情加深 |
+| 卷三:高调官宣 | 35% | 公开关系,打脸黑子,甜蜜日常 |
+
+**关键节点**:
+```
+开篇钩子:隐婚身份差点暴露的惊险场面
+甜蜜日常:片场探班/私下约会
+事业高光:女主作品大爆/获奖
+危机时刻:恶意绯闻/前任纠缠
+男主护妻:霸气回应/资源支持
+官宣时刻:高调公开,全网震惊
+撒糖结局:婚礼/孕事/日常甜
+```
+
+---
+
+### 模板5:豪门恩怨
+
+**核心公式**:`身世之谜 + 家族斗争 + 真爱相守 + 夺回一切`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 卷一:身世迷雾 | 30% | 真假千金,身份疑云,初入豪门 |
+| 卷二:明争暗斗 | 40% | 家族内斗,阴谋陷害,步步惊心 |
+| 卷三:真相大白 | 30% | 身世揭晓,夺回一切,HE |
+
+---
+
+## 三、短篇模板(10-30万字)
+
+### 模板6:破镜重圆
+
+**核心公式**:`分手多年 + 意外重逢 + 旧情复燃 + 解开心结`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 前期 | 35% | 重逢场景,回忆杀,表面疏离 |
+| 中期 | 40% | 被迫接触,旧情复燃,误会揭开 |
+| 后期 | 25% | 坦诚相对,破镜重圆 |
+
+---
+
+### 模板7:契约恋爱
+
+**核心公式**:`假戏 + 日久生情 + 真心告白`
+
+**章节分配**:
+| 阶段 | 字数占比 | 核心内容 |
+|------|---------|---------|
+| 前期 | 30% | 契约原因,规则制定,开始演戏 |
+| 中期 | 45% | 假戏真做,心动瞬间,患得患失 |
+| 后期 | 25% | 契约到期,真心告白,HE |
+
+---
+
+## 四、经典剧情节点库
+
+### 开篇钩子
+| 类型 | 示例 | 适用模板 |
+|------|------|---------|
+| 冲突开场 | 婚礼上被抛弃/当众羞辱 | 重生、追妻 |
+| 悬念开场 | 醒来发现怀孕/失忆 | 先婚后爱、豪门 |
+| 反转开场 | 以为是灰姑娘实则大佬 | 甜宠、豪门 |
+| 重逢开场 | 多年后意外相遇 | 破镜重圆 |
+
+### 虐心名场面
+| 场景 | 情绪值 | 使用注意 |
+|------|--------|---------|
+| 雨中分手 | ★★★★★ | 需要足够铺垫 |
+| 病床守候 | ★★★★☆ | 不宜过长 |
+| 误会目睹 | ★★★★☆ | 需要合理解释 |
+| 被迫分离 | ★★★★★ | 外力因素要充分 |
+
+### 甜蜜名场面
+| 场景 | 甜度 | 适用时机 |
+|------|------|---------|
+| 吃醋宣誓主权 | ★★★★☆ | 感情确认后 |
+| 霸道壁咚 | ★★★☆☆ | 暧昧期 |
+| 婚礼/求婚 | ★★★★★ | 结局或重要节点 |
+| 孕期宠溺 | ★★★★☆ | 番外或结局 |
+
+---
+
+## 五、模板使用指南
+
+### 四步套用法
+
+1. **选择基础模板**:根据预计字数选择长/中/短篇模板
+2. **填充人设**:套入角色设定(参考 character-archetypes.md)
+3. **调整节点**:根据需要增删剧情节点
+4. **添加特色**:加入独特设定或反转
+
+### 模板组合技巧
+
+| 组合方式 | 示例 | 效果 |
+|---------|------|------|
+| 主线+副线 | 追妻火葬场 + 商战 | 增加厚度 |
+| 双重身份 | 先婚后爱 + 娱乐圈 | 增加看点 |
+| 时间线交错 | 重生 + 破镜重圆 | 增加层次 |
+
+---
+
+## 六、常见问题
+
+**Q:模板会不会写出来很套路?**
+A:模板是骨架,血肉靠细节。同样的追妻模板,人设、金句、名场面不同,效果天差地别。
+
+**Q:可以混用多个模板吗?**
+A:可以,但要有主次。建议一个主模板+1-2个元素借鉴,避免结构混乱。
+
+**Q:字数分配必须严格遵守吗?**
+A:可以根据实际情况调整±5%,但大体比例要保持,避免头重脚轻或虎头蛇尾。

+ 574 - 0
.claude/genres/dog-blood-romance/romance-pacing.md

@@ -0,0 +1,574 @@
+# 言情节奏与章节结构 (Romance Pacing & Chapter Structure)
+
+> **核心原则**: 言情小说的节奏要"快慢结合"。感情线要层层递进,既不能太快(第1章就在一起),也不能太慢(50章还在暧昧)。每章要有明确的情感推进或冲突爆发。
+
+---
+
+## 1. 言情小说的三大节奏类型
+
+### 类型 1: 快节奏言情(爽文型)
+
+**特点**: 开局即暧昧,10-20章确认关系,主打甜宠
+
+**节奏表**:
+```
+第1-5章: 相遇,一见钟情/日久生情苗头
+第6-10章: 暧昧升温,肢体接触增加
+第11-15章: 告白,确认关系
+第16-30章: 甜蜜日常 + 小误会小虐
+第31-40章: 大危机(第三者/家族反对)
+第41-50章: 解决危机,结婚/订婚
+```
+
+**适用**: 短篇甜文、快节奏爽文
+
+**示例**:
+```
+第1章: 女主撞到男主怀里(一见钟情)
+第2章: 男主霸道留下女主联系方式
+第5章: 男主开始追求女主
+第10章: 男主壁咚告白
+第15章: 确认关系,公开
+```
+
+---
+
+### 类型 2: 中速节奏言情(标准型)
+
+**特点**: 20-40章确认关系,甜虐交替,主打情感深度
+
+**节奏表**:
+```
+第1-10章: 相遇,建立联系,初步好感
+第11-20章: 暧昧期,试探,心动
+第21-30章: 小误会,分分合合
+第31-40章: 告白,确认关系
+第41-60章: 甜蜜期 + 外部危机
+第61-80章: 大危机,分手
+第81-100章: 追妻火葬场,和好,结婚
+```
+
+**适用**: 中长篇言情,情感细腻型
+
+**示例**:
+```
+第1-10章: 相遇,从陌生到熟悉
+第20章: 女主意识到自己心动了
+第30章: 男主表白,女主犹豫
+第40章: 确认关系
+第60章: 第三者出现,危机
+第80章: 分手
+第100章: 和好,结婚
+```
+
+---
+
+### 类型 3: 慢节奏言情(虐文型)
+
+**特点**: 50章+才确认关系,主打虐恋情深
+
+**节奏表**:
+```
+第1-20章: 相遇,误会,敌对或冷漠
+第21-40章: 被迫接触,逐渐了解
+第41-60章: 暗生情愫,但不自知
+第61-80章: 意识到爱情,但有障碍
+第81-100章: 克服障碍,告白
+第101-120章: 确认关系,甜蜜补偿
+```
+
+**适用**: 长篇虐文、先虐后甜型
+
+**示例**:
+```
+第1-30章: 男主误会女主,冷暴力
+第40章: 女主离开,男主后悔
+第60章: 追妻火葬场
+第80章: 真相大白
+第100章: 表白,确认关系
+第120章: 结婚,超甜补偿
+```
+
+---
+
+## 2. 感情线的五大阶段
+
+### 阶段 1: 相遇期(建立联系)
+
+**目标**: 让男女主产生联系,埋下感情伏笔
+
+**章节分配**: 1-5章
+
+**必备元素**:
+- 初次见面(印象深刻)
+- 建立联系(互留联系方式/成为同事/邻居等)
+- 埋伏笔(男主对女主有不同的反应)
+
+**节奏要点**:
+```
+第1章: 相遇(意外/救命/撞见/被迫相亲)
+第2-3章: 建立联系(成为同事/邻居/假扮情侣)
+第4-5章: 初步好感(男主对女主有特殊关注)
+```
+
+**示例场景**:
+```
+【第1章:意外相遇】
+苏念在电梯里,突然停电。
+黑暗中,她撞进一个温暖的怀抱。
+"抱歉……"她慌忙退开。
+电梯恢复,灯光亮起。
+她看清了那个人——高大、英俊、冷峻。
+那是她的新老板,林慕寒。
+
+【第3章:建立联系】
+"苏念,你是我的秘书。"
+林慕寒看着她,眼中闪过一丝不易察觉的兴趣。
+```
+
+---
+
+### 阶段 2: 暧昧期(试探心意)
+
+**目标**: 双方互相试探,暧昧升温,但不表白
+
+**章节分配**: 6-20章(可调整)
+
+**必备元素**:
+- 肢体接触增加(牵手、拥抱)
+- 吃醋场景(男主/女主吃醋)
+- 若即若离(欲擒故纵)
+- 心理活动(意识到心动但不敢确认)
+
+**节奏要点**:
+```
+每3-5章一个小甜蜜高潮:
+- 第一次牵手
+- 第一次被壁咚
+- 第一次吃醋
+- 第一次亲密接触(拥抱/额头相抵)
+```
+
+**示例场景**:
+```
+【第8章:第一次牵手】
+人群拥挤,林慕寒一把握住她的手。
+"别走丢了。"他淡淡地说。
+苏念的脸瞬间红透了。
+(他……他牵我的手了!)
+
+【第12章:吃醋】
+"你为什么和那个男人说话?"
+林慕寒的脸色很不好。
+"他只是问我路……"
+"以后不许和别的男人说话。"
+"这也太霸道了吧?"
+"我就是霸道。"
+```
+
+---
+
+### 阶段 3: 表白期(确认关系)
+
+**目标**: 男主表白,女主接受,确认恋爱关系
+
+**章节分配**: 20-40章(根据总长度调整)
+
+**必备元素**:
+- 情感铺垫(暧昧到极点)
+- 表白场景(深情/霸道/意外)
+- 女主反应(震惊/感动/接受)
+- 确认关系(公开或暂时保密)
+
+**节奏要点**:
+```
+表白前3章: 铺垫情绪,制造契机
+表白章: 情感爆发,告白
+表白后2章: 确认关系,甜蜜升级
+```
+
+**示例场景**:
+```
+【第30章:表白】
+"苏念,我喜欢你。"
+林慕寒看着她的眼睛,眼中满是深情。
+"从见到你的第一天起,我就喜欢上你了。"
+苏念愣住了,眼泪止不住地流。
+"我……"
+"你不用现在回答我。"他温柔地说。
+"但我想让你知道……我爱你。"
+
+【第31章:确认关系】
+"林慕寒……"
+"嗯?"
+"我……我也喜欢你。"苏念小声说。
+林慕寒笑了,将她拥入怀中。
+"再说一遍。"
+"我喜欢你!"
+"真乖。"他吻了吻她的额头。
+```
+
+---
+
+### 阶段 4: 甜蜜期(恋爱日常)
+
+**目标**: 展示恋爱日常,甜蜜互动,偶尔小虐
+
+**章节分配**: 40-70章(根据总长度调整)
+
+**必备元素**:
+- 日常甜宠(喂饭、摸头、抱抱)
+- 小误会(吃醋、赌气)
+- 外部威胁(第三者/家族反对)
+- 感情稳固(克服困难)
+
+**节奏要点**:
+```
+甜蜜日常(5章) → 小误会(2章) → 和好更甜(3章) → 循环
+穿插外部危机(每10-15章一次大危机)
+```
+
+**示例场景**:
+```
+【第45章:日常甜蜜】
+"张嘴。"林慕寒喂她吃饭。
+"我又不是小孩子……"苏念嘟囔。
+"在我眼里就是。"他宠溺地笑。
+
+【第52章:小误会】
+"你为什么不告诉我你要加班?"
+"我忘了……"
+"你心里还有我吗?"苏念委屈。
+"当然有。"林慕寒把她抱进怀里。
+"只有你。"
+
+【第60章:外部危机】
+"我儿子不能娶你这种女人!"
+林母扔给她一张支票。
+苏念的心,碎了……
+```
+
+---
+
+### 阶段 5: 结局期(圆满结局)
+
+**目标**: 解决所有危机,圆满结局(结婚/生子)
+
+**章节分配**: 最后5-10章
+
+**必备元素**:
+- 解决最后障碍(家族同意/第三者退出)
+- 求婚/结婚场景
+- 幸福结局(生子/白头到老)
+
+**节奏要点**:
+```
+倒数第5章: 最后危机解决
+倒数第3章: 求婚
+倒数第2章: 结婚
+最后1章: 番外(生子/老年幸福)
+```
+
+**示例场景**:
+```
+【倒数第3章:求婚】
+"苏念,嫁给我。"
+林慕寒单膝跪地,手里捧着戒指。
+周围的人都在起哄:"嫁给他!嫁给他!"
+苏念哭着点头:"我愿意……"
+
+【最后章:圆满结局】
+三年后。
+苏念抱着孩子,林慕寒揽着她的腰。
+"幸福吗?"他问。
+"嗯,很幸福。"她笑。
+```
+
+---
+
+## 3. 单章结构设计
+
+### 标准单章结构(爽点章)
+
+**结构**:
+```
+开头(引入矛盾/延续上章悬念)
+    ↓
+发展(情感推进/冲突爆发)
+    ↓
+高潮(爽点/虐点/甜点)
+    ↓
+结尾(留悬念/埋伏笔)
+```
+
+**字数分配**:
+- 总字数: 2000-3000字/章(主流)
+- 开头: 200-300字
+- 发展: 1000-1500字
+- 高潮: 500-800字
+- 结尾: 200-300字
+
+---
+
+### 单章类型与节奏
+
+#### 类型 A: 甜蜜章
+
+**目标**: 给读者发糖,展示男主宠溺
+
+**节奏**:
+```
+开头: 日常场景(起床/吃饭/约会)
+发展: 甜蜜互动(喂饭/摸头/亲吻)
+高潮: 情话/告白/甜蜜爆表
+结尾: 留白(戛然而止,意犹未尽)
+```
+
+**示例**:
+```
+【开头】
+苏念刚睡醒,迷迷糊糊睁开眼。
+
+【发展】
+林慕寒俯身,在她额头上印下一吻。
+"早安,我的宝贝。"
+
+【高潮】
+"你知道吗?"他看着她,"每天醒来第一眼看到你,是我最幸福的事。"
+苏念脸红了。
+
+【结尾】
+她把头埋进他怀里。
+"……我也是。"
+```
+
+---
+
+#### 类型 B: 虐心章
+
+**目标**: 制造冲突,虐读者心
+
+**节奏**:
+```
+开头: 危机预警(气氛不对)
+发展: 矛盾爆发(误会/第三者)
+高潮: 虐点爆发(分手/冷暴力)
+结尾: 留悬念(真相是什么?会和好吗?)
+```
+
+**示例**:
+```
+【开头】
+苏念感觉到,林慕寒最近很反常。
+
+【发展】
+她看到他和一个美女拥抱。
+心,碎了一地。
+
+【高潮】
+"林慕寒,我们分手吧。"
+她说完这句话,转身离开。
+
+【结尾】
+林慕寒站在原地,没有追上去。
+雨,下起来了……
+(下章:真相是什么?)
+```
+
+---
+
+#### 类型 C: 爽点章(打脸/反转)
+
+**目标**: 让读者大呼过瘾
+
+**节奏**:
+```
+开头: 女主被欺负/看不起
+发展: 男主登场(或女主反击)
+高潮: 打脸/碾压/装逼
+结尾: 众人震惊(爽!)
+```
+
+**示例**:
+```
+【开头】
+"就凭你这个穷鬼,也配进这家店?"
+店员看不起苏念。
+
+【发展】
+林慕寒走过来,揽住她的腰。
+"她是我女朋友。"
+
+【高潮】
+"把你们店长叫来。"林慕寒冷冷地说。
+"告诉他,以后这个店员不用来了。"
+店员脸色惨白。
+
+【结尾】
+"林……林总……"
+周围的人都震惊了。
+(原来她男朋友这么厉害!)
+```
+
+---
+
+## 4. 付费卡点设计(网文必备)
+
+### 黄金卡点位置
+
+**卡点原则**: 在读者最想看下文的地方断章
+
+**经典卡点**:
+```
+1. 表白前: "苏念,我……" (下章才说"我喜欢你")
+2. 真相前: "其实那天……" (下章揭晓真相)
+3. 危机时: "她出事了!" (下章才知道什么事)
+4. 关键对话前: "我有话要说。" (下章才说)
+5. 吻戏前: "他低头……" (下章才写吻)
+```
+
+---
+
+### 卡点示例
+
+#### 示例 1: 表白卡点
+
+```
+【本章结尾】
+"苏念,我……"
+林慕寒看着她,欲言又止。
+"你什么?"她好奇。
+"我……"
+他深吸一口气——
+
+【下章开头】
+"我喜欢你。"
+```
+
+#### 示例 2: 真相卡点
+
+```
+【本章结尾】
+"其实那天,她不是我女朋友……"
+林慕寒终于开口。
+"那她是谁?"苏念问。
+"她是……"
+
+【下章开头】
+"她是我妹妹。"
+```
+
+#### 示例 3: 危机卡点
+
+```
+【本章结尾】
+"林总!苏小姐她……她出事了!"
+助理慌张地跑进来。
+林慕寒脸色大变:"什么事?!"
+"她……"
+
+【下章开头】
+"她被车撞了!现在在医院抢救!"
+```
+
+---
+
+## 5. 高潮章节设计
+
+### 高潮章节的位置
+
+**标准分布**(以100章为例):
+```
+第10章: 小高潮(初吻/牵手)
+第30章: 中高潮(表白/确认关系)
+第60章: 大高潮(分手/生死危机)
+第80章: 追妻高潮(火葬场/和好)
+第100章: 终极高潮(结婚/圆满)
+```
+
+---
+
+### 高潮章节的写作要点
+
+**要点 1: 情绪铺垫**
+```
+高潮前3章: 逐步铺垫情绪
+高潮章: 情感爆发
+高潮后2章: 余韵/后续
+```
+
+**要点 2: 字数加长**
+```
+普通章: 2000-3000字
+高潮章: 3000-5000字(更详细的描写)
+```
+
+**要点 3: 细节丰富**
+```
+- 心理活动更多
+- 环境描写更细
+- 对话更深情
+- 动作更细腻
+```
+
+---
+
+## 6. 节奏自检清单
+
+- [ ] **感情推进合理吗**: 是否太快(第1章就在一起)或太慢(50章还暧昧)?
+- [ ] **每章有重点吗**: 每章是否有明确的情感推进或冲突爆发?
+- [ ] **甜虐平衡吗**: 甜虐比例是否合理(建议6:4或7:3)?
+- [ ] **节奏有变化吗**: 是否有快慢交替,避免单调?
+- [ ] **卡点合理吗**: 付费卡点是否在读者最想看下文的地方?
+- [ ] **高潮够强吗**: 高潮章节是否情绪饱满,细节丰富?
+
+---
+
+## 🛠️ 言情节奏速查表
+
+| 节奏类型 | 确认关系章节 | 总章节数 | 甜虐比 | 适用类型 |
+|---------|-------------|---------|-------|---------|
+| **快节奏** | 10-20章 | 50-100章 | 7:3 | 甜文、爽文 |
+| **中速** | 20-40章 | 100-200章 | 6:4 | 标准言情 |
+| **慢节奏** | 50章+ | 200章+ | 4:6 | 虐文、深情文 |
+
+---
+
+## 附录:经典节奏案例
+
+### 案例 1: 《何以笙箫默》中速虐恋
+
+**节奏**:
+- 前期: 相遇、暗恋(大学时期)
+- 中期: 误会、分离(七年)
+- 后期: 重逢、追妻、和好
+
+**优点**: 虐恋情深,先虐后甜。
+
+---
+
+### 案例 2: 反面教材(某扑街文)
+
+```
+第1章: 相遇
+第2章: 表白
+第3章: 在一起
+第4-100章: 天天撒糖,没有任何冲突
+```
+
+**问题**:
+- 太快,没有感情铺垫
+- 太甜,没有波折,读者会腻
+
+---
+
+## 言情节奏的终极原则
+
+```
+快慢结合
+甜虐交替
+层层递进
+高潮迭起
+```
+
+**记住**: 节奏是言情小说的生命线。节奏对了,读者就会一直追下去。

+ 528 - 0
.claude/genres/dog-blood-romance/romance-tropes.md

@@ -0,0 +1,528 @@
+# 狗血言情经典套路库 (Romance Tropes)
+
+> **核心原则**: 套路不是问题,会用套路才是王道。读者爱看的不是"新",而是"爽"。经典套路之所以经典,是因为它们能精准击中读者的情感痛点。
+
+---
+
+## 1. 十大经典套路
+
+### 套路 1: 霸道总裁(Dominant CEO)
+
+**核心公式**:
+```
+霸道总裁 + 灰姑娘女主 = 权力差+身份差 → 碰撞 → 征服 → 甜蜜
+```
+
+**必备元素**:
+- **男主设定**: 年轻有为(28-35岁)、身家过亿、外冷内热、有洁癖/强迫症
+- **女主设定**: 普通女孩、善良坚强、经济困难、有自尊心
+- **关键情节**:
+  1. 意外相遇(撞车/救人/误会)
+  2. 契约/协议(假扮女友/代孕/交易)
+  3. 强势追求(堵门/壁咚/强吻)
+  4. 虐恋虐心(误会/分离/疾病)
+  5. 甜蜜结局(表白/婚礼/生子)
+
+**示例场景**:
+```markdown
+【霸道宣言】
+"女人,你成功引起了我的注意。"
+陆总裁眼神危险地盯着她。
+
+【壁咚桥段】
+他单手撑墙,将她困在怀中。
+"想逃?晚了。"
+```
+
+**经典变体**:
+- **冷面总裁** × 傻白甜秘书
+- **腹黑总裁** × 高冷女强人
+- **病娇总裁** × 治愈系女主
+
+---
+
+### 套路 2: 替身文(Substitute)
+
+**核心公式**:
+```
+女主是白月光替身 → 男主虐待女主 → 女主离开 → 男主追妻火葬场
+```
+
+**必备元素**:
+- **白月光**: 男主心中的完美女神(通常已死/失踪)
+- **女主**: 长得像白月光,被当成替身
+- **虐点设计**:
+  1. 男主用女主发泄对白月光的思念
+  2. 男主在女主面前提起白月光
+  3. 男主让女主穿白月光的衣服
+  4. 白月光回归,女主被抛弃
+
+**经典桥段**:
+```markdown
+【虐心对话】
+"你以为我娶你是因为爱你?不,你只是她的影子。"
+他冷冷地说,手中握着白月光的照片。
+
+【追妻火葬场】
+"我错了,求你回来……"
+他跪在雨中,但她头也不回地走了。
+"对不起,你爱的人不是我,我也不爱你了。"
+```
+
+**反转技巧**:
+- **女主黑化**: 从隐忍到报复
+- **真相揭露**: 白月光其实是绿茶/坏女人
+- **身份反转**: 女主才是真正的白月光
+
+---
+
+### 套路 3: 豪门恩怨(Wealthy Family Drama)
+
+**核心公式**:
+```
+豪门家族斗争 + 男女主爱情 = 权力游戏 + 情感纠葛
+```
+
+**必备元素**:
+- **家族背景**: 百年豪门/商业帝国
+- **权力斗争**: 继承权/股权争夺
+- **情感障碍**: 门不当户不对/家族反对/世仇
+- **反派角色**: 恶毒长辈/白莲花情敌/腹黑兄弟
+
+**经典情节**:
+```markdown
+【家族反对】
+"她配不上我们陆家!除非从我尸体上踏过去!"
+陆老太太拍桌怒吼。
+
+【世仇化解】
+"为了你,我可以放弃整个陆氏集团。"
+他当众宣布,全场哗然。
+```
+
+**子套路**:
+- **联姻**: 家族联姻 → 假戏真做 → 真爱
+- **复仇**: 女主接近男主为复仇 → 日久生情
+- **夺权**: 男主夺权 + 女主助攻 → 共同掌权
+
+---
+
+### 套路 4: 先婚后爱(Marriage First, Love Later)
+
+**核心公式**:
+```
+闪婚(契约/协议) → 日常相处 → 日久生情 → 虐点 → 真爱
+```
+
+**必备元素**:
+- **闪婚原因**: 
+  - 家族逼婚
+  - 互相利用(继承遗产/拿项目)
+  - 报复前任
+  - 意外怀孕
+- **相处模式**: 同居不同房 → 逐渐亲密 → 突破底线
+- **情感转折**: 假装不在意 → 吃醋 → 占有欲爆发
+
+**经典桥段**:
+```markdown
+【契约条款】
+"第一条:保持距离,各睡各的。"
+"第二条:一年后离婚,互不干涉。"
+"第三条……"
+半年后——
+"老婆,第一条是不是可以作废了?"
+他抱着她不放手。
+
+【吃醋桥段】
+看到她和别的男人说话,他黑着脸走过去。
+"陈太太,我们该回家了。"
+语气危险。
+```
+
+---
+
+### 套路 5: 虐恋情深(Tortured Love)
+
+**核心公式**:
+```
+相爱 → 误会/背叛 → 虐心分离 → 真相揭露 → HE/BE
+```
+
+**必备元素**:
+- **虐点来源**:
+  - 误会(第三者挑拨/巧合)
+  - 背叛(表面背叛实为保护)
+  - 伤害(堕胎/离婚/囚禁)
+  - 疾病(癌症/失忆/残疾)
+- **虐恋强度**:
+  - 轻虐: 误会 → 解释 → 和好
+  - 中虐: 分手 → 各自痛苦 → 重逢
+  - 重虐: 生离死别 → 追悔莫及
+
+**经典桥段**:
+```markdown
+【误会虐心】
+"你亲手杀了我们的孩子……"
+她泪流满面,眼神死寂。
+他想解释,却发现所有证据都指向自己。
+
+【真相揭露】
+"对不起,都是我的错……"
+他跪在她的病床前,泪如雨下。
+但她已经昏迷不醒。
+```
+
+**BE vs HE**:
+- **BE(悲剧结局)**: 女主死亡/失忆/离开,男主追悔终生
+- **HE(圆满结局)**: 误会解除,重归于好,结婚生子
+
+---
+
+### 套路 6: 重生复仇(Rebirth Revenge)
+
+**核心公式**:
+```
+前世惨死 → 重生回到过去 → 复仇渣男/绿茶 → 遇真爱 → 逆袭
+```
+
+**必备元素**:
+- **前世经历**: 被渣男背叛/被闺蜜陷害/被豪门虐待/惨死
+- **重生优势**: 知道未来/记得前世/先知先觉
+- **复仇对象**: 渣男前夫/白莲花姐妹/恶毒婆婆
+- **真命天子**: 前世被忽略的好男人/新出现的霸总
+
+**经典桥段**:
+```markdown
+【重生觉醒】
+睁开眼,她发现自己回到了十年前。
+"这一世,我绝不会重蹈覆辙!"
+
+【复仇打脸】
+"还想骗我?你那套把戏,我见得太多了。"
+她冷笑着拿出证据,渣男脸色惨白。
+
+【遇到真爱】
+前世他默默守护她,她却视而不见。
+这一世,她主动走向他:"我们重新开始,好吗?"
+```
+
+---
+
+### 套路 7: 娱乐圈(Entertainment Industry)
+
+**核心公式**:
+```
+小透明女主 + 影帝/金主男主 = 潜规则传闻 → 官宣 → 打脸黑粉
+```
+
+**必备元素**:
+- **女主设定**: 十八线小演员/新人/替身
+- **男主设定**: 影帝/导演/娱乐公司总裁
+- **情节设计**:
+  1. 女主遭遇潜规则/黑料
+  2. 男主霸气护妻
+  3. 女主靠实力逆袭
+  4. 公开恋情,全网祝福
+
+**经典桥段**:
+```markdown
+【潜规则传闻】
+#某十八线女星陪睡上位#
+热搜爆了。
+
+【霸气护妻】
+影帝亲自下场:
+"她是我老婆,有意见?"
+配图:结婚证。
+全网炸了。
+
+【实力打脸】
+女主凭演技拿下影后,黑粉闭嘴。
+"演技才是硬道理。"
+```
+
+---
+
+### 套路 8: 甜宠无虐(Pure Fluff)
+
+**核心公式**:
+```
+男主宠女主 + 女主被宠 = 全程甜蜜 + 零虐点
+```
+
+**必备元素**:
+- **男主属性**: 宠妻狂魔/妻奴/女儿奴
+- **女主属性**: 可爱/软萌/治愈系
+- **甜点设计**:
+  - 每天不同的宠溺方式
+  - 男主为女主做傻事
+  - 秀恩爱虐狗
+  - 生活日常甜蜜
+
+**经典桥段**:
+```markdown
+【宠溺日常】
+"老婆说的都对。"
+"老婆想要的,我都给。"
+"老婆开心最重要。"
+
+【秀恩爱】
+记者:"陆总,成功的秘诀是什么?"
+陆总:"听老婆的话。"
+全场:"......"
+```
+
+**甜点类型**:
+- **宠溺型**: 男主无条件宠女主
+- **撒娇型**: 霸总在女主面前撒娇
+- **反差萌**: 外冷内热,只对女主温柔
+
+---
+
+### 套路 9: 年下/姐弟恋(Younger Man Romance)
+
+**核心公式**:
+```
+成熟女主 + 年轻男主 = 姐弟恋 → 追妻 → 打破世俗
+```
+
+**必备元素**:
+- **年龄差**: 3-10岁
+- **女主设定**: 成熟独立/事业成功/离婚/丧偶
+- **男主设定**: 阳光/执着/宠姐狂魔
+- **障碍**: 世俗眼光/女主自卑/家人反对
+
+**经典桥段**:
+```markdown
+【追求】
+"姐姐,我喜欢你。"
+年轻男人认真地说。
+"你还小……"
+"我已经成年了,而且,年龄不是问题。"
+
+【打破偏见】
+"我爱的是她这个人,不是她的年龄。"
+他当众宣布,不顾世俗眼光。
+```
+
+---
+
+### 套路 10: 隐婚(Secret Marriage)
+
+**核心公式**:
+```
+结婚但隐瞒身份 → 各种巧合差点暴露 → 官宣 → 全网震惊
+```
+
+**必备元素**:
+- **隐婚原因**: 
+  - 家族反对
+  - 事业考虑(明星)
+  - 低调(富豪)
+- **险些暴露**: 
+  - 被拍到同框
+  - 戴婚戒被发现
+  - 孩子叫妈妈
+- **官宣方式**: 
+  - 意外曝光
+  - 主动公开
+  - 被逼无奈
+
+**经典桥段**:
+```markdown
+【险些暴露】
+记者:"林小姐,您手上的婚戒……"
+女主:"哦,这是……道具!"
+
+【官宣震惊】
+影帝发微博:
+"介绍一下,我老婆 @林小姐"
+配图:结婚证+全家福
+热搜爆炸。
+```
+
+---
+
+## 2. 套路组合公式
+
+### 公式 1: 霸总 + 替身 + 追妻火葬场
+```markdown
+霸道总裁把女主当替身 → 虐待女主 → 女主离开 → 霸总追妻
+```
+**代表作**: 《替身新娘》类
+
+---
+
+### 公式 2: 重生 + 复仇 + 真爱
+```markdown
+女主重生 → 远离渣男 → 遇到真爱霸总 → 复仇成功 → HE
+```
+**代表作**: 《重生之豪门盛宠》类
+
+---
+
+### 公式 3: 先婚后爱 + 隐婚 + 甜宠
+```markdown
+闪婚 → 隐婚 → 日常甜蜜 → 官宣 → 全网羡慕
+```
+**代表作**: 《隐婚甜妻》类
+
+---
+
+### 公式 4: 娱乐圈 + 豪门 + 打脸
+```markdown
+小演员 × 豪门继承人 → 黑料缠身 → 真相揭露 → 打脸黑粉
+```
+**代表作**: 《影后的隐婚老公》类
+
+---
+
+### 公式 5: 虐恋 + 误会 + 追悔莫及
+```markdown
+相爱 → 误会分手 → 女主离开/假死 → 男主追悔 → HE/BE
+```
+**代表作**: 《何以笙箫默》类
+
+---
+
+## 3. 套路创新技巧
+
+### 技巧 1: 性格反转
+**传统**: 霸道总裁 × 傻白甜
+**创新**: 沙雕总裁 × 高冷女主
+
+**示例**:
+```markdown
+【传统霸总】
+"女人,你成功引起了我的注意。"
+
+【沙雕总裁】
+"小姐姐,加个微信呗~"
+他眨着无辜的大眼睛。
+```
+
+---
+
+### 技巧 2: 身份互换
+**传统**: 霸总男 × 灰姑娘女
+**创新**: 霸总女 × 小奶狗男
+
+---
+
+### 技巧 3: 背景创新
+**传统**: 现代都市
+**创新**: 
+- 民国背景 + 霸总套路
+- 末世背景 + 甜宠
+- 古代架空 + 先婚后爱
+
+---
+
+### 技巧 4: 多重套路叠加
+**示例**: 替身 + 重生 + 追妻 + 真假千金
+```markdown
+女主前世是替身,被虐死 → 重生发现自己是真千金 → 
+复仇假千金 → 霸总追妻 → 真相揭露 → HE
+```
+
+---
+
+## 4. 套路自检清单
+
+写完套路后,逐项检查:
+- [ ] **核心公式清晰**: 读者能一眼看出是什么套路吗?
+- [ ] **必备元素齐全**: 该套路的关键要素都有吗?
+- [ ] **情感节奏合理**: 甜虐比例是否合适?(甜宠7:3,虐恋3:7)
+- [ ] **有创新点吗**: 是否在经典基础上有新意?
+- [ ] **爽点足够吗**: 每5000字至少1个甜蜜/打脸/逆袭爽点
+- [ ] **避免俗套吗**: 是否避开了读者最反感的狗血点?
+
+---
+
+## 🛠️ 套路速查表
+
+| 套路类型 | 核心元素 | 主要爽点 | 适合读者 | 虐恋比例 |
+|---------|---------|---------|---------|---------|
+| **霸道总裁** | 权力差+强势追求 | 被宠+壁咚 | 少女向 | 3:7(甜多) |
+| **替身文** | 白月光+虐恋 | 追妻火葬场 | 虐恋爱好者 | 7:3(虐多) |
+| **豪门恩怨** | 家族斗争+爱情 | 权谋+护妻 | 爽文爱好者 | 5:5(均衡) |
+| **先婚后爱** | 契约→真爱 | 日久生情 | 日常向 | 2:8(甜多) |
+| **虐恋情深** | 误会+伤害 | 虐心+BE/HE | 虐文爱好者 | 8:2(虐为主) |
+| **重生复仇** | 先知+逆袭 | 打脸渣男 | 爽文爱好者 | 2:8(爽多) |
+| **娱乐圈** | 明星+八卦 | 官宣+打脸 | 追星族 | 3:7(甜多) |
+| **甜宠无虐** | 全程宠溺 | 甜蜜+撒糖 | 甜文爱好者 | 0:10(纯甜) |
+| **姐弟恋** | 年龄差+追妻 | 小狼狗+宠姐 | 成熟女性向 | 2:8(甜多) |
+| **隐婚** | 秘密+官宣 | 身份反差 | 八卦向 | 1:9(甜多) |
+
+---
+
+## 附录:反面教材
+
+### 错误 1: 套路堆砌,逻辑混乱
+```markdown
+女主既是霸总,又是替身,还重生了,同时还是真千金……
+```
+**问题**: 太多套路,读者会乱。
+
+**改进**: 专注1-2个主套路,其他作为辅助。
+
+---
+
+### 错误 2: 套路老套,毫无新意
+```markdown
+又是霸总壁咚,又是"女人,你成功引起了我的注意"……
+```
+**问题**: 2025年了,这些梗已经被用烂了。
+
+**改进**: 在经典基础上加入新元素(如沙雕、反套路)。
+
+---
+
+### 错误 3: 为了套路而套路
+```markdown
+明明可以解释清楚的误会,非要拖十章。
+明明可以直接表白,非要搞契约。
+```
+**问题**: 生硬,为了凑字数而用套路。
+
+**改进**: 套路要服务于剧情,不是剧情服务于套路。
+
+---
+
+## 经典案例分析
+
+### 案例 1: 《何以笙箫默》
+**套路**: 虐恋情深 + 执着等待
+**成功点**: 
+- "如果当时……"的遗憾感
+- 男主执着等待七年
+- 误会有深度,不狗血
+
+---
+
+### 案例 2: 《杉杉来吃》
+**套路**: 霸道总裁 + 甜宠无虐
+**成功点**:
+- 男主人设讨喜(傲娇+宠溺)
+- 女主不傻白甜,有主见
+- 日常细节甜蜜
+
+---
+
+### 案例 3: 《微微一笑很倾城》
+**套路**: 校园 + 游戏 + 甜宠
+**成功点**:
+- 双学霸人设
+- 游戏元素新颖
+- 全程高甜,零狗血
+
+---
+
+## 总结
+
+套路不是贬义词,而是"读者期待的情节模式"。用好套路的关键:
+1. **熟悉经典**: 知道读者爱看什么
+2. **适度创新**: 在经典基础上加新意
+3. **服务剧情**: 套路为剧情服务,不是反过来
+4. **控制节奏**: 甜虐比例要合理
+5. **避免生硬**: 套路要自然,不要为了用而用

+ 620 - 0
.claude/genres/dog-blood-romance/sweet-moments.md

@@ -0,0 +1,620 @@
+# 甜蜜场景设计 (Sweet Moments Design)
+
+> **核心原则**: 甜蜜场景是言情小说的"糖",用来平衡"虐"。甜度要适中,过甜则腻,过淡则无味。读者追文的动力 = 虐后的甜 + 期待下一次的甜。
+
+---
+
+## 1. 甜蜜场景的五大类型
+
+### 类型 1: 宠溺日常(Pet Daily Life)
+
+**核心公式**:
+```
+男主霸道宠溺 + 女主撒娇/傲娇 = 甜蜜暴击
+```
+
+**经典场景**:
+| 场景 | 男主行为 | 女主反应 | 甜度等级 |
+|------|---------|---------|---------|
+| **喂食** | "张嘴。"(亲手喂饭) | "我又不是小孩子……" | ⭐⭐⭐ |
+| **穿衣** | 帮女主系扣子/系鞋带 | 脸红,推开:"我自己来!" | ⭐⭐⭐ |
+| **抱抱** | 从背后抱住,下巴搁在肩上 | "你又来……" | ⭐⭐⭐⭐ |
+| **摸头杀** | 揉她的头:"乖。" | 噘嘴:"别摸我头,长不高!" | ⭐⭐⭐ |
+| **壁咚** | 将她困在墙角 | 紧张,心跳加速 | ⭐⭐⭐⭐⭐ |
+
+**示例**:
+```
+【喂食甜蜜】
+"张嘴。"林慕寒夹起一块鱼肉,递到她嘴边。
+"我又不是小孩子……"苏念嘟囔,但还是乖乖张嘴。
+"嗯,真乖。"他笑了,眼中满是宠溺。
+
+【壁咚甜蜜】
+林慕寒突然逼近,双手撑在她身侧,将她困在墙角。
+"你……你干什么……"苏念紧张地看着他。
+"你说呢?"他低头,呼吸喷洒在她脸上。
+苏念的心跳快得像要跳出来。
+```
+
+---
+
+### 类型 2: 肢体接触(Physical Contact)
+
+**核心公式**:
+```
+意外接触 → 心跳加速 → 暧昧气氛 → 读者尖叫
+```
+
+**接触进度阶梯**(循序渐进):
+```
+1级: 牵手(初次接触,心跳加速)
+2级: 搂腰/揽肩(亲密升级)
+3级: 拥抱(安全感满满)
+4级: 额头相抵/鼻尖碰触(超近距离)
+5级: 亲吻(情感爆发)
+6级: 床戏暗示(点到为止,不描写细节)
+```
+
+**各级示例**:
+
+#### 1级: 牵手
+```
+【意外牵手】
+人群拥挤,林慕寒一把握住她的手。
+"别走丢了。"他淡淡地说。
+苏念的脸瞬间红透了。
+(他……他牵我的手了!)
+
+【主动牵手】
+"手给我。"
+"干嘛?"
+"怕你跑了。"林慕寒强硬地握住她的手,十指相扣。
+```
+
+#### 3级: 拥抱
+```
+【保护性拥抱】
+"别怕,有我在。"
+林慕寒将她紧紧抱在怀里。
+苏念闻到他身上淡淡的古龙水香,心跳如鼓。
+
+【背后拥抱】
+林慕寒从背后环住她的腰。
+"你……你别这样……"苏念挣扎。
+"就抱一会儿,别动。"他的声音低沉温柔。
+```
+
+#### 5级: 亲吻
+```
+【初吻】
+林慕寒俯身,吻住了她的唇。
+苏念瞪大眼睛,大脑一片空白。
+(他……他吻我了!)
+
+【霸道吻】
+"你是我的,别再看别的男人。"
+话音未落,他捧起她的脸,狠狠吻了下去。
+```
+
+---
+
+### 类型 3: 告白表白(Confession)
+
+**核心公式**:
+```
+铺垫情绪 → 男主表白 → 女主震惊/感动 → 确认关系
+```
+
+**经典告白类型**:
+
+#### A型: 霸道告白
+```
+"苏念,我喜欢你。"
+"什么……?"
+"从见到你的第一天起,我就喜欢上你了。"
+林慕寒将她拥入怀中。
+"所以,你只能是我的。"
+```
+
+#### B型: 深情告白
+```
+"苏念,你知道吗?"
+"什么?"
+"我这辈子,只爱过你一个人。"
+他看着她的眼睛,眼中满是深情。
+"能不能……给我一个机会?"
+```
+
+#### C型: 行动告白(不说爱,用行动证明)
+```
+"你为什么对我这么好?"苏念问。
+林慕寒没有回答,只是揉了揉她的头。
+"傻瓜。"
+(因为我爱你啊……)
+```
+
+---
+
+### 类型 4: 吃醋撒娇(Jealousy & Acting Cute)
+
+**核心公式**:
+```
+男主吃醋 → 占有欲爆发 → 霸道宣示主权 → 甜蜜
+```
+
+**男主吃醋场景**:
+```
+【吃醋1.0】
+"你刚才为什么和那个男人说话?"
+"他问我路……"
+"以后不许和别的男人说话。"
+"这也太霸道了吧!"
+"我就是霸道。"他搂住她的腰,"你只能是我的。"
+
+【吃醋2.0(醋王上线)】
+"谁允许你笑得这么甜?"
+"啊?"苏念懵了。
+"对别的男人,不许笑。"
+"……你这是什么逻辑?"
+"我的逻辑。"林慕寒霸道地吻住她。
+```
+
+**女主撒娇场景**:
+```
+【撒娇要抱抱】
+"林慕寒……"苏念拉着他的袖子。
+"嗯?"
+"抱抱……"她小声说,脸红得像苹果。
+林慕寒心都化了,将她抱进怀里。
+"怎么这么可爱……"
+
+【撒娇求原谅】
+"我错了嘛……"苏念抱住他的胳膊,眨着大眼睛。
+"……"林慕寒别过脸,"别以为撒娇就有用。"
+"那你还生气吗?"
+"……不生了。"他败下阵来。
+```
+
+---
+
+### 类型 5: 英雄救美(Hero Saves Beauty)
+
+**核心公式**:
+```
+女主遇险 → 男主霸气登场 → 保护女主 → 女主芳心暗许
+```
+
+**经典场景**:
+
+#### 场景A: 救场(社交场合)
+```
+"苏小姐,不如陪我喝一杯?"油腻男人纠缠。
+"不好意思,我男朋友在等我。"苏念推辞。
+"你男朋友是谁啊?"
+就在这时,林慕寒走过来,揽住苏念的腰。
+"我就是她男朋友。有问题吗?"
+他的眼神冰冷,油腻男人吓得连连后退。
+```
+
+#### 场景B: 救命(危险场合)
+```
+【绑架救援】
+"放开她!"林慕寒踹开门,眼中满是杀意。
+"林……林慕寒……"苏念哭着叫他。
+"别怕,我来了。"
+他三两下解决了绑匪,将她紧紧抱住。
+"对不起,让你受苦了……"
+```
+
+---
+
+## 2. 甜蜜场景的阶段分布
+
+### 初期(暧昧期): 小甜饼
+
+**特点**: 试探、脸红、心跳加速、若即若离
+
+**甜蜜方式**:
+- 意外身体接触(撞一起、牵手)
+- 眼神对视(对视3秒,脸红移开)
+- 小心机(故意接近、制造偶遇)
+- 保护(挡雨、挡危险)
+
+**示例**:
+```
+【意外接触】
+地铁突然刹车,苏念一个踉跄,撞进林慕寒怀里。
+"对……对不起……"她慌忙退开,脸红得像煮熟的虾。
+"小心点。"他扶住她,嘴角微微上扬。
+
+【眼神杀】
+苏念抬头,正好对上林慕寒的眼睛。
+两人对视了三秒。
+她慌忙移开视线,心跳如鼓。
+(他的眼睛……好好看……)
+```
+
+---
+
+### 中期(恋爱期): 日常甜宠
+
+**特点**: 确认关系,公开秀恩爱,宠溺互动
+
+**甜蜜方式**:
+- 日常互动(喂饭、摸头、抱抱)
+- 小惊喜(送花、送礼物、准备惊喜)
+- 吃醋撒娇(占有欲、争风吃醋)
+- 亲密接触(亲吻、拥抱、牵手)
+
+**示例**:
+```
+【早安吻】
+苏念刚睡醒,迷迷糊糊睁开眼。
+林慕寒俯身,在她额头上印下一吻。
+"早安,我的宝贝。"
+"唔……"她脸红,把头埋进被子里。
+
+【喂药甜蜜】
+"张嘴。"林慕寒端着药。
+"好苦……"
+"乖,喝了就给你吃糖。"
+"我又不是小孩子……"
+"那你喝不喝?"
+"……喝。"苏念妥协。
+喝完药,林慕寒递给她一颗糖。
+"真乖。"他揉她的头。
+```
+
+---
+
+### 后期(婚后/稳定期): 细水长流
+
+**特点**: 生活化甜蜜,老夫老妻的温馨
+
+**甜蜜方式**:
+- 生活互动(做饭、洗碗、看电视)
+- 小情趣(突然的吻、拥抱、情话)
+- 纪念日(结婚纪念日、第一次见面纪念日)
+- 孩子(有了孩子后的家庭温馨)
+
+**示例**:
+```
+【做饭甜蜜】
+"老公,帮我尝尝咸不咸。"
+林慕寒走过来,尝了一口。
+"嗯,刚好。"
+"那就好~"苏念笑。
+林慕寒突然从背后抱住她。
+"不过,还是你比较甜。"
+
+【睡前甜蜜】
+"困了吗?"林慕寒问。
+"嗯……"苏念窝在他怀里。
+"睡吧,晚安。"他吻了吻她的额头。
+"晚安……"
+```
+
+---
+
+## 3. 甜蜜对话模板
+
+### 模板 1: 情话攻击
+
+**男主情话**:
+```
+"你知道你最大的缺点是什么吗?"
+"什么?"
+"太可爱了。"
+
+"你猜我想吃什么?"
+"什么?"
+"痴痴地看着你。"
+
+"你是我的什么?"
+"……女朋友?"
+"不,你是我的命。"
+```
+
+**女主害羞回应**:
+```
+"你……你别贫嘴!"(脸红)
+"油嘴滑舌……"(嘴角上扬)
+"讨厌……"(小声)
+```
+
+---
+
+### 模板 2: 宠溺对话
+
+```
+【宠溺1】
+女主:"我想吃冰淇淋。"
+男主:"不行,你感冒了。"
+女主(撒娇):"就吃一口嘛……"
+男主(败下阵来):"……只能一口。"
+女主(开心):"好!"
+
+【宠溺2】
+女主:"你为什么对我这么好?"
+男主:"因为你是我的女人。"
+女主(脸红):"……"
+男主(揉头):"傻瓜。"
+```
+
+---
+
+### 模板 3: 占有欲对话
+
+```
+【占有欲1】
+男主:"你是谁的?"
+女主:"……你的。"
+男主:"大声点。"
+女主(脸红):"我是你的!"
+男主(满意):"嗯,真乖。"
+
+【占有欲2】
+男主:"记住,你只能是我的。"
+女主:"我知道啦……"
+男主:"别的男人,看都不许看。"
+女主:"这也太霸道了吧?"
+男主:"我就是霸道。"
+```
+
+---
+
+## 4. 身体接触的进度设计
+
+### 进度控制原则
+
+**禁忌**: 太快太猛(第1章就床戏)
+**正确**: 循序渐进,层层递进
+
+**标准进度表**:
+```
+第1-10章: 意外接触(撞一起、牵手)
+第11-20章: 主动接触(搂腰、揽肩、摸头)
+第21-30章: 亲密接触(拥抱、额头相抵)
+第31-40章: 情感爆发(亲吻)
+第41章+: 更进一步(暗示即可,不细写)
+```
+
+**每次接触后的反应**:
+```
+【第一次牵手】
+苏念的心跳快得要跳出来。
+(他……他牵我的手了!)
+她偷偷看他,他却面不改色。
+
+【第一次亲吻】
+苏念大脑一片空白,浑身僵硬。
+林慕寒松开她,额头抵着她的额头。
+"别紧张……"他低声说。
+```
+
+---
+
+## 5. 甜蜜场景的节奏控制
+
+### 黄金比例
+
+**甜虐比**: 6:4 或 7:3(甜多虐少,但要有虐)
+
+**章节分布**:
+```
+连续甜: 3-5章(不要超过5章,会腻)
+连续虐: 2-3章(不要超过3章,会弃文)
+```
+
+**示例节奏**:
+```
+第1-5章: 甜(确认关系,日常宠溺)
+第6-8章: 虐(误会,冷战)
+第9-12章: 甜(和好,更甜蜜)
+第13-15章: 虐(第三者出现)
+第16-25章: 甜(表态,虐后加倍甜)
+```
+
+---
+
+### 甜蜜强度递增
+
+**原则**: 后面的甜要比前面更甜
+
+```
+初期甜蜜: ⭐⭐⭐(牵手、脸红)
+中期甜蜜: ⭐⭐⭐⭐(拥抱、亲吻)
+后期甜蜜: ⭐⭐⭐⭐⭐(表白、确认关系、公开)
+高潮甜蜜: ⭐⭐⭐⭐⭐⭐(求婚、结婚、生子)
+```
+
+---
+
+## 6. 避免过度甜腻
+
+### 反面教材(太甜太腻)
+
+```
+【过度甜腻】
+第1-50章:
+- 男主每天对女主各种宠
+- 没有任何矛盾
+- 天天撒糖
+- 读者:无聊,弃文
+
+【正确做法】
+甜中带虐,虐后更甜。
+```
+
+---
+
+### 甜蜜场景的"留白"
+
+**不要什么都写**:
+```
+❌ 错误: 描写他们从早上起床到晚上睡觉的每一个细节
+✅ 正确: 只写关键的甜蜜时刻,其余跳过
+```
+
+**示例**:
+```
+【留白示例】
+两人逛了一下午街,买了很多东西。
+【跳过逛街细节,直接到晚餐】
+晚餐时,林慕寒给她夹菜。
+"多吃点,太瘦了。"
+"我不瘦……"苏念嘟囔。
+"在我眼里就是瘦。"他坚持。
+```
+
+---
+
+## 7. 特殊甜蜜场景
+
+### 场景 A: 生病照顾
+
+```
+【女主生病】
+苏念发高烧,迷迷糊糊睡着了。
+林慕寒一夜没睡,守在床边。
+天亮时,她睁开眼,看到的是他红肿的眼睛。
+"你……一夜没睡?"
+"嗯。"他握住她的手,"吓死我了……"
+
+【男主生病(反差萌)】
+"我……好难受……"林慕寒趴在床上,像个小孩子。
+苏念哭笑不得:"不就是感冒吗……"
+"你要照顾我……"他拉住她的手。
+"好好好,我照顾你。"她无奈。
+(平时这么霸道,生病了就变小孩……)
+```
+
+---
+
+### 场景 B: 纪念日惊喜
+
+```
+【生日惊喜】
+"生日快乐!"
+林慕寒打开灯,房间里摆满了玫瑰花。
+苏念捂住嘴,眼泪止不住流下来。
+"你……你什么时候准备的……"
+"早就准备了。"他走过来,将她拥入怀中。
+"喜欢吗?"
+"喜欢……太喜欢了……"
+
+【求婚场景】
+"苏念,嫁给我。"
+林慕寒单膝跪地,手里捧着戒指。
+周围的人都在起哄:"嫁给他!嫁给他!"
+苏念哭着点头:"我愿意……"
+```
+
+---
+
+### 场景 C: 保护女主(霸气护短)
+
+```
+【护短1.0】
+"她是我的女人,谁敢动她?"
+林慕寒挡在苏念面前,眼中满是杀意。
+
+【护短2.0】
+"你欺负她,就是欺负我。"
+林慕寒冷冷地看着对方。
+"识相的话,现在道歉。"
+
+【护短3.0(极致)】
+"动她一根头发,我灭你全家。"
+全场寂静。
+没人敢动。
+```
+
+---
+
+## 8. 甜蜜场景自检清单
+
+- [ ] **有铺垫吗**: 甜蜜场景是否有情绪铺垫?(不能突兀)
+- [ ] **符合人设吗**: 男主/女主的反应是否符合其性格?
+- [ ] **节奏合理吗**: 甜蜜强度是否递增?是否有甜虐交替?
+- [ ] **够细腻吗**: 是否描写了心理活动和身体反应?(脸红、心跳)
+- [ ] **不过度吗**: 是否避免了连续5章以上的纯甜?
+- [ ] **有新意吗**: 这次的甜蜜和上次有什么不同?
+
+---
+
+## 🛠️ 甜蜜场景速查表
+
+| 甜蜜类型 | 甜度 | 适用阶段 | 关键元素 | 示例 |
+|---------|------|---------|---------|------|
+| **日常宠溺** | ⭐⭐⭐ | 全阶段 | 喂食、摸头、抱抱 | "张嘴。" |
+| **肢体接触** | ⭐⭐⭐⭐ | 暧昧期+ | 牵手、拥抱、亲吻 | 十指相扣 |
+| **告白表白** | ⭐⭐⭐⭐⭐ | 关键节点 | 深情眼神、情话 | "我喜欢你。" |
+| **吃醋撒娇** | ⭐⭐⭐⭐ | 恋爱期 | 占有欲、霸道宣示 | "你是我的!" |
+| **英雄救美** | ⭐⭐⭐⭐⭐ | 危机时刻 | 保护、霸气登场 | "谁敢动她?" |
+| **纪念日惊喜** | ⭐⭐⭐⭐⭐⭐ | 高潮 | 惊喜、感动流泪 | 求婚、生日 |
+
+---
+
+## 附录:经典甜蜜场景案例
+
+### 案例 1: 《何以笙箫默》壁咚名场面
+
+```
+何以琛将赵默笙困在墙角。
+"默笙,你为什么不回来?"
+"我……"
+"你知不知道我等了你七年?"
+他的眼中满是深情。
+赵默笙的眼泪滑落。
+```
+
+**优点**: 深情+霸道,直击读者内心。
+
+---
+
+### 案例 2: 反面教材(某扑街文)
+
+```
+第1章: 男主对女主好
+第2章: 男主对女主好
+第3章: 男主对女主好
+……
+第50章: 男主对女主好
+```
+
+**问题**: 一直甜,没有任何波折,读者看腻了。
+
+---
+
+## 甜蜜场景的终极公式
+
+```
+铺垫情绪(心理活动)
+    ↓
+甜蜜互动(对话+动作)
+    ↓
+身体反应(脸红、心跳)
+    ↓
+留白(戛然而止,留悬念)
+```
+
+**示例**:
+```
+【铺垫】
+苏念有些紧张,不敢看他。
+(他会不会……)
+
+【互动】
+林慕寒突然握住她的手。
+"跟我在一起,好吗?"
+
+【反应】
+苏念的心跳快得要跳出来。
+她小声说:"……好。"
+
+【留白】
+林慕寒笑了,将她拥入怀中。
+夕阳的余晖洒在两人身上……
+(后续留给读者想象)
+```

+ 612 - 0
.claude/genres/dog-blood-romance/torture-points.md

@@ -0,0 +1,612 @@
+# 虐点设计与复仇快感 (Torture Points & Revenge Satisfaction)
+
+> **核心原则**: 虐是为了后面更甜。虐要虐得有理有据,虐完必须给出解释和补偿。读者能接受虐,但不能接受"虐而不解"或"为虐而虐"。
+
+---
+
+## 1. 虐点的五大类型
+
+### 类型 1: 误会虐(Misunderstanding Torture)
+
+**核心公式**:
+```
+关键误会 → 男主冷暴力/惩罚女主 → 女主心碎 → 真相大白 → 追妻火葬场
+```
+
+**经典误会来源**:
+| 误会类型 | 表象 | 真相 | 虐点 |
+|---------|------|------|------|
+| **出轨误会** | 男主和别的女人暧昧 | 那女人是妹妹/救命恩人 | 女主伤心欲绝 |
+| **背叛误会** | 女主被陷害"背叛"男主 | 女主是被迫/被陷害的 | 男主报复女主 |
+| **身份误会** | 女主隐瞒真实身份 | 女主有苦衷 | 男主觉得被欺骗 |
+| **目的误会** | 男主以为女主接近他有目的 | 女主只是单纯喜欢他 | 男主冷酷对待 |
+
+**虐点爆发模板**:
+```
+【误会产生】
+苏念亲眼看到林慕寒和一个美女拥抱。
+她的心,碎了一地。
+(原来……他有别的女人了……)
+
+【误会加深】
+"林慕寒,我们分手吧。"
+"为什么?"他皱眉。
+"因为……我配不上你。"(真实原因:以为他有别的女人)
+"好。"他冷冷地说,转身离开。
+
+【真相揭晓】
+"她是我妹妹!我唯一的妹妹!"
+林慕寒抓住她的肩膀,眼中满是急切。
+"苏念,你怎么能因为这个误会就要离开我?"
+苏念愣住了。
+(妹妹……?我误会他了……)
+```
+
+**虐点强度**: ⭐⭐⭐⭐(中高强度)
+
+---
+
+### 类型 2: 冷暴力虐(Cold Violence Torture)
+
+**核心公式**:
+```
+男主因误会对女主冷暴力 → 女主委屈隐忍 → 积累到极限 → 女主离开 → 男主后悔
+```
+
+**冷暴力表现形式**:
+```
+1. 语言冷漠: "我很忙。" / "你走吧。" / "别烦我。"
+2. 视而不见: 女主在他面前,他完全当她不存在
+3. 拒绝接触: 不让女主碰他,不回家,不接电话
+4. 言语伤害: "你以为你是谁?" / "我从来没爱过你。"
+```
+
+**虐点爆发模板**:
+```
+【冷暴力开始】
+"林慕寒……"苏念想拉他的手。
+他冷冷地甩开:"别碰我。"
+苏念的手僵在半空,眼泪滑落。
+
+【冷暴力升级】
+苏念做好晚饭等他,但他根本不回家。
+她打电话,他挂断。
+她发消息,他不回。
+她等到深夜,一个人哭着吃完冷掉的饭菜。
+
+【女主极限】
+"够了……我真的……撑不下去了……"
+苏念收拾行李,泪流满面。
+"林慕寒,我走了。这次,真的走了。"
+她留下一封信,离开了这个城市。
+
+【男主后悔】
+林慕寒看到信,脸色惨白。
+"苏念……"
+他疯狂地找她,但她已经消失了。
+```
+
+**虐点强度**: ⭐⭐⭐⭐⭐(超高强度)
+
+---
+
+### 类型 3: 第三者虐(Third Party Torture)
+
+**核心公式**:
+```
+第三者出现 → 威胁女主地位 → 女主危机感 → 男主表态(或暧昧) → 女主心碎
+```
+
+**第三者类型**:
+| 类型 | 设定 | 虐点 | 结局 |
+|------|------|------|------|
+| **白月光** | 男主初恋回来了 | 女主自卑:"我比不上她" | 男主表态:"过去的就过去了" |
+| **门当户对** | 家族安排的联姻对象 | 家族施压 | 男主对抗家族 |
+| **青梅竹马** | 从小一起长大 | 有共同回忆 | 男主澄清:"她只是朋友" |
+| **暗恋者** | 一直守护女主的人 | 男主吃醋 | 女主拒绝,选择男主 |
+
+**虐点爆发模板**:
+```
+【第三者登场】
+"慕寒哥哥,好久不见~"
+一个精致的女人走进来,亲昵地挽住林慕寒的手臂。
+苏念的心,揪了起来。
+(她是谁……为什么这么亲密……)
+
+【女主危机感】
+"她是你的初恋吧?"
+"……是。"林慕寒沉默。
+"那你们……"
+"过去的事了。"他淡淡地说。
+但苏念还是不安……
+(他……还爱着她吗……)
+
+【虐点爆发】
+那女人故意在苏念面前秀恩爱。
+"我和慕寒从小一起长大,感情可好了~"
+"我们还一起……"
+"够了!"苏念忍不住了。
+"对不起,我……我先走了……"
+她转身逃走,眼泪止不住地流。
+
+【男主表态】
+林慕寒当着所有人的面,牵起苏念的手。
+"我只爱她。过去的,就让它过去吧。"
+他看着那个女人:"以后别再来找我了。"
+```
+
+**虐点强度**: ⭐⭐⭐⭐(中高强度)
+
+---
+
+### 类型 4: 生离死别虐(Life & Death Torture)
+
+**核心公式**:
+```
+女主/男主遇到生命危险 → 另一方崩溃 → 拼命救 → 劫后余生 → 更珍惜
+```
+
+**生死场景**:
+```
+1. 意外: 车祸、坠崖、溺水
+2. 疾病: 绝症、失忆、植物人
+3. 牺牲: 为保护对方而受伤
+4. 绑架: 被坏人绑架,生死未卜
+```
+
+**虐点爆发模板**:
+```
+【女主遇险】
+"苏念!"
+林慕寒看到她从楼上掉下去,眼睛瞬间红了。
+他飞奔过去,接住了她。
+但她已经昏迷,鲜血染红了他的衣服。
+"苏念……别死……求你别死……"
+他的泪水滴在她脸上。
+
+【医院抢救】
+手术进行了八个小时。
+林慕寒在门外等待,浑身颤抖。
+(如果她有事……我也不活了……)
+
+【劫后余生】
+"她醒了!"
+林慕寒冲进病房,紧紧抱住她。
+"苏念……你终于醒了……"
+他的声音哽咽,眼中满是劫后余生的庆幸。
+```
+
+**虐点强度**: ⭐⭐⭐⭐⭐⭐(极致强度)
+
+---
+
+### 类型 5: 身份悬殊虐(Identity Gap Torture)
+
+**核心公式**:
+```
+身份差距 → 女主自卑 → 外界压力 → 女主想离开 → 男主霸道留下
+```
+
+**身份差距来源**:
+```
+- 金钱: 男主亿万富翁 vs 女主穷困潦倒
+- 地位: 男主总裁/王爷 vs 女主员工/平民
+- 家世: 男主豪门继承人 vs 女主孤儿
+- 能力: 男主天才 vs 女主普通人
+```
+
+**虐点爆发模板**:
+```
+【外界压力】
+"你配不上我儿子!"林慕寒的母亲扔给她一张支票。
+"拿着钱,离开他。"
+苏念咬着嘴唇,泪水滑落。
+(她说得对……我确实配不上他……)
+
+【女主自卑】
+"林慕寒,我们分手吧。"
+"为什么?"
+"因为……我配不上你。"
+"配不上?"他冷笑,"谁说的?"
+"你母亲说得对……我只是一个普通人……"
+"那又怎样?"他抓住她的肩膀。
+"苏念,我爱的是你,不是你的身份!"
+
+【男主霸道】
+"我不管别人怎么说。"
+"我只知道,你是我的。"
+林慕寒将她紧紧抱住。
+"谁都不能把你从我身边带走。"
+```
+
+**虐点强度**: ⭐⭐⭐⭐(中高强度)
+
+---
+
+## 2. 虐点的强度分级
+
+### 1级虐(日常小委屈)
+
+**特点**: 小误会、小吃醋、小赌气
+
+**示例**:
+```
+"你为什么不接我电话?"苏念噘嘴。
+"开会,手机静音了。"林慕寒无奈。
+"哼,你就是不在乎我!"
+"好好好,我的错。"他把她抱进怀里。
+```
+
+**虐点强度**: ⭐
+**作用**: 调节气氛,增加小情趣
+
+---
+
+### 2级虐(明显矛盾)
+
+**特点**: 冷战、赌气、小分手
+
+**示例**:
+```
+"苏念,你别无理取闹!"
+"我无理取闹?那你去找你的白月光吧!"
+"你……!"
+苏念摔门而出。
+林慕寒站在原地,懊恼地扯了扯领带。
+```
+
+**虐点强度**: ⭐⭐⭐
+**作用**: 制造波折,推动剧情
+
+---
+
+### 3级虐(严重危机)
+
+**特点**: 分手、背叛、误会爆发
+
+**示例**:
+```
+"林慕寒,我们分手吧。"
+"为什么?!"
+"因为……我们不合适。"
+"不合适?你昨天还说爱我!"
+"我……我不爱了。"
+她转身离去,留下他一人站在雨中。
+```
+
+**虐点强度**: ⭐⭐⭐⭐⭐
+**作用**: 情节高潮,虐心虐肺
+
+---
+
+### 4级虐(生死相隔)
+
+**特点**: 意外、疾病、生离死别
+
+**示例**:
+```
+"林慕寒……如果我死了……你会想我吗?"
+"别说傻话!你不会有事的!"
+他紧紧抱住她,声音颤抖。
+"医生!医生!求你救救她!"
+```
+
+**虐点强度**: ⭐⭐⭐⭐⭐⭐
+**作用**: 极致虐点,情感爆发
+
+---
+
+## 3. 虐点的节奏控制
+
+### 黄金虐点公式
+
+```
+甜(5-10章) → 虐(2-5章) → 甜(5-10章) → 虐(3-8章) → 甜(10-20章)
+```
+
+**原则**:
+- 虐完必甜,虐不过三
+- 虐点不要拖太久(最多10-15章解开)
+- 虐后的甜要比虐前更甜
+
+---
+
+### 错误节奏(禁忌)
+
+#### 禁忌 1: 虐而不解
+```
+第10章: 男主误会女主
+第20章: 还在误会
+第30章: 继续误会
+第40章: 依然误会
+```
+**问题**: 读者会骂:"真相什么时候大白?!"
+
+#### 禁忌 2: 为虐而虐
+```
+第10章: 误会→分手
+第15章: 和好
+第20章: 又误会→又分手
+第25章: 又和好
+第30章: 又误会→又分手
+```
+**问题**: 重复套路,读者会腻。
+
+#### 禁忌 3: 虐过头不补偿
+```
+男主一直虐女主,从头虐到尾。
+女主被虐得死去活来。
+最后男主道个歉,女主就原谅了。
+```
+**问题**: 补偿不足,读者不买账。
+
+---
+
+### 正确节奏示例
+
+```
+第1-10章: 甜蜜日常,感情升温
+第11章: 第三者出现(虐点预警)
+第12-15章: 第三者挑拨,女主危机感(虐)
+第16章: 男主表态,赶走第三者(甜)
+第17-25章: 感情更稳定,加倍甜蜜(补偿)
+第26章: 男主母亲反对(虐点预警)
+第27-32章: 家族施压,女主想离开(虐)
+第33章: 男主对抗家族,保护女主(甜)
+第34-50章: 结婚,超级甜(大补偿)
+```
+
+---
+
+## 4. 追妻火葬场(虐后补偿)
+
+### 火葬场公式
+
+```
+男主犯错 → 女主离开 → 男主后悔 → 疯狂追妻 → 女主原谅
+```
+
+**追妻火葬场必备元素**:
+1. 男主真诚道歉
+2. 男主疯狂补偿(送花、跪求、各种哄)
+3. 男主改变(从冷酷变温柔)
+4. 真相大白(误会解开)
+5. 女主心软(但不能太快原谅)
+
+---
+
+### 火葬场强度分级
+
+#### Level 1: 轻度火葬场(道歉+送花)
+
+```
+"苏念,对不起。"
+林慕寒捧着一大束玫瑰花。
+"是我错了,原谅我好吗?"
+苏念别过脸:"……你走吧。"
+"我不走。"他固执地站在门外。
+"你不原谅我,我就一直站在这里。"
+```
+
+#### Level 2: 中度火葬场(下跪+求原谅)
+
+```
+"苏念,求你原谅我……"
+林慕寒跪在她面前,眼中满是悔恨。
+"当初是我瞎了眼,是我不信你……"
+"求你……再给我一次机会……"
+苏念转过身,泪流满面。
+"林慕寒……你让我怎么原谅你……"
+```
+
+#### Level 3: 重度火葬场(自残+生死相逼)
+
+```
+"苏念,如果你不原谅我……"
+林慕寒拿起刀,抵在自己心口。
+"我就死在你面前!"
+"你疯了吗!"苏念慌了。
+"我不疯,我只是……真的很爱你……"
+他的泪水滑落。
+"没有你……我活着还有什么意义……"
+```
+
+---
+
+### 火葬场的甜度补偿
+
+**原则**: 虐得有多深,补偿就要有多甜
+
+```
+1级虐 → 补偿: 道歉+小礼物
+2级虐 → 补偿: 道歉+送花+陪伴
+3级虐 → 补偿: 道歉+下跪+疯狂追求+公开表白
+4级虐 → 补偿: 道歉+下跪+以命相逼+结婚+生子
+```
+
+---
+
+## 5. 虐点的细节描写
+
+### 女主哭泣描写
+
+**错误示例(流水账)**:
+```
+苏念哭了。
+她哭得很伤心。
+```
+
+**正确示例(细节丰富)**:
+```
+苏念咬着嘴唇,拼命忍住。
+但泪水还是不争气地滑落。
+她蹲在地上,肩膀剧烈颤抖。
+"为什么……为什么要这样对我……"
+她哭得撕心裂肺。
+```
+
+---
+
+### 男主后悔描写
+
+**错误示例(告诉式)**:
+```
+林慕寒很后悔。
+```
+
+**正确示例(展示式)**:
+```
+林慕寒坐在空荡荡的房间里,手里拿着她留下的信。
+"苏念……"
+他的手颤抖,眼睛发红。
+(我到底做了什么……)
+(我怎么能这样对她……)
+他第一次感受到,什么叫后悔。
+```
+
+---
+
+### 冷暴力细节
+
+```
+【冷暴力的杀伤力】
+苏念做好晚饭,等他回家。
+但他根本不回来。
+她打电话,他挂断。
+她发消息,他不回。
+她一个人坐在餐桌前,看着冷掉的饭菜。
+泪水滴在碗里。
+"为什么……为什么要这样……"
+她哭着吃完所有的饭菜。
+(也许……他真的不爱我了……)
+```
+
+---
+
+## 6. 虐点与复仇快感结合
+
+### 复仇虐(女主复仇)
+
+**公式**:
+```
+女主被虐→女主变强→女主回归→打脸渣男→男主后悔→追妻火葬场
+```
+
+**示例**:
+```
+【前期被虐】
+林慕寒当众羞辱她:"你算什么东西?"
+苏念心如刀割,离开了他。
+
+【中期蜕变】
+三年后。
+苏念已经成为商业女强人,美丽又强大。
+
+【后期打脸】
+"林总,好久不见。"
+苏念笑得优雅,眼中却是冷漠。
+林慕寒愣住了。
+(这……这还是当初那个卑微的她吗……)
+
+【追妻火葬场】
+"苏念,对不起……"
+林慕寒放下所有骄傲,跪在她面前。
+"求你……原谅我……"
+苏念看着他,冷笑:"林总,您这是做什么?"
+"当初您不是说我算什么东西吗?"
+"现在怎么……跪下了?"
+```
+
+**爽点**: 女主复仇+打脸+男主后悔
+
+---
+
+### 男配复仇(男二上位)
+
+**公式**:
+```
+男主渣→男二默默守护→女主离开男主→男二趁虚而入→男主后悔莫及
+```
+
+**示例**:
+```
+【男二守护】
+男主冷暴力时,男二默默陪伴女主。
+"别哭了,不值得。"
+他递给她纸巾,眼中满是心疼。
+
+【男二表白】
+"苏念,跟我在一起吧。"
+"我会好好对你的。"
+"不会让你再受伤。"
+
+【男主后悔】
+林慕寒看到苏念和男二在一起,眼睛红了。
+(我……我失去她了……)
+(都是我的错……)
+```
+
+**爽点**: 渣男后悔+男二上位+女主幸福
+
+---
+
+## 7. 虐点自检清单
+
+- [ ] **有原因吗**: 虐点是否有明确的触发原因?(不能无缘无故虐)
+- [ ] **会解开吗**: 虐点是否有明确的解决方案?(虐而不解=弃文)
+- [ ] **够深刻吗**: 虐点是否能引发读者共鸣?(要虐到读者心里)
+- [ ] **有补偿吗**: 虐完之后是否有足够的甜蜜补偿?(虐:甜 = 4:6)
+- [ ] **不过度吗**: 是否避免了连续10章以上的纯虐?(虐不过三)
+- [ ] **符合人设吗**: 角色的反应是否符合其性格?(不能OOC)
+
+---
+
+## 🛠️ 虐点设计速查表
+
+| 虐点类型 | 虐点强度 | 持续章节 | 解决方式 | 补偿方式 |
+|---------|---------|---------|---------|---------|
+| **误会虐** | ⭐⭐⭐⭐ | 5-15章 | 真相大白 | 追妻火葬场 |
+| **冷暴力虐** | ⭐⭐⭐⭐⭐ | 3-10章 | 男主后悔 | 疯狂追妻+改变 |
+| **第三者虐** | ⭐⭐⭐⭐ | 3-10章 | 男主表态 | 公开宣示+赶走第三者 |
+| **生离死别虐** | ⭐⭐⭐⭐⭐⭐ | 2-5章 | 劫后余生 | 结婚+珍惜+超甜 |
+| **身份悬殊虐** | ⭐⭐⭐⭐ | 长期背景 | 男主对抗家族 | 公开关系+结婚 |
+
+---
+
+## 附录:经典虐点案例
+
+### 案例 1: 《何以笙箫默》七年分离虐
+
+**虐点**: 何以琛因误会赵默笙,冷暴力,导致她离开七年。
+
+**补偿**: 七年后重逢,何以琛疯狂追妻,各种补偿。
+
+**名台词**: "如果当时没有错过,我们会不会更幸福?"
+
+---
+
+### 案例 2: 反面教材(某扑街文)
+
+```
+男主一直虐女主,从头虐到尾。
+女主被虐得死去活来,男主还是不悔改。
+最后女主原谅了男主,两人在一起了。
+```
+
+**问题**:
+- 男主没有成长,虐而不改
+- 女主太圣母,没有自尊
+- 补偿不足,读者不买账
+
+---
+
+## 虐点设计的终极原则
+
+```
+虐要虐得有理有据
+虐要虐得刻骨铭心
+虐完必须真相大白
+虐完必须加倍补偿
+```
+
+**记住**: 虐是手段,甜是目的。虐得再深,最后都要给读者一个圆满的结局。

+ 288 - 0
.claude/genres/period-drama/ancient-dialogue.md

@@ -0,0 +1,288 @@
+# 古言对话与文风
+
+> 古言的魅力在于语言。本文档提供写出古风韵味对话的方法。
+
+---
+
+## 一、古风语言基础
+
+### 现代词 → 古风词 转换表
+
+| 现代词 | 古风词 | 使用场景 |
+|--------|--------|---------|
+| 我 | 我/吾/本宫/臣妾/奴婢 | 根据身份选择 |
+| 你 | 你/汝/尔/阁下/公子 | 根据关系选择 |
+| 是 | 是/然/正是/确是 | 肯定回答 |
+| 不是 | 非/不然/并非 | 否定回答 |
+| 为什么 | 为何/何故/缘何 | 疑问 |
+| 怎么办 | 如何是好/该当如何 | 询问 |
+| 知道 | 知晓/知悉/得知 | 了解信息 |
+| 不知道 | 不知/不晓得/未曾听闻 | 不了解 |
+| 谢谢 | 多谢/谢过/感激不尽 | 感谢 |
+| 对不起 | 恕罪/告罪/得罪 | 道歉 |
+| 没关系 | 无妨/不打紧/无碍 | 原谅 |
+| 好的 | 好/善/遵命/是 | 同意 |
+| 等一下 | 且慢/稍候/且住 | 暂停 |
+| 快点 | 速速/快些/赶紧 | 催促 |
+| 走吧 | 走罢/去罢/启程 | 离开 |
+
+---
+
+## 二、不同身份的说话方式
+
+### 皇帝
+```
+自称:朕、寡人
+特点:威严、简洁、不容置疑
+示例:
+- "准奏。"
+- "朕乏了。"
+- "此事,朕自有决断。"
+- "你可知罪?"
+```
+
+### 皇后/太后
+```
+自称:本宫、哀家(太后)
+特点:端庄、威仪、不怒自威
+示例:
+- "本宫知道了,你退下吧。"
+- "放肆!谁给你的胆子?"
+- "这后宫,还轮不到你来指手画脚。"
+```
+
+### 妃嫔
+```
+自称:臣妾、妾身、本宫(高位)
+特点:根据性格和地位变化
+示例:
+- 温婉型:"臣妾不敢。"
+- 骄纵型:"本宫说的话,你敢不听?"
+- 心机型:"姐姐说的是,妹妹受教了。"
+```
+
+### 宫女/太监
+```
+自称:奴婢、奴才
+特点:恭敬、谨慎、察言观色
+示例:
+- "奴婢遵命。"
+- "回主子的话,是这样的..."
+- "奴婢不敢妄言。"
+```
+
+### 官员
+```
+自称:臣、微臣、下官
+特点:正式、谨慎、有分寸
+示例:
+- "臣有本要奏。"
+- "微臣惶恐。"
+- "此事,臣以为..."
+```
+
+### 世家公子/小姐
+```
+自称:在下、小生、小女
+特点:有教养、知书达理
+示例:
+- "在下有礼了。"
+- "小女不才,献丑了。"
+- "公子谬赞。"
+```
+
+---
+
+## 三、古风对话技巧
+
+### 1. 省略主语
+```
+现代:我知道了。
+古风:知道了。
+
+现代:你先退下吧。
+古风:退下吧。
+```
+
+### 2. 倒装句式
+```
+现代:你怎么来了?
+古风:你如何来了?/ 怎的来了?
+
+现代:这是什么意思?
+古风:此话何意?/ 这是何意?
+```
+
+### 3. 文言虚词
+```
+常用虚词:
+- 罢了:算了、而已
+- 便是:就是
+- 竟是:居然是
+- 原是:原来是
+- 倒是:反而是
+```
+
+### 4. 四字成语/词组
+```
+多用四字表达:
+- 不必多言
+- 无需如此
+- 言重了
+- 过奖了
+- 恕难从命
+- 恭敬不如从命
+```
+
+---
+
+## 四、情绪表达
+
+### 愤怒
+```
+轻度:"放肆!"
+中度:"你好大的胆子!"
+重度:"来人!拖下去!"
+极度:"朕要诛你九族!"
+```
+
+### 悲伤
+```
+含蓄:"罢了..."
+委婉:"心中...有些不适。"
+直接:"为何...要如此待我?"
+崩溃:"老天何其不公!"
+```
+
+### 喜悦
+```
+含蓄:"甚好。"
+明显:"当真?太好了!"
+激动:"天佑我大X!"
+```
+
+### 讽刺
+```
+轻度:"妹妹好手段。"
+中度:"姐姐真是好算计。"
+重度:"不愧是X家的女儿,果然...与众不同。"
+```
+
+---
+
+## 五、经典对话模板
+
+### 初次见面
+```
+A:"敢问姑娘芳名?"
+B:"小女姓X,单名一个X字。"
+A:"原来是X姑娘,幸会。"
+B:"公子客气了。"
+```
+
+### 请安问好
+```
+"给皇上/娘娘请安。"
+"起来吧。"
+"谢皇上/娘娘。"
+```
+
+### 拒绝请求
+```
+委婉:"此事...恐怕不妥。"
+直接:"恕难从命。"
+强硬:"休要再提!"
+```
+
+### 威胁警告
+```
+含蓄:"姐姐好自为之。"
+明显:"若再有下次,休怪本宫不客气。"
+直接:"你最好祈祷,别让本宫抓到把柄。"
+```
+
+### 表白/暗示
+```
+含蓄:"公子于我,不同旁人。"
+明显:"我心悦你。"
+直接:"此生,我只认你一人。"
+```
+
+---
+
+## 六、文风把控
+
+### 轻古风(推荐新手)
+```
+特点:
+- 基本用白话文
+- 关键词用古风词替换
+- 对话有古风感,叙述偏现代
+
+示例:
+她看着他,心中五味杂陈。
+"你为何要这样做?"
+"因为..."他顿了顿,"我不想你受伤。"
+```
+
+### 中古风(主流)
+```
+特点:
+- 对话全部古风化
+- 叙述半文半白
+- 有一定的文言句式
+
+示例:
+她凝视着他,心中百感交集。
+"你为何要如此?"
+他沉默片刻,方才开口:"只因...不愿见你受伤。"
+```
+
+### 重古风(需要功底)
+```
+特点:
+- 全文文言化
+- 大量使用典故
+- 句式讲究
+
+示例:
+她凝眸望他,心下五味陈杂。
+"君何以至此?"
+他默然良久,方启唇道:"只因...不忍卿受半分委屈。"
+```
+
+---
+
+## 七、常见错误
+
+### 称谓错误
+```
+❌ 皇后自称"哀家"(哀家是太后用的)
+❌ 对皇帝说"您"(古代没有"您")
+❌ 妃嫔互称"亲爱的"(太现代)
+```
+
+### 用词错误
+```
+❌ "OK"、"没问题"(现代词)
+❌ "我觉得"(太口语)
+❌ "你知道吗"(太现代)
+```
+
+### 语气错误
+```
+❌ 宫女对主子说"我不"(太硬)
+❌ 皇帝说"拜托"(太卑微)
+❌ 大臣说"随便"(太随意)
+```
+
+---
+
+## 八、对话检查清单
+
+- [ ] 称谓是否符合身份?
+- [ ] 用词是否有古风感?
+- [ ] 语气是否符合人物性格?
+- [ ] 是否有现代词汇混入?
+- [ ] 文风是否前后一致?
+- [ ] 对话是否推进了剧情?

+ 268 - 0
.claude/genres/period-drama/character-design.md

@@ -0,0 +1,268 @@
+# 古言人物设计
+
+> 古言人物需要"古"的气质。本文档提供设计符合时代的人物方法。
+
+---
+
+## 一、女主类型
+
+### 1. 宅斗型女主
+```
+特点:
+- 聪慧、隐忍、有手段
+- 善于察言观色
+- 懂得借力打力
+
+成长线:
+被欺负 → 学会反击 → 掌控内宅 → 成为当家主母
+
+代表:《知否》盛明兰
+```
+
+### 2. 宫斗型女主
+```
+特点:
+- 心思缜密
+- 善于隐藏
+- 关键时刻果断
+
+成长线:
+入宫 → 站稳脚跟 → 步步高升 → 登顶/全身而退
+
+代表:《甄嬛传》甄嬛
+```
+
+### 3. 权谋型女主
+```
+特点:
+- 有政治头脑
+- 格局大
+- 不拘泥于后宅
+
+成长线:
+被卷入 → 展露才能 → 参与大事 → 影响朝局
+
+代表:《琅琊榜》霓凰郡主
+```
+
+### 4. 甜宠型女主
+```
+特点:
+- 性格讨喜
+- 有小聪明
+- 运气好
+
+成长线:
+相遇 → 被宠 → 小波折 → 幸福结局
+
+适用:轻松甜文
+```
+
+---
+
+## 二、男主类型
+
+### 1. 帝王型
+```
+特点:
+- 威严、深沉
+- 多疑但深情
+- 权力与爱情的矛盾
+
+魅力点:
+- 万人之上只对她温柔
+- 为她打破规则
+- 霸道占有
+```
+
+### 2. 王爷型
+```
+特点:
+- 尊贵但不是最高位
+- 可以更自由
+- 有自己的势力
+
+魅力点:
+- 不争皇位只要她
+- 可以带她远走
+- 亦正亦邪
+```
+
+### 3. 世家公子型
+```
+特点:
+- 温润如玉
+- 家世显赫
+- 有教养
+
+魅力点:
+- 君子风度
+- 默默守护
+- 温柔坚定
+```
+
+### 4. 将军型
+```
+特点:
+- 铁血硬汉
+- 战场英雄
+- 不善言辞
+
+魅力点:
+- 外冷内热
+- 用行动表达爱
+- 保护欲强
+```
+
+---
+
+## 三、配角设计
+
+### 反派女配
+```
+类型1:嫡女/正室
+- 出身高贵,看不起女主
+- 手段狠辣
+- 最终下场凄惨
+
+类型2:白月光
+- 表面清纯
+- 实则心机
+- 与男主有旧情
+
+类型3:恶婆婆/恶嫂
+- 刁难女主
+- 偏心其他人
+- 最终被打脸
+```
+
+### 助攻配角
+```
+类型1:忠心丫鬟
+- 从小跟着女主
+- 忠心耿耿
+- 关键时刻帮忙
+
+类型2:好姐妹
+- 同为妃嫔/妯娌
+- 互相扶持
+- 真心相待
+
+类型3:暗卫/侍卫
+- 保护女主
+- 可能暗恋女主
+- 关键时刻救命
+```
+
+---
+
+## 四、人物关系设计
+
+### 宫廷关系
+```
+核心三角:皇帝 - 女主 - 皇后/宠妃
+辅助关系:太后、其他妃嫔、皇子公主
+外围关系:前朝势力、母族势力
+```
+
+### 宅斗关系
+```
+核心三角:女主 - 丈夫 - 婆婆/妾室
+辅助关系:妯娌、小姑、下人
+外围关系:娘家、姻亲
+```
+
+### 权谋关系
+```
+核心三角:女主 - 男主 - 政敌
+辅助关系:盟友、下属、家族
+外围关系:朝廷各派、边疆势力
+```
+
+---
+
+## 五、人物弧光
+
+### 女主成长弧
+```
+阶段1:天真/弱小
+- 不懂规则
+- 被欺负
+- 有人保护
+
+阶段2:觉醒/学习
+- 开始反击
+- 学会手段
+- 建立势力
+
+阶段3:强大/掌控
+- 成为高手
+- 保护他人
+- 达成目标
+```
+
+### 男主变化弧
+```
+类型1:冷到热
+- 初期冷漠
+- 逐渐心动
+- 最终深情
+
+类型2:误解到理解
+- 初期误会
+- 发现真相
+- 追悔弥补
+
+类型3:成长型
+- 初期不成熟
+- 经历磨难
+- 成为合格的丈夫/帝王
+```
+
+---
+
+## 六、人物细节
+
+### 外貌描写
+```
+女主:
+- 不必倾国倾城
+- 有特点即可(眼睛/气质)
+- 符合时代审美
+
+男主:
+- 俊美但不娘
+- 有气势
+- 符合身份
+```
+
+### 技能设定
+```
+女主可以有的技能:
+- 医术(常见)
+- 厨艺(讨喜)
+- 琴棋书画(才女)
+- 经商头脑(独立)
+- 武功(特殊)
+
+注意:技能要有来源,不能凭空出现
+```
+
+### 性格缺陷
+```
+让人物更真实:
+- 女主可以有小心眼、记仇
+- 男主可以有大男子主义、多疑
+- 缺陷要在可接受范围内
+```
+
+---
+
+## 七、人物检查清单
+
+- [ ] 人物是否符合时代背景?
+- [ ] 人物的行为是否符合身份?
+- [ ] 人物是否有成长变化?
+- [ ] 人物关系是否清晰?
+- [ ] 人物是否有记忆点?
+- [ ] 人物的技能是否有来源?

+ 278 - 0
.claude/genres/period-drama/historical-setting.md

@@ -0,0 +1,278 @@
+# 古言题材历史背景设定
+
+> 古言的魅力在于"古"。本文档提供构建可信历史背景的方法。
+
+---
+
+## 一、朝代选择
+
+### 常用朝代特点
+
+| 朝代 | 特点 | 适合题材 |
+|------|------|---------|
+| 架空 | 自由度高 | 宫斗、权谋、甜宠 |
+| 唐 | 开放、繁华 | 盛世、女性题材 |
+| 宋 | 文雅、市井 | 商战、日常、探案 |
+| 明 | 礼教、锦衣卫 | 权谋、悬疑 |
+| 清 | 宫廷、满汉 | 宫斗、夺嫡 |
+
+### 架空朝代设定
+```
+优势:
+- 不受历史约束
+- 可以自由设定规则
+- 避免历史考据争议
+
+注意:
+- 要有内在一致性
+- 参考真实朝代但不照搬
+- 建立自己的世界观
+```
+
+---
+
+## 二、社会结构
+
+### 阶层划分
+```
+皇室:皇帝、皇后、妃嫔、皇子、公主
+宗室:亲王、郡王、贝勒、贝子
+勋贵:公侯伯子男、世家大族
+官员:文官、武将、地方官
+平民:商人、农民、工匠
+贱籍:奴婢、乐户、罪犯
+```
+
+### 女性地位
+```
+正妻:主母,管理内宅
+妾室:地位低于正妻
+通房:丫鬟抬举
+婢女:服侍主人
+
+注意:
+- 嫡庶之分很重要
+- 正妻有管理妾室的权力
+- 妾室所生为庶出
+```
+
+### 家族结构
+```
+大家族:
+- 族长(通常是嫡长房)
+- 各房(大房、二房...)
+- 嫡出、庶出
+- 旁支、远亲
+
+小家庭:
+- 父母
+- 兄弟姐妹
+- 妻妾子女
+```
+
+---
+
+## 三、日常生活
+
+### 称谓系统
+```
+对皇帝:皇上、陛下、圣上
+对皇后:皇后娘娘、母后
+对妃嫔:X妃娘娘、娘娘
+对王爷:王爷、殿下
+对官员:大人、老爷
+对夫人:夫人、太太、奶奶
+自称:臣、臣妾、奴婢、小女子
+```
+
+### 服饰
+```
+男子:
+- 常服:长袍、直裰
+- 官服:补服、朝服
+- 配饰:玉佩、扇子
+
+女子:
+- 常服:襦裙、褙子
+- 礼服:凤冠霞帔
+- 配饰:步摇、钗环
+```
+
+### 饮食
+```
+主食:米饭、面食、粥
+菜肴:根据地域和阶层不同
+饮品:茶、酒
+点心:糕点、果子
+
+注意:
+- 土豆、玉米、辣椒是明朝后才有
+- 不同阶层饮食差异大
+```
+
+### 出行
+```
+女子:轿子、马车(不宜抛头露面)
+男子:骑马、坐轿、马车
+平民:步行、驴车
+```
+
+---
+
+## 四、礼仪规范
+
+### 见面礼
+```
+跪拜:对皇帝、长辈
+作揖:平辈男子
+万福:女子行礼
+请安:日常问候
+```
+
+### 婚嫁礼
+```
+六礼:
+1. 纳采(提亲)
+2. 问名(合八字)
+3. 纳吉(订婚)
+4. 纳征(送聘礼)
+5. 请期(定日子)
+6. 亲迎(迎娶)
+```
+
+### 丧葬礼
+```
+- 守孝三年(实际27个月)
+- 丁忧(官员守孝需辞官)
+- 服丧期间不能婚嫁
+```
+
+### 禁忌
+```
+- 女子不能随意见外男
+- 寡妇再嫁受歧视
+- 冲撞长辈是大不敬
+- 后宫不得干政
+```
+
+---
+
+## 五、宫廷设定
+
+### 后宫等级(以清朝为例)
+```
+皇后(1人)
+皇贵妃(1人)
+贵妃(2人)
+妃(4人)
+嫔(6人)
+贵人(无定数)
+常在(无定数)
+答应(无定数)
+```
+
+### 宫廷建筑
+```
+前朝:处理政务
+后宫:妃嫔居住
+东宫:太子居所
+各宫:以宫名命名(如:坤宁宫、长春宫)
+```
+
+### 宫廷人员
+```
+太监:内侍、公公
+宫女:服侍妃嫔
+嬷嬷:年长女官
+侍卫:保护安全
+```
+
+---
+
+## 六、科举仕途
+
+### 科举流程
+```
+童试 → 秀才
+乡试 → 举人
+会试 → 贡士
+殿试 → 进士(状元、榜眼、探花)
+```
+
+### 官职系统
+```
+中央:
+- 内阁(大学士)
+- 六部(尚书、侍郎)
+- 都察院(御史)
+
+地方:
+- 总督、巡抚
+- 知府、知县
+```
+
+### 仕途发展
+```
+- 翰林院(清贵)
+- 六部(实权)
+- 外放(地方官)
+- 京官(中央)
+```
+
+---
+
+## 七、常见设定错误
+
+### 称谓错误
+```
+❌ 皇后自称"本宫"(应为"本宫"或"臣妾"视场合)
+❌ 对皇帝称"皇上您"(不能用"您")
+❌ 妃嫔互称"姐姐"(应按位分称呼)
+```
+
+### 礼仪错误
+```
+❌ 女子随意出门逛街
+❌ 未婚男女单独相处
+❌ 妾室与正妻平起平坐
+```
+
+### 常识错误
+```
+❌ 古代有玻璃窗(应为纸窗或纱窗)
+❌ 随便吃辣椒(明朝前没有)
+❌ 女子读书识字很普遍(实际是少数)
+```
+
+---
+
+## 八、历史感营造
+
+### 语言风格
+```
+避免现代词汇:
+❌ "OK""没问题""搞定"
+✓ "好""可以""妥了"
+
+使用古风词汇:
+- 用"银子"不用"钱"
+- 用"用膳"不用"吃饭"
+- 用"歇息"不用"休息"
+```
+
+### 细节描写
+```
+用古代物品:
+- 油灯、蜡烛(不是电灯)
+- 毛笔、砚台(不是钢笔)
+- 铜镜(不是玻璃镜)
+```
+
+### 思维方式
+```
+古人的价值观:
+- 重视家族荣誉
+- 讲究门当户对
+- 遵守礼教规范
+- 相信命运天意
+```

+ 268 - 0
.claude/genres/period-drama/palace-intrigue.md

@@ -0,0 +1,268 @@
+# 古言宫斗权谋设计
+
+> 宫斗是古言的经典题材。本文档提供设计精彩宫斗的方法。
+
+---
+
+## 一、宫斗核心要素
+
+| 要素 | 内容 | 作用 |
+|------|------|------|
+| 目标 | 争宠/夺位/复仇/生存 | 驱动剧情 |
+| 资源 | 圣宠/家世/人脉/情报 | 斗争筹码 |
+| 手段 | 阴谋/阳谋/借刀/联盟 | 实现目标 |
+| 代价 | 失宠/降位/性命/亲人 | 增加张力 |
+
+---
+
+## 二、宫斗势力布局
+
+### 后宫势力划分
+```
+第一梯队(顶级威胁):
+- 皇后(正宫之主)
+- 宠妃(圣眷正隆)
+- 太后(幕后大佬)
+
+第二梯队(潜在对手):
+- 有子嗣的妃嫔
+- 有家世的妃嫔
+- 有手段的妃嫔
+
+第三梯队(可拉拢):
+- 失宠的旧人
+- 无依无靠的新人
+- 心怀不满的宫人
+```
+
+### 前朝势力关联
+```
+后宫 ←→ 前朝 对应关系:
+- 皇后 ←→ 皇后母族(外戚)
+- 宠妃 ←→ 宠妃父兄(新贵)
+- 太后 ←→ 太后母族(老牌势力)
+- 皇子 ←→ 皇子党羽(夺嫡势力)
+```
+
+---
+
+## 三、宫斗手段分类
+
+### 1. 阴谋类
+```
+陷害:
+- 栽赃嫁祸(在对方宫中放违禁品)
+- 制造丑闻(捏造私通/不敬)
+- 借刀杀人(挑拨他人动手)
+
+暗害:
+- 下毒(慢性毒/绝育药/致疯药)
+- 意外(滑倒/落水/走水)
+- 收买(买通对方身边人)
+```
+
+### 2. 阳谋类
+```
+争宠:
+- 投其所好(研究皇帝喜好)
+- 制造机会(偶遇/才艺展示)
+- 欲擒故纵(若即若离)
+
+立功:
+- 揭发阴谋(救驾/告密)
+- 解决难题(帮皇帝分忧)
+- 生育子嗣(最大的功劳)
+```
+
+### 3. 联盟类
+```
+结盟:
+- 利益交换(互相帮助)
+- 共同敌人(敌人的敌人是朋友)
+- 主从关系(依附强者)
+
+分化:
+- 挑拨离间(制造矛盾)
+- 利益诱惑(拉拢对方的人)
+- 暴露真相(揭示盟友的背叛)
+```
+
+---
+
+## 四、宫斗节奏设计
+
+### 升级模式
+```
+初入宫 → 站稳脚跟 → 小有地位 → 遭遇危机 → 绝地反击 → 登顶/结局
+
+每个阶段的对手等级递增:
+1. 同级妃嫔的小打小闹
+2. 高位妃嫔的打压
+3. 皇后/宠妃的正面冲突
+4. 太后/前朝的终极对决
+```
+
+### 攻防节奏
+```
+被动防守期:
+- 初入宫,不了解规则
+- 被针对,学会自保
+- 积累资源和人脉
+
+主动出击期:
+- 有了一定地位
+- 开始反击
+- 逐步扩大势力
+
+攻守兼备期:
+- 成为主要势力
+- 既要防守又要进攻
+- 多线作战
+```
+
+---
+
+## 五、经典宫斗桥段
+
+### 1. 初入宫
+```
+- 被高位妃嫔立下马威
+- 被分配到冷宫/偏殿
+- 第一次见皇帝(惊艳/失礼/特别)
+- 结识第一个盟友/敌人
+```
+
+### 2. 争宠
+```
+- 偶遇皇帝(精心设计的偶遇)
+- 才艺展示(琴棋书画/厨艺/医术)
+- 与众不同(不争不抢反而引起注意)
+- 救驾/解围(危机中的表现)
+```
+
+### 3. 被陷害
+```
+- 被栽赃(宫中发现违禁品)
+- 被诬陷(捏造的证人证词)
+- 被设计(落入圈套)
+- 被误会(皇帝亲眼所见的假象)
+```
+
+### 4. 反击
+```
+- 揭露真相(找到真正的证据)
+- 将计就计(利用对方的阴谋)
+- 借力打力(让对方自相残杀)
+- 绝地反击(濒死时的翻盘)
+```
+
+### 5. 高潮对决
+```
+- 当众对质(大殿/宴会上的摊牌)
+- 证据链条(一环扣一环的揭露)
+- 最终审判(皇帝/太后的裁决)
+- 敌人下场(死/废/贬/疯)
+```
+
+---
+
+## 六、宫斗智商线
+
+### 聪明的表现
+```
+✓ 提前布局,留有后手
+✓ 不轻易暴露真实意图
+✓ 善于利用规则和人心
+✓ 知道什么时候该忍
+✓ 能看穿对方的阴谋
+```
+
+### 避免降智
+```
+✗ 明显的陷阱还往里跳
+✗ 轻易相信敌人的话
+✗ 在不该说话时说话
+✗ 把计划告诉不该知道的人
+✗ 低估对手的智商
+```
+
+### 合理的失误
+```
+可以犯错,但要合理:
+- 信息不对称导致的误判
+- 感情用事的冲动
+- 被更高明的对手算计
+- 意外因素的干扰
+```
+
+---
+
+## 七、宫斗情感线
+
+### 帝妃感情
+```
+类型:
+- 真爱型:皇帝真心爱女主
+- 利用型:互相利用,日久生情
+- 虐恋型:爱而不得,误会重重
+- 救赎型:女主改变了皇帝
+```
+
+### 感情与权谋的平衡
+```
+纯宫斗:感情是工具,权谋是核心
+宫斗言情:权谋是背景,感情是核心
+平衡型:权谋和感情并重
+
+注意:
+- 不要让感情线拖慢宫斗节奏
+- 不要让宫斗冲淡感情发展
+- 关键时刻要有取舍
+```
+
+---
+
+## 八、宫斗结局类型
+
+### HE结局
+```
+- 登上后位,与帝王相守
+- 扶持皇子登基,成为太后
+- 离开皇宫,与真爱远走
+- 权倾天下,功成身退
+```
+
+### BE结局
+```
+- 斗赢了所有人,却失去了爱
+- 登上高位,却发现一切都是空
+- 为了复仇,付出了一切
+- 最终死在权力的游戏中
+```
+
+### 开放结局
+```
+- 她看着那把椅子,笑了
+- 新的妃嫔入宫了,故事重新开始
+- 她终于走出了那道宫门
+```
+
+---
+
+## 九、宫斗检查清单
+
+### 逻辑检查
+- [ ] 势力布局是否清晰?
+- [ ] 每个阴谋是否有合理的执行方式?
+- [ ] 女主的反击是否有足够的铺垫?
+- [ ] 智商线是否在线?
+
+### 节奏检查
+- [ ] 是否有张有弛?
+- [ ] 对手等级是否递进?
+- [ ] 高潮是否足够精彩?
+
+### 情感检查
+- [ ] 感情线是否与宫斗线协调?
+- [ ] 读者是否会为女主紧张?
+- [ ] 结局是否有情感满足?

+ 276 - 0
.claude/genres/period-drama/plot-patterns.md

@@ -0,0 +1,276 @@
+# 古言剧情模式
+
+> 古言有其经典的剧情模式。本文档提供常用的剧情框架和桥段。
+
+---
+
+## 一、经典剧情框架
+
+### 1. 宫斗框架
+```
+入宫 → 站稳 → 争宠 → 危机 → 登顶/退出
+
+详细:
+1. 入宫(选秀/赐婚/家族安排)
+2. 初入后宫(被欺负/找靠山)
+3. 获得圣宠(才艺/性格/机缘)
+4. 树敌(皇后/宠妃的打压)
+5. 步步高升(晋位/生子)
+6. 重大危机(被陷害/失宠)
+7. 绝地反击(揭露真相/翻盘)
+8. 最终结局(登后位/离宫/帝后和谐)
+```
+
+### 2. 宅斗框架
+```
+嫁入 → 立足 → 掌家 → 危机 → 当家
+
+详细:
+1. 嫁入(高嫁/低嫁/冲喜)
+2. 婆媳过招(立规矩/被刁难)
+3. 妯娌争斗(争资源/争宠)
+4. 丈夫关系(冷淡/渐热/误会)
+5. 管家权争夺
+6. 家族危机(抄家/落魄)
+7. 力挽狂澜
+8. 成为当家主母
+```
+
+### 3. 权谋框架
+```
+卷入 → 站队 → 博弈 → 决战 → 新局
+
+详细:
+1. 被卷入政治漩涡
+2. 选择阵营/被迫站队
+3. 参与谋划
+4. 多方博弈
+5. 关键情报/转折
+6. 最终决战
+7. 新的格局
+```
+
+### 4. 甜宠框架
+```
+相遇 → 相知 → 相爱 → 小虐 → HE
+
+详细:
+1. 特别的相遇
+2. 日常相处
+3. 心动时刻
+4. 确定关系
+5. 小波折(误会/阻碍)
+6. 解决问题
+7. 甜蜜结局
+```
+
+---
+
+## 二、经典桥段库
+
+### 相遇桥段
+```
+1. 救命之恩
+   - 她救了他(不知身份)
+   - 他救了她(英雄救美)
+
+2. 冲撞/冒犯
+   - 不小心冲撞了贵人
+   - 不知身份出言不逊
+
+3. 替嫁/冲喜
+   - 代替姐妹出嫁
+   - 冲喜给"将死之人"
+
+4. 指腹为婚
+   - 父辈的约定
+   - 多年后相见
+
+5. 意外同处
+   - 被困一处
+   - 意外同床
+```
+
+### 虐心桥段
+```
+1. 误会
+   - 亲眼看到"出轨"
+   - 被人挑拨离间
+
+2. 被迫分离
+   - 家族反对
+   - 政治原因
+   - 生死相隔
+
+3. 替身/白月光
+   - 发现自己是替身
+   - 白月光归来
+
+4. 小产/失子
+   - 被害小产
+   - 孩子夭折
+
+5. 冷暴力
+   - 被冷落
+   - 被忽视
+```
+
+### 甜蜜桥段
+```
+1. 吃醋
+   - 看到她和别人说话
+   - 听到有人追求她
+
+2. 偷偷关心
+   - 以为她不知道
+   - 其实她都知道
+
+3. 当众护短
+   - 有人欺负她
+   - 他霸气护妻
+
+4. 日常宠溺
+   - 投其所好
+   - 事事顺着
+
+5. 仪式感
+   - 大婚/册封
+   - 生辰/节日
+```
+
+### 打脸桥段
+```
+1. 身份反转
+   - 以为是穷亲戚,其实是贵女
+   - 以为是弃妃,其实是真爱
+
+2. 实力展示
+   - 被嘲笑后展露才华
+   - 关键时刻力挽狂澜
+
+3. 真相揭露
+   - 陷害者被揭穿
+   - 白月光原形毕露
+
+4. 打脸现场
+   - 曾经看不起的人求上门
+   - 曾经嘲笑的话被打脸
+```
+
+---
+
+## 三、剧情节奏
+
+### 宫斗节奏
+```
+卷一(入宫):20%
+- 建立人设
+- 了解规则
+- 第一次小胜
+
+卷二(争宠):30%
+- 获得圣宠
+- 树敌
+- 步步高升
+
+卷三(危机):30%
+- 重大危机
+- 跌入谷底
+- 绝地反击
+
+卷四(结局):20%
+- 最终对决
+- 登顶/退出
+- 收尾
+```
+
+### 甜宠节奏
+```
+相遇期:15%
+暧昧期:25%
+热恋期:30%
+小虐期:15%
+HE期:15%
+```
+
+---
+
+## 四、剧情设计技巧
+
+### 1. 伏笔设置
+```
+前期埋下的线索:
+- 一个不起眼的人物
+- 一句无心的话
+- 一个小物件
+
+后期揭示:
+- 原来那个人是关键
+- 那句话暗示了真相
+- 那个物件是证据
+```
+
+### 2. 反转设计
+```
+类型1:身份反转
+- 以为是A,其实是B
+
+类型2:关系反转
+- 以为是敌人,其实是盟友
+
+类型3:真相反转
+- 以为是这样,其实是那样
+
+类型4:结局反转
+- 以为是HE,其实是BE(或反之)
+```
+
+### 3. 高潮设计
+```
+宫斗高潮:
+- 当众对质
+- 证据链揭露
+- 最终审判
+
+宅斗高潮:
+- 分家
+- 休妻/和离
+- 真相大白
+
+权谋高潮:
+- 政变
+- 决战
+- 登基
+```
+
+---
+
+## 五、避免的问题
+
+### 剧情问题
+```
+❌ 金手指太多(运气太好)
+❌ 反派太蠢(智商不在线)
+❌ 节奏拖沓(无意义的日常)
+❌ 虐得没道理(为虐而虐)
+❌ 结局仓促(草草收尾)
+```
+
+### 逻辑问题
+```
+❌ 时间线混乱
+❌ 人物行为不符合身份
+❌ 历史常识错误
+❌ 权力运作不合理
+```
+
+---
+
+## 六、剧情检查清单
+
+- [ ] 主线是否清晰?
+- [ ] 节奏是否合适?
+- [ ] 伏笔是否回收?
+- [ ] 高潮是否精彩?
+- [ ] 结局是否满意?
+- [ ] 逻辑是否通顺?

+ 234 - 0
.claude/genres/realistic/character-depth.md

@@ -0,0 +1,234 @@
+# 现实题材人物深度塑造
+
+> 现实题材的人物必须"像真人"。本文档提供塑造立体人物的方法。
+
+---
+
+## 一、立体人物三维度
+
+| 维度 | 内容 | 作用 |
+|------|------|------|
+| 社会维度 | 职业、阶层、家庭背景 | 定义人物的外在处境 |
+| 心理维度 | 性格、欲望、恐惧、创伤 | 定义人物的内在驱动 |
+| 道德维度 | 价值观、底线、灰色地带 | 定义人物的选择逻辑 |
+
+---
+
+## 二、社会维度构建
+
+### 1. 职业身份
+```
+不只是"她是医生"
+而是:
+- 哪个科室?(急诊/外科/儿科)
+- 什么职称?(住院医/主治/主任)
+- 工作几年?(新人/中坚/老资历)
+- 职业困境?(晋升/医患/平衡)
+```
+
+### 2. 经济状况
+```
+具体化:
+- 月收入多少?
+- 有无房贷车贷?
+- 存款有多少?
+- 消费习惯如何?
+```
+
+### 3. 家庭背景
+```
+原生家庭:
+- 父母职业和性格
+- 家庭氛围(温暖/冷漠/控制)
+- 童年经历(创伤/幸福)
+- 与父母的现状关系
+```
+
+### 4. 社会关系
+```
+- 朋友圈层(多少/质量)
+- 同事关系(融洽/紧张)
+- 邻里关系(熟悉/陌生)
+- 社交习惯(内向/外向)
+```
+
+---
+
+## 三、心理维度构建
+
+### 1. 核心欲望
+```
+每个人都有一个核心欲望:
+- 被爱/被认可/被尊重
+- 安全感/控制感/自由
+- 成功/金钱/权力
+- 归属感/存在感/意义感
+```
+
+### 2. 深层恐惧
+```
+与欲望对应的恐惧:
+- 渴望被爱 → 害怕被抛弃
+- 渴望成功 → 害怕失败
+- 渴望自由 → 害怕被束缚
+- 渴望安全 → 害怕失控
+```
+
+### 3. 心理创伤
+```
+过去的伤痕影响现在的行为:
+- 童年被忽视 → 成年后讨好型人格
+- 曾被背叛 → 难以信任他人
+- 经历过贫穷 → 对金钱极度敏感
+- 失去过至亲 → 害怕亲密关系
+```
+
+### 4. 防御机制
+```
+人物如何保护自己:
+- 逃避:不面对问题
+- 攻击:先发制人
+- 讨好:牺牲自己
+- 隔离:切断情感
+- 合理化:给自己找借口
+```
+
+---
+
+## 四、道德维度构建
+
+### 1. 价值排序
+```
+当价值冲突时,人物会选择什么?
+- 家庭 vs 事业
+- 爱情 vs 面包
+- 原则 vs 利益
+- 自己 vs 他人
+```
+
+### 2. 道德底线
+```
+人物绝对不会做的事:
+- 有人底线是不说谎
+- 有人底线是不伤害家人
+- 有人底线是不违法
+- 有人没有底线
+```
+
+### 3. 灰色地带
+```
+人物会纠结的选择:
+- 为了孩子,要不要忍受糟糕的婚姻?
+- 为了晋升,要不要做违心的事?
+- 为了生存,要不要放弃原则?
+```
+
+---
+
+## 五、人物弧光设计
+
+### 1. 正向弧光
+```
+从缺陷到成长:
+- 自卑 → 自信
+- 逃避 → 面对
+- 依赖 → 独立
+- 封闭 → 开放
+```
+
+### 2. 负向弧光
+```
+从正常到堕落:
+- 善良 → 冷漠
+- 信任 → 怀疑
+- 热情 → 麻木
+- 坚持 → 妥协
+```
+
+### 3. 平弧光
+```
+坚持自我,改变世界:
+- 人物始终坚持某个信念
+- 通过行动影响周围的人
+- 证明某种价值观的力量
+```
+
+---
+
+## 六、人物关系网络
+
+### 1. 核心关系
+```
+影响人物最深的关系:
+- 原生家庭(父母/兄弟姐妹)
+- 亲密关系(伴侣/前任)
+- 重要他人(导师/挚友/对手)
+```
+
+### 2. 关系动态
+```
+关系不是静态的:
+- 从陌生到亲密
+- 从信任到背叛
+- 从依赖到独立
+- 从对立到和解
+```
+
+### 3. 关系功能
+```
+每段关系都有功能:
+- 镜子:照出人物的真实
+- 催化剂:推动人物改变
+- 对照组:衬托人物特质
+- 考验:测试人物底线
+```
+
+---
+
+## 七、人物细节清单
+
+### 外在细节
+```
+- 外貌特征(不需要完美)
+- 穿着习惯(反映性格/经济)
+- 小动作(紧张时/思考时)
+- 说话方式(口头禅/语速/用词)
+```
+
+### 生活细节
+```
+- 作息习惯
+- 饮食偏好
+- 休闲方式
+- 消费习惯
+```
+
+### 内在细节
+```
+- 最骄傲的事
+- 最后悔的事
+- 最害怕的事
+- 最想要的东西
+```
+
+---
+
+## 八、人物检查清单
+
+### 立体度检查
+- [ ] 人物是否有明确的欲望和恐惧?
+- [ ] 人物是否有过去的创伤或经历?
+- [ ] 人物是否有道德灰色地带?
+- [ ] 人物是否会让读者又爱又恨?
+
+### 真实度检查
+- [ ] 人物的行为是否符合其背景?
+- [ ] 人物的选择是否有充分动机?
+- [ ] 人物的情感反应是否真实?
+- [ ] 人物是否有成长或变化?
+
+### 独特度检查
+- [ ] 这个人物是否可以被其他人替代?
+- [ ] 人物是否有独特的说话方式?
+- [ ] 人物是否有标志性的特征?
+- [ ] 人物是否让人印象深刻?

+ 284 - 0
.claude/genres/realistic/dialogue-authenticity.md

@@ -0,0 +1,284 @@
+# 现实题材对话真实感
+
+> 对话是现实题材的照妖镜。本文档提供写出真实对话的技巧。
+
+---
+
+## 一、真实对话特征
+
+### 1. 不完整性
+```
+书面语:我认为这件事情应该这样处理。
+口语:这事儿吧...我觉得...算了,你看着办。
+```
+
+### 2. 重复与犹豫
+```
+书面语:我不同意你的观点。
+口语:不是...我不是那个意思...就是...你懂吗?
+```
+
+### 3. 省略与跳跃
+```
+书面语:你今天下班后要去哪里?
+口语:今晚?
+```
+
+### 4. 口头禅与语气词
+```
+- 嗯、啊、哦、呃
+- 然后、就是、反正
+- 真的假的、不是吧、我去
+```
+
+---
+
+## 二、不同身份的说话方式
+
+### 按年龄
+```
+00后:
+- "绝绝子""yyds""无语子"
+- 语速快,网络用语多
+- 表情包式表达
+
+80后:
+- 相对正式
+- 偶尔用网络语但不熟练
+- "这个...怎么说呢"
+
+60后:
+- 更正式,少用网络语
+- 喜欢用成语或俗语
+- "我跟你说啊..."
+```
+
+### 按职业
+```
+医生:
+- 专业术语自然带出
+- 说话简洁直接
+- "情况不太乐观"
+
+律师:
+- 逻辑严密
+- 喜欢用"首先、其次、最后"
+- "从法律角度来说..."
+
+程序员:
+- 技术词汇
+- 直接,不绕弯
+- "这个需求不合理"
+```
+
+### 按性格
+```
+内向:
+- 话少,常用"嗯""哦"
+- 被动回应多
+- 长句少
+
+外向:
+- 话多,主动发起话题
+- 语气词丰富
+- 喜欢反问
+
+强势:
+- 陈述句多,疑问句少
+- 打断别人
+- "听我说"
+
+弱势:
+- 疑问句多
+- 常用"可能""也许"
+- "你觉得呢?"
+```
+
+---
+
+## 三、对话功能
+
+### 1. 推进剧情
+```
+错误:用对话解释背景
+"你知道吗,我们公司成立于2010年,主要业务是..."
+
+正确:用对话制造冲突
+"辞职信我放你桌上了。"
+"你疯了?"
+```
+
+### 2. 展示人物
+```
+不要说"她很强势"
+而是写对话:
+"这事就这么定了。"
+"可是..."
+"没有可是。"
+```
+
+### 3. 传递信息
+```
+错误:直接告知
+"他出轨了,对象是你闺蜜。"
+
+正确:暗示+反应
+"昨晚我看到他的车...停在小区门口。"
+"哪个小区?"
+"...你闺蜜家那个。"
+```
+
+### 4. 制造张力
+```
+潜台词比明说更有力:
+"你最近...还好吗?"
+"挺好的。"
+"那就好。"
+(两人都知道不好,但都不说破)
+```
+
+---
+
+## 四、对话节奏
+
+### 快节奏(冲突/紧张)
+```
+"你什么意思?"
+"字面意思。"
+"你..."
+"我怎么了?"
+"我们离婚。"
+```
+
+### 慢节奏(情感/沉重)
+```
+她沉默了很久。
+"你...是认真的?"
+他没有回答。
+窗外的雨越下越大。
+"我等你三年了。"她终于开口,"三年。"
+```
+
+### 节奏变化
+```
+先快后慢:争吵后的沉默
+先慢后快:酝酿后的爆发
+```
+
+---
+
+## 五、潜台词技巧
+
+### 1. 说A指B
+```
+表面:"天气真好。"
+潜台词:"我不想聊这个话题。"
+```
+
+### 2. 答非所问
+```
+"你爱我吗?"
+"饭做好了。"
+```
+
+### 3. 沉默
+```
+"你能解释一下吗?"
+他没有说话。
+"我问你话呢。"
+还是沉默。
+```
+
+### 4. 转移话题
+```
+"你昨晚去哪了?"
+"对了,你妈打电话来了。"
+```
+
+---
+
+## 六、常见对话场景模板
+
+### 争吵
+```
+特点:
+- 打断对方
+- 翻旧账
+- 人身攻击
+- 沉默或摔门
+
+示例:
+"你每次都这样!"
+"我怎么了?我——"
+"你闭嘴!"
+"行,我闭嘴,我走还不行吗?"
+```
+
+### 分手
+```
+特点:
+- 长沉默
+- 回忆
+- 最后的挣扎
+- 体面或崩溃
+
+示例:
+"我们...还是算了吧。"
+"...为什么?"
+"没有为什么。"
+"是因为她吗?"
+"...你想多了。"
+```
+
+### 表白
+```
+特点:
+- 紧张
+- 试探
+- 犹豫
+- 等待回应
+
+示例:
+"那个...我有件事想跟你说。"
+"嗯?"
+"就是...算了,没什么。"
+"你说啊。"
+"我...我喜欢你。"
+```
+
+### 摊牌
+```
+特点:
+- 直接
+- 证据
+- 质问
+- 等待解释
+
+示例:
+"这是什么?"
+"...你翻我手机?"
+"我问你这是什么。"
+"我可以解释..."
+"那你解释。"
+```
+
+---
+
+## 七、对话检查清单
+
+### 真实性检查
+- [ ] 这句话真的会有人这么说吗?
+- [ ] 符合人物的年龄/职业/性格吗?
+- [ ] 有没有太书面化?
+- [ ] 有没有适当的口语特征?
+
+### 功能性检查
+- [ ] 这段对话推进了剧情吗?
+- [ ] 展示了人物性格吗?
+- [ ] 有没有废话可以删?
+- [ ] 潜台词够不够?
+
+### 节奏检查
+- [ ] 对话长度是否合适?
+- [ ] 快慢节奏是否有变化?
+- [ ] 沉默和停顿用得好吗?

+ 273 - 0
.claude/genres/realistic/plot-logic.md

@@ -0,0 +1,273 @@
+# 现实题材剧情逻辑
+
+> 现实题材最怕"不合理"。本文档提供构建严密剧情逻辑的方法。
+
+---
+
+## 一、逻辑链构建
+
+### 因果链原则
+```
+每个事件都需要:
+- 原因(为什么发生)
+- 过程(怎么发生)
+- 结果(发生后怎样)
+- 影响(对后续的影响)
+```
+
+### 示例
+```
+事件:她辞职了
+
+错误写法:
+她突然辞职了。(没有原因)
+
+正确写法:
+原因:连续加班三个月,母亲又住院
+过程:深夜在办公室收到医院电话,第二天递交辞呈
+结果:失去收入来源,但能照顾母亲
+影响:经济压力增大,为后续矛盾埋下伏笔
+```
+
+---
+
+## 二、动机合理性
+
+### 动机三要素
+```
+1. 欲望:想要什么
+2. 障碍:什么阻碍
+3. 行动:如何克服
+```
+
+### 动机强度检验
+```
+问自己:这个动机足够让人物做出这个行为吗?
+
+弱动机:
+"因为无聊所以出轨" ❌
+
+强动机:
+"婚姻名存实亡五年,在最脆弱的时候遇到了理解她的人" ✓
+```
+
+### 常见动机
+```
+生存类:金钱、健康、安全
+情感类:爱、被爱、归属
+尊严类:尊重、认可、地位
+自我类:成长、自由、意义
+```
+
+---
+
+## 三、巧合处理
+
+### 巧合使用原则
+```
+1. 巧合可以制造困境,不能解决困境
+2. 一个故事最多用1-2个巧合
+3. 巧合需要铺垫
+```
+
+### 错误示例
+```
+"正当她走投无路时,突然中了彩票。" ❌
+(巧合解决困境)
+```
+
+### 正确示例
+```
+"她在咖啡店偶遇前男友,而她正挽着现任的手。"
+(巧合制造困境)
+```
+
+### 巧合铺垫技巧
+```
+让巧合看起来不那么巧:
+- 提前暗示可能性
+- 给出合理的时空条件
+- 用人物反应强化真实感
+```
+
+---
+
+## 四、时间线管理
+
+### 时间线检查
+```
+- 事件发生的顺序是否合理?
+- 时间间隔是否足够?
+- 有没有时间冲突?
+```
+
+### 常见时间问题
+```
+问题1:时间不够
+"她用一周时间从零开始学会了编程" ❌
+
+问题2:时间冲突
+"她上午在北京开会,下午在上海见客户" ❌
+(除非有合理的交通安排)
+
+问题3:时间跳跃不自然
+"三年后"突然出现,没有任何过渡 ❌
+```
+
+### 时间线工具
+```
+建议制作时间轴:
+- 列出所有重要事件
+- 标注具体时间
+- 检查逻辑冲突
+```
+
+---
+
+## 五、人物行为逻辑
+
+### 行为一致性
+```
+人物的行为要符合其:
+- 性格设定
+- 过往经历
+- 当前处境
+- 认知水平
+```
+
+### 行为转变
+```
+如果人物行为发生重大转变,需要:
+- 足够的触发事件
+- 渐进的心理变化
+- 合理的转变过程
+```
+
+### 示例
+```
+错误:
+"一向懦弱的她突然变得强势起来。"
+
+正确:
+"被欺负了三年后,当她看到女儿也被同样对待时,
+她第一次站了出来。"
+(有触发事件,有合理动机)
+```
+
+---
+
+## 六、信息传递逻辑
+
+### 信息来源
+```
+人物知道的信息必须有来源:
+- 亲眼所见
+- 亲耳所闻
+- 他人告知
+- 推理得出
+```
+
+### 常见问题
+```
+问题:人物知道不该知道的事
+"她知道他在外面有人了。"
+(她怎么知道的?需要交代)
+
+解决:
+"她在他衬衫上闻到了陌生的香水味。"
+```
+
+### 信息差设计
+```
+利用信息差制造戏剧性:
+- 读者知道,人物不知道(悬念)
+- 人物知道,读者不知道(反转)
+- A知道,B不知道(误会)
+```
+
+---
+
+## 七、社会规则逻辑
+
+### 法律常识
+```
+涉及法律问题时要准确:
+- 离婚程序
+- 财产分割
+- 抚养权
+- 劳动法
+- 刑事责任
+```
+
+### 职场规则
+```
+- 晋升流程
+- 辞职程序
+- 竞业协议
+- 加班规定
+```
+
+### 医疗常识
+```
+- 疾病症状
+- 治疗流程
+- 医院规则
+- 医保报销
+```
+
+---
+
+## 八、逻辑漏洞检查清单
+
+### 因果检查
+- [ ] 每个重要事件都有原因吗?
+- [ ] 原因足够充分吗?
+- [ ] 结果符合逻辑吗?
+
+### 动机检查
+- [ ] 人物的行为有足够动机吗?
+- [ ] 动机符合人物性格吗?
+- [ ] 读者能理解这个动机吗?
+
+### 时间检查
+- [ ] 时间线是否清晰?
+- [ ] 有没有时间冲突?
+- [ ] 时间跨度是否合理?
+
+### 信息检查
+- [ ] 人物的信息来源清楚吗?
+- [ ] 有没有"全知"问题?
+- [ ] 信息传递是否合理?
+
+### 常识检查
+- [ ] 有没有违反法律常识?
+- [ ] 有没有违反职业常识?
+- [ ] 有没有违反生活常识?
+
+---
+
+## 九、修复逻辑漏洞
+
+### 方法1:补充铺垫
+```
+发现漏洞:她怎么知道密码的?
+修复:前文加一句"她无意中看到他输入密码"
+```
+
+### 方法2:调整设定
+```
+发现漏洞:时间不够
+修复:把"一周"改成"三个月"
+```
+
+### 方法3:增加解释
+```
+发现漏洞:行为不合理
+修复:增加心理描写,解释动机
+```
+
+### 方法4:删除情节
+```
+发现漏洞:无法自圆其说
+修复:删掉这个情节,换一种方式
+```

+ 229 - 0
.claude/genres/realistic/reality-anchoring.md

@@ -0,0 +1,229 @@
+# 现实题材真实感锚定
+
+> 现实题材的核心是"真实感"。本文档提供让读者相信故事的技巧。
+
+---
+
+## 一、真实感三层架构
+
+| 层次 | 内容 | 作用 |
+|------|------|------|
+| 表层 | 细节真实 | 让读者"看到"场景 |
+| 中层 | 逻辑真实 | 让读者"相信"情节 |
+| 深层 | 情感真实 | 让读者"共鸣"人物 |
+
+---
+
+## 二、细节真实技巧
+
+### 1. 职业细节
+```
+错误:她是一名医生,每天救死扶伤。
+正确:她是急诊科的住院医师,值完36小时班后,
+      在休息室的行军床上睡了4个小时,
+      被护士叫醒时,嘴里还含着没吃完的面包。
+```
+
+### 2. 生活细节
+```
+错误:他很穷。
+正确:月底还有三天,他的银行卡余额是47.6元。
+      他把外卖软件上的"满减"研究了半小时,
+      最后点了一份9.9的黄焖鸡。
+```
+
+### 3. 环境细节
+```
+错误:这是一个老旧的小区。
+正确:楼道里的声控灯坏了三个月没人修,
+      她每天摸黑爬六楼,
+      楼梯扶手上的油漆已经斑驳脱落。
+```
+
+### 4. 时代细节
+```
+2024年特征:
+- 手机支付、外卖、网约车
+- 996、内卷、躺平
+- 短视频、直播带货
+- 房贷、车贷、消费贷
+```
+
+---
+
+## 三、逻辑真实技巧
+
+### 1. 因果链完整
+```
+错误:她突然辞职了。
+正确:连续加班三个月后,她在凌晨两点的办公室里
+      收到了母亲住院的消息。
+      第二天,她递交了辞职信。
+```
+
+### 2. 动机合理
+```
+错误:他为了爱情放弃了一切。
+正确:他权衡了三个月。
+      最后让他下定决心的,
+      是她在机场送他时说的那句"我等你"。
+```
+
+### 3. 后果真实
+```
+错误:她创业成功了。
+正确:她的公司终于盈利了,
+      但她也失去了三年的青春、
+      一段婚姻、和曾经最好的朋友。
+```
+
+---
+
+## 四、情感真实技巧
+
+### 1. 复杂情感
+```
+单一情感(假):她恨他。
+复杂情感(真):她恨他,但更恨自己还爱着他。
+                每次看到他的朋友圈,
+                她都会先点进去看,再骂自己犯贱。
+```
+
+### 2. 矛盾心理
+```
+人物内心的真实矛盾:
+- 想辞职又怕找不到工作
+- 想离婚又舍不得孩子
+- 想表白又怕被拒绝
+- 想回家又怕面对父母的催婚
+```
+
+### 3. 情绪渐变
+```
+错误:她突然崩溃大哭。
+正确:她一直忍着,直到回到家,
+      关上门的那一刻,
+      眼泪才终于流下来。
+      她蹲在玄关,哭了整整十分钟。
+```
+
+---
+
+## 五、常见职业真实感要点
+
+### 医生
+```
+- 值班制度(24小时/36小时)
+- 医患关系紧张
+- 论文、职称压力
+- 手术前的紧张、术后的疲惫
+- 面对死亡的无力感
+```
+
+### 律师
+```
+- 加班是常态
+- 案源压力
+- 法律条文的枯燥
+- 当事人的不理解
+- 胜诉的成就感vs败诉的挫败
+```
+
+### 程序员
+```
+- 996/007
+- 需求变更的崩溃
+- Bug修不完
+- 35岁危机
+- 头发和健康问题
+```
+
+### 教师
+```
+- 备课、批改作业
+- 家长群的压力
+- 学生的叛逆期
+- 职称评定
+- 寒暑假的"隐形加班"
+```
+
+### 销售
+```
+- 业绩压力
+- 客户的刁难
+- 应酬文化
+- 提成制度
+- 月底冲业绩
+```
+
+---
+
+## 六、社会现实锚点
+
+### 经济压力
+```
+- 房贷:月供占收入的多少
+- 车贷:养车成本
+- 教育:补习班、学区房
+- 医疗:一场大病的花费
+- 养老:父母的医疗和养老
+```
+
+### 职场现实
+```
+- 内卷:加班文化
+- 裁员:35岁危机
+- 晋升:办公室政治
+- 性别:职场歧视
+- 学历:第一学历歧视
+```
+
+### 家庭现实
+```
+- 催婚:父母的压力
+- 婆媳:家庭矛盾
+- 育儿:教育焦虑
+- 养老:独生子女的压力
+- 离婚:财产分割、孩子抚养
+```
+
+---
+
+## 七、避免失真的检查清单
+
+### 细节检查
+- [ ] 职业描写是否符合实际?
+- [ ] 收入水平是否合理?
+- [ ] 生活场景是否真实?
+- [ ] 时代特征是否准确?
+
+### 逻辑检查
+- [ ] 人物动机是否充分?
+- [ ] 情节发展是否合理?
+- [ ] 后果是否符合现实?
+- [ ] 时间线是否可行?
+
+### 情感检查
+- [ ] 情感反应是否真实?
+- [ ] 心理变化是否渐进?
+- [ ] 是否有矛盾和复杂性?
+- [ ] 读者能否产生共鸣?
+
+---
+
+## 八、真实感 vs 戏剧性平衡
+
+### 原则
+```
+真实感是基础,戏剧性是调味
+太真实 = 流水账
+太戏剧 = 假大空
+```
+
+### 平衡技巧
+```
+1. 用真实的细节包装戏剧性的情节
+2. 用合理的逻辑支撑巧合的发生
+3. 用复杂的情感软化极端的冲突
+4. 用日常的场景衬托高光的时刻
+```

+ 232 - 0
.claude/genres/realistic/social-issues.md

@@ -0,0 +1,232 @@
+# 现实题材社会议题处理
+
+> 现实题材常涉及敏感社会议题。本文档提供安全且有深度的处理方式。
+
+---
+
+## 一、常见社会议题
+
+| 类别 | 议题 | 敏感度 |
+|------|------|--------|
+| 职场 | 996、裁员、性骚扰、职场霸凌 | 中 |
+| 家庭 | 家暴、出轨、婆媳矛盾、原生家庭 | 中 |
+| 社会 | 贫富差距、阶层固化、地域歧视 | 高 |
+| 性别 | 性别歧视、生育权、女性困境 | 高 |
+| 教育 | 内卷、学区房、教育公平 | 中 |
+| 医疗 | 看病难、医患矛盾、大病返贫 | 中 |
+
+---
+
+## 二、处理原则
+
+### 1. 呈现而非评判
+```
+错误:这个社会太不公平了!(直接评判)
+正确:她看着银行卡里的余额,又看了看医院的账单。
+      差距是三个零。(呈现事实,让读者自己感受)
+```
+
+### 2. 个体视角切入
+```
+错误:讨论整个社会的996问题
+正确:写一个具体的人在996中的挣扎
+      - 她的身体变化
+      - 她的家庭影响
+      - 她的心理状态
+```
+
+### 3. 多元立场呈现
+```
+不要只写一方的声音:
+- 员工的苦 + 老板的难
+- 患者的怨 + 医生的累
+- 婆婆的固执 + 媳妇的委屈
+```
+
+### 4. 留有希望
+```
+即使是沉重的议题,也要有光:
+- 困境中的温情
+- 绝望中的坚持
+- 黑暗中的微光
+```
+
+---
+
+## 三、具体议题处理指南
+
+### 职场996
+```
+切入角度:
+- 一个人的身体健康变化
+- 一段感情因加班而破裂
+- 一个家庭因工作而疏离
+
+避免:
+- 直接批判企业/制度
+- 煽动对立情绪
+- 过于绝望的结局
+
+可以:
+- 展示个体的挣扎与选择
+- 呈现不同人的不同态度
+- 给出个人层面的出路
+```
+
+### 家庭暴力
+```
+切入角度:
+- 受害者的心理变化
+- 旁观者的无力感
+- 施暴者的成因(不是洗白)
+
+避免:
+- 美化或浪漫化暴力
+- 让受害者"感化"施暴者
+- 过于血腥的描写
+
+可以:
+- 展示受害者的觉醒
+- 呈现求助的困难
+- 给出逃离的可能
+```
+
+### 性别议题
+```
+切入角度:
+- 具体的不公平事件
+- 个人的成长与觉醒
+- 两性关系的复杂性
+
+避免:
+- 极端的性别对立
+- 标签化任何一方
+- 说教式的女权宣言
+
+可以:
+- 展示真实的困境
+- 呈现觉醒的过程
+- 探讨平等的可能
+```
+
+### 贫富差距
+```
+切入角度:
+- 两个阶层的人的相遇
+- 一个人的阶层跨越尝试
+- 金钱与幸福的关系
+
+避免:
+- 仇富或嫌贫
+- 简单的阶层对立
+- 不切实际的逆袭
+
+可以:
+- 展示不同阶层的真实生活
+- 呈现努力与机遇的关系
+- 探讨金钱之外的价值
+```
+
+---
+
+## 四、安全写作技巧
+
+### 1. 模糊化处理
+```
+敏感内容模糊化:
+- 不点名具体公司/地区
+- 用"某市""某公司"代替
+- 时间线适当模糊
+```
+
+### 2. 虚构声明
+```
+在文首或文末加入:
+"本故事纯属虚构,如有雷同,纯属巧合。"
+```
+
+### 3. 平衡叙事
+```
+每个敏感议题都呈现多个角度:
+- 不同立场的人物
+- 不同的观点和选择
+- 不同的结局可能
+```
+
+### 4. 情感落点
+```
+把落点放在情感而非议题:
+- 不是"996不对"
+- 而是"她在996中失去了什么"
+```
+
+---
+
+## 五、议题与故事的融合
+
+### 议题作为背景
+```
+议题是故事发生的环境,不是故事本身
+示例:996是背景,爱情/成长才是主线
+```
+
+### 议题作为冲突
+```
+议题制造人物的困境和选择
+示例:生育问题成为夫妻矛盾的导火索
+```
+
+### 议题作为成长
+```
+人物在面对议题中成长
+示例:她在职场歧视中学会了为自己发声
+```
+
+---
+
+## 六、敏感度自检清单
+
+### 发布前检查
+- [ ] 是否有可能被理解为攻击特定群体?
+- [ ] 是否有可能引发极端情绪?
+- [ ] 是否呈现了多元视角?
+- [ ] 是否有希望和出路?
+- [ ] 是否可能触及红线?
+
+### 红线警示
+```
+绝对避免:
+- 涉及政治敏感话题
+- 煽动群体对立
+- 传播负面价值观
+- 美化违法行为
+- 泄露国家机密
+```
+
+---
+
+## 七、成功案例分析
+
+### 《我的前半生》- 婚姻议题
+```
+成功点:
+- 聚焦个人成长而非批判出轨
+- 展示女性独立的可能
+- 多元人物,没有绝对的好坏
+```
+
+### 《欢乐颂》- 阶层议题
+```
+成功点:
+- 五个女孩代表不同阶层
+- 展示各自的困境和努力
+- 友情作为温暖的底色
+```
+
+### 《都挺好》- 原生家庭
+```
+成功点:
+- 深入剖析家庭问题
+- 人物复杂,不脸谱化
+- 最终走向和解而非对立
+```

+ 524 - 0
.claude/genres/rules-mystery/clue-design.md

@@ -0,0 +1,524 @@
+# 线索设计与公平性 (Clue Design & Fair Play)
+
+> **核心原则**: 线索是推理的基石。公平的线索设计 = 读者能看到 + 读者能理解 + 读者能推理。
+
+---
+
+## 1. 什么是"公平线索"?
+
+### 定义
+**公平线索(Fair Clue)**: 侦探发现的所有关键线索,必须同时向读者展示,且读者**理论上**可以基于这些线索推理出真相。
+
+### 三大标准
+```markdown
+✅ 标准1:可见性(Visibility)
+线索必须在文中明确呈现,不能只存在于侦探脑中
+
+✅ 标准2:可理解性(Comprehensibility)
+线索的含义必须是读者能理解的,不能依赖专业知识
+
+✅ 标准3:可推导性(Deducibility)
+基于线索,读者应该能推导出(或接近)真相
+```
+
+---
+
+## 2. 线索的分类
+
+### 按功能分类
+
+#### 类型1:指向性线索(Pointing Clue)
+**作用**: 直接指向凶手或真相
+
+**示例**:
+```markdown
+线索: 凶手在现场留下了一枚刻有姓名的戒指
+→ 直接指向凶手身份
+```
+
+---
+
+#### 类型2:排除性线索(Elimination Clue)
+**作用**: 排除某些可能性,缩小范围
+
+**示例**:
+```markdown
+线索: 凶案发生时是凌晨3点,而张三有不在场证明
+→ 排除张三是凶手的可能
+```
+
+---
+
+#### 类型3:关联性线索(Connecting Clue)
+**作用**: 连接两个看似无关的事件或人物
+
+**示例**:
+```markdown
+线索A: 死者生前收到一封匿名恐吓信
+线索B: 李四的打字机缺少一个字母键
+→ 推测:恐吓信可能是李四所写
+```
+
+---
+
+#### 类型4:时间线索(Temporal Clue)
+**作用**: 确定事件发生的时间顺序
+
+**示例**:
+```markdown
+线索: 死者的手表停在晚上8点
+→ 推测:案发时间可能是晚上8点
+```
+
+---
+
+#### 类型5:反转线索(Reversal Clue)
+**作用**: 推翻之前的推理,引发反转
+
+**示例**:
+```markdown
+前期线索: 凶器是一把匕首
+反转线索: 法医发现致命伤其实是钝器造成的
+→ 推翻匕首是凶器的假设
+```
+
+---
+
+### 按重要性分类
+
+| 等级 | 名称 | 作用 | 占比 |
+|------|------|------|------|
+| **A级** | 核心线索 | 直接指向真相 | 10% |
+| **B级** | 关键线索 | 缩小嫌疑范围 | 30% |
+| **C级** | 辅助线索 | 提供背景信息 | 40% |
+| **D级** | 装饰线索 | 营造氛围 | 20% |
+
+**注意**: A级线索虽然重要,但数量不宜过多,否则太容易被猜到。
+
+---
+
+## 3. 线索埋设技巧
+
+### 技巧1:前置埋设(Foreshadowing)
+**定义**: 在案发前或侦探调查前,"不经意"提及关键物品或信息。
+
+**示例**:
+```markdown
+## 案例:毒杀案
+第3章(案发前):
+"张三从花园里摘了几朵夹竹桃,说要装饰房间。"
+
+第15章(案发后):
+法医:"死者是夹竹桃中毒而亡。"
+→ 读者恍然大悟:第3章的夹竹桃是关键!
+```
+
+**要点**: 前置埋设要**自然**,不能太刻意。
+
+---
+
+### 技巧2:伪装成背景(Camouflage)
+**定义**: 把关键线索藏在环境描写或日常对话中。
+
+**示例**:
+```markdown
+## 案例:密室杀人
+第7章(环境描写):
+"房间角落的壁橱门微微开着,门框上有些许划痕。"
+
+第20章(揭示):
+侦探:"凶手曾藏在壁橱里,划痕是他进出时留下的。"
+```
+
+**要点**: 读者第一次看时可能忽略,但回看时能发现。
+
+---
+
+### 技巧3:多次提及(Repetition)
+**定义**: 重要线索在文中出现2-3次,加深读者印象。
+
+**示例**:
+```markdown
+第5章: 死者手上有一块昂贵的手表
+第12章: 侦探注意到死者手表的指针停在8点
+第18章: 侦探:"这块手表是关键……"
+```
+
+**要点**: 每次提及角度不同,逐步揭示重要性。
+
+---
+
+### 技巧4:对比呈现(Contrast)
+**定义**: 通过对比,让读者注意到异常。
+
+**示例**:
+```markdown
+张三的证言: "我8点就回家了。"
+监控记录: 张三8点15分才离开公司。
+→ 读者发现:张三在撒谎
+```
+
+---
+
+### 技巧5:分散呈现(Scattered Clues)
+**定义**: 把完整线索拆成多个碎片,分散在不同章节。
+
+**示例**:
+```markdown
+第10章: 死者日记写着"7月15日见到了他"
+第15章: 李四的行程表显示7月15日去了A市
+第20章: A市是死者的老家
+→ 推测:李四在A市见过死者
+```
+
+**要点**: 碎片单独看不起眼,组合起来才有意义。
+
+---
+
+## 4. 线索呈现的"三层法"
+
+### 层次1:明线索(Obvious Clue)
+**定义**: 明确告诉读者"这是线索"
+
+**示例**:
+```markdown
+侦探拿起一根头发:"这是关键线索。"
+```
+
+**作用**: 引导读者注意,适合新手向作品
+
+---
+
+### 层次2:暗线索(Subtle Clue)
+**定义**: 不明说,但有经验的读者能看出来
+
+**示例**:
+```markdown
+侦探扫视房间,目光在壁橱门上停留了片刻。
+```
+
+**作用**: 给读者推理空间,增加参与感
+
+---
+
+### 层次3:隐线索(Hidden Clue)
+**定义**: 极其隐蔽,只有回看时才能发现
+
+**示例**:
+```markdown
+第5章(闲聊):
+李四:"我最近总是失眠,都靠安眠药了。"
+
+第30章(揭示):
+侦探:"凶手用安眠药迷晕死者……李四曾说过他有安眠药!"
+```
+
+**作用**: 制造"恍然大悟"的惊喜感
+
+---
+
+### 三层配比建议
+```markdown
+明线索: 40%(确保读者能跟上)
+暗线索: 40%(给推理空间)
+隐线索: 20%(制造惊喜)
+```
+
+---
+
+## 5. 公平性检验清单
+
+### 检查项1:线索是否向读者呈现?
+**错误示例**:
+```markdown
+侦探(心想):我在现场发现了一个关键证据。
+→ 读者不知道是什么证据
+```
+
+**正确示例**:
+```markdown
+侦探在桌子下方发现了一张撕碎的纸条。
+→ 读者同步看到证据
+```
+
+---
+
+### 检查项2:线索是否可理解?
+**错误示例**:
+```markdown
+侦探:"这种斑点是氰化钾中毒的典型症状。"
+→ 读者不了解化学知识,无法推理
+```
+
+**正确示例**:
+```markdown
+侦探:"这种斑点是中毒症状。而现场只有李四有化学背景。"
+→ 简化专业知识,读者可理解
+```
+
+---
+
+### 检查项3:线索是否足够?
+**标准**:
+```markdown
+✅ 核心真相至少需要 **3条独立线索** 支持
+✅ 每条线索单独看不明显,组合起来指向真相
+```
+
+**示例**:
+```markdown
+线索1: 凶器上有李四的指纹
+线索2: 李四与死者有财务纠纷
+线索3: 李四的不在场证明有漏洞
+→ 三条线索综合,指向李四是凶手
+```
+
+---
+
+### 检查项4:有没有"作者全知陷阱"?
+**定义**: 作者知道真相,所以默认读者也"应该"知道。
+
+**错误示例**:
+```markdown
+作者(潜意识):我已经在第5章提到了钥匙,读者应该能想到密室诡计。
+实际:读者根本没注意到钥匙
+```
+
+**避免方法**:
+```markdown
+✅ 重要线索至少提及 **2次**
+✅ 让测试读者试读,看他们能否推理出真相
+```
+
+---
+
+## 6. 线索与红鲱鱼的平衡
+
+### 黄金比例
+```markdown
+真线索(指向真相): 60%
+红鲱鱼(误导): 40%
+```
+
+**作用**: 既给读者推理线索,又不让真相太容易被猜到。
+
+---
+
+### 红鲱鱼的使用原则
+**原则1**: 红鲱鱼必须**事后可解释**
+
+**错误示例**:
+```markdown
+第10章: 张三在现场,手上有血
+第30章: 侦探:"那个不重要,真凶是李四。"
+→ 张三的血迹没有解释,读者会觉得不合理
+```
+
+**正确示例**:
+```markdown
+第10章: 张三在现场,手上有血
+第30章: 侦探:"张三的血是因为他帮死者包扎过伤口。"
+→ 合理解释
+```
+
+---
+
+**原则2**: 红鲱鱼不能完全凭空捏造
+
+**错误示例**:
+```markdown
+作者突然编造:"李四其实有个双胞胎兄弟。"
+→ 前文从未提及,读者无法推理
+```
+
+**正确示例**:
+```markdown
+第5章: 李四的母亲说:"你小时候总和弟弟打架。"
+第30章: 揭示李四有个弟弟,是真凶
+→ 前文有伏笔
+```
+
+---
+
+## 7. 线索密度控制
+
+### 密度建议
+```markdown
+每章线索数量: 1-3条
+每10章至少有: 1条核心线索(A级)
+```
+
+**过少**: 读者无法推理  
+**过多**: 读者信息过载
+
+---
+
+### 线索分布策略
+
+#### 前期(1-30%进度)
+```markdown
+- 埋设基础线索
+- 建立人物关系
+- 暗示可疑点
+```
+
+#### 中期(30-70%进度)
+```markdown
+- 逐步揭示关键线索
+- 设置红鲱鱼误导
+- 制造小反转
+```
+
+#### 后期(70-100%进度)
+```markdown
+- 汇总所有线索
+- 揭示核心真相
+- 解释红鲱鱼
+```
+
+---
+
+## 8. 线索设计案例分析
+
+### 案例1:《东方快车谋杀案》
+
+**线索呈现**:
+```markdown
+线索1: 12个嫌疑人都有完美不在场证明
+线索2: 死者身上有12处刀伤
+线索3: 每个嫌疑人与死者都有间接联系
+```
+
+**公平性**:
+- ✅ 所有线索公开呈现
+- ✅ 读者可以推理:12人集体作案
+- ✅ 真相既意外又合理
+
+---
+
+### 案例2:《无人生还》
+
+**线索呈现**:
+```markdown
+线索1: 童谣暗示杀人顺序
+线索2: 每个被害者死法与童谣对应
+线索3: 法官"被杀"后医生确认死亡
+```
+
+**公平性**:
+- ✅ 童谣提前呈现
+- ✅ 读者可以推测死亡顺序
+- ✅ 但法官假死的诡计难以预料(意外性)
+
+---
+
+## 9. 常见线索设计错误
+
+### 错误1:线索过于明显
+**示例**:
+```markdown
+第5章: 李四说:"我恨死他了,真想杀了他!"
+→ 太明显,读者立刻怀疑李四
+```
+
+**改进**:
+```markdown
+第5章: 李四(低声):"总有一天……"
+→ 暗示而非明说
+```
+
+---
+
+### 错误2:线索过于隐蔽
+**示例**:
+```markdown
+第3章: 桌上有一只苍蝇
+第30章: 侦探:"苍蝇证明了房间曾被开过窗!"
+→ 读者根本没注意苍蝇
+```
+
+**改进**:
+```markdown
+第3章: 侦探皱眉:"奇怪,这个季节怎么会有苍蝇?"
+→ 引起读者注意
+```
+
+---
+
+### 错误3:线索自相矛盾
+**示例**:
+```markdown
+第10章: 死者手表停在8点
+第15章: 目击者说8点30分听到枪声
+→ 时间线矛盾
+```
+
+**解决**:
+```markdown
+第20章: 侦探:"手表是凶手故意调慢的!"
+→ 解释矛盾
+```
+
+---
+
+## 10. 线索设计工具
+
+### 工具1:线索登记表
+```markdown
+| 线索ID | 出现章节 | 内容 | 类型 | 重要性 | 指向 |
+|--------|---------|------|------|--------|------|
+| C01 | 第5章 | 血迹手帕 | 物证 | A级 | 李四 |
+| C02 | 第10章 | 不在场证明 | 证言 | B级 | 排除张三 |
+```
+
+---
+
+### 工具2:公平性检查清单
+```markdown
+- [ ] 所有A级线索都已向读者呈现?
+- [ ] 线索是否可理解(无需专业知识)?
+- [ ] 是否至少3条独立线索指向真相?
+- [ ] 红鲱鱼是否事后可解释?
+- [ ] 线索密度是否合理(每章1-3条)?
+```
+
+---
+
+### 工具3:测试读者验证法
+**步骤**:
+```markdown
+1. 找3-5名测试读者
+2. 让他们读到揭示真相前的一章
+3. 询问他们的推理结果
+4. 如果没人能推理出真相 → 线索不够
+   如果所有人都猜到 → 线索太明显
+   如果少数人猜到 → 公平性良好
+```
+
+---
+
+## 🛠️ 线索设计速查表
+
+| 线索类型 | 作用 | 数量建议 | 呈现方式 |
+|---------|------|---------|---------|
+| **指向性线索** | 指向凶手 | 2-3条 | 明线索+暗线索 |
+| **排除性线索** | 缩小范围 | 3-5条 | 明线索 |
+| **关联性线索** | 连接事件 | 2-3条 | 暗线索 |
+| **反转线索** | 制造反转 | 1-2条 | 隐线索 |
+
+---
+
+## 附录:线索设计黄金法则
+
+1. **3-2-1法则**: 核心真相需要至少**3条独立线索**,其中**2条明显**,**1条隐蔽**
+2. **双重呈现**: 重要线索必须**至少出现2次**
+3. **合理解释**: 所有红鲱鱼必须**事后可解释**
+4. **测试验证**: 完稿后让测试读者验证公平性
+
+---
+
+## 总结
+
+**公平的线索设计 = 可见性 + 可理解性 + 可推导性**
+
+记住:线索不是用来"炫技"的,而是用来让读者**有能力**推理出真相的。公平竞技的乐趣在于"我也能想到"。

+ 431 - 0
.claude/genres/rules-mystery/core-elements.md

@@ -0,0 +1,431 @@
+# 本格推理核心要素 (Core Elements of Fair-Play Mystery)
+
+> **核心原则**: 本格推理的本质是"读者与侦探的智力游戏"——所有线索必须公平呈现,真相必须可推导。
+
+---
+
+## 1. 什么是本格推理?
+
+### 定义
+**本格推理(Honkaku Mystery)**:又称"正统推理"或"公平竞技推理",强调**逻辑推理**和**线索公平性**。读者与侦探处于同一信息起跑线,理论上可在揭晓前推导出真相。
+
+### 与社会派/冷硬派的区别
+| 类型 | 核心 | 重点 | 示例 |
+|------|------|------|------|
+| **本格推理** | 谜题解答 | 密室/诡计/逻辑 | 《暴风雪山庄》 |
+| **社会派** | 人性/动机 | 犯罪原因/社会批判 | 《白夜行》 |
+| **冷硬派** | 氛围/暴力 | 侦探历险/黑色风格 | 《马耳他之鹰》 |
+
+---
+
+## 2. 本格推理的"十诫"(改良版诺克斯十诫)
+
+### 原版诺克斯十诫(Knox's Ten Commandments, 1929)
+经典规则,但部分已过时。以下为**网文适配版**:
+
+**诫律1**: 罪犯必须在故事早期登场  
+**诫律2**: 超自然力量禁止用于真相解释(但可以用于误导)  
+**诫律3**: 密室最多只能有一个秘密通道  
+**诫律4**: 未知毒药或需要长篇科学解释的装置禁用  
+**诫律5**: 不能有中国人角色(原版种族歧视,**已废弃**)→ **改为:不能有完全未登场的神秘人作为凶手**  
+**诫律6**: 侦探不能依靠偶然或第六感破案  
+**诫律7**: 侦探本人不能是罪犯(除非双侦探设定)  
+**诫律8**: 侦探必须向读者公开所有线索  
+**诫律9**: 侦探的助手(华生角色)的思考必须向读者透明  
+**诫律10**: 双胞胎/分身必须在前期明示  
+
+---
+
+## 3. 本格推理的四大支柱
+
+### 支柱1:公平性(Fair Play)
+**定义**: 读者与侦探拥有相同的信息,理论上可以推理出真相。
+
+**实现方法**:
+```
+✅ 正确示例
+第10章: 侦探发现花瓶碎片(读者同时看到)
+第20章: 侦探推理:"花瓶是从内部打碎的"
+→ 读者可以回顾第10章验证
+
+❌ 错误示例
+第10章: 侦探发现一个线索(未描述内容)
+第20章: 侦探说:"我早就发现了这个关键证据!"
+→ 读者无法验证,不公平
+```
+
+---
+
+### 支柱2:逻辑性(Logic)
+**定义**: 推理过程必须符合逻辑,不能靠直觉或巧合。
+
+**逻辑链示例**:
+```markdown
+## 案例:密室杀人
+**已知线索**:
+1. 死者在上锁的房间内
+2. 窗户从内部锁死
+3. 死者手持钥匙
+
+**错误推理**:
+"我有预感,凶手是隔壁的张三"
+→ 无逻辑依据
+
+**正确推理**:
+1. 死者手持钥匙 → 他可能自己锁门
+2. 窗户内锁 → 凶手未从窗户逃离
+3. 房间上锁 → 凶手可能在死者锁门前已离开
+4. → 推测:死者锁门后被提前藏在房内的凶手杀害
+5. → 寻找房内可藏身的地点(衣柜/床下/天花板)
+```
+
+---
+
+### 支柱3:可解性(Solvability)
+**定义**: 真相必须是读者**有可能**推理出来的,而非完全意外。
+
+**可解性标准**:
+- ✅ 读者事后恍然大悟:"原来是这样!"
+- ❌ 读者事后仍困惑:"什么?这也行?"
+
+**示例**:
+```markdown
+✅ 可解的反转
+线索: 死者日记写着"7月15日见到了他"
+真相: "他"指的是凶手,日记是证据
+
+❌ 不可解的反转
+真相: 死者有个从未提及的双胞胎兄弟
+→ 读者无从推测
+```
+
+---
+
+### 支柱4:意外性(Surprise)
+**定义**: 真相既符合逻辑,又出人意料。
+
+**平衡公式**:
+```
+公平性(80%线索) + 隐藏性(20%误导) = 意外性
+```
+
+**示例**:
+```markdown
+## 案例:《东方快车谋杀案》(阿加莎·克里斯蒂)
+**线索公平**: 12个嫌疑人都有不在场证明
+**误导**: 暗示凶手是外来者
+**真相**: 12人集体作案
+**意外性**: 颠覆"凶手是一人"的常识
+```
+
+---
+
+## 4. 本格推理的五大要素
+
+### 要素1:谜题(Mystery)
+**定义**: 核心悬念,推动读者继续阅读。
+
+**常见谜题类型**:
+| 类型 | 问题 | 示例 |
+|------|------|------|
+| **Who(谁)** | 凶手是谁? | 封闭空间多人,找出凶手 |
+| **How(如何)** | 如何做到的? | 密室杀人,如何逃脱 |
+| **Why(为何)** | 动机是什么? | 看似无冤无仇,为何杀人 |
+| **When(何时)** | 案发时间? | 伪造不在场证明 |
+| **Where(何地)** | 案发地点? | 尸体被移动,真正现场在哪 |
+
+---
+
+### 要素2:线索(Clues)
+**定义**: 指向真相的证据,必须**公平呈现**。
+
+**线索分类**:
+```markdown
+## 物证
+- 凶器、指纹、血迹、毛发、纤维
+- 时间证据(手表停止、日记日期)
+- 空间证据(脚印方向、物品位置)
+
+## 人证
+- 目击证言(可能虚假)
+- 专家鉴定(法医、化验师)
+
+## 行为证据
+- 嫌疑人的异常举动
+- 不在场证明的破绽
+```
+
+**线索埋设技巧**:
+```
+1. 前置埋设: 在案发前章节"不经意"提及关键物品
+2. 伪装成背景: 把线索藏在环境描写中
+3. 多次提及: 重要线索出现2-3次,加深印象
+```
+
+---
+
+### 要素3:红鲱鱼(Red Herring,误导)
+**定义**: 故意设置的虚假线索,引导读者做出错误推理。
+
+**示例**:
+```markdown
+## 案例:指向无辜者的证据
+线索A: 张三的刀上有血迹
+线索B: 张三曾与死者争吵
+→ 读者推测:张三是凶手
+
+真相: 张三是猎人,刀上是动物血
+     争吵是因为债务,与谋杀无关
+→ 真凶另有其人
+```
+
+**注意**: 红鲱鱼必须**事后可解释**,不能毫无逻辑。
+
+---
+
+### 要素4:侦探(Detective)
+**定义**: 推理的执行者,代表读者的视角。
+
+**侦探类型**:
+| 类型 | 特点 | 示例 |
+|------|------|------|
+| **天才型** | 超凡逻辑,冷静理性 | 福尔摩斯 |
+| **老实人型** | 依靠常识和踏实调查 | 布朗神父 |
+| **业余型** | 非职业侦探,误打误撞破案 | 杰西卡·弗莱彻 |
+
+**网文常见设定**:
+- 重生侦探(知道部分真相,但需重新推理)
+- 系统辅助侦探(获得线索提示)
+- 双侦探(一明一暗,最后合作)
+
+---
+
+### 要素5:真相揭示(Revelation)
+**定义**: 侦探公开推理过程,解释所有疑点。
+
+**揭示结构**:
+```markdown
+## 标准三段式
+1. 回顾案情(30%): 梳理已知线索
+2. 逐点破解(50%): 逐一排除嫌疑,指出红鲱鱼
+3. 指认凶手(20%): 揭示核心诡计,指认真凶
+```
+
+**揭示技巧**:
+```
+✅ 层层递进: 先破解小谜题,再揭示核心
+✅ 制造反转: 先指向A,再突然转向B
+✅ 留悬念: 解释80%,留20%让读者自己思考
+
+❌ 一次性倾倒: 把所有信息一口气说完
+❌ 侦探全知: 侦探事先知道但不说
+```
+
+---
+
+## 5. 本格推理的创作流程
+
+### 流程图
+```mermaid
+flowchart TD
+    A[1. 设计谜题] --> B[2. 构思诡计]
+    B --> C[3. 埋设线索]
+    C --> D[4. 设置误导]
+    D --> E[5. 设计侦探]
+    E --> F[6. 编写推理]
+    F --> G[7. 公平性检验]
+    G -->|不通过| C
+    G -->|通过| H[8. 完稿]
+```
+
+---
+
+### Step 1: 设计谜题
+**核心问题**: 你想让读者猜什么?
+
+**示例**:
+```
+谜题: 密室杀人——凶手如何在上锁房间杀人后逃脱?
+```
+
+---
+
+### Step 2: 构思诡计
+**核心问题**: 真相是什么?如何做到的?
+
+**示例**:
+```
+诡计: 凶手提前藏在房内壁橱,杀人后伪装成第一发现者
+```
+
+---
+
+### Step 3: 埋设线索
+**核心问题**: 如何让读者**有可能**推理出真相?
+
+**示例**:
+```
+线索1(第5章): 壁橱门有轻微划痕
+线索2(第10章): 第一发现者衣服上有灰尘
+线索3(第15章): 壁橱内发现一根头发
+```
+
+---
+
+### Step 4: 设置误导
+**核心问题**: 如何让读者**不那么容易**猜到真相?
+
+**示例**:
+```
+红鲱鱼1: 窗外有脚印(实际是园丁的)
+红鲱鱼2: 死者手持遗书(实际是自杀假象)
+```
+
+---
+
+### Step 5: 设计侦探
+**核心问题**: 由谁来推理?性格如何?
+
+**示例**:
+```
+侦探: 退休警探,经验丰富但不善言辞
+特点: 通过观察细节破案,而非天才直觉
+```
+
+---
+
+### Step 6: 编写推理
+**核心问题**: 侦探如何一步步接近真相?
+
+**示例**:
+```
+第20章: 发现壁橱划痕,怀疑有人藏过
+第25章: 发现第一发现者撒谎
+第30章: 揭示真相
+```
+
+---
+
+### Step 7: 公平性检验
+**检查清单**:
+- [ ] 所有关键线索都已向读者呈现?
+- [ ] 推理过程符合逻辑?
+- [ ] 没有依赖读者不知道的信息?
+- [ ] 红鲱鱼都可以合理解释?
+- [ ] 真相既意外又合理?
+
+---
+
+## 6. 本格推理的常见陷阱
+
+### 陷阱1:信息不对等
+**错误示例**:
+```
+侦探(心想):我早就知道凶手是谁了。
+→ 读者无法同步推理
+```
+
+**正确做法**:
+```
+侦探(心想):壁橱的划痕……难道……
+→ 暗示推理方向,但不直接说出
+```
+
+---
+
+### 陷阱2:逻辑跳跃
+**错误示例**:
+```
+线索: 现场有根头发
+侦探: 所以凶手是张三!
+→ 缺少中间推理步骤
+```
+
+**正确做法**:
+```
+线索: 现场有根头发
+侦探: 这根头发是黑色长发
+     现场只有3名女性
+     其中2人是短发
+     → 推测:凶手是长发女性李四
+```
+
+---
+
+### 陷阱3:过度依赖巧合
+**错误示例**:
+```
+侦探碰巧看到凶手在处理凶器
+→ 靠运气破案
+```
+
+**正确做法**:
+```
+侦探通过追踪线索,推理出凶器藏匿地点
+→ 靠逻辑破案
+```
+
+---
+
+### 陷阱4:真相过于复杂
+**错误示例**:
+```
+真相: 凶手利用量子力学原理制造密室
+→ 读者完全无法理解
+```
+
+**正确做法**:
+```
+真相: 凶手利用冰块固定门栓,融化后门自动上锁
+→ 简单但巧妙
+```
+
+---
+
+## 7. 本格推理自检清单
+
+**写完后逐项检查**:
+- [ ] 谜题是否足够吸引人?
+- [ ] 所有线索是否公平呈现?
+- [ ] 推理逻辑是否严密?
+- [ ] 红鲱鱼是否事后可解释?
+- [ ] 真相是否既意外又合理?
+- [ ] 侦探的推理过程是否清晰?
+- [ ] 有没有违反"十诫"?
+- [ ] 读者是否**有可能**推理出真相?
+
+---
+
+## 🛠️ 本格推理速查表
+
+| 要素 | 关键点 | 常见错误 | 正确做法 |
+|------|--------|---------|---------|
+| **谜题** | 吸引人的悬念 | 谜题太简单 | 多层谜题 |
+| **线索** | 公平呈现 | 关键线索隐藏 | 至少呈现2次 |
+| **诡计** | 既巧妙又可解 | 过于复杂 | 简单原理 |
+| **推理** | 逻辑严密 | 跳跃推理 | 逐步推导 |
+| **揭示** | 层层递进 | 一次性倾倒 | 分段揭示 |
+
+---
+
+## 附录:经典本格推理案例
+
+### 案例1:《暴风雪山庄》(东野圭吾)
+**谜题**: 封闭山庄内连环杀人,凶手是谁?  
+**诡计**: 被害者其实是凶手,伪装成被害  
+**公平性**: 所有线索均已呈现,读者可推理  
+
+---
+
+### 案例2:《无人生还》(阿加莎·克里斯蒂)
+**谜题**: 孤岛上10人逐一被杀,凶手是谁?  
+**诡计**: 法官假死,最后自杀  
+**公平性**: 童谣暗示杀人顺序,所有线索公开  
+
+---
+
+## 总结
+
+**本格推理 = 公平性 + 逻辑性 + 可解性 + 意外性**
+
+记住:本格推理的核心不是"炫技",而是"公平竞技"。读者与侦探站在同一起跑线,享受推理的乐趣。

+ 560 - 0
.claude/genres/rules-mystery/detective-design.md

@@ -0,0 +1,560 @@
+# 侦探角色设计 (Detective Character Design)
+
+> **核心原则**: 侦探是读者的眼睛和大脑。一个好的侦探 = 鲜明个性 + 合理推理能力 + 读者认同感。
+
+---
+
+## 1. 侦探的三大职能
+
+### 职能1:推理引擎(Logic Engine)
+**作用**: 代替读者进行逻辑推理,揭示真相。
+
+**要求**:
+```markdown
+✅ 推理过程必须符合逻辑
+✅ 不能依赖直觉或"天降灵感"
+✅ 线索→推理→结论,每一步都要清晰
+```
+
+---
+
+### 职能2:读者视角(Reader's Proxy)
+**作用**: 侦探发现的线索 = 读者看到的线索。
+
+**要求**:
+```markdown
+✅ 侦探的思考过程要向读者透明
+✅ 侦探不能"藏私"(发现线索不说)
+✅ 侦探的疑惑 = 读者的疑惑
+```
+
+---
+
+### 职能3:人格魅力(Character Charm)
+**作用**: 让读者喜欢这个角色,愿意跟随他破案。
+
+**要求**:
+```markdown
+✅ 有鲜明的性格特征
+✅ 有个人魅力或怪癖
+✅ 有成长空间(不是完美人)
+```
+
+---
+
+## 2. 侦探的分类
+
+### 按职业分类
+
+#### 类型1:职业侦探
+**特点**: 以侦探为职业,经验丰富。
+
+**代表人物**:
+```markdown
+✅ 私家侦探: 福尔摩斯
+✅ 警探: 柯南·道尔(《神探夏洛克》)
+✅ 顾问侦探: 波洛(阿加莎·克里斯蒂)
+```
+
+**优点**: 推理能力强,读者信服  
+**缺点**: 容易显得"全知全能",缺少成长
+
+---
+
+#### 类型2:业余侦探
+**特点**: 本职不是侦探,但因缘巧合卷入案件。
+
+**代表人物**:
+```markdown
+✅ 家庭主妇: 杰西卡·弗莱彻(《女作家与谋杀案》)
+✅ 神父: 布朗神父
+✅ 学生: 江户川柯南(《名侦探柯南》)
+```
+
+**优点**: 有成长空间,读者代入感强  
+**缺点**: 推理能力需要合理解释
+
+---
+
+#### 类型3:特殊身份侦探(网文常见)
+**特点**: 具有超自然能力或特殊背景。
+
+**示例**:
+```markdown
+✅ 重生侦探: 知道部分真相,但需重新推理
+✅ 系统辅助: 获得线索提示,但仍需自己推理
+✅ 读心术: 能读取部分信息,但不是全知
+```
+
+**注意**: 能力要有**限制**,否则失去推理乐趣。
+
+---
+
+### 按性格分类
+
+| 类型 | 特征 | 优点 | 缺点 | 代表 |
+|------|------|------|------|------|
+| **天才型** | 超高智商,冷静理性 | 推理精彩 | 难以共情 | 福尔摩斯 |
+| **老实人型** | 踏实调查,常识推理 | 亲切可信 | 缺少惊艳 | 布朗神父 |
+| **怪人型** | 古怪行为,独特思路 | 个性鲜明 | 过于夸张 | 波洛 |
+| **成长型** | 从新手到高手 | 代入感强 | 前期弱 | 金田一 |
+
+---
+
+## 3. 侦探的能力设计
+
+### 能力1:观察力(Observation)
+**定义**: 发现常人忽略的细节。
+
+**示例**:
+```markdown
+## 案例:福尔摩斯
+普通人: 看到一个人
+福尔摩斯: 
+- 鞋上有泥土 → 刚从郊外回来
+- 手指有墨迹 → 从事文字工作
+- 袖口磨损 → 经济拮据
+```
+
+**网文应用**:
+```markdown
+侦探注意到:
+"死者手表停在8点,但窗外天已大亮。"
+→ 推测:手表可能被调慢了
+```
+
+---
+
+### 能力2:逻辑推理(Deduction)
+**定义**: 从已知线索推导出结论。
+
+**推理模式**:
+```markdown
+## 三段论推理
+大前提: 凶器上有指纹的人曾接触过凶器
+小前提: 张三的指纹在凶器上
+结论: 张三曾接触过凶器
+```
+
+**注意**: 推理要**逐步展开**,不能跳跃。
+
+---
+
+### 能力3:知识储备(Knowledge)
+**定义**: 侦探掌握的专业知识。
+
+**常见知识领域**:
+```markdown
+✅ 法医学: 判断死亡时间、死因
+✅ 化学: 识别毒药、爆炸物
+✅ 心理学: 分析犯罪动机
+✅ 密码学: 破解暗号
+```
+
+**注意**: 知识要**提前交代**,不能临时"掏出"。
+
+**示例**:
+```markdown
+❌ 错误:
+第30章: 侦探突然说:"我懂密码学,这是摩斯密码!"
+→ 前文从未提及
+
+✅ 正确:
+第5章: 介绍侦探曾学过密码学
+第30章: 侦探破译密码
+→ 有伏笔
+```
+
+---
+
+### 能力4:直觉(Intuition)
+**定义**: 基于经验的快速判断。
+
+**使用原则**:
+```markdown
+✅ 直觉可以作为**线索方向**
+✅ 但最终结论必须基于**逻辑推理**
+✅ 不能完全依赖直觉破案
+```
+
+**示例**:
+```markdown
+侦探(直觉): "我总觉得张三有问题。"
+→ 然后通过调查找到证据
+→ 而非直接说:"我的直觉告诉我,张三是凶手!"
+```
+
+---
+
+## 4. 侦探的性格塑造
+
+### 性格要素:怪癖(Quirk)
+**作用**: 让角色更有记忆点。
+
+**经典怪癖**:
+```markdown
+✅ 福尔摩斯: 拉小提琴、吸烟、冷漠
+✅ 波洛: 强迫症、修胡子、自恋
+✅ 柯南: 喜欢推理、口头禅"真相只有一个"
+```
+
+**网文应用**:
+```markdown
+侦探怪癖示例:
+- 破案时必须吃糖
+- 思考时转笔
+- 口头禅:"有意思……"
+```
+
+**注意**: 怪癖要**适度**,过多会显得刻意。
+
+---
+
+### 性格要素:弱点(Flaw)
+**作用**: 让角色更真实,有成长空间。
+
+**常见弱点**:
+```markdown
+✅ 社交障碍: 不善与人交流
+✅ 傲慢: 看不起警察
+✅ 冲动: 容易被激怒
+✅ 恐惧: 害怕某种事物(高空/黑暗)
+```
+
+**示例**:
+```markdown
+侦探弱点:
+"他推理能力极强,但不善言辞,总是得罪人。"
+→ 为后续剧情埋下冲突点
+```
+
+---
+
+### 性格要素:动机(Motivation)
+**作用**: 解释侦探为什么要破案。
+
+**常见动机**:
+```markdown
+✅ 职业使命: "这是我的工作"
+✅ 个人兴趣: "我喜欢解谜"
+✅ 复仇: "凶手杀了我的亲人"
+✅ 正义感: "我要为死者伸冤"
+```
+
+**示例**:
+```markdown
+侦探动机:
+"十年前,我父亲被诬陷为凶手,含冤而死。
+从那天起,我发誓要揭开所有谎言。"
+→ 强烈的个人动机
+```
+
+---
+
+## 5. 侦探的成长弧线
+
+### 成长模式:从新手到高手
+
+#### 阶段1:新手期
+**特点**: 推理能力弱,依赖他人帮助。
+
+**示例**:
+```markdown
+第1-10章:
+侦探刚入行,破案依赖师父指点
+→ 建立"成长空间"
+```
+
+---
+
+#### 阶段2:成长期
+**特点**: 逐步掌握推理技巧,独立破案。
+
+**示例**:
+```markdown
+第11-30章:
+侦探独立破获小案件,偶尔失误
+→ 展示成长过程
+```
+
+---
+
+#### 阶段3:成熟期
+**特点**: 推理能力强,能处理复杂案件。
+
+**示例**:
+```markdown
+第31-50章:
+侦探成为名侦探,破获连环杀人案
+→ 能力巅峰
+```
+
+---
+
+#### 阶段4:突破期(可选)
+**特点**: 遇到超难案件,突破自我。
+
+**示例**:
+```markdown
+第51-70章:
+侦探遇到宿敌,陷入困境,最终顿悟
+→ 再次成长
+```
+
+---
+
+## 6. 侦探与助手的关系
+
+### 经典搭档模式:侦探 + 华生
+
+#### 华生的作用
+```markdown
+1. 提问者: 代替读者提出疑问
+2. 记录者: 记录案件过程
+3. 对比者: 凸显侦探的聪明
+```
+
+**示例**:
+```markdown
+华生: "你是怎么知道凶手是张三的?"
+侦探: "很简单,你看这三条线索……"
+→ 华生提问,侦探解释,读者理解
+```
+
+---
+
+#### 华生的性格设定
+```markdown
+✅ 老实憨厚: 不如侦探聪明,但踏实可靠
+✅ 勇敢: 保护侦探的人身安全
+✅ 忠诚: 无条件相信侦探
+```
+
+---
+
+### 反面教材:没有华生的问题
+**问题**:
+```markdown
+侦探独自破案 → 没人提问 → 读者不知道侦探在想什么
+```
+
+**解决**:
+```markdown
+✅ 方案1: 增加华生角色
+✅ 方案2: 侦探自言自语(内心独白)
+✅ 方案3: 警察/记者充当提问者
+```
+
+---
+
+## 7. 侦探的推理展示
+
+### 展示方式1:边调查边推理
+**特点**: 侦探每发现一条线索,就进行推理。
+
+**示例**:
+```markdown
+侦探发现线索A:
+"这根头发是黑色长发……现场只有3名女性,其中2人是短发……"
+→ 读者跟随侦探思路
+```
+
+---
+
+### 展示方式2:集中揭示
+**特点**: 侦探调查完毕后,集中公开推理。
+
+**示例**:
+```markdown
+第25章(调查完毕):
+侦探召集所有人:"现在,我要揭示真相……"
+→ 适合短篇或最终揭示
+```
+
+---
+
+### 展示方式3:分段揭示
+**特点**: 侦探逐步揭示部分真相,制造悬念。
+
+**示例**:
+```markdown
+第10章: 侦探揭示密室诡计
+第20章: 侦探揭示不在场证明破绽
+第30章: 侦探指认真凶
+→ 层层递进
+```
+
+---
+
+## 8. 侦探设计常见错误
+
+### 错误1:全知全能
+**问题**:
+```markdown
+侦探一眼看穿所有诡计,毫无悬念
+→ 读者:"没意思"
+```
+
+**解决**:
+```markdown
+✅ 给侦探设置障碍(误导线索、凶手反击)
+✅ 让侦探也会犯错(推理错误,再修正)
+```
+
+---
+
+### 错误2:依赖直觉
+**问题**:
+```markdown
+侦探: "我的直觉告诉我,凶手是张三!"
+→ 没有逻辑推理
+```
+
+**解决**:
+```markdown
+✅ 直觉只是方向,必须找到证据
+✅ 最终结论基于逻辑
+```
+
+---
+
+### 错误3:性格单薄
+**问题**:
+```markdown
+侦探只会破案,没有个人生活和情感
+→ 读者无法共情
+```
+
+**解决**:
+```markdown
+✅ 给侦探个人生活(家庭/爱好/过去)
+✅ 给侦探情感冲突(亲人被害/道德困境)
+```
+
+---
+
+### 错误4:"藏私"
+**问题**:
+```markdown
+侦探发现关键线索,但不告诉读者
+第30章突然说:"我早就知道了!"
+→ 不公平
+```
+
+**解决**:
+```markdown
+✅ 侦探的所有发现都要向读者展示
+✅ 可以暂时不解释,但线索必须呈现
+```
+
+---
+
+## 9. 网文特色侦探设计
+
+### 特色1:系统辅助侦探
+**设定**: 侦探拥有推理系统,获得提示。
+
+**限制设计**:
+```markdown
+✅ 系统只提供线索,不直接给答案
+✅ 侦探仍需自己推理
+✅ 系统有使用次数限制
+```
+
+**示例**:
+```markdown
+【系统提示: 现场存在3处异常】
+侦探: "3处异常……分别是什么?"
+→ 侦探自己寻找
+```
+
+---
+
+### 特色2:重生侦探
+**设定**: 侦探重生,知道部分真相。
+
+**冲突设计**:
+```markdown
+✅ 重生后时间线改变,真相也变了
+✅ 侦探需要重新推理
+✅ 不能完全依赖前世记忆
+```
+
+**示例**:
+```markdown
+前世: 凶手是张三
+今生: 张三提前被捕,真凶变成了李四
+→ 侦探必须重新破案
+```
+
+---
+
+### 特色3:双侦探
+**设定**: 两个侦探,一明一暗,最后合作。
+
+**关系设计**:
+```markdown
+✅ 一个擅长逻辑推理,一个擅长心理分析
+✅ 前期竞争,后期合作
+✅ 各自发现不同线索,最终拼图
+```
+
+---
+
+## 10. 侦探设计自检清单
+
+**设计完侦探后逐项检查**:
+- [ ] 侦探的推理能力是否合理?
+- [ ] 侦探的知识储备是否有交代?
+- [ ] 侦探是否有鲜明的性格特征?
+- [ ] 侦探是否有弱点或成长空间?
+- [ ] 侦探的动机是否明确?
+- [ ] 侦探是否向读者公开所有线索?
+- [ ] 侦探是否有助手(华生角色)?
+- [ ] 侦探的推理过程是否符合逻辑?
+
+---
+
+## 🛠️ 侦探设计速查表
+
+| 要素 | 标准 | 常见错误 | 解决方案 |
+|------|------|---------|---------|
+| **能力** | 观察+推理+知识 | 全知全能 | 设置障碍 |
+| **性格** | 怪癖+弱点+动机 | 性格单薄 | 增加细节 |
+| **成长** | 从新手到高手 | 一开始就很强 | 设计成长弧 |
+| **公平** | 线索向读者公开 | 藏私 | 透明化 |
+| **助手** | 华生角色 | 没有提问者 | 增加搭档 |
+
+---
+
+## 附录:经典侦探分析
+
+### 案例1:福尔摩斯
+**能力**: 超强观察力、逻辑推理、化学知识  
+**性格**: 冷漠、高傲、拉小提琴  
+**弱点**: 不善社交、吸毒(早期)  
+**搭档**: 华生医生  
+
+---
+
+### 案例2:波洛
+**能力**: 心理分析、逻辑推理  
+**性格**: 自恋、强迫症、修胡子  
+**弱点**: 过于自信  
+**搭档**: 黑斯廷斯上尉  
+
+---
+
+### 案例3:柯南
+**能力**: 超强推理、化学知识  
+**性格**: 好奇心强、正义感  
+**弱点**: 身体变小,行动受限  
+**特殊**: 有各种黑科技道具  
+
+---
+
+## 总结
+
+**好侦探 = 合理能力 + 鲜明性格 + 成长弧线 + 公平推理**
+
+记住:侦探不是"神",而是读者的代言人。让读者跟随侦探的思路,享受推理的乐趣。

+ 472 - 0
.claude/genres/rules-mystery/revelation-design.md

@@ -0,0 +1,472 @@
+# 真相揭示技巧 (Revelation Design)
+
+> **核心原则**: 真相揭示是本格推理的高潮。好的揭示要做到"意料之外,情理之中"——既要震撼读者,又要让读者恍然大悟"原来如此"。
+
+---
+
+## 1. 真相揭示的三大目标
+
+### 目标1:逻辑自洽(Logic Consistency)
+**定义**: 真相必须能解释所有线索和疑点,不留逻辑漏洞。
+
+**检验方法**:
+```
+✅ 每一条线索都能在真相中找到解释
+✅ 没有"孤立线索"(无法解释的线索)
+✅ 不依赖"巧合"或"读者不知道的事实"
+```
+
+**反面教材**:
+```
+真相:"凶手是 A,他用了密室诡计杀人"
+遗漏:为什么现场有 B 的指纹?为什么 C 的证词与现场不符?
+→ 逻辑不自洽,读者不满意
+```
+
+---
+
+### 目标2:意外性(Surprise)
+**定义**: 真相要出乎读者意料,打破常规推理。
+
+**实现方法**:
+- **颠覆第一印象**:最无辜的人是凶手
+- **反转叙述视角**:读者以为的受害者其实是凶手
+- **时间错位**:案件发生时间与读者认知不同
+- **身份错位**:A 和 B 的身份互换
+
+**示例**:
+```
+读者推理:凶手是 A(所有证据指向 A)
+真相揭示:A 是替罪羊,真凶是 B(一直在误导读者)
+震撼点:B 是侦探的助手/警察/受害者家属
+```
+
+---
+
+### 目标3:恍然大悟(Enlightenment)
+**定义**: 真相揭示后,读者回顾全文,发现"所有线索都在那里"。
+
+**要求**:
+```
+✅ 读者事后能验证:第X章的描写确实是伏笔
+✅ 线索不应"事后补充"(揭示时才说"其实还有一条线索……")
+✅ 读者感叹:"我怎么没想到!"而非"作者耍赖!"
+```
+
+---
+
+## 2. 揭示场景的四种类型
+
+### 类型A:经典解谜场景(Detective's Revelation)
+**特点**: 侦探召集所有人,逐一推理,揭示真相。
+
+**标准流程(6步)**:
+```markdown
+## 经典解谜场景流程
+
+Step 1: 场景设定(100-200字)
+- 所有嫌疑人聚集在一个房间
+- 侦探宣布:"我已经知道真相了"
+- 紧张气氛营造
+
+Step 2: 重述案情(200-300字)
+- 侦探回顾案件经过
+- 列出关键疑点(3-5个)
+- 提醒读者重要线索
+
+Step 3: 排除法(300-500字)
+- 逐一分析嫌疑人
+- 指出每个人的不在场证明/动机缺失
+- 缩小范围到1-2人
+
+Step 4: 关键推理(500-800字)
+- 指出"决定性线索"
+- 展示推理链条:线索 A + 线索 B → 结论 C
+- 揭示诡计原理
+
+Step 5: 真凶反应(100-200字)
+- 真凶辩解/狡辩/沉默
+- 侦探拿出"最后一击"(物证/证人)
+- 真凶认罪/崩溃
+
+Step 6: 动机揭示(200-300字)
+- 解释真凶的动机
+- 补充案件背景故事
+- 结局:真凶被捕/自杀/逃脱(埋伏笔)
+```
+
+**示例(简化版)**:
+```
+侦探:"各位,我已经知道谁是凶手了。"
+众人紧张。
+
+侦探:"首先,让我们回顾案情……死者在晚上8点被杀,凶器是匕首……"
+
+侦探:"张三有不在场证明,李四没有动机,王五……"
+
+侦探:"关键在于这个!"(拿出一根头发)
+"这根头发在现场发现,但它属于一个不该在那里的人——赵六!"
+
+赵六脸色大变:"你……你胡说!"
+
+侦探:"不仅如此,赵六你说你8点在家,但你的手机定位显示你在案发现场!"
+
+赵六崩溃:"是……是我杀的……他毁了我的人生……"
+```
+
+---
+
+### 类型B:分段揭示(Progressive Revelation)
+**特点**: 不是一次性揭示,而是分多次逐步揭开真相。
+
+**适用场景**:
+- 案件复杂,涉及多个谜题
+- 网文连载,需要分章节保持悬念
+
+**流程**:
+```markdown
+第30章: 揭示"凶器来源"(部分真相)
+第35章: 揭示"不在场证明诡计"(更多真相)
+第40章: 揭示"真凶身份"(完整真相)
+```
+
+**优点**:
+- 保持悬念,读者持续关注
+- 适合长篇连载
+
+**缺点**:
+- 节奏难把控,容易拖沓
+
+---
+
+### 类型C:反转式揭示(Twist Revelation)
+**特点**: 先给出一个"假真相",再推翻,揭示"真真相"。
+
+**经典模板**:
+```markdown
+第一次揭示(假真相):
+侦探:"凶手是 A!"
+A 被捕,案件似乎结束。
+
+第二次揭示(真真相):
+侦探:"等等……有些地方不对……"
+侦探重新推理:"A 不是真凶!真凶是 B!"
+震撼反转。
+```
+
+**示例(《东方快车谋杀案》式)**:
+```
+侦探:"凶手是车上的某一个人。"
+→ 读者推理:12个嫌疑人,谁是凶手?
+
+最终揭示:"凶手不是一个人,是所有人。"
+→ 震撼反转
+```
+
+---
+
+### 类型D:读者先知(Reader-First Revelation)
+**特点**: 读者比侦探更早知道真相。
+
+**适用场景**:
+- 倒叙推理(先展示犯罪,再展示侦探破案)
+- "逆转裁判"式(读者知道真相,看侦探如何证明)
+
+**效果**:
+- 读者有"上帝视角",看侦探如何一步步接近真相
+- 产生"快来发现啊!"的紧张感
+
+**示例**:
+```
+第1章: 展示真凶 A 的犯罪过程(读者知道真相)
+第2-20章: 侦探调查,逐步接近真相(读者看侦探破案)
+第21章: 侦探揭示:"凶手是 A!"(读者早就知道了)
+```
+
+---
+
+## 3. 揭示的节奏控制
+
+### 节奏公式(推理小说标准结构)
+```
+总字数的 70-80% → 开始揭示
+总字数的 80-90% → 完成揭示
+总字数的 90-100% → 收尾(抓捕/动机解释/尾声)
+```
+
+**示例(10万字推理小说)**:
+```
+0-7万字: 调查、线索收集、嫌疑人筛选
+7-8万字: 侦探解谜场景(揭示真相)
+8-9万字: 真凶认罪、抓捕
+9-10万字: 尾声、动机解释、伏笔
+```
+
+### 避免过早揭示
+**反面教材**:
+```
+总字数一半就揭示真相 → 后半部分无悬念,读者弃书
+```
+
+### 避免过晚揭示
+**反面教材**:
+```
+到最后1000字才揭示 → 读者感觉被耍,推理过程太短
+```
+
+---
+
+## 4. 真相揭示的五大技巧
+
+### 技巧1:回顾式揭示
+**方法**: 侦探逐章回顾,指出每一处伏笔。
+
+**示例**:
+```
+侦探:"还记得第5章,死者的手表停在8点吗?"
+"当时我们以为这是死亡时间,但其实……"
+"这是凶手故意制造的假象!"
+```
+
+**优点**: 读者跟着侦探回顾全文,恍然大悟。
+
+---
+
+### 技巧2:物证展示
+**方法**: 侦探拿出关键物证,作为"最后一击"。
+
+**示例**:
+```
+侦探:"你说你从未去过现场,但这是什么?"
+(拿出一张照片,照片里凶手出现在现场)
+凶手哑口无言。
+```
+
+**优点**: 直观、有力、读者信服。
+
+---
+
+### 技巧3:逻辑链条展示
+**方法**: 侦探展示完整的推理链条(A → B → C → 结论)。
+
+**示例**:
+```
+侦探:
+"线索 A: 现场发现红色纤维"
+"线索 B: 张三有一件红色外套"
+"线索 C: 张三的外套在案发后被洗过"
+"结论: 张三去过现场,并试图销毁证据"
+```
+
+**优点**: 逻辑严密,读者服气。
+
+---
+
+### 技巧4:反问式揭示
+**方法**: 侦探用反问引导读者思考。
+
+**示例**:
+```
+侦探:"为什么凶手要在现场留下这么多线索?"
+"答案很简单——因为他想让我们找到这些线索。"
+"他在误导我们!"
+```
+
+**优点**: 互动性强,读者参与感高。
+
+---
+
+### 技巧5:情感渲染
+**方法**: 在揭示真相时,加入情感描写(悲伤/愤怒/震惊)。
+
+**示例**:
+```
+侦探:"凶手……是死者的亲生儿子。"
+全场震惊。
+儿子崩溃:"是……是他先抛弃我的……他不配做我的父亲!"
+泪水滑落……
+```
+
+**优点**: 增加共鸣,读者更投入。
+
+---
+
+## 5. 常见揭示错误
+
+### 错误1:事后补充线索
+**反面教材**:
+```
+侦探:"其实,在第3章,我还发现了一条线索(之前从未提及)……"
+→ 读者:"你耍赖!"
+```
+
+**正确做法**:
+```
+所有关键线索必须在揭示前出现(哪怕隐藏得很深)
+```
+
+---
+
+### 错误2:依赖巧合
+**反面教材**:
+```
+侦探:"凶手恰好在案发时路过现场,恰好带了凶器……"
+→ 读者:"太巧了吧?"
+```
+
+**正确做法**:
+```
+真相必须有充分的动机和计划,不能依赖巧合
+```
+
+---
+
+### 错误3:解释不清
+**反面教材**:
+```
+侦探:"凶手用了XX诡计(但不解释原理)……"
+读者:"什么诡计?没听懂啊!"
+```
+
+**正确做法**:
+```
+用通俗语言解释诡计原理,必要时配图
+```
+
+---
+
+### 错误4:动机牵强
+**反面教材**:
+```
+凶手:"我杀他是因为……他踩了我一脚。"
+读者:"就因为这个?!"
+```
+
+**正确做法**:
+```
+动机必须充分:仇恨/利益/秘密/绝望
+```
+
+---
+
+## 6. 揭示后的收尾
+
+### 收尾要点(3选2)
+1. **真凶的最后独白**:解释动机,补充背景故事
+2. **侦探的反思**:对案件的感悟,对人性的思考
+3. **伏笔/续集预告**:暗示下一个案件/未解之谜
+
+**示例(真凶独白)**:
+```
+真凶:"你知道吗,20年前,他毁了我的一切……"
+"我忍了20年,终于等到这一天……"
+"我不后悔。"
+```
+
+**示例(侦探反思)**:
+```
+侦探看着真凶被带走,叹了口气。
+"每个凶手背后,都有一个悲伤的故事……"
+"但这不是杀人的理由。"
+```
+
+**示例(伏笔)**:
+```
+案件结束,侦探收到一封匿名信:
+"这只是开始……下一个受害者已经选好了。"
+侦探脸色凝重……
+```
+
+---
+
+## 7. 揭示场景自检清单
+
+揭示真相前,检查以下项目:
+
+- [ ] **所有线索都出现过吗**: 揭示时提到的每一条线索,在之前章节都有展示?
+- [ ] **逻辑自洽吗**: 真相能解释所有疑点,没有逻辑漏洞?
+- [ ] **有意外性吗**: 真相是否出乎读者意料(但不违背逻辑)?
+- [ ] **读者能验证吗**: 读者回顾全文,能找到所有伏笔和线索?
+- [ ] **动机充分吗**: 凶手的动机是否足以支撑其犯罪行为?
+- [ ] **诡计可理解吗**: 诡计原理是否用通俗语言解释清楚?
+- [ ] **节奏合适吗**: 揭示场景是否在总字数的 70-80% 处?
+- [ ] **有情感渲染吗**: 是否加入了情感描写,增强共鸣?
+
+---
+
+## 🛠️ 揭示技巧速查表
+
+| 揭示类型 | 特点 | 适用场景 | 优点 | 缺点 |
+|---------|------|---------|------|------|
+| **经典解谜** | 侦探召集众人,逐一推理 | 短篇、中篇 | 仪式感强,读者满足感高 | 套路化,需创新 |
+| **分段揭示** | 逐步揭开真相 | 长篇连载 | 保持悬念 | 节奏难把控 |
+| **反转式** | 先假真相,再真真相 | 需要震撼效果 | 意外性强 | 容易显得刻意 |
+| **读者先知** | 读者比侦探先知道 | 倒叙推理 | 紧张感强 | 失去"解谜"乐趣 |
+
+---
+
+## 附录:经典揭示场景案例
+
+### 案例1:《东方快车谋杀案》
+**揭示方式**: 反转式揭示
+**流程**:
+1. 侦探推理:"凶手在12个人中"
+2. 逐一排除,最后锁定某人
+3. 最终反转:"凶手是所有人"
+
+**优点**: 震撼反转,出乎意料又在情理之中。
+
+---
+
+### 案例2:《罗杰疑案》
+**揭示方式**: 叙述性诡计
+**流程**:
+1. 读者一直跟随叙述者视角
+2. 最后揭示:"叙述者就是凶手"
+
+**优点**: 利用叙述视角误导读者,极具创新性。
+
+---
+
+### 案例3:反面教材(某扑街推理文)
+```
+侦探:"凶手是 A。"
+众人:"为什么?"
+侦探:"因为我猜的。"
+→ 读者:"???"
+```
+
+**问题**: 没有推理过程,没有线索支撑,纯靠猜测。
+
+---
+
+## 特殊场景:网文推理的揭示调整
+
+### 调整1:分章揭示(适应连载)
+```
+标准推理: 一章内完成揭示(5000字)
+网文调整: 分3-5章逐步揭示(每章2000字)
+→ 保持订阅,防止读者"看完真相就跑"
+```
+
+### 调整2:高潮前置(抓住读者)
+```
+标准推理: 70% 处开始揭示
+网文调整: 50-60% 处就开始部分揭示
+→ 更早进入高潮,抓住读者
+```
+
+### 调整3:加入爽点(网文特色)
+```
+标准推理: 揭示真相 → 结束
+网文调整: 揭示真相 → 侦探装逼 → 打脸真凶 → 读者爽
+```
+
+**示例(网文式揭示)**:
+```
+侦探:"你以为你计划完美?可笑。"
+真凶:"你……你怎么知道的……"
+侦探:"从一开始,我就知道是你。"(装逼)
+真凸崩溃:"不可能!"(打脸)
+众人震惊:"侦探大人太厉害了!"(爽点)
+```

+ 520 - 0
.claude/genres/rules-mystery/structure-pacing.md

@@ -0,0 +1,520 @@
+# 结构与节奏 (Structure & Pacing)
+
+> **核心原则**: 本格推理的结构和节奏决定了读者的体验。好的结构让读者始终保持"想往下看"的欲望,好的节奏让读者在"紧张"与"放松"之间切换,避免疲劳。
+
+---
+
+## 1. 推理小说的经典三幕结构
+
+### 第一幕:引入(Setup)- 占全文 20-25%
+
+**目标**: 吸引读者,建立世界观,引出案件。
+
+**必备要素(5项)**:
+1. **案件发生**:展示案发现场,描述受害者状态
+2. **侦探登场**:介绍主角侦探,展示其特点
+3. **初步调查**:侦探开始收集线索
+4. **嫌疑人出场**:引入3-12名嫌疑人
+5. **核心谜题确立**:明确"要解决什么问题"
+
+**字数分配(10万字推理小说为例)**:
+```
+第1-5章(2万字):
+- 第1章(5000字): 案件发生,展示案发现场
+- 第2章(5000字): 侦探登场,接手案件
+- 第3-4章(1万字): 初步调查,嫌疑人登场
+- 第5章(5000字): 确立核心谜题(密室/不在场证明等)
+```
+
+**第一幕结束标志**:
+```
+✅ 读者清楚:谁死了?怎么死的?有哪些嫌疑人?
+✅ 读者好奇:凶手是谁?诡计是什么?
+✅ 读者投入:开始跟着侦探推理
+```
+
+---
+
+### 第二幕:调查(Investigation)- 占全文 50-60%
+
+**目标**: 收集线索,筛选嫌疑人,制造悬念。
+
+**必备要素(6项)**:
+1. **线索收集**:侦探逐步发现关键线索(明线索 + 暗线索)
+2. **嫌疑人筛选**:逐一排除/锁定嫌疑人
+3. **红鲱鱼**:误导读者的假线索
+4. **中期反转**:打破读者的固有推测
+5. **侦探受阻**:侦探遇到困难(线索矛盾/嫌疑人反击)
+6. **新发现**:关键线索出现,推动剧情
+
+**字数分配(10万字推理小说为例)**:
+```
+第6-15章(5-6万字):
+- 第6-8章(1.5万字): 线索收集,嫌疑人调查
+- 第9-10章(1万字): 中期反转(某嫌疑人被排除/新嫌疑人出现)
+- 第11-13章(1.5万字): 侦探受阻,陷入困境
+- 第14-15章(1-1.5万字): 关键线索出现,柳暗花明
+```
+
+**第二幕的节奏控制(重要)**:
+```
+前1/3(快节奏): 大量线索,快速推进
+中1/3(慢节奏): 深入分析,嫌疑人心理战
+后1/3(加速): 关键线索,接近真相
+```
+
+**第二幕结束标志**:
+```
+✅ 侦探掌握所有关键线索
+✅ 嫌疑人范围缩小到1-3人
+✅ 读者急切想知道真相
+```
+
+---
+
+### 第三幕:揭示(Revelation)- 占全文 20-25%
+
+**目标**: 揭示真相,收尾剧情,给出结局。
+
+**必备要素(4项)**:
+1. **真相揭示**:侦探召集众人,逐一推理
+2. **真凶认罪**:真凶辩解失败,认罪
+3. **动机解释**:补充真凶的犯罪动机与背景
+4. **尾声**:案件结束,侦探反思/伏笔
+
+**字数分配(10万字推理小说为例)**:
+```
+第16-20章(2-2.5万字):
+- 第16-18章(1.5万字): 侦探解谜,揭示真相
+- 第19章(5000字): 真凶认罪,动机解释
+- 第20章(3000-5000字): 尾声,收尾
+```
+
+**第三幕的黄金法则**:
+```
+❌ 禁止:揭示阶段出现"新线索"(事后补充)
+✅ 正确:所有线索在第二幕已经出现
+```
+
+---
+
+## 2. 推理小说的节奏控制
+
+### 节奏的两种模式
+
+#### 模式A:张弛有度(标准推理小说)
+```
+紧张 → 放松 → 紧张 → 放松 → 大紧张(高潮)
+```
+
+**示例**:
+```
+第1章: 案件发生(紧张)
+第2章: 日常调查(放松)
+第3章: 发现关键线索(紧张)
+第4章: 分析线索,嫌疑人对话(放松)
+第5章: 中期反转(紧张)
+……
+第18章: 揭示真相(大紧张)
+```
+
+**优点**: 读者不会疲劳,节奏舒适。
+
+---
+
+#### 模式B:步步紧逼(快节奏推理)
+```
+紧张 → 更紧张 → 极度紧张 → 高潮
+```
+
+**示例**:
+```
+第1章: 案件发生
+第2章: 第二个受害者出现
+第3章: 第三个受害者出现
+第4章: 侦探发现"凶手在杀人清单上的下一个目标是自己"
+第5章: 侦探与凶手对决
+```
+
+**优点**: 持续高能,抓住读者。  
+**缺点**: 容易疲劳,适合短篇。
+
+---
+
+### 节奏工具:高潮点设计
+
+**高潮点定义**: 剧情的转折点,让读者"眼前一亮"或"倒吸一口凉气"。
+
+**推理小说的标准高潮点配置**:
+```
+第一高潮(20-30%): 案件发生 / 密室揭开
+第二高潮(50%): 中期反转 / 嫌疑人死亡
+第三高潮(70-80%): 真相揭示开始
+终极高潮(80-90%): 真凶认罪 / 意外反转
+```
+
+**示例(10万字推理小说)**:
+```
+2万字: 案件发生,密室谜题(第一高潮)
+5万字: 嫌疑人 A 被杀,案件升级(第二高潮)
+7万字: 侦探开始揭示真相(第三高潮)
+8.5万字: 真相大白,真凶是意想不到的人(终极高潮)
+```
+
+---
+
+## 3. 经典推理结构模板
+
+### 模板A:密室推理结构
+```markdown
+## 密室推理标准结构(10万字)
+
+第一幕(2万字):
+- 第1章: 密室案件发生,展示密室状态
+- 第2章: 侦探登场,初步调查
+- 第3-4章: 嫌疑人登场,初步推理
+- 第5章: 确立核心谜题:"如何在密室中杀人?"
+
+第二幕(5-6万字):
+- 第6-8章: 收集线索,分析密室诡计的可能性
+- 第9-10章: 中期反转(某个假设被推翻)
+- 第11-13章: 侦探陷入困境,密室诡计难以解释
+- 第14-15章: 关键线索出现(如:发现隐藏的机关)
+
+第三幕(2-2.5万字):
+- 第16-18章: 侦探揭示密室诡计原理
+- 第19章: 真凶认罪,动机解释
+- 第20章: 尾声
+```
+
+---
+
+### 模板B:连续杀人结构
+```markdown
+## 连续杀人标准结构(15万字)
+
+第一幕(3万字):
+- 第1-2章: 第一个受害者出现
+- 第3-5章: 侦探调查,发现受害者之间的共同点
+
+第二幕(9万字):
+- 第6-10章: 第二个受害者出现,凶手的杀人规律浮现
+- 第11-15章: 侦探推理,锁定嫌疑人
+- 第16-20章: 第三个受害者出现,侦探受阻
+- 第21-25章: 关键线索,侦探接近真相
+
+第三幕(3万字):
+- 第26-28章: 侦探揭示真相,凶手是"最不可能的人"
+- 第29章: 真凶认罪,解释杀人动机
+- 第30章: 尾声,伏笔
+```
+
+---
+
+### 模板C:本格推理 + 爽文结构(网文特色)
+```markdown
+## 网文推理结构(20万字连载)
+
+第一卷(5万字): 引入篇
+- 第1-10章: 侦探登场,解决小案件(展示能力)
+- 第10-20章: 大案件发生,引入主线
+
+第二卷(8万字): 调查篇
+- 第21-40章: 收集线索,嫌疑人筛选
+- 第41-50章: 中期反转,案件升级
+
+第三卷(5万字): 揭示篇
+- 第51-60章: 真相揭示,真凶落网
+- 第61-70章: 收尾,侦探装逼,打脸真凶
+
+第四卷(2万字): 余波篇
+- 第71-80章: 伏笔,引出下一个大案件
+```
+
+**网文调整要点**:
+1. **更早进入高潮**:第1卷就要有完整案件
+2. **加入爽点**:侦探破案后打脸反派
+3. **连载节奏**:每5-10章一个小高潮
+
+---
+
+## 4. 网文推理的节奏调整
+
+### 调整1:快速进入案件(抓住读者)
+```
+标准推理: 第1章铺垫背景,第2章案件发生
+网文调整: 第1章直接案件发生,倒叙补充背景
+```
+
+**对比**:
+```
+标准推理:
+第1章: 介绍侦探的日常生活(5000字)
+第2章: 案件发生(5000字)
+→ 读者可能在第1章就弃书
+
+网文调整:
+第1章: 案件发生,侦探登场(5000字)
+→ 第一章就抓住读者
+```
+
+---
+
+### 调整2:高频小高潮(保持订阅)
+```
+标准推理: 5-10万字一个大高潮
+网文调整: 每5-10章(1-2万字)一个小高潮
+```
+
+**小高潮设计**:
+- 发现关键线索
+- 嫌疑人死亡
+- 侦探受伤/遇险
+- 真相部分揭示
+- 意外反转
+
+---
+
+### 调整3:章末悬念(防止跳订)
+```
+标准推理: 章末自然结束
+网文调整: 每章结尾设置悬念
+```
+
+**示例**:
+```
+标准推理章末:
+"侦探收起笔记本,离开了现场。"
+→ 读者可以停下,第二天再看
+
+网文章末:
+"侦探刚走出门,突然接到电话:'又有人死了!'"
+→ 读者忍不住点下一章
+```
+
+---
+
+## 5. 常见结构错误
+
+### 错误1:第一幕过长(铺垫太多)
+**反面教材**:
+```
+第1-10章(5万字): 介绍背景、人物关系、侦探过往
+第11章: 案件终于发生
+→ 读者:"前10章在干嘛?"
+```
+
+**正确做法**:
+```
+第1章: 案件发生
+第2章: 侦探登场,背景通过对话/回忆补充
+```
+
+---
+
+### 错误2:第二幕拖沓(注水严重)
+**反面教材**:
+```
+第10-50章(20万字): 侦探反复调查,没有实质进展
+→ 读者:"能不能快点?"
+```
+
+**正确做法**:
+```
+每5-10章必须有实质进展:
+- 排除一名嫌疑人
+- 发现关键线索
+- 中期反转
+```
+
+---
+
+### 错误3:第三幕仓促(草草收尾)
+**反面教材**:
+```
+第19章: 揭示真相(3000字)
+第20章: 真凶认罪(1000字)
+结束。
+→ 读者:"就这?"
+```
+
+**正确做法**:
+```
+第三幕至少占全文 20%:
+- 充分展示推理过程
+- 解释所有线索
+- 补充动机与背景
+```
+
+---
+
+## 6. 推理小说的分卷策略(网文连载)
+
+### 单案件分卷(短篇)
+```
+第一卷(5-8万字): 一个完整案件
+- 引入 → 调查 → 揭示 → 收尾
+```
+
+**优点**: 结构完整,读者满足感高  
+**缺点**: 无法长篇连载
+
+---
+
+### 多案件串联(长篇)
+```
+第一卷(8万字): 案件 A(完结)
+第二卷(8万字): 案件 B(完结)
+第三卷(8万字): 案件 C(完结)
+……
+```
+
+**优点**: 可以无限连载  
+**缺点**: 缺乏主线,读者容易疲劳
+
+---
+
+### 主线 + 支线(推荐)
+```
+主线: 大案件(贯穿全书,分多卷解决)
+支线: 小案件(每卷一个,独立完结)
+
+第一卷(8万字): 小案件 A + 主线线索 1
+第二卷(8万字): 小案件 B + 主线线索 2
+第三卷(8万字): 小案件 C + 主线线索 3
+第四卷(10万字): 主线案件揭示,大结局
+```
+
+**优点**: 
+- 每卷有完整案件,读者有满足感
+- 主线贯穿,保持长期悬念
+
+---
+
+## 7. 推理小说的字数配比(参考)
+
+### 短篇推理(3-5万字)
+```
+第一幕(引入): 5000-8000字(15-20%)
+第二幕(调查): 1.5-2.5万字(50-60%)
+第三幕(揭示): 8000-1.2万字(20-25%)
+```
+
+---
+
+### 中篇推理(10-15万字)
+```
+第一幕(引入): 2-3万字(20%)
+第二幕(调查): 5-9万字(50-60%)
+第三幕(揭示): 3-4万字(20-25%)
+```
+
+---
+
+### 长篇推理(30万字以上)
+```
+第一卷(引入): 5-8万字(15-20%)
+第二卷(调查): 15-20万字(50-60%)
+第三卷(揭示): 8-12万字(20-25%)
+```
+
+---
+
+## 8. 结构与节奏自检清单
+
+写完推理小说后,检查以下项目:
+
+- [ ] **第一幕是否吸引人**: 前3章是否引入案件,吸引读者?
+- [ ] **第二幕是否有进展**: 每5-10章是否有实质性进展(线索/嫌疑人筛选/反转)?
+- [ ] **高潮点是否合理**: 是否有2-3个明显的高潮点?
+- [ ] **节奏是否张弛有度**: 是否有"紧张-放松"的节奏切换?
+- [ ] **第三幕是否充分**: 揭示阶段是否占全文 20% 以上?
+- [ ] **章末是否有悬念**: 每章结尾是否设置了悬念?(网文)
+- [ ] **字数配比是否合理**: 第一幕 20%,第二幕 50-60%,第三幕 20-25%?
+
+---
+
+## 🛠️ 结构与节奏速查表
+
+| 结构类型 | 适用篇幅 | 第一幕 | 第二幕 | 第三幕 | 特点 |
+|---------|---------|-------|-------|-------|------|
+| **经典三幕** | 短中篇 | 20% | 60% | 20% | 标准结构,适合所有推理 |
+| **快节奏** | 短篇 | 15% | 50% | 35% | 快速进入高潮,揭示充分 |
+| **多案件串联** | 长篇 | 每卷20% | 每卷60% | 每卷20% | 适合连载,每卷独立 |
+| **主线+支线** | 长篇 | 15% | 65% | 20% | 主线贯穿,支线独立 |
+
+---
+
+## 附录:经典推理小说结构分析
+
+### 案例1:《福尔摩斯探案集》- 短篇结构
+```
+第一幕(15%): 委托人求助,案件引入
+第二幕(60%): 福尔摩斯调查,收集线索
+第三幕(25%): 福尔摩斯揭示真相,抓捕凶手
+```
+
+**特点**: 结构紧凑,每篇独立。
+
+---
+
+### 案例2:《东方快车谋杀案》- 经典三幕
+```
+第一幕(25%): 火车上发生谋杀案,波洛登场
+第二幕(50%): 波洛调查12名乘客,收集线索
+第三幕(25%): 波洛揭示"所有人都是凶手"
+```
+
+**特点**: 严格遵循三幕结构,节奏完美。
+
+---
+
+### 案例3:网文推理《谜案追踪》(架空示例)
+```
+第一卷(8万字): 小案件 + 主线线索(神秘组织)
+第二卷(8万字): 小案件 + 主线线索(组织成员露面)
+第三卷(8万字): 小案件 + 主线线索(发现组织总部)
+第四卷(10万字): 主线案件,揭示组织阴谋
+```
+
+**特点**: 主线 + 支线结构,适合长篇连载。
+
+---
+
+## 特殊技巧:倒计时结构(制造紧张感)
+
+### 倒计时结构定义
+**核心**: 给侦探设定一个"截止时间",时间到了会有严重后果。
+
+**示例**:
+```
+设定: 凶手每24小时杀一人,侦探必须在下一个24小时内找到凶手
+效果: 读者持续紧张,"侦探能及时阻止吗?"
+```
+
+**经典案例**:
+- 《七宗罪》: 凶手按七宗罪顺序杀人,侦探必须在第七个受害者出现前抓住凶手
+- 《电锯惊魂》: 受害者被困,必须在规定时间内逃脱
+
+**网文应用**:
+```
+第1章: 第一个受害者死亡
+第5章: 第二个受害者死亡(24小时后)
+第9章: 第三个受害者死亡(又过24小时)
+侦探发现规律:"下一个受害者将在明天晚上8点死亡!"
+→ 倒计时开始
+```
+
+---
+
+## 结语:结构是骨架,节奏是血肉
+
+好的结构让推理小说"站得住",好的节奏让推理小说"跑得快"。
+
+**记住**:
+- 结构要清晰(三幕式)
+- 节奏要张弛(紧张-放松)
+- 高潮要明显(2-3个高潮点)
+- 悬念要持续(章末钩子)
+
+遵循这些原则,你的推理小说就能抓住读者的心。

+ 497 - 0
.claude/genres/rules-mystery/suspect-management.md

@@ -0,0 +1,497 @@
+# 嫌疑人管理 (Suspect Management)
+
+> **核心原则**: 嫌疑人不是"路人甲",每个人都要有动机、机会和性格。管理好嫌疑人 = 控制好推理难度。
+
+---
+
+## 1. 什么是"嫌疑人管理"?
+
+### 定义
+**嫌疑人管理(Suspect Management)**: 在本格推理中,合理设计嫌疑人的**数量、动机、机会、性格**,并通过**排除法**逐步缩小范围,最终指向真凶。
+
+### 为什么重要?
+```markdown
+✅ 嫌疑人太少: 读者轻易猜到凶手
+✅ 嫌疑人太多: 读者记不住,放弃推理
+✅ 动机不足: 读者觉得不合理
+✅ 机会不足: 读者觉得破绽太大
+```
+
+---
+
+## 2. 嫌疑人数量的黄金法则
+
+### 黄金数量
+```markdown
+短篇(<5万字): 3-5人
+中篇(5-15万字): 5-8人
+长篇(>15万字): 8-12人
+```
+
+**原则**: 嫌疑人数量不宜超过**12人**,否则读者记不住。
+
+---
+
+### 经典案例参考
+| 作品 | 嫌疑人数 | 效果 |
+|------|---------|------|
+| 《东方快车谋杀案》 | 12人 | 极限(所有人都是凶手) |
+| 《无人生还》 | 10人 | 逐一排除 |
+| 《暴风雪山庄》 | 7人 | 平衡(不多不少) |
+
+---
+
+## 3. 嫌疑人的"三要素"
+
+### 要素1:动机(Motive)
+**定义**: 嫌疑人为什么要杀死被害者?
+
+**常见动机**:
+| 动机类型 | 示例 | 强度 |
+|---------|------|------|
+| **仇恨** | 被害者杀了嫌疑人的家人 | ★★★★★ |
+| **利益** | 争夺遗产/生意 | ★★★★☆ |
+| **情感** | 三角恋/嫉妒 | ★★★☆☆ |
+| **秘密** | 被害者掌握嫌疑人的犯罪证据 | ★★★★☆ |
+| **意外** | 过失杀人/正当防卫过当 | ★★☆☆☆ |
+
+**注意**: 每个嫌疑人至少要有**一个**看似合理的动机。
+
+---
+
+### 要素2:机会(Opportunity)
+**定义**: 嫌疑人是否有时间和条件实施犯罪?
+
+**机会检查清单**:
+```markdown
+- [ ] 案发时嫌疑人在现场附近吗?
+- [ ] 嫌疑人有接触被害者的机会吗?
+- [ ] 嫌疑人的不在场证明是否有破绽?
+```
+
+**示例**:
+```markdown
+✅ 有机会:
+张三在案发时间(晚8点)独自在家,无人证明
+→ 有作案时间
+
+❌ 无机会:
+李四在案发时在国外出差,有机票和护照记录
+→ 不在场证明完美(除非有帮凶)
+```
+
+---
+
+### 要素3:性格(Character)
+**定义**: 嫌疑人的性格特征,影响读者对其是否可疑的判断。
+
+**性格光谱**:
+```markdown
+极端可疑 ←→ 极端无辜
+暴躁/阴险     温和/善良
+```
+
+**示例**:
+```markdown
+✅ 可疑性格:
+- 暴躁易怒(容易冲动杀人)
+- 阴险狡诈(精心策划)
+- 有犯罪前科
+
+✅ 无辜性格:
+- 温和善良
+- 老实本分
+- 弱小无力(老人/小孩/残疾人)
+```
+
+**陷阱**: 最不可疑的人往往是真凶(反套路)。
+
+---
+
+## 4. 嫌疑人设计模板
+
+### 模板A:标准嫌疑人卡
+```markdown
+## 嫌疑人01:张三
+**身份**: 被害者的商业合作伙伴
+**年龄**: 45岁
+**性格**: 精明、冷静、有城府
+
+**动机**: 
+被害者欠他500万,拒不还钱
+
+**机会**: 
+案发当晚(晚8点)独自在办公室,无人证明
+
+**不在场证明**: 
+声称在办公室加班,但无监控证据
+
+**可疑点**: 
+- 案发前一天与被害者发生激烈争吵
+- 手机记录显示案发时间关机
+
+**无辜证据**: 
+- 没有直接证据证明他在现场
+
+**真相**: (创作时标注,读者不可见)
+红鲱鱼,实际上在办公室加班
+```
+
+---
+
+### 模板B:嫌疑人对比表
+```markdown
+| 嫌疑人 | 动机 | 机会 | 可疑度 | 真实身份 |
+|--------|------|------|--------|---------|
+| 张三 | 金钱纠纷 | 无不在场证明 | ★★★★☆ | 红鲱鱼 |
+| 李四 | 情感纠纷 | 有破绽的不在场证明 | ★★★☆☆ | 红鲱鱼 |
+| 王五 | 秘密威胁 | 完美不在场证明 | ★☆☆☆☆ | **真凶** |
+```
+
+**作用**: 真凶往往是**可疑度最低**的那个人(反套路)。
+
+---
+
+## 5. 嫌疑人排除策略
+
+### 策略1:逐一排除法
+**流程**:
+```markdown
+Step 1: 列出所有嫌疑人
+Step 2: 侦探调查,发现部分人有完美不在场证明
+Step 3: 排除这些人
+Step 4: 剩余嫌疑人继续调查
+Step 5: 最终指向真凶
+```
+
+**示例**:
+```markdown
+初始嫌疑人: 7人
+第10章: 排除3人(有不在场证明)
+第20章: 再排除2人(动机不足)
+第25章: 剩余2人
+第30章: 指认真凶
+```
+
+---
+
+### 策略2:红鲱鱼误导法
+**定义**: 故意让某个无辜者看起来最可疑,误导读者。
+
+**示例**:
+```markdown
+## 案例:《东方快车谋杀案》
+误导: 所有人都有不在场证明 → 暗示凶手是外来者
+真相: 12人集体作案
+```
+
+---
+
+### 策略3:最不可疑的人法
+**定义**: 真凶是看起来最无辜的人。
+
+**经典套路**:
+```markdown
+✅ 真凶特征:
+- 弱小无力(老人/小孩/残疾人)
+- 温和善良(人设完美)
+- 看似与案件无关
+- 最先被排除嫌疑
+```
+
+**示例**:
+```markdown
+## 案例:《暴风雪山庄》
+最无辜者: 被害者本人
+真相: 被害者其实是凶手,假死伪装
+```
+
+---
+
+## 6. 嫌疑人的动机设计
+
+### 动机层次
+```markdown
+表层动机(明显): 金钱/情感
+深层动机(隐藏): 秘密/复仇
+```
+
+**示例**:
+```markdown
+## 案例
+表层: 张三与被害者有金钱纠纷
+深层: 张三其实是被害者的私生子,复仇
+```
+
+---
+
+### 假动机(误导)
+**作用**: 让读者以为某人有动机,实际上是假的。
+
+**示例**:
+```markdown
+线索: 李四曾威胁被害者:"我要杀了你!"
+真相: 李四只是一时气话,真凶另有其人
+```
+
+---
+
+## 7. 嫌疑人的机会管理
+
+### 不在场证明的设计
+
+#### 完美不在场证明
+**特征**: 有铁证证明嫌疑人不在现场
+
+**示例**:
+```markdown
+张三在案发时(晚8点)正在警局报案
+→ 有警局记录,完美不在场证明
+```
+
+**作用**: 排除嫌疑,缩小范围
+
+---
+
+#### 有破绽的不在场证明
+**特征**: 不在场证明看似完美,但有漏洞
+
+**示例**:
+```markdown
+李四声称晚8点在家看电视
+证据: 邻居听到电视声音
+破绽: 电视可以定时播放,李四可能不在
+```
+
+**作用**: 增加悬念
+
+---
+
+#### 虚假不在场证明(真凶常用)
+**特征**: 真凶通过诡计伪造不在场证明
+
+**示例**:
+```markdown
+王五伪造时间线,让死者看起来在晚9点死亡
+实际: 死者在晚7点就被杀,王五有时间作案
+```
+
+---
+
+## 8. 嫌疑人的性格塑造
+
+### 性格与可疑度的关系
+
+| 性格类型 | 可疑度 | 读者反应 | 真凶概率 |
+|---------|--------|---------|---------|
+| **暴躁易怒** | 高 | "肯定是他!" | 低(红鲱鱼) |
+| **阴险狡诈** | 高 | "很可疑" | 中 |
+| **温和善良** | 低 | "不可能是他" | 高(反套路) |
+| **弱小无力** | 极低 | "绝对不是" | 极高(大反转) |
+
+---
+
+### 性格塑造技巧
+
+#### 技巧1:言行塑造
+**示例**:
+```markdown
+张三(暴躁):
+"你再说一句,我就杀了你!"(威胁语气)
+
+李四(温和):
+"算了算了,都是误会。"(好好先生)
+```
+
+---
+
+#### 技巧2:他人评价
+**示例**:
+```markdown
+其他人评价张三:"他脾气很坏,经常和人吵架。"
+其他人评价李四:"他人很好,从不和人结仇。"
+```
+
+---
+
+#### 技巧3:行为细节
+**示例**:
+```markdown
+张三发现尸体时:
+"该死!"(冷静,没有惊慌)
+→ 可疑:为什么不惊讶?
+
+李四发现尸体时:
+"啊!"(惊恐,双腿发软)
+→ 看似无辜
+```
+
+---
+
+## 9. 嫌疑人管理常见错误
+
+### 错误1:嫌疑人太多,读者记不住
+**问题**:
+```markdown
+嫌疑人: 15人
+→ 读者:"谁是谁?我记不住!"
+```
+
+**解决**:
+```markdown
+✅ 控制在12人以内
+✅ 给每个人鲜明特征(外貌/性格/口头禅)
+```
+
+---
+
+### 错误2:所有人动机都很强
+**问题**:
+```markdown
+所有人都有杀人动机
+→ 读者无法判断谁最可疑
+```
+
+**解决**:
+```markdown
+✅ 动机分层: 强/中/弱
+✅ 只有2-3人有强动机
+```
+
+---
+
+### 错误3:真凶毫无动机
+**问题**:
+```markdown
+真凶: 王五
+动机: 无
+→ 读者:"这也太牵强了!"
+```
+
+**解决**:
+```markdown
+✅ 真凶必须有合理动机(可以隐藏,但不能没有)
+✅ 在揭示时补充解释
+```
+
+---
+
+### 错误4:排除嫌疑太快
+**问题**:
+```markdown
+第5章: 7个嫌疑人
+第10章: 排除6个
+→ 读者:"那肯定是剩下的那个了"
+```
+
+**解决**:
+```markdown
+✅ 逐步排除,不要一次性排除太多
+✅ 排除后再引入新嫌疑人
+```
+
+---
+
+## 10. 嫌疑人管理工具
+
+### 工具1:嫌疑人追踪表
+```markdown
+| 嫌疑人 | 动机 | 机会 | 证据 | 状态 |
+|--------|------|------|------|------|
+| 张三 | 金钱 | 无证明 | 争吵记录 | 嫌疑中 |
+| 李四 | 情感 | 有破绽证明 | 威胁短信 | 嫌疑中 |
+| 王五 | 秘密 | 完美证明 | 无 | 已排除 |
+```
+
+---
+
+### 工具2:时间线对照表
+```markdown
+## 案发时间:晚8点
+| 嫌疑人 | 晚7点 | 晚8点 | 晚9点 | 证明 |
+|--------|-------|-------|-------|------|
+| 张三 | 公司 | ? | 家 | 无 |
+| 李四 | 家 | 家 | 家 | 邻居证言 |
+| 王五 | 警局 | 警局 | 警局 | 警局记录 |
+```
+
+---
+
+### 工具3:嫌疑度评分表
+```markdown
+| 嫌疑人 | 动机(0-5) | 机会(0-5) | 证据(0-5) | 总分 |
+|--------|----------|----------|----------|------|
+| 张三 | 5 | 4 | 3 | 12 |
+| 李四 | 3 | 2 | 2 | 7 |
+| 王五 | 1 | 0 | 0 | 1 |
+```
+
+**作用**: 真凶往往是**总分最低**的那个(反套路)。
+
+---
+
+## 11. 高级技巧:多重身份
+
+### 技巧:一人分饰多角
+**原理**: 一个人伪装成两个不同的身份,制造"两个人"的假象。
+
+**示例**:
+```markdown
+## 案例
+表面: A和B是两个人
+真相: A和B是同一人,通过易容伪装
+```
+
+**注意**: 必须在前文埋伏笔(如"从未同时出现")。
+
+---
+
+## 12. 嫌疑人管理自检清单
+
+**设计完嫌疑人后逐项检查**:
+- [ ] 嫌疑人数量是否合理(3-12人)?
+- [ ] 每个嫌疑人都有明确动机吗?
+- [ ] 每个嫌疑人的机会是否清晰?
+- [ ] 真凶的动机是否合理?
+- [ ] 真凶的不在场证明破绽是否设计好?
+- [ ] 红鲱鱼是否足够迷惑读者?
+- [ ] 排除嫌疑的节奏是否合理?
+- [ ] 真凶是否是"最不可疑的人"?
+
+---
+
+## 🛠️ 嫌疑人管理速查表
+
+| 要素 | 标准 | 常见错误 | 解决方案 |
+|------|------|---------|---------|
+| **数量** | 3-12人 | 太多/太少 | 控制在黄金范围 |
+| **动机** | 每人至少1个 | 真凶无动机 | 补充隐藏动机 |
+| **机会** | 时间线清晰 | 时间线矛盾 | 绘制时间表 |
+| **性格** | 鲜明特征 | 人物脸谱化 | 增加细节 |
+| **排除** | 逐步排除 | 一次排除太多 | 分阶段排除 |
+
+---
+
+## 附录:经典案例分析
+
+### 案例1:《东方快车谋杀案》
+**嫌疑人**: 12人  
+**特点**: 所有人都有完美不在场证明  
+**真相**: 12人集体作案  
+**技巧**: 用"完美证明"误导读者  
+
+---
+
+### 案例2:《无人生还》
+**嫌疑人**: 10人  
+**特点**: 逐一被杀  
+**真相**: 法官假死,是真凶  
+**技巧**: 最早"被杀"的人是凶手  
+
+---
+
+## 总结
+
+**嫌疑人管理 = 数量控制 + 动机设计 + 机会管理 + 性格塑造 + 排除策略**
+
+记住:真凶往往是"最不可疑的人"。管理好嫌疑人,就是控制好推理的难度和乐趣。

+ 507 - 0
.claude/genres/rules-mystery/trick-design.md

@@ -0,0 +1,507 @@
+# 诡计设计 (Trick Design)
+
+> **核心原则**: 好的诡计 = 简单原理 + 巧妙应用 + 意外性。复杂不等于高明。
+
+---
+
+## 1. 什么是"诡计"?
+
+### 定义
+**诡计(Trick/Gimmick)**: 凶手为了实施犯罪或掩盖罪行而使用的特殊手段,通常涉及**密室**、**不在场证明**、**身份伪装**等。
+
+### 诡计的三大作用
+```markdown
+1. 制造谜题: 让读者困惑"这怎么可能?"
+2. 延迟破案: 增加侦探调查难度
+3. 制造惊喜: 揭示时让读者恍然大悟
+```
+
+---
+
+## 2. 诡计的分类
+
+### 按目的分类
+
+#### 类型1:密室诡计(Locked Room)
+**目的**: 制造"密闭空间内杀人,凶手却消失"的不可能犯罪
+
+**经典模式**:
+```markdown
+✅ 冰块诡计: 用冰块固定门栓,融化后门自动上锁
+✅ 双重密室: 内室是真密室,外室是伪装
+✅ 时间差: 凶手在死者锁门前藏在房内
+✅ 机关装置: 利用绳索/重物制造延时上锁
+```
+
+---
+
+#### 类型2:不在场证明诡计(Alibi Trick)
+**目的**: 让凶手在案发时看似不在现场
+
+**经典模式**:
+```markdown
+✅ 时间诡计: 伪造死亡时间(调慢钟表、冰冻尸体)
+✅ 替身诡计: 找人冒充自己在别处出现
+✅ 远程杀人: 提前设置机关,人不在也能杀人
+✅ 证人串通: 买通证人作伪证
+```
+
+---
+
+#### 类型3:身份诡计(Identity Trick)
+**目的**: 隐藏凶手真实身份或制造混淆
+
+**经典模式**:
+```markdown
+✅ 双胞胎诡计: 两人轮流出现,伪造不在场证明
+✅ 易容诡计: 化妆成他人
+✅ 死者身份诡计: 被害者其实是凶手
+✅ 身份互换: A假死,伪装成B
+```
+
+---
+
+#### 类型4:凶器诡计(Weapon Trick)
+**目的**: 隐藏或伪装凶器
+
+**经典模式**:
+```markdown
+✅ 冰凶器: 用冰锥杀人,融化后消失
+✅ 日常物品: 用绳索/枕头等非典型凶器
+✅ 毒物诡计: 慢性毒药,延迟发作
+✅ 凶器转移: 杀人后把凶器藏在意外地点
+```
+
+---
+
+#### 类型5:心理诡计(Psychological Trick)
+**目的**: 利用读者/侦探的思维定势误导推理
+
+**经典模式**:
+```markdown
+✅ 最不可能的人: 凶手是最不可疑的人(小孩/老人/残疾人)
+✅ 叙述性诡计: 叙述者本身是凶手(《罗杰疑案》)
+✅ 暴风雪山庄反转: 被害者其实是凶手
+✅ 多重身份: 一人分饰多角
+```
+
+---
+
+### 按复杂度分类
+
+| 等级 | 特点 | 适用场景 | 示例 |
+|------|------|---------|------|
+| **简单诡计** | 单一原理 | 短篇/网文 | 冰块固定门栓 |
+| **复合诡计** | 2-3个诡计组合 | 中长篇 | 时间诡计+替身诡计 |
+| **多重诡计** | 诡计套诡计 | 长篇神作 | 《无人生还》 |
+
+**注意**: 网文推荐使用**简单诡计**或**复合诡计**,多重诡计容易让读者看不懂。
+
+---
+
+## 3. 诡计设计的四大原则
+
+### 原则1:简单性(Simplicity)
+**定义**: 核心原理必须简单,读者事后能理解。
+
+**反面教材**:
+```markdown
+❌ 错误: 凶手利用量子纠缠原理制造密室
+→ 读者完全不懂
+```
+
+**正面示例**:
+```markdown
+✅ 正确: 凶手用冰块固定门栓,冰融化后门自动上锁
+→ 原理简单,一听就懂
+```
+
+---
+
+### 原则2:新颖性(Novelty)
+**定义**: 诡计要有创新,不能完全照搬经典。
+
+**避免陈词滥调**:
+```markdown
+❌ 过时诡计:
+- 双胞胎(用烂了)
+- 冰锥杀人(太经典)
+- 左轮手枪俄罗斯轮盘赌(老套)
+```
+
+**创新方法**:
+```markdown
+✅ 旧瓶装新酒:
+- 双胞胎 → 改为整容成双胞胎
+- 冰锥 → 改为干冰/液氮
+- 密室 → 改为"开放式密室"(人人都能进,但凶手消失)
+```
+
+---
+
+### 原则3:合理性(Plausibility)
+**定义**: 诡计必须在现实中**理论上可行**,不能过于魔幻。
+
+**合理性检查**:
+```markdown
+✅ 物理可行: 符合物理定律(重力/惯性/热力学)
+✅ 时间可行: 凶手有足够时间实施
+✅ 技术可行: 不依赖未来科技或魔法
+```
+
+**反面教材**:
+```markdown
+❌ 凶手用瞬移能力逃出密室
+→ 超自然力量,违背本格推理原则
+```
+
+---
+
+### 原则4:公平性(Fairness)
+**定义**: 读者能根据线索推理出诡计。
+
+**公平性要求**:
+```markdown
+✅ 诡计所需的关键物品必须在前文提及
+✅ 诡计原理不能过于专业(除非有解释)
+✅ 不能依赖读者不知道的隐藏信息
+```
+
+**示例**:
+```markdown
+✅ 公平:
+第5章: 提到房间有个壁橱
+第20章: 揭示凶手藏在壁橱里
+
+❌ 不公平:
+第20章: 突然揭示房间有密道
+→ 前文从未提及
+```
+
+---
+
+## 4. 经典诡计解析
+
+### 诡计1:冰块密室
+**原理**: 用冰块固定门栓,冰融化后门自动上锁
+
+**实施步骤**:
+```markdown
+1. 凶手杀人后,用冰块顶住门栓
+2. 凶手离开房间,从外面关门
+3. 冰块融化,门栓自动落下,形成密室
+```
+
+**线索**:
+```markdown
+- 门下方有水渍
+- 房间温度略高
+- 时间线有疑点(冰融化需要时间)
+```
+
+---
+
+### 诡计2:时间诡计(伪造死亡时间)
+**原理**: 让死者看起来在凶手有不在场证明的时间死亡
+
+**常用方法**:
+```markdown
+1. 调慢死者手表
+2. 提前杀人,用冰块冷冻尸体(降低尸温)
+3. 制造"死者还活着"的假象(录音/定时发短信)
+```
+
+**线索**:
+```markdown
+- 尸体温度异常
+- 手表时间与其他证据矛盾
+- 目击证言有漏洞
+```
+
+---
+
+### 诡计3:叙述性诡计(叙述者是凶手)
+**原理**: 故事以第一人称叙述,但叙述者本身是凶手,通过省略关键信息误导读者
+
+**经典案例**: 《罗杰疑案》(阿加莎·克里斯蒂)
+
+**实施技巧**:
+```markdown
+1. 叙述者描述事件时,省略自己的犯罪行为
+2. 用暗示性语言误导读者
+3. 最后揭示:"我就是凶手"
+```
+
+**注意**: 这是高难度诡计,需要极高的叙事技巧。
+
+---
+
+### 诡计4:暴风雪山庄反转
+**原理**: 读者以为A是被害者,实际上A是凶手,伪装成被害者
+
+**实施步骤**:
+```markdown
+1. 凶手A杀死B
+2. A伪装成B的尸体
+3. A假死,让大家以为A被杀
+4. 实际上死者是B,凶手A还活着
+```
+
+**经典案例**: 《暴风雪山庄》(东野圭吾)
+
+---
+
+### 诡计5:多人共犯(集体作案)
+**原理**: 所有嫌疑人都是凶手,集体作案
+
+**实施步骤**:
+```markdown
+1. 每个人只负责一部分(A负责下毒,B负责处理尸体)
+2. 互相提供不在场证明
+3. 侦探难以找到单一凶手
+```
+
+**经典案例**: 《东方快车谋杀案》
+
+---
+
+## 5. 诡计设计流程
+
+### Step 1: 确定诡计类型
+**选择标准**: 根据故事需要选择合适的诡计类型
+
+**示例**:
+```markdown
+故事需要: 密闭空间杀人
+→ 选择: 密室诡计
+```
+
+---
+
+### Step 2: 构思核心原理
+**核心问题**: "凶手如何做到的?"
+
+**示例**:
+```markdown
+问题: 如何在上锁房间内杀人后逃脱?
+核心原理: 提前藏在房内,杀人后伪装成第一发现者
+```
+
+---
+
+### Step 3: 检查可行性
+**可行性检查清单**:
+```markdown
+- [ ] 物理上可行吗?
+- [ ] 凶手有足够时间吗?
+- [ ] 需要什么道具?(道具必须在前文提及)
+- [ ] 有没有破绽?(如果有,如何掩盖?)
+```
+
+---
+
+### Step 4: 设计破绽
+**核心问题**: "侦探如何发现破绽?"
+
+**示例**:
+```markdown
+诡计: 凶手藏在壁橱里
+破绽: 壁橱门有划痕,凶手衣服上有灰尘
+```
+
+---
+
+### Step 5: 埋设线索
+**核心问题**: "如何让读者能推理出诡计?"
+
+**示例**:
+```markdown
+第5章: 提到房间有壁橱
+第10章: 第一发现者衣服上有灰尘
+第15章: 壁橱门有划痕
+→ 读者可以推理:凶手藏在壁橱里
+```
+
+---
+
+## 6. 诡计的常见错误
+
+### 错误1:过于复杂
+**示例**:
+```markdown
+❌ 错误:
+凶手利用镜面反射+声音传播延迟+心理暗示
+→ 三重诡计叠加,读者看不懂
+```
+
+**改进**:
+```markdown
+✅ 正确:
+凶手利用镜面反射制造"死者还活着"的错觉
+→ 单一诡计,简单明了
+```
+
+---
+
+### 错误2:依赖运气
+**示例**:
+```markdown
+❌ 错误:
+凶手碰巧发现密道
+→ 全靠运气,不合理
+```
+
+**改进**:
+```markdown
+✅ 正确:
+凶手提前调查房屋结构,发现密道
+→ 有准备,合理
+```
+
+---
+
+### 错误3:违背物理定律
+**示例**:
+```markdown
+❌ 错误:
+凶手用意念移动物体
+→ 超自然力量
+```
+
+**改进**:
+```markdown
+✅ 正确:
+凶手用隐形细线拉动物体
+→ 符合物理定律
+```
+
+---
+
+### 错误4:前文无伏笔
+**示例**:
+```markdown
+❌ 错误:
+第30章突然揭示:"房间其实有密道!"
+→ 前文从未提及
+```
+
+**改进**:
+```markdown
+✅ 正确:
+第5章: 房屋是古宅,有很多秘密
+第10章: 侦探注意到墙壁厚度异常
+第30章: 揭示密道
+→ 有伏笔
+```
+
+---
+
+## 7. 诡计创新方法
+
+### 方法1:旧诡计新用
+**示例**:
+```markdown
+经典: 冰块密室
+创新: 干冰密室(升华不留水渍)
+```
+
+---
+
+### 方法2:逆向思维
+**示例**:
+```markdown
+常规: 凶手制造密室逃脱
+逆向: 凶手制造"开放式密室"(人人能进,但凶手消失)
+```
+
+---
+
+### 方法3:组合创新
+**示例**:
+```markdown
+诡计A: 时间诡计(伪造死亡时间)
+诡计B: 替身诡计(找人冒充)
+组合: 替身在A地制造不在场证明,凶手在B地杀人
+```
+
+---
+
+### 方法4:现代化改造
+**示例**:
+```markdown
+经典: 电话录音制造"死者还活着"的假象
+现代: 用AI语音合成制造假电话
+```
+
+---
+
+## 8. 诡计设计自检清单
+
+**完成诡计设计后逐项检查**:
+- [ ] 诡计原理是否简单?
+- [ ] 诡计是否有新意?(不是完全照搬经典)
+- [ ] 诡计在物理上可行吗?
+- [ ] 凶手有足够时间实施吗?
+- [ ] 所需道具是否在前文提及?
+- [ ] 诡计的破绽是否合理?
+- [ ] 读者能否根据线索推理出诡计?
+- [ ] 有没有违背"十诫"?
+
+---
+
+## 🛠️ 诡计设计速查表
+
+| 诡计类型 | 难度 | 适用篇幅 | 核心要素 | 常见破绽 |
+|---------|------|---------|---------|---------|
+| **密室诡计** | 中 | 短篇/中篇 | 物理机关 | 现场痕迹 |
+| **时间诡计** | 低 | 短篇 | 时间线 | 尸体温度 |
+| **身份诡计** | 高 | 中长篇 | 伪装/易容 | 细节破绽 |
+| **叙述诡计** | 极高 | 长篇 | 叙事技巧 | 逻辑漏洞 |
+| **心理诡计** | 中 | 短篇/中篇 | 思维定势 | 动机不合理 |
+
+---
+
+## 附录:诡计创作工具
+
+### 工具1:诡计设计模板
+```markdown
+## 诡计名称
+**类型**: 密室/时间/身份/凶器/心理
+
+**核心原理**:
+(用一句话描述诡计核心)
+
+**实施步骤**:
+1. 
+2. 
+3. 
+
+**所需道具**:
+-
+
+**破绽**:
+-
+
+**线索埋设**:
+- 第X章: 
+- 第Y章: 
+```
+
+---
+
+### 工具2:可行性检验表
+```markdown
+- [ ] 物理可行性: 是/否
+- [ ] 时间可行性: 是/否
+- [ ] 技术可行性: 是/否
+- [ ] 公平性: 线索是否充分
+- [ ] 新颖性: 是否有创新
+```
+
+---
+
+## 总结
+
+**好诡计 = 简单原理 + 巧妙应用 + 意外性 + 公平性**
+
+记住:诡计的目的不是"炫技",而是让读者看完后恍然大悟:"原来是这样!我也能想到!"

+ 476 - 0
.claude/genres/xuanhuan/cultivation-levels.md

@@ -0,0 +1,476 @@
+# 修炼境界设定 (Cultivation Levels Design)
+
+> **核心原则**: 修炼境界是玄幻小说的进度条。好的境界设定 = 清晰的层次感 + 合理的突破难度 + 足够的悬念空间。
+
+---
+
+## 1. 境界设定的五大核心要素
+
+### 要素 1: 境界名称(Naming)
+**目的**: 让读者一听就知道"这个境界很牛"。
+
+**命名技巧**:
+
+#### 技巧 A: 递进式命名(由弱到强逐渐递进)
+```
+炼体 → 凝气 → 筑基 → 金丹 → 元婴 → 化神 → 炼虚 → 合体 → 大乘 → 渡劫
+```
+**特点**: 每个名称都暗示"修炼的阶段"(炼体 = 锤炼肉身,凝气 = 凝聚灵气)
+
+#### 技巧 B: 数字等级式命名
+```
+一星武者 → 二星武者 → ... → 九星武者
+一级魔法师 → 二级魔法师 → ... → 十级魔法师
+```
+**特点**: 简单直观,读者不会搞混
+
+#### 技巧 C: 称号式命名
+```
+武徒 → 武者 → 武师 → 武宗 → 武尊 → 武圣 → 武帝 → 武神
+```
+**特点**: 称号越高越牛逼,有"王者气息"
+
+#### 技巧 D: 意境式命名
+```
+凡境 → 灵境 → 王境 → 皇境 → 帝境 → 圣境 → 神境
+```
+**特点**: 抽象感强,适合高逼格作品
+
+#### 技巧 E: 混搭式命名(高级玩法)
+```
+前期(具体):炼体 → 凝气 → 筑基
+中期(称号):金丹 → 元婴 → 化神
+后期(意境):真仙 → 玄仙 → 金仙 → 太乙 → 大罗
+```
+**特点**: 前期易懂,后期高大上
+
+---
+
+### 要素 2: 境界数量(How Many)
+**核心**: 境界太少撑不到完本,境界太多读者记不住。
+
+**建议数量**:
+| 小说长度 | 大境界数量 | 小境界数量 | 总层级 |
+|---------|-----------|-----------|--------|
+| **短篇(30万字)** | 5-7个 | 每个3-4层 | 15-28层 |
+| **中篇(50-100万字)** | 7-9个 | 每个3-5层 | 21-45层 |
+| **长篇(100-300万字)** | 9-12个 | 每个4-9层 | 36-108层 |
+| **超长篇(300万字+)** | 12-15个 | 分多个大世界 | 不限 |
+
+**示例(《凡人修仙传》- 900万字)**:
+```
+人界: 练气(13层) → 筑基 → 结丹 → 元婴 → 化神
+灵界: 炼虚 → 合体 → 大乘 → 渡劫
+仙界: 真仙 → 玄仙 → 金仙 → 太乙 → 大罗
+```
+
+---
+
+### 要素 3: 境界描述(What It Means)
+**目的**: 让读者理解"这个境界在干什么"。
+
+**描述模板**:
+```markdown
+## 境界:金丹期
+**核心**: 凝聚金丹,将体内灵力压缩成一颗"金色丹丸"。
+
+**修炼过程**:
+1. 筑基大圆满后,灵力饱和
+2. 服用"凝丹丹",开始凝聚金丹
+3. 灵力在丹田内高速旋转、压缩
+4. 最终形成一颗金色的"金丹"
+
+**能力提升**:
+- 灵力总量: 是筑基期的10倍
+- 灵力质量: 更加精纯(1份金丹灵力 = 10份筑基灵力)
+- 寿命: 500年(筑基期只有200年)
+- 神识: 可以神识外放,探测方圆百里
+
+**标志性能力**:
+- 飞行: 可以御剑飞行(筑基期只能短距离滑翔)
+- 法术: 可以施展"金丹级法术"(如"金光斩"、"灵气护盾")
+```
+
+---
+
+### 要素 4: 突破难度(How Hard)
+**目的**: 制造紧张感和成就感。
+
+**难度分级**:
+```
+低级突破(炼气 → 筑基): 成功率 50-80%
+中级突破(筑基 → 金丹): 成功率 30-50%
+高级突破(金丹 → 元婴): 成功率 10-30%
+神级突破(元婴 → 化神): 成功率 1-10%
+```
+
+**失败后果**:
+```
+轻度失败: 境界跌落(金丹后期 → 金丹初期)
+中度失败: 金丹破碎,修为尽失,沦为废人
+重度失败: 走火入魔,经脉尽断,死亡
+极重度失败: 被天劫劈死(肉身、神魂俱灭)
+```
+
+---
+
+### 要素 5: 资源需求(What It Costs)
+**目的**: 让"升级"有代价感。
+
+**资源类型**:
+| 资源类型 | 作用 | 稀缺度 | 示例 |
+|---------|------|--------|------|
+| **丹药** | 辅助突破 | 高 | 破婴丹、化神丹 |
+| **灵石** | 提供灵力 | 中 | 下品/中品/上品/极品灵石 |
+| **功法** | 修炼路线 | 高 | 天级功法、神级功法 |
+| **天材地宝** | 洗髓伐体 | 极高 | 千年灵芝、龙血果 |
+| **传承** | 经验指导 | 极高 | 大能传承、上古秘法 |
+
+**示例(金丹期 → 元婴期)**:
+```markdown
+## 突破资源清单
+**必需资源**:
+- 破婴丹 × 1(价值:100万灵石)
+- 极品灵石 × 100(价值:100万灵石)
+- 元婴期功法一部(无价)
+
+**可选资源(提高成功率)**:
+- 天劫符 × 3(减轻天劫威力)
+- 护体法宝(抵御雷劫)
+- 闭关洞府(灵气浓郁之地)
+
+**总花费**: 至少200万灵石(普通家族倾家荡产)
+```
+
+---
+
+## 2. 境界与战力的关系
+
+### 关系 1: 基础战力倍增
+**公式**: 每提升一个大境界,基础战力提升5-10倍。
+
+**示例**:
+```
+炼气期: 10战力
+筑基期: 50战力(5倍)
+金丹期: 500战力(10倍)
+元婴期: 5000战力(10倍)
+化神期: 50000战力(10倍)
+```
+
+**用途**: 制造"境界压制"感(高一个大境界就是碾压)
+
+---
+
+### 关系 2: 小境界差距
+**公式**: 每提升一个小境界,战力提升20-50%。
+
+**示例**:
+```
+金丹初期: 500战力
+金丹中期: 750战力(1.5倍)
+金丹后期: 1125战力(1.5倍)
+金丹大圆满: 1687战力(1.5倍)
+```
+
+**用途**: 同境界内也有强弱之分
+
+---
+
+### 关系 3: 越级挑战公式
+**核心**: 主角凭什么越级挑战?
+
+**公式**:
+```
+主角实际战力 = 境界基础战力 × 功法倍数 × 武技倍数 × 法宝倍数 × 血脉倍数 × 秘法倍数
+```
+
+**示例**:
+```
+主角(筑基期):
+- 境界战力: 50
+- 天级功法: ×3
+- 绝学武技: ×2
+- 神器: ×2
+- 上古血脉: ×2
+→ 实际战力: 50 × 3 × 2 × 2 × 2 = 1200
+
+敌人(金丹初期):
+- 境界战力: 500
+- 玄级功法: ×1.5
+→ 实际战力: 500 × 1.5 = 750
+
+结果: 主角虽然境界低,但战力更强!
+```
+
+---
+
+## 3. 境界突破的剧情设计
+
+### 设计 1: 常规突破(5000字标准流程)
+**流程**:
+```markdown
+## 第1步:准备阶段(1000字)
+林天盘膝坐下,拿出破婴丹。
+"成败在此一举……"
+他深吸一口气,将丹药吞下。
+
+## 第2步:冲击阶段(2000字)
+丹药入腹,化作滚滚热流!
+林天体内的金丹开始颤动。
+"要碎了!"
+金丹表面出现裂痕……
+
+(此处描写:灵力暴动、经脉剧痛、生死关头)
+
+## 第3步:天劫阶段(1500字)
+就在这时,天空乌云密布!
+"是天劫!"
+轰隆——
+第一道雷霆劈下!
+
+(此处描写:雷劫威力、林天苦苦支撑、险些失败)
+
+## 第4步:成功阶段(500字)
+最后一道雷霆散去。
+林天睁开眼睛,眼中神光湛湛。
+"元婴期……终于成功了!"
+他握了握拳,感受着体内澎湃的力量。
+
+(此处描写:力量暴增、境界稳固)
+```
+
+---
+
+### 设计 2: 特殊突破(创新玩法)
+
+#### 类型 A: 战斗中突破
+```
+林天被敌人压制,生死关头突然顿悟,当场突破!
+→ 爽点:绝地反击,打脸敌人
+```
+
+#### 类型 B: 奇遇突破
+```
+林天误入上古洞府,获得大能传承,直接连跳三级!
+→ 爽点:一波肥,实力暴涨
+```
+
+#### 类型 C: 压抑后爆发突破
+```
+林天被封印修为十年,解封后积累爆发,连续突破!
+→ 爽点:憋屈后的大爽
+```
+
+#### 类型 D: 群体突破(团队向)
+```
+主角团队所有人同时突破(因为某个特殊机缘)
+→ 爽点:团队共同成长
+```
+
+---
+
+## 4. 境界与世界观的融合
+
+### 融合点 1: 地图分层
+**核心**: 不同境界对应不同地图。
+
+**示例**:
+```markdown
+## 世界地图分层(大陆飞升)
+**低级大陆(青玄大陆)**:
+- 灵气稀薄
+- 最高只能修炼到金丹期
+- 金丹期就是"陆地神仙"
+
+**中级大陆(天元大陆)**:
+- 灵气浓郁
+- 最高可以修炼到化神期
+- 金丹期只是"普通修士"
+
+**高级大陆(圣界)**:
+- 灵气充沛
+- 最高可以修炼到合体期
+- 化神期才能勉强立足
+
+**神域(仙界)**:
+- 仙灵之气
+- 可以修炼到渡劫期乃至成仙
+- 合体期也只是"下等仙人"
+```
+
+**用途**:
+- 制造"井底之蛙"反转
+- 主角从"无敌"到"垫底"再到"无敌"的循环
+
+---
+
+### 融合点 2: 寿命与时间线
+**核心**: 境界越高,寿命越长,故事时间跨度越大。
+
+**寿命对照表**:
+| 境界 | 寿命 | 时间跨度 | 剧情影响 |
+|------|------|---------|---------|
+| **炼气期** | 100年 | 10-20年 | 快节奏,紧迫感 |
+| **筑基期** | 200年 | 20-50年 | 可以培养后代 |
+| **金丹期** | 500年 | 50-100年 | 故人逐渐老去 |
+| **元婴期** | 1000年 | 100-300年 | 朝代更迭 |
+| **化神期** | 2000年 | 300-500年 | 岁月沧桑感 |
+| **炼虚期** | 5000年 | 500-1000年 | 上古遗迹主人可能还活着 |
+| **合体期** | 10000年 | 1000-3000年 | 见证文明兴衰 |
+| **大乘期** | 100000年 | 万年起步 | 时间失去意义 |
+
+**剧情应用**:
+```
+示例1(金丹期):
+林天突破到金丹期,寿命500年。
+他的凡人朋友已经白发苍苍……
+"时间……过得真快……"
+
+示例2(化神期):
+林天化神期,寿命2000年。
+闭关300年后出关,当年的宗门已经换了十代宗主。
+```
+
+---
+
+### 融合点 3: 势力等级门槛
+**核心**: 加入高级势力需要达到特定境界。
+
+**示例**:
+```markdown
+## 势力等级表
+| 势力等级 | 最低境界 | 代表势力 | 地位 |
+|---------|---------|---------|------|
+| **凡俗势力** | 无 | 王国、家族 | 最底层 |
+| **三流宗门** | 筑基期 | 青云宗 | 入门弟子 |
+| **二流宗门** | 金丹期 | 天剑宗 | 核心弟子 |
+| **一流宗门** | 元婴期 | 太玄宗 | 长老 |
+| **圣地** | 化神期 | 五大圣地 | 太上长老 |
+| **隐世家族** | 炼虚期 | 古族 | 族长 |
+| **神秘组织** | 合体期 | 天机阁 | 阁主 |
+```
+
+---
+
+## 5. 境界设定的常见问题与解决
+
+### 问题 1: 境界名称太复杂,读者记不住
+**现象**:
+```
+第1境界:炼体淬骨九转涅槃期
+第2境界:凝气聚灵百脉归元期
+……
+读者:这什么鬼???
+```
+
+**解决方案**:
+- **简化命名**: 炼体期、凝气期、筑基期……
+- **统一前缀**: 斗之气、斗者、斗师、斗灵……(都是"斗"字辈)
+
+---
+
+### 问题 2: 境界太多,后期崩盘
+**现象**:
+```
+第1卷: 炼气期 → 筑基期
+第2卷: 金丹期
+第3卷: 元婴期
+……
+第50卷: 已经渡劫飞升十次了,还在升级……
+```
+
+**解决方案**:
+- **横向发展**: 不升境界,提升战技/法宝/血脉
+- **多元目标**: 不只是"升级",还有"复仇/寻宝/守护"等目标
+
+---
+
+### 问题 3: 突破太容易,没有成就感
+**现象**:
+```
+主角每次突破都是"轻松愉快",从不失败
+```
+
+**解决方案**:
+- **设置失败**: 第一次突破失败,第二次才成功
+- **提高代价**: 突破需要付出巨大代价(燃烧寿命/失去记忆)
+
+---
+
+### 问题 4: 境界描述枯燥
+**现象**:
+```
+"林天突破到了金丹期,实力变强了。"
+```
+
+**解决方案**:
+- **具体化描写**: "金丹在丹田内缓缓旋转,散发金色光芒……"
+- **能力展示**: "他一拳轰出,空气炸裂,地面龟裂!"
+
+---
+
+## 6. 境界设定自检清单
+
+- [ ] **名称易记**: 境界名称是否简洁、有规律?
+- [ ] **数量合理**: 境界数量是否匹配预期字数?
+- [ ] **描述清晰**: 每个境界的特点是否明确?
+- [ ] **难度递增**: 突破难度是否逐渐提高?
+- [ ] **资源需求明确**: 突破需要什么资源?
+- [ ] **战力倍增**: 境界提升后战力是否有质变?
+- [ ] **世界观融合**: 境界是否与地图/势力/寿命对应?
+- [ ] **前后一致**: 设定是否有矛盾?
+
+---
+
+## 🛠️ 境界设定速查表
+
+| 境界类型 | 命名方式 | 境界数量 | 突破难度 | 适用题材 |
+|---------|---------|---------|---------|---------|
+| **传统修仙** | 炼气/筑基/金丹… | 9-12个 | 高(需天劫) | 修真/仙侠 |
+| **简化修仙** | 一层/二层… | 不限 | 中(积累即可) | 快节奏爽文 |
+| **武道体系** | 武徒/武者/武师… | 7-10个 | 中(需打磨肉身) | 武侠/玄幻 |
+| **魔法体系** | 1级/2级… | 7-9个 | 中(需冥想) | 西幻 |
+| **异能体系** | 觉醒/进化… | 6-8个 | 低(自然进化) | 科幻/末世 |
+
+---
+
+## 附录:经典境界设定案例分析
+
+### 案例 1:《凡人修仙传》- 修仙境界标杆
+**境界**:
+```
+练气(13层) → 筑基 → 结丹 → 元婴 → 化神 → 炼虚 → 合体 → 大乘 → 渡劫
+```
+
+**优点**:
+- 名称规范,易记
+- 每个境界都有明确描述
+- 突破难度递增
+
+---
+
+### 案例 2:《斗破苍穹》- 简化境界典范
+**境界**:
+```
+斗之气(1-10段) → 斗者 → 斗师 → 大斗师 → 斗灵 → 斗王 → 斗皇 → 斗宗 → 斗尊 → 斗圣 → 斗帝
+```
+
+**优点**:
+- 统一前缀(斗),易记
+- 小境界用"星级"划分(一星斗者、二星斗者)
+- 境界名称有递进感(者 → 师 → 王 → 皇 → 宗 → 尊 → 圣 → 帝)
+
+---
+
+### 案例 3: 反面教材(某扑街文)
+**问题**:
+```
+境界混乱:
+炼气期 → 灵力期(?) → 筑基期 → 元婴期(金丹期呢?) → 化神期 → 真仙期(?)
+```
+
+**问题分析**:
+- 跳过关键境界(金丹期)
+- 名称不规范(灵力期是什么鬼?)
+- 读者会困惑

+ 373 - 0
.claude/genres/xuanhuan/power-systems.md

@@ -0,0 +1,373 @@
+# 力量体系设计 (Power Systems Design)
+
+> **核心原则**: 力量体系是玄幻小说的骨架。好的力量体系 = 清晰的等级划分 + 合理的成长路径 + 足够的想象空间。
+
+---
+
+## 1. 力量体系的四大基础元素
+
+### 元素 1: 能量本质(What)
+**定义**: 这个世界的力量来源是什么?
+
+**常见类型**:
+| 类型 | 能量名称 | 代表作品 | 特点 |
+|------|---------|---------|------|
+| **灵气修炼** | 灵气/真气/元力 | 《凡人修仙传》 | 天地灵气,可吸收炼化 |
+| **魔法元素** | 魔力/元素之力 | 《斗罗大陆》 | 火/水/风/土等元素 |
+| **武道意志** | 气血/武道真意 | 《武动乾坤》 | 肉身力量+意志 |
+| **异能基因** | 基因能量/异能 | 《吞噬星空》 | 基因进化,科幻向 |
+| **信仰神力** | 信仰之力/神力 | 《盘龙》 | 信徒信仰凝聚 |
+| **混合体系** | 多种能量共存 | 《完美世界》 | 灵气+法则+血脉 |
+
+**设计要点**:
+```
+✅ 能量本质要独特(避免"又是灵气修炼")
+✅ 能量获取方式要明确(吸收/转化/修炼)
+✅ 能量特性要有区分(攻击型/防御型/辅助型)
+```
+
+---
+
+### 元素 2: 境界等级(How High)
+**定义**: 从弱到强的划分标准。
+
+**经典九阶体系**:
+```
+第1阶(凡人期): 炼气/学徒/觉醒
+第2阶(入门期): 筑基/魔法师/武者
+第3阶(进阶期): 金丹/大魔法师/宗师
+第4阶(精英期): 元婴/魔导师/大宗师
+第5阶(顶尖期): 化神/圣魔导/武圣
+第6阶(传说期): 炼虚/传奇法师/武帝
+第7阶(神话期): 合体/半神/武神
+第8阶(准神期): 大乘/准神/神王
+第9阶(神级): 渡劫/真神/至尊
+```
+
+**命名技巧**:
+- **修仙向**: 炼气 → 筑基 → 金丹 → 元婴 → 化神 → 炼虚 → 合体 → 大乘 → 渡劫
+- **魔法向**: 学徒 → 魔法师 → 大魔法师 → 魔导师 → 圣魔导 → 传奇 → 半神 → 真神
+- **武道向**: 武徒 → 武者 → 武师 → 宗师 → 大宗师 → 武圣 → 武帝 → 武神 → 至尊
+- **异能向**: 觉醒 → 进化 → 变异 → 超凡 → 传说 → 神话 → 永恒
+
+**境界数量建议**:
+- **短篇(30万字以内)**: 5-7个大境界
+- **中篇(30-100万字)**: 7-9个大境界
+- **长篇(100万字以上)**: 9-12个大境界,可分多个大世界
+
+---
+
+### 元素 3: 能力表现(What Can Do)
+**定义**: 每个境界能做什么?有什么能力?
+
+**能力升级示例**:
+```markdown
+## 境界能力对照表
+
+| 境界 | 移动能力 | 攻击范围 | 破坏力 | 寿命 | 特殊能力 |
+|------|---------|---------|--------|------|---------|
+| **炼气期** | 地面奔跑 | 10米 | 碎石 | 100年 | 无 |
+| **筑基期** | 短距飞行 | 50米 | 碎墙 | 200年 | 简单法术 |
+| **金丹期** | 长距飞行 | 100米 | 毁屋 | 500年 | 神识探测 |
+| **元婴期** | 御空飞行 | 1公里 | 夷平小镇 | 1000年 | 元婴分身 |
+| **化神期** | 瞬移 | 10公里 | 毁城 | 2000年 | 法则初悟 |
+| **炼虚期** | 空间穿梭 | 百公里 | 毁国 | 5000年 | 法则掌控 |
+| **合体期** | 跨界传送 | 千公里 | 灭大陆 | 万年 | 法则融合 |
+| **大乘期** | 自由穿梭 | 无限 | 碎星辰 | 十万年 | 创造法则 |
+| **渡劫期** | 时空掌控 | 无限 | 毁星域 | 不死 | 开天辟地 |
+```
+
+**设计要点**:
+```
+✅ 每个境界都要有"质变"(不能只是"更强")
+✅ 能力要视觉化(读者能想象出画面)
+✅ 避免过早"爆炸"(第3境界就毁天灭地)
+```
+
+---
+
+### 元素 4: 突破条件(How to Advance)
+**定义**: 如何从低境界突破到高境界?
+
+**常见突破方式**:
+1. **资源堆积型**: 需要丹药/灵石/天材地宝
+2. **顿悟型**: 需要感悟法则/道的本质
+3. **试炼型**: 需要通过天劫/试炼场/生死战
+4. **传承型**: 需要获得功法/血脉/传承
+5. **混合型**: 以上多种方式结合
+
+**示例(金丹期 → 元婴期)**:
+```markdown
+## 突破条件清单(缺一不可)
+1. **修为条件**: 金丹期大圆满(灵力饱和)
+2. **资源需求**: 破婴丹 × 1,灵石 × 10万
+3. **感悟要求**: 领悟一种法则(火/水/风/土/雷等)
+4. **天劫考验**: 渡三九雷劫(27道雷霆)
+5. **成功率**: 10-30%(天赋越高成功率越高)
+
+## 突破过程
+```
+第1步: 服用破婴丹,灵力暴涨
+第2步: 金丹开始分裂,孕育元婴
+第3步: 天劫降临,雷霆洗礼
+第4步: 元婴成形,破丹而出
+第5步: 突破成功,境界稳固
+```
+
+## 突破失败后果
+- 轻则境界跌落(金丹后期 → 金丹初期)
+- 重则金丹破碎,沦为废人
+- 极重则被雷劫劈死
+```
+
+---
+
+## 2. 力量体系的三大进阶设计
+
+### 进阶 1: 小境界划分
+**目的**: 让读者感受"持续进步"。
+
+**划分方式**:
+```
+每个大境界分为:初期 → 中期 → 后期 → 大圆满
+或者:一层 → 二层 → ... → 九层
+```
+
+**示例**:
+```
+金丹期 = 金丹初期 + 金丹中期 + 金丹后期 + 金丹大圆满
+每个小境界之间也有实力差距(约1.5-2倍)
+```
+
+**字数分配建议**:
+- 大境界突破:5-10万字一次
+- 小境界突破:1-2万字一次
+
+---
+
+### 进阶 2: 战力倍增系统
+**目的**: 让主角"越级挑战"合理化。
+
+**常见倍增设计**:
+| 倍增因素 | 倍数 | 示例 |
+|---------|------|------|
+| **功法品级** | 1-10倍 | 天级功法 vs 凡级功法 |
+| **武技品级** | 1-5倍 | 绝学 vs 普通招式 |
+| **法宝神兵** | 1-3倍 | 神器 vs 凡器 |
+| **血脉天赋** | 1-5倍 | 神兽血脉 vs 凡人血脉 |
+| **秘法爆发** | 2-10倍 | 燃烧精血/禁术 |
+| **阵法加成** | 1-10倍 | 困杀大阵 |
+
+**计算公式**:
+```
+实际战力 = 境界基础战力 × 功法倍数 × 武技倍数 × 法宝倍数 × 血脉倍数
+
+示例:
+- 叶良辰(金丹初期): 100战力(基础)× 1.5(玄级功法)= 150战力
+- 林天(筑基大圆满): 50战力(基础)× 3(天级功法)× 2(绝学)× 2(神器)= 600战力
+→ 林天以筑基期越级挑战金丹期!
+```
+
+---
+
+### 进阶 3: 隐藏境界/特殊路线
+**目的**: 增加体系深度和悬念。
+
+**常见设计**:
+1. **隐藏大境界**: 在已知最高境界之上,还有"真正的最高境界"
+   - 示例:原以为"渡劫期"是最高 → 实际上还有"仙人/神级"
+2. **特殊修炼路线**: 主角走的是"不同的路"
+   - 示例:别人修灵气,主角修"吞噬"
+3. **伪境界陷阱**: 某些境界是"歧路"
+   - 示例:走错路会进入"魔道",无法再进步
+
+---
+
+## 3. 力量体系的世界观融合
+
+### 融合点 1: 地图与境界对应
+**设计逻辑**: 不同境界对应不同地图。
+
+**示例**:
+```markdown
+## 世界地图分层
+**第1层世界(低级大陆)**: 炼气-金丹期
+- 灵气稀薄,最高只能修炼到金丹期
+- 代表:青玄大陆
+
+**第2层世界(中级大陆)**: 元婴-化神期
+- 灵气浓郁,可修炼到化神期
+- 代表:天元大陆
+
+**第3层世界(高级大陆)**: 炼虚-合体期
+- 灵气充沛,可修炼到合体期
+- 代表:圣界
+
+**第4层世界(神域)**: 大乘-渡劫期
+- 仙灵之气,可修炼到渡劫期乃至成仙
+- 代表:仙界
+```
+
+**好处**:
+- 主角从低级大陆开始,逐步"飞升"到更高大陆
+- 制造"井底之蛙"反转:主角在低级大陆无敌 → 到高级大陆发现高手如云
+
+---
+
+### 融合点 2: 势力与境界门槛
+**设计逻辑**: 加入势力需要达到特定境界。
+
+**示例**:
+```markdown
+## 势力等级表
+| 势力等级 | 最低境界要求 | 代表势力 |
+|---------|-------------|---------|
+| **凡俗势力** | 无要求 | 王国、家族 |
+| **三流宗门** | 筑基期 | 青云宗 |
+| **二流宗门** | 金丹期 | 天剑宗 |
+| **一流宗门** | 元婴期 | 太玄宗 |
+| **圣地** | 化神期 | 五大圣地 |
+| **隐世家族** | 炼虚期 | 古族 |
+| **神秘组织** | 合体期 | 天机阁 |
+```
+
+---
+
+### 融合点 3: 寿命与时间跨度
+**设计逻辑**: 境界越高,寿命越长,故事时间跨度越大。
+
+**示例**:
+```
+炼气期:寿命100年 → 故事时间线:10-20年
+金丹期:寿命500年 → 故事时间线:50-100年
+化神期:寿命2000年 → 故事时间线:数百年
+大乘期:寿命十万年 → 故事时间线:数千上万年
+```
+
+**用途**:
+- 制造"岁月沧桑感"(主角修炼千年,故人已逝)
+- 合理化"闭关"(闭关百年突破)
+
+---
+
+## 4. 力量体系的常见问题与解决
+
+### 问题 1: 体系崩溃(前后矛盾)
+**现象**:
+```
+前期:金丹期可以"毁城"
+后期:元婴期打架只能"碎石"
+```
+
+**原因**: 作者忘记了前期设定,或者为了压制战斗规模而降低描写。
+
+**解决方案**:
+- **设定手册**: 记录每个境界的能力上限
+- **环境限制**: 在特殊场景(如宗门内部)禁止大规模破坏
+- **法则压制**: 高级大陆的"天道规则"更强,同境界威力被削弱
+
+---
+
+### 问题 2: 境界通胀(升级太快)
+**现象**:
+```
+第1卷:炼气期 → 筑基期(10万字)
+第2卷:筑基期 → 金丹期(10万字)
+第10卷:已经渡劫飞升了,没东西可写
+```
+
+**解决方案**:
+- **延缓突破**: 每个大境界至少20-30万字
+- **小境界填充**: 金丹初期 → 中期 → 后期 → 大圆满(每个5万字)
+- **横向拓展**: 不升境界,但提升战技/法宝/血脉
+
+---
+
+### 问题 3: 主角无敌综合症
+**现象**:
+```
+主角永远比敌人强,没有悬念
+```
+
+**解决方案**:
+- **隐藏高手**: 始终有"更强的人"存在
+- **特殊限制**: 主角虽强,但有某些弱点(如不能杀人/不能用某种力量)
+- **团队作战**: 主角需要队友配合才能赢
+
+---
+
+## 5. 力量体系设计自检清单
+
+完成体系设计后,检查以下项目:
+
+- [ ] **能量本质明确**: 能量是什么?从哪来?
+- [ ] **境界等级清晰**: 有多少个境界?每个境界叫什么?
+- [ ] **能力差异合理**: 每个境界的能力是否有"质变"?
+- [ ] **突破条件明确**: 如何突破?需要什么资源/条件?
+- [ ] **小境界划分**: 是否有初期/中期/后期?
+- [ ] **战力倍增系统**: 主角凭什么越级挑战?
+- [ ] **世界观融合**: 境界与地图/势力/寿命是否对应?
+- [ ] **前后一致性**: 设定是否有矛盾?
+- [ ] **成长空间**: 是否有足够的升级空间撑到完本?
+
+---
+
+## 🛠️ 力量体系速查表
+
+| 体系类型 | 能量本质 | 境界数量 | 升级方式 | 适用题材 |
+|---------|---------|---------|---------|---------|
+| **修仙体系** | 灵气 | 9-12阶 | 资源+顿悟+天劫 | 仙侠/修真 |
+| **魔法体系** | 魔力/元素 | 7-9阶 | 冥想+施法练习 | 西幻/魔法 |
+| **武道体系** | 气血/真气 | 7-10阶 | 肉身锤炼+武技 | 武侠/东方玄幻 |
+| **异能体系** | 基因/异能 | 6-8阶 | 基因进化+觉醒 | 科幻/末世 |
+| **信仰体系** | 神力/信仰 | 8-10阶 | 收集信徒+点燃神火 | 神话/史诗 |
+
+---
+
+## 附录:经典力量体系案例分析
+
+### 案例 1:《凡人修仙传》- 修仙体系标杆
+**体系**:
+```
+练气期(13层) → 筑基期 → 结丹期 → 元婴期 → 化神期 → 炼虚期 → 合体期 → 大乘期 → 渡劫期
+```
+
+**优点**:
+- 境界清晰,每个境界都有明确能力
+- 突破条件合理(天劫、丹药、感悟)
+- 世界地图分层(人界 → 灵界 → 仙界)
+
+**缺点**:
+- 后期境界通胀(大乘期之后难以为继)
+
+---
+
+### 案例 2:《斗破苍穹》- 简化体系典范
+**体系**:
+```
+斗之气(1-10段) → 斗者 → 斗师 → 大斗师 → 斗灵 → 斗王 → 斗皇 → 斗宗 → 斗尊 → 斗圣 → 斗帝
+```
+
+**优点**:
+- 命名简洁易记(都是"斗"字辈)
+- 小境界划分清晰(星级)
+- 战力倍增系统完善(斗技、异火、血脉)
+
+---
+
+### 案例 3: 反面教材(某扑街文)
+**问题**:
+```
+境界名称混乱:
+第1境界:炼气期
+第2境界:灵力期(???和炼气期有什么区别?)
+第3境界:筑基期
+第4境界:元婴期(???金丹期呢?)
+第5境界:化神期
+...
+```
+
+**问题分析**:
+- 境界命名不规范
+- 跳过关键境界(金丹期)
+- 读者会困惑

+ 672 - 0
.claude/genres/xuanhuan/xuanhuan-cool-points.md

@@ -0,0 +1,672 @@
+# 玄幻小说爽点设计 (Xuanhuan Cool Points Design)
+
+> **核心原则**: 玄幻爽点 = 力量碾压 + 打脸反转 + 装逼展示。读者要的是"主角牛逼"的快感。
+
+---
+
+## 1. 玄幻爽点的五大类型
+
+### 类型 1: 战力碾压(最直接)
+**定义**: 主角实力远超敌人,一招秒杀
+
+**示例**:
+```
+叶良辰(金丹初期):"就凭你这个筑基期?"
+
+林天淡淡一笑:"试试就知道了。"
+
+一剑斩出——
+
+叶良辰连反应都来不及,头颅飞起。
+
+全场震惊:"筑基期……秒杀金丹?!"
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+### 类型 2: 装逼打脸(最经典)
+**定义**: 敌人装逼 → 主角打脸
+
+**示例**:
+```
+【装逼】
+龙套:"就凭你这个废物?我一只手就能……"
+
+【打脸】
+话音未落,林天出手。
+一拳。
+龙套的头颅爆碎成血雾。
+
+林天收拳,淡淡道:"废话太多。"
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+### 类型 3: 越级挑战(最燃)
+**定义**: 低境界击败高境界
+
+**示例**:
+```
+"什么?!筑基期挑战金丹期?"
+"他疯了吗?!"
+
+战斗开始——
+
+林天剑气纵横,金色剑光如龙。
+叶良辰被压制,连连后退。
+
+"这……怎么可能……你明明只是筑基期……"
+
+"谁告诉你筑基期不能杀金丹的?"
+林天一剑刺穿叶良辰心脏。
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+### 类型 4: 震惊全场(最装逼)
+**定义**: 主角做出惊世之举,震惊所有人
+
+**示例**:
+```
+【炼丹大会】
+众人:"炼制三品丹药已是极限。"
+
+林天:"那我就炼四品。"
+
+全场哄笑:"哈哈哈,口气真大!"
+
+【结果】
+丹成,四道丹纹!
+
+"四品丹药?!"
+"还是极品!"
+"这……这怎么可能!"
+
+全场死寂,所有人目瞪口呆。
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+### 类型 5: 反转逆袭(最燃)
+**定义**: 主角被压制 → 突然爆发 → 反杀
+
+**示例**:
+```
+【被压制】
+林天被打得吐血,浑身是伤。
+叶良辰:"哈哈哈,废物就是废物!"
+
+【反转】
+就在这时,林天体内的封印解开。
+轰——
+恐怖的气息爆发!
+
+"什么?!"
+叶良辰脸色大变。
+
+林天眼中神光湛湛:"现在……该我了。"
+一拳轰出,叶良辰被打爆成血雾。
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+## 2. 玄幻小说的十大经典爽点桥段
+
+### 桥段 1: 拍卖会打脸
+**流程**:
+```
+龙套炫富 → 主角出价更高 → 龙套震惊 → 主角拿下宝物
+```
+
+**示例**:
+```
+拍卖师:"起拍价,十万灵石!"
+叶良辰:"我出二十万!"
+众人惊叹:"叶家果然有钱!"
+
+林天淡淡道:"一百万。"
+
+全场震惊!
+叶良辰脸色铁青:"你……你哪来这么多灵石?"
+
+林天:"不关你事。"
+```
+
+---
+
+### 桥段 2: 比武大会夺冠
+**流程**:
+```
+所有人看不起主角 → 主角一路碾压 → 夺冠 → 震惊全场
+```
+
+**示例**:
+```
+第一轮: 林天一剑秒杀对手。
+众人:"运气好而已。"
+
+第二轮: 林天又是一剑秒杀。
+众人:"……"
+
+决赛: 林天对战叶良辰。
+叶良辰:"我要让你知道……"
+话音未落,林天出剑。
+一剑,枭首!
+
+全场死寂。
+"冠军……是林天?!"
+```
+
+---
+
+### 桥段 3: 炼丹/炼器震惊全场
+**流程**:
+```
+众人嘲笑主角不会炼丹 → 主角炼出极品丹药 → 打脸
+```
+
+**示例**:
+```
+叶良辰:"你一个武夫,也配炼丹?"
+众人哄笑。
+
+林天不语,开始炼丹。
+一炷香后——
+丹成,五道丹纹!
+
+炼丹大师震惊:"五品丹药?!还是极品?!"
+全场傻眼。
+```
+
+---
+
+### 桥段 4: 秘境夺宝
+**流程**:
+```
+众人争夺宝物 → 主角黄雀在后 → 收走宝物
+```
+
+**示例**:
+```
+叶良辰和血神教打得两败俱伤。
+就在这时,林天出现。
+
+"谢谢你们帮我打扫战场。"
+林天收走宝物,转身就走。
+
+"该死!别跑!"
+但林天早已消失。
+```
+
+---
+
+### 桥段 5: 退婚打脸
+**流程**:
+```
+被退婚 → 主角崛起 → 前未婚妻后悔 → 主角不屑
+```
+
+**示例**:
+```
+三年后——
+叶倾城见到林天,已是元婴期强者。
+"林天,当年是我错了……我们能不能……"
+
+林天:"三年前,你说我配不上你。"
+"三年后,你配不上我。"
+
+转身离去,留下叶倾城愣在原地。
+```
+
+---
+
+### 桥段 6: 一招制敌
+**流程**:
+```
+敌人嚣张 → 主角一招秒杀 → 震惊全场
+```
+
+**示例**:
+```
+龙套:"就凭你……"
+
+话音未落,林天已经出手。
+一剑。
+龙套的头颅飞起。
+
+全场鸦雀无声。
+林天收剑:"我说过,废话太多。"
+```
+
+---
+
+### 桥段 7: 群攻碾压
+**流程**:
+```
+众人围攻主角 → 主角群体秒杀 → 无人敢动
+```
+
+**示例**:
+```
+数十人将林天围住。
+"今天,你插翅难飞!"
+
+林天冷笑:"是吗?"
+
+剑气爆发——
+千百道剑影斩出!
+
+惨叫声此起彼伏……
+片刻后,满地尸体,唯有林天一人站立。
+```
+
+---
+
+### 桥段 8: 一招破阵
+**流程**:
+```
+众人破不了阵法 → 主角一招破阵 → 震惊
+```
+
+**示例**:
+```
+长老:"这是上古禁制,无人能破。"
+
+林天:"让我试试。"
+他伸手按在禁制上,灵力涌入。
+
+咔嚓——
+禁制碎裂。
+
+全场震惊:"这……他怎么做到的?"
+```
+
+---
+
+### 桥段 9: 收美女
+**流程**:
+```
+美女遇险 → 主角英雄救美 → 美女芳心暗许
+```
+
+**示例**:
+```
+"救命!"
+苏倾城被血神教追杀。
+
+林天出现,一剑斩杀血神教众。
+"没事了。"
+
+苏倾城脸红:"谢谢你……"
+(内心:好强……好帅……)
+```
+
+---
+
+### 桥段 10: 吊打反派
+**流程**:
+```
+反派嚣张 → 主角压制 → 反派求饶 → 主角不屑
+```
+
+**示例**:
+```
+叶良辰:"我是叶家大少爷!你敢动我?"
+
+林天:"叶家?"
+一脚踩在叶良辰脸上。
+
+"在我眼里,叶家什么都不是。"
+```
+
+---
+
+## 3. 爽点强度分级
+
+### S级爽点(大高潮)
+**触发条件**:
+- 击杀BOSS
+- 境界大突破
+- 灭敌对势力
+- 收绝世美女
+
+**频率**: 每10万字1次
+
+**示例**:
+```
+林天一剑斩杀血神教主。
+"师尊,我为你报仇了……"
+
+天剑宗上下欢呼:"林天万岁!"
+```
+
+---
+
+### A级爽点(中高潮)
+**触发条件**:
+- 越级挑战成功
+- 获得神器
+- 打脸强敌
+
+**频率**: 每2万字1次
+
+**示例**:
+```
+林天以筑基期击败金丹期。
+全场震惊:"天才!绝世天才!"
+```
+
+---
+
+### B级爽点(小高潮)
+**触发条件**:
+- 秒杀龙套
+- 炼丹/炼器成功
+- 小突破
+
+**频率**: 每5000字1次
+
+**示例**:
+```
+林天一拳打爆龙套。
+"不堪一击。"
+```
+
+---
+
+### C级爽点(日常)
+**触发条件**:
+- 装逼一句话
+- 收获小宝物
+- 路人震惊
+
+**频率**: 每2000字1次
+
+**示例**:
+```
+林天:"区区金丹,何足挂齿?"
+路人:"好狂!"
+```
+
+---
+
+## 4. 爽点组合技(连击)
+
+### 组合 1: 装逼 → 打脸 → 秒杀
+```
+龙套:"就凭你?"
+林天:"试试就知道了。"
+一剑,枭首!
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+### 组合 2: 被压制 → 反转 → 碾压
+```
+林天被打得吐血。
+突然,封印解开!
+力量爆发,反杀敌人!
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+### 组合 3: 嘲笑 → 震惊 → 跪舔
+```
+众人嘲笑林天。
+林天展示实力。
+众人震惊,纷纷道歉。
+```
+
+**爽点强度**: ⭐⭐⭐⭐
+
+---
+
+### 组合 4: 挑衅 → 接受 → 碾压 → 震惊
+```
+叶良辰挑衅林天。
+林天接受挑战。
+战斗开始,林天碾压叶良辰。
+全场震惊:"怎么可能?!"
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐
+
+---
+
+## 5. 玄幻特色爽点(区别于都市/武侠)
+
+### 特色 1: 渡劫爽点
+```
+众人:"渡劫九死一生!"
+林天:"我来试试。"
+
+雷劫降临——
+林天不躲不避,硬抗雷劫!
+最后一道雷霆散去,林天毫发无伤。
+
+"他……他竟然硬抗天劫?!"
+```
+
+---
+
+### 特色 2: 炼丹爽点
+```
+众人:"炼三品丹药已是极限。"
+林天:"那我炼四品。"
+
+丹成,四道丹纹!
+全场震惊:"四品丹药?!"
+```
+
+---
+
+### 特色 3: 收妖兽爽点
+```
+众人:"这是上古凶兽,无人能收服!"
+林天:"让我试试。"
+
+一滴精血滴在凶兽额头。
+凶兽臣服:"主人!"
+
+全场傻眼。
+```
+
+---
+
+### 特色 4: 破禁制爽点
+```
+长老:"这是上古禁制,无解。"
+林天:"让我试试。"
+
+咔嚓——
+禁制碎裂。
+
+"这……怎么可能?"
+```
+
+---
+
+### 特色 5: 开神脉爽点
+```
+林天体内,九条神脉同时觉醒!
+轰——
+恐怖的气息席卷全场!
+
+"九脉齐开?!"
+"万古以来,从未有过!"
+```
+
+---
+
+## 6. 爽点密度控制
+
+### 密度标准
+- **每2000字**: 至少1个C级爽点(日常装逼)
+- **每5000字**: 至少1个B级爽点(秒杀/小突破)
+- **每2万字**: 至少1个A级爽点(越级挑战/获得神器)
+- **每10万字**: 至少1个S级爽点(击杀BOSS/灭势力)
+
+### 密度检查公式
+```
+爽点总数 = 章节字数 ÷ 2000
+```
+
+**示例**:
+```
+第10章(5000字)
+应有爽点数: 5000 ÷ 2000 = 2.5个 → 至少2个爽点
+
+实际爽点:
+1. 林天秒杀龙套(B级)
+2. 林天装逼一句话(C级)
+3. 路人震惊(C级)
+
+✅ 达标(3个爽点)
+```
+
+---
+
+## 7. 避免爽点疲劳
+
+### 问题: 爽点重复
+```
+第10章: 林天秒杀龙套
+第20章: 林天秒杀龙套
+第30章: 林天秒杀龙套
+```
+**读者**: "又是秒杀?没新意!"
+
+### 解决方案: 爽点多样化
+```
+第10章: 秒杀龙套(战力碾压)
+第20章: 炼丹震惊(技能展示)
+第30章: 破禁制(智慧体现)
+第40章: 收美女(情感线)
+第50章: 越级挑战(热血战斗)
+```
+
+---
+
+## 8. 爽点与虐点的配合
+
+### 黄金比例
+- **爽点**: 70%
+- **虐点**: 20%
+- **平淡**: 10%
+
+### 虐点 → 爽点的转换
+```
+【虐点】(压抑)
+林天被叶良辰羞辱。
+"废物就是废物!"
+
+【爽点】(爆发)
+三年后——
+林天一剑斩杀叶良辰。
+"谁才是废物?"
+```
+
+**效果**: 虐得越狠,爽得越爽。
+
+---
+
+## 9. 爽点设计自检清单
+
+- [ ] **频率够吗**: 是否每2000字有爽点?
+- [ ] **强度够吗**: 是否有S/A/B/C各级爽点分布?
+- [ ] **多样吗**: 是否避免了爽点重复?
+- [ ] **符合人设吗**: 爽点是否符合主角性格?
+- [ ] **推动剧情吗**: 爽点是否推动了剧情发展?
+
+---
+
+## 🛠️ 爽点速查表
+
+| 爽点类型 | 触发条件 | 强度 | 频率 | 示例 |
+|---------|---------|------|------|------|
+| **秒杀** | 碾压龙套 | B级 | 5000字/次 | 一剑枭首 |
+| **打脸** | 敌人装逼 | A级 | 2万字/次 | 退婚打脸 |
+| **越级** | 低境界挑战高境界 | A级 | 2万字/次 | 筑基杀金丹 |
+| **震惊** | 做惊世之举 | A级 | 2万字/次 | 炼四品丹 |
+| **反转** | 劣势 → 优势 | S级 | 10万字/次 | 封印解开 |
+
+---
+
+## 附录:经典爽点案例分析
+
+### 案例 1: 《斗破苍穹》三年之约
+**爽点类型**: 打脸 + 越级
+
+**流程**:
+```
+被退婚 → 立誓三年后打脸 → 三年苦修 → 赴约 → 战胜纳兰嫣然 → 打脸成功
+```
+
+**爽点强度**: ⭐⭐⭐⭐⭐(最经典)
+
+---
+
+### 案例 2: 《完美世界》石昊吃兽奶
+**爽点类型**: 反差萌 + 天赋碾压
+
+**流程**:
+```
+小孩喝兽奶 → 众人嘲笑 → 展示惊人天赋 → 震惊全场
+```
+
+**爽点强度**: ⭐⭐⭐⭐
+
+---
+
+### 案例 3: 反面教材(某扑街文)
+```
+第10章: 林天秒杀龙套A
+第20章: 林天秒杀龙套B
+第30章: 林天秒杀龙套C
+第40章: 林天秒杀龙套D
+……
+```
+
+**问题**: 爽点重复,毫无新意,读者审美疲劳。
+
+---
+
+## 爽点设计进阶技巧
+
+### 技巧 1: 爽点递进
+```
+第1次: 秒杀炼气期(小爽)
+第2次: 秒杀筑基期(中爽)
+第3次: 秒杀金丹期(大爽)
+```
+
+### 技巧 2: 爽点叠加
+```
+秒杀 + 震惊 + 收美女 = 超级爽点
+```
+
+### 技巧 3: 爽点反差
+```
+前一章: 被虐(虐点)
+这一章: 反杀(爽点)
+```
+
+### 技巧 4: 爽点悬念
+```
+第1章: 林天说"三年后见"
+第50章: 三年后,林天归来,碾压全场
+```

+ 547 - 0
.claude/genres/xuanhuan/xuanhuan-plot-patterns.md

@@ -0,0 +1,547 @@
+# 玄幻小说经典套路设计 (Xuanhuan Plot Patterns)
+
+> **核心原则**: 玄幻小说的套路是"熟悉感+创新感"的结合。读者喜欢熟悉的套路(有代入感),但也需要新意(不会审美疲劳)。
+
+---
+
+## 1. 玄幻小说的十大经典开局
+
+### 开局 1: 废柴流(最经典)
+**模板**:
+```
+主角原本是天才 → 因某事变成废柴 → 被退婚/羞辱 → 获得金手指 → 逆袭
+```
+
+**代表作**: 《斗破苍穹》萧炎
+
+**爽点**: 前期憋屈 → 后期打脸
+
+**变体**:
+- 穿越后发现自己是废柴
+- 武功被废,从头再来
+- 天生废脉,但有隐藏体质
+
+**示例**:
+```
+林天原本是天剑宗第一天才,但三年前修炼时走火入魔,经脉尽断,沦为废人。
+
+未婚妻叶倾城退婚:"林天,你已经是个废物了,我叶家不会要你这种拖累。"
+
+就在林天绝望之际,一道神秘声音响起:"检测到宿主,绑定【诸天万界签到系统】……"
+```
+
+---
+
+### 开局 2: 穿越流(最自由)
+**模板**:
+```
+现代人穿越到玄幻世界 → 带着现代知识/系统 → 逆天改命
+```
+
+**代表作**: 《斗罗大陆》唐三
+
+**爽点**: 现代知识碾压古代认知
+
+**变体**:
+- 穿越成反派
+- 穿越成妖兽
+- 穿越成NPC
+
+**示例**:
+```
+"我……穿越了?"
+林天看着镜中陌生的脸,心中震惊。
+这是一个修仙的世界,而自己穿越成了一个小家族的废柴少爷。
+"不过,我有前世的记忆!我知道这个世界的所有机缘!"
+```
+
+---
+
+### 开局 3: 重生流(最爽)
+**模板**:
+```
+主角前世是强者 → 被背叛/陨落 → 重生回到年轻时 → 重走修仙路
+```
+
+**代表作**: 《修罗武神》楚枫
+
+**爽点**: 先知先觉,提前布局
+
+**变体**:
+- 带着记忆重生
+- 带着前世功法重生
+- 重生到敌人身上
+
+**示例**:
+```
+"我……我重生了?!"
+林天睁开眼,发现自己回到了十五岁那年。
+前世,他被叶良辰陷害,惨死在血神教手中。
+这一世,他要改变一切!
+"叶良辰,等着吧……这一次,死的是你!"
+```
+
+---
+
+### 开局 4: 退婚流(爽点集中)
+**模板**:
+```
+主角被退婚 → 当场立誓 → 三年/五年后打脸 → 前未婚妻后悔
+```
+
+**代表作**: 《斗破苍穹》三年之约
+
+**爽点**: 打脸前未婚妻
+
+**变体**:
+- 主角主动退婚
+- 多次被退婚
+- 退婚后发现未婚妻是天骄
+
+**示例**:
+```
+"林天,你我之间的婚约,从今日起作废!"
+叶倾城当众撕毁婚书。
+全场哄堂大笑:"哈哈哈,废物配不上叶家大小姐!"
+
+林天抬起头,眼中闪过一丝冷意:
+"三年之后,我会让你后悔今天的决定。"
+```
+
+---
+
+### 开局 5: 系统流(最万能)
+**模板**:
+```
+主角绑定系统 → 完成任务获得奖励 → 快速变强
+```
+
+**代表作**: 《万古神帝》张若尘
+
+**爽点**: 挂机升级,任务刷宝
+
+**常见系统类型**:
+| 系统类型 | 功能 | 代表 |
+|---------|------|------|
+| **签到系统** | 每日签到获奖励 | 诸天签到 |
+| **商城系统** | 用积分兑换宝物 | 万界商城 |
+| **任务系统** | 完成任务升级 | 无限任务 |
+| **抽奖系统** | 抽奖获得神器 | 超级抽奖 |
+| **吞噬系统** | 吞噬敌人变强 | 吞天噬地 |
+
+**示例**:
+```
+【叮!检测到宿主,绑定成功!】
+【新手大礼包已发放:《九转玄功》×1、《灭世剑诀》×1、破境丹×10】
+林天:"系统,我爱你!"
+```
+
+---
+
+### 开局 6: 夺舍流(黑暗向)
+**模板**:
+```
+强者临死夺舍 → 占据年轻人身体 → 带着前世记忆崛起
+```
+
+**代表作**: 《仙逆》王林
+
+**爽点**: 老怪物+年轻身体=无敌
+
+**变体**:
+- 被夺舍但反杀
+- 主动夺舍
+- 双魂共存
+
+**示例**:
+```
+"什么?我占据了这具身体?"
+血神老祖的神魂融入林天体内。
+"哈哈哈!虽然修为尽失,但我有万年记忆!"
+"这一世,我要重回巅峰!"
+```
+
+---
+
+### 开局 7: 天选之子流(最霸道)
+**模板**:
+```
+主角一出生就是天骄 → 拥有顶级血脉/体质 → 碾压同代
+```
+
+**代表作**: 《完美世界》石昊
+
+**爽点**: 天赋碾压,从小就强
+
+**常见天赋**:
+- 双瞳
+- 圣体
+- 神王血脉
+- 先天道胎
+
+**示例**:
+```
+"此子一出生,天降异象,必成大器!"
+长老们惊叹。
+林天睁开眼,瞳孔深处闪烁着神秘符文。
+【先天圣体道胎】——万古唯一的体质!
+```
+
+---
+
+### 开局 8: 捡宝流(最轻松)
+**模板**:
+```
+主角偶然捡到神器/秘籍 → 快速变强 → 崛起
+```
+
+**代表作**: 《凡人修仙传》韩立捡到小瓶
+
+**爽点**: 运气好,捡到宝
+
+**常见宝物**:
+- 古老戒指(藏有老爷爷)
+- 残破秘籍
+- 神秘石头(蕴含神兽)
+- 上古传承
+
+**示例**:
+```
+林天在垃圾堆里捡到一枚破旧的戒指。
+突然,戒指光芒大盛!
+一个老者虚影出现:"小子,你就是我等了千年的传人!"
+```
+
+---
+
+### 开局 9: 被陷害流(虐点开局)
+**模板**:
+```
+主角被陷害 → 被逐出宗门/家族 → 复仇崛起
+```
+
+**代表作**: 《武动乾坤》林动
+
+**爽点**: 复仇打脸
+
+**常见陷害方式**:
+- 被诬陷偷盗
+- 被栽赃杀人
+- 被诬陷勾结魔道
+
+**示例**:
+```
+"林天,你勾结魔道,罪不可赦!"
+宗主怒喝,剥夺林天的弟子身份。
+林天:"我没有!这是陷害!"
+"哼,证据确凿,还敢狡辩!"
+
+(内心:等着吧,总有一天我会证明清白,并报今日之仇!)
+```
+
+---
+
+### 开局 10: 双修流(成人向)
+**模板**:
+```
+主角修炼特殊功法 → 需要双修 → 收集美女
+```
+
+**代表作**: 某些成人向玄幻
+
+**爽点**: 后宫+修炼
+
+**注意**: 此类开局尺度较大,需控制描写。
+
+---
+
+## 2. 玄幻小说的五大主线模板
+
+### 主线 A: 复仇线(最燃)
+**流程**:
+```
+家破人亡 → 隐忍修炼 → 逐一击败仇人 → 最终大boss
+```
+
+**里程碑**:
+1. 第1卷: 灭门惨案,主角逃生
+2. 第2卷: 拜师学艺,初露锋芒
+3. 第3卷: 击杀第一个仇人
+4. 第4卷: 铲除中层仇敌
+5. 第5卷: 最终决战,灭杀大boss
+
+---
+
+### 主线 B: 争霸线(最宏大)
+**流程**:
+```
+建立势力 → 扩张地盘 → 统一大陆 → 飞升上界
+```
+
+**里程碑**:
+1. 第1卷: 创建宗门/势力
+2. 第2卷: 收服第一批势力
+3. 第3卷: 统一一方
+4. 第4卷: 大陆争霸
+5. 第5卷: 飞升上界,新篇章
+
+---
+
+### 主线 C: 寻宝线(最自由)
+**流程**:
+```
+探索秘境 → 获得机缘 → 提升实力 → 下一个秘境
+```
+
+**里程碑**:
+- 每20-30章开启一个新秘境
+- 秘境类型: 上古遗迹、秘密洞府、试炼之地、禁地
+
+---
+
+### 主线 D: 守护线(最温情)
+**流程**:
+```
+守护家人/爱人 → 对抗强敌 → 保护弱小 → 成为守护神
+```
+
+**里程碑**:
+1. 第1卷: 家人遇险
+2. 第2卷: 守护第一次成功
+3. 第3卷: 面对更强敌人
+4. 第4卷: 成为一方守护者
+5. 第5卷: 守护整个世界
+
+---
+
+### 主线 E: 成神线(最宏大)
+**流程**:
+```
+凡人 → 修仙者 → 仙人 → 神灵 → 至高神
+```
+
+**里程碑**:
+- 每个大境界是一个篇章
+- 每次境界突破都是高潮
+
+---
+
+## 3. 玄幻小说的六大副线套路
+
+### 副线 1: 红颜线(后宫)
+**套路**:
+```
+遇到美女 → 误会/冲突 → 化解误会 → 并肩作战 → 情愫暗生 → 收入后宫
+```
+
+**常见桥段**:
+- 英雄救美
+- 意外亲密接触
+- 共同战斗
+- 生死相依
+
+---
+
+### 副线 2: 兄弟线(情谊)
+**套路**:
+```
+遇到兄弟 → 共患难 → 生死与共 → 兄弟被害 → 主角复仇
+```
+
+**常见桥段**:
+- 结拜兄弟
+- 兄弟牺牲自己救主角
+- 主角为兄弟报仇
+
+---
+
+### 副线 3: 师徒线(传承)
+**套路**:
+```
+拜师 → 师尊传授绝学 → 师尊遇害 → 主角复仇/继承遗志
+```
+
+**常见桥段**:
+- 师尊牺牲保护主角
+- 主角为师尊报仇
+- 继承师尊的衣钵
+
+---
+
+### 副线 4: 势力线(建立王朝)
+**套路**:
+```
+创建势力 → 招募人才 → 扩张地盘 → 成为一方霸主
+```
+
+**常见桥段**:
+- 收服天才
+- 灭敌对势力
+- 建立联盟
+
+---
+
+### 副线 5: 寻宝线(机缘)
+**套路**:
+```
+发现秘境 → 闯关 → 获得宝物/传承 → 实力大增
+```
+
+**常见桥段**:
+- 上古遗迹
+- 秘密洞府
+- 试炼之地
+
+---
+
+### 副线 6: 宠物线(灵兽)
+**套路**:
+```
+收服妖兽 → 培养 → 妖兽进化 → 成为战力
+```
+
+**常见桥段**:
+- 收服幼兽
+- 妖兽渡劫
+- 妖兽化形
+
+---
+
+## 4. 玄幻小说的情节节奏控制
+
+### 节奏模板(以100万字为例)
+```
+前10万字(开局): 建立世界观、主角崛起
+10-30万字(第一卷): 第一次大冲突、初步复仇
+30-50万字(第二卷): 进入更高层次世界
+50-70万字(第三卷): 主线推进、收后宫
+70-90万字(第四卷): 面对终极boss前的磨炼
+90-100万字(大结局): 最终决战、飞升/成神
+```
+
+### 爽点密度
+- 每5000字至少1个小爽点(打脸/获得宝物/突破)
+- 每2万字至少1个中爽点(击败强敌/收美女)
+- 每10万字至少1个大爽点(灭敌对势力/境界大突破)
+
+---
+
+## 5. 常见套路组合(爆款公式)
+
+### 组合 1: 废柴流 + 退婚 + 系统
+```
+主角是废柴 → 被退婚 → 绑定系统 → 逆袭打脸
+```
+**代表**: 《斗破苍穹》变体
+
+---
+
+### 组合 2: 穿越 + 重生 + 先知
+```
+现代人穿越 → 重生到过去 → 利用先知布局
+```
+**代表**: 《修罗武神》类型
+
+---
+
+### 组合 3: 天才 + 被陷害 + 复仇
+```
+主角是天才 → 被陷害沦为废人 → 复仇崛起
+```
+**代表**: 《武动乾坤》类型
+
+---
+
+### 组合 4: 捡宝 + 老爷爷 + 双修
+```
+捡到戒指 → 戒指里有老爷爷 → 传授双修功法
+```
+**代表**: 成人向玄幻
+
+---
+
+### 组合 5: 系统 + 签到 + 后宫
+```
+绑定签到系统 → 签到获得神器 → 收集美女
+```
+**代表**: 爽文快餐
+
+---
+
+## 6. 避免套路陈旧的技巧
+
+### 技巧 1: 反转套路
+```
+传统: 主角被退婚 → 逆袭打脸
+反转: 主角主动退婚 → 前未婚妻后悔
+```
+
+### 技巧 2: 融合创新
+```
+传统: 单一系统
+创新: 双系统(修仙系统+科技系统)
+```
+
+### 技巧 3: 人设创新
+```
+传统: 热血主角
+创新: 苟道主角(极度谨慎,不冒险)
+```
+
+### 技巧 4: 世界观创新
+```
+传统: 单一修仙世界
+创新: 多元宇宙(修仙+科技+魔法)
+```
+
+---
+
+## 7. 套路设计自检清单
+
+- [ ] **开局有吸引力**: 前3章能否抓住读者?
+- [ ] **主线明确**: 主角的最终目标是什么?
+- [ ] **爽点密集**: 是否每5000字有小爽点?
+- [ ] **套路有新意**: 是否在传统套路上做了创新?
+- [ ] **副线丰富**: 是否有后宫/兄弟/师徒等副线?
+- [ ] **节奏合理**: 是否有张有弛,不拖沓?
+
+---
+
+## 🛠️ 套路速查表
+
+| 套路类型 | 开局 | 主线 | 爽点 | 代表作 |
+|---------|------|------|------|-------|
+| **废柴流** | 天才变废柴 | 复仇/逆袭 | 打脸 | 斗破苍穹 |
+| **穿越流** | 穿越异界 | 争霸/成神 | 碾压 | 斗罗大陆 |
+| **重生流** | 重生回到过去 | 改命/复仇 | 先知先觉 | 修罗武神 |
+| **系统流** | 绑定系统 | 任务/签到 | 刷宝 | 万古神帝 |
+| **天才流** | 天选之子 | 争霸/成神 | 碾压 | 完美世界 |
+
+---
+
+## 附录:套路创新案例
+
+### 案例 1: 《诡秘之主》的创新
+**传统套路**: 穿越 + 金手指
+**创新点**:
+- 克苏鲁风世界观
+- 命运途径体系
+- 群像戏(不只写主角)
+
+---
+
+### 案例 2: 《一世之尊》的创新
+**传统套路**: 重生 + 复仇
+**创新点**:
+- 主角不是废柴,而是前世至尊
+- 重生后低调发育
+- 武侠+仙侠+诸天融合
+
+---
+
+### 案例 3: 反面教材(某扑街文)
+**问题**:
+```
+主角被退婚 → 绑定系统 → 打脸 → 又被退婚 → 又打脸 → 无限循环
+```
+**问题**: 套路重复,毫无新意。

+ 190 - 0
.claude/genres/zhihu-short/character-quick-build.md

@@ -0,0 +1,190 @@
+# 知乎短篇快速人设构建
+
+> 短篇没有时间慢慢塑造人物。本文档提供快速建立鲜明人设的方法。
+
+---
+
+## 一、核心原则
+
+### 短篇人设三要素
+| 要素 | 作用 | 建立方式 |
+|------|------|---------|
+| 标签 | 快速识别 | 一句话定位 |
+| 反差 | 制造记忆点 | 表里不一 |
+| 欲望 | 驱动剧情 | 明确目标 |
+
+### 人设经济学
+```
+长篇:可以用10万字塑造一个复杂人物
+短篇:必须用500字让读者记住这个人
+```
+
+---
+
+## 二、一句话人设模板
+
+### 基础公式
+```
+[身份] + [反差特质] + [核心欲望]
+```
+
+### 示例
+| 类型 | 一句话人设 |
+|------|-----------|
+| 女主 | 豪门千金假装穷亲戚,只为报复当年抛弃她的生母 |
+| 男主 | 冷面总裁对所有人都冷漠,唯独对前妻卑微到尘埃 |
+| 反派 | 温柔贤惠的白月光,背地里是心机最深的绿茶 |
+| 配角 | 看似八卦的闺蜜,关键时刻永远站在女主这边 |
+
+---
+
+## 三、快速人设工具箱
+
+### 1. 身份标签库
+```
+女主:
+- 豪门:真千金/假千金/落魄千金/私生女
+- 职场:女总裁/小职员/设计师/医生/律师
+- 特殊:孤儿/重生者/穿书者/替身
+
+男主:
+- 豪门:霸总/太子爷/私生子/落魄少爷
+- 职场:CEO/医生/律师/军人/警察
+- 特殊:病娇/禁欲系/奶狗/狼狗
+```
+
+### 2. 反差特质库
+```
+外冷内热:表面冷漠,实则深情
+外热内冷:表面热情,实则算计
+强者示弱:明明很强,装作很弱
+弱者逞强:明明很弱,硬撑着强
+黑化反转:原本善良,被逼黑化
+洗白反转:原本反派,揭示苦衷
+```
+
+### 3. 核心欲望库
+```
+复仇:报复伤害过自己的人
+证明:证明自己的价值
+守护:保护重要的人
+逃离:摆脱某种困境
+获得:得到某样东西(爱/权/钱)
+救赎:弥补过去的错误
+```
+
+---
+
+## 四、人设速写技巧
+
+### 1. 行动定义法
+```
+不要说"她很坚强"
+而是写"被推下楼的瞬间,她抓住了栏杆"
+
+不要说"他很深情"
+而是写"她的遗物,他保存了整整十年"
+```
+
+### 2. 对话定义法
+```
+不要说"她很毒舌"
+而是写对话:
+"你以为你是谁?"
+"你未来的嫂子。"
+```
+
+### 3. 细节定义法
+```
+不要说"他很有钱"
+而是写"他随手给的小费,是我一个月工资"
+
+不要说"她很惨"
+而是写"冰箱里只有一颗发芽的土豆"
+```
+
+---
+
+## 五、常用人设模板
+
+### 女主模板
+
+#### A. 隐忍觉醒型
+```
+前期:善良隐忍,被欺负不反抗
+转折:某个事件触发觉醒
+后期:手撕渣男贱女,强势反击
+适用:虐文转爽文
+```
+
+#### B. 扮猪吃虎型
+```
+表面:人畜无害,任人欺负
+实际:深藏不露,步步为营
+揭示:关键时刻露出獠牙
+适用:爽文、复仇文
+```
+
+#### C. 清醒独立型
+```
+特点:从头到尾都很清醒
+行为:不恋爱脑,不圣母
+魅力:理性决策,果断行动
+适用:大女主文
+```
+
+### 男主模板
+
+#### A. 追妻火葬场型
+```
+前期:冷漠、误会、伤害女主
+转折:发现真相/失去女主
+后期:卑微追妻,疯狂弥补
+适用:虐文、追妻文
+```
+
+#### B. 隐藏深情型
+```
+表面:冷漠疏离,不近人情
+实际:默默守护,深情至极
+揭示:通过他人视角或回忆揭示
+适用:甜虐文
+```
+
+#### C. 病娇占有型
+```
+特点:对女主有强烈占有欲
+表现:嫉妒、控制、不择手段
+底线:不会真正伤害女主
+适用:病娇文、禁忌文
+```
+
+---
+
+## 六、配角速写
+
+### 反派配角
+```
+绿茶白月光:表面清纯,实则心机
+恶毒女配:明着使坏,智商在线
+渣男前任:始乱终弃,后悔莫及
+恶婆婆:偏心眼,看不起女主
+```
+
+### 助攻配角
+```
+毒舌闺蜜:嘴毒心善,关键时刻靠谱
+腹黑助理:看穿一切,默默帮忙
+暖心备胎:深情守护,成全女主
+觉醒家人:从误解到支持
+```
+
+---
+
+## 七、人设检查清单
+
+- [ ] 能否用一句话概括这个人物?
+- [ ] 人物是否有明确的欲望/目标?
+- [ ] 人物是否有记忆点(反差/特质)?
+- [ ] 人物行为是否符合设定?
+- [ ] 读者是否能快速区分不同人物?

+ 236 - 0
.claude/genres/zhihu-short/emotional-peaks.md

@@ -0,0 +1,236 @@
+# 知乎短篇情绪高潮设计
+
+> 情绪高潮是短篇的灵魂。本文档提供设计和执行情绪高潮的方法。
+
+---
+
+## 一、情绪高潮类型
+
+| 类型 | 情绪 | 触发方式 | 适用场景 |
+|------|------|---------|---------|
+| 爽点高潮 | 爽快 | 打脸/反转/复仇 | 爽文、复仇文 |
+| 虐点高潮 | 心痛 | 误会/背叛/失去 | 虐文、悲剧 |
+| 甜点高潮 | 心动 | 告白/守护/宠溺 | 甜文、言情 |
+| 燃点高潮 | 热血 | 逆袭/觉醒/反击 | 大女主、爽文 |
+| 泪点高潮 | 感动 | 牺牲/真相/重逢 | 治愈、亲情 |
+
+---
+
+## 二、高潮设计公式
+
+### 基础公式
+```
+高潮 = 铺垫(憋) + 触发(爆) + 释放(爽/虐/甜)
+```
+
+### 铺垫技巧
+```
+1. 情绪积累:让读者和主角一起"憋"
+2. 信息差:读者知道真相,主角不知道
+3. 期待建立:暗示即将发生的事
+4. 障碍堆叠:一个接一个的困难
+```
+
+### 触发技巧
+```
+1. 关键台词:一句话点燃情绪
+2. 关键动作:一个动作改变局面
+3. 真相揭露:隐藏信息曝光
+4. 身份反转:真实身份揭示
+```
+
+---
+
+## 三、爽点高潮设计
+
+### 打脸爽点
+```
+铺垫:被看不起/被欺负/被误解
+触发:真实身份/实力曝光
+释放:围观者震惊,打脸者后悔
+
+示例:
+铺垫:"一个穷亲戚也配坐主桌?"
+触发:总裁亲自来接她
+释放:"这是我未婚妻。"全场寂静。
+```
+
+### 复仇爽点
+```
+铺垫:被伤害的过程(让读者愤怒)
+触发:复仇计划启动
+释放:仇人下场(要惨,要解气)
+
+示例:
+铺垫:她被陷害入狱三年
+触发:出狱后一步步布局
+释放:仇人跪在她面前求饶,她冷笑转身
+```
+
+### 反转爽点
+```
+铺垫:所有人都以为A
+触发:真相揭露是B
+释放:认知颠覆的震撼
+
+示例:
+铺垫:所有人都以为她是小三
+触发:结婚证曝光
+释放:原来正室是她,那个才是小三
+```
+
+---
+
+## 四、虐点高潮设计
+
+### 误会虐点
+```
+铺垫:建立深厚感情
+触发:误会产生(要合理)
+释放:心碎的反应
+
+示例:
+铺垫:他们相爱三年
+触发:她亲眼看到他和别人在一起
+释放:她没有哭,只是笑着说"原来如此"
+```
+
+### 失去虐点
+```
+铺垫:展示珍贵的东西
+触发:失去的瞬间
+释放:无法挽回的痛
+
+示例:
+铺垫:孩子是她活下去的唯一理由
+触发:孩子没了
+释放:她在产房外站了一夜,一滴泪都没有
+```
+
+### 真相虐点
+```
+铺垫:隐藏一个残酷真相
+触发:真相揭露
+释放:"原来一切都是假的"
+
+示例:
+铺垫:他对她百般好
+触发:她发现这一切都是为了接近她父亲
+释放:她把戒指放在桌上,"演得真好。"
+```
+
+---
+
+## 五、甜点高潮设计
+
+### 告白甜点
+```
+铺垫:暧昧期的拉扯
+触发:正式告白
+释放:双向奔赴的甜
+
+示例:
+铺垫:他们互相试探了三个月
+触发:"我喜欢你,不是开玩笑。"
+释放:"我也是。"
+```
+
+### 守护甜点
+```
+铺垫:她遇到困难/危险
+触发:他出现保护她
+释放:安全感和心动
+
+示例:
+铺垫:她被前男友纠缠
+触发:他挡在她面前
+释放:"她是我的人,有什么事冲我来。"
+```
+
+### 宠溺甜点
+```
+铺垫:日常相处
+触发:一个宠溺的细节
+释放:被偏爱的幸福感
+
+示例:
+铺垫:她随口说了一句想吃草莓
+触发:第二天桌上出现一箱草莓
+释放:卡片上写着"以后想吃什么直接说"
+```
+
+---
+
+## 六、高潮执行技巧
+
+### 1. 慢镜头法
+```
+在高潮时刻放慢节奏,用细节描写延长情绪
+
+示例:
+普通写法:他打了她一巴掌。
+慢镜头:他的手抬起来,她看着那只手落下,脸上火辣辣的疼。她没有躲,甚至没有眨眼。
+```
+
+### 2. 对比强化法
+```
+用对比强化情绪冲击
+
+示例:
+"三年前,他说'我养你'。三年后,他说'你配吗'。"
+"她曾经为他挡过刀,他却连一句解释都不愿意给。"
+```
+
+### 3. 金句点睛法
+```
+用一句有力的话定格情绪
+
+示例:
+- "我等了你三年,你让我再等三年?"
+- "从今天起,你死你的,我活我的。"
+- "我不是不爱了,是不敢了。"
+```
+
+### 4. 留白法
+```
+在最强烈的情绪点戛然而止
+
+示例:
+"她看着他,张了张嘴,最终什么都没说。"
+"他追出去的时候,只看到一个消失在雨中的背影。"
+```
+
+---
+
+## 七、高潮密度控制
+
+### 3000字短篇
+```
+1个主高潮 + 0-1个小高潮
+主高潮位置:70%-80%处
+```
+
+### 8000字中篇
+```
+1个主高潮 + 2-3个小高潮
+小高潮位置:30%、50%
+主高潮位置:75%-85%
+```
+
+### 15000字长短篇
+```
+1个主高潮 + 3-5个小高潮
+分布:每3000字左右一个情绪点
+主高潮位置:80%-90%
+```
+
+---
+
+## 八、高潮检查清单
+
+- [ ] 铺垫是否足够?(读者是否"憋"够了)
+- [ ] 触发是否有力?(是否有"爆点")
+- [ ] 释放是否充分?(情绪是否宣泄完整)
+- [ ] 节奏是否合适?(是否在高潮处放慢)
+- [ ] 是否有金句/名场面?
+- [ ] 高潮后是否有收尾?

+ 217 - 0
.claude/genres/zhihu-short/ending-patterns.md

@@ -0,0 +1,217 @@
+# 知乎短篇结局模式
+
+> 结局决定读者是否会推荐你的文。本文档提供知乎短篇常用的结局模式。
+
+---
+
+## 一、结局类型总览
+
+| 类型 | 情绪 | 适用题材 | 读者反应 |
+|------|------|---------|---------|
+| 大反转HE | 惊喜 | 虐转甜、悬疑 | "没想到!" |
+| 开放式 | 回味 | 文艺、悬疑 | "细思极恐" |
+| 意料之中HE | 满足 | 甜文、爽文 | "太甜了!" |
+| 虐心BE | 心痛 | 纯虐文 | "刀我!" |
+| 反转BE | 震撼 | 悬疑、暗黑 | "卧槽!" |
+
+---
+
+## 二、HE结局模板
+
+### 1. 大反转HE
+```
+模式:全程虐 → 最后反转 → 原来一切都是误会/保护
+示例:
+- 以为他不爱 → 其实默默守护多年
+- 以为被抛弃 → 其实是为了保护她
+- 以为是渣男 → 其实是卧底/失忆
+
+写法要点:
+- 反转要有足够铺垫(前文埋伏笔)
+- 揭示方式要有冲击力
+- 给读者"恍然大悟"的感觉
+```
+
+### 2. 复仇成功HE
+```
+模式:被虐 → 反击 → 手撕仇人 → 收获幸福
+示例:
+- 渣男跪求复合,女主冷笑转身
+- 绿茶身败名裂,女主事业爱情双丰收
+- 恶婆婆被儿子断绝关系
+
+写法要点:
+- 复仇要解气
+- 仇人下场要惨
+- 女主要有新的幸福
+```
+
+### 3. 破镜重圆HE
+```
+模式:分离 → 重逢 → 误会解开 → 复合
+示例:
+- 多年后重逢,发现他一直单身等她
+- 真相揭露,原来当年是被迫分开
+- 他用十年证明自己的改变
+
+写法要点:
+- 分离原因要充分
+- 复合要有诚意
+- 给读者"终于等到"的感觉
+```
+
+---
+
+## 三、BE结局模板
+
+### 1. 遗憾BE
+```
+模式:相爱 → 错过 → 永远失去
+示例:
+- 她等了他一辈子,他来的时候她已经不在了
+- 他终于说出口,她已经嫁给别人
+- 真相大白时,她已经病逝
+
+写法要点:
+- 错过要有必然性
+- 遗憾要深刻
+- 留下"如果当初"的叹息
+```
+
+### 2. 黑化BE
+```
+模式:被伤害 → 黑化 → 同归于尽/毁灭一切
+示例:
+- 她终于报了仇,但也失去了自己
+- 他毁掉了所有人,包括自己
+- 她笑着点燃了那把火
+
+写法要点:
+- 黑化要有充分理由
+- 结局要有震撼力
+- 让读者"心疼又无奈"
+```
+
+### 3. 反转BE
+```
+模式:以为HE → 最后一句反转 → 原来是BE
+示例:
+- "我醒来,发现这一切都是梦。"
+- "她不知道,他已经在三年前死了。"
+- "监控显示,那天晚上只有她一个人。"
+
+写法要点:
+- 反转要出人意料
+- 前文要有隐藏伏笔
+- 让读者"细思极恐"
+```
+
+---
+
+## 四、开放式结局
+
+### 1. 留白式
+```
+模式:故事在关键时刻戛然而止
+示例:
+- "门铃响了。她深吸一口气,打开门——"
+- "他看着那两条杠,沉默了。"
+- "她拿起笔,在离婚协议上——"
+
+效果:让读者自己想象结局
+```
+
+### 2. 暗示式
+```
+模式:不明说,但暗示了结局
+示例:
+- "一年后,她的肚子已经很大了。"(暗示复合)
+- "他的墓前,多了一束新鲜的白玫瑰。"(暗示她还爱他)
+- "她笑了,笑得像个疯子。"(暗示黑化)
+
+效果:让读者"懂的都懂"
+```
+
+### 3. 双重解读式
+```
+模式:结局可以有两种理解
+示例:
+- "她终于自由了。"(解脱/死亡?)
+- "他们再也没有见过面。"(放下/遗憾?)
+- "一切都结束了。"(HE/BE?)
+
+效果:引发讨论
+```
+
+---
+
+## 五、结局写作技巧
+
+### 1. 情绪收尾法
+```
+不要:直接写"他们幸福地生活在一起"
+而是:用一个细节展示幸福
+
+示例:
+"十年后,她还是会在他加班时,给他留一盏灯。"
+"他的手机壁纸,从未换过。"
+```
+
+### 2. 呼应开篇法
+```
+开篇:她签下离婚协议
+结尾:她撕掉了那张协议
+
+开篇:他说"我不爱你"
+结尾:他说"我爱你,从一开始就是"
+```
+
+### 3. 金句收尾法
+```
+用一句有力的话结束全文
+
+示例:
+- "有些人,错过就是一辈子。"
+- "她终于学会了,爱自己。"
+- "原来,他一直都在。"
+```
+
+### 4. 番外暗示法
+```
+正文结束后,用一小段番外暗示后续
+
+示例:
+"【番外】三年后,某医院产房外——"
+"【彩蛋】他的日记本最后一页——"
+```
+
+---
+
+## 六、结局检查清单
+
+### HE检查
+- [ ] 是否解决了核心矛盾?
+- [ ] 是否给了读者满足感?
+- [ ] 是否有足够的甜度?
+- [ ] 是否呼应了开篇?
+
+### BE检查
+- [ ] 悲剧是否有必然性?
+- [ ] 是否给了读者情绪宣泄?
+- [ ] 是否有回味的空间?
+- [ ] 是否避免了"为虐而虐"?
+
+### 开放式检查
+- [ ] 暗示是否足够清晰?
+- [ ] 是否留下了讨论空间?
+- [ ] 是否避免了"烂尾"感?
+
+---
+
+## 七、按字数选择结局
+
+| 字数 | 推荐结局 | 原因 |
+|------|---------|------|
+| 3000字 | 单一反转 | 篇幅有限,一个反转足够 |
+| 8000字 | 双重反转/开放式 | 有空间铺垫和收尾 |
+| 15000字 | 复杂HE/BE | 可以完整展开情感弧线 |

+ 224 - 0
.claude/genres/zhihu-short/genre-templates.md

@@ -0,0 +1,224 @@
+# 知乎短篇体裁模板
+
+> 本文档提供知乎热门短篇体裁的完整模板,可直接套用。
+
+---
+
+## 一、追妻火葬场(8000字模板)
+
+### 结构
+```
+开篇(500字):男主追妻进行时,女主冷漠
+闪回(2500字):当初如何虐她
+现在(2500字):她的改变,他的卑微
+转折(1500字):真相/契机
+结局(1000字):破镜重圆/彻底放下
+```
+
+### 关键场景
+1. **追妻名场面**:雨中等待/跪求原谅/放弃一切
+2. **虐心回忆**:冷暴力/偏心白月光/不信任
+3. **女主蜕变**:事业成功/有新欢/不再在意
+4. **真相揭露**:当年误会/他的苦衷/她的牺牲
+
+### 情绪曲线
+```
+开篇:爽(看他卑微)
+闪回:虐(心疼女主)
+现在:爽+虐(她变强了但还是心痛)
+结局:甜或释然
+```
+
+---
+
+## 二、重生复仇(5000字模板)
+
+### 结构
+```
+开篇(500字):前世惨死/重生瞬间
+布局(1500字):改变关键节点
+对决(2000字):直面仇人
+结局(1000字):复仇成功+新生
+```
+
+### 关键场景
+1. **前世惨状**:被害死的场景(震撼开局)
+2. **蝴蝶效应**:改变一个选择,改变命运
+3. **打脸时刻**:仇人震惊脸
+4. **最终审判**:仇人下场
+
+### 情绪曲线
+```
+开篇:虐(前世)→ 燃(重生)
+布局:爽(步步为营)
+对决:爽爆(手撕仇人)
+结局:满足
+```
+
+---
+
+## 三、豪门真假千金(10000字模板)
+
+### 结构
+```
+开篇(800字):身份揭露/回归豪门
+适应(2500字):被排挤/被比较
+反击(3500字):展露实力/打脸
+真相(2200字):当年真相/幕后黑手
+结局(1000字):夺回一切
+```
+
+### 关键场景
+1. **身份对比**:假千金的优越vs真千金的"土气"
+2. **被欺负**:假千金挑衅/家人偏心
+3. **实力展示**:某个领域碾压假千金
+4. **真相揭露**:当年被换的原因
+
+### 情绪曲线
+```
+开篇:期待
+适应:憋屈(为女主不平)
+反击:爽(打脸)
+真相:震惊+爽
+结局:满足
+```
+
+---
+
+## 四、娱乐圈马甲(8000字模板)
+
+### 结构
+```
+开篇(500字):被嘲讽/被看不起
+铺垫(2000字):各种马甲暗示
+揭马甲(3500字):一个接一个曝光
+收尾(2000字):全网震惊+撒糖
+```
+
+### 关键场景
+1. **被嘲场景**:黑粉嘲讽/对家挑衅
+2. **马甲暗示**:神秘大佬出现
+3. **揭马甲高潮**:身份曝光,全场震惊
+4. **打脸时刻**:之前嘲讽的人脸疼
+
+### 常用马甲
+```
+- 顶级设计师/画家/音乐人
+- 神秘财团继承人
+- 某领域学术大佬
+- 救命恩人/投资人
+```
+
+### 情绪曲线
+```
+开篇:憋(被嘲)
+铺垫:期待(知道要打脸)
+揭马甲:爽爆(一个比一个震撼)
+收尾:满足+甜
+```
+
+---
+
+## 五、契约婚姻(6000字模板)
+
+### 结构
+```
+开篇(500字):契约原因/规则制定
+相处(2000字):从陌生到熟悉
+心动(2000字):不自知的在意
+摊牌(1500字):契约到期/真心告白
+```
+
+### 关键场景
+1. **契约签订**:各取所需的交易
+2. **破冰时刻**:第一次真正交流
+3. **嫉妒桥段**:第三者引发占有欲
+4. **真心暴露**:不小心说出真心话
+
+### 情绪曲线
+```
+开篇:好奇
+相处:轻松+甜
+心动:小虐+甜
+摊牌:大甜
+```
+
+---
+
+## 六、病娇偏执(5000字模板)
+
+### 结构
+```
+开篇(500字):病娇行为展示
+回忆(1500字):为什么会变成这样
+现在(2000字):占有与挣扎
+结局(1000字):救赎/沉沦
+```
+
+### 关键场景
+1. **病娇名场面**:极端占有行为
+2. **童年/过去**:造成病娇的原因
+3. **女主态度**:恐惧→理解→接受/逃离
+4. **关键选择**:留下还是离开
+
+### 注意事项
+```
+- 病娇要有成因,不是无缘无故
+- 底线:不能真正伤害女主
+- 要有救赎可能或悲剧必然性
+```
+
+---
+
+## 七、先婚后爱(8000字模板)
+
+### 结构
+```
+开篇(600字):联姻原因/婚礼
+磨合(2400字):同居日常/互相试探
+暧昧(2500字):心动瞬间/嫉妒
+确认(2500字):表白/危机/HE
+```
+
+### 关键场景
+1. **尴尬同居**:分房睡/假装不在意
+2. **日常甜蜜**:不经意的关心
+3. **嫉妒时刻**:看到对方和异性接触
+4. **坦诚时刻**:承认喜欢对方
+
+### 情绪曲线
+```
+全程甜,小虐调味
+节奏:慢热 → 升温 → 沸腾
+```
+
+---
+
+## 八、模板使用指南
+
+### 选择模板
+1. 确定核心情绪(爽/虐/甜)
+2. 确定字数范围
+3. 选择匹配的模板
+
+### 填充内容
+1. 套入人设(参考 character-quick-build.md)
+2. 设计关键场景
+3. 写出金句/名台词
+
+### 调整优化
+1. 检查节奏(参考 pacing-rhythm.md)
+2. 强化高潮(参考 emotional-peaks.md)
+3. 打磨开篇(参考 hook-techniques.md)
+4. 优化结局(参考 ending-patterns.md)
+
+---
+
+## 九、模板组合技
+
+| 组合 | 效果 | 示例 |
+|------|------|------|
+| 重生+追妻 | 虐+爽 | 重生后看他追妻 |
+| 豪门+娱乐圈 | 爽+甜 | 豪门千金进娱乐圈 |
+| 契约+先婚后爱 | 纯甜 | 契约结婚日久生情 |
+| 病娇+重生 | 虐+救赎 | 重生后治愈病娇 |

+ 151 - 0
.claude/genres/zhihu-short/hook-techniques.md

@@ -0,0 +1,151 @@
+# 知乎短篇开篇钩子技巧
+
+> 知乎短篇(3000-15000字)的生死线在前300字。本文档提供经过验证的开篇钩子模板。
+
+---
+
+## 一、核心原则
+
+### 黄金300字法则
+- **前50字**:制造悬念或冲突
+- **50-150字**:建立人物处境
+- **150-300字**:抛出核心矛盾
+
+### 钩子三要素
+| 要素 | 作用 | 示例 |
+|------|------|------|
+| 反常 | 打破预期 | "我死后第三天,前夫来上坟" |
+| 悬念 | 引发好奇 | "那个秘密,我藏了整整十年" |
+| 冲突 | 制造张力 | "婚礼当天,新郎跪在了我面前" |
+
+---
+
+## 二、十大钩子模板
+
+### 1. 死亡开局
+```
+模板:我死了,然后[反转]
+示例:我死了,死在结婚纪念日。更讽刺的是,杀我的人正在我的葬礼上哭得最凶。
+适用:复仇、悬疑、虐文
+```
+
+### 2. 时间倒叙
+```
+模板:[结果],一切要从[时间点]说起
+示例:他跪在我面前求复合的时候,我只觉得好笑。三年前,他可不是这副嘴脸。
+适用:追妻火葬场、破镜重圆
+```
+
+### 3. 身份反转
+```
+模板:所有人都以为我是[A],其实我是[B]
+示例:所有人都以为我是高攀的穷亲戚,包括我那个"好心收留"我的表姐。
+适用:打脸、爽文、豪门
+```
+
+### 4. 极端处境
+```
+模板:[极端情况],我[反常反应]
+示例:被困在零下三十度的冷库里,我反而笑了。终于,可以名正言顺地死了。
+适用:虐文、救赎、悬疑
+```
+
+### 5. 对话开场
+```
+模板:"[冲击性台词]"
+示例:"签字吧,孩子我会养,你净身出户。"他把离婚协议推到我面前,眼里全是厌恶。
+适用:婚恋、虐文、爽文
+```
+
+### 6. 重生/穿越
+```
+模板:睁开眼,我回到了[关键时间点]
+示例:睁开眼,我回到了被推下楼的前一天。这一次,该死的人不会是我。
+适用:重生复仇、穿书
+```
+
+### 7. 秘密揭露
+```
+模板:[某人]不知道的是,[秘密]
+示例:我老公不知道的是,他的白月光,是我花钱雇来演戏的。
+适用:反转、复仇、爽文
+```
+
+### 8. 数字冲击
+```
+模板:[数字]+[反常事实]
+示例:结婚七年,他送我三十二次急诊,我终于学会了一件事——装死。
+适用:虐文、觉醒、复仇
+```
+
+### 9. 仪式感场景
+```
+模板:在[重要场合],[反转发生]
+示例:在我的葬礼上,我前夫带着小三来撒纸钱。他不知道,棺材里躺的不是我。
+适用:复仇、悬疑、爽文
+```
+
+### 10. 认知颠覆
+```
+模板:我一直以为[A],直到[B]
+示例:我一直以为自己是被收养的孤儿,直到亲生父母开着劳斯莱斯来接我。
+适用:身世、豪门、打脸
+```
+
+---
+
+## 三、钩子组合技
+
+### 双重钩子
+```
+死亡 + 身份反转:
+"我死了。死在知道自己是豪门千金的第二天。"
+
+时间倒叙 + 秘密揭露:
+"他跪着求我原谅的时候,不知道我口袋里装着他的亲子鉴定报告。"
+```
+
+### 三重钩子
+```
+极端处境 + 对话 + 反转:
+"你以为绑架我就能要挟我爸?"我看着绑匪,笑了,"他巴不得我死。"
+```
+
+---
+
+## 四、避坑指南
+
+### 常见失败开篇
+| 类型 | 问题 | 改进 |
+|------|------|------|
+| 流水账 | "我叫xxx,今年xx岁" | 直接进入冲突场景 |
+| 环境描写 | 大段景物描写 | 用一句话带过,聚焦人物 |
+| 背景铺垫 | 先讲前因后果 | 先给结果,再倒叙原因 |
+| 情绪宣泄 | 大段内心独白 | 用行动和对话展示 |
+
+### 钩子检验清单
+- [ ] 前50字是否有反常/悬念/冲突?
+- [ ] 读者是否会产生"为什么"的疑问?
+- [ ] 是否建立了人物的核心困境?
+- [ ] 是否暗示了故事走向?
+- [ ] 是否有继续阅读的欲望?
+
+---
+
+## 五、按题材分类
+
+### 虐文钩子
+- 死亡视角、极端处境、数字冲击
+- 关键词:死、恨、绝望、心死
+
+### 爽文钩子
+- 身份反转、秘密揭露、认知颠覆
+- 关键词:打脸、反转、真相
+
+### 甜文钩子
+- 对话开场、仪式感场景
+- 关键词:误会、心动、甜蜜
+
+### 悬疑钩子
+- 死亡开局、秘密揭露、极端处境
+- 关键词:真相、秘密、谜团

+ 190 - 0
.claude/genres/zhihu-short/pacing-rhythm.md

@@ -0,0 +1,190 @@
+# 知乎短篇节奏控制
+
+> 短篇的节奏是生命线。本文档提供知乎短篇(3000-15000字)的节奏控制方法。
+
+---
+
+## 一、节奏基础理论
+
+### 短篇 vs 长篇节奏差异
+| 维度 | 长篇 | 短篇 |
+|------|------|------|
+| 铺垫 | 可以慢热 | 必须快进 |
+| 转折 | 多次小转折 | 1-3次大转折 |
+| 情绪 | 波浪起伏 | 直线冲顶 |
+| 留白 | 大量留白 | 精准留白 |
+
+### 黄金节奏公式
+```
+短篇节奏 = 钩子(10%) + 升温(30%) + 高潮(40%) + 收尾(20%)
+```
+
+---
+
+## 二、字数分配模板
+
+### 3000字短篇
+| 阶段 | 字数 | 内容 |
+|------|------|------|
+| 钩子 | 300 | 制造悬念,建立处境 |
+| 升温 | 900 | 矛盾加深,情绪积累 |
+| 高潮 | 1200 | 爆发点,核心冲突 |
+| 收尾 | 600 | 反转或升华 |
+
+### 8000字中篇
+| 阶段 | 字数 | 内容 |
+|------|------|------|
+| 钩子 | 500 | 悬念+人物+处境 |
+| 第一幕 | 2000 | 建立矛盾,第一转折 |
+| 第二幕 | 3000 | 矛盾升级,情绪高潮 |
+| 第三幕 | 2500 | 大反转+结局 |
+
+### 15000字长短篇
+| 阶段 | 字数 | 内容 |
+|------|------|------|
+| 开篇 | 1000 | 钩子+背景+人物 |
+| 发展 | 4000 | 矛盾建立,关系变化 |
+| 转折 | 3000 | 第一次大反转 |
+| 高潮 | 4000 | 核心冲突爆发 |
+| 结局 | 3000 | 最终反转+收尾 |
+
+---
+
+## 三、节奏控制技巧
+
+### 1. 场景切换法
+```
+快节奏:场景跳跃,省略过渡
+"三天后,法院。"
+"一个月后,他的公司倒闭了。"
+
+慢节奏:细节描写,情绪渲染
+用于关键场景,如对峙、告白、死亡
+```
+
+### 2. 句式控制法
+```
+加速:短句、断句、对话
+"他来了。""我知道。""然后呢?""没有然后。"
+
+减速:长句、描写、内心
+她站在窗前,看着楼下那个熟悉的身影,心里五味杂陈。
+```
+
+### 3. 信息密度法
+```
+高密度(快):每句话都有新信息
+"他出轨了,对象是我闺蜜,证据在我手机里,离婚协议我已经拟好。"
+
+低密度(慢):情绪渲染,氛围营造
+用于情感高潮前的铺垫
+```
+
+---
+
+## 四、情绪曲线设计
+
+### 虐文曲线
+```
+     情绪
+      ↑
+   虐 |    ╱╲
+      |   ╱  ╲    ╱ (HE/BE)
+      |  ╱    ╲  ╱
+   甜 | ╱      ╲╱
+      +—————————————→ 进度
+        开篇  中段  结局
+```
+
+### 爽文曲线
+```
+     情绪
+      ↑
+   爽 |         ╱
+      |       ╱
+      |     ╱
+   憋 |   ╱
+      | ╱
+      +—————————————→ 进度
+        铺垫  打脸  高潮
+```
+
+### 甜文曲线
+```
+     情绪
+      ↑
+   甜 |    ╱╲  ╱╲  ╱
+      |   ╱  ╲╱  ╲╱
+      |  ╱
+   虐 | ╱
+      +—————————————→ 进度
+        误会  拉扯  HE
+```
+
+---
+
+## 五、转折点设计
+
+### 单转折结构(3000字)
+```
+铺垫 → 【转折】 → 结局
+示例:以为被抛弃 → 【发现真相】 → 破镜重圆
+```
+
+### 双转折结构(8000字)
+```
+铺垫 → 【转折1】 → 发展 → 【转折2】 → 结局
+示例:被虐 → 【觉醒】 → 反击 → 【真相】 → 复仇成功
+```
+
+### 三转折结构(15000字)
+```
+铺垫 → 【转折1】 → 发展 → 【转折2】 → 高潮 → 【转折3】 → 结局
+```
+
+---
+
+## 六、节奏急救指南
+
+### 问题:开篇太慢
+```
+解决:
+1. 删除背景介绍,直接进入冲突
+2. 用对话代替叙述
+3. 从故事中段开始,倒叙补充
+```
+
+### 问题:中段拖沓
+```
+解决:
+1. 删除无关场景
+2. 合并相似情节
+3. 用时间跳跃加速
+```
+
+### 问题:高潮不够爽
+```
+解决:
+1. 增加铺垫的"憋"
+2. 延长高潮场景
+3. 添加细节描写
+```
+
+### 问题:结尾仓促
+```
+解决:
+1. 给反转足够篇幅
+2. 添加情绪收尾
+3. 留一个回味点
+```
+
+---
+
+## 七、节奏检查清单
+
+- [ ] 前300字是否有钩子?
+- [ ] 每1000字是否有一个小高潮?
+- [ ] 转折点是否足够有力?
+- [ ] 高潮部分是否占比40%以上?
+- [ ] 结尾是否有回味?
+- [ ] 整体是否一气呵成?

+ 207 - 0
.claude/genres/zhihu-short/plot-compression.md

@@ -0,0 +1,207 @@
+# 知乎短篇剧情压缩技巧
+
+> 短篇的艺术是删减的艺术。本文档提供将长篇剧情压缩为短篇的实用技巧。
+
+---
+
+## 一、压缩核心原则
+
+### 减法思维
+```
+长篇思维:这个情节可以加进去
+短篇思维:这个情节必须删掉吗?
+```
+
+### 压缩公式
+```
+短篇 = 核心冲突 + 关键转折 + 情绪高潮
+删除 = 支线 + 过渡 + 重复 + 解释
+```
+
+---
+
+## 二、五大压缩技法
+
+### 1. 时间跳跃法
+```
+原文(500字):
+详细描写女主如何发现老公出轨,跟踪取证,内心挣扎...
+
+压缩(50字):
+"三个月的跟踪取证,换来一叠照片和一份离婚协议。"
+```
+
+### 2. 结果前置法
+```
+原文:先铺垫原因,再写结果
+压缩:先给结果,用闪回补充原因
+
+示例:
+"他跪在我面前的时候,我想起三年前,他也是这样跪着——向别的女人求婚。"
+```
+
+### 3. 对话代替叙述
+```
+原文(200字):
+她知道婆婆一直看不起她,觉得她配不上自己的儿子...
+
+压缩(50字):
+"我儿子什么条件,你什么条件?"婆婆的话,她听了三年。
+```
+
+### 4. 细节代替描写
+```
+原文(300字):
+详细描写她有多穷,住的房子多破,吃的多差...
+
+压缩(30字):
+冰箱里只有半颗白菜,她已经吃了一周。
+```
+
+### 5. 留白暗示法
+```
+原文:详细解释来龙去脉
+压缩:只给关键信息,让读者自己推断
+
+示例:
+"验DNA的时候,她没哭。结果出来的时候,她笑了。"
+(不解释为什么笑,让读者自己理解)
+```
+
+---
+
+## 三、删减优先级
+
+### 必须保留
+```
+1. 核心冲突(故事存在的理由)
+2. 主要转折(剧情推进的关键)
+3. 情绪高潮(读者爽点/虐点)
+4. 结局反转(故事的落点)
+```
+
+### 优先删除
+```
+1. 次要人物的支线
+2. 重复强调的信息
+3. 过渡性场景
+4. 背景解释说明
+5. 心理活动的反复
+```
+
+### 可以压缩
+```
+1. 时间跨度大的发展
+2. 相似情节的重复
+3. 环境氛围描写
+4. 人物关系介绍
+```
+
+---
+
+## 四、常见剧情压缩示例
+
+### 追妻火葬场压缩
+```
+长篇版(50万字):
+卷一:相识相爱
+卷二:误会虐心
+卷三:分离
+卷四:追妻
+卷五:HE
+
+短篇版(8000字):
+开篇:已经分离,男主追妻中(500字)
+闪回:当初如何虐她(2000字)
+现在:她的改变和冷漠(2000字)
+转折:真相揭露(2000字)
+结局:破镜重圆(1500字)
+```
+
+### 重生复仇压缩
+```
+长篇版(80万字):
+详细的重生布局,一个个收拾仇人
+
+短篇版(5000字):
+开篇:重生瞬间+核心仇人锁定(500字)
+布局:关键一步棋(1500字)
+执行:直接对决大Boss(2000字)
+结局:复仇成功(1000字)
+```
+
+### 豪门恩怨压缩
+```
+长篇版:复杂的家族关系和权力斗争
+
+短篇版(10000字):
+聚焦一个核心矛盾(真假千金/遗产争夺)
+只保留2-3个关键人物
+一次关键对决定胜负
+```
+
+---
+
+## 五、信息传递技巧
+
+### 1. 嵌入式背景
+```
+错误:先用500字介绍背景
+正确:在对话/行动中自然带出
+
+示例:
+"三年了,你还住在这个破出租屋?"
+(一句话带出:时间跨度+经济状况+人物关系)
+```
+
+### 2. 冰山式写作
+```
+只写冰山一角,让读者脑补水下部分
+
+示例:
+"她看了一眼手机,是第47个未接来电。"
+(不解释谁打的、为什么不接,读者自己理解)
+```
+
+### 3. 符号化表达
+```
+用一个符号代替大段描写
+
+示例:
+- 戒指:婚姻状态
+- 验孕棒:怀孕
+- 病历本:重病
+- 亲子鉴定:身世
+```
+
+---
+
+## 六、压缩检查清单
+
+### 删减检查
+- [ ] 是否有可以删除的次要人物?
+- [ ] 是否有重复表达的信息?
+- [ ] 是否有可以跳过的过渡场景?
+- [ ] 是否有过多的心理描写?
+
+### 效率检查
+- [ ] 每个场景是否都推进了剧情?
+- [ ] 每段对话是否都有信息量?
+- [ ] 是否用最少的字数传递了信息?
+
+### 完整性检查
+- [ ] 核心冲突是否清晰?
+- [ ] 转折是否足够有力?
+- [ ] 结局是否有回味?
+
+---
+
+## 七、字数控制参考
+
+| 元素 | 3000字 | 8000字 | 15000字 |
+|------|--------|--------|---------|
+| 主要人物 | 2人 | 3-4人 | 5-6人 |
+| 场景数量 | 3-5个 | 6-10个 | 10-15个 |
+| 转折次数 | 1次 | 2次 | 3次 |
+| 时间跨度 | 1天-1周 | 1周-1月 | 1月-1年 |
+| 支线数量 | 0 | 1条 | 1-2条 |

+ 487 - 0
.claude/references/entity-management-spec.md

@@ -0,0 +1,487 @@
+# 实体管理规范 (Entity Management Specification)
+
+> **版本**: 5.0
+> **适用范围**: 所有实体类型(角色/地点/物品/势力/招式)
+> **核心目标**: AI 驱动的实体提取、别名管理、版本追踪
+
+---
+
+## v5.0 变更
+
+1. **AI 提取替代 XML 标签**: Data Agent 从纯正文语义提取实体,不再依赖 `extract_entities.py`
+2. **alias_index 一对多**: 同一别名可映射到多个实体,内嵌在 `state.json`
+3. **entities_v3 分组格式**: 按类型分组(角色/地点/物品/势力/招式)
+4. **置信度消歧**: >0.8 自动采用,0.5-0.8 警告,<0.5 人工确认
+5. **无向后兼容**: 不保留旧版 `entities` 列表格式
+6. **双 Agent 架构**: Context Agent (读) + Data Agent (写)
+
+> **注意**: XML 标签仍可用于手动标注场景,但 v5.0 主流程不再要求。
+
+---
+
+## 一、问题分析
+
+### 1.1 当前问题
+
+1. **别名问题**: 同一角色在不同章节有不同称呼
+   - 第1章: "废物" (贬称)
+   - 第10章: "林天" (真名)
+   - 第50章: "林宗主" (地位称呼)
+   - 第200章: "不灭战神" (称号)
+
+2. **创建/更新问题**: 当前使用 `setdefault()` 只能创建,无法更新
+
+3. **版本追踪问题**: 无法追踪属性变更历史
+
+### 1.2 各类实体特点
+
+| 实体类型 | 别名复杂度 | 属性变化 | 层级关系 |
+|---------|-----------|---------|---------|
+| 角色    | 高(多种称呼)| 高(境界/位置/关系)| 无 |
+| 地点    | 中(简称/全称)| 低(状态变化)| 有(省>市>区)|
+| 物品    | 低(别称较少)| 中(升级/转移)| 无 |
+| 势力    | 中(简称/别称)| 中(等级/领地)| 有(总部>分部)|
+| 招式    | 低(别名少见)| 中(升级)| 无 |
+
+---
+
+## 二、标签体系设计
+
+### 2.1 新建实体 (`<entity>`)
+
+首次出场时使用,**推荐**包含 `id` 属性作为唯一标识(便于后续更新/加别名);不写 `id` 时脚本会自动生成并注册 `name/alias`。
+
+```xml
+<entity type="角色" id="lintian" name="林天" desc="主角,觉醒吞噬金手指" tier="核心">
+  <alias>废物</alias>
+  <alias>那个少年</alias>
+</entity>
+
+<entity type="地点" id="tianyunzong" name="天云宗" desc="东域三大宗门之一" tier="核心">
+  <alias>宗门</alias>
+  <alias>天云</alias>
+</entity>
+
+<entity type="地点" id="tianyunzong_waimen" name="天云宗外门" parent="tianyunzong" desc="外门弟子修炼区" tier="支线">
+  <alias>外门</alias>
+</entity>
+```
+
+> 注:当前脚本不解析 `<sub-location>` 这种嵌套子标签;子地点请用独立 `<entity>` + `parent` 字段表达。
+
+### 2.2 添加别名 (`<entity-alias>`)
+
+后续章节出现新称呼时使用:
+
+```xml
+<!-- 方式1: 通过 id 引用 -->
+<entity-alias id="lintian" alias="林宗主" context="成为天云宗主后"/>
+
+<!-- 方式2: 通过已知别名引用(自动解析) -->
+<entity-alias ref="林天" alias="不灭战神" context="晋升战神称号后"/>
+```
+
+### 2.3 更新属性 (`<entity-update>`)
+
+属性发生重大变化时使用(v5.0 支持多种操作):
+
+```xml
+<!-- 基础操作 -->
+<entity-update id="lintian">
+  <set key="realm" value="筑基期一层" reason="血煞秘境突破"/>
+  <set key="location" value="天云宗"/>
+</entity-update>
+
+<!-- 删除属性 -->
+<entity-update id="lintian">
+  <unset key="bottleneck"/>
+</entity-update>
+
+<!-- 数组操作 -->
+<entity-update id="lintian">
+  <add key="titles" value="不灭战神"/>
+  <remove key="allies" value="张三"/>
+</entity-update>
+
+<!-- 计数操作 -->
+<entity-update id="lintian">
+  <inc key="kill_count" delta="1"/>
+</entity-update>
+
+<!-- 顶层字段修改 -->
+<entity-update id="lintian">
+  <set key="tier" value="核心"/>
+  <set key="canonical_name" value="林不灭" reason="觉醒后改名"/>
+</entity-update>
+
+<!-- 通过别名引用(需 type 消歧) -->
+<entity-update ref="林宗主" type="角色">
+  <set key="realm" value="金丹期"/>
+</entity-update>
+```
+
+**顶层字段白名单**: `tier`, `desc`, `canonical_name`, `importance`, `status`, `parent`
+
+**操作类型**:
+| 操作 | 语法 | 说明 |
+|------|------|------|
+| set | `<set key="k" value="v"/>` | 设置属性值 |
+| unset | `<unset key="k"/>` | 删除属性 |
+| add | `<add key="k" value="v"/>` | 向数组添加元素 |
+| remove | `<remove key="k" value="v"/>` | 从数组删除元素 |
+| inc | `<inc key="k" delta="1"/>` | 数值递增(默认+1) |
+
+### 2.4 简化写法(自动检测模式)
+
+对于简单场景,可使用传统标签格式,系统自动检测:
+
+```xml
+<!-- 系统自动查询 alias_index,判断是创建还是更新 -->
+<entity type="角色" name="林宗主" realm="金丹期"/>
+```
+
+**自动检测逻辑**:
+1. 查询 `alias_index`,检查 `name` 是否已是某个实体的别名
+2. 如找到 → 更新该实体
+3. 如未找到 → 视为新实体,创建并生成 `id`
+
+---
+
+## 三、存储结构设计
+
+### 3.1 state.json 结构
+
+```json
+{
+  "entities_v3": {
+    "角色": {
+      "lintian": {
+        "id": "lintian",
+        "canonical_name": "林天",
+        "aliases": ["废物", "那个少年", "林宗主", "不灭战神"],
+        "tier": "核心",
+        "desc": "主角,觉醒吞噬金手指",
+        "current": {
+          "realm": "金丹期",
+          "location": "天云宗",
+          "last_chapter": 100
+        },
+        "history": [
+          {"chapter": 1, "changes": {"realm": "练气期一层"}, "reasons": {"realm": "初始状态"}, "added_at": "2026-01-01 00:00:00"},
+          {"chapter": 10, "changes": {"realm": "练气期九层"}, "reasons": {"realm": "吞噬突破"}, "added_at": "2026-01-01 00:00:00"},
+          {"chapter": 50, "changes": {"realm": "筑基期一层"}, "reasons": {"realm": "血煞秘境突破"}, "added_at": "2026-01-01 00:00:00"}
+        ],
+        "created_chapter": 1,
+        "first_appearance": "正文/第0001章.md"
+      }
+    },
+    "地点": {},
+    "物品": {},
+    "势力": {},
+    "招式": {}
+  },
+
+  "alias_index": {
+    "废物": [{"type": "角色", "id": "lintian"}],
+    "林天": [{"type": "角色", "id": "lintian"}],
+    "林宗主": [{"type": "角色", "id": "lintian"}],
+    "天云宗": [
+      {"type": "地点", "id": "loc_tianyunzong"},
+      {"type": "势力", "id": "faction_tianyunzong"}
+    ],
+    "外门": [{"type": "地点", "id": "tianyunzong_waimen"}]
+  }
+}
+```
+
+**注意**: v5.0 的 `alias_index` 值为数组(一对多),不再是单个对象。
+
+### 3.2 ID 生成规则
+
+```python
+import hashlib
+from pypinyin import lazy_pinyin
+
+def generate_entity_id(entity_type: str, name: str, existing_ids: set) -> str:
+    """
+    生成唯一实体 ID
+
+    规则:
+    1. 优先使用拼音(去空格、小写)
+    2. 冲突时追加数字后缀
+    3. 特殊前缀按类型
+    """
+    # 类型前缀映射
+    prefix_map = {
+        "物品": "item_",
+        "势力": "faction_",
+        "招式": "skill_",
+        "地点": "loc_"
+        # 角色无前缀
+    }
+
+    # 生成基础 ID
+    pinyin = ''.join(lazy_pinyin(name))
+    base_id = prefix_map.get(entity_type, '') + pinyin.lower()
+
+    # 处理冲突
+    final_id = base_id
+    counter = 1
+    while final_id in existing_ids:
+        final_id = f"{base_id}_{counter}"
+        counter += 1
+
+    return final_id
+```
+
+---
+
+## 四、处理流程
+
+> **v5.0 说明**: 以下流程描述的是 XML 标签解析流程,仅适用于**手动标注场景**。
+> v5.0 主流程使用 Data Agent 从纯正文 AI 提取实体,参见 `agents/data-agent.md`。
+
+### 4.1 完整流程图(手动标注场景)
+
+```
+章节内容
+    ↓
+extract_entities.py
+    ↓
+┌─────────────────────────────────────────────────────────┐
+│ 1. 解析所有 XML 标签                                      │
+│    - <entity> 标签 → 新实体候选                           │
+│    - <entity-alias> 标签 → 别名注册                       │
+│    - <entity-update> 标签 → 属性更新                      │
+│                                                          │
+│ 2. 加载 state.json 的 alias_index                        │
+│                                                          │
+│ 3. 对每个 <entity> 标签:                                  │
+│    ├─ 有 id 属性 → 使用指定 id                            │
+│    └─ 无 id 属性 → 查询 alias_index:                      │
+│        ├─ 找到 → 更新模式(使用找到的 id)                  │
+│        └─ 未找到 → 创建模式(生成新 id)                    │
+│                                                          │
+│ 4. 创建模式:                                              │
+│    - 生成唯一 id                                         │
+│    - 初始化 entity 对象(canonical_name, aliases, etc.)  │
+│    - 设置 current 初始属性                                │
+│    - 记录 history[0] 初始状态                             │
+│    - 更新 alias_index(所有别名 → id)                    │
+│                                                          │
+│ 5. 更新模式:                                              │
+│    - 合并新属性到 current                                 │
+│    - 追加 history 记录(如有重要变更)                     │
+│    - 更新 last_chapter                                   │
+│    - 添加新别名到 aliases 和 alias_index                  │
+│                                                          │
+│ 6. 处理 <entity-alias>:                                   │
+│    - 解析 id 或 ref                                       │
+│    - 添加 alias 到 aliases 列表                           │
+│    - 更新 alias_index                                    │
+│                                                          │
+│ 7. 处理 <entity-update>:                                  │
+│    - 解析 id 或 ref(通过 alias_index 解析)               │
+│    - 应用 <set> 更新到 current                            │
+│    - 追加 history 记录                                    │
+└─────────────────────────────────────────────────────────┘
+    ↓
+state.json 更新
+```
+
+### 4.2 别名解析函数
+
+```python
+def resolve_entity_by_alias(alias: str, entity_type: str, state: dict) -> tuple:
+    """
+    通过别名解析实体 ID
+
+    Args:
+        alias: 别名或名称
+        entity_type: 实体类型(角色/地点/物品/势力/招式)
+        state: state.json 内容
+
+    Returns:
+        (entity_id, entity_data) 或 (None, None)
+    """
+    alias_index = state.get("alias_index", {})
+
+    # 1. 精确匹配
+    if alias in alias_index:
+        ref = alias_index[alias]
+        if ref["type"] == entity_type:
+            entity_id = ref["id"]
+            entity_data = state["entities_v3"].get(entity_type, {}).get(entity_id)
+            return (entity_id, entity_data)
+
+    # 2. 模糊匹配(可选,适用于"云长老" vs "云长老(天云宗)")
+    for key, ref in alias_index.items():
+        if ref["type"] == entity_type and alias in key:
+            entity_id = ref["id"]
+            entity_data = state["entities_v3"].get(entity_type, {}).get(entity_id)
+            return (entity_id, entity_data)
+
+    return (None, None)
+```
+
+---
+
+## 五、特殊场景处理
+
+### 5.1 角色改名
+
+当角色正式改名(如赐名、觉醒后改名):
+
+```xml
+<!-- 保留旧别名,添加新的 canonical_name -->
+<entity-update id="lintian">
+  <set key="canonical_name" value="林不灭" reason="觉醒战神血脉后改名"/>
+</entity-update>
+<entity-alias id="lintian" alias="林不灭"/>
+```
+
+### 5.2 地点层级
+
+子地点作为独立实体,但记录父子关系:
+
+```xml
+<entity type="地点" id="tianyunzong_neimen" name="天云宗内门"
+        parent="tianyunzong" desc="核心弟子修炼区域" tier="支线">
+  <alias>内门</alias>
+</entity>
+```
+
+### 5.3 物品转移
+
+物品更换主人:
+
+```xml
+<entity-update ref="混沌珠">
+  <set key="owner" value="李雪" reason="林天将混沌珠赠予李雪"/>
+</entity-update>
+```
+
+### 5.4 势力合并/覆灭
+
+```xml
+<entity-update id="xueshamen">
+  <set key="status" value="覆灭" reason="被天云宗剿灭"/>
+  <set key="destroyed_chapter" value="75"/>
+</entity-update>
+```
+
+---
+
+## 六、迁移策略(已移除)
+
+本插件不再提供旧格式迁移与向后兼容。v5.0 推荐做法:
+
+1. 删除 `.webnovel/index.db`(索引可重建)
+2. 保留章节文件不动(纯正文是唯一真相)
+3. 运行 `python -m data_modules.index_manager rebuild --project-root .` 重建索引
+4. Data Agent 会在后续章节中自动提取实体
+
+> **注意**: v5.0 不再依赖 `extract_entities.py`,实体提取由 Data Agent 自动完成。
+
+---
+
+## 七、查询接口
+
+### 7.1 通过别名查询实体
+
+```python
+def query_entity(name_or_alias: str, entity_type: str = None) -> dict:
+    """
+    通过名称或别名查询实体完整信息
+
+    返回:
+    {
+        "id": "lintian",
+        "type": "角色",
+        "canonical_name": "林天",
+        "aliases": [...],
+        "current": {...},
+        "history": [...]
+    }
+    """
+```
+
+### 7.2 查询实体变更历史
+
+```python
+def query_entity_history(entity_id: str, entity_type: str) -> list:
+    """
+    查询实体的属性变更历史
+
+    返回:
+    [
+        {"chapter": 1, "changes": {"realm": "练气期一层"}, "reasons": {"realm": "初始"}, "added_at": "YYYY-MM-DD HH:MM:SS"},
+        {"chapter": 50, "changes": {"realm": "筑基期"}, "reasons": {"realm": "突破"}, "added_at": "YYYY-MM-DD HH:MM:SS"},
+        ...
+    ]
+    """
+```
+
+### 7.3 查询某章节实体状态
+
+```python
+def query_entity_at_chapter(entity_id: str, entity_type: str, chapter: int) -> dict:
+    """
+    查询实体在特定章节时的状态(通过历史回溯)
+
+    用于一致性检查:验证描述是否与当时状态匹配
+    """
+```
+
+---
+
+## 八、错误处理
+
+### 8.1 别名冲突
+
+v5.0 允许 **alias_index 一对多**:同一别名可以指向多个实体(跨类型或同类型)。
+
+当你用 `ref="别名"` 进行引用,但命中多个实体且无法消歧时,脚本会直接报错:
+
+```
+⚠️ 别名歧义: '宗主' 命中 2 个实体,请改用 id 或补充 type 属性
+
+解决方案:
+  1. 改用稳定 id:<entity-update id="...">...</entity-update>
+  2. 补充 type(仅能消歧跨类型;同类型重名仍需 id)
+  3. 追加更具体的 alias(避免以后持续歧义)
+```
+
+### 8.2 未知引用
+
+当 `<entity-update ref="xxx">` 找不到对应实体:
+
+```
+⚠️ 未知实体引用: "xxx" 在 alias_index 中未找到
+   建议: 先使用 <entity> 创建,或检查拼写
+```
+
+---
+
+## 九、总结
+
+### 9.1 核心改进
+
+1. **统一 ID 系统**: 所有实体有唯一 ID,别名映射到 ID
+2. **自动检测**: 无需显式指定创建/更新,系统自动判断
+3. **版本追踪**: history 数组记录重要属性变更
+4. **v5.0 架构**: 使用 `entities_v3` 分组格式,XML 标签为可选(手动标注场景)
+
+### 9.2 新增标签
+
+| 标签 | 用途 | 必填属性 |
+|------|------|---------|
+| `<entity>` | 创建/更新实体 | type, name |
+| `<entity-alias>` | 添加别名 | id/ref, alias |
+| `<entity-update>` | 更新属性 | id/ref, `<set>`/`<unset>`/`<add>`/`<remove>`/`<inc>` |
+
+### 9.3 实现优先级
+
+1. **P0**: alias_index 和自动检测(解决核心问题)
+2. **P1**: 属性更新和历史记录
+3. **P2**: 索引主键迁移(entity_id)+ Context Pack

+ 19 - 0
.claude/scripts/__init__.py

@@ -0,0 +1,19 @@
+"""
+webnovel-writer scripts package
+
+This package contains all Python scripts for the webnovel-writer plugin.
+"""
+
+__version__ = "5.0.0"
+__author__ = "lcy"
+
+# Expose main modules
+from . import security_utils
+from . import project_locator
+from . import chapter_paths
+
+__all__ = [
+    "security_utils",
+    "project_locator",
+    "chapter_paths",
+]

+ 594 - 0
.claude/scripts/archive_manager.py

@@ -0,0 +1,594 @@
+#!/usr/bin/env python3
+"""
+state.json 数据归档管理脚本
+
+目标:防止 state.json 无限增长,确保 200 万字长跑稳定运行
+
+功能:
+1. 智能归档长期未使用的数据(角色/伏笔/审查报告)
+2. 自动触发条件检测(文件大小/章节数)
+3. 安全备份与恢复机制
+4. 归档数据可随时恢复
+
+归档策略:
+- 角色:超过 50 章未出场的次要角色 → archive/characters.json
+- 伏笔:status="已回收" 且超过 20 章的伏笔 → archive/plot_threads.json
+- 审查报告:超过 50 章的旧报告 → archive/reviews.json
+
+使用方式:
+  # 自动归档检查(推荐在 update_state.py 之后调用)
+  python archive_manager.py --auto-check
+
+  # 强制归档(忽略触发条件)
+  python archive_manager.py --force
+
+  # 恢复特定角色
+  python archive_manager.py --restore-character "李雪"
+
+  # 查看归档统计
+  python archive_manager.py --stats
+
+  # Dry-run 模式(仅显示将被归档的数据)
+  python archive_manager.py --auto-check --dry-run
+"""
+
+import json
+import os
+import sys
+import argparse
+from datetime import datetime
+from pathlib import Path
+
+# ============================================================================
+# 安全修复:导入安全工具函数(P1 MEDIUM)
+# ============================================================================
+from security_utils import create_secure_directory, atomic_write_json
+from project_locator import resolve_project_root
+
+# Windows UTF-8 编码修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+
+class ArchiveManager:
+    """state.json 数据归档管理器"""
+
+    def __init__(self, project_root=None):
+        if project_root is None:
+            # 默认使用当前目录
+            project_root = Path.cwd()
+        else:
+            project_root = Path(project_root)
+
+        self.state_file = project_root / ".webnovel" / "state.json"
+        self.archive_dir = project_root / ".webnovel" / "archive"
+
+        # ============================================================================
+        # 安全修复:使用安全目录创建函数(P1 MEDIUM)
+        # 原代码: self.archive_dir.mkdir(parents=True, exist_ok=True)
+        # 漏洞: 未设置权限,使用OS默认(可能为755,允许同组用户读取)
+        # ============================================================================
+        create_secure_directory(str(self.archive_dir))
+
+        # 归档文件路径
+        self.characters_archive = self.archive_dir / "characters.json"
+        self.plot_threads_archive = self.archive_dir / "plot_threads.json"
+        self.reviews_archive = self.archive_dir / "reviews.json"
+
+        # 归档规则配置
+        self.config = {
+            "character_inactive_threshold": 50,  # 角色超过 50 章未出场视为不活跃
+            "plot_resolved_threshold": 20,       # 已回收伏笔超过 20 章后归档
+            "review_old_threshold": 50,          # 审查报告超过 50 章后归档
+            "file_size_trigger_mb": 1.0,         # state.json 超过 1.0MB 触发强制归档
+            "chapter_trigger": 10                # 每 10 章检查一次
+        }
+
+    def load_state(self):
+        """加载 state.json"""
+        if not self.state_file.exists():
+            print(f"❌ state.json 不存在: {self.state_file}")
+            sys.exit(1)
+
+        with open(self.state_file, 'r', encoding='utf-8') as f:
+            return json.load(f)
+
+    def save_state(self, state):
+        """保存 state.json(原子化写入)"""
+        # 使用集中式原子写入(自动备份)
+        atomic_write_json(self.state_file, state, use_lock=True, backup=True)
+        print(f"✅ state.json 已原子化更新")
+
+    def load_archive(self, archive_file):
+        """加载归档文件"""
+        if not archive_file.exists():
+            return []
+
+        with open(archive_file, 'r', encoding='utf-8') as f:
+            return json.load(f)
+
+    def save_archive(self, archive_file, data):
+        """保存归档文件"""
+        with open(archive_file, 'w', encoding='utf-8') as f:
+            json.dump(data, f, ensure_ascii=False, indent=2)
+
+    def check_trigger_conditions(self, state):
+        """检查是否需要触发归档"""
+        current_chapter = state.get("progress", {}).get("current_chapter", 0)
+
+        # 条件 1: 文件大小超过阈值
+        file_size_mb = self.state_file.stat().st_size / (1024 * 1024)
+        size_trigger = file_size_mb >= self.config["file_size_trigger_mb"]
+
+        # 条件 2: 章节数是触发间隔的倍数
+        chapter_trigger = (current_chapter % self.config["chapter_trigger"]) == 0 and current_chapter > 0
+
+        return {
+            "should_archive": size_trigger or chapter_trigger,
+            "file_size_mb": file_size_mb,
+            "current_chapter": current_chapter,
+            "size_trigger": size_trigger,
+            "chapter_trigger": chapter_trigger
+        }
+
+    def identify_inactive_characters(self, state):
+        """识别不活跃的次要角色 (v5.0 entities_v3 格式)"""
+        current_chapter = state.get("progress", {}).get("current_chapter", 0)
+        # v5.0: 从 entities_v3.角色 获取角色列表
+        entities_v3 = state.get("entities_v3", {})
+        characters_dict = entities_v3.get("角色", {})
+        threshold = self.config["character_inactive_threshold"]
+
+        inactive = []
+        for char_id, char in characters_dict.items():
+            # 只归档次要角色(tier="装饰" 或 tier="支线")
+            tier = str(char.get("tier", "")).strip()
+            if tier == "核心":
+                continue
+
+            # 检查最后出场章节
+            last_appearance = char.get("last_appearance", 0)
+            try:
+                last_appearance = int(last_appearance)
+            except (TypeError, ValueError):
+                last_appearance = 0
+            if last_appearance <= 0:
+                continue
+
+            inactive_chapters = current_chapter - last_appearance
+
+            if inactive_chapters >= threshold:
+                # 构造兼容结构
+                char_data = {
+                    "id": char_id,
+                    "name": char.get("canonical_name", char_id),
+                    "tier": tier,
+                    "last_appearance_chapter": last_appearance
+                }
+                char_data.update(char)
+                inactive.append({
+                    "character": char_data,
+                    "inactive_chapters": inactive_chapters,
+                    "last_appearance": last_appearance
+                })
+
+        return inactive
+
+    def identify_resolved_plot_threads(self, state):
+        """识别可归档的已回收伏笔"""
+        current_chapter = state.get("progress", {}).get("current_chapter", 0)
+        plot_threads = state.get("plot_threads", {}) or {}
+        foreshadowing = plot_threads.get("foreshadowing", []) or []
+        resolved_legacy = plot_threads.get("resolved", []) or []
+        threshold = self.config["plot_resolved_threshold"]
+
+        archivable = []
+        # 新格式:plot_threads.foreshadowing(用 status 标识是否已回收)
+        if isinstance(foreshadowing, list):
+            for item in foreshadowing:
+                if not isinstance(item, dict):
+                    continue
+                status = str(item.get("status", "")).strip()
+                if status not in ["已回收", "resolved"]:
+                    continue
+                try:
+                    resolved_chapter = int(item.get("resolved_chapter", 0))
+                except (TypeError, ValueError):
+                    continue
+                chapters_since_resolved = current_chapter - resolved_chapter
+                if chapters_since_resolved >= threshold:
+                    archivable.append({
+                        "thread": item,
+                        "chapters_since_resolved": chapters_since_resolved,
+                        "resolved_chapter": resolved_chapter
+                    })
+
+        # 旧格式兼容:plot_threads.resolved(直接存已回收列表)
+        if isinstance(resolved_legacy, list):
+            for item in resolved_legacy:
+                if not isinstance(item, dict):
+                    continue
+                try:
+                    resolved_chapter = int(item.get("resolved_chapter", 0))
+                except (TypeError, ValueError):
+                    continue
+                chapters_since_resolved = current_chapter - resolved_chapter
+                if chapters_since_resolved >= threshold:
+                    archivable.append({
+                        "thread": item,
+                        "chapters_since_resolved": chapters_since_resolved,
+                        "resolved_chapter": resolved_chapter
+                    })
+
+        return archivable
+
+    def identify_old_reviews(self, state):
+        """识别可归档的旧审查报告"""
+        current_chapter = state.get("progress", {}).get("current_chapter", 0)
+        reviews = state.get("review_checkpoints", [])
+        threshold = self.config["review_old_threshold"]
+
+        def _parse_end_chapter(review: dict) -> int:
+            # 新格式:{"chapters":"5-6","report":"...","reviewed_at":"..."}
+            chapters = review.get("chapters")
+            if isinstance(chapters, str):
+                parts = [p.strip() for p in chapters.replace("—", "-").split("-") if p.strip()]
+                if parts:
+                    try:
+                        return int(parts[-1])
+                    except ValueError:
+                        pass
+
+            # 旧格式:{"chapter_range":[5,6], "date":"..."}
+            cr = review.get("chapter_range")
+            if isinstance(cr, (list, tuple)) and len(cr) >= 2:
+                try:
+                    return int(cr[1])
+                except (TypeError, ValueError):
+                    pass
+
+            # 兜底:从 report 文件名里抓 "Ch5-6" 或 "第005-006"
+            report = review.get("report")
+            if isinstance(report, str):
+                import re
+                m = re.search(r"Ch(\d+)[-–—](\d+)", report)
+                if m:
+                    try:
+                        return int(m.group(2))
+                    except ValueError:
+                        pass
+                m = re.search(r"第(\d+)[-–—](\d+)章", report)
+                if m:
+                    try:
+                        return int(m.group(2))
+                    except ValueError:
+                        pass
+
+            return 0
+
+        old_reviews = []
+        for review in reviews:
+            review_chapter = _parse_end_chapter(review)
+            chapters_since_review = current_chapter - review_chapter
+
+            if chapters_since_review >= threshold:
+                old_reviews.append({
+                    "review": review,
+                    "chapters_since_review": chapters_since_review,
+                    "review_chapter": review_chapter
+                })
+
+        return old_reviews
+
+    def archive_characters(self, inactive_list, dry_run=False):
+        """归档不活跃角色(Priority 2 修复:与索引集成)"""
+        if not inactive_list:
+            return 0
+
+        # 加载现有归档
+        archived = self.load_archive(self.characters_archive)
+
+        # 添加时间戳
+        timestamp = datetime.now().isoformat()
+        for item in inactive_list:
+            item["character"]["archived_at"] = timestamp
+            archived.append(item["character"])
+
+            # ✅ Priority 2 修复:同步更新索引状态(而非删除)
+            if not dry_run:
+                try:
+                    # 导入索引模块
+                    import sys
+                    from pathlib import Path
+                    script_dir = Path(__file__).parent
+                    sys.path.insert(0, str(script_dir))
+                    from structured_index import StructuredIndex
+
+                    # 更新索引状态为 'archived'
+                    project_root = self.state_file.parent.parent
+                    index = StructuredIndex(str(project_root))
+                    index.mark_character_archived(item["character"]["name"], timestamp)
+                except Exception as e:
+                    # 索引更新失败不影响归档流程
+                    print(f"⚠️ 索引状态更新失败(不影响归档): {e}")
+
+        if not dry_run:
+            self.save_archive(self.characters_archive, archived)
+
+        return len(inactive_list)
+
+    def archive_plot_threads(self, resolved_list, dry_run=False):
+        """归档已回收伏笔"""
+        if not resolved_list:
+            return 0
+
+        # 加载现有归档
+        archived = self.load_archive(self.plot_threads_archive)
+
+        # 添加时间戳
+        timestamp = datetime.now().isoformat()
+        for item in resolved_list:
+            item["thread"]["archived_at"] = timestamp
+            archived.append(item["thread"])
+
+        if not dry_run:
+            self.save_archive(self.plot_threads_archive, archived)
+
+        return len(resolved_list)
+
+    def archive_reviews(self, old_reviews_list, dry_run=False):
+        """归档旧审查报告"""
+        if not old_reviews_list:
+            return 0
+
+        # 加载现有归档
+        archived = self.load_archive(self.reviews_archive)
+
+        # 添加时间戳
+        timestamp = datetime.now().isoformat()
+        for item in old_reviews_list:
+            item["review"]["archived_at"] = timestamp
+            archived.append(item["review"])
+
+        if not dry_run:
+            self.save_archive(self.reviews_archive, archived)
+
+        return len(old_reviews_list)
+
+    def remove_from_state(self, state, inactive_chars, resolved_threads, old_reviews):
+        """从 state.json 中移除已归档的数据 (v5.0 entities_v3 格式)"""
+        # 移除不活跃角色 (v5.0: 从 entities_v3.角色 中移除)
+        if inactive_chars:
+            char_ids = {item["character"].get("id") for item in inactive_chars}
+            entities_v3 = state.get("entities_v3", {})
+            characters_dict = entities_v3.get("角色", {})
+            for char_id in char_ids:
+                if char_id in characters_dict:
+                    del characters_dict[char_id]
+
+        # 移除已归档的伏笔
+        if resolved_threads:
+            thread_ids = {
+                (item.get("thread", {}) or {}).get("content") or (item.get("thread", {}) or {}).get("description")
+                for item in resolved_threads
+            }
+            thread_ids = {t for t in thread_ids if isinstance(t, str) and t.strip()}
+
+            plot_threads = state.get("plot_threads", {}) or {}
+            if isinstance(plot_threads.get("foreshadowing"), list):
+                plot_threads["foreshadowing"] = [
+                    t for t in plot_threads["foreshadowing"]
+                    if not isinstance(t, dict) or (t.get("content") or t.get("description")) not in thread_ids
+                ]
+            if isinstance(plot_threads.get("resolved"), list):
+                plot_threads["resolved"] = [
+                    t for t in plot_threads["resolved"]
+                    if not isinstance(t, dict) or (t.get("content") or t.get("description")) not in thread_ids
+                ]
+            state["plot_threads"] = plot_threads
+
+        # 移除旧审查报告
+        if old_reviews:
+            review_keys = set()
+            for item in old_reviews:
+                review = item.get("review", {}) or {}
+                key = review.get("report") or review.get("reviewed_at") or review.get("date")
+                if isinstance(key, str) and key.strip():
+                    review_keys.add(key)
+
+            state["review_checkpoints"] = [
+                review for review in state.get("review_checkpoints", [])
+                if (review.get("report") or review.get("reviewed_at") or review.get("date")) not in review_keys
+            ]
+
+        return state
+
+    def run_auto_check(self, force=False, dry_run=False):
+        """自动归档检查"""
+        state = self.load_state()
+
+        # 检查触发条件
+        trigger = self.check_trigger_conditions(state)
+
+        if not force and not trigger["should_archive"]:
+            print("✅ 无需归档(触发条件未满足)")
+            print(f"   文件大小: {trigger['file_size_mb']:.2f} MB (阈值: {self.config['file_size_trigger_mb']} MB)")
+            print(f"   当前章节: {trigger['current_chapter']} (每 {self.config['chapter_trigger']} 章触发)")
+            return
+
+        print("🔍 开始归档检查...")
+        print(f"   文件大小: {trigger['file_size_mb']:.2f} MB")
+        print(f"   当前章节: {trigger['current_chapter']}")
+
+        # 识别可归档数据
+        inactive_chars = self.identify_inactive_characters(state)
+        resolved_threads = self.identify_resolved_plot_threads(state)
+        old_reviews = self.identify_old_reviews(state)
+
+        # 输出统计
+        print(f"\n📊 归档统计:")
+        print(f"   不活跃角色: {len(inactive_chars)}")
+        print(f"   已回收伏笔: {len(resolved_threads)}")
+        print(f"   旧审查报告: {len(old_reviews)}")
+
+        if not (inactive_chars or resolved_threads or old_reviews):
+            print("\n✅ 无需归档(无符合条件的数据)")
+            return
+
+        # Dry-run 模式
+        if dry_run:
+            print("\n🔍 [Dry-run] 将被归档的数据:")
+            if inactive_chars:
+                print("\n   不活跃角色:")
+                for item in inactive_chars[:5]:  # 只显示前 5 个
+                    print(f"   - {item['character']['name']} (超过 {item['inactive_chapters']} 章未出场)")
+            if resolved_threads:
+                print("\n   已回收伏笔:")
+                for item in resolved_threads[:5]:
+                    desc = item["thread"].get("content") or item["thread"].get("description") or ""
+                    print(f"   - {str(desc)[:30]}... (已回收 {item['chapters_since_resolved']} 章)")
+            if old_reviews:
+                print("\n   旧审查报告:")
+                for item in old_reviews[:5]:
+                    print(f"   - Ch{item['review_chapter']} ({item['chapters_since_review']} 章前)")
+            return
+
+        # 执行归档
+        chars_archived = self.archive_characters(inactive_chars, dry_run=dry_run)
+        threads_archived = self.archive_plot_threads(resolved_threads, dry_run=dry_run)
+        reviews_archived = self.archive_reviews(old_reviews, dry_run=dry_run)
+
+        # 从 state.json 中移除
+        state = self.remove_from_state(state, inactive_chars, resolved_threads, old_reviews)
+        self.save_state(state)
+
+        # 最终统计
+        print(f"\n✅ 归档完成:")
+        print(f"   角色归档: {chars_archived} → {self.characters_archive.name}")
+        print(f"   伏笔归档: {threads_archived} → {self.plot_threads_archive.name}")
+        print(f"   报告归档: {reviews_archived} → {self.reviews_archive.name}")
+
+        # 显示归档后的文件大小
+        new_size_mb = self.state_file.stat().st_size / (1024 * 1024)
+        saved_mb = trigger["file_size_mb"] - new_size_mb
+        print(f"\n💾 文件大小: {trigger['file_size_mb']:.2f} MB → {new_size_mb:.2f} MB (节省 {saved_mb:.2f} MB)")
+
+    def restore_character(self, name):
+        """恢复归档的角色(Priority 2 修复:同步恢复索引状态)"""
+        archived = self.load_archive(self.characters_archive)
+        state = self.load_state()
+
+        # 查找角色
+        char_to_restore = None
+        for char in archived:
+            if char["name"] == name:
+                char_to_restore = char
+                break
+
+        if not char_to_restore:
+            print(f"❌ 归档中未找到角色: {name}")
+            return
+
+        # 移除 archived_at 字段
+        char_to_restore.pop("archived_at", None)
+
+        # ✅ 原子性修复:先从归档中移除,再添加到 state.json
+        # 理由:即使崩溃,数据仍在归档中,可重新恢复,不会丢失或重复
+        archived = [char for char in archived if char["name"] != name]
+        self.save_archive(self.characters_archive, archived)
+
+        # 恢复到 state.json (v5.0: 添加到 entities_v3.角色)
+        if "entities_v3" not in state:
+            state["entities_v3"] = {"角色": {}, "地点": {}, "物品": {}, "势力": {}, "招式": {}}
+        if "角色" not in state["entities_v3"]:
+            state["entities_v3"]["角色"] = {}
+
+        char_id = char_to_restore.get("id", char_to_restore.get("name", "unknown"))
+        state["entities_v3"]["角色"][char_id] = {
+            "canonical_name": char_to_restore.get("name", char_id),
+            "tier": char_to_restore.get("tier", "装饰"),
+            "desc": char_to_restore.get("desc", ""),
+            "current": char_to_restore.get("current", {}),
+            "first_appearance": char_to_restore.get("first_appearance", 0),
+            "last_appearance": char_to_restore.get("last_appearance", 0),
+            "history": char_to_restore.get("history", [])
+        }
+        self.save_state(state)
+
+        # ✅ Priority 2 修复:同步恢复索引状态为 'active'
+        try:
+            import sys
+            from pathlib import Path
+            script_dir = Path(__file__).parent
+            sys.path.insert(0, str(script_dir))
+            from structured_index import StructuredIndex
+
+            project_root = self.state_file.parent.parent
+            index = StructuredIndex(str(project_root))
+            index.mark_character_active(name)
+        except Exception as e:
+            print(f"⚠️ 索引状态恢复失败(不影响数据恢复): {e}")
+
+        print(f"✅ 角色已恢复: {name}")
+
+    def show_stats(self):
+        """显示归档统计"""
+        chars = self.load_archive(self.characters_archive)
+        threads = self.load_archive(self.plot_threads_archive)
+        reviews = self.load_archive(self.reviews_archive)
+
+        print("📊 归档统计:")
+        print(f"   角色归档: {len(chars)}")
+        print(f"   伏笔归档: {len(threads)}")
+        print(f"   报告归档: {len(reviews)}")
+
+        # 计算归档文件大小
+        total_size = 0
+        for archive_file in [self.characters_archive, self.plot_threads_archive, self.reviews_archive]:
+            if archive_file.exists():
+                total_size += archive_file.stat().st_size
+
+        print(f"   归档大小: {total_size / 1024:.2f} KB")
+
+        # 显示 state.json 大小
+        state_size_mb = self.state_file.stat().st_size / (1024 * 1024)
+        print(f"\n💾 state.json 当前大小: {state_size_mb:.2f} MB")
+
+
+def main():
+    parser = argparse.ArgumentParser(description="state.json 数据归档管理")
+
+    parser.add_argument("--auto-check", action="store_true", help="自动归档检查")
+    parser.add_argument("--force", action="store_true", help="强制归档(忽略触发条件)")
+    parser.add_argument("--dry-run", action="store_true", help="Dry-run 模式(仅显示将被归档的数据)")
+    parser.add_argument("--restore-character", metavar="NAME", help="恢复归档的角色")
+    parser.add_argument("--stats", action="store_true", help="显示归档统计")
+    parser.add_argument("--project-root", metavar="PATH", help="项目根目录(默认为当前目录)")
+
+    args = parser.parse_args()
+
+    # 创建管理器(支持从仓库根目录运行)
+    project_root = args.project_root
+    if project_root is None and not (Path.cwd() / ".webnovel" / "state.json").exists():
+        try:
+            project_root = str(resolve_project_root())
+        except FileNotFoundError:
+            project_root = None
+
+    manager = ArchiveManager(project_root=project_root)
+
+    # 执行操作
+    if args.auto_check or args.force:
+        manager.run_auto_check(force=args.force, dry_run=args.dry_run)
+    elif args.restore_character:
+        manager.restore_character(args.restore_character)
+    elif args.stats:
+        manager.show_stats()
+    else:
+        parser.print_help()
+
+
+if __name__ == "__main__":
+    main()

+ 471 - 0
.claude/scripts/backup_manager.py

@@ -0,0 +1,471 @@
+#!/usr/bin/env python3
+"""
+Git 集成备份管理系统 (Backup Manager with Git)
+
+核心理念:写 200万字必然会"写废设定",需要支持任意时间点回滚。
+
+🔧 重大升级:使用 Git 进行原子性版本控制
+
+为什么选择 Git:
+1. ✅ 原子性回滚:state.json + 正文/*.md 同时回滚,数据 100% 一致
+2. ✅ 增量存储:只存储 diff,节省 95% 空间
+3. ✅ 成熟稳定:经过 20 年验证的版本控制系统
+4. ✅ 分支管理:天然支持"平行世界"创作
+
+功能:
+1. 自动 Git 提交:每次 /webnovel-write 完成后自动 commit
+2. 原子性回滚:git checkout 同时回滚所有文件
+3. 版本历史:git log 查看完整历史
+4. 差异对比:git diff 查看任意两个版本的差异
+5. 分支创建:git branch 从任意时间点创建分支
+
+使用方式:
+  # 在第 45 章完成后自动备份(自动 git commit)
+  python backup_manager.py --chapter 45
+
+  # 回滚到第 30 章状态(git checkout)
+  python backup_manager.py --rollback 30
+
+  # 查看第 20 章和第 40 章的差异(git diff)
+  python backup_manager.py --diff 20 40
+
+  # 从第 50 章创建分支(git branch)
+  python backup_manager.py --create-branch 50 --branch-name "alternative-ending"
+
+  # 列出所有备份(git log)
+  python backup_manager.py --list
+
+Git 提交规范:
+  - 提交信息格式: "Chapter {N}: {章节标题}"
+  - Tag 格式: "ch{N}" (如 ch0045)
+  - 每个章节对应一个 commit + 一个 tag
+
+数据一致性保证:
+  ✅ 回滚时,state.json 和所有 .md 文件同步回滚
+  ✅ 不会出现"状态记录筑基期,但文件里写着金丹期"的数据撕裂
+  ✅ 原子性操作,要么全部成功,要么全部失败
+"""
+
+import subprocess
+import json
+import os
+import sys
+import shutil
+from pathlib import Path
+from datetime import datetime
+from typing import Optional, List, Tuple
+
+# ============================================================================
+# 安全修复:导入安全工具函数(P1 MEDIUM)
+# ============================================================================
+from security_utils import sanitize_commit_message, is_git_available, is_git_repo, git_graceful_operation
+from project_locator import resolve_project_root
+
+# Windows 编码兼容性修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+class GitBackupManager:
+    """基于 Git 的备份管理器(支持优雅降级)"""
+
+    def __init__(self, project_root: str):
+        self.project_root = Path(project_root)
+        self.git_dir = self.project_root / ".git"
+        self.git_available = is_git_available()
+
+        if not self.git_available:
+            print("⚠️  Git 不可用,将使用本地备份模式")
+            print("💡 如需启用 Git 版本控制,请安装 Git: https://git-scm.com/")
+            return
+
+        # 检查 Git 是否初始化
+        if not self.git_dir.exists():
+            print("⚠️  Git 未初始化,请先运行 /webnovel-init 或手动执行 git init")
+            print("💡 现在自动初始化 Git...")
+            self._init_git()
+
+    def _init_git(self) -> bool:
+        """初始化 Git 仓库"""
+        try:
+            # git init
+            subprocess.run(
+                ["git", "init"],
+                cwd=self.project_root,
+                check=True,
+                capture_output=True
+            )
+
+            # 创建 .gitignore
+            gitignore_file = self.project_root / ".gitignore"
+            if not gitignore_file.exists():
+                with open(gitignore_file, 'w', encoding='utf-8') as f:
+                    f.write("""# Python
+__pycache__/
+*.py[cod]
+*.so
+
+# Temporary files
+*.tmp
+*.bak
+.DS_Store
+
+# IDE
+.vscode/
+.idea/
+
+# Don't ignore .webnovel (we need to track state.json)
+# But ignore cache files
+.webnovel/context_cache.json
+""")
+
+            # 初始提交
+            subprocess.run(
+                ["git", "add", "."],
+                cwd=self.project_root,
+                check=True,
+                capture_output=True
+            )
+
+            subprocess.run(
+                ["git", "commit", "-m", "Initial commit: Project initialized"],
+                cwd=self.project_root,
+                check=True,
+                capture_output=True
+            )
+
+            print("✅ Git 仓库已初始化")
+            return True
+
+        except subprocess.CalledProcessError as e:
+            print(f"❌ Git 初始化失败: {e}")
+            return False
+
+    def _run_git_command(self, args: List[str], check: bool = True) -> Tuple[bool, str]:
+        """执行 Git 命令(支持优雅降级)"""
+        if not self.git_available:
+            return False, "Git 不可用"
+
+        try:
+            result = subprocess.run(
+                ["git"] + args,
+                cwd=self.project_root,
+                check=check,
+                capture_output=True,
+                text=True,
+                encoding='utf-8',
+                timeout=60
+            )
+
+            return True, result.stdout
+
+        except subprocess.CalledProcessError as e:
+            return False, e.stderr
+        except subprocess.TimeoutExpired:
+            return False, "Git 命令超时"
+        except OSError as e:
+            return False, str(e)
+
+    def _local_backup(self, chapter_num: int) -> bool:
+        """本地备份(Git 不可用时的降级方案)"""
+        backup_dir = self.project_root / ".webnovel" / "backups"
+        backup_dir.mkdir(parents=True, exist_ok=True)
+
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        backup_name = f"ch{chapter_num:04d}_{timestamp}"
+        backup_path = backup_dir / backup_name
+
+        try:
+            # 备份 state.json
+            state_file = self.project_root / ".webnovel" / "state.json"
+            if state_file.exists():
+                backup_path.mkdir(parents=True, exist_ok=True)
+                shutil.copy2(state_file, backup_path / "state.json")
+
+            print(f"✅ 本地备份完成: {backup_path}")
+            return True
+        except OSError as e:
+            print(f"❌ 本地备份失败: {e}")
+            return False
+
+    def backup(self, chapter_num: int, chapter_title: str = "") -> bool:
+        """
+        备份当前状态(Git commit + tag,或本地备份)
+
+        Args:
+            chapter_num: 章节号
+            chapter_title: 章节标题(可选)
+        """
+        print(f"📝 正在备份第 {chapter_num} 章...")
+
+        # 如果 Git 不可用,使用本地备份
+        if not self.git_available:
+            return self._local_backup(chapter_num)
+
+        # Step 1: git add .
+        success, output = self._run_git_command(["add", "."])
+        if not success:
+            print(f"❌ git add 失败: {output}")
+            return False
+
+        # Step 2: git commit
+        commit_message = f"Chapter {chapter_num}"
+        if chapter_title:
+            # ============================================================================
+            # 安全修复:清理提交消息,防止命令注入 (CWE-77) - P1 MEDIUM
+            # 原代码: commit_message += f": {chapter_title}"
+            # 漏洞: chapter_title可能包含 Git 标志(如 --author, --amend)导致命令注入
+            # ============================================================================
+            safe_chapter_title = sanitize_commit_message(chapter_title)
+            commit_message += f": {safe_chapter_title}"
+
+        success, output = self._run_git_command(
+            ["commit", "-m", commit_message],
+            check=False  # 允许"无变更"的情况
+        )
+
+        if not success and "nothing to commit" in output:
+            print("⚠️  无变更,跳过提交")
+            return True
+        elif not success:
+            print(f"❌ git commit 失败: {output}")
+            return False
+
+        print(f"✅ Git 提交完成: {commit_message}")
+
+        # Step 3: git tag
+        tag_name = f"ch{chapter_num:04d}"
+
+        # 删除旧 tag(如果存在)
+        self._run_git_command(["tag", "-d", tag_name], check=False)
+
+        success, output = self._run_git_command(["tag", tag_name])
+        if not success:
+            print(f"⚠️  创建 tag 失败(非致命): {output}")
+        else:
+            print(f"✅ Git tag 已创建: {tag_name}")
+
+        return True
+
+    def rollback(self, chapter_num: int) -> bool:
+        """
+        回滚到指定章节(Git checkout)
+
+        ⚠️ 警告:这会丢弃所有未提交的变更!
+        """
+
+        tag_name = f"ch{chapter_num:04d}"
+
+        print(f"🔄 正在回滚到第 {chapter_num} 章...")
+        print(f"⚠️  警告:这将丢弃所有未提交的变更!")
+
+        # 检查是否有未提交的变更
+        success, status_output = self._run_git_command(["status", "--porcelain"])
+
+        if status_output.strip():
+            print("\n⚠️  检测到未提交的变更:")
+            print(status_output)
+
+            # 创建备份提交
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+            backup_branch = f"backup_before_rollback_{timestamp}"
+
+            print(f"\n💾 正在创建备份分支: {backup_branch}")
+
+            success, _ = self._run_git_command(["checkout", "-b", backup_branch])
+            if not success:
+                print("❌ 创建备份分支失败")
+                return False
+
+            success, _ = self._run_git_command(["add", "."])
+            success, _ = self._run_git_command(
+                ["commit", "-m", f"Backup before rollback to chapter {chapter_num}"]
+            )
+
+            print(f"✅ 备份分支已创建: {backup_branch}")
+
+            # 切换回 master
+            success, _ = self._run_git_command(["checkout", "master"])
+
+        # 执行回滚
+        success, output = self._run_git_command(["checkout", tag_name])
+
+        if not success:
+            print(f"❌ 回滚失败: {output}")
+            print(f"💡 提示:确保 tag '{tag_name}' 存在(运行 --list 查看所有备份)")
+            return False
+
+        print(f"✅ 已回滚到第 {chapter_num} 章!")
+        print(f"\n💡 提示:")
+        print(f"  - 所有文件(state.json + 正文/*.md)已同步回滚")
+        print(f"  - 如需恢复,运行: git checkout master")
+
+        return True
+
+    def diff(self, chapter_a: int, chapter_b: int):
+        """对比两个版本的差异(Git diff)"""
+
+        tag_a = f"ch{chapter_a:04d}"
+        tag_b = f"ch{chapter_b:04d}"
+
+        print(f"📊 对比第 {chapter_a} 章 与 第 {chapter_b} 章的差异...\n")
+
+        success, output = self._run_git_command(["diff", tag_a, tag_b, "--stat"])
+
+        if not success:
+            print(f"❌ 对比失败: {output}")
+            return
+
+        print("📈 文件变更统计:")
+        print(output)
+
+        # 显示 state.json 的详细差异
+        print("\n📝 state.json 详细差异:")
+        success, state_diff = self._run_git_command(
+            ["diff", tag_a, tag_b, "--", ".webnovel/state.json"]
+        )
+
+        if success and state_diff:
+            print(state_diff[:2000])  # 限制输出长度
+            if len(state_diff) > 2000:
+                print("\n...(输出过长,已截断)")
+        else:
+            print("(无变更)")
+
+    def list_backups(self):
+        """列出所有备份(Git log + tags)"""
+
+        print("\n📚 备份列表(Git tags):\n")
+
+        # 获取所有 tags
+        success, tags_output = self._run_git_command(["tag", "-l", "ch*"])
+
+        if not success or not tags_output:
+            print("⚠️  暂无备份")
+            return
+
+        tags = sorted(tags_output.strip().split('\n'))
+
+        for tag in tags:
+            # 提取章节号
+            chapter_num = int(tag[2:])
+
+            # 获取该 tag 的提交信息
+            success, commit_info = self._run_git_command(
+                ["log", tag, "-1", "--format=%h %ci %s"]
+            )
+
+            if success:
+                print(f"📖 {tag} | {commit_info.strip()}")
+
+        print(f"\n总计:{len(tags)} 个备份")
+
+        # 显示最近 5 次提交
+        print("\n📜 最近提交历史:\n")
+        success, log_output = self._run_git_command(
+            ["log", "--oneline", "-5"]
+        )
+
+        if success:
+            print(log_output)
+
+    def create_branch(self, chapter_num: int, branch_name: str) -> bool:
+        """从指定章节创建分支(Git branch)"""
+
+        tag_name = f"ch{chapter_num:04d}"
+
+        print(f"🌿 从第 {chapter_num} 章创建分支: {branch_name}")
+
+        # 检查 tag 是否存在
+        success, _ = self._run_git_command(["rev-parse", tag_name], check=False)
+
+        if not success:
+            print(f"❌ Tag '{tag_name}' 不存在")
+            return False
+
+        # 创建分支
+        success, output = self._run_git_command(["branch", branch_name, tag_name])
+
+        if not success:
+            print(f"❌ 创建分支失败: {output}")
+            return False
+
+        print(f"✅ 分支已创建: {branch_name}")
+        print(f"\n💡 切换到分支:")
+        print(f"  git checkout {branch_name}")
+
+        return True
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description="Git 集成备份管理系统",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 在第 45 章完成后自动备份
+  python backup_manager.py --chapter 45
+
+  # 回滚到第 30 章(原子性:state.json + 所有 .md 文件)
+  python backup_manager.py --rollback 30
+
+  # 查看第 20 章和第 40 章的差异
+  python backup_manager.py --diff 20 40
+
+  # 从第 50 章创建分支
+  python backup_manager.py --create-branch 50 --branch-name "alternative-ending"
+
+  # 列出所有备份
+  python backup_manager.py --list
+        """
+    )
+
+    parser.add_argument('--chapter', type=int, help='备份章节号')
+    parser.add_argument('--chapter-title', help='章节标题(可选)')
+    parser.add_argument('--rollback', type=int, metavar='CHAPTER', help='回滚到指定章节')
+    parser.add_argument('--diff', nargs=2, type=int, metavar=('A', 'B'), help='对比两个版本')
+    parser.add_argument('--create-branch', type=int, metavar='CHAPTER', help='从指定章节创建分支')
+    parser.add_argument('--branch-name', help='分支名称')
+    parser.add_argument('--list', action='store_true', help='列出所有备份')
+    parser.add_argument('--project-root', default='.', help='项目根目录')
+
+    args = parser.parse_args()
+
+    # 解析项目根目录(支持从仓库根目录运行)
+    project_root = args.project_root
+    if project_root == '.' and not (Path('.') / '.webnovel' / 'state.json').exists():
+        try:
+            project_root = str(resolve_project_root())
+        except FileNotFoundError:
+            # 维持向后兼容:仍然使用用户提供的 cwd
+            project_root = args.project_root
+
+    # 创建管理器
+    manager = GitBackupManager(project_root)
+
+    # 执行操作
+    if args.chapter:
+        manager.backup(args.chapter, args.chapter_title or "")
+
+    elif args.rollback:
+        manager.rollback(args.rollback)
+
+    elif args.diff:
+        manager.diff(args.diff[0], args.diff[1])
+
+    elif args.create_branch:
+        if not args.branch_name:
+            print("❌ 创建分支需要 --branch-name 参数")
+            sys.exit(1)
+        manager.create_branch(args.create_branch, args.branch_name)
+
+    elif args.list:
+        manager.list_backups()
+
+    else:
+        parser.print_help()
+
+if __name__ == "__main__":
+    main()

+ 84 - 0
.claude/scripts/chapter_paths.py

@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""
+Chapter file path helpers.
+
+This project has seen multiple chapter filename conventions:
+1) Legacy flat layout: 正文/第0007章.md
+2) Volume layout:    正文/第1卷/第007章-章节标题.md
+
+To keep scripts robust, always resolve chapter files via these helpers instead of hardcoding a format.
+"""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+from typing import Optional
+
+
+_CHAPTER_NUM_RE = re.compile(r"第(?P<num>\d+)章")
+
+
+def volume_num_for_chapter(chapter_num: int, *, chapters_per_volume: int = 50) -> int:
+    if chapter_num <= 0:
+        raise ValueError("chapter_num must be >= 1")
+    return (chapter_num - 1) // chapters_per_volume + 1
+
+
+def extract_chapter_num_from_filename(filename: str) -> Optional[int]:
+    m = _CHAPTER_NUM_RE.search(filename)
+    if not m:
+        return None
+    try:
+        return int(m.group("num"))
+    except ValueError:
+        return None
+
+
+def find_chapter_file(project_root: Path, chapter_num: int) -> Optional[Path]:
+    """
+    Find an existing chapter file for chapter_num under project_root/正文.
+    Returns the first match (stable sorted order) or None if not found.
+    """
+    chapters_dir = project_root / "正文"
+    if not chapters_dir.exists():
+        return None
+
+    legacy = chapters_dir / f"第{chapter_num:04d}章.md"
+    if legacy.exists():
+        return legacy
+
+    vol_dir = chapters_dir / f"第{volume_num_for_chapter(chapter_num)}卷"
+    if vol_dir.exists():
+        candidates = sorted(vol_dir.glob(f"第{chapter_num:03d}章*.md")) + sorted(vol_dir.glob(f"第{chapter_num:04d}章*.md"))
+        for c in candidates:
+            if c.is_file():
+                return c
+
+    # Fallback: search anywhere under 正文/ (supports custom layouts)
+    candidates = sorted(chapters_dir.rglob(f"第{chapter_num:03d}章*.md")) + sorted(chapters_dir.rglob(f"第{chapter_num:04d}章*.md"))
+    for c in candidates:
+        if c.is_file():
+            return c
+
+    return None
+
+
+def default_chapter_draft_path(project_root: Path, chapter_num: int, *, use_volume_layout: bool = False) -> Path:
+    """
+    Preferred draft path when creating a new chapter file.
+
+    Args:
+        project_root: 项目根目录
+        chapter_num: 章节号
+        use_volume_layout: True 使用卷布局 (正文/第N卷/第NNN章.md),False 使用平坦布局 (正文/第NNNN章.md)
+
+    Default is flat layout to match SKILL.md documentation.
+    """
+    if use_volume_layout:
+        vol_dir = project_root / "正文" / f"第{volume_num_for_chapter(chapter_num)}卷"
+        return vol_dir / f"第{chapter_num:03d}章.md"
+    else:
+        # Flat layout: 正文/第NNNN章.md (matches SKILL.md)
+        return project_root / "正文" / f"第{chapter_num:04d}章.md"
+

+ 514 - 0
.claude/scripts/context_pack_builder.py

@@ -0,0 +1,514 @@
+#!/usr/bin/env python3
+"""
+Context Pack Builder v4.0
+
+为章节写作生成结构化上下文包,取代直接读取 state.json。
+
+输出 Schema:
+{
+  "core": {
+    "chapter_outline": "本章大纲内容",
+    "protagonist_snapshot": {...},
+    "recent_summaries": [{...}, ...]
+  },
+  "scene": {
+    "location_context": {...},
+    "appearing_characters": [{entity_id, name, snapshot}, ...],
+    "urgent_foreshadowing": [{...}, ...]
+  },
+  "global": {
+    "worldview_skeleton": "...",
+    "power_system_skeleton": "...",
+    "style_contract_ref": "..."
+  }
+}
+
+使用方式:
+  python context_pack_builder.py --chapter 45 --project-root /path/to/project
+  python context_pack_builder.py --chapter 45 --output /tmp/context_pack.json
+"""
+
+import json
+import os
+import sys
+import argparse
+import re
+import sqlite3
+from pathlib import Path
+from typing import Optional, Dict, List, Any
+
+# 导入项目工具
+from project_locator import resolve_project_root
+from chapter_paths import find_chapter_file
+
+# 导入配置
+try:
+    from data_modules.config import get_config, DataModulesConfig
+except ImportError:
+    from scripts.data_modules.config import get_config, DataModulesConfig
+
+
+class ContextPackBuilder:
+    """上下文包构建器"""
+
+    def __init__(self, project_root: Path = None):
+        if project_root is None:
+            try:
+                project_root = resolve_project_root()
+            except FileNotFoundError:
+                project_root = Path.cwd()
+        else:
+            project_root = Path(project_root)
+
+        self.project_root = project_root
+        self.config = get_config(project_root)
+        self.state_file = project_root / ".webnovel" / "state.json"
+        self.index_db = project_root / ".webnovel" / "index.db"
+        self.outline_dir = project_root / "大纲"
+        self.settings_dir = project_root / "设定集"
+        self.chapters_dir = project_root / "正文"
+
+        self._conn: Optional[sqlite3.Connection] = None
+
+    def _conn_index(self) -> Optional[sqlite3.Connection]:
+        if self._conn is not None:
+            return self._conn
+        if not self.index_db.exists():
+            return None
+        conn = sqlite3.connect(str(self.index_db))
+        conn.row_factory = sqlite3.Row
+        self._conn = conn
+        return conn
+
+    def build(self, chapter_num: int) -> Dict[str, Any]:
+        """构建完整上下文包"""
+        state = self._load_state()
+
+        return {
+            "meta": {
+                "chapter": chapter_num,
+                "project_root": str(self.project_root),
+                "version": "5.0"
+            },
+            "core": self._build_core(chapter_num),
+            "scene": self._build_scene(chapter_num),
+            "global": self._build_global(),
+            "alerts": self._build_alerts(state)
+        }
+
+    def _build_core(self, chapter_num: int) -> Dict[str, Any]:
+        """核心上下文:大纲、主角状态、近期摘要"""
+        state = self._load_state()
+
+        return {
+            "chapter_outline": self._get_chapter_outline(chapter_num),
+            "protagonist_snapshot": self._get_protagonist_snapshot(state),
+            "recent_summaries": self._get_recent_summaries(
+                chapter_num, window=self.config.context_recent_summaries_window
+            )
+        }
+
+    def _build_scene(self, chapter_num: int) -> Dict[str, Any]:
+        """场景上下文:地点、出场角色、紧急伏笔"""
+        state = self._load_state()
+
+        # 从大纲推断本章地点和角色
+        outline = self._get_chapter_outline(chapter_num)
+        predicted_location = self._predict_location(outline, state)
+        predicted_characters = self._predict_characters(outline, state)
+
+        return {
+            "location_context": predicted_location,
+            "appearing_characters": predicted_characters,
+            "urgent_foreshadowing": self._get_urgent_foreshadowing(state, chapter_num)
+        }
+
+    def _build_global(self) -> Dict[str, Any]:
+        """全局上下文:世界观、力量体系、风格契约"""
+        return {
+            "worldview_skeleton": self._load_skeleton("世界观"),
+            "power_system_skeleton": self._load_skeleton("力量体系"),
+            "style_contract_ref": self._get_style_contract_ref()
+        }
+
+    def _build_alerts(self, state: Dict) -> Dict[str, Any]:
+        """风险提示:消歧警告、待确认项(v5.0)"""
+        slice_size = self.config.context_alerts_slice
+        return {
+            "disambiguation_warnings": state.get("disambiguation_warnings", [])[-slice_size:],
+            "disambiguation_pending": state.get("disambiguation_pending", [])[-slice_size:]
+        }
+
+    # ================== 辅助方法 ==================
+
+    def _load_state(self) -> Dict:
+        """加载 state.json"""
+        if not self.state_file.exists():
+            return {}
+        with open(self.state_file, 'r', encoding='utf-8') as f:
+            return json.load(f)
+
+    def _get_chapter_outline(self, chapter_num: int) -> str:
+        """获取本章大纲"""
+        # 尝试多种大纲文件格式
+        patterns = [
+            f"第{chapter_num}章*.md",
+            f"第{chapter_num:02d}章*.md",
+            f"第{chapter_num:03d}章*.md",
+            f"第{chapter_num:04d}章*.md",
+            f"章纲/第{chapter_num}章*.md",
+            f"章纲/第{chapter_num:02d}章*.md",
+        ]
+
+        for pattern in patterns:
+            matches = list(self.outline_dir.glob(pattern))
+            if matches:
+                with open(matches[0], 'r', encoding='utf-8') as f:
+                    return f.read()
+
+        # 尝试从卷纲中提取
+        volume_outline = self._extract_from_volume_outline(chapter_num)
+        if volume_outline:
+            return volume_outline
+
+        return f"[大纲未找到: 第{chapter_num}章]"
+
+    def _extract_from_volume_outline(self, chapter_num: int) -> Optional[str]:
+        """从卷纲中提取章节大纲"""
+        volume_files = list(self.outline_dir.glob("卷纲*.md")) + list(self.outline_dir.glob("*卷*.md"))
+
+        for vf in volume_files:
+            with open(vf, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            # 查找章节标记
+            pattern = rf'第{chapter_num}章[^\n]*\n(.*?)(?=第\d+章|$)'
+            match = re.search(pattern, content, re.DOTALL)
+            if match:
+                return match.group(0).strip()
+
+        return None
+
+    def _get_protagonist_snapshot(self, state: Dict) -> Dict:
+        """获取主角状态快照"""
+        protagonist = state.get("protagonist_state", {}) or {}
+        power = protagonist.get("power", {}) or {}
+        location = protagonist.get("location", {}) or {}
+
+        snapshot: Dict[str, Any] = {
+            "entity_id": str(protagonist.get("entity_id", "") or "").strip(),
+            "name": str(protagonist.get("name", "") or "").strip() or "主角",
+            "realm": str(power.get("realm", "") or "").strip(),
+            "layer": power.get("layer", 0),
+            "bottleneck": str(power.get("bottleneck", "") or "").strip(),
+            "golden_finger": protagonist.get("golden_finger", {}) or {},
+            "location": str(location.get("current", "") or "").strip(),
+        }
+
+        # 可选:从 index.db 补齐(以 entity_id 为准)
+        protagonist_id = snapshot.get("entity_id", "")
+        conn = self._conn_index()
+        if protagonist_id and conn is not None:
+            row = conn.execute(
+                "SELECT canonical_name FROM entities WHERE entity_id = ? LIMIT 1",
+                (protagonist_id,),
+            ).fetchone()
+            if row and row["canonical_name"]:
+                snapshot["name"] = row["canonical_name"]
+
+            kv_rows = conn.execute(
+                "SELECT key, value FROM entity_kv WHERE entity_id = ?",
+                (protagonist_id,),
+            ).fetchall()
+
+            def _parse(v: str):
+                try:
+                    return json.loads(v)
+                except Exception:
+                    return v
+
+            kv = {r["key"]: _parse(r["value"]) for r in kv_rows} if kv_rows else {}
+            if isinstance(kv.get("realm"), str) and kv.get("realm"):
+                snapshot["realm"] = kv["realm"]
+            if kv.get("layer") is not None and kv.get("layer") != "":
+                snapshot["layer"] = kv["layer"]
+            if isinstance(kv.get("bottleneck"), str) and kv.get("bottleneck"):
+                snapshot["bottleneck"] = kv["bottleneck"]
+            if isinstance(kv.get("location"), str) and kv.get("location"):
+                snapshot["location"] = kv["location"]
+
+        return snapshot
+
+    def _get_recent_summaries(self, chapter_num: int, window: int = 5) -> List[Dict]:
+        """获取最近 N 章的摘要"""
+        summaries = []
+        start = max(1, chapter_num - window)
+
+        for ch in range(start, chapter_num):
+            chapter_file = find_chapter_file(self.project_root, ch)
+            if chapter_file and chapter_file.exists():
+                summary = self._extract_summary_from_chapter(chapter_file, ch)
+                if summary:
+                    summaries.append(summary)
+
+        return summaries
+
+    def _extract_summary_from_chapter(self, chapter_file: Path, chapter_num: int) -> Optional[Dict]:
+        """从章节文件中提取摘要"""
+        with open(chapter_file, 'r', encoding='utf-8') as f:
+            content = f.read()
+
+        # 查找摘要区块
+        summary_match = re.search(r'## 本章摘要\s*\n(.*?)(?=\n##|$)', content, re.DOTALL)
+        if summary_match:
+            summary_text = summary_match.group(1).strip()
+            return {
+                "chapter": chapter_num,
+                "summary": summary_text
+            }
+
+        # 没有摘要,返回章节标题
+        title_match = re.match(r'^#\s*(.+)', content)
+        title = title_match.group(1).strip() if title_match else f"第{chapter_num}章"
+
+        return {
+            "chapter": chapter_num,
+            "title": title,
+            "summary": None
+        }
+
+    def _predict_location(self, outline: str, state: Dict) -> Dict:
+        """从大纲推断地点(优先使用 index.db 别名表)"""
+        conn = self._conn_index()
+        if conn is None:
+            return {"name": "未知地点", "desc": ""}
+
+        rows = conn.execute(
+            "SELECT alias, entity_id FROM entity_aliases WHERE entity_type = ?",
+            ("地点",),
+        ).fetchall()
+        if not rows:
+            return {"name": "未知地点", "desc": ""}
+
+        # 先匹配更长的别名,降低误命中
+        candidates = sorted(
+            ((r["alias"], r["entity_id"]) for r in rows if r["alias"]),
+            key=lambda x: len(x[0]),
+            reverse=True,
+        )
+        for alias, entity_id in candidates:
+            if len(alias) < 2:
+                continue
+            if alias not in outline:
+                continue
+
+            e = conn.execute(
+                "SELECT canonical_name, desc FROM entities WHERE entity_id = ? LIMIT 1",
+                (entity_id,),
+            ).fetchone()
+            return {
+                "entity_id": entity_id,
+                "name": (e["canonical_name"] if e else "") or alias,
+                "desc": (e["desc"] if e else "") or "",
+                "match": alias,
+            }
+
+        return {"name": "未知地点", "desc": ""}
+
+    def _predict_characters(self, outline: str, state: Dict) -> List[Dict]:
+        """从大纲推断出场角色(优先使用 index.db 别名表)"""
+        conn = self._conn_index()
+        if conn is None:
+            return []
+
+        rows = conn.execute(
+            "SELECT alias, entity_id FROM entity_aliases WHERE entity_type = ?",
+            ("角色",),
+        ).fetchall()
+        if not rows:
+            return []
+
+        matched_ids: set[str] = set()
+        for r in rows:
+            alias = r["alias"] or ""
+            if len(alias) < 2:
+                continue
+            if alias in outline:
+                matched_ids.add(r["entity_id"])
+
+        if not matched_ids:
+            return []
+
+        def _parse(v: str):
+            try:
+                return json.loads(v)
+            except Exception:
+                return v
+
+        tier_order = {"核心": 0, "支线": 1, "装饰": 2, "": 3}
+        matched: List[Dict[str, Any]] = []
+        for entity_id in matched_ids:
+            e = conn.execute(
+                "SELECT canonical_name, tier FROM entities WHERE entity_id = ? LIMIT 1",
+                (entity_id,),
+            ).fetchone()
+            if not e:
+                continue
+
+            kv_rows = conn.execute(
+                "SELECT key, value FROM entity_kv WHERE entity_id = ?",
+                (entity_id,),
+            ).fetchall()
+            snapshot = {r["key"]: _parse(r["value"]) for r in kv_rows} if kv_rows else {}
+
+            matched.append(
+                {
+                    "entity_id": entity_id,
+                    "name": e["canonical_name"] or entity_id,
+                    "tier": e["tier"] or "",
+                    "snapshot": snapshot,
+                }
+            )
+
+        matched.sort(key=lambda x: tier_order.get(x.get("tier", ""), 3))
+        return matched[:self.config.context_max_appearing_characters]
+
+    def _get_urgent_foreshadowing(self, state: Dict, chapter_num: int) -> List[Dict]:
+        """获取紧急伏笔(优先使用 index.db 伏笔索引)"""
+        conn = self._conn_index()
+        if conn is not None:
+            try:
+                rows = conn.execute(
+                    "SELECT content, introduced_chapter, resolved_chapter, status, urgency, location "
+                    "FROM foreshadowing_index WHERE status = '未回收' ORDER BY urgency DESC LIMIT 5"
+                ).fetchall()
+                return [dict(r) for r in rows] if rows else []
+            except sqlite3.Error:
+                pass
+
+        # fallback:项目未建索引时直接读取 state.json
+        plot_threads = state.get("plot_threads", {}) or {}
+        items = plot_threads.get("foreshadowing", []) or []
+        urgent: List[Dict[str, Any]] = []
+
+        for fs in items:
+            if not isinstance(fs, dict):
+                continue
+            status = str(fs.get("status", "")).strip()
+            if status in {"已回收"}:
+                continue
+
+            planted_chapter = fs.get("planted_chapter") or fs.get("introduced_chapter") or 0
+            target_chapter = fs.get("target_chapter") or fs.get("target") or 0
+            try:
+                planted_chapter = int(planted_chapter)
+            except (TypeError, ValueError):
+                planted_chapter = 0
+            try:
+                target_chapter = int(target_chapter) if target_chapter else 0
+            except (TypeError, ValueError):
+                target_chapter = 0
+
+            chapters_pending = chapter_num - planted_chapter if planted_chapter else 0
+
+            # 使用配置的紧急度阈值
+            cfg = self.config
+            if chapters_pending > cfg.foreshadowing_urgency_pending_high:
+                urgency = cfg.foreshadowing_urgency_score_high
+            elif chapters_pending > cfg.foreshadowing_urgency_pending_medium:
+                urgency = cfg.foreshadowing_urgency_score_medium
+            elif target_chapter and chapter_num >= target_chapter - cfg.foreshadowing_urgency_target_proximity:
+                urgency = cfg.foreshadowing_urgency_score_target
+            else:
+                urgency = cfg.foreshadowing_urgency_score_low
+
+            if urgency >= cfg.foreshadowing_urgency_threshold_show:
+                urgent.append(
+                    {
+                        "content": fs.get("content") or fs.get("description") or "",
+                        "planted_chapter": planted_chapter,
+                        "target_chapter": target_chapter,
+                        "tier": fs.get("tier", ""),
+                        "urgency": urgency,
+                    }
+                )
+
+        urgent.sort(key=lambda x: x.get("urgency", 0), reverse=True)
+        return urgent[:self.config.context_max_urgent_foreshadowing]
+
+    def _load_skeleton(self, setting_type: str) -> str:
+        """加载设定骨架"""
+        patterns = [
+            f"{setting_type}.md",
+            f"{setting_type}/*.md",
+            f"*{setting_type}*.md"
+        ]
+
+        for pattern in patterns:
+            matches = list(self.settings_dir.glob(pattern))
+            if matches:
+                # 如果是目录,合并所有文件
+                if matches[0].is_dir():
+                    content = []
+                    for f in sorted(matches[0].glob("*.md")):
+                        with open(f, 'r', encoding='utf-8') as file:
+                            content.append(f"## {f.stem}\n{file.read()}")
+                    return "\n\n".join(content)
+                else:
+                    with open(matches[0], 'r', encoding='utf-8') as f:
+                        return f.read()
+
+        return f"[{setting_type}设定未找到]"
+
+    def _get_style_contract_ref(self) -> str:
+        """获取风格契约引用"""
+        style_file = self.settings_dir / "风格契约.md"
+        if style_file.exists():
+            with open(style_file, 'r', encoding='utf-8') as f:
+                return f.read()
+
+        # 检查其他可能的位置
+        for pattern in ["风格*.md", "写作风格*.md", "style*.md"]:
+            matches = list(self.settings_dir.glob(pattern))
+            if matches:
+                with open(matches[0], 'r', encoding='utf-8') as f:
+                    return f.read()
+
+        return "[风格契约未定义]"
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Context Pack Builder v4.0")
+    parser.add_argument("--chapter", type=int, required=True, help="章节编号")
+    parser.add_argument("--project-root", metavar="PATH", help="项目根目录")
+    parser.add_argument("--output", metavar="FILE", help="输出文件路径(默认输出到 stdout)")
+    parser.add_argument("--pretty", action="store_true", help="格式化 JSON 输出")
+
+    args = parser.parse_args()
+
+    # 构建上下文包
+    builder = ContextPackBuilder(project_root=args.project_root)
+    context_pack = builder.build(args.chapter)
+
+    # 输出
+    if args.pretty:
+        output = json.dumps(context_pack, ensure_ascii=False, indent=2)
+    else:
+        output = json.dumps(context_pack, ensure_ascii=False)
+
+    if args.output:
+        with open(args.output, 'w', encoding='utf-8') as f:
+            f.write(output)
+        print(f"✅ 上下文包已保存到: {args.output}")
+    else:
+        print(output)
+
+
+if __name__ == "__main__":
+    # Windows UTF-8 编码修复
+    if sys.platform == 'win32':
+        import io
+        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+    main()

+ 50 - 0
.claude/scripts/data_modules/__init__.py

@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Data Modules - 数据链模块包
+
+用于 webnovel-writer 的数据处理:
+- 实体消歧 (entity_linker)
+- 状态管理 (state_manager)
+- 索引管理 (index_manager)
+- RAG 检索 (rag_adapter)
+- 风格样本 (style_sampler)
+- API 客户端 (api_client) - 只有 Embed + Rerank
+"""
+
+from .config import DataModulesConfig, get_config, set_project_root
+from .api_client import ModalAPIClient, get_client
+from .entity_linker import EntityLinker, DisambiguationResult
+from .state_manager import StateManager, EntityState, Relationship, StateChange
+from .index_manager import IndexManager, ChapterMeta, SceneMeta
+from .rag_adapter import RAGAdapter, SearchResult
+from .style_sampler import StyleSampler, StyleSample, SceneType
+
+__all__ = [
+    # Config
+    "DataModulesConfig",
+    "get_config",
+    "set_project_root",
+    # API Client
+    "ModalAPIClient",
+    "get_client",
+    # Entity Linker
+    "EntityLinker",
+    "DisambiguationResult",
+    # State Manager
+    "StateManager",
+    "EntityState",
+    "Relationship",
+    "StateChange",
+    # Index Manager
+    "IndexManager",
+    "ChapterMeta",
+    "SceneMeta",
+    # RAG Adapter
+    "RAGAdapter",
+    "SearchResult",
+    # Style Sampler
+    "StyleSampler",
+    "StyleSample",
+    "SceneType",
+]

+ 424 - 0
.claude/scripts/data_modules/api_client.py

@@ -0,0 +1,424 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Data Modules - API 客户端 (v5.0 OpenAI 兼容接口)
+
+支持两种 API 类型:
+1. openai: OpenAI 兼容的 /v1/embeddings 和 /v1/rerank 接口
+   - 适用于: OpenAI, Jina, Cohere, vLLM, Ollama 等
+2. modal: Modal 自定义接口格式
+   - 适用于: 自部署的 Modal 服务
+
+配置示例 (config.py):
+    embed_api_type = "openai"
+    embed_base_url = "https://api.openai.com/v1"
+    embed_model = "text-embedding-3-small"
+    embed_api_key = "sk-xxx"
+
+    rerank_api_type = "openai"  # Jina/Cohere 也使用此类型
+    rerank_base_url = "https://api.jina.ai/v1"
+    rerank_model = "jina-reranker-v2-base-multilingual"
+    rerank_api_key = "jina_xxx"
+"""
+
+import asyncio
+import aiohttp
+import time
+from typing import List, Dict, Any, Optional
+from dataclasses import dataclass
+
+from .config import get_config
+
+
+@dataclass
+class APIStats:
+    """API 调用统计"""
+    total_calls: int = 0
+    total_time: float = 0.0
+    errors: int = 0
+
+
+class EmbeddingAPIClient:
+    """
+    通用 Embedding API 客户端
+
+    支持 OpenAI 兼容接口 (/v1/embeddings) 和 Modal 自定义接口
+    """
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self.sem = asyncio.Semaphore(self.config.embed_concurrency)
+        self.stats = APIStats()
+        self._warmed_up = False
+        self._session: Optional[aiohttp.ClientSession] = None
+
+    async def _get_session(self) -> aiohttp.ClientSession:
+        if self._session is None or self._session.closed:
+            connector = aiohttp.TCPConnector(limit=200, limit_per_host=100)
+            self._session = aiohttp.ClientSession(connector=connector)
+        return self._session
+
+    async def close(self):
+        if self._session and not self._session.closed:
+            await self._session.close()
+
+    def _build_headers(self) -> Dict[str, str]:
+        """构建请求头"""
+        headers = {"Content-Type": "application/json"}
+        if self.config.embed_api_key:
+            headers["Authorization"] = f"Bearer {self.config.embed_api_key}"
+        return headers
+
+    def _build_url(self) -> str:
+        """构建请求 URL"""
+        base_url = self.config.embed_base_url.rstrip("/")
+        if self.config.embed_api_type == "openai":
+            # OpenAI 兼容: /v1/embeddings
+            if not base_url.endswith("/embeddings"):
+                if base_url.endswith("/v1"):
+                    return f"{base_url}/embeddings"
+                return f"{base_url}/v1/embeddings"
+            return base_url
+        else:
+            # Modal 自定义接口: 直接使用配置的 URL
+            return base_url
+
+    def _build_payload(self, texts: List[str]) -> Dict[str, Any]:
+        """构建请求体"""
+        if self.config.embed_api_type == "openai":
+            return {
+                "input": texts,
+                "model": self.config.embed_model
+            }
+        else:
+            # Modal 格式
+            return {
+                "input": texts,
+                "model": self.config.embed_model
+            }
+
+    def _parse_response(self, data: Dict[str, Any]) -> Optional[List[List[float]]]:
+        """解析响应"""
+        if self.config.embed_api_type == "openai":
+            # OpenAI 格式: {"data": [{"embedding": [...], "index": 0}, ...]}
+            if "data" in data:
+                # 按 index 排序,确保顺序正确
+                sorted_data = sorted(data["data"], key=lambda x: x.get("index", 0))
+                return [item["embedding"] for item in sorted_data]
+            return None
+        else:
+            # Modal 格式: {"data": [{"embedding": [...]}, ...]}
+            if "data" in data:
+                return [item["embedding"] for item in data["data"]]
+            return None
+
+    async def embed(self, texts: List[str]) -> Optional[List[List[float]]]:
+        """调用 Embedding 服务"""
+        if not texts:
+            return []
+
+        timeout = self.config.cold_start_timeout if not self._warmed_up else self.config.normal_timeout
+
+        async with self.sem:
+            start = time.time()
+            session = await self._get_session()
+
+            try:
+                url = self._build_url()
+                headers = self._build_headers()
+                payload = self._build_payload(texts)
+
+                async with session.post(
+                    url,
+                    json=payload,
+                    headers=headers,
+                    timeout=aiohttp.ClientTimeout(total=timeout)
+                ) as resp:
+                    if resp.status == 200:
+                        data = await resp.json()
+                        embeddings = self._parse_response(data)
+
+                        if embeddings:
+                            self.stats.total_calls += 1
+                            self.stats.total_time += time.time() - start
+                            return embeddings
+
+                    self.stats.errors += 1
+                    print(f"[ERR] Embed {resp.status}: {await resp.text()[:200]}")
+                    return None
+
+            except Exception as e:
+                self.stats.errors += 1
+                print(f"[ERR] Embed: {e}")
+                return None
+
+    async def embed_batch(
+        self, texts: List[str], *, skip_failures: bool = True
+    ) -> List[Optional[List[float]]]:
+        """
+        分批 Embedding
+
+        Args:
+            texts: 要嵌入的文本列表
+            skip_failures: True 时失败的文本返回 None;False 时任一失败则整体返回空列表
+
+        Returns:
+            与 texts 等长的列表,成功的位置是向量,失败的位置是 None
+        """
+        if not texts:
+            return []
+
+        all_embeddings: List[Optional[List[float]]] = []
+        batch_size = self.config.embed_batch_size
+
+        batches = [texts[i:i + batch_size] for i in range(0, len(texts), batch_size)]
+        tasks = [self.embed(batch) for batch in batches]
+        results = await asyncio.gather(*tasks)
+
+        for batch_idx, result in enumerate(results):
+            actual_batch_size = len(batches[batch_idx])
+            if result and len(result) == actual_batch_size:
+                all_embeddings.extend(result)
+            else:
+                if not skip_failures:
+                    print(f"[WARN] Embed batch {batch_idx} failed, aborting all")
+                    return []
+                print(f"[WARN] Embed batch {batch_idx} failed, marking {actual_batch_size} items as None")
+                all_embeddings.extend([None] * actual_batch_size)
+
+        return all_embeddings[:len(texts)]
+
+    async def warmup(self):
+        """预热服务"""
+        await self.embed(["test"])
+        self._warmed_up = True
+
+
+class RerankAPIClient:
+    """
+    通用 Rerank API 客户端
+
+    支持 OpenAI 兼容接口 (Jina/Cohere 格式) 和 Modal 自定义接口
+    """
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self.sem = asyncio.Semaphore(self.config.rerank_concurrency)
+        self.stats = APIStats()
+        self._warmed_up = False
+        self._session: Optional[aiohttp.ClientSession] = None
+
+    async def _get_session(self) -> aiohttp.ClientSession:
+        if self._session is None or self._session.closed:
+            connector = aiohttp.TCPConnector(limit=200, limit_per_host=100)
+            self._session = aiohttp.ClientSession(connector=connector)
+        return self._session
+
+    async def close(self):
+        if self._session and not self._session.closed:
+            await self._session.close()
+
+    def _build_headers(self) -> Dict[str, str]:
+        """构建请求头"""
+        headers = {"Content-Type": "application/json"}
+        if self.config.rerank_api_key:
+            headers["Authorization"] = f"Bearer {self.config.rerank_api_key}"
+        return headers
+
+    def _build_url(self) -> str:
+        """构建请求 URL"""
+        base_url = self.config.rerank_base_url.rstrip("/")
+        if self.config.rerank_api_type == "openai":
+            # Jina/Cohere 兼容: /v1/rerank
+            if not base_url.endswith("/rerank"):
+                if base_url.endswith("/v1"):
+                    return f"{base_url}/rerank"
+                return f"{base_url}/v1/rerank"
+            return base_url
+        else:
+            # Modal 自定义接口
+            return base_url
+
+    def _build_payload(self, query: str, documents: List[str], top_n: Optional[int]) -> Dict[str, Any]:
+        """构建请求体"""
+        if self.config.rerank_api_type == "openai":
+            # Jina/Cohere 格式
+            payload: Dict[str, Any] = {
+                "query": query,
+                "documents": documents,
+                "model": self.config.rerank_model
+            }
+            if top_n:
+                payload["top_n"] = top_n
+            return payload
+        else:
+            # Modal 格式
+            payload = {"query": query, "documents": documents}
+            if top_n:
+                payload["top_n"] = top_n
+            return payload
+
+    def _parse_response(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
+        """解析响应"""
+        if self.config.rerank_api_type == "openai":
+            # Jina/Cohere 格式: {"results": [{"index": 0, "relevance_score": 0.9}, ...]}
+            return data.get("results", [])
+        else:
+            # Modal 格式: {"results": [...]}
+            return data.get("results", [])
+
+    async def rerank(
+        self,
+        query: str,
+        documents: List[str],
+        top_n: Optional[int] = None
+    ) -> Optional[List[Dict[str, Any]]]:
+        """调用 Rerank 服务"""
+        if not documents:
+            return []
+
+        timeout = self.config.cold_start_timeout if not self._warmed_up else self.config.normal_timeout
+
+        async with self.sem:
+            start = time.time()
+            session = await self._get_session()
+
+            try:
+                url = self._build_url()
+                headers = self._build_headers()
+                payload = self._build_payload(query, documents, top_n)
+
+                async with session.post(
+                    url,
+                    json=payload,
+                    headers=headers,
+                    timeout=aiohttp.ClientTimeout(total=timeout)
+                ) as resp:
+                    if resp.status == 200:
+                        data = await resp.json()
+
+                        self.stats.total_calls += 1
+                        self.stats.total_time += time.time() - start
+
+                        return self._parse_response(data)
+                    else:
+                        self.stats.errors += 1
+                        print(f"[ERR] Rerank {resp.status}: {await resp.text()[:200]}")
+                        return None
+
+            except Exception as e:
+                self.stats.errors += 1
+                print(f"[ERR] Rerank: {e}")
+                return None
+
+    async def warmup(self):
+        """预热服务"""
+        await self.rerank("test", ["doc1", "doc2"])
+        self._warmed_up = True
+
+
+class ModalAPIClient:
+    """
+    统一 API 客户端 (兼容旧接口)
+
+    整合 Embedding + Rerank 客户端,保持向后兼容
+    """
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self._embed_client = EmbeddingAPIClient(self.config)
+        self._rerank_client = RerankAPIClient(self.config)
+
+        # 兼容旧代码的信号量
+        self.sem_embed = self._embed_client.sem
+        self.sem_rerank = self._rerank_client.sem
+
+        self._warmed_up = {"embed": False, "rerank": False}
+        self._session: Optional[aiohttp.ClientSession] = None
+
+    @property
+    def stats(self) -> Dict[str, APIStats]:
+        return {
+            "embed": self._embed_client.stats,
+            "rerank": self._rerank_client.stats
+        }
+
+    async def _get_session(self) -> aiohttp.ClientSession:
+        # 复用 embed client 的 session
+        return await self._embed_client._get_session()
+
+    async def close(self):
+        await self._embed_client.close()
+        await self._rerank_client.close()
+
+    # ==================== 预热 ====================
+
+    async def warmup(self):
+        """预热 Embedding 和 Rerank 服务"""
+        print("[WARMUP] Warming up Embed + Rerank...")
+        start = time.time()
+
+        tasks = [self._warmup_embed(), self._warmup_rerank()]
+        results = await asyncio.gather(*tasks, return_exceptions=True)
+
+        for name, result in zip(["Embed", "Rerank"], results):
+            if isinstance(result, Exception):
+                print(f"  [FAIL] {name}: {result}")
+            else:
+                print(f"  [OK] {name} ready")
+
+        print(f"[WARMUP] Done in {time.time() - start:.1f}s")
+
+    async def _warmup_embed(self):
+        await self._embed_client.warmup()
+        self._warmed_up["embed"] = True
+
+    async def _warmup_rerank(self):
+        await self._rerank_client.warmup()
+        self._warmed_up["rerank"] = True
+
+    # ==================== Embedding API ====================
+
+    async def embed(self, texts: List[str]) -> Optional[List[List[float]]]:
+        """调用 Embedding 服务"""
+        return await self._embed_client.embed(texts)
+
+    async def embed_batch(
+        self, texts: List[str], *, skip_failures: bool = True
+    ) -> List[Optional[List[float]]]:
+        """分批 Embedding"""
+        return await self._embed_client.embed_batch(texts, skip_failures=skip_failures)
+
+    # ==================== Rerank API ====================
+
+    async def rerank(
+        self,
+        query: str,
+        documents: List[str],
+        top_n: Optional[int] = None
+    ) -> Optional[List[Dict[str, Any]]]:
+        """调用 Rerank 服务"""
+        return await self._rerank_client.rerank(query, documents, top_n)
+
+    # ==================== 统计 ====================
+
+    def print_stats(self):
+        print("\n[API STATS]")
+        for name, stats in self.stats.items():
+            if stats.total_calls > 0:
+                avg_time = stats.total_time / stats.total_calls
+                print(f"  {name.upper()}: {stats.total_calls} calls, "
+                      f"{stats.total_time:.1f}s total, "
+                      f"{avg_time:.2f}s avg, "
+                      f"{stats.errors} errors")
+
+
+# 全局客户端
+_client: Optional[ModalAPIClient] = None
+
+
+def get_client(config=None) -> ModalAPIClient:
+    global _client
+    if _client is None or config is not None:
+        _client = ModalAPIClient(config)
+    return _client

+ 208 - 0
.claude/scripts/data_modules/config.py

@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Data Modules - 配置文件
+"""
+
+import os
+from pathlib import Path
+from dataclasses import dataclass, field
+from typing import Optional
+
+
+@dataclass
+class DataModulesConfig:
+    """数据模块配置"""
+
+    # ================= 项目路径 =================
+    project_root: Path = field(default_factory=lambda: Path.cwd())
+
+    @property
+    def webnovel_dir(self) -> Path:
+        return self.project_root / ".webnovel"
+
+    @property
+    def state_file(self) -> Path:
+        return self.webnovel_dir / "state.json"
+
+    @property
+    def index_db(self) -> Path:
+        return self.webnovel_dir / "index.db"
+
+    @property
+    def alias_index_file(self) -> Path:
+        return self.webnovel_dir / "alias_index.json"
+
+    @property
+    def chapters_dir(self) -> Path:
+        return self.project_root / "正文"
+
+    @property
+    def settings_dir(self) -> Path:
+        return self.project_root / "设定集"
+
+    @property
+    def outline_dir(self) -> Path:
+        return self.project_root / "大纲"
+
+    # ================= Modal API Endpoints =================
+    # 注意:以下为默认 Modal 端点,可通过环境变量或显式传参覆盖
+    llm_base_url: str = "https://lingfengqaq--qwen3-30b-vllm-serve.modal.run/v1"
+    llm_model: str = "Qwen/Qwen3-30B-A3B-Instruct-2507"
+
+    # ================= Embedding API 配置 =================
+    # api_type: "openai" (通用 OpenAI 兼容接口) | "modal" (Modal 自定义接口)
+    embed_api_type: str = "openai"
+    embed_base_url: str = "https://lingfengqaq--qwen-embedding-server-qwenembedding-embeddings.modal.run"
+    embed_model: str = "qwen-embedding"
+    embed_api_key: str = ""  # OpenAI 兼容接口需要 API Key
+
+    # 保留旧字段兼容
+    @property
+    def embed_url(self) -> str:
+        """兼容旧代码:返回 embed_base_url"""
+        return self.embed_base_url
+
+    # ================= Rerank API 配置 =================
+    # api_type: "openai" (如 Jina/Cohere 兼容接口) | "modal" (Modal 自定义接口)
+    rerank_api_type: str = "modal"
+    rerank_base_url: str = "https://lingfengqaq--qwen-reranker-server-qwenreranker-rerank.modal.run"
+    rerank_model: str = "qwen-reranker"
+    rerank_api_key: str = ""  # Jina/Cohere 等需要 API Key
+
+    # 保留旧字段兼容
+    @property
+    def rerank_url(self) -> str:
+        """兼容旧代码:返回 rerank_base_url"""
+        return self.rerank_base_url
+
+    # ================= 并发配置 =================
+    llm_concurrency: int = 32
+    embed_concurrency: int = 64
+    rerank_concurrency: int = 32
+    embed_batch_size: int = 64
+
+    # ================= 超时配置 =================
+    cold_start_timeout: int = 300  # 5 分钟
+    normal_timeout: int = 180      # 3 分钟
+
+    # ================= LLM 生成配置 =================
+    llm_temperature: float = 0.1
+    llm_max_tokens: int = 4096
+
+    # ================= 检索配置 =================
+    vector_top_k: int = 30
+    bm25_top_k: int = 20
+    rerank_top_n: int = 10
+    rrf_k: int = 60
+
+    # 向量检索性能开关
+    # - 向量数量较少时(<= full_scan_max_vectors)可全表扫描,召回更稳
+    # - 规模变大后默认走预筛选(BM25 + 最近片段),避免 O(n) 扫描拖慢 Context Agent
+    vector_full_scan_max_vectors: int = 500
+    vector_prefilter_bm25_candidates: int = 200
+    vector_prefilter_recent_candidates: int = 200
+
+    # ================= 实体提取配置 =================
+    extraction_confidence_high: float = 0.8
+    extraction_confidence_medium: float = 0.5
+
+    # ================= 列表截断限制 =================
+    # state.json 列表最大保留条数
+    max_disambiguation_warnings: int = 500
+    max_disambiguation_pending: int = 1000
+    max_state_changes: int = 2000
+
+    # Context Pack 输出切片
+    context_recent_summaries_window: int = 5
+    context_alerts_slice: int = 10
+    context_max_appearing_characters: int = 10
+    context_max_urgent_foreshadowing: int = 5
+
+    # 导出上下文时的列表截断
+    export_recent_changes_slice: int = 20
+    export_disambiguation_slice: int = 20
+
+    # ================= 查询默认限制 =================
+    query_recent_chapters_limit: int = 10
+    query_scenes_by_location_limit: int = 20
+    query_entity_appearances_limit: int = 50
+    query_recent_appearances_limit: int = 20
+
+    # ================= 伏笔紧急度 =================
+    # 紧急度阈值(基于 章节差 / 目标差 × 权重)
+    foreshadowing_urgency_pending_high: int = 100  # 超过 100 章未回收
+    foreshadowing_urgency_pending_medium: int = 50  # 超过 50 章
+    foreshadowing_urgency_target_proximity: int = 5  # 距目标章节 5 章内
+    foreshadowing_urgency_score_high: int = 100
+    foreshadowing_urgency_score_medium: int = 60
+    foreshadowing_urgency_score_target: int = 80
+    foreshadowing_urgency_score_low: int = 20
+    foreshadowing_urgency_threshold_show: int = 60  # >= 此值才显示
+
+    # 层级权重
+    foreshadowing_tier_weight_core: float = 3.0
+    foreshadowing_tier_weight_sub: float = 2.0
+    foreshadowing_tier_weight_decor: float = 1.0
+
+    # ================= 角色活跃度 =================
+    character_absence_warning: int = 30  # 轻度掉线阈值
+    character_absence_critical: int = 100  # 严重掉线阈值
+    character_candidates_limit: int = 800  # 扫描时候选角色上限
+
+    # ================= Strand Weave 节奏 =================
+    strand_quest_max_consecutive: int = 5  # Quest 线最大连续章数
+    strand_fire_max_gap: int = 10  # Fire 线最大缺失章数
+    strand_constellation_max_gap: int = 15  # Constellation 线最大缺失章数
+
+    # 目标占比范围 (%)
+    strand_quest_ratio_min: int = 55
+    strand_quest_ratio_max: int = 65
+    strand_fire_ratio_min: int = 20
+    strand_fire_ratio_max: int = 30
+    strand_constellation_ratio_min: int = 10
+    strand_constellation_ratio_max: int = 20
+
+    # ================= 爽点节奏 =================
+    pacing_segment_size: int = 100  # 每段分析的章节数
+    pacing_words_per_point_excellent: int = 1000
+    pacing_words_per_point_good: int = 1500
+    pacing_words_per_point_acceptable: int = 2000
+
+    # ================= RAG 存储 =================
+    @property
+    def rag_db(self) -> Path:
+        return self.webnovel_dir / "rag.db"
+
+    @property
+    def vector_db(self) -> Path:
+        return self.webnovel_dir / "vectors.db"
+
+    def ensure_dirs(self):
+        """确保必要目录存在"""
+        self.webnovel_dir.mkdir(parents=True, exist_ok=True)
+
+    @classmethod
+    def from_project_root(cls, project_root: str | Path) -> "DataModulesConfig":
+        """从项目根目录创建配置"""
+        return cls(project_root=Path(project_root))
+
+
+# 全局默认配置
+_default_config: Optional[DataModulesConfig] = None
+
+
+def get_config(project_root: Optional[Path] = None) -> DataModulesConfig:
+    """获取配置实例"""
+    global _default_config
+    if project_root is not None:
+        return DataModulesConfig.from_project_root(project_root)
+    if _default_config is None:
+        _default_config = DataModulesConfig()
+    return _default_config
+
+
+def set_project_root(project_root: str | Path):
+    """设置项目根目录"""
+    global _default_config
+    _default_config = DataModulesConfig.from_project_root(project_root)

+ 343 - 0
.claude/scripts/data_modules/entity_linker.py

@@ -0,0 +1,343 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Entity Linker - 实体消歧辅助模块
+
+为 Data Agent 提供实体消歧的辅助功能:
+- 置信度判断
+- 别名索引管理
+- 消歧结果记录
+"""
+
+import json
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+from dataclasses import dataclass, field
+import filelock
+
+from .config import get_config
+
+try:
+    # 常见:从 scripts/ 目录运行,security_utils 在 sys.path 顶层
+    from security_utils import atomic_write_json, read_json_safe
+except ImportError:  # pragma: no cover
+    # 兼容:从仓库根目录以 `python -m scripts...` 运行
+    from scripts.security_utils import atomic_write_json, read_json_safe
+
+
+@dataclass
+class DisambiguationResult:
+    """消歧结果"""
+    mention: str
+    entity_id: Optional[str]
+    confidence: float
+    candidates: List[str] = field(default_factory=list)
+    adopted: bool = False
+    warning: Optional[str] = None
+
+
+class EntityLinker:
+    """实体链接器 - 辅助 Data Agent 进行实体消歧 (v5.0 一对多别名)"""
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        # v5.0: alias_index 改为一对多格式 {alias: [{"type": ..., "id": ...}, ...]}
+        self._alias_index: Dict[str, List[Dict]] = {}
+        self._state_file = self.config.state_file
+        self._load_alias_index()
+
+    def _load_alias_index(self):
+        """从 state.json 加载 alias_index"""
+        if self._state_file.exists():
+            try:
+                with open(self._state_file, "r", encoding="utf-8") as f:
+                    state = json.load(f)
+                self._alias_index = state.get("alias_index", {})
+            except (json.JSONDecodeError, IOError):
+                self._alias_index = {}
+        else:
+            self._alias_index = {}
+
+    def save_alias_index(self):
+        """保存 alias_index 到 state.json(v5.0 内嵌格式,锁内合并 + 原子写入)"""
+        if not self._state_file.exists():
+            return
+
+        lock_path = self._state_file.with_suffix(self._state_file.suffix + ".lock")
+        lock = filelock.FileLock(str(lock_path), timeout=10)
+        try:
+            with lock:
+                state = read_json_safe(self._state_file, default={})
+
+                disk_alias = state.get("alias_index", {})
+                if not isinstance(disk_alias, dict):
+                    disk_alias = {}
+
+                # 一对多:合并去重(避免覆盖其他进程刚写入的 state 字段/别名)
+                for alias, entries in (self._alias_index or {}).items():
+                    if not alias or not isinstance(entries, list):
+                        continue
+
+                    existing = disk_alias.get(alias)
+                    if not isinstance(existing, list):
+                        existing = []
+                        disk_alias[alias] = existing
+
+                    for entry in entries:
+                        if not isinstance(entry, dict):
+                            continue
+                        et = entry.get("type")
+                        eid = entry.get("id")
+                        if not et or not eid:
+                            continue
+                        if any(
+                            isinstance(e, dict) and e.get("type") == et and e.get("id") == eid
+                            for e in existing
+                        ):
+                            continue
+                        existing.append({"type": et, "id": eid})
+
+                state["alias_index"] = disk_alias
+
+                self.config.ensure_dirs()
+                atomic_write_json(self._state_file, state, use_lock=False, backup=True)
+
+                # 同步内存到磁盘最新快照
+                self._alias_index = disk_alias
+        except filelock.Timeout:
+            raise RuntimeError("无法获取 state.json 文件锁,请稍后重试")
+
+    # ==================== 别名管理 (v5.0 一对多) ====================
+
+    def register_alias(self, entity_id: str, alias: str, entity_type: str = "角色") -> bool:
+        """注册新别名(v5.0 一对多:同一别名可映射多个实体)"""
+        if not alias:
+            return False
+
+        if alias not in self._alias_index:
+            self._alias_index[alias] = []
+
+        # 检查是否已存在相同 (type, id) 组合
+        for entry in self._alias_index[alias]:
+            if entry.get("type") == entity_type and entry.get("id") == entity_id:
+                return True  # 已存在,视为成功
+
+        self._alias_index[alias].append({
+            "type": entity_type,
+            "id": entity_id
+        })
+        return True
+
+    def lookup_alias(self, mention: str, entity_type: str = None) -> Optional[str]:
+        """查找别名对应的实体ID(返回第一个匹配,可选按类型过滤)"""
+        entries = self._alias_index.get(mention, [])
+        if not entries:
+            return None
+
+        if entity_type:
+            for entry in entries:
+                if entry.get("type") == entity_type:
+                    return entry.get("id")
+            return None
+        else:
+            return entries[0].get("id") if entries else None
+
+    def lookup_alias_all(self, mention: str) -> List[Dict]:
+        """查找别名对应的所有实体(一对多)"""
+        return self._alias_index.get(mention, [])
+
+    def get_all_aliases(self, entity_id: str, entity_type: str = None) -> List[str]:
+        """获取实体的所有别名"""
+        aliases = []
+        for alias, entries in self._alias_index.items():
+            for entry in entries:
+                if entry.get("id") == entity_id:
+                    if entity_type is None or entry.get("type") == entity_type:
+                        aliases.append(alias)
+                        break
+        return aliases
+
+    # ==================== 置信度判断 ====================
+
+    def evaluate_confidence(self, confidence: float) -> Tuple[str, bool, Optional[str]]:
+        """
+        评估置信度,返回 (action, adopt, warning)
+
+        - action: "auto" | "warn" | "manual"
+        - adopt: 是否采用
+        - warning: 警告信息
+        """
+        if confidence >= self.config.extraction_confidence_high:
+            return ("auto", True, None)
+        elif confidence >= self.config.extraction_confidence_medium:
+            return ("warn", True, f"中置信度匹配 (confidence: {confidence:.2f})")
+        else:
+            return ("manual", False, f"需人工确认 (confidence: {confidence:.2f})")
+
+    def process_uncertain(
+        self,
+        mention: str,
+        candidates: List[str],
+        suggested: str,
+        confidence: float,
+        context: str = ""
+    ) -> DisambiguationResult:
+        """
+        处理不确定的实体匹配
+
+        返回消歧结果,包含是否采用、警告信息等
+        """
+        action, adopt, warning = self.evaluate_confidence(confidence)
+
+        result = DisambiguationResult(
+            mention=mention,
+            entity_id=suggested if adopt else None,
+            confidence=confidence,
+            candidates=candidates,
+            adopted=adopt,
+            warning=warning
+        )
+
+        return result
+
+    # ==================== 批量处理 ====================
+
+    def process_extraction_result(
+        self,
+        uncertain_items: List[Dict]
+    ) -> Tuple[List[DisambiguationResult], List[str]]:
+        """
+        处理 AI 提取结果中的 uncertain 项
+
+        返回 (results, warnings)
+        """
+        results = []
+        warnings = []
+
+        for item in uncertain_items:
+            result = self.process_uncertain(
+                mention=item.get("mention", ""),
+                candidates=item.get("candidates", []),
+                suggested=item.get("suggested", ""),
+                confidence=item.get("confidence", 0.0),
+                context=item.get("context", "")
+            )
+            results.append(result)
+
+            if result.warning:
+                warnings.append(f"{result.mention} → {result.entity_id}: {result.warning}")
+
+        return results, warnings
+
+    def register_new_entities(
+        self,
+        new_entities: List[Dict]
+    ) -> List[str]:
+        """
+        注册新实体的别名 (v5.0)
+
+        返回注册的实体ID列表
+        """
+        registered = []
+
+        for entity in new_entities:
+            entity_id = entity.get("suggested_id") or entity.get("id")
+            if not entity_id or entity_id == "NEW":
+                continue
+
+            entity_type = entity.get("type", "角色")
+
+            # 注册主名称
+            name = entity.get("name", "")
+            if name:
+                self.register_alias(entity_id, name, entity_type)
+
+            # 注册提及方式
+            for mention in entity.get("mentions", []):
+                if mention and mention != name:
+                    self.register_alias(entity_id, mention, entity_type)
+
+            registered.append(entity_id)
+
+        return registered
+
+
+# ==================== CLI 接口 ====================
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(description="Entity Linker CLI (v5.0 一对多别名)")
+    parser.add_argument("--project-root", type=str, help="项目根目录")
+
+    subparsers = parser.add_subparsers(dest="command")
+
+    # 注册别名
+    register_parser = subparsers.add_parser("register-alias")
+    register_parser.add_argument("--entity", required=True, help="实体ID")
+    register_parser.add_argument("--alias", required=True, help="别名")
+    register_parser.add_argument("--type", default="角色", help="实体类型(默认:角色)")
+
+    # 查找别名
+    lookup_parser = subparsers.add_parser("lookup")
+    lookup_parser.add_argument("--mention", required=True, help="提及文本")
+    lookup_parser.add_argument("--type", help="按类型过滤")
+
+    # 查找所有匹配(一对多)
+    lookup_all_parser = subparsers.add_parser("lookup-all")
+    lookup_all_parser.add_argument("--mention", required=True, help="提及文本")
+
+    # 列出别名
+    list_parser = subparsers.add_parser("list-aliases")
+    list_parser.add_argument("--entity", required=True, help="实体ID")
+    list_parser.add_argument("--type", help="实体类型")
+
+    args = parser.parse_args()
+
+    # 初始化
+    config = None
+    if args.project_root:
+        from .config import DataModulesConfig
+        config = DataModulesConfig.from_project_root(args.project_root)
+
+    linker = EntityLinker(config)
+
+    if args.command == "register-alias":
+        entity_type = getattr(args, "type", "角色")
+        success = linker.register_alias(args.entity, args.alias, entity_type)
+        if success:
+            linker.save_alias_index()
+            print(f"✓ 已注册: {args.alias} → {args.entity} (类型: {entity_type})")
+        else:
+            print(f"✗ 注册失败")
+
+    elif args.command == "lookup":
+        entity_type = getattr(args, "type", None)
+        entity_id = linker.lookup_alias(args.mention, entity_type)
+        if entity_id:
+            print(f"{args.mention} → {entity_id}")
+        else:
+            print(f"未找到: {args.mention}")
+
+    elif args.command == "lookup-all":
+        entries = linker.lookup_alias_all(args.mention)
+        if entries:
+            print(f"{args.mention} 的所有匹配:")
+            for entry in entries:
+                print(f"  - {entry.get('id')} (类型: {entry.get('type')})")
+        else:
+            print(f"未找到: {args.mention}")
+
+    elif args.command == "list-aliases":
+        entity_type = getattr(args, "type", None)
+        aliases = linker.get_all_aliases(args.entity, entity_type)
+        if aliases:
+            print(f"{args.entity} 的别名:")
+            for alias in aliases:
+                print(f"  - {alias}")
+        else:
+            print(f"未找到 {args.entity} 的别名")
+
+
+if __name__ == "__main__":
+    main()

+ 471 - 0
.claude/scripts/data_modules/index_manager.py

@@ -0,0 +1,471 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Index Manager - 索引管理模块
+
+管理 index.db (SQLite) 的读写操作:
+- 章节元数据索引
+- 实体出场记录
+- 场景索引
+- 快速查询接口
+"""
+
+import sqlite3
+import json
+from pathlib import Path
+from typing import Dict, List, Optional, Any, Tuple
+from dataclasses import dataclass
+from contextlib import contextmanager
+
+from .config import get_config
+
+
+@dataclass
+class ChapterMeta:
+    """章节元数据"""
+    chapter: int
+    title: str
+    location: str
+    word_count: int
+    characters: List[str]
+    summary: str = ""
+
+
+@dataclass
+class SceneMeta:
+    """场景元数据"""
+    chapter: int
+    scene_index: int
+    start_line: int
+    end_line: int
+    location: str
+    summary: str
+    characters: List[str]
+
+
+class IndexManager:
+    """索引管理器"""
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self._init_db()
+
+    def _init_db(self):
+        """初始化数据库表"""
+        self.config.ensure_dirs()
+
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            # 章节表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS chapters (
+                    chapter INTEGER PRIMARY KEY,
+                    title TEXT,
+                    location TEXT,
+                    word_count INTEGER,
+                    characters TEXT,
+                    summary TEXT,
+                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+                )
+            """)
+
+            # 场景表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS scenes (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    chapter INTEGER,
+                    scene_index INTEGER,
+                    start_line INTEGER,
+                    end_line INTEGER,
+                    location TEXT,
+                    summary TEXT,
+                    characters TEXT,
+                    UNIQUE(chapter, scene_index)
+                )
+            """)
+
+            # 实体出场表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS appearances (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    entity_id TEXT,
+                    chapter INTEGER,
+                    mentions TEXT,
+                    confidence REAL,
+                    UNIQUE(entity_id, chapter)
+                )
+            """)
+
+            # 创建索引
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_scenes_chapter ON scenes(chapter)")
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_appearances_entity ON appearances(entity_id)")
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_appearances_chapter ON appearances(chapter)")
+
+            conn.commit()
+
+    @contextmanager
+    def _get_conn(self):
+        """获取数据库连接"""
+        conn = sqlite3.connect(str(self.config.index_db))
+        conn.row_factory = sqlite3.Row
+        try:
+            yield conn
+        finally:
+            conn.close()
+
+    # ==================== 章节操作 ====================
+
+    def add_chapter(self, meta: ChapterMeta):
+        """添加/更新章节元数据"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                INSERT OR REPLACE INTO chapters
+                (chapter, title, location, word_count, characters, summary)
+                VALUES (?, ?, ?, ?, ?, ?)
+            """, (
+                meta.chapter,
+                meta.title,
+                meta.location,
+                meta.word_count,
+                json.dumps(meta.characters, ensure_ascii=False),
+                meta.summary
+            ))
+            conn.commit()
+
+    def get_chapter(self, chapter: int) -> Optional[Dict]:
+        """获取章节元数据"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("SELECT * FROM chapters WHERE chapter = ?", (chapter,))
+            row = cursor.fetchone()
+            if row:
+                return self._row_to_dict(row, parse_json=["characters"])
+            return None
+
+    def get_recent_chapters(self, limit: int = None) -> List[Dict]:
+        """获取最近章节"""
+        if limit is None:
+            limit = self.config.query_recent_chapters_limit
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT * FROM chapters
+                ORDER BY chapter DESC
+                LIMIT ?
+            """, (limit,))
+            return [self._row_to_dict(row, parse_json=["characters"]) for row in cursor.fetchall()]
+
+    # ==================== 场景操作 ====================
+
+    def add_scenes(self, chapter: int, scenes: List[SceneMeta]):
+        """添加章节场景"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            # 先删除该章节旧场景
+            cursor.execute("DELETE FROM scenes WHERE chapter = ?", (chapter,))
+
+            # 插入新场景
+            for scene in scenes:
+                cursor.execute("""
+                    INSERT INTO scenes
+                    (chapter, scene_index, start_line, end_line, location, summary, characters)
+                    VALUES (?, ?, ?, ?, ?, ?, ?)
+                """, (
+                    scene.chapter,
+                    scene.scene_index,
+                    scene.start_line,
+                    scene.end_line,
+                    scene.location,
+                    scene.summary,
+                    json.dumps(scene.characters, ensure_ascii=False)
+                ))
+
+            conn.commit()
+
+    def get_scenes(self, chapter: int) -> List[Dict]:
+        """获取章节场景"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT * FROM scenes
+                WHERE chapter = ?
+                ORDER BY scene_index
+            """, (chapter,))
+            return [self._row_to_dict(row, parse_json=["characters"]) for row in cursor.fetchall()]
+
+    def search_scenes_by_location(self, location: str, limit: int = None) -> List[Dict]:
+        """按地点搜索场景"""
+        if limit is None:
+            limit = self.config.query_scenes_by_location_limit
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT * FROM scenes
+                WHERE location LIKE ?
+                ORDER BY chapter DESC
+                LIMIT ?
+            """, (f"%{location}%", limit))
+            return [self._row_to_dict(row, parse_json=["characters"]) for row in cursor.fetchall()]
+
+    # ==================== 出场记录操作 ====================
+
+    def record_appearance(
+        self,
+        entity_id: str,
+        chapter: int,
+        mentions: List[str],
+        confidence: float = 1.0
+    ):
+        """记录实体出场"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                INSERT OR REPLACE INTO appearances
+                (entity_id, chapter, mentions, confidence)
+                VALUES (?, ?, ?, ?)
+            """, (
+                entity_id,
+                chapter,
+                json.dumps(mentions, ensure_ascii=False),
+                confidence
+            ))
+            conn.commit()
+
+    def get_entity_appearances(self, entity_id: str, limit: int = None) -> List[Dict]:
+        """获取实体出场记录"""
+        if limit is None:
+            limit = self.config.query_entity_appearances_limit
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT * FROM appearances
+                WHERE entity_id = ?
+                ORDER BY chapter DESC
+                LIMIT ?
+            """, (entity_id, limit))
+            return [self._row_to_dict(row, parse_json=["mentions"]) for row in cursor.fetchall()]
+
+    def get_recent_appearances(self, limit: int = None) -> List[Dict]:
+        """获取最近出场的实体"""
+        if limit is None:
+            limit = self.config.query_recent_appearances_limit
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT entity_id, MAX(chapter) as last_chapter, COUNT(*) as total
+                FROM appearances
+                GROUP BY entity_id
+                ORDER BY last_chapter DESC
+                LIMIT ?
+            """, (limit,))
+            return [dict(row) for row in cursor.fetchall()]
+
+    def get_chapter_appearances(self, chapter: int) -> List[Dict]:
+        """获取某章所有出场实体"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT * FROM appearances
+                WHERE chapter = ?
+                ORDER BY confidence DESC
+            """, (chapter,))
+            return [self._row_to_dict(row, parse_json=["mentions"]) for row in cursor.fetchall()]
+
+    # ==================== 批量操作 ====================
+
+    def process_chapter_data(
+        self,
+        chapter: int,
+        title: str,
+        location: str,
+        word_count: int,
+        entities: List[Dict],
+        scenes: List[Dict]
+    ) -> Dict[str, int]:
+        """
+        处理章节数据,批量写入索引
+
+        返回写入统计
+        """
+        stats = {"chapters": 0, "scenes": 0, "appearances": 0}
+
+        # 提取出场角色
+        characters = [e.get("id") for e in entities if e.get("type") == "角色"]
+
+        # 写入章节元数据
+        self.add_chapter(ChapterMeta(
+            chapter=chapter,
+            title=title,
+            location=location,
+            word_count=word_count,
+            characters=characters,
+            summary=""  # 可后续由 Data Agent 生成
+        ))
+        stats["chapters"] = 1
+
+        # 写入场景
+        scene_metas = []
+        for s in scenes:
+            scene_metas.append(SceneMeta(
+                chapter=chapter,
+                scene_index=s.get("index", 0),
+                start_line=s.get("start_line", 0),
+                end_line=s.get("end_line", 0),
+                location=s.get("location", ""),
+                summary=s.get("summary", ""),
+                characters=s.get("characters", [])
+            ))
+        self.add_scenes(chapter, scene_metas)
+        stats["scenes"] = len(scene_metas)
+
+        # 写入出场记录
+        for entity in entities:
+            entity_id = entity.get("id")
+            if entity_id and entity_id != "NEW":
+                self.record_appearance(
+                    entity_id=entity_id,
+                    chapter=chapter,
+                    mentions=entity.get("mentions", []),
+                    confidence=entity.get("confidence", 1.0)
+                )
+                stats["appearances"] += 1
+
+        return stats
+
+    # ==================== 辅助方法 ====================
+
+    def _row_to_dict(self, row: sqlite3.Row, parse_json: List[str] = None) -> Dict:
+        """将 Row 转换为字典"""
+        d = dict(row)
+        if parse_json:
+            for key in parse_json:
+                if key in d and d[key]:
+                    try:
+                        d[key] = json.loads(d[key])
+                    except json.JSONDecodeError:
+                        pass
+        return d
+
+    def get_stats(self) -> Dict[str, int]:
+        """获取索引统计"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("SELECT COUNT(*) FROM chapters")
+            chapters = cursor.fetchone()[0]
+
+            cursor.execute("SELECT COUNT(*) FROM scenes")
+            scenes = cursor.fetchone()[0]
+
+            cursor.execute("SELECT COUNT(DISTINCT entity_id) FROM appearances")
+            entities = cursor.fetchone()[0]
+
+            cursor.execute("SELECT MAX(chapter) FROM chapters")
+            max_chapter = cursor.fetchone()[0] or 0
+
+            return {
+                "chapters": chapters,
+                "scenes": scenes,
+                "entities": entities,
+                "max_chapter": max_chapter
+            }
+
+
+# ==================== CLI 接口 ====================
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(description="Index Manager CLI")
+    parser.add_argument("--project-root", type=str, help="项目根目录")
+
+    subparsers = parser.add_subparsers(dest="command")
+
+    # 获取统计
+    subparsers.add_parser("stats")
+
+    # 查询章节
+    chapter_parser = subparsers.add_parser("get-chapter")
+    chapter_parser.add_argument("--chapter", type=int, required=True)
+
+    # 查询最近出场
+    recent_parser = subparsers.add_parser("recent-appearances")
+    recent_parser.add_argument("--limit", type=int, default=None)
+
+    # 查询实体出场
+    entity_parser = subparsers.add_parser("entity-appearances")
+    entity_parser.add_argument("--entity", required=True)
+    entity_parser.add_argument("--limit", type=int, default=None)
+
+    # 搜索场景
+    search_parser = subparsers.add_parser("search-scenes")
+    search_parser.add_argument("--location", required=True)
+    search_parser.add_argument("--limit", type=int, default=None)
+
+    # 处理章节数据 (写入)
+    process_parser = subparsers.add_parser("process-chapter")
+    process_parser.add_argument("--chapter", type=int, required=True)
+    process_parser.add_argument("--title", required=True)
+    process_parser.add_argument("--location", required=True)
+    process_parser.add_argument("--word-count", type=int, required=True)
+    process_parser.add_argument("--entities", required=True, help="JSON 格式的实体列表")
+    process_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
+
+    args = parser.parse_args()
+
+    # 初始化
+    config = None
+    if args.project_root:
+        from .config import DataModulesConfig
+        config = DataModulesConfig.from_project_root(args.project_root)
+
+    manager = IndexManager(config)
+
+    if args.command == "stats":
+        stats = manager.get_stats()
+        print(json.dumps(stats, ensure_ascii=False, indent=2))
+
+    elif args.command == "get-chapter":
+        chapter = manager.get_chapter(args.chapter)
+        if chapter:
+            print(json.dumps(chapter, ensure_ascii=False, indent=2))
+        else:
+            print(f"未找到章节: {args.chapter}")
+
+    elif args.command == "recent-appearances":
+        appearances = manager.get_recent_appearances(args.limit)
+        for a in appearances:
+            print(f"{a['entity_id']}: 最后出场第 {a['last_chapter']} 章, 共 {a['total']} 次")
+
+    elif args.command == "entity-appearances":
+        appearances = manager.get_entity_appearances(args.entity, args.limit)
+        print(f"{args.entity} 出场记录:")
+        for a in appearances:
+            print(f"  第 {a['chapter']} 章: {a['mentions']}")
+
+    elif args.command == "search-scenes":
+        scenes = manager.search_scenes_by_location(args.location, args.limit)
+        for s in scenes:
+            print(f"第 {s['chapter']} 章 场景 {s['scene_index']}: {s['location']}")
+            print(f"  {s['summary'][:50]}...")
+
+    elif args.command == "process-chapter":
+        entities = json.loads(args.entities)
+        scenes = json.loads(args.scenes)
+        stats = manager.process_chapter_data(
+            chapter=args.chapter,
+            title=args.title,
+            location=args.location,
+            word_count=args.word_count,
+            entities=entities,
+            scenes=scenes
+        )
+        print(f"✓ 已处理第 {args.chapter} 章")
+        print(f"  章节: {stats['chapters']}, 场景: {stats['scenes']}, 出场记录: {stats['appearances']}")
+
+
+if __name__ == "__main__":
+    main()

+ 638 - 0
.claude/scripts/data_modules/rag_adapter.py

@@ -0,0 +1,638 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+RAG Adapter - RAG 检索适配模块
+
+封装向量检索功能:
+- 向量嵌入 (调用 Modal API)
+- 语义搜索
+- 重排序
+- 混合检索 (向量 + BM25)
+"""
+
+import asyncio
+import sqlite3
+import json
+import math
+from pathlib import Path
+from typing import Dict, List, Optional, Any, Tuple
+from dataclasses import dataclass
+from collections import Counter
+import re
+from contextlib import contextmanager
+import itertools
+
+from .config import get_config
+from .api_client import get_client
+
+
+@dataclass
+class SearchResult:
+    """搜索结果"""
+    chunk_id: str
+    chapter: int
+    scene_index: int
+    content: str
+    score: float
+    source: str  # "vector" | "bm25" | "hybrid"
+
+
+class RAGAdapter:
+    """RAG 检索适配器"""
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self.api_client = get_client(config)
+        self._init_db()
+
+    def _init_db(self):
+        """初始化向量数据库"""
+        self.config.ensure_dirs()
+
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            # 向量存储表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS vectors (
+                    chunk_id TEXT PRIMARY KEY,
+                    chapter INTEGER,
+                    scene_index INTEGER,
+                    content TEXT,
+                    embedding BLOB,
+                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+                )
+            """)
+
+            # BM25 倒排索引表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS bm25_index (
+                    term TEXT,
+                    chunk_id TEXT,
+                    tf REAL,
+                    PRIMARY KEY (term, chunk_id)
+                )
+            """)
+
+            # 文档统计表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS doc_stats (
+                    chunk_id TEXT PRIMARY KEY,
+                    doc_length INTEGER
+                )
+            """)
+
+            # 创建索引
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_vectors_chapter ON vectors(chapter)")
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_bm25_term ON bm25_index(term)")
+
+            conn.commit()
+
+    @contextmanager
+    def _get_conn(self):
+        """获取数据库连接(确保关闭,避免 Windows 下文件句柄泄漏)"""
+        conn = sqlite3.connect(str(self.config.vector_db))
+        try:
+            yield conn
+        finally:
+            conn.close()
+
+    def _get_vectors_count(self) -> int:
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("SELECT COUNT(*) FROM vectors")
+            row = cursor.fetchone()
+            return int(row[0] or 0) if row else 0
+
+    def _get_recent_chunk_ids(self, limit: int) -> List[str]:
+        if limit <= 0:
+            return []
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute(
+                "SELECT chunk_id FROM vectors ORDER BY chapter DESC, scene_index DESC LIMIT ?",
+                (int(limit),),
+            )
+            return [str(r[0]) for r in cursor.fetchall() if r and r[0]]
+
+    def _fetch_vectors_by_chunk_ids(self, chunk_ids: List[str]) -> List[Tuple]:
+        if not chunk_ids:
+            return []
+
+        # SQLite 参数数量限制(默认 999),这里做分片查询
+        def _chunks(xs: List[str], size: int = 500):
+            it = iter(xs)
+            while True:
+                batch = list(itertools.islice(it, size))
+                if not batch:
+                    break
+                yield batch
+
+        rows: List[Tuple] = []
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            for batch in _chunks(chunk_ids):
+                placeholders = ",".join(["?"] * len(batch))
+                cursor.execute(
+                    f"SELECT chunk_id, chapter, scene_index, content, embedding FROM vectors WHERE chunk_id IN ({placeholders})",
+                    tuple(batch),
+                )
+                rows.extend(cursor.fetchall())
+        return rows
+
+    def _vector_search_rows(
+        self,
+        query_embedding: List[float],
+        rows: List[Tuple],
+        *,
+        top_k: int,
+    ) -> List[SearchResult]:
+        results: List[SearchResult] = []
+        for row in rows:
+            chunk_id, chapter, scene_index, content, embedding_bytes = row
+            if not embedding_bytes:
+                continue
+            embedding = self._deserialize_embedding(embedding_bytes)
+            score = self._cosine_similarity(query_embedding, embedding)
+            results.append(
+                SearchResult(
+                    chunk_id=chunk_id,
+                    chapter=chapter,
+                    scene_index=scene_index,
+                    content=content,
+                    score=score,
+                    source="vector",
+                )
+            )
+
+        results.sort(key=lambda x: x.score, reverse=True)
+        return results[:top_k]
+
+    # ==================== 向量存储 ====================
+
+    async def store_chunks(self, chunks: List[Dict]) -> int:
+        """
+        存储场景切片的向量
+
+        chunks 格式:
+        [
+            {
+                "chapter": 100,
+                "scene_index": 1,
+                "content": "场景内容..."
+            }
+        ]
+
+        返回存储数量
+        """
+        if not chunks:
+            return 0
+
+        # 提取内容用于嵌入
+        contents = [c["content"] for c in chunks]
+
+        # 调用 API 获取嵌入向量(可能包含 None 表示失败)
+        embeddings = await self.api_client.embed_batch(contents)
+
+        if not embeddings:
+            return 0
+
+        # 存储到数据库(跳过嵌入失败的 chunk)
+        stored = 0
+        skipped = 0
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            for chunk, embedding in zip(chunks, embeddings):
+                if embedding is None:
+                    # 嵌入失败,跳过该 chunk(仅存储 BM25 索引供关键词检索)
+                    skipped += 1
+                    chunk_id = f"ch{chunk['chapter']}_s{chunk['scene_index']}"
+                    self._update_bm25_index(cursor, chunk_id, chunk["content"])
+                    continue
+
+                chunk_id = f"ch{chunk['chapter']}_s{chunk['scene_index']}"
+
+                # 将向量序列化为 bytes
+                embedding_bytes = self._serialize_embedding(embedding)
+
+                cursor.execute("""
+                    INSERT OR REPLACE INTO vectors
+                    (chunk_id, chapter, scene_index, content, embedding)
+                    VALUES (?, ?, ?, ?, ?)
+                """, (
+                    chunk_id,
+                    chunk["chapter"],
+                    chunk["scene_index"],
+                    chunk["content"],
+                    embedding_bytes
+                ))
+
+                # 同时更新 BM25 索引
+                self._update_bm25_index(cursor, chunk_id, chunk["content"])
+
+                stored += 1
+
+            conn.commit()
+
+        if skipped > 0:
+            print(f"[WARN] store_chunks: {skipped} chunks skipped due to embedding failure (BM25 only)")
+
+        return stored
+
+    def _serialize_embedding(self, embedding: List[float]) -> bytes:
+        """序列化向量"""
+        import struct
+        return struct.pack(f"{len(embedding)}f", *embedding)
+
+    def _deserialize_embedding(self, data: bytes) -> List[float]:
+        """反序列化向量"""
+        import struct
+        count = len(data) // 4
+        return list(struct.unpack(f"{count}f", data))
+
+    # ==================== BM25 索引 ====================
+
+    def _tokenize(self, text: str) -> List[str]:
+        """简单分词(中文按字符,英文按单词)"""
+        # 中文字符
+        chinese = re.findall(r'[\u4e00-\u9fff]+', text)
+        chinese_chars = list("".join(chinese))
+
+        # 英文单词
+        english = re.findall(r'[a-zA-Z]+', text.lower())
+
+        return chinese_chars + english
+
+    def _update_bm25_index(self, cursor, chunk_id: str, content: str):
+        """更新 BM25 索引"""
+        # 删除旧索引
+        cursor.execute("DELETE FROM bm25_index WHERE chunk_id = ?", (chunk_id,))
+        cursor.execute("DELETE FROM doc_stats WHERE chunk_id = ?", (chunk_id,))
+
+        # 分词
+        tokens = self._tokenize(content)
+        doc_length = len(tokens)
+
+        # 计算词频
+        tf_counter = Counter(tokens)
+
+        # 插入倒排索引
+        for term, count in tf_counter.items():
+            tf = count / doc_length if doc_length > 0 else 0
+            cursor.execute("""
+                INSERT INTO bm25_index (term, chunk_id, tf)
+                VALUES (?, ?, ?)
+            """, (term, chunk_id, tf))
+
+        # 更新文档统计
+        cursor.execute("""
+            INSERT INTO doc_stats (chunk_id, doc_length)
+            VALUES (?, ?)
+        """, (chunk_id, doc_length))
+
+    # ==================== 向量检索 ====================
+
+    async def vector_search(
+        self,
+        query: str,
+        top_k: int = None
+    ) -> List[SearchResult]:
+        """向量相似度搜索"""
+        top_k = top_k or self.config.vector_top_k
+
+        # 获取查询向量
+        query_embeddings = await self.api_client.embed([query])
+        if not query_embeddings:
+            return []
+
+        query_embedding = query_embeddings[0]
+
+        # 从数据库读取所有向量并计算相似度
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("SELECT chunk_id, chapter, scene_index, content, embedding FROM vectors")
+
+            results = []
+            for row in cursor.fetchall():
+                chunk_id, chapter, scene_index, content, embedding_bytes = row
+                embedding = self._deserialize_embedding(embedding_bytes)
+
+                # 计算余弦相似度
+                score = self._cosine_similarity(query_embedding, embedding)
+
+                results.append(SearchResult(
+                    chunk_id=chunk_id,
+                    chapter=chapter,
+                    scene_index=scene_index,
+                    content=content,
+                    score=score,
+                    source="vector"
+                ))
+
+        # 排序并返回 top_k
+        results.sort(key=lambda x: x.score, reverse=True)
+        return results[:top_k]
+
+    def _cosine_similarity(self, a: List[float], b: List[float]) -> float:
+        """计算余弦相似度"""
+        dot_product = sum(x * y for x, y in zip(a, b))
+        norm_a = math.sqrt(sum(x * x for x in a))
+        norm_b = math.sqrt(sum(x * x for x in b))
+        if norm_a == 0 or norm_b == 0:
+            return 0.0
+        return dot_product / (norm_a * norm_b)
+
+    # ==================== BM25 检索 ====================
+
+    def bm25_search(
+        self,
+        query: str,
+        top_k: int = None,
+        k1: float = 1.5,
+        b: float = 0.75
+    ) -> List[SearchResult]:
+        """BM25 关键词搜索"""
+        top_k = top_k or self.config.bm25_top_k
+
+        query_terms = self._tokenize(query)
+        if not query_terms:
+            return []
+
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            # 获取文档总数和平均长度
+            cursor.execute("SELECT COUNT(*), AVG(doc_length) FROM doc_stats")
+            row = cursor.fetchone()
+            total_docs = row[0] or 1
+            avg_doc_length = row[1] or 1
+
+            # 计算每个文档的 BM25 分数
+            doc_scores = {}
+
+            for term in set(query_terms):
+                # 获取包含该词的文档
+                cursor.execute("""
+                    SELECT b.chunk_id, b.tf, d.doc_length
+                    FROM bm25_index b
+                    JOIN doc_stats d ON b.chunk_id = d.chunk_id
+                    WHERE b.term = ?
+                """, (term,))
+
+                docs_with_term = cursor.fetchall()
+                df = len(docs_with_term)
+
+                if df == 0:
+                    continue
+
+                # IDF
+                idf = math.log((total_docs - df + 0.5) / (df + 0.5) + 1)
+
+                for chunk_id, tf, doc_length in docs_with_term:
+                    # BM25 公式
+                    score = idf * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * doc_length / avg_doc_length))
+
+                    if chunk_id not in doc_scores:
+                        doc_scores[chunk_id] = 0
+                    doc_scores[chunk_id] += score
+
+            # 获取文档内容
+            results = []
+            for chunk_id, score in doc_scores.items():
+                cursor.execute("""
+                    SELECT chapter, scene_index, content
+                    FROM vectors
+                    WHERE chunk_id = ?
+                """, (chunk_id,))
+                row = cursor.fetchone()
+                if row:
+                    results.append(SearchResult(
+                        chunk_id=chunk_id,
+                        chapter=row[0],
+                        scene_index=row[1],
+                        content=row[2],
+                        score=score,
+                        source="bm25"
+                    ))
+
+        results.sort(key=lambda x: x.score, reverse=True)
+        return results[:top_k]
+
+    # ==================== 混合检索 ====================
+
+    async def hybrid_search(
+        self,
+        query: str,
+        vector_top_k: int = None,
+        bm25_top_k: int = None,
+        rerank_top_n: int = None
+    ) -> List[SearchResult]:
+        """
+        混合检索:向量 + BM25 + RRF 融合 + Rerank
+
+        步骤:
+        1. 向量检索 top_k
+        2. BM25 检索 top_k
+        3. RRF 融合
+        4. Rerank 精排
+        """
+        vector_top_k = vector_top_k or self.config.vector_top_k
+        bm25_top_k = bm25_top_k or self.config.bm25_top_k
+        rerank_top_n = rerank_top_n or self.config.rerank_top_n
+
+        # 小规模:全表向量扫描(召回更稳);大规模:预筛选避免 O(n) 扫描拖慢
+        vectors_count = await asyncio.to_thread(self._get_vectors_count)
+        use_full_scan = vectors_count <= int(self.config.vector_full_scan_max_vectors)
+
+        if use_full_scan:
+            # 并行执行向量和 BM25 检索
+            vector_results, bm25_results = await asyncio.gather(
+                self.vector_search(query, vector_top_k),
+                asyncio.to_thread(self.bm25_search, query, bm25_top_k)
+            )
+        else:
+            bm25_candidates = max(
+                int(self.config.vector_prefilter_bm25_candidates),
+                int(bm25_top_k),
+                int(vector_top_k) * 5,
+                int(rerank_top_n) * 10,
+            )
+            recent_candidates = max(
+                int(self.config.vector_prefilter_recent_candidates),
+                int(vector_top_k) * 5,
+                int(rerank_top_n) * 10,
+            )
+
+            bm25_task = asyncio.to_thread(self.bm25_search, query, bm25_candidates)
+            recent_task = asyncio.to_thread(self._get_recent_chunk_ids, recent_candidates)
+            embed_task = self.api_client.embed([query])
+
+            bm25_candidates_results, recent_ids, query_embeddings = await asyncio.gather(
+                bm25_task,
+                recent_task,
+                embed_task,
+            )
+
+            if not query_embeddings:
+                return []
+            query_embedding = query_embeddings[0]
+
+            candidate_ids = {r.chunk_id for r in bm25_candidates_results}
+            candidate_ids.update(recent_ids)
+
+            rows = await asyncio.to_thread(self._fetch_vectors_by_chunk_ids, list(candidate_ids))
+            vector_results = await asyncio.to_thread(
+                self._vector_search_rows,
+                query_embedding,
+                rows,
+                top_k=int(vector_top_k),
+            )
+
+            # BM25 结果用于融合时只取 top_k
+            bm25_results = list(bm25_candidates_results)[: int(bm25_top_k)]
+
+        # RRF 融合
+        rrf_scores = {}
+        k = self.config.rrf_k
+
+        for rank, result in enumerate(vector_results):
+            if result.chunk_id not in rrf_scores:
+                rrf_scores[result.chunk_id] = {"result": result, "score": 0}
+            rrf_scores[result.chunk_id]["score"] += 1 / (k + rank + 1)
+
+        for rank, result in enumerate(bm25_results):
+            if result.chunk_id not in rrf_scores:
+                rrf_scores[result.chunk_id] = {"result": result, "score": 0}
+            rrf_scores[result.chunk_id]["score"] += 1 / (k + rank + 1)
+
+        # 按 RRF 分数排序
+        sorted_results = sorted(
+            rrf_scores.values(),
+            key=lambda x: x["score"],
+            reverse=True
+        )
+
+        # 取 top candidates 进行 rerank
+        candidates = [item["result"] for item in sorted_results[:rerank_top_n * 2]]
+
+        if not candidates:
+            return []
+
+        # 调用 Rerank API
+        documents = [c.content for c in candidates]
+        rerank_results = await self.api_client.rerank(query, documents, top_n=rerank_top_n)
+
+        if not rerank_results:
+            # Rerank 失败,返回 RRF 结果
+            return [item["result"] for item in sorted_results[:rerank_top_n]]
+
+        # 组装最终结果
+        final_results = []
+        for r in rerank_results:
+            idx = r.get("index", 0)
+            if idx < len(candidates):
+                result = candidates[idx]
+                result.score = r.get("relevance_score", 0)
+                result.source = "hybrid"
+                final_results.append(result)
+
+        return final_results
+
+    # ==================== 统计 ====================
+
+    def get_stats(self) -> Dict[str, int]:
+        """获取 RAG 统计"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("SELECT COUNT(*) FROM vectors")
+            vectors = cursor.fetchone()[0]
+
+            cursor.execute("SELECT COUNT(DISTINCT term) FROM bm25_index")
+            terms = cursor.fetchone()[0]
+
+            cursor.execute("SELECT MAX(chapter) FROM vectors")
+            max_chapter = cursor.fetchone()[0] or 0
+
+            return {
+                "vectors": vectors,
+                "terms": terms,
+                "max_chapter": max_chapter
+            }
+
+
+# ==================== CLI 接口 ====================
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(description="RAG Adapter CLI")
+    parser.add_argument("--project-root", type=str, help="项目根目录")
+
+    subparsers = parser.add_subparsers(dest="command")
+
+    # 获取统计
+    subparsers.add_parser("stats")
+
+    # 搜索
+    search_parser = subparsers.add_parser("search")
+    search_parser.add_argument("--query", required=True, help="搜索查询")
+    search_parser.add_argument("--mode", choices=["vector", "bm25", "hybrid"], default="hybrid")
+    search_parser.add_argument("--top-k", type=int, default=10)
+
+    # 索引章节
+    index_parser = subparsers.add_parser("index-chapter")
+    index_parser.add_argument("--chapter", type=int, required=True)
+    index_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
+
+    args = parser.parse_args()
+
+    # 初始化
+    config = None
+    if args.project_root:
+        from .config import DataModulesConfig
+        config = DataModulesConfig.from_project_root(args.project_root)
+
+    adapter = RAGAdapter(config)
+
+    if args.command == "stats":
+        stats = adapter.get_stats()
+        print(json.dumps(stats, ensure_ascii=False, indent=2))
+
+    elif args.command == "search":
+        async def do_search():
+            if args.mode == "vector":
+                results = await adapter.vector_search(args.query, args.top_k)
+            elif args.mode == "bm25":
+                results = adapter.bm25_search(args.query, args.top_k)
+            else:
+                results = await adapter.hybrid_search(args.query)
+
+            print(f"搜索结果 ({len(results)} 条):")
+            for r in results:
+                print(f"\n[{r.source}] 第 {r.chapter} 章 场景 {r.scene_index} (score: {r.score:.4f})")
+                print(f"  {r.content[:100]}...")
+
+        asyncio.run(do_search())
+
+    elif args.command == "index-chapter":
+        scenes = json.loads(args.scenes)
+        chunks = [
+            {
+                "chapter": args.chapter,
+                "scene_index": s.get("index", i),
+                "content": s.get("summary", "") + "\n" + s.get("content", "")
+            }
+            for i, s in enumerate(scenes)
+        ]
+
+        async def do_index():
+            stored = await adapter.store_chunks(chunks)
+            print(f"✓ 已索引 {stored} 个场景")
+
+        asyncio.run(do_index())
+
+
+if __name__ == "__main__":
+    main()

+ 1045 - 0
.claude/scripts/data_modules/state_manager.py

@@ -0,0 +1,1045 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+State Manager - 状态管理模块
+
+管理 state.json 的读写操作:
+- 实体状态管理
+- 进度追踪
+- 关系记录
+"""
+
+import json
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+from dataclasses import dataclass, field, asdict
+from datetime import datetime
+import filelock
+
+from .config import get_config
+
+try:
+    # 当 scripts 目录在 sys.path 中(常见:从 scripts/ 运行)
+    from security_utils import atomic_write_json, read_json_safe
+except ImportError:  # pragma: no cover
+    # 当以 `python -m scripts.data_modules...` 从仓库根目录运行
+    from scripts.security_utils import atomic_write_json, read_json_safe
+
+
+@dataclass
+class EntityState:
+    """实体状态"""
+    id: str
+    name: str
+    type: str  # 角色/地点/物品/势力
+    tier: str = "装饰"  # 核心/支线/装饰
+    aliases: List[str] = field(default_factory=list)
+    attributes: Dict[str, Any] = field(default_factory=dict)
+    first_appearance: int = 0
+    last_appearance: int = 0
+
+
+@dataclass
+class Relationship:
+    """实体关系"""
+    from_entity: str
+    to_entity: str
+    type: str
+    description: str
+    chapter: int
+
+
+@dataclass
+class StateChange:
+    """状态变化记录"""
+    entity_id: str
+    field: str
+    old_value: Any
+    new_value: Any
+    reason: str
+    chapter: int
+    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
+
+
+@dataclass
+class _EntityPatch:
+    """待写入的实体增量补丁(用于锁内合并)"""
+    entity_type: str
+    entity_id: str
+    replace: bool = False
+    base_entity: Optional[Dict[str, Any]] = None  # 新建实体时的完整快照(用于填充缺失字段)
+    top_updates: Dict[str, Any] = field(default_factory=dict)
+    current_updates: Dict[str, Any] = field(default_factory=dict)
+    appearance_chapter: Optional[int] = None
+
+
+class StateManager:
+    """状态管理器 (v5.0 entities_v3 格式)"""
+
+    # v5.0 支持的实体类型
+    ENTITY_TYPES = ["角色", "地点", "物品", "势力", "招式"]
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self._state: Dict[str, Any] = {}
+        # 与 security_utils.atomic_write_json 保持一致:state.json.lock
+        self._lock_path = self.config.state_file.with_suffix(self.config.state_file.suffix + ".lock")
+
+        # 待写入的增量(锁内重读 + 合并 + 写入)
+        self._pending_entity_patches: Dict[tuple[str, str], _EntityPatch] = {}
+        self._pending_alias_entries: Dict[str, List[Dict[str, str]]] = {}
+        self._pending_state_changes: List[Dict[str, Any]] = []
+        self._pending_structured_relationships: List[Dict[str, Any]] = []
+        self._pending_disambiguation_warnings: List[Dict[str, Any]] = []
+        self._pending_disambiguation_pending: List[Dict[str, Any]] = []
+        self._pending_progress_chapter: Optional[int] = None
+        self._pending_progress_words_delta: int = 0
+
+        self._load_state()
+
+    def _now_progress_timestamp(self) -> str:
+        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+    def _ensure_state_schema(self, state: Dict[str, Any]) -> Dict[str, Any]:
+        """确保 state.json 具备运行所需的关键字段(尽量不破坏既有数据)。"""
+        if not isinstance(state, dict):
+            state = {}
+
+        state.setdefault("project_info", {})
+        state.setdefault("progress", {})
+        state.setdefault("protagonist_state", {})
+
+        # relationships: 旧版本可能是 list(实体关系),v5.0 运行态用 dict(人物关系/重要关系)
+        relationships = state.get("relationships")
+        if isinstance(relationships, list):
+            state.setdefault("structured_relationships", [])
+            if isinstance(state.get("structured_relationships"), list):
+                state["structured_relationships"].extend(relationships)
+            state["relationships"] = {}
+        elif not isinstance(relationships, dict):
+            state["relationships"] = {}
+
+        state.setdefault("world_settings", {"power_system": [], "factions": [], "locations": []})
+        state.setdefault("plot_threads", {"active_threads": [], "foreshadowing": []})
+        state.setdefault("review_checkpoints", [])
+        state.setdefault(
+            "strand_tracker",
+            {
+                "last_quest_chapter": 0,
+                "last_fire_chapter": 0,
+                "last_constellation_chapter": 0,
+                "current_dominant": "quest",
+                "chapters_since_switch": 0,
+                "history": [],
+            },
+        )
+
+        entities_v3 = state.get("entities_v3")
+        if not isinstance(entities_v3, dict):
+            entities_v3 = {}
+            state["entities_v3"] = entities_v3
+        for t in self.ENTITY_TYPES:
+            if not isinstance(entities_v3.get(t), dict):
+                entities_v3[t] = {}
+
+        if not isinstance(state.get("alias_index"), dict):
+            state["alias_index"] = {}
+
+        if not isinstance(state.get("state_changes"), list):
+            state["state_changes"] = []
+
+        if not isinstance(state.get("structured_relationships"), list):
+            state["structured_relationships"] = []
+
+        if not isinstance(state.get("disambiguation_warnings"), list):
+            state["disambiguation_warnings"] = []
+
+        if not isinstance(state.get("disambiguation_pending"), list):
+            state["disambiguation_pending"] = []
+
+        # progress 基础字段
+        progress = state["progress"]
+        if not isinstance(progress, dict):
+            progress = {}
+            state["progress"] = progress
+        progress.setdefault("current_chapter", 0)
+        progress.setdefault("total_words", 0)
+        progress.setdefault("last_updated", self._now_progress_timestamp())
+
+        return state
+
+    def _load_state(self):
+        """加载状态文件"""
+        if self.config.state_file.exists():
+            self._state = read_json_safe(self.config.state_file, default={})
+            self._state = self._ensure_state_schema(self._state)
+        else:
+            self._state = self._ensure_state_schema({})
+
+    def save_state(self):
+        """
+        保存状态文件(锁内重读 + 合并 + 原子写入)。
+
+        解决多 Agent 并行下的“读-改-写覆盖”风险:
+        - 获取锁
+        - 重新读取磁盘最新 state.json
+        - 仅合并本实例产生的增量(pending_*)
+        - 原子化写入
+        """
+        # 无增量时不写入,避免无意义覆盖
+        has_pending = any(
+            [
+                self._pending_entity_patches,
+                self._pending_alias_entries,
+                self._pending_state_changes,
+                self._pending_structured_relationships,
+                self._pending_disambiguation_warnings,
+                self._pending_disambiguation_pending,
+                self._pending_progress_chapter is not None,
+                self._pending_progress_words_delta != 0,
+            ]
+        )
+        if not has_pending:
+            return
+
+        self.config.ensure_dirs()
+
+        lock = filelock.FileLock(str(self._lock_path), timeout=10)
+        try:
+            with lock:
+                disk_state = read_json_safe(self.config.state_file, default={})
+                disk_state = self._ensure_state_schema(disk_state)
+
+                # progress(合并为 max(chapter) + words_delta 累加)
+                if self._pending_progress_chapter is not None or self._pending_progress_words_delta != 0:
+                    progress = disk_state.get("progress", {})
+                    if not isinstance(progress, dict):
+                        progress = {}
+                        disk_state["progress"] = progress
+
+                    try:
+                        current_chapter = int(progress.get("current_chapter", 0) or 0)
+                    except (TypeError, ValueError):
+                        current_chapter = 0
+
+                    if self._pending_progress_chapter is not None:
+                        progress["current_chapter"] = max(current_chapter, int(self._pending_progress_chapter))
+
+                    if self._pending_progress_words_delta:
+                        try:
+                            total_words = int(progress.get("total_words", 0) or 0)
+                        except (TypeError, ValueError):
+                            total_words = 0
+                        progress["total_words"] = total_words + int(self._pending_progress_words_delta)
+
+                    progress["last_updated"] = self._now_progress_timestamp()
+
+                # entities_v3(按补丁应用)
+                entities_v3 = disk_state.get("entities_v3", {})
+                if not isinstance(entities_v3, dict):
+                    entities_v3 = {}
+                    disk_state["entities_v3"] = entities_v3
+                for t in self.ENTITY_TYPES:
+                    if not isinstance(entities_v3.get(t), dict):
+                        entities_v3[t] = {}
+
+                for (entity_type, entity_id), patch in self._pending_entity_patches.items():
+                    bucket = entities_v3.setdefault(entity_type, {})
+                    if not isinstance(bucket, dict):
+                        bucket = {}
+                        entities_v3[entity_type] = bucket
+
+                    entity = bucket.get(entity_id)
+                    if not isinstance(entity, dict):
+                        entity = {}
+                        bucket[entity_id] = entity
+
+                    # 新建实体时:只填充缺失字段,避免覆盖并发写入的更完整信息
+                    if patch.base_entity:
+                        for k, v in patch.base_entity.items():
+                            if k not in entity:
+                                entity[k] = v
+                            elif isinstance(entity.get(k), dict) and isinstance(v, dict):
+                                # 递归填充缺失
+                                for kk, vv in v.items():
+                                    if kk not in entity[k]:
+                                        entity[k][kk] = vv
+
+                    # top-level updates(明确写入)
+                    for k, v in patch.top_updates.items():
+                        entity[k] = v
+
+                    # current updates(明确写入)
+                    if patch.current_updates:
+                        current = entity.get("current")
+                        if not isinstance(current, dict):
+                            current = {}
+                            entity["current"] = current
+                        current.update(patch.current_updates)
+
+                    # appearance updates(first=min(non-zero), last=max)
+                    if patch.appearance_chapter is not None:
+                        chapter = int(patch.appearance_chapter)
+                        try:
+                            first = int(entity.get("first_appearance", 0) or 0)
+                        except (TypeError, ValueError):
+                            first = 0
+                        try:
+                            last = int(entity.get("last_appearance", 0) or 0)
+                        except (TypeError, ValueError):
+                            last = 0
+
+                        if first <= 0:
+                            entity["first_appearance"] = chapter
+                        else:
+                            entity["first_appearance"] = min(first, chapter)
+                        entity["last_appearance"] = max(last, chapter)
+
+                # alias_index(一对多:合并去重)
+                alias_index = disk_state.get("alias_index", {})
+                if not isinstance(alias_index, dict):
+                    alias_index = {}
+                    disk_state["alias_index"] = alias_index
+
+                for alias, entries in self._pending_alias_entries.items():
+                    if not alias:
+                        continue
+                    existing = alias_index.get(alias)
+                    if not isinstance(existing, list):
+                        existing = []
+                        alias_index[alias] = existing
+
+                    for entry in entries:
+                        et = entry.get("type")
+                        eid = entry.get("id")
+                        if not et or not eid:
+                            continue
+                        if any(e.get("type") == et and e.get("id") == eid for e in existing if isinstance(e, dict)):
+                            continue
+                        existing.append({"type": et, "id": eid})
+
+                # state_changes(追加)
+                if self._pending_state_changes:
+                    changes = disk_state.get("state_changes")
+                    if not isinstance(changes, list):
+                        changes = []
+                        disk_state["state_changes"] = changes
+                    changes.extend(self._pending_state_changes)
+
+                # structured_relationships(追加去重)
+                if self._pending_structured_relationships:
+                    rels = disk_state.get("structured_relationships")
+                    if not isinstance(rels, list):
+                        rels = []
+                        disk_state["structured_relationships"] = rels
+
+                    def _rel_key(r: Dict[str, Any]) -> tuple:
+                        return (
+                            r.get("from_entity"),
+                            r.get("to_entity"),
+                            r.get("type"),
+                            r.get("description"),
+                            r.get("chapter"),
+                        )
+
+                    existing_keys = {_rel_key(r) for r in rels if isinstance(r, dict)}
+                    for r in self._pending_structured_relationships:
+                        if not isinstance(r, dict):
+                            continue
+                        k = _rel_key(r)
+                        if k in existing_keys:
+                            continue
+                        rels.append(r)
+                        existing_keys.add(k)
+
+                # disambiguation_warnings(追加去重 + 截断)
+                if self._pending_disambiguation_warnings:
+                    warnings_list = disk_state.get("disambiguation_warnings")
+                    if not isinstance(warnings_list, list):
+                        warnings_list = []
+                        disk_state["disambiguation_warnings"] = warnings_list
+
+                    def _warn_key(w: Dict[str, Any]) -> tuple:
+                        return (
+                            w.get("chapter"),
+                            w.get("mention"),
+                            w.get("chosen_id"),
+                            w.get("confidence"),
+                        )
+
+                    existing_keys = {_warn_key(w) for w in warnings_list if isinstance(w, dict)}
+                    for w in self._pending_disambiguation_warnings:
+                        if not isinstance(w, dict):
+                            continue
+                        k = _warn_key(w)
+                        if k in existing_keys:
+                            continue
+                        warnings_list.append(w)
+                        existing_keys.add(k)
+
+                    # 只保留最近 N 条,避免文件无限增长
+                    max_keep = self.config.max_disambiguation_warnings
+                    if len(warnings_list) > max_keep:
+                        disk_state["disambiguation_warnings"] = warnings_list[-max_keep:]
+
+                # disambiguation_pending(追加去重 + 截断)
+                if self._pending_disambiguation_pending:
+                    pending_list = disk_state.get("disambiguation_pending")
+                    if not isinstance(pending_list, list):
+                        pending_list = []
+                        disk_state["disambiguation_pending"] = pending_list
+
+                    def _pending_key(w: Dict[str, Any]) -> tuple:
+                        return (
+                            w.get("chapter"),
+                            w.get("mention"),
+                            w.get("suggested_id"),
+                            w.get("confidence"),
+                        )
+
+                    existing_keys = {_pending_key(w) for w in pending_list if isinstance(w, dict)}
+                    for w in self._pending_disambiguation_pending:
+                        if not isinstance(w, dict):
+                            continue
+                        k = _pending_key(w)
+                        if k in existing_keys:
+                            continue
+                        pending_list.append(w)
+                        existing_keys.add(k)
+
+                    max_keep = self.config.max_disambiguation_pending
+                    if len(pending_list) > max_keep:
+                        disk_state["disambiguation_pending"] = pending_list[-max_keep:]
+
+                # 原子写入(锁已持有,不再二次加锁)
+                atomic_write_json(self.config.state_file, disk_state, use_lock=False, backup=True)
+
+                # 同步内存为磁盘最新快照,并清空增量队列
+                self._state = disk_state
+                self._pending_entity_patches.clear()
+                self._pending_alias_entries.clear()
+                self._pending_state_changes.clear()
+                self._pending_structured_relationships.clear()
+                self._pending_disambiguation_warnings.clear()
+                self._pending_disambiguation_pending.clear()
+                self._pending_progress_chapter = None
+                self._pending_progress_words_delta = 0
+        except filelock.Timeout:
+            raise RuntimeError("无法获取 state.json 文件锁,请稍后重试")
+
+    # ==================== 进度管理 ====================
+
+    def get_current_chapter(self) -> int:
+        """获取当前章节号"""
+        return self._state.get("progress", {}).get("current_chapter", 0)
+
+    def update_progress(self, chapter: int, words: int = 0):
+        """更新进度"""
+        if "progress" not in self._state:
+            self._state["progress"] = {}
+        self._state["progress"]["current_chapter"] = chapter
+        if words > 0:
+            total = self._state["progress"].get("total_words", 0)
+            self._state["progress"]["total_words"] = total + words
+
+        # 记录增量:锁内合并时用 max(chapter) + words_delta 累加
+        if self._pending_progress_chapter is None:
+            self._pending_progress_chapter = chapter
+        else:
+            self._pending_progress_chapter = max(self._pending_progress_chapter, chapter)
+        if words > 0:
+            self._pending_progress_words_delta += int(words)
+
+    # ==================== 实体管理 (v5.0 entities_v3) ====================
+
+    def get_entity(self, entity_id: str, entity_type: str = None) -> Optional[Dict]:
+        """获取实体 (v5.0 entities_v3 格式)"""
+        entities_v3 = self._state.get("entities_v3", {})
+
+        if entity_type:
+            return entities_v3.get(entity_type, {}).get(entity_id)
+
+        # 遍历所有类型查找
+        for type_name, entities in entities_v3.items():
+            if entity_id in entities:
+                return entities[entity_id]
+        return None
+
+    def get_entity_type(self, entity_id: str) -> Optional[str]:
+        """获取实体所属类型"""
+        for type_name, entities in self._state.get("entities_v3", {}).items():
+            if entity_id in entities:
+                return type_name
+        return None
+
+    def get_all_entities(self) -> Dict[str, Dict]:
+        """获取所有实体(扁平化视图,兼容旧代码)"""
+        result = {}
+        for type_name, entities in self._state.get("entities_v3", {}).items():
+            for eid, e in entities.items():
+                result[eid] = {**e, "type": type_name}
+        return result
+
+    def get_entities_by_type(self, entity_type: str) -> Dict[str, Dict]:
+        """按类型获取实体"""
+        return self._state.get("entities_v3", {}).get(entity_type, {})
+
+    def get_entities_by_tier(self, tier: str) -> Dict[str, Dict]:
+        """按层级获取实体"""
+        result = {}
+        for type_name, entities in self._state.get("entities_v3", {}).items():
+            for eid, e in entities.items():
+                if e.get("tier") == tier:
+                    result[eid] = {**e, "type": type_name}
+        return result
+
+    def add_entity(self, entity: EntityState) -> bool:
+        """添加新实体 (v5.0 entities_v3 格式)"""
+        entity_type = entity.type
+        if entity_type not in self.ENTITY_TYPES:
+            entity_type = "角色"
+
+        if "entities_v3" not in self._state:
+            self._state["entities_v3"] = {t: {} for t in self.ENTITY_TYPES}
+
+        if entity_type not in self._state["entities_v3"]:
+            self._state["entities_v3"][entity_type] = {}
+
+        # 检查是否已存在
+        if entity.id in self._state["entities_v3"][entity_type]:
+            return False
+
+        # 转换为 v3 格式
+        v3_entity = {
+            "canonical_name": entity.name,
+            "tier": entity.tier,
+            "desc": "",
+            "current": entity.attributes,
+            "first_appearance": entity.first_appearance,
+            "last_appearance": entity.last_appearance,
+            "history": []
+        }
+        self._state["entities_v3"][entity_type][entity.id] = v3_entity
+
+        # 记录实体补丁(新建:仅填充缺失字段,避免覆盖并发写入)
+        patch = self._pending_entity_patches.get((entity_type, entity.id))
+        if patch is None:
+            patch = _EntityPatch(entity_type=entity_type, entity_id=entity.id)
+            self._pending_entity_patches[(entity_type, entity.id)] = patch
+        patch.replace = True
+        patch.base_entity = v3_entity
+
+        # 注册别名到 alias_index
+        self._register_alias_internal(entity.id, entity_type, entity.name)
+        for alias in entity.aliases:
+            self._register_alias_internal(entity.id, entity_type, alias)
+
+        return True
+
+    def _register_alias_internal(self, entity_id: str, entity_type: str, alias: str):
+        """内部方法:注册别名到 state.json 的 alias_index"""
+        if not alias:
+            return
+        if "alias_index" not in self._state:
+            self._state["alias_index"] = {}
+
+        if alias not in self._state["alias_index"]:
+            self._state["alias_index"][alias] = []
+
+        # 检查是否已存在
+        exists = any(
+            e.get("type") == entity_type and e.get("id") == entity_id
+            for e in self._state["alias_index"][alias]
+        )
+        if not exists:
+            self._state["alias_index"][alias].append({
+                "type": entity_type,
+                "id": entity_id
+            })
+            # 记录待合并增量:避免锁外读-改-写覆盖
+            pending = self._pending_alias_entries.setdefault(alias, [])
+            if not any(e.get("type") == entity_type and e.get("id") == entity_id for e in pending):
+                pending.append({"type": entity_type, "id": entity_id})
+
+    def update_entity(self, entity_id: str, updates: Dict[str, Any], entity_type: str = None) -> bool:
+        """更新实体属性 (v5.0)"""
+        # 查找实体
+        if entity_type:
+            if entity_id not in self._state.get("entities_v3", {}).get(entity_type, {}):
+                return False
+            entity = self._state["entities_v3"][entity_type][entity_id]
+        else:
+            entity_type = self.get_entity_type(entity_id)
+            if not entity_type:
+                return False
+            entity = self._state["entities_v3"][entity_type][entity_id]
+
+        for key, value in updates.items():
+            if key == "attributes" and isinstance(value, dict):
+                # v5.0: attributes 存在 current 字段
+                if "current" not in entity:
+                    entity["current"] = {}
+                entity["current"].update(value)
+                # 记录补丁(current 增量)
+                patch = self._pending_entity_patches.get((entity_type, entity_id))
+                if patch is None:
+                    patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
+                    self._pending_entity_patches[(entity_type, entity_id)] = patch
+                patch.current_updates.update(value)
+            elif key == "current" and isinstance(value, dict):
+                if "current" not in entity:
+                    entity["current"] = {}
+                entity["current"].update(value)
+                patch = self._pending_entity_patches.get((entity_type, entity_id))
+                if patch is None:
+                    patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
+                    self._pending_entity_patches[(entity_type, entity_id)] = patch
+                patch.current_updates.update(value)
+            else:
+                entity[key] = value
+                patch = self._pending_entity_patches.get((entity_type, entity_id))
+                if patch is None:
+                    patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
+                    self._pending_entity_patches[(entity_type, entity_id)] = patch
+                patch.top_updates[key] = value
+
+        return True
+
+    def update_entity_appearance(self, entity_id: str, chapter: int, entity_type: str = None):
+        """更新实体出场章节"""
+        if not entity_type:
+            entity_type = self.get_entity_type(entity_id)
+        if not entity_type:
+            return
+
+        entity = self._state["entities_v3"][entity_type].get(entity_id)
+        if entity:
+            if entity.get("first_appearance", 0) == 0:
+                entity["first_appearance"] = chapter
+            entity["last_appearance"] = chapter
+
+            # 记录补丁:锁内应用 first=min(non-zero), last=max
+            patch = self._pending_entity_patches.get((entity_type, entity_id))
+            if patch is None:
+                patch = _EntityPatch(entity_type=entity_type, entity_id=entity_id)
+                self._pending_entity_patches[(entity_type, entity_id)] = patch
+            if patch.appearance_chapter is None:
+                patch.appearance_chapter = chapter
+            else:
+                patch.appearance_chapter = max(int(patch.appearance_chapter), int(chapter))
+
+    # ==================== 状态变化记录 ====================
+
+    def record_state_change(
+        self,
+        entity_id: str,
+        field: str,
+        old_value: Any,
+        new_value: Any,
+        reason: str,
+        chapter: int
+    ):
+        """记录状态变化"""
+        if "state_changes" not in self._state:
+            self._state["state_changes"] = []
+
+        change = StateChange(
+            entity_id=entity_id,
+            field=field,
+            old_value=old_value,
+            new_value=new_value,
+            reason=reason,
+            chapter=chapter
+        )
+        change_dict = asdict(change)
+        self._state["state_changes"].append(change_dict)
+        self._pending_state_changes.append(change_dict)
+
+        # 同时更新实体属性
+        self.update_entity(entity_id, {"attributes": {field: new_value}})
+
+    def get_state_changes(self, entity_id: Optional[str] = None) -> List[Dict]:
+        """获取状态变化历史"""
+        changes = self._state.get("state_changes", [])
+        if entity_id:
+            changes = [c for c in changes if c.get("entity_id") == entity_id]
+        return changes
+
+    # ==================== 关系管理 ====================
+
+    def add_relationship(
+        self,
+        from_entity: str,
+        to_entity: str,
+        rel_type: str,
+        description: str,
+        chapter: int
+    ):
+        """添加关系"""
+        rel = Relationship(
+            from_entity=from_entity,
+            to_entity=to_entity,
+            type=rel_type,
+            description=description,
+            chapter=chapter
+        )
+
+        # v5.0: 实体关系存入 structured_relationships,避免与 relationships(人物关系字典) 冲突
+        if "structured_relationships" not in self._state:
+            self._state["structured_relationships"] = []
+        rel_dict = asdict(rel)
+        self._state["structured_relationships"].append(rel_dict)
+        self._pending_structured_relationships.append(rel_dict)
+
+    def get_relationships(self, entity_id: Optional[str] = None) -> List[Dict]:
+        """获取关系列表"""
+        rels = self._state.get("structured_relationships", [])
+        if entity_id:
+            rels = [
+                r for r in rels
+                if r.get("from_entity") == entity_id or r.get("to_entity") == entity_id
+            ]
+        return rels
+
+    # ==================== 批量操作 ====================
+
+    def _record_disambiguation(self, chapter: int, uncertain_items: Any) -> List[str]:
+        """
+        记录消歧反馈到 state.json,便于 Writer/Context Agent 感知风险。
+
+        约定:
+        - >= extraction_confidence_medium:写入 disambiguation_warnings(采用但警告)
+        - < extraction_confidence_medium:写入 disambiguation_pending(需人工确认)
+        """
+        if not isinstance(uncertain_items, list) or not uncertain_items:
+            return []
+
+        warnings: List[str] = []
+        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+        for item in uncertain_items:
+            if not isinstance(item, dict):
+                continue
+
+            mention = str(item.get("mention", "") or "").strip()
+            if not mention:
+                continue
+
+            raw_conf = item.get("confidence", 0.0)
+            try:
+                confidence = float(raw_conf)
+            except (TypeError, ValueError):
+                confidence = 0.0
+
+            # 候选:支持 [{"type","id"}...] 或 ["id1","id2"] 两种形式
+            candidates_raw = item.get("candidates", [])
+            candidates: List[Dict[str, str]] = []
+            if isinstance(candidates_raw, list):
+                for c in candidates_raw:
+                    if isinstance(c, dict):
+                        cid = str(c.get("id", "") or "").strip()
+                        ctype = str(c.get("type", "") or "").strip()
+                        entry: Dict[str, str] = {}
+                        if ctype:
+                            entry["type"] = ctype
+                        if cid:
+                            entry["id"] = cid
+                        if entry:
+                            candidates.append(entry)
+                    else:
+                        cid = str(c).strip()
+                        if cid:
+                            candidates.append({"id": cid})
+
+            entity_type = str(item.get("type", "") or "").strip()
+            suggested_id = str(item.get("suggested", "") or "").strip()
+
+            adopted_raw = item.get("adopted", None)
+            chosen_id = ""
+            if isinstance(adopted_raw, str):
+                chosen_id = adopted_raw.strip()
+            elif adopted_raw is True:
+                chosen_id = suggested_id
+            else:
+                # 兼容字段名:entity_id / chosen_id
+                chosen_id = str(item.get("entity_id") or item.get("chosen_id") or "").strip() or suggested_id
+
+            context = str(item.get("context", "") or "").strip()
+            note = str(item.get("warning", "") or "").strip()
+
+            record: Dict[str, Any] = {
+                "chapter": int(chapter),
+                "mention": mention,
+                "type": entity_type,
+                "suggested_id": suggested_id,
+                "chosen_id": chosen_id,
+                "confidence": confidence,
+                "candidates": candidates,
+                "context": context,
+                "note": note,
+                "created_at": now,
+            }
+
+            if confidence >= float(self.config.extraction_confidence_medium):
+                self._state.setdefault("disambiguation_warnings", []).append(record)
+                self._pending_disambiguation_warnings.append(record)
+                chosen_part = f" → {chosen_id}" if chosen_id else ""
+                warnings.append(f"消歧警告: {mention}{chosen_part} (confidence: {confidence:.2f})")
+            else:
+                self._state.setdefault("disambiguation_pending", []).append(record)
+                self._pending_disambiguation_pending.append(record)
+                warnings.append(f"消歧需人工确认: {mention} (confidence: {confidence:.2f})")
+
+        return warnings
+
+    def process_chapter_result(self, chapter: int, result: Dict) -> List[str]:
+        """
+        处理 Data Agent 的章节处理结果 (v5.0)
+
+        输入格式:
+        - entities_appeared: 出场实体列表
+        - entities_new: 新实体列表
+        - state_changes: 状态变化列表
+        - relationships_new: 新关系列表
+
+        返回警告列表
+        """
+        warnings = []
+
+        # 处理出场实体
+        for entity in result.get("entities_appeared", []):
+            entity_id = entity.get("id")
+            entity_type = entity.get("type")
+            if entity_id:
+                self.update_entity_appearance(entity_id, chapter, entity_type)
+
+        # 处理新实体
+        for entity in result.get("entities_new", []):
+            entity_id = entity.get("suggested_id") or entity.get("id")
+            if entity_id and entity_id != "NEW":
+                new_entity = EntityState(
+                    id=entity_id,
+                    name=entity.get("name", ""),
+                    type=entity.get("type", "角色"),
+                    tier=entity.get("tier", "装饰"),
+                    aliases=entity.get("mentions", []),
+                    first_appearance=chapter,
+                    last_appearance=chapter
+                )
+                if not self.add_entity(new_entity):
+                    warnings.append(f"实体已存在: {entity_id}")
+
+        # 处理状态变化
+        for change in result.get("state_changes", []):
+            self.record_state_change(
+                entity_id=change.get("entity_id", ""),
+                field=change.get("field", ""),
+                old_value=change.get("old"),
+                new_value=change.get("new"),
+                reason=change.get("reason", ""),
+                chapter=chapter
+            )
+
+        # 处理关系
+        for rel in result.get("relationships_new", []):
+            self.add_relationship(
+                from_entity=rel.get("from", ""),
+                to_entity=rel.get("to", ""),
+                rel_type=rel.get("type", ""),
+                description=rel.get("description", ""),
+                chapter=chapter
+            )
+
+        # 处理消歧不确定项(不影响实体写入,但必须对 Writer 可见)
+        warnings.extend(self._record_disambiguation(chapter, result.get("uncertain", [])))
+
+        # 更新进度
+        self.update_progress(chapter)
+
+        # 同步主角状态(entities_v3 → protagonist_state)
+        self.sync_protagonist_from_entity()
+
+        return warnings
+
+    # ==================== 导出 ====================
+
+    def export_for_context(self) -> Dict:
+        """导出用于上下文的精简版状态 (v5.0)"""
+        # 从 entities_v3 构建精简视图
+        entities_flat = {}
+        for type_name, entities in self._state.get("entities_v3", {}).items():
+            for eid, e in entities.items():
+                entities_flat[eid] = {
+                    "name": e.get("canonical_name", eid),
+                    "type": type_name,
+                    "tier": e.get("tier", "装饰"),
+                    "current": e.get("current", {})
+                }
+
+        return {
+            "progress": self._state.get("progress", {}),
+            "entities": entities_flat,
+            "alias_index": self._state.get("alias_index", {}),
+            "recent_changes": self._state.get("state_changes", [])[-self.config.export_recent_changes_slice:],
+            "disambiguation": {
+                "warnings": self._state.get("disambiguation_warnings", [])[-self.config.export_disambiguation_slice:],
+                "pending": self._state.get("disambiguation_pending", [])[-self.config.export_disambiguation_slice:],
+            },
+        }
+
+    # ==================== 主角同步 ====================
+
+    def get_protagonist_entity_id(self) -> Optional[str]:
+        """获取主角实体 ID(通过 is_protagonist 标记或 protagonist_state.name 查找)"""
+        # 方式1: 查找 is_protagonist 标记
+        for eid, e in self._state.get("entities_v3", {}).get("角色", {}).items():
+            if e.get("is_protagonist"):
+                return eid
+
+        # 方式2: 通过 protagonist_state.name 查找
+        protag_name = self._state.get("protagonist_state", {}).get("name")
+        if protag_name:
+            alias_entries = self._state.get("alias_index", {}).get(protag_name, [])
+            for entry in alias_entries:
+                if entry.get("type") == "角色":
+                    return entry.get("id")
+
+        return None
+
+    def sync_protagonist_from_entity(self, entity_id: str = None):
+        """
+        将 entities_v3 中主角实体的状态同步到 protagonist_state
+
+        用于确保 consistency-checker 等依赖 protagonist_state 的组件获取最新数据
+        """
+        if entity_id is None:
+            entity_id = self.get_protagonist_entity_id()
+        if entity_id is None:
+            return
+
+        entity = self.get_entity(entity_id, "角色")
+        if not entity:
+            return
+
+        current = entity.get("current", {})
+        protag = self._state.setdefault("protagonist_state", {})
+
+        # 同步境界
+        if "realm" in current:
+            power = protag.setdefault("power", {})
+            power["realm"] = current["realm"]
+            if "layer" in current:
+                power["layer"] = current["layer"]
+
+        # 同步位置
+        if "location" in current:
+            loc = protag.setdefault("location", {})
+            loc["current"] = current["location"]
+            if "last_chapter" in current:
+                loc["last_chapter"] = current["last_chapter"]
+
+    def sync_protagonist_to_entity(self, entity_id: str = None):
+        """
+        将 protagonist_state 同步到 entities_v3 中的主角实体
+
+        用于初始化或手动编辑 protagonist_state 后保持一致性
+        """
+        if entity_id is None:
+            entity_id = self.get_protagonist_entity_id()
+        if entity_id is None:
+            return
+
+        protag = self._state.get("protagonist_state", {})
+        if not protag:
+            return
+
+        updates = {}
+
+        # 同步境界
+        power = protag.get("power", {})
+        if power.get("realm"):
+            updates["realm"] = power["realm"]
+        if power.get("layer"):
+            updates["layer"] = power["layer"]
+
+        # 同步位置
+        loc = protag.get("location", {})
+        if loc.get("current"):
+            updates["location"] = loc["current"]
+
+        if updates:
+            self.update_entity(entity_id, updates, "角色")
+
+
+# ==================== CLI 接口 ====================
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(description="State Manager CLI")
+    parser.add_argument("--project-root", type=str, help="项目根目录")
+
+    subparsers = parser.add_subparsers(dest="command")
+
+    # 获取进度
+    subparsers.add_parser("get-progress")
+
+    # 获取实体
+    get_entity_parser = subparsers.add_parser("get-entity")
+    get_entity_parser.add_argument("--id", required=True, help="实体ID")
+
+    # 列出实体
+    list_parser = subparsers.add_parser("list-entities")
+    list_parser.add_argument("--type", help="按类型过滤")
+    list_parser.add_argument("--tier", help="按层级过滤")
+
+    # 处理章节结果
+    process_parser = subparsers.add_parser("process-chapter")
+    process_parser.add_argument("--chapter", type=int, required=True, help="章节号")
+    process_parser.add_argument("--data", required=True, help="JSON 格式的处理结果")
+
+    args = parser.parse_args()
+
+    # 初始化
+    config = None
+    if args.project_root:
+        from .config import DataModulesConfig
+        config = DataModulesConfig.from_project_root(args.project_root)
+
+    manager = StateManager(config)
+
+    if args.command == "get-progress":
+        print(json.dumps(manager._state.get("progress", {}), ensure_ascii=False, indent=2))
+
+    elif args.command == "get-entity":
+        entity = manager.get_entity(args.id)
+        if entity:
+            print(json.dumps(entity, ensure_ascii=False, indent=2))
+        else:
+            print(f"未找到实体: {args.id}")
+
+    elif args.command == "list-entities":
+        if args.type:
+            entities = manager.get_entities_by_type(args.type)
+        elif args.tier:
+            entities = manager.get_entities_by_tier(args.tier)
+        else:
+            entities = manager.get_all_entities()
+
+        for eid, e in entities.items():
+            print(f"{eid}: {e.get('name')} ({e.get('type')}/{e.get('tier')})")
+
+    elif args.command == "process-chapter":
+        data = json.loads(args.data)
+        warnings = manager.process_chapter_result(args.chapter, data)
+        manager.save_state()
+
+        print(f"✓ 已处理第 {args.chapter} 章")
+        if warnings:
+            print("警告:")
+            for w in warnings:
+                print(f"  - {w}")
+
+
+if __name__ == "__main__":
+    main()

+ 385 - 0
.claude/scripts/data_modules/style_sampler.py

@@ -0,0 +1,385 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Style Sampler - 风格样本管理模块
+
+管理高质量章节片段作为风格参考:
+- 风格样本存储
+- 按场景类型分类
+- 样本选择策略
+"""
+
+import json
+import sqlite3
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+from dataclasses import dataclass, asdict
+from datetime import datetime
+from enum import Enum
+from contextlib import contextmanager
+
+from .config import get_config
+
+
+class SceneType(Enum):
+    """场景类型"""
+    BATTLE = "战斗"
+    DIALOGUE = "对话"
+    DESCRIPTION = "描写"
+    TRANSITION = "过渡"
+    EMOTION = "情感"
+    TENSION = "紧张"
+    COMEDY = "轻松"
+
+
+@dataclass
+class StyleSample:
+    """风格样本"""
+    id: str
+    chapter: int
+    scene_type: str
+    content: str
+    score: float
+    tags: List[str]
+    created_at: str = ""
+
+
+class StyleSampler:
+    """风格样本管理器"""
+
+    def __init__(self, config=None):
+        self.config = config or get_config()
+        self._init_db()
+
+    def _init_db(self):
+        """初始化数据库"""
+        self.config.ensure_dirs()
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS samples (
+                    id TEXT PRIMARY KEY,
+                    chapter INTEGER,
+                    scene_type TEXT,
+                    content TEXT,
+                    score REAL,
+                    tags TEXT,
+                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+                )
+            """)
+
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_samples_type ON samples(scene_type)")
+            cursor.execute("CREATE INDEX IF NOT EXISTS idx_samples_score ON samples(score DESC)")
+
+            conn.commit()
+
+    @contextmanager
+    def _get_conn(self):
+        """获取数据库连接(确保关闭,避免 Windows 下文件句柄泄漏导致无法清理临时目录)"""
+        db_path = self.config.webnovel_dir / "style_samples.db"
+        conn = sqlite3.connect(str(db_path))
+        try:
+            yield conn
+        finally:
+            conn.close()
+
+    # ==================== 样本管理 ====================
+
+    def add_sample(self, sample: StyleSample) -> bool:
+        """添加风格样本"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            try:
+                cursor.execute("""
+                    INSERT INTO samples
+                    (id, chapter, scene_type, content, score, tags, created_at)
+                    VALUES (?, ?, ?, ?, ?, ?, ?)
+                """, (
+                    sample.id,
+                    sample.chapter,
+                    sample.scene_type,
+                    sample.content,
+                    sample.score,
+                    json.dumps(sample.tags, ensure_ascii=False),
+                    sample.created_at or datetime.now().isoformat()
+                ))
+                conn.commit()
+                return True
+            except sqlite3.IntegrityError:
+                return False
+
+    def get_samples_by_type(
+        self,
+        scene_type: str,
+        limit: int = 5,
+        min_score: float = 0.0
+    ) -> List[StyleSample]:
+        """按场景类型获取样本"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT id, chapter, scene_type, content, score, tags, created_at
+                FROM samples
+                WHERE scene_type = ? AND score >= ?
+                ORDER BY score DESC
+                LIMIT ?
+            """, (scene_type, min_score, limit))
+
+            return [self._row_to_sample(row) for row in cursor.fetchall()]
+
+    def get_best_samples(self, limit: int = 10) -> List[StyleSample]:
+        """获取最高分样本"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute("""
+                SELECT id, chapter, scene_type, content, score, tags, created_at
+                FROM samples
+                ORDER BY score DESC
+                LIMIT ?
+            """, (limit,))
+
+            return [self._row_to_sample(row) for row in cursor.fetchall()]
+
+    def _row_to_sample(self, row) -> StyleSample:
+        """将数据库行转换为样本对象"""
+        return StyleSample(
+            id=row[0],
+            chapter=row[1],
+            scene_type=row[2],
+            content=row[3],
+            score=row[4],
+            tags=json.loads(row[5]) if row[5] else [],
+            created_at=row[6]
+        )
+
+    # ==================== 样本提取 ====================
+
+    def extract_candidates(
+        self,
+        chapter: int,
+        content: str,
+        review_score: float,
+        scenes: List[Dict]
+    ) -> List[StyleSample]:
+        """
+        从章节中提取风格样本候选
+
+        只有高分章节 (review_score >= 80) 才提取样本
+        """
+        if review_score < 80:
+            return []
+
+        candidates = []
+
+        for scene in scenes:
+            scene_type = self._classify_scene_type(scene)
+            scene_content = scene.get("content", "")
+
+            # 跳过过短的场景
+            if len(scene_content) < 200:
+                continue
+
+            # 创建样本
+            sample = StyleSample(
+                id=f"ch{chapter}_s{scene.get('index', 0)}",
+                chapter=chapter,
+                scene_type=scene_type,
+                content=scene_content[:2000],  # 限制长度
+                score=review_score / 100.0,
+                tags=self._extract_tags(scene_content)
+            )
+            candidates.append(sample)
+
+        return candidates
+
+    def _classify_scene_type(self, scene: Dict) -> str:
+        """分类场景类型"""
+        summary = scene.get("summary", "").lower()
+        content = scene.get("content", "").lower()
+
+        # 简单关键词分类
+        battle_keywords = ["战斗", "攻击", "出手", "拳", "剑", "杀", "打", "斗"]
+        dialogue_keywords = ["说道", "问道", "笑道", "冷声", "对话"]
+        emotion_keywords = ["心中", "感觉", "情", "泪", "痛", "喜"]
+        tension_keywords = ["危险", "紧张", "恐惧", "压力"]
+
+        text = summary + content
+
+        if any(kw in text for kw in battle_keywords):
+            return SceneType.BATTLE.value
+        elif any(kw in text for kw in tension_keywords):
+            return SceneType.TENSION.value
+        elif any(kw in text for kw in dialogue_keywords):
+            return SceneType.DIALOGUE.value
+        elif any(kw in text for kw in emotion_keywords):
+            return SceneType.EMOTION.value
+        else:
+            return SceneType.DESCRIPTION.value
+
+    def _extract_tags(self, content: str) -> List[str]:
+        """提取内容标签"""
+        tags = []
+
+        # 简单标签提取
+        if "战斗" in content or "攻击" in content:
+            tags.append("战斗")
+        if "修炼" in content or "突破" in content:
+            tags.append("修炼")
+        if "对话" in content or "说道" in content:
+            tags.append("对话")
+        if "描写" in content or "景色" in content:
+            tags.append("描写")
+
+        return tags[:5]
+
+    # ==================== 样本选择 ====================
+
+    def select_samples_for_chapter(
+        self,
+        chapter_outline: str,
+        target_types: List[str] = None,
+        max_samples: int = 3
+    ) -> List[StyleSample]:
+        """
+        为章节写作选择合适的风格样本
+
+        基于大纲分析需要什么类型的样本
+        """
+        if target_types is None:
+            # 根据大纲推断需要的场景类型
+            target_types = self._infer_scene_types(chapter_outline)
+
+        samples = []
+        per_type = max(1, max_samples // len(target_types)) if target_types else max_samples
+
+        for scene_type in target_types:
+            type_samples = self.get_samples_by_type(scene_type, limit=per_type, min_score=0.8)
+            samples.extend(type_samples)
+
+        return samples[:max_samples]
+
+    def _infer_scene_types(self, outline: str) -> List[str]:
+        """从大纲推断需要的场景类型"""
+        types = []
+
+        if any(kw in outline for kw in ["战斗", "对决", "比试", "交手"]):
+            types.append(SceneType.BATTLE.value)
+
+        if any(kw in outline for kw in ["对话", "谈话", "商议", "讨论"]):
+            types.append(SceneType.DIALOGUE.value)
+
+        if any(kw in outline for kw in ["情感", "感情", "心理"]):
+            types.append(SceneType.EMOTION.value)
+
+        if not types:
+            types = [SceneType.DESCRIPTION.value]
+
+        return types
+
+    # ==================== 统计 ====================
+
+    def get_stats(self) -> Dict[str, Any]:
+        """获取样本统计"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("SELECT COUNT(*) FROM samples")
+            total = cursor.fetchone()[0]
+
+            cursor.execute("""
+                SELECT scene_type, COUNT(*) as count
+                FROM samples
+                GROUP BY scene_type
+            """)
+            by_type = {row[0]: row[1] for row in cursor.fetchall()}
+
+            cursor.execute("SELECT AVG(score) FROM samples")
+            avg_score = cursor.fetchone()[0] or 0
+
+            return {
+                "total": total,
+                "by_type": by_type,
+                "avg_score": round(avg_score, 3)
+            }
+
+
+# ==================== CLI 接口 ====================
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(description="Style Sampler CLI")
+    parser.add_argument("--project-root", type=str, help="项目根目录")
+
+    subparsers = parser.add_subparsers(dest="command")
+
+    # 获取统计
+    subparsers.add_parser("stats")
+
+    # 列出样本
+    list_parser = subparsers.add_parser("list")
+    list_parser.add_argument("--type", help="按类型过滤")
+    list_parser.add_argument("--limit", type=int, default=10)
+
+    # 提取样本
+    extract_parser = subparsers.add_parser("extract")
+    extract_parser.add_argument("--chapter", type=int, required=True)
+    extract_parser.add_argument("--score", type=float, required=True)
+    extract_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
+
+    # 选择样本
+    select_parser = subparsers.add_parser("select")
+    select_parser.add_argument("--outline", required=True, help="章节大纲")
+    select_parser.add_argument("--max", type=int, default=3)
+
+    args = parser.parse_args()
+
+    # 初始化
+    config = None
+    if args.project_root:
+        from .config import DataModulesConfig
+        config = DataModulesConfig.from_project_root(args.project_root)
+
+    sampler = StyleSampler(config)
+
+    if args.command == "stats":
+        stats = sampler.get_stats()
+        print(json.dumps(stats, ensure_ascii=False, indent=2))
+
+    elif args.command == "list":
+        if args.type:
+            samples = sampler.get_samples_by_type(args.type, args.limit)
+        else:
+            samples = sampler.get_best_samples(args.limit)
+
+        for s in samples:
+            print(f"\n[{s.scene_type}] 第 {s.chapter} 章 (score: {s.score:.2f})")
+            print(f"  {s.content[:100]}...")
+
+    elif args.command == "extract":
+        scenes = json.loads(args.scenes)
+        candidates = sampler.extract_candidates(
+            chapter=args.chapter,
+            content="",
+            review_score=args.score,
+            scenes=scenes
+        )
+
+        for c in candidates:
+            if sampler.add_sample(c):
+                print(f"✓ 添加样本: {c.id} ({c.scene_type})")
+            else:
+                print(f"✗ 样本已存在: {c.id}")
+
+    elif args.command == "select":
+        samples = sampler.select_samples_for_chapter(args.outline, max_samples=args.max)
+
+        print(f"选择了 {len(samples)} 个风格样本:")
+        for s in samples:
+            print(f"\n[{s.scene_type}] 第 {s.chapter} 章")
+            print(f"  {s.content[:200]}...")
+
+
+if __name__ == "__main__":
+    main()

+ 1 - 0
.claude/scripts/data_modules/tests/__init__.py

@@ -0,0 +1 @@
+# data_modules tests package

+ 488 - 0
.claude/scripts/data_modules/tests/test_data_modules.py

@@ -0,0 +1,488 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Data Modules 单元测试
+"""
+
+import pytest
+import asyncio
+import json
+import tempfile
+from pathlib import Path
+
+from data_modules import (
+    DataModulesConfig,
+    EntityLinker,
+    StateManager,
+    IndexManager,
+    RAGAdapter,
+    StyleSampler,
+    EntityState,
+    ChapterMeta,
+    SceneMeta,
+    StyleSample,
+)
+
+
+@pytest.fixture
+def temp_project():
+    """创建临时项目目录"""
+    with tempfile.TemporaryDirectory() as tmpdir:
+        config = DataModulesConfig.from_project_root(tmpdir)
+        config.ensure_dirs()
+        yield config
+
+
+class TestEntityLinker:
+    """实体链接器测试"""
+
+    def test_register_and_lookup_alias(self, temp_project):
+        linker = EntityLinker(temp_project)
+
+        # 注册别名
+        assert linker.register_alias("xiaoyan", "萧炎")
+        assert linker.register_alias("xiaoyan", "小炎子")
+
+        # 查找
+        assert linker.lookup_alias("萧炎") == "xiaoyan"
+        assert linker.lookup_alias("小炎子") == "xiaoyan"
+        assert linker.lookup_alias("不存在") is None
+
+    def test_alias_one_to_many(self, temp_project):
+        """v5.0: 同一别名可映射多个实体(一对多)"""
+        linker = EntityLinker(temp_project)
+
+        linker.register_alias("xiaoyan", "萧炎", "角色")
+        # v5.0: 同一别名可绑定不同实体(一对多)
+        assert linker.register_alias("other_person", "萧炎", "角色")
+
+        # 查找所有匹配
+        entries = linker.lookup_alias_all("萧炎")
+        assert len(entries) == 2
+
+    def test_get_all_aliases(self, temp_project):
+        linker = EntityLinker(temp_project)
+
+        linker.register_alias("xiaoyan", "萧炎")
+        linker.register_alias("xiaoyan", "小炎子")
+        linker.register_alias("xiaoyan", "炎哥")
+
+        aliases = linker.get_all_aliases("xiaoyan")
+        assert len(aliases) == 3
+        assert "萧炎" in aliases
+
+    def test_confidence_evaluation(self, temp_project):
+        linker = EntityLinker(temp_project)
+
+        # 高置信度
+        action, adopt, warning = linker.evaluate_confidence(0.9)
+        assert action == "auto"
+        assert adopt is True
+        assert warning is None
+
+        # 中置信度
+        action, adopt, warning = linker.evaluate_confidence(0.6)
+        assert action == "warn"
+        assert adopt is True
+        assert warning is not None
+
+        # 低置信度
+        action, adopt, warning = linker.evaluate_confidence(0.3)
+        assert action == "manual"
+        assert adopt is False
+
+    def test_process_uncertain(self, temp_project):
+        linker = EntityLinker(temp_project)
+
+        result = linker.process_uncertain(
+            mention="那位前辈",
+            candidates=["yaolao", "elder_zhang"],
+            suggested="yaolao",
+            confidence=0.7
+        )
+
+        assert result.mention == "那位前辈"
+        assert result.entity_id == "yaolao"
+        assert result.adopted is True
+        assert result.warning is not None
+
+
+class TestStateManager:
+    """状态管理器测试"""
+
+    def test_add_and_get_entity(self, temp_project):
+        manager = StateManager(temp_project)
+
+        entity = EntityState(
+            id="xiaoyan",
+            name="萧炎",
+            type="角色",
+            tier="核心"
+        )
+        assert manager.add_entity(entity)
+
+        # 获取实体
+        result = manager.get_entity("xiaoyan")
+        assert result is not None
+        assert result["canonical_name"] == "萧炎"
+
+    def test_update_entity(self, temp_project):
+        manager = StateManager(temp_project)
+
+        entity = EntityState(id="xiaoyan", name="萧炎", type="角色")
+        manager.add_entity(entity)
+
+        # 更新属性 (v5.0: attributes 存在 current 字段)
+        manager.update_entity("xiaoyan", {"current": {"realm": "斗师"}})
+
+        result = manager.get_entity("xiaoyan")
+        assert result["current"]["realm"] == "斗师"
+
+    def test_record_state_change(self, temp_project):
+        manager = StateManager(temp_project)
+
+        entity = EntityState(id="xiaoyan", name="萧炎", type="角色")
+        manager.add_entity(entity)
+
+        manager.record_state_change(
+            entity_id="xiaoyan",
+            field="realm",
+            old_value="斗者",
+            new_value="斗师",
+            reason="突破",
+            chapter=100
+        )
+
+        changes = manager.get_state_changes("xiaoyan")
+        assert len(changes) == 1
+        assert changes[0]["new_value"] == "斗师"
+
+    def test_add_relationship(self, temp_project):
+        manager = StateManager(temp_project)
+
+        manager.add_relationship(
+            from_entity="xiaoyan",
+            to_entity="yaolao",
+            rel_type="师徒",
+            description="药老收萧炎为徒",
+            chapter=10
+        )
+
+        rels = manager.get_relationships("xiaoyan")
+        assert len(rels) == 1
+        assert rels[0]["type"] == "师徒"
+
+    def test_process_chapter_result(self, temp_project):
+        manager = StateManager(temp_project)
+
+        result = {
+            "entities_appeared": [
+                {"id": "xiaoyan", "mentions": ["萧炎", "他"]}
+            ],
+            "entities_new": [
+                {"suggested_id": "hongyi_girl", "name": "红衣女子", "type": "角色", "tier": "装饰"}
+            ],
+            "state_changes": [
+                {"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破"}
+            ],
+            "relationships_new": [
+                {"from": "xiaoyan", "to": "hongyi_girl", "type": "相识", "description": "初次见面"}
+            ]
+        }
+
+        # 先添加萧炎
+        manager.add_entity(EntityState(id="xiaoyan", name="萧炎", type="角色"))
+
+        warnings = manager.process_chapter_result(100, result)
+
+        # 验证新实体被添加
+        assert manager.get_entity("hongyi_girl") is not None
+
+        # 验证状态变化
+        changes = manager.get_state_changes("xiaoyan")
+        assert len(changes) == 1
+
+        # 验证进度更新
+        assert manager.get_current_chapter() == 100
+
+    def test_save_state_with_init_project_schema(self, temp_project):
+        """回归:init_project 生成的 state.json 无 meta 字段,StateManager 仍应可写入。"""
+        # 模拟 init_project.py 生成的 v5.0 state.json 形状(包含 entities_v3/alias_index)
+        init_state = {
+            "project_info": {"title": "测试书名", "genre": "修仙/玄幻", "created_at": "2026-01-01"},
+            "progress": {"current_chapter": 0, "total_words": 0, "last_updated": "2026-01-01 00:00:00"},
+            "protagonist_state": {"name": "测试主角"},
+            "relationships": {},
+            "world_settings": {"power_system": [], "factions": [], "locations": []},
+            "plot_threads": {"active_threads": [], "foreshadowing": []},
+            "review_checkpoints": [],
+            "strand_tracker": {"current_dominant": "quest", "history": []},
+            "entities_v3": {"角色": {}, "地点": {}, "物品": {}, "势力": {}, "招式": {}},
+            "alias_index": {},
+        }
+        temp_project.state_file.write_text(json.dumps(init_state, ensure_ascii=False, indent=2), encoding="utf-8")
+
+        manager = StateManager(temp_project)
+        manager.update_progress(5, words=100)
+        manager.save_state()
+
+        saved = json.loads(temp_project.state_file.read_text(encoding="utf-8"))
+        assert "meta" not in saved
+        assert saved["progress"]["current_chapter"] == 5
+        assert saved["progress"]["total_words"] == 100
+        assert isinstance(saved.get("entities_v3"), dict)
+        assert isinstance(saved.get("alias_index"), dict)
+
+    def test_save_state_preserves_unrelated_fields(self, temp_project):
+        """回归:仅写入增量,不应覆盖/丢失其他模块维护的字段。"""
+        init_state = {
+            "project_info": {"title": "测试书名", "genre": "修仙/玄幻", "created_at": "2026-01-01"},
+            "progress": {"current_chapter": 10, "total_words": 1000, "last_updated": "2026-01-01 00:00:00"},
+            "protagonist_state": {"name": "测试主角"},
+            "relationships": {"allies": ["药老"], "enemies": []},
+            "world_settings": {"power_system": [], "factions": [], "locations": []},
+            "plot_threads": {"active_threads": [{"id": "t1", "title": "主线"}], "foreshadowing": []},
+            "review_checkpoints": [],
+            "strand_tracker": {"current_dominant": "quest", "history": []},
+            "entities_v3": {"角色": {}, "地点": {}, "物品": {}, "势力": {}, "招式": {}},
+            "alias_index": {},
+            "custom_field": {"keep": True},
+        }
+        temp_project.state_file.write_text(json.dumps(init_state, ensure_ascii=False, indent=2), encoding="utf-8")
+
+        manager = StateManager(temp_project)
+        manager.add_entity(EntityState(id="xiaoyan", name="萧炎", type="角色", tier="核心"))
+        manager.save_state()
+
+        saved = json.loads(temp_project.state_file.read_text(encoding="utf-8"))
+        assert saved.get("custom_field", {}).get("keep") is True
+        assert saved.get("plot_threads", {}).get("active_threads", [])[0].get("id") == "t1"
+        assert isinstance(saved.get("relationships"), dict)
+
+    def test_disambiguation_feedback_persisted(self, temp_project):
+        """回归:中/低置信度消歧必须对 Writer 可见(写入 state.json)。"""
+        manager = StateManager(temp_project)
+
+        result = {
+            "entities_appeared": [],
+            "entities_new": [],
+            "state_changes": [],
+            "relationships_new": [],
+            "uncertain": [
+                {
+                    "mention": "那位前辈",
+                    "context": "那位前辈看了他一眼",
+                    "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}],
+                    "suggested": "yaolao",
+                    "confidence": 0.6,
+                },
+                {
+                    "mention": "宗主",
+                    "context": "宗主出现在血煞秘境",
+                    "candidates": ["xueshazonzhu", "lintian"],
+                    "suggested": "xueshazonzhu",
+                    "confidence": 0.4,
+                },
+            ],
+        }
+
+        warnings = manager.process_chapter_result(100, result)
+        manager.save_state()
+
+        state = json.loads(temp_project.state_file.read_text(encoding="utf-8"))
+        assert isinstance(state.get("disambiguation_warnings"), list)
+        assert isinstance(state.get("disambiguation_pending"), list)
+
+        assert len(state["disambiguation_warnings"]) == 1
+        assert len(state["disambiguation_pending"]) == 1
+
+        warn = state["disambiguation_warnings"][0]
+        assert warn.get("chapter") == 100
+        assert warn.get("mention") == "那位前辈"
+        assert warn.get("chosen_id") == "yaolao"
+
+        pending = state["disambiguation_pending"][0]
+        assert pending.get("chapter") == 100
+        assert pending.get("mention") == "宗主"
+
+        # 返回值也应包含可见警告,便于 CLI/日志透出
+        assert any("消歧警告" in w for w in warnings)
+        assert any("需人工确认" in w for w in warnings)
+
+
+class TestIndexManager:
+    """索引管理器测试"""
+
+    def test_add_and_get_chapter(self, temp_project):
+        manager = IndexManager(temp_project)
+
+        meta = ChapterMeta(
+            chapter=100,
+            title="突破",
+            location="天云宗",
+            word_count=3500,
+            characters=["xiaoyan", "yaolao"]
+        )
+        manager.add_chapter(meta)
+
+        result = manager.get_chapter(100)
+        assert result is not None
+        assert result["title"] == "突破"
+        assert "xiaoyan" in result["characters"]
+
+    def test_add_scenes(self, temp_project):
+        manager = IndexManager(temp_project)
+
+        scenes = [
+            SceneMeta(chapter=100, scene_index=1, start_line=1, end_line=50,
+                     location="天云宗·闭关室", summary="萧炎闭关突破", characters=["xiaoyan"]),
+            SceneMeta(chapter=100, scene_index=2, start_line=51, end_line=100,
+                     location="天云宗·演武场", summary="展示实力", characters=["xiaoyan", "lintian"])
+        ]
+        manager.add_scenes(100, scenes)
+
+        result = manager.get_scenes(100)
+        assert len(result) == 2
+        assert result[0]["location"] == "天云宗·闭关室"
+
+    def test_record_appearance(self, temp_project):
+        manager = IndexManager(temp_project)
+
+        manager.record_appearance("xiaoyan", 100, ["萧炎", "他"], 0.95)
+        manager.record_appearance("yaolao", 100, ["药老"], 0.92)
+
+        appearances = manager.get_chapter_appearances(100)
+        assert len(appearances) == 2
+
+        entity_history = manager.get_entity_appearances("xiaoyan")
+        assert len(entity_history) == 1
+
+    def test_search_scenes_by_location(self, temp_project):
+        manager = IndexManager(temp_project)
+
+        scenes = [
+            SceneMeta(chapter=100, scene_index=1, start_line=1, end_line=50,
+                     location="天云宗·闭关室", summary="闭关", characters=[]),
+            SceneMeta(chapter=101, scene_index=1, start_line=1, end_line=50,
+                     location="天云宗·大殿", summary="议事", characters=[])
+        ]
+        manager.add_scenes(100, scenes[:1])
+        manager.add_scenes(101, scenes[1:])
+
+        results = manager.search_scenes_by_location("天云宗")
+        assert len(results) == 2
+
+    def test_get_stats(self, temp_project):
+        manager = IndexManager(temp_project)
+
+        manager.add_chapter(ChapterMeta(chapter=1, title="", location="", word_count=1000, characters=[]))
+        manager.add_scenes(1, [SceneMeta(chapter=1, scene_index=1, start_line=1, end_line=50,
+                                        location="", summary="", characters=[])])
+        manager.record_appearance("xiaoyan", 1, [], 1.0)
+
+        stats = manager.get_stats()
+        assert stats["chapters"] == 1
+        assert stats["scenes"] == 1
+        assert stats["entities"] == 1
+
+
+class TestStyleSampler:
+    """风格样本测试"""
+
+    def test_add_and_get_sample(self, temp_project):
+        sampler = StyleSampler(temp_project)
+
+        sample = StyleSample(
+            id="ch100_s1",
+            chapter=100,
+            scene_type="战斗",
+            content="萧炎一拳轰出...",
+            score=0.85,
+            tags=["战斗", "激烈"]
+        )
+        assert sampler.add_sample(sample)
+
+        results = sampler.get_samples_by_type("战斗")
+        assert len(results) == 1
+        assert results[0].id == "ch100_s1"
+
+    def test_extract_candidates(self, temp_project):
+        sampler = StyleSampler(temp_project)
+
+        scenes = [
+            {"index": 1, "summary": "战斗场景", "content": "萧炎一拳轰出,斗气如虹,直接将对手击退三丈,周围的空气都被震得嗡嗡作响..." + "a" * 200}
+        ]
+
+        # 低分不提取
+        candidates = sampler.extract_candidates(100, "", 70, scenes)
+        assert len(candidates) == 0
+
+        # 高分提取
+        candidates = sampler.extract_candidates(100, "", 85, scenes)
+        assert len(candidates) == 1
+        assert candidates[0].scene_type == "战斗"
+
+    def test_select_samples_for_chapter(self, temp_project):
+        sampler = StyleSampler(temp_project)
+
+        # 添加一些样本
+        for i in range(3):
+            sampler.add_sample(StyleSample(
+                id=f"battle_{i}",
+                chapter=i,
+                scene_type="战斗",
+                content=f"战斗内容 {i}",
+                score=0.9,
+                tags=[]
+            ))
+
+        samples = sampler.select_samples_for_chapter("本章有一场激烈的战斗")
+        assert len(samples) <= 3
+        assert all(s.scene_type == "战斗" for s in samples)
+
+
+class TestRAGAdapter:
+    """RAG 适配器测试(不包含 API 调用)"""
+
+    def test_bm25_search(self, temp_project):
+        adapter = RAGAdapter(temp_project)
+
+        # 手动插入一些测试数据
+        with adapter._get_conn() as conn:
+            cursor = conn.cursor()
+
+            # 插入向量记录(空向量,只测试 BM25)
+            cursor.execute("""
+                INSERT INTO vectors (chunk_id, chapter, scene_index, content, embedding)
+                VALUES (?, ?, ?, ?, ?)
+            """, ("ch1_s1", 1, 1, "萧炎在天云宗修炼斗气", b""))
+
+            cursor.execute("""
+                INSERT INTO vectors (chunk_id, chapter, scene_index, content, embedding)
+                VALUES (?, ?, ?, ?, ?)
+            """, ("ch1_s2", 1, 2, "药老传授炼药技巧", b""))
+
+            conn.commit()
+
+            # 更新 BM25 索引
+            adapter._update_bm25_index(cursor, "ch1_s1", "萧炎在天云宗修炼斗气")
+            adapter._update_bm25_index(cursor, "ch1_s2", "药老传授炼药技巧")
+            conn.commit()
+
+        # BM25 搜索
+        results = adapter.bm25_search("萧炎修炼", top_k=5)
+        assert len(results) >= 1
+        assert results[0].chunk_id == "ch1_s1"
+
+    def test_tokenize(self, temp_project):
+        adapter = RAGAdapter(temp_project)
+
+        tokens = adapter._tokenize("萧炎hello世界world")
+        assert "萧" in tokens
+        assert "炎" in tokens
+        assert "hello" in tokens
+        assert "world" in tokens
+
+
+if __name__ == "__main__":
+    pytest.main([__file__, "-v"])

+ 217 - 0
.claude/scripts/extract_chapter_context.py

@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+"""
+extract_chapter_context.py - 提取章节创作所需的精简上下文
+
+功能:
+- 提取当前章节的大纲片段(~500字)
+- 提取前2章的摘要(~400字)
+- 提取 state.json 关键字段(~300字)
+
+用法:
+    python extract_chapter_context.py --chapter 7
+    python extract_chapter_context.py --chapter 7 --project-root ./webnovel-project
+"""
+
+import argparse
+import json
+import re
+import sys
+from pathlib import Path
+
+
+def find_project_root(start_path: Path = None) -> Path:
+    """查找包含 .webnovel 目录的项目根目录"""
+    if start_path is None:
+        start_path = Path.cwd()
+
+    search_paths = [
+        start_path,
+        start_path / "webnovel-project",
+        start_path.parent,
+    ]
+
+    for path in search_paths:
+        if (path / ".webnovel").exists():
+            return path
+
+    raise FileNotFoundError("未找到 .webnovel 目录,请确认项目路径")
+
+
+def extract_chapter_outline(project_root: Path, chapter_num: int) -> str:
+    """从大纲文件中提取指定章节的大纲片段"""
+    volume_num = (chapter_num - 1) // 50 + 1
+    outline_file = project_root / "大纲" / f"第{volume_num}卷-详细大纲.md"
+
+    if not outline_file.exists():
+        return f"⚠️ 大纲文件不存在: {outline_file}"
+
+    content = outline_file.read_text(encoding="utf-8")
+
+    # 匹配章节大纲块
+    # 格式:### 第 N 章:标题 或 ### 第 N 章: 标题
+    pattern = rf"###\s*第\s*{chapter_num}\s*章[::]\s*(.+?)(?=###\s*第\s*\d+\s*章|##\s|$)"
+    match = re.search(pattern, content, re.DOTALL)
+
+    if match:
+        outline = match.group(0).strip()
+        # 限制长度
+        if len(outline) > 1500:
+            outline = outline[:1500] + "\n...(已截断)"
+        return outline
+
+    # 尝试另一种格式:### 第 1 章:标题(无空格)
+    pattern2 = rf"###\s*第{chapter_num}章[::]\s*(.+?)(?=###\s*第\d+章|##\s|$)"
+    match2 = re.search(pattern2, content, re.DOTALL)
+
+    if match2:
+        outline = match2.group(0).strip()
+        if len(outline) > 1500:
+            outline = outline[:1500] + "\n...(已截断)"
+        return outline
+
+    return f"⚠️ 未找到第 {chapter_num} 章的大纲"
+
+
+def extract_chapter_summary(project_root: Path, chapter_num: int) -> str:
+    """提取指定章节的摘要(从章节文件末尾的"本章摘要"部分)"""
+    volume_num = (chapter_num - 1) // 50 + 1
+    chapter_dir = project_root / "正文" / f"第{volume_num}卷"
+
+    # 尝试匹配章节文件
+    patterns = [
+        f"第{chapter_num:03d}章*.md",
+        f"第{chapter_num:04d}章*.md",
+    ]
+
+    chapter_file = None
+    for pattern in patterns:
+        matches = list(chapter_dir.glob(pattern))
+        if matches:
+            chapter_file = matches[0]
+            break
+
+    if not chapter_file or not chapter_file.exists():
+        return f"⚠️ 第 {chapter_num} 章文件不存在"
+
+    content = chapter_file.read_text(encoding="utf-8")
+
+    # 尝试提取"本章摘要"部分
+    summary_match = re.search(r"##\s*本章摘要\s*\n(.+?)(?=##|$)", content, re.DOTALL)
+    if summary_match:
+        return summary_match.group(1).strip()
+
+    # 如果没有摘要,提取"本章统计"部分
+    stats_match = re.search(r"##\s*本章统计\s*\n(.+?)(?=##|$)", content, re.DOTALL)
+    if stats_match:
+        return f"[无摘要,仅统计]\n{stats_match.group(1).strip()}"
+
+    # 最后降级:提取前500字作为摘要
+    # 跳过标题
+    lines = content.split("\n")
+    text_lines = [l for l in lines if not l.startswith("#") and l.strip()]
+    text = "\n".join(text_lines)[:500]
+    return f"[自动截取前500字]\n{text}..."
+
+
+def extract_state_summary(project_root: Path) -> str:
+    """提取 state.json 的关键字段"""
+    state_file = project_root / ".webnovel" / "state.json"
+
+    if not state_file.exists():
+        return "⚠️ state.json 不存在"
+
+    state = json.loads(state_file.read_text(encoding="utf-8"))
+
+    # 提取关键字段
+    summary_parts = []
+
+    # 进度
+    if "progress" in state:
+        p = state["progress"]
+        summary_parts.append(f"**进度**: 第 {p.get('current_chapter', '?')} 章 / {p.get('total_words', '?')} 字")
+
+    # 主角状态
+    if "protagonist_state" in state:
+        ps = state["protagonist_state"]
+        power = ps.get("power", {})
+        summary_parts.append(f"**主角实力**: {power.get('realm', '?')} {power.get('layer', '?')}层")
+        summary_parts.append(f"**当前位置**: {ps.get('location', '?')}")
+
+        gf = ps.get("golden_finger", {})
+        summary_parts.append(f"**金手指**: {gf.get('name', '?')} Lv.{gf.get('level', '?')}")
+
+    # Strand 追踪
+    if "strand_tracker" in state:
+        st = state["strand_tracker"]
+        history = st.get("history", [])[-5:]  # 最近5章
+        if history:
+            strand_str = ", ".join([f"Ch{h['chapter']}:{h['strand']}" for h in history])
+            summary_parts.append(f"**近5章Strand**: {strand_str}")
+
+    # 活跃伏笔(只显示紧急的)
+    if "foreshadowing" in state:
+        fs = state["foreshadowing"]
+        active = [f for f in fs if f.get("status") == "active"]
+        urgent = [f for f in active if f.get("urgency", 0) > 50]
+        if urgent:
+            urgent_list = [f"{f.get('content', '?')[:30]}... (紧急度:{f.get('urgency')})" for f in urgent[:3]]
+            summary_parts.append(f"**紧急伏笔**: {'; '.join(urgent_list)}")
+
+    return "\n".join(summary_parts)
+
+
+def main():
+    parser = argparse.ArgumentParser(description="提取章节创作所需的精简上下文")
+    parser.add_argument("--chapter", type=int, required=True, help="目标章节号")
+    parser.add_argument("--project-root", type=str, help="项目根目录")
+    parser.add_argument("--format", choices=["text", "json"], default="text", help="输出格式")
+
+    args = parser.parse_args()
+
+    try:
+        if args.project_root:
+            project_root = Path(args.project_root)
+        else:
+            project_root = find_project_root()
+
+        chapter_num = args.chapter
+
+        # 提取各部分
+        outline = extract_chapter_outline(project_root, chapter_num)
+
+        # 提取前2章摘要
+        prev_summaries = []
+        for prev_ch in range(max(1, chapter_num - 2), chapter_num):
+            summary = extract_chapter_summary(project_root, prev_ch)
+            prev_summaries.append(f"### 第 {prev_ch} 章摘要\n{summary}")
+
+        state_summary = extract_state_summary(project_root)
+
+        if args.format == "json":
+            result = {
+                "chapter": chapter_num,
+                "outline": outline,
+                "previous_summaries": prev_summaries,
+                "state_summary": state_summary,
+            }
+            print(json.dumps(result, ensure_ascii=False, indent=2))
+        else:
+            print(f"# 第 {chapter_num} 章创作上下文\n")
+            print("## 本章大纲\n")
+            print(outline)
+            print("\n---\n")
+            print("## 前文摘要\n")
+            for s in prev_summaries:
+                print(s)
+                print()
+            print("---\n")
+            print("## 当前状态\n")
+            print(state_summary)
+
+    except Exception as e:
+        print(f"❌ 错误: {e}", file=sys.stderr)
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

+ 1816 - 0
.claude/scripts/extract_entities.py

@@ -0,0 +1,1816 @@
+#!/usr/bin/env python3
+"""
+XML 标签提取与同步脚本 (v4.0)
+
+> **v5.0 说明**: 此脚本用于**手动标注场景**(可选)。
+> v5.0 主流程使用 Data Agent 从纯正文进行 AI 语义提取,不再依赖 XML 标签。
+> 如果章节中包含 XML 标签,此脚本仍可用于解析和同步。
+
+功能:
+1. 扫描指定章节正文,提取所有 XML 格式标签
+2. 支持标签类型:
+   - <entity>: 实体(角色/地点/物品/势力/招式)
+   - <entity-alias>: 实体别名注册
+   - <entity-update>: 实体属性更新(支持 set/unset/add/remove/inc)
+   - <skill>: 金手指技能
+   - <foreshadow>: 伏笔标签
+   - <deviation>: 大纲偏离标记
+   - <relationship>: 角色关系
+3. 支持实体层级分类(核心/支线/装饰)
+4. 同步到设定集对应文件
+5. 更新 state.json(entities_v3 + alias_index 一对多)
+6. 支持自动化模式和交互式模式
+
+v4.0 变更:
+- alias_index 改为一对多(同一别名可映射多个实体)
+- 删除旧格式兼容代码
+- 新增操作:<unset>/<add>/<remove>/<inc>
+- 顶层字段白名单支持
+
+使用方式:
+  python extract_entities.py <章节文件> [--auto] [--dry-run]
+  python extract_entities.py --project-root "path" --chapter 1 --auto
+"""
+
+import re
+import json
+import os
+import shutil
+import sys
+import argparse
+from pathlib import Path
+from datetime import datetime
+from typing import List, Dict, Tuple, Optional, Any
+
+# ============================================================================
+# 安全修复:导入安全工具函数(P0 CRITICAL)
+# ============================================================================
+from security_utils import sanitize_filename, create_secure_directory, atomic_write_json
+from project_locator import resolve_project_root, resolve_state_file
+from chapter_paths import find_chapter_file, extract_chapter_num_from_filename
+
+# Windows 编码兼容性修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+# 实体类型与目标文件映射
+ENTITY_TYPE_MAP = {
+    "角色": "设定集/角色库/{category}/{name}.md",
+    "地点": "设定集/世界观.md",
+    "物品": "设定集/物品库/{name}.md",
+    "势力": "设定集/世界观.md",
+    "招式": "设定集/力量体系.md",
+    "其他": "设定集/其他设定/{name}.md"
+}
+
+# 有效实体类型(v4.0 不再兼容旧别名)
+VALID_ENTITY_TYPES = {"角色", "地点", "物品", "势力", "招式"}
+
+# 顶层字段白名单(可通过 entity-update 直接修改)
+TOP_LEVEL_FIELDS = {"tier", "desc", "canonical_name", "importance", "status", "parent"}
+
+
+class AmbiguousAliasError(RuntimeError):
+    """别名命中多个实体且无法消歧(必须改用 id 或补充 type)。"""
+
+
+def normalize_entity_type(raw: Any) -> str:
+    """验证实体类型(v4.0 不再支持别名转换)。"""
+    t = str(raw or "").strip()
+    if not t:
+        return ""
+    if t in VALID_ENTITY_TYPES:
+        return t
+    return ""  # 无效类型返回空
+
+# 角色分类规则
+ROLE_CATEGORY_MAP = {
+    "主角": "主要角色",
+    "配角": "次要角色",
+    "反派": "反派角色",
+    "路人": "次要角色"
+}
+
+# 实体层级权重(匹配伏笔三层级系统)
+ENTITY_TIER_MAP = {
+    "核心": {"weight": 3.0, "desc": "必须追踪,影响主线"},
+    "core": {"weight": 3.0, "desc": "必须追踪,影响主线"},
+    "支线": {"weight": 2.0, "desc": "应该追踪,丰富剧情"},
+    "sub": {"weight": 2.0, "desc": "应该追踪,丰富剧情"},
+    "装饰": {"weight": 1.0, "desc": "可选追踪,增加真实感"},
+    "decor": {"weight": 1.0, "desc": "可选追踪,增加真实感"}
+}
+
+# ============================================================================
+# 实体管理核心函数 (v3.0 新增)
+# ============================================================================
+
+def generate_entity_id(entity_type: str, name: str, existing_ids: set) -> str:
+    """
+    生成唯一实体 ID
+
+    规则:
+    1. 优先使用拼音(去空格、小写)
+    2. 冲突时追加数字后缀
+    3. 特殊前缀按类型
+
+    Args:
+        entity_type: 实体类型(角色/地点/物品/势力/招式)
+        name: 实体名称
+        existing_ids: 已存在的 ID 集合
+
+    Returns:
+        str: 唯一的实体 ID
+    """
+    # 类型前缀映射
+    prefix_map = {
+        "物品": "item_",
+        "势力": "faction_",
+        "招式": "skill_",
+        "地点": "loc_"
+        # 角色无前缀
+    }
+
+    # 尝试使用 pypinyin,如果不可用则用简单的 hash
+    try:
+        from pypinyin import lazy_pinyin
+        pinyin = ''.join(lazy_pinyin(name))
+        base_id = prefix_map.get(entity_type, '') + pinyin.lower()
+    except ImportError:
+        # pypinyin 不可用时,使用简化方案
+        import hashlib
+        hash_suffix = hashlib.md5(name.encode('utf-8')).hexdigest()[:8]
+        base_id = prefix_map.get(entity_type, '') + hash_suffix
+
+    # 清理非法字符
+    base_id = re.sub(r'[^a-z0-9_]', '', base_id)
+
+    # 处理冲突
+    final_id = base_id
+    counter = 1
+    while final_id in existing_ids:
+        final_id = f"{base_id}_{counter}"
+        counter += 1
+
+    return final_id
+
+
+def resolve_entity_by_alias(alias: str, entity_type: Optional[str], state: dict) -> Tuple[Optional[str], Optional[str], Optional[dict]]:
+    """
+    通过别名解析实体(v4.0 一对多版本)
+
+    Args:
+        alias: 别名或名称
+        entity_type: 实体类型提示(可选,用于歧义消解)
+        state: state.json 内容
+
+    Returns:
+        (entity_type, entity_id, entity_data) 或 (None, None, None)
+
+    Raises:
+        AmbiguousAliasError: 别名命中多个实体且无法消歧(必须改用 id 或补充 type)
+        ValueError: alias_index 数据格式不符合 v4.0 规范
+    """
+    alias_index = state.get("alias_index", {})
+
+    # alias_index 新格式: {"别名": [{"type": "角色", "id": "xxx"}, ...]}
+    entries = alias_index.get(alias)
+    if not entries:
+        return (None, None, None)
+
+    if not isinstance(entries, list):
+        raise ValueError(
+            f"alias_index 数据格式错误:期望 alias_index[{alias!r}] 为 list[{{type,id,...}}],实际为 {type(entries).__name__}"
+        )
+
+    # 只有一个匹配 -> 直接返回
+    if len(entries) == 1:
+        ref = entries[0]
+        et = ref.get("type", "")
+        eid = ref.get("id", "")
+        entities_v3 = state.get("entities_v3", {})
+        entity_data = entities_v3.get(et, {}).get(eid)
+        return (et, eid, entity_data) if entity_data else (None, None, None)
+
+    # 多个匹配 -> 尝试用 type 消解
+    if entity_type:
+        matches = [e for e in entries if e.get("type") == entity_type]
+        if len(matches) == 1:
+            ref = matches[0]
+            et = ref.get("type", "")
+            eid = ref.get("id", "")
+            entities_v3 = state.get("entities_v3", {})
+            entity_data = entities_v3.get(et, {}).get(eid)
+            return (et, eid, entity_data) if entity_data else (None, None, None)
+
+    # 歧义无法消解:必须强制报错,避免写错实体
+    raise AmbiguousAliasError(f"别名歧义: {alias!r} 命中 {len(entries)} 个实体,请改用 id 或补充 type 属性")
+
+
+def ensure_entities_v3_structure(state: dict) -> dict:
+    """
+    确保 state.json 有 entities_v3 和 alias_index 结构
+
+    entities_v3 格式:
+    {
+        "角色": {
+            "lintian": {
+                "id": "lintian",
+                "canonical_name": "林天",
+                "aliases": ["废物", "林天"],
+                "tier": "核心",
+                "current": {...},
+                "history": [...],
+                "created_chapter": 1
+            }
+        },
+        "地点": {...},
+        ...
+    }
+
+    alias_index 格式 (v4.0 一对多):
+    {
+        "废物": [{"type": "角色", "id": "lintian"}],
+        "天云宗": [
+            {"type": "地点", "id": "loc_tianyunzong"},
+            {"type": "势力", "id": "faction_tianyunzong"}
+        ],
+        ...
+    }
+    """
+    if "entities_v3" not in state:
+        state["entities_v3"] = {
+            "角色": {},
+            "地点": {},
+            "物品": {},
+            "势力": {},
+            "招式": {}
+        }
+
+    if "alias_index" not in state:
+        state["alias_index"] = {}
+
+    return state
+
+
+_XML_ATTR_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(["\'])(.*?)\2', re.DOTALL)
+
+
+def parse_xml_attributes(tag: str) -> Dict[str, str]:
+    """从形如 `<tag a=\"1\" b='2'/>` 的片段中提取属性字典(不做 XML 语义校验)。"""
+    attrs: Dict[str, str] = {}
+    for m in _XML_ATTR_RE.finditer(tag):
+        key = m.group(1).strip()
+        value = m.group(3).strip()
+        if not key:
+            continue
+        attrs[key] = value
+    return attrs
+
+
+def _line_number_from_index(text: str, index: int) -> int:
+    return text[:index].count("\n") + 1
+
+
+def extract_new_entities(file_path: str) -> List[Dict]:
+    """
+    从章节文件中提取所有实体标签(v4.0 仅支持 XML 格式)。
+
+    支持 XML 形态:
+      1) 自闭合:<entity type="角色" name="林天" desc="..." tier="核心" [id="lintian"] [任意属性...]/>
+      2) 成对:
+         <entity type="角色" id="lintian" name="林天" desc="..." tier="核心">
+           <alias>废物</alias>
+           <alias>林宗主</alias>
+         </entity>
+
+    Returns:
+        List[Dict]: [{"type","name","desc","tier","id?","attrs","aliases","line","source_file"}, ...]
+    """
+    p = Path(file_path)
+    text = p.read_text(encoding="utf-8")
+
+    entities: List[Dict[str, Any]] = []
+
+    # ============================================================
+    # XML 成对格式: <entity ...> ... </entity>(用于内嵌 alias)
+    # ============================================================
+    block_pattern = re.compile(r"(?s)(<entity\b[^>]*>)(.*?)</entity>")
+    for m in block_pattern.finditer(text):
+        open_tag = m.group(1)
+        body = m.group(2)
+        attrs = parse_xml_attributes(open_tag)
+
+        entity_type = str(attrs.get("type", "")).strip()
+        entity_name = str(attrs.get("name", "")).strip()
+        if not entity_type or not entity_name:
+            continue
+
+        # 验证 entity_type
+        if entity_type not in VALID_ENTITY_TYPES:
+            print(f"⚠️ 无效实体类型: {entity_type}(第{_line_number_from_index(text, m.start())}行),跳过")
+            continue
+
+        entity_desc = str(attrs.get("desc", "")).strip()
+        entity_tier = str(attrs.get("tier", "支线")).strip() or "支线"
+        if entity_tier.lower() not in ENTITY_TIER_MAP:
+            entity_tier = "支线"
+
+        entity_id = str(attrs.get("id", "")).strip() or None
+        extra_attrs = {k: v for k, v in attrs.items() if k not in {"type", "id", "name", "desc", "tier"}}
+        aliases = [a.strip() for a in re.findall(r"(?s)<alias>(.*?)</alias>", body) if str(a).strip()]
+
+        entities.append(
+            {
+                "type": entity_type,
+                "id": entity_id,
+                "name": entity_name,
+                "desc": entity_desc,
+                "tier": entity_tier,
+                "attrs": extra_attrs,
+                "aliases": aliases,
+                "line": _line_number_from_index(text, m.start()),
+                "source_file": file_path,
+            }
+        )
+
+    # ============================================================
+    # XML 自闭合格式: <entity .../>
+    # ============================================================
+    self_closing_pattern = re.compile(r"<entity\b[^>]*?/\s*>")
+    for m in self_closing_pattern.finditer(text):
+        tag = m.group(0)
+        attrs = parse_xml_attributes(tag)
+
+        entity_type = str(attrs.get("type", "")).strip()
+        entity_name = str(attrs.get("name", "")).strip()
+        if not entity_type or not entity_name:
+            continue
+
+        # 验证 entity_type
+        if entity_type not in VALID_ENTITY_TYPES:
+            print(f"⚠️ 无效实体类型: {entity_type}(第{_line_number_from_index(text, m.start())}行),跳过")
+            continue
+
+        entity_desc = str(attrs.get("desc", "")).strip()
+        entity_tier = str(attrs.get("tier", "支线")).strip() or "支线"
+        if entity_tier.lower() not in ENTITY_TIER_MAP:
+            entity_tier = "支线"
+
+        entity_id = str(attrs.get("id", "")).strip() or None
+        extra_attrs = {k: v for k, v in attrs.items() if k not in {"type", "id", "name", "desc", "tier"}}
+
+        entities.append(
+            {
+                "type": entity_type,
+                "id": entity_id,
+                "name": entity_name,
+                "desc": entity_desc,
+                "tier": entity_tier,
+                "attrs": extra_attrs,
+                "aliases": [],
+                "line": _line_number_from_index(text, m.start()),
+                "source_file": file_path,
+            }
+        )
+
+    return entities
+
+
+def extract_entity_alias_ops(file_path: str) -> List[Dict[str, Any]]:
+    """
+    提取实体别名操作:
+      <entity-alias id="lintian" alias="林宗主" context="成为宗主后"/>
+      <entity-alias ref="林天" alias="不灭战神" context="晋升称号后"/>
+
+    可选:type="角色|地点|物品|势力|招式" 用于 disambiguation。
+    """
+    p = Path(file_path)
+    text = p.read_text(encoding="utf-8")
+
+    results: List[Dict[str, Any]] = []
+    pattern = re.compile(r"<entity[-_]alias\b[^>]*?/\s*>", re.IGNORECASE)
+    for m in pattern.finditer(text):
+        tag = m.group(0)
+        attrs = parse_xml_attributes(tag)
+
+        alias = str(attrs.get("alias", "")).strip()
+        if not alias:
+            continue
+
+        results.append(
+            {
+                "id": str(attrs.get("id", "")).strip() or None,
+                "ref": str(attrs.get("ref", "")).strip() or None,
+                "type": str(attrs.get("type", "")).strip() or None,
+                "alias": alias,
+                "context": str(attrs.get("context", "")).strip(),
+                "line": _line_number_from_index(text, m.start()),
+                "source_file": file_path,
+            }
+        )
+
+    return results
+
+
+def extract_entity_update_ops(file_path: str) -> List[Dict[str, Any]]:
+    """
+    提取实体更新操作(v4.0 支持 set/unset/add/remove/inc):
+      <entity-update id="lintian">
+        <set key="realm" value="筑基期一层" reason="突破"/>
+        <unset key="bottleneck"/>
+        <add key="titles" value="不灭战神"/>
+        <remove key="allies" value="张三"/>
+        <inc key="kill_count" delta="1"/>
+      </entity-update>
+
+      <entity-update ref="林宗主" type="角色">
+        <set key="realm" value="金丹期"/>
+      </entity-update>
+
+    可选:type="角色|地点|物品|势力|招式" 用于 disambiguation。
+    """
+    p = Path(file_path)
+    text = p.read_text(encoding="utf-8")
+
+    results: List[Dict[str, Any]] = []
+
+    block_pattern = re.compile(r"(?s)(<entity-update\b[^>]*>)(.*?)</entity-update>", re.IGNORECASE)
+    for m in block_pattern.finditer(text):
+        open_tag = m.group(1)
+        body = m.group(2)
+        attrs = parse_xml_attributes(open_tag)
+
+        operations: List[Dict[str, Any]] = []
+
+        # <set key="..." value="..." reason="..."/>
+        for sm in re.finditer(r"<set\b[^>]*?/\s*>", body, re.IGNORECASE):
+            set_attrs = parse_xml_attributes(sm.group(0))
+            key = str(set_attrs.get("key", "")).strip()
+            value = str(set_attrs.get("value", "")).strip()
+            if not key:
+                continue
+            operations.append({
+                "op": "set",
+                "key": key,
+                "value": value,
+                "reason": str(set_attrs.get("reason", "")).strip()
+            })
+
+        # <unset key="..."/>
+        for sm in re.finditer(r"<unset\b[^>]*?/\s*>", body, re.IGNORECASE):
+            set_attrs = parse_xml_attributes(sm.group(0))
+            key = str(set_attrs.get("key", "")).strip()
+            if not key:
+                continue
+            operations.append({
+                "op": "unset",
+                "key": key,
+                "reason": str(set_attrs.get("reason", "")).strip()
+            })
+
+        # <add key="..." value="..."/>
+        for sm in re.finditer(r"<add\b[^>]*?/\s*>", body, re.IGNORECASE):
+            set_attrs = parse_xml_attributes(sm.group(0))
+            key = str(set_attrs.get("key", "")).strip()
+            value = str(set_attrs.get("value", "")).strip()
+            if not key or not value:
+                continue
+            operations.append({
+                "op": "add",
+                "key": key,
+                "value": value,
+                "reason": str(set_attrs.get("reason", "")).strip()
+            })
+
+        # <remove key="..." value="..."/>
+        for sm in re.finditer(r"<remove\b[^>]*?/\s*>", body, re.IGNORECASE):
+            set_attrs = parse_xml_attributes(sm.group(0))
+            key = str(set_attrs.get("key", "")).strip()
+            value = str(set_attrs.get("value", "")).strip()
+            if not key or not value:
+                continue
+            operations.append({
+                "op": "remove",
+                "key": key,
+                "value": value,
+                "reason": str(set_attrs.get("reason", "")).strip()
+            })
+
+        # <inc key="..." delta="..."/>
+        for sm in re.finditer(r"<inc\b[^>]*?/\s*>", body, re.IGNORECASE):
+            set_attrs = parse_xml_attributes(sm.group(0))
+            key = str(set_attrs.get("key", "")).strip()
+            delta_str = str(set_attrs.get("delta", "1")).strip()
+            if not key:
+                continue
+            try:
+                delta = int(delta_str)
+            except ValueError:
+                delta = 1
+            operations.append({
+                "op": "inc",
+                "key": key,
+                "delta": delta,
+                "reason": str(set_attrs.get("reason", "")).strip()
+            })
+
+        if not operations:
+            continue
+
+        results.append(
+            {
+                "id": str(attrs.get("id", "")).strip() or None,
+                "ref": str(attrs.get("ref", "")).strip() or None,
+                "type": str(attrs.get("type", "")).strip() or None,
+                "operations": operations,
+                "line": _line_number_from_index(text, m.start()),
+                "source_file": file_path,
+            }
+        )
+
+    return results
+
+
+def extract_golden_finger_skills(file_path: str) -> List[Dict]:
+    """
+    从章节文件中提取金手指技能标签(v4.0 仅支持 XML 格式)
+
+    XML 格式:
+      <skill name="技能名" level="等级" desc="描述" cooldown="冷却时间"/>
+
+      示例:
+      <skill name="时间回溯" level="1" desc="回到10秒前的状态" cooldown="24小时"/>
+
+    Returns:
+        List[Dict]: [{"name": "吞噬", "level": "Lv1", "desc": "...", "cooldown": "10秒"}, ...]
+    """
+    skills = []
+
+    with open(file_path, 'r', encoding='utf-8') as f:
+        for line_num, line in enumerate(f, 1):
+            xml_matches = re.findall(
+                r'<skill\s+name=["\']([^"\']+)["\']\s+level=["\']([^"\']+)["\']\s+desc=["\']([^"\']+)["\']\s+cooldown=["\']([^"\']+)["\']\s*/?>',
+                line
+            )
+            for match in xml_matches:
+                skills.append({
+                    "name": match[0].strip(),
+                    "level": match[1].strip(),
+                    "desc": match[2].strip(),
+                    "cooldown": match[3].strip(),
+                    "line": line_num,
+                    "source_file": file_path
+                })
+
+    return skills
+
+
+def extract_foreshadowing_json(file_path: str) -> List[Dict[str, Any]]:
+    """
+    从章节文件提取伏笔标签(v4.0 仅支持 XML 格式)
+
+    XML 格式:
+      <foreshadow content="伏笔内容" tier="层级" target="目标章节" location="地点" characters="角色1,角色2"/>
+
+      示例:
+      <foreshadow content="神秘老者留下的玉佩开始发光" tier="核心" target="50" location="废弃实验室" characters="陆辰"/>
+
+    字段:
+      - content (必填)
+      - tier (可选: 核心/支线/装饰,默认 支线)
+      - planted_chapter (可选: 默认由调用方补齐)
+      - target_chapter / target (可选: 默认 planted_chapter + 100)
+      - location (可选)
+      - characters (可选: 逗号分隔字符串)
+    """
+    p = Path(file_path)
+    text = p.read_text(encoding="utf-8")
+
+    results: List[Dict[str, Any]] = []
+
+    xml_pattern = re.compile(
+        r'<foreshadow\s+'
+        r'content=["\']([^"\']+)["\']\s+'
+        r'tier=["\']([^"\']+)["\']'
+        r'(?:\s+target=["\']([^"\']*)["\'])?'
+        r'(?:\s+location=["\']([^"\']*)["\'])?'
+        r'(?:\s+characters=["\']([^"\']*)["\'])?'
+        r'\s*/?>',
+        re.DOTALL
+    )
+
+    for m in xml_pattern.finditer(text):
+        line_num = text[: m.start()].count("\n") + 1
+        content = m.group(1).strip()
+        if not content:
+            continue
+
+        tier = m.group(2).strip() or "支线"
+        if tier.lower() not in ENTITY_TIER_MAP:
+            tier = "支线"
+
+        target_str = m.group(3)
+        target_chapter = None
+        if target_str:
+            try:
+                target_chapter = int(target_str.strip())
+            except (TypeError, ValueError):
+                pass
+
+        location = (m.group(4) or "").strip()
+
+        characters_str = m.group(5) or ""
+        characters_list = [c.strip() for c in re.split(r"[,,]", characters_str) if c.strip()]
+
+        results.append({
+            "content": content,
+            "tier": tier,
+            "planted_chapter": None,
+            "target_chapter": target_chapter,
+            "location": location,
+            "characters": characters_list,
+            "line": line_num,
+            "source_file": str(p),
+        })
+
+    return results
+
+
+def extract_deviations(file_path: str) -> List[Dict[str, Any]]:
+    """
+    从章节文件提取大纲偏离标签(v4.0 仅支持 XML 格式)
+
+    XML 格式:
+      <deviation reason="偏离原因"/>
+
+      示例:
+      <deviation reason="临时灵感,增加李薇与陆辰的情感互动,为后续感情线铺垫"/>
+
+    Returns:
+        List[Dict]: [{"reason": "...", "line": 123}, ...]
+    """
+    p = Path(file_path)
+    text = p.read_text(encoding="utf-8")
+
+    results: List[Dict[str, Any]] = []
+
+    xml_pattern = re.compile(
+        r'<deviation\s+reason=["\']([^"\']+)["\']\s*/?>',
+        re.DOTALL
+    )
+
+    for m in xml_pattern.finditer(text):
+        line_num = text[: m.start()].count("\n") + 1
+        reason = m.group(1).strip()
+        if reason:
+            results.append({
+                "reason": reason,
+                "line": line_num,
+                "source_file": str(p),
+            })
+
+    return results
+
+
+def extract_relationships(file_path: str) -> List[Dict[str, Any]]:
+    """
+    从章节文件提取角色关系标签
+
+    XML 格式(推荐使用 entity_id,避免改名导致断链):
+      <relationship char1_id="lintian" char2_id="lixue" type="romance" intensity="60" desc="暧昧中,互有好感"/>
+      <relationship char1="林天" char2="李雪" type="romance" intensity="60" desc="暧昧中,互有好感"/>
+
+      示例:
+      <relationship char1="林天" char2="李雪" type="romance" intensity="60" desc="暧昧中,互有好感"/>
+      <relationship char1="林天" char2="王少" type="enemy" intensity="90" desc="杀父之仇"/>
+      <relationship char1="林天" char2="云长老" type="mentor" intensity="80" desc="师徒关系,受其指点"/>
+
+    关系类型 (type):
+      - ally: 盟友
+      - enemy: 敌人
+      - romance: 恋人/暧昧
+      - mentor: 师徒
+      - debtor: 恩怨(欠人情/被欠)
+      - family: 家族/血缘
+      - rival: 竞争对手
+
+    强度 (intensity): 0-100,越高关系越强烈
+
+    Returns:
+        List[Dict]: [{"char1","char2","char1_id?","char2_id?","type","intensity","desc",...}, ...]
+    """
+    p = Path(file_path)
+    text = p.read_text(encoding="utf-8")
+
+    results: List[Dict[str, Any]] = []
+
+    valid_types = {"ally", "enemy", "romance", "mentor", "debtor", "family", "rival"}
+
+    # XML 格式: <relationship .../>
+    xml_pattern = re.compile(r"<relationship\b[^>]*?/\s*>", re.IGNORECASE)
+    for m in xml_pattern.finditer(text):
+        line_num = text[: m.start()].count("\n") + 1
+        attrs = parse_xml_attributes(m.group(0))
+
+        char1 = str(attrs.get("char1", "")).strip()
+        char2 = str(attrs.get("char2", "")).strip()
+        char1_id = str(attrs.get("char1_id", "")).strip() or None
+        char2_id = str(attrs.get("char2_id", "")).strip() or None
+        rel_type = str(attrs.get("type", "")).strip().lower() or "ally"
+        intensity_str = str(attrs.get("intensity", "")).strip() or "50"
+        desc = str(attrs.get("desc", "")).strip()
+
+        if not ((char1_id or char1) and (char2_id or char2)):
+            continue
+
+        # 验证关系类型
+        if rel_type not in valid_types:
+            print(f"⚠️ 未知关系类型 '{rel_type}'(第{line_num}行),使用默认 'ally'")
+            rel_type = "ally"
+
+        # 解析强度
+        try:
+            intensity = int(intensity_str)
+            intensity = max(0, min(100, intensity))  # 限制 0-100
+        except ValueError:
+            intensity = 50  # 默认中等强度
+
+        results.append({
+            "char1": char1,
+            "char2": char2,
+            "char1_id": char1_id,
+            "char2_id": char2_id,
+            "type": rel_type,
+            "intensity": intensity,
+            "desc": desc,
+            "line": line_num,
+            "source_file": str(p),
+        })
+
+    return results
+
+
+def categorize_character(desc: str) -> str:
+    """
+    根据描述判断角色分类
+
+    规则:
+      - 包含"主角"/"林天" → 主要角色
+      - 包含"反派"/"敌对"/"血煞门" → 反派角色
+      - 其他 → 次要角色
+    """
+    if "主角" in desc or "重要" in desc:
+        return "主要角色"
+    elif "反派" in desc or "敌对" in desc or "血煞" in desc:
+        return "反派角色"
+    else:
+        return "次要角色"
+
+def generate_character_card(entity: Dict, category: str) -> str:
+    """生成角色卡 Markdown 内容"""
+    return f"""# {entity['name']}
+
+> **首次登场**: {entity.get('source_file', '未知')}(第 {entity.get('line', '?')} 行)
+> **创建时间**: {datetime.now().strftime('%Y-%m-%d')}
+
+## 基本信息
+
+- **姓名**: {entity['name']}
+- **性别**: 待补充
+- **年龄**: 待补充
+- **身份**: {entity['desc']}
+- **所属势力**: 待补充
+
+## 实力设定
+
+- **当前境界**: 待补充
+- **擅长招式**: 待补充
+- **特殊能力**: 待补充
+
+## 性格特点
+
+{entity['desc']}
+
+## 外貌描述
+
+待补充
+
+## 人际关系
+
+- **与主角**: 待补充
+
+## 重要剧情
+
+- 【第 X 章】{entity['desc']}
+
+## 备注
+
+自动提取自 `<entity/>` 标签,请补充完善。
+"""
+
+def update_world_view(entity: Dict, target_file: str, section: str):
+    """更新世界观.md(追加地点/势力信息)"""
+    if not os.path.exists(target_file):
+        # 创建基础模板
+        content = f"""# 世界观
+
+## 地理
+
+## 势力
+
+## 历史背景
+
+"""
+        with open(target_file, 'w', encoding='utf-8') as f:
+            f.write(content)
+
+    # 读取现有内容
+    with open(target_file, 'r', encoding='utf-8') as f:
+        content = f.read()
+
+    # 追加到对应章节
+    if section == "地理":
+        entry = f"""
+### {entity['name']}
+
+{entity['desc']}
+
+> 首次登场: {entity.get('source_file', '未知')}
+"""
+    elif section == "势力":
+        entry = f"""
+### {entity['name']}
+
+{entity['desc']}
+
+> 首次登场: {entity.get('source_file', '未知')}
+"""
+
+    # 在对应章节后追加
+    pattern = f"## {section}"
+    if pattern in content:
+        content = content.replace(pattern, f"{pattern}\n{entry}")
+    else:
+        content += f"\n## {section}\n{entry}"
+
+    with open(target_file, 'w', encoding='utf-8') as f:
+        f.write(content)
+
+def update_power_system(entity: Dict, target_file: str):
+    """更新力量体系.md(追加招式)"""
+    if not os.path.exists(target_file):
+        content = f"""# 力量体系
+
+## 境界划分
+
+## 修炼方法
+
+## 招式库
+
+"""
+        with open(target_file, 'w', encoding='utf-8') as f:
+            f.write(content)
+
+    with open(target_file, 'r', encoding='utf-8') as f:
+        content = f.read()
+
+    entry = f"""
+### {entity['name']}
+
+{entity['desc']}
+
+> 首次登场: {entity.get('source_file', '未知')}
+"""
+
+    if "## 招式库" in content:
+        content = content.replace("## 招式库", f"## 招式库\n{entry}")
+    else:
+        content += f"\n## 招式库\n{entry}"
+
+    with open(target_file, 'w', encoding='utf-8') as f:
+        f.write(content)
+
+def update_state_json(
+    entities: List[Dict],
+    state_file: str,
+    golden_finger_skills: Optional[List[Dict]] = None,
+    foreshadowing_items: Optional[List[Dict[str, Any]]] = None,
+    relationship_items: Optional[List[Dict[str, Any]]] = None,
+    entity_alias_ops: Optional[List[Dict[str, Any]]] = None,
+    entity_update_ops: Optional[List[Dict[str, Any]]] = None,
+    *,
+    default_planted_chapter: Optional[int] = None,
+):
+    """更新 state.json(实体/别名/属性更新 + 金手指/伏笔/关系)。"""
+
+    def _to_int(value: Any, default: int = 0) -> int:
+        try:
+            return int(value)
+        except (TypeError, ValueError):
+            return default
+
+    with open(state_file, 'r', encoding='utf-8') as f:
+        state = json.load(f)
+
+    first_seen_chapter = _to_int(default_planted_chapter, 0)
+    project_root = Path(state_file).resolve().parent.parent
+
+    # 确保存在金手指技能列表
+    if 'protagonist_state' not in state:
+        state['protagonist_state'] = {}
+    golden_finger = state['protagonist_state'].get('golden_finger')
+    if not isinstance(golden_finger, dict):
+        golden_finger = {}
+        state['protagonist_state']['golden_finger'] = golden_finger
+    golden_finger.setdefault("name", "")
+    golden_finger.setdefault("level", 1)
+    golden_finger.setdefault("cooldown", 0)
+    golden_finger.setdefault("skills", [])
+
+    # --- 实体别名/更新系统(entities_v3 + alias_index)---
+    state = ensure_entities_v3_structure(state)
+
+    entity_alias_ops = entity_alias_ops or []
+    entity_update_ops = entity_update_ops or []
+
+    touched = set()
+
+    def _normalize_entity_type(raw: Any) -> str:
+        t = normalize_entity_type(raw)
+        if not t or t not in state.get("entities_v3", {}):
+            return ""
+        return t
+
+    def _normalize_first_appearance(source_file: Any) -> str:
+        raw = str(source_file or "").strip()
+        if not raw:
+            return ""
+        try:
+            p = Path(raw)
+            if not p.is_absolute():
+                p = (Path.cwd() / p).resolve()
+            if p == project_root or project_root in p.parents:
+                return str(p.relative_to(project_root)).replace("\\", "/")
+            return str(p).replace("\\", "/")
+        except Exception:
+            return raw.replace("\\", "/")
+
+    def _resolve_by_id(entity_id: Any, entity_type: Optional[str]) -> tuple[Optional[str], Optional[str], Optional[dict]]:
+        eid = str(entity_id or "").strip()
+        if not eid:
+            return (None, None, None)
+
+        if entity_type:
+            et = _normalize_entity_type(entity_type)
+            data = state.get("entities_v3", {}).get(et, {}).get(eid)
+            return (et, eid, data) if isinstance(data, dict) else (None, None, None)
+
+        hits: list[tuple[str, dict]] = []
+        for et, bucket in (state.get("entities_v3") or {}).items():
+            if isinstance(bucket, dict) and eid in bucket:
+                data = bucket.get(eid)
+                if isinstance(data, dict):
+                    hits.append((et, data))
+        if len(hits) == 1:
+            return (hits[0][0], eid, hits[0][1])
+        return (None, None, None)
+
+    def _resolve_ref(ref: Any, entity_type: Optional[str]) -> tuple[Optional[str], Optional[str], Optional[dict]]:
+        """通过别名/名称解析实体(v4.0 使用一对多 alias_index)"""
+        r = str(ref or "").strip()
+        if not r:
+            return (None, None, None)
+
+        # 使用新版 resolve_entity_by_alias(支持一对多 + 歧义检测)
+        et_hint = _normalize_entity_type(entity_type) if entity_type else None
+        et, eid, data = resolve_entity_by_alias(r, et_hint, state)
+        if et and eid and isinstance(data, dict):
+            return (et, eid, data)
+
+        return (None, None, None)
+
+    def _register_alias(entity_type: str, entity_id: str, alias: Any, *, context: str = "", first_seen: int = 0) -> None:
+        """注册别名到 alias_index(v4.0 一对多版本)"""
+        a = str(alias or "").strip()
+        if not a:
+            return
+
+        state.setdefault("alias_index", {})
+        alias_index = state["alias_index"]
+
+        # 新格式:alias_index[alias] = [{type, id, first_seen_chapter?, context?}, ...]
+        entries = alias_index.get(a)
+        if entries is None:
+            entries = []
+        if not isinstance(entries, list):
+            raise ValueError(
+                f"alias_index 数据格式错误:期望 alias_index[{a!r}] 为 list[{{type,id,...}}],实际为 {type(entries).__name__}"
+            )
+
+        # 检查是否已存在相同的 (type, id) 组合
+        new_entry: Dict[str, Any] = {"type": entity_type, "id": entity_id}
+        if first_seen:
+            new_entry["first_seen_chapter"] = int(first_seen)
+        if context:
+            new_entry["context"] = context
+        for existing in entries:
+            if existing.get("type") == entity_type and existing.get("id") == entity_id:
+                # 补齐首次出现/上下文(只填空缺)
+                if first_seen and not existing.get("first_seen_chapter"):
+                    existing["first_seen_chapter"] = int(first_seen)
+                if context and not existing.get("context"):
+                    existing["context"] = context
+                return  # 已存在,无需重复注册
+
+        # 添加新条目
+        entries.append(new_entry)
+        alias_index[a] = entries
+
+        # 同时更新实体的 aliases 列表
+        data = state.get("entities_v3", {}).get(entity_type, {}).get(entity_id)
+        if not isinstance(data, dict):
+            return
+        data.setdefault("aliases", [])
+        if a not in data["aliases"]:
+            data["aliases"].append(a)
+
+    def _ensure_v3_entity(entity_type: str, entity_id: str, canonical_name: str, *, tier: str, desc: str, first_appearance: str) -> dict:
+        bucket = state.setdefault("entities_v3", {}).setdefault(entity_type, {})
+        data = bucket.get(entity_id)
+        if not isinstance(data, dict):
+            data = {
+                "id": entity_id,
+                "canonical_name": canonical_name,
+                "aliases": [],
+                "tier": tier or "支线",
+                "desc": desc or "",
+                "current": {},
+                "history": [],
+                "created_chapter": first_seen_chapter or 1,
+                "first_appearance": first_appearance or "",
+            }
+            bucket[entity_id] = data
+
+        if canonical_name and not data.get("canonical_name"):
+            data["canonical_name"] = canonical_name
+        if tier and str(tier).lower() in ENTITY_TIER_MAP:
+            data["tier"] = tier
+        if desc:
+            data["desc"] = desc
+        if first_appearance and not data.get("first_appearance"):
+            data["first_appearance"] = first_appearance
+
+        data.setdefault("current", {})
+        data.setdefault("history", [])
+        data.setdefault("aliases", [])
+        return data
+
+    def _apply_operations(entity_type: str, entity_id: str, data: dict, operations: List[Dict[str, Any]]) -> None:
+        """应用实体更新操作(v4.0 支持 set/unset/add/remove/inc + 顶层字段)"""
+        if not operations:
+            return
+
+        current = data.setdefault("current", {})
+        changes: Dict[str, Any] = {}
+        reasons: Dict[str, str] = {}
+
+        def _rename(new_name: str, reason: str = "") -> None:
+            new_name = str(new_name or "").strip()
+            if not new_name:
+                return
+            old_name = str(data.get("canonical_name", "")).strip()
+            if old_name and old_name != new_name:
+                _register_alias(entity_type, entity_id, old_name, first_seen=first_seen_chapter)
+            data["canonical_name"] = new_name
+            _register_alias(entity_type, entity_id, new_name, first_seen=first_seen_chapter)
+            changes["canonical_name"] = new_name
+            if reason:
+                reasons["canonical_name"] = reason
+
+        for op_item in operations:
+            op = str(op_item.get("op", "set")).strip().lower()
+            key = str(op_item.get("key", "")).strip()
+            reason = str(op_item.get("reason", "")).strip()
+            if not key:
+                continue
+
+            # 顶层字段处理
+            if key in TOP_LEVEL_FIELDS:
+                if op == "set":
+                    value = str(op_item.get("value", "")).strip()
+                    if key == "canonical_name":
+                        _rename(value, reason)
+                    elif key == "tier":
+                        # 校验 tier 值
+                        if value.lower() in ENTITY_TIER_MAP or value in {"核心", "支线", "装饰"}:
+                            if data.get("tier") != value:
+                                data["tier"] = value
+                                changes["tier"] = value
+                                if reason:
+                                    reasons["tier"] = reason
+                        else:
+                            print(f"⚠️ 无效 tier 值: {value},跳过")
+                    else:
+                        if data.get(key) != value:
+                            data[key] = value
+                            changes[key] = value
+                            if reason:
+                                reasons[key] = reason
+                elif op == "unset":
+                    if key in data:
+                        del data[key]
+                        changes[key] = None
+                        if reason:
+                            reasons[key] = reason
+                continue
+
+            # canonical_name 的特殊别名
+            if key in {"name", "canonical_name"} and op == "set":
+                value = str(op_item.get("value", "")).strip()
+                _rename(value, reason)
+                continue
+
+            # current 字段操作
+            if op == "set":
+                value = str(op_item.get("value", "")).strip()
+                prev = current.get(key)
+                if prev != value:
+                    current[key] = value
+                    changes[key] = value
+                    if reason:
+                        reasons[key] = reason
+
+            elif op == "unset":
+                if key in current:
+                    del current[key]
+                    changes[key] = None
+                    if reason:
+                        reasons[key] = reason
+
+            elif op == "add":
+                value = str(op_item.get("value", "")).strip()
+                if not value:
+                    continue
+                arr = current.get(key, [])
+                if not isinstance(arr, list):
+                    arr = [arr] if arr else []
+                if value not in arr:
+                    arr.append(value)
+                    current[key] = arr
+                    changes[key] = arr
+                    if reason:
+                        reasons[key] = reason
+
+            elif op == "remove":
+                value = str(op_item.get("value", "")).strip()
+                if not value:
+                    continue
+                arr = current.get(key, [])
+                if isinstance(arr, list) and value in arr:
+                    arr.remove(value)
+                    current[key] = arr
+                    changes[key] = arr
+                    if reason:
+                        reasons[key] = reason
+
+            elif op == "inc":
+                delta = op_item.get("delta", 1)
+                try:
+                    delta = int(delta)
+                except (TypeError, ValueError):
+                    delta = 1
+                prev = current.get(key, 0)
+                try:
+                    prev = int(prev)
+                except (TypeError, ValueError):
+                    prev = 0
+                new_val = prev + delta
+                current[key] = new_val
+                changes[key] = new_val
+                if reason:
+                    reasons[key] = reason
+
+        if first_seen_chapter:
+            current["last_chapter"] = max(_to_int(current.get("last_chapter"), 0), first_seen_chapter)
+
+        if changes:
+            entry: Dict[str, Any] = {"chapter": first_seen_chapter or 0, "changes": changes}
+            if reasons:
+                entry["reasons"] = reasons
+            entry["added_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+            data.setdefault("history", []).append(entry)
+
+    # 1) 处理 <entity .../> / <entity>...</entity>
+    for entity in entities or []:
+        entity_type = _normalize_entity_type(entity.get("type", ""))
+        name = str(entity.get("name", "")).strip()
+        if not name:
+            continue
+
+        raw_id = entity.get("id")
+        entity_id = (str(raw_id).strip() if raw_id is not None else "") or None
+        data: Optional[dict] = None
+
+        if entity_id:
+            _, _, data = _resolve_by_id(entity_id, entity_type)
+        else:
+            _, rid, rdata = _resolve_ref(name, entity_type)
+            if rid and isinstance(rdata, dict):
+                entity_id = rid
+                data = rdata
+
+        if not entity_id:
+            existing_ids = set((state.get("entities_v3") or {}).get(entity_type, {}).keys())
+            entity_id = generate_entity_id(entity_type, name, existing_ids)
+
+        first_appearance = _normalize_first_appearance(entity.get("source_file", ""))
+        tier = str(entity.get("tier", "支线")).strip() or "支线"
+        if tier.lower() not in ENTITY_TIER_MAP:
+            tier = "支线"
+        desc = str(entity.get("desc", "")).strip()
+
+        data = _ensure_v3_entity(entity_type, entity_id, name, tier=tier, desc=desc, first_appearance=first_appearance)
+
+        # canonical name & aliases
+        _register_alias(entity_type, entity_id, str(data.get("canonical_name", "")).strip() or name, first_seen=first_seen_chapter)
+        _register_alias(entity_type, entity_id, name, first_seen=first_seen_chapter)
+        for alias in (entity.get("aliases") or []):
+            _register_alias(entity_type, entity_id, alias, first_seen=first_seen_chapter)
+
+        # attribute updates (auto mode)
+        extra_attrs = entity.get("attrs") or {}
+        if isinstance(extra_attrs, dict) and extra_attrs:
+            ops = [{"op": "set", "key": k, "value": str(v), "reason": ""} for k, v in extra_attrs.items()]
+            _apply_operations(entity_type, entity_id, data, ops)
+
+        touched.add((entity_type, entity_id))
+
+    # 2) 处理 <entity-alias .../>
+    for op in entity_alias_ops:
+        alias = str(op.get("alias", "")).strip()
+        if not alias:
+            continue
+
+        hint = op.get("type")
+        entity_type_hint = _normalize_entity_type(hint) if hint else None
+
+        et: Optional[str] = None
+        eid: Optional[str] = None
+        data: Optional[dict] = None
+
+        if op.get("id"):
+            et, eid, data = _resolve_by_id(op.get("id"), entity_type_hint)
+        elif op.get("ref"):
+            et, eid, data = _resolve_ref(op.get("ref"), entity_type_hint)
+
+        if not (et and eid and isinstance(data, dict)):
+            print(f"??  entity-alias 无法解析引用: id={op.get('id')!r} ref={op.get('ref')!r}")
+            continue
+
+        _register_alias(et, eid, alias, context=str(op.get("context", "")).strip(), first_seen=first_seen_chapter)
+        touched.add((et, eid))
+
+    # 3) 处理 <entity-update>...</entity-update>
+    for op in entity_update_ops:
+        operations = op.get("operations") or []
+        if not isinstance(operations, list) or not operations:
+            continue
+
+        hint = op.get("type")
+        entity_type_hint = _normalize_entity_type(hint) if hint else None
+
+        et: Optional[str] = None
+        eid: Optional[str] = None
+        data: Optional[dict] = None
+
+        if op.get("id"):
+            et, eid, data = _resolve_by_id(op.get("id"), entity_type_hint)
+        elif op.get("ref"):
+            et, eid, data = _resolve_ref(op.get("ref"), entity_type_hint)
+
+        if not (et and eid and isinstance(data, dict)):
+            print(f"⚠️ entity-update 无法解析引用: id={op.get('id')!r} ref={op.get('ref')!r}")
+            continue
+
+        _apply_operations(et, eid, data, operations)
+        touched.add((et, eid))
+
+    # 4) 更新金手指技能
+    if golden_finger_skills:
+        existing = state['protagonist_state']['golden_finger'].get('skills', [])
+        if not isinstance(existing, list):
+            existing = []
+            state['protagonist_state']['golden_finger']['skills'] = existing
+
+        existing_by_name = {s.get("name"): s for s in existing if isinstance(s, dict) and s.get("name")}
+        for skill in golden_finger_skills:
+            if not isinstance(skill, dict):
+                continue
+
+            name = str(skill.get("name", "")).strip()
+            if not name:
+                continue
+
+            level = str(skill.get("level", "")).strip()
+            desc = str(skill.get("desc", "")).strip()
+            cooldown = str(skill.get("cooldown", "")).strip()
+            source_file = str(skill.get("source_file", "")).strip()
+
+            existing_skill = existing_by_name.get(name)
+            if existing_skill is None:
+                new_skill = {
+                    "name": name,
+                    "level": level,
+                    "desc": desc,
+                    "cooldown": cooldown,
+                    "unlocked_at": source_file,
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                }
+                existing.append(new_skill)
+                existing_by_name[name] = new_skill
+                print(f"  ✨ 新增金手指技能: {name} ({level})")
+                continue
+
+            changed = False
+            if level and existing_skill.get("level") != level:
+                existing_skill["level"] = level
+                changed = True
+            if desc and existing_skill.get("desc") != desc:
+                existing_skill["desc"] = desc
+                changed = True
+            if cooldown and existing_skill.get("cooldown") != cooldown:
+                existing_skill["cooldown"] = cooldown
+                changed = True
+            if source_file and not existing_skill.get("unlocked_at"):
+                existing_skill["unlocked_at"] = source_file
+                changed = True
+
+            if changed:
+                existing_skill["updated_at"] = datetime.now().strftime('%Y-%m-%d')
+                print(f"  🔁 更新金手指技能: {name} ({existing_skill.get('level', level)})")
+
+    # 更新伏笔(结构化)
+    if foreshadowing_items:
+        state.setdefault("plot_threads", {"active_threads": [], "foreshadowing": []})
+        state["plot_threads"].setdefault("foreshadowing", [])
+
+        existing = state["plot_threads"]["foreshadowing"]
+
+        for item in foreshadowing_items:
+            content = str(item.get("content", "")).strip()
+            if not content:
+                continue
+
+            planted = item.get("planted_chapter") or default_planted_chapter or 1
+            try:
+                planted = int(planted)
+            except (TypeError, ValueError):
+                planted = default_planted_chapter or 1
+
+            target = item.get("target_chapter")
+            if target is None:
+                target = planted + 100
+            try:
+                target = int(target)
+            except (TypeError, ValueError):
+                target = planted + 100
+
+            tier = str(item.get("tier", "支线")).strip() or "支线"
+            if tier.lower() not in ENTITY_TIER_MAP:
+                tier = "支线"
+
+            location = str(item.get("location", "")).strip()
+            characters = item.get("characters", [])
+            if not isinstance(characters, list):
+                characters = []
+
+            found = None
+            for old in existing:
+                if old.get("content") == content:
+                    found = old
+                    break
+
+            if found is None:
+                existing.append({
+                    "content": content,
+                    "status": "未回收",
+                    "tier": tier,
+                    "planted_chapter": planted,
+                    "target_chapter": target,
+                    "location": location,
+                    "characters": characters,
+                    "added_at": datetime.now().strftime("%Y-%m-%d"),
+                })
+                print(f"  ?? 新增伏笔: {content[:30]}...")
+            else:
+                found["tier"] = tier
+                found["planted_chapter"] = planted
+                found["target_chapter"] = target
+                if location:
+                    found["location"] = location
+
+                old_chars = found.get("characters", [])
+                if not isinstance(old_chars, list):
+                    old_chars = []
+                merged = []
+                seen = set()
+                for n in [*old_chars, *characters]:
+                    s = str(n).strip()
+                    if not s or s in seen:
+                        continue
+                    merged.append(s)
+                    seen.add(s)
+                found["characters"] = merged
+
+    # 更新关系(结构化,推荐使用 entity_id)
+    if relationship_items:
+        state.setdefault("structured_relationships", [])
+        existing = state["structured_relationships"]
+
+        for item in relationship_items:
+            # 优先使用显式 entity_id;否则按别名解析(强制消歧)
+            char1_id = str(item.get("char1_id", "") or "").strip()
+            char2_id = str(item.get("char2_id", "") or "").strip()
+            char1_ref = str(item.get("char1", "")).strip()
+            char2_ref = str(item.get("char2", "")).strip()
+
+            # relationship 只允许角色
+            if char1_id:
+                _, rid, rdata = _resolve_by_id(char1_id, "角色")
+                if not rid or not isinstance(rdata, dict):
+                    raise ValueError(f"relationship.char1_id 无法解析: {char1_id!r}")
+                char1_id = rid
+                char1_name = str(rdata.get("canonical_name", "")).strip() or char1_ref
+            else:
+                _, rid, rdata = _resolve_ref(char1_ref, "角色")
+                if not rid or not isinstance(rdata, dict):
+                    raise ValueError(f"relationship.char1 无法解析: {char1_ref!r}")
+                char1_id = rid
+                char1_name = str(rdata.get("canonical_name", "")).strip() or char1_ref
+
+            if char2_id:
+                _, rid, rdata = _resolve_by_id(char2_id, "角色")
+                if not rid or not isinstance(rdata, dict):
+                    raise ValueError(f"relationship.char2_id 无法解析: {char2_id!r}")
+                char2_id = rid
+                char2_name = str(rdata.get("canonical_name", "")).strip() or char2_ref
+            else:
+                _, rid, rdata = _resolve_ref(char2_ref, "角色")
+                if not rid or not isinstance(rdata, dict):
+                    raise ValueError(f"relationship.char2 无法解析: {char2_ref!r}")
+                char2_id = rid
+                char2_name = str(rdata.get("canonical_name", "")).strip() or char2_ref
+
+            rel_type = str(item.get("type", "ally")).strip().lower() or "ally"
+            intensity = item.get("intensity", 50)
+            desc = str(item.get("desc", "")).strip()
+
+            try:
+                intensity = int(intensity)
+                intensity = max(0, min(100, intensity))
+            except (TypeError, ValueError):
+                intensity = 50
+
+            # 查找是否已存在相同关系
+            found = None
+            for old in existing:
+                if (
+                    old.get("char1_id") == char1_id
+                    and old.get("char2_id") == char2_id
+                    and old.get("type") == rel_type
+                ):
+                    found = old
+                    break
+
+            if found is None:
+                existing.append({
+                    "char1_id": char1_id,
+                    "char2_id": char2_id,
+                    "char1_name": char1_name,
+                    "char2_name": char2_name,
+                    "type": rel_type,
+                    "intensity": intensity,
+                    "description": desc,
+                    "last_update_chapter": default_planted_chapter or 1,
+                    "added_at": datetime.now().strftime("%Y-%m-%d"),
+                })
+                print(f"  💕 新增关系: {char1_name} ↔ {char2_name} ({rel_type}, 强度 {intensity})")
+            else:
+                # 更新强度和描述
+                found["intensity"] = intensity
+                found["description"] = desc
+                found["last_update_chapter"] = default_planted_chapter or found.get("last_update_chapter", 1)
+                found.setdefault("char1_name", char1_name)
+                found.setdefault("char2_name", char2_name)
+                print(f"  💕 更新关系: {char1_name} ↔ {char2_name} ({rel_type}, 强度 {intensity})")
+
+    # 使用集中式原子写入(带 filelock + 自动备份)
+    atomic_write_json(state_file, state, use_lock=True, backup=True)
+    print(f"✅ state.json 已原子化更新(带备份)")
+
+def sync_entity_to_settings(entity: Dict, project_root: str, auto_mode: bool = False) -> bool:
+    """
+    将实体同步到设定集
+
+    Returns:
+        bool: 是否成功同步
+    """
+    entity_type = normalize_entity_type(entity.get('type'))
+    entity_name = entity['name']
+
+    if entity_type == "角色":
+        category = categorize_character(entity['desc'])
+        category_dir = ROLE_CATEGORY_MAP.get(category.split('/')[0], "次要角色")
+
+        target_dir = Path(project_root) / f"设定集/角色库/{category_dir}"
+        # ============================================================================
+        # 安全修复:使用安全目录创建函数(文件权限修复)
+        # ============================================================================
+        create_secure_directory(str(target_dir))
+
+        # ============================================================================
+        # 安全修复:清理文件名,防止路径遍历 (CWE-22) - P0 CRITICAL
+        # 原代码: target_file = target_dir / f"{entity_name}.md"
+        # 漏洞: entity_name可能包含 "../" 导致目录遍历攻击
+        # ============================================================================
+        safe_entity_name = sanitize_filename(entity_name)
+        target_file = target_dir / f"{safe_entity_name}.md"
+
+        if target_file.exists():
+            print(f"⚠️  角色卡已存在: {target_file}")
+            if not auto_mode:
+                choice = input("是否覆盖?(y/n): ")
+                if choice.lower() != 'y':
+                    return False
+
+        with open(target_file, 'w', encoding='utf-8') as f:
+            f.write(generate_character_card(entity, category))
+
+        print(f"✅ 已创建角色卡: {target_file}")
+        return True
+
+    elif entity_type == "地点":
+        target_file = Path(project_root) / "设定集/世界观.md"
+        update_world_view(entity, str(target_file), "地理")
+        print(f"✅ 已更新世界观(地理): {entity_name}")
+        return True
+
+    elif entity_type == "势力":
+        target_file = Path(project_root) / "设定集/世界观.md"
+        update_world_view(entity, str(target_file), "势力")
+        print(f"✅ 已更新世界观(势力): {entity_name}")
+        return True
+
+    elif entity_type == "招式":
+        target_file = Path(project_root) / "设定集/力量体系.md"
+        update_power_system(entity, str(target_file))
+        print(f"✅ 已更新力量体系(招式): {entity_name}")
+        return True
+
+    elif entity_type == "物品":
+        target_dir = Path(project_root) / "设定集/物品库"
+        # ============================================================================
+        # 安全修复:使用安全目录创建函数(文件权限修复)
+        # ============================================================================
+        create_secure_directory(str(target_dir))
+
+        # ============================================================================
+        # 安全修复:清理文件名,防止路径遍历 (CWE-22) - P0 CRITICAL
+        # 原代码: target_file = target_dir / f"{entity_name}.md"
+        # 漏洞: entity_name可能包含 "../" 导致目录遍历攻击
+        # ============================================================================
+        safe_entity_name = sanitize_filename(entity_name)
+        target_file = target_dir / f"{safe_entity_name}.md"
+
+        if target_file.exists():
+            print(f"⚠️  物品卡已存在: {target_file}")
+            if not auto_mode:
+                choice = input("是否覆盖?(y/n): ")
+                if choice.lower() != 'y':
+                    return False
+
+        content = f"""# {entity_name}
+
+> **首次登场**: {entity.get('source_file', '未知')}
+> **创建时间**: {datetime.now().strftime('%Y-%m-%d')}
+
+## 基本信息
+
+{entity['desc']}
+
+## 详细设定
+
+待补充
+
+## 相关剧情
+
+- 【第 X 章】首次出现
+
+## 备注
+
+自动提取自 `<entity/>` 标签,请补充完善。
+"""
+
+        with open(target_file, 'w', encoding='utf-8') as f:
+            f.write(content)
+
+        print(f"✅ 已创建物品卡: {target_file}")
+        return True
+
+    else:
+        print(f"⚠️  未知实体类型: {entity_type}")
+        return False
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="XML 标签提取与同步 (<entity/>, <entity-alias/>, <entity-update>, <skill/>, <foreshadow/>, <deviation/>, <relationship/>)",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 指定文件(兼容卷目录)
+  python extract_entities.py "webnovel-project/正文/第1卷/第001章-死亡降临.md" --auto
+
+  # 指定章节号(推荐)
+  python extract_entities.py --project-root "webnovel-project" --chapter 1 --auto
+""".strip(),
+    )
+
+    parser.add_argument("chapter_file", nargs="?", help="章节文件路径(或使用 --chapter)")
+    parser.add_argument("--chapter", type=int, help="章节号(与 --project-root 配合,自动定位章节文件)")
+    parser.add_argument("--project-root", default=None, help="项目根目录(包含 .webnovel/state.json)")
+    parser.add_argument("--auto", action="store_true", help="自动模式(非交互)")
+    parser.add_argument("--dry-run", action="store_true", help="仅预览,不写入文件/状态")
+
+    args = parser.parse_args()
+
+    auto_mode = args.auto
+    dry_run = args.dry_run
+
+    project_root: Optional[Path] = None
+    if args.project_root:
+        project_root = resolve_project_root(args.project_root)
+    else:
+        try:
+            project_root = resolve_project_root()
+        except FileNotFoundError:
+            project_root = None
+
+    chapter_file: Optional[str] = None
+    chapter_num: Optional[int] = None
+
+    if args.chapter is not None:
+        if not project_root:
+            print("❌ 未提供有效的 --project-root,无法用 --chapter 定位章节文件")
+            sys.exit(1)
+
+        chapter_num = int(args.chapter)
+        chapter_path = find_chapter_file(project_root, chapter_num)
+        if not chapter_path:
+            print(f"❌ 未找到第{chapter_num}章文件(请先生成/保存章节)")
+            sys.exit(1)
+        chapter_file = str(chapter_path)
+    else:
+        if not args.chapter_file:
+            parser.error("必须提供 chapter_file 或 --chapter")
+        chapter_file = args.chapter_file
+        if not os.path.exists(chapter_file):
+            print(f"❌ 文件不存在: {chapter_file}")
+            sys.exit(1)
+
+        chapter_num = extract_chapter_num_from_filename(Path(chapter_file).name)
+
+    print(f"📖 正在扫描: {chapter_file}")
+    entities = extract_new_entities(chapter_file)
+    entity_alias_ops = extract_entity_alias_ops(chapter_file)
+    entity_update_ops = extract_entity_update_ops(chapter_file)
+    golden_finger_skills = extract_golden_finger_skills(chapter_file)
+    foreshadowing_items = extract_foreshadowing_json(chapter_file)
+    deviations = extract_deviations(chapter_file)
+    relationship_items = extract_relationships(chapter_file)
+
+    if not entities and not entity_alias_ops and not entity_update_ops and not golden_finger_skills and not foreshadowing_items and not deviations and not relationship_items:
+        print("✅ 未发现任何 XML 标签(<entity>/<entity-alias>/<entity-update>/<skill>/<foreshadow>/<deviation>/<relationship>)")
+        return
+
+    if entities:
+        print(f"\n🔍 发现 {len(entities)} 个新实体:")
+        for i, entity in enumerate(entities, 1):
+            tier_emoji = {"核心": "🔴", "支线": "🟡", "装饰": "🟢"}.get(entity.get("tier", "支线"), "⚪")
+            print(
+                f"  {i}. [{entity['type']}] {entity['name']} {tier_emoji}{entity.get('tier', '支线')} - {entity['desc'][:25]}..."
+            )
+
+    if golden_finger_skills:
+        print(f"\n✨ 发现 {len(golden_finger_skills)} 个金手指技能:")
+        for i, skill in enumerate(golden_finger_skills, 1):
+            print(f"  {i}. {skill['name']} ({skill['level']}) - {skill['desc'][:25]}...")
+
+    if entity_alias_ops:
+        print(f"\n🏷️ 发现 {len(entity_alias_ops)} 条实体别名:")
+        for i, op in enumerate(entity_alias_ops, 1):
+            ref = op.get("id") or op.get("ref") or "?"
+            print(f"  {i}. {ref} -> {op.get('alias', '')}")
+
+    if entity_update_ops:
+        print(f"\n🛠️ 发现 {len(entity_update_ops)} 条实体更新:")
+        for i, op in enumerate(entity_update_ops, 1):
+            ref = op.get("id") or op.get("ref") or "?"
+            operations = op.get("operations") or []
+            ops_preview = []
+            for o in operations[:6]:
+                if isinstance(o, dict):
+                    op_type = o.get("op", "set")
+                    key = o.get("key", "")
+                    ops_preview.append(f"{op_type}:{key}")
+            preview = ", ".join(ops_preview) + ("..." if len(operations) > 6 else "")
+            print(f"  {i}. {ref}: {preview}")
+
+    if foreshadowing_items:
+        print(f"\n🧩 发现 {len(foreshadowing_items)} 条伏笔:")
+        for i, item in enumerate(foreshadowing_items, 1):
+            tier = item.get("tier", "支线")
+            target = item.get("target_chapter", "未设定")
+            print(f"  {i}. {tier} → 目标Ch{target}: {str(item.get('content', ''))[:40]}...")
+
+    if deviations:
+        print(f"\n⚡ 发现 {len(deviations)} 条大纲偏离:")
+        for i, dev in enumerate(deviations, 1):
+            print(f"  {i}. {dev.get('reason', '')[:50]}...")
+
+    if relationship_items:
+        print(f"\n💕 发现 {len(relationship_items)} 条关系:")
+        for i, rel in enumerate(relationship_items, 1):
+            char1 = str(rel.get("char1") or rel.get("char1_id") or "").strip() or "?"
+            char2 = str(rel.get("char2") or rel.get("char2_id") or "").strip() or "?"
+            print(f"  {i}. {char1} ↔ {char2} ({rel['type']}, 强度 {rel['intensity']})")
+
+    if dry_run:
+        print("\n⚠️  Dry-run 模式,不执行实际写入")
+        return
+
+    if not project_root:
+        chapter_path = Path(chapter_file).resolve()
+        for parent in [chapter_path.parent] + list(chapter_path.parents):
+            if (parent / ".webnovel" / "state.json").exists():
+                project_root = parent
+                break
+
+    if not project_root:
+        print("❌ 找不到项目根目录(缺少 .webnovel/state.json)")
+        print("请先运行 /webnovel-init 初始化项目,或使用 --project-root 指定路径")
+        sys.exit(1)
+
+    state_file = resolve_state_file(explicit_project_root=str(project_root))
+
+    print("\n📝 开始同步到设定集...")
+    success_count = 0
+    for entity in entities:
+        if sync_entity_to_settings(entity, str(project_root), auto_mode):
+            success_count += 1
+
+    print("\n💾 更新 state.json...")
+    try:
+        update_state_json(
+            entities=entities,
+            state_file=str(state_file),
+            golden_finger_skills=golden_finger_skills,
+            foreshadowing_items=foreshadowing_items,
+            relationship_items=relationship_items,
+            entity_alias_ops=entity_alias_ops,
+            entity_update_ops=entity_update_ops,
+            default_planted_chapter=chapter_num,
+        )
+    except (AmbiguousAliasError, ValueError) as e:
+        print(f"❌ {e}")
+        sys.exit(2)
+
+    print("\n✅ 完成!")
+    print(f"  - 实体同步: {success_count}/{len(entities)} 个")
+    if golden_finger_skills:
+        print(f"  - 金手指技能: {len(golden_finger_skills)} 个")
+    if foreshadowing_items:
+        print(f"  - 伏笔同步: {len(foreshadowing_items)} 条")
+    if relationship_items:
+        print(f"  - 关系同步: {len(relationship_items)} 条")
+    if deviations:
+        print(f"  - 大纲偏离: {len(deviations)} 条(仅记录,不同步到 state.json)")
+
+    if not auto_mode:
+        print("\n💡 建议:")
+        print("  1. 检查生成的角色卡/物品卡,补充详细设定")
+        print("  2. 查看 世界观.md 和 力量体系.md 的更新")
+        print("  3. 确认 .webnovel/state.json 中的实体记录")
+        if golden_finger_skills:
+            print("  4. 检查金手指技能是否正确记录在 protagonist_state.golden_finger.skills")
+        if foreshadowing_items:
+            print("  5. 检查 plot_threads.foreshadowing 的 planted/target/tier/location/characters 是否合理")
+        if relationship_items:
+            print("  6. 检查 structured_relationships 关系记录是否合理")
+        if deviations:
+            print("  7. 大纲偏离已记录,请在 plan.md 或大纲中同步调整")
+
+if __name__ == "__main__":
+    main()

+ 570 - 0
.claude/scripts/golden_three_checker.py

@@ -0,0 +1,570 @@
+# -*- coding: utf-8 -*-
+"""
+黄金三章检查工具 v2.0 (LLM-Driven)
+
+功能:检测小说前三章是否符合"黄金三章"标准
+
+v2.0 重大升级:
+- 保留关键词预检作为快速模式
+- 新增 LLM 深度评估模式(AI Native)
+- 生成结构化评估 Prompt,解析 XML 评估结果
+
+核心检查点:
+- 第 1 章:300 字内主角出场 + 金手指线索 + 强冲突开局
+- 第 2 章:金手指展示 + 初次小胜 + 即时爽点
+- 第 3 章:悬念钩子 + 下一阶段预告 + 爽点密度 >= 1
+
+使用方法:
+python golden_three_checker.py --auto                    # 快速关键词模式
+python golden_three_checker.py --auto --mode llm         # LLM 深度评估(推荐)
+python golden_three_checker.py --auto --generate-prompt  # 仅生成评估 Prompt
+"""
+
+import sys
+import os
+import re
+import json
+import argparse
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+
+# 导入项目定位和章节路径模块
+from project_locator import resolve_project_root
+from chapter_paths import find_chapter_file
+
+# Windows UTF-8 输出修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+
+# ============================================================================
+# LLM 评估 Prompt 模板
+# ============================================================================
+
+LLM_EVALUATION_PROMPT = """你是一位网文编辑,专门负责评估小说开篇的"黄金三章"质量。
+
+请根据以下标准,对这三章内容进行专业评估:
+
+## 黄金三章标准
+
+### 第 1 章核心检查点:
+1. **主角 300 字内出场**:主角是否在前 300 字内登场?身份是否清晰?
+2. **金手指线索**:是否有金手指/外挂的暗示或线索?
+3. **强冲突开局**:开篇是否有足够强的冲突/危机/矛盾?
+
+### 第 2 章核心检查点:
+1. **金手指展示**:金手指是否有明确展示?读者能否理解其能力?
+2. **初次小胜**:主角是否获得了第一次小规模胜利/成功?
+3. **即时爽点**:是否有让读者感到爽快/满足的场景?
+
+### 第 3 章核心检查点:
+1. **悬念钩子**:章节结尾是否有悬念?能否驱动读者继续阅读?
+2. **下一阶段预告**:是否暗示了接下来的剧情走向/新挑战?
+3. **爽点密度**:本章是否至少有 1 个明显的爽点场景?
+
+---
+
+## 待评估内容
+
+### 第 1 章
+```
+{chapter1_content}
+```
+
+### 第 2 章
+```
+{chapter2_content}
+```
+
+### 第 3 章
+```
+{chapter3_content}
+```
+
+---
+
+## 输出要求
+
+请以如下 XML 格式输出你的评估结果(务必严格遵循格式):
+
+```xml
+<golden_three_assessment>
+  <chapter num="1">
+    <check name="主角300字内出场" passed="true|false" score="0-100">
+      <evidence>具体证据/引用原文</evidence>
+      <suggestion>如未通过,给出改进建议</suggestion>
+    </check>
+    <check name="金手指线索" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+    <check name="强冲突开局" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+  </chapter>
+
+  <chapter num="2">
+    <check name="金手指展示" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+    <check name="初次小胜" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+    <check name="即时爽点" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+  </chapter>
+
+  <chapter num="3">
+    <check name="悬念钩子" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+    <check name="下一阶段预告" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+    <check name="爽点密度>=1" passed="true|false" score="0-100">
+      <evidence>具体证据</evidence>
+      <suggestion>改进建议</suggestion>
+    </check>
+  </chapter>
+
+  <overall_score>0-100</overall_score>
+  <verdict>优秀|良好|需改进|严重不足</verdict>
+  <top_issues>
+    <issue priority="1">最需要改进的问题</issue>
+    <issue priority="2">次要问题</issue>
+  </top_issues>
+</golden_three_assessment>
+```
+
+现在开始评估:
+"""
+
+
+class GoldenThreeChecker:
+    """黄金三章检查器 v2.0"""
+
+    def __init__(self, chapter_files: List[str], mode: str = "keyword"):
+        """
+        初始化检查器
+
+        Args:
+            chapter_files: 章节文件路径列表(必须是前3章)
+            mode: 检查模式 ("keyword" 快速模式, "llm" LLM评估模式)
+        """
+        if len(chapter_files) != 3:
+            raise ValueError("必须提供前 3 章的文件路径")
+
+        self.chapter_files = chapter_files
+        self.mode = mode
+        self.chapters: List[Dict[str, Any]] = []
+        self.results: Dict[str, Any] = {
+            "mode": mode,
+            "ch1": {"主角300字内出场": False, "金手指线索": False, "强冲突开局": False, "详细": {}},
+            "ch2": {"金手指展示": False, "初次小胜": False, "即时爽点": False, "详细": {}},
+            "ch3": {"悬念钩子": False, "下一阶段预告": False, "爽点密度>=1": False, "详细": {}},
+        }
+
+    def load_chapters(self) -> None:
+        """加载章节内容"""
+        for i, file_path in enumerate(self.chapter_files):
+            if not os.path.exists(file_path):
+                raise FileNotFoundError(f"文件不存在: {file_path}")
+
+            with open(file_path, 'r', encoding='utf-8') as f:
+                content = f.read()
+                self.chapters.append({
+                    "number": i + 1,
+                    "path": file_path,
+                    "content": content,
+                    "word_count": len(re.sub(r'\s+', '', content))
+                })
+
+    # ============================================================================
+    # 快速关键词模式(保留原有逻辑)
+    # ============================================================================
+
+    def check_chapter1_keywords(self) -> None:
+        """检查第1章(关键词模式)"""
+        content = self.chapters[0]["content"]
+        first_300_chars = content[:300]
+
+        # 检查1: 主角 300 字内出场
+        protagonist_keywords = ["林天", "我", "主角", "少年", "他", "叶凡", "萧炎", "楚枫"]
+        for keyword in protagonist_keywords:
+            if keyword in first_300_chars:
+                self.results["ch1"]["主角300字内出场"] = True
+                self.results["ch1"]["详细"]["主角出场关键词"] = keyword
+                break
+
+        # 检查2: 金手指线索
+        golden_finger_keywords = [
+            "系统", "空间", "重生", "穿越", "戒指", "老爷爷",
+            "器灵", "传承", "血脉", "觉醒", "签到", "任务", "面板", "属性"
+        ]
+        found = [kw for kw in golden_finger_keywords if kw in content]
+        self.results["ch1"]["金手指线索"] = len(found) > 0
+        self.results["ch1"]["详细"]["金手指关键词"] = found
+
+        # 检查3: 强冲突开局
+        conflict_keywords = [
+            "退婚", "羞辱", "嘲讽", "废物", "落魄", "危机",
+            "追杀", "绝境", "被困", "重伤", "濒死", "灭族"
+        ]
+        found = [kw for kw in conflict_keywords if kw in content]
+        self.results["ch1"]["强冲突开局"] = len(found) > 0
+        self.results["ch1"]["详细"]["冲突关键词"] = found
+
+    def check_chapter2_keywords(self) -> None:
+        """检查第2章(关键词模式)"""
+        content = self.chapters[1]["content"]
+
+        system_display_keywords = ["【", "╔", "姓名", "境界", "力量", "属性", "获得", "奖励", "升级"]
+        found = [kw for kw in system_display_keywords if kw in content]
+        self.results["ch2"]["金手指展示"] = len(found) >= 2
+        self.results["ch2"]["详细"]["展示关键词"] = found
+
+        victory_keywords = ["击败", "胜利", "获胜", "成功", "通过", "突破", "秒杀", "碾压"]
+        found = [kw for kw in victory_keywords if kw in content]
+        self.results["ch2"]["初次小胜"] = len(found) > 0
+        self.results["ch2"]["详细"]["胜利关键词"] = found
+
+        cool_keywords = ["震惊", "不可能", "怎么会", "全场哗然", "目瞪口呆", "难以置信"]
+        found = [kw for kw in cool_keywords if kw in content]
+        self.results["ch2"]["即时爽点"] = len(found) >= 2
+        self.results["ch2"]["详细"]["爽点关键词"] = found
+
+    def check_chapter3_keywords(self) -> None:
+        """检查第3章(关键词模式)"""
+        content = self.chapters[2]["content"]
+        last_300_chars = content[-300:]
+
+        suspense_keywords = ["?", "!", "危机", "即将", "突然", "就在这时", "阴影", "杀机"]
+        found = [kw for kw in suspense_keywords if kw in last_300_chars]
+        self.results["ch3"]["悬念钩子"] = len(found) >= 2
+        self.results["ch3"]["详细"]["悬念关键词"] = found
+
+        preview_keywords = ["秘境", "大比", "选拔", "试炼", "任务", "挑战", "前往", "即将"]
+        found = [kw for kw in preview_keywords if kw in content]
+        self.results["ch3"]["下一阶段预告"] = len(found) > 0
+        self.results["ch3"]["详细"]["预告关键词"] = found
+
+        cool_count = sum(content.count(kw) for kw in ["震惊", "不可能", "全场哗然", "天才", "击败", "获得"])
+        self.results["ch3"]["爽点密度>=1"] = cool_count >= 1
+        self.results["ch3"]["详细"]["爽点统计"] = cool_count
+
+    # ============================================================================
+    # LLM 评估模式
+    # ============================================================================
+
+    def generate_llm_prompt(self) -> str:
+        """生成 LLM 评估 Prompt"""
+        # 截取每章内容(避免过长)
+        max_chars_per_chapter = 6000
+
+        ch1 = self.chapters[0]["content"][:max_chars_per_chapter]
+        ch2 = self.chapters[1]["content"][:max_chars_per_chapter]
+        ch3 = self.chapters[2]["content"][:max_chars_per_chapter]
+
+        prompt = LLM_EVALUATION_PROMPT.format(
+            chapter1_content=ch1,
+            chapter2_content=ch2,
+            chapter3_content=ch3
+        )
+        return prompt
+
+    def parse_llm_response(self, xml_response: str) -> Dict[str, Any]:
+        """解析 LLM 返回的 XML 评估结果"""
+        results: Dict[str, Any] = {
+            "mode": "llm",
+            "ch1": {"详细": {}},
+            "ch2": {"详细": {}},
+            "ch3": {"详细": {}},
+            "overall_score": 0,
+            "verdict": "",
+            "top_issues": []
+        }
+
+        # 提取 overall_score
+        score_match = re.search(r'<overall_score>(\d+)</overall_score>', xml_response)
+        if score_match:
+            results["overall_score"] = int(score_match.group(1))
+
+        # 提取 verdict
+        verdict_match = re.search(r'<verdict>([^<]+)</verdict>', xml_response)
+        if verdict_match:
+            results["verdict"] = verdict_match.group(1).strip()
+
+        # 提取每章的检查点
+        chapter_pattern = re.compile(
+            r'<chapter num="(\d)">(.*?)</chapter>',
+            re.DOTALL
+        )
+        check_pattern = re.compile(
+            r'<check name="([^"]+)" passed="(true|false)" score="(\d+)">\s*'
+            r'<evidence>([^<]*)</evidence>\s*'
+            r'<suggestion>([^<]*)</suggestion>\s*'
+            r'</check>',
+            re.DOTALL
+        )
+
+        for chapter_match in chapter_pattern.finditer(xml_response):
+            chapter_num = chapter_match.group(1)
+            chapter_content = chapter_match.group(2)
+            chapter_key = f"ch{chapter_num}"
+
+            for check_match in check_pattern.finditer(chapter_content):
+                check_name = check_match.group(1)
+                passed = check_match.group(2) == "true"
+                score = int(check_match.group(3))
+                evidence = check_match.group(4).strip()
+                suggestion = check_match.group(5).strip()
+
+                results[chapter_key][check_name] = passed
+                results[chapter_key]["详细"][check_name] = {
+                    "score": score,
+                    "evidence": evidence,
+                    "suggestion": suggestion
+                }
+
+        # 提取 top_issues
+        issue_pattern = re.compile(r'<issue priority="(\d)">([^<]+)</issue>')
+        for issue_match in issue_pattern.finditer(xml_response):
+            priority = int(issue_match.group(1))
+            issue_text = issue_match.group(2).strip()
+            results["top_issues"].append({"priority": priority, "issue": issue_text})
+
+        return results
+
+    # ============================================================================
+    # 报告生成
+    # ============================================================================
+
+    def calculate_score(self) -> tuple:
+        """计算总体得分"""
+        total_checks = 0
+        passed_checks = 0
+
+        for chapter_key in ["ch1", "ch2", "ch3"]:
+            for check_key, check_value in self.results[chapter_key].items():
+                if check_key != "详细" and isinstance(check_value, bool):
+                    total_checks += 1
+                    if check_value:
+                        passed_checks += 1
+
+        score = (passed_checks / total_checks) * 100 if total_checks > 0 else 0
+        return score, passed_checks, total_checks
+
+    def generate_report(self) -> str:
+        """生成检查报告"""
+        score, passed, total = self.calculate_score()
+
+        report = []
+        report.append("=" * 60)
+        report.append(f"黄金三章诊断报告 (模式: {self.mode})")
+        report.append("=" * 60)
+        report.append(f"\n总体得分: {score:.1f}% ({passed}/{total} 项通过)\n")
+
+        # 第 1 章
+        report.append("-" * 60)
+        report.append("【第 1 章】检查结果")
+        report.append("-" * 60)
+        for check_name in ["主角300字内出场", "金手指线索", "强冲突开局"]:
+            passed = self.results["ch1"].get(check_name, False)
+            icon = "✅" if passed else "❌"
+            report.append(f"{icon} {check_name}: {'通过' if passed else '未通过'}")
+
+            # 显示详细信息
+            detail = self.results["ch1"]["详细"].get(check_name)
+            if isinstance(detail, dict):
+                if detail.get("evidence"):
+                    report.append(f"   └─ 证据: {detail['evidence'][:100]}...")
+                if not passed and detail.get("suggestion"):
+                    report.append(f"   └─ 建议: {detail['suggestion']}")
+            elif isinstance(detail, list) and detail:
+                report.append(f"   └─ 关键词: {', '.join(detail[:5])}")
+
+        # 第 2 章
+        report.append("\n" + "-" * 60)
+        report.append("【第 2 章】检查结果")
+        report.append("-" * 60)
+        for check_name in ["金手指展示", "初次小胜", "即时爽点"]:
+            passed = self.results["ch2"].get(check_name, False)
+            icon = "✅" if passed else "❌"
+            report.append(f"{icon} {check_name}: {'通过' if passed else '未通过'}")
+            detail = self.results["ch2"]["详细"].get(check_name)
+            if isinstance(detail, dict) and detail.get("evidence"):
+                report.append(f"   └─ 证据: {detail['evidence'][:100]}...")
+            elif isinstance(detail, list) and detail:
+                report.append(f"   └─ 关键词: {', '.join(detail[:5])}")
+
+        # 第 3 章
+        report.append("\n" + "-" * 60)
+        report.append("【第 3 章】检查结果")
+        report.append("-" * 60)
+        for check_name in ["悬念钩子", "下一阶段预告", "爽点密度>=1"]:
+            passed = self.results["ch3"].get(check_name, False)
+            icon = "✅" if passed else "❌"
+            report.append(f"{icon} {check_name}: {'通过' if passed else '未通过'}")
+            detail = self.results["ch3"]["详细"].get(check_name)
+            if isinstance(detail, dict) and detail.get("evidence"):
+                report.append(f"   └─ 证据: {detail['evidence'][:100]}...")
+
+        # 改进建议
+        report.append("\n" + "=" * 60)
+        report.append("【改进建议】")
+        report.append("=" * 60)
+
+        if score < 60:
+            report.append("\n🔴 警告: 开篇吸引力不足,严重影响读者留存率!")
+        elif score < 80:
+            report.append("\n🟡 注意: 开篇有改进空间")
+        else:
+            report.append("\n✅ 很好!开篇符合黄金三章标准")
+
+        # LLM 模式的额外信息
+        if self.mode == "llm" and self.results.get("top_issues"):
+            report.append("\n优先修复:")
+            for issue in self.results["top_issues"]:
+                report.append(f"  {issue['priority']}. {issue['issue']}")
+
+        report.append("\n" + "=" * 60)
+        return "\n".join(report)
+
+    def run(self) -> None:
+        """执行检查"""
+        print("正在加载章节...")
+        self.load_chapters()
+
+        print(f"✅ 已加载 {len(self.chapters)} 章")
+        for ch in self.chapters:
+            print(f"   - 第 {ch['number']} 章: {ch['word_count']} 字")
+        print(f"\n正在执行检查 (模式: {self.mode})...\n")
+
+        if self.mode == "keyword":
+            self.check_chapter1_keywords()
+            self.check_chapter2_keywords()
+            self.check_chapter3_keywords()
+            report = self.generate_report()
+            print(report)
+
+        elif self.mode == "llm":
+            prompt = self.generate_llm_prompt()
+            print("=" * 60)
+            print("LLM 评估模式:请将以下 Prompt 发送给 Claude/GPT")
+            print("=" * 60)
+            print("\n--- PROMPT START ---\n")
+            print(prompt[:2000] + "\n...[内容已截断,完整版见输出文件]...")
+            print("\n--- PROMPT END ---\n")
+
+            # 保存完整 prompt
+            output_dir = Path(".webnovel")
+            output_dir.mkdir(exist_ok=True)
+            prompt_file = output_dir / "golden_three_prompt.md"
+            with open(prompt_file, 'w', encoding='utf-8') as f:
+                f.write(prompt)
+            print(f"📄 完整 Prompt 已保存至: {prompt_file}")
+            print("\n💡 使用方法:")
+            print("   1. 将 Prompt 发送给 Claude/GPT")
+            print("   2. 获取 XML 格式的评估结果")
+            print("   3. 运行: python golden_three_checker.py --parse-response <response.xml>")
+
+        # 保存结果
+        output_dir = Path(".webnovel")
+        output_dir.mkdir(exist_ok=True)
+        output_file = output_dir / "golden_three_report.json"
+        with open(output_file, 'w', encoding='utf-8') as f:
+            json.dump(self.results, f, ensure_ascii=False, indent=2)
+        print(f"\n📄 详细结果已保存至: {output_file}")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="黄金三章检查工具 v2.0 (LLM-Driven)",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 快速关键词模式(默认)
+  python golden_three_checker.py --auto
+
+  # LLM 深度评估模式(推荐)
+  python golden_three_checker.py --auto --mode llm
+
+  # 解析 LLM 返回的评估结果
+  python golden_three_checker.py --parse-response response.xml
+""".strip(),
+    )
+
+    parser.add_argument("chapter_files", nargs="*", help="前三章文件路径")
+    parser.add_argument("--auto", action="store_true", help="自动定位前三章文件")
+    parser.add_argument("--mode", choices=["keyword", "llm"], default="keyword",
+                        help="检查模式: keyword(快速) / llm(深度)")
+    parser.add_argument("--project-root", default=None, help="项目根目录")
+    parser.add_argument("--parse-response", metavar="FILE", help="解析 LLM 返回的 XML 文件")
+
+    args = parser.parse_args()
+
+    # 解析 LLM 响应模式
+    if args.parse_response:
+        if not os.path.exists(args.parse_response):
+            print(f"❌ 文件不存在: {args.parse_response}")
+            sys.exit(1)
+
+        with open(args.parse_response, 'r', encoding='utf-8') as f:
+            xml_content = f.read()
+
+        checker = GoldenThreeChecker(["dummy"] * 3, mode="llm")
+        checker.results = checker.parse_llm_response(xml_content)
+
+        print("=" * 60)
+        print("LLM 评估结果解析")
+        print("=" * 60)
+        print(json.dumps(checker.results, ensure_ascii=False, indent=2))
+        sys.exit(0)
+
+    # 正常检查模式
+    chapter_files = []
+
+    if args.auto or not args.chapter_files:
+        try:
+            project_root = resolve_project_root(args.project_root)
+        except FileNotFoundError as e:
+            print(f"❌ {e}")
+            sys.exit(1)
+
+        for i in range(1, 4):
+            chapter_path = find_chapter_file(project_root, i)
+            if chapter_path:
+                chapter_files.append(str(chapter_path))
+            else:
+                print(f"❌ 找不到第 {i} 章文件")
+                sys.exit(1)
+
+        print(f"📂 项目根目录: {project_root}")
+        print(f"📄 检测到前三章: {', '.join(Path(f).name for f in chapter_files)}\n")
+    else:
+        if len(args.chapter_files) < 3:
+            print("用法: python golden_three_checker.py <第1章路径> <第2章路径> <第3章路径>")
+            sys.exit(1)
+        chapter_files = args.chapter_files[:3]
+
+    try:
+        checker = GoldenThreeChecker(chapter_files, mode=args.mode)
+        checker.run()
+    except Exception as e:
+        print(f"❌ 错误: {e}")
+        import traceback
+        traceback.print_exc()
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

+ 491 - 0
.claude/scripts/init_project.py

@@ -0,0 +1,491 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+网文项目初始化脚本
+
+目标:
+- 生成可运行的项目结构(webnovel-project)
+- 创建/更新 .webnovel/state.json(运行时真相)
+- 生成基础设定集与大纲模板文件(供 /webnovel-plan 与 /webnovel-write 使用)
+
+说明:
+- 该脚本是命令 /webnovel-init 的“唯一允许的文件生成入口”(与命令文档保持一致)。
+- 生成的内容以“模板骨架”为主,便于 AI/作者后续补全;但保证所有关键文件存在。
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import subprocess
+import sys
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict
+
+# 安全修复:导入安全工具函数
+from security_utils import sanitize_commit_message, atomic_write_json, is_git_available
+
+
+# Windows 编码兼容性修复
+if sys.platform == "win32":
+    import io
+
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
+
+
+def _read_text_if_exists(path: Path) -> str:
+    if not path.exists():
+        return ""
+    return path.read_text(encoding="utf-8")
+
+
+def _write_text_if_missing(path: Path, content: str) -> None:
+    path.parent.mkdir(parents=True, exist_ok=True)
+    if path.exists():
+        return
+    path.write_text(content, encoding="utf-8")
+
+
+def _ensure_state_schema(state: Dict[str, Any]) -> Dict[str, Any]:
+    """确保 state.json 具备 v5.0 架构所需的字段集合。"""
+    state.setdefault("project_info", {})
+    state.setdefault("progress", {})
+    state.setdefault("protagonist_state", {})
+    state.setdefault("relationships", {})
+    state.setdefault("structured_relationships", [])
+    state.setdefault("disambiguation_warnings", [])
+    state.setdefault("disambiguation_pending", [])
+    state.setdefault("world_settings", {"power_system": [], "factions": [], "locations": []})
+    state.setdefault("plot_threads", {"active_threads": [], "foreshadowing": []})
+    state.setdefault("review_checkpoints", [])
+    state.setdefault(
+        "strand_tracker",
+        {
+            "last_quest_chapter": 0,
+            "last_fire_chapter": 0,
+            "last_constellation_chapter": 0,
+            "current_dominant": "quest",
+            "chapters_since_switch": 0,
+            "history": [],
+        },
+    )
+    # v5.0: entities_v3 分组格式(按类型)
+    state.setdefault(
+        "entities_v3",
+        {"角色": {}, "地点": {}, "物品": {}, "势力": {}, "招式": {}},
+    )
+    # v5.0: alias_index 一对多映射
+    state.setdefault("alias_index", {})
+
+    # progress schema evolution
+    state["progress"].setdefault("current_chapter", 0)
+    state["progress"].setdefault("total_words", 0)
+    state["progress"].setdefault("last_updated", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+    state["progress"].setdefault("volumes_completed", [])
+    state["progress"].setdefault("current_volume", 1)
+    state["progress"].setdefault("volumes_planned", [])
+
+    # protagonist schema evolution
+    ps = state["protagonist_state"]
+    ps.setdefault("name", "")
+    ps.setdefault("power", {"realm": "", "layer": 1, "bottleneck": ""})
+    ps.setdefault("location", {"current": "", "last_chapter": 0})
+    ps.setdefault("golden_finger", {"name": "", "level": 1, "cooldown": 0, "skills": []})
+    ps.setdefault("attributes", {})
+
+    return state
+
+
+def _build_master_outline(target_chapters: int, *, chapters_per_volume: int = 50) -> str:
+    volumes = (target_chapters - 1) // chapters_per_volume + 1 if target_chapters > 0 else 1
+    lines: list[str] = [
+        "# 总纲",
+        "",
+        "> 本文件为“总纲骨架”,用于 /webnovel-plan 细化为卷大纲与章纲。",
+        "",
+        "## 卷结构",
+        "",
+    ]
+
+    for v in range(1, volumes + 1):
+        start = (v - 1) * chapters_per_volume + 1
+        end = min(v * chapters_per_volume, target_chapters)
+        lines.extend(
+            [
+                f"### 第{v}卷(第{start}-{end}章)",
+                "- 核心冲突:",
+                "- 关键爽点:",
+                "- 卷末高潮:",
+                "- 主要登场角色:",
+                "- 关键伏笔(埋/收):",
+                "",
+            ]
+        )
+
+    return "\n".join(lines).rstrip() + "\n"
+
+
+def init_project(
+    project_dir: str,
+    title: str,
+    genre: str,
+    *,
+    protagonist_name: str = "",
+    target_words: int = 2_000_000,
+    target_chapters: int = 600,
+    golden_finger_name: str = "",
+    golden_finger_type: str = "",
+    golden_finger_style: str = "",
+    core_selling_points: str = "",
+    protagonist_desire: str = "",
+    protagonist_flaw: str = "",
+    protagonist_archetype: str = "",
+    antagonist_level: str = "",
+    target_reader: str = "",
+    platform: str = "",
+) -> None:
+    project_path = Path(project_dir).expanduser().resolve()
+    project_path.mkdir(parents=True, exist_ok=True)
+
+    # 目录结构(同时兼容“卷目录”与后续扩展)
+    directories = [
+        ".webnovel/backups",
+        ".webnovel/archive",
+        "设定集/角色库/主要角色",
+        "设定集/角色库/次要角色",
+        "设定集/角色库/反派角色",
+        "设定集/物品库",
+        "设定集/其他设定",
+        "大纲",
+        "正文/第1卷",
+        "审查报告",
+    ]
+    for dir_path in directories:
+        (project_path / dir_path).mkdir(parents=True, exist_ok=True)
+
+    # state.json(创建或增量补齐)
+    state_path = project_path / ".webnovel" / "state.json"
+    if state_path.exists():
+        try:
+            state: Dict[str, Any] = json.loads(state_path.read_text(encoding="utf-8"))
+        except json.JSONDecodeError:
+            state = {}
+    else:
+        state = {}
+
+    state = _ensure_state_schema(state)
+    created_at = state.get("project_info", {}).get("created_at") or datetime.now().strftime("%Y-%m-%d")
+
+    state["project_info"].update(
+        {
+            "title": title,
+            "genre": genre,
+            "created_at": created_at,
+            "target_words": int(target_words),
+            "target_chapters": int(target_chapters),
+            # 下面字段属于“初始化元信息”,不影响运行时脚本
+            "golden_finger_name": golden_finger_name,
+            "golden_finger_type": golden_finger_type,
+            "golden_finger_style": golden_finger_style,
+            "core_selling_points": core_selling_points,
+            "target_reader": target_reader,
+            "platform": platform,
+        }
+    )
+
+    if protagonist_name:
+        state["protagonist_state"]["name"] = protagonist_name
+
+    if golden_finger_name:
+        state["protagonist_state"]["golden_finger"]["name"] = golden_finger_name
+
+    # 确保 golden_finger 字段存在且可编辑
+    if not state["protagonist_state"]["golden_finger"].get("name"):
+        state["protagonist_state"]["golden_finger"]["name"] = "未命名金手指"
+
+    state["progress"]["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+    state_path.parent.mkdir(parents=True, exist_ok=True)
+    # 使用原子化写入(初始化不需要备份旧文件)
+    atomic_write_json(state_path, state, use_lock=True, backup=False)
+
+    # 读取内置模板(可选)
+    script_dir = Path(__file__).resolve().parent
+    templates_dir = script_dir.parent / "templates"
+    genre_key = (genre or "").strip()
+    genre_template_key = {
+        "修仙/玄幻": "修仙",
+        "玄幻": "修仙",
+    }.get(genre_key, genre_key)
+    genre_template = _read_text_if_exists(templates_dir / "genres" / f"{genre_template_key}.md")
+    golden_finger_templates = _read_text_if_exists(templates_dir / "golden-finger-templates.md")
+
+    # 基础文件(只在缺失时生成,避免覆盖已有内容)
+    now = datetime.now().strftime("%Y-%m-%d")
+
+    _write_text_if_missing(
+        project_path / "设定集" / "世界观.md",
+        "\n".join(
+            [
+                "# 世界观",
+                "",
+                f"> 项目:{title}|题材:{genre}|创建:{now}",
+                "",
+                "## 一句话世界观",
+                "- (用一句话说明世界的核心规则与卖点)",
+                "",
+                "## 核心规则(设定即物理)",
+                "- 规则1:",
+                "- 规则2:",
+                "- 规则3:",
+                "",
+                "## 势力与地理(简版)",
+                "- 主要势力:",
+                "- 关键地点:",
+                "",
+                "## 参考题材模板(可删/可改)",
+                "",
+                (genre_template.strip() + "\n") if genre_template else "(未找到对应题材模板,可自行补充)\n",
+            ]
+        ),
+    )
+
+    _write_text_if_missing(
+        project_path / "设定集" / "力量体系.md",
+        "\n".join(
+            [
+                "# 力量体系",
+                "",
+                f"> 项目:{title}|题材:{genre}|创建:{now}",
+                "",
+                "## 等级/境界划分",
+                "- (列出从弱到强的等级,含突破条件与代价)",
+                "",
+                "## 技能/招式规则",
+                "- 获得方式:",
+                "- 成本与副作用:",
+                "- 进阶与组合:",
+                "",
+                "## 禁止事项(防崩坏)",
+                "- 未达等级不得使用高阶能力(设定即物理)",
+                "- 新增能力必须申报并入库(发明需申报)",
+                "",
+            ]
+        ),
+    )
+
+    _write_text_if_missing(
+        project_path / "设定集" / "主角卡.md",
+        "\n".join(
+            [
+                "# 主角卡",
+                "",
+                f"> 主角:{protagonist_name or '(待填写)'}|项目:{title}|创建:{now}",
+                "",
+                "## 三要素",
+                f"- 欲望:{protagonist_desire or '(待填写)'}",
+                f"- 弱点:{protagonist_flaw or '(待填写)'}",
+                f"- 人设类型:{protagonist_archetype or '(待填写)'}",
+                "",
+                "## 初始状态(开局)",
+                "- 身份:",
+                "- 资源:",
+                "- 约束:",
+                "",
+                "## 金手指概览",
+                f"- 称呼:{golden_finger_name or '(待填写)'}",
+                f"- 类型:{golden_finger_type or '(待填写)'}",
+                f"- 风格:{golden_finger_style or '(待填写)'}",
+                "- 成长曲线:",
+                "",
+            ]
+        ),
+    )
+
+    _write_text_if_missing(
+        project_path / "设定集" / "金手指设计.md",
+        "\n".join(
+            [
+                "# 金手指设计",
+                "",
+                f"> 项目:{title}|题材:{genre}|创建:{now}",
+                "",
+                "## 选型",
+                f"- 称呼:{golden_finger_name or '(待填写)'}",
+                f"- 类型:{golden_finger_type or '(待填写)'}",
+                f"- 风格:{golden_finger_style or '(待填写)'}",
+                "",
+                "## 规则(必须写清)",
+                "- 触发条件:",
+                "- 冷却/代价:",
+                "- 上限:",
+                "- 反噬/风险:",
+                "",
+                "## 成长曲线(章节规划)",
+                "- Lv1:",
+                "- Lv2:",
+                "- Lv3:",
+                "",
+                "## 模板参考(可删/可改)",
+                "",
+                (golden_finger_templates.strip() + "\n") if golden_finger_templates else "(未找到金手指模板库)\n",
+            ]
+        ),
+    )
+
+    if antagonist_level:
+        _write_text_if_missing(
+            project_path / "设定集" / "反派设计.md",
+            "\n".join(
+                [
+                    "# 反派设计",
+                    "",
+                    f"> 项目:{title}|创建:{now}",
+                    "",
+                    f"- 反派等级:{antagonist_level}",
+                    "- 动机:",
+                    "- 资源/势力:",
+                    "- 与主角的镜像关系:",
+                    "- 终局:",
+                    "",
+                ]
+            ),
+        )
+
+    _write_text_if_missing(project_path / "大纲" / "总纲.md", _build_master_outline(int(target_chapters)))
+
+    _write_text_if_missing(
+        project_path / "大纲" / "爽点规划.md",
+        "\n".join(
+            [
+                "# 爽点规划",
+                "",
+                f"> 项目:{title}|题材:{genre}|创建:{now}",
+                "",
+                "## 核心卖点(来自初始化输入)",
+                f"- {core_selling_points or '(待填写,建议 1-3 条,用逗号分隔)'}",
+                "",
+                "## 密度目标(建议)",
+                "- 每章至少 1 个小爽点",
+                "- 每 5 章至少 1 个大爽点",
+                "",
+                "## 分布表(示例,可改)",
+                "",
+                "| 章节范围 | 主导爽点类型 | 备注 |",
+                "|---|---|---|",
+                "| 1-5 | 金手指/打脸/反转 | 开篇钩子 + 立人设 |",
+                "| 6-10 | 升级/收获 | 进入主线节奏 |",
+                "",
+            ]
+        ),
+    )
+
+    # Git 初始化(仅当项目目录内尚无 .git 且 Git 可用)
+    git_dir = project_path / ".git"
+    if not git_dir.exists():
+        if not is_git_available():
+            print("\n⚠️  Git 不可用,跳过版本控制初始化")
+            print("💡 如需启用 Git 版本控制,请安装 Git: https://git-scm.com/")
+        else:
+            print("\nInitializing Git repository...")
+            try:
+                subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True, text=True)
+
+                gitignore_file = project_path / ".gitignore"
+                if not gitignore_file.exists():
+                    gitignore_file.write_text(
+                        """# Python
+__pycache__/
+*.py[cod]
+*.so
+
+# Temporary files
+*.tmp
+*.bak
+.DS_Store
+
+# IDE
+.vscode/
+.idea/
+
+# Don't ignore .webnovel (we need to track state.json)
+# But ignore cache files
+.webnovel/context_cache.json
+.webnovel/*.lock
+.webnovel/*.bak
+""",
+                        encoding="utf-8",
+                    )
+
+                subprocess.run(["git", "add", "."], cwd=project_path, check=True, capture_output=True)
+                # 安全修复:清理 title 防止命令注入
+                safe_title = sanitize_commit_message(title)
+                subprocess.run(
+                    ["git", "commit", "-m", f"初始化网文项目:{safe_title}"],
+                    cwd=project_path,
+                    check=True,
+                    capture_output=True,
+                )
+                print("Git initialized.")
+            except subprocess.CalledProcessError as e:
+                print(f"Git init failed (non-fatal): {e}")
+
+    print(f"\nProject initialized at: {project_path}")
+    print("Key files:")
+    print(" - .webnovel/state.json")
+    print(" - 设定集/世界观.md")
+    print(" - 设定集/力量体系.md")
+    print(" - 设定集/主角卡.md")
+    print(" - 设定集/金手指设计.md")
+    print(" - 大纲/总纲.md")
+    print(" - 大纲/爽点规划.md")
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="网文项目初始化脚本(生成项目结构 + state.json + 基础模板)")
+    parser.add_argument("project_dir", help="项目目录(建议 ./webnovel-project)")
+    parser.add_argument("title", help="小说标题")
+    parser.add_argument("genre", help="题材类型(如:修仙/系统流/都市异能/狗血言情/古言/现实题材/规则怪谈/知乎短篇)")
+
+    parser.add_argument("--protagonist-name", default="", help="主角姓名")
+    parser.add_argument("--target-words", type=int, default=2_000_000, help="目标总字数(默认 2000000)")
+    parser.add_argument("--target-chapters", type=int, default=600, help="目标总章节数(默认 600)")
+
+    parser.add_argument("--golden-finger-name", default="", help="金手指称呼/系统名(建议读者可见的代号)")
+    parser.add_argument("--golden-finger-type", default="", help="金手指类型(如 系统流/鉴定流/签到流)")
+    parser.add_argument("--golden-finger-style", default="", help="金手指风格(如 冷漠工具型/毒舌吐槽型)")
+    parser.add_argument("--core-selling-points", default="", help="核心卖点(逗号分隔)")
+
+    # 深度模式可选参数(用于预填模板)
+    parser.add_argument("--protagonist-desire", default="", help="主角核心欲望(深度模式)")
+    parser.add_argument("--protagonist-flaw", default="", help="主角性格弱点(深度模式)")
+    parser.add_argument("--protagonist-archetype", default="", help="主角人设类型(深度模式)")
+    parser.add_argument("--antagonist-level", default="", help="反派等级(深度模式)")
+    parser.add_argument("--target-reader", default="", help="目标读者(深度模式)")
+    parser.add_argument("--platform", default="", help="发布平台(深度模式)")
+
+    args = parser.parse_args()
+
+    init_project(
+        args.project_dir,
+        args.title,
+        args.genre,
+        protagonist_name=args.protagonist_name,
+        target_words=args.target_words,
+        target_chapters=args.target_chapters,
+        golden_finger_name=args.golden_finger_name,
+        golden_finger_type=args.golden_finger_type,
+        golden_finger_style=args.golden_finger_style,
+        core_selling_points=args.core_selling_points,
+        protagonist_desire=args.protagonist_desire,
+        protagonist_flaw=args.protagonist_flaw,
+        protagonist_archetype=args.protagonist_archetype,
+        antagonist_level=args.antagonist_level,
+        target_reader=args.target_reader,
+        platform=args.platform,
+    )
+
+
+if __name__ == "__main__":
+    main()

+ 94 - 0
.claude/scripts/project_locator.py

@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+"""
+Project location helpers for webnovel-writer scripts.
+
+Problem this solves:
+- Many scripts assumed CWD is the project root and used relative paths like `.webnovel/state.json`.
+- In this repo, commands/scripts are often invoked from the repo root, while the actual project lives
+  in a subdirectory (default: `webnovel-project/`).
+
+These helpers provide a single, consistent way to locate the active project root.
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from typing import Iterable, Optional
+
+
+DEFAULT_PROJECT_DIR_NAMES: tuple[str, ...] = ("webnovel-project",)
+
+
+def _candidate_roots(cwd: Path) -> Iterable[Path]:
+    yield cwd
+    for name in DEFAULT_PROJECT_DIR_NAMES:
+        yield cwd / name
+
+    for parent in cwd.parents:
+        yield parent
+        for name in DEFAULT_PROJECT_DIR_NAMES:
+            yield parent / name
+
+
+def _is_project_root(path: Path) -> bool:
+    return (path / ".webnovel" / "state.json").is_file()
+
+
+def resolve_project_root(explicit_project_root: Optional[str] = None, *, cwd: Optional[Path] = None) -> Path:
+    """
+    Resolve the webnovel project root directory (the directory containing `.webnovel/state.json`).
+
+    Resolution order:
+    1) explicit_project_root (if provided)
+    2) env var WEBNOVEL_PROJECT_ROOT (if set)
+    3) Search from cwd and parents, including common subdir `webnovel-project/`
+
+    Raises:
+        FileNotFoundError: if no valid project root can be found.
+    """
+    if explicit_project_root:
+        root = Path(explicit_project_root).expanduser().resolve()
+        if _is_project_root(root):
+            return root
+        raise FileNotFoundError(f"Not a webnovel project root (missing .webnovel/state.json): {root}")
+
+    env_root = os.environ.get("WEBNOVEL_PROJECT_ROOT")
+    if env_root:
+        root = Path(env_root).expanduser().resolve()
+        if _is_project_root(root):
+            return root
+        raise FileNotFoundError(f"WEBNOVEL_PROJECT_ROOT is set but invalid (missing .webnovel/state.json): {root}")
+
+    base = (cwd or Path.cwd()).resolve()
+    for candidate in _candidate_roots(base):
+        if _is_project_root(candidate):
+            return candidate.resolve()
+
+    raise FileNotFoundError(
+        "Unable to locate webnovel project root. Expected `.webnovel/state.json` under the current directory, "
+        "a parent directory, or `webnovel-project/`. Run /webnovel-init first or pass --project-root / set "
+        "WEBNOVEL_PROJECT_ROOT."
+    )
+
+
+def resolve_state_file(
+    explicit_state_file: Optional[str] = None,
+    *,
+    explicit_project_root: Optional[str] = None,
+    cwd: Optional[Path] = None,
+) -> Path:
+    """
+    Resolve `.webnovel/state.json` path.
+
+    If explicit_state_file is provided, returns it as-is (resolved to absolute if relative).
+    Otherwise derives it from resolve_project_root().
+    """
+    base = (cwd or Path.cwd()).resolve()
+    if explicit_state_file:
+        p = Path(explicit_state_file).expanduser()
+        return (base / p).resolve() if not p.is_absolute() else p.resolve()
+
+    root = resolve_project_root(explicit_project_root, cwd=base)
+    return root / ".webnovel" / "state.json"
+

+ 585 - 0
.claude/scripts/security_utils.py

@@ -0,0 +1,585 @@
+#!/usr/bin/env python3
+"""
+安全工具函数库
+用于webnovel-writer系统的通用安全函数
+
+创建时间: 2026-01-02
+创建原因: 安全审计发现路径遍历和命令注入漏洞
+修复方案: 集中管理所有安全相关的输入清理函数
+"""
+
+import json
+import os
+import re
+import sys
+import tempfile
+from pathlib import Path
+from typing import Any, Dict, Optional, Union
+
+# 尝试导入 filelock(可选依赖)
+try:
+    from filelock import FileLock
+    HAS_FILELOCK = True
+except ImportError:
+    HAS_FILELOCK = False
+
+
+def sanitize_filename(name: str, max_length: int = 100) -> str:
+    """
+    清理文件名,防止路径遍历攻击 (CWE-22)
+
+    安全关键函数 - 修复extract_entities.py路径遍历漏洞
+
+    Args:
+        name: 原始文件名(可能包含路径遍历字符)
+        max_length: 文件名最大长度(默认100字符)
+
+    Returns:
+        安全的文件名(仅包含基本文件名,移除所有路径信息)
+
+    示例:
+        >>> sanitize_filename("../../../etc/passwd")
+        'passwd'
+        >>> sanitize_filename("C:\\Windows\\System32")
+        'System32'
+        >>> sanitize_filename("正常角色名")
+        '正常角色名'
+
+    安全验证:
+        - ✅ 防止目录遍历(../、..\\)
+        - ✅ 防止绝对路径(/、C:\\)
+        - ✅ 移除特殊字符
+        - ✅ 长度限制
+    """
+    # Step 1: 仅保留基础文件名(移除所有路径)
+    safe_name = os.path.basename(name)
+
+    # Step 2: 移除路径分隔符(双重保险)
+    safe_name = safe_name.replace('/', '_').replace('\\', '_')
+
+    # Step 3: 只保留安全字符
+    # 允许:中文(\u4e00-\u9fff)、字母(a-zA-Z)、数字(0-9)、下划线(_)、连字符(-)
+    safe_name = re.sub(r'[^\w\u4e00-\u9fff-]', '_', safe_name)
+
+    # Step 4: 移除连续的下划线(美化)
+    safe_name = re.sub(r'_+', '_', safe_name)
+
+    # Step 5: 长度限制
+    if len(safe_name) > max_length:
+        safe_name = safe_name[:max_length]
+
+    # Step 6: 移除首尾下划线
+    safe_name = safe_name.strip('_')
+
+    # Step 7: 确保非空(防御性编程)
+    if not safe_name:
+        safe_name = "unnamed_entity"
+
+    return safe_name
+
+
+def sanitize_commit_message(message: str, max_length: int = 200) -> str:
+    """
+    清理Git提交消息,防止命令注入 (CWE-77)
+
+    安全关键函数 - 修复backup_manager.py命令注入漏洞
+
+    Args:
+        message: 原始提交消息(可能包含Git标志)
+        max_length: 消息最大长度(默认200字符)
+
+    Returns:
+        安全的提交消息(移除Git特殊标志和危险字符)
+
+    示例:
+        >>> sanitize_commit_message("Test\\n--author='Attacker'")
+        'Test  author Attacker'
+        >>> sanitize_commit_message("--amend Chapter 1")
+        'amend Chapter 1'
+
+    安全验证:
+        - ✅ 防止多行注入(换行符)
+        - ✅ 防止Git标志注入(--xxx)
+        - ✅ 防止参数分隔符混淆(引号)
+        - ✅ 防止单字母标志(-x)
+    """
+    # Step 1: 移除换行符(防止多行参数注入)
+    safe_msg = message.replace('\n', ' ').replace('\r', ' ')
+
+    # Step 2: 移除Git特殊标志(--开头的参数)
+    safe_msg = re.sub(r'--[\w-]+', '', safe_msg)
+
+    # Step 3: 移除引号(防止参数分隔符混淆)
+    safe_msg = safe_msg.replace("'", "").replace('"', '')
+
+    # Step 4: 移除前导的-(防止单字母标志如-m)
+    safe_msg = safe_msg.lstrip('-')
+
+    # Step 5: 移除连续空格(美化)
+    safe_msg = re.sub(r'\s+', ' ', safe_msg)
+
+    # Step 6: 长度限制
+    if len(safe_msg) > max_length:
+        safe_msg = safe_msg[:max_length]
+
+    # Step 7: 移除首尾空格
+    safe_msg = safe_msg.strip()
+
+    # Step 8: 确保非空
+    if not safe_msg:
+        safe_msg = "Untitled commit"
+
+    return safe_msg
+
+
+def create_secure_directory(path: str, mode: int = 0o700) -> Path:
+    """
+    创建安全目录(仅所有者可访问)
+
+    安全关键函数 - 修复文件权限配置缺失漏洞
+
+    Args:
+        path: 目录路径
+        mode: 权限模式(默认0o700,仅所有者可读写执行)
+
+    Returns:
+        Path对象
+
+    示例:
+        >>> create_secure_directory('.webnovel')
+        PosixPath('.webnovel')  # drwx------ (700)
+
+    安全验证:
+        - ✅ 仅所有者可访问(0o700)
+        - ✅ 防止同组用户读取
+        - ✅ 跨平台兼容(Windows/Linux/macOS)
+    """
+    path_obj = Path(path)
+
+    # 创建目录(设置安全权限)
+    os.makedirs(path, mode=mode, exist_ok=True)
+
+    # 双重保险:显式设置权限(某些系统可能忽略makedirs的mode参数)
+    if os.name != 'nt':  # Unix系统(Linux/macOS)
+        os.chmod(path, mode)
+
+    return path_obj
+
+
+def create_secure_file(file_path: str, content: str, mode: int = 0o600) -> None:
+    """
+    创建安全文件(仅所有者可读写)
+
+    Args:
+        file_path: 文件路径
+        content: 文件内容
+        mode: 权限模式(默认0o600,仅所有者可读写)
+
+    安全验证:
+        - ✅ 仅所有者可读写(0o600)
+        - ✅ 防止其他用户访问
+    """
+    # 创建文件
+    with open(file_path, 'w', encoding='utf-8') as f:
+        f.write(content)
+
+    # 设置权限(仅Unix系统)
+    if os.name != 'nt':
+        os.chmod(file_path, mode)
+
+
+def validate_integer_input(value: str, field_name: str) -> int:
+    """
+    验证并转换整数输入(严格模式)
+
+    安全关键函数 - 修复update_state.py弱验证漏洞
+
+    Args:
+        value: 输入值(字符串)
+        field_name: 字段名称(用于错误消息)
+
+    Returns:
+        转换后的整数
+
+    Raises:
+        ValueError: 输入不是有效整数
+
+    示例:
+        >>> validate_integer_input("123", "chapter_num")
+        123
+        >>> validate_integer_input("abc", "level")
+        ValueError: ❌ 错误:level 必须是整数,收到: abc
+    """
+    try:
+        return int(value)
+    except ValueError:
+        print(f"❌ 错误:{field_name} 必须是整数,收到: {value}", file=sys.stderr)
+        raise ValueError(f"Invalid integer input for {field_name}: {value}")
+
+
+# ============================================================================
+# Git 环境检测(优雅降级支持)
+# ============================================================================
+
+# 缓存 Git 可用性检测结果
+_git_available: Optional[bool] = None
+
+
+def is_git_available() -> bool:
+    """
+    检测 Git 是否可用
+
+    Returns:
+        bool: Git 是否可用
+
+    说明:
+        - 检测结果会被缓存,避免重复检测
+        - 用于支持在无 Git 环境下优雅降级
+    """
+    global _git_available
+
+    if _git_available is not None:
+        return _git_available
+
+    import subprocess
+
+    try:
+        result = subprocess.run(
+            ["git", "--version"],
+            capture_output=True,
+            text=True,
+            timeout=5
+        )
+        _git_available = result.returncode == 0
+    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
+        _git_available = False
+
+    return _git_available
+
+
+def is_git_repo(path: Union[str, Path]) -> bool:
+    """
+    检测指定目录是否是 Git 仓库
+
+    Args:
+        path: 目录路径
+
+    Returns:
+        bool: 是否是 Git 仓库
+    """
+    if not is_git_available():
+        return False
+
+    path = Path(path)
+    git_dir = path / ".git"
+    return git_dir.exists() and git_dir.is_dir()
+
+
+def git_graceful_operation(
+    args: list,
+    cwd: Union[str, Path],
+    *,
+    fallback_msg: str = "Git 不可用,跳过版本控制操作"
+) -> tuple:
+    """
+    优雅执行 Git 操作(Git 不可用时静默降级)
+
+    Args:
+        args: Git 命令参数(不含 'git')
+        cwd: 工作目录
+        fallback_msg: 降级时的提示消息
+
+    Returns:
+        (success: bool, output: str, was_skipped: bool)
+        - success: 操作是否成功
+        - output: 输出内容
+        - was_skipped: 是否因 Git 不可用而跳过
+
+    示例:
+        >>> success, output, skipped = git_graceful_operation(
+        ...     ["add", "."], cwd="/path/to/project"
+        ... )
+        >>> if skipped:
+        ...     print("Git not available, using fallback")
+    """
+    if not is_git_available():
+        print(f"⚠️  {fallback_msg}", file=sys.stderr)
+        return False, "", True
+
+    import subprocess
+
+    try:
+        result = subprocess.run(
+            ["git"] + args,
+            cwd=cwd,
+            capture_output=True,
+            text=True,
+            encoding='utf-8',
+            timeout=60
+        )
+        return result.returncode == 0, result.stdout, False
+    except subprocess.TimeoutExpired:
+        print(f"⚠️  Git 操作超时: git {' '.join(args)}", file=sys.stderr)
+        return False, "", False
+    except OSError as e:
+        print(f"⚠️  Git 操作失败: {e}", file=sys.stderr)
+        return False, "", False
+
+
+# ============================================================================
+# 原子化文件写入(防止并发冲突和数据损坏)
+# ============================================================================
+
+
+class AtomicWriteError(Exception):
+    """原子写入失败异常"""
+    pass
+
+
+def atomic_write_json(
+    file_path: Union[str, Path],
+    data: Dict[str, Any],
+    *,
+    use_lock: bool = True,
+    backup: bool = True,
+    indent: int = 2
+) -> None:
+    """
+    原子化写入 JSON 文件,防止并发冲突和数据损坏 (CWE-362, CWE-367)
+
+    安全关键函数 - 修复 state.json 并发写入风险
+
+    实现策略:
+    1. 写入临时文件(同目录,确保同文件系统)
+    2. 可选:使用 filelock 获取排他锁
+    3. 可选:备份原文件
+    4. 原子重命名(os.replace 在 POSIX 上是原子的)
+
+    Args:
+        file_path: 目标文件路径
+        data: 要写入的字典数据
+        use_lock: 是否使用文件锁(需要 filelock 库)
+        backup: 是否在写入前备份原文件
+        indent: JSON 缩进(默认 2)
+
+    Raises:
+        AtomicWriteError: 写入失败时抛出
+
+    示例:
+        >>> atomic_write_json('.webnovel/state.json', {'progress': {'chapter': 10}})
+
+    安全验证:
+        - ✅ 防止写入中断导致的数据损坏(先写临时文件)
+        - ✅ 防止并发写入冲突(filelock)
+        - ✅ 支持回滚(备份机制)
+        - ✅ 跨平台兼容
+    """
+    file_path = Path(file_path)
+    parent_dir = file_path.parent
+    parent_dir.mkdir(parents=True, exist_ok=True)
+
+    # 准备 JSON 内容
+    try:
+        json_content = json.dumps(data, ensure_ascii=False, indent=indent)
+    except (TypeError, ValueError) as e:
+        raise AtomicWriteError(f"JSON 序列化失败: {e}")
+
+    # 锁文件路径
+    lock_path = file_path.with_suffix(file_path.suffix + '.lock')
+    backup_path = file_path.with_suffix(file_path.suffix + '.bak')
+
+    # 创建临时文件(同目录确保同文件系统,os.replace 才能原子操作)
+    fd, temp_path = tempfile.mkstemp(
+        suffix='.tmp',
+        prefix=file_path.stem + '_',
+        dir=parent_dir
+    )
+
+    try:
+        # Step 1: 写入临时文件
+        with os.fdopen(fd, 'w', encoding='utf-8') as f:
+            f.write(json_content)
+            f.flush()
+            os.fsync(f.fileno())  # 确保写入磁盘
+
+        # Step 2: 获取锁(如果可用且启用)
+        lock = None
+        if use_lock and HAS_FILELOCK:
+            lock = FileLock(str(lock_path), timeout=10)
+            lock.acquire()
+
+        try:
+            # Step 3: 备份原文件(如果存在且启用备份)
+            if backup and file_path.exists():
+                try:
+                    import shutil
+                    shutil.copy2(file_path, backup_path)
+                except OSError:
+                    pass  # 备份失败不阻止写入
+
+            # Step 4: 原子重命名
+            os.replace(temp_path, file_path)
+            temp_path = None  # 标记已成功,不需要清理
+
+        finally:
+            if lock is not None:
+                lock.release()
+
+    except Exception as e:
+        raise AtomicWriteError(f"原子写入失败: {e}")
+
+    finally:
+        # 清理:删除临时文件(如果仍存在说明写入失败)
+        if temp_path is not None:
+            try:
+                os.unlink(temp_path)
+            except OSError:
+                pass
+
+
+def read_json_safe(
+    file_path: Union[str, Path],
+    default: Optional[Dict[str, Any]] = None
+) -> Dict[str, Any]:
+    """
+    安全读取 JSON 文件(带默认值和错误处理)
+
+    Args:
+        file_path: 文件路径
+        default: 文件不存在或解析失败时的默认值
+
+    Returns:
+        解析后的字典,或默认值
+
+    示例:
+        >>> state = read_json_safe('.webnovel/state.json', {})
+    """
+    file_path = Path(file_path)
+    if default is None:
+        default = {}
+
+    if not file_path.exists():
+        return default
+
+    try:
+        with open(file_path, 'r', encoding='utf-8') as f:
+            return json.load(f)
+    except (json.JSONDecodeError, OSError) as e:
+        print(f"⚠️ 读取 JSON 失败 ({file_path}): {e}", file=sys.stderr)
+        return default
+
+
+def restore_from_backup(file_path: Union[str, Path]) -> bool:
+    """
+    从备份恢复文件
+
+    Args:
+        file_path: 原文件路径
+
+    Returns:
+        是否成功恢复
+
+    示例:
+        >>> restore_from_backup('.webnovel/state.json')
+        True
+    """
+    file_path = Path(file_path)
+    backup_path = file_path.with_suffix(file_path.suffix + '.bak')
+
+    if not backup_path.exists():
+        print(f"⚠️ 备份文件不存在: {backup_path}", file=sys.stderr)
+        return False
+
+    try:
+        import shutil
+        shutil.copy2(backup_path, file_path)
+        print(f"✅ 已从备份恢复: {file_path}")
+        return True
+    except OSError as e:
+        print(f"❌ 恢复失败: {e}", file=sys.stderr)
+        return False
+
+
+# ============================================================================
+# 单元测试(内置自检)
+# ============================================================================
+
+def _run_self_tests():
+    """运行内置安全测试"""
+    print("🔍 运行安全工具函数自检...")
+
+    # Test 1: sanitize_filename
+    assert sanitize_filename("../../../etc/passwd") == "passwd", "路径遍历测试失败"
+    assert sanitize_filename("C:\\Windows\\System32") == "System32", "Windows路径测试失败"
+    assert sanitize_filename("正常角色名") == "正常角色名", "中文测试失败"
+    assert sanitize_filename("/tmp/../../../../../etc/hosts") == "hosts", "复杂路径遍历测试失败"
+    assert sanitize_filename("test///file...name") == "file_name", "特殊字符测试失败"  # . 会被替换
+    print("  ✅ sanitize_filename: 所有测试通过")
+
+    # Test 2: sanitize_commit_message
+    result = sanitize_commit_message("Test\n--author='Attacker'")
+    assert "\n" not in result, "换行符未移除"
+    assert "--author" not in result, "Git标志未移除"
+    assert "Attacker" in result, "内容被错误移除"
+
+    assert sanitize_commit_message("--amend Chapter 1") == "Chapter 1", "Git标志测试失败"  # --amend被完全移除
+    assert "'" not in sanitize_commit_message("Test'message"), "引号测试失败"
+    assert sanitize_commit_message("-m Test") == "m Test", "单字母标志测试失败"  # -m被移除后是"m Test"
+    print("  ✅ sanitize_commit_message: 所有测试通过")
+
+    # Test 3: validate_integer_input
+    assert validate_integer_input("123", "test") == 123, "整数验证测试失败"
+    try:
+        validate_integer_input("abc", "test")
+        assert False, "应该抛出ValueError"
+    except ValueError:
+        pass
+    print("  ✅ validate_integer_input: 所有测试通过")
+
+    # Test 4: atomic_write_json
+    import tempfile as tf
+    test_dir = Path(tf.mkdtemp())
+    test_file = test_dir / "test_state.json"
+
+    # 写入测试
+    test_data = {"chapter": 10, "中文键": "中文值"}
+    atomic_write_json(test_file, test_data, use_lock=False, backup=False)
+    assert test_file.exists(), "原子写入未创建文件"
+
+    # 读取验证
+    with open(test_file, 'r', encoding='utf-8') as f:
+        loaded = json.load(f)
+    assert loaded == test_data, "原子写入数据不匹配"
+
+    # 备份测试
+    atomic_write_json(test_file, {"updated": True}, use_lock=False, backup=True)
+    backup_file = test_file.with_suffix('.json.bak')
+    assert backup_file.exists(), "备份未创建"
+
+    # 恢复测试
+    restore_from_backup(test_file)
+    with open(test_file, 'r', encoding='utf-8') as f:
+        restored = json.load(f)
+    assert restored == test_data, "恢复数据不匹配"
+
+    # 清理
+    import shutil
+    shutil.rmtree(test_dir)
+    print("  ✅ atomic_write_json: 所有测试通过")
+    if HAS_FILELOCK:
+        print("  ℹ️  filelock 可用,已启用文件锁支持")
+    else:
+        print("  ⚠️  filelock 未安装,文件锁功能不可用")
+
+    print("\n✅ 所有安全工具函数测试通过!")
+
+
+if __name__ == "__main__":
+    # Windows UTF-8 编码修复(必须在打印前执行)
+    if sys.platform == 'win32':
+        import io
+        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+    # 运行自检测试
+    _run_self_tests()

+ 992 - 0
.claude/scripts/status_reporter.py

@@ -0,0 +1,992 @@
+#!/usr/bin/env python3
+"""
+可视化状态报告系统 (Status Reporter)
+
+核心理念:面对 1000 个章节,作者会迷失。需要"宏观俯瞰"能力。
+
+功能:
+1. 角色活跃度分析:哪些角色太久没出场(掉线统计)
+2. 伏笔深度分析:哪些坑挖得太久了(超过 20 万字未收)+ 紧急度排序
+3. 爽点节奏分布:全书高潮点的分布频率(热力图)
+4. 字数分布统计:各卷、各篇的字数分布
+5. 人际关系图谱:好感度/仇恨度趋势
+6. Strand Weave 节奏分析:Quest/Fire/Constellation 三线占比统计
+7. 伏笔紧急度排序:基于三层级系统(核心/支线/装饰)的优先级计算
+
+输出格式:
+  - Markdown 报告(.webnovel/health_report.md)
+  - 包含 Mermaid 图表(角色关系图、爽点热力图)
+
+使用方式:
+  # 生成完整健康报告
+  python status_reporter.py --output .webnovel/health_report.md
+
+  # 仅分析角色活跃度
+  python status_reporter.py --focus characters
+
+  # 仅分析伏笔
+  python status_reporter.py --focus foreshadowing
+
+  # 仅分析爽点节奏
+  python status_reporter.py --focus pacing
+
+  # 分析 Strand Weave 节奏
+  python status_reporter.py --focus strand
+
+报告示例:
+  # 全书健康报告
+
+  ## 📊 基本数据
+
+  - **总章节数**: 450 章
+  - **总字数**: 1,985,432 字
+  - **平均章节字数**: 4,412 字
+  - **创作进度**: 99.3%(目标 200万字)
+
+  ## ⚠️ 角色掉线(3人)
+
+  | 角色 | 最后出场 | 缺席章节 | 状态 |
+  |------|---------|---------|------|
+  | 李雪 | 第 350 章 | 100 章 | 🔴 严重掉线 |
+  | 血煞门主 | 第 300 章 | 150 章 | 🔴 严重掉线 |
+  | 天云宗宗主 | 第 400 章 | 50 章 | 🟡 轻度掉线 |
+
+  ## ⚠️ 伏笔超时(2条)
+
+  | 伏笔内容 | 埋设章节 | 已过章节 | 状态 |
+  |---------|---------|---------|------|
+  | "林家宝库铭文的秘密" | 第 200 章 | 250 章 | 🔴 严重超时 |
+  | "神秘玉佩的来历" | 第 270 章 | 180 章 | 🟡 轻度超时 |
+
+  ## 📈 爽点节奏分布
+
+  ```
+  第 1-100 章   ████████████ 优秀(1200字/爽点)
+  第 101-200章  ██████████ 良好(1500字/爽点)
+  第 201-300章  ████████ 良好(1600字/爽点)
+  第 301-400章  ████ 偏低(2200字/爽点)⚠️
+  第 401-450章  ██████ 良好(1550字/爽点)
+  ```
+
+  ## 💑 人际关系趋势
+
+  ```mermaid
+  graph LR
+    主角 -->|好感度95| 李雪
+    主角 -->|好感度60| 慕容雪
+    主角 -->|仇恨度100| 血煞门
+  ```
+"""
+
+import json
+import os
+import re
+import sys
+from pathlib import Path
+from typing import Dict, List, Any, Tuple
+from datetime import datetime
+from collections import defaultdict
+from project_locator import resolve_project_root
+from chapter_paths import extract_chapter_num_from_filename
+
+# 导入配置
+try:
+    from data_modules.config import get_config, DataModulesConfig
+except ImportError:
+    from scripts.data_modules.config import get_config, DataModulesConfig
+
+def _is_resolved_foreshadowing_status(raw_status: Any) -> bool:
+    """判断伏笔是否已回收(兼容历史字段与同义词)。"""
+    if raw_status is None:
+        return False
+
+    status = str(raw_status).strip()
+    if not status:
+        return False
+
+    status_lower = status.lower()
+    if status in {"已回收", "已完成", "已解决", "完成"}:
+        return True
+    if status_lower in {"resolved", "done", "complete"}:
+        return True
+    if "已回收" in status:
+        return True
+    return False
+
+# Windows 编码兼容性修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+class StatusReporter:
+    """状态报告生成器"""
+
+    def __init__(self, project_root: str):
+        self.project_root = Path(project_root)
+        self.config = get_config(self.project_root)
+        self.state_file = self.project_root / ".webnovel/state.json"
+        self.chapters_dir = self.project_root / "正文"
+
+        self.state = None
+        self.chapters_data = []
+
+        # 可选:集成结构化索引(如果可用,角色统计更准)
+        self.index = None
+        try:
+            from structured_index import StructuredIndex
+            self.index = StructuredIndex(self.project_root)
+        except Exception:
+            self.index = None
+
+    def _extract_stats_field(self, content: str, field_name: str) -> str:
+        """
+        从“本章统计”区块提取字段值,例如:
+        - **主导Strand**: quest
+        """
+        pattern = rf"^\s*-\s*\*\*{re.escape(field_name)}\*\*\s*:\s*(.+?)\s*$"
+        for line in content.splitlines():
+            m = re.match(pattern, line)
+            if m:
+                return m.group(1).strip()
+        return ""
+
+    def load_state(self) -> bool:
+        """加载 state.json"""
+        if not self.state_file.exists():
+            print(f"❌ 状态文件不存在: {self.state_file}")
+            return False
+
+        with open(self.state_file, 'r', encoding='utf-8') as f:
+            self.state = json.load(f)
+
+        return True
+
+    def scan_chapters(self):
+        """扫描所有章节文件"""
+        if not self.chapters_dir.exists():
+            print(f"⚠️  正文目录不存在: {self.chapters_dir}")
+            return
+
+        # 支持两种目录结构:
+        # 1) 正文/第0001章.md
+        # 2) 正文/第1卷/第001章-标题.md
+        chapter_files = sorted(self.chapters_dir.rglob("第*.md"))
+
+        # 角色候选(fallback 用):从 state.json 获取已知角色名 (v5.0 entities_v3 格式)
+        known_character_names: List[str] = []
+        protagonist_name = ""
+        if self.state:
+            protagonist_name = self.state.get("protagonist_state", {}).get("name", "") or ""
+            # v5.0: 从 entities_v3.角色 获取角色名
+            entities_v3 = self.state.get("entities_v3", {})
+            characters_dict = entities_v3.get("角色", {})
+            known_character_names = [
+                c.get("canonical_name", char_id)
+                for char_id, c in characters_dict.items()
+                if c.get("canonical_name")
+            ]
+
+        for chapter_file in chapter_files:
+            chapter_num = extract_chapter_num_from_filename(chapter_file.name)
+            if not chapter_num:
+                continue
+
+            # 读取章节内容
+            with open(chapter_file, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            # 统计字数(去除 Markdown 标记)
+            text = re.sub(r'```[\s\S]*?```', '', content)  # 去除代码块
+            text = re.sub(r'#+ .+', '', text)  # 去除标题
+            text = re.sub(r'---', '', text)  # 去除分隔线
+            word_count = len(text.strip())
+
+            # 主导 Strand / 爽点类型(优先从“本章统计”解析)
+            dominant_strand = (self._extract_stats_field(content, "主导Strand") or "").lower()
+            cool_point_type = self._extract_stats_field(content, "爽点")
+
+            # 角色提取:优先从结构化索引读取(若有),否则 fallback 用“出现即算出场”
+            characters: List[str] = []
+            if self.index is not None:
+                try:
+                    cursor = self.index.conn.execute(
+                        "SELECT characters FROM chapters WHERE chapter_num = ?",
+                        (chapter_num,),
+                    )
+                    row = cursor.fetchone()
+                    if row and row[0]:
+                        try:
+                            stored = json.loads(row[0])
+                            if isinstance(stored, list):
+                                # v4.0: chapters.characters 存 entity_id 列表,输出时尽量还原为 canonical_name
+                                for x in stored:
+                                    entity_id = str(x).strip()
+                                    if not entity_id:
+                                        continue
+                                    name = entity_id
+                                    try:
+                                        ent = self.index.query_entity_by_id(entity_id)
+                                        if ent and ent.get("canonical_name"):
+                                            name = str(ent["canonical_name"]).strip() or entity_id
+                                    except Exception:
+                                        name = entity_id
+                                    characters.append(name)
+                        except json.JSONDecodeError:
+                            characters = []
+                except Exception:
+                    characters = []
+
+            if not characters and (protagonist_name or known_character_names):
+                # 限制候选规模,避免在超大角色库下过慢
+                candidates = []
+                if protagonist_name:
+                    candidates.append(protagonist_name)
+                candidates.extend(known_character_names[:self.config.character_candidates_limit])
+
+                seen = set()
+                for name in candidates:
+                    if not name or name in seen:
+                        continue
+                    if name in content:
+                        characters.append(name)
+                        seen.add(name)
+
+            self.chapters_data.append({
+                "chapter": chapter_num,
+                "file": chapter_file,
+                "word_count": word_count,
+                "characters": characters,
+                "dominant": dominant_strand,
+                "cool_point": cool_point_type,
+            })
+
+    def analyze_characters(self) -> Dict:
+        """分析角色活跃度 (v5.0 entities_v3 格式)"""
+        if not self.state:
+            return {}
+
+        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
+        # v5.0: 从 entities_v3.角色 获取角色
+        entities_v3 = self.state.get("entities_v3", {})
+        characters_dict = entities_v3.get("角色", {})
+
+        # 统计每个角色的最后出场章节
+        character_activity = {}
+
+        for char_id, char in characters_dict.items():
+            char_name = char.get("canonical_name", char_id)
+            if not char_name:
+                continue
+
+            # 查找最后出场章节
+            last_appearance = 0
+
+            for ch_data in self.chapters_data:
+                if char_name in ch_data.get("characters", []):
+                    last_appearance = max(last_appearance, ch_data["chapter"])
+
+            absence = current_chapter - last_appearance
+
+            character_activity[char_name] = {
+                "last_appearance": last_appearance,
+                "absence": absence,
+                "status": self._get_absence_status(absence)
+            }
+
+        return character_activity
+
+    def _get_absence_status(self, absence: int) -> str:
+        """判断掉线状态"""
+        if absence == 0:
+            return "✅ 活跃"
+        elif absence < self.config.character_absence_warning:
+            return "🟢 正常"
+        elif absence < self.config.character_absence_critical:
+            return "🟡 轻度掉线"
+        else:
+            return "🔴 严重掉线"
+
+    def analyze_foreshadowing(self) -> List[Dict]:
+        """分析伏笔深度"""
+        if not self.state:
+            return []
+
+        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
+        plot_threads = self.state.get("plot_threads", {})
+        foreshadowing = plot_threads.get("foreshadowing", [])
+
+        overdue = []
+
+        for item in foreshadowing:
+            status = item.get("status")
+            if _is_resolved_foreshadowing_status(status):
+                continue
+
+            # 假设每个伏笔记录了"added_chapter"(埋设章节)
+            # 如果没有,使用 added_at 日期估算(粗略)
+            # 这里简化:假设第 1 章开始,平均每天写 1 章
+
+            # 简化:假设伏笔按添加顺序,第 N 个伏笔大约在第 N*10 章埋下
+            # 实际项目应该在伏笔记录中加入 "埋设章节号" 字段
+
+            # 这里使用 content 中的关键词匹配(极度简化)
+            content = item.get("content", "")
+
+            # 假设伏笔平均埋设时间 = 当前章节的一半(极度粗糙估算)
+            estimated_chapter = current_chapter // 2
+            elapsed = current_chapter - estimated_chapter
+
+            overdue.append({
+                "content": content,
+                "estimated_chapter": estimated_chapter,
+                "elapsed": elapsed,
+                "status": self._get_foreshadowing_status(elapsed)
+            })
+
+        return overdue
+
+    def _get_foreshadowing_status(self, elapsed: int) -> str:
+        """判断伏笔超时状态"""
+        if elapsed < self.config.foreshadowing_urgency_pending_medium:
+            return "🟢 正常"
+        elif elapsed < self.config.foreshadowing_urgency_pending_high + 50:
+            return "🟡 轻度超时"
+        else:
+            return "🔴 严重超时"
+
+    def analyze_foreshadowing_urgency(self) -> List[Dict]:
+        """
+        分析伏笔紧急度(基于三层级系统)
+
+        三层级权重:
+        - 核心(Core): 权重 3.0 - 必须回收,否则剧情崩塌
+        - 支线(Sub): 权重 2.0 - 应该回收,否则显得作者健忘
+        - 装饰(Decor): 权重 1.0 - 可回收可不回收,仅增加真实感
+
+        紧急度计算公式:
+        urgency = (已过章节 / 目标回收章节) × 层级权重
+        """
+        if not self.state:
+            return []
+
+        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
+        plot_threads = self.state.get("plot_threads", {})
+        foreshadowing = plot_threads.get("foreshadowing", [])
+
+        # 层级权重映射
+        tier_weights = {
+            "核心": self.config.foreshadowing_tier_weight_core,
+            "core": self.config.foreshadowing_tier_weight_core,
+            "支线": self.config.foreshadowing_tier_weight_sub,
+            "sub": self.config.foreshadowing_tier_weight_sub,
+            "装饰": self.config.foreshadowing_tier_weight_decor,
+            "decor": self.config.foreshadowing_tier_weight_decor
+        }
+
+        urgency_list = []
+
+        for item in foreshadowing:
+            if _is_resolved_foreshadowing_status(item.get("status")):
+                continue
+
+            content = item.get("content", "")
+            tier = item.get("tier", "支线")  # 默认支线
+            planted_chapter = item.get("planted_chapter", 1)
+            target_chapter = item.get("target_chapter", planted_chapter + 100)
+
+            weight = tier_weights.get(tier.lower(), self.config.foreshadowing_tier_weight_sub)
+            elapsed = current_chapter - planted_chapter
+            remaining = target_chapter - current_chapter
+
+            # 紧急度计算
+            if target_chapter > planted_chapter:
+                urgency = (elapsed / (target_chapter - planted_chapter)) * weight
+            else:
+                urgency = weight * 2  # 已超期
+
+            urgency_list.append({
+                "content": content,
+                "tier": tier,
+                "weight": weight,
+                "planted_chapter": planted_chapter,
+                "target_chapter": target_chapter,
+                "elapsed": elapsed,
+                "remaining": remaining,
+                "urgency": round(urgency, 2),
+                "status": self._get_urgency_status(urgency, remaining)
+            })
+
+        # 按紧急度排序(降序)
+        return sorted(urgency_list, key=lambda x: x["urgency"], reverse=True)
+
+    def _get_urgency_status(self, urgency: float, remaining: int) -> str:
+        """判断紧急度状态"""
+        if remaining < 0:
+            return "🔴 已超期"
+        elif urgency >= self.config.foreshadowing_tier_weight_sub:
+            return "🔴 紧急"
+        elif urgency >= 1.0:
+            return "🟡 警告"
+        else:
+            return "🟢 正常"
+
+    def analyze_strand_weave(self) -> Dict:
+        """
+        分析 Strand Weave 节奏分布
+
+        三线定义:
+        - Quest(主线): 战斗、任务、升级 - 目标 55-65%
+        - Fire(感情): 感情线、人际互动 - 目标 20-30%
+        - Constellation(世界观): 世界观展开、势力背景 - 目标 10-20%
+
+        检查规则:
+        - Quest 线连续不超过 5 章
+        - Fire 线缺失不超过 10 章
+        - Constellation 线缺失不超过 15 章
+        """
+        if not self.state:
+            return {}
+
+        strand_tracker = self.state.get("strand_tracker", {})
+        history = strand_tracker.get("history", [])
+
+        if not history:
+            return {
+                "has_data": False,
+                "message": "暂无 Strand Weave 数据"
+            }
+
+        # 统计各线占比
+        quest_count = 0
+        fire_count = 0
+        constellation_count = 0
+        total = len(history)
+
+        for entry in history:
+            strand = (entry.get("strand") or entry.get("dominant") or "").lower()
+            if strand in ["quest", "主线", "战斗", "任务"]:
+                quest_count += 1
+            elif strand in ["fire", "感情", "感情线", "互动"]:
+                fire_count += 1
+            elif strand in ["constellation", "世界观", "背景", "势力"]:
+                constellation_count += 1
+
+        # 计算占比
+        quest_ratio = (quest_count / total * 100) if total > 0 else 0
+        fire_ratio = (fire_count / total * 100) if total > 0 else 0
+        constellation_ratio = (constellation_count / total * 100) if total > 0 else 0
+
+        # 检查违规
+        violations = []
+
+        # 检查 Quest 连续超过 5 章
+        quest_streak = 0
+        max_quest_streak = 0
+        for entry in history:
+            strand = (entry.get("strand") or entry.get("dominant") or "").lower()
+            if strand in ["quest", "主线", "战斗", "任务"]:
+                quest_streak += 1
+                max_quest_streak = max(max_quest_streak, quest_streak)
+            else:
+                quest_streak = 0
+
+        if max_quest_streak > self.config.strand_quest_max_consecutive:
+            violations.append(f"Quest 线连续 {max_quest_streak} 章(超过 {self.config.strand_quest_max_consecutive} 章限制)")
+
+        # 检查 Fire 缺失超过 10 章
+        fire_gap = 0
+        max_fire_gap = 0
+        for entry in history:
+            strand = (entry.get("strand") or entry.get("dominant") or "").lower()
+            if strand in ["fire", "感情", "感情线", "互动"]:
+                max_fire_gap = max(max_fire_gap, fire_gap)
+                fire_gap = 0
+            else:
+                fire_gap += 1
+        max_fire_gap = max(max_fire_gap, fire_gap)
+
+        if max_fire_gap > self.config.strand_fire_max_gap:
+            violations.append(f"Fire 线缺失 {max_fire_gap} 章(超过 {self.config.strand_fire_max_gap} 章限制)")
+
+        # 检查 Constellation 缺失超过 15 章
+        const_gap = 0
+        max_const_gap = 0
+        for entry in history:
+            strand = (entry.get("strand") or entry.get("dominant") or "").lower()
+            if strand in ["constellation", "世界观", "背景", "势力"]:
+                max_const_gap = max(max_const_gap, const_gap)
+                const_gap = 0
+            else:
+                const_gap += 1
+        max_const_gap = max(max_const_gap, const_gap)
+
+        if max_const_gap > self.config.strand_constellation_max_gap:
+            violations.append(f"Constellation 线缺失 {max_const_gap} 章(超过 {self.config.strand_constellation_max_gap} 章限制)")
+
+        # 检查占比是否在合理范围
+        cfg = self.config
+        if quest_ratio < cfg.strand_quest_ratio_min:
+            violations.append(f"Quest 占比 {quest_ratio:.1f}% 偏低(目标 {cfg.strand_quest_ratio_min}-{cfg.strand_quest_ratio_max}%)")
+        elif quest_ratio > cfg.strand_quest_ratio_max:
+            violations.append(f"Quest 占比 {quest_ratio:.1f}% 偏高(目标 {cfg.strand_quest_ratio_min}-{cfg.strand_quest_ratio_max}%)")
+
+        if fire_ratio < cfg.strand_fire_ratio_min:
+            violations.append(f"Fire 占比 {fire_ratio:.1f}% 偏低(目标 {cfg.strand_fire_ratio_min}-{cfg.strand_fire_ratio_max}%)")
+        elif fire_ratio > cfg.strand_fire_ratio_max:
+            violations.append(f"Fire 占比 {fire_ratio:.1f}% 偏高(目标 {cfg.strand_fire_ratio_min}-{cfg.strand_fire_ratio_max}%)")
+
+        if constellation_ratio < cfg.strand_constellation_ratio_min:
+            violations.append(f"Constellation 占比 {constellation_ratio:.1f}% 偏低(目标 {cfg.strand_constellation_ratio_min}-{cfg.strand_constellation_ratio_max}%)")
+        elif constellation_ratio > cfg.strand_constellation_ratio_max:
+            violations.append(f"Constellation 占比 {constellation_ratio:.1f}% 偏高(目标 {cfg.strand_constellation_ratio_min}-{cfg.strand_constellation_ratio_max}%)")
+
+        return {
+            "has_data": True,
+            "total_chapters": total,
+            "quest": {"count": quest_count, "ratio": quest_ratio},
+            "fire": {"count": fire_count, "ratio": fire_ratio},
+            "constellation": {"count": constellation_count, "ratio": constellation_ratio},
+            "violations": violations,
+            "max_quest_streak": max_quest_streak,
+            "max_fire_gap": max_fire_gap,
+            "max_const_gap": max_const_gap,
+            "health": "✅ 健康" if not violations else f"⚠️ {len(violations)} 个问题"
+        }
+
+    def analyze_pacing(self) -> List[Dict]:
+        """分析爽点节奏分布(每 N 章为一段)"""
+        segment_size = self.config.pacing_segment_size
+        segments = []
+
+        for i in range(0, len(self.chapters_data), segment_size):
+            segment_chapters = self.chapters_data[i:i+segment_size]
+
+            if not segment_chapters:
+                continue
+
+            start_ch = segment_chapters[0]["chapter"]
+            end_ch = segment_chapters[-1]["chapter"]
+            total_words = sum(ch["word_count"] for ch in segment_chapters)
+
+            # 假设爽点数量 = 章节数(简化:每章至少 1 个爽点)
+            # 实际项目应该在审查报告中记录爽点数量
+            assumed_cool_points = len(segment_chapters)
+
+            words_per_point = total_words / assumed_cool_points if assumed_cool_points > 0 else 0
+
+            segments.append({
+                "start": start_ch,
+                "end": end_ch,
+                "total_words": total_words,
+                "cool_points": assumed_cool_points,
+                "words_per_point": words_per_point,
+                "rating": self._get_pacing_rating(words_per_point)
+            })
+
+        return segments
+
+    def _get_pacing_rating(self, words_per_point: float) -> str:
+        """判断节奏评级"""
+        if words_per_point < self.config.pacing_words_per_point_excellent:
+            return "优秀"
+        elif words_per_point < self.config.pacing_words_per_point_good:
+            return "良好"
+        elif words_per_point < self.config.pacing_words_per_point_acceptable:
+            return "及格"
+        else:
+            return "偏低⚠️"
+
+    def generate_relationship_graph(self) -> str:
+        """生成人际关系 Mermaid 图"""
+        if not self.state:
+            return ""
+
+        relationships = self.state.get("relationships", {})
+        protagonist_name = self.state.get("protagonist_state", {}).get("name", "主角")
+
+        lines = ["```mermaid", "graph LR"]
+
+        # 支持两种格式:
+        # 格式1(新): {"allies": [...], "enemies": [...]}
+        # 格式2(旧): {"角色名": {"affection": X, "hatred": Y}}
+
+        allies = relationships.get("allies", [])
+        enemies = relationships.get("enemies", [])
+
+        if allies or enemies:
+            # 新格式
+            for ally in allies:
+                if isinstance(ally, dict):
+                    name = ally.get("name", "未知")
+                    relation = ally.get("relation", "友好")
+                    lines.append(f"    {protagonist_name} -->|{relation}| {name}")
+
+            for enemy in enemies:
+                if isinstance(enemy, dict):
+                    name = enemy.get("name", "未知")
+                    relation = enemy.get("relation", "敌对")
+                    lines.append(f"    {protagonist_name} -.->|{relation}| {name}")
+        else:
+            # 旧格式兼容
+            for char_name, rel_data in relationships.items():
+                if isinstance(rel_data, dict):
+                    affection = rel_data.get("affection", 0)
+                    hatred = rel_data.get("hatred", 0)
+
+                    if affection > 0:
+                        lines.append(f"    {protagonist_name} -->|好感度{affection}| {char_name}")
+
+                    if hatred > 0:
+                        lines.append(f"    {protagonist_name} -.->|仇恨度{hatred}| {char_name}")
+
+        lines.append("```")
+
+        return "\n".join(lines)
+
+    def generate_report(self, focus: str = "all") -> str:
+        """生成健康报告(Markdown 格式)"""
+
+        report_lines = [
+            "# 全书健康报告",
+            "",
+            f"> **生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
+            "",
+            "---",
+            ""
+        ]
+
+        # 基本数据
+        if focus in ["all", "basic"]:
+            report_lines.extend(self._generate_basic_stats())
+
+        # 角色活跃度
+        if focus in ["all", "characters"]:
+            report_lines.extend(self._generate_character_section())
+
+        # 伏笔深度
+        if focus in ["all", "foreshadowing"]:
+            report_lines.extend(self._generate_foreshadowing_section())
+
+        # 伏笔紧急度(新增)
+        if focus in ["all", "foreshadowing", "urgency"]:
+            report_lines.extend(self._generate_urgency_section())
+
+        # 爽点节奏
+        if focus in ["all", "pacing"]:
+            report_lines.extend(self._generate_pacing_section())
+
+        # Strand Weave 节奏(新增)
+        if focus in ["all", "strand", "pacing"]:
+            report_lines.extend(self._generate_strand_section())
+
+        # 人际关系
+        if focus in ["all", "relationships"]:
+            report_lines.extend(self._generate_relationship_section())
+
+        return "\n".join(report_lines)
+
+    def _generate_basic_stats(self) -> List[str]:
+        """生成基本统计"""
+        if not self.state:
+            return []
+
+        progress = self.state.get("progress", {})
+        current_chapter = progress.get("current_chapter", 0)
+        total_words = progress.get("total_words", 0)
+        target_words = self.state.get("project_info", {}).get("target_words", 2000000)
+
+        avg_words = total_words / current_chapter if current_chapter > 0 else 0
+        completion = (total_words / target_words * 100) if target_words > 0 else 0
+
+        return [
+            "## 📊 基本数据",
+            "",
+            f"- **总章节数**: {current_chapter} 章",
+            f"- **总字数**: {total_words:,} 字",
+            f"- **平均章节字数**: {avg_words:,.0f} 字",
+            f"- **创作进度**: {completion:.1f}%(目标 {target_words:,} 字)",
+            "",
+            "---",
+            ""
+        ]
+
+    def _generate_character_section(self) -> List[str]:
+        """生成角色分析章节"""
+        activity = self.analyze_characters()
+
+        if not activity:
+            return []
+
+        # 筛选掉线角色
+        dropped = {name: data for name, data in activity.items()
+                  if "掉线" in data["status"]}
+
+        lines = [
+            f"## ⚠️ 角色掉线({len(dropped)}人)",
+            ""
+        ]
+
+        if dropped:
+            lines.extend([
+                "| 角色 | 最后出场 | 缺席章节 | 状态 |",
+                "|------|---------|---------|------|"
+            ])
+
+            for char_name, data in sorted(dropped.items(),
+                                         key=lambda x: x[1]["absence"],
+                                         reverse=True):
+                lines.append(
+                    f"| {char_name} | 第 {data['last_appearance']} 章 | "
+                    f"{data['absence']} 章 | {data['status']} |"
+                )
+        else:
+            lines.append("✅ 所有角色活跃度正常")
+
+        lines.extend(["", "---", ""])
+
+        return lines
+
+    def _generate_foreshadowing_section(self) -> List[str]:
+        """生成伏笔分析章节"""
+        overdue = self.analyze_foreshadowing()
+
+        # 筛选超时伏笔
+        overdue_items = [item for item in overdue if "超时" in item["status"]]
+
+        lines = [
+            f"## ⚠️ 伏笔超时({len(overdue_items)}条)",
+            ""
+        ]
+
+        if overdue_items:
+            lines.extend([
+                "| 伏笔内容 | 估计埋设 | 已过章节 | 状态 |",
+                "|---------|---------|---------|------|"
+            ])
+
+            for item in sorted(overdue_items, key=lambda x: x["elapsed"], reverse=True):
+                lines.append(
+                    f"| {item['content'][:30]}... | 第 {item['estimated_chapter']} 章 | "
+                    f"{item['elapsed']} 章 | {item['status']} |"
+                )
+        else:
+            lines.append("✅ 所有伏笔进度正常")
+
+        lines.extend(["", "---", ""])
+
+        return lines
+
+    def _generate_urgency_section(self) -> List[str]:
+        """生成伏笔紧急度章节(基于三层级系统)"""
+        urgency_list = self.analyze_foreshadowing_urgency()
+
+        # 筛选紧急伏笔
+        urgent_items = [item for item in urgency_list if item["urgency"] >= 1.0]
+
+        lines = [
+            f"## 🚨 伏笔紧急度排序({len(urgent_items)}条需关注)",
+            "",
+            "> 基于三层级系统:核心(×3) / 支线(×2) / 装饰(×1)",
+            "> 紧急度 = (已过章节 / 目标回收章节) × 层级权重",
+            ""
+        ]
+
+        if urgency_list:
+            lines.extend([
+                "| 伏笔内容 | 层级 | 埋设 | 目标 | 紧急度 | 状态 |",
+                "|---------|------|------|------|--------|------|"
+            ])
+
+            for item in urgency_list[:10]:  # 只显示前10条
+                lines.append(
+                    f"| {item['content'][:20]}... | {item['tier']} | "
+                    f"第{item['planted_chapter']}章 | 第{item['target_chapter']}章 | "
+                    f"{item['urgency']:.2f} | {item['status']} |"
+                )
+        else:
+            lines.append("✅ 暂无伏笔数据")
+
+        lines.extend(["", "---", ""])
+
+        return lines
+
+    def _generate_strand_section(self) -> List[str]:
+        """生成 Strand Weave 节奏章节"""
+        strand_data = self.analyze_strand_weave()
+
+        lines = [
+            "## 🎭 Strand Weave 节奏分析",
+            ""
+        ]
+
+        if not strand_data.get("has_data"):
+            lines.append(f"⚠️ {strand_data.get('message', '暂无数据')}")
+            lines.extend(["", "---", ""])
+            return lines
+
+        # 占比统计
+        cfg = self.config
+        lines.extend([
+            "### 三线占比",
+            "",
+            "| Strand | 章节数 | 占比 | 目标范围 | 状态 |",
+            "|--------|--------|------|----------|------|"
+        ])
+
+        q = strand_data["quest"]
+        q_status = "✅" if cfg.strand_quest_ratio_min <= q["ratio"] <= cfg.strand_quest_ratio_max else "⚠️"
+        lines.append(f"| Quest(主线) | {q['count']} | {q['ratio']:.1f}% | {cfg.strand_quest_ratio_min}-{cfg.strand_quest_ratio_max}% | {q_status} |")
+
+        f = strand_data["fire"]
+        f_status = "✅" if cfg.strand_fire_ratio_min <= f["ratio"] <= cfg.strand_fire_ratio_max else "⚠️"
+        lines.append(f"| Fire(感情) | {f['count']} | {f['ratio']:.1f}% | {cfg.strand_fire_ratio_min}-{cfg.strand_fire_ratio_max}% | {f_status} |")
+
+        c = strand_data["constellation"]
+        c_status = "✅" if cfg.strand_constellation_ratio_min <= c["ratio"] <= cfg.strand_constellation_ratio_max else "⚠️"
+        lines.append(f"| Constellation(世界观) | {c['count']} | {c['ratio']:.1f}% | {cfg.strand_constellation_ratio_min}-{cfg.strand_constellation_ratio_max}% | {c_status} |")
+
+        lines.append("")
+
+        # 连续性检查
+        lines.extend([
+            "### 连续性检查",
+            "",
+            f"- Quest 最大连续: {strand_data['max_quest_streak']} 章(限制 ≤5)",
+            f"- Fire 最大缺失: {strand_data['max_fire_gap']} 章(限制 ≤10)",
+            f"- Constellation 最大缺失: {strand_data['max_const_gap']} 章(限制 ≤15)",
+            ""
+        ])
+
+        # 违规清单
+        if strand_data["violations"]:
+            lines.extend([
+                "### ⚠️ 违规清单",
+                ""
+            ])
+            for v in strand_data["violations"]:
+                lines.append(f"- {v}")
+        else:
+            lines.append("### ✅ 无违规")
+
+        lines.extend(["", f"**综合健康度**: {strand_data['health']}", "", "---", ""])
+
+        return lines
+
+    def _generate_pacing_section(self) -> List[str]:
+        """生成节奏分析章节"""
+        segments = self.analyze_pacing()
+
+        lines = [
+            "## 📈 爽点节奏分布",
+            "",
+            "```"
+        ]
+
+        for seg in segments:
+            bar_length = int(12 - (seg["words_per_point"] / 2000 * 12))
+            bar_length = max(1, min(12, bar_length))
+
+            bar = "█" * bar_length
+
+            lines.append(
+                f"第 {seg['start']}-{seg['end']}章   {bar} "
+                f"{seg['rating']}({seg['words_per_point']:.0f}字/爽点)"
+            )
+
+        lines.extend(["```", "", "---", ""])
+
+        return lines
+
+    def _generate_relationship_section(self) -> List[str]:
+        """生成人际关系章节"""
+        graph = self.generate_relationship_graph()
+
+        lines = [
+            "## 💑 人际关系趋势",
+            "",
+            graph,
+            "",
+            "---",
+            ""
+        ]
+
+        return lines
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description="可视化状态报告生成器",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 生成完整健康报告
+  python status_reporter.py --output .webnovel/health_report.md
+
+  # 仅分析角色活跃度
+  python status_reporter.py --focus characters
+
+  # 仅分析伏笔
+  python status_reporter.py --focus foreshadowing
+
+  # 仅分析爽点节奏
+  python status_reporter.py --focus pacing
+        """
+    )
+
+    parser.add_argument('--output', default='.webnovel/health_report.md',
+                       help='输出文件路径')
+    parser.add_argument('--focus', choices=['all', 'basic', 'characters',
+                                            'foreshadowing', 'urgency', 'pacing',
+                                            'strand', 'relationships'],
+                       default='all', help='分析焦点(新增 urgency, strand)')
+    parser.add_argument('--project-root', default='.', help='项目根目录')
+
+    args = parser.parse_args()
+
+    # 解析项目根目录(支持从仓库根目录运行)
+    project_root = args.project_root
+    if project_root == '.' and not (Path('.') / '.webnovel' / 'state.json').exists():
+        try:
+            project_root = str(resolve_project_root())
+        except FileNotFoundError:
+            project_root = args.project_root
+
+    # 创建报告生成器
+    reporter = StatusReporter(project_root)
+
+    # 加载状态
+    if not reporter.load_state():
+        sys.exit(1)
+
+    print("📖 正在扫描章节文件...")
+    reporter.scan_chapters()
+
+    print(f"✅ 已扫描 {len(reporter.chapters_data)} 个章节")
+
+    print("\n📊 正在分析...")
+
+    # 生成报告
+    report = reporter.generate_report(args.focus)
+
+    # 保存报告
+    output_file = Path(args.output)
+    if args.output == '.webnovel/health_report.md' and project_root != '.':
+        output_file = Path(project_root) / '.webnovel' / 'health_report.md'
+    output_file.parent.mkdir(parents=True, exist_ok=True)
+
+    with open(output_file, 'w', encoding='utf-8') as f:
+        f.write(report)
+
+    print(f"\n✅ 健康报告已生成: {output_file}")
+
+    # 预览报告(前 30 行)
+    print("\n" + "="*60)
+    print("📄 报告预览:\n")
+    print("\n".join(report.split("\n")[:30]))
+    print("\n...")
+    print("="*60)
+
+if __name__ == "__main__":
+    main()

+ 520 - 0
.claude/scripts/stress_test_500chapters.py

@@ -0,0 +1,520 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+500章写作沙盘模拟 - 数据链稳定性压力测试
+
+测试目标:
+1. state.json 增长曲线(文件大小随章节变化)
+2. entities_v3 实体数量增长
+3. alias_index 别名索引膨胀
+4. 伏笔追踪(埋设/回收比例)
+5. 原子写入性能
+6. index.db 查询性能
+
+模拟参数(基于典型网文):
+- 500章,每章约3500字
+- 平均每章新增 0.8 个角色(前100章密集,后期稀疏)
+- 平均每章新增 0.3 个地点
+- 平均每章埋设 0.5 个伏笔,回收 0.3 个
+- 主角每 10 章升级一次境界
+- 每 5 章更新一次关系
+"""
+
+import json
+import os
+import sys
+import time
+import random
+import shutil
+import tempfile
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, Any, List
+
+# 添加脚本目录到路径
+script_dir = Path(__file__).resolve().parent
+sys.path.insert(0, str(script_dir))
+
+from security_utils import atomic_write_json, read_json_safe
+
+# Windows 编码修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+
+# ============================================================================
+# 模拟配置
+# ============================================================================
+
+CONFIG = {
+    "total_chapters": 500,
+    "words_per_chapter": 3500,
+
+    # 实体生成概率(随章节递减)
+    "new_character_base_rate": 0.8,  # 前50章
+    "new_character_decay": 0.95,      # 每50章衰减
+    "new_location_rate": 0.3,
+    "new_item_rate": 0.2,
+    "new_faction_rate": 0.1,
+    "new_technique_rate": 0.15,
+
+    # 伏笔
+    "foreshadow_plant_rate": 0.5,
+    "foreshadow_resolve_rate": 0.3,
+    "foreshadow_tiers": ["核心", "支线", "装饰"],
+    "foreshadow_tier_weights": [0.1, 0.3, 0.6],
+
+    # 主角升级
+    "protagonist_upgrade_interval": 10,
+    "realms": ["练气", "筑基", "金丹", "元婴", "化神", "炼虚", "合体", "大乘", "渡劫"],
+    "layers_per_realm": 9,
+
+    # 关系更新
+    "relationship_update_interval": 5,
+    "relationship_types": ["ally", "enemy", "romance", "mentor", "rival", "family"],
+
+    # 别名生成
+    "alias_per_character": 2.5,  # 平均每个角色的别名数
+}
+
+# 随机名字池
+SURNAME_POOL = ["林", "陈", "王", "李", "张", "刘", "赵", "黄", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "罗", "郑"]
+NAME_POOL = ["天", "云", "风", "雷", "火", "水", "月", "星", "龙", "凤", "虎", "鹤", "剑", "刀", "枪", "棍", "拳", "掌", "指", "心"]
+LOCATION_PREFIX = ["天", "云", "龙", "凤", "青", "白", "黑", "红", "金", "玉"]
+LOCATION_SUFFIX = ["山", "谷", "城", "峰", "洞", "海", "林", "湖", "殿", "宗"]
+
+
+class SimulationMetrics:
+    """模拟指标收集器"""
+
+    def __init__(self):
+        self.checkpoints: List[Dict] = []
+        self.write_times: List[float] = []
+        self.errors: List[str] = []
+
+    def record_checkpoint(self, chapter: int, state: Dict, state_file: Path):
+        """记录检查点"""
+        file_size = state_file.stat().st_size if state_file.exists() else 0
+
+        entities_v3 = state.get("entities_v3", {})
+        entity_counts = {
+            etype: len(entities)
+            for etype, entities in entities_v3.items()
+        }
+        total_entities = sum(entity_counts.values())
+
+        alias_count = len(state.get("alias_index", {}))
+
+        foreshadowing = state.get("foreshadowing", [])
+        active_foreshadow = len([f for f in foreshadowing if f.get("status") == "未回收"])
+        resolved_foreshadow = len([f for f in foreshadowing if f.get("status") == "已回收"])
+
+        relationships = state.get("relationships", [])
+        if isinstance(relationships, dict):
+            relationships = list(relationships.values())
+
+        self.checkpoints.append({
+            "chapter": chapter,
+            "file_size_kb": file_size / 1024,
+            "total_entities": total_entities,
+            "entity_counts": entity_counts,
+            "alias_count": alias_count,
+            "active_foreshadow": active_foreshadow,
+            "resolved_foreshadow": resolved_foreshadow,
+            "relationship_count": len(relationships) if isinstance(relationships, list) else 0,
+            "avg_write_time_ms": sum(self.write_times[-10:]) / max(len(self.write_times[-10:]), 1) * 1000,
+        })
+
+    def record_write_time(self, duration: float):
+        self.write_times.append(duration)
+
+    def record_error(self, error: str):
+        self.errors.append(error)
+
+    def generate_report(self) -> str:
+        """生成测试报告"""
+        if not self.checkpoints:
+            return "No data collected"
+
+        final = self.checkpoints[-1]
+        first = self.checkpoints[0]
+
+        lines = [
+            "=" * 60,
+            "📊 500章沙盘模拟测试报告",
+            "=" * 60,
+            "",
+            "## 基础指标",
+            f"- 总章节数: {final['chapter']}",
+            f"- 总字数: {final['chapter'] * CONFIG['words_per_chapter']:,}",
+            "",
+            "## state.json 增长",
+            f"- 初始大小: {first['file_size_kb']:.2f} KB",
+            f"- 最终大小: {final['file_size_kb']:.2f} KB",
+            f"- 增长倍数: {final['file_size_kb'] / max(first['file_size_kb'], 0.1):.1f}x",
+            "",
+            "## 实体统计",
+            f"- 总实体数: {final['total_entities']}",
+        ]
+
+        for etype, count in final['entity_counts'].items():
+            lines.append(f"  - {etype}: {count}")
+
+        lines.extend([
+            f"- 别名索引条目: {final['alias_count']}",
+            "",
+            "## 伏笔统计",
+            f"- 活跃伏笔: {final['active_foreshadow']}",
+            f"- 已回收伏笔: {final['resolved_foreshadow']}",
+            f"- 回收率: {final['resolved_foreshadow'] / max(final['active_foreshadow'] + final['resolved_foreshadow'], 1) * 100:.1f}%",
+            "",
+            "## 性能指标",
+            f"- 平均写入时间: {sum(self.write_times) / max(len(self.write_times), 1) * 1000:.2f} ms",
+            f"- 最大写入时间: {max(self.write_times) * 1000:.2f} ms" if self.write_times else "N/A",
+            f"- 最小写入时间: {min(self.write_times) * 1000:.2f} ms" if self.write_times else "N/A",
+            "",
+            "## 错误统计",
+            f"- 错误数: {len(self.errors)}",
+        ])
+
+        if self.errors:
+            lines.append("- 错误详情:")
+            for err in self.errors[:5]:
+                lines.append(f"  - {err}")
+
+        # 增长曲线(每100章采样)
+        lines.extend([
+            "",
+            "## 增长曲线(每100章)",
+            "| 章节 | 文件大小(KB) | 实体数 | 别名数 | 活跃伏笔 | 写入时间(ms) |",
+            "|------|-------------|-------|-------|---------|-------------|",
+        ])
+
+        for cp in self.checkpoints:
+            if cp['chapter'] % 100 == 0 or cp['chapter'] == final['chapter']:
+                lines.append(
+                    f"| {cp['chapter']} | {cp['file_size_kb']:.1f} | "
+                    f"{cp['total_entities']} | {cp['alias_count']} | "
+                    f"{cp['active_foreshadow']} | {cp['avg_write_time_ms']:.1f} |"
+                )
+
+        # 稳定性评估
+        lines.extend([
+            "",
+            "## 稳定性评估",
+        ])
+
+        # 检查文件大小是否在合理范围
+        if final['file_size_kb'] < 500:
+            lines.append("✅ 文件大小合理 (< 500KB)")
+        elif final['file_size_kb'] < 1024:
+            lines.append("⚠️ 文件大小偏大 (500KB-1MB),建议启用归档")
+        else:
+            lines.append("❌ 文件过大 (> 1MB),需要优化")
+
+        # 检查写入性能
+        avg_write = sum(self.write_times) / max(len(self.write_times), 1) * 1000
+        if avg_write < 50:
+            lines.append("✅ 写入性能良好 (< 50ms)")
+        elif avg_write < 200:
+            lines.append("⚠️ 写入性能一般 (50-200ms)")
+        else:
+            lines.append("❌ 写入性能差 (> 200ms)")
+
+        # 检查错误率
+        if not self.errors:
+            lines.append("✅ 无错误")
+        else:
+            lines.append(f"❌ 有 {len(self.errors)} 个错误")
+
+        lines.append("")
+        lines.append("=" * 60)
+
+        return "\n".join(lines)
+
+
+class ChapterSimulator:
+    """章节模拟器"""
+
+    def __init__(self, project_root: Path):
+        self.project_root = project_root
+        self.state_file = project_root / ".webnovel" / "state.json"
+        self.metrics = SimulationMetrics()
+        self.generated_names = set()
+        self.entity_id_counter = 0
+
+    def _generate_id(self, prefix: str) -> str:
+        self.entity_id_counter += 1
+        return f"{prefix}_{self.entity_id_counter:05d}"
+
+    def _generate_character_name(self) -> str:
+        for _ in range(100):
+            name = random.choice(SURNAME_POOL) + random.choice(NAME_POOL) + random.choice(NAME_POOL)
+            if name not in self.generated_names:
+                self.generated_names.add(name)
+                return name
+        return f"角色_{len(self.generated_names)}"
+
+    def _generate_location_name(self) -> str:
+        return random.choice(LOCATION_PREFIX) + random.choice(LOCATION_SUFFIX)
+
+    def _get_character_rate(self, chapter: int) -> float:
+        """根据章节获取角色生成概率(递减)"""
+        decay_periods = chapter // 50
+        rate = CONFIG["new_character_base_rate"] * (CONFIG["new_character_decay"] ** decay_periods)
+        return max(rate, 0.1)  # 最低 10%
+
+    def init_project(self):
+        """初始化模拟项目"""
+        self.project_root.mkdir(parents=True, exist_ok=True)
+        (self.project_root / ".webnovel").mkdir(exist_ok=True)
+        (self.project_root / "正文").mkdir(exist_ok=True)
+
+        # 初始 state.json
+        initial_state = {
+            "project_info": {
+                "title": "模拟测试小说",
+                "genre": "玄幻",
+                "created_at": datetime.now().strftime("%Y-%m-%d"),
+                "target_chapters": 500,
+            },
+            "progress": {
+                "current_chapter": 0,
+                "total_words": 0,
+            },
+            "protagonist_state": {
+                "name": "林天",
+                "realm": "练气",
+                "layer": 1,
+                "golden_finger": {"name": "混沌珠", "level": 1},
+            },
+            "entities_v3": {
+                "角色": {},
+                "地点": {},
+                "物品": {},
+                "势力": {},
+                "招式": {},
+            },
+            "alias_index": {},
+            "foreshadowing": [],
+            "relationships": [],
+        }
+
+        # 添加主角到实体
+        protagonist_id = "protagonist_lintian"
+        initial_state["entities_v3"]["角色"][protagonist_id] = {
+            "canonical_name": "林天",
+            "desc": "主角,拥有混沌珠",
+            "tier": "核心",
+            "aliases": ["林天", "天哥", "林少侠"],
+            "current": {"realm": "练气", "layer": 1},
+            "history": [],
+        }
+        initial_state["alias_index"]["林天"] = [{"type": "角色", "id": protagonist_id}]
+        initial_state["alias_index"]["天哥"] = [{"type": "角色", "id": protagonist_id}]
+
+        atomic_write_json(self.state_file, initial_state, backup=False)
+        return initial_state
+
+    def simulate_chapter(self, chapter: int, state: Dict) -> Dict:
+        """模拟一章的数据变化"""
+
+        # 1. 更新进度
+        state["progress"]["current_chapter"] = chapter
+        state["progress"]["total_words"] += CONFIG["words_per_chapter"]
+
+        entities_v3 = state["entities_v3"]
+        alias_index = state["alias_index"]
+
+        # 2. 新增角色(概率递减)
+        if random.random() < self._get_character_rate(chapter):
+            char_name = self._generate_character_name()
+            char_id = self._generate_id("char")
+            tier = random.choices(
+                ["核心", "支线", "装饰"],
+                weights=[0.1, 0.3, 0.6]
+            )[0]
+
+            entities_v3["角色"][char_id] = {
+                "canonical_name": char_name,
+                "desc": f"第{chapter}章出场的{tier}角色",
+                "tier": tier,
+                "aliases": [char_name],
+                "current": {"first_appearance": chapter},
+                "history": [],
+            }
+            alias_index[char_name] = [{"type": "角色", "id": char_id}]
+
+            # 生成额外别名
+            if random.random() < 0.5:
+                alias = char_name[0] + "兄" if random.random() < 0.5 else char_name + "前辈"
+                entities_v3["角色"][char_id]["aliases"].append(alias)
+                if alias not in alias_index:
+                    alias_index[alias] = []
+                alias_index[alias].append({"type": "角色", "id": char_id})
+
+        # 3. 新增地点
+        if random.random() < CONFIG["new_location_rate"]:
+            loc_name = self._generate_location_name()
+            loc_id = self._generate_id("loc")
+            entities_v3["地点"][loc_id] = {
+                "canonical_name": loc_name,
+                "desc": f"第{chapter}章出现的地点",
+                "tier": "装饰",
+                "aliases": [loc_name],
+                "current": {},
+                "history": [],
+            }
+            alias_index[loc_name] = [{"type": "地点", "id": loc_id}]
+
+        # 4. 新增物品
+        if random.random() < CONFIG["new_item_rate"]:
+            item_name = random.choice(["灵", "仙", "神", "圣"]) + random.choice(["剑", "丹", "符", "器"])
+            item_id = self._generate_id("item")
+            entities_v3["物品"][item_id] = {
+                "canonical_name": item_name,
+                "desc": f"第{chapter}章获得的物品",
+                "tier": "装饰",
+                "aliases": [item_name],
+                "current": {},
+                "history": [],
+            }
+            if item_name not in alias_index:
+                alias_index[item_name] = []
+            alias_index[item_name].append({"type": "物品", "id": item_id})
+
+        # 5. 埋设伏笔
+        if random.random() < CONFIG["foreshadow_plant_rate"]:
+            tier = random.choices(
+                CONFIG["foreshadow_tiers"],
+                weights=CONFIG["foreshadow_tier_weights"]
+            )[0]
+            target = chapter + random.randint(10, 100)
+
+            state["foreshadowing"].append({
+                "id": f"fs_{chapter}_{random.randint(1000, 9999)}",
+                "content": f"第{chapter}章埋设的{tier}伏笔",
+                "tier": tier,
+                "status": "未回收",
+                "planted_chapter": chapter,
+                "target_chapter": target,
+            })
+
+        # 6. 回收伏笔
+        active_foreshadows = [
+            f for f in state["foreshadowing"]
+            if f.get("status") == "未回收" and f.get("target_chapter", 999) <= chapter
+        ]
+        for fs in active_foreshadows:
+            if random.random() < CONFIG["foreshadow_resolve_rate"]:
+                fs["status"] = "已回收"
+                fs["resolved_chapter"] = chapter
+
+        # 7. 主角升级
+        if chapter % CONFIG["protagonist_upgrade_interval"] == 0:
+            ps = state["protagonist_state"]
+            current_layer = ps.get("layer", 1)
+            current_realm_idx = CONFIG["realms"].index(ps.get("realm", "练气"))
+
+            if current_layer < CONFIG["layers_per_realm"]:
+                ps["layer"] = current_layer + 1
+            elif current_realm_idx < len(CONFIG["realms"]) - 1:
+                ps["realm"] = CONFIG["realms"][current_realm_idx + 1]
+                ps["layer"] = 1
+
+        # 8. 更新关系
+        if chapter % CONFIG["relationship_update_interval"] == 0:
+            char_ids = list(entities_v3["角色"].keys())
+            if len(char_ids) >= 2:
+                char1, char2 = random.sample(char_ids, 2)
+                rel_type = random.choice(CONFIG["relationship_types"])
+
+                state["relationships"].append({
+                    "char1_id": char1,
+                    "char2_id": char2,
+                    "type": rel_type,
+                    "intensity": random.randint(30, 100),
+                    "established_chapter": chapter,
+                })
+
+        return state
+
+    def run_simulation(self, checkpoint_interval: int = 10):
+        """运行完整模拟"""
+        print("🚀 开始500章沙盘模拟...")
+        print(f"📁 测试目录: {self.project_root}")
+        print()
+
+        state = self.init_project()
+        self.metrics.record_checkpoint(0, state, self.state_file)
+
+        start_time = time.time()
+
+        for chapter in range(1, CONFIG["total_chapters"] + 1):
+            try:
+                # 模拟章节
+                state = self.simulate_chapter(chapter, state)
+
+                # 原子写入
+                write_start = time.time()
+                atomic_write_json(self.state_file, state, use_lock=True, backup=False)
+                write_duration = time.time() - write_start
+                self.metrics.record_write_time(write_duration)
+
+                # 记录检查点
+                if chapter % checkpoint_interval == 0:
+                    self.metrics.record_checkpoint(chapter, state, self.state_file)
+                    elapsed = time.time() - start_time
+                    eta = elapsed / chapter * (CONFIG["total_chapters"] - chapter)
+                    print(f"  第 {chapter:3d} 章完成 | "
+                          f"文件 {self.state_file.stat().st_size / 1024:.1f}KB | "
+                          f"实体 {sum(len(e) for e in state['entities_v3'].values())} | "
+                          f"写入 {write_duration*1000:.1f}ms | "
+                          f"ETA {eta:.0f}s")
+
+            except Exception as e:
+                self.metrics.record_error(f"Chapter {chapter}: {str(e)}")
+                print(f"  ❌ 第 {chapter} 章错误: {e}")
+
+        # 最终检查点
+        self.metrics.record_checkpoint(CONFIG["total_chapters"], state, self.state_file)
+
+        total_time = time.time() - start_time
+        print()
+        print(f"✅ 模拟完成!总耗时: {total_time:.1f}s")
+        print()
+
+        return self.metrics.generate_report()
+
+
+def main():
+    """主函数"""
+    # 创建临时测试目录
+    test_dir = Path(tempfile.mkdtemp(prefix="webnovel_stress_test_"))
+
+    try:
+        simulator = ChapterSimulator(test_dir)
+        report = simulator.run_simulation(checkpoint_interval=10)
+
+        print(report)
+
+        # 保存报告
+        report_file = test_dir / "stress_test_report.md"
+        report_file.write_text(report, encoding="utf-8")
+        print(f"\n📄 报告已保存: {report_file}")
+
+        # 询问是否保留测试数据
+        print(f"\n测试数据目录: {test_dir}")
+        print("(测试完成后可手动删除)")
+
+    except KeyboardInterrupt:
+        print("\n⚠️ 测试被中断")
+    except Exception as e:
+        print(f"\n❌ 测试失败: {e}")
+        raise
+
+
+if __name__ == "__main__":
+    main()

+ 721 - 0
.claude/scripts/stress_test_index.py

@@ -0,0 +1,721 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+500章索引系统压力测试
+
+测试目标:
+1. index.db 大小增长曲线
+2. 实体同步性能(entities_v3 → index.db)
+3. 别名查询性能
+4. 模糊搜索性能
+5. 伏笔紧急度计算性能
+6. 关系图查询性能
+7. 并发读写稳定性
+
+依赖:stress_test_500chapters.py 生成的 state.json
+"""
+
+import json
+import os
+import sys
+import time
+import random
+import sqlite3
+import tempfile
+import shutil
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, Any, List, Tuple
+
+# 添加脚本目录到路径
+script_dir = Path(__file__).resolve().parent
+sys.path.insert(0, str(script_dir))
+
+from security_utils import atomic_write_json, read_json_safe
+
+# Windows 编码修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+
+# ============================================================================
+# 模拟配置(与 stress_test_500chapters.py 保持一致)
+# ============================================================================
+
+CONFIG = {
+    "total_chapters": 500,
+    "words_per_chapter": 3500,
+    "new_character_base_rate": 0.8,
+    "new_character_decay": 0.95,
+    "new_location_rate": 0.3,
+    "new_item_rate": 0.2,
+    "foreshadow_plant_rate": 0.5,
+    "foreshadow_resolve_rate": 0.3,
+    "relationship_update_interval": 5,
+}
+
+SURNAME_POOL = ["林", "陈", "王", "李", "张", "刘", "赵", "黄", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "罗", "郑"]
+NAME_POOL = ["天", "云", "风", "雷", "火", "水", "月", "星", "龙", "凤", "虎", "鹤", "剑", "刀", "枪", "棍", "拳", "掌", "指", "心"]
+
+
+class IndexMetrics:
+    """索引性能指标收集器"""
+
+    def __init__(self):
+        self.checkpoints: List[Dict] = []
+        self.sync_times: List[float] = []
+        self.query_times: Dict[str, List[float]] = {
+            "alias_lookup": [],
+            "fuzzy_search": [],
+            "foreshadow_urgency": [],
+            "relationship_query": [],
+            "entity_by_type": [],
+        }
+        self.errors: List[str] = []
+
+    def record_checkpoint(self, chapter: int, db_path: Path, state: Dict):
+        """记录检查点"""
+        db_size = db_path.stat().st_size if db_path.exists() else 0
+
+        # 统计各表行数
+        table_counts = {}
+        if db_path.exists():
+            try:
+                conn = sqlite3.connect(str(db_path))
+                cursor = conn.cursor()
+                for table in ["chapters", "entities", "entity_aliases", "entity_kv",
+                              "entity_history", "foreshadowing_index", "relationships"]:
+                    try:
+                        cursor.execute(f"SELECT COUNT(*) FROM {table}")
+                        table_counts[table] = cursor.fetchone()[0]
+                    except sqlite3.OperationalError:
+                        table_counts[table] = 0
+                conn.close()
+            except Exception as e:
+                self.errors.append(f"DB stats error: {e}")
+
+        self.checkpoints.append({
+            "chapter": chapter,
+            "db_size_kb": db_size / 1024,
+            "table_counts": table_counts,
+            "avg_sync_time_ms": sum(self.sync_times[-10:]) / max(len(self.sync_times[-10:]), 1) * 1000,
+            "query_performance": {
+                k: sum(v[-10:]) / max(len(v[-10:]), 1) * 1000
+                for k, v in self.query_times.items()
+            }
+        })
+
+    def record_sync_time(self, duration: float):
+        self.sync_times.append(duration)
+
+    def record_query_time(self, query_type: str, duration: float):
+        if query_type in self.query_times:
+            self.query_times[query_type].append(duration)
+
+    def record_error(self, error: str):
+        self.errors.append(error)
+
+    def generate_report(self) -> str:
+        """生成测试报告"""
+        if not self.checkpoints:
+            return "No data collected"
+
+        final = self.checkpoints[-1]
+        first = self.checkpoints[0] if self.checkpoints else final
+
+        lines = [
+            "=" * 70,
+            "📊 500章索引系统压力测试报告",
+            "=" * 70,
+            "",
+            "## index.db 增长",
+            f"- 初始大小: {first['db_size_kb']:.2f} KB",
+            f"- 最终大小: {final['db_size_kb']:.2f} KB",
+            f"- 增长倍数: {final['db_size_kb'] / max(first['db_size_kb'], 0.1):.1f}x",
+            "",
+            "## 表行数统计",
+        ]
+
+        for table, count in final.get('table_counts', {}).items():
+            lines.append(f"  - {table}: {count:,}")
+
+        lines.extend([
+            "",
+            "## 同步性能",
+            f"- 平均同步时间: {sum(self.sync_times) / max(len(self.sync_times), 1) * 1000:.2f} ms",
+            f"- 最大同步时间: {max(self.sync_times) * 1000:.2f} ms" if self.sync_times else "N/A",
+            f"- 最小同步时间: {min(self.sync_times) * 1000:.2f} ms" if self.sync_times else "N/A",
+            "",
+            "## 查询性能(平均)",
+        ])
+
+        for query_type, times in self.query_times.items():
+            if times:
+                avg = sum(times) / len(times) * 1000
+                lines.append(f"  - {query_type}: {avg:.2f} ms")
+
+        lines.extend([
+            "",
+            "## 错误统计",
+            f"- 错误数: {len(self.errors)}",
+        ])
+
+        if self.errors:
+            lines.append("- 错误详情:")
+            for err in self.errors[:10]:
+                lines.append(f"  - {err[:80]}")
+
+        # 增长曲线
+        lines.extend([
+            "",
+            "## 增长曲线(每100章)",
+            "| 章节 | DB大小(KB) | entities | aliases | foreshadow | 同步(ms) |",
+            "|------|-----------|----------|---------|------------|----------|",
+        ])
+
+        for cp in self.checkpoints:
+            if cp['chapter'] % 100 == 0 or cp['chapter'] == final['chapter']:
+                tc = cp.get('table_counts', {})
+                lines.append(
+                    f"| {cp['chapter']} | {cp['db_size_kb']:.1f} | "
+                    f"{tc.get('entities', 0)} | {tc.get('entity_aliases', 0)} | "
+                    f"{tc.get('foreshadowing_index', 0)} | {cp['avg_sync_time_ms']:.1f} |"
+                )
+
+        # 查询性能趋势
+        lines.extend([
+            "",
+            "## 查询性能趋势(每100章)",
+            "| 章节 | alias查询(ms) | 模糊搜索(ms) | 伏笔紧急度(ms) | 关系查询(ms) |",
+            "|------|--------------|-------------|---------------|-------------|",
+        ])
+
+        for cp in self.checkpoints:
+            if cp['chapter'] % 100 == 0 or cp['chapter'] == final['chapter']:
+                qp = cp.get('query_performance', {})
+                lines.append(
+                    f"| {cp['chapter']} | {qp.get('alias_lookup', 0):.2f} | "
+                    f"{qp.get('fuzzy_search', 0):.2f} | "
+                    f"{qp.get('foreshadow_urgency', 0):.2f} | "
+                    f"{qp.get('relationship_query', 0):.2f} |"
+                )
+
+        # 稳定性评估
+        lines.extend([
+            "",
+            "## 稳定性评估",
+        ])
+
+        if final['db_size_kb'] < 1024:
+            lines.append("✅ 数据库大小合理 (< 1MB)")
+        elif final['db_size_kb'] < 5120:
+            lines.append("⚠️ 数据库偏大 (1-5MB)")
+        else:
+            lines.append("❌ 数据库过大 (> 5MB)")
+
+        avg_sync = sum(self.sync_times) / max(len(self.sync_times), 1) * 1000
+        if avg_sync < 100:
+            lines.append("✅ 同步性能良好 (< 100ms)")
+        elif avg_sync < 500:
+            lines.append("⚠️ 同步性能一般 (100-500ms)")
+        else:
+            lines.append("❌ 同步性能差 (> 500ms)")
+
+        # 查询性能评估
+        for query_type, times in self.query_times.items():
+            if times:
+                avg = sum(times) / len(times) * 1000
+                if avg < 10:
+                    lines.append(f"✅ {query_type} 查询快速 (< 10ms)")
+                elif avg < 50:
+                    lines.append(f"⚠️ {query_type} 查询一般 (10-50ms)")
+                else:
+                    lines.append(f"❌ {query_type} 查询慢 (> 50ms)")
+
+        if not self.errors:
+            lines.append("✅ 无错误")
+        else:
+            lines.append(f"❌ 有 {len(self.errors)} 个错误")
+
+        lines.append("")
+        lines.append("=" * 70)
+
+        return "\n".join(lines)
+
+
+class IndexSimulator:
+    """索引系统模拟器"""
+
+    def __init__(self, project_root: Path):
+        self.project_root = project_root
+        self.state_file = project_root / ".webnovel" / "state.json"
+        self.db_path = project_root / ".webnovel" / "index.db"
+        self.metrics = IndexMetrics()
+        self.generated_names = set()
+        self.entity_id_counter = 0
+
+    def _generate_id(self, prefix: str) -> str:
+        self.entity_id_counter += 1
+        return f"{prefix}_{self.entity_id_counter:05d}"
+
+    def _generate_character_name(self) -> str:
+        for _ in range(100):
+            name = random.choice(SURNAME_POOL) + random.choice(NAME_POOL) + random.choice(NAME_POOL)
+            if name not in self.generated_names:
+                self.generated_names.add(name)
+                return name
+        return f"角色_{len(self.generated_names)}"
+
+    def _get_character_rate(self, chapter: int) -> float:
+        decay_periods = chapter // 50
+        rate = CONFIG["new_character_base_rate"] * (CONFIG["new_character_decay"] ** decay_periods)
+        return max(rate, 0.1)
+
+    def init_database(self):
+        """初始化数据库"""
+        conn = sqlite3.connect(str(self.db_path))
+        cursor = conn.cursor()
+
+        # 创建表结构(与 structured_index.py 一致)
+        cursor.executescript("""
+            -- 章节表
+            CREATE TABLE IF NOT EXISTS chapters (
+                chapter_num INTEGER PRIMARY KEY,
+                title TEXT,
+                word_count INTEGER,
+                summary TEXT,
+                main_location TEXT,
+                characters TEXT,
+                content_hash TEXT,
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            );
+
+            -- 实体主表
+            CREATE TABLE IF NOT EXISTS entities (
+                entity_id TEXT PRIMARY KEY,
+                entity_type TEXT NOT NULL,
+                canonical_name TEXT,
+                tier TEXT,
+                desc TEXT,
+                created_chapter INTEGER,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            );
+
+            -- 别名表
+            CREATE TABLE IF NOT EXISTS entity_aliases (
+                alias TEXT,
+                entity_id TEXT,
+                entity_type TEXT,
+                first_seen_chapter INTEGER,
+                context TEXT,
+                PRIMARY KEY (alias, entity_id)
+            );
+            CREATE INDEX IF NOT EXISTS idx_alias ON entity_aliases(alias);
+
+            -- 实体属性 (KV)
+            CREATE TABLE IF NOT EXISTS entity_kv (
+                entity_id TEXT,
+                key TEXT,
+                value TEXT,
+                last_chapter INTEGER,
+                PRIMARY KEY (entity_id, key)
+            );
+
+            -- 实体历史
+            CREATE TABLE IF NOT EXISTS entity_history (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                entity_id TEXT,
+                chapter INTEGER,
+                changes_json TEXT,
+                reasons_json TEXT,
+                added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            );
+
+            -- 伏笔索引
+            CREATE TABLE IF NOT EXISTS foreshadowing_index (
+                foreshadow_id TEXT PRIMARY KEY,
+                content TEXT,
+                tier TEXT,
+                status TEXT,
+                planted_chapter INTEGER,
+                target_chapter INTEGER,
+                resolved_chapter INTEGER,
+                urgency_score REAL
+            );
+
+            -- 关系表
+            CREATE TABLE IF NOT EXISTS relationships (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                char1_id TEXT,
+                char2_id TEXT,
+                rel_type TEXT,
+                intensity INTEGER,
+                established_chapter INTEGER,
+                description TEXT
+            );
+            CREATE INDEX IF NOT EXISTS idx_rel_char1 ON relationships(char1_id);
+            CREATE INDEX IF NOT EXISTS idx_rel_char2 ON relationships(char2_id);
+        """)
+
+        conn.commit()
+        conn.close()
+
+    def init_project(self):
+        """初始化模拟项目"""
+        self.project_root.mkdir(parents=True, exist_ok=True)
+        (self.project_root / ".webnovel").mkdir(exist_ok=True)
+
+        # 初始 state.json
+        initial_state = {
+            "project_info": {"title": "索引测试小说", "genre": "玄幻"},
+            "progress": {"current_chapter": 0, "total_words": 0},
+            "protagonist_state": {"name": "林天", "realm": "练气", "layer": 1},
+            "entities_v3": {"角色": {}, "地点": {}, "物品": {}, "势力": {}, "招式": {}},
+            "alias_index": {},
+            "foreshadowing": [],
+            "relationships": [],
+        }
+
+        # 添加主角
+        protagonist_id = "protagonist_lintian"
+        initial_state["entities_v3"]["角色"][protagonist_id] = {
+            "canonical_name": "林天",
+            "desc": "主角",
+            "tier": "核心",
+            "aliases": ["林天", "天哥"],
+            "current": {"realm": "练气"},
+            "history": [],
+        }
+        initial_state["alias_index"]["林天"] = [{"type": "角色", "id": protagonist_id}]
+
+        atomic_write_json(self.state_file, initial_state, backup=False)
+        self.init_database()
+        return initial_state
+
+    def sync_to_index(self, state: Dict, chapter: int):
+        """同步 state.json 到 index.db"""
+        conn = sqlite3.connect(str(self.db_path))
+        cursor = conn.cursor()
+
+        try:
+            # 同步章节
+            cursor.execute("""
+                INSERT OR REPLACE INTO chapters
+                (chapter_num, title, word_count, summary)
+                VALUES (?, ?, ?, ?)
+            """, (chapter, f"第{chapter}章", CONFIG["words_per_chapter"], f"第{chapter}章摘要"))
+
+            # 同步实体
+            entities_v3 = state.get("entities_v3", {})
+            for entity_type, entities in entities_v3.items():
+                for entity_id, entity_data in entities.items():
+                    cursor.execute("""
+                        INSERT OR REPLACE INTO entities
+                        (entity_id, entity_type, canonical_name, tier, desc, created_chapter)
+                        VALUES (?, ?, ?, ?, ?, ?)
+                    """, (
+                        entity_id,
+                        entity_type,
+                        entity_data.get("canonical_name", ""),
+                        entity_data.get("tier", "装饰"),
+                        entity_data.get("desc", ""),
+                        chapter
+                    ))
+
+                    # 同步别名
+                    for alias in entity_data.get("aliases", []):
+                        cursor.execute("""
+                            INSERT OR IGNORE INTO entity_aliases
+                            (alias, entity_id, entity_type, first_seen_chapter)
+                            VALUES (?, ?, ?, ?)
+                        """, (alias, entity_id, entity_type, chapter))
+
+                    # 同步当前属性
+                    for key, value in entity_data.get("current", {}).items():
+                        cursor.execute("""
+                            INSERT OR REPLACE INTO entity_kv
+                            (entity_id, key, value, last_chapter)
+                            VALUES (?, ?, ?, ?)
+                        """, (entity_id, key, str(value), chapter))
+
+            # 同步伏笔
+            for fs in state.get("foreshadowing", []):
+                # 计算紧急度
+                if fs.get("status") == "未回收":
+                    target = fs.get("target_chapter", chapter + 100)
+                    urgency = max(0, 100 - (target - chapter))
+                else:
+                    urgency = 0
+
+                cursor.execute("""
+                    INSERT OR REPLACE INTO foreshadowing_index
+                    (foreshadow_id, content, tier, status, planted_chapter,
+                     target_chapter, resolved_chapter, urgency_score)
+                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+                """, (
+                    fs.get("id", f"fs_{chapter}"),
+                    fs.get("content", ""),
+                    fs.get("tier", "装饰"),
+                    fs.get("status", "未回收"),
+                    fs.get("planted_chapter", chapter),
+                    fs.get("target_chapter"),
+                    fs.get("resolved_chapter"),
+                    urgency
+                ))
+
+            # 同步关系(使用 REPLACE 避免重复)
+            # 先清空再重建(简化策略,实际生产应增量同步)
+            cursor.execute("DELETE FROM relationships WHERE established_chapter <= ?", (chapter,))
+            for rel in state.get("relationships", []):
+                cursor.execute("""
+                    INSERT INTO relationships
+                    (char1_id, char2_id, rel_type, intensity, established_chapter)
+                    VALUES (?, ?, ?, ?, ?)
+                """, (
+                    rel.get("char1_id", ""),
+                    rel.get("char2_id", ""),
+                    rel.get("type", "ally"),
+                    rel.get("intensity", 50),
+                    rel.get("established_chapter", chapter)
+                ))
+
+            conn.commit()
+
+        finally:
+            conn.close()
+
+    def run_queries(self, state: Dict, chapter: int):
+        """执行各类查询并计时"""
+        conn = sqlite3.connect(str(self.db_path))
+        cursor = conn.cursor()
+
+        try:
+            # 1. 别名查询
+            alias_list = list(state.get("alias_index", {}).keys())
+            if alias_list:
+                test_alias = random.choice(alias_list)
+                start = time.time()
+                cursor.execute("SELECT entity_id, entity_type FROM entity_aliases WHERE alias = ?", (test_alias,))
+                cursor.fetchall()
+                self.metrics.record_query_time("alias_lookup", time.time() - start)
+
+            # 2. 模糊搜索
+            if alias_list:
+                search_term = random.choice(alias_list)[:2]  # 取前两个字
+                start = time.time()
+                cursor.execute("""
+                    SELECT DISTINCT entity_id, entity_type, alias
+                    FROM entity_aliases
+                    WHERE alias LIKE ?
+                    LIMIT 20
+                """, (f"%{search_term}%",))
+                cursor.fetchall()
+                self.metrics.record_query_time("fuzzy_search", time.time() - start)
+
+            # 3. 伏笔紧急度查询
+            start = time.time()
+            cursor.execute("""
+                SELECT foreshadow_id, content, urgency_score
+                FROM foreshadowing_index
+                WHERE status = '未回收'
+                ORDER BY urgency_score DESC
+                LIMIT 10
+            """)
+            cursor.fetchall()
+            self.metrics.record_query_time("foreshadow_urgency", time.time() - start)
+
+            # 4. 关系查询
+            entities_v3 = state.get("entities_v3", {})
+            char_ids = list(entities_v3.get("角色", {}).keys())
+            if char_ids:
+                test_char = random.choice(char_ids)
+                start = time.time()
+                cursor.execute("""
+                    SELECT char2_id, rel_type, intensity
+                    FROM relationships
+                    WHERE char1_id = ?
+                    UNION
+                    SELECT char1_id, rel_type, intensity
+                    FROM relationships
+                    WHERE char2_id = ?
+                """, (test_char, test_char))
+                cursor.fetchall()
+                self.metrics.record_query_time("relationship_query", time.time() - start)
+
+            # 5. 按类型查询实体
+            start = time.time()
+            cursor.execute("""
+                SELECT entity_id, canonical_name, tier
+                FROM entities
+                WHERE entity_type = '角色' AND tier = '核心'
+            """)
+            cursor.fetchall()
+            self.metrics.record_query_time("entity_by_type", time.time() - start)
+
+        finally:
+            conn.close()
+
+    def simulate_chapter(self, chapter: int, state: Dict) -> Dict:
+        """模拟一章的数据变化(与主测试脚本类似)"""
+        state["progress"]["current_chapter"] = chapter
+        state["progress"]["total_words"] += CONFIG["words_per_chapter"]
+
+        entities_v3 = state["entities_v3"]
+        alias_index = state["alias_index"]
+
+        # 新增角色
+        if random.random() < self._get_character_rate(chapter):
+            char_name = self._generate_character_name()
+            char_id = self._generate_id("char")
+            tier = random.choices(["核心", "支线", "装饰"], weights=[0.1, 0.3, 0.6])[0]
+
+            entities_v3["角色"][char_id] = {
+                "canonical_name": char_name,
+                "desc": f"第{chapter}章出场",
+                "tier": tier,
+                "aliases": [char_name],
+                "current": {"first_appearance": chapter},
+                "history": [],
+            }
+            alias_index[char_name] = [{"type": "角色", "id": char_id}]
+
+            # 额外别名
+            if random.random() < 0.5:
+                alias = char_name[0] + "兄"
+                entities_v3["角色"][char_id]["aliases"].append(alias)
+                if alias not in alias_index:
+                    alias_index[alias] = []
+                alias_index[alias].append({"type": "角色", "id": char_id})
+
+        # 新增地点
+        if random.random() < CONFIG["new_location_rate"]:
+            loc_name = random.choice(["天", "云", "龙"]) + random.choice(["山", "谷", "城"])
+            loc_id = self._generate_id("loc")
+            entities_v3["地点"][loc_id] = {
+                "canonical_name": loc_name,
+                "desc": f"第{chapter}章",
+                "tier": "装饰",
+                "aliases": [loc_name],
+                "current": {},
+                "history": [],
+            }
+            if loc_name not in alias_index:
+                alias_index[loc_name] = []
+            alias_index[loc_name].append({"type": "地点", "id": loc_id})
+
+        # 伏笔
+        if random.random() < CONFIG["foreshadow_plant_rate"]:
+            state["foreshadowing"].append({
+                "id": f"fs_{chapter}_{random.randint(1000, 9999)}",
+                "content": f"第{chapter}章伏笔",
+                "tier": random.choice(["核心", "支线", "装饰"]),
+                "status": "未回收",
+                "planted_chapter": chapter,
+                "target_chapter": chapter + random.randint(10, 100),
+            })
+
+        # 回收伏笔
+        for fs in state["foreshadowing"]:
+            if (fs.get("status") == "未回收" and
+                fs.get("target_chapter", 999) <= chapter and
+                random.random() < CONFIG["foreshadow_resolve_rate"]):
+                fs["status"] = "已回收"
+                fs["resolved_chapter"] = chapter
+
+        # 关系
+        if chapter % CONFIG["relationship_update_interval"] == 0:
+            char_ids = list(entities_v3["角色"].keys())
+            if len(char_ids) >= 2:
+                char1, char2 = random.sample(char_ids, 2)
+                state["relationships"].append({
+                    "char1_id": char1,
+                    "char2_id": char2,
+                    "type": random.choice(["ally", "enemy", "romance", "rival"]),
+                    "intensity": random.randint(30, 100),
+                    "established_chapter": chapter,
+                })
+
+        return state
+
+    def run_simulation(self, checkpoint_interval: int = 10):
+        """运行完整模拟"""
+        print("🚀 开始500章索引系统压力测试...")
+        print(f"📁 测试目录: {self.project_root}")
+        print()
+
+        state = self.init_project()
+        self.metrics.record_checkpoint(0, self.db_path, state)
+
+        start_time = time.time()
+
+        for chapter in range(1, CONFIG["total_chapters"] + 1):
+            try:
+                # 模拟章节数据
+                state = self.simulate_chapter(chapter, state)
+
+                # 保存 state.json
+                atomic_write_json(self.state_file, state, use_lock=True, backup=False)
+
+                # 同步到索引
+                sync_start = time.time()
+                self.sync_to_index(state, chapter)
+                sync_duration = time.time() - sync_start
+                self.metrics.record_sync_time(sync_duration)
+
+                # 执行查询测试
+                self.run_queries(state, chapter)
+
+                # 记录检查点
+                if chapter % checkpoint_interval == 0:
+                    self.metrics.record_checkpoint(chapter, self.db_path, state)
+                    elapsed = time.time() - start_time
+                    eta = elapsed / chapter * (CONFIG["total_chapters"] - chapter)
+                    db_size = self.db_path.stat().st_size / 1024 if self.db_path.exists() else 0
+                    print(f"  第 {chapter:3d} 章 | "
+                          f"DB {db_size:.1f}KB | "
+                          f"同步 {sync_duration*1000:.1f}ms | "
+                          f"ETA {eta:.0f}s")
+
+            except Exception as e:
+                self.metrics.record_error(f"Chapter {chapter}: {str(e)}")
+                print(f"  ❌ 第 {chapter} 章错误: {e}")
+
+        # 最终检查点
+        self.metrics.record_checkpoint(CONFIG["total_chapters"], self.db_path, state)
+
+        total_time = time.time() - start_time
+        print()
+        print(f"✅ 索引测试完成!总耗时: {total_time:.1f}s")
+        print()
+
+        return self.metrics.generate_report()
+
+
+def main():
+    """主函数"""
+    test_dir = Path(tempfile.mkdtemp(prefix="webnovel_index_test_"))
+
+    try:
+        simulator = IndexSimulator(test_dir)
+        report = simulator.run_simulation(checkpoint_interval=10)
+
+        print(report)
+
+        # 保存报告
+        report_file = test_dir / "index_stress_test_report.md"
+        report_file.write_text(report, encoding="utf-8")
+        print(f"\n📄 报告已保存: {report_file}")
+        print(f"\n测试数据目录: {test_dir}")
+
+    except KeyboardInterrupt:
+        print("\n⚠️ 测试被中断")
+    except Exception as e:
+        print(f"\n❌ 测试失败: {e}")
+        import traceback
+        traceback.print_exc()
+
+
+if __name__ == "__main__":
+    main()

+ 1256 - 0
.claude/scripts/structured_index.py

@@ -0,0 +1,1256 @@
+#!/usr/bin/env python3
+"""
+结构化索引系统(Structured Index System)v4.0
+
+目标:取代向量化检索,使用 SQLite 提供精确、快速的结构化查询
+
+v4.0 变更:
+- 新增 entities/entity_aliases/entity_kv/entity_history 表
+- 主键从 name 迁移到 entity_id
+- relationships 表使用 char1_id/char2_id
+- 不再写回 state.json(消除循环依赖)
+- 从 entities_v3 + alias_index 同步数据
+
+核心功能:
+1. 实体索引(entities, entity_aliases, entity_kv, entity_history)
+2. 章节元数据索引(location, characters, word_count)
+3. 伏笔追踪索引(status, urgency calculation)
+4. 文件 Hash 自愈机制(auto-rebuild on change)
+
+性能目标:
+- 查询速度:2-5ms(vs 文件遍历 500ms,提升 250x)
+- 索引构建:10ms/章(增量更新)
+- 存储开销:200 章 ≈ 100 KB
+
+使用方式:
+  # 更新单章索引
+  python structured_index.py --update-chapter 7 --metadata-file /tmp/ch7.json
+
+  # 批量重建索引(历史章节)
+  python structured_index.py --rebuild-index
+
+  # 查询地点相关章节
+  python structured_index.py --query-location "血煞秘境"
+
+  # 查询紧急伏笔
+  python structured_index.py --query-urgent-foreshadowing
+
+  # 模糊查询角色
+  python structured_index.py --fuzzy-search "姓李" "女弟子"
+
+  # 查看统计信息
+  python structured_index.py --stats
+"""
+
+import json
+import os
+import sys
+import argparse
+import sqlite3
+import hashlib
+import re
+import tempfile
+from datetime import datetime
+from pathlib import Path
+from typing import Optional, List, Dict, Tuple
+
+# ============================================================================
+# 安全修复:导入安全工具函数(P1 MEDIUM)
+# ============================================================================
+from security_utils import create_secure_directory
+from project_locator import resolve_project_root
+from chapter_paths import find_chapter_file
+
+
+class StructuredIndex:
+    """结构化索引管理器(取代向量化检索)"""
+
+    def __init__(self, project_root=None):
+        if project_root is None:
+            try:
+                project_root = resolve_project_root()
+            except FileNotFoundError:
+                project_root = Path.cwd()
+        else:
+            project_root = Path(project_root)
+
+        self.project_root = project_root
+        self.state_file = project_root / ".webnovel" / "state.json"
+        self.chapters_dir = project_root / "正文"
+        self.index_db = project_root / ".webnovel" / "index.db"
+
+        # ============================================================================
+        # 安全修复:使用安全目录创建函数(P1 MEDIUM)
+        # 原代码: self.index_db.parent.mkdir(parents=True, exist_ok=True)
+        # 漏洞: 未设置权限,使用OS默认(可能为755,允许同组用户读取)
+        # ============================================================================
+        create_secure_directory(str(self.index_db.parent))
+
+        # 连接数据库
+        self.conn = sqlite3.connect(str(self.index_db))
+        self.conn.row_factory = sqlite3.Row  # 返回字典式行
+
+        # 创建表结构
+        self._create_tables()
+
+    def _create_tables(self):
+        """创建索引表结构(v4.0 主键迁移到 entity_id)"""
+
+        # ============== 新增实体表(v4.0)==============
+
+        # 实体主表(取代旧 characters 表)
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS entities (
+                entity_id TEXT PRIMARY KEY,
+                entity_type TEXT NOT NULL,
+                canonical_name TEXT,
+                tier TEXT,
+                desc TEXT,
+                created_chapter INTEGER,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            )
+        """)
+
+        # 实体类型索引
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_entity_type
+            ON entities(entity_type)
+        """)
+
+        # 别名表(支持一对多查询)
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS entity_aliases (
+                alias TEXT,
+                entity_id TEXT,
+                entity_type TEXT,
+                first_seen_chapter INTEGER,
+                context TEXT,
+                PRIMARY KEY (alias, entity_id)
+            )
+        """)
+
+        # 别名索引(加速反向查询)
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_alias
+            ON entity_aliases(alias)
+        """)
+
+        # 实体属性 KV 表
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS entity_kv (
+                entity_id TEXT,
+                key TEXT,
+                value TEXT,
+                last_chapter INTEGER,
+                PRIMARY KEY (entity_id, key)
+            )
+        """)
+
+        # 实体历史表
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS entity_history (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                entity_id TEXT,
+                chapter INTEGER,
+                changes_json TEXT,
+                reasons_json TEXT,
+                added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            )
+        """)
+
+        # 历史索引
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_entity_history
+            ON entity_history(entity_id, chapter)
+        """)
+
+        # ============== 章节元数据表 ==============
+
+        # 1. 章节元数据表(v4.0: characters 改为存 entity_id 列表)
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS chapters (
+                chapter_num INTEGER PRIMARY KEY,
+                title TEXT,
+                location TEXT,
+                location_id TEXT,
+                characters TEXT,  -- JSON: ["entity_id_1", "entity_id_2"]
+                word_count INTEGER,
+                content_hash TEXT,
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            )
+        """)
+
+        # 地点索引(加速查询)
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_location
+            ON chapters(location)
+        """)
+
+        # 2. 伏笔追踪表
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS foreshadowing_index (
+                id INTEGER PRIMARY KEY,
+                content TEXT,
+                location TEXT,
+                characters TEXT,  -- JSON: ["李雪", "主角"]
+                introduced_chapter INTEGER,
+                resolved_chapter INTEGER,
+                status TEXT,  -- '未回收' / '已回收'
+                urgency INTEGER DEFAULT 0,  -- 0-100,自动计算
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            )
+        """)
+
+        # 状态索引
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_status
+            ON foreshadowing_index(status)
+        """)
+
+        # 紧急度索引
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_urgency
+            ON foreshadowing_index(urgency)
+        """)
+
+        # 3. 角色关系表(v4.0: 使用 entity_id)
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS relationships (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                char1_id TEXT,
+                char2_id TEXT,
+                char1_name TEXT,
+                char2_name TEXT,
+                relation_type TEXT,  -- 'ally', 'enemy', 'romance', 'mentor', 'debtor'
+                intensity INTEGER,    -- 关系强度 0-100
+                description TEXT,
+                last_update_chapter INTEGER,
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                UNIQUE(char1_id, char2_id, relation_type)  -- 防止重复
+            )
+        """)
+
+        # 关系索引(v4.0: 使用 entity_id)
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_char1_char2
+            ON relationships(char1_id, char2_id)
+        """)
+
+        # 4. 角色索引表(v4.0 已废弃,保留兼容)
+        # 新代码应使用 entities 表
+        self.conn.execute("""
+            CREATE TABLE IF NOT EXISTS characters (
+                name TEXT PRIMARY KEY,
+                description TEXT,
+                personality TEXT,
+                importance TEXT,  -- 'major' / 'minor'
+                power_level TEXT,
+                first_appearance INTEGER,
+                last_appearance INTEGER,
+                status TEXT DEFAULT 'active',  -- 'active' / 'archived'
+                archived_at TEXT,  -- ISO timestamp
+                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+            )
+        """)
+
+        # 角色名索引(加速模糊搜索)
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_character_name
+            ON characters(name)
+        """)
+
+        # 状态索引
+        self.conn.execute("""
+            CREATE INDEX IF NOT EXISTS idx_character_status
+            ON characters(status)
+        """)
+
+        self.conn.commit()
+
+    # ================== 核心功能 1:章节元数据索引 ==================
+
+    def index_chapter(self, chapter_num: int, metadata: Dict):
+        """为新章节建立索引(在 webnovel-write Step 4.6 调用)
+
+        Args:
+            chapter_num: 章节编号
+            metadata: {
+                'title': '章节标题',
+                'location': '地点',
+                'characters': ['李雪', '主角'],
+                'word_count': 3500,
+                'hash': 'md5_hash'
+            }
+        """
+        def _normalize_str_list(v) -> List[str]:
+            if v is None:
+                return []
+            if isinstance(v, list):
+                return [str(x).strip() for x in v if str(x).strip()]
+            if isinstance(v, str):
+                return [s.strip() for s in re.split(r"[,,]", v) if s.strip()]
+            return [str(v).strip()] if str(v).strip() else []
+
+        def _exists_entity(entity_id: str, entity_type: str) -> bool:
+            row = self.conn.execute(
+                "SELECT 1 FROM entities WHERE entity_id = ? AND entity_type = ? LIMIT 1",
+                (entity_id, entity_type),
+            ).fetchone()
+            return bool(row)
+
+        def _resolve_alias_ids(alias: str, entity_type: str) -> List[str]:
+            rows = self.conn.execute(
+                "SELECT entity_id FROM entity_aliases WHERE alias = ? AND entity_type = ?",
+                (alias, entity_type),
+            ).fetchall()
+            return [r["entity_id"] for r in rows] if rows else []
+
+        # v4.0: chapters.characters 存 entity_id 列表(metadata 允许传入 name/alias,索引层负责解析)
+        resolved_character_ids: List[str] = []
+        seen_ids = set()
+        for ref in _normalize_str_list(metadata.get("characters", [])):
+            if _exists_entity(ref, "角色"):
+                if ref not in seen_ids:
+                    resolved_character_ids.append(ref)
+                    seen_ids.add(ref)
+                continue
+
+            candidates = _resolve_alias_ids(ref, "角色")
+            if len(candidates) == 1:
+                cid = candidates[0]
+                if cid not in seen_ids:
+                    resolved_character_ids.append(cid)
+                    seen_ids.add(cid)
+                continue
+
+            if len(candidates) > 1:
+                print(f"⚠️ 角色别名歧义,跳过: {ref!r} 命中 {len(candidates)} 个角色")
+            else:
+                print(f"⚠️ 未知角色,跳过: {ref!r}")
+
+        # v4.0: 可选 location_id(只解析为地点实体)
+        location = str(metadata.get("location", "")).strip()
+        location_id = ""
+        if location:
+            if _exists_entity(location, "地点"):
+                location_id = location
+            else:
+                loc_candidates = _resolve_alias_ids(location, "地点")
+                if len(loc_candidates) == 1:
+                    location_id = loc_candidates[0]
+                elif len(loc_candidates) > 1:
+                    print(f"⚠️ 地点别名歧义,location_id 留空: {location!r} 命中 {len(loc_candidates)} 个地点")
+
+        self.conn.execute("""
+            INSERT OR REPLACE INTO chapters
+            (chapter_num, title, location, location_id, characters, word_count, content_hash, updated_at)
+            VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+        """, (
+            chapter_num,
+            metadata['title'],
+            location,
+            location_id,
+            json.dumps(resolved_character_ids, ensure_ascii=False),
+            metadata['word_count'],
+            metadata['hash']
+        ))
+
+        self.conn.commit()
+        print(f"✅ 章节索引已更新:Ch{chapter_num} - {metadata['title']}")
+
+    # bump_character_last_appearance_in_state 已删除(v4.0)
+    # 原因:消除索引层写回 state.json 的循环依赖
+    # last_appearance_chapter 现在作为 index.db 的派生字段
+
+    def query_chapters_by_location(self, location: str, limit: int = 10) -> List[Tuple]:
+        """O(log n) 查询:返回该地点的最近 N 章
+
+        Args:
+            location: 地点名称
+            limit: 返回数量
+
+        Returns:
+            [(chapter_num, title, characters), ...]
+        """
+        cursor = self.conn.execute("""
+            SELECT chapter_num, title, characters
+            FROM chapters
+            WHERE location = ?
+            ORDER BY chapter_num DESC
+            LIMIT ?
+        """, (location, limit))
+
+        return cursor.fetchall()
+
+    def calculate_chapter_hash(self, chapter_file: Path) -> str:
+        """计算章节文件 MD5 Hash(用于自愈机制)"""
+        if not chapter_file.exists():
+            return ""
+
+        with open(chapter_file, 'rb') as f:
+            return hashlib.md5(f.read()).hexdigest()
+
+    def get_stored_hash(self, chapter_num: int) -> Optional[str]:
+        """从索引中读取存储的 Hash"""
+        cursor = self.conn.execute("""
+            SELECT content_hash FROM chapters WHERE chapter_num = ?
+        """, (chapter_num,))
+
+        row = cursor.fetchone()
+        return row['content_hash'] if row else None
+
+    def validate_and_rebuild_if_needed(self, chapter_num: int):
+        """校验章节 Hash,不一致则自动重建索引(Self-Healing Index)
+
+        触发时机:
+        - context_manager.py 查询章节前调用
+        - 增加耗时:~5ms(Hash 计算 + 对比)
+        - 仅当检测到变更时才重建(增量成本)
+        """
+        chapter_file = find_chapter_file(self.project_root, chapter_num)
+        if chapter_file is None or not chapter_file.exists():
+            return  # 文件不存在,跳过
+
+        # 计算当前文件 Hash
+        current_hash = self.calculate_chapter_hash(chapter_file)
+
+        # 从索引中读取存储的 Hash
+        stored_hash = self.get_stored_hash(chapter_num)
+
+        if current_hash != stored_hash:
+            print(f"⚠️ 检测到 Ch{chapter_num} 已修改,自动重建索引...")
+            self._rebuild_chapter_index(chapter_num, chapter_file)
+            print(f"✅ Ch{chapter_num} 索引已更新")
+
+    def _rebuild_chapter_index(self, chapter_num: int, chapter_file: Path):
+        """重建单章索引(自动提取元数据)"""
+
+        # 读取章节内容
+        with open(chapter_file, 'r', encoding='utf-8') as f:
+            content = f.read()
+
+        # 提取元数据
+        metadata = self._extract_metadata_from_content(content, chapter_num)
+
+        # 重建索引
+        self.index_chapter(chapter_num, metadata)
+
+    def _extract_metadata_from_content(self, content: str, chapter_num: int) -> Dict:
+        """从章节内容中提取元数据"""
+
+        # 提取标题(第一行)
+        lines = content.split('\n')
+        title = lines[0].strip('# ').strip() if lines else f"第{chapter_num}章"
+
+        # 提取地点(在章节开头查找,通常格式为 **地点:XXX**)
+        location_match = re.search(r'\*\*地点[::]\s*(.+?)\*\*', content)
+        location = location_match.group(1).strip() if location_match else "未知"
+
+        # 提取角色(查找所有对话和描述中的角色名)
+        # 简化实现:从 state.json 读取已知角色,匹配出现频率
+        characters = self._extract_characters_from_content(content)
+
+        # 计算字数
+        word_count = len(content)
+
+        # 计算 Hash
+        content_hash = hashlib.md5(content.encode('utf-8')).hexdigest()
+
+        return {
+            'title': title,
+            'location': location,
+            'characters': characters[:5],  # 最多 5 个主要角色
+            'word_count': word_count,
+            'hash': content_hash
+        }
+
+    def _extract_characters_from_content(self, content: str) -> List[str]:
+        """从内容中提取角色(简化实现:读取索引中已知角色 canonical_name)"""
+
+        # 获取已知角色列表(限制规模,避免超大角色库拖慢)
+        rows = self.conn.execute(
+            "SELECT canonical_name FROM entities WHERE entity_type = ? AND canonical_name != '' LIMIT 800",
+            ("角色",),
+        ).fetchall()
+        known_characters = [r["canonical_name"] for r in rows] if rows else []
+        if not known_characters:
+            return []
+
+        # 统计每个角色在内容中的出现次数
+        char_counts = {}
+        for char_name in known_characters:
+            count = content.count(char_name)
+            if count > 0:
+                char_counts[char_name] = count
+
+        # 按出现次数排序,返回前 5 个
+        sorted_chars = sorted(char_counts.items(), key=lambda x: x[1], reverse=True)
+        return [char for char, _ in sorted_chars[:5]]
+
+    # ================== 核心功能 2:伏笔追踪索引 ==================
+
+    def sync_foreshadowing_from_state(self):
+        """从 state.json 同步伏笔数据到索引
+
+        触发时机:
+        - update_state.py 更新伏笔后调用
+        - --rebuild-index 批量重建时调用
+        """
+        if not self.state_file.exists():
+            print("❌ state.json 不存在,跳过伏笔同步")
+            return
+
+        # 读取 state.json
+        with open(self.state_file, 'r', encoding='utf-8') as f:
+            state = json.load(f)
+
+        current_chapter = state.get('progress', {}).get('current_chapter', 0)
+
+        plot_threads = state.get('plot_threads', {}) or {}
+
+        # 兼容新格式:plot_threads.foreshadowing = [{"content": "...", "status": "active", ...}, ...]
+        foreshadowing_items = plot_threads.get('foreshadowing', []) or []
+        active_count = 0
+        resolved_count = 0
+
+        for item in foreshadowing_items:
+            desc = item.get('description') or item.get('content') or ''
+            if not desc:
+                continue
+
+            raw_status = (item.get('status') or '').strip()
+            if raw_status in ['已回收', 'resolved']:
+                status = '已回收'
+                resolved_count += 1
+            else:
+                # 默认都视为未回收(兼容 active/未回收/pending/空)
+                status = '未回收'
+                active_count += 1
+
+            normalized = {
+                'description': desc,
+                'location': item.get('location', ''),
+                'characters': item.get('characters', []),
+                # 如果没有明确记录,至少给一个可用的默认值(避免紧急度恒为0)
+                'introduced_chapter': item.get('introduced_chapter') or item.get('planted_chapter') or 1,
+                'resolved_chapter': item.get('resolved_chapter', None),
+            }
+
+            self._index_foreshadowing(normalized, current_chapter, status=status)
+
+        self.conn.commit()
+        print(f"✅ 伏笔索引已同步:{active_count} 条活跃 + {resolved_count} 条已回收")
+
+    def _index_foreshadowing(self, plot: Dict, current_chapter: int, status: str):
+        """为单个伏笔建立索引"""
+
+        # 计算紧急度
+        urgency = self._calculate_urgency(plot, current_chapter)
+
+        # 提取地点和角色(如果有)
+        location = plot.get('location', '')
+        characters = plot.get('characters', [])
+
+        self.conn.execute("""
+            INSERT OR REPLACE INTO foreshadowing_index
+            (id, content, location, characters, introduced_chapter, resolved_chapter, status, urgency, updated_at)
+            VALUES ((SELECT id FROM foreshadowing_index WHERE content = ?), ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+        """, (
+            plot.get('description', ''),  # 用于查重
+            plot.get('description', ''),
+            location,
+            json.dumps(characters, ensure_ascii=False),
+            plot.get('introduced_chapter', 0),
+            plot.get('resolved_chapter', None),
+            status,
+            urgency
+        ))
+
+    def _calculate_urgency(self, plot: Dict, current_chapter: int) -> int:
+        """计算伏笔紧急度(0-100)
+
+        规则:
+        - 超过 100 章未回收 → 极度紧急(100)
+        - 超过 50 章未回收 → 中等紧急(60)
+        - 其他 → 正常(20)
+        """
+        introduced_ch = plot.get('introduced_chapter', 0)
+        chapters_pending = current_chapter - introduced_ch
+
+        if chapters_pending > 100:
+            return 100  # 极度紧急
+        elif chapters_pending > 50:
+            return 60   # 中等紧急
+        else:
+            return 20   # 正常
+
+    # ================== v4.0 实体同步(使用 entities_v3)==================
+
+    def sync_entities_from_state(self):
+        """从 state.json.entities_v3 同步实体到 entities/entity_aliases 表
+
+        v4.0 新增:取代旧的 sync_characters_from_state
+        数据源:state.json.entities_v3 + alias_index
+        """
+        if not self.state_file.exists():
+            print("❌ state.json 不存在,跳过实体同步")
+            return
+
+        with open(self.state_file, 'r', encoding='utf-8') as f:
+            state = json.load(f)
+
+        entities_v3 = state.get('entities_v3', {})
+        alias_index = state.get('alias_index', {})
+
+        # v4.0:索引层为派生数据,可直接重建(避免重复插入导致膨胀)
+        self.conn.execute("DELETE FROM entity_kv")
+        self.conn.execute("DELETE FROM entity_aliases")
+        self.conn.execute("DELETE FROM entity_history")
+        self.conn.execute("DELETE FROM entities")
+
+        entity_count = 0
+        alias_count = 0
+
+        # 遍历所有实体类型
+        for entity_type, entities in entities_v3.items():
+            for entity_id, entity_data in entities.items():
+                # 写入 entities 主表
+                canonical_name = entity_data.get('canonical_name', '')
+                tier = entity_data.get('tier', '')
+                desc = entity_data.get('desc', '')
+                created_chapter = entity_data.get('created_chapter', 0)
+
+                self.conn.execute("""
+                    INSERT OR REPLACE INTO entities
+                    (entity_id, entity_type, canonical_name, tier, desc, created_chapter, updated_at)
+                    VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+                """, (entity_id, entity_type, canonical_name, tier, desc, created_chapter))
+                entity_count += 1
+
+                # 写入实体 KV 属性
+                current = entity_data.get('current', {})
+                last_chapter = current.get("last_chapter", created_chapter) if isinstance(current, dict) else created_chapter
+                try:
+                    last_chapter = int(last_chapter)
+                except (TypeError, ValueError):
+                    last_chapter = int(created_chapter or 0)
+                for key, value in current.items():
+                    value_str = json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
+                    self.conn.execute("""
+                        INSERT OR REPLACE INTO entity_kv
+                        (entity_id, key, value, last_chapter)
+                        VALUES (?, ?, ?, ?)
+                    """, (entity_id, key, value_str, last_chapter))
+
+                # 写入历史记录
+                history = entity_data.get('history', [])
+                for record in history:
+                    chapter = record.get('chapter', 0)
+                    changes = record.get('changes', {})
+                    reasons = record.get('reasons', {})
+                    self.conn.execute("""
+                        INSERT OR IGNORE INTO entity_history
+                        (entity_id, chapter, changes_json, reasons_json)
+                        VALUES (?, ?, ?, ?)
+                    """, (entity_id, chapter, json.dumps(changes, ensure_ascii=False), json.dumps(reasons, ensure_ascii=False)))
+
+        # 同步别名索引
+        for alias, entries in alias_index.items():
+            # v4.0: entries 必须是数组(一对多)
+            if not isinstance(entries, list):
+                raise ValueError(
+                    f"alias_index 数据格式错误:期望 alias_index[{alias!r}] 为 list[{{type,id,...}}],实际为 {type(entries).__name__}"
+                )
+            for entry in entries:
+                entry_type = entry.get('type', '')
+                entry_id = entry.get('id', '')
+                first_seen = entry.get('first_seen_chapter', 0)
+                context = entry.get('context', '')
+
+                self.conn.execute("""
+                    INSERT OR REPLACE INTO entity_aliases
+                    (alias, entity_id, entity_type, first_seen_chapter, context)
+                    VALUES (?, ?, ?, ?, ?)
+                """, (alias, entry_id, entry_type, first_seen, context))
+                alias_count += 1
+
+        self.conn.commit()
+        print(f"✅ 实体索引已同步:{entity_count} 个实体,{alias_count} 个别名")
+
+    def query_entity_by_id(self, entity_id: str) -> Optional[Dict]:
+        """通过 entity_id 查询实体详情"""
+        cursor = self.conn.execute("""
+            SELECT entity_id, entity_type, canonical_name, tier, desc, created_chapter
+            FROM entities WHERE entity_id = ?
+        """, (entity_id,))
+        row = cursor.fetchone()
+        if not row:
+            return None
+
+        result = dict(row)
+
+        # 获取 KV 属性
+        cursor = self.conn.execute("""
+            SELECT key, value FROM entity_kv WHERE entity_id = ?
+        """, (entity_id,))
+        result['current'] = {}
+        for kv_row in cursor.fetchall():
+            try:
+                result['current'][kv_row['key']] = json.loads(kv_row['value'])
+            except json.JSONDecodeError:
+                result['current'][kv_row['key']] = kv_row['value']
+
+        # 获取别名
+        cursor = self.conn.execute("""
+            SELECT alias FROM entity_aliases WHERE entity_id = ?
+        """, (entity_id,))
+        result['aliases'] = [row['alias'] for row in cursor.fetchall()]
+
+        return result
+
+    def query_entities_by_alias(self, alias: str) -> List[Dict]:
+        """通过别名查询实体(支持一对多)"""
+        cursor = self.conn.execute("""
+            SELECT ea.entity_id, ea.entity_type, e.canonical_name, e.tier
+            FROM entity_aliases ea
+            LEFT JOIN entities e ON ea.entity_id = e.entity_id
+            WHERE ea.alias = ?
+        """, (alias,))
+        return [dict(row) for row in cursor.fetchall()]
+
+    def query_entities_by_type(self, entity_type: str, limit: int = 50) -> List[Dict]:
+        """按类型查询实体"""
+        cursor = self.conn.execute("""
+            SELECT entity_id, canonical_name, tier, desc
+            FROM entities
+            WHERE entity_type = ?
+            ORDER BY created_chapter DESC
+            LIMIT ?
+        """, (entity_type, limit))
+        return [dict(row) for row in cursor.fetchall()]
+
+    def sync_characters_from_state(self):
+        """从 state.json 同步角色数据到索引(v4.0 已废弃)
+
+        保留兼容:调用新的 sync_entities_from_state
+        """
+        # v4.0: 委托给新函数
+        self.sync_entities_from_state()
+
+    def _index_character(self, char: Dict, status: str = 'active'):
+        """为单个角色建立索引"""
+        description = char.get('description') or char.get('desc') or ''
+        tier = str(char.get('tier', '') or '').strip()
+        importance = char.get('importance') or ('major' if tier == '核心' else 'minor')
+
+        first_appearance = char.get('first_appearance_chapter', 0) or 0
+        try:
+            first_appearance = int(first_appearance)
+        except (TypeError, ValueError):
+            first_appearance = 0
+
+        if first_appearance == 0:
+            src = char.get('first_appearance')
+            if isinstance(src, str):
+                m = re.search(r'第(\d+)章', src)
+                if m:
+                    try:
+                        first_appearance = int(m.group(1))
+                    except ValueError:
+                        first_appearance = 0
+
+        last_appearance = char.get('last_appearance_chapter', 0) or first_appearance
+        try:
+            last_appearance = int(last_appearance)
+        except (TypeError, ValueError):
+            last_appearance = first_appearance
+
+        self.conn.execute("""
+            INSERT OR REPLACE INTO characters
+            (name, description, personality, importance, power_level,
+             first_appearance, last_appearance, status, updated_at)
+            VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+        """, (
+            char.get('name', ''),
+            description,
+            char.get('personality', ''),
+            importance,
+            char.get('power_level', ''),
+            first_appearance,
+            last_appearance,
+            status
+        ))
+
+    def mark_character_archived(self, name: str, archived_at: str = None):
+        """标记角色为已归档状态(Priority 2 修复)
+
+        Args:
+            name: 角色名
+            archived_at: 归档时间戳(ISO格式),默认当前时间
+        """
+        if archived_at is None:
+            from datetime import datetime
+            archived_at = datetime.now().isoformat()
+
+        self.conn.execute("""
+            UPDATE characters
+            SET status = 'archived', archived_at = ?, updated_at = CURRENT_TIMESTAMP
+            WHERE name = ?
+        """, (archived_at, name))
+        self.conn.commit()
+
+    def mark_character_active(self, name: str):
+        """恢复角色为活跃状态(与 mark_character_archived 对应)"""
+        self.conn.execute("""
+            UPDATE characters
+            SET status = 'active', archived_at = NULL, updated_at = CURRENT_TIMESTAMP
+            WHERE name = ?
+        """, (name,))
+        self.conn.commit()
+
+    def query_urgent_foreshadowing(self, threshold: int = 60) -> List[Dict]:
+        """查询紧急伏笔(urgency >= threshold)
+
+        Args:
+            threshold: 紧急度阈值(60=中等紧急,80=高度紧急,100=极度紧急)
+
+        Returns:
+            [{'content': '...', 'introduced_chapter': 45, 'urgency': 80}, ...]
+        """
+        cursor = self.conn.execute("""
+            SELECT content, introduced_chapter, urgency
+            FROM foreshadowing_index
+            WHERE status = '未回收' AND urgency >= ?
+            ORDER BY urgency DESC
+        """, (threshold,))
+
+        return [dict(row) for row in cursor.fetchall()]
+
+    def sync_relationships_from_state(self):
+        """从 state.json 同步关系数据到索引(v4.0: 使用 entity_id)
+
+        触发时机:
+        - extract_entities.py 更新关系后调用
+        - --rebuild-index 批量重建时调用
+
+        数据来源: state.json 的 structured_relationships 列表
+        """
+        if not self.state_file.exists():
+            print("❌ state.json 不存在,跳过关系同步")
+            return
+
+        # 读取 state.json
+        with open(self.state_file, 'r', encoding='utf-8') as f:
+            state = json.load(f)
+
+        # 获取结构化关系列表
+        relationships = state.get('structured_relationships', [])
+        if not relationships:
+            print("ℹ️ 无结构化关系数据")
+            return
+
+        count = 0
+        for rel in relationships:
+            # v4.0: 关系必须用 entity_id(chapter tags 是真相,避免 name 漂移)
+            char1_id = str(rel.get('char1_id', '') or '').strip()
+            char2_id = str(rel.get('char2_id', '') or '').strip()
+            char1_name = str(rel.get('char1_name', '') or '').strip()
+            char2_name = str(rel.get('char2_name', '') or '').strip()
+            rel_type = rel.get('type', 'ally')
+            intensity = rel.get('intensity', 50)
+            desc = rel.get('description', '')
+            last_chapter = rel.get('last_update_chapter', 0)
+
+            if not char1_id or not char2_id:
+                print("⚠️ 跳过无效关系(缺少 char1_id/char2_id)")
+                continue
+
+            # 补齐显示名(可选)
+            if not char1_name:
+                row = self.conn.execute("SELECT canonical_name FROM entities WHERE entity_id = ? LIMIT 1", (char1_id,)).fetchone()
+                char1_name = (row["canonical_name"] if row else "") or char1_id
+            if not char2_name:
+                row = self.conn.execute("SELECT canonical_name FROM entities WHERE entity_id = ? LIMIT 1", (char2_id,)).fetchone()
+                char2_name = (row["canonical_name"] if row else "") or char2_id
+
+            self.conn.execute("""
+                INSERT OR REPLACE INTO relationships
+                (id, char1_id, char2_id, char1_name, char2_name, relation_type, intensity, description, last_update_chapter, updated_at)
+                VALUES (
+                    (SELECT id FROM relationships WHERE char1_id = ? AND char2_id = ? AND relation_type = ?),
+                    ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP
+                )
+            """, (
+                char1_id, char2_id, rel_type,  # for subquery
+                char1_id, char2_id, char1_name, char2_name, rel_type, intensity, desc, last_chapter
+            ))
+            count += 1
+
+        self.conn.commit()
+        print(f"✅ 关系索引已同步:{count} 条关系")
+
+    def query_relationships(self, char_id: str = None, rel_type: str = None) -> List[Dict]:
+        """查询角色关系(v4.0: 使用 entity_id)
+
+        Args:
+            char_id: 角色 entity_id(可选,查该角色的所有关系)
+            rel_type: 关系类型(可选,过滤特定类型)
+
+        Returns:
+            [{'char1_id': '...', 'char2_id': '...', 'type': 'romance', 'intensity': 80, ...}, ...]
+        """
+        conditions = []
+        params = []
+
+        if char_id:
+            conditions.append("(char1_id = ? OR char2_id = ?)")
+            params.extend([char_id, char_id])
+
+        if rel_type:
+            conditions.append("relation_type = ?")
+            params.append(rel_type)
+
+        where_clause = " AND ".join(conditions) if conditions else "1=1"
+
+        cursor = self.conn.execute(f"""
+            SELECT char1_id, char2_id, char1_name, char2_name, relation_type, intensity, description, last_update_chapter
+            FROM relationships
+            WHERE {where_clause}
+            ORDER BY intensity DESC
+        """, params)
+
+        return [dict(row) for row in cursor.fetchall()]
+
+    # ================== 核心功能 3:模糊查询(Fuzzy Search via SQL LIKE)==================
+
+    def fuzzy_search_entity(self, keywords: List[str], entity_type: str = None) -> List[Dict]:
+        """模糊查询实体(v4.0 新增,支持多关键词 + 类型过滤)
+
+        Args:
+            keywords: 关键词列表,如 ["李", "女弟子"]
+            entity_type: 可选,过滤实体类型(角色/地点/物品/势力/招式)
+
+        Returns:
+            [{'entity_id': '...', 'canonical_name': '...', 'desc': '...', 'tier': '...'}, ...]
+        """
+        # 构建 WHERE 子句
+        conditions = []
+        params = []
+
+        for kw in keywords:
+            # 每个关键词在 canonical_name/desc 任一字段中出现即可
+            conditions.append("(e.canonical_name LIKE ? OR e.desc LIKE ? OR ea.alias LIKE ?)")
+            params.extend([f'%{kw}%', f'%{kw}%', f'%{kw}%'])
+
+        if entity_type:
+            conditions.append("e.entity_type = ?")
+            params.append(entity_type)
+
+        where_clause = " AND ".join(conditions)
+
+        query = f"""
+            SELECT DISTINCT e.entity_id, e.entity_type, e.canonical_name, e.tier, e.desc, e.created_chapter
+            FROM entities e
+            LEFT JOIN entity_aliases ea ON e.entity_id = ea.entity_id
+            WHERE {where_clause}
+            ORDER BY e.tier DESC, e.created_chapter DESC
+            LIMIT 20
+        """
+
+        cursor = self.conn.execute(query, params)
+        return [dict(row) for row in cursor.fetchall()]
+
+    def fuzzy_search_character(self, keywords: List[str]) -> List[Dict]:
+        """模糊查询角色(v4.0: 委托给 fuzzy_search_entity)
+
+        Args:
+            keywords: 关键词列表,如 ["李", "女弟子"]
+
+        Returns:
+            [{'entity_id': '...', 'canonical_name': '...', 'desc': '...', ...}, ...]
+        """
+        return self.fuzzy_search_entity(keywords, entity_type="角色")
+
+    # ================== 批量操作 ==================
+
+    def rebuild_all_indexes(self):
+        """批量重建所有历史章节的索引
+
+        使用场景:
+        - 索引系统首次上线
+        - 索引数据库损坏
+        """
+        if not self.chapters_dir.exists():
+            print("❌ 章节目录不存在")
+            return
+
+        # 获取所有章节文件
+        chapter_files = sorted(self.chapters_dir.rglob("第*.md"))
+
+        print(f"🔍 发现 {len(chapter_files)} 个章节文件,开始重建索引...")
+
+        seen = set()
+        for chapter_file in chapter_files:
+            # 提取章节编号
+            match = re.search(r'第(\d+)章', chapter_file.name)
+            if not match:
+                continue
+
+            chapter_num = int(match.group(1))
+            if chapter_num in seen:
+                continue
+            seen.add(chapter_num)
+
+            # 重建索引
+            self._rebuild_chapter_index(chapter_num, chapter_file)
+
+        # 同步伏笔索引
+        self.sync_foreshadowing_from_state()
+        self.sync_characters_from_state()
+        self.sync_relationships_from_state()
+
+        print(f"✅ 批量重建完成:{len(seen)} 章")
+
+    # ================== 查询与统计 ==================
+
+    def get_index_stats(self) -> Dict:
+        """获取索引统计信息(v4.0: 增加实体/别名统计)"""
+
+        # 章节统计
+        cursor = self.conn.execute("SELECT COUNT(*) as count FROM chapters")
+        chapter_count = cursor.fetchone()['count']
+
+        # 实体统计(v4.0 新增)
+        cursor = self.conn.execute("""
+            SELECT entity_type, COUNT(*) as count
+            FROM entities
+            GROUP BY entity_type
+        """)
+        entity_stats = {row['entity_type']: row['count'] for row in cursor.fetchall()}
+
+        # 别名统计(v4.0 新增)
+        cursor = self.conn.execute("SELECT COUNT(*) as count FROM entity_aliases")
+        alias_count = cursor.fetchone()['count']
+
+        # 伏笔统计
+        cursor = self.conn.execute("""
+            SELECT status, COUNT(*) as count
+            FROM foreshadowing_index
+            GROUP BY status
+        """)
+        foreshadowing_stats = {row['status']: row['count'] for row in cursor.fetchall()}
+
+        # 关系统计
+        cursor = self.conn.execute("SELECT COUNT(*) as count FROM relationships")
+        relationship_count = cursor.fetchone()['count']
+
+        # 数据库大小
+        db_size_kb = self.index_db.stat().st_size / 1024
+
+        return {
+            'chapter_count': chapter_count,
+            'entity_stats': entity_stats,
+            'alias_count': alias_count,
+            'foreshadowing_active': foreshadowing_stats.get('未回收', 0),
+            'foreshadowing_resolved': foreshadowing_stats.get('已回收', 0),
+            'relationship_count': relationship_count,
+            'db_size_kb': round(db_size_kb, 2)
+        }
+
+    def __del__(self):
+        """析构函数:关闭数据库连接"""
+        if hasattr(self, 'conn'):
+            self.conn.close()
+
+
+def main():
+    parser = argparse.ArgumentParser(description="结构化索引系统(取代向量化检索)")
+
+    # 更新操作
+    parser.add_argument("--update-chapter", type=int, metavar="NUM", help="更新单章索引")
+    parser.add_argument("--metadata", metavar="PATH", help="章节文件路径(配合 --update-chapter)")
+    parser.add_argument("--metadata-json", metavar="JSON", help="元数据 JSON 字符串(配合 --update-chapter,由 metadata-extractor agent 提供)")
+    parser.add_argument("--metadata-file", metavar="FILE", help="元数据 JSON 文件路径(配合 --update-chapter,Windows 推荐使用此参数)")
+
+    # 批量操作
+    parser.add_argument("--rebuild-index", action="store_true", help="批量重建所有索引")
+
+    # 查询操作
+    parser.add_argument("--query-location", metavar="LOCATION", help="查询地点相关章节")
+    parser.add_argument("--query-urgent-foreshadowing", action="store_true", help="查询紧急伏笔")
+    parser.add_argument("--fuzzy-search", nargs='+', metavar="KEYWORD", help="模糊查询角色(多个关键词)")
+
+    # 统计信息
+    parser.add_argument("--stats", action="store_true", help="显示索引统计信息")
+
+    # 项目路径
+    parser.add_argument("--project-root", metavar="PATH", help="项目根目录(默认为当前目录)")
+
+    args = parser.parse_args()
+
+    # 创建索引管理器
+    index = StructuredIndex(project_root=args.project_root)
+
+    # 执行操作
+    if args.update_chapter:
+        # 模式1:从 JSON 文件读取(Windows 推荐,避免 CLI 引号转义问题)
+        if args.metadata_file:
+            try:
+                metadata_file = Path(args.metadata_file)
+                if not metadata_file.exists():
+                    print(f"❌ 元数据文件不存在: {metadata_file}")
+                    return
+
+                with open(metadata_file, 'r', encoding='utf-8') as f:
+                    metadata = json.load(f)
+
+                # 验证必需字段
+                required_fields = ['title', 'location', 'characters', 'word_count', 'hash']
+                missing_fields = [f for f in required_fields if f not in metadata]
+
+                if missing_fields:
+                    print(f"❌ JSON 缺少必需字段: {', '.join(missing_fields)}")
+                    return
+
+                # 先同步实体(用于将 metadata.characters/name 解析为 entity_id)
+                index.sync_entities_from_state()
+
+                # 更新章节索引
+                index.index_chapter(args.update_chapter, metadata)
+
+                # 同步伏笔索引
+                index.sync_foreshadowing_from_state()
+                # bump_character_last_appearance_in_state 已删除(v4.0)
+                index.sync_relationships_from_state()
+
+            except json.JSONDecodeError as e:
+                print(f"❌ JSON 解析失败: {e}")
+                return
+
+        # 模式2:直接接收 JSON 字符串(Linux/macOS,或测试时使用)
+        elif args.metadata_json:
+            try:
+                metadata = json.loads(args.metadata_json)
+
+                # 验证必需字段
+                required_fields = ['title', 'location', 'characters', 'word_count', 'hash']
+                missing_fields = [f for f in required_fields if f not in metadata]
+
+                if missing_fields:
+                    print(f"❌ JSON 缺少必需字段: {', '.join(missing_fields)}")
+                    return
+
+                # 先同步实体(用于将 metadata.characters/name 解析为 entity_id)
+                index.sync_entities_from_state()
+
+                # 更新章节索引
+                index.index_chapter(args.update_chapter, metadata)
+
+                # 同步伏笔索引
+                index.sync_foreshadowing_from_state()
+                # bump_character_last_appearance_in_state 已删除(v4.0)
+                index.sync_relationships_from_state()
+
+            except json.JSONDecodeError as e:
+                print(f"❌ JSON 解析失败: {e}")
+                return
+
+        # 模式3:从章节文件提取元数据(旧模式,保持向后兼容)
+        elif args.metadata:
+            # 读取章节文件
+            chapter_file = Path(args.metadata)
+            if not chapter_file.exists():
+                print(f"❌ 章节文件不存在: {chapter_file}")
+                return
+
+            # 提取元数据
+            with open(chapter_file, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            metadata = index._extract_metadata_from_content(content, args.update_chapter)
+
+            # 先同步实体(用于将 metadata.characters/name 解析为 entity_id)
+            index.sync_entities_from_state()
+
+            # 更新章节索引
+            index.index_chapter(args.update_chapter, metadata)
+
+            # 同步伏笔索引
+            index.sync_foreshadowing_from_state()
+            # bump_character_last_appearance_in_state 已删除(v4.0)
+            index.sync_relationships_from_state()
+
+        else:
+            print("❌ 缺少参数:--metadata-file (推荐) / --metadata-json / --metadata")
+            return
+
+    elif args.rebuild_index:
+        index.rebuild_all_indexes()
+
+    elif args.query_location:
+        results = index.query_chapters_by_location(args.query_location)
+
+        if not results:
+            print(f"未找到地点相关章节: {args.query_location}")
+        else:
+            print(f"找到 {len(results)} 个相关章节:")
+            for chapter_num, title, characters in results:
+                print(f"  Ch{chapter_num}: {title} - 角色: {characters}")
+
+    elif args.query_urgent_foreshadowing:
+        results = index.query_urgent_foreshadowing(threshold=60)
+
+        if not results:
+            print("✅ 无紧急伏笔")
+        else:
+            print(f"⚠️ 检测到 {len(results)} 条紧急伏笔:")
+            for item in results:
+                print(f"  - {item['content'][:30]}...(第 {item['introduced_chapter']} 章埋设,紧急度 {item['urgency']}/100)")
+
+    elif args.fuzzy_search:
+        results = index.fuzzy_search_character(args.fuzzy_search)
+
+        if not results:
+            print(f"未找到匹配角色: {' + '.join(args.fuzzy_search)}")
+        else:
+            print(f"找到 {len(results)} 个匹配角色:")
+            for i, char in enumerate(results, 1):
+                # v4.0: 使用新字段名
+                name = char.get('canonical_name', char.get('name', ''))
+                desc = char.get('desc', char.get('description', ''))[:50]
+                tier = char.get('tier', '')
+                print(f"{i}. {name} [{tier}] - {desc}...")
+
+    elif args.stats:
+        stats = index.get_index_stats()
+
+        print("📊 索引统计信息:")
+        print(f"   章节索引: {stats['chapter_count']}")
+
+        # v4.0: 显示实体统计
+        entity_stats = stats.get('entity_stats', {})
+        if entity_stats:
+            entity_summary = ", ".join([f"{t}: {c}" for t, c in entity_stats.items()])
+            print(f"   实体索引: {entity_summary}")
+        print(f"   别名索引: {stats.get('alias_count', 0)}")
+
+        print(f"   伏笔索引: {stats['foreshadowing_active']} 条活跃 + {stats['foreshadowing_resolved']} 条已回收")
+        print(f"   关系索引: {stats['relationship_count']}")
+        print(f"   数据库大小: {stats['db_size_kb']} KB")
+
+    else:
+        parser.print_help()
+
+
+if __name__ == "__main__":
+    # Windows UTF-8 编码修复(仅在脚本直接运行时)
+    if sys.platform == 'win32':
+        import io
+        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+    main()

+ 120 - 0
.claude/scripts/token_budget_report.md

@@ -0,0 +1,120 @@
+======================================================================
+📊 章节创作 Token 预算分析
+======================================================================
+
+## 1. 参考文件 Token 消耗
+
+### 必须加载(每章)
+| 文件 | 字符数 | Token数 | 说明 |
+|------|-------|---------|------|
+| core-constraints.md | 1,356 | 1,076 | ✅ |
+| **小计** | - | **1,076** | - |
+
+### 按需加载(场景相关)
+| 文件 | Token数 | 触发条件 | 加载概率 |
+|------|---------|---------|---------|
+| combat-scenes.md | 1,647 | 战斗章节(约30%) | 30% |
+| emotion-psychology.md | 4,492 | 情感章节(约20%) | 20% |
+| dialogue-writing.md | 4,269 | 对话密集章节(约40%) | 40% |
+| scene-description.md | 4,520 | 复杂场景(约15%) | 15% |
+| polish-guide.md | 1,942 | 需要润色时(约50%) | 50% |
+| **加权平均** | **4,749** | - | - |
+
+### 首次加载(题材专项)
+| 文件 | Token数 | 说明 |
+|------|---------|------|
+| xuanhuan-cultivation.md | 6,966 | 仅首章 |
+| xuanhuan-power.md | 6,563 | 仅首章 |
+| xuanhuan-cool-points.md | 6,248 | 仅首章 |
+| **小计** | **19,777** | 摊销到全书 |
+
+## 2. Context Pack Token 消耗
+
+| 组件 | Token数 | 说明 |
+|------|---------|------|
+| chapter_outline | 728 | - |
+| protagonist_snapshot | 416 | - |
+| recent_summaries | 1,560 | - |
+| location_context | 312 | - |
+| appearing_characters | 936 | - |
+| urgent_foreshadowing | 312 | - |
+| worldview_skeleton | 1,560 | - |
+| power_system_skeleton | 832 | - |
+| **合计** | **6,656** | 第100章时 |
+
+## 3. 输出 Token 消耗
+
+| 组件 | 字符数 | Token数 |
+|------|-------|---------|
+| 章节正文 | 3000-5000 | 6000-10000 |
+| 章末标签 | ~350 | ~700 |
+| 章末摘要 | ~200 | ~400 |
+| **合计** | ~4550 | **~9,100** |
+
+## 4. 审查子代理 Token 消耗(每2章)
+
+| 子代理 | 输入 | 输出 | 合计 |
+|--------|------|------|------|
+| high-point-checker | 18,000 | 1,500 | 19,500 |
+| consistency-checker | 20,000 | 2,000 | 22,000 |
+| pacing-checker | 18,000 | 1,500 | 19,500 |
+| ooc-checker | 22,000 | 2,000 | 24,000 |
+| continuity-checker | 20,000 | 1,500 | 21,500 |
+| **5代理总计** | 98,000 | 8,500 | 106,500 |
+| **摊销/章** | - | - | **53,250** |
+
+======================================================================
+## 📋 单章 Token 预算汇总
+======================================================================
+
+### 输入 Token(上下文加载)
+- 必须加载参考文件: 1,076
+- Context Pack: 6,656
+- 按需场景参考(加权): 4,749
+- **输入小计: 12,481**
+
+### 输出 Token(内容生成)
+- 章节正文: ~8,000
+- 标签+摘要: ~1,100
+- **输出小计: 9,100**
+
+### 审查 Token(每章摊销)
+- 5个子代理/2章: 53,250
+
+======================================================================
+### 🎯 单章总预算: ~74,831 tokens
+======================================================================
+
+## 6. 成本估算(API 价格)
+
+| 模型 | 输入成本 | 输出成本 | 单章成本 | 500章成本 |
+|------|---------|---------|---------|----------|
+| Claude Sonnet | $0.0374 | $0.9353 | $0.9727 | $486.35 |
+| Claude Opus | $0.1872 | $4.6763 | $4.8635 | $2431.73 |
+
+## 7. 优化建议
+
+### 高优先级
+- [ ] 摘要压缩:recent_summaries 从5章压缩到3章,节省 ~600 tokens
+- [ ] 骨架精简:worldview/power_system 精简50%,节省 ~1,150 tokens
+- [ ] 审查合并:5个子代理合并为2个,节省 ~50,000 tokens/2章
+
+### 中优先级
+- [ ] 参考文件缓存:首次加载后缓存到上下文,避免重复传输
+- [ ] 增量 Context Pack:只传输变化部分
+
+### 低优先级(Claude 4.5 Opus 上下文充足)
+- [ ] 场景参考按需精简
+- [ ] 角色快照压缩
+
+======================================================================
+## 📊 500章总预算
+======================================================================
+
+- 单章平均: 74,831 tokens
+- 500章总计: 37,435,277 tokens (37.44M)
+- 首次加载题材: 19,777 tokens(一次性)
+
+### 预估成本
+- Claude Sonnet 500章: ~$486.35
+- Claude Opus 500章: ~$2431.73

+ 638 - 0
.claude/scripts/update_state.py

@@ -0,0 +1,638 @@
+#!/usr/bin/env python3
+"""
+安全的 state.json 更新脚本
+
+功能:
+1. 提供结构化的 state.json 更新接口
+2. 自动验证 JSON 格式和数据完整性
+3. 自动备份(带时间戳)
+4. 支持部分更新(不影响其他字段)
+5. 原子性操作(要么全部成功,要么全部回滚)
+
+使用方式:
+  # 更新主角状态
+  python update_state.py --protagonist-power "金丹" 3 "雷劫"
+
+  # 更新人际关系
+  python update_state.py --relationship "李雪" affection 95
+
+  # 记录伏笔
+  python update_state.py --add-foreshadowing "神秘玉佩的秘密" "未回收"
+
+  # 回收伏笔
+  python update_state.py --resolve-foreshadowing "天雷果的下落" 45
+
+  # 更新进度
+  python update_state.py --progress 45 198765
+
+  # 标记卷已规划
+  python update_state.py --volume-planned 1 --chapters-range 1-100
+
+  # 组合更新(原子性)
+  python update_state.py \
+    --protagonist-power "金丹" 3 "雷劫" \
+    --progress 45 198765 \
+    --relationship "李雪" affection 95 \
+    --add-foreshadowing "神秘玉佩" "未回收"
+
+安全特性:
+  - 自动备份原文件(.backup_TIMESTAMP.json)
+  - JSON 格式验证
+  - Schema 完整性检查
+  - 原子性操作(失败自动回滚)
+  - Dry-run 模式(--dry-run)
+"""
+
+import json
+import os
+import sys
+import argparse
+import shutil
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, Any, Optional
+
+# ============================================================================
+# 安全修复:导入安全工具函数(P1 MEDIUM)
+# ============================================================================
+from security_utils import create_secure_directory, atomic_write_json, restore_from_backup
+from project_locator import resolve_state_file
+
+# Windows 编码兼容性修复
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+class StateUpdater:
+    """state.json 安全更新器"""
+
+    def __init__(self, state_file: str, dry_run: bool = False):
+        self.state_file = state_file
+        self.dry_run = dry_run
+        self.backup_file = None
+        self.state = None
+
+    def _validate_schema(self, state: Dict) -> bool:
+        """验证 state.json 的基本结构 (v5.0)"""
+        required_keys = [
+            "project_info",
+            "progress",
+            "protagonist_state",
+            "relationships",
+            "world_settings",
+            "plot_threads",
+            "review_checkpoints"
+        ]
+
+        for key in required_keys:
+            if key not in state:
+                print(f"❌ 缺少必需字段: {key}")
+                return False
+
+        # 验证嵌套结构(支持两种格式:嵌套和平铺)
+        ps = state["protagonist_state"]
+        # power 字段:支持 power.realm 或直接 realm
+        has_nested_power = "power" in ps and isinstance(ps.get("power"), dict)
+        has_flat_power = "realm" in ps
+        if not (has_nested_power or has_flat_power):
+            print(f"❌ 缺少 protagonist_state.power 或 protagonist_state.realm 字段")
+            return False
+
+        # location 字段:支持 location.current 或直接 location
+        has_nested_location = isinstance(ps.get("location"), dict) and "current" in ps.get("location", {})
+        has_flat_location = isinstance(ps.get("location"), str)
+        if not (has_nested_location or has_flat_location):
+            print(f"❌ 缺少 protagonist_state.location 字段")
+            return False
+
+        # 验证并补全 strand_tracker 结构(兼容旧 state.json)
+        tracker = state.get("strand_tracker")
+        if tracker is None or not isinstance(tracker, dict):
+            if tracker is None:
+                print("⚠️ strand_tracker 缺失,已自动补全默认结构")
+            else:
+                print("⚠️ strand_tracker 类型异常,已重置默认结构")
+            state["strand_tracker"] = {
+                "last_quest_chapter": 0,
+                "last_fire_chapter": 0,
+                "last_constellation_chapter": 0,
+                "current_dominant": "quest",
+                "chapters_since_switch": 0,
+                "history": [],
+            }
+        else:
+            tracker.setdefault("last_quest_chapter", 0)
+            tracker.setdefault("last_fire_chapter", 0)
+            tracker.setdefault("last_constellation_chapter", 0)
+            tracker.setdefault("current_dominant", "quest")
+            tracker.setdefault("chapters_since_switch", 0)
+            tracker.setdefault("history", [])
+
+        return True
+
+    def load(self) -> bool:
+        """加载并验证 state.json"""
+        if not os.path.exists(self.state_file):
+            print(f"❌ 状态文件不存在: {self.state_file}")
+            return False
+
+        try:
+            with open(self.state_file, 'r', encoding='utf-8') as f:
+                self.state = json.load(f)
+
+            if not self._validate_schema(self.state):
+                print("❌ state.json 结构不完整,请检查")
+                return False
+
+            return True
+
+        except json.JSONDecodeError as e:
+            print(f"❌ JSON 格式错误: {e}")
+            return False
+
+    def backup(self) -> bool:
+        """备份当前 state.json"""
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        backup_dir = Path(self.state_file).parent / "backups"
+        # ============================================================================
+        # 安全修复:使用安全目录创建函数(P1 MEDIUM)
+        # 原代码: backup_dir.mkdir(exist_ok=True)
+        # 漏洞: 未设置权限,使用OS默认(可能为755,允许同组用户读取)
+        # ============================================================================
+        create_secure_directory(str(backup_dir))
+
+        self.backup_file = backup_dir / f"state.backup_{timestamp}.json"
+
+        try:
+            shutil.copy2(self.state_file, self.backup_file)
+            print(f"✅ 已备份: {self.backup_file}")
+            return True
+        except Exception as e:
+            print(f"❌ 备份失败: {e}")
+            return False
+
+    def save(self) -> bool:
+        """保存更新后的 state.json(原子化写入)"""
+        if self.dry_run:
+            print("\n⚠️  Dry-run 模式,不执行实际写入")
+            print("\n📄 预览更新后的内容:")
+            print(json.dumps(self.state, ensure_ascii=False, indent=2))
+            return True
+
+        try:
+            # 使用集中式原子写入(带 filelock + 自动备份)
+            atomic_write_json(self.state_file, self.state, use_lock=True, backup=True)
+            print(f"✅ 已保存(原子化): {self.state_file}")
+            return True
+
+        except Exception as e:
+            print(f"❌ 保存失败: {e}")
+            # 尝试从备份恢复
+            if restore_from_backup(self.state_file):
+                print(f"✅ 已从备份恢复")
+            return False
+
+    def update_protagonist_power(self, realm: str, layer: int, bottleneck: str):
+        """更新主角实力(支持嵌套和平铺两种格式)"""
+        ps = self.state["protagonist_state"]
+        # 检测当前格式
+        if "power" in ps and isinstance(ps.get("power"), dict):
+            # 嵌套格式
+            ps["power"] = {
+                "realm": realm,
+                "layer": layer,
+                "bottleneck": bottleneck if bottleneck != "null" else None
+            }
+        else:
+            # 平铺格式
+            ps["realm"] = realm
+            ps["layer"] = layer
+            ps["bottleneck"] = bottleneck if bottleneck != "null" else None
+        print(f"📝 更新主角实力: {realm} {layer}层, 瓶颈: {bottleneck}")
+
+    def update_protagonist_location(self, location: str, chapter: int):
+        """更新主角位置(支持嵌套和平铺两种格式)"""
+        ps = self.state["protagonist_state"]
+        # 检测当前格式
+        if isinstance(ps.get("location"), dict):
+            # 嵌套格式
+            ps["location"] = {
+                "current": location,
+                "last_chapter": chapter
+            }
+        else:
+            # 平铺格式
+            ps["location"] = location
+            ps["location_since_chapter"] = chapter
+        print(f"📝 更新主角位置: {location}(第{chapter}章)")
+
+    def update_golden_finger(self, name: str, level: int, cooldown: int):
+        """更新金手指状态"""
+        ps = self.state.setdefault("protagonist_state", {})
+        golden_finger = ps.get("golden_finger")
+        if not isinstance(golden_finger, dict):
+            golden_finger = {}
+            ps["golden_finger"] = golden_finger
+
+        golden_finger.setdefault("skills", [])
+        golden_finger["name"] = name
+        golden_finger["level"] = level
+        golden_finger["cooldown"] = cooldown
+        print(f"📝 更新金手指: {name} Lv.{level}, 冷却: {cooldown}天")
+
+    def update_relationship(self, char_name: str, key: str, value: Any):
+        """更新人际关系"""
+        if char_name not in self.state["relationships"]:
+            self.state["relationships"][char_name] = {}
+
+        self.state["relationships"][char_name][key] = value
+        print(f"📝 更新关系: {char_name}.{key} = {value}")
+
+    def add_foreshadowing(self, content: str, status: str = "未回收"):
+        """添加伏笔"""
+        if "foreshadowing" not in self.state["plot_threads"]:
+            self.state["plot_threads"]["foreshadowing"] = []
+
+        # 检查是否已存在
+        for item in self.state["plot_threads"]["foreshadowing"]:
+            if item.get("content") == content:
+                print(f"⚠️  伏笔已存在: {content}")
+                return
+
+        # 归一化状态,避免 "待回收/进行中/active/pending" 等混用导致下游过滤漏掉
+        raw_status = "" if status is None else str(status).strip()
+        raw_status_lower = raw_status.lower()
+        if raw_status in {"已回收", "已完成", "已解决", "完成"} or raw_status_lower in {"resolved", "done", "complete"}:
+            status = "已回收"
+        elif (
+            raw_status in {"未回收", "待回收", "进行中", "未解决"}
+            or raw_status_lower in {"active", "pending"}
+            or not raw_status
+        ):
+            status = "未回收"
+        else:
+            status = "未回收"
+
+        planted_chapter = int(self.state.get("progress", {}).get("current_chapter", 0) or 0)
+        if planted_chapter <= 0:
+            planted_chapter = 1
+            print("? 未找到有效 progress.current_chapter,默认 planted_chapter=1")
+
+        target_chapter = planted_chapter + 100
+
+        self.state["plot_threads"]["foreshadowing"].append({
+            "content": content,
+            "status": status,
+            "added_at": datetime.now().strftime("%Y-%m-%d"),
+            "planted_chapter": planted_chapter,
+            "target_chapter": target_chapter,
+            "tier": "支线"
+        })
+        print(f"📝 添加伏笔: {content}({status})")
+
+    def resolve_foreshadowing(self, content: str, chapter: int):
+        """回收伏笔"""
+        if "foreshadowing" not in self.state["plot_threads"]:
+            print(f"❌ 未找到伏笔列表")
+            return
+
+        for item in self.state["plot_threads"]["foreshadowing"]:
+            if item.get("content") == content:
+                item["status"] = "已回收"
+                item["resolved_chapter"] = chapter
+                item["resolved_at"] = datetime.now().strftime("%Y-%m-%d")
+                print(f"📝 回收伏笔: {content}(第{chapter}章)")
+                return
+
+        print(f"⚠️  未找到伏笔: {content}")
+
+    def update_progress(self, current_chapter: int, total_words: int):
+        """更新创作进度"""
+        self.state["progress"]["current_chapter"] = current_chapter
+        self.state["progress"]["total_words"] = total_words
+        self.state["progress"]["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        print(f"📝 更新进度: 第{current_chapter}章, 总字数: {total_words}")
+
+    def mark_volume_planned(self, volume: int, chapters_range: str):
+        """标记卷已规划"""
+        if "volumes_planned" not in self.state["progress"]:
+            self.state["progress"]["volumes_planned"] = []
+
+        # 检查是否已存在
+        for item in self.state["progress"]["volumes_planned"]:
+            if item.get("volume") == volume:
+                print(f"⚠️  第{volume}卷已规划,更新章节范围")
+                item["chapters_range"] = chapters_range
+                item["updated_at"] = datetime.now().strftime("%Y-%m-%d")
+                return
+
+        self.state["progress"]["volumes_planned"].append({
+            "volume": volume,
+            "chapters_range": chapters_range,
+            "planned_at": datetime.now().strftime("%Y-%m-%d")
+        })
+        print(f"📝 标记第{volume}卷已规划: 第{chapters_range}章")
+
+    def add_review_checkpoint(self, chapters_range: str, report_file: str):
+        """添加审查记录"""
+        if "review_checkpoints" not in self.state:
+            self.state["review_checkpoints"] = []
+
+        self.state["review_checkpoints"].append({
+            "chapters": chapters_range,
+            "report": report_file,
+            "reviewed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        })
+        print(f"📝 添加审查记录: 第{chapters_range}章 → {report_file}")
+
+    def update_strand_tracker(self, strand: str, chapter: int):
+        """更新主导情节线(Strand Weave系统)"""
+        # 验证 strand 参数
+        valid_strands = ["quest", "fire", "constellation"]
+        if strand.lower() not in valid_strands:
+            print(f"❌ 无效的情节线类型: {strand}(有效值: quest, fire, constellation)")
+            return False
+
+        strand = strand.lower()
+
+        # 初始化 strand_tracker(如果不存在)
+        if "strand_tracker" not in self.state:
+            self.state["strand_tracker"] = {
+                "last_quest_chapter": 0,
+                "last_fire_chapter": 0,
+                "last_constellation_chapter": 0,
+                "current_dominant": None,
+                "chapters_since_switch": 0,
+                "history": []
+            }
+
+        tracker = self.state["strand_tracker"]
+
+        # 更新对应 strand 的最后章节
+        tracker[f"last_{strand}_chapter"] = chapter
+
+        # 判断是否切换 strand
+        if tracker.get("current_dominant") != strand:
+            tracker["current_dominant"] = strand
+            tracker["chapters_since_switch"] = 1
+        else:
+            tracker["chapters_since_switch"] += 1
+
+        # 添加到历史记录
+        tracker["history"].append({
+            "chapter": chapter,
+            "dominant": strand
+        })
+
+        # 只保留最近50章的历史(避免文件过大)
+        if len(tracker["history"]) > 50:
+            tracker["history"] = tracker["history"][-50:]
+
+        print(f"✅ strand_tracker 已更新")
+        print(f"   - 第{chapter}章主导情节线: {strand}")
+        print(f"   - 该情节线已连续{tracker['chapters_since_switch']}章")
+
+        return True
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="安全更新 state.json",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 更新主角实力
+  python update_state.py --protagonist-power "金丹" 3 "雷劫"
+
+  # 更新人际关系
+  python update_state.py --relationship "李雪" affection 95
+
+  # 添加伏笔
+  python update_state.py --add-foreshadowing "神秘玉佩的秘密" "未回收"
+
+  # 回收伏笔
+  python update_state.py --resolve-foreshadowing "天雷果的下落" 45
+
+  # 更新进度
+  python update_state.py --progress 45 198765
+
+  # 标记卷已规划
+  python update_state.py --volume-planned 1 --chapters-range "1-100"
+
+  # 组合更新(原子性)
+  python update_state.py \
+    --protagonist-power "金丹" 3 "雷劫" \
+    --progress 45 198765 \
+    --relationship "李雪" affection 95
+        """
+    )
+
+    parser.add_argument(
+        '--project-root',
+        default=None,
+        help='项目根目录(包含 .webnovel/state.json)。不提供时自动搜索(支持 webnovel-project/ 与父目录)。'
+    )
+
+    parser.add_argument(
+        '--state-file',
+        default=None,
+        help='state.json 文件路径(可选)。不提供时从项目根目录自动定位为 .webnovel/state.json。'
+    )
+
+    parser.add_argument(
+        '--dry-run',
+        action='store_true',
+        help='预览模式,不执行实际写入'
+    )
+
+    # 主角状态更新
+    parser.add_argument(
+        '--protagonist-power',
+        nargs=3,
+        metavar=('REALM', 'LAYER', 'BOTTLENECK'),
+        help='更新主角实力(境界 层数 瓶颈)'
+    )
+
+    parser.add_argument(
+        '--protagonist-location',
+        nargs=2,
+        metavar=('LOCATION', 'CHAPTER'),
+        help='更新主角位置(地点 章节号)'
+    )
+
+    parser.add_argument(
+        '--golden-finger',
+        nargs=3,
+        metavar=('NAME', 'LEVEL', 'COOLDOWN'),
+        help='更新金手指(名称 等级 冷却天数)'
+    )
+
+    # 人际关系更新
+    parser.add_argument(
+        '--relationship',
+        nargs=3,
+        action='append',
+        metavar=('CHAR_NAME', 'KEY', 'VALUE'),
+        help='更新人际关系(角色名 属性 值)'
+    )
+
+    # 伏笔管理
+    parser.add_argument(
+        '--add-foreshadowing',
+        nargs=2,
+        metavar=('CONTENT', 'STATUS'),
+        help='添加伏笔(内容 状态)'
+    )
+
+    parser.add_argument(
+        '--resolve-foreshadowing',
+        nargs=2,
+        metavar=('CONTENT', 'CHAPTER'),
+        help='回收伏笔(内容 章节号)'
+    )
+
+    # 进度更新
+    parser.add_argument(
+        '--progress',
+        nargs=2,
+        type=int,
+        metavar=('CHAPTER', 'WORDS'),
+        help='更新进度(当前章节 总字数)'
+    )
+
+    # 卷规划
+    parser.add_argument(
+        '--volume-planned',
+        type=int,
+        metavar='VOLUME',
+        help='标记卷已规划(卷号)'
+    )
+
+    parser.add_argument(
+        '--chapters-range',
+        metavar='RANGE',
+        help='章节范围(如 "1-100")'
+    )
+
+    # 审查记录
+    parser.add_argument(
+        '--add-review',
+        nargs=2,
+        metavar=('CHAPTERS_RANGE', 'REPORT_FILE'),
+        help='添加审查记录(章节范围 报告文件)'
+    )
+
+    # Strand Tracker 更新
+    parser.add_argument(
+        '--strand-dominant',
+        nargs=2,
+        metavar=('STRAND', 'CHAPTER'),
+        help='更新主导情节线(quest/fire/constellation 章节号)'
+    )
+
+    args = parser.parse_args()
+
+    # 如果没有任何更新参数,显示帮助并退出
+    if not any([
+        args.protagonist_power,
+        args.protagonist_location,
+        args.golden_finger,
+        args.relationship,
+        args.add_foreshadowing,
+        args.resolve_foreshadowing,
+        args.progress,
+        args.volume_planned,
+        args.add_review,
+        args.strand_dominant
+    ]):
+        parser.print_help()
+        sys.exit(1)
+
+    # 解析 state.json 路径(支持从仓库根目录运行)
+    state_file_path = resolve_state_file(args.state_file, explicit_project_root=args.project_root)
+
+    # 创建更新器
+    updater = StateUpdater(str(state_file_path), args.dry_run)
+
+    # 加载状态文件
+    if not updater.load():
+        sys.exit(1)
+
+    # 备份(除非是 dry-run)
+    if not args.dry_run:
+        if not updater.backup():
+            sys.exit(1)
+
+    print("\n📝 开始更新...")
+
+    # 执行更新操作
+    try:
+        if args.protagonist_power:
+            realm, layer, bottleneck = args.protagonist_power
+            updater.update_protagonist_power(realm, int(layer), bottleneck)
+
+        if args.protagonist_location:
+            location, chapter = args.protagonist_location
+            updater.update_protagonist_location(location, int(chapter))
+
+        if args.golden_finger:
+            name, level, cooldown = args.golden_finger
+            updater.update_golden_finger(name, int(level), int(cooldown))
+
+        if args.relationship:
+            for char_name, key, value in args.relationship:
+                # 尝试转换为数字
+                try:
+                    value = int(value)
+                except ValueError:
+                    pass
+                updater.update_relationship(char_name, key, value)
+
+        if args.add_foreshadowing:
+            content, status = args.add_foreshadowing
+            updater.add_foreshadowing(content, status)
+
+        if args.resolve_foreshadowing:
+            content, chapter = args.resolve_foreshadowing
+            updater.resolve_foreshadowing(content, int(chapter))
+
+        if args.progress:
+            chapter, words = args.progress
+            updater.update_progress(chapter, words)
+
+        if args.volume_planned:
+            if not args.chapters_range:
+                print("❌ --volume-planned 需要 --chapters-range 参数")
+                sys.exit(1)
+            updater.mark_volume_planned(args.volume_planned, args.chapters_range)
+
+        if args.add_review:
+            chapters_range, report_file = args.add_review
+            updater.add_review_checkpoint(chapters_range, report_file)
+
+        # Strand Tracker 更新
+        if args.strand_dominant:
+            strand, chapter = args.strand_dominant
+            updater.update_strand_tracker(strand, int(chapter))
+
+        # 保存更新
+        if not updater.save():
+            sys.exit(1)
+
+        print("\n✅ 更新完成!")
+
+        if not args.dry_run:
+            print(f"\n💡 提示:")
+            print(f"  - 原文件已备份: {updater.backup_file}")
+            print(f"  - 如需回滚,可复制备份文件到 {updater.state_file}")
+
+    except Exception as e:
+        print(f"\n❌ 更新失败: {e}")
+        if updater.backup_file and os.path.exists(updater.backup_file):
+            print(f"🔄 正在回滚...")
+            shutil.copy2(updater.backup_file, updater.state_file)
+            print(f"✅ 已回滚到备份版本")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()

+ 504 - 0
.claude/scripts/workflow_manager.py

@@ -0,0 +1,504 @@
+#!/usr/bin/env python3
+"""
+工作流状态管理器
+- 追踪命令执行状态
+- 检测中断点
+- 提供恢复策略
+"""
+
+import json
+import os
+import sys
+import subprocess
+from datetime import datetime
+from pathlib import Path
+
+# ============================================================================
+# 安全修复:导入安全工具函数(P1 MEDIUM)
+# ============================================================================
+from security_utils import create_secure_directory, atomic_write_json
+from project_locator import resolve_project_root
+from chapter_paths import default_chapter_draft_path, find_chapter_file
+
+# UTF-8 编码修复(Windows兼容)
+if sys.platform == 'win32':
+    import io
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+
+def find_project_root():
+    """解析项目根目录(包含 .webnovel/state.json)"""
+    return resolve_project_root()
+
+def get_workflow_state_path():
+    """获取 workflow_state.json 的完整路径"""
+    project_root = find_project_root()
+    return project_root / '.webnovel' / 'workflow_state.json'
+
+def start_task(command, args):
+    """开始新任务"""
+    state = load_state()
+    state['current_task'] = {
+        'command': command,
+        'args': args,
+        'started_at': datetime.now().isoformat(),
+        'last_heartbeat': datetime.now().isoformat(),
+        'status': 'running',
+        'current_step': None,
+        'completed_steps': [],
+        'pending_steps': get_pending_steps(command),
+        'artifacts': {
+            'chapter_file': {},
+            'git_status': {},
+            'state_json_modified': False,
+            'entities_appeared': False,
+            'review_completed': False
+        }
+    }
+    save_state(state)
+    print(f"✅ 任务已启动: {command} {json.dumps(args, ensure_ascii=False)}")
+
+def start_step(step_id, step_name, progress_note=None):
+    """标记Step开始"""
+    state = load_state()
+    if not state.get('current_task'):
+        print("⚠️ 无活动任务,请先使用 start-task")
+        return
+
+    state['current_task']['current_step'] = {
+        'id': step_id,
+        'name': step_name,
+        'status': 'in_progress',
+        'started_at': datetime.now().isoformat(),
+        'progress_note': progress_note
+    }
+    state['current_task']['last_heartbeat'] = datetime.now().isoformat()
+    save_state(state)
+    print(f"▶️  {step_id} 开始: {step_name}")
+
+def complete_step(step_id, artifacts_json=None):
+    """标记Step完成"""
+    state = load_state()
+    if not state.get('current_task') or not state['current_task'].get('current_step'):
+        print("⚠️ 无活动Step")
+        return
+
+    current_step = state['current_task']['current_step']
+    current_step['status'] = 'completed'
+    current_step['completed_at'] = datetime.now().isoformat()
+
+    if artifacts_json:
+        try:
+            artifacts = json.loads(artifacts_json)
+            current_step['artifacts'] = artifacts
+            # 更新task级别的artifacts
+            state['current_task']['artifacts'].update(artifacts)
+        except json.JSONDecodeError as e:
+            print(f"⚠️ Artifacts JSON解析失败: {e}")
+
+    state['current_task']['completed_steps'].append(current_step)
+    state['current_task']['current_step'] = None
+    state['current_task']['last_heartbeat'] = datetime.now().isoformat()
+    save_state(state)
+    print(f"✅ {step_id} 完成")
+
+def complete_task(final_artifacts_json=None):
+    """标记任务完成"""
+    state = load_state()
+    if not state.get('current_task'):
+        print("⚠️ 无活动任务")
+        return
+
+    state['current_task']['status'] = 'completed'
+    state['current_task']['completed_at'] = datetime.now().isoformat()
+
+    if final_artifacts_json:
+        try:
+            final_artifacts = json.loads(final_artifacts_json)
+            state['current_task']['artifacts'].update(final_artifacts)
+        except json.JSONDecodeError as e:
+            print(f"⚠️ Final artifacts JSON解析失败: {e}")
+
+    # 保存到历史记录
+    state['last_stable_state'] = extract_stable_state(state['current_task'])
+    if 'history' not in state:
+        state['history'] = []
+    state['history'].append({
+        'task_id': f"task_{len(state['history']) + 1:03d}",
+        'command': state['current_task']['command'],
+        'chapter': state['current_task']['args'].get('chapter_num'),
+        'status': 'completed',
+        'completed_at': state['current_task']['completed_at']
+    })
+
+    # 清除当前任务
+    state['current_task'] = None
+    save_state(state)
+    print(f"🎉 任务完成")
+
+def detect_interruption():
+    """检测中断状态"""
+    state = load_state()
+    if not state or 'current_task' not in state or state['current_task'] is None:
+        return None  # 无中断任务
+
+    task = state['current_task']
+    if task['status'] == 'completed':
+        return None  # 任务已完成
+
+    # 判断中断原因
+    last_heartbeat = datetime.fromisoformat(task['last_heartbeat'])
+    elapsed = (datetime.now() - last_heartbeat).total_seconds()
+
+    interrupt_info = {
+        'command': task['command'],
+        'args': task['args'],
+        'current_step': task['current_step'],
+        'completed_steps': task['completed_steps'],
+        'elapsed_seconds': elapsed,
+        'artifacts': task['artifacts'],
+        'started_at': task['started_at']
+    }
+
+    return interrupt_info
+
+def analyze_recovery_options(interrupt_info):
+    """分析恢复选项(基于中断点)"""
+    current_step = interrupt_info['current_step']
+    command = interrupt_info['command']
+    chapter_num = interrupt_info['args'].get('chapter_num', '?')
+
+    if not current_step:
+        # 任务刚开始就中断
+        return [{
+            'option': 'A',
+            'label': '从头开始',
+            'risk': 'low',
+            'description': '重新执行完整流程',
+            'actions': [
+                f"删除 workflow_state.json 当前任务",
+                f"执行 /{command} {chapter_num}"
+            ]
+        }]
+
+    step_id = current_step['id']
+
+    # 基于Step ID的恢复策略
+    if step_id == 'Step 1':
+        # Step 1中断:无副作用
+        return [{
+            'option': 'A',
+            'label': '从Step 1重新开始',
+            'risk': 'low',
+            'description': '重新加载上下文',
+            'actions': [
+                f"清理中断状态",
+                f"执行 /{command} {chapter_num}"
+            ]
+        }]
+
+    elif step_id == 'Step 2':
+        # Step 2中断:可能有半成品文件
+        chapter_file = interrupt_info['artifacts'].get('chapter_file', {})
+
+        # 使用 chapter_paths 模块定位章节文件(兼容新旧目录结构)
+        project_root = find_project_root()
+        existing_chapter = find_chapter_file(project_root, chapter_num)
+        draft_path = None
+        if existing_chapter:
+            chapter_path = str(existing_chapter.relative_to(project_root))
+        else:
+            # 如果不存在,使用新格式的默认路径
+            draft_path = default_chapter_draft_path(project_root, chapter_num)
+            chapter_path = str(draft_path.relative_to(project_root))
+
+        options = [{
+            'option': 'A',
+            'label': '删除半成品,从Step 1重新开始',
+            'risk': 'low',
+            'description': f"清理 {chapter_path},重新生成章节",
+            'actions': [
+                f"删除 {chapter_path}(如存在)",
+                f"清理 Git 暂存区",
+                f"清理中断状态",
+                f"执行 /{command} {chapter_num}"
+            ]
+        }]
+
+        # 检查文件是否存在
+        candidate = existing_chapter or draft_path
+        if candidate and candidate.exists():
+            options.append({
+                'option': 'B',
+                'label': '回滚到上一章',
+                'risk': 'medium',
+                'description': '丢弃所有当前章节进度',
+                'actions': [
+                    f"git reset --hard ch{(chapter_num-1):04d}",
+                    f"清理中断状态",
+                    "重新决定是否继续Ch{chapter_num}"
+                ]
+            })
+
+        return options
+
+    elif step_id == 'Step 3':
+        # Step 3 中断:审查未完成
+        return [
+            {
+                'option': 'A',
+                'label': '重新执行审查',
+                'risk': 'medium',
+                'description': '重新调用5个审查员(并行)',
+                'actions': [
+                    "重新调用5个审查员(并行)",
+                    "生成审查报告",
+                    "继续 Step 4 润色"
+                ]
+            },
+            {
+                'option': 'B',
+                'label': '跳过审查,直接润色',
+                'risk': 'low',
+                'description': '不进行审查,可后续用 /webnovel-review 补审',
+                'actions': [
+                    "标记审查为已跳过",
+                    "继续 Step 4 润色"
+                ]
+            }
+        ]
+
+    elif step_id == 'Step 4':
+        # Step 4 中断:润色中
+        project_root = find_project_root()
+        existing_chapter = find_chapter_file(project_root, chapter_num)
+        draft_path = None
+        if existing_chapter:
+            chapter_path = str(existing_chapter.relative_to(project_root))
+        else:
+            draft_path = default_chapter_draft_path(project_root, chapter_num)
+            chapter_path = str(draft_path.relative_to(project_root))
+
+        return [
+            {
+                'option': 'A',
+                'label': '继续润色',
+                'risk': 'low',
+                'description': f"继续润色 {chapter_path},完成后进入 Step 5",
+                'actions': [
+                    f"打开并继续润色 {chapter_path}",
+                    "保存文件",
+                    "继续 Step 5(Data Agent)"
+                ]
+            },
+            {
+                'option': 'B',
+                'label': '删除润色稿,从 Step 2 重写',
+                'risk': 'medium',
+                'description': f"删除 {chapter_path},重新生成章节内容",
+                'actions': [
+                    f"删除 {chapter_path}",
+                    "清理 Git 暂存区",
+                    "清理中断状态",
+                    f"执行 /{command} {chapter_num}"
+                ]
+            }
+        ]
+
+    elif step_id == 'Step 5':
+        # Step 5 中断:Data Agent 处理中
+        return [{
+            'option': 'A',
+            'label': '从 Step 5 重新开始',
+            'risk': 'low',
+            'description': '重新运行 Data Agent(幂等操作)',
+            'actions': [
+                "重新调用 Data Agent",
+                "继续 Step 6(Git 备份)"
+            ]
+        }]
+
+    elif step_id == 'Step 6':
+        # Step 6 中断:Git 未提交
+        return [
+            {
+                'option': 'A',
+                'label': '继续 Git 提交',
+                'risk': 'low',
+                'description': '完成未完成的 Git commit + tag',
+                'actions': [
+                    "检查 Git 暂存区",
+                    "重新执行 backup_manager.py",
+                    "继续完成工作流追踪(complete-task)"
+                ]
+            },
+            {
+                'option': 'B',
+                'label': '回滚 Git 改动',
+                'risk': 'medium',
+                'description': '丢弃暂存区所有改动',
+                'actions': [
+                    "git reset HEAD .",
+                    f"删除第{chapter_num}章文件",
+                    "清理中断状态"
+                ]
+            }
+        ]
+
+    # 默认选项
+    return [{
+        'option': 'A',
+        'label': '从头开始',
+        'risk': 'low',
+        'description': '重新执行完整流程',
+        'actions': [
+            f"清理所有中断artifacts",
+            f"执行 /{command} {chapter_num}"
+        ]
+    }]
+
+def cleanup_artifacts(chapter_num):
+    """清理半成品artifacts"""
+    artifacts_cleaned = []
+
+    project_root = find_project_root()
+
+    # 删除章节文件(兼容多种命名/目录结构)
+    chapter_path = find_chapter_file(project_root, chapter_num)
+    if chapter_path is None:
+        # 可能是“草稿路径”但尚未重命名
+        draft_path = default_chapter_draft_path(project_root, chapter_num)
+        if draft_path.exists():
+            chapter_path = draft_path
+
+    if chapter_path and chapter_path.exists():
+        chapter_path.unlink()
+        artifacts_cleaned.append(str(chapter_path.relative_to(project_root)))
+
+    # 清理Git暂存区
+    result = subprocess.run(
+        ['git', 'reset', 'HEAD', '.'],
+        cwd=project_root,
+        capture_output=True,
+        text=True
+    )
+    if result.returncode == 0:
+        artifacts_cleaned.append("Git暂存区已清理(project)")
+
+    return artifacts_cleaned
+
+def clear_current_task():
+    """清除当前中断任务"""
+    state = load_state()
+    if state.get('current_task'):
+        state['current_task'] = None
+        save_state(state)
+        print("✅ 中断任务已清除")
+    else:
+        print("⚠️ 无中断任务")
+
+def load_state():
+    """加载workflow状态"""
+    state_file = get_workflow_state_path()
+    if not state_file.exists():
+        return {'current_task': None, 'last_stable_state': None, 'history': []}
+    with open(state_file, 'r', encoding='utf-8') as f:
+        return json.load(f)
+
+def save_state(state):
+    """保存workflow状态(原子化写入)"""
+    state_file = get_workflow_state_path()
+    # ============================================================================
+    # 安全修复:使用原子化写入(P1 MEDIUM)
+    # ============================================================================
+    create_secure_directory(str(state_file.parent))
+    atomic_write_json(state_file, state, use_lock=True, backup=False)
+
+def get_pending_steps(command):
+    """获取待执行步骤列表 (v5.0)"""
+    if command == 'webnovel-write':
+        # v5.0 工作流:6 步
+        # Step 1: Context Agent 搜集上下文
+        # Step 2: 生成章节内容 (纯正文,3000-5000字)
+        # Step 3: 审查 (5个Agent并行,只报告)
+        # Step 4: 润色 (基于审查报告修复 + 去AI痕迹)
+        # Step 5: Data Agent 处理数据链
+        # Step 6: Git 备份
+        return ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Step 6']
+    elif command == 'webnovel-review':
+        return ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Step 6', 'Step 7', 'Step 8']
+    # 其他命令...
+    return []
+
+def extract_stable_state(task):
+    """提取稳定状态快照"""
+    return {
+        'command': task['command'],
+        'chapter_num': task['args'].get('chapter_num'),
+        'completed_at': task.get('completed_at'),
+        'artifacts': task.get('artifacts', {})
+    }
+
+# CLI接口
+if __name__ == '__main__':
+    import argparse
+    parser = argparse.ArgumentParser(description='工作流状态管理')
+    subparsers = parser.add_subparsers(dest='action', help='操作类型')
+
+    # start-task
+    p_start_task = subparsers.add_parser('start-task', help='开始新任务')
+    p_start_task.add_argument('--command', required=True, help='命令名称')
+    p_start_task.add_argument('--chapter', type=int, help='章节号')
+
+    # start-step
+    p_start_step = subparsers.add_parser('start-step', help='开始Step')
+    p_start_step.add_argument('--step-id', required=True, help='Step ID')
+    p_start_step.add_argument('--step-name', required=True, help='Step名称')
+    p_start_step.add_argument('--note', help='进度备注')
+
+    # complete-step
+    p_complete_step = subparsers.add_parser('complete-step', help='完成Step')
+    p_complete_step.add_argument('--step-id', required=True, help='Step ID')
+    p_complete_step.add_argument('--artifacts', help='Artifacts JSON')
+
+    # complete-task
+    p_complete_task = subparsers.add_parser('complete-task', help='完成任务')
+    p_complete_task.add_argument('--artifacts', help='Final artifacts JSON')
+
+    # detect
+    subparsers.add_parser('detect', help='检测中断')
+
+    # cleanup
+    p_cleanup = subparsers.add_parser('cleanup', help='清理artifacts')
+    p_cleanup.add_argument('--chapter', type=int, required=True, help='章节号')
+
+    # clear
+    subparsers.add_parser('clear', help='清除中断任务')
+
+    args = parser.parse_args()
+
+    if args.action == 'start-task':
+        start_task(args.command, {'chapter_num': args.chapter})
+    elif args.action == 'start-step':
+        start_step(args.step_id, args.step_name, args.note)
+    elif args.action == 'complete-step':
+        complete_step(args.step_id, args.artifacts)
+    elif args.action == 'complete-task':
+        complete_task(args.artifacts)
+    elif args.action == 'detect':
+        interrupt = detect_interruption()
+        if interrupt:
+            print("\n🔴 检测到中断任务:")
+            print(json.dumps(interrupt, ensure_ascii=False, indent=2))
+            print("\n💡 恢复选项:")
+            options = analyze_recovery_options(interrupt)
+            print(json.dumps(options, ensure_ascii=False, indent=2))
+        else:
+            print("✅ 无中断任务")
+    elif args.action == 'cleanup':
+        cleaned = cleanup_artifacts(args.chapter)
+        print(f"✅ 已清理: {', '.join(cleaned)}")
+    elif args.action == 'clear':
+        clear_current_task()
+    else:
+        parser.print_help()

+ 18 - 0
.claude/settings.local.json

@@ -0,0 +1,18 @@
+{
+  "permissions": {
+    "allow": [
+      "mcp__mcpServers__search_context",
+      "Bash(python:*)",
+      "Bash(grep:*)",
+      "Bash(ls -la \"D:/wk/novel skill/webnovel-writer/scripts/\"*.deprecated)",
+      "Bash(git add -A)",
+      "Bash(git commit:*)",
+      "Bash(ls:*)",
+      "Bash(xargs:*)",
+      "Bash(for script in token_budget_analyzer stress_test_500chapters stress_test_index golden_three_checker)",
+      "Bash(do echo \"=== $script ===\" grep -rn \"$script\" \"D:/wk/novel skill/webnovel-writer/\" --include=\"*.md\" --include=\"*.py\")",
+      "Bash(done)",
+      "WebSearch"
+    ]
+  }
+}

+ 155 - 0
.claude/skills/webnovel-init/SKILL.md

@@ -0,0 +1,155 @@
+---
+name: webnovel-init
+description: Initializes webnovel projects with settings, outline framework, and state.json. Supports quick/standard/deep modes. Activates when user wants to start a new novel or /webnovel-init.
+allowed-tools: Bash Write Read Edit AskUserQuestion Task
+---
+
+# Project Initialization Skill
+
+## Workflow Checklist
+
+Copy and track progress:
+
+```
+项目初始化进度:
+- [ ] Step 1: 加载题材套路 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/genre-tropes.md")
+- [ ] Step 2: 加载数据规范 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/system-data-flow.md")
+- [ ] Step 3: 确认上下文充足
+- [ ] Step 4: 检查现有项目
+- [ ] Step 5: 收集基本信息 (AskUserQuestion)
+- [ ] Step 5.5: 加载题材模板 (根据用户选择)
+- [ ] Step 6: 金手指设计 (Standard+)
+- [ ] Step 7: 创意深挖 (Deep模式)
+- [ ] Step 8: 生成项目文件
+- [ ] Step 9: 验证并报告
+```
+
+---
+
+## Step 1: 加载题材套路(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/genre-tropes.md"
+```
+
+## Step 2: 加载数据规范
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/system-data-flow.md"
+```
+
+## Step 3: 确认上下文充足
+
+**检查清单**:
+- [ ] 题材套路和金手指类型已了解
+- [ ] state.json 结构已理解
+- [ ] 项目目录结构已明确
+- [ ] 题材模板将在 Step 5.5 加载
+
+**如有缺失 → 返回对应 Step**
+
+## Step 4: 检查现有项目
+
+```bash
+ls .webnovel/state.json 2>/dev/null && echo "项目已存在"
+```
+
+如存在,询问用户:保留/备份/覆盖
+
+## 初始化模式
+
+| 模式 | 时长 | 内容 |
+|------|------|------|
+| ⚡ Quick | 5分钟 | 基本信息 |
+| 📝 Standard | 15-20分钟 | +金手指+核心卖点 |
+| 🎯 Deep | 30-45分钟 | +创意评估+市场定位+角色深度 |
+
+## Step 5: 收集基本信息
+
+**使用 AskUserQuestion 收集**:
+- 题材类型(修仙/系统流/都市异能/狗血言情)
+- 小说标题
+- 主角姓名
+- 目标字数
+
+**参考 genre-tropes.md** 建议合适的金手指类型。
+
+## Step 5.5: 加载题材模板(必须执行)
+
+**用户选择题材后,必须加载对应模板**:
+
+| 题材 | 执行命令 |
+|------|---------|
+| 修仙 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/修仙.md"` |
+| 系统流 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/系统流.md"` |
+| 都市异能 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/都市异能.md"` |
+| 狗血言情 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/狗血言情.md"` |
+| 知乎短篇 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/知乎短篇.md"` |
+| 古言 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/古言.md"` |
+| 现实题材 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/现实题材.md"` |
+| 规则怪谈 | `cat "${CLAUDE_PLUGIN_ROOT}/templates/genres/规则怪谈.md"` |
+
+**金手指设计参考**(Standard+ 模式必须加载):
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/templates/golden-finger-templates.md"
+```
+
+## Step 6: 金手指设计(Standard + Deep)
+
+**使用 AskUserQuestion 收集**:
+- 金手指类型(系统面板/签到/鉴定/吞噬)
+- 系统名称/代号
+- 代价/限制(反套路)
+- 系统性格
+- 成长曲线
+- 核心卖点(1-3个)
+
+## Step 7: 创意深挖(Deep 模式)
+
+如为 Deep 模式,额外加载:
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/creativity/inspiration-collection.md"
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/worldbuilding/power-systems.md"
+```
+
+收集:
+- 灵感五维评估
+- 创意 A+B+C 组合
+- 市场定位
+- 主角深度设计(欲望/缺陷/原型)
+- 反派设计(C/B/A/S 级)
+
+## Step 8: 生成项目文件
+
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/init_project.py" \
+  "./webnovel-project" \
+  "{title}" \
+  "{genre}" \
+  --protagonist-name "{name}" \
+  --target-words {count} \
+  --golden-finger-name "{gf_name}" \
+  --golden-finger-type "{gf_type}" \
+  --core-selling-points "{points}"
+```
+
+**生成文件**:
+- `.webnovel/state.json` - 运行时状态
+- `设定集/世界观.md` - 世界设定
+- `设定集/力量体系.md` - 力量体系
+- `设定集/主角卡.md` - 主角卡
+- `设定集/金手指设计.md` - 金手指设计
+- `大纲/总纲.md` - 总纲
+
+## Step 9: 验证并报告
+
+```bash
+ls -la .webnovel/state.json
+ls -la 设定集/*.md
+git init && git add . && git commit -m "初始化网文项目:{title}"
+```
+
+**输出三大定律提醒**:
+1. **大纲即法律**: 遵循大纲,不擅自发挥
+2. **设定即物理**: 遵守设定,不自相矛盾
+3. **发明需识别**: 新实体由 Data Agent 自动提取

+ 511 - 0
.claude/skills/webnovel-init/references/creativity/creative-combination.md

@@ -0,0 +1,511 @@
+# 创意组合创新参考指南
+
+## 核心定位
+
+通过系统化方法将多个创意元素(题材、卖点、人设等)进行深度融合,创造出具有独特吸引力和市场竞争力的小说概念。
+
+---
+
+## 1. 成功组合的五大特征
+
+### 1.1 化学反应效果
+
+**定义**:不同元素结合后产生的"1+1>2"效果。
+
+**判断标准**:
+- ✅ **强化学反应**:两元素互相增强,创造新的吸引力
+  - 示例:都市重生 + 反派视角 → 重生成反派的逆袭文(反派身份强化重生优势)
+- ⚠️ **弱化学反应**:两元素共存但无增强效果
+  - 示例:玄幻修仙 + 日常种田 → 元素割裂,读者困惑
+- ❌ **负反应**:两元素相互冲突或削弱
+  - 示例:硬核科幻 + 玄幻仙侠 → 世界观逻辑冲突
+
+### 1.2 核心卖点清晰
+
+组合后必须能用**一句话**说清楚核心吸引力。
+
+**测试方法**:
+```
+如果你只有10秒向读者推荐这本书,你会怎么说?
+```
+
+**正面示例**:
+- "重生成反派,用前世记忆洗白逆袭" ✅
+- "都市修仙文" ❌(过于宽泛)
+
+### 1.3 逻辑自洽性
+
+**关键问题**:
+- 世界观规则是否统一?
+- 主角能力来源是否合理?
+- 不同元素是否有明确的交互规则?
+
+**自洽性检查清单**:
+- [ ] 金手指设定与世界观不矛盾
+- [ ] 主角行为符合人物设定
+- [ ] 不同题材元素有清晰的融合逻辑
+- [ ] 爽点设计不违背基础规则
+
+### 1.4 读者共鸣强
+
+**共鸣来源**:
+- **情感共鸣**:主角经历能引发读者代入感
+- **欲望满足**:组合后的故事满足读者核心欲望(复仇、逆袭、成长等)
+- **新鲜感**:提供足够的创新点,避免审美疲劳
+
+### 1.5 故事可持续性
+
+**长期发展潜力评估**:
+```
+这个组合能否支撑:
+- 30万字+ 的长篇连载?
+- 持续的冲突升级?
+- 角色成长空间?
+- 世界观扩展可能性?
+```
+
+---
+
+## 2. 元素多维分析与拆解
+
+### 2.1 核心特征识别
+
+对每个待组合元素进行拆解:
+
+| 维度 | 分析要点 | 示例(都市重生) |
+|------|---------|-----------------|
+| **核心机制** | 这个元素的运作方式 | 主角携带前世记忆回到过去 |
+| **情感内核** | 这个元素满足什么情感需求 | 弥补遗憾、改写命运 |
+| **市场表现** | 当前市场热度与饱和度 | 成熟期,竞争激烈 |
+| **典型爽点** | 读者期待的高潮设计 | 先知先觉、打脸反转 |
+| **常见套路** | 需要规避的俗套设计 | 无脑打脸、圣母主角 |
+
+### 2.2 互动关系评估
+
+**三种互动关系**:
+
+1. **互补型**:A元素的短板由B元素弥补
+   - 示例:系统流(缺少情节张力)+ 悬疑推理(强情节)
+   - 结果:系统提供升级框架,悬疑提供剧情张力
+
+2. **增强型**:A元素与B元素相互放大优势
+   - 示例:重生(信息差优势)+ 商战(信息战核心)
+   - 结果:重生带来的先知先觉在商战中发挥最大价值
+
+3. **冲突型**:两元素存在逻辑或风格冲突
+   - 示例:严肃现实题材 + 无厘头搞笑
+   - 结果:风格割裂,读者群体困惑
+
+---
+
+## 3. 组合方案生成与筛选
+
+### 3.1 A+B 双元素组合法
+
+**基础公式**:
+```
+核心题材 + 差异化卖点 = 新创意
+```
+
+**常见组合模式**:
+
+| 题材元素 | 卖点元素 | 组合结果 | 化学反应评估 |
+|---------|---------|---------|-------------|
+| 都市重生 | 系统签到 | 重生都市签到流 | ⭐⭐⭐ 中等(常见组合) |
+| 玄幻修仙 | 反派视角 | 反派修仙文 | ⭐⭐⭐⭐ 强(视角创新) |
+| 科幻星际 | 规则怪谈 | 星际规则解谜 | ⭐⭐⭐⭐⭐ 极强(题材融合新颖) |
+| 古代宫斗 | 现代记忆 | 穿越宫斗文 | ⭐⭐ 弱(过度饱和) |
+
+**筛选标准**:
+- ✅ 化学反应 ≥ ⭐⭐⭐
+- ✅ 市场同质化程度 ≤ 中等
+- ✅ 逻辑自洽性 ≥ 高
+
+### 3.2 A+B+C 三元素组合法
+
+**进阶公式**:
+```
+题材 + 核心卖点 + 人设/视角特色 = 差异化创意
+```
+
+**示例拆解**:
+```
+都市(题材)+ 重生(卖点)+ 反派洗白(人设特色)
+= 重生成反派的都市洗白逆袭文
+
+化学反应分析:
+- 都市 + 重生:常规组合(⭐⭐)
+- 重生 + 反派洗白:创新组合(⭐⭐⭐⭐)
+- 都市 + 反派洗白:新颖组合(⭐⭐⭐⭐)
+综合评分:⭐⭐⭐⭐(高潜力)
+```
+
+**警告**:三元素组合需控制复杂度
+- ✅ 一主二辅(1个主导元素 + 2个辅助元素)
+- ❌ 三主并行(容易导致主线混乱)
+
+### 3.3 化学反应强度评估表
+
+| 反应强度 | 特征 | 示例 | 市场潜力 |
+|---------|------|------|---------|
+| ⭐⭐⭐⭐⭐ 极强 | 开创新品类,读者眼前一亮 | 规则怪谈 + 无限流 | 蓝海市场 |
+| ⭐⭐⭐⭐ 强 | 显著创新,有明确差异化 | 反派视角 + 玄幻修仙 | 竞争适中 |
+| ⭐⭐⭐ 中等 | 常见组合,需深耕细节 | 都市 + 重生 + 系统 | 红海市场 |
+| ⭐⭐ 弱 | 元素共存但无增强效果 | 玄幻 + 种田 | 需重新设计 |
+| ⭐ 负反应 | 逻辑冲突或风格割裂 | 硬核科幻 + 玄学修仙 | 不建议 |
+
+---
+
+## 4. 融合机制设计与测试
+
+### 4.1 确定主导与辅助关系
+
+**主导元素**(占比60-70%):
+- 定义故事的核心框架与世界观
+- 决定主要叙事风格
+- 提供主线冲突
+
+**辅助元素**(占比30-40%):
+- 提供差异化卖点
+- 增强爽点设计
+- 补充次要故事线
+
+**示例**:
+```
+主导:都市重生(框架:现代都市背景 + 时间回溯)
+辅助:商战权谋(卖点:商业博弈 + 智商较量)
+
+融合方式:
+- 主角重生后利用前世商业经验(信息差)进行商战布局
+- 都市背景提供真实感,商战提供剧情张力
+- 重生机制确保主角始终占据信息优势
+```
+
+### 4.2 融合机制六大维度
+
+**1. 情节交织**
+- 不同元素在情节中如何互动?
+- 是否有明确的触发机制?
+
+**示例**:
+```
+重生(主)+ 系统(辅)融合机制:
+- 重生后系统激活(触发机制)
+- 系统任务与重生目标一致(情节交织)
+- 系统奖励推动重生计划(功能互补)
+```
+
+**2. 能力体现**
+- 主角能力如何体现元素融合?
+- 不同元素的能力是否有层次感?
+
+**3. 世界观融合**
+- 不同元素的世界观规则如何统一?
+- 是否存在逻辑漏洞?
+
+**4. 社会环境影响**
+- 组合后的设定如何影响社会结构?
+- 是否产生新的冲突源?
+
+**5. 爽点设计**
+- 元素组合后产生什么新爽点?
+- 爽点频率与强度是否合理?
+
+**6. 长期可持续性**
+- 组合后的设定能否支撑长篇?
+- 冲突升级路径是否清晰?
+
+### 4.3 主角设定与开篇故事测试
+
+**测试方法**:为组合方案构思开篇故事
+
+**必须包含**:
+- 主角核心身份(体现元素融合)
+- 主要能力(来自哪个元素)
+- 核心驱动目标(与元素相关)
+- 开篇冲突(展示化学反应)
+
+**示例开篇测试**:
+```
+组合方案:都市重生 + 反派洗白
+
+主角设定:
+- 身份:前世反派大佬,重生回高中时代
+- 能力:前世积累的人脉/商业经验/读人心的能力
+- 目标:改写命运,从反派洗白成正面人物
+- 性格:外表高冷,内心戏多(反差萌)
+
+开篇故事:
+第1章:主角重生,发现自己回到了成为反派的起点
+- 展示元素1(重生):熟悉的教室,陌生的同学
+- 展示元素2(反派身份):回忆前世作恶的关键选择
+- 展示化学反应:主角决定改变,但发现"洗白"比想象中困难
+
+测试结果:
+✅ 两元素在开篇都有体现
+✅ 化学反应清晰(反派 + 重生 = 洗白难题)
+✅ 读者期待明确(想看主角如何洗白)
+```
+
+---
+
+## 5. 组合方案量化评分系统
+
+### 5.1 综合评分公式
+
+```
+总分 = 创新度×3 + 市场潜力×2.5 + 逻辑自洽性×2 + 实施难度×(-1) + 化学反应强度×2.5
+
+满分:50分
+及格线:30分
+高潜力:40分+
+```
+
+### 5.2 评分参考表
+
+| 评分维度 | 1分 | 3分 | 5分 |
+|---------|-----|-----|-----|
+| **创新度** | 烂大街组合 | 微创新组合 | 开创新品类 |
+| **市场潜力** | 小众冷门 | 中等受众 | 大众热门 |
+| **逻辑自洽性** | 多处矛盾 | 基本合理 | 无懈可击 |
+| **实施难度** | 极难驾驭 | 中等难度 | 易于实现 |
+| **化学反应强度** | 负反应 | 弱反应 | 强反应 |
+
+### 5.3 示例评分
+
+**方案A:都市重生 + 反派洗白**
+
+| 维度 | 得分 | 理由 |
+|------|------|------|
+| 创新度 | 4×3=12 | 反派视角创新,但重生常见 |
+| 市场潜力 | 4×2.5=10 | 都市文受众广,反派洗白有吸引力 |
+| 逻辑自洽性 | 5×2=10 | 重生+洗白逻辑通顺 |
+| 实施难度 | 3×(-1)=-3 | 需设计复杂人物弧线 |
+| 化学反应 | 4×2.5=10 | 两元素互相增强 |
+| **总分** | **39** | **高潜力方案** |
+
+---
+
+## 6. 常见组合陷阱与规避策略
+
+### 6.1 陷阱1:要素堆砌
+
+**表现**:A+B+C+D+E...无限叠加元素
+
+**后果**:
+- 主线混乱,读者困惑
+- 每个元素都浅尝辄止,无深度
+- 后期难以控制,容易崩盘
+
+**规避策略**:
+- 限制元素数量(建议2-3个)
+- 明确主导与辅助关系(1主+1-2辅)
+- 每个元素必须有明确功能
+
+### 6.2 陷阱2:逻辑冲突
+
+**表现**:不同元素的世界观规则相互矛盾
+
+**示例**:
+```
+硬核科幻(强调科学逻辑)+ 玄幻修仙(强调玄学力量)
+→ 读者无法代入,沉浸感破裂
+```
+
+**规避策略**:
+- 组合前检查世界观兼容性
+- 设计统一的底层逻辑
+- 必要时选择软科幻等过渡类型
+
+### 6.3 陷阱3:风格割裂
+
+**表现**:不同元素的叙事风格差异过大
+
+**示例**:
+```
+严肃现实题材 + 无厘头搞笑
+→ 读者群体定位混乱
+```
+
+**规避策略**:
+- 确定统一的叙事基调
+- 辅助元素服从主导元素风格
+- 避免过度风格跳跃
+
+### 6.4 陷阱4:爽点单一
+
+**表现**:组合后爽点类型反而减少
+
+**示例**:
+```
+都市重生 + 系统签到
+→ 两者都依赖"金手指碾压",爽点同质化
+```
+
+**规避策略**:
+- 选择爽点类型互补的元素
+- 一个提供"实力碾压",另一个提供"智商博弈"
+- 确保爽点多样性
+
+---
+
+## 7. 实战组合方案模板
+
+### 7.1 方案描述模板
+
+```markdown
+## 组合方案:[方案名称]
+
+### 核心元素
+- **主导元素**:[元素A](占比70%)
+- **辅助元素**:[元素B](占比30%)
+
+### 核心吸引力
+- **一句话卖点**:[10秒电梯pitch]
+- **独特阅读快感**:[读者能获得什么情感满足]
+- **化学反应效果**:[两元素如何互相增强]
+
+### 主角核心设定
+- **身份**:[主角是谁]
+- **主要能力**:[来自哪个元素]
+- **性格特质**:[2-3个关键词]
+- **核心目标**:[驱动主线的目标]
+
+### 开篇故事(3章大纲)
+**第1章**:[引入主角 + 展示核心设定]
+**第2章**:[第一次冲突 + 展示化学反应]
+**第3章**:[确立主线目标 + 爽点爆发]
+
+### 融合机制
+- **情节交织**:[元素在情节中如何互动]
+- **能力体现**:[主角能力如何体现融合]
+- **世界观融合**:[如何统一世界观规则]
+
+### 发展潜力
+- **长期可持续性**:[能否支撑30万字+]
+- **冲突升级路径**:[冲突如何逐步升级]
+- **角色成长空间**:[主角成长路线]
+- **世界观扩展**:[后期如何拓展]
+
+### 优势与挑战
+**优势**:
+- [优势1]
+- [优势2]
+
+**挑战**:
+- [挑战1 及应对策略]
+- [挑战2 及应对策略]
+
+### 综合评分
+| 维度 | 得分 | 理由 |
+|------|------|------|
+| 创新度 | /5 | |
+| 市场潜力 | /5 | |
+| 逻辑自洽性 | /5 | |
+| 实施难度 | /5 | |
+| 化学反应 | /5 | |
+| **总分** | **/50** | |
+```
+
+### 7.2 快速决策清单
+
+在提交方案前,逐项确认:
+
+- [ ] 能用一句话说清核心卖点?
+- [ ] 两个元素有明确的化学反应?
+- [ ] 主角设定体现了元素融合?
+- [ ] 开篇故事能吸引目标读者?
+- [ ] 世界观规则逻辑自洽?
+- [ ] 爽点设计多样化?
+- [ ] 能支撑30万字+连载?
+- [ ] 综合评分 ≥ 30分?
+
+---
+
+## 8. 进阶技巧:从组合到创新
+
+### 8.1 反向组合法
+
+**传统组合**:从现有元素出发
+**反向组合**:从读者需求出发
+
+**步骤**:
+1. 识别读者未被满足的需求
+2. 寻找能满足该需求的元素组合
+3. 验证组合的可行性
+
+**示例**:
+```
+需求:想看智商在线的主角,但不要无脑碾压
+↓
+寻找元素:
+- 悬疑推理(强调智商)
+- 平等对手(非碾压)
+↓
+组合方案:悬疑推理 + 势均力敌的对手 = 烧脑博弈文
+```
+
+### 8.2 颠覆性组合
+
+**方法**:刻意选择看似矛盾的元素
+
+**示例**:
+```
+传统:强者主角 + 升级流
+颠覆:弱者主角 + 降级流
+
+化学反应:
+- 主角越来越弱,反而更有张力
+- 读者好奇:如何在实力下降中求生?
+```
+
+**警告**:颠覆性组合风险高,需深思熟虑
+
+### 8.3 跨品类迁移
+
+**方法**:将A品类的成功元素迁移到B品类
+
+**示例**:
+```
+从"规则怪谈"迁移"规则破解"到"玄幻修仙"
+→ 修仙世界有严格规则,主角通过破解规则逆袭
+```
+
+---
+
+## 9. 组合方案对比与选择
+
+### 9.1 多方案对比表
+
+| 方案编号 | 核心组合 | 创新度 | 市场潜力 | 实施难度 | 总分 | 优先级 |
+|---------|---------|--------|---------|---------|------|--------|
+| 方案1 | 都市+重生+反派 | 4 | 4 | 3 | 39 | ⭐⭐⭐⭐ |
+| 方案2 | 玄幻+系统+群像 | 3 | 5 | 4 | 35 | ⭐⭐⭐ |
+| 方案3 | 科幻+规则怪谈 | 5 | 3 | 5 | 32 | ⭐⭐⭐ |
+
+### 9.2 最终选择建议
+
+**推荐顺序**:
+1. **方案1(39分)**:创新度与市场潜力均衡,实施难度适中
+2. **方案2(35分)**:市场潜力最高,但创新度略低
+3. **方案3(32分)**:创新度高但市场风险较大
+
+**选择原则**:
+- 新手作者:优先选择实施难度低、市场潜力高的方案
+- 成熟作者:可挑战高创新度、高难度的方案
+- 商业化导向:优先选择市场潜力最高的方案
+
+---
+
+## 10. 关联参考文档
+
+本文档是「创意阶段」的核心方法论,完成后可参考:
+
+- **前置步骤**:`inspiration-collection.md`(提供待组合的元素)
+- **前置步骤**:`market-positioning.md`(提供市场导向的组合建议)
+- **下一步**:`selling-points.md`(从组合方案中提炼核心卖点)
+- **后续阶段**:设定阶段的世界观构建与角色设定(将组合方案具体化)
+
+---
+
+**使用建议**:组合创新是创意阶段的核心环节,建议先完成灵感收集与市场定位分析,再进行元素组合。每个组合方案都应经过开篇测试与评分验证,确保可行性后再进入后续阶段。

+ 298 - 0
.claude/skills/webnovel-init/references/creativity/inspiration-collection.md

@@ -0,0 +1,298 @@
+# 灵感收集整理参考指南
+
+## 核心定位
+
+将零散想法转化为可执行的创作方向,通过系统化方法提炼、标注和组合灵感。
+
+---
+
+## 1. 灵感分类体系
+
+### 1.1 灵感类型识别
+
+| 类型 | 特征 | 示例 |
+|------|------|------|
+| **题材灵感** | 故事背景与世界类型 | 都市重生、星际争霸、古代宫斗 |
+| **人设灵感** | 角色特质与身份 | 冷酷杀手、天真学霸、腹黑总裁 |
+| **世界观灵感** | 设定规则与体系 | 修仙体系、未来科技、平行世界 |
+| **情节钩子** | 吸引读者的剧情点 | 身份互换、时间循环、真相反转 |
+| **金手指灵感** | 主角特殊优势 | 系统签到、重生记忆、异能觉醒 |
+| **情绪体验灵感** | 想传达的情感 | 热血燃情、甜蜜治愈、紧张刺激 |
+
+### 1.2 创意成熟度分级
+
+- **基础层**:仅有概念,缺少具体设定
+- **发展层**:有初步人设或世界观框架
+- **成熟层**:具备完整故事结构和主线设计
+
+---
+
+## 2. 单一灵感深度挖掘
+
+### 2.1 核心概念提取
+
+**公式**:"如果 [设定/事件],会怎样?"
+
+示例:
+- "如果主角可以看到别人的死亡倒计时,会怎样?"
+- "如果所有人都失去了昨天的记忆,会怎样?"
+
+### 2.2 五维潜力评估(1-5分制)
+
+| 维度 | 评估要点 | 1分 | 5分 |
+|------|---------|-----|-----|
+| **原创性** | 市场同质化程度 | 烂大街套路 | 独一无二 |
+| **市场潜力** | 读者群体规模 | 小众冷门 | 大众热门 |
+| **扩展性** | 能否支撑长篇(30万字+) | 难以展开 | 故事空间大 |
+| **创作难度** | 设定/情节复杂度 | 极难驾驭 | 易于实现 |
+| **个人匹配** | 知识储备/兴趣/资源 | 完全陌生 | 驾轻就熟 |
+
+**总分参考**:
+- 20-25分:高潜力,优先开发
+- 15-19分:中等潜力,需打磨
+- <15分:暂时搁置或重组
+
+### 2.3 风险识别清单
+
+常见创作风险:
+- ❌ 设定过于复杂,易崩盘
+- ❌ 节奏难以把控(过快/过慢)
+- ❌ 同质化竞争激烈
+- ❌ 需要专业知识但储备不足
+- ❌ 爽点设计单一,后期乏力
+
+---
+
+## 3. 灵感标签化系统
+
+### 3.1 标签命名规范
+
+**格式**:`[主类别]-[子类别]` 或 `[核心元素]-[变体]`
+
+示例:
+```
+都市-重生
+系统-签到流
+反派-洗白
+群像-多视角
+悬念-规则怪谈
+玄幻-修仙
+科幻-星际
+```
+
+### 3.2 标签组合策略
+
+**单标签**:明确题材定位
+```
+玄幻
+```
+
+**双标签**:题材 + 卖点
+```
+都市-重生
+玄幻-系统
+```
+
+**三标签+**:题材 + 卖点 + 特色
+```
+都市-重生-复仇
+玄幻-系统-群像
+科幻-星际-权谋
+```
+
+---
+
+## 4. 创意组合创新法
+
+### 4.1 A+B 双元素组合
+
+**公式**:不同灵感的核心元素叠加
+
+示例:
+- `都市重生 + 系统签到` → 重生都市签到流
+- `悬疑推理 + 规则怪谈` → 规则解谜流
+- `古代宫斗 + 现代记忆` → 穿越宫斗文
+
+**评估标准**:
+- ✅ 化学反应:两元素结合后产生新的吸引力
+- ✅ 可行性:组合后设定逻辑自洽
+- ❌ 冲突:元素相互矛盾,难以融合
+
+### 4.2 A+B+C 三元素组合
+
+**公式**:题材 + 卖点 + 人设特色
+
+示例:
+```
+都市 + 重生 + 反派洗白
+= 重生成反派的都市文,通过洗白实现逆袭
+
+玄幻 + 系统 + 群像多视角
+= 多主角都有系统,相互博弈的玄幻文
+
+科幻 + 时间循环 + 推理解谜
+= 困在时间循环中破解真相的科幻悬疑
+```
+
+**警告**:避免要素过多导致主线混乱、节奏失控。
+
+### 4.3 微创新与变体
+
+**方法**:对既有灵感进行"换X"操作
+
+- **换视角**:反派视角、配角视角、多视角
+- **换载体**:书变游戏、梦境变现实
+- **换时代**:现代背景变古代、未来变过去
+- **换能力**:主角弱化、敌人强化、规则改变
+
+---
+
+## 5. 创意地图可视化
+
+### 5.1 四维分类法
+
+**按题材分类**:
+```
+玄幻类:
+  - INSP-001:修仙体系创新
+  - INSP-005:异世大陆争霸
+
+都市类:
+  - INSP-002:重生复仇
+  - INSP-008:商战权谋
+```
+
+**按创新度分类**:
+```
+传统型(稳妥但竞争大):
+  - INSP-001:经典修仙升级
+
+微创新(风险适中):
+  - INSP-003:修仙 + 现代科技
+
+颠覆型(高风险高回报):
+  - INSP-007:无修炼的修仙世界
+```
+
+**按成熟度分类**:
+```
+概念级(只有想法):
+  - INSP-004:时间倒流的世界
+
+雏形级(有初步设定):
+  - INSP-006:主角可以复制他人能力
+
+完整级(有完整故事框架):
+  - INSP-002:重生复仇文,3卷大纲已完成
+```
+
+**按优先级分类**:
+```
+高潜力(立即开发):
+  - INSP-002:市场潜力5分,个人匹配5分
+
+中等潜力(需打磨):
+  - INSP-005:原创性高但难度大
+
+低优先级(暂时搁置):
+  - INSP-009:市场过于小众
+```
+
+### 5.2 缺口分析
+
+**检查清单**:
+- [ ] 是否只有题材没有卖点?
+- [ ] 是否只有设定没有冲突?
+- [ ] 是否缺少金手指系统?
+- [ ] 是否缺少明确的读者爽点?
+- [ ] 是否缺少主角成长路线?
+
+**建议**:发现缺口后,通过「创意组合」或「微创新」补齐。
+
+---
+
+## 6. Top 推荐筛选法
+
+### 6.1 筛选标准
+
+综合评分公式:
+```
+总分 = 原创性×2 + 市场潜力×3 + 扩展性×2 + 个人匹配×2 - 创作难度×1
+```
+
+**权重说明**:
+- 市场潜力最重要(×3)
+- 原创性、扩展性、个人匹配同等重要(×2)
+- 创作难度作为负面因子(-×1)
+
+### 6.2 推荐理由模板
+
+```
+【推荐 #1】INSP-002 - 都市重生复仇
+优先级:1(最高)
+理由:市场潜力高(大众题材)+ 创新度适中(反派洗白新颖)+
+     创作难度可控(都市背景易驾驭)+ 个人匹配度高(熟悉都市生活)
+下一步:进入「市场定位分析」阶段,明确目标读者与平台策略
+```
+
+---
+
+## 7. 实战工作流程
+
+### 阶段一:灵感捕捉(1-3天)
+
+1. **广泛收集**:记录所有想法,不做评判
+2. **初步分类**:按灵感类型归档
+3. **快速标注**:为每条灵感打上初步标签
+
+### 阶段二:深度挖掘(3-5天)
+
+1. **逐条评估**:使用五维评估法打分
+2. **识别风险**:列出潜在创作难点
+3. **补充信息**:丰富灵感的背景与细节
+
+### 阶段三:组合创新(2-3天)
+
+1. **尝试A+B组合**:寻找化学反应
+2. **探索A+B+C**:构建差异化优势
+3. **微创新变体**:从不同角度重塑灵感
+
+### 阶段四:可视化整理(1-2天)
+
+1. **建立创意地图**:四维分类法整理
+2. **缺口分析**:识别短板并补强
+3. **Top筛选**:选出3-5个最值得开发的创意
+
+---
+
+## 8. 常见问题与解决方案
+
+### Q1: 灵感太多,不知道选哪个?
+
+**A**: 使用「综合评分公式」量化评估,优先开发高分项。
+
+### Q2: 灵感感觉很好,但无法扩展?
+
+**A**: 检查「扩展性」维度,尝试A+B组合引入新元素。
+
+### Q3: 同质化严重,如何创新?
+
+**A**: 使用「微创新法」(换视角/换载体/换时代),在熟悉框架上做变体。
+
+### Q4: 灵感只有概念,缺少具体设定?
+
+**A**: 进入下一步「市场定位分析」或「核心卖点定位」,通过目标读者反推设定需求。
+
+---
+
+## 9. 关联参考文档
+
+本文档是「创意阶段」的第一步,完成后可参考:
+
+- **下一步**:`market-positioning.md`(市场定位分析)
+- **平行参考**:`creative-combination.md`(更深入的组合方法论)
+- **后续步骤**:`selling-points.md`(从灵感中提炼核心卖点)
+
+---
+
+**使用建议**:将灵感整理视为持续迭代的过程,定期回顾和更新创意地图,保持灵感池的活力与新鲜度。

+ 421 - 0
.claude/skills/webnovel-init/references/creativity/market-positioning.md

@@ -0,0 +1,421 @@
+# 市场定位分析参考指南
+
+## 核心定位
+
+精准识别目标平台与读者群体,制定差异化竞争策略,确保作品在市场中找到最佳位置。
+
+---
+
+## 1. 主流平台分析框架(2025)
+
+### 1.1 起点中文网
+
+**读者画像**:
+- **核心年龄**:25-35岁男性为主(占比70%+)
+- **付费习惯**:高付费意愿,月均消费50-200元
+- **阅读偏好**:深度世界观、逻辑自洽、长篇连载
+
+**题材优势**:
+- ✅ 玄幻修仙(传统强项)
+- ✅ 都市异能(稳定受众)
+- ✅ 科幻军事(垂直深度)
+- ✅ 游戏竞技(电竞崛起)
+
+**平台特色**:
+- 订阅付费模式(千字5分)
+- 推荐位竞争激烈(新书榜、分类榜)
+- 读者互动活跃(本章说、书评区)
+
+**2025年趋势**:
+- 🔥 多元宇宙/平行世界设定
+- 🔥 硬核升级流(数值可视化)
+- 🔥 群像多主角(非单一视角)
+- ❌ 过时:无脑打脸、后宫流水账、拖沓升级
+
+### 1.2 番茄小说
+
+**读者画像**:
+- **核心年龄**:18-28岁,下沉市场用户
+- **付费习惯**:免费阅读为主,广告变现
+- **阅读偏好**:快节奏、爽点密集、短中篇
+
+**题材优势**:
+- ✅ 赘婿逆袭(爽文经典)
+- ✅ 甜宠短篇(女频热门)
+- ✅ 快穿系统(节奏紧凑)
+- ✅ 乡村题材(贴近生活)
+
+**平台特色**:
+- 算法推荐驱动(CTR为王)
+- 免费模式(字数越多收益越高)
+- 短篇友好(5-20万字完本)
+
+**2025年趋势**:
+- 🔥 知乎体短篇(2-3万字)
+- 🔥 多题材融合(都市+玄幻)
+- 🔥 反套路创新(反转传统爽文)
+- ❌ 过时:拖沓铺垫、慢热开篇、复杂世界观
+
+### 1.3 七猫小说
+
+**读者画像**:
+- **核心年龄**:30-45岁,三四线城市为主
+- **付费习惯**:免费阅读,广告容忍度高
+- **阅读偏好**:家庭伦理、现实题材、情感共鸣
+
+**题材优势**:
+- ✅ 婆媳关系(现实共鸣)
+- ✅ 重生复仇(情感宣泄)
+- ✅ 乡村振兴(时代主题)
+- ✅ 职场奋斗(励志向上)
+
+**平台特色**:
+- 推荐算法(完读率>点击率)
+- 女频占比高(60%+)
+- 现实题材友好
+
+### 1.4 晋江文学城
+
+**读者画像**:
+- **核心年龄**:20-30岁女性(95%+)
+- **付费习惯**:高粘性付费用户
+- **阅读偏好**:耽美言情、古言仙侠、娱乐圈
+
+**题材优势**:
+- ✅ 耽美(平台标志性题材)
+- ✅ 古言宫斗(经典品类)
+- ✅ 娱乐圈(现实向甜文)
+- ✅ 无限流(女性向冒险)
+
+**平台特色**:
+- 积分付费(VIP订阅)
+- 文笔要求较高
+- 社区文化浓厚(读者粘性强)
+
+---
+
+## 2. 目标读者画像深度构建
+
+### 2.1 基础特征四维度
+
+| 维度 | 核心问题 | 分析要点 |
+|------|---------|---------|
+| **年龄分布** | 主要读者年龄段? | 18-25(学生党)/ 25-35(职场人)/ 35+(资深读者) |
+| **性别比例** | 男女读者占比? | 男频(70%+ 男性)/ 女频(70%+ 女性)/ 通吃型 |
+| **地域分布** | 主要来源地区? | 一二线城市(高消费)/ 三四线(下沉市场)|
+| **消费能力** | 付费意愿如何? | 高付费(月均100+)/ 中等(30-100)/ 低付费(<30) |
+
+### 2.2 阅读行为分析
+
+**阅读场景识别**:
+- 🚇 通勤碎片时间(10-30分钟)→ 快节奏短篇
+- 🌙 睡前放松时段(30-60分钟)→ 轻松甜文
+- ⏰ 周末集中追更(1-3小时)→ 长篇深度文
+
+**阅读设备偏好**:
+- 📱 手机为主(90%+)→ 排版需适配小屏
+- 💻 PC为辅(10%)→ 长评论区互动
+
+**追更习惯**:
+- 日更党:期望每日更新,断更即弃
+- 养肥党:等完本或积累章节再看
+- 随缘党:有空就看,不强求更新
+
+### 2.3 内容偏好深度分析
+
+**核心爽点偏好**(按题材分类):
+
+| 题材 | 读者期待爽点 | 爽点频率要求 |
+|------|-------------|-------------|
+| **都市重生** | 打脸反转、装逼啪啪啪 | 每3-5章一个小高潮 |
+| **玄幻修仙** | 越级击杀、境界突破 | 每10章一次大战 |
+| **系统流** | 任务奖励、能力提升 | 每章至少一次签到/抽奖 |
+| **甜宠文** | 霸总宠溺、误会解除 | 每2-3章一次甜蜜互动 |
+
+**情节模式偏好**:
+- ✅ 升级流:明确成长路径,数值可视化
+- ✅ 种田流:稳步发展,成就感积累
+- ✅ 群像流:多角色视角,剧情丰富
+- ❌ 避免:拖沓铺垫、重复情节、强行虐主
+
+**人物期待**:
+- 主角:杀伐果断 > 圣母犹豫
+- 主角:智商在线 > 降智决策
+- 配角:立体鲜明 > 工具人脸谱化
+- 反派:动机合理 > 无脑送人头
+
+**创新边界**:
+- ✅ 可接受:世界观微创新、人设反套路
+- ✅ 必须保留:核心爽点模式、基础升级逻辑
+- ❌ 禁区:完全颠覆读者期待、拒绝常规爽点
+
+### 2.4 读者雷区清单(必须规避)
+
+**通用雷区**:
+- ❌ 圣母主角(优柔寡断、滥发善心)
+- ❌ 降智反派(无脑送死、重复犯错)
+- ❌ 拖沓升级(十几章才升一级)
+- ❌ 后宫无脑收(女角色工具化)
+- ❌ NTR情节(绿帽/被背叛)
+
+**平台特定雷区**:
+- 起点:过度水字数、无逻辑金手指
+- 番茄:开篇节奏慢、前三章无爽点
+- 晋江:三观不正、性别歧视
+
+---
+
+## 3. 市场趋势识别(2025)
+
+### 3.1 题材生命周期判断
+
+**生命周期阶段**:
+
+| 阶段 | 特征 | 策略 |
+|------|------|------|
+| **萌芽期** | 少量试水作品,市场接受度未知 | 高风险高回报,适合创新者 |
+| **成长期** | 爆款出现,跟风作品增多 | 黄金入场期,需差异化 |
+| **成熟期** | 市场饱和,竞争激烈 | 必须有独特卖点 |
+| **衰退期** | 读者审美疲劳,新作难出头 | 避免入场或深度创新 |
+
+**2025年题材生命周期速查**:
+```
+萌芽期:AI觉醒流、元宇宙冒险
+成长期:规则怪谈、无限流女频
+成熟期:都市重生、玄幻修仙、系统流
+衰退期:传统后宫、无脑打脸流
+```
+
+### 3.2 新兴热门元素
+
+**2025年趋势关键词**:
+- 🔥 **群像多视角**:不再单一主角,多线叙事
+- 🔥 **反转真相**:悬念铺设+真相揭秘
+- 🔥 **规则怪谈**:SCP风格,规则破解
+- 🔥 **现实题材**:贴近生活,情感共鸣
+- 🔥 **女强不恋爱**:事业线为主,感情线为辅
+
+**正在过时的元素**:
+- ❌ 单一升级流(纯数值堆砌)
+- ❌ 无脑打脸(缺少智商博弈)
+- ❌ 后宫流水账(女角色脸谱化)
+- ❌ 拖沓铺垫(前期过慢)
+
+### 3.3 跨题材融合趋势
+
+**成功组合案例**:
+- 都市 + 玄幻 = 都市修仙(现代背景+修炼体系)
+- 悬疑 + 科幻 = 未来犯罪推理
+- 历史 + 穿越 = 架空历史争霸
+- 甜宠 + 悬疑 = 破案小甜文
+
+**融合原则**:
+1. 主题材占70%,辅题材30%
+2. 两者产生化学反应,非简单拼凑
+3. 目标读者群体有交集
+
+---
+
+## 4. 三句话作品定位法
+
+### 4.1 第一句:题材+卖点+承诺
+
+**模板**:
+```
+这是一部 [题材] 小说,核心卖点是 [独特卖点],
+向读者承诺 [核心体验/情感满足]。
+```
+
+**示例**:
+```
+这是一部都市重生文,核心卖点是「反派洗白+反转复仇」,
+向读者承诺极致的反转快感与情绪释放。
+```
+
+### 4.2 第二句:目标读者+平台策略
+
+**模板**:
+```
+主要面向 [年龄+性别] 的 [平台] 读者,
+满足他们对 [核心需求] 的期待。
+```
+
+**示例**:
+```
+主要面向25-35岁男性起点读者,
+满足他们对「智商在线+爽点密集+世界观完整」的期待。
+```
+
+### 4.3 第三句:差异化+竞争策略
+
+**模板**:
+```
+与同类作品相比,本作的差异化在于 [独特点],
+通过 [具体策略] 实现市场竞争优势。
+```
+
+**示例**:
+```
+与同类作品相比,本作的差异化在于「多视角群像+反转真相」,
+通过「每章一个小反转+每10章一个大反转」的节奏设计实现竞争优势。
+```
+
+---
+
+## 5. 对标作品分析法
+
+### 5.1 成功案例研究
+
+**分析维度**(每部作品):
+1. **核心卖点**:最吸引读者的要素是什么?
+2. **差异化**:与同题材作品有何不同?
+3. **节奏把控**:如何设置爽点与高潮?
+4. **人设亮点**:主角/配角/反派的特色?
+5. **读者好评**:评论区高频称赞点?
+
+**示例分析表格**:
+
+| 作品名 | 核心卖点 | 成功要素 | 读者好评点 |
+|--------|---------|---------|-----------|
+| 作品A | 反派洗白 | 节奏紧凑+反转密集 | "反转出人意料" |
+| 作品B | 系统签到 | 数值可视化+成长感 | "爽点频率高" |
+
+### 5.2 失败案例避坑
+
+**常见失败模式**:
+- 高开低走:开篇吸引人,后期崩盘
+- 节奏失控:前期拖沓 or 后期仓促
+- 人设崩塌:主角OOC(Out of Character)
+- 逻辑混乱:设定自相矛盾
+
+**分析要点**:
+- 在哪个阶段开始走下坡路?
+- 读者流失的直接原因是什么?
+- 哪些错误可以提前规避?
+
+---
+
+## 6. 平台策略与发布规划
+
+### 6.1 首发平台选择决策树
+
+```
+是否有付费读者基础?
+├─ 是 → 起点/晋江(订阅收益高)
+└─ 否 → 番茄/七猫(免费流量大)
+    │
+    └─ 题材偏爽文?
+        ├─ 是 → 番茄(算法推荐快)
+        └─ 否 → 七猫(女频现实题材友好)
+```
+
+### 6.2 多平台策略
+
+**何时选择多平台**:
+- ✅ 题材受众广(都市、甜宠)
+- ✅ 有精力维护多个更新渠道
+- ✅ 不同平台内容差异化(如番茄版节奏更快)
+
+**多平台发布策略**:
+1. **首发独占期**:选择一个平台独占1-3个月
+2. **分发期**:授权其他平台同步或延迟发布
+3. **内容差异化**:根据平台调整节奏/篇幅
+   - 起点版:深度世界观,长篇连载
+   - 番茄版:快节奏,章节精简
+
+### 6.3 更新策略
+
+**更新频率建议**:
+
+| 平台 | 建议频率 | 单章字数 | 理由 |
+|------|---------|---------|------|
+| 起点 | 日更 | 3000-4000字 | 榜单竞争需要稳定更新 |
+| 番茄 | 日更2-3章 | 2000-2500字 | 算法偏好高频更新 |
+| 晋江 | 隔日更或日更 | 3000-5000字 | 读者接受度高 |
+
+**免费章节数量**:
+- 起点:10-15章(约3-5万字)
+- 番茄:前20-30章免费(吸引流量)
+- 晋江:5-10章(快速进入VIP)
+
+### 6.4 商业化路径
+
+**VIP上架时机**:
+- 字数标准:3-5万字
+- 章节数:10-20章
+- 数据指标:收藏≥500,推荐票≥200
+
+**推荐位申请策略**:
+1. **新书期**(0-30天):争取新书榜、签约推荐
+2. **成长期**(1-3个月):分类推荐、强推
+3. **稳定期**(3个月+):精品推荐、首页推荐
+
+**读者运营建议**:
+- 定期互动:每周至少回复一次评论
+- 单章说:提前预告剧情,吊读者胃口
+- 福利活动:加更、抽奖、角色投票
+
+---
+
+## 7. 风险预警与规避
+
+### 7.1 市场竞争风险
+
+**风险描述**:
+- 同类作品过多,头部作品挤压市场空间
+- 新作难以突围,流量被分流
+
+**规避策略**:
+1. **差异化卖点**:避开红海,寻找蓝海细分
+2. **提升质量**:文笔/逻辑/节奏全面优化
+3. **精准定位**:深耕垂直读者群体
+
+### 7.2 读者需求变化风险
+
+**风险描述**:
+- 读者口味快速变化,题材过时
+- 目标读者群体过小,天花板低
+
+**规避策略**:
+1. **持续调研**:关注平台热榜、读者评论
+2. **灵活调整**:根据反馈微调剧情方向
+3. **多元尝试**:不把鸡蛋放在一个篮子
+
+### 7.3 平台规则风险
+
+**风险描述**:
+- 平台算法调整,推荐机制变化
+- 审核标准收紧,内容被下架
+
+**规避策略**:
+1. **研究规则**:定期查看平台公告
+2. **合规创作**:避开敏感题材与违禁词
+3. **多平台备份**:不依赖单一平台
+
+---
+
+## 8. 实战检查清单
+
+在完成市场定位分析后,逐项确认:
+
+- [ ] 明确首发平台及理由?
+- [ ] 绘制目标读者画像(年龄/性别/付费习惯)?
+- [ ] 列出核心爽点偏好与雷区清单?
+- [ ] 完成三句话作品定位?
+- [ ] 分析至少3部成功对标作品?
+- [ ] 制定更新策略(频率/字数/免费章节)?
+- [ ] 识别主要竞争风险并制定应对方案?
+
+---
+
+## 9. 关联参考文档
+
+本文档是「创意阶段」的第二步,完成后可参考:
+
+- **上一步**:`inspiration-collection.md`(从灵感池中选定方向)
+- **下一步**:`selling-points.md`(提炼核心卖点)
+- **平行参考**:`creative-combination.md`(组合创新强化差异化)
+
+---
+
+**使用建议**:市场定位不是一次性工作,应在创作过程中持续追踪数据,根据读者反馈动态调整策略。

+ 670 - 0
.claude/skills/webnovel-init/references/creativity/selling-points.md

@@ -0,0 +1,670 @@
+# 核心卖点定位参考指南
+
+## 核心定位
+
+精准识别并定义作品的核心吸引力,确保所有创作要素(主角、金手指、世界观、情节)都围绕这个卖点展开,为读者提供独特且一致的阅读体验。
+
+---
+
+## 1. 核心卖点理论基础
+
+### 1.1 核心卖点的定义
+
+**核心卖点 = 读者选择这本书的唯一理由**
+
+> 如果读者只有10秒了解你的小说,你会用哪一句话吸引他们?
+
+**三层结构**:
+- **表层(题材标签)**:都市、玄幻、科幻等 → 决定读者是否点开
+- **中层(核心卖点)**:重生复仇、规则怪谈、反派洗白等 → 决定读者是否继续看
+- **底层(情感体验)**:爽、虐、燃、甜、悬疑等 → 决定读者是否付费追更
+
+### 1.2 核心卖点 vs 其他概念
+
+| 概念 | 定义 | 区别 |
+|------|------|------|
+| **核心卖点** | 作品最核心的吸引力 | 唯一、贯穿全文、不可替代 |
+| **题材** | 故事背景类型 | 玄幻/都市/科幻,范围广泛 |
+| **金手指** | 主角特殊优势 | 服务于卖点的工具 |
+| **爽点** | 读者爽感来源 | 卖点的具体表现形式 |
+| **人设** | 角色设定 | 卖点的人格化载体 |
+
+**示例**:
+```
+❌ 错误:"我的小说是都市重生文"(这是题材,不是卖点)
+✅ 正确:"重生成反派,用前世记忆洗白逆袭"(这是核心卖点)
+```
+
+### 1.3 核心卖点黄金公式
+
+**公式一:一句话卖点**
+```
+[主角身份/优势] + [核心行动] + [独特目标] = 核心卖点
+```
+
+**示例**:
+- 重生商战大佬(身份)+ 回到高考前(行动)+ 改写人生巅峰(目标)
+- 签到系统宿主(优势)+ 每日获得随机奖励(行动)+ 成为最强者(目标)
+- 看穿规则漏洞(优势)+ 在规则怪谈中求生(行动)+ 破解世界真相(目标)
+
+**公式二:差异化卖点**
+```
+同类作品常见设定 + 微创新/反转 = 差异化卖点
+```
+
+**示例**:
+- 常规重生文:主角碾压一切 → **微创新**:重生后发现未来已被改变
+- 常规系统文:无限奖励 → **反转**:系统每次奖励都有致命代价
+- 常规修仙文:主角天赋异禀 → **微创新**:主角无法修炼,靠"外挂"补救
+
+---
+
+## 2. 市场热点与创新机会识别
+
+### 2.1 热门卖点生命周期分析(2025)
+
+| 卖点类型 | 生命周期阶段 | 市场饱和度 | 创新机会 | 建议策略 |
+|---------|------------|-----------|---------|---------|
+| **都市重生** | 成熟期 | 高(红海) | 微创新(反派视角、女频视角) | 必须有差异化 |
+| **系统签到** | 成熟期 | 高(红海) | 结合其他元素(悬疑、推理) | 避免纯签到流 |
+| **规则怪谈** | 成长期 | 中等 | 跨题材融合(都市、玄幻) | 黄金入场期 |
+| **反派洗白** | 成长期 | 中等 | 视角创新(女频、甜宠) | 潜力较大 |
+| **无限流** | 成熟期 | 高(红海) | 女性向无限流 | 需深耕细分 |
+| **快穿** | 衰退期 | 极高 | 深度创新或转型 | 不建议新手 |
+
+### 2.2 市场空白点识别清单
+
+**检查以下组合是否已被充分开发**:
+
+- [ ] 题材A + 热门卖点B = 是否已有爆款?
+- [ ] 传统卖点 + 新视角(女频/反派/配角) = 是否有人尝试?
+- [ ] 热门卖点 + 反套路设定 = 是否足够新颖?
+- [ ] 跨题材融合(如:都市+规则怪谈) = 是否有成功案例?
+
+**市场空白示例**(2025年潜力方向):
+- 规则怪谈 + 都市背景(现代城市中的规则世界)
+- 重生 + 配角视角(重生成原著配角,改写剧情)
+- 系统 + 群像多主角(多人都有系统,相互博弈)
+- 反派洗白 + 女频甜宠(反派大佬洗白后追妻)
+
+### 2.3 读者群体迁移趋势(2025)
+
+**从"纯爽"到"爽+烧脑"**:
+- 读者越来越期待智商在线的主角
+- 单纯的金手指碾压已不足以吸引付费
+- 悬疑、推理、反转元素受欢迎
+
+**从"单一主角"到"群像多视角"**:
+- 多主角、多视角叙事开始流行
+- 配角也需要完整的成长线
+- 反派不再是工具人,要有合理动机
+
+**从"男频独大"到"性别平衡"**:
+- 女频市场快速增长
+- 男频作品开始尝试女性视角
+- 性别反转、性别平等主题受关注
+
+---
+
+## 3. 卖点与要素的系统性匹配
+
+### 3.1 主角身份与卖点匹配
+
+**匹配原则**:主角身份必须**最大化展现**核心卖点
+
+**示例分析**:
+
+| 核心卖点 | 最佳主角身份 | 为什么匹配 | 不匹配的身份 |
+|---------|------------|-----------|-------------|
+| 重生复仇 | 前世受害者 | 有复仇动机+信息差优势 | 无冤无仇的路人 |
+| 规则破解 | 逻辑能力强的推理者 | 能发现规则漏洞 | 无脑莽夫 |
+| 反派洗白 | 前世反派大佬 | 有洗白动机+反差萌 | 一直是好人 |
+| 系统签到 | 需要快速成长的弱者 | 签到能弥补弱势 | 本身就是天才 |
+| 商战权谋 | 商业天才/重生者 | 有商业知识+信息优势 | 对商业一无所知 |
+
+**主角性格与卖点匹配**:
+
+```
+卖点:重生复仇
+✅ 匹配性格:冷静、有城府、耐心布局
+❌ 不匹配:冲动、圣母、容易原谅
+
+卖点:规则怪谈
+✅ 匹配性格:谨慎、理性、观察力强
+❌ 不匹配:鲁莽、粗心、不动脑
+
+卖点:甜宠文
+✅ 匹配性格:温柔、体贴、会撩
+❌ 不匹配:木讷、直男、情商低
+```
+
+### 3.2 金手指与卖点匹配
+
+**匹配原则**:金手指必须**专门服务于**核心卖点,而非通用万能
+
+**金手指设计三问**:
+1. 这个金手指是否让卖点更突出?
+2. 没有这个金手指,卖点是否无法成立?
+3. 金手指是否会抢走卖点的风头?
+
+**匹配示例**:
+
+| 核心卖点 | 匹配的金手指 | 不匹配的金手指 |
+|---------|------------|---------------|
+| **重生复仇** | 前世记忆+关键信息 | 无敌战力(与复仇无关) |
+| **规则破解** | 看穿规则漏洞的能力 | 直接无视规则(破坏卖点) |
+| **系统签到** | 每日签到获得奖励 | 一次性获得无敌装备 |
+| **反派洗白** | 读心术(了解他人想法) | 时间倒流(太万能) |
+| **商战权谋** | 预知未来商机 | 武力值爆表(跑题) |
+
+**金手指升级逻辑与卖点关联**:
+```
+卖点:规则怪谈破解
+金手指升级路径:
+Lv1: 能看到基础规则
+Lv2: 能发现规则漏洞
+Lv3: 能预测规则变化
+Lv4: 能创造新规则
+
+每次升级都强化"破解规则"这个核心卖点
+```
+
+### 3.3 世界观与卖点匹配
+
+**匹配原则**:世界观规则必须**放大**卖点的价值
+
+**示例分析**:
+
+```
+卖点:重生带来的信息差优势
+
+✅ 匹配的世界观:
+- 股市/商战背景(信息差=财富)
+- 修仙世界(提前知道机缘位置)
+- 娱乐圈(知道哪些歌会火)
+
+❌ 不匹配的世界观:
+- 完全静态的世界(未来不会变化,重生无意义)
+- 随机性极强的世界(信息差无用)
+```
+
+```
+卖点:规则破解
+
+✅ 匹配的世界观:
+- 严格规则约束的怪谈世界
+- 有明确规则的游戏世界
+- 等级森严的修仙体系
+
+❌ 不匹配的世界观:
+- 无规则的混乱世界
+- 规则随时改变的世界
+```
+
+---
+
+## 4. 主线结构与卖点贯彻
+
+### 4.1 "大目标+小循环"发展模型
+
+**大目标**:围绕卖点设定的终极目标
+
+**小循环**:每个阶段重复强化卖点的情节模式
+
+**示例(重生复仇文)**:
+
+```
+大目标:彻底扳倒仇人,洗刷前世冤屈
+
+小循环(每10-15章一个循环):
+1. 发现仇人的一个弱点(信息差优势)
+2. 制定针对性计划(智商博弈)
+3. 执行计划并打脸(复仇爽点)
+4. 仇人反扑(制造新冲突)
+5. 主角更进一步(成长+复仇推进)
+
+每个循环都强化"重生复仇"这个核心卖点
+```
+
+### 4.2 阶段性卖点体现
+
+**三幕式结构与卖点贯彻**:
+
+| 阶段 | 字数范围 | 卖点体现方式 | 示例(规则怪谈) |
+|------|---------|------------|-----------------|
+| **第一幕(开局)** | 0-10万字 | 建立卖点,展示独特性 | 主角进入规则世界,展示破解能力 |
+| **第二幕(发展)** | 10-50万字 | 深化卖点,升级变化 | 规则越来越复杂,破解难度提升 |
+| **第三幕(高潮)** | 50万字+ | 卖点的终极体现 | 破解世界终极规则,揭开真相 |
+
+**每个阶段的卖点强化检查**:
+
+- [ ] 第一幕:读者是否在前3章就理解了核心卖点?
+- [ ] 第二幕:卖点是否在每个小高潮都有体现?
+- [ ] 第三幕:终极高潮是否是卖点的最强表现?
+
+### 4.3 情节贯彻卖点的四种方式
+
+**方式一:直接展示**
+```
+卖点:重生带来的先知先觉
+体现:主角直接预言某事,然后应验
+```
+
+**方式二:对比反差**
+```
+卖点:反派洗白
+体现:前世作恶 vs 今生行善,形成强烈对比
+```
+
+**方式三:冲突放大**
+```
+卖点:规则破解
+体现:规则越来越严格,破解难度越来越大
+```
+
+**方式四:角色互动**
+```
+卖点:系统签到
+体现:其他角色惊叹主角成长速度,凸显系统优势
+```
+
+---
+
+## 5. 差异化策略与创新方向
+
+### 5.1 同类作品对比分析法
+
+**步骤一**:找出3-5部同卖点的成功作品
+
+**步骤二**:分析它们的共同点与差异点
+
+| 作品 | 核心卖点 | 主角设定 | 金手指 | 差异化亮点 |
+|------|---------|---------|--------|-----------|
+| 作品A | 重生复仇 | 商战大佬 | 前世记忆 | 商战权谋 |
+| 作品B | 重生复仇 | 娱乐明星 | 前世记忆 | 娱乐圈打脸 |
+| 作品C | 重生复仇 | 学霸 | 前世记忆 | 高考改命 |
+
+**步骤三**:找到市场空白或微创新点
+
+```
+已有组合:重生复仇 + 商战/娱乐/学业
+市场空白:重生复仇 + 科研/体育/政坛
+微创新:重生复仇 + 反派视角(重生成反派的洗白)
+```
+
+### 5.2 六种差异化策略
+
+**策略一:视角创新**
+```
+常规:男性主角 → 创新:女性主角
+常规:正派主角 → 创新:反派主角
+常规:单一视角 → 创新:多视角群像
+```
+
+**策略二:人设反转**
+```
+常规:天才主角 → 创新:废材主角逆袭
+常规:冷酷主角 → 创新:软萌主角(反差萌)
+常规:全能主角 → 创新:有明显短板的主角
+```
+
+**策略三:金手指创新**
+```
+常规:无限奖励系统 → 创新:有代价的系统
+常规:全知全能 → 创新:只在特定领域强大
+常规:永久金手指 → 创新:有时限或次数限制
+```
+
+**策略四:世界观融合**
+```
+常规:纯都市 → 创新:都市+玄幻元素
+常规:纯修仙 → 创新:修仙+科技元素
+常规:纯历史 → 创新:历史+穿越元素
+```
+
+**策略五:节奏创新**
+```
+常规:慢热铺垫 → 创新:开篇即高潮
+常规:线性叙事 → 创新:多线交织+反转
+常规:单一爽点 → 创新:多类型爽点结合
+```
+
+**策略六:情感创新**
+```
+常规:纯爽文 → 创新:爽+虐+燃结合
+常规:无感情线 → 创新:感情线与主线深度融合
+常规:甜宠文 → 创新:先虐后甜(虐恋情深)
+```
+
+---
+
+## 6. 一致性机制与读者期待管理
+
+### 6.1 卖点贯彻的五大一致性原则
+
+**原则一:开篇承诺 = 全文兑现**
+```
+开篇承诺:"重生复仇"
+全文兑现:每个阶段都在推进复仇进度
+违反示例:前期复仇,后期变成种田文
+```
+
+**原则二:金手指强度保持平衡**
+```
+✅ 正确:金手指随主角成长逐步升级
+❌ 错误:开篇无敌 → 中期被吊打 → 后期又无敌
+```
+
+**原则三:主角性格不OOC**
+```
+✅ 正确:冷静主角始终理性决策
+❌ 错误:前期冷静,后期突然冲动送人头
+```
+
+**原则四:世界观规则不自相矛盾**
+```
+✅ 正确:修仙体系前后一致
+❌ 错误:前期说"境界不可越级",后期主角越级如喝水
+```
+
+**原则五:爽点频率稳定**
+```
+✅ 正确:每3-5章一个小爽点,每10-15章一个大爽点
+❌ 错误:前期爽点密集,后期10章没一个爽点
+```
+
+### 6.2 读者期待管理策略
+
+**策略一:明确承诺,及时兑现**
+```
+第1章:承诺"主角会复仇"
+第3章:给出第一次小复仇(兑现承诺)
+第10章:第一次大复仇(强化承诺)
+```
+
+**策略二:设置小目标,持续满足**
+```
+大目标:成为最强者
+小目标:
+- 第1阶段:成为班级第一
+- 第2阶段:成为学校第一
+- 第3阶段:成为全市第一
+每个小目标达成都给读者成就感
+```
+
+**策略三:适度留白,制造悬念**
+```
+明确告知:主角有重生优势
+留白:但未来是否会因主角改变而变化?
+制造悬念:主角能否真正改写命运?
+```
+
+**策略四:定期刷新,避免审美疲劳**
+```
+前10章:重生复仇(商战)
+10-20章:引入新敌人(权谋)
+20-30章:发现更大阴谋(悬疑)
+保持核心卖点,但换场景、换对手
+```
+
+### 6.3 卖点偏移预警机制
+
+**偏移信号识别**:
+
+- ⚠️ 读者评论:"怎么感觉变成XX文了?"
+- ⚠️ 订阅数据下降
+- ⚠️ 核心卖点连续3章未出现
+- ⚠️ 新引入的元素抢走卖点风头
+
+**纠偏方法**:
+
+1. **快速回归**:下一章立即强化核心卖点
+2. **节奏调整**:削减与卖点无关的情节
+3. **重新聚焦**:通过角色对话提醒读者核心卖点
+4. **读者互动**:在作者说中重申核心卖点
+
+---
+
+## 7. 实战检查清单
+
+### 7.1 核心定义清晰度检查
+
+- [ ] 能否用**一句话**说清核心卖点?
+- [ ] 这个卖点是否**独一无二**,而非泛泛的题材描述?
+- [ ] 读者看到这个卖点,是否会**立即产生兴趣**?
+
+**测试方法**:
+```
+请用一句话向陌生人介绍你的小说:
+❌ "这是一部都市重生文"(太宽泛)
+✅ "重生成反派大佬,用前世记忆洗白逆袭"(清晰)
+```
+
+### 7.2 要素匹配度检查
+
+- [ ] 主角身份是否**最大化展现**卖点?
+- [ ] 金手指是否**专门服务于**卖点?
+- [ ] 世界观是否**放大**卖点的价值?
+- [ ] 情节是否**每章都强化**卖点?
+
+### 7.3 差异化竞争力检查
+
+- [ ] 与同题材作品相比,有何**微创新**?
+- [ ] 这个差异化是否足以让读者**选择你的书**?
+- [ ] 差异化是否**可持续**(能支撑30万字+)?
+
+### 7.4 长期可持续性检查
+
+- [ ] 卖点能否支撑**30万字+**连载?
+- [ ] 卖点是否有**升级空间**(避免重复)?
+- [ ] 卖点是否有**冲突来源**(不会写到无事可写)?
+
+### 7.5 读者承诺兑现检查
+
+- [ ] 开篇承诺的爽点,是否在**前3章**就有体现?
+- [ ] 核心卖点是否**每10章**至少强化一次?
+- [ ] 读者期待的情节,是否**及时兑现**(不拖沓)?
+
+---
+
+## 8. 常见问题与解决方案
+
+### Q1: 卖点太多,不知道选哪个作为核心?
+
+**A**: 使用"删除法"
+- 依次删除每个卖点,看哪个删除后整个故事就不成立
+- 那个无法删除的卖点,就是核心卖点
+- 其他卖点作为辅助卖点或爽点设计
+
+**示例**:
+```
+候选卖点:重生 + 系统 + 商战
+删除重生 → 系统+商战仍然成立
+删除系统 → 重生+商战仍然成立
+删除商战 → 重生+系统变成泛泛的升级文
+结论:商战是核心卖点,重生和系统是辅助
+```
+
+### Q2: 卖点很好,但不知道怎么持续30万字+?
+
+**A**: 使用"阶梯式升级"
+- 将卖点分解为多个阶段
+- 每个阶段难度递增,但核心不变
+- 换场景、换对手、换规则,但不换卖点
+
+**示例(规则怪谈)**:
+```
+第1阶段:破解基础规则(简单)
+第2阶段:破解复合规则(中等)
+第3阶段:破解动态规则(困难)
+第4阶段:破解终极规则(地狱)
+卖点不变:破解规则
+但难度和深度逐步提升
+```
+
+### Q3: 卖点同质化严重,如何差异化?
+
+**A**: 使用"组合创新法"(参见 `creative-combination.md`)
+- A卖点 + B视角 = 新卖点
+- A卖点 + C人设 = 新卖点
+- A卖点 + D世界观 = 新卖点
+
+**示例**:
+```
+常规:重生复仇(男主)
+创新:重生复仇(女主)
+创新:重生复仇(反派视角)
+创新:重生复仇(配角视角)
+```
+
+### Q4: 卖点在开篇很吸引人,但中后期乏力怎么办?
+
+**A**: 建立"卖点升级路径"
+- 开篇:展示卖点基础形态
+- 中期:卖点遇到挑战,被迫升级
+- 后期:卖点的终极形态
+
+**示例(系统签到)**:
+```
+开篇:每日签到获得小奖励
+中期:发现隐藏签到点,奖励更好
+后期:签到系统进化,可自定义奖励
+卖点始终是"签到",但玩法在升级
+```
+
+### Q5: 读者说"卖点偏移了",如何纠正?
+
+**A**: 三步纠偏法
+1. **立即回归**:下一章强化核心卖点
+2. **删减无关情节**:砍掉与卖点无关的支线
+3. **读者互动**:在作者说中重申核心卖点
+
+**示例**:
+```
+偏移情况:重生复仇文写成了种田文
+纠偏步骤:
+1. 下一章立即安排复仇情节
+2. 删减种田细节描写
+3. 作者说:"接下来会加快复仇节奏"
+```
+
+---
+
+## 9. 核心卖点定位模板
+
+### 9.1 一句话卖点公式
+
+```markdown
+## 核心卖点定位
+
+**一句话卖点**:
+[主角身份/优势] + [核心行动] + [独特目标]
+
+**示例**:
+重生商战大佬(身份)+ 回到高考前(行动)+ 改写人生巅峰(目标)
+
+**独特体验**:
+读者能从这个卖点中获得什么?(爽/虐/燃/甜/悬疑等)
+
+**情感共鸣**:
+这个卖点触发读者的哪种情感需求?(复仇/成长/逆袭/治愈等)
+
+**读者承诺**:
+向读者承诺的核心阅读体验是什么?
+```
+
+### 9.2 要素匹配设计表
+
+```markdown
+## 核心要素匹配
+
+### 主角设定
+- **身份**:[必须能最大化展现卖点]
+- **性格**:[必须与卖点匹配]
+- **动机**:[必须与卖点一致]
+- **成长路径**:[围绕卖点展开]
+
+### 金手指设计
+- **类型**:[专门服务于卖点]
+- **核心功能**:[强化卖点的独特性]
+- **升级逻辑**:[与卖点深度绑定]
+- **限制条件**:[避免金手指抢风头]
+
+### 世界观架构
+- **世界类型**:[放大卖点的价值]
+- **核心规则**:[与卖点相互支撑]
+- **冲突来源**:[围绕卖点设计]
+- **发展空间**:[为卖点提供持续舞台]
+```
+
+### 9.3 主线结构设计表
+
+```markdown
+## 主线结构(围绕卖点)
+
+### 终极目标
+[与核心卖点直接相关的终极目标]
+
+### 阶段分解
+
+**第一阶段**(0-10万字)
+- 地图/场景:[具体设定]
+- 核心冲突:[与卖点相关]
+- 卖点体现:[如何展示卖点]
+- 成长收获:[推进卖点进度]
+
+**第二阶段**(10-30万字)
+- 地图/场景:[升级场景]
+- 核心冲突:[冲突升级]
+- 卖点体现:[深化卖点]
+- 成长收获:[卖点进化]
+
+**第三阶段**(30万字+)
+- 地图/场景:[终极舞台]
+- 核心冲突:[终极对抗]
+- 卖点体现:[卖点的最强形态]
+- 成长收获:[卖点终极兑现]
+
+### 小循环设计
+[每10-15章重复的情节模式,确保卖点持续强化]
+```
+
+### 9.4 一致性检查表
+
+```markdown
+## 卖点贯彻一致性检查
+
+### 开篇承诺
+- [ ] 前3章是否明确展示核心卖点?
+- [ ] 承诺的爽点是否在开篇就有体现?
+
+### 中期强化
+- [ ] 每10章是否至少强化一次卖点?
+- [ ] 卖点是否在每个小高潮都有体现?
+
+### 后期兑现
+- [ ] 终极高潮是否是卖点的最强表现?
+- [ ] 结局是否完整兑现开篇承诺?
+
+### 全程一致
+- [ ] 金手指强度是否保持平衡?
+- [ ] 主角性格是否前后一致?
+- [ ] 世界观规则是否无矛盾?
+- [ ] 爽点频率是否稳定?
+```
+
+---
+
+## 10. 关联参考文档
+
+本文档是「创意阶段」的核心方法论,完成后可参考:
+
+- **前置步骤**:`inspiration-collection.md`(提供初步创意方向)
+- **前置步骤**:`market-positioning.md`(明确目标读者与市场定位)
+- **前置步骤**:`creative-combination.md`(通过元素组合强化卖点)
+- **下一步**:设定阶段的世界观构建、角色设定、故事框架(将卖点具体化)
+- **后续验证**:创作阶段的状态管理(检查卖点是否持续贯彻)
+
+---
+
+**使用建议**:核心卖点是整个创作的灵魂,应在创意阶段就明确定位,并在后续所有阶段(设定、框架、创作)中持续强化。每次大纲调整或情节设计前,都应回顾核心卖点,确保所有创作要素都服务于这个唯一的吸引力核心。

+ 115 - 0
.claude/skills/webnovel-init/references/genre-tropes.md

@@ -0,0 +1,115 @@
+---
+name: genre-tropes
+purpose: 项目初始化和大纲规划时加载,选择合适的题材套路
+---
+
+<context>
+此文件用于题材套路参考。Claude 已知一般叙事套路,这里只补充中文网文特定的题材公式和爽点模板。
+</context>
+
+<instructions>
+
+## 修仙题材套路
+
+### 经典金手指
+
+| 类型 | 核心机制 | 典型爽点 |
+|------|---------|---------|
+| 吞噬系统 | 吞噬敌人获得能力 | 越级击杀 → 实力暴涨 |
+| 签到系统 | 每日签到获得奖励 | 签到神器 → 秒杀同阶 |
+| 戒指老爷爷 | 强者灵魂指导 | 传授绝学 → 打脸天才 |
+| 重生流 | 带着前世记忆 | 先知先觉 → 夺取机缘 |
+| 特殊体质 | 天生修炼速度快 | 三天突破 → 震惊众人 |
+
+### 典型剧情套路
+
+**退婚流**: 悔婚 → 主角崛起 → 打脸 → 对方后悔
+**打脸路人流**: 嘲讽 → 展示实力 → 震惊/下跪
+**秘境夺宝流**: 进入 → 遇险 → 争夺 → 获宝 → 实力暴涨
+**宗门大比流**: 报名被嘲 → 连胜 → 决赛逆转 → 震惊全场
+
+## 都市题材套路
+
+### 经典金手指
+
+| 类型 | 核心机制 | 典型爽点 |
+|------|---------|---------|
+| 透视眼 | 看穿物品/人体 | 赌石暴富 |
+| 医术系统 | 治愈疑难杂症 | 治好权贵 → 获得资源 |
+| 武道传承 | 古武世家传承 | 击败外国高手 |
+| 超能力 | 念力/雷电/火焰 | 装逼打脸 |
+
+### 典型剧情套路
+
+**装逼打脸流**: 被看不起 → 展示身份 → 震惊后悔
+**扮猪吃虎流**: 隐藏身份 → 关键暴露 → 众人跪地
+
+## 反派类型库
+
+| 类型 | 特征 | 打脸方式 | 适用阶段 |
+|------|------|---------|----------|
+| 纨绔少爷 | 嚣张跋扈 | 当众击败 | 前期 |
+| 伪君子长老 | 表面正派 | 当众揭露 | 中期 |
+| 同门师兄 | 嫉妒天赋 | 反杀 | 前期 |
+| 敌对宗门 | 宗门仇恨 | 歼灭小队 | 中后期 |
+
+## 套路使用建议
+
+**70% 套路 + 30% 创新**:
+- 70% 套路:满足读者预期,提供爽感
+- 30% 创新:制造惊喜,避免无聊
+
+**避免过度使用**:
+- 同一套路连续使用不超过 3 次
+- 反派台词不要完全一样
+- 插入意外元素(反派认怂/主角翻车)
+
+</instructions>
+
+<examples>
+
+<example>
+<input>设计退婚流剧情</input>
+<output>
+**第 1-5 章**: 家族没落 → 未婚妻悔婚 → 主角获得金手指
+**第 6-10 章**: 主角低调修炼 → 小有成就
+**第 11-15 章**: 参加比武 → 击败未婚妻新欢 → 未婚妻震惊
+**第 16-20 章**: 未婚妻求复合 → 主角拒绝
+
+经典对白: "当年你对我爱答不理,今日我让你高攀不起"
+</output>
+</example>
+
+<example>
+<input>设计打脸路人场景</input>
+<output>
+**路人**: "就你这废物也配参加大比?"
+**主角**: (不语,一招秒杀)
+**路人**: "怎么可能!他明明是练气九层,怎么有筑基期的实力?"
+**旁观者**: "那是...天雷掌!只有亲传弟子才能学会的绝技!"
+
+使用频率: 每 3-5 章插入 1 次
+</output>
+</example>
+
+<example>
+<input>设计秘境夺宝结构</input>
+<output>
+**第 1 章**: 进入秘境 + 遇险(凶兽袭击)
+**第 2 章**: 发现宝物线索 + 遇到竞争对手
+**第 3-4 章**: 与竞争对手争夺 + 激战
+**第 5 章**: 主角获胜 + 夺得宝物
+**第 6 章**: 吸收宝物 + 实力暴涨 + 震惊同门
+
+关键: 必须有竞争对手,宝物必须"千年难遇"
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 同一套路连续 3 次以上 → ✅ 套路变体或插入意外
+❌ 反派台词完全一样 → ✅ 根据反派类型定制
+❌ 完全照搬不改 → ✅ 结合项目设定调整细节
+❌ 爽点公式不完整 → ✅ 确保"震惊"环节不省略
+</errors>

+ 42 - 0
.claude/skills/webnovel-init/references/system-data-flow.md

@@ -0,0 +1,42 @@
+---
+name: system-data-flow-redirect
+purpose: 重定向到权威版本
+---
+
+<context>
+此文件已迁移到统一位置,避免多版本不同步问题。
+</context>
+
+<instructions>
+
+## 权威版本位置
+
+`skills/webnovel-query/references/system-data-flow.md` (v5.0)
+
+## 加载方式
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/system-data-flow.md"
+```
+
+## 快速参考
+
+### 目录结构
+```
+项目根目录/
+├── 正文/           # 章节文件
+├── 大纲/           # 卷纲/章纲
+├── 设定集/         # 世界观/力量体系/角色卡
+└── .webnovel/
+    ├── state.json          # 权威状态
+    ├── workflow_state.json # 工作流断点
+    ├── index.db            # SQLite 索引
+    └── archive/            # 归档数据
+```
+
+### v5.0 核心变化
+- **双 Agent 架构**: Context Agent (读) + Data Agent (写)
+- **无 XML 标签**: 纯正文写作,Data Agent AI 自动提取实体
+- **统一 schema**: entities_v3 + alias_index(一对多)
+
+</instructions>

+ 89 - 0
.claude/skills/webnovel-init/references/worldbuilding/character-design.md

@@ -0,0 +1,89 @@
+# 人物设计指南 (Character Design)
+
+> **核心原则**: 人物是剧情的载体。主角负责“爽”,反派负责“阻”,配角负责“捧”和“补”。
+
+---
+
+## 1. 主角设计 (Protagonist)
+
+### 核心三要素
+1. **欲望 (Desire)**: 他最想要什么?(长生?复仇?守护妹妹?回家?)
+   - *必须足够强烈,贯穿全书。*
+2. **弱点 (Flaw)**: 他的性格缺陷是什么?(贪财?好色?傲慢?太重感情?)
+   - *没有弱点的主角不可爱,完美的圣人令人厌烦。*
+3. **金手指 (Cheat)**: 他的核心优势。(见 `golden-finger-templates.md`)
+
+### 人设模板:网文经典款
+- **废柴流**: 起点低,被嘲讽,后期打脸。(适合小白文,爽感强)
+- **天才流**: 起点高,遭陷害,陨落再崛起。(适合老白文,虐主开局)
+- **苟道流**: 实力强,但极度谨慎,不出头。(适合轻松向,反套路)
+- **魔头流**: 利益至上,杀伐果断,不圣母。(适合现在的市场风向)
+
+### 代入感设计
+让读者觉得“这就是我”。
+- **身世**: 平凡(方便代入)或 极惨(引发同情)。
+- **三观**: 必须正向(至少底线要正,比如爱国、护短、孝顺),反派主角也要有原则。
+
+---
+
+## 2. 反派设计 (Antagonist)
+
+### 反派的等级
+- **C 级 (脸谱怪)**: 纯粹为了送经验和嘲讽而存在。无脑,嚣张。“你敢惹我?你知道我是谁吗?”
+- **B 级 (利益冲突)**: 为了争夺资源/地位而与主角对立。智商在线,输在实力或运气。
+- **A 级 (理念之争)**: 有自己的信仰和正义,只是立场不同。令人敬佩的对手。
+- **S 级 (宿命之敌)**: 与主角互为镜像,深层羁绊。如:杀死主角父母的凶手,或者是主角失散多年的黑化兄弟。
+
+### 反派塑造技巧
+- **不要为了坏而坏**: 给他一个理由(为了复活爱人而毁灭世界)。
+- **保持压迫感**: 反派必须比主角强(至少在前期),让主角感到恐惧和无力,这样反杀时才爽。
+- **智商在线**: 不要让反派犯低级错误,主角赢要赢得漂亮。
+
+---
+
+## 3. 女主设计 (Heroine)
+
+### 类型库
+- **青梅竹马**: 温柔、陪伴、白月光。
+- **冰山师尊/仙子**: 高冷、强大、反差萌。
+- **魔教妖女**: 古灵精怪、撩人、危险。
+- **傲娇大小姐**: 口嫌体正直、欢喜冤家。
+
+### 避免“花瓶”
+- 女主必须有自己的**高光时刻**和**剧情作用**(如:她是唯一能开启秘境的人,或者她替主角挡了一刀)。
+- 感情线要**服务于主线**,不要为了谈恋爱而拖慢升级节奏(除非是言情文)。
+
+---
+
+## 4. 配角设计 (Supporting Cast)
+
+### 黄金配角组合
+1. **死党/跟班**: 负责捧哏、收集情报、替主角吹牛。
+   - *功能*: 嘴替,借他的口说出主角有多牛。
+2. **导师/引路人**: 负责解说设定、提供庇护、最后献祭(刀子)。
+   - *功能*: 新手引导员。
+3. **搞笑担当**: 负责调节气氛,缓解紧张感。
+
+### “工具人”觉醒
+即使是只有几句台词的龙套,也要给他一个**记忆点**(如:口头禅、特殊的武器、奇怪的走路姿势)。
+
+---
+
+## 🛠️ 人物卡模板 (Character Sheet)
+
+在 `/webnovel-init` 生成的 `设定集/主角卡.md` 中,应包含:
+
+```markdown
+**姓名**: 林天
+**称号**: 噬神者 / 天南第一狂人
+**外貌**: 清秀少年,眼神坚毅,嘴角常带一丝冷笑
+**性格**: 
+  - 显性: 随和、幽默、有点小贪财
+  - 隐性: 极度记仇、护短、疯狂
+**核心动机**: 复活死去的妹妹
+**金手指**: 吞噬系统
+**人际关系**:
+  - 妹妹(林雨):逆鳞,触之必死
+  - 死党(王胖子):钱袋子
+  - 宿敌(叶良辰):夺妻之恨(误)
+```

+ 179 - 0
.claude/skills/webnovel-init/references/worldbuilding/faction-systems.md

@@ -0,0 +1,179 @@
+# 势力体系设计指南 (Faction Systems)
+
+> **核心原则**: 势力不是背景板,而是剧情的推动器。好的势力设计能让主角的每一次选择都充满戏剧张力。
+
+---
+
+## 1. 势力分层架构 (Hierarchy)
+
+### 金字塔模型
+```
+顶层势力 (1-3个)
+  ↓ 控制/影响
+中层势力 (5-10个)
+  ↓ 依附/对抗
+底层势力 (数十个)
+  ↓ 争夺资源
+散修/平民 (无数)
+```
+
+### 顶层势力 (Top-Tier)
+- **定义**: 掌握世界规则的制定权(如:修仙界的六大圣地)
+- **特征**:
+  - 拥有最强战力(至少有一位顶尖强者坐镇)
+  - 控制核心资源(如:唯一的传送阵、最大的灵脉)
+  - 历史悠久(传承千年以上)
+- **与主角关系**: 初期仰望 → 中期接触 → 后期制衡/取代
+
+### 中层势力 (Mid-Tier)
+- **定义**: 区域霸主(如:一城一国的统治者)
+- **特征**:
+  - 在顶层势力的夹缝中求生存
+  - 内部派系林立,政治斗争激烈
+  - 对底层势力有绝对话语权
+- **与主角关系**: 初期庇护所 → 中期跳板 → 后期被超越
+
+### 底层势力 (Low-Tier)
+- **定义**: 家族、帮派、小宗门
+- **特征**:
+  - 资源匮乏,随时可能灭亡
+  - 依附于中层势力以获得保护
+  - 主角的新手村
+- **与主角关系**: 起点 → 快速超越 → 回归时碾压
+
+---
+
+## 2. 势力关系网 (Relationship Network)
+
+### 四大关系类型
+
+#### A. 世仇 (Blood Feud)
+- **成因**: 历史积怨(如:争夺圣地、灭族之恨)
+- **特点**: 见面必打,不死不休
+- **剧情作用**: 主角加入一方,自动树敌另一方
+
+#### B. 利益同盟 (Alliance)
+- **成因**: 共同利益(如:对抗兽潮、开发秘境)
+- **特点**: 表面和气,暗地较劲
+- **剧情作用**: 合作任务中暴露各方真面目,产生新矛盾
+
+#### C. 附庸关系 (Vassal)
+- **成因**: 实力差距悬殊,弱者依附强者
+- **特点**: 强者提供保护,弱者上贡资源
+- **剧情作用**: 主角所在势力被欺压,激发反抗剧情
+
+#### D. 中立竞争 (Neutral Competition)
+- **成因**: 没有深仇大恨,但在争夺同一资源
+- **特点**: 井水不犯河水,除非利益冲突
+- **剧情作用**: 比武招亲、拍卖会竞价、秘境夺宝
+
+---
+
+## 3. 势力设计模板 (Templates)
+
+### 模板 1: 宗门/学院
+```markdown
+**名称**: 天剑宗
+**类型**: 剑修宗门(顶层势力)
+**核心资源**:
+  - 祖师遗留的剑冢(内有百把神剑)
+  - 掌握一条极品剑灵脉
+**标志性功法**: 《万剑归宗诀》
+**势力等级**: S级(六大圣地之一)
+**内部结构**:
+  - 宗主一脉(务实派)
+  - 大长老一脉(守旧派)
+  - 剑阁长老(中立派)
+**外交策略**:
+  - 与天云宗世代联姻
+  - 与魔道势力不死不休
+  - 对中立商会保持友好
+**主角定位**: 外门弟子 → 亲传弟子 → 接任宗主
+```
+
+### 模板 2: 家族/血脉
+```markdown
+**名称**: 叶家
+**类型**: 修仙世家(中层势力)
+**核心资源**:
+  - 祖传血脉(火属性天赋)
+  - 三座城池的税收
+**标志性功法**: 《焚天诀》
+**势力等级**: A级(地方豪门)
+**内部结构**:
+  - 嫡系(纯血)vs 旁系(杂血)
+  - 长房、二房、三房的权力斗争
+**外交策略**:
+  - 依附于天剑宗
+  - 联合其他家族对抗朝廷
+**主角定位**: 被逐出家族 → 荣归故里打脸
+```
+
+### 模板 3: 反派组织
+```markdown
+**名称**: 血神教
+**类型**: 魔道邪教(顶层反派)
+**核心资源**:
+  - 禁忌的血祭之法
+  - 掌控一片死灵之地
+**标志性功法**: 《血神经》
+**势力等级**: S级(魔道三大势力之首)
+**内部结构**:
+  - 教主(半疯状态,实力恐怖)
+  - 四大护法(各有异心)
+  - 无数狂信徒
+**外交策略**:
+  - 与正道势力不死不休
+  - 暗中扶植傀儡王朝
+**主角定位**: 宿敌(杀主角全家的凶手)
+```
+
+---
+
+## 4. 势力剧情引擎 (Plot Drivers)
+
+### 内部矛盾(推动主角崛起)
+1. **权力斗争**: 主角被卷入派系之争,被迫站队。
+2. **资源分配**: 核心资源(如灵脉、传承)的争夺。
+3. **叛徒/内鬼**: 势力内部有人勾结外敌。
+
+### 外部冲突(创造危机)
+1. **灭门危机**: 敌对势力突然来袭,主角被迫背水一战。
+2. **资源争夺**: 秘境开启,各大势力混战。
+3. **联盟破裂**: 原本的盟友因利益分配不均反目。
+
+---
+
+## 5. 势力崛起剧情线 (Rise to Power)
+
+主角从零开始建立自己的势力(适用于爽文)。
+
+### 阶段 1: 草创期
+- **起因**: 主角被原势力背叛/逐出,或发现某个被遗弃的宗门遗址。
+- **初始成员**: 3-5人的小队(死党、救命恩人、被救的弱者)。
+- **第一桶金**: 夺取小型资源点(如:矿脉、药园)。
+
+### 阶段 2: 扩张期
+- **招兵买马**: 收服散修、吸纳小家族。
+- **建立名声**: 通过击败强敌、救助弱者、公开挑战旧势力。
+- **占领地盘**: 从一座城扩张到一个区域。
+
+### 阶段 3: 争霸期
+- **制度建设**: 建立功法库、炼丹房、藏书阁。
+- **外交手段**: 与其他中层势力结盟,孤立敌对势力。
+- **培养死忠**: 提拔心腹,形成核心圈子。
+
+### 阶段 4: 霸主期
+- **挑战顶层**: 击败或收服原六大圣地。
+- **制定新规则**: 改变世界的运行方式(如:废除奴隶制、开放传承)。
+
+---
+
+## 🛠️ 设计检查清单
+
+在 `/webnovel-plan` 阶段,检查势力设计是否合格:
+- [ ] **层级清晰**: 顶层、中层、底层势力是否有明显的实力差距?
+- [ ] **关系复杂**: 至少有3对以上的敌对/同盟关系?
+- [ ] **内部矛盾**: 每个重要势力是否有内部派系斗争?
+- [ ] **剧情推动**: 势力冲突是否能自然产生新的剧情线?
+- [ ] **主角定位**: 主角在每个阶段与各大势力的关系是否明确?

+ 95 - 0
.claude/skills/webnovel-init/references/worldbuilding/power-systems.md

@@ -0,0 +1,95 @@
+# 力量体系设计指南 (Power Systems)
+
+> **核心原则**: 力量体系不仅是升级的阶梯,更是控制剧情节奏的阀门。好的体系能让读者清晰地感知“变强”,同时避免后期数值崩坏。
+
+---
+
+## 1. 等级体系构建 (Leveling)
+
+### 标准阶梯模型
+设定明确的等级,每级之间有显著的实力差距。
+- **大境界**: 质变(如:练气 → 筑基,凡人 → 修士)。
+  - **差距描述**: “筑基之下皆蝼蚁”,“金丹强者恐怖如斯”。
+- **小境界**: 量变(如:初期 → 中期 → 后期 → 圆满)。
+  - **差距描述**: “灵力更加深厚”,“能多放两个大招”。
+
+### 命名规范
+- **修仙**: 练气、筑基、金丹、元婴、化神...
+- **异能**: F级、E级、D级... S级、SS级、SSS级。
+- **玄幻**: 斗者、斗师、大斗师... / 搬血、洞天、化灵...
+- **西幻**: 学徒、正式法师、大魔法师、魔导士、传奇、半神。
+
+---
+
+## 2. 战力平衡与膨胀控制
+
+### 越级挑战原则 (The Underdog Rule)
+主角必须能越级,但不能无限越。
+- **合理越级**: 练气九层打败筑基初期(跨一个大境界门槛)。
+  - **理由**: 极品功法 + 绝世神兵 + 偷袭 + 对方轻敌。
+- **不合理越级**: 练气三层打败元婴老怪。
+  - **后果**: 战力体系瞬间崩塌,读者弃书。
+
+### 数值膨胀控制阀 (Inflation Control)
+- **换地图 (Map Reset)**: 在新手村称霸后,进入大地图发现自己只是底层。
+  - *示例*: “在天南城我是第一天才,到了中州圣地,我也就勉强算个内门弟子。”
+- **新维度 (New Dimension)**: 引入新的力量参数。
+  - *示例*: 前期拼灵力总量,后期拼法则感悟/灵魂强度/领域掌控。
+
+---
+
+## 3. 技能与功法体系
+
+### 技能树设计
+- **基础技**: 平 A,冷却短,消耗低。
+- **爆发技**: 大招,冷却长,消耗大,也是“底牌”。
+- **被动技**: 常驻增益(如:回血速度+100%,对火属性免疫)。
+- **禁术**: 威力巨大但有严重副作用(如:燃烧寿命换取 10 倍战力)。
+
+### 功法分级
+- **天地玄黄 / SABC**: 简单明了,读者认知成本低。
+- **唯一性**: 主角必须修练全书唯一的、或失传已久的顶级功法。
+
+---
+
+## 4. 力量体系模板 (Reference)
+
+### 模板 A: 传统修仙
+```
+练气 (1-9层): 引气入体,强身健体
+筑基 (初中后): 铸就道基,寿元两百
+金丹 (一至九转): 凝结金丹,御空飞行
+元婴: 碎丹成婴,肉身损毁可夺舍
+化神: 领悟天地法则,初步掌控空间
+炼虚: 遨游太虚
+合体: 身化万千
+大乘: 渡劫飞升
+```
+
+### 模板 B: 高武/异能
+```
+一品/二品/三品 (武徒): 淬炼皮肉骨
+四品/五品/六品 (武师): 练出内劲/罡气
+七品 (宗师): 精神力外放,宗师不可辱
+八品 (大宗师): 凝练金身
+九品 (武圣): 镇压国运
+绝巅: 人类守护神
+```
+
+### 模板 C: 诡秘/序列 (魔药体系)
+```
+序列 9 (起始): 获得微弱超凡能力
+...
+序列 5 (质变): 获得神性
+...
+序列 0 (真神): 掌握权柄
+*特点*: 晋升需要扮演法 + 魔药,有失控风险。
+```
+
+---
+
+## 🛠️ 设计检查清单
+
+- [ ] **可视化**: 读者能否通过面板/描述直观感受到主角变强了?(如:一拳打爆测试机)
+- [ ] **瓶颈**: 升级是否有难度?(需要丹药/机缘/心境突破,不能光靠挂机)
+- [ ] **天花板**: 世界最强者的实力设定好了吗?(作为主角的长期追赶目标)

+ 215 - 0
.claude/skills/webnovel-init/references/worldbuilding/setting-consistency.md

@@ -0,0 +1,215 @@
+# 设定自洽性检查指南 (Setting Consistency Check)
+
+> **核心原则**: 设定可以天马行空,但逻辑必须自洽。读者可以接受"魔法存在",但无法接受"这个世界的魔法时灵时不灵"。
+
+---
+
+## 1. 自洽性的三大支柱
+
+### 物理法则自洽 (Physics Consistency)
+世界的基本规则一旦确立,就不能随意打破。
+
+#### 正面案例 ✅
+```
+设定: 修仙者飞行需要消耗灵力
+→ 第100章: 主角灵力耗尽,从空中坠落
+→ 逻辑自洽,读者认可
+```
+
+#### 反面案例 ❌
+```
+设定: 凡人无法修炼,需要觉醒灵根
+→ 第50章: 主角是凡人,通过刻苦努力修炼成功
+→ 前后矛盾,读者质疑:"那要灵根干嘛?"
+
+修复方案:
+方案A: 主角其实是隐性灵根(后期觉醒)
+方案B: 主角获得外挂道具(如:夺灵珠,可窃取他人灵根)
+```
+
+### 力量体系自洽 (Power System Consistency)
+等级压制、战力对比必须前后一致。
+
+#### 常见问题
+| 问题类型 | 案例 | 修复方案 |
+|---------|------|---------|
+| **战力崩坏** | 第10章说"金丹强者一掌毁城",<br/>第100章金丹只能打爆一座房子 | 引入新维度(城有护城大阵)或承认前期夸张了(回忆杀修正) |
+| **越级无限制** | 主角从练气越级打金丹,后来练气十层连筑基都打不过 | 设定越级条件(需要神兵/阵法/偷袭,不可常态化) |
+| **境界失效** | 设定境界越高寿命越长,但某大佬活了十万年却还是元婴 | 解释为特殊体质/夺舍/时间流速不同 |
+
+### 社会生态自洽 (Social Ecology Consistency)
+世界的经济、政治、文化必须符合基本逻辑。
+
+#### 自洽检查清单
+- [ ] **货币体系**: 灵石/金币的购买力是否稳定?(不要第10章一颗丹药1000灵石,第50章同样的丹药只要10灵石)
+- [ ] **势力平衡**: 为什么正邪两道能并存千年?(必须有制衡因素,如:绝世强者约定、共同敌人、资源互补)
+- [ ] **人口比例**: 修士与凡人的数量比是否合理?(如果人人都能修仙,谁来种地?)
+
+---
+
+## 2. 常见逻辑漏洞与修复
+
+### 漏洞 1: "反派智商突然下降"
+**表现**: 前期深谋远虑的反派,到了主角要打脸时突然变蠢。
+
+**修复方案**:
+- **预设陷阱**: 反派看似愚蠢,实际在钓鱼(将计就计)。
+- **信息差**: 反派不知道主角已突破/获得神器,所以轻敌。
+- **内部分裂**: 反派势力内部有叛徒,故意泄露情报。
+
+### 漏洞 2: "设定为剧情服务而扭曲"
+**表现**:
+- 设定: 使用禁术会燃烧寿命
+- 剧情: 主角用了十次禁术,毫无副作用
+- 读者: "说好的燃烧寿命呢?"
+
+**修复方案**:
+- **提前埋伏笔**: 主角获得"不死之身"类的外挂,可以免疫副作用。
+- **后期还债**: 寿命在暗中燃烧,后期突然爆发危机(触发新剧情)。
+- **等价交换**: 每次使用禁术后,明确展示主角付出的代价(如:头发变白、境界跌落)。
+
+### 漏洞 3: "时间线混乱"
+**表现**:
+- 第30章: "这个秘境只在百年一次的血月之夜开启,上次开启是50年前"
+- 第80章: "50年前血月之夜,那位前辈进入秘境后失踪"
+- 读者: "那这次血月是第几次?时间对不上啊!"
+
+**修复方案**:
+- **建立时间轴文档**: 记录所有重大事件的时间点(见下方模板)。
+- **统一纪年法**: 使用"修真历XXX年"或"主角15岁那年"作为锚点。
+
+---
+
+## 3. 设定自洽检查模板
+
+### 世界法则自查表
+```markdown
+## 核心法则 (铁律,不可违背)
+1. **死亡不可逆**: 死人无法复活(除非……特殊条件)
+2. **境界压制**: 低境界无法伤害高境界(除非……外挂/阵法)
+3. **能量守恒**: 使用力量必有消耗(灵力/体力/寿命)
+
+## 可变法则 (有前置条件可打破)
+1. **飞行限制**: 筑基以下无法飞行
+   - 例外: 乘坐飞舟/被高手带飞
+2. **寿命上限**: 金丹寿命500年
+   - 例外: 服用延寿丹/特殊体质
+
+## 当前版本号
+v1.2(每次修改设定时更新版本号,并记录变更理由)
+```
+
+### 力量体系自查表
+```markdown
+## 境界战力对照表
+| 境界 | 代表性战绩 | 寿命 | 灵力总量 |
+|------|-----------|------|---------|
+| 练气 | 徒手碎石 | 100年 | 100 |
+| 筑基 | 御剑飞行,一人灭百人军队 | 200年 | 1,000 |
+| 金丹 | 一掌毁小镇,可短时间改变地貌 | 500年 | 10,000 |
+| 元婴 | 毁灭一国,翻江倒海 | 1,000年 | 100,000 |
+
+## 越级规则
+- **最大跨度**: 主角可越1个大境界(练气杀筑基)
+- **必要条件**: 神兵+阵法+偷袭+敌方轻敌
+- **不可常态化**: 每次越级必须付出惨重代价(重伤/消耗底牌)
+```
+
+### 时间轴自查表
+```markdown
+## 主线时间轴
+| 事件 | 主角年龄 | 修真历年份 | 备注 |
+|------|---------|-----------|------|
+| 出生 | 0岁 | 10050年 | - |
+| 觉醒灵根 | 15岁 | 10065年 | 双灵根,资质平平 |
+| 进入宗门 | 16岁 | 10066年 | 天剑宗外门弟子 |
+| 获得系统 | 17岁 | 10067年 | 掉落悬崖,奇遇 |
+| 筑基成功 | 20岁 | 10070年 | 用时3年(同代最快) |
+
+## 副线时间轴(反派)
+| 事件 | 时间 | 影响 |
+|------|------|------|
+| 血神教建立 | 5000年前 | 正魔大战爆发 |
+| 上任血神教主陨落 | 100年前 | 当前教主上位(实力未恢复) |
+| 血神教密谋复活血神 | 当前 | 主线冲突起因 |
+```
+
+---
+
+## 4. 动态维护:设定集文件结构
+
+在 `/webnovel-init` 生成的项目中,应包含以下文件:
+
+```
+设定集/
+├── 世界观设定.md        # 物理法则、地理、历史
+├── 力量体系设定.md      # 境界、功法、战力对照表
+├── 势力设定.md          # 各大宗门/家族/势力关系
+├── 人物卡/             # 每个重要角色一个文件
+│   ├── 主角-林天.md
+│   ├── 反派-叶良辰.md
+│   └── 师尊-云仙子.md
+├── 时间轴.md            # 主线+副线时间轴
+└── 设定变更日志.md      # 记录每次修改设定的原因
+```
+
+### 设定变更日志模板
+```markdown
+## v1.0 → v1.1 变更记录
+**日期**: 2025-01-01
+**变更内容**:
+- 修改:金丹寿命从300年改为500年
+- 原因:与第50章"那位金丹前辈活了400年"的描述冲突
+- 影响范围:需回查前30章所有涉及金丹寿命的描述
+
+## v1.1 → v1.2 变更记录
+**日期**: 2025-01-10
+**变更内容**:
+- 新增:引入"法则感悟"维度
+- 原因:避免后期数值膨胀(不能一直靠灵力总量决胜负)
+- 影响范围:从元婴期开始,法则感悟成为新的战力维度
+```
+
+---
+
+## 5. 实时检查工作流(写作中使用)
+
+### 每次写新章节前
+1. **回顾设定集**: 确认本章涉及的设定(境界/势力/时间)
+2. **搜索历史章节**: 用关键词搜索之前是否写过类似设定
+3. **标记矛盾点**: 如发现前后矛盾,立即记录到`设定变更日志.md`
+
+### 每完成一个大剧情后
+1. **更新时间轴**: 记录剧情发生的时间点
+2. **更新战力表**: 如有新境界/新强者出现,补充到对照表
+3. **检查副作用**: 主角使用过的禁术/外挂是否有后续代价展示
+
+---
+
+## 🛠️ 自洽性检查清单
+
+在 `/webnovel-write` 生成章节后,必须通过以下检查:
+- [ ] **境界战力**: 本章的战斗场景是否符合力量体系设定?
+- [ ] **时间逻辑**: 事件发生的时间是否与之前矛盾?
+- [ ] **人物设定**: 角色的行为是否符合其性格/立场?
+- [ ] **道具限制**: 神器/丹药/法宝的使用是否超出设定范围?
+- [ ] **势力关系**: 本章涉及的势力互动是否符合既定关系网?
+
+---
+
+## 附录:常见读者质疑案例库
+
+### 案例 1: "为什么主角不用XXX方法?"
+- **问题**: 主角明明有更简单的解决方案,却偏要走复杂路线。
+- **原因**: 作者忘记了主角之前获得的能力/道具。
+- **预防**: 维护一份"主角当前能力清单",每次写作前浏览。
+
+### 案例 2: "这个反派怎么又活了?"
+- **问题**: 明明写死的反派又出现了,没有合理解释。
+- **原因**: 想让反派继续推动剧情,但没提前埋伏笔。
+- **修复**: 补充说明(分身/假死/被人救走),并在设定集标注"待填坑"。
+
+### 案例 3: "这个世界的经济崩了"
+- **问题**: 灵石的购买力前后相差百倍。
+- **原因**: 没有建立货币锚定物(如:1斤灵米=10灵石)。
+- **预防**: 设定"物价参考表",固定核心商品价格。

+ 86 - 0
.claude/skills/webnovel-init/references/worldbuilding/world-rules.md

@@ -0,0 +1,86 @@
+# 世界观构建指南 (World Building)
+
+> **核心原则**: 世界观不是“说明书”,而是“舞台”。所有的设定必须服务于剧情冲突和主角成长。
+
+---
+
+## 1. 世界观的三大基石
+
+### 物理法则 (Physics & Magic)
+决定这个世界“什么能做,什么不能做”。
+- **能量来源**: 灵气?魔力?核能?精神力?
+- **代价机制**: 获得力量需要付出什么?(寿命?理智?资源?)
+- **铁律**: 绝对不可打破的规则(如:死人不能复活 / 凡人不可直视神)。
+
+### 地理格局 (Geography)
+- **中心与边缘**: 资源富集区(圣地/主城)vs 贫瘠区(新手村/边境)。
+- **隔离机制**: 为什么新手不能直接去高级地图?(界壁?海洋?实力限制?)
+- **险地/秘境**: 专门用于刷怪、夺宝、奇遇的副本区域。
+
+### 势力架构 (Factions)
+建立“冲突网络”。
+- **统治者**: 谁制定规则?(皇室/联盟/神殿)
+- **挑战者**: 谁想破坏规则?(反抗军/魔道/邪神)
+- **中立者**: 谁提供服务?(商会/佣兵公会/炼丹师协会)
+
+---
+
+## 2. 势力设计模板
+
+### 宗门/组织卡
+```markdown
+**名称**: 天云宗
+**定位**: 正道魁首 / 霸主级势力
+**核心资源**: 拥有一条极品灵脉 + 上古剑冢
+**标志性功法**: 《流云剑诀》(以快著称)
+**内部矛盾**: 宗主一脉 vs 大长老一脉(权力斗争)
+**对外态度**: 表面护短,实则利益至上
+**与主角关系**: 初期庇护所 → 中期束缚 → 后期被主角超越/掌控
+```
+
+### 势力生态链
+- **依附关系**: 小家族依附于大宗门,大宗门依附于圣地。
+- **敌对关系**: 世仇(争夺资源)、道统之争(理念不合)。
+- **利益同盟**: 面对共同敌人(如兽潮)时的临时结盟。
+
+---
+
+## 3. 世界观构建的“冰山理论”
+
+- **显性设定 (20%)**: 直接在文中写出来的(如:货币是灵石,最高境界是渡劫)。
+- **隐性设定 (80%)**: 作者知道但不必全写出来的(如:灵石的开采工艺、渡劫期的具体寿命)。
+  - **作用**: 当读者深究逻辑时,你能给出合理的解释,让世界显得真实。
+
+---
+
+## 4. 常见世界观类型库
+
+### 修仙界 (Cultivation)
+- **残酷法则**: 弱肉强食,资源为王。
+- **社会结构**: 凡人如蝼蚁,修士高高在上。
+- **核心冲突**: 争夺长生机缘。
+
+### 赛博朋克 (Cyberpunk)
+- **高科技,低生活**: 霓虹灯下的贫民窟。
+- **控制者**: 超级公司垄断一切。
+- **反抗**: 黑客、改造人、街头武士。
+
+### 克苏鲁 (Cthulhu)
+- **不可知**: 世界的真相会导致疯狂。
+- **力量体系**: 知识=力量=诅咒。
+- **氛围**: 压抑、绝望、粘稠。
+
+### 废土末世 (Wasteland)
+- **资源匮乏**: 一瓶水能换一条命。
+- **秩序崩坏**: 拳头大就是硬道理。
+- **变异**: 怪物横行,人类也在变异。
+
+---
+
+## 🛠️ 构建检查清单
+
+在 `/webnovel-plan` 阶段,检查世界观是否合格:
+- [ ] **冲突源**: 这个世界是否天然存在导致争斗的因素?(如:资源有限)
+- [ ] **上升通道**: 主角是否有明确的变强路径?(如:考入学院/加入宗门)
+- [ ] **独特性**: 有没有一个让人眼前一亮的微创新?(如:在这个修仙界,灵气有毒)
+- [ ] **自洽性**: 设定之间是否有明显矛盾?

+ 142 - 0
.claude/skills/webnovel-plan/SKILL.md

@@ -0,0 +1,142 @@
+---
+name: webnovel-plan
+description: Plans detailed volume outlines with chapter-by-chapter breakdown, cool-point distribution, and Strand Weave pacing. Activates when user requests outline planning or /webnovel-plan.
+allowed-tools: Read Write Edit AskUserQuestion Bash
+---
+
+# Outline Planning Skill
+
+## Workflow Checklist
+
+Copy and track progress:
+
+```
+大纲规划进度:
+- [ ] Step 1: 加载爽点指南 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/cool-points-guide.md")
+- [ ] Step 2: 加载节奏规范 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/strand-weave-pattern.md")
+- [ ] Step 3: 加载题材套路 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/genre-tropes.md")
+- [ ] Step 4: 加载项目数据 (state.json + 总纲)
+- [ ] Step 5: 确认上下文充足
+- [ ] Step 6: 交互式收集需求 (AskUserQuestion)
+- [ ] Step 7: 生成详细大纲
+- [ ] Step 8: 质量验证
+- [ ] Step 9: 保存并更新状态
+```
+
+---
+
+## Step 1: 加载爽点指南(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/cool-points-guide.md"
+```
+
+关键规则:
+- 每章 ≥1 个爽点
+- 每 5 章 ≥1 个大爽点 (⭐⭐⭐)
+- 避免连续 3 章同类型
+
+## Step 2: 加载节奏规范(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/strand-weave-pattern.md"
+```
+
+关键规则:
+- Quest ≤5 连续章
+- Fire 每 10 章内出现
+- Constellation 每 15 章内出现
+- 目标比例: Quest 55-65%, Fire 20-30%, Constellation 10-20%
+
+## Step 3: 加载题材套路
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-init/references/genre-tropes.md"
+```
+
+## Step 4: 加载项目数据
+
+```bash
+cat .webnovel/state.json
+cat 大纲/总纲.md
+```
+
+## Step 5: 确认上下文充足
+
+**检查清单**:
+- [ ] 爽点类型和密度要求已理解
+- [ ] Strand Weave 比例已理解
+- [ ] 主角当前状态已知
+- [ ] 总纲框架已加载
+- [ ] 题材套路已参考
+
+**如有缺失 → 返回对应 Step**
+
+## Step 6: 交互式收集需求(必须执行)
+
+**使用 AskUserQuestion 收集**:
+- 本卷核心冲突类型
+- 实力提升计划
+- 主要爽点类型偏好
+- 感情线发展 (Fire strand)
+- 金手指差异化
+
+## Step 7: 生成详细大纲
+
+**大纲结构**:
+
+```markdown
+# 第 {volume_id} 卷:{卷名}
+
+> **章节范围**: 第 {start} - {end} 章
+> **核心冲突**: {conflict}
+
+## 卷摘要
+{2-3 段落}
+
+## 章节详细大纲
+
+### 第 {N} 章:{标题}
+**目标**: {章节目标}
+**爽点**: {类型}: {内容}
+**Strand**: {Quest|Fire|Constellation}
+**新增实体**: {角色/物品/地点}
+**伏笔**: {埋设内容}
+
+## Strand Weave 规划
+
+| 章节范围 | 主导 Strand | 内容概要 |
+|---------|------------|---------|
+| 第1-5章 | Quest | ... |
+
+### Strand 占比
+- Quest: X% (目标: 55-65%)
+- Fire: Y% (目标: 20-30%)
+- Constellation: Z% (目标: 10-20%)
+
+## 爽点密度规划
+
+| 章节 | 爽点类型 | 内容 | 强度 |
+|------|---------|------|------|
+| 第1章 | 系统觉醒 | 金手指激活 | ⭐⭐⭐ |
+```
+
+## Step 8: 质量验证
+
+**验证清单**:
+- [ ] 每章有 ≥1 爽点
+- [ ] 每 5 章有 ≥1 大爽点
+- [ ] 无 3+ 连续同类型爽点
+- [ ] Quest ≤5 连续章
+- [ ] Fire 每 10 章内出现
+- [ ] Constellation 每 15 章内出现
+
+## Step 9: 保存并更新状态
+
+保存到: `大纲/第{volume_id}卷-详细大纲.md`
+
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/update_state.py" \
+  --volume-planned {volume_id} \
+  --chapters-range "{start}-{end}"
+```

+ 96 - 0
.claude/skills/webnovel-plan/references/cool-points-guide.md

@@ -0,0 +1,96 @@
+---
+name: cool-points-guide
+purpose: 爽点设计参考,规划大纲时和写作时按需加载
+---
+
+<context>
+此文件用于爽点(cool-points)设计。Claude 已知基本叙事技巧,这里只补充网文特定的爽点工程方法论。
+</context>
+
+<instructions>
+
+## 爽点密度标准
+
+| 周期 | 要求 |
+|------|------|
+| 每章 | ≥1 个爽点 |
+| 每 5 章 | ≥1 个大爽点 |
+| 每 10 章 | ≥1 次实力提升 |
+
+## 爽点类型库
+
+| 类型 | 公式 | 适用场景 |
+|------|------|----------|
+| **打脸型** | 嘲讽→铺垫→反转→震惊 | 大比/拍卖/宴会 |
+| **升级型** | 困境→机缘→突破→展示 | 修炼/战斗后 |
+| **收获型** | 危机→解决→奖励 | 秘境/任务结束 |
+| **装逼型** | 低调→惊艳→众人侧目 | 日常/社交场合 |
+
+## 打脸四步法(核心套路)
+
+**Step 1 铺垫**: 提前1-2章建立信息差(读者知道主角底牌,反派不知道)
+
+**Step 2 挑衅**: 至少3次嘲讽 + 对照组(反派被追捧 vs 主角被贬低)
+
+**Step 3 拉扯**: 2-3轮交锋,主角示弱 → 反派得意 → 期待拉满
+
+**Step 4 爆发**: 物理碾压 + 精神打击 + 围观群众反应 + 实质收获
+
+</instructions>
+
+<examples>
+
+<example>
+<input>设计一个打脸爽点:主角在宗门大比被嘲笑</input>
+<output>
+**铺垫**(第45章): 主角已筑基但隐藏修为,王少以为主角只有练气七层
+**挑衅**: 王少当众嘲讽"废物也来参加?",长老们追捧王少
+**拉扯**: 第1轮主角用三成力被轻视,第2轮继续示弱,王少大笑"不过如此"
+**爆发**: 主角爆发筑基威压,一拳击飞王少,全场震惊"十八岁筑基天才!"
+**收获**: 晋升内门 + 5000灵石 + 女主好感度+20
+</output>
+</example>
+
+<example>
+<input>设计升级型爽点:主角突破境界</input>
+<output>
+**困境**: 被追杀至悬崖,走投无路
+**机缘**: 发现隐藏洞府,获得天材地宝
+**突破**: 服用后突破至筑基期
+**展示**: 回头一掌拍飞追兵,"你们...还追吗?"
+</output>
+</example>
+
+<example type="edge_case">
+<input>连续3章都是打脸,如何调整?</input>
+<output>
+❌ 继续打脸 → 审美疲劳
+✅ 第4章改为升级型或收获型爽点
+✅ 或穿插 Fire Strand(感情线)调节节奏
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 无铺垫的突然反转 → ✅ 提前1-2章埋信息差
+❌ 一招秒杀 → ✅ 至少2-3轮拉扯
+❌ 降智反派 → ✅ 基于已知信息的合理轻视
+❌ 打完无收获 → ✅ 必须有战利品/认可/资格
+❌ 缺少围观群众 → ✅ 侧面烘托震惊效果
+</errors>
+
+## 微反转技巧(高自由度)
+
+在爽点完成后追加"还有一手":
+- **更强型**: 展示筑基 → 其实是金丹伪装
+- **意外收获型**: 获得储物袋 → 里面还有天级功法
+- **对方更惨型**: 反派被打脸 → 录音石传遍全城
+
+## 伏笔三层级
+
+| 层级 | 回收周期 | 示例 |
+|------|----------|------|
+| 核心 | 20-50章 | 主角身世/终极敌人 |
+| 支线 | 10-30章 | 配角动机/神秘物品 |
+| 装饰 | 5-15章 | 场景细节/小习惯 |

+ 295 - 0
.claude/skills/webnovel-plan/references/outlining/chapter-planning.md

@@ -0,0 +1,295 @@
+# 章节规划技巧 (Chapter Planning)
+
+> **核心原则**: 每一章都是一个小故事,有开头、发展、高潮、结尾。读者看完后应该有"爽了"或"想看下一章"的感觉。
+
+---
+
+## 1. 章节的黄金结构
+
+### 标准章节结构(3000字)
+```
+开头(300字): 钩子,吸引读者继续看
+发展(1500字): 推进剧情,制造冲突
+高潮(1000字): 爽点/反转/打脸
+结尾(200字): 悬念/钩子,引导订阅下一章
+```
+
+### 开头钩子的5种类型
+1. **悬念式**: "就在这时,一道黑影突然出现在林天身后!"
+2. **冲突式**: "叶良辰冷笑道:'今天,就是你的死期!'"
+3. **信息差**: "林天不知道的是,他刚才的举动已经暴露了身份。"
+4. **倒叙式**: "三天后,天剑宗会沦为一片废墟。而这一切,都源于今天的一个决定……"
+5. **承接式**: "(接上章)林天深吸一口气,推开了秘境的大门。"
+
+---
+
+## 2. 章节节奏设计
+
+### 节奏类型
+
+#### A. 爽点章(70%)
+**目的**: 让读者爽,提升订阅
+**结构**:
+```
+1. 设置冲突(主角被挑衅/遇到危险)
+2. 主角展示实力(打脸/秒杀/装逼)
+3. 众人震惊("这怎么可能!")
+4. 主角收获(经验/宝物/名声)
+```
+
+**示例**:
+```markdown
+## 第50章:一剑斩金丹
+- 开头: 金丹长老嘲讽林天不自量力
+- 发展: 林天拔剑,展示剑意
+- 高潮: 一剑斩杀金丹长老,全场震惊
+- 结尾: 宗主现身:"有趣,我要收你为亲传弟子。"(埋下下一章钩子)
+```
+
+#### B. 过渡章(20%)
+**目的**: 承上启下,铺垫伏笔
+**结构**:
+```
+1. 整理收获(清点战利品/总结战斗)
+2. 交代背景(补充世界观/引入新势力)
+3. 布置悬念(暗示下一个危机)
+```
+
+**示例**:
+```markdown
+## 第51章:暗流涌动
+- 开头: 林天回到住处,清点从秘境获得的宝物
+- 发展: 介绍宝物的来历(补充设定)
+- 发展: 暗中有人监视林天(埋伏笔)
+- 结尾: "血神教的密探传回情报:'目标已锁定。'"
+```
+
+#### C. 刀子章(10%)
+**目的**: 制造虐点,引发情绪波动
+**结构**:
+```
+1. 主角遭遇重大打击(师尊被杀/亲人被掳)
+2. 主角悲痛欲绝
+3. 立下复仇誓言
+```
+
+**示例**:
+```markdown
+## 第200章:师尊陨落
+- 开头: 师尊为了保护林天,以一敌百
+- 发展: 林天被迫逃离,眼睁睁看着师尊被围杀
+- 高潮: 师尊临死前传功给林天:"记住,报仇!"
+- 结尾: 林天跪地嘶吼:"我发誓,血神教,我必灭你满门!"
+```
+
+---
+
+## 3. 章节字数控制
+
+### 标准字数
+- **起点等长篇平台**: 3000字/章(最舒适)
+- **短篇平台(知乎/微信)**: 1500-2000字/章
+- **付费章节**: 2000字起(低于2000字读者会觉得不划算)
+
+### 字数分配黄金比例
+```
+开头钩子: 10%(300字)
+剧情发展: 50%(1500字)
+高潮爽点: 33%(1000字)
+结尾钩子: 7%(200字)
+```
+
+### 特殊情况
+- **大高潮章节**: 可以写到5000-6000字(如:宗门大比决赛、击败大BOSS)
+- **过渡章节**: 可以压缩到2500字(但不要低于2000字)
+
+---
+
+## 4. 章节标题技巧
+
+### 标题的作用
+- **吸引点击**: 在目录页脱颖而出
+- **剧透适度**: 让读者知道本章有什么看点,但不要完全剧透
+
+### 标题公式
+
+#### 公式 1: 核心事件
+```
+第1章:退婚之辱
+第50章:一剑斩金丹
+第100章:血洗叶家
+```
+**优点**: 直接,读者一看就知道本章内容
+**缺点**: 缺少悬念
+
+#### 公式 2: 悬念式
+```
+第1章:神秘的声音
+第50章:他,来了
+第100章:那一剑的风情
+```
+**优点**: 有悬念,吸引点击
+**缺点**: 标题党风险,内容要撑得住
+
+#### 公式 3: 对话式
+```
+第1章:"你,不配!"
+第50章:"这不可能!"
+第100章:"我回来了。"
+```
+**优点**: 有代入感,情绪强烈
+**缺点**: 用多了会审美疲劳
+
+#### 公式 4: 组合式(推荐)
+```
+第50章:一剑斩金丹!全场震惊
+第100章:血洗叶家,复仇之路开启
+第200章:师尊陨落,"我必杀你!"
+```
+**优点**: 既有核心事件,又有情绪点
+
+---
+
+## 5. 章节间的衔接
+
+### 衔接方式
+
+#### A. 直接承接
+```
+第49章结尾: "林天深吸一口气,一剑斩出!"
+第50章开头: "剑光闪过,金丹长老瞳孔骤缩……"
+```
+**适用**: 战斗章节、高潮章节
+
+#### B. 时间跳跃
+```
+第49章结尾: "林天走进了秘境。"
+第50章开头: "三天后,林天从秘境中走出,气息大变。"
+```
+**适用**: 过渡章节、修炼闭关
+
+#### C. 视角切换
+```
+第49章: 主角视角(林天在秘境中探索)
+第50章: 反派视角("宗主,林天进入秘境已经三天,要不要动手?")
+```
+**适用**: 多线叙事、埋伏笔
+
+---
+
+## 6. 爽点密度控制
+
+### 爽点分级
+| 等级 | 爽感强度 | 频率 | 示例 |
+|------|---------|------|------|
+| **S级** | 超级爽 | 每50-100章 | 打败大BOSS、飞升、灭敌对势力满门 |
+| **A级** | 很爽 | 每20-30章 | 打脸前未婚妻、夺得宗门大比第一 |
+| **B级** | 爽 | 每10章 | 秒杀挑衅者、获得神器 |
+| **C级** | 小爽 | 每3-5章 | 打脸龙套、赚到一笔灵石 |
+
+### 爽点密度检查
+- **最低标准**: 每10章至少有1个B级或以上爽点
+- **理想状态**: 每5章1个C级 + 每10章1个B级 + 每30章1个A级
+- **禁忌**: 连续20章没有任何爽点(读者会弃书)
+
+---
+
+## 7. 章节规划模板
+
+### 模板 A: 战斗章
+```markdown
+## 第XX章:[标题]
+**字数**: 3000
+**主线剧情**: 主角 vs [敌人]
+**冲突来源**: [为什么打?]
+**战斗流程**:
+  1. 敌人嘲讽/轻视主角
+  2. 主角隐藏实力,先示弱
+  3. 敌人以为稳赢,放松警惕
+  4. 主角爆发,一招制敌
+  5. 众人震惊:"他竟然这么强!"
+**爽点**: 打脸/秒杀/装逼
+**伏笔**: [埋什么坑?]
+**结尾钩子**: [引导下一章]
+```
+
+### 模板 B: 剧情章
+```markdown
+## 第XX章:[标题]
+**字数**: 3000
+**主线剧情**: [推进哪条主线?]
+**副线剧情**: [涉及哪些支线?]
+**新增信息**:
+  - 世界观补充: [介绍新势力/新地图]
+  - 人物关系: [建立新的联系]
+**爽点**: 无(过渡章)或 小爽(获得情报/宝物)
+**伏笔**: [为后续剧情埋坑]
+**结尾钩子**: [制造悬念]
+```
+
+### 模板 C: 修炼章
+```markdown
+## 第XX章:突破!
+**字数**: 3000
+**主线剧情**: 主角突破到[新境界]
+**修炼过程**:
+  1. 回顾当前境界瓶颈
+  2. 顿悟/吃丹药/吸收能量
+  3. 描写突破时的异象(天地灵气汇聚/雷劫降临)
+  4. 突破成功,实力大涨
+**爽点**: 境界提升,战力飙升
+**对比**: 展示突破前后的实力差距(如:之前打不过的敌人,现在一招秒)
+**结尾钩子**: "就在这时,一股恐怖的气息锁定了林天……"
+```
+
+---
+
+## 8. 章节自检清单
+
+每写完一章,用此清单自检:
+- [ ] **字数**: 是否在2000-4000字区间?(付费章至少2000字)
+- [ ] **开头**: 前300字是否有足够的吸引力?
+- [ ] **冲突**: 本章是否有明确的冲突或推进?
+- [ ] **爽点**: 本章是否有至少1个小爽点?(如果是过渡章可以没有,但不能连续3章都没有)
+- [ ] **结尾**: 是否有钩子引导读者订阅下一章?
+- [ ] **逻辑**: 本章内容是否符合设定集和大纲?
+- [ ] **节奏**: 本章的节奏是快(战斗/打脸)还是慢(过渡/修炼)?是否与前后章形成对比?
+
+---
+
+## 9. 常见章节问题与解决
+
+### 问题 1: 章节太水,读者骂"注水"
+**表现**: 大段环境描写、内心独白、重复前文内容
+**解决**:
+- 删除无用的环境描写(除非是首次进入新地图)
+- 内心独白不超过200字
+- 不要重复上一章的内容(除非是倒叙/回忆)
+
+### 问题 2: 章节太干,信息量过大
+**表现**: 一章引入5个新角色、3个新势力、10个新设定
+**解决**:
+- 每章最多引入2个新角色
+- 新设定分散到多章介绍
+- 用对话而非旁白介绍设定
+
+### 问题 3: 章节没有高潮,平淡无奇
+**表现**: 从头到尾都是"他做了A,然后做了B,然后做了C"
+**解决**:
+- 强制要求:每章至少有1个情绪高点(爽点/笑点/虐点/悬念)
+- 使用"抑扬法":先压抑,再爆发
+
+---
+
+## 🛠️ 快速规划工具
+
+### 10章规划表(Excel/Notion)
+| 章节 | 类型 | 主要剧情 | 爽点 | 结尾钩子 | 字数 |
+|------|------|---------|------|---------|------|
+| 41 | 战斗章 | 林天 vs 叶良辰 | 打脸前未婚妻 | "宗主:我要收你为徒" | 3000 |
+| 42 | 剧情章 | 成为亲传弟子 | 地位提升 | "长老派系暗中不满" | 3000 |
+| 43 | 过渡章 | 认识新队友 | 无 | "秘境即将开启" | 2500 |
+| 44 | 探险章 | 进入秘境 | 发现宝藏 | "遇到强大妖兽" | 3000 |
+| 45 | 战斗章 | 斩杀妖兽 | 获得神器 | "神器认主" | 3500 |
+
+使用此表格,可以提前规划10-20章的节奏,确保爽点分布均匀。

+ 277 - 0
.claude/skills/webnovel-plan/references/outlining/conflict-design.md

@@ -0,0 +1,277 @@
+# 冲突设计指南 (Conflict Design)
+
+> **核心原则**: 没有冲突就没有故事。冲突是剧情的引擎,是爽点的源泉。
+
+---
+
+## 1. 冲突的三大层次
+
+### 层次 1: 外部冲突(人 vs 人/势力/环境)
+**定义**: 主角与外界的对抗
+**类型**:
+- **个人恩怨**: 主角 vs 叶良辰(情敌/仇人)
+- **势力对抗**: 天剑宗 vs 血神教
+- **环境挑战**: 渡劫/兽潮/天灾
+
+### 层次 2: 内部冲突(人 vs 自我)
+**定义**: 主角内心的挣扎
+**类型**:
+- **道德困境**: 为了救妹妹,要不要屠城?
+- **身份认同**: 我是正道弟子,还是魔道传人?
+- **欲望冲突**: 追求力量 vs 守护爱人
+
+### 层次 3: 理念冲突(价值观 vs 价值观)
+**定义**: 不同世界观的碰撞
+**类型**:
+- **正邪之争**: 正道的虚伪 vs 魔道的真实
+- **天道之辩**: 顺天而为 vs 逆天改命
+- **弱肉强食 vs 守护弱者**
+
+---
+
+## 2. 冲突强度分级
+
+### S级冲突(生死之战)
+**特征**: 不死不休,双方至少有一方会消失
+**触发条件**:
+- 杀父之仇/夺妻之恨
+- 灭门灭宗
+- 世界存亡
+
+**示例**:
+```markdown
+主角 vs 血神教主(最终BOSS)
+- 仇恨: 血神教杀了主角全家、掳走妹妹
+- 目标: 彻底消灭血神教
+- 结果: 必须有一方死亡
+```
+
+### A级冲突(重大利益)
+**特征**: 涉及核心利益,但可以妥协
+**触发条件**:
+- 争夺圣地传承
+- 宗门继承人之争
+- 秘境夺宝
+
+**示例**:
+```markdown
+主角 vs 同门师兄(争夺亲传弟子名额)
+- 冲突: 只有一个名额
+- 结果: 可以通过比试决出胜负,败者不一定死
+```
+
+### B级冲突(面子/地位)
+**特征**: 争的是一口气,可以和解
+**触发条件**:
+- 被嘲讽/挑衅
+- 争风吃醋
+- 排名竞争
+
+**示例**:
+```markdown
+主角 vs 龙套(被嘲讽"你也配?")
+- 冲突: 维护尊严
+- 结果: 打一顿/打脸即可,不必杀人
+```
+
+### C级冲突(日常摩擦)
+**特征**: 小打小闹,调节气氛
+**触发条件**:
+- 抢座位
+- 口角
+- 误会
+
+**示例**:
+```markdown
+主角 vs 路人(食堂排队插队)
+- 冲突: 小矛盾
+- 结果: 吵两句,或者用实力小小震慑
+```
+
+---
+
+## 3. 冲突升级机制
+
+### 升级公式
+```
+小冲突 → 误会加深 → 利益纠葛 → 不死不休
+```
+
+### 升级案例
+```markdown
+阶段1(C级): 主角和叶良辰在酒楼发生口角
+阶段2(B级): 叶良辰因面子受损,派人暗杀主角(失败)
+阶段3(A级): 主角反杀叶家护卫,叶家派长老追杀
+阶段4(S级): 主角灭了叶家分支,叶家族长亲自出手
+```
+
+**作用**: 通过逐步升级,让冲突更合理,避免"一言不合就生死大战"的突兀感。
+
+---
+
+## 4. 冲突设计模板
+
+### 模板 A: 对立冲突(主角 vs 单一敌人)
+```markdown
+## 冲突卡:主角 vs 叶良辰
+**起因**: 叶良辰退婚羞辱主角
+**发展**:
+  - 第10章: 叶良辰在宗门嘲讽主角
+  - 第30章: 宗门大比,主角击败叶良辰
+  - 第50章: 叶良辰请出家族长老报复
+**高潮**: 第80章,主角血洗叶家
+**结局**: 叶良辰死亡,冲突彻底解决
+**遗留**: 叶家背后的靠山(圣地)对主角产生敌意
+```
+
+### 模板 B: 三方冲突(主角 vs 敌人A + 敌人B)
+```markdown
+## 冲突卡:正邪魔三方混战
+**正道**: 天剑宗(主角所在)
+**邪道**: 血神教
+**魔道**: 万毒窟
+
+**导火索**: 秘境开启,三方都想独吞
+**发展**:
+  - 第100章: 三方在秘境入口对峙
+  - 第110章: 进入秘境后,正道内部分裂(有人想和邪道联手)
+  - 第120章: 魔道偷袭邪道,主角趁乱夺宝
+**高潮**: 第130章,主角在三方混战中黄雀在后
+**结局**: 三方两败俱伤,主角得利
+```
+
+### 模板 C: 内部冲突(主角 vs 所在势力)
+```markdown
+## 冲突卡:主角 vs 宗门长老派
+**背景**: 宗门内部分为宗主一脉和长老一脉
+**起因**: 主角因实力太强,威胁到长老一脉的利益
+**发展**:
+  - 第200章: 长老派暗中打压主角
+  - 第210章: 主角被陷害,差点被逐出宗门
+  - 第220章: 主角找到证据,反击长老派
+**高潮**: 第230章,宗门大会,主角揭露长老派勾结外敌
+**结局**: 长老派被清洗,主角成为宗门核心人物
+```
+
+---
+
+## 5. 冲突的节奏控制
+
+### 冲突密度
+- **最低标准**: 每10章至少有1个B级或以上冲突
+- **理想状态**: 每5章1个小冲突 + 每20章1个大冲突
+- **禁忌**: 连续30章没有明显冲突(剧情会显得拖沓)
+
+### 冲突与爽点的配合
+```
+冲突建立(压抑) → 主角反击(爽) → 冲突升级(再压抑) → 主角碾压(大爽)
+```
+
+**示例**:
+```markdown
+第40章: 叶良辰嘲讽主角(压抑)
+第41章: 主角展示实力,小小打脸(小爽)
+第42章: 叶良辰请出家族长老(压抑升级)
+第45章: 主角击杀长老,血洗叶家(大爽)
+```
+
+---
+
+## 6. 避免无意义冲突
+
+### 无意义冲突的特征
+1. **无缘无故**: 两人完全没有交集,突然就打起来了
+2. **重复冲突**: 主角已经打脸过叶良辰三次了,还在打第四次
+3. **无后果冲突**: 打完了,双方都没有损失,也没有收获
+
+### 如何让冲突有意义?
+- **必须有起因**: 利益冲突/价值观冲突/误会/仇恨
+- **必须有后果**: 胜者获得好处(经验/宝物/名声),败者付出代价(死亡/失去地位)
+- **必须推动剧情**: 通过这次冲突,主角变强了/认识新人了/获得新线索了
+
+---
+
+## 7. 多线冲突交织
+
+### 为什么要多线?
+- 避免单一冲突线太单调
+- 制造复杂性,增加可读性
+- 给配角发挥空间
+
+### 多线冲突示例
+```markdown
+## 第一卷同时进行的三条冲突线
+**主线**: 主角 vs 叶良辰(个人恩怨)
+**副线A**: 天剑宗 vs 血神教(势力对抗,背景板)
+**副线B**: 主角 vs 宗门考核(环境挑战)
+
+## 交织点
+第30章: 宗门大比(主线 + 副线B)
+第50章: 血神教突袭宗门,主角趁乱击杀叶良辰(三线汇聚)
+```
+
+---
+
+## 8. 冲突解决方式
+
+### 方式 1: 武力碾压(70%)
+**适用**: 大部分网文
+**流程**: 主角实力强→直接碾压敌人→爽
+**示例**: "一剑斩金丹!全场震惊!"
+
+### 方式 2: 智谋取胜(20%)
+**适用**: 主角暂时打不过敌人
+**流程**: 用计谋/阵法/毒药削弱敌人→再出手
+**示例**: "主角提前在秘境布下陷阱,引诱敌人进入,借秘境机关杀敌"
+
+### 方式 3: 和解/妥协(5%)
+**适用**: 非生死冲突
+**流程**: 双方找到共同利益点→暂时联手
+**示例**: "正邪两道虽然对立,但兽潮来袭,只能联手御敌"
+
+### 方式 4: 暂时搁置(5%)
+**适用**: 为后续剧情埋伏笔
+**流程**: 外力介入(强者出现/时间到/秘境关闭)→冲突中断
+**示例**: "就在两人生死相搏之际,宗主现身:'住手!'"
+
+---
+
+## 9. 冲突设计自检清单
+
+每次设计冲突时,检查以下项目:
+- [ ] **起因明确**: 为什么打?(不能"看你不爽"这种弱智理由)
+- [ ] **实力对比**: 敌人是否比主角强?(如果太弱,没有张力)
+- [ ] **利益相关**: 冲突是否涉及核心利益?(面子不如宝物吸引人)
+- [ ] **升级合理**: 冲突是否有逐步升级的过程?(不要一上来就生死大战)
+- [ ] **有后续影响**: 冲突结束后,是否有遗留问题?(埋伏笔)
+- [ ] **符合人设**: 主角/反派的行为是否符合其性格?
+
+---
+
+## 🛠️ 冲突库(常用模板)
+
+### 冲突类型速查表
+| 冲突类型 | 起因 | 强度 | 解决方式 | 爽点 |
+|---------|------|------|---------|------|
+| 退婚打脸 | 被看不起 | A级 | 武力碾压 | 打脸前未婚妻 |
+| 秘境夺宝 | 资源争夺 | S级 | 智谋+武力 | 获得神器 |
+| 宗门内斗 | 权力斗争 | A级 | 站队+清洗 | 地位提升 |
+| 兽潮来袭 | 环境危机 | S级 | 联手御敌 | 立功+突破 |
+| 正邪大战 | 理念冲突 | S级 | 武力决战 | 击败魔头 |
+| 被陷害 | 误会/小人 | B级 | 找证据反击 | 揭穿真相 |
+
+使用此表格,可以快速选择合适的冲突类型,并确保每种冲突都有明确的解决方式和爽点。
+
+---
+
+## 附录:冲突设计案例分析
+
+### 案例:《斗破苍穹》的冲突设计
+**核心冲突**: 萧炎 vs 纳兰嫣然(三年之约)
+**冲突升级**:
+1. 退婚(C级→B级冲突)
+2. 三年之约(A级冲突)
+3. 云岚宗干涉(升级为S级,变成萧炎 vs 云岚宗)
+4. 最终决战(灭云岚宗)
+
+**优点**: 冲突升级自然,从个人恩怨上升到势力对抗,层层递进。

+ 210 - 0
.claude/skills/webnovel-plan/references/outlining/outline-structure.md

@@ -0,0 +1,210 @@
+# 大纲结构设计指南 (Outline Structure)
+
+> **核心原则**: 大纲是地图,不是枷锁。好的大纲能指引方向,但也要留白,给灵感留空间。
+
+---
+
+## 1. 大纲的三层架构
+
+### 第一层:骨架大纲(必需,30%)
+**目的**: 确保故事有清晰的起承转合。
+
+```markdown
+## 骨架大纲示例
+**开篇(1-50章)**: 废材少年林天被退婚,机缘下获得吞噬系统
+**前期(51-200章)**: 在宗门崛起,打脸曾经嘲笑他的人,发现妹妹被人掳走
+**中期(201-500章)**: 为救妹妹闯荡修仙界,与各大势力周旋,实力飞速提升
+**后期(501-800章)**: 揭开身世之谜,原来是上古大能转世,最终对决血神教主
+**结局(801-850章)**: 击败血神,救回妹妹,飞升仙界(留续集接口)
+```
+
+**骨架大纲检查清单**:
+- [ ] **起点明确**: 主角的初始状态和核心矛盾是什么?
+- [ ] **转折点**: 至少3个重大剧情转折(如:获得金手指、加入宗门、师尊被杀)
+- [ ] **终点明确**: 故事的最终目标(打败魔王/飞升/称霸)
+
+---
+
+### 第二层:分卷大纲(建议,50%)
+**目的**: 将长篇切分为多个小高潮,避免读者疲劳。
+
+#### 分卷原则
+- **每卷一个小目标**: 如第一卷"新手村成长",第二卷"宗门试炼"
+- **卷末必有高潮**: 打败小BOSS、获得神器、突破境界
+- **卷与卷之间有过渡**: 不要突然换地图,要有铺垫
+
+#### 分卷大纲模板
+```markdown
+## 第一卷:崛起之路(1-150章)
+**核心目标**: 从废材到天才的逆袭
+**主要冲突**: 家族内斗、退婚风波、宗门选拔
+**高潮**: 宗门大比夺得第一,打脸叶良辰
+**伏笔**: 暗示主角身世不凡,埋下后续"上古血脉"线索
+
+## 第二卷:宗门风云(151-300章)
+**核心目标**: 在天剑宗站稳脚跟
+**主要冲突**: 内门弟子排挤、长老派系斗争、秘境夺宝
+**高潮**: 秘境中获得剑冢传承,实力暴涨
+**伏笔**: 发现妹妹被血神教掳走的线索
+
+## 第三卷:救妹之旅(301-500章)
+**核心目标**: 追踪血神教,救回妹妹
+**主要冲突**: 与血神教的多次交锋、正邪两道的误会、力量不足的焦虑
+**高潮**: 孤身闯血神教总坛,与教主初次交手(战败但存活)
+**伏笔**: 得知妹妹身怀特殊体质,是血神复活的关键
+```
+
+---
+
+### 第三层:章节大纲(可选,20%)
+**目的**: 细化到每一章的具体内容,适合新手或卡文时使用。
+
+#### 章节大纲模板
+```markdown
+## 第1章:退婚之辱
+**字数**: 3000字
+**主线剧情**:
+  - 叶家少主叶良辰上门退婚
+  - 林天被羞辱,全家蒙羞
+  - 林天愤怒但无力反抗(展示废材状态)
+**副线剧情**:
+  - 父亲叹气,妹妹安慰
+  - 埋下伏笔:林天手中的玉佩微微发光
+**爽点**: 无(压抑章节,为后续打脸蓄力)
+**结尾钩子**: "就在这时,林天脑海中突然响起一个机械声音:'吞噬系统激活……'"
+```
+
+**章节大纲的灵活性**:
+- 只需写核心内容,具体对话和细节在写作时发挥
+- 如果灵感爆发,可以偏离大纲(但要记得回来修正大纲)
+
+---
+
+## 2. 大纲的详细程度选择
+
+### 粗纲流(适合老手)
+- **内容**: 只写骨架大纲 + 分卷大纲
+- **优点**: 写作自由度高,可以随时调整
+- **缺点**: 容易跑偏或卡文
+
+### 细纲流(适合新手)
+- **内容**: 骨架 + 分卷 + 章节大纲(至少前50章)
+- **优点**: 写作时不迷茫,每天知道写什么
+- **缺点**: 前期准备时间长
+
+### 混合流(推荐)
+- **内容**: 骨架大纲(全书)+ 分卷大纲(全书)+ 章节大纲(未来10-20章)
+- **优点**: 既有方向感,又保留灵活性
+- **做法**: 每写完一卷,就补充下一卷的章节大纲
+
+---
+
+## 3. 大纲的动态调整
+
+### 何时需要调整大纲?
+1. **读者反馈**: 如果某个角色/剧情特别受欢迎,可以增加戏份
+2. **灵感爆发**: 突然想到更好的剧情走向
+3. **数据异常**: 某章订阅/收藏暴跌,说明剧情有问题
+
+### 如何调整大纲?
+```markdown
+## 大纲变更日志
+**日期**: 2025-01-03
+**变更内容**: 原计划第二卷就让妹妹死亡(刀子),但读者反馈妹妹人气太高
+**新方案**: 改为"妹妹被掳走",延后到第三卷再揭晓生死(拉长悬念)
+**影响范围**: 第二卷高潮需重新设计,改为"获得剑冢传承"
+```
+
+### 大纲调整的红线(不可触碰)
+- **主角人设**: 不能突然从好人变坏人(或反之),除非有充分铺垫
+- **力量体系**: 不能随意修改境界设定,否则前文战力对比全崩
+- **核心主线**: 可以调整支线,但"救妹妹"这种核心目标不能轻易放弃
+
+---
+
+## 4. 大纲工具推荐
+
+### 工具 A: Markdown + Git
+```
+大纲/
+├── 骨架大纲.md
+├── 分卷大纲/
+│   ├── 第一卷.md
+│   ├── 第二卷.md
+│   └── 第三卷.md
+├── 章节大纲/
+│   ├── 001-050章.md
+│   └── 051-100章.md
+└── 大纲变更日志.md
+```
+**优点**: 纯文本,易于版本控制和搜索
+
+### 工具 B: 思维导图(XMind/MindNode)
+```
+主线:救妹妹
+├── 第一卷:新手村崛起
+│   ├── 1-10章:退婚事件
+│   ├── 11-30章:获得系统
+│   └── 31-50章:宗门选拔
+├── 第二卷:宗门风云
+│   └── ...
+```
+**优点**: 可视化强,层级关系清晰
+
+### 工具 C: 表格(Excel/Notion)
+| 章节 | 主线剧情 | 副线剧情 | 爽点 | 伏笔 | 字数 |
+|------|---------|---------|------|------|------|
+| 1 | 退婚 | 妹妹安慰 | 无 | 玉佩发光 | 3000 |
+| 2 | 激活系统 | 测试能力 | 吞噬野兽 | 系统有隐藏功能 | 3000 |
+**优点**: 适合数据化管理,方便统计爽点密度
+
+---
+
+## 5. 大纲与设定集的联动
+
+大纲不是孤立的,必须与设定集配合:
+
+### 检查清单
+- [ ] **人物卡**: 每个重要角色出场前,是否已在设定集中建立人物卡?
+- [ ] **势力关系**: 涉及势力冲突时,是否符合设定集中的势力关系网?
+- [ ] **力量体系**: 主角的境界提升是否按照力量体系设定的节奏?
+- [ ] **时间轴**: 每卷的时间跨度是否合理?(不要突然跳跃十年)
+
+### 示例:大纲与设定集的冲突
+```markdown
+## 问题
+大纲: 第50章主角突破到金丹期
+设定集: 主角16岁,正常修炼速度20年才能金丹
+冲突: 时间线不合理
+
+## 解决方案
+方案A: 调整大纲,改为筑基期
+方案B: 在设定集补充"吞噬系统可加速修炼100倍"
+方案C: 增加奇遇剧情(吃了万年灵果)
+```
+
+---
+
+## 🛠️ 大纲自检清单
+
+在开始写作前,用此清单检查大纲是否合格:
+- [ ] **完整性**: 开头、中间、结尾是否都有?
+- [ ] **节奏**: 是否有明确的高潮和低谷交替?(不能一直高潮或一直平淡)
+- [ ] **逻辑**: 剧情推进是否合理?(不能靠角色突然变蠢)
+- [ ] **爽点**: 每10-20章是否至少有1个爽点?
+- [ ] **伏笔**: 挖的坑是否有填坑计划?(标注在哪一章填)
+- [ ] **字数**: 预估总字数是否符合平台要求?(起点至少100万字)
+
+---
+
+## 附录:经典大纲案例分析
+
+### 案例 1: 《凡人修仙传》大纲特点
+- **骨架**: 平凡少年修仙 → 成为仙界大能(超长线,800万字)
+- **分卷**: 以"地图"划分(人界→灵界→仙界),每换地图实力清零重来
+- **节奏**: 每个小境界都有独立的小高潮(获得法宝/打败敌人)
+
+### 案例 2: 《斗破苍穹》大纲特点
+- **骨架**: 天才少年陨落 → 崛起复仇 → 成为斗帝
+- **分卷**: 以"实力等级"划分(斗者→斗王→斗圣),每个大境界一卷
+- **节奏**: "三年之约"这种明确的时间节点,制造紧迫感

+ 240 - 0
.claude/skills/webnovel-plan/references/outlining/plot-frameworks.md

@@ -0,0 +1,240 @@
+# 剧情框架模板库 (Plot Frameworks)
+
+> **核心原则**: 不要重复造轮子。经典剧情框架已被验证有效,关键是如何包装出新意。
+
+---
+
+## 1. 通用剧情框架
+
+### 框架 A: 英雄之旅(Hero's Journey)
+**适用**: 90% 的网文都可以套用
+
+#### 12个阶段
+```
+1. 平凡世界: 主角是普通人/废材
+2. 冒险召唤: 遭遇危机(退婚/亲人被杀/世界末日)
+3. 拒绝召唤: 主角一开始不想卷入(我只想安静修炼)
+4. 遇见导师: 获得金手指/师尊指点
+5. 跨越第一道门槛: 加入宗门/进入修仙界
+6. 试炼与盟友: 遇到死党、女主,经历小战斗
+7. 深入洞穴: 进入危险区域(秘境/敌对势力)
+8. 苦难考验: 遭遇最大危机(师尊被杀/被人陷害)
+9. 奖赏: 获得神器/顿悟/突破境界
+10. 返回之路: 带着收获回归
+11. 复活: 浴火重生,实力大涨
+12. 带着灵药回归: 成为传奇,改变世界
+```
+
+#### 网文简化版(3幕剧)
+```
+第一幕(前30%): 建立世界观,主角遇到危机
+第二幕(中间40%): 主角成长,多次试炼,积累实力
+第三幕(后30%): 终极对决,打败大BOSS
+```
+
+---
+
+## 2. 网文专属框架
+
+### 框架 B: 打脸爽文框架
+**公式**: 被嘲讽 → 隐忍 → 爆发 → 打脸 → 循环
+
+#### 标准流程
+```markdown
+Step 1: 设置落差
+- 主角曾经是天才/豪门少爷
+- 因某种原因跌落(灵根被废/家族灭门)
+
+Step 2: 第一次嘲讽
+- 曾经的未婚妻退婚
+- 台词: "你这个废物,配不上我!"
+
+Step 3: 隐忍阶段
+- 主角表面忍气吞声
+- 内心: "三十年河东,三十年河西,莫欺少年穷!"
+- 暗中努力(获得金手指/苦修)
+
+Step 4: 小规模打脸
+- 在某个小场合展示实力(如:宗门考核第一)
+- 打脸对象: 曾经嘲笑他的龙套
+- 读者: "爽!"
+
+Step 5: 大规模打脸
+- 在重要场合(宗门大比/家族会议)碾压敌人
+- 打脸对象: 曾经的未婚妻/敌对家族少主
+- 读者: "爽爽爽!"
+
+Step 6: 进入下一循环
+- 换更大的地图(从新手村到主城)
+- 遇到更强的嘲讽者(从家族少主到宗门天才)
+```
+
+#### 打脸节奏控制
+- **频率**: 每10-20章至少有1次小打脸
+- **递增**: 打脸的爽感要逐渐升级(从口头打脸→实力打脸→碾压式打脸)
+- **避免重复**: 不能总是"未婚妻退婚"这一个梗
+
+---
+
+### 框架 C: 废材逆袭框架
+**公式**: 废材 → 奇遇 → 逆袭 → 称霸
+
+```markdown
+## 第一阶段:废材期(1-50章)
+- 主角是公认的废材(无灵根/天赋差/身体残疾)
+- 被欺负、被嘲笑、被抛弃
+- **关键**: 必须让读者同情主角
+
+## 第二阶段:奇遇期(51-100章)
+- 掉落悬崖/进入秘境/获得传承
+- 得到金手指(系统/神器/功法)
+- 开始逆袭
+
+## 第三阶段:崛起期(101-500章)
+- 在新手村称霸
+- 进入更大的舞台(宗门/圣地)
+- 不断打脸曾经看不起他的人
+
+## 第四阶段:称霸期(501章+)
+- 成为顶尖强者
+- 改变世界规则
+- 留续集接口(飞升/前往更高位面)
+```
+
+---
+
+### 框架 D: 扮猪吃虎框架
+**公式**: 强者 → 隐藏实力 → 被小觑 → 爆发 → 震惊
+
+```markdown
+## 设定
+- 主角其实很强(重生/夺舍/真身被封印)
+- 但选择低调(苟道/不想惹麻烦)
+
+## 剧情推进
+1. 主角刻意表现平庸
+2. 反派/龙套因此轻视他
+3. 主角被逼无奈出手
+4. 一招秒杀,全场震惊
+5. 主角: "我只想低调,你们为什么逼我?"
+
+## 读者爽点
+- "装逼被雷劈"的反向运用
+- 主角明明很强,却被当成弱鸡(反差萌)
+- 爆发时的震撼感
+```
+
+---
+
+## 3. 类型化剧情框架
+
+### 修仙类框架
+```
+炼气期(1-100章): 新手村,建立基础设定
+筑基期(101-200章): 加入宗门,站稳脚跟
+金丹期(201-400章): 秘境夺宝,获得机缘
+元婴期(401-600章): 宗门危机,成为救世主
+化神期(601-800章): 跨域冒险,面对更强敌人
+飞升(801章+): 飞升仙界,开启续集
+```
+
+### 都市异能类框架
+```
+觉醒期(1-50章): 获得异能,适应能力
+成长期(51-200章): 在异能者圈子崭露头角
+争霸期(201-500章): 参与异能者大战/国际赛事
+巅峰期(501章+): 成为最强异能者,守护地球
+```
+
+### 游戏竞技类框架
+```
+新手期(1-100章): 进入游戏,熟悉机制
+升级期(101-300章): 刷副本、打装备、建公会
+竞技期(301-600章): 参加联赛/世界赛
+巅峰期(601章+): 成为电竞传奇/游戏与现实融合
+```
+
+---
+
+## 4. 剧情推进的万能引擎
+
+### 引擎 1: 时间限制
+- **三年之约**: "三年后,我会亲自上门提亲!"
+- **倒计时**: "秘境七天后关闭"/"血月三日后降临"
+- **作用**: 制造紧迫感,防止剧情拖沓
+
+### 引擎 2: 空间转移
+- **换地图**: 从新手村→主城→圣地→仙界
+- **作用**: 重置战力层级,避免膨胀;引入新角色、新势力
+
+### 引擎 3: 外部威胁
+- **兽潮来袭**: 迫使正邪两道联手
+- **异族入侵**: 提供共同敌人
+- **作用**: 制造大事件,推动剧情高潮
+
+### 引擎 4: 寻人/寻物
+- **救妹妹**: 主角的核心动机
+- **找齐七颗龙珠**: 收集类剧情
+- **作用**: 提供明确的阶段性目标
+
+---
+
+## 5. 剧情框架组合拳
+
+单一框架容易单调,可以组合使用:
+
+### 组合案例 1: 废材逆袭 + 打脸爽文
+```
+1-50章: 废材被退婚(打脸框架的开局)
+51-100章: 获得系统(废材逆袭框架的奇遇)
+101-200章: 打脸前未婚妻(打脸框架的第一高潮)
+201-500章: 继续逆袭,打脸更强者(组合循环)
+```
+
+### 组合案例 2: 英雄之旅 + 扮猪吃虎
+```
+1-50章: 主角重生回到少年时期(英雄之旅的"召唤")
+51-100章: 刻意隐藏实力(扮猪吃虎)
+101-200章: 被逼出手,震惊全场(扮猪吃虎的爆发)
+201章+: 按英雄之旅的"试炼→考验→回归"推进
+```
+
+---
+
+## 🛠️ 框架选择指南
+
+### 选择标准
+| 读者群体 | 推荐框架 | 理由 |
+|---------|---------|------|
+| **小白读者** | 废材逆袭 + 打脸爽文 | 爽感直接,代入感强 |
+| **老白读者** | 英雄之旅 + 扮猪吃虎 | 有深度,反套路 |
+| **女性读者** | 宫斗/娱乐圈竞争框架 | 符合阅读习惯 |
+| **付费读者** | 修仙/都市异能框架 | 长线连载,易拉长 |
+
+### 框架检查清单
+- [ ] **是否有明确的三幕结构?**(开头/中间/结尾)
+- [ ] **是否有阶段性目标?**(让读者知道"下一步要干什么")
+- [ ] **是否有节奏变化?**(高潮和过渡交替,不能一直紧张或一直平淡)
+- [ ] **是否符合类型特点?**(修仙文要有飞升,都市文要有装逼打脸)
+
+---
+
+## 附录:框架速查表
+
+### 快速套用模板
+```markdown
+## 我的小说框架选择
+**类型**: 修仙文
+**主框架**: 废材逆袭(吸引新读者)
+**辅助框架**: 打脸爽文(提升爽感)
+**剧情引擎**: 救妹妹(核心动机) + 三年之约(时间限制)
+
+## 分卷规划
+第一卷: 废材期(被退婚,获得系统)
+第二卷: 逆袭期(宗门大比,打脸前未婚妻)
+第三卷: 救妹期(追踪线索,与反派初次交锋)
+第四卷: 终极对决(击败血神教主,救回妹妹)
+第五卷: 飞升(成为传奇,飞升仙界)
+```
+
+使用此模板,可以快速搭建出一个结构完整的网文框架。

+ 106 - 0
.claude/skills/webnovel-plan/references/strand-weave-pattern.md

@@ -0,0 +1,106 @@
+---
+name: strand-weave-pattern
+purpose: 章节规划时检查三线平衡,避免节奏单调
+---
+
+<context>
+此文件用于情节线平衡控制。Claude 已知多线叙事技巧,这里只补充网文特定的三线交织机制和 state.json 中的追踪器结构。
+</context>
+
+<instructions>
+
+## 三条线定义与占比
+
+| 线条 | 占比 | 定义 | 典型剧情 |
+|------|------|------|----------|
+| **Quest(主线)** | 60% | 核心任务、升级、战斗、夺宝 | 宗门大比、秘境、突破境界、复仇打脸 |
+| **Fire(感情线)** | 25% | 情感关系发展(爱情/友情/师徒) | 相识暧昧、英雄救美、确认关系 |
+| **Constellation(世界观)** | 15% | 扩展设定、展示新势力/地点 | 揭示隐藏势力、介绍新大陆、主角身世 |
+
+## 交织规则(低自由度 - 必须执行)
+
+| 规则 | 警告条件 | 建议操作 |
+|------|----------|----------|
+| Quest 不连续超过 5 章 | chapters_since_switch >= 5 | 切换到 Fire 或 Constellation |
+| Fire 不超过 10 章不出现 | current - last_fire > 10 | 安排感情戏(小甜蜜/吃醋) |
+| Constellation 不超过 15 章不出现 | current - last_constellation > 15 | 展示新设定/势力/伏笔 |
+
+## state.json 中的 strand_tracker 结构
+
+```json
+{
+  "strand_tracker": {
+    "last_quest_chapter": 45,
+    "last_fire_chapter": 43,
+    "last_constellation_chapter": 40,
+    "current_dominant": "quest",
+    "chapters_since_switch": 3,
+    "history": [{"chapter": 46, "strand": "quest"}, ...]
+  }
+}
+```
+
+## 前 30 章织网模板
+
+```
+第 1-5 章:   Quest ×5(开局快速推进主线)
+第 6 章:     Fire(首次与女主相遇)
+第 7-10 章:  Quest ×4(宗门大比)
+第 11 章:    Fire(英雄救美)
+第 12-14 章: Quest ×3(秘境夺宝)
+第 15 章:    Constellation(揭示"圣地"存在)
+第 16-19 章: Quest ×4(击败强敌)
+第 20 章:    Fire(确认关系)
+第 21-24 章: Quest ×4(突破境界)
+第 25 章:    Constellation(发现主角身世线索)
+第 26-30 章: Quest ×5(卷末高潮)
+```
+
+</instructions>
+
+<examples>
+
+<example>
+<input>当前第 46 章,last_quest=45, last_fire=43, last_constellation=40, current_dominant="quest", chapters_since_switch=3</input>
+<output>
+**警告判断**:
+- Quest 连续 3 章 < 5 章 → 暂无警告
+- 距离 Fire 3 章 < 10 章 → 暂无警告
+- 距离 Constellation 6 章 < 15 章 → 暂无警告
+
+**本章建议**: 可继续 Quest,但建议下 1-2 章内安排 Fire 调节节奏
+</output>
+</example>
+
+<example>
+<input>当前第 55 章,last_fire=42,连续 13 章无感情线</input>
+<output>
+⚠️ **警告**: 距离上次感情线 > 10 章!
+
+**建议**: 本章安排 Fire 线
+- 选项 A: 女主出场,小甜蜜互动
+- 选项 B: 英雄救美场景
+- 选项 C: 吃醋/误会小剧情
+</output>
+</example>
+
+<example type="edge_case">
+<input>开局前 10 章需要快速建立主线,感情线可以少一点吗?</input>
+<output>
+✅ 可以。前 10 章占比可调整为:
+- Quest: 70-80%
+- Fire: 10-15%
+- Constellation: 5-10%
+
+但第 6-8 章必须安排至少 1 次 Fire(首次相遇/印象建立)
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 连续 10 章纯 Quest → ✅ 最多 5 章后切换
+❌ 感情线超过 10 章不出现 → ✅ 每 5-10 章安排一次
+❌ 世界观线超过 15 章不出现 → ✅ 每 10-15 章展示新设定
+❌ 切换 Strand 后忘记更新 strand_tracker → ✅ 每章结束后自动更新
+</errors>

+ 151 - 0
.claude/skills/webnovel-query/SKILL.md

@@ -0,0 +1,151 @@
+---
+name: webnovel-query
+description: Queries project settings for characters, powers, factions, items, and foreshadowing. Supports urgency analysis and golden finger status. Activates when user asks about story elements or /webnovel-query.
+allowed-tools: Read Grep Bash AskUserQuestion
+---
+
+# Information Query Skill
+
+## Workflow Checklist
+
+Copy and track progress:
+
+```
+信息查询进度:
+- [ ] Step 1: 识别查询类型
+- [ ] Step 2: 加载对应参考文件
+- [ ] Step 3: 加载项目数据 (state.json)
+- [ ] Step 4: 确认上下文充足
+- [ ] Step 5: 执行查询
+- [ ] Step 6: 格式化输出
+```
+
+---
+
+## Step 1: 识别查询类型
+
+| 关键词 | 查询类型 | 需加载 |
+|--------|---------|--------|
+| 角色/主角/配角 | 标准查询 | system-data-flow.md |
+| 境界/筑基/金丹 | 标准查询 | system-data-flow.md |
+| 伏笔/紧急伏笔 | 伏笔分析 | foreshadowing.md |
+| 金手指/系统 | 金手指状态 | system-data-flow.md |
+| 节奏/Strand | 节奏分析 | strand-weave-pattern.md |
+| 标签/实体格式 | 格式查询 | tag-specification.md |
+
+## Step 2: 加载对应参考文件
+
+**所有查询必须执行**:
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/system-data-flow.md"
+```
+
+**伏笔查询额外执行**:
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/advanced/foreshadowing.md"
+```
+
+**节奏查询额外执行**:
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-plan/references/strand-weave-pattern.md"
+```
+
+**标签格式查询额外执行**:
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/tag-specification.md"
+```
+
+## Step 3: 加载项目数据
+
+```bash
+cat .webnovel/state.json
+```
+
+## Step 4: 确认上下文充足
+
+**检查清单**:
+- [ ] 查询类型已识别
+- [ ] 对应参考文件已加载
+- [ ] state.json 已加载
+- [ ] 知道在哪里搜索答案
+
+**如有缺失 → 返回对应 Step**
+
+## Step 5: 执行查询
+
+### 标准查询
+
+| 关键词 | 搜索目标 |
+|--------|---------|
+| 角色/主角/配角 | 主角卡.md, 角色库/ |
+| 境界/实力 | 力量体系.md |
+| 宗门/势力 | 世界观.md |
+| 物品/宝物 | 物品库/ |
+| 地点/秘境 | 世界观.md |
+
+### 伏笔紧急度分析
+
+**三层分类**(来自 foreshadowing.md):
+- **核心伏笔**: 主线剧情 - 权重 3.0x
+- **支线伏笔**: 配角/支线 - 权重 2.0x
+- **装饰伏笔**: 氛围/细节 - 权重 1.0x
+
+**紧急度公式**:
+```
+紧急度 = (已过章节 / 目标章节) × 层级权重
+```
+
+**状态判定**:
+- 🔴 Critical: 超过目标 OR 核心 >20 章
+- 🟡 Warning: >80% 目标 OR 支线 >30 章
+- 🟢 Normal: 计划范围内
+
+**快速分析**:
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/status_reporter.py" --focus urgency
+```
+
+### 金手指状态
+
+输出包含:
+- 基本信息(名称/类型/激活章节)
+- 当前等级和进度
+- 已解锁技能及冷却
+- 待解锁技能预览
+- 升级条件
+- 发展建议
+
+### Strand 节奏分析
+
+**快速分析**:
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/status_reporter.py" --focus strand
+```
+
+**检查警告**:
+- Quest >5 连续章
+- Fire >10 章未出现
+- Constellation >15 章未出现
+
+## Step 6: 格式化输出
+
+```markdown
+# 查询结果:{关键词}
+
+## 📊 概要
+- **匹配类型**: {type}
+- **数据源**: state.json + 设定集 + 大纲
+- **匹配数量**: X 条
+
+## 🔍 详细信息
+
+### 1. Runtime State (state.json)
+{结构化数据}
+**Source**: `.webnovel/state.json` (lines XX-XX)
+
+### 2. 设定集匹配结果
+{匹配内容,含文件路径和行号}
+
+## ⚠️ 数据一致性检查
+{state.json 与静态文件的差异}
+```

+ 120 - 0
.claude/skills/webnovel-query/references/advanced/foreshadowing.md

@@ -0,0 +1,120 @@
+---
+name: foreshadowing
+purpose: 伏笔紧急度分析和管理时加载
+---
+
+<context>
+此文件用于伏笔管理参考。Claude 已知一般叙事伏笔技巧,这里只补充网文特定的伏笔层级、回收周期和紧急度计算。
+</context>
+
+<instructions>
+
+## 伏笔层级分类
+
+| 层级 | 回收周期 | 权重 | 示例 |
+|------|---------|------|------|
+| **核心** | 50-300章 | 3.0x | 主角身世、终极敌人、金手指来历 |
+| **支线** | 30-100章 | 2.0x | 配角动机、神秘物品、师门秘密 |
+| **装饰** | 10-30章 | 1.0x | 场景细节、小习惯、路人提及 |
+
+## 紧急度计算
+
+```
+紧急度 = (已过章节 / 目标章节) × 层级权重
+```
+
+**状态判定**:
+- 🔴 Critical: 超过目标章节 OR 核心伏笔 >50 章未回收
+- 🟡 Warning: >80% 目标 OR 支线 >80 章未回收
+- 🟢 Normal: 在计划范围内
+
+## 伏笔埋设技巧
+
+| 技巧 | 方法 | 示例 |
+|------|------|------|
+| 顺手埋 | 正常剧情中顺带一提 | 藏书阁翻书时看到"吞天魔功"传说 |
+| 对话埋 | 通过角色对话透露 | "你的体质很特殊..." |
+| 细节埋 | 不起眼的细节 | 破庙神像缺了一只手臂 |
+| 梦境埋 | 通过梦境暗示未来 | 梦到血色世界和模糊身影 |
+
+## 伏笔回收方式
+
+| 方式 | 适用层级 | 效果 |
+|------|---------|------|
+| 直接揭晓 | 浅层 | 快速交代 |
+| 层层揭开 | 中层 | 逐步揭示真相 |
+| 意外反转 | 深层 | 制造惊喜震撼 |
+
+## 伏笔密度建议
+
+| 作品长度 | 浅层 | 中层 | 深层 |
+|---------|------|------|------|
+| 短篇(10万字) | 5-10条 | 2-3条 | 1条 |
+| 中篇(50万字) | 20-30条 | 5-10条 | 2-3条 |
+| 长篇(200万字+) | 50+条 | 20+条 | 5-10条 |
+
+**原则**: 同时进行的伏笔不超过 5 条
+
+## 禁忌
+
+- 埋了不收(读者期待落空)
+- 强行圆回(明显是后补的)
+- 伏笔太明显(一眼看穿)
+- 伏笔过多(读者记不住)
+
+</instructions>
+
+<examples>
+
+<example>
+<input>设计主角身世伏笔(核心层级)</input>
+<output>
+**第 1 章埋**: 主角父亲神秘失踪
+**第 50 章线索**: 发现父亲留下的信,提到北域
+**第 100 章线索**: 在北域找到父亲踪迹
+**第 200 章回收**: 父亲现身,原来是为了封印魔神!
+
+**回收方式**: 层层揭开
+**埋设技巧**: 对话埋(母亲临终遗言)+ 细节埋(父亲留下的玉佩)
+</output>
+</example>
+
+<example>
+<input>伏笔紧急度分析</input>
+<output>
+**当前章节**: 80
+**伏笔 F-001**: 神秘玉佩(核心,target=50)
+  紧急度 = (80/50) × 3.0 = 4.8
+  状态: 🔴 Critical(已超过目标!)
+
+**伏笔 F-002**: 师妹身份(支线,target=100)
+  紧急度 = (80/100) × 2.0 = 1.6
+  状态: 🟡 Warning(>80%)
+
+**伏笔 F-003**: 破庙细节(装饰,target=120)
+  紧急度 = (80/120) × 1.0 = 0.67
+  状态: 🟢 Normal
+</output>
+</example>
+
+<example>
+<input>草蛇灰线设计</input>
+<output>
+**第 10 章**: 主角遇到乞丐老者,给了他一颗丹药
+**第 30 章**: 主角又遇到这个老者,老者笑而不语
+**第 50 章**: 关键时刻,老者出手相救
+**第 100 章**: 老者身份揭晓——上古大能!
+
+**技巧**: 每次出现都留悬念,但不明说
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 埋了不收(读者骂坑) → ✅ 建立伏笔清单,定期回收
+❌ 强行圆回(牵强附会) → ✅ 埋得越早,回收越要合理
+❌ 伏笔太明显(一眼看穿) → ✅ 若隐若现,不要太直白
+❌ 同时 10+ 条伏笔 → ✅ 同时进行不超过 5 条
+❌ 核心伏笔 >50 章不回收 → ✅ 检查紧急度,及时回收
+</errors>

+ 219 - 0
.claude/skills/webnovel-query/references/system-data-flow.md

@@ -0,0 +1,219 @@
+---
+name: system-data-flow
+purpose: 项目初始化和状态查询时加载,理解数据结构
+version: "5.0"
+---
+
+<context>
+此文件用于项目数据结构参考。Claude 已知一般文件组织,这里只补充网文工作流特定的目录约定和脚本职责。
+</context>
+
+<instructions>
+
+## 目录约定
+
+```
+项目根目录/
+├── 正文/           # 章节文件(第0001章.md 或 第1卷/第001章-标题.md)
+├── 大纲/           # 卷纲/章纲/场景纲
+├── 设定集/         # 世界观/力量体系/角色卡/物品卡
+└── .webnovel/
+    ├── state.json          # 权威状态(entities_v3 + alias_index + 进度/主角/strand_tracker)
+    ├── workflow_state.json # 工作流断点(用于 /webnovel-resume)
+    ├── index.db            # SQLite 索引(章节/实体/别名/关系/伏笔,可重建)
+    └── archive/            # 归档数据(不活跃角色/已回收伏笔)
+```
+
+## v5.0 双 Agent 架构
+
+```
+写作前: Context Agent 读取数据 → 组装上下文包
+写作中: Writer 使用上下文包生成纯正文(无 XML 标签)
+写作后: Data Agent 处理正文 → AI 提取实体 → 写入数据链
+
+Context Agent (读) ←→ 数据存储 ←→ Data Agent (写)
+```
+
+## 脚本/模块职责速查 (v5.0)
+
+### 核心脚本
+
+| 脚本 | 输入 | 输出 |
+|------|------|------|
+| `init_project.py` | 项目信息 | 生成 `.webnovel/state.json` 等 |
+| `update_state.py` | 参数 | 原子更新 `state.json` 字段(进度/主角/strand_tracker) |
+| `backup_manager.py` | 章节号 | 自动 Git 备份 |
+| `status_reporter.py` | 无 | 生成健康报告/伏笔紧急度 |
+| `archive_manager.py` | 无 | 归档不活跃数据 |
+
+### data_modules 模块
+
+| 模块 | 职责 |
+|------|------|
+| `state_manager.py` | 实体状态管理(读写 entities_v3) |
+| `index_manager.py` | SQLite 索引管理(章节/实体/场景查询) |
+| `entity_linker.py` | 别名注册与消歧(alias_index 管理) |
+| `rag_adapter.py` | 向量嵌入与语义检索 |
+| `style_sampler.py` | 风格样本提取与管理 |
+| `api_client.py` | LLM API 调用封装 |
+| `config.py` | 配置管理 |
+
+## 每章数据链(v5.0 顺序)
+
+```
+1. Context Agent 组装上下文包
+   → 读取大纲/state.json/index.db/RAG
+   → 输出上下文包 JSON
+
+2. Writer 生成章节内容
+   → 纯正文,3000-5000 字
+   → 无需写 XML 标签
+
+3. 审查 (5 个 Agent 并行)
+   → 爽点/一致性/节奏/OOC/连贯性检查
+   → 输出审查报告
+
+4. 润色
+   → 基于审查报告修复问题
+   → AI 痕迹清除
+
+5. Data Agent 处理数据链
+   → AI 实体提取(替代 XML 标签解析)
+   → 实体消歧(置信度策略)
+   → 更新 state.json (entities_v3 + alias_index + 进度/消歧记录)
+   → 更新 index.db
+   → 向量嵌入 (RAG)
+   → 风格样本评估
+
+6. Git 备份(强制)
+```
+
+> `update_state.py` 用于手动/脚本化更新 `progress`/`protagonist_state`/`strand_tracker` 等字段;主流程通常由 Data Agent 在处理数据链时同步推进进度。
+
+## state.json 核心字段 (v5.0)
+
+```json
+{
+  "project_info": {"title": "", "genre": ""},
+  "progress": {"current_chapter": N, "total_words": W, "current_volume": 1},
+  "protagonist_state": {
+    "name": "",
+    "power": {"realm": "", "layer": 1, "bottleneck": ""},
+    "location": {"current": "", "last_chapter": 0},
+    "golden_finger": {"name": "", "level": 1, "skills": []}
+  },
+  "entities_v3": {
+    "角色": {"entity_id": {"canonical_name": "", "aliases": [], "tier": "", "current": {}, "history": []}},
+    "地点": {},
+    "物品": {},
+    "势力": {},
+    "招式": {}
+  },
+  "alias_index": {
+    "别名": [{"type": "角色", "id": "entity_id"}]
+  },
+  "relationships": {},
+  "structured_relationships": [],
+  "disambiguation_warnings": [],
+  "disambiguation_pending": [],
+  "plot_threads": {"active_threads": [], "foreshadowing": []},
+  "world_settings": {},
+  "strand_tracker": {
+    "last_quest_chapter": 0,
+    "last_fire_chapter": 0,
+    "last_constellation_chapter": 0,
+    "current_dominant": "quest",
+    "chapters_since_switch": 0,
+    "history": []
+  },
+  "review_checkpoints": []
+}
+```
+
+## Data Agent AI 提取流程
+
+v5.0 不再要求 XML 标签,由 Data Agent 智能提取:
+
+1. **实体识别**: 从正文语义识别角色/地点/物品/势力
+2. **实体匹配**: 优先匹配已有实体(通过 alias_index)
+3. **消歧处理**:
+   - 置信度 > 0.8: 自动采用
+   - 置信度 0.5-0.8: 采用但记录 warning
+   - 置信度 < 0.5: 标记待人工确认
+4. **状态变化识别**: 境界突破/位置移动/关系变化
+5. **写入存储**: entities_v3 + alias_index + index.db
+
+## 伏笔字段规范
+
+| 字段 | 规范值 | 兼容值(历史) |
+|------|--------|---------------|
+| status | `未回收` / `已回收` | 待回收/进行中/active/pending |
+
+**推荐字段**: content, status, planted_chapter, target_chapter, tier
+
+## alias_index 格式 (v5.0 一对多)
+
+```json
+{
+  "林天": [{"type": "角色", "id": "lintian"}],
+  "天云宗": [
+    {"type": "地点", "id": "loc_tianyunzong"},
+    {"type": "势力", "id": "faction_tianyunzong"}
+  ]
+}
+```
+
+同一别名可映射到多个实体,消歧时根据 type 或上下文判断。
+
+</instructions>
+
+<examples>
+
+<example>
+<input>查询当前进度</input>
+<output>
+```bash
+cat .webnovel/state.json | jq '.progress'
+# 输出: { "current_chapter": 45, "total_words": 135000 }
+```
+</output>
+</example>
+
+<example>
+<input>查询实体别名</input>
+<output>
+```bash
+cat .webnovel/state.json | jq '.alias_index["林天"]'
+# 输出: [{"type": "角色", "id": "lintian"}]
+```
+</output>
+</example>
+
+<example>
+<input>检查伏笔紧急度</input>
+<output>
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/status_reporter.py" --focus urgency
+```
+</output>
+</example>
+
+<example>
+<input>查询实体出场记录</input>
+<output>
+```bash
+python -m data_modules.index_manager entity-appearances --entity "lintian" --project-root "."
+```
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 伏笔状态写成"待回收" → ✅ 使用规范值"未回收"
+❌ 手工更新忘记加 planted_chapter → ✅ 脚本已自动补全
+❌ 归档路径混淆 → ✅ 固定为 `.webnovel/archive/*.json`
+❌ alias_index 期望单对象 → ✅ v5.0 使用数组格式(一对多)
+❌ 期望 XML 标签提取 → ✅ v5.0 由 Data Agent AI 自动提取
+❌ 使用旧版 data_modules.state_manager schema → ✅ 统一使用 entities_v3 结构
+</errors>

+ 155 - 0
.claude/skills/webnovel-query/references/tag-specification.md

@@ -0,0 +1,155 @@
+---
+name: tag-specification
+purpose: XML 标签格式参考(v5.0 可选使用)
+version: "5.0"
+---
+
+<context>
+此文件用于 XML 标签格式参考。
+
+**v5.0 重要变更**:
+- 章节写作时**不再要求**添加 XML 标签
+- Data Agent 会自动从纯正文中提取实体
+- 标签仅用于**手动标注**场景(如明确标记重要实体、补充提取遗漏)
+- 如果你选择使用标签,请遵循以下规范
+</context>
+
+<instructions>
+
+## 标签总览
+
+| 标签 | 用途 | 必填属性 |
+|------|------|----------|
+| `<entity>` | 新建/自动更新实体(角色/地点/物品/势力/招式) | type, name |
+| `<entity-alias>` | 注册实体别名/称号 | id/ref, alias |
+| `<entity-update>` | 更新实体属性(支持 set/unset/add/remove/inc + 历史追踪) | id/ref, `<set>` 等 |
+| `<skill>` | 金手指技能 | name, level, desc, cooldown |
+| `<foreshadow>` | 伏笔埋设 | content, tier |
+| `<relationship>` | 角色关系 | char1, char2, type |
+| `<deviation>` | 大纲偏离标记 | reason |
+
+## 属性详解
+
+### tier(层级)
+- **核心**: 影响主线剧情,必须追踪
+- **支线**: 丰富剧情,应该追踪
+- **装饰**: 增加真实感,可选追踪
+
+### type(实体类型)
+角色 / 地点 / 物品 / 势力 / 招式
+
+### id / ref(实体引用)
+- **id(推荐)**: 稳定唯一标识(便于后续更新/加别名)
+- **ref**: 用已出现过的名称/别名引用(脚本会通过 `alias_index` 自动解析)
+- **type(可选)**: 当 ref 有歧义时用于消歧(如同名不同人);若仍歧义必须改用 `id`
+
+### `<entity-update>` 子操作
+- **set**: `<set key="k" value="v" reason="可选"/>`
+- **unset**: `<unset key="k" reason="可选"/>`
+- **add**: `<add key="k" value="v" reason="可选"/>`(数组追加,自动去重)
+- **remove**: `<remove key="k" value="v" reason="可选"/>`(数组移除)
+- **inc**: `<inc key="k" delta="1" reason="可选"/>`(数值递增,默认 +1)
+
+**顶层字段白名单**(可直接更新实体顶层而非 current):`tier`, `desc`, `canonical_name`, `importance`, `status`, `parent`
+
+> **建议**: `<entity>` 强烈建议补充 `desc` 和 `tier`,否则后续检索和一致性检查会变差。
+
+## 放置规则
+
+- **推荐**: 章节末尾统一放置(便于管理)
+- **允许**: 实体首次出现的段落末尾
+- **要求**: 标签独占一行,不夹在正文句子中
+
+### 隐藏写法(推荐)
+
+```markdown
+正文内容...
+
+<!--
+<entity type="角色" id="luchen" name="陆辰" desc="主角,觉醒时空能力" tier="核心"/>
+<entity-alias id="luchen" alias="陆队" context="加入特勤队后"/>
+<entity-update id="luchen"><set key="realm" value="F级-觉醒者" reason="觉醒完成"/></entity-update>
+<skill name="时间回溯" level="1" desc="回到10秒前" cooldown="24小时"/>
+<foreshadow content="神秘老者的玉佩" tier="核心" target="50"/>
+<relationship char1_id="luchen" char2_id="liwe" type="ally" intensity="60" desc="初步合作"/>
+-->
+```
+
+</instructions>
+
+<examples>
+
+<example>
+<input>标记新角色</input>
+<output>
+```xml
+<entity type="角色" id="luchen" name="陆辰" desc="主角,觉醒时空能力的大学生" tier="核心"/>
+<entity type="角色" id="liwe" name="李薇" desc="女主,神秘背景的校花" tier="核心"/>
+<entity type="角色" name="咖啡店老板" desc="看似普通实则深藏不露" tier="装饰"/>
+```
+</output>
+</example>
+
+<example>
+<input>注册新称号/别名</input>
+<output>
+```xml
+<entity-alias id="luchen" alias="陆队" context="加入特勤队后"/>
+<entity-alias ref="陆辰" alias="继承者" context="系统确认身份后"/>
+```
+</output>
+</example>
+
+<example>
+<input>更新实体属性(境界/位置/状态/归属等)</input>
+<output>
+```xml
+<entity-update id="luchen">
+  <set key="realm" value="E级-掌控者" reason="危机中突破"/>
+  <set key="location" value="城西废弃实验室"/>
+</entity-update>
+```
+</output>
+</example>
+
+<example>
+<input>标记新技能</input>
+<output>
+```xml
+<skill name="时间回溯" level="1" desc="回到10秒前的状态" cooldown="24小时"/>
+<skill name="空间锚点" level="2" desc="设置传送锚点,可瞬移返回" cooldown="1小时"/>
+<skill name="时间感知" level="1" desc="被动技能,预知3秒内的危险" cooldown="无"/>
+```
+</output>
+</example>
+
+<example>
+<input>埋设伏笔</input>
+<output>
+```xml
+<foreshadow content="神秘老者留下的玉佩开始发光" tier="核心" target="50" location="废弃实验室"/>
+<foreshadow content="李薇手腕上的奇怪纹身" tier="支线" target="30" characters="李薇,陆辰"/>
+<foreshadow content="咖啡店老板意味深长的眼神" tier="装饰"/>
+```
+</output>
+</example>
+
+<example>
+<input>标记大纲偏离</input>
+<output>
+```xml
+<deviation reason="临时灵感,增加李薇与陆辰的情感互动,为后续感情线铺垫"/>
+<deviation reason="原计划本章突破,但节奏过快,延迟到下章"/>
+```
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ `<entity type='角色' .../>` → ✅ 使用双引号 `type="角色"`
+❌ `<entity type="角色" ...>` → ✅ 自闭合 `.../>` 或补全 `</entity>`
+❌ `<Entity type="角色" .../>` → ✅ 小写标签名 `<entity`
+❌ `[NEW_ENTITY: 角色, 陆辰, ...]` → ✅ 使用XML格式
+❌ `<entity-update ref="xxx"></entity-update>` → ✅ 至少包含一个 `<set key="..." value="..."/>`
+</errors>

+ 165 - 0
.claude/skills/webnovel-resume/SKILL.md

@@ -0,0 +1,165 @@
+---
+name: webnovel-resume
+description: Recovers interrupted webnovel tasks with precise workflow state tracking. Detects interruption point and provides safe recovery options. Activates when user wants to resume or /webnovel-resume.
+allowed-tools: Read Bash AskUserQuestion
+---
+
+# Task Resume Skill
+
+## Workflow Checklist
+
+Copy and track progress:
+
+```
+任务恢复进度:
+- [ ] Step 1: 加载恢复协议 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-resume/references/workflow-resume.md")
+- [ ] Step 2: 加载数据规范 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-resume/references/system-data-flow.md")
+- [ ] Step 3: 确认上下文充足
+- [ ] Step 4: 检测中断状态
+- [ ] Step 5: 展示恢复选项 (AskUserQuestion)
+- [ ] Step 6: 执行恢复
+- [ ] Step 7: 继续任务 (可选)
+```
+
+---
+
+## Step 1: 加载恢复协议(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-resume/references/workflow-resume.md"
+```
+
+**核心原则**(读取后应用):
+- **禁止智能续写**: 上下文丢失风险高
+- **必须检测后恢复**: 不猜测中断点
+- **必须用户确认**: 不自动恢复
+
+## Step 2: 加载数据规范
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-resume/references/system-data-flow.md"
+```
+
+## Step 3: 确认上下文充足
+
+**检查清单**:
+- [ ] 恢复协议已理解
+- [ ] Step 难度分级已知
+- [ ] 状态结构已理解
+- [ ] "删除重来" vs "智能续写" 原则已明确
+
+**如有缺失 → 返回对应 Step**
+
+## Step 难度分级(来自 workflow-resume.md)
+
+| Step | 难度 | 恢复策略 |
+|------|------|---------|
+| Step 1 | ⭐ | 直接重新执行 |
+| Step 2 | ⭐⭐ | 删除半成品,重新开始 |
+| Step 3 | ⭐⭐ | 重新运行提取 |
+| Step 4 | ⭐⭐⭐ | 验证 state.json 一致性 |
+| Step 5 | ⭐⭐ | 重新运行 strand 更新 |
+| Step 6 | ⭐⭐⭐⭐⭐ | 成本高 - 5个检查员 ~$0.15 |
+| Step 7 | ⭐ | 重新运行 git 备份 |
+
+## Step 4: 检测中断状态
+
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" detect
+```
+
+**输出情况**:
+- 无中断 → 结束流程,通知用户
+- 检测到中断 → 继续 Step 5
+
+## Step 5: 展示恢复选项(必须执行)
+
+**展示给用户**:
+- 任务命令和参数
+- 中断时间和已过时长
+- 已完成步骤
+- 当前(中断)步骤
+- 剩余步骤
+- 恢复选项及风险等级
+
+**示例输出**:
+
+```
+🔴 检测到中断任务:
+
+任务:/webnovel-write 7
+中断位置:Step 2 - 章节内容生成中
+
+已完成:
+  ✅ Step 1: 上下文加载
+
+未完成:
+  ⏸️ Step 2: 章节内容(已写1500字)
+  ⏹️ Step 3-7: 未开始
+
+恢复选项:
+A) 删除半成品,从Step 1重新开始(推荐)
+B) 回滚到Ch6,放弃Ch7所有进度
+
+请选择(A/B):
+```
+
+## Step 6: 执行恢复
+
+**选项 A - 删除重来**(推荐):
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" cleanup --chapter {N}
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" clear
+```
+
+**选项 B - Git 回滚**:
+```bash
+git reset --hard ch{N-1:04d}
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" clear
+```
+
+## Step 7: 继续任务(可选)
+
+如用户选择立即继续:
+```bash
+/{original_command} {original_args}
+```
+
+---
+
+## 特殊场景
+
+### Step 6 中断(成本高)
+
+```
+恢复选项:
+A) 重新执行双章审查(成本:~$0.15)⚠️
+B) 跳过审查,继续下一章(可后续补审)
+```
+
+### Step 4 中断(部分状态)
+
+```
+⚠️ state.json 可能部分更新
+
+A) 检查并修复 state.json
+B) 回滚到上一章(安全)
+```
+
+### 长时间中断(>1小时)
+
+```
+⚠️ 中断已超过1小时
+
+上下文丢失风险高
+建议重新开始而非续写
+```
+
+---
+
+## 禁止事项
+
+- ❌ 智能续写半成品内容
+- ❌ 自动选择恢复策略
+- ❌ 跳过中断检测
+- ❌ 不验证就修复 state.json

+ 42 - 0
.claude/skills/webnovel-resume/references/system-data-flow.md

@@ -0,0 +1,42 @@
+---
+name: system-data-flow-redirect
+purpose: 重定向到权威版本
+---
+
+<context>
+此文件已迁移到统一位置,避免多版本不同步问题。
+</context>
+
+<instructions>
+
+## 权威版本位置
+
+`skills/webnovel-query/references/system-data-flow.md` (v5.0)
+
+## 加载方式
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/system-data-flow.md"
+```
+
+## 快速参考
+
+### 目录结构
+```
+项目根目录/
+├── 正文/           # 章节文件
+├── 大纲/           # 卷纲/章纲
+├── 设定集/         # 世界观/力量体系/角色卡
+└── .webnovel/
+    ├── state.json          # 权威状态
+    ├── workflow_state.json # 工作流断点
+    ├── index.db            # SQLite 索引
+    └── archive/            # 归档数据
+```
+
+### v5.0 核心变化
+- **双 Agent 架构**: Context Agent (读) + Data Agent (写)
+- **无 XML 标签**: 纯正文写作,Data Agent AI 自动提取实体
+- **统一 schema**: entities_v3 + alias_index(一对多)
+
+</instructions>

+ 139 - 0
.claude/skills/webnovel-resume/references/workflow-resume.md

@@ -0,0 +1,139 @@
+---
+name: workflow-resume
+purpose: 任务恢复时加载,指导中断恢复流程
+version: "5.0"
+---
+
+<context>
+此文件用于中断任务恢复。Claude 已知错误处理流程,这里只补充网文创作工作流特定的 Step 难度分级和恢复策略。
+</context>
+
+<instructions>
+
+## Step 中断难度分级 (v5.0)
+
+| Step | 名称 | 影响 | 难度 | 默认策略 |
+|------|------|------|------|----------|
+| Step 1 | Context Agent | 无副作用(仅读取) | ⭐ | 直接重新执行 |
+| Step 2 | 生成章节内容 | 半成品章节文件 | ⭐⭐ | **删除半成品**,从 Step 1 重新开始 |
+| Step 3 | 审查 | 审查未完成 | ⭐⭐⭐ | 用户决定:重审或跳过 |
+| Step 4 | 润色 | 部分润色的文件 | ⭐⭐ | 继续润色或删除重写 |
+| Step 5 | Data Agent | 实体未提取完 | ⭐⭐ | 重新运行(幂等) |
+| Step 6 | Git 备份 | 未提交 | ⭐⭐⭐ | 检查暂存区,决定提交/回滚 |
+
+## 恢复流程
+
+### Phase 1: 检测中断状态
+
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" detect
+```
+
+### Phase 2: 询问用户
+
+**必须展示**:
+- 任务命令和参数
+- 中断时间和位置
+- 已完成步骤
+- 恢复选项和风险等级
+
+### Phase 3: 执行恢复
+
+**选项 A(推荐)**: 删除半成品重新开始
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" cleanup --chapter {N}
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" clear
+/webnovel-write {N}
+```
+
+**选项 B**: 回滚到上一章
+```bash
+git reset --hard ch{N-1:04d}
+python "${CLAUDE_PLUGIN_ROOT}/scripts/workflow_manager.py" clear
+```
+
+## 为什么删除而不续写?
+
+1. **质量保证**: 半成品可能包含未完成句子、逻辑断裂
+2. **上下文丢失**: 新会话无法记住之前创作思路
+3. **防幻觉**: 续写容易产生前后矛盾
+4. **成本可控**: 重新生成 < 修复半成品 + 审查
+
+## 特殊场景
+
+| 场景 | 策略 |
+|------|------|
+| 超过24小时 | 推荐回滚(上下文完全丢失) |
+| 超过1小时 | 推荐重新开始 |
+| 多个半成品 | 删除所有,从稳定状态重新开始 |
+| workflow_state.json不存在 | 启发式检测 state.json vs 文件 |
+
+</instructions>
+
+<examples>
+
+<example>
+<input>Step 2 中断(最常见)</input>
+<output>
+🔴 检测到中断任务:
+
+任务:/webnovel-write 7
+中断位置:Step 2 - 章节内容生成中
+
+已完成:
+  ✅ Step 1: Context Agent 搜集上下文
+
+未完成:
+  ⏸️ Step 2: 生成章节内容(已写1500字)
+  ⏹️ Step 3-6: 未开始
+
+恢复选项:
+A) 删除半成品,从 Step 1 重新开始(推荐)⭐
+B) 回滚到 Ch6,放弃 Ch7 所有进度
+
+请选择(A/B):
+</output>
+</example>
+
+<example>
+<input>Step 3 中断(审查)</input>
+<output>
+恢复选项:
+A) 重新执行审查 ⚠️
+   - 调用5个审查员
+   - 生成审查报告
+   - 继续 Step 4 润色
+
+B) 跳过审查,直接润色(推荐)
+   - 不生成审查报告
+   - 可后续用 /webnovel-review 补审
+
+请选择(A/B):
+</output>
+</example>
+
+<example>
+<input>Step 4 中断(润色)</input>
+<output>
+恢复选项:
+A) 继续润色(推荐)⭐
+   - 打开并继续润色 正文/第0007章.md
+   - 保存文件
+   - 继续 Step 5(Data Agent)
+
+B) 删除润色稿,从 Step 2 重写
+   - 删除 正文/第0007章.md
+   - 重新生成章节内容
+
+请选择(A/B):
+</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 智能续写半成品 → ✅ 删除后重新生成
+❌ 自动决定恢复策略 → ✅ 必须用户确认
+❌ 跳过中断检测 → ✅ 先运行 workflow_manager.py detect
+❌ 修复 state.json 不验证 → ✅ 逐字段检查一致性
+</errors>

+ 133 - 0
.claude/skills/webnovel-review/SKILL.md

@@ -0,0 +1,133 @@
+---
+name: webnovel-review
+description: Reviews chapter quality using 5 specialized checkers and generates comprehensive reports. Activates when user requests chapter review or /webnovel-review.
+allowed-tools: Read Grep Write Edit Bash Task AskUserQuestion
+---
+
+# Quality Review Skill
+
+## Workflow Checklist
+
+Copy and track progress:
+
+```
+质量审查进度:
+- [ ] Step 1: 加载核心约束 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/core-constraints.md")
+- [ ] Step 2: 加载爽点标准 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/cool-points-guide.md")
+- [ ] Step 3: 加载节奏标准 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/strand-weave-pattern.md")
+- [ ] Step 4: 加载常见错误 (cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/common-mistakes.md")
+- [ ] Step 5: 加载项目状态 (cat .webnovel/state.json)
+- [ ] Step 6: 确认上下文充足
+- [ ] Step 7: 调用 5 个检查员 (并行 Task)
+- [ ] Step 8: 生成审查报告
+- [ ] Step 9: 处理关键问题
+```
+
+---
+
+## Step 1: 加载核心约束(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/core-constraints.md"
+```
+
+## Step 2: 加载爽点标准(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/cool-points-guide.md"
+```
+
+## Step 3: 加载节奏标准(必须执行)
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/strand-weave-pattern.md"
+```
+
+## Step 4: 加载常见错误
+
+```bash
+cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-review/references/common-mistakes.md"
+```
+
+## Step 5: 加载项目状态
+
+```bash
+cat .webnovel/state.json
+```
+
+## Step 6: 确认上下文充足
+
+**检查清单**:
+- [ ] 三大定律已理解
+- [ ] 爽点密度要求已理解
+- [ ] Strand Weave 规范已理解
+- [ ] 常见错误模式已了解
+- [ ] state.json 可用于一致性检查
+- [ ] 待审查章节已确定
+
+**如有缺失 → 返回对应 Step**
+
+## Step 7: 调用 5 个检查员(并行)
+
+**使用 Task 工具并行调用 5 个专职检查员**:
+
+调用格式示例(所有检查员并行执行):
+- 调用 `high-point-checker` 子代理:审查章节 {range},重点检查爽点密度和多样性
+- 调用 `consistency-checker` 子代理:审查章节 {range},重点检查设定违规 vs state.json
+- 调用 `pacing-checker` 子代理:审查章节 {range},重点检查 Strand 分布
+- 调用 `ooc-checker` 子代理:审查章节 {range},重点检查角色行为一致性
+- 调用 `continuity-checker` 子代理:审查章节 {range},重点检查时间线和剧情连贯
+
+**注意**:Claude 会自动根据描述匹配并调用对应的子代理
+
+## Step 8: 生成审查报告
+
+保存到: `审查报告/第{start}-{end}章审查报告.md`
+
+**报告结构**:
+
+```markdown
+# 第 {start}-{end} 章质量审查报告
+
+## 📊 综合评分
+
+| 维度 | 评分 | 状态 |
+|------|------|------|
+| 爽点密度 | X/10 | ✅/🟡/🟠/🔴 |
+| 设定一致性 | X/10 | ... |
+| 节奏控制 | X/10 | ... |
+| 人物塑造 | X/10 | ... |
+| 连贯性 | X/10 | ... |
+| **总评** | **X/50** | **等级** |
+
+## 📋 修改优先级
+
+### 🔴 高优先级(必须修改)
+{检查员发现的问题}
+
+### 🟠 中优先级(建议修改)
+{检查员发现的问题}
+
+### 🟡 低优先级(可选优化)
+{检查员发现的问题}
+
+## 📈 改进建议
+{具体可行的建议}
+```
+
+**评分标准**:
+- 9-10: 优秀
+- 7-8: 良好
+- 5-6: 及格
+- <5: 不及格(高流失风险)
+
+## Step 9: 处理关键问题
+
+如发现 🔴 问题,询问用户:
+- A) 立即修复(推荐)
+- B) 保存报告稍后处理
+
+```bash
+python "${CLAUDE_PLUGIN_ROOT}/scripts/update_state.py" \
+  --add-review "{start}-{end}" "审查报告/第{start}-{end}章审查报告.md"
+```

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно