import type { CharacterChatReplyRequest, CharacterChatSuggestionsRequest, CharacterChatSummaryRequest, NpcChatDialogueRequest, NpcChatTurnRequest, NpcRecruitDialogueRequest, } from '../../../../packages/shared/src/contracts/story.js'; type JsonRecord = Record; export const CHARACTER_PANEL_CHAT_SYSTEM_PROMPT = `你是像素动作 RPG 里的同行角色。 只回复这名角色此刻会对玩家说的话。 不要输出角色名、引号、旁白、动作提示、Markdown、JSON 或解释。 保持人设,结合最近剧情和关系变化,回复简洁自然。`; export const CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT = `生成恰好 3 条玩家回复建议。 只输出纯文本,共 3 行,每行一条。 不要加编号、项目符号、Markdown 或额外说明。 三条建议语气要有区分:关心、追问、轻松或拉近关系。`; export const CHARACTER_PANEL_CHAT_SUMMARY_SYSTEM_PROMPT = `总结玩家与这名角色之间不断变化的关系。 只输出一段简洁文字。 包含当前关系气氛、态度变化,以及最近聊天里最重要的新信息、承诺、担忧或线索。`; export const NPC_CHAT_DIALOGUE_STRICT_SYSTEM_PROMPT = `你是角色扮演 RPG 的角色对话编剧。 你只能输出纯中文对话正文,不能输出解释、代码、markdown、JSON 或额外说明。 硬性规则: - 每一行都必须严格以“你:”或“角色名字:”开头。 - 第一行必须是“你:”开头。 - 总行数控制在 4 到 6 行。 - 玩家和对方至少各说 2 次。 - 这段内容只是聊天,不是做决定。 - 禁止在聊天里主动引导、建议、安排或预告交易、招募、切磋、战斗、送礼、求助、离开、继续前进、切换场景等其他 function。 - 禁止把情报直接写成对玩家的指令。 - 结束时要让玩家感觉到气氛、情报或关系发生了变化,但变化仍停留在聊天层面。`; export const NPC_RECRUIT_DIALOGUE_SYSTEM_PROMPT = `你是角色扮演 RPG 的招募剧情对话编剧。 你只能输出纯中文对话正文,不能输出解释、代码、markdown、JSON 或额外说明。 硬性规则: - 每一行都必须严格以“你:”或“角色名字:”开头。 - 第一行必须是“你:”开头。 - 总行数控制在 4 到 6 行。 - 玩家和对方至少各说 2 次。 - 这段对话的目标是把“邀请对方入队”自然谈成。 - 不允许出现拒绝入队、继续观望、以后再说、条件未满足等结果。 - 不允许出现“我不能答应”“我还没想好”“再让我考虑”“暂时不行”“以后再说”这类拒绝或拖延表述。 - 最后一行必须由对方明确答应加入队伍。`; export const NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT = `你是角色扮演 RPG 里的当前 NPC。 你只输出这名 NPC 此刻会对玩家说的一轮回复。 只输出纯中文口语回复正文,不要输出角色名、引号、旁白、动作描写、Markdown、JSON 或解释。 回复长度控制在 1 到 3 句,必须紧接玩家刚说的话,自然推进气氛、情报或关系。`; export const NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT = `你要为玩家生成下一轮可直接点击的 3 条聊天续写候选。 只输出纯文本,共 3 行,每行 1 条。 不要加编号、项目符号、Markdown、JSON 或额外说明。 三条候选必须明显不同,分别体现继续追问、表达态度、轻微拉近关系这三种不同方向。`; function asRecord(value: unknown): JsonRecord | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonRecord) : null; } function readString(value: unknown) { return typeof value === 'string' && value.trim() ? value.trim() : null; } function readNumber(value: unknown, fallback = 0) { return typeof value === 'number' && Number.isFinite(value) ? value : fallback; } function readStringArray(value: unknown) { return Array.isArray(value) ? value .map((item) => readString(item)) .filter((item): item is string => Boolean(item)) : []; } function describeWorld(worldType: string) { switch (worldType) { case 'WUXIA': return '边城模板'; case 'XIANXIA': return '灵潮模板'; case 'CUSTOM': return '自定义世界'; default: return worldType || '未知世界'; } } function describeStats(label: string, record: JsonRecord | null) { const hp = readNumber(record?.hp); const maxHp = Math.max(1, readNumber(record?.maxHp, hp)); const mana = readNumber(record?.mana); const maxMana = Math.max(1, readNumber(record?.maxMana, mana)); return `${label}生命 ${hp}/${maxHp},灵力 ${mana}/${maxMana}`; } function describeCharacter(label: string, value: unknown) { const record = asRecord(value); const name = readString(record?.name) ?? '未知角色'; const title = readString(record?.title) ?? '未知称号'; const description = readString(record?.description) ?? '暂无额外描述'; const personality = readString(record?.personality) ?? '性格信息未显式提供'; return [ `${label}姓名:${name}`, `${label}称号:${title}`, `${label}描述:${description}`, `${label}性格:${personality}`, ].join('\n'); } function describeStoryHistory(history: unknown) { if (!Array.isArray(history) || history.length === 0) { return '近期剧情:暂无。'; } const lines = history .slice(-4) .map((item) => readString(asRecord(item)?.text)) .filter((item): item is string => Boolean(item)); return lines.length > 0 ? ['近期剧情:', ...lines.map((line) => `- ${line}`)].join('\n') : '近期剧情:暂无。'; } function describeConversationHistory(history: unknown) { if (!Array.isArray(history) || history.length === 0) { return '聊天记录:暂无。'; } const lines = history .slice(-12) .map((item) => { const record = asRecord(item); const speaker = readString(record?.speaker) === 'player' ? '玩家' : '角色'; const text = readString(record?.text); return text ? `- ${speaker}:${text}` : null; }) .filter((item): item is string => Boolean(item)); return lines.length > 0 ? ['聊天记录:', ...lines].join('\n') : '聊天记录:暂无。'; } function describeNpcConversationHistory(history: unknown, npcName: string) { if (!Array.isArray(history) || history.length === 0) { return '当前聊天记录:暂无。'; } const lines = history .slice(-10) .map((item) => { const record = asRecord(item); const speaker = readString(record?.speaker); const speakerName = readString(record?.speakerName); const text = readString(record?.text); if (!text) return null; if (speaker === 'player') { return `- 玩家:${text}`; } if (speaker === 'npc') { return `- ${speakerName ?? npcName}:${text}`; } if (speaker === 'system') { return `- 系统提示:${text}`; } return `- ${speakerName ?? '同伴'}:${text}`; }) .filter((item): item is string => Boolean(item)); return lines.length > 0 ? ['当前聊天记录:', ...lines].join('\n') : '当前聊天记录:暂无。'; } function describeSceneContext(context: unknown) { const record = asRecord(context); const sceneName = readString(record?.sceneName) ?? '当前区域'; const sceneDescription = readString(record?.sceneDescription) ?? '周围气氛仍未完全安定。'; const inBattle = record?.inBattle === true ? '战斗中' : '非战斗'; const customWorldProfile = asRecord(record?.customWorldProfile); const customWorldName = readString(customWorldProfile?.name); const customWorldSummary = readString(customWorldProfile?.summary); return [ `世界补充:${customWorldName ?? '无'}`, customWorldSummary ? `世界摘要:${customWorldSummary}` : null, `场景:${sceneName}`, `场景描述:${sceneDescription}`, `当前状态:${inBattle}`, describeStats('玩家', record), ] .filter(Boolean) .join('\n'); } function describeTargetStatus(status: unknown) { const record = asRecord(status); const roleLabel = readString(record?.roleLabel) ?? '同行角色'; const affinity = record?.affinity; return [ `对方身份:${roleLabel}`, describeStats('对方', record), typeof affinity === 'number' ? `当前好感:${affinity}` : null, ] .filter(Boolean) .join('\n'); } function describeEncounter(encounter: unknown) { const record = asRecord(encounter); const npcName = readString(record?.npcName) ?? '眼前角色'; const contextText = readString(record?.context) ?? readString(record?.npcDescription) ?? '你们正在当前遭遇里继续对话。'; return { npcName, block: [`当前对象:${npcName}`, `对象背景:${contextText}`].join('\n'), }; } function describeMonsters(monsters: unknown) { if (!Array.isArray(monsters) || monsters.length === 0) { return '当前敌对目标:无。'; } const lines = monsters .slice(0, 4) .map((item) => { const record = asRecord(item); const name = readString(record?.name) ?? readString(record?.npcName) ?? readString(record?.id); const hp = readNumber(record?.hp); const maxHp = Math.max(1, readNumber(record?.maxHp, hp)); return name ? `- ${name}(生命 ${hp}/${maxHp})` : null; }) .filter((item): item is string => Boolean(item)); return lines.length > 0 ? ['当前敌对目标:', ...lines].join('\n') : '当前敌对目标:无。'; } function describeTargetCharacterName(payload: { targetCharacter?: unknown; encounter?: unknown; }) { return ( readString(asRecord(payload.targetCharacter)?.name) ?? readString(asRecord(payload.encounter)?.npcName) ?? '对方' ); } export function buildCharacterPanelChatPrompt( payload: CharacterChatReplyRequest, ) { const targetName = describeTargetCharacterName(payload); return [ `世界:${describeWorld(payload.worldType)}`, describeSceneContext(payload.context), describeCharacter('玩家 / ', payload.playerCharacter), describeCharacter('对方 / ', payload.targetCharacter), describeTargetStatus(payload.targetStatus), describeStoryHistory(payload.storyHistory), payload.conversationSummary ? `之前聊天摘要:${payload.conversationSummary}` : '之前聊天摘要:暂无。', describeConversationHistory(payload.conversationHistory), `玩家刚刚对 ${targetName} 说:${payload.playerMessage}`, `现在请以 ${targetName} 的身份,直接回复玩家。`, ] .filter(Boolean) .join('\n\n'); } export function buildCharacterPanelChatSuggestionPrompt( payload: CharacterChatSuggestionsRequest, ) { const targetName = describeTargetCharacterName(payload); const latestCharacterReply = Array.isArray(payload.conversationHistory) ? [...payload.conversationHistory] .reverse() .map((item) => asRecord(item)) .find((record) => readString(record?.speaker) === 'character') : null; const latestReplyText = readString(latestCharacterReply?.text); return [ `世界:${describeWorld(payload.worldType)}`, describeSceneContext(payload.context), describeCharacter('玩家 / ', payload.playerCharacter), describeCharacter('对方 / ', payload.targetCharacter), describeTargetStatus(payload.targetStatus), describeStoryHistory(payload.storyHistory), payload.conversationSummary ? `之前聊天摘要:${payload.conversationSummary}` : '之前聊天摘要:暂无。', describeConversationHistory(payload.conversationHistory), latestReplyText ? `角色刚刚的回复:${latestReplyText}` : `玩家正准备与 ${targetName} 开始一段新的私聊。`, `请围绕当前气氛,为玩家生成 3 条可以直接发送给 ${targetName} 的简短回复候选。`, ] .filter(Boolean) .join('\n\n'); } export function buildCharacterPanelChatSummaryPrompt( payload: CharacterChatSummaryRequest, ) { const targetName = describeTargetCharacterName(payload); return [ `世界:${describeWorld(payload.worldType)}`, describeSceneContext(payload.context), describeCharacter('玩家 / ', payload.playerCharacter), describeCharacter('对方 / ', payload.targetCharacter), describeTargetStatus(payload.targetStatus), describeStoryHistory(payload.storyHistory), payload.previousSummary ? `旧摘要:${payload.previousSummary}` : '旧摘要:暂无。', describeConversationHistory(payload.conversationHistory), `请把玩家与 ${targetName} 的旧摘要和最新聊天整理成一段更新后的关系摘要。`, ] .filter(Boolean) .join('\n\n'); } function buildNpcDialoguePromptBase( payload: NpcChatDialogueRequest | NpcChatTurnRequest | NpcRecruitDialogueRequest, ) { const encounter = describeEncounter(payload.encounter); return [ `世界:${describeWorld(payload.worldType)}`, describeSceneContext(payload.context), describeCharacter('玩家 / ', payload.character), encounter.block, describeMonsters(payload.monsters), describeStoryHistory(payload.history), ] .filter(Boolean) .join('\n\n'); } export function buildStrictNpcChatDialoguePrompt( payload: NpcChatDialogueRequest, ) { const encounter = describeEncounter(payload.encounter); const context = asRecord(payload.context); const openingCampBackground = readString(context?.openingCampBackground); const openingCampDialogue = readString(context?.openingCampDialogue); const allowedTopics = readStringArray(context?.encounterAllowedTopics); const blockedTopics = readStringArray(context?.encounterBlockedTopics); return [ buildNpcDialoguePromptBase(payload), openingCampBackground ? `营地开场背景:${openingCampBackground}` : null, openingCampDialogue ? `刚刚发生的第一段对话:${openingCampDialogue}` : null, allowedTopics.length > 0 ? `当前更适合谈的内容:${allowedTopics.join('、')}` : null, blockedTopics.length > 0 ? `当前避免直接说破:${blockedTopics.join('、')}` : null, `当前聊天主题:${payload.topic}`, payload.resultSummary ? `这段聊天希望带来的变化:${payload.resultSummary}` : '这段聊天要让气氛、情报或关系出现一层新的变化。', `请围绕“${payload.topic}”写一段刚刚发生的对话。必须只输出对白正文,每一行都必须以“你:”或“${encounter.npcName}:”开头。`, ] .filter(Boolean) .join('\n\n'); } export function buildNpcRecruitDialoguePrompt( payload: NpcRecruitDialogueRequest, ) { const encounter = describeEncounter(payload.encounter); return [ buildNpcDialoguePromptBase(payload), `玩家邀请:${payload.invitationText}`, payload.recruitSummary ? `招募补充条件:${payload.recruitSummary}` : '这轮对话已经具备自然邀请对方入队的条件。', '这是一段“邀请对方入队”的对话。请让几轮交流逐步导向成功加入队伍,不要写出拒绝、观望或延期答复。', `最后一行必须由 ${encounter.npcName} 明确答应加入队伍。`, ] .filter(Boolean) .join('\n\n'); } export function buildNpcChatTurnReplyPrompt( payload: NpcChatTurnRequest, ) { const encounter = describeEncounter(payload.encounter); const npcState = asRecord(payload.npcState); const affinity = readNumber(npcState?.affinity, 0); const chattedCount = readNumber(npcState?.chattedCount, 0); return [ buildNpcDialoguePromptBase(payload), describeNpcConversationHistory(payload.conversationHistory, encounter.npcName), `当前关系值:${affinity}`, `已聊天轮次:${chattedCount}`, `玩家刚刚说:${payload.playerMessage}`, `现在请只写 ${encounter.npcName} 这一轮会回复玩家的话。`, ] .filter(Boolean) .join('\n\n'); } export function buildNpcChatTurnSuggestionPrompt( payload: NpcChatTurnRequest, npcReply: string, ) { const encounter = describeEncounter(payload.encounter); return [ buildNpcDialoguePromptBase(payload), describeNpcConversationHistory(payload.conversationHistory, encounter.npcName), `玩家刚刚说:${payload.playerMessage}`, `NPC 刚刚回复:${npcReply}`, `请围绕刚刚这轮对话,为玩家生成 3 条可以继续和 ${encounter.npcName} 聊下去的中文短句候选。`, ] .filter(Boolean) .join('\n\n'); }