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
+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)