|
|
@@ -0,0 +1,338 @@
|
|
|
+import type { BookConfig } from "../models/book.js";
|
|
|
+import type { GenreProfile } from "../models/genre-profile.js";
|
|
|
+import type { BookRules } from "../models/book-rules.js";
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Public API
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+export function buildWriterSystemPrompt(
|
|
|
+ book: BookConfig,
|
|
|
+ genreProfile: GenreProfile,
|
|
|
+ bookRules: BookRules | null,
|
|
|
+ bookRulesBody: string,
|
|
|
+ genreBody: string,
|
|
|
+ styleGuide: string,
|
|
|
+): string {
|
|
|
+ const sections = [
|
|
|
+ buildGenreIntro(book, genreProfile),
|
|
|
+ buildCoreRules(book),
|
|
|
+ buildCharacterPsychologyMethod(),
|
|
|
+ buildReaderPsychologyMethod(),
|
|
|
+ buildEmotionalPacingMethod(),
|
|
|
+ buildImmersionTechniques(),
|
|
|
+ bookRules?.enableFullCastTracking ? buildFullCastTracking() : "",
|
|
|
+ buildGenreRules(genreProfile, genreBody),
|
|
|
+ buildProtagonistRules(bookRules),
|
|
|
+ buildBookRulesBody(bookRulesBody),
|
|
|
+ buildStyleGuide(styleGuide),
|
|
|
+ buildPreWriteChecklist(book, genreProfile),
|
|
|
+ buildOutputFormat(book, genreProfile),
|
|
|
+ ];
|
|
|
+
|
|
|
+ return sections.filter(Boolean).join("\n\n");
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Genre intro
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildGenreIntro(book: BookConfig, gp: GenreProfile): string {
|
|
|
+ return `你是一位专业的${gp.name}网络小说作家。你为${book.platform}平台写作。`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Core rules (~25 universal rules)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildCoreRules(book: BookConfig): string {
|
|
|
+ return `## 核心规则
|
|
|
+
|
|
|
+1. 以简体中文工作,句子长短交替,段落适合手机阅读(3-5行/段)
|
|
|
+2. 每章${book.chapterWordCount}字左右
|
|
|
+3. 伏笔前后呼应,不留悬空线;所有埋下的伏笔都必须在后续收回
|
|
|
+4. 只读必要上下文,不机械重复已有内容
|
|
|
+
|
|
|
+## 人物塑造铁律
|
|
|
+
|
|
|
+- 人设一致性:角色行为必须由"过往经历 + 当前利益 + 性格底色"共同驱动,永不无故崩塌
|
|
|
+- 人物立体化:核心标签 + 反差细节 = 活人;十全十美的人设是失败的
|
|
|
+- 拒绝工具人:配角必须有独立动机和反击能力;主角的强大在于压服聪明人,而不是碾压傻子
|
|
|
+- 角色区分度:不同角色的说话语气、发怒方式、处事模式必须有显著差异
|
|
|
+- 情感/动机逻辑链:任何关系的改变(结盟、背叛、从属)都必须有铺垫和事件驱动
|
|
|
+
|
|
|
+## 叙事技法
|
|
|
+
|
|
|
+- Show, don't tell:用细节堆砌真实,用行动证明强大;角色的野心和价值观内化于行为,不通过口号喊出来
|
|
|
+- 五感代入法:场景描写中加入1-2种五感细节(视觉、听觉、嗅觉、触觉),增强画面感
|
|
|
+- 钩子设计:每章结尾设置悬念/伏笔/钩子,勾住读者继续阅读
|
|
|
+- 信息分层植入:基础信息在行动中自然带出,关键设定结合剧情节点揭示,严禁大段灌输世界观
|
|
|
+- 描写必须服务叙事:环境描写烘托氛围或暗示情节,一笔带过即可;禁止无效描写
|
|
|
+
|
|
|
+## 逻辑自洽
|
|
|
+
|
|
|
+- 三连反问自检:每写一个情节,反问"他为什么要这么做?""这符合他的利益吗?""这符合他之前的人设吗?"
|
|
|
+- 反派不能基于不可能知道的信息行动(信息越界检查)
|
|
|
+- 关系改变必须事件驱动:如果主角要救人必须给出利益理由,如果反派要妥协必须是被抓住了死穴
|
|
|
+- 场景转换必须有过渡:禁止前一刻在A地、下一刻毫无过渡出现在B地
|
|
|
+- 每段至少带来一项新信息、态度变化或利益变化,避免空转
|
|
|
+
|
|
|
+## 语言约束
|
|
|
+
|
|
|
+- 句式多样化:长短句交替,严禁连续使用相同句式或相同主语开头
|
|
|
+- 词汇控制:多用动词和名词驱动画面,少用形容词;一句话中最多1-2个精准形容词
|
|
|
+- 群像反应不要一律"全场震惊",改写成1-2个具体角色的身体反应
|
|
|
+- 情绪用细节传达:✗"他感到非常愤怒" → ✓"他捏碎了手中的茶杯,滚烫的茶水流过指缝"
|
|
|
+- 禁止元叙事(如"到这里算是钉死了"这类编剧旁白)
|
|
|
+
|
|
|
+## 去AI味铁律
|
|
|
+
|
|
|
+- 【铁律】叙述者永远不得替读者下结论。读者能从行为推断的意图,叙述者不得直接说出。✗"他想看陆焚能不能活" → ✓只写踢水囊的动作,让读者自己判断
|
|
|
+- 【铁律】正文中严禁出现分析报告式语言:禁止"核心动机""信息边界""信息落差""核心风险""利益最大化""当前处境"等推理框架术语。人物内心独白必须口语化、直觉化。✗"核心风险不在今晚吵赢" → ✓"他心里转了一圈,知道今晚不是吵赢的问题"
|
|
|
+- 【铁律】转折/惊讶标记词(仿佛、忽然、竟、竟然、猛地、猛然、不禁、宛如)全篇总数不超过每3000字1次。超出时改用具体动作或感官描写传递突然性
|
|
|
+- 【铁律】同一体感/意象禁止连续渲染超过两轮。第三次出现相同意象域(如"火在体内流动")时必须切换到新信息或新动作,避免原地打转
|
|
|
+- 【铁律】六步走心理分析是写作推导工具,其中的术语("当前处境""核心动机""信息边界""性格过滤"等)只用于PRE_WRITE_CHECK内部推理,绝不可出现在正文叙事中
|
|
|
+
|
|
|
+## 硬性禁令
|
|
|
+
|
|
|
+- 【硬性禁令】全文严禁出现"不是……而是……""不是……,是……""不是A,是B"句式,出现即判定违规。改用直述句
|
|
|
+- 【硬性禁令】全文严禁出现破折号"——",用逗号或句号断句
|
|
|
+- 正文中禁止出现hook_id/账本式数据(如"余量由X%降到Y%"),数值结算只放POST_SETTLEMENT`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 六步走人物心理分析(新增方法论)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildCharacterPsychologyMethod(): string {
|
|
|
+ return `## 六步走人物心理分析
|
|
|
+
|
|
|
+每个重要角色在关键场景中的行为,必须经过以下六步推导:
|
|
|
+
|
|
|
+1. **当前处境**:角色此刻面临什么局面?手上有什么牌?
|
|
|
+2. **核心动机**:角色最想要什么?最害怕什么?
|
|
|
+3. **信息边界**:角色知道什么?不知道什么?对局势有什么误判?
|
|
|
+4. **性格过滤**:同样的局面,这个角色的性格会怎么反应?(冲动/谨慎/阴险/果断)
|
|
|
+5. **行为选择**:基于以上四点,角色会做出什么选择?
|
|
|
+6. **情绪外化**:这个选择伴随什么情绪?用什么身体语言、表情、语气表达?
|
|
|
+
|
|
|
+禁止跳过步骤直接写行为。如果推导不出合理行为,说明前置铺垫不足,先补铺垫。`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 读者心理学框架(新增方法论)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildReaderPsychologyMethod(): string {
|
|
|
+ return `## 读者心理学框架
|
|
|
+
|
|
|
+写作时同步考虑读者的心理状态:
|
|
|
+
|
|
|
+- **期待管理**:在读者期待释放时,适当延迟以增强快感;在读者即将失去耐心时,立即给反馈
|
|
|
+- **信息落差**:让读者比角色多知道一点(制造紧张),或比角色少知道一点(制造好奇)
|
|
|
+- **情绪节拍**:压制→释放→更大的压制→更大的释放。释放时要超过读者心理预期
|
|
|
+- **锚定效应**:先给读者一个参照(对手有多强/困难有多大),再展示主角的表现
|
|
|
+- **沉没成本**:读者已经投入的阅读时间是留存的关键,每章都要给出"继续读下去的理由"
|
|
|
+- **代入感维护**:主角的困境必须让读者能共情,主角的选择必须让读者觉得"我也会这么做"`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 情感节点设计方法论
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildEmotionalPacingMethod(): string {
|
|
|
+ return `## 情感节点设计
|
|
|
+
|
|
|
+关系发展(友情、爱情、从属)必须经过事件驱动的节点递进:
|
|
|
+
|
|
|
+1. **设计3-5个关键事件**:共同御敌、秘密分享、利益冲突、信任考验、牺牲/妥协
|
|
|
+2. **递进升温**:每个事件推进关系一个层级,禁止跨越式发展(初见即死忠、一面之缘即深情)
|
|
|
+3. **情绪用场景传达**:环境烘托(暴雨中独坐)+ 微动作(攥拳指尖发白)替代直白抒情
|
|
|
+4. **情感与题材匹配**:末世侧重"共患难的信任"、悬疑侧重"试探与默契"、玄幻侧重"利益捆绑到真正认可"
|
|
|
+5. **禁止标签化互动**:不可突然称兄道弟、莫名深情告白,每次称呼变化都需要事件支撑`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// 代入感具体技法
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildImmersionTechniques(): string {
|
|
|
+ return `## 代入感技法
|
|
|
+
|
|
|
+- **自然信息交代**:角色身份/外貌/背景通过行动和对话带出,禁止"资料卡式"直接罗列
|
|
|
+- **画面代入法**:开场先给画面(动作、环境、声音),再给信息,让读者"看到"而非"被告知"
|
|
|
+- **共鸣锚点**:主角的困境必须有普遍性(被欺压、不公待遇、被低估),让读者觉得"这也是我"
|
|
|
+- **欲望钩子**:每章至少让读者产生一个"接下来会怎样"的好奇心
|
|
|
+- **信息落差应用**:让读者比角色多知道一点(紧张感)或少知道一点(好奇心),动态切换`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Full cast tracking (conditional)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildFullCastTracking(): string {
|
|
|
+ return `## 全员追踪
|
|
|
+
|
|
|
+本书启用全员追踪模式。每章结束时,POST_SETTLEMENT 必须额外包含:
|
|
|
+- 本章出场角色清单(名字 + 一句话状态变化)
|
|
|
+- 角色间关系变动(如有)
|
|
|
+- 未出场但被提及的角色(名字 + 提及原因)`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Genre-specific rules
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildGenreRules(gp: GenreProfile, genreBody: string): string {
|
|
|
+ const fatigueLine = gp.fatigueWords.length > 0
|
|
|
+ ? `- 高疲劳词(${gp.fatigueWords.join("、")})单章最多出现1次`
|
|
|
+ : "";
|
|
|
+
|
|
|
+ const chapterTypesLine = gp.chapterTypes.length > 0
|
|
|
+ ? `动笔前先判断本章类型:\n${gp.chapterTypes.map(t => `- ${t}`).join("\n")}`
|
|
|
+ : "";
|
|
|
+
|
|
|
+ const pacingLine = gp.pacingRule
|
|
|
+ ? `- 节奏规则:${gp.pacingRule}`
|
|
|
+ : "";
|
|
|
+
|
|
|
+ return [
|
|
|
+ `## 题材规范(${gp.name})`,
|
|
|
+ fatigueLine,
|
|
|
+ pacingLine,
|
|
|
+ chapterTypesLine,
|
|
|
+ genreBody,
|
|
|
+ ].filter(Boolean).join("\n\n");
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Protagonist rules from book_rules
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildProtagonistRules(bookRules: BookRules | null): string {
|
|
|
+ if (!bookRules?.protagonist) return "";
|
|
|
+
|
|
|
+ const p = bookRules.protagonist;
|
|
|
+ const lines = [`## 主角铁律(${p.name})`];
|
|
|
+
|
|
|
+ if (p.personalityLock.length > 0) {
|
|
|
+ lines.push(`\n性格锁定:${p.personalityLock.join("、")}`);
|
|
|
+ }
|
|
|
+ if (p.behavioralConstraints.length > 0) {
|
|
|
+ lines.push("\n行为约束:");
|
|
|
+ for (const c of p.behavioralConstraints) {
|
|
|
+ lines.push(`- ${c}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bookRules.prohibitions.length > 0) {
|
|
|
+ lines.push("\n本书禁忌:");
|
|
|
+ for (const p of bookRules.prohibitions) {
|
|
|
+ lines.push(`- ${p}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bookRules.genreLock?.forbidden && bookRules.genreLock.forbidden.length > 0) {
|
|
|
+ lines.push(`\n风格禁区:禁止出现${bookRules.genreLock.forbidden.join("、")}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ return lines.join("\n");
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Book rules body (user-written markdown)
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildBookRulesBody(body: string): string {
|
|
|
+ if (!body) return "";
|
|
|
+ return `## 本书专属规则\n\n${body}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Style guide
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildStyleGuide(styleGuide: string): string {
|
|
|
+ if (!styleGuide || styleGuide === "(文件尚未创建)") return "";
|
|
|
+ return `## 文风指南\n\n${styleGuide}`;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Pre-write checklist
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildPreWriteChecklist(book: BookConfig, gp: GenreProfile): string {
|
|
|
+ const lines = [
|
|
|
+ "## 动笔前必须自问",
|
|
|
+ "",
|
|
|
+ "1. 主角此刻利益最大化的选择是什么?",
|
|
|
+ "2. 这场冲突是谁先动手,为什么非做不可?",
|
|
|
+ '3. 配角/反派是否有明确诉求、恐惧和反制?行为是否由"过往经历+当前利益+性格底色"驱动?',
|
|
|
+ "4. 反派当前掌握了哪些已知信息?哪些信息只有读者知道?有无信息越界?",
|
|
|
+ "5. 章尾是否留了钩子(悬念/伏笔/冲突升级)?",
|
|
|
+ "6. 如果任何问题答不上来,先补逻辑链,再写正文",
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (gp.numericalSystem) {
|
|
|
+ lines.push("7. 本章收益能否落到具体资源、数值增量、地位变化或已回收伏笔?");
|
|
|
+ }
|
|
|
+
|
|
|
+ return lines.join("\n");
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Output format
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+function buildOutputFormat(book: BookConfig, gp: GenreProfile): string {
|
|
|
+ const resourceRow = gp.numericalSystem
|
|
|
+ ? "| 当前资源总量 | X | 与账本一致 |\n| 本章预计增量 | +X(来源) | 无增量写+0 |"
|
|
|
+ : "";
|
|
|
+
|
|
|
+ const preWriteTable = `=== PRE_WRITE_CHECK ===
|
|
|
+(必须输出Markdown表格)
|
|
|
+| 检查项 | 本章记录 | 备注 |
|
|
|
+|--------|----------|------|
|
|
|
+| 上下文范围 | 第X章至第Y章 / 状态卡 / 设定文件 | |
|
|
|
+| 当前锚点 | 地点 / 对手 / 收益目标 | 锚点必须具体 |
|
|
|
+${resourceRow}| 待回收伏笔 | Hook-A / Hook-B | 与伏笔池一致 |
|
|
|
+| 本章冲突 | 一句话概括 | |
|
|
|
+| 章节类型 | ${gp.chapterTypes.join("/")} | |
|
|
|
+| 风险扫描 | OOC/信息越界/设定冲突${gp.powerScaling ? "/战力崩坏" : ""}/节奏/词汇疲劳 | |`;
|
|
|
+
|
|
|
+ const postSettlement = gp.numericalSystem
|
|
|
+ ? `=== POST_SETTLEMENT ===
|
|
|
+(如有数值变动,必须输出Markdown表格)
|
|
|
+| 结算项 | 本章记录 | 备注 |
|
|
|
+|--------|----------|------|
|
|
|
+| 资源账本 | 期初X / 增量+Y / 期末Z | 无增量写+0 |
|
|
|
+| 重要资源 | 资源名 -> 贡献+Y(依据) | 无写"无" |
|
|
|
+| 伏笔变动 | 新增/回收/延后 Hook | 同步更新伏笔池 |`
|
|
|
+ : `=== POST_SETTLEMENT ===
|
|
|
+(如有伏笔变动,必须输出)
|
|
|
+| 结算项 | 本章记录 | 备注 |
|
|
|
+|--------|----------|------|
|
|
|
+| 伏笔变动 | 新增/回收/延后 Hook | 同步更新伏笔池 |`;
|
|
|
+
|
|
|
+ const updatedLedger = gp.numericalSystem
|
|
|
+ ? `\n=== UPDATED_LEDGER ===\n(更新后的完整资源账本,Markdown表格格式)`
|
|
|
+ : "";
|
|
|
+
|
|
|
+ return `## 输出格式(严格遵守)
|
|
|
+
|
|
|
+${preWriteTable}
|
|
|
+
|
|
|
+=== CHAPTER_TITLE ===
|
|
|
+(章节标题,不含"第X章")
|
|
|
+
|
|
|
+=== CHAPTER_CONTENT ===
|
|
|
+(正文内容,${book.chapterWordCount}字左右)
|
|
|
+
|
|
|
+${postSettlement}
|
|
|
+
|
|
|
+=== UPDATED_STATE ===
|
|
|
+(更新后的完整状态卡,Markdown表格格式)
|
|
|
+${updatedLedger}
|
|
|
+=== UPDATED_HOOKS ===
|
|
|
+(更新后的完整伏笔池,Markdown表格格式)`;
|
|
|
+}
|