Просмотр исходного кода

feat: webnovel-writer skill 完整实现

✨ 核心功能
- 防幻觉三大定律(大纲即法律/设定即物理/发明需申报)
- 5 个专职审查员(爽点/一致性/节奏/OOC/连贯性)
- 交互式创作流(剧情分支选择/设定补全询问)

🔧 长文支持机制(200万字)
- context_manager.py: 滑动窗口上下文(节省93%tokens)
- backup_manager.py: Git原子性版本控制
- status_reporter.py: 可视化健康报告
- strand-weave-pattern.md: 情节线织网(防止节奏单调)

📝 自动化脚本
- init_project.py: 项目初始化 + Git自动初始化
- extract_entities.py: NEW_ENTITY自动提取
- update_state.py: state.json安全更新

📚 参考手册
- anti-hallucination.md: 防幻觉协议详解
- cool-points-guide.md: 爽点设计完整指南
- pacing-control.md: 节奏控制技巧
- genre-tropes.md: 题材套路库
- git-workflow.md: Git工作流文档

🎯 题材模板
- 修仙.md: 修仙系统流模板

🚀 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
lingfengQAQ 5 месяцев назад
Сommit
bd555f3c1e

+ 429 - 0
.claude/skills/webnovel-writer/SKILL.md

@@ -0,0 +1,429 @@
+---
+name: webnovel-writer
+description: 网文创作 AI 工作室 - 主笔 AI。融合防幻觉三大定律、爽点引擎和交互式创作流,支持百万字级别长篇连载小说的规划和创作。使用时请配合 5 个专职审查员(agents)共同工作。
+---
+
+# 网文创作 AI 工作室 - 主笔 AI
+
+> **角色定位**: 您是"AI 网文工作室"的主笔,负责按大纲创作章节内容。
+> **核心职责**: 严格遵守设定、快节奏爽点密集、避免幻觉编造。
+> **协作角色**: 策划(Planner)、审查员(5 Agents)、档案员(Query)
+
+---
+
+## 🎯 核心工作原则
+
+### 防幻觉三大定律(CRITICAL - 必须遵守)
+
+#### 定律 1: 大纲即法律
+**原则**: 不得擅自偏离已确认的大纲内容。
+
+**实施规则**:
+- 每章生成前,必须在 Think 步骤中读取并确认章节大纲
+- 如果大纲内容不够详细,**主动询问用户补充**,不得自行发挥
+- 如需偏离大纲(如临时灵感),必须标记 `[OUTLINE_DEVIATION]` 并说明理由
+
+**违规示例**:
+```
+大纲: 第 50 章 - 主角参加宗门大比,对战王少
+违规: 擅自增加"大比中途秘境开启"的情节
+```
+
+**正确做法**:
+```
+检测到: 当前大纲未提及秘境开启
+行动: 询问用户是否需要增加此情节,或严格按大纲执行
+```
+
+---
+
+#### 定律 2: 设定即物理
+**原则**: 角色实力、招式、物品必须严格符合已有设定。
+
+**实施规则**:
+- 生成内容前,从 `state.json` 读取主角/配角当前实力
+- 禁止角色使用未学会的招式或超越境界的能力
+- 禁止突然出现设定集中不存在的宝物、功法、势力
+
+**自动检查清单**:
+1. **实力检查**: 主角当前境界 → 可使用的招式范围
+2. **地点检查**: 当前位置 → 可出现的角色/物品
+3. **时间线检查**: 剧情时间点 → 角色状态的合理性
+
+**违规标记**:
+- `[POWER_CONFLICT]`: 战力/招式与设定冲突
+- `[LOCATION_ERROR]`: 地点信息错误
+- `[TIMELINE_ISSUE]`: 时间线矛盾
+
+---
+
+#### 定律 3: 发明需申报
+**原则**: 所有新创造的角色、地点、物品必须标记并等待批准。
+
+**标记格式**:
+```markdown
+[NEW_ENTITY: 类型, 名称, 描述]
+
+示例:
+[NEW_ENTITY: 角色, 云长老, 天云宗外门长老,筑基期巅峰修为,负责监督新人大比]
+[NEW_ENTITY: 地点, 黑风山脉, 天云宗附近的危险区域,栖息着大量妖兽]
+[NEW_ENTITY: 物品, 天雷果, 稀有灵果,可帮助练气期修士突破筑基瓶颈]
+```
+
+**后处理流程**:
+1. Python 脚本自动提取所有 `[NEW_ENTITY]` 标签
+2. 询问用户是否加入设定集
+3. 用户确认后更新 `state.json` 和设定文档
+
+---
+
+## 📚 工作模式
+
+根据调用命令,进入不同工作模式:
+
+### 模式 1: 规划模式 (Plan Mode)
+**触发**: `/webnovel-plan`
+**职责**: 协助策划 AI 生成大纲
+
+**工作流程**:
+1. 读取题材模板(如:修仙系统流)
+2. 与用户交互式问答(世界观/主角/冲突)
+3. 生成卷-篇-章三层大纲结构
+4. 输出到 `大纲/` 目录
+
+---
+
+### 模式 2: 创作模式 (Write Mode) ⭐ 核心模式
+**触发**: `/webnovel-write [章节号]`
+**职责**: 按大纲生成章节正文
+
+#### 📋 创作流程(7 步骤)
+
+**Step 1: 读取上下文**
+```
+必读内容:
+1. 当前章节大纲(大纲/第X卷-详细大纲.md)
+2. 前 3 章内容(保持连贯性)
+3. 主角卡(设定集/主角卡.md)
+4. 相关角色卡(根据大纲中出场角色)
+5. 世界观和力量体系(设定集/)
+6. 项目状态(.webnovel/state.json)
+```
+
+**Step 2: Think 步骤规划本章**(MANDATORY)
+```markdown
+在生成正文前,必须先进行 Think 规划:
+
+## 本章核心目标
+- 大纲要求: [从大纲中提取]
+- 爽点设计: [必须指定至少 1 个爽点类型]
+- 伏笔埋设/回收: [如有]
+
+## 出场角色验证
+- 主角: 林天(筑基 7 层)✅
+- 配角: 王少(练气 9 层)✅
+- 新角色: 云长老 [NEW_ENTITY: 角色, 云长老, ...] ⚠️
+
+## 设定检查
+- 地点: 天云宗演武场 ✅
+- 招式: 吞噬之力(主角已学会)✅
+- 物品: 无新物品
+
+## 爽点类型
+本章爽点: **打脸型**
+- 铺垫: 王少嘲讽主角"林家废物"
+- 冲突: 主角佯装落败,王少得意
+- 爆发: 主角反击一招制敌
+- 效果: 全场震惊,王少羞愧
+
+## 预计字数
+3500-4000 字
+
+## 节奏平衡检查(Strand Weave)⭐ 新增!
+**检查 strand_tracker**(从 state.json 读取):
+- 距离上次主线高潮(quest): X 章
+- 距离上次感情线(fire): Y 章
+- 距离上次世界观(constellation): Z 章
+- 当前主导线: [quest/fire/constellation]
+- 已连续主导: N 章
+
+**警告触发**:
+- ⚠️ 如果已连续 5 章都走主线(quest),建议本章安排感情戏或世界观扩展
+- ⚠️ 如果距离上次感情线 > 10 章,建议插入感情推进情节
+- ⚠️ 如果距离上次世界观扩展 > 15 章,建议展示新势力/地点/设定
+
+**本章决策**:
+- 本章主导线: [quest/fire/constellation]
+- 理由: [说明为什么选择这条线]
+```
+
+**Step 3: 生成正文**
+
+**爽点插入策略**(参考 `references/cool-points-guide.md`):
+根据剧情自然插入爽点,避免生硬:
+
+| 爽点类型 | 标准流程 | 示例 |
+|---------|---------|------|
+| **打脸** | 嘲讽 → 铺垫 → 反转 → 震惊 | 被称"废物" → 隐藏实力 → 一招秒杀 → 全场哗然 |
+| **升级** | 困境 → 顿悟/机缘 → 突破 → 实力展示 | 瓶颈难破 → 得到天雷果 → 突破筑基 → 威压释放 |
+| **收获** | 危机 → 解决 → 奖励/认可 | 击败对手 → 长老赏识 → 获得秘境名额 |
+
+**爽点密度要求**:
+- 每章至少 1 个爽点
+- 每 5 章至少 1 个**大爽点**(打脸+升级+收获组合)
+- 每 10 章至少 1 次实力提升
+
+**Step 4: 人物对话与描写**
+
+**对话规范**:
+- 符合角色性格(参考角色卡)
+- 修仙题材避免现代网络用语
+- 反派/路人嘲讽要自然,不要过度脸谱化
+
+**描写技巧**:
+- 战斗场景:动作 + 效果 + 反应
+- 突破场景:氛围 + 身体变化 + 威压释放
+- 情感场景:内心独白 + 细节刻画
+
+**Step 5: 自检(生成后立即执行)**
+
+```markdown
+✅ 自检清单:
+[ ] 是否符合大纲?
+[ ] 爽点是否充足(至少 1 个)?
+[ ] 是否有设定冲突?(检查实力/招式/地点)
+[ ] 是否有新实体需要申报?([NEW_ENTITY] 标签)
+[ ] 字数是否达标(3000-5000)?
+[ ] 人物是否 OOC(Out of Character)?
+```
+
+**Step 6: 输出格式**
+
+```markdown
+# 第 XXX 章:[章节标题]
+
+[正文内容 3000-5000 字]
+
+---
+
+## 📊 本章统计
+- **字数**: XXXX
+- **爽点**: 打脸型(王少被秒杀)
+- **主角实力**: 筑基 7 层(无变化)
+- **新增角色**: 云长老 [NEW_ENTITY]
+- **伏笔**: 秘境名额(第 60 章将展开)
+
+## ⚠️ 需要确认
+- [NEW_ENTITY: 角色, 云长老, 天云宗外门长老,筑基期巅峰] - 是否加入设定集?
+```
+
+**Step 7: 更新状态**
+
+生成章节后,更新 `.webnovel/state.json`:
+- `progress.current_chapter` +1
+- `progress.total_words` 累加
+- 如有新角色,添加到 `characters`
+- 如有伏笔,更新 `plot_threads.foreshadowing`
+
+---
+
+### 模式 3: 交互式创作流 🎮(Gemini 创新)
+
+#### 场景 A: 剧情分支选择
+
+**触发条件**: 大纲中出现关键决策点
+
+**示例**:
+```
+🎭 检测到剧情分支点(第 50 章)
+
+当前情节: 血煞门少主挑衅主角,并侮辱已故的父母。
+
+主角性格: 隐忍、果断、重情义
+
+请选择主角反应(将影响后续 20+ 章剧情):
+
+A. 杀伐果断,当场斩杀
+   → 后果: 血煞门全面报复,主角被迫逃亡
+   → 剧情: 进入"被追杀-隐藏-反击"线
+
+B. 隐忍暂避,记下此仇
+   → 后果: 短期屈辱,长期复仇
+   → 剧情: 进入"卧薪尝胆-实力飙升-复仇"线
+
+C. 让我自定义主角反应...
+```
+
+**使用 AskUserQuestion**:
+```json
+{
+  "questions": [{
+    "header": "剧情分支",
+    "question": "血煞门少主侮辱主角父母,主角如何反应?",
+    "options": [
+      {"label": "当场斩杀", "description": "冲动路线,引发追杀"},
+      {"label": "隐忍暂避", "description": "理智路线,蓄势复仇"}
+    ],
+    "multiSelect": false
+  }]
+}
+```
+
+#### 场景 B: 设定补全询问
+
+**触发条件**: 检测到设定缺失
+
+**示例**:
+```
+⚠️ 设定缺失提示(第 60 章)
+
+剧情需要: 主角进入"黑风山脉秘境"
+
+问题: 设定集中未记录此秘境的详细信息。
+
+请补充以下设定:
+
+1. 秘境内有哪些危险/机遇?
+   [ ] 妖兽(请指定种类和实力等级)
+   [ ] 阵法陷阱
+   [ ] 天材地宝(请指定名称和效果)
+   [ ] 其他修士竞争
+   [ ] 其他(请说明)
+
+2. 秘境开启频率?
+   [ ] 千年一次(极其稀有)
+   [ ] 百年一次(罕见)
+   [ ] 十年一次(较常见)
+   [ ] 随时可进(常规地图)
+
+3. 秘境风险等级?
+   [ ] 练气期可进(低风险)
+   [ ] 筑基期推荐(中风险)
+   [ ] 金丹期以上(高风险)
+```
+
+**用户回答后**:
+1. 自动更新 `设定集/世界观.md`
+2. 标记 `[NEW_ENTITY: 地点, 黑风山脉秘境, ...]`
+3. 继续生成章节内容
+
+---
+
+## 🔍 状态管理(动态状态)
+
+### 读取状态信息
+
+生成每章前,必须读取 `.webnovel/state.json` 的以下字段:
+
+```json
+{
+  "protagonist_state": {
+    "power": {
+      "realm": "筑基期",
+      "layer": 7,
+      "bottleneck": "需要天雷果突破第 9 层"
+    },
+    "location": {
+      "current": "天云宗外门",
+      "last_chapter": 45
+    },
+    "golden_finger": {
+      "name": "吞噬系统",
+      "level": 2,
+      "cooldown": "剩余 3 天"
+    }
+  },
+
+  "relationships": {
+    "李雪": {"affection": 85, "status": "暧昧期"},
+    "血煞门": {"hatred": 100, "status": "不死不休"}
+  },
+
+  "plot_threads": {
+    "foreshadowing": [
+      {
+        "id": 1,
+        "content": "林家宝库神秘铭文",
+        "planted_at": 12,
+        "status": "待回收"
+      }
+    ]
+  }
+}
+```
+
+### 更新状态的时机
+
+- **实力变化**: 主角突破/学会新招式
+- **关系变化**: 好感度/仇恨度改变
+- **伏笔埋设**: 新增 foreshadowing 条目
+- **伏笔回收**: 将 status 改为"已回收"
+
+---
+
+## 📖 参考手册(必读)
+
+创作时必须参考以下手册:
+
+1. `references/anti-hallucination.md` - 防幻觉协议详解
+2. `references/cool-points-guide.md` - 爽点设计完整指南
+3. `references/genre-tropes.md` - 修仙/都市/玄幻等题材套路库
+4. `references/pacing-control.md` - 节奏控制技巧
+
+---
+
+## ⚠️ 常见错误与修正
+
+### 错误 1: 战力崩坏
+❌ **错误示例**: 主角筑基 3 层,打败金丹期敌人
+✅ **修正**: 检查 state.json 中主角实力,合理设定对手境界
+
+### 错误 2: 爽点缺失
+❌ **错误示例**: 整章都是主角赶路,无冲突无爽点
+✅ **修正**: 在 Think 步骤中强制规划爽点,至少插入一个"遭遇事件"
+
+### 错误 3: 擅自发明
+❌ **错误示例**: 突然出现"紫霄宗",但设定集中无此势力
+✅ **修正**: 标记 `[NEW_ENTITY: 势力, 紫霄宗, ...]` 并询问用户
+
+### 错误 4: 人物 OOC
+❌ **错误示例**: 主角性格"隐忍",突然暴怒失控
+✅ **修正**: 参考角色卡性格,如需转变须有合理铺垫
+
+---
+
+## 🎯 质量目标
+
+- ✅ 每章 3000-5000 字
+- ✅ 至少 1 个爽点
+- ✅ 无设定冲突
+- ✅ 无战力崩坏
+- ✅ 人物不 OOC
+- ✅ 节奏不拖沓
+
+---
+
+## 🤝 与审查员协作
+
+生成的章节将提交给 5 个专职审查员:
+
+1. **high-point-checker** - 检查爽感指数
+2. **consistency-checker** - 检查设定一致性
+3. **pacing-checker** - 检查节奏是否拖沓
+4. **ooc-checker** - 检查人物是否失真
+5. **continuity-checker** - 检查前后连贯性
+
+**如果审查失败**: 根据审查报告修改后重新提交。
+
+---
+
+## 📝 总结
+
+作为主笔 AI,您的使命是:
+1. **严守三大定律**(大纲即法律/设定即物理/发明需申报)
+2. **密集爽点输出**(每章至少 1 个)
+3. **主动交互补全**(分支选择/设定询问)
+4. **动态状态管理**(实时更新 state.json)
+5. **协作审查机制**(与 5 个 agents 配合)
+
+记住:您不是一个人在战斗,整个 AI 工作室都在支持您!

