feat: 流式展示设计 Agent 对话

需求:设计 Agent 对话与确认生成的回复需要实时展示。

实现:统一通过 Agent Gateway 提交 Turn,接收并去重 assistant delta,以 canonical Workspace 收口,并修复跨项目旧请求回写竞态。
This commit is contained in:
2026-08-02 21:21:50 +08:00
parent 518d7ab4cd
commit 90a249db31
9 changed files with 819 additions and 93 deletions

View File

@@ -6,6 +6,7 @@ import { ImageCanvas } from '@/pages/ImageCanvas';
import { useAuthStore } from '@/stores/auth';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
import type {
DesignAssistantDeltaEvent,
DesignGenerationTask,
DesignGenerationTaskUpdatedEvent,
DesignWorkspace,
@@ -40,6 +41,14 @@ class MockEventSource {
}
}
function deferred<T>(): { promise: Promise<T>; resolve(value: T): void } {
let resolve!: (value: T) => void;
const promise = new Promise<T>((accept) => {
resolve = accept;
});
return { promise, resolve };
}
vi.mock('@/lib/image-workspace', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/image-workspace')>();
return {
@@ -309,10 +318,57 @@ describe('ImageCanvas Workspace-first design experience', () => {
'workspace-cloud',
1,
'把主角换成暖色轮廓光',
expect.stringMatching(/^turn-/),
));
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
});
it('renders the pending user turn and assistant deltas while the command is running', async () => {
const response = deferred<DesignWorkspace>();
sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
await screen.findByText('云端角色设定');
await waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce());
fireEvent.change(screen.getByLabelText('设计需求'), {
target: { value: '让画面更有海洋呼吸感' },
});
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
expect(await screen.findByText('让画面更有海洋呼吸感')).toBeInTheDocument();
expect(screen.getByRole('status', { name: '设计 Agent 正在回复' })).toBeInTheDocument();
const pending = useImageWorkspaceStore.getState().pendingTurn!;
act(() => {
taskEventSource.emit('design.assistant.delta', {
id: 'session-one:2',
type: 'design.assistant.delta',
workspaceId: 'workspace-cloud',
clientTurnId: pending.clientTurnId,
turnRevision: pending.turnRevision,
chunkIndex: 0,
delta: '可以先增加',
} satisfies DesignAssistantDeltaEvent);
taskEventSource.emit('design.assistant.delta', {
id: 'session-one:3',
type: 'design.assistant.delta',
workspaceId: 'workspace-cloud',
clientTurnId: pending.clientTurnId,
turnRevision: pending.turnRevision,
chunkIndex: 1,
delta: '留白和水流节奏。',
} satisfies DesignAssistantDeltaEvent);
});
expect(screen.getByTestId('image-workspace-conversation'))
.toHaveTextContent('可以先增加留白和水流节奏。');
act(() => response.resolve({
...workspaceFixture(),
turnRevision: 2,
viewRevision: 2,
}));
await waitFor(() => expect(useImageWorkspaceStore.getState().pendingTurn).toBeNull());
});
it('refreshes tasks after an Agent turn creates a generation task', async () => {
const queuedTask = {
...taskFixture,
@@ -337,6 +393,7 @@ describe('ImageCanvas Workspace-first design experience', () => {
'workspace-cloud',
1,
confirmationReply,
expect.stringMatching(/^turn-/),
));
await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
expect(await screen.findByTestId('design-task-task-two')).toBeInTheDocument();
@@ -357,7 +414,12 @@ describe('ImageCanvas Workspace-first design experience', () => {
fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' }));
await waitFor(() => expect(confirmImageWorkspaceGenerationMock)
.toHaveBeenCalledWith('workspace-cloud', 1, 'quote-one'));
.toHaveBeenCalledWith(
'workspace-cloud',
1,
'quote-one',
expect.stringMatching(/^turn-/),
));
await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
});