fix: 会话快速切换pollute message
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
|
||||
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
|
||||
- 快速切换会话或会话轮询刷新产生并发详情请求时,前端只允许最后发起的请求更新当前会话、消息、错误和加载态;旧响应不得覆盖用户最新选择。
|
||||
- 普通 JSON 消息请求的回包必须绑定发送时的会话:用户在等待期间切换到其他会话后,只更新原会话的列表摘要,不得把原会话的 `deltaMessages` 、错误或画布刷新副作用应用到当前面板。
|
||||
- 收起对话框只是隐藏面板,不卸载当前会话 hook;普通 JSON 消息请求的等待态和外部生成任务状态必须在收起 / 重新打开之间保持一致。
|
||||
|
||||
## 附件
|
||||
|
||||
+97
@@ -327,6 +327,103 @@ describe('useEditorAgentConversation', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not apply a completed message response after switching conversations', async () => {
|
||||
const client = createClient();
|
||||
const onCanvasRefreshRequested = vi.fn();
|
||||
let resolveSend!: (response: EditorAgentMessageResponse) => void;
|
||||
vi.mocked(client.sendMessage).mockImplementation(
|
||||
() =>
|
||||
new Promise<EditorAgentMessageResponse>((resolve) => {
|
||||
resolveSend = resolve;
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
useEditorAgentConversation({
|
||||
projectId: 'project-1',
|
||||
client,
|
||||
onCanvasRefreshRequested,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeConversationId).toBe('conversation-1');
|
||||
});
|
||||
|
||||
let sendPromise!: Promise<void>;
|
||||
act(() => {
|
||||
sendPromise = result.current.sendMessage('在第一个会话生成图片');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isWaiting).toBe(true);
|
||||
});
|
||||
|
||||
vi.mocked(client.getConversation).mockResolvedValueOnce({
|
||||
conversationId: 'conversation-2',
|
||||
projectId: 'project-1',
|
||||
title: '第二个会话',
|
||||
messages: [
|
||||
{
|
||||
id: 20,
|
||||
role: 'assistant',
|
||||
text: '第二个会话原有消息',
|
||||
attachments: [],
|
||||
toolCall: null,
|
||||
createdAt: '2026-07-03T00:02:00.000Z',
|
||||
},
|
||||
],
|
||||
createdAt: '2026-07-03T00:02:00.000Z',
|
||||
updatedAt: '2026-07-03T00:02:00.000Z',
|
||||
});
|
||||
await act(async () => {
|
||||
await result.current.selectConversation('conversation-2');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
resolveSend({
|
||||
conversation: {
|
||||
conversationId: 'conversation-1',
|
||||
projectId: 'project-1',
|
||||
title: '第一个会话已更新',
|
||||
updatedAt: '2026-07-03T00:03:00.000Z',
|
||||
},
|
||||
deltaMessages: [
|
||||
{
|
||||
id: 21,
|
||||
role: 'assistant',
|
||||
text: '第一个会话生成完成',
|
||||
attachments: [],
|
||||
toolCall: {
|
||||
toolName: 'generate_image',
|
||||
status: 'completed',
|
||||
externalJobId: 'task-conversation-1',
|
||||
args: {},
|
||||
displayArgs: {
|
||||
stringArgs: [],
|
||||
imageArgs: [],
|
||||
extras: { priceMudPoints: 0 },
|
||||
},
|
||||
images: [{ imageSrc: '/conversation-1-result.png' }],
|
||||
error: null,
|
||||
},
|
||||
createdAt: '2026-07-03T00:03:00.000Z',
|
||||
},
|
||||
],
|
||||
errorMessage: null,
|
||||
});
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
expect(result.current.activeConversationId).toBe('conversation-2');
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0]?.text).toBe('第二个会话原有消息');
|
||||
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
|
||||
expect(
|
||||
result.current.conversations.find(
|
||||
(conversation) => conversation.conversationId === 'conversation-1',
|
||||
)?.title,
|
||||
).toBe('第一个会话已更新');
|
||||
});
|
||||
|
||||
it('creates a conversation before sending when the project has no history', async () => {
|
||||
const client = createClient();
|
||||
vi.mocked(client.listConversations).mockResolvedValueOnce([]);
|
||||
|
||||
@@ -189,6 +189,7 @@ export function useEditorAgentConversation({
|
||||
|
||||
const applyConversationDetail = useCallback(
|
||||
(detail: EditorAgentConversationDetail) => {
|
||||
activeConversationIdRef.current = detail.conversationId;
|
||||
setConversations((currentConversations) =>
|
||||
upsertConversationSummary(
|
||||
currentConversations,
|
||||
@@ -240,6 +241,7 @@ export function useEditorAgentConversation({
|
||||
setIsLoadingMessages(false);
|
||||
if (!normalizedProjectId) {
|
||||
setConversations([]);
|
||||
activeConversationIdRef.current = null;
|
||||
setActiveConversationId(null);
|
||||
setMessages([]);
|
||||
setErrorMessage(null);
|
||||
@@ -259,6 +261,7 @@ export function useEditorAgentConversation({
|
||||
setConversations(sortedConversations);
|
||||
const firstConversation = sortedConversations[0] ?? null;
|
||||
if (!firstConversation) {
|
||||
activeConversationIdRef.current = null;
|
||||
setActiveConversationId(null);
|
||||
setMessages([]);
|
||||
return;
|
||||
@@ -413,6 +416,9 @@ export function useEditorAgentConversation({
|
||||
response.conversation,
|
||||
),
|
||||
);
|
||||
if (activeConversationIdRef.current !== conversationId) {
|
||||
return;
|
||||
}
|
||||
if (response.errorMessage) {
|
||||
setErrorMessage(response.errorMessage);
|
||||
} else {
|
||||
@@ -424,10 +430,12 @@ export function useEditorAgentConversation({
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : '发送画布 Agent 消息失败';
|
||||
setErrorMessage(message);
|
||||
setMessages((currentMessages) =>
|
||||
currentMessages.filter((message) => message !== optimisticMessage),
|
||||
);
|
||||
if (activeConversationIdRef.current === conversationId) {
|
||||
setErrorMessage(message);
|
||||
setMessages((currentMessages) =>
|
||||
currentMessages.filter((message) => message !== optimisticMessage),
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (activeRequestAbortControllerRef.current === abortController) {
|
||||
@@ -532,6 +540,7 @@ export function useEditorAgentConversation({
|
||||
await loadConversation(nextConversation.conversationId);
|
||||
return;
|
||||
}
|
||||
activeConversationIdRef.current = null;
|
||||
setActiveConversationId(null);
|
||||
setMessages([]);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user