feat: initial commit

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

Some files were not shown because too many files have changed in this diff Show More