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

fix: normalize foreshadowing status pipeline

lingfengQAQ 5 месяцев назад
Родитель
Сommit
798a408bda

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

@@ -394,6 +394,7 @@ Ch 10: Quest + Fire(融合)
 - **[strand-weave-pattern.md](references/strand-weave-pattern.md)** - Strand Weave 详细规范
 - **[polish-guide.md](references/polish-guide.md)** - 内容润色指南(AI痕迹清除/语言优化/风格统一/自然化)
 - **[workflow-resume.md](references/workflow-resume.md)** - 工作流中断恢复机制(用于 /webnovel-resume 命令)
+- **[system-data-flow.md](references/system-data-flow.md)** - 数据链与组件地图(state/index/archive 口径与顺序)
 - **[archiving-strategy.md](references/archiving-strategy.md)** - 数据归档策略(200万字长跑保障)
 - **[git-workflow.md](references/git-workflow.md)** - Git 版本控制工作流规范
 
@@ -423,6 +424,7 @@ Ch 10: Quest + Fire(融合)
 - **[scene-description.md](references/writing/scene-description.md)** - 场景描写方法
 - **[combat-scenes.md](references/writing/combat-scenes.md)** - 战斗场景写作
 - **[emotion-psychology.md](references/writing/emotion-psychology.md)** - 情感与心理描写
+- **[typesetting.md](references/writing/typesetting.md)** - 排版与阅读体验(移动端优先)
 
 #### 高级技巧 (advanced/)
 - **[multi-threading.md](references/advanced/multi-threading.md)** - 多线叙事技巧

+ 10 - 4
.claude/skills/webnovel-writer/references/advanced/foreshadowing.md

@@ -286,14 +286,20 @@
 
 | ID | 埋设章节 | 伏笔内容 | 预计回收章节 | 回收方式 | 状态 |
 |----|---------|---------|-------------|---------|------|
-| F01 | 第10章 | 主角父亲失踪 | 第300章 | 层层揭开 | 进行中 |
-| F02 | 第20章 | 神秘玉佩 | 第150章 | 意外反转 | 进行中 |
-| F03 | 第30章 | 破庙神像 | 第200章 | 直接揭晓 | 回收 |
+| F01 | 第10章 | 主角父亲失踪 | 第300章 | 层层揭开 | 未回收 |
+| F02 | 第20章 | 神秘玉佩 | 第150章 | 意外反转 | 未回收 |
+| F03 | 第30章 | 破庙神像 | 第200章 | 直接揭晓 | 回收 |
 | F04 | 第50章 | 师妹身份 | 第180章 | 层层揭开 | 已回收 |
 
+**与本 Skill 数据结构对齐(推荐)**:
+- `state.json` 中伏笔存储在 `plot_threads.foreshadowing`
+- 状态字段统一用:`未回收` / `已回收`(脚本会兼容 `待回收/进行中/active/pending/resolved` 并自动归一化)
+- 可选字段:`tier`(核心/支线/装饰)、`planted_chapter`、`target_chapter`、`location`、`characters`
+- 紧急度分析:`python .claude/skills/webnovel-writer/scripts/status_reporter.py --focus urgency`
+
 **使用方法**:
 1. 每次埋伏笔时,立即登记
-2. 定期检查"待回收"伏笔
+2. 定期检查"回收"伏笔
 3. 回收后标记"已回收"
 
 ---

+ 4 - 4
.claude/skills/webnovel-writer/references/archiving-strategy.md

@@ -55,7 +55,7 @@
 - **超过 50 章未出场**(`current_chapter - last_appearance_chapter ≥ 50`)
 
 **归档目标**:
-- `.webnovel/.webnovel/archive/characters.json`
+- `.webnovel/archive/characters.json`
 
 **保留数据**:
 - 主要角色(`importance="major"`)**永不归档**
@@ -82,7 +82,7 @@ python archive_manager.py --restore-character "李雪"
 - **超过 20 章**(`current_chapter - resolved_chapter ≥ 20`)
 
 **归档目标**:
