Integrate role asset studio into custom world agent flow
This commit is contained in:
233
server-node/src/services/customWorldWorkSummaryService.ts
Normal file
233
server-node/src/services/customWorldWorkSummaryService.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user