import type { ProjectMemory, ProjectRequirements } from '#shared/types' import type { AgentRow, ChapterRow, CharacterRow, LoreRow, ProjectContext } from './context' /* ============================================================ * 系统提示词分层:用户自定义(最高优先级)> 角色内置 > 写作要求 * ============================================================ */ export function composeSystem(customPrompt: string, builtin: string, extra = '') { const parts: string[] = [] if (customPrompt.trim()) { parts.push([ '', '以下是用户自定义的最高优先级指令。它们的优先级高于本提示中其他所有内容;如有任何冲突,一律以此为准。', '', customPrompt.trim(), '' ].join('\n')) } parts.push(builtin.trim()) if (extra.trim()) parts.push(extra.trim()) return parts.join('\n\n') } const section = (tag: string, body: string) => (body.trim() ? `<${tag}>\n${body.trim()}\n` : '') const join = (...parts: string[]) => parts.filter(Boolean).join('\n\n') export function formatRequirements(r: ProjectRequirements) { const lines = [ r.language && `- 语言:${r.language}`, r.style && `- 文风:${r.style}`, r.pov && `- 叙事视角:${r.pov}`, r.narration && `- 叙述方式:${r.narration}`, r.wordsPerChapter && `- 每章目标字数:约 ${r.wordsPerChapter} 字`, r.extra && `- 其他要求:${r.extra}` ].filter(Boolean) return lines.join('\n') } export function formatCharacter(c: CharacterRow, full = true) { const lines = [`## ${c.name}${c.identity ? `(${c.identity})` : ''}`] if (c.description) lines.push(full ? c.description : truncate(c.description, 300)) if (c.personality) lines.push(`性格:${c.personality}`) if (full && c.speechExamples) lines.push(`说话风格示例:\n${truncate(c.speechExamples, 1500)}`) if (full && c.notes) lines.push(`备注:${c.notes}`) return lines.join('\n') } /** 根据文本挑出出场人物;一个都没匹配到时返回全部角色 */ export function pickCharacters(chars: CharacterRow[], scanText: string) { const hit = chars.filter(c => scanText.includes(c.name)) return hit.length ? hit : chars } function keyMatches(key: string, text: string) { const m = key.match(/^\/(.+)\/([a-z]*)$/) if (m) { try { return new RegExp(m[1]!, m[2]).test(text) } catch { return false } } return key.trim() !== '' && text.toLowerCase().includes(key.toLowerCase()) } /** 世界书:常驻条目 + 关键词命中的条目 */ export function selectLore(lore: LoreRow[], scanText: string) { return lore.filter(l => l.enabled && (l.constant || l.keys.some(k => keyMatches(k, scanText)))) } export function formatLore(entries: LoreRow[]) { return entries.map(e => `### ${e.title || e.keys.slice(0, 3).join('/') || '设定'}\n${e.content}`).join('\n\n') } export function formatMemory(m: ProjectMemory) { const parts: string[] = [] if (m.characterStates.length) parts.push(`人物当前状态:\n${m.characterStates.map(s => `- ${s.name}:${s.state}`).join('\n')}`) const open = m.foreshadowing.filter(f => f.status === 'open') if (open.length) parts.push(`未回收的伏笔:\n${open.map(f => `- (第${f.chapter}章埋下)${f.text}`).join('\n')}`) return parts.join('\n\n') } export function formatChapterPlan(chapters: ChapterRow[], highlightIndex?: number) { return chapters.map((c) => { const mark = c.index === highlightIndex ? ' ← 当前章节' : '' return `第${c.index}章 ${c.title || '(未命名)'}${mark}${c.synopsis ? `:${c.synopsis}` : ''}` }).join('\n') } /** 前情提要:最近 N 章给完整摘要,更早的章节只给梗概 */ export function formatPreviousSummaries(chapters: ChapterRow[], beforeIndex: number, recent: number) { const prev = chapters.filter(c => c.index < beforeIndex && (c.summary || c.synopsis)) const older = prev.slice(0, Math.max(0, prev.length - recent)) const near = prev.slice(-recent) const parts: string[] = [] if (older.length) parts.push(`更早章节梗概:\n${older.map(c => `- 第${c.index}章 ${c.title}:${truncate(c.summary || c.synopsis, 120)}`).join('\n')}`) if (near.length) parts.push(near.map(c => `### 第${c.index}章 ${c.title}\n${c.summary || c.synopsis}`).join('\n\n')) return parts.join('\n\n') } export function truncate(s: string, n: number) { return s.length > n ? `${s.slice(0, n)}…` : s } export function tail(s: string, n: number) { return s.length > n ? `…${s.slice(-n)}` : s } function storyBible(ctx: ProjectContext, scanText: string, opts: { fullCharacters: boolean }) { const { project, characters, lore } = ctx const chars = opts.fullCharacters ? characters : pickCharacters(characters, scanText) return join( section('story_title', project.title), section('world_setting', project.worldSetting), section('characters', chars.map(c => formatCharacter(c, true)).join('\n\n')), section('lore', formatLore(selectLore(lore, scanText))), section('story_outline', project.storyOutline), section('writing_requirements', formatRequirements(project.requirements)) ) } /* ============================================================ * 主编 Editor * ============================================================ */ export const EDITOR_BUILTIN = `你是一部长篇小说的主编,负责把控整部作品的走向、结构与质量。 你的职责: 1. 依据故事背景、人物设定和故事大纲,规划全书的章节结构,保证起承转合、节奏张弛有度、伏笔有埋有收。 2. 在每一章开写之前,为作家撰写一份清晰、可执行的「章节简报」:本章要完成的剧情目标、关键情节点(按顺序)、出场人物及其状态与动机、场景、情绪基调、节奏、文风要求、目标字数、需要埋下或回收的伏笔、结尾钩子。 3. 从作家团队中挑选最适合本章风格的作家(或在允许时创建新作家)来执行写作。 4. 你不亲自写正文。你看到的是前文的摘要而不是全文,请据此保证连续性。 工作原则:简报要具体,避免空泛的形容词;尊重既定设定和人物性格,不要擅自改动核心设定;输出使用简体中文(除非写作要求另有规定)。` export function buildEditorBriefPrompt(ctx: ProjectContext, chapter: ChapterRow, writers: AgentRow[], allowCreate: boolean) { const { project, chapters } = ctx const scan = `${chapter.title}\n${chapter.synopsis}\n${project.storyOutline}` const roster = writers.length ? writers.map((w) => { const model = ctx.models.find(m => m.id === w.modelId) return `- id=${w.id}|${w.name}|模型:${model ? `${model.providerName}/${model.label || model.modelId}` : '未配置'}${w.styleNotes ? `|擅长:${w.styleNotes}` : ''}` }).join('\n') : '(暂无作家)' const modelList = ctx.models.map(m => `- modelId=${m.id}|${m.providerName} / ${m.label || m.modelId}`).join('\n') const prompt = join( storyBible(ctx, scan, { fullCharacters: true }), section('chapter_plan', formatChapterPlan(chapters, chapter.index)), section('previous_chapters', formatPreviousSummaries(chapters, chapter.index, project.settings.recentSummaryCount)), section('story_memory', formatMemory(project.memory)), section('writers', roster), allowCreate ? section('available_models', modelList) : '', chapter.brief ? section('previous_brief_draft', chapter.brief) : '', section('task', [ `请为第${chapter.index}章${chapter.title ? `「${chapter.title}」` : ''}制定章节简报。`, chapter.synopsis ? `本章既定梗概:${chapter.synopsis}` : '本章尚无梗概,请根据大纲和前情自行确定本章内容。', '步骤:', '1. 调用 write_brief 工具提交章节简报(必须调用)。', chapter.writerLocked ? '2. 用户已为本章指定作家,无需再选择作家。' : allowCreate ? '2. 调用 assign_writer 从作家团队中选择一位作家;如果没有合适的作家,可以调用 create_writer 用 available_models 中的模型创建一位新作家(创建后自动指派)。' : '2. 调用 assign_writer 从作家团队中选择一位作家。', '3. 完成后用一两句话说明你的安排思路。' ].join('\n')) ) return prompt } export function buildEditorOutlinePrompt(ctx: ProjectContext, count: number, fromIndex: number) { const { project, chapters } = ctx return join( storyBible(ctx, project.storyOutline, { fullCharacters: true }), section('existing_chapter_plan', formatChapterPlan(chapters)), section('previous_chapters', formatPreviousSummaries(chapters, fromIndex, project.settings.recentSummaryCount)), section('story_memory', formatMemory(project.memory)), section('task', [ `请规划从第${fromIndex}章开始的 ${count} 个章节(第${fromIndex}章 ~ 第${fromIndex + count - 1}章)。`, '要求:每章给出标题和 80~200 字的梗概,梗概需写明本章的核心事件、冲突与转折、结尾落点;章节之间要有因果递进,节奏有张有弛。', '已确认(approved)或已写完的章节不要改动;之后的章节可以根据需要重新规划。', '请调用 set_chapter_plan 工具一次性提交规划结果,然后用一两句话概括你的整体设计思路。' ].join('\n')) ) } export function buildEditorReviewPrompt(ctx: ProjectContext, chapter: ChapterRow, text: string) { return join( section('writing_requirements', formatRequirements(ctx.project.requirements)), section('characters', pickCharacters(ctx.characters, text).map(c => formatCharacter(c, false)).join('\n\n')), section('story_memory', formatMemory(ctx.project.memory)), section('chapter_brief', chapter.brief), section('chapter_text', text), section('task', [ `请以主编身份审阅第${chapter.index}章正文。`, '从以下维度给出评价:简报完成度(逐条对照情节点)、人物是否 OOC、与设定/前文是否矛盾、文风与要求是否一致、节奏与可读性、语言问题。', '最后给出 1~10 分的总评分,并列出可以直接交给作家执行的修改意见(编号列表)。用 Markdown 输出。' ].join('\n')) ) } /* ============================================================ * 作家 Writer * ============================================================ */ export const WRITER_BUILTIN = `你是一位专业的小说作家,正在为一部长篇小说执笔其中一章。 你会收到故事设定、人物资料、前情提要和主编给出的「章节简报」。你的任务是把简报写成完整、生动、可直接发表的小说正文。 写作原则: - 严格完成简报中的全部情节点,按顺序推进,不要跳过,也不要擅自写到下一章的内容。 - 人物言行必须符合其性格与当前状态;对话要有个性。 - 多用具体的动作、细节、感官描写和对白来展现,少用概括性的叙述。 - 与上一章结尾自然衔接,不要重复上一章已经写过的内容。 - 只输出小说正文本身:不要输出章节标题、不要写“好的”“以下是”等说明,不要在结尾加总结或注释。` export function buildWriterPrompt(ctx: ProjectContext, chapter: ChapterRow, prevTail: string, feedback: string, previousDraft: string) { const { project, chapters } = ctx const scan = `${chapter.brief}\n${chapter.title}\n${chapter.synopsis}` const plan = chapters.filter(c => Math.abs(c.index - chapter.index) <= 2) return join( storyBible(ctx, scan, { fullCharacters: false }), section('nearby_chapter_plan', formatChapterPlan(plan, chapter.index)), section('previous_chapters', formatPreviousSummaries(chapters, chapter.index, project.settings.recentSummaryCount)), section('story_memory', formatMemory(project.memory)), section('previous_chapter_ending', prevTail), section('chapter_brief', `第${chapter.index}章 ${chapter.title}\n\n${chapter.brief || chapter.synopsis}`), previousDraft ? section('previous_draft', previousDraft) : '', feedback ? section('revision_feedback', feedback) : '', section('task', previousDraft ? `请根据 revision_feedback 修改 previous_draft,输出修改后的第${chapter.index}章完整正文。` : `请开始写第${chapter.index}章正文,目标约 ${project.requirements.wordsPerChapter} 字。`) ) } export function writerExtra(r: ProjectRequirements) { const req = formatRequirements(r) return req ? `写作要求:\n${req}` : '' } /* ============================================================ * 摘要员 Summarizer * ============================================================ */ export const SUMMARIZER_BUILTIN = `你是小说编辑部的资料员,负责在每章定稿后整理“故事记忆”,供主编和后续作家保持连贯。 你只输出一个 JSON 对象,不要输出任何其他文字或 Markdown 代码块标记。` export function buildSummaryPrompt(ctx: ProjectContext, chapter: ChapterRow, text: string) { return join( section('character_names', ctx.characters.map(c => c.name).join('、')), section('story_memory_before', formatMemory(ctx.project.memory)), section('chapter_text', `第${chapter.index}章 ${chapter.title}\n\n${text}`), section('task', `请阅读本章正文,输出如下 JSON: { "summary": "本章剧情摘要,300~500 字,按时间顺序写清楚发生了什么、人物做出了什么选择、关系和局势有什么变化、结尾停在哪里", "characterStates": [{ "name": "人物名", "state": "该人物在本章结束时的处境、情绪、位置、身体状况、关键持有物等(只列本章有变化的人物)" }], "newForeshadowing": ["本章新埋下的伏笔或悬念"], "resolvedForeshadowing": ["本章回收/揭晓的伏笔(尽量引用 story_memory_before 中的原文)"], "events": ["本章关键事件,一句话一条,最多 5 条"] }`) ) }