Files
Genarrative/server-node/src/services/customWorldWorkSummaryService.ts

234 lines
7.8 KiB
TypeScript

import type {
CustomWorldAgentStage,
CustomWorldWorkSummary,
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
import type { CustomWorldProfileRecord } from '../../../packages/shared/src/contracts/runtime.js';
import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.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';
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<string, unknown>)
: 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 ||
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 ||
compiledSummary ||
toText(session.draftProfile?.summary) ||
truncateText(session.seedText, 72) ||
'还在收集你的世界锚点。'
);
}
function resolveDraftCounts(session: CustomWorldAgentSessionRecord) {
const draftProfile = normalizeFoundationDraftProfile(session.draftProfile);
if (draftProfile) {
return {
playableNpcCount: [
...new Set(
[...draftProfile.playableNpcs, ...draftProfile.storyNpcs].map(
(entry) => entry.id,
),
),
].length,
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 resolvePublishedCover(profile: Record<string, unknown>) {
const camp = toRecord(profile.camp);
const playableNpcs = toRecordArray(profile.playableNpcs);
const leadNpc = toRecord(playableNpcs[0]);
return toText(camp?.imageSrc) || toText(leadNpc?.imageSrc) || null;
}
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);
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: null,
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.map((profile) => {
const profileRecord = profile as CustomWorldProfileRecord &
Record<string, unknown>;
const playableNpcs = toRecordArray(profileRecord.playableNpcs);
const landmarks = toRecordArray(profileRecord.landmarks);
const updatedAt =
toText(profileRecord.updatedAt) || new Date().toISOString();
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(profileRecord.name) || '未命名世界',
subtitle: toText(profileRecord.subtitle) || '已发布作品',
summary:
toText(profileRecord.summary) || '这个世界已经可以直接进入体验。',
coverImageSrc: resolvePublishedCover(profileRecord),
updatedAt,
publishedAt: toText(profileRecord.publishedAt) || updatedAt,
stage: 'published',
stageLabel: '已发布',
playableNpcCount: playableNpcs.length,
landmarkCount: landmarks.length,
roleVisualReadyCount,
roleAnimationReadyCount,
roleAssetSummaryLabel:
roleAnimationReadyCount > 0
? `动作已就绪 ${roleAnimationReadyCount}`
: roleVisualReadyCount > 0
? `主图已就绪 ${roleVisualReadyCount}`
: null,
sessionId: null,
profileId: toText(profileRecord.id) || null,
canResume: false,
canEnterWorld: true,
};
});
return [...draftItems, ...publishedItems].sort((left, right) =>
right.updatedAt.localeCompare(left.updatedAt),
);
}