1
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { BigFishAgentWorkspace } from './BigFishAgentWorkspace';
|
||||
|
||||
const baseSession: BigFishSessionSnapshotResponse = {
|
||||
sessionId: 'big-fish-session-1',
|
||||
currentTurn: 3,
|
||||
progressPercent: 64,
|
||||
stage: 'collecting_anchors',
|
||||
anchorPack: {
|
||||
gameplayPromise: {
|
||||
key: 'gameplayPromise',
|
||||
label: '玩法承诺',
|
||||
value: '从微光小鱼一路吞噬成长为深海巨兽',
|
||||
status: 'confirmed',
|
||||
},
|
||||
ecologyVisualTheme: {
|
||||
key: 'ecologyVisualTheme',
|
||||
label: '生态视觉主题',
|
||||
value: '幽蓝珊瑚海沟',
|
||||
status: 'confirmed',
|
||||
},
|
||||
growthLadder: {
|
||||
key: 'growthLadder',
|
||||
label: '成长阶梯',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
riskTempo: {
|
||||
key: 'riskTempo',
|
||||
label: '风险节奏',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
},
|
||||
draft: null,
|
||||
assetSlots: [],
|
||||
assetCoverage: {
|
||||
levelMainImageReadyCount: 0,
|
||||
levelMotionReadyCount: 0,
|
||||
backgroundReady: false,
|
||||
requiredLevelCount: 8,
|
||||
publishReady: false,
|
||||
blockers: [],
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
id: 'message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '爽点和生态已经清楚,继续补剩余关键词。',
|
||||
createdAt: '2026-04-24T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
lastAssistantReply: '爽点和生态已经清楚,继续补剩余关键词。',
|
||||
publishReady: false,
|
||||
updatedAt: '2026-04-24T10:00:00.000Z',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
});
|
||||
|
||||
test('big fish workspace submits quick keyword fill request after two turns', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitMessage = vi.fn();
|
||||
|
||||
render(
|
||||
<BigFishAgentWorkspace
|
||||
session={baseSession}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={onSubmitMessage}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余关键字' }));
|
||||
|
||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '请补充剩余关键字。',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('big fish workspace hides keyword fill before two turns', () => {
|
||||
render(
|
||||
<BigFishAgentWorkspace
|
||||
session={{ ...baseSession, currentTurn: 1 }}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '补充剩余关键字' })).toBeNull();
|
||||
});
|
||||
@@ -84,6 +84,17 @@ export function BigFishAgentWorkspace({
|
||||
isStreamingReply={Boolean(streamingReplyText)}
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={[
|
||||
{
|
||||
key: 'summarize',
|
||||
label: '总结当前设定',
|
||||
},
|
||||
{
|
||||
key: 'quickFill',
|
||||
label: '补充剩余关键字',
|
||||
minTurn: 2,
|
||||
},
|
||||
]}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage({
|
||||
@@ -94,6 +105,15 @@ export function BigFishAgentWorkspace({
|
||||
onPrimaryAction={() => {
|
||||
onExecuteAction({ action: 'big_fish_compile_draft' });
|
||||
}}
|
||||
onQuickAction={(action) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||
text:
|
||||
action.key === 'quickFill'
|
||||
? '请补充剩余关键字。'
|
||||
: '请总结一下当前已经成形的大鱼吃小鱼设定。',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
BigFishAssetSlotResponse,
|
||||
BigFishRuntimeEntityResponse,
|
||||
BigFishRuntimeSnapshotResponse,
|
||||
SubmitBigFishInputRequest,
|
||||
} from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type BigFishRuntimeShellProps = {
|
||||
run: BigFishRuntimeSnapshotResponse | null;
|
||||
assetSlots?: BigFishAssetSlotResponse[];
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onBack: () => void;
|
||||
@@ -54,32 +57,87 @@ function projectEntity(
|
||||
};
|
||||
}
|
||||
|
||||
function findBigFishAssetSlot(
|
||||
slots: BigFishAssetSlotResponse[],
|
||||
assetKind: string,
|
||||
level?: number,
|
||||
motionKey?: string,
|
||||
) {
|
||||
return slots.find((slot) => {
|
||||
if (slot.assetKind !== assetKind || slot.status !== 'ready') {
|
||||
return false;
|
||||
}
|
||||
if (level !== undefined && slot.level !== level) {
|
||||
return false;
|
||||
}
|
||||
if (motionKey !== undefined && slot.motionKey !== motionKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveRuntimeEntityAsset(
|
||||
entity: BigFishRuntimeEntityResponse,
|
||||
assetSlots: BigFishAssetSlotResponse[],
|
||||
) {
|
||||
return (
|
||||
findBigFishAssetSlot(assetSlots, 'level_motion', entity.level, 'move_swim') ??
|
||||
findBigFishAssetSlot(assetSlots, 'level_motion', entity.level, 'idle_float') ??
|
||||
findBigFishAssetSlot(assetSlots, 'level_main_image', entity.level)
|
||||
);
|
||||
}
|
||||
|
||||
function BigFishEntityDot({
|
||||
entity,
|
||||
run,
|
||||
owned,
|
||||
assetSlots,
|
||||
}: {
|
||||
entity: BigFishRuntimeEntityResponse;
|
||||
run: BigFishRuntimeSnapshotResponse;
|
||||
owned: boolean;
|
||||
assetSlots: BigFishAssetSlotResponse[];
|
||||
}) {
|
||||
const projected = projectEntity(entity, run);
|
||||
const isLeader = run.leaderEntityId === entity.entityId;
|
||||
const assetSlot = resolveRuntimeEntityAsset(entity, assetSlots);
|
||||
const entityImageSrc = assetSlot?.assetUrl?.trim() || null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2 rounded-full border shadow-lg transition-all ${
|
||||
owned
|
||||
? isLeader
|
||||
? 'border-cyan-100 bg-cyan-300 shadow-cyan-950/30'
|
||||
: 'border-cyan-100/70 bg-cyan-500/88 shadow-cyan-950/24'
|
||||
: entity.level > run.playerLevel
|
||||
? 'border-rose-100/70 bg-rose-500/88 shadow-rose-950/24'
|
||||
: 'border-emerald-100/70 bg-emerald-400/88 shadow-emerald-950/20'
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-full border shadow-lg transition-all ${
|
||||
entityImageSrc
|
||||
? owned
|
||||
? isLeader
|
||||
? 'border-cyan-50 shadow-cyan-950/40'
|
||||
: 'border-cyan-100/80 shadow-cyan-950/28'
|
||||
: entity.level > run.playerLevel
|
||||
? 'border-rose-100/80 shadow-rose-950/28'
|
||||
: 'border-emerald-100/80 shadow-emerald-950/24'
|
||||
: owned
|
||||
? isLeader
|
||||
? 'border-cyan-100 bg-cyan-300 shadow-cyan-950/30'
|
||||
: 'border-cyan-100/70 bg-cyan-500/88 shadow-cyan-950/24'
|
||||
: entity.level > run.playerLevel
|
||||
? 'border-rose-100/70 bg-rose-500/88 shadow-rose-950/24'
|
||||
: 'border-emerald-100/70 bg-emerald-400/88 shadow-emerald-950/20'
|
||||
}`}
|
||||
style={projected}
|
||||
>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-[0.62rem] font-black text-slate-950">
|
||||
{entityImageSrc ? (
|
||||
<>
|
||||
<ResolvedAssetImage
|
||||
src={entityImageSrc}
|
||||
alt={`Lv.${entity.level} 实体`}
|
||||
className={`h-full w-full object-cover ${
|
||||
owned && isLeader ? 'scale-110' : ''
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,transparent_32%,rgba(2,6,23,0.18)_72%,rgba(2,6,23,0.36)_100%)]" />
|
||||
</>
|
||||
) : null}
|
||||
<span className="absolute inset-0 flex items-center justify-center text-[0.62rem] font-black text-white [text-shadow:0_1px_2px_rgba(2,6,23,0.9)]">
|
||||
{entity.level}
|
||||
</span>
|
||||
</div>
|
||||
@@ -88,6 +146,7 @@ function BigFishEntityDot({
|
||||
|
||||
export function BigFishRuntimeShell({
|
||||
run,
|
||||
assetSlots = [],
|
||||
isBusy = false,
|
||||
error = null,
|
||||
onBack,
|
||||
@@ -142,10 +201,20 @@ export function BigFishRuntimeShell({
|
||||
|
||||
const statusLabel =
|
||||
run.status === 'won' ? '通关' : run.status === 'failed' ? '失败' : '进行中';
|
||||
const backgroundAsset =
|
||||
findBigFishAssetSlot(assetSlots, 'stage_background')?.assetUrl?.trim() || null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex justify-center bg-slate-950 text-white">
|
||||
<div className="relative h-full w-full max-w-[430px] overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(34,211,238,0.2),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(16,185,129,0.18),transparent_26%),linear-gradient(180deg,#082f49,#020617)]">
|
||||
{backgroundAsset ? (
|
||||
<ResolvedAssetImage
|
||||
src={backgroundAsset}
|
||||
alt="大鱼吃小鱼场地背景"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(8,47,73,0.2),rgba(2,6,23,0.6))]" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:32px_32px] opacity-30" />
|
||||
|
||||
<div className="absolute left-0 top-0 z-20 flex w-full items-center justify-between px-4 py-4">
|
||||
@@ -168,6 +237,7 @@ export function BigFishRuntimeShell({
|
||||
entity={entity}
|
||||
run={run}
|
||||
owned={false}
|
||||
assetSlots={assetSlots}
|
||||
/>
|
||||
))}
|
||||
{run.ownedEntities.map((entity) => (
|
||||
@@ -176,6 +246,7 @@ export function BigFishRuntimeShell({
|
||||
entity={entity}
|
||||
run={run}
|
||||
owned
|
||||
assetSlots={assetSlots}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -61,11 +61,11 @@ import {
|
||||
} from '../../services/puzzle-agent';
|
||||
import { getPuzzleGalleryDetail } from '../../services/puzzle-gallery';
|
||||
import {
|
||||
advancePuzzleNextLevel,
|
||||
dragPuzzlePieceOrGroup,
|
||||
startPuzzleRun,
|
||||
swapPuzzlePieces,
|
||||
} from '../../services/puzzle-runtime';
|
||||
advanceLocalPuzzleLevel,
|
||||
dragLocalPuzzlePiece,
|
||||
startLocalPuzzleRun,
|
||||
swapLocalPuzzlePieces,
|
||||
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
|
||||
import { deletePuzzleWork, listPuzzleWorks } from '../../services/puzzle-works';
|
||||
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry';
|
||||
import { getRpgEntryWorldGalleryDetailByCode } from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||
@@ -1028,8 +1028,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const { run } = await startPuzzleRun({ profileId });
|
||||
setPuzzleRun(run);
|
||||
const { item } = await getPuzzleGalleryDetail(profileId);
|
||||
setSelectedPuzzleDetail(item);
|
||||
setPuzzleRun(startLocalPuzzleRun(item));
|
||||
setSelectionStage('puzzle-runtime');
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图玩法失败。'));
|
||||
@@ -1064,28 +1065,19 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
|
||||
const swapPuzzlePiecesInRun = useCallback(
|
||||
async (payload: { firstPieceId: string; secondPieceId: string }) => {
|
||||
(payload: { firstPieceId: string; secondPieceId: string }) => {
|
||||
if (!puzzleRun || isPuzzleBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPuzzleBusy(true);
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const { run } = await swapPuzzlePieces(puzzleRun.runId, payload);
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '交换拼图块失败。'));
|
||||
} finally {
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
setPuzzleRun(swapLocalPuzzlePieces(puzzleRun, payload));
|
||||
},
|
||||
[isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage],
|
||||
[isPuzzleBusy, puzzleRun],
|
||||
);
|
||||
|
||||
const dragPuzzlePiece = useCallback(
|
||||
async (payload: {
|
||||
(payload: {
|
||||
pieceId: string;
|
||||
targetRow: number;
|
||||
targetCol: number;
|
||||
@@ -1094,19 +1086,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPuzzleBusy(true);
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const { run } = await dragPuzzlePieceOrGroup(puzzleRun.runId, payload);
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '拖动拼图块失败。'));
|
||||
} finally {
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
setPuzzleRun(dragLocalPuzzlePiece(puzzleRun, payload));
|
||||
},
|
||||
[isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage],
|
||||
[isPuzzleBusy, puzzleRun],
|
||||
);
|
||||
|
||||
const advancePuzzleLevel = useCallback(async () => {
|
||||
@@ -1114,18 +1097,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPuzzleBusy(true);
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const { run } = await advancePuzzleNextLevel(puzzleRun.runId);
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '进入下一关失败。'));
|
||||
} finally {
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
}, [isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage]);
|
||||
setPuzzleRun(advanceLocalPuzzleLevel(puzzleRun));
|
||||
}, [isPuzzleBusy, puzzleRun]);
|
||||
|
||||
const leaveAgentWorkspace = useCallback(() => {
|
||||
enterCreateTab();
|
||||
@@ -1872,6 +1846,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<BigFishRuntimeShell
|
||||
run={bigFishRun}
|
||||
assetSlots={bigFishSession?.assetSlots ?? []}
|
||||
isBusy={isBigFishBusy}
|
||||
error={bigFishError}
|
||||
onBack={() => {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import { PuzzleAgentWorkspace } from './PuzzleAgentWorkspace';
|
||||
|
||||
const baseSession: PuzzleAgentSessionSnapshot = {
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 3,
|
||||
progressPercent: 62,
|
||||
stage: 'collecting_anchors',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'themePromise',
|
||||
label: '题材承诺',
|
||||
value: '雾港遗迹拼图',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visualSubject',
|
||||
label: '画面主体',
|
||||
value: '潮雾中的灯塔与断桥',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visualMood',
|
||||
label: '视觉气质',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'compositionHooks',
|
||||
label: '拼图记忆点',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tagsAndForbidden',
|
||||
label: '标签与禁忌',
|
||||
value: '',
|
||||
status: 'missing',
|
||||
},
|
||||
},
|
||||
draft: null,
|
||||
messages: [
|
||||
{
|
||||
id: 'message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '画面主体已经清楚,继续收束剩余关键词。',
|
||||
createdAt: '2026-04-24T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
lastAssistantReply: '画面主体已经清楚,继续收束剩余关键词。',
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-04-24T10:00:00.000Z',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
});
|
||||
|
||||
test('puzzle workspace submits quick keyword fill request after two turns', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitMessage = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={baseSession}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={onSubmitMessage}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '补充剩余关键字' }));
|
||||
|
||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: '请补充剩余关键字。',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('puzzle workspace hides keyword fill before two turns', () => {
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={{ ...baseSession, currentTurn: 1 }}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '补充剩余关键字' })).toBeNull();
|
||||
});
|
||||
@@ -100,6 +100,17 @@ export function PuzzleAgentWorkspace({
|
||||
isStreamingReply={Boolean(streamingReplyText)}
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={[
|
||||
{
|
||||
key: 'summarize',
|
||||
label: '总结当前设定',
|
||||
},
|
||||
{
|
||||
key: 'quickFill',
|
||||
label: '补充剩余关键字',
|
||||
minTurn: 2,
|
||||
},
|
||||
]}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage({
|
||||
@@ -110,6 +121,15 @@ export function PuzzleAgentWorkspace({
|
||||
onPrimaryAction={() => {
|
||||
onExecuteAction({ action: 'compile_puzzle_draft' });
|
||||
}}
|
||||
onQuickAction={(action) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||
text:
|
||||
action.key === 'quickFill'
|
||||
? '请补充剩余关键字。'
|
||||
: '请总结一下当前已经成形的拼图设定。',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ export function useRpgCreationSessionController(
|
||||
const hasRequestedInitialAgentWorkspaceAuthRef = useRef(false);
|
||||
const isAgentDraftResultAutoOpenSuppressedRef = useRef(false);
|
||||
const currentAgentSessionIdRef = useRef<string | null>(null);
|
||||
const activeAgentReplyAbortControllerRef = useRef<AbortController | null>(
|
||||
null,
|
||||
);
|
||||
const latestAgentSessionSyncRequestIdRef = useRef(0);
|
||||
|
||||
const [isCreatingAgentSession, setIsCreatingAgentSession] = useState(false);
|
||||
@@ -128,6 +131,11 @@ export function useRpgCreationSessionController(
|
||||
latestAgentSessionSyncRequestIdRef.current += 1;
|
||||
}, []);
|
||||
|
||||
const abortActiveAgentReplyStream = useCallback(() => {
|
||||
activeAgentReplyAbortControllerRef.current?.abort();
|
||||
activeAgentReplyAbortControllerRef.current = null;
|
||||
}, []);
|
||||
|
||||
const mergePendingAgentUserMessageIntoSession = useCallback(
|
||||
(
|
||||
session: CustomWorldAgentSessionSnapshot | null,
|
||||
@@ -223,8 +231,26 @@ export function useRpgCreationSessionController(
|
||||
setSelectionStage('agent-workspace');
|
||||
}, [enterCreateTab, openLoginModal, persistAgentUiState, setSelectionStage, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectionStage !== 'agent-workspace' &&
|
||||
selectionStage !== 'custom-world-generating'
|
||||
) {
|
||||
abortActiveAgentReplyStream();
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
}
|
||||
}, [abortActiveAgentReplyStream, selectionStage]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortActiveAgentReplyStream();
|
||||
};
|
||||
}, [abortActiveAgentReplyStream]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAgentSessionId) {
|
||||
abortActiveAgentReplyStream();
|
||||
invalidateAgentSessionSyncRequests();
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
@@ -238,6 +264,7 @@ export function useRpgCreationSessionController(
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
abortActiveAgentReplyStream();
|
||||
invalidateAgentSessionSyncRequests();
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
@@ -255,6 +282,7 @@ export function useRpgCreationSessionController(
|
||||
activeAgentSessionId === initialAgentUiStateRef.current.activeSessionId;
|
||||
|
||||
if (currentAgentSessionIdRef.current !== activeAgentSessionId) {
|
||||
abortActiveAgentReplyStream();
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
@@ -306,6 +334,7 @@ export function useRpgCreationSessionController(
|
||||
};
|
||||
}, [
|
||||
activeAgentSessionId,
|
||||
abortActiveAgentReplyStream,
|
||||
enterCreateTab,
|
||||
invalidateAgentSessionSyncRequests,
|
||||
persistAgentUiState,
|
||||
@@ -540,6 +569,8 @@ export function useRpgCreationSessionController(
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(true);
|
||||
setPendingAgentUserMessage(pendingMessagePayload);
|
||||
const replyAbortController = new AbortController();
|
||||
activeAgentReplyAbortControllerRef.current = replyAbortController;
|
||||
setAgentSession((current) =>
|
||||
mergePendingAgentUserMessageIntoSession(current, pendingMessagePayload),
|
||||
);
|
||||
@@ -550,10 +581,17 @@ export function useRpgCreationSessionController(
|
||||
payload,
|
||||
{
|
||||
onUpdate: (text) => {
|
||||
if (replyAbortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
setStreamingAgentReplyText(text);
|
||||
},
|
||||
signal: replyAbortController.signal,
|
||||
},
|
||||
);
|
||||
if (replyAbortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
const mergedNextSession = mergePendingAgentUserMessageIntoSession(
|
||||
nextSession,
|
||||
pendingMessagePayload,
|
||||
@@ -568,6 +606,9 @@ export function useRpgCreationSessionController(
|
||||
hasServerEchoedPendingMessage ? null : pendingMessagePayload,
|
||||
);
|
||||
} catch (error) {
|
||||
if (replyAbortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
const errorMessage = resolveRpgCreationErrorMessage(
|
||||
error,
|
||||
'发送共创消息失败。',
|
||||
@@ -597,7 +638,12 @@ export function useRpgCreationSessionController(
|
||||
setStreamingAgentReplyText('');
|
||||
persistAgentUiState(activeAgentSessionId, null);
|
||||
} finally {
|
||||
setIsStreamingAgentReply(false);
|
||||
if (activeAgentReplyAbortControllerRef.current === replyAbortController) {
|
||||
activeAgentReplyAbortControllerRef.current = null;
|
||||
}
|
||||
if (!replyAbortController.signal.aborted) {
|
||||
setIsStreamingAgentReply(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user