Răsfoiți Sursa

chore: 清理安全修复备份文件 + 添加 .gitignore

🧹 清理冗余备份文件
- 删除 6 个 *.backup_20260102 文件
- Git 版本历史已提供完整备份,无需物理备份文件

📋 新增 .gitignore
- 忽略 *.backup_* 和 *.bak 文件(防止未来误提交)
- 忽略 __pycache__/ 和 *.pyc(Python 编译缓存)
- 忽略测试临时文件(test-security-project/)
- 保留重要的 .webnovel/ 运行时数据

✅ 代码库清洁度提升
- 移除 5,460 行冗余备份代码
- 保留所有安全修复功能代码
- 符合 Git 最佳实践(版本控制代替物理备份)

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

Co-Authored-By: Claude <noreply@anthropic.com>
lingfengQAQ 5 luni în urmă
părinte
comite
ba066eb222

+ 0 - 469
.claude/skills/webnovel-writer/scripts/archive_manager.py.backup_20260102

@@ -1,469 +0,0 @@
-#!/usr/bin/env python3
-"""
-state.json 数据归档管理脚本
-
-目标:防止 state.json 无限增长,确保 200 万字长跑稳定运行
-
-功能:
-1. 智能归档长期未使用的数据(角色/伏笔/审查报告)
-2. 自动触发条件检测(文件大小/章节数)
-3. 安全备份与恢复机制
-4. 归档数据可随时恢复
-
-归档策略:
-- 角色:超过 50 章未出场的次要角色 → archive/characters.json
-- 伏笔:status="已回收" 且超过 20 章的伏笔 → archive/plot_threads.json
-- 审查报告:超过 50 章的旧报告 → archive/reviews.json
-
-使用方式:
-  # 自动归档检查(推荐在 update_state.py 之后调用)
-  python archive_manager.py --auto-check
-
-  # 强制归档(忽略触发条件)
-  python archive_manager.py --force
-
-  # 恢复特定角色
-  python archive_manager.py --restore-character "李雪"
-
-  # 查看归档统计
-  python archive_manager.py --stats
-
-  # Dry-run 模式(仅显示将被归档的数据)
-  python archive_manager.py --auto-check --dry-run
-"""
-
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from pathlib import Path
-
-# Windows UTF-8 编码修复
-if sys.platform == 'win32':
-    import io
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-
-class ArchiveManager:
-    """state.json 数据归档管理器"""
-
-    def __init__(self, project_root=None):
-        if project_root is None:
-            # 默认使用当前目录
-            project_root = Path.cwd()
-        else:
-            project_root = Path(project_root)
-
-        self.state_file = project_root / ".webnovel" / "state.json"
-        self.archive_dir = project_root / ".webnovel" / "archive"
-
-        # 确保归档目录存在
-        self.archive_dir.mkdir(parents=True, exist_ok=True)
-
-        # 归档文件路径
-        self.characters_archive = self.archive_dir / "characters.json"
-        self.plot_threads_archive = self.archive_dir / "plot_threads.json"
-        self.reviews_archive = self.archive_dir / "reviews.json"
-
-        # 归档规则配置
-        self.config = {
-            "character_inactive_threshold": 50,  # 角色超过 50 章未出场视为不活跃
-            "plot_resolved_threshold": 20,       # 已回收伏笔超过 20 章后归档
-            "review_old_threshold": 20,          # 审查报告超过 20 章后归档(从 50 降至 20)
-            "file_size_trigger_mb": 0.5,         # state.json 超过 0.5MB 触发归档(从 1.0 降至 0.5)
-            "chapter_trigger": 10                # 每 10 章检查一次
-        }
-
-    def load_state(self):
-        """加载 state.json"""
-        if not self.state_file.exists():
-            print(f"❌ state.json 不存在: {self.state_file}")
-            sys.exit(1)
-
-        with open(self.state_file, 'r', encoding='utf-8') as f:
-            return json.load(f)
-
-    def save_state(self, state):
-        """保存 state.json(带备份)"""
-        # 备份原文件
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        backup_file = self.state_file.parent / f"state.backup_{timestamp}.json"
-
-        if self.state_file.exists():
-            import shutil
-            shutil.copy2(self.state_file, backup_file)
-
-        # 写入新文件
-        with open(self.state_file, 'w', encoding='utf-8') as f:
-            json.dump(state, f, ensure_ascii=False, indent=2)
-
-        print(f"✅ state.json 已更新(备份: {backup_file.name})")
-
-    def load_archive(self, archive_file):
-        """加载归档文件"""
-        if not archive_file.exists():
-            return []
-
-        with open(archive_file, 'r', encoding='utf-8') as f:
-            return json.load(f)
-
-    def save_archive(self, archive_file, data):
-        """保存归档文件"""
-        with open(archive_file, 'w', encoding='utf-8') as f:
-            json.dump(data, f, ensure_ascii=False, indent=2)
-
-    def check_trigger_conditions(self, state):
-        """检查是否需要触发归档"""
-        current_chapter = state.get("progress", {}).get("current_chapter", 0)
-
-        # 条件 1: 文件大小超过阈值
-        file_size_mb = self.state_file.stat().st_size / (1024 * 1024)
-        size_trigger = file_size_mb >= self.config["file_size_trigger_mb"]
-
-        # 条件 2: 章节数是触发间隔的倍数
-        chapter_trigger = (current_chapter % self.config["chapter_trigger"]) == 0 and current_chapter > 0
-
-        return {
-            "should_archive": size_trigger or chapter_trigger,
-            "file_size_mb": file_size_mb,
-            "current_chapter": current_chapter,
-            "size_trigger": size_trigger,
-            "chapter_trigger": chapter_trigger
-        }
-
-    def identify_inactive_characters(self, state):
-        """识别不活跃的次要角色"""
-        current_chapter = state.get("progress", {}).get("current_chapter", 0)
-        characters = state.get("entities", {}).get("characters", [])
-        threshold = self.config["character_inactive_threshold"]
-
-        inactive = []
-        for char in characters:
-            # 只归档次要角色(importance="minor")
-            if char.get("importance") != "minor":
-                continue
-
-            # 检查最后出场章节
-            last_appearance = char.get("last_appearance_chapter", 0)
-            inactive_chapters = current_chapter - last_appearance
-
-            if inactive_chapters >= threshold:
-                inactive.append({
-                    "character": char,
-                    "inactive_chapters": inactive_chapters,
-                    "last_appearance": last_appearance
-                })
-
-        return inactive
-
-    def identify_resolved_plot_threads(self, state):
-        """识别可归档的已回收伏笔"""
-        current_chapter = state.get("progress", {}).get("current_chapter", 0)
-        plot_threads = state.get("plot_threads", {}).get("active", [])
-        resolved = state.get("plot_threads", {}).get("resolved", [])
-        threshold = self.config["plot_resolved_threshold"]
-
-        archivable = []
-        for thread in resolved:
-            resolved_chapter = thread.get("resolved_chapter", 0)
-            chapters_since_resolved = current_chapter - resolved_chapter
-
-            if chapters_since_resolved >= threshold:
-                archivable.append({
-                    "thread": thread,
-                    "chapters_since_resolved": chapters_since_resolved,
-                    "resolved_chapter": resolved_chapter
-                })
-
-        return archivable
-
-    def identify_old_reviews(self, state):
-        """识别可归档的旧审查报告"""
-        current_chapter = state.get("progress", {}).get("current_chapter", 0)
-        reviews = state.get("review_checkpoints", [])
-        threshold = self.config["review_old_threshold"]
-
-        old_reviews = []
-        for review in reviews:
-            review_chapter = review.get("chapter_range", [0, 0])[1]  # 取结束章节
-            chapters_since_review = current_chapter - review_chapter
-
-            if chapters_since_review >= threshold:
-                old_reviews.append({
-                    "review": review,
-                    "chapters_since_review": chapters_since_review,
-                    "review_chapter": review_chapter
-                })
-
-        return old_reviews
-
-    def archive_characters(self, inactive_list, dry_run=False):
-        """归档不活跃角色(Priority 2 修复:与索引集成)"""
-        if not inactive_list:
-            return 0
-
-        # 加载现有归档
-        archived = self.load_archive(self.characters_archive)
-
-        # 添加时间戳
-        timestamp = datetime.now().isoformat()
-        for item in inactive_list:
-            item["character"]["archived_at"] = timestamp
-            archived.append(item["character"])
-
-            # ✅ Priority 2 修复:同步更新索引状态(而非删除)
-            if not dry_run:
-                try:
-                    # 导入索引模块
-                    import sys
-                    from pathlib import Path
-                    script_dir = Path(__file__).parent
-                    sys.path.insert(0, str(script_dir))
-                    from structured_index import StructuredIndex
-
-                    # 更新索引状态为 'archived'
-                    project_root = self.state_file.parent.parent
-                    index = StructuredIndex(str(project_root))
-                    index.mark_character_archived(item["character"]["name"], timestamp)
-                except Exception as e:
-                    # 索引更新失败不影响归档流程
-                    print(f"⚠️ 索引状态更新失败(不影响归档): {e}")
-
-        if not dry_run:
-            self.save_archive(self.characters_archive, archived)
-
-        return len(inactive_list)
-
-    def archive_plot_threads(self, resolved_list, dry_run=False):
-        """归档已回收伏笔"""
-        if not resolved_list:
-            return 0
-
-        # 加载现有归档
-        archived = self.load_archive(self.plot_threads_archive)
-
-        # 添加时间戳
-        timestamp = datetime.now().isoformat()
-        for item in resolved_list:
-            item["thread"]["archived_at"] = timestamp
-            archived.append(item["thread"])
-
-        if not dry_run:
-            self.save_archive(self.plot_threads_archive, archived)
-
-        return len(resolved_list)
-
-    def archive_reviews(self, old_reviews_list, dry_run=False):
-        """归档旧审查报告"""
-        if not old_reviews_list:
-            return 0
-
-        # 加载现有归档
-        archived = self.load_archive(self.reviews_archive)
-
-        # 添加时间戳
-        timestamp = datetime.now().isoformat()
-        for item in old_reviews_list:
-            item["review"]["archived_at"] = timestamp
-            archived.append(item["review"])
-
-        if not dry_run:
-            self.save_archive(self.reviews_archive, archived)
-
-        return len(old_reviews_list)
-
-    def remove_from_state(self, state, inactive_chars, resolved_threads, old_reviews):
-        """从 state.json 中移除已归档的数据"""
-        # 移除不活跃角色
-        if inactive_chars:
-            char_names = {item["character"]["name"] for item in inactive_chars}
-            state["entities"]["characters"] = [
-                char for char in state["entities"]["characters"]
-                if char["name"] not in char_names
-            ]
-
-        # 移除已归档的伏笔
-        if resolved_threads:
-            thread_ids = {item["thread"]["description"] for item in resolved_threads}
-            state["plot_threads"]["resolved"] = [
-                thread for thread in state["plot_threads"]["resolved"]
-                if thread["description"] not in thread_ids
-            ]
-
-        # 移除旧审查报告
-        if old_reviews:
-            review_dates = {item["review"]["date"] for item in old_reviews}
-            state["review_checkpoints"] = [
-                review for review in state["review_checkpoints"]
-                if review["date"] not in review_dates
-            ]
-
-        return state
-
-    def run_auto_check(self, force=False, dry_run=False):
-        """自动归档检查"""
-        state = self.load_state()
-
-        # 检查触发条件
-        trigger = self.check_trigger_conditions(state)
-
-        if not force and not trigger["should_archive"]:
-            print("✅ 无需归档(触发条件未满足)")
-            print(f"   文件大小: {trigger['file_size_mb']:.2f} MB (阈值: {self.config['file_size_trigger_mb']} MB)")
-            print(f"   当前章节: {trigger['current_chapter']} (每 {self.config['chapter_trigger']} 章触发)")
-            return
-
-        print("🔍 开始归档检查...")
-        print(f"   文件大小: {trigger['file_size_mb']:.2f} MB")
-        print(f"   当前章节: {trigger['current_chapter']}")
-
-        # 识别可归档数据
-        inactive_chars = self.identify_inactive_characters(state)
-        resolved_threads = self.identify_resolved_plot_threads(state)
-        old_reviews = self.identify_old_reviews(state)
-
-        # 输出统计
-        print(f"\n📊 归档统计:")
-        print(f"   不活跃角色: {len(inactive_chars)}")
-        print(f"   已回收伏笔: {len(resolved_threads)}")
-        print(f"   旧审查报告: {len(old_reviews)}")
-
-        if not (inactive_chars or resolved_threads or old_reviews):
-            print("\n✅ 无需归档(无符合条件的数据)")
-            return
-
-        # Dry-run 模式
-        if dry_run:
-            print("\n🔍 [Dry-run] 将被归档的数据:")
-            if inactive_chars:
-                print("\n   不活跃角色:")
-                for item in inactive_chars[:5]:  # 只显示前 5 个
-                    print(f"   - {item['character']['name']} (超过 {item['inactive_chapters']} 章未出场)")
-            if resolved_threads:
-                print("\n   已回收伏笔:")
-                for item in resolved_threads[:5]:
-                    print(f"   - {item['thread']['description'][:30]}... (已回收 {item['chapters_since_resolved']} 章)")
-            if old_reviews:
-                print("\n   旧审查报告:")
-                for item in old_reviews[:5]:
-                    print(f"   - Ch{item['review_chapter']} ({item['chapters_since_review']} 章前)")
-            return
-
-        # 执行归档
-        chars_archived = self.archive_characters(inactive_chars, dry_run=dry_run)
-        threads_archived = self.archive_plot_threads(resolved_threads, dry_run=dry_run)
-        reviews_archived = self.archive_reviews(old_reviews, dry_run=dry_run)
-
-        # 从 state.json 中移除
-        state = self.remove_from_state(state, inactive_chars, resolved_threads, old_reviews)
-        self.save_state(state)
-
-        # 最终统计
-        print(f"\n✅ 归档完成:")
-        print(f"   角色归档: {chars_archived} → {self.characters_archive.name}")
-        print(f"   伏笔归档: {threads_archived} → {self.plot_threads_archive.name}")
-        print(f"   报告归档: {reviews_archived} → {self.reviews_archive.name}")
-
-        # 显示归档后的文件大小
-        new_size_mb = self.state_file.stat().st_size / (1024 * 1024)
-        saved_mb = trigger["file_size_mb"] - new_size_mb
-        print(f"\n💾 文件大小: {trigger['file_size_mb']:.2f} MB → {new_size_mb:.2f} MB (节省 {saved_mb:.2f} MB)")
-
-    def restore_character(self, name):
-        """恢复归档的角色(Priority 2 修复:同步恢复索引状态)"""
-        archived = self.load_archive(self.characters_archive)
-        state = self.load_state()
-
-        # 查找角色
-        char_to_restore = None
-        for char in archived:
-            if char["name"] == name:
-                char_to_restore = char
-                break
-
-        if not char_to_restore:
-            print(f"❌ 归档中未找到角色: {name}")
-            return
-
-        # 移除 archived_at 字段
-        char_to_restore.pop("archived_at", None)
-
-        # ✅ 原子性修复:先从归档中移除,再添加到 state.json
-        # 理由:即使崩溃,数据仍在归档中,可重新恢复,不会丢失或重复
-        archived = [char for char in archived if char["name"] != name]
-        self.save_archive(self.characters_archive, archived)
-
-        # 恢复到 state.json
-        state["entities"]["characters"].append(char_to_restore)
-        self.save_state(state)
-
-        # ✅ Priority 2 修复:同步恢复索引状态为 'active'
-        try:
-            import sys
-            from pathlib import Path
-            script_dir = Path(__file__).parent
-            sys.path.insert(0, str(script_dir))
-            from structured_index import StructuredIndex
-
-            project_root = self.state_file.parent.parent
-            index = StructuredIndex(str(project_root))
-            index.mark_character_active(name)
-        except Exception as e:
-            print(f"⚠️ 索引状态恢复失败(不影响数据恢复): {e}")
-
-        print(f"✅ 角色已恢复: {name}")
-
-    def show_stats(self):
-        """显示归档统计"""
-        chars = self.load_archive(self.characters_archive)
-        threads = self.load_archive(self.plot_threads_archive)
-        reviews = self.load_archive(self.reviews_archive)
-
-        print("📊 归档统计:")
-        print(f"   角色归档: {len(chars)}")
-        print(f"   伏笔归档: {len(threads)}")
-        print(f"   报告归档: {len(reviews)}")
-
-        # 计算归档文件大小
-        total_size = 0
-        for archive_file in [self.characters_archive, self.plot_threads_archive, self.reviews_archive]:
-            if archive_file.exists():
-                total_size += archive_file.stat().st_size
-
-        print(f"   归档大小: {total_size / 1024:.2f} KB")
-
-        # 显示 state.json 大小
-        state_size_mb = self.state_file.stat().st_size / (1024 * 1024)
-        print(f"\n💾 state.json 当前大小: {state_size_mb:.2f} MB")
-
-
-def main():
-    parser = argparse.ArgumentParser(description="state.json 数据归档管理")
-
-    parser.add_argument("--auto-check", action="store_true", help="自动归档检查")
-    parser.add_argument("--force", action="store_true", help="强制归档(忽略触发条件)")
-    parser.add_argument("--dry-run", action="store_true", help="Dry-run 模式(仅显示将被归档的数据)")
-    parser.add_argument("--restore-character", metavar="NAME", help="恢复归档的角色")
-    parser.add_argument("--stats", action="store_true", help="显示归档统计")
-    parser.add_argument("--project-root", metavar="PATH", help="项目根目录(默认为当前目录)")
-
-    args = parser.parse_args()
-
-    # 创建管理器
-    manager = ArchiveManager(project_root=args.project_root)
-
-    # 执行操作
-    if args.auto_check or args.force:
-        manager.run_auto_check(force=args.force, dry_run=args.dry_run)
-    elif args.restore_character:
-        manager.restore_character(args.restore_character)
-    elif args.stats:
-        manager.show_stats()
-    else:
-        parser.print_help()
-
-
-if __name__ == "__main__":
-    main()

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