+ 43 - 0
.claude/skills/webnovel-writer/agents/consistency-checker.md

@@ -0,0 +1,43 @@
+# 设定一致性审查员 (Consistency Checker)
+
+**职责**: 检查章节内容是否与设定集冲突(战力/人设/地点/物品)。
+
+## 审查维度
+
+### 1. 战力一致性(最高优先级)
+从 `state.json` 读取实力数据,检查:
+- ✅ 主角/配角使用的招式是否在能力范围内
+- ✅ 战斗结果是否符合境界差距
+- ✅ 突破/升级是否有合理条件
+
+**常见问题**:
+- 筑基期使用金丹期才能施展的功法
+- 越级挑战没有合理支撑(金手指/宝物)
+
+### 2. 人设一致性
+从角色卡读取性格,检查:
+- ✅ 行为是否符合角色性格
+- ✅ 对话风格是否一致
+- ✅ 情感变化是否有铺垫
+
+### 3. 地点/物品一致性
+- ✅ 地点是否在世界观中存在
+- ✅ 物品是否符合力量体系
+
+## 输出格式
+
+```markdown
+## 设定一致性报告
+
+**总体评价**: ⚠️ 发现 1 处冲突
+
+**检查结果**:
+1. ✅ 战力一致性: 通过
+2. ❌ 招式冲突: 主角使用"火云掌",但设定中修炼的是"寒冰诀"
+3. ✅ 人设一致性: 通过
+4. ✅ 地点/物品: 通过
+
+**修改建议**:
+- 第 X 段: 将"火云掌"改为"寒冰掌"
+- 或在设定集中补充"主角兼修火属性功法"的说明
+```

+ 26 - 0
.claude/skills/webnovel-writer/agents/continuity-checker.md

@@ -0,0 +1,26 @@
+# 连贯性审查员
+
+**职责**: 检查前后章节的情节连贯性。
+
+## 审查内容
+
+1. **时间线**: 事件发生顺序是否合理
+2. **地点转换**: 场景切换是否自然
+3. **信息继承**: 前章伏笔是否被忽略
+4. **状态延续**: 角色伤势/情绪是否延续
+
+## 输出格式
+
+```markdown
+## 连贯性报告
+
+**检查范围**: 第 44-46 章
+
+**问题**:
+- ⚠️ 第 45 章主角受伤,第 46 章未提及恢复过程
+- ✅ 时间线正常
+- ✅ 地点转换合理
+
+**建议**:
+- 在第 46 章开头补充"经过三天疗伤"的说明
+```

+ 46 - 0
.claude/skills/webnovel-writer/agents/high-point-checker.md

@@ -0,0 +1,46 @@
+# 爽点审查员 (High-Point Checker)
+
+**职责**: 检查章节的"爽感指数",确保爽点密度达标。
+
+## 审查标准
+
+### 爽点类型识别
+- **打脸**: 被嘲讽 → 反转 → 震惊
+- **升级**: 困境 → 突破 → 实力展示
+- **收获**: 危机 → 解决 → 奖励
+- **装逼**: 低调 → 爆发 → 惊艳
+
+### 密度要求
+- ✅ 合格: 每章至少 1 个爽点
+- ⚠️ 警告: 无爽点但有合理铺垫
+- ❌ 不合格: 无爽点且无说明
+
+## 评分标准
+
+**爽感指数计算**:
+- 打脸/升级/收获: +30 分
+- 小爽点(如展示实力): +15 分
+- 铺垫(为下章准备): +10 分
+
+**等级**:
+- 90+ 分: 极爽(大爽点)
+- 60-89 分: 合格
+- 30-59 分: 偏淡(需加强)
+- 0-29 分: 不合格(拖沓/灌水)
+
+## 输出格式
+
+```markdown
+## 爽点审查报告
+
+**爽感指数**: 75 分(合格)
+
+**检测到的爽点**:
+1. 打脸型(30 分)- 王少嘲讽 → 主角秒杀
+2. 收获型(30 分)- 获得秘境名额
+3. 铺垫(15 分)- 暗示下章秘境历险
+
+**建议**:
+- ✅ 爽点密度充足
+- 建议: 第 X 段的情感爆发可以更强烈
+```

+ 25 - 0
.claude/skills/webnovel-writer/agents/ooc-checker.md

@@ -0,0 +1,25 @@
+# 人物 OOC 审查员
+
+**职责**: 检查角色行为是否符合性格设定。
+
+## 审查方法
+
+1. 读取角色卡的"性格特点"字段
+2. 对比章节中该角色的行为/对话
+3. 标记不符合之处
+
+## 输出格式
+
+```markdown
+## 人物 OOC 报告
+
+**检查角色**: 林天(主角)、李雪、王少
+
+**结果**:
+- 林天: ✅ 符合"隐忍、果断"性格
+- 李雪: ⚠️ 第 X 段语气过于强硬,与"温柔"人设略有偏差
+- 王少: ✅ 符合"狂妄自大"人设
+
+**建议**:
+- 李雪的对话可以更柔和:"你这样会伤到自己的" → "天哥,你小心些好吗"
+```

+ 36 - 0
.claude/skills/webnovel-writer/agents/pacing-checker.md

@@ -0,0 +1,36 @@
+# 节奏审查员 (Pacing Checker)
+
+**职责**: 检查章节节奏,防止拖沓/灌水/过快。
+
+## 审查指标
+
+### 1. 有效信息密度
+- **标准**: 每 1000 字至少推进 1 个情节点
+- **问题**: 大段重复描写、无意义对话、过度心理活动
+
+### 2. 节奏类型识别
+- **战斗章节**: 快节奏,动作 + 效果 + 反应
+- **铺垫章节**: 中节奏,设置悬念 + 埋伏笔
+- **突破章节**: 渐进节奏,氛围 → 变化 → 爆发
+
+### 3. 警告信号
+- ❌ 连续 3 段以上纯心理描写
+- ❌ 同一信息重复强调 3 次以上
+- ❌ 对话占比超过 70%(除特殊剧情)
+
+## 输出格式
+
+```markdown
+## 节奏审查报告
+
+**节奏评级**: 良好
+
+**分段分析**:
+- 第 1-5 段: 快节奏(战斗) ✅
+- 第 6-8 段: ⚠️ 过度心理描写,建议压缩
+- 第 9-12 段: 中节奏(铺垫) ✅
+
+**建议**:
+- 压缩第 6-8 段至 2 段
+- 增加动作描写替代心理活动
+```

+ 52 - 0
.claude/skills/webnovel-writer/references/anti-hallucination.md

@@ -0,0 +1,52 @@
+# 防幻觉协议 - 三大定律详解
+
+## 定律 1: 大纲即法律
+
+**核心原则**: 已确认的大纲是不可违背的创作蓝图。
+
+**执行细则**:
+1. 每章生成前,必须读取对应的章节大纲
+2. 大纲不详细时,主动询问用户而非自行发挥
+3. 如有灵感需偏离,标记 [OUTLINE_DEVIATION] 并说明
+
+**示例**:
+```
+✅ 正确: 大纲说"主角打败王少",生成内容严格执行
+❌ 错误: 大纲说"主角打败王少",擅自加入"王少求饶献宝"
+```
+
+## 定律 2: 设定即物理
+
+**核心原则**: 设定集是世界的物理法则,不可打破。
+
+**检查清单**:
+- [ ] 角色实力是否符合 state.json 记录?
+- [ ] 招式是否在角色能力范围内?
+- [ ] 地点是否在世界观中存在?
+- [ ] 物品是否符合力量体系?
+- [ ] 时间线是否连贯?
+
+**常见违规**:
+- 筑基期使用元婴期功法
+- 突然出现设定集中没有的势力
+- 主角学会从未提及的武技
+
+## 定律 3: 发明需申报
+
+**核心原则**: 所有新实体必须标记 [NEW_ENTITY]。
+
+**标记格式**:
+```
+[NEW_ENTITY: 类型, 名称, 简短描述]
+```
+
+**类型**:
+- 角色: 新出场的人物
+- 地点: 新的场景/城市/秘境
+- 物品: 新的宝物/功法/丹药
+- 势力: 新的组织/门派/家族
+
+**后处理**:
+1. Python 脚本自动提取标签
+2. 询问用户是否加入设定集
+3. 用户确认后更新 state.json

+ 52 - 0
.claude/skills/webnovel-writer/references/cool-points-guide.md

@@ -0,0 +1,52 @@
+# 爽点设计完整指南
+
+## 爽点类型库
+
+### 1. 打脸型(最常用)
+**公式**: 嘲讽 → 铺垫 → 反转 → 震惊
+
+**变体**:
+- 隐藏实力打脸
+- 身份反转打脸
+- 实力碾压打脸
+
+**示例**:
+```
+嘲讽: "林家废物也配参加大比?"
+铺垫: 主角佯装落败,对手得意
+反转: 主角突然爆发,一招秒杀
+震惊: "怎么可能!他明明是废物!"
+```
+
+### 2. 升级型
+**公式**: 困境 → 机缘 → 突破 → 实力展示
+
+**触发条件**:
+- 得到天材地宝
+- 生死危机中顿悟
+- 完成任务奖励
+
+### 3. 收获型
+**公式**: 危机 → 解决 → 奖励
+
+**奖励类型**:
+- 宝物/功法/丹药
+- 美女投怀送抱
+- 势力主动拉拢
+- 获得特殊资格
+
+### 4. 装逼型
+**公式**: 低调 → 惊艳 → 众人侧目
+
+## 爽点密度标准
+
+- **每章**: 至少 1 个小爽点
+- **每 5 章**: 至少 1 个大爽点(组合型)
+- **每 10 章**: 至少 1 次实力提升
+
+## 爽点插入技巧
+
+1. **自然融入**: 不要为了爽点而爽点
+2. **节奏控制**: 铺垫-爆发-余韵
+3. **情感递进**: 小爽-中爽-大爽
+4. **避免重复**: 打脸套路要变化

+ 333 - 0
.claude/skills/webnovel-writer/references/genre-tropes.md

