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

feat(scripts): 增强脚本系统,支持Strand Weave和实体层级分类

status_reporter.py:
- 新增 Strand Weave 节奏分析(Quest/Fire/Constellation 三线占比统计)
- 新增伏笔紧急度分析(基于三层级系统:核心/支线/装饰)
- 新增违规检测(连续超限、缺失超限、占比偏离)
- 新增 --focus strand 和 --focus urgency 命令行选项

extract_entities.py:
- 支持实体层级分类(核心/支线/装饰)匹配伏笔三层级系统
- 新增 [GOLDEN_FINGER_SKILL] 标签提取
- 增强格式支持:[NEW_ENTITY: 类型, 名称, 描述, 层级]
- 金手指技能同步到 state.json 的 protagonist_state.golden_finger.skills

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

Co-Authored-By: Claude <noreply@anthropic.com>
lingfengQAQ 5 месяцев назад
Родитель
Сommit
796aafb881

+ 156 - 23
.claude/skills/webnovel-writer/scripts/extract_entities.py

@@ -5,9 +5,11 @@
 功能:
 1. 扫描指定章节正文,提取所有 [NEW_ENTITY] 标签
 2. 解析实体类型(角色/地点/物品/势力/招式)
-3. 同步到设定集对应文件
-4. 更新 state.json 中的相关记录
-5. 支持自动化模式和交互式模式
+3. 支持实体层级分类(核心/支线/装饰)- 匹配伏笔三层级系统
+4. 提取金手指技能标签 [GOLDEN_FINGER_SKILL]
+5. 同步到设定集对应文件
+6. 更新 state.json 中的相关记录
+7. 支持自动化模式和交互式模式
 
 使用方式:
   python extract_entities.py <章节文件> [--auto] [--dry-run]
@@ -55,44 +57,129 @@ ROLE_CATEGORY_MAP = {
     "路人": "次要角色"
 }
 
