Merge remote-tracking branch 'origin/codex/unified-creation-flow-phase1'

# Conflicts:
#	server-rs/crates/api-server/src/wooden_fish.rs
This commit is contained in:
kdletters
2026-06-01 15:22:58 +08:00
86 changed files with 4944 additions and 967 deletions

View File

@@ -17,6 +17,10 @@ import type {
BabyObjectMatchDraft,
CreateBabyObjectMatchDraftRequest,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
JumpHopWorkDetailResponse,
JumpHopWorkProfileResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
@@ -82,6 +86,7 @@ import {
regenerateBabyObjectMatchDraftAssets,
saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
import { match3dCreationClient } from '../../services/match3d-creation';
import {
createServerMatch3DRuntimeAdapter,
@@ -625,6 +630,22 @@ vi.mock('../../services/edutainment-baby-object', () => ({
saveBabyObjectMatchDraft: vi.fn(),
}));
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
jumpHopClient: {
createSession: vi.fn(),
executeAction: vi.fn(),
getGalleryDetail: vi.fn(),
getSession: vi.fn(),
getWorkDetail: vi.fn(),
listGallery: vi.fn(),
listWorks: vi.fn(),
publishWork: vi.fn(),
restartRun: vi.fn(),
startRun: vi.fn(),
submitJump: vi.fn(),
},
}));
vi.mock('../../services/match3d-creation', () => ({
match3dCreationClient: {
createSession: vi.fn(),
@@ -782,8 +803,8 @@ vi.mock('../../services/puzzle-agent', () => ({
streamPuzzleAgentMessage: vi.fn(),
}));
vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
PuzzleAgentWorkspace: ({
vi.mock('../unified-creation/workspaces/PuzzleCreationWorkspace', () => ({
PuzzleCreationWorkspace: ({
session,
isBusy,
error,
@@ -986,8 +1007,8 @@ vi.mock('../match3d-result/Match3DResultView', () => ({
),
}));
vi.mock('../match3d-creation/Match3DAgentWorkspace', () => ({
Match3DAgentWorkspace: ({
vi.mock('../unified-creation/workspaces/Match3DCreationWorkspace', () => ({
Match3DCreationWorkspace: ({
session,
isBusy,
error,
@@ -1465,6 +1486,139 @@ function buildMockBabyObjectMatchDraft(
};
}
function buildMockJumpHopWork(
overrides: Partial<JumpHopWorkProfileResponse> = {},
): JumpHopWorkProfileResponse {
const profileId = overrides.summary?.profileId ?? 'jump-hop-profile-1';
const path = overrides.path ?? {
seed: 'jump-hop-seed',
difficulty: 'standard' as const,
platforms: [
{
platformId: 'platform-start',
tileType: 'start' as const,
x: 0,
y: 0,
width: 48,
height: 36,
landingRadius: 22,
perfectRadius: 12,
scoreValue: 1,
},
{
platformId: 'platform-finish',
tileType: 'finish' as const,
x: 16,
y: 18,
width: 60,
height: 42,
landingRadius: 22,
perfectRadius: 12,
scoreValue: 2,
},
],
finishIndex: 1,
cameraPreset: 'default',
scoring: {
chargeToDistanceRatio: 1.2,
maxChargeMs: 1800,
hitBonus: 20,
perfectBonus: 50,
},
};
const characterAsset = overrides.characterAsset ?? {
assetId: 'jump-hop-character-1',
imageSrc: 'data:image/png;base64,character',
imageObjectKey: 'jump-hop/character.png',
assetObjectId: 'asset-jump-hop-character',
generationProvider: 'vector-engine-gpt-image-2',
prompt: '纸片小兔',
width: 1024,
height: 1024,
};
const tileAtlasAsset = overrides.tileAtlasAsset ?? {
assetId: 'jump-hop-tiles-1',
imageSrc: 'data:image/png;base64,tiles',
imageObjectKey: 'jump-hop/tiles.png',
assetObjectId: 'asset-jump-hop-tiles',
generationProvider: 'vector-engine-gpt-image-2',
prompt: '柔软云朵平台',
width: 1024,
height: 1024,
};
const tileAssets = overrides.tileAssets ?? [
{
tileType: 'start' as const,
imageSrc: 'data:image/png;base64,tile-start',
imageObjectKey: 'jump-hop/tile-start.png',
assetObjectId: 'asset-jump-hop-tile-start',
sourceAtlasCell: 'A1',
visualWidth: 128,
visualHeight: 96,
topSurfaceRadius: 24,
landingRadius: 28,
},
{
tileType: 'finish' as const,
imageSrc: 'data:image/png;base64,tile-finish',
imageObjectKey: 'jump-hop/tile-finish.png',
assetObjectId: 'asset-jump-hop-tile-finish',
sourceAtlasCell: 'A2',
visualWidth: 128,
visualHeight: 96,
topSurfaceRadius: 24,
landingRadius: 28,
},
];
const draft = overrides.draft ?? {
templateId: 'jump-hop',
templateName: '跳一跳',
profileId,
workTitle: '云端跳台',
workDescription: '一路跳到星星。',
themeTags: ['云朵', '星空'],
difficulty: 'standard' as const,
stylePreset: 'paper-toy' as const,
characterPrompt: '纸片小兔',
tilePrompt: '柔软云朵平台',
endMoodPrompt: '星光门',
characterAsset,
tileAtlasAsset,
tileAssets,
path,
coverComposite: 'data:image/png;base64,cover',
generationStatus: 'ready' as const,
};
return {
summary: {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-1',
profileId,
ownerUserId: 'user-1',
sourceSessionId: 'jump-hop-session-1',
workTitle: draft.workTitle,
workDescription: draft.workDescription,
themeTags: draft.themeTags,
difficulty: draft.difficulty,
stylePreset: draft.stylePreset,
coverImageSrc: draft.coverComposite,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-30T10:00:00.000Z',
publishedAt: null,
publishReady: true,
generationStatus: 'ready',
...overrides.summary,
},
draft,
path,
characterAsset,
tileAtlasAsset,
tileAssets,
};
}
function buildMockBarkBattleWork(
overrides: Partial<BarkBattleWorkSummary> = {},
): BarkBattleWorkSummary {
@@ -2520,6 +2674,18 @@ beforeEach(() => {
vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
items: [],
hasMore: false,
nextCursor: null,
});
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({ items: [] });
vi.mocked(jumpHopClient.getSession).mockRejectedValue(
new Error('未找到跳一跳会话'),
);
vi.mocked(jumpHopClient.getWorkDetail).mockRejectedValue(
new Error('未找到跳一跳作品'),
);
vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
draft: payload.draft,
}));
@@ -7215,6 +7381,58 @@ test('refreshing RPG agent path restores stored agent workspace pointer', async
).toBeTruthy();
});
test('direct jump hop result route shows recovery panel when no draft pointer exists', async () => {
window.history.replaceState(null, '', '/creation/jump-hop/result');
render(<TestWrapper withAuth />);
expect(await screen.findByText('跳一跳草稿未恢复')).toBeTruthy();
expect(screen.getByRole('button', { name: '返回创作' })).toBeTruthy();
expect(jumpHopClient.getWorkDetail).not.toHaveBeenCalled();
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
});
test('direct jump hop result route restores work detail by profile id', async () => {
const work = buildMockJumpHopWork({
summary: {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-restore-1',
profileId: 'jump-hop-profile-restore-1',
ownerUserId: 'user-1',
sourceSessionId: null,
workTitle: '恢复后的云端跳台',
workDescription: '从 profileId 回读完整跳一跳结果。',
themeTags: ['云朵'],
difficulty: 'standard',
stylePreset: 'paper-toy',
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-30T10:00:00.000Z',
publishedAt: null,
publishReady: true,
generationStatus: 'ready',
},
});
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
item: work,
} satisfies JumpHopWorkDetailResponse);
window.history.replaceState(
null,
'',
'/creation/jump-hop/result?profileId=jump-hop-profile-restore-1',
);
render(<TestWrapper withAuth />);
expect(await screen.findByText('恢复后的云端跳台')).toBeTruthy();
expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull();
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
'jump-hop-profile-restore-1',
);
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
});
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
const user = userEvent.setup();

View File

@@ -6104,6 +6104,11 @@ export function RpgEntryHomeView({
<div className={MOBILE_PAGE_STAGE_CLASS}>
<section>
<SectionHeader title="我的创作" detail="草稿与已发布" />
{platformError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
{platformError}
</div>
) : null}
{isLoadingPlatform ? (
<EmptyShelf text="正在读取你的作品..." />
) : myEntries.length > 0 ? (
@@ -6140,7 +6145,16 @@ export function RpgEntryHomeView({
const createContent: ReactNode =
createTabContent ?? fallbackCreateStartContent;
const savesContent: ReactNode = draftTabContent ?? fallbackDraftContent;
const savesContent: ReactNode = (
<>
{platformError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
{platformError}
</div>
) : null}
{draftTabContent ?? fallbackDraftContent}
</>
);
const profileContent: ReactNode = (
<div className={`${MOBILE_PROFILE_PAGE_STAGE_CLASS} platform-profile-page`}>

View File

@@ -870,6 +870,10 @@ export function resolvePlatformWorkAuthorDisplayName(
const displayName = authorSummary?.displayName?.trim();
const publicUserCode = authorSummary?.publicUserCode?.trim();
if (displayName && publicUserCode) {
return `${displayName} · ${publicUserCode}`;
}
return displayName || publicUserCode || entry.authorDisplayName.trim() || '玩家';
}
@@ -1079,4 +1083,4 @@ function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
.map((tag) => tag.trim())
.filter(Boolean)
.slice(0, 3);
}
}