feat: initial commit

This commit is contained in:
2026-06-23 20:29:02 +08:00
commit ea8f6066c4
217 changed files with 60754 additions and 0 deletions
+446
View File
@@ -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()
+22
View File
@@ -0,0 +1,22 @@
"""
Agents Checkers
审查器模块,包括 tension_checker 等。
"""
from .tension_checker import (
TensionChecker,
TensionChecker,
CatharsisModel,
TensionLevel,
TensionReading,
TensionIssue,
)
__all__ = [
"TensionChecker",
"CatharsisModel",
"TensionLevel",
"TensionReading",
"TensionIssue",
]
+228
View File
@@ -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 个高优先级时间线问题(**倒计时错误、时间回跳、重大事件无时间推进**)
- 所有新实体与现有世界观一致
- 地点和时间线过渡合乎逻辑
- 报告为润色步骤提供具体修复建议
+455
View File
@@ -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
]
}
+250
View File
@@ -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 个重大逻辑漏洞
- 大纲偏差已正确标记
- 报告指出需修复的具体章节
+146
View File
@@ -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
+217
View File
@@ -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": "爽点密度达标,类型分布健康,执行质量稳定。"
}
```
+181
View File
@@ -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)
}
+213
View File
@@ -0,0 +1,213 @@
---
name: ooc-checker
description: 人物OOC检查,输出结构化报告供润色步骤参考
tools: Read, Grep
model: inherit
---
# ooc-checker (人物OOC检查器)
> **职责**: 角色完整性守卫者,防止 OOCOut-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 和合理成长
+146
View File
@@ -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
+215
View File
@@ -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 | Fire20%| 高(战斗密集)|
| {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%
- 所有情节线在各自阈值内至少出现一次
- 报告提供可执行的下一章建议
- 趋势分析显示分布均衡(若有足够历史数据)
+173
View File
@@ -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"
))
+317
View File
@@ -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章以上同型
- [ ] 输出清晰的"下章动机"
+207
View File
@@ -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
}
+416
View File
@@ -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)
+269
View File
@@ -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.**时间逻辑红线通过**(无回跳、无倒计时跳跃、大跨度有过渡要求)
+318
View File
@@ -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
+204
View File
@@ -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 预算
+151
View File
@@ -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 有回收计划
- 张力曲线平滑,无断层
- 每次高压释放后有适当缓冲
+137
View File
@@ -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