소스 검색

chore: 提交剩余已跟踪改动(排除未跟踪文件)

lingfengQAQ 3 달 전
부모
커밋
6ea9748bd7

+ 36 - 2
.claude/agents/data-agent.md

@@ -189,7 +189,24 @@ python -m data_modules.index_manager accrue-interest --current-chapter {chapter}
 - 将逾期债务标记为 `status='overdue'`
 - 将逾期债务标记为 `status='overdue'`
 - 记录利息事件到 `debt_events` 表
 - 记录利息事件到 `debt_events` 表
 
 
-### Step J: 生成处理报告
+### Step J: 生成处理报告(含性能日志)
+
+**必须记录分步耗时**(用于定位慢点):
+- A 加载上下文
+- B AI 实体提取
+- C 实体消歧
+- D 写入 state/index
+- E 写入章节摘要
+- F AI 场景切片
+- G RAG 向量索引
+- H 风格样本评估(若跳过写 0)
+- I 债务利息(若跳过写 0)
+- TOTAL 总耗时
+
+**性能日志落盘(新增,必做)**:
+- 脚本自动写入:`.webnovel/observability/data_agent_timing.jsonl`
+- Data Agent 报告中仍需返回:`timing_ms` + `bottlenecks_top3`
+- 规则:`bottlenecks_top3` 始终按耗时降序返回;当 `TOTAL > 30000ms` 时,需在报告文字部分附加原因说明。
 
 
 ```json
 ```json
 {
 {
@@ -205,7 +222,24 @@ python -m data_modules.index_manager accrue-interest --current-chapter {chapter}
   "warnings": [
   "warnings": [
     "中置信度匹配: 那位前辈 → yaolao (confidence: 0.6)"
     "中置信度匹配: 那位前辈 → yaolao (confidence: 0.6)"
   ],
   ],
-  "errors": []
+  "errors": [],
+  "timing_ms": {
+    "A_load_context": 120,
+    "B_entity_extract": 18500,
+    "C_disambiguation": 210,
+    "D_state_index_write": 430,
+    "E_summary_write": 90,
+    "F_scene_chunking": 6200,
+    "G_rag_index": 2800,
+    "H_style_sample": 150,
+    "I_debt_interest": 0,
+    "TOTAL": 28500
+  },
+  "bottlenecks_top3": [
+    {"step": "B_entity_extract", "elapsed_ms": 18500, "ratio": 64.9},
+    {"step": "F_scene_chunking", "elapsed_ms": 6200, "ratio": 21.8},
+    {"step": "G_rag_index", "elapsed_ms": 2800, "ratio": 9.8}
+  ]
 }
 }
 ```
 ```
 
 

+ 23 - 1
.claude/scripts/data_modules/index_manager.py

@@ -34,6 +34,7 @@ v5.1 变更:
 
 
 import sqlite3
 import sqlite3
 import json
 import json
+import time
 from pathlib import Path
 from pathlib import Path
 
 
 from runtime_compat import enable_windows_utf8_stdio
 from runtime_compat import enable_windows_utf8_stdio
@@ -48,7 +49,7 @@ from .index_entity_mixin import IndexEntityMixin
 from .index_debt_mixin import IndexDebtMixin
 from .index_debt_mixin import IndexDebtMixin
 from .index_reading_mixin import IndexReadingMixin
 from .index_reading_mixin import IndexReadingMixin
 from .index_observability_mixin import IndexObservabilityMixin
 from .index_observability_mixin import IndexObservabilityMixin
-from .observability import safe_log_tool_call
+from .observability import safe_append_perf_timing, safe_log_tool_call
 
 
 
 
 @dataclass
 @dataclass
@@ -864,6 +865,7 @@ def main():
     )
     )
 
 
     args = parser.parse_args()
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
 
     # 初始化
     # 初始化
     config = None
     config = None
@@ -875,9 +877,28 @@ def main():
     manager = IndexManager(config)
     manager = IndexManager(config)
     tool_name = f"index_manager:{args.command or 'unknown'}"
     tool_name = f"index_manager:{args.command or 'unknown'}"
 
 
+    def _append_timing(
+        success: bool,
+        *,
+        error_code: Optional[str] = None,
+        error_message: Optional[str] = None,
+        chapter: Optional[int] = None,
+    ):
+        elapsed_ms = int((time.perf_counter() - command_started_at) * 1000)
+        safe_append_perf_timing(
+            manager.config.project_root,
+            tool_name=tool_name,
+            success=success,
+            elapsed_ms=elapsed_ms,
+            chapter=chapter,
+            error_code=error_code,
+            error_message=error_message,
+        )
+
     def emit_success(data=None, message: str = "ok", chapter: Optional[int] = None):
     def emit_success(data=None, message: str = "ok", chapter: Optional[int] = None):
         print_success(data, message=message)
         print_success(data, message=message)
         safe_log_tool_call(manager, tool_name=tool_name, success=True, chapter=chapter)
         safe_log_tool_call(manager, tool_name=tool_name, success=True, chapter=chapter)
