import type { VisualNovelAgentSessionSnapshot, VisualNovelResultDraft, VisualNovelRunSnapshot, VisualNovelSourceMode, } from '../../../packages/shared/src/contracts/visualNovel'; const LOCAL_PREVIEW_NOW = '2026-05-05T12:00:00.000Z'; // 中文注释:VN-04 只提供 UI 骨架,本地预览数据必须保持在前端组件域内,避免被误用成业务真相源。 export const visualNovelLocalPreviewDraft: VisualNovelResultDraft = { profileId: 'vn-profile-preview-1', workTitle: '雪线电台', workDescription: '一列停在雪夜边境的列车,牵出旧电台、失踪乘客和未寄出的告白。', workTags: ['悬疑', '冬夜', '双人叙事'], coverImageSrc: null, sourceMode: 'idea', sourceAssetIds: [], world: { title: '北境终点线', summary: '终年落雪的边境小城依靠一座旧电台维系列车、灯塔和远方来信。', background: '十二年前的雪崩掩埋了山间支线,也留下无法解释的夜间广播。现在,最后一班列车重新驶入废弃站台。', premise: '玩家需要在日出前找出列车停摆的原因,并决定是否公开电台里的旧录音。', literaryStyle: '克制、冷光感、对白带有细微试探。', playerRole: '临时接任的列车广播员', defaultTone: '安静、紧张、带一点温柔', }, characters: [ { characterId: 'vn-char-lin-yao', name: '林遥', gender: '女', role: 'main', appearance: '深灰长外套,围巾边缘有细小电台徽章。', personality: '谨慎、敏锐,习惯先观察再回答。', tone: '短句多,情绪压得很低。', background: '曾在旧电台做夜班实习生,熟悉失踪列车的第一份报案。', relationshipToPlayer: '玩家的临时搭档', imageAssets: [], defaultExpression: 'calm', isPlayerVisible: false, }, { characterId: 'vn-char-he-shen', name: '赫慎', gender: '男', role: 'supporting', appearance: '车长制服整洁,袖口有被火星燎过的旧痕。', personality: '礼貌、固执,对列车时刻表近乎偏执。', tone: '正式、慢速,偶尔忽然沉默。', background: '事故后仍守着废弃站台的人。', relationshipToPlayer: '提供线索,也隐瞒关键事故记录', imageAssets: [], defaultExpression: 'reserved', isPlayerVisible: false, }, ], scenes: [ { sceneId: 'vn-scene-platform', name: '风雪站台', description: '站灯忽明忽暗,远处铁轨被雪压出发亮的线。', backgroundImageSrc: null, musicSrc: null, ambientSoundSrc: null, availability: 'opening', phaseIds: ['vn-phase-1'], }, { sceneId: 'vn-scene-radio-room', name: '旧电台室', description: '木桌上堆着线圈、胶带和没来得及寄出的明信片。', backgroundImageSrc: null, musicSrc: null, ambientSoundSrc: null, availability: 'phase_locked', phaseIds: ['vn-phase-2'], }, ], storyPhases: [ { phaseId: 'vn-phase-1', title: '重启站台', goal: '确认列车为何停在废弃站台。', summary: '玩家抵达风雪站台,第一次听见旧电台播出自己的名字。', entryCondition: '开场进入', exitCondition: '找到车长日志或解开广播频段', sceneIds: ['vn-scene-platform'], characterIds: ['vn-char-lin-yao', 'vn-char-he-shen'], suggestedChoices: ['询问林遥旧电台的来历', '检查站台广播柜', '追问车长日志'], }, { phaseId: 'vn-phase-2', title: '未寄出的录音', goal: '判断旧录音是否可信。', summary: '旧电台室中出现十二年前的夜班录音,内容和现实开始互相抵触。', entryCondition: '完成站台调查', exitCondition: '选择公开或隐藏录音', sceneIds: ['vn-scene-radio-room'], characterIds: ['vn-char-lin-yao'], suggestedChoices: ['倒回录音最后十秒', '让林遥辨认声音', '检查明信片落款'], }, ], opening: { sceneId: 'vn-scene-platform', narration: '雪落得很慢,像有人把整座车站的时间调低了一格。', speakerCharacterId: 'vn-char-lin-yao', firstDialogue: '你听见了吗?电台刚才念出了你的名字。', initialChoices: [ { choiceId: 'vn-choice-radio', text: '靠近广播柜,确认频段来源。', actionHint: 'investigate_radio', }, { choiceId: 'vn-choice-lin', text: '先问林遥为什么认识这段广播。', actionHint: 'ask_lin_yao', }, ], }, runtimeConfig: { textModeEnabled: true, defaultTextMode: false, maxHistoryEntries: 80, maxAssistantStepCountPerTurn: 8, allowFreeTextAction: true, allowHistoryRegeneration: true, attributePanelMode: 'template_config', saveArchiveEnabled: true, }, publishReady: false, validationIssues: [ { issueId: 'vn-issue-cover', code: 'MISSING_COVER_IMAGE', severity: 'warning', path: 'coverImageSrc', message: '封面待补齐。', }, ], updatedAt: LOCAL_PREVIEW_NOW, }; export const visualNovelLocalPreviewSession: VisualNovelAgentSessionSnapshot = { sessionId: 'vn-session-preview-1', ownerUserId: 'local-preview-user', sourceMode: 'idea', status: 'ready', messages: [ { id: 'vn-message-1', role: 'user', kind: 'chat', text: '想做一个雪夜列车和旧电台有关的悬疑视觉小说。', createdAt: LOCAL_PREVIEW_NOW, }, { id: 'vn-message-2', role: 'assistant', kind: 'summary', text: '底稿已整理到世界观、角色、场景和前两段剧情阶段。', createdAt: LOCAL_PREVIEW_NOW, }, ], draft: visualNovelLocalPreviewDraft, pendingAction: { actionId: 'vn-action-compile-preview', kind: 'compile_work_profile', label: '生成结果页', }, createdAt: LOCAL_PREVIEW_NOW, updatedAt: LOCAL_PREVIEW_NOW, }; function createVisualNovelDraftId(prefix: string) { return `${prefix}-${Date.now()}-${Math.round(Math.random() * 1_000_000)}`; } export function createBlankVisualNovelDraft( sourceMode: VisualNovelSourceMode = 'blank', ): VisualNovelResultDraft { const draftId = createVisualNovelDraftId('vn-profile-local'); const sceneId = `${draftId}-scene-opening`; const characterId = `${draftId}-char-main`; const phaseId = `${draftId}-phase-opening`; return { profileId: draftId, workTitle: '未命名视觉小说', workDescription: '', workTags: [], coverImageSrc: null, sourceMode, sourceAssetIds: [], world: { title: '', summary: '', background: '', premise: '', literaryStyle: '', playerRole: '', defaultTone: '', }, characters: [ { characterId, name: '', gender: null, role: 'main', appearance: '', personality: '', tone: '', background: '', relationshipToPlayer: '', imageAssets: [], defaultExpression: null, isPlayerVisible: false, }, ], scenes: [ { sceneId, name: '', description: '', backgroundImageSrc: null, musicSrc: null, ambientSoundSrc: null, availability: 'opening', phaseIds: [phaseId], }, ], storyPhases: [ { phaseId, title: '', goal: '', summary: '', entryCondition: '开场进入', exitCondition: '', sceneIds: [sceneId], characterIds: [characterId], suggestedChoices: ['', ''], }, ], opening: { sceneId, narration: '', speakerCharacterId: characterId, firstDialogue: '', initialChoices: [ { choiceId: `${draftId}-choice-1`, text: '', actionHint: null, }, { choiceId: `${draftId}-choice-2`, text: '', actionHint: null, }, ], }, runtimeConfig: { textModeEnabled: true, defaultTextMode: false, maxHistoryEntries: 80, maxAssistantStepCountPerTurn: 8, allowFreeTextAction: true, allowHistoryRegeneration: true, attributePanelMode: 'off', saveArchiveEnabled: true, }, publishReady: false, validationIssues: [], updatedAt: new Date().toISOString(), }; } export function createLocalPreviewVisualNovelSessionFromDraft( draft: VisualNovelResultDraft, ): VisualNovelAgentSessionSnapshot { const now = new Date().toISOString(); return { sessionId: createVisualNovelDraftId('vn-session-local'), ownerUserId: 'local-user', sourceMode: draft.sourceMode, status: 'ready', messages: [ { id: createVisualNovelDraftId('vn-message-local'), role: 'assistant', kind: 'summary', text: '已创建可编辑底稿。', createdAt: now, }, ], draft, pendingAction: { actionId: createVisualNovelDraftId('vn-action-local'), kind: 'compile_work_profile', label: '保存草稿', }, createdAt: now, updatedAt: now, }; } export function createLocalPreviewVisualNovelRunFromDraft( draft: VisualNovelResultDraft, ): VisualNovelRunSnapshot { const now = new Date().toISOString(); const openingSceneId = draft.opening.sceneId ?? draft.scenes[0]?.sceneId ?? null; const firstPhase = draft.storyPhases.find((phase) => openingSceneId ? phase.sceneIds.includes(openingSceneId) : false, ); const speaker = draft.opening.speakerCharacterId ? draft.characters.find( (character) => character.characterId === draft.opening.speakerCharacterId, ) : null; return { runId: createVisualNovelDraftId('vn-run-local'), ownerUserId: 'local-user', profileId: draft.profileId ?? createVisualNovelDraftId('vn-profile-local'), mode: 'test', status: 'active', currentSceneId: openingSceneId, currentPhaseId: firstPhase?.phaseId ?? draft.storyPhases[0]?.phaseId ?? null, visibleCharacterIds: draft.characters .filter((character) => character.role !== 'protagonist') .map((character) => character.characterId) .slice(0, 2), flags: {}, metrics: {}, history: [ { entryId: createVisualNovelDraftId('vn-history-local'), runId: 'local-test-run', turnIndex: 1, source: 'assistant', actionText: null, steps: [ ...(openingSceneId ? [ { type: 'scene_change' as const, sceneId: openingSceneId, backgroundImageSrc: draft.scenes.find((scene) => scene.sceneId === openingSceneId) ?.backgroundImageSrc ?? null, musicSrc: draft.scenes.find((scene) => scene.sceneId === openingSceneId) ?.musicSrc ?? null, }, ] : []), { type: 'narration' as const, text: draft.opening.narration || draft.workDescription || draft.world.summary, }, ...(speaker && draft.opening.firstDialogue ? [ { type: 'dialogue' as const, characterId: speaker.characterId, characterName: speaker.name, expression: speaker.defaultExpression, text: draft.opening.firstDialogue, }, ] : []), ], snapshotBeforeHash: null, snapshotAfterHash: 'local-test-snapshot', createdAt: now, }, ], availableChoices: draft.opening.initialChoices.filter((choice) => choice.text.trim(), ), textModeEnabled: draft.runtimeConfig.defaultTextMode, createdAt: now, updatedAt: now, }; } export const visualNovelLocalPreviewRun: VisualNovelRunSnapshot = { runId: 'vn-run-preview-1', ownerUserId: 'local-preview-user', profileId: 'vn-profile-preview-1', mode: 'test', status: 'active', currentSceneId: 'vn-scene-platform', currentPhaseId: 'vn-phase-1', visibleCharacterIds: ['vn-char-lin-yao', 'vn-char-he-shen'], flags: { radioUnlocked: false, conductorTrust: 1, }, metrics: { tension: 42, trust: 18, }, history: [ { entryId: 'vn-history-1', runId: 'vn-run-preview-1', turnIndex: 1, source: 'assistant', actionText: null, steps: [ { type: 'scene_change', sceneId: 'vn-scene-platform', backgroundImageSrc: null, musicSrc: null, }, { type: 'narration', text: '雪落得很慢,站台灯把铁轨照成两道苍白的线。', }, { type: 'dialogue', characterId: 'vn-char-lin-yao', characterName: '林遥', expression: 'calm', text: '你听见了吗?电台刚才念出了你的名字。', }, ], snapshotBeforeHash: null, snapshotAfterHash: 'preview-hash-1', createdAt: LOCAL_PREVIEW_NOW, }, { entryId: 'vn-history-2', runId: 'vn-run-preview-1', turnIndex: 2, source: 'player', actionText: '靠近广播柜,确认频段来源,同时留意林遥是否在隐瞒什么。', steps: [], snapshotBeforeHash: 'preview-hash-1', snapshotAfterHash: 'preview-hash-2', createdAt: LOCAL_PREVIEW_NOW, }, ], availableChoices: visualNovelLocalPreviewDraft.opening.initialChoices, textModeEnabled: true, createdAt: LOCAL_PREVIEW_NOW, updatedAt: LOCAL_PREVIEW_NOW, };