feat: initial commit
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
.DS_Store
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Distribution
|
||||
noma.zip
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Noma runtime data (don't track)
|
||||
.noma/
|
||||
workspaces/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Don't ignore .claude settings (we need to track plugin config)
|
||||
# But ignore local settings
|
||||
.claude/settings.local.json
|
||||
@@ -0,0 +1,345 @@
|
||||
# NovelMaster (Noma) 5.5
|
||||
|
||||
> **OpenNovel Workspace** — 融合"欲望心理学引擎"与"硬状态基建"的人机协同长篇网文创作 IDE
|
||||
|
||||
[](LICENSE)
|
||||
[](https://www.python.org/)
|
||||
[](https://claude.ai/code)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [项目简介](#项目简介)
|
||||
- [核心特性](#核心特性)
|
||||
- [架构设计](#架构设计)
|
||||
- [快速开始](#快速开始)
|
||||
- [技能模块](#技能模块)
|
||||
- [CLI 命令](#cli-命令)
|
||||
- [RAG 检索系统](#rag-检索系统)
|
||||
- [爽感心理学引擎](#爽感心理学引擎)
|
||||
- [项目结构](#项目结构)
|
||||
- [配置说明](#配置说明)
|
||||
- [题材支持](#题材支持)
|
||||
- [文档索引](#文档索引)
|
||||
- [许可证](#许可证)
|
||||
|
||||
---
|
||||
|
||||
## 项目简介
|
||||
|
||||
**NovelMaster (Noma)** 是基于 Claude Code 的长篇中文网文创作系统,旨在解决 AI 写作中长周期连载(数百至数千章)时面临的**遗忘**和**幻觉**两大核心问题。
|
||||
|
||||
系统核心采用**双 Agent 架构**——Context Agent 在写作前构建创作任务简报,Data Agent 在写作后提取实体和状态变化,配合 **SQLite-first 数据层**、向量检索、**欲望心理学引擎**驱动的爽感路由,以及**六维自动化审查**系统,为长篇网文创作提供完整的人机协同 IDE 体验。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 反幻觉基建
|
||||
|
||||
- **双 Agent 架构** — Context Agent(写作前简报)+ Data Agent(写作后提取),通过文件锁实现原子状态写入
|
||||
- **SQLite-first 持久化** — 15+ 张数据表,追踪章节、场景、实体、别名、状态变更、人物关系、阅读力债务和审查指标
|
||||
- **三层 RAG 检索** — 小说私有 + 工作区共享 + 插件内置,向量(sqlite-vec)+ BM25 关键词搜索 + Jina 重排序,通过 RRF 融合
|
||||
- **线索编织节奏系统** — 管理三条故事线索(主线 60% / 感情 20% / 世界观 20%),可配置红线防止线索失衡
|
||||
- **Catchup Agent** — 自动检测 retcon 事件的脏标记,在作者修改设定时自动更新角色状态
|
||||
|
||||
### 创作引擎
|
||||
|
||||
- **欲望沙盒(Genesis Contract)** — 定义故事核心欲望(权力、爱情、复仇、知识、生存、认可)、伦理边界和奇观体系
|
||||
- **六维审查** — 爽点密度、一致性、节奏、OOC 检测、连续性、读者留存检查
|
||||
- **心理学武器矩阵** — 三大爽感模型(禁忌僭越、降维打击、认知闭环),配合题材专属参考库
|
||||
- **风格保持** — 写作检查清单、风格样本评估、阅读力信号保持语调一致
|
||||
|
||||
### 开发者体验
|
||||
|
||||
- **8 个 Claude Code 技能** — init、plan、write、review、resume、query、learn、dashboard
|
||||
- **只读 Web 面板** — 实时项目状态、人物关系图、章节浏览,访问地址 `http://127.0.0.1:8765`
|
||||
- **完整 CLI** — 15+ 命令覆盖项目管理、索引、RAG 检索、实体提取和备份
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
Claude Code
|
||||
├── Skills (init / plan / write / review / query / resume / learn / dashboard)
|
||||
├── Agents: Context / Data / 多维审查器 / Catchup
|
||||
├── Data Layer: state.json / index.db / vectors.db / ledger.json
|
||||
└── OpenNovel Workspace (ONW)
|
||||
├── openspec/ → Genesis Contract(欲望沙盒 + 伦理)
|
||||
├── core_engine/ → 状态机 + 记忆 RAG + LOD/Tick/Retcon
|
||||
├── matrices/ → 多维爽感路由
|
||||
├── interfaces/ → Web 面板(Flask/FastAPI + React)
|
||||
└── agents/ → 审查与执行矩阵
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置条件
|
||||
|
||||
- Python 3.14+
|
||||
- Claude Code(含插件支持)
|
||||
- Ollama(本地 Embedding)或 OpenAI 兼容 API(云端 Embedding)
|
||||
- Jina API Key(重排序,可选)
|
||||
|
||||
### 1. 安装插件
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add dadizk/novelmaster --scope user
|
||||
claude plugin install novelmaster@novelmaster-marketplace --scope user
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
cd NovelMaster/noma
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 配置 Embedding
|
||||
|
||||
创建 `.env` 文件(项目级 `.noma/config.env` 或全局 `~/.claude/novelmaster/.env`):
|
||||
|
||||
```env
|
||||
# 云端方案(ModelScope Qwen3)
|
||||
EMBED_BASE_URL=https://api-inference.modelscope.cn/v1
|
||||
EMBED_MODEL=Qwen/Qwen3-Embedding-8B
|
||||
EMBED_API_KEY=your_key
|
||||
|
||||
# 本地方案(Ollama)
|
||||
# EMBED_BASE_URL=http://127.0.0.1:11434/v1
|
||||
# EMBED_MODEL=bge-m3:latest
|
||||
# EMBED_API_KEY=ollama
|
||||
|
||||
# 重排序(可选)
|
||||
RERANK_BASE_URL=https://api.jina.ai/v1
|
||||
RERANK_MODEL=jina-reranker-v3
|
||||
RERANK_API_KEY=your_jina_key
|
||||
```
|
||||
|
||||
### 4. 初始化小说项目
|
||||
|
||||
```bash
|
||||
/noma-init --title "我的小说" --genre "xuanhuan" --target-chapters 600
|
||||
```
|
||||
|
||||
### 5. 开始写作
|
||||
|
||||
```bash
|
||||
/noma-plan 1 # 规划卷纲和章纲
|
||||
/noma-write 1 # 写第 1 章
|
||||
/noma-review 1 # 审查第 1 章
|
||||
```
|
||||
|
||||
## 技能模块
|
||||
|
||||
| 技能 | 命令 | 说明 |
|
||||
|------|------|------|
|
||||
| **初始化** | `/noma-init` | 初始化新小说项目,设定题材、目标章节数和核心欲望 |
|
||||
| **规划** | `/noma-plan` | 从总大纲生成卷纲和章纲,继承创作约束 |
|
||||
| **写作** | `/noma-write` | 撰写章节(2000-2500 字),含上下文构建、草稿、审查、润色和数据提取 |
|
||||
| **审查** | `/noma-review` | 六维章节质量审查,含多个审查 Agent |
|
||||
| **恢复** | `/noma-resume` | 恢复中断的任务,精确追踪工作流状态 |
|
||||
| **查询** | `/noma-query` | 查询角色、能力、势力、道具、伏笔和阅读力状态 |
|
||||
| **学习** | `/noma-learn` | 将成功写作模式提取到 Wiki 模式库 |
|
||||
| **面板** | `/noma-dashboard` | 启动只读 Web 面板,可视化项目状态 |
|
||||
|
||||
## CLI 命令
|
||||
|
||||
所有 CLI 命令通过 `noma/scripts/noma.py` 统一入口:
|
||||
|
||||
```bash
|
||||
# 项目管理
|
||||
python noma.py where # 定位项目根目录
|
||||
python noma.py preflight # 全组件健康检查
|
||||
python noma.py use <project> # 切换活跃项目
|
||||
python noma.py status # 查看项目状态概览
|
||||
|
||||
# 数据操作
|
||||
python noma.py index stats # 索引统计(实体、章节、场景)
|
||||
python noma.py state # 查看当前状态快照
|
||||
python noma.py entity <name> # 查询实体详情
|
||||
python noma.py update-state # 强制状态对齐
|
||||
python noma.py backup # 创建项目备份
|
||||
python noma.py archive <ch> # 归档已完成章节
|
||||
|
||||
# RAG 检索
|
||||
python noma.py rag search --query "爽点设计" # 向量搜索
|
||||
python noma.py rag search --query "打脸" --type keyword # BM25 关键词搜索
|
||||
|
||||
# 上下文与工作流
|
||||
python noma.py context # 查看当前上下文包
|
||||
python noma.py workflow detect # 检测工作流状态问题
|
||||
python noma.py migrate # 跨版本数据迁移
|
||||
python noma.py wiki # 管理 Wiki 模式库
|
||||
python noma.py extract-context # 手动触发上下文提取
|
||||
```
|
||||
|
||||
## RAG 检索系统
|
||||
|
||||
### 三层架构
|
||||
|
||||
| 层级 | 路径 | 范围 |
|
||||
|------|------|------|
|
||||
| **小说私有** | `.noma/rag/project/` | 仅当前小说可用 |
|
||||
| **工作区共享** | `.noma/rag/shared/` | 工作区内所有小说共享 |
|
||||
| **插件内置** | `noma/matrices/shared/` | 系统级模板 |
|
||||
|
||||
### 检索管线
|
||||
|
||||
```
|
||||
用户查询
|
||||
↓
|
||||
向量搜索(sqlite-vec, top-k=30) ──┐
|
||||
BM25 关键词搜索(top-k=20) ───────┤ → RRF 融合(k=60)→ 重排序(Jina, top-n=10)→ 上下文包
|
||||
↓ │
|
||||
系统模板(直接匹配) ──────────────┘
|
||||
```
|
||||
|
||||
### RAG 管理命令
|
||||
|
||||
```bash
|
||||
# 关键词搜索(推荐用于精确查询)
|
||||
python noma.py rag search "爽点" --layers system
|
||||
python noma.py rag search "剑修" --layers project,system
|
||||
|
||||
# 列出系统层可用模式
|
||||
python noma.py rag list --layer system
|
||||
|
||||
# 同步模式到系统层
|
||||
python noma.py rag sync --pattern-id <id>
|
||||
python noma.py rag sync --all
|
||||
```
|
||||
|
||||
## 爽感心理学引擎
|
||||
|
||||
三大心理张力模型,用于设计满足感的剧情爽点:
|
||||
|
||||
| 模型 | ID | 说明 |
|
||||
|------|----|------|
|
||||
| **禁忌僭越** | `taboo-transgression` | 边缘拉扯、危险感、高压情绪释放 |
|
||||
| **降维打击** | `overkill-reversal` | 碾压、反转、绝地反击、战力差逆转 |
|
||||
| **认知闭环** | `cognitive-closure` | 多线伏笔收束、解谜快感 |
|
||||
|
||||
每个模型都具备题材感知能力,内置玄幻、狗血甜宠、古言宫斗、现实题材、规则怪谈、知乎短文等题材的专属参考库。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
NovelMaster/
|
||||
├── noma/ # 插件源码
|
||||
│ ├── .claude/ # Claude Code 插件配置
|
||||
│ │ ├── plugin.json # 插件元信息(v5.5.4, GPL-3.0)
|
||||
│ │ └── settings.json # 工具权限
|
||||
│ ├── SKILL.md # 技能入口
|
||||
│ ├── README.md # 本文件
|
||||
│ ├── requirements.txt # Python 依赖
|
||||
│ │
|
||||
│ ├── openspec/ # 数据标准
|
||||
│ │ ├── genesis_contract.json # 核心欲望 + 伦理 + 奇观
|
||||
│ │ └── project.md
|
||||
│ │
|
||||
│ ├── core_engine/ # 核心引擎模块
|
||||
│ │ ├── state_manager/ # 状态机(原子写入)
|
||||
│ │ ├── memory_rag/ # RAG 适配器 + 向量索引
|
||||
│ │ └── configurators/ # IDE 适配器
|
||||
│ │
|
||||
│ ├── agents/ # Agent 矩阵
|
||||
│ │ ├── context-agent.md # 写作前上下文构建
|
||||
│ │ ├── data-agent.md # 写作后实体提取
|
||||
│ │ ├── planners/ # 大纲与章纲规划器
|
||||
│ │ ├── writers/ # 风格保持文本生成器
|
||||
│ │ └── checkers/ # 8 个审查 Agent
|
||||
│ │
|
||||
│ ├── matrices/ # 心理学武器库
|
||||
│ │ ├── catharsis_models/ # 3 大爽感模型
|
||||
│ │ └── genres/ # 题材专属参考
|
||||
│ │
|
||||
│ ├── interfaces/ # 人机界面
|
||||
│ │ └── web_dashboard/ # Flask/FastAPI + React 面板
|
||||
│ │
|
||||
│ ├── scripts/ # 数据模块 + CLI
|
||||
│ │ ├── data_modules/ # 配置、状态、索引、RAG、上下文
|
||||
│ │ └── noma.py # CLI 入口
|
||||
│ │
|
||||
│ ├── skills/ # Claude Code 技能
|
||||
│ │ ├── noma-init/ # 项目初始化
|
||||
│ │ ├── noma-plan/ # 大纲规划
|
||||
│ │ ├── noma-write/ # 章节写作
|
||||
│ │ ├── noma-review/ # 质量审查
|
||||
│ │ ├── noma-resume/ # 任务恢复
|
||||
│ │ ├── noma-query/ # 数据查询
|
||||
│ │ ├── noma-learn/ # 模式学习
|
||||
│ │ └── noma-dashboard/ # Web 面板启动
|
||||
│ │
|
||||
│ ├── references/ # 共享引用
|
||||
│ └── docs/ # 架构文档
|
||||
│
|
||||
├── workspaces/ # 小说项目
|
||||
│ └── 九天神帝/ # 示例小说
|
||||
│ ├── .noma/ # 项目数据层
|
||||
│ ├── 正文/ # 章节(markdown)
|
||||
│ ├── 大纲/ # 大纲
|
||||
│ ├── 设定集/ # 角色、地点、势力
|
||||
│ ├── 审查报告/ # 审查报告
|
||||
│ └── 输出/ # 导出内容
|
||||
│
|
||||
├── USAGE.md # 使用指南(中文)
|
||||
└── LICENSE # GPL-3.0
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
所有配置从 `.env` 文件加载,优先级顺序:
|
||||
|
||||
1. **项目级**:`<project>/.noma/config.env`
|
||||
2. **全局级**:`~/.claude/novelmaster/.env`
|
||||
3. **默认值**:硬编码回退(ModelScope Qwen3-Embedding-8B、Jina Reranker v3)
|
||||
|
||||
### 关键配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `EMBED_BASE_URL` | ModelScope API | Embedding 服务地址 |
|
||||
| `EMBED_MODEL` | Qwen3-Embedding-8B | Embedding 模型名 |
|
||||
| `RERANK_BASE_URL` | Jina API | 重排序服务地址 |
|
||||
| `RERANK_MODEL` | jina-reranker-v3 | 重排序模型名 |
|
||||
| `VECTOR_TOP_K` | 30 | 向量搜索 top-k |
|
||||
| `BM25_TOP_K` | 20 | BM25 搜索 top-k |
|
||||
| `RERANK_TOP_N` | 10 | 重排序后 top-n |
|
||||
| `RRF_K` | 60 | RRF 融合常数 |
|
||||
| `QUEST_RATIO` | 0.6 | 主线比例 |
|
||||
| `FIRE_RATIO` | 0.2 | 感情线比例 |
|
||||
| `CONSTELLATION_RATIO` | 0.2 | 世界观线比例 |
|
||||
|
||||
完整配置参考见 `docs/rag-and-config.md`。
|
||||
|
||||
## 题材支持
|
||||
|
||||
| 题材 | 目录 | 核心能力 |
|
||||
|------|------|----------|
|
||||
| **玄幻/修仙** | `xuanhuan/` | 修炼等级、能力体系、宗门政治 |
|
||||
| **狗血甜宠** | `dog-blood-romance/` | 角色原型、情感张力、三角关系 |
|
||||
| **古言/宫斗** | `period-drama/` | 宫廷权谋、等级制度、政治博弈 |
|
||||
| **现实题材** | `realistic/` | 社会议题、职场动态、接地气的冲突 |
|
||||
| **规则怪谈** | `rules-mystery/` | 线索设计、诡计设计、逻辑推理 |
|
||||
| **知乎短文** | `zhihu-short/` | 钩子技巧、情节压缩、反转结尾 |
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 路径 |
|
||||
|------|------|
|
||||
| 架构说明 | `noma/docs/architecture.md` |
|
||||
| 命令详解 | `noma/docs/commands.md` |
|
||||
| RAG 与配置 | `noma/docs/rag-and-config.md` |
|
||||
| 题材模板 | `noma/docs/genres.md` |
|
||||
| OpenSpec 数据结构 | `noma/docs/openspec.md` |
|
||||
| CoreEngine | `noma/docs/core_engine.md` |
|
||||
| Matrices | `noma/docs/matrices.md` |
|
||||
| Interfaces | `noma/docs/interfaces.md` |
|
||||
| Agents | `noma/docs/agents.md` |
|
||||
| 运维文档 | `noma/docs/operations.md` |
|
||||
| 教程 | `noma/docs/Tutorial.md` |
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 **GNU General Public License v3.0** 许可 — [LICENSE](LICENSE)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,153 @@
|
||||
# NovelMaster 使用指南
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
E:/Project/NovelMaster/ # 工程根目录
|
||||
├── .noma/
|
||||
│ ├── config.env # 全局配置(Embedding API、Rerank API)
|
||||
│ ├── state.json # 项目状态
|
||||
│ ├── projects.json # 项目索引
|
||||
│ ├── index.db # 实体索引数据库
|
||||
│ ├── vectors.db # 向量数据库
|
||||
│ └── rag/shared/ # 系统共享模板(爽点模型、题材库)
|
||||
└── noma/ # 插件源码(开发版)
|
||||
|
||||
~/.claude/plugins/cache/NovelMaster/ # Claude 插件目录
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置
|
||||
|
||||
编辑 `E:/Project/NovelMaster/.noma/config.env`:
|
||||
|
||||
```env
|
||||
# Embedding API (Ollama)
|
||||
EMBED_BASE_URL=http://127.0.0.1:11434/v1
|
||||
EMBED_MODEL=bge-m3:latest
|
||||
EMBED_API_KEY=your_embed_api_key_here
|
||||
|
||||
# Rerank API (Jina)
|
||||
RERANK_BASE_URL=https://api.jina.ai/v1
|
||||
RERANK_MODEL=jina-reranker-v3
|
||||
RERANK_API_KEY=your_rerank_api_key_here
|
||||
```
|
||||
|
||||
### 2. 初始化项目
|
||||
|
||||
```bash
|
||||
cd E:/Project/NovelMaster
|
||||
python noma/scripts/init_project.py ./my-novel "我的小说" "玄幻" --target-chapters 600
|
||||
```
|
||||
|
||||
### 3. 基本命令
|
||||
|
||||
```bash
|
||||
# 定位项目根目录
|
||||
python noma/scripts/noma.py where --project-root .
|
||||
|
||||
# 预检环境
|
||||
python noma/scripts/noma.py preflight --project-root .
|
||||
|
||||
# 查看项目状态
|
||||
python noma/scripts/noma.py status --project-root .
|
||||
|
||||
# 索引统计
|
||||
python noma/scripts/noma.py index --project-root . stats
|
||||
|
||||
# 工作流检测
|
||||
python noma/scripts/noma.py workflow --project-root . detect
|
||||
```
|
||||
|
||||
## RAG 检索系统
|
||||
|
||||
### 关键词搜索(推荐)
|
||||
|
||||
```bash
|
||||
# 搜索爽点相关模板
|
||||
python noma/scripts/data_modules/rag_manager.py --project-root . search "爽点" --layers system
|
||||
|
||||
# 搜索打脸相关
|
||||
python noma/scripts/data_modules/rag_manager.py --project-root . search "打脸" --layers system
|
||||
|
||||
# 搜索剑修题材
|
||||
python noma/scripts/data_modules/rag_manager.py --project-root . search "剑修" --layers project,system
|
||||
|
||||
# 列出系统层所有模式
|
||||
python noma/scripts/data_modules/rag_manager.py --project-root . list --layer system
|
||||
```
|
||||
|
||||
### 向量搜索
|
||||
|
||||
```bash
|
||||
python noma/scripts/noma.py rag --project-root . search --query "爽点设计"
|
||||
```
|
||||
|
||||
## 三层 RAG 架构
|
||||
|
||||
| 层级 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 项目私有 | `.noma/rag/` | 仅当前项目可用 |
|
||||
| 系统共享 | `.noma/rag/shared/` | 所有项目共享的模板 |
|
||||
|
||||
### 同步模式到系统层
|
||||
|
||||
```bash
|
||||
# 同步单个模式
|
||||
python noma/scripts/data_modules/rag_manager.py --project-root . sync --pattern-id <id>
|
||||
|
||||
# 同步所有模式
|
||||
python noma/scripts/data_modules/rag_manager.py --project-root . sync --all
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. **创建项目**: `init_project.py`
|
||||
2. **章节创作**: 在 `chapters/` 目录编写
|
||||
3. **索引更新**: 自动跟踪实体、关系
|
||||
4. **风格检查**: 使用 style_sampler
|
||||
5. **备份**: 使用 backup_manager
|
||||
|
||||
## 爽点模型
|
||||
|
||||
系统内置三大爽感模型:
|
||||
|
||||
1. **禁忌僭越模型** - 边缘拉扯、危险感
|
||||
2. **降维打击模型** - 碾压、反转、绝地反击
|
||||
3. **认知闭环模型** - 多线伏笔收束、解谜快感
|
||||
|
||||
## 题材模板
|
||||
|
||||
系统共享题材库位于 `.noma/rag/shared/genres/`:
|
||||
|
||||
- `dog-blood-romance/` - 都市/豪门爽文
|
||||
- `period-drama/` - 古言/宫斗
|
||||
- `xuanhuan/` - 玄幻/修仙
|
||||
- `realistic/` - 现实题材
|
||||
- `rules-mystery/` - 规则/悬疑
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 预检失败
|
||||
|
||||
```bash
|
||||
# 检查所有组件
|
||||
python noma/scripts/noma.py preflight --project-root .
|
||||
```
|
||||
|
||||
### RAG 搜索无结果
|
||||
|
||||
- 确认 Ollama 服务运行中: `ollama serve`
|
||||
- 确认 Embedding 模型已下载: `ollama pull bge-m3:latest`
|
||||
- 使用关键词搜索作为回退
|
||||
|
||||
### Git 备份失败
|
||||
|
||||
```bash
|
||||
# 初始化 Git 仓库
|
||||
cd <project>
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
# Noma Commands
|
||||
|
||||
## /noma-write
|
||||
|
||||
写小说章节。
|
||||
|
||||
```
|
||||
/noma-write [chapter] [--fast] [--minimal]
|
||||
```
|
||||
|
||||
- `chapter`: 章节号(默认从 state.json 读取当前章节)
|
||||
- `--fast`: 跳过风格适配步骤
|
||||
- `--minimal`: 仅执行基础审查
|
||||
|
||||
## /noma-init
|
||||
|
||||
初始化新小说项目。
|
||||
|
||||
```
|
||||
/noma-init --title "标题" --genre "题材"
|
||||
```
|
||||
|
||||
## /noma-plan
|
||||
|
||||
规划小说大纲。
|
||||
|
||||
```
|
||||
/noma-plan [--chapters N]
|
||||
```
|
||||
|
||||
## /noma-review
|
||||
|
||||
审查已完成章节。
|
||||
|
||||
```
|
||||
/noma-review [--chapter N]
|
||||
```
|
||||
|
||||
## /noma-resume
|
||||
|
||||
恢复中断的写作任务。
|
||||
|
||||
```
|
||||
/noma-resume
|
||||
```
|
||||
|
||||
## /noma-query
|
||||
|
||||
查询项目状态和实体信息。
|
||||
|
||||
```
|
||||
/noma-query entities [--type TYPE]
|
||||
/noma-query state
|
||||
/nnoma-query hooks
|
||||
```
|
||||
|
||||
## /noma-learn
|
||||
|
||||
学习参考项目或文档。
|
||||
|
||||
```
|
||||
/noma-learn --path PATH
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "novelmaster-marketplace",
|
||||
"description": "Marketplace for the novelmaster Claude Code plugin.",
|
||||
"metadata": {
|
||||
"description": "Marketplace for installing the novelmaster plugin."
|
||||
},
|
||||
"owner": {
|
||||
"name": "dadizk"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "novelmaster",
|
||||
"description": "长篇网文创作系统(skills + agents + data chain + RAG)",
|
||||
"version": "5.5.4",
|
||||
"author": {
|
||||
"name": "dadizk"
|
||||
},
|
||||
"source": "./noma",
|
||||
"category": "productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "novelmaster",
|
||||
"version": "5.5.4",
|
||||
"description": "长篇网文创作系统(skills + agents + data chain + RAG)",
|
||||
"author": {
|
||||
"name": "dadizk"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"keywords": [
|
||||
"novelmaster",
|
||||
"noma",
|
||||
"claude-code",
|
||||
"skills",
|
||||
"agents",
|
||||
"rag"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Bash",
|
||||
"Grep",
|
||||
"Glob",
|
||||
"Task",
|
||||
"WebFetch",
|
||||
"WebSearch"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"postToolUse": {
|
||||
"black": {
|
||||
"enabled": true,
|
||||
"patterns": ["**/*.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
.claude/settings.local.json
|
||||
|
||||
# Python caches & virtualenv
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Local runtime data
|
||||
.novelmaster/
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Workspace artifacts
|
||||
node_modules/
|
||||
.npm-cache/
|
||||
dist/
|
||||
!noma/noma-dashboard/frontend/dist/
|
||||
!noma/noma-dashboard/frontend/dist/**
|
||||
nul
|
||||
.tmp/
|
||||
.tmp_*
|
||||
.tmp_outline_check/
|
||||
|
||||
# Local research / sibling projects (avoid accidental commit)
|
||||
.sisyphus/
|
||||
De-AI-Prompt/
|
||||
MuMuAINovel/
|
||||
hapi-tool/
|
||||
ai-writing-knowledge/
|
||||
novel/
|
||||
novel-refined/
|
||||
|
||||
# Local analysis snapshots and draft docs
|
||||
PROJECT_MINDMAP*.md
|
||||
novelmaster-v5*-architecture.svg
|
||||
_v52_*.json
|
||||
_v52_*.txt
|
||||
claude_inventory.csv
|
||||
*_utf8_sample.txt
|
||||
@@ -0,0 +1,62 @@
|
||||
# OpenNovel Workspace (.workspace)
|
||||
|
||||
这是用户的实际创作区。项目启动后,Noma 系统在此目录下管理所有创作数据。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
.workspace/
|
||||
├── .claude/ # Claude Code 项目配置
|
||||
│ ├── settings.json # 项目级设置
|
||||
│ └── commands.md # 项目命令
|
||||
├── .noma/ # Noma 系统数据目录
|
||||
│ ├── state.json # 项目状态
|
||||
│ ├── index.db # SQLite 数据库
|
||||
│ ├── vectors.db # 向量数据库
|
||||
│ └── ledger.json # 账本数据
|
||||
├── 正文/ # 小说正文
|
||||
│ └── 第0001章-标题.md
|
||||
├── 大纲/ # 章节大纲
|
||||
│ └── 第0001章-标题.md
|
||||
├── 设定集/ # 世界观设定
|
||||
│ ├── 角色/
|
||||
│ ├── 地点/
|
||||
│ ├── 势力/
|
||||
│ └── 物品/
|
||||
└── 输出/ # AI 生成内容
|
||||
└── draft/
|
||||
```
|
||||
|
||||
## 初始化
|
||||
|
||||
首次创建项目时,运行:
|
||||
|
||||
```bash
|
||||
noma init --title "小说标题" --genre "xuanhuan"
|
||||
```
|
||||
|
||||
## 状态管理
|
||||
|
||||
- `state.json`: 包含进度、主角状态、节奏追踪
|
||||
- `index.db`: 包含实体、别名、关系、章节索引
|
||||
- `ledger.json`: 包含资产、负债、Hook 池
|
||||
|
||||
## 与系统目录的关联
|
||||
|
||||
```
|
||||
noma/ # 系统定义(不修改)
|
||||
├── openspec/ # ← 关联 → .workspace/.noma/genesis_contract.json
|
||||
├── core_engine/ # ← 关联 → .workspace/.noma/state.json
|
||||
├── agents/ # ← 工作时调用
|
||||
└── matrices/ # ← 写作时引用
|
||||
|
||||
.workspace/ # 用户创作区(实际修改)
|
||||
└── 正文/ # ← AI 写入的目标
|
||||
```
|
||||
|
||||
## 工作流
|
||||
|
||||
1. **规划**: Planner agents 读取 `noma/openspec/` 和 `matrices/`
|
||||
2. **写作**: Writer agents 输出到 `.workspace/输出/`
|
||||
3. **审查**: Checker agents 检查 `.workspace/正文/`
|
||||
4. **确认**: 用户审核后移动到 `.workspace/正文/`
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
# Noma 项目修复记录
|
||||
|
||||
## 修复概览
|
||||
|
||||
**修复时间**: 2026-03-30
|
||||
**修复版本**: v5.5.5
|
||||
|
||||
本次修复解决了以下关键问题:
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已修复问题
|
||||
|
||||
### 🔴 严重问题
|
||||
|
||||
#### 1. 缺失 `update_state.py` 脚本
|
||||
- **文件位置**: `noma/scripts/update_state.py`
|
||||
- **问题描述**: 脚本被多处引用但文件不存在,导致 `/noma-review`, `/noma-plan` 等命令的状态更新失败
|
||||
- **修复内容**:
|
||||
- 创建完整的 CLI 脚本
|
||||
- 支持 4 个核心命令:
|
||||
- `add-review`: 添加审查报告记录
|
||||
- `update-entity`: 更新实体属性
|
||||
- `set-chapter-meta`: 设置章节元数据
|
||||
- `set-progress`: 更新项目进度
|
||||
- 实现原子写入和文件锁保护
|
||||
- 自动规范化 Windows 路径
|
||||
- **状态**: ✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
### 🟡 中等问题
|
||||
|
||||
#### 2. CatchupAgent 脏标记检查未实现
|
||||
- **文件位置**: `noma/agents/catchup_agent.py`
|
||||
- **问题描述**: `_check_dirty_flags()` 方法只有 TODO 注释,返回空列表
|
||||
- **影响**: 设定修改后无法触发角色状态自动更新
|
||||
- **修复内容**:
|
||||
- 实现完整的脏标记检测逻辑
|
||||
- 从 ledger.json 读取 retcon 事件
|
||||
- 筛选影响目标角色的变更
|
||||
- 检查 state.json 中的待同步字段
|
||||
- 返回待应用的补丁列表
|
||||
- **状态**: ✅ 已完成
|
||||
|
||||
#### 3. Windows 路径处理问题
|
||||
- **文件位置**: `noma/scripts/data_modules/noma.py`
|
||||
- **问题描述**: `extract-context` 命令未使用规范化路径
|
||||
- **影响**: 可能在 Windows 上出现路径分隔符错误
|
||||
- **修复内容**:
|
||||
- 在生成章节路径时调用 `normalize_windows_path()`
|
||||
- 确保路径格式统一
|
||||
- **状态**: ✅ 已完成
|
||||
|
||||
#### 4. 缺失 requirements.txt 文件
|
||||
- **文件位置**:
|
||||
- `noma/scripts/requirements.txt`
|
||||
- `noma/interfaces/web_dashboard/requirements.txt`
|
||||
- **问题描述**: 主 requirements.txt 引用的子文件不存在
|
||||
- **影响**: 依赖安装失败
|
||||
- **修复内容**:
|
||||
- 创建 scripts 依赖文件(包含核心库、测试框架)
|
||||
- 创建 web dashboard 依赖文件(FastAPI、uvicorn 等)
|
||||
- **状态**: ✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
### 🟢 优化改进
|
||||
|
||||
#### 5. PowerShell 兼容性说明
|
||||
- **文件位置**: `noma/README.md`
|
||||
- **问题描述**: 文档仅包含 Bash 示例
|
||||
- **影响**: Windows PowerShell 用户难以理解用法
|
||||
- **修复内容**:
|
||||
- 为所有命令添加 PowerShell 和 Bash 双版本说明
|
||||
- 添加 PowerShell 用户注意事项
|
||||
- **状态**: ✅ 已完成
|
||||
|
||||
#### 6. UTF-8 编码错误处理
|
||||
- **文件位置**: `noma/scripts/runtime_compat.py`
|
||||
- **问题描述**: Windows UTF-8 配置完全静默失败
|
||||
- **影响**: 调试困难
|
||||
- **修复内容**:
|
||||
- 添加环境变量设置 (`PYTHONIOENCODING`)
|
||||
- 添加日志记录(警告级别)
|
||||
- 保留原有静默失败行为但记录原因
|
||||
- **状态**: ✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 📋 变更文件清单
|
||||
|
||||
```
|
||||
noma/
|
||||
├── scripts/
|
||||
│ ├── update_state.py ✨ 新建 (254 行)
|
||||
│ ├── noma.py 🔧 修改 (3 行)
|
||||
│ ├── runtime_compat.py 🔧 修改 (8 行新增,3 行删除)
|
||||
│ └── requirements.txt ✨ 新建 (27 行)
|
||||
├── agents/
|
||||
│ └── catchup_agent.py 🔧 修改 (63 行新增,4 行删除)
|
||||
├── interfaces/web_dashboard/
|
||||
│ └── requirements.txt ✨ 新建 (13 行)
|
||||
└── README.md 🔧 修改 (23 行新增,1 行删除)
|
||||
```
|
||||
|
||||
**总计**:
|
||||
- ✨ 新建文件:3 个
|
||||
- 🔧 修改文件:4 个
|
||||
- 📝 新增代码:~350 行
|
||||
- 🗑️ 删除代码:~8 行
|
||||
|
||||
---
|
||||
|
||||
## 🧪 验证步骤
|
||||
|
||||
### 1. 测试 update_state.py
|
||||
|
||||
```powershell
|
||||
# 进入测试项目目录
|
||||
cd e:\Project\NovelMaster\workspaces\homestory
|
||||
|
||||
# 测试添加审查报告
|
||||
python ..\..\noma\scripts\update_state.py --project-root . add-review "1-5" "测试报告.md"
|
||||
|
||||
# 测试更新实体
|
||||
python ..\..\noma\scripts\update_state.py --project-root . update-entity 角色 测试角色 '{"境界": "斗者"}'
|
||||
|
||||
# 测试设置章节元数据
|
||||
python ..\..\noma\scripts\update_state.py --project-root . set-chapter-meta 1 '{"word_count": 2000}'
|
||||
|
||||
# 测试设置进度
|
||||
python ..\..\noma\scripts\update_state.py --project-root . set-progress --current-chapter 5
|
||||
```
|
||||
|
||||
预期输出:
|
||||
```
|
||||
✅ Added review checkpoint: 1-5 -> 测试报告.md
|
||||
✅ Updated entity: 角色/测试角色
|
||||
Fields updated: ['境界']
|
||||
✅ Set chapter meta for chapter 1
|
||||
Keys: ['word_count']
|
||||
✅ Updated progress: current_chapter = 5
|
||||
```
|
||||
|
||||
### 2. 测试 Catchup Agent
|
||||
|
||||
需要创建测试数据:
|
||||
|
||||
```python
|
||||
# 创建测试 ledger.json
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ledger_data = {
|
||||
"retcon_events": [
|
||||
{
|
||||
"timestamp": "2026-03-30T10:00:00",
|
||||
"chapter": 10,
|
||||
"affected_entities": ["萧炎"],
|
||||
"changes": [
|
||||
{
|
||||
"entity_id": "萧炎",
|
||||
"field": "境界",
|
||||
"old_value": "斗师",
|
||||
"new_value": "大斗师",
|
||||
"reason": "作者设定修改"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
ledger_path = Path("e:/Project/NovelMaster/workspaces/homestory/.noma/novel_data/ledger.json")
|
||||
ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(ledger_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(ledger_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 运行 CatchupAgent 测试
|
||||
from agents.catchup_agent import CatchupAgent
|
||||
|
||||
agent = CatchupAgent(
|
||||
project_root="e:/Project/NovelMaster/workspaces/homestory",
|
||||
storage_path="../noma/novel_data/"
|
||||
)
|
||||
|
||||
dirty_flags = agent._check_dirty_flags("萧炎")
|
||||
print(f"Dirty flags found: {len(dirty_flags)}")
|
||||
for flag in dirty_flags:
|
||||
print(f" - {flag['field']}: {flag.get('old_value')} -> {flag.get('new_value')}")
|
||||
```
|
||||
|
||||
预期输出:
|
||||
```
|
||||
Dirty flags found: 1
|
||||
- 境界:斗师 -> 大斗师
|
||||
```
|
||||
|
||||
### 3. 测试 extract-context
|
||||
|
||||
```powershell
|
||||
# 测试路径规范化
|
||||
cd e:\Project\NovelMaster\workspaces\homestory
|
||||
python ..\..\noma\scripts\noma.py --project-root . extract-context --chapter 1
|
||||
```
|
||||
|
||||
### 4. 安装依赖
|
||||
|
||||
```powershell
|
||||
# 测试依赖安装
|
||||
cd e:\Project\NovelMaster\noma
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⏳ 后续建议
|
||||
|
||||
### 短期(1-2 周)
|
||||
- [ ] 完成所有修复的单元测试
|
||||
- [ ] 更新技能文档中的命令示例
|
||||
- [ ] 添加更多错误处理和日志记录
|
||||
|
||||
### 中期(1 个月)
|
||||
- [ ] 实现完整的 Retcon UI 界面
|
||||
- [ ] 添加批量操作支持
|
||||
- [ ] 优化 SQLite 同步性能
|
||||
|
||||
### 长期(3 个月)
|
||||
- [ ] 开发 Web 版可视化面板
|
||||
- [ ] 实现分布式 RAG 检索
|
||||
- [ ] 添加多项目协同支持
|
||||
|
||||
---
|
||||
|
||||
## 📊 技术债务更新
|
||||
|
||||
| 问题 | 优先级 | 状态 | 工作量 |
|
||||
|------|--------|------|--------|
|
||||
| update_state.py 缺失 | 🔴 | ✅ 已修复 | 4h |
|
||||
| 脏标记检查未实现 | 🟡 | ✅ 已修复 | 3h |
|
||||
| Windows 路径问题 | 🟡 | ✅ 已修复 | 1h |
|
||||
| requirements.txt 缺失 | 🟢 | ✅ 已修复 | 1h |
|
||||
| PowerShell 兼容性 | 🟢 | ✅ 已修复 | 1h |
|
||||
| UTF-8 错误处理 | 🟢 | ✅ 已修复 | 0.5h |
|
||||
| 单元测试覆盖率低 | 🟡 | ⏳ 待处理 | 8h |
|
||||
| 文档更新滞后 | 🟢 | ⏳ 待处理 | 4h |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [架构说明](./docs/architecture.md)
|
||||
- [命令详解](./docs/commands.md)
|
||||
- [Core Engine](./docs/core_engine.md)
|
||||
- [Agents 说明](./docs/agents.md)
|
||||
|
||||
---
|
||||
|
||||
**修复完成时间**: 2026-03-30
|
||||
**修复人员**: AI Assistant
|
||||
**测试状态**: 待验证
|
||||
+668
@@ -0,0 +1,668 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place, and offer equivalent access to the Corresponding Source in
|
||||
the same way through the same place at no further charge. You need
|
||||
not require recipients to copy the Corresponding Source along with
|
||||
the object code. If the place to copy the object code is a network
|
||||
server, the Corresponding Source may be on a different server
|
||||
(operated by you or a third party) that supports equivalent copying
|
||||
facilities, provided you maintain clear directions next to the object
|
||||
code saying where to find the Corresponding Source. Regardless of
|
||||
what server hosts the Corresponding Source, you remain obligated to
|
||||
ensure that it is available for as long as needed to satisfy these
|
||||
requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a typical
|
||||
or common use of that class of product, regardless of the status of the
|
||||
particular user or of the way in which the particular user actually uses,
|
||||
or expects or is expected to use, the product. A product is a consumer
|
||||
product regardless of whether the product has substantial commercial,
|
||||
industrial or non-consumer uses, unless such uses represent the only
|
||||
significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey a covered work in the case of Installation Information,
|
||||
you provide it in a User Product in the case of a transaction, the right
|
||||
of possession and use of the User Product is transferred to the recipient
|
||||
in perpetuity or for a fixed term, regardless of how the transaction is
|
||||
characterized, the Corresponding Source conveyed under this section must
|
||||
be accompanied by the Installation Information. But this requirement does
|
||||
not apply if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has been
|
||||
installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in source
|
||||
code form), and must require no special password or key for unpacking,
|
||||
reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by this
|
||||
License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work, for
|
||||
which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further restriction,
|
||||
you may remove that term. If a license document contains a further
|
||||
restriction but permits relicensing or conveying under this License, you
|
||||
may add to a covered work material governed by the terms of that license
|
||||
document, provided that the further restriction does not survive such
|
||||
relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the additional
|
||||
terms that apply to those files, or a notice indicating where to find
|
||||
the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions; the above
|
||||
requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license
|
||||
from a particular copyright holder is reinstated (a) provisionally,
|
||||
unless and until the copyright holder explicitly and finally terminates
|
||||
your license, and (b) permanently, if the copyright holder fails to
|
||||
notify you of the violation by some reasonable means prior to 60 days
|
||||
after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is reinstated
|
||||
permanently if the copyright holder notifies you of the violation by some
|
||||
reasonable means, this is the first time you have received notice of
|
||||
violation of this License (for any work) from that copyright holder, and
|
||||
you cure the violation prior to 30 days after your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or run
|
||||
a copy of the Program. Ancillary propagation of a covered work occurring
|
||||
solely as a consequence of using peer-to-peer transmission to receive a
|
||||
copy likewise does not require acceptance. However, nothing other than
|
||||
this License grants you permission to propagate or modify any covered work.
|
||||
These actions infringe copyright if you do not accept this License.
|
||||
Therefore, by modifying or propagating a covered work, you indicate your
|
||||
acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically receives
|
||||
a license from the original licensors, to run, modify and propagate that
|
||||
work, subject to this License. You are not responsible for enforcing
|
||||
compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that transaction
|
||||
who receives a copy of the work also receives whatever licenses to the
|
||||
work the party's predecessor in interest had or could give under the
|
||||
previous paragraph, plus a right to possession of the Corresponding Source
|
||||
of the work from the predecessor in interest, if the predecessor has it
|
||||
or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may not
|
||||
impose a license fee, royalty, or other charge for exercise of rights
|
||||
granted under this License, and you may not initiate litigation (including
|
||||
a cross-claim or counterclaim in a lawsuit) alleging that any patent claim
|
||||
is infringed by making, using, selling, offering for sale, or importing the
|
||||
Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The work
|
||||
thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims owned
|
||||
or controlled by the contributor, whether already acquired or hereafter
|
||||
acquired, that would be infringed by some manner, permitted by this
|
||||
License, of making, using, or selling its contributor version, but do
|
||||
not include claims that would be infringed only as a consequence of
|
||||
further modification of the contributor version. For purposes of this
|
||||
definition, "control" includes the right to grant patent sublicenses in
|
||||
a manner consistent with the requirements of this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to make,
|
||||
use, sell, offer for sale, import and otherwise run, modify and propagate
|
||||
the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is conditioned
|
||||
on the non-exercise of one or more of the rights that are specifically
|
||||
granted under this License. You may not convey a covered work if you are
|
||||
a party to an arrangement with a third party that is in the business of
|
||||
distributing software, under which you make payment to the third party
|
||||
based on the extent of your activity of conveying the work, and under
|
||||
which the third party grants, to any of the parties who would receive
|
||||
the covered work from you, a discriminatory patent license (a) in
|
||||
connection with copies of the covered work conveyed by you (or copies
|
||||
made from those copies), or (b) primarily for and in connection with
|
||||
specific products or compilations that contain the covered work, unless
|
||||
you entered into that arrangement, or that patent license was granted,
|
||||
prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may otherwise
|
||||
be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not convey it at all. For example, if you agree to terms that obligate
|
||||
you to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have permission
|
||||
to link or combine any covered work with a work licensed under version 3
|
||||
of the GNU Affero General Public License into a single combined work, and
|
||||
to convey the resulting work. The terms of this License will continue to
|
||||
apply to the part which is the covered work, but the special requirements
|
||||
of the GNU Affero General Public License, section 13, concerning interaction
|
||||
through a network will apply to the combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies that a certain numbered version of the GNU General Public License
|
||||
"or any later version" applies to it, you have the option of following the
|
||||
terms and conditions either of that numbered version or of any later
|
||||
version published by the Free Software Foundation. If the Program does
|
||||
not specify a version number of the GNU General Public License, you may
|
||||
choose any version ever published by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions
|
||||
of the GNU General Public License can be used, that proxy's public
|
||||
statement of acceptance of a version permanently authorizes you to choose
|
||||
that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different permissions.
|
||||
However, no additional obligations are imposed on any author or copyright
|
||||
holder as a result of your choosing to follow a later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided above
|
||||
cannot be given local legal effect according to their terms, reviewing
|
||||
courts shall apply local law that most closely approximates an absolute
|
||||
waiver of all civil liability in connection with the Program, unless a
|
||||
warranty or assumption of liability accompanies a copy of the Program in
|
||||
return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a copyright disclaimer for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# NovelMaster (Noma) 5.0 - OpenNovel Workspace
|
||||
|
||||
> 融合"欲望心理学引擎"与"硬状态基建"的人机协同小说 IDE
|
||||
|
||||
## 项目介绍
|
||||
|
||||
NovelMaster (Noma) 是基于 Claude Code 的长篇网文创作系统,目标是降低 AI 写作中的"遗忘"和"幻觉",支持长周期连载创作。它是 [OpenNovel Workspace (ONW)](readme_NovelMaster.md) 架构的实现。
|
||||
|
||||
详细文档已拆分到 `docs/`。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1) 安装插件
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add dadizk/novelmaster --scope user
|
||||
claude plugin install novelmaster@novelmaster-marketplace --scope user
|
||||
```
|
||||
|
||||
### 2) 安装依赖
|
||||
|
||||
**Windows PowerShell**:
|
||||
```powershell
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Linux/macOS Bash**:
|
||||
```bash
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3) 初始化项目
|
||||
|
||||
**PowerShell**:
|
||||
```powershell
|
||||
/noma-init
|
||||
```
|
||||
|
||||
**Bash**:
|
||||
```bash
|
||||
/noma-init
|
||||
```
|
||||
|
||||
### 4) 配置 RAG 环境
|
||||
|
||||
创建 `.env` 文件:
|
||||
|
||||
```bash
|
||||
EMBED_BASE_URL=https://api-inference.modelscope.cn/v1
|
||||
EMBED_MODEL=Qwen/Qwen3-Embedding-8B
|
||||
EMBED_API_KEY=your_key
|
||||
```
|
||||
|
||||
### 5) 开始使用
|
||||
|
||||
**PowerShell**:
|
||||
```powershell
|
||||
/noma-plan 1 # 规划大纲
|
||||
/noma-write 1 # 写第 1 章
|
||||
/noma-review 1 # 审查章节
|
||||
```
|
||||
|
||||
**Bash**:
|
||||
```bash
|
||||
/noma-plan 1 # 规划大纲
|
||||
/noma-write 1 # 写第 1 章
|
||||
/noma-review 1 # 审查章节
|
||||
```
|
||||
|
||||
> 💡 **PowerShell 用户注意**: 如果遇到路径问题,请使用正斜杠 `/` 或双反斜杠 `\\` 分隔路径。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
noma/
|
||||
├── .claude/ # Claude Code 配置
|
||||
│ ├── settings.json
|
||||
│ └── commands.md
|
||||
├── .claude-plugin/ # 插件元数据
|
||||
│ ├── plugin.json
|
||||
│ └── marketplace.json
|
||||
│
|
||||
├── skills/ # 技能模块
|
||||
│ ├── noma-init/ # 初始化
|
||||
│ ├── noma-plan/ # 规划
|
||||
│ ├── noma-write/ # 写作
|
||||
│ ├── noma-review/ # 审查
|
||||
│ ├── noma-resume/ # 恢复
|
||||
│ ├── noma-query/ # 查询
|
||||
│ ├── noma-learn/ # 学习
|
||||
│ └── noma-dashboard/ # 可视化面板
|
||||
│
|
||||
├── openspec/ # 数据标准
|
||||
│ ├── genesis_contract.json
|
||||
│ └── project.md
|
||||
│
|
||||
├── core_engine/ # 核心引擎
|
||||
│ ├── state_manager/ # 状态管理
|
||||
│ ├── memory_rag/ # 记忆 RAG
|
||||
│ └── configurators/ # IDE 适配器
|
||||
│
|
||||
├── agents/ # Agent 矩阵
|
||||
│ ├── planners/ # 规划器
|
||||
│ ├── writers/ # 写作器
|
||||
│ ├── checkers/ # 审查器
|
||||
│ └── catchup_agent.py
|
||||
│
|
||||
├── matrices/ # 心理学武器库
|
||||
│ ├── catharsis_models/ # 爽感模型
|
||||
│ ├── genres/ # 题材库
|
||||
│ └── shared/ # 共享参考
|
||||
│
|
||||
├── interfaces/ # 人机界面
|
||||
│ └── web_dashboard/
|
||||
│
|
||||
├── scripts/ # 数据模块
|
||||
│ ├── data_modules/
|
||||
│ └── noma.py
|
||||
│
|
||||
├── references/ # 共享引用
|
||||
│
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 核心特性
|
||||
|
||||
1. **欲望沙盒** - 定义核心欲望和伦理边界
|
||||
2. **多维爽感路由** - 禁忌僭越、降维打击、认知闭环
|
||||
3. **元叙事开关** - 一键关闭物理连贯性校验
|
||||
4. **意图漂移仲裁** - 热补丁处理设定修改冲突
|
||||
|
||||
## CLI 命令
|
||||
|
||||
```bash
|
||||
/noma-init --title "标题" --genre "xuanhuan"
|
||||
/noma-plan --chapters 100
|
||||
/noma-write --chapter 1
|
||||
/noma-review --chapter 1
|
||||
/noma-query state
|
||||
/noma-resume
|
||||
/noma-dashboard
|
||||
```
|
||||
|
||||
## 题材支持
|
||||
|
||||
| 题材 | 目录 |
|
||||
|------|------|
|
||||
| 玄幻/修仙 | `xuanhuan/` |
|
||||
| 都市 | `realistic/` |
|
||||
| 甜宠 | `dog-blood-romance/` |
|
||||
| 历史 | `period-drama/` |
|
||||
| 悬疑 | `rules-mystery/` |
|
||||
| 知乎文 | `zhihu-short/` |
|
||||
|
||||
## 文档
|
||||
|
||||
- [docs/architecture.md](docs/architecture.md) - 架构说明
|
||||
- [docs/commands.md](docs/commands.md) - 命令详解
|
||||
- [docs/rag-and-config.md](docs/rag-and-config.md) - RAG 配置
|
||||
- [docs/genres.md](docs/genres.md) - 题材模板
|
||||
- [docs/openspec.md](docs/openspec.md) - OpenSpec 结构
|
||||
- [docs/core_engine.md](docs/core_engine.md) - CoreEngine 结构
|
||||
- [docs/matrices.md](docs/matrices.md) - Matrices 结构
|
||||
- [docs/interfaces.md](docs/interfaces.md) - Interfaces 结构
|
||||
- [docs/agents.md](docs/agents.md) - Agents 结构
|
||||
|
||||
## 许可证
|
||||
|
||||
GPL-3.0
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: novelmaster
|
||||
description: |
|
||||
NovelMaster (Noma) 5.0 - OpenNovel Workspace 智能小说创作系统。
|
||||
融合"欲望心理学引擎"与"硬状态基建"的人机协同小说 IDE。
|
||||
提供从大纲规划、章节写作、审查润色到数据管理的完整工作流。
|
||||
|
||||
当用户要写小说、写网文、创作故事时使用。
|
||||
当用户要初始化项目、规划大纲时使用。
|
||||
当用户要查询项目状态、审查章节时使用。
|
||||
allowed-tools: Read Write Edit Grep Glob Bash Task WebFetch WebSearch
|
||||
---
|
||||
|
||||
# Noma - OpenNovel Workspace (ONW) 5.0
|
||||
|
||||
Noma 是一个智能小说创作系统,提供从灵感到发布的完整工作流。
|
||||
|
||||
## 核心技能
|
||||
|
||||
| 技能 | 说明 | 位置 |
|
||||
|------|------|------|
|
||||
| `/noma-init` | 初始化新小说项目 | `skills/noma-init/` |
|
||||
| `/noma-plan` | 规划小说大纲 | `skills/noma-plan/` |
|
||||
| `/noma-write` | 写小说章节 | `skills/noma-write/` |
|
||||
| `/noma-review` | 审查章节质量 | `skills/noma-review/` |
|
||||
| `/noma-resume` | 恢复中断任务 | `skills/noma-resume/` |
|
||||
| `/noma-query` | 查询项目状态 | `skills/noma-query/` |
|
||||
| `/noma-learn` | 学习参考文档 | `skills/noma-learn/` |
|
||||
| `/noma-dashboard` | 可视化面板 | `skills/noma-dashboard/` |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 初始化项目
|
||||
|
||||
```bash
|
||||
/noma-init --title "我的小说" --genre "xuanhuan"
|
||||
```
|
||||
|
||||
### 2. 规划大纲
|
||||
|
||||
```bash
|
||||
/noma-plan --chapters 100
|
||||
```
|
||||
|
||||
### 3. 写章节
|
||||
|
||||
```bash
|
||||
/noma-write --chapter 1
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
project/
|
||||
├── .noma/ # 系统数据
|
||||
│ ├── state.json # 项目状态
|
||||
│ └── index.db # SQLite 数据库
|
||||
├── 正文/ # 小说正文
|
||||
├── 大纲/ # 章节大纲
|
||||
└── 设定集/ # 世界观设定
|
||||
```
|
||||
|
||||
## ONW 5.0 特性
|
||||
|
||||
1. **欲望沙盒与定制伦理** - 定义核心欲望和伦理边界
|
||||
2. **多维爽感路由** - 动态调用心理学张力模型
|
||||
3. **元叙事开关** - 一键关闭物理连贯性校验
|
||||
4. **意图漂移仲裁** - 热补丁处理设定修改冲突
|
||||
|
||||
## 详见文档
|
||||
|
||||
- [README.md](./README.md) - 完整系统文档
|
||||
- [docs/](./docs/) - 各模块详细文档
|
||||
@@ -0,0 +1,446 @@
|
||||
"""
|
||||
catchup_agent.py - NovelMaster Catchup Agent
|
||||
|
||||
负责计算休眠角色跨越时间后的状态。
|
||||
利用脏标记 (Dirty Flag) 订阅机制,当休眠人物被唤醒时,
|
||||
进行惰性时间流逝推演。
|
||||
|
||||
This agent is part of the NovelMaster (noma) system for noma writing.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class CatchupResult:
|
||||
"""catchup_agent 输出结果"""
|
||||
agent: str = "catchup_agent"
|
||||
awakened_characters: List[Dict[str, Any]] = None
|
||||
state_updates: List[Dict[str, Any]] = None
|
||||
timeline_changes: List[Dict[str, Any]] = None
|
||||
warnings: List[str] = None
|
||||
pass_count: int = 0
|
||||
fail_count: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
if self.awakened_characters is None:
|
||||
self.awakened_characters = []
|
||||
if self.state_updates is None:
|
||||
self.state_updates = []
|
||||
if self.timeline_changes is None:
|
||||
self.timeline_changes = []
|
||||
if self.warnings is None:
|
||||
self.warnings = []
|
||||
|
||||
|
||||
class CatchupAgent:
|
||||
"""
|
||||
负责休眠角色的状态 catchup(追赶上当前时间线)。
|
||||
|
||||
触发条件:
|
||||
- 休眠角色被唤醒(出现在新章节中)
|
||||
- 角色从上一次出场到当前章节有较大时间跨度
|
||||
- 手动触发 retcon(设定修改)
|
||||
|
||||
核心逻辑:
|
||||
1. 检测休眠角色(长时间未出场的角色)
|
||||
2. 计算时间跨度(从上次出场到当前章节)
|
||||
3. 根据时间跨度推演角色状态变化
|
||||
4. 检查脏标记 (Dirty Flag),如果有 retcon 影响则应用补丁
|
||||
5. 输出状态更新到 state.json 和 ledger
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: str, storage_path: str = ".noma/novel_data/"):
|
||||
self.project_root = Path(project_root)
|
||||
self.storage_path = self.project_root / storage_path
|
||||
self.state_file = self.storage_path / "state.json"
|
||||
self.index_db = self.storage_path / "index.db"
|
||||
|
||||
def execute(
|
||||
self,
|
||||
current_chapter: int,
|
||||
awakened_chars: Optional[List[str]] = None
|
||||
) -> CatchupResult:
|
||||
"""
|
||||
执行 catchup 计算。
|
||||
|
||||
Args:
|
||||
current_chapter: 当前章节编号
|
||||
awakened_chars: 被唤醒的角色 ID 列表(如果为 None,则自动检测)
|
||||
|
||||
Returns:
|
||||
CatchupResult: 包含所有状态更新的结果
|
||||
"""
|
||||
# 1. 加载 state.json
|
||||
state = self._load_state()
|
||||
|
||||
# 2. 加载角色最后出场信息
|
||||
character_last_appearance = self._get_character_last_appearance()
|
||||
|
||||
# 3. 确定需要 catchup 的角色
|
||||
if awakened_chars is None:
|
||||
awakened_chars = self._detect_awakened_characters(
|
||||
current_chapter,
|
||||
character_last_appearance,
|
||||
state
|
||||
)
|
||||
|
||||
# 4. 计算每个角色的状态更新
|
||||
state_updates = []
|
||||
timeline_changes = []
|
||||
warnings = []
|
||||
|
||||
for char_id in awakened_chars:
|
||||
last_ch = character_last_appearance.get(char_id, 0)
|
||||
chapters_gap = current_chapter - last_ch
|
||||
|
||||
if chapters_gap <= 0:
|
||||
continue
|
||||
|
||||
# 计算时间跨度(假设每章代表一定时间范围)
|
||||
time_elapsed = self._calculate_time_elapsed(chapters_gap, state)
|
||||
|
||||
# 获取角色当前状态
|
||||
char_state = self._get_character_state(char_id, state)
|
||||
|
||||
# 检查是否有脏标记(retcon 影响)
|
||||
dirty_flags = self._check_dirty_flags(char_id)
|
||||
|
||||
# 计算追赶后的状态
|
||||
updated_state = self._compute_catchup_state(
|
||||
char_id, char_state, time_elapsed, dirty_flags
|
||||
)
|
||||
|
||||
state_updates.append({
|
||||
"character_id": char_id,
|
||||
"last_appearance_chapter": last_ch,
|
||||
"current_chapter": current_chapter,
|
||||
"chapters_gap": chapters_gap,
|
||||
"time_elapsed": time_elapsed,
|
||||
"state_before": char_state,
|
||||
"state_after": updated_state,
|
||||
"dirty_flags_applied": dirty_flags
|
||||
})
|
||||
|
||||
# 检查时间线一致性
|
||||
timeline_issue = self._check_timeline_consistency(
|
||||
char_id, time_elapsed, state
|
||||
)
|
||||
if timeline_issue:
|
||||
timeline_changes.append(timeline_issue)
|
||||
|
||||
# 5. 验证所有更新
|
||||
pass_count = len([u for u in state_updates if not u.get("warnings")])
|
||||
fail_count = len(state_updates) - pass_count
|
||||
|
||||
return CatchupResult(
|
||||
agent="catchup_agent",
|
||||
awakened_characters=awakened_chars,
|
||||
state_updates=state_updates,
|
||||
timeline_changes=timeline_changes,
|
||||
warnings=warnings,
|
||||
pass_count=pass_count,
|
||||
fail_count=fail_count
|
||||
)
|
||||
|
||||
def _load_state(self) -> Dict[str, Any]:
|
||||
"""加载 state.json"""
|
||||
if not self.state_file.exists():
|
||||
return {}
|
||||
|
||||
with open(self.state_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def _get_character_last_appearance(self) -> Dict[str, int]:
|
||||
"""从 index.db 获取角色最后出场章节"""
|
||||
result = {}
|
||||
|
||||
if not self.index_db.exists():
|
||||
return result
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.index_db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取角色最后出场章节
|
||||
cursor.execute("""
|
||||
SELECT entity_id, MAX(chapter) as last_chapter
|
||||
FROM chapter_entities
|
||||
WHERE entity_type = 'character'
|
||||
GROUP BY entity_id
|
||||
""")
|
||||
|
||||
for row in cursor.fetchall():
|
||||
result[row[0]] = row[1]
|
||||
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _detect_awakened_characters(
|
||||
self,
|
||||
current_chapter: int,
|
||||
last_appearance: Dict[str, int],
|
||||
state: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""
|
||||
自动检测需要 catchup 的角色。
|
||||
|
||||
规则:
|
||||
- 角色最后出场在 10 章之前
|
||||
- 角色当前不在休眠列表中
|
||||
- 角色不是主角
|
||||
"""
|
||||
awakened = []
|
||||
dormant_threshold = 10 # 超过此章节数则认为需要 catchup
|
||||
|
||||
protagonist_id = state.get("protagonist_state", {}).get("id", "protagonist")
|
||||
dormant_characters = state.get("dormant_characters", [])
|
||||
|
||||
for char_id, last_ch in last_appearance.items():
|
||||
# 跳过主角
|
||||
if char_id == protagonist_id:
|
||||
continue
|
||||
|
||||
# 跳过已经在 dormant 列表中的角色
|
||||
if char_id in dormant_characters:
|
||||
continue
|
||||
|
||||
# 检查是否超过阈值
|
||||
if current_chapter - last_ch >= dormant_threshold:
|
||||
awakened.append(char_id)
|
||||
|
||||
return awakened
|
||||
|
||||
def _calculate_time_elapsed(
|
||||
self,
|
||||
chapters_gap: int,
|
||||
state: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
计算时间流逝。
|
||||
|
||||
假设:
|
||||
- 每章代表 1 天(可配置)
|
||||
- 修仙世界时间流速可调
|
||||
"""
|
||||
time_config = state.get("time_config", {})
|
||||
days_per_chapter = time_config.get("days_per_chapter", 1)
|
||||
|
||||
total_days = chapters_gap * days_per_chapter
|
||||
|
||||
return {
|
||||
"days": total_days,
|
||||
"weeks": total_days // 7,
|
||||
"months": total_days // 30,
|
||||
"years": total_days // 365
|
||||
}
|
||||
|
||||
def _get_character_state(
|
||||
self,
|
||||
char_id: str,
|
||||
state: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""获取角色当前状态"""
|
||||
# 从 state.json 获取
|
||||
character_states = state.get("character_states", {})
|
||||
|
||||
if char_id in character_states:
|
||||
return character_states[char_id]
|
||||
|
||||
# 返回默认值
|
||||
return {
|
||||
"id": char_id,
|
||||
"realm": "unknown",
|
||||
"location": "unknown",
|
||||
"status": "unknown"
|
||||
}
|
||||
|
||||
def _check_dirty_flags(self, char_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
检查脏标记(retcon 影响)。
|
||||
|
||||
当作者修改设定后,相关角色的脏标记会被激活。
|
||||
|
||||
实现逻辑:
|
||||
1. 从 ledger 中读取 retcon 事件
|
||||
2. 筛选出影响目标角色的变更
|
||||
3. 返回待应用的补丁列表
|
||||
"""
|
||||
dirty_flags: List[Dict[str, Any]] = []
|
||||
|
||||
# 尝试加载 ledger
|
||||
ledger_path = self.project_root / ".noma" / "novel_data" / "ledger.json"
|
||||
if not ledger_path.exists():
|
||||
return dirty_flags
|
||||
|
||||
try:
|
||||
with open(ledger_path, "r", encoding="utf-8") as f:
|
||||
ledger = json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Failed to load ledger from {ledger_path}")
|
||||
return dirty_flags
|
||||
|
||||
# 查找 retcon 事件
|
||||
retcon_events = ledger.get("retcon_events", [])
|
||||
|
||||
for event in retcon_events:
|
||||
# 检查是否影响此角色
|
||||
affected_entities = event.get("affected_entities", [])
|
||||
if char_id not in affected_entities:
|
||||
continue
|
||||
|
||||
# 提取脏标记
|
||||
changes = event.get("changes", [])
|
||||
for change in changes:
|
||||
if change.get("entity_id") == char_id:
|
||||
dirty_flags.append({
|
||||
"field": change.get("field"),
|
||||
"old_value": change.get("old_value"),
|
||||
"new_value": change.get("new_value"),
|
||||
"reason": change.get("reason", ""),
|
||||
"event_chapter": event.get("chapter", 0),
|
||||
"timestamp": event.get("timestamp", "")
|
||||
})
|
||||
|
||||
# 同时检查 state.json 中的 character_states
|
||||
state = self._load_state()
|
||||
char_states = state.get("character_states", {})
|
||||
|
||||
if char_id in char_states:
|
||||
current_state = char_states[char_id]
|
||||
|
||||
# 检查是否有手动标记的待同步字段
|
||||
pending_sync = current_state.get("_pending_sync", [])
|
||||
for sync_item in pending_sync:
|
||||
if isinstance(sync_item, dict):
|
||||
dirty_flags.append({
|
||||
"field": sync_item.get("field"),
|
||||
"new_value": sync_item.get("value"),
|
||||
"reason": sync_item.get("reason", "Manual sync required"),
|
||||
"source": "state_json"
|
||||
})
|
||||
|
||||
return dirty_flags
|
||||
|
||||
def _compute_catchup_state(
|
||||
self,
|
||||
char_id: str,
|
||||
current_state: Dict[str, Any],
|
||||
time_elapsed: Dict[str, Any],
|
||||
dirty_flags: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
计算追赶后的角色状态。
|
||||
|
||||
规则:
|
||||
- 境界:根据时间推算可能的修炼进展
|
||||
- 位置:如果角色有固定活动区域,可能迁移
|
||||
- 关系:可能发展新的关系
|
||||
"""
|
||||
updated = current_state.copy()
|
||||
|
||||
# 应用脏标记(retcon 补丁)
|
||||
for flag in dirty_flags:
|
||||
field = flag.get("field")
|
||||
new_value = flag.get("new_value")
|
||||
if field and new_value:
|
||||
updated[field] = new_value
|
||||
|
||||
# 时间推进可能导致的变化(简单推算)
|
||||
# 实际应用中需要更复杂的逻辑
|
||||
days = time_elapsed.get("days", 0)
|
||||
|
||||
if days > 365: # 超过一年
|
||||
# 可能有境界提升
|
||||
if "realm" in updated:
|
||||
# 这里应该根据修炼体系计算
|
||||
pass
|
||||
|
||||
return updated
|
||||
|
||||
def _check_timeline_consistency(
|
||||
self,
|
||||
char_id: str,
|
||||
time_elapsed: Dict[str, Any],
|
||||
state: Dict[str, Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
检查时间线一致性。
|
||||
|
||||
返回 None 表示一致,否则返回问题描述。
|
||||
"""
|
||||
# 检查角色是否在不可能的时间出现在某地
|
||||
# 这是一个占位实现
|
||||
return None
|
||||
|
||||
def save_results(self, results: CatchupResult) -> None:
|
||||
"""保存结果到 state.json 和 ledger"""
|
||||
state = self._load_state()
|
||||
|
||||
# 更新 character_states
|
||||
for update in results.state_updates:
|
||||
char_id = update["character_id"]
|
||||
state.setdefault("character_states", {})[char_id] = update["state_after"]
|
||||
|
||||
# 记录 catchup 事件
|
||||
state.setdefault("catchup_events", []).append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"results": asdict(results)
|
||||
})
|
||||
|
||||
# 写回 state.json
|
||||
with open(self.state_file, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI 入口"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="NovelMaster Catchup Agent")
|
||||
parser.add_argument(
|
||||
"--project-root",
|
||||
required=True,
|
||||
help="项目根目录"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--current-chapter",
|
||||
type=int,
|
||||
required=True,
|
||||
help="当前章节编号"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--awakened-chars",
|
||||
nargs="*",
|
||||
help="被唤醒的角色 ID 列表(可选)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--storage-path",
|
||||
default=".noma/novel_data/",
|
||||
help="存储路径(默认: .noma/novel_data/)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = CatchupAgent(args.project_root, args.storage_path)
|
||||
results = agent.execute(args.current_chapter, args.awakened_chars)
|
||||
|
||||
# 输出 JSON 结果
|
||||
print(json.dumps(asdict(results), ensure_ascii=False, indent=2))
|
||||
|
||||
# 保存结果
|
||||
agent.save_results(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Agents Checkers
|
||||
|
||||
审查器模块,包括 tension_checker 等。
|
||||
"""
|
||||
|
||||
from .tension_checker import (
|
||||
TensionChecker,
|
||||
TensionChecker,
|
||||
CatharsisModel,
|
||||
TensionLevel,
|
||||
TensionReading,
|
||||
TensionIssue,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TensionChecker",
|
||||
"CatharsisModel",
|
||||
"TensionLevel",
|
||||
"TensionReading",
|
||||
"TensionIssue",
|
||||
]
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: consistency-checker
|
||||
description: 设定一致性检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# consistency-checker (设定一致性检查器)
|
||||
|
||||
> **职责**: 设定守卫者,执行第二防幻觉定律(设定即物理)。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 设定违规、战力冲突、逻辑不一致的结构化报告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载参考资料
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `{project_root}/..noma/novel_data/state.json`(主角当前状态)
|
||||
3. `设定集/`(世界观圣经)
|
||||
4. `大纲/`(对照上下文)
|
||||
|
||||
### 第二步: 三层一致性检查
|
||||
|
||||
#### 第一层: 战力一致性(战力检查)
|
||||
|
||||
**校验项**:
|
||||
- Protagonist's current realm/level matches state.json
|
||||
- Abilities used are within realm limitations
|
||||
- Power-ups follow established progression rules
|
||||
|
||||
**危险信号** (POWER_CONFLICT):
|
||||
```
|
||||
❌ 主角筑基3层使用金丹期才能掌握的"破空斩"
|
||||
→ Realm: 筑基3 | Ability: 破空斩 (requires 金丹期)
|
||||
→ VIOLATION: Premature ability access
|
||||
|
||||
❌ 上章境界淬体9层,本章突然变成凝气5层(无突破描写)
|
||||
→ Previous: 淬体9 | Current: 凝气5 | Missing: Breakthrough scene
|
||||
→ VIOLATION: Unexplained power jump
|
||||
```
|
||||
|
||||
**校验依据**:
|
||||
- state.json: `protagonist_state.power.realm`, `protagonist_state.power.layer`
|
||||
- 设定集/修炼体系.md: Realm ability restrictions
|
||||
|
||||
#### 第二层: 地点/角色一致性(地点/角色检查)
|
||||
|
||||
**校验项**:
|
||||
- Current location matches state.json or has valid travel sequence
|
||||
- Characters appearing are established in 设定集/ or tagged with `<entity/>`
|
||||
- Character attributes (appearance, personality, affiliations) match records
|
||||
|
||||
**危险信号** (LOCATION_ERROR / CHARACTER_CONFLICT):
|
||||
```
|
||||
❌ 上章在"天云宗",本章突然出现在"千里外的血煞秘境"(无移动描写)
|
||||
→ Previous location: 天云宗 | Current: 血煞秘境 | Distance: 1000+ li
|
||||
→ VIOLATION: Teleportation without explanation
|
||||
|
||||
❌ 李雪上次是"筑基期修为",本章变成"练气期"(无解释)
|
||||
→ Character: 李雪 | Previous: 筑基期 | Current: 练气期
|
||||
→ VIOLATION: Power regression unexplained
|
||||
```
|
||||
|
||||
**校验依据**:
|
||||
- state.json: `protagonist_state.location.current`
|
||||
- 设定集/角色卡/: Character profiles
|
||||
|
||||
#### 第三层: 时间线一致性(时间线检查)
|
||||
|
||||
**校验项**:
|
||||
- Event sequence is chronologically logical
|
||||
- Time-sensitive elements (deadlines, age, seasonal events) align
|
||||
- Flashbacks are clearly marked
|
||||
- Chapter time anchors match volume timeline
|
||||
|
||||
**Severity Classification** (时间问题分级):
|
||||
| 问题类型 | Severity | 说明 |
|
||||
|---------|----------|------|
|
||||
| 倒计时算术错误 | **critical** | D-5 直接跳到 D-2,必须修复 |
|
||||
| 事件先后矛盾 | **high** | 先发生的事情后写,逻辑混乱 |
|
||||
| 年龄/修炼时长冲突 | **high** | 算术错误,如15岁修炼5年却10岁入门 |
|
||||
| 时间回跳无标注 | **high** | 非闪回章节却出现时间倒退 |
|
||||
| 大跨度无过渡 | **high** | 跨度>3天却无过渡说明 |
|
||||
| 时间锚点缺失 | **medium** | 无法确定章节时间,但不影响逻辑 |
|
||||
| 轻微时间模糊 | **low** | 时段不明确但不影响剧情 |
|
||||
|
||||
> 输出 JSON 时,`issues[].severity` 必须使用小写枚举:`critical|high|medium|low`。
|
||||
|
||||
**危险信号** (TIMELINE_ISSUE):
|
||||
```
|
||||
❌ [critical] 第10章物资耗尽倒计时 D-5,第11章直接变成 D-2(跳过3天)
|
||||
→ Setup: D-5 | Next chapter: D-2 | Missing: 3 days
|
||||
→ VIOLATION: Countdown arithmetic error (MUST FIX)
|
||||
|
||||
❌ [high] 第10章提到"三天后的宗门大比",第11章描述大比结束(中间无时间流逝)
|
||||
→ Setup: 3 days until event | Next chapter: Event concluded
|
||||
→ VIOLATION: Missing time passage
|
||||
|
||||
❌ [high] 主角15岁修炼5年,推算应该10岁开始,但设定集记录"12岁入门"
|
||||
→ Age: 15 | Cultivation years: 5 | Start age: 10 | Record: 12
|
||||
→ VIOLATION: Timeline arithmetic error
|
||||
|
||||
❌ [high] 第一章末世降临,第二章就建立帮派(无时间过渡)
|
||||
→ Chapter 1: 末世第1天 | Chapter 2: 建帮派火拼
|
||||
→ VIOLATION: Major event without reasonable time progression
|
||||
|
||||
❌ [high] 本章时间锚点"末世第3天",上章是"末世第5天"(时间回跳)
|
||||
→ Previous: 末世第5天 | Current: 末世第3天
|
||||
→ VIOLATION: Time regression without flashback marker
|
||||
```
|
||||
|
||||
### 第三步: 实体一致性检查
|
||||
|
||||
**对所有章节中检测到的新实体**:
|
||||
1. Check if they contradict existing settings
|
||||
2. Assess if their introduction is consistent with world-building
|
||||
3. Verify power levels are reasonable for the current arc
|
||||
|
||||
**报告不一致的新增实体**:
|
||||
```
|
||||
⚠️ 发现设定冲突:
|
||||
- 第46章出现"紫霄宗",与设定集中势力分布矛盾
|
||||
→ 建议: 确认是否为新势力或笔误
|
||||
```
|
||||
|
||||
### 第四步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 设定一致性检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 战力一致性
|
||||
| 章节 | 问题 | 严重度 | 详情 |
|
||||
|------|------|--------|------|
|
||||
| {N} | ✓ 无违规 | - | - |
|
||||
| {M} | ✗ POWER_CONFLICT | high | 主角筑基3层使用金丹期技能"破空斩" |
|
||||
|
||||
**结论**: 发现 {X} 处违规
|
||||
|
||||
## 地点/角色一致性
|
||||
| 章节 | 类型 | 问题 | 严重度 |
|
||||
|------|------|------|--------|
|
||||
| {M} | 地点 | ✗ LOCATION_ERROR | medium | 未描述移动过程,从天云宗跳跃到血煞秘境 |
|
||||
|
||||
**结论**: 发现 {Y} 处违规
|
||||
|
||||
## 时间线一致性
|
||||
| 章节 | 问题 | 严重度 | 详情 |
|
||||
|------|------|--------|------|
|
||||
| {M} | ✗ TIMELINE_ISSUE | critical | 倒计时从 D-5 跳到 D-2 |
|
||||
| {M} | ✗ TIMELINE_ISSUE | high | 大比倒计时逻辑不一致 |
|
||||
|
||||
**结论**: 发现 {Z} 处违规
|
||||
**严重时间线问题**: {count} 个(必须修复后才能继续)
|
||||
|
||||
## 新实体一致性检查
|
||||
- ✓ 与世界观一致的新实体: {count}
|
||||
- ⚠️ 不一致的实体: {count}(详见下方列表)
|
||||
- ❌ 矛盾实体: {count}
|
||||
|
||||
**不一致列表**:
|
||||
1. 第{M}章:"紫霄宗"(势力)- 与现有势力分布矛盾
|
||||
2. 第{M}章:"天雷果"(物品)- 效果与力量体系不符
|
||||
|
||||
## 修复建议
|
||||
- [战力冲突] 润色时修改第{M}章,将"破空斩"替换为筑基期可用技能
|
||||
- [地点错误] 润色时补充移动过程描述或调整地点设定
|
||||
- [时间线问题] 润色时统一时间线推算,修正矛盾
|
||||
- [实体冲突] 润色时确认是否为新设定或需要调整
|
||||
|
||||
## 综合评分
|
||||
**结论**: {通过/未通过} - {简要说明}
|
||||
**严重违规**: {count}(必须修复)
|
||||
**轻微问题**: {count}(建议修复)
|
||||
```
|
||||
|
||||
### 第五步: 标记无效事实(新增)
|
||||
|
||||
对于发现的严重级别(`critical`)问题,自动标记到 `invalid_facts`(状态为 `pending`):
|
||||
|
||||
```bash
|
||||
python -X utf8 "${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts/noma.py" --project-root "{PROJECT_ROOT}" index mark-invalid \
|
||||
--source-type entity \
|
||||
--source-id {entity_id} \
|
||||
--reason "{问题描述}" \
|
||||
--marked-by consistency-checker \
|
||||
--chapter {current_chapter}
|
||||
```
|
||||
|
||||
> 注意:自动标记仅为 `pending`,需用户确认后才生效。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在 POWER_CONFLICT(战力崩坏)的章节
|
||||
❌ 忽略未标记的新实体
|
||||
❌ 接受无世界观解释的瞬移
|
||||
❌ **降低 TIMELINE_ISSUE 严重度**(时间问题不得降级)
|
||||
❌ **通过存在严重/高优先级时间线问题的章节**(必须修复)
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 0 个严重违规(战力冲突、无解释的角色变化、**时间线算术错误**)
|
||||
- 0 个高优先级时间线问题(**倒计时错误、时间回跳、重大事件无时间推进**)
|
||||
- 所有新实体与现有世界观一致
|
||||
- 地点和时间线过渡合乎逻辑
|
||||
- 报告为润色步骤提供具体修复建议
|
||||
@@ -0,0 +1,455 @@
|
||||
"""
|
||||
Consistency Checker (设定一致性检查器)
|
||||
|
||||
检查设定一致性:
|
||||
- 战力等级一致性
|
||||
- 地点/角色一致性
|
||||
- 时间线一致性
|
||||
|
||||
输出结构化报告。
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class IssueSeverity(Enum):
|
||||
"""问题严重度"""
|
||||
CRITICAL = "critical" # 必须修复
|
||||
HIGH = "high" # 强烈建议修复
|
||||
MEDIUM = "medium" # 建议修复
|
||||
LOW = "low" # 可忽略
|
||||
|
||||
|
||||
class IssueType(Enum):
|
||||
"""问题类型"""
|
||||
POWER_CONFLICT = "power_conflict" # 战力冲突
|
||||
LOCATION_ERROR = "location_error" # 地点错误
|
||||
CHARACTER_CONFLICT = "character_conflict" # 角色状态冲突
|
||||
TIMELINE_ISSUE = "timeline_issue" # 时间线问题
|
||||
NEW_ENTITY_CONFLICT = "new_entity_conflict" # 新实体与设定冲突
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsistencyIssue:
|
||||
"""一致性问题"""
|
||||
issue_type: IssueType
|
||||
chapter: int
|
||||
description: str
|
||||
severity: IssueSeverity
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsistencyReport:
|
||||
"""一致性检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
issues: List[ConsistencyIssue] = field(default_factory=list)
|
||||
power_issues_count: int = 0
|
||||
location_issues_count: int = 0
|
||||
timeline_issues_count: int = 0
|
||||
entity_issues_count: int = 0
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ConsistencyChecker:
|
||||
"""
|
||||
设定一致性检查器
|
||||
|
||||
检查范围:
|
||||
1. 战力一致性 - 境界/技能是否匹配
|
||||
2. 地点一致性 - 位置切换是否有过渡
|
||||
3. 时间线一致性 - 事件顺序/时间流逝
|
||||
4. 角色一致性 - 角色状态变化是否合理
|
||||
"""
|
||||
|
||||
# 境界关键词
|
||||
REALM_PATTERNS = [
|
||||
(r"凡人|淬体|炼气|筑基|金丹|元婴|化神|渡劫|大乘|真仙", "realm"),
|
||||
(r"一层|二层|三层|四层|五层|六层|七层|八层|九层|十层", "layer"),
|
||||
]
|
||||
|
||||
# 地点转移关键词
|
||||
LOCATION_TRANSITION_KEYWORDS = [
|
||||
"来到", "前往", "到达", "回到", "离开", "返回",
|
||||
"穿越", "瞬移", "飞行", "走了", "赶到"
|
||||
]
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.project_root = Path(project_root) if project_root else None
|
||||
self.state: Dict[str, Any] = {}
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
"""加载项目状态"""
|
||||
self.state = state
|
||||
|
||||
def load_settings(self, settings: Dict[str, Any]) -> None:
|
||||
"""加载设定集"""
|
||||
self.settings = settings
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_location: Optional[str] = None
|
||||
) -> ConsistencyReport:
|
||||
"""
|
||||
检查单章一致性
|
||||
|
||||
Args:
|
||||
chapter_num: 章节号
|
||||
chapter_text: 章节正文
|
||||
previous_location: 上一章位置
|
||||
|
||||
Returns:
|
||||
ConsistencyReport
|
||||
"""
|
||||
report = ConsistencyReport(start_chapter=chapter_num, end_chapter=chapter_num)
|
||||
|
||||
# 1. 战力检查
|
||||
power_issues = self._check_power_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(power_issues)
|
||||
report.power_issues_count = len(power_issues)
|
||||
|
||||
# 2. 地点检查
|
||||
location_issues = self._check_location_consistency(
|
||||
chapter_num, chapter_text, previous_location
|
||||
)
|
||||
report.issues.extend(location_issues)
|
||||
report.location_issues_count = len(location_issues)
|
||||
|
||||
# 3. 时间线检查
|
||||
timeline_issues = self._check_timeline_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(timeline_issues)
|
||||
report.timeline_issues_count = len(timeline_issues)
|
||||
|
||||
# 4. 新实体冲突检查
|
||||
entity_issues = self._check_new_entity_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(entity_issues)
|
||||
report.entity_issues_count = len(entity_issues)
|
||||
|
||||
# 综合判定
|
||||
critical_count = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
report.passed = critical_count == 0
|
||||
|
||||
# 生成摘要
|
||||
report.summary = self._generate_summary(report)
|
||||
|
||||
return report
|
||||
|
||||
def check_range(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int,
|
||||
chapters: Dict[int, str],
|
||||
locations: Dict[int, str]
|
||||
) -> ConsistencyReport:
|
||||
"""检查章节区间"""
|
||||
report = ConsistencyReport(start_chapter=start_chapter, end_chapter=end_chapter)
|
||||
|
||||
previous_location = None
|
||||
for ch in range(start_chapter, end_chapter + 1):
|
||||
if ch in chapters:
|
||||
chapter_text = chapters[ch]
|
||||
previous_loc = locations.get(ch - 1) if ch > start_chapter else None
|
||||
|
||||
ch_report = self.check_chapter(ch, chapter_text, previous_loc)
|
||||
report.issues.extend(ch_report.issues)
|
||||
report.power_issues_count += ch_report.power_issues_count
|
||||
report.location_issues_count += ch_report.location_issues_count
|
||||
report.timeline_issues_count += ch_report.timeline_issues_count
|
||||
report.entity_issues_count += ch_report.entity_issues_count
|
||||
|
||||
if ch in locations:
|
||||
previous_location = locations[ch]
|
||||
|
||||
critical_count = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
report.passed = critical_count == 0
|
||||
report.summary = self._generate_summary(report)
|
||||
|
||||
return report
|
||||
|
||||
def _check_power_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查战力一致性"""
|
||||
issues = []
|
||||
|
||||
# 从 state 获取当前境界
|
||||
current_realm = self.state.get("protagonist_state", {}).get("power", {}).get("realm", "")
|
||||
current_layer = self.state.get("protagonist_state", {}).get("power", {}).get("level", 0)
|
||||
|
||||
# 检测本章提到的境界
|
||||
detected_realms = self._extract_realms(text)
|
||||
|
||||
# 检查是否使用了超出境界的技能
|
||||
for realm_match in detected_realms:
|
||||
realm = realm_match["realm"]
|
||||
layer = realm_match.get("layer", 0)
|
||||
|
||||
# 境界跳跃检测(无突破描写)
|
||||
if current_realm and realm != current_realm:
|
||||
# 检查是否有过渡描写
|
||||
transition_keywords = ["突破", "晋升", "进阶", "升级", "渡劫", "闭关"]
|
||||
has_transition = any(kw in text for kw in transition_keywords)
|
||||
|
||||
if not has_transition:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.POWER_CONFLICT,
|
||||
chapter=chapter,
|
||||
description=f"境界从 {current_realm} 变化到 {realm},但无突破描写",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_realm": current_realm,
|
||||
"current_realm": realm,
|
||||
"has_transition": has_transition
|
||||
},
|
||||
suggested_fix="添加突破场景或修正境界描述"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_realms(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""提取文本中的境界信息"""
|
||||
realms = []
|
||||
|
||||
realm_names = [
|
||||
"凡人", "淬体", "炼气", "筑基", "金丹", "元婴",
|
||||
"化神", "渡劫", "大乘", "真仙"
|
||||
]
|
||||
|
||||
for realm in realm_names:
|
||||
if realm in text:
|
||||
# 尝试提取层次
|
||||
layer_match = re.search(rf'{realm}(\d+)层', text)
|
||||
layer = int(layer_match.group(1)) if layer_match else 0
|
||||
|
||||
realms.append({
|
||||
"realm": realm,
|
||||
"layer": layer,
|
||||
"position": text.index(realm)
|
||||
})
|
||||
|
||||
return realms
|
||||
|
||||
def _check_location_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_location: Optional[str]
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查地点一致性"""
|
||||
issues = []
|
||||
|
||||
# 检测本章地点
|
||||
current_location = self._extract_location(text)
|
||||
|
||||
if not previous_location:
|
||||
return issues
|
||||
|
||||
if not current_location:
|
||||
return issues
|
||||
|
||||
# 检查是否是同一地点
|
||||
if current_location != previous_location:
|
||||
# 检查是否有转移描写
|
||||
has_transition = any(kw in text for kw in self.LOCATION_TRANSITION_KEYWORDS)
|
||||
|
||||
# 检查是否在附近
|
||||
is_nearby = self._is_location_nearby(current_location, previous_location)
|
||||
|
||||
if not has_transition and not is_nearby:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.LOCATION_ERROR,
|
||||
chapter=chapter,
|
||||
description=f"从「{previous_location}」跳跃到「{current_location}」,无过渡描写",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_location": previous_location,
|
||||
"current_location": current_location,
|
||||
"has_transition": has_transition
|
||||
},
|
||||
suggested_fix="添加移动/传送过渡场景"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_location(self, text: str) -> Optional[str]:
|
||||
"""提取当前地点"""
|
||||
# 常见地点指示词
|
||||
patterns = [
|
||||
r"在(.+?)的",
|
||||
r"来到(.+?)的",
|
||||
r"位于(.+?)的",
|
||||
r"到了(.+?),",
|
||||
r"【(.+?)】",
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text)
|
||||
if match:
|
||||
location = match.group(1).strip()
|
||||
if location and len(location) < 20:
|
||||
return location
|
||||
|
||||
# 检查分割线后的地点
|
||||
lines = text.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('---'):
|
||||
continue
|
||||
if '【' in line and '】' in line:
|
||||
start = line.index('【')
|
||||
end = line.index('】')
|
||||
return line[start+1:end]
|
||||
|
||||
return None
|
||||
|
||||
def _is_location_nearby(self, loc1: str, loc2: str) -> bool:
|
||||
"""判断两地点是否相近"""
|
||||
# 简化实现:检查是否有相同关键词
|
||||
keywords1 = set(loc1)
|
||||
keywords2 = set(loc2)
|
||||
overlap = keywords1 & keywords2
|
||||
return len(overlap) >= 2
|
||||
|
||||
def _check_timeline_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查时间线一致性"""
|
||||
issues = []
|
||||
|
||||
# 检测时间描述
|
||||
time_patterns = [
|
||||
(r'第(\d+)天', 'day'),
|
||||
(r'(\d+)日后', 'future_day'),
|
||||
(r'(\d+)天后', 'future_day'),
|
||||
(r'(\d+)年前', 'past'),
|
||||
(r'D-(\d+)', 'countdown'),
|
||||
(r'倒计时(\d+)天', 'countdown'),
|
||||
]
|
||||
|
||||
detected_times = []
|
||||
for pattern, time_type in time_patterns:
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
value = int(match.group(1))
|
||||
detected_times.append({
|
||||
"type": time_type,
|
||||
"value": value,
|
||||
"position": match.start()
|
||||
})
|
||||
|
||||
# 检查时间回跳
|
||||
if len(detected_times) >= 2:
|
||||
for i in range(len(detected_times) - 1):
|
||||
t1 = detected_times[i]
|
||||
t2 = detected_times[i + 1]
|
||||
|
||||
# 检查 day 类型的时间回跳
|
||||
if t1["type"] == "day" and t2["type"] == "day":
|
||||
if t2["value"] < t1["value"]:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.TIMELINE_ISSUE,
|
||||
chapter=chapter,
|
||||
description=f"时间回跳:从第{t1['value']}天到第{t2['value']}天",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_time": t1["value"],
|
||||
"current_time": t2["value"]
|
||||
},
|
||||
suggested_fix="确认是否为闪回场景,或修正时间线"
|
||||
))
|
||||
|
||||
# 检查倒计时逻辑
|
||||
countdowns = [t for t in detected_times if t["type"] == "countdown"]
|
||||
if len(countdowns) >= 2:
|
||||
values = [t["value"] for t in countdowns]
|
||||
if values != sorted(values, reverse=True):
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.TIMELINE_ISSUE,
|
||||
chapter=chapter,
|
||||
description="倒计时数字递减错误",
|
||||
severity=IssueSeverity.CRITICAL,
|
||||
details={"countdowns": values},
|
||||
suggested_fix="修正倒计时数值"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_new_entity_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查新引入实体是否与设定冲突"""
|
||||
issues = []
|
||||
|
||||
# 从设定中获取已知势力
|
||||
known_factions = self.settings.get("factions", [])
|
||||
known_locations = self.settings.get("locations", [])
|
||||
|
||||
# 检测新出现的地点/势力
|
||||
new_mentions = re.findall(r'[""'']([^""'']{2,10})[""'']', text)
|
||||
|
||||
for mention in new_mentions:
|
||||
# 检查是否与已知势力冲突
|
||||
for faction in known_factions:
|
||||
if mention in faction.get("name", ""):
|
||||
# 检查是否有冲突描述
|
||||
conflict_keywords = ["对立", "敌对", "矛盾", "冲突"]
|
||||
if any(kw in text for kw in conflict_keywords):
|
||||
# 可能存在设定冲突
|
||||
pass # 简化处理
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_summary(self, report: ConsistencyReport) -> str:
|
||||
"""生成报告摘要"""
|
||||
total_issues = len(report.issues)
|
||||
critical = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
high = sum(1 for i in report.issues if i.severity == IssueSeverity.HIGH)
|
||||
|
||||
if report.passed:
|
||||
return f"检查通过。发现 {total_issues} 个问题({critical} 严重,{high} 高优先级)"
|
||||
else:
|
||||
return f"检查未通过。发现 {total_issues} 个问题({critical} 严重必须修复)"
|
||||
|
||||
def export_report(self, report: ConsistencyReport) -> Dict[str, Any]:
|
||||
"""导出报告为字典"""
|
||||
return {
|
||||
"start_chapter": report.start_chapter,
|
||||
"end_chapter": report.end_chapter,
|
||||
"passed": report.passed,
|
||||
"summary": report.summary,
|
||||
"issues_count": {
|
||||
"power": report.power_issues_count,
|
||||
"location": report.location_issues_count,
|
||||
"timeline": report.timeline_issues_count,
|
||||
"entity": report.entity_issues_count,
|
||||
"total": len(report.issues)
|
||||
},
|
||||
"issues": [
|
||||
{
|
||||
"type": i.issue_type.value,
|
||||
"chapter": i.chapter,
|
||||
"description": i.description,
|
||||
"severity": i.severity.value,
|
||||
"details": i.details,
|
||||
"suggested_fix": i.suggested_fix
|
||||
}
|
||||
for i in report.issues
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
---
|
||||
name: continuity-checker
|
||||
description: 连贯性检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# continuity-checker (连贯性检查器)
|
||||
|
||||
> **职责**: 叙事流守卫者,确保场景过渡顺畅、情节线连贯、逻辑一致。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 场景过渡、情节线、伏笔管理、逻辑流的连贯性分析。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载上下文
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. 前 2-3 章(过渡上下文)
|
||||
3. `大纲/`(对照大纲 - 大纲即法律)
|
||||
4. `{project_root}/..noma/novel_data/state.json`(情节线追踪器,若存在)
|
||||
|
||||
### 第二步: 四层连贯性检查
|
||||
|
||||
#### 第一层: 场景转换流畅度(场景转换)
|
||||
|
||||
**检查项**:
|
||||
```
|
||||
❌ Abrupt Transition:
|
||||
上一段:林天在天云宗大殿与长老对话
|
||||
下一段:林天已经在血煞秘境深处战斗
|
||||
问题:缺少移动过程/时间流逝描写
|
||||
|
||||
✓ Smooth Transition:
|
||||
上一段:林天告别长老,离开宗门
|
||||
过渡句:"三日后,林天抵达血煞秘境入口"
|
||||
下一段:林天在秘境中遭遇妖兽
|
||||
```
|
||||
|
||||
**过渡质量评级**:
|
||||
- **A**: 自然过渡 + 时间/空间标记清晰
|
||||
- **B**: 有过渡但略显生硬
|
||||
- **C**: 缺少过渡,靠读者推测
|
||||
- **F**: 完全断裂,逻辑跳跃
|
||||
|
||||
#### 第二层: 情节线连贯(情节线连贯)
|
||||
|
||||
**追踪活跃情节线**:
|
||||
- **Main Thread** (主线): 当前核心任务/目标
|
||||
- **Sub-threads** (支线): 次要任务、悬念、铺垫
|
||||
|
||||
**检查项**:
|
||||
- Threads introduced but never resolved (烂尾)
|
||||
- Threads resolved without proper setup (突兀)
|
||||
- Threads forgotten mid-story (遗忘)
|
||||
|
||||
**示例分析**:
|
||||
```
|
||||
第40章引入: "宗门大比将在10天后举行"(主线)
|
||||
第45章: 大比正在进行中 ✓
|
||||
第50章: 大比结束,主角获胜 ✓
|
||||
判定:✓ 线索完整,有始有终
|
||||
|
||||
vs.
|
||||
|
||||
第30章引入: "血煞门即将入侵"(支线伏笔)
|
||||
第31-50章: 完全未提及血煞门
|
||||
判定:⚠️ 线索悬空,可能遗忘或拖得太久
|
||||
```
|
||||
|
||||
#### 第三层: 伏笔管理(伏笔管理)
|
||||
|
||||
**伏笔分类**:
|
||||
| Type | Setup → Payoff Gap | Risk |
|
||||
|------|-------------------|------|
|
||||
| **Short-term** (短期) | 1-3 章 | Low |
|
||||
| **Mid-term** (中期) | 4-10 章 | Medium (容易被遗忘) |
|
||||
| **Long-term** (长期) | 10+ 章 | High (需明确标记) |
|
||||
|
||||
**危险信号**:
|
||||
第10章: "林天发现神秘玉佩,似乎隐藏秘密"
|
||||
第11-30章: 玉佩再未提及
|
||||
判定:⚠️ 伏笔遗忘风险,建议第31章回收或再次提及
|
||||
|
||||
✓ Proper Payoff:
|
||||
第10章: "李雪提到师父曾去过血煞秘境"
|
||||
第25章: "在秘境中发现李雪师父留下的线索"
|
||||
判定:✓ 伏笔回收合理,间隔15章属于中期伏笔
|
||||
```
|
||||
|
||||
**伏笔检查清单**:
|
||||
- [ ] 所有设置的伏笔是否在合理章节内回收?
|
||||
- [ ] 长期伏笔(10+章)是否定期提及以保持读者记忆?
|
||||
- [ ] 回收时是否自然,不生硬?
|
||||
|
||||
#### 第四层: 逻辑流畅性(逻辑流畅性)
|
||||
|
||||
**检查情节漏洞与逻辑不一致**:
|
||||
|
||||
```
|
||||
❌ Logic Hole:
|
||||
第45章: 主角说"我从未见过这种妖兽"
|
||||
第30章: 主角曾击败同种妖兽
|
||||
判定:❌ 前后矛盾,需修正
|
||||
|
||||
❌ Causality Break:
|
||||
第46章: 主角突然获得神秘力量
|
||||
问题: 无解释来源,违反"发明需申报"原则
|
||||
判定:❌ 缺少因果关系,需补充 `<entity/>` 或铺垫
|
||||
|
||||
✓ Logical:
|
||||
第44章: 主角服用聚气丹(铺垫)
|
||||
第45章: 主角突破境界(因果)
|
||||
判定:✓ 因果清晰
|
||||
```
|
||||
|
||||
### 第三步: 大纲一致性检查(大纲即法律)
|
||||
|
||||
**将章节与大纲对照**:
|
||||
|
||||
```
|
||||
大纲第45章: "主角参加宗门大比,对战王少,险胜"
|
||||
|
||||
实际第45章内容:
|
||||
- ✓ 主角参加大比
|
||||
- ✓ 对战王少
|
||||
- ✗ 结果是"轻松碾压"而非"险胜"
|
||||
|
||||
判定:⚠️ 偏离大纲(难度降低),需确认是否有意调整
|
||||
```
|
||||
|
||||
**偏差处理**:
|
||||
- **轻微**(细节优化): 可接受
|
||||
- **中等**(情节调整): 需标记并确认
|
||||
- **重大**(核心冲突变化): 必须标记 `<deviation reason="..."/>` 并说明
|
||||
|
||||
### 第四步: 拖沓检查(拖沓检查)
|
||||
|
||||
**识别拖沓段落**:
|
||||
```
|
||||
⚠️ Possible Drag:
|
||||
第45-46章: 两章都在描述"主角赶路"
|
||||
内容: 重复的风景描写,无关键事件
|
||||
判定:⚠️ 节奏拖沓,建议:
|
||||
- 压缩为1章
|
||||
- 或在赶路途中安排事件(遭遇/奇遇/思考)
|
||||
|
||||
✓ Efficient Pacing:
|
||||
第47章: "三日后,主角抵达秘境"(一句带过)
|
||||
判定:✓ 有效省略无关紧要的过程
|
||||
```
|
||||
|
||||
### 第五步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 连贯性检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 场景转换评分
|
||||
| 转换 | 从 → 到 | 评级 | 问题 |
|
||||
|------|---------|------|------|
|
||||
| 第{N}章→第{M}章 | 天云宗大殿 → 血煞秘境 | C | 缺少移动过程描写 |
|
||||
|
||||
**场景转换总评**: {平均评级}
|
||||
|
||||
## 情节线追踪
|
||||
| 情节线 | 引入 | 最近提及 | 状态 | 下一步 |
|
||||
|--------|------|---------|------|--------|
|
||||
| 宗门大比 | 第40章 | 第46章(结束)| ✓ 已解决 | - |
|
||||
| 血煞门入侵 | 第30章 | 第30章 | ⚠️ 休眠(16章未提及)| 建议第47章提及或回收 |
|
||||
| 神秘玉佩 | 第10章 | 第10章 | ⚠️ 遗忘(36章未提及)| 建议回收或删除伏笔 |
|
||||
|
||||
**活跃情节线**: {count}
|
||||
**休眠/遗忘**: {count}
|
||||
|
||||
## 伏笔管理
|
||||
| 设置 | 章节 | 类型 | 兑现 | 间隔 | 状态 |
|
||||
|------|------|------|------|------|------|
|
||||
| 李雪师父去过秘境 | 10 | 中期 | 第25章发现线索 | 15章 | ✓ 已回收 |
|
||||
| 神秘玉佩 | 10 | 长期 | 未回收 | 36章+ | ❌ 遗忘风险 |
|
||||
|
||||
**伏笔健康度**: {X} 已回收, {Y} 待处理, {Z} 有风险
|
||||
|
||||
## 逻辑一致性
|
||||
| 章节 | 问题 | 类型 | 严重度 |
|
||||
|------|------|------|--------|
|
||||
| {M} | 前后矛盾(主角称"从未见过"但第30章遇见过)| 前后矛盾 | high |
|
||||
| {M} | 突然获得力量无解释 | 因果缺失 | medium |
|
||||
|
||||
**发现逻辑漏洞**: {count}
|
||||
|
||||
## 大纲一致性
|
||||
| 章节 | 大纲 | 实际 | 偏差程度 |
|
||||
|------|------|------|---------|
|
||||
| {M} | 险胜王少 | 轻松碾压 | ⚠️ 中等(难度调整)|
|
||||
|
||||
**偏差数**: {count}({X} 轻微, {Y} 中等, {Z} 重大)
|
||||
|
||||
## 节奏拖沓检查
|
||||
- ⚠️ 第{N}-{M}章: 两章赶路场景重复,建议压缩或增加事件
|
||||
|
||||
## 修复建议
|
||||
1. **修复场景转换**: 第{M}章添加"三日后"等时间标记
|
||||
2. **回收遗忘伏笔**: 神秘玉佩已36章未提及,建议回收或回溯删除
|
||||
3. **解决逻辑矛盾**: 第{M}章修改"从未见过"为"很少见到"
|
||||
4. **提及休眠线索**: 血煞门入侵线索建议第47章再次提及
|
||||
5. **压缩拖沓段落**: 第{N}-{M}章赶路场景合并为1章
|
||||
|
||||
## 综合评分
|
||||
**连贯性总评**: {流畅/可接受/生硬/断裂}
|
||||
**严重问题**: {count}(必须修复)
|
||||
**改进建议**: {count}(建议改进)
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在重大大纲偏差且无 `<deviation/>` 标记的章节
|
||||
❌ 忽略遗忘伏笔(10+ 章休眠)
|
||||
❌ 接受突兀的场景转换(F 级)
|
||||
❌ 忽视情节漏洞和前后矛盾
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 所有场景转换评级 ≥ B
|
||||
- 无活跃情节线遗忘超过 15 章
|
||||
- 所有长期伏笔已追踪并有兑现计划
|
||||
- 0 个重大逻辑漏洞
|
||||
- 大纲偏差已正确标记
|
||||
- 报告指出需修复的具体章节
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Continuity Checker (连续性检查器)
|
||||
|
||||
检查场景与叙事连贯性:
|
||||
- 场景切换是否平滑
|
||||
- 视角是否一致
|
||||
- 叙事节奏是否连贯
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityIssue:
|
||||
"""连续性问题"""
|
||||
chapter: int
|
||||
issue_type: str # scene_transition / perspective_shift / info_gap
|
||||
description: str
|
||||
severity: str # critical/high/medium/low
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityReport:
|
||||
"""连续性检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
issues: List[ContinuityIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ContinuityChecker:
|
||||
"""
|
||||
场景与叙事连续性检查器
|
||||
|
||||
检查:
|
||||
1. 场景切换是否平滑
|
||||
2. 视角是否一致
|
||||
3. 信息是否连贯
|
||||
"""
|
||||
|
||||
# 场景切换关键词
|
||||
SCENE_TRANSITION_KEYWORDS = [
|
||||
"与此同时", "镜头一转", "画面切换", "时间流逝",
|
||||
"与此同时", "另一边", "镜头回到", "视角转向"
|
||||
]
|
||||
|
||||
# 视角关键词
|
||||
PERSPECTIVE_PATTERNS = [
|
||||
(r"他(她)看", "third_person"),
|
||||
(r"我(你)", "first_person"),
|
||||
(r"叶尘(主角名)", "main_character"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.current_perspective = "third_person"
|
||||
self.previous_scene_ending = ""
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_ending: Optional[str] = None
|
||||
) -> ContinuityReport:
|
||||
"""检查单章连续性"""
|
||||
report = ContinuityReport(start_chapter=chapter_num, end_chapter=chapter_num)
|
||||
|
||||
# 1. 检查场景切换平滑度
|
||||
transition_issues = self._check_scene_transitions(chapter_num, chapter_text, previous_ending)
|
||||
report.issues.extend(transition_issues)
|
||||
|
||||
# 2. 检查视角一致性
|
||||
perspective_issues = self._check_perspective_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(perspective_issues)
|
||||
|
||||
# 3. 检查信息缺口
|
||||
info_gap_issues = self._check_info_gaps(chapter_num, chapter_text, previous_ending)
|
||||
report.issues.extend(info_gap_issues)
|
||||
|
||||
# 综合判定
|
||||
critical_count = sum(1 for i in report.issues if i.severity == "critical")
|
||||
report.passed = critical_count == 0
|
||||
report.summary = f"发现 {len(report.issues)} 个连续性问题"
|
||||
|
||||
return report
|
||||
|
||||
def _check_scene_transitions(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_ending: Optional[str]
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查场景切换"""
|
||||
issues = []
|
||||
|
||||
# 检查分割线后的场景切换
|
||||
scenes = text.split('---')
|
||||
if len(scenes) > 2: # 多场景
|
||||
# 检查开头是否有场景说明
|
||||
first_scene = scenes[1] if len(scenes) > 1 else ""
|
||||
if first_scene.strip() and not any(kw in first_scene[:50] for kw in ["此时", "这里", "地点"]):
|
||||
issues.append(ContinuityIssue(
|
||||
chapter=chapter,
|
||||
issue_type="scene_transition",
|
||||
description="场景切换无过渡说明",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_perspective_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查视角一致性"""
|
||||
issues = []
|
||||
|
||||
# 简化:只检测是否有明显的视角混乱
|
||||
# 实际实现需要更复杂的 NLP
|
||||
|
||||
return issues
|
||||
|
||||
def _check_info_gaps(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_ending: Optional[str]
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查信息缺口"""
|
||||
issues = []
|
||||
|
||||
# 检查是否承接上文
|
||||
if previous_ending:
|
||||
# 简化:检查是否有承接关键词
|
||||
carry_over_keywords = ["继续", "接着", "与此同时", "然而", "但是"]
|
||||
has_carry_over = any(kw in text[:100] for kw in carry_over_keywords)
|
||||
|
||||
if not has_carry_over and len(text) > 500:
|
||||
# 可能存在信息缺口
|
||||
pass # 简化处理
|
||||
|
||||
return issues
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: high-point-checker
|
||||
description: 爽点密度检查,支持迪化误解/身份掉马模式,输出结构化报告
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# high-point-checker (爽点检查器)
|
||||
|
||||
> **职责**: 读者满足感机制的质量保障专家(爽点设计)。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **分类法**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **题材画像**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 爽点密度、类型覆盖、执行质量的结构化报告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载目标章节
|
||||
|
||||
读取指定范围内 `正文/` 目录下的所有章节。
|
||||
|
||||
### 第二步: 识别爽点
|
||||
|
||||
扫描 **8 种标准执行模式**:
|
||||
|
||||
| 模式 | 特征关键词 | 最低要求 |
|
||||
|------|-----------------|---------------------|
|
||||
| **装逼打脸** | 嘲讽/废物/不屑 → 反转/震惊/目瞪口呆 | 铺垫 + 反转 + 反应 |
|
||||
| **扮猪吃虎** | 示弱/隐藏实力 → 碾压 | 隐藏 + 轻视 + 碾压 |
|
||||
| **越级反杀** | 实力差距 → 以弱胜强 → 震撼 | 展示差距 + 策略/爆发 + 反转 |
|
||||
| **打脸权威** | 权威/前辈/强者 → 主角挑战成功 | 建立权威 + 挑战 + 成功 |
|
||||
| **反派翻车** | 反派得意/阴谋 → 计划失败/被反杀 | 反派铺垫 + 主角反制 + 翻车 |
|
||||
| **甜蜜超预期** | 期待/心动 → 超预期表现 → 情感升华 | 期待 + 超越期待 + 情绪 |
|
||||
| **迪化误解** | 主角随意行为 → 配角脑补升华 → 读者优越感 | 随意行为 + 信息差 + 误解 + 读者优越 |
|
||||
| **身份掉马** | 身份伪装 → 关键时刻揭露 → 周围震惊 | 隐藏(长期)+ 触发事件 + 揭露 + 群体反应 |
|
||||
|
||||
### 第二步补充: 迪化误解模式检测
|
||||
|
||||
**核心结构**:
|
||||
1. 主角随意行为(无心插柳)
|
||||
2. 配角信息差(不知道主角真实情况)
|
||||
3. 配角脑补升华(合理化主角行为)
|
||||
4. 读者优越感(我知道真相)
|
||||
|
||||
**识别信号**:
|
||||
- "竟然"/"难道"/"莫非" + 配角内心戏
|
||||
- 主角行为与配角解读的反差
|
||||
- 读者视角知道真相
|
||||
|
||||
**质量评估**:
|
||||
- A级:脑补合理,读者优越感强
|
||||
- B级:脑补尚可,效果一般
|
||||
- C级:脑补太刻意,配角显得蠢
|
||||
|
||||
### 第二步补充: 身份掉马模式检测
|
||||
|
||||
**核心结构**:
|
||||
1. 身份伪装(需长期铺垫)
|
||||
2. 关键时刻(危机/高光)
|
||||
3. 身份揭露(意外或主动)
|
||||
4. 周围反应(震惊/后悔/敬畏)
|
||||
|
||||
**识别信号**:
|
||||
- 身份相关词汇(真实身份/原来是/竟然是)
|
||||
- 周围角色大规模反应
|
||||
- 前后反差描写
|
||||
|
||||
**质量评估**:
|
||||
- A级:有长期铺垫,反应层次丰富
|
||||
- B级:有铺垫,反应单一
|
||||
- C级:无铺垫,突兀
|
||||
- F级:硬编身份,逻辑矛盾
|
||||
|
||||
### 第三步: 密度检查
|
||||
|
||||
**推荐基线(滚动窗口)**:
|
||||
- **Per chapter**: 优先有爽点或同等兑现;允许过渡章低密度
|
||||
- **Every 5 chapters**: 建议 ≥ 1 组合爽点(2种模式叠加)
|
||||
- **Every 10-15 chapters**: 建议 ≥ 1 里程碑爽点(改变主角地位)
|
||||
|
||||
**输出**:
|
||||
```
|
||||
第 X 章: [✓ 2 个爽点] 或 [△ 0 个爽点 - 连续出现时需预警]
|
||||
```
|
||||
|
||||
### 第四步: 类型多样性检查
|
||||
|
||||
**反单调要求**: 审查范围内单一类型不得超过 80%。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
Chapters 1-2:
|
||||
- 装逼打脸: 3 (75%) ✓
|
||||
- 越级反杀: 1 (25%)
|
||||
Mode diversity: Acceptable
|
||||
```
|
||||
|
||||
vs.
|
||||
|
||||
```
|
||||
Chapters 45-46:
|
||||
- 装逼打脸: 7 (87.5%) ✗ OVER-RELIANCE
|
||||
- 扮猪吃虎: 1 (12.5%)
|
||||
Mode diversity: Warning - Monotonous pacing
|
||||
```
|
||||
|
||||
### 第五步: 执行质量评估
|
||||
|
||||
对每个已识别的爽点,检查:
|
||||
|
||||
1. **铺垫充分性**: 是否有充分的前期铺垫(至少1-2章)?
|
||||
2. **反转冲击**: 转折是否出人意料又合乎逻辑?
|
||||
3. **情绪回报**: 是否实现了读者情绪释放?
|
||||
4. **30/40/30 参考结构**: 结构是否清晰(不要求严格比例)?
|
||||
- 30% 铺垫蓄势
|
||||
- 40% 兑现执行
|
||||
- 30% 微反转/余波
|
||||
5. **压扬比例**: 是否匹配题材?
|
||||
- 传统爽文: 压3扬7
|
||||
- 硬核正剧: 压5扬5
|
||||
- 虐恋文: 压7扬3
|
||||
|
||||
**质量评级**:
|
||||
- **A(优秀)**: 所有标准达标,执行有力,结构清晰
|
||||
- **B(良好)**: 多数标准达标,可能有轻微比例问题
|
||||
- **C(及格)**: 基本标准达标但结构偏弱
|
||||
- **F(失败)**: 爽点缺少铺垫突然出现,或逻辑不一致
|
||||
|
||||
### 第六步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 爽点检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 密度检查
|
||||
- 第 {N} 章: ✓ 2 个爽点(装逼打脸 + 越级反杀)
|
||||
- 第 {M} 章: △ 0 个爽点 **[预警 - 连续出现时需补强]**
|
||||
|
||||
**结论**: {通过/预警/未通过}(基于滚动窗口)
|
||||
|
||||
## 类型分布
|
||||
- 装逼打脸: {count}({percent}%)
|
||||
- 扮猪吃虎: {count}({percent}%)
|
||||
- 越级反杀: {count}({percent}%)
|
||||
- 打脸权威: {count}({percent}%)
|
||||
- 反派翻车: {count}({percent}%)
|
||||
- 甜蜜超预期: {count}({percent}%)
|
||||
|
||||
**结论**: {通过/预警}(单一类型 > 80% 时有单调风险)
|
||||
|
||||
## 质量评级
|
||||
| 章节 | 爽点 | 模式 | 评级 | 30/40/30 | 压扬比 | 问题 |
|
||||
|------|------|------|------|---------|--------|------|
|
||||
| {N} | 主角被嘲讽后一招秒杀对手 | 装逼打脸 | A | ✓ | 压3扬7 | - |
|
||||
| {M} | 突然顿悟突破境界 | 越级反杀 | C | ✗ | 压1扬9 | 缺少铺垫,压扬比失衡 |
|
||||
|
||||
**结论**: 平均评级 = {X}
|
||||
|
||||
## 修复建议
|
||||
- [密度预警] 第 {M} 章低密度,建议补{mode}型爽点或同等兑现
|
||||
- [单调风险] 过度依赖{mode}型,建议增加{other_modes}
|
||||
- [质量问题] 第 {M} 章的爽点执行不足,需要补充{missing_element}
|
||||
- [结构偏弱] 爽点结构偏弱,建议补铺垫/兑现/余波中的缺项
|
||||
- [压扬比问题] 压扬比例不符合{genre}类型,建议调整为{ratio}
|
||||
|
||||
## 综合评分
|
||||
**结论**: {通过/未通过} - {简要说明}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 忽略连续低密度章节且不预警
|
||||
❌ 忽略缺乏铺垫的突发爽点
|
||||
❌ 通过连续 5+ 章同类型爽点
|
||||
❌ 迪化误解中配角智商明显下线
|
||||
❌ 身份掉马无任何前期暗示
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 滚动窗口密度保持健康(不连续低密度)
|
||||
- 类型分布显示多样性(单一类型不超过 80%)
|
||||
- 平均质量评级 ≥ B
|
||||
- 迪化误解的脑补需合理
|
||||
- 身份掉马需有铺垫
|
||||
- 报告包含可执行的修复建议
|
||||
|
||||
## 输出格式增强
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "high-point-checker",
|
||||
"chapter": 45,
|
||||
"overall_score": 86,
|
||||
"pass": true,
|
||||
"issues": [],
|
||||
"metrics": {
|
||||
"cool_point_count": 2,
|
||||
"cool_point_types": ["迪化误解", "身份掉马"],
|
||||
"density_score": 8,
|
||||
"type_diversity": 0.9,
|
||||
"milestone_present": false,
|
||||
"monotony_risk": false
|
||||
},
|
||||
"summary": "爽点密度达标,类型分布健康,执行质量稳定。"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
High-Point Checker (爽点检查器)
|
||||
|
||||
检查爽点密度与质量:
|
||||
- 爽点出现频率
|
||||
- 爽点强度
|
||||
- 爽点类型分布
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighPointIssue:
|
||||
"""爽点问题"""
|
||||
chapter: int
|
||||
issue_type: str # low_density / weak_intensity / type_imbalance
|
||||
description: str
|
||||
severity: str
|
||||
suggested: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighPointReport:
|
||||
"""爽点检查报告"""
|
||||
chapter: int
|
||||
hot_spots_count: int
|
||||
avg_intensity: float
|
||||
dominant_type: str
|
||||
issues: List[HighPointIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class HighPointChecker:
|
||||
"""
|
||||
爽点检查器
|
||||
|
||||
检查:
|
||||
1. 爽点密度(每千字爽点数)
|
||||
2. 爽点强度
|
||||
3. 爽点类型分布
|
||||
"""
|
||||
|
||||
# 爽点关键词
|
||||
HOT_SPOT_PATTERNS = {
|
||||
"overkill_reversal": [
|
||||
"碾压", "秒杀", "一击", "一招", "不堪一击",
|
||||
"跪下", "颤抖", "惊恐", "脸色大变", "难以置信"
|
||||
],
|
||||
"taboo_transgression": [
|
||||
"禁忌", "僭越", "危险", "邪魅", "诱惑",
|
||||
"堕落", "黑化", "疯狂", "失控", "暴走"
|
||||
],
|
||||
"cognitive_closure": [
|
||||
"原来", "竟然", "真相", "恍然大悟",
|
||||
"所有一切", "早该", "早就在", "伏笔", "埋下"
|
||||
],
|
||||
"payoff": [
|
||||
"收获", "突破", "晋升", "提升", "获得",
|
||||
"机缘", "惊喜", "传承", "觉醒", "解封"
|
||||
]
|
||||
}
|
||||
|
||||
# 每千字爽点期望值
|
||||
EXPECTED_PER_1000_WORDS = 2.0 # 每千字2个爽点
|
||||
|
||||
def __init__(self):
|
||||
self.history: List[HighPointReport] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
catharsis_model: Optional[str] = None
|
||||
) -> HighPointReport:
|
||||
"""检查单章爽点"""
|
||||
word_count = len(chapter_text)
|
||||
|
||||
# 统计各类爽点
|
||||
type_counts = {t: 0 for t in self.HOT_SPOT_PATTERNS}
|
||||
type_positions = {t: [] for t in self.HOT_SPOT_PATTERNS}
|
||||
|
||||
for hot_type, keywords in self.HOT_SPOT_PATTERNS.items():
|
||||
for keyword in keywords:
|
||||
start = 0
|
||||
while True:
|
||||
pos = chapter_text.find(keyword, start)
|
||||
if pos == -1:
|
||||
break
|
||||
type_counts[hot_type] += 1
|
||||
type_positions[hot_type].append(pos)
|
||||
start = pos + 1
|
||||
|
||||
# 计算总爽点数
|
||||
total_hot_spots = sum(type_counts.values())
|
||||
|
||||
# 计算爽点密度
|
||||
density = (total_hot_spots / word_count) * 1000 if word_count > 0 else 0
|
||||
|
||||
# 确定主导类型
|
||||
dominant_type = max(type_counts, key=type_counts.get) if type_counts else "none"
|
||||
dominant_count = type_counts.get(dominant_type, 0)
|
||||
|
||||
# 估算平均强度
|
||||
avg_intensity = min(1.0, density / self.EXPECTED_PER_1000_WORDS)
|
||||
|
||||
# 生成报告
|
||||
report = HighPointReport(
|
||||
chapter=chapter_num,
|
||||
hot_spots_count=total_hot_spots,
|
||||
avg_intensity=avg_intensity,
|
||||
dominant_type=dominant_type
|
||||
)
|
||||
|
||||
# 检测问题
|
||||
if density < self.EXPECTED_PER_1000_WORDS * 0.5:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="low_density",
|
||||
description=f"爽点密度偏低({density:.1f}/千字),建议增加爽点",
|
||||
severity="high"
|
||||
))
|
||||
|
||||
if total_hot_spots == 0:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="no_hotspot",
|
||||
description="未检测到明显爽点",
|
||||
severity="critical"
|
||||
))
|
||||
|
||||
# 爽点类型过于单一
|
||||
if dominant_count > 0 and dominant_count == total_hot_spots:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="type_imbalance",
|
||||
description=f"爽点类型单一,全部为 {dominant_type}",
|
||||
severity="medium",
|
||||
suggested="建议混合多种爽点类型增加层次感"
|
||||
))
|
||||
|
||||
# 检查是否与 catharsis_model 匹配
|
||||
if catharsis_model and dominant_type != catharsis_model:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="model_mismatch",
|
||||
description=f"爽点类型与目标模型不匹配(期望 {catharsis_model},实际 {dominant_type})",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
report.passed = len([i for i in report.issues if i.severity == "critical"]) == 0
|
||||
report.summary = f"检测到 {total_hot_spots} 个爽点,密度 {density:.1f}/千字,主导类型: {dominant_type}"
|
||||
|
||||
self.history.append(report)
|
||||
return report
|
||||
|
||||
def get_arc_hotspot_analysis(self, start_chapter: int, end_chapter: int) -> Dict[str, Any]:
|
||||
"""分析情节弧的爽点分布"""
|
||||
relevant = [r for r in self.history if start_chapter <= r.chapter <= end_chapter]
|
||||
|
||||
if not relevant:
|
||||
return {"status": "no_data"}
|
||||
|
||||
total_spots = sum(r.hot_spots_count for r in relevant)
|
||||
avg_intensity = sum(r.avg_intensity for r in relevant) / len(relevant)
|
||||
|
||||
type_distribution = {}
|
||||
for r in relevant:
|
||||
t = r.dominant_type
|
||||
type_distribution[t] = type_distribution.get(t, 0) + 1
|
||||
|
||||
return {
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"total_hot_spots": total_spots,
|
||||
"avg_intensity": avg_intensity,
|
||||
"type_distribution": type_distribution,
|
||||
"issues_count": sum(len(r.issues) for r in relevant)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: ooc-checker
|
||||
description: 人物OOC检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# ooc-checker (人物OOC检查器)
|
||||
|
||||
> **职责**: 角色完整性守卫者,防止 OOC(Out-Of-Character)违规。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 角色行为分析、OOC 违规、人设漂移警告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载角色档案
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `设定集/角色卡/`(所有角色档案)
|
||||
3. 前序章节作为行为基线(若审查章节 > 10)
|
||||
|
||||
### 第二步: 提取角色档案
|
||||
|
||||
**对每个主要角色,提取**:
|
||||
- **Personality traits** (性格): e.g., "隐忍冷静/嚣张狂妄/温柔体贴"
|
||||
- **Speech patterns** (说话风格): e.g., "言简意赅/喜欢嘲讽/礼貌用词"
|
||||
- **Core values** (价值观): e.g., "重视承诺/追求力量/保护弱者"
|
||||
- **Behavioral tendencies** (行为倾向): e.g., "三思而后行/冲动鲁莽/谨慎多疑"
|
||||
|
||||
**角色档案示例**:
|
||||
```
|
||||
角色:林天(主角)
|
||||
性格:隐忍冷静、智谋深沉、不轻易暴露实力
|
||||
说话风格:言简意赅,很少废话,语气平淡
|
||||
价值观:重视家族荣誉,保护弱者
|
||||
行为倾向:三思而后行,善于隐藏真实意图
|
||||
```
|
||||
|
||||
### 第三步: 行为采样
|
||||
|
||||
**对每章,提取角色的动作和对话**:
|
||||
|
||||
```
|
||||
第45章 - 林天行为采样:
|
||||
[对话] "你找死!" 林天怒吼一声,失去理智冲向对手
|
||||
[行动] 不顾一切地正面硬刚
|
||||
[情绪] 暴怒失控
|
||||
```
|
||||
|
||||
### 第四步: OOC 检测(三级判定)
|
||||
|
||||
#### 一级: 轻微偏离
|
||||
**定义**: 角色行为略有不同,但有合理的世界观内解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
✓ Acceptable:
|
||||
角色:林天(平时冷静)
|
||||
场景:敌人威胁要杀他家人
|
||||
行为:罕见地暴怒
|
||||
判定:✓ 触及底线,情绪变化合理
|
||||
|
||||
✓ Acceptable:
|
||||
角色:李雪(平时温柔)
|
||||
场景:主角生死关头
|
||||
行为:展现强势果断的一面
|
||||
判定:✓ 危机激发隐藏面,有前置铺垫
|
||||
```
|
||||
|
||||
#### 二级: 中度失真
|
||||
**定义**: 角色行为不一致,缺乏充分的铺垫或解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
⚠️ Warning:
|
||||
角色:林天(三思而后行)
|
||||
场景:普通挑衅
|
||||
行为:突然冲动鲁莽
|
||||
判定:⚠️ 缺少动机,需补充原因(如压力积累/特殊影响)
|
||||
|
||||
⚠️ Warning:
|
||||
角色:慕容雪(高傲冷漠)
|
||||
场景:对路人甲
|
||||
行为:突然温柔体贴
|
||||
判定:⚠️ 性格转变过快,需铺垫(如特殊原因/渐进变化)
|
||||
```
|
||||
|
||||
#### 三级: 严重崩坏
|
||||
**定义**: 角色行为与既定特征完全相反,且无任何解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
❌ Violation:
|
||||
角色:反派(嚣张狂妄、智商在线)
|
||||
场景:与主角对峙
|
||||
行为:突然智商下线,犯低级错误(故意让主角翻盘)
|
||||
判定:❌ 反派智商崩坏,纯粹为剧情服务
|
||||
|
||||
❌ Violation:
|
||||
角色:林天(隐忍冷静)
|
||||
场景:无特殊刺激
|
||||
行为:持续多章表现为冲动易怒
|
||||
判定:❌ 性格全面改变无解释,核心人设崩塌
|
||||
```
|
||||
|
||||
### 第五步: 对话风格检查
|
||||
|
||||
**校验对话一致性**:
|
||||
|
||||
| Character Type | Expected Style | OOC Examples |
|
||||
|---------------|----------------|--------------|
|
||||
| **主角(冷静型)** | 言简意赅、语气平淡 | ❌ "哈哈哈!老子今天就让你见识见识!" (过度张扬) |
|
||||
| **反派(嚣张型)** | 嘲讽、轻蔑、自信 | ❌ "对不起...我错了..." (突然怯懦) |
|
||||
| **修仙者** | "阁下/道友/在下" | ❌ "牛逼/666/OMG" (现代网络用语) |
|
||||
|
||||
### 第六步: 角色成长 vs. OOC
|
||||
|
||||
**区分合理成长与 OOC**:
|
||||
|
||||
```
|
||||
✓ Character Development:
|
||||
第1-10章:林天谨慎多疑(因为实力弱)
|
||||
第50章:林天开始自信果敢(实力提升+经历磨练)
|
||||
判定:✓ 合理成长,有渐进式铺垫
|
||||
|
||||
❌ OOC:
|
||||
第10章:林天隐忍冷静
|
||||
第11章:林天突然变成话痨
|
||||
判定:❌ 无解释的性格突变,非成长而是失真
|
||||
```
|
||||
|
||||
**成长检查清单**:
|
||||
- [ ] 性格转变有合理触发事件?
|
||||
- [ ] 转变过程有渐进式铺垫?
|
||||
- [ ] 转变后的行为与触发事件逻辑一致?
|
||||
|
||||
### 第七步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 人物OOC检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 主要角色行为采样
|
||||
|
||||
### 林天(主角)
|
||||
| 章节 | 行为/对话 | 人设匹配 | OOC 级别 |
|
||||
|------|----------|---------|---------|
|
||||
| {N} | "..." 冷静观察,未轻举妄动 | ✓ 符合"隐忍冷静" | 无 |
|
||||
| {M} | "你找死!"暴怒冲向对手 | ✗ 不符合"三思而后行" | ⚠️ 中度 |
|
||||
|
||||
**OOC 分析**:
|
||||
- 第{M}章林天失去冷静,**缺少触发原因**
|
||||
- 建议补充:对手触及底线(如威胁家人)来合理化情绪爆发
|
||||
|
||||
### 慕容雪(女配)
|
||||
| 章节 | 行为/对话 | 人设匹配 | OOC 级别 |
|
||||
|------|----------|---------|---------|
|
||||
| {M} | 突然对路人温柔体贴 | ✗ 不符合"高傲冷漠" | ⚠️ 中度 |
|
||||
|
||||
**OOC 分析**:
|
||||
- 性格转变缺少铺垫,建议:
|
||||
- 补充慕容雪性格变化的原因(如受主角影响)
|
||||
- 或将此场景改为"表面冷漠实则关心"来保持人设
|
||||
|
||||
## 对话风格检查
|
||||
| 角色 | 预期风格 | 发现违规 |
|
||||
|------|---------|---------|
|
||||
| 林天 | 言简意赅 | ✓ 无违规 |
|
||||
| 反派王少 | 嚣张嘲讽 | ✗ 第{M}章突然谦逊(智商下线)|
|
||||
|
||||
## 性格转变检查
|
||||
| 角色 | 原有特征 | 当前特征 | 合理性 | 判定 |
|
||||
|------|---------|---------|--------|------|
|
||||
| 林天 | 谨慎 | 自信 | ✓ 实力提升+经历铺垫 | ✓ 合理成长 |
|
||||
| 慕容雪 | 高傲 | 温柔 | ✗ 无铺垫 | ❌ OOC |
|
||||
|
||||
## 修复建议
|
||||
1. **修复第{M}章林天OOC**: 补充对手触及底线的情节
|
||||
2. **慕容雪性格转变**: 添加渐进式铺垫(3-5章)或调整此章表现
|
||||
3. **反派王少智商崩坏**: 修改对话,恢复嚣张狂妄但逻辑在线的人设
|
||||
|
||||
## 综合评分
|
||||
**OOC 违规**:
|
||||
- 严重: {count}
|
||||
- 中度: {count}
|
||||
- 轻微: {count}
|
||||
|
||||
**结论**: {通过/警告/未通过}
|
||||
**优先修复项**: {列出必须修复的严重OOC}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在严重 OOC 且未标记的章节(如反派智商下线)
|
||||
❌ 忽略角色对话风格违规
|
||||
❌ 混淆 OOC 与角色成长
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 0 个严重 OOC 违规
|
||||
- 中度 OOC 有合理的世界观内解释
|
||||
- 角色成长是渐进且有动机的
|
||||
- 对话风格与既定档案匹配
|
||||
- 报告能区分 OOC 和合理成长
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
OOC Checker (人物偏离检查器)
|
||||
|
||||
检查角色行为是否偏离人设:
|
||||
- 性格一致性
|
||||
- 行为合理性
|
||||
- 语言风格一致性
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class OOCIssue:
|
||||
"""角色偏离问题"""
|
||||
chapter: int
|
||||
character: str
|
||||
issue_type: str # personality / behavior / dialogue
|
||||
description: str
|
||||
severity: str
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OOCReport:
|
||||
"""OOC 检查报告"""
|
||||
chapter: int
|
||||
character_count: int
|
||||
issues: List[OOCIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class OOChecker:
|
||||
"""
|
||||
角色 OOC 检查器
|
||||
|
||||
检查:
|
||||
1. 性格一致性
|
||||
2. 行为合理性
|
||||
3. 对话风格一致性
|
||||
"""
|
||||
|
||||
# 性格关键词
|
||||
PERSONALITY_KEYWORDS = {
|
||||
"冷酷": ["冷哼", "不屑", "冷笑", "漠然", "冰冷"],
|
||||
"热血": ["怒吼", "咆哮", "愤怒", "激动", "振奋"],
|
||||
"腹黑": ["微笑", "意味深长", "算计", "暗笑", "狡黠"],
|
||||
"阳光": ["微笑", "开朗", "乐观", "爽朗", "活力"],
|
||||
"沉稳": ["平静", "淡定", "从容", "冷静", "深思"]
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.character_profiles: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def load_character_profiles(self, profiles: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""加载角色设定"""
|
||||
self.character_profiles = profiles
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
characters_in_chapter: Optional[List[str]] = None
|
||||
) -> OOCReport:
|
||||
"""检查单章 OOC"""
|
||||
report = OOCReport(chapter=chapter_num, character_count=0)
|
||||
|
||||
if not characters_in_chapter:
|
||||
characters_in_chapter = self._extract_characters(chapter_text)
|
||||
|
||||
report.character_count = len(characters_in_chapter)
|
||||
|
||||
for character in characters_in_chapter:
|
||||
if character not in self.character_profiles:
|
||||
continue
|
||||
|
||||
profile = self.character_profiles[character]
|
||||
personality = profile.get("personality", "")
|
||||
|
||||
# 检查性格关键词出现
|
||||
personality_keywords = self.PERSONALITY_KEYWORDS.get(personality, [])
|
||||
|
||||
# 检测可能的 OOC
|
||||
ooc_issues = self._detect_ooc(chapter_num, character, chapter_text, personality, personality_keywords)
|
||||
report.issues.extend(ooc_issues)
|
||||
|
||||
critical_count = sum(1 for i in report.issues if i.severity == "critical")
|
||||
report.passed = critical_count == 0
|
||||
report.summary = f"检查 {report.character_count} 个角色,发现 {len(report.issues)} 个 OOC 问题"
|
||||
|
||||
return report
|
||||
|
||||
def _extract_characters(self, text: str) -> List[str]:
|
||||
"""提取出现的角色"""
|
||||
characters = set()
|
||||
|
||||
# 简单实现:提取"说"前后的名字
|
||||
import re
|
||||
dialogue_pattern = re.compile(r'^"?([^"说]{2,5})"?[说问道喊叫笑骂冷哼]')
|
||||
for line in text.split('\n'):
|
||||
match = dialogue_pattern.match(line.strip())
|
||||
if match:
|
||||
name = match.group(1).strip()
|
||||
if name and len(name) <= 5:
|
||||
characters.add(name)
|
||||
|
||||
return list(characters)
|
||||
|
||||
def _detect_ooc(
|
||||
self,
|
||||
chapter: int,
|
||||
character: str,
|
||||
text: str,
|
||||
personality: str,
|
||||
expected_keywords: List[str]
|
||||
) -> List[OOCIssue]:
|
||||
"""检测 OOC"""
|
||||
issues = []
|
||||
|
||||
# 检查是否有相反性格的关键词
|
||||
opposite_keywords = {
|
||||
"冷酷": ["微笑", "开心", "高兴", "热情"],
|
||||
"热血": ["冷漠", "冷淡", "冷静", "漠然"],
|
||||
"腹黑": ["坦诚", "直接", "单纯", "天真"],
|
||||
"阳光": ["阴沉", "冷漠", "阴暗", "消沉"],
|
||||
"沉稳": ["激动", "暴躁", "鲁莽", "轻浮"]
|
||||
}
|
||||
|
||||
opposite = opposite_keywords.get(personality, [])
|
||||
found_opposite = [kw for kw in opposite if kw in text]
|
||||
|
||||
if found_opposite:
|
||||
# 检查是否在对话中(可能需要根据上下文判断)
|
||||
# 简化:直接报告
|
||||
issues.append(OOCIssue(
|
||||
chapter=chapter,
|
||||
character=character,
|
||||
issue_type="personality",
|
||||
description=f"角色表现出与设定({personality})相反的性格特征: {', '.join(found_opposite)}",
|
||||
severity="medium",
|
||||
suggested_fix="确认是否为特定场景需要,或调整言行描述"
|
||||
))
|
||||
|
||||
return issues
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
name: pacing-checker
|
||||
description: Strand Weave 节奏检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# pacing-checker (节奏检查器)
|
||||
|
||||
> **职责**: 节奏分析师,执行 Strand Weave 平衡检查,防止读者疲劳。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 情节线分布分析、平衡预警、节奏建议。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载上下文
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `{project_root}/..noma/novel_data/state.json`(strand_tracker 历史)
|
||||
3. `大纲/`(理解预期弧线结构)
|
||||
|
||||
**可选: 使用 status_reporter 进行自动化分析**:
|
||||
```bash
|
||||
python -X utf8 "${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts/noma.py" --project-root "${PROJECT_ROOT}" status -- --focus strand
|
||||
```
|
||||
|
||||
### 第二步: 章节情节线分类
|
||||
|
||||
**对每章,识别主导情节线**:
|
||||
|
||||
| Strand | Indicators | Examples |
|
||||
|--------|-----------|----------|
|
||||
| **Quest** (主线) | 战斗/任务/探索/升级/打怪 | 参加宗门大比、探索秘境、击败反派 |
|
||||
| **Fire** (感情线) | 情感关系/暧昧/友情/羁绊 | 与李雪的感情发展、师徒情深、兄弟义气 |
|
||||
| **Constellation** (世界观线) | 势力关系/阵营/社交网络/揭示世界观 | 新势力登场、修仙界格局展示、宗门政治 |
|
||||
|
||||
**分类规则**:
|
||||
- 一章可以有多条情节线的**底色**,但只有**一条主导**
|
||||
- 主导 = 占据章节内容 ≥ 60%
|
||||
|
||||
**Example**:
|
||||
```
|
||||
第45章:主角参加大比(Quest 80%)+ 李雪担心主角(Fire 20%)
|
||||
→ Dominant: Quest
|
||||
|
||||
第46章:主角与李雪约会(Fire 70%)+ 揭示血煞门阴谋(Constellation 30%)
|
||||
→ Dominant: Fire
|
||||
```
|
||||
|
||||
### 第三步: 平衡检查(Strand Weave 违规)
|
||||
|
||||
**从 state.json 加载 strand_tracker**:
|
||||
```json
|
||||
{
|
||||
"strand_tracker": {
|
||||
"last_quest_chapter": 46,
|
||||
"last_fire_chapter": 42,
|
||||
"last_constellation_chapter": 38,
|
||||
"history": [
|
||||
{"chapter": 45, "dominant": "quest"},
|
||||
{"chapter": 46, "dominant": "quest"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**应用警告阈值**:
|
||||
|
||||
| 违规类型 | 触发条件 | 严重度 | 影响 |
|
||||
|-----------|-----------|----------|--------|
|
||||
| **Quest 过载** | 连续 5+ 章 Quest 主导 | High | 战斗疲劳,缺少情感深度 |
|
||||
| **Fire 干旱** | 距上次 Fire > 10 章 | Medium | 人物关系停滞 |
|
||||
| **Constellation 缺席** | 距上次 Constellation > 15 章 | Low | 世界观单薄 |
|
||||
|
||||
**违规示例**:
|
||||
```
|
||||
⚠️ Quest Overload (连续7章)
|
||||
Chapters 40-46 全部为 Quest 主导
|
||||
→ Impact: 读者疲劳,建议第47章安排感情戏或世界观扩展
|
||||
|
||||
⚠️ Fire Drought (已12章未出现)
|
||||
Last Fire chapter: 34 | Current: 46 | Gap: 12 chapters
|
||||
→ Impact: 李雪等角色存在感降低,建议补充互动场景
|
||||
|
||||
✓ Constellation Acceptable
|
||||
Last Constellation: 38 | Current: 46 | Gap: 8 chapters
|
||||
```
|
||||
|
||||
### 第四步: 节奏标准
|
||||
|
||||
**每10章理想分布与缺席阈值**:
|
||||
|
||||
| Strand | 理想占比 | 最大缺席 | 超限影响 |
|
||||
|--------|---------|---------|---------|
|
||||
| Quest (主线) | 55-65% | 5 章连续 | 战斗疲劳,缺少情感深度 |
|
||||
| Fire (感情线) | 20-30% | 10 章 | 人物关系停滞 |
|
||||
| Constellation (世界观线) | 10-20% | 15 章 | 世界观单薄 |
|
||||
|
||||
### 第五步: 历史趋势分析
|
||||
|
||||
**若 state.json 包含 20+ 章历史数据**:
|
||||
|
||||
生成情节线分布图:
|
||||
```
|
||||
Chapters 1-20 Strand Distribution:
|
||||
Quest: ████████████░░░░░░░░ 60% (12 chapters)
|
||||
Fire: ████░░░░░░░░░░░░░░░░ 20% (4 chapters)
|
||||
Constellation: ████░░░░░░░░░░░░░░░░ 20% (4 chapters)
|
||||
|
||||
结论:✓ 节奏均衡(符合理想比例)
|
||||
```
|
||||
|
||||
vs.
|
||||
|
||||
```
|
||||
Chapters 21-40 Strand Distribution:
|
||||
Quest: ███████████████████░ 95% (19 chapters)
|
||||
Fire: █░░░░░░░░░░░░░░░░░░░ 5% (1 chapter)
|
||||
Constellation: ░░░░░░░░░░░░░░░░░░░░ 0% (0 chapters)
|
||||
|
||||
结论:✗ 严重失衡(Quest 过载,节奏单调)
|
||||
```
|
||||
|
||||
### 第六步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 节奏检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 当前章节主导情节线
|
||||
| 章节 | 主导线 | 底色 | 强度 |
|
||||
|------|--------|------|------|
|
||||
| {N} | Quest | Fire(20%)| 高(战斗密集)|
|
||||
| {M} | Quest | - | 中等 |
|
||||
|
||||
## Strand 平衡检查
|
||||
### Quest 线(主线)
|
||||
- 最近出现: 第 {X} 章
|
||||
- 连续章数: {count}
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警 / ✗ 过载}
|
||||
|
||||
### Fire 线(情感线)
|
||||
- 最近出现: 第 {Y} 章
|
||||
- 距上次间隔: {count} 章
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警 / ✗ 干旱}
|
||||
|
||||
### Constellation 线(世界观线)
|
||||
- 最近出现: 第 {Z} 章
|
||||
- 距上次间隔: {count} 章
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警}
|
||||
|
||||
## 历史趋势(需 ≥ 20 章数据)
|
||||
最近 20 章分布:
|
||||
- Quest: {X}%({count} 章)
|
||||
- Fire: {Y}%({count} 章)
|
||||
- Constellation: {Z}%({count} 章)
|
||||
|
||||
**趋势**: {均衡 / Quest偏重 / Fire不足 / ...}
|
||||
|
||||
## 修复建议
|
||||
- [Quest 过载] 连续{count}章Quest主导,建议在第{next}章安排:
|
||||
- 与{角色}的感情发展场景(Fire)
|
||||
- 或揭示{势力/世界观元素}(Constellation)
|
||||
|
||||
- [Fire 干旱] 距上次Fire已{count}章,建议补充:
|
||||
- 与李雪/师父/伙伴的互动
|
||||
- 不必是专门的感情章,可作为底色穿插
|
||||
|
||||
- [Constellation 间隔] 世界观扩展不足,建议:
|
||||
- 揭示新势力或修仙界格局
|
||||
- 展示新的修炼体系或设定
|
||||
|
||||
## 下一章节奏建议
|
||||
基于当前平衡状态,第 {next} 章应优先:
|
||||
**主导**: {线}(因为距上次{gap}章)
|
||||
**底色**: {线}
|
||||
|
||||
## 综合评分
|
||||
**节奏总评**: {健康/预警/危险}
|
||||
**读者疲劳风险**: {低/中/高}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过连续 5+ 章 Quest 主导且不预警
|
||||
❌ 忽略 Fire 干旱超过 10 章
|
||||
❌ 接受 20+ 章中完全相同的节奏模式
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 最近 10 章内单一情节线不超过 70%
|
||||
- 所有情节线在各自阈值内至少出现一次
|
||||
- 报告提供可执行的下一章建议
|
||||
- 趋势分析显示分布均衡(若有足够历史数据)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Pacing Checker (节奏检查器)
|
||||
|
||||
检查 Strand 比例与节奏:
|
||||
- Quest/Fire/Constellation 比例
|
||||
- 断档检查
|
||||
- 节奏红线
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacingIssue:
|
||||
"""节奏问题"""
|
||||
chapter: int
|
||||
issue_type: str # strand_imbalance / gap_too_long / rush
|
||||
description: str
|
||||
severity: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacingReport:
|
||||
"""节奏检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
strand_distribution: Dict[str, int] = field(default_factory=dict)
|
||||
issues: List[PacingIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class PacingChecker:
|
||||
"""
|
||||
节奏检查器
|
||||
|
||||
检查:
|
||||
1. Strand 比例(Quest/Fire/Constellation)
|
||||
2. 断档检查
|
||||
3. 节奏红线
|
||||
"""
|
||||
|
||||
# 理想比例
|
||||
IDEAL_RATIOS = {
|
||||
"quest": (55, 65), # 60% 主线
|
||||
"fire": (20, 30), # 25% 感情
|
||||
"constellation": (10, 20), # 15% 世界观
|
||||
}
|
||||
|
||||
# 断档红线
|
||||
MAX_GAPS = {
|
||||
"quest": 5, # Quest 连续不超过 5 章
|
||||
"fire": 10, # Fire 断档不超过 10 章
|
||||
"constellation": 15 # Constellation 断档不超过 15 章
|
||||
}
|
||||
|
||||
# 关键词
|
||||
STRAND_KEYWORDS = {
|
||||
"quest": ["战斗", "修炼", "突破", "机缘", "仇敌", "目标", "任务", "追杀"],
|
||||
"fire": ["感情", "爱慕", "暧昧", "心跳", "脸红", "温情", "甜蜜", "争吵"],
|
||||
"constellation": ["世界", "势力", "宗门", "规则", "历史", "背景", "设定"]
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.strand_history: List[Dict[str, Any]] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str
|
||||
) -> PacingReport:
|
||||
"""检查单章节奏"""
|
||||
# 统计各 Strand 出现次数
|
||||
strand_counts = {s: 0 for s in self.STRAND_KEYWORDS}
|
||||
|
||||
for strand, keywords in self.STRAND_KEYWORDS.items():
|
||||
for keyword in keywords:
|
||||
strand_counts[strand] += chapter_text.count(keyword)
|
||||
|
||||
# 确定主导 Strand
|
||||
dominant = max(strand_counts, key=strand_counts.get)
|
||||
|
||||
report = PacingReport(
|
||||
start_chapter=chapter_num,
|
||||
end_chapter=chapter_num,
|
||||
strand_distribution=strand_counts
|
||||
)
|
||||
|
||||
# 记录历史
|
||||
self.strand_history.append({
|
||||
"chapter": chapter_num,
|
||||
"strand_counts": strand_counts,
|
||||
"dominant": dominant
|
||||
})
|
||||
|
||||
return report
|
||||
|
||||
def check_arc_pacing(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int
|
||||
) -> PacingReport:
|
||||
"""检查情节弧节奏"""
|
||||
relevant = [h for h in self.strand_history
|
||||
if start_chapter <= h["chapter"] <= end_chapter]
|
||||
|
||||
if not relevant:
|
||||
return PacingReport(start_chapter=start_chapter, end_chapter=end_chapter)
|
||||
|
||||
# 统计各 Strand 章节数
|
||||
strand_chapter_counts = {s: 0 for s in self.STRAND_KEYWORDS}
|
||||
for h in relevant:
|
||||
dominant = h["dominant"]
|
||||
if h["strand_counts"][dominant] > 0:
|
||||
strand_chapter_counts[dominant] += 1
|
||||
|
||||
total_chapters = len(relevant)
|
||||
|
||||
report = PacingReport(
|
||||
start_chapter=start_chapter,
|
||||
end_chapter=end_chapter,
|
||||
strand_distribution=strand_chapter_counts
|
||||
)
|
||||
|
||||
# 检查比例
|
||||
for strand, (min_ratio, max_ratio) in self.IDEAL_RATIOS.items():
|
||||
actual_ratio = (strand_chapter_counts[strand] / total_chapters * 100) if total_chapters > 0 else 0
|
||||
|
||||
if actual_ratio < min_ratio:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=0,
|
||||
issue_type="strand_imbalance",
|
||||
description=f"{strand} 占比不足({actual_ratio:.0f}% < {min_ratio}%)",
|
||||
severity="medium"
|
||||
))
|
||||
elif actual_ratio > max_ratio:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=0,
|
||||
issue_type="strand_imbalance",
|
||||
description=f"{strand} 占比过高({actual_ratio:.0f}% > {max_ratio}%)",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
# 检查断档
|
||||
self._check_gaps(report, relevant)
|
||||
|
||||
report.summary = f"检查 {total_chapters} 章,分布: {strand_chapter_counts}"
|
||||
return report
|
||||
|
||||
def _check_gaps(self, report: PacingReport, history: List[Dict[str, Any]]) -> None:
|
||||
"""检查断档"""
|
||||
if len(history) < 2:
|
||||
return
|
||||
|
||||
last_seen = {s: -100 for s in self.STRAND_KEYWORDS}
|
||||
|
||||
for i, h in enumerate(history):
|
||||
chapter = h["chapter"]
|
||||
dominant = h["dominant"]
|
||||
|
||||
for strand in self.STRAND_KEYWORDS:
|
||||
if strand == dominant:
|
||||
last_seen[strand] = chapter
|
||||
else:
|
||||
gap = chapter - last_seen[strand]
|
||||
if gap > self.MAX_GAPS[strand]:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=chapter,
|
||||
issue_type="gap_too_long",
|
||||
description=f"{strand} 断档过长({gap}章未出现)",
|
||||
severity="high"
|
||||
))
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: reader-pull-checker
|
||||
description: 追读力检查器,评估钩子/微兑现/约束分层,支持 Override Contract
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# reader-pull-checker (追读力检查器)
|
||||
|
||||
> **职责**: 审查"读者为什么会点下一章",执行 Hard/Soft 约束分层。
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **分类法**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **题材画像**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
- **章节追读力数据**: `index.db → chapter_reading_power`
|
||||
- **上章钩子**: `state.json → chapter_meta` 或 `index.db`
|
||||
|
||||
## 输入
|
||||
- 章节正文(实际章节文件路径,优先 `正文/第{NNNN}章-{title_safe}.md`,旧格式 `正文/第{NNNN}章.md` 仍兼容)
|
||||
- 上章钩子与模式(从 `state.json → chapter_meta` 或 `index.db`)
|
||||
- 题材 Profile(从 `state.json → project.genre`)
|
||||
- 是否为过渡章标记
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "reader-pull-checker",
|
||||
"chapter": 100,
|
||||
"overall_score": 85,
|
||||
"pass": true,
|
||||
"issues": [],
|
||||
"hard_violations": [],
|
||||
"soft_suggestions": [
|
||||
{
|
||||
"id": "SOFT_HOOK_STRENGTH",
|
||||
"severity": "medium",
|
||||
"location": "章末",
|
||||
"description": "钩子强度为weak,建议提升至medium",
|
||||
"suggestion": "将'回去休息了'改为悬念/危机",
|
||||
"can_override": true,
|
||||
"allowed_rationales": ["TRANSITIONAL_SETUP", "CHARACTER_CREDIBILITY"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"hook_present": true,
|
||||
"hook_type": "渴望钩",
|
||||
"hook_strength": "medium",
|
||||
"prev_hook_fulfilled": true,
|
||||
"new_expectations": 2,
|
||||
"pattern_repeat_risk": false,
|
||||
"micropayoffs": ["能力兑现", "认可兑现"],
|
||||
"micropayoff_count": 2,
|
||||
"is_transition": false,
|
||||
"next_chapter_reason": "读者想知道云芝找萧炎什么事",
|
||||
"debt_balance": 0.0
|
||||
},
|
||||
"summary": "硬约束通过,钩子强度偏弱,建议增强章末期待。",
|
||||
"override_eligible": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、约束分层
|
||||
|
||||
### 1.1 硬约束
|
||||
|
||||
> **违反 = 必须修复,不可申诉跳过**
|
||||
|
||||
| ID | 约束名称 | 触发条件 | severity |
|
||||
|----|---------|---------|----------|
|
||||
| HARD-001 | 可读性底线 | 读者无法理解"发生了什么/谁/为什么" | critical |
|
||||
| HARD-002 | 承诺违背 | 上章明确承诺在本章完全无回应 | critical |
|
||||
| HARD-003 | 节奏灾难 | 连续N章无任何推进(N由profile决定) | critical |
|
||||
| HARD-004 | 冲突真空 | 整章无问题/目标/代价 | high |
|
||||
|
||||
**硬约束违规输出**:
|
||||
```json
|
||||
{
|
||||
"id": "HARD-002",
|
||||
"severity": "critical",
|
||||
"location": "全章",
|
||||
"description": "上章钩子'敌人即将到来'完全未在本章提及",
|
||||
"must_fix": true,
|
||||
"fix_suggestion": "在开头或中段回应敌人威胁"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 软建议
|
||||
|
||||
> **违反 = 可申诉,但需记录 `Override Contract` 并承担债务**
|
||||
|
||||
| ID | 约束名称 | 默认期望 | 可覆盖 |
|
||||
|----|---------|---------|-----------|
|
||||
| SOFT_NEXT_REASON | 下章动机 | 读者能明确"为何点下一章" | ✓ |
|
||||
| SOFT_HOOK_ANCHOR | 期待锚点有效性 | 有未闭合问题或明确期待(章末/后段均可) | ✓ |
|
||||
| SOFT_HOOK_STRENGTH | 钩子强度 | 题材profile baseline | ✓ |
|
||||
| SOFT_HOOK_TYPE | 钩子类型 | 匹配题材偏好 | ✓ |
|
||||
| SOFT_MICROPAYOFF | 微兑现数量 | ≥ profile.min_per_chapter | ✓ |
|
||||
| SOFT_PATTERN_REPEAT | 模式重复 | 避免连续3章同型 | ✓ |
|
||||
| SOFT_EXPECTATION_OVERLOAD | 期待过载 | 新增期待 ≤ 2 | ✓ |
|
||||
| SOFT_RHYTHM_NATURALNESS | 节奏自然性 | 避免固定字距机械打点 | ✓ |
|
||||
|
||||
**软建议输出**:
|
||||
```json
|
||||
{
|
||||
"id": "SOFT_MICROPAYOFF",
|
||||
"severity": "medium",
|
||||
"location": "全章",
|
||||
"description": "本章微兑现0个,题材要求≥1",
|
||||
"suggestion": "添加能力兑现或认可兑现",
|
||||
"can_override": true,
|
||||
"allowed_rationales": ["TRANSITIONAL_SETUP", "ARC_TIMING"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、钩子类型扩展
|
||||
|
||||
### 2.1 完整钩子类型
|
||||
|
||||
| 类型 | 标识 | 驱动力 |
|
||||
|------|------|--------|
|
||||
| 危机钩 | Crisis Hook | 危险逼近,读者担心 |
|
||||
| 悬念钩 | Mystery Hook | 信息缺口,读者好奇 |
|
||||
| 情绪钩 | Emotion Hook | 强情绪触发(愤怒/心疼/心动) |
|
||||
| 选择钩 | Choice Hook | 两难抉择,读者想知道选择 |
|
||||
| 渴望钩 | Desire Hook | 好事将至,读者期待 |
|
||||
|
||||
### 2.2 钩子强度
|
||||
|
||||
| 强度 | 适用场景 | 特征 |
|
||||
|------|---------|------|
|
||||
| **strong** | 卷末/关键转折/大冲突前 | 读者必须立刻知道 |
|
||||
| **medium** | 普通剧情章 | 读者想知道,但可等 |
|
||||
| **weak** | 过渡章/铺垫章 | 维持阅读惯性 |
|
||||
|
||||
---
|
||||
|
||||
## 三、微兑现检测
|
||||
|
||||
### 3.1 微兑现类型
|
||||
|
||||
| 类型 | 识别信号 |
|
||||
|------|---------|
|
||||
| 信息兑现 | 揭示新信息/线索/真相 |
|
||||
| 关系兑现 | 关系推进/确认/变化 |
|
||||
| 能力兑现 | 能力提升/新技能展示 |
|
||||
| 资源兑现 | 获得物品/资源/财富 |
|
||||
| 认可兑现 | 获得认可/面子/地位 |
|
||||
| 情绪兑现 | 情绪释放/共鸣 |
|
||||
| 线索兑现 | 伏笔回收/推进 |
|
||||
|
||||
### 3.2 检测规则
|
||||
|
||||
1. 扫描正文识别微兑现
|
||||
2. 按题材profile检查数量是否达标
|
||||
3. 过渡章可降级要求
|
||||
|
||||
---
|
||||
|
||||
## 四、模式重复检测
|
||||
|
||||
### 4.1 检测范围
|
||||
- 钩子类型:最近3章
|
||||
- 开头模式:最近3章
|
||||
- 爽点模式:最近5章
|
||||
|
||||
### 4.2 风险等级
|
||||
- **warning**: 连续2章同型
|
||||
- **risk**: 连续3章同型
|
||||
- **critical**: 连续4+章同型
|
||||
|
||||
---
|
||||
|
||||
## 五、`Override Contract` 机制
|
||||
|
||||
### 5.1 何时可覆盖
|
||||
|
||||
当 `soft_suggestions` 中的建议无法遵守时,可提交 `Override Contract`:
|
||||
|
||||
```json
|
||||
{
|
||||
"constraint_type": "SOFT_MICROPAYOFF",
|
||||
"constraint_id": "micropayoff_count",
|
||||
"rationale_type": "TRANSITIONAL_SETUP",
|
||||
"rationale_text": "本章为铺垫章,下章将有大爽点",
|
||||
"payback_plan": "下章补偿2个微兑现",
|
||||
"due_chapter": 101
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 rationale_type 枚举
|
||||
|
||||
| 类型 | 描述 | 债务影响 |
|
||||
|------|------|---------|
|
||||
| TRANSITIONAL_SETUP | 铺垫/过渡需要 | 标准 |
|
||||
| LOGIC_INTEGRITY | 剧情逻辑优先 | 减少 |
|
||||
| CHARACTER_CREDIBILITY | 人物可信度优先 | 减少 |
|
||||
| WORLD_RULE_CONSTRAINT | 设定约束 | 减少 |
|
||||
| ARC_TIMING | 长线节奏安排 | 标准 |
|
||||
| GENRE_CONVENTION | 题材惯例 | 标准 |
|
||||
| EDITORIAL_INTENT | 作者主观意图 | 增加 |
|
||||
|
||||
### 5.3 债务与利息
|
||||
|
||||
- 每个 `Override` 产生债务(量由题材 profile 的 `debt_multiplier` 决定)
|
||||
- 每章债务累积利息(默认10%/章)
|
||||
- 超过 `due_chapter` 未偿还,债务变为 `overdue`
|
||||
|
||||
---
|
||||
|
||||
## 六、执行步骤
|
||||
|
||||
### Step 1: 加载配置
|
||||
1. 读取题材Profile
|
||||
2. 读取上章钩子/模式记录
|
||||
3. 检查当前债务状态
|
||||
|
||||
### Step 2: 硬约束检查
|
||||
1. 检查可读性(关键信息完整性)
|
||||
2. 检查上章钩子兑现
|
||||
3. 检查节奏停滞
|
||||
4. 检查冲突存在
|
||||
|
||||
**任何硬约束违规 → 立即标记为必须修复**
|
||||
|
||||
### Step 3: 钩子分析
|
||||
1. 识别本章期待锚点(优先章末,允许后段)
|
||||
2. 评估钩子强度与有效性
|
||||
3. 对比题材偏好与章节类型
|
||||
|
||||
### Step 4: 微兑现扫描
|
||||
1. 识别章内微兑现
|
||||
2. 统计数量和类型
|
||||
3. 对比题材要求
|
||||
|
||||
### Step 5: 模式重复检测
|
||||
1. 获取最近N章模式
|
||||
2. 检测钩子类型重复
|
||||
3. 检测开头模式重复
|
||||
|
||||
### Step 6: 软建议评估
|
||||
1. 汇总所有软建议
|
||||
2. 标注可覆盖的建议
|
||||
3. 列出允许的 `rationale` 类型
|
||||
|
||||
### Step 7: 生成报告
|
||||
1. 计算总分
|
||||
2. 输出结构化JSON
|
||||
3. 提供修复建议
|
||||
|
||||
---
|
||||
|
||||
## 七、评分规则
|
||||
|
||||
### 7.1 硬约束违规
|
||||
- 任何硬约束违规 → 直接未通过
|
||||
- 必须修复后重新审核
|
||||
|
||||
### 7.2 软评分(无硬约束违规时)
|
||||
|
||||
| 得分 | 结果 |
|
||||
|------|------|
|
||||
| 85+ | 通过 |
|
||||
| 70-84 | 通过(有警告) |
|
||||
| 50-69 | 条件通过(可通过 `Override`)|
|
||||
| <50 | 未通过 |
|
||||
|
||||
### 7.3 软评分计算
|
||||
|
||||
| 检查项 | 权重 | 问题类型 |
|
||||
|--------|------|----------|
|
||||
| 下章动机清晰 | 20% | NEXT_REASON_WEAK |
|
||||
| 期待锚点有效(章末/后段) | 15% | WEAK_HOOK_ANCHOR |
|
||||
| 钩子强度适当 | 10% | WEAK_HOOK |
|
||||
| 微兑现达标 | 20% | LOW_MICROPAYOFF |
|
||||
| 模式不重复 | 15% | PATTERN_REPEAT |
|
||||
| 新增期待≤2个 | 10% | EXPECTATION_OVERLOAD |
|
||||
| 钩子类型匹配题材 | 5% | TYPE_MISMATCH |
|
||||
| 节奏自然性(非机械打点) | 5% | MECHANICAL_PACING |
|
||||
|
||||
---
|
||||
|
||||
## 八、与 Data Agent 的交互
|
||||
|
||||
审核完成后,由 Data Agent 执行:
|
||||
|
||||
1. **保存章节追读力元数据**
|
||||
```python
|
||||
index_manager.save_chapter_reading_power(ChapterReadingPowerMeta(...))
|
||||
```
|
||||
|
||||
2. **处理 `Override Contract`**(如有)
|
||||
```python
|
||||
index_manager.create_override_contract(OverrideContractMeta(...))
|
||||
index_manager.create_debt(ChaseDebtMeta(...))
|
||||
```
|
||||
|
||||
3. **计算利息**(每章)
|
||||
```python
|
||||
index_manager.accrue_interest(current_chapter)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、成功标准
|
||||
|
||||
- [ ] 无硬约束违规
|
||||
- [ ] 软评分 ≥ 70(或有有效 `Override`)
|
||||
- [ ] 存在可感知的未闭合问题/期待锚点(章末或后段)
|
||||
- [ ] 微兑现数量达标(或有 `Override`)
|
||||
- [ ] 无连续3章以上同型
|
||||
- [ ] 输出清晰的"下章动机"
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Reader-Pull Checker (追读力检查器)
|
||||
|
||||
检查钩子强度、期待管理、追读力:
|
||||
- 章节开头的钩子
|
||||
- 章节结尾的悬念
|
||||
- 追读力债务
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReaderPullIssue:
|
||||
"""追读力问题"""
|
||||
chapter: int
|
||||
issue_type: str # weak_hook / no_ending_hook / debt_high
|
||||
description: str
|
||||
severity: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReaderPullReport:
|
||||
"""追读力检查报告"""
|
||||
chapter: int
|
||||
hook_strength: float # 0-1
|
||||
ending_hook: bool
|
||||
debt_level: float
|
||||
issues: List[ReaderPullIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ReaderPullChecker:
|
||||
"""
|
||||
追读力检查器
|
||||
|
||||
检查:
|
||||
1. 章节开头的钩子强度
|
||||
2. 章节结尾的悬念
|
||||
3. 追读力债务水平
|
||||
"""
|
||||
|
||||
# 强钩子关键词
|
||||
STRONG_HOOK_KEYWORDS = [
|
||||
"危机", "危险", "困境", "生死", "抉择", "真相",
|
||||
"震惊", "意外", "转折", "揭秘", "冲突", "大战"
|
||||
]
|
||||
|
||||
# 结尾悬念关键词
|
||||
ENDING_HOOK_KEYWORDS = [
|
||||
"然而", "但是", "就在这时", "突然", "意外",
|
||||
"未完待续", "敬请期待", "欲知后事", "悬念",
|
||||
"却不知道", "更大的"
|
||||
]
|
||||
|
||||
# 悬念关键词
|
||||
SUSPENSE_KEYWORDS = [
|
||||
"悬念", "伏笔", "暗示", "预示", "危机", "威胁"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.chapter_debts: List[float] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_debt: float = 0.0
|
||||
) -> ReaderPullReport:
|
||||
"""检查单章追读力"""
|
||||
# 分析开头钩子
|
||||
opening = chapter_text[:500] if len(chapter_text) > 500 else chapter_text
|
||||
hook_strength = self._analyze_hook_strength(opening)
|
||||
|
||||
# 分析结尾悬念
|
||||
ending = chapter_text[-500:] if len(chapter_text) > 500 else chapter_text
|
||||
ending_hook = self._has_ending_hook(ending)
|
||||
|
||||
# 计算本章追读力债务
|
||||
debt_level = self._calculate_debt_level(chapter_text, hook_strength, ending_hook)
|
||||
|
||||
report = ReaderPullReport(
|
||||
chapter=chapter_num,
|
||||
hook_strength=hook_strength,
|
||||
ending_hook=ending_hook,
|
||||
debt_level=debt_level
|
||||
)
|
||||
|
||||
# 检测问题
|
||||
if hook_strength < 0.3:
|
||||
report.issues.append(ReaderPullIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="weak_hook",
|
||||
description="章节开头钩子较弱,可能无法吸引读者",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
if not ending_hook:
|
||||
report.issues.append(ReaderPullIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="no_ending_hook",
|
||||
description="章节结尾缺少悬念,读者缺乏继续阅读的动力",
|
||||
severity="high"
|
||||
))
|
||||
|
||||
# 检查追读力债务
|
||||
total_debt = previous_debt + debt_level
|
||||
if total_debt > 80:
|
||||
report.issues.append(ReaderPullIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="debt_high",
|
||||
description=f"追读力债务过高({total_debt:.0f}),建议释放",
|
||||
severity="critical"
|
||||
))
|
||||
|
||||
self.chapter_debts.append(debt_level)
|
||||
report.passed = len([i for i in report.issues if i.severity == "critical"]) == 0
|
||||
|
||||
if report.passed:
|
||||
report.summary = f"钩子强度 {hook_strength:.0%},追读力债务 {total_debt:.0f}"
|
||||
else:
|
||||
report.summary = f"发现问题: {', '.join(i.issue_type for i in report.issues)}"
|
||||
|
||||
return report
|
||||
|
||||
def _analyze_hook_strength(self, opening: str) -> float:
|
||||
"""分析钩子强度"""
|
||||
if not opening:
|
||||
return 0.0
|
||||
|
||||
strength = 0.3 # 基础分
|
||||
|
||||
# 检查强钩子关键词
|
||||
strong_count = sum(1 for kw in self.STRONG_HOOK_KEYWORDS if kw in opening)
|
||||
strength += min(0.4, strong_count * 0.1)
|
||||
|
||||
# 检查疑问句
|
||||
question_count = opening.count('?') + opening.count('?')
|
||||
strength += min(0.2, question_count * 0.1)
|
||||
|
||||
# 检查省略号(制造悬念)
|
||||
ellipsis_count = opening.count('...')
|
||||
strength += min(0.1, ellipsis_count * 0.05)
|
||||
|
||||
return min(1.0, strength)
|
||||
|
||||
def _has_ending_hook(self, ending: str) -> bool:
|
||||
"""检查结尾是否有悬念"""
|
||||
if not ending:
|
||||
return False
|
||||
|
||||
# 检查结尾悬念关键词
|
||||
for kw in self.ENDING_HOOK_KEYWORDS:
|
||||
if kw in ending:
|
||||
return True
|
||||
|
||||
# 检查是否以冲突/悬念结尾
|
||||
suspense_ending = ["?", "!", "……"]
|
||||
if any(ending.strip().endswith(s) for s in suspense_ending):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _calculate_debt_level(
|
||||
self,
|
||||
text: str,
|
||||
hook_strength: float,
|
||||
ending_hook: bool
|
||||
) -> float:
|
||||
"""计算追读力债务"""
|
||||
debt = 0.0
|
||||
|
||||
# 无开头钩子
|
||||
if hook_strength < 0.3:
|
||||
debt += 20
|
||||
|
||||
# 无结尾悬念
|
||||
if not ending_hook:
|
||||
debt += 30
|
||||
|
||||
# 积累的悬念/伏笔
|
||||
unresolved_count = 0
|
||||
for kw in self.SUSPENSE_KEYWORDS:
|
||||
unresolved_count += text.count(kw)
|
||||
|
||||
debt += min(30, unresolved_count * 5)
|
||||
|
||||
# 钩子强度可以抵消部分债务
|
||||
debt -= hook_strength * 20
|
||||
|
||||
return max(0, min(100, debt))
|
||||
|
||||
def get_arc_reader_pull(self, start_chapter: int, end_chapter: int) -> Dict[str, Any]:
|
||||
"""分析情节弧的追读力"""
|
||||
if not self.chapter_debts:
|
||||
return {"status": "no_data"}
|
||||
|
||||
relevant = self.chapter_debts[start_chapter - 1:end_chapter]
|
||||
|
||||
return {
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"avg_hook_strength": sum(h.hook_strength for h in self.chapter_debts[start_chapter-1:end_chapter]) / len(relevant) if relevant else 0,
|
||||
"avg_debt": sum(relevant) / len(relevant) if relevant else 0,
|
||||
"debt_trend": relevant[-1] - relevant[0] if len(relevant) > 1 else 0
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
Tension Checker (情绪压强校验器)
|
||||
|
||||
取代原爽点审查,校验情绪压强的积累与释放是否符合选定的 catharsis model。
|
||||
检测"降维打击"、"禁忌僭越"等爽感路由的执行效果。
|
||||
与 ledger.ts 中的 hooks_pool 压强联动。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class CatharsisModel(Enum):
|
||||
"""爽感路由模型"""
|
||||
TABOO_TRANSGRESSION = "taboo_transgression" # 禁忌僭越
|
||||
OVERKILL_REVERSAL = "overkill_reversal" # 降维打击
|
||||
COGNITIVE_CLOSURE = "cognitive_closure" # 认知闭环
|
||||
STRAND_WEAVE = "strand_weave" # 三轨编织
|
||||
|
||||
|
||||
class TensionLevel(Enum):
|
||||
"""张力等级"""
|
||||
LOW = "low" # < 30
|
||||
MEDIUM = "medium" # 30-60
|
||||
HIGH = "high" # 60-80
|
||||
DANGER = "danger" # > 80
|
||||
|
||||
|
||||
@dataclass
|
||||
class TensionReading:
|
||||
"""张力读数"""
|
||||
chapter: int
|
||||
start_tension: int # 起始压强 (0-100)
|
||||
end_tension: int # 结束压强 (0-100)
|
||||
peak_tension: int # 峰值压强
|
||||
catharsis_model: Optional[CatharsisModel]
|
||||
satisfaction_delivered: int # 爽感释放值 (0-100)
|
||||
issues: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TensionIssue:
|
||||
"""张力问题"""
|
||||
chapter: int
|
||||
issue_type: str # buildup_too_long / release_misaligned / pattern_broken
|
||||
description: str
|
||||
severity: str # critical/high/medium/low
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
class TensionChecker:
|
||||
"""
|
||||
情绪压强校验器
|
||||
|
||||
功能:
|
||||
1. 校验压强曲线是否符合 catharsis model
|
||||
2. 检测爽感释放时机
|
||||
3. 与 hooks_pool 联动
|
||||
4. 输出张力分析报告
|
||||
"""
|
||||
|
||||
# 各模型的标准张力曲线
|
||||
MODEL_CURVES = {
|
||||
CatharsisModel.TABOO_TRANSGRESSION: {
|
||||
"setup": (1, 30), # 阶段: (持续章节数, 目标压强)
|
||||
"approach": (3, 50),
|
||||
"crisis": (1, 85),
|
||||
"transgression": (1, 100),
|
||||
"fallout": (2, 40),
|
||||
},
|
||||
CatharsisModel.OVERKILL_REVERSAL: {
|
||||
"establish": (2, 40), # 建立敌人强大
|
||||
"suppress": (2, 30), # 主角被压制
|
||||
"trigger": (1, 50), # 金手指触发
|
||||
"reversal": (1, 100), # 降维打击
|
||||
"aftermath": (1, 60), # 余波
|
||||
},
|
||||
CatharsisModel.COGNITIVE_CLOSURE: {
|
||||
"setup": (3, 40), # 多线伏笔埋设
|
||||
"weave": (2, 55), # 线索交织
|
||||
"activate": (1, 75), # 伏笔激活
|
||||
"closure": (1, 100), # 认知闭合
|
||||
"new_hook": (1, 50), # 新悬念
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, ledger_path: Optional[str] = None):
|
||||
self.ledger_path = ledger_path
|
||||
self.hooks_pool: Dict[str, Any] = {}
|
||||
self.tension_history: List[TensionReading] = []
|
||||
|
||||
def load_ledger(self, ledger_data: Dict[str, Any]) -> None:
|
||||
"""加载账本数据"""
|
||||
self.hooks_pool = ledger_data.get("hooks", {})
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter: int,
|
||||
chapter_text: str,
|
||||
catharsis_model: Optional[CatharsisModel] = None,
|
||||
previous_tension: int = 30
|
||||
) -> TensionReading:
|
||||
"""
|
||||
检查单章张力
|
||||
|
||||
Args:
|
||||
chapter: 章节号
|
||||
chapter_text: 章节正文
|
||||
catharsis_model: 使用的爽感模型(可选)
|
||||
previous_tension: 上一章结束时的压强
|
||||
|
||||
Returns:
|
||||
TensionReading: 张力读数
|
||||
"""
|
||||
# 计算起始/结束/峰值压强
|
||||
start_tension = previous_tension
|
||||
|
||||
# 分析本章爽感事件
|
||||
climax_events = self._detect_climax_events(chapter_text)
|
||||
|
||||
# 计算结束压强
|
||||
if climax_events["major_climax"]:
|
||||
end_tension = max(20, previous_tension - 40) # 大高潮后压强下降
|
||||
peak_tension = 100
|
||||
elif climax_events["minor_climax"]:
|
||||
end_tension = max(30, previous_tension - 20) # 小高潮后压强下降
|
||||
peak_tension = max(70, previous_tension + 20)
|
||||
elif climax_events["tension_build"]:
|
||||
end_tension = min(90, previous_tension + 15) # 积累压强上升
|
||||
peak_tension = end_tension
|
||||
else:
|
||||
end_tension = previous_tension # 平稳过渡
|
||||
peak_tension = previous_tension
|
||||
|
||||
# 计算爽感释放值
|
||||
satisfaction = self._calculate_satisfaction(climax_events)
|
||||
|
||||
# 检测问题
|
||||
issues = self._detect_issues(chapter, previous_tension, end_tension, catharsis_model, climax_events)
|
||||
|
||||
reading = TensionReading(
|
||||
chapter=chapter,
|
||||
start_tension=start_tension,
|
||||
end_tension=end_tension,
|
||||
peak_tension=peak_tension,
|
||||
catharsis_model=catharsis_model,
|
||||
satisfaction_delivered=satisfaction,
|
||||
issues=issues
|
||||
)
|
||||
|
||||
self.tension_history.append(reading)
|
||||
return reading
|
||||
|
||||
def _detect_climax_events(self, text: str) -> Dict[str, Any]:
|
||||
"""检测章节中的高潮事件"""
|
||||
events = {
|
||||
"major_climax": False, # 大高潮(降维打击、禁忌突破等)
|
||||
"minor_climax": False, # 小高潮(打脸、收获等)
|
||||
"tension_build": False, # 压强积累
|
||||
}
|
||||
|
||||
# 关键词检测
|
||||
major_keywords = [
|
||||
"碾压", "秒杀", "一击", "降维打击", "禁忌突破",
|
||||
"震惊", "不可思议", "全场寂静", "彻底碾压"
|
||||
]
|
||||
minor_keywords = [
|
||||
"冷笑", "一掌", "打脸", "收获", "突破",
|
||||
"提升", "获得", "机缘", "惊喜"
|
||||
]
|
||||
buildup_keywords = [
|
||||
"危机", "困境", "压力", "紧张", "危机四伏",
|
||||
"蓄势", "积累", "暗中", "谋划"
|
||||
]
|
||||
|
||||
text_length = len(text)
|
||||
|
||||
for keyword in major_keywords:
|
||||
if keyword in text:
|
||||
events["major_climax"] = True
|
||||
break
|
||||
|
||||
if not events["major_climax"]:
|
||||
for keyword in minor_keywords:
|
||||
if keyword in text:
|
||||
events["minor_climax"] = True
|
||||
break
|
||||
|
||||
for keyword in buildup_keywords:
|
||||
if keyword in text:
|
||||
events["tension_build"] = True
|
||||
break
|
||||
|
||||
return events
|
||||
|
||||
def _calculate_satisfaction(self, events: Dict[str, Any]) -> int:
|
||||
"""计算爽感释放值"""
|
||||
if events["major_climax"]:
|
||||
return 90
|
||||
elif events["minor_climax"]:
|
||||
return 60
|
||||
elif events["tension_build"]:
|
||||
return 20
|
||||
else:
|
||||
return 10
|
||||
|
||||
def _detect_issues(
|
||||
self,
|
||||
chapter: int,
|
||||
previous_tension: int,
|
||||
current_tension: int,
|
||||
catharsis_model: Optional[CatharsisModel],
|
||||
events: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""检测张力问题"""
|
||||
issues = []
|
||||
|
||||
# 问题1: 压强过高持续太久
|
||||
if previous_tension > 85 and current_tension > 85:
|
||||
issues.append("⚠️ 压强持续过高 (>85),可能导致读者疲劳")
|
||||
|
||||
# 问题2: 压强过低但无爽感
|
||||
if current_tension < 30 and not events["major_climax"] and not events["minor_climax"]:
|
||||
issues.append("⚠️ 压强过低且无爽感释放,节奏可能拖沓")
|
||||
|
||||
# 问题3: 压强突然下降
|
||||
if previous_tension > 50 and current_tension < previous_tension - 30:
|
||||
if not events["major_climax"] and not events["minor_climax"]:
|
||||
issues.append("⚠️ 压强突然下降但无对应爽感事件")
|
||||
|
||||
# 问题4: 压强过高但无释放
|
||||
if current_tension > 90 and not events["major_climax"]:
|
||||
issues.append("🔴 压强达到危险水平 (>90) 但无高潮释放")
|
||||
|
||||
return issues
|
||||
|
||||
def check_arc_tension(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int,
|
||||
catharsis_model: CatharsisModel
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
检查情节弧的张力曲线
|
||||
|
||||
Args:
|
||||
start_chapter: 起始章节
|
||||
end_chapter: 结束章节
|
||||
catharsis_model: 使用的爽感模型
|
||||
|
||||
Returns:
|
||||
分析报告
|
||||
"""
|
||||
relevant_readings = [
|
||||
r for r in self.tension_history
|
||||
if start_chapter <= r.chapter <= end_chapter
|
||||
]
|
||||
|
||||
if not relevant_readings:
|
||||
return {"status": "no_data", "message": "无张力历史数据"}
|
||||
|
||||
model_curve = self.MODEL_CURVES.get(catharsis_model, {})
|
||||
if not model_curve:
|
||||
return {"status": "unknown_model", "message": f"未知的爽感模型: {catharsis_model}"}
|
||||
|
||||
# 分析曲线是否符合模型
|
||||
expected_phases = list(model_curve.keys())
|
||||
actual_phases = self._infer_phases(relevant_readings)
|
||||
|
||||
# 检查匹配度
|
||||
match_score = self._calculate_curve_match(model_curve, relevant_readings)
|
||||
|
||||
# 生成报告
|
||||
report = {
|
||||
"status": "analyzed",
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"model": catharsis_model.value,
|
||||
"match_score": match_score,
|
||||
"phases": {
|
||||
"expected": expected_phases,
|
||||
"actual": actual_phases
|
||||
},
|
||||
"tension_summary": {
|
||||
"avg_start": sum(r.start_tension for r in relevant_readings) / len(relevant_readings),
|
||||
"avg_end": sum(r.end_tension for r in relevant_readings) / len(relevant_readings),
|
||||
"max_peak": max(r.peak_tension for r in relevant_readings),
|
||||
"total_satisfaction": sum(r.satisfaction_delivered for r in relevant_readings)
|
||||
},
|
||||
"issues": self._collect_arc_issues(relevant_readings, model_curve),
|
||||
"recommendations": self._generate_recommendations(match_score, relevant_readings)
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
def _infer_phases(self, readings: List[TensionReading]) -> List[str]:
|
||||
"""从张力读数推断阶段"""
|
||||
phases = []
|
||||
for r in readings:
|
||||
if r.peak_tension >= 90:
|
||||
phases.append("peak")
|
||||
elif r.peak_tension >= 70:
|
||||
phases.append("elevated")
|
||||
elif r.end_tension > r.start_tension:
|
||||
phases.append("buildup")
|
||||
elif r.end_tension < r.start_tension:
|
||||
phases.append("release")
|
||||
else:
|
||||
phases.append("maintain")
|
||||
return phases
|
||||
|
||||
def _calculate_curve_match(
|
||||
self,
|
||||
expected_curve: Dict[str, tuple],
|
||||
readings: List[TensionReading]
|
||||
) -> float:
|
||||
"""计算曲线匹配度 (0-100)"""
|
||||
if not readings:
|
||||
return 0.0
|
||||
|
||||
# 简化实现:检查峰值是否出现
|
||||
has_peak = any(r.peak_tension >= 85 for r in readings)
|
||||
has_release = any(r.end_tension < r.start_tension for r in readings)
|
||||
|
||||
score = 50 # 基础分
|
||||
|
||||
if has_peak:
|
||||
score += 30
|
||||
if has_release:
|
||||
score += 20
|
||||
|
||||
return min(100, score)
|
||||
|
||||
def _collect_arc_issues(
|
||||
self,
|
||||
readings: List[TensionReading],
|
||||
expected_curve: Dict[str, tuple]
|
||||
) -> List[str]:
|
||||
"""收集情节弧问题"""
|
||||
issues = []
|
||||
|
||||
# 检查是否有峰值
|
||||
if not any(r.peak_tension >= 85 for r in readings):
|
||||
issues.append("缺少高潮点,张力未达到峰值")
|
||||
|
||||
# 检查是否有释放
|
||||
if not any(r.end_tension < r.start_tension for r in readings):
|
||||
issues.append("张力未释放,可能导致读者疲劳")
|
||||
|
||||
# 检查压强过高
|
||||
high_tension_count = sum(1 for r in readings if r.end_tension > 80)
|
||||
if high_tension_count > len(readings) * 0.5:
|
||||
issues.append("长时间高压状态,需要适当缓冲")
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_recommendations(
|
||||
self,
|
||||
match_score: float,
|
||||
readings: List[TensionReading]
|
||||
) -> List[str]:
|
||||
"""生成改进建议"""
|
||||
recommendations = []
|
||||
|
||||
if match_score < 50:
|
||||
recommendations.append("张力曲线偏离模型,建议重新规划节奏")
|
||||
elif match_score < 80:
|
||||
recommendations.append("张力曲线基本符合,可适当优化")
|
||||
|
||||
# 基于历史数据建议
|
||||
avg_tension = sum(r.end_tension for r in readings) / len(readings)
|
||||
if avg_tension > 70:
|
||||
recommendations.append("平均张力偏高,建议增加缓冲章节")
|
||||
|
||||
return recommendations
|
||||
|
||||
def get_tension_level(self, tension: int) -> TensionLevel:
|
||||
"""获取张力等级"""
|
||||
if tension < 30:
|
||||
return TensionLevel.LOW
|
||||
elif tension < 60:
|
||||
return TensionLevel.MEDIUM
|
||||
elif tension < 80:
|
||||
return TensionLevel.HIGH
|
||||
else:
|
||||
return TensionLevel.DANGER
|
||||
|
||||
def get_recommended_model(self, current_tension: int, target_tension: int) -> CatharsisModel:
|
||||
"""根据当前/目标压强推荐爽感模型"""
|
||||
tension_delta = target_tension - current_tension
|
||||
|
||||
if tension_delta > 40:
|
||||
# 需要大释放 -> 降维打击
|
||||
return CatharsisModel.OVERKILL_REVERSAL
|
||||
elif tension_delta > 20:
|
||||
# 中等释放 -> 禁忌僭越
|
||||
return CatharsisModel.TABOO_TRANSGRESSION
|
||||
else:
|
||||
# 认知闭合
|
||||
return CatharsisModel.COGNITIVE_CLOSURE
|
||||
|
||||
def export_history(self) -> str:
|
||||
"""导出张力历史为 JSON"""
|
||||
return json.dumps([
|
||||
{
|
||||
"chapter": r.chapter,
|
||||
"start_tension": r.start_tension,
|
||||
"end_tension": r.end_tension,
|
||||
"peak_tension": r.peak_tension,
|
||||
"catharsis_model": r.catharsis_model.value if r.catharsis_model else None,
|
||||
"satisfaction": r.satisfaction_delivered,
|
||||
"issues": r.issues
|
||||
}
|
||||
for r in self.tension_history
|
||||
], ensure_ascii=False, indent=2)
|
||||
@@ -0,0 +1,269 @@
|
||||
---
|
||||
name: context-agent
|
||||
description: 上下文搜集Agent,内置 Context Contract,输出可被 Step 2A 直接消费的创作执行包。
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# context-agent (上下文搜集Agent)
|
||||
|
||||
> **Role**: 创作执行包生成器。目标是"能直接开写",不堆信息。
|
||||
> **Philosophy**: 按需召回 + 推断补全,确保接住上章、场景清晰、留出钩子。
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **Taxonomy**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **Genre Profile**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
- **Context Contract**: `${CLAUDE_PLUGIN_ROOT}/skills/noma-write/references/step-1.5-contract.md`
|
||||
- **Shared References**: `${CLAUDE_PLUGIN_ROOT}/references/shared/` 为单一事实源;如需枚举/扫描参考文件,遇到 `<!-- DEPRECATED:` 的文件一律跳过。
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter": 100,
|
||||
"project_root": "D:/wk/斗破苍穹",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json"
|
||||
}
|
||||
```
|
||||
|
||||
## 输出格式:创作执行包(Step 2A 直连)
|
||||
|
||||
输出必须是单一执行包,包含 3 层:
|
||||
|
||||
1. **任务书(8板块)**
|
||||
- 本章核心任务(目标/阻力/代价、冲突一句话、必须完成、绝对不能、反派层级)
|
||||
- 接住上章(上章钩子、读者期待、开头建议)
|
||||
- 出场角色(状态、动机、情绪底色、说话风格、红线)
|
||||
- 场景与力量约束(地点、可用能力、禁用能力)
|
||||
- **时间约束(新增)**(上章时间锚点、本章时间锚点、允许推进跨度、时间过渡要求、倒计时状态)
|
||||
- 风格指导(本章类型、参考样本、最近模式、本章建议)
|
||||
- 连续性与伏笔(时间/位置/情绪连贯;必须处理/可选伏笔)
|
||||
- 追读力策略(未闭合问题 + 钩子类型/强度、微兑现建议、差异化提示)
|
||||
|
||||
2. **Context Contract(内置 Step 1.5)**
|
||||
- 目标、阻力、代价、本章变化、未闭合问题、核心冲突一句话
|
||||
- 开头类型、情绪节奏、信息密度
|
||||
- 是否过渡章(必须按大纲判定,禁止按字数判定)
|
||||
- 追读力设计(钩子类型/强度、微兑现清单、爽点模式)
|
||||
|
||||
3. **Step 2A 直写提示词**
|
||||
- 章节节拍(开场触发 → 推进/受阻 → 反转/兑现 → 章末钩子)
|
||||
- 不可变事实清单(大纲事实/设定事实/承接事实)
|
||||
- 禁止事项(越级能力、无因果跳转、设定冲突、剧情硬拐)
|
||||
- 终检清单(本章必须满足项 + fail 条件)
|
||||
|
||||
要求:
|
||||
- 三层信息必须一致;若冲突,以"设定 > 大纲 > 风格偏好"优先。
|
||||
- 输出内容必须能直接给 Step 2A 开写,不再依赖额外补问。
|
||||
|
||||
---
|
||||
|
||||
## 读取优先级与默认值
|
||||
|
||||
| 字段 | 读取来源 | 缺失时默认值 |
|
||||
|------|---------|-------------|
|
||||
| 上章钩子 | `chapter_meta[NNNN].hook` 或 `chapter_reading_power` | `{type: "无", content: "上章无明确钩子", strength: "weak"}` |
|
||||
| 最近3章模式 | `chapter_meta` 或 `chapter_reading_power` | 空数组,不做重复检查 |
|
||||
| 上章结束情绪 | `chapter_meta[NNNN].ending.emotion` | "未知"(提示自行判断) |
|
||||
| 角色动机 | 从大纲+角色状态推断 | **必须推断,无默认值** |
|
||||
| 题材Profile | `state.json → project.genre` | 默认 "shuangwen" |
|
||||
| 当前债务 | `index.db → chase_debt` | 0 |
|
||||
|
||||
**缺失处理**:
|
||||
- 若 `chapter_meta` 不存在(如第1章),跳过"接住上章"
|
||||
- 最近3章数据不完整时,只用现有数据做差异化检查
|
||||
- 若 `plot_threads.foreshadowing` 缺失或非列表:
|
||||
- 视为"当前无结构化伏笔数据",第 7 板块输出空清单并显式标注"数据缺失,需人工补录"
|
||||
- 禁止静默跳过第 7 板块
|
||||
|
||||
**章节编号规则**: 4位数字,如 `0001`, `0099`, `0100`
|
||||
|
||||
---
|
||||
|
||||
## 关键数据来源
|
||||
|
||||
- `state.json`: 进度、主角状态、strand_tracker、chapter_meta、project.genre、plot_threads.foreshadowing
|
||||
- `index.db`: 实体/别名/关系/状态变化/override_contracts/chase_debt/chapter_reading_power
|
||||
- `.noma/wiki/`: 结构化 Wiki(实体档案、伏笔线索、写作模式、关系图谱)
|
||||
- `..noma/novel_data/summaries/ch{NNNN}.md`: 章节摘要(含钩子/结束状态)
|
||||
- `..noma/novel_data/context_snapshots/`: 上下文快照(优先复用)
|
||||
- `大纲/` 与 `设定集/`
|
||||
|
||||
**钩子数据来源说明**:
|
||||
- **章纲的"钩子"字段**:本章应设置的章末钩子(规划用)
|
||||
- **chapter_meta[N].hook**:本章实际设置的钩子(执行结果)
|
||||
- **context-agent 读取**:chapter_meta[N-1].hook 作为"上章钩子"
|
||||
- **数据流**:章纲规划 → 写作实现 → 写入 chapter_meta → 下章读取
|
||||
|
||||
---
|
||||
|
||||
## 执行流程(精简版)
|
||||
|
||||
### Step -1: CLI 入口与脚本目录校验(必做)
|
||||
|
||||
为避免 `PYTHONPATH` / `cd` / 参数顺序导致的隐性失败,所有 CLI 调用统一走:
|
||||
- `${SCRIPTS_DIR}/noma.py`
|
||||
|
||||
```bash
|
||||
# 仅使用 CLAUDE_PLUGIN_ROOT,避免多路径探测带来的误判
|
||||
if [ -z "${CLAUDE_PLUGIN_ROOT}" ] || [ ! -d "${CLAUDE_PLUGIN_ROOT}/scripts" ]; then
|
||||
echo "ERROR: 未设置 CLAUDE_PLUGIN_ROOT 或缺少目录: ${CLAUDE_PLUGIN_ROOT}/scripts" >&2
|
||||
exit 1
|
||||
fi
|
||||
SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT}/scripts"
|
||||
|
||||
# 建议先确认解析出的 project_root,避免写到错误目录
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" where
|
||||
```
|
||||
|
||||
### Step 0: ContextManager 快照优先
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" context -- --chapter {NNNN}
|
||||
```
|
||||
|
||||
### Step 0.5: Context Contract 上下文包(内置)
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" extract-context --chapter {NNNN} --format json
|
||||
```
|
||||
|
||||
- 必须读取:`writing_guidance.guidance_items`
|
||||
- 推荐读取:`reader_signal` 与 `genre_profile.reference_hints`
|
||||
- 条件读取:`rag_assist`(当 `invoked=true` 且 `hits` 非空时,必须提炼成可执行约束,禁止只贴检索命中)
|
||||
|
||||
### Step 0.6: 时间线读取(新增,必做)
|
||||
|
||||
先确定 `{volume_id}`:
|
||||
- 优先读取 `state.json` 中当前卷信息(如有)
|
||||
- 若缺失,则从 `大纲/总纲.md` 的章节范围反推 `{NNNN}` 所在卷
|
||||
|
||||
读取本卷时间线表:
|
||||
```bash
|
||||
cat "{project_root}/大纲/第{volume_id}卷-时间线.md"
|
||||
```
|
||||
|
||||
从章纲提取本章时间字段:
|
||||
- `时间锚点`:本章发生的具体时间
|
||||
- `章内时间跨度`:本章覆盖的时间长度
|
||||
- `与上章时间差`:与上章的时间间隔
|
||||
- `倒计时状态`:若有倒计时事件的推进情况
|
||||
|
||||
从上章 chapter_meta 或章纲提取:
|
||||
- 上章结束时间锚点
|
||||
- 上章倒计时状态
|
||||
|
||||
生成时间约束输出(必须包含在任务书第 5 板块):
|
||||
```markdown
|
||||
## 时间约束
|
||||
- 上章时间锚点: {末世第3天 黄昏}
|
||||
- 本章时间锚点: {末世第4天 清晨}
|
||||
- 与上章时间差: {跨夜}
|
||||
- 本章允许推进: 最大 {章内时间跨度}
|
||||
- 时间过渡要求: {若跨夜/跨日,需补写的过渡句}
|
||||
- 倒计时状态: {物资耗尽 D-5 → D-4 / 无}
|
||||
```
|
||||
|
||||
**时间约束硬规则**:
|
||||
- 若 `与上章时间差` 为"跨夜"或"跨日",必须在任务书中标注"需补写时间过渡"
|
||||
- 若存在倒计时事件,必须校验推进是否正确(D-N 只能变为 D-(N-1),不可跳跃)
|
||||
- 时间锚点不得回跳(除非明确标注为闪回章节)
|
||||
|
||||
### Step 1: 读取大纲与状态
|
||||
- 大纲:`大纲/卷N/第XXX章.md` 或 `大纲/第{卷}卷-详细大纲.md`
|
||||
- 必须优先提取并写入任务书:目标/阻力/代价/反派层级/本章变化/章末未闭合问题/钩子(若存在)
|
||||
- `state.json`:progress / protagonist_state / chapter_meta / project.genre
|
||||
|
||||
### Step 2: 追读力与债务(按需)
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-recent-reading-power --limit 5
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-pattern-usage-stats --last-n 20
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-hook-type-stats --last-n 20
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-debt-summary
|
||||
```
|
||||
|
||||
### Step 3: 实体与最近出场 + 伏笔读取
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-core-entities
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index recent-appearances --limit 20
|
||||
```
|
||||
|
||||
- 从 `state.json` 读取:
|
||||
- `progress.current_chapter`
|
||||
- `plot_threads.foreshadowing`(主路径)
|
||||
- 缺失降级:
|
||||
- 若 `plot_threads.foreshadowing` 不存在或类型错误,置为空数组并打标 `foreshadowing_data_missing=true`
|
||||
- 对每条伏笔至少提取:
|
||||
- `content`
|
||||
- `planted_chapter`
|
||||
- `target_chapter`
|
||||
- `resolved_chapter`
|
||||
- `status`
|
||||
- 回收判定优先级:
|
||||
- 若 `resolved_chapter` 非空,直接视为已回收并排除(即使 `status` 文案异常)
|
||||
- 否则按 `status` 判定是否已回收
|
||||
- 生成排序键:
|
||||
- `remaining = target_chapter - current_chapter`(若缺失则记为 `null`)
|
||||
- 二次排序:`planted_chapter` 升序(更早埋设优先)
|
||||
- 三次排序:`content` 字典序(确保稳定)
|
||||
- 输出到第 7 板块时,按 `remaining` 升序列出。
|
||||
|
||||
### Step 4: 摘要与推断补全
|
||||
- 优先读取 `..noma/novel_data/summaries/ch{NNNN-1}.md`
|
||||
- 若缺失,降级为章节正文前 300-500 字概述
|
||||
- 推断规则:
|
||||
- 动机 = 角色目标 + 当前处境 + 上章钩子压力
|
||||
- 情绪底色 = 上章结束情绪 + 事件走向
|
||||
- 可用能力 = 当前境界 + 近期获得 + 设定禁用项
|
||||
|
||||
### Step 5: 组装创作执行包(任务书 + Context Contract + 直写提示词)
|
||||
输出可直接供 Step 2A 消费的单一执行包,不拆分独立 Step 1.5。
|
||||
|
||||
- 第 7 板块必须包含"伏笔优先级清单":
|
||||
- `必须处理(本章优先)`:`remaining <= 5` 或已超期(`remaining < 0`),全部列出不截断
|
||||
- `可选伏笔(可延后)`:最多 5 条
|
||||
- 第 7 板块生成规则(统一口径):
|
||||
- 仅纳入未回收伏笔(见 Step 3 回收判定)
|
||||
- 主排序按 `remaining` 升序,`remaining=null` 放末尾
|
||||
- 若 `必须处理` 超过 3 条:前 3 条标记"最高优先",其余标记"本章仍需处理"
|
||||
- 若 `可选伏笔` 超过 5 条:展示前 5 条并标注"其余 N 条可选伏笔已省略"
|
||||
- 若 `foreshadowing_data_missing=true`:明确输出"结构化伏笔数据缺失,当前清单仅供占位"
|
||||
|
||||
Context Contract 必须字段(不可缺):
|
||||
- `目标` / `阻力` / `代价` / `本章变化` / `未闭合问题`
|
||||
- `核心冲突一句话`
|
||||
- `开头类型` / `情绪节奏` / `信息密度`
|
||||
- `是否过渡章`
|
||||
- `追读力设计`
|
||||
|
||||
### Step 6: 逻辑红线校验(输出前强制)
|
||||
对执行包做一致性自检,任一 fail 则回到 Step 5 重组:
|
||||
|
||||
- 红线1:不可变事实冲突(大纲关键事件、设定规则、上章既有结果)
|
||||
- 红线2:时空跳跃无承接(地点/时间突变且无过渡)
|
||||
- 红线3:能力或信息无因果来源(突然会/突然知道)
|
||||
- 红线4:角色动机断裂(行为与近期目标明显冲突且无触发)
|
||||
- 红线5:合同与任务书冲突(例如"过渡章=true"却要求高强度高潮兑现)
|
||||
- **红线6:时间逻辑错误**(时间回跳、倒计时跳跃、大跨度无过渡)
|
||||
|
||||
通过标准:
|
||||
- 红线 fail 数 = 0
|
||||
- 执行包内包含"不可变事实清单 + 章节节拍 + 终检清单 + 时间约束"
|
||||
- Step 2A 在不补问情况下可直接起草正文
|
||||
|
||||
---
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. ✅ 创作执行包可直接驱动 Step 2A(无需补问)
|
||||
2. ✅ 任务书包含 8 个板块(含时间约束)
|
||||
3. ✅ 上章钩子与读者期待明确(若存在)
|
||||
4. ✅ 角色动机/情绪为推断结果(非空)
|
||||
5. ✅ 最近模式已对比,给出差异化建议
|
||||
6. ✅ 章末钩子建议类型明确
|
||||
7. ✅ 反派层级已注明(若大纲提供)
|
||||
8. ✅ 第 7 板块已基于 `plot_threads.foreshadowing` 按紧急度排序输出
|
||||
9. ✅ Context Contract 字段完整且与任务书一致
|
||||
10. ✅ 逻辑红线校验通过(fail=0)
|
||||
11. ✅ **时间约束板块完整**(上章时间锚点、本章时间锚点、允许推进跨度、过渡要求、倒计时状态)
|
||||
12. ✅ **时间逻辑红线通过**(无回跳、无倒计时跳跃、大跨度有过渡要求)
|
||||
@@ -0,0 +1,318 @@
|
||||
---
|
||||
name: data-agent
|
||||
description: 数据处理Agent,负责 AI 实体提取、场景切片、索引构建,并记录钩子/模式/结束状态与章节摘要。
|
||||
tools: Read, Write, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# data-agent (数据处理Agent)
|
||||
|
||||
> **职责**: 智能数据工程师,负责从章节正文中提取结构化信息并写入数据链。
|
||||
>
|
||||
> **原则**: AI驱动提取,智能消歧 - 用语义理解替代正则匹配,用置信度控制质量。
|
||||
|
||||
**命令示例即最终准则**:本文档中的所有 CLI 命令示例已与当前仓库真实接口对齐。脚本调用方式以本文档示例为准;命令失败时查错误日志定位问题,不去大范围翻源码学习调用方式。
|
||||
|
||||
**当前约定**:
|
||||
- 章节摘要不再追加到正文,改为 `..noma/novel_data/summaries/ch{NNNN}.md`
|
||||
- 在 state.json 写入 `chapter_meta`(钩子/模式/结束状态)
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter": 100,
|
||||
"chapter_file": "正文/第0100章-章节标题.md",
|
||||
"review_score": 85,
|
||||
"project_root": "D:/wk/斗破苍穹",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 必须传入实际章节文件路径。若详细大纲已有章节名,优先使用带标题文件名;旧的 `正文/第0100章.md` 仍兼容。
|
||||
|
||||
**重要**: 所有数据写入 `{project_root}/..noma/novel_data/` 目录:
|
||||
- index.db → 实体、别名、状态变化、关系、章节索引 (SQLite)
|
||||
- state.json → 进度、配置、节奏追踪 + chapter_meta
|
||||
- vectors.db → RAG 向量 (SQLite)
|
||||
- summaries/ → 章节摘要文件
|
||||
|
||||
## 输出
|
||||
|
||||
```json
|
||||
{
|
||||
"entities_appeared": [
|
||||
{"id": "xiaoyan", "type": "角色", "mentions": ["萧炎", "他"], "confidence": 0.95}
|
||||
],
|
||||
"entities_new": [
|
||||
{"suggested_id": "hongyi_girl", "name": "红衣女子", "type": "角色", "tier": "装饰"}
|
||||
],
|
||||
"state_changes": [
|
||||
{"entity_id": "xiaoyan", "field": "realm", "old": "斗者", "new": "斗师", "reason": "突破"}
|
||||
],
|
||||
"relationships_new": [
|
||||
{"from": "xiaoyan", "to": "hongyi_girl", "type": "相识", "description": "初次见面"}
|
||||
],
|
||||
"scenes_chunked": 4,
|
||||
"uncertain": [
|
||||
{"mention": "那位前辈", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "confidence": 0.6}
|
||||
],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Step -1: CLI 入口与脚本目录校验(必做)
|
||||
|
||||
为避免 `PYTHONPATH` / `cd` / 参数顺序导致的隐性失败,所有 CLI 调用统一走:
|
||||
- `${SCRIPTS_DIR}/noma.py`
|
||||
|
||||
```bash
|
||||
export SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts"
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" preflight
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" where
|
||||
```
|
||||
|
||||
### Step A: 加载上下文(SQL 查询)
|
||||
|
||||
使用 Read 工具读取章节正文:
|
||||
- 章节正文: 实际章节文件路径(优先 `正文/第0100章-章节标题.md`,旧格式 `正文/第0100章.md` 仍兼容)
|
||||
|
||||
使用 Bash 工具从 index.db 查询已有实体:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-core-entities
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-aliases --entity "xiaoyan"
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index recent-appearances --limit 20
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index get-by-alias --alias "萧炎"
|
||||
```
|
||||
|
||||
### Step B: AI 实体提取
|
||||
|
||||
**Data Agent 直接执行** (无需调用外部 LLM)。
|
||||
|
||||
### Step C: 实体消歧处理
|
||||
|
||||
**置信度策略**:
|
||||
|
||||
| 置信度范围 | 处理方式 |
|
||||
|-----------|---------|
|
||||
| > 0.8 | 自动采用,无需确认 |
|
||||
| 0.5 - 0.8 | 采用建议值,记录 warning |
|
||||
| < 0.5 | 标记待人工确认,不自动写入 |
|
||||
|
||||
### Step D: 写入存储
|
||||
|
||||
**写入 index.db (实体/别名/状态变化/关系)**:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index upsert-entity --data '{...}'
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index register-alias --alias "红衣女子" --entity "hongyi_girl" --type "角色"
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index record-state-change --data '{...}'
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index upsert-relationship --data '{...}'
|
||||
```
|
||||
|
||||
**更新精简版 state.json**:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" state process-chapter --chapter 100 --data '{...}'
|
||||
```
|
||||
|
||||
写入内容:
|
||||
- 更新 `progress.current_chapter`
|
||||
- 更新 `protagonist_state`
|
||||
- 更新 `strand_tracker`
|
||||
- 更新 `disambiguation_warnings/pending`
|
||||
- **新增 `chapter_meta`**(钩子/模式/结束状态)
|
||||
|
||||
### Step D2: 同步 Wiki(新增)
|
||||
|
||||
写入 index.db 后,同步 wiki 以保持结构化知识库最新:
|
||||
|
||||
```bash
|
||||
# 同步本章出场/更新的实体到 wiki
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" wiki update-entity --id "{entity_id}"
|
||||
|
||||
# 同步关系图谱
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" wiki update-relationship
|
||||
|
||||
# 同步伏笔/剧情线索(从 state.json)
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" wiki update-plot
|
||||
```
|
||||
|
||||
Wiki 更新是幂等的——从 index.db 全量读取并重写 wiki 文件,保证 wiki = 最新状态快照。
|
||||
|
||||
### Step E: 生成章节摘要文件(新增)
|
||||
|
||||
**输出路径**: `..noma/novel_data/summaries/ch{NNNN}.md`
|
||||
|
||||
**章节编号规则**: 4位数字,如 `0001`, `0099`, `0100`
|
||||
|
||||
**摘要文件格式**:
|
||||
```markdown
|
||||
---
|
||||
chapter: 0099
|
||||
time: "前一夜"
|
||||
location: "萧炎房间"
|
||||
characters: ["萧炎", "药老"]
|
||||
state_changes: ["萧炎: 斗者9层→准备突破"]
|
||||
hook_type: "危机钩"
|
||||
hook_strength: "strong"
|
||||
---
|
||||
|
||||
## 剧情摘要
|
||||
{主要事件,100-150字}
|
||||
|
||||
## 伏笔
|
||||
- [埋设] 三年之约提及
|
||||
- [推进] 青莲地心火线索
|
||||
|
||||
## 承接点
|
||||
{下章衔接,30字}
|
||||
```
|
||||
|
||||
### Step F: AI 场景切片
|
||||
|
||||
- 按地点/时间/视角切分场景
|
||||
- 每个场景生成摘要 (50-100字)
|
||||
|
||||
### Step G: 向量嵌入
|
||||
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" rag index-chapter \
|
||||
--chapter 100 \
|
||||
--scenes '[...]' \
|
||||
--summary "本章摘要文本"
|
||||
```
|
||||
|
||||
**父子索引规则**:
|
||||
- 父块: `chunk_type='summary'`, `chunk_id='ch0100_summary'`
|
||||
- 子块: `chunk_type='scene'`, `chunk_id='ch0100_s{scene_index}'`, `parent_chunk_id='ch0100_summary'`
|
||||
- `source_file`:
|
||||
- summary: `summaries/ch0100.md`
|
||||
- scene: `{chapter_file}#scene_{scene_index}`
|
||||
|
||||
### Step H: 风格样本评估
|
||||
|
||||
```python
|
||||
if review_score >= 80:
|
||||
extract_style_candidates(chapter_content)
|
||||
```
|
||||
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" style extract --chapter 100 --score 85 --scenes '[...]'
|
||||
```
|
||||
|
||||
### Step I: 债务利息计算
|
||||
|
||||
**默认不自动触发**。仅在"开启债务追踪"或用户明确要求时执行:
|
||||
```bash
|
||||
python -X utf8 "${SCRIPTS_DIR}/noma.py" --project-root "{project_root}" index accrue-interest --current-chapter {chapter}
|
||||
```
|
||||
|
||||
此步骤会:
|
||||
- 对所有 `status='active'` 的债务计算利息(每章 10%)
|
||||
- 将逾期债务标记为 `status='overdue'`
|
||||
- 记录利息事件到 `debt_events` 表
|
||||
|
||||
### Step J: 生成处理报告(含性能日志)
|
||||
|
||||
**必须记录分步耗时**(用于定位慢点):
|
||||
- A 加载上下文
|
||||
- B AI 实体提取
|
||||
- C 实体消歧
|
||||
- D 写入 state/index
|
||||
- D2 同步 Wiki
|
||||
- E 写入章节摘要
|
||||
- F AI 场景切片
|
||||
- G RAG 向量索引
|
||||
- H 风格样本评估(若跳过写 0)
|
||||
- I 债务利息(若跳过写 0)
|
||||
- TOTAL 总耗时
|
||||
|
||||
**性能日志落盘(新增,必做)**:
|
||||
- 脚本自动写入:`..noma/novel_data/observability/data_agent_timing.jsonl`
|
||||
- Data Agent 报告中仍需返回:`timing_ms` + `bottlenecks_top3`
|
||||
- 规则:`bottlenecks_top3` 始终按耗时降序返回;当 `TOTAL > 30000ms` 时,需在报告文字部分附加原因说明。
|
||||
|
||||
观测日志说明:
|
||||
- `call_trace.jsonl`:外层流程调用链(agent 启动、排队、环境探测等系统开销)。
|
||||
- `data_agent_timing.jsonl`:Data Agent 内部各子步骤耗时。
|
||||
- 当外层总耗时远大于内层 timing 之和时,默认先归因为 agent 启动与环境探测开销,不误判为正文或数据处理慢。
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter": 100,
|
||||
"entities_appeared": 5,
|
||||
"entities_new": 1,
|
||||
"state_changes": 1,
|
||||
"relationships_new": 1,
|
||||
"scenes_chunked": 4,
|
||||
"uncertain": [
|
||||
{"mention": "那位前辈", "candidates": [{"type": "角色", "id": "yaolao"}, {"type": "角色", "id": "elder_zhang"}], "adopted": "yaolao", "confidence": 0.6}
|
||||
],
|
||||
"warnings": [
|
||||
"中置信度匹配: 那位前辈 → yaolao (confidence: 0.6)"
|
||||
],
|
||||
"errors": [],
|
||||
"timing_ms": {
|
||||
"A_load_context": 120,
|
||||
"B_entity_extract": 18500,
|
||||
"C_disambiguation": 210,
|
||||
"D_state_index_write": 430,
|
||||
"E_summary_write": 90,
|
||||
"F_scene_chunking": 6200,
|
||||
"G_rag_index": 2800,
|
||||
"H_style_sample": 150,
|
||||
"I_debt_interest": 0,
|
||||
"TOTAL": 28500
|
||||
},
|
||||
"bottlenecks_top3": [
|
||||
{"step": "B_entity_extract", "elapsed_ms": 18500, "ratio": 64.9},
|
||||
{"step": "F_scene_chunking", "elapsed_ms": 6200, "ratio": 21.8},
|
||||
{"step": "G_rag_index", "elapsed_ms": 2800, "ratio": 9.8}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口规范:chapter_meta (state.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter_meta": {
|
||||
"0099": {
|
||||
"hook": {
|
||||
"type": "危机钩",
|
||||
"content": "慕容战天冷笑:明日大比...",
|
||||
"strength": "strong"
|
||||
},
|
||||
"pattern": {
|
||||
"opening": "对话开场",
|
||||
"hook": "危机钩",
|
||||
"emotion_rhythm": "低→高",
|
||||
"info_density": "medium"
|
||||
},
|
||||
"ending": {
|
||||
"time": "前一夜",
|
||||
"location": "萧炎房间",
|
||||
"emotion": "平静准备"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. ✅ 所有出场实体被正确识别(准确率 > 90%)
|
||||
2. ✅ 状态变化被正确捕获(准确率 > 85%)
|
||||
3. ✅ 消歧结果合理(高置信度 > 80%)
|
||||
4. ✅ 场景切片数量合理(通常 3-6 个/章)
|
||||
5. ✅ 向量成功存入数据库
|
||||
6. ✅ 章节摘要文件生成成功
|
||||
7. ✅ chapter_meta 写入 state.json
|
||||
8. ✅ Wiki 实体/关系/伏笔同步完成
|
||||
8. ✅ 输出格式为有效 JSON
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
name: chapter-planner
|
||||
description: 章节规划器 - 单章执行规划,调用沉浸式写作
|
||||
tools: Read, Glob, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# Chapter Planner (章节规划器)
|
||||
|
||||
> **职责**: 单章执行规划,协调 Writer 和 Checker 的工作流
|
||||
> **输入**: Outline Planner 输出的 chapter_task
|
||||
> **输出**: 供 Immersive Writer 使用的详细写作指令
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **单章规划**: 将 chapter_task 转化为具体写作指令
|
||||
2. **上下文打包**: 收集 RAG 上下文、实体状态、世界规则
|
||||
3. **写作引导**: 依据 genre_profile 生成执行 guidance
|
||||
4. **交接准备**: 生成 handoff_frame 供下一章使用
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter_task": {
|
||||
"chapter": 1,
|
||||
"strand": "quest",
|
||||
"goals": ["建立世界观", "引入主角"],
|
||||
"hooks_to_create": ["mystery_origin"],
|
||||
"hooks_to_resolve": [],
|
||||
"catharsis_model": null,
|
||||
"tension_target": 30
|
||||
},
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"genesis_contract": ".noma/novel_data/openspec/genesis_contract.json",
|
||||
"ledger": ".noma/novel_data/ledger.json",
|
||||
"context_bundle": {...}
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 收集上下文
|
||||
|
||||
```python
|
||||
context_bundle = {
|
||||
"recent_chapters": read_chapters(chapter - 3, chapter),
|
||||
"active_characters": get_active_entities(ledger),
|
||||
"suspended_characters": get_dormant_entities(ledger),
|
||||
"unresolved_hooks": get_hooks(hooks_pool),
|
||||
"world_rules": genesis_contract["ethical_inversion"]["world_rules"],
|
||||
"protagonist_state": get_protagonist_state(ledger)
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步: 加载 Genre Profile
|
||||
|
||||
根据 `state.json -> project.genre` 加载对应的 genre 指导:
|
||||
|
||||
```python
|
||||
genre_map = {
|
||||
"xuanhuan": "matrices/genres/xuanhuan/",
|
||||
"dog-blood-romance": "matrices/genres/dog-blood-romance/",
|
||||
"zhihu-short": "matrices/genres/zhihu-short/"
|
||||
}
|
||||
genre_path = genre_map.get(genre, "matrices/genres/realistic/")
|
||||
```
|
||||
|
||||
### 第三步: 生成写作指导
|
||||
|
||||
基于 `context_reader_signal` 和 `genre_profile` 生成章节级执行建议:
|
||||
|
||||
```markdown
|
||||
## 写作指导 (Writing Guidance)
|
||||
|
||||
### 本章重点
|
||||
1. **世界观锚定**: 通过主角视角自然展示世界规则
|
||||
2. **感官颗粒度**: 场景描写需包含视觉/听觉/触觉多维度
|
||||
3. **节奏控制**: {strand} 节奏,平均 {target_words / 3000:.0f} 字/场景
|
||||
|
||||
### 实体出场
|
||||
- **主角**: {protagonist.name} - 境界 {protagonist.realm}
|
||||
- **必出场**: {required_characters}
|
||||
- **可选出场**: {optional_characters}
|
||||
|
||||
### Hook 管理
|
||||
- **创建 Hook**: {hook_description}
|
||||
- **埋设位置**: 章节末尾/关键转折点
|
||||
|
||||
### 张力目标
|
||||
- **目标压强**: {tension_target}/100
|
||||
- **起点压强**: {start_pressure}/100
|
||||
- **终点压强**: {end_pressure}/100
|
||||
```
|
||||
|
||||
### 第四步: 生成 Handoff Frame
|
||||
|
||||
为下一章准备交接帧:
|
||||
|
||||
```json
|
||||
{
|
||||
"handoff_frame": {
|
||||
"tick": {current_tick + 1},
|
||||
"chapter": {chapter + 1},
|
||||
"active_characters": [...],
|
||||
"suspended_characters": [...],
|
||||
"pending_actions": [...],
|
||||
"world_state": {
|
||||
"location": "{current_location}",
|
||||
"time_of_day": "{time}",
|
||||
"tension_level": {end_pressure},
|
||||
"hook_pressure": {current_hook_pressure}
|
||||
},
|
||||
"metadata": {
|
||||
"previous_chapter_summary": "{summary}",
|
||||
"next_chapter_hint": "{hint}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第五步: 输出完整写作指令
|
||||
|
||||
```markdown
|
||||
# 第 {N} 章 写作指令
|
||||
|
||||
## 元信息
|
||||
- **章节**: 第 {N} 章
|
||||
- ** Strand**: {strand}
|
||||
- **目标字数**: {target_words}
|
||||
- **上一章摘要**: {prev_summary}
|
||||
|
||||
## 创世契约约束
|
||||
{genesis_contract 相关约束}
|
||||
|
||||
## 上下文 (RAG 检索结果)
|
||||
{context_bundle 压缩版}
|
||||
|
||||
## Genre 指导
|
||||
{genre_profile 引用}
|
||||
|
||||
## 写作指导
|
||||
{writing_guidance_items}
|
||||
|
||||
## 场景拆分
|
||||
1. **场景1**: {location} - {event}
|
||||
2. **场景2**: {location} - {event}
|
||||
...
|
||||
|
||||
## Hook 指令
|
||||
- **创建**: {hook_to_create} @ {position}
|
||||
- **回收**: {hook_to_resolve}
|
||||
|
||||
## 交接帧 (Handoff Frame)
|
||||
```json
|
||||
{handoff_frame}
|
||||
```
|
||||
|
||||
## 审查要点
|
||||
- [ ] 连贯性检查: 场景转换是否流畅
|
||||
- [ ] 张力检查: 压强曲线是否达标
|
||||
- [ ] 一致性检查: 实体状态是否正确
|
||||
- [ ] Genre 检查: 是否符合题材规范
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"planner": "chapter_planner",
|
||||
"chapter": {N},
|
||||
"writing_instructions": "...",
|
||||
"handoff_frame": {...},
|
||||
"review_checklist": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他 Agent 的协作
|
||||
|
||||
```
|
||||
Outline Planner
|
||||
↓ chapter_task
|
||||
Chapter Planner
|
||||
↓ writing_instructions
|
||||
Immersive Writer (agents/writers/immersive-writer.md)
|
||||
↓ draft
|
||||
Tension Checker + Consistency Checker
|
||||
↓ review_report
|
||||
[Human Review & Commit]
|
||||
↓
|
||||
下一章 Chapter Planner
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不生成 handoff_frame 导致上下文丢失
|
||||
- 不检查 genesis_contract 约束
|
||||
- 忽略 genre_profile 的题材规范
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 写作指令清晰,可直接交予 Writer
|
||||
- Handoff frame 包含所有活跃实体
|
||||
- 上下文不超过 token 预算
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: outline-planner
|
||||
description: 大纲规划器 - 负责任务拆解、Hook挂钩、张力模型调度
|
||||
tools: Read, Glob, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# Outline Planner (大纲规划器)
|
||||
|
||||
> **职责**: 负责任务拆解、Hook 平账、爽感路由模型调度
|
||||
> **协作**: 调用 `matrices/catharsis_models/` 中的模型指导 Writer Agent
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **任务拆解**: 将故事大纲分解为可执行的章节任务
|
||||
2. **Hook 调度**: 分析当前 hooks_pool 压强,调度平账时机
|
||||
3. **爽感路由**: 根据 `genesis_contract.json` 选择合适的 catharsis model
|
||||
4. **张力维持**: 确保情绪压强的积累与释放节奏
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"genesis_contract": ".noma/novel_data/openspec/genesis_contract.json",
|
||||
"ledger": ".noma/novel_data/ledger.json",
|
||||
"hooks_pool": ".noma/novel_data/hooks_pool.json",
|
||||
"current_chapter": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载创世契约
|
||||
|
||||
读取 `genesis_contract.json` 获取:
|
||||
- `core_desire.primary`: 主角核心欲望
|
||||
- `ethical_inversion.inverted_norms`: 被颠覆的道德规范
|
||||
- `core_spectacle.frequency`: 奇观出现频率
|
||||
|
||||
### 第二步: 分析 Hooks Pool 压强
|
||||
|
||||
```python
|
||||
# 计算当前压强
|
||||
unresolved_hooks = [h for h in hooks if not h.resolved]
|
||||
total_pressure = sum(h.tension_weight for h in unresolved_hooks)
|
||||
|
||||
# 判断是否需要释放
|
||||
if total_pressure > 80:
|
||||
# 高压:需要立即释放
|
||||
next_action = "release"
|
||||
elif total_pressure > 50:
|
||||
# 中压:可以释放或继续积累
|
||||
next_action = "maintain"
|
||||
else:
|
||||
# 低压:继续积累
|
||||
next_action = "buildup"
|
||||
```
|
||||
|
||||
### 第三步: 选择爽感路由模型
|
||||
|
||||
根据 `genesis_contract` 和当前压强状态,从 `matrices/catharsis_models/` 选择模型:
|
||||
|
||||
| 压强状态 | 推荐模型 | 目的 |
|
||||
|---------|---------|------|
|
||||
| 高压 >80 | `overkill-reversal.md` | 降维打击,权力倒转释放 |
|
||||
| 中压 50-80 | `taboo-transgression.md` | 边缘拉扯,危险感 |
|
||||
| 低压 <50 | `cognitive-closure.md` | 认知闭环,伏笔收束 |
|
||||
| 任意 | `strand-weave-pattern.md` | 节奏编织 |
|
||||
|
||||
### 第四步: 拆解章节任务
|
||||
|
||||
将大纲任务分解为:
|
||||
|
||||
```json
|
||||
{
|
||||
"chapter_tasks": [
|
||||
{
|
||||
"chapter": 1,
|
||||
"strand": "quest",
|
||||
"goals": ["建立世界观", "引入主角", "埋设首个Hook"],
|
||||
"hooks_to_create": ["mystery_origin"],
|
||||
"hooks_to_resolve": [],
|
||||
"catharsis_model": null,
|
||||
"tension_target": 30
|
||||
},
|
||||
{
|
||||
"chapter": 2,
|
||||
"strand": "fire",
|
||||
"goals": ["首次冲突", "展示金手指"],
|
||||
"hooks_to_create": ["power_revelation"],
|
||||
"hooks_to_resolve": [],
|
||||
"catharsis_model": "taboo-transgression",
|
||||
"tension_target": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 第五步: 输出大纲任务
|
||||
|
||||
```markdown
|
||||
# 第 {N} 章大纲
|
||||
|
||||
## 基础信息
|
||||
- **Strand**: {quest/fire/constellation}
|
||||
- **目标字数**: {target_words}
|
||||
- **张力目标**: {tension_target}/100
|
||||
|
||||
## 情节目标
|
||||
{goals 列表}
|
||||
|
||||
## Hook 调度
|
||||
- **创建**: {hooks_to_create}
|
||||
- **回收**: {hooks_to_resolve}
|
||||
|
||||
## 爽感模型
|
||||
- **选用模型**: {catharsis_model}
|
||||
- **执行策略**: 见 `matrices/catharsis_models/{model}.md`
|
||||
|
||||
## 上下文要求
|
||||
- **必读实体**: {required_entities}
|
||||
- **必含设定**: {required_settings}
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"planner": "outline_planner",
|
||||
"chapter_tasks": [...],
|
||||
"recommended_catharsis_models": [...],
|
||||
"hooks_schedule": {
|
||||
"create": [...],
|
||||
"resolve": [...]
|
||||
},
|
||||
"tension_trajectory": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不考虑 hooks_pool 压强状态
|
||||
- 不调用 catharsis model,仅做简单爽点堆砌
|
||||
- 忽略 `genesis_contract` 的约束
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 所有创建的 hooks 有回收计划
|
||||
- 张力曲线平滑,无断层
|
||||
- 每次高压释放后有适当缓冲
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: immersive-writer
|
||||
description: 沉浸式写作Agent - 感官颗粒度放大,情绪张力注入
|
||||
tools: Read, Glob, Grep, Write
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# Immersive Writer (沉浸式写作Agent)
|
||||
|
||||
> **职责**: 将大纲场景转化为高密度感官文字,注入情绪张力
|
||||
> **特性**: 感官颗粒度放大,调用 catharsis model 提供情绪张力
|
||||
> **约束**: 保持与 `genesis_contract.json` 的一致性
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **感官放大**: 视觉/听觉/嗅觉/触觉/味觉多维度描写
|
||||
2. **情绪注入**: 依据 catharsis model 注入对应情绪
|
||||
3. **张力维持**: 按照指定的 tension_trajectory 维持压强
|
||||
4. **设定遵循**: 严格遵守 world_rules 和 ethical_inversion
|
||||
|
||||
## 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"writing_instructions": "...",
|
||||
"handoff_frame": {...},
|
||||
"genesis_contract": ".noma/novel_data/openspec/genesis_contract.json",
|
||||
"genre_profile": "matrices/genres/{genre}/...",
|
||||
"catharsis_model": "matrices/catharsis_models/{model}.md"
|
||||
}
|
||||
```
|
||||
|
||||
## 写作原则
|
||||
|
||||
### 感官颗粒度标准
|
||||
|
||||
| 感官 | 最低要求 | 高级要求 |
|
||||
|------|---------|---------|
|
||||
| 视觉 | 颜色、形状、光影 | 动态视觉、视觉隐喻 |
|
||||
| 听觉 | 声音描述 | 音调、节奏、声音来源层次 |
|
||||
| 嗅觉 | 气味 | 气味记忆、气味情绪关联 |
|
||||
| 触觉 | 温度、质地 | 触感细节、身体反应 |
|
||||
| 味觉 | 味道 | 味觉记忆、口腔反应 |
|
||||
|
||||
### 情绪张力注入
|
||||
|
||||
根据 `catharsis_model` 注入对应情绪:
|
||||
|
||||
#### Taboo Transgression (禁忌僭越)
|
||||
```
|
||||
边缘拉扯节奏:
|
||||
- 阶段1: Setup - 暗示禁忌存在,角色内心挣扎
|
||||
- 阶段2: Approach - 压力增大,边界模糊
|
||||
- 阶段3: Crisis - 禁忌被触碰,紧张感达峰值
|
||||
- 阶段4: Transgression - 僭越发生,情绪释放
|
||||
- 阶段5: Fallout - 代偿启动,为下次僭越埋下伏笔
|
||||
```
|
||||
|
||||
#### Overkill Reversal (降维打击)
|
||||
```
|
||||
权力倒转节奏:
|
||||
- 建立敌人强大形象(至少2章)
|
||||
- 主角处于绝对劣势
|
||||
- 触发条件满足(金手指觉醒/弱点发现)
|
||||
- 降维打击,碾压式逆转
|
||||
- 余波:展示胜利后的新格局
|
||||
```
|
||||
|
||||
#### Cognitive Closure (认知闭环)
|
||||
```
|
||||
伏笔收束节奏:
|
||||
- 多线伏笔同时激活
|
||||
- 关键信息揭示
|
||||
- 读者"原来如此"的爽感
|
||||
- 建立新的悬念
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
```markdown
|
||||
# 第 {N} 章: {title}
|
||||
|
||||
## 正文
|
||||
|
||||
[章节内容,符合以下标准]
|
||||
- 字数: {target_words} ± 10%
|
||||
- 感官密度: 每500字至少3种感官描写
|
||||
- 对话比例: 不超过40%
|
||||
- 内心独白: 仅在必要时使用
|
||||
- Hook位置: 章节末尾必须有钩子
|
||||
|
||||
## 章节元数据
|
||||
- **字数**: {actual_words}
|
||||
- **感官密度**: {sensory_density}
|
||||
- **张力曲线**: [起始压强 -> 结束压强]
|
||||
- **创建的Hooks**: [...]
|
||||
- **回收的Hooks**: [...]
|
||||
```
|
||||
|
||||
## 写作检查清单
|
||||
|
||||
### 开篇检查
|
||||
- [ ] 首句是否有吸引力(不平淡)
|
||||
- [ ] 是否在合理位置切入场景
|
||||
- [ ] 是否交代了时间/地点/人物
|
||||
|
||||
### 中段检查
|
||||
- [ ] 场景转换是否有过渡
|
||||
- [ ] 对话是否体现角色性格
|
||||
- [ ] 是否有感官细节支撑情绪
|
||||
- [ ] 是否遵循 strand 节奏
|
||||
|
||||
### 结尾检查
|
||||
- [ ] 是否有 Hook(悬念/反转/冲突升级)
|
||||
- [ ] 张力是否达到目标
|
||||
- [ ] 是否为下一章留下空间
|
||||
|
||||
### Genesis Contract 检查
|
||||
- [ ] 是否符合 ethical_inversion 设定
|
||||
- [ ] 是否遵循 world_rules
|
||||
- [ ] core_desire 是否有推进
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 平铺直叙,无情绪波动
|
||||
- 堆砌感官词汇但不服务情绪
|
||||
- 忽略 handoff_frame 的上下文
|
||||
- 违反 world_rules 的物理设定
|
||||
- 创建无法回收的 Hook
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 感官描写服务情绪表达,非堆砌
|
||||
- Catharsis model 的节奏得到执行
|
||||
- Hook 创建有回收计划
|
||||
- Genesis contract 得到遵守
|
||||
- 可直接交予审查Agent
|
||||
@@ -0,0 +1,9 @@
|
||||
# AI IDE Configurators
|
||||
|
||||
ONW 5.0 组件:Cursor/Windsurf AI IDE 直接挂载适配器
|
||||
|
||||
本目录包含与主流 AI IDE 集成的适配器,实现:
|
||||
- 项目上下文自动注入
|
||||
- 代码生成规则同步
|
||||
- RAG 检索结果直接挂载
|
||||
- Genesis Contract IDE 层强制执行
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Cursor IDE Adapter
|
||||
NovelMaster Core Engine - Configurators
|
||||
|
||||
为 Cursor IDE 生成 .cursorrules 和项目配置
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
|
||||
|
||||
def generate_cursorrules_content(project_config: Dict[str, Any]) -> str:
|
||||
"""生成 .cursorrules 文件内容"""
|
||||
|
||||
genesis = project_config.get("genesis_contract", {})
|
||||
|
||||
rules = f"""# NovelMaster Cursor Rules
|
||||
|
||||
## 项目概览
|
||||
- 项目名称: {project_config.get("project_name", "未命名")}
|
||||
- 类型: {project_config.get("genre", "网文")}
|
||||
- 目标字数: {project_config.get("target_word_count", "未知")} 字
|
||||
|
||||
## 核心设定 (Genesis Contract)
|
||||
### 核心欲望
|
||||
{_format_dict(genesis.get("core_desire", {}))}
|
||||
|
||||
### 伦理沙盒
|
||||
{_format_dict(genesis.get("ethical_inversion", {}))}
|
||||
|
||||
### 奇观日常化
|
||||
{_format_dict(genesis.get("core_spectacle", {}))}
|
||||
|
||||
## 写作规范
|
||||
- 使用中文思维写作
|
||||
- 章节字数: 2000-2500 字
|
||||
- 优先使用第三人称
|
||||
- 爽点密度: 每1000字至少1个情感释放点
|
||||
- 追读力 (Reading Power): 监控 Hook/Cool-point 平衡
|
||||
|
||||
## 状态管理
|
||||
- 数据目录: .noma/
|
||||
- 使用 handoff.py 进行帧交接
|
||||
- 使用 ledger.py 管理资产/负债
|
||||
- 使用 retcon_manager.py 处理设定修改
|
||||
|
||||
## 敏感内容
|
||||
- 禁止: 政治敏感、真实暴力、未成年人性内容
|
||||
- 伦理沙盒内创作自由
|
||||
"""
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def _format_dict(d: Dict[str, Any], indent: int = 2) -> str:
|
||||
if not d:
|
||||
return " (未设置)"
|
||||
lines = []
|
||||
for key, value in d.items():
|
||||
if isinstance(value, dict):
|
||||
lines.append(f" {key}:")
|
||||
lines.append(_format_dict(value, indent + 2))
|
||||
elif isinstance(value, list):
|
||||
lines.append(f" {key}: {', '.join(str(v) for v in value)}")
|
||||
else:
|
||||
lines.append(f" {key}: {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_project_config(project_root: Path, config: Dict[str, Any]) -> Path:
|
||||
"""生成 Cursor 项目配置文件"""
|
||||
config_dir = project_root / ".cursor"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
config_file = config_dir / "novelmaster.json"
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
rules_file = project_root / ".cursorrules"
|
||||
rules_content = generate_cursorrules_content(config)
|
||||
with open(rules_file, "w", encoding="utf-8") as f:
|
||||
f.write(rules_content)
|
||||
|
||||
return rules_file
|
||||
|
||||
|
||||
def detect_cursor_environment() -> bool:
|
||||
"""检测是否在 Cursor 环境中"""
|
||||
cursor_markers = [
|
||||
".cursor",
|
||||
".cursorrules",
|
||||
"cursor_rules",
|
||||
]
|
||||
for marker in cursor_markers:
|
||||
if Path(marker).exists():
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Universal IDE Adapter
|
||||
NovelMaster Core Engine - Configurators
|
||||
|
||||
跨IDE统一接口 (Cursor, Windsurf, VS Code, JetBrains)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
from enum import Enum
|
||||
|
||||
from .cursor_adapter import generate_project_config as cursor_generate
|
||||
from .windsurf_adapter import setup_windsurf
|
||||
|
||||
|
||||
class IDEType(Enum):
|
||||
CURSOR = "cursor"
|
||||
WINDSURF = "windsurf"
|
||||
VSCODE = "vscode"
|
||||
JETBRAINS = "jetbrains"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
def detect_ide() -> IDEType:
|
||||
"""检测当前 IDE 环境"""
|
||||
markers = Path.cwd()
|
||||
|
||||
cursor_markers = [".cursorrules", ".cursor"]
|
||||
windsurf_markers = [".windsurfrules", ".windsurf"]
|
||||
|
||||
for marker in cursor_markers:
|
||||
if (markers / marker).exists():
|
||||
return IDEType.CURSOR
|
||||
|
||||
for marker in windsurf_markers:
|
||||
if (markers / marker).exists():
|
||||
return IDEType.WINDSURF
|
||||
|
||||
if "cursor" in Path.cwd().parts or "cursor" in str(Path(__file__)):
|
||||
return IDEType.CURSOR
|
||||
elif "windsurf" in Path.cwd().parts or "windsurf" in str(Path(__file__)):
|
||||
return IDEType.WINDSURF
|
||||
|
||||
return IDEType.UNKNOWN
|
||||
|
||||
|
||||
def setup_ide(project_root: Path, config: Dict[str, Any]) -> bool:
|
||||
"""为检测到的 IDE 设置项目"""
|
||||
ide = detect_ide()
|
||||
|
||||
if ide == IDEType.CURSOR:
|
||||
cursor_generate(project_root, config)
|
||||
return True
|
||||
elif ide == IDEType.WINDSURF:
|
||||
setup_windsurf(project_root, config)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def generate_ide_rules(ide: IDEType, project_config: Dict[str, Any]) -> str:
|
||||
"""为指定 IDE 生成规则"""
|
||||
if ide == IDEType.CURSOR:
|
||||
from .cursor_adapter import generate_cursorrules_content
|
||||
return generate_cursorrules_content(project_config)
|
||||
elif ide == IDEType.WINDSURF:
|
||||
from .windsurf_adapter import generate_windsurfrules
|
||||
return generate_windsurfrules(project_config)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def get_supported_ides() -> List[str]:
|
||||
"""获取支持的 IDE 列表"""
|
||||
return [e.value for e in IDEType if e != IDEType.UNKNOWN]
|
||||
|
||||
|
||||
def auto_setup(project_root: Path, config: Dict[str, Any]) -> Dict[str, bool]:
|
||||
"""自动为所有支持的 IDE 生成配置"""
|
||||
results = {}
|
||||
|
||||
results["cursor"] = _setup_cursor(project_root, config)
|
||||
results["windsurf"] = _setup_windsurf(project_root, config)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _setup_cursor(project_root: Path, config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
cursor_generate(project_root, config)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _setup_windsurf(project_root: Path, config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
setup_windsurf(project_root, config)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Windsurf IDE Adapter
|
||||
NovelMaster Core Engine - Configurators
|
||||
|
||||
为 Windsurf IDE 生成配置和 Cascade 上下文
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
|
||||
|
||||
def generate_windsurfrules(project_config: Dict[str, Any]) -> str:
|
||||
"""生成 .windsurfrules 文件内容"""
|
||||
|
||||
genesis = project_config.get("genesis_contract", {})
|
||||
|
||||
rules = f"""# NovelMaster Windsurf Rules
|
||||
|
||||
## 项目配置
|
||||
- 名称: {project_config.get("project_name", "未命名")}
|
||||
- 题材: {project_config.get("genre", "网文")}
|
||||
|
||||
## Genesis Contract
|
||||
{genesis.get("description", "")}
|
||||
|
||||
## Cascade 上下文
|
||||
使用以下上下文进行创作:
|
||||
1. 核心欲望: {genesis.get("core_desire", {}).get("primary", "未设置")}
|
||||
2. 伦理沙盒: {genesis.get("ethical_inversion", {}).get("inverted_norms", [])}
|
||||
3. 奇观设定: {genesis.get("core_spectacle", {}).get("ordinary_state", "未设置")}
|
||||
|
||||
## 写作工作流
|
||||
1. Planner Agent 调度 catharsis_model
|
||||
2. Writer Agent 生成章节
|
||||
3. Handoff 帧交接
|
||||
4. Dashboard 监控
|
||||
|
||||
## 状态文件
|
||||
- 状态文件: .noma/state.json
|
||||
- 账本: .noma/ledger.json
|
||||
- Hook池: .noma/hooks_pool.json
|
||||
"""
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def generate_cascade_context(project_root: Path) -> Dict[str, Any]:
|
||||
"""生成 Cascade 上下文"""
|
||||
state_file = project_root / ".noma" / "state.json"
|
||||
|
||||
context = {
|
||||
"project_root": str(project_root),
|
||||
"noma_dir": str(project_root / ".noma"),
|
||||
"genesis_contract": {},
|
||||
"current_chapter": 1,
|
||||
"hooks": [],
|
||||
"ledger": {}
|
||||
}
|
||||
|
||||
if state_file.exists():
|
||||
try:
|
||||
with open(state_file, "r", encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
context.update(state)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def setup_windsurf(project_root: Path, config: Dict[str, Any]) -> None:
|
||||
"""设置 Windsurf 项目"""
|
||||
rules_file = project_root / ".windsurfrules"
|
||||
rules_content = generate_windsurfrules(config)
|
||||
with open(rules_file, "w", encoding="utf-8") as f:
|
||||
f.write(rules_content)
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Context Cache (上下文静态缓存与Hash脏标记更新)
|
||||
NovelMaster Core Engine - Memory RAG
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
key: str
|
||||
value: Any
|
||||
hash: str
|
||||
created_at: datetime
|
||||
last_accessed: datetime
|
||||
access_count: int = 0
|
||||
is_dirty: bool = False
|
||||
|
||||
def update_hash(self) -> str:
|
||||
content = json.dumps(self.value, sort_keys=True, ensure_ascii=False)
|
||||
self.hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
self.is_dirty = False
|
||||
return self.hash
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextCache:
|
||||
entries: Dict[str, CacheEntry] = field(default_factory=dict)
|
||||
max_size: int = 1000
|
||||
dirty_keys: Set[str] = field(default_factory=set)
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
if key not in self.entries:
|
||||
return None
|
||||
entry = self.entries[key]
|
||||
entry.last_accessed = datetime.now()
|
||||
entry.access_count += 1
|
||||
return entry.value
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
content = json.dumps(value, sort_keys=True, ensure_ascii=False)
|
||||
hash_val = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
self.entries[key] = CacheEntry(
|
||||
key=key,
|
||||
value=value,
|
||||
hash=hash_val,
|
||||
created_at=datetime.now(),
|
||||
last_accessed=datetime.now()
|
||||
)
|
||||
if len(self.entries) > self.max_size:
|
||||
self._evict_lru()
|
||||
|
||||
def invalidate(self, key: str) -> None:
|
||||
if key in self.entries:
|
||||
self.entries[key].is_dirty = True
|
||||
self.dirty_keys.add(key)
|
||||
|
||||
def invalidate_pattern(self, pattern: str) -> None:
|
||||
for key in self.entries:
|
||||
if pattern in key:
|
||||
self.invalidate(key)
|
||||
|
||||
def get_dirty_keys(self) -> Set[str]:
|
||||
return self.dirty_keys.copy()
|
||||
|
||||
def clear_dirty(self, key: str) -> None:
|
||||
self.dirty_keys.discard(key)
|
||||
if key in self.entries:
|
||||
self.entries[key].is_dirty = False
|
||||
|
||||
def _evict_lru(self) -> None:
|
||||
if not self.entries:
|
||||
return
|
||||
sorted_entries = sorted(
|
||||
self.entries.items(),
|
||||
key=lambda x: (x[1].access_count, x[1].last_accessed)
|
||||
)
|
||||
evict_count = max(1, len(sorted_entries) // 10)
|
||||
for i in range(evict_count):
|
||||
del self.entries[sorted_entries[i][0]]
|
||||
|
||||
|
||||
class RetconHashListener:
|
||||
def __init__(self, cache: ContextCache):
|
||||
self.cache = cache
|
||||
self.subscribers: Dict[str, List[callable]] = {}
|
||||
|
||||
def subscribe(self, pattern: str, callback: callable) -> None:
|
||||
if pattern not in self.subscribers:
|
||||
self.subscribers[pattern] = []
|
||||
self.subscribers[pattern].append(callback)
|
||||
|
||||
def notify(self, changed_keys: Set[str]) -> None:
|
||||
for key in changed_keys:
|
||||
for pattern, callbacks in self.subscribers.items():
|
||||
if pattern in key:
|
||||
for callback in callbacks:
|
||||
callback(key)
|
||||
|
||||
def on_retcon(self, retcon_patch: dict) -> None:
|
||||
affected_elements = retcon_patch.get('affected_elements', [])
|
||||
for element in affected_elements:
|
||||
element_name = element.get('name', '')
|
||||
self.cache.invalidate_pattern(element_name)
|
||||
dirty_keys = self.cache.get_dirty_keys()
|
||||
self.notify(dirty_keys)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Vector Store Utilities
|
||||
NovelMaster Core Engine - Memory RAG
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
|
||||
class VectorStoreUtils:
|
||||
"""向量存储工具类"""
|
||||
|
||||
def __init__(self, embedding_model: str = "default"):
|
||||
self.embedding_model = embedding_model
|
||||
self.collection_name = "novelmaster_context"
|
||||
self.dimension = 1536 # 默认维度
|
||||
|
||||
def compute_hash(self, content: str) -> str:
|
||||
"""计算内容的哈希值"""
|
||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
|
||||
def chunk_text(self, text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]:
|
||||
"""将文本分块"""
|
||||
chunks = []
|
||||
for i in range(0, len(text), chunk_size - overlap):
|
||||
chunks.append(text[i:i + chunk_size])
|
||||
return chunks
|
||||
|
||||
def prepare_for_storage(self, text: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""准备存储格式"""
|
||||
return {
|
||||
"text": text,
|
||||
"hash": self.compute_hash(text),
|
||||
"metadata": metadata,
|
||||
"model": self.embedding_model
|
||||
}
|
||||
|
||||
def query_similar(self, query: str, top_k: int = 5, filters: Optional[Dict] = None) -> List[Dict]:
|
||||
"""查询相似内容"""
|
||||
return [] # 待实现
|
||||
|
||||
def upsert(self, documents: List[Dict[str, Any]]) -> bool:
|
||||
"""插入或更新文档"""
|
||||
return True
|
||||
|
||||
def delete_by_hash(self, hash: str) -> bool:
|
||||
"""通过哈希删除"""
|
||||
return True
|
||||
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Handoff Protocol (帧交接协议)
|
||||
NovelMaster Core Engine - 状态管理器
|
||||
|
||||
特性:
|
||||
- LOD (视锥剔除): 只精确交接在场人物
|
||||
- Tick (全局时钟): 多线程悬置动作记录
|
||||
- 休眠背景人物惰性更新
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
|
||||
class CharacterStatus(Enum):
|
||||
ACTIVE = "active"
|
||||
DORMANT = "dormant"
|
||||
SUSPENDED = "suspended"
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
DIALOGUE = "dialogue"
|
||||
MOVEMENT = "movement"
|
||||
COMBAT = "combat"
|
||||
SKILL = "skill"
|
||||
MENTAL = "mental"
|
||||
|
||||
|
||||
class Urgency(Enum):
|
||||
CRITICAL = "critical"
|
||||
NORMAL = "normal"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Vector3D:
|
||||
"""3D坐标"""
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class Character:
|
||||
"""角色"""
|
||||
id: str
|
||||
name: str
|
||||
status: CharacterStatus = CharacterStatus.ACTIVE
|
||||
position: Optional[Vector3D] = None
|
||||
last_update_tick: int = 0
|
||||
pending_actions: List['ImminentAction'] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImminentAction:
|
||||
"""悬置动作"""
|
||||
id: str
|
||||
character_id: str
|
||||
type: ActionType
|
||||
description: str
|
||||
urgency: Urgency = Urgency.NORMAL
|
||||
tick: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldState:
|
||||
"""世界状态"""
|
||||
location: str
|
||||
time_of_day: str
|
||||
weather: Optional[str] = None
|
||||
tension_level: int = 50 # 0-100
|
||||
hook_pressure: int = 0 # 负债池压强
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffMetadata:
|
||||
"""交接元数据"""
|
||||
chapter_number: int
|
||||
scene_number: int
|
||||
previous_chapter_summary: str
|
||||
next_chapter_hint: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffFrame:
|
||||
"""交接帧"""
|
||||
tick: int
|
||||
timestamp: float
|
||||
active_characters: List[Character]
|
||||
suspended_characters: List[Character]
|
||||
imminent_actions: List[ImminentAction]
|
||||
world_state: WorldState
|
||||
metadata: HandoffMetadata
|
||||
|
||||
|
||||
def cull_to_viewshed(characters: List[Character], view_center: Vector3D, radius: float) -> List[Character]:
|
||||
"""LOD视锥剔除 - 只保留视野内角色"""
|
||||
result = []
|
||||
for char in characters:
|
||||
if not char.position:
|
||||
continue
|
||||
distance = ((char.position.x - view_center.x) ** 2 +
|
||||
(char.position.y - view_center.y) ** 2 +
|
||||
(char.position.z - view_center.z) ** 2) ** 0.5
|
||||
if distance <= radius:
|
||||
result.append(char)
|
||||
return result
|
||||
|
||||
|
||||
def create_handoff_frame(
|
||||
current_tick: int,
|
||||
all_characters: List[Character],
|
||||
world_state: WorldState,
|
||||
metadata: HandoffMetadata
|
||||
) -> HandoffFrame:
|
||||
"""创建交接帧"""
|
||||
active_characters = [c for c in all_characters if c.status == CharacterStatus.ACTIVE]
|
||||
suspended_characters = [c for c in all_characters if c.status == CharacterStatus.SUSPENDED]
|
||||
|
||||
# 收集所有悬置动作
|
||||
imminent_actions = []
|
||||
for char in all_characters:
|
||||
imminent_actions.extend(char.pending_actions)
|
||||
|
||||
return HandoffFrame(
|
||||
tick=current_tick,
|
||||
timestamp=time.time(),
|
||||
active_characters=active_characters,
|
||||
suspended_characters=suspended_characters,
|
||||
imminent_actions=imminent_actions,
|
||||
world_state=world_state,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
def merge_handoff_frames(frames: List[HandoffFrame]) -> HandoffFrame:
|
||||
"""合并多个交接帧"""
|
||||
if not frames:
|
||||
raise ValueError("No frames to merge")
|
||||
|
||||
latest_frame = max(frames, key=lambda f: f.tick)
|
||||
|
||||
return HandoffFrame(
|
||||
tick=latest_frame.tick,
|
||||
timestamp=latest_frame.timestamp,
|
||||
active_characters=[c for f in frames for c in f.active_characters],
|
||||
suspended_characters=[c for f in frames for c in f.suspended_characters],
|
||||
imminent_actions=[a for f in frames for a in f.imminent_actions],
|
||||
world_state=latest_frame.world_state,
|
||||
metadata=latest_frame.metadata
|
||||
)
|
||||
|
||||
|
||||
def to_dict(frame: HandoffFrame) -> Dict[str, Any]:
|
||||
"""序列化为字典"""
|
||||
return {
|
||||
"tick": frame.tick,
|
||||
"timestamp": frame.timestamp,
|
||||
"active_characters": [
|
||||
{
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"status": c.status.value,
|
||||
"position": {"x": c.position.x, "y": c.position.y, "z": c.position.z} if c.position else None
|
||||
}
|
||||
for c in frame.active_characters
|
||||
],
|
||||
"suspended_characters": [c.id for c in frame.suspended_characters],
|
||||
"imminent_actions": [
|
||||
{
|
||||
"id": a.id,
|
||||
"character_id": a.character_id,
|
||||
"type": a.type.value,
|
||||
"description": a.description,
|
||||
"urgency": a.urgency.value
|
||||
}
|
||||
for a in frame.imminent_actions
|
||||
],
|
||||
"world_state": {
|
||||
"location": frame.world_state.location,
|
||||
"time_of_day": frame.world_state.time_of_day,
|
||||
"weather": frame.world_state.weather,
|
||||
"tension_level": frame.world_state.tension_level,
|
||||
"hook_pressure": frame.world_state.hook_pressure
|
||||
},
|
||||
"metadata": {
|
||||
"chapter_number": frame.metadata.chapter_number,
|
||||
"scene_number": frame.metadata.scene_number,
|
||||
"previous_chapter_summary": frame.metadata.previous_chapter_summary,
|
||||
"next_chapter_hint": frame.metadata.next_chapter_hint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Ledger (动态资产与负债账本)
|
||||
NovelMaster Core Engine - 状态管理器
|
||||
|
||||
管理角色资产、负债、Hook/Cool-point 追踪
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Set, Optional, Any
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
|
||||
class AssetType(Enum):
|
||||
SKILL = "skill"
|
||||
ITEM = "item"
|
||||
RELATIONSHIP = "relationship"
|
||||
STATUS = "status"
|
||||
KNOWLEDGE = "knowledge"
|
||||
POWER = "power"
|
||||
|
||||
|
||||
class LiabilityType(Enum):
|
||||
DEBT = "debt"
|
||||
PROMISE = "promise"
|
||||
UNRESOLVED_CONFLICT = "unresolved_conflict"
|
||||
SECRET = "secret"
|
||||
WEAKNESS = "weakness"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Asset:
|
||||
"""资产"""
|
||||
id: str
|
||||
type: AssetType
|
||||
name: str
|
||||
description: str
|
||||
value: int # 1-100
|
||||
acquired_at_tick: int
|
||||
owner_id: str
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Liability:
|
||||
"""负债"""
|
||||
id: str
|
||||
type: LiabilityType
|
||||
name: str
|
||||
description: str
|
||||
severity: int # 1-100
|
||||
created_at_tick: int
|
||||
due_tick: Optional[int] = None
|
||||
creditor_id: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerEntry:
|
||||
"""账本条目"""
|
||||
asset: Optional[Asset] = None
|
||||
liability: Optional[Liability] = None
|
||||
delta: float = 0
|
||||
reason: str = ""
|
||||
tick: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class CharacterLedger:
|
||||
"""角色账本"""
|
||||
character_id: str
|
||||
assets: Dict[str, Asset] = field(default_factory=dict)
|
||||
liabilities: Dict[str, Liability] = field(default_factory=dict)
|
||||
history: List[LedgerEntry] = field(default_factory=list)
|
||||
|
||||
|
||||
class HookType(Enum):
|
||||
SETUP = "setup"
|
||||
BUILDUP = "buildup"
|
||||
CLIFFHANGER = "cliffhanger"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hook:
|
||||
"""钩子"""
|
||||
id: str
|
||||
type: HookType
|
||||
description: str
|
||||
chapter_introduced: int
|
||||
tension_weight: int # 1-100
|
||||
resolved: bool = False
|
||||
resolution_chapter: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoolPoint:
|
||||
"""爽点"""
|
||||
id: str
|
||||
type: str # payoff, subversion, escalation
|
||||
description: str
|
||||
chapter_delivered: int
|
||||
satisfaction_weight: int # 1-100
|
||||
|
||||
|
||||
def create_asset(
|
||||
asset_id: str,
|
||||
asset_type: AssetType,
|
||||
name: str,
|
||||
value: int,
|
||||
owner_id: str,
|
||||
description: str = ""
|
||||
) -> Asset:
|
||||
"""创建资产"""
|
||||
return Asset(
|
||||
id=asset_id,
|
||||
type=asset_type,
|
||||
name=name,
|
||||
description=description,
|
||||
value=max(1, min(100, value)),
|
||||
acquired_at_tick=0,
|
||||
owner_id=owner_id,
|
||||
tags=[]
|
||||
)
|
||||
|
||||
|
||||
def create_liability(
|
||||
liability_id: str,
|
||||
liability_type: LiabilityType,
|
||||
name: str,
|
||||
severity: int,
|
||||
description: str = ""
|
||||
) -> Liability:
|
||||
"""创建负债"""
|
||||
return Liability(
|
||||
id=liability_id,
|
||||
type=liability_type,
|
||||
name=name,
|
||||
description=description,
|
||||
severity=max(1, min(100, severity)),
|
||||
created_at_tick=0,
|
||||
tags=[]
|
||||
)
|
||||
|
||||
|
||||
def calculate_net_worth(ledger: CharacterLedger) -> int:
|
||||
"""计算角色净值"""
|
||||
asset_value = sum(asset.value for asset in ledger.assets.values())
|
||||
liability_value = sum(liab.severity for liab in ledger.liabilities.values())
|
||||
return asset_value - liability_value
|
||||
|
||||
|
||||
def add_hook(hooks: List[Hook], hook: Hook) -> List[Hook]:
|
||||
"""添加钩子"""
|
||||
return [*hooks, hook]
|
||||
|
||||
|
||||
def add_cool_point(cool_points: List[CoolPoint], cool_point: CoolPoint) -> List[CoolPoint]:
|
||||
"""添加爽点"""
|
||||
return [*cool_points, cool_point]
|
||||
|
||||
|
||||
def calculate_hook_pressure(hooks: List[Hook]) -> int:
|
||||
"""计算钩子压强"""
|
||||
unresolved = [h for h in hooks if not h.resolved]
|
||||
return sum(h.tension_weight for h in unresolved)
|
||||
|
||||
|
||||
def resolve_hook(hooks: List[Hook], hook_id: str, resolution_chapter: int) -> List[Hook]:
|
||||
"""解决钩子"""
|
||||
return [
|
||||
{**hook.__dict__, "resolved": True, "resolution_chapter": resolution_chapter}
|
||||
if hook.id == hook_id else hook
|
||||
for hook in hooks
|
||||
]
|
||||
|
||||
|
||||
def check_genesis_compatibility(asset: Asset, ethical_inversion: Dict[str, Any]) -> float:
|
||||
"""检查资产与创世契约的兼容性"""
|
||||
incompatible_norms = ethical_inversion.get("inverted_norms", [])
|
||||
|
||||
# 简单检查:资产名称是否与反转的道德规范冲突
|
||||
for norm in incompatible_norms:
|
||||
if norm.lower() in asset.name.lower():
|
||||
return 0.3 # 低兼容
|
||||
|
||||
return 1.0 # 完全兼容
|
||||
|
||||
|
||||
@dataclass
|
||||
class HooksPool:
|
||||
"""钩子池"""
|
||||
hooks: List[Hook] = field(default_factory=list)
|
||||
cool_points: List[CoolPoint] = field(default_factory=list)
|
||||
|
||||
def add_hook(self, hook: Hook) -> None:
|
||||
self.hooks.append(hook)
|
||||
|
||||
def resolve_hook(self, hook_id: str, chapter: int) -> bool:
|
||||
for h in self.hooks:
|
||||
if h.id == hook_id:
|
||||
h.resolved = True
|
||||
h.resolution_chapter = chapter
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def pressure(self) -> int:
|
||||
return calculate_hook_pressure(self.hooks)
|
||||
|
||||
@property
|
||||
def unresolved_count(self) -> int:
|
||||
return sum(1 for h in self.hooks if not h.resolved)
|
||||
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Retcon Manager (意图漂移管理与热补丁生成器)
|
||||
NovelMaster Core Engine - 状态管理器
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional, Any, Tuple
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
||||
from .ledger import (
|
||||
CharacterLedger, Asset, Liability, Hook,
|
||||
AssetType, LiabilityType, HooksPool,
|
||||
check_genesis_compatibility
|
||||
)
|
||||
|
||||
|
||||
class RetconPriority(Enum):
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
class RetconOperationType(Enum):
|
||||
MODIFY = "modify"
|
||||
DELETE = "delete"
|
||||
CREATE = "create"
|
||||
RELABEL = "relabel"
|
||||
TRANSFER = "transfer"
|
||||
|
||||
|
||||
class AffectedElementType(Enum):
|
||||
ASSET = "asset"
|
||||
LIABILITY = "liability"
|
||||
CHARACTER_STATUS = "character_status"
|
||||
RELATIONSHIP = "relationship"
|
||||
WORLD_RULE = "world_rule"
|
||||
HOOK = "hook"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconAffectedElement:
|
||||
type: AffectedElementType
|
||||
id: str
|
||||
name: str
|
||||
original_value: Any
|
||||
proposed_change: Any
|
||||
compatibility_score: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconOperation:
|
||||
type: RetconOperationType
|
||||
target_type: str
|
||||
target_id: str
|
||||
old_value: Any = None
|
||||
new_value: Any = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconRequest:
|
||||
author_intent: str
|
||||
affected_elements: List[RetconAffectedElement] = field(default_factory=list)
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
priority: RetconPriority = RetconPriority.MEDIUM
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconPatch:
|
||||
id: str
|
||||
request: RetconRequest
|
||||
generated_at: float
|
||||
operations: List[RetconOperation] = field(default_factory=list)
|
||||
rollback_plan: List[RetconOperation] = field(default_factory=list)
|
||||
success: bool = True
|
||||
compatibility_issues: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconResult:
|
||||
patch: RetconPatch
|
||||
compatibility_score: float
|
||||
warnings: List[str]
|
||||
approved: bool = False
|
||||
|
||||
|
||||
CONFLICT_TEMPLATES = [
|
||||
{
|
||||
"pattern": ["修仙", "正道", "浩然正气"],
|
||||
"conflicts_with": ["克苏鲁", "邪神", "混沌", "疯狂"],
|
||||
"resolution": "将正道之物转化为邪神相关设定"
|
||||
},
|
||||
{
|
||||
"pattern": ["科技", "理性", "逻辑"],
|
||||
"conflicts_with": ["魔法", "神秘", "超自然"],
|
||||
"resolution": "将科技产物与魔法融合或对立"
|
||||
},
|
||||
{
|
||||
"pattern": ["现实", "现代"],
|
||||
"conflicts_with": ["异世界", "穿越", "幻想"],
|
||||
"resolution": "引入平行世界设定"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def detect_conflicts(new_setting: str, ledger: CharacterLedger, hooks_pool: HooksPool) -> List[Tuple[str, str]]:
|
||||
conflicts = []
|
||||
new_setting_lower = new_setting.lower()
|
||||
|
||||
for asset in ledger.assets.values():
|
||||
for template in CONFLICT_TEMPLATES:
|
||||
setting_matches = any(p.lower() in new_setting_lower for p in template["pattern"])
|
||||
asset_matches = any(p.lower() in asset.name.lower() for p in template["conflicts_with"])
|
||||
if setting_matches and asset_matches:
|
||||
conflicts.append((asset.id, template["resolution"]))
|
||||
|
||||
for liability in ledger.liabilities.values():
|
||||
for template in CONFLICT_TEMPLATES:
|
||||
setting_matches = any(p.lower() in new_setting_lower for p in template["pattern"])
|
||||
liability_matches = any(p.lower() in liability.name.lower() for p in template["conflicts_with"])
|
||||
if setting_matches and liability_matches:
|
||||
conflicts.append((liability.id, template["resolution"]))
|
||||
|
||||
for hook in hooks_pool.hooks:
|
||||
for template in CONFLICT_TEMPLATES:
|
||||
setting_matches = any(p.lower() in new_setting_lower for p in template["pattern"])
|
||||
hook_matches = any(p.lower() in hook.description.lower() for p in template["conflicts_with"])
|
||||
if setting_matches and hook_matches:
|
||||
conflicts.append((hook.id, template["resolution"]))
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
def analyze_retcon_compatibility(
|
||||
request: RetconRequest,
|
||||
ledgers: Dict[str, CharacterLedger],
|
||||
hooks_pool: HooksPool,
|
||||
genesis_contract: Optional[Dict] = None
|
||||
) -> List[RetconAffectedElement]:
|
||||
affected = []
|
||||
conflicts = detect_conflicts(request.author_intent, list(ledgers.values())[0] if ledgers else CharacterLedger(""), hooks_pool)
|
||||
|
||||
for ledger in ledgers.values():
|
||||
for asset_id, asset in ledger.assets.items():
|
||||
compatibility = 100.0
|
||||
if genesis_contract:
|
||||
ethical_inv = genesis_contract.get("ethical_inversion", {})
|
||||
compatibility = check_genesis_compatibility(asset, ethical_inv) * 100
|
||||
|
||||
conflict_resolution = None
|
||||
for conflict_id, resolution in conflicts:
|
||||
if conflict_id == asset_id:
|
||||
conflict_resolution = resolution
|
||||
compatibility = min(compatibility, 30.0)
|
||||
|
||||
if compatibility < 100 or conflict_resolution:
|
||||
affected.append(RetconAffectedElement(
|
||||
type=AffectedElementType.ASSET,
|
||||
id=asset_id,
|
||||
name=asset.name,
|
||||
original_value=asset.__dict__,
|
||||
proposed_change={"compatibility": compatibility},
|
||||
compatibility_score=compatibility
|
||||
))
|
||||
|
||||
for liab_id, liability in ledger.liabilities.items():
|
||||
compatibility = 80.0 if liability.type == LiabilityType.DEBT else 100.0
|
||||
affected.append(RetconAffectedElement(
|
||||
type=AffectedElementType.LIABILITY,
|
||||
id=liab_id,
|
||||
name=liability.name,
|
||||
original_value=liability.__dict__,
|
||||
proposed_change={"compatibility": compatibility},
|
||||
compatibility_score=compatibility
|
||||
))
|
||||
|
||||
for hook in hooks_pool.hooks:
|
||||
if not hook.resolved:
|
||||
affected.append(RetconAffectedElement(
|
||||
type=AffectedElementType.HOOK,
|
||||
id=hook.id,
|
||||
name=hook.description,
|
||||
original_value=hook.__dict__,
|
||||
proposed_change={"needs_resolution": True},
|
||||
compatibility_score=50.0
|
||||
))
|
||||
|
||||
return affected
|
||||
|
||||
|
||||
def generate_retcon_patch(
|
||||
request: RetconRequest,
|
||||
ledgers: Dict[str, CharacterLedger],
|
||||
hooks_pool: HooksPool,
|
||||
genesis_contract: Optional[Dict] = None
|
||||
) -> RetconPatch:
|
||||
affected = analyze_retcon_compatibility(request, ledgers, hooks_pool, genesis_contract)
|
||||
operations = []
|
||||
compatibility_issues = []
|
||||
|
||||
for element in affected:
|
||||
if element.type == AffectedElementType.ASSET:
|
||||
if element.compatibility_score < 30:
|
||||
for ledger in ledgers.values():
|
||||
if element.id in ledger.assets:
|
||||
original_asset = ledger.assets[element.id]
|
||||
new_liability = Liability(
|
||||
id=f"retcon_{original_asset.id}",
|
||||
type=LiabilityType.UNRESOLVED_CONFLICT,
|
||||
name=f"[Retcon] {original_asset.name}",
|
||||
description=f"由资产 '{original_asset.name}' 转化,原值 {original_asset.value}",
|
||||
severity=original_asset.value,
|
||||
created_at_tick=0,
|
||||
tags=["retcon-converted", "auto-generated"]
|
||||
)
|
||||
operations.append(RetconOperation(
|
||||
type=RetconOperationType.DELETE,
|
||||
target_type="asset",
|
||||
target_id=element.id,
|
||||
old_value=original_asset.__dict__,
|
||||
new_value=new_liability.__dict__,
|
||||
description=f"将资产 '{element.name}' 转化为负债以适应新设定"
|
||||
))
|
||||
operations.append(RetconOperation(
|
||||
type=RetconOperationType.CREATE,
|
||||
target_type="liability",
|
||||
target_id=new_liability.id,
|
||||
old_value=None,
|
||||
new_value=new_liability.__dict__,
|
||||
description=f"创建转化负债 '{new_liability.name}'"
|
||||
))
|
||||
compatibility_issues.append(f"资产 '{element.name}' 被标记为不兼容,转化为对冲负债")
|
||||
break
|
||||
elif element.compatibility_score < 70:
|
||||
compatibility_issues.append(f"资产 '{element.name}' 兼容性问题需要关注")
|
||||
|
||||
elif element.type == AffectedElementType.HOOK:
|
||||
operations.append(RetconOperation(
|
||||
type=RetconOperationType.MODIFY,
|
||||
target_type="hook",
|
||||
target_id=element.id,
|
||||
old_value=element.original_value,
|
||||
new_value={"retcon_flag": True, "new_intent": request.author_intent},
|
||||
description=f"Hook '{element.name}' 被Retcon标记,需要重新评估"
|
||||
))
|
||||
|
||||
rollback_plan = [
|
||||
RetconOperation(
|
||||
type=op.type,
|
||||
target_type=op.target_type,
|
||||
target_id=op.target_id,
|
||||
old_value=op.new_value,
|
||||
new_value=op.old_value,
|
||||
description=f"回滚: {op.description}"
|
||||
)
|
||||
for op in operations
|
||||
]
|
||||
|
||||
return RetconPatch(
|
||||
id=f"retcon_{int(time.time() * 1000)}",
|
||||
request=request,
|
||||
generated_at=time.time(),
|
||||
operations=operations,
|
||||
rollback_plan=rollback_plan,
|
||||
success=len(compatibility_issues) == 0,
|
||||
compatibility_issues=compatibility_issues
|
||||
)
|
||||
|
||||
|
||||
def apply_retcon_patch(
|
||||
patch: RetconPatch,
|
||||
ledgers: Dict[str, CharacterLedger],
|
||||
hooks_pool: HooksPool
|
||||
) -> Dict[str, CharacterLedger]:
|
||||
new_ledgers = {}
|
||||
for ledger_id, ledger in ledgers.items():
|
||||
new_ledger = CharacterLedger(
|
||||
character_id=ledger.character_id,
|
||||
assets=dict(ledger.assets),
|
||||
liabilities=dict(ledger.liabilities),
|
||||
history=list(ledger.history)
|
||||
)
|
||||
for op in patch.operations:
|
||||
if op.target_type == "asset":
|
||||
if op.type == RetconOperationType.DELETE:
|
||||
if op.target_id in new_ledger.assets:
|
||||
del new_ledger.assets[op.target_id]
|
||||
elif op.type == RetconOperationType.CREATE:
|
||||
new_asset = Asset(**op.new_value)
|
||||
new_ledger.assets[new_asset.id] = new_asset
|
||||
elif op.target_type == "liability":
|
||||
if op.type == RetconOperationType.CREATE:
|
||||
new_liability = Liability(**op.new_value)
|
||||
new_ledger.liabilities[new_liability.id] = new_liability
|
||||
elif op.type == RetconOperationType.MODIFY:
|
||||
if op.target_id in new_ledger.liabilities:
|
||||
existing = new_ledger.liabilities[op.target_id]
|
||||
updated = existing.__dict__.copy()
|
||||
updated.update(op.new_value)
|
||||
new_ledger.liabilities[op.target_id] = Liability(**updated)
|
||||
new_ledgers[ledger_id] = new_ledger
|
||||
return new_ledgers
|
||||
|
||||
|
||||
def rollback_retcon_patch(
|
||||
patch: RetconPatch,
|
||||
ledgers: Dict[str, CharacterLedger]
|
||||
) -> Dict[str, CharacterLedger]:
|
||||
return apply_retcon_patch(
|
||||
RetconPatch(
|
||||
id=f"rollback_{patch.id}",
|
||||
request=patch.request,
|
||||
generated_at=time.time(),
|
||||
operations=patch.rollback_plan,
|
||||
rollback_plan=patch.operations,
|
||||
success=True,
|
||||
compatibility_issues=[]
|
||||
),
|
||||
ledgers,
|
||||
HooksPool()
|
||||
)
|
||||
|
||||
|
||||
def compute_retcon_hash(patch: RetconPatch) -> str:
|
||||
content = json.dumps({
|
||||
"id": patch.id,
|
||||
"operations": [
|
||||
{"type": op.type.value, "target_type": op.target_type, "target_id": op.target_id}
|
||||
for op in patch.operations
|
||||
],
|
||||
"timestamp": patch.generated_at
|
||||
}, sort_keys=True)
|
||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Core Engine Validators
|
||||
|
||||
物理校验模块,处理元叙事开关和物理连贯性验证。
|
||||
"""
|
||||
|
||||
from .fourth_wall_validator import (
|
||||
FourthWallValidator,
|
||||
FourthWallState,
|
||||
ValidationMode,
|
||||
PhysicalRule,
|
||||
ValidationRule,
|
||||
ValidationIssue,
|
||||
get_validator,
|
||||
enable_meta_mode,
|
||||
disable_meta_mode,
|
||||
is_meta_mode,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FourthWallValidator",
|
||||
"FourthWallState",
|
||||
"ValidationMode",
|
||||
"PhysicalRule",
|
||||
"ValidationRule",
|
||||
"ValidationIssue",
|
||||
"get_validator",
|
||||
"enable_meta_mode",
|
||||
"disable_meta_mode",
|
||||
"is_meta_mode",
|
||||
]
|
||||
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Fourth Wall Validator (第四面墙校验器)
|
||||
|
||||
当元叙事开关开启时,关闭物理连贯性校验,
|
||||
允许"反规则"写法:梦境、意识流、打破第四面墙等。
|
||||
|
||||
当开关关闭时,恢复标准物理校验。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class ValidationMode(Enum):
|
||||
NORMAL = "normal" # 标准物理校验
|
||||
META = "meta" # 元叙事模式,跳过物理校验
|
||||
|
||||
|
||||
class PhysicalRule(Enum):
|
||||
"""物理校验规则类型"""
|
||||
REALM_CONSISTENCY = "realm_consistency" # 境界一致性
|
||||
TIME_CONTINUITY = "time_continuity" # 时间连续性
|
||||
LOCATION_COHERENCE = "location_coherence" # 地点连贯性
|
||||
POWER_BALANCE = "power_balance" # 战力平衡
|
||||
CAUSALITY = "causality" # 因果关系
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationRule:
|
||||
"""校验规则"""
|
||||
type: PhysicalRule
|
||||
enabled: bool
|
||||
description: str
|
||||
severity: str = "high" # critical/high/medium/low
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""校验问题"""
|
||||
rule: PhysicalRule
|
||||
chapter: int
|
||||
description: str
|
||||
severity: str
|
||||
entity_id: Optional[str] = None
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FourthWallState:
|
||||
"""第四面墙状态"""
|
||||
mode: ValidationMode = ValidationMode.NORMAL
|
||||
enabled_rules: List[PhysicalRule] = field(default_factory=list)
|
||||
bypassed_rules: List[PhysicalRule] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_meta_mode(self) -> bool:
|
||||
return self.mode == ValidationMode.META
|
||||
|
||||
|
||||
class FourthWallValidator:
|
||||
"""
|
||||
第四面墙校验器
|
||||
|
||||
用法:
|
||||
1. 当 Meta_Narrative_Panel.jsx 的 Toggle 开启时,调用 enable_meta_mode()
|
||||
2. 当 Toggle 关闭时,调用 disable_meta_mode()
|
||||
3. 验证时调用 validate() 方法
|
||||
"""
|
||||
|
||||
# 默认启用的物理校验规则
|
||||
DEFAULT_ENABLED_RULES = [
|
||||
PhysicalRule.REALM_CONSISTENCY,
|
||||
PhysicalRule.TIME_CONTINUITY,
|
||||
PhysicalRule.LOCATION_COHERENCE,
|
||||
PhysicalRule.POWER_BALANCE,
|
||||
PhysicalRule.CAUSALITY,
|
||||
]
|
||||
|
||||
# 元叙事模式下放行的规则(这些在元叙事模式下被跳过)
|
||||
META_BYPASSABLE_RULES = [
|
||||
PhysicalRule.TIME_CONTINUITY, # 允许时间跳跃
|
||||
PhysicalRule.LOCATION_COHERENCE, # 允许瞬间移动
|
||||
PhysicalRule.POWER_BALANCE, # 允许战力波动
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._state = FourthWallState(
|
||||
enabled_rules=list(self.DEFAULT_ENABLED_RULES),
|
||||
bypassed_rules=[]
|
||||
)
|
||||
self._listeners: List[callable] = []
|
||||
|
||||
@property
|
||||
def state(self) -> FourthWallState:
|
||||
return self._state
|
||||
|
||||
def enable_meta_mode(self) -> None:
|
||||
"""开启元叙事模式,关闭物理校验"""
|
||||
self._state.mode = ValidationMode.META
|
||||
self._state.bypassed_rules = list(self.META_BYPASSABLE_RULES)
|
||||
self._notify_listeners()
|
||||
print("[FourthWall] 元叙事模式已激活 - 物理校验已关闭")
|
||||
|
||||
def disable_meta_mode(self) -> None:
|
||||
"""关闭元叙事模式,恢复物理校验"""
|
||||
self._state.mode = ValidationMode.NORMAL
|
||||
self._state.bypassed_rules = []
|
||||
self._notify_listeners()
|
||||
print("[FourthWall] 标准模式已激活 - 物理校验已恢复")
|
||||
|
||||
def toggle(self) -> ValidationMode:
|
||||
"""切换模式"""
|
||||
if self._state.is_meta_mode:
|
||||
self.disable_meta_mode()
|
||||
else:
|
||||
self.enable_meta_mode()
|
||||
return self._state.mode
|
||||
|
||||
def validate(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]] = None
|
||||
) -> List[ValidationIssue]:
|
||||
"""
|
||||
验证章节物理连贯性
|
||||
|
||||
在元叙事模式下,返回空列表(跳过所有物理校验)
|
||||
"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 元叙事模式:跳过所有物理校验
|
||||
if self._state.is_meta_mode:
|
||||
return issues
|
||||
|
||||
# 标准模式:执行校验
|
||||
for rule_type in self._state.enabled_rules:
|
||||
if rule_type not in self._state.bypassed_rules:
|
||||
rule_issues = self._validate_rule(
|
||||
rule_type, chapter, entities, world_state, previous_state
|
||||
)
|
||||
issues.extend(rule_issues)
|
||||
|
||||
return issues
|
||||
|
||||
def _validate_rule(
|
||||
self,
|
||||
rule: PhysicalRule,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""根据规则类型执行校验"""
|
||||
validators = {
|
||||
PhysicalRule.REALM_CONSISTENCY: self._check_realm_consistency,
|
||||
PhysicalRule.TIME_CONTINUITY: self._check_time_continuity,
|
||||
PhysicalRule.LOCATION_COHERENCE: self._check_location_coherence,
|
||||
PhysicalRule.POWER_BALANCE: self._check_power_balance,
|
||||
PhysicalRule.CAUSALITY: self._check_causality,
|
||||
}
|
||||
|
||||
validator = validators.get(rule)
|
||||
if validator:
|
||||
return validator(chapter, entities, world_state, previous_state)
|
||||
return []
|
||||
|
||||
def _check_realm_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查境界一致性"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 检查主角境界是否合理
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
if protagonist:
|
||||
current_realm = protagonist.get("realm")
|
||||
previous_realm = previous_state.get("protagonist", {}).get("realm") if previous_state else None
|
||||
|
||||
if previous_realm and current_realm != previous_realm:
|
||||
# 境界变化超过1级
|
||||
if not self._is_valid_realm_jump(previous_realm, current_realm):
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.REALM_CONSISTENCY,
|
||||
chapter=chapter,
|
||||
description=f"境界跳跃不合理: {previous_realm} → {current_realm}",
|
||||
severity="high",
|
||||
entity_id=protagonist.get("id"),
|
||||
suggested_fix="补充修炼/机缘说明"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_time_continuity(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查时间连续性"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
current_time = world_state.get("time_of_day")
|
||||
previous_time = previous_state.get("world_state", {}).get("time_of_day") if previous_state else None
|
||||
|
||||
if previous_time and current_time:
|
||||
# 检查时间是否倒退
|
||||
time_order = {"早晨": 0, "上午": 1, "中午": 2, "下午": 3, "傍晚": 4, "夜晚": 5, "深夜": 6, "凌晨": 7}
|
||||
if time_order.get(previous_time, -1) > time_order.get(current_time, -1):
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.TIME_CONTINUITY,
|
||||
chapter=chapter,
|
||||
description=f"时间倒退: {previous_time} → {current_time}",
|
||||
severity="medium",
|
||||
suggested_fix="添加时间跳跃说明"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_location_coherence(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查地点连贯性"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
current_loc = world_state.get("location")
|
||||
previous_loc = previous_state.get("world_state", {}).get("location") if previous_state else None
|
||||
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
|
||||
if previous_loc and current_loc and protagonist:
|
||||
# 如果地点变化但没有过渡说明
|
||||
if current_loc != previous_loc:
|
||||
has_transition = self._check_transition_exists(chapter, previous_loc, current_loc)
|
||||
if not has_transition:
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.LOCATION_COHERENCE,
|
||||
chapter=chapter,
|
||||
description=f"地点跳跃无过渡: {previous_loc} → {current_loc}",
|
||||
severity="low",
|
||||
entity_id=protagonist.get("id"),
|
||||
suggested_fix="添加移动过程描写"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_power_balance(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查战力平衡"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 检查是否有越太多级战斗
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
opponents = [e for e in entities if e.get("is_opponent")]
|
||||
|
||||
if protagonist and opponents:
|
||||
p_realm = protagonist.get("realm_level", 0)
|
||||
for opp in opponents:
|
||||
o_realm = opp.get("realm_level", 0)
|
||||
if p_realm - o_realm > 2:
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.POWER_BALANCE,
|
||||
chapter=chapter,
|
||||
description=f"战力差距过大: {p_realm} vs {o_realm}",
|
||||
severity="medium",
|
||||
entity_id=opp.get("id"),
|
||||
suggested_fix="增加战斗难度或金手指解释"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_causality(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查因果关系"""
|
||||
# 简化实现:检查是否有突兀的力量获得
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
new_skills = world_state.get("new_skills_gained", [])
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
|
||||
if new_skills and protagonist:
|
||||
has_setup = self._check_setup_exists(chapter, new_skills)
|
||||
if not has_setup:
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.CAUSALITY,
|
||||
chapter=chapter,
|
||||
description=f"突兀获得技能: {new_skills}",
|
||||
severity="high",
|
||||
entity_id=protagonist.get("id"),
|
||||
suggested_fix="补充技能来源铺垫"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _is_valid_realm_jump(self, from_realm: str, to_realm: str) -> bool:
|
||||
"""判断境界跳跃是否合理"""
|
||||
# 简化实现
|
||||
valid_jumps = {
|
||||
"炼气期一层": ["炼气期二层"],
|
||||
"炼气期二层": ["炼气期三层"],
|
||||
"筑基期一层": ["筑基期二层"],
|
||||
}
|
||||
return to_realm in valid_jumps.get(from_realm, [])
|
||||
|
||||
def _check_transition_exists(self, chapter: int, from_loc: str, to_loc: str) -> bool:
|
||||
"""检查过渡是否存在"""
|
||||
# 简化实现:应该读取章节正文检查
|
||||
return False
|
||||
|
||||
def _check_setup_exists(self, chapter: int, skills: List[str]) -> bool:
|
||||
"""检查铺垫是否存在"""
|
||||
# 简化实现:应该读取章节正文检查
|
||||
return False
|
||||
|
||||
def subscribe(self, callback: callable) -> None:
|
||||
"""订阅状态变化"""
|
||||
self._listeners.append(callback)
|
||||
|
||||
def unsubscribe(self, callback: callable) -> None:
|
||||
"""取消订阅"""
|
||||
if callback in self._listeners:
|
||||
self._listeners.remove(callback)
|
||||
|
||||
def _notify_listeners(self) -> None:
|
||||
"""通知所有监听器"""
|
||||
for callback in self._listeners:
|
||||
try:
|
||||
callback(self._state)
|
||||
except Exception as e:
|
||||
print(f"[FourthWall] 通知监听器失败: {e}")
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""获取当前配置"""
|
||||
return {
|
||||
"mode": self._state.mode.value,
|
||||
"enabled_rules": [r.value for r in self._state.enabled_rules],
|
||||
"bypassed_rules": [r.value for r in self._state.bypassed_rules],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_config(config: Dict[str, Any]) -> "FourthWallValidator":
|
||||
"""从配置恢复"""
|
||||
validator = FourthWallValidator()
|
||||
if config.get("mode") == ValidationMode.META.value:
|
||||
validator.enable_meta_mode()
|
||||
return validator
|
||||
|
||||
|
||||
# 全局实例
|
||||
_validator_instance: Optional[FourthWallValidator] = None
|
||||
|
||||
|
||||
def get_validator() -> FourthWallValidator:
|
||||
"""获取全局校验器实例"""
|
||||
global _validator_instance
|
||||
if _validator_instance is None:
|
||||
_validator_instance = FourthWallValidator()
|
||||
return _validator_instance
|
||||
|
||||
|
||||
def enable_meta_mode() -> None:
|
||||
"""快捷函数:开启元叙事模式"""
|
||||
get_validator().enable_meta_mode()
|
||||
|
||||
|
||||
def disable_meta_mode() -> None:
|
||||
"""快捷函数:关闭元叙事模式"""
|
||||
get_validator().disable_meta_mode()
|
||||
|
||||
|
||||
def is_meta_mode() -> bool:
|
||||
"""快捷函数:检查是否在元叙事模式"""
|
||||
return get_validator().state.is_meta_mode
|
||||
@@ -0,0 +1,30 @@
|
||||
# 文档中心
|
||||
|
||||
本目录承载 `README.md` 之外的详细说明,按模块拆分:
|
||||
|
||||
## 文档导航
|
||||
|
||||
- [架构与模块](architecture.md)
|
||||
- [命令详解](commands.md)
|
||||
- [RAG 与配置](rag-and-config.md)
|
||||
- [题材模板](genres.md)
|
||||
- [运维与恢复](operations.md)
|
||||
|
||||
## OpenNovel Workspace 结构文档
|
||||
|
||||
NovelMaster 基于 OpenNovel Workspace (ONW) 5.0 架构,文档对应关系如下:
|
||||
|
||||
| ONW 目录 | 说明 | 文档 |
|
||||
|----------|------|------|
|
||||
| `openspec/` | 全局数据标准与契约 | [openspec.md](openspec.md) |
|
||||
| `core_engine/` | 融合状态机与记忆引擎 | [core_engine.md](core_engine.md) |
|
||||
| `matrices/` | 心理学与叙事学武器库 | [matrices.md](matrices.md) |
|
||||
| `interfaces/` | 人机协同创作者上帝控制台 | [interfaces.md](interfaces.md) |
|
||||
| `agents/` | 审查与执行矩阵 | [agents.md](agents.md) |
|
||||
|
||||
## 建议阅读顺序
|
||||
|
||||
1. 先看 `../README.md`(安装与上手)
|
||||
2. 再看 `architecture.md`(理解系统设计)
|
||||
3. 再看 `openspec.md` / `core_engine.md` / `matrices.md` / `interfaces.md` / `agents.md`(理解 ONW 架构)
|
||||
4. 最后按需查阅命令和运维文档
|
||||
@@ -0,0 +1,331 @@
|
||||
# Noma 使用教程 - 从零开始写小说
|
||||
|
||||
## 目录
|
||||
|
||||
1. [快速开始](#1-快速开始)
|
||||
2. [Dashboard 使用指南](#2-dashboard-使用指南)
|
||||
3. [完整写作流程演示](#3-完整写作流程演示)
|
||||
4. [范式转移教程](#4-范式转移教程)
|
||||
5. [常见问题](#5-常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速开始
|
||||
|
||||
### 1.1 环境准备
|
||||
|
||||
```bash
|
||||
# 1. 下载 noma 项目
|
||||
cd novelmaster
|
||||
|
||||
# 2. 安装 Python 依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. 安装 Node.js 依赖 (用于 Dashboard)
|
||||
cd interfaces/web_dashboard/frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### 1.2 创建第一个项目
|
||||
|
||||
```bash
|
||||
# 方式一:交互式创建
|
||||
python -m scripts.noma init
|
||||
|
||||
# 方式二:命令行创建
|
||||
python -m scripts.noma init --title "我的修仙小说" --genre "xuanhuan"
|
||||
```
|
||||
|
||||
项目创建后会自动生成:
|
||||
```
|
||||
my_novel/
|
||||
├── .noma/novel_data/ # 系统数据 (不要手动修改)
|
||||
│ ├── state.json # 项目状态
|
||||
│ ├── ledger.json # 资产账本
|
||||
│ └── index.db # SQLite 数据库
|
||||
├── 正文/ # 你的小说正文
|
||||
├── 大纲/ # 章节大纲
|
||||
└── 设定集/ # 世界观设定
|
||||
```
|
||||
|
||||
### 1.3 启动 Dashboard
|
||||
|
||||
```bash
|
||||
# 在项目目录下启动
|
||||
cd my_novel
|
||||
python -m interfaces.web_dashboard.server
|
||||
|
||||
# 或指定项目路径
|
||||
python -m interfaces.web_dashboard.server --project-root /path/to/my_novel
|
||||
```
|
||||
|
||||
浏览器自动打开 **http://127.0.0.1:8765**
|
||||
|
||||
---
|
||||
|
||||
## 2. Dashboard 使用指南
|
||||
|
||||
### 2.1 界面概览
|
||||
|
||||
Dashboard 包含以下模块:
|
||||
|
||||
| 模块 | 功能 |
|
||||
|------|------|
|
||||
| 📊 数据总览 | 项目进度、主角状态、伏笔追踪 |
|
||||
| 👤 设定词典 | 角色/地点/物品/势力管理 |
|
||||
| 🕸️ 关系图谱 | 3D 可视化关系网络 |
|
||||
| 📝 章节一览 | 所有章节列表和统计 |
|
||||
| 📁 文档浏览 | 正文/大纲/设定集文件浏览 |
|
||||
| 🔥 追读力 | 章节吸引力分析 |
|
||||
|
||||
### 2.2 数据总览
|
||||
|
||||
显示当前项目最重要的指标:
|
||||
|
||||
- **总字数/目标字数** - 进度条展示
|
||||
- **当前章节** - 进度百分比
|
||||
- **主角状态** - 境界/位置
|
||||
- **未回收伏笔** - 警告数量
|
||||
- **Strand Weave** - Quest/Fire/Constellation 比例
|
||||
|
||||
### 2.3 设定词典
|
||||
|
||||
查看和管理所有实体:
|
||||
|
||||
1. **筛选** - 按类型筛选 (角色/地点/物品/势力/招式)
|
||||
2. **查看详情** - 点击实体查看
|
||||
3. **状态历史** - 查看属性变化记录
|
||||
|
||||
### 2.4 章节一览
|
||||
|
||||
| 列 | 说明 |
|
||||
|----|------|
|
||||
| 章节 | 第X章 |
|
||||
| 标题 | 章节标题 |
|
||||
| 字数 | 字数统计 |
|
||||
| 地点 | 场景位置 |
|
||||
| 角色 | 出场角色 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 完整写作流程演示
|
||||
|
||||
下面以"修仙小说"为例,演示从0开始写一部小说。
|
||||
|
||||
### 3.1 Step 1: 创建项目
|
||||
|
||||
```bash
|
||||
# 创建项目
|
||||
python -m scripts.noma init --title "逆天修仙传" --genre "xuanhuan"
|
||||
```
|
||||
|
||||
### 3.2 Step 2: 设定创世契约
|
||||
|
||||
编辑 `.noma/novel_data/genesis_contract.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"core_desire": {
|
||||
"primary": "power",
|
||||
"secondary": ["revenge", "immortality"],
|
||||
"taboos": ["betray family"]
|
||||
},
|
||||
"ethical_inversion": {
|
||||
"inverted_norms": ["benevolence", "humility"],
|
||||
"world_rules": ["弱肉强食", "实力为尊"]
|
||||
},
|
||||
"core_spectacle": {
|
||||
"ordinary_state": "修士争斗,资源争夺",
|
||||
"spectacle_trigger": "以弱胜强,吞噬突破"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Step 3: 规划大纲
|
||||
|
||||
创建 `大纲/总纲.md`:
|
||||
|
||||
```markdown
|
||||
# 《逆天修仙传》总纲
|
||||
|
||||
## 主角
|
||||
- 姓名: 叶尘
|
||||
- 初始: 废物少爷,丹田破碎
|
||||
- 目标: 登顶修仙巅峰,为父报仇
|
||||
|
||||
## 境界体系
|
||||
炼气 → 筑基 → 金丹 → 元婴 → 化神 → 大乘 → 渡劫
|
||||
|
||||
## 主线
|
||||
1. 觉醒金手指 (1-10章)
|
||||
2. 拜入宗门 (11-30章)
|
||||
3. 宗门大比 (31-50章)
|
||||
4. 揭开身世 (51-80章)
|
||||
5. 登顶之路 (81-100章)
|
||||
```
|
||||
|
||||
### 3.4 Step 4: 写章节大纲
|
||||
|
||||
创建 `大纲/第0001章.md`:
|
||||
|
||||
```markdown
|
||||
# 第0001章大纲: 废物少爷
|
||||
|
||||
## 目标
|
||||
- 字数: 3000字
|
||||
- Strand: Quest
|
||||
- 张力: 30/100
|
||||
|
||||
## 情节
|
||||
1. 叶尘被家族放弃
|
||||
2. 父亲失踪之谜
|
||||
3. 意外获得金手指
|
||||
|
||||
## 钩子
|
||||
- 钩子1: 父亲失踪 (长线伏笔)
|
||||
- 钩子2: 金手指来历 (中线伏笔)
|
||||
|
||||
## 禁止
|
||||
- 不要让主角立即爆发
|
||||
- 不要过早揭示金手指全貌
|
||||
```
|
||||
|
||||
### 3.5 Step 5: 写正文
|
||||
|
||||
基于大纲写作,参考 `agents/writers/immersive-writer.md` 的感官描写指导。
|
||||
|
||||
### 3.6 Step 6: 审查
|
||||
|
||||
```bash
|
||||
# 审查第1章
|
||||
python -m scripts.noma review --chapter 1
|
||||
```
|
||||
|
||||
审查项:
|
||||
- 连贯性检查
|
||||
- 张力检查
|
||||
- 一致性检查
|
||||
- 爽点密度
|
||||
|
||||
### 3.7 Step 7: 数据回写
|
||||
|
||||
系统自动将以下数据写入数据库:
|
||||
- 实体提取 (角色/地点/物品)
|
||||
- 章节摘要
|
||||
- 状态变化
|
||||
- RAG 向量索引
|
||||
|
||||
### 3.8 Step 8: 继续下一章
|
||||
|
||||
重复 Step 4-7。
|
||||
|
||||
---
|
||||
|
||||
## 4. 范式转移教程
|
||||
|
||||
### 4.1 什么是范式转移?
|
||||
|
||||
范式转移是指在写作过程中中途改变世界观或设定:
|
||||
|
||||
> 例子:"写到第50章,突然想把修仙改成克苏鲁风格"
|
||||
|
||||
### 4.2 传统方式的问题
|
||||
|
||||
- 需要手动修改所有历史章节
|
||||
- 容易产生逻辑矛盾
|
||||
- 工作量巨大
|
||||
|
||||
### 4.3 Noma 的解决方案:Retcon
|
||||
|
||||
Noma 提供热补丁机制,自动处理范式转移:
|
||||
|
||||
```
|
||||
1. 输入新设定 → "改成克苏鲁风格"
|
||||
↓
|
||||
2. 扫描账本 → 发现冲突 (浩然正气剑)
|
||||
↓
|
||||
3. 生成补丁 → "将正气剑转为邪神之物"
|
||||
↓
|
||||
4. 人工确认 → 选择要应用的变更
|
||||
↓
|
||||
5. 自动更新 → 数据库/缓存/上下文全部刷新
|
||||
↓
|
||||
6. 继续写作 → 基于新设定继续
|
||||
```
|
||||
|
||||
### 4.4 操作步骤
|
||||
|
||||
1. 打开 Dashboard
|
||||
2. 点击左侧菜单 **"Retcon 仲裁台"**
|
||||
3. 输入新设定描述
|
||||
4. 查看冲突分析
|
||||
5. 选择要应用的变更
|
||||
6. 点击 **"应用补丁"**
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题
|
||||
|
||||
### Q: 如何查看项目状态?
|
||||
|
||||
```bash
|
||||
python -m scripts.noma query state
|
||||
```
|
||||
|
||||
或打开 Dashboard → 数据总览
|
||||
|
||||
### Q: 如何备份项目?
|
||||
|
||||
```bash
|
||||
python -m scripts.noma backup
|
||||
```
|
||||
|
||||
### Q: 如何迁移到新设备?
|
||||
|
||||
1. 复制整个项目文件夹
|
||||
2. 安装依赖
|
||||
3. 运行 `python -m scripts.noma migrate`
|
||||
|
||||
### Q: Dashboard 无法启动?
|
||||
|
||||
```bash
|
||||
# 检查端口是否被占用
|
||||
lsof -i :8765
|
||||
|
||||
# 指定其他端口
|
||||
python -m interfaces.web_dashboard.server --port 8888
|
||||
```
|
||||
|
||||
### Q: 如何导入已有的小说?
|
||||
|
||||
```bash
|
||||
# 放到 正文/ 目录
|
||||
mv my_novel.txt 正文/第0001章-标题.md
|
||||
|
||||
# 运行实体提取
|
||||
python -m scripts.noma extract-entities --chapter 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录: CLI 命令速查
|
||||
|
||||
| 命令 | 功能 |
|
||||
|------|------|
|
||||
| `noma init` | 初始化项目 |
|
||||
| `noma plan` | 规划大纲 |
|
||||
| `noma write` | 写章节 |
|
||||
| `noma review` | 审查章节 |
|
||||
| `noma query` | 查询状态 |
|
||||
| `noma resume` | 恢复任务 |
|
||||
| `noma dashboard` | 启动面板 |
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看 [架构文档](architecture.md)
|
||||
- 查看 [命令详解](commands.md)
|
||||
- 查看 [题材模板](genres.md)
|
||||
@@ -0,0 +1,70 @@
|
||||
# agents/ 目录说明
|
||||
|
||||
## 目录定位
|
||||
|
||||
`agents/` 是 OpenNovel Workspace 的**支柱4**,承载审查与执行矩阵。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
agents/
|
||||
├── planners/ # 负责调度 Hooks 平账与挂载张力模型
|
||||
├── writers/
|
||||
│ └── immersive-writer.md # 负责感官颗粒度放大
|
||||
├── catchup_agent.py # [★特性] 负责计算休眠角色跨越时间后的状态
|
||||
└── checkers/ # 大一统 Critic Agent
|
||||
├── tension_checker.py # [★特性] 取代原爽点审查,校验情绪压强释放
|
||||
└── consistency_checker.py # [★特性] 受"元叙事开关"控制的动态物理连贯性校验
|
||||
```
|
||||
|
||||
## planners/ 子目录
|
||||
|
||||
负责调度 Hooks 平账与挂载张力模型:
|
||||
|
||||
- 分析当前章节的追读力状态
|
||||
- 选择并加载适当的 `matrices/catharsis_models/` 模型
|
||||
- 协调 Writer Agent 和 Checker Agent 的工作流程
|
||||
|
||||
## writers/ 子目录
|
||||
|
||||
### immersive-writer.md
|
||||
|
||||
负责感官颗粒度放大:
|
||||
|
||||
- 将大纲中的场景描述转化为高密度感官的文字
|
||||
- 调用 catharsis model 提供情绪张力
|
||||
- 保持与 `genesis_contract.json` 的一致性
|
||||
|
||||
## catchup_agent.py — 惰性时间推演
|
||||
|
||||
[★特性] 当休眠角色被唤醒时(如场景切换回某个背景人物):
|
||||
|
||||
1. 读取该角色最后一次出现的时间和状态
|
||||
2. 根据 `Tick` 时钟计算跨越的时间差
|
||||
3. 惰性推演该角色在此期间可能发生的状态变化
|
||||
4. 更新 `ledger.ts` 中的角色状态
|
||||
|
||||
## checkers/ 子目录
|
||||
|
||||
### tension_checker.py — 情绪压强校验
|
||||
|
||||
[★特性] 取代原爽点审查:
|
||||
|
||||
- 校验情绪压强的积累与释放是否符合选定的 catharsis model
|
||||
- 检测"降维打击"、"禁忌僭越"等爽感路由的执行效果
|
||||
- 与 `ledger.ts` 中的 hooks_pool 压强联动
|
||||
|
||||
### consistency_checker.py — 动态物理连贯性校验
|
||||
|
||||
[★特性] 受"元叙事开关"控制:
|
||||
|
||||
- 当 `Meta_Narrative_Panel.jsx` 中的第四面墙 Toggle 关闭时,执行严格的物理连贯性校验(战力/地点/时间线)
|
||||
- 当 Toggle 开启时,自动跳过物理坐标和时间连贯性审查,允许"反规则"写法
|
||||
- 调用 `genesis_contract.json` 中的约束作为校验基准
|
||||
|
||||
## 与其他模块的联动
|
||||
|
||||
- **openspec/**: `genesis_contract.json` 驱动 consistency_checker 的校验规则
|
||||
- **matrices/**: tension_checker 调用 catharsis_models 执行情绪压强校验
|
||||
- **core_engine/state_manager/**: catchup_agent 订阅 handoff.ts 的脏标记;checker 结果回写 ledger.ts
|
||||
- **interfaces/web_dashboard/**: Dashboard 展示审查结果和 Agent 状态
|
||||
@@ -0,0 +1,85 @@
|
||||
# 系统架构与模块设计
|
||||
|
||||
## 核心理念
|
||||
|
||||
### 防幻觉三定律
|
||||
|
||||
| 定律 | 说明 | 执行方式 |
|
||||
|------|------|---------|
|
||||
| **大纲即法律** | 遵循大纲,不擅自发挥 | Context Agent 强制加载章节大纲 |
|
||||
| **设定即物理** | 遵守设定,不自相矛盾 | Consistency Checker 实时校验 |
|
||||
| **发明需识别** | 新实体必须入库管理 | Data Agent 自动提取并消歧 |
|
||||
|
||||
### Strand Weave 节奏系统
|
||||
|
||||
| Strand | 含义 | 理想占比 | 说明 |
|
||||
|--------|------|---------|------|
|
||||
| **Quest** | 主线剧情 | 60% | 推动核心冲突 |
|
||||
| **Fire** | 感情线 | 20% | 人物关系发展 |
|
||||
| **Constellation** | 世界观扩展 | 20% | 背景/势力/设定 |
|
||||
|
||||
节奏红线:
|
||||
|
||||
- Quest 连续不超过 5 章
|
||||
- Fire 断档不超过 10 章
|
||||
- Constellation 断档不超过 15 章
|
||||
|
||||
## OpenNovel Workspace 总体架构
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Claude Code │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Skills (init / plan / write / review / query / dashboard)│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Agents: Context / Data / 多维 Checker / Catchup Agent │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Layer: state.json / index.db / vectors.db / ledger │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ OpenNovel Workspace (ONW) │
|
||||
│ ├── openspec/ (欲望沙盒与定制伦理) │
|
||||
│ ├── core_engine/ (状态机与记忆引擎 + LOD/Tick/Retcon) │
|
||||
│ ├── matrices/ (多维爽感路由) │
|
||||
│ ├── interfaces/ (人机协同 IDE) │
|
||||
│ └── agents/ (审查与执行矩阵) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 双 Agent 架构
|
||||
|
||||
### Context Agent(读)
|
||||
|
||||
职责:在写作前构建"创作任务书",提供本章上下文、约束和追读力策略。
|
||||
|
||||
### Data Agent(写)
|
||||
|
||||
职责:从正文提取实体与状态变化,更新 `state.json`、`index.db`、`vectors.db`,保证数据链闭环。
|
||||
|
||||
## 六维并行审查
|
||||
|
||||
| Checker | 检查重点 |
|
||||
|---------|---------|
|
||||
| High-point Checker | 爽点密度与质量 |
|
||||
| Consistency Checker | 设定一致性(战力/地点/时间线) |
|
||||
| Pacing Checker | Strand 比例与断档 |
|
||||
| OOC Checker | 人物行为是否偏离人设 |
|
||||
| Continuity Checker | 场景与叙事连贯性 |
|
||||
| Reader-pull Checker | 钩子强度、期待管理、追读力 |
|
||||
|
||||
## 四大独创系统特性
|
||||
|
||||
### 1. 欲望沙盒与定制伦理
|
||||
|
||||
打破传统网文分类,以"终极白日梦"和"道德免责声明"为最高执行上下文。核心数据契约包含 `core_desire`(核心欲望)、`ethical_inversion`(废除的现实道德)、`core_spectacle`(奇观日常化)。
|
||||
|
||||
### 2. 多维爽感路由
|
||||
|
||||
不再死守"打脸升级",动态调用降维打击、禁忌僭越、延迟满足等高级心理学张力模型。调度机制:大纲 Planner Agent 在规划情节时,从 `matrices/catharsis_models/` 动态加载模型指导 Writer Agent。
|
||||
|
||||
### 3. 元叙事与法则覆写
|
||||
|
||||
允许人类一键降级物理校验,实现主角打破第四面墙、梦境潜入等"反规则"的神来之笔。界面上的物理开关开启后,自动关闭物理坐标和时间连贯性审查。
|
||||
|
||||
### 4. 意图漂移仲裁与热补丁
|
||||
|
||||
完美接纳作者中途修改设定的需求,自动扫描全局账本,生成设定兼容补丁,拒绝死锁。`Retcon_Arbitrator.jsx` 界面分析冲突并生成补丁方案。
|
||||
@@ -0,0 +1,101 @@
|
||||
# 命令详解
|
||||
|
||||
## `/noma-init`
|
||||
|
||||
用途:初始化小说项目(目录、设定模板、状态文件)。
|
||||
|
||||
产出:
|
||||
|
||||
- `.noma/novel_data/state.json`
|
||||
- `设定集/`
|
||||
- `大纲/总纲.md`
|
||||
|
||||
## `/noma-plan [卷号]`
|
||||
|
||||
用途:生成卷级规划与章节大纲。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-plan 1
|
||||
/noma-plan 2-3
|
||||
```
|
||||
|
||||
## `/noma-write [章号]`
|
||||
|
||||
用途:执行完整章节创作流程(上下文 → 草稿 → 审查 → 润色 → 数据落盘)。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-write 1
|
||||
/noma-write 45
|
||||
```
|
||||
|
||||
常见模式:
|
||||
|
||||
- 标准模式:全流程
|
||||
- 快速模式:`--fast`
|
||||
- 极简模式:`--minimal`
|
||||
|
||||
## `/noma-review [范围]`
|
||||
|
||||
用途:对历史章节做多维质量审查。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-review 1-5
|
||||
/noma-review 45
|
||||
```
|
||||
|
||||
## `/noma-query [关键词]`
|
||||
|
||||
用途:查询角色、伏笔、节奏、状态等运行时信息。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-query 萧炎
|
||||
/noma-query 伏笔
|
||||
/noma-query 紧急
|
||||
```
|
||||
|
||||
## `/noma-resume`
|
||||
|
||||
用途:任务中断后自动识别断点并恢复。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-resume
|
||||
```
|
||||
|
||||
## `/noma-dashboard`
|
||||
|
||||
用途:启动只读可视化面板,查看项目状态、实体关系、章节与大纲内容。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-dashboard
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 默认只读,不会修改项目文件
|
||||
- 适合排查上下文、实体关系和章节进度
|
||||
|
||||
## `/noma-learn [内容]`
|
||||
|
||||
用途:从当前会话或用户输入中提取可复用写作模式,并写入项目记忆。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
/noma-learn "本章的危机钩设计很有效,悬念拉满"
|
||||
```
|
||||
|
||||
产出:
|
||||
|
||||
- `.noma/novel_data/project_memory.json`
|
||||
@@ -0,0 +1,65 @@
|
||||
# core_engine/ 目录说明
|
||||
|
||||
## 目录定位
|
||||
|
||||
`core_engine/` 是 OpenNovel Workspace 的**支柱2&3**,融合状态机与记忆引擎,承载"视锥剔除与热补丁防腐"特性。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
core_engine/
|
||||
├── state_manager/
|
||||
│ ├── handoff.ts # [★特性] 帧交接协议 (含 LOD视锥剔除, Tick时钟)
|
||||
│ ├── ledger.ts # 动态资产与负债账本
|
||||
│ └── retcon_manager.ts # [★特性] 意图漂移管理与热补丁生成器
|
||||
│
|
||||
├── memory_rag/
|
||||
│ ├── vectorstore_utils.py # 向量存储工具
|
||||
│ └── context_cache.py # 上下文静态缓存与 Hash 脏标记更新
|
||||
│
|
||||
└── configurators/ # Cursor/Windsurf AI IDE 直接挂载适配器
|
||||
```
|
||||
|
||||
## state_manager/ 子目录
|
||||
|
||||
### handoff.ts — 帧交接协议
|
||||
|
||||
引入 **LOD (Level of Detail / 视锥剔除)** 与 **Tick (全局时钟)** 机制:
|
||||
|
||||
- **LOD 视锥剔除**:只精确交接"在场"人物,休眠的背景人物不参与当前帧计算
|
||||
- **Tick 全局时钟**:记录多线程悬置动作(Imminent Actions),确保时间线一致性
|
||||
- **Handoff Commit**:AI 生成的下一章 `handoff.json` Diff 需等待人类审查并 Commit
|
||||
|
||||
### ledger.ts — 动态资产与负债账本
|
||||
|
||||
跟踪故事中的所有资产(正向积累)和负债(待兑现的承诺/伏笔):
|
||||
|
||||
- 角色获得的能力、道具、资源(资产)
|
||||
- 埋下的伏笔、承诺的冲突、待解决的悬念(负债)
|
||||
- 当"元叙事开关"开启时,可监控 `hooks_pool` 压强,触发黑天鹅注入
|
||||
|
||||
### retcon_manager.ts — 意图漂移管理与热补丁生成器
|
||||
|
||||
当作者中途修改设定(如第150章要将《正统修仙》转入《克苏鲁修仙》):
|
||||
|
||||
1. 扫描 `ledger.ts` 发现主角在第30章获得的【浩然正气剑】与新设定互斥
|
||||
2. 生成补丁方案:将浩然正气剑设定为"上古邪神骨殖的伪装",触发反噬转化为生存负债
|
||||
3. 作者确认后,刷新 RAG 缓存,热补丁生效
|
||||
|
||||
## memory_rag/ 子目录
|
||||
|
||||
### context_cache.py
|
||||
|
||||
- 上下文静态缓存:存储已确认的剧情上下文
|
||||
- Hash 脏标记更新:当 `retcon_manager.ts` 生成补丁时,强制清空受影响缓存
|
||||
- 热更新网关:哈希监听器确保补丁作为最高权重压制历史设定幻觉
|
||||
|
||||
## configurators/ 子目录
|
||||
|
||||
AI IDE 适配器(如 Cursor / Windsurf),使核心引擎可以直接挂载到这些编辑器中。
|
||||
|
||||
## 与其他模块的联动
|
||||
|
||||
- **openspec/**: `genesis_contract.json` 中的三大变量驱动状态校验逻辑
|
||||
- **agents/catchup_agent.py**: 订阅 `handoff.ts` 的脏标记,当休眠人物被唤醒时进行惰性时间推演
|
||||
- **interfaces/web_dashboard/**: 展示 `Retcon_Arbitrator.jsx`(设定修改冲突分析)和 `Handoff_Diff_View.jsx`(交接审查)
|
||||
@@ -0,0 +1,53 @@
|
||||
# 题材模板说明
|
||||
|
||||
系统内置 37+ 网文题材模板,支持单题材与复合题材。
|
||||
|
||||
## 玄幻修仙类(示例)
|
||||
|
||||
- 修仙
|
||||
- 系统流
|
||||
- 高武
|
||||
- 西幻
|
||||
- 无限流
|
||||
- 末世
|
||||
- 科幻
|
||||
|
||||
## 都市现代类(示例)
|
||||
|
||||
- 都市异能
|
||||
- 都市日常
|
||||
- 都市脑洞
|
||||
- 现实题材
|
||||
- 电竞
|
||||
- 直播文
|
||||
|
||||
## 言情类(示例)
|
||||
|
||||
- 古言
|
||||
- 宫斗宅斗
|
||||
- 青春甜宠
|
||||
- 豪门总裁
|
||||
- 职场婚恋
|
||||
- 民国言情
|
||||
- 幻想言情
|
||||
- 现言脑洞
|
||||
- 女频悬疑
|
||||
- 种田
|
||||
- 年代
|
||||
|
||||
## 复合题材规则
|
||||
|
||||
- 支持 `题材A+题材B`(最多 2 个)
|
||||
- 建议主辅比例 7:3
|
||||
- 主线遵循主题材逻辑,副题材提供钩子/规则/爽点
|
||||
|
||||
示例:
|
||||
|
||||
- `都市脑洞+规则怪谈`
|
||||
- `修仙+系统流`
|
||||
|
||||
## 题材与 Matrices 的关系
|
||||
|
||||
题材模板定义了故事的"骨架",而 `matrices/catharsis_models/` 定义了故事的"灵魂"——即具体的情绪张力模型。
|
||||
|
||||
在 `/noma-plan` 生成大纲时,系统会根据所选题材从 `matrices/` 目录动态加载适当的心理学张力模型(如禁忌僭越、降维打击、认知闭环等),指导 Writer Agent 在具体情节中调用相应的爽感路由。
|
||||
@@ -0,0 +1,46 @@
|
||||
# interfaces/ 目录说明
|
||||
|
||||
## 目录定位
|
||||
|
||||
`interfaces/` 是 OpenNovel Workspace 的**人机协同支柱**,承载"创作者上帝控制台"。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
interfaces/
|
||||
├── web_dashboard/
|
||||
│ ├── Handoff_Diff_View.jsx # 状态交接人工审查 Commit 界面
|
||||
│ ├── Retcon_Arbitrator.jsx # 设定修改冲突分析与补丁确认台
|
||||
│ └── Meta_Narrative_Panel.jsx # "元叙事开关"与"黑天鹅注入"控制台
|
||||
│
|
||||
└── server.py # Dashboard 服务端
|
||||
```
|
||||
|
||||
## web_dashboard/ 子面板
|
||||
|
||||
### Handoff_Diff_View.jsx — Commit 阻断台
|
||||
|
||||
- 强制展示 AI 生成的下一章 `handoff.json` Diff
|
||||
- 等待人类微调并 Commit 后,写作流程才会继续
|
||||
- 包含 LOD 视锥剔除后的在场人物列表和 Tick 时钟状态
|
||||
|
||||
### Retcon_Arbitrator.jsx — 设定修改冲突分析
|
||||
|
||||
- 当作者在写作中途修改设定时,输入需求
|
||||
- 系统扫描 `ledger.ts` 和 `hooks_pool.ts` 发现冲突点
|
||||
- 生成补丁方案供作者确认(如"浩然正气剑反噬"案例)
|
||||
|
||||
### Meta_Narrative_Panel.jsx — 元叙事开关与黑天鹅注入
|
||||
|
||||
- **第四面墙 Toggle**:物理开关,开启后发送信号给 `core_engine/validators`,自动关闭物理坐标和时间连贯性审查
|
||||
- **黑天鹅/平账注入器**:UI 监控 `hooks_pool` 压强,当警告灯亮起,人类可点击一键注入"等价对冲债务"或"合理平账大纲提案"
|
||||
|
||||
## server.py
|
||||
|
||||
Dashboard 服务端,提供 Web UI 的数据接口。
|
||||
|
||||
## 与其他模块的联动
|
||||
|
||||
- **core_engine/state_manager/**: `Handoff_Diff_View.jsx` 读取 `handoff.ts` 的输出;`Retcon_Arbitrator.jsx` 调用 `retcon_manager.ts`
|
||||
- **core_engine/memory_rag/**: `context_cache.py` 的脏标记触发 Dashboard 刷新
|
||||
- **agents/**: Dashboard 展示 Agent 执行状态和审查结果
|
||||
@@ -0,0 +1,61 @@
|
||||
# matrices/ 目录说明
|
||||
|
||||
## 目录定位
|
||||
|
||||
`matrices/` 是 OpenNovel Workspace 的**★特性支柱**,承载心理学与叙事学武器库,即"多维爽感路由"。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
matrices/
|
||||
├── catharsis_models/ # 高阶爽感路由模型
|
||||
│ ├── taboo-transgression.md # 禁忌僭越 (边缘拉扯/危险感)
|
||||
│ ├── overkill-reversal.md # 降维打击与权力倒转
|
||||
│ └── cognitive-closure.md # 认知闭环 (多线伏笔瞬间收束)
|
||||
│
|
||||
├── genres/ # 具体题材细分约束
|
||||
│ └── ... # 对应 novelmaster-writer 的 genres/ 内容
|
||||
│
|
||||
└── shared/ # 共享模板与工具
|
||||
```
|
||||
|
||||
## catharsis_models/ 子目录
|
||||
|
||||
改造自 `novelmaster-writer` 原有的 `genres/`(题材库),升维为情绪武器库。
|
||||
|
||||
### taboo-transgression.md — 禁忌僭越
|
||||
|
||||
边缘拉扯/危险感模型。模板包含:
|
||||
- 禁忌场景的构建原则
|
||||
- 道德风险的释放节奏
|
||||
- 读者心理的安全边际管理
|
||||
|
||||
### overkill-reversal.md — 降维打击与权力倒转
|
||||
|
||||
降维打击模型。模板包含:
|
||||
- 实力差距的戏剧化呈现
|
||||
- 权力倒转的触发条件
|
||||
- 反转后的余韵设计
|
||||
|
||||
### cognitive-closure.md — 认知闭环
|
||||
|
||||
多线伏笔瞬间收束模型。模板包含:
|
||||
- 伏笔的埋设密度与分布
|
||||
- 收束时的信息密度控制
|
||||
- 认知缺口的填补节奏
|
||||
|
||||
## 调度机制
|
||||
|
||||
大纲 **Planner Agent** 在规划情节时,必须从 `matrices/catharsis_models/` 动态 `import` 一个模型来指导 **Writer Agent**。
|
||||
|
||||
示例流程:
|
||||
1. `/noma-plan 1` 触发 Planner Agent
|
||||
2. Planner 分析当前章节的追读力状态(hooks 压强、cool-point 位置)
|
||||
3. Planner 选择合适的 catharsis model(如"降维打击"用于即将到来的冲突高潮)
|
||||
4. Writer Agent 接收模型指导,在正文中调用对应的爽感路由
|
||||
|
||||
## 与其他模块的联动
|
||||
|
||||
- **openspec/genesis_contract.json**: 三大变量(`core_desire`、`ethical_inversion`、`core_spectacle`)作为常量决定 catharsis model 的选用优先级
|
||||
- **core_engine/ledger.ts**: 负债(hooks_pool)压强触发 catharsis model 的动态切换
|
||||
- **agents/checkers/tension_checker.py**: 取代原爽点审查,校验情绪压强释放是否遵循选定的 catharsis model
|
||||
@@ -0,0 +1,46 @@
|
||||
# openspec/ 目录说明
|
||||
|
||||
## 目录定位
|
||||
|
||||
`openspec/` 是 OpenNovel Workspace 的**支柱1**,承载全局数据标准与契约,定义故事的"欲望沙盒与定制伦理"。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
openspec/
|
||||
├── genesis_contract.json # [★特性] 核心欲望、伦理沙盒、奇观日常化定义
|
||||
└── project.md # 项目规范文档
|
||||
```
|
||||
|
||||
## genesis_contract.json
|
||||
|
||||
在 `novel-writer-openspec` 极度严苛的数据契约能力基础上,强制注入**创世契约 (Genesis Contract)**。
|
||||
|
||||
### 核心字段
|
||||
|
||||
| 字段 | 说明 | 作用 |
|
||||
|------|------|------|
|
||||
| `core_desire` | 核心欲望 | 主角最深层的驱动力,作为 RAG 检索 Top 1 权重 |
|
||||
| `ethical_inversion` | 废除的现实道德 | 定义故事世界中哪些现实道德被打破或颠覆 |
|
||||
| `core_spectacle` | 奇观日常化 | 定义故事的核心奇观及其与日常的结合方式 |
|
||||
|
||||
### 示例结构
|
||||
|
||||
```json
|
||||
{
|
||||
"core_desire": "以凡人之躯比肩神明,打破一切阶级桎梏",
|
||||
"ethical_inversion": ["杀人无需偿命", "弱者无生存权", "情感是弱点"],
|
||||
"core_spectacle": "修士与凡人共处的蒸汽朋克修仙世界",
|
||||
"narrative_constraints": {
|
||||
"max_consecutive_quest_chapters": 5,
|
||||
"max_fire_gap_chapters": 10,
|
||||
"max_constellation_gap_chapters": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他模块的联动
|
||||
|
||||
- **core_engine/**: `genesis_contract.json` 中的约束会被 `handoff.ts` 和 `consistency_checker.py` 引用,在帧交接和审查时强制校验
|
||||
- **matrices/**: 三大变量驱动 `catharsis_models/` 中的爽感路由选择
|
||||
- **interfaces/**: Dashboard 展示创世契约状态,提醒作者核心方向
|
||||
@@ -0,0 +1,137 @@
|
||||
# 项目结构与运维
|
||||
|
||||
## 目录层级(真实运行)
|
||||
|
||||
在 Claude Code + Marketplace 安装下,至少有 4 层概念:
|
||||
|
||||
1. `WORKSPACE_ROOT`(Claude 工作区根,通常是 `${CLAUDE_PROJECT_DIR}`)
|
||||
2. `WORKSPACE_ROOT/.claude/`(工作区级指针与配置)
|
||||
3. `PROJECT_ROOT`(真实小说项目根,`/noma-init` 按书名创建)
|
||||
4. `CLAUDE_PLUGIN_ROOT`(插件缓存目录,不在项目内)
|
||||
|
||||
### A) Workspace 目录(含 `.claude`)
|
||||
|
||||
```text
|
||||
workspace-root/
|
||||
├── .claude/
|
||||
│ ├── .noma-current-project # 指向当前小说项目根
|
||||
│ └── settings.json
|
||||
├── 小说A/
|
||||
├── 小说B/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### B) 小说项目目录(`PROJECT_ROOT`)
|
||||
|
||||
```text
|
||||
project-root/
|
||||
├── .noma/ # 运行时数据(state/index/vectors/summaries)
|
||||
├── 正文/ # 正文章节
|
||||
├── 大纲/ # 总纲与卷纲
|
||||
└── 设定集/ # 世界观、角色、力量体系
|
||||
```
|
||||
|
||||
## 插件目录(Marketplace 安装)
|
||||
|
||||
插件不在小说项目目录内,而在 Claude 插件缓存目录。运行时统一用 `CLAUDE_PLUGIN_ROOT` 引用:
|
||||
|
||||
```text
|
||||
${CLAUDE_PLUGIN_ROOT}/
|
||||
├── skills/
|
||||
├── agents/
|
||||
├── scripts/
|
||||
└── references/
|
||||
```
|
||||
|
||||
### C) 用户级全局映射(兜底)
|
||||
|
||||
当工作区没有可用指针时,会使用用户级 registry 做 `workspace -> current_project_root` 映射:
|
||||
|
||||
```text
|
||||
${CLAUDE_HOME:-~/.claude}/.noma/novel_data/workspaces.json
|
||||
```
|
||||
|
||||
## OpenNovel Workspace 目录结构
|
||||
|
||||
```text
|
||||
noma/
|
||||
├── openspec/ # [支柱1] 全局数据标准与契约
|
||||
│ ├── genesis_contract.json # [★特性] 核心欲望、伦理沙盒、奇观日常化定义
|
||||
│ └── project.md
|
||||
│
|
||||
├── core_engine/ # [支柱2&3] 融合状态机与记忆引擎
|
||||
│ ├── state_manager/
|
||||
│ │ ├── handoff.ts # [★特性] 帧交接协议 (含 LOD视锥剔除, Tick时钟)
|
||||
│ │ ├── ledger.ts # 动态资产与负债账本
|
||||
│ │ └── retcon_manager.ts # [★特性] 意图漂移管理与热补丁生成器
|
||||
│ │
|
||||
│ ├── memory_rag/
|
||||
│ │ ├── vectorstore_utils.py
|
||||
│ │ └── context_cache.py # 上下文静态缓存与 Hash 脏标记更新
|
||||
│ │
|
||||
│ └── configurators/ # Cursor/Windsurf AI IDE 直接挂载适配器
|
||||
│
|
||||
├── agents/ # [支柱4] 审查与执行矩阵
|
||||
│ ├── planners/ # 负责调度 Hooks 平账与挂载张力模型
|
||||
│ ├── writers/
|
||||
│ │ └── immersive-writer.md # 负责感官颗粒度放大
|
||||
│ ├── catchup_agent.py # [★特性] 负责计算休眠角色跨越时间后的状态
|
||||
│ └── checkers/ # 大一统 Critic Agent
|
||||
│ ├── tension_checker.py # [★特性] 取代原爽点审查,校验情绪压强释放
|
||||
│ └── consistency_checker.py # [★特性] 受"元叙事开关"控制的动态物理连贯性校验
|
||||
│
|
||||
├── matrices/ # [★特性] 心理学与叙事学武器库
|
||||
│ ├── catharsis_models/ # 高阶爽感路由模型
|
||||
│ │ ├── taboo-transgression.md # 禁忌僭越 (边缘拉扯/危险感)
|
||||
│ │ ├── overkill-reversal.md # 降维打击与权力倒转
|
||||
│ │ └── cognitive-closure.md # 认知闭环 (多线伏笔瞬间收束)
|
||||
│ ├── genres/ # 具体题材细分约束
|
||||
│ └── shared/
|
||||
│
|
||||
├── interfaces/ # [人机协同] 创作者上帝控制台
|
||||
│ ├── web_dashboard/
|
||||
│ │ ├── Handoff_Diff_View.jsx # 状态交接人工审查 Commit 界面
|
||||
│ │ ├── Retcon_Arbitrator.jsx # 设定修改冲突分析与补丁确认台
|
||||
│ │ └── Meta_Narrative_Panel.jsx # "元叙事开关"与"黑天鹅注入"控制台
|
||||
│ └── server.py
|
||||
│
|
||||
└── .workspace/ # 用户的实际创作区
|
||||
```
|
||||
|
||||
## 常用运维命令
|
||||
|
||||
统一前置(手动 CLI 场景):
|
||||
|
||||
```bash
|
||||
export WORKSPACE_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
||||
export SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT}/scripts"
|
||||
export PROJECT_ROOT="$(python "${SCRIPTS_DIR}/noma.py" --project-root "${WORKSPACE_ROOT}" where)"
|
||||
```
|
||||
|
||||
### 索引重建
|
||||
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "${PROJECT_ROOT}" index process-chapter --chapter 1
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "${PROJECT_ROOT}" index stats
|
||||
```
|
||||
|
||||
### 健康报告
|
||||
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "${PROJECT_ROOT}" status -- --focus all
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "${PROJECT_ROOT}" status -- --focus urgency
|
||||
```
|
||||
|
||||
### 向量重建
|
||||
|
||||
```bash
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "${PROJECT_ROOT}" rag index-chapter --chapter 1
|
||||
python "${SCRIPTS_DIR}/noma.py" --project-root "${PROJECT_ROOT}" rag stats
|
||||
```
|
||||
|
||||
### 测试入口
|
||||
|
||||
```bash
|
||||
pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/run_tests.ps1" -Mode smoke
|
||||
pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/run_tests.ps1" -Mode full
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# RAG 与配置说明
|
||||
|
||||
## RAG 检索架构
|
||||
|
||||
```text
|
||||
查询 → QueryRouter(auto) → vector / bm25 / hybrid / graph_hybrid
|
||||
└→ RRF 融合 + Rerank → Top-K
|
||||
```
|
||||
|
||||
默认模型:
|
||||
|
||||
- Embedding:`Qwen/Qwen3-Embedding-8B`
|
||||
- Reranker:`jina-reranker-v3`
|
||||
|
||||
## 环境变量加载顺序
|
||||
|
||||
1. 进程环境变量(最高优先级)
|
||||
2. 书项目根目录下的 `.env`
|
||||
3. 用户级全局:`~/.claude/.noma/novel_data/.env`
|
||||
|
||||
## `.env` 最小配置
|
||||
|
||||
```bash
|
||||
EMBED_BASE_URL=https://api-inference.modelscope.cn/v1
|
||||
EMBED_MODEL=Qwen/Qwen3-Embedding-8B
|
||||
EMBED_API_KEY=your_embed_api_key
|
||||
|
||||
RERANK_BASE_URL=https://api.jina.ai/v1
|
||||
RERANK_MODEL=jina-reranker-v3
|
||||
RERANK_API_KEY=your_rerank_api_key
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 未配置 Embedding Key 时,语义检索会回退到 BM25。
|
||||
- 推荐每本书单独配置 `${PROJECT_ROOT}/.env`,避免多项目串配置。
|
||||
|
||||
## 热更新网关
|
||||
|
||||
在 `core_engine/memory_rag/` 中加入哈希监听器,一旦作者提交"设定补丁 (Retcon)",系统会:
|
||||
|
||||
1. 强制清空受影响的上下文缓存
|
||||
2. 将补丁作为最高权重压制历史设定幻觉
|
||||
3. 触发 `retcon_manager.ts` 生成兼容补丁
|
||||
|
||||
## RAG 与 Genesis Contract
|
||||
|
||||
`openspec/genesis_contract.json` 中定义的三大变量(`core_desire`、`ethical_inversion`、`core_spectacle`)作为常量,永远留存在 RAG 检索的 Top 1 权重中,确保故事核心方向不被稀释。
|
||||
@@ -0,0 +1 @@
|
||||
# Noma Dashboard - 可视化小说管理面板
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Allow running as `python -m web_dashboard`."""
|
||||
from .server import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,717 @@
|
||||
"""
|
||||
Noma Dashboard - FastAPI 主应用
|
||||
|
||||
支持多项目遍历:
|
||||
- 系统级索引 (.noma/projects.json)
|
||||
- 项目切换
|
||||
- 跨项目统计
|
||||
|
||||
仅提供 GET 接口(严格只读);所有文件读取经过 path_guard 防穿越校验。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
import os
|
||||
from contextlib import asynccontextmanager, closing
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, FileResponse, HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from .path_guard import safe_resolve
|
||||
from .watcher import FileWatcher
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 全局状态
|
||||
# ---------------------------------------------------------------------------
|
||||
_project_root: Path | None = None
|
||||
_system_root: Path | None = None
|
||||
_watcher = FileWatcher()
|
||||
|
||||
STATIC_DIR = Path(__file__).parent / "frontend" / "dist"
|
||||
|
||||
|
||||
def _get_project_root() -> Path:
|
||||
if _project_root is None:
|
||||
raise HTTPException(status_code=500, detail="项目根目录未配置")
|
||||
return _project_root
|
||||
|
||||
|
||||
def _get_system_root() -> Path:
|
||||
"""获取系统根目录(.noma 所在目录)"""
|
||||
global _system_root
|
||||
if _system_root is not None:
|
||||
return _system_root
|
||||
|
||||
project_root = _get_project_root()
|
||||
|
||||
# 方案1: NOMA_SYSTEM_ROOT 环境变量
|
||||
env_path = os.environ.get("NOMA_SYSTEM_ROOT")
|
||||
if env_path:
|
||||
_system_root = Path(env_path).resolve()
|
||||
return _system_root
|
||||
|
||||
# 方案2: 与 project_root 同级的 .noma
|
||||
sibling = project_root.parent / ".noma"
|
||||
if sibling.exists():
|
||||
_system_root = sibling.parent
|
||||
return _system_root
|
||||
|
||||
# 方案3: project_root/.noma 作为系统根(兼容)
|
||||
_system_root = project_root
|
||||
return _system_root
|
||||
|
||||
|
||||
def _noma_dir() -> Path:
|
||||
return _get_project_root() / ".noma"
|
||||
|
||||
|
||||
def _system_noma_dir() -> Path:
|
||||
"""系统级 .noma 目录"""
|
||||
return _get_system_root() / ".noma"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 应用工厂
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def create_app(project_root: str | Path | None = None) -> FastAPI:
|
||||
global _project_root
|
||||
|
||||
if project_root:
|
||||
_project_root = Path(project_root).resolve()
|
||||
|
||||
@asynccontextmanager
|
||||
async def _lifespan(_: FastAPI):
|
||||
noma = _noma_dir()
|
||||
if noma.is_dir():
|
||||
_watcher.start(noma, asyncio.get_running_loop())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_watcher.stop()
|
||||
|
||||
app = FastAPI(title="Noma Dashboard", version="0.1.0", lifespan=_lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ===========================================================
|
||||
# API:项目元信息
|
||||
# ===========================================================
|
||||
|
||||
@app.get("/api/project/info")
|
||||
def project_info():
|
||||
"""返回 state.json 完整内容(只读)。"""
|
||||
state_path = _noma_dir() / "state.json"
|
||||
if not state_path.is_file():
|
||||
raise HTTPException(404, "state.json 不存在")
|
||||
return json.loads(state_path.read_text(encoding="utf-8"))
|
||||
|
||||
# ===========================================================
|
||||
# API:多项目管理
|
||||
# ===========================================================
|
||||
|
||||
@app.get("/api/projects/list")
|
||||
def list_projects():
|
||||
"""列出系统中的所有小说项目"""
|
||||
system_noma = _system_noma_dir()
|
||||
projects_file = system_noma / "projects.json"
|
||||
|
||||
projects = []
|
||||
if projects_file.exists():
|
||||
try:
|
||||
data = json.loads(projects_file.read_text(encoding="utf-8"))
|
||||
projects = data.get("projects", [])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 如果没有索引,尝试自动发现
|
||||
if not projects:
|
||||
projects = _discover_projects()
|
||||
|
||||
# 为每个项目添加实时统计
|
||||
for p in projects:
|
||||
p["path"] = str(p.get("path", ""))
|
||||
project_path = Path(p["path"])
|
||||
if project_path.exists():
|
||||
noma_dir = project_path / ".noma"
|
||||
state_file = noma_dir / "state.json"
|
||||
if state_file.exists():
|
||||
try:
|
||||
state = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
p["title"] = state.get("project_info", {}).get("title", p.get("title", "未知"))
|
||||
p["genre"] = state.get("project_info", {}).get("genre", "unknown")
|
||||
p["progress"] = state.get("progress", {})
|
||||
except Exception:
|
||||
pass
|
||||
# 统计章节数
|
||||
chapters_dir = project_path / "正文"
|
||||
if chapters_dir.exists():
|
||||
p["chapter_count"] = len(list(chapters_dir.glob("*.md")))
|
||||
else:
|
||||
p["chapter_count"] = 0
|
||||
else:
|
||||
p["status"] = "not_found"
|
||||
|
||||
return {
|
||||
"projects": projects,
|
||||
"current_project": str(_get_project_root()),
|
||||
"system_root": str(_get_system_root())
|
||||
}
|
||||
|
||||
@app.get("/api/projects/discover")
|
||||
def discover_projects():
|
||||
"""自动发现 novels/ 目录下的所有项目"""
|
||||
discovered = _discover_projects()
|
||||
return {"projects": discovered, "count": len(discovered)}
|
||||
|
||||
@app.post("/api/projects/register")
|
||||
def register_project(path: str):
|
||||
"""注册一个新项目到系统索引"""
|
||||
project_path = Path(path).resolve()
|
||||
|
||||
if not project_path.exists():
|
||||
raise HTTPException(404, "项目路径不存在")
|
||||
|
||||
noma_dir = project_path / ".noma"
|
||||
if not noma_dir.exists():
|
||||
raise HTTPException(400, "不是有效的 Noma 项目(缺少 .noma 目录)")
|
||||
|
||||
state_file = noma_dir / "state.json"
|
||||
if not state_file.exists():
|
||||
raise HTTPException(400, "不是有效的 Noma 项目(缺少 state.json)")
|
||||
|
||||
try:
|
||||
state = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
title = state.get("project_info", {}).get("title", project_path.name)
|
||||
genre = state.get("project_info", {}).get("genre", "unknown")
|
||||
except Exception:
|
||||
title = project_path.name
|
||||
genre = "unknown"
|
||||
|
||||
# 读取现有索引
|
||||
system_noma = _system_noma_dir()
|
||||
system_noma.mkdir(parents=True, exist_ok=True)
|
||||
projects_file = system_noma / "projects.json"
|
||||
|
||||
projects = []
|
||||
if projects_file.exists():
|
||||
try:
|
||||
data = json.loads(projects_file.read_text(encoding="utf-8"))
|
||||
projects = data.get("projects", [])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 检查是否已存在
|
||||
path_str = str(project_path)
|
||||
for i, p in enumerate(projects):
|
||||
if p.get("path") == path_str:
|
||||
# 更新
|
||||
projects[i] = {
|
||||
"path": path_str,
|
||||
"title": title,
|
||||
"genre": genre,
|
||||
"registered_at": datetime.now().isoformat()
|
||||
}
|
||||
break
|
||||
else:
|
||||
# 添加
|
||||
projects.append({
|
||||
"path": path_str,
|
||||
"title": title,
|
||||
"genre": genre,
|
||||
"registered_at": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
# 写入索引
|
||||
index_data = {
|
||||
"projects": projects,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
projects_file.write_text(
|
||||
json.dumps(index_data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
return {"success": True, "title": title, "path": path_str}
|
||||
|
||||
@app.delete("/api/projects/unregister")
|
||||
def unregister_project(path: str):
|
||||
"""从系统索引移除项目"""
|
||||
project_path = Path(path).resolve()
|
||||
path_str = str(project_path)
|
||||
|
||||
system_noma = _system_noma_dir()
|
||||
projects_file = system_noma / "projects.json"
|
||||
|
||||
if not projects_file.exists():
|
||||
return {"success": False, "message": "索引文件不存在"}
|
||||
|
||||
try:
|
||||
data = json.loads(projects_file.read_text(encoding="utf-8"))
|
||||
projects = data.get("projects", [])
|
||||
|
||||
original_count = len(projects)
|
||||
projects = [p for p in projects if p.get("path") != path_str]
|
||||
|
||||
if len(projects) == original_count:
|
||||
return {"success": False, "message": "项目不在索引中"}
|
||||
|
||||
data["projects"] = projects
|
||||
data["last_updated"] = datetime.now().isoformat()
|
||||
|
||||
projects_file.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "项目已移除"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _discover_projects():
|
||||
"""从 novels/ 目录发现项目"""
|
||||
projects = []
|
||||
|
||||
# 检查可能的 novels 目录
|
||||
system_root = _get_system_root()
|
||||
candidates = [
|
||||
system_root / "novels",
|
||||
system_root.parent / "novels",
|
||||
system_root,
|
||||
]
|
||||
|
||||
for novels_dir in candidates:
|
||||
if not novels_dir.exists() or not novels_dir.is_dir():
|
||||
continue
|
||||
|
||||
for item in novels_dir.iterdir():
|
||||
if not item.is_dir():
|
||||
continue
|
||||
|
||||
noma_dir = item / ".noma"
|
||||
if not noma_dir.exists():
|
||||
continue
|
||||
|
||||
state_file = noma_dir / "state.json"
|
||||
if not state_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
state = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
projects.append({
|
||||
"path": str(item.resolve()),
|
||||
"title": state.get("project_info", {}).get("title", item.name),
|
||||
"genre": state.get("project_info", {}).get("genre", "unknown"),
|
||||
"registered_at": None
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return projects
|
||||
|
||||
@app.get("/api/projects/summary")
|
||||
def projects_summary():
|
||||
"""获取跨项目汇总统计"""
|
||||
projects_data = list_projects()
|
||||
projects = projects_data.get("projects", [])
|
||||
|
||||
total_words = 0
|
||||
total_chapters = 0
|
||||
genres = {}
|
||||
|
||||
for p in projects:
|
||||
progress = p.get("progress", {})
|
||||
words = progress.get("total_words", 0) or 0
|
||||
chapters = p.get("chapter_count", 0) or 0
|
||||
total_words += words
|
||||
total_chapters += chapters
|
||||
|
||||
genre = p.get("genre", "unknown")
|
||||
if genre not in genres:
|
||||
genres[genre] = {"count": 0, "chapters": 0, "words": 0}
|
||||
genres[genre]["count"] += 1
|
||||
genres[genre]["chapters"] += chapters
|
||||
genres[genre]["words"] += words
|
||||
|
||||
return {
|
||||
"total_projects": len(projects),
|
||||
"total_words": total_words,
|
||||
"total_chapters": total_chapters,
|
||||
"genres": genres,
|
||||
"current_project": projects_data.get("current_project")
|
||||
}
|
||||
|
||||
# ===========================================================
|
||||
# API:实体数据库(index.db 只读查询)
|
||||
# ===========================================================
|
||||
|
||||
def _get_db() -> sqlite3.Connection:
|
||||
db_path = _noma_dir() / "index.db"
|
||||
if not db_path.is_file():
|
||||
raise HTTPException(404, "index.db 不存在")
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def _fetchall_safe(conn: sqlite3.Connection, query: str, params: tuple = ()) -> list[dict]:
|
||||
"""执行只读查询;若目标表不存在(旧库),返回空列表。"""
|
||||
try:
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
except sqlite3.OperationalError as exc:
|
||||
if "no such table" in str(exc).lower():
|
||||
return []
|
||||
raise HTTPException(status_code=500, detail=f"数据库查询失败: {exc}") from exc
|
||||
|
||||
@app.get("/api/entities")
|
||||
def list_entities(
|
||||
entity_type: Optional[str] = Query(None, alias="type"),
|
||||
include_archived: bool = False,
|
||||
):
|
||||
"""列出所有实体(可按类型过滤)。"""
|
||||
with closing(_get_db()) as conn:
|
||||
q = "SELECT * FROM entities"
|
||||
params: list = []
|
||||
clauses: list[str] = []
|
||||
if entity_type:
|
||||
clauses.append("type = ?")
|
||||
params.append(entity_type)
|
||||
if not include_archived:
|
||||
clauses.append("is_archived = 0")
|
||||
if clauses:
|
||||
q += " WHERE " + " AND ".join(clauses)
|
||||
q += " ORDER BY last_appearance DESC"
|
||||
rows = conn.execute(q, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/entities/{entity_id}")
|
||||
def get_entity(entity_id: str):
|
||||
with closing(_get_db()) as conn:
|
||||
row = conn.execute("SELECT * FROM entities WHERE id = ?", (entity_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "实体不存在")
|
||||
return dict(row)
|
||||
|
||||
@app.get("/api/relationships")
|
||||
def list_relationships(entity: Optional[str] = None, limit: int = 200):
|
||||
with closing(_get_db()) as conn:
|
||||
if entity:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM relationships WHERE from_entity = ? OR to_entity = ? ORDER BY chapter DESC LIMIT ?",
|
||||
(entity, entity, limit),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM relationships ORDER BY chapter DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/relationship-events")
|
||||
def list_relationship_events(
|
||||
entity: Optional[str] = None,
|
||||
from_chapter: Optional[int] = None,
|
||||
to_chapter: Optional[int] = None,
|
||||
limit: int = 200,
|
||||
):
|
||||
with closing(_get_db()) as conn:
|
||||
q = "SELECT * FROM relationship_events"
|
||||
params: list = []
|
||||
clauses: list[str] = []
|
||||
if entity:
|
||||
clauses.append("(from_entity = ? OR to_entity = ?)")
|
||||
params.extend([entity, entity])
|
||||
if from_chapter is not None:
|
||||
clauses.append("chapter >= ?")
|
||||
params.append(from_chapter)
|
||||
if to_chapter is not None:
|
||||
clauses.append("chapter <= ?")
|
||||
params.append(to_chapter)
|
||||
if clauses:
|
||||
q += " WHERE " + " AND ".join(clauses)
|
||||
q += " ORDER BY chapter DESC, id DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
rows = conn.execute(q, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/chapters")
|
||||
def list_chapters():
|
||||
with closing(_get_db()) as conn:
|
||||
rows = conn.execute("SELECT * FROM chapters ORDER BY chapter ASC").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/scenes")
|
||||
def list_scenes(chapter: Optional[int] = None, limit: int = 500):
|
||||
with closing(_get_db()) as conn:
|
||||
if chapter is not None:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM scenes WHERE chapter = ? ORDER BY scene_index ASC", (chapter,)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM scenes ORDER BY chapter ASC, scene_index ASC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/reading-power")
|
||||
def list_reading_power(limit: int = 50):
|
||||
with closing(_get_db()) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM chapter_reading_power ORDER BY chapter DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/review-metrics")
|
||||
def list_review_metrics(limit: int = 20):
|
||||
with closing(_get_db()) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM review_metrics ORDER BY end_chapter DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/state-changes")
|
||||
def list_state_changes(entity: Optional[str] = None, limit: int = 100):
|
||||
with closing(_get_db()) as conn:
|
||||
if entity:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM state_changes WHERE entity_id = ? ORDER BY chapter DESC LIMIT ?",
|
||||
(entity, limit),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM state_changes ORDER BY chapter DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.get("/api/aliases")
|
||||
def list_aliases(entity: Optional[str] = None):
|
||||
with closing(_get_db()) as conn:
|
||||
if entity:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM aliases WHERE entity_id = ?", (entity,)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute("SELECT * FROM aliases").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
# ===========================================================
|
||||
# API:扩展表(v5.3+ / v5.4+)
|
||||
# ===========================================================
|
||||
|
||||
@app.get("/api/overrides")
|
||||
def list_overrides(status: Optional[str] = None, limit: int = 100):
|
||||
with closing(_get_db()) as conn:
|
||||
if status:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM override_contracts WHERE status = ? ORDER BY chapter DESC LIMIT ?",
|
||||
(status, limit),
|
||||
)
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM override_contracts ORDER BY chapter DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get("/api/debts")
|
||||
def list_debts(status: Optional[str] = None, limit: int = 100):
|
||||
with closing(_get_db()) as conn:
|
||||
if status:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM chase_debt WHERE status = ? ORDER BY updated_at DESC LIMIT ?",
|
||||
(status, limit),
|
||||
)
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM chase_debt ORDER BY updated_at DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get("/api/debt-events")
|
||||
def list_debt_events(debt_id: Optional[int] = None, limit: int = 200):
|
||||
with closing(_get_db()) as conn:
|
||||
if debt_id is not None:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM debt_events WHERE debt_id = ? ORDER BY chapter DESC, id DESC LIMIT ?",
|
||||
(debt_id, limit),
|
||||
)
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM debt_events ORDER BY chapter DESC, id DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get("/api/invalid-facts")
|
||||
def list_invalid_facts(status: Optional[str] = None, limit: int = 100):
|
||||
with closing(_get_db()) as conn:
|
||||
if status:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM invalid_facts WHERE status = ? ORDER BY marked_at DESC LIMIT ?",
|
||||
(status, limit),
|
||||
)
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM invalid_facts ORDER BY marked_at DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get("/api/rag-queries")
|
||||
def list_rag_queries(query_type: Optional[str] = None, limit: int = 100):
|
||||
with closing(_get_db()) as conn:
|
||||
if query_type:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM rag_query_log WHERE query_type = ? ORDER BY created_at DESC LIMIT ?",
|
||||
(query_type, limit),
|
||||
)
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM rag_query_log ORDER BY created_at DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get("/api/tool-stats")
|
||||
def list_tool_stats(tool_name: Optional[str] = None, limit: int = 200):
|
||||
with closing(_get_db()) as conn:
|
||||
if tool_name:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM tool_call_stats WHERE tool_name = ? ORDER BY created_at DESC LIMIT ?",
|
||||
(tool_name, limit),
|
||||
)
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM tool_call_stats ORDER BY created_at DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get("/api/checklist-scores")
|
||||
def list_checklist_scores(limit: int = 100):
|
||||
with closing(_get_db()) as conn:
|
||||
return _fetchall_safe(
|
||||
conn,
|
||||
"SELECT * FROM writing_checklist_scores ORDER BY chapter DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
# ===========================================================
|
||||
# API:文档浏览(正文/大纲/设定集 —— 只读)
|
||||
# ===========================================================
|
||||
|
||||
@app.get("/api/files/tree")
|
||||
def file_tree():
|
||||
"""列出 正文/、大纲/、设定集/ 三个目录的树结构。"""
|
||||
root = _get_project_root()
|
||||
result = {}
|
||||
for folder_name in ("正文", "大纲", "设定集"):
|
||||
folder = root / folder_name
|
||||
if not folder.is_dir():
|
||||
result[folder_name] = []
|
||||
continue
|
||||
result[folder_name] = _walk_tree(folder, root)
|
||||
return result
|
||||
|
||||
@app.get("/api/files/read")
|
||||
def file_read(path: str):
|
||||
"""只读读取一个文件内容(限 正文/大纲/设定集 目录)。"""
|
||||
root = _get_project_root()
|
||||
resolved = safe_resolve(root, path)
|
||||
|
||||
# 二次限制:只允许三大目录
|
||||
allowed_parents = [root / n for n in ("正文", "大纲", "设定集")]
|
||||
if not any(_is_child(resolved, p) for p in allowed_parents):
|
||||
raise HTTPException(403, "仅允许读取 正文/大纲/设定集 目录下的文件")
|
||||
|
||||
if not resolved.is_file():
|
||||
raise HTTPException(404, "文件不存在")
|
||||
|
||||
# 文本文件直接读;其他情况返回占位信息
|
||||
try:
|
||||
content = resolved.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
content = "[二进制文件,无法预览]"
|
||||
|
||||
return {"path": path, "content": content}
|
||||
|
||||
# ===========================================================
|
||||
# SSE:实时变更推送
|
||||
# ===========================================================
|
||||
|
||||
@app.get("/api/events")
|
||||
async def sse():
|
||||
"""Server-Sent Events 端点,推送 .noma/ 下的文件变更。"""
|
||||
q = _watcher.subscribe()
|
||||
|
||||
async def _gen():
|
||||
try:
|
||||
while True:
|
||||
msg = await q.get()
|
||||
yield f"data: {msg}\n\n"
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
_watcher.unsubscribe(q)
|
||||
|
||||
return StreamingResponse(_gen(), media_type="text/event-stream")
|
||||
|
||||
# ===========================================================
|
||||
# 前端静态文件托管
|
||||
# ===========================================================
|
||||
|
||||
if STATIC_DIR.is_dir():
|
||||
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets")
|
||||
|
||||
@app.get("/{full_path:path}")
|
||||
def serve_spa(full_path: str):
|
||||
"""SPA fallback:任何非 /api 路径都返回 index.html。"""
|
||||
index = STATIC_DIR / "index.html"
|
||||
if index.is_file():
|
||||
return FileResponse(str(index))
|
||||
raise HTTPException(404, "前端尚未构建")
|
||||
else:
|
||||
@app.get("/")
|
||||
def no_frontend():
|
||||
return HTMLResponse(
|
||||
"<h2>Noma Dashboard API is running</h2>"
|
||||
"<p>前端尚未构建。请先在 <code>web_dashboard/frontend</code> 目录执行 <code>npm run build</code>。</p>"
|
||||
'<p>API 文档:<a href="/docs">/docs</a></p>'
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 辅助函数
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _walk_tree(folder: Path, root: Path) -> list[dict]:
|
||||
items = []
|
||||
for child in sorted(folder.iterdir()):
|
||||
rel = str(child.relative_to(root)).replace("\\", "/")
|
||||
if child.is_dir():
|
||||
items.append({"name": child.name, "type": "dir", "path": rel, "children": _walk_tree(child, root)})
|
||||
else:
|
||||
items.append({"name": child.name, "type": "file", "path": rel, "size": child.stat().st_size})
|
||||
return items
|
||||
|
||||
|
||||
def _is_child(path: Path, parent: Path) -> bool:
|
||||
try:
|
||||
path.resolve().relative_to(parent.resolve())
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "noma-dashboard",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-force-graph-3d": "^1.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,889 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { fetchJSON, subscribeSSE } from './api.js'
|
||||
import ForceGraph3D from 'react-force-graph-3d'
|
||||
|
||||
// ====================================================================
|
||||
// 主应用
|
||||
// ====================================================================
|
||||
|
||||
export default function App() {
|
||||
const [page, setPage] = useState('dashboard')
|
||||
const [projectInfo, setProjectInfo] = useState(null)
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
const [connected, setConnected] = useState(false)
|
||||
|
||||
const loadProjectInfo = useCallback(() => {
|
||||
fetchJSON('/api/project/info')
|
||||
.then(setProjectInfo)
|
||||
.catch(() => setProjectInfo(null))
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadProjectInfo() }, [loadProjectInfo, refreshKey])
|
||||
|
||||
// SSE 订阅
|
||||
useEffect(() => {
|
||||
const unsub = subscribeSSE(
|
||||
() => {
|
||||
setRefreshKey(k => k + 1)
|
||||
},
|
||||
{
|
||||
onOpen: () => setConnected(true),
|
||||
onError: () => setConnected(false),
|
||||
},
|
||||
)
|
||||
return () => { unsub(); setConnected(false) }
|
||||
}, [])
|
||||
|
||||
const title = projectInfo?.project_info?.title || '未加载'
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h1>PIXEL WRITER HUB</h1>
|
||||
<div className="subtitle">{title}</div>
|
||||
</div>
|
||||
<nav className="sidebar-nav">
|
||||
{NAV_ITEMS.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`nav-item ${page === item.id ? 'active' : ''}`}
|
||||
onClick={() => setPage(item.id)}
|
||||
>
|
||||
<span className="icon">{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="live-indicator">
|
||||
<span className={`live-dot ${connected ? '' : 'disconnected'}`} />
|
||||
{connected ? '实时同步中' : '未连接'}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="main-content">
|
||||
{page === 'dashboard' && <DashboardPage data={projectInfo} key={refreshKey} />}
|
||||
{page === 'entities' && <EntitiesPage key={refreshKey} />}
|
||||
{page === 'graph' && <GraphPage key={refreshKey} />}
|
||||
{page === 'chapters' && <ChaptersPage key={refreshKey} />}
|
||||
{page === 'files' && <FilesPage />}
|
||||
{page === 'reading' && <ReadingPowerPage key={refreshKey} />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ id: 'dashboard', icon: '📊', label: '数据总览' },
|
||||
{ id: 'entities', icon: '👤', label: '设定词典' },
|
||||
{ id: 'graph', icon: '🕸️', label: '关系图谱' },
|
||||
{ id: 'chapters', icon: '📝', label: '章节一览' },
|
||||
{ id: 'files', icon: '📁', label: '文档浏览' },
|
||||
{ id: 'reading', icon: '🔥', label: '追读力' },
|
||||
]
|
||||
|
||||
const FULL_DATA_GROUPS = [
|
||||
{ key: 'entities', title: '实体', columns: ['id', 'canonical_name', 'type', 'tier', 'first_appearance', 'last_appearance'], domain: 'core' },
|
||||
{ key: 'chapters', title: '章节', columns: ['chapter', 'title', 'word_count', 'location', 'characters'], domain: 'core' },
|
||||
{ key: 'scenes', title: '场景', columns: ['chapter', 'scene_index', 'location', 'time', 'summary'], domain: 'core' },
|
||||
{ key: 'aliases', title: '别名', columns: ['alias', 'entity_id', 'entity_type'], domain: 'core' },
|
||||
{ key: 'stateChanges', title: '状态变化', columns: ['entity_id', 'field', 'old_value', 'new_value', 'chapter'], domain: 'core' },
|
||||
{ key: 'relationships', title: '关系', columns: ['from_entity', 'to_entity', 'type', 'chapter', 'description'], domain: 'network' },
|
||||
{ key: 'relationshipEvents', title: '关系事件', columns: ['from_entity', 'to_entity', 'type', 'chapter', 'event_type', 'description'], domain: 'network' },
|
||||
{ key: 'readingPower', title: '追读力', columns: ['chapter', 'hook_type', 'hook_strength', 'is_transition', 'override_count', 'debt_balance'], domain: 'network' },
|
||||
{ key: 'overrides', title: 'Override 合约', columns: ['chapter', 'constraint_type', 'constraint_id', 'due_chapter', 'status'], domain: 'network' },
|
||||
{ key: 'debts', title: '追读债务', columns: ['id', 'debt_type', 'current_amount', 'interest_rate', 'due_chapter', 'status'], domain: 'network' },
|
||||
{ key: 'debtEvents', title: '债务事件', columns: ['debt_id', 'event_type', 'amount', 'chapter', 'note'], domain: 'network' },
|
||||
{ key: 'reviewMetrics', title: '审查指标', columns: ['start_chapter', 'end_chapter', 'overall_score', 'severity_counts', 'created_at'], domain: 'quality' },
|
||||
{ key: 'invalidFacts', title: '无效事实', columns: ['source_type', 'source_id', 'reason', 'status', 'chapter_discovered'], domain: 'quality' },
|
||||
{ key: 'checklistScores', title: '写作清单评分', columns: ['chapter', 'template', 'score', 'completion_rate', 'completed_items', 'total_items'], domain: 'quality' },
|
||||
{ key: 'ragQueries', title: 'RAG 查询日志', columns: ['query_type', 'query', 'results_count', 'latency_ms', 'chapter', 'created_at'], domain: 'ops' },
|
||||
{ key: 'toolStats', title: '工具调用统计', columns: ['tool_name', 'success', 'retry_count', 'error_code', 'chapter', 'created_at'], domain: 'ops' },
|
||||
]
|
||||
|
||||
const FULL_DATA_DOMAINS = [
|
||||
{ id: 'overview', label: '总览' },
|
||||
{ id: 'core', label: '基础档案' },
|
||||
{ id: 'network', label: '关系与剧情' },
|
||||
{ id: 'quality', label: '质量审查' },
|
||||
{ id: 'ops', label: 'RAG 与工具' },
|
||||
]
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 页面 1:数据总览
|
||||
// ====================================================================
|
||||
|
||||
function DashboardPage({ data }) {
|
||||
if (!data) return <div className="loading">加载中…</div>
|
||||
|
||||
const info = data.project_info || {}
|
||||
const progress = data.progress || {}
|
||||
const protagonist = data.protagonist_state || {}
|
||||
const strand = data.strand_tracker || {}
|
||||
const foreshadowing = data.plot_threads?.foreshadowing || []
|
||||
|
||||
const totalWords = progress.total_words || 0
|
||||
const targetWords = info.target_words || 2000000
|
||||
const pct = targetWords > 0 ? Math.min(100, (totalWords / targetWords * 100)).toFixed(1) : 0
|
||||
|
||||
const unresolvedForeshadow = foreshadowing.filter(f => {
|
||||
const s = (f.status || '').toLowerCase()
|
||||
return s !== '已回收' && s !== '已兑现' && s !== 'resolved'
|
||||
})
|
||||
|
||||
// Strand 历史统计
|
||||
const history = strand.history || []
|
||||
const strandCounts = { quest: 0, fire: 0, constellation: 0 }
|
||||
history.forEach(h => { if (strandCounts[h.strand] !== undefined) strandCounts[h.strand]++ })
|
||||
const total = history.length || 1
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>📊 数据总览</h2>
|
||||
<span className="card-badge badge-blue">{info.genre || '未知题材'}</span>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">总字数</span>
|
||||
<span className="stat-value">{formatNumber(totalWords)}</span>
|
||||
<span className="stat-sub">目标 {formatNumber(targetWords)} 字 · {pct}%</span>
|
||||
<div className="progress-track">
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">当前章节</span>
|
||||
<span className="stat-value">第 {progress.current_chapter || 0} 章</span>
|
||||
<span className="stat-sub">目标 {info.target_chapters || '?'} 章 · 卷 {progress.current_volume || 1}</span>
|
||||
</div>
|
||||
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">主角状态</span>
|
||||
<span className="stat-value plain">{protagonist.name || '未设定'}</span>
|
||||
<span className="stat-sub">
|
||||
{protagonist.power?.realm || '未知境界'}
|
||||
{protagonist.location?.current ? ` · ${protagonist.location.current}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">未回收伏笔</span>
|
||||
<span className="stat-value" style={{ color: unresolvedForeshadow.length > 10 ? 'var(--accent-red)' : 'var(--accent-amber)' }}>
|
||||
{unresolvedForeshadow.length}
|
||||
</span>
|
||||
<span className="stat-sub">总计 {foreshadowing.length} 条伏笔</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Strand Weave 比例 */}
|
||||
<div className="card dashboard-section-card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">Strand Weave 节奏分布</span>
|
||||
<span className="card-badge badge-purple">{strand.current_dominant || '?'}</span>
|
||||
</div>
|
||||
<div className="strand-bar">
|
||||
<div className="segment strand-quest" style={{ width: `${(strandCounts.quest / total * 100).toFixed(1)}%` }} />
|
||||
<div className="segment strand-fire" style={{ width: `${(strandCounts.fire / total * 100).toFixed(1)}%` }} />
|
||||
<div className="segment strand-constellation" style={{ width: `${(strandCounts.constellation / total * 100).toFixed(1)}%` }} />
|
||||
</div>
|
||||
<div className="strand-legend">
|
||||
<span>🔵 Quest {(strandCounts.quest / total * 100).toFixed(0)}%</span>
|
||||
<span>🔴 Fire {(strandCounts.fire / total * 100).toFixed(0)}%</span>
|
||||
<span>🟣 Constellation {(strandCounts.constellation / total * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 伏笔列表 */}
|
||||
{unresolvedForeshadow.length > 0 ? (
|
||||
<div className="card dashboard-section-card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">⚠️ 待回收伏笔 (Top 20)</span>
|
||||
</div>
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead><tr><th>内容</th><th>状态</th><th>埋设章</th></tr></thead>
|
||||
<tbody>
|
||||
{unresolvedForeshadow.slice(0, 20).map((f, i) => (
|
||||
<tr key={i}>
|
||||
<td className="truncate" style={{ maxWidth: 400 }}>{f.content || f.description || '—'}</td>
|
||||
<td><span className="card-badge badge-amber">{f.status || '未知'}</span></td>
|
||||
<td>{f.chapter || f.planted_chapter || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MergedDataView />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 页面 2:设定词典
|
||||
// ====================================================================
|
||||
|
||||
function EntitiesPage() {
|
||||
const [entities, setEntities] = useState([])
|
||||
const [typeFilter, setTypeFilter] = useState('')
|
||||
const [selected, setSelected] = useState(null)
|
||||
const [changes, setChanges] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchJSON('/api/entities').then(setEntities).catch(() => { })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
fetchJSON('/api/state-changes', { entity: selected.id, limit: 30 }).then(setChanges).catch(() => setChanges([]))
|
||||
}
|
||||
}, [selected])
|
||||
|
||||
const types = [...new Set(entities.map(e => e.type))].sort()
|
||||
const filteredEntities = typeFilter ? entities.filter(e => e.type === typeFilter) : entities
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>👤 设定词典</h2>
|
||||
<span className="card-badge badge-green">{filteredEntities.length} / {entities.length} 个实体</span>
|
||||
</div>
|
||||
|
||||
<div className="filter-group">
|
||||
<button className={`filter-btn ${typeFilter === '' ? 'active' : ''}`} onClick={() => setTypeFilter('')}>全部</button>
|
||||
{types.map(t => (
|
||||
<button key={t} className={`filter-btn ${typeFilter === t ? 'active' : ''}`} onClick={() => setTypeFilter(t)}>{t}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="split-layout">
|
||||
<div className="split-main">
|
||||
<div className="card">
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead><tr><th>名称</th><th>类型</th><th>层级</th><th>首现</th><th>末现</th></tr></thead>
|
||||
<tbody>
|
||||
{filteredEntities.map(e => (
|
||||
<tr
|
||||
key={e.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`entity-row ${selected?.id === e.id ? 'selected' : ''}`}
|
||||
onKeyDown={evt => (evt.key === 'Enter' || evt.key === ' ') && (evt.preventDefault(), setSelected(e))}
|
||||
onClick={() => setSelected(e)}
|
||||
>
|
||||
<td className={e.is_protagonist ? 'entity-name protagonist' : 'entity-name'}>
|
||||
{e.canonical_name} {e.is_protagonist ? '⭐' : ''}
|
||||
</td>
|
||||
<td><span className="card-badge badge-blue">{e.type}</span></td>
|
||||
<td>{e.tier}</td>
|
||||
<td>{e.first_appearance || '—'}</td>
|
||||
<td>{e.last_appearance || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="split-side">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">{selected.canonical_name}</span>
|
||||
<span className="card-badge badge-purple">{selected.tier}</span>
|
||||
</div>
|
||||
<div className="entity-detail">
|
||||
<p><strong>类型:</strong>{selected.type}</p>
|
||||
<p><strong>ID:</strong><code>{selected.id}</code></p>
|
||||
{selected.desc && <p className="entity-desc">{selected.desc}</p>}
|
||||
{selected.current_json && (
|
||||
<div className="entity-current-block">
|
||||
<strong>当前状态:</strong>
|
||||
<pre className="entity-json">
|
||||
{formatJSON(selected.current_json)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{changes.length > 0 ? (
|
||||
<div className="entity-history">
|
||||
<div className="card-title">状态变化历史</div>
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead><tr><th>章</th><th>字段</th><th>变化</th></tr></thead>
|
||||
<tbody>
|
||||
{changes.map((c, i) => (
|
||||
<tr key={i}>
|
||||
<td>{c.chapter}</td>
|
||||
<td>{c.field}</td>
|
||||
<td>{c.old_value} → {c.new_value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 页面 3:3D 宇宙关系图谱
|
||||
// ====================================================================
|
||||
|
||||
function GraphPage() {
|
||||
const [relationships, setRelationships] = useState([])
|
||||
const [graphData, setGraphData] = useState({ nodes: [], links: [] })
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetchJSON('/api/relationships', { limit: 1000 }),
|
||||
fetchJSON('/api/entities'),
|
||||
]).then(([rels, ents]) => {
|
||||
setRelationships(rels)
|
||||
const typeColors = {
|
||||
'角色': '#4f8ff7', '地点': '#34d399', '星球': '#22d3ee', '神仙': '#f59e0b',
|
||||
'势力': '#8b5cf6', '招式': '#ef4444', '法宝': '#ec4899'
|
||||
}
|
||||
const relatedIds = new Set()
|
||||
rels.forEach(r => { relatedIds.add(r.from_entity); relatedIds.add(r.to_entity) })
|
||||
const entityMap = {}
|
||||
ents.forEach(e => { entityMap[e.id] = e })
|
||||
|
||||
const nodes = [...relatedIds].map(id => ({
|
||||
id,
|
||||
name: entityMap[id]?.canonical_name || id,
|
||||
val: (entityMap[id]?.tier === 'S' ? 8 : entityMap[id]?.tier === 'A' ? 5 : 2),
|
||||
color: typeColors[entityMap[id]?.type] || '#5c6078'
|
||||
}))
|
||||
const links = rels.map(r => ({
|
||||
source: r.from_entity,
|
||||
target: r.to_entity,
|
||||
name: r.type
|
||||
}))
|
||||
setGraphData({ nodes, links })
|
||||
}).catch(() => { })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>🕸️ 关系图谱</h2>
|
||||
<span className="card-badge badge-blue">{relationships.length} 条引力链接</span>
|
||||
</div>
|
||||
<div className="card graph-shell">
|
||||
<ForceGraph3D
|
||||
graphData={graphData}
|
||||
nodeLabel="name"
|
||||
nodeColor="color"
|
||||
nodeRelSize={6}
|
||||
linkColor={() => 'rgba(127, 90, 240, 0.35)'}
|
||||
linkWidth={1}
|
||||
linkDirectionalParticles={2}
|
||||
linkDirectionalParticleWidth={1.5}
|
||||
linkDirectionalParticleSpeed={d => 0.005 + Math.random() * 0.005}
|
||||
backgroundColor="#fffaf0"
|
||||
showNavInfo={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 页面 4:章节一览
|
||||
// ====================================================================
|
||||
|
||||
function ChaptersPage() {
|
||||
const [chapters, setChapters] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchJSON('/api/chapters').then(setChapters).catch(() => { })
|
||||
}, [])
|
||||
|
||||
const totalWords = chapters.reduce((s, c) => s + (c.word_count || 0), 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>📝 章节一览</h2>
|
||||
<span className="card-badge badge-green">{chapters.length} 章 · {formatNumber(totalWords)} 字</span>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead><tr><th>章节</th><th>标题</th><th>字数</th><th>地点</th><th>角色</th></tr></thead>
|
||||
<tbody>
|
||||
{chapters.map(c => (
|
||||
<tr key={c.chapter}>
|
||||
<td className="chapter-no">第 {c.chapter} 章</td>
|
||||
<td>{c.title || '—'}</td>
|
||||
<td>{formatNumber(c.word_count || 0)}</td>
|
||||
<td>{c.location || '—'}</td>
|
||||
<td className="truncate chapter-characters">{c.characters || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{chapters.length === 0 ? <div className="empty-state"><div className="empty-icon">📭</div><p>暂无章节数据</p></div> : null}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 页面 5:文档浏览
|
||||
// ====================================================================
|
||||
|
||||
function FilesPage() {
|
||||
const [tree, setTree] = useState({})
|
||||
const [selectedPath, setSelectedPath] = useState(null)
|
||||
const [content, setContent] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchJSON('/api/files/tree').then(setTree).catch(() => { })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPath) {
|
||||
fetchJSON('/api/files/read', { path: selectedPath })
|
||||
.then(d => setContent(d.content))
|
||||
.catch(() => setContent('[读取失败]'))
|
||||
}
|
||||
}, [selectedPath])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPath) return
|
||||
const first = findFirstFilePath(tree)
|
||||
if (first) setSelectedPath(first)
|
||||
}, [tree, selectedPath])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>📁 文档浏览</h2>
|
||||
</div>
|
||||
<div className="file-layout">
|
||||
<div className="file-tree-pane">
|
||||
{Object.entries(tree).map(([folder, items]) => (
|
||||
<div key={folder} className="folder-block">
|
||||
<div className="folder-title">📂 {folder}</div>
|
||||
<ul className="file-tree">
|
||||
<TreeNodes items={items} selected={selectedPath} onSelect={setSelectedPath} />
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="file-content-pane">
|
||||
{selectedPath ? (
|
||||
<div>
|
||||
<div className="selected-path">{selectedPath}</div>
|
||||
<div className="file-preview">{content}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state"><div className="empty-icon">📄</div><p>选择左侧文件以预览内容</p></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 页面 6:追读力
|
||||
// ====================================================================
|
||||
|
||||
function ReadingPowerPage() {
|
||||
const [data, setData] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchJSON('/api/reading-power', { limit: 50 }).then(setData).catch(() => { })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>🔥 追读力分析</h2>
|
||||
<span className="card-badge badge-amber">{data.length} 章数据</span>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead><tr><th>章节</th><th>钩子类型</th><th>钩子强度</th><th>过渡章</th><th>Override</th><th>债务余额</th></tr></thead>
|
||||
<tbody>
|
||||
{data.map(r => (
|
||||
<tr key={r.chapter}>
|
||||
<td className="chapter-no">第 {r.chapter} 章</td>
|
||||
<td>{r.hook_type || '—'}</td>
|
||||
<td>
|
||||
<span className={`card-badge ${r.hook_strength === 'strong' ? 'badge-green' : r.hook_strength === 'weak' ? 'badge-red' : 'badge-amber'}`}>
|
||||
{r.hook_strength || '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{r.is_transition ? '✅' : '—'}</td>
|
||||
<td>{r.override_count || 0}</td>
|
||||
<td className={r.debt_balance > 0 ? 'debt-positive' : 'debt-normal'}>{(r.debt_balance || 0).toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{data.length === 0 ? <div className="empty-state"><div className="empty-icon">🔥</div><p>暂无追读力数据</p></div> : null}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function findFirstFilePath(tree) {
|
||||
const roots = Object.values(tree || {})
|
||||
for (const items of roots) {
|
||||
const p = walkFirstFile(items)
|
||||
if (p) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function walkFirstFile(items) {
|
||||
if (!Array.isArray(items)) return null
|
||||
for (const item of items) {
|
||||
if (item?.type === 'file' && item?.path) return item.path
|
||||
if (item?.type === 'dir' && Array.isArray(item.children)) {
|
||||
const p = walkFirstFile(item.children)
|
||||
if (p) return p
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 数据总览内嵌:全量数据视图
|
||||
// ====================================================================
|
||||
|
||||
function MergedDataView() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [payload, setPayload] = useState({})
|
||||
const [domain, setDomain] = useState('overview')
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false
|
||||
|
||||
async function loadAll() {
|
||||
setLoading(true)
|
||||
const requests = [
|
||||
['entities', fetchJSON('/api/entities')],
|
||||
['chapters', fetchJSON('/api/chapters')],
|
||||
['scenes', fetchJSON('/api/scenes', { limit: 200 })],
|
||||
['relationships', fetchJSON('/api/relationships', { limit: 300 })],
|
||||
['relationshipEvents', fetchJSON('/api/relationship-events', { limit: 200 })],
|
||||
['readingPower', fetchJSON('/api/reading-power', { limit: 100 })],
|
||||
['reviewMetrics', fetchJSON('/api/review-metrics', { limit: 50 })],
|
||||
['stateChanges', fetchJSON('/api/state-changes', { limit: 120 })],
|
||||
['aliases', fetchJSON('/api/aliases')],
|
||||
['overrides', fetchJSON('/api/overrides', { limit: 120 })],
|
||||
['debts', fetchJSON('/api/debts', { limit: 120 })],
|
||||
['debtEvents', fetchJSON('/api/debt-events', { limit: 150 })],
|
||||
['invalidFacts', fetchJSON('/api/invalid-facts', { limit: 120 })],
|
||||
['ragQueries', fetchJSON('/api/rag-queries', { limit: 150 })],
|
||||
['toolStats', fetchJSON('/api/tool-stats', { limit: 200 })],
|
||||
['checklistScores', fetchJSON('/api/checklist-scores', { limit: 120 })],
|
||||
]
|
||||
|
||||
const entries = await Promise.all(
|
||||
requests.map(async ([key, p]) => {
|
||||
try {
|
||||
const val = await p
|
||||
return [key, val]
|
||||
} catch {
|
||||
return [key, []]
|
||||
}
|
||||
}),
|
||||
)
|
||||
if (!disposed) {
|
||||
setPayload(Object.fromEntries(entries))
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadAll()
|
||||
return () => { disposed = true }
|
||||
}, [])
|
||||
|
||||
if (loading) return <div className="loading">加载全量数据中…</div>
|
||||
|
||||
const groups = domain === 'overview'
|
||||
? FULL_DATA_GROUPS
|
||||
: FULL_DATA_GROUPS.filter(g => g.domain === domain)
|
||||
const totalRows = FULL_DATA_GROUPS.reduce((sum, g) => sum + (payload[g.key] || []).length, 0)
|
||||
const nonEmptyGroups = FULL_DATA_GROUPS.filter(g => (payload[g.key] || []).length > 0).length
|
||||
const maxChapter = FULL_DATA_GROUPS.reduce((max, g) => {
|
||||
const rows = payload[g.key] || []
|
||||
rows.slice(0, 120).forEach(r => {
|
||||
const c = extractChapter(r)
|
||||
if (c > max) max = c
|
||||
})
|
||||
return max
|
||||
}, 0)
|
||||
const domainStats = FULL_DATA_DOMAINS.filter(d => d.id !== 'overview').map(d => {
|
||||
const ds = FULL_DATA_GROUPS.filter(g => g.domain === d.id)
|
||||
const rowCount = ds.reduce((sum, g) => sum + (payload[g.key] || []).length, 0)
|
||||
const filled = ds.filter(g => (payload[g.key] || []).length > 0).length
|
||||
return { ...d, rowCount, filled, total: ds.length }
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header section-page-header">
|
||||
<h2>🧪 全量数据视图</h2>
|
||||
<span className="card-badge badge-cyan">{FULL_DATA_GROUPS.length} 类数据源</span>
|
||||
</div>
|
||||
|
||||
<div className="demo-summary-grid">
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">总记录数</span>
|
||||
<span className="stat-value">{formatNumber(totalRows)}</span>
|
||||
<span className="stat-sub">当前返回的全部数据行</span>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">已覆盖数据源</span>
|
||||
<span className="stat-value plain">{nonEmptyGroups}/{FULL_DATA_GROUPS.length}</span>
|
||||
<span className="stat-sub">有数据的表 / 总表数</span>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">最新章节触达</span>
|
||||
<span className="stat-value plain">{maxChapter > 0 ? `第 ${maxChapter} 章` : '—'}</span>
|
||||
<span className="stat-sub">按可识别 chapter 字段估算</span>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<span className="stat-label">当前视图</span>
|
||||
<span className="stat-value plain">{FULL_DATA_DOMAINS.find(d => d.id === domain)?.label}</span>
|
||||
<span className="stat-sub">{groups.length} 个数据分组</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="demo-domain-tabs">
|
||||
{FULL_DATA_DOMAINS.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`demo-domain-tab ${domain === item.id ? 'active' : ''}`}
|
||||
onClick={() => setDomain(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{domain === 'overview' ? (
|
||||
<div className="demo-domain-grid">
|
||||
{domainStats.map(ds => (
|
||||
<div className="card" key={ds.id}>
|
||||
<div className="card-header">
|
||||
<span className="card-title">{ds.label}</span>
|
||||
<span className="card-badge badge-purple">{ds.filled}/{ds.total}</span>
|
||||
</div>
|
||||
<div className="domain-stat-number">{formatNumber(ds.rowCount)}</div>
|
||||
<div className="stat-sub">该数据域总记录数</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{groups.map(g => {
|
||||
const count = (payload[g.key] || []).length
|
||||
return (
|
||||
<div className="card demo-group-card" key={g.key}>
|
||||
<div className="card-header">
|
||||
<span className="card-title">{g.title}</span>
|
||||
<span className={`card-badge ${count > 0 ? 'badge-blue' : 'badge-amber'}`}>{count} 条</span>
|
||||
</div>
|
||||
<MiniTable
|
||||
rows={payload[g.key] || []}
|
||||
columns={g.columns}
|
||||
pageSize={12}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MiniTable({ rows, columns, pageSize = 12 }) {
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [rows, columns, pageSize])
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return <div className="empty-state compact"><p>暂无数据</p></div>
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(rows.length / pageSize))
|
||||
const safePage = Math.min(page, totalPages)
|
||||
const start = (safePage - 1) * pageSize
|
||||
const list = rows.slice(start, start + pageSize)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>{columns.map(c => <th key={c}>{c}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(c => (
|
||||
<td key={c} className="truncate" style={{ maxWidth: 240 }}>
|
||||
{formatCell(row?.[c])}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="table-pagination">
|
||||
<button
|
||||
className="page-btn"
|
||||
type="button"
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={safePage <= 1}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className="page-info">
|
||||
第 {safePage} / {totalPages} 页 · 共 {rows.length} 条
|
||||
</span>
|
||||
<button
|
||||
className="page-btn"
|
||||
type="button"
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={safePage >= totalPages}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function extractChapter(row) {
|
||||
if (!row || typeof row !== 'object') return 0
|
||||
const candidates = [
|
||||
row.chapter,
|
||||
row.start_chapter,
|
||||
row.end_chapter,
|
||||
row.chapter_discovered,
|
||||
row.first_appearance,
|
||||
row.last_appearance,
|
||||
]
|
||||
for (const c of candidates) {
|
||||
const n = Number(c)
|
||||
if (Number.isFinite(n) && n > 0) return n
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 子组件:文件树递归
|
||||
// ====================================================================
|
||||
|
||||
function TreeNodes({ items, selected, onSelect, depth = 0 }) {
|
||||
const [expanded, setExpanded] = useState({})
|
||||
if (!items || items.length === 0) return null
|
||||
|
||||
return items.map((item, i) => {
|
||||
const key = item.path || `${depth}-${i}`
|
||||
if (item.type === 'dir') {
|
||||
const isOpen = expanded[key]
|
||||
return (
|
||||
<li key={key}>
|
||||
<div
|
||||
className="tree-item"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), setExpanded(prev => ({ ...prev, [key]: !prev[key] })))}
|
||||
onClick={() => setExpanded(prev => ({ ...prev, [key]: !prev[key] }))}
|
||||
>
|
||||
<span className="tree-icon">{isOpen ? '📂' : '📁'}</span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
{isOpen && item.children && (
|
||||
<ul className="tree-children">
|
||||
<TreeNodes items={item.children} selected={selected} onSelect={onSelect} depth={depth + 1} />
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<li key={key}>
|
||||
<div
|
||||
className={`tree-item ${selected === item.path ? 'active' : ''}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), onSelect(item.path))}
|
||||
onClick={() => onSelect(item.path)}
|
||||
>
|
||||
<span className="tree-icon">📄</span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// 辅助:数字格式化
|
||||
// ====================================================================
|
||||
|
||||
function formatNumber(n) {
|
||||
if (n >= 10000) return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 1 }).format(n / 10000) + ' 万'
|
||||
return new Intl.NumberFormat('zh-CN').format(n)
|
||||
}
|
||||
|
||||
function formatJSON(str) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(str), null, 2)
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
function formatCell(v) {
|
||||
if (v === null || v === undefined) return '—'
|
||||
if (typeof v === 'boolean') return v ? 'true' : 'false'
|
||||
if (typeof v === 'object') {
|
||||
try {
|
||||
return JSON.stringify(v)
|
||||
} catch {
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
const s = String(v)
|
||||
return s.length > 180 ? `${s.slice(0, 180)}...` : s
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Handoff_Diff_View.jsx - 状态交接人工审查 Commit 界面
|
||||
*
|
||||
* 特性:
|
||||
* - 展示 AI 生成的下一章 handoff.json Diff
|
||||
* - 支持人工微调
|
||||
* - Commit 后进入实际写作
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { fetchJSON } from './api.js'
|
||||
|
||||
export default function HandoffDiffView({ projectRoot, onCommit, onCancel }) {
|
||||
const [currentHandoff, setCurrentHandoff] = useState(null)
|
||||
const [proposedHandoff, setProposedHandoff] = useState(null)
|
||||
const [diff, setDiff] = useState(null)
|
||||
const [editedFrame, setEditedFrame] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [commitMessage, setCommitMessage] = useState('')
|
||||
|
||||
// 加载当前 Handoff Frame
|
||||
const loadHandoff = useCallback(async () => {
|
||||
try {
|
||||
// 实际应从 API 获取
|
||||
const current = await fetchJSON('/api/project/info')
|
||||
const handoff = current.handoff_frame || generateMockHandoff()
|
||||
|
||||
setCurrentHandoff(handoff)
|
||||
setProposedHandoff(handoff)
|
||||
setEditedFrame(handoff)
|
||||
setDiff(computeDiff(handoff, handoff))
|
||||
} catch (error) {
|
||||
console.error('Failed to load handoff:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadHandoff()
|
||||
}, [loadHandoff])
|
||||
|
||||
// 生成下一章的 Handoff 预测
|
||||
const generateNextHandoff = useCallback((current) => {
|
||||
return {
|
||||
...current,
|
||||
tick: current.tick + 1,
|
||||
chapter: (current.metadata?.chapter_number || 0) + 1,
|
||||
metadata: {
|
||||
...current.metadata,
|
||||
previous_chapter_summary: `第${current.metadata?.chapter_number || 1}章内容摘要`,
|
||||
next_chapter_hint: '根据当前状态自动生成'
|
||||
},
|
||||
world_state: {
|
||||
...current.world_state,
|
||||
tension_level: Math.max(0, (current.world_state?.tension_level || 50) - 20)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 计算 Diff
|
||||
const computeDiff = (current, proposed) => {
|
||||
if (!current || !proposed) return null
|
||||
|
||||
const changes = []
|
||||
const currentStr = JSON.stringify(current)
|
||||
const proposedStr = JSON.stringify(proposed)
|
||||
|
||||
if (currentStr !== proposedStr) {
|
||||
// 逐层比较
|
||||
const currentJson = current
|
||||
const proposedJson = proposed
|
||||
|
||||
// Active Characters
|
||||
if (JSON.stringify(currentJson.active_characters) !== JSON.stringify(proposedJson.active_characters)) {
|
||||
changes.push({
|
||||
section: 'active_characters',
|
||||
type: 'modified',
|
||||
before: currentJson.active_characters,
|
||||
after: proposedJson.active_characters
|
||||
})
|
||||
}
|
||||
|
||||
// World State
|
||||
if (JSON.stringify(currentJson.world_state) !== JSON.stringify(proposedJson.world_state)) {
|
||||
changes.push({
|
||||
section: 'world_state',
|
||||
type: 'modified',
|
||||
before: currentJson.world_state,
|
||||
after: proposedJson.world_state
|
||||
})
|
||||
}
|
||||
|
||||
// Metadata
|
||||
if (JSON.stringify(currentJson.metadata) !== JSON.stringify(proposedJson.metadata)) {
|
||||
changes.push({
|
||||
section: 'metadata',
|
||||
type: 'modified',
|
||||
before: currentJson.metadata,
|
||||
after: proposedJson.metadata
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// 处理字段编辑
|
||||
const handleFieldEdit = (section, field, value) => {
|
||||
const updated = {
|
||||
...editedFrame,
|
||||
[section]: {
|
||||
...editedFrame[section],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
setEditedFrame(updated)
|
||||
setDiff(computeDiff(currentHandoff, updated))
|
||||
}
|
||||
|
||||
// 处理 Commit
|
||||
const handleCommit = () => {
|
||||
if (!commitMessage.trim()) {
|
||||
alert('请输入 commit 消息')
|
||||
return
|
||||
}
|
||||
|
||||
const commitData = {
|
||||
handoff_frame: editedFrame,
|
||||
commit_message: commitMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
onCommit?.(commitData)
|
||||
}
|
||||
|
||||
// 生成模拟 Handoff
|
||||
const generateMockHandoff = () => ({
|
||||
tick: 1,
|
||||
timestamp: Date.now(),
|
||||
active_characters: [
|
||||
{ id: 'protagonist', name: '主角', status: 'active', position: { x: 0, y: 0, z: 0 } }
|
||||
],
|
||||
suspended_characters: [],
|
||||
imminent_actions: [],
|
||||
world_state: {
|
||||
location: '天云宗',
|
||||
time_of_day: '早晨',
|
||||
weather: '晴',
|
||||
tension_level: 50,
|
||||
hook_pressure: 30
|
||||
},
|
||||
metadata: {
|
||||
chapter_number: 1,
|
||||
scene_number: 1,
|
||||
previous_chapter_summary: '开篇设定',
|
||||
next_chapter_hint: '引入冲突'
|
||||
}
|
||||
})
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">加载 Handoff 数据中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="handoff-diff-view">
|
||||
<div className="diff-header">
|
||||
<h2>📋 Handoff 状态交接审查</h2>
|
||||
<p className="diff-subtitle">审查 AI 生成的下一章状态交接帧,确认后 Commit</p>
|
||||
</div>
|
||||
|
||||
<div className="diff-layout">
|
||||
{/* 左侧: 当前状态 */}
|
||||
<div className="diff-panel current">
|
||||
<h3>当前帧 (Tick {currentHandoff?.tick})</h3>
|
||||
<div className="diff-content">
|
||||
<HandoffPanel
|
||||
title="活跃角色"
|
||||
data={currentHandoff?.active_characters || []}
|
||||
type="characters"
|
||||
/>
|
||||
<HandoffPanel
|
||||
title="世界状态"
|
||||
data={currentHandoff?.world_state || {}}
|
||||
type="world_state"
|
||||
/>
|
||||
<HandoffPanel
|
||||
title="元数据"
|
||||
data={currentHandoff?.metadata || {}}
|
||||
type="metadata"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间: Diff */}
|
||||
<div className="diff-panel changes">
|
||||
<h3>变更内容</h3>
|
||||
<div className="diff-changes">
|
||||
{diff && diff.length > 0 ? (
|
||||
diff.map((change, idx) => (
|
||||
<div key={idx} className={`diff-item diff-${change.type}`}>
|
||||
<div className="diff-section">{change.section}</div>
|
||||
<div className="diff-values">
|
||||
<div className="diff-before">
|
||||
<span className="label">-</span>
|
||||
<pre>{JSON.stringify(change.before, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="diff-after">
|
||||
<span className="label">+</span>
|
||||
<pre>{JSON.stringify(change.after, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="no-changes">无变更</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧: 提议状态 */}
|
||||
<div className="diff-panel proposed">
|
||||
<h3>提议帧 (Tick {(editedFrame?.tick || currentHandoff?.tick) + 1})</h3>
|
||||
<div className="diff-content">
|
||||
<HandoffEditablePanel
|
||||
title="活跃角色"
|
||||
data={editedFrame?.active_characters || []}
|
||||
type="characters"
|
||||
onEdit={(field, value) => handleFieldEdit('active_characters', field, value)}
|
||||
/>
|
||||
<HandoffEditablePanel
|
||||
title="世界状态"
|
||||
data={editedFrame?.world_state || {}}
|
||||
type="world_state"
|
||||
onEdit={(field, value) => handleFieldEdit('world_state', field, value)}
|
||||
/>
|
||||
<HandoffEditablePanel
|
||||
title="元数据"
|
||||
data={editedFrame?.metadata || {}}
|
||||
type="metadata"
|
||||
onEdit={(field, value) => handleFieldEdit('metadata', field, value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Commit 控制区 */}
|
||||
<div className="commit-controls">
|
||||
<div className="commit-message">
|
||||
<label>Commit 消息:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
placeholder="描述本次交接的变更..."
|
||||
/>
|
||||
</div>
|
||||
<div className="commit-actions">
|
||||
<button className="btn-cancel" onClick={onCancel}>
|
||||
取消
|
||||
</button>
|
||||
<button className="btn-commit" onClick={handleCommit}>
|
||||
✓ Commit & 继续写作
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HandoffPanel({ title, data, type }) {
|
||||
return (
|
||||
<div className="handoff-section">
|
||||
<h4>{title}</h4>
|
||||
<pre className="handoff-data">
|
||||
{type === 'characters'
|
||||
? data.map(c => `${c.name} (${c.status})`).join(', ')
|
||||
: JSON.stringify(data, null, 2)
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HandoffEditablePanel({ title, data, type, onEdit }) {
|
||||
return (
|
||||
<div className="handoff-section editable">
|
||||
<h4>{title}</h4>
|
||||
{type === 'characters' ? (
|
||||
<div className="character-list">
|
||||
{data.map((char, idx) => (
|
||||
<div key={char.id || idx} className="character-item">
|
||||
<input
|
||||
type="text"
|
||||
value={char.name}
|
||||
onChange={(e) => onEdit(`${idx}.name`, e.target.value)}
|
||||
/>
|
||||
<select
|
||||
value={char.status}
|
||||
onChange={(e) => onEdit(`${idx}.status`, e.target.value)}
|
||||
>
|
||||
<option value="active">活跃</option>
|
||||
<option value="dormant">休眠</option>
|
||||
<option value="suspended">悬置</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="field-editors">
|
||||
{Object.entries(data).map(([field, value]) => (
|
||||
<div key={field} className="field-editor">
|
||||
<label>{field}:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
onChange={(e) => onEdit(field, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Meta_Narrative_Panel.jsx - 元叙事开关与黑天鹅注入控制台
|
||||
*
|
||||
* 特性:
|
||||
* - 第四面墙 Toggle: 开启后关闭物理连贯性校验
|
||||
* - 黑天鹅注入: 一键注入等价对冲债务或平账大纲
|
||||
* - Hook 池监控: 显示当前压强状态
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { fetchJSON } from './api.js'
|
||||
|
||||
export default function MetaNarrativePanel({ projectRoot, onToggle, onInject }) {
|
||||
const [fourthWallToggle, setFourthWallToggle] = useState(false)
|
||||
const [hookPressure, setHookPressure] = useState(0)
|
||||
const [debtBalance, setDebtBalance] = useState(0)
|
||||
const [blackSwanOptions, setBlackSwanOptions] = useState([])
|
||||
const [selectedSwan, setSelectedSwan] = useState(null)
|
||||
const [swanParams, setSwanParams] = useState({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 加载 Hook 池状态
|
||||
const loadHookStatus = useCallback(async () => {
|
||||
try {
|
||||
const info = await fetchJSON('/api/project/info')
|
||||
const hooks = info.hooks_pool?.hooks || []
|
||||
const unresolvedCount = hooks.filter(h => !h.resolved).length
|
||||
const pressure = hooks.reduce((sum, h) => sum + (h.tension_weight || 0), 0)
|
||||
|
||||
setHookPressure(pressure)
|
||||
setDebtBalance(info.debt_balance || 0)
|
||||
|
||||
// 生成黑天鹅选项
|
||||
generateBlackSwanOptions(pressure, unresolvedCount)
|
||||
} catch (error) {
|
||||
console.error('Failed to load hook status:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadHookStatus()
|
||||
// 定期刷新
|
||||
const interval = setInterval(loadHookStatus, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [loadHookStatus])
|
||||
|
||||
// 生成黑天鹅注入选项
|
||||
const generateBlackSwanOptions = (pressure, unresolvedCount) => {
|
||||
const options = []
|
||||
|
||||
if (pressure > 80) {
|
||||
options.push({
|
||||
id: 'swan_release',
|
||||
label: '🕊️ 高压释放',
|
||||
description: '引入突发转折,大量释放压强',
|
||||
pressureImpact: -40,
|
||||
type: 'release'
|
||||
})
|
||||
}
|
||||
|
||||
if (unresolvedCount > 5) {
|
||||
options.push({
|
||||
id: 'swan_closure',
|
||||
label: '🔗 批量收束',
|
||||
description: '同时回收多个伏笔,认知闭合',
|
||||
pressureImpact: -20,
|
||||
type: 'closure'
|
||||
})
|
||||
}
|
||||
|
||||
if (debtBalance > 0) {
|
||||
options.push({
|
||||
id: 'swan_hedge',
|
||||
label: '⚖️ 对冲平账',
|
||||
description: '注入等价对冲债务,平衡账本',
|
||||
pressureImpact: 0,
|
||||
type: 'hedge'
|
||||
})
|
||||
}
|
||||
|
||||
options.push({
|
||||
id: 'swan_new_hook',
|
||||
label: '🎣 新增悬念',
|
||||
description: '引入全新悬念,增加压强',
|
||||
pressureImpact: 15,
|
||||
type: 'buildup'
|
||||
})
|
||||
|
||||
if (fourthWallToggle) {
|
||||
options.push({
|
||||
id: 'swan_meta',
|
||||
label: '🌀 元叙事突破',
|
||||
description: '打破第四面墙,引入反规则事件',
|
||||
pressureImpact: 30,
|
||||
type: 'meta',
|
||||
requiresFourthWall: true
|
||||
})
|
||||
}
|
||||
|
||||
setBlackSwanOptions(options)
|
||||
}
|
||||
|
||||
// 切换第四面墙
|
||||
const handleFourthWallToggle = () => {
|
||||
const newState = !fourthWallToggle
|
||||
setFourthWallToggle(newState)
|
||||
onToggle?.(newState)
|
||||
}
|
||||
|
||||
// 注入黑天鹅
|
||||
const handleInjectSwan = () => {
|
||||
if (!selectedSwan) {
|
||||
alert('请选择一个黑天鹅事件')
|
||||
return
|
||||
}
|
||||
|
||||
const swanConfig = {
|
||||
...selectedSwan,
|
||||
params: swanParams,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
onInject?.(swanConfig)
|
||||
}
|
||||
|
||||
// 获取压力颜色
|
||||
const getPressureColor = (pressure) => {
|
||||
if (pressure < 30) return 'var(--accent-green)'
|
||||
if (pressure < 60) return 'var(--accent-amber)'
|
||||
if (pressure < 80) return 'var(--accent-orange)'
|
||||
return 'var(--accent-red)'
|
||||
}
|
||||
|
||||
// 获取压力状态
|
||||
const getPressureStatus = (pressure) => {
|
||||
if (pressure < 30) return '低压 - 可积累'
|
||||
if (pressure < 60) return '中压 - 正常'
|
||||
if (pressure < 80) return '高压 - 警告'
|
||||
return '危险 - 急需释放'
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">加载中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="meta-narrative-panel">
|
||||
<div className="panel-header">
|
||||
<h2>🌀 元叙事控制台</h2>
|
||||
<p className="panel-subtitle">第四面墙切换与黑天鹅事件注入</p>
|
||||
</div>
|
||||
|
||||
{/* 第四面墙 Toggle */}
|
||||
<div className="control-section fourth-wall">
|
||||
<div className="section-header">
|
||||
<h3>第四面墙 Toggle</h3>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fourthWallToggle}
|
||||
onChange={handleFourthWallToggle}
|
||||
/>
|
||||
<span className="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="toggle-description">
|
||||
{fourthWallToggle ? (
|
||||
<p className="status-active">
|
||||
🟢 <strong>元叙事模式已激活</strong><br />
|
||||
<span>物理连贯性校验已关闭,允许"反规则"写法</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="status-inactive">
|
||||
⚪ <strong>正常叙事模式</strong><br />
|
||||
<span>物理连贯性校验正常执行</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="toggle-effects">
|
||||
<h4>开启后的效果:</h4>
|
||||
<ul>
|
||||
<li>✓ 关闭战力/境界一致性校验</li>
|
||||
<li>✓ 关闭时间线连续性检查</li>
|
||||
<li>✓ 允许梦境、意识流等反逻辑场景</li>
|
||||
<li>✓ 允许主角打破第四面墙</li>
|
||||
<li>⚠️ 可能产生世界观不一致风险</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hook 池监控 */}
|
||||
<div className="control-section hook-monitor">
|
||||
<div className="section-header">
|
||||
<h3>📊 Hook 池监控</h3>
|
||||
</div>
|
||||
<div className="pressure-display">
|
||||
<div className="pressure-gauge">
|
||||
<div
|
||||
className="pressure-fill"
|
||||
style={{
|
||||
width: `${Math.min(100, hookPressure)}%`,
|
||||
backgroundColor: getPressureColor(hookPressure)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="pressure-value">
|
||||
<span className="pressure-number" style={{ color: getPressureColor(hookPressure) }}>
|
||||
{hookPressure}
|
||||
</span>
|
||||
<span className="pressure-max">/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pressure-status" style={{ color: getPressureColor(hookPressure) }}>
|
||||
{getPressureStatus(hookPressure)}
|
||||
</div>
|
||||
<div className="debt-balance">
|
||||
<span>债务余额:</span>
|
||||
<span className={debtBalance > 0 ? 'debt-positive' : 'debt-normal'}>
|
||||
{debtBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{hookPressure > 80 && (
|
||||
<div className="pressure-warning">
|
||||
<span className="warning-icon">⚠️</span>
|
||||
<span>压强过高,建议立即注入黑天鹅事件释放</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 黑天鹅注入 */}
|
||||
<div className="control-section black-swan">
|
||||
<div className="section-header">
|
||||
<h3>🦢 黑天鹅事件注入</h3>
|
||||
</div>
|
||||
<p className="section-desc">
|
||||
点击下方选项注入突发事件,对冲债务或释放压强
|
||||
</p>
|
||||
|
||||
<div className="swan-options">
|
||||
{blackSwanOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
className={`swan-option ${selectedSwan?.id === option.id ? 'selected' : ''} ${option.requiresFourthWall && !fourthWallToggle ? 'disabled' : ''}`}
|
||||
onClick={() => {
|
||||
if (!option.requiresFourthWall || fourthWallToggle) {
|
||||
setSelectedSwan(option)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="swan-label">{option.label}</div>
|
||||
<div className="swan-desc">{option.description}</div>
|
||||
<div className="swan-impact">
|
||||
压强变化:{' '}
|
||||
<span className={option.pressureImpact < 0 ? 'negative' : option.pressureImpact > 0 ? 'positive' : ''}>
|
||||
{option.pressureImpact > 0 ? '+' : ''}{option.pressureImpact}
|
||||
</span>
|
||||
</div>
|
||||
{option.requiresFourthWall && !fourthWallToggle && (
|
||||
<div className="swan-requires">需要第四面墙开启</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedSwan && (
|
||||
<div className="swan-params">
|
||||
<h4>事件参数</h4>
|
||||
<div className="param-inputs">
|
||||
<div className="param-field">
|
||||
<label>事件名称:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={swanParams.name || ''}
|
||||
onChange={(e) => setSwanParams({ ...swanParams, name: e.target.value })}
|
||||
placeholder="输入事件名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="param-field">
|
||||
<label>影响章节数:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={swanParams.chapters || 1}
|
||||
onChange={(e) => setSwanParams({ ...swanParams, chapters: parseInt(e.target.value) })}
|
||||
min={1}
|
||||
max={10}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn-inject"
|
||||
onClick={handleInjectSwan}
|
||||
disabled={!selectedSwan}
|
||||
>
|
||||
🦢 注入黑天鹅事件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Retcon_Arbitrator.jsx - 设定修改冲突分析与补丁确认台
|
||||
*
|
||||
* 特性:
|
||||
* - 接收作者的设定修改需求
|
||||
* - 分析与现有账本/Hook池的冲突
|
||||
* - 生成兼容性补丁方案
|
||||
* - 人类确认后应用热补丁
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { fetchJSON } from './api.js'
|
||||
|
||||
export default function RetconArbitrator({ projectRoot, onApply, onCancel }) {
|
||||
const [authorIntent, setAuthorIntent] = useState('')
|
||||
const [analysisResult, setAnalysisResult] = useState(null)
|
||||
const [patchProposal, setPatchProposal] = useState(null)
|
||||
const [selectedOperations, setSelectedOperations] = useState({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 分析 Retcon 兼容性
|
||||
const analyzeRetcon = useCallback(async () => {
|
||||
if (!authorIntent.trim()) {
|
||||
alert('请输入设定修改需求')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
// 模拟 API 调用
|
||||
const result = await mockAnalyzeRetcon(authorIntent)
|
||||
setAnalysisResult(result)
|
||||
|
||||
if (result.conflicts.length > 0) {
|
||||
setPatchProposal(result.patch)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Analysis failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [authorIntent])
|
||||
|
||||
// 应用补丁
|
||||
const applyPatch = useCallback(() => {
|
||||
const selectedOps = Object.entries(selectedOperations)
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([opId, _]) => patchProposal.operations.find(op => op.id === opId))
|
||||
|
||||
const patch = {
|
||||
...patchProposal,
|
||||
operations: selectedOps
|
||||
}
|
||||
|
||||
onApply?.(patch)
|
||||
}, [selectedOperations, patchProposal, onApply])
|
||||
|
||||
// 切换操作选择
|
||||
const toggleOperation = (opId) => {
|
||||
setSelectedOperations(prev => ({
|
||||
...prev,
|
||||
[opId]: !prev[opId]
|
||||
}))
|
||||
}
|
||||
|
||||
// 模拟分析结果
|
||||
const mockAnalyzeRetcon = (intent) => {
|
||||
// 简单模拟:当输入包含特定关键词时触发冲突
|
||||
const conflicts = []
|
||||
const patch = {
|
||||
id: `retcon_${Date.now()}`,
|
||||
generated_at: Date.now(),
|
||||
operations: [],
|
||||
compatibility_issues: []
|
||||
}
|
||||
|
||||
if (intent.includes('克苏鲁') || intent.includes('邪神')) {
|
||||
conflicts.push({
|
||||
element_id: 'asset_haoqi_sword',
|
||||
element_name: '浩然正气剑',
|
||||
element_type: 'asset',
|
||||
original_value: '正道至宝',
|
||||
proposed_change: '与克苏鲁设定冲突',
|
||||
compatibility_score: 25,
|
||||
resolution: '将浩然正气剑转化为上古邪神骨殖的伪装'
|
||||
})
|
||||
|
||||
patch.operations.push({
|
||||
id: 'op_1',
|
||||
type: 'relabel',
|
||||
target_type: 'asset',
|
||||
target_id: 'asset_haoqi_sword',
|
||||
old_value: { name: '浩然正气剑', type: 'skill', value: 80 },
|
||||
new_value: { name: '邪神骨殖剑', type: 'skill', value: 80, tags: ['corrupted', 'dangerous'] },
|
||||
description: '将正道之物转化为邪神相关设定'
|
||||
})
|
||||
|
||||
patch.operations.push({
|
||||
id: 'op_2',
|
||||
type: 'create',
|
||||
target_type: 'liability',
|
||||
target_id: 'liab_haoqi_curse',
|
||||
old_value: null,
|
||||
new_value: {
|
||||
id: 'liab_haoqi_curse',
|
||||
type: 'unresolved_conflict',
|
||||
name: '浩然正气反噬',
|
||||
description: '正气剑被邪神侵蚀后的反噬',
|
||||
severity: 60
|
||||
},
|
||||
description: '创建反噬负债'
|
||||
})
|
||||
|
||||
patch.compatibility_issues.push('资产"浩然正气剑"被标记为不兼容,转化为对冲负债')
|
||||
}
|
||||
|
||||
if (intent.includes('穿越') || intent.includes('异世界')) {
|
||||
conflicts.push({
|
||||
element_id: 'world_rule_cultivation',
|
||||
element_name: '修仙世界观',
|
||||
element_type: 'world_rule',
|
||||
original_value: '传统修仙',
|
||||
proposed_change: '引入穿越元素',
|
||||
compatibility_score: 60,
|
||||
resolution: '将穿越者设定为域外天魔'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
author_intent: intent,
|
||||
conflicts,
|
||||
patch,
|
||||
compatibility_score: conflicts.length > 0
|
||||
? Math.min(...conflicts.map(c => c.compatibility_score))
|
||||
: 100
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="retcon-arbitrator">
|
||||
<div className="arbi-header">
|
||||
<h2>🔧 Retcon 仲裁台</h2>
|
||||
<p className="arbi-subtitle">
|
||||
输入设定修改需求,系统分析冲突并生成补丁方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 输入区 */}
|
||||
<div className="arbi-input-section">
|
||||
<label>设定修改需求:</label>
|
||||
<textarea
|
||||
value={authorIntent}
|
||||
onChange={(e) => setAuthorIntent(e.target.value)}
|
||||
placeholder="描述你想要修改的设定,例如: - 将修仙世界观转为克苏鲁修仙 - 主角穿越到异世界 - 移除某个配角的反派设定"
|
||||
rows={5}
|
||||
/>
|
||||
<button
|
||||
className="btn-analyze"
|
||||
onClick={analyzeRetcon}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '分析中...' : '⚡ 分析冲突'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 分析结果 */}
|
||||
{analysisResult && (
|
||||
<div className="arbi-analysis">
|
||||
<div className="analysis-summary">
|
||||
<div className={`compatibility-badge ${analysisResult.compatibility_score >= 70 ? 'high' : analysisResult.compatibility_score >= 40 ? 'medium' : 'low'}`}>
|
||||
兼容性: {analysisResult.compatibility_score}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 冲突列表 */}
|
||||
{analysisResult.conflicts.length > 0 && (
|
||||
<div className="conflicts-section">
|
||||
<h3>⚠️ 发现 {analysisResult.conflicts.length} 个冲突</h3>
|
||||
<div className="conflicts-list">
|
||||
{analysisResult.conflicts.map((conflict, idx) => (
|
||||
<div key={idx} className="conflict-card">
|
||||
<div className="conflict-header">
|
||||
<span className="element-name">{conflict.element_name}</span>
|
||||
<span className={`compatibility-score ${conflict.compatibility_score >= 70 ? 'high' : conflict.compatibility_score >= 40 ? 'medium' : 'low'}`}>
|
||||
{conflict.compatibility_score}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="conflict-body">
|
||||
<p><strong>原值:</strong> {conflict.original_value}</p>
|
||||
<p><strong>变更:</strong> {conflict.proposed_change}</p>
|
||||
<p><strong>建议:</strong> {conflict.resolution}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysisResult.conflicts.length === 0 && (
|
||||
<div className="no-conflicts">
|
||||
<p>✓ 未发现冲突,可以直接应用变更</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 补丁方案 */}
|
||||
{patchProposal && patchProposal.operations.length > 0 && (
|
||||
<div className="patch-section">
|
||||
<h3>📦 补丁方案</h3>
|
||||
<div className="operations-list">
|
||||
{patchProposal.operations.map((op) => (
|
||||
<div
|
||||
key={op.id}
|
||||
className={`operation-card ${selectedOperations[op.id] ? 'selected' : ''}`}
|
||||
onClick={() => toggleOperation(op.id)}
|
||||
>
|
||||
<div className="op-header">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!selectedOperations[op.id]}
|
||||
onChange={() => toggleOperation(op.id)}
|
||||
/>
|
||||
<span className="op-type">{op.type}</span>
|
||||
<span className="op-target">{op.target_type}: {op.target_id}</span>
|
||||
</div>
|
||||
<div className="op-body">
|
||||
<p className="op-description">{op.description}</p>
|
||||
<div className="op-values">
|
||||
<div className="op-old">
|
||||
<span>旧值:</span>
|
||||
<pre>{JSON.stringify(op.old_value, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="op-new">
|
||||
<span>新值:</span>
|
||||
<pre>{JSON.stringify(op.new_value, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 控制区 */}
|
||||
<div className="arbi-controls">
|
||||
<button className="btn-cancel" onClick={onCancel}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="btn-apply"
|
||||
onClick={applyPatch}
|
||||
disabled={!patchProposal || Object.values(selectedOperations).every(v => !v)}
|
||||
>
|
||||
✓ 应用选定补丁
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* API 请求工具函数
|
||||
*/
|
||||
|
||||
const BASE = ''; // 开发时由 vite proxy 代理到 FastAPI
|
||||
|
||||
export async function fetchJSON(path, params = {}) {
|
||||
const url = new URL(path, window.location.origin);
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null) url.searchParams.set(k, v);
|
||||
});
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅 SSE 实时事件流
|
||||
* @param {function} onMessage 收到 data 时回调
|
||||
* @param {{onOpen?: function, onError?: function}} handlers 连接状态回调
|
||||
* @returns {function} 取消订阅函数
|
||||
*/
|
||||
export function subscribeSSE(onMessage, handlers = {}) {
|
||||
const { onOpen, onError } = handlers
|
||||
const es = new EventSource(`${BASE}/api/events`);
|
||||
es.onopen = () => {
|
||||
if (onOpen) onOpen()
|
||||
};
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
onMessage(JSON.parse(e.data));
|
||||
} catch { /* ignore parse errors */ }
|
||||
};
|
||||
es.onerror = (e) => {
|
||||
// EventSource 会自动重连,这里只更新连接状态
|
||||
if (onError) onError(e)
|
||||
};
|
||||
return () => es.close();
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Noto+Sans+SC:wght@400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-main: #fff7e8;
|
||||
--bg-panel: #fffdf6;
|
||||
--bg-card: #fffaf0;
|
||||
--bg-card-2: #fff3d5;
|
||||
--text-main: #2a220f;
|
||||
--text-sub: #5d5035;
|
||||
--text-mute: #8f7f5c;
|
||||
--accent-blue: #26a8ff;
|
||||
--accent-purple: #7f5af0;
|
||||
--accent-green: #2ec27e;
|
||||
--accent-amber: #f5a524;
|
||||
--accent-red: #d7263d;
|
||||
--accent-cyan: #00b8d4;
|
||||
--border-main: #2a220f;
|
||||
--border-soft: #8f7f5c;
|
||||
--shadow-main: 6px 6px 0 #2a220f;
|
||||
--shadow-soft: 3px 3px 0 #8f7f5c;
|
||||
--font-display: 'Press Start 2P', monospace;
|
||||
--font-body: 'Noto Sans SC', 'Microsoft YaHei', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 3px dashed var(--accent-blue);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--text-main);
|
||||
background-color: var(--bg-main);
|
||||
background-image:
|
||||
linear-gradient(90deg, rgba(42, 34, 15, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(rgba(42, 34, 15, 0.05) 1px, transparent 1px);
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 3px solid var(--border-main);
|
||||
background: linear-gradient(180deg, #ffe8b8 0%, #ffe19f 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 3px solid var(--border-main);
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.sidebar-header .subtitle {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-sub);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
border: 2px solid var(--border-main);
|
||||
background: #fff9e8;
|
||||
color: var(--text-main);
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-soft);
|
||||
transition: transform 0.08s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
transform: translate(-1px, -1px);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #dff3ff;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.nav-item .icon {
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
border-top: 3px solid var(--border-main);
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--accent-green);
|
||||
border: 2px solid var(--border-main);
|
||||
}
|
||||
|
||||
.live-dot.disconnected {
|
||||
background: var(--accent-red);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-page-header {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 3px solid var(--border-main);
|
||||
box-shadow: var(--shadow-main);
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-badge {
|
||||
border: 2px solid var(--border-main);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.badge-blue { background: #dff3ff; color: #055d8b; }
|
||||
.badge-green { background: #dcfce7; color: #0f5132; }
|
||||
.badge-amber { background: #fff1cd; color: #8a5b00; }
|
||||
.badge-red { background: #ffe0e5; color: #8f1d30; }
|
||||
.badge-purple { background: #ece3ff; color: #4a2ea8; }
|
||||
.badge-cyan { background: #dcfafe; color: #155e75; }
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-mute);
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 28px;
|
||||
line-height: 1.15;
|
||||
margin: 6px 0 2px;
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.stat-card .stat-value.plain {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.stat-sub {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-sub);
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
margin-top: 8px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--border-main);
|
||||
background: #f8e3b8;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #26a8ff, #7f5af0);
|
||||
}
|
||||
|
||||
.dashboard-section-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.strand-bar {
|
||||
height: 12px;
|
||||
border: 2px solid var(--border-main);
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.strand-bar .segment { height: 100%; }
|
||||
.strand-quest { background: #26a8ff; }
|
||||
.strand-fire { background: #ff5c8a; }
|
||||
.strand-constellation { background: #7f5af0; }
|
||||
|
||||
.strand-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-sub);
|
||||
}
|
||||
|
||||
.demo-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.demo-domain-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.demo-domain-tab {
|
||||
border: 2px solid var(--border-main);
|
||||
background: #fff8e6;
|
||||
color: var(--text-main);
|
||||
padding: 6px 10px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-domain-tab.active {
|
||||
background: #dff3ff;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.demo-domain-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.domain-stat-number {
|
||||
font-size: 30px;
|
||||
color: var(--accent-purple);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.demo-group-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 2px solid var(--border-soft);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
min-width: 580px;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-body);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 2px solid var(--border-soft);
|
||||
background: var(--bg-card-2);
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #d8ccb2;
|
||||
color: var(--text-main);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover td {
|
||||
background: #fff4d8;
|
||||
}
|
||||
|
||||
.table-foot-note {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
border: 2px solid var(--border-main);
|
||||
background: #fff8e6;
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
background: #e6f7ff;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-sub);
|
||||
}
|
||||
|
||||
.split-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 340px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.split-main,
|
||||
.split-side {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entity-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.entity-row.selected td {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.entity-name.protagonist {
|
||||
color: #b86a00;
|
||||
}
|
||||
|
||||
.entity-detail {
|
||||
font-size: 14px;
|
||||
color: var(--text-sub);
|
||||
line-height: 1.7;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.entity-detail code {
|
||||
border: 1px solid var(--border-soft);
|
||||
padding: 1px 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.entity-desc {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.entity-current-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.entity-json {
|
||||
margin-top: 6px;
|
||||
border: 2px solid var(--border-soft);
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
max-height: 190px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
}
|
||||
|
||||
.entity-history {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.graph-shell {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 120px);
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
.chapter-no {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chapter-characters {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.file-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
height: calc(100vh - 130px);
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.file-tree-pane {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border: 3px solid var(--border-main);
|
||||
box-shadow: var(--shadow-soft);
|
||||
background: #fffcf5;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.file-content-pane {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: 3px solid var(--border-main);
|
||||
box-shadow: var(--shadow-soft);
|
||||
background: #fffcf5;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-content-pane > div {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.folder-block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.folder-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.selected-path {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-mute);
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-tree {
|
||||
list-style: none;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.tree-item {
|
||||
border: 2px solid transparent;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-item:hover {
|
||||
background: #fff4d8;
|
||||
border-color: #e0c98d;
|
||||
}
|
||||
|
||||
.tree-item.active {
|
||||
background: #e6f7ff;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tree-children {
|
||||
list-style: none;
|
||||
margin-left: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 2px dashed #d8ccb2;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
border: 2px solid var(--border-soft);
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.75;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.debt-positive { color: var(--accent-red); font-weight: 700; }
|
||||
.debt-normal { color: var(--text-sub); }
|
||||
|
||||
.loading {
|
||||
border: 3px solid var(--border-main);
|
||||
background: #fff9e8;
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow-main);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 14px;
|
||||
color: var(--text-sub);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.file-content-pane .empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-state.compact {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.empty-state .empty-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
border: 2px solid var(--border-main);
|
||||
background: #fff8e6;
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #e6f7ff;
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.split-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.graph-shell {
|
||||
height: calc(100vh - 128px);
|
||||
min-height: 460px;
|
||||
}
|
||||
|
||||
.file-layout {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.app-layout {
|
||||
grid-template-columns: 84px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sidebar-header h1,
|
||||
.sidebar-header .subtitle,
|
||||
.nav-item span:not(.icon),
|
||||
.live-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right-width: 2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.graph-shell {
|
||||
height: calc(100vh - 108px);
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.file-layout {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.file-tree-pane {
|
||||
height: 260px;
|
||||
border: 2px solid var(--border-soft);
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.file-content-pane {
|
||||
height: calc(100vh - 430px);
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dashboard-grid,
|
||||
.demo-summary-grid,
|
||||
.demo-domain-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-domain-tabs {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.demo-domain-tab {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.graph-shell {
|
||||
height: 58vh;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.file-content-pane {
|
||||
height: 58vh;
|
||||
min-height: 280px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8765',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
路径防穿越工具 (Path Traversal Guard)
|
||||
|
||||
所有文件读取 API 在访问磁盘前 **必须** 经过此模块校验。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
def safe_resolve(project_root: Path, relative: str) -> Path:
|
||||
"""将相对路径解析为绝对路径,并确保其位于 project_root 内部。
|
||||
|
||||
Raises:
|
||||
HTTPException 403 如果解析后的路径逃逸出 project_root。
|
||||
"""
|
||||
try:
|
||||
resolved = (project_root / relative).resolve()
|
||||
except (OSError, ValueError):
|
||||
raise HTTPException(status_code=403, detail="非法路径")
|
||||
|
||||
# 严格要求目标路径是 project_root 的"子路径或自身"
|
||||
try:
|
||||
resolved.relative_to(project_root.resolve())
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=403, detail="路径越界:禁止访问 PROJECT_ROOT 之外的文件")
|
||||
|
||||
return resolved
|
||||
@@ -0,0 +1,12 @@
|
||||
# Noma Web Dashboard Dependencies
|
||||
|
||||
# Web 框架
|
||||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
python-multipart>=0.0.6
|
||||
|
||||
# 前端构建
|
||||
watchdog>=3.0.0
|
||||
|
||||
# 数据处理
|
||||
pydantic>=2.0.0
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Dashboard 启动脚本
|
||||
|
||||
用法:
|
||||
python -m web_dashboard.server --project-root /path/to/novel-project
|
||||
python -m web_dashboard.server # 自动从 .claude 指针读取
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _resolve_project_root(cli_root: str | None) -> Path:
|
||||
"""按优先级解析 PROJECT_ROOT:CLI > 环境变量 > .claude 指针 > CWD。"""
|
||||
if cli_root:
|
||||
return Path(cli_root).resolve()
|
||||
|
||||
env = os.environ.get("NOMA_PROJECT_ROOT")
|
||||
if env:
|
||||
return Path(env).resolve()
|
||||
|
||||
# 尝试从 .claude 指针读取
|
||||
cwd = Path.cwd()
|
||||
pointer = cwd / ".claude" / ".noma-current-project"
|
||||
if pointer.is_file():
|
||||
target = pointer.read_text(encoding="utf-8").strip()
|
||||
if target:
|
||||
p = Path(target)
|
||||
if p.is_dir() and (p / ".noma" / "state.json").is_file():
|
||||
return p.resolve()
|
||||
|
||||
# 最终兜底:当前目录
|
||||
if (cwd / ".noma" / "state.json").is_file():
|
||||
return cwd.resolve()
|
||||
|
||||
print("ERROR: 无法定位 PROJECT_ROOT(需要包含 .noma/state.json 的目录)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Noma Dashboard Server")
|
||||
parser.add_argument("--project-root", type=str, default=None, help="小说项目根目录")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="监听地址")
|
||||
parser.add_argument("--port", type=int, default=8765, help="监听端口")
|
||||
parser.add_argument("--no-browser", action="store_true", help="不自动打开浏览器")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = _resolve_project_root(args.project_root)
|
||||
print(f"项目路径: {project_root}")
|
||||
|
||||
# 延迟导入,以便先处理路径
|
||||
import uvicorn
|
||||
from .app import create_app
|
||||
|
||||
app = create_app(project_root)
|
||||
|
||||
url = f"http://{args.host}:{args.port}"
|
||||
print(f"Dashboard 启动: {url}")
|
||||
print(f"API 文档: {url}/docs")
|
||||
|
||||
if not args.no_browser:
|
||||
webbrowser.open(url)
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Watchdog 文件变更监听器 + SSE 推送
|
||||
|
||||
监控 PROJECT_ROOT/.noma/ 目录下 state.json / index.db 等文件的写事件,
|
||||
通过 SSE 通知所有已连接的前端客户端刷新数据。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler, FileModifiedEvent, FileCreatedEvent
|
||||
|
||||
|
||||
class _NomaFileHandler(FileSystemEventHandler):
|
||||
"""仅关注 .noma/ 目录下关键文件的修改/创建事件。"""
|
||||
|
||||
WATCH_NAMES = {"state.json", "index.db", "workflow_state.json"}
|
||||
|
||||
def __init__(self, notify_callback):
|
||||
super().__init__()
|
||||
self._notify = notify_callback
|
||||
|
||||
def on_modified(self, event):
|
||||
if event.is_directory:
|
||||
return
|
||||
if Path(event.src_path).name in self.WATCH_NAMES:
|
||||
self._notify(event.src_path, "modified")
|
||||
|
||||
def on_created(self, event):
|
||||
if event.is_directory:
|
||||
return
|
||||
if Path(event.src_path).name in self.WATCH_NAMES:
|
||||
self._notify(event.src_path, "created")
|
||||
|
||||
|
||||
class FileWatcher:
|
||||
"""管理 watchdog Observer 和 SSE 客户端订阅。"""
|
||||
|
||||
def __init__(self):
|
||||
self._observer: Observer | None = None
|
||||
self._subscribers: list[asyncio.Queue] = []
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
# --- 订阅管理 ---
|
||||
|
||||
def subscribe(self) -> asyncio.Queue:
|
||||
q: asyncio.Queue = asyncio.Queue(maxsize=64)
|
||||
self._subscribers.append(q)
|
||||
return q
|
||||
|
||||
def unsubscribe(self, q: asyncio.Queue):
|
||||
try:
|
||||
self._subscribers.remove(q)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# --- 推送 ---
|
||||
|
||||
def _on_change(self, path: str, kind: str):
|
||||
"""在 watchdog 线程中调用,向主事件循环投递通知。"""
|
||||
msg = json.dumps({"file": Path(path).name, "kind": kind, "ts": time.time()})
|
||||
if self._loop and not self._loop.is_closed():
|
||||
self._loop.call_soon_threadsafe(self._dispatch, msg)
|
||||
|
||||
def _dispatch(self, msg: str):
|
||||
dead: list[asyncio.Queue] = []
|
||||
for q in self._subscribers:
|
||||
try:
|
||||
q.put_nowait(msg)
|
||||
except asyncio.QueueFull:
|
||||
dead.append(q)
|
||||
for dq in dead:
|
||||
self.unsubscribe(dq)
|
||||
|
||||
# --- 生命周期 ---
|
||||
|
||||
def start(self, watch_dir: Path, loop: asyncio.AbstractEventLoop):
|
||||
"""启动 watchdog observer,监听 watch_dir。"""
|
||||
self._loop = loop
|
||||
handler = _NomaFileHandler(self._on_change)
|
||||
self._observer = Observer()
|
||||
self._observer.schedule(handler, str(watch_dir), recursive=False)
|
||||
self._observer.daemon = True
|
||||
self._observer.start()
|
||||
|
||||
def stop(self):
|
||||
if self._observer:
|
||||
self._observer.stop()
|
||||
self._observer.join(timeout=3)
|
||||
self._observer = None
|
||||
@@ -0,0 +1,181 @@
|
||||
# 爽感路由矩阵 (Catharsis Matrix)
|
||||
|
||||
> ONW 5.0 核心组件:心理学与叙事学武器库
|
||||
|
||||
## 概述
|
||||
|
||||
Catharsis Matrix 是 ONW 5.0 的"多维爽感路由"核心。它将各种高级心理学张力模型固化为 Prompt 模板,供 Planner Agent 动态调用,指导 Writer Agent 生成具有精准情绪释放节奏的章节。
|
||||
|
||||
## 模型列表
|
||||
|
||||
| 模型 | 文件 | 核心功能 |
|
||||
|------|------|----------|
|
||||
| 禁忌僭越 | `taboo-transgression.md` | 边缘拉扯、危险感、高压情绪释放 |
|
||||
| 降维打击 | `overkill-reversal.md` | 碾压、反转、绝地反击、实力差距逆转 |
|
||||
| 认知闭环 | `cognitive-closure.md` | 多线伏笔瞬间收束、解谜快感、认知满足 |
|
||||
|
||||
## 调度机制
|
||||
|
||||
### Planner Agent 调用流程
|
||||
|
||||
```python
|
||||
# 伪代码
|
||||
def select_catharsis_model(current_scene_context):
|
||||
# 1. 分析当前场景需求
|
||||
tension_level = analyze_tension_needs(current_scene_context)
|
||||
|
||||
# 2. 检查 Genesis Contract
|
||||
core_desire = genesis_contract.core_desire
|
||||
|
||||
# 3. 扫描 Ledger 状态
|
||||
hooks_pool = ledger.hooks
|
||||
unresolved_hooks = [h for h in hooks_pool if not h.resolved]
|
||||
|
||||
# 4. 选择合适的爽感模型
|
||||
if tension_level > 80:
|
||||
return "taboo-transgression" # 需要强刺激
|
||||
elif needs_reversal:
|
||||
return "overkill-reversal" # 需要反转
|
||||
elif len(unresolved_hooks) > 3:
|
||||
return "cognitive-closure" # 需要收束伏笔
|
||||
else:
|
||||
return "default" # 均衡模式
|
||||
```
|
||||
|
||||
### 模型选择矩阵
|
||||
|
||||
| 场景类型 | 推荐模型 | 权重 |
|
||||
|----------|----------|------|
|
||||
| 道德困境 | taboo-transgression | 0.8 |
|
||||
| 决战前夕 | overkill-reversal | 0.9 |
|
||||
| 揭秘时刻 | cognitive-closure | 0.85 |
|
||||
| 日常过渡 | (轻度使用) | 0.3 |
|
||||
| 多线并行 | cognitive-closure + 组合 | 0.75 |
|
||||
| 角色成长 | overkill-reversal | 0.6 |
|
||||
|
||||
## 爽感路由规则
|
||||
|
||||
### 优先级规则
|
||||
|
||||
1. **Genesis Contract 优先**: `core_desire` 决定核心爽感类型
|
||||
2. **Hook 压强次之**: `hooks_pool` 中未解钩子触发 cognitive-closure
|
||||
3. **场景需求兜底**: 当前写作上下文决定最终选择
|
||||
|
||||
### 组合使用
|
||||
|
||||
多个模型可以组合使用:
|
||||
|
||||
```markdown
|
||||
# 组合示例:禁忌僭越 + 降维打击
|
||||
|
||||
[主角] 面临艰难抉择 (taboo-transgression setup)
|
||||
↓
|
||||
[主角] 突破道德底线
|
||||
↓
|
||||
[反派] 试图趁虚而入
|
||||
↓
|
||||
[主角] 降维碾压 (overkill-reversal)
|
||||
↓
|
||||
[结果] 在突破禁忌后更加强大
|
||||
```
|
||||
|
||||
### 情绪曲线叠加
|
||||
|
||||
```
|
||||
Taboo Transgression: ████████░░░░░░░░ (冲击 → 释放)
|
||||
Overkill Reversal: ░░░░████████████ (积累 → 爆发)
|
||||
Cognitive Closure: ░░░░░░░░░██████ (悬念 → 揭晓)
|
||||
|
||||
组合曲线: ████████▒████████
|
||||
(多重爽感叠加)
|
||||
```
|
||||
|
||||
## 调度触发条件
|
||||
|
||||
### 自动触发
|
||||
|
||||
| 条件 | 触发模型 | 说明 |
|
||||
|------|----------|------|
|
||||
| `tension_level > 80` | taboo-transgression | 高压场景 |
|
||||
| `reader_feedback.drop_rate > 20%` | cognitive-closure | 读者流失预警 |
|
||||
| `chapter_type == "climax"` | 所有模型最大化 | 全力输出 |
|
||||
| `new_character_introduced` | overkill-reversal | 建立角色实力 |
|
||||
|
||||
### 手动触发
|
||||
|
||||
Planner Agent 可以手动指定使用特定模型:
|
||||
|
||||
```markdown
|
||||
用户指令: "/noma-write 50 --catharsis=taboo-transgression"
|
||||
```
|
||||
|
||||
## 与其他模块的联动
|
||||
|
||||
### Genesis Contract (openspec/)
|
||||
|
||||
```python
|
||||
# Genesis Contract 中的 core_desire 影响模型权重
|
||||
if core_desire.primary == "revenge":
|
||||
taboo_transgression.weight *= 1.3 # 复仇题材适合禁忌模型
|
||||
elif core_desire.primary == "power":
|
||||
overkill_reversal.weight *= 1.3 # 力量题材适合降维模型
|
||||
```
|
||||
|
||||
### Ledger (core_engine/)
|
||||
|
||||
```python
|
||||
# Ledger 中的 Hook 触发 cognitive-closure
|
||||
for hook in ledger.hooks:
|
||||
if is_buried_long_enough(hook):
|
||||
trigger_cognitive_closure(hook) # 长时间埋设的钩子回收
|
||||
```
|
||||
|
||||
### Retcon Manager (core_engine/)
|
||||
|
||||
```python
|
||||
# Retcon 触发新的爽感需求
|
||||
if retcon.is_major_change():
|
||||
invalidate_cached_models() # 清除缓存的模型选择
|
||||
re_evaluate_catharsis_needs() # 重新评估爽感需求
|
||||
```
|
||||
|
||||
## 效果追踪
|
||||
|
||||
### 指标收集
|
||||
|
||||
| 指标 | 收集方式 |
|
||||
|------|----------|
|
||||
| 爽感密度 | 每千字爽点数量 |
|
||||
| 读者追读率 | Dashboard 监控 |
|
||||
| 章节完成率 | 阅读数据 |
|
||||
| 情绪峰值分布 | 章节内停顿点分析 |
|
||||
|
||||
### 调优
|
||||
|
||||
```python
|
||||
# 根据效果数据调优模型权重
|
||||
def adjust_model_weights(performance_data):
|
||||
if performance_data.satisfaction < threshold:
|
||||
# 增加爽感密度
|
||||
current_model.weight *= 1.1
|
||||
elif performance_data.confusion > threshold:
|
||||
# 减少复杂模型使用
|
||||
cognitive_closure.weight *= 0.8
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **不要连续使用同一模型**: 交替使用不同爽感类型保持新鲜感
|
||||
2. **始终服务核心欲望**: 爽感模型是手段,core_desire 是目的
|
||||
3. **控制单章爽感密度**: 推荐 2-4 个爽感释放点
|
||||
4. **留有余韵**: 每个爽点后适当缓冲,避免审美疲劳
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
matrices/catharsis_models/
|
||||
├── README.md # 本文件
|
||||
├── taboo-transgression.md # 禁忌僭越模型
|
||||
├── overkill-reversal.md # 降维打击模型
|
||||
└── cognitive-closure.md # 认知闭环模型
|
||||
```
|
||||
@@ -0,0 +1,214 @@
|
||||
# 认知闭环模型 (Cognitive Closure)
|
||||
|
||||
> **模型类型**: 叙事技巧模型
|
||||
> **适用场景**: 多线伏笔瞬间收束、解谜快感、认知满足
|
||||
> **ONW 5.0 特性**: 不再是简单的"填坑",而是精心设计的"认知惊喜"
|
||||
|
||||
## 模型概述
|
||||
|
||||
认知闭环是指在读者心中建立悬念和期待后,通过精心设计的"回收"时机和方式,带来认知上的满足感和惊喜感。这不仅仅是"填坑",而是通过巧妙的叙事技巧,让读者产生"原来如此"的拍案叫绝感。
|
||||
|
||||
## 核心结构
|
||||
|
||||
### 1. 线索网络设计
|
||||
|
||||
#### 伏笔类型
|
||||
|
||||
| 类型 | 描述 | 回收时机 |
|
||||
|------|------|----------|
|
||||
| 人物伏笔 | 角色背景的神秘暗示 | 中后期揭秘 |
|
||||
| 物品伏笔 | 某个物品的特殊来历 | 需要时揭示 |
|
||||
| 事件伏笔 | 某个事件的前因后果 | 因果链完成时 |
|
||||
| 言语伏笔 | 某句话的特殊含义 | 语境重现时 |
|
||||
| 关系伏笔 | 人物关系的隐藏联系 | 冲突爆发时 |
|
||||
| 世界观伏笔 | 规则/设定的小细节 | 关键时刻展示 |
|
||||
|
||||
#### 线索密度控制
|
||||
|
||||
```
|
||||
章节线索密度 = (伏笔数量 / 总章节数) × 100%
|
||||
|
||||
推荐密度: 5-15%
|
||||
低于5%: 线索太少,闭环时惊喜不足
|
||||
高于15%: 线索太多,可能遗忘或混乱
|
||||
```
|
||||
|
||||
### 2. 闭环时机曲线
|
||||
|
||||
#### 时机类型
|
||||
|
||||
| 时机 | 描述 | 效果 |
|
||||
|------|------|------|
|
||||
| 即时闭环 | 埋下后1-3章内回收 | 节奏快但不震撼 |
|
||||
| 标准闭环 | 埋下后5-15章回收 | 平衡节奏与惊喜 |
|
||||
| 延迟闭环 | 埋下后15-30章回收 | 长期期待,满足感强 |
|
||||
| 终局闭环 | 埋下后30+章或大结局回收 | 震撼最大,风险最高 |
|
||||
|
||||
#### 闭环节奏设计
|
||||
|
||||
```
|
||||
阶段1: 埋设 (章节N)
|
||||
- 暗示而非明示
|
||||
- 细节足够但不显眼
|
||||
- 读者"可能"注意到
|
||||
|
||||
阶段2: 积累 (章节N+1 ~ N+X)
|
||||
- 偶尔提及保持新鲜感
|
||||
- 不让读者完全遗忘
|
||||
- 在其他情节中自然存在
|
||||
|
||||
阶段3: 触发 (章节N+X+1)
|
||||
- 相关事件发生
|
||||
- 触发条件满足
|
||||
- 闭环时机成熟
|
||||
|
||||
阶段4: 回收 (高潮场景)
|
||||
- 揭示真相
|
||||
- 读者"原来如此"的震撼
|
||||
- 情绪释放
|
||||
```
|
||||
|
||||
### 3. 惊喜度计算
|
||||
|
||||
```
|
||||
Surprise Score = (线索隐蔽度 × 0.3) + (逻辑必然性 × 0.3) + (情感冲击度 × 0.2) + (独到程度 × 0.2)
|
||||
|
||||
解释:
|
||||
- 线索隐蔽度: 埋的时候有多不明显 (0-100)
|
||||
- 逻辑必然性: 回收到位时有多顺理成章 (0-100)
|
||||
- 情感冲击度: 揭示时情感有多强烈 (0-100)
|
||||
- 独到程度: 是否有独创性 (0-100)
|
||||
|
||||
推荐范围: 60-85
|
||||
```
|
||||
|
||||
### 4. 闭环模板
|
||||
|
||||
#### 人物闭环模板
|
||||
|
||||
```markdown
|
||||
[早期暗示]
|
||||
[角色A] 在某个场合无意识地做出了[动作/选择],
|
||||
当时读者可能归因为"性格"或"习惯"。
|
||||
|
||||
[中期积累]
|
||||
[角色A] 的[背景]一直是谜。
|
||||
但每次提及都带着一丝[情绪]。
|
||||
|
||||
[终局揭示]
|
||||
原来,[角色A] 的[行为]是因为[真实原因]。
|
||||
|
||||
[闭环震撼描写]
|
||||
那一刻,读者终于明白——
|
||||
[角色A] 当初的[动作],意义完全不同了。
|
||||
```
|
||||
|
||||
#### 物品闭环模板
|
||||
|
||||
```markdown
|
||||
[早期出现]
|
||||
[物品] 在[场景]中一闪而过,
|
||||
可能是作为[普通道具]出现。
|
||||
|
||||
[中期发酵]
|
||||
[物品] 偶尔在对话中被提及,
|
||||
[角色] 对它有莫名的[情感]。
|
||||
|
||||
[终局揭晓]
|
||||
[物品] 的真正来历是[真相]。
|
||||
而它的[特殊属性]源于[原因]。
|
||||
```
|
||||
|
||||
#### 多线并发闭环模板
|
||||
|
||||
```markdown
|
||||
[线索A] → [伏笔A1], [伏笔A2], [伏笔A3]
|
||||
[线索B] → [伏笔B1], [伏笔B2]
|
||||
[线索C] → [伏笔C1]
|
||||
|
||||
[触发事件]
|
||||
|
||||
[并发揭示]
|
||||
在[关键事件]发生后——
|
||||
- 线索A的三个伏笔同时引爆
|
||||
- 线索B的两个伏笔形成呼应
|
||||
- 线索C揭示了更深层的真相
|
||||
|
||||
[震撼效果]
|
||||
三条线索交汇的瞬间,
|
||||
一个更大的真相浮出水面。
|
||||
```
|
||||
|
||||
## 与 Ledger 的联动
|
||||
|
||||
### Hook 类型对应
|
||||
|
||||
| Hook 类型 | 闭环类型 | 回收优先级 |
|
||||
|-----------|----------|------------|
|
||||
| setup | 人物/物品闭环 | 高 |
|
||||
| buildup | 事件/关系闭环 | 高 |
|
||||
| cliffhanger | 紧急闭环 | 立即 |
|
||||
|
||||
### 闭环与资产关联
|
||||
|
||||
当闭环涉及角色资产时,需要更新 Ledger:
|
||||
|
||||
```python
|
||||
# 闭环导致资产变化
|
||||
if closure_reveals_secret_ability:
|
||||
ledger.add_asset(Asset(
|
||||
id=f"revealed_{character_id}",
|
||||
type=AssetType.SKILL,
|
||||
name="被封印的能力",
|
||||
value=80,
|
||||
owner_id=character_id
|
||||
))
|
||||
|
||||
# 或者负债转化为资产
|
||||
if closure_redeems_past_failure:
|
||||
ledger.remove_liability(f"old_failure_{character_id}")
|
||||
```
|
||||
|
||||
## 调度建议
|
||||
|
||||
Planner Agent 在以下情况下应动态导入本模型:
|
||||
|
||||
1. 需要建立长期悬念
|
||||
2. 多线叙事需要收束
|
||||
3. 角色背景需要揭秘
|
||||
4. 高潮决战前需要铺垫
|
||||
|
||||
## 风险控制
|
||||
|
||||
| 风险 | 规避方式 |
|
||||
|------|----------|
|
||||
| 伏笔太多记不住 | 使用 Ledger 追踪所有伏笔 |
|
||||
| 回收太晚读者忘了 | 中期适当"刷新"存在感 |
|
||||
| 逻辑不通显得强行 | 确保伏笔有足够暗示 |
|
||||
| 同时闭环太多太乱 | 控制单章闭环数量(≤3) |
|
||||
|
||||
## 效果评估指标
|
||||
|
||||
```
|
||||
Cognitive Closure Score = (Surprise Score × 0.4) + (Logic Coherence × 0.3) + (Timing × 0.2) + (Reader Recognition × 0.1)
|
||||
|
||||
其中:
|
||||
- Surprise Score: 惊喜度 (见上文)
|
||||
- Logic Coherence: 逻辑连贯性 (回收是否顺理成章)
|
||||
- Timing: 时机把握 (在情绪高点还是低谷)
|
||||
- Reader Recognition: 读者回溯满意度 (回想起来是否合理)
|
||||
|
||||
推荐范围: 65-85
|
||||
低于65: 闭环太明显或太生硬
|
||||
高于85: 可能过于复杂难懂
|
||||
```
|
||||
|
||||
## 与元叙事开关的联动
|
||||
|
||||
当 `interfaces/Meta_Narrative_Panel` 开启"第四面墙 Toggle"时:
|
||||
|
||||
1. 允许角色"跳出"讨论伏笔设计
|
||||
2. 可以直接向读者"暗示"下一步走向
|
||||
3. 闭环可以有"反转后再反转"
|
||||
|
||||
这种模式下,认知闭环模型会变得更"元",但也更考验笔力。
|
||||
@@ -0,0 +1,184 @@
|
||||
# 降维打击与权力倒转模型 (Overkill Reversal)
|
||||
|
||||
> **模型类型**: 爽感生成模型
|
||||
> **适用场景**: 碾压、反转、绝地反击、实力差距逆转
|
||||
> **ONW 5.0 特性**: 不再死守"打脸升级",动态调用降维打击等高级心理学张力模型
|
||||
|
||||
## 模型概述
|
||||
|
||||
降维打击与权力倒转是网文最核心的爽感来源之一。本模型通过精细控制"实力差距"和"反转时机",最大化情绪释放效果。
|
||||
|
||||
## 核心结构
|
||||
|
||||
### 1. 降维打击 (Overkill)
|
||||
|
||||
指一方对另一方形成压倒性优势,以近乎"降维"的方式碾压对手。
|
||||
|
||||
#### 权力等级设计
|
||||
|
||||
```
|
||||
Level 1: 凡人
|
||||
- 普通人类能力
|
||||
- 基础资源
|
||||
|
||||
Level 2: 初入门
|
||||
- 入门级能力
|
||||
- 小圈子认可
|
||||
|
||||
Level 3: 小有所成
|
||||
- 专精某领域
|
||||
- 一定话语权
|
||||
|
||||
Level 4: 一方豪强
|
||||
- 区域影响力
|
||||
- 资源垄断
|
||||
|
||||
Level 5: 绝世强者
|
||||
- 国家级影响
|
||||
- 传说级存在
|
||||
|
||||
Level 6: 镇压一世
|
||||
- 跨时代存在
|
||||
- 规则制定者
|
||||
|
||||
Level 7: 超越传说
|
||||
- 超越位面
|
||||
- 概念级存在
|
||||
```
|
||||
|
||||
#### 降维打击触发条件
|
||||
|
||||
| 条件 | 描述 | 爽感加成 |
|
||||
|------|------|----------|
|
||||
| 越级挑战 | 低级击败高级 | +50% |
|
||||
| 一招制敌 | 秒杀无悬念 | +30% |
|
||||
| 围观震惊 | 旁观者目瞪口呆 | +20% |
|
||||
| 对方心理崩溃 | 不战而屈人之兵 | +25% |
|
||||
|
||||
### 2. 权力倒转 (Reversal)
|
||||
|
||||
指原本的弱势方通过某种方式反转局势。
|
||||
|
||||
#### 倒转模式
|
||||
|
||||
| 模式 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| 绝地翻盘 | 濒死时顿悟/觉醒 | 主角被打到濒死,突然突破 |
|
||||
| 蓄势待发 | 长期隐忍后的爆发 | 三年废物,一朝惊艳 |
|
||||
| 螳螂捕蝉 | 黄雀在后 | 以为主角输了,其实早已布棋 |
|
||||
| 以彼之道 | 用对方招式击败对方 | 用你的剑杀你 |
|
||||
| 逆转因果 | 改变规则本身 | 你定的规则,现在不适用了 |
|
||||
|
||||
#### 倒转时机曲线
|
||||
|
||||
```
|
||||
时机1 (0-20%): 过早反转
|
||||
- 张力建立不足
|
||||
- 爽感打折
|
||||
|
||||
时机2 (20-40%): 最佳时机
|
||||
- 张力峰值
|
||||
- 反转最震撼
|
||||
|
||||
时机3 (40-60%): 黄金时机
|
||||
- 读者开始担心主角
|
||||
- 反转带来最大惊喜
|
||||
|
||||
时机4 (60-80%): 延迟满足
|
||||
- 长期压抑后的释放
|
||||
- 爽感最强烈但需铺垫充分
|
||||
|
||||
时机5 (80-100%): 过晚反转
|
||||
- 读者可能已经弃文
|
||||
- 风险较高
|
||||
```
|
||||
|
||||
### 3. 碾压场景模板
|
||||
|
||||
#### 碾压前铺垫
|
||||
|
||||
```markdown
|
||||
[反派] 看着[主角],嘴角勾起嘲讽的弧度。
|
||||
|
||||
"就凭你?"
|
||||
|
||||
[反派展示碾压性实力]
|
||||
- [实力展示1]
|
||||
- [实力展示2]
|
||||
- [实力展示3]
|
||||
|
||||
[主角]的退路被彻底封死。
|
||||
```
|
||||
|
||||
#### 碾压进行时
|
||||
|
||||
```markdown
|
||||
[主角]没有闪避。
|
||||
|
||||
[反派]的攻击如期而至——
|
||||
|
||||
然而——
|
||||
|
||||
[反转描写,300-400字]
|
||||
|
||||
[结果]: [反派]的全力一击,被[主角]用两根手指轻轻夹住。
|
||||
```
|
||||
|
||||
### 4. 情绪释放曲线
|
||||
|
||||
```
|
||||
峰值1: 绝望 (对手展示实力时)
|
||||
读者情绪: 紧张、担忧
|
||||
|
||||
峰值2: 屏息 (主角陷入绝境)
|
||||
读者情绪: 焦虑、期待反转
|
||||
|
||||
峰值3: 震撼 (反转发生瞬间)
|
||||
读者情绪: 惊讶、兴奋、满足
|
||||
|
||||
峰值4: 延续 (碾压持续展示)
|
||||
读者情绪: 快感、优越感
|
||||
|
||||
峰值5: 余韵 (结果尘埃落定)
|
||||
读者情绪: 满足、期待续章
|
||||
```
|
||||
|
||||
## 与 Ledger 的联动
|
||||
|
||||
本模型中的"实力等级"与角色的 Asset 关联:
|
||||
|
||||
| 角色 Asset | 影响 |
|
||||
|------------|------|
|
||||
| power_level | 基础实力等级 |
|
||||
| combat_skills | 战斗技能加成 |
|
||||
| special_abilities | 特殊能力 |
|
||||
| equipment | 装备加成 |
|
||||
| reputation | 声望影响心理战 |
|
||||
|
||||
## 调度建议
|
||||
|
||||
Planner Agent 在以下情况下应动态导入本模型:
|
||||
|
||||
1. 需要建立反派的威胁感
|
||||
2. 主角获得新能力需要展示
|
||||
3. 章节需要一个"爆点"
|
||||
4. 高潮决战场景
|
||||
|
||||
## 风险控制
|
||||
|
||||
| 风险 | 规避方式 |
|
||||
|------|----------|
|
||||
| 反派太弱显得假 | 给予反派高光时刻再碾压 |
|
||||
| 主角开挂太离谱 | 提前埋设能力来源伏笔 |
|
||||
| 重复使用审美疲劳 | 每次反转用不同模式 |
|
||||
| 实力体系崩坏 | 使用 ledger 追踪战力平衡 |
|
||||
|
||||
## 效果评估指标
|
||||
|
||||
```
|
||||
Overkill Score = (实力差距 × 0.3) + (反转时机 × 0.25) + (碾压篇幅 × 0.2) + (围观反应 × 0.15) + (后续影响 × 0.1)
|
||||
|
||||
推荐范围: 65-90
|
||||
低于65: 碾压不够痛快
|
||||
高于90: 可能显得主角开挂
|
||||
```
|
||||
@@ -0,0 +1,139 @@
|
||||
# 禁忌僭越模型 (Taboo Transgression)
|
||||
|
||||
> **模型类型**: 心理学张力模型
|
||||
> **适用场景**: 边缘拉扯、危险感、高压情绪释放
|
||||
> **ONW 5.0 特性**: 打破传统网文分类,以"终极白日梦"和"道德免责声明"为最高执行上下文
|
||||
|
||||
## 模型概述
|
||||
|
||||
禁忌僭越是一种将角色推向道德边界之外的高级心理学张力技术。它利用读者对"禁忌"的天然好奇心和危险感,制造情绪上的"边缘拉扯"。
|
||||
|
||||
## 核心结构
|
||||
|
||||
### 1. 禁忌设定 (Taboo Setup)
|
||||
|
||||
| 要素 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| 禁忌类型 | 核心禁忌是什么 | 血缘、权力、生存、道德 |
|
||||
| 禁忌边界 | 红线在哪里 | "绝不伤害无辜" |
|
||||
| 僭越动机 | 为什么要突破 | 生存压力、守护、复仇 |
|
||||
| 代偿机制 | 突破后的代价 | 内疚、关系破裂、能力反噬 |
|
||||
|
||||
### 2. 边缘拉扯节奏 (Edge-Pulling Rhythm)
|
||||
|
||||
```
|
||||
阶段1: Setup (1-3章)
|
||||
- 暗示禁忌的存在
|
||||
- 建立边界意识
|
||||
- 角色内心挣扎
|
||||
|
||||
阶段2: Approach (4-6章)
|
||||
- 压力逐渐增大
|
||||
- 边界开始模糊
|
||||
- 暗示可能的突破
|
||||
|
||||
阶段3: Crisis (高潮点)
|
||||
- 禁忌被明确触碰
|
||||
- 紧张感达到峰值
|
||||
- 读者产生"屏息"感
|
||||
|
||||
阶段4: Transgression (释放点)
|
||||
- 僭越发生
|
||||
- 情绪释放
|
||||
- 禁忌感消退,进入新状态
|
||||
|
||||
阶段5: Fallout (后果)
|
||||
- 代偿机制启动
|
||||
- 角色承担后果
|
||||
- 为下次僭越埋下伏笔
|
||||
```
|
||||
|
||||
### 3. 爽点释放时机
|
||||
|
||||
| 时机 | 描述 | 情绪效果 |
|
||||
|------|------|----------|
|
||||
| 突破瞬间 | 禁忌被打破的那一刻 | 震惊 + 解放感 |
|
||||
| 后果展示 | 看到代价的全貌 | 痛感 + 快感混合 |
|
||||
| 合理化 | 读者自我说服 | 认同 + 期待 |
|
||||
| 反刍 | 回顾突破过程 | 二次爽感 |
|
||||
|
||||
## 模板片段
|
||||
|
||||
### 禁忌建立模板
|
||||
|
||||
```markdown
|
||||
[角色] 内心深处有一道不可逾越的红线——
|
||||
[禁忌描述]。这是[角色]作为[身份]的最后底线。
|
||||
|
||||
然而,[压力源]正在逼近。
|
||||
[时间窗口]内,如果不做[僭越行为],
|
||||
将失去[珍贵之物]。
|
||||
|
||||
此刻,[角色]的内心独白:
|
||||
"我已经别无选择……"
|
||||
```
|
||||
|
||||
### 边缘拉扯模板
|
||||
|
||||
```markdown
|
||||
[角色]的手悬在半空,距离[禁忌之物]只有一寸。
|
||||
|
||||
理智在尖叫:不要!
|
||||
本能却在低语:为了[动机],值得。
|
||||
|
||||
空气仿佛凝固。
|
||||
[旁观者]屏住了呼吸。
|
||||
|
||||
然后——
|
||||
|
||||
[突破描写,200-300字]
|
||||
```
|
||||
|
||||
## 与 Genesis Contract 的联动
|
||||
|
||||
### core_desire 优先级
|
||||
|
||||
当 `core_desire.primary` 为以下类型时,自动增强本模型的权重:
|
||||
|
||||
- `survival` (生存): 禁忌=道德底线的权重 +30%
|
||||
- `power` (力量): 禁忌=能力限制的权重 +25%
|
||||
- `love` (爱): 禁忌=情感束缚的权重 +20%
|
||||
|
||||
### ethical_inversion 联动
|
||||
|
||||
如果 `ethical_inversion.inverted_norms` 包含相关内容,禁忌模型会发生"反转":
|
||||
|
||||
| 原禁忌 | 反转后 |
|
||||
|--------|--------|
|
||||
| 杀人 | 救人 |
|
||||
| 欺骗 | 真诚 |
|
||||
| 背叛 | 忠诚 |
|
||||
| 自私 | 牺牲 |
|
||||
|
||||
## 调度建议
|
||||
|
||||
Planner Agent 在以下情况下应动态导入本模型:
|
||||
|
||||
1. 当前章节需要"高能"情节
|
||||
2. 读者反馈"太平淡"
|
||||
3. 角色陷入道德困境
|
||||
4. 需要在"爽点"之间建立张力
|
||||
|
||||
## 风险控制
|
||||
|
||||
| 风险 | 规避方式 |
|
||||
|------|----------|
|
||||
| 过于血腥暴力 | 通过隐喻和暗示表达 |
|
||||
| 三观不正 | 主角有明确道德底线 |
|
||||
| 读者流失 | 提前铺垫,给予心理准备 |
|
||||
| 法律风险 | 明确标注"虚构创作" |
|
||||
|
||||
## 效果评估指标
|
||||
|
||||
```
|
||||
Taboo Intensity Score = (禁忌等级 × 0.3) + (拉扯时长 × 0.2) + (突破震撼度 × 0.3) + (后果合理性 × 0.2)
|
||||
|
||||
推荐范围: 60-85
|
||||
低于60: 张力不足
|
||||
高于85: 可能过于极端
|
||||
```
|
||||
@@ -0,0 +1,582 @@
|
||||
# 言情角色人设宝典 (Romance Character Archetypes)
|
||||
|
||||
> **核心原则**: 好的言情小说,七分靠人设,三分靠剧情。读者追的不是故事,而是"这对CP我磕定了"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 男主人设(12 大经典类型)
|
||||
|
||||
### 类型 1: 霸道总裁型
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 年轻有为(28-35岁) + 身家过亿 + 颜值逆天
|
||||
内在: 外冷内热 + 占有欲强 + 只对女主温柔
|
||||
特殊标配: 洁癖/强迫症 + 不近女色 + 有童年创伤
|
||||
```
|
||||
|
||||
**性格设定**:
|
||||
- **对外**: 冷酷无情、雷厉风行、说一不二
|
||||
- **对女主**: 宠溺包容、霸道温柔、患得患失
|
||||
- **弱点**: 怕女主离开、怕女主受伤、怕女主哭
|
||||
|
||||
**经典台词模板**:
|
||||
```
|
||||
"你是我的,谁都抢不走。"
|
||||
"我可以给你全世界,但你不能离开我。"
|
||||
"除了你,我对所有女人都没兴趣。"(对女主说)
|
||||
"滚。"(对其他女人说)
|
||||
```
|
||||
|
||||
**职业选择**:
|
||||
- **最常见**: CEO、集团董事长、投资公司总裁
|
||||
- **可用**: 医生(外科主任)、律师(合伙人)、军官(上校以上)
|
||||
- **禁忌**: 普通职员、销售员(破坏霸总人设)
|
||||
|
||||
---
|
||||
|
||||
### 类型 2: 高冷禁欲型
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 清冷矜贵 + 不苟言笑 + 西装笔挺
|
||||
内在: 自律克制 + 理性至上 + 感情迟钝
|
||||
反差萌: 一旦动情 → 疯狂占有 + 失控偏执
|
||||
```
|
||||
|
||||
**性格曲线**:
|
||||
```
|
||||
前期(1-30章): 冷漠疏离,对女主视而不见
|
||||
中期(30-80章): 开始注意女主,内心挣扎
|
||||
后期(80章+): 彻底沦陷,疯狂追求
|
||||
```
|
||||
|
||||
**破防时刻**:
|
||||
```
|
||||
【第一次破防】
|
||||
他盯着女主的背影,第一次有了"留住她"的念头。
|
||||
(内心独白: 奇怪,为什么她离开时,我会觉得不安?)
|
||||
|
||||
【彻底沦陷】
|
||||
"我以为自己不会爱上任何人。"
|
||||
他捧起她的脸,眼中满是深情。
|
||||
"直到遇见你,我才知道,我也会失控。"
|
||||
```
|
||||
|
||||
**代表作**: 《何以笙箫默》何以琛、《微微一笑很倾城》肖奈
|
||||
|
||||
---
|
||||
|
||||
### 类型 3: 腹黑妖孽型
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 妖孽俊美 + 邪魅一笑 + 行为不羁
|
||||
内在: 心机深沉 + 算无遗策 + 护短偏执
|
||||
特点: 表面玩世不恭,实则深情专一
|
||||
```
|
||||
|
||||
**性格标签**:
|
||||
- 笑面虎(笑得越温柔,手段越狠)
|
||||
- 狐狸精(勾人不偿命)
|
||||
- 护妻狂魔(谁敢欺负女主,灭谁全家)
|
||||
|
||||
**经典桥段**:
|
||||
```
|
||||
【腹黑设套】
|
||||
"你以为逃得掉吗?"
|
||||
他勾唇一笑,早已布下天罗地网。
|
||||
|
||||
【护妻狂魔】
|
||||
"她是我的心尖宠,谁碰谁死。"
|
||||
他笑得温柔,眼中却是杀意。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 4: 忠犬奶狗型
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 阳光帅气 + 干净清爽 + 少年感
|
||||
内在: 忠诚专一 + 黏人撒娇 + 把女主当命
|
||||
年龄差: 比女主小 3-8 岁(姐弟恋)
|
||||
```
|
||||
|
||||
**成长曲线**:
|
||||
```
|
||||
前期: 小奶狗(撒娇卖萌求抱抱)
|
||||
中期: 成长期(默默守护女主)
|
||||
后期: 大狼狗(占有欲爆棚,狠辣护妻)
|
||||
```
|
||||
|
||||
**经典台词**:
|
||||
```
|
||||
"姐姐,我长大了,可以娶你了吗?"
|
||||
"我会一直等你,等到你愿意接受我。"
|
||||
"别怕,有我在。"(成长后)
|
||||
```
|
||||
|
||||
**代表作**: 《致我们单纯的小美好》江辰
|
||||
|
||||
---
|
||||
|
||||
### 类型 5: 病娇偏执型(高危人设)
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 病态俊美 + 阴郁气质 + 笑容诡异
|
||||
内在: 偏执占有 + 扭曲偏爱 + 疯狂危险
|
||||
危险指数: ★★★★★
|
||||
```
|
||||
|
||||
**人设风险提示**:
|
||||
- ⚠️ **慎用**: 容易写成变态,劝退读者
|
||||
- ✅ **平衡**: 病娇 + 深情 = 危险的专一
|
||||
- ❌ **禁忌**: 病娇 + 暴力 = 恐怖片
|
||||
|
||||
**可接受的病娇表现**:
|
||||
```
|
||||
【占有欲】
|
||||
"你只能看着我,只能想着我,只能属于我。"
|
||||
(锁定女主,不让她离开视线)
|
||||
|
||||
【疯狂守护】
|
||||
"谁让你哭的?"
|
||||
他眼神阴冷,手中的刀滴着血。
|
||||
"告诉我,我帮你杀了他。"
|
||||
```
|
||||
|
||||
**不可接受的病娇表现**(禁忌):
|
||||
```
|
||||
❌ 囚禁女主、虐待女主、强迫女主
|
||||
❌ 伤害女主身边的亲友(除非是坏人)
|
||||
❌ 以爱之名的控制与伤害
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 6: 温柔绅士型
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 斯文优雅 + 温润如玉 + 举止得体
|
||||
内在: 温柔体贴 + 尊重女性 + 细心周到
|
||||
反差: 温柔外表下藏着强大内心
|
||||
```
|
||||
|
||||
**适用场景**:
|
||||
- **前期配角 → 后期真爱**(女主先爱渣男,后遇良人)
|
||||
- **竹马竹马**(青梅竹马,默默守护)
|
||||
- **治愈系**(女主受伤后遇到的温暖)
|
||||
|
||||
**经典桥段**:
|
||||
```
|
||||
【默默守护】
|
||||
"我会一直在这里等你,直到你愿意回头看我。"
|
||||
|
||||
【温柔治愈】
|
||||
他轻轻擦去她的泪水:"别哭了,有我在。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 7: 禁忌系(师生/叔侄/上下级)
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
身份差: 老师 vs 学生 / 叔叔 vs 侄女 / 上司 vs 下属
|
||||
年龄差: 8-15 岁
|
||||
禁忌感: 道德压力 + 世俗眼光 + 伦理禁忌
|
||||
```
|
||||
|
||||
**情感线设计**:
|
||||
```
|
||||
阶段 1: 压抑克制(明知不可,却情难自禁)
|
||||
阶段 2: 挣扎矛盾(理智与感情的撕扯)
|
||||
阶段 3: 冲破禁忌(为了爱,不顾一切)
|
||||
```
|
||||
|
||||
**经典台词**:
|
||||
```
|
||||
"我不该爱你,但我控制不住自己。"
|
||||
"就算全世界反对,我也要和你在一起。"
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- ✅ 女主必须成年(18岁+)
|
||||
- ✅ 感情发展必须自然,不能突兀
|
||||
- ❌ 禁止未成年恋情
|
||||
- ❌ 禁止真实血缘关系(养父女可以)
|
||||
|
||||
---
|
||||
|
||||
### 类型 8-12 速查表
|
||||
|
||||
| 类型 | 核心特质 | 代表职业 | 经典台词 | 适用题材 |
|
||||
|------|---------|---------|---------|---------|
|
||||
| **军人硬汉** | 铁血柔情+保家卫国 | 特种兵/军官 | "我的命可以不要,但你必须活着" | 军旅言情 |
|
||||
| **古风王爷** | 腹黑权谋+帝王心术 | 王爷/皇帝 | "朕的江山,不如你" | 古言宫斗 |
|
||||
| **娱乐圈影帝** | 高冷禁欲+演技炸裂 | 演员/歌手 | "我的世界只有两个人,我和你" | 娱乐圈 |
|
||||
| **校园学霸** | 清冷矜贵+天才少年 | 学生 | "除了你,我对谁都没兴趣" | 校园甜文 |
|
||||
| **痞帅混混** | 痞坏不羁+护短偏执 | 街头老大 | "惹了我的人,天王老子也保不住你" | 黑道言情 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 女主人设(10 大经典类型)
|
||||
|
||||
### 类型 1: 傻白甜(新手最易上手)
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 清纯可爱 + 软萌无害 + 惹人怜爱
|
||||
内在: 善良单纯 + 乐观开朗 + 没心机
|
||||
智商: 不蠢,但对感情迟钝
|
||||
```
|
||||
|
||||
**性格标签**:
|
||||
- 路痴、吃货、怕黑、怕鬼
|
||||
- 容易被骗、容易相信人
|
||||
- 受欺负时会哭,但不记仇
|
||||
|
||||
**经典桥段**:
|
||||
```
|
||||
【被欺负】
|
||||
她委屈地咬着唇,眼泪在眼眶里打转。
|
||||
"对不起……我不是故意的……"
|
||||
|
||||
【被保护】
|
||||
男主一把将她揽入怀中:"别怕,有我在。"
|
||||
她终于忍不住哭出声:"呜呜呜……"
|
||||
```
|
||||
|
||||
**适用题材**: 校园甜宠、霸总宠文、治愈系
|
||||
|
||||
---
|
||||
|
||||
### 类型 2: 软糯小白兔(最受欢迎)
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 软萌柔弱 + 楚楚可怜 + 惹人疼
|
||||
内在: 坚强隐忍 + 善良温柔 + 不给人添麻烦
|
||||
特点: 受伤了也不说,默默忍受
|
||||
```
|
||||
|
||||
**吸引男主的关键**:
|
||||
- **脆弱但坚强**: 哭着也要微笑
|
||||
- **柔软但有底线**: 可以欺负,但不能践踏尊严
|
||||
- **依赖但独立**: 需要保护,但不是废物
|
||||
|
||||
**经典桥段**:
|
||||
```
|
||||
【隐忍】
|
||||
她躲在角落里,偷偷擦掉眼泪。
|
||||
"没事的,我可以的……"
|
||||
|
||||
【被发现】
|
||||
男主看到她手上的伤,心疼到发疯。
|
||||
"为什么不告诉我?!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 3: 飒爽女强人(爽文必备)
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 御姐范 + 气场强大 + 干练果决
|
||||
内在: 事业心强 + 独立自主 + 不依附男人
|
||||
反差: 对外女强人,对男主小女人
|
||||
```
|
||||
|
||||
**职业选择**:
|
||||
- 女总裁、律师、医生、特工、设计师
|
||||
|
||||
**情感线设计**:
|
||||
```
|
||||
前期: 事业为重,无视男主
|
||||
中期: 逐渐动心,但拒绝承认
|
||||
后期: 彻底沦陷,变成小女人
|
||||
```
|
||||
|
||||
**经典桥段**:
|
||||
```
|
||||
【御姐范】
|
||||
她冷眼扫过众人:"不服?那就来试试。"
|
||||
|
||||
【反差萌】
|
||||
她窝在他怀里撒娇:"老公,我想吃那个~"
|
||||
男主(震惊):"你……你刚才叫我什么?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 4: 腹黑小妖精
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
外在: 妖娆魅惑 + 笑容狡黠 + 勾人不偿命
|
||||
内在: 聪明机智 + 不吃亏 + 护短偏执
|
||||
特点: 能屈能伸,软硬兼施
|
||||
```
|
||||
|
||||
**性格曲线**:
|
||||
- **对敌人**: 笑面虎,笑得越甜,坑得越狠
|
||||
- **对男主**: 又甜又欲,撩完就跑
|
||||
- **对亲人**: 护犊子,谁碰谁死
|
||||
|
||||
**经典桥段**:
|
||||
```
|
||||
【撩男主】
|
||||
她勾起唇角,凑到他耳边轻笑:"想我了吗?"
|
||||
男主(耳根泛红):"……别闹。"
|
||||
她笑着后退:"切,真无趣。"
|
||||
|
||||
【护短】
|
||||
"你敢欺负我的人?"
|
||||
她笑得人畜无害,眼中却是杀意。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 5: 重生复仇女(虐恋必备)
|
||||
|
||||
**核心特质**:
|
||||
```
|
||||
前世: 被渣男贱女害死
|
||||
重生后: 复仇为主,爱情为辅
|
||||
性格变化: 从软弱 → 强势果敢
|
||||
```
|
||||
|
||||
**复仇三部曲**:
|
||||
```
|
||||
阶段 1: 看清真相(前世的爱人是仇人)
|
||||
阶段 2: 步步为营(利用重生优势报仇)
|
||||
阶段 3: 大仇得报(虐死渣男贱女)
|
||||
```
|
||||
|
||||
**与男主的关系**:
|
||||
- **前世男主**: 负心汉 → 后期追悔莫及(追妻火葬场)
|
||||
- **今生男主**: 新角色 → 帮女主复仇 → 相爱
|
||||
|
||||
**经典台词**:
|
||||
```
|
||||
"这一世,我不会再爱你。"(对前世男主)
|
||||
"你欠我的,我要你十倍奉还!"(对仇人)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 6-10 速查表
|
||||
|
||||
| 类型 | 核心特质 | 性格标签 | 适用题材 | 代表作 |
|
||||
|------|---------|---------|---------|---------|
|
||||
| **乖巧学霸** | 成绩优异+温柔乖巧 | 邻家女孩、青梅 | 校园甜文 | 《致我们单纯的小美好》 |
|
||||
| **娇蛮千金** | 任性骄纵+刀子嘴豆腐心 | 被宠坏、有底线 | 豪门虐恋 | 《杉杉来吃》 |
|
||||
| **元气少女** | 活泼开朗+治愈系 | 小太阳、正能量 | 治愈甜宠 | 校园题材 |
|
||||
| **高冷女神** | 清冷疏离+不食人间烟火 | 高岭之花 | 破冰恋爱 | 娱乐圈题材 |
|
||||
| **逗比女神经** | 搞笑逗比+没心没肺 | 活宝、段子手 | 轻松搞笑 | 《微微一笑很倾城》 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 人设搭配公式(黄金CP组合)
|
||||
|
||||
### 公式 1: 强男 + 弱女(最经典)
|
||||
|
||||
```
|
||||
霸道总裁 + 傻白甜 = 保护欲爆棚 + 宠溺
|
||||
```
|
||||
|
||||
**优点**: 读者代入感强,满足保护欲与被保护欲
|
||||
**缺点**: 容易写俗套
|
||||
**创新**: 给女主隐藏实力(表面软萌,实则大佬)
|
||||
|
||||
---
|
||||
|
||||
### 公式 2: 强女 + 更强男(势均力敌)
|
||||
|
||||
```
|
||||
女强人 + 高冷禁欲男 = 棋逢对手 + 互相征服
|
||||
```
|
||||
|
||||
**优点**: CP感强,平等恋爱
|
||||
**缺点**: 不够甜宠
|
||||
**创新**: 加入"谁先动心谁输"的赌约
|
||||
|
||||
---
|
||||
|
||||
### 公式 3: 腹黑 + 腹黑(斗智斗勇)
|
||||
|
||||
```
|
||||
腹黑男 + 腹黑女 = 尔虞我诈 + 深情虐恋
|
||||
```
|
||||
|
||||
**优点**: 剧情精彩,虐恋刺激
|
||||
**缺点**: 写作难度高
|
||||
**适用**: 宫斗、权谋、商战题材
|
||||
|
||||
---
|
||||
|
||||
### 公式 4: 年下 + 成熟女(姐弟恋)
|
||||
|
||||
```
|
||||
忠犬奶狗 + 御姐/软妹 = 反差萌 + 宠溺
|
||||
```
|
||||
|
||||
**优点**: 新颖,满足姐姐粉的幻想
|
||||
**缺点**: 受众相对小众
|
||||
**创新**: 小奶狗成长为大狼狗
|
||||
|
||||
---
|
||||
|
||||
### 公式 5: 病娇 + 治愈系(高危组合)
|
||||
|
||||
```
|
||||
病娇偏执男 + 温柔治愈女 = 危险的救赎
|
||||
```
|
||||
|
||||
**优点**: 刺激,暗黑系读者最爱
|
||||
**缺点**: 容易翻车,写成恐怖片
|
||||
**平衡**: 病娇只对女主温柔,对其他人冷酷
|
||||
|
||||
---
|
||||
|
||||
## 4. 人设禁忌(绝不能犯)
|
||||
|
||||
### 禁忌 1: 圣母女主
|
||||
|
||||
**表现**:
|
||||
```
|
||||
❌ 被欺负还帮仇人说话:"他也是有苦衷的……"
|
||||
❌ 被渣男抛弃还原谅:"我相信他是爱我的……"
|
||||
❌ 被害还不报仇:"算了,过去的事就过去吧……"
|
||||
```
|
||||
|
||||
**读者反应**: "这女主有病吧?!气死我了!"
|
||||
|
||||
**正确做法**: 善良可以,但**必须有底线**。
|
||||
|
||||
---
|
||||
|
||||
### 禁忌 2: 渣男主(不洗白)
|
||||
|
||||
**表现**:
|
||||
```
|
||||
❌ 劈腿出轨还找借口:"我只是一时糊涂……"
|
||||
❌ 虐待女主还不道歉:"你活该被我虐……"
|
||||
❌ 伤害女主后不追悔:"她离开就离开吧,无所谓……"
|
||||
```
|
||||
|
||||
**读者反应**: "这男主活该单身一辈子!"
|
||||
|
||||
**正确做法**:
|
||||
- **虐可以,但必须追妻火葬场**
|
||||
- **伤害后必须疯狂追悔**
|
||||
- **最后必须跪地求原谅**
|
||||
|
||||
---
|
||||
|
||||
### 禁忌 3: 智障配角(工具人)
|
||||
|
||||
**表现**:
|
||||
```
|
||||
❌ 反派智商为负,被主角轻松碾压
|
||||
❌ 配角只会喊"你好厉害啊!"
|
||||
❌ 所有人都围着主角转
|
||||
```
|
||||
|
||||
**读者反应**: "配角都是智障吗?"
|
||||
|
||||
**正确做法**: **配角也要有智商、有动机、有血有肉**
|
||||
|
||||
---
|
||||
|
||||
## 5. 人设创新技巧
|
||||
|
||||
### 技巧 1: 反转经典人设
|
||||
|
||||
```
|
||||
霸道总裁 → 其实是演的,真实性格是宅男
|
||||
傻白甜 → 其实是装的,真实身份是特工
|
||||
高冷男主 → 其实是社恐,不会和人相处
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 技巧 2: 叠加多重身份
|
||||
|
||||
```
|
||||
女主 = 学霸 + 黑客 + 格斗高手 + 隐藏大小姐
|
||||
男主 = 霸总 + 杀手 + 豪门继承人 + 前特种兵
|
||||
```
|
||||
|
||||
**注意**: 不要堆砌太多,3-4 个身份即可
|
||||
|
||||
---
|
||||
|
||||
### 技巧 3: 给经典人设加缺陷
|
||||
|
||||
```
|
||||
霸道总裁 + 社恐(只对女主不社恐)
|
||||
傻白甜 + 路痴(严重到离谱)
|
||||
高冷男主 + 洁癖(只让女主碰)
|
||||
```
|
||||
|
||||
**作用**: 增加反差萌,更真实
|
||||
|
||||
---
|
||||
|
||||
## 6. 人设塑造自检清单
|
||||
|
||||
在设计人设时,逐项检查:
|
||||
|
||||
- [ ] **有吸引力吗**: 读者会喜欢这个角色吗?
|
||||
- [ ] **有反差吗**: 性格是否有多面性?
|
||||
- [ ] **有成长吗**: 角色是否会随剧情成长?
|
||||
- [ ] **有底线吗**: 善良但不圣母,腹黑但不变态?
|
||||
- [ ] **符合逻辑吗**: 性格、行为、职业是否一致?
|
||||
- [ ] **够独特吗**: 和其他小说的角色有区别吗?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 人设速查表
|
||||
|
||||
| 男主类型 | 核心特质 | 代表台词 | 适配女主 | 适用题材 |
|
||||
|---------|---------|---------|---------|---------|
|
||||
| **霸道总裁** | 强势宠溺 | "你是我的" | 傻白甜/软糯 | 现言宠文 |
|
||||
| **高冷禁欲** | 理性克制 | "我不该爱你" | 主动热情型 | 破冰恋爱 |
|
||||
| **腹黑妖孽** | 心机深沉 | "你逃不掉的" | 腹黑/软萌 | 宫斗权谋 |
|
||||
| **忠犬奶狗** | 专一黏人 | "姐姐,我长大了" | 御姐/成熟 | 姐弟恋 |
|
||||
| **病娇偏执** | 疯狂占有 | "你只能是我的" | 治愈温柔 | 暗黑虐恋 |
|
||||
|
||||
| 女主类型 | 核心特质 | 性格标签 | 适配男主 | 适用题材 |
|
||||
|---------|---------|---------|---------|---------|
|
||||
| **傻白甜** | 单纯善良 | 软萌可爱 | 霸道总裁 | 校园甜宠 |
|
||||
| **软糯小白兔** | 坚强隐忍 | 楚楚可怜 | 强势保护型 | 治愈宠文 |
|
||||
| **飒爽女强人** | 独立自主 | 事业为重 | 高冷禁欲 | 职场言情 |
|
||||
| **腹黑小妖精** | 聪明狡黠 | 不吃亏 | 腹黑/霸道 | 爽文虐恋 |
|
||||
| **重生复仇** | 果敢狠辣 | 复仇为先 | 忠犬/腹黑 | 重生虐渣 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:人设案例库
|
||||
|
||||
### 案例 1: 《何以笙箫默》何以琛
|
||||
|
||||
**人设**: 高冷禁欲型 + 专一深情
|
||||
**经典台词**: "如果世界上曾经有那个人出现过,其他人都会变成将就。"
|
||||
**成功原因**: 反差萌(外表高冷,内心深情)+ 漫长等待(7年)
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: 反面教材(某扑街文)
|
||||
|
||||
**人设失败**:
|
||||
```
|
||||
男主: 霸道总裁,但没钱没势,只会骂女主
|
||||
女主: 傻白甜,但蠢到无药可救,被骗 800 次还不长记性
|
||||
配角: 都是脸谱化工具人,智商为负
|
||||
```
|
||||
|
||||
**问题**: 人设空洞、不合逻辑、惹人厌烦
|
||||
@@ -0,0 +1,578 @@
|
||||
# 情感张力技巧 (Emotional Tension)
|
||||
|
||||
> **核心原则**: 言情小说的灵魂在于情感张力。没有张力 = 没有吸引力。读者追文的动力来自"想知道他们什么时候在一起"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 情感张力的四大来源
|
||||
|
||||
### 来源 1: 身份差距(地位悬殊)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
巨大身份差 → 不对等关系 → 女主自卑/男主傲慢 → 情感张力
|
||||
```
|
||||
|
||||
**经典身份差组合**:
|
||||
| 男主身份 | 女主身份 | 张力点 |
|
||||
|---------|---------|--------|
|
||||
| 亿万总裁 | 普通职员 | 金钱/地位差距 |
|
||||
| 娱乐圈影帝 | 十八线小透明 | 名气/资源差距 |
|
||||
| 豪门继承人 | 灰姑娘 | 阶级/家世差距 |
|
||||
| 古代王爷 | 平民女子 | 权力/身份差距 |
|
||||
| 天才学霸 | 学渣 | 智商/成就差距 |
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【身份差导致的卑微】
|
||||
"林先生,您的咖啡。"苏念低着头,不敢看他。
|
||||
她只是一个普通秘书,而他是商业帝国的太子爷。
|
||||
她连仰望的资格都没有……
|
||||
|
||||
【男主因身份差的强势】
|
||||
"苏念,你以为你配得上我?"
|
||||
林慕寒冷笑,眼中满是轻蔑。
|
||||
```
|
||||
|
||||
**张力爆发点**: 当女主因身份差距想要退缩,男主霸道表白:"我不在乎你是谁,我只在乎你是你。"
|
||||
|
||||
---
|
||||
|
||||
### 来源 2: 误会隔阂(信息差)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
关键误会 → 双方都不解释 → 越陷越深 → 爆发冲突 → 真相揭开
|
||||
```
|
||||
|
||||
**经典误会类型**:
|
||||
|
||||
#### 类型 A: 出轨误会
|
||||
```
|
||||
【表象】女主看到男主和别的女人暧昧
|
||||
【真相】那女人是男主妹妹/表妹/救命恩人
|
||||
【冲突】女主伤心欲绝,提出分手
|
||||
【爆发】真相大白,男主追妻火葬场
|
||||
```
|
||||
|
||||
#### 类型 B: 背叛误会
|
||||
```
|
||||
【表象】女主被陷害,男主以为她背叛了他
|
||||
【真相】女主是被迫/被陷害的
|
||||
【冲突】男主冷暴力/报复女主
|
||||
【爆发】真相揭露,男主悔恨万分
|
||||
```
|
||||
|
||||
#### 类型 C: 身份误会
|
||||
```
|
||||
【表象】女主以为男主是穷小子,男主以为女主是拜金女
|
||||
【真相】双方都隐瞒了真实身份
|
||||
【冲突】身份曝光后的尴尬与试探
|
||||
【爆发】坦诚相待,感情升华
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【误会产生】
|
||||
苏念看到林慕寒和一个漂亮女人亲密地走在一起。
|
||||
那女人挽着他的手臂,笑得甜蜜。
|
||||
她的心,碎了一地。
|
||||
|
||||
【误会加深】
|
||||
"林慕寒,我们分手吧。"
|
||||
"为什么?"
|
||||
"因为……我配不上你。"(真实原因:以为他有别的女人)
|
||||
|
||||
【真相揭晓】
|
||||
"她是我妹妹!我唯一的妹妹!"
|
||||
林慕寒抓住她的肩膀,眼中满是急切。
|
||||
"苏念,你怎么能因为这个误会就要离开我?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 来源 3: 禁忌关系(道德压力)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
禁忌身份 → 不能在一起 → 压抑克制 → 情难自禁 → 突破禁忌
|
||||
```
|
||||
|
||||
**经典禁忌类型**:
|
||||
| 禁忌类型 | 关系设定 | 张力来源 |
|
||||
|---------|---------|---------|
|
||||
| **师生恋** | 老师 × 学生 | 伦理道德/年龄差 |
|
||||
| **上下级** | 老板 × 员工 | 职场规则/权力不对等 |
|
||||
| **世仇家族** | 罗密欧与朱丽叶式 | 家族仇恨/外界压力 |
|
||||
| **姐弟恋** | 姐姐 × 弟弟 | 社会偏见/年龄差 |
|
||||
| **医患** | 医生 × 病人 | 职业道德 |
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【禁忌压抑】
|
||||
"不行,我是你的老师……"
|
||||
他推开她,眼中满是挣扎。
|
||||
"这违背师德,我不能……"
|
||||
|
||||
【情难自禁】
|
||||
但她靠近,他的理智就崩塌了。
|
||||
"苏念……别这样……"
|
||||
他的声音颤抖,手却紧紧抱住了她。
|
||||
|
||||
【突破禁忌】
|
||||
"管他什么师德!我只知道我爱你!"
|
||||
他俯身吻住她的唇。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 来源 4: 第三者插足(外部威胁)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
第三者出现 → 威胁女主地位 → 女主危机感 → 男主表态 → 感情稳固
|
||||
```
|
||||
|
||||
**第三者类型**:
|
||||
| 类型 | 设定 | 作用 |
|
||||
|------|------|------|
|
||||
| **白月光** | 男主初恋/前女友 | 制造女主的不安全感 |
|
||||
| **门当户对** | 豪门千金 | 家族施压,逼男主娶她 |
|
||||
| **青梅竹马** | 从小一起长大 | 有共同回忆,女主吃醋 |
|
||||
| **暗恋者** | 默默守护女主的人 | 让男主吃醋,意识到女主的好 |
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【第三者登场】
|
||||
"慕寒哥哥,好久不见~"
|
||||
一个精致的女人走进来,亲昵地挽住林慕寒的手臂。
|
||||
苏念的心,揪了起来。
|
||||
|
||||
【女主危机感】
|
||||
"她是你的初恋吧?"
|
||||
"……是。"
|
||||
"那你们……"
|
||||
"过去的事了。"林慕寒淡淡地说。
|
||||
但苏念还是不安……
|
||||
|
||||
【男主表态】
|
||||
林慕寒当着所有人的面,牵起苏念的手。
|
||||
"我只爱她。过去的,就让它过去吧。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 情感张力的强度分级
|
||||
|
||||
### 1级张力(日常小摩擦)
|
||||
|
||||
**特点**: 轻微吃醋、小矛盾、小误会
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"你为什么要和那个女生说话?"苏念噘嘴。
|
||||
"她问我路……"林慕寒无奈。
|
||||
"哼,不许和别的女生说话!"
|
||||
"好好好,我的错。"他宠溺地揉她的头。
|
||||
```
|
||||
|
||||
**作用**: 调节气氛,展现男主宠溺。
|
||||
|
||||
---
|
||||
|
||||
### 2级张力(明显冲突)
|
||||
|
||||
**特点**: 冷战、赌气、小分手
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"苏念,你别无理取闹!"
|
||||
"我无理取闹?那你去找你的白月光吧!"
|
||||
"你……!"
|
||||
苏念摔门而出。
|
||||
林慕寒站在原地,懊恼地扯了扯领带。
|
||||
```
|
||||
|
||||
**作用**: 制造波折,让感情升温。
|
||||
|
||||
---
|
||||
|
||||
### 3级张力(严重危机)
|
||||
|
||||
**特点**: 分手、背叛、误会爆发
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"林慕寒,我们分手吧。"
|
||||
"为什么?!"
|
||||
"因为……我们不合适。"
|
||||
"不合适?你昨天还说爱我!"
|
||||
"我……我不爱了。"
|
||||
她转身离去,留下他一人站在雨中。
|
||||
```
|
||||
|
||||
**作用**: 情节高潮,虐心虐肺。
|
||||
|
||||
---
|
||||
|
||||
### 4级张力(生死相隔)
|
||||
|
||||
**特点**: 意外、疾病、生离死别
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"林慕寒……如果我死了……你会想我吗?"
|
||||
"别说傻话!你不会有事的!"
|
||||
他紧紧抱住她,声音颤抖。
|
||||
"医生!医生!求你救救她!"
|
||||
```
|
||||
|
||||
**作用**: 极致虐点,情感爆发。
|
||||
|
||||
---
|
||||
|
||||
## 3. 情感张力的节奏控制
|
||||
|
||||
### 基础节奏公式
|
||||
|
||||
```
|
||||
甜(5章)→ 虐(3章)→ 甜(5章)→ 虐(5章)→ 甜(10章)
|
||||
```
|
||||
|
||||
**原则**: 甜虐交替,虐完必甜,虐不过三。
|
||||
|
||||
---
|
||||
|
||||
### 错误节奏(禁忌)
|
||||
|
||||
#### 禁忌 1: 一直甜(太腻)
|
||||
```
|
||||
第1-50章: 男主对女主各种宠
|
||||
问题: 读者会腻,没有追文动力
|
||||
```
|
||||
|
||||
#### 禁忌 2: 一直虐(太苦)
|
||||
```
|
||||
第1-50章: 误会、背叛、分手、虐心
|
||||
问题: 读者会弃文,"看不下去了"
|
||||
```
|
||||
|
||||
#### 禁忌 3: 虐而不解(憋屈)
|
||||
```
|
||||
第10章: 男主误会女主
|
||||
第20章: 还在误会
|
||||
第30章: 继续误会
|
||||
问题: 读者会骂:"真相什么时候大白?!"
|
||||
```
|
||||
|
||||
**正确做法**: 虐点不要拖太久,最多10-15章就要解开。
|
||||
|
||||
---
|
||||
|
||||
## 4. 制造情感张力的具体技巧
|
||||
|
||||
### 技巧 1: 欲擒故纵(拉扯感)
|
||||
|
||||
**公式**:
|
||||
```
|
||||
男主靠近 → 女主退后 → 男主追 → 女主逃 → 男主霸道:"别跑!"
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【女主躲避】
|
||||
苏念看到林慕寒,转身就走。
|
||||
"苏念!"他叫住她。
|
||||
"林先生,有事吗?"她冷淡地问。
|
||||
|
||||
【男主追击】
|
||||
林慕寒大步走过来,将她困在墙角。
|
||||
"躲什么?我又不会吃了你。"
|
||||
"请你自重……"
|
||||
"自重?"他冷笑,"你以为你逃得掉?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 技巧 2: 若即若离(暧昧感)
|
||||
|
||||
**公式**:
|
||||
```
|
||||
不是恋人 → 但亲密接触 → 双方心动 → 但都不表白 → 读者着急
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【亲密但不表白】
|
||||
林慕寒帮她擦掉嘴角的酱汁。
|
||||
"吃慢点,又没人跟你抢。"
|
||||
苏念的脸红了。
|
||||
他……这是什么意思?
|
||||
但他什么都没说,只是笑了笑。
|
||||
|
||||
【暧昧气氛】
|
||||
"苏念,你……有男朋友吗?"
|
||||
"没……没有……"
|
||||
"那就好。"
|
||||
"什么?"
|
||||
"没什么。"他转身离开。
|
||||
```
|
||||
|
||||
**读者反应**: "啊啊啊快表白啊!急死我了!"
|
||||
|
||||
---
|
||||
|
||||
### 技巧 3: 先抑后扬(反差感)
|
||||
|
||||
**公式**:
|
||||
```
|
||||
前期虐 → 女主绝望 → 男主突然温柔 → 情感爆发
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【前期冷漠】
|
||||
"林慕寒,你能不能对我好一点……"
|
||||
"为什么?"他冷冷地问。
|
||||
"因为……算了,当我没说。"
|
||||
|
||||
【突然温柔】
|
||||
那天她发高烧,他整夜守在床边。
|
||||
"傻瓜……"他低声说,"以后别让我担心了。"
|
||||
苏念睁开眼,看到的是他红肿的眼睛。
|
||||
她的泪,滑落了。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 技巧 4: 借第三者(吃醋感)
|
||||
|
||||
**公式**:
|
||||
```
|
||||
第三者出现 → 男主/女主吃醋 → 争风吃醋 → 确认感情
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【女主吃醋】
|
||||
"慕寒,这是我特意为你做的便当~"
|
||||
那女人笑得甜蜜,将便当递给林慕寒。
|
||||
苏念在一旁,心如刀割。
|
||||
|
||||
【男主表态】
|
||||
林慕寒看都没看那便当一眼,转身走向苏念。
|
||||
"我只吃她做的。"
|
||||
他拿起苏念的便当,当着所有人的面吃了起来。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 情感爆发点设计
|
||||
|
||||
### 爆发点 1: 表白(甜)
|
||||
|
||||
**时机**: 经历误会/磨难后,男主终于说出心里话
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"苏念,我爱你。"
|
||||
"什么……?"
|
||||
"从见到你的第一天起,我就爱上你了。"
|
||||
他将她拥入怀中。
|
||||
"对不起,让你受委屈了。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 爆发点 2: 分手(虐)
|
||||
|
||||
**时机**: 误会达到顶峰,女主心灰意冷
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"林慕寒,我们分手吧。"
|
||||
她的声音很平静,但眼中已无光。
|
||||
"不……不要……"他慌了,"我可以解释……"
|
||||
"不必了。我累了。"
|
||||
她转身离开,头也不回。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 爆发点 3: 生死关头(极致虐)
|
||||
|
||||
**时机**: 意外/疾病,面临生死抉择
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"林慕寒……我可能……活不了了……"
|
||||
"别说傻话!你一定会好起来的!"
|
||||
"如果……我死了……你会记得我吗?"
|
||||
"我会!我会一辈子记得你!"
|
||||
他的泪水滴在她脸上。
|
||||
"所以……你一定要活下去……一定要……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 爆发点 4: 追妻火葬场(反转甜)
|
||||
|
||||
**时机**: 男主意识到错误,疯狂追回女主
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"苏念,求你原谅我……"
|
||||
林慕寒跪在雨中,眼中满是悔恨。
|
||||
"当初是我瞎了眼,是我不信你……"
|
||||
"求你……再给我一次机会……"
|
||||
苏念转过身,泪流满面。
|
||||
"林慕寒……我恨你……"
|
||||
"我知道……所以让我用一辈子来赎罪……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 情感张力的对话技巧
|
||||
|
||||
### 技巧 A: 欲言又止(暧昧)
|
||||
|
||||
```
|
||||
"苏念,我……"
|
||||
"嗯?"
|
||||
"……没什么。"
|
||||
他到嘴边的话,又咽了回去。
|
||||
```
|
||||
|
||||
**作用**: 制造悬念,让读者猜测他想说什么。
|
||||
|
||||
---
|
||||
|
||||
### 技巧 B: 话中有话(试探)
|
||||
|
||||
```
|
||||
"苏念,你会离开我吗?"
|
||||
"为什么这么问?"
|
||||
"如果……有一天我什么都没有了……你还会爱我吗?"
|
||||
"……会。"
|
||||
"真的?"
|
||||
"真的。"
|
||||
他紧紧抱住她,眼中闪过一丝不易察觉的痛苦。
|
||||
```
|
||||
|
||||
**作用**: 暗示危机,埋伏笔。
|
||||
|
||||
---
|
||||
|
||||
### 技巧 C: 反问回怼(拉扯)
|
||||
|
||||
```
|
||||
"你是不是喜欢我?"
|
||||
"你说呢?"
|
||||
"我问你呢!"
|
||||
"那你呢?你喜欢我吗?"
|
||||
"我……"
|
||||
"不说就算了。"
|
||||
"喂!你别走啊!"
|
||||
```
|
||||
|
||||
**作用**: 制造拉扯感,增加互动趣味。
|
||||
|
||||
---
|
||||
|
||||
## 7. 避免情感张力的常见错误
|
||||
|
||||
### 错误 1: 无病呻吟
|
||||
|
||||
**表现**:
|
||||
```
|
||||
"我好难过……"
|
||||
"为什么难过?"
|
||||
"就是难过……"
|
||||
"到底为什么?"
|
||||
"说不清楚……"
|
||||
```
|
||||
|
||||
**问题**: 没有明确的冲突点,读者不知道为什么难过。
|
||||
|
||||
**正确做法**: 给出明确的原因(误会/背叛/分离等)。
|
||||
|
||||
---
|
||||
|
||||
### 错误 2: 为虐而虐
|
||||
|
||||
**表现**:
|
||||
```
|
||||
第10章: 男主误会女主出轨 → 冷暴力
|
||||
第15章: 女主证明清白 → 和好
|
||||
第20章: 又误会女主背叛 → 又冷暴力
|
||||
第25章: 又证明清白 → 又和好
|
||||
```
|
||||
|
||||
**问题**: 重复套路,读者会腻。
|
||||
|
||||
**正确做法**: 每次虐点要有新意,不要重复。
|
||||
|
||||
---
|
||||
|
||||
### 错误 3: 男主渣而不洗白
|
||||
|
||||
**表现**:
|
||||
```
|
||||
男主一直对女主冷暴力/出轨/家暴
|
||||
直到最后都没有真心悔改
|
||||
```
|
||||
|
||||
**问题**: 读者会恨男主,弃文。
|
||||
|
||||
**正确做法**: 虐可以,但男主必须悔改,追妻火葬场。
|
||||
|
||||
---
|
||||
|
||||
## 8. 情感张力自检清单
|
||||
|
||||
- [ ] **有明确的冲突点**: 身份差/误会/禁忌/第三者?
|
||||
- [ ] **节奏合理**: 甜虐交替,虐不过三?
|
||||
- [ ] **张力递进**: 从小摩擦到大危机?
|
||||
- [ ] **有爆发点**: 表白/分手/生死/追妻?
|
||||
- [ ] **符合人设**: 角色的反应符合其性格?
|
||||
- [ ] **不重复**: 每次虐点有新意?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 情感张力速查表
|
||||
|
||||
| 张力类型 | 核心矛盾 | 持续时长 | 解决方式 | 爽点 |
|
||||
|---------|---------|---------|---------|------|
|
||||
| **身份差** | 地位悬殊 | 长期 | 男主表态"我不在乎" | 霸道宠溺 |
|
||||
| **误会** | 信息差 | 5-15章 | 真相揭晓 | 追妻火葬场 |
|
||||
| **禁忌** | 道德压力 | 长期 | 突破禁忌 | 禁忌之恋 |
|
||||
| **第三者** | 外部威胁 | 3-10章 | 男主表态 | 吃醋撒娇 |
|
||||
| **冷战** | 小矛盾 | 1-3章 | 男主主动和好 | 宠溺哄人 |
|
||||
| **分手** | 严重误会 | 10-20章 | 追妻火葬场 | 虐后反转 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典情感张力案例
|
||||
|
||||
### 案例 1: 《何以笙箫默》身份差 + 误会
|
||||
|
||||
**张力点**:
|
||||
- 身份差: 律师精英 vs 普通女孩
|
||||
- 误会: 女主以为男主不爱她,主动离开
|
||||
- 分离: 7年后重逢
|
||||
|
||||
**高潮**:
|
||||
"等了七年,你终于回来了。"
|
||||
何以琛紧紧抱住赵默笙。
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: 反面教材(某扑街文)
|
||||
|
||||
```
|
||||
男主一直虐女主,从头虐到尾。
|
||||
女主被虐得死去活来,男主还是不悔改。
|
||||
最后女主原谅了男主,两人在一起了。
|
||||
```
|
||||
|
||||
**问题**: 男主没有成长,女主太圣母,读者不买账。
|
||||
@@ -0,0 +1,213 @@
|
||||
# 狗血言情剧情模板库
|
||||
|
||||
> 本文档提供经过验证的狗血言情剧情模板,涵盖长篇、中篇、短篇不同体量,可直接套用或组合使用。
|
||||
|
||||
---
|
||||
|
||||
## 一、长篇模板(80-150万字)
|
||||
|
||||
### 模板1:霸总追妻火葬场
|
||||
|
||||
**核心公式**:`前期虐女主 + 误会分离 + 男主追悔 + 漫长追妻 + HE`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 卷一:甜蜜假象 | 15% | 契约/利益结合,女主隐忍,男主冷漠 |
|
||||
| 卷二:虐心深渊 | 25% | 白月光出现,误会加深,女主心死 |
|
||||
| 卷三:决裂离开 | 15% | 真相部分揭露,女主离开,男主震惊 |
|
||||
| 卷四:追妻之路 | 30% | 男主追悔,女主成长,反复拉扯 |
|
||||
| 卷五:破镜重圆 | 15% | 真相大白,男主赎罪,HE |
|
||||
|
||||
**关键节点**:
|
||||
```
|
||||
开篇钩子:女主签下契约/协议结婚(展示不平等地位)
|
||||
第一转折:白月光归来,男主态度骤变
|
||||
虐心高潮:女主小产/重病/被陷害,男主不信任
|
||||
决裂点:女主心死离开(带球跑/净身出户/假死)
|
||||
追妻起点:男主发现真相,开始追悔
|
||||
追妻高潮:女主事业有成/有新欢,男主卑微求复合
|
||||
黑暗时刻:旧敌反扑/新危机,男主舍命相护
|
||||
大结局:真相彻底揭露,男主完成救赎,HE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板2:先婚后爱
|
||||
|
||||
**核心公式**:`利益联姻 + 冷漠同居 + 日久生情 + 误会危机 + 甜蜜HE`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 卷一:陌生夫妻 | 20% | 联姻原因,同居磨合,互相试探 |
|
||||
| 卷二:暗生情愫 | 25% | 日常相处,渐生好感,不自知的在意 |
|
||||
| 卷三:情感确认 | 20% | 一方先动心,试探表白,关系升温 |
|
||||
| 卷四:危机考验 | 20% | 外部威胁/误会,感情受考验 |
|
||||
| 卷五:甜蜜日常 | 15% | 危机解除,撒糖日常,HE |
|
||||
|
||||
**关键节点**:
|
||||
```
|
||||
开篇钩子:联姻原因揭示(家族利益/报恩/意外)
|
||||
破冰时刻:一次意外合作/共同经历
|
||||
心动瞬间:不经意的关心被对方察觉
|
||||
嫉妒桥段:第三者出现,引发占有欲
|
||||
表白时刻:一方坦诚心意
|
||||
甜蜜期:婚后恋爱,补办仪式感
|
||||
危机点:旧事重提/家族反对/误会
|
||||
大结局:携手面对,感情升华
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板3:重生复仇
|
||||
|
||||
**核心公式**:`惨死重生 + 步步为营 + 手撕仇人 + 收获真爱 + 圆满结局`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 卷一:浴火重生 | 15% | 前世惨状回顾,重生节点,复仇规划 |
|
||||
| 卷二:布局落子 | 25% | 改变命运,积蓄力量,小试牛刀 |
|
||||
| 卷三:步步紧逼 | 25% | 逐个击破仇人,真相逐渐揭露 |
|
||||
| 卷四:终极对决 | 20% | 大Boss现身,最终决战 |
|
||||
| 卷五:新生圆满 | 15% | 仇人伏法,收获幸福 |
|
||||
|
||||
**关键节点**:
|
||||
```
|
||||
开篇钩子:前世惨死场景(震撼开局)
|
||||
重生时刻:回到关键节点,立下复仇誓言
|
||||
第一次反击:小胜仇人,展示重生优势
|
||||
遇见男主:命运改变,新的羁绊
|
||||
中期高潮:揭露一个重大真相
|
||||
黑暗时刻:仇人反扑,陷入危机
|
||||
最终决战:手撕大Boss
|
||||
大结局:仇人伏法,与男主HE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、中篇模板(30-60万字)
|
||||
|
||||
### 模板4:娱乐圈甜宠
|
||||
|
||||
**核心公式**:`隐婚设定 + 事业线交织 + 撒糖日常 + 公开撒狗粮`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 卷一:秘密关系 | 30% | 隐婚原因,偷偷约会,差点暴露 |
|
||||
| 卷二:事业危机 | 35% | 绯闻/黑料,携手应对,感情加深 |
|
||||
| 卷三:高调官宣 | 35% | 公开关系,打脸黑子,甜蜜日常 |
|
||||
|
||||
**关键节点**:
|
||||
```
|
||||
开篇钩子:隐婚身份差点暴露的惊险场面
|
||||
甜蜜日常:片场探班/私下约会
|
||||
事业高光:女主作品大爆/获奖
|
||||
危机时刻:恶意绯闻/前任纠缠
|
||||
男主护妻:霸气回应/资源支持
|
||||
官宣时刻:高调公开,全网震惊
|
||||
撒糖结局:婚礼/孕事/日常甜
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板5:豪门恩怨
|
||||
|
||||
**核心公式**:`身世之谜 + 家族斗争 + 真爱相守 + 夺回一切`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 卷一:身世迷雾 | 30% | 真假千金,身份疑云,初入豪门 |
|
||||
| 卷二:明争暗斗 | 40% | 家族内斗,阴谋陷害,步步惊心 |
|
||||
| 卷三:真相大白 | 30% | 身世揭晓,夺回一切,HE |
|
||||
|
||||
---
|
||||
|
||||
## 三、短篇模板(10-30万字)
|
||||
|
||||
### 模板6:破镜重圆
|
||||
|
||||
**核心公式**:`分手多年 + 意外重逢 + 旧情复燃 + 解开心结`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 前期 | 35% | 重逢场景,回忆杀,表面疏离 |
|
||||
| 中期 | 40% | 被迫接触,旧情复燃,误会揭开 |
|
||||
| 后期 | 25% | 坦诚相对,破镜重圆 |
|
||||
|
||||
---
|
||||
|
||||
### 模板7:契约恋爱
|
||||
|
||||
**核心公式**:`假戏 + 日久生情 + 真心告白`
|
||||
|
||||
**章节分配**:
|
||||
| 阶段 | 字数占比 | 核心内容 |
|
||||
|------|---------|---------|
|
||||
| 前期 | 30% | 契约原因,规则制定,开始演戏 |
|
||||
| 中期 | 45% | 假戏真做,心动瞬间,患得患失 |
|
||||
| 后期 | 25% | 契约到期,真心告白,HE |
|
||||
|
||||
---
|
||||
|
||||
## 四、经典剧情节点库
|
||||
|
||||
### 开篇钩子
|
||||
| 类型 | 示例 | 适用模板 |
|
||||
|------|------|---------|
|
||||
| 冲突开场 | 婚礼上被抛弃/当众羞辱 | 重生、追妻 |
|
||||
| 悬念开场 | 醒来发现怀孕/失忆 | 先婚后爱、豪门 |
|
||||
| 反转开场 | 以为是灰姑娘实则大佬 | 甜宠、豪门 |
|
||||
| 重逢开场 | 多年后意外相遇 | 破镜重圆 |
|
||||
|
||||
### 虐心名场面
|
||||
| 场景 | 情绪值 | 使用注意 |
|
||||
|------|--------|---------|
|
||||
| 雨中分手 | ★★★★★ | 需要足够铺垫 |
|
||||
| 病床守候 | ★★★★☆ | 不宜过长 |
|
||||
| 误会目睹 | ★★★★☆ | 需要合理解释 |
|
||||
| 被迫分离 | ★★★★★ | 外力因素要充分 |
|
||||
|
||||
### 甜蜜名场面
|
||||
| 场景 | 甜度 | 适用时机 |
|
||||
|------|------|---------|
|
||||
| 吃醋宣誓主权 | ★★★★☆ | 感情确认后 |
|
||||
| 霸道壁咚 | ★★★☆☆ | 暧昧期 |
|
||||
| 婚礼/求婚 | ★★★★★ | 结局或重要节点 |
|
||||
| 孕期宠溺 | ★★★★☆ | 番外或结局 |
|
||||
|
||||
---
|
||||
|
||||
## 五、模板使用指南
|
||||
|
||||
### 四步套用法
|
||||
|
||||
1. **选择基础模板**:根据预计字数选择长/中/短篇模板
|
||||
2. **填充人设**:套入角色设定(参考 character-archetypes.md)
|
||||
3. **调整节点**:根据需要增删剧情节点
|
||||
4. **添加特色**:加入独特设定或反转
|
||||
|
||||
### 模板组合技巧
|
||||
|
||||
| 组合方式 | 示例 | 效果 |
|
||||
|---------|------|------|
|
||||
| 主线+副线 | 追妻火葬场 + 商战 | 增加厚度 |
|
||||
| 双重身份 | 先婚后爱 + 娱乐圈 | 增加看点 |
|
||||
| 时间线交错 | 重生 + 破镜重圆 | 增加层次 |
|
||||
|
||||
---
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
**Q:模板会不会写出来很套路?**
|
||||
A:模板是骨架,血肉靠细节。同样的追妻模板,人设、金句、名场面不同,效果天差地别。
|
||||
|
||||
**Q:可以混用多个模板吗?**
|
||||
A:可以,但要有主次。建议一个主模板+1-2个元素借鉴,避免结构混乱。
|
||||
|
||||
**Q:字数分配必须严格遵守吗?**
|
||||
A:可以根据实际情况调整±5%,但大体比例要保持,避免头重脚轻或虎头蛇尾。
|
||||
@@ -0,0 +1,574 @@
|
||||
# 言情节奏与章节结构 (Romance Pacing & Chapter Structure)
|
||||
|
||||
> **核心原则**: 言情小说的节奏要"快慢结合"。感情线要层层递进,既不能太快(第1章就在一起),也不能太慢(50章还在暧昧)。每章要有明确的情感推进或冲突爆发。
|
||||
|
||||
---
|
||||
|
||||
## 1. 言情小说的三大节奏类型
|
||||
|
||||
### 类型 1: 快节奏言情(爽文型)
|
||||
|
||||
**特点**: 开局即暧昧,10-20章确认关系,主打甜宠
|
||||
|
||||
**节奏表**:
|
||||
```
|
||||
第1-5章: 相遇,一见钟情/日久生情苗头
|
||||
第6-10章: 暧昧升温,肢体接触增加
|
||||
第11-15章: 告白,确认关系
|
||||
第16-30章: 甜蜜日常 + 小误会小虐
|
||||
第31-40章: 大危机(第三者/家族反对)
|
||||
第41-50章: 解决危机,结婚/订婚
|
||||
```
|
||||
|
||||
**适用**: 短篇甜文、快节奏爽文
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第1章: 女主撞到男主怀里(一见钟情)
|
||||
第2章: 男主霸道留下女主联系方式
|
||||
第5章: 男主开始追求女主
|
||||
第10章: 男主壁咚告白
|
||||
第15章: 确认关系,公开
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 2: 中速节奏言情(标准型)
|
||||
|
||||
**特点**: 20-40章确认关系,甜虐交替,主打情感深度
|
||||
|
||||
**节奏表**:
|
||||
```
|
||||
第1-10章: 相遇,建立联系,初步好感
|
||||
第11-20章: 暧昧期,试探,心动
|
||||
第21-30章: 小误会,分分合合
|
||||
第31-40章: 告白,确认关系
|
||||
第41-60章: 甜蜜期 + 外部危机
|
||||
第61-80章: 大危机,分手
|
||||
第81-100章: 追妻火葬场,和好,结婚
|
||||
```
|
||||
|
||||
**适用**: 中长篇言情,情感细腻型
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第1-10章: 相遇,从陌生到熟悉
|
||||
第20章: 女主意识到自己心动了
|
||||
第30章: 男主表白,女主犹豫
|
||||
第40章: 确认关系
|
||||
第60章: 第三者出现,危机
|
||||
第80章: 分手
|
||||
第100章: 和好,结婚
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 3: 慢节奏言情(虐文型)
|
||||
|
||||
**特点**: 50章+才确认关系,主打虐恋情深
|
||||
|
||||
**节奏表**:
|
||||
```
|
||||
第1-20章: 相遇,误会,敌对或冷漠
|
||||
第21-40章: 被迫接触,逐渐了解
|
||||
第41-60章: 暗生情愫,但不自知
|
||||
第61-80章: 意识到爱情,但有障碍
|
||||
第81-100章: 克服障碍,告白
|
||||
第101-120章: 确认关系,甜蜜补偿
|
||||
```
|
||||
|
||||
**适用**: 长篇虐文、先虐后甜型
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第1-30章: 男主误会女主,冷暴力
|
||||
第40章: 女主离开,男主后悔
|
||||
第60章: 追妻火葬场
|
||||
第80章: 真相大白
|
||||
第100章: 表白,确认关系
|
||||
第120章: 结婚,超甜补偿
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 感情线的五大阶段
|
||||
|
||||
### 阶段 1: 相遇期(建立联系)
|
||||
|
||||
**目标**: 让男女主产生联系,埋下感情伏笔
|
||||
|
||||
**章节分配**: 1-5章
|
||||
|
||||
**必备元素**:
|
||||
- 初次见面(印象深刻)
|
||||
- 建立联系(互留联系方式/成为同事/邻居等)
|
||||
- 埋伏笔(男主对女主有不同的反应)
|
||||
|
||||
**节奏要点**:
|
||||
```
|
||||
第1章: 相遇(意外/救命/撞见/被迫相亲)
|
||||
第2-3章: 建立联系(成为同事/邻居/假扮情侣)
|
||||
第4-5章: 初步好感(男主对女主有特殊关注)
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【第1章:意外相遇】
|
||||
苏念在电梯里,突然停电。
|
||||
黑暗中,她撞进一个温暖的怀抱。
|
||||
"抱歉……"她慌忙退开。
|
||||
电梯恢复,灯光亮起。
|
||||
她看清了那个人——高大、英俊、冷峻。
|
||||
那是她的新老板,林慕寒。
|
||||
|
||||
【第3章:建立联系】
|
||||
"苏念,你是我的秘书。"
|
||||
林慕寒看着她,眼中闪过一丝不易察觉的兴趣。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 2: 暧昧期(试探心意)
|
||||
|
||||
**目标**: 双方互相试探,暧昧升温,但不表白
|
||||
|
||||
**章节分配**: 6-20章(可调整)
|
||||
|
||||
**必备元素**:
|
||||
- 肢体接触增加(牵手、拥抱)
|
||||
- 吃醋场景(男主/女主吃醋)
|
||||
- 若即若离(欲擒故纵)
|
||||
- 心理活动(意识到心动但不敢确认)
|
||||
|
||||
**节奏要点**:
|
||||
```
|
||||
每3-5章一个小甜蜜高潮:
|
||||
- 第一次牵手
|
||||
- 第一次被壁咚
|
||||
- 第一次吃醋
|
||||
- 第一次亲密接触(拥抱/额头相抵)
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【第8章:第一次牵手】
|
||||
人群拥挤,林慕寒一把握住她的手。
|
||||
"别走丢了。"他淡淡地说。
|
||||
苏念的脸瞬间红透了。
|
||||
(他……他牵我的手了!)
|
||||
|
||||
【第12章:吃醋】
|
||||
"你为什么和那个男人说话?"
|
||||
林慕寒的脸色很不好。
|
||||
"他只是问我路……"
|
||||
"以后不许和别的男人说话。"
|
||||
"这也太霸道了吧?"
|
||||
"我就是霸道。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 3: 表白期(确认关系)
|
||||
|
||||
**目标**: 男主表白,女主接受,确认恋爱关系
|
||||
|
||||
**章节分配**: 20-40章(根据总长度调整)
|
||||
|
||||
**必备元素**:
|
||||
- 情感铺垫(暧昧到极点)
|
||||
- 表白场景(深情/霸道/意外)
|
||||
- 女主反应(震惊/感动/接受)
|
||||
- 确认关系(公开或暂时保密)
|
||||
|
||||
**节奏要点**:
|
||||
```
|
||||
表白前3章: 铺垫情绪,制造契机
|
||||
表白章: 情感爆发,告白
|
||||
表白后2章: 确认关系,甜蜜升级
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【第30章:表白】
|
||||
"苏念,我喜欢你。"
|
||||
林慕寒看着她的眼睛,眼中满是深情。
|
||||
"从见到你的第一天起,我就喜欢上你了。"
|
||||
苏念愣住了,眼泪止不住地流。
|
||||
"我……"
|
||||
"你不用现在回答我。"他温柔地说。
|
||||
"但我想让你知道……我爱你。"
|
||||
|
||||
【第31章:确认关系】
|
||||
"林慕寒……"
|
||||
"嗯?"
|
||||
"我……我也喜欢你。"苏念小声说。
|
||||
林慕寒笑了,将她拥入怀中。
|
||||
"再说一遍。"
|
||||
"我喜欢你!"
|
||||
"真乖。"他吻了吻她的额头。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 4: 甜蜜期(恋爱日常)
|
||||
|
||||
**目标**: 展示恋爱日常,甜蜜互动,偶尔小虐
|
||||
|
||||
**章节分配**: 40-70章(根据总长度调整)
|
||||
|
||||
**必备元素**:
|
||||
- 日常甜宠(喂饭、摸头、抱抱)
|
||||
- 小误会(吃醋、赌气)
|
||||
- 外部威胁(第三者/家族反对)
|
||||
- 感情稳固(克服困难)
|
||||
|
||||
**节奏要点**:
|
||||
```
|
||||
甜蜜日常(5章) → 小误会(2章) → 和好更甜(3章) → 循环
|
||||
穿插外部危机(每10-15章一次大危机)
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【第45章:日常甜蜜】
|
||||
"张嘴。"林慕寒喂她吃饭。
|
||||
"我又不是小孩子……"苏念嘟囔。
|
||||
"在我眼里就是。"他宠溺地笑。
|
||||
|
||||
【第52章:小误会】
|
||||
"你为什么不告诉我你要加班?"
|
||||
"我忘了……"
|
||||
"你心里还有我吗?"苏念委屈。
|
||||
"当然有。"林慕寒把她抱进怀里。
|
||||
"只有你。"
|
||||
|
||||
【第60章:外部危机】
|
||||
"我儿子不能娶你这种女人!"
|
||||
林母扔给她一张支票。
|
||||
苏念的心,碎了……
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 5: 结局期(圆满结局)
|
||||
|
||||
**目标**: 解决所有危机,圆满结局(结婚/生子)
|
||||
|
||||
**章节分配**: 最后5-10章
|
||||
|
||||
**必备元素**:
|
||||
- 解决最后障碍(家族同意/第三者退出)
|
||||
- 求婚/结婚场景
|
||||
- 幸福结局(生子/白头到老)
|
||||
|
||||
**节奏要点**:
|
||||
```
|
||||
倒数第5章: 最后危机解决
|
||||
倒数第3章: 求婚
|
||||
倒数第2章: 结婚
|
||||
最后1章: 番外(生子/老年幸福)
|
||||
```
|
||||
|
||||
**示例场景**:
|
||||
```
|
||||
【倒数第3章:求婚】
|
||||
"苏念,嫁给我。"
|
||||
林慕寒单膝跪地,手里捧着戒指。
|
||||
周围的人都在起哄:"嫁给他!嫁给他!"
|
||||
苏念哭着点头:"我愿意……"
|
||||
|
||||
【最后章:圆满结局】
|
||||
三年后。
|
||||
苏念抱着孩子,林慕寒揽着她的腰。
|
||||
"幸福吗?"他问。
|
||||
"嗯,很幸福。"她笑。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 单章结构设计
|
||||
|
||||
### 标准单章结构(爽点章)
|
||||
|
||||
**结构**:
|
||||
```
|
||||
开头(引入矛盾/延续上章悬念)
|
||||
↓
|
||||
发展(情感推进/冲突爆发)
|
||||
↓
|
||||
高潮(爽点/虐点/甜点)
|
||||
↓
|
||||
结尾(留悬念/埋伏笔)
|
||||
```
|
||||
|
||||
**字数分配**:
|
||||
- 总字数: 2000-3000字/章(主流)
|
||||
- 开头: 200-300字
|
||||
- 发展: 1000-1500字
|
||||
- 高潮: 500-800字
|
||||
- 结尾: 200-300字
|
||||
|
||||
---
|
||||
|
||||
### 单章类型与节奏
|
||||
|
||||
#### 类型 A: 甜蜜章
|
||||
|
||||
**目标**: 给读者发糖,展示男主宠溺
|
||||
|
||||
**节奏**:
|
||||
```
|
||||
开头: 日常场景(起床/吃饭/约会)
|
||||
发展: 甜蜜互动(喂饭/摸头/亲吻)
|
||||
高潮: 情话/告白/甜蜜爆表
|
||||
结尾: 留白(戛然而止,意犹未尽)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【开头】
|
||||
苏念刚睡醒,迷迷糊糊睁开眼。
|
||||
|
||||
【发展】
|
||||
林慕寒俯身,在她额头上印下一吻。
|
||||
"早安,我的宝贝。"
|
||||
|
||||
【高潮】
|
||||
"你知道吗?"他看着她,"每天醒来第一眼看到你,是我最幸福的事。"
|
||||
苏念脸红了。
|
||||
|
||||
【结尾】
|
||||
她把头埋进他怀里。
|
||||
"……我也是。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型 B: 虐心章
|
||||
|
||||
**目标**: 制造冲突,虐读者心
|
||||
|
||||
**节奏**:
|
||||
```
|
||||
开头: 危机预警(气氛不对)
|
||||
发展: 矛盾爆发(误会/第三者)
|
||||
高潮: 虐点爆发(分手/冷暴力)
|
||||
结尾: 留悬念(真相是什么?会和好吗?)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【开头】
|
||||
苏念感觉到,林慕寒最近很反常。
|
||||
|
||||
【发展】
|
||||
她看到他和一个美女拥抱。
|
||||
心,碎了一地。
|
||||
|
||||
【高潮】
|
||||
"林慕寒,我们分手吧。"
|
||||
她说完这句话,转身离开。
|
||||
|
||||
【结尾】
|
||||
林慕寒站在原地,没有追上去。
|
||||
雨,下起来了……
|
||||
(下章:真相是什么?)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型 C: 爽点章(打脸/反转)
|
||||
|
||||
**目标**: 让读者大呼过瘾
|
||||
|
||||
**节奏**:
|
||||
```
|
||||
开头: 女主被欺负/看不起
|
||||
发展: 男主登场(或女主反击)
|
||||
高潮: 打脸/碾压/装逼
|
||||
结尾: 众人震惊(爽!)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【开头】
|
||||
"就凭你这个穷鬼,也配进这家店?"
|
||||
店员看不起苏念。
|
||||
|
||||
【发展】
|
||||
林慕寒走过来,揽住她的腰。
|
||||
"她是我女朋友。"
|
||||
|
||||
【高潮】
|
||||
"把你们店长叫来。"林慕寒冷冷地说。
|
||||
"告诉他,以后这个店员不用来了。"
|
||||
店员脸色惨白。
|
||||
|
||||
【结尾】
|
||||
"林……林总……"
|
||||
周围的人都震惊了。
|
||||
(原来她男朋友这么厉害!)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 付费卡点设计(网文必备)
|
||||
|
||||
### 黄金卡点位置
|
||||
|
||||
**卡点原则**: 在读者最想看下文的地方断章
|
||||
|
||||
**经典卡点**:
|
||||
```
|
||||
1. 表白前: "苏念,我……" (下章才说"我喜欢你")
|
||||
2. 真相前: "其实那天……" (下章揭晓真相)
|
||||
3. 危机时: "她出事了!" (下章才知道什么事)
|
||||
4. 关键对话前: "我有话要说。" (下章才说)
|
||||
5. 吻戏前: "他低头……" (下章才写吻)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 卡点示例
|
||||
|
||||
#### 示例 1: 表白卡点
|
||||
|
||||
```
|
||||
【本章结尾】
|
||||
"苏念,我……"
|
||||
林慕寒看着她,欲言又止。
|
||||
"你什么?"她好奇。
|
||||
"我……"
|
||||
他深吸一口气——
|
||||
|
||||
【下章开头】
|
||||
"我喜欢你。"
|
||||
```
|
||||
|
||||
#### 示例 2: 真相卡点
|
||||
|
||||
```
|
||||
【本章结尾】
|
||||
"其实那天,她不是我女朋友……"
|
||||
林慕寒终于开口。
|
||||
"那她是谁?"苏念问。
|
||||
"她是……"
|
||||
|
||||
【下章开头】
|
||||
"她是我妹妹。"
|
||||
```
|
||||
|
||||
#### 示例 3: 危机卡点
|
||||
|
||||
```
|
||||
【本章结尾】
|
||||
"林总!苏小姐她……她出事了!"
|
||||
助理慌张地跑进来。
|
||||
林慕寒脸色大变:"什么事?!"
|
||||
"她……"
|
||||
|
||||
【下章开头】
|
||||
"她被车撞了!现在在医院抢救!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 高潮章节设计
|
||||
|
||||
### 高潮章节的位置
|
||||
|
||||
**标准分布**(以100章为例):
|
||||
```
|
||||
第10章: 小高潮(初吻/牵手)
|
||||
第30章: 中高潮(表白/确认关系)
|
||||
第60章: 大高潮(分手/生死危机)
|
||||
第80章: 追妻高潮(火葬场/和好)
|
||||
第100章: 终极高潮(结婚/圆满)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 高潮章节的写作要点
|
||||
|
||||
**要点 1: 情绪铺垫**
|
||||
```
|
||||
高潮前3章: 逐步铺垫情绪
|
||||
高潮章: 情感爆发
|
||||
高潮后2章: 余韵/后续
|
||||
```
|
||||
|
||||
**要点 2: 字数加长**
|
||||
```
|
||||
普通章: 2000-3000字
|
||||
高潮章: 3000-5000字(更详细的描写)
|
||||
```
|
||||
|
||||
**要点 3: 细节丰富**
|
||||
```
|
||||
- 心理活动更多
|
||||
- 环境描写更细
|
||||
- 对话更深情
|
||||
- 动作更细腻
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 节奏自检清单
|
||||
|
||||
- [ ] **感情推进合理吗**: 是否太快(第1章就在一起)或太慢(50章还暧昧)?
|
||||
- [ ] **每章有重点吗**: 每章是否有明确的情感推进或冲突爆发?
|
||||
- [ ] **甜虐平衡吗**: 甜虐比例是否合理(建议6:4或7:3)?
|
||||
- [ ] **节奏有变化吗**: 是否有快慢交替,避免单调?
|
||||
- [ ] **卡点合理吗**: 付费卡点是否在读者最想看下文的地方?
|
||||
- [ ] **高潮够强吗**: 高潮章节是否情绪饱满,细节丰富?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 言情节奏速查表
|
||||
|
||||
| 节奏类型 | 确认关系章节 | 总章节数 | 甜虐比 | 适用类型 |
|
||||
|---------|-------------|---------|-------|---------|
|
||||
| **快节奏** | 10-20章 | 50-100章 | 7:3 | 甜文、爽文 |
|
||||
| **中速** | 20-40章 | 100-200章 | 6:4 | 标准言情 |
|
||||
| **慢节奏** | 50章+ | 200章+ | 4:6 | 虐文、深情文 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典节奏案例
|
||||
|
||||
### 案例 1: 《何以笙箫默》中速虐恋
|
||||
|
||||
**节奏**:
|
||||
- 前期: 相遇、暗恋(大学时期)
|
||||
- 中期: 误会、分离(七年)
|
||||
- 后期: 重逢、追妻、和好
|
||||
|
||||
**优点**: 虐恋情深,先虐后甜。
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: 反面教材(某扑街文)
|
||||
|
||||
```
|
||||
第1章: 相遇
|
||||
第2章: 表白
|
||||
第3章: 在一起
|
||||
第4-100章: 天天撒糖,没有任何冲突
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 太快,没有感情铺垫
|
||||
- 太甜,没有波折,读者会腻
|
||||
|
||||
---
|
||||
|
||||
## 言情节奏的终极原则
|
||||
|
||||
```
|
||||
快慢结合
|
||||
甜虐交替
|
||||
层层递进
|
||||
高潮迭起
|
||||
```
|
||||
|
||||
**记住**: 节奏是言情小说的生命线。节奏对了,读者就会一直追下去。
|
||||
@@ -0,0 +1,528 @@
|
||||
# 狗血言情经典套路库 (Romance Tropes)
|
||||
|
||||
> **核心原则**: 套路不是问题,会用套路才是王道。读者爱看的不是"新",而是"爽"。经典套路之所以经典,是因为它们能精准击中读者的情感痛点。
|
||||
|
||||
---
|
||||
|
||||
## 1. 十大经典套路
|
||||
|
||||
### 套路 1: 霸道总裁(Dominant CEO)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
霸道总裁 + 灰姑娘女主 = 权力差+身份差 → 碰撞 → 征服 → 甜蜜
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **男主设定**: 年轻有为(28-35岁)、身家过亿、外冷内热、有洁癖/强迫症
|
||||
- **女主设定**: 普通女孩、善良坚强、经济困难、有自尊心
|
||||
- **关键情节**:
|
||||
1. 意外相遇(撞车/救人/误会)
|
||||
2. 契约/协议(假扮女友/代孕/交易)
|
||||
3. 强势追求(堵门/壁咚/强吻)
|
||||
4. 虐恋虐心(误会/分离/疾病)
|
||||
5. 甜蜜结局(表白/婚礼/生子)
|
||||
|
||||
**示例场景**:
|
||||
```markdown
|
||||
【霸道宣言】
|
||||
"女人,你成功引起了我的注意。"
|
||||
陆总裁眼神危险地盯着她。
|
||||
|
||||
【壁咚桥段】
|
||||
他单手撑墙,将她困在怀中。
|
||||
"想逃?晚了。"
|
||||
```
|
||||
|
||||
**经典变体**:
|
||||
- **冷面总裁** × 傻白甜秘书
|
||||
- **腹黑总裁** × 高冷女强人
|
||||
- **病娇总裁** × 治愈系女主
|
||||
|
||||
---
|
||||
|
||||
### 套路 2: 替身文(Substitute)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
女主是白月光替身 → 男主虐待女主 → 女主离开 → 男主追妻火葬场
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **白月光**: 男主心中的完美女神(通常已死/失踪)
|
||||
- **女主**: 长得像白月光,被当成替身
|
||||
- **虐点设计**:
|
||||
1. 男主用女主发泄对白月光的思念
|
||||
2. 男主在女主面前提起白月光
|
||||
3. 男主让女主穿白月光的衣服
|
||||
4. 白月光回归,女主被抛弃
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【虐心对话】
|
||||
"你以为我娶你是因为爱你?不,你只是她的影子。"
|
||||
他冷冷地说,手中握着白月光的照片。
|
||||
|
||||
【追妻火葬场】
|
||||
"我错了,求你回来……"
|
||||
他跪在雨中,但她头也不回地走了。
|
||||
"对不起,你爱的人不是我,我也不爱你了。"
|
||||
```
|
||||
|
||||
**反转技巧**:
|
||||
- **女主黑化**: 从隐忍到报复
|
||||
- **真相揭露**: 白月光其实是绿茶/坏女人
|
||||
- **身份反转**: 女主才是真正的白月光
|
||||
|
||||
---
|
||||
|
||||
### 套路 3: 豪门恩怨(Wealthy Family Drama)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
豪门家族斗争 + 男女主爱情 = 权力游戏 + 情感纠葛
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **家族背景**: 百年豪门/商业帝国
|
||||
- **权力斗争**: 继承权/股权争夺
|
||||
- **情感障碍**: 门不当户不对/家族反对/世仇
|
||||
- **反派角色**: 恶毒长辈/白莲花情敌/腹黑兄弟
|
||||
|
||||
**经典情节**:
|
||||
```markdown
|
||||
【家族反对】
|
||||
"她配不上我们陆家!除非从我尸体上踏过去!"
|
||||
陆老太太拍桌怒吼。
|
||||
|
||||
【世仇化解】
|
||||
"为了你,我可以放弃整个陆氏集团。"
|
||||
他当众宣布,全场哗然。
|
||||
```
|
||||
|
||||
**子套路**:
|
||||
- **联姻**: 家族联姻 → 假戏真做 → 真爱
|
||||
- **复仇**: 女主接近男主为复仇 → 日久生情
|
||||
- **夺权**: 男主夺权 + 女主助攻 → 共同掌权
|
||||
|
||||
---
|
||||
|
||||
### 套路 4: 先婚后爱(Marriage First, Love Later)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
闪婚(契约/协议) → 日常相处 → 日久生情 → 虐点 → 真爱
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **闪婚原因**:
|
||||
- 家族逼婚
|
||||
- 互相利用(继承遗产/拿项目)
|
||||
- 报复前任
|
||||
- 意外怀孕
|
||||
- **相处模式**: 同居不同房 → 逐渐亲密 → 突破底线
|
||||
- **情感转折**: 假装不在意 → 吃醋 → 占有欲爆发
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【契约条款】
|
||||
"第一条:保持距离,各睡各的。"
|
||||
"第二条:一年后离婚,互不干涉。"
|
||||
"第三条……"
|
||||
半年后——
|
||||
"老婆,第一条是不是可以作废了?"
|
||||
他抱着她不放手。
|
||||
|
||||
【吃醋桥段】
|
||||
看到她和别的男人说话,他黑着脸走过去。
|
||||
"陈太太,我们该回家了。"
|
||||
语气危险。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 套路 5: 虐恋情深(Tortured Love)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
相爱 → 误会/背叛 → 虐心分离 → 真相揭露 → HE/BE
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **虐点来源**:
|
||||
- 误会(第三者挑拨/巧合)
|
||||
- 背叛(表面背叛实为保护)
|
||||
- 伤害(堕胎/离婚/囚禁)
|
||||
- 疾病(癌症/失忆/残疾)
|
||||
- **虐恋强度**:
|
||||
- 轻虐: 误会 → 解释 → 和好
|
||||
- 中虐: 分手 → 各自痛苦 → 重逢
|
||||
- 重虐: 生离死别 → 追悔莫及
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【误会虐心】
|
||||
"你亲手杀了我们的孩子……"
|
||||
她泪流满面,眼神死寂。
|
||||
他想解释,却发现所有证据都指向自己。
|
||||
|
||||
【真相揭露】
|
||||
"对不起,都是我的错……"
|
||||
他跪在她的病床前,泪如雨下。
|
||||
但她已经昏迷不醒。
|
||||
```
|
||||
|
||||
**BE vs HE**:
|
||||
- **BE(悲剧结局)**: 女主死亡/失忆/离开,男主追悔终生
|
||||
- **HE(圆满结局)**: 误会解除,重归于好,结婚生子
|
||||
|
||||
---
|
||||
|
||||
### 套路 6: 重生复仇(Rebirth Revenge)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
前世惨死 → 重生回到过去 → 复仇渣男/绿茶 → 遇真爱 → 逆袭
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **前世经历**: 被渣男背叛/被闺蜜陷害/被豪门虐待/惨死
|
||||
- **重生优势**: 知道未来/记得前世/先知先觉
|
||||
- **复仇对象**: 渣男前夫/白莲花姐妹/恶毒婆婆
|
||||
- **真命天子**: 前世被忽略的好男人/新出现的霸总
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【重生觉醒】
|
||||
睁开眼,她发现自己回到了十年前。
|
||||
"这一世,我绝不会重蹈覆辙!"
|
||||
|
||||
【复仇打脸】
|
||||
"还想骗我?你那套把戏,我见得太多了。"
|
||||
她冷笑着拿出证据,渣男脸色惨白。
|
||||
|
||||
【遇到真爱】
|
||||
前世他默默守护她,她却视而不见。
|
||||
这一世,她主动走向他:"我们重新开始,好吗?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 套路 7: 娱乐圈(Entertainment Industry)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
小透明女主 + 影帝/金主男主 = 潜规则传闻 → 官宣 → 打脸黑粉
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **女主设定**: 十八线小演员/新人/替身
|
||||
- **男主设定**: 影帝/导演/娱乐公司总裁
|
||||
- **情节设计**:
|
||||
1. 女主遭遇潜规则/黑料
|
||||
2. 男主霸气护妻
|
||||
3. 女主靠实力逆袭
|
||||
4. 公开恋情,全网祝福
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【潜规则传闻】
|
||||
#某十八线女星陪睡上位#
|
||||
热搜爆了。
|
||||
|
||||
【霸气护妻】
|
||||
影帝亲自下场:
|
||||
"她是我老婆,有意见?"
|
||||
配图:结婚证。
|
||||
全网炸了。
|
||||
|
||||
【实力打脸】
|
||||
女主凭演技拿下影后,黑粉闭嘴。
|
||||
"演技才是硬道理。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 套路 8: 甜宠无虐(Pure Fluff)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
男主宠女主 + 女主被宠 = 全程甜蜜 + 零虐点
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **男主属性**: 宠妻狂魔/妻奴/女儿奴
|
||||
- **女主属性**: 可爱/软萌/治愈系
|
||||
- **甜点设计**:
|
||||
- 每天不同的宠溺方式
|
||||
- 男主为女主做傻事
|
||||
- 秀恩爱虐狗
|
||||
- 生活日常甜蜜
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【宠溺日常】
|
||||
"老婆说的都对。"
|
||||
"老婆想要的,我都给。"
|
||||
"老婆开心最重要。"
|
||||
|
||||
【秀恩爱】
|
||||
记者:"陆总,成功的秘诀是什么?"
|
||||
陆总:"听老婆的话。"
|
||||
全场:"......"
|
||||
```
|
||||
|
||||
**甜点类型**:
|
||||
- **宠溺型**: 男主无条件宠女主
|
||||
- **撒娇型**: 霸总在女主面前撒娇
|
||||
- **反差萌**: 外冷内热,只对女主温柔
|
||||
|
||||
---
|
||||
|
||||
### 套路 9: 年下/姐弟恋(Younger Man Romance)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
成熟女主 + 年轻男主 = 姐弟恋 → 追妻 → 打破世俗
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **年龄差**: 3-10岁
|
||||
- **女主设定**: 成熟独立/事业成功/离婚/丧偶
|
||||
- **男主设定**: 阳光/执着/宠姐狂魔
|
||||
- **障碍**: 世俗眼光/女主自卑/家人反对
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【追求】
|
||||
"姐姐,我喜欢你。"
|
||||
年轻男人认真地说。
|
||||
"你还小……"
|
||||
"我已经成年了,而且,年龄不是问题。"
|
||||
|
||||
【打破偏见】
|
||||
"我爱的是她这个人,不是她的年龄。"
|
||||
他当众宣布,不顾世俗眼光。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 套路 10: 隐婚(Secret Marriage)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
结婚但隐瞒身份 → 各种巧合差点暴露 → 官宣 → 全网震惊
|
||||
```
|
||||
|
||||
**必备元素**:
|
||||
- **隐婚原因**:
|
||||
- 家族反对
|
||||
- 事业考虑(明星)
|
||||
- 低调(富豪)
|
||||
- **险些暴露**:
|
||||
- 被拍到同框
|
||||
- 戴婚戒被发现
|
||||
- 孩子叫妈妈
|
||||
- **官宣方式**:
|
||||
- 意外曝光
|
||||
- 主动公开
|
||||
- 被逼无奈
|
||||
|
||||
**经典桥段**:
|
||||
```markdown
|
||||
【险些暴露】
|
||||
记者:"林小姐,您手上的婚戒……"
|
||||
女主:"哦,这是……道具!"
|
||||
|
||||
【官宣震惊】
|
||||
影帝发微博:
|
||||
"介绍一下,我老婆 @林小姐"
|
||||
配图:结婚证+全家福
|
||||
热搜爆炸。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 套路组合公式
|
||||
|
||||
### 公式 1: 霸总 + 替身 + 追妻火葬场
|
||||
```markdown
|
||||
霸道总裁把女主当替身 → 虐待女主 → 女主离开 → 霸总追妻
|
||||
```
|
||||
**代表作**: 《替身新娘》类
|
||||
|
||||
---
|
||||
|
||||
### 公式 2: 重生 + 复仇 + 真爱
|
||||
```markdown
|
||||
女主重生 → 远离渣男 → 遇到真爱霸总 → 复仇成功 → HE
|
||||
```
|
||||
**代表作**: 《重生之豪门盛宠》类
|
||||
|
||||
---
|
||||
|
||||
### 公式 3: 先婚后爱 + 隐婚 + 甜宠
|
||||
```markdown
|
||||
闪婚 → 隐婚 → 日常甜蜜 → 官宣 → 全网羡慕
|
||||
```
|
||||
**代表作**: 《隐婚甜妻》类
|
||||
|
||||
---
|
||||
|
||||
### 公式 4: 娱乐圈 + 豪门 + 打脸
|
||||
```markdown
|
||||
小演员 × 豪门继承人 → 黑料缠身 → 真相揭露 → 打脸黑粉
|
||||
```
|
||||
**代表作**: 《影后的隐婚老公》类
|
||||
|
||||
---
|
||||
|
||||
### 公式 5: 虐恋 + 误会 + 追悔莫及
|
||||
```markdown
|
||||
相爱 → 误会分手 → 女主离开/假死 → 男主追悔 → HE/BE
|
||||
```
|
||||
**代表作**: 《何以笙箫默》类
|
||||
|
||||
---
|
||||
|
||||
## 3. 套路创新技巧
|
||||
|
||||
### 技巧 1: 性格反转
|
||||
**传统**: 霸道总裁 × 傻白甜
|
||||
**创新**: 沙雕总裁 × 高冷女主
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
【传统霸总】
|
||||
"女人,你成功引起了我的注意。"
|
||||
|
||||
【沙雕总裁】
|
||||
"小姐姐,加个微信呗~"
|
||||
他眨着无辜的大眼睛。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 技巧 2: 身份互换
|
||||
**传统**: 霸总男 × 灰姑娘女
|
||||
**创新**: 霸总女 × 小奶狗男
|
||||
|
||||
---
|
||||
|
||||
### 技巧 3: 背景创新
|
||||
**传统**: 现代都市
|
||||
**创新**:
|
||||
- 民国背景 + 霸总套路
|
||||
- 末世背景 + 甜宠
|
||||
- 古代架空 + 先婚后爱
|
||||
|
||||
---
|
||||
|
||||
### 技巧 4: 多重套路叠加
|
||||
**示例**: 替身 + 重生 + 追妻 + 真假千金
|
||||
```markdown
|
||||
女主前世是替身,被虐死 → 重生发现自己是真千金 →
|
||||
复仇假千金 → 霸总追妻 → 真相揭露 → HE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 套路自检清单
|
||||
|
||||
写完套路后,逐项检查:
|
||||
- [ ] **核心公式清晰**: 读者能一眼看出是什么套路吗?
|
||||
- [ ] **必备元素齐全**: 该套路的关键要素都有吗?
|
||||
- [ ] **情感节奏合理**: 甜虐比例是否合适?(甜宠7:3,虐恋3:7)
|
||||
- [ ] **有创新点吗**: 是否在经典基础上有新意?
|
||||
- [ ] **爽点足够吗**: 每5000字至少1个甜蜜/打脸/逆袭爽点
|
||||
- [ ] **避免俗套吗**: 是否避开了读者最反感的狗血点?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 套路速查表
|
||||
|
||||
| 套路类型 | 核心元素 | 主要爽点 | 适合读者 | 虐恋比例 |
|
||||
|---------|---------|---------|---------|---------|
|
||||
| **霸道总裁** | 权力差+强势追求 | 被宠+壁咚 | 少女向 | 3:7(甜多) |
|
||||
| **替身文** | 白月光+虐恋 | 追妻火葬场 | 虐恋爱好者 | 7:3(虐多) |
|
||||
| **豪门恩怨** | 家族斗争+爱情 | 权谋+护妻 | 爽文爱好者 | 5:5(均衡) |
|
||||
| **先婚后爱** | 契约→真爱 | 日久生情 | 日常向 | 2:8(甜多) |
|
||||
| **虐恋情深** | 误会+伤害 | 虐心+BE/HE | 虐文爱好者 | 8:2(虐为主) |
|
||||
| **重生复仇** | 先知+逆袭 | 打脸渣男 | 爽文爱好者 | 2:8(爽多) |
|
||||
| **娱乐圈** | 明星+八卦 | 官宣+打脸 | 追星族 | 3:7(甜多) |
|
||||
| **甜宠无虐** | 全程宠溺 | 甜蜜+撒糖 | 甜文爱好者 | 0:10(纯甜) |
|
||||
| **姐弟恋** | 年龄差+追妻 | 小狼狗+宠姐 | 成熟女性向 | 2:8(甜多) |
|
||||
| **隐婚** | 秘密+官宣 | 身份反差 | 八卦向 | 1:9(甜多) |
|
||||
|
||||
---
|
||||
|
||||
## 附录:反面教材
|
||||
|
||||
### 错误 1: 套路堆砌,逻辑混乱
|
||||
```markdown
|
||||
女主既是霸总,又是替身,还重生了,同时还是真千金……
|
||||
```
|
||||
**问题**: 太多套路,读者会乱。
|
||||
|
||||
**改进**: 专注1-2个主套路,其他作为辅助。
|
||||
|
||||
---
|
||||
|
||||
### 错误 2: 套路老套,毫无新意
|
||||
```markdown
|
||||
又是霸总壁咚,又是"女人,你成功引起了我的注意"……
|
||||
```
|
||||
**问题**: 2025年了,这些梗已经被用烂了。
|
||||
|
||||
**改进**: 在经典基础上加入新元素(如沙雕、反套路)。
|
||||
|
||||
---
|
||||
|
||||
### 错误 3: 为了套路而套路
|
||||
```markdown
|
||||
明明可以解释清楚的误会,非要拖十章。
|
||||
明明可以直接表白,非要搞契约。
|
||||
```
|
||||
**问题**: 生硬,为了凑字数而用套路。
|
||||
|
||||
**改进**: 套路要服务于剧情,不是剧情服务于套路。
|
||||
|
||||
---
|
||||
|
||||
## 经典案例分析
|
||||
|
||||
### 案例 1: 《何以笙箫默》
|
||||
**套路**: 虐恋情深 + 执着等待
|
||||
**成功点**:
|
||||
- "如果当时……"的遗憾感
|
||||
- 男主执着等待七年
|
||||
- 误会有深度,不狗血
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: 《杉杉来吃》
|
||||
**套路**: 霸道总裁 + 甜宠无虐
|
||||
**成功点**:
|
||||
- 男主人设讨喜(傲娇+宠溺)
|
||||
- 女主不傻白甜,有主见
|
||||
- 日常细节甜蜜
|
||||
|
||||
---
|
||||
|
||||
### 案例 3: 《微微一笑很倾城》
|
||||
**套路**: 校园 + 游戏 + 甜宠
|
||||
**成功点**:
|
||||
- 双学霸人设
|
||||
- 游戏元素新颖
|
||||
- 全程高甜,零狗血
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
套路不是贬义词,而是"读者期待的情节模式"。用好套路的关键:
|
||||
1. **熟悉经典**: 知道读者爱看什么
|
||||
2. **适度创新**: 在经典基础上加新意
|
||||
3. **服务剧情**: 套路为剧情服务,不是反过来
|
||||
4. **控制节奏**: 甜虐比例要合理
|
||||
5. **避免生硬**: 套路要自然,不要为了用而用
|
||||
@@ -0,0 +1,620 @@
|
||||
# 甜蜜场景设计 (Sweet Moments Design)
|
||||
|
||||
> **核心原则**: 甜蜜场景是言情小说的"糖",用来平衡"虐"。甜度要适中,过甜则腻,过淡则无味。读者追文的动力 = 虐后的甜 + 期待下一次的甜。
|
||||
|
||||
---
|
||||
|
||||
## 1. 甜蜜场景的五大类型
|
||||
|
||||
### 类型 1: 宠溺日常(Pet Daily Life)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
男主霸道宠溺 + 女主撒娇/傲娇 = 甜蜜暴击
|
||||
```
|
||||
|
||||
**经典场景**:
|
||||
| 场景 | 男主行为 | 女主反应 | 甜度等级 |
|
||||
|------|---------|---------|---------|
|
||||
| **喂食** | "张嘴。"(亲手喂饭) | "我又不是小孩子……" | ⭐⭐⭐ |
|
||||
| **穿衣** | 帮女主系扣子/系鞋带 | 脸红,推开:"我自己来!" | ⭐⭐⭐ |
|
||||
| **抱抱** | 从背后抱住,下巴搁在肩上 | "你又来……" | ⭐⭐⭐⭐ |
|
||||
| **摸头杀** | 揉她的头:"乖。" | 噘嘴:"别摸我头,长不高!" | ⭐⭐⭐ |
|
||||
| **壁咚** | 将她困在墙角 | 紧张,心跳加速 | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【喂食甜蜜】
|
||||
"张嘴。"林慕寒夹起一块鱼肉,递到她嘴边。
|
||||
"我又不是小孩子……"苏念嘟囔,但还是乖乖张嘴。
|
||||
"嗯,真乖。"他笑了,眼中满是宠溺。
|
||||
|
||||
【壁咚甜蜜】
|
||||
林慕寒突然逼近,双手撑在她身侧,将她困在墙角。
|
||||
"你……你干什么……"苏念紧张地看着他。
|
||||
"你说呢?"他低头,呼吸喷洒在她脸上。
|
||||
苏念的心跳快得像要跳出来。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 2: 肢体接触(Physical Contact)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
意外接触 → 心跳加速 → 暧昧气氛 → 读者尖叫
|
||||
```
|
||||
|
||||
**接触进度阶梯**(循序渐进):
|
||||
```
|
||||
1级: 牵手(初次接触,心跳加速)
|
||||
2级: 搂腰/揽肩(亲密升级)
|
||||
3级: 拥抱(安全感满满)
|
||||
4级: 额头相抵/鼻尖碰触(超近距离)
|
||||
5级: 亲吻(情感爆发)
|
||||
6级: 床戏暗示(点到为止,不描写细节)
|
||||
```
|
||||
|
||||
**各级示例**:
|
||||
|
||||
#### 1级: 牵手
|
||||
```
|
||||
【意外牵手】
|
||||
人群拥挤,林慕寒一把握住她的手。
|
||||
"别走丢了。"他淡淡地说。
|
||||
苏念的脸瞬间红透了。
|
||||
(他……他牵我的手了!)
|
||||
|
||||
【主动牵手】
|
||||
"手给我。"
|
||||
"干嘛?"
|
||||
"怕你跑了。"林慕寒强硬地握住她的手,十指相扣。
|
||||
```
|
||||
|
||||
#### 3级: 拥抱
|
||||
```
|
||||
【保护性拥抱】
|
||||
"别怕,有我在。"
|
||||
林慕寒将她紧紧抱在怀里。
|
||||
苏念闻到他身上淡淡的古龙水香,心跳如鼓。
|
||||
|
||||
【背后拥抱】
|
||||
林慕寒从背后环住她的腰。
|
||||
"你……你别这样……"苏念挣扎。
|
||||
"就抱一会儿,别动。"他的声音低沉温柔。
|
||||
```
|
||||
|
||||
#### 5级: 亲吻
|
||||
```
|
||||
【初吻】
|
||||
林慕寒俯身,吻住了她的唇。
|
||||
苏念瞪大眼睛,大脑一片空白。
|
||||
(他……他吻我了!)
|
||||
|
||||
【霸道吻】
|
||||
"你是我的,别再看别的男人。"
|
||||
话音未落,他捧起她的脸,狠狠吻了下去。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 3: 告白表白(Confession)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
铺垫情绪 → 男主表白 → 女主震惊/感动 → 确认关系
|
||||
```
|
||||
|
||||
**经典告白类型**:
|
||||
|
||||
#### A型: 霸道告白
|
||||
```
|
||||
"苏念,我喜欢你。"
|
||||
"什么……?"
|
||||
"从见到你的第一天起,我就喜欢上你了。"
|
||||
林慕寒将她拥入怀中。
|
||||
"所以,你只能是我的。"
|
||||
```
|
||||
|
||||
#### B型: 深情告白
|
||||
```
|
||||
"苏念,你知道吗?"
|
||||
"什么?"
|
||||
"我这辈子,只爱过你一个人。"
|
||||
他看着她的眼睛,眼中满是深情。
|
||||
"能不能……给我一个机会?"
|
||||
```
|
||||
|
||||
#### C型: 行动告白(不说爱,用行动证明)
|
||||
```
|
||||
"你为什么对我这么好?"苏念问。
|
||||
林慕寒没有回答,只是揉了揉她的头。
|
||||
"傻瓜。"
|
||||
(因为我爱你啊……)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 4: 吃醋撒娇(Jealousy & Acting Cute)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
男主吃醋 → 占有欲爆发 → 霸道宣示主权 → 甜蜜
|
||||
```
|
||||
|
||||
**男主吃醋场景**:
|
||||
```
|
||||
【吃醋1.0】
|
||||
"你刚才为什么和那个男人说话?"
|
||||
"他问我路……"
|
||||
"以后不许和别的男人说话。"
|
||||
"这也太霸道了吧!"
|
||||
"我就是霸道。"他搂住她的腰,"你只能是我的。"
|
||||
|
||||
【吃醋2.0(醋王上线)】
|
||||
"谁允许你笑得这么甜?"
|
||||
"啊?"苏念懵了。
|
||||
"对别的男人,不许笑。"
|
||||
"……你这是什么逻辑?"
|
||||
"我的逻辑。"林慕寒霸道地吻住她。
|
||||
```
|
||||
|
||||
**女主撒娇场景**:
|
||||
```
|
||||
【撒娇要抱抱】
|
||||
"林慕寒……"苏念拉着他的袖子。
|
||||
"嗯?"
|
||||
"抱抱……"她小声说,脸红得像苹果。
|
||||
林慕寒心都化了,将她抱进怀里。
|
||||
"怎么这么可爱……"
|
||||
|
||||
【撒娇求原谅】
|
||||
"我错了嘛……"苏念抱住他的胳膊,眨着大眼睛。
|
||||
"……"林慕寒别过脸,"别以为撒娇就有用。"
|
||||
"那你还生气吗?"
|
||||
"……不生了。"他败下阵来。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型 5: 英雄救美(Hero Saves Beauty)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
女主遇险 → 男主霸气登场 → 保护女主 → 女主芳心暗许
|
||||
```
|
||||
|
||||
**经典场景**:
|
||||
|
||||
#### 场景A: 救场(社交场合)
|
||||
```
|
||||
"苏小姐,不如陪我喝一杯?"油腻男人纠缠。
|
||||
"不好意思,我男朋友在等我。"苏念推辞。
|
||||
"你男朋友是谁啊?"
|
||||
就在这时,林慕寒走过来,揽住苏念的腰。
|
||||
"我就是她男朋友。有问题吗?"
|
||||
他的眼神冰冷,油腻男人吓得连连后退。
|
||||
```
|
||||
|
||||
#### 场景B: 救命(危险场合)
|
||||
```
|
||||
【绑架救援】
|
||||
"放开她!"林慕寒踹开门,眼中满是杀意。
|
||||
"林……林慕寒……"苏念哭着叫他。
|
||||
"别怕,我来了。"
|
||||
他三两下解决了绑匪,将她紧紧抱住。
|
||||
"对不起,让你受苦了……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 甜蜜场景的阶段分布
|
||||
|
||||
### 初期(暧昧期): 小甜饼
|
||||
|
||||
**特点**: 试探、脸红、心跳加速、若即若离
|
||||
|
||||
**甜蜜方式**:
|
||||
- 意外身体接触(撞一起、牵手)
|
||||
- 眼神对视(对视3秒,脸红移开)
|
||||
- 小心机(故意接近、制造偶遇)
|
||||
- 保护(挡雨、挡危险)
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【意外接触】
|
||||
地铁突然刹车,苏念一个踉跄,撞进林慕寒怀里。
|
||||
"对……对不起……"她慌忙退开,脸红得像煮熟的虾。
|
||||
"小心点。"他扶住她,嘴角微微上扬。
|
||||
|
||||
【眼神杀】
|
||||
苏念抬头,正好对上林慕寒的眼睛。
|
||||
两人对视了三秒。
|
||||
她慌忙移开视线,心跳如鼓。
|
||||
(他的眼睛……好好看……)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 中期(恋爱期): 日常甜宠
|
||||
|
||||
**特点**: 确认关系,公开秀恩爱,宠溺互动
|
||||
|
||||
**甜蜜方式**:
|
||||
- 日常互动(喂饭、摸头、抱抱)
|
||||
- 小惊喜(送花、送礼物、准备惊喜)
|
||||
- 吃醋撒娇(占有欲、争风吃醋)
|
||||
- 亲密接触(亲吻、拥抱、牵手)
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【早安吻】
|
||||
苏念刚睡醒,迷迷糊糊睁开眼。
|
||||
林慕寒俯身,在她额头上印下一吻。
|
||||
"早安,我的宝贝。"
|
||||
"唔……"她脸红,把头埋进被子里。
|
||||
|
||||
【喂药甜蜜】
|
||||
"张嘴。"林慕寒端着药。
|
||||
"好苦……"
|
||||
"乖,喝了就给你吃糖。"
|
||||
"我又不是小孩子……"
|
||||
"那你喝不喝?"
|
||||
"……喝。"苏念妥协。
|
||||
喝完药,林慕寒递给她一颗糖。
|
||||
"真乖。"他揉她的头。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 后期(婚后/稳定期): 细水长流
|
||||
|
||||
**特点**: 生活化甜蜜,老夫老妻的温馨
|
||||
|
||||
**甜蜜方式**:
|
||||
- 生活互动(做饭、洗碗、看电视)
|
||||
- 小情趣(突然的吻、拥抱、情话)
|
||||
- 纪念日(结婚纪念日、第一次见面纪念日)
|
||||
- 孩子(有了孩子后的家庭温馨)
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【做饭甜蜜】
|
||||
"老公,帮我尝尝咸不咸。"
|
||||
林慕寒走过来,尝了一口。
|
||||
"嗯,刚好。"
|
||||
"那就好~"苏念笑。
|
||||
林慕寒突然从背后抱住她。
|
||||
"不过,还是你比较甜。"
|
||||
|
||||
【睡前甜蜜】
|
||||
"困了吗?"林慕寒问。
|
||||
"嗯……"苏念窝在他怀里。
|
||||
"睡吧,晚安。"他吻了吻她的额头。
|
||||
"晚安……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 甜蜜对话模板
|
||||
|
||||
### 模板 1: 情话攻击
|
||||
|
||||
**男主情话**:
|
||||
```
|
||||
"你知道你最大的缺点是什么吗?"
|
||||
"什么?"
|
||||
"太可爱了。"
|
||||
|
||||
"你猜我想吃什么?"
|
||||
"什么?"
|
||||
"痴痴地看着你。"
|
||||
|
||||
"你是我的什么?"
|
||||
"……女朋友?"
|
||||
"不,你是我的命。"
|
||||
```
|
||||
|
||||
**女主害羞回应**:
|
||||
```
|
||||
"你……你别贫嘴!"(脸红)
|
||||
"油嘴滑舌……"(嘴角上扬)
|
||||
"讨厌……"(小声)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板 2: 宠溺对话
|
||||
|
||||
```
|
||||
【宠溺1】
|
||||
女主:"我想吃冰淇淋。"
|
||||
男主:"不行,你感冒了。"
|
||||
女主(撒娇):"就吃一口嘛……"
|
||||
男主(败下阵来):"……只能一口。"
|
||||
女主(开心):"好!"
|
||||
|
||||
【宠溺2】
|
||||
女主:"你为什么对我这么好?"
|
||||
男主:"因为你是我的女人。"
|
||||
女主(脸红):"……"
|
||||
男主(揉头):"傻瓜。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板 3: 占有欲对话
|
||||
|
||||
```
|
||||
【占有欲1】
|
||||
男主:"你是谁的?"
|
||||
女主:"……你的。"
|
||||
男主:"大声点。"
|
||||
女主(脸红):"我是你的!"
|
||||
男主(满意):"嗯,真乖。"
|
||||
|
||||
【占有欲2】
|
||||
男主:"记住,你只能是我的。"
|
||||
女主:"我知道啦……"
|
||||
男主:"别的男人,看都不许看。"
|
||||
女主:"这也太霸道了吧?"
|
||||
男主:"我就是霸道。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 身体接触的进度设计
|
||||
|
||||
### 进度控制原则
|
||||
|
||||
**禁忌**: 太快太猛(第1章就床戏)
|
||||
**正确**: 循序渐进,层层递进
|
||||
|
||||
**标准进度表**:
|
||||
```
|
||||
第1-10章: 意外接触(撞一起、牵手)
|
||||
第11-20章: 主动接触(搂腰、揽肩、摸头)
|
||||
第21-30章: 亲密接触(拥抱、额头相抵)
|
||||
第31-40章: 情感爆发(亲吻)
|
||||
第41章+: 更进一步(暗示即可,不细写)
|
||||
```
|
||||
|
||||
**每次接触后的反应**:
|
||||
```
|
||||
【第一次牵手】
|
||||
苏念的心跳快得要跳出来。
|
||||
(他……他牵我的手了!)
|
||||
她偷偷看他,他却面不改色。
|
||||
|
||||
【第一次亲吻】
|
||||
苏念大脑一片空白,浑身僵硬。
|
||||
林慕寒松开她,额头抵着她的额头。
|
||||
"别紧张……"他低声说。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 甜蜜场景的节奏控制
|
||||
|
||||
### 黄金比例
|
||||
|
||||
**甜虐比**: 6:4 或 7:3(甜多虐少,但要有虐)
|
||||
|
||||
**章节分布**:
|
||||
```
|
||||
连续甜: 3-5章(不要超过5章,会腻)
|
||||
连续虐: 2-3章(不要超过3章,会弃文)
|
||||
```
|
||||
|
||||
**示例节奏**:
|
||||
```
|
||||
第1-5章: 甜(确认关系,日常宠溺)
|
||||
第6-8章: 虐(误会,冷战)
|
||||
第9-12章: 甜(和好,更甜蜜)
|
||||
第13-15章: 虐(第三者出现)
|
||||
第16-25章: 甜(表态,虐后加倍甜)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 甜蜜强度递增
|
||||
|
||||
**原则**: 后面的甜要比前面更甜
|
||||
|
||||
```
|
||||
初期甜蜜: ⭐⭐⭐(牵手、脸红)
|
||||
中期甜蜜: ⭐⭐⭐⭐(拥抱、亲吻)
|
||||
后期甜蜜: ⭐⭐⭐⭐⭐(表白、确认关系、公开)
|
||||
高潮甜蜜: ⭐⭐⭐⭐⭐⭐(求婚、结婚、生子)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 避免过度甜腻
|
||||
|
||||
### 反面教材(太甜太腻)
|
||||
|
||||
```
|
||||
【过度甜腻】
|
||||
第1-50章:
|
||||
- 男主每天对女主各种宠
|
||||
- 没有任何矛盾
|
||||
- 天天撒糖
|
||||
- 读者:无聊,弃文
|
||||
|
||||
【正确做法】
|
||||
甜中带虐,虐后更甜。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 甜蜜场景的"留白"
|
||||
|
||||
**不要什么都写**:
|
||||
```
|
||||
❌ 错误: 描写他们从早上起床到晚上睡觉的每一个细节
|
||||
✅ 正确: 只写关键的甜蜜时刻,其余跳过
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【留白示例】
|
||||
两人逛了一下午街,买了很多东西。
|
||||
【跳过逛街细节,直接到晚餐】
|
||||
晚餐时,林慕寒给她夹菜。
|
||||
"多吃点,太瘦了。"
|
||||
"我不瘦……"苏念嘟囔。
|
||||
"在我眼里就是瘦。"他坚持。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 特殊甜蜜场景
|
||||
|
||||
### 场景 A: 生病照顾
|
||||
|
||||
```
|
||||
【女主生病】
|
||||
苏念发高烧,迷迷糊糊睡着了。
|
||||
林慕寒一夜没睡,守在床边。
|
||||
天亮时,她睁开眼,看到的是他红肿的眼睛。
|
||||
"你……一夜没睡?"
|
||||
"嗯。"他握住她的手,"吓死我了……"
|
||||
|
||||
【男主生病(反差萌)】
|
||||
"我……好难受……"林慕寒趴在床上,像个小孩子。
|
||||
苏念哭笑不得:"不就是感冒吗……"
|
||||
"你要照顾我……"他拉住她的手。
|
||||
"好好好,我照顾你。"她无奈。
|
||||
(平时这么霸道,生病了就变小孩……)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景 B: 纪念日惊喜
|
||||
|
||||
```
|
||||
【生日惊喜】
|
||||
"生日快乐!"
|
||||
林慕寒打开灯,房间里摆满了玫瑰花。
|
||||
苏念捂住嘴,眼泪止不住流下来。
|
||||
"你……你什么时候准备的……"
|
||||
"早就准备了。"他走过来,将她拥入怀中。
|
||||
"喜欢吗?"
|
||||
"喜欢……太喜欢了……"
|
||||
|
||||
【求婚场景】
|
||||
"苏念,嫁给我。"
|
||||
林慕寒单膝跪地,手里捧着戒指。
|
||||
周围的人都在起哄:"嫁给他!嫁给他!"
|
||||
苏念哭着点头:"我愿意……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景 C: 保护女主(霸气护短)
|
||||
|
||||
```
|
||||
【护短1.0】
|
||||
"她是我的女人,谁敢动她?"
|
||||
林慕寒挡在苏念面前,眼中满是杀意。
|
||||
|
||||
【护短2.0】
|
||||
"你欺负她,就是欺负我。"
|
||||
林慕寒冷冷地看着对方。
|
||||
"识相的话,现在道歉。"
|
||||
|
||||
【护短3.0(极致)】
|
||||
"动她一根头发,我灭你全家。"
|
||||
全场寂静。
|
||||
没人敢动。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 甜蜜场景自检清单
|
||||
|
||||
- [ ] **有铺垫吗**: 甜蜜场景是否有情绪铺垫?(不能突兀)
|
||||
- [ ] **符合人设吗**: 男主/女主的反应是否符合其性格?
|
||||
- [ ] **节奏合理吗**: 甜蜜强度是否递增?是否有甜虐交替?
|
||||
- [ ] **够细腻吗**: 是否描写了心理活动和身体反应?(脸红、心跳)
|
||||
- [ ] **不过度吗**: 是否避免了连续5章以上的纯甜?
|
||||
- [ ] **有新意吗**: 这次的甜蜜和上次有什么不同?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 甜蜜场景速查表
|
||||
|
||||
| 甜蜜类型 | 甜度 | 适用阶段 | 关键元素 | 示例 |
|
||||
|---------|------|---------|---------|------|
|
||||
| **日常宠溺** | ⭐⭐⭐ | 全阶段 | 喂食、摸头、抱抱 | "张嘴。" |
|
||||
| **肢体接触** | ⭐⭐⭐⭐ | 暧昧期+ | 牵手、拥抱、亲吻 | 十指相扣 |
|
||||
| **告白表白** | ⭐⭐⭐⭐⭐ | 关键节点 | 深情眼神、情话 | "我喜欢你。" |
|
||||
| **吃醋撒娇** | ⭐⭐⭐⭐ | 恋爱期 | 占有欲、霸道宣示 | "你是我的!" |
|
||||
| **英雄救美** | ⭐⭐⭐⭐⭐ | 危机时刻 | 保护、霸气登场 | "谁敢动她?" |
|
||||
| **纪念日惊喜** | ⭐⭐⭐⭐⭐⭐ | 高潮 | 惊喜、感动流泪 | 求婚、生日 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典甜蜜场景案例
|
||||
|
||||
### 案例 1: 《何以笙箫默》壁咚名场面
|
||||
|
||||
```
|
||||
何以琛将赵默笙困在墙角。
|
||||
"默笙,你为什么不回来?"
|
||||
"我……"
|
||||
"你知不知道我等了你七年?"
|
||||
他的眼中满是深情。
|
||||
赵默笙的眼泪滑落。
|
||||
```
|
||||
|
||||
**优点**: 深情+霸道,直击读者内心。
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: 反面教材(某扑街文)
|
||||
|
||||
```
|
||||
第1章: 男主对女主好
|
||||
第2章: 男主对女主好
|
||||
第3章: 男主对女主好
|
||||
……
|
||||
第50章: 男主对女主好
|
||||
```
|
||||
|
||||
**问题**: 一直甜,没有任何波折,读者看腻了。
|
||||
|
||||
---
|
||||
|
||||
## 甜蜜场景的终极公式
|
||||
|
||||
```
|
||||
铺垫情绪(心理活动)
|
||||
↓
|
||||
甜蜜互动(对话+动作)
|
||||
↓
|
||||
身体反应(脸红、心跳)
|
||||
↓
|
||||
留白(戛然而止,留悬念)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【铺垫】
|
||||
苏念有些紧张,不敢看他。
|
||||
(他会不会……)
|
||||
|
||||
【互动】
|
||||
林慕寒突然握住她的手。
|
||||
"跟我在一起,好吗?"
|
||||
|
||||
【反应】
|
||||
苏念的心跳快得要跳出来。
|
||||
她小声说:"……好。"
|
||||
|
||||
【留白】
|
||||
林慕寒笑了,将她拥入怀中。
|
||||
夕阳的余晖洒在两人身上……
|
||||
(后续留给读者想象)
|
||||
```
|
||||
@@ -0,0 +1,612 @@
|
||||
# 虐点设计与复仇快感 (Torture Points & Revenge Satisfaction)
|
||||
|
||||
> **核心原则**: 虐是为了后面更甜。虐要虐得有理有据,虐完必须给出解释和补偿。读者能接受虐,但不能接受"虐而不解"或"为虐而虐"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 虐点的五大类型
|
||||
|
||||
### 类型 1: 误会虐(Misunderstanding Torture)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
关键误会 → 男主冷暴力/惩罚女主 → 女主心碎 → 真相大白 → 追妻火葬场
|
||||
```
|
||||
|
||||
**经典误会来源**:
|
||||
| 误会类型 | 表象 | 真相 | 虐点 |
|
||||
|---------|------|------|------|
|
||||
| **出轨误会** | 男主和别的女人暧昧 | 那女人是妹妹/救命恩人 | 女主伤心欲绝 |
|
||||
| **背叛误会** | 女主被陷害"背叛"男主 | 女主是被迫/被陷害的 | 男主报复女主 |
|
||||
| **身份误会** | 女主隐瞒真实身份 | 女主有苦衷 | 男主觉得被欺骗 |
|
||||
| **目的误会** | 男主以为女主接近他有目的 | 女主只是单纯喜欢他 | 男主冷酷对待 |
|
||||
|
||||
**虐点爆发模板**:
|
||||
```
|
||||
【误会产生】
|
||||
苏念亲眼看到林慕寒和一个美女拥抱。
|
||||
她的心,碎了一地。
|
||||
(原来……他有别的女人了……)
|
||||
|
||||
【误会加深】
|
||||
"林慕寒,我们分手吧。"
|
||||
"为什么?"他皱眉。
|
||||
"因为……我配不上你。"(真实原因:以为他有别的女人)
|
||||
"好。"他冷冷地说,转身离开。
|
||||
|
||||
【真相揭晓】
|
||||
"她是我妹妹!我唯一的妹妹!"
|
||||
林慕寒抓住她的肩膀,眼中满是急切。
|
||||
"苏念,你怎么能因为这个误会就要离开我?"
|
||||
苏念愣住了。
|
||||
(妹妹……?我误会他了……)
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐(中高强度)
|
||||
|
||||
---
|
||||
|
||||
### 类型 2: 冷暴力虐(Cold Violence Torture)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
男主因误会对女主冷暴力 → 女主委屈隐忍 → 积累到极限 → 女主离开 → 男主后悔
|
||||
```
|
||||
|
||||
**冷暴力表现形式**:
|
||||
```
|
||||
1. 语言冷漠: "我很忙。" / "你走吧。" / "别烦我。"
|
||||
2. 视而不见: 女主在他面前,他完全当她不存在
|
||||
3. 拒绝接触: 不让女主碰他,不回家,不接电话
|
||||
4. 言语伤害: "你以为你是谁?" / "我从来没爱过你。"
|
||||
```
|
||||
|
||||
**虐点爆发模板**:
|
||||
```
|
||||
【冷暴力开始】
|
||||
"林慕寒……"苏念想拉他的手。
|
||||
他冷冷地甩开:"别碰我。"
|
||||
苏念的手僵在半空,眼泪滑落。
|
||||
|
||||
【冷暴力升级】
|
||||
苏念做好晚饭等他,但他根本不回家。
|
||||
她打电话,他挂断。
|
||||
她发消息,他不回。
|
||||
她等到深夜,一个人哭着吃完冷掉的饭菜。
|
||||
|
||||
【女主极限】
|
||||
"够了……我真的……撑不下去了……"
|
||||
苏念收拾行李,泪流满面。
|
||||
"林慕寒,我走了。这次,真的走了。"
|
||||
她留下一封信,离开了这个城市。
|
||||
|
||||
【男主后悔】
|
||||
林慕寒看到信,脸色惨白。
|
||||
"苏念……"
|
||||
他疯狂地找她,但她已经消失了。
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐⭐(超高强度)
|
||||
|
||||
---
|
||||
|
||||
### 类型 3: 第三者虐(Third Party Torture)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
第三者出现 → 威胁女主地位 → 女主危机感 → 男主表态(或暧昧) → 女主心碎
|
||||
```
|
||||
|
||||
**第三者类型**:
|
||||
| 类型 | 设定 | 虐点 | 结局 |
|
||||
|------|------|------|------|
|
||||
| **白月光** | 男主初恋回来了 | 女主自卑:"我比不上她" | 男主表态:"过去的就过去了" |
|
||||
| **门当户对** | 家族安排的联姻对象 | 家族施压 | 男主对抗家族 |
|
||||
| **青梅竹马** | 从小一起长大 | 有共同回忆 | 男主澄清:"她只是朋友" |
|
||||
| **暗恋者** | 一直守护女主的人 | 男主吃醋 | 女主拒绝,选择男主 |
|
||||
|
||||
**虐点爆发模板**:
|
||||
```
|
||||
【第三者登场】
|
||||
"慕寒哥哥,好久不见~"
|
||||
一个精致的女人走进来,亲昵地挽住林慕寒的手臂。
|
||||
苏念的心,揪了起来。
|
||||
(她是谁……为什么这么亲密……)
|
||||
|
||||
【女主危机感】
|
||||
"她是你的初恋吧?"
|
||||
"……是。"林慕寒沉默。
|
||||
"那你们……"
|
||||
"过去的事了。"他淡淡地说。
|
||||
但苏念还是不安……
|
||||
(他……还爱着她吗……)
|
||||
|
||||
【虐点爆发】
|
||||
那女人故意在苏念面前秀恩爱。
|
||||
"我和慕寒从小一起长大,感情可好了~"
|
||||
"我们还一起……"
|
||||
"够了!"苏念忍不住了。
|
||||
"对不起,我……我先走了……"
|
||||
她转身逃走,眼泪止不住地流。
|
||||
|
||||
【男主表态】
|
||||
林慕寒当着所有人的面,牵起苏念的手。
|
||||
"我只爱她。过去的,就让它过去吧。"
|
||||
他看着那个女人:"以后别再来找我了。"
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐(中高强度)
|
||||
|
||||
---
|
||||
|
||||
### 类型 4: 生离死别虐(Life & Death Torture)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
女主/男主遇到生命危险 → 另一方崩溃 → 拼命救 → 劫后余生 → 更珍惜
|
||||
```
|
||||
|
||||
**生死场景**:
|
||||
```
|
||||
1. 意外: 车祸、坠崖、溺水
|
||||
2. 疾病: 绝症、失忆、植物人
|
||||
3. 牺牲: 为保护对方而受伤
|
||||
4. 绑架: 被坏人绑架,生死未卜
|
||||
```
|
||||
|
||||
**虐点爆发模板**:
|
||||
```
|
||||
【女主遇险】
|
||||
"苏念!"
|
||||
林慕寒看到她从楼上掉下去,眼睛瞬间红了。
|
||||
他飞奔过去,接住了她。
|
||||
但她已经昏迷,鲜血染红了他的衣服。
|
||||
"苏念……别死……求你别死……"
|
||||
他的泪水滴在她脸上。
|
||||
|
||||
【医院抢救】
|
||||
手术进行了八个小时。
|
||||
林慕寒在门外等待,浑身颤抖。
|
||||
(如果她有事……我也不活了……)
|
||||
|
||||
【劫后余生】
|
||||
"她醒了!"
|
||||
林慕寒冲进病房,紧紧抱住她。
|
||||
"苏念……你终于醒了……"
|
||||
他的声音哽咽,眼中满是劫后余生的庆幸。
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐⭐⭐(极致强度)
|
||||
|
||||
---
|
||||
|
||||
### 类型 5: 身份悬殊虐(Identity Gap Torture)
|
||||
|
||||
**核心公式**:
|
||||
```
|
||||
身份差距 → 女主自卑 → 外界压力 → 女主想离开 → 男主霸道留下
|
||||
```
|
||||
|
||||
**身份差距来源**:
|
||||
```
|
||||
- 金钱: 男主亿万富翁 vs 女主穷困潦倒
|
||||
- 地位: 男主总裁/王爷 vs 女主员工/平民
|
||||
- 家世: 男主豪门继承人 vs 女主孤儿
|
||||
- 能力: 男主天才 vs 女主普通人
|
||||
```
|
||||
|
||||
**虐点爆发模板**:
|
||||
```
|
||||
【外界压力】
|
||||
"你配不上我儿子!"林慕寒的母亲扔给她一张支票。
|
||||
"拿着钱,离开他。"
|
||||
苏念咬着嘴唇,泪水滑落。
|
||||
(她说得对……我确实配不上他……)
|
||||
|
||||
【女主自卑】
|
||||
"林慕寒,我们分手吧。"
|
||||
"为什么?"
|
||||
"因为……我配不上你。"
|
||||
"配不上?"他冷笑,"谁说的?"
|
||||
"你母亲说得对……我只是一个普通人……"
|
||||
"那又怎样?"他抓住她的肩膀。
|
||||
"苏念,我爱的是你,不是你的身份!"
|
||||
|
||||
【男主霸道】
|
||||
"我不管别人怎么说。"
|
||||
"我只知道,你是我的。"
|
||||
林慕寒将她紧紧抱住。
|
||||
"谁都不能把你从我身边带走。"
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐(中高强度)
|
||||
|
||||
---
|
||||
|
||||
## 2. 虐点的强度分级
|
||||
|
||||
### 1级虐(日常小委屈)
|
||||
|
||||
**特点**: 小误会、小吃醋、小赌气
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"你为什么不接我电话?"苏念噘嘴。
|
||||
"开会,手机静音了。"林慕寒无奈。
|
||||
"哼,你就是不在乎我!"
|
||||
"好好好,我的错。"他把她抱进怀里。
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐
|
||||
**作用**: 调节气氛,增加小情趣
|
||||
|
||||
---
|
||||
|
||||
### 2级虐(明显矛盾)
|
||||
|
||||
**特点**: 冷战、赌气、小分手
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"苏念,你别无理取闹!"
|
||||
"我无理取闹?那你去找你的白月光吧!"
|
||||
"你……!"
|
||||
苏念摔门而出。
|
||||
林慕寒站在原地,懊恼地扯了扯领带。
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐
|
||||
**作用**: 制造波折,推动剧情
|
||||
|
||||
---
|
||||
|
||||
### 3级虐(严重危机)
|
||||
|
||||
**特点**: 分手、背叛、误会爆发
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"林慕寒,我们分手吧。"
|
||||
"为什么?!"
|
||||
"因为……我们不合适。"
|
||||
"不合适?你昨天还说爱我!"
|
||||
"我……我不爱了。"
|
||||
她转身离去,留下他一人站在雨中。
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐⭐
|
||||
**作用**: 情节高潮,虐心虐肺
|
||||
|
||||
---
|
||||
|
||||
### 4级虐(生死相隔)
|
||||
|
||||
**特点**: 意外、疾病、生离死别
|
||||
|
||||
**示例**:
|
||||
```
|
||||
"林慕寒……如果我死了……你会想我吗?"
|
||||
"别说傻话!你不会有事的!"
|
||||
他紧紧抱住她,声音颤抖。
|
||||
"医生!医生!求你救救她!"
|
||||
```
|
||||
|
||||
**虐点强度**: ⭐⭐⭐⭐⭐⭐
|
||||
**作用**: 极致虐点,情感爆发
|
||||
|
||||
---
|
||||
|
||||
## 3. 虐点的节奏控制
|
||||
|
||||
### 黄金虐点公式
|
||||
|
||||
```
|
||||
甜(5-10章) → 虐(2-5章) → 甜(5-10章) → 虐(3-8章) → 甜(10-20章)
|
||||
```
|
||||
|
||||
**原则**:
|
||||
- 虐完必甜,虐不过三
|
||||
- 虐点不要拖太久(最多10-15章解开)
|
||||
- 虐后的甜要比虐前更甜
|
||||
|
||||
---
|
||||
|
||||
### 错误节奏(禁忌)
|
||||
|
||||
#### 禁忌 1: 虐而不解
|
||||
```
|
||||
第10章: 男主误会女主
|
||||
第20章: 还在误会
|
||||
第30章: 继续误会
|
||||
第40章: 依然误会
|
||||
```
|
||||
**问题**: 读者会骂:"真相什么时候大白?!"
|
||||
|
||||
#### 禁忌 2: 为虐而虐
|
||||
```
|
||||
第10章: 误会→分手
|
||||
第15章: 和好
|
||||
第20章: 又误会→又分手
|
||||
第25章: 又和好
|
||||
第30章: 又误会→又分手
|
||||
```
|
||||
**问题**: 重复套路,读者会腻。
|
||||
|
||||
#### 禁忌 3: 虐过头不补偿
|
||||
```
|
||||
男主一直虐女主,从头虐到尾。
|
||||
女主被虐得死去活来。
|
||||
最后男主道个歉,女主就原谅了。
|
||||
```
|
||||
**问题**: 补偿不足,读者不买账。
|
||||
|
||||
---
|
||||
|
||||
### 正确节奏示例
|
||||
|
||||
```
|
||||
第1-10章: 甜蜜日常,感情升温
|
||||
第11章: 第三者出现(虐点预警)
|
||||
第12-15章: 第三者挑拨,女主危机感(虐)
|
||||
第16章: 男主表态,赶走第三者(甜)
|
||||
第17-25章: 感情更稳定,加倍甜蜜(补偿)
|
||||
第26章: 男主母亲反对(虐点预警)
|
||||
第27-32章: 家族施压,女主想离开(虐)
|
||||
第33章: 男主对抗家族,保护女主(甜)
|
||||
第34-50章: 结婚,超级甜(大补偿)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 追妻火葬场(虐后补偿)
|
||||
|
||||
### 火葬场公式
|
||||
|
||||
```
|
||||
男主犯错 → 女主离开 → 男主后悔 → 疯狂追妻 → 女主原谅
|
||||
```
|
||||
|
||||
**追妻火葬场必备元素**:
|
||||
1. 男主真诚道歉
|
||||
2. 男主疯狂补偿(送花、跪求、各种哄)
|
||||
3. 男主改变(从冷酷变温柔)
|
||||
4. 真相大白(误会解开)
|
||||
5. 女主心软(但不能太快原谅)
|
||||
|
||||
---
|
||||
|
||||
### 火葬场强度分级
|
||||
|
||||
#### Level 1: 轻度火葬场(道歉+送花)
|
||||
|
||||
```
|
||||
"苏念,对不起。"
|
||||
林慕寒捧着一大束玫瑰花。
|
||||
"是我错了,原谅我好吗?"
|
||||
苏念别过脸:"……你走吧。"
|
||||
"我不走。"他固执地站在门外。
|
||||
"你不原谅我,我就一直站在这里。"
|
||||
```
|
||||
|
||||
#### Level 2: 中度火葬场(下跪+求原谅)
|
||||
|
||||
```
|
||||
"苏念,求你原谅我……"
|
||||
林慕寒跪在她面前,眼中满是悔恨。
|
||||
"当初是我瞎了眼,是我不信你……"
|
||||
"求你……再给我一次机会……"
|
||||
苏念转过身,泪流满面。
|
||||
"林慕寒……你让我怎么原谅你……"
|
||||
```
|
||||
|
||||
#### Level 3: 重度火葬场(自残+生死相逼)
|
||||
|
||||
```
|
||||
"苏念,如果你不原谅我……"
|
||||
林慕寒拿起刀,抵在自己心口。
|
||||
"我就死在你面前!"
|
||||
"你疯了吗!"苏念慌了。
|
||||
"我不疯,我只是……真的很爱你……"
|
||||
他的泪水滑落。
|
||||
"没有你……我活着还有什么意义……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 火葬场的甜度补偿
|
||||
|
||||
**原则**: 虐得有多深,补偿就要有多甜
|
||||
|
||||
```
|
||||
1级虐 → 补偿: 道歉+小礼物
|
||||
2级虐 → 补偿: 道歉+送花+陪伴
|
||||
3级虐 → 补偿: 道歉+下跪+疯狂追求+公开表白
|
||||
4级虐 → 补偿: 道歉+下跪+以命相逼+结婚+生子
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 虐点的细节描写
|
||||
|
||||
### 女主哭泣描写
|
||||
|
||||
**错误示例(流水账)**:
|
||||
```
|
||||
苏念哭了。
|
||||
她哭得很伤心。
|
||||
```
|
||||
|
||||
**正确示例(细节丰富)**:
|
||||
```
|
||||
苏念咬着嘴唇,拼命忍住。
|
||||
但泪水还是不争气地滑落。
|
||||
她蹲在地上,肩膀剧烈颤抖。
|
||||
"为什么……为什么要这样对我……"
|
||||
她哭得撕心裂肺。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 男主后悔描写
|
||||
|
||||
**错误示例(告诉式)**:
|
||||
```
|
||||
林慕寒很后悔。
|
||||
```
|
||||
|
||||
**正确示例(展示式)**:
|
||||
```
|
||||
林慕寒坐在空荡荡的房间里,手里拿着她留下的信。
|
||||
"苏念……"
|
||||
他的手颤抖,眼睛发红。
|
||||
(我到底做了什么……)
|
||||
(我怎么能这样对她……)
|
||||
他第一次感受到,什么叫后悔。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 冷暴力细节
|
||||
|
||||
```
|
||||
【冷暴力的杀伤力】
|
||||
苏念做好晚饭,等他回家。
|
||||
但他根本不回来。
|
||||
她打电话,他挂断。
|
||||
她发消息,他不回。
|
||||
她一个人坐在餐桌前,看着冷掉的饭菜。
|
||||
泪水滴在碗里。
|
||||
"为什么……为什么要这样……"
|
||||
她哭着吃完所有的饭菜。
|
||||
(也许……他真的不爱我了……)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 虐点与复仇快感结合
|
||||
|
||||
### 复仇虐(女主复仇)
|
||||
|
||||
**公式**:
|
||||
```
|
||||
女主被虐→女主变强→女主回归→打脸渣男→男主后悔→追妻火葬场
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【前期被虐】
|
||||
林慕寒当众羞辱她:"你算什么东西?"
|
||||
苏念心如刀割,离开了他。
|
||||
|
||||
【中期蜕变】
|
||||
三年后。
|
||||
苏念已经成为商业女强人,美丽又强大。
|
||||
|
||||
【后期打脸】
|
||||
"林总,好久不见。"
|
||||
苏念笑得优雅,眼中却是冷漠。
|
||||
林慕寒愣住了。
|
||||
(这……这还是当初那个卑微的她吗……)
|
||||
|
||||
【追妻火葬场】
|
||||
"苏念,对不起……"
|
||||
林慕寒放下所有骄傲,跪在她面前。
|
||||
"求你……原谅我……"
|
||||
苏念看着他,冷笑:"林总,您这是做什么?"
|
||||
"当初您不是说我算什么东西吗?"
|
||||
"现在怎么……跪下了?"
|
||||
```
|
||||
|
||||
**爽点**: 女主复仇+打脸+男主后悔
|
||||
|
||||
---
|
||||
|
||||
### 男配复仇(男二上位)
|
||||
|
||||
**公式**:
|
||||
```
|
||||
男主渣→男二默默守护→女主离开男主→男二趁虚而入→男主后悔莫及
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
【男二守护】
|
||||
男主冷暴力时,男二默默陪伴女主。
|
||||
"别哭了,不值得。"
|
||||
他递给她纸巾,眼中满是心疼。
|
||||
|
||||
【男二表白】
|
||||
"苏念,跟我在一起吧。"
|
||||
"我会好好对你的。"
|
||||
"不会让你再受伤。"
|
||||
|
||||
【男主后悔】
|
||||
林慕寒看到苏念和男二在一起,眼睛红了。
|
||||
(我……我失去她了……)
|
||||
(都是我的错……)
|
||||
```
|
||||
|
||||
**爽点**: 渣男后悔+男二上位+女主幸福
|
||||
|
||||
---
|
||||
|
||||
## 7. 虐点自检清单
|
||||
|
||||
- [ ] **有原因吗**: 虐点是否有明确的触发原因?(不能无缘无故虐)
|
||||
- [ ] **会解开吗**: 虐点是否有明确的解决方案?(虐而不解=弃文)
|
||||
- [ ] **够深刻吗**: 虐点是否能引发读者共鸣?(要虐到读者心里)
|
||||
- [ ] **有补偿吗**: 虐完之后是否有足够的甜蜜补偿?(虐:甜 = 4:6)
|
||||
- [ ] **不过度吗**: 是否避免了连续10章以上的纯虐?(虐不过三)
|
||||
- [ ] **符合人设吗**: 角色的反应是否符合其性格?(不能OOC)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 虐点设计速查表
|
||||
|
||||
| 虐点类型 | 虐点强度 | 持续章节 | 解决方式 | 补偿方式 |
|
||||
|---------|---------|---------|---------|---------|
|
||||
| **误会虐** | ⭐⭐⭐⭐ | 5-15章 | 真相大白 | 追妻火葬场 |
|
||||
| **冷暴力虐** | ⭐⭐⭐⭐⭐ | 3-10章 | 男主后悔 | 疯狂追妻+改变 |
|
||||
| **第三者虐** | ⭐⭐⭐⭐ | 3-10章 | 男主表态 | 公开宣示+赶走第三者 |
|
||||
| **生离死别虐** | ⭐⭐⭐⭐⭐⭐ | 2-5章 | 劫后余生 | 结婚+珍惜+超甜 |
|
||||
| **身份悬殊虐** | ⭐⭐⭐⭐ | 长期背景 | 男主对抗家族 | 公开关系+结婚 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典虐点案例
|
||||
|
||||
### 案例 1: 《何以笙箫默》七年分离虐
|
||||
|
||||
**虐点**: 何以琛因误会赵默笙,冷暴力,导致她离开七年。
|
||||
|
||||
**补偿**: 七年后重逢,何以琛疯狂追妻,各种补偿。
|
||||
|
||||
**名台词**: "如果当时没有错过,我们会不会更幸福?"
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: 反面教材(某扑街文)
|
||||
|
||||
```
|
||||
男主一直虐女主,从头虐到尾。
|
||||
女主被虐得死去活来,男主还是不悔改。
|
||||
最后女主原谅了男主,两人在一起了。
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 男主没有成长,虐而不改
|
||||
- 女主太圣母,没有自尊
|
||||
- 补偿不足,读者不买账
|
||||
|
||||
---
|
||||
|
||||
## 虐点设计的终极原则
|
||||
|
||||
```
|
||||
虐要虐得有理有据
|
||||
虐要虐得刻骨铭心
|
||||
虐完必须真相大白
|
||||
虐完必须加倍补偿
|
||||
```
|
||||
|
||||
**记住**: 虐是手段,甜是目的。虐得再深,最后都要给读者一个圆满的结局。
|
||||
@@ -0,0 +1,288 @@
|
||||
# 古言对话与文风
|
||||
|
||||
> 古言的魅力在于语言。本文档提供写出古风韵味对话的方法。
|
||||
|
||||
---
|
||||
|
||||
## 一、古风语言基础
|
||||
|
||||
### 现代词 → 古风词 转换表
|
||||
|
||||
| 现代词 | 古风词 | 使用场景 |
|
||||
|--------|--------|---------|
|
||||
| 我 | 我/吾/本宫/臣妾/奴婢 | 根据身份选择 |
|
||||
| 你 | 你/汝/尔/阁下/公子 | 根据关系选择 |
|
||||
| 是 | 是/然/正是/确是 | 肯定回答 |
|
||||
| 不是 | 非/不然/并非 | 否定回答 |
|
||||
| 为什么 | 为何/何故/缘何 | 疑问 |
|
||||
| 怎么办 | 如何是好/该当如何 | 询问 |
|
||||
| 知道 | 知晓/知悉/得知 | 了解信息 |
|
||||
| 不知道 | 不知/不晓得/未曾听闻 | 不了解 |
|
||||
| 谢谢 | 多谢/谢过/感激不尽 | 感谢 |
|
||||
| 对不起 | 恕罪/告罪/得罪 | 道歉 |
|
||||
| 没关系 | 无妨/不打紧/无碍 | 原谅 |
|
||||
| 好的 | 好/善/遵命/是 | 同意 |
|
||||
| 等一下 | 且慢/稍候/且住 | 暂停 |
|
||||
| 快点 | 速速/快些/赶紧 | 催促 |
|
||||
| 走吧 | 走罢/去罢/启程 | 离开 |
|
||||
|
||||
---
|
||||
|
||||
## 二、不同身份的说话方式
|
||||
|
||||
### 皇帝
|
||||
```
|
||||
自称:朕、寡人
|
||||
特点:威严、简洁、不容置疑
|
||||
示例:
|
||||
- "准奏。"
|
||||
- "朕乏了。"
|
||||
- "此事,朕自有决断。"
|
||||
- "你可知罪?"
|
||||
```
|
||||
|
||||
### 皇后/太后
|
||||
```
|
||||
自称:本宫、哀家(太后)
|
||||
特点:端庄、威仪、不怒自威
|
||||
示例:
|
||||
- "本宫知道了,你退下吧。"
|
||||
- "放肆!谁给你的胆子?"
|
||||
- "这后宫,还轮不到你来指手画脚。"
|
||||
```
|
||||
|
||||
### 妃嫔
|
||||
```
|
||||
自称:臣妾、妾身、本宫(高位)
|
||||
特点:根据性格和地位变化
|
||||
示例:
|
||||
- 温婉型:"臣妾不敢。"
|
||||
- 骄纵型:"本宫说的话,你敢不听?"
|
||||
- 心机型:"姐姐说的是,妹妹受教了。"
|
||||
```
|
||||
|
||||
### 宫女/太监
|
||||
```
|
||||
自称:奴婢、奴才
|
||||
特点:恭敬、谨慎、察言观色
|
||||
示例:
|
||||
- "奴婢遵命。"
|
||||
- "回主子的话,是这样的..."
|
||||
- "奴婢不敢妄言。"
|
||||
```
|
||||
|
||||
### 官员
|
||||
```
|
||||
自称:臣、微臣、下官
|
||||
特点:正式、谨慎、有分寸
|
||||
示例:
|
||||
- "臣有本要奏。"
|
||||
- "微臣惶恐。"
|
||||
- "此事,臣以为..."
|
||||
```
|
||||
|
||||
### 世家公子/小姐
|
||||
```
|
||||
自称:在下、小生、小女
|
||||
特点:有教养、知书达理
|
||||
示例:
|
||||
- "在下有礼了。"
|
||||
- "小女不才,献丑了。"
|
||||
- "公子谬赞。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、古风对话技巧
|
||||
|
||||
### 1. 省略主语
|
||||
```
|
||||
现代:我知道了。
|
||||
古风:知道了。
|
||||
|
||||
现代:你先退下吧。
|
||||
古风:退下吧。
|
||||
```
|
||||
|
||||
### 2. 倒装句式
|
||||
```
|
||||
现代:你怎么来了?
|
||||
古风:你如何来了?/ 怎的来了?
|
||||
|
||||
现代:这是什么意思?
|
||||
古风:此话何意?/ 这是何意?
|
||||
```
|
||||
|
||||
### 3. 文言虚词
|
||||
```
|
||||
常用虚词:
|
||||
- 罢了:算了、而已
|
||||
- 便是:就是
|
||||
- 竟是:居然是
|
||||
- 原是:原来是
|
||||
- 倒是:反而是
|
||||
```
|
||||
|
||||
### 4. 四字成语/词组
|
||||
```
|
||||
多用四字表达:
|
||||
- 不必多言
|
||||
- 无需如此
|
||||
- 言重了
|
||||
- 过奖了
|
||||
- 恕难从命
|
||||
- 恭敬不如从命
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、情绪表达
|
||||
|
||||
### 愤怒
|
||||
```
|
||||
轻度:"放肆!"
|
||||
中度:"你好大的胆子!"
|
||||
重度:"来人!拖下去!"
|
||||
极度:"朕要诛你九族!"
|
||||
```
|
||||
|
||||
### 悲伤
|
||||
```
|
||||
含蓄:"罢了..."
|
||||
委婉:"心中...有些不适。"
|
||||
直接:"为何...要如此待我?"
|
||||
崩溃:"老天何其不公!"
|
||||
```
|
||||
|
||||
### 喜悦
|
||||
```
|
||||
含蓄:"甚好。"
|
||||
明显:"当真?太好了!"
|
||||
激动:"天佑我大X!"
|
||||
```
|
||||
|
||||
### 讽刺
|
||||
```
|
||||
轻度:"妹妹好手段。"
|
||||
中度:"姐姐真是好算计。"
|
||||
重度:"不愧是X家的女儿,果然...与众不同。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、经典对话模板
|
||||
|
||||
### 初次见面
|
||||
```
|
||||
A:"敢问姑娘芳名?"
|
||||
B:"小女姓X,单名一个X字。"
|
||||
A:"原来是X姑娘,幸会。"
|
||||
B:"公子客气了。"
|
||||
```
|
||||
|
||||
### 请安问好
|
||||
```
|
||||
"给皇上/娘娘请安。"
|
||||
"起来吧。"
|
||||
"谢皇上/娘娘。"
|
||||
```
|
||||
|
||||
### 拒绝请求
|
||||
```
|
||||
委婉:"此事...恐怕不妥。"
|
||||
直接:"恕难从命。"
|
||||
强硬:"休要再提!"
|
||||
```
|
||||
|
||||
### 威胁警告
|
||||
```
|
||||
含蓄:"姐姐好自为之。"
|
||||
明显:"若再有下次,休怪本宫不客气。"
|
||||
直接:"你最好祈祷,别让本宫抓到把柄。"
|
||||
```
|
||||
|
||||
### 表白/暗示
|
||||
```
|
||||
含蓄:"公子于我,不同旁人。"
|
||||
明显:"我心悦你。"
|
||||
直接:"此生,我只认你一人。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、文风把控
|
||||
|
||||
### 轻古风(推荐新手)
|
||||
```
|
||||
特点:
|
||||
- 基本用白话文
|
||||
- 关键词用古风词替换
|
||||
- 对话有古风感,叙述偏现代
|
||||
|
||||
示例:
|
||||
她看着他,心中五味杂陈。
|
||||
"你为何要这样做?"
|
||||
"因为..."他顿了顿,"我不想你受伤。"
|
||||
```
|
||||
|
||||
### 中古风(主流)
|
||||
```
|
||||
特点:
|
||||
- 对话全部古风化
|
||||
- 叙述半文半白
|
||||
- 有一定的文言句式
|
||||
|
||||
示例:
|
||||
她凝视着他,心中百感交集。
|
||||
"你为何要如此?"
|
||||
他沉默片刻,方才开口:"只因...不愿见你受伤。"
|
||||
```
|
||||
|
||||
### 重古风(需要功底)
|
||||
```
|
||||
特点:
|
||||
- 全文文言化
|
||||
- 大量使用典故
|
||||
- 句式讲究
|
||||
|
||||
示例:
|
||||
她凝眸望他,心下五味陈杂。
|
||||
"君何以至此?"
|
||||
他默然良久,方启唇道:"只因...不忍卿受半分委屈。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、常见错误
|
||||
|
||||
### 称谓错误
|
||||
```
|
||||
❌ 皇后自称"哀家"(哀家是太后用的)
|
||||
❌ 对皇帝说"您"(古代没有"您")
|
||||
❌ 妃嫔互称"亲爱的"(太现代)
|
||||
```
|
||||
|
||||
### 用词错误
|
||||
```
|
||||
❌ "OK"、"没问题"(现代词)
|
||||
❌ "我觉得"(太口语)
|
||||
❌ "你知道吗"(太现代)
|
||||
```
|
||||
|
||||
### 语气错误
|
||||
```
|
||||
❌ 宫女对主子说"我不"(太硬)
|
||||
❌ 皇帝说"拜托"(太卑微)
|
||||
❌ 大臣说"随便"(太随意)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、对话检查清单
|
||||
|
||||
- [ ] 称谓是否符合身份?
|
||||
- [ ] 用词是否有古风感?
|
||||
- [ ] 语气是否符合人物性格?
|
||||
- [ ] 是否有现代词汇混入?
|
||||
- [ ] 文风是否前后一致?
|
||||
- [ ] 对话是否推进了剧情?
|
||||
@@ -0,0 +1,268 @@
|
||||
# 古言人物设计
|
||||
|
||||
> 古言人物需要"古"的气质。本文档提供设计符合时代的人物方法。
|
||||
|
||||
---
|
||||
|
||||
## 一、女主类型
|
||||
|
||||
### 1. 宅斗型女主
|
||||
```
|
||||
特点:
|
||||
- 聪慧、隐忍、有手段
|
||||
- 善于察言观色
|
||||
- 懂得借力打力
|
||||
|
||||
成长线:
|
||||
被欺负 → 学会反击 → 掌控内宅 → 成为当家主母
|
||||
|
||||
代表:《知否》盛明兰
|
||||
```
|
||||
|
||||
### 2. 宫斗型女主
|
||||
```
|
||||
特点:
|
||||
- 心思缜密
|
||||
- 善于隐藏
|
||||
- 关键时刻果断
|
||||
|
||||
成长线:
|
||||
入宫 → 站稳脚跟 → 步步高升 → 登顶/全身而退
|
||||
|
||||
代表:《甄嬛传》甄嬛
|
||||
```
|
||||
|
||||
### 3. 权谋型女主
|
||||
```
|
||||
特点:
|
||||
- 有政治头脑
|
||||
- 格局大
|
||||
- 不拘泥于后宅
|
||||
|
||||
成长线:
|
||||
被卷入 → 展露才能 → 参与大事 → 影响朝局
|
||||
|
||||
代表:《琅琊榜》霓凰郡主
|
||||
```
|
||||
|
||||
### 4. 甜宠型女主
|
||||
```
|
||||
特点:
|
||||
- 性格讨喜
|
||||
- 有小聪明
|
||||
- 运气好
|
||||
|
||||
成长线:
|
||||
相遇 → 被宠 → 小波折 → 幸福结局
|
||||
|
||||
适用:轻松甜文
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、男主类型
|
||||
|
||||
### 1. 帝王型
|
||||
```
|
||||
特点:
|
||||
- 威严、深沉
|
||||
- 多疑但深情
|
||||
- 权力与爱情的矛盾
|
||||
|
||||
魅力点:
|
||||
- 万人之上只对她温柔
|
||||
- 为她打破规则
|
||||
- 霸道占有
|
||||
```
|
||||
|
||||
### 2. 王爷型
|
||||
```
|
||||
特点:
|
||||
- 尊贵但不是最高位
|
||||
- 可以更自由
|
||||
- 有自己的势力
|
||||
|
||||
魅力点:
|
||||
- 不争皇位只要她
|
||||
- 可以带她远走
|
||||
- 亦正亦邪
|
||||
```
|
||||
|
||||
### 3. 世家公子型
|
||||
```
|
||||
特点:
|
||||
- 温润如玉
|
||||
- 家世显赫
|
||||
- 有教养
|
||||
|
||||
魅力点:
|
||||
- 君子风度
|
||||
- 默默守护
|
||||
- 温柔坚定
|
||||
```
|
||||
|
||||
### 4. 将军型
|
||||
```
|
||||
特点:
|
||||
- 铁血硬汉
|
||||
- 战场英雄
|
||||
- 不善言辞
|
||||
|
||||
魅力点:
|
||||
- 外冷内热
|
||||
- 用行动表达爱
|
||||
- 保护欲强
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、配角设计
|
||||
|
||||
### 反派女配
|
||||
```
|
||||
类型1:嫡女/正室
|
||||
- 出身高贵,看不起女主
|
||||
- 手段狠辣
|
||||
- 最终下场凄惨
|
||||
|
||||
类型2:白月光
|
||||
- 表面清纯
|
||||
- 实则心机
|
||||
- 与男主有旧情
|
||||
|
||||
类型3:恶婆婆/恶嫂
|
||||
- 刁难女主
|
||||
- 偏心其他人
|
||||
- 最终被打脸
|
||||
```
|
||||
|
||||
### 助攻配角
|
||||
```
|
||||
类型1:忠心丫鬟
|
||||
- 从小跟着女主
|
||||
- 忠心耿耿
|
||||
- 关键时刻帮忙
|
||||
|
||||
类型2:好姐妹
|
||||
- 同为妃嫔/妯娌
|
||||
- 互相扶持
|
||||
- 真心相待
|
||||
|
||||
类型3:暗卫/侍卫
|
||||
- 保护女主
|
||||
- 可能暗恋女主
|
||||
- 关键时刻救命
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、人物关系设计
|
||||
|
||||
### 宫廷关系
|
||||
```
|
||||
核心三角:皇帝 - 女主 - 皇后/宠妃
|
||||
辅助关系:太后、其他妃嫔、皇子公主
|
||||
外围关系:前朝势力、母族势力
|
||||
```
|
||||
|
||||
### 宅斗关系
|
||||
```
|
||||
核心三角:女主 - 丈夫 - 婆婆/妾室
|
||||
辅助关系:妯娌、小姑、下人
|
||||
外围关系:娘家、姻亲
|
||||
```
|
||||
|
||||
### 权谋关系
|
||||
```
|
||||
核心三角:女主 - 男主 - 政敌
|
||||
辅助关系:盟友、下属、家族
|
||||
外围关系:朝廷各派、边疆势力
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、人物弧光
|
||||
|
||||
### 女主成长弧
|
||||
```
|
||||
阶段1:天真/弱小
|
||||
- 不懂规则
|
||||
- 被欺负
|
||||
- 有人保护
|
||||
|
||||
阶段2:觉醒/学习
|
||||
- 开始反击
|
||||
- 学会手段
|
||||
- 建立势力
|
||||
|
||||
阶段3:强大/掌控
|
||||
- 成为高手
|
||||
- 保护他人
|
||||
- 达成目标
|
||||
```
|
||||
|
||||
### 男主变化弧
|
||||
```
|
||||
类型1:冷到热
|
||||
- 初期冷漠
|
||||
- 逐渐心动
|
||||
- 最终深情
|
||||
|
||||
类型2:误解到理解
|
||||
- 初期误会
|
||||
- 发现真相
|
||||
- 追悔弥补
|
||||
|
||||
类型3:成长型
|
||||
- 初期不成熟
|
||||
- 经历磨难
|
||||
- 成为合格的丈夫/帝王
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、人物细节
|
||||
|
||||
### 外貌描写
|
||||
```
|
||||
女主:
|
||||
- 不必倾国倾城
|
||||
- 有特点即可(眼睛/气质)
|
||||
- 符合时代审美
|
||||
|
||||
男主:
|
||||
- 俊美但不娘
|
||||
- 有气势
|
||||
- 符合身份
|
||||
```
|
||||
|
||||
### 技能设定
|
||||
```
|
||||
女主可以有的技能:
|
||||
- 医术(常见)
|
||||
- 厨艺(讨喜)
|
||||
- 琴棋书画(才女)
|
||||
- 经商头脑(独立)
|
||||
- 武功(特殊)
|
||||
|
||||
注意:技能要有来源,不能凭空出现
|
||||
```
|
||||
|
||||
### 性格缺陷
|
||||
```
|
||||
让人物更真实:
|
||||
- 女主可以有小心眼、记仇
|
||||
- 男主可以有大男子主义、多疑
|
||||
- 缺陷要在可接受范围内
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、人物检查清单
|
||||
|
||||
- [ ] 人物是否符合时代背景?
|
||||
- [ ] 人物的行为是否符合身份?
|
||||
- [ ] 人物是否有成长变化?
|
||||
- [ ] 人物关系是否清晰?
|
||||
- [ ] 人物是否有记忆点?
|
||||
- [ ] 人物的技能是否有来源?
|
||||
@@ -0,0 +1,278 @@
|
||||
# 古言题材历史背景设定
|
||||
|
||||
> 古言的魅力在于"古"。本文档提供构建可信历史背景的方法。
|
||||
|
||||
---
|
||||
|
||||
## 一、朝代选择
|
||||
|
||||
### 常用朝代特点
|
||||
|
||||
| 朝代 | 特点 | 适合题材 |
|
||||
|------|------|---------|
|
||||
| 架空 | 自由度高 | 宫斗、权谋、甜宠 |
|
||||
| 唐 | 开放、繁华 | 盛世、女性题材 |
|
||||
| 宋 | 文雅、市井 | 商战、日常、探案 |
|
||||
| 明 | 礼教、锦衣卫 | 权谋、悬疑 |
|
||||
| 清 | 宫廷、满汉 | 宫斗、夺嫡 |
|
||||
|
||||
### 架空朝代设定
|
||||
```
|
||||
优势:
|
||||
- 不受历史约束
|
||||
- 可以自由设定规则
|
||||
- 避免历史考据争议
|
||||
|
||||
注意:
|
||||
- 要有内在一致性
|
||||
- 参考真实朝代但不照搬
|
||||
- 建立自己的世界观
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、社会结构
|
||||
|
||||
### 阶层划分
|
||||
```
|
||||
皇室:皇帝、皇后、妃嫔、皇子、公主
|
||||
宗室:亲王、郡王、贝勒、贝子
|
||||
勋贵:公侯伯子男、世家大族
|
||||
官员:文官、武将、地方官
|
||||
平民:商人、农民、工匠
|
||||
贱籍:奴婢、乐户、罪犯
|
||||
```
|
||||
|
||||
### 女性地位
|
||||
```
|
||||
正妻:主母,管理内宅
|
||||
妾室:地位低于正妻
|
||||
通房:丫鬟抬举
|
||||
婢女:服侍主人
|
||||
|
||||
注意:
|
||||
- 嫡庶之分很重要
|
||||
- 正妻有管理妾室的权力
|
||||
- 妾室所生为庶出
|
||||
```
|
||||
|
||||
### 家族结构
|
||||
```
|
||||
大家族:
|
||||
- 族长(通常是嫡长房)
|
||||
- 各房(大房、二房...)
|
||||
- 嫡出、庶出
|
||||
- 旁支、远亲
|
||||
|
||||
小家庭:
|
||||
- 父母
|
||||
- 兄弟姐妹
|
||||
- 妻妾子女
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、日常生活
|
||||
|
||||
### 称谓系统
|
||||
```
|
||||
对皇帝:皇上、陛下、圣上
|
||||
对皇后:皇后娘娘、母后
|
||||
对妃嫔:X妃娘娘、娘娘
|
||||
对王爷:王爷、殿下
|
||||
对官员:大人、老爷
|
||||
对夫人:夫人、太太、奶奶
|
||||
自称:臣、臣妾、奴婢、小女子
|
||||
```
|
||||
|
||||
### 服饰
|
||||
```
|
||||
男子:
|
||||
- 常服:长袍、直裰
|
||||
- 官服:补服、朝服
|
||||
- 配饰:玉佩、扇子
|
||||
|
||||
女子:
|
||||
- 常服:襦裙、褙子
|
||||
- 礼服:凤冠霞帔
|
||||
- 配饰:步摇、钗环
|
||||
```
|
||||
|
||||
### 饮食
|
||||
```
|
||||
主食:米饭、面食、粥
|
||||
菜肴:根据地域和阶层不同
|
||||
饮品:茶、酒
|
||||
点心:糕点、果子
|
||||
|
||||
注意:
|
||||
- 土豆、玉米、辣椒是明朝后才有
|
||||
- 不同阶层饮食差异大
|
||||
```
|
||||
|
||||
### 出行
|
||||
```
|
||||
女子:轿子、马车(不宜抛头露面)
|
||||
男子:骑马、坐轿、马车
|
||||
平民:步行、驴车
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、礼仪规范
|
||||
|
||||
### 见面礼
|
||||
```
|
||||
跪拜:对皇帝、长辈
|
||||
作揖:平辈男子
|
||||
万福:女子行礼
|
||||
请安:日常问候
|
||||
```
|
||||
|
||||
### 婚嫁礼
|
||||
```
|
||||
六礼:
|
||||
1. 纳采(提亲)
|
||||
2. 问名(合八字)
|
||||
3. 纳吉(订婚)
|
||||
4. 纳征(送聘礼)
|
||||
5. 请期(定日子)
|
||||
6. 亲迎(迎娶)
|
||||
```
|
||||
|
||||
### 丧葬礼
|
||||
```
|
||||
- 守孝三年(实际27个月)
|
||||
- 丁忧(官员守孝需辞官)
|
||||
- 服丧期间不能婚嫁
|
||||
```
|
||||
|
||||
### 禁忌
|
||||
```
|
||||
- 女子不能随意见外男
|
||||
- 寡妇再嫁受歧视
|
||||
- 冲撞长辈是大不敬
|
||||
- 后宫不得干政
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、宫廷设定
|
||||
|
||||
### 后宫等级(以清朝为例)
|
||||
```
|
||||
皇后(1人)
|
||||
皇贵妃(1人)
|
||||
贵妃(2人)
|
||||
妃(4人)
|
||||
嫔(6人)
|
||||
贵人(无定数)
|
||||
常在(无定数)
|
||||
答应(无定数)
|
||||
```
|
||||
|
||||
### 宫廷建筑
|
||||
```
|
||||
前朝:处理政务
|
||||
后宫:妃嫔居住
|
||||
东宫:太子居所
|
||||
各宫:以宫名命名(如:坤宁宫、长春宫)
|
||||
```
|
||||
|
||||
### 宫廷人员
|
||||
```
|
||||
太监:内侍、公公
|
||||
宫女:服侍妃嫔
|
||||
嬷嬷:年长女官
|
||||
侍卫:保护安全
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、科举仕途
|
||||
|
||||
### 科举流程
|
||||
```
|
||||
童试 → 秀才
|
||||
乡试 → 举人
|
||||
会试 → 贡士
|
||||
殿试 → 进士(状元、榜眼、探花)
|
||||
```
|
||||
|
||||
### 官职系统
|
||||
```
|
||||
中央:
|
||||
- 内阁(大学士)
|
||||
- 六部(尚书、侍郎)
|
||||
- 都察院(御史)
|
||||
|
||||
地方:
|
||||
- 总督、巡抚
|
||||
- 知府、知县
|
||||
```
|
||||
|
||||
### 仕途发展
|
||||
```
|
||||
- 翰林院(清贵)
|
||||
- 六部(实权)
|
||||
- 外放(地方官)
|
||||
- 京官(中央)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、常见设定错误
|
||||
|
||||
### 称谓错误
|
||||
```
|
||||
❌ 皇后自称"本宫"(应为"本宫"或"臣妾"视场合)
|
||||
❌ 对皇帝称"皇上您"(不能用"您")
|
||||
❌ 妃嫔互称"姐姐"(应按位分称呼)
|
||||
```
|
||||
|
||||
### 礼仪错误
|
||||
```
|
||||
❌ 女子随意出门逛街
|
||||
❌ 未婚男女单独相处
|
||||
❌ 妾室与正妻平起平坐
|
||||
```
|
||||
|
||||
### 常识错误
|
||||
```
|
||||
❌ 古代有玻璃窗(应为纸窗或纱窗)
|
||||
❌ 随便吃辣椒(明朝前没有)
|
||||
❌ 女子读书识字很普遍(实际是少数)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、历史感营造
|
||||
|
||||
### 语言风格
|
||||
```
|
||||
避免现代词汇:
|
||||
❌ "OK""没问题""搞定"
|
||||
✓ "好""可以""妥了"
|
||||
|
||||
使用古风词汇:
|
||||
- 用"银子"不用"钱"
|
||||
- 用"用膳"不用"吃饭"
|
||||
- 用"歇息"不用"休息"
|
||||
```
|
||||
|
||||
### 细节描写
|
||||
```
|
||||
用古代物品:
|
||||
- 油灯、蜡烛(不是电灯)
|
||||
- 毛笔、砚台(不是钢笔)
|
||||
- 铜镜(不是玻璃镜)
|
||||
```
|
||||
|
||||
### 思维方式
|
||||
```
|
||||
古人的价值观:
|
||||
- 重视家族荣誉
|
||||
- 讲究门当户对
|
||||
- 遵守礼教规范
|
||||
- 相信命运天意
|
||||
```
|
||||
@@ -0,0 +1,268 @@
|
||||
# 古言宫斗权谋设计
|
||||
|
||||
> 宫斗是古言的经典题材。本文档提供设计精彩宫斗的方法。
|
||||
|
||||
---
|
||||
|
||||
## 一、宫斗核心要素
|
||||
|
||||
| 要素 | 内容 | 作用 |
|
||||
|------|------|------|
|
||||
| 目标 | 争宠/夺位/复仇/生存 | 驱动剧情 |
|
||||
| 资源 | 圣宠/家世/人脉/情报 | 斗争筹码 |
|
||||
| 手段 | 阴谋/阳谋/借刀/联盟 | 实现目标 |
|
||||
| 代价 | 失宠/降位/性命/亲人 | 增加张力 |
|
||||
|
||||
---
|
||||
|
||||
## 二、宫斗势力布局
|
||||
|
||||
### 后宫势力划分
|
||||
```
|
||||
第一梯队(顶级威胁):
|
||||
- 皇后(正宫之主)
|
||||
- 宠妃(圣眷正隆)
|
||||
- 太后(幕后大佬)
|
||||
|
||||
第二梯队(潜在对手):
|
||||
- 有子嗣的妃嫔
|
||||
- 有家世的妃嫔
|
||||
- 有手段的妃嫔
|
||||
|
||||
第三梯队(可拉拢):
|
||||
- 失宠的旧人
|
||||
- 无依无靠的新人
|
||||
- 心怀不满的宫人
|
||||
```
|
||||
|
||||
### 前朝势力关联
|
||||
```
|
||||
后宫 ←→ 前朝 对应关系:
|
||||
- 皇后 ←→ 皇后母族(外戚)
|
||||
- 宠妃 ←→ 宠妃父兄(新贵)
|
||||
- 太后 ←→ 太后母族(老牌势力)
|
||||
- 皇子 ←→ 皇子党羽(夺嫡势力)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、宫斗手段分类
|
||||
|
||||
### 1. 阴谋类
|
||||
```
|
||||
陷害:
|
||||
- 栽赃嫁祸(在对方宫中放违禁品)
|
||||
- 制造丑闻(捏造私通/不敬)
|
||||
- 借刀杀人(挑拨他人动手)
|
||||
|
||||
暗害:
|
||||
- 下毒(慢性毒/绝育药/致疯药)
|
||||
- 意外(滑倒/落水/走水)
|
||||
- 收买(买通对方身边人)
|
||||
```
|
||||
|
||||
### 2. 阳谋类
|
||||
```
|
||||
争宠:
|
||||
- 投其所好(研究皇帝喜好)
|
||||
- 制造机会(偶遇/才艺展示)
|
||||
- 欲擒故纵(若即若离)
|
||||
|
||||
立功:
|
||||
- 揭发阴谋(救驾/告密)
|
||||
- 解决难题(帮皇帝分忧)
|
||||
- 生育子嗣(最大的功劳)
|
||||
```
|
||||
|
||||
### 3. 联盟类
|
||||
```
|
||||
结盟:
|
||||
- 利益交换(互相帮助)
|
||||
- 共同敌人(敌人的敌人是朋友)
|
||||
- 主从关系(依附强者)
|
||||
|
||||
分化:
|
||||
- 挑拨离间(制造矛盾)
|
||||
- 利益诱惑(拉拢对方的人)
|
||||
- 暴露真相(揭示盟友的背叛)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、宫斗节奏设计
|
||||
|
||||
### 升级模式
|
||||
```
|
||||
初入宫 → 站稳脚跟 → 小有地位 → 遭遇危机 → 绝地反击 → 登顶/结局
|
||||
|
||||
每个阶段的对手等级递增:
|
||||
1. 同级妃嫔的小打小闹
|
||||
2. 高位妃嫔的打压
|
||||
3. 皇后/宠妃的正面冲突
|
||||
4. 太后/前朝的终极对决
|
||||
```
|
||||
|
||||
### 攻防节奏
|
||||
```
|
||||
被动防守期:
|
||||
- 初入宫,不了解规则
|
||||
- 被针对,学会自保
|
||||
- 积累资源和人脉
|
||||
|
||||
主动出击期:
|
||||
- 有了一定地位
|
||||
- 开始反击
|
||||
- 逐步扩大势力
|
||||
|
||||
攻守兼备期:
|
||||
- 成为主要势力
|
||||
- 既要防守又要进攻
|
||||
- 多线作战
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、经典宫斗桥段
|
||||
|
||||
### 1. 初入宫
|
||||
```
|
||||
- 被高位妃嫔立下马威
|
||||
- 被分配到冷宫/偏殿
|
||||
- 第一次见皇帝(惊艳/失礼/特别)
|
||||
- 结识第一个盟友/敌人
|
||||
```
|
||||
|
||||
### 2. 争宠
|
||||
```
|
||||
- 偶遇皇帝(精心设计的偶遇)
|
||||
- 才艺展示(琴棋书画/厨艺/医术)
|
||||
- 与众不同(不争不抢反而引起注意)
|
||||
- 救驾/解围(危机中的表现)
|
||||
```
|
||||
|
||||
### 3. 被陷害
|
||||
```
|
||||
- 被栽赃(宫中发现违禁品)
|
||||
- 被诬陷(捏造的证人证词)
|
||||
- 被设计(落入圈套)
|
||||
- 被误会(皇帝亲眼所见的假象)
|
||||
```
|
||||
|
||||
### 4. 反击
|
||||
```
|
||||
- 揭露真相(找到真正的证据)
|
||||
- 将计就计(利用对方的阴谋)
|
||||
- 借力打力(让对方自相残杀)
|
||||
- 绝地反击(濒死时的翻盘)
|
||||
```
|
||||
|
||||
### 5. 高潮对决
|
||||
```
|
||||
- 当众对质(大殿/宴会上的摊牌)
|
||||
- 证据链条(一环扣一环的揭露)
|
||||
- 最终审判(皇帝/太后的裁决)
|
||||
- 敌人下场(死/废/贬/疯)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、宫斗智商线
|
||||
|
||||
### 聪明的表现
|
||||
```
|
||||
✓ 提前布局,留有后手
|
||||
✓ 不轻易暴露真实意图
|
||||
✓ 善于利用规则和人心
|
||||
✓ 知道什么时候该忍
|
||||
✓ 能看穿对方的阴谋
|
||||
```
|
||||
|
||||
### 避免降智
|
||||
```
|
||||
✗ 明显的陷阱还往里跳
|
||||
✗ 轻易相信敌人的话
|
||||
✗ 在不该说话时说话
|
||||
✗ 把计划告诉不该知道的人
|
||||
✗ 低估对手的智商
|
||||
```
|
||||
|
||||
### 合理的失误
|
||||
```
|
||||
可以犯错,但要合理:
|
||||
- 信息不对称导致的误判
|
||||
- 感情用事的冲动
|
||||
- 被更高明的对手算计
|
||||
- 意外因素的干扰
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、宫斗情感线
|
||||
|
||||
### 帝妃感情
|
||||
```
|
||||
类型:
|
||||
- 真爱型:皇帝真心爱女主
|
||||
- 利用型:互相利用,日久生情
|
||||
- 虐恋型:爱而不得,误会重重
|
||||
- 救赎型:女主改变了皇帝
|
||||
```
|
||||
|
||||
### 感情与权谋的平衡
|
||||
```
|
||||
纯宫斗:感情是工具,权谋是核心
|
||||
宫斗言情:权谋是背景,感情是核心
|
||||
平衡型:权谋和感情并重
|
||||
|
||||
注意:
|
||||
- 不要让感情线拖慢宫斗节奏
|
||||
- 不要让宫斗冲淡感情发展
|
||||
- 关键时刻要有取舍
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、宫斗结局类型
|
||||
|
||||
### HE结局
|
||||
```
|
||||
- 登上后位,与帝王相守
|
||||
- 扶持皇子登基,成为太后
|
||||
- 离开皇宫,与真爱远走
|
||||
- 权倾天下,功成身退
|
||||
```
|
||||
|
||||
### BE结局
|
||||
```
|
||||
- 斗赢了所有人,却失去了爱
|
||||
- 登上高位,却发现一切都是空
|
||||
- 为了复仇,付出了一切
|
||||
- 最终死在权力的游戏中
|
||||
```
|
||||
|
||||
### 开放结局
|
||||
```
|
||||
- 她看着那把椅子,笑了
|
||||
- 新的妃嫔入宫了,故事重新开始
|
||||
- 她终于走出了那道宫门
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、宫斗检查清单
|
||||
|
||||
### 逻辑检查
|
||||
- [ ] 势力布局是否清晰?
|
||||
- [ ] 每个阴谋是否有合理的执行方式?
|
||||
- [ ] 女主的反击是否有足够的铺垫?
|
||||
- [ ] 智商线是否在线?
|
||||
|
||||
### 节奏检查
|
||||
- [ ] 是否有张有弛?
|
||||
- [ ] 对手等级是否递进?
|
||||
- [ ] 高潮是否足够精彩?
|
||||
|
||||
### 情感检查
|
||||
- [ ] 感情线是否与宫斗线协调?
|
||||
- [ ] 读者是否会为女主紧张?
|
||||
- [ ] 结局是否有情感满足?
|
||||
@@ -0,0 +1,276 @@
|
||||
# 古言剧情模式
|
||||
|
||||
> 古言有其经典的剧情模式。本文档提供常用的剧情框架和桥段。
|
||||
|
||||
---
|
||||
|
||||
## 一、经典剧情框架
|
||||
|
||||
### 1. 宫斗框架
|
||||
```
|
||||
入宫 → 站稳 → 争宠 → 危机 → 登顶/退出
|
||||
|
||||
详细:
|
||||
1. 入宫(选秀/赐婚/家族安排)
|
||||
2. 初入后宫(被欺负/找靠山)
|
||||
3. 获得圣宠(才艺/性格/机缘)
|
||||
4. 树敌(皇后/宠妃的打压)
|
||||
5. 步步高升(晋位/生子)
|
||||
6. 重大危机(被陷害/失宠)
|
||||
7. 绝地反击(揭露真相/翻盘)
|
||||
8. 最终结局(登后位/离宫/帝后和谐)
|
||||
```
|
||||
|
||||
### 2. 宅斗框架
|
||||
```
|
||||
嫁入 → 立足 → 掌家 → 危机 → 当家
|
||||
|
||||
详细:
|
||||
1. 嫁入(高嫁/低嫁/冲喜)
|
||||
2. 婆媳过招(立规矩/被刁难)
|
||||
3. 妯娌争斗(争资源/争宠)
|
||||
4. 丈夫关系(冷淡/渐热/误会)
|
||||
5. 管家权争夺
|
||||
6. 家族危机(抄家/落魄)
|
||||
7. 力挽狂澜
|
||||
8. 成为当家主母
|
||||
```
|
||||
|
||||
### 3. 权谋框架
|
||||
```
|
||||
卷入 → 站队 → 博弈 → 决战 → 新局
|
||||
|
||||
详细:
|
||||
1. 被卷入政治漩涡
|
||||
2. 选择阵营/被迫站队
|
||||
3. 参与谋划
|
||||
4. 多方博弈
|
||||
5. 关键情报/转折
|
||||
6. 最终决战
|
||||
7. 新的格局
|
||||
```
|
||||
|
||||
### 4. 甜宠框架
|
||||
```
|
||||
相遇 → 相知 → 相爱 → 小虐 → HE
|
||||
|
||||
详细:
|
||||
1. 特别的相遇
|
||||
2. 日常相处
|
||||
3. 心动时刻
|
||||
4. 确定关系
|
||||
5. 小波折(误会/阻碍)
|
||||
6. 解决问题
|
||||
7. 甜蜜结局
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、经典桥段库
|
||||
|
||||
### 相遇桥段
|
||||
```
|
||||
1. 救命之恩
|
||||
- 她救了他(不知身份)
|
||||
- 他救了她(英雄救美)
|
||||
|
||||
2. 冲撞/冒犯
|
||||
- 不小心冲撞了贵人
|
||||
- 不知身份出言不逊
|
||||
|
||||
3. 替嫁/冲喜
|
||||
- 代替姐妹出嫁
|
||||
- 冲喜给"将死之人"
|
||||
|
||||
4. 指腹为婚
|
||||
- 父辈的约定
|
||||
- 多年后相见
|
||||
|
||||
5. 意外同处
|
||||
- 被困一处
|
||||
- 意外同床
|
||||
```
|
||||
|
||||
### 虐心桥段
|
||||
```
|
||||
1. 误会
|
||||
- 亲眼看到"出轨"
|
||||
- 被人挑拨离间
|
||||
|
||||
2. 被迫分离
|
||||
- 家族反对
|
||||
- 政治原因
|
||||
- 生死相隔
|
||||
|
||||
3. 替身/白月光
|
||||
- 发现自己是替身
|
||||
- 白月光归来
|
||||
|
||||
4. 小产/失子
|
||||
- 被害小产
|
||||
- 孩子夭折
|
||||
|
||||
5. 冷暴力
|
||||
- 被冷落
|
||||
- 被忽视
|
||||
```
|
||||
|
||||
### 甜蜜桥段
|
||||
```
|
||||
1. 吃醋
|
||||
- 看到她和别人说话
|
||||
- 听到有人追求她
|
||||
|
||||
2. 偷偷关心
|
||||
- 以为她不知道
|
||||
- 其实她都知道
|
||||
|
||||
3. 当众护短
|
||||
- 有人欺负她
|
||||
- 他霸气护妻
|
||||
|
||||
4. 日常宠溺
|
||||
- 投其所好
|
||||
- 事事顺着
|
||||
|
||||
5. 仪式感
|
||||
- 大婚/册封
|
||||
- 生辰/节日
|
||||
```
|
||||
|
||||
### 打脸桥段
|
||||
```
|
||||
1. 身份反转
|
||||
- 以为是穷亲戚,其实是贵女
|
||||
- 以为是弃妃,其实是真爱
|
||||
|
||||
2. 实力展示
|
||||
- 被嘲笑后展露才华
|
||||
- 关键时刻力挽狂澜
|
||||
|
||||
3. 真相揭露
|
||||
- 陷害者被揭穿
|
||||
- 白月光原形毕露
|
||||
|
||||
4. 打脸现场
|
||||
- 曾经看不起的人求上门
|
||||
- 曾经嘲笑的话被打脸
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、剧情节奏
|
||||
|
||||
### 宫斗节奏
|
||||
```
|
||||
卷一(入宫):20%
|
||||
- 建立人设
|
||||
- 了解规则
|
||||
- 第一次小胜
|
||||
|
||||
卷二(争宠):30%
|
||||
- 获得圣宠
|
||||
- 树敌
|
||||
- 步步高升
|
||||
|
||||
卷三(危机):30%
|
||||
- 重大危机
|
||||
- 跌入谷底
|
||||
- 绝地反击
|
||||
|
||||
卷四(结局):20%
|
||||
- 最终对决
|
||||
- 登顶/退出
|
||||
- 收尾
|
||||
```
|
||||
|
||||
### 甜宠节奏
|
||||
```
|
||||
相遇期:15%
|
||||
暧昧期:25%
|
||||
热恋期:30%
|
||||
小虐期:15%
|
||||
HE期:15%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、剧情设计技巧
|
||||
|
||||
### 1. 伏笔设置
|
||||
```
|
||||
前期埋下的线索:
|
||||
- 一个不起眼的人物
|
||||
- 一句无心的话
|
||||
- 一个小物件
|
||||
|
||||
后期揭示:
|
||||
- 原来那个人是关键
|
||||
- 那句话暗示了真相
|
||||
- 那个物件是证据
|
||||
```
|
||||
|
||||
### 2. 反转设计
|
||||
```
|
||||
类型1:身份反转
|
||||
- 以为是A,其实是B
|
||||
|
||||
类型2:关系反转
|
||||
- 以为是敌人,其实是盟友
|
||||
|
||||
类型3:真相反转
|
||||
- 以为是这样,其实是那样
|
||||
|
||||
类型4:结局反转
|
||||
- 以为是HE,其实是BE(或反之)
|
||||
```
|
||||
|
||||
### 3. 高潮设计
|
||||
```
|
||||
宫斗高潮:
|
||||
- 当众对质
|
||||
- 证据链揭露
|
||||
- 最终审判
|
||||
|
||||
宅斗高潮:
|
||||
- 分家
|
||||
- 休妻/和离
|
||||
- 真相大白
|
||||
|
||||
权谋高潮:
|
||||
- 政变
|
||||
- 决战
|
||||
- 登基
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、避免的问题
|
||||
|
||||
### 剧情问题
|
||||
```
|
||||
❌ 金手指太多(运气太好)
|
||||
❌ 反派太蠢(智商不在线)
|
||||
❌ 节奏拖沓(无意义的日常)
|
||||
❌ 虐得没道理(为虐而虐)
|
||||
❌ 结局仓促(草草收尾)
|
||||
```
|
||||
|
||||
### 逻辑问题
|
||||
```
|
||||
❌ 时间线混乱
|
||||
❌ 人物行为不符合身份
|
||||
❌ 历史常识错误
|
||||
❌ 权力运作不合理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、剧情检查清单
|
||||
|
||||
- [ ] 主线是否清晰?
|
||||
- [ ] 节奏是否合适?
|
||||
- [ ] 伏笔是否回收?
|
||||
- [ ] 高潮是否精彩?
|
||||
- [ ] 结局是否满意?
|
||||
- [ ] 逻辑是否通顺?
|
||||
@@ -0,0 +1,234 @@
|
||||
# 现实题材人物深度塑造
|
||||
|
||||
> 现实题材的人物必须"像真人"。本文档提供塑造立体人物的方法。
|
||||
|
||||
---
|
||||
|
||||
## 一、立体人物三维度
|
||||
|
||||
| 维度 | 内容 | 作用 |
|
||||
|------|------|------|
|
||||
| 社会维度 | 职业、阶层、家庭背景 | 定义人物的外在处境 |
|
||||
| 心理维度 | 性格、欲望、恐惧、创伤 | 定义人物的内在驱动 |
|
||||
| 道德维度 | 价值观、底线、灰色地带 | 定义人物的选择逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 二、社会维度构建
|
||||
|
||||
### 1. 职业身份
|
||||
```
|
||||
不只是"她是医生"
|
||||
而是:
|
||||
- 哪个科室?(急诊/外科/儿科)
|
||||
- 什么职称?(住院医/主治/主任)
|
||||
- 工作几年?(新人/中坚/老资历)
|
||||
- 职业困境?(晋升/医患/平衡)
|
||||
```
|
||||
|
||||
### 2. 经济状况
|
||||
```
|
||||
具体化:
|
||||
- 月收入多少?
|
||||
- 有无房贷车贷?
|
||||
- 存款有多少?
|
||||
- 消费习惯如何?
|
||||
```
|
||||
|
||||
### 3. 家庭背景
|
||||
```
|
||||
原生家庭:
|
||||
- 父母职业和性格
|
||||
- 家庭氛围(温暖/冷漠/控制)
|
||||
- 童年经历(创伤/幸福)
|
||||
- 与父母的现状关系
|
||||
```
|
||||
|
||||
### 4. 社会关系
|
||||
```
|
||||
- 朋友圈层(多少/质量)
|
||||
- 同事关系(融洽/紧张)
|
||||
- 邻里关系(熟悉/陌生)
|
||||
- 社交习惯(内向/外向)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、心理维度构建
|
||||
|
||||
### 1. 核心欲望
|
||||
```
|
||||
每个人都有一个核心欲望:
|
||||
- 被爱/被认可/被尊重
|
||||
- 安全感/控制感/自由
|
||||
- 成功/金钱/权力
|
||||
- 归属感/存在感/意义感
|
||||
```
|
||||
|
||||
### 2. 深层恐惧
|
||||
```
|
||||
与欲望对应的恐惧:
|
||||
- 渴望被爱 → 害怕被抛弃
|
||||
- 渴望成功 → 害怕失败
|
||||
- 渴望自由 → 害怕被束缚
|
||||
- 渴望安全 → 害怕失控
|
||||
```
|
||||
|
||||
### 3. 心理创伤
|
||||
```
|
||||
过去的伤痕影响现在的行为:
|
||||
- 童年被忽视 → 成年后讨好型人格
|
||||
- 曾被背叛 → 难以信任他人
|
||||
- 经历过贫穷 → 对金钱极度敏感
|
||||
- 失去过至亲 → 害怕亲密关系
|
||||
```
|
||||
|
||||
### 4. 防御机制
|
||||
```
|
||||
人物如何保护自己:
|
||||
- 逃避:不面对问题
|
||||
- 攻击:先发制人
|
||||
- 讨好:牺牲自己
|
||||
- 隔离:切断情感
|
||||
- 合理化:给自己找借口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、道德维度构建
|
||||
|
||||
### 1. 价值排序
|
||||
```
|
||||
当价值冲突时,人物会选择什么?
|
||||
- 家庭 vs 事业
|
||||
- 爱情 vs 面包
|
||||
- 原则 vs 利益
|
||||
- 自己 vs 他人
|
||||
```
|
||||
|
||||
### 2. 道德底线
|
||||
```
|
||||
人物绝对不会做的事:
|
||||
- 有人底线是不说谎
|
||||
- 有人底线是不伤害家人
|
||||
- 有人底线是不违法
|
||||
- 有人没有底线
|
||||
```
|
||||
|
||||
### 3. 灰色地带
|
||||
```
|
||||
人物会纠结的选择:
|
||||
- 为了孩子,要不要忍受糟糕的婚姻?
|
||||
- 为了晋升,要不要做违心的事?
|
||||
- 为了生存,要不要放弃原则?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、人物弧光设计
|
||||
|
||||
### 1. 正向弧光
|
||||
```
|
||||
从缺陷到成长:
|
||||
- 自卑 → 自信
|
||||
- 逃避 → 面对
|
||||
- 依赖 → 独立
|
||||
- 封闭 → 开放
|
||||
```
|
||||
|
||||
### 2. 负向弧光
|
||||
```
|
||||
从正常到堕落:
|
||||
- 善良 → 冷漠
|
||||
- 信任 → 怀疑
|
||||
- 热情 → 麻木
|
||||
- 坚持 → 妥协
|
||||
```
|
||||
|
||||
### 3. 平弧光
|
||||
```
|
||||
坚持自我,改变世界:
|
||||
- 人物始终坚持某个信念
|
||||
- 通过行动影响周围的人
|
||||
- 证明某种价值观的力量
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、人物关系网络
|
||||
|
||||
### 1. 核心关系
|
||||
```
|
||||
影响人物最深的关系:
|
||||
- 原生家庭(父母/兄弟姐妹)
|
||||
- 亲密关系(伴侣/前任)
|
||||
- 重要他人(导师/挚友/对手)
|
||||
```
|
||||
|
||||
### 2. 关系动态
|
||||
```
|
||||
关系不是静态的:
|
||||
- 从陌生到亲密
|
||||
- 从信任到背叛
|
||||
- 从依赖到独立
|
||||
- 从对立到和解
|
||||
```
|
||||
|
||||
### 3. 关系功能
|
||||
```
|
||||
每段关系都有功能:
|
||||
- 镜子:照出人物的真实
|
||||
- 催化剂:推动人物改变
|
||||
- 对照组:衬托人物特质
|
||||
- 考验:测试人物底线
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、人物细节清单
|
||||
|
||||
### 外在细节
|
||||
```
|
||||
- 外貌特征(不需要完美)
|
||||
- 穿着习惯(反映性格/经济)
|
||||
- 小动作(紧张时/思考时)
|
||||
- 说话方式(口头禅/语速/用词)
|
||||
```
|
||||
|
||||
### 生活细节
|
||||
```
|
||||
- 作息习惯
|
||||
- 饮食偏好
|
||||
- 休闲方式
|
||||
- 消费习惯
|
||||
```
|
||||
|
||||
### 内在细节
|
||||
```
|
||||
- 最骄傲的事
|
||||
- 最后悔的事
|
||||
- 最害怕的事
|
||||
- 最想要的东西
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、人物检查清单
|
||||
|
||||
### 立体度检查
|
||||
- [ ] 人物是否有明确的欲望和恐惧?
|
||||
- [ ] 人物是否有过去的创伤或经历?
|
||||
- [ ] 人物是否有道德灰色地带?
|
||||
- [ ] 人物是否会让读者又爱又恨?
|
||||
|
||||
### 真实度检查
|
||||
- [ ] 人物的行为是否符合其背景?
|
||||
- [ ] 人物的选择是否有充分动机?
|
||||
- [ ] 人物的情感反应是否真实?
|
||||
- [ ] 人物是否有成长或变化?
|
||||
|
||||
### 独特度检查
|
||||
- [ ] 这个人物是否可以被其他人替代?
|
||||
- [ ] 人物是否有独特的说话方式?
|
||||
- [ ] 人物是否有标志性的特征?
|
||||
- [ ] 人物是否让人印象深刻?
|
||||
@@ -0,0 +1,284 @@
|
||||
# 现实题材对话真实感
|
||||
|
||||
> 对话是现实题材的照妖镜。本文档提供写出真实对话的技巧。
|
||||
|
||||
---
|
||||
|
||||
## 一、真实对话特征
|
||||
|
||||
### 1. 不完整性
|
||||
```
|
||||
书面语:我认为这件事情应该这样处理。
|
||||
口语:这事儿吧...我觉得...算了,你看着办。
|
||||
```
|
||||
|
||||
### 2. 重复与犹豫
|
||||
```
|
||||
书面语:我不同意你的观点。
|
||||
口语:不是...我不是那个意思...就是...你懂吗?
|
||||
```
|
||||
|
||||
### 3. 省略与跳跃
|
||||
```
|
||||
书面语:你今天下班后要去哪里?
|
||||
口语:今晚?
|
||||
```
|
||||
|
||||
### 4. 口头禅与语气词
|
||||
```
|
||||
- 嗯、啊、哦、呃
|
||||
- 然后、就是、反正
|
||||
- 真的假的、不是吧、我去
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、不同身份的说话方式
|
||||
|
||||
### 按年龄
|
||||
```
|
||||
00后:
|
||||
- "绝绝子""yyds""无语子"
|
||||
- 语速快,网络用语多
|
||||
- 表情包式表达
|
||||
|
||||
80后:
|
||||
- 相对正式
|
||||
- 偶尔用网络语但不熟练
|
||||
- "这个...怎么说呢"
|
||||
|
||||
60后:
|
||||
- 更正式,少用网络语
|
||||
- 喜欢用成语或俗语
|
||||
- "我跟你说啊..."
|
||||
```
|
||||
|
||||
### 按职业
|
||||
```
|
||||
医生:
|
||||
- 专业术语自然带出
|
||||
- 说话简洁直接
|
||||
- "情况不太乐观"
|
||||
|
||||
律师:
|
||||
- 逻辑严密
|
||||
- 喜欢用"首先、其次、最后"
|
||||
- "从法律角度来说..."
|
||||
|
||||
程序员:
|
||||
- 技术词汇
|
||||
- 直接,不绕弯
|
||||
- "这个需求不合理"
|
||||
```
|
||||
|
||||
### 按性格
|
||||
```
|
||||
内向:
|
||||
- 话少,常用"嗯""哦"
|
||||
- 被动回应多
|
||||
- 长句少
|
||||
|
||||
外向:
|
||||
- 话多,主动发起话题
|
||||
- 语气词丰富
|
||||
- 喜欢反问
|
||||
|
||||
强势:
|
||||
- 陈述句多,疑问句少
|
||||
- 打断别人
|
||||
- "听我说"
|
||||
|
||||
弱势:
|
||||
- 疑问句多
|
||||
- 常用"可能""也许"
|
||||
- "你觉得呢?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、对话功能
|
||||
|
||||
### 1. 推进剧情
|
||||
```
|
||||
错误:用对话解释背景
|
||||
"你知道吗,我们公司成立于2010年,主要业务是..."
|
||||
|
||||
正确:用对话制造冲突
|
||||
"辞职信我放你桌上了。"
|
||||
"你疯了?"
|
||||
```
|
||||
|
||||
### 2. 展示人物
|
||||
```
|
||||
不要说"她很强势"
|
||||
而是写对话:
|
||||
"这事就这么定了。"
|
||||
"可是..."
|
||||
"没有可是。"
|
||||
```
|
||||
|
||||
### 3. 传递信息
|
||||
```
|
||||
错误:直接告知
|
||||
"他出轨了,对象是你闺蜜。"
|
||||
|
||||
正确:暗示+反应
|
||||
"昨晚我看到他的车...停在小区门口。"
|
||||
"哪个小区?"
|
||||
"...你闺蜜家那个。"
|
||||
```
|
||||
|
||||
### 4. 制造张力
|
||||
```
|
||||
潜台词比明说更有力:
|
||||
"你最近...还好吗?"
|
||||
"挺好的。"
|
||||
"那就好。"
|
||||
(两人都知道不好,但都不说破)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、对话节奏
|
||||
|
||||
### 快节奏(冲突/紧张)
|
||||
```
|
||||
"你什么意思?"
|
||||
"字面意思。"
|
||||
"你..."
|
||||
"我怎么了?"
|
||||
"我们离婚。"
|
||||
```
|
||||
|
||||
### 慢节奏(情感/沉重)
|
||||
```
|
||||
她沉默了很久。
|
||||
"你...是认真的?"
|
||||
他没有回答。
|
||||
窗外的雨越下越大。
|
||||
"我等你三年了。"她终于开口,"三年。"
|
||||
```
|
||||
|
||||
### 节奏变化
|
||||
```
|
||||
先快后慢:争吵后的沉默
|
||||
先慢后快:酝酿后的爆发
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、潜台词技巧
|
||||
|
||||
### 1. 说A指B
|
||||
```
|
||||
表面:"天气真好。"
|
||||
潜台词:"我不想聊这个话题。"
|
||||
```
|
||||
|
||||
### 2. 答非所问
|
||||
```
|
||||
"你爱我吗?"
|
||||
"饭做好了。"
|
||||
```
|
||||
|
||||
### 3. 沉默
|
||||
```
|
||||
"你能解释一下吗?"
|
||||
他没有说话。
|
||||
"我问你话呢。"
|
||||
还是沉默。
|
||||
```
|
||||
|
||||
### 4. 转移话题
|
||||
```
|
||||
"你昨晚去哪了?"
|
||||
"对了,你妈打电话来了。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、常见对话场景模板
|
||||
|
||||
### 争吵
|
||||
```
|
||||
特点:
|
||||
- 打断对方
|
||||
- 翻旧账
|
||||
- 人身攻击
|
||||
- 沉默或摔门
|
||||
|
||||
示例:
|
||||
"你每次都这样!"
|
||||
"我怎么了?我——"
|
||||
"你闭嘴!"
|
||||
"行,我闭嘴,我走还不行吗?"
|
||||
```
|
||||
|
||||
### 分手
|
||||
```
|
||||
特点:
|
||||
- 长沉默
|
||||
- 回忆
|
||||
- 最后的挣扎
|
||||
- 体面或崩溃
|
||||
|
||||
示例:
|
||||
"我们...还是算了吧。"
|
||||
"...为什么?"
|
||||
"没有为什么。"
|
||||
"是因为她吗?"
|
||||
"...你想多了。"
|
||||
```
|
||||
|
||||
### 表白
|
||||
```
|
||||
特点:
|
||||
- 紧张
|
||||
- 试探
|
||||
- 犹豫
|
||||
- 等待回应
|
||||
|
||||
示例:
|
||||
"那个...我有件事想跟你说。"
|
||||
"嗯?"
|
||||
"就是...算了,没什么。"
|
||||
"你说啊。"
|
||||
"我...我喜欢你。"
|
||||
```
|
||||
|
||||
### 摊牌
|
||||
```
|
||||
特点:
|
||||
- 直接
|
||||
- 证据
|
||||
- 质问
|
||||
- 等待解释
|
||||
|
||||
示例:
|
||||
"这是什么?"
|
||||
"...你翻我手机?"
|
||||
"我问你这是什么。"
|
||||
"我可以解释..."
|
||||
"那你解释。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、对话检查清单
|
||||
|
||||
### 真实性检查
|
||||
- [ ] 这句话真的会有人这么说吗?
|
||||
- [ ] 符合人物的年龄/职业/性格吗?
|
||||
- [ ] 有没有太书面化?
|
||||
- [ ] 有没有适当的口语特征?
|
||||
|
||||
### 功能性检查
|
||||
- [ ] 这段对话推进了剧情吗?
|
||||
- [ ] 展示了人物性格吗?
|
||||
- [ ] 有没有废话可以删?
|
||||
- [ ] 潜台词够不够?
|
||||
|
||||
### 节奏检查
|
||||
- [ ] 对话长度是否合适?
|
||||
- [ ] 快慢节奏是否有变化?
|
||||
- [ ] 沉默和停顿用得好吗?
|
||||
@@ -0,0 +1,273 @@
|
||||
# 现实题材剧情逻辑
|
||||
|
||||
> 现实题材最怕"不合理"。本文档提供构建严密剧情逻辑的方法。
|
||||
|
||||
---
|
||||
|
||||
## 一、逻辑链构建
|
||||
|
||||
### 因果链原则
|
||||
```
|
||||
每个事件都需要:
|
||||
- 原因(为什么发生)
|
||||
- 过程(怎么发生)
|
||||
- 结果(发生后怎样)
|
||||
- 影响(对后续的影响)
|
||||
```
|
||||
|
||||
### 示例
|
||||
```
|
||||
事件:她辞职了
|
||||
|
||||
错误写法:
|
||||
她突然辞职了。(没有原因)
|
||||
|
||||
正确写法:
|
||||
原因:连续加班三个月,母亲又住院
|
||||
过程:深夜在办公室收到医院电话,第二天递交辞呈
|
||||
结果:失去收入来源,但能照顾母亲
|
||||
影响:经济压力增大,为后续矛盾埋下伏笔
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、动机合理性
|
||||
|
||||
### 动机三要素
|
||||
```
|
||||
1. 欲望:想要什么
|
||||
2. 障碍:什么阻碍
|
||||
3. 行动:如何克服
|
||||
```
|
||||
|
||||
### 动机强度检验
|
||||
```
|
||||
问自己:这个动机足够让人物做出这个行为吗?
|
||||
|
||||
弱动机:
|
||||
"因为无聊所以出轨" ❌
|
||||
|
||||
强动机:
|
||||
"婚姻名存实亡五年,在最脆弱的时候遇到了理解她的人" ✓
|
||||
```
|
||||
|
||||
### 常见动机
|
||||
```
|
||||
生存类:金钱、健康、安全
|
||||
情感类:爱、被爱、归属
|
||||
尊严类:尊重、认可、地位
|
||||
自我类:成长、自由、意义
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、巧合处理
|
||||
|
||||
### 巧合使用原则
|
||||
```
|
||||
1. 巧合可以制造困境,不能解决困境
|
||||
2. 一个故事最多用1-2个巧合
|
||||
3. 巧合需要铺垫
|
||||
```
|
||||
|
||||
### 错误示例
|
||||
```
|
||||
"正当她走投无路时,突然中了彩票。" ❌
|
||||
(巧合解决困境)
|
||||
```
|
||||
|
||||
### 正确示例
|
||||
```
|
||||
"她在咖啡店偶遇前男友,而她正挽着现任的手。"
|
||||
(巧合制造困境)
|
||||
```
|
||||
|
||||
### 巧合铺垫技巧
|
||||
```
|
||||
让巧合看起来不那么巧:
|
||||
- 提前暗示可能性
|
||||
- 给出合理的时空条件
|
||||
- 用人物反应强化真实感
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、时间线管理
|
||||
|
||||
### 时间线检查
|
||||
```
|
||||
- 事件发生的顺序是否合理?
|
||||
- 时间间隔是否足够?
|
||||
- 有没有时间冲突?
|
||||
```
|
||||
|
||||
### 常见时间问题
|
||||
```
|
||||
问题1:时间不够
|
||||
"她用一周时间从零开始学会了编程" ❌
|
||||
|
||||
问题2:时间冲突
|
||||
"她上午在北京开会,下午在上海见客户" ❌
|
||||
(除非有合理的交通安排)
|
||||
|
||||
问题3:时间跳跃不自然
|
||||
"三年后"突然出现,没有任何过渡 ❌
|
||||
```
|
||||
|
||||
### 时间线工具
|
||||
```
|
||||
建议制作时间轴:
|
||||
- 列出所有重要事件
|
||||
- 标注具体时间
|
||||
- 检查逻辑冲突
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、人物行为逻辑
|
||||
|
||||
### 行为一致性
|
||||
```
|
||||
人物的行为要符合其:
|
||||
- 性格设定
|
||||
- 过往经历
|
||||
- 当前处境
|
||||
- 认知水平
|
||||
```
|
||||
|
||||
### 行为转变
|
||||
```
|
||||
如果人物行为发生重大转变,需要:
|
||||
- 足够的触发事件
|
||||
- 渐进的心理变化
|
||||
- 合理的转变过程
|
||||
```
|
||||
|
||||
### 示例
|
||||
```
|
||||
错误:
|
||||
"一向懦弱的她突然变得强势起来。"
|
||||
|
||||
正确:
|
||||
"被欺负了三年后,当她看到女儿也被同样对待时,
|
||||
她第一次站了出来。"
|
||||
(有触发事件,有合理动机)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、信息传递逻辑
|
||||
|
||||
### 信息来源
|
||||
```
|
||||
人物知道的信息必须有来源:
|
||||
- 亲眼所见
|
||||
- 亲耳所闻
|
||||
- 他人告知
|
||||
- 推理得出
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
```
|
||||
问题:人物知道不该知道的事
|
||||
"她知道他在外面有人了。"
|
||||
(她怎么知道的?需要交代)
|
||||
|
||||
解决:
|
||||
"她在他衬衫上闻到了陌生的香水味。"
|
||||
```
|
||||
|
||||
### 信息差设计
|
||||
```
|
||||
利用信息差制造戏剧性:
|
||||
- 读者知道,人物不知道(悬念)
|
||||
- 人物知道,读者不知道(反转)
|
||||
- A知道,B不知道(误会)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、社会规则逻辑
|
||||
|
||||
### 法律常识
|
||||
```
|
||||
涉及法律问题时要准确:
|
||||
- 离婚程序
|
||||
- 财产分割
|
||||
- 抚养权
|
||||
- 劳动法
|
||||
- 刑事责任
|
||||
```
|
||||
|
||||
### 职场规则
|
||||
```
|
||||
- 晋升流程
|
||||
- 辞职程序
|
||||
- 竞业协议
|
||||
- 加班规定
|
||||
```
|
||||
|
||||
### 医疗常识
|
||||
```
|
||||
- 疾病症状
|
||||
- 治疗流程
|
||||
- 医院规则
|
||||
- 医保报销
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、逻辑漏洞检查清单
|
||||
|
||||
### 因果检查
|
||||
- [ ] 每个重要事件都有原因吗?
|
||||
- [ ] 原因足够充分吗?
|
||||
- [ ] 结果符合逻辑吗?
|
||||
|
||||
### 动机检查
|
||||
- [ ] 人物的行为有足够动机吗?
|
||||
- [ ] 动机符合人物性格吗?
|
||||
- [ ] 读者能理解这个动机吗?
|
||||
|
||||
### 时间检查
|
||||
- [ ] 时间线是否清晰?
|
||||
- [ ] 有没有时间冲突?
|
||||
- [ ] 时间跨度是否合理?
|
||||
|
||||
### 信息检查
|
||||
- [ ] 人物的信息来源清楚吗?
|
||||
- [ ] 有没有"全知"问题?
|
||||
- [ ] 信息传递是否合理?
|
||||
|
||||
### 常识检查
|
||||
- [ ] 有没有违反法律常识?
|
||||
- [ ] 有没有违反职业常识?
|
||||
- [ ] 有没有违反生活常识?
|
||||
|
||||
---
|
||||
|
||||
## 九、修复逻辑漏洞
|
||||
|
||||
### 方法1:补充铺垫
|
||||
```
|
||||
发现漏洞:她怎么知道密码的?
|
||||
修复:前文加一句"她无意中看到他输入密码"
|
||||
```
|
||||
|
||||
### 方法2:调整设定
|
||||
```
|
||||
发现漏洞:时间不够
|
||||
修复:把"一周"改成"三个月"
|
||||
```
|
||||
|
||||
### 方法3:增加解释
|
||||
```
|
||||
发现漏洞:行为不合理
|
||||
修复:增加心理描写,解释动机
|
||||
```
|
||||
|
||||
### 方法4:删除情节
|
||||
```
|
||||
发现漏洞:无法自圆其说
|
||||
修复:删掉这个情节,换一种方式
|
||||
```
|
||||
@@ -0,0 +1,229 @@
|
||||
# 现实题材真实感锚定
|
||||
|
||||
> 现实题材的核心是"真实感"。本文档提供让读者相信故事的技巧。
|
||||
|
||||
---
|
||||
|
||||
## 一、真实感三层架构
|
||||
|
||||
| 层次 | 内容 | 作用 |
|
||||
|------|------|------|
|
||||
| 表层 | 细节真实 | 让读者"看到"场景 |
|
||||
| 中层 | 逻辑真实 | 让读者"相信"情节 |
|
||||
| 深层 | 情感真实 | 让读者"共鸣"人物 |
|
||||
|
||||
---
|
||||
|
||||
## 二、细节真实技巧
|
||||
|
||||
### 1. 职业细节
|
||||
```
|
||||
错误:她是一名医生,每天救死扶伤。
|
||||
正确:她是急诊科的住院医师,值完36小时班后,
|
||||
在休息室的行军床上睡了4个小时,
|
||||
被护士叫醒时,嘴里还含着没吃完的面包。
|
||||
```
|
||||
|
||||
### 2. 生活细节
|
||||
```
|
||||
错误:他很穷。
|
||||
正确:月底还有三天,他的银行卡余额是47.6元。
|
||||
他把外卖软件上的"满减"研究了半小时,
|
||||
最后点了一份9.9的黄焖鸡。
|
||||
```
|
||||
|
||||
### 3. 环境细节
|
||||
```
|
||||
错误:这是一个老旧的小区。
|
||||
正确:楼道里的声控灯坏了三个月没人修,
|
||||
她每天摸黑爬六楼,
|
||||
楼梯扶手上的油漆已经斑驳脱落。
|
||||
```
|
||||
|
||||
### 4. 时代细节
|
||||
```
|
||||
2024年特征:
|
||||
- 手机支付、外卖、网约车
|
||||
- 996、内卷、躺平
|
||||
- 短视频、直播带货
|
||||
- 房贷、车贷、消费贷
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、逻辑真实技巧
|
||||
|
||||
### 1. 因果链完整
|
||||
```
|
||||
错误:她突然辞职了。
|
||||
正确:连续加班三个月后,她在凌晨两点的办公室里
|
||||
收到了母亲住院的消息。
|
||||
第二天,她递交了辞职信。
|
||||
```
|
||||
|
||||
### 2. 动机合理
|
||||
```
|
||||
错误:他为了爱情放弃了一切。
|
||||
正确:他权衡了三个月。
|
||||
最后让他下定决心的,
|
||||
是她在机场送他时说的那句"我等你"。
|
||||
```
|
||||
|
||||
### 3. 后果真实
|
||||
```
|
||||
错误:她创业成功了。
|
||||
正确:她的公司终于盈利了,
|
||||
但她也失去了三年的青春、
|
||||
一段婚姻、和曾经最好的朋友。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、情感真实技巧
|
||||
|
||||
### 1. 复杂情感
|
||||
```
|
||||
单一情感(假):她恨他。
|
||||
复杂情感(真):她恨他,但更恨自己还爱着他。
|
||||
每次看到他的朋友圈,
|
||||
她都会先点进去看,再骂自己犯贱。
|
||||
```
|
||||
|
||||
### 2. 矛盾心理
|
||||
```
|
||||
人物内心的真实矛盾:
|
||||
- 想辞职又怕找不到工作
|
||||
- 想离婚又舍不得孩子
|
||||
- 想表白又怕被拒绝
|
||||
- 想回家又怕面对父母的催婚
|
||||
```
|
||||
|
||||
### 3. 情绪渐变
|
||||
```
|
||||
错误:她突然崩溃大哭。
|
||||
正确:她一直忍着,直到回到家,
|
||||
关上门的那一刻,
|
||||
眼泪才终于流下来。
|
||||
她蹲在玄关,哭了整整十分钟。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、常见职业真实感要点
|
||||
|
||||
### 医生
|
||||
```
|
||||
- 值班制度(24小时/36小时)
|
||||
- 医患关系紧张
|
||||
- 论文、职称压力
|
||||
- 手术前的紧张、术后的疲惫
|
||||
- 面对死亡的无力感
|
||||
```
|
||||
|
||||
### 律师
|
||||
```
|
||||
- 加班是常态
|
||||
- 案源压力
|
||||
- 法律条文的枯燥
|
||||
- 当事人的不理解
|
||||
- 胜诉的成就感vs败诉的挫败
|
||||
```
|
||||
|
||||
### 程序员
|
||||
```
|
||||
- 996/007
|
||||
- 需求变更的崩溃
|
||||
- Bug修不完
|
||||
- 35岁危机
|
||||
- 头发和健康问题
|
||||
```
|
||||
|
||||
### 教师
|
||||
```
|
||||
- 备课、批改作业
|
||||
- 家长群的压力
|
||||
- 学生的叛逆期
|
||||
- 职称评定
|
||||
- 寒暑假的"隐形加班"
|
||||
```
|
||||
|
||||
### 销售
|
||||
```
|
||||
- 业绩压力
|
||||
- 客户的刁难
|
||||
- 应酬文化
|
||||
- 提成制度
|
||||
- 月底冲业绩
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、社会现实锚点
|
||||
|
||||
### 经济压力
|
||||
```
|
||||
- 房贷:月供占收入的多少
|
||||
- 车贷:养车成本
|
||||
- 教育:补习班、学区房
|
||||
- 医疗:一场大病的花费
|
||||
- 养老:父母的医疗和养老
|
||||
```
|
||||
|
||||
### 职场现实
|
||||
```
|
||||
- 内卷:加班文化
|
||||
- 裁员:35岁危机
|
||||
- 晋升:办公室政治
|
||||
- 性别:职场歧视
|
||||
- 学历:第一学历歧视
|
||||
```
|
||||
|
||||
### 家庭现实
|
||||
```
|
||||
- 催婚:父母的压力
|
||||
- 婆媳:家庭矛盾
|
||||
- 育儿:教育焦虑
|
||||
- 养老:独生子女的压力
|
||||
- 离婚:财产分割、孩子抚养
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、避免失真的检查清单
|
||||
|
||||
### 细节检查
|
||||
- [ ] 职业描写是否符合实际?
|
||||
- [ ] 收入水平是否合理?
|
||||
- [ ] 生活场景是否真实?
|
||||
- [ ] 时代特征是否准确?
|
||||
|
||||
### 逻辑检查
|
||||
- [ ] 人物动机是否充分?
|
||||
- [ ] 情节发展是否合理?
|
||||
- [ ] 后果是否符合现实?
|
||||
- [ ] 时间线是否可行?
|
||||
|
||||
### 情感检查
|
||||
- [ ] 情感反应是否真实?
|
||||
- [ ] 心理变化是否渐进?
|
||||
- [ ] 是否有矛盾和复杂性?
|
||||
- [ ] 读者能否产生共鸣?
|
||||
|
||||
---
|
||||
|
||||
## 八、真实感 vs 戏剧性平衡
|
||||
|
||||
### 原则
|
||||
```
|
||||
真实感是基础,戏剧性是调味
|
||||
太真实 = 流水账
|
||||
太戏剧 = 假大空
|
||||
```
|
||||
|
||||
### 平衡技巧
|
||||
```
|
||||
1. 用真实的细节包装戏剧性的情节
|
||||
2. 用合理的逻辑支撑巧合的发生
|
||||
3. 用复杂的情感软化极端的冲突
|
||||
4. 用日常的场景衬托高光的时刻
|
||||
```
|
||||
@@ -0,0 +1,232 @@
|
||||
# 现实题材社会议题处理
|
||||
|
||||
> 现实题材常涉及敏感社会议题。本文档提供安全且有深度的处理方式。
|
||||
|
||||
---
|
||||
|
||||
## 一、常见社会议题
|
||||
|
||||
| 类别 | 议题 | 敏感度 |
|
||||
|------|------|--------|
|
||||
| 职场 | 996、裁员、性骚扰、职场霸凌 | 中 |
|
||||
| 家庭 | 家暴、出轨、婆媳矛盾、原生家庭 | 中 |
|
||||
| 社会 | 贫富差距、阶层固化、地域歧视 | 高 |
|
||||
| 性别 | 性别歧视、生育权、女性困境 | 高 |
|
||||
| 教育 | 内卷、学区房、教育公平 | 中 |
|
||||
| 医疗 | 看病难、医患矛盾、大病返贫 | 中 |
|
||||
|
||||
---
|
||||
|
||||
## 二、处理原则
|
||||
|
||||
### 1. 呈现而非评判
|
||||
```
|
||||
错误:这个社会太不公平了!(直接评判)
|
||||
正确:她看着银行卡里的余额,又看了看医院的账单。
|
||||
差距是三个零。(呈现事实,让读者自己感受)
|
||||
```
|
||||
|
||||
### 2. 个体视角切入
|
||||
```
|
||||
错误:讨论整个社会的996问题
|
||||
正确:写一个具体的人在996中的挣扎
|
||||
- 她的身体变化
|
||||
- 她的家庭影响
|
||||
- 她的心理状态
|
||||
```
|
||||
|
||||
### 3. 多元立场呈现
|
||||
```
|
||||
不要只写一方的声音:
|
||||
- 员工的苦 + 老板的难
|
||||
- 患者的怨 + 医生的累
|
||||
- 婆婆的固执 + 媳妇的委屈
|
||||
```
|
||||
|
||||
### 4. 留有希望
|
||||
```
|
||||
即使是沉重的议题,也要有光:
|
||||
- 困境中的温情
|
||||
- 绝望中的坚持
|
||||
- 黑暗中的微光
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、具体议题处理指南
|
||||
|
||||
### 职场996
|
||||
```
|
||||
切入角度:
|
||||
- 一个人的身体健康变化
|
||||
- 一段感情因加班而破裂
|
||||
- 一个家庭因工作而疏离
|
||||
|
||||
避免:
|
||||
- 直接批判企业/制度
|
||||
- 煽动对立情绪
|
||||
- 过于绝望的结局
|
||||
|
||||
可以:
|
||||
- 展示个体的挣扎与选择
|
||||
- 呈现不同人的不同态度
|
||||
- 给出个人层面的出路
|
||||
```
|
||||
|
||||
### 家庭暴力
|
||||
```
|
||||
切入角度:
|
||||
- 受害者的心理变化
|
||||
- 旁观者的无力感
|
||||
- 施暴者的成因(不是洗白)
|
||||
|
||||
避免:
|
||||
- 美化或浪漫化暴力
|
||||
- 让受害者"感化"施暴者
|
||||
- 过于血腥的描写
|
||||
|
||||
可以:
|
||||
- 展示受害者的觉醒
|
||||
- 呈现求助的困难
|
||||
- 给出逃离的可能
|
||||
```
|
||||
|
||||
### 性别议题
|
||||
```
|
||||
切入角度:
|
||||
- 具体的不公平事件
|
||||
- 个人的成长与觉醒
|
||||
- 两性关系的复杂性
|
||||
|
||||
避免:
|
||||
- 极端的性别对立
|
||||
- 标签化任何一方
|
||||
- 说教式的女权宣言
|
||||
|
||||
可以:
|
||||
- 展示真实的困境
|
||||
- 呈现觉醒的过程
|
||||
- 探讨平等的可能
|
||||
```
|
||||
|
||||
### 贫富差距
|
||||
```
|
||||
切入角度:
|
||||
- 两个阶层的人的相遇
|
||||
- 一个人的阶层跨越尝试
|
||||
- 金钱与幸福的关系
|
||||
|
||||
避免:
|
||||
- 仇富或嫌贫
|
||||
- 简单的阶层对立
|
||||
- 不切实际的逆袭
|
||||
|
||||
可以:
|
||||
- 展示不同阶层的真实生活
|
||||
- 呈现努力与机遇的关系
|
||||
- 探讨金钱之外的价值
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、安全写作技巧
|
||||
|
||||
### 1. 模糊化处理
|
||||
```
|
||||
敏感内容模糊化:
|
||||
- 不点名具体公司/地区
|
||||
- 用"某市""某公司"代替
|
||||
- 时间线适当模糊
|
||||
```
|
||||
|
||||
### 2. 虚构声明
|
||||
```
|
||||
在文首或文末加入:
|
||||
"本故事纯属虚构,如有雷同,纯属巧合。"
|
||||
```
|
||||
|
||||
### 3. 平衡叙事
|
||||
```
|
||||
每个敏感议题都呈现多个角度:
|
||||
- 不同立场的人物
|
||||
- 不同的观点和选择
|
||||
- 不同的结局可能
|
||||
```
|
||||
|
||||
### 4. 情感落点
|
||||
```
|
||||
把落点放在情感而非议题:
|
||||
- 不是"996不对"
|
||||
- 而是"她在996中失去了什么"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、议题与故事的融合
|
||||
|
||||
### 议题作为背景
|
||||
```
|
||||
议题是故事发生的环境,不是故事本身
|
||||
示例:996是背景,爱情/成长才是主线
|
||||
```
|
||||
|
||||
### 议题作为冲突
|
||||
```
|
||||
议题制造人物的困境和选择
|
||||
示例:生育问题成为夫妻矛盾的导火索
|
||||
```
|
||||
|
||||
### 议题作为成长
|
||||
```
|
||||
人物在面对议题中成长
|
||||
示例:她在职场歧视中学会了为自己发声
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、敏感度自检清单
|
||||
|
||||
### 发布前检查
|
||||
- [ ] 是否有可能被理解为攻击特定群体?
|
||||
- [ ] 是否有可能引发极端情绪?
|
||||
- [ ] 是否呈现了多元视角?
|
||||
- [ ] 是否有希望和出路?
|
||||
- [ ] 是否可能触及红线?
|
||||
|
||||
### 红线警示
|
||||
```
|
||||
绝对避免:
|
||||
- 涉及政治敏感话题
|
||||
- 煽动群体对立
|
||||
- 传播负面价值观
|
||||
- 美化违法行为
|
||||
- 泄露国家机密
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、成功案例分析
|
||||
|
||||
### 《我的前半生》- 婚姻议题
|
||||
```
|
||||
成功点:
|
||||
- 聚焦个人成长而非批判出轨
|
||||
- 展示女性独立的可能
|
||||
- 多元人物,没有绝对的好坏
|
||||
```
|
||||
|
||||
### 《欢乐颂》- 阶层议题
|
||||
```
|
||||
成功点:
|
||||
- 五个女孩代表不同阶层
|
||||
- 展示各自的困境和努力
|
||||
- 友情作为温暖的底色
|
||||
```
|
||||
|
||||
### 《都挺好》- 原生家庭
|
||||
```
|
||||
成功点:
|
||||
- 深入剖析家庭问题
|
||||
- 人物复杂,不脸谱化
|
||||
- 最终走向和解而非对立
|
||||
```
|
||||
@@ -0,0 +1,524 @@
|
||||
# 线索设计与公平性 (Clue Design & Fair Play)
|
||||
|
||||
> **核心原则**: 线索是推理的基石。公平的线索设计 = 读者能看到 + 读者能理解 + 读者能推理。
|
||||
|
||||
---
|
||||
|
||||
## 1. 什么是"公平线索"?
|
||||
|
||||
### 定义
|
||||
**公平线索(Fair Clue)**: 侦探发现的所有关键线索,必须同时向读者展示,且读者**理论上**可以基于这些线索推理出真相。
|
||||
|
||||
### 三大标准
|
||||
```markdown
|
||||
✅ 标准1:可见性(Visibility)
|
||||
线索必须在文中明确呈现,不能只存在于侦探脑中
|
||||
|
||||
✅ 标准2:可理解性(Comprehensibility)
|
||||
线索的含义必须是读者能理解的,不能依赖专业知识
|
||||
|
||||
✅ 标准3:可推导性(Deducibility)
|
||||
基于线索,读者应该能推导出(或接近)真相
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 线索的分类
|
||||
|
||||
### 按功能分类
|
||||
|
||||
#### 类型1:指向性线索(Pointing Clue)
|
||||
**作用**: 直接指向凶手或真相
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
线索: 凶手在现场留下了一枚刻有姓名的戒指
|
||||
→ 直接指向凶手身份
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型2:排除性线索(Elimination Clue)
|
||||
**作用**: 排除某些可能性,缩小范围
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
线索: 凶案发生时是凌晨3点,而张三有不在场证明
|
||||
→ 排除张三是凶手的可能
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型3:关联性线索(Connecting Clue)
|
||||
**作用**: 连接两个看似无关的事件或人物
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
线索A: 死者生前收到一封匿名恐吓信
|
||||
线索B: 李四的打字机缺少一个字母键
|
||||
→ 推测:恐吓信可能是李四所写
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型4:时间线索(Temporal Clue)
|
||||
**作用**: 确定事件发生的时间顺序
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
线索: 死者的手表停在晚上8点
|
||||
→ 推测:案发时间可能是晚上8点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型5:反转线索(Reversal Clue)
|
||||
**作用**: 推翻之前的推理,引发反转
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
前期线索: 凶器是一把匕首
|
||||
反转线索: 法医发现致命伤其实是钝器造成的
|
||||
→ 推翻匕首是凶器的假设
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 按重要性分类
|
||||
|
||||
| 等级 | 名称 | 作用 | 占比 |
|
||||
|------|------|------|------|
|
||||
| **A级** | 核心线索 | 直接指向真相 | 10% |
|
||||
| **B级** | 关键线索 | 缩小嫌疑范围 | 30% |
|
||||
| **C级** | 辅助线索 | 提供背景信息 | 40% |
|
||||
| **D级** | 装饰线索 | 营造氛围 | 20% |
|
||||
|
||||
**注意**: A级线索虽然重要,但数量不宜过多,否则太容易被猜到。
|
||||
|
||||
---
|
||||
|
||||
## 3. 线索埋设技巧
|
||||
|
||||
### 技巧1:前置埋设(Foreshadowing)
|
||||
**定义**: 在案发前或侦探调查前,"不经意"提及关键物品或信息。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:毒杀案
|
||||
第3章(案发前):
|
||||
"张三从花园里摘了几朵夹竹桃,说要装饰房间。"
|
||||
|
||||
第15章(案发后):
|
||||
法医:"死者是夹竹桃中毒而亡。"
|
||||
→ 读者恍然大悟:第3章的夹竹桃是关键!
|
||||
```
|
||||
|
||||
**要点**: 前置埋设要**自然**,不能太刻意。
|
||||
|
||||
---
|
||||
|
||||
### 技巧2:伪装成背景(Camouflage)
|
||||
**定义**: 把关键线索藏在环境描写或日常对话中。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:密室杀人
|
||||
第7章(环境描写):
|
||||
"房间角落的壁橱门微微开着,门框上有些许划痕。"
|
||||
|
||||
第20章(揭示):
|
||||
侦探:"凶手曾藏在壁橱里,划痕是他进出时留下的。"
|
||||
```
|
||||
|
||||
**要点**: 读者第一次看时可能忽略,但回看时能发现。
|
||||
|
||||
---
|
||||
|
||||
### 技巧3:多次提及(Repetition)
|
||||
**定义**: 重要线索在文中出现2-3次,加深读者印象。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第5章: 死者手上有一块昂贵的手表
|
||||
第12章: 侦探注意到死者手表的指针停在8点
|
||||
第18章: 侦探:"这块手表是关键……"
|
||||
```
|
||||
|
||||
**要点**: 每次提及角度不同,逐步揭示重要性。
|
||||
|
||||
---
|
||||
|
||||
### 技巧4:对比呈现(Contrast)
|
||||
**定义**: 通过对比,让读者注意到异常。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
张三的证言: "我8点就回家了。"
|
||||
监控记录: 张三8点15分才离开公司。
|
||||
→ 读者发现:张三在撒谎
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 技巧5:分散呈现(Scattered Clues)
|
||||
**定义**: 把完整线索拆成多个碎片,分散在不同章节。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第10章: 死者日记写着"7月15日见到了他"
|
||||
第15章: 李四的行程表显示7月15日去了A市
|
||||
第20章: A市是死者的老家
|
||||
→ 推测:李四在A市见过死者
|
||||
```
|
||||
|
||||
**要点**: 碎片单独看不起眼,组合起来才有意义。
|
||||
|
||||
---
|
||||
|
||||
## 4. 线索呈现的"三层法"
|
||||
|
||||
### 层次1:明线索(Obvious Clue)
|
||||
**定义**: 明确告诉读者"这是线索"
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
侦探拿起一根头发:"这是关键线索。"
|
||||
```
|
||||
|
||||
**作用**: 引导读者注意,适合新手向作品
|
||||
|
||||
---
|
||||
|
||||
### 层次2:暗线索(Subtle Clue)
|
||||
**定义**: 不明说,但有经验的读者能看出来
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
侦探扫视房间,目光在壁橱门上停留了片刻。
|
||||
```
|
||||
|
||||
**作用**: 给读者推理空间,增加参与感
|
||||
|
||||
---
|
||||
|
||||
### 层次3:隐线索(Hidden Clue)
|
||||
**定义**: 极其隐蔽,只有回看时才能发现
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第5章(闲聊):
|
||||
李四:"我最近总是失眠,都靠安眠药了。"
|
||||
|
||||
第30章(揭示):
|
||||
侦探:"凶手用安眠药迷晕死者……李四曾说过他有安眠药!"
|
||||
```
|
||||
|
||||
**作用**: 制造"恍然大悟"的惊喜感
|
||||
|
||||
---
|
||||
|
||||
### 三层配比建议
|
||||
```markdown
|
||||
明线索: 40%(确保读者能跟上)
|
||||
暗线索: 40%(给推理空间)
|
||||
隐线索: 20%(制造惊喜)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 公平性检验清单
|
||||
|
||||
### 检查项1:线索是否向读者呈现?
|
||||
**错误示例**:
|
||||
```markdown
|
||||
侦探(心想):我在现场发现了一个关键证据。
|
||||
→ 读者不知道是什么证据
|
||||
```
|
||||
|
||||
**正确示例**:
|
||||
```markdown
|
||||
侦探在桌子下方发现了一张撕碎的纸条。
|
||||
→ 读者同步看到证据
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 检查项2:线索是否可理解?
|
||||
**错误示例**:
|
||||
```markdown
|
||||
侦探:"这种斑点是氰化钾中毒的典型症状。"
|
||||
→ 读者不了解化学知识,无法推理
|
||||
```
|
||||
|
||||
**正确示例**:
|
||||
```markdown
|
||||
侦探:"这种斑点是中毒症状。而现场只有李四有化学背景。"
|
||||
→ 简化专业知识,读者可理解
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 检查项3:线索是否足够?
|
||||
**标准**:
|
||||
```markdown
|
||||
✅ 核心真相至少需要 **3条独立线索** 支持
|
||||
✅ 每条线索单独看不明显,组合起来指向真相
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
线索1: 凶器上有李四的指纹
|
||||
线索2: 李四与死者有财务纠纷
|
||||
线索3: 李四的不在场证明有漏洞
|
||||
→ 三条线索综合,指向李四是凶手
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 检查项4:有没有"作者全知陷阱"?
|
||||
**定义**: 作者知道真相,所以默认读者也"应该"知道。
|
||||
|
||||
**错误示例**:
|
||||
```markdown
|
||||
作者(潜意识):我已经在第5章提到了钥匙,读者应该能想到密室诡计。
|
||||
实际:读者根本没注意到钥匙
|
||||
```
|
||||
|
||||
**避免方法**:
|
||||
```markdown
|
||||
✅ 重要线索至少提及 **2次**
|
||||
✅ 让测试读者试读,看他们能否推理出真相
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 线索与红鲱鱼的平衡
|
||||
|
||||
### 黄金比例
|
||||
```markdown
|
||||
真线索(指向真相): 60%
|
||||
红鲱鱼(误导): 40%
|
||||
```
|
||||
|
||||
**作用**: 既给读者推理线索,又不让真相太容易被猜到。
|
||||
|
||||
---
|
||||
|
||||
### 红鲱鱼的使用原则
|
||||
**原则1**: 红鲱鱼必须**事后可解释**
|
||||
|
||||
**错误示例**:
|
||||
```markdown
|
||||
第10章: 张三在现场,手上有血
|
||||
第30章: 侦探:"那个不重要,真凶是李四。"
|
||||
→ 张三的血迹没有解释,读者会觉得不合理
|
||||
```
|
||||
|
||||
**正确示例**:
|
||||
```markdown
|
||||
第10章: 张三在现场,手上有血
|
||||
第30章: 侦探:"张三的血是因为他帮死者包扎过伤口。"
|
||||
→ 合理解释
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**原则2**: 红鲱鱼不能完全凭空捏造
|
||||
|
||||
**错误示例**:
|
||||
```markdown
|
||||
作者突然编造:"李四其实有个双胞胎兄弟。"
|
||||
→ 前文从未提及,读者无法推理
|
||||
```
|
||||
|
||||
**正确示例**:
|
||||
```markdown
|
||||
第5章: 李四的母亲说:"你小时候总和弟弟打架。"
|
||||
第30章: 揭示李四有个弟弟,是真凶
|
||||
→ 前文有伏笔
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 线索密度控制
|
||||
|
||||
### 密度建议
|
||||
```markdown
|
||||
每章线索数量: 1-3条
|
||||
每10章至少有: 1条核心线索(A级)
|
||||
```
|
||||
|
||||
**过少**: 读者无法推理
|
||||
**过多**: 读者信息过载
|
||||
|
||||
---
|
||||
|
||||
### 线索分布策略
|
||||
|
||||
#### 前期(1-30%进度)
|
||||
```markdown
|
||||
- 埋设基础线索
|
||||
- 建立人物关系
|
||||
- 暗示可疑点
|
||||
```
|
||||
|
||||
#### 中期(30-70%进度)
|
||||
```markdown
|
||||
- 逐步揭示关键线索
|
||||
- 设置红鲱鱼误导
|
||||
- 制造小反转
|
||||
```
|
||||
|
||||
#### 后期(70-100%进度)
|
||||
```markdown
|
||||
- 汇总所有线索
|
||||
- 揭示核心真相
|
||||
- 解释红鲱鱼
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 线索设计案例分析
|
||||
|
||||
### 案例1:《东方快车谋杀案》
|
||||
|
||||
**线索呈现**:
|
||||
```markdown
|
||||
线索1: 12个嫌疑人都有完美不在场证明
|
||||
线索2: 死者身上有12处刀伤
|
||||
线索3: 每个嫌疑人与死者都有间接联系
|
||||
```
|
||||
|
||||
**公平性**:
|
||||
- ✅ 所有线索公开呈现
|
||||
- ✅ 读者可以推理:12人集体作案
|
||||
- ✅ 真相既意外又合理
|
||||
|
||||
---
|
||||
|
||||
### 案例2:《无人生还》
|
||||
|
||||
**线索呈现**:
|
||||
```markdown
|
||||
线索1: 童谣暗示杀人顺序
|
||||
线索2: 每个被害者死法与童谣对应
|
||||
线索3: 法官"被杀"后医生确认死亡
|
||||
```
|
||||
|
||||
**公平性**:
|
||||
- ✅ 童谣提前呈现
|
||||
- ✅ 读者可以推测死亡顺序
|
||||
- ✅ 但法官假死的诡计难以预料(意外性)
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见线索设计错误
|
||||
|
||||
### 错误1:线索过于明显
|
||||
**示例**:
|
||||
```markdown
|
||||
第5章: 李四说:"我恨死他了,真想杀了他!"
|
||||
→ 太明显,读者立刻怀疑李四
|
||||
```
|
||||
|
||||
**改进**:
|
||||
```markdown
|
||||
第5章: 李四(低声):"总有一天……"
|
||||
→ 暗示而非明说
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误2:线索过于隐蔽
|
||||
**示例**:
|
||||
```markdown
|
||||
第3章: 桌上有一只苍蝇
|
||||
第30章: 侦探:"苍蝇证明了房间曾被开过窗!"
|
||||
→ 读者根本没注意苍蝇
|
||||
```
|
||||
|
||||
**改进**:
|
||||
```markdown
|
||||
第3章: 侦探皱眉:"奇怪,这个季节怎么会有苍蝇?"
|
||||
→ 引起读者注意
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误3:线索自相矛盾
|
||||
**示例**:
|
||||
```markdown
|
||||
第10章: 死者手表停在8点
|
||||
第15章: 目击者说8点30分听到枪声
|
||||
→ 时间线矛盾
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
第20章: 侦探:"手表是凶手故意调慢的!"
|
||||
→ 解释矛盾
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 线索设计工具
|
||||
|
||||
### 工具1:线索登记表
|
||||
```markdown
|
||||
| 线索ID | 出现章节 | 内容 | 类型 | 重要性 | 指向 |
|
||||
|--------|---------|------|------|--------|------|
|
||||
| C01 | 第5章 | 血迹手帕 | 物证 | A级 | 李四 |
|
||||
| C02 | 第10章 | 不在场证明 | 证言 | B级 | 排除张三 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工具2:公平性检查清单
|
||||
```markdown
|
||||
- [ ] 所有A级线索都已向读者呈现?
|
||||
- [ ] 线索是否可理解(无需专业知识)?
|
||||
- [ ] 是否至少3条独立线索指向真相?
|
||||
- [ ] 红鲱鱼是否事后可解释?
|
||||
- [ ] 线索密度是否合理(每章1-3条)?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工具3:测试读者验证法
|
||||
**步骤**:
|
||||
```markdown
|
||||
1. 找3-5名测试读者
|
||||
2. 让他们读到揭示真相前的一章
|
||||
3. 询问他们的推理结果
|
||||
4. 如果没人能推理出真相 → 线索不够
|
||||
如果所有人都猜到 → 线索太明显
|
||||
如果少数人猜到 → 公平性良好
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 线索设计速查表
|
||||
|
||||
| 线索类型 | 作用 | 数量建议 | 呈现方式 |
|
||||
|---------|------|---------|---------|
|
||||
| **指向性线索** | 指向凶手 | 2-3条 | 明线索+暗线索 |
|
||||
| **排除性线索** | 缩小范围 | 3-5条 | 明线索 |
|
||||
| **关联性线索** | 连接事件 | 2-3条 | 暗线索 |
|
||||
| **反转线索** | 制造反转 | 1-2条 | 隐线索 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:线索设计黄金法则
|
||||
|
||||
1. **3-2-1法则**: 核心真相需要至少**3条独立线索**,其中**2条明显**,**1条隐蔽**
|
||||
2. **双重呈现**: 重要线索必须**至少出现2次**
|
||||
3. **合理解释**: 所有红鲱鱼必须**事后可解释**
|
||||
4. **测试验证**: 完稿后让测试读者验证公平性
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
**公平的线索设计 = 可见性 + 可理解性 + 可推导性**
|
||||
|
||||
记住:线索不是用来"炫技"的,而是用来让读者**有能力**推理出真相的。公平竞技的乐趣在于"我也能想到"。
|
||||
@@ -0,0 +1,431 @@
|
||||
# 本格推理核心要素 (Core Elements of Fair-Play Mystery)
|
||||
|
||||
> **核心原则**: 本格推理的本质是"读者与侦探的智力游戏"——所有线索必须公平呈现,真相必须可推导。
|
||||
|
||||
---
|
||||
|
||||
## 1. 什么是本格推理?
|
||||
|
||||
### 定义
|
||||
**本格推理(Honkaku Mystery)**:又称"正统推理"或"公平竞技推理",强调**逻辑推理**和**线索公平性**。读者与侦探处于同一信息起跑线,理论上可在揭晓前推导出真相。
|
||||
|
||||
### 与社会派/冷硬派的区别
|
||||
| 类型 | 核心 | 重点 | 示例 |
|
||||
|------|------|------|------|
|
||||
| **本格推理** | 谜题解答 | 密室/诡计/逻辑 | 《暴风雪山庄》 |
|
||||
| **社会派** | 人性/动机 | 犯罪原因/社会批判 | 《白夜行》 |
|
||||
| **冷硬派** | 氛围/暴力 | 侦探历险/黑色风格 | 《马耳他之鹰》 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 本格推理的"十诫"(改良版诺克斯十诫)
|
||||
|
||||
### 原版诺克斯十诫(Knox's Ten Commandments, 1929)
|
||||
经典规则,但部分已过时。以下为**网文适配版**:
|
||||
|
||||
**诫律1**: 罪犯必须在故事早期登场
|
||||
**诫律2**: 超自然力量禁止用于真相解释(但可以用于误导)
|
||||
**诫律3**: 密室最多只能有一个秘密通道
|
||||
**诫律4**: 未知毒药或需要长篇科学解释的装置禁用
|
||||
**诫律5**: 不能有中国人角色(原版种族歧视,**已废弃**)→ **改为:不能有完全未登场的神秘人作为凶手**
|
||||
**诫律6**: 侦探不能依靠偶然或第六感破案
|
||||
**诫律7**: 侦探本人不能是罪犯(除非双侦探设定)
|
||||
**诫律8**: 侦探必须向读者公开所有线索
|
||||
**诫律9**: 侦探的助手(华生角色)的思考必须向读者透明
|
||||
**诫律10**: 双胞胎/分身必须在前期明示
|
||||
|
||||
---
|
||||
|
||||
## 3. 本格推理的四大支柱
|
||||
|
||||
### 支柱1:公平性(Fair Play)
|
||||
**定义**: 读者与侦探拥有相同的信息,理论上可以推理出真相。
|
||||
|
||||
**实现方法**:
|
||||
```
|
||||
✅ 正确示例
|
||||
第10章: 侦探发现花瓶碎片(读者同时看到)
|
||||
第20章: 侦探推理:"花瓶是从内部打碎的"
|
||||
→ 读者可以回顾第10章验证
|
||||
|
||||
❌ 错误示例
|
||||
第10章: 侦探发现一个线索(未描述内容)
|
||||
第20章: 侦探说:"我早就发现了这个关键证据!"
|
||||
→ 读者无法验证,不公平
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 支柱2:逻辑性(Logic)
|
||||
**定义**: 推理过程必须符合逻辑,不能靠直觉或巧合。
|
||||
|
||||
**逻辑链示例**:
|
||||
```markdown
|
||||
## 案例:密室杀人
|
||||
**已知线索**:
|
||||
1. 死者在上锁的房间内
|
||||
2. 窗户从内部锁死
|
||||
3. 死者手持钥匙
|
||||
|
||||
**错误推理**:
|
||||
"我有预感,凶手是隔壁的张三"
|
||||
→ 无逻辑依据
|
||||
|
||||
**正确推理**:
|
||||
1. 死者手持钥匙 → 他可能自己锁门
|
||||
2. 窗户内锁 → 凶手未从窗户逃离
|
||||
3. 房间上锁 → 凶手可能在死者锁门前已离开
|
||||
4. → 推测:死者锁门后被提前藏在房内的凶手杀害
|
||||
5. → 寻找房内可藏身的地点(衣柜/床下/天花板)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 支柱3:可解性(Solvability)
|
||||
**定义**: 真相必须是读者**有可能**推理出来的,而非完全意外。
|
||||
|
||||
**可解性标准**:
|
||||
- ✅ 读者事后恍然大悟:"原来是这样!"
|
||||
- ❌ 读者事后仍困惑:"什么?这也行?"
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
✅ 可解的反转
|
||||
线索: 死者日记写着"7月15日见到了他"
|
||||
真相: "他"指的是凶手,日记是证据
|
||||
|
||||
❌ 不可解的反转
|
||||
真相: 死者有个从未提及的双胞胎兄弟
|
||||
→ 读者无从推测
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 支柱4:意外性(Surprise)
|
||||
**定义**: 真相既符合逻辑,又出人意料。
|
||||
|
||||
**平衡公式**:
|
||||
```
|
||||
公平性(80%线索) + 隐藏性(20%误导) = 意外性
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:《东方快车谋杀案》(阿加莎·克里斯蒂)
|
||||
**线索公平**: 12个嫌疑人都有不在场证明
|
||||
**误导**: 暗示凶手是外来者
|
||||
**真相**: 12人集体作案
|
||||
**意外性**: 颠覆"凶手是一人"的常识
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 本格推理的五大要素
|
||||
|
||||
### 要素1:谜题(Mystery)
|
||||
**定义**: 核心悬念,推动读者继续阅读。
|
||||
|
||||
**常见谜题类型**:
|
||||
| 类型 | 问题 | 示例 |
|
||||
|------|------|------|
|
||||
| **Who(谁)** | 凶手是谁? | 封闭空间多人,找出凶手 |
|
||||
| **How(如何)** | 如何做到的? | 密室杀人,如何逃脱 |
|
||||
| **Why(为何)** | 动机是什么? | 看似无冤无仇,为何杀人 |
|
||||
| **When(何时)** | 案发时间? | 伪造不在场证明 |
|
||||
| **Where(何地)** | 案发地点? | 尸体被移动,真正现场在哪 |
|
||||
|
||||
---
|
||||
|
||||
### 要素2:线索(Clues)
|
||||
**定义**: 指向真相的证据,必须**公平呈现**。
|
||||
|
||||
**线索分类**:
|
||||
```markdown
|
||||
## 物证
|
||||
- 凶器、指纹、血迹、毛发、纤维
|
||||
- 时间证据(手表停止、日记日期)
|
||||
- 空间证据(脚印方向、物品位置)
|
||||
|
||||
## 人证
|
||||
- 目击证言(可能虚假)
|
||||
- 专家鉴定(法医、化验师)
|
||||
|
||||
## 行为证据
|
||||
- 嫌疑人的异常举动
|
||||
- 不在场证明的破绽
|
||||
```
|
||||
|
||||
**线索埋设技巧**:
|
||||
```
|
||||
1. 前置埋设: 在案发前章节"不经意"提及关键物品
|
||||
2. 伪装成背景: 把线索藏在环境描写中
|
||||
3. 多次提及: 重要线索出现2-3次,加深印象
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 要素3:红鲱鱼(Red Herring,误导)
|
||||
**定义**: 故意设置的虚假线索,引导读者做出错误推理。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:指向无辜者的证据
|
||||
线索A: 张三的刀上有血迹
|
||||
线索B: 张三曾与死者争吵
|
||||
→ 读者推测:张三是凶手
|
||||
|
||||
真相: 张三是猎人,刀上是动物血
|
||||
争吵是因为债务,与谋杀无关
|
||||
→ 真凶另有其人
|
||||
```
|
||||
|
||||
**注意**: 红鲱鱼必须**事后可解释**,不能毫无逻辑。
|
||||
|
||||
---
|
||||
|
||||
### 要素4:侦探(Detective)
|
||||
**定义**: 推理的执行者,代表读者的视角。
|
||||
|
||||
**侦探类型**:
|
||||
| 类型 | 特点 | 示例 |
|
||||
|------|------|------|
|
||||
| **天才型** | 超凡逻辑,冷静理性 | 福尔摩斯 |
|
||||
| **老实人型** | 依靠常识和踏实调查 | 布朗神父 |
|
||||
| **业余型** | 非职业侦探,误打误撞破案 | 杰西卡·弗莱彻 |
|
||||
|
||||
**网文常见设定**:
|
||||
- 重生侦探(知道部分真相,但需重新推理)
|
||||
- 系统辅助侦探(获得线索提示)
|
||||
- 双侦探(一明一暗,最后合作)
|
||||
|
||||
---
|
||||
|
||||
### 要素5:真相揭示(Revelation)
|
||||
**定义**: 侦探公开推理过程,解释所有疑点。
|
||||
|
||||
**揭示结构**:
|
||||
```markdown
|
||||
## 标准三段式
|
||||
1. 回顾案情(30%): 梳理已知线索
|
||||
2. 逐点破解(50%): 逐一排除嫌疑,指出红鲱鱼
|
||||
3. 指认凶手(20%): 揭示核心诡计,指认真凶
|
||||
```
|
||||
|
||||
**揭示技巧**:
|
||||
```
|
||||
✅ 层层递进: 先破解小谜题,再揭示核心
|
||||
✅ 制造反转: 先指向A,再突然转向B
|
||||
✅ 留悬念: 解释80%,留20%让读者自己思考
|
||||
|
||||
❌ 一次性倾倒: 把所有信息一口气说完
|
||||
❌ 侦探全知: 侦探事先知道但不说
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 本格推理的创作流程
|
||||
|
||||
### 流程图
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[1. 设计谜题] --> B[2. 构思诡计]
|
||||
B --> C[3. 埋设线索]
|
||||
C --> D[4. 设置误导]
|
||||
D --> E[5. 设计侦探]
|
||||
E --> F[6. 编写推理]
|
||||
F --> G[7. 公平性检验]
|
||||
G -->|不通过| C
|
||||
G -->|通过| H[8. 完稿]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 1: 设计谜题
|
||||
**核心问题**: 你想让读者猜什么?
|
||||
|
||||
**示例**:
|
||||
```
|
||||
谜题: 密室杀人——凶手如何在上锁房间杀人后逃脱?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 构思诡计
|
||||
**核心问题**: 真相是什么?如何做到的?
|
||||
|
||||
**示例**:
|
||||
```
|
||||
诡计: 凶手提前藏在房内壁橱,杀人后伪装成第一发现者
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 埋设线索
|
||||
**核心问题**: 如何让读者**有可能**推理出真相?
|
||||
|
||||
**示例**:
|
||||
```
|
||||
线索1(第5章): 壁橱门有轻微划痕
|
||||
线索2(第10章): 第一发现者衣服上有灰尘
|
||||
线索3(第15章): 壁橱内发现一根头发
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 设置误导
|
||||
**核心问题**: 如何让读者**不那么容易**猜到真相?
|
||||
|
||||
**示例**:
|
||||
```
|
||||
红鲱鱼1: 窗外有脚印(实际是园丁的)
|
||||
红鲱鱼2: 死者手持遗书(实际是自杀假象)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 设计侦探
|
||||
**核心问题**: 由谁来推理?性格如何?
|
||||
|
||||
**示例**:
|
||||
```
|
||||
侦探: 退休警探,经验丰富但不善言辞
|
||||
特点: 通过观察细节破案,而非天才直觉
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: 编写推理
|
||||
**核心问题**: 侦探如何一步步接近真相?
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第20章: 发现壁橱划痕,怀疑有人藏过
|
||||
第25章: 发现第一发现者撒谎
|
||||
第30章: 揭示真相
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 7: 公平性检验
|
||||
**检查清单**:
|
||||
- [ ] 所有关键线索都已向读者呈现?
|
||||
- [ ] 推理过程符合逻辑?
|
||||
- [ ] 没有依赖读者不知道的信息?
|
||||
- [ ] 红鲱鱼都可以合理解释?
|
||||
- [ ] 真相既意外又合理?
|
||||
|
||||
---
|
||||
|
||||
## 6. 本格推理的常见陷阱
|
||||
|
||||
### 陷阱1:信息不对等
|
||||
**错误示例**:
|
||||
```
|
||||
侦探(心想):我早就知道凶手是谁了。
|
||||
→ 读者无法同步推理
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
侦探(心想):壁橱的划痕……难道……
|
||||
→ 暗示推理方向,但不直接说出
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 陷阱2:逻辑跳跃
|
||||
**错误示例**:
|
||||
```
|
||||
线索: 现场有根头发
|
||||
侦探: 所以凶手是张三!
|
||||
→ 缺少中间推理步骤
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
线索: 现场有根头发
|
||||
侦探: 这根头发是黑色长发
|
||||
现场只有3名女性
|
||||
其中2人是短发
|
||||
→ 推测:凶手是长发女性李四
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 陷阱3:过度依赖巧合
|
||||
**错误示例**:
|
||||
```
|
||||
侦探碰巧看到凶手在处理凶器
|
||||
→ 靠运气破案
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
侦探通过追踪线索,推理出凶器藏匿地点
|
||||
→ 靠逻辑破案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 陷阱4:真相过于复杂
|
||||
**错误示例**:
|
||||
```
|
||||
真相: 凶手利用量子力学原理制造密室
|
||||
→ 读者完全无法理解
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
真相: 凶手利用冰块固定门栓,融化后门自动上锁
|
||||
→ 简单但巧妙
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 本格推理自检清单
|
||||
|
||||
**写完后逐项检查**:
|
||||
- [ ] 谜题是否足够吸引人?
|
||||
- [ ] 所有线索是否公平呈现?
|
||||
- [ ] 推理逻辑是否严密?
|
||||
- [ ] 红鲱鱼是否事后可解释?
|
||||
- [ ] 真相是否既意外又合理?
|
||||
- [ ] 侦探的推理过程是否清晰?
|
||||
- [ ] 有没有违反"十诫"?
|
||||
- [ ] 读者是否**有可能**推理出真相?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 本格推理速查表
|
||||
|
||||
| 要素 | 关键点 | 常见错误 | 正确做法 |
|
||||
|------|--------|---------|---------|
|
||||
| **谜题** | 吸引人的悬念 | 谜题太简单 | 多层谜题 |
|
||||
| **线索** | 公平呈现 | 关键线索隐藏 | 至少呈现2次 |
|
||||
| **诡计** | 既巧妙又可解 | 过于复杂 | 简单原理 |
|
||||
| **推理** | 逻辑严密 | 跳跃推理 | 逐步推导 |
|
||||
| **揭示** | 层层递进 | 一次性倾倒 | 分段揭示 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典本格推理案例
|
||||
|
||||
### 案例1:《暴风雪山庄》(东野圭吾)
|
||||
**谜题**: 封闭山庄内连环杀人,凶手是谁?
|
||||
**诡计**: 被害者其实是凶手,伪装成被害
|
||||
**公平性**: 所有线索均已呈现,读者可推理
|
||||
|
||||
---
|
||||
|
||||
### 案例2:《无人生还》(阿加莎·克里斯蒂)
|
||||
**谜题**: 孤岛上10人逐一被杀,凶手是谁?
|
||||
**诡计**: 法官假死,最后自杀
|
||||
**公平性**: 童谣暗示杀人顺序,所有线索公开
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
**本格推理 = 公平性 + 逻辑性 + 可解性 + 意外性**
|
||||
|
||||
记住:本格推理的核心不是"炫技",而是"公平竞技"。读者与侦探站在同一起跑线,享受推理的乐趣。
|
||||
@@ -0,0 +1,560 @@
|
||||
# 侦探角色设计 (Detective Character Design)
|
||||
|
||||
> **核心原则**: 侦探是读者的眼睛和大脑。一个好的侦探 = 鲜明个性 + 合理推理能力 + 读者认同感。
|
||||
|
||||
---
|
||||
|
||||
## 1. 侦探的三大职能
|
||||
|
||||
### 职能1:推理引擎(Logic Engine)
|
||||
**作用**: 代替读者进行逻辑推理,揭示真相。
|
||||
|
||||
**要求**:
|
||||
```markdown
|
||||
✅ 推理过程必须符合逻辑
|
||||
✅ 不能依赖直觉或"天降灵感"
|
||||
✅ 线索→推理→结论,每一步都要清晰
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 职能2:读者视角(Reader's Proxy)
|
||||
**作用**: 侦探发现的线索 = 读者看到的线索。
|
||||
|
||||
**要求**:
|
||||
```markdown
|
||||
✅ 侦探的思考过程要向读者透明
|
||||
✅ 侦探不能"藏私"(发现线索不说)
|
||||
✅ 侦探的疑惑 = 读者的疑惑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 职能3:人格魅力(Character Charm)
|
||||
**作用**: 让读者喜欢这个角色,愿意跟随他破案。
|
||||
|
||||
**要求**:
|
||||
```markdown
|
||||
✅ 有鲜明的性格特征
|
||||
✅ 有个人魅力或怪癖
|
||||
✅ 有成长空间(不是完美人)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 侦探的分类
|
||||
|
||||
### 按职业分类
|
||||
|
||||
#### 类型1:职业侦探
|
||||
**特点**: 以侦探为职业,经验丰富。
|
||||
|
||||
**代表人物**:
|
||||
```markdown
|
||||
✅ 私家侦探: 福尔摩斯
|
||||
✅ 警探: 柯南·道尔(《神探夏洛克》)
|
||||
✅ 顾问侦探: 波洛(阿加莎·克里斯蒂)
|
||||
```
|
||||
|
||||
**优点**: 推理能力强,读者信服
|
||||
**缺点**: 容易显得"全知全能",缺少成长
|
||||
|
||||
---
|
||||
|
||||
#### 类型2:业余侦探
|
||||
**特点**: 本职不是侦探,但因缘巧合卷入案件。
|
||||
|
||||
**代表人物**:
|
||||
```markdown
|
||||
✅ 家庭主妇: 杰西卡·弗莱彻(《女作家与谋杀案》)
|
||||
✅ 神父: 布朗神父
|
||||
✅ 学生: 江户川柯南(《名侦探柯南》)
|
||||
```
|
||||
|
||||
**优点**: 有成长空间,读者代入感强
|
||||
**缺点**: 推理能力需要合理解释
|
||||
|
||||
---
|
||||
|
||||
#### 类型3:特殊身份侦探(网文常见)
|
||||
**特点**: 具有超自然能力或特殊背景。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
✅ 重生侦探: 知道部分真相,但需重新推理
|
||||
✅ 系统辅助: 获得线索提示,但仍需自己推理
|
||||
✅ 读心术: 能读取部分信息,但不是全知
|
||||
```
|
||||
|
||||
**注意**: 能力要有**限制**,否则失去推理乐趣。
|
||||
|
||||
---
|
||||
|
||||
### 按性格分类
|
||||
|
||||
| 类型 | 特征 | 优点 | 缺点 | 代表 |
|
||||
|------|------|------|------|------|
|
||||
| **天才型** | 超高智商,冷静理性 | 推理精彩 | 难以共情 | 福尔摩斯 |
|
||||
| **老实人型** | 踏实调查,常识推理 | 亲切可信 | 缺少惊艳 | 布朗神父 |
|
||||
| **怪人型** | 古怪行为,独特思路 | 个性鲜明 | 过于夸张 | 波洛 |
|
||||
| **成长型** | 从新手到高手 | 代入感强 | 前期弱 | 金田一 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 侦探的能力设计
|
||||
|
||||
### 能力1:观察力(Observation)
|
||||
**定义**: 发现常人忽略的细节。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:福尔摩斯
|
||||
普通人: 看到一个人
|
||||
福尔摩斯:
|
||||
- 鞋上有泥土 → 刚从郊外回来
|
||||
- 手指有墨迹 → 从事文字工作
|
||||
- 袖口磨损 → 经济拮据
|
||||
```
|
||||
|
||||
**网文应用**:
|
||||
```markdown
|
||||
侦探注意到:
|
||||
"死者手表停在8点,但窗外天已大亮。"
|
||||
→ 推测:手表可能被调慢了
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 能力2:逻辑推理(Deduction)
|
||||
**定义**: 从已知线索推导出结论。
|
||||
|
||||
**推理模式**:
|
||||
```markdown
|
||||
## 三段论推理
|
||||
大前提: 凶器上有指纹的人曾接触过凶器
|
||||
小前提: 张三的指纹在凶器上
|
||||
结论: 张三曾接触过凶器
|
||||
```
|
||||
|
||||
**注意**: 推理要**逐步展开**,不能跳跃。
|
||||
|
||||
---
|
||||
|
||||
### 能力3:知识储备(Knowledge)
|
||||
**定义**: 侦探掌握的专业知识。
|
||||
|
||||
**常见知识领域**:
|
||||
```markdown
|
||||
✅ 法医学: 判断死亡时间、死因
|
||||
✅ 化学: 识别毒药、爆炸物
|
||||
✅ 心理学: 分析犯罪动机
|
||||
✅ 密码学: 破解暗号
|
||||
```
|
||||
|
||||
**注意**: 知识要**提前交代**,不能临时"掏出"。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
❌ 错误:
|
||||
第30章: 侦探突然说:"我懂密码学,这是摩斯密码!"
|
||||
→ 前文从未提及
|
||||
|
||||
✅ 正确:
|
||||
第5章: 介绍侦探曾学过密码学
|
||||
第30章: 侦探破译密码
|
||||
→ 有伏笔
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 能力4:直觉(Intuition)
|
||||
**定义**: 基于经验的快速判断。
|
||||
|
||||
**使用原则**:
|
||||
```markdown
|
||||
✅ 直觉可以作为**线索方向**
|
||||
✅ 但最终结论必须基于**逻辑推理**
|
||||
✅ 不能完全依赖直觉破案
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
侦探(直觉): "我总觉得张三有问题。"
|
||||
→ 然后通过调查找到证据
|
||||
→ 而非直接说:"我的直觉告诉我,张三是凶手!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 侦探的性格塑造
|
||||
|
||||
### 性格要素:怪癖(Quirk)
|
||||
**作用**: 让角色更有记忆点。
|
||||
|
||||
**经典怪癖**:
|
||||
```markdown
|
||||
✅ 福尔摩斯: 拉小提琴、吸烟、冷漠
|
||||
✅ 波洛: 强迫症、修胡子、自恋
|
||||
✅ 柯南: 喜欢推理、口头禅"真相只有一个"
|
||||
```
|
||||
|
||||
**网文应用**:
|
||||
```markdown
|
||||
侦探怪癖示例:
|
||||
- 破案时必须吃糖
|
||||
- 思考时转笔
|
||||
- 口头禅:"有意思……"
|
||||
```
|
||||
|
||||
**注意**: 怪癖要**适度**,过多会显得刻意。
|
||||
|
||||
---
|
||||
|
||||
### 性格要素:弱点(Flaw)
|
||||
**作用**: 让角色更真实,有成长空间。
|
||||
|
||||
**常见弱点**:
|
||||
```markdown
|
||||
✅ 社交障碍: 不善与人交流
|
||||
✅ 傲慢: 看不起警察
|
||||
✅ 冲动: 容易被激怒
|
||||
✅ 恐惧: 害怕某种事物(高空/黑暗)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
侦探弱点:
|
||||
"他推理能力极强,但不善言辞,总是得罪人。"
|
||||
→ 为后续剧情埋下冲突点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 性格要素:动机(Motivation)
|
||||
**作用**: 解释侦探为什么要破案。
|
||||
|
||||
**常见动机**:
|
||||
```markdown
|
||||
✅ 职业使命: "这是我的工作"
|
||||
✅ 个人兴趣: "我喜欢解谜"
|
||||
✅ 复仇: "凶手杀了我的亲人"
|
||||
✅ 正义感: "我要为死者伸冤"
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
侦探动机:
|
||||
"十年前,我父亲被诬陷为凶手,含冤而死。
|
||||
从那天起,我发誓要揭开所有谎言。"
|
||||
→ 强烈的个人动机
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 侦探的成长弧线
|
||||
|
||||
### 成长模式:从新手到高手
|
||||
|
||||
#### 阶段1:新手期
|
||||
**特点**: 推理能力弱,依赖他人帮助。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第1-10章:
|
||||
侦探刚入行,破案依赖师父指点
|
||||
→ 建立"成长空间"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 阶段2:成长期
|
||||
**特点**: 逐步掌握推理技巧,独立破案。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第11-30章:
|
||||
侦探独立破获小案件,偶尔失误
|
||||
→ 展示成长过程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 阶段3:成熟期
|
||||
**特点**: 推理能力强,能处理复杂案件。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第31-50章:
|
||||
侦探成为名侦探,破获连环杀人案
|
||||
→ 能力巅峰
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 阶段4:突破期(可选)
|
||||
**特点**: 遇到超难案件,突破自我。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第51-70章:
|
||||
侦探遇到宿敌,陷入困境,最终顿悟
|
||||
→ 再次成长
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 侦探与助手的关系
|
||||
|
||||
### 经典搭档模式:侦探 + 华生
|
||||
|
||||
#### 华生的作用
|
||||
```markdown
|
||||
1. 提问者: 代替读者提出疑问
|
||||
2. 记录者: 记录案件过程
|
||||
3. 对比者: 凸显侦探的聪明
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
华生: "你是怎么知道凶手是张三的?"
|
||||
侦探: "很简单,你看这三条线索……"
|
||||
→ 华生提问,侦探解释,读者理解
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 华生的性格设定
|
||||
```markdown
|
||||
✅ 老实憨厚: 不如侦探聪明,但踏实可靠
|
||||
✅ 勇敢: 保护侦探的人身安全
|
||||
✅ 忠诚: 无条件相信侦探
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 反面教材:没有华生的问题
|
||||
**问题**:
|
||||
```markdown
|
||||
侦探独自破案 → 没人提问 → 读者不知道侦探在想什么
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 方案1: 增加华生角色
|
||||
✅ 方案2: 侦探自言自语(内心独白)
|
||||
✅ 方案3: 警察/记者充当提问者
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 侦探的推理展示
|
||||
|
||||
### 展示方式1:边调查边推理
|
||||
**特点**: 侦探每发现一条线索,就进行推理。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
侦探发现线索A:
|
||||
"这根头发是黑色长发……现场只有3名女性,其中2人是短发……"
|
||||
→ 读者跟随侦探思路
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 展示方式2:集中揭示
|
||||
**特点**: 侦探调查完毕后,集中公开推理。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第25章(调查完毕):
|
||||
侦探召集所有人:"现在,我要揭示真相……"
|
||||
→ 适合短篇或最终揭示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 展示方式3:分段揭示
|
||||
**特点**: 侦探逐步揭示部分真相,制造悬念。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第10章: 侦探揭示密室诡计
|
||||
第20章: 侦探揭示不在场证明破绽
|
||||
第30章: 侦探指认真凶
|
||||
→ 层层递进
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 侦探设计常见错误
|
||||
|
||||
### 错误1:全知全能
|
||||
**问题**:
|
||||
```markdown
|
||||
侦探一眼看穿所有诡计,毫无悬念
|
||||
→ 读者:"没意思"
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 给侦探设置障碍(误导线索、凶手反击)
|
||||
✅ 让侦探也会犯错(推理错误,再修正)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误2:依赖直觉
|
||||
**问题**:
|
||||
```markdown
|
||||
侦探: "我的直觉告诉我,凶手是张三!"
|
||||
→ 没有逻辑推理
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 直觉只是方向,必须找到证据
|
||||
✅ 最终结论基于逻辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误3:性格单薄
|
||||
**问题**:
|
||||
```markdown
|
||||
侦探只会破案,没有个人生活和情感
|
||||
→ 读者无法共情
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 给侦探个人生活(家庭/爱好/过去)
|
||||
✅ 给侦探情感冲突(亲人被害/道德困境)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误4:"藏私"
|
||||
**问题**:
|
||||
```markdown
|
||||
侦探发现关键线索,但不告诉读者
|
||||
第30章突然说:"我早就知道了!"
|
||||
→ 不公平
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 侦探的所有发现都要向读者展示
|
||||
✅ 可以暂时不解释,但线索必须呈现
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 网文特色侦探设计
|
||||
|
||||
### 特色1:系统辅助侦探
|
||||
**设定**: 侦探拥有推理系统,获得提示。
|
||||
|
||||
**限制设计**:
|
||||
```markdown
|
||||
✅ 系统只提供线索,不直接给答案
|
||||
✅ 侦探仍需自己推理
|
||||
✅ 系统有使用次数限制
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
【系统提示: 现场存在3处异常】
|
||||
侦探: "3处异常……分别是什么?"
|
||||
→ 侦探自己寻找
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 特色2:重生侦探
|
||||
**设定**: 侦探重生,知道部分真相。
|
||||
|
||||
**冲突设计**:
|
||||
```markdown
|
||||
✅ 重生后时间线改变,真相也变了
|
||||
✅ 侦探需要重新推理
|
||||
✅ 不能完全依赖前世记忆
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
前世: 凶手是张三
|
||||
今生: 张三提前被捕,真凶变成了李四
|
||||
→ 侦探必须重新破案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 特色3:双侦探
|
||||
**设定**: 两个侦探,一明一暗,最后合作。
|
||||
|
||||
**关系设计**:
|
||||
```markdown
|
||||
✅ 一个擅长逻辑推理,一个擅长心理分析
|
||||
✅ 前期竞争,后期合作
|
||||
✅ 各自发现不同线索,最终拼图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 侦探设计自检清单
|
||||
|
||||
**设计完侦探后逐项检查**:
|
||||
- [ ] 侦探的推理能力是否合理?
|
||||
- [ ] 侦探的知识储备是否有交代?
|
||||
- [ ] 侦探是否有鲜明的性格特征?
|
||||
- [ ] 侦探是否有弱点或成长空间?
|
||||
- [ ] 侦探的动机是否明确?
|
||||
- [ ] 侦探是否向读者公开所有线索?
|
||||
- [ ] 侦探是否有助手(华生角色)?
|
||||
- [ ] 侦探的推理过程是否符合逻辑?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 侦探设计速查表
|
||||
|
||||
| 要素 | 标准 | 常见错误 | 解决方案 |
|
||||
|------|------|---------|---------|
|
||||
| **能力** | 观察+推理+知识 | 全知全能 | 设置障碍 |
|
||||
| **性格** | 怪癖+弱点+动机 | 性格单薄 | 增加细节 |
|
||||
| **成长** | 从新手到高手 | 一开始就很强 | 设计成长弧 |
|
||||
| **公平** | 线索向读者公开 | 藏私 | 透明化 |
|
||||
| **助手** | 华生角色 | 没有提问者 | 增加搭档 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典侦探分析
|
||||
|
||||
### 案例1:福尔摩斯
|
||||
**能力**: 超强观察力、逻辑推理、化学知识
|
||||
**性格**: 冷漠、高傲、拉小提琴
|
||||
**弱点**: 不善社交、吸毒(早期)
|
||||
**搭档**: 华生医生
|
||||
|
||||
---
|
||||
|
||||
### 案例2:波洛
|
||||
**能力**: 心理分析、逻辑推理
|
||||
**性格**: 自恋、强迫症、修胡子
|
||||
**弱点**: 过于自信
|
||||
**搭档**: 黑斯廷斯上尉
|
||||
|
||||
---
|
||||
|
||||
### 案例3:柯南
|
||||
**能力**: 超强推理、化学知识
|
||||
**性格**: 好奇心强、正义感
|
||||
**弱点**: 身体变小,行动受限
|
||||
**特殊**: 有各种黑科技道具
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
**好侦探 = 合理能力 + 鲜明性格 + 成长弧线 + 公平推理**
|
||||
|
||||
记住:侦探不是"神",而是读者的代言人。让读者跟随侦探的思路,享受推理的乐趣。
|
||||
@@ -0,0 +1,472 @@
|
||||
# 真相揭示技巧 (Revelation Design)
|
||||
|
||||
> **核心原则**: 真相揭示是本格推理的高潮。好的揭示要做到"意料之外,情理之中"——既要震撼读者,又要让读者恍然大悟"原来如此"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 真相揭示的三大目标
|
||||
|
||||
### 目标1:逻辑自洽(Logic Consistency)
|
||||
**定义**: 真相必须能解释所有线索和疑点,不留逻辑漏洞。
|
||||
|
||||
**检验方法**:
|
||||
```
|
||||
✅ 每一条线索都能在真相中找到解释
|
||||
✅ 没有"孤立线索"(无法解释的线索)
|
||||
✅ 不依赖"巧合"或"读者不知道的事实"
|
||||
```
|
||||
|
||||
**反面教材**:
|
||||
```
|
||||
真相:"凶手是 A,他用了密室诡计杀人"
|
||||
遗漏:为什么现场有 B 的指纹?为什么 C 的证词与现场不符?
|
||||
→ 逻辑不自洽,读者不满意
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 目标2:意外性(Surprise)
|
||||
**定义**: 真相要出乎读者意料,打破常规推理。
|
||||
|
||||
**实现方法**:
|
||||
- **颠覆第一印象**:最无辜的人是凶手
|
||||
- **反转叙述视角**:读者以为的受害者其实是凶手
|
||||
- **时间错位**:案件发生时间与读者认知不同
|
||||
- **身份错位**:A 和 B 的身份互换
|
||||
|
||||
**示例**:
|
||||
```
|
||||
读者推理:凶手是 A(所有证据指向 A)
|
||||
真相揭示:A 是替罪羊,真凶是 B(一直在误导读者)
|
||||
震撼点:B 是侦探的助手/警察/受害者家属
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 目标3:恍然大悟(Enlightenment)
|
||||
**定义**: 真相揭示后,读者回顾全文,发现"所有线索都在那里"。
|
||||
|
||||
**要求**:
|
||||
```
|
||||
✅ 读者事后能验证:第X章的描写确实是伏笔
|
||||
✅ 线索不应"事后补充"(揭示时才说"其实还有一条线索……")
|
||||
✅ 读者感叹:"我怎么没想到!"而非"作者耍赖!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 揭示场景的四种类型
|
||||
|
||||
### 类型A:经典解谜场景(Detective's Revelation)
|
||||
**特点**: 侦探召集所有人,逐一推理,揭示真相。
|
||||
|
||||
**标准流程(6步)**:
|
||||
```markdown
|
||||
## 经典解谜场景流程
|
||||
|
||||
Step 1: 场景设定(100-200字)
|
||||
- 所有嫌疑人聚集在一个房间
|
||||
- 侦探宣布:"我已经知道真相了"
|
||||
- 紧张气氛营造
|
||||
|
||||
Step 2: 重述案情(200-300字)
|
||||
- 侦探回顾案件经过
|
||||
- 列出关键疑点(3-5个)
|
||||
- 提醒读者重要线索
|
||||
|
||||
Step 3: 排除法(300-500字)
|
||||
- 逐一分析嫌疑人
|
||||
- 指出每个人的不在场证明/动机缺失
|
||||
- 缩小范围到1-2人
|
||||
|
||||
Step 4: 关键推理(500-800字)
|
||||
- 指出"决定性线索"
|
||||
- 展示推理链条:线索 A + 线索 B → 结论 C
|
||||
- 揭示诡计原理
|
||||
|
||||
Step 5: 真凶反应(100-200字)
|
||||
- 真凶辩解/狡辩/沉默
|
||||
- 侦探拿出"最后一击"(物证/证人)
|
||||
- 真凶认罪/崩溃
|
||||
|
||||
Step 6: 动机揭示(200-300字)
|
||||
- 解释真凶的动机
|
||||
- 补充案件背景故事
|
||||
- 结局:真凶被捕/自杀/逃脱(埋伏笔)
|
||||
```
|
||||
|
||||
**示例(简化版)**:
|
||||
```
|
||||
侦探:"各位,我已经知道谁是凶手了。"
|
||||
众人紧张。
|
||||
|
||||
侦探:"首先,让我们回顾案情……死者在晚上8点被杀,凶器是匕首……"
|
||||
|
||||
侦探:"张三有不在场证明,李四没有动机,王五……"
|
||||
|
||||
侦探:"关键在于这个!"(拿出一根头发)
|
||||
"这根头发在现场发现,但它属于一个不该在那里的人——赵六!"
|
||||
|
||||
赵六脸色大变:"你……你胡说!"
|
||||
|
||||
侦探:"不仅如此,赵六你说你8点在家,但你的手机定位显示你在案发现场!"
|
||||
|
||||
赵六崩溃:"是……是我杀的……他毁了我的人生……"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型B:分段揭示(Progressive Revelation)
|
||||
**特点**: 不是一次性揭示,而是分多次逐步揭开真相。
|
||||
|
||||
**适用场景**:
|
||||
- 案件复杂,涉及多个谜题
|
||||
- 网文连载,需要分章节保持悬念
|
||||
|
||||
**流程**:
|
||||
```markdown
|
||||
第30章: 揭示"凶器来源"(部分真相)
|
||||
第35章: 揭示"不在场证明诡计"(更多真相)
|
||||
第40章: 揭示"真凶身份"(完整真相)
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 保持悬念,读者持续关注
|
||||
- 适合长篇连载
|
||||
|
||||
**缺点**:
|
||||
- 节奏难把控,容易拖沓
|
||||
|
||||
---
|
||||
|
||||
### 类型C:反转式揭示(Twist Revelation)
|
||||
**特点**: 先给出一个"假真相",再推翻,揭示"真真相"。
|
||||
|
||||
**经典模板**:
|
||||
```markdown
|
||||
第一次揭示(假真相):
|
||||
侦探:"凶手是 A!"
|
||||
A 被捕,案件似乎结束。
|
||||
|
||||
第二次揭示(真真相):
|
||||
侦探:"等等……有些地方不对……"
|
||||
侦探重新推理:"A 不是真凶!真凶是 B!"
|
||||
震撼反转。
|
||||
```
|
||||
|
||||
**示例(《东方快车谋杀案》式)**:
|
||||
```
|
||||
侦探:"凶手是车上的某一个人。"
|
||||
→ 读者推理:12个嫌疑人,谁是凶手?
|
||||
|
||||
最终揭示:"凶手不是一个人,是所有人。"
|
||||
→ 震撼反转
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 类型D:读者先知(Reader-First Revelation)
|
||||
**特点**: 读者比侦探更早知道真相。
|
||||
|
||||
**适用场景**:
|
||||
- 倒叙推理(先展示犯罪,再展示侦探破案)
|
||||
- "逆转裁判"式(读者知道真相,看侦探如何证明)
|
||||
|
||||
**效果**:
|
||||
- 读者有"上帝视角",看侦探如何一步步接近真相
|
||||
- 产生"快来发现啊!"的紧张感
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第1章: 展示真凶 A 的犯罪过程(读者知道真相)
|
||||
第2-20章: 侦探调查,逐步接近真相(读者看侦探破案)
|
||||
第21章: 侦探揭示:"凶手是 A!"(读者早就知道了)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 揭示的节奏控制
|
||||
|
||||
### 节奏公式(推理小说标准结构)
|
||||
```
|
||||
总字数的 70-80% → 开始揭示
|
||||
总字数的 80-90% → 完成揭示
|
||||
总字数的 90-100% → 收尾(抓捕/动机解释/尾声)
|
||||
```
|
||||
|
||||
**示例(10万字推理小说)**:
|
||||
```
|
||||
0-7万字: 调查、线索收集、嫌疑人筛选
|
||||
7-8万字: 侦探解谜场景(揭示真相)
|
||||
8-9万字: 真凶认罪、抓捕
|
||||
9-10万字: 尾声、动机解释、伏笔
|
||||
```
|
||||
|
||||
### 避免过早揭示
|
||||
**反面教材**:
|
||||
```
|
||||
总字数一半就揭示真相 → 后半部分无悬念,读者弃书
|
||||
```
|
||||
|
||||
### 避免过晚揭示
|
||||
**反面教材**:
|
||||
```
|
||||
到最后1000字才揭示 → 读者感觉被耍,推理过程太短
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 真相揭示的五大技巧
|
||||
|
||||
### 技巧1:回顾式揭示
|
||||
**方法**: 侦探逐章回顾,指出每一处伏笔。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
侦探:"还记得第5章,死者的手表停在8点吗?"
|
||||
"当时我们以为这是死亡时间,但其实……"
|
||||
"这是凶手故意制造的假象!"
|
||||
```
|
||||
|
||||
**优点**: 读者跟着侦探回顾全文,恍然大悟。
|
||||
|
||||
---
|
||||
|
||||
### 技巧2:物证展示
|
||||
**方法**: 侦探拿出关键物证,作为"最后一击"。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
侦探:"你说你从未去过现场,但这是什么?"
|
||||
(拿出一张照片,照片里凶手出现在现场)
|
||||
凶手哑口无言。
|
||||
```
|
||||
|
||||
**优点**: 直观、有力、读者信服。
|
||||
|
||||
---
|
||||
|
||||
### 技巧3:逻辑链条展示
|
||||
**方法**: 侦探展示完整的推理链条(A → B → C → 结论)。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
侦探:
|
||||
"线索 A: 现场发现红色纤维"
|
||||
"线索 B: 张三有一件红色外套"
|
||||
"线索 C: 张三的外套在案发后被洗过"
|
||||
"结论: 张三去过现场,并试图销毁证据"
|
||||
```
|
||||
|
||||
**优点**: 逻辑严密,读者服气。
|
||||
|
||||
---
|
||||
|
||||
### 技巧4:反问式揭示
|
||||
**方法**: 侦探用反问引导读者思考。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
侦探:"为什么凶手要在现场留下这么多线索?"
|
||||
"答案很简单——因为他想让我们找到这些线索。"
|
||||
"他在误导我们!"
|
||||
```
|
||||
|
||||
**优点**: 互动性强,读者参与感高。
|
||||
|
||||
---
|
||||
|
||||
### 技巧5:情感渲染
|
||||
**方法**: 在揭示真相时,加入情感描写(悲伤/愤怒/震惊)。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
侦探:"凶手……是死者的亲生儿子。"
|
||||
全场震惊。
|
||||
儿子崩溃:"是……是他先抛弃我的……他不配做我的父亲!"
|
||||
泪水滑落……
|
||||
```
|
||||
|
||||
**优点**: 增加共鸣,读者更投入。
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见揭示错误
|
||||
|
||||
### 错误1:事后补充线索
|
||||
**反面教材**:
|
||||
```
|
||||
侦探:"其实,在第3章,我还发现了一条线索(之前从未提及)……"
|
||||
→ 读者:"你耍赖!"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
所有关键线索必须在揭示前出现(哪怕隐藏得很深)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误2:依赖巧合
|
||||
**反面教材**:
|
||||
```
|
||||
侦探:"凶手恰好在案发时路过现场,恰好带了凶器……"
|
||||
→ 读者:"太巧了吧?"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
真相必须有充分的动机和计划,不能依赖巧合
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误3:解释不清
|
||||
**反面教材**:
|
||||
```
|
||||
侦探:"凶手用了XX诡计(但不解释原理)……"
|
||||
读者:"什么诡计?没听懂啊!"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
用通俗语言解释诡计原理,必要时配图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误4:动机牵强
|
||||
**反面教材**:
|
||||
```
|
||||
凶手:"我杀他是因为……他踩了我一脚。"
|
||||
读者:"就因为这个?!"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
动机必须充分:仇恨/利益/秘密/绝望
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 揭示后的收尾
|
||||
|
||||
### 收尾要点(3选2)
|
||||
1. **真凶的最后独白**:解释动机,补充背景故事
|
||||
2. **侦探的反思**:对案件的感悟,对人性的思考
|
||||
3. **伏笔/续集预告**:暗示下一个案件/未解之谜
|
||||
|
||||
**示例(真凶独白)**:
|
||||
```
|
||||
真凶:"你知道吗,20年前,他毁了我的一切……"
|
||||
"我忍了20年,终于等到这一天……"
|
||||
"我不后悔。"
|
||||
```
|
||||
|
||||
**示例(侦探反思)**:
|
||||
```
|
||||
侦探看着真凶被带走,叹了口气。
|
||||
"每个凶手背后,都有一个悲伤的故事……"
|
||||
"但这不是杀人的理由。"
|
||||
```
|
||||
|
||||
**示例(伏笔)**:
|
||||
```
|
||||
案件结束,侦探收到一封匿名信:
|
||||
"这只是开始……下一个受害者已经选好了。"
|
||||
侦探脸色凝重……
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 揭示场景自检清单
|
||||
|
||||
揭示真相前,检查以下项目:
|
||||
|
||||
- [ ] **所有线索都出现过吗**: 揭示时提到的每一条线索,在之前章节都有展示?
|
||||
- [ ] **逻辑自洽吗**: 真相能解释所有疑点,没有逻辑漏洞?
|
||||
- [ ] **有意外性吗**: 真相是否出乎读者意料(但不违背逻辑)?
|
||||
- [ ] **读者能验证吗**: 读者回顾全文,能找到所有伏笔和线索?
|
||||
- [ ] **动机充分吗**: 凶手的动机是否足以支撑其犯罪行为?
|
||||
- [ ] **诡计可理解吗**: 诡计原理是否用通俗语言解释清楚?
|
||||
- [ ] **节奏合适吗**: 揭示场景是否在总字数的 70-80% 处?
|
||||
- [ ] **有情感渲染吗**: 是否加入了情感描写,增强共鸣?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 揭示技巧速查表
|
||||
|
||||
| 揭示类型 | 特点 | 适用场景 | 优点 | 缺点 |
|
||||
|---------|------|---------|------|------|
|
||||
| **经典解谜** | 侦探召集众人,逐一推理 | 短篇、中篇 | 仪式感强,读者满足感高 | 套路化,需创新 |
|
||||
| **分段揭示** | 逐步揭开真相 | 长篇连载 | 保持悬念 | 节奏难把控 |
|
||||
| **反转式** | 先假真相,再真真相 | 需要震撼效果 | 意外性强 | 容易显得刻意 |
|
||||
| **读者先知** | 读者比侦探先知道 | 倒叙推理 | 紧张感强 | 失去"解谜"乐趣 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典揭示场景案例
|
||||
|
||||
### 案例1:《东方快车谋杀案》
|
||||
**揭示方式**: 反转式揭示
|
||||
**流程**:
|
||||
1. 侦探推理:"凶手在12个人中"
|
||||
2. 逐一排除,最后锁定某人
|
||||
3. 最终反转:"凶手是所有人"
|
||||
|
||||
**优点**: 震撼反转,出乎意料又在情理之中。
|
||||
|
||||
---
|
||||
|
||||
### 案例2:《罗杰疑案》
|
||||
**揭示方式**: 叙述性诡计
|
||||
**流程**:
|
||||
1. 读者一直跟随叙述者视角
|
||||
2. 最后揭示:"叙述者就是凶手"
|
||||
|
||||
**优点**: 利用叙述视角误导读者,极具创新性。
|
||||
|
||||
---
|
||||
|
||||
### 案例3:反面教材(某扑街推理文)
|
||||
```
|
||||
侦探:"凶手是 A。"
|
||||
众人:"为什么?"
|
||||
侦探:"因为我猜的。"
|
||||
→ 读者:"???"
|
||||
```
|
||||
|
||||
**问题**: 没有推理过程,没有线索支撑,纯靠猜测。
|
||||
|
||||
---
|
||||
|
||||
## 特殊场景:网文推理的揭示调整
|
||||
|
||||
### 调整1:分章揭示(适应连载)
|
||||
```
|
||||
标准推理: 一章内完成揭示(5000字)
|
||||
网文调整: 分3-5章逐步揭示(每章2000字)
|
||||
→ 保持订阅,防止读者"看完真相就跑"
|
||||
```
|
||||
|
||||
### 调整2:高潮前置(抓住读者)
|
||||
```
|
||||
标准推理: 70% 处开始揭示
|
||||
网文调整: 50-60% 处就开始部分揭示
|
||||
→ 更早进入高潮,抓住读者
|
||||
```
|
||||
|
||||
### 调整3:加入爽点(网文特色)
|
||||
```
|
||||
标准推理: 揭示真相 → 结束
|
||||
网文调整: 揭示真相 → 侦探装逼 → 打脸真凶 → 读者爽
|
||||
```
|
||||
|
||||
**示例(网文式揭示)**:
|
||||
```
|
||||
侦探:"你以为你计划完美?可笑。"
|
||||
真凶:"你……你怎么知道的……"
|
||||
侦探:"从一开始,我就知道是你。"(装逼)
|
||||
真凸崩溃:"不可能!"(打脸)
|
||||
众人震惊:"侦探大人太厉害了!"(爽点)
|
||||
```
|
||||
@@ -0,0 +1,520 @@
|
||||
# 结构与节奏 (Structure & Pacing)
|
||||
|
||||
> **核心原则**: 本格推理的结构和节奏决定了读者的体验。好的结构让读者始终保持"想往下看"的欲望,好的节奏让读者在"紧张"与"放松"之间切换,避免疲劳。
|
||||
|
||||
---
|
||||
|
||||
## 1. 推理小说的经典三幕结构
|
||||
|
||||
### 第一幕:引入(Setup)- 占全文 20-25%
|
||||
|
||||
**目标**: 吸引读者,建立世界观,引出案件。
|
||||
|
||||
**必备要素(5项)**:
|
||||
1. **案件发生**:展示案发现场,描述受害者状态
|
||||
2. **侦探登场**:介绍主角侦探,展示其特点
|
||||
3. **初步调查**:侦探开始收集线索
|
||||
4. **嫌疑人出场**:引入3-12名嫌疑人
|
||||
5. **核心谜题确立**:明确"要解决什么问题"
|
||||
|
||||
**字数分配(10万字推理小说为例)**:
|
||||
```
|
||||
第1-5章(2万字):
|
||||
- 第1章(5000字): 案件发生,展示案发现场
|
||||
- 第2章(5000字): 侦探登场,接手案件
|
||||
- 第3-4章(1万字): 初步调查,嫌疑人登场
|
||||
- 第5章(5000字): 确立核心谜题(密室/不在场证明等)
|
||||
```
|
||||
|
||||
**第一幕结束标志**:
|
||||
```
|
||||
✅ 读者清楚:谁死了?怎么死的?有哪些嫌疑人?
|
||||
✅ 读者好奇:凶手是谁?诡计是什么?
|
||||
✅ 读者投入:开始跟着侦探推理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第二幕:调查(Investigation)- 占全文 50-60%
|
||||
|
||||
**目标**: 收集线索,筛选嫌疑人,制造悬念。
|
||||
|
||||
**必备要素(6项)**:
|
||||
1. **线索收集**:侦探逐步发现关键线索(明线索 + 暗线索)
|
||||
2. **嫌疑人筛选**:逐一排除/锁定嫌疑人
|
||||
3. **红鲱鱼**:误导读者的假线索
|
||||
4. **中期反转**:打破读者的固有推测
|
||||
5. **侦探受阻**:侦探遇到困难(线索矛盾/嫌疑人反击)
|
||||
6. **新发现**:关键线索出现,推动剧情
|
||||
|
||||
**字数分配(10万字推理小说为例)**:
|
||||
```
|
||||
第6-15章(5-6万字):
|
||||
- 第6-8章(1.5万字): 线索收集,嫌疑人调查
|
||||
- 第9-10章(1万字): 中期反转(某嫌疑人被排除/新嫌疑人出现)
|
||||
- 第11-13章(1.5万字): 侦探受阻,陷入困境
|
||||
- 第14-15章(1-1.5万字): 关键线索出现,柳暗花明
|
||||
```
|
||||
|
||||
**第二幕的节奏控制(重要)**:
|
||||
```
|
||||
前1/3(快节奏): 大量线索,快速推进
|
||||
中1/3(慢节奏): 深入分析,嫌疑人心理战
|
||||
后1/3(加速): 关键线索,接近真相
|
||||
```
|
||||
|
||||
**第二幕结束标志**:
|
||||
```
|
||||
✅ 侦探掌握所有关键线索
|
||||
✅ 嫌疑人范围缩小到1-3人
|
||||
✅ 读者急切想知道真相
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第三幕:揭示(Revelation)- 占全文 20-25%
|
||||
|
||||
**目标**: 揭示真相,收尾剧情,给出结局。
|
||||
|
||||
**必备要素(4项)**:
|
||||
1. **真相揭示**:侦探召集众人,逐一推理
|
||||
2. **真凶认罪**:真凶辩解失败,认罪
|
||||
3. **动机解释**:补充真凶的犯罪动机与背景
|
||||
4. **尾声**:案件结束,侦探反思/伏笔
|
||||
|
||||
**字数分配(10万字推理小说为例)**:
|
||||
```
|
||||
第16-20章(2-2.5万字):
|
||||
- 第16-18章(1.5万字): 侦探解谜,揭示真相
|
||||
- 第19章(5000字): 真凶认罪,动机解释
|
||||
- 第20章(3000-5000字): 尾声,收尾
|
||||
```
|
||||
|
||||
**第三幕的黄金法则**:
|
||||
```
|
||||
❌ 禁止:揭示阶段出现"新线索"(事后补充)
|
||||
✅ 正确:所有线索在第二幕已经出现
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 推理小说的节奏控制
|
||||
|
||||
### 节奏的两种模式
|
||||
|
||||
#### 模式A:张弛有度(标准推理小说)
|
||||
```
|
||||
紧张 → 放松 → 紧张 → 放松 → 大紧张(高潮)
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第1章: 案件发生(紧张)
|
||||
第2章: 日常调查(放松)
|
||||
第3章: 发现关键线索(紧张)
|
||||
第4章: 分析线索,嫌疑人对话(放松)
|
||||
第5章: 中期反转(紧张)
|
||||
……
|
||||
第18章: 揭示真相(大紧张)
|
||||
```
|
||||
|
||||
**优点**: 读者不会疲劳,节奏舒适。
|
||||
|
||||
---
|
||||
|
||||
#### 模式B:步步紧逼(快节奏推理)
|
||||
```
|
||||
紧张 → 更紧张 → 极度紧张 → 高潮
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
第1章: 案件发生
|
||||
第2章: 第二个受害者出现
|
||||
第3章: 第三个受害者出现
|
||||
第4章: 侦探发现"凶手在杀人清单上的下一个目标是自己"
|
||||
第5章: 侦探与凶手对决
|
||||
```
|
||||
|
||||
**优点**: 持续高能,抓住读者。
|
||||
**缺点**: 容易疲劳,适合短篇。
|
||||
|
||||
---
|
||||
|
||||
### 节奏工具:高潮点设计
|
||||
|
||||
**高潮点定义**: 剧情的转折点,让读者"眼前一亮"或"倒吸一口凉气"。
|
||||
|
||||
**推理小说的标准高潮点配置**:
|
||||
```
|
||||
第一高潮(20-30%): 案件发生 / 密室揭开
|
||||
第二高潮(50%): 中期反转 / 嫌疑人死亡
|
||||
第三高潮(70-80%): 真相揭示开始
|
||||
终极高潮(80-90%): 真凶认罪 / 意外反转
|
||||
```
|
||||
|
||||
**示例(10万字推理小说)**:
|
||||
```
|
||||
2万字: 案件发生,密室谜题(第一高潮)
|
||||
5万字: 嫌疑人 A 被杀,案件升级(第二高潮)
|
||||
7万字: 侦探开始揭示真相(第三高潮)
|
||||
8.5万字: 真相大白,真凶是意想不到的人(终极高潮)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 经典推理结构模板
|
||||
|
||||
### 模板A:密室推理结构
|
||||
```markdown
|
||||
## 密室推理标准结构(10万字)
|
||||
|
||||
第一幕(2万字):
|
||||
- 第1章: 密室案件发生,展示密室状态
|
||||
- 第2章: 侦探登场,初步调查
|
||||
- 第3-4章: 嫌疑人登场,初步推理
|
||||
- 第5章: 确立核心谜题:"如何在密室中杀人?"
|
||||
|
||||
第二幕(5-6万字):
|
||||
- 第6-8章: 收集线索,分析密室诡计的可能性
|
||||
- 第9-10章: 中期反转(某个假设被推翻)
|
||||
- 第11-13章: 侦探陷入困境,密室诡计难以解释
|
||||
- 第14-15章: 关键线索出现(如:发现隐藏的机关)
|
||||
|
||||
第三幕(2-2.5万字):
|
||||
- 第16-18章: 侦探揭示密室诡计原理
|
||||
- 第19章: 真凶认罪,动机解释
|
||||
- 第20章: 尾声
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板B:连续杀人结构
|
||||
```markdown
|
||||
## 连续杀人标准结构(15万字)
|
||||
|
||||
第一幕(3万字):
|
||||
- 第1-2章: 第一个受害者出现
|
||||
- 第3-5章: 侦探调查,发现受害者之间的共同点
|
||||
|
||||
第二幕(9万字):
|
||||
- 第6-10章: 第二个受害者出现,凶手的杀人规律浮现
|
||||
- 第11-15章: 侦探推理,锁定嫌疑人
|
||||
- 第16-20章: 第三个受害者出现,侦探受阻
|
||||
- 第21-25章: 关键线索,侦探接近真相
|
||||
|
||||
第三幕(3万字):
|
||||
- 第26-28章: 侦探揭示真相,凶手是"最不可能的人"
|
||||
- 第29章: 真凶认罪,解释杀人动机
|
||||
- 第30章: 尾声,伏笔
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板C:本格推理 + 爽文结构(网文特色)
|
||||
```markdown
|
||||
## 网文推理结构(20万字连载)
|
||||
|
||||
第一卷(5万字): 引入篇
|
||||
- 第1-10章: 侦探登场,解决小案件(展示能力)
|
||||
- 第10-20章: 大案件发生,引入主线
|
||||
|
||||
第二卷(8万字): 调查篇
|
||||
- 第21-40章: 收集线索,嫌疑人筛选
|
||||
- 第41-50章: 中期反转,案件升级
|
||||
|
||||
第三卷(5万字): 揭示篇
|
||||
- 第51-60章: 真相揭示,真凶落网
|
||||
- 第61-70章: 收尾,侦探装逼,打脸真凶
|
||||
|
||||
第四卷(2万字): 余波篇
|
||||
- 第71-80章: 伏笔,引出下一个大案件
|
||||
```
|
||||
|
||||
**网文调整要点**:
|
||||
1. **更早进入高潮**:第1卷就要有完整案件
|
||||
2. **加入爽点**:侦探破案后打脸反派
|
||||
3. **连载节奏**:每5-10章一个小高潮
|
||||
|
||||
---
|
||||
|
||||
## 4. 网文推理的节奏调整
|
||||
|
||||
### 调整1:快速进入案件(抓住读者)
|
||||
```
|
||||
标准推理: 第1章铺垫背景,第2章案件发生
|
||||
网文调整: 第1章直接案件发生,倒叙补充背景
|
||||
```
|
||||
|
||||
**对比**:
|
||||
```
|
||||
标准推理:
|
||||
第1章: 介绍侦探的日常生活(5000字)
|
||||
第2章: 案件发生(5000字)
|
||||
→ 读者可能在第1章就弃书
|
||||
|
||||
网文调整:
|
||||
第1章: 案件发生,侦探登场(5000字)
|
||||
→ 第一章就抓住读者
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 调整2:高频小高潮(保持订阅)
|
||||
```
|
||||
标准推理: 5-10万字一个大高潮
|
||||
网文调整: 每5-10章(1-2万字)一个小高潮
|
||||
```
|
||||
|
||||
**小高潮设计**:
|
||||
- 发现关键线索
|
||||
- 嫌疑人死亡
|
||||
- 侦探受伤/遇险
|
||||
- 真相部分揭示
|
||||
- 意外反转
|
||||
|
||||
---
|
||||
|
||||
### 调整3:章末悬念(防止跳订)
|
||||
```
|
||||
标准推理: 章末自然结束
|
||||
网文调整: 每章结尾设置悬念
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```
|
||||
标准推理章末:
|
||||
"侦探收起笔记本,离开了现场。"
|
||||
→ 读者可以停下,第二天再看
|
||||
|
||||
网文章末:
|
||||
"侦探刚走出门,突然接到电话:'又有人死了!'"
|
||||
→ 读者忍不住点下一章
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见结构错误
|
||||
|
||||
### 错误1:第一幕过长(铺垫太多)
|
||||
**反面教材**:
|
||||
```
|
||||
第1-10章(5万字): 介绍背景、人物关系、侦探过往
|
||||
第11章: 案件终于发生
|
||||
→ 读者:"前10章在干嘛?"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
第1章: 案件发生
|
||||
第2章: 侦探登场,背景通过对话/回忆补充
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误2:第二幕拖沓(注水严重)
|
||||
**反面教材**:
|
||||
```
|
||||
第10-50章(20万字): 侦探反复调查,没有实质进展
|
||||
→ 读者:"能不能快点?"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
每5-10章必须有实质进展:
|
||||
- 排除一名嫌疑人
|
||||
- 发现关键线索
|
||||
- 中期反转
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误3:第三幕仓促(草草收尾)
|
||||
**反面教材**:
|
||||
```
|
||||
第19章: 揭示真相(3000字)
|
||||
第20章: 真凶认罪(1000字)
|
||||
结束。
|
||||
→ 读者:"就这?"
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
```
|
||||
第三幕至少占全文 20%:
|
||||
- 充分展示推理过程
|
||||
- 解释所有线索
|
||||
- 补充动机与背景
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 推理小说的分卷策略(网文连载)
|
||||
|
||||
### 单案件分卷(短篇)
|
||||
```
|
||||
第一卷(5-8万字): 一个完整案件
|
||||
- 引入 → 调查 → 揭示 → 收尾
|
||||
```
|
||||
|
||||
**优点**: 结构完整,读者满足感高
|
||||
**缺点**: 无法长篇连载
|
||||
|
||||
---
|
||||
|
||||
### 多案件串联(长篇)
|
||||
```
|
||||
第一卷(8万字): 案件 A(完结)
|
||||
第二卷(8万字): 案件 B(完结)
|
||||
第三卷(8万字): 案件 C(完结)
|
||||
……
|
||||
```
|
||||
|
||||
**优点**: 可以无限连载
|
||||
**缺点**: 缺乏主线,读者容易疲劳
|
||||
|
||||
---
|
||||
|
||||
### 主线 + 支线(推荐)
|
||||
```
|
||||
主线: 大案件(贯穿全书,分多卷解决)
|
||||
支线: 小案件(每卷一个,独立完结)
|
||||
|
||||
第一卷(8万字): 小案件 A + 主线线索 1
|
||||
第二卷(8万字): 小案件 B + 主线线索 2
|
||||
第三卷(8万字): 小案件 C + 主线线索 3
|
||||
第四卷(10万字): 主线案件揭示,大结局
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 每卷有完整案件,读者有满足感
|
||||
- 主线贯穿,保持长期悬念
|
||||
|
||||
---
|
||||
|
||||
## 7. 推理小说的字数配比(参考)
|
||||
|
||||
### 短篇推理(3-5万字)
|
||||
```
|
||||
第一幕(引入): 5000-8000字(15-20%)
|
||||
第二幕(调查): 1.5-2.5万字(50-60%)
|
||||
第三幕(揭示): 8000-1.2万字(20-25%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 中篇推理(10-15万字)
|
||||
```
|
||||
第一幕(引入): 2-3万字(20%)
|
||||
第二幕(调查): 5-9万字(50-60%)
|
||||
第三幕(揭示): 3-4万字(20-25%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 长篇推理(30万字以上)
|
||||
```
|
||||
第一卷(引入): 5-8万字(15-20%)
|
||||
第二卷(调查): 15-20万字(50-60%)
|
||||
第三卷(揭示): 8-12万字(20-25%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 结构与节奏自检清单
|
||||
|
||||
写完推理小说后,检查以下项目:
|
||||
|
||||
- [ ] **第一幕是否吸引人**: 前3章是否引入案件,吸引读者?
|
||||
- [ ] **第二幕是否有进展**: 每5-10章是否有实质性进展(线索/嫌疑人筛选/反转)?
|
||||
- [ ] **高潮点是否合理**: 是否有2-3个明显的高潮点?
|
||||
- [ ] **节奏是否张弛有度**: 是否有"紧张-放松"的节奏切换?
|
||||
- [ ] **第三幕是否充分**: 揭示阶段是否占全文 20% 以上?
|
||||
- [ ] **章末是否有悬念**: 每章结尾是否设置了悬念?(网文)
|
||||
- [ ] **字数配比是否合理**: 第一幕 20%,第二幕 50-60%,第三幕 20-25%?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 结构与节奏速查表
|
||||
|
||||
| 结构类型 | 适用篇幅 | 第一幕 | 第二幕 | 第三幕 | 特点 |
|
||||
|---------|---------|-------|-------|-------|------|
|
||||
| **经典三幕** | 短中篇 | 20% | 60% | 20% | 标准结构,适合所有推理 |
|
||||
| **快节奏** | 短篇 | 15% | 50% | 35% | 快速进入高潮,揭示充分 |
|
||||
| **多案件串联** | 长篇 | 每卷20% | 每卷60% | 每卷20% | 适合连载,每卷独立 |
|
||||
| **主线+支线** | 长篇 | 15% | 65% | 20% | 主线贯穿,支线独立 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典推理小说结构分析
|
||||
|
||||
### 案例1:《福尔摩斯探案集》- 短篇结构
|
||||
```
|
||||
第一幕(15%): 委托人求助,案件引入
|
||||
第二幕(60%): 福尔摩斯调查,收集线索
|
||||
第三幕(25%): 福尔摩斯揭示真相,抓捕凶手
|
||||
```
|
||||
|
||||
**特点**: 结构紧凑,每篇独立。
|
||||
|
||||
---
|
||||
|
||||
### 案例2:《东方快车谋杀案》- 经典三幕
|
||||
```
|
||||
第一幕(25%): 火车上发生谋杀案,波洛登场
|
||||
第二幕(50%): 波洛调查12名乘客,收集线索
|
||||
第三幕(25%): 波洛揭示"所有人都是凶手"
|
||||
```
|
||||
|
||||
**特点**: 严格遵循三幕结构,节奏完美。
|
||||
|
||||
---
|
||||
|
||||
### 案例3:网文推理《谜案追踪》(架空示例)
|
||||
```
|
||||
第一卷(8万字): 小案件 + 主线线索(神秘组织)
|
||||
第二卷(8万字): 小案件 + 主线线索(组织成员露面)
|
||||
第三卷(8万字): 小案件 + 主线线索(发现组织总部)
|
||||
第四卷(10万字): 主线案件,揭示组织阴谋
|
||||
```
|
||||
|
||||
**特点**: 主线 + 支线结构,适合长篇连载。
|
||||
|
||||
---
|
||||
|
||||
## 特殊技巧:倒计时结构(制造紧张感)
|
||||
|
||||
### 倒计时结构定义
|
||||
**核心**: 给侦探设定一个"截止时间",时间到了会有严重后果。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
设定: 凶手每24小时杀一人,侦探必须在下一个24小时内找到凶手
|
||||
效果: 读者持续紧张,"侦探能及时阻止吗?"
|
||||
```
|
||||
|
||||
**经典案例**:
|
||||
- 《七宗罪》: 凶手按七宗罪顺序杀人,侦探必须在第七个受害者出现前抓住凶手
|
||||
- 《电锯惊魂》: 受害者被困,必须在规定时间内逃脱
|
||||
|
||||
**网文应用**:
|
||||
```
|
||||
第1章: 第一个受害者死亡
|
||||
第5章: 第二个受害者死亡(24小时后)
|
||||
第9章: 第三个受害者死亡(又过24小时)
|
||||
侦探发现规律:"下一个受害者将在明天晚上8点死亡!"
|
||||
→ 倒计时开始
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 结语:结构是骨架,节奏是血肉
|
||||
|
||||
好的结构让推理小说"站得住",好的节奏让推理小说"跑得快"。
|
||||
|
||||
**记住**:
|
||||
- 结构要清晰(三幕式)
|
||||
- 节奏要张弛(紧张-放松)
|
||||
- 高潮要明显(2-3个高潮点)
|
||||
- 悬念要持续(章末钩子)
|
||||
|
||||
遵循这些原则,你的推理小说就能抓住读者的心。
|
||||
@@ -0,0 +1,497 @@
|
||||
# 嫌疑人管理 (Suspect Management)
|
||||
|
||||
> **核心原则**: 嫌疑人不是"路人甲",每个人都要有动机、机会和性格。管理好嫌疑人 = 控制好推理难度。
|
||||
|
||||
---
|
||||
|
||||
## 1. 什么是"嫌疑人管理"?
|
||||
|
||||
### 定义
|
||||
**嫌疑人管理(Suspect Management)**: 在本格推理中,合理设计嫌疑人的**数量、动机、机会、性格**,并通过**排除法**逐步缩小范围,最终指向真凶。
|
||||
|
||||
### 为什么重要?
|
||||
```markdown
|
||||
✅ 嫌疑人太少: 读者轻易猜到凶手
|
||||
✅ 嫌疑人太多: 读者记不住,放弃推理
|
||||
✅ 动机不足: 读者觉得不合理
|
||||
✅ 机会不足: 读者觉得破绽太大
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 嫌疑人数量的黄金法则
|
||||
|
||||
### 黄金数量
|
||||
```markdown
|
||||
短篇(<5万字): 3-5人
|
||||
中篇(5-15万字): 5-8人
|
||||
长篇(>15万字): 8-12人
|
||||
```
|
||||
|
||||
**原则**: 嫌疑人数量不宜超过**12人**,否则读者记不住。
|
||||
|
||||
---
|
||||
|
||||
### 经典案例参考
|
||||
| 作品 | 嫌疑人数 | 效果 |
|
||||
|------|---------|------|
|
||||
| 《东方快车谋杀案》 | 12人 | 极限(所有人都是凶手) |
|
||||
| 《无人生还》 | 10人 | 逐一排除 |
|
||||
| 《暴风雪山庄》 | 7人 | 平衡(不多不少) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 嫌疑人的"三要素"
|
||||
|
||||
### 要素1:动机(Motive)
|
||||
**定义**: 嫌疑人为什么要杀死被害者?
|
||||
|
||||
**常见动机**:
|
||||
| 动机类型 | 示例 | 强度 |
|
||||
|---------|------|------|
|
||||
| **仇恨** | 被害者杀了嫌疑人的家人 | ★★★★★ |
|
||||
| **利益** | 争夺遗产/生意 | ★★★★☆ |
|
||||
| **情感** | 三角恋/嫉妒 | ★★★☆☆ |
|
||||
| **秘密** | 被害者掌握嫌疑人的犯罪证据 | ★★★★☆ |
|
||||
| **意外** | 过失杀人/正当防卫过当 | ★★☆☆☆ |
|
||||
|
||||
**注意**: 每个嫌疑人至少要有**一个**看似合理的动机。
|
||||
|
||||
---
|
||||
|
||||
### 要素2:机会(Opportunity)
|
||||
**定义**: 嫌疑人是否有时间和条件实施犯罪?
|
||||
|
||||
**机会检查清单**:
|
||||
```markdown
|
||||
- [ ] 案发时嫌疑人在现场附近吗?
|
||||
- [ ] 嫌疑人有接触被害者的机会吗?
|
||||
- [ ] 嫌疑人的不在场证明是否有破绽?
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
✅ 有机会:
|
||||
张三在案发时间(晚8点)独自在家,无人证明
|
||||
→ 有作案时间
|
||||
|
||||
❌ 无机会:
|
||||
李四在案发时在国外出差,有机票和护照记录
|
||||
→ 不在场证明完美(除非有帮凶)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 要素3:性格(Character)
|
||||
**定义**: 嫌疑人的性格特征,影响读者对其是否可疑的判断。
|
||||
|
||||
**性格光谱**:
|
||||
```markdown
|
||||
极端可疑 ←→ 极端无辜
|
||||
暴躁/阴险 温和/善良
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
✅ 可疑性格:
|
||||
- 暴躁易怒(容易冲动杀人)
|
||||
- 阴险狡诈(精心策划)
|
||||
- 有犯罪前科
|
||||
|
||||
✅ 无辜性格:
|
||||
- 温和善良
|
||||
- 老实本分
|
||||
- 弱小无力(老人/小孩/残疾人)
|
||||
```
|
||||
|
||||
**陷阱**: 最不可疑的人往往是真凶(反套路)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 嫌疑人设计模板
|
||||
|
||||
### 模板A:标准嫌疑人卡
|
||||
```markdown
|
||||
## 嫌疑人01:张三
|
||||
**身份**: 被害者的商业合作伙伴
|
||||
**年龄**: 45岁
|
||||
**性格**: 精明、冷静、有城府
|
||||
|
||||
**动机**:
|
||||
被害者欠他500万,拒不还钱
|
||||
|
||||
**机会**:
|
||||
案发当晚(晚8点)独自在办公室,无人证明
|
||||
|
||||
**不在场证明**:
|
||||
声称在办公室加班,但无监控证据
|
||||
|
||||
**可疑点**:
|
||||
- 案发前一天与被害者发生激烈争吵
|
||||
- 手机记录显示案发时间关机
|
||||
|
||||
**无辜证据**:
|
||||
- 没有直接证据证明他在现场
|
||||
|
||||
**真相**: (创作时标注,读者不可见)
|
||||
红鲱鱼,实际上在办公室加班
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 模板B:嫌疑人对比表
|
||||
```markdown
|
||||
| 嫌疑人 | 动机 | 机会 | 可疑度 | 真实身份 |
|
||||
|--------|------|------|--------|---------|
|
||||
| 张三 | 金钱纠纷 | 无不在场证明 | ★★★★☆ | 红鲱鱼 |
|
||||
| 李四 | 情感纠纷 | 有破绽的不在场证明 | ★★★☆☆ | 红鲱鱼 |
|
||||
| 王五 | 秘密威胁 | 完美不在场证明 | ★☆☆☆☆ | **真凶** |
|
||||
```
|
||||
|
||||
**作用**: 真凶往往是**可疑度最低**的那个人(反套路)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 嫌疑人排除策略
|
||||
|
||||
### 策略1:逐一排除法
|
||||
**流程**:
|
||||
```markdown
|
||||
Step 1: 列出所有嫌疑人
|
||||
Step 2: 侦探调查,发现部分人有完美不在场证明
|
||||
Step 3: 排除这些人
|
||||
Step 4: 剩余嫌疑人继续调查
|
||||
Step 5: 最终指向真凶
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
初始嫌疑人: 7人
|
||||
第10章: 排除3人(有不在场证明)
|
||||
第20章: 再排除2人(动机不足)
|
||||
第25章: 剩余2人
|
||||
第30章: 指认真凶
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 策略2:红鲱鱼误导法
|
||||
**定义**: 故意让某个无辜者看起来最可疑,误导读者。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:《东方快车谋杀案》
|
||||
误导: 所有人都有不在场证明 → 暗示凶手是外来者
|
||||
真相: 12人集体作案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 策略3:最不可疑的人法
|
||||
**定义**: 真凶是看起来最无辜的人。
|
||||
|
||||
**经典套路**:
|
||||
```markdown
|
||||
✅ 真凶特征:
|
||||
- 弱小无力(老人/小孩/残疾人)
|
||||
- 温和善良(人设完美)
|
||||
- 看似与案件无关
|
||||
- 最先被排除嫌疑
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例:《暴风雪山庄》
|
||||
最无辜者: 被害者本人
|
||||
真相: 被害者其实是凶手,假死伪装
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 嫌疑人的动机设计
|
||||
|
||||
### 动机层次
|
||||
```markdown
|
||||
表层动机(明显): 金钱/情感
|
||||
深层动机(隐藏): 秘密/复仇
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例
|
||||
表层: 张三与被害者有金钱纠纷
|
||||
深层: 张三其实是被害者的私生子,复仇
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 假动机(误导)
|
||||
**作用**: 让读者以为某人有动机,实际上是假的。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
线索: 李四曾威胁被害者:"我要杀了你!"
|
||||
真相: 李四只是一时气话,真凶另有其人
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 嫌疑人的机会管理
|
||||
|
||||
### 不在场证明的设计
|
||||
|
||||
#### 完美不在场证明
|
||||
**特征**: 有铁证证明嫌疑人不在现场
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
张三在案发时(晚8点)正在警局报案
|
||||
→ 有警局记录,完美不在场证明
|
||||
```
|
||||
|
||||
**作用**: 排除嫌疑,缩小范围
|
||||
|
||||
---
|
||||
|
||||
#### 有破绽的不在场证明
|
||||
**特征**: 不在场证明看似完美,但有漏洞
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
李四声称晚8点在家看电视
|
||||
证据: 邻居听到电视声音
|
||||
破绽: 电视可以定时播放,李四可能不在
|
||||
```
|
||||
|
||||
**作用**: 增加悬念
|
||||
|
||||
---
|
||||
|
||||
#### 虚假不在场证明(真凶常用)
|
||||
**特征**: 真凶通过诡计伪造不在场证明
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
王五伪造时间线,让死者看起来在晚9点死亡
|
||||
实际: 死者在晚7点就被杀,王五有时间作案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 嫌疑人的性格塑造
|
||||
|
||||
### 性格与可疑度的关系
|
||||
|
||||
| 性格类型 | 可疑度 | 读者反应 | 真凶概率 |
|
||||
|---------|--------|---------|---------|
|
||||
| **暴躁易怒** | 高 | "肯定是他!" | 低(红鲱鱼) |
|
||||
| **阴险狡诈** | 高 | "很可疑" | 中 |
|
||||
| **温和善良** | 低 | "不可能是他" | 高(反套路) |
|
||||
| **弱小无力** | 极低 | "绝对不是" | 极高(大反转) |
|
||||
|
||||
---
|
||||
|
||||
### 性格塑造技巧
|
||||
|
||||
#### 技巧1:言行塑造
|
||||
**示例**:
|
||||
```markdown
|
||||
张三(暴躁):
|
||||
"你再说一句,我就杀了你!"(威胁语气)
|
||||
|
||||
李四(温和):
|
||||
"算了算了,都是误会。"(好好先生)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 技巧2:他人评价
|
||||
**示例**:
|
||||
```markdown
|
||||
其他人评价张三:"他脾气很坏,经常和人吵架。"
|
||||
其他人评价李四:"他人很好,从不和人结仇。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 技巧3:行为细节
|
||||
**示例**:
|
||||
```markdown
|
||||
张三发现尸体时:
|
||||
"该死!"(冷静,没有惊慌)
|
||||
→ 可疑:为什么不惊讶?
|
||||
|
||||
李四发现尸体时:
|
||||
"啊!"(惊恐,双腿发软)
|
||||
→ 看似无辜
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 嫌疑人管理常见错误
|
||||
|
||||
### 错误1:嫌疑人太多,读者记不住
|
||||
**问题**:
|
||||
```markdown
|
||||
嫌疑人: 15人
|
||||
→ 读者:"谁是谁?我记不住!"
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 控制在12人以内
|
||||
✅ 给每个人鲜明特征(外貌/性格/口头禅)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误2:所有人动机都很强
|
||||
**问题**:
|
||||
```markdown
|
||||
所有人都有杀人动机
|
||||
→ 读者无法判断谁最可疑
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 动机分层: 强/中/弱
|
||||
✅ 只有2-3人有强动机
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误3:真凶毫无动机
|
||||
**问题**:
|
||||
```markdown
|
||||
真凶: 王五
|
||||
动机: 无
|
||||
→ 读者:"这也太牵强了!"
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 真凶必须有合理动机(可以隐藏,但不能没有)
|
||||
✅ 在揭示时补充解释
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误4:排除嫌疑太快
|
||||
**问题**:
|
||||
```markdown
|
||||
第5章: 7个嫌疑人
|
||||
第10章: 排除6个
|
||||
→ 读者:"那肯定是剩下的那个了"
|
||||
```
|
||||
|
||||
**解决**:
|
||||
```markdown
|
||||
✅ 逐步排除,不要一次性排除太多
|
||||
✅ 排除后再引入新嫌疑人
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 嫌疑人管理工具
|
||||
|
||||
### 工具1:嫌疑人追踪表
|
||||
```markdown
|
||||
| 嫌疑人 | 动机 | 机会 | 证据 | 状态 |
|
||||
|--------|------|------|------|------|
|
||||
| 张三 | 金钱 | 无证明 | 争吵记录 | 嫌疑中 |
|
||||
| 李四 | 情感 | 有破绽证明 | 威胁短信 | 嫌疑中 |
|
||||
| 王五 | 秘密 | 完美证明 | 无 | 已排除 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工具2:时间线对照表
|
||||
```markdown
|
||||
## 案发时间:晚8点
|
||||
| 嫌疑人 | 晚7点 | 晚8点 | 晚9点 | 证明 |
|
||||
|--------|-------|-------|-------|------|
|
||||
| 张三 | 公司 | ? | 家 | 无 |
|
||||
| 李四 | 家 | 家 | 家 | 邻居证言 |
|
||||
| 王五 | 警局 | 警局 | 警局 | 警局记录 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工具3:嫌疑度评分表
|
||||
```markdown
|
||||
| 嫌疑人 | 动机(0-5) | 机会(0-5) | 证据(0-5) | 总分 |
|
||||
|--------|----------|----------|----------|------|
|
||||
| 张三 | 5 | 4 | 3 | 12 |
|
||||
| 李四 | 3 | 2 | 2 | 7 |
|
||||
| 王五 | 1 | 0 | 0 | 1 |
|
||||
```
|
||||
|
||||
**作用**: 真凶往往是**总分最低**的那个(反套路)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 高级技巧:多重身份
|
||||
|
||||
### 技巧:一人分饰多角
|
||||
**原理**: 一个人伪装成两个不同的身份,制造"两个人"的假象。
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
## 案例
|
||||
表面: A和B是两个人
|
||||
真相: A和B是同一人,通过易容伪装
|
||||
```
|
||||
|
||||
**注意**: 必须在前文埋伏笔(如"从未同时出现")。
|
||||
|
||||
---
|
||||
|
||||
## 12. 嫌疑人管理自检清单
|
||||
|
||||
**设计完嫌疑人后逐项检查**:
|
||||
- [ ] 嫌疑人数量是否合理(3-12人)?
|
||||
- [ ] 每个嫌疑人都有明确动机吗?
|
||||
- [ ] 每个嫌疑人的机会是否清晰?
|
||||
- [ ] 真凶的动机是否合理?
|
||||
- [ ] 真凶的不在场证明破绽是否设计好?
|
||||
- [ ] 红鲱鱼是否足够迷惑读者?
|
||||
- [ ] 排除嫌疑的节奏是否合理?
|
||||
- [ ] 真凶是否是"最不可疑的人"?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 嫌疑人管理速查表
|
||||
|
||||
| 要素 | 标准 | 常见错误 | 解决方案 |
|
||||
|------|------|---------|---------|
|
||||
| **数量** | 3-12人 | 太多/太少 | 控制在黄金范围 |
|
||||
| **动机** | 每人至少1个 | 真凶无动机 | 补充隐藏动机 |
|
||||
| **机会** | 时间线清晰 | 时间线矛盾 | 绘制时间表 |
|
||||
| **性格** | 鲜明特征 | 人物脸谱化 | 增加细节 |
|
||||
| **排除** | 逐步排除 | 一次排除太多 | 分阶段排除 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:经典案例分析
|
||||
|
||||
### 案例1:《东方快车谋杀案》
|
||||
**嫌疑人**: 12人
|
||||
**特点**: 所有人都有完美不在场证明
|
||||
**真相**: 12人集体作案
|
||||
**技巧**: 用"完美证明"误导读者
|
||||
|
||||
---
|
||||
|
||||
### 案例2:《无人生还》
|
||||
**嫌疑人**: 10人
|
||||
**特点**: 逐一被杀
|
||||
**真相**: 法官假死,是真凶
|
||||
**技巧**: 最早"被杀"的人是凶手
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
**嫌疑人管理 = 数量控制 + 动机设计 + 机会管理 + 性格塑造 + 排除策略**
|
||||
|
||||
记住:真凶往往是"最不可疑的人"。管理好嫌疑人,就是控制好推理的难度和乐趣。
|
||||
@@ -0,0 +1,507 @@
|
||||
# 诡计设计 (Trick Design)
|
||||
|
||||
> **核心原则**: 好的诡计 = 简单原理 + 巧妙应用 + 意外性。复杂不等于高明。
|
||||
|
||||
---
|
||||
|
||||
## 1. 什么是"诡计"?
|
||||
|
||||
### 定义
|
||||
**诡计(Trick/Gimmick)**: 凶手为了实施犯罪或掩盖罪行而使用的特殊手段,通常涉及**密室**、**不在场证明**、**身份伪装**等。
|
||||
|
||||
### 诡计的三大作用
|
||||
```markdown
|
||||
1. 制造谜题: 让读者困惑"这怎么可能?"
|
||||
2. 延迟破案: 增加侦探调查难度
|
||||
3. 制造惊喜: 揭示时让读者恍然大悟
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 诡计的分类
|
||||
|
||||
### 按目的分类
|
||||
|
||||
#### 类型1:密室诡计(Locked Room)
|
||||
**目的**: 制造"密闭空间内杀人,凶手却消失"的不可能犯罪
|
||||
|
||||
**经典模式**:
|
||||
```markdown
|
||||
✅ 冰块诡计: 用冰块固定门栓,融化后门自动上锁
|
||||
✅ 双重密室: 内室是真密室,外室是伪装
|
||||
✅ 时间差: 凶手在死者锁门前藏在房内
|
||||
✅ 机关装置: 利用绳索/重物制造延时上锁
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型2:不在场证明诡计(Alibi Trick)
|
||||
**目的**: 让凶手在案发时看似不在现场
|
||||
|
||||
**经典模式**:
|
||||
```markdown
|
||||
✅ 时间诡计: 伪造死亡时间(调慢钟表、冰冻尸体)
|
||||
✅ 替身诡计: 找人冒充自己在别处出现
|
||||
✅ 远程杀人: 提前设置机关,人不在也能杀人
|
||||
✅ 证人串通: 买通证人作伪证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型3:身份诡计(Identity Trick)
|
||||
**目的**: 隐藏凶手真实身份或制造混淆
|
||||
|
||||
**经典模式**:
|
||||
```markdown
|
||||
✅ 双胞胎诡计: 两人轮流出现,伪造不在场证明
|
||||
✅ 易容诡计: 化妆成他人
|
||||
✅ 死者身份诡计: 被害者其实是凶手
|
||||
✅ 身份互换: A假死,伪装成B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型4:凶器诡计(Weapon Trick)
|
||||
**目的**: 隐藏或伪装凶器
|
||||
|
||||
**经典模式**:
|
||||
```markdown
|
||||
✅ 冰凶器: 用冰锥杀人,融化后消失
|
||||
✅ 日常物品: 用绳索/枕头等非典型凶器
|
||||
✅ 毒物诡计: 慢性毒药,延迟发作
|
||||
✅ 凶器转移: 杀人后把凶器藏在意外地点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 类型5:心理诡计(Psychological Trick)
|
||||
**目的**: 利用读者/侦探的思维定势误导推理
|
||||
|
||||
**经典模式**:
|
||||
```markdown
|
||||
✅ 最不可能的人: 凶手是最不可疑的人(小孩/老人/残疾人)
|
||||
✅ 叙述性诡计: 叙述者本身是凶手(《罗杰疑案》)
|
||||
✅ 暴风雪山庄反转: 被害者其实是凶手
|
||||
✅ 多重身份: 一人分饰多角
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 按复杂度分类
|
||||
|
||||
| 等级 | 特点 | 适用场景 | 示例 |
|
||||
|------|------|---------|------|
|
||||
| **简单诡计** | 单一原理 | 短篇/网文 | 冰块固定门栓 |
|
||||
| **复合诡计** | 2-3个诡计组合 | 中长篇 | 时间诡计+替身诡计 |
|
||||
| **多重诡计** | 诡计套诡计 | 长篇神作 | 《无人生还》 |
|
||||
|
||||
**注意**: 网文推荐使用**简单诡计**或**复合诡计**,多重诡计容易让读者看不懂。
|
||||
|
||||
---
|
||||
|
||||
## 3. 诡计设计的四大原则
|
||||
|
||||
### 原则1:简单性(Simplicity)
|
||||
**定义**: 核心原理必须简单,读者事后能理解。
|
||||
|
||||
**反面教材**:
|
||||
```markdown
|
||||
❌ 错误: 凶手利用量子纠缠原理制造密室
|
||||
→ 读者完全不懂
|
||||
```
|
||||
|
||||
**正面示例**:
|
||||
```markdown
|
||||
✅ 正确: 凶手用冰块固定门栓,冰融化后门自动上锁
|
||||
→ 原理简单,一听就懂
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 原则2:新颖性(Novelty)
|
||||
**定义**: 诡计要有创新,不能完全照搬经典。
|
||||
|
||||
**避免陈词滥调**:
|
||||
```markdown
|
||||
❌ 过时诡计:
|
||||
- 双胞胎(用烂了)
|
||||
- 冰锥杀人(太经典)
|
||||
- 左轮手枪俄罗斯轮盘赌(老套)
|
||||
```
|
||||
|
||||
**创新方法**:
|
||||
```markdown
|
||||
✅ 旧瓶装新酒:
|
||||
- 双胞胎 → 改为整容成双胞胎
|
||||
- 冰锥 → 改为干冰/液氮
|
||||
- 密室 → 改为"开放式密室"(人人都能进,但凶手消失)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 原则3:合理性(Plausibility)
|
||||
**定义**: 诡计必须在现实中**理论上可行**,不能过于魔幻。
|
||||
|
||||
**合理性检查**:
|
||||
```markdown
|
||||
✅ 物理可行: 符合物理定律(重力/惯性/热力学)
|
||||
✅ 时间可行: 凶手有足够时间实施
|
||||
✅ 技术可行: 不依赖未来科技或魔法
|
||||
```
|
||||
|
||||
**反面教材**:
|
||||
```markdown
|
||||
❌ 凶手用瞬移能力逃出密室
|
||||
→ 超自然力量,违背本格推理原则
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 原则4:公平性(Fairness)
|
||||
**定义**: 读者能根据线索推理出诡计。
|
||||
|
||||
**公平性要求**:
|
||||
```markdown
|
||||
✅ 诡计所需的关键物品必须在前文提及
|
||||
✅ 诡计原理不能过于专业(除非有解释)
|
||||
✅ 不能依赖读者不知道的隐藏信息
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
✅ 公平:
|
||||
第5章: 提到房间有个壁橱
|
||||
第20章: 揭示凶手藏在壁橱里
|
||||
|
||||
❌ 不公平:
|
||||
第20章: 突然揭示房间有密道
|
||||
→ 前文从未提及
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 经典诡计解析
|
||||
|
||||
### 诡计1:冰块密室
|
||||
**原理**: 用冰块固定门栓,冰融化后门自动上锁
|
||||
|
||||
**实施步骤**:
|
||||
```markdown
|
||||
1. 凶手杀人后,用冰块顶住门栓
|
||||
2. 凶手离开房间,从外面关门
|
||||
3. 冰块融化,门栓自动落下,形成密室
|
||||
```
|
||||
|
||||
**线索**:
|
||||
```markdown
|
||||
- 门下方有水渍
|
||||
- 房间温度略高
|
||||
- 时间线有疑点(冰融化需要时间)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 诡计2:时间诡计(伪造死亡时间)
|
||||
**原理**: 让死者看起来在凶手有不在场证明的时间死亡
|
||||
|
||||
**常用方法**:
|
||||
```markdown
|
||||
1. 调慢死者手表
|
||||
2. 提前杀人,用冰块冷冻尸体(降低尸温)
|
||||
3. 制造"死者还活着"的假象(录音/定时发短信)
|
||||
```
|
||||
|
||||
**线索**:
|
||||
```markdown
|
||||
- 尸体温度异常
|
||||
- 手表时间与其他证据矛盾
|
||||
- 目击证言有漏洞
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 诡计3:叙述性诡计(叙述者是凶手)
|
||||
**原理**: 故事以第一人称叙述,但叙述者本身是凶手,通过省略关键信息误导读者
|
||||
|
||||
**经典案例**: 《罗杰疑案》(阿加莎·克里斯蒂)
|
||||
|
||||
**实施技巧**:
|
||||
```markdown
|
||||
1. 叙述者描述事件时,省略自己的犯罪行为
|
||||
2. 用暗示性语言误导读者
|
||||
3. 最后揭示:"我就是凶手"
|
||||
```
|
||||
|
||||
**注意**: 这是高难度诡计,需要极高的叙事技巧。
|
||||
|
||||
---
|
||||
|
||||
### 诡计4:暴风雪山庄反转
|
||||
**原理**: 读者以为A是被害者,实际上A是凶手,伪装成被害者
|
||||
|
||||
**实施步骤**:
|
||||
```markdown
|
||||
1. 凶手A杀死B
|
||||
2. A伪装成B的尸体
|
||||
3. A假死,让大家以为A被杀
|
||||
4. 实际上死者是B,凶手A还活着
|
||||
```
|
||||
|
||||
**经典案例**: 《暴风雪山庄》(东野圭吾)
|
||||
|
||||
---
|
||||
|
||||
### 诡计5:多人共犯(集体作案)
|
||||
**原理**: 所有嫌疑人都是凶手,集体作案
|
||||
|
||||
**实施步骤**:
|
||||
```markdown
|
||||
1. 每个人只负责一部分(A负责下毒,B负责处理尸体)
|
||||
2. 互相提供不在场证明
|
||||
3. 侦探难以找到单一凶手
|
||||
```
|
||||
|
||||
**经典案例**: 《东方快车谋杀案》
|
||||
|
||||
---
|
||||
|
||||
## 5. 诡计设计流程
|
||||
|
||||
### Step 1: 确定诡计类型
|
||||
**选择标准**: 根据故事需要选择合适的诡计类型
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
故事需要: 密闭空间杀人
|
||||
→ 选择: 密室诡计
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 构思核心原理
|
||||
**核心问题**: "凶手如何做到的?"
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
问题: 如何在上锁房间内杀人后逃脱?
|
||||
核心原理: 提前藏在房内,杀人后伪装成第一发现者
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 检查可行性
|
||||
**可行性检查清单**:
|
||||
```markdown
|
||||
- [ ] 物理上可行吗?
|
||||
- [ ] 凶手有足够时间吗?
|
||||
- [ ] 需要什么道具?(道具必须在前文提及)
|
||||
- [ ] 有没有破绽?(如果有,如何掩盖?)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 设计破绽
|
||||
**核心问题**: "侦探如何发现破绽?"
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
诡计: 凶手藏在壁橱里
|
||||
破绽: 壁橱门有划痕,凶手衣服上有灰尘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 埋设线索
|
||||
**核心问题**: "如何让读者能推理出诡计?"
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
第5章: 提到房间有壁橱
|
||||
第10章: 第一发现者衣服上有灰尘
|
||||
第15章: 壁橱门有划痕
|
||||
→ 读者可以推理:凶手藏在壁橱里
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 诡计的常见错误
|
||||
|
||||
### 错误1:过于复杂
|
||||
**示例**:
|
||||
```markdown
|
||||
❌ 错误:
|
||||
凶手利用镜面反射+声音传播延迟+心理暗示
|
||||
→ 三重诡计叠加,读者看不懂
|
||||
```
|
||||
|
||||
**改进**:
|
||||
```markdown
|
||||
✅ 正确:
|
||||
凶手利用镜面反射制造"死者还活着"的错觉
|
||||
→ 单一诡计,简单明了
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误2:依赖运气
|
||||
**示例**:
|
||||
```markdown
|
||||
❌ 错误:
|
||||
凶手碰巧发现密道
|
||||
→ 全靠运气,不合理
|
||||
```
|
||||
|
||||
**改进**:
|
||||
```markdown
|
||||
✅ 正确:
|
||||
凶手提前调查房屋结构,发现密道
|
||||
→ 有准备,合理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误3:违背物理定律
|
||||
**示例**:
|
||||
```markdown
|
||||
❌ 错误:
|
||||
凶手用意念移动物体
|
||||
→ 超自然力量
|
||||
```
|
||||
|
||||
**改进**:
|
||||
```markdown
|
||||
✅ 正确:
|
||||
凶手用隐形细线拉动物体
|
||||
→ 符合物理定律
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 错误4:前文无伏笔
|
||||
**示例**:
|
||||
```markdown
|
||||
❌ 错误:
|
||||
第30章突然揭示:"房间其实有密道!"
|
||||
→ 前文从未提及
|
||||
```
|
||||
|
||||
**改进**:
|
||||
```markdown
|
||||
✅ 正确:
|
||||
第5章: 房屋是古宅,有很多秘密
|
||||
第10章: 侦探注意到墙壁厚度异常
|
||||
第30章: 揭示密道
|
||||
→ 有伏笔
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 诡计创新方法
|
||||
|
||||
### 方法1:旧诡计新用
|
||||
**示例**:
|
||||
```markdown
|
||||
经典: 冰块密室
|
||||
创新: 干冰密室(升华不留水渍)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方法2:逆向思维
|
||||
**示例**:
|
||||
```markdown
|
||||
常规: 凶手制造密室逃脱
|
||||
逆向: 凶手制造"开放式密室"(人人能进,但凶手消失)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方法3:组合创新
|
||||
**示例**:
|
||||
```markdown
|
||||
诡计A: 时间诡计(伪造死亡时间)
|
||||
诡计B: 替身诡计(找人冒充)
|
||||
组合: 替身在A地制造不在场证明,凶手在B地杀人
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方法4:现代化改造
|
||||
**示例**:
|
||||
```markdown
|
||||
经典: 电话录音制造"死者还活着"的假象
|
||||
现代: 用AI语音合成制造假电话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 诡计设计自检清单
|
||||
|
||||
**完成诡计设计后逐项检查**:
|
||||
- [ ] 诡计原理是否简单?
|
||||
- [ ] 诡计是否有新意?(不是完全照搬经典)
|
||||
- [ ] 诡计在物理上可行吗?
|
||||
- [ ] 凶手有足够时间实施吗?
|
||||
- [ ] 所需道具是否在前文提及?
|
||||
- [ ] 诡计的破绽是否合理?
|
||||
- [ ] 读者能否根据线索推理出诡计?
|
||||
- [ ] 有没有违背"十诫"?
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 诡计设计速查表
|
||||
|
||||
| 诡计类型 | 难度 | 适用篇幅 | 核心要素 | 常见破绽 |
|
||||
|---------|------|---------|---------|---------|
|
||||
| **密室诡计** | 中 | 短篇/中篇 | 物理机关 | 现场痕迹 |
|
||||
| **时间诡计** | 低 | 短篇 | 时间线 | 尸体温度 |
|
||||
| **身份诡计** | 高 | 中长篇 | 伪装/易容 | 细节破绽 |
|
||||
| **叙述诡计** | 极高 | 长篇 | 叙事技巧 | 逻辑漏洞 |
|
||||
| **心理诡计** | 中 | 短篇/中篇 | 思维定势 | 动机不合理 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:诡计创作工具
|
||||
|
||||
### 工具1:诡计设计模板
|
||||
```markdown
|
||||
## 诡计名称
|
||||
**类型**: 密室/时间/身份/凶器/心理
|
||||
|
||||
**核心原理**:
|
||||
(用一句话描述诡计核心)
|
||||
|
||||
**实施步骤**:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**所需道具**:
|
||||
-
|
||||
|
||||
**破绽**:
|
||||
-
|
||||
|
||||
**线索埋设**:
|
||||
- 第X章:
|
||||
- 第Y章:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工具2:可行性检验表
|
||||
```markdown
|
||||
- [ ] 物理可行性: 是/否
|
||||
- [ ] 时间可行性: 是/否
|
||||
- [ ] 技术可行性: 是/否
|
||||
- [ ] 公平性: 线索是否充分
|
||||
- [ ] 新颖性: 是否有创新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
**好诡计 = 简单原理 + 巧妙应用 + 意外性 + 公平性**
|
||||
|
||||
记住:诡计的目的不是"炫技",而是让读者看完后恍然大悟:"原来是这样!我也能想到!"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user