feat: workerize external generation
This commit is contained in:
@@ -112,6 +112,7 @@ import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets'
|
||||
import {
|
||||
buildPublicWorkStagePath,
|
||||
pushAppHistoryPath,
|
||||
resolvePathForSelectionStage,
|
||||
} from '../../routing/appPageRoutes';
|
||||
import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFoundRecovery';
|
||||
import {
|
||||
@@ -623,9 +624,143 @@ async function buildRecommendRuntimeAuthOptions(
|
||||
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
}
|
||||
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
|
||||
const PUZZLE_BACKGROUND_ACTION_POLL_INTERVAL_MS = 3000;
|
||||
const PUZZLE_BACKGROUND_ACTION_MAX_POLL_ATTEMPTS = 160;
|
||||
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
||||
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
||||
|
||||
function isPuzzleBackgroundAction(payload: PuzzleAgentActionRequest) {
|
||||
return (
|
||||
payload.action === 'generate_puzzle_images' ||
|
||||
payload.action === 'generate_puzzle_ui_background'
|
||||
);
|
||||
}
|
||||
|
||||
function findPuzzleActionLevel(
|
||||
session: PuzzleAgentSessionSnapshot | null | undefined,
|
||||
payload: PuzzleAgentActionRequest,
|
||||
) {
|
||||
const levels = session?.draft?.levels ?? [];
|
||||
const targetLevelId =
|
||||
'levelId' in payload ? payload.levelId?.trim() : undefined;
|
||||
if (targetLevelId) {
|
||||
return levels.find((level) => level.levelId === targetLevelId) ?? null;
|
||||
}
|
||||
|
||||
return levels[0] ?? null;
|
||||
}
|
||||
|
||||
function buildPuzzleGeneratedImageSignature(
|
||||
level: PuzzleDraftLevel | null | undefined,
|
||||
) {
|
||||
if (!level) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return [
|
||||
level.levelName,
|
||||
level.selectedCandidateId ?? '',
|
||||
level.coverImageSrc ?? '',
|
||||
level.coverAssetId ?? '',
|
||||
level.levelSceneImageSrc ?? '',
|
||||
level.levelSceneImageObjectKey ?? '',
|
||||
level.uiSpritesheetImageSrc ?? '',
|
||||
level.uiSpritesheetImageObjectKey ?? '',
|
||||
level.levelBackgroundImageSrc ?? '',
|
||||
level.levelBackgroundImageObjectKey ?? '',
|
||||
(level.candidates ?? [])
|
||||
.map((candidate) =>
|
||||
[
|
||||
candidate.candidateId,
|
||||
candidate.imageSrc,
|
||||
candidate.assetId,
|
||||
String(candidate.selected),
|
||||
].join(':'),
|
||||
)
|
||||
.join('|'),
|
||||
].join('::');
|
||||
}
|
||||
|
||||
function buildPuzzleUiBackgroundSignature(
|
||||
level: PuzzleDraftLevel | null | undefined,
|
||||
) {
|
||||
if (!level) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return [
|
||||
level.uiBackgroundPrompt ?? '',
|
||||
level.uiBackgroundImageSrc ?? '',
|
||||
level.uiBackgroundImageObjectKey ?? '',
|
||||
].join('::');
|
||||
}
|
||||
|
||||
function buildPuzzleBackgroundActionSignature(
|
||||
payload: PuzzleAgentActionRequest,
|
||||
level: PuzzleDraftLevel | null | undefined,
|
||||
) {
|
||||
if (payload.action === 'generate_puzzle_ui_background') {
|
||||
return buildPuzzleUiBackgroundSignature(level);
|
||||
}
|
||||
|
||||
return buildPuzzleGeneratedImageSignature(level);
|
||||
}
|
||||
|
||||
function hasPuzzleBackgroundActionAsset(
|
||||
payload: PuzzleAgentActionRequest,
|
||||
level: PuzzleDraftLevel | null | undefined,
|
||||
) {
|
||||
if (!level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.action === 'generate_puzzle_ui_background') {
|
||||
return Boolean(level.uiBackgroundImageSrc?.trim());
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
level.coverImageSrc?.trim() ||
|
||||
level.candidates?.some((candidate) => candidate.imageSrc.trim()),
|
||||
);
|
||||
}
|
||||
|
||||
function isPuzzleBackgroundActionSettled(
|
||||
payload: PuzzleAgentActionRequest,
|
||||
baselineSession: PuzzleAgentSessionSnapshot,
|
||||
latestSession: PuzzleAgentSessionSnapshot,
|
||||
) {
|
||||
const latestLevel = findPuzzleActionLevel(latestSession, payload);
|
||||
if (!latestLevel) {
|
||||
return false;
|
||||
}
|
||||
if (latestLevel.generationStatus === 'failed') {
|
||||
return true;
|
||||
}
|
||||
if (latestLevel.generationStatus === 'generating') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baselineSignature = buildPuzzleBackgroundActionSignature(
|
||||
payload,
|
||||
findPuzzleActionLevel(baselineSession, payload),
|
||||
);
|
||||
const latestSignature = buildPuzzleBackgroundActionSignature(
|
||||
payload,
|
||||
latestLevel,
|
||||
);
|
||||
|
||||
return (
|
||||
hasPuzzleBackgroundActionAsset(payload, latestLevel) &&
|
||||
latestSignature !== baselineSignature
|
||||
);
|
||||
}
|
||||
|
||||
function waitForPuzzleBackgroundActionPollTick() {
|
||||
return new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, PUZZLE_BACKGROUND_ACTION_POLL_INTERVAL_MS);
|
||||
});
|
||||
}
|
||||
|
||||
function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
|
||||
const rawTime = entry.publishedAt ?? entry.updatedAt;
|
||||
const timestamp = new Date(rawTime).getTime();
|
||||
@@ -2361,7 +2496,9 @@ function buildMatch3DFormPayloadFromSession(
|
||||
seedText: themeText,
|
||||
themeText,
|
||||
referenceImageSrc:
|
||||
session.config?.referenceImageSrc ?? session.draft?.referenceImageSrc ?? null,
|
||||
session.config?.referenceImageSrc ??
|
||||
session.draft?.referenceImageSrc ??
|
||||
null,
|
||||
clearCount:
|
||||
session.config?.clearCount ??
|
||||
session.draft?.clearCount ??
|
||||
@@ -2579,6 +2716,22 @@ function hasRecoverableGeneratedPuzzleDraft(
|
||||
);
|
||||
}
|
||||
|
||||
function hasFailedPuzzleDraftGeneration(session: PuzzleAgentSessionSnapshot) {
|
||||
const draft = session.draft;
|
||||
if (!draft) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
isPersistedDraftFailed(draft.generationStatus) ||
|
||||
Boolean(
|
||||
draft.levels?.some((level) =>
|
||||
isPersistedDraftFailed(level.generationStatus),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem): string[] {
|
||||
switch (item.source.kind) {
|
||||
case 'rpg':
|
||||
@@ -3071,7 +3224,8 @@ function buildPuzzleCompileActionFromFormPayload(
|
||||
const pictureDescription =
|
||||
payload?.pictureDescription?.trim() || payload?.seedText?.trim();
|
||||
const workTitle = payload?.workTitle?.trim();
|
||||
const workDescription = payload?.workDescription?.trim() || pictureDescription;
|
||||
const workDescription =
|
||||
payload?.workDescription?.trim() || pictureDescription;
|
||||
|
||||
return {
|
||||
action: 'compile_puzzle_draft',
|
||||
@@ -3736,6 +3890,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<MiniGameDraftGenerationState | null>(null);
|
||||
const [puzzleBackgroundCompileTasks, setPuzzleBackgroundCompileTasks] =
|
||||
useState<Record<string, PuzzleBackgroundCompileTask>>({});
|
||||
const puzzleGenerationViewSnapshotRef = useRef<{
|
||||
payload: CreatePuzzleAgentSessionRequest | null;
|
||||
generationState: MiniGameDraftGenerationState | null;
|
||||
}>({ payload: null, generationState: null });
|
||||
const puzzleRuntimeReturnSessionRef =
|
||||
useRef<PuzzleAgentSessionSnapshot | null>(null);
|
||||
const [miniGameGenerationProgressNowMs, setMiniGameGenerationProgressNowMs] =
|
||||
useState(() => Date.now());
|
||||
const [puzzleFormDraftPayload, setPuzzleFormDraftPayload] =
|
||||
@@ -4109,6 +4269,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
viewedImmediately,
|
||||
);
|
||||
setProfileTaskRefreshKey((current) => current + 1);
|
||||
if (viewedImmediately) {
|
||||
setPendingPlatformTaskCompletionDialog(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const completedAtMs = Date.now();
|
||||
setPendingPlatformTaskCompletionDialog({
|
||||
key: `${kind}:${collectDraftNoticeKeys(kind, ids).join('|')}:${completedAtMs}`,
|
||||
@@ -4384,9 +4549,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
|
||||
async (pointsCost: number) => {
|
||||
try {
|
||||
const latestDashboard = await getPlatformProfileDashboardWithLocalWalletDelta(
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
);
|
||||
const latestDashboard =
|
||||
await getPlatformProfileDashboardWithLocalWalletDelta(
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
);
|
||||
platformBootstrap.setProfileDashboard(latestDashboard);
|
||||
const walletBalance = resolveProfileWalletBalance(latestDashboard);
|
||||
if (walletBalance >= pointsCost) {
|
||||
@@ -5916,7 +6082,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
markPendingDraftFailed('match3d', session.sessionId);
|
||||
markDraftFailed(
|
||||
'match3d',
|
||||
[session.draft?.profileId, session.publishedProfileId, session.sessionId],
|
||||
[
|
||||
session.draft?.profileId,
|
||||
session.publishedProfileId,
|
||||
session.sessionId,
|
||||
],
|
||||
errorMessage,
|
||||
);
|
||||
try {
|
||||
@@ -6198,7 +6368,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
markPendingDraftFailed('square-hole', session.sessionId);
|
||||
markDraftFailed(
|
||||
'square-hole',
|
||||
[session.draft?.profileId, session.publishedProfileId, session.sessionId],
|
||||
[
|
||||
session.draft?.profileId,
|
||||
session.publishedProfileId,
|
||||
session.sessionId,
|
||||
],
|
||||
errorMessage,
|
||||
);
|
||||
void refreshSquareHoleShelf().catch(() => undefined);
|
||||
@@ -6275,6 +6449,81 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
if (payload.action === 'compile_puzzle_draft') {
|
||||
const openResult = selectionStageRef.current === 'puzzle-generating';
|
||||
if (response.operation.status !== 'completed') {
|
||||
const nextPayload =
|
||||
formPayload ?? buildPuzzleFormPayloadFromSession(response.session);
|
||||
activePuzzleGenerationSessionIdRef.current =
|
||||
response.session.sessionId;
|
||||
selectionStageRef.current = 'puzzle-generating';
|
||||
setSelectionStage('puzzle-generating');
|
||||
|
||||
if (response.operation.status === 'failed') {
|
||||
const errorMessage =
|
||||
response.operation.error ?? '拼图草稿生成失败,请稍后再试。';
|
||||
const failedGenerationState =
|
||||
resolveFinishedMiniGameDraftGenerationState(
|
||||
createPuzzleDraftGenerationStateFromPayload(
|
||||
nextPayload,
|
||||
response.session,
|
||||
),
|
||||
'failed',
|
||||
{ error: errorMessage },
|
||||
);
|
||||
setPuzzleBackgroundCompileTasks((current) => ({
|
||||
...current,
|
||||
[response.session.sessionId]: {
|
||||
session: response.session,
|
||||
payload: nextPayload,
|
||||
generationState: failedGenerationState,
|
||||
error: errorMessage,
|
||||
},
|
||||
}));
|
||||
setPuzzleGenerationState(failedGenerationState);
|
||||
markPendingDraftFailed('puzzle', response.session.sessionId);
|
||||
markDraftFailed(
|
||||
'puzzle',
|
||||
[
|
||||
response.session.sessionId,
|
||||
buildPuzzleResultWorkId(response.session.sessionId),
|
||||
response.session.publishedProfileId,
|
||||
buildPuzzleResultProfileId(response.session.sessionId),
|
||||
],
|
||||
errorMessage,
|
||||
);
|
||||
void refreshPuzzleShelf();
|
||||
return { openResult: false };
|
||||
}
|
||||
|
||||
const generatingState = mergePuzzleSessionProgressIntoGenerationState(
|
||||
createPuzzleDraftGenerationStateFromPayload(
|
||||
nextPayload,
|
||||
response.session,
|
||||
),
|
||||
response.session,
|
||||
);
|
||||
setPuzzleGenerationState(generatingState);
|
||||
setPuzzleBackgroundCompileTasks((current) => ({
|
||||
...current,
|
||||
[response.session.sessionId]: {
|
||||
session: response.session,
|
||||
payload: nextPayload,
|
||||
generationState: generatingState,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
markDraftGenerating('puzzle', [
|
||||
response.session.sessionId,
|
||||
buildPuzzleResultWorkId(response.session.sessionId),
|
||||
response.session.publishedProfileId,
|
||||
buildPuzzleResultProfileId(response.session.sessionId),
|
||||
]);
|
||||
markPendingDraftGenerating(
|
||||
'puzzle',
|
||||
response.session.sessionId,
|
||||
buildPendingPuzzleDraftMetadata(nextPayload),
|
||||
);
|
||||
return { openResult: false };
|
||||
}
|
||||
setPuzzleGenerationState((current) =>
|
||||
current
|
||||
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
|
||||
@@ -6328,6 +6577,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleRun(run);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleRuntimeReturnStage('puzzle-result');
|
||||
puzzleRuntimeReturnSessionRef.current = response.session;
|
||||
openPuzzleRuntimeStage(
|
||||
setSelectionStage,
|
||||
buildPuzzleDraftRuntimeUrlState(item, null),
|
||||
@@ -6756,6 +7006,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
activePuzzleBackgroundCompileTask?.payload ?? puzzleFormDraftPayload;
|
||||
const puzzleGenerationViewError =
|
||||
activePuzzleBackgroundCompileTask?.error ?? puzzleError;
|
||||
puzzleGenerationViewSnapshotRef.current = {
|
||||
payload: puzzleGenerationViewPayload,
|
||||
generationState: puzzleGenerationViewState,
|
||||
};
|
||||
const isPuzzleGenerationViewBusy =
|
||||
isPuzzleBusy ||
|
||||
isMiniGameDraftGenerating(
|
||||
@@ -7170,6 +7424,60 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
|
||||
setPuzzleSession(latestSession);
|
||||
const snapshot = puzzleGenerationViewSnapshotRef.current;
|
||||
const pollPayload =
|
||||
snapshot.payload ?? buildPuzzleFormPayloadFromSession(latestSession);
|
||||
const pollGenerationState =
|
||||
snapshot.generationState ??
|
||||
createPuzzleDraftGenerationStateFromPayload(
|
||||
pollPayload,
|
||||
latestSession,
|
||||
);
|
||||
if (hasRecoverableGeneratedPuzzleDraft(latestSession)) {
|
||||
await recoverCompletedPuzzleDraftGeneration({
|
||||
sessionId: activePuzzleGenerationSessionId,
|
||||
payload: pollPayload,
|
||||
generationState: pollGenerationState,
|
||||
setSession: setPuzzleSession,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (hasFailedPuzzleDraftGeneration(latestSession)) {
|
||||
const errorMessage =
|
||||
latestSession.lastAssistantReply?.trim() ||
|
||||
'拼图草稿生成失败,请稍后再试。';
|
||||
const failedGenerationState =
|
||||
resolveFinishedMiniGameDraftGenerationState(
|
||||
pollGenerationState,
|
||||
'failed',
|
||||
{ error: errorMessage },
|
||||
);
|
||||
setPuzzleBackgroundCompileTasks((current) => ({
|
||||
...current,
|
||||
[activePuzzleGenerationSessionId]: {
|
||||
session: latestSession,
|
||||
payload: pollPayload,
|
||||
generationState: failedGenerationState,
|
||||
error: errorMessage,
|
||||
},
|
||||
}));
|
||||
setPuzzleGenerationState(failedGenerationState);
|
||||
markPendingDraftFailed('puzzle', activePuzzleGenerationSessionId);
|
||||
markDraftFailed(
|
||||
'puzzle',
|
||||
[
|
||||
latestSession.sessionId,
|
||||
buildPuzzleResultWorkId(latestSession.sessionId),
|
||||
latestSession.publishedProfileId,
|
||||
buildPuzzleResultProfileId(latestSession.sessionId),
|
||||
],
|
||||
errorMessage,
|
||||
);
|
||||
puzzleErrorSetterRef.current(errorMessage);
|
||||
void refreshPuzzleShelf();
|
||||
refreshPlatformDashboardSilently();
|
||||
return;
|
||||
}
|
||||
setPuzzleBackgroundCompileTasks((current) => {
|
||||
const task = current[activePuzzleGenerationSessionId];
|
||||
if (!task) {
|
||||
@@ -7212,6 +7520,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
};
|
||||
}, [
|
||||
activePuzzleGenerationSessionId,
|
||||
markDraftFailed,
|
||||
markPendingDraftFailed,
|
||||
recoverCompletedPuzzleDraftGeneration,
|
||||
refreshPlatformDashboardSilently,
|
||||
refreshPuzzleShelf,
|
||||
shouldPollPuzzleGenerationSession,
|
||||
setPuzzleSession,
|
||||
]);
|
||||
@@ -7561,7 +7874,79 @@ export function PlatformEntryFlowShellImpl({
|
||||
actionPayload,
|
||||
);
|
||||
setPuzzleOperation(response.operation);
|
||||
const openResult = isViewingPuzzleGeneration(nextSession.sessionId);
|
||||
activePuzzleGenerationSessionIdRef.current = response.session.sessionId;
|
||||
selectionStageRef.current = 'puzzle-generating';
|
||||
const openResult = selectionStageRef.current === 'puzzle-generating';
|
||||
if (response.operation.status !== 'completed') {
|
||||
if (response.operation.status === 'failed') {
|
||||
const errorMessage =
|
||||
response.operation.error ?? '拼图草稿生成失败,请稍后再试。';
|
||||
const failedGenerationState =
|
||||
resolveFinishedMiniGameDraftGenerationState(
|
||||
generationState,
|
||||
'failed',
|
||||
{ error: errorMessage },
|
||||
);
|
||||
setPuzzleBackgroundCompileTasks((current) => ({
|
||||
...current,
|
||||
[response.session.sessionId]: {
|
||||
session: response.session,
|
||||
payload,
|
||||
generationState: failedGenerationState,
|
||||
error: errorMessage,
|
||||
},
|
||||
}));
|
||||
markPendingDraftFailed('puzzle', response.session.sessionId);
|
||||
markDraftFailed(
|
||||
'puzzle',
|
||||
[
|
||||
response.session.sessionId,
|
||||
buildPuzzleResultWorkId(response.session.sessionId),
|
||||
response.session.publishedProfileId,
|
||||
buildPuzzleResultProfileId(response.session.sessionId),
|
||||
],
|
||||
errorMessage,
|
||||
!openResult,
|
||||
);
|
||||
void refreshPuzzleShelf();
|
||||
if (openResult) {
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleError(errorMessage);
|
||||
setPuzzleGenerationState(failedGenerationState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const generatingState = mergePuzzleSessionProgressIntoGenerationState(
|
||||
generationState,
|
||||
response.session,
|
||||
);
|
||||
setPuzzleBackgroundCompileTasks((current) => ({
|
||||
...current,
|
||||
[response.session.sessionId]: {
|
||||
session: response.session,
|
||||
payload,
|
||||
generationState: generatingState,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
markDraftGenerating('puzzle', [
|
||||
response.session.sessionId,
|
||||
buildPuzzleResultWorkId(response.session.sessionId),
|
||||
response.session.publishedProfileId,
|
||||
buildPuzzleResultProfileId(response.session.sessionId),
|
||||
]);
|
||||
markPendingDraftGenerating(
|
||||
'puzzle',
|
||||
response.session.sessionId,
|
||||
buildPendingPuzzleDraftMetadata(payload),
|
||||
);
|
||||
if (openResult) {
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleGenerationState(generatingState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const readyGenerationState =
|
||||
resolveFinishedMiniGameDraftGenerationState(
|
||||
generationState,
|
||||
@@ -7580,8 +7965,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
puzzleFlow.setSession(response.session);
|
||||
if (isViewingPuzzleGeneration(nextSession.sessionId)) {
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleGenerationState(readyGenerationState);
|
||||
}
|
||||
|
||||
@@ -7631,6 +8016,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleRun(run);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleRuntimeReturnStage('puzzle-result');
|
||||
puzzleRuntimeReturnSessionRef.current = response.session;
|
||||
openPuzzleRuntimeStage(
|
||||
setSelectionStage,
|
||||
buildPuzzleDraftRuntimeUrlState(item, null),
|
||||
@@ -10295,6 +10681,67 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const executePuzzleAction = puzzleFlow.executeAction;
|
||||
|
||||
const pollPuzzleBackgroundActionUntilSettled = useCallback(
|
||||
(
|
||||
payload: PuzzleAgentActionRequest,
|
||||
baselineSession: PuzzleAgentSessionSnapshot,
|
||||
) => {
|
||||
if (!isPuzzleBackgroundAction(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < PUZZLE_BACKGROUND_ACTION_MAX_POLL_ATTEMPTS;
|
||||
attempt += 1
|
||||
) {
|
||||
await waitForPuzzleBackgroundActionPollTick();
|
||||
try {
|
||||
const response = await getPuzzleAgentSession(
|
||||
baselineSession.sessionId,
|
||||
);
|
||||
puzzleFlow.setSession(response.session);
|
||||
if (
|
||||
isPuzzleBackgroundActionSettled(
|
||||
payload,
|
||||
baselineSession,
|
||||
response.session,
|
||||
)
|
||||
) {
|
||||
refreshPlatformDashboardSilently();
|
||||
await Promise.allSettled([
|
||||
refreshPuzzleShelf(),
|
||||
refreshPuzzleGallery(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} catch (pollError) {
|
||||
if (attempt >= 2) {
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(
|
||||
pollError,
|
||||
'刷新拼图图片生成结果失败。',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPuzzleError('拼图图片仍在后台生成,请稍后刷新草稿查看结果。');
|
||||
})();
|
||||
},
|
||||
[
|
||||
puzzleFlow,
|
||||
refreshPlatformDashboardSilently,
|
||||
refreshPuzzleGallery,
|
||||
refreshPuzzleShelf,
|
||||
resolvePuzzleErrorMessage,
|
||||
setPuzzleError,
|
||||
],
|
||||
);
|
||||
|
||||
const executePuzzleBackgroundAction = useCallback(
|
||||
async (payload: PuzzleAgentActionRequest) => {
|
||||
const targetSession = puzzleSession;
|
||||
@@ -10315,6 +10762,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
setPuzzleOperation(response.operation);
|
||||
puzzleFlow.setSession(response.session);
|
||||
if (
|
||||
isPuzzleBackgroundAction(payload) &&
|
||||
(response.operation.status === 'queued' ||
|
||||
response.operation.status === 'running')
|
||||
) {
|
||||
pollPuzzleBackgroundActionUntilSettled(payload, targetSession);
|
||||
}
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '执行拼图操作失败。'));
|
||||
} finally {
|
||||
@@ -10329,6 +10783,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[
|
||||
puzzleFlow,
|
||||
puzzleSession,
|
||||
pollPuzzleBackgroundActionUntilSettled,
|
||||
refreshPlatformDashboardSilently,
|
||||
resolvePuzzleErrorMessage,
|
||||
setPuzzleError,
|
||||
@@ -11010,6 +11465,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleRun(run);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleRuntimeReturnStage('puzzle-result');
|
||||
puzzleRuntimeReturnSessionRef.current =
|
||||
puzzleSession?.draft && !isPuzzleFormOnlyDraft(puzzleSession)
|
||||
? puzzleSession
|
||||
: null;
|
||||
openPuzzleRuntimeStage(
|
||||
setSelectionStage,
|
||||
buildPuzzleDraftRuntimeUrlState(item, options.levelId ?? null),
|
||||
@@ -11033,6 +11492,21 @@ export function PlatformEntryFlowShellImpl({
|
||||
],
|
||||
);
|
||||
|
||||
const returnFromPuzzleRuntime = useCallback(() => {
|
||||
const targetStage = puzzleRuntimeReturnStage;
|
||||
if (
|
||||
targetStage === 'puzzle-result' &&
|
||||
(!puzzleSession?.draft || isPuzzleFormOnlyDraft(puzzleSession)) &&
|
||||
puzzleRuntimeReturnSessionRef.current
|
||||
) {
|
||||
puzzleFlow.setSession(puzzleRuntimeReturnSessionRef.current);
|
||||
}
|
||||
clearPuzzleRuntimeUrlState();
|
||||
pushAppHistoryPath(resolvePathForSelectionStage(targetStage));
|
||||
selectionStageRef.current = targetStage;
|
||||
setSelectionStage(targetStage);
|
||||
}, [puzzleFlow, puzzleRuntimeReturnStage, puzzleSession, setSelectionStage]);
|
||||
|
||||
const submitBigFishInput = useCallback(
|
||||
async (payload: SubmitBigFishInputRequest) => {
|
||||
if (
|
||||
@@ -17971,7 +18445,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('visual-novel')}
|
||||
onBack={leaveVisualNovelFlow}
|
||||
isBackDisabled={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||||
isBackDisabled={
|
||||
isVisualNovelBusy || isVisualNovelStreamingReply
|
||||
}
|
||||
>
|
||||
<VisualNovelAgentWorkspace
|
||||
session={visualNovelSession}
|
||||
@@ -18205,9 +18681,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
error={puzzleError}
|
||||
hideBackButton={Boolean(puzzleOnboardingDraft)}
|
||||
onBack={() => {
|
||||
setSelectionStage(puzzleRuntimeReturnStage);
|
||||
}}
|
||||
onBack={returnFromPuzzleRuntime}
|
||||
onRemodelWork={
|
||||
selectedPuzzleDetail?.publicationStatus === 'published'
|
||||
? remodelCurrentPuzzleRuntimeWork
|
||||
|
||||
@@ -4769,6 +4769,60 @@ test('running puzzle form generation creates a new puzzle draft on same template
|
||||
});
|
||||
});
|
||||
|
||||
test('queued puzzle form generation stays on generation progress', async () => {
|
||||
const user = userEvent.setup();
|
||||
const queuedSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-queued-session-1',
|
||||
progressPercent: 5,
|
||||
lastAssistantReply: '拼图生成任务已进入后台队列。',
|
||||
});
|
||||
vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: queuedSession,
|
||||
});
|
||||
vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
|
||||
operation: {
|
||||
operationId: 'compile-puzzle-queued-1',
|
||||
type: 'compile_puzzle_draft',
|
||||
status: 'queued',
|
||||
phaseLabel: '已进入后台队列',
|
||||
phaseDetail: '拼图草稿生成已进入后台队列。',
|
||||
progress: 5,
|
||||
error: null,
|
||||
},
|
||||
session: queuedSession,
|
||||
});
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValue({
|
||||
session: {
|
||||
...queuedSession,
|
||||
progressPercent: 12,
|
||||
},
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('拼图'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
const progressbar = await screen.findByRole('progressbar', {
|
||||
name: '拼图图片生成进度',
|
||||
});
|
||||
expect(progressbar).toBeTruthy();
|
||||
expect(updatePuzzleWork).not.toHaveBeenCalled();
|
||||
expect(startLocalPuzzleRun).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
expect(window.location.pathname).not.toBe('/runtime/puzzle');
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('saves')).getAllByRole('button', {
|
||||
name: /继续创作《[^》]+》,生成中/u,
|
||||
}).length,
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('failed parallel puzzle generations stay as separate non-generating drafts', async () => {
|
||||
const user = userEvent.setup();
|
||||
const firstSession = buildMockPuzzleAgentSession({
|
||||
|
||||
Reference in New Issue
Block a user