Update spacetime-client bindings and frontend

Large update across server and web clients: regenerated/added many spacetime-client module bindings and input types (including new delete/work_delete input types and numerous procedure/reducer files), updates to server-rs API modules (bark_battle, jump_hop, wooden_fish, auth, module-runtime and shared contracts), and fixes in module-runtime behavior and domain logic. Frontend changes include new/updated components and tests (creative audio helpers, bark-battle/jump-hop/wooden-fish clients and views, unified generation pages, RPG entry views, and runtime shells), plus CSS and service updates. Documentation and operational notes updated (.hermes pitfalls and multiple PRD/docs) to cover daily-task refresh, banner asset fallback, recommend-key bug, and other platform behaviors. Tests and verification steps added/updated alongside these changes.
This commit is contained in:
2026-06-04 22:44:19 +08:00
parent 2678954627
commit 27b30f974b
326 changed files with 4374 additions and 2539 deletions

View File

@@ -20,6 +20,7 @@ import type {
import type {
JumpHopWorkDetailResponse,
JumpHopWorkProfileResponse,
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
@@ -40,6 +41,7 @@ import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
@@ -50,6 +52,7 @@ import { ApiClientError } from '../../services/apiClient';
import type { AuthUser } from '../../services/authService';
import {
createBarkBattleDraft,
deleteBarkBattleWork,
generateAllBarkBattleImageAssets,
listBarkBattleGallery,
listBarkBattleWorks,
@@ -634,6 +637,7 @@ vi.mock('../../services/big-fish-runtime', () => ({
vi.mock('../../services/bark-battle-creation', () => ({
createBarkBattleDraft: vi.fn(),
deleteBarkBattleWork: vi.fn(),
generateAllBarkBattleImageAssets: vi.fn(),
listBarkBattleGallery: vi.fn(),
listBarkBattleWorks: vi.fn(),
@@ -656,6 +660,7 @@ vi.mock('../../services/edutainment-baby-object', () => ({
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
jumpHopClient: {
createSession: vi.fn(),
deleteWork: vi.fn(),
executeAction: vi.fn(),
getGalleryDetail: vi.fn(),
getSession: vi.fn(),
@@ -673,6 +678,7 @@ vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
woodenFishClient: {
checkpointRun: vi.fn(),
createSession: vi.fn(),
deleteWork: vi.fn(),
executeAction: vi.fn(),
finishRun: vi.fn(),
getGalleryDetail: vi.fn(),
@@ -802,6 +808,7 @@ vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
woodenFishClient: {
checkpointRun: vi.fn(),
createSession: vi.fn(),
deleteWork: vi.fn(),
executeAction: vi.fn(),
finishRun: vi.fn(),
getGalleryDetail: vi.fn(),
@@ -2725,6 +2732,7 @@ beforeEach(() => {
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
vi.mocked(deleteBarkBattleWork).mockResolvedValue({ items: [] });
vi.mocked(listVisualNovelGallery).mockResolvedValue({ works: [] });
vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
@@ -2733,6 +2741,7 @@ beforeEach(() => {
nextCursor: null,
});
vi.mocked(woodenFishClient.listWorks).mockResolvedValue({ items: [] });
vi.mocked(woodenFishClient.deleteWork).mockResolvedValue({ items: [] });
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
@@ -2741,6 +2750,7 @@ beforeEach(() => {
nextCursor: null,
});
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({ items: [] });
vi.mocked(jumpHopClient.deleteWork).mockResolvedValue({ items: [] });
vi.mocked(jumpHopClient.getSession).mockRejectedValue(
new Error('未找到跳一跳会话'),
);
@@ -2753,6 +2763,7 @@ beforeEach(() => {
nextCursor: null,
});
vi.mocked(woodenFishClient.listWorks).mockResolvedValue({ items: [] });
vi.mocked(woodenFishClient.deleteWork).mockResolvedValue({ items: [] });
vi.mocked(woodenFishClient.getSession).mockRejectedValue(
new Error('未找到敲木鱼会话'),
);
@@ -9135,10 +9146,10 @@ test('starting draft generation leaves the agent workspace and shows the generat
}),
).toBeTruthy();
expect(screen.queryByText(/Agent工作区/u)).toBeNull();
expect(screen.getByText('当前世界信息')).toBeTruthy();
expect(screen.queryByText('当前世界信息')).toBeNull();
expect(screen.queryByText('回到工作区')).toBeNull();
expect(screen.getByText('世界承诺')).toBeTruthy();
expect(screen.getByText(/灯塔与禁航令共同决定谁能穿过死潮/u)).toBeTruthy();
expect(screen.queryByText('世界承诺')).toBeNull();
expect(screen.queryByText(/灯塔与禁航令共同决定谁能穿过死潮/u)).toBeNull();
expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
});
@@ -11364,3 +11375,145 @@ test('creation hub published work card reveals delete action after card action r
expect(within(dialog).getByRole('button', { name: '确认删除' })).toBeTruthy();
expect(deleteRpgEntryWorldProfile).not.toHaveBeenCalled();
});
test('creation hub gives jump hop wooden fish and bark battle cards the shared delete interaction', async () => {
const user = userEvent.setup();
const jumpHopWork = {
...buildMockJumpHopWork({
summary: {
runtimeKind: 'jump-hop',
workId: 'jump-hop-work-delete',
profileId: 'jump-hop-profile-delete',
ownerUserId: 'user-1',
sourceSessionId: 'jump-hop-session-delete',
workTitle: '跳台删除草稿',
workDescription: '跳一跳草稿也应接入统一删除。',
themeTags: ['跳台'],
difficulty: 'standard',
stylePreset: 'paper-toy',
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-21T10:20:00.000Z',
publishedAt: null,
publishReady: true,
generationStatus: 'ready',
},
}).summary,
} satisfies JumpHopWorkSummaryResponse;
const woodenFishWork = {
runtimeKind: 'wooden-fish',
workId: 'wooden-fish-work-delete',
profileId: 'wooden-fish-profile-delete',
ownerUserId: 'user-1',
sourceSessionId: 'wooden-fish-session-delete',
workTitle: '木鱼删除草稿',
workDescription: '敲木鱼草稿也应接入统一删除。',
themeTags: ['木鱼'],
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-21T10:10:00.000Z',
publishedAt: null,
publishReady: true,
generationStatus: 'ready',
} satisfies WoodenFishWorkSummaryResponse;
const barkBattleWork = buildMockBarkBattleWork({
workId: 'bark-battle-work-delete',
draftId: 'bark-battle-draft-delete',
title: '声浪删除已发布',
summary: '汪汪声浪已发布作品也应接入统一删除。',
updatedAt: '2026-05-21T10:00:00.000Z',
publishedAt: '2026-05-21T10:00:00.000Z',
});
vi.mocked(fetchCreationEntryConfig).mockResolvedValueOnce({
...testCreationEntryConfig,
creationTypes: [
...testCreationEntryConfig.creationTypes,
{
id: 'jump-hop',
title: '跳一跳',
subtitle: '俯视角跳台挑战',
badge: '可创建',
imageSrc: '/creation-type-references/jump-hop.webp',
visible: true,
open: true,
sortOrder: 46,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
id: 'wooden-fish',
title: '敲木鱼',
subtitle: '功德敲击小游戏',
badge: '可创建',
imageSrc: '/creation-type-references/wooden-fish.webp',
visible: true,
open: true,
sortOrder: 47,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
],
});
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({
items: [jumpHopWork],
});
vi.mocked(woodenFishClient.listWorks).mockResolvedValue({
items: [woodenFishWork],
});
vi.mocked(listBarkBattleWorks).mockResolvedValue({
items: [barkBattleWork],
});
vi.mocked(jumpHopClient.deleteWork).mockResolvedValueOnce({ items: [] });
vi.mocked(woodenFishClient.deleteWork).mockResolvedValueOnce({ items: [] });
vi.mocked(deleteBarkBattleWork).mockResolvedValueOnce({ items: [] });
render(<TestWrapper withAuth />);
await openDraftHub(user);
async function revealAndConfirmDelete(
cardName: RegExp,
title: string,
): Promise<void> {
const card = await screen.findByRole('button', { name: cardName });
card.focus();
await user.keyboard('{ArrowLeft}');
const shell = card.closest('.creation-work-card-shell');
if (!shell) {
throw new Error('作品卡应位于统一操作壳内');
}
await user.click(within(shell as HTMLElement).getByRole('button', { name: '删除' }));
const dialog = await screen.findByRole('dialog', { name: '删除作品' });
expect(within(dialog).getByText(`${title}`)).toBeTruthy();
await user.click(within(dialog).getByRole('button', { name: '确认删除' }));
}
await revealAndConfirmDelete(/继续创作《跳台删除草稿》/u, '跳台删除草稿');
await waitFor(() => {
expect(jumpHopClient.deleteWork).toHaveBeenCalledWith(
'jump-hop-profile-delete',
);
});
await revealAndConfirmDelete(/继续创作《木鱼删除草稿》/u, '木鱼删除草稿');
await waitFor(() => {
expect(woodenFishClient.deleteWork).toHaveBeenCalledWith(
'wooden-fish-profile-delete',
);
});
await revealAndConfirmDelete(/查看详情《声浪删除已发布》/u, '声浪删除已发布');
await waitFor(() => {
expect(deleteBarkBattleWork).toHaveBeenCalledWith(
'bark-battle-work-delete',
);
});
});