1
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
|
||||
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import { PixelIcon } from './PixelIcon';
|
||||
|
||||
interface DeveloperTeamModalProps {
|
||||
isOpen: boolean;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DeveloperTeamModal({
|
||||
isOpen,
|
||||
message,
|
||||
onClose,
|
||||
}: DeveloperTeamModalProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[74] flex items-center justify-center bg-black/78 p-3 sm:p-4 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.96, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.96, y: 10 }}
|
||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||||
className="pixel-nine-slice pixel-modal-shell flex max-h-[min(92vh,48rem)] w-full max-w-[min(96vw,42rem)] flex-col overflow-hidden shadow-[0_28px_90px_rgba(0,0,0,0.58)]"
|
||||
style={getNineSliceStyle(UI_CHROME.modalPanel)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
|
||||
<div className="min-w-0">
|
||||
<div className="mt-1 text-sm font-semibold text-white">{'\u5f00\u53d1\u56e2\u961f'}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-full border border-white/10 bg-black/20 p-2 text-zinc-400 transition-colors hover:text-white"
|
||||
aria-label="关闭开发团队弹窗"
|
||||
>
|
||||
<PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-5">
|
||||
<div
|
||||
className="pixel-nine-slice pixel-panel flex flex-col items-center gap-5"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 16, paddingY: 16 })}
|
||||
>
|
||||
<div className="whitespace-pre-line text-center text-sm leading-7 text-zinc-100">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import {lazy, Suspense} from 'react';
|
||||
|
||||
import type {SkillEffectPreviewProps} from './SkillEffectPreview';
|
||||
|
||||
const SkillEffectPreview = lazy(async () => {
|
||||
const module = await import('./SkillEffectPreview');
|
||||
|
||||
return {
|
||||
default: module.SkillEffectPreview,
|
||||
};
|
||||
});
|
||||
|
||||
function SkillEffectPreviewFallback() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<div className="mb-3 space-y-2">
|
||||
<div className="h-4 w-28 rounded bg-white/10" />
|
||||
<div className="h-3 w-40 rounded bg-white/5" />
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-2xl border border-white/10 bg-black">
|
||||
<div className="h-[300px] animate-pulse bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_42%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LazySkillEffectPreview(props: SkillEffectPreviewProps) {
|
||||
return (
|
||||
<Suspense fallback={<SkillEffectPreviewFallback />}>
|
||||
<SkillEffectPreview {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import {
|
||||
buildBodyPath,
|
||||
buildMedievalAtlasSpec,
|
||||
buildRaceAssetPath,
|
||||
clampMedievalAtlasFrame,
|
||||
getMedievalAtlasOptions,
|
||||
getMedievalPoseOptions,
|
||||
MEDIEVAL_BODY_COLORS,
|
||||
type MedievalAtlasSourceType,
|
||||
type MedievalNpcVisualOverride,
|
||||
type MedievalRace,
|
||||
} from '../data/medievalNpcVisuals';
|
||||
import type { Encounter } from '../types';
|
||||
import { type NpcLayoutConfig, type NpcLayoutPart } from './npcVisualShared';
|
||||
|
||||
export type GearSourceType = 'none' | MedievalAtlasSourceType;
|
||||
|
||||
export type EditableNpcVisualState = {
|
||||
race: MedievalRace;
|
||||
bodyColor: string;
|
||||
headIndex: number;
|
||||
hairColorIndex: number;
|
||||
hairStyleFrame: number;
|
||||
facialHairEnabled: boolean;
|
||||
facialHairColorIndex: number;
|
||||
facialHairStyleFrame: number;
|
||||
headgearType: GearSourceType;
|
||||
headgearFile: string;
|
||||
headgearFrame: number;
|
||||
mainHandType: GearSourceType;
|
||||
mainHandFile: string;
|
||||
mainHandFrame: number;
|
||||
offHandType: GearSourceType;
|
||||
offHandFile: string;
|
||||
offHandFrame: number;
|
||||
};
|
||||
|
||||
export type EditorNpcOption = {
|
||||
encounter: Encounter;
|
||||
sceneNames: string[];
|
||||
};
|
||||
|
||||
const NPC_LAYOUT_PARTS: NpcLayoutPart[] = [
|
||||
'body',
|
||||
'head',
|
||||
'facialHair',
|
||||
'hair',
|
||||
'headgear',
|
||||
'hand',
|
||||
'mainHand',
|
||||
'offHand',
|
||||
];
|
||||
|
||||
export function sanitizeFrameSelection(
|
||||
type: GearSourceType,
|
||||
file: string,
|
||||
frame: number,
|
||||
usage: 'headgear' | 'mainHand' | 'offHand',
|
||||
) {
|
||||
if (type === 'none' || !file) return 0;
|
||||
const poseOptions = getMedievalPoseOptions(type, file, usage);
|
||||
if (poseOptions.length === 0) return 0;
|
||||
if (poseOptions.some(option => option.value === frame)) {
|
||||
return clampMedievalAtlasFrame(type, file, frame);
|
||||
}
|
||||
const firstOption = poseOptions[0];
|
||||
return firstOption ? firstOption.value : 0;
|
||||
}
|
||||
|
||||
export function getDefaultFileForType(type: GearSourceType) {
|
||||
if (type === 'none') return '';
|
||||
return getMedievalAtlasOptions(type)[0]?.file ?? '';
|
||||
}
|
||||
|
||||
export function getDefaultFrameForSelection(
|
||||
type: GearSourceType,
|
||||
file: string,
|
||||
usage: 'headgear' | 'mainHand' | 'offHand',
|
||||
) {
|
||||
if (type === 'none' || !file) return 0;
|
||||
return getMedievalPoseOptions(type, file, usage)[0]?.value ?? 0;
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function isNpcLayoutConfig(value: unknown): value is NpcLayoutConfig {
|
||||
return (
|
||||
isRecord(value)
|
||||
&& NPC_LAYOUT_PARTS.every(part => {
|
||||
const coordinate = value[part];
|
||||
return (
|
||||
isRecord(coordinate)
|
||||
&& typeof coordinate.x === 'number'
|
||||
&& Number.isFinite(coordinate.x)
|
||||
&& typeof coordinate.y === 'number'
|
||||
&& Number.isFinite(coordinate.y)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOverrideFromEditorState(
|
||||
state: EditableNpcVisualState,
|
||||
): MedievalNpcVisualOverride {
|
||||
return {
|
||||
race: state.race,
|
||||
bodySrc: buildBodyPath(
|
||||
state.bodyColor as (typeof MEDIEVAL_BODY_COLORS)[number],
|
||||
),
|
||||
headSrc: buildRaceAssetPath(state.race, 'head', state.headIndex),
|
||||
hairSrc: buildRaceAssetPath(state.race, 'hair', state.hairColorIndex),
|
||||
handSrc: buildRaceAssetPath(state.race, 'hand', 1),
|
||||
facialHairSrc: state.facialHairEnabled
|
||||
? buildRaceAssetPath(state.race, 'facialHair', state.facialHairColorIndex)
|
||||
: undefined,
|
||||
headgear:
|
||||
state.headgearType === 'none'
|
||||
? undefined
|
||||
: buildMedievalAtlasSpec(
|
||||
state.headgearType,
|
||||
state.headgearFile,
|
||||
sanitizeFrameSelection(
|
||||
state.headgearType,
|
||||
state.headgearFile,
|
||||
state.headgearFrame,
|
||||
'headgear',
|
||||
),
|
||||
),
|
||||
mainHand:
|
||||
state.mainHandType === 'none'
|
||||
? undefined
|
||||
: buildMedievalAtlasSpec(
|
||||
state.mainHandType,
|
||||
state.mainHandFile,
|
||||
sanitizeFrameSelection(
|
||||
state.mainHandType,
|
||||
state.mainHandFile,
|
||||
state.mainHandFrame,
|
||||
'mainHand',
|
||||
),
|
||||
),
|
||||
offHand:
|
||||
state.offHandType === 'none'
|
||||
? undefined
|
||||
: buildMedievalAtlasSpec(
|
||||
state.offHandType,
|
||||
state.offHandFile,
|
||||
sanitizeFrameSelection(
|
||||
state.offHandType,
|
||||
state.offHandFile,
|
||||
state.offHandFrame,
|
||||
'offHand',
|
||||
),
|
||||
),
|
||||
bodyFrames: [0, 1, 2, 3],
|
||||
headFrame: 0,
|
||||
hairFrame: state.hairStyleFrame,
|
||||
handFrame: 0,
|
||||
facialHairFrame: state.facialHairEnabled
|
||||
? state.facialHairStyleFrame
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildNpcVisualSavePayload(
|
||||
overrideMap: Record<string, MedievalNpcVisualOverride>,
|
||||
npcId: string,
|
||||
editorState: EditableNpcVisualState,
|
||||
) {
|
||||
return {
|
||||
...overrideMap,
|
||||
[npcId]: buildOverrideFromEditorState(editorState),
|
||||
};
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { MedievalNpcVisualOverride } from '../data/medievalNpcVisuals';
|
||||
import type { EditableNpcVisualState } from './npcVisualEditorModel';
|
||||
import {
|
||||
NPC_LAYOUT_CONFIG_API_PATH,
|
||||
NPC_VISUAL_OVERRIDES_API_PATH,
|
||||
persistNpcLayoutConfig,
|
||||
persistNpcVisualOverrides,
|
||||
} from './npcVisualEditorPersistence';
|
||||
import type { NpcLayoutConfig } from './npcVisualShared';
|
||||
|
||||
function createEditorState(): EditableNpcVisualState {
|
||||
return {
|
||||
race: 'human',
|
||||
bodyColor: 'black',
|
||||
headIndex: 1,
|
||||
hairColorIndex: 1,
|
||||
hairStyleFrame: 0,
|
||||
facialHairEnabled: false,
|
||||
facialHairColorIndex: 1,
|
||||
facialHairStyleFrame: 0,
|
||||
headgearType: 'none',
|
||||
headgearFile: '',
|
||||
headgearFrame: 0,
|
||||
mainHandType: 'none',
|
||||
mainHandFile: '',
|
||||
mainHandFrame: 0,
|
||||
offHandType: 'none',
|
||||
offHandFile: '',
|
||||
offHandFrame: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function createExistingOverride(): MedievalNpcVisualOverride {
|
||||
return {
|
||||
race: 'elf',
|
||||
bodySrc: '/body.png',
|
||||
headSrc: '/head.png',
|
||||
hairSrc: '/hair.png',
|
||||
handSrc: '/hand.png',
|
||||
bodyFrames: [0, 1, 2, 3],
|
||||
headFrame: 0,
|
||||
hairFrame: 1,
|
||||
handFrame: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function createLayoutDraft(): NpcLayoutConfig {
|
||||
return {
|
||||
body: { x: 0, y: 0 },
|
||||
head: { x: 1, y: 2 },
|
||||
facialHair: { x: 3, y: 4 },
|
||||
hair: { x: 5, y: 6 },
|
||||
headgear: { x: 7, y: 8 },
|
||||
hand: { x: 9, y: 10 },
|
||||
mainHand: { x: 11, y: 12 },
|
||||
offHand: { x: 13, y: 14 },
|
||||
};
|
||||
}
|
||||
|
||||
describe('npcVisualEditorPersistence', () => {
|
||||
it('persists merged npc visual overrides and returns the writeback payload', async () => {
|
||||
const saveJson = vi.fn(async () => undefined);
|
||||
const result = await persistNpcVisualOverrides({
|
||||
overrideMap: {
|
||||
existing: createExistingOverride(),
|
||||
},
|
||||
npcId: 'npc-1',
|
||||
editorState: createEditorState(),
|
||||
saveJson,
|
||||
});
|
||||
|
||||
expect(saveJson).toHaveBeenCalledWith(
|
||||
NPC_VISUAL_OVERRIDES_API_PATH,
|
||||
expect.objectContaining({
|
||||
existing: createExistingOverride(),
|
||||
'npc-1': expect.objectContaining({
|
||||
race: 'human',
|
||||
bodyFrames: [0, 1, 2, 3],
|
||||
}),
|
||||
}),
|
||||
'保存角色形象覆盖配置失败',
|
||||
);
|
||||
expect(result.nextOverrideMap.existing).toEqual(createExistingOverride());
|
||||
expect(result.nextOverrideMap['npc-1']).toEqual(expect.objectContaining({ race: 'human' }));
|
||||
expect(result.saveMessage).toContain('npcVisualOverrides.json');
|
||||
});
|
||||
|
||||
it('persists layout config with a cloned payload for local writeback', async () => {
|
||||
const saveJson = vi.fn(async () => undefined);
|
||||
const layoutDraft = createLayoutDraft();
|
||||
const result = await persistNpcLayoutConfig({
|
||||
layoutDraft,
|
||||
saveJson,
|
||||
});
|
||||
|
||||
expect(saveJson).toHaveBeenCalledWith(
|
||||
NPC_LAYOUT_CONFIG_API_PATH,
|
||||
expect.objectContaining(layoutDraft),
|
||||
'保存角色布局配置失败',
|
||||
);
|
||||
expect(result.nextLayout).toEqual(layoutDraft);
|
||||
expect(result.nextLayout).not.toBe(layoutDraft);
|
||||
expect(result.saveMessage).toContain('角色布局');
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { MedievalNpcVisualOverride } from '../data/medievalNpcVisuals';
|
||||
import {
|
||||
buildEditorJsonApiPath,
|
||||
EDITOR_JSON_RESOURCE_IDS,
|
||||
} from '../editor/shared/editorApiClient';
|
||||
import { saveJsonObject } from '../editor/shared/jsonClient';
|
||||
import {
|
||||
buildNpcVisualSavePayload,
|
||||
type EditableNpcVisualState,
|
||||
} from './npcVisualEditorModel';
|
||||
import { cloneNpcLayoutConfig, type NpcLayoutConfig } from './npcVisualShared';
|
||||
|
||||
export const NPC_VISUAL_OVERRIDES_API_PATH = buildEditorJsonApiPath(
|
||||
EDITOR_JSON_RESOURCE_IDS.npcVisualOverrides,
|
||||
);
|
||||
export const NPC_LAYOUT_CONFIG_API_PATH = buildEditorJsonApiPath(
|
||||
EDITOR_JSON_RESOURCE_IDS.npcLayoutConfig,
|
||||
);
|
||||
|
||||
type SaveEditorJsonFn = typeof saveJsonObject;
|
||||
|
||||
export async function persistNpcVisualOverrides(params: {
|
||||
overrideMap: Record<string, MedievalNpcVisualOverride>;
|
||||
npcId: string;
|
||||
editorState: EditableNpcVisualState;
|
||||
saveJson?: SaveEditorJsonFn;
|
||||
}) {
|
||||
const {
|
||||
overrideMap,
|
||||
npcId,
|
||||
editorState,
|
||||
saveJson = saveJsonObject,
|
||||
} = params;
|
||||
const nextOverrideMap = buildNpcVisualSavePayload(overrideMap, npcId, editorState);
|
||||
|
||||
await saveJson(
|
||||
NPC_VISUAL_OVERRIDES_API_PATH,
|
||||
nextOverrideMap,
|
||||
'保存角色形象覆盖配置失败',
|
||||
);
|
||||
|
||||
return {
|
||||
nextOverrideMap,
|
||||
saveMessage: '已将角色形象覆盖配置保存到 src/data/npcVisualOverrides.json。',
|
||||
};
|
||||
}
|
||||
|
||||
export async function persistNpcLayoutConfig(params: {
|
||||
layoutDraft: NpcLayoutConfig;
|
||||
saveJson?: SaveEditorJsonFn;
|
||||
}) {
|
||||
const { layoutDraft, saveJson = saveJsonObject } = params;
|
||||
const nextLayout = cloneNpcLayoutConfig(layoutDraft);
|
||||
|
||||
await saveJson(
|
||||
NPC_LAYOUT_CONFIG_API_PATH,
|
||||
nextLayout,
|
||||
'保存角色布局配置失败',
|
||||
);
|
||||
|
||||
return {
|
||||
nextLayout,
|
||||
saveMessage: '已保存共享角色布局配置。',
|
||||
};
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
RpgCreationRoleAssetStudioModal,
|
||||
type RpgCreationRoleAssetStudioModalProps,
|
||||
} from './RpgCreationRoleAssetStudioModal';
|
||||
@@ -1 +0,0 @@
|
||||
export { SectionPanel as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
RpgCreationEntityEditorModal,
|
||||
type RpgCreationEntityEditorModalProps,
|
||||
} from './RpgCreationEntityEditorModal';
|
||||
@@ -1,4 +0,0 @@
|
||||
export {
|
||||
RpgCreationResultView,
|
||||
type RpgCreationResultViewProps,
|
||||
} from './RpgCreationResultView';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
upsertRpgWorldProfile,
|
||||
} from '../../services/rpg-creation';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import {
|
||||
clearRpgProfileBrowseHistory as clearProfileBrowseHistory,
|
||||
deleteRpgEntryWorldProfile,
|
||||
@@ -517,6 +518,7 @@ beforeEach(() => {
|
||||
vi.mocked(createRpgCreationSession).mockResolvedValue({
|
||||
session: mockSession,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(listRpgCreationWorks).mockResolvedValue([]);
|
||||
vi.mocked(executeRpgCreationAction).mockResolvedValue({
|
||||
operation: {
|
||||
@@ -669,6 +671,67 @@ test('create tab resumes agent workspace when draft has no compiled result yet',
|
||||
expect(screen.queryByText('世界档案')).toBeNull();
|
||||
});
|
||||
|
||||
test('opening a compiled draft with a missing agent session falls back to create hub', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listRpgCreationWorks)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
workId: 'draft:custom-world-agent-session-missing',
|
||||
sourceType: 'agent_session',
|
||||
status: 'draft',
|
||||
title: '潮雾列岛',
|
||||
subtitle: '世界底稿已生成',
|
||||
summary: '这是一份已经整理过首版结果页的草稿。',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
updatedAt: '2026-04-20T11:00:00.000Z',
|
||||
publishedAt: null,
|
||||
stage: 'object_refining',
|
||||
stageLabel: '整理关键对象',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
roleVisualReadyCount: 0,
|
||||
roleAnimationReadyCount: 0,
|
||||
roleAssetSummaryLabel: null,
|
||||
sessionId: 'custom-world-agent-session-missing',
|
||||
profileId: null,
|
||||
canResume: true,
|
||||
canEnterWorld: false,
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
vi.mocked(getRpgCreationSession).mockRejectedValueOnce(
|
||||
new ApiClientError({
|
||||
message: 'custom world agent session not found',
|
||||
status: 404,
|
||||
code: 'NOT_FOUND',
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await user.click(screen.getByRole('button', { name: /继续完善/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(window.location.search).toBe('');
|
||||
expect(listRpgCreationWorks).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByText('还没有作品')).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText('Agent工作区:custom-world-agent-session-missing'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('clicking a public work while logged out routes through requireAuth', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
publishRpgEntryWorldProfile,
|
||||
unpublishRpgEntryWorldProfile,
|
||||
} from '../../services/rpg-entry';
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
normalizeRpgEntryAgentBackedProfile,
|
||||
@@ -79,6 +80,14 @@ type UseRpgEntryLibraryDetailParams = {
|
||||
markAutoSavedProfile: (profile: CustomWorldProfile) => void;
|
||||
};
|
||||
|
||||
function isMissingRpgEntryAgentSessionError(error: unknown) {
|
||||
return (
|
||||
error instanceof ApiClientError &&
|
||||
error.status === 404 &&
|
||||
error.code === 'NOT_FOUND'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 负责平台详情、创作作品入口和结果页打开路径。
|
||||
* 平台壳层只消费“打开哪个面板”的结果,不再自己拼接恢复流程细节。
|
||||
@@ -218,7 +227,6 @@ export function useRpgEntryLibraryDetail(
|
||||
const handleOpenCreationWork = useCallback(
|
||||
async (work: CustomWorldWorkSummary) => {
|
||||
if (work.status === 'draft' && work.sessionId) {
|
||||
persistAgentUiState(work.sessionId, null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
@@ -228,33 +236,57 @@ export function useRpgEntryLibraryDetail(
|
||||
const shouldOpenAgentWorkspace =
|
||||
work.playableNpcCount <= 0 && work.landmarkCount <= 0;
|
||||
|
||||
if (shouldOpenAgentWorkspace) {
|
||||
// 仅八锚点未整理成底稿时才恢复 Agent 对话工作区。
|
||||
suppressAgentDraftResultAutoOpen();
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
try {
|
||||
if (shouldOpenAgentWorkspace) {
|
||||
// 仅八锚点未整理成底稿时才恢复 Agent 对话工作区。
|
||||
suppressAgentDraftResultAutoOpen();
|
||||
persistAgentUiState(work.sessionId, null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('agent-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
releaseAgentDraftResultAutoOpenSuppression();
|
||||
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
|
||||
const nextProfile = buildDraftResultProfile(latestSession);
|
||||
if (!nextProfile) {
|
||||
persistAgentUiState(work.sessionId, null);
|
||||
setPlatformError('当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。');
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('agent-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
persistAgentUiState(work.sessionId, null);
|
||||
setGeneratedCustomWorldProfile(
|
||||
normalizeRpgEntryAgentBackedProfile(nextProfile),
|
||||
);
|
||||
setCustomWorldResultViewSource('agent-draft');
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('agent-workspace');
|
||||
setSelectionStage('custom-world-result');
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isMissingRpgEntryAgentSessionError(error)) {
|
||||
// 失效会话不能继续保留在恢复状态里,否则刷新后会重复命中同一个坏 session。
|
||||
persistAgentUiState(null, null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
await refreshCustomWorldWorks().catch(() => []);
|
||||
setPlatformError(
|
||||
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
|
||||
);
|
||||
} else {
|
||||
setPlatformError(
|
||||
resolveRpgEntryErrorMessage(error, '读取创作草稿失败。'),
|
||||
);
|
||||
}
|
||||
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('platform');
|
||||
return;
|
||||
}
|
||||
|
||||
releaseAgentDraftResultAutoOpenSuppression();
|
||||
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
|
||||
const nextProfile = buildDraftResultProfile(latestSession);
|
||||
if (!nextProfile) {
|
||||
setPlatformError('当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。');
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('agent-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneratedCustomWorldProfile(
|
||||
normalizeRpgEntryAgentBackedProfile(nextProfile),
|
||||
);
|
||||
setCustomWorldResultViewSource('agent-draft');
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('custom-world-result');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!work.profileId) {
|
||||
|
||||
Reference in New Issue
Block a user