@@ -1,411 +0,0 @@
-#!/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
-
-# Windows 编码兼容性修复
-if sys.platform == 'win32':
-    import io
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-class GitBackupManager:
-    """基于 Git 的备份管理器"""
-
-    def __init__(self, project_root: str):
-        self.project_root = Path(project_root)
-        self.git_dir = self.project_root / ".git"
-
-        # 检查 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()

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

@@ -1,460 +0,0 @@
-#!/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
-
-# Windows 编码兼容性修复
-if sys.platform == 'win32':
-    import io
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-# 实体类型与目标文件映射
-ENTITY_TYPE_MAP = {
-    "角色": "设定集/角色库/{category}/{name}.md",
-    "地点": "设定集/世界观.md",  # 追加到世界观地理章节
-    "物品": "设定集/物品库/{name}.md",
-    "势力": "设定集/世界观.md",  # 追加到势力章节
-    "招式": "设定集/力量体系.md",  # 追加到招式章节
-    "其他": "设定集/其他设定/{name}.md"
-}
-
-# 角色分类规则
-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()

+ 0 - 792
.claude/skills/webnovel-writer/scripts/structured_index.py.backup_20260102

@@ -1,792 +0,0 @@
-#!/usr/bin/env python3
-"""
-结构化索引系统(Structured Index System)
-
-目标:取代向量化检索,使用 SQLite 提供精确、快速的结构化查询
-
-核心功能:
-1. 章节元数据索引(location, characters, word_count)
-2. 伏笔追踪索引(status, urgency calculation)
-3. 文件 Hash 自愈机制(auto-rebuild on change)
-
-性能目标:
-- 查询速度:2-5ms(vs 文件遍历 500ms,提升 250x)
-- 索引构建:10ms/章(增量更新)
-- 存储开销:200 章 ≈ 100 KB
-
-使用方式:
-  # 更新单章索引
-  python structured_index.py --update-chapter 7 --metadata "正文/第0007章.md"
-
-  # 批量重建索引(历史章节)
-  python structured_index.py --rebuild-index
-
-  # 查询地点相关章节
-  python structured_index.py --query-location "血煞秘境"
-
-  # 查询紧急伏笔
-  python structured_index.py --query-urgent-foreshadowing
-
-  # 模糊查询角色
-  python structured_index.py --fuzzy-search "姓李" "女弟子"
-
-  # 导出关系图
-  python structured_index.py --export-graph > relationships.md
-"""
-
-import json
-import os
-import sys
-import argparse
-import sqlite3
-import hashlib
-import re
-from datetime import datetime
-from pathlib import Path
-from typing import Optional, List, Dict, Tuple
-
-
-class StructuredIndex:
-    """结构化索引管理器(取代向量化检索)"""
-
-    def __init__(self, project_root=None):
-        if project_root is None:
-            project_root = Path.cwd()
-        else:
-            project_root = Path(project_root)
-
-        self.project_root = project_root
-        self.state_file = project_root / ".webnovel" / "state.json"
-        self.chapters_dir = project_root / "正文"
-        self.index_db = project_root / ".webnovel" / "index.db"
-
-        # 确保数据库目录存在
-        self.index_db.parent.mkdir(parents=True, exist_ok=True)
-
-        # 连接数据库
-        self.conn = sqlite3.connect(str(self.index_db))
-        self.conn.row_factory = sqlite3.Row  # 返回字典式行
-
-        # 创建表结构
-        self._create_tables()
-
-    def _create_tables(self):
-        """创建索引表结构"""
-
-        # 1. 章节元数据表
-        self.conn.execute("""
-            CREATE TABLE IF NOT EXISTS chapters (
-                chapter_num INTEGER PRIMARY KEY,
-                title TEXT,
-                location TEXT,
-                characters TEXT,  -- JSON: ["李雪", "主角"]
-                word_count INTEGER,
-                content_hash TEXT,
-                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-            )
-        """)
-
-        # 地点索引(加速查询)
-        self.conn.execute("""
-            CREATE INDEX IF NOT EXISTS idx_location
-            ON chapters(location)
-        """)
-
-        # 2. 伏笔追踪表
-        self.conn.execute("""
-            CREATE TABLE IF NOT EXISTS foreshadowing_index (
-                id INTEGER PRIMARY KEY,
-                content TEXT,
-                location TEXT,
-                characters TEXT,  -- JSON: ["李雪", "主角"]
-                introduced_chapter INTEGER,
-                resolved_chapter INTEGER,
-                status TEXT,  -- '未回收' / '已回收'
-                urgency INTEGER DEFAULT 0,  -- 0-100,自动计算
-                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-            )
-        """)
-
-        # 状态索引
-        self.conn.execute("""
-            CREATE INDEX IF NOT EXISTS idx_status
-            ON foreshadowing_index(status)
-        """)
-
-        # 紧急度索引
-        self.conn.execute("""
-            CREATE INDEX IF NOT EXISTS idx_urgency
-            ON foreshadowing_index(urgency)
-        """)
-
-        # 3. 角色关系表
-        self.conn.execute("""
-            CREATE TABLE IF NOT EXISTS relationships (
-                id INTEGER PRIMARY KEY AUTOINCREMENT,
-                char1 TEXT,
-                char2 TEXT,
-                relation_type TEXT,  -- 'ally', 'enemy', 'romance', 'mentor', 'debtor'
-                intensity INTEGER,    -- 关系强度 0-100
-                description TEXT,
-                last_update_chapter INTEGER,
-                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                UNIQUE(char1, char2, relation_type)  -- 防止重复
-            )
-        """)
-
-        # 关系索引
-        self.conn.execute("""
-            CREATE INDEX IF NOT EXISTS idx_char1_char2
-            ON relationships(char1, char2)
-        """)
-
-        # 4. 角色索引表(优化模糊搜索性能)
-        self.conn.execute("""
-            CREATE TABLE IF NOT EXISTS characters (
-                name TEXT PRIMARY KEY,
-                description TEXT,
-                personality TEXT,
-                importance TEXT,  -- 'major' / 'minor'
-                power_level TEXT,
-                first_appearance INTEGER,
-                last_appearance INTEGER,
-                status TEXT DEFAULT 'active',  -- 'active' / 'archived'
-                archived_at TEXT,  -- ISO timestamp
-                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-            )
-        """)
-
-        # 角色名索引(加速模糊搜索)
-        self.conn.execute("""
-            CREATE INDEX IF NOT EXISTS idx_character_name
-            ON characters(name)
-        """)
-
-        # 状态索引
-        self.conn.execute("""
-            CREATE INDEX IF NOT EXISTS idx_character_status
-            ON characters(status)
-        """)
-
-        self.conn.commit()
-
-    # ================== 核心功能 1:章节元数据索引 ==================
-
-    def index_chapter(self, chapter_num: int, metadata: Dict):
-        """为新章节建立索引(在 webnovel-write Step 4.6 调用)
-
-        Args:
-            chapter_num: 章节编号
-            metadata: {
-                'title': '章节标题',
-                'location': '地点',
-                'characters': ['李雪', '主角'],
-                'word_count': 3500,
-                'hash': 'md5_hash'
-            }
-        """
-        self.conn.execute("""
-            INSERT OR REPLACE INTO chapters
-            (chapter_num, title, location, characters, word_count, content_hash, updated_at)
-            VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
-        """, (
-            chapter_num,
-            metadata['title'],
-            metadata['location'],
-            json.dumps(metadata['characters'], ensure_ascii=False),
-            metadata['word_count'],
-            metadata['hash']
-        ))
-
-        self.conn.commit()
-        print(f"✅ 章节索引已更新:Ch{chapter_num} - {metadata['title']}")
-
-    def query_chapters_by_location(self, location: str, limit: int = 10) -> List[Tuple]:
-        """O(log n) 查询:返回该地点的最近 N 章
-
-        Args:
-            location: 地点名称
-            limit: 返回数量
-
-        Returns:
-            [(chapter_num, title, characters), ...]
-        """
-        cursor = self.conn.execute("""
-            SELECT chapter_num, title, characters
-            FROM chapters
-            WHERE location = ?
-            ORDER BY chapter_num DESC
-            LIMIT ?
-        """, (location, limit))
-
-        return cursor.fetchall()
-
-    def calculate_chapter_hash(self, chapter_file: Path) -> str:
-        """计算章节文件 MD5 Hash(用于自愈机制)"""
-        if not chapter_file.exists():
-            return ""
-
-        with open(chapter_file, 'rb') as f:
-            return hashlib.md5(f.read()).hexdigest()
-
-    def get_stored_hash(self, chapter_num: int) -> Optional[str]:
-        """从索引中读取存储的 Hash"""
-        cursor = self.conn.execute("""
-            SELECT content_hash FROM chapters WHERE chapter_num = ?
-        """, (chapter_num,))
-
-        row = cursor.fetchone()
-        return row['content_hash'] if row else None
-
-    def validate_and_rebuild_if_needed(self, chapter_num: int):
-        """校验章节 Hash,不一致则自动重建索引(Self-Healing Index)
-
-        触发时机:
-        - context_manager.py 查询章节前调用
-        - 增加耗时:~5ms(Hash 计算 + 对比)
-        - 仅当检测到变更时才重建(增量成本)
-        """
-        chapter_file = self.chapters_dir / f"第{chapter_num:04d}章.md"
-
-        if not chapter_file.exists():
-            return  # 文件不存在,跳过
-
-        # 计算当前文件 Hash
-        current_hash = self.calculate_chapter_hash(chapter_file)
-
-        # 从索引中读取存储的 Hash
-        stored_hash = self.get_stored_hash(chapter_num)
-
-        if current_hash != stored_hash:
-            print(f"⚠️ 检测到 Ch{chapter_num} 已修改,自动重建索引...")
-            self._rebuild_chapter_index(chapter_num, chapter_file)
-            print(f"✅ Ch{chapter_num} 索引已更新")
-
-    def _rebuild_chapter_index(self, chapter_num: int, chapter_file: Path):
-        """重建单章索引(自动提取元数据)"""
-
-        # 读取章节内容
-        with open(chapter_file, 'r', encoding='utf-8') as f:
-            content = f.read()
-
-        # 提取元数据
-        metadata = self._extract_metadata_from_content(content, chapter_num)
-
-        # 重建索引
-        self.index_chapter(chapter_num, metadata)
-
-    def _extract_metadata_from_content(self, content: str, chapter_num: int) -> Dict:
-        """从章节内容中提取元数据"""
-
-        # 提取标题(第一行)
-        lines = content.split('\n')
-        title = lines[0].strip('# ').strip() if lines else f"第{chapter_num}章"
-
-        # 提取地点(在章节开头查找,通常格式为 **地点:XXX**)
-        location_match = re.search(r'\*\*地点[::]\s*(.+?)\*\*', content)
-        location = location_match.group(1).strip() if location_match else "未知"
-
-        # 提取角色(查找所有对话和描述中的角色名)
-        # 简化实现:从 state.json 读取已知角色,匹配出现频率
-        characters = self._extract_characters_from_content(content)
-
-        # 计算字数
-        word_count = len(content)
-
-        # 计算 Hash
-        content_hash = hashlib.md5(content.encode('utf-8')).hexdigest()
-
-        return {
-            'title': title,
-            'location': location,
-            'characters': characters[:5],  # 最多 5 个主要角色
-            'word_count': word_count,
-            'hash': content_hash
-        }
-
-    def _extract_characters_from_content(self, content: str) -> List[str]:
-        """从内容中提取角色(简化实现:读取 state.json 已知角色)"""
-
-        if not self.state_file.exists():
-            return []
-
-        # 读取 state.json
-        with open(self.state_file, 'r', encoding='utf-8') as f:
-            state = json.load(f)
-
-        # 获取已知角色列表
-        known_characters = [
-            char['name']
-            for char in state.get('entities', {}).get('characters', [])
-        ]
-
-        # 统计每个角色在内容中的出现次数
-        char_counts = {}
-        for char_name in known_characters:
-            count = content.count(char_name)
-            if count > 0:
-                char_counts[char_name] = count
-
-        # 按出现次数排序,返回前 5 个
-        sorted_chars = sorted(char_counts.items(), key=lambda x: x[1], reverse=True)
-        return [char for char, _ in sorted_chars[:5]]
-
-    # ================== 核心功能 2:伏笔追踪索引 ==================
-
-    def sync_foreshadowing_from_state(self):
-        """从 state.json 同步伏笔数据到索引
-
-        触发时机:
-        - update_state.py 更新伏笔后调用
-        - --rebuild-index 批量重建时调用
-        """
-        if not self.state_file.exists():
-            print("❌ state.json 不存在,跳过伏笔同步")
-            return
-
-        # 读取 state.json
-        with open(self.state_file, 'r', encoding='utf-8') as f:
-            state = json.load(f)
-
-        current_chapter = state.get('progress', {}).get('current_chapter', 0)
-
-        # 同步活跃伏笔(未回收)
-        active_plots = state.get('plot_threads', {}).get('active', [])
-        for plot in active_plots:
-            self._index_foreshadowing(plot, current_chapter, status="未回收")
-
-        # 同步已回收伏笔
-        resolved_plots = state.get('plot_threads', {}).get('resolved', [])
-        for plot in resolved_plots:
-            self._index_foreshadowing(plot, current_chapter, status="已回收")
-
-        self.conn.commit()
-        print(f"✅ 伏笔索引已同步:{len(active_plots)} 条活跃 + {len(resolved_plots)} 条已回收")
-
-    def _index_foreshadowing(self, plot: Dict, current_chapter: int, status: str):
-        """为单个伏笔建立索引"""
-
-        # 计算紧急度
-        urgency = self._calculate_urgency(plot, current_chapter)
-
-        # 提取地点和角色(如果有)
-        location = plot.get('location', '')
-        characters = plot.get('characters', [])
-
-        self.conn.execute("""
-            INSERT OR REPLACE INTO foreshadowing_index
-            (id, content, location, characters, introduced_chapter, resolved_chapter, status, urgency, updated_at)
-            VALUES ((SELECT id FROM foreshadowing_index WHERE content = ?), ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
-        """, (
-            plot['description'],  # 用于查重
-            plot['description'],
-            location,
-            json.dumps(characters, ensure_ascii=False),
-            plot.get('introduced_chapter', 0),
-            plot.get('resolved_chapter', None),
-            status,
-            urgency
-        ))
-
-    def _calculate_urgency(self, plot: Dict, current_chapter: int) -> int:
-        """计算伏笔紧急度(0-100)
-
-        规则:
-        - 超过 100 章未回收 → 极度紧急(100)
-        - 超过 50 章未回收 → 中等紧急(60)
-        - 其他 → 正常(20)
-        """
-        introduced_ch = plot.get('introduced_chapter', 0)
-        chapters_pending = current_chapter - introduced_ch
-
-        if chapters_pending > 100:
-            return 100  # 极度紧急
-        elif chapters_pending > 50:
-            return 60   # 中等紧急
-        else:
-            return 20   # 正常
-
-    def sync_characters_from_state(self):
-        """从 state.json 同步角色数据到索引(优化模糊搜索性能)
-
-        触发时机:
-        - update_state.py 更新角色后调用
-        - --rebuild-index 批量重建时调用
-        """
-        if not self.state_file.exists():
-            print("❌ state.json 不存在,跳过角色同步")
-            return
-
-        # 读取 state.json
-        with open(self.state_file, 'r', encoding='utf-8') as f:
-            state = json.load(f)
-
-        characters = state.get('entities', {}).get('characters', [])
-
-        for char in characters:
-            self._index_character(char, status='active')
-
-        self.conn.commit()
-        print(f"✅ 角色索引已同步:{len(characters)} 个角色")
-
-    def _index_character(self, char: Dict, status: str = 'active'):
-        """为单个角色建立索引"""
-        self.conn.execute("""
-            INSERT OR REPLACE INTO characters
-            (name, description, personality, importance, power_level,
-             first_appearance, last_appearance, status, updated_at)
-            VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
-        """, (
-            char.get('name', ''),
-            char.get('description', ''),
-            char.get('personality', ''),
-            char.get('importance', 'minor'),
-            char.get('power_level', ''),
-            char.get('first_appearance_chapter', 0),
-            char.get('last_appearance_chapter', 0),
-            status
-        ))
-
-    def mark_character_archived(self, name: str, archived_at: str = None):
-        """标记角色为已归档状态(Priority 2 修复)
-
-        Args:
-            name: 角色名
-            archived_at: 归档时间戳(ISO格式),默认当前时间
-        """
-        if archived_at is None:
-            from datetime import datetime
-            archived_at = datetime.now().isoformat()
-
-        self.conn.execute("""
-            UPDATE characters
-            SET status = 'archived', archived_at = ?, updated_at = CURRENT_TIMESTAMP
-            WHERE name = ?
-        """, (archived_at, name))
-        self.conn.commit()
-
-    def mark_character_active(self, name: str):
-        """恢复角色为活跃状态(与 mark_character_archived 对应)"""
-        self.conn.execute("""
-            UPDATE characters
-            SET status = 'active', archived_at = NULL, updated_at = CURRENT_TIMESTAMP
-            WHERE name = ?
-        """, (name,))
-        self.conn.commit()
-
-    def query_urgent_foreshadowing(self, threshold: int = 60) -> List[Dict]:
-        """查询紧急伏笔(urgency >= threshold)
-
-        Args:
-            threshold: 紧急度阈值(60=中等紧急,80=高度紧急,100=极度紧急)
-
-        Returns:
-            [{'content': '...', 'introduced_chapter': 45, 'urgency': 80}, ...]
-        """
-        cursor = self.conn.execute("""
-            SELECT content, introduced_chapter, urgency
-            FROM foreshadowing_index
-            WHERE status = '未回收' AND urgency >= ?
-            ORDER BY urgency DESC
-        """, (threshold,))
-
-        return [dict(row) for row in cursor.fetchall()]
-
-    # ================== 核心功能 3:模糊查询(Fuzzy Search via SQL LIKE)==================
-
-    def fuzzy_search_character(self, keywords: List[str]) -> List[Dict]:
-        """模糊查询角色(支持多关键词)- O(log n) SQL查询
-
-        Args:
-            keywords: 关键词列表,如 ["李", "女弟子"]
-
-        Returns:
-            [{'name': '李雪', 'description': '...', 'last_appearance_chapter': 45, 'status': 'active'}, ...]
-
-        示例:
-            fuzzy_search_character(["李", "女弟子"])
-            → 返回所有名字或描述包含"李"和"女弟子"的角色
-
-        性能:
-            - 旧版:O(n) 遍历 state.json 所有角色(210个角色 = ~500ms)
-            - 新版:O(log n) SQL 索引查询(~10ms)
-        """
-        # 构建 WHERE 子句(每个关键词都必须匹配)
-        conditions = []
-        params = []
-
-        for kw in keywords:
-            # 每个关键词在 name/description/personality 任一字段中出现即可
-            conditions.append("(name LIKE ? OR description LIKE ? OR personality LIKE ?)")
-            params.extend([f'%{kw}%', f'%{kw}%', f'%{kw}%'])
-
-        # AND 连接所有关键词条件(所有关键词都必须匹配)
-        where_clause = " AND ".join(conditions)
-
-        # 执行 SQL 查询
-        query = f"""
-            SELECT name, description, personality, importance, power_level,
-                   first_appearance, last_appearance, status
-            FROM characters
-            WHERE {where_clause}
-            ORDER BY
-                status ASC,  -- 活跃角色优先
-                last_appearance DESC  -- 最近出场优先
-            LIMIT 10
-        """
-
-        cursor = self.conn.execute(query, params)
-        rows = cursor.fetchall()
-
-        # 转换为字典列表
-        matched = []
-        for row in rows:
-            matched.append({
-                'name': row[0],
-                'description': row[1],
-                'personality': row[2],
-                'importance': row[3],
-                'power_level': row[4],
-                'first_appearance_chapter': row[5],
-                'last_appearance_chapter': row[6],
-                'status': row[7]  # 'active' / 'archived'
-            })
-
-        return matched
-
-    # ================== 批量操作 ==================
-
-    def rebuild_all_indexes(self):
-        """批量重建所有历史章节的索引
-
-        使用场景:
-        - 索引系统首次上线
-        - 索引数据库损坏
-        """
-        if not self.chapters_dir.exists():
-            print("❌ 章节目录不存在")
-            return
-
-        # 获取所有章节文件
-        chapter_files = sorted(self.chapters_dir.glob("第*.md"))
-
-        print(f"🔍 发现 {len(chapter_files)} 个章节文件,开始重建索引...")
-
-        for chapter_file in chapter_files:
-            # 提取章节编号
-            match = re.search(r'第(\d+)章', chapter_file.name)
-            if not match:
-                continue
-
-            chapter_num = int(match.group(1))
-
-            # 重建索引
-            self._rebuild_chapter_index(chapter_num, chapter_file)
-
-        # 同步伏笔索引
-        self.sync_foreshadowing_from_state()
-
-        print(f"✅ 批量重建完成:{len(chapter_files)} 章")
-
-    # ================== 查询与统计 ==================
-
-    def get_index_stats(self) -> Dict:
-        """获取索引统计信息"""
-
-        # 章节统计
-        cursor = self.conn.execute("SELECT COUNT(*) as count FROM chapters")
-        chapter_count = cursor.fetchone()['count']
-
-        # 伏笔统计
-        cursor = self.conn.execute("""
-            SELECT status, COUNT(*) as count
-            FROM foreshadowing_index
-            GROUP BY status
-        """)
-        foreshadowing_stats = {row['status']: row['count'] for row in cursor.fetchall()}
-
-        # 关系统计
-        cursor = self.conn.execute("SELECT COUNT(*) as count FROM relationships")
-        relationship_count = cursor.fetchone()['count']
-
-        # 数据库大小
-        db_size_kb = self.index_db.stat().st_size / 1024
-
-        return {
-            'chapter_count': chapter_count,
-            'foreshadowing_active': foreshadowing_stats.get('未回收', 0),
-            'foreshadowing_resolved': foreshadowing_stats.get('已回收', 0),
-            'relationship_count': relationship_count,
-            'db_size_kb': round(db_size_kb, 2)
-        }
-
-    def __del__(self):
-        """析构函数:关闭数据库连接"""
-        if hasattr(self, 'conn'):
-            self.conn.close()
-
-
-def main():
-    parser = argparse.ArgumentParser(description="结构化索引系统(取代向量化检索)")
-
-    # 更新操作
-    parser.add_argument("--update-chapter", type=int, metavar="NUM", help="更新单章索引")
-    parser.add_argument("--metadata", metavar="PATH", help="章节文件路径(配合 --update-chapter)")
-    parser.add_argument("--metadata-json", metavar="JSON", help="元数据 JSON 字符串(配合 --update-chapter,由 metadata-extractor agent 提供)")
-    parser.add_argument("--metadata-file", metavar="FILE", help="元数据 JSON 文件路径(配合 --update-chapter,Windows 推荐使用此参数)")
-
-    # 批量操作
-    parser.add_argument("--rebuild-index", action="store_true", help="批量重建所有索引")
-
-    # 查询操作
-    parser.add_argument("--query-location", metavar="LOCATION", help="查询地点相关章节")
-    parser.add_argument("--query-urgent-foreshadowing", action="store_true", help="查询紧急伏笔")
-    parser.add_argument("--fuzzy-search", nargs='+', metavar="KEYWORD", help="模糊查询角色(多个关键词)")
-
-    # 统计信息
-    parser.add_argument("--stats", action="store_true", help="显示索引统计信息")
-
-    # 项目路径
-    parser.add_argument("--project-root", metavar="PATH", help="项目根目录(默认为当前目录)")
-
-    args = parser.parse_args()
-
-    # 创建索引管理器
-    index = StructuredIndex(project_root=args.project_root)
-
-    # 执行操作
-    if args.update_chapter:
-        # 模式1:从 JSON 文件读取(Windows 推荐,避免 CLI 引号转义问题)
-        if args.metadata_file:
-            try:
-                metadata_file = Path(args.metadata_file)
-                if not metadata_file.exists():
-                    print(f"❌ 元数据文件不存在: {metadata_file}")
-                    return
-
-                with open(metadata_file, 'r', encoding='utf-8') as f:
-                    metadata = json.load(f)
-
-                # 验证必需字段
-                required_fields = ['title', 'location', 'characters', 'word_count', 'hash']
-                missing_fields = [f for f in required_fields if f not in metadata]
-
-                if missing_fields:
-                    print(f"❌ JSON 缺少必需字段: {', '.join(missing_fields)}")
-                    return
-
-                # 更新索引
-                index.index_chapter(args.update_chapter, metadata)
-
-                # 同步伏笔索引
-                index.sync_foreshadowing_from_state()
-
-            except json.JSONDecodeError as e:
-                print(f"❌ JSON 解析失败: {e}")
-                return
-
-        # 模式2:直接接收 JSON 字符串(Linux/macOS,或测试时使用)
-        elif args.metadata_json:
-            try:
-                metadata = json.loads(args.metadata_json)
-
-                # 验证必需字段
-                required_fields = ['title', 'location', 'characters', 'word_count', 'hash']
-                missing_fields = [f for f in required_fields if f not in metadata]
-
-                if missing_fields:
-                    print(f"❌ JSON 缺少必需字段: {', '.join(missing_fields)}")
-                    return
-
-                # 更新索引
-                index.index_chapter(args.update_chapter, metadata)
-
-                # 同步伏笔索引
-                index.sync_foreshadowing_from_state()
-
-            except json.JSONDecodeError as e:
-                print(f"❌ JSON 解析失败: {e}")
-                return
-
-        # 模式3:从章节文件提取元数据(旧模式,保持向后兼容)
-        elif args.metadata:
-            # 读取章节文件
-            chapter_file = Path(args.metadata)
-            if not chapter_file.exists():
-                print(f"❌ 章节文件不存在: {chapter_file}")
-                return
-
-            # 提取元数据
-            with open(chapter_file, 'r', encoding='utf-8') as f:
-                content = f.read()
-
-            metadata = index._extract_metadata_from_content(content, args.update_chapter)
-
-            # 更新索引
-            index.index_chapter(args.update_chapter, metadata)
-
-            # 同步伏笔索引
-            index.sync_foreshadowing_from_state()
-
-        else:
-            print("❌ 缺少参数:--metadata-file (推荐) / --metadata-json / --metadata")
-            return
-
-    elif args.rebuild_index:
-        index.rebuild_all_indexes()
-
-    elif args.query_location:
-        results = index.query_chapters_by_location(args.query_location)
-
-        if not results:
-            print(f"未找到地点相关章节: {args.query_location}")
-        else:
-            print(f"找到 {len(results)} 个相关章节:")
-            for chapter_num, title, characters in results:
-                print(f"  Ch{chapter_num}: {title} - 角色: {characters}")
-
-    elif args.query_urgent_foreshadowing:
-        results = index.query_urgent_foreshadowing(threshold=60)
-
-        if not results:
-            print("✅ 无紧急伏笔")
-        else:
-            print(f"⚠️ 检测到 {len(results)} 条紧急伏笔:")
-            for item in results:
-                print(f"  - {item['content'][:30]}...(第 {item['introduced_chapter']} 章埋设,紧急度 {item['urgency']}/100)")
-
-    elif args.fuzzy_search:
-        results = index.fuzzy_search_character(args.fuzzy_search)
-
-        if not results:
-            print(f"未找到匹配角色: {' + '.join(args.fuzzy_search)}")
-        else:
-            print(f"找到 {len(results)} 个匹配角色:")
-            for i, char in enumerate(results, 1):
-                print(f"{i}. {char['name']} - {char['description'][:50]}...(最后出场:Ch {char['last_appearance_chapter']})")
-
-    elif args.stats:
-        stats = index.get_index_stats()
-
-        print("📊 索引统计信息:")
-        print(f"   章节索引: {stats['chapter_count']}")
-        print(f"   伏笔索引: {stats['foreshadowing_active']} 条活跃 + {stats['foreshadowing_resolved']} 条已回收")
-        print(f"   关系索引: {stats['relationship_count']}")
-        print(f"   数据库大小: {stats['db_size_kb']} KB")
-
-    else:
-        parser.print_help()
-
-
-if __name__ == "__main__":
-    # Windows UTF-8 编码修复(仅在脚本直接运行时)
-    if sys.platform == 'win32':
-        import io
-        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-    main()

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

