import type { CustomWorldAgentStage, CustomWorldWorkSummary, } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import type { CustomWorldLibraryEntry, CustomWorldProfileRecord, } from '../../../packages/shared/src/contracts/runtime.js'; import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js'; import { resolveCustomWorldCoverPresentation } from '../repositories/customWorldLibraryMetadata.js'; import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js'; import { buildDraftSummaryFromIntent, buildDraftTitleFromIntent, normalizeCreatorIntentRecord, } from './customWorldAgentIntentExtractionService.js'; import { rebuildRoleAssetCoverage, resolveRoleAssetStatusLabel, } from './customWorldAgentRoleAssetStateService.js'; import type { CustomWorldAgentSessionRecord, CustomWorldAgentSessionStore, } from './customWorldAgentSessionStore.js'; import { buildDraftSummaryFromEightAnchorContent, buildDraftTitleFromEightAnchorContent, } from './eightAnchorCompatibilityService.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 && typeof item === 'object') : []; } function truncateText(value: string, maxLength: number) { if (value.length <= maxLength) { return value; } return `${value.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } function formatDraftStageLabel(stage: CustomWorldAgentStage) { if (stage === 'collecting_intent') return '收集世界锚点'; if (stage === 'clarifying') return '补齐关键锚点'; if (stage === 'foundation_review') return '准备整理底稿'; if (stage === 'object_refining') return '待完善草稿'; if (stage === 'visual_refining') return '视觉工坊'; if (stage === 'long_tail_review') return '扩展长尾'; if (stage === 'ready_to_publish') return '准备发布'; if (stage === 'published') return '已发布'; return '发生错误'; } function resolveDraftTitle(session: CustomWorldAgentSessionRecord) { const intent = normalizeCreatorIntentRecord(session.creatorIntent); const draftProfile = normalizeFoundationDraftProfile(session.draftProfile); return ( draftProfile?.name || buildDraftTitleFromEightAnchorContent(session.anchorContent) || buildDraftTitleFromIntent(intent) || toText(session.draftProfile?.title) || truncateText(session.seedText, 18) || '未命名草稿' ); } function resolveDraftSummary(session: CustomWorldAgentSessionRecord) { const intent = normalizeCreatorIntentRecord(session.creatorIntent); const compiledSummary = buildDraftSummaryFromIntent(intent); const draftProfile = normalizeFoundationDraftProfile(session.draftProfile); return ( draftProfile?.summary || buildDraftSummaryFromEightAnchorContent(session.anchorContent) || compiledSummary || toText(session.draftProfile?.summary) || truncateText(session.seedText, 72) || '还在收集你的世界锚点。' ); } function resolveDraftCounts(session: CustomWorldAgentSessionRecord) { const draftProfile = normalizeFoundationDraftProfile(session.draftProfile); if (draftProfile) { // 草稿列表里的“角色”展示的是当前草稿中全部可编辑角色,而不是仅限可扮演角色。 const totalRoleCount = [ ...new Set( [...draftProfile.playableNpcs, ...draftProfile.storyNpcs].map( (entry) => entry.id, ), ), ].length; return { playableNpcCount: totalRoleCount, landmarkCount: draftProfile.landmarks.length, }; } const playableNpcCount = session.draftCards.filter( (card) => card.kind === 'character', ).length; const landmarkCount = session.draftCards.filter( (card) => card.kind === 'landmark' || card.kind === 'camp', ).length; return { playableNpcCount, landmarkCount, }; } function resolveDraftRoleAssetProgress(session: CustomWorldAgentSessionRecord) { const coverage = rebuildRoleAssetCoverage(session.draftProfile); const roleVisualReadyCount = coverage.roleAssets.filter( (entry) => entry.status !== 'missing', ).length; const roleAnimationReadyCount = coverage.roleAssets.filter( (entry) => entry.status === 'complete', ).length; const leadRole = coverage.roleAssets[0]; return { roleVisualReadyCount, roleAnimationReadyCount, roleAssetSummaryLabel: leadRole ? `${leadRole.roleName} · ${resolveRoleAssetStatusLabel(leadRole.status)}` : coverage.roleAssets.length > 0 ? '角色资产进行中' : null, }; } function resolveDraftCover(session: CustomWorldAgentSessionRecord) { const draftProfile = toRecord(session.draftProfile); if (!draftProfile) { return { imageSrc: null, renderMode: 'image' as const, characterImageSrcs: [], }; } return resolveCustomWorldCoverPresentation( draftProfile as CustomWorldProfileRecord, ); } function isLibraryEntry( value: unknown, ): value is CustomWorldLibraryEntry { const record = toRecord(value); return ( Boolean(record) && typeof record.ownerUserId === 'string' && typeof record.profileId === 'string' && Boolean(toRecord(record.profile)) ); } function isPublishedLibraryEntry( value: unknown, ): value is CustomWorldLibraryEntry { return isLibraryEntry(value) && value.visibility === 'published'; } export async function listCustomWorldWorkSummaries( userId: string, dependencies: { runtimeRepository: RuntimeRepositoryPort; customWorldAgentSessions: CustomWorldAgentSessionStore; }, ) { const [profiles, sessions] = await Promise.all([ dependencies.runtimeRepository.listCustomWorldProfiles(userId), dependencies.customWorldAgentSessions.list(userId), ]); const draftItems: CustomWorldWorkSummary[] = sessions.map((session) => { const counts = resolveDraftCounts(session); const roleAssetProgress = resolveDraftRoleAssetProgress(session); const coverPresentation = resolveDraftCover(session); return { workId: `draft:${session.sessionId}`, sourceType: 'agent_session', status: 'draft', title: resolveDraftTitle(session), subtitle: normalizeFoundationDraftProfile(session.draftProfile)?.subtitle || formatDraftStageLabel(session.stage), summary: resolveDraftSummary(session), coverImageSrc: coverPresentation.imageSrc, coverRenderMode: coverPresentation.renderMode, coverCharacterImageSrcs: coverPresentation.characterImageSrcs, updatedAt: session.updatedAt, publishedAt: null, stage: session.stage, stageLabel: formatDraftStageLabel(session.stage), playableNpcCount: counts.playableNpcCount, landmarkCount: counts.landmarkCount, roleVisualReadyCount: roleAssetProgress.roleVisualReadyCount, roleAnimationReadyCount: roleAssetProgress.roleAnimationReadyCount, roleAssetSummaryLabel: roleAssetProgress.roleAssetSummaryLabel, sessionId: session.sessionId, profileId: null, canResume: true, canEnterWorld: false, }; }); const publishedItems: CustomWorldWorkSummary[] = profiles .filter((profile) => isPublishedLibraryEntry(profile)) .map((profile) => { const libraryEntry = profile; const profileRecord = ( libraryEntry?.profile ?? profile ) as CustomWorldProfileRecord & Record; const playableNpcs = toRecordArray(profileRecord.playableNpcs); const landmarks = toRecordArray(profileRecord.landmarks); const updatedAt = (libraryEntry ? toText(libraryEntry.updatedAt) : '') || toText(profileRecord.updatedAt) || new Date().toISOString(); const coverPresentation = resolveCustomWorldCoverPresentation(profileRecord); const roleVisualReadyCount = playableNpcs.filter( (entry) => Boolean(toText(entry.imageSrc)) && Boolean(toText(entry.generatedVisualAssetId)), ).length; const roleAnimationReadyCount = playableNpcs.filter( (entry) => Boolean(toText(entry.generatedAnimationSetId)), ).length; return { workId: `published:${toText(profileRecord.id) || updatedAt}`, sourceType: 'published_profile', status: 'published', title: toText(libraryEntry.worldName) || toText(profileRecord.name) || '未命名世界', subtitle: toText(libraryEntry.subtitle) || toText(profileRecord.subtitle) || '已保存作品', summary: toText(libraryEntry.summaryText) || toText(profileRecord.summary) || '这个世界已经可以直接进入体验。', coverImageSrc: libraryEntry.coverImageSrc || coverPresentation.imageSrc, coverRenderMode: coverPresentation.renderMode, coverCharacterImageSrcs: coverPresentation.characterImageSrcs, updatedAt, publishedAt: toText(libraryEntry.publishedAt) || toText(profileRecord.publishedAt) || updatedAt, stage: 'published', stageLabel: '已发布', playableNpcCount: libraryEntry.playableNpcCount > 0 ? libraryEntry.playableNpcCount : playableNpcs.length, landmarkCount: libraryEntry.landmarkCount > 0 ? libraryEntry.landmarkCount : landmarks.length, roleVisualReadyCount, roleAnimationReadyCount, roleAssetSummaryLabel: roleAnimationReadyCount > 0 ? `动作已就绪 ${roleAnimationReadyCount}` : roleVisualReadyCount > 0 ? `主图已就绪 ${roleVisualReadyCount}` : null, sessionId: null, profileId: toText(libraryEntry.profileId) || toText(profileRecord.id) || null, canResume: false, canEnterWorld: true, }; }); return [...draftItems, ...publishedItems].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt), ); }