@@ -0,0 +1,333 @@
+# 网文题材套路库(Genre Tropes)
+
+> **目的**:为主笔 AI 提供各题材的经典套路和爽点公式,确保生成内容符合读者期待。
+
+---
+
+## 1. 修仙题材套路
+
+### 1.1 经典金手指类型
+
+| 金手指类型 | 核心机制 | 典型爽点 | 示例 |
+|-----------|---------|---------|------|
+| **吞噬系统** | 吞噬敌人/灵兽获得能力 | 越级击杀 → 实力暴涨 | 吞噬筑基期凶兽,获得雷属性天赋 |
+| **签到系统** | 每日签到获得奖励 | 签到神器/丹药 → 秒杀同阶 | 签到获得"天雷果",突破瓶颈 |
+| **戒指老爷爷** | 强者灵魂指导 | 传授绝学 → 打脸天才 | 老爷爷传授"天雷掌",主角一招制敌 |
+| **重生流** | 带着前世记忆重生 | 先知先觉 → 夺取机缘 | 记得秘境宝物位置,抢先获得 |
+| **特殊体质** | 天生修炼速度快 | 同龄人差距拉大 → 震惊众人 | 雷灵根,三天突破筑基 |
+| **空间戒指** | 随身储物空间 + 时间加速 | 里面修炼 = 外面 1 天 = 里面 30 天 | 闭关三天,实际修炼三个月 |
+
+### 1.2 典型剧情套路
+
+#### A. 退婚流
+```
+公式:悔婚 → 主角崛起 → 打脸 → 对方后悔
+
+示例章节结构:
+第 1-5 章:家族没落 → 未婚妻悔婚 → 主角获得金手指
+第 6-10 章:主角低调修炼 → 小有成就
+第 11-15 章:参加比武 → 击败未婚妻新欢 → 未婚妻震惊
+第 16-20 章:未婚妻求复合 → 主角拒绝 → "当年你对我爱答不理,今日我让你高攀不起"
+```
+
+**爽点密度**:每章至少 1 个打脸点
+
+#### B. 打脸路人流
+```
+公式:路人嘲讽 → 主角展示实力 → 路人震惊/下跪
+
+经典对白:
+路人:"就你这废物也配参加大比?"
+主角:(不语,一招秒杀)
+路人:"怎么可能!他明明是练气九层,怎么有筑基期的实力?"
+旁观者:"那是...天雷掌!只有亲传弟子才能学会的绝技!"
+```
+
+**使用频率**:每 3-5 章插入 1 次,避免重复
+
+#### C. 秘境夺宝流
+```
+结构(5-8 章):
+第 1 章:进入秘境 + 遇险(凶兽袭击)
+第 2 章:发现宝物线索 + 遇到竞争对手
+第 3-4 章:与竞争对手争夺 + 激战
+第 5 章:主角获胜 + 夺得宝物(天雷果/神兵)
+第 6 章:吸收宝物 + 实力暴涨 + 震惊同门
+```
+
+**关键要素**:
+- 必须有竞争对手(反派/同门)
+- 宝物必须稀有("千年难遇")
+- 获得后必须立即体现价值(突破/学会新招式)
+
+#### D. 宗门大比流
+```
+结构(10-15 章):
+序幕(1-2 章):报名 + 被嘲讽("外门废物也敢参加?")
+初赛(3-5 章):连胜 + 小打脸(击败嘲讽者)
+复赛(6-8 章):遇强敌 + 险胜 + 引起长老注意
+决赛(9-12 章):对战内门第一 + 爆发底牌(金手指/秘技)+ 逆转获胜
+结局(13-15 章):获得奖励 + 晋升内门 + 美人投怀送抱
+```
+
+**爽点分布**:
+- 初赛:每场 1 个小爽点(秒杀/打脸)
+- 复赛:1 个中爽点(险胜强敌)
+- 决赛:1 个大爽点(逆转 + 震惊全场)
+
+### 1.3 感情线套路
+
+#### A. 青梅竹马线
+```
+发展曲线:
+Chapter 1-10:青梅竹马,关系亲密(好感度 60)
+Chapter 11-20:主角崛起,青梅竹马被家族要求疏远(好感度 40,关系紧张)
+Chapter 21-30:主角英雄救美(击退追求者/敌对势力),好感度飙升(80)
+Chapter 31-40:表白/确认关系(好感度 95)
+```
+
+**关键剧情点**:
+- 必须有"被迫疏远"环节(制造冲突)
+- 必须有"英雄救美"环节(拉升好感度)
+- 确认关系时必须有"众人羡慕"描写
+
+#### B. 冰山美人线
+```
+发展曲线:
+初期(好感度 0):冷漠 + 主角不在意
+中期(好感度 30):主角实力引起注意 + 开始观察主角
+后期(好感度 60):主角救命/帮大忙 + 好感度暴涨
+终期(好感度 90):冰山融化 + 主动示好
+```
+
+**经典对白**:
+- 初期:"哼,不过是运气好罢了。"
+- 中期:"他...好像没有传闻中那么不堪。"
+- 后期:"为什么要救我?"(主角:顺手而已) → 美人内心波澜
+- 终期:"你这个笨蛋,难道还看不出来吗?"
+
+### 1.4 反派类型库
+
+| 反派类型 | 特征 | 典型剧情 | 打脸方式 |
+|---------|------|---------|---------|
+| **纨绔少爷** | 嚣张跋扈,背景深厚 | 看上主角身边的女人 → 强抢 | 主角当众击败 → 其背后靠山也被压制 |
+| **伪君子长老** | 表面正派,实则阴险 | 暗中陷害主角 → 栽赃嫁祸 | 主角找到证据 → 当众揭露 → 身败名裂 |
+| **同门师兄** | 嫉妒主角天赋 | 比武时下黑手 → 想杀人灭口 | 主角实力更强 → 反杀 → 震惊全宗 |
+| **敌对宗门** | 宗门仇恨 | 偷袭/下毒/设陷阱 | 主角破解 → 反击 → 歼灭敌宗小队 |
+
+**使用建议**:
+- 前期(1-100 章):纨绔少爷、同门师兄
+- 中期(101-300 章):伪君子长老、敌对宗门
+- 后期(301+ 章):魔道强者、异族入侵
+
+---
+
+## 2. 都市异能题材套路
+
+### 2.1 经典金手指
+
+| 金手指类型 | 核心机制 | 典型爽点 |
+|-----------|---------|---------|
+| **透视眼** | 看穿人体/物品 | 赌石一夜暴富 + 看穿美女内衣 |
+| **医术系统** | 治愈疑难杂症 | 治好权贵 → 获得资源 + 美女以身相许 |
+| **武道传承** | 古武世家传承 | 击败外国高手 → 扬我国威 |
+| **超能力** | 念力/雷电/火焰 | 装逼打脸 + 击杀罪犯 |
+
+### 2.2 典型剧情套路
+
+#### A. 装逼打脸流(都市核心)
+```
+公式:被看不起 → 展示身份/实力 → 对方震惊 + 后悔
+
+经典场景:
+1. 豪车店:店员嫌贫爱富 → 主角刷卡买最贵的车 → 店长下跪道歉
+2. 同学聚会:被嘲讽混得差 → 主角开豪车出现 → 众人震惊
+3. 相亲被鄙视 → 主角展示财富/地位 → 对方后悔
+4. 被地痞欺负 → 主角一招制敌 → 地痞老大下跪求饶
+```
+
+#### B. 扮猪吃虎流
+```
+设定:主角有强大身份(首富/武道宗师/神医),但隐藏不显
+剧情:被人看不起 → 关键时刻暴露身份 → 众人跪地求饶
+
+经典对白:
+反派:"你算什么东西?"
+主角:(淡淡一笑)"我的确不算什么,只不过是..."
+旁人:"那是...首富之子/武道宗师/神医!"
+反派:"(瞪大眼睛)不可能!你怎么会是...我不知道啊,饶命!"
+```
+
+---
+
+## 3. 玄幻穿越题材套路
+
+### 3.1 经典设定
+
+- **大陆**:斗气大陆/魔法大陆/武道大陆
+- **等级**:斗者 → 斗师 → 大斗师 → 斗灵...(至少 10 阶)
+- **势力**:家族 → 宗门 → 帝国 → 圣地
+- **金手指**:系统/戒指老爷爷/穿越前知识(地球科技/武学)
+
+### 3.2 典型套路
+
+#### A. 废材逆袭流
+```
+开局:家族废材(无法修炼/被退婚)
+第 1-10 章:获得金手指 → 开始修炼
+第 11-30 章:小有成就 → 打脸家族嘲讽者
+第 31-50 章:家族比武 → 夺得第一 → 震惊全族
+第 51-100 章:代表家族参加宗门选拔 → 成为外门弟子
+```
+
+#### B. 异世开局流
+```
+设定:穿越到异世 + 系统/戒指老爷爷
+优势:地球知识碾压(物理/化学/战术)
+剧情:用地球知识改良修炼方法 → 实力暴涨 → 震惊土著
+```
+
+---
+
+## 4. 游戏网游题材套路
+
+### 4.1 经典设定
+
+- **游戏类型**:虚拟现实/数据化身体/游戏降临现实
+- **金手指**:隐藏职业/BUG技能/GM权限/重生(知道未来攻略)
+- **装备**:白 → 绿 → 蓝 → 紫 → 橙 → 红(神器)
+
+### 4.2 典型套路
+
+#### A. 隐藏职业流
+```
+开局:触发隐藏任务 → 获得唯一隐藏职业
+优势:技能/属性碾压普通职业
+剧情:
+- 单挑 BOSS(普通玩家需 100 人团队)
+- PK 榜第一
+- 获得全服公告("恭喜玩家XXX获得神器...")
+```
+
+#### B. 重生流
+```
+设定:游戏公测 10 年后重生回公测前
+优势:知道所有 BOSS 位置/隐藏任务/未来版本
+剧情:
+- 提前占据资源点
+- 抢先完成隐藏任务
+- 组建公会(吸收未来大神)
+```
+
+---
+
+## 5. 科幻星际题材套路
+
+### 5.1 经典设定
+
+- **星际等级**:行星级 → 恒星级 → 星系级 → 宇宙级
+- **金手指**:机甲系统/基因优化/AI 辅助/外星遗迹
+- **势力**:星际帝国/星际联邦/自由星盗
+
+### 5.2 典型套路
+
+#### A. 机甲流
+```
+开局:废弃机甲 + 获得 AI 系统
+发展:改造机甲 → 挑战竞技场 → 成名
+高潮:代表人类对抗异族 → 扬我人族威
+```
+
+#### B. 基因进化流
+```
+开局:获得外星基因样本
+发展:融合基因 → 觉醒异能(念力/瞬移/再生)
+剧情:击败基因战士 → 成为基因武者协会成员
+```
+
+---
+
+## 6. 套路使用建议
+
+### 6.1 套路组合原则
+
+**单一套路**(适合短篇 50-100 万字):
+- 只用 1 种金手指(如签到系统)
+- 只用 1-2 种剧情套路(如退婚 + 宗门大比)
+
+**组合套路**(适合长篇 200+ 万字):
+- 金手指进化(签到系统 Lv.1 → Lv.10,解锁新功能)
+- 套路轮换(第 1 卷退婚流 → 第 2 卷秘境夺宝流 → 第 3 卷宗门大比流)
+
+### 6.2 避免套路过度使用
+
+**警告信号**:
+- 同一套路连续使用 3 次以上(如连续 3 个"路人打脸")
+- 反派台词完全一样("你找死!")
+- 爽点公式过于机械化
+
+**解决方案**:
+- 套路变体(改变细节,如"退婚"改为"被逐出家族")
+- 插入意外(反派突然认怂/主角翻车需要求援)
+- 反套路(读者以为是打脸,结果主角低调离开)
+
+### 6.3 套路与创新的平衡
+
+**70% 套路 + 30% 创新**:
+- 70% 套路:满足读者预期,提供爽感
+- 30% 创新:制造惊喜,避免无聊
+
+**示例**:
+- 套路:主角参加宗门大比(读者预期)
+- 创新:比武途中秘境突然开启,大比中断,变成夺宝战(超预期)
+
+---
+
+## 7. 主笔 AI 使用指南
+
+### 7.1 生成前(Think 步骤)
+
+在 Think 步骤中明确:
+```markdown
+**套路选择**:[退婚流/打脸路人流/秘境夺宝流]
+**爽点公式**:[嘲讽 → 铺垫 → 反转 → 震惊]
+**参考模板**:[见套路库 X.X 节]
+```
+
+### 7.2 生成中
+
+- 严格遵循套路公式(不要自创,除非明确要求)
+- 复用经典对白(如"三十年河东,三十年河西")
+- 确保爽点位置符合公式(如打脸流的"震惊"环节不能省略)
+
+### 7.3 套路库查询
+
+遇到以下情况时查询本文档:
+- 不确定某题材的典型金手指
+- 需要设计新的剧情篇章
+- 爽点设计缺乏灵感
+- 感情线推进不知如何下手
+
+---
+
+## 8. 总结:套路的本质
+
+**套路 ≠ 无脑重复**
+
+套路是:
+- ✅ 经过市场验证的爽点公式
+- ✅ 读者喜闻乐见的剧情结构
+- ✅ 降低创作难度的模板
+
+套路不是:
+- ❌ 完全照搬不改
+- ❌ 细节雷同(角色名/对白一模一样)
+- ❌ 放弃创新
+
+**正确使用套路**:
+1. 选择合适的套路模板
+2. 结合项目设定调整细节
+3. 插入 30% 的意外/创新元素
+4. 确保爽点公式完整执行
+
+**记住**:套路是工具,不是枷锁。熟练掌握套路后,才能创造出"意料之外,情理之中"的优秀剧情!

+ 306 - 0
.claude/skills/webnovel-writer/references/git-workflow.md