+        _append_timing(True, chapter=chapter)
 
 
     def emit_error(code: str, message: str, suggestion: Optional[str] = None, chapter: Optional[int] = None):
     def emit_error(code: str, message: str, suggestion: Optional[str] = None, chapter: Optional[int] = None):
         print_error(code, message, suggestion=suggestion)
         print_error(code, message, suggestion=suggestion)
@@ -889,6 +910,7 @@ def main():
             error_message=message,
             error_message=message,
             chapter=chapter,
             chapter=chapter,
         )
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
 
     if args.command == "stats":
     if args.command == "stats":
         emit_success(manager.get_stats(), message="stats")
         emit_success(manager.get_stats(), message="stats")

+ 48 - 1
.claude/scripts/data_modules/observability.py

@@ -6,8 +6,11 @@ Shared observability helpers for data modules.
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import json
 import logging
 import logging
-from typing import Optional
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict, Optional
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -38,3 +41,47 @@ def safe_log_tool_call(
             tool_name,
             tool_name,
             exc,
             exc,
         )
         )
+
+
+def safe_append_perf_timing(
+    project_root: str | Path,
+    *,
+    tool_name: str,
+    success: bool,
+    elapsed_ms: int,
+    chapter: Optional[int] = None,
+    error_code: Optional[str] = None,
+    error_message: Optional[str] = None,
+    meta: Optional[Dict[str, Any]] = None,
+) -> None:
+    """
+    Append timing trace for profiling long-running data-agent pipeline steps.
+
+    Output path:
+    - {project_root}/.webnovel/observability/data_agent_timing.jsonl
+    """
+    try:
+        root = Path(project_root).resolve()
+        obs_dir = root / ".webnovel" / "observability"
+        obs_dir.mkdir(parents=True, exist_ok=True)
+        log_path = obs_dir / "data_agent_timing.jsonl"
+
+        payload: Dict[str, Any] = {
+            "timestamp": datetime.now().isoformat(),
+            "tool_name": tool_name,
+            "success": bool(success),
+            "elapsed_ms": int(max(0, elapsed_ms)),
+        }
+        if chapter is not None:
+            payload["chapter"] = int(chapter)
+        if error_code:
+            payload["error_code"] = error_code
+        if error_message:
+            payload["error_message"] = error_message
+        if meta:
+            payload["meta"] = meta
+
+        with open(log_path, "a", encoding="utf-8") as f:
+            f.write(json.dumps(payload, ensure_ascii=False) + "\n")
+    except Exception as exc:
+        logger.warning("failed to append perf timing for %s: %s", tool_name, exc)

+ 21 - 5
.claude/scripts/data_modules/rag_adapter.py

@@ -32,7 +32,7 @@ from .config import get_config
 from .api_client import get_client
 from .api_client import get_client
 from .index_manager import IndexManager
 from .index_manager import IndexManager
 from .query_router import QueryRouter
 from .query_router import QueryRouter
-from .observability import safe_log_tool_call
+from .observability import safe_append_perf_timing, safe_log_tool_call
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -1423,6 +1423,7 @@ def main():
     )
     )
 
 
     args = parser.parse_args()
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
 
     # 初始化
     # 初始化
     config = None
     config = None
@@ -1434,11 +1435,24 @@ def main():
     adapter = RAGAdapter(config)
     adapter = RAGAdapter(config)
     tool_name = f"rag_adapter:{args.command or 'unknown'}"
     tool_name = f"rag_adapter:{args.command or 'unknown'}"
 
 
-    def emit_success(data=None, message: str = "ok"):
+    def _append_timing(success: bool, *, error_code: str | None = None, error_message: str | None = None, chapter: int | None = None):
+        elapsed_ms = int((time.perf_counter() - command_started_at) * 1000)
+        safe_append_perf_timing(
+            adapter.config.project_root,
+            tool_name=tool_name,
+            success=success,
+            elapsed_ms=elapsed_ms,
+            chapter=chapter,
+            error_code=error_code,
+            error_message=error_message,
+        )
+
+    def emit_success(data=None, message: str = "ok", chapter: int | None = None):
         print_success(data, message=message)
         print_success(data, message=message)
         safe_log_tool_call(adapter.index_manager, tool_name=tool_name, success=True)
         safe_log_tool_call(adapter.index_manager, tool_name=tool_name, success=True)
