deea0e8655
自动发现 H5 HostBridge 真实能力调用链 补齐 runtime 回读与 wrapper 消费者扫描兜底 区分壳源码和 H5 业务调用链的禁替身词表 将视觉小说生产预览数据从 mock 命名收口为本地预览命名 更新原生壳验收文档和共享决策记录
456 lines
14 KiB
TypeScript
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,
|
|
};
|