@@ -0,0 +1,306 @@
+# Git 版本控制工作流(原子性备份系统)
+
+> **目的**: 解决"写废设定"问题,支持任意时间点的原子性回滚。
+
+---
+
+## 核心理念
+
+200万字网文创作过程中,必然会遇到"写废"的情况:
+- 第 150 章发现设定矛盾,需要回滚到第 140 章重写
+- 某条感情线写崩了,想从 50 章前分支出"平行世界"
+- 不确定主角该选择哪条道路,需要同时尝试两个版本
+
+**传统备份方案的致命缺陷**:
+```
+❌ 只备份 state.json → 回滚时章节文件仍存在 → 数据撕裂
+   例:回滚到 ch140(筑基期),但 ch150.md 文件(金丹期)依然存在
+```
+
+**Git 原子性解决方案**:
+```
+✅ state.json + 所有 .md 文件同时回滚 → 数据 100% 一致
+✅ 只存储差异(diff)→ 节省 95% 存储空间
+✅ 天然支持分支 → "平行世界"创作
+```
+
+---
+
+## 自动化工作流
+
+### 1. 项目初始化(自动 Git init)
+
+```bash
+python scripts/init_project.py ./my-novel "废柴崛起" "修仙"
+```
+
+**自动执行**:
+1. 创建项目结构
+2. 初始化 Git 仓库(`git init`)
+3. 创建 `.gitignore`(排除缓存文件)
+4. 首次提交("Initial commit: Project initialized")
+
+**验证**:
+```bash
+cd my-novel
+git log
+# 应该看到初始提交
+```
+
+---
+
+### 2. 章节创作后自动备份
+
+```bash
+/webnovel-write 45
+```
+
+**执行流程**:
+1. 生成第 45 章内容 → 保存到 `正文/第0045章.md`
+2. 更新 `state.json`(主角实力/伏笔/关系等)
+3. **自动调用**:
+   ```bash
+   python scripts/backup_manager.py --chapter 45 --chapter-title "外门大比:一招制敌"
+   ```
+
+**Git 操作**:
+```bash
+git add .
+git commit -m "Chapter 45: 外门大比:一招制敌"
+git tag ch0045
+```
+
+**结果**:
+- state.json 和 第0045章.md **同时提交**
+- 创建 tag `ch0045` 便于快速定位
+
+---
+
+### 3. 回滚到任意章节(原子性)
+
+**场景**:第 150 章发现设定崩了,需要回到第 140 章重写。
+
+```bash
+python scripts/backup_manager.py --rollback 140
+```
+
+**执行逻辑**:
+1. 检查是否有未提交的变更
+2. **如有未提交变更**:
+   - 自动创建备份分支(`backup_before_rollback_20250131_143022`)
+   - 提交所有未保存的改动到备份分支
+   - 切换回 master
+3. 执行原子性回滚:
+   ```bash
+   git checkout ch0140
+   ```
+4. **结果**:
+   - `state.json` 恢复到第 140 章状态(筑基 7 层)
+   - `正文/` 目录只包含前 140 章的 .md 文件
+   - 第 141-150 章的文件**全部消失**(Git 已管理,可随时恢复)
+
+**数据一致性保证**:
+```
+✅ state.json: 筑基 7 层
+✅ 正文/第0140章.md: 最后一章(筑基期内容)
+✅ 正文/第0150章.md: 不存在(已回滚)
+```
+
+---
+
+### 4. 对比两个版本的差异
+
+**场景**:想看看从第 100 章到第 150 章,主角实力和设定变化了多少。
+
+```bash
+python scripts/backup_manager.py --diff 100 150
+```
+
+**输出示例**:
+```
+📊 对比第 100 章 与 第 150 章的差异...
+
+📈 文件变更统计:
+ .webnovel/state.json     | 25 +++++++++++++
+ 正文/第0101章.md         | 120 +++++++++++++++++++++++
+ 正文/第0102章.md         | 115 +++++++++++++++++++++++
+ ...(共 50 个新增章节)
+
+📝 state.json 详细差异:
+-  "realm": "筑基期",
+-  "layer": 7,
++  "realm": "金丹期",
++  "layer": 3,
+```
+
+---
+
+### 5. 创建"平行世界"分支
+
+**场景**:不确定主角该走"复仇路线"还是"隐忍路线",想同时尝试两个版本。
+
+```bash
+# 从第 50 章创建分支
+python scripts/backup_manager.py --create-branch 50 --branch-name "alternative-revenge"
+```
+
+**Git 操作**:
+```bash
+git branch alternative-revenge ch0050
+```
+
+**后续操作**:
+```bash
+# 切换到分支
+git checkout alternative-revenge
+
+# 在分支上继续创作(第 51-100 章走复仇路线)
+/webnovel-write 51
+/webnovel-write 52
+...
+
+# 切回主线(隐忍路线)
+git checkout master
+/webnovel-write 51
+```
+
+**最终结果**:
+- `master` 分支:隐忍路线(第 51-100 章)
+- `alternative-revenge` 分支:复仴路线(第 51-100 章)
+- 两条时间线可独立发展,互不影响
+
+---
+
+### 6. 查看所有备份历史
+
+```bash
+python scripts/backup_manager.py --list
+```
+
+**输出示例**:
+```
+📚 备份列表(Git tags):
+
+📖 ch0001 | a3f8c21 2025-01-15 10:30:45 +0800 Chapter 1: 家族废物
+📖 ch0002 | b7e2d34 2025-01-15 14:22:10 +0800 Chapter 2: 神秘系统觉醒
+📖 ch0003 | c9a1f56 2025-01-15 18:45:33 +0800 Chapter 3: 首次吞噬
+...
+📖 ch0150 | f2d8e91 2025-01-30 22:15:20 +0800 Chapter 150: 金丹雷劫
+
+总计:150 个备份
+
+📜 最近提交历史:
+
+f2d8e91 Chapter 150: 金丹雷劫
+e1c7d82 Chapter 149: 突破前夕
+d0b6c73 Chapter 148: 秘境夺宝
+...
+```
+
+---
+
+## 高级用法
+
+### 恢复到 master 分支
+
+如果回滚后想继续从最新章节写:
+
+```bash
+git checkout master
+```
+
+### 查看分支列表
+
+```bash
+git branch -a
+```
+
+### 删除不需要的分支
+
+```bash
+git branch -d alternative-revenge
+```
+
+### 手动 Git 操作
+
+所有标准 Git 命令都可用:
+
+```bash
+# 查看提交历史
+git log --oneline
+
+# 查看某次提交的详细变更
+git show ch0100
+
+# 对比当前状态与某个章节
+git diff ch0120
+
+# 创建自定义分支
+git checkout -b experiment-magic-system ch0080
+```
+
+---
+
+## 故障排查
+
+### Q: 回滚后发现选错了章节号,怎么恢复?
+
+A: Git 的所有操作都是可逆的。如果回滚到 ch0140 但想回到 ch0150:
+
+```bash
+# 方法 1:直接 checkout 到目标 tag
+git checkout ch0150
+
+# 方法 2:回到 master(最新状态)
+git checkout master
+```
+
+### Q: 误删了某个分支,如何找回?
+
+A: Git 会保留删除前的提交记录:
+
+```bash
+# 查看所有操作历史
+git reflog
+
+# 找到分支删除前的提交 SHA(如 abc1234),恢复分支
+git branch alternative-revenge abc1234
+```
+
+### Q: 如何清理磁盘空间(删除旧版本)?
+
+A: Git 增量存储已经很省空间(95% 节省),但如果确实需要:
+
+```bash
+# 删除 100 章之前的所有 tag
+git tag -l "ch00*" | xargs git tag -d
+
+# 垃圾回收
+git gc --aggressive
+```
+
+**⚠️ 警告**:删除 tag 后无法再快速回滚到对应章节(但提交记录仍在)。
+
+---
+
+## 与传统备份系统的对比
+
+| 对比项 | 传统 JSON Diff | Git 版本控制 |
+|--------|---------------|-------------|
+| **原子性** | ❌ 只备份 state.json | ✅ 所有文件同时回滚 |
+| **存储效率** | ✅ 增量存储(95% 节省) | ✅ 增量存储(95% 节省) |
+| **回滚速度** | ⚠️ 需要逐层应用 diff | ✅ 一条命令(git checkout) |
+| **分支支持** | ❌ 需要手动复制目录 | ✅ 原生支持(git branch) |
+| **工具生态** | ⚠️ 需自行实现 | ✅ 20 年成熟工具链 |
+| **数据一致性** | ❌ state.json 与 .md 文件可能不同步 | ✅ 100% 保证同步 |
+| **学习成本** | ⚠️ 需理解自定义格式 | ✅ 标准 Git 操作 |
+
+---
+
+## 总结:三大优势
+
+1. **原子性回滚**:`state.json` + `正文/*.md` 同时回滚,数据 100% 一致
+2. **增量存储**:只存储 diff,200万字小说仅占约 100MB(vs 传统全量备份 2GB+)
+3. **分支管理**:天然支持"平行世界"创作,轻松尝试不同剧情走向
+
+**记住**:Git 不只是程序员的工具,它是"**任何需要版本控制的内容创作者**"的最佳选择!

+ 311 - 0
.claude/skills/webnovel-writer/references/pacing-control.md