+        _append_timing(True, chapter=chapter)
 
 
-    def emit_error(code: str, message: str, suggestion: str | None = None):
+    def emit_error(code: str, message: str, suggestion: str | None = None, chapter: int | None = None):
         print_error(code, message, suggestion=suggestion)
         print_error(code, message, suggestion=suggestion)
         safe_log_tool_call(
         safe_log_tool_call(
             adapter.index_manager,
             adapter.index_manager,
@@ -1447,6 +1461,7 @@ def main():
             error_code=code,
             error_code=code,
             error_message=message,
             error_message=message,
         )
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
 
     if args.command == "stats":
     if args.command == "stats":
         stats = adapter.get_stats()
         stats = adapter.get_stats()
@@ -1496,9 +1511,9 @@ def main():
         skipped = len(chunks) - stored
         skipped = len(chunks) - stored
         result = {"stored": stored, "skipped": skipped, "total": len(chunks)}
         result = {"stored": stored, "skipped": skipped, "total": len(chunks)}
         if skipped > 0:
         if skipped > 0:
-            emit_success(result, message="indexed_with_warnings")
+            emit_success(result, message="indexed_with_warnings", chapter=args.chapter)
         else:
         else:
-            emit_success(result, message="indexed")
+            emit_success(result, message="indexed", chapter=args.chapter)
 
 
     elif args.command == "search":
     elif args.command == "search":
         center_entities: List[str] | None = None
         center_entities: List[str] | None = None
@@ -1546,6 +1561,7 @@ def main():
             warnings = [{"code": "DEGRADED_MODE", "reason": degraded_reason}]
             warnings = [{"code": "DEGRADED_MODE", "reason": degraded_reason}]
             print_success(payload, message="search_results", warnings=warnings)
             print_success(payload, message="search_results", warnings=warnings)
             safe_log_tool_call(adapter.index_manager, tool_name=tool_name, success=True)
             safe_log_tool_call(adapter.index_manager, tool_name=tool_name, success=True)
+            _append_timing(True)
         else:
         else:
             emit_success(payload, message="search_results")
             emit_success(payload, message="search_results")
 
 

+ 20 - 4
.claude/scripts/data_modules/state_manager.py

@@ -16,6 +16,7 @@ v5.1 变更(v5.4 沿用):
 import json
 import json
 import logging
 import logging
 import sys
 import sys
+import time
 from copy import deepcopy
 from copy import deepcopy
 from pathlib import Path
 from pathlib import Path
 
 
@@ -26,7 +27,7 @@ from datetime import datetime
 import filelock
 import filelock
 
 
 from .config import get_config
 from .config import get_config
-from .observability import safe_log_tool_call
+from .observability import safe_append_perf_timing, safe_log_tool_call
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -1234,6 +1235,7 @@ def main():
     process_parser.add_argument("--data", required=True, help="JSON 格式的处理结果")
     process_parser.add_argument("--data", required=True, help="JSON 格式的处理结果")
 
 
     args = parser.parse_args()
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
 
     # 初始化
     # 初始化
     config = None
     config = None
@@ -1245,11 +1247,24 @@ def main():
     logger = IndexManager(config)
     logger = IndexManager(config)
     tool_name = f"state_manager:{args.command or 'unknown'}"
     tool_name = f"state_manager:{args.command or 'unknown'}"
 
 
-    def emit_success(data=None, message: str = "ok"):
+    def _append_timing(success: bool, *, error_code: str | None = None, error_message: str | None = None, chapter: int | None = None):
+        elapsed_ms = int((time.perf_counter() - command_started_at) * 1000)
+        safe_append_perf_timing(
+            manager.config.project_root,
+            tool_name=tool_name,
+            success=success,
+            elapsed_ms=elapsed_ms,
+            chapter=chapter,
+            error_code=error_code,
+            error_message=error_message,
+        )
+
+    def emit_success(data=None, message: str = "ok", chapter: int | None = None):
         print_success(data, message=message)
         print_success(data, message=message)
         safe_log_tool_call(logger, tool_name=tool_name, success=True)
         safe_log_tool_call(logger, tool_name=tool_name, success=True)
+        _append_timing(True, chapter=chapter)
 
 
-    def emit_error(code: str, message: str, suggestion: str | None = None):
+    def emit_error(code: str, message: str, suggestion: str | None = None, chapter: int | None = None):
         print_error(code, message, suggestion=suggestion)
         print_error(code, message, suggestion=suggestion)
         safe_log_tool_call(
         safe_log_tool_call(
             logger,
             logger,
@@ -1258,6 +1273,7 @@ def main():
             error_code=code,
             error_code=code,
             error_message=message,
             error_message=message,
         )
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
 
     if args.command == "get-progress":
     if args.command == "get-progress":
         emit_success(manager._state.get("progress", {}), message="progress")
         emit_success(manager._state.get("progress", {}), message="progress")
@@ -1303,7 +1319,7 @@ def main():
 
 
         warnings = manager.process_chapter_result(args.chapter, validated.model_dump(by_alias=True))
         warnings = manager.process_chapter_result(args.chapter, validated.model_dump(by_alias=True))
         manager.save_state()
         manager.save_state()
-        emit_success({"chapter": args.chapter, "warnings": warnings}, message="chapter_processed")
+        emit_success({"chapter": args.chapter, "warnings": warnings}, message="chapter_processed", chapter=args.chapter)
 
 
     else:
     else:
         emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")
         emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")

+ 20 - 4
.claude/scripts/data_modules/style_sampler.py

@@ -11,6 +11,7 @@ Style Sampler - 风格样本管理模块
 
 
 import json
 import json
 import sqlite3
 import sqlite3
+import time
 from pathlib import Path
 from pathlib import Path
 from typing import Dict, List, Optional, Any
 from typing import Dict, List, Optional, Any
 from dataclasses import dataclass, asdict
 from dataclasses import dataclass, asdict
@@ -19,7 +20,7 @@ from enum import Enum
 from contextlib import contextmanager
 from contextlib import contextmanager
 
 
 from .config import get_config
 from .config import get_config
-from .observability import safe_log_tool_call
+from .observability import safe_append_perf_timing, safe_log_tool_call
 
 
 
 
 class SceneType(Enum):
 class SceneType(Enum):
@@ -337,6 +338,7 @@ def main():
     select_parser.add_argument("--max", type=int, default=3)
     select_parser.add_argument("--max", type=int, default=3)
 
 
     args = parser.parse_args()
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
 
     # 初始化
     # 初始化
     config = None
     config = None
@@ -348,11 +350,24 @@ def main():
     logger = IndexManager(config)
     logger = IndexManager(config)
     tool_name = f"style_sampler:{args.command or 'unknown'}"
     tool_name = f"style_sampler:{args.command or 'unknown'}"
 
 
-    def emit_success(data=None, message: str = "ok"):
+    def _append_timing(success: bool, *, error_code: str | None = None, error_message: str | None = None, chapter: int | None = None):
+        elapsed_ms = int((time.perf_counter() - command_started_at) * 1000)
+        safe_append_perf_timing(
+            sampler.config.project_root,
+            tool_name=tool_name,
+            success=success,
+            elapsed_ms=elapsed_ms,
+            chapter=chapter,
+            error_code=error_code,
+            error_message=error_message,
+        )
+
+    def emit_success(data=None, message: str = "ok", chapter: int | None = None):
         print_success(data, message=message)
         print_success(data, message=message)
         safe_log_tool_call(logger, tool_name=tool_name, success=True)
         safe_log_tool_call(logger, tool_name=tool_name, success=True)
+        _append_timing(True, chapter=chapter)
 
 
-    def emit_error(code: str, message: str, suggestion: str | None = None):
+    def emit_error(code: str, message: str, suggestion: str | None = None, chapter: int | None = None):
         print_error(code, message, suggestion=suggestion)
         print_error(code, message, suggestion=suggestion)
         safe_log_tool_call(
         safe_log_tool_call(
             logger,
             logger,
@@ -361,6 +376,7 @@ def main():
             error_code=code,
             error_code=code,
             error_message=message,
             error_message=message,
         )
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
 
     if args.command == "stats":
     if args.command == "stats":
         stats = sampler.get_stats()
         stats = sampler.get_stats()
@@ -389,7 +405,7 @@ def main():
                 added.append(c.id)
                 added.append(c.id)
             else:
             else:
                 skipped.append(c.id)
                 skipped.append(c.id)
-        emit_success({"added": added, "skipped": skipped}, message="extracted")
+        emit_success({"added": added, "skipped": skipped}, message="extracted", chapter=args.chapter)
 
 
     elif args.command == "select":
     elif args.command == "select":
         samples = sampler.select_samples_for_chapter(args.outline, max_samples=args.max)
         samples = sampler.select_samples_for_chapter(args.outline, max_samples=args.max)

+ 159 - 161
.claude/skills/webnovel-write/references/polish-guide.md

@@ -1,107 +1,60 @@
 ---
 ---
 name: polish-guide
 name: polish-guide
 purpose: 章节生成后的润色阶段加载,基于审查报告修复问题 + 强化网文口感
 purpose: 章节生成后的润色阶段加载,基于审查报告修复问题 + 强化网文口感
-version: "5.4"
+version: "6.0"
 ---
 ---
 
 
 <context>
 <context>
-此文件用于内容润色,v5.2 引入"网文口感 + 追读力"重点,v5.4 沿用
+此文件用于 Step 4 润色阶段,目标是“修问题”而非“重写剧情”
 
 
-润色步骤接收两个输入:
-1. 章节正文
-2. 审查报告(来自 4-6 个 checker agents,默认 4 个,关键章可扩展到 6 个
+输入来自两部分
+1. 章节正文(Step 2A/2B 输出)
+2. 审查报告(Step 3 聚合结果
 
 
-**与 Step 2B (style-adapter) 的职责边界**:
-- **Step 2B**: 纯风格转换 —— 句式改写、抽象→具体、长句拆分
-- **Step 4 (本步骤)**: 问题修复 —— 基于审查报告修复问题 + AI痕迹检测 + 规则校验
+与 Step 2B 的职责边界:
+- Step 2B:风格转译(表达层)
+- Step 4:问题修复(质量层),包括审查问题修复、Anti-AI 终检、毒点规避
 
 
-> 注意:如果已执行 Step 2B,本步骤不需要重复句式转换,专注于修复审查问题
+若已执行 Step 2B,本步骤不重复全量句式改写,只做“必要修改”
 </context>
 </context>
 
 
 <instructions>
 <instructions>
 
 
-## v5.2 引入(v5.4 沿用):基于审查报告修复
-
-### 输入格式
+## 1. 输入契约
 
 
 ```json
 ```json
 {
 {
-  "overall_score": 85,
+  "chapter_file": "正文/第0123章.md",
+  "overall_score": 82,
   "issues": [
   "issues": [
-    {"agent": "ooc-checker", "type": "OOC", "severity": "high", "location": "第3段", "suggestion": "林天对敌人太客气,应更冷酷"},
-    {"agent": "consistency-checker", "type": "POWER_CONFLICT", "severity": "critical", "location": "第5段", "suggestion": "筑基3层不能使用金丹期技能"}
+    {"agent": "consistency-checker", "type": "POWER_CONFLICT", "severity": "critical", "location": "第6段", "suggestion": "境界越权"},
+    {"agent": "ooc-checker", "type": "OOC", "severity": "high", "location": "第9段", "suggestion": "角色口吻偏离"}
   ],
   ],
-  "pacing_analysis": {
-    "quest_ratio": 0.4,
-    "fire_ratio": 0.35,
-    "constellation_ratio": 0.25
-  },
   "pass": true
   "pass": true
 }
 }
 ```
 ```
 
 
-### 问题修复优先级
-
-| 严重度 | 处理方式 |
-|-------|---------|
-| critical | **必须修复**,否则记录 deviation |
-| high | 优先修复 |
-| medium | 建议修复 |
-| low | 可选修复 |
-
-### 问题类型修复指南
-
-| 问题类型 | 修复方法 |
-|---------|---------|
-| **OOC** (人物失真) | 调整角色言行,使其符合人设;若需要OOC,补充触发原因 |
-| **POWER_CONFLICT** (战力冲突) | 修改能力描述,使用当前境界可用的技能/能力 |
-| **TIMELINE_ISSUE** (时间线问题) | 调整时间描述,补充时间流逝过渡 |
-| **LOCATION_ERROR** (地点跳跃) | 补充移动过程描述,或调整地点设定 |
-| **PACING_IMBALANCE** (节奏失衡) | 调整 Strand 比例,增加缺失的内容类型 |
-| **LOW_COOL_POINTS** (爽点不足) | 增加爽点密度,补充打脸/升级/收获等元素 |
-| **CONTINUITY_BREAK** (连贯断裂) | 补充过渡句,连接断裂的场景 |
-
----
-
-## 网文化规则(分层)
-
-### Hard(必须)
-
-1. **保证可读推进**:本章至少有 1 个明确推进点(信息/行动/关系/局势)。
-2. **对话可判定意图**:关键对话应能看出试探/施压/回避/诱导等目的。
-3. **抽象判断句 → 动作/反应/代价**(把结论写成行为)。
-
-### Soft(建议)
-
-1. 开头尽早进入冲突/风险/强情绪(建议前 200-400 字)。
-2. 局面变化保持脉冲感(参考 800-1400 字一个脉冲,短章至少一次)。
-3. 未闭合问题/下一章期待锚点放在后段或章末(不限定 80-150 字窗口)。
+## 2. 执行顺序(必须按序)
 
 
-### Style(可选强化)
+1. 修复审查报告中的问题(先 `critical/high`)
+2. 校验网文化 Hard/Soft/Style 规则
+3. 执行 Phase 1 Anti-AI 终检并改写
+4. 执行 No-Poison 毒点规避检查
+5. 输出润色结果与 deviation(若有)
 
 
-1. 避免连续大段纯解释,优先“信息 + 动作/反应”交替。
-2. 允许平缓收尾,但避免“回去休息了”式机械截断。
+## 2A. Anti-AI 检测细则(对应执行顺序第 3 步)
 
 
-**章长适配**:
-- 标准章(3000-5000字):执行全部 Hard + 尽量满足 Soft。
-- 短章/过场章(<2000字):
-  - 冲突导入可放宽到前 400 字。
-  - 可采用低强度期待锚点(weak)。
-  - 允许降低微兑现密度,但不允许“整章无推进”。
-
----
-
-## AI 痕迹检测(辅助提醒)
-
-> 词频统计**仅作为提醒**,不再作为硬性门槛。若明显超标,需修复并简要说明。
+> 词频统计**仅作为提醒**,不再作为硬性门槛。若明显超标,需修复并简要说明。  
+> 说明:即使词频仅作提醒,若最终文本仍有明显 AI 模板痕迹,`anti_ai_force_check` 仍应判定为 `fail`。
+> 本节为执行摘要;完整 7 层规则与高频词库见下方「Phase 1 增补」章节,两节配套执行。
 
 
 ### 常见AI痕迹
 ### 常见AI痕迹
 
 
 | 特征 | 表现 |
 | 特征 | 表现 |
 |------|------|
 |------|------|
 | 句式规整 | 句子长度相近,结构相似 |
 | 句式规整 | 句子长度相近,结构相似 |
-| 模式化词汇 | 首先、其次、最后值得注意的是 |
-| 过度连接 | 大量使用然而、因此、此外 |
+| 模式化词汇 | 首先、其次、最后、值得注意的是 |
+| 过度连接 | 大量使用然而、因此、此外 |
 | 情感平淡 | 缺乏个性化情绪表达 |
 | 情感平淡 | 缺乏个性化情绪表达 |
 | 信息过密 | 每句都有信息,缺少留白 |
 | 信息过密 | 每句都有信息,缺少留白 |
 
 
@@ -121,80 +74,30 @@ version: "5.4"
 | 指标 | 不达标 | 达标 |
 | 指标 | 不达标 | 达标 |
 |-----|-------|------|
 |-----|-------|------|
 | 停顿词 | < 0.5次/500字 | 1-2次/500字 |
 | 停顿词 | < 0.5次/500字 | 1-2次/500字 |
-| 不确定表达 | 0次 |  2次/章 |
+| 不确定表达 | 0次 | >= 2次/章 |
 | 短句占比 | < 20% | 30-50% |
 | 短句占比 | < 20% | 30-50% |
-| 口语词 | 0次/1000字 |  2次/1000字 |
+| 口语词 | 0次/1000字 | >= 2次/1000字 |
 
 
 > **提示**:自然化不是越多越好,超量会显得刻意。
 > **提示**:自然化不是越多越好,超量会显得刻意。
 
 
----
-
-## 润色红线
-
-- 改变情节走向 → 违反“**大纲即法律**”
-- 修改主角实力(除非修复 POWER_CONFLICT)→ 违反“**设定即物理**”
-- 改变人物关系 → 违反设定
-- 删除伏笔 → 破坏长线剧情
-
----
-
-## 风格一致性检查
+### 强制执行协议(新增)
 
 
-### 视角一致
-- 第一人称:全程“我”视角
-- 第三人称限制:只描写主角能感知的
-- 第三人称全知:可以切换视角
+- 必须完整执行本节“7层规则 + 高频词库 + 终检清单”,禁止裁剪为“建议项”。
+- 必须按全文逐段检查执行,禁止抽样检查。
+- Step 4 完成后必须输出 `anti_ai_force_check: pass/fail`。
+- 若结果为 `fail`,必须继续留在 Step 4 重写,不得进入 Step 5。
+- 若命中高风险表达但因剧情必要保留,必须写入 `deviation`(位置 + 原因 + 代价)。
 
 
-### 时态一致
-- 过去时为主
-- 避免时态混乱
+### 全文检查范围(必查)
 
 
-### 文风一致
-- 轻松吐槽风 vs 正剧严肃风
-- 热血燃向 vs 冷静克制
-- 保持全文统一
-
-</instructions>
-
-<examples>
-
-<example>
-<input>AI风格:综合以上分析,林天认为自己需要做三件事:第一,提升修为;第二,获取资源;第三,寻找盟友。</input>
-<output>林天心里盘算着,修为是根本,资源也不能少,至于盟友……先走一步看一步吧。</output>
-</example>
-
-<example>
-<input>AI风格:首先,他查看了周围环境。其次,他分析了敌人的位置。最后,他制定了突围计划。</input>
-<output>他扫了一眼四周——三面是墙,只有正前方有条路。敌人嘛,两个在左,一个在右。得想个法子冲出去才行。</output>
-</example>
-
-<example>
-<input>AI风格:他感到非常愤怒,同时又有些无奈,心中五味杂陈。</input>
-<output>他的拳头攥紧了,指节发白,半晌又松开,无力地垂下。</output>
-</example>
-
-</examples>
-
-<errors>
-❌ 忽略审查报告的 critical 问题 → ✅ 必须修复或记录 deviation
-❌ 整章无推进点(无信息/行动/关系/局势变化) → ✅ 补至少一项推进
-❌ 关键对话无意图、纯说明书宣讲 → ✅ 改为带目标冲突的对白
-❌ 连续大段纯解释压节奏 → ✅ 打散并转化为动作或对白
-</errors>
-
-<checklist>
-润色完成前检查:
-- [ ] 审查报告中的 critical 问题已修复
-- [ ] 审查报告中的 high 问题已修复或有合理解释
-- [ ] Hard 规则已通过,Soft 规则已校验
-- [ ] AI 痕迹提示已检查
-- [ ] 未违反润色红线
-- [ ] 风格一致性检查通过
-</checklist>
+- 全文章节逐段检查
+- 全部对白逐轮检查
+- 章首/章中/章末都必须通过 Anti-AI 规则
+- 任何命中项都必须改写或记录 deviation
 
 
 ---
 ---
 
 
-## Phase 1 增补:Anti-AI 规范(7层)
+## Phase 1 增补:Anti-AI 规范(7层,原版)
 
 
 > 目标:降低模板腔、说明腔、机械腔。  
 > 目标:降低模板腔、说明腔、机械腔。  
 > 原则:优先“可读性与人物真实”,不是机械替换每个词。
 > 原则:优先“可读性与人物真实”,不是机械替换每个词。
@@ -278,41 +181,136 @@ version: "5.4"
 - 省略号、感叹号每段最多 1 次(特殊情绪爆点除外)。
 - 省略号、感叹号每段最多 1 次(特殊情绪爆点除外)。
 - 长句中逗号超过 4 个时,优先拆句。
 - 长句中逗号超过 4 个时,优先拆句。
 
 
----
+### Anti-AI 改写算法(执行动作)
+
+1. 抽象情绪句 → `生理反应 + 当下意图 + 下一动作`
+2. 结论句 → `事实细节 + 代价/风险 + 决策`
+3. 连续说明句(>=3)→ 改成“对白/动作/反问”混排
+4. 连续同构句(>=3)→ 至少打断 1 句为短句或插入动作锚点
+5. 对话整段解释背景 → 改为“带意图的对抗式对白”
 
 
-## Phase 1 增补:No-Poison 毒点规避(5类)
+### 通过阈值(手工判定
 
 
-### 1) 降智推进
+- 全文不得保留未处理的三段式枚举模板句。
+- 全文命中的高风险表达均已改写或记录 deviation。
+- 至少完成 3 处“抽象句→行为句”改写(全文范围内统计)。
+- 章末存在可感知的下一步压力或未闭合问题。
+- 若仍保留高风险表达,需在 deviation 说明保留理由。
 
 
-- 红线:为推进剧情强行让核心角色失智、失忆常识、无视已知信息。
-- 修复:补足信息差来源,或改为“资源不足/时间压力”导致的有限最优。
+### Phase 1 终检(原版保留)
 
 
-### 2) 强行误会
+- [ ] 高风险词汇已做全文检查与必要替换。
+- [ ] 未出现三段式说明句(首先/其次/最后)。
+- [ ] 对话存在真实冲突意图,不是信息宣讲。
+- [ ] No-Poison 毒点红线检查见第 5 节(本清单仅覆盖 Anti-AI)。
+- [ ] 修改后未破坏“设定即物理 / 大纲即法律”。
 
 
-- 红线:角色明明可一句话说清,却强行拖十几章误会。
-- 修复:误会必须具备“说不清/不敢说/不能说”的真实阻力,并限章回收。
+## 3. 审查问题修复优先级
 
 
-### 3) 圣母无底线
+| 严重度 | 处理规则 |
+|---|---|
+| critical | 必须修复;无法修复必须记录 deviation 与原因 |
+| high | 必须优先处理;无法修复记录 deviation |
+| medium | 视篇幅和收益处理 |
+| low | 可择优处理 |
 
 
-- 红线:主角无限原谅高危反派,且无成本。
-- 修复:给出明确边界与代价,必要时让“原谅”转化为“控制/利用/审判”。
+类型对应修复动作:
+- `OOC`:恢复角色话术、风险偏好、决策边界
+- `POWER_CONFLICT`:能力回落到合法境界,或补出“获得路径+代价”
+- `TIMELINE_ISSUE`:补足时间流逝锚点
+- `LOCATION_ERROR`:补移动过程与空间锚点
+- `PACING_IMBALANCE`:增加缺失的推进事件或删冗余说明段
+- `CONTINUITY_BREAK`:补衔接句与过渡动作
 
 
-### 4) 工具人配角
+## 4. 网文化分层规则
 
 
-- 红线:配角只在需要时出现,完成功能后立刻消失且无动机。
-- 修复:补配角目标与收益损失,让其有独立行动逻辑。
+### Hard(必须满足)
 
 
-### 5) 双标裁决
+1. 本章至少有 1 个明确推进点(信息/行动/关系/局势至少一项)。
+2. 关键对话可判定意图(试探/施压/回避/诱导至少一种)。
+3. 抽象判断句改为“动作/反应/代价”表达。
 
 
-- 红线:主角做同类行为被美化,配角做同类行为被妖魔化,且无叙事解释。
-- 修复:统一评价标尺,或在文本中明确立场偏见来源与代价。
+### Soft(建议满足)
 
 
----
+1. 章首建议在前 200-400 字进入冲突/风险/强情绪。
+2. 章节中段有节奏脉冲(参考 800-1400 字一波,短章至少一次)。
+3. 后段或章末保留未闭合问题或下一步期待锚点。
 
 
-## Phase 1 终检(新增)
+### Style(按章型择优
 
 
-- [ ] 高风险词汇已做抽样替换(至少覆盖章首/章中/章末各1处)。
-- [ ] 未出现三段式说明句(首先/其次/最后)。
-- [ ] 对话存在真实冲突意图,不是信息宣讲。
-- [ ] 未触发 5 类毒点红线;如有争议点,已补叙事理由与代价。
-- [ ] 修改后未破坏“设定即物理 / 大纲即法律”。
+1. 避免连续大段纯解释,优先“信息+动作/反应”交替。
+2. 允许平缓收尾,但不能机械截断。
+
+## 5. No-Poison 毒点规避(5类)
+
+1. 降智推进:角色忽略常识仅为推进剧情服务。
+2. 强行误会:可一句话说清却长期拖延。
+3. 圣母无代价:无边界原谅高风险对象。
+4. 工具人配角:只在功能节点出现,没有独立动机。
+5. 双标裁决:同类行为评价标准不一致且无叙事解释。
+
+命中任一毒点时,必须补“动机/阻力/代价”中的至少两项。
+
+## 6. 润色红线(不可突破)
+
+- 不改剧情走向(大纲即法律)
+- 不改设定物理边界(设定即物理)
+- 不删除关键伏笔
+- 不强行改写角色关系基线
+
+## 7. 输出格式(润色完成后)
+
+必须输出:
+1. 润色后的章节正文
+2. 修复摘要(建议结构)
+
+```text
+[Polish Report]
+- critical_fixed: N
+- high_fixed: N
+- medium_low_fixed: N
+- anti_ai_rewrites: N
+- anti_ai_force_check: pass/fail
+- poison_risk: pass/fail
+- deviations:
+  - {location}: {reason}
+```
+
+若 `critical` 未清零,必须显式标注“未通过”,并返回 Step 4 继续修复。
+
+</instructions>
+
+<examples>
+
+<example>
+<input>AI风格:首先,他要拿到证据;其次,他要稳住队友;最后,他再公开反击。</input>
+<output>他先把证据攥在手里,转身去找队友。人得先稳住,反击才能一刀见血。</output>
+</example>
+
+<example>
+<input>AI风格:他非常愤怒,内心五味杂陈。</input>
+<output>他指节捏得发白,喉咙里压着一口气。再等一秒,他就会动手。</output>
+</example>
+
+</examples>
+
+<errors>
+❌ 跳过 critical/high 直接做文风微调
+✅ 先清问题,再做风格
+
+❌ 只替换高风险词,不改句群结构
+✅ 按“抽象句→行为句”重写句群
+
+❌ 为去 AI 味而改动剧情事实
+✅ 只改表达,不改事件结果
+</errors>
+
+<checklist>
+- [ ] `critical` 已修复或记录 deviation
+- [ ] `high` 已修复或记录 deviation
+- [ ] Hard 规则全部通过
+- [ ] Phase 1 Anti-AI 全文检查已通过
+- [ ] `anti_ai_force_check=pass`
+- [ ] No-Poison 五类毒点已检查
+- [ ] 未触碰润色红线
+</checklist>

+ 7 - 3
.claude/skills/webnovel-write/references/style-adapter.md

@@ -16,6 +16,9 @@ Step 2B 专用提示词,将粗稿改写为网文风格。
 - 网文化正文(字数 ±10%)
 - 网文化正文(字数 ±10%)
 - 改写日志(记录主要调整)
 - 改写日志(记录主要调整)
 
 
+默认字数目标:
+- 常规章节默认 2000-2500 字(除非大纲或用户另有指定)。
+
 ## 禁改项(红线)
 ## 禁改项(红线)
 - ❌ 剧情走向
 - ❌ 剧情走向
 - ❌ 事件顺序
 - ❌ 事件顺序
@@ -57,9 +60,10 @@ Step 2B 专用提示词,将粗稿改写为网文风格。
 | 长句比例 | >40字的句子 <10% | 拆分长句 |
 | 长句比例 | >40字的句子 <10% | 拆分长句 |
 | 解释段 | 无连续>200字的纯解释 | 打散或删除 |
 | 解释段 | 无连续>200字的纯解释 | 打散或删除 |
 
 
-## 章长适配
-- **标准章(3000-5000字)**: 执行全部硬约束 + 软建议优先落实
-- **短章/过场章(<2000字)**:
+## 章节类型适配(按大纲,不按字数)
+- **常规推进章(默认 2000-2500字)**: 执行全部硬约束 + 软建议优先落实
+- **过渡章(按大纲判定)**:
   - 冲突导入可放宽到前 400 字
   - 冲突导入可放宽到前 400 字
   - 微兑现可降为 0-1 次,但仍需 1 个明确推进点
   - 微兑现可降为 0-1 次,但仍需 1 个明确推进点
   - 期待锚点强度可为 weak
   - 期待锚点强度可为 weak
+  - 过渡章身份由章纲/卷纲决定,禁止用字数阈值判定