refactor: 收口 RPG 结果预览门禁

This commit is contained in:
2026-06-04 03:09:13 +08:00
parent 671f5da86a
commit 23314e62aa
7 changed files with 385 additions and 151 deletions

View File

@@ -556,6 +556,10 @@ import {
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
} from './platformPuzzleIdentityModel';
import {
buildPlatformRpgAgentResultPublishGateView,
resolvePlatformRpgAgentResultPreviewSourceLabel,
} from './platformRpgAgentResultPreviewModel';
import {
resolveSelectionStageAfterMissingCreationState,
resolveSelectionStageAfterProtectedDataLoss,
@@ -567,10 +571,6 @@ import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
type AgentResultPublishGateView = {
blockers: string[];
publishReady: boolean;
};
type CreationWorkShelfKind = CreationWorkShelfItem['kind'];
type CreationFlowReturnTarget = 'create' | 'draft-shelf';
type Match3DBackgroundCompileTask = {
@@ -653,18 +653,6 @@ async function resumePuzzleProfileSaveArchiveRaw(worldKey: string) {
);
}
type AgentResultBlockerView = {
code?: string;
message: string;
};
const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
'publish_missing_world_hook',
'publish_missing_player_premise',
'publish_missing_core_conflict',
'publish_missing_main_chapter',
'publish_missing_first_act',
]);
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
BACKGROUND_AUTH_REQUEST_OPTIONS;
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS =
@@ -1029,130 +1017,6 @@ async function resolvePublicWorkAuthorSummary(
return null;
}
function readProfileTextField(
profile: CustomWorldProfile | null,
paths: string[],
) {
for (const path of paths) {
let current: unknown = profile;
for (const segment of path.split('.')) {
if (!current || typeof current !== 'object') {
current = null;
break;
}
current = (current as Record<string, unknown>)[segment];
}
if (typeof current === 'string' && current.trim()) {
return current.trim();
}
}
return null;
}
function hasProfileTextArray(profile: CustomWorldProfile | null, key: string) {
const value = profile
? (profile as unknown as Record<string, unknown>)[key]
: null;
return Array.isArray(value)
? value.some((entry) => typeof entry === 'string' && entry.trim())
: false;
}
function hasProfileArray(profile: CustomWorldProfile | null, key: string) {
const value = profile
? (profile as unknown as Record<string, unknown>)[key]
: null;
return Array.isArray(value) && value.length > 0;
}
function hasSceneAct(profile: CustomWorldProfile | null) {
const rawProfile = profile as unknown as Record<string, unknown> | null;
const chapters =
rawProfile &&
(Array.isArray(rawProfile.sceneChapterBlueprints)
? rawProfile.sceneChapterBlueprints
: Array.isArray(rawProfile.sceneChapters)
? rawProfile.sceneChapters
: []);
return Array.isArray(chapters)
? chapters.some((chapter) => {
const acts =
chapter && typeof chapter === 'object'
? (chapter as Record<string, unknown>).acts
: null;
return Array.isArray(acts) && acts.length > 0;
})
: false;
}
function isAgentResultStructuralBlockerResolved(
profile: CustomWorldProfile,
code: string | undefined,
) {
if (!code || !AGENT_RESULT_STRUCTURAL_BLOCKER_CODES.has(code)) {
return false;
}
if (code === 'publish_missing_world_hook') {
return Boolean(
readProfileTextField(profile, [
'worldHook',
'creatorIntent.worldHook',
'anchorContent.worldPromise',
'anchorContent.worldPromise.hook',
'settingText',
]),
);
}
if (code === 'publish_missing_player_premise') {
return Boolean(
readProfileTextField(profile, [
'playerPremise',
'creatorIntent.playerPremise',
'anchorContent.playerEntryPoint',
'anchorContent.playerEntryPoint.openingIdentity',
'anchorContent.playerEntryPoint.openingProblem',
'anchorContent.playerEntryPoint.entryMotivation',
]),
);
}
if (code === 'publish_missing_core_conflict') {
return hasProfileTextArray(profile, 'coreConflicts');
}
if (code === 'publish_missing_main_chapter') {
return (
hasProfileArray(profile, 'chapters') ||
hasProfileArray(profile, 'sceneChapterBlueprints') ||
hasProfileArray(profile, 'sceneChapters')
);
}
return hasSceneAct(profile);
}
function buildAgentResultPublishGateView(
profile: CustomWorldProfile | null,
fallbackBlockers: AgentResultBlockerView[],
fallbackPublishReady: boolean,
): AgentResultPublishGateView {
if (!profile) {
return {
blockers: fallbackBlockers.map((entry) => entry.message),
publishReady: fallbackPublishReady,
};
}
const blockers = fallbackBlockers
.filter(
(entry) => !isAgentResultStructuralBlockerResolved(profile, entry.code),
)
.map((entry) => entry.message);
return {
blockers,
publishReady: blockers.length === 0,
};
}
function openPuzzleRuntimeStage(
setSelectionStage: (stage: SelectionStage) => void,
state: PuzzleRuntimeUrlState,
@@ -3337,7 +3201,7 @@ export function PlatformEntryFlowShellImpl({
);
const agentResultPublishGateView = useMemo(
() =>
buildAgentResultPublishGateView(
buildPlatformRpgAgentResultPublishGateView(
sessionController.generatedCustomWorldProfile,
agentResultPreviewBlockers,
Boolean(agentResultPreview?.publishReady),
@@ -3397,16 +3261,9 @@ export function PlatformEntryFlowShellImpl({
[openPublishShareModal, platformBootstrap],
);
const agentResultPreviewSourceLabel = useMemo(() => {
if (!agentResultPreview?.source) {
return null;
}
if (agentResultPreview.source === 'published_profile') {
return '已发布世界';
}
if (agentResultPreview.source === 'session_preview') {
return '会话预览';
}
return '服务端预览';
return resolvePlatformRpgAgentResultPreviewSourceLabel(
agentResultPreview?.source,
);
}, [agentResultPreview]);
const featuredGalleryEntries = useMemo(() => {
const bigFishPublicEntries = isBigFishCreationVisible

View File

@@ -0,0 +1,168 @@
import { describe, expect, test } from 'vitest';
import { createRpgCreationPublishedProfileFixture } from '../../../packages/shared/src/contracts/rpgCreationFixtures';
import type { CustomWorldProfile } from '../../types';
import {
buildPlatformRpgAgentResultPublishGateView,
type PlatformRpgAgentResultBlockerView,
resolvePlatformRpgAgentResultPreviewSourceLabel,
} from './platformRpgAgentResultPreviewModel';
function buildProfile(
overrides: Record<string, unknown> = {},
): CustomWorldProfile {
return {
...createRpgCreationPublishedProfileFixture(),
worldHook: '潮雾列岛旧灯塔重新点亮。',
playerPremise: '玩家从回潮旧灯塔切入沉船旧案。',
coreConflicts: ['守灯会与沉船旧案的冲突'],
sceneChapterBlueprints: [
{
id: 'chapter-1',
acts: [
{
id: 'act-1',
},
],
},
],
...overrides,
} as unknown as CustomWorldProfile;
}
const missingWorldHookBlocker: PlatformRpgAgentResultBlockerView = {
code: 'publish_missing_world_hook',
message: '缺少世界钩子',
};
const missingPlayerPremiseBlocker: PlatformRpgAgentResultBlockerView = {
code: 'publish_missing_player_premise',
message: '缺少玩家前提',
};
const missingCoreConflictBlocker: PlatformRpgAgentResultBlockerView = {
code: 'publish_missing_core_conflict',
message: '缺少核心冲突',
};
const missingMainChapterBlocker: PlatformRpgAgentResultBlockerView = {
code: 'publish_missing_main_chapter',
message: '缺少主章节',
};
const missingFirstActBlocker: PlatformRpgAgentResultBlockerView = {
code: 'publish_missing_first_act',
message: '缺少首幕',
};
const structuralBlockers: PlatformRpgAgentResultBlockerView[] = [
missingWorldHookBlocker,
missingPlayerPremiseBlocker,
missingCoreConflictBlocker,
missingMainChapterBlocker,
missingFirstActBlocker,
];
describe('platformRpgAgentResultPreviewModel', () => {
test('uses fallback blockers and publish readiness without a profile', () => {
expect(
buildPlatformRpgAgentResultPublishGateView(
null,
structuralBlockers.slice(0, 2),
false,
),
).toEqual({
blockers: ['缺少世界钩子', '缺少玩家前提'],
publishReady: false,
});
});
test('filters structural blockers already satisfied by the profile', () => {
expect(
buildPlatformRpgAgentResultPublishGateView(
buildProfile(),
[
...structuralBlockers,
{
code: 'future_blocker',
message: '未知服务端阻断',
},
],
false,
),
).toEqual({
blockers: ['未知服务端阻断'],
publishReady: false,
});
});
test('keeps unresolved structural blockers when profile fields are empty', () => {
expect(
buildPlatformRpgAgentResultPublishGateView(
buildProfile({
worldHook: '',
playerPremise: '',
settingText: '',
creatorIntent: null,
anchorContent: null,
coreConflicts: [],
chapters: [],
sceneChapterBlueprints: [],
sceneChapters: [],
}),
structuralBlockers,
true,
),
).toEqual({
blockers: structuralBlockers.map((entry) => entry.message),
publishReady: false,
});
});
test('resolves structural blockers from nested profile compatibility fields', () => {
expect(
buildPlatformRpgAgentResultPublishGateView(
buildProfile({
worldHook: '',
playerPremise: '',
settingText: '',
creatorIntent: {
worldHook: '旧灯塔潮路重新开启。',
},
anchorContent: {
playerEntryPoint: {
openingProblem: '玩家被卷入沉船旧案。',
},
},
coreConflicts: [''],
chapters: [],
sceneChapterBlueprints: null,
sceneChapters: [
{
acts: [{}],
},
],
}),
[
missingWorldHookBlocker,
missingPlayerPremiseBlocker,
missingFirstActBlocker,
],
false,
),
).toEqual({
blockers: [],
publishReady: true,
});
});
test('maps preview source to result label', () => {
expect(resolvePlatformRpgAgentResultPreviewSourceLabel(null)).toBeNull();
expect(
resolvePlatformRpgAgentResultPreviewSourceLabel('published_profile'),
).toBe('已发布世界');
expect(
resolvePlatformRpgAgentResultPreviewSourceLabel('session_preview'),
).toBe('会话预览');
expect(
resolvePlatformRpgAgentResultPreviewSourceLabel('future_source'),
).toBe(
'服务端预览',
);
});
});

View File

@@ -0,0 +1,159 @@
import type { RpgCreationPreviewSource } from '../../../packages/shared/src/contracts/rpgCreationPreview';
import type { CustomWorldProfile } from '../../types';
export type PlatformRpgAgentResultBlockerView = {
code?: string | null;
message: string;
};
export type PlatformRpgAgentResultPublishGateView = {
blockers: string[];
publishReady: boolean;
};
const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
'publish_missing_world_hook',
'publish_missing_player_premise',
'publish_missing_core_conflict',
'publish_missing_main_chapter',
'publish_missing_first_act',
]);
function readProfileTextField(
profile: CustomWorldProfile | null,
paths: string[],
) {
for (const path of paths) {
let current: unknown = profile;
for (const segment of path.split('.')) {
if (!current || typeof current !== 'object') {
current = null;
break;
}
current = (current as Record<string, unknown>)[segment];
}
if (typeof current === 'string' && current.trim()) {
return current.trim();
}
}
return null;
}
function hasProfileTextArray(profile: CustomWorldProfile | null, key: string) {
const value = profile
? (profile as unknown as Record<string, unknown>)[key]
: null;
return Array.isArray(value)
? value.some((entry) => typeof entry === 'string' && entry.trim())
: false;
}
function hasProfileArray(profile: CustomWorldProfile | null, key: string) {
const value = profile
? (profile as unknown as Record<string, unknown>)[key]
: null;
return Array.isArray(value) && value.length > 0;
}
function hasSceneAct(profile: CustomWorldProfile | null) {
const rawProfile = profile as unknown as Record<string, unknown> | null;
const chapters =
rawProfile &&
(Array.isArray(rawProfile.sceneChapterBlueprints)
? rawProfile.sceneChapterBlueprints
: Array.isArray(rawProfile.sceneChapters)
? rawProfile.sceneChapters
: []);
return Array.isArray(chapters)
? chapters.some((chapter) => {
const acts =
chapter && typeof chapter === 'object'
? (chapter as Record<string, unknown>).acts
: null;
return Array.isArray(acts) && acts.length > 0;
})
: false;
}
function isAgentResultStructuralBlockerResolved(
profile: CustomWorldProfile,
code: string | null | undefined,
) {
if (!code || !AGENT_RESULT_STRUCTURAL_BLOCKER_CODES.has(code)) {
return false;
}
if (code === 'publish_missing_world_hook') {
return Boolean(
readProfileTextField(profile, [
'worldHook',
'creatorIntent.worldHook',
'anchorContent.worldPromise',
'anchorContent.worldPromise.hook',
'settingText',
]),
);
}
if (code === 'publish_missing_player_premise') {
return Boolean(
readProfileTextField(profile, [
'playerPremise',
'creatorIntent.playerPremise',
'anchorContent.playerEntryPoint',
'anchorContent.playerEntryPoint.openingIdentity',
'anchorContent.playerEntryPoint.openingProblem',
'anchorContent.playerEntryPoint.entryMotivation',
]),
);
}
if (code === 'publish_missing_core_conflict') {
return hasProfileTextArray(profile, 'coreConflicts');
}
if (code === 'publish_missing_main_chapter') {
return (
hasProfileArray(profile, 'chapters') ||
hasProfileArray(profile, 'sceneChapterBlueprints') ||
hasProfileArray(profile, 'sceneChapters')
);
}
return hasSceneAct(profile);
}
export function buildPlatformRpgAgentResultPublishGateView(
profile: CustomWorldProfile | null,
fallbackBlockers: PlatformRpgAgentResultBlockerView[],
fallbackPublishReady: boolean,
): PlatformRpgAgentResultPublishGateView {
if (!profile) {
return {
blockers: fallbackBlockers.map((entry) => entry.message),
publishReady: fallbackPublishReady,
};
}
const blockers = fallbackBlockers
.filter(
(entry) => !isAgentResultStructuralBlockerResolved(profile, entry.code),
)
.map((entry) => entry.message);
return {
blockers,
publishReady: blockers.length === 0,
};
}
export function resolvePlatformRpgAgentResultPreviewSourceLabel(
source: RpgCreationPreviewSource | string | null | undefined,
) {
if (!source) {
return null;
}
if (source === 'published_profile') {
return '已发布世界';
}
if (source === 'session_preview') {
return '会话预览';
}
return '服务端预览';
}