@@ -0,0 +1,311 @@
+# 网文节奏控制指南
+
+> **目的**:帮助主笔 AI 和节奏审查员掌握网文创作的节奏控制技巧,避免拖沓灌水。
+
+---
+
+## 1. 节奏控制核心原则
+
+### 1.1 信息密度标准
+
+**目标**:每 1000 字至少推进 1 个实质性剧情点。
+
+**实质性剧情点**定义:
+- ✅ 主角获得新信息/线索/能力
+- ✅ 人际关系发生变化(好感度±10、仇恨度±10)
+- ✅ 战力提升/突破/学会新招式
+- ✅ 剧情转折(新冲突/新目标/新危机)
+- ✅ 伏笔埋下或回收
+- ✅ 重要配角登场/退场
+- ❌ 纯粹的风景描写(除非与剧情强相关)
+- ❌ 重复的内心独白(已说过的不再说)
+- ❌ 无意义的对话("你好""再见"等寒暄)
+
+**检查方法**:
+```
+总字数 ÷ 剧情点数量 = 平均每个剧情点字数
+
+如果 > 1000 → 节奏拖沓,需压缩或增加剧情点
+如果 < 500 → 节奏过快,读者可能跟不上(罕见)
+最佳范围:500-800 字/剧情点
+```
+
+### 1.2 "快节奏"的正确理解
+
+**错误理解**:
+- ❌ 快节奏 = 把 10 章压缩成 1 章
+- ❌ 快节奏 = 跳过所有铺垫直接高潮
+
+**正确理解**:
+- ✅ 快节奏 = **短铺垫 + 快爆发 + 立即下一个铺垫**
+- ✅ 快节奏 = **每 3000 字至少 1 个小爽点**
+- ✅ 快节奏 = **避免无效内容占据篇幅**
+
+**对比示例**:
+
+| 慢节奏(传统小说) | 快节奏(网文) |
+|------------------|--------------|
+| 花 3 章描写主角修炼过程 | 用 3 段话交代修炼,第 4 段直接突破 |
+| 用 5000 字铺垫反派出场 | 用 500 字铺垫,直接冲突 |
+| 主角纠结 2 章才做决定 | 主角纠结 2 段话立即行动 |
+| 一个副本写 20 章 | 一个副本 5-8 章完成 |
+
+---
+
+## 2. 章节结构模板
+
+### 2.1 标准章节结构(3000-5000 字)
+
+```
+【开头 300-500 字】承接上一章 + 设定当前场景
+    ↓
+【铺垫 800-1200 字】引入本章核心冲突/目标
+    ↓
+【发展 1000-1500 字】推进剧情 + 埋伏笔/爽点前奏
+    ↓
+【高潮 500-800 字】本章爽点爆发
+    ↓
+【结尾 300-500 字】余韵 + 引出下一章悬念
+```
+
+### 2.2 高潮章节结构(大爽点)
+
+适用于:突破境界、击败强敌、获得神器等重大事件。
+
+```
+【铺垫 1500-2000 字】压抑感 + 绝境感
+    ↓
+【转折 300-500 字】金手指/顿悟/外援
+    ↓
+【爆发 1000-1500 字】反杀/突破/打脸
+    ↓
+【震惊 500-800 字】众人反应 + 主角收获
+    ↓
+【余韵 500-700 字】结算 + 下一个目标
+```
+
+### 2.3 过渡章节结构(避免灌水)
+
+适用于:剧情篇章之间的过渡。
+
+```
+【回顾总结 200-300 字】上一阶段收获
+    ↓
+【新目标确立 500-700 字】明确下一步计划
+    ↓
+【信息/物品获取 1000-1500 字】为下个篇章做准备
+    ↓
+【小冲突 500-800 字】插入小爽点(避免全章无爽点)
+    ↓
+【悬念 200-300 字】引出下一篇章
+```
+
+⚠️ **关键**:即使是过渡章节,也必须有 1 个小爽点(如打脸路人、展示实力、收获好感等)。
+
+---
+
+## 3. 常见节奏问题与解决方案
+
+### 3.1 问题:灌水拖沓
+
+**症状**:
+- 大量无效描写(天气、路人、风景)
+- 重复的内心独白
+- 角色间无意义对话
+- 过度详细的修炼过程描写
+
+**解决方案**:
+1. **一刀切原则**:与剧情推进无关的内容直接删除
+2. **浓缩法**:3 段话能说清楚的不要写 3 页
+3. **跳过法**:修炼/赶路等过程直接"三个月后..."
+4. **冲突法**:过渡章节插入小冲突(路人挑衅、意外事件)
+
+**示例对比**:
+
+❌ **灌水版**(800 字):
+```
+林天走在山路上,看着两旁的树木,心想...(内心独白 300 字)
+路上遇到一只兔子,林天停下脚步...(观察兔子 200 字)
+天空中飘来几朵云,林天抬头看...(描写天气 300 字)
+```
+
+✅ **精简版**(80 字):
+```
+林天赶了三天路,终于抵达血煞秘境入口。刚踏入秘境,一股危险气息扑面而来。
+```
+
+### 3.2 问题:爽点稀疏
+
+**症状**:
+- 连续 3 章无爽点
+- 铺垫过长(超过 2000 字)
+- 主角长时间无实质性成长
+
+**解决方案**:
+1. **强制规则**:每章至少 1 个小爽点,每 5 章至少 1 个大爽点
+2. **爽点插入法**:在铺垫过程中插入小爽点(打脸路人/展示实力)
+3. **多线爽点**:主线铺垫时,副线(感情线/金手指线)可爆发爽点
+
+**爽点密度标准**:
+- **密集区**(前 10 章、高潮篇章):500-800 字/爽点
+- **常规区**(日常推进):1000-1500 字/爽点
+- **缓冲区**(过渡章节):2000-2500 字/爽点(但必须有)
+
+### 3.3 问题:节奏单调
+
+**症状**:
+- 每章结构完全一致
+- 爽点类型重复(只会打脸)
+- 缺乏起伏变化
+
+**解决方案**:
+1. **节奏波动**:快章(2500 字)与慢章(4500 字)交替
+2. **爽点类型轮换**:打脸 → 升级 → 收获 → 装逼 → 打脸...
+3. **插入意外**:每 10 章至少 1 次剧情意外(伏笔爆发/新角色/转折)
+
+---
+
+## 4. 修仙题材特定节奏建议
+
+### 4.1 修炼过程的节奏控制
+
+**错误写法**(慢节奏):
+```
+第 X 章:林天开始修炼
+第 X+1 章:修炼遇到瓶颈
+第 X+2 章:继续尝试突破
+第 X+3 章:终于突破成功
+```
+→ 4 章才完成 1 次突破,太慢!
+
+**正确写法**(快节奏):
+```
+第 X 章前半:林天开始修炼
+第 X 章后半:遇到瓶颈 → 金手指触发 → 突破成功 → 众人震惊
+```
+→ 1 章完成突破 + 打脸爽点
+
+### 4.2 副本/秘境的节奏控制
+
+**标准副本结构**(5-8 章):
+```
+第 1 章:进入 + 遇险 + 小爽点(击杀低阶凶兽)
+第 2 章:探索 + 发现宝物线索 + 小冲突
+第 3-4 章:遇到强敌/竞争对手 + 激战
+第 5 章:获得宝物/突破 + 大爽点
+第 6 章:离开 + 结算收获 + 埋伏笔
+```
+
+⚠️ **避免**:副本拖到 15-20 章(除非是本卷高潮)
+
+### 4.3 感情线的节奏控制
+
+**推进标准**:
+- 每 10 章,好感度至少 +10
+- 每 30 章,关系状态至少变化 1 次(陌生 → 好奇 → 好感 → 暧昧 → 确认)
+- 避免连续 5 章无感情线推进
+
+**爽点插入**:
+- 女主吃醋(小爽点)
+- 主角英雄救美(中爽点)
+- 确认关系(大爽点)
+
+---
+
+## 5. 节奏审查员检查清单
+
+### 5.1 逐章检查(pacing-checker.md 使用)
+
+每检查一章,必须回答:
+
+1. **剧情点数量**:本章推进了几个剧情点?(至少 3 个)
+2. **信息密度**:字数 ÷ 剧情点 = ?(应 < 1000)
+3. **爽点数量**:本章有几个爽点?(至少 1 个)
+4. **无效内容**:是否存在与剧情无关的描写?(标注删减位置)
+5. **节奏类型**:快章/慢章/高潮章?(是否符合当前篇章定位)
+
+### 5.2 评分标准
+
+| 分数 | 信息密度 | 爽点数量 | 无效内容 | 判定 |
+|------|---------|---------|---------|------|
+| 90-100 | < 800 字/点 | ≥ 2 个 | 无 | 优秀 |
+| 70-89 | 800-1000 字/点 | 1 个 | 极少 | 良好 |
+| 50-69 | 1000-1500 字/点 | 1 个 | 有但不多 | 及格 |
+| < 50 | > 1500 字/点 | 0 个 | 大量 | 不及格,需重写 |
+
+### 5.3 典型问题标记
+
+发现以下情况时,必须在审查报告中标注:
+
+- 🔴 **严重拖沓**:连续 500 字无剧情推进
+- 🟡 **轻度灌水**:过度描写(风景/内心独白 > 300 字)
+- 🟢 **节奏优秀**:剧情点密集,爽点恰到好处
+
+---
+
+## 6. 实战案例
+
+### 案例 1:修炼突破章节
+
+**原版**(节奏拖沓):
+```
+林天盘膝坐下,开始运转功法...(500 字详细描写)
+灵气缓缓进入体内,汇聚丹田...(300 字过程描写)
+时间一分一秒过去,林天额头渗出汗珠...(200 字)
+终于,一股力量在体内爆发!(50 字)
+林天睁开眼,感觉实力大增...(150 字)
+```
+→ 总计 1200 字,只推进了 1 个剧情点(突破成功),信息密度 = 1200 字/点
+
+**优化版**(快节奏):
+```
+林天盘膝坐下,吞噬系统疯狂吸收天雷果的能量。(15 字)
+三个时辰后——(5 字)
+"轰!"体内传来一声巨响,筑基八层!(15 字)
+林天睁眼,拳头一握,空气爆鸣。(14 字)
+"这力量...我现在能秒杀筑基六层!"林天眼中闪过精光。(24 字)
+门外传来惊呼:"怎么可能!三天就突破了?"(17 字)
+```
+→ 总计 90 字,推进了 3 个剧情点(突破 + 战力评估 + 引发震惊),信息密度 = 30 字/点
+
+**对比总结**:
+- 原版:1200 字 / 1 点 = 严重拖沓
+- 优化版:90 字 / 3 点 = 节奏紧凑
+
+---
+
+## 7. 主笔 AI 执行建议
+
+### 7.1 生成前(Think 步骤)
+
+在 Think 步骤中强制规划:
+```markdown
+**节奏规划**:
+- 本章剧情点:[列出 3-5 个]
+- 本章爽点:[类型 + 位置]
+- 预计字数:3500 字
+- 信息密度目标:< 1000 字/点
+```
+
+### 7.2 生成中
+
+- 每写 500 字,检查是否推进了至少 1 个剧情点
+- 发现无效描写立即删减
+- 铺垫超过 1000 字仍未进入冲突/爽点 → 立即加速
+
+### 7.3 生成后(自查)
+
+1. 统计本章剧情点数量
+2. 计算信息密度(总字数 ÷ 剧情点)
+3. 如果 > 1000,标记待优化区域
+4. 提交给节奏审查员复核
+
+---
+
+## 8. 总结:节奏控制三原则
+
+1. **快铺垫**:500-1000 字内完成铺垫
+2. **密爽点**:每 1000 字至少 1 个剧情推进
+3. **零灌水**:与剧情无关的内容直接删除
+
+**记住**:网文读者要的是"爽",不是"美"。宁可快节奏缺少细节,也不要慢节奏拖沓注水!

+ 264 - 0
.claude/skills/webnovel-writer/references/strand-weave-pattern.md