@@ -1,544 +0,0 @@
-#!/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
-
-# Windows 编码兼容性修复
-if sys.platform == 'win32':
-    import io
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-class StateUpdater:
-    """state.json 安全更新器"""
-
-    def __init__(self, state_file: str, dry_run: bool = False):
-        self.state_file = state_file
-        self.dry_run = dry_run
-        self.backup_file = None
-        self.state = None
-
-    def _validate_schema(self, state: Dict) -> bool:
-        """验证 state.json 的基本结构"""
-        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 update_strand_tracker(self, strand: str, chapter: int):
-        """更新主导情节线(Strand Weave系统)"""
-        # 验证 strand 参数
-        valid_strands = ["quest", "fire", "constellation"]
-        if strand.lower() not in valid_strands:
-            print(f"❌ 无效的情节线类型: {strand}(有效值: quest, fire, constellation)")
-            return False
-
-        strand = strand.lower()
-
-        # 初始化 strand_tracker(如果不存在)
-        if "strand_tracker" not in self.state:
-            self.state["strand_tracker"] = {
-                "last_quest_chapter": 0,
-                "last_fire_chapter": 0,
-                "last_constellation_chapter": 0,
-                "current_dominant": None,
-                "chapters_since_switch": 0,
-                "history": []
-            }
-
-        tracker = self.state["strand_tracker"]
-
-        # 更新对应 strand 的最后章节
-        tracker[f"last_{strand}_chapter"] = chapter
-
-        # 判断是否切换 strand
-        if tracker.get("current_dominant") != strand:
-            tracker["current_dominant"] = strand
-            tracker["chapters_since_switch"] = 1
-        else:
-            tracker["chapters_since_switch"] += 1
-
-        # 添加到历史记录
-        tracker["history"].append({
-            "chapter": chapter,
-            "dominant": strand
-        })
-
-        # 只保留最近50章的历史(避免文件过大)
-        if len(tracker["history"]) > 50:
-            tracker["history"] = tracker["history"][-50:]
-
-        print(f"✅ strand_tracker 已更新")
-        print(f"   - 第{chapter}章主导情节线: {strand}")
-        print(f"   - 该情节线已连续{tracker['chapters_since_switch']}章")
-
-        return True
-
-def main():
-    parser = argparse.ArgumentParser(
-        description="安全更新 state.json",
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog="""
-示例:
-  # 更新主角实力
-  python update_state.py --protagonist-power "金丹" 3 "雷劫"
-
-  # 更新人际关系
-  python update_state.py --relationship "李雪" affection 95
-
-  # 添加伏笔
-  python update_state.py --add-foreshadowing "神秘玉佩的秘密" "未回收"
-
-  # 回收伏笔
-  python update_state.py --resolve-foreshadowing "天雷果的下落" 45
-
-  # 更新进度
-  python update_state.py --progress 45 198765
-
-  # 标记卷已规划
-  python update_state.py --volume-planned 1 --chapters-range "1-100"
-
-  # 组合更新(原子性)
-  python update_state.py \
-    --protagonist-power "金丹" 3 "雷劫" \
-    --progress 45 198765 \
-    --relationship "李雪" affection 95
-        """
-    )
-
-    parser.add_argument(
-        '--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='添加审查记录(章节范围 报告文件)'
-    )
-
-    # Strand Tracker 更新
-    parser.add_argument(
-        '--strand-dominant',
-        nargs=2,
-        metavar=('STRAND', 'CHAPTER'),
-        help='更新主导情节线(quest/fire/constellation 章节号)'
-    )
-
-    args = parser.parse_args()
-
-    # 如果没有任何更新参数,显示帮助并退出
-    if not any([
-        args.protagonist_power,
-        args.protagonist_location,
-        args.golden_finger,
-        args.relationship,
-        args.add_foreshadowing,
-        args.resolve_foreshadowing,
-        args.progress,
-        args.volume_planned,
-        args.add_review,
-        args.strand_dominant
-    ]):
-        parser.print_help()
-        sys.exit(1)
-
-    # 创建更新器
-    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)
-
-        # Strand Tracker 更新
-        if args.strand_dominant:
-            strand, chapter = args.strand_dominant
-            updater.update_strand_tracker(strand, int(chapter))
-
-        # 保存更新
-        if not updater.save():
-            sys.exit(1)
-
-        print("\n✅ 更新完成!")
-
-        if not args.dry_run:
-            print(f"\n💡 提示:")
-            print(f"  - 原文件已备份: {updater.backup_file}")
-            print(f"  - 如需回滚,可复制备份文件到 {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()

+ 0 - 449
.claude/skills/webnovel-writer/scripts/workflow_manager.py.backup_20260102

@@ -1,449 +0,0 @@
-#!/usr/bin/env python3
-"""
-工作流状态管理器
-- 追踪命令执行状态
-- 检测中断点
-- 提供恢复策略
-"""
-
-import json
-import os
-import sys
-import subprocess
-from datetime import datetime
-from pathlib import Path
-
-# UTF-8 编码修复(Windows兼容)
-if sys.platform == 'win32':
-    import io
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-WORKFLOW_STATE_FILE = '.webnovel/workflow_state.json'
-
-def start_task(command, args):
-    """开始新任务"""
-    state = load_state()
-    state['current_task'] = {
-        'command': command,
-        'args': args,
-        'started_at': datetime.now().isoformat(),
-        'last_heartbeat': datetime.now().isoformat(),
-        'status': 'running',
-        'current_step': None,
-        'completed_steps': [],
-        'pending_steps': get_pending_steps(command),
-        'artifacts': {
-            'chapter_file': {},
-            'git_status': {},
-            'state_json_modified': False,
-            'entities_extracted': False,
-            'review_completed': False
-        }
-    }
-    save_state(state)
-    print(f"✅ 任务已启动: {command} {json.dumps(args, ensure_ascii=False)}")
-
-def start_step(step_id, step_name, progress_note=None):
-    """标记Step开始"""
-    state = load_state()
-    if not state.get('current_task'):
-        print("⚠️ 无活动任务,请先使用 start-task")
-        return
-
-    state['current_task']['current_step'] = {
-        'id': step_id,
-        'name': step_name,
-        'status': 'in_progress',
-        'started_at': datetime.now().isoformat(),
-        'progress_note': progress_note
-    }
-    state['current_task']['last_heartbeat'] = datetime.now().isoformat()
-    save_state(state)
-    print(f"▶️  {step_id} 开始: {step_name}")
-
-def complete_step(step_id, artifacts_json=None):
-    """标记Step完成"""
-    state = load_state()
-    if not state.get('current_task') or not state['current_task'].get('current_step'):
-        print("⚠️ 无活动Step")
-        return
-
-    current_step = state['current_task']['current_step']
-    current_step['status'] = 'completed'
-    current_step['completed_at'] = datetime.now().isoformat()
-
-    if artifacts_json:
-        try:
-            artifacts = json.loads(artifacts_json)
-            current_step['artifacts'] = artifacts
-            # 更新task级别的artifacts
-            state['current_task']['artifacts'].update(artifacts)
-        except json.JSONDecodeError as e:
-            print(f"⚠️ Artifacts JSON解析失败: {e}")
-
-    state['current_task']['completed_steps'].append(current_step)
-    state['current_task']['current_step'] = None
-    state['current_task']['last_heartbeat'] = datetime.now().isoformat()
-    save_state(state)
-    print(f"✅ {step_id} 完成")
-
-def complete_task(final_artifacts_json=None):
-    """标记任务完成"""
-    state = load_state()
-    if not state.get('current_task'):
-        print("⚠️ 无活动任务")
-        return
-
-    state['current_task']['status'] = 'completed'
-    state['current_task']['completed_at'] = datetime.now().isoformat()
-
-    if final_artifacts_json:
-        try:
-            final_artifacts = json.loads(final_artifacts_json)
-            state['current_task']['artifacts'].update(final_artifacts)
-        except json.JSONDecodeError as e:
-            print(f"⚠️ Final artifacts JSON解析失败: {e}")
-
-    # 保存到历史记录
-    state['last_stable_state'] = extract_stable_state(state['current_task'])
-    if 'history' not in state:
-        state['history'] = []
-    state['history'].append({
-        'task_id': f"task_{len(state['history']) + 1:03d}",
-        'command': state['current_task']['command'],
-        'chapter': state['current_task']['args'].get('chapter_num'),
-        'status': 'completed',
-        'completed_at': state['current_task']['completed_at']
-    })
-
-    # 清除当前任务
-    state['current_task'] = None
-    save_state(state)
-    print(f"🎉 任务完成")
-
-def detect_interruption():
-    """检测中断状态"""
-    state = load_state()
-    if not state or 'current_task' not in state or state['current_task'] is None:
-        return None  # 无中断任务
-
-    task = state['current_task']
-    if task['status'] == 'completed':
-        return None  # 任务已完成
-
-    # 判断中断原因
-    last_heartbeat = datetime.fromisoformat(task['last_heartbeat'])
-    elapsed = (datetime.now() - last_heartbeat).total_seconds()
-
-    interrupt_info = {
-        'command': task['command'],
-        'args': task['args'],
-        'current_step': task['current_step'],
-        'completed_steps': task['completed_steps'],
-        'elapsed_seconds': elapsed,
-        'artifacts': task['artifacts'],
-        'started_at': task['started_at']
-    }
-
-    return interrupt_info
-
-def analyze_recovery_options(interrupt_info):
-    """分析恢复选项(基于中断点)"""
-    current_step = interrupt_info['current_step']
-    command = interrupt_info['command']
-    chapter_num = interrupt_info['args'].get('chapter_num', '?')
-
-    if not current_step:
-        # 任务刚开始就中断
-        return [{
-            'option': 'A',
-            'label': '从头开始',
-            'risk': 'low',
-            'description': '重新执行完整流程',
-            'actions': [
-                f"删除 workflow_state.json 当前任务",
-                f"执行 /{command} {chapter_num}"
-            ]
-        }]
-
-    step_id = current_step['id']
-
-    # 基于Step ID的恢复策略
-    if step_id == 'Step 1':
-        # Step 1中断:无副作用
-        return [{
-            'option': 'A',
-            'label': '从Step 1重新开始',
-            'risk': 'low',
-            'description': '重新加载上下文',
-            'actions': [
-                f"清理中断状态",
-                f"执行 /{command} {chapter_num}"
-            ]
-        }]
-
-    elif step_id == 'Step 2':
-        # Step 2中断:可能有半成品文件
-        chapter_file = interrupt_info['artifacts'].get('chapter_file', {})
-        chapter_path = f"正文/第{chapter_num:04d}章.md"
-
-        options = [{
-            'option': 'A',
-            'label': '删除半成品,从Step 1重新开始',
-            'risk': 'low',
-            'description': f"清理 {chapter_path},重新生成章节",
-            'actions': [
-                f"删除 {chapter_path}(如存在)",
-                f"清理 Git 暂存区",
-                f"清理中断状态",
-                f"执行 /{command} {chapter_num}"
-            ]
-        }]
-
-        # 检查文件是否存在
-        if os.path.exists(chapter_path):
-            options.append({
-                'option': 'B',
-                'label': '回滚到上一章',
-                'risk': 'medium',
-                'description': '丢弃所有当前章节进度',
-                'actions': [
-                    f"git reset --hard ch{(chapter_num-1):04d}",
-                    f"清理中断状态",
-                    "重新决定是否继续Ch{chapter_num}"
-                ]
-            })
-
-        return options
-
-    elif step_id in ['Step 3', 'Step 6']:
-        # Step 3/6中断:脚本未执行完
-        return [{
-            'option': 'A',
-            'label': f'从{step_id}重新开始',
-            'risk': 'low',
-            'description': '重新运行脚本(幂等操作)',
-            'actions': [
-                f"重新执行脚本",
-                f"继续后续Step"
-            ]
-        }]
-
-    elif step_id == 'Step 4':
-        # Step 4中断:state.json可能部分更新
-        return [
-            {
-                'option': 'A',
-                'label': '检查并修复state.json',
-                'risk': 'medium',
-                'description': '验证state.json一致性,补全缺失字段',
-                'actions': [
-                    "读取 state.json",
-                    "检查必要字段(progress, protagonist_state等)",
-                    "如缺失则从前一章推断",
-                    "重新执行 update_state.py",
-                    "继续Step 5"
-                ]
-            },
-            {
-                'option': 'B',
-                'label': '回滚到上一章',
-                'risk': 'high',
-                'description': '恢复到上一章的state.json快照',
-                'actions': [
-                    f"git checkout ch{(chapter_num-1):04d} -- .webnovel/state.json",
-                    f"删除第{chapter_num}章文件",
-                    "清理中断状态"
-                ]
-            }
-        ]
-
-    elif step_id == 'Step 5':
-        # Step 5中断:Git未提交
-        return [
-            {
-                'option': 'A',
-                'label': '继续Git提交',
-                'risk': 'low',
-                'description': '完成未完成的Git commit + tag',
-                'actions': [
-                    "检查 Git 暂存区",
-                    "重新执行 backup_manager.py",
-                    "继续Step 6"
-                ]
-            },
-            {
-                'option': 'B',
-                'label': '回滚Git改动',
-                'risk': 'medium',
-                'description': '丢弃暂存区所有改动',
-                'actions': [
-                    "git reset HEAD .",
-                    f"删除第{chapter_num}章文件",
-                    "清理中断状态"
-                ]
-            }
-        ]
-
-    elif step_id == 'Step 7':
-        # Step 7中断:审查未完成
-        return [
-            {
-                'option': 'A',
-                'label': '重新执行双章审查',
-                'risk': 'high',
-                'description': '重新调用5个审查员(成本高,耗时长)',
-                'actions': [
-                    "重新调用5个审查员(并行)",
-                    "生成审查报告",
-                    "更新 state.json review_checkpoints"
-                ]
-            },
-            {
-                'option': 'B',
-                'label': '跳过审查,继续下一章',
-                'risk': 'medium',
-                'description': '不进行审查(可后续用 /webnovel-review 补审)',
-                'actions': [
-                    "标记审查为已跳过",
-                    "清理中断状态",
-                    "可继续创作下一章"
-                ]
-            }
-        ]
-
-    # 默认选项
-    return [{
-        'option': 'A',
-        'label': '从头开始',
-        'risk': 'low',
-        'description': '重新执行完整流程',
-        'actions': [
-            f"清理所有中断artifacts",
-            f"执行 /{command} {chapter_num}"
-        ]
-    }]
-
-def cleanup_artifacts(chapter_num):
-    """清理半成品artifacts"""
-    artifacts_cleaned = []
-
-    # 删除章节文件
-    chapter_file = f"正文/第{chapter_num:04d}章.md"
-    if os.path.exists(chapter_file):
-        os.remove(chapter_file)
-        artifacts_cleaned.append(chapter_file)
-
-    # 清理Git暂存区
-    result = subprocess.run(['git', 'reset', 'HEAD', '.'],
-                          capture_output=True, text=True)
-    if result.returncode == 0:
-        artifacts_cleaned.append("Git暂存区已清理")
-
-    return artifacts_cleaned
-
-def clear_current_task():
-    """清除当前中断任务"""
-    state = load_state()
-    if state.get('current_task'):
-        state['current_task'] = None
-        save_state(state)
-        print("✅ 中断任务已清除")
-    else:
-        print("⚠️ 无中断任务")
-
-def load_state():
-    """加载workflow状态"""
-    if not os.path.exists(WORKFLOW_STATE_FILE):
-        return {'current_task': None, 'last_stable_state': None, 'history': []}
-    with open(WORKFLOW_STATE_FILE, 'r', encoding='utf-8') as f:
-        return json.load(f)
-
-def save_state(state):
-    """保存workflow状态"""
-    os.makedirs(os.path.dirname(WORKFLOW_STATE_FILE), exist_ok=True)
-    with open(WORKFLOW_STATE_FILE, 'w', encoding='utf-8') as f:
-        json.dump(state, f, ensure_ascii=False, indent=2)
-
-def get_pending_steps(command):
-    """获取待执行步骤列表"""
-    if command == 'webnovel-write':
-        return ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Step 6', 'Step 7']
-    elif command == 'webnovel-review':
-        return ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Step 6', 'Step 7', 'Step 8']
-    # 其他命令...
-    return []
-
-def extract_stable_state(task):
-    """提取稳定状态快照"""
-    return {
-        'command': task['command'],
-        'chapter_num': task['args'].get('chapter_num'),
-        'completed_at': task.get('completed_at'),
-        'artifacts': task.get('artifacts', {})
-    }
-
-# CLI接口
-if __name__ == '__main__':
-    import argparse
-    parser = argparse.ArgumentParser(description='工作流状态管理')
-    subparsers = parser.add_subparsers(dest='action', help='操作类型')
-
-    # start-task
-    p_start_task = subparsers.add_parser('start-task', help='开始新任务')
-    p_start_task.add_argument('--command', required=True, help='命令名称')
-    p_start_task.add_argument('--chapter', type=int, help='章节号')
-
-    # start-step
-    p_start_step = subparsers.add_parser('start-step', help='开始Step')
-    p_start_step.add_argument('--step-id', required=True, help='Step ID')
-    p_start_step.add_argument('--step-name', required=True, help='Step名称')
-    p_start_step.add_argument('--note', help='进度备注')
-
-    # complete-step
-    p_complete_step = subparsers.add_parser('complete-step', help='完成Step')
-    p_complete_step.add_argument('--step-id', required=True, help='Step ID')
-    p_complete_step.add_argument('--artifacts', help='Artifacts JSON')
-
-    # complete-task
-    p_complete_task = subparsers.add_parser('complete-task', help='完成任务')
-    p_complete_task.add_argument('--artifacts', help='Final artifacts JSON')
-
-    # detect
-    subparsers.add_parser('detect', help='检测中断')
-
-    # cleanup
-    p_cleanup = subparsers.add_parser('cleanup', help='清理artifacts')
-    p_cleanup.add_argument('--chapter', type=int, required=True, help='章节号')
-
-    # clear
-    subparsers.add_parser('clear', help='清除中断任务')
-
-    args = parser.parse_args()
-
-    if args.action == 'start-task':
-        start_task(args.command, {'chapter_num': args.chapter})
-    elif args.action == 'start-step':
-        start_step(args.step_id, args.step_name, args.note)
-    elif args.action == 'complete-step':
-        complete_step(args.step_id, args.artifacts)
-    elif args.action == 'complete-task':
-        complete_task(args.artifacts)
-    elif args.action == 'detect':
-        interrupt = detect_interruption()
-        if interrupt:
-            print("\n🔴 检测到中断任务:")
-            print(json.dumps(interrupt, ensure_ascii=False, indent=2))
-            print("\n💡 恢复选项:")
-            options = analyze_recovery_options(interrupt)
-            print(json.dumps(options, ensure_ascii=False, indent=2))
-        else:
-            print("✅ 无中断任务")
-    elif args.action == 'cleanup':
-        cleaned = cleanup_artifacts(args.chapter)
-        print(f"✅ 已清理: {', '.join(cleaned)}")
-    elif args.action == 'clear':
-        clear_current_task()
-    else:
-        parser.print_help()