Files
Genarrative/src/components/visual-novel-runtime/visualNovelLocalPreviewData.ts
T
kdletters deea0e8655 扩展宿主调用链扫描
自动发现 H5 HostBridge 真实能力调用链

补齐 runtime 回读与 wrapper 消费者扫描兜底

区分壳源码和 H5 业务调用链的禁替身词表

将视觉小说生产预览数据从 mock 命名收口为本地预览命名

更新原生壳验收文档和共享决策记录
2026-06-19 02:45:56 +08:00

456 lines
14 KiB
TypeScript

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,
};