1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-14 21:49:44 +08:00
parent fa435aa6a6
commit 6363267bca
13 changed files with 2743 additions and 237 deletions

View File

@@ -10,6 +10,12 @@ import {
} from 'react';
import type { JsonObject } from '../../../packages/shared/src/contracts/common';
import type {
CustomWorldAgentActionRequest,
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
SendCustomWorldAgentMessageRequest,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type {
CustomWorldGalleryCard,
CustomWorldGenerationProgress,
@@ -17,7 +23,24 @@ import type {
} from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import { generateCustomWorldProfile } from '../../services/aiService';
import {
createCustomWorldAgentSession,
executeCustomWorldAgentAction,
generateCustomWorldProfile,
getCustomWorldAgentOperation,
getCustomWorldAgentSession,
sendCustomWorldAgentMessage,
} from '../../services/aiService';
import {
readCustomWorldAgentUiState,
writeCustomWorldAgentUiState,
} from '../../services/customWorldAgentUiState';
import {
buildAgentDraftFoundationGenerationProgress,
buildAgentDraftFoundationSettingText,
isDraftFoundationOperation,
isDraftFoundationOperationRunning,
} from '../../services/customWorldAgentGenerationProgress';
import {
buildCustomWorldCreatorIntentDisplayText,
buildCustomWorldCreatorIntentGenerationText,
@@ -37,7 +60,8 @@ import {
type CustomWorldProfile,
type GameState,
} from '../../types';
import { PlatformHomeView } from './PlatformHomeView';
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
import { type PlatformHomeTab,PlatformHomeView } from './PlatformHomeView';
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
const CustomWorldGenerationView = lazy(async () => {
@@ -61,12 +85,27 @@ const CustomWorldCreatorModal = lazy(async () => {
};
});
const CustomWorldAgentWorkspace = lazy(async () => {
const module = await import(
'../custom-world-agent/CustomWorldAgentWorkspace'
);
return {
default: module.CustomWorldAgentWorkspace,
};
});
export type SelectionStage =
| 'platform'
| 'detail'
| 'agent-workspace'
| 'custom-world-generating'
| 'custom-world-result';
type CustomWorldGenerationViewSource =
| 'classic'
| 'agent-draft-foundation'
| null;
type PreGameSelectionFlowProps = {
selectionStage: SelectionStage;
setSelectionStage: (stage: SelectionStage) => void;
@@ -151,6 +190,22 @@ function resolveErrorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
function createFailedAgentOperation(params: {
type: CustomWorldAgentOperationRecord['type'];
phaseLabel: string;
error: string;
}): CustomWorldAgentOperationRecord {
return {
operationId: `local-failed-${Date.now()}`,
type: params.type,
status: 'failed',
phaseLabel: params.phaseLabel,
phaseDetail: params.error,
progress: 100,
error: params.error,
};
}
function LazyPanelFallback({ label }: { label: string }) {
return (
<div className="flex h-full min-h-0 items-center justify-center">
@@ -170,6 +225,8 @@ export function PreGameSelectionFlow({
handleStartNewGame,
handleCustomWorldSelect,
}: PreGameSelectionFlowProps) {
const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState());
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
useState<CustomWorldProfile | null>(null);
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
@@ -178,8 +235,25 @@ export function PreGameSelectionFlow({
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
CustomWorldGalleryCard[]
>([]);
const [platformTab, setPlatformTab] = useState<PlatformHomeTab>('home');
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
const [creationTypeError, setCreationTypeError] = useState<string | null>(
null,
);
const [isCreatingAgentSession, setIsCreatingAgentSession] = useState(false);
const [activeAgentSessionId, setActiveAgentSessionId] = useState<
string | null
>(() => initialAgentUiStateRef.current.activeSessionId ?? null);
const [activeAgentOperationId, setActiveAgentOperationId] = useState<
string | null
>(() => initialAgentUiStateRef.current.activeOperationId ?? null);
const [agentSession, setAgentSession] =
useState<CustomWorldAgentSessionSnapshot | null>(null);
const [agentOperation, setAgentOperation] =
useState<CustomWorldAgentOperationRecord | null>(null);
const [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
const [showCustomWorldModal, setShowCustomWorldModal] = useState(false);
const [customWorldCreatorIntent, setCustomWorldCreatorIntent] =
useState<CustomWorldCreatorIntent>(() =>
@@ -196,6 +270,10 @@ export function PreGameSelectionFlow({
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
const [customWorldProgress, setCustomWorldProgress] =
useState<CustomWorldGenerationProgress | null>(null);
const [customWorldGenerationViewSource, setCustomWorldGenerationViewSource] =
useState<CustomWorldGenerationViewSource>(null);
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
useState<number | null>(null);
const customWorldAbortControllerRef = useRef<AbortController | null>(null);
const previewCustomWorldCharacters = useMemo(
@@ -211,6 +289,24 @@ export function PreGameSelectionFlow({
[publishedGalleryEntries],
);
const persistAgentUiState = useCallback(
(nextSessionId: string | null, nextOperationId: string | null) => {
setActiveAgentSessionId(nextSessionId);
setActiveAgentOperationId(nextOperationId);
writeCustomWorldAgentUiState({
activeSessionId: nextSessionId,
activeOperationId: nextOperationId,
});
},
[],
);
const syncAgentSessionSnapshot = useCallback(async (sessionId: string) => {
const nextSession = await getCustomWorldAgentSession(sessionId);
setAgentSession(nextSession);
return nextSession;
}, []);
const refreshPlatformData = useCallback(async () => {
setIsLoadingPlatform(true);
setPlatformError(null);
@@ -239,6 +335,18 @@ export function PreGameSelectionFlow({
}
}, [selectedDetailEntry]);
useEffect(() => {
if (hasAppliedInitialAgentWorkspaceRef.current) {
return;
}
hasAppliedInitialAgentWorkspaceRef.current = true;
if (initialAgentUiStateRef.current.activeSessionId) {
setPlatformTab('create');
setSelectionStage('agent-workspace');
}
}, [setSelectionStage]);
useEffect(() => {
let isActive = true;
@@ -293,6 +401,117 @@ export function PreGameSelectionFlow({
[],
);
useEffect(() => {
if (!activeAgentSessionId) {
setAgentSession(null);
setIsLoadingAgentSession(false);
return;
}
let cancelled = false;
setIsLoadingAgentSession(true);
void syncAgentSessionSnapshot(activeAgentSessionId)
.then(() => {
if (!cancelled) {
setCreationTypeError(null);
}
})
.catch((error) => {
if (cancelled) {
return;
}
setCreationTypeError(
resolveErrorMessage(error, '读取 Agent 共创工作区失败。'),
);
setAgentSession(null);
setAgentOperation(null);
persistAgentUiState(null, null);
setPlatformTab('create');
setSelectionStage('platform');
})
.finally(() => {
if (!cancelled) {
setIsLoadingAgentSession(false);
}
});
return () => {
cancelled = true;
};
}, [
activeAgentSessionId,
persistAgentUiState,
setSelectionStage,
syncAgentSessionSnapshot,
]);
useEffect(() => {
if (!activeAgentSessionId || !activeAgentOperationId) {
return;
}
let cancelled = false;
const pollOperation = async () => {
try {
const nextOperation = await getCustomWorldAgentOperation(
activeAgentSessionId,
activeAgentOperationId,
);
if (cancelled) {
return;
}
setAgentOperation(nextOperation);
if (
nextOperation.status === 'completed' ||
nextOperation.status === 'failed'
) {
persistAgentUiState(activeAgentSessionId, null);
await syncAgentSessionSnapshot(activeAgentSessionId).catch(
() => null,
);
}
} catch (error) {
if (cancelled) {
return;
}
const errorMessage = resolveErrorMessage(
error,
'读取共创操作状态失败。',
);
setAgentOperation(
createFailedAgentOperation({
type: 'process_message',
phaseLabel: '读取操作状态失败',
error: errorMessage,
}),
);
persistAgentUiState(activeAgentSessionId, null);
}
};
void pollOperation();
const intervalId = window.setInterval(() => {
void pollOperation();
}, 1200);
return () => {
cancelled = true;
window.clearInterval(intervalId);
};
}, [
activeAgentOperationId,
activeAgentSessionId,
persistAgentUiState,
syncAgentSessionSnapshot,
]);
const customWorldSettingPreview = useMemo(() => {
if (customWorldCreatorIntent.sourceMode === 'freeform') {
return customWorldCreatorIntent.rawSettingText.trim();
@@ -308,10 +527,40 @@ export function PreGameSelectionFlow({
return customWorldCreatorIntent.rawSettingText.trim();
}, [customWorldCreatorIntent]);
const agentDraftSettingPreview = useMemo(
() => buildAgentDraftFoundationSettingText(agentSession),
[agentSession],
);
const agentDraftGenerationProgress = useMemo(
() =>
buildAgentDraftFoundationGenerationProgress(
agentOperation,
agentDraftGenerationStartedAt,
),
[agentDraftGenerationStartedAt, agentOperation],
);
const isAgentDraftGenerationView =
customWorldGenerationViewSource === 'agent-draft-foundation';
const activeGenerationProgress = isAgentDraftGenerationView
? agentDraftGenerationProgress
: customWorldProgress;
const isActiveGenerationRunning = isAgentDraftGenerationView
? isDraftFoundationOperationRunning(agentOperation)
: isGeneratingCustomWorld;
const activeGenerationError =
isAgentDraftGenerationView &&
isDraftFoundationOperation(agentOperation) &&
agentOperation.status === 'failed'
? agentOperation.error || agentOperation.phaseDetail
: customWorldError;
const leaveCustomWorldResult = () => {
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
};
@@ -322,6 +571,101 @@ export function PreGameSelectionFlow({
setCustomWorldError(null);
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setSelectionStage('platform');
};
const openCreationTypePicker = () => {
if (isCreatingAgentSession) {
return;
}
setCreationTypeError(null);
setShowCreationTypeModal(true);
};
const openRpgAgentWorkspace = async () => {
if (isCreatingAgentSession) {
return;
}
setIsCreatingAgentSession(true);
setCreationTypeError(null);
try {
const { session } = await createCustomWorldAgentSession({});
setAgentSession(session);
setAgentOperation(null);
persistAgentUiState(session.sessionId, null);
setShowCreationTypeModal(false);
setPlatformTab('create');
setSelectionStage('agent-workspace');
} catch (error) {
setCreationTypeError(resolveErrorMessage(error, '开启共创工作台失败。'));
} finally {
setIsCreatingAgentSession(false);
}
};
const submitAgentMessage = async (
payload: SendCustomWorldAgentMessageRequest,
) => {
if (!activeAgentSessionId) {
return;
}
try {
const { operation } = await sendCustomWorldAgentMessage(
activeAgentSessionId,
payload,
);
setAgentOperation(operation);
persistAgentUiState(activeAgentSessionId, operation.operationId);
} catch (error) {
const errorMessage = resolveErrorMessage(error, '发送共创消息失败。');
setAgentOperation(
createFailedAgentOperation({
type: 'process_message',
phaseLabel: '发送消息失败',
error: errorMessage,
}),
);
persistAgentUiState(activeAgentSessionId, null);
}
};
const executeAgentAction = async (payload: CustomWorldAgentActionRequest) => {
if (!activeAgentSessionId) {
return;
}
try {
const { operation } = await executeCustomWorldAgentAction(
activeAgentSessionId,
payload,
);
setAgentOperation(operation);
persistAgentUiState(activeAgentSessionId, operation.operationId);
} catch (error) {
const errorMessage = resolveErrorMessage(error, '执行共创操作失败。');
setAgentOperation(
createFailedAgentOperation({
type:
payload.action === 'draft_foundation'
? 'draft_foundation'
: payload.action,
phaseLabel: '执行操作失败',
error: errorMessage,
}),
);
persistAgentUiState(activeAgentSessionId, null);
}
};
const leaveAgentWorkspace = () => {
setPlatformTab('create');
setAgentOperation(null);
persistAgentUiState(activeAgentSessionId, null);
setSelectionStage('platform');
};
@@ -340,7 +684,9 @@ export function PreGameSelectionFlow({
setDetailError(null);
setCustomWorldError(null);
setCustomWorldProgress(null);
setCustomWorldCreatorIntent(createEmptyCustomWorldCreatorIntent('freeform'));
setCustomWorldCreatorIntent(
createEmptyCustomWorldCreatorIntent('freeform'),
);
setCustomWorldGenerationMode('fast');
setShowCustomWorldModal(true);
};
@@ -400,7 +746,9 @@ export function PreGameSelectionFlow({
}
try {
const mutation = await upsertCustomWorldProfile(generatedCustomWorldProfile);
const mutation = await upsertCustomWorldProfile(
generatedCustomWorldProfile,
);
setSavedCustomWorldEntries(mutation.entries);
setSelectedDetailEntry(mutation.entry);
await refreshPlatformData();
@@ -684,18 +1032,20 @@ export function PreGameSelectionFlow({
className="flex h-full min-h-0 flex-col"
>
<PlatformHomeView
activeTab={platformTab}
onTabChange={setPlatformTab}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
featuredEntries={featuredGalleryEntries}
latestEntries={publishedGalleryEntries}
myEntries={savedCustomWorldEntries}
isLoadingPlatform={isLoadingPlatform}
platformError={isLoadingPlatform ? null : platformError}
platformError={
isLoadingPlatform ? null : (platformError ?? creationTypeError)
}
onContinueGame={handleContinueGame}
onRefresh={() => {
void refreshPlatformData();
}}
onOpenCreateWorld={openCustomWorldCreator}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={(entry) => {
void openGalleryDetail(entry);
}}
@@ -750,6 +1100,50 @@ export function PreGameSelectionFlow({
</motion.div>
)}
{selectionStage === 'agent-workspace' && (
<motion.div
key="agent-workspace"
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="正在加载 Agent 共创工作区..." />
}
>
{agentSession ? (
<CustomWorldAgentWorkspace
session={agentSession}
activeOperation={agentOperation}
onBack={leaveAgentWorkspace}
onRefresh={() => {
if (!activeAgentSessionId) {
return;
}
void syncAgentSessionSnapshot(activeAgentSessionId);
}}
onSubmitMessage={(payload) => {
void submitAgentMessage(payload);
}}
onExecuteAction={(payload) => {
void executeAgentAction(payload);
}}
/>
) : (
<div className="flex h-full items-center justify-center">
<div className="rounded-2xl border border-white/10 bg-black/30 px-5 py-4 text-sm text-zinc-300">
{isLoadingAgentSession
? '正在准备 Agent 共创工作区...'
: creationTypeError || '正在恢复创作工作区...'}
</div>
</div>
)}
</Suspense>
</motion.div>
)}
{selectionStage === 'custom-world-generating' && (
<motion.div
key="custom-world-generating"
@@ -759,9 +1153,7 @@ export function PreGameSelectionFlow({
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载世界生成面板..." />
}
fallback={<LazyPanelFallback label="正在加载世界生成面板..." />}
>
<CustomWorldGenerationView
settingText={customWorldSettingPreview}
@@ -816,6 +1208,21 @@ export function PreGameSelectionFlow({
)}
</AnimatePresence>
<PlatformCreationTypeModal
isOpen={showCreationTypeModal}
isBusy={isCreatingAgentSession}
error={creationTypeError}
onClose={() => {
if (isCreatingAgentSession) {
return;
}
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
void openRpgAgentWorkspace();
}}
/>
{showCustomWorldModal ? (
<Suspense fallback={null}>
<CustomWorldCreatorModal