Add big fish settlement actions and publish feedback
This commit is contained in:
24
docs/experience/BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md
Normal file
24
docs/experience/BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 大鱼吃小鱼发布反馈修复 2026-04-26
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
大鱼吃小鱼结果页的“发布”按钮已经会向后端发送 `big_fish_publish_game` action。后端发布成功后会把当前 Agent session 的 `stage` 改成 `published`,作品列表也会从 session 聚合出已发布作品。
|
||||||
|
|
||||||
|
问题出在前端发布成功后的反馈链路不完整:
|
||||||
|
|
||||||
|
1. 结果页没有把 `stage: published` 显示成“已发布”状态,用户点击后看起来没有变化。
|
||||||
|
2. 平台父层没有在大鱼发布成功后刷新“大鱼吃小鱼”作品列表,创作中心仍可能保留旧的草稿状态。
|
||||||
|
|
||||||
|
## 落地口径
|
||||||
|
|
||||||
|
1. `BigFishResultView` 以 `session.stage === 'published'` 作为已发布态真相。
|
||||||
|
2. 已发布态下发布按钮显示“已发布”并禁用,避免重复提交。
|
||||||
|
3. 已发布态下发布校验区显示“已发布”状态,继续保留资源完成度信息。
|
||||||
|
4. `PlatformEntryFlowShellImpl` 在 `big_fish_publish_game` 成功后刷新 `bigFishWorks`。
|
||||||
|
5. 发布失败仍沿用既有错误模态,展示后端 `details.message` 里的具体校验原因。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
1. 在大鱼结果页点击“发布”会调用 `/api/runtime/big-fish/agent/sessions/{sessionId}/actions` 的 `big_fish_publish_game`。
|
||||||
|
2. 后端返回已发布 session 后,结果页按钮变为“已发布”。
|
||||||
|
3. 返回创作中心后,该作品卡片状态通过刷新后的作品列表体现为已发布。
|
||||||
@@ -30,3 +30,4 @@
|
|||||||
- [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。
|
- [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。
|
||||||
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
||||||
- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。
|
- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。
|
||||||
|
- [BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md](./BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md):记录大鱼吃小鱼发布成功后结果页反馈与作品列表刷新的修复口径。
|
||||||
|
|||||||
@@ -242,6 +242,7 @@
|
|||||||
2. 发送聊天、action 和摇杆输入。
|
2. 发送聊天、action 和摇杆输入。
|
||||||
3. 根据后端 snapshot 渲染实体。
|
3. 根据后端 snapshot 渲染实体。
|
||||||
4. 当后端 snapshot 返回 `won` 或 `failed` 时,必须在玩法舞台中央显示清晰结算浮层;通关与失败都不能只依赖顶部状态标签或事件日志。
|
4. 当后端 snapshot 返回 `won` 或 `failed` 时,必须在玩法舞台中央显示清晰结算浮层;通关与失败都不能只依赖顶部状态标签或事件日志。
|
||||||
|
5. 结算浮层必须提供可继续操作的出口:`failed` 至少包含“重来”和“退出”,`won` 至少包含“退出”。“重来”只能重新启动当前大鱼作品的一局后端 run,不能在前端本地篡改旧 run snapshot;“退出”回到当前作品结果页或直达入口的上级页面。
|
||||||
|
|
||||||
前端禁止:
|
前端禁止:
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
## 验收口径
|
## 验收口径
|
||||||
|
|
||||||
1. 浏览器访问 `/big-fish` 后直接显示竖屏大鱼吃小鱼舞台。
|
1. 浏览器访问 `/big-fish` 后直接显示竖屏大鱼吃小鱼舞台。
|
||||||
2. 左下摇杆可移动玩家实体。
|
2. 屏幕任意位置按下并拖动可移动玩家实体。
|
||||||
3. 玩家碰到不高于自身等级的实体后成长,并在事件日志显示成长结果。
|
3. 玩家碰到不高于自身等级的实体后成长,并在事件日志显示成长结果。
|
||||||
4. 左上返回按钮在直达页语义为重开当前占位局。
|
4. 左上返回按钮在直达页语义为退出到平台首页。
|
||||||
|
5. 直达页通关或失败后,结算浮层继续复用正式运行态出口;失败态点击“重来”重开本地占位局,点击“退出”回到平台首页。
|
||||||
|
|||||||
@@ -208,11 +208,16 @@ export default function BigFishPlaygroundApp() {
|
|||||||
setRun(buildInitialRun());
|
setRun(buildInitialRun());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleExit = useCallback(() => {
|
||||||
|
window.location.assign('/');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BigFishRuntimeShell
|
<BigFishRuntimeShell
|
||||||
run={run}
|
run={run}
|
||||||
assetSlots={assetSlots}
|
assetSlots={assetSlots}
|
||||||
onBack={handleRestart}
|
onBack={handleExit}
|
||||||
|
onRestart={handleRestart}
|
||||||
onSubmitInput={handleSubmitInput}
|
onSubmitInput={handleSubmitInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -170,4 +170,35 @@ describe('BigFishResultView', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '知道了' }));
|
fireEvent.click(screen.getByRole('button', { name: '知道了' }));
|
||||||
expect(onDismissError).toHaveBeenCalledTimes(1);
|
expect(onDismissError).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows published state and prevents duplicate publish clicks', () => {
|
||||||
|
const onExecuteAction = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BigFishResultView
|
||||||
|
session={{
|
||||||
|
...createSession(),
|
||||||
|
stage: 'published',
|
||||||
|
publishReady: true,
|
||||||
|
assetCoverage: {
|
||||||
|
levelMainImageReadyCount: 1,
|
||||||
|
levelMotionReadyCount: 2,
|
||||||
|
backgroundReady: true,
|
||||||
|
requiredLevelCount: 1,
|
||||||
|
publishReady: true,
|
||||||
|
blockers: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onBack={() => {}}
|
||||||
|
onExecuteAction={onExecuteAction}
|
||||||
|
onStartTestRun={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const publishedButton = screen.getByRole('button', { name: '已发布' });
|
||||||
|
expect((publishedButton as HTMLButtonElement).disabled).toBe(true);
|
||||||
|
expect(screen.getAllByText('已发布').length).toBeGreaterThan(0);
|
||||||
|
fireEvent.click(publishedButton);
|
||||||
|
expect(onExecuteAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
Waves,
|
Waves,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BigFishAssetSlotResponse,
|
BigFishAssetSlotResponse,
|
||||||
@@ -338,6 +338,7 @@ export function BigFishResultView({
|
|||||||
}: BigFishResultViewProps) {
|
}: BigFishResultViewProps) {
|
||||||
const [studioTarget, setStudioTarget] =
|
const [studioTarget, setStudioTarget] =
|
||||||
useState<BigFishAssetStudioTarget | null>(null);
|
useState<BigFishAssetStudioTarget | null>(null);
|
||||||
|
const [isPublishSubmitting, setIsPublishSubmitting] = useState(false);
|
||||||
const draft = session.draft;
|
const draft = session.draft;
|
||||||
const backgroundSlot = findAssetSlot(session.assetSlots, 'stage_background');
|
const backgroundSlot = findAssetSlot(session.assetSlots, 'stage_background');
|
||||||
const backgroundPreviewUrl = buildLevelAssetPreview(backgroundSlot);
|
const backgroundPreviewUrl = buildLevelAssetPreview(backgroundSlot);
|
||||||
@@ -345,6 +346,8 @@ export function BigFishResultView({
|
|||||||
() => session.assetCoverage.blockers.filter(Boolean),
|
() => session.assetCoverage.blockers.filter(Boolean),
|
||||||
[session.assetCoverage.blockers],
|
[session.assetCoverage.blockers],
|
||||||
);
|
);
|
||||||
|
const isPublished = session.stage === 'published';
|
||||||
|
const canClickPublish = !isPublished && !isBusy;
|
||||||
const studioPreviewUrl = useMemo(() => {
|
const studioPreviewUrl = useMemo(() => {
|
||||||
if (!studioTarget) {
|
if (!studioTarget) {
|
||||||
return null;
|
return null;
|
||||||
@@ -352,6 +355,12 @@ export function BigFishResultView({
|
|||||||
return buildStudioAssetPreview(session.assetSlots, studioTarget);
|
return buildStudioAssetPreview(session.assetSlots, studioTarget);
|
||||||
}, [session.assetSlots, studioTarget]);
|
}, [session.assetSlots, studioTarget]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isBusy || isPublished || error) {
|
||||||
|
setIsPublishSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [error, isBusy, isPublished]);
|
||||||
|
|
||||||
if (!draft) {
|
if (!draft) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
@@ -388,14 +397,23 @@ export function BigFishResultView({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isBusy}
|
disabled={!canClickPublish}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setIsPublishSubmitting(true);
|
||||||
onExecuteAction({ action: 'big_fish_publish_game' });
|
onExecuteAction({ action: 'big_fish_publish_game' });
|
||||||
}}
|
}}
|
||||||
className="inline-flex items-center gap-2 rounded-full bg-cyan-200 px-4 py-2 text-sm font-bold text-slate-950 disabled:opacity-45"
|
className="inline-flex items-center gap-2 rounded-full bg-cyan-200 px-4 py-2 text-sm font-bold text-slate-950 disabled:opacity-45"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
{isPublishSubmitting && isBusy && !isPublished ? (
|
||||||
发布
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{isPublished
|
||||||
|
? '已发布'
|
||||||
|
: isPublishSubmitting && isBusy
|
||||||
|
? '发布中'
|
||||||
|
: '发布'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -487,7 +505,11 @@ export function BigFishResultView({
|
|||||||
背景 {session.assetCoverage.backgroundReady ? '已完成' : '待生成'}
|
背景 {session.assetCoverage.backgroundReady ? '已完成' : '待生成'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{blockers.length > 0 ? (
|
{isPublished ? (
|
||||||
|
<div className="mt-3 text-sm font-semibold text-emerald-600">
|
||||||
|
已发布
|
||||||
|
</div>
|
||||||
|
) : blockers.length > 0 ? (
|
||||||
<div className="mt-3 space-y-1 text-xs leading-5 text-amber-700">
|
<div className="mt-3 space-y-1 text-xs leading-5 text-amber-700">
|
||||||
{blockers.slice(0, 4).map((blocker) => (
|
{blockers.slice(0, 4).map((blocker) => (
|
||||||
<div key={blocker}>{blocker}</div>
|
<div key={blocker}>{blocker}</div>
|
||||||
|
|||||||
80
src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx
Normal file
80
src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { BigFishRuntimeSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
||||||
|
import { BigFishRuntimeShell } from './BigFishRuntimeShell';
|
||||||
|
|
||||||
|
vi.mock('../ResolvedAssetImage', () => ({
|
||||||
|
ResolvedAssetImage: ({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
src?: string | null;
|
||||||
|
alt?: string;
|
||||||
|
className?: string;
|
||||||
|
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createRun(
|
||||||
|
status: BigFishRuntimeSnapshotResponse['status'],
|
||||||
|
): BigFishRuntimeSnapshotResponse {
|
||||||
|
return {
|
||||||
|
runId: 'big-fish-run-1',
|
||||||
|
sessionId: 'big-fish-session-1',
|
||||||
|
status,
|
||||||
|
tick: 18,
|
||||||
|
playerLevel: 2,
|
||||||
|
winLevel: 5,
|
||||||
|
leaderEntityId: null,
|
||||||
|
ownedEntities: [],
|
||||||
|
wildEntities: [],
|
||||||
|
cameraCenter: { x: 0, y: 0 },
|
||||||
|
lastInput: { x: 0, y: 0 },
|
||||||
|
eventLog: ['己方鱼群已经耗尽'],
|
||||||
|
updatedAt: '2026-04-26T12:00:00.000Z',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BigFishRuntimeShell', () => {
|
||||||
|
test('renders restart and exit actions after a failed run', () => {
|
||||||
|
const onBack = vi.fn();
|
||||||
|
const onRestart = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BigFishRuntimeShell
|
||||||
|
run={createRun('failed')}
|
||||||
|
onBack={onBack}
|
||||||
|
onRestart={onRestart}
|
||||||
|
onSubmitInput={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '重来' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '退出' }));
|
||||||
|
|
||||||
|
expect(screen.getByText('本轮失败')).toBeTruthy();
|
||||||
|
expect(onRestart).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onBack).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps an exit action after a won run', () => {
|
||||||
|
const onBack = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BigFishRuntimeShell
|
||||||
|
run={createRun('won')}
|
||||||
|
onBack={onBack}
|
||||||
|
onSubmitInput={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('通关完成')).toBeTruthy();
|
||||||
|
expect(screen.queryByRole('button', { name: '重来' })).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '退出' }));
|
||||||
|
expect(onBack).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ArrowLeft, Loader2 } from 'lucide-react';
|
import { ArrowLeft, Loader2, RotateCcw } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState, type PointerEvent } from 'react';
|
import { type PointerEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BigFishAssetSlotResponse,
|
BigFishAssetSlotResponse,
|
||||||
@@ -21,6 +21,7 @@ type BigFishRuntimeShellProps = {
|
|||||||
isBusy?: boolean;
|
isBusy?: boolean;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
onRestart?: () => void;
|
||||||
onSubmitInput: (payload: SubmitBigFishInputRequest) => void;
|
onSubmitInput: (payload: SubmitBigFishInputRequest) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,6 +189,7 @@ export function BigFishRuntimeShell({
|
|||||||
isBusy = false,
|
isBusy = false,
|
||||||
error = null,
|
error = null,
|
||||||
onBack,
|
onBack,
|
||||||
|
onRestart,
|
||||||
onSubmitInput,
|
onSubmitInput,
|
||||||
}: BigFishRuntimeShellProps) {
|
}: BigFishRuntimeShellProps) {
|
||||||
const stageRef = useRef<HTMLDivElement | null>(null);
|
const stageRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -200,6 +202,10 @@ export function BigFishRuntimeShell({
|
|||||||
}, [stick]);
|
}, [stick]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (run?.status !== 'running') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
const current = stickRef.current;
|
const current = stickRef.current;
|
||||||
// 即使没有方向输入也持续回传当前状态,让后端持续推进刷怪、清理与胜负裁决。
|
// 即使没有方向输入也持续回传当前状态,让后端持续推进刷怪、清理与胜负裁决。
|
||||||
@@ -209,7 +215,7 @@ export function BigFishRuntimeShell({
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timer);
|
window.clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, [onSubmitInput]);
|
}, [onSubmitInput, run?.status]);
|
||||||
|
|
||||||
const submitDirection = (direction: SubmitBigFishInputRequest) => {
|
const submitDirection = (direction: SubmitBigFishInputRequest) => {
|
||||||
setStick(direction);
|
setStick(direction);
|
||||||
@@ -318,16 +324,39 @@ export function BigFishRuntimeShell({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settlementCopy ? (
|
{settlementCopy ? (
|
||||||
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center px-5">
|
<div className="absolute inset-0 z-40 flex items-center justify-center px-5">
|
||||||
<div
|
<div
|
||||||
className={`w-full max-w-[20rem] rounded-[2rem] border border-white/24 bg-gradient-to-br ${settlementCopy.tone} p-6 text-center shadow-2xl shadow-slate-950/45 backdrop-blur-xl`}
|
className={`w-full max-w-[20rem] rounded-[1.5rem] border border-white/24 bg-gradient-to-br ${settlementCopy.tone} p-6 text-center shadow-2xl shadow-slate-950/45 backdrop-blur-xl`}
|
||||||
>
|
>
|
||||||
<div className="text-3xl font-black tracking-[0.22em] text-white [text-shadow:0_2px_12px_rgba(2,6,23,0.6)]">
|
<div className="text-3xl font-black text-white [text-shadow:0_2px_12px_rgba(2,6,23,0.6)]">
|
||||||
{settlementCopy.title}
|
{settlementCopy.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-sm font-semibold leading-6 text-white/82">
|
<div className="mt-3 text-sm font-semibold leading-6 text-white/82">
|
||||||
{settlementCopy.message}
|
{settlementCopy.message}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-5 grid grid-cols-2 gap-2">
|
||||||
|
{run.status === 'failed' && onRestart ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={onRestart}
|
||||||
|
className="inline-flex h-11 items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-bold text-slate-950 shadow-lg shadow-slate-950/20 disabled:opacity-45"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" />
|
||||||
|
重来
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBack}
|
||||||
|
className={`inline-flex h-11 items-center justify-center gap-2 rounded-full border border-white/30 bg-black/24 px-4 text-sm font-bold text-white backdrop-blur ${
|
||||||
|
run.status === 'failed' && onRestart ? '' : 'col-span-2'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
退出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -755,6 +755,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
onActionComplete: ({ payload, response, setSession }) => {
|
onActionComplete: ({ payload, response, setSession }) => {
|
||||||
setSession(response.session);
|
setSession(response.session);
|
||||||
|
if (payload.action === 'big_fish_publish_game') {
|
||||||
|
void refreshBigFishShelf();
|
||||||
|
}
|
||||||
if (payload.action !== 'big_fish_compile_draft') {
|
if (payload.action !== 'big_fish_compile_draft') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1102,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const submitBigFishInput = useCallback(
|
const submitBigFishInput = useCallback(
|
||||||
(payload: SubmitBigFishInputRequest) => {
|
(payload: SubmitBigFishInputRequest) => {
|
||||||
if (!bigFishRun || bigFishInputInFlightRef.current) {
|
if (
|
||||||
|
!bigFishRun ||
|
||||||
|
bigFishRun.status !== 'running' ||
|
||||||
|
bigFishInputInFlightRef.current
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2096,6 +2103,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
onBack={() => {
|
onBack={() => {
|
||||||
setSelectionStage('big-fish-result');
|
setSelectionStage('big-fish-result');
|
||||||
}}
|
}}
|
||||||
|
onRestart={() => {
|
||||||
|
void startBigFishRun();
|
||||||
|
}}
|
||||||
onSubmitInput={submitBigFishInput}
|
onSubmitInput={submitBigFishInput}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from '../../services/rpg-entry';
|
} from '../../services/rpg-entry';
|
||||||
import {
|
import {
|
||||||
createBigFishCreationSession,
|
createBigFishCreationSession,
|
||||||
|
executeBigFishCreationAction,
|
||||||
getBigFishCreationSession,
|
getBigFishCreationSession,
|
||||||
} from '../../services/big-fish-creation';
|
} from '../../services/big-fish-creation';
|
||||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||||
@@ -172,13 +173,23 @@ vi.mock('../big-fish-result/BigFishResultView', () => ({
|
|||||||
BigFishResultView: ({
|
BigFishResultView: ({
|
||||||
session,
|
session,
|
||||||
onBack,
|
onBack,
|
||||||
|
onExecuteAction,
|
||||||
}: {
|
}: {
|
||||||
session: { draft?: { title: string } | null };
|
session: { draft?: { title: string } | null };
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
onExecuteAction: (payload: { action: string }) => void;
|
||||||
}) => (
|
}) => (
|
||||||
<div className="big-fish-result-view-mock">
|
<div className="big-fish-result-view-mock">
|
||||||
<div>大鱼吃小鱼结果页</div>
|
<div>大鱼吃小鱼结果页</div>
|
||||||
<div>{session.draft?.title ?? '缺少草稿标题'}</div>
|
<div>{session.draft?.title ?? '缺少草稿标题'}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onExecuteAction({ action: 'big_fish_publish_game' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
发布
|
||||||
|
</button>
|
||||||
<button type="button" onClick={onBack}>
|
<button type="button" onClick={onBack}>
|
||||||
返回
|
返回
|
||||||
</button>
|
</button>
|
||||||
@@ -1815,6 +1826,102 @@ test('big fish draft card restores the bound agent session and opens the result
|
|||||||
expect(screen.getByText('我想做机械深海里微生物互相吞并进化。')).toBeTruthy();
|
expect(screen.getByText('我想做机械深海里微生物互相吞并进化。')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('big fish result publish action refreshes creation works', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const baseBigFishSession = (await getBigFishCreationSession('big-fish-session-1'))
|
||||||
|
.session;
|
||||||
|
vi.mocked(getBigFishCreationSession).mockClear();
|
||||||
|
vi.mocked(listBigFishWorks).mockClear();
|
||||||
|
const publishedBigFishSession = {
|
||||||
|
...baseBigFishSession,
|
||||||
|
stage: 'published',
|
||||||
|
publishReady: true,
|
||||||
|
assetCoverage: {
|
||||||
|
levelMainImageReadyCount: 8,
|
||||||
|
levelMotionReadyCount: 16,
|
||||||
|
backgroundReady: true,
|
||||||
|
requiredLevelCount: 8,
|
||||||
|
publishReady: true,
|
||||||
|
blockers: [],
|
||||||
|
},
|
||||||
|
updatedAt: '2026-04-22T12:20:00.000Z',
|
||||||
|
};
|
||||||
|
vi.mocked(executeBigFishCreationAction).mockResolvedValue({
|
||||||
|
session: publishedBigFishSession,
|
||||||
|
});
|
||||||
|
vi.mocked(listBigFishWorks)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
workId: 'big-fish-work-big-fish-session-1',
|
||||||
|
sourceSessionId: 'big-fish-session-1',
|
||||||
|
title: '机械深海 大鱼吃小鱼',
|
||||||
|
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||||
|
summary: '机械微生物吞并进化',
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'draft',
|
||||||
|
updatedAt: '2026-04-22T12:10:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 8,
|
||||||
|
levelMainImageReadyCount: 8,
|
||||||
|
levelMotionReadyCount: 16,
|
||||||
|
backgroundReady: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.mockResolvedValue({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
workId: 'big-fish-work-big-fish-session-1',
|
||||||
|
sourceSessionId: 'big-fish-session-1',
|
||||||
|
title: '机械深海 大鱼吃小鱼',
|
||||||
|
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
|
||||||
|
summary: '机械微生物吞并进化',
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: '2026-04-22T12:20:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 8,
|
||||||
|
levelMainImageReadyCount: 8,
|
||||||
|
levelMotionReadyCount: 16,
|
||||||
|
backgroundReady: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TestWrapper withAuth />);
|
||||||
|
|
||||||
|
await openCreationHub(user);
|
||||||
|
const title = await screen.findByText('机械深海 大鱼吃小鱼');
|
||||||
|
const card = title.closest('.platform-surface');
|
||||||
|
if (!(card instanceof HTMLElement)) {
|
||||||
|
throw new Error('Missing big fish draft card');
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.click(card);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getBigFishCreationSession).toHaveBeenCalledWith(
|
||||||
|
'big-fish-session-1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
vi.mocked(listBigFishWorks).mockClear();
|
||||||
|
|
||||||
|
expect(await screen.findByText('大鱼吃小鱼结果页')).toBeTruthy();
|
||||||
|
await user.click(await screen.findByRole('button', { name: '发布' }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(executeBigFishCreationAction).toHaveBeenCalledWith(
|
||||||
|
'big-fish-session-1',
|
||||||
|
{
|
||||||
|
action: 'big_fish_publish_game',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(listBigFishWorks).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user