This commit is contained in:
2026-04-21 18:27:46 +08:00
parent 04bff9617d
commit 4372ab5be1
358 changed files with 30788 additions and 14737 deletions

View File

@@ -0,0 +1,256 @@
import type { CustomWorldProfileRecord } from '../../../packages/shared/src/contracts/runtime.js';
import type { RpgWorldProfileRepositoryPort } from '../repositories/RpgWorldProfileRepository.js';
import { buildRpgCreationPreviewProfileFromDraftProfile } from './rpgCreationPreviewProfileBuilder.js';
import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js';
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function hasGeneratedSceneAsset(
value: unknown,
) {
return Boolean(toText((value as Record<string, unknown> | null)?.generatedSceneAssetId));
}
export class CustomWorldAgentPublishingService {
constructor(
private readonly rpgWorldProfileRepository: RpgWorldProfileRepositoryPort,
) {}
/**
* Phase4 需要把“能不能发布”收成可读的后端真相,
* 这样结果页、works 和 publish executor 才能共享同一套 blocker 语义。
*/
evaluatePublishReadiness(params: {
sessionId: string;
draftProfile: unknown;
qualityFindings?: Array<{
severity: 'info' | 'warning' | 'blocker';
code?: string;
targetId?: string | null;
message: string;
}>;
}) {
const draftProfile = normalizeFoundationDraftProfile(params.draftProfile);
if (!draftProfile) {
return {
profileId: `agent-draft-${params.sessionId}`,
blockers: [
{
severity: 'blocker' as const,
code: 'publish_empty_draft',
message: '当前世界草稿为空,无法发布。',
},
],
};
}
const findings = params.qualityFindings ?? [];
const blockers = findings.filter((entry) => entry.severity === 'blocker');
const readinessBlockers = [...blockers];
if (!draftProfile.worldHook.trim()) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_missing_world_hook',
message: '当前世界缺少 world hook发布前需要先补齐世界一句话钩子。',
});
}
if (!draftProfile.playerPremise.trim()) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_missing_player_premise',
message: '当前世界缺少玩家身份与切入前提,发布前需要先补齐玩家 premise。',
});
}
if (
draftProfile.coreConflicts.length <= 0 ||
!draftProfile.coreConflicts.some((entry) => toText(entry))
) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_missing_core_conflict',
message: '当前世界缺少核心冲突,发布前需要先补齐核心冲突。',
});
}
if ((draftProfile.chapters?.length ?? 0) <= 0) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_missing_main_chapter',
message: '当前世界还没有主线章节草稿,发布前至少要保留主线第一幕。',
});
}
const firstSceneActExists = draftProfile.sceneChapters.some(
(chapter) => chapter.acts.length > 0,
);
if (!firstSceneActExists) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_missing_first_act',
message: '当前世界还没有主线第一幕,发布前至少要保留一个场景幕。',
});
}
const missingRoleAssets = [
...draftProfile.playableNpcs,
...draftProfile.storyNpcs,
].filter(
(role) =>
!toText(role.generatedVisualAssetId) ||
!toText(role.generatedAnimationSetId),
);
if (missingRoleAssets.length > 0) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_role_assets_incomplete',
targetId: missingRoleAssets[0]?.id ?? null,
message: '仍有角色缺少正式主图或动作资产,发布前需要先补齐。',
});
}
if (!draftProfile.camp || !toText(draftProfile.camp.imageSrc) || !hasGeneratedSceneAsset(draftProfile.camp)) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_camp_scene_missing',
targetId: draftProfile.camp?.id ?? null,
message: '营地还缺少正式场景图资产,发布前需要先确认营地图。',
});
}
const missingLandmarkScenes = draftProfile.landmarks.filter(
(landmark) =>
!toText(landmark.imageSrc) || !hasGeneratedSceneAsset(landmark),
);
if (missingLandmarkScenes.length > 0) {
readinessBlockers.push({
severity: 'blocker',
code: 'publish_landmark_scene_missing',
targetId: missingLandmarkScenes[0]?.id ?? null,
message: '仍有地点缺少正式场景图资产,发布前需要先补齐地点图。',
});
}
return {
profileId:
toText(
(params.draftProfile as Record<string, unknown> | null)
?.legacyResultProfile?.id,
) || `agent-draft-${params.sessionId}`,
blockers: readinessBlockers,
};
}
/**
* Phase4 统一复用发布门禁摘要,避免 preview / works / enter-world 各自拼 blocker 口径。
*/
summarizePublishGate(params: {
sessionId: string;
stage?: string | null;
draftProfile: unknown;
qualityFindings?: Array<{
severity: 'info' | 'warning' | 'blocker';
code?: string;
targetId?: string | null;
message: string;
}>;
}) {
const readiness = this.evaluatePublishReadiness(params);
const blockers = readiness.blockers.map((entry) => ({
id:
typeof entry.code === 'string' && entry.code.trim()
? entry.code
: `publish-blocker-${entry.message}`,
code:
typeof entry.code === 'string' && entry.code.trim()
? entry.code
: 'publish_blocker',
message: entry.message,
}));
return {
profileId: readiness.profileId,
blockers,
blockerCount: blockers.length,
publishReady: blockers.length === 0,
canEnterWorld:
String(params.stage ?? '').trim() === 'published' && blockers.length === 0,
};
}
buildPublishReadiness(params: {
sessionId: string;
draftProfile: unknown;
qualityFindings?: Array<{
severity: 'info' | 'warning' | 'blocker';
code?: string;
targetId?: string | null;
message: string;
}>;
}) {
const readiness = this.evaluatePublishReadiness(params);
if (readiness.blockers.length > 0) {
throw new Error(
`当前世界仍有 ${readiness.blockers.length} 个 blocker暂时不能发布${readiness.blockers
.map((entry) => entry.message)
.join('')}`,
);
}
return {
profileId: readiness.profileId,
};
}
async publishSessionDraft(params: {
userId: string;
authorDisplayName: string;
sessionId: string;
draftProfile: Record<string, unknown>;
qualityFindings?: Array<{
severity: 'info' | 'warning' | 'blocker';
message: string;
}>;
}) {
const readiness = this.buildPublishReadiness({
sessionId: params.sessionId,
draftProfile: params.draftProfile,
qualityFindings: params.qualityFindings,
});
const publishedProfile = buildRpgCreationPreviewProfileFromDraftProfile({
sessionId: params.sessionId,
draftProfile: params.draftProfile,
profileId: readiness.profileId,
});
await this.rpgWorldProfileRepository.upsertOwnProfile(
params.userId,
readiness.profileId,
publishedProfile as unknown as Record<string, unknown>,
params.authorDisplayName,
);
const mutation = await this.rpgWorldProfileRepository.publishOwnProfile(
params.userId,
readiness.profileId,
params.authorDisplayName,
);
if (!mutation) {
throw new Error('世界发布失败,未找到目标作品。');
}
return {
profileId: readiness.profileId,
publishedProfile,
mutation,
};
}
}