+# 实体层级权重(匹配伏笔三层级系统)
+ENTITY_TIER_MAP = {
+    "核心": {"weight": 3.0, "desc": "必须追踪,影响主线"},
+    "core": {"weight": 3.0, "desc": "必须追踪,影响主线"},
+    "支线": {"weight": 2.0, "desc": "应该追踪,丰富剧情"},
+    "sub": {"weight": 2.0, "desc": "应该追踪,丰富剧情"},
+    "装饰": {"weight": 1.0, "desc": "可选追踪,增加真实感"},
+    "decor": {"weight": 1.0, "desc": "可选追踪,增加真实感"}
+}
+
 def extract_new_entities(file_path: str) -> List[Dict]:
     """
     从章节文件中提取所有 [NEW_ENTITY] 标签
 
-    标签格式:
-      [NEW_ENTITY: 角色, 李雪, 天云宗外门弟子,主角的青梅竹马]
-      [NEW_ENTITY: 地点, 血煞秘境, 危险的试炼之地,内有金丹期凶兽]
-      [NEW_ENTITY: 物品, 天雷果, 可提升雷属性修炼速度的灵果]
+    标签格式(支持两种):
+      基础格式:[NEW_ENTITY: 类型, 名称, 描述]
+      增强格式:[NEW_ENTITY: 类型, 名称, 描述, 层级]  (层级可选:核心/支线/装饰)
+
+      示例:
+      [NEW_ENTITY: 角色, 李雪, 天云宗外门弟子]
+      [NEW_ENTITY: 角色, 血煞门主, 本卷最终BOSS, 核心]
+      [NEW_ENTITY: 地点, 血煞秘境, 危险的试炼之地, 支线]
+      [NEW_ENTITY: 物品, 天雷果, 可提升雷属性修炼速度的灵果, 装饰]
 
     Returns:
-        List[Dict]: [{"type": "角色", "name": "李雪", "desc": "...", "line": 123}, ...]
+        List[Dict]: [{"type": "角色", "name": "李雪", "desc": "...", "tier": "支线", "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*([^\]]+)\]',
+            # 增强格式:4个字段(包含层级)
+            matches_enhanced = re.findall(
+                r'\[NEW_ENTITY:\s*([^,,]+)[,,]\s*([^,,]+)[,,]\s*([^,,]+)[,,]\s*([^,,\]]+)\]',
                 line
             )
 
-            for match in matches:
+            # 基础格式:3个字段
+            matches_basic = re.findall(
+                r'\[NEW_ENTITY:\s*([^,,]+)[,,]\s*([^,,]+)[,,]\s*([^,,\]]+)\]',
+                line
+            )
+
+            # 优先处理增强格式
+            for match in matches_enhanced:
                 entity_type = match[0].strip()
                 entity_name = match[1].strip()
                 entity_desc = match[2].strip()
+                entity_tier = match[3].strip()
+
+                # 验证层级有效性
+                if entity_tier.lower() not in ENTITY_TIER_MAP:
+                    entity_tier = "支线"  # 默认支线
 
                 entities.append({
                     "type": entity_type,
                     "name": entity_name,
                     "desc": entity_desc,
+                    "tier": entity_tier,
                     "line": line_num,
                     "source_file": file_path
                 })
 
+            # 处理基础格式(排除已被增强格式匹配的)
+            for match in matches_basic:
+                entity_type = match[0].strip()
+                entity_name = match[1].strip()
+                entity_desc = match[2].strip()
+
+                # 检查是否已被增强格式匹配
+                already_matched = any(
+                    e["name"] == entity_name and e["line"] == line_num
+                    for e in entities
+                )
+
+                if not already_matched:
+                    entities.append({
+                        "type": entity_type,
+                        "name": entity_name,
+                        "desc": entity_desc,
+                        "tier": "支线",  # 默认层级
+                        "line": line_num,
+                        "source_file": file_path
+                    })
+
     return entities
 
+
+def extract_golden_finger_skills(file_path: str) -> List[Dict]:
+    """
+    从章节文件中提取金手指技能标签 [GOLDEN_FINGER_SKILL]
+
+    标签格式:
+      [GOLDEN_FINGER_SKILL: 技能名, 等级, 描述, 冷却时间]
+
+      示例:
+      [GOLDEN_FINGER_SKILL: 吞噬, Lv1, 可吞噬敌人获得经验, 10秒]
+      [GOLDEN_FINGER_SKILL: 鉴定术, Lv2, 查看物品/角色属性, 无冷却]
+
+    Returns:
+        List[Dict]: [{"name": "吞噬", "level": "Lv1", "desc": "...", "cooldown": "10秒"}, ...]
+    """
+    skills = []
+
+    with open(file_path, 'r', encoding='utf-8') as f:
+        for line_num, line in enumerate(f, 1):
+            matches = re.findall(
+                r'\[GOLDEN_FINGER_SKILL:\s*([^,,]+)[,,]\s*([^,,]+)[,,]\s*([^,,]+)[,,]\s*([^\]]+)\]',
+                line
+            )
+
+            for match in matches:
+                skills.append({
+                    "name": match[0].strip(),
+                    "level": match[1].strip(),
+                    "desc": match[2].strip(),
+                    "cooldown": match[3].strip(),
+                    "line": line_num,
+                    "source_file": file_path
+                })
+
+    return skills
+
 def categorize_character(desc: str) -> str:
     """
     根据描述判断角色分类
