import type { CustomWorldRoleAssetSummary, CustomWorldSceneAssetSummary, } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import { getRoleAssetSummaryById, rebuildRoleAssetCoverage, mergeRoleAssetIntoDraftProfile, } from './customWorldAgentRoleAssetStateService.js'; function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function toRecord(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null; } function toRecordArray(value: unknown) { return Array.isArray(value) ? value.filter( (item): item is Record => Boolean(item) && typeof item === 'object' && !Array.isArray(item), ) : []; } type SyncRoleAssetsPayload = { roleId: string; portraitPath: string; generatedVisualAssetId: string; generatedAnimationSetId?: string | null; animationMap?: Record | null; }; type SceneKind = 'camp' | 'landmark'; type SyncSceneAssetsPayload = { sceneId: string; sceneKind: SceneKind; imageSrc: string; generatedSceneAssetId: string; generatedScenePrompt?: string | null; generatedSceneModel?: string | null; }; export type SyncRoleAssetsResult = { roleId: string; updatedRole: Record; updatedAssetSummary: CustomWorldRoleAssetSummary; draftProfile: Record; }; export type SceneAssetStudioContext = { sceneId: string; sceneKind: SceneKind; sceneName: string; sceneDescription: string; imageSrc: string | null; readyActCount: number; missingActCount: number; }; export type SyncSceneAssetsResult = { sceneId: string; sceneKind: SceneKind; updatedScene: Record; updatedAssetSummaries: CustomWorldSceneAssetSummary[]; draftProfile: Record; }; function cloneRecord>(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } function toSceneDescription(scene: Record, sceneKind: SceneKind) { if (sceneKind === 'camp') { return ( toText(scene.description) || toText(scene.summary) || toText(scene.mood) ); } return ( toText(scene.description) || toText(scene.summary) || toText(scene.purpose) || toText(scene.mood) ); } function findSceneActsBySceneId( draftProfile: Record, sceneId: string, ) { return toRecordArray(draftProfile.sceneChapters) .filter((chapter) => toText(chapter.sceneId) === sceneId) .flatMap((chapter) => toRecordArray(chapter.acts)); } function updateSceneChapterActsForScene(params: { draftProfile: Record; sceneId: string; imageSrc: string; generatedSceneAssetId: string; }) { return toRecordArray(params.draftProfile.sceneChapters).map((chapter) => { if (toText(chapter.sceneId) !== params.sceneId) { return chapter; } return { ...chapter, acts: toRecordArray(chapter.acts).map((act) => ({ ...act, backgroundImageSrc: params.imageSrc, backgroundAssetId: params.generatedSceneAssetId, })), } satisfies Record; }); } function buildSceneAssetFallbackSummary(params: { sceneId: string; sceneKind: SceneKind; updatedScene: Record; imageSrc: string; generatedSceneAssetId: string; }) { return { sceneId: params.sceneId, sceneName: toText(params.updatedScene.name) || (params.sceneKind === 'camp' ? '开局营地' : '未命名场景'), actId: null, actTitle: params.sceneKind === 'camp' ? '营地正式背景图' : '场景正式背景图', imageSrc: params.imageSrc, assetId: params.generatedSceneAssetId, status: 'ready', nextPointCost: 0, } satisfies CustomWorldSceneAssetSummary; } export class CustomWorldAgentAssetBridgeService { buildRoleAssetStudioContext(snapshot: unknown, roleId: string) { const profile = toRecord(snapshot); if (!profile) { throw new Error('当前世界草稿为空,无法打开角色资产工坊。'); } const playableRole = toRecordArray(profile.playableNpcs).find( (item) => toText(item.id) === roleId, ); const storyRole = toRecordArray(profile.storyNpcs).find( (item) => toText(item.id) === roleId, ); const role = playableRole ?? storyRole; if (!role) { throw new Error('未找到目标角色,无法进入角色资产工坊。'); } const assetSummary = getRoleAssetSummaryById(profile, roleId); if (!assetSummary) { throw new Error('未找到目标角色的资产摘要。'); } return { roleId, roleName: toText(role.name) || assetSummary.roleName, roleKind: playableRole ? ('playable' as const) : ('story' as const), startFrom: assetSummary.status === 'missing' ? ('visual' as const) : ('animation' as const), assetSummary, }; } applyRoleAssetPublishResult( snapshot: unknown, payload: SyncRoleAssetsPayload, ): SyncRoleAssetsResult { const profile = toRecord(snapshot); if (!profile) { throw new Error('当前世界草稿为空,无法同步角色资产。'); } const { draftProfile, updatedRole } = mergeRoleAssetIntoDraftProfile( profile, payload, ); const assetSummary = getRoleAssetSummaryById(draftProfile, payload.roleId); if (!assetSummary) { throw new Error('角色资产同步后未能生成新的资产摘要。'); } return { roleId: payload.roleId, updatedRole, updatedAssetSummary: assetSummary, draftProfile, }; } buildSceneAssetStudioContext( snapshot: unknown, sceneId: string, sceneKind: SceneKind, ): SceneAssetStudioContext { const profile = toRecord(snapshot); if (!profile) { throw new Error('当前世界草稿为空,无法打开场景资产工坊。'); } const scene = sceneKind === 'camp' ? toRecord(profile.camp) : toRecordArray(profile.landmarks).find( (item) => toText(item.id) === sceneId, ) ?? null; if (!scene) { throw new Error('未找到目标场景,无法进入场景资产工坊。'); } const sceneActs = findSceneActsBySceneId(profile, sceneId); const readyActCount = sceneActs.filter((act) => Boolean(toText(act.backgroundImageSrc) || toText(act.backgroundAssetId)), ).length; return { sceneId, sceneKind, sceneName: toText(scene.name) || (sceneKind === 'camp' ? '开局营地' : '未命名场景'), sceneDescription: toSceneDescription(scene, sceneKind), imageSrc: toText(scene.imageSrc) || null, readyActCount, missingActCount: Math.max(0, sceneActs.length - readyActCount), }; } applySceneAssetPublishResult( snapshot: unknown, payload: SyncSceneAssetsPayload, ): SyncSceneAssetsResult { const profile = toRecord(snapshot); if (!profile) { throw new Error('当前世界草稿为空,无法同步场景资产。'); } const nextDraftProfile = cloneRecord(profile); let updatedScene: Record | null = null; if (payload.sceneKind === 'camp') { const currentCamp = toRecord(nextDraftProfile.camp); if (!currentCamp || toText(currentCamp.id) !== payload.sceneId) { throw new Error('目标营地不存在,无法同步场景资产。'); } updatedScene = { ...currentCamp, imageSrc: payload.imageSrc, generatedSceneAssetId: payload.generatedSceneAssetId, generatedScenePrompt: payload.generatedScenePrompt ?? null, generatedSceneModel: payload.generatedSceneModel ?? null, }; nextDraftProfile.camp = updatedScene; } else { let touched = false; nextDraftProfile.landmarks = toRecordArray(nextDraftProfile.landmarks).map( (item) => { if (toText(item.id) !== payload.sceneId) { return item; } touched = true; updatedScene = { ...item, imageSrc: payload.imageSrc, generatedSceneAssetId: payload.generatedSceneAssetId, generatedScenePrompt: payload.generatedScenePrompt ?? null, generatedSceneModel: payload.generatedSceneModel ?? null, }; return updatedScene; }, ); if (!touched || !updatedScene) { throw new Error('目标地点不存在,无法同步场景资产。'); } } nextDraftProfile.sceneChapters = updateSceneChapterActsForScene({ draftProfile: nextDraftProfile, sceneId: payload.sceneId, imageSrc: payload.imageSrc, generatedSceneAssetId: payload.generatedSceneAssetId, }); const updatedAssetSummaries = rebuildRoleAssetCoverage( nextDraftProfile, ).sceneAssets.filter((entry) => entry.sceneId === payload.sceneId); return { sceneId: payload.sceneId, sceneKind: payload.sceneKind, updatedScene: updatedScene ?? {}, updatedAssetSummaries: updatedAssetSummaries.length > 0 ? updatedAssetSummaries : [ buildSceneAssetFallbackSummary({ sceneId: payload.sceneId, sceneKind: payload.sceneKind, updatedScene: updatedScene ?? {}, imageSrc: payload.imageSrc, generatedSceneAssetId: payload.generatedSceneAssetId, }), ], draftProfile: nextDraftProfile, }; } }