This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -56,6 +56,7 @@ import {
streamBigFishCreationMessage,
} from '../../services/big-fish-creation';
import {
likeBigFishGalleryWork,
listBigFishGallery,
remixBigFishGalleryWork,
} from '../../services/big-fish-gallery';
@@ -94,11 +95,13 @@ import {
} from '../../services/puzzle-agent';
import {
getPuzzleGalleryDetail,
likePuzzleGalleryWork,
listPuzzleGallery,
remixPuzzleGalleryWork,
} from '../../services/puzzle-gallery';
import {
advanceLocalPuzzleNextLevel,
advancePuzzleNextLevel,
getPuzzleRun,
startPuzzleRun,
submitPuzzleLeaderboard,
@@ -121,6 +124,7 @@ import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreati
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetailByCode,
likeRpgEntryWorldGallery,
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
@@ -202,6 +206,13 @@ function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
function isSamePlatformPublicGalleryEntry(
left: PlatformPublicGalleryCard,
right: PlatformPublicGalleryCard,
) {
return getPlatformPublicGalleryEntryKey(left) === getPlatformPublicGalleryEntryKey(right);
}
function mergePlatformPublicGalleryEntries(
rpgEntries: CustomWorldGalleryCard[],
puzzleEntries: PlatformPublicGalleryCard[],
@@ -281,6 +292,7 @@ function mapPublicWorkDetailToBigFishWork(
workId: entry.workId,
sourceSessionId: entry.profileId,
ownerUserId: entry.ownerUserId,
authorDisplayName: entry.authorDisplayName,
title: entry.worldName,
subtitle: entry.subtitle,
summary: entry.summaryText,
@@ -299,6 +311,20 @@ function mapPublicWorkDetailToBigFishWork(
};
}
function mergePuzzleWorkSummary(
current: PuzzleWorkSummary,
updated: PuzzleWorkSummary,
): PuzzleWorkSummary {
return current.profileId === updated.profileId ? updated : current;
}
function mergeBigFishWorkSummary(
current: BigFishWorkSummary,
updated: BigFishWorkSummary,
): BigFishWorkSummary {
return current.sourceSessionId === updated.sourceSessionId ? updated : current;
}
async function resolvePublicWorkAuthorSummary(
entry: PlatformPublicGalleryCard,
): Promise<PublicUserSummary | null> {
@@ -457,15 +483,85 @@ function buildPuzzleResultProfileId(sessionId: string | null | undefined) {
function buildPuzzleCompileActionFromFormPayload(
payload: CreatePuzzleAgentSessionRequest | null,
): PuzzleAgentActionRequest {
const workTitle = payload?.workTitle?.trim() || payload?.seedText?.trim();
const workDescription = payload?.workDescription?.trim();
const pictureDescription = payload?.pictureDescription?.trim();
return {
action: 'compile_puzzle_draft',
promptText:
payload?.pictureDescription?.trim() || payload?.seedText?.trim(),
promptText: pictureDescription || workTitle,
...(workTitle ? { workTitle } : {}),
...(workDescription ? { workDescription } : {}),
...(pictureDescription ? { pictureDescription } : {}),
referenceImageSrc: payload?.referenceImageSrc || null,
candidateCount: 1,
};
}
function buildPuzzleFormPayloadFromSession(
session: PuzzleAgentSessionSnapshot,
): CreatePuzzleAgentSessionRequest {
const formDraft = session.draft?.formDraft;
const workTitle =
formDraft?.workTitle?.trim() ||
session.draft?.workTitle?.trim() ||
session.draft?.levelName?.trim() ||
session.anchorPack.themePromise.value.trim() ||
session.seedText?.trim() ||
'';
const workDescription =
formDraft?.workDescription?.trim() ||
session.draft?.workDescription?.trim() ||
session.draft?.summary?.trim() ||
'';
const pictureDescription =
formDraft?.pictureDescription?.trim() ||
session.draft?.levels?.[0]?.pictureDescription?.trim() ||
session.anchorPack.visualSubject.value.trim() ||
'';
return {
seedText: workTitle,
workTitle,
workDescription,
pictureDescription,
referenceImageSrc: null,
};
}
function buildPuzzleFormPayloadFromAction(
payload: PuzzleAgentActionRequest,
): CreatePuzzleAgentSessionRequest | null {
if (
payload.action !== 'compile_puzzle_draft' &&
payload.action !== 'save_puzzle_form_draft'
) {
return null;
}
const workTitle = payload.workTitle?.trim() ?? '';
const workDescription = payload.workDescription?.trim() ?? '';
const pictureDescription =
payload.pictureDescription?.trim() || payload.promptText?.trim() || '';
return {
seedText: workTitle,
workTitle,
workDescription,
pictureDescription,
referenceImageSrc:
payload.action === 'compile_puzzle_draft'
? (payload.referenceImageSrc ?? null)
: null,
};
}
function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) {
return Boolean(
session?.stage === 'collecting_anchors' && session.draft?.formDraft,
);
}
const CustomWorldGenerationView = lazy(async () => {
const module = await import('../CustomWorldGenerationView');
return {
@@ -1125,6 +1221,10 @@ export function PlatformEntryFlowShellImpl({
onActionComplete: async ({ payload, response, setSession }) => {
setPuzzleOperation(response.operation);
setSession(response.session);
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
setPuzzleFormDraftPayload(formPayload);
}
if (payload.action === 'publish_puzzle_work') {
await Promise.allSettled([
@@ -1167,6 +1267,11 @@ export function PlatformEntryFlowShellImpl({
}
},
beforeExecuteAction: ({ payload }) => {
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
setPuzzleFormDraftPayload(formPayload);
}
if (payload.action !== 'compile_puzzle_draft') {
return;
}
@@ -1224,19 +1329,16 @@ export function PlatformEntryFlowShellImpl({
setPuzzleOperation(null);
setPuzzleGenerationState(null);
setPuzzleFormDraftPayload(null);
puzzleFlow.setSession(null);
puzzleFlow.setError(null);
puzzleFlow.setStreamingReplyText('');
puzzleFlow.setIsStreamingReply(false);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('puzzle-agent-workspace');
}, [enterCreateTab, puzzleFlow, setSelectionStage]);
const nextSession = await puzzleFlow.openWorkspace({});
if (nextSession) {
void refreshPuzzleShelf();
}
}, [puzzleFlow, refreshPuzzleShelf]);
const createPuzzleDraftFromForm = useCallback(
async (payload: CreatePuzzleAgentSessionRequest) => {
setPuzzleFormDraftPayload(payload);
const nextSession = await puzzleFlow.openWorkspace(payload);
const nextSession = puzzleFlow.session ?? (await puzzleFlow.openWorkspace(payload));
if (!nextSession) {
return;
}
@@ -1249,6 +1351,36 @@ export function PlatformEntryFlowShellImpl({
[puzzleFlow],
);
const savePuzzleFormDraft = useCallback(
async (payload: CreatePuzzleAgentSessionRequest) => {
const session = puzzleFlow.session;
if (!session || session.stage !== 'collecting_anchors') {
return;
}
setPuzzleFormDraftPayload(payload);
try {
const response = await executePuzzleAgentAction(session.sessionId, {
action: 'save_puzzle_form_draft',
promptText: payload.pictureDescription ?? null,
workTitle: payload.workTitle ?? payload.seedText ?? '',
workDescription: payload.workDescription ?? '',
pictureDescription: payload.pictureDescription ?? '',
});
setPuzzleOperation(response.operation);
puzzleFlow.setSession(response.session);
setPuzzleError(null);
void refreshPuzzleShelf();
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '保存拼图表单草稿失败。'),
);
}
},
[puzzleFlow, refreshPuzzleShelf, resolvePuzzleErrorMessage, setPuzzleError],
);
useEffect(() => {
if (platformBootstrap.canReadProtectedData) {
hadReadableProtectedDataRef.current = true;
@@ -1318,7 +1450,7 @@ export function PlatformEntryFlowShellImpl({
const handleCreationHubCreateType = useCallback(
(type: PlatformCreationTypeId) => {
if (type === 'airp' || type === 'visual-novel') {
if (type === 'rpg' || type === 'airp' || type === 'visual-novel') {
return;
}
@@ -1326,13 +1458,6 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (type === 'rpg') {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
return;
}
if (type === 'big-fish') {
runProtectedAction(() => {
void openBigFishAgentWorkspace();
@@ -1351,7 +1476,6 @@ export function PlatformEntryFlowShellImpl({
openPuzzleAgentWorkspace,
prepareCreationLaunch,
runProtectedAction,
sessionController,
],
);
@@ -1459,6 +1583,7 @@ export function PlatformEntryFlowShellImpl({
returnStage: PuzzleRuntimeReturnStage = 'work-detail',
detailItem?: PuzzleWorkSummary,
mirrorErrorToPublicDetail = false,
levelId?: string | null,
) => {
if (isPuzzleBusy) {
return;
@@ -1470,7 +1595,10 @@ export function PlatformEntryFlowShellImpl({
try {
const item =
detailItem ?? (await getPuzzleGalleryDetail(profileId)).item;
const { run } = await startPuzzleRun({ profileId: item.profileId });
const { run } = await startPuzzleRun({
profileId: item.profileId,
levelId: levelId ?? null,
});
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeReturnStage(returnStage);
@@ -1513,6 +1641,8 @@ export function PlatformEntryFlowShellImpl({
ownerUserId: authUi?.user?.id ?? 'current-user',
sourceSessionId: puzzleSession?.sessionId ?? null,
authorDisplayName: authUi?.user?.displayName ?? '玩家',
workTitle: draft.workTitle || draft.levelName,
workDescription: draft.workDescription || draft.summary,
levelName: draft.levelName,
summary: draft.summary,
themeTags: draft.themeTags,
@@ -1787,7 +1917,7 @@ export function PlatformEntryFlowShellImpl({
]);
const advancePuzzleLevel = useCallback(async () => {
if (!puzzleRun || isPuzzleBusy) {
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
return;
}
@@ -1801,13 +1931,15 @@ export function PlatformEntryFlowShellImpl({
setPuzzleError(null);
try {
const { run } = await advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ??
puzzleSession?.sessionId ??
null,
});
const { run } = isLocalPuzzleRun(puzzleRun)
? await advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ??
puzzleSession?.sessionId ??
null,
})
: await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
@@ -1817,6 +1949,7 @@ export function PlatformEntryFlowShellImpl({
}
}, [
isPuzzleBusy,
isPuzzleLeaderboardBusy,
puzzleRun,
puzzleSession,
resolvePuzzleErrorMessage,
@@ -2022,14 +2155,17 @@ export function PlatformEntryFlowShellImpl({
}
runProtectedAction(() => {
const displayName =
work.workTitle?.trim() || work.levelName.trim() || '未命名拼图';
const confirmed = window.confirm(
`确认删除作品《${work.levelName}》吗?删除后会从你的作品列表和公开广场中移除。`,
`确认删除作品《${displayName}》吗?删除后会从你的作品列表和公开广场中移除。`,
);
if (!confirmed) {
return;
}
setDeletingCreationWorkId(work.workId);
setPuzzleFormDraftPayload(null);
setPuzzleError(null);
void deletePuzzleWork(work.profileId)
@@ -2095,6 +2231,127 @@ export function PlatformEntryFlowShellImpl({
[setSelectionStage],
);
const syncUpdatedPublicWorkDetail = useCallback(
(updatedEntry: PlatformPublicGalleryCard) => {
setSelectedPublicWorkDetail((current) =>
current && isSamePlatformPublicGalleryEntry(current, updatedEntry)
? updatedEntry
: current,
);
},
[],
);
const likePublicWork = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (isPublicWorkDetailBusy) {
return;
}
runProtectedAction(() => {
setIsPublicWorkDetailBusy(true);
setPublicWorkDetailError(null);
if (isBigFishGalleryEntry(entry)) {
void likeBigFishGalleryWork(entry.profileId)
.then((response) => {
const updatedWork = response.items.find(
(item) => item.sourceSessionId === entry.profileId,
);
if (!updatedWork) {
return;
}
setBigFishGalleryEntries((current) =>
current.map((item) => mergeBigFishWorkSummary(item, updatedWork)),
);
setBigFishWorks((current) =>
current.map((item) => mergeBigFishWorkSummary(item, updatedWork)),
);
syncUpdatedPublicWorkDetail(
mapBigFishWorkToPublicWorkDetail(updatedWork),
);
setBigFishRuntimeWork((current) =>
current ? mergeBigFishWorkSummary(current, updatedWork) : current,
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveBigFishErrorMessage(
error,
'点赞大鱼吃小鱼作品失败。',
),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (isPuzzleGalleryEntry(entry)) {
void likePuzzleGalleryWork(entry.profileId)
.then((response) => {
const updatedWork = response.item;
setPuzzleGalleryEntries((current) =>
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
);
setPuzzleWorks((current) =>
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
);
setSelectedPuzzleDetail((current) =>
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
);
syncUpdatedPublicWorkDetail(
mapPuzzleWorkToPublicWorkDetail(updatedWork),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, '点赞拼图作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId ? updatedEntry : current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId
? mapRpgGalleryCardToPublicWorkDetail(updatedEntry)
: item,
),
);
syncUpdatedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
});
},
[
isPublicWorkDetailBusy,
platformBootstrap,
resolveBigFishErrorMessage,
resolvePuzzleErrorMessage,
runProtectedAction,
syncUpdatedPublicWorkDetail,
],
);
useEffect(() => {
const detailEntry =
selectionStage === 'work-detail'
@@ -2244,9 +2501,25 @@ export function PlatformEntryFlowShellImpl({
);
if (!restoredSession) {
await refreshPuzzleShelf().catch(() => undefined);
return;
}
if (isPuzzleFormOnlyDraft(restoredSession)) {
setPuzzleFormDraftPayload(
buildPuzzleFormPayloadFromSession(restoredSession),
);
setSelectionStage('puzzle-agent-workspace');
} else {
setPuzzleFormDraftPayload(null);
}
},
[openPuzzleDetail, puzzleFlow, refreshPuzzleShelf, setPuzzleError],
[
openPuzzleDetail,
puzzleFlow,
refreshPuzzleShelf,
setPuzzleError,
setSelectionStage,
],
);
const startBigFishRunFromWork = useCallback(
@@ -2649,6 +2922,7 @@ export function PlatformEntryFlowShellImpl({
workId: `big-fish:${sessionId}`,
sourceSessionId: sessionId,
ownerUserId: work.ownerUserId ?? '',
authorDisplayName: work.worldSubtitle || '玩家',
title: work.worldTitle,
subtitle: work.worldSubtitle,
summary: work.worldSubtitle,
@@ -2977,6 +3251,7 @@ export function PlatformEntryFlowShellImpl({
<PlatformWorkDetailView
entry={selectedPublicWorkDetail}
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
isBusy={isPublicWorkDetailBusy || isPuzzleBusy || isBigFishBusy}
error={publicWorkDetailError}
onBack={() => {
@@ -2984,6 +3259,9 @@ export function PlatformEntryFlowShellImpl({
clearSelectedPublicWorkAuthor();
setSelectionStage('platform');
}}
onLike={() => {
likePublicWork(selectedPublicWorkDetail);
}}
onStart={startSelectedPublicWork}
onRemix={remixSelectedPublicWork}
/>
@@ -3008,6 +3286,7 @@ export function PlatformEntryFlowShellImpl({
<PlatformWorkDetailView
entry={mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)}
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
isBusy={detailNavigation.isMutatingDetail}
error={detailNavigation.detailError}
onBack={() => {
@@ -3015,6 +3294,11 @@ export function PlatformEntryFlowShellImpl({
clearSelectedPublicWorkAuthor();
entryNavigation.backToPlatformHome();
}}
onLike={() => {
likePublicWork(
mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry),
);
}}
onStart={handleStartSelectedWorld}
onRemix={() => {
remixPublicWork(
@@ -3290,6 +3574,9 @@ export function PlatformEntryFlowShellImpl({
onCreateFromForm={(payload) => {
void createPuzzleDraftFromForm(payload);
}}
onAutoSaveForm={(payload) => {
void savePuzzleFormDraft(payload);
}}
/>
</Suspense>
</motion.div>
@@ -3312,6 +3599,7 @@ export function PlatformEntryFlowShellImpl({
}
anchorEntries={buildPuzzleGenerationAnchorEntries(
puzzleSession,
puzzleFormDraftPayload,
)}
progress={buildMiniGameDraftGenerationProgress(
puzzleGenerationState,
@@ -3344,7 +3632,9 @@ export function PlatformEntryFlowShellImpl({
</motion.div>
)}
{selectionStage === 'puzzle-result' && puzzleSession?.draft && (
{selectionStage === 'puzzle-result' &&
puzzleSession?.draft &&
!isPuzzleFormOnlyDraft(puzzleSession) && (
<motion.div
key="puzzle-result"
initial={{ opacity: 0, y: 12 }}
@@ -3717,9 +4007,7 @@ export function PlatformEntryFlowShellImpl({
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
// RPG 创作入口当前为敬请期待;保留回调防御,避免旧入口绕过锁定态。
}}
onSelectBigFish={() => {
runProtectedAction(() => {