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

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

lingfengQAQ 3 месяцев назад
Родитель
Сommit
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'`
 - 记录利息事件到 `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
 {
@@ -205,7 +222,24 @@ python -m data_modules.index_manager accrue-interest --current-chapter {chapter}
   "warnings": [
     "中置信度匹配: 那位前辈 → 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 json
+import time
 from pathlib import Path
 
 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_reading_mixin import IndexReadingMixin
 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
@@ -864,6 +865,7 @@ def main():
     )
 
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
     # 初始化
     config = None
@@ -875,9 +877,28 @@ def main():
     manager = IndexManager(config)
     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):
         print_success(data, message=message)
         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):
         print_error(code, message, suggestion=suggestion)
@@ -889,6 +910,7 @@ def main():
             error_message=message,
             chapter=chapter,
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
     if args.command == "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
 
+import json
 import logging
-from typing import Optional
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict, Optional
 
 
 logger = logging.getLogger(__name__)
@@ -38,3 +41,47 @@ def safe_log_tool_call(
             tool_name,
             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 .index_manager import IndexManager
 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__)
@@ -1423,6 +1423,7 @@ def main():
     )
 
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
     # 初始化
     config = None
@@ -1434,11 +1435,24 @@ def main():
     adapter = RAGAdapter(config)
     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)
         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)
         safe_log_tool_call(
             adapter.index_manager,
@@ -1447,6 +1461,7 @@ def main():
             error_code=code,
             error_message=message,
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
     if args.command == "stats":
         stats = adapter.get_stats()
@@ -1496,9 +1511,9 @@ def main():
         skipped = len(chunks) - stored
         result = {"stored": stored, "skipped": skipped, "total": len(chunks)}
         if skipped > 0:
-            emit_success(result, message="indexed_with_warnings")
+            emit_success(result, message="indexed_with_warnings", chapter=args.chapter)
         else:
-            emit_success(result, message="indexed")
+            emit_success(result, message="indexed", chapter=args.chapter)
 
     elif args.command == "search":
         center_entities: List[str] | None = None
@@ -1546,6 +1561,7 @@ def main():
             warnings = [{"code": "DEGRADED_MODE", "reason": degraded_reason}]
             print_success(payload, message="search_results", warnings=warnings)
             safe_log_tool_call(adapter.index_manager, tool_name=tool_name, success=True)
+            _append_timing(True)
         else:
             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 logging
 import sys
+import time
 from copy import deepcopy
 from pathlib import Path
 
@@ -26,7 +27,7 @@ from datetime import datetime
 import filelock
 
 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__)
@@ -1234,6 +1235,7 @@ def main():
     process_parser.add_argument("--data", required=True, help="JSON 格式的处理结果")
 
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
     # 初始化
     config = None
@@ -1245,11 +1247,24 @@ def main():
     logger = IndexManager(config)
     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)
         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)
         safe_log_tool_call(
             logger,
@@ -1258,6 +1273,7 @@ def main():
             error_code=code,
             error_message=message,
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
     if args.command == "get-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))
         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:
         emit_error("UNKNOWN_COMMAND", "未指定有效命令", suggestion="请查看 --help")

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

@@ -11,6 +11,7 @@ Style Sampler - 风格样本管理模块
 
 import json
 import sqlite3
+import time
 from pathlib import Path
 from typing import Dict, List, Optional, Any
 from dataclasses import dataclass, asdict
@@ -19,7 +20,7 @@ from enum import Enum
 from contextlib import contextmanager
 
 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):
@@ -337,6 +338,7 @@ def main():
     select_parser.add_argument("--max", type=int, default=3)
 
     args = parser.parse_args()
+    command_started_at = time.perf_counter()
 
     # 初始化
     config = None
@@ -348,11 +350,24 @@ def main():
     logger = IndexManager(config)
     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)
         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)
         safe_log_tool_call(
             logger,
@@ -361,6 +376,7 @@ def main():
             error_code=code,
             error_message=message,
         )
+        _append_timing(False, error_code=code, error_message=message, chapter=chapter)
 
     if args.command == "stats":
         stats = sampler.get_stats()
@@ -389,7 +405,7 @@ def main():
                 added.append(c.id)
             else:
                 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":
         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
 purpose: 章节生成后的润色阶段加载,基于审查报告修复问题 + 强化网文口感
-version: "5.4"
+version: "6.0"
 ---
 
 <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>
 
 <instructions>
 
-## v5.2 引入(v5.4 沿用):基于审查报告修复
-
-### 输入格式
+## 1. 输入契约
 
 ```json
 {
-  "overall_score": 85,
+  "chapter_file": "正文/第0123章.md",
+  "overall_score": 82,
   "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
 }
 ```
 
-### 问题修复优先级
-
-| 严重度 | 处理方式 |
-|-------|---------|
-| 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痕迹
 
 | 特征 | 表现 |
 |------|------|
 | 句式规整 | 句子长度相近,结构相似 |
-| 模式化词汇 | 首先、其次、最后值得注意的是 |
-| 过度连接 | 大量使用然而、因此、此外 |
+| 模式化词汇 | 首先、其次、最后、值得注意的是 |
+| 过度连接 | 大量使用然而、因此、此外 |
 | 情感平淡 | 缺乏个性化情绪表达 |
 | 信息过密 | 每句都有信息,缺少留白 |
 
@@ -121,80 +74,30 @@ version: "5.4"
 | 指标 | 不达标 | 达标 |
 |-----|-------|------|
 | 停顿词 | < 0.5次/500字 | 1-2次/500字 |
-| 不确定表达 | 0次 |  2次/章 |
+| 不确定表达 | 0次 | >= 2次/章 |
 | 短句占比 | < 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 次(特殊情绪爆点除外)。
 - 长句中逗号超过 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%)
 - 改写日志(记录主要调整)
 
+默认字数目标:
+- 常规章节默认 2000-2500 字(除非大纲或用户另有指定)。
+
 ## 禁改项(红线)
 - ❌ 剧情走向
 - ❌ 事件顺序
@@ -57,9 +60,10 @@ Step 2B 专用提示词,将粗稿改写为网文风格。
 | 长句比例 | >40字的句子 <10% | 拆分长句 |
 | 解释段 | 无连续>200字的纯解释 | 打散或删除 |
 
-## 章长适配
-- **标准章(3000-5000字)**: 执行全部硬约束 + 软建议优先落实
-- **短章/过场章(<2000字)**:
+## 章节类型适配(按大纲,不按字数)
+- **常规推进章(默认 2000-2500字)**: 执行全部硬约束 + 软建议优先落实
+- **过渡章(按大纲判定)**:
   - 冲突导入可放宽到前 400 字
   - 微兑现可降为 0-1 次,但仍需 1 个明确推进点
   - 期待锚点强度可为 weak
+  - 过渡章身份由章纲/卷纲决定,禁止用字数阈值判定