@@ -233,8 +320,8 @@ def update_power_system(entity: Dict, target_file: str):
     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 中的实体记录"""
+def update_state_json(entities: List[Dict], state_file: str, golden_finger_skills: List[Dict] = None):
+    """更新 state.json 中的实体记录(支持层级分类和金手指技能)"""
     with open(state_file, 'r', encoding='utf-8') as f:
         state = json.load(f)
 
@@ -248,8 +335,19 @@ def update_state_json(entities: List[Dict], state_file: str):
             "techniques": []
         }
 
+    # 确保存在金手指技能列表
+    if 'protagonist_state' not in state:
+        state['protagonist_state'] = {}
+    if 'golden_finger' not in state['protagonist_state']:
+        state['protagonist_state']['golden_finger'] = {
+            "name": "",
+            "skills": [],
+            "level": 1
+        }
+
     for entity in entities:
         entity_type = entity['type']
+        entity_tier = entity.get('tier', '支线')
 
         if entity_type == "角色":
             if entity['name'] not in [c.get('name') for c in state['entities']['characters']]:
@@ -257,6 +355,7 @@ def update_state_json(entities: List[Dict], state_file: str):
                     "name": entity['name'],
                     "desc": entity['desc'],
                     "category": categorize_character(entity['desc']),
+                    "tier": entity_tier,
                     "first_appearance": entity.get('source_file', ''),
                     "added_at": datetime.now().strftime('%Y-%m-%d')
                 })
@@ -266,6 +365,7 @@ def update_state_json(entities: List[Dict], state_file: str):
                 state['entities']['locations'].append({
                     "name": entity['name'],
                     "desc": entity['desc'],
+                    "tier": entity_tier,
                     "first_appearance": entity.get('source_file', ''),
                     "added_at": datetime.now().strftime('%Y-%m-%d')
                 })
@@ -275,6 +375,7 @@ def update_state_json(entities: List[Dict], state_file: str):
                 state['entities']['items'].append({
                     "name": entity['name'],
                     "desc": entity['desc'],
+                    "tier": entity_tier,
                     "first_appearance": entity.get('source_file', ''),
                     "added_at": datetime.now().strftime('%Y-%m-%d')
                 })
@@ -284,6 +385,7 @@ def update_state_json(entities: List[Dict], state_file: str):
                 state['entities']['factions'].append({
                     "name": entity['name'],
                     "desc": entity['desc'],
+                    "tier": entity_tier,
                     "first_appearance": entity.get('source_file', ''),
                     "added_at": datetime.now().strftime('%Y-%m-%d')
                 })
@@ -293,10 +395,26 @@ def update_state_json(entities: List[Dict], state_file: str):
                 state['entities']['techniques'].append({
                     "name": entity['name'],
                     "desc": entity['desc'],
+                    "tier": entity_tier,
                     "first_appearance": entity.get('source_file', ''),
                     "added_at": datetime.now().strftime('%Y-%m-%d')
                 })
 
+    # 更新金手指技能
+    if golden_finger_skills:
+        existing_skills = [s.get('name') for s in state['protagonist_state']['golden_finger'].get('skills', [])]
+        for skill in golden_finger_skills:
+            if skill['name'] not in existing_skills:
+                state['protagonist_state']['golden_finger']['skills'].append({
+                    "name": skill['name'],
+                    "level": skill['level'],
+                    "desc": skill['desc'],
+                    "cooldown": skill['cooldown'],
+                    "unlocked_at": skill.get('source_file', ''),
+                    "added_at": datetime.now().strftime('%Y-%m-%d')
+                })
+                print(f"  ✨ 新增金手指技能: {skill['name']} ({skill['level']})")
+
     # 备份旧文件
     backup_file = state_file.replace('.json', f'.backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json')
     os.rename(state_file, backup_file)
@@ -438,13 +556,23 @@ def main():
     print(f"📖 正在扫描: {chapter_file}")
     entities = extract_new_entities(chapter_file)
 
-    if not entities:
-        print("✅ 未发现 [NEW_ENTITY] 标签")
+    # 提取金手指技能
+    golden_finger_skills = extract_golden_finger_skills(chapter_file)
+
+    if not entities and not golden_finger_skills:
+        print("✅ 未发现 [NEW_ENTITY] 或 [GOLDEN_FINGER_SKILL] 标签")
         return
 
-    print(f"\n🔍 发现 {len(entities)} 个新实体:")
-    for i, entity in enumerate(entities, 1):
-        print(f"  {i}. [{entity['type']}] {entity['name']} - {entity['desc'][:30]}...")
+    if entities:
+        print(f"\n🔍 发现 {len(entities)} 个新实体:")
+        for i, entity in enumerate(entities, 1):
+            tier_emoji = {"核心": "🔴", "支线": "🟡", "装饰": "🟢"}.get(entity.get('tier', '支线'), "⚪")
+            print(f"  {i}. [{entity['type']}] {entity['name']} {tier_emoji}{entity.get('tier', '支线')} - {entity['desc'][:25]}...")
+
+    if golden_finger_skills:
+        print(f"\n✨ 发现 {len(golden_finger_skills)} 个金手指技能:")
+        for i, skill in enumerate(golden_finger_skills, 1):
+            print(f"  {i}. {skill['name']} ({skill['level']}) - {skill['desc'][:25]}...")
 
     if dry_run:
         print("\n⚠️  Dry-run 模式,不执行实际写入")
@@ -467,17 +595,22 @@ def main():
         if sync_entity_to_settings(entity, str(project_root), auto_mode):
             success_count += 1
 
-    # 更新 state.json
+    # 更新 state.json(包含金手指技能)
     print(f"\n💾 更新 state.json...")
-    update_state_json(entities, str(state_file))
+    update_state_json(entities, str(state_file), golden_finger_skills)
 
-    print(f"\n✅ 完成!成功同步 {success_count}/{len(entities)} 个实体")
+    print(f"\n✅ 完成!")
+    print(f"  - 实体同步: {success_count}/{len(entities)} 个")
+    if golden_finger_skills:
+        print(f"  - 金手指技能: {len(golden_finger_skills)} 个")
 
     if not auto_mode:
         print("\n💡 建议:")
         print("  1. 检查生成的角色卡/物品卡,补充详细设定")
         print("  2. 查看 世界观.md 和 力量体系.md 的更新")
         print("  3. 确认 .webnovel/state.json 中的实体记录")
+        if golden_finger_skills:
+            print("  4. 检查金手指技能是否正确记录在 protagonist_state.golden_finger.skills")
 
 if __name__ == "__main__":
     main()

+ 310 - 3
.claude/skills/webnovel-writer/scripts/status_reporter.py

@@ -6,10 +6,12 @@
 
 功能:
 1. 角色活跃度分析:哪些角色太久没出场(掉线统计)
-2. 伏笔深度分析:哪些坑挖得太久了(超过 20 万字未收)
+2. 伏笔深度分析:哪些坑挖得太久了(超过 20 万字未收)+ 紧急度排序
 3. 爽点节奏分布:全书高潮点的分布频率(热力图)
 4. 字数分布统计:各卷、各篇的字数分布
 5. 人际关系图谱:好感度/仇恨度趋势
+6. Strand Weave 节奏分析:Quest/Fire/Constellation 三线占比统计
+7. 伏笔紧急度排序:基于三层级系统(核心/支线/装饰)的优先级计算
 
 输出格式:
   - Markdown 报告(.webnovel/health_report.md)
@@ -28,6 +30,9 @@
   # 仅分析爽点节奏
   python status_reporter.py --focus pacing
 
+  # 分析 Strand Weave 节奏
+  python status_reporter.py --focus strand
+
 报告示例:
   # 全书健康报告
 
@@ -236,6 +241,204 @@ class StatusReporter:
         else:
             return "🔴 严重超时"
 
+    def analyze_foreshadowing_urgency(self) -> List[Dict]:
+        """
+        分析伏笔紧急度(基于三层级系统)
+
+        三层级权重:
+        - 核心(Core): 权重 3.0 - 必须回收,否则剧情崩塌
+        - 支线(Sub): 权重 2.0 - 应该回收,否则显得作者健忘
+        - 装饰(Decor): 权重 1.0 - 可回收可不回收,仅增加真实感
+
+        紧急度计算公式:
+        urgency = (已过章节 / 目标回收章节) × 层级权重
+        """
+        if not self.state:
+            return []
+
+        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
+        plot_threads = self.state.get("plot_threads", {})
+        foreshadowing = plot_threads.get("foreshadowing", [])
+
+        # 层级权重映射
+        tier_weights = {
+            "核心": 3.0,
+            "core": 3.0,
+            "支线": 2.0,
+            "sub": 2.0,
+            "装饰": 1.0,
+            "decor": 1.0
+        }
+
+        urgency_list = []
+
+        for item in foreshadowing:
+            if item.get("status") == "已回收":
+                continue
+
+            content = item.get("content", "")
+            tier = item.get("tier", "支线")  # 默认支线
+            planted_chapter = item.get("planted_chapter", 1)
+            target_chapter = item.get("target_chapter", planted_chapter + 100)
+
+            weight = tier_weights.get(tier.lower(), 2.0)
+            elapsed = current_chapter - planted_chapter
+            remaining = target_chapter - current_chapter
+
+            # 紧急度计算
+            if target_chapter > planted_chapter:
+                urgency = (elapsed / (target_chapter - planted_chapter)) * weight
+            else:
+                urgency = weight * 2  # 已超期
+
+            urgency_list.append({
+                "content": content,
+                "tier": tier,
+                "weight": weight,
+                "planted_chapter": planted_chapter,
+                "target_chapter": target_chapter,
+                "elapsed": elapsed,
+                "remaining": remaining,
+                "urgency": round(urgency, 2),
+                "status": self._get_urgency_status(urgency, remaining)
+            })
+
+        # 按紧急度排序(降序)
+        return sorted(urgency_list, key=lambda x: x["urgency"], reverse=True)
+
+    def _get_urgency_status(self, urgency: float, remaining: int) -> str:
+        """判断紧急度状态"""
+        if remaining < 0:
+            return "🔴 已超期"
+        elif urgency >= 2.0:
+            return "🔴 紧急"
+        elif urgency >= 1.0:
+            return "🟡 警告"
+        else:
+            return "🟢 正常"
+
+    def analyze_strand_weave(self) -> Dict:
+        """
+        分析 Strand Weave 节奏分布
+
+        三线定义:
+        - Quest(主线): 战斗、任务、升级 - 目标 55-65%
+        - Fire(感情): 感情线、人际互动 - 目标 20-30%
+        - Constellation(世界观): 世界观展开、势力背景 - 目标 10-20%
+
+        检查规则:
+        - Quest 线连续不超过 5 章
+        - Fire 线缺失不超过 10 章
+        - Constellation 线缺失不超过 15 章
+        """
+        if not self.state:
+            return {}
+
+        strand_tracker = self.state.get("strand_tracker", {})
+        history = strand_tracker.get("history", [])
+
+        if not history:
+            return {
+                "has_data": False,
+                "message": "暂无 Strand Weave 数据"
+            }
+
+        # 统计各线占比
+        quest_count = 0
+        fire_count = 0
+        constellation_count = 0
+        total = len(history)
+
+        for entry in history:
+            strand = entry.get("strand", "").lower()
+            if strand in ["quest", "主线", "战斗", "任务"]:
+                quest_count += 1
+            elif strand in ["fire", "感情", "感情线", "互动"]:
+                fire_count += 1
+            elif strand in ["constellation", "世界观", "背景", "势力"]:
+                constellation_count += 1
+
+        # 计算占比
+        quest_ratio = (quest_count / total * 100) if total > 0 else 0
+        fire_ratio = (fire_count / total * 100) if total > 0 else 0
+        constellation_ratio = (constellation_count / total * 100) if total > 0 else 0
+
+        # 检查违规
+        violations = []
+
+        # 检查 Quest 连续超过 5 章
+        quest_streak = 0
+        max_quest_streak = 0
+        for entry in history:
+            strand = entry.get("strand", "").lower()
+            if strand in ["quest", "主线", "战斗", "任务"]:
+                quest_streak += 1
+                max_quest_streak = max(max_quest_streak, quest_streak)
+            else:
+                quest_streak = 0
+
+        if max_quest_streak > 5:
+            violations.append(f"Quest 线连续 {max_quest_streak} 章(超过 5 章限制)")
+
+        # 检查 Fire 缺失超过 10 章
+        fire_gap = 0
+        max_fire_gap = 0
+        for entry in history:
+            strand = entry.get("strand", "").lower()
+            if strand in ["fire", "感情", "感情线", "互动"]:
+                max_fire_gap = max(max_fire_gap, fire_gap)
+                fire_gap = 0
+            else:
+                fire_gap += 1
+        max_fire_gap = max(max_fire_gap, fire_gap)
+
+        if max_fire_gap > 10:
+            violations.append(f"Fire 线缺失 {max_fire_gap} 章(超过 10 章限制)")
+
+        # 检查 Constellation 缺失超过 15 章
+        const_gap = 0
+        max_const_gap = 0
+        for entry in history:
+            strand = entry.get("strand", "").lower()
+            if strand in ["constellation", "世界观", "背景", "势力"]:
+                max_const_gap = max(max_const_gap, const_gap)
+                const_gap = 0
+            else:
+                const_gap += 1
+        max_const_gap = max(max_const_gap, const_gap)
+
+        if max_const_gap > 15:
+            violations.append(f"Constellation 线缺失 {max_const_gap} 章(超过 15 章限制)")
+
+        # 检查占比是否在合理范围
+        if quest_ratio < 55:
+            violations.append(f"Quest 占比 {quest_ratio:.1f}% 偏低(目标 55-65%)")
+        elif quest_ratio > 65:
+            violations.append(f"Quest 占比 {quest_ratio:.1f}% 偏高(目标 55-65%)")
+
+        if fire_ratio < 20:
+            violations.append(f"Fire 占比 {fire_ratio:.1f}% 偏低(目标 20-30%)")
+        elif fire_ratio > 30:
+            violations.append(f"Fire 占比 {fire_ratio:.1f}% 偏高(目标 20-30%)")
+
+        if constellation_ratio < 10:
+            violations.append(f"Constellation 占比 {constellation_ratio:.1f}% 偏低(目标 10-20%)")
+        elif constellation_ratio > 20:
+            violations.append(f"Constellation 占比 {constellation_ratio:.1f}% 偏高(目标 10-20%)")
+
+        return {
+            "has_data": True,
+            "total_chapters": total,
+            "quest": {"count": quest_count, "ratio": quest_ratio},
+            "fire": {"count": fire_count, "ratio": fire_ratio},
+            "constellation": {"count": constellation_count, "ratio": constellation_ratio},
+            "violations": violations,
+            "max_quest_streak": max_quest_streak,
+            "max_fire_gap": max_fire_gap,
+            "max_const_gap": max_const_gap,
+            "health": "✅ 健康" if not violations else f"⚠️ {len(violations)} 个问题"
+        }
+
     def analyze_pacing(self) -> List[Dict]:
         """分析爽点节奏分布(每 100 章为一段)"""
         segment_size = 100
@@ -327,10 +530,18 @@ class StatusReporter:
         if focus in ["all", "foreshadowing"]:
             report_lines.extend(self._generate_foreshadowing_section())
 
+        # 伏笔紧急度(新增)
+        if focus in ["all", "foreshadowing", "urgency"]:
+            report_lines.extend(self._generate_urgency_section())
+
         # 爽点节奏
         if focus in ["all", "pacing"]:
             report_lines.extend(self._generate_pacing_section())
 
+        # Strand Weave 节奏(新增)
+        if focus in ["all", "strand", "pacing"]:
+            report_lines.extend(self._generate_strand_section())
+
         # 人际关系
         if focus in ["all", "relationships"]:
             report_lines.extend(self._generate_relationship_section())
@@ -428,6 +639,101 @@ class StatusReporter:
 
         return lines
 
+    def _generate_urgency_section(self) -> List[str]:
+        """生成伏笔紧急度章节(基于三层级系统)"""
+        urgency_list = self.analyze_foreshadowing_urgency()
+
+        # 筛选紧急伏笔
+        urgent_items = [item for item in urgency_list if item["urgency"] >= 1.0]
+
+        lines = [
+            f"## 🚨 伏笔紧急度排序({len(urgent_items)}条需关注)",
+            "",
+            "> 基于三层级系统:核心(×3) / 支线(×2) / 装饰(×1)",
+            "> 紧急度 = (已过章节 / 目标回收章节) × 层级权重",
+            ""
+        ]
+
+        if urgency_list:
+            lines.extend([
+                "| 伏笔内容 | 层级 | 埋设 | 目标 | 紧急度 | 状态 |",
+                "|---------|------|------|------|--------|------|"
+            ])
+
+            for item in urgency_list[:10]:  # 只显示前10条
+                lines.append(
+                    f"| {item['content'][:20]}... | {item['tier']} | "
+                    f"第{item['planted_chapter']}章 | 第{item['target_chapter']}章 | "
+                    f"{item['urgency']:.2f} | {item['status']} |"
+                )
+        else:
+            lines.append("✅ 暂无伏笔数据")
+
+        lines.extend(["", "---", ""])
+
+        return lines
+
+    def _generate_strand_section(self) -> List[str]:
+        """生成 Strand Weave 节奏章节"""
+        strand_data = self.analyze_strand_weave()
+
+        lines = [
+            "## 🎭 Strand Weave 节奏分析",
+            ""
+        ]
+
+        if not strand_data.get("has_data"):
+            lines.append(f"⚠️ {strand_data.get('message', '暂无数据')}")
+            lines.extend(["", "---", ""])
+            return lines
+
+        # 占比统计
+        lines.extend([
+            "### 三线占比",
+            "",
+            "| Strand | 章节数 | 占比 | 目标范围 | 状态 |",
+            "|--------|--------|------|----------|------|"
+        ])
+
+        q = strand_data["quest"]
+        q_status = "✅" if 55 <= q["ratio"] <= 65 else "⚠️"
+        lines.append(f"| Quest(主线) | {q['count']} | {q['ratio']:.1f}% | 55-65% | {q_status} |")
+
+        f = strand_data["fire"]
+        f_status = "✅" if 20 <= f["ratio"] <= 30 else "⚠️"
+        lines.append(f"| Fire(感情) | {f['count']} | {f['ratio']:.1f}% | 20-30% | {f_status} |")
+
+        c = strand_data["constellation"]
+        c_status = "✅" if 10 <= c["ratio"] <= 20 else "⚠️"
+        lines.append(f"| Constellation(世界观) | {c['count']} | {c['ratio']:.1f}% | 10-20% | {c_status} |")
+
+        lines.append("")
+
+        # 连续性检查
+        lines.extend([
+            "### 连续性检查",
+            "",
+            f"- Quest 最大连续: {strand_data['max_quest_streak']} 章(限制 ≤5)",
+            f"- Fire 最大缺失: {strand_data['max_fire_gap']} 章(限制 ≤10)",
+            f"- Constellation 最大缺失: {strand_data['max_const_gap']} 章(限制 ≤15)",
+            ""
+        ])
+
+        # 违规清单
+        if strand_data["violations"]:
+            lines.extend([
+                "### ⚠️ 违规清单",
+                ""
+            ])
+            for v in strand_data["violations"]:
+                lines.append(f"- {v}")
+        else:
+            lines.append("### ✅ 无违规")
+
+        lines.extend(["", f"**综合健康度**: {strand_data['health']}", "", "---", ""])
+
+        return lines
+
     def _generate_pacing_section(self) -> List[str]:
         """生成节奏分析章节"""
         segments = self.analyze_pacing()
@@ -493,8 +799,9 @@ def main():
     parser.add_argument('--output', default='.webnovel/health_report.md',
                        help='输出文件路径')
     parser.add_argument('--focus', choices=['all', 'basic', 'characters',
-                                            'foreshadowing', 'pacing', 'relationships'],
-                       default='all', help='分析焦点')
+                                            'foreshadowing', 'urgency', 'pacing',
+                                            'strand', 'relationships'],
+                       default='all', help='分析焦点(新增 urgency, strand)')
     parser.add_argument('--project-root', default='.', help='项目根目录')
 
     args = parser.parse_args()