feat: initial commit
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
"""
|
||||
catchup_agent.py - NovelMaster Catchup Agent
|
||||
|
||||
负责计算休眠角色跨越时间后的状态。
|
||||
利用脏标记 (Dirty Flag) 订阅机制,当休眠人物被唤醒时,
|
||||
进行惰性时间流逝推演。
|
||||
|
||||
This agent is part of the NovelMaster (noma) system for noma writing.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class CatchupResult:
|
||||
"""catchup_agent 输出结果"""
|
||||
agent: str = "catchup_agent"
|
||||
awakened_characters: List[Dict[str, Any]] = None
|
||||
state_updates: List[Dict[str, Any]] = None
|
||||
timeline_changes: List[Dict[str, Any]] = None
|
||||
warnings: List[str] = None
|
||||
pass_count: int = 0
|
||||
fail_count: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
if self.awakened_characters is None:
|
||||
self.awakened_characters = []
|
||||
if self.state_updates is None:
|
||||
self.state_updates = []
|
||||
if self.timeline_changes is None:
|
||||
self.timeline_changes = []
|
||||
if self.warnings is None:
|
||||
self.warnings = []
|
||||
|
||||
|
||||
class CatchupAgent:
|
||||
"""
|
||||
负责休眠角色的状态 catchup(追赶上当前时间线)。
|
||||
|
||||
触发条件:
|
||||
- 休眠角色被唤醒(出现在新章节中)
|
||||
- 角色从上一次出场到当前章节有较大时间跨度
|
||||
- 手动触发 retcon(设定修改)
|
||||
|
||||
核心逻辑:
|
||||
1. 检测休眠角色(长时间未出场的角色)
|
||||
2. 计算时间跨度(从上次出场到当前章节)
|
||||
3. 根据时间跨度推演角色状态变化
|
||||
4. 检查脏标记 (Dirty Flag),如果有 retcon 影响则应用补丁
|
||||
5. 输出状态更新到 state.json 和 ledger
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: str, storage_path: str = ".noma/novel_data/"):
|
||||
self.project_root = Path(project_root)
|
||||
self.storage_path = self.project_root / storage_path
|
||||
self.state_file = self.storage_path / "state.json"
|
||||
self.index_db = self.storage_path / "index.db"
|
||||
|
||||
def execute(
|
||||
self,
|
||||
current_chapter: int,
|
||||
awakened_chars: Optional[List[str]] = None
|
||||
) -> CatchupResult:
|
||||
"""
|
||||
执行 catchup 计算。
|
||||
|
||||
Args:
|
||||
current_chapter: 当前章节编号
|
||||
awakened_chars: 被唤醒的角色 ID 列表(如果为 None,则自动检测)
|
||||
|
||||
Returns:
|
||||
CatchupResult: 包含所有状态更新的结果
|
||||
"""
|
||||
# 1. 加载 state.json
|
||||
state = self._load_state()
|
||||
|
||||
# 2. 加载角色最后出场信息
|
||||
character_last_appearance = self._get_character_last_appearance()
|
||||
|
||||
# 3. 确定需要 catchup 的角色
|
||||
if awakened_chars is None:
|
||||
awakened_chars = self._detect_awakened_characters(
|
||||
current_chapter,
|
||||
character_last_appearance,
|
||||
state
|
||||
)
|
||||
|
||||
# 4. 计算每个角色的状态更新
|
||||
state_updates = []
|
||||
timeline_changes = []
|
||||
warnings = []
|
||||
|
||||
for char_id in awakened_chars:
|
||||
last_ch = character_last_appearance.get(char_id, 0)
|
||||
chapters_gap = current_chapter - last_ch
|
||||
|
||||
if chapters_gap <= 0:
|
||||
continue
|
||||
|
||||
# 计算时间跨度(假设每章代表一定时间范围)
|
||||
time_elapsed = self._calculate_time_elapsed(chapters_gap, state)
|
||||
|
||||
# 获取角色当前状态
|
||||
char_state = self._get_character_state(char_id, state)
|
||||
|
||||
# 检查是否有脏标记(retcon 影响)
|
||||
dirty_flags = self._check_dirty_flags(char_id)
|
||||
|
||||
# 计算追赶后的状态
|
||||
updated_state = self._compute_catchup_state(
|
||||
char_id, char_state, time_elapsed, dirty_flags
|
||||
)
|
||||
|
||||
state_updates.append({
|
||||
"character_id": char_id,
|
||||
"last_appearance_chapter": last_ch,
|
||||
"current_chapter": current_chapter,
|
||||
"chapters_gap": chapters_gap,
|
||||
"time_elapsed": time_elapsed,
|
||||
"state_before": char_state,
|
||||
"state_after": updated_state,
|
||||
"dirty_flags_applied": dirty_flags
|
||||
})
|
||||
|
||||
# 检查时间线一致性
|
||||
timeline_issue = self._check_timeline_consistency(
|
||||
char_id, time_elapsed, state
|
||||
)
|
||||
if timeline_issue:
|
||||
timeline_changes.append(timeline_issue)
|
||||
|
||||
# 5. 验证所有更新
|
||||
pass_count = len([u for u in state_updates if not u.get("warnings")])
|
||||
fail_count = len(state_updates) - pass_count
|
||||
|
||||
return CatchupResult(
|
||||
agent="catchup_agent",
|
||||
awakened_characters=awakened_chars,
|
||||
state_updates=state_updates,
|
||||
timeline_changes=timeline_changes,
|
||||
warnings=warnings,
|
||||
pass_count=pass_count,
|
||||
fail_count=fail_count
|
||||
)
|
||||
|
||||
def _load_state(self) -> Dict[str, Any]:
|
||||
"""加载 state.json"""
|
||||
if not self.state_file.exists():
|
||||
return {}
|
||||
|
||||
with open(self.state_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def _get_character_last_appearance(self) -> Dict[str, int]:
|
||||
"""从 index.db 获取角色最后出场章节"""
|
||||
result = {}
|
||||
|
||||
if not self.index_db.exists():
|
||||
return result
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.index_db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取角色最后出场章节
|
||||
cursor.execute("""
|
||||
SELECT entity_id, MAX(chapter) as last_chapter
|
||||
FROM chapter_entities
|
||||
WHERE entity_type = 'character'
|
||||
GROUP BY entity_id
|
||||
""")
|
||||
|
||||
for row in cursor.fetchall():
|
||||
result[row[0]] = row[1]
|
||||
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _detect_awakened_characters(
|
||||
self,
|
||||
current_chapter: int,
|
||||
last_appearance: Dict[str, int],
|
||||
state: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""
|
||||
自动检测需要 catchup 的角色。
|
||||
|
||||
规则:
|
||||
- 角色最后出场在 10 章之前
|
||||
- 角色当前不在休眠列表中
|
||||
- 角色不是主角
|
||||
"""
|
||||
awakened = []
|
||||
dormant_threshold = 10 # 超过此章节数则认为需要 catchup
|
||||
|
||||
protagonist_id = state.get("protagonist_state", {}).get("id", "protagonist")
|
||||
dormant_characters = state.get("dormant_characters", [])
|
||||
|
||||
for char_id, last_ch in last_appearance.items():
|
||||
# 跳过主角
|
||||
if char_id == protagonist_id:
|
||||
continue
|
||||
|
||||
# 跳过已经在 dormant 列表中的角色
|
||||
if char_id in dormant_characters:
|
||||
continue
|
||||
|
||||
# 检查是否超过阈值
|
||||
if current_chapter - last_ch >= dormant_threshold:
|
||||
awakened.append(char_id)
|
||||
|
||||
return awakened
|
||||
|
||||
def _calculate_time_elapsed(
|
||||
self,
|
||||
chapters_gap: int,
|
||||
state: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
计算时间流逝。
|
||||
|
||||
假设:
|
||||
- 每章代表 1 天(可配置)
|
||||
- 修仙世界时间流速可调
|
||||
"""
|
||||
time_config = state.get("time_config", {})
|
||||
days_per_chapter = time_config.get("days_per_chapter", 1)
|
||||
|
||||
total_days = chapters_gap * days_per_chapter
|
||||
|
||||
return {
|
||||
"days": total_days,
|
||||
"weeks": total_days // 7,
|
||||
"months": total_days // 30,
|
||||
"years": total_days // 365
|
||||
}
|
||||
|
||||
def _get_character_state(
|
||||
self,
|
||||
char_id: str,
|
||||
state: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""获取角色当前状态"""
|
||||
# 从 state.json 获取
|
||||
character_states = state.get("character_states", {})
|
||||
|
||||
if char_id in character_states:
|
||||
return character_states[char_id]
|
||||
|
||||
# 返回默认值
|
||||
return {
|
||||
"id": char_id,
|
||||
"realm": "unknown",
|
||||
"location": "unknown",
|
||||
"status": "unknown"
|
||||
}
|
||||
|
||||
def _check_dirty_flags(self, char_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
检查脏标记(retcon 影响)。
|
||||
|
||||
当作者修改设定后,相关角色的脏标记会被激活。
|
||||
|
||||
实现逻辑:
|
||||
1. 从 ledger 中读取 retcon 事件
|
||||
2. 筛选出影响目标角色的变更
|
||||
3. 返回待应用的补丁列表
|
||||
"""
|
||||
dirty_flags: List[Dict[str, Any]] = []
|
||||
|
||||
# 尝试加载 ledger
|
||||
ledger_path = self.project_root / ".noma" / "novel_data" / "ledger.json"
|
||||
if not ledger_path.exists():
|
||||
return dirty_flags
|
||||
|
||||
try:
|
||||
with open(ledger_path, "r", encoding="utf-8") as f:
|
||||
ledger = json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Failed to load ledger from {ledger_path}")
|
||||
return dirty_flags
|
||||
|
||||
# 查找 retcon 事件
|
||||
retcon_events = ledger.get("retcon_events", [])
|
||||
|
||||
for event in retcon_events:
|
||||
# 检查是否影响此角色
|
||||
affected_entities = event.get("affected_entities", [])
|
||||
if char_id not in affected_entities:
|
||||
continue
|
||||
|
||||
# 提取脏标记
|
||||
changes = event.get("changes", [])
|
||||
for change in changes:
|
||||
if change.get("entity_id") == char_id:
|
||||
dirty_flags.append({
|
||||
"field": change.get("field"),
|
||||
"old_value": change.get("old_value"),
|
||||
"new_value": change.get("new_value"),
|
||||
"reason": change.get("reason", ""),
|
||||
"event_chapter": event.get("chapter", 0),
|
||||
"timestamp": event.get("timestamp", "")
|
||||
})
|
||||
|
||||
# 同时检查 state.json 中的 character_states
|
||||
state = self._load_state()
|
||||
char_states = state.get("character_states", {})
|
||||
|
||||
if char_id in char_states:
|
||||
current_state = char_states[char_id]
|
||||
|
||||
# 检查是否有手动标记的待同步字段
|
||||
pending_sync = current_state.get("_pending_sync", [])
|
||||
for sync_item in pending_sync:
|
||||
if isinstance(sync_item, dict):
|
||||
dirty_flags.append({
|
||||
"field": sync_item.get("field"),
|
||||
"new_value": sync_item.get("value"),
|
||||
"reason": sync_item.get("reason", "Manual sync required"),
|
||||
"source": "state_json"
|
||||
})
|
||||
|
||||
return dirty_flags
|
||||
|
||||
def _compute_catchup_state(
|
||||
self,
|
||||
char_id: str,
|
||||
current_state: Dict[str, Any],
|
||||
time_elapsed: Dict[str, Any],
|
||||
dirty_flags: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
计算追赶后的角色状态。
|
||||
|
||||
规则:
|
||||
- 境界:根据时间推算可能的修炼进展
|
||||
- 位置:如果角色有固定活动区域,可能迁移
|
||||
- 关系:可能发展新的关系
|
||||
"""
|
||||
updated = current_state.copy()
|
||||
|
||||
# 应用脏标记(retcon 补丁)
|
||||
for flag in dirty_flags:
|
||||
field = flag.get("field")
|
||||
new_value = flag.get("new_value")
|
||||
if field and new_value:
|
||||
updated[field] = new_value
|
||||
|
||||
# 时间推进可能导致的变化(简单推算)
|
||||
# 实际应用中需要更复杂的逻辑
|
||||
days = time_elapsed.get("days", 0)
|
||||
|
||||
if days > 365: # 超过一年
|
||||
# 可能有境界提升
|
||||
if "realm" in updated:
|
||||
# 这里应该根据修炼体系计算
|
||||
pass
|
||||
|
||||
return updated
|
||||
|
||||
def _check_timeline_consistency(
|
||||
self,
|
||||
char_id: str,
|
||||
time_elapsed: Dict[str, Any],
|
||||
state: Dict[str, Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
检查时间线一致性。
|
||||
|
||||
返回 None 表示一致,否则返回问题描述。
|
||||
"""
|
||||
# 检查角色是否在不可能的时间出现在某地
|
||||
# 这是一个占位实现
|
||||
return None
|
||||
|
||||
def save_results(self, results: CatchupResult) -> None:
|
||||
"""保存结果到 state.json 和 ledger"""
|
||||
state = self._load_state()
|
||||
|
||||
# 更新 character_states
|
||||
for update in results.state_updates:
|
||||
char_id = update["character_id"]
|
||||
state.setdefault("character_states", {})[char_id] = update["state_after"]
|
||||
|
||||
# 记录 catchup 事件
|
||||
state.setdefault("catchup_events", []).append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"results": asdict(results)
|
||||
})
|
||||
|
||||
# 写回 state.json
|
||||
with open(self.state_file, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI 入口"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="NovelMaster Catchup Agent")
|
||||
parser.add_argument(
|
||||
"--project-root",
|
||||
required=True,
|
||||
help="项目根目录"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--current-chapter",
|
||||
type=int,
|
||||
required=True,
|
||||
help="当前章节编号"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--awakened-chars",
|
||||
nargs="*",
|
||||
help="被唤醒的角色 ID 列表(可选)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--storage-path",
|
||||
default=".noma/novel_data/",
|
||||
help="存储路径(默认: .noma/novel_data/)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = CatchupAgent(args.project_root, args.storage_path)
|
||||
results = agent.execute(args.current_chapter, args.awakened_chars)
|
||||
|
||||
# 输出 JSON 结果
|
||||
print(json.dumps(asdict(results), ensure_ascii=False, indent=2))
|
||||
|
||||
# 保存结果
|
||||
agent.save_results(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Agents Checkers
|
||||
|
||||
审查器模块,包括 tension_checker 等。
|
||||
"""
|
||||
|
||||
from .tension_checker import (
|
||||
TensionChecker,
|
||||
TensionChecker,
|
||||
CatharsisModel,
|
||||
TensionLevel,
|
||||
TensionReading,
|
||||
TensionIssue,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TensionChecker",
|
||||
"CatharsisModel",
|
||||
"TensionLevel",
|
||||
"TensionReading",
|
||||
"TensionIssue",
|
||||
]
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: consistency-checker
|
||||
description: 设定一致性检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# consistency-checker (设定一致性检查器)
|
||||
|
||||
> **职责**: 设定守卫者,执行第二防幻觉定律(设定即物理)。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 设定违规、战力冲突、逻辑不一致的结构化报告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载参考资料
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `{project_root}/..noma/novel_data/state.json`(主角当前状态)
|
||||
3. `设定集/`(世界观圣经)
|
||||
4. `大纲/`(对照上下文)
|
||||
|
||||
### 第二步: 三层一致性检查
|
||||
|
||||
#### 第一层: 战力一致性(战力检查)
|
||||
|
||||
**校验项**:
|
||||
- Protagonist's current realm/level matches state.json
|
||||
- Abilities used are within realm limitations
|
||||
- Power-ups follow established progression rules
|
||||
|
||||
**危险信号** (POWER_CONFLICT):
|
||||
```
|
||||
❌ 主角筑基3层使用金丹期才能掌握的"破空斩"
|
||||
→ Realm: 筑基3 | Ability: 破空斩 (requires 金丹期)
|
||||
→ VIOLATION: Premature ability access
|
||||
|
||||
❌ 上章境界淬体9层,本章突然变成凝气5层(无突破描写)
|
||||
→ Previous: 淬体9 | Current: 凝气5 | Missing: Breakthrough scene
|
||||
→ VIOLATION: Unexplained power jump
|
||||
```
|
||||
|
||||
**校验依据**:
|
||||
- state.json: `protagonist_state.power.realm`, `protagonist_state.power.layer`
|
||||
- 设定集/修炼体系.md: Realm ability restrictions
|
||||
|
||||
#### 第二层: 地点/角色一致性(地点/角色检查)
|
||||
|
||||
**校验项**:
|
||||
- Current location matches state.json or has valid travel sequence
|
||||
- Characters appearing are established in 设定集/ or tagged with `<entity/>`
|
||||
- Character attributes (appearance, personality, affiliations) match records
|
||||
|
||||
**危险信号** (LOCATION_ERROR / CHARACTER_CONFLICT):
|
||||
```
|
||||
❌ 上章在"天云宗",本章突然出现在"千里外的血煞秘境"(无移动描写)
|
||||
→ Previous location: 天云宗 | Current: 血煞秘境 | Distance: 1000+ li
|
||||
→ VIOLATION: Teleportation without explanation
|
||||
|
||||
❌ 李雪上次是"筑基期修为",本章变成"练气期"(无解释)
|
||||
→ Character: 李雪 | Previous: 筑基期 | Current: 练气期
|
||||
→ VIOLATION: Power regression unexplained
|
||||
```
|
||||
|
||||
**校验依据**:
|
||||
- state.json: `protagonist_state.location.current`
|
||||
- 设定集/角色卡/: Character profiles
|
||||
|
||||
#### 第三层: 时间线一致性(时间线检查)
|
||||
|
||||
**校验项**:
|
||||
- Event sequence is chronologically logical
|
||||
- Time-sensitive elements (deadlines, age, seasonal events) align
|
||||
- Flashbacks are clearly marked
|
||||
- Chapter time anchors match volume timeline
|
||||
|
||||
**Severity Classification** (时间问题分级):
|
||||
| 问题类型 | Severity | 说明 |
|
||||
|---------|----------|------|
|
||||
| 倒计时算术错误 | **critical** | D-5 直接跳到 D-2,必须修复 |
|
||||
| 事件先后矛盾 | **high** | 先发生的事情后写,逻辑混乱 |
|
||||
| 年龄/修炼时长冲突 | **high** | 算术错误,如15岁修炼5年却10岁入门 |
|
||||
| 时间回跳无标注 | **high** | 非闪回章节却出现时间倒退 |
|
||||
| 大跨度无过渡 | **high** | 跨度>3天却无过渡说明 |
|
||||
| 时间锚点缺失 | **medium** | 无法确定章节时间,但不影响逻辑 |
|
||||
| 轻微时间模糊 | **low** | 时段不明确但不影响剧情 |
|
||||
|
||||
> 输出 JSON 时,`issues[].severity` 必须使用小写枚举:`critical|high|medium|low`。
|
||||
|
||||
**危险信号** (TIMELINE_ISSUE):
|
||||
```
|
||||
❌ [critical] 第10章物资耗尽倒计时 D-5,第11章直接变成 D-2(跳过3天)
|
||||
→ Setup: D-5 | Next chapter: D-2 | Missing: 3 days
|
||||
→ VIOLATION: Countdown arithmetic error (MUST FIX)
|
||||
|
||||
❌ [high] 第10章提到"三天后的宗门大比",第11章描述大比结束(中间无时间流逝)
|
||||
→ Setup: 3 days until event | Next chapter: Event concluded
|
||||
→ VIOLATION: Missing time passage
|
||||
|
||||
❌ [high] 主角15岁修炼5年,推算应该10岁开始,但设定集记录"12岁入门"
|
||||
→ Age: 15 | Cultivation years: 5 | Start age: 10 | Record: 12
|
||||
→ VIOLATION: Timeline arithmetic error
|
||||
|
||||
❌ [high] 第一章末世降临,第二章就建立帮派(无时间过渡)
|
||||
→ Chapter 1: 末世第1天 | Chapter 2: 建帮派火拼
|
||||
→ VIOLATION: Major event without reasonable time progression
|
||||
|
||||
❌ [high] 本章时间锚点"末世第3天",上章是"末世第5天"(时间回跳)
|
||||
→ Previous: 末世第5天 | Current: 末世第3天
|
||||
→ VIOLATION: Time regression without flashback marker
|
||||
```
|
||||
|
||||
### 第三步: 实体一致性检查
|
||||
|
||||
**对所有章节中检测到的新实体**:
|
||||
1. Check if they contradict existing settings
|
||||
2. Assess if their introduction is consistent with world-building
|
||||
3. Verify power levels are reasonable for the current arc
|
||||
|
||||
**报告不一致的新增实体**:
|
||||
```
|
||||
⚠️ 发现设定冲突:
|
||||
- 第46章出现"紫霄宗",与设定集中势力分布矛盾
|
||||
→ 建议: 确认是否为新势力或笔误
|
||||
```
|
||||
|
||||
### 第四步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 设定一致性检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 战力一致性
|
||||
| 章节 | 问题 | 严重度 | 详情 |
|
||||
|------|------|--------|------|
|
||||
| {N} | ✓ 无违规 | - | - |
|
||||
| {M} | ✗ POWER_CONFLICT | high | 主角筑基3层使用金丹期技能"破空斩" |
|
||||
|
||||
**结论**: 发现 {X} 处违规
|
||||
|
||||
## 地点/角色一致性
|
||||
| 章节 | 类型 | 问题 | 严重度 |
|
||||
|------|------|------|--------|
|
||||
| {M} | 地点 | ✗ LOCATION_ERROR | medium | 未描述移动过程,从天云宗跳跃到血煞秘境 |
|
||||
|
||||
**结论**: 发现 {Y} 处违规
|
||||
|
||||
## 时间线一致性
|
||||
| 章节 | 问题 | 严重度 | 详情 |
|
||||
|------|------|--------|------|
|
||||
| {M} | ✗ TIMELINE_ISSUE | critical | 倒计时从 D-5 跳到 D-2 |
|
||||
| {M} | ✗ TIMELINE_ISSUE | high | 大比倒计时逻辑不一致 |
|
||||
|
||||
**结论**: 发现 {Z} 处违规
|
||||
**严重时间线问题**: {count} 个(必须修复后才能继续)
|
||||
|
||||
## 新实体一致性检查
|
||||
- ✓ 与世界观一致的新实体: {count}
|
||||
- ⚠️ 不一致的实体: {count}(详见下方列表)
|
||||
- ❌ 矛盾实体: {count}
|
||||
|
||||
**不一致列表**:
|
||||
1. 第{M}章:"紫霄宗"(势力)- 与现有势力分布矛盾
|
||||
2. 第{M}章:"天雷果"(物品)- 效果与力量体系不符
|
||||
|
||||
## 修复建议
|
||||
- [战力冲突] 润色时修改第{M}章,将"破空斩"替换为筑基期可用技能
|
||||
- [地点错误] 润色时补充移动过程描述或调整地点设定
|
||||
- [时间线问题] 润色时统一时间线推算,修正矛盾
|
||||
- [实体冲突] 润色时确认是否为新设定或需要调整
|
||||
|
||||
## 综合评分
|
||||
**结论**: {通过/未通过} - {简要说明}
|
||||
**严重违规**: {count}(必须修复)
|
||||
**轻微问题**: {count}(建议修复)
|
||||
```
|
||||
|
||||
### 第五步: 标记无效事实(新增)
|
||||
|
||||
对于发现的严重级别(`critical`)问题,自动标记到 `invalid_facts`(状态为 `pending`):
|
||||
|
||||
```bash
|
||||
python -X utf8 "${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts/noma.py" --project-root "{PROJECT_ROOT}" index mark-invalid \
|
||||
--source-type entity \
|
||||
--source-id {entity_id} \
|
||||
--reason "{问题描述}" \
|
||||
--marked-by consistency-checker \
|
||||
--chapter {current_chapter}
|
||||
```
|
||||
|
||||
> 注意:自动标记仅为 `pending`,需用户确认后才生效。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在 POWER_CONFLICT(战力崩坏)的章节
|
||||
❌ 忽略未标记的新实体
|
||||
❌ 接受无世界观解释的瞬移
|
||||
❌ **降低 TIMELINE_ISSUE 严重度**(时间问题不得降级)
|
||||
❌ **通过存在严重/高优先级时间线问题的章节**(必须修复)
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 0 个严重违规(战力冲突、无解释的角色变化、**时间线算术错误**)
|
||||
- 0 个高优先级时间线问题(**倒计时错误、时间回跳、重大事件无时间推进**)
|
||||
- 所有新实体与现有世界观一致
|
||||
- 地点和时间线过渡合乎逻辑
|
||||
- 报告为润色步骤提供具体修复建议
|
||||
@@ -0,0 +1,455 @@
|
||||
"""
|
||||
Consistency Checker (设定一致性检查器)
|
||||
|
||||
检查设定一致性:
|
||||
- 战力等级一致性
|
||||
- 地点/角色一致性
|
||||
- 时间线一致性
|
||||
|
||||
输出结构化报告。
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class IssueSeverity(Enum):
|
||||
"""问题严重度"""
|
||||
CRITICAL = "critical" # 必须修复
|
||||
HIGH = "high" # 强烈建议修复
|
||||
MEDIUM = "medium" # 建议修复
|
||||
LOW = "low" # 可忽略
|
||||
|
||||
|
||||
class IssueType(Enum):
|
||||
"""问题类型"""
|
||||
POWER_CONFLICT = "power_conflict" # 战力冲突
|
||||
LOCATION_ERROR = "location_error" # 地点错误
|
||||
CHARACTER_CONFLICT = "character_conflict" # 角色状态冲突
|
||||
TIMELINE_ISSUE = "timeline_issue" # 时间线问题
|
||||
NEW_ENTITY_CONFLICT = "new_entity_conflict" # 新实体与设定冲突
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsistencyIssue:
|
||||
"""一致性问题"""
|
||||
issue_type: IssueType
|
||||
chapter: int
|
||||
description: str
|
||||
severity: IssueSeverity
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsistencyReport:
|
||||
"""一致性检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
issues: List[ConsistencyIssue] = field(default_factory=list)
|
||||
power_issues_count: int = 0
|
||||
location_issues_count: int = 0
|
||||
timeline_issues_count: int = 0
|
||||
entity_issues_count: int = 0
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ConsistencyChecker:
|
||||
"""
|
||||
设定一致性检查器
|
||||
|
||||
检查范围:
|
||||
1. 战力一致性 - 境界/技能是否匹配
|
||||
2. 地点一致性 - 位置切换是否有过渡
|
||||
3. 时间线一致性 - 事件顺序/时间流逝
|
||||
4. 角色一致性 - 角色状态变化是否合理
|
||||
"""
|
||||
|
||||
# 境界关键词
|
||||
REALM_PATTERNS = [
|
||||
(r"凡人|淬体|炼气|筑基|金丹|元婴|化神|渡劫|大乘|真仙", "realm"),
|
||||
(r"一层|二层|三层|四层|五层|六层|七层|八层|九层|十层", "layer"),
|
||||
]
|
||||
|
||||
# 地点转移关键词
|
||||
LOCATION_TRANSITION_KEYWORDS = [
|
||||
"来到", "前往", "到达", "回到", "离开", "返回",
|
||||
"穿越", "瞬移", "飞行", "走了", "赶到"
|
||||
]
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.project_root = Path(project_root) if project_root else None
|
||||
self.state: Dict[str, Any] = {}
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
"""加载项目状态"""
|
||||
self.state = state
|
||||
|
||||
def load_settings(self, settings: Dict[str, Any]) -> None:
|
||||
"""加载设定集"""
|
||||
self.settings = settings
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_location: Optional[str] = None
|
||||
) -> ConsistencyReport:
|
||||
"""
|
||||
检查单章一致性
|
||||
|
||||
Args:
|
||||
chapter_num: 章节号
|
||||
chapter_text: 章节正文
|
||||
previous_location: 上一章位置
|
||||
|
||||
Returns:
|
||||
ConsistencyReport
|
||||
"""
|
||||
report = ConsistencyReport(start_chapter=chapter_num, end_chapter=chapter_num)
|
||||
|
||||
# 1. 战力检查
|
||||
power_issues = self._check_power_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(power_issues)
|
||||
report.power_issues_count = len(power_issues)
|
||||
|
||||
# 2. 地点检查
|
||||
location_issues = self._check_location_consistency(
|
||||
chapter_num, chapter_text, previous_location
|
||||
)
|
||||
report.issues.extend(location_issues)
|
||||
report.location_issues_count = len(location_issues)
|
||||
|
||||
# 3. 时间线检查
|
||||
timeline_issues = self._check_timeline_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(timeline_issues)
|
||||
report.timeline_issues_count = len(timeline_issues)
|
||||
|
||||
# 4. 新实体冲突检查
|
||||
entity_issues = self._check_new_entity_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(entity_issues)
|
||||
report.entity_issues_count = len(entity_issues)
|
||||
|
||||
# 综合判定
|
||||
critical_count = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
report.passed = critical_count == 0
|
||||
|
||||
# 生成摘要
|
||||
report.summary = self._generate_summary(report)
|
||||
|
||||
return report
|
||||
|
||||
def check_range(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int,
|
||||
chapters: Dict[int, str],
|
||||
locations: Dict[int, str]
|
||||
) -> ConsistencyReport:
|
||||
"""检查章节区间"""
|
||||
report = ConsistencyReport(start_chapter=start_chapter, end_chapter=end_chapter)
|
||||
|
||||
previous_location = None
|
||||
for ch in range(start_chapter, end_chapter + 1):
|
||||
if ch in chapters:
|
||||
chapter_text = chapters[ch]
|
||||
previous_loc = locations.get(ch - 1) if ch > start_chapter else None
|
||||
|
||||
ch_report = self.check_chapter(ch, chapter_text, previous_loc)
|
||||
report.issues.extend(ch_report.issues)
|
||||
report.power_issues_count += ch_report.power_issues_count
|
||||
report.location_issues_count += ch_report.location_issues_count
|
||||
report.timeline_issues_count += ch_report.timeline_issues_count
|
||||
report.entity_issues_count += ch_report.entity_issues_count
|
||||
|
||||
if ch in locations:
|
||||
previous_location = locations[ch]
|
||||
|
||||
critical_count = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
report.passed = critical_count == 0
|
||||
report.summary = self._generate_summary(report)
|
||||
|
||||
return report
|
||||
|
||||
def _check_power_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查战力一致性"""
|
||||
issues = []
|
||||
|
||||
# 从 state 获取当前境界
|
||||
current_realm = self.state.get("protagonist_state", {}).get("power", {}).get("realm", "")
|
||||
current_layer = self.state.get("protagonist_state", {}).get("power", {}).get("level", 0)
|
||||
|
||||
# 检测本章提到的境界
|
||||
detected_realms = self._extract_realms(text)
|
||||
|
||||
# 检查是否使用了超出境界的技能
|
||||
for realm_match in detected_realms:
|
||||
realm = realm_match["realm"]
|
||||
layer = realm_match.get("layer", 0)
|
||||
|
||||
# 境界跳跃检测(无突破描写)
|
||||
if current_realm and realm != current_realm:
|
||||
# 检查是否有过渡描写
|
||||
transition_keywords = ["突破", "晋升", "进阶", "升级", "渡劫", "闭关"]
|
||||
has_transition = any(kw in text for kw in transition_keywords)
|
||||
|
||||
if not has_transition:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.POWER_CONFLICT,
|
||||
chapter=chapter,
|
||||
description=f"境界从 {current_realm} 变化到 {realm},但无突破描写",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_realm": current_realm,
|
||||
"current_realm": realm,
|
||||
"has_transition": has_transition
|
||||
},
|
||||
suggested_fix="添加突破场景或修正境界描述"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_realms(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""提取文本中的境界信息"""
|
||||
realms = []
|
||||
|
||||
realm_names = [
|
||||
"凡人", "淬体", "炼气", "筑基", "金丹", "元婴",
|
||||
"化神", "渡劫", "大乘", "真仙"
|
||||
]
|
||||
|
||||
for realm in realm_names:
|
||||
if realm in text:
|
||||
# 尝试提取层次
|
||||
layer_match = re.search(rf'{realm}(\d+)层', text)
|
||||
layer = int(layer_match.group(1)) if layer_match else 0
|
||||
|
||||
realms.append({
|
||||
"realm": realm,
|
||||
"layer": layer,
|
||||
"position": text.index(realm)
|
||||
})
|
||||
|
||||
return realms
|
||||
|
||||
def _check_location_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_location: Optional[str]
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查地点一致性"""
|
||||
issues = []
|
||||
|
||||
# 检测本章地点
|
||||
current_location = self._extract_location(text)
|
||||
|
||||
if not previous_location:
|
||||
return issues
|
||||
|
||||
if not current_location:
|
||||
return issues
|
||||
|
||||
# 检查是否是同一地点
|
||||
if current_location != previous_location:
|
||||
# 检查是否有转移描写
|
||||
has_transition = any(kw in text for kw in self.LOCATION_TRANSITION_KEYWORDS)
|
||||
|
||||
# 检查是否在附近
|
||||
is_nearby = self._is_location_nearby(current_location, previous_location)
|
||||
|
||||
if not has_transition and not is_nearby:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.LOCATION_ERROR,
|
||||
chapter=chapter,
|
||||
description=f"从「{previous_location}」跳跃到「{current_location}」,无过渡描写",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_location": previous_location,
|
||||
"current_location": current_location,
|
||||
"has_transition": has_transition
|
||||
},
|
||||
suggested_fix="添加移动/传送过渡场景"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_location(self, text: str) -> Optional[str]:
|
||||
"""提取当前地点"""
|
||||
# 常见地点指示词
|
||||
patterns = [
|
||||
r"在(.+?)的",
|
||||
r"来到(.+?)的",
|
||||
r"位于(.+?)的",
|
||||
r"到了(.+?),",
|
||||
r"【(.+?)】",
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text)
|
||||
if match:
|
||||
location = match.group(1).strip()
|
||||
if location and len(location) < 20:
|
||||
return location
|
||||
|
||||
# 检查分割线后的地点
|
||||
lines = text.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('---'):
|
||||
continue
|
||||
if '【' in line and '】' in line:
|
||||
start = line.index('【')
|
||||
end = line.index('】')
|
||||
return line[start+1:end]
|
||||
|
||||
return None
|
||||
|
||||
def _is_location_nearby(self, loc1: str, loc2: str) -> bool:
|
||||
"""判断两地点是否相近"""
|
||||
# 简化实现:检查是否有相同关键词
|
||||
keywords1 = set(loc1)
|
||||
keywords2 = set(loc2)
|
||||
overlap = keywords1 & keywords2
|
||||
return len(overlap) >= 2
|
||||
|
||||
def _check_timeline_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查时间线一致性"""
|
||||
issues = []
|
||||
|
||||
# 检测时间描述
|
||||
time_patterns = [
|
||||
(r'第(\d+)天', 'day'),
|
||||
(r'(\d+)日后', 'future_day'),
|
||||
(r'(\d+)天后', 'future_day'),
|
||||
(r'(\d+)年前', 'past'),
|
||||
(r'D-(\d+)', 'countdown'),
|
||||
(r'倒计时(\d+)天', 'countdown'),
|
||||
]
|
||||
|
||||
detected_times = []
|
||||
for pattern, time_type in time_patterns:
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
value = int(match.group(1))
|
||||
detected_times.append({
|
||||
"type": time_type,
|
||||
"value": value,
|
||||
"position": match.start()
|
||||
})
|
||||
|
||||
# 检查时间回跳
|
||||
if len(detected_times) >= 2:
|
||||
for i in range(len(detected_times) - 1):
|
||||
t1 = detected_times[i]
|
||||
t2 = detected_times[i + 1]
|
||||
|
||||
# 检查 day 类型的时间回跳
|
||||
if t1["type"] == "day" and t2["type"] == "day":
|
||||
if t2["value"] < t1["value"]:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.TIMELINE_ISSUE,
|
||||
chapter=chapter,
|
||||
description=f"时间回跳:从第{t1['value']}天到第{t2['value']}天",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_time": t1["value"],
|
||||
"current_time": t2["value"]
|
||||
},
|
||||
suggested_fix="确认是否为闪回场景,或修正时间线"
|
||||
))
|
||||
|
||||
# 检查倒计时逻辑
|
||||
countdowns = [t for t in detected_times if t["type"] == "countdown"]
|
||||
if len(countdowns) >= 2:
|
||||
values = [t["value"] for t in countdowns]
|
||||
if values != sorted(values, reverse=True):
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.TIMELINE_ISSUE,
|
||||
chapter=chapter,
|
||||
description="倒计时数字递减错误",
|
||||
severity=IssueSeverity.CRITICAL,
|
||||
details={"countdowns": values},
|
||||
suggested_fix="修正倒计时数值"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_new_entity_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查新引入实体是否与设定冲突"""
|
||||
issues = []
|
||||
|
||||
# 从设定中获取已知势力
|
||||
known_factions = self.settings.get("factions", [])
|
||||
known_locations = self.settings.get("locations", [])
|
||||
|
||||
# 检测新出现的地点/势力
|
||||
new_mentions = re.findall(r'[""'']([^""'']{2,10})[""'']', text)
|
||||
|
||||
for mention in new_mentions:
|
||||
# 检查是否与已知势力冲突
|
||||
for faction in known_factions:
|
||||
if mention in faction.get("name", ""):
|
||||
# 检查是否有冲突描述
|
||||
conflict_keywords = ["对立", "敌对", "矛盾", "冲突"]
|
||||
if any(kw in text for kw in conflict_keywords):
|
||||
# 可能存在设定冲突
|
||||
pass # 简化处理
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_summary(self, report: ConsistencyReport) -> str:
|
||||
"""生成报告摘要"""
|
||||
total_issues = len(report.issues)
|
||||
critical = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
high = sum(1 for i in report.issues if i.severity == IssueSeverity.HIGH)
|
||||
|
||||
if report.passed:
|
||||
return f"检查通过。发现 {total_issues} 个问题({critical} 严重,{high} 高优先级)"
|
||||
else:
|
||||
return f"检查未通过。发现 {total_issues} 个问题({critical} 严重必须修复)"
|
||||
|
||||
def export_report(self, report: ConsistencyReport) -> Dict[str, Any]:
|
||||
"""导出报告为字典"""
|
||||
return {
|
||||
"start_chapter": report.start_chapter,
|
||||
"end_chapter": report.end_chapter,
|
||||
"passed": report.passed,
|
||||
"summary": report.summary,
|
||||
"issues_count": {
|
||||
"power": report.power_issues_count,
|
||||
"location": report.location_issues_count,
|
||||
"timeline": report.timeline_issues_count,
|
||||
"entity": report.entity_issues_count,
|
||||
"total": len(report.issues)
|
||||
},
|
||||
"issues": [
|
||||
{
|
||||
"type": i.issue_type.value,
|
||||
"chapter": i.chapter,
|
||||
"description": i.description,
|
||||
"severity": i.severity.value,
|
||||
"details": i.details,
|
||||
"suggested_fix": i.suggested_fix
|
||||
}
|
||||
for i in report.issues
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
---
|
||||
name: continuity-checker
|
||||
description: 连贯性检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# continuity-checker (连贯性检查器)
|
||||
|
||||
> **职责**: 叙事流守卫者,确保场景过渡顺畅、情节线连贯、逻辑一致。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 场景过渡、情节线、伏笔管理、逻辑流的连贯性分析。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载上下文
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. 前 2-3 章(过渡上下文)
|
||||
3. `大纲/`(对照大纲 - 大纲即法律)
|
||||
4. `{project_root}/..noma/novel_data/state.json`(情节线追踪器,若存在)
|
||||
|
||||
### 第二步: 四层连贯性检查
|
||||
|
||||
#### 第一层: 场景转换流畅度(场景转换)
|
||||
|
||||
**检查项**:
|
||||
```
|
||||
❌ Abrupt Transition:
|
||||
上一段:林天在天云宗大殿与长老对话
|
||||
下一段:林天已经在血煞秘境深处战斗
|
||||
问题:缺少移动过程/时间流逝描写
|
||||
|
||||
✓ Smooth Transition:
|
||||
上一段:林天告别长老,离开宗门
|
||||
过渡句:"三日后,林天抵达血煞秘境入口"
|
||||
下一段:林天在秘境中遭遇妖兽
|
||||
```
|
||||
|
||||
**过渡质量评级**:
|
||||
- **A**: 自然过渡 + 时间/空间标记清晰
|
||||
- **B**: 有过渡但略显生硬
|
||||
- **C**: 缺少过渡,靠读者推测
|
||||
- **F**: 完全断裂,逻辑跳跃
|
||||
|
||||
#### 第二层: 情节线连贯(情节线连贯)
|
||||
|
||||
**追踪活跃情节线**:
|
||||
- **Main Thread** (主线): 当前核心任务/目标
|
||||
- **Sub-threads** (支线): 次要任务、悬念、铺垫
|
||||
|
||||
**检查项**:
|
||||
- Threads introduced but never resolved (烂尾)
|
||||
- Threads resolved without proper setup (突兀)
|
||||
- Threads forgotten mid-story (遗忘)
|
||||
|
||||
**示例分析**:
|
||||
```
|
||||
第40章引入: "宗门大比将在10天后举行"(主线)
|
||||
第45章: 大比正在进行中 ✓
|
||||
第50章: 大比结束,主角获胜 ✓
|
||||
判定:✓ 线索完整,有始有终
|
||||
|
||||
vs.
|
||||
|
||||
第30章引入: "血煞门即将入侵"(支线伏笔)
|
||||
第31-50章: 完全未提及血煞门
|
||||
判定:⚠️ 线索悬空,可能遗忘或拖得太久
|
||||
```
|
||||
|
||||
#### 第三层: 伏笔管理(伏笔管理)
|
||||
|
||||
**伏笔分类**:
|
||||
| Type | Setup → Payoff Gap | Risk |
|
||||
|------|-------------------|------|
|
||||
| **Short-term** (短期) | 1-3 章 | Low |
|
||||
| **Mid-term** (中期) | 4-10 章 | Medium (容易被遗忘) |
|
||||
| **Long-term** (长期) | 10+ 章 | High (需明确标记) |
|
||||
|
||||
**危险信号**:
|
||||
第10章: "林天发现神秘玉佩,似乎隐藏秘密"
|
||||
第11-30章: 玉佩再未提及
|
||||
判定:⚠️ 伏笔遗忘风险,建议第31章回收或再次提及
|
||||
|
||||
✓ Proper Payoff:
|
||||
第10章: "李雪提到师父曾去过血煞秘境"
|
||||
第25章: "在秘境中发现李雪师父留下的线索"
|
||||
判定:✓ 伏笔回收合理,间隔15章属于中期伏笔
|
||||
```
|
||||
|
||||
**伏笔检查清单**:
|
||||
- [ ] 所有设置的伏笔是否在合理章节内回收?
|
||||
- [ ] 长期伏笔(10+章)是否定期提及以保持读者记忆?
|
||||
- [ ] 回收时是否自然,不生硬?
|
||||
|
||||
#### 第四层: 逻辑流畅性(逻辑流畅性)
|
||||
|
||||
**检查情节漏洞与逻辑不一致**:
|
||||
|
||||
```
|
||||
❌ Logic Hole:
|
||||
第45章: 主角说"我从未见过这种妖兽"
|
||||
第30章: 主角曾击败同种妖兽
|
||||
判定:❌ 前后矛盾,需修正
|
||||
|
||||
❌ Causality Break:
|
||||
第46章: 主角突然获得神秘力量
|
||||
问题: 无解释来源,违反"发明需申报"原则
|
||||
判定:❌ 缺少因果关系,需补充 `<entity/>` 或铺垫
|
||||
|
||||
✓ Logical:
|
||||
第44章: 主角服用聚气丹(铺垫)
|
||||
第45章: 主角突破境界(因果)
|
||||
判定:✓ 因果清晰
|
||||
```
|
||||
|
||||
### 第三步: 大纲一致性检查(大纲即法律)
|
||||
|
||||
**将章节与大纲对照**:
|
||||
|
||||
```
|
||||
大纲第45章: "主角参加宗门大比,对战王少,险胜"
|
||||
|
||||
实际第45章内容:
|
||||
- ✓ 主角参加大比
|
||||
- ✓ 对战王少
|
||||
- ✗ 结果是"轻松碾压"而非"险胜"
|
||||
|
||||
判定:⚠️ 偏离大纲(难度降低),需确认是否有意调整
|
||||
```
|
||||
|
||||
**偏差处理**:
|
||||
- **轻微**(细节优化): 可接受
|
||||
- **中等**(情节调整): 需标记并确认
|
||||
- **重大**(核心冲突变化): 必须标记 `<deviation reason="..."/>` 并说明
|
||||
|
||||
### 第四步: 拖沓检查(拖沓检查)
|
||||
|
||||
**识别拖沓段落**:
|
||||
```
|
||||
⚠️ Possible Drag:
|
||||
第45-46章: 两章都在描述"主角赶路"
|
||||
内容: 重复的风景描写,无关键事件
|
||||
判定:⚠️ 节奏拖沓,建议:
|
||||
- 压缩为1章
|
||||
- 或在赶路途中安排事件(遭遇/奇遇/思考)
|
||||
|
||||
✓ Efficient Pacing:
|
||||
第47章: "三日后,主角抵达秘境"(一句带过)
|
||||
判定:✓ 有效省略无关紧要的过程
|
||||
```
|
||||
|
||||
### 第五步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 连贯性检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 场景转换评分
|
||||
| 转换 | 从 → 到 | 评级 | 问题 |
|
||||
|------|---------|------|------|
|
||||
| 第{N}章→第{M}章 | 天云宗大殿 → 血煞秘境 | C | 缺少移动过程描写 |
|
||||
|
||||
**场景转换总评**: {平均评级}
|
||||
|
||||
## 情节线追踪
|
||||
| 情节线 | 引入 | 最近提及 | 状态 | 下一步 |
|
||||
|--------|------|---------|------|--------|
|
||||
| 宗门大比 | 第40章 | 第46章(结束)| ✓ 已解决 | - |
|
||||
| 血煞门入侵 | 第30章 | 第30章 | ⚠️ 休眠(16章未提及)| 建议第47章提及或回收 |
|
||||
| 神秘玉佩 | 第10章 | 第10章 | ⚠️ 遗忘(36章未提及)| 建议回收或删除伏笔 |
|
||||
|
||||
**活跃情节线**: {count}
|
||||
**休眠/遗忘**: {count}
|
||||
|
||||
## 伏笔管理
|
||||
| 设置 | 章节 | 类型 | 兑现 | 间隔 | 状态 |
|
||||
|------|------|------|------|------|------|
|
||||
| 李雪师父去过秘境 | 10 | 中期 | 第25章发现线索 | 15章 | ✓ 已回收 |
|
||||
| 神秘玉佩 | 10 | 长期 | 未回收 | 36章+ | ❌ 遗忘风险 |
|
||||
|
||||
**伏笔健康度**: {X} 已回收, {Y} 待处理, {Z} 有风险
|
||||
|
||||
## 逻辑一致性
|
||||
| 章节 | 问题 | 类型 | 严重度 |
|
||||
|------|------|------|--------|
|
||||
| {M} | 前后矛盾(主角称"从未见过"但第30章遇见过)| 前后矛盾 | high |
|
||||
| {M} | 突然获得力量无解释 | 因果缺失 | medium |
|
||||
|
||||
**发现逻辑漏洞**: {count}
|
||||
|
||||
## 大纲一致性
|
||||
| 章节 | 大纲 | 实际 | 偏差程度 |
|
||||
|------|------|------|---------|
|
||||
| {M} | 险胜王少 | 轻松碾压 | ⚠️ 中等(难度调整)|
|
||||
|
||||
**偏差数**: {count}({X} 轻微, {Y} 中等, {Z} 重大)
|
||||
|
||||
## 节奏拖沓检查
|
||||
- ⚠️ 第{N}-{M}章: 两章赶路场景重复,建议压缩或增加事件
|
||||
|
||||
## 修复建议
|
||||
1. **修复场景转换**: 第{M}章添加"三日后"等时间标记
|
||||
2. **回收遗忘伏笔**: 神秘玉佩已36章未提及,建议回收或回溯删除
|
||||
3. **解决逻辑矛盾**: 第{M}章修改"从未见过"为"很少见到"
|
||||
4. **提及休眠线索**: 血煞门入侵线索建议第47章再次提及
|
||||
5. **压缩拖沓段落**: 第{N}-{M}章赶路场景合并为1章
|
||||
|
||||
## 综合评分
|
||||
**连贯性总评**: {流畅/可接受/生硬/断裂}
|
||||
**严重问题**: {count}(必须修复)
|
||||
**改进建议**: {count}(建议改进)
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在重大大纲偏差且无 `<deviation/>` 标记的章节
|
||||
❌ 忽略遗忘伏笔(10+ 章休眠)
|
||||
❌ 接受突兀的场景转换(F 级)
|
||||
❌ 忽视情节漏洞和前后矛盾
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 所有场景转换评级 ≥ B
|
||||
- 无活跃情节线遗忘超过 15 章
|
||||
- 所有长期伏笔已追踪并有兑现计划
|
||||
- 0 个重大逻辑漏洞
|
||||
- 大纲偏差已正确标记
|
||||
- 报告指出需修复的具体章节
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Continuity Checker (连续性检查器)
|
||||
|
||||
检查场景与叙事连贯性:
|
||||
- 场景切换是否平滑
|
||||
- 视角是否一致
|
||||
- 叙事节奏是否连贯
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityIssue:
|
||||
"""连续性问题"""
|
||||
chapter: int
|
||||
issue_type: str # scene_transition / perspective_shift / info_gap
|
||||
description: str
|
||||
severity: str # critical/high/medium/low
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityReport:
|
||||
"""连续性检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
issues: List[ContinuityIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ContinuityChecker:
|
||||
"""
|
||||
场景与叙事连续性检查器
|
||||
|
||||
检查:
|
||||
1. 场景切换是否平滑
|
||||
2. 视角是否一致
|
||||
3. 信息是否连贯
|
||||
"""
|
||||
|
||||
# 场景切换关键词
|
||||
SCENE_TRANSITION_KEYWORDS = [
|
||||
"与此同时", "镜头一转", "画面切换", "时间流逝",
|
||||
"与此同时", "另一边", "镜头回到", "视角转向"
|
||||
]
|
||||
|
||||
# 视角关键词
|
||||
PERSPECTIVE_PATTERNS = [
|
||||
(r"他(她)看", "third_person"),
|
||||
(r"我(你)", "first_person"),
|
||||
(r"叶尘(主角名)", "main_character"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.current_perspective = "third_person"
|
||||
self.previous_scene_ending = ""
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_ending: Optional[str] = None
|
||||
) -> ContinuityReport:
|
||||
"""检查单章连续性"""
|
||||
report = ContinuityReport(start_chapter=chapter_num, end_chapter=chapter_num)
|
||||
|
||||
# 1. 检查场景切换平滑度
|
||||
transition_issues = self._check_scene_transitions(chapter_num, chapter_text, previous_ending)
|
||||
report.issues.extend(transition_issues)
|
||||
|
||||
# 2. 检查视角一致性
|
||||
perspective_issues = self._check_perspective_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(perspective_issues)
|
||||
|
||||
# 3. 检查信息缺口
|
||||
info_gap_issues = self._check_info_gaps(chapter_num, chapter_text, previous_ending)
|
||||
report.issues.extend(info_gap_issues)
|
||||
|
||||
# 综合判定
|
||||
critical_count = sum(1 for i in report.issues if i.severity == "critical")
|
||||
report.passed = critical_count == 0
|
||||
report.summary = f"发现 {len(report.issues)} 个连续性问题"
|
||||
|
||||
return report
|
||||
|
||||
def _check_scene_transitions(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_ending: Optional[str]
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查场景切换"""
|
||||
issues = []
|
||||
|
||||
# 检查分割线后的场景切换
|
||||
scenes = text.split('---')
|
||||
if len(scenes) > 2: # 多场景
|
||||
# 检查开头是否有场景说明
|
||||
first_scene = scenes[1] if len(scenes) > 1 else ""
|
||||
if first_scene.strip() and not any(kw in first_scene[:50] for kw in ["此时", "这里", "地点"]):
|
||||
issues.append(ContinuityIssue(
|
||||
chapter=chapter,
|
||||
issue_type="scene_transition",
|
||||
description="场景切换无过渡说明",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_perspective_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查视角一致性"""
|
||||
issues = []
|
||||
|
||||
# 简化:只检测是否有明显的视角混乱
|
||||
# 实际实现需要更复杂的 NLP
|
||||
|
||||
return issues
|
||||
|
||||
def _check_info_gaps(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_ending: Optional[str]
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查信息缺口"""
|
||||
issues = []
|
||||
|
||||
# 检查是否承接上文
|
||||
if previous_ending:
|
||||
# 简化:检查是否有承接关键词
|
||||
carry_over_keywords = ["继续", "接着", "与此同时", "然而", "但是"]
|
||||
has_carry_over = any(kw in text[:100] for kw in carry_over_keywords)
|
||||
|
||||
if not has_carry_over and len(text) > 500:
|
||||
# 可能存在信息缺口
|
||||
pass # 简化处理
|
||||
|
||||
return issues
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: high-point-checker
|
||||
description: 爽点密度检查,支持迪化误解/身份掉马模式,输出结构化报告
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# high-point-checker (爽点检查器)
|
||||
|
||||
> **职责**: 读者满足感机制的质量保障专家(爽点设计)。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **分类法**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **题材画像**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 爽点密度、类型覆盖、执行质量的结构化报告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载目标章节
|
||||
|
||||
读取指定范围内 `正文/` 目录下的所有章节。
|
||||
|
||||
### 第二步: 识别爽点
|
||||
|
||||
扫描 **8 种标准执行模式**:
|
||||
|
||||
| 模式 | 特征关键词 | 最低要求 |
|
||||
|------|-----------------|---------------------|
|
||||
| **装逼打脸** | 嘲讽/废物/不屑 → 反转/震惊/目瞪口呆 | 铺垫 + 反转 + 反应 |
|
||||
| **扮猪吃虎** | 示弱/隐藏实力 → 碾压 | 隐藏 + 轻视 + 碾压 |
|
||||
| **越级反杀** | 实力差距 → 以弱胜强 → 震撼 | 展示差距 + 策略/爆发 + 反转 |
|
||||
| **打脸权威** | 权威/前辈/强者 → 主角挑战成功 | 建立权威 + 挑战 + 成功 |
|
||||
| **反派翻车** | 反派得意/阴谋 → 计划失败/被反杀 | 反派铺垫 + 主角反制 + 翻车 |
|
||||
| **甜蜜超预期** | 期待/心动 → 超预期表现 → 情感升华 | 期待 + 超越期待 + 情绪 |
|
||||
| **迪化误解** | 主角随意行为 → 配角脑补升华 → 读者优越感 | 随意行为 + 信息差 + 误解 + 读者优越 |
|
||||
| **身份掉马** | 身份伪装 → 关键时刻揭露 → 周围震惊 | 隐藏(长期)+ 触发事件 + 揭露 + 群体反应 |
|
||||
|
||||
### 第二步补充: 迪化误解模式检测
|
||||
|
||||
**核心结构**:
|
||||
1. 主角随意行为(无心插柳)
|
||||
2. 配角信息差(不知道主角真实情况)
|
||||
3. 配角脑补升华(合理化主角行为)
|
||||
4. 读者优越感(我知道真相)
|
||||
|
||||
**识别信号**:
|
||||
- "竟然"/"难道"/"莫非" + 配角内心戏
|
||||
- 主角行为与配角解读的反差
|
||||
- 读者视角知道真相
|
||||
|
||||
**质量评估**:
|
||||
- A级:脑补合理,读者优越感强
|
||||
- B级:脑补尚可,效果一般
|
||||
- C级:脑补太刻意,配角显得蠢
|
||||
|
||||
### 第二步补充: 身份掉马模式检测
|
||||
|
||||
**核心结构**:
|
||||
1. 身份伪装(需长期铺垫)
|
||||
2. 关键时刻(危机/高光)
|
||||
3. 身份揭露(意外或主动)
|
||||
4. 周围反应(震惊/后悔/敬畏)
|
||||
|
||||
**识别信号**:
|
||||
- 身份相关词汇(真实身份/原来是/竟然是)
|
||||
- 周围角色大规模反应
|
||||
- 前后反差描写
|
||||
|
||||
**质量评估**:
|
||||
- A级:有长期铺垫,反应层次丰富
|
||||
- B级:有铺垫,反应单一
|
||||
- C级:无铺垫,突兀
|
||||
- F级:硬编身份,逻辑矛盾
|
||||
|
||||
### 第三步: 密度检查
|
||||
|
||||
**推荐基线(滚动窗口)**:
|
||||
- **Per chapter**: 优先有爽点或同等兑现;允许过渡章低密度
|
||||
- **Every 5 chapters**: 建议 ≥ 1 组合爽点(2种模式叠加)
|
||||
- **Every 10-15 chapters**: 建议 ≥ 1 里程碑爽点(改变主角地位)
|
||||
|
||||
**输出**:
|
||||
```
|
||||
第 X 章: [✓ 2 个爽点] 或 [△ 0 个爽点 - 连续出现时需预警]
|
||||
```
|
||||
|
||||
### 第四步: 类型多样性检查
|
||||
|
||||
**反单调要求**: 审查范围内单一类型不得超过 80%。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
Chapters 1-2:
|
||||
- 装逼打脸: 3 (75%) ✓
|
||||
- 越级反杀: 1 (25%)
|
||||
Mode diversity: Acceptable
|
||||
```
|
||||
|
||||
vs.
|
||||
|
||||
```
|
||||
Chapters 45-46:
|
||||
- 装逼打脸: 7 (87.5%) ✗ OVER-RELIANCE
|
||||
- 扮猪吃虎: 1 (12.5%)
|
||||
Mode diversity: Warning - Monotonous pacing
|
||||
```
|
||||
|
||||
### 第五步: 执行质量评估
|
||||
|
||||
对每个已识别的爽点,检查:
|
||||
|
||||
1. **铺垫充分性**: 是否有充分的前期铺垫(至少1-2章)?
|
||||
2. **反转冲击**: 转折是否出人意料又合乎逻辑?
|
||||
3. **情绪回报**: 是否实现了读者情绪释放?
|
||||
4. **30/40/30 参考结构**: 结构是否清晰(不要求严格比例)?
|
||||
- 30% 铺垫蓄势
|
||||
- 40% 兑现执行
|
||||
- 30% 微反转/余波
|
||||
5. **压扬比例**: 是否匹配题材?
|
||||
- 传统爽文: 压3扬7
|
||||
- 硬核正剧: 压5扬5
|
||||
- 虐恋文: 压7扬3
|
||||
|
||||
**质量评级**:
|
||||
- **A(优秀)**: 所有标准达标,执行有力,结构清晰
|
||||
- **B(良好)**: 多数标准达标,可能有轻微比例问题
|
||||
- **C(及格)**: 基本标准达标但结构偏弱
|
||||
- **F(失败)**: 爽点缺少铺垫突然出现,或逻辑不一致
|
||||
|
||||
### 第六步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 爽点检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 密度检查
|
||||
- 第 {N} 章: ✓ 2 个爽点(装逼打脸 + 越级反杀)
|
||||
- 第 {M} 章: △ 0 个爽点 **[预警 - 连续出现时需补强]**
|
||||
|
||||
**结论**: {通过/预警/未通过}(基于滚动窗口)
|
||||
|
||||
## 类型分布
|
||||
- 装逼打脸: {count}({percent}%)
|
||||
- 扮猪吃虎: {count}({percent}%)
|
||||
- 越级反杀: {count}({percent}%)
|
||||
- 打脸权威: {count}({percent}%)
|
||||
- 反派翻车: {count}({percent}%)
|
||||
- 甜蜜超预期: {count}({percent}%)
|
||||
|
||||
**结论**: {通过/预警}(单一类型 > 80% 时有单调风险)
|
||||
|
||||
## 质量评级
|
||||
| 章节 | 爽点 | 模式 | 评级 | 30/40/30 | 压扬比 | 问题 |
|
||||
|------|------|------|------|---------|--------|------|
|
||||
| {N} | 主角被嘲讽后一招秒杀对手 | 装逼打脸 | A | ✓ | 压3扬7 | - |
|
||||
| {M} | 突然顿悟突破境界 | 越级反杀 | C | ✗ | 压1扬9 | 缺少铺垫,压扬比失衡 |
|
||||
|
||||
**结论**: 平均评级 = {X}
|
||||
|
||||
## 修复建议
|
||||
- [密度预警] 第 {M} 章低密度,建议补{mode}型爽点或同等兑现
|
||||
- [单调风险] 过度依赖{mode}型,建议增加{other_modes}
|
||||
- [质量问题] 第 {M} 章的爽点执行不足,需要补充{missing_element}
|
||||
- [结构偏弱] 爽点结构偏弱,建议补铺垫/兑现/余波中的缺项
|
||||
- [压扬比问题] 压扬比例不符合{genre}类型,建议调整为{ratio}
|
||||
|
||||
## 综合评分
|
||||
**结论**: {通过/未通过} - {简要说明}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 忽略连续低密度章节且不预警
|
||||
❌ 忽略缺乏铺垫的突发爽点
|
||||
❌ 通过连续 5+ 章同类型爽点
|
||||
❌ 迪化误解中配角智商明显下线
|
||||
❌ 身份掉马无任何前期暗示
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 滚动窗口密度保持健康(不连续低密度)
|
||||
- 类型分布显示多样性(单一类型不超过 80%)
|
||||
- 平均质量评级 ≥ B
|
||||
- 迪化误解的脑补需合理
|
||||
- 身份掉马需有铺垫
|
||||
- 报告包含可执行的修复建议
|
||||
|
||||
## 输出格式增强
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "high-point-checker",
|
||||
"chapter": 45,
|
||||
"overall_score": 86,
|
||||
"pass": true,
|
||||
"issues": [],
|
||||
"metrics": {
|
||||
"cool_point_count": 2,
|
||||
"cool_point_types": ["迪化误解", "身份掉马"],
|
||||
"density_score": 8,
|
||||
"type_diversity": 0.9,
|
||||
"milestone_present": false,
|
||||
"monotony_risk": false
|
||||
},
|
||||
"summary": "爽点密度达标,类型分布健康,执行质量稳定。"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
High-Point Checker (爽点检查器)
|
||||
|
||||
检查爽点密度与质量:
|
||||
- 爽点出现频率
|
||||
- 爽点强度
|
||||
- 爽点类型分布
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighPointIssue:
|
||||
"""爽点问题"""
|
||||
chapter: int
|
||||
issue_type: str # low_density / weak_intensity / type_imbalance
|
||||
description: str
|
||||
severity: str
|
||||
suggested: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighPointReport:
|
||||
"""爽点检查报告"""
|
||||
chapter: int
|
||||
hot_spots_count: int
|
||||
avg_intensity: float
|
||||
dominant_type: str
|
||||
issues: List[HighPointIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class HighPointChecker:
|
||||
"""
|
||||
爽点检查器
|
||||
|
||||
检查:
|
||||
1. 爽点密度(每千字爽点数)
|
||||
2. 爽点强度
|
||||
3. 爽点类型分布
|
||||
"""
|
||||
|
||||
# 爽点关键词
|
||||
HOT_SPOT_PATTERNS = {
|
||||
"overkill_reversal": [
|
||||
"碾压", "秒杀", "一击", "一招", "不堪一击",
|
||||
"跪下", "颤抖", "惊恐", "脸色大变", "难以置信"
|
||||
],
|
||||
"taboo_transgression": [
|
||||
"禁忌", "僭越", "危险", "邪魅", "诱惑",
|
||||
"堕落", "黑化", "疯狂", "失控", "暴走"
|
||||
],
|
||||
"cognitive_closure": [
|
||||
"原来", "竟然", "真相", "恍然大悟",
|
||||
"所有一切", "早该", "早就在", "伏笔", "埋下"
|
||||
],
|
||||
"payoff": [
|
||||
"收获", "突破", "晋升", "提升", "获得",
|
||||
"机缘", "惊喜", "传承", "觉醒", "解封"
|
||||
]
|
||||
}
|
||||
|
||||
# 每千字爽点期望值
|
||||
EXPECTED_PER_1000_WORDS = 2.0 # 每千字2个爽点
|
||||
|
||||
def __init__(self):
|
||||
self.history: List[HighPointReport] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
catharsis_model: Optional[str] = None
|
||||
) -> HighPointReport:
|
||||
"""检查单章爽点"""
|
||||
word_count = len(chapter_text)
|
||||
|
||||
# 统计各类爽点
|
||||
type_counts = {t: 0 for t in self.HOT_SPOT_PATTERNS}
|
||||
type_positions = {t: [] for t in self.HOT_SPOT_PATTERNS}
|
||||
|
||||
for hot_type, keywords in self.HOT_SPOT_PATTERNS.items():
|
||||
for keyword in keywords:
|
||||
start = 0
|
||||
while True:
|
||||
pos = chapter_text.find(keyword, start)
|
||||
if pos == -1:
|
||||
break
|
||||
type_counts[hot_type] += 1
|
||||
type_positions[hot_type].append(pos)
|
||||
start = pos + 1
|
||||
|
||||
# 计算总爽点数
|
||||
total_hot_spots = sum(type_counts.values())
|
||||
|
||||
# 计算爽点密度
|
||||
density = (total_hot_spots / word_count) * 1000 if word_count > 0 else 0
|
||||
|
||||
# 确定主导类型
|
||||
dominant_type = max(type_counts, key=type_counts.get) if type_counts else "none"
|
||||
dominant_count = type_counts.get(dominant_type, 0)
|
||||
|
||||
# 估算平均强度
|
||||
avg_intensity = min(1.0, density / self.EXPECTED_PER_1000_WORDS)
|
||||
|
||||
# 生成报告
|
||||
report = HighPointReport(
|
||||
chapter=chapter_num,
|
||||
hot_spots_count=total_hot_spots,
|
||||
avg_intensity=avg_intensity,
|
||||
dominant_type=dominant_type
|
||||
)
|
||||
|
||||
# 检测问题
|
||||
if density < self.EXPECTED_PER_1000_WORDS * 0.5:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="low_density",
|
||||
description=f"爽点密度偏低({density:.1f}/千字),建议增加爽点",
|
||||
severity="high"
|
||||
))
|
||||
|
||||
if total_hot_spots == 0:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="no_hotspot",
|
||||
description="未检测到明显爽点",
|
||||
severity="critical"
|
||||
))
|
||||
|
||||
# 爽点类型过于单一
|
||||
if dominant_count > 0 and dominant_count == total_hot_spots:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="type_imbalance",
|
||||
description=f"爽点类型单一,全部为 {dominant_type}",
|
||||
severity="medium",
|
||||
suggested="建议混合多种爽点类型增加层次感"
|
||||
))
|
||||
|
||||
# 检查是否与 catharsis_model 匹配
|
||||
if catharsis_model and dominant_type != catharsis_model:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="model_mismatch",
|
||||
description=f"爽点类型与目标模型不匹配(期望 {catharsis_model},实际 {dominant_type})",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
report.passed = len([i for i in report.issues if i.severity == "critical"]) == 0
|
||||
report.summary = f"检测到 {total_hot_spots} 个爽点,密度 {density:.1f}/千字,主导类型: {dominant_type}"
|
||||
|
||||
self.history.append(report)
|
||||
return report
|
||||
|
||||
def get_arc_hotspot_analysis(self, start_chapter: int, end_chapter: int) -> Dict[str, Any]:
|
||||
"""分析情节弧的爽点分布"""
|
||||
relevant = [r for r in self.history if start_chapter <= r.chapter <= end_chapter]
|
||||
|
||||
if not relevant:
|
||||
return {"status": "no_data"}
|
||||
|
||||
total_spots = sum(r.hot_spots_count for r in relevant)
|
||||
avg_intensity = sum(r.avg_intensity for r in relevant) / len(relevant)
|
||||
|
||||
type_distribution = {}
|
||||
for r in relevant:
|
||||
t = r.dominant_type
|
||||
type_distribution[t] = type_distribution.get(t, 0) + 1
|
||||
|
||||
return {
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"total_hot_spots": total_spots,
|
||||
"avg_intensity": avg_intensity,
|
||||
"type_distribution": type_distribution,
|
||||
"issues_count": sum(len(r.issues) for r in relevant)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: ooc-checker
|
||||
description: 人物OOC检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# ooc-checker (人物OOC检查器)
|
||||
|
||||
> **职责**: 角色完整性守卫者,防止 OOC(Out-Of-Character)违规。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 角色行为分析、OOC 违规、人设漂移警告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载角色档案
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `设定集/角色卡/`(所有角色档案)
|
||||
3. 前序章节作为行为基线(若审查章节 > 10)
|
||||
|
||||
### 第二步: 提取角色档案
|
||||
|
||||
**对每个主要角色,提取**:
|
||||
- **Personality traits** (性格): e.g., "隐忍冷静/嚣张狂妄/温柔体贴"
|
||||
- **Speech patterns** (说话风格): e.g., "言简意赅/喜欢嘲讽/礼貌用词"
|
||||
- **Core values** (价值观): e.g., "重视承诺/追求力量/保护弱者"
|
||||
- **Behavioral tendencies** (行为倾向): e.g., "三思而后行/冲动鲁莽/谨慎多疑"
|
||||
|
||||
**角色档案示例**:
|
||||
```
|
||||
角色:林天(主角)
|
||||
性格:隐忍冷静、智谋深沉、不轻易暴露实力
|
||||
说话风格:言简意赅,很少废话,语气平淡
|
||||
价值观:重视家族荣誉,保护弱者
|
||||
行为倾向:三思而后行,善于隐藏真实意图
|
||||
```
|
||||
|
||||
### 第三步: 行为采样
|
||||
|
||||
**对每章,提取角色的动作和对话**:
|
||||
|
||||
```
|
||||
第45章 - 林天行为采样:
|
||||
[对话] "你找死!" 林天怒吼一声,失去理智冲向对手
|
||||
[行动] 不顾一切地正面硬刚
|
||||
[情绪] 暴怒失控
|
||||
```
|
||||
|
||||
### 第四步: OOC 检测(三级判定)
|
||||
|
||||
#### 一级: 轻微偏离
|
||||
**定义**: 角色行为略有不同,但有合理的世界观内解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
✓ Acceptable:
|
||||
角色:林天(平时冷静)
|
||||
场景:敌人威胁要杀他家人
|
||||
行为:罕见地暴怒
|
||||
判定:✓ 触及底线,情绪变化合理
|
||||
|
||||
✓ Acceptable:
|
||||
角色:李雪(平时温柔)
|
||||
场景:主角生死关头
|
||||
行为:展现强势果断的一面
|
||||
判定:✓ 危机激发隐藏面,有前置铺垫
|
||||
```
|
||||
|
||||
#### 二级: 中度失真
|
||||
**定义**: 角色行为不一致,缺乏充分的铺垫或解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
⚠️ Warning:
|
||||
角色:林天(三思而后行)
|
||||
场景:普通挑衅
|
||||
行为:突然冲动鲁莽
|
||||
判定:⚠️ 缺少动机,需补充原因(如压力积累/特殊影响)
|
||||
|
||||
⚠️ Warning:
|
||||
角色:慕容雪(高傲冷漠)
|
||||
场景:对路人甲
|
||||
行为:突然温柔体贴
|
||||
判定:⚠️ 性格转变过快,需铺垫(如特殊原因/渐进变化)
|
||||
```
|
||||
|
||||
#### 三级: 严重崩坏
|
||||
**定义**: 角色行为与既定特征完全相反,且无任何解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
❌ Violation:
|
||||
角色:反派(嚣张狂妄、智商在线)
|
||||
场景:与主角对峙
|
||||
行为:突然智商下线,犯低级错误(故意让主角翻盘)
|
||||
判定:❌ 反派智商崩坏,纯粹为剧情服务
|
||||
|
||||
❌ Violation:
|
||||
角色:林天(隐忍冷静)
|
||||
场景:无特殊刺激
|
||||
行为:持续多章表现为冲动易怒
|
||||
判定:❌ 性格全面改变无解释,核心人设崩塌
|
||||
```
|
||||
|
||||
### 第五步: 对话风格检查
|
||||
|
||||
**校验对话一致性**:
|
||||
|
||||
| Character Type | Expected Style | OOC Examples |
|
||||
|---------------|----------------|--------------|
|
||||
| **主角(冷静型)** | 言简意赅、语气平淡 | ❌ "哈哈哈!老子今天就让你见识见识!" (过度张扬) |
|
||||
| **反派(嚣张型)** | 嘲讽、轻蔑、自信 | ❌ "对不起...我错了..." (突然怯懦) |
|
||||
| **修仙者** | "阁下/道友/在下" | ❌ "牛逼/666/OMG" (现代网络用语) |
|
||||
|
||||
### 第六步: 角色成长 vs. OOC
|
||||
|
||||
**区分合理成长与 OOC**:
|
||||
|
||||
```
|
||||
✓ Character Development:
|
||||
第1-10章:林天谨慎多疑(因为实力弱)
|
||||
第50章:林天开始自信果敢(实力提升+经历磨练)
|
||||
判定:✓ 合理成长,有渐进式铺垫
|
||||
|
||||
❌ OOC:
|
||||
第10章:林天隐忍冷静
|
||||
第11章:林天突然变成话痨
|
||||
判定:❌ 无解释的性格突变,非成长而是失真
|
||||
```
|
||||
|
||||
**成长检查清单**:
|
||||
- [ ] 性格转变有合理触发事件?
|
||||
- [ ] 转变过程有渐进式铺垫?
|
||||
- [ ] 转变后的行为与触发事件逻辑一致?
|
||||
|
||||
### 第七步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 人物OOC检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 主要角色行为采样
|
||||
|
||||
### 林天(主角)
|
||||
| 章节 | 行为/对话 | 人设匹配 | OOC 级别 |
|
||||
|------|----------|---------|---------|
|
||||
| {N} | "..." 冷静观察,未轻举妄动 | ✓ 符合"隐忍冷静" | 无 |
|
||||
| {M} | "你找死!"暴怒冲向对手 | ✗ 不符合"三思而后行" | ⚠️ 中度 |
|
||||
|
||||
**OOC 分析**:
|
||||
- 第{M}章林天失去冷静,**缺少触发原因**
|
||||
- 建议补充:对手触及底线(如威胁家人)来合理化情绪爆发
|
||||
|
||||
### 慕容雪(女配)
|
||||
| 章节 | 行为/对话 | 人设匹配 | OOC 级别 |
|
||||
|------|----------|---------|---------|
|
||||
| {M} | 突然对路人温柔体贴 | ✗ 不符合"高傲冷漠" | ⚠️ 中度 |
|
||||
|
||||
**OOC 分析**:
|
||||
- 性格转变缺少铺垫,建议:
|
||||
- 补充慕容雪性格变化的原因(如受主角影响)
|
||||
- 或将此场景改为"表面冷漠实则关心"来保持人设
|
||||
|
||||
## 对话风格检查
|
||||
| 角色 | 预期风格 | 发现违规 |
|
||||
|------|---------|---------|
|
||||
| 林天 | 言简意赅 | ✓ 无违规 |
|
||||
| 反派王少 | 嚣张嘲讽 | ✗ 第{M}章突然谦逊(智商下线)|
|
||||
|
||||
## 性格转变检查
|
||||
| 角色 | 原有特征 | 当前特征 | 合理性 | 判定 |
|
||||
|------|---------|---------|--------|------|
|
||||
| 林天 | 谨慎 | 自信 | ✓ 实力提升+经历铺垫 | ✓ 合理成长 |
|
||||
| 慕容雪 | 高傲 | 温柔 | ✗ 无铺垫 | ❌ OOC |
|
||||
|
||||
## 修复建议
|
||||
1. **修复第{M}章林天OOC**: 补充对手触及底线的情节
|
||||
2. **慕容雪性格转变**: 添加渐进式铺垫(3-5章)或调整此章表现
|
||||
3. **反派王少智商崩坏**: 修改对话,恢复嚣张狂妄但逻辑在线的人设
|
||||
|
||||
## 综合评分
|
||||
**OOC 违规**:
|
||||
- 严重: {count}
|
||||
- 中度: {count}
|
||||
- 轻微: {count}
|
||||
|
||||
**结论**: {通过/警告/未通过}
|
||||
**优先修复项**: {列出必须修复的严重OOC}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在严重 OOC 且未标记的章节(如反派智商下线)
|
||||
❌ 忽略角色对话风格违规
|
||||
❌ 混淆 OOC 与角色成长
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 0 个严重 OOC 违规
|
||||
- 中度 OOC 有合理的世界观内解释
|
||||
- 角色成长是渐进且有动机的
|
||||
- 对话风格与既定档案匹配
|
||||
- 报告能区分 OOC 和合理成长
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
OOC Checker (人物偏离检查器)
|
||||
|
||||
检查角色行为是否偏离人设:
|
||||
- 性格一致性
|
||||
- 行为合理性
|
||||
- 语言风格一致性
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class OOCIssue:
|
||||
"""角色偏离问题"""
|
||||
chapter: int
|
||||
character: str
|
||||
issue_type: str # personality / behavior / dialogue
|
||||
description: str
|
||||
severity: str
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OOCReport:
|
||||
"""OOC 检查报告"""
|
||||
chapter: int
|
||||
character_count: int
|
||||
issues: List[OOCIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class OOChecker:
|
||||
"""
|
||||
角色 OOC 检查器
|
||||
|
||||
检查:
|
||||
1. 性格一致性
|
||||
2. 行为合理性
|
||||
3. 对话风格一致性
|
||||
"""
|
||||
|
||||
# 性格关键词
|
||||
PERSONALITY_KEYWORDS = {
|
||||
"冷酷": ["冷哼", "不屑", "冷笑", "漠然", "冰冷"],
|
||||
"热血": ["怒吼", "咆哮", "愤怒", "激动", "振奋"],
|
||||
"腹黑": ["微笑", "意味深长", "算计", "暗笑", "狡黠"],
|
||||
"阳光": ["微笑", "开朗", "乐观", "爽朗", "活力"],
|
||||
"沉稳": ["平静", "淡定", "从容", "冷静", "深思"]
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.character_profiles: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def load_character_profiles(self, profiles: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""加载角色设定"""
|
||||
self.character_profiles = profiles
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
characters_in_chapter: Optional[List[str]] = None
|
||||
) -> OOCReport:
|
||||
"""检查单章 OOC"""
|
||||
report = OOCReport(chapter=chapter_num, character_count=0)
|
||||
|
||||
if not characters_in_chapter:
|
||||
characters_in_chapter = self._extract_characters(chapter_text)
|
||||
|
||||
report.character_count = len(characters_in_chapter)
|
||||
|
||||
for character in characters_in_chapter:
|
||||
if character not in self.character_profiles:
|
||||
continue
|
||||
|
||||
profile = self.character_profiles[character]
|
||||
personality = profile.get("personality", "")
|
||||
|
||||
# 检查性格关键词出现
|
||||
personality_keywords = self.PERSONALITY_KEYWORDS.get(personality, [])
|
||||
|
||||
# 检测可能的 OOC
|
||||
ooc_issues = self._detect_ooc(chapter_num, character, chapter_text, personality, personality_keywords)
|
||||
report.issues.extend(ooc_issues)
|
||||
|
||||
critical_count = sum(1 for i in report.issues if i.severity == "critical")
|
||||
report.passed = critical_count == 0
|
||||
report.summary = f"检查 {report.character_count} 个角色,发现 {len(report.issues)} 个 OOC 问题"
|
||||
|
||||
return report
|
||||
|
||||
def _extract_characters(self, text: str) -> List[str]:
|
||||
"""提取出现的角色"""
|
||||
characters = set()
|
||||
|
||||
# 简单实现:提取"说"前后的名字
|
||||
import re
|
||||
dialogue_pattern = re.compile(r'^"?([^"说]{2,5})"?[说问道喊叫笑骂冷哼]')
|
||||
for line in text.split('\n'):
|
||||
match = dialogue_pattern.match(line.strip())
|
||||
if match:
|
||||
name = match.group(1).strip()
|
||||
if name and len(name) <= 5:
|
||||
characters.add(name)
|
||||
|
||||
return list(characters)
|
||||
|
||||
def _detect_ooc(
|
||||
self,
|
||||
chapter: int,
|
||||
character: str,
|
||||
text: str,
|
||||
personality: str,
|
||||
expected_keywords: List[str]
|
||||
) -> List[OOCIssue]:
|
||||
"""检测 OOC"""
|
||||
issues = []
|
||||
|
||||
# 检查是否有相反性格的关键词
|
||||
opposite_keywords = {
|
||||
"冷酷": ["微笑", "开心", "高兴", "热情"],
|
||||
"热血": ["冷漠", "冷淡", "冷静", "漠然"],
|
||||
"腹黑": ["坦诚", "直接", "单纯", "天真"],
|
||||
"阳光": ["阴沉", "冷漠", "阴暗", "消沉"],
|
||||
"沉稳": ["激动", "暴躁", "鲁莽", "轻浮"]
|
||||
}
|
||||
|
||||
opposite = opposite_keywords.get(personality, [])
|
||||
found_opposite = [kw for kw in opposite if kw in text]
|
||||
|
||||
if found_opposite:
|
||||
# 检查是否在对话中(可能需要根据上下文判断)
|
||||
# 简化:直接报告
|
||||
issues.append(OOCIssue(
|
||||
chapter=chapter,
|
||||
character=character,
|
||||
issue_type="personality",
|
||||
description=f"角色表现出与设定({personality})相反的性格特征: {', '.join(found_opposite)}",
|
||||
severity="medium",
|
||||
suggested_fix="确认是否为特定场景需要,或调整言行描述"
|
||||
))
|
||||
|
||||
return issues
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
name: pacing-checker
|
||||
description: Strand Weave 节奏检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# pacing-checker (节奏检查器)
|
||||
|
||||
> **职责**: 节奏分析师,执行 Strand Weave 平衡检查,防止读者疲劳。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 情节线分布分析、平衡预警、节奏建议。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载上下文
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `{project_root}/..noma/novel_data/state.json`(strand_tracker 历史)
|
||||
3. `大纲/`(理解预期弧线结构)
|
||||
|
||||
**可选: 使用 status_reporter 进行自动化分析**:
|
||||
```bash
|
||||
python -X utf8 "${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts/noma.py" --project-root "${PROJECT_ROOT}" status -- --focus strand
|
||||
```
|
||||
|
||||
### 第二步: 章节情节线分类
|
||||
|
||||
**对每章,识别主导情节线**:
|
||||
|
||||
| Strand | Indicators | Examples |
|
||||
|--------|-----------|----------|
|
||||
| **Quest** (主线) | 战斗/任务/探索/升级/打怪 | 参加宗门大比、探索秘境、击败反派 |
|
||||
| **Fire** (感情线) | 情感关系/暧昧/友情/羁绊 | 与李雪的感情发展、师徒情深、兄弟义气 |
|
||||
| **Constellation** (世界观线) | 势力关系/阵营/社交网络/揭示世界观 | 新势力登场、修仙界格局展示、宗门政治 |
|
||||
|
||||
**分类规则**:
|
||||
- 一章可以有多条情节线的**底色**,但只有**一条主导**
|
||||
- 主导 = 占据章节内容 ≥ 60%
|
||||
|
||||
**Example**:
|
||||
```
|
||||
第45章:主角参加大比(Quest 80%)+ 李雪担心主角(Fire 20%)
|
||||
→ Dominant: Quest
|
||||
|
||||
第46章:主角与李雪约会(Fire 70%)+ 揭示血煞门阴谋(Constellation 30%)
|
||||
→ Dominant: Fire
|
||||
```
|
||||
|
||||
### 第三步: 平衡检查(Strand Weave 违规)
|
||||
|
||||
**从 state.json 加载 strand_tracker**:
|
||||
```json
|
||||
{
|
||||
"strand_tracker": {
|
||||
"last_quest_chapter": 46,
|
||||
"last_fire_chapter": 42,
|
||||
"last_constellation_chapter": 38,
|
||||
"history": [
|
||||
{"chapter": 45, "dominant": "quest"},
|
||||
{"chapter": 46, "dominant": "quest"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**应用警告阈值**:
|
||||
|
||||
| 违规类型 | 触发条件 | 严重度 | 影响 |
|
||||
|-----------|-----------|----------|--------|
|
||||
| **Quest 过载** | 连续 5+ 章 Quest 主导 | High | 战斗疲劳,缺少情感深度 |
|
||||
| **Fire 干旱** | 距上次 Fire > 10 章 | Medium | 人物关系停滞 |
|
||||
| **Constellation 缺席** | 距上次 Constellation > 15 章 | Low | 世界观单薄 |
|
||||
|
||||
**违规示例**:
|
||||
```
|
||||
⚠️ Quest Overload (连续7章)
|
||||
Chapters 40-46 全部为 Quest 主导
|
||||
→ Impact: 读者疲劳,建议第47章安排感情戏或世界观扩展
|
||||
|
||||
⚠️ Fire Drought (已12章未出现)
|
||||
Last Fire chapter: 34 | Current: 46 | Gap: 12 chapters
|
||||
→ Impact: 李雪等角色存在感降低,建议补充互动场景
|
||||
|
||||
✓ Constellation Acceptable
|
||||
Last Constellation: 38 | Current: 46 | Gap: 8 chapters
|
||||
```
|
||||
|
||||
### 第四步: 节奏标准
|
||||
|
||||
**每10章理想分布与缺席阈值**:
|
||||
|
||||
| Strand | 理想占比 | 最大缺席 | 超限影响 |
|
||||
|--------|---------|---------|---------|
|
||||
| Quest (主线) | 55-65% | 5 章连续 | 战斗疲劳,缺少情感深度 |
|
||||
| Fire (感情线) | 20-30% | 10 章 | 人物关系停滞 |
|
||||
| Constellation (世界观线) | 10-20% | 15 章 | 世界观单薄 |
|
||||
|
||||
### 第五步: 历史趋势分析
|
||||
|
||||
**若 state.json 包含 20+ 章历史数据**:
|
||||
|
||||
生成情节线分布图:
|
||||
```
|
||||
Chapters 1-20 Strand Distribution:
|
||||
Quest: ████████████░░░░░░░░ 60% (12 chapters)
|
||||
Fire: ████░░░░░░░░░░░░░░░░ 20% (4 chapters)
|
||||
Constellation: ████░░░░░░░░░░░░░░░░ 20% (4 chapters)
|
||||
|
||||
结论:✓ 节奏均衡(符合理想比例)
|
||||
```
|
||||
|
||||
vs.
|
||||
|
||||
```
|
||||
Chapters 21-40 Strand Distribution:
|
||||
Quest: ███████████████████░ 95% (19 chapters)
|
||||
Fire: █░░░░░░░░░░░░░░░░░░░ 5% (1 chapter)
|
||||
Constellation: ░░░░░░░░░░░░░░░░░░░░ 0% (0 chapters)
|
||||
|
||||
结论:✗ 严重失衡(Quest 过载,节奏单调)
|
||||
```
|
||||
|
||||
### 第六步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 节奏检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 当前章节主导情节线
|
||||
| 章节 | 主导线 | 底色 | 强度 |
|
||||
|------|--------|------|------|
|
||||
| {N} | Quest | Fire(20%)| 高(战斗密集)|
|
||||
| {M} | Quest | - | 中等 |
|
||||
|
||||
## Strand 平衡检查
|
||||
### Quest 线(主线)
|
||||
- 最近出现: 第 {X} 章
|
||||
- 连续章数: {count}
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警 / ✗ 过载}
|
||||
|
||||
### Fire 线(情感线)
|
||||
- 最近出现: 第 {Y} 章
|
||||
- 距上次间隔: {count} 章
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警 / ✗ 干旱}
|
||||
|
||||
### Constellation 线(世界观线)
|
||||
- 最近出现: 第 {Z} 章
|
||||
- 距上次间隔: {count} 章
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警}
|
||||
|
||||
## 历史趋势(需 ≥ 20 章数据)
|
||||
最近 20 章分布:
|
||||
- Quest: {X}%({count} 章)
|
||||
- Fire: {Y}%({count} 章)
|
||||
- Constellation: {Z}%({count} 章)
|
||||
|
||||
**趋势**: {均衡 / Quest偏重 / Fire不足 / ...}
|
||||
|
||||
## 修复建议
|
||||
- [Quest 过载] 连续{count}章Quest主导,建议在第{next}章安排:
|
||||
- 与{角色}的感情发展场景(Fire)
|
||||
- 或揭示{势力/世界观元素}(Constellation)
|
||||
|
||||
- [Fire 干旱] 距上次Fire已{count}章,建议补充:
|
||||
- 与李雪/师父/伙伴的互动
|
||||
- 不必是专门的感情章,可作为底色穿插
|
||||
|
||||
- [Constellation 间隔] 世界观扩展不足,建议:
|
||||
- 揭示新势力或修仙界格局
|
||||
- 展示新的修炼体系或设定
|
||||
|
||||
## 下一章节奏建议
|
||||
基于当前平衡状态,第 {next} 章应优先:
|
||||
**主导**: {线}(因为距上次{gap}章)
|
||||
**底色**: {线}
|
||||
|
||||
## 综合评分
|
||||
**节奏总评**: {健康/预警/危险}
|
||||
**读者疲劳风险**: {低/中/高}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过连续 5+ 章 Quest 主导且不预警
|
||||
❌ 忽略 Fire 干旱超过 10 章
|
||||
❌ 接受 20+ 章中完全相同的节奏模式
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 最近 10 章内单一情节线不超过 70%
|
||||
- 所有情节线在各自阈值内至少出现一次
|
||||
- 报告提供可执行的下一章建议
|
||||
- 趋势分析显示分布均衡(若有足够历史数据)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Pacing Checker (节奏检查器)
|
||||
|
||||
检查 Strand 比例与节奏:
|
||||
- Quest/Fire/Constellation 比例
|
||||
- 断档检查
|
||||
- 节奏红线
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacingIssue:
|
||||
"""节奏问题"""
|
||||
chapter: int
|
||||
issue_type: str # strand_imbalance / gap_too_long / rush
|
||||
description: str
|
||||
severity: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacingReport:
|
||||
"""节奏检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
strand_distribution: Dict[str, int] = field(default_factory=dict)
|
||||
issues: List[PacingIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class PacingChecker:
|
||||
"""
|
||||
节奏检查器
|
||||
|
||||
检查:
|
||||
1. Strand 比例(Quest/Fire/Constellation)
|
||||
2. 断档检查
|
||||
3. 节奏红线
|
||||
"""
|
||||
|
||||
# 理想比例
|
||||
IDEAL_RATIOS = {
|
||||
"quest": (55, 65), # 60% 主线
|
||||
"fire": (20, 30), # 25% 感情
|
||||
"constellation": (10, 20), # 15% 世界观
|
||||
}
|
||||
|
||||
# 断档红线
|
||||
MAX_GAPS = {
|
||||
"quest": 5, # Quest 连续不超过 5 章
|
||||
"fire": 10, # Fire 断档不超过 10 章
|
||||
"constellation": 15 # Constellation 断档不超过 15 章
|
||||
}
|
||||
|
||||
# 关键词
|
||||
STRAND_KEYWORDS = {
|
||||
"quest": ["战斗", "修炼", "突破", "机缘", "仇敌", "目标", "任务", "追杀"],
|
||||
"fire": ["感情", "爱慕", "暧昧", "心跳", "脸红", "温情", "甜蜜", "争吵"],
|
||||
"constellation": ["世界", "势力", "宗门", "规则", "历史", "背景", "设定"]
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.strand_history: List[Dict[str, Any]] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str
|
||||
) -> PacingReport:
|
||||
"""检查单章节奏"""
|
||||
# 统计各 Strand 出现次数
|
||||
strand_counts = {s: 0 for s in self.STRAND_KEYWORDS}
|
||||
|
||||
for strand, keywords in self.STRAND_KEYWORDS.items():
|
||||
for keyword in keywords:
|
||||
strand_counts[strand] += chapter_text.count(keyword)
|
||||
|
||||
# 确定主导 Strand
|
||||
dominant = max(strand_counts, key=strand_counts.get)
|
||||
|
||||
report = PacingReport(
|
||||
start_chapter=chapter_num,
|
||||
end_chapter=chapter_num,
|
||||
strand_distribution=strand_counts
|
||||
)
|
||||
|
||||
# 记录历史
|
||||
self.strand_history.append({
|
||||
"chapter": chapter_num,
|
||||
"strand_counts": strand_counts,
|
||||
"dominant": dominant
|
||||
})
|
||||
|
||||
return report
|
||||
|
||||
def check_arc_pacing(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int
|
||||
) -> PacingReport:
|
||||
"""检查情节弧节奏"""
|
||||
relevant = [h for h in self.strand_history
|
||||
if start_chapter <= h["chapter"] <= end_chapter]
|
||||
|
||||
if not relevant:
|
||||
return PacingReport(start_chapter=start_chapter, end_chapter=end_chapter)
|
||||
|
||||
# 统计各 Strand 章节数
|
||||
strand_chapter_counts = {s: 0 for s in self.STRAND_KEYWORDS}
|
||||
for h in relevant:
|
||||
dominant = h["dominant"]
|
||||
if h["strand_counts"][dominant] > 0:
|
||||
strand_chapter_counts[dominant] += 1
|
||||
|
||||
total_chapters = len(relevant)
|
||||
|
||||
report = PacingReport(
|
||||
start_chapter=start_chapter,
|
||||
end_chapter=end_chapter,
|
||||
strand_distribution=strand_chapter_counts
|
||||
)
|
||||
|
||||
# 检查比例
|
||||
for strand, (min_ratio, max_ratio) in self.IDEAL_RATIOS.items():
|
||||
actual_ratio = (strand_chapter_counts[strand] / total_chapters * 100) if total_chapters > 0 else 0
|
||||
|
||||
if actual_ratio < min_ratio:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=0,
|
||||
issue_type="strand_imbalance",
|
||||
description=f"{strand} 占比不足({actual_ratio:.0f}% < {min_ratio}%)",
|
||||
severity="medium"
|
||||
))
|
||||
elif actual_ratio > max_ratio:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=0,
|
||||
issue_type="strand_imbalance",
|
||||
description=f"{strand} 占比过高({actual_ratio:.0f}% > {max_ratio}%)",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
# 检查断档
|
||||
self._check_gaps(report, relevant)
|
||||
|
||||
report.summary = f"检查 {total_chapters} 章,分布: {strand_chapter_counts}"
|
||||
return report
|
||||
|
||||
def _check_gaps(self, report: PacingReport, history: List[Dict[str, Any]]) -> None:
|
||||
"""检查断档"""
|
||||
if len(history) < 2:
|
||||
return
|
||||
|
||||
last_seen = {s: -100 for s in self.STRAND_KEYWORDS}
|
||||
|
||||
for i, h in enumerate(history):
|
||||
chapter = h["chapter"]
|
||||
dominant = h["dominant"]
|
||||
|
||||
for strand in self.STRAND_KEYWORDS:
|
||||
if strand == dominant:
|
||||
last_seen[strand] = chapter
|
||||
else:
|
||||
gap = chapter - last_seen[strand]
|
||||
if gap > self.MAX_GAPS[strand]:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=chapter,
|
||||
issue_type="gap_too_long",
|
||||
description=f"{strand} 断档过长({gap}章未出现)",
|
||||
severity="high"
|
||||
))
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: reader-pull-checker
|
||||
description: 追读力检查器,评估钩子/微兑现/约束分层,支持 Override Contract
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# reader-pull-checker (追读力检查器)
|
||||
|
||||
> **职责**: 审查"读者为什么会点下一章",执行 Hard/Soft 约束分层。
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **分类法**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **题材画像**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
- **章节追读力数据**: `index.db → chapter_reading_power`
|
||||
- **上章钩子**: `state.json → chapter_meta` 或 `index.db`
|
||||
|
||||
## 输入
|
||||
- 章节正文(实际章节文件路径,优先 `正文/第{NNNN}章-{title_safe}.md`,旧格式 `正文/第{NNNN}章.md` 仍兼容)
|
||||
- 上章钩子与模式(从 `state.json → chapter_meta` 或 `index.db`)
|
||||
- 题材 Profile(从 `state.json → project.genre`)
|
||||
- 是否为过渡章标记
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "reader-pull-checker",
|
||||
"chapter": 100,
|
||||
"overall_score": 85,
|
||||
"pass": true,
|
||||
"issues": [],
|
||||
"hard_violations": [],
|
||||
"soft_suggestions": [
|
||||
{
|
||||
"id": "SOFT_HOOK_STRENGTH",
|
||||
"severity": "medium",
|
||||
"location": "章末",
|
||||
"description": "钩子强度为weak,建议提升至medium",
|
||||
"suggestion": "将'回去休息了'改为悬念/危机",
|
||||
"can_override": true,
|
||||
"allowed_rationales": ["TRANSITIONAL_SETUP", "CHARACTER_CREDIBILITY"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"hook_present": true,
|
||||
"hook_type": "渴望钩",
|
||||
"hook_strength": "medium",
|
||||
"prev_hook_fulfilled": true,
|
||||
"new_expectations": 2,
|
||||
"pattern_repeat_risk": false,
|
||||
"micropayoffs": ["能力兑现", "认可兑现"],
|
||||
"micropayoff_count": 2,
|
||||
"is_transition": false,
|
||||
"next_chapter_reason": "读者想知道云芝找萧炎什么事",
|
||||
"debt_balance": 0.0
|
||||
},
|
||||
"summary": "硬约束通过,钩子强度偏弱,建议增强章末期待。",
|
||||
"override_eligible": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、约束分层
|
||||
|
||||
### 1.1 硬约束
|
||||
|
||||
> **违反 = 必须修复,不可申诉跳过**
|
||||
|
||||
| ID | 约束名称 | 触发条件 | severity |
|
||||
|----|---------|---------|----------|
|
||||
| HARD-001 | 可读性底线 | 读者无法理解"发生了什么/谁/为什么" | critical |
|
||||
| HARD-002 | 承诺违背 | 上章明确承诺在本章完全无回应 | critical |
|
||||
| HARD-003 | 节奏灾难 | 连续N章无任何推进(N由profile决定) | critical |
|
||||
| HARD-004 | 冲突真空 | 整章无问题/目标/代价 | high |
|
||||
|
||||
**硬约束违规输出**:
|
||||
```json
|
||||
{
|
||||
"id": "HARD-002",
|
||||
"severity": "critical",
|
||||
"location": "全章",
|
||||
"description": "上章钩子'敌人即将到来'完全未在本章提及",
|
||||
"must_fix": true,
|
||||
"fix_suggestion": "在开头或中段回应敌人威胁"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 软建议
|
||||
|
||||
> **违反 = 可申诉,但需记录 `Override Contract` 并承担债务**
|
||||
|
||||
| ID | 约束名称 | 默认期望 | 可覆盖 |
|
||||
|----|---------|---------|-----------|
|
||||
| SOFT_NEXT_REASON | 下章动机 | 读者能明确"为何点下一章" | ✓ |
|
||||
| SOFT_HOOK_ANCHOR | 期待锚点有效性 | 有未闭合问题或明确期待(章末/后段均可) | ✓ |
|
||||
| SOFT_HOOK_STRENGTH | 钩子强度 | 题材profile baseline | ✓ |
|
||||
| SOFT_HOOK_TYPE | 钩子类型 | 匹配题材偏好 | ✓ |
|
||||
| SOFT_MICROPAYOFF | 微兑现数量 | ≥ profile.min_per_chapter | ✓ |
|
||||
| SOFT_PATTERN_REPEAT | 模式重复 | 避免连续3章同型 | ✓ |
|
||||
| SOFT_EXPECTATION_OVERLOAD | 期待过载 | 新增期待 ≤ 2 | ✓ |
|
||||
| SOFT_RHYTHM_NATURALNESS | 节奏自然性 | 避免固定字距机械打点 | ✓ |
|
||||
|
||||
**软建议输出**:
|
||||
```json
|
||||
{
|
||||
"id": "SOFT_MICROPAYOFF",
|
||||
"severity": "medium",
|
||||
"location": "全章",
|
||||
"description": "本章微兑现0个,题材要求≥1",
|
||||
"suggestion": "添加能力兑现或认可兑现",
|
||||
"can_override": true,
|
||||
"allowed_rationales": ["TRANSITIONAL_SETUP", "ARC_TIMING"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、钩子类型扩展
|
||||
|
||||
### 2.1 完整钩子类型
|
||||
|
||||
| 类型 | 标识 | 驱动力 |
|
||||
|------|------|--------|
|
||||
| 危机钩 | Crisis Hook | 危险逼近,读者担心 |
|
||||
| 悬念钩 | Mystery Hook | 信息缺口,读者好奇 |
|
||||
| 情绪钩 | Emotion Hook | 强情绪触发(愤怒/心疼/心动) |
|
||||
| 选择钩 | Choice Hook | 两难抉择,读者想知道选择 |
|
||||
| 渴望钩 | Desire Hook | 好事将至,读者期待 |
|
||||
|
||||
### 2.2 钩子强度
|
||||
|
||||
| 强度 | 适用场景 | 特征 |
|
||||
|------|---------|------|
|
||||
| **strong** | 卷末/关键转折/大冲突前 | 读者必须立刻知道 |
|
||||
| **medium** | 普通剧情章 | 读者想知道,但可等 |
|
||||
| **weak** | 过渡章/铺垫章 | 维持阅读惯性 |
|
||||
|
||||
---
|
||||
|
||||
## 三、微兑现检测
|
||||
|
||||
### 3.1 微兑现类型
|
||||
|
||||
| 类型 | 识别信号 |
|
||||
|------|---------|
|
||||
| 信息兑现 | 揭示新信息/线索/真相 |
|
||||
| 关系兑现 | 关系推进/确认/变化 |
|
||||
| 能力兑现 | 能力提升/新技能展示 |
|
||||
| 资源兑现 | 获得物品/资源/财富 |
|
||||
| 认可兑现 | 获得认可/面子/地位 |
|
||||
| 情绪兑现 | 情绪释放/共鸣 |
|
||||
| 线索兑现 | 伏笔回收/推进 |
|
||||
|
||||
### 3.2 检测规则
|
||||
|
||||
1. 扫描正文识别微兑现
|
||||
2. 按题材profile检查数量是否达标
|
||||
3. 过渡章可降级要求
|
||||
|
||||
---
|
||||
|
||||
## 四、模式重复检测
|
||||
|
||||
### 4.1 检测范围
|
||||
- 钩子类型:最近3章
|
||||
- 开头模式:最近3章
|
||||
- 爽点模式:最近5章
|
||||
|
||||
### 4.2 风险等级
|
||||
- **warning**: 连续2章同型
|
||||
- **risk**: 连续3章同型
|
||||
- **critical**: 连续4+章同型
|
||||
|
||||
---
|
||||
|
||||
## 五、`Override Contract` 机制
|
||||
|
||||
### 5.1 何时可覆盖
|
||||
|
||||
当 `soft_suggestions` 中的建议无法遵守时,可提交 `Override Contract`:
|
||||
|
||||
```json
|
||||
{
|
||||
"constraint_type": "SOFT_MICROPAYOFF",
|
||||
"constraint_id": "micropayoff_count",
|
||||
"rationale_type": "TRANSITIONAL_SETUP",
|
||||
"rationale_text": "本章为铺垫章,下章将有大爽点",
|
||||
"payback_plan": "下章补偿2个微兑现",
|
||||
"due_chapter": 101
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 rationale_type 枚举
|
||||
|
||||
| 类型 | 描述 | 债务影响 |
|
||||
|------|------|---------|
|
||||
| TRANSITIONAL_SETUP | 铺垫/过渡需要 | 标准 |
|
||||
| LOGIC_INTEGRITY | 剧情逻辑优先 | 减少 |
|
||||
| CHARACTER_CREDIBILITY | 人物可信度优先 | 减少 |
|
||||
| WORLD_RULE_CONSTRAINT | 设定约束 | 减少 |
|
||||
| ARC_TIMING | 长线节奏安排 | 标准 |
|
||||
| GENRE_CONVENTION | 题材惯例 | 标准 |
|
||||
| EDITORIAL_INTENT | 作者主观意图 | 增加 |
|
||||
|
||||
### 5.3 债务与利息
|
||||
|
||||
- 每个 `Override` 产生债务(量由题材 profile 的 `debt_multiplier` 决定)
|
||||
- 每章债务累积利息(默认10%/章)
|
||||
- 超过 `due_chapter` 未偿还,债务变为 `overdue`
|
||||
|
||||
---
|
||||
|
||||
## 六、执行步骤
|
||||
|
||||
### Step 1: 加载配置
|
||||
1. 读取题材Profile
|
||||
2. 读取上章钩子/模式记录
|
||||
3. 检查当前债务状态
|
||||
|
||||
### Step 2: 硬约束检查
|
||||
1. 检查可读性(关键信息完整性)
|
||||
2. 检查上章钩子兑现
|
||||
3. 检查节奏停滞
|
||||
4. 检查冲突存在
|
||||
|
||||
**任何硬约束违规 → 立即标记为必须修复**
|
||||
|
||||
### Step 3: 钩子分析
|
||||
1. 识别本章期待锚点(优先章末,允许后段)
|
||||
2. 评估钩子强度与有效性
|
||||
3. 对比题材偏好与章节类型
|
||||
|
||||
### Step 4: 微兑现扫描
|
||||
1. 识别章内微兑现
|
||||
2. 统计数量和类型
|
||||
3. 对比题材要求
|
||||
|
||||
### Step 5: 模式重复检测
|
||||
1. 获取最近N章模式
|
||||
2. 检测钩子类型重复
|
||||
3. 检测开头模式重复
|
||||
|
||||
### Step 6: 软建议评估
|
||||
1. 汇总所有软建议
|
||||
2. 标注可覆盖的建议
|
||||
3. 列出允许的 `rationale` 类型
|
||||
|
||||
### Step 7: 生成报告
|
||||
1. 计算总分
|
||||
2. 输出结构化JSON
|
||||
3. 提供修复建议
|
||||
|
||||
---
|
||||
|
||||
## 七、评分规则
|
||||
|
||||
### 7.1 硬约束违规
|
||||
- 任何硬约束违规 → 直接未通过
|
||||
- 必须修复后重新审核
|
||||
|
||||
### 7.2 软评分(无硬约束违规时)
|
||||
|
||||
| 得分 | 结果 |
|
||||
|------|------|
|
||||
| 85+ | 通过 |
|
||||
| 70-84 | 通过(有警告) |
|
||||
| 50-69 | 条件通过(可通过 `Override`)|
|
||||
| <50 | 未通过 |
|
||||
|
||||
### 7.3 软评分计算
|
||||
|
||||
| 检查项 | 权重 | 问题类型 |
|
||||
|--------|------|----------|
|
||||
| 下章动机清晰 | 20% | NEXT_REASON_WEAK |
|
||||
| 期待锚点有效(章末/后段) | 15% | WEAK_HOOK_ANCHOR |
|
||||
| 钩子强度适当 | 10% | WEAK_HOOK |
|
||||
| 微兑现达标 | 20% | LOW_MICROPAYOFF |
|
||||
| 模式不重复 | 15% | PATTERN_REPEAT |
|
||||
| 新增期待≤2个 | 10% | EXPECTATION_OVERLOAD |
|
||||
| 钩子类型匹配题材 | 5% | TYPE_MISMATCH |
|
||||
| 节奏自然性(非机械打点) | 5% | MECHANICAL_PACING |
|
||||
|
||||
---
|
||||
|
||||
## 八、与 Data Agent 的交互
|
||||
|
||||
审核完成后,由 Data Agent 执行:
|
||||
|
||||
1. **保存章节追读力元数据**
|
||||
```python
|
||||
index_manager.save_chapter_reading_power(ChapterReadingPowerMeta(...))
|
||||
```
|
||||
|
||||
2. **处理 `Override Contract`**(如有)
|
||||
```python
|
||||
index_manager.create_override_contract(OverrideContractMeta(...))
|
||||
index_manager.create_debt(ChaseDebtMeta(...))
|
||||
```
|
||||
|
||||
3. **计算利息**(每章)
|
||||
```python
|
||||
index_manager.accrue_interest(current_chapter)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、成功标准
|
||||
|
||||
- [ ] 无硬约束违规
|
||||
- [ ] 软评分 ≥ 70(或有有效 `Override`)
|
||||
- [ ] 存在可感知的未闭合问题/期待锚点(章末或后段)
|
||||
- [ ] 微兑现数量达标(或有 `Override`)
|
||||
- [ ] 无连续3章以上同型
|
||||
- [ ] 输出清晰的"下章动机"
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Reader-Pull Checker (追读力检查器)
|
||||
|
||||
检查钩子强度、期待管理、追读力:
|
||||
- 章节开头的钩子
|
||||
- 章节结尾的悬念
|
||||
- 追读力债务
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReaderPullIssue:
|
||||
"""追读力问题"""
|
||||
chapter: int
|
||||
issue_type: str # weak_hook / no_ending_hook / debt_high
|
||||
description: str
|
||||
severity: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReaderPullReport:
|
||||
"""追读力检查报告"""
|
||||
chapter: int
|
||||
hook_strength: float # 0-1
|
||||
ending_hook: bool
|
||||
debt_level: float
|
||||
issues: List[ReaderPullIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ReaderPullChecker:
|
||||
"""
|
||||
追读力检查器
|
||||
|
||||
检查:
|
||||
1. 章节开头的钩子强度
|
||||
2. 章节结尾的悬念
|
||||
3. 追读力债务水平
|
||||
"""
|
||||
|
||||
# 强钩子关键词
|
||||
STRONG_HOOK_KEYWORDS = [
|
||||
"危机", "危险", "困境", "生死", "抉择", "真相",
|
||||
"震惊", "意外", "转折", "揭秘", "冲突", "大战"
|
||||
]
|
||||
|
||||
# 结尾悬念关键词
|
||||
ENDING_HOOK_KEYWORDS = [
|
||||
"然而", "但是", "就在这时", "突然", "意外",
|
||||
"未完待续", "敬请期待", "欲知后事", "悬念",
|
||||
"却不知道", "更大的"
|
||||
]
|
||||
|
||||
# 悬念关键词
|
||||
SUSPENSE_KEYWORDS = [
|
||||
"悬念", "伏笔", "暗示", "预示", "危机", "威胁"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.chapter_debts: List[float] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_debt: float = 0.0
|
||||
) -> ReaderPullReport:
|
||||
"""检查单章追读力"""
|
||||
# 分析开头钩子
|
||||
opening = chapter_text[:500] if len(chapter_text) > 500 else chapter_text
|
||||
hook_strength = self._analyze_hook_strength(opening)
|
||||
|
||||
# 分析结尾悬念
|
||||
ending = chapter_text[-500:] if len(chapter_text) > 500 else chapter_text
|
||||
ending_hook = self._has_ending_hook(ending)
|
||||
|
||||
# 计算本章追读力债务
|
||||
debt_level = self._calculate_debt_level(chapter_text, hook_strength, ending_hook)
|
||||
|
||||
report = ReaderPullReport(
|
||||
chapter=chapter_num,
|
||||
hook_strength=hook_strength,
|
||||
ending_hook=ending_hook,
|
||||
debt_level=debt_level
|
||||
)
|
||||
|
||||
# 检测问题
|
||||
if hook_strength < 0.3:
|
||||
report.issues.append(ReaderPullIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="weak_hook",
|
||||
description="章节开头钩子较弱,可能无法吸引读者",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
if not ending_hook:
|
||||
report.issues.append(ReaderPullIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="no_ending_hook",
|
||||
description="章节结尾缺少悬念,读者缺乏继续阅读的动力",
|
||||
severity="high"
|
||||
))
|
||||
|
||||
# 检查追读力债务
|
||||
total_debt = previous_debt + debt_level
|
||||
if total_debt > 80:
|
||||
report.issues.append(ReaderPullIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="debt_high",
|
||||
description=f"追读力债务过高({total_debt:.0f}),建议释放",
|
||||
severity="critical"
|
||||
))
|
||||
|
||||
self.chapter_debts.append(debt_level)
|
||||
report.passed = len([i for i in report.issues if i.severity == "critical"]) == 0
|
||||
|
||||
if report.passed:
|
||||
report.summary = f"钩子强度 {hook_strength:.0%},追读力债务 {total_debt:.0f}"
|
||||
else:
|
||||
report.summary = f"发现问题: {', '.join(i.issue_type for i in report.issues)}"
|
||||
|
||||
return report
|
||||
|
||||
def _analyze_hook_strength(self, opening: str) -> float:
|
||||
"""分析钩子强度"""
|
||||
if not opening:
|
||||
return 0.0
|
||||
|
||||
strength = 0.3 # 基础分
|
||||
|
||||
# 检查强钩子关键词
|
||||
strong_count = sum(1 for kw in self.STRONG_HOOK_KEYWORDS if kw in opening)
|
||||
strength += min(0.4, strong_count * 0.1)
|
||||
|
||||
# 检查疑问句
|
||||
question_count = opening.count('?') + opening.count('?')
|
||||
strength += min(0.2, question_count * 0.1)
|
||||
|
||||
# 检查省略号(制造悬念)
|
||||
ellipsis_count = opening.count('...')
|
||||
strength += min(0.1, ellipsis_count * 0.05)
|
||||
|
||||
return min(1.0, strength)
|
||||
|
||||
def _has_ending_hook(self, ending: str) -> bool:
|
||||
"""检查结尾是否有悬念"""
|
||||
if not ending:
|
||||
return False
|
||||
|
||||
# 检查结尾悬念关键词
|
||||
for kw in self.ENDING_HOOK_KEYWORDS:
|
||||
if kw in ending:
|
||||
return True
|
||||
|
||||
# 检查是否以冲突/悬念结尾
|
||||
suspense_ending = ["?", "!", "……"]
|
||||
if any(ending.strip().endswith(s) for s in suspense_ending):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _calculate_debt_level(
|
||||
self,
|
||||
text: str,
|
||||
hook_strength: float,
|
||||
ending_hook: bool
|
||||
) -> float:
|
||||
"""计算追读力债务"""
|
||||
debt = 0.0
|
||||
|
||||
# 无开头钩子
|
||||
if hook_strength < 0.3:
|
||||
debt += 20
|
||||
|
||||
# 无结尾悬念
|
||||
if not ending_hook:
|
||||
debt += 30
|
||||
|
||||
# 积累的悬念/伏笔
|
||||
unresolved_count = 0
|
||||
for kw in self.SUSPENSE_KEYWORDS:
|
||||
unresolved_count += text.count(kw)
|
||||
|
||||
debt += min(30, unresolved_count * 5)
|
||||
|
||||
# 钩子强度可以抵消部分债务
|
||||
debt -= hook_strength * 20
|
||||
|
||||
return max(0, min(100, debt))
|
||||
|
||||
def get_arc_reader_pull(self, start_chapter: int, end_chapter: int) -> Dict[str, Any]:
|
||||
"""分析情节弧的追读力"""
|
||||
if not self.chapter_debts:
|
||||
return {"status": "no_data"}
|
||||
|
||||
relevant = self.chapter_debts[start_chapter - 1:end_chapter]
|
||||
|
||||
return {
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"avg_hook_strength": sum(h.hook_strength for h in self.chapter_debts[start_chapter-1:end_chapter]) / len(relevant) if relevant else 0,
|
||||
"avg_debt": sum(relevant) / len(relevant) if relevant else 0,
|
||||
"debt_trend": relevant[-1] - relevant[0] if len(relevant) > 1 else 0
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
Tension Checker (情绪压强校验器)
|
||||
|
||||
取代原爽点审查,校验情绪压强的积累与释放是否符合选定的 catharsis model。
|
||||
检测"降维打击"、"禁忌僭越"等爽感路由的执行效果。
|
||||
与 ledger.ts 中的 hooks_pool 压强联动。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class CatharsisModel(Enum):
|
||||
"""爽感路由模型"""
|
||||
TABOO_TRANSGRESSION = "taboo_transgression" # 禁忌僭越
|
||||
OVERKILL_REVERSAL = "overkill_reversal" # 降维打击
|
||||
COGNITIVE_CLOSURE = "cognitive_closure" # 认知闭环
|
||||
STRAND_WEAVE = "strand_weave" # 三轨编织
|
||||
|
||||
|
||||
class TensionLevel(Enum):
|
||||
"""张力等级"""
|
||||
LOW = "low" # < 30
|
||||
MEDIUM = "medium" # 30-60
|
||||
HIGH = "high" # 60-80
|
||||
DANGER = "danger" # > 80
|
||||
|
||||
|
||||
@dataclass
|
||||
class TensionReading:
|
||||
"""张力读数"""
|
||||
chapter: int
|
||||
start_tension: int # 起始压强 (0-100)
|
||||
end_tension: int # 结束压强 (0-100)
|
||||
peak_tension: int # 峰值压强
|
||||
catharsis_model: Optional[CatharsisModel]
|
||||
satisfaction_delivered: int # 爽感释放值 (0-100)
|
||||
issues: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TensionIssue:
|
||||
"""张力问题"""
|
||||
chapter: int
|
||||
issue_type: str # buildup_too_long / release_misaligned / pattern_broken
|
||||
description: str
|
||||
severity: str # critical/high/medium/low
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
class TensionChecker:
|
||||
"""
|
||||
情绪压强校验器
|
||||
|
||||
功能:
|
||||
1. 校验压强曲线是否符合 catharsis model
|
||||
2. 检测爽感释放时机
|
||||
3. 与 hooks_pool 联动
|
||||
4. 输出张力分析报告
|
||||
"""
|
||||
|
||||
# 各模型的标准张力曲线
|
||||
MODEL_CURVES = {
|
||||
CatharsisModel.TABOO_TRANSGRESSION: {
|
||||
"setup": (1, 30), # 阶段: (持续章节数, 目标压强)
|
||||
"approach": (3, 50),
|
||||
"crisis": (1, 85),
|
||||
"transgression": (1, 100),
|
||||
"fallout": (2, 40),
|
||||
},
|
||||
CatharsisModel.OVERKILL_REVERSAL: {
|
||||
"establish": (2, 40), # 建立敌人强大
|
||||
"suppress": (2, 30), # 主角被压制
|
||||
"trigger": (1, 50), # 金手指触发
|
||||
"reversal": (1, 100), # 降维打击
|
||||
"aftermath": (1, 60), # 余波
|
||||
},
|
||||
CatharsisModel.COGNITIVE_CLOSURE: {
|
||||
"setup": (3, 40), # 多线伏笔埋设
|
||||
"weave": (2, 55), # 线索交织
|
||||
"activate": (1, 75), # 伏笔激活
|
||||
"closure": (1, 100), # 认知闭合
|
||||
"new_hook": (1, 50), # 新悬念
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, ledger_path: Optional[str] = None):
|
||||
self.ledger_path = ledger_path
|
||||
self.hooks_pool: Dict[str, Any] = {}
|
||||
self.tension_history: List[TensionReading] = []
|
||||
|
||||
def load_ledger(self, ledger_data: Dict[str, Any]) -> None:
|
||||
"""加载账本数据"""
|
||||
self.hooks_pool = ledger_data.get("hooks", {})
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter: int,
|
||||
chapter_text: str,
|
||||
catharsis_model: Optional[CatharsisModel] = None,
|
||||
previous_tension: int = 30
|
||||
) -> TensionReading:
|
||||
"""
|
||||
检查单章张力
|
||||
|
||||
Args:
|
||||
chapter: 章节号
|
||||
chapter_text: 章节正文
|
||||
catharsis_model: 使用的爽感模型(可选)
|
||||
previous_tension: 上一章结束时的压强
|
||||
|
||||
Returns:
|
||||
TensionReading: 张力读数
|
||||
"""
|
||||
# 计算起始/结束/峰值压强
|
||||
start_tension = previous_tension
|
||||
|
||||
# 分析本章爽感事件
|
||||
climax_events = self._detect_climax_events(chapter_text)
|
||||
|
||||
# 计算结束压强
|
||||
if climax_events["major_climax"]:
|
||||
end_tension = max(20, previous_tension - 40) # 大高潮后压强下降
|
||||
peak_tension = 100
|
||||
elif climax_events["minor_climax"]:
|
||||
end_tension = max(30, previous_tension - 20) # 小高潮后压强下降
|
||||
peak_tension = max(70, previous_tension + 20)
|
||||
elif climax_events["tension_build"]:
|
||||
end_tension = min(90, previous_tension + 15) # 积累压强上升
|
||||
peak_tension = end_tension
|
||||
else:
|
||||
end_tension = previous_tension # 平稳过渡
|
||||
peak_tension = previous_tension
|
||||
|
||||
# 计算爽感释放值
|
||||
satisfaction = self._calculate_satisfaction(climax_events)
|
||||
|
||||
# 检测问题
|
||||
issues = self._detect_issues(chapter, previous_tension, end_tension, catharsis_model, climax_events)
|
||||
|
||||
reading = TensionReading(
|
||||
chapter=chapter,
|
||||
start_tension=start_tension,
|
||||
end_tension=end_tension,
|
||||
peak_tension=peak_tension,
|
||||
catharsis_model=catharsis_model,
|
||||
satisfaction_delivered=satisfaction,
|
||||
issues=issues
|
||||
)
|
||||
|
||||
self.tension_history.append(reading)
|
||||
return reading
|
||||
|
||||
def _detect_climax_events(self, text: str) -> Dict[str, Any]:
|
||||
"""检测章节中的高潮事件"""
|
||||
events = {
|
||||
"major_climax": False, # 大高潮(降维打击、禁忌突破等)
|
||||
"minor_climax": False, # 小高潮(打脸、收获等)
|
||||
"tension_build": False, # 压强积累
|
||||
}
|
||||
|
||||
# 关键词检测
|
||||
major_keywords = [
|
||||
"碾压", "秒杀", "一击", "降维打击", "禁忌突破",
|
||||
"震惊", "不可思议", "全场寂静", "彻底碾压"
|
||||
]
|
||||
minor_keywords = [
|
||||
"冷笑", "一掌", "打脸", "收获", "突破",
|
||||
"提升", "获得", "机缘", "惊喜"
|
||||
]
|
||||
buildup_keywords = [
|
||||
"危机", "困境", "压力", "紧张", "危机四伏",
|
||||
"蓄势", "积累", "暗中", "谋划"
|
||||
]
|
||||
|
||||
text_length = len(text)
|
||||
|
||||
for keyword in major_keywords:
|
||||
if keyword in text:
|
||||
events["major_climax"] = True
|
||||
break
|
||||
|
||||
if not events["major_climax"]:
|
||||
for keyword in minor_keywords:
|
||||
if keyword in text:
|
||||
events["minor_climax"] = True
|
||||
break
|
||||
|
||||
for keyword in buildup_keywords:
|
||||
if keyword in text:
|
||||
events["tension_build"] = True
|
||||
break
|
||||
|
||||
return events
|
||||
|
||||
def _calculate_satisfaction(self, events: Dict[str, Any]) -> int:
|
||||
"""计算爽感释放值"""
|
||||
if events["major_climax"]:
|
||||
return 90
|
||||
elif events["minor_climax"]:
|
||||
return 60
|
||||
elif events["tension_build"]:
|
||||
return 20
|
||||
else:
|
||||
return 10
|
||||
|
||||
def _detect_issues(
|
||||
self,
|
||||
chapter: int,
|
||||
previous_tension: int,
|
||||
current_tension: int,
|
||||
catharsis_model: Optional[CatharsisModel],
|
||||
events: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""检测张力问题"""
|
||||
issues = []
|
||||
|
||||
# 问题1: 压强过高持续太久
|
||||
if previous_tension > 85 and current_tension > 85:
|
||||
issues.append("⚠️ 压强持续过高 (>85),可能导致读者疲劳")
|
||||
|
||||
# 问题2: 压强过低但无爽感
|
||||
if current_tension < 30 and not events["major_climax"] and not events["minor_climax"]:
|
||||
issues.append("⚠️ 压强过低且无爽感释放,节奏可能拖沓")
|
||||
|
||||
# 问题3: 压强突然下降
|
||||
if previous_tension > 50 and current_tension < previous_tension - 30:
|
||||
if not events["major_climax"] and not events["minor_climax"]:
|
||||
issues.append("⚠️ 压强突然下降但无对应爽感事件")
|
||||
|
||||
# 问题4: 压强过高但无释放
|
||||
if current_tension > 90 and not events["major_climax"]:
|
||||
issues.append("🔴 压强达到危险水平 (>90) 但无高潮释放")
|
||||
|
||||
return issues
|
||||
|
||||
def check_arc_tension(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int,
|
||||
catharsis_model: CatharsisModel
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
检查情节弧的张力曲线
|
||||
|
||||
Args:
|
||||
start_chapter: 起始章节
|
||||
end_chapter: 结束章节
|
||||
catharsis_model: 使用的爽感模型
|
||||
|
||||
Returns:
|
||||
分析报告
|
||||
"""
|
||||
relevant_readings = [
|
||||
r for r in self.tension_history
|
||||
if start_chapter <= r.chapter <= end_chapter
|
||||
]
|
||||
|
||||
if not relevant_readings:
|
||||
return {"status": "no_data", "message": "无张力历史数据"}
|
||||
|
||||
model_curve = self.MODEL_CURVES.get(catharsis_model, {})
|
||||
if not model_curve:
|
||||
return {"status": "unknown_model", "message": f"未知的爽感模型: {catharsis_model}"}
|
||||
|
||||
# 分析曲线是否符合模型
|
||||
expected_phases = list(model_curve.keys())
|
||||
actual_phases = self._infer_phases(relevant_readings)
|
||||
|
||||
# 检查匹配度
|
||||
match_score = self._calculate_curve_match(model_curve, relevant_readings)
|
||||
|
||||
# 生成报告
|
||||
report = {
|
||||
"status": "analyzed",
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"model": catharsis_model.value,
|
||||
"match_score": match_score,
|
||||
"phases": {
|
||||
"expected": expected_phases,
|
||||
"actual": actual_phases
|
||||
},
|
||||
"tension_summary": {
|
||||
"avg_start": sum(r.start_tension for r in relevant_readings) / len(relevant_readings),
|
||||
"avg_end": sum(r.end_tension for r in relevant_readings) / len(relevant_readings),
|
||||
"max_peak": max(r.peak_tension for r in relevant_readings),
|
||||
"total_satisfaction": sum(r.satisfaction_delivered for r in relevant_readings)
|
||||
},
|
||||
"issues": self._collect_arc_issues(relevant_readings, model_curve),
|
||||
"recommendations": self._generate_recommendations(match_score, relevant_readings)
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
def _infer_phases(self, readings: List[TensionReading]) -> List[str]:
|
||||
"""从张力读数推断阶段"""
|
||||
phases = []
|
||||
for r in readings:
|
||||
if r.peak_tension >= 90:
|
||||
phases.append("peak")
|
||||
elif r.peak_tension >= 70:
|
||||
phases.append("elevated")
|
||||
elif r.end_tension > r.start_tension:
|
||||
phases.append("buildup")
|
||||
elif r.end_tension < r.start_tension:
|
||||
phases.append("release")
|
||||
else:
|
||||
phases.append("maintain")
|
||||
return phases
|
||||
|
||||
def _calculate_curve_match(
|
||||
self,
|
||||
expected_curve: Dict[str, tuple],
|
||||
readings: List[TensionReading]
|
||||
) -> float:
|
||||
"""计算曲线匹配度 (0-100)"""
|
||||
if not readings:
|
||||
return 0.0
|
||||
|
||||
# 简化实现:检查峰值是否出现
|
||||
has_peak = any(r.peak_tension >= 85 for r in readings)
|
||||
has_release = any(r.end_tension < r.start_tension for r in readings)
|
||||
|
||||
score = 50 # 基础分
|
||||
|
||||
if has_peak:
|
||||
score += 30
|
||||
if has_release:
|
||||
score += 20
|
||||
|
||||
return min(100, score)
|
||||
|
||||
def _collect_arc_issues(
|
||||
self,
|
||||
readings: List[TensionReading],
|
||||
expected_curve: Dict[str, tuple]
|
||||
) -> List[str]:
|
||||
"""收集情节弧问题"""
|
||||
issues = []
|
||||
|
||||
# 检查是否有峰值
|
||||
if not any(r.peak_tension >= 85 for r in readings):
|
||||
issues.append("缺少高潮点,张力未达到峰值")
|
||||
|
||||
# 检查是否有释放
|
||||
if not any(r.end_tension < r.start_tension for r in readings):
|
||||
issues.append("张力未释放,可能导致读者疲劳")
|
||||
|
||||
# 检查压强过高
|
||||
high_tension_count = sum(1 for r in readings if r.end_tension > 80)
|
||||
if high_tension_count > len(readings) * 0.5:
|
||||
issues.append("长时间高压状态,需要适当缓冲")
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_recommendations(
|
||||
self,
|
||||
match_score: float,
|
||||
readings: List[TensionReading]
|
||||
) -> List[str]:
|
||||
"""生成改进建议"""
|
||||
recommendations = []
|
||||
|
||||
if match_score < 50:
|
||||
recommendations.append("张力曲线偏离模型,建议重新规划节奏")
|
||||
elif match_score < 80:
|
||||
recommendations.append("张力曲线基本符合,可适当优化")
|
||||
|
||||
# 基于历史数据建议
|
||||
avg_tension = sum(r.end_tension for r in readings) / len(readings)
|
||||
if avg_tension > 70:
|
||||
recommendations.append("平均张力偏高,建议增加缓冲章节")
|
||||
|
||||
return recommendations
|
||||
|
||||
def get_tension_level(self, tension: int) -> TensionLevel:
|
||||
"""获取张力等级"""
|
||||
if tension < 30:
|
||||
return TensionLevel.LOW
|
||||
elif tension < 60:
|
||||
return TensionLevel.MEDIUM
|
||||
elif tension < 80:
|
||||
return TensionLevel.HIGH
|
||||
else:
|
||||
return TensionLevel.DANGER
|
||||
|
||||
def get_recommended_model(self, current_tension: int, target_tension: int) -> CatharsisModel:
|
||||
"""根据当前/目标压强推荐爽感模型"""
|
||||
tension_delta = target_tension - current_tension
|
||||
|
||||
if tension_delta > 40:
|
||||
# 需要大释放 -> 降维打击
|
||||
return CatharsisModel.OVERKILL_REVERSAL
|
||||
elif tension_delta > 20:
|
||||
# 中等释放 -> 禁忌僭越
|
||||
return CatharsisModel.TABOO_TRANSGRESSION
|
||||
else:
|
||||
# 认知闭合
|
||||
return CatharsisModel.COGNITIVE_CLOSURE
|
||||
|
||||
def export_history(self) -> str:
|
||||
"""导出张力历史为 JSON"""
|
||||
return json.dumps([
|
||||
{
|
||||
"chapter": r.chapter,
|
||||
"start_tension": r.start_tension,
|
||||
"end_tension": r.end_tension,
|
||||
"peak_tension": r.peak_tension,
|
||||
"catharsis_model": r.catharsis_model.value if r.catharsis_model else None,
|
||||
"satisfaction": r.satisfaction_delivered,
|
||||
"issues": r.issues
|
||||
}
|
||||
for r in self.tension_history
|
||||
], ensure_ascii=False, indent=2)
|
||||
@@ -0,0 +1,269 @@
|
||||
---
|
||||
name: context-agent
|
||||
description: 上下文搜集Agent,内置 Context Contract,输出可被 Step 2A 直接消费的创作执行包。
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# context-agent (上下文搜集Agent)
|
||||
|
||||
> **Role**: 创作执行包生成器。目标是"能直接开写",不堆信息。
|
||||
> **Philosophy**: 按需召回 + 推断补全,确保接住上章、场景清晰、留出钩子。
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **Taxonomy**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **Genre Profile**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
- **Context Contract**: `${CLAUDE_PLUGIN_ROOT}/skills/noma-write/references/step-1.5-contract.md`
|
||||
- **Shared References**: `${CLAUDE_PLUGIN_ROOT}/references/shared/` 为单一事实源;如需枚举/扫描参考文件,遇到 `<!-- DEPRECATED:` 的文件一律跳过。
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter": 100,
|
||||
"project_root": "D:/wk/斗破苍穹",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json"
|
||||
}
|
||||
```
|
||||
|
||||
## 输出格式:创作执行包(Step 2A 直连)
|
||||
|
||||
输出必须是单一执行包,包含 3 层:
|
||||
|
||||
1. **任务书(8板块)**
|
||||
- 本章核心任务(目标/阻力/代价、冲突一句话、必须完成、绝对不能、反派层级)
|
||||
- 接住上章(上章钩子、读者期待、开头建议)
|
||||
- 出场角色(状态、动机、情绪底色、说话风格、红线)
|
||||
- 场景与力量约束(地点、可用能力、禁用能力)
|
||||
- **时间约束(新增)**(上章时间锚点、本章时间锚点、允许推进跨度、时间过渡要求、倒计时状态)
|
||||
- 风格指导(本章类型、参考样本、最近模式、本章建议)
|
||||
- 连续性与伏笔(时间/位置/情绪连贯;必须处理/可选伏笔)
|
||||
- 追读力策略(未闭合问题 + 钩子类型/强度、微兑现建议、差异化提示)
|
||||
|
||||
2. **Context Contract(内置 Step 1.5)**
|
||||
- 目标、阻力、代价、本章变化、未闭合问题、核心冲突一句话
|
||||
- 开头类型、情绪节奏、信息密度
|
||||
- 是否过渡章(必须按大纲判定,禁止按字数判定)
|
||||
- 追读力设计(钩子类型/强度、微兑现清单、爽点模式)
|
||||
|
||||
3. **Step 2A 直写提示词**
|
||||
- 章节节拍(开场触发 → 推进/受阻 → 反转/兑现 → 章末钩子)
|
||||
- 不可变事实清单(大纲事实/设定事实/承接事实)
|
||||
- 禁止事项(越级能力、无因果跳转、设定冲突、剧情硬拐)
|
||||
- 终检清单(本章必须满足项 + fail 条件)
|
||||
|
||||
要求:
|
||||
- 三层信息必须一致;若冲突,以"设定 > 大纲 > 风格偏好"优先。
|
||||
- 输出内容必须能直接给 Step 2A 开写,不再依赖额外补问。
|
||||
|
||||
---
|
||||
|
||||
## 读取优先级与默认值
|
||||
|
||||
| 字段 | 读取来源 | 缺失时默认值 |
|
||||
|------|---------|-------------|
|
||||
| 上章钩子 | `chapter_meta[NNNN].hook` 或 `chapter_reading_power` | `{type: "无", content: "上章无明确钩子", strength: "weak"}` |
|
||||
| 最近3章模式 | `chapter_meta` 或 `chapter_reading_power` | 空数组,不做重复检查 |
|
||||
| 上章结束情绪 | `chapter_meta[NNNN].ending.emotion` | "未知"(提示自行判断) |
|
||||
| 角色动机 | 从大纲+角色状态推断 | **必须推断,无默认值** |
|
||||
| 题材Profile | `state.json → project.genre` | 默认 "shuangwen" |
|
||||
| 当前债务 | `index.db → chase_debt` | 0 |
|
||||
|
||||
**缺失处理**:
|
||||
- 若 `chapter_meta` 不存在(如第1章),跳过"接住上章"
|
||||
- 最近3章数据不完整时,只用现有数据做差异化检查
|
||||
- 若 `plot_threads.foreshadowing` 缺失或非列表:
|
||||
- 视为"当前无结构化伏笔数据",第 7 板块输出空清单并显式标注"数据缺失,需人工补录"
|
||||
- 禁止静默跳过第 7 板块
|
||||
|
||||
**章节编号规则**: 4位数字,如 `0001`, `0099`, `0100`
|
||||
|
||||
---
|
||||
|
||||
## 关键数据来源
|
||||
|
||||
- `state.json`: 进度、主角状态、strand_tracker、chapter_meta、project.genre、plot_threads.foreshadowing
|
||||
- `index.db`: 实体/别名/关系/状态变化/override_contracts/chase_debt/chapter_reading_power
|
||||
- `.noma/wiki/`: 结构化 Wiki(实体档案、伏笔线索、写作模式、关系图谱)
|
||||
- `..noma/novel_data/summaries/ch{NNNN}.md`: 章节摘要(含钩子/结束状态)
|
||||
- `..noma/novel_data/context_snapshots/`: 上下文快照(优先复用)
|
||||
- `大纲/` 与 `设定集/`
|
||||
|
||||
**钩子数据来源说明**:
|
||||
- **章纲的"钩子"字段**:本章应设置的章末钩子(规划用)
|
||||
- **chapter_meta[N].hook**:本章实际设置的钩子(执行结果)
|
||||
- **context-agent 读取**:chapter_meta[N-1].hook 作为"上章钩子"
|
||||
- **数据流**:章纲规划 → 写作实现 → 写入 chapter_meta → 下章读取
|
||||
|
||||
---
|
||||
|
||||
## 执行流程(精简版)
|
||||
|
||||
### Step -1: CLI 入口与脚本目录校验(必做)
|
||||
|
||||
为避免 `PYTHONPATH` / `cd` / 参数顺序导致的隐性失败,所有 CLI 调用统一走:
|
||||
- `${SCRIPTS_DIR}/noma.py`
|
||||
|
||||
```bash
|
||||
# 仅使用 CLAUDE_PLUGIN_ROOT,避免多路径探测带来的误判
|
||||
if [ -z "${CLAUDE_PLUGIN_ROOT}" ] || [ ! -d "${CLAUDE_PLUGIN_ROOT}/scripts" ]; then
|
||||
echo "ERROR: 未设置 CLAUDE_PLUGIN_ROOT 或缺少目录: ${CLAUDE_PLUGIN_ROOT}/scripts" >&2
|
||||
exit 1
|
||||
fi
|
||||
SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT}/scripts"
|
||||
|
||||
# 建议先确认解析出的 project_root,避免写到错误目录
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" where
|
||||
```
|
||||
|
||||
### Step 0: ContextManager 快照优先
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" context -- --chapter {NNNN}
|
||||
```
|
||||
|
||||
### Step 0.5: Context Contract 上下文包(内置)
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" extract-context --chapter {NNNN} --format json
|
||||
```
|
||||
|
||||
- 必须读取:`writing_guidance.guidance_items`
|
||||
- 推荐读取:`reader_signal` 与 `genre_profile.reference_hints`
|
||||
- 条件读取:`rag_assist`(当 `invoked=true` 且 `hits` 非空时,必须提炼成可执行约束,禁止只贴检索命中)
|
||||
|
||||
### Step 0.6: 时间线读取(新增,必做)
|
||||
|
||||
先确定 `{volume_id}`:
|
||||
- 优先读取 `state.json` 中当前卷信息(如有)
|
||||
- 若缺失,则从 `大纲/总纲.md` 的章节范围反推 `{NNNN}` 所在卷
|
||||
|
||||
读取本卷时间线表:
|
||||
```bash
|
||||
cat "{project_root}/大纲/第{volume_id}卷-时间线.md"
|
||||
```
|
||||
|
||||
从章纲提取本章时间字段:
|
||||
- `时间锚点`:本章发生的具体时间
|
||||
- `章内时间跨度`:本章覆盖的时间长度
|
||||
- `与上章时间差`:与上章的时间间隔
|
||||
- `倒计时状态`:若有倒计时事件的推进情况
|
||||
|
||||
从上章 chapter_meta 或章纲提取:
|
||||
- 上章结束时间锚点
|
||||
- 上章倒计时状态
|
||||
|
||||
生成时间约束输出(必须包含在任务书第 5 板块):
|
||||
```markdown
|
||||
## 时间约束
|
||||
- 上章时间锚点: {末世第3天 黄昏}
|
||||
- 本章时间锚点: {末世第4天 清晨}
|
||||
- 与上章时间差: {跨夜}
|
||||
- 本章允许推进: 最大 {章内时间跨度}
|
||||
- 时间过渡要求: {若跨夜/跨日,需补写的过渡句}
|
||||
- 倒计时状态: {物资耗尽 D-5 → D-4 / 无}
|
||||
```
|
||||
|
||||
**时间约束硬规则**:
|
||||
- 若 `与上章时间差` 为"跨夜"或"跨日",必须在任务书中标注"需补写时间过渡"
|
||||
- 若存在倒计时事件,必须校验推进是否正确(D-N 只能变为 D-(N-1),不可跳跃)
|
||||
- 时间锚点不得回跳(除非明确标注为闪回章节)
|
||||
|
||||
### Step 1: 读取大纲与状态
|
||||
- 大纲:`大纲/卷N/第XXX章.md` 或 `大纲/第{卷}卷-详细大纲.md`
|
||||
- 必须优先提取并写入任务书:目标/阻力/代价/反派层级/本章变化/章末未闭合问题/钩子(若存在)
|
||||
- `state.json`:progress / protagonist_state / chapter_meta / project.genre
|
||||
|
||||
### Step 2: 追读力与债务(按需)
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-recent-reading-power --limit 5
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-pattern-usage-stats --last-n 20
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-hook-type-stats --last-n 20
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-debt-summary
|
||||
```
|
||||
|
||||
### Step 3: 实体与最近出场 + 伏笔读取
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-core-entities
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index recent-appearances --limit 20
|
||||
```
|
||||
|
||||
- 从 `state.json` 读取:
|
||||
- `progress.current_chapter`
|
||||
- `plot_threads.foreshadowing`(主路径)
|
||||
- 缺失降级:
|
||||
- 若 `plot_threads.foreshadowing` 不存在或类型错误,置为空数组并打标 `foreshadowing_data_missing=true`
|
||||
- 对每条伏笔至少提取:
|
||||
- `content`
|
||||
- `planted_chapter`
|
||||
- `target_chapter`
|
||||
- `resolved_chapter`
|
||||
- `status`
|
||||
- 回收判定优先级:
|
||||
- 若 `resolved_chapter` 非空,直接视为已回收并排除(即使 `status` 文案异常)
|
||||
- 否则按 `status` 判定是否已回收
|
||||
- 生成排序键:
|
||||
- `remaining = target_chapter - current_chapter`(若缺失则记为 `null`)
|
||||
- 二次排序:`planted_chapter` 升序(更早埋设优先)
|
||||
- 三次排序:`content` 字典序(确保稳定)
|
||||
- 输出到第 7 板块时,按 `remaining` 升序列出。
|
||||
|
||||
### Step 4: 摘要与推断补全
|
||||
- 优先读取 `..noma/novel_data/summaries/ch{NNNN-1}.md`
|
||||
- 若缺失,降级为章节正文前 300-500 字概述
|
||||
- 推断规则:
|
||||
- 动机 = 角色目标 + 当前处境 + 上章钩子压力
|
||||
- 情绪底色 = 上章结束情绪 + 事件走向
|
||||
- 可用能力 = 当前境界 + 近期获得 + 设定禁用项
|
||||
|
||||
### Step 5: 组装创作执行包(任务书 + Context Contract + 直写提示词)
|
||||
输出可直接供 Step 2A 消费的单一执行包,不拆分独立 Step 1.5。
|
||||
|
||||
- 第 7 板块必须包含"伏笔优先级清单":
|
||||
- `必须处理(本章优先)`:`remaining <= 5` 或已超期(`remaining < 0`),全部列出不截断
|
||||
- `可选伏笔(可延后)`:最多 5 条
|
||||
- 第 7 板块生成规则(统一口径):
|
||||
- 仅纳入未回收伏笔(见 Step 3 回收判定)
|
||||
- 主排序按 `remaining` 升序,`remaining=null` 放末尾
|
||||
- 若 `必须处理` 超过 3 条:前 3 条标记"最高优先",其余标记"本章仍需处理"
|
||||
- 若 `可选伏笔` 超过 5 条:展示前 5 条并标注"其余 N 条可选伏笔已省略"
|
||||
- 若 `foreshadowing_data_missing=true`:明确输出"结构化伏笔数据缺失,当前清单仅供占位"
|
||||
|
||||
Context Contract 必须字段(不可缺):
|
||||
- `目标` / `阻力` / `代价` / `本章变化` / `未闭合问题`
|
||||
- `核心冲突一句话`
|
||||
- `开头类型` / `情绪节奏` / `信息密度`
|
||||
- `是否过渡章`
|
||||
- `追读力设计`
|
||||
|
||||
### Step 6: 逻辑红线校验(输出前强制)
|
||||
对执行包做一致性自检,任一 fail 则回到 Step 5 重组:
|
||||
|
||||
- 红线1:不可变事实冲突(大纲关键事件、设定规则、上章既有结果)
|
||||
- 红线2:时空跳跃无承接(地点/时间突变且无过渡)
|
||||
- 红线3:能力或信息无因果来源(突然会/突然知道)
|
||||
- 红线4:角色动机断裂(行为与近期目标明显冲突且无触发)
|
||||
- 红线5:合同与任务书冲突(例如"过渡章=true"却要求高强度高潮兑现)
|
||||
- **红线6:时间逻辑错误**(时间回跳、倒计时跳跃、大跨度无过渡)
|
||||
|
||||
通过标准:
|
||||
- 红线 fail 数 = 0
|
||||
- 执行包内包含"不可变事实清单 + 章节节拍 + 终检清单 + 时间约束"
|
||||
- Step 2A 在不补问情况下可直接起草正文
|
||||
|
||||
---
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. ✅ 创作执行包可直接驱动 Step 2A(无需补问)
|
||||
2. ✅ 任务书包含 8 个板块(含时间约束)
|
||||
3. ✅ 上章钩子与读者期待明确(若存在)
|
||||
4. ✅ 角色动机/情绪为推断结果(非空)
|
||||
5. ✅ 最近模式已对比,给出差异化建议
|
||||
6. ✅ 章末钩子建议类型明确
|
||||
7. ✅ 反派层级已注明(若大纲提供)
|
||||
8. ✅ 第 7 板块已基于 `plot_threads.foreshadowing` 按紧急度排序输出
|
||||
9. ✅ Context Contract 字段完整且与任务书一致
|
||||
10. ✅ 逻辑红线校验通过(fail=0)
|
||||
11. ✅ **时间约束板块完整**(上章时间锚点、本章时间锚点、允许推进跨度、过渡要求、倒计时状态)
|
||||
12. ✅ **时间逻辑红线通过**(无回跳、无倒计时跳跃、大跨度有过渡要求)
|
||||
@@ -0,0 +1,318 @@
|
||||
---
|
||||
name: data-agent
|
||||
description: 数据处理Agent,负责 AI 实体提取、场景切片、索引构建,并记录钩子/模式/结束状态与章节摘要。
|
||||
tools: Read, Write, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# data-agent (数据处理Agent)
|
||||
|
||||
> **职责**: 智能数据工程师,负责从章节正文中提取结构化信息并写入数据链。
|
||||
>
|
||||
> **原则**: AI驱动提取,智能消歧 - 用语义理解替代正则匹配,用置信度控制质量。
|
||||
|
||||
**命令示例即最终准则**:本文档中的所有 CLI 命令示例已与当前仓库真实接口对齐。脚本调用方式以本文档示例为准;命令失败时查错误日志定位问题,不去大范围翻源码学习调用方式。
|
||||
|
||||
**当前约定**:
|
||||
- 章节摘要不再追加到正文,改为 `..noma/novel_data/summaries/ch{NNNN}.md`
|
||||
- 在 state.json 写入 `chapter_meta`(钩子/模式/结束状态)
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter": 100,
|
||||
"chapter_file": "正文/第0100章-章节标题.md",
|
||||
"review_score": 85,
|
||||
"project_root": "D:/wk/斗破苍穹",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 必须传入实际章节文件路径。若详细大纲已有章节名,优先使用带标题文件名;旧的 `正文/第0100章.md` 仍兼容。
|
||||
|
||||
**重要**: 所有数据写入 `{project_root}/..noma/novel_data/` 目录:
|
||||
- index.db → 实体、别名、状态变化、关系、章节索引 (SQLite)
|
||||
- state.json → 进度、配置、节奏追踪 + chapter_meta
|
||||
- vectors.db → RAG 向量 (SQLite)
|
||||
- summaries/ → 章节摘要文件
|
||||
|
||||
## 输出
|
||||
|
||||
```json
|
||||
{
|
||||
"entities_appeared": [
|
||||
{"id": "xiaoyan", "type": "角色", "mentions": ["萧炎", "他"], "confidence": 0.95}
|
||||
],
|
||||
"entities_new": [
|
||||
{"suggested_id": "hongyi_girl", "name": "红衣女子", "type": "角色", "tier": "装饰"}
|
||||
],
|
||||
"state_changes": [
|
||||
{"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破"}
|
||||
],
|
||||
"relationships_new": [
|
||||
{"from": "xiaoyan", "to": "hongyi_girl", "type": "相识", "description": "初次见面"}
|
||||
],
|
||||
"scenes_chunked": 4,
|
||||
"uncertain": [
|
||||
{"mention": "那位前辈", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "confidence": 0.6}
|
||||
],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step -1: CLI 入口与脚本目录校验(必做)
|
||||
|
||||
为避免 `PYTHONPATH` / `cd` / 参数顺序导致的隐性失败,所有 CLI 调用统一走:
|
||||
- `${SCRIPTS_DIR}/noma.py`
|
||||
|
||||
```bash
|
||||
export SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts"
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" preflight
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" where
|
||||
```
|
||||
|
||||
### Step A: 加载上下文(SQL 查询)
|
||||
|
||||
使用 Read 工具读取章节正文:
|
||||
- 章节正文: 实际章节文件路径(优先 `正文/第0100章-章节标题.md`,旧格式 `正文/第0100章.md` 仍兼容)
|
||||
|
||||
使用 Bash 工具从 index.db 查询已有实体:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-core-entities
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-aliases --entity "xiaoyan"
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index recent-appearances --limit 20
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-by-alias --alias "萧炎"
|
||||
```
|
||||
|
||||
### Step B: AI 实体提取
|
||||
|
||||
**Data Agent 直接执行** (无需调用外部 LLM)。
|
||||
|
||||
### Step C: 实体消歧处理
|
||||
|
||||
**置信度策略**:
|
||||
|
||||
| 置信度范围 | 处理方式 |
|
||||
|-----------|---------|
|
||||
| > 0.8 | 自动采用,无需确认 |
|
||||
| 0.5 - 0.8 | 采用建议值,记录 warning |
|
||||
| < 0.5 | 标记待人工确认,不自动写入 |
|
||||
|
||||
### Step D: 写入存储
|
||||
|
||||
**写入 index.db (实体/别名/状态变化/关系)**:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index upsert-entity --data '{...}'
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index register-alias --alias "红衣女子" --entity "hongyi_girl" --type "角色"
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index record-state-change --data '{...}'
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index upsert-relationship --data '{...}'
|
||||
```
|
||||
|
||||
**更新精简版 state.json**:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" state process-chapter --chapter 100 --data '{...}'
|
||||
```
|
||||
|
||||
写入内容:
|
||||
- 更新 `progress.current_chapter`
|
||||
- 更新 `protagonist_state`
|
||||
- 更新 `strand_tracker`
|
||||
- 更新 `disambiguation_warnings/pending`
|
||||
- **新增 `chapter_meta`**(钩子/模式/结束状态)
|
||||
|
||||
### Step D2: 同步 Wiki(新增)
|
||||
|
||||
写入 index.db 后,同步 wiki 以保持结构化知识库最新:
|
||||
|
||||
```bash
|
||||
# 同步本章出场/更新的实体到 wiki
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" wiki update-entity --id "{entity_id}"
|
||||
|
||||
# 同步关系图谱
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" wiki update-relationship
|
||||
|
||||
# 同步伏笔/剧情线索(从 state.json)
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" wiki update-plot
|
||||
```
|
||||
|
||||
Wiki 更新是幂等的——从 index.db 全量读取并重写 wiki 文件,保证 wiki = 最新状态快照。
|
||||
|
||||
### Step E: 生成章节摘要文件(新增)
|
||||
|
||||
**输出路径**: `..noma/novel_data/summaries/ch{NNNN}.md`
|
||||
|
||||
**章节编号规则**: 4位数字,如 `0001`, `0099`, `0100`
|
||||
|
||||
**摘要文件格式**:
|
||||
```markdown
|
||||
---
|
||||
chapter: 0099
|
||||
time: "前一夜"
|
||||
location: "萧炎房间"
|
||||
characters: ["萧炎", "药老"]
|
||||
state_changes: ["萧炎: 斗者9层→准备突破"]
|
||||
hook_type: "危机钩"
|
||||
hook_strength: "strong"
|
||||
---
|
||||
|
||||
## 剧情摘要
|
||||
{主要事件,100-150字}
|
||||
|
||||
## 伏笔
|
||||
- [埋设] 三年之约提及
|
||||
- [推进] 青莲地心火线索
|
||||
|
||||
## 承接点
|
||||
{下章衔接,30字}
|
||||
```
|
||||
|
||||
### Step F: AI 场景切片
|
||||
|
||||
- 按地点/时间/视角切分场景
|
||||
- 每个场景生成摘要 (50-100字)
|
||||
|
||||
### Step G: 向量嵌入
|
||||
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" rag index-chapter \
|
||||
--chapter 100 \
|
||||
--scenes '[...]' \
|
||||
--summary "本章摘要文本"
|
||||
```
|
||||
|
||||
**父子索引规则**:
|
||||
- 父块: `chunk_type='summary'`, `chunk_id='ch0100_summary'`
|
||||
- 子块: `chunk_type='scene'`, `chunk_id='ch0100_s{scene_index}'`, `parent_chunk_id='ch0100_summary'`
|
||||
- `source_file`:
|
||||
- summary: `summaries/ch0100.md`
|
||||
- scene: `{chapter_file}#scene_{scene_index}`
|
||||
|
||||
### Step H: 风格样本评估
|
||||
|
||||
```python
|
||||
if review_score >= 80:
|
||||
extract_style_candidates(chapter_content)
|
||||
```
|
||||
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" style extract --chapter 100 --score 85 --scenes '[...]'
|
||||
```
|
||||
|
||||
### Step I: 债务利息计算
|
||||
|
||||
**默认不自动触发**。仅在"开启债务追踪"或用户明确要求时执行:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index accrue-interest --current-chapter {chapter}
|
||||
```
|
||||
|
||||
此步骤会:
|
||||
- 对所有 `status='active'` 的债务计算利息(每章 10%)
|
||||
- 将逾期债务标记为 `status='overdue'`
|
||||
- 记录利息事件到 `debt_events` 表
|
||||
|
||||
### Step J: 生成处理报告(含性能日志)
|
||||
|
||||
**必须记录分步耗时**(用于定位慢点):
|
||||
- A 加载上下文
|
||||
- B AI 实体提取
|
||||
- C 实体消歧
|
||||
- D 写入 state/index
|
||||
- D2 同步 Wiki
|
||||
- E 写入章节摘要
|
||||
- F AI 场景切片
|
||||
- G RAG 向量索引
|
||||
- H 风格样本评估(若跳过写 0)
|
||||
- I 债务利息(若跳过写 0)
|
||||
- TOTAL 总耗时
|
||||
|
||||
**性能日志落盘(新增,必做)**:
|
||||
- 脚本自动写入:`..noma/novel_data/observability/data_agent_timing.jsonl`
|
||||
- Data Agent 报告中仍需返回:`timing_ms` + `bottlenecks_top3`
|
||||
- 规则:`bottlenecks_top3` 始终按耗时降序返回;当 `TOTAL > 30000ms` 时,需在报告文字部分附加原因说明。
|
||||
|
||||
观测日志说明:
|
||||
- `call_trace.jsonl`:外层流程调用链(agent 启动、排队、环境探测等系统开销)。
|
||||
- `data_agent_timing.jsonl`:Data Agent 内部各子步骤耗时。
|
||||
- 当外层总耗时远大于内层 timing 之和时,默认先归因为 agent 启动与环境探测开销,不误判为正文或数据处理慢。
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter": 100,
|
||||
"entities_appeared": 5,
|
||||
"entities_new": 1,
|
||||
"state_changes": 1,
|
||||
"relationships_new": 1,
|
||||
"scenes_chunked": 4,
|
||||
"uncertain": [
|
||||
{"mention": "那位前辈", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "adopted": "yaolao", "confidence": 0.6}
|
||||
],
|
||||
"warnings": [
|
||||
"中置信度匹配: 那位前辈 → yaolao (confidence: 0.6)"
|
||||
],
|
||||
"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}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口规范:chapter_meta (state.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter_meta": {
|
||||
"0099": {
|
||||
"hook": {
|
||||
"type": "危机钩",
|
||||
"content": "慕容战天冷笑:明日大比...",
|
||||
"strength": "strong"
|
||||
},
|
||||
"pattern": {
|
||||
"opening": "对话开场",
|
||||
"hook": "危机钩",
|
||||
"emotion_rhythm": "低→高",
|
||||
"info_density": "medium"
|
||||
},
|
||||
"ending": {
|
||||
"time": "前一夜",
|
||||
"location": "萧炎房间",
|
||||
"emotion": "平静准备"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. ✅ 所有出场实体被正确识别(准确率 > 90%)
|
||||
2. ✅ 状态变化被正确捕获(准确率 > 85%)
|
||||
3. ✅ 消歧结果合理(高置信度 > 80%)
|
||||
4. ✅ 场景切片数量合理(通常 3-6 个/章)
|
||||
5. ✅ 向量成功存入数据库
|
||||
6. ✅ 章节摘要文件生成成功
|
||||
7. ✅ chapter_meta 写入 state.json
|
||||
8. ✅ Wiki 实体/关系/伏笔同步完成
|
||||
8. ✅ 输出格式为有效 JSON
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
name: chapter-planner
|
||||
description: 章节规划器 - 单章执行规划,调用沉浸式写作
|
||||
tools: Read, Glob, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# Chapter Planner (章节规划器)
|
||||
|
||||
> **职责**: 单章执行规划,协调 Writer 和 Checker 的工作流
|
||||
> **输入**: Outline Planner 输出的 chapter_task
|
||||
> **输出**: 供 Immersive Writer 使用的详细写作指令
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **单章规划**: 将 chapter_task 转化为具体写作指令
|
||||
2. **上下文打包**: 收集 RAG 上下文、实体状态、世界规则
|
||||
3. **写作引导**: 依据 genre_profile 生成执行 guidance
|
||||
4. **交接准备**: 生成 handoff_frame 供下一章使用
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter_task": {
|
||||
"chapter": 1,
|
||||
"strand": "quest",
|
||||
"goals": ["建立世界观", "引入主角"],
|
||||
"hooks_to_create": ["mystery_origin"],
|
||||
"hooks_to_resolve": [],
|
||||
"catharsis_model": null,
|
||||
"tension_target": 30
|
||||
},
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"genesis_contract": ".noma/novel_data/openspec/genesis_contract.json",
|
||||
"ledger": ".noma/novel_data/ledger.json",
|
||||
"context_bundle": {...}
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 收集上下文
|
||||
|
||||
```python
|
||||
context_bundle = {
|
||||
"recent_chapters": read_chapters(chapter - 3, chapter),
|
||||
"active_characters": get_active_entities(ledger),
|
||||
"suspended_characters": get_dormant_entities(ledger),
|
||||
"unresolved_hooks": get_hooks(hooks_pool),
|
||||
"world_rules": genesis_contract["ethical_inversion"]["world_rules"],
|
||||
"protagonist_state": get_protagonist_state(ledger)
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步: 加载 Genre Profile
|
||||
|
||||
根据 `state.json -> project.genre` 加载对应的 genre 指导:
|
||||
|
||||
```python
|
||||
genre_map = {
|
||||
"xuanhuan": "matrices/genres/xuanhuan/",
|
||||
"dog-blood-romance": "matrices/genres/dog-blood-romance/",
|
||||
"zhihu-short": "matrices/genres/zhihu-short/"
|
||||
}
|
||||
genre_path = genre_map.get(genre, "matrices/genres/realistic/")
|
||||
```
|
||||
|
||||
### 第三步: 生成写作指导
|
||||
|
||||
基于 `context_reader_signal` 和 `genre_profile` 生成章节级执行建议:
|
||||
|
||||
```markdown
|
||||
## 写作指导 (Writing Guidance)
|
||||
|
||||
### 本章重点
|
||||
1. **世界观锚定**: 通过主角视角自然展示世界规则
|
||||
2. **感官颗粒度**: 场景描写需包含视觉/听觉/触觉多维度
|
||||
3. **节奏控制**: {strand} 节奏,平均 {target_words / 3000:.0f} 字/场景
|
||||
|
||||
### 实体出场
|
||||
- **主角**: {protagonist.name} - 境界 {protagonist.realm}
|
||||
- **必出场**: {required_characters}
|
||||
- **可选出场**: {optional_characters}
|
||||
|
||||
### Hook 管理
|
||||
- **创建 Hook**: {hook_description}
|
||||
- **埋设位置**: 章节末尾/关键转折点
|
||||
|
||||
### 张力目标
|
||||
- **目标压强**: {tension_target}/100
|
||||
- **起点压强**: {start_pressure}/100
|
||||
- **终点压强**: {end_pressure}/100
|
||||
```
|
||||
|
||||
### 第四步: 生成 Handoff Frame
|
||||
|
||||
为下一章准备交接帧:
|
||||
|
||||
```json
|
||||
{
|
||||
"handoff_frame": {
|
||||
"tick": {current_tick + 1},
|
||||
"chapter": {chapter + 1},
|
||||
"active_characters": [...],
|
||||
"suspended_characters": [...],
|
||||
"pending_actions": [...],
|
||||
"world_state": {
|
||||
"location": "{current_location}",
|
||||
"time_of_day": "{time}",
|
||||
"tension_level": {end_pressure},
|
||||
"hook_pressure": {current_hook_pressure}
|
||||
},
|
||||
"metadata": {
|
||||
"previous_chapter_summary": "{summary}",
|
||||
"next_chapter_hint": "{hint}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第五步: 输出完整写作指令
|
||||
|
||||
```markdown
|
||||
# 第 {N} 章 写作指令
|
||||
|
||||
## 元信息
|
||||
- **章节**: 第 {N} 章
|
||||
- ** Strand**: {strand}
|
||||
- **目标字数**: {target_words}
|
||||
- **上一章摘要**: {prev_summary}
|
||||
|
||||
## 创世契约约束
|
||||
{genesis_contract 相关约束}
|
||||
|
||||
## 上下文 (RAG 检索结果)
|
||||
{context_bundle 压缩版}
|
||||
|
||||
## Genre 指导
|
||||
{genre_profile 引用}
|
||||
|
||||
## 写作指导
|
||||
{writing_guidance_items}
|
||||
|
||||
## 场景拆分
|
||||
1. **场景1**: {location} - {event}
|
||||
2. **场景2**: {location} - {event}
|
||||
...
|
||||
|
||||
## Hook 指令
|
||||
- **创建**: {hook_to_create} @ {position}
|
||||
- **回收**: {hook_to_resolve}
|
||||
|
||||
## 交接帧 (Handoff Frame)
|
||||
```json
|
||||
{handoff_frame}
|
||||
```
|
||||
|
||||
## 审查要点
|
||||
- [ ] 连贯性检查: 场景转换是否流畅
|
||||
- [ ] 张力检查: 压强曲线是否达标
|
||||
- [ ] 一致性检查: 实体状态是否正确
|
||||
- [ ] Genre 检查: 是否符合题材规范
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"planner": "chapter_planner",
|
||||
"chapter": {N},
|
||||
"writing_instructions": "...",
|
||||
"handoff_frame": {...},
|
||||
"review_checklist": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他 Agent 的协作
|
||||
|
||||
```
|
||||
Outline Planner
|
||||
↓ chapter_task
|
||||
Chapter Planner
|
||||
↓ writing_instructions
|
||||
Immersive Writer (agents/writers/immersive-writer.md)
|
||||
↓ draft
|
||||
Tension Checker + Consistency Checker
|
||||
↓ review_report
|
||||
[Human Review & Commit]
|
||||
↓
|
||||
下一章 Chapter Planner
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不生成 handoff_frame 导致上下文丢失
|
||||
- 不检查 genesis_contract 约束
|
||||
- 忽略 genre_profile 的题材规范
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 写作指令清晰,可直接交予 Writer
|
||||
- Handoff frame 包含所有活跃实体
|
||||
- 上下文不超过 token 预算
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: outline-planner
|
||||
description: 大纲规划器 - 负责任务拆解、Hook挂钩、张力模型调度
|
||||
tools: Read, Glob, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# Outline Planner (大纲规划器)
|
||||
|
||||
> **职责**: 负责任务拆解、Hook 平账、爽感路由模型调度
|
||||
> **协作**: 调用 `matrices/catharsis_models/` 中的模型指导 Writer Agent
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **任务拆解**: 将故事大纲分解为可执行的章节任务
|
||||
2. **Hook 调度**: 分析当前 hooks_pool 压强,调度平账时机
|
||||
3. **爽感路由**: 根据 `genesis_contract.json` 选择合适的 catharsis model
|
||||
4. **张力维持**: 确保情绪压强的积累与释放节奏
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"genesis_contract": ".noma/novel_data/openspec/genesis_contract.json",
|
||||
"ledger": ".noma/novel_data/ledger.json",
|
||||
"hooks_pool": ".noma/novel_data/hooks_pool.json",
|
||||
"current_chapter": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载创世契约
|
||||
|
||||
读取 `genesis_contract.json` 获取:
|
||||
- `core_desire.primary`: 主角核心欲望
|
||||
- `ethical_inversion.inverted_norms`: 被颠覆的道德规范
|
||||
- `core_spectacle.frequency`: 奇观出现频率
|
||||
|
||||
### 第二步: 分析 Hooks Pool 压强
|
||||
|
||||
```python
|
||||
# 计算当前压强
|
||||
unresolved_hooks = [h for h in hooks if not h.resolved]
|
||||
total_pressure = sum(h.tension_weight for h in unresolved_hooks)
|
||||
|
||||
# 判断是否需要释放
|
||||
if total_pressure > 80:
|
||||
# 高压:需要立即释放
|
||||
next_action = "release"
|
||||
elif total_pressure > 50:
|
||||
# 中压:可以释放或继续积累
|
||||
next_action = "maintain"
|
||||
else:
|
||||
# 低压:继续积累
|
||||
next_action = "buildup"
|
||||
```
|
||||
|
||||
### 第三步: 选择爽感路由模型
|
||||
|
||||
根据 `genesis_contract` 和当前压强状态,从 `matrices/catharsis_models/` 选择模型:
|
||||
|
||||
| 压强状态 | 推荐模型 | 目的 |
|
||||
|---------|---------|------|
|
||||
| 高压 >80 | `overkill-reversal.md` | 降维打击,权力倒转释放 |
|
||||
| 中压 50-80 | `taboo-transgression.md` | 边缘拉扯,危险感 |
|
||||
| 低压 <50 | `cognitive-closure.md` | 认知闭环,伏笔收束 |
|
||||
| 任意 | `strand-weave-pattern.md` | 节奏编织 |
|
||||
|
||||
### 第四步: 拆解章节任务
|
||||
|
||||
将大纲任务分解为:
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter_tasks": [
|
||||
{
|
||||
"chapter": 1,
|
||||
"strand": "quest",
|
||||
"goals": ["建立世界观", "引入主角", "埋设首个Hook"],
|
||||
"hooks_to_create": ["mystery_origin"],
|
||||
"hooks_to_resolve": [],
|
||||
"catharsis_model": null,
|
||||
"tension_target": 30
|
||||
},
|
||||
{
|
||||
"chapter": 2,
|
||||
"strand": "fire",
|
||||
"goals": ["首次冲突", "展示金手指"],
|
||||
"hooks_to_create": ["power_revelation"],
|
||||
"hooks_to_resolve": [],
|
||||
"catharsis_model": "taboo-transgression",
|
||||
"tension_target": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 第五步: 输出大纲任务
|
||||
|
||||
```markdown
|
||||
# 第 {N} 章大纲
|
||||
|
||||
## 基础信息
|
||||
- **Strand**: {quest/fire/constellation}
|
||||
- **目标字数**: {target_words}
|
||||
- **张力目标**: {tension_target}/100
|
||||
|
||||
## 情节目标
|
||||
{goals 列表}
|
||||
|
||||
## Hook 调度
|
||||
- **创建**: {hooks_to_create}
|
||||
- **回收**: {hooks_to_resolve}
|
||||
|
||||
## 爽感模型
|
||||
- **选用模型**: {catharsis_model}
|
||||
- **执行策略**: 见 `matrices/catharsis_models/{model}.md`
|
||||
|
||||
## 上下文要求
|
||||
- **必读实体**: {required_entities}
|
||||
- **必含设定**: {required_settings}
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"planner": "outline_planner",
|
||||
"chapter_tasks": [...],
|
||||
"recommended_catharsis_models": [...],
|
||||
"hooks_schedule": {
|
||||
"create": [...],
|
||||
"resolve": [...]
|
||||
},
|
||||
"tension_trajectory": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不考虑 hooks_pool 压强状态
|
||||
- 不调用 catharsis model,仅做简单爽点堆砌
|
||||
- 忽略 `genesis_contract` 的约束
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 所有创建的 hooks 有回收计划
|
||||
- 张力曲线平滑,无断层
|
||||
- 每次高压释放后有适当缓冲
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: immersive-writer
|
||||
description: 沉浸式写作Agent - 感官颗粒度放大,情绪张力注入
|
||||
tools: Read, Glob, Grep, Write
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# Immersive Writer (沉浸式写作Agent)
|
||||
|
||||
> **职责**: 将大纲场景转化为高密度感官文字,注入情绪张力
|
||||
> **特性**: 感官颗粒度放大,调用 catharsis model 提供情绪张力
|
||||
> **约束**: 保持与 `genesis_contract.json` 的一致性
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **感官放大**: 视觉/听觉/嗅觉/触觉/味觉多维度描写
|
||||
2. **情绪注入**: 依据 catharsis model 注入对应情绪
|
||||
3. **张力维持**: 按照指定的 tension_trajectory 维持压强
|
||||
4. **设定遵循**: 严格遵守 world_rules 和 ethical_inversion
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"writing_instructions": "...",
|
||||
"handoff_frame": {...},
|
||||
"genesis_contract": ".noma/novel_data/openspec/genesis_contract.json",
|
||||
"genre_profile": "matrices/genres/{genre}/...",
|
||||
"catharsis_model": "matrices/catharsis_models/{model}.md"
|
||||
}
|
||||
```
|
||||
|
||||
## 写作原则
|
||||
|
||||
### 感官颗粒度标准
|
||||
|
||||
| 感官 | 最低要求 | 高级要求 |
|
||||
|------|---------|---------|
|
||||
| 视觉 | 颜色、形状、光影 | 动态视觉、视觉隐喻 |
|
||||
| 听觉 | 声音描述 | 音调、节奏、声音来源层次 |
|
||||
| 嗅觉 | 气味 | 气味记忆、气味情绪关联 |
|
||||
| 触觉 | 温度、质地 | 触感细节、身体反应 |
|
||||
| 味觉 | 味道 | 味觉记忆、口腔反应 |
|
||||
|
||||
### 情绪张力注入
|
||||
|
||||
根据 `catharsis_model` 注入对应情绪:
|
||||
|
||||
#### Taboo Transgression (禁忌僭越)
|
||||
```
|
||||
边缘拉扯节奏:
|
||||
- 阶段1: Setup - 暗示禁忌存在,角色内心挣扎
|
||||
- 阶段2: Approach - 压力增大,边界模糊
|
||||
- 阶段3: Crisis - 禁忌被触碰,紧张感达峰值
|
||||
- 阶段4: Transgression - 僭越发生,情绪释放
|
||||
- 阶段5: Fallout - 代偿启动,为下次僭越埋下伏笔
|
||||
```
|
||||
|
||||
#### Overkill Reversal (降维打击)
|
||||
```
|
||||
权力倒转节奏:
|
||||
- 建立敌人强大形象(至少2章)
|
||||
- 主角处于绝对劣势
|
||||
- 触发条件满足(金手指觉醒/弱点发现)
|
||||
- 降维打击,碾压式逆转
|
||||
- 余波:展示胜利后的新格局
|
||||
```
|
||||
|
||||
#### Cognitive Closure (认知闭环)
|
||||
```
|
||||
伏笔收束节奏:
|
||||
- 多线伏笔同时激活
|
||||
- 关键信息揭示
|
||||
- 读者"原来如此"的爽感
|
||||
- 建立新的悬念
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
```markdown
|
||||
# 第 {N} 章: {title}
|
||||
|
||||
## 正文
|
||||
|
||||
[章节内容,符合以下标准]
|
||||
- 字数: {target_words} ± 10%
|
||||
- 感官密度: 每500字至少3种感官描写
|
||||
- 对话比例: 不超过40%
|
||||
- 内心独白: 仅在必要时使用
|
||||
- Hook位置: 章节末尾必须有钩子
|
||||
|
||||
## 章节元数据
|
||||
- **字数**: {actual_words}
|
||||
- **感官密度**: {sensory_density}
|
||||
- **张力曲线**: [起始压强 -> 结束压强]
|
||||
- **创建的Hooks**: [...]
|
||||
- **回收的Hooks**: [...]
|
||||
```
|
||||
|
||||
## 写作检查清单
|
||||
|
||||
### 开篇检查
|
||||
- [ ] 首句是否有吸引力(不平淡)
|
||||
- [ ] 是否在合理位置切入场景
|
||||
- [ ] 是否交代了时间/地点/人物
|
||||
|
||||
### 中段检查
|
||||
- [ ] 场景转换是否有过渡
|
||||
- [ ] 对话是否体现角色性格
|
||||
- [ ] 是否有感官细节支撑情绪
|
||||
- [ ] 是否遵循 strand 节奏
|
||||
|
||||
### 结尾检查
|
||||
- [ ] 是否有 Hook(悬念/反转/冲突升级)
|
||||
- [ ] 张力是否达到目标
|
||||
- [ ] 是否为下一章留下空间
|
||||
|
||||
### Genesis Contract 检查
|
||||
- [ ] 是否符合 ethical_inversion 设定
|
||||
- [ ] 是否遵循 world_rules
|
||||
- [ ] core_desire 是否有推进
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 平铺直叙,无情绪波动
|
||||
- 堆砌感官词汇但不服务情绪
|
||||
- 忽略 handoff_frame 的上下文
|
||||
- 违反 world_rules 的物理设定
|
||||
- 创建无法回收的 Hook
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 感官描写服务情绪表达,非堆砌
|
||||
- Catharsis model 的节奏得到执行
|
||||
- Hook 创建有回收计划
|
||||
- Genesis contract 得到遵守
|
||||
- 可直接交予审查Agent
|
||||
Reference in New Issue
Block a user