@@ -0,0 +1,264 @@
+# 情节线织网模式(Strand Weave Pattern)
+
+> **来源**: 借鉴自 Crucible Writing System 的多线交织机制
+> **目的**: 防止网文节奏单调,确保主线/感情线/世界观均衡发展
+
+---
+
+## 核心理念
+
+200万字网文最怕"单一节奏":
+- ❌ 连续 20 章都在打架(审美疲劳)
+- ❌ 连续 15 章都在谈恋爱(主线停滞)
+- ❌ 连续 10 章都在修炼(枯燥乏味)
+
+**解决方案**: 将小说拆分为三条主线,按"频率"交织:
+
+```
+Quest (主线) ━━━━━━━━━━━━━━  占比 60%
+Fire (感情)     ━━━━━━      占比 25%
+Constellation   ━━━━        占比 15%
+(世界观)
+```
+
+---
+
+## 三条线定义
+
+### 1. Quest(主线,占比 60%)
+
+**定义**: 主角的核心任务、升级、战斗、夺宝等推进剧情的主要冲突。
+
+**典型剧情**:
+- 宗门大比
+- 秘境夺宝
+- 击败强敌
+- 突破境界
+- 复仇/打脸
+
+**示例**:
+```
+第 1-5 章: Quest(主角参加外门大比)
+第 10-15 章: Quest(主角进入血煞秘境)
+第 20-25 章: Quest(主角突破金丹期)
+```
+
+---
+
+### 2. Fire(感情线,占比 25%)
+
+**定义**: 主角与配角的情感关系发展(爱情/友情/师徒情/亲情)。
+
+**典型剧情**:
+- 与女主相识/暧昧/确认关系
+- 英雄救美
+- 情侣日常
+- 后宫争宠(如有)
+- 师徒对话/成长
+
+**示例**:
+```
+第 6 章: Fire(主角与李雪首次深度对话)
+第 12 章: Fire(主角英雄救美,好感度 +20)
+第 18 章: Fire(确认关系,接吻场景)
+```
+
+---
+
+### 3. Constellation(世界观,占比 15%)
+
+**定义**: 扩展世界观、展示新势力/新地点/新设定,为后续剧情铺垫。
+
+**典型剧情**:
+- 揭示隐藏势力(如"圣地"/"魔道")
+- 介绍新大陆/新秘境
+- 展示更高境界的强者
+- 揭秘主角身世/金手指来历
+- 埋设大伏笔(如"千年前的大战")
+
+**示例**:
+```
+第 8 章: Constellation(主角听闻"圣地"传说)
+第 16 章: Constellation(遇到金丹期强者,见识更强实力)
+第 30 章: Constellation(发现主角身世秘密)
+```
+
+---
+
+## 交织规则
+
+### 规则 1: 避免单线连续超过 5 章
+
+**错误示例**(Quest 连续 10 章):
+```
+第 1-10 章: Quest Quest Quest Quest Quest Quest Quest Quest Quest Quest
+→ 读者会感到疲劳,缺乏变化
+```
+
+**正确示例**(Quest + Fire 交织):
+```
+第 1-3 章: Quest Quest Quest
+第 4 章:   Fire(感情线插入,缓和节奏)
+第 5-7 章: Quest Quest Quest
+第 8 章:   Constellation(世界观扩展)
+第 9-10 章: Quest Quest
+```
+
+### 规则 2: Fire 不能超过 10 章不出现
+
+**警告**:
+- 距离上次感情线 > 10 章 → 读者会忘记女主
+- 建议每 5-10 章插入一次感情戏(小甜蜜/英雄救美/吃醋)
+
+### 规则 3: Constellation 不能超过 15 章不出现
+
+**警告**:
+- 距离上次世界观扩展 > 15 章 → 世界观停滞,缺乏新鲜感
+- 建议每 10-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": [                      // 历史记录(最近 20 章)
+      {"chapter": 46, "strand": "quest"},
+      {"chapter": 45, "strand": "quest"},
+      {"chapter": 44, "strand": "quest"},
+      {"chapter": 43, "strand": "fire"},
+      ...
+    ]
+  }
+}
+```
+
+---
+
+## Think 步骤中的检查逻辑
+
+在生成第 N 章前,主笔 AI 必须执行以下检查:
+
+```python
+# 伪代码
+current_chapter = 46
+last_quest = 45
+last_fire = 43
+last_constellation = 40
+current_dominant = "quest"
+chapters_since_switch = 3
+
+# 警告判断
+if chapters_since_switch >= 5:
+    ⚠️ 警告:已连续 5 章走主线,建议切换到 Fire 或 Constellation
+
+if current_chapter - last_fire > 10:
+    ⚠️ 警告:距离上次感情线 > 10 章,建议安排感情戏
+
+if current_chapter - last_constellation > 15:
+    ⚠️ 警告:距离上次世界观扩展 > 15 章,建议展示新设定
+
+# 本章决策
+本章主导线: Fire(因为距离上次感情线已 3 章,且主线已连续 3 章)
+理由: 平衡节奏,插入主角与李雪的甜蜜日常
+```
+
+---
+
+## 实战案例
+
+### 案例 1:前 30 章的织网示例
+
+```
+第 1-5 章:   Quest Quest Quest Quest Quest(开局必须快速推进主线)
+第 6 章:     Fire(首次与女主相遇)
+第 7-10 章:  Quest Quest Quest Quest(宗门大比)
+第 11 章:    Fire(英雄救美)
+第 12-14 章: Quest Quest Quest(秘境夺宝)
+第 15 章:    Constellation(揭示"圣地"存在)
+第 16-19 章: Quest Quest Quest Quest(击败强敌)
+第 20 章:    Fire(确认关系)
+第 21-24 章: Quest Quest Quest Quest(突破境界)
+第 25 章:    Constellation(发现主角身世线索)
+第 26-30 章: Quest Quest Quest Quest Quest(卷末高潮)
+```
+
+**分析**:
+- Quest 占比: 23/30 = 77%(略高于 60%,因为前期需要快速建立主线)
+- Fire 占比: 3/30 = 10%(前期感情线可以少一点)
+- Constellation 占比: 2/30 = 7%(前期世界观埋伏笔即可)
+
+### 案例 2:中期(第 200-250 章)的织网示例
+
+此时主线已稳定,需要更多感情线和世界观:
+
+```
+第 200-203 章: Quest Quest Quest Quest(主线推进)
+第 204 章:    Fire(后宫日常/吃醋戏)
+第 205-208 章: Quest Quest Quest Quest(新副本)
+第 209 章:    Fire(感情线深化)
+第 210-212 章: Quest Quest Quest(击败 BOSS)
+第 213 章:    Constellation(揭示魔道入侵预兆)
+第 214-216 章: Quest Quest Quest(收尾)
+第 217 章:    Fire(女主表白/甜蜜)
+第 218-220 章: Quest Quest Quest(下一个副本开启)
+第 221 章:    Constellation(新大陆线索)
+...
+```
+
+**分析**:
+- Quest 占比: 约 55-60%(主线稳定)
+- Fire 占比: 约 25-30%(感情线需要深化)
+- Constellation 占比: 约 15%(世界观扩展)
+
+---
+
+## 自动更新 strand_tracker
+
+每次生成章节后,系统应自动更新:
+
+```python
+# 伪代码(在 webnovel-write 完成后执行)
+if 本章主导线 == "quest":
+    state.strand_tracker.last_quest_chapter = current_chapter
+    if state.strand_tracker.current_dominant == "quest":
+        state.strand_tracker.chapters_since_switch += 1
+    else:
+        state.strand_tracker.current_dominant = "quest"
+        state.strand_tracker.chapters_since_switch = 1
+
+# 同理处理 fire 和 constellation
+
+# 记录历史
+state.strand_tracker.history.append({
+    "chapter": current_chapter,
+    "strand": 本章主导线
+})
+
+# 保持历史记录最多 20 条
+if len(state.strand_tracker.history) > 20:
+    state.strand_tracker.history.pop(0)
+```
+
+---
+
+## 总结:三大好处
+
+1. **防止节奏单调**: 强制多线交织,避免"连续 10 章打架"
+2. **保持新鲜感**: 定期切换主导线,读者不会审美疲劳
+3. **平衡发展**: 确保主线/感情线/世界观都不被遗忘
+
+**记住**: 200万字网文 = 几十个"小短篇"的嵌套,每个篇章需要节奏变化!
+
+---
+
+**集成到主笔 AI**:
+- 在 Think 步骤中强制检查 `strand_tracker`
+- 根据警告调整本章主导线
+- 生成后更新 `state.json`

+ 405 - 0
.claude/skills/webnovel-writer/scripts/backup_manager.py

@@ -0,0 +1,405 @@
+#!/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
+
+class GitBackupManager:
+    """基于 Git 的备份管理器"""
+
+    def __init__(self, project_root: str):
+        self.project_root = Path(project_root)
+        self.git_dir = self.project_root / ".git"
+
+        # 检查 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 命令"""
+        try:
+            result = subprocess.run(
+                ["git"] + args,
+                cwd=self.project_root,
+                check=check,
+                capture_output=True,
+                text=True,
+                encoding='utf-8'
+            )
+
+            return True, result.stdout
+
+        except subprocess.CalledProcessError as e:
+            return False, e.stderr
+
+    def backup(self, chapter_num: int, chapter_title: str = "") -> bool:
+        """
+        备份当前状态(Git commit + tag)
+
+        Args:
+            chapter_num: 章节号
+            chapter_title: 章节标题(可选)
+        """
+
+        print(f"📝 正在备份第 {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:
+            commit_message += f": {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()
+
+    # 创建管理器
+    manager = GitBackupManager(args.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()

+ 457 - 0
.claude/skills/webnovel-writer/scripts/context_manager.py

@@ -0,0 +1,457 @@
+#!/usr/bin/env python3
+"""
+上下文分页管理系统 (Context Manager)
+
+核心理念:200万字小说不能每次都加载全部设定,需要"滑动窗口"机制。
+
+功能:
+1. 根据当前章节的 location 和 characters,动态筛选相关设定
+2. 分层加载:核心上下文(必须)+ 场景上下文(按需)+ 全局概览(极简)
+3. Token 优化:从 50,000 Token 压缩到 3,500 Token(节省 93%)
+4. 缓存机制:避免重复计算
+
+使用方式:
+  # 为第 45 章构建上下文
+  python context_manager.py --chapter 45 --output .webnovel/context_cache.json
+
+  # 指定主角所在地点(可选,否则从 state.json 读取)
+  python context_manager.py --chapter 45 --location "血煞秘境" --output context.json
+
+  # Dry-run 模式(预览 Token 消耗)
+  python context_manager.py --chapter 45 --dry-run
+
+架构设计:
+  核心上下文(Core Context)- 必须加载
+    └── 当前章节大纲(本章目标、出场角色、爽点设计)
+    └── 主角卡(简版:姓名、境界、金手指、核心性格)
+    └── 前 2 章摘要(各 200 字)
+
+  场景上下文(Scene Context)- 按需加载
+    └── 当前地点详情(从 世界观.md 提取对应章节)
+    └── 出场角色卡(完整版,最多 5 个)
+    └── 相关伏笔(status=未回收 且 涉及当前地点/角色)
+    └── 相关物品/招式(主角当前拥有 + 本章可能用到)
+
+  全局概览(Global Overview)- 极简版
+    └── 世界观骨架(500 Token:势力关系图 + 地理框架)
+    └── 力量体系(300 Token:境界列表 + 主角当前位置)
+    └── 关键伏笔提醒(100 Token:未回收且紧急的)
+
+Token 预算分配:
+  - 核心上下文:1500 Token
+  - 场景上下文:1500 Token
+  - 全局概览:500 Token
+  - 总计:3500 Token(约 2600 字中文)
+"""
+
+import json
+import os
+import sys
+import re
+from pathlib import Path
+from typing import Dict, List, Any, Optional
+
+class ContextManager:
+    """上下文滑动窗口管理器"""
+
+    def __init__(self, project_root: str):
+        self.project_root = Path(project_root)
+        self.state_file = self.project_root / ".webnovel/state.json"
+        self.outline_dir = self.project_root / "大纲"
+        self.settings_dir = self.project_root / "设定集"
+        self.chapters_dir = self.project_root / "正文"
+
+        self.state = None
+        self.token_budget = {
+            "core": 1500,
+            "scene": 1500,
+            "global": 500
+        }
+
+    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 estimate_tokens(self, text: str) -> int:
+        """估算文本的 Token 数量(粗略:中文 1.5 字/token,英文 4 字符/token)"""
+        chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
+        english_chars = len(re.findall(r'[a-zA-Z]', text))
+
+        tokens = (chinese_chars / 1.5) + (english_chars / 4)
+        return int(tokens)
+
+    def truncate_to_tokens(self, text: str, max_tokens: int) -> str:
+        """截断文本到指定 Token 数"""
+        current_tokens = self.estimate_tokens(text)
+
+        if current_tokens <= max_tokens:
+            return text
+
+        # 按比例截断
+        ratio = max_tokens / current_tokens
+        target_length = int(len(text) * ratio * 0.95)  # 留 5% 余量
+
+        return text[:target_length] + "..."
+
+    def build_core_context(self, chapter_num: int) -> Dict[str, Any]:
+        """构建核心上下文(1500 Token)"""
+        core = {
+            "current_outline": self._get_chapter_outline(chapter_num),
+            "protagonist_brief": self._get_protagonist_brief(),
+            "recent_summaries": self._get_recent_summaries(chapter_num, count=2)
+        }
+
+        return core
+
+    def _get_chapter_outline(self, chapter_num: int) -> str:
+        """获取当前章节大纲(从详细大纲中提取)"""
+        # 查找包含该章节的卷
+        for outline_file in self.outline_dir.glob("第*卷-详细大纲.md"):
+            with open(outline_file, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            # 查找章节标题(格式:#### 第 XXX 章:标题)
+            pattern = f"#### 第 {chapter_num:03d} 章:(.+?)(?=####|$)"
+            match = re.search(pattern, content, re.DOTALL)
+
+            if match:
+                outline = match.group(0)
+                return self.truncate_to_tokens(outline, 600)  # 限制 600 Token
+
+        return f"[未找到第 {chapter_num} 章大纲,请检查详细大纲文件]"
+
+    def _get_protagonist_brief(self) -> Dict[str, Any]:
+        """获取主角卡(简版:400 Token)"""
+        if not self.state:
+            return {}
+
+        protag_state = self.state.get("protagonist_state", {})
+
+        brief = {
+            "name": protag_state.get("name", "主角"),
+            "power": protag_state.get("power", {}),
+            "location": protag_state.get("location", {}).get("current", "未知"),
+            "golden_finger": protag_state.get("golden_finger", {}).get("name", "无")
+        }
+
+        # 读取主角卡的"核心性格"章节(如果存在)
+        protag_card_file = self.settings_dir / "主角卡.md"
+        if protag_card_file.exists():
+            with open(protag_card_file, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            # 提取"性格特点"章节
+            personality_match = re.search(r'## 性格特点\n\n(.+?)(?=\n##|$)', content, re.DOTALL)
+            if personality_match:
+                personality = personality_match.group(1).strip()
+                brief["personality"] = self.truncate_to_tokens(personality, 200)
+
+        return brief
+
+    def _get_recent_summaries(self, chapter_num: int, count: int = 2) -> List[str]:
+        """获取前 N 章的摘要(每章 200 字)"""
+        summaries = []
+
+        for i in range(chapter_num - count, chapter_num):
+            if i <= 0:
+                continue
+
+            chapter_file = self.chapters_dir / f"第{i:04d}章.md"
+            if not chapter_file.exists():
+                continue
+
+            with open(chapter_file, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            # 提取正文(去除标题、元数据等)
+            text_match = re.search(r'---\n\n(.+)', content, re.DOTALL)
+            if text_match:
+                text = text_match.group(1).strip()
+            else:
+                text = content
+
+            # 生成摘要(取前 200 字)
+            summary = text[:200] + "..."
+            summaries.append(f"第 {i} 章摘要:{summary}")
+
+        return summaries
+
+    def build_scene_context(self, chapter_num: int, location: Optional[str] = None,
+                           characters: Optional[List[str]] = None) -> Dict[str, Any]:
+        """构建场景上下文(1500 Token)"""
+
+        # 确定当前地点
+        if not location and self.state:
+            location = self.state.get("protagonist_state", {}).get("location", {}).get("current")
+
+        scene = {
+            "location_details": self._get_location_details(location) if location else None,
+            "character_cards": self._get_character_cards(characters) if characters else [],
+            "relevant_foreshadowing": self._get_relevant_foreshadowing(location, characters),
+            "relevant_items": self._get_relevant_items()
+        }
+
+        return scene
+
+    def _get_location_details(self, location: str) -> str:
+        """获取地点详情(从 世界观.md 提取)"""
+        worldview_file = self.settings_dir / "世界观.md"
+
+        if not worldview_file.exists():
+            return f"[地点:{location}](详情待补充)"
+
+        with open(worldview_file, 'r', encoding='utf-8') as f:
+            content = f.read()
+
+        # 查找地点章节(格式:### 地点名)
+        pattern = f"### {re.escape(location)}\n\n(.+?)(?=\n###|$)"
+        match = re.search(pattern, content, re.DOTALL)
+
+        if match:
+            details = match.group(1).strip()
+            return self.truncate_to_tokens(details, 400)  # 限制 400 Token
+
+        return f"[地点:{location}](世界观.md 中未找到详情)"
+
+    def _get_character_cards(self, characters: List[str]) -> List[Dict[str, str]]:
+        """获取角色卡(完整版,最多 5 个,每个 200 Token)"""
+        cards = []
+
+        for char_name in characters[:5]:  # 最多 5 个
+            # 在角色库中查找
+            for category in ["主要角色", "次要角色", "反派角色"]:
+                char_file = self.settings_dir / f"角色库/{category}/{char_name}.md"
+
+                if char_file.exists():
+                    with open(char_file, 'r', encoding='utf-8') as f:
+                        content = f.read()
+
+                    # 截断到 200 Token
+                    truncated = self.truncate_to_tokens(content, 200)
+                    cards.append({
+                        "name": char_name,
+                        "content": truncated
+                    })
+                    break
+
+        return cards
+
+    def _get_relevant_foreshadowing(self, location: Optional[str],
+                                   characters: Optional[List[str]]) -> List[Dict[str, str]]:
+        """获取相关伏笔(未回收 且 涉及当前地点/角色)"""
+        if not self.state:
+            return []
+
+        all_foreshadowing = self.state.get("plot_threads", {}).get("foreshadowing", [])
+        relevant = []
+
+        for item in all_foreshadowing:
+            if item.get("status") != "未回收":
+                continue
+
+            content = item.get("content", "")
+
+            # 检查是否与当前地点/角色相关
+            is_relevant = False
+
+            if location and location in content:
+                is_relevant = True
+
+            if characters:
+                for char in characters:
+                    if char in content:
+                        is_relevant = True
+                        break
+
+            if is_relevant:
+                relevant.append(item)
+
+        return relevant[:3]  # 最多 3 条
+
+    def _get_relevant_items(self) -> List[str]:
+        """获取相关物品/招式(主角当前拥有)"""
+        if not self.state:
+            return []
+
+        # 从 state.json 的 entities 中提取主角拥有的物品
+        entities = self.state.get("entities", {})
+        items = entities.get("items", [])
+
+        # 简化:只返回物品名称列表
+        return [item.get("name") for item in items[:5]]  # 最多 5 个
+
+    def build_global_overview(self) -> Dict[str, str]:
+        """构建全局概览(500 Token)"""
+        overview = {
+            "worldview_skeleton": self._get_worldview_skeleton(),
+            "power_system_brief": self._get_power_system_brief(),
+            "urgent_foreshadowing": self._get_urgent_foreshadowing()
+        }
+
+        return overview
+
+    def _get_worldview_skeleton(self) -> str:
+        """获取世界观骨架(200 Token)"""
+        worldview_file = self.settings_dir / "世界观.md"
+
+        if not worldview_file.exists():
+            return "[世界观骨架待补充]"
+
+        with open(worldview_file, 'r', encoding='utf-8') as f:
+            content = f.read()
+
+        # 提取"势力"章节的标题列表
+        factions = re.findall(r'### (.+)', content)
+
+        skeleton = "势力:" + "、".join(factions[:10])  # 最多 10 个
+        return self.truncate_to_tokens(skeleton, 200)
+
+    def _get_power_system_brief(self) -> str:
+        """获取力量体系(200 Token)"""
+        power_file = self.settings_dir / "力量体系.md"
+
+        if not power_file.exists():
+            return "[力量体系待补充]"
+
+        with open(power_file, 'r', encoding='utf-8') as f:
+            content = f.read()
+
+        # 提取"境界划分"章节
+        realm_match = re.search(r'## 境界划分\n\n(.+?)(?=\n##|$)', content, re.DOTALL)
+
+        if realm_match:
+            realms = realm_match.group(1).strip()
+            return self.truncate_to_tokens(realms, 200)
+
+        return "[境界划分待补充]"
+
+    def _get_urgent_foreshadowing(self) -> List[str]:
+        """获取紧急伏笔(未回收 且 已埋超过 100 章)"""
+        if not self.state:
+            return []
+
+        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
+        all_foreshadowing = self.state.get("plot_threads", {}).get("foreshadowing", [])
+
+        urgent = []
+
+        for item in all_foreshadowing:
+            if item.get("status") != "未回收":
+                continue
+
+            # 计算已埋章节数(粗略:假设每章对应 1 个章节号增量)
+            # 实际项目中应该记录"埋设章节号"
+            # 这里简化:如果 added_at 距离现在超过 100 天,视为紧急
+
+            content = item.get("content", "")
+            urgent.append(f"⚠️ {content}")
+
+        return urgent[:3]  # 最多 3 条
+
+    def build_context(self, chapter_num: int, location: Optional[str] = None,
+                     characters: Optional[List[str]] = None) -> Dict[str, Any]:
+        """构建完整上下文"""
+
+        context = {
+            "chapter": chapter_num,
+            "core_context": self.build_core_context(chapter_num),
+            "scene_context": self.build_scene_context(chapter_num, location, characters),
+            "global_overview": self.build_global_overview(),
+            "metadata": {
+                "token_usage": {}
+            }
+        }
+
+        # 估算 Token 消耗
+        core_tokens = self.estimate_tokens(json.dumps(context["core_context"], ensure_ascii=False))
+        scene_tokens = self.estimate_tokens(json.dumps(context["scene_context"], ensure_ascii=False))
+        global_tokens = self.estimate_tokens(json.dumps(context["global_overview"], ensure_ascii=False))
+
+        context["metadata"]["token_usage"] = {
+            "core": core_tokens,
+            "scene": scene_tokens,
+            "global": global_tokens,
+            "total": core_tokens + scene_tokens + global_tokens
+        }
+
+        return context
+
+    def save_context(self, context: Dict[str, Any], output_file: str):
+        """保存上下文到文件"""
+        with open(output_file, 'w', encoding='utf-8') as f:
+            json.dump(context, f, ensure_ascii=False, indent=2)
+
+        print(f"✅ 上下文已保存: {output_file}")
+        print(f"\n📊 Token 使用情况:")
+        usage = context["metadata"]["token_usage"]
+        print(f"  核心上下文: {usage['core']} Token")
+        print(f"  场景上下文: {usage['scene']} Token")
+        print(f"  全局概览: {usage['global']} Token")
+        print(f"  总计: {usage['total']} Token")
+
+        # 节省百分比(相比全量加载 50,000 Token)
+        savings = (1 - usage['total'] / 50000) * 100
+        print(f"\n💰 相比全量加载节省: {savings:.1f}%")
+
+def main():
+    import argparse
+
+    parser = argparse.ArgumentParser(
+        description="上下文滑动窗口管理器",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 为第 45 章构建上下文
+  python context_manager.py --chapter 45 --output .webnovel/context_cache.json
+
+  # 指定地点和角色
+  python context_manager.py --chapter 45 --location "血煞秘境" --characters "李雪,血煞门主"
+
+  # Dry-run 模式(预览 Token 消耗)
+  python context_manager.py --chapter 45 --dry-run
+        """
+    )
+
+    parser.add_argument('--chapter', type=int, required=True, help='章节号')
+    parser.add_argument('--location', help='主角所在地点(可选)')
+    parser.add_argument('--characters', help='出场角色列表(逗号分隔)')
+    parser.add_argument('--output', default='.webnovel/context_cache.json', help='输出文件路径')
+    parser.add_argument('--project-root', default='.', help='项目根目录')
+    parser.add_argument('--dry-run', action='store_true', help='预览模式,不保存文件')
+
+    args = parser.parse_args()
+
+    # 解析角色列表
+    characters = None
+    if args.characters:
+        characters = [c.strip() for c in args.characters.split(',')]
+
+    # 创建管理器
+    manager = ContextManager(args.project_root)
+
+    # 加载状态
+    if not manager.load_state():
+        sys.exit(1)
+
+    print(f"📖 正在为第 {args.chapter} 章构建上下文...")
+
+    # 构建上下文
+    context = manager.build_context(args.chapter, args.location, characters)
+
+    # 保存或预览
+    if args.dry_run:
+        print("\n⚠️  Dry-run 模式,不保存文件")
+        print("\n📄 上下文预览:")
+        print(json.dumps(context, ensure_ascii=False, indent=2))
+    else:
+        manager.save_context(context, args.output)
+
+if __name__ == "__main__":
+    main()

+ 453 - 0
.claude/skills/webnovel-writer/scripts/extract_entities.py

@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+"""
+[NEW_ENTITY] 标签提取与同步脚本
+
+功能:
+1. 扫描指定章节正文,提取所有 [NEW_ENTITY] 标签
+2. 解析实体类型(角色/地点/物品/势力/招式)
+3. 同步到设定集对应文件
+4. 更新 state.json 中的相关记录
+5. 支持自动化模式和交互式模式
+
+使用方式:
+  python extract_entities.py <章节文件> [--auto] [--dry-run]
+
+示例:
+  python extract_entities.py ../../../正文/第0001章.md           # 交互式模式
+  python extract_entities.py ../../../正文/第0001章.md --auto    # 自动化模式
+  python extract_entities.py ../../../正文/第0001章.md --dry-run # 仅预览不写入
+"""
+
+import re
+import json
+import os
+import sys
+from pathlib import Path
+from datetime import datetime
+from typing import List, Dict, Tuple
+
+# 实体类型与目标文件映射
+ENTITY_TYPE_MAP = {
+    "角色": "设定集/角色库/{category}/{name}.md",
+    "地点": "设定集/世界观.md",  # 追加到世界观地理章节
+    "物品": "设定集/物品库/{name}.md",
+    "势力": "设定集/世界观.md",  # 追加到势力章节
+    "招式": "设定集/力量体系.md",  # 追加到招式章节
+    "其他": "设定集/其他设定/{name}.md"
+}
+
+# 角色分类规则
+ROLE_CATEGORY_MAP = {
+    "主角": "主要角色",
+    "配角": "次要角色",
+    "反派": "反派角色",
+    "路人": "次要角色"
+}
+
+def extract_new_entities(file_path: str) -> List[Dict]:
+    """
+    从章节文件中提取所有 [NEW_ENTITY] 标签
+
+    标签格式:
+      [NEW_ENTITY: 角色, 李雪, 天云宗外门弟子,主角的青梅竹马]
+      [NEW_ENTITY: 地点, 血煞秘境, 危险的试炼之地,内有金丹期凶兽]
+      [NEW_ENTITY: 物品, 天雷果, 可提升雷属性修炼速度的灵果]
+
+    Returns:
+        List[Dict]: [{"type": "角色", "name": "李雪", "desc": "...", "line": 123}, ...]
+    """
+    entities = []
+
+    with open(file_path, 'r', encoding='utf-8') as f:
+        for line_num, line in enumerate(f, 1):
+            # 匹配 [NEW_ENTITY: 类型, 名称, 描述]
+            matches = re.findall(
+                r'\[NEW_ENTITY:\s*([^,]+),\s*([^,]+),\s*([^\]]+)\]',
+                line
+            )
+
+            for match in matches:
+                entity_type = match[0].strip()
+                entity_name = match[1].strip()
+                entity_desc = match[2].strip()
+
+                entities.append({
+                    "type": entity_type,
+                    "name": entity_name,
+                    "desc": entity_desc,
+                    "line": line_num,
+                    "source_file": file_path
+                })
+
+    return entities
+
+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']}
+
+## 备注
+
+自动提取自 [NEW_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):
+    """更新 state.json 中的实体记录"""
+    with open(state_file, 'r', encoding='utf-8') as f:
+        state = json.load(f)
+
+    # 确保存在实体列表
+    if 'entities' not in state:
+        state['entities'] = {
+            "characters": [],
+            "locations": [],
+            "items": [],
+            "factions": [],
+            "techniques": []
+        }
+
+    for entity in entities:
+        entity_type = entity['type']
+
+        if entity_type == "角色":
+            if entity['name'] not in [c.get('name') for c in state['entities']['characters']]:
+                state['entities']['characters'].append({
+                    "name": entity['name'],
+                    "desc": entity['desc'],
+                    "category": categorize_character(entity['desc']),
+                    "first_appearance": entity.get('source_file', ''),
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                })
+
+        elif entity_type == "地点":
+            if entity['name'] not in [l.get('name') for l in state['entities']['locations']]:
+                state['entities']['locations'].append({
+                    "name": entity['name'],
+                    "desc": entity['desc'],
+                    "first_appearance": entity.get('source_file', ''),
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                })
+
+        elif entity_type == "物品":
+            if entity['name'] not in [i.get('name') for i in state['entities']['items']]:
+                state['entities']['items'].append({
+                    "name": entity['name'],
+                    "desc": entity['desc'],
+                    "first_appearance": entity.get('source_file', ''),
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                })
+
+        elif entity_type == "势力":
+            if entity['name'] not in [f.get('name') for f in state['entities']['factions']]:
+                state['entities']['factions'].append({
+                    "name": entity['name'],
+                    "desc": entity['desc'],
+                    "first_appearance": entity.get('source_file', ''),
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                })
+
+        elif entity_type == "招式":
+            if entity['name'] not in [t.get('name') for t in state['entities']['techniques']]:
+                state['entities']['techniques'].append({
+                    "name": entity['name'],
+                    "desc": entity['desc'],
+                    "first_appearance": entity.get('source_file', ''),
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                })
+
+    # 备份旧文件
+    backup_file = state_file.replace('.json', f'.backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json')
+    os.rename(state_file, backup_file)
+
+    # 写入新文件
+    with open(state_file, 'w', encoding='utf-8') as f:
+        json.dump(state, f, ensure_ascii=False, indent=2)
+
+    print(f"✅ 已备份旧状态文件到: {backup_file}")
+
+def sync_entity_to_settings(entity: Dict, project_root: str, auto_mode: bool = False) -> bool:
+    """
+    将实体同步到设定集
+
+    Returns:
+        bool: 是否成功同步
+    """
+    entity_type = entity['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}"
+        target_dir.mkdir(parents=True, exist_ok=True)
+
+        target_file = target_dir / f"{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) / "设定集/物品库"
+        target_dir.mkdir(parents=True, exist_ok=True)
+
+        target_file = target_dir / f"{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 章】首次出现
+
+## 备注
+
+自动提取自 [NEW_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():
+    if len(sys.argv) < 2:
+        print("用法: python extract_entities.py <章节文件> [--auto] [--dry-run]")
+        print("示例: python extract_entities.py ../../../正文/第0001章.md")
+        sys.exit(1)
+
+    chapter_file = sys.argv[1]
+    auto_mode = '--auto' in sys.argv
+    dry_run = '--dry-run' in sys.argv
+
+    if not os.path.exists(chapter_file):
+        print(f"❌ 文件不存在: {chapter_file}")
+        sys.exit(1)
+
+    # 提取实体
+    print(f"📖 正在扫描: {chapter_file}")
+    entities = extract_new_entities(chapter_file)
+
+    if not entities:
+        print("✅ 未发现 [NEW_ENTITY] 标签")
+        return
+
+    print(f"\n🔍 发现 {len(entities)} 个新实体:")
+    for i, entity in enumerate(entities, 1):
+        print(f"  {i}. [{entity['type']}] {entity['name']} - {entity['desc'][:30]}...")
+
+    if dry_run:
+        print("\n⚠️  Dry-run 模式,不执行实际写入")
+        return
+
+    # 确定项目根目录
+    project_root = Path(chapter_file).parent.parent
+    state_file = project_root / ".webnovel/state.json"
+
+    if not state_file.exists():
+        print(f"❌ 状态文件不存在: {state_file}")
+        print("请先运行 /webnovel-init 初始化项目")
+        sys.exit(1)
+
+    # 同步实体到设定集
+    print(f"\n📝 开始同步到设定集...")
+    success_count = 0
+
+    for entity in entities:
+        if sync_entity_to_settings(entity, str(project_root), auto_mode):
+            success_count += 1
+
+    # 更新 state.json
+    print(f"\n💾 更新 state.json...")
+    update_state_json(entities, str(state_file))
+
+    print(f"\n✅ 完成!成功同步 {success_count}/{len(entities)} 个实体")
+
+    if not auto_mode:
+        print("\n💡 建议:")
+        print("  1. 检查生成的角色卡/物品卡,补充详细设定")
+        print("  2. 查看 世界观.md 和 力量体系.md 的更新")
+        print("  3. 确认 .webnovel/state.json 中的实体记录")
+
+if __name__ == "__main__":
+    main()

+ 178 - 0
.claude/skills/webnovel-writer/scripts/init_project.py

@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+网文项目初始化脚本
+创建 AI 工作室所需的完整项目结构
+"""
+
+import os
+import sys
+import json
+import subprocess
+from pathlib import Path
+from datetime import datetime
+
+# 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 init_project(project_dir, title, genre):
+    """初始化项目结构"""
+
+    project_path = Path(project_dir)
+
+    # 创建目录结构
+    directories = [
+        ".webnovel/backups",
+        "设定集/角色库/主要角色",
+        "设定集/角色库/次要角色",
+        "设定集/角色库/反派角色",
+        "大纲",
+        "正文",
+        "审查报告"
+    ]
+
+    for dir_path in directories:
+        (project_path / dir_path).mkdir(parents=True, exist_ok=True)
+
+    # 创建 state.json(动态状态管理)
+    state = {
+        "project_info": {
+            "title": title,
+            "genre": genre,
+            "author": "",
+            "created_at": datetime.now().strftime("%Y-%m-%d"),
+            "target_words": 2000000,
+            "target_chapters": 600
+        },
+        "progress": {
+            "current_chapter": 0,
+            "total_words": 0,
+            "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+            "volumes_completed": [],
+            "current_volume": 1
+        },
+        "protagonist_state": {
+            "name": "",
+            "power": {
+                "realm": "",
+                "layer": 1,
+                "bottleneck": ""
+            },
+            "location": {
+                "current": "",
+                "last_chapter": 0
+            },
+            "golden_finger": {
+                "name": "",
+                "level": 1,
+                "cooldown": 0
+            }
+        },
+        "relationships": {},
+        "world_settings": {
+            "power_system": [],
+            "factions": [],
+            "locations": []
+        },
+        "plot_threads": {
+            "active_threads": [],
+            "foreshadowing": []
+        },
+        "strand_tracker": {
+            "last_quest_chapter": 0,
+            "last_fire_chapter": 0,
+            "last_constellation_chapter": 0,
+            "current_dominant": "quest",
+            "chapters_since_switch": 0,
+            "history": []
+        },
+        "review_checkpoints": []
+    }
+
+    with open(project_path / ".webnovel/state.json", "w", encoding="utf-8") as f:
+        json.dump(state, f, ensure_ascii=False, indent=2)
+
+    # Git 初始化(自动版本控制)
+    print("\n🔧 正在初始化 Git 仓库...")
+    try:
+        # Step 1: git init
+        subprocess.run(
+            ["git", "init"],
+            cwd=project_path,
+            check=True,
+            capture_output=True,
+            text=True
+        )
+
+        # Step 2: 创建 .gitignore
+        gitignore_file = project_path / ".gitignore"
+        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
+""")
+
+        # Step 3: git add .
+        subprocess.run(
+            ["git", "add", "."],
+            cwd=project_path,
+            check=True,
+            capture_output=True
+        )
+
+        # Step 4: Initial commit
+        subprocess.run(
+            ["git", "commit", "-m", "Initial commit: Project initialized"],
+            cwd=project_path,
+            check=True,
+            capture_output=True
+        )
+
+        print("✅ Git 仓库已初始化(原子性版本控制已启用)")
+
+    except subprocess.CalledProcessError as e:
+        print(f"⚠️  Git 初始化失败(非致命): {e.stderr if hasattr(e, 'stderr') else str(e)}")
+        print("💡 提示:可以稍后手动执行 git init")
+    except FileNotFoundError:
+        print("⚠️  未检测到 Git(请安装 Git 以启用版本控制功能)")
+
+    print(f"\n✅ 项目初始化完成:{project_path}")
+    print(f"📁 项目目录已创建")
+    print(f"💾 状态文件已保存:.webnovel/state.json")
+    print(f"🔖 Git 版本控制已启用")
+    print(f"\n📚 题材:{genre}")
+    print(f"📖 标题:{title}")
+    print(f"\n🎯 下一步:")
+    print(f"   1. 编辑设定集中的文件")
+    print(f"   2. 运行 /webnovel-plan 1 规划第一卷")
+    print(f"   3. 运行 /webnovel-write 1 开始创作")
+    print(f"\n💾 版本控制提示:")
+    print(f"   - 每章完成后自动 Git 备份")
+    print(f"   - 回滚命令:python backup_manager.py --rollback N")
+    print(f"   - 查看历史:python backup_manager.py --list")
+
+if __name__ == "__main__":
+    import sys
+    if len(sys.argv) < 4:
+        print("用法: python init_project.py <项目目录> <标题> <题材>")
+        print("示例: python init_project.py ./my-novel '废柴崛起' '修仙'")
+        sys.exit(1)
+
+    init_project(sys.argv[1], sys.argv[2], sys.argv[3])

+ 530 - 0
.claude/skills/webnovel-writer/scripts/status_reporter.py

@@ -0,0 +1,530 @@
+#!/usr/bin/env python3
+"""
+可视化状态报告系统 (Status Reporter)
+
+核心理念:面对 1000 个章节,作者会迷失。需要"宏观俯瞰"能力。
+
+功能:
+1. 角色活跃度分析:哪些角色太久没出场(掉线统计)
+2. 伏笔深度分析:哪些坑挖得太久了(超过 20 万字未收)
+3. 爽点节奏分布:全书高潮点的分布频率(热力图)
+4. 字数分布统计:各卷、各篇的字数分布
+5. 人际关系图谱:好感度/仇恨度趋势
+
+输出格式:
+  - 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
+
+报告示例:
+  # 全书健康报告
+
+  ## 📊 基本数据
+
+  - **总章节数**: 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
+
+class StatusReporter:
+    """状态报告生成器"""
+
+    def __init__(self, project_root: str):
+        self.project_root = Path(project_root)
+        self.state_file = self.project_root / ".webnovel/state.json"
+        self.chapters_dir = self.project_root / "正文"
+
+        self.state = None
+        self.chapters_data = []
+
+    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
+
+        for chapter_file in sorted(self.chapters_dir.glob("第*.md")):
+            # 提取章节号
+            match = re.search(r'第(\d+)章', chapter_file.name)
+            if not match:
+                continue
+
+            chapter_num = int(match.group(1))
+
+            # 读取章节内容
+            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())
+
+            # 提取出场角色(粗略:查找 [角色: XXX])
+            characters = re.findall(r'\[角色:\s*([^\]]+)\]', content)
+
+            self.chapters_data.append({
+                "chapter": chapter_num,
+                "file": chapter_file,
+                "word_count": word_count,
+                "characters": characters
+            })
+
+    def analyze_characters(self) -> Dict:
+        """分析角色活跃度"""
+        if not self.state:
+            return {}
+
+        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
+        entities = self.state.get("entities", {})
+        characters = entities.get("characters", [])
+
+        # 统计每个角色的最后出场章节
+        character_activity = {}
+
+        for char in characters:
+            char_name = char.get("name")
+            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 < 30:
+            return "🟢 正常"
+        elif absence < 100:
+            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:
+            if item.get("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 < 50:
+            return "🟢 正常"
+        elif elapsed < 150:
+            return "🟡 轻度超时"
+        else:
+            return "🔴 严重超时"
+
+    def analyze_pacing(self) -> List[Dict]:
+        """分析爽点节奏分布(每 100 章为一段)"""
+        segment_size = 100
+        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 < 1000:
+            return "优秀"
+        elif words_per_point < 1500:
+            return "良好"
+        elif words_per_point < 2000:
+            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"]
+
+        for char_name, rel_data in relationships.items():
+            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", "pacing"]:
+            report_lines.extend(self._generate_pacing_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_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', 'pacing', 'relationships'],
+                       default='all', help='分析焦点')
+    parser.add_argument('--project-root', default='.', help='项目根目录')
+
+    args = parser.parse_args()
+
+    # 创建报告生成器
+    reporter = StatusReporter(args.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)
+    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()

+ 475 - 0
.claude/skills/webnovel-writer/scripts/update_state.py

@@ -0,0 +1,475 @@
+#!/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 --relationship-status "李雪" "确认关系"
+
+  # 记录伏笔
+  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
+
+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 的基本结构"""
+        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
+
+        # 验证嵌套结构
+        if "power" not in state["protagonist_state"]:
+            print(f"❌ 缺少 protagonist_state.power 字段")
+            return False
+
+        if "location" not in state["protagonist_state"]:
+            print(f"❌ 缺少 protagonist_state.location 字段")
+            return False
+
+        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"
+        backup_dir.mkdir(exist_ok=True)
+
+        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:
+            with open(self.state_file, 'w', encoding='utf-8') as f:
+                json.dump(self.state, f, ensure_ascii=False, indent=2)
+
+            print(f"✅ 已保存: {self.state_file}")
+            return True
+
+        except Exception as e:
+            print(f"❌ 保存失败: {e}")
+            if self.backup_file and os.path.exists(self.backup_file):
+                print(f"🔄 正在回滚到备份文件...")
+                shutil.copy2(self.backup_file, self.state_file)
+                print(f"✅ 已回滚")
+            return False
+
+    def update_protagonist_power(self, realm: str, layer: int, bottleneck: str):
+        """更新主角实力"""
+        self.state["protagonist_state"]["power"] = {
+            "realm": realm,
+            "layer": layer,
+            "bottleneck": bottleneck
+        }
+        print(f"📝 更新主角实力: {realm} {layer}层, 瓶颈: {bottleneck}")
+
+    def update_protagonist_location(self, location: str, chapter: int):
+        """更新主角位置"""
+        self.state["protagonist_state"]["location"] = {
+            "current": location,
+            "last_chapter": chapter
+        }
+        print(f"📝 更新主角位置: {location}(第{chapter}章)")
+
+    def update_golden_finger(self, name: str, level: int, cooldown: int):
+        """更新金手指状态"""
+        self.state["protagonist_state"]["golden_finger"] = {
+            "name": name,
+            "level": level,
+            "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
+
+        self.state["plot_threads"]["foreshadowing"].append({
+            "content": content,
+            "status": status,
+            "added_at": datetime.now().strftime("%Y-%m-%d")
+        })
+        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 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(
+        '--state-file',
+        default='.webnovel/state.json',
+        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='添加审查记录(章节范围 报告文件)'
+    )
+
+    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
+    ]):
+        parser.print_help()
+        sys.exit(1)
+
+    # 创建更新器
+    updater = StateUpdater(args.state_file, 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)
+
+        # 保存更新
+        if not updater.save():
+            sys.exit(1)
+
+        print("\n✅ 更新完成!")
+
+        if not args.dry_run:
+            print(f"\n💡 提示:")
+            print(f"  - 原文件已备份: {updater.backup_file}")
+            print(f"  - 如需回滚,可复制备份文件到 {args.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()

+ 64 - 0
.claude/skills/webnovel-writer/templates/genres/修仙.md

@@ -0,0 +1,64 @@
+# 修仙(系统流)题材模板
+
+## 世界观框架
+
+### 力量体系
+```
+练气期(1-9 层)→ 筑基期 → 金丹期 → 元婴期 → 化神期
+→ 合体期 → 大乘期 → 渡劫期 → 仙人
+```
+
+### 典型金手指
+- **系统流**: 签到系统/吞噬系统/抽奖系统
+- **重生流**: 带着前世记忆重生
+- **老爷爷**: 戒指老爷爷传承
+- **特殊体质**: 圣体/神体/废体逆袭
+
+### 常见势力架构
+```
+家族 → 宗门 → 圣地 → 仙域
+├─ 小家族(主角起点)
+├─ 大宗门(中期舞台)
+└─ 圣地/仙域(后期舞台)
+```
+
+## 标准大纲结构(200 万字)
+
+### 第 1 卷:新手村篇(1-100 章,30 万字)
+- **第 1-30 章**: 觉醒(废柴→获得系统→初步崛起)
+- **第 31-70 章**: 崛起(家族内斗→打脸族人→小有名气)
+- **第 71-100 章**: 离开(家族被灭→仇恨种下→加入宗门)
+
+### 第 2 卷:宗门风云(101-250 章,45 万字)
+- 外门弟子→内门弟子→真传弟子
+
+### 第 3 卷:秘境历练(251-400 章,45 万字)
+- 各种秘境/试炼/夺宝
+
+### 第 4-8 卷:...(依此类推)
+
+## 爽点套路库
+
+**修仙特色爽点**:
+1. **炼丹装逼**: 炼出极品丹药震惊全场
+2. **越级挑战**: 筑基期战胜金丹期
+3. **收徒**: 路边捡到天才少女
+4. **拍卖会**: 竞价夺宝/身份曝光
+5. **宗门大比**: 黑马逆袭
+
+## 主角模板
+
+```yaml
+姓名: [待定]
+起点: 家族废柴/被退婚/资质低下
+金手指: [系统类/重生类/体质类]
+性格: 隐忍+果断+护短
+目标: 复仇/成仙/保护所爱之人
+```
+
+## 配角模板
+
+- **青梅竹马**: 温柔善良,死忠主角
+- **反派大少**: 狂妄自大,看不起主角
+- **神秘师尊**: 实力深不可测
+- **霸道女帝**: 冰冷外表,内心柔软