Increase VectorEngine timeouts and add image UI

Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
This commit is contained in:
2026-05-15 02:40:59 +08:00
parent 4642855fd0
commit 74fd9a33ac
87 changed files with 5508 additions and 1261 deletions

View File

@@ -473,6 +473,8 @@ const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
BACKGROUND_AUTH_REQUEST_OPTIONS;
const PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS =
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
const rawTime = entry.publishedAt ?? entry.updatedAt;
@@ -1576,6 +1578,7 @@ function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
return collectDraftNoticeKeys('baby-object-match', [
item.id,
item.source.item.profileId,
item.source.item.draftId,
]);
}
}
@@ -1588,6 +1591,15 @@ function isMiniGameDraftGenerating(state: MiniGameDraftGenerationState | null) {
return Boolean(state && state.phase !== 'ready' && state.phase !== 'failed');
}
function resolveProfileWalletBalance(
dashboard: { walletBalance?: number | null } | null | undefined,
) {
const walletBalance = dashboard?.walletBalance;
return typeof walletBalance === 'number' && Number.isFinite(walletBalance)
? Math.max(0, Math.floor(walletBalance))
: 0;
}
function buildPendingBigFishWorks(
pending: Record<string, PendingDraftShelfState> | undefined,
existingItems: readonly BigFishWorkSummary[],
@@ -1774,6 +1786,7 @@ function buildPuzzleCompileActionFromFormPayload(
promptText: pictureDescription,
...(pictureDescription ? { pictureDescription } : {}),
referenceImageSrc: payload?.referenceImageSrc || null,
referenceImageSrcs: payload?.referenceImageSrcs ?? [],
imageModel: payload?.imageModel ?? null,
aiRedraw: payload?.aiRedraw ?? true,
candidateCount: 1,
@@ -1795,6 +1808,7 @@ function buildPuzzleFormPayloadFromSession(
seedText: pictureDescription,
pictureDescription,
referenceImageSrc: null,
referenceImageSrcs: [],
imageModel: null,
aiRedraw: true,
};
@@ -1824,6 +1838,7 @@ function buildPuzzleFormPayloadFromAction(
payload.action === 'compile_puzzle_draft'
? (payload.referenceImageSrc ?? null)
: (payload.referenceImageSrc ?? null),
referenceImageSrcs: payload.referenceImageSrcs ?? [],
imageModel:
payload.action === 'compile_puzzle_draft'
? (payload.imageModel ?? null)
@@ -2649,6 +2664,35 @@ export function PlatformEntryFlowShellImpl({
},
[draftGenerationNotices],
);
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
async (pointsCost: number, setError: (message: string | null) => void) => {
try {
const latestDashboard = await getPlatformProfileDashboard(
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
);
platformBootstrap.setProfileDashboard(latestDashboard);
const walletBalance = resolveProfileWalletBalance(latestDashboard);
if (walletBalance >= pointsCost) {
return true;
}
setError(
`泥点不足,本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
);
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
return false;
} catch {
setError('读取泥点余额失败,请稍后重试。');
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
return false;
}
},
[enterCreateTab, platformBootstrap, setSelectionStage],
);
const resolveBigFishErrorMessage = useCallback(
(error: unknown, fallback: string) =>
@@ -3262,8 +3306,15 @@ export function PlatformEntryFlowShellImpl({
...visualNovelShelfItems.flatMap((item) =>
collectDraftNoticeKeys('visual-novel', [item.profileId]),
),
...babyObjectMatchDrafts.flatMap((item) =>
collectDraftNoticeKeys('baby-object-match', [
item.profileId,
item.draftId,
]),
),
],
[
babyObjectMatchDrafts,
bigFishShelfItems,
creationHubItems,
isSquareHoleCreationVisible,
@@ -4318,6 +4369,28 @@ export function PlatformEntryFlowShellImpl({
const persistRpgAgentUiState = sessionController.persistAgentUiState;
const resetAutoSaveTrackingToIdle =
autosaveCoordinator.resetAutoSaveTrackingToIdle;
const preflightPuzzleDraftGeneration = useCallback(async () => {
setPuzzleCreationError(null);
setPuzzleError(null);
return ensureEnoughDraftGenerationPointsFromServer(
PUZZLE_DRAFT_GENERATION_POINT_COST,
(message) => {
setPuzzleCreationError(message);
setPuzzleError(message);
},
);
}, [
ensureEnoughDraftGenerationPointsFromServer,
setPuzzleCreationError,
setPuzzleError,
]);
const preflightMatch3DDraftGeneration = useCallback(async () => {
setMatch3DError(null);
return ensureEnoughDraftGenerationPointsFromServer(
MATCH3D_DRAFT_GENERATION_POINT_COST,
setMatch3DError,
);
}, [ensureEnoughDraftGenerationPointsFromServer, setMatch3DError]);
const activeMatch3DGenerationSessionId =
selectionStage === 'match3d-generating'
@@ -4519,6 +4592,13 @@ export function PlatformEntryFlowShellImpl({
setPuzzleCreationError(null);
setPuzzleError(null);
if (
payload.aiRedraw !== false &&
!(await preflightPuzzleDraftGeneration())
) {
return;
}
let nextSession: PuzzleAgentSessionSnapshot;
try {
const response = await createPuzzleAgentSession(payload);
@@ -4669,6 +4749,7 @@ export function PlatformEntryFlowShellImpl({
markPendingDraftGenerating,
markPendingDraftReady,
isViewingPuzzleGeneration,
preflightPuzzleDraftGeneration,
puzzleFlow,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
@@ -4684,6 +4765,10 @@ export function PlatformEntryFlowShellImpl({
setStreamingMatch3DReplyText('');
setIsStreamingMatch3DReply(false);
if (!(await preflightMatch3DDraftGeneration())) {
return;
}
let nextSession: Match3DAgentSessionSnapshot;
try {
const response = await match3dCreationClient.createSession(payload);
@@ -4869,6 +4954,7 @@ export function PlatformEntryFlowShellImpl({
markDraftReady,
markPendingDraftGenerating,
markPendingDraftReady,
preflightMatch3DDraftGeneration,
refreshMatch3DShelf,
resolveMatch3DErrorMessage,
setIsStreamingMatch3DReply,
@@ -4970,6 +5056,7 @@ export function PlatformEntryFlowShellImpl({
setBabyObjectMatchGenerationState(
createMiniGameDraftGenerationState('baby-object-match'),
);
selectionStageRef.current = 'baby-object-match-generating';
setSelectionStage('baby-object-match-generating');
try {
@@ -4987,7 +5074,16 @@ export function PlatformEntryFlowShellImpl({
}
: current,
);
setSelectionStage('baby-object-match-result');
const openResult =
selectionStageRef.current === 'baby-object-match-generating';
markDraftReady(
'baby-object-match',
[response.draft.profileId, response.draft.draftId],
openResult,
);
if (openResult) {
setSelectionStage('baby-object-match-result');
}
} catch (error) {
const errorMessage = resolvePuzzleErrorMessage(
error,
@@ -5008,7 +5104,12 @@ export function PlatformEntryFlowShellImpl({
setIsBabyObjectMatchBusy(false);
}
},
[refreshBabyObjectMatchShelf, resolvePuzzleErrorMessage, setSelectionStage],
[
markDraftReady,
refreshBabyObjectMatchShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const savePuzzleFormDraft = useCallback(
@@ -5026,6 +5127,7 @@ export function PlatformEntryFlowShellImpl({
promptText: payload.pictureDescription ?? null,
pictureDescription: payload.pictureDescription ?? '',
referenceImageSrc: payload.referenceImageSrc ?? null,
referenceImageSrcs: payload.referenceImageSrcs ?? [],
imageModel: payload.imageModel ?? null,
aiRedraw: payload.aiRedraw ?? true,
});
@@ -5234,7 +5336,6 @@ export function PlatformEntryFlowShellImpl({
setShowCreationTypeModal(false);
setActiveCreationFormType('bark-battle');
setBarkBattleError(null);
setSelectionStage('bark-battle-config');
return;
}
@@ -5272,7 +5373,6 @@ export function PlatformEntryFlowShellImpl({
setMatch3DError,
setPuzzleCreationError,
setPuzzleError,
setSelectionStage,
setVisualNovelError,
],
);
@@ -5306,6 +5406,7 @@ export function PlatformEntryFlowShellImpl({
setBarkBattlePublishedConfig(null);
setBarkBattleError(null);
setIsBarkBattleBusy(false);
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [setSelectionStage]);
@@ -5361,6 +5462,7 @@ export function PlatformEntryFlowShellImpl({
setBabyObjectMatchGenerationPhase('generating');
setBabyObjectMatchError(null);
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [enterCreateTab, setSelectionStage]);
@@ -8316,19 +8418,24 @@ export function PlatformEntryFlowShellImpl({
const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => {
const noticeKeys = collectDraftNoticeKeys('puzzle', [
item.workId,
item.profileId,
item.sourceSessionId,
buildPuzzleResultWorkId(item.sourceSessionId),
buildPuzzleResultProfileId(item.sourceSessionId),
]);
const isMarkedGenerating = isDraftNoticeGenerating('puzzle', [
item.workId,
item.profileId,
item.sourceSessionId,
buildPuzzleResultWorkId(item.sourceSessionId),
buildPuzzleResultProfileId(item.sourceSessionId),
]);
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setSelectedPuzzleDetail(null);
markDraftNoticeSeen(
collectDraftNoticeKeys('puzzle', [
item.workId,
item.profileId,
item.sourceSessionId,
buildPuzzleResultWorkId(item.sourceSessionId),
buildPuzzleResultProfileId(item.sourceSessionId),
]),
);
if (!item.sourceSessionId?.trim()) {
if (item.publicationStatus === 'published') {
await openPuzzleDetail(item.profileId, { tab: 'create' });
@@ -8373,6 +8480,30 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isMarkedGenerating) {
try {
const { session: latestSession } = await getPuzzleAgentSession(
item.sourceSessionId,
);
puzzleFlow.setSession(latestSession);
setPuzzleFormDraftPayload(buildPuzzleFormPayloadFromSession(latestSession));
setPuzzleGenerationState(createMiniGameDraftGenerationState('puzzle'));
enterCreateTab();
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = item.sourceSessionId;
setSelectionStage('puzzle-generating');
return;
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '读取拼图创作草稿失败。'),
);
await refreshPuzzleShelf().catch(() => undefined);
return;
}
}
markDraftNoticeSeen(noticeKeys);
const restoredSession = await puzzleFlow.restoreDraft(
item.sourceSessionId,
);
@@ -8393,12 +8524,14 @@ export function PlatformEntryFlowShellImpl({
[
enterCreateTab,
getPuzzleBackgroundCompileTask,
isDraftNoticeGenerating,
markDraftNoticeSeen,
openPuzzleDetail,
puzzleFlow,
puzzleGenerationViewState,
puzzleSession?.sessionId,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setPuzzleError,
setSelectionStage,
],
@@ -8746,6 +8879,12 @@ export function PlatformEntryFlowShellImpl({
const openBabyObjectMatchDraft = useCallback(
(draft: BabyObjectMatchDraft) => {
markDraftNoticeSeen(
collectDraftNoticeKeys('baby-object-match', [
draft.profileId,
draft.draftId,
]),
);
setBabyObjectMatchDraft(draft);
setBabyObjectMatchFormPayload({
itemAName: draft.itemNames[0],
@@ -8757,7 +8896,7 @@ export function PlatformEntryFlowShellImpl({
enterCreateTab();
setSelectionStage('baby-object-match-result');
},
[enterCreateTab, setSelectionStage],
[enterCreateTab, markDraftNoticeSeen, setSelectionStage],
);
const startBigFishRunFromWork = useCallback(
@@ -10601,6 +10740,21 @@ export function PlatformEntryFlowShellImpl({
title={null}
/>
</Suspense>
) : activeCreationFormType === 'bark-battle' ? (
<Suspense
fallback={<LazyPanelFallback label="正在加载汪汪声浪创作..." />}
>
<BarkBattleConfigEditor
isBusy={isBarkBattleBusy}
error={barkBattleError}
onBack={leaveBarkBattleFlow}
onPublish={(payload) => {
void publishBarkBattleConfig(payload);
}}
showBackButton={false}
title={null}
/>
</Suspense>
) : (
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图创作..." />}
@@ -11182,6 +11336,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="素材生成中"
pausedBadgeLabel="素材生成已暂停"
idleBadgeLabel="等待返回工作区"
hideBatchModule
/>
</Suspense>
</motion.div>
@@ -11848,6 +12003,7 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
hideBatchModule
/>
</Suspense>
</motion.div>
@@ -12215,33 +12371,6 @@ export function PlatformEntryFlowShellImpl({
</motion.div>
)}
{selectionStage === 'bark-battle-config' && (
<motion.div
key="bark-battle-config"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载汪汪声浪配置..." />}
>
<BarkBattleConfigEditor
isBusy={isBarkBattleBusy}
onBack={leaveBarkBattleFlow}
onPublish={(payload) => {
void publishBarkBattleConfig(payload);
}}
/>
{barkBattleError ? (
<div className="platform-subpanel mx-auto mt-3 max-w-5xl rounded-2xl px-4 py-3 text-sm text-rose-200">
{barkBattleError}
</div>
) : null}
</Suspense>
</motion.div>
)}
{selectionStage === 'bark-battle-runtime' && barkBattlePublishedConfig && (
<motion.div
key="bark-battle-runtime"
@@ -12258,7 +12387,9 @@ export function PlatformEntryFlowShellImpl({
workId={barkBattlePublishedConfig.workId}
publishedConfig={barkBattlePublishedConfig}
onExit={() => {
setSelectionStage('bark-battle-config');
enterCreateTab();
setActiveCreationFormType('bark-battle');
setSelectionStage('platform');
}}
/>
</Suspense>

View File

@@ -31,7 +31,6 @@ export type SelectionStage =
| 'square-hole-generating'
| 'square-hole-result'
| 'square-hole-runtime'
| 'bark-battle-config'
| 'bark-battle-runtime'
| 'creative-agent-workspace'
| 'visual-novel-agent-workspace'