-- `.webnovel/.webnovel/archive/plot_threads.json`
+- `.webnovel/archive/plot_threads.json`
 
 **保留数据**:
 - 未回收的伏笔(`status="未回收"`)**永不归档**
@@ -107,7 +107,7 @@ python archive_manager.py --restore-character "李雪"
 - 审查报告超过 **50 章**(`current_chapter - review_chapter ≥ 50`)
 
 **归档目标**:
-- `.webnovel/.webnovel/archive/reviews.json`
+- `.webnovel/archive/reviews.json`
 
 **保留数据**:
 - 最近 50 章的审查报告保持活跃
@@ -308,7 +308,7 @@ python archive_manager.py --restore-character "李雪"
 ```
 
 **执行逻辑**:
-1. 从 `.webnovel/.webnovel/archive/characters.json` 中查找角色"李雪"
+1. 从 `.webnovel/archive/characters.json` 中查找角色"李雪"
 2. 移除 `archived_at` 字段
 3. 将角色恢复到 `state.json` 的 `entities.characters`
 4. 从归档文件中移除该角色

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

@@ -642,8 +642,8 @@
 ```markdown
 | ID | 类型 | 埋设章节 | 预计回收 | 状态 |
 |----|------|---------|---------|------|
-| F-001 | 身份 | 第5章 | 第30章 | 回收 |
-| F-002 | 物品 | 第12章 | 第40章 | 回收 |
+| F-001 | 身份 | 第5章 | 第30章 | 回收 |
+| F-002 | 物品 | 第12章 | 第40章 | 回收 |
 ```
 
 **Step 2: 定期回顾**

+ 68 - 0
.claude/skills/webnovel-writer/references/system-data-flow.md

@@ -0,0 +1,68 @@
+# Webnovel Studio 数据链与组件地图
+
+本文件用于快速确认:**每个组件负责什么、读写哪些数据、以及推荐的调用顺序**(避免“文档说法/历史数据/脚本实现”互相打架)。
+
+---
+
+## 1. 目录约定(项目根目录)
+
+- `正文/`:章节文件(支持 `正文/第0001章.md` 或 `正文/第1卷/第001章-标题.md`)
+- `大纲/`:卷纲/章纲/场景纲
+- `设定集/`:世界观/力量体系/角色卡/物品卡等设定库
+- `.webnovel/state.json`:唯一权威状态(角色/伏笔/进度/主角状态/关系/审查记录)
+- `.webnovel/workflow_state.json`:工作流断点(用于 `/webnovel-resume`)
+- `.webnovel/index.db`:结构化索引(章节/角色/伏笔的 SQLite 索引)
+- `.webnovel/archive/*.json`:归档数据(角色/伏笔/审查报告)
+
+---
+
+## 2. 关键脚本职责(输入/输出)
+
+- `init_project.py`:初始化项目结构与模板(生成 `.webnovel/state.json` 等)
+- `extract_entities.py`:扫描章节中的 `[NEW_ENTITY]` / `[GOLDEN_FINGER_SKILL]` / `[FORESHADOWING_JSON]` → 写入 `设定集/` + 更新 `state.json`
+- `update_state.py`:**原子性**更新 `state.json`(进度/位置/战力/关系/伏笔/审查记录/Strand)
+- `structured_index.py`:把章节元数据写入 `.webnovel/index.db`;并从 `state.json` 同步角色/伏笔到索引(用于快速查询/上下文筛选)
+- `status_reporter.py`:生成健康报告、伏笔紧急度分析、Strand 分布等
+- `archive_manager.py`:归档不活跃角色、已回收伏笔、过旧审查报告(写入 `.webnovel/archive/*.json` 并从 `state.json` 移除)
+- `context_manager.py`:生成“滑动窗口”上下文(优先读索引;失败则降级读 `state.json` + 文件遍历)
+
+---
+
+## 3. 每章推荐数据链(写作→索引→设定→状态)
+
+1. 写/保存章节 → `正文/…`
+2. `metadata-extractor`(agent)抽取:`title/location/characters/word_count/hash`
+3. `structured_index.py --update-chapter ...`(更新 `.webnovel/index.db`)
+4. `extract_entities.py --chapter N --auto`(同步 `设定集/` + 更新 `state.json`)
+5. `update_state.py --progress N WORDS`(必要时记录位置/战力/Strand/关系/伏笔)
+6. `archive_manager.py --auto-check`(可选:自动触发归档,控制 `state.json` 体积)
+7. Git 备份(可选)
+
+---
+
+## 4. 伏笔字段规范(避免数据链断裂)
+
+### 4.1 状态字段(统一口径)
+
+- 规范值:`未回收` / `已回收`
+- 兼容值(历史/别名):`待回收` / `进行中` / `active` / `pending` / `resolved`
+- 说明:脚本会尽量**自动归一化**到规范值,但写作与手工更新时仍建议统一用规范值。
+
+### 4.2 推荐字段(越全越好)
+
+- 最小可用:`content`, `status`
+- 强烈建议:`planted_chapter`, `target_chapter`, `tier`(核心/支线/装饰)
+- 可选增强:`location`, `characters`, `resolved_chapter`
+
+### 4.3 紧急度来源(两套,别混淆)
+
+- **分析用**(更精细):`status_reporter.py --focus urgency`(基于 `tier + target_chapter`)
+- **快速提醒**(更粗糙):`structured_index.py --query-urgent-foreshadowing`(基于“超过 50 章未回收”的阈值)
+
+---
+
+## 5. 常见冲突与已修复点
+
+- 伏笔状态写成“待回收/进行中”导致被过滤:脚本已兼容并建议统一写“未回收”
+- 手工 `--add-foreshadowing` 没有埋设/目标章节:`update_state.py` 已自动补 `planted_chapter/target_chapter/tier`
+- 归档路径混淆:固定为 `.webnovel/archive/*.json`

+ 60 - 0
.claude/skills/webnovel-writer/references/writing/typesetting.md

@@ -0,0 +1,60 @@
+# 排版与阅读体验(移动端优先)
+
+> **目的**:让读者“滑着读”更顺(手机屏幕),避免因为段落/对话/标点问题造成弃读。
+
+---
+
+## 1. 段落规则(最重要)
+
+- **一段一动作/一信息**:每段只承担一个功能(推进/展示/情绪/信息)。
+- **段落不要太长**:连续 5 行以上的长段落,优先拆成 2-3 段(尤其是解释说明段)。
+- **“快段落”用在高潮**:高潮/战斗/追逐时,多用短句+短段落,提升阅读速度与冲击力。
+- **“慢段落”用在铺垫**:铺垫允许稍长,但要确保每段都有新信息,不要纯形容堆砌。
+
+---
+
+## 2. 对话排版(默认规范)
+
+- **一人一句一行**:换人就换行;不要同段塞 3-4 句多人对白。
+- **对话夹动作**:用动作/表情/环境插入,帮助读者辨认说话者、控制节奏。
+- **避免“谁在说”混乱**:多人场景每 2-3 句补一次明确的说话者或动作锚点。
+
+**示例(推荐)**:
+```markdown
+“你确定要进去?”李雪压低声音。
+
+林天看向黑雾翻涌的洞口,喉结滚了滚:“不进去,答案永远都在外面。”
+
+“那就别死。”她把一枚玉符塞到他掌心。
+```
+
+---
+
+## 3. 标点与可读性
+
+- **少用长串省略号/感叹号**:`……!!!` 这类组合一章内别反复出现。
+- **解释句别太“论文味”**:避免“首先/其次/总之/由此可见”式结构化套话。
+- **专有名词统一写法**:人名/势力/境界/技能名,统一一个版本(写入 `设定集/` 并在 `state.json` 追踪)。
+
+---
+
+## 4. 场景切换与章节结构
+
+- **切镜头要留呼吸**:场景切换前后各留一个空行;必要时用 `---` 做分隔。
+- **信息密度要可见**:关键设定/关键线索/关键转折,尽量落在段首或段尾,减少“翻半屏才看到重点”。
+- **章末必须有钩子**:疑问/威胁/承诺/反转其一即可(不要结束在“回去睡觉了”)。
+
+---
+
+## 5. 发布前 10 项速查
+
+1. 对话是否“换人换行”
+2. 是否存在 5 行以上的长段落(能否拆)
+3. 同一段是否混入多名角色对白(能否拆)
+4. 是否出现大量“总结/列举口吻”的解释段
+5. 专有名词是否与 `设定集/` 一致
+6. 是否有“???/占位符正文”暴露给读者
+7. 场景切换是否清晰(空行/分隔/过渡句)
+8. 章内是否至少 1 个明确推进点(信息/行动/转折)
+9. 章末是否有钩子
+10. 新实体是否标注 `[NEW_ENTITY]` / `[GOLDEN_FINGER_SKILL]` / `[FORESHADOWING_JSON]`

+ 54 - 11
.claude/skills/webnovel-writer/scripts/context_manager.py

@@ -370,6 +370,25 @@ class ContextManager:
 
         return cards
 
+    @staticmethod
+    def _is_resolved_foreshadowing_status(raw_status: Any) -> bool:
+        """判断伏笔是否已回收(兼容历史字段与同义词)。"""
+        if raw_status is None:
+            return False
+
+        status = str(raw_status).strip()
+        if not status:
+            return False
+
+        status_lower = status.lower()
+        if status in {"已回收", "已完成", "已解决", "完成"}:
+            return True
+        if status_lower in {"resolved", "done", "complete"}:
+            return True
+        if "已回收" in status:
+            return True
+        return False
+
     def _get_relevant_foreshadowing(self, location: Optional[str],
                                    characters: Optional[List[str]]) -> List[Dict[str, str]]:
         """获取相关伏笔(优先使用索引,支持复杂条件查询)"""
@@ -413,7 +432,7 @@ class ContextManager:
         relevant = []
 
         for item in all_foreshadowing:
-            if item.get("status") not in ["未回收", "active", "pending", None, ""]:
+            if self._is_resolved_foreshadowing_status(item.get("status")):
                 continue
 
             content = item.get("content", "")
@@ -493,27 +512,51 @@ class ContextManager:
         return "[境界划分待补充]"
 
     def _get_urgent_foreshadowing(self) -> List[str]:
-        """获取紧急伏笔(未回收 且 已埋超过 100 章)"""
+        """获取紧急伏笔(未回收 且 已埋超过 50 章)"""
         if not self.state:
             return []
 
-        current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
-        all_foreshadowing = self.state.get("plot_threads", {}).get("foreshadowing", [])
+        # 优先:使用索引的紧急度(简单阈值:>50章)
+        if self.use_index and self.index:
+            try:
+                urgent_plots = self.index.query_urgent_foreshadowing(threshold=60)
+                formatted = []
+                for plot in urgent_plots:
+                    content = plot.get("content", "")
+                    if not content:
+                        continue
+                    introduced = plot.get("introduced_chapter", 0) or 0
+                    formatted.append(f"⚠️ {content}(埋设Ch{introduced})")
+                return formatted[:3]
+            except Exception as e:
+                print(f"⚠️ 伏笔索引查询失败,降级到 state.json: {e}")
 
-        urgent = []
+        current_chapter = int(self.state.get("progress", {}).get("current_chapter", 0) or 0)
+        all_foreshadowing = self.state.get("plot_threads", {}).get("foreshadowing", []) or []
 
+        scored = []
         for item in all_foreshadowing:
-            if item.get("status") not in ["未回收", "active", "pending", None, ""]:
+            if self._is_resolved_foreshadowing_status(item.get("status")):
                 continue
 
-            # 计算已埋章节数(粗略:假设每章对应 1 个章节号增量)
-            # 实际项目中应该记录"埋设章节号"
-            # 这里简化:如果 added_at 距离现在超过 100 天,视为紧急
+            introduced = item.get("introduced_chapter") or item.get("planted_chapter") or 1
+            try:
+                introduced_chapter = int(introduced)
+            except (TypeError, ValueError):
+                introduced_chapter = 1
+
+            pending = current_chapter - introduced_chapter
+            if pending < 50:
+                continue
 
             content = item.get("content", "")
-            urgent.append(f"⚠️ {content}")
+            if not content:
+                continue
+
+            scored.append((pending, content))
 
-        return urgent[:3]  # 最多 3 条
+        scored.sort(key=lambda x: x[0], reverse=True)
+        return [f"⚠️ {content}(已埋 {pending} 章)" for pending, content in scored[:3]]
 
     def build_context(self, chapter_num: int, location: Optional[str] = None,
                      characters: Optional[List[str]] = None) -> Dict[str, Any]:

+ 20 - 2
.claude/skills/webnovel-writer/scripts/status_reporter.py

@@ -89,6 +89,24 @@ from collections import defaultdict
 from project_locator import resolve_project_root
 from chapter_paths import extract_chapter_num_from_filename
 
+def _is_resolved_foreshadowing_status(raw_status: Any) -> bool:
+    """判断伏笔是否已回收(兼容历史字段与同义词)。"""
+    if raw_status is None:
+        return False
+
+    status = str(raw_status).strip()
+    if not status:
+        return False
+
+    status_lower = status.lower()
+    if status in {"已回收", "已完成", "已解决", "完成"}:
+        return True
+    if status_lower in {"resolved", "done", "complete"}:
+        return True
+    if "已回收" in status:
+        return True
+    return False
+
 # Windows 编码兼容性修复
 if sys.platform == 'win32':
     import io
@@ -278,7 +296,7 @@ class StatusReporter:
 
         for item in foreshadowing:
             status = item.get("status")
-            if status not in ["未回收", "active", "pending", None, ""]:
+            if _is_resolved_foreshadowing_status(status):
                 continue
 
             # 假设每个伏笔记录了"added_chapter"(埋设章节)
@@ -345,7 +363,7 @@ class StatusReporter:
         urgency_list = []
 
         for item in foreshadowing:
-            if item.get("status") in ["已回收", "resolved"]:
+            if _is_resolved_foreshadowing_status(item.get("status")):
                 continue
 
             content = item.get("content", "")

+ 26 - 2
.claude/skills/webnovel-writer/scripts/update_state.py

@@ -14,7 +14,7 @@
   python update_state.py --protagonist-power "金丹" 3 "雷劫"
 
   # 更新人际关系
-  python update_state.py --relationship "李雪" affection 95 --relationship-status "李雪" "确认关系"
+  python update_state.py --relationship "李雪" affection 95
 
   # 记录伏笔
   python update_state.py --add-foreshadowing "神秘玉佩的秘密" "未回收"
@@ -239,10 +239,34 @@ class StateUpdater:
                 print(f"⚠️  伏笔已存在: {content}")
                 return
 
+        # 归一化状态,避免 "待回收/进行中/active/pending" 等混用导致下游过滤漏掉
+        raw_status = "" if status is None else str(status).strip()
+        raw_status_lower = raw_status.lower()
+        if raw_status in {"已回收", "已完成", "已解决", "完成"} or raw_status_lower in {"resolved", "done", "complete"}:
+            status = "已回收"
+        elif (
+            raw_status in {"未回收", "待回收", "进行中", "未解决"}
+            or raw_status_lower in {"active", "pending"}
+            or not raw_status
+        ):
+            status = "未回收"
+        else:
+            status = "未回收"
+
+        planted_chapter = int(self.state.get("progress", {}).get("current_chapter", 0) or 0)
+        if planted_chapter <= 0:
+            planted_chapter = 1
+            print("? 未找到有效 progress.current_chapter,默认 planted_chapter=1")
+
+        target_chapter = planted_chapter + 100
+
         self.state["plot_threads"]["foreshadowing"].append({
             "content": content,
             "status": status,
-            "added_at": datetime.now().strftime("%Y-%m-%d")
+            "added_at": datetime.now().strftime("%Y-%m-%d"),
+            "planted_chapter": planted_chapter,
+            "target_chapter": target_chapter,
+            "tier": "支线"
         })
         print(f"📝 添加伏笔: {content}({status})")