🐛 fix(ai): 复用待响应气泡承载发送失败状态

This commit is contained in:
Syngnat
2026-06-12 11:21:05 +08:00
parent 5061ec081a
commit ff8bf20680
4 changed files with 197 additions and 26 deletions

View File

@@ -301,8 +301,10 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
messages: allMessages,
tools: availableTools,
addAIChatMessage,
updateAIChatMessage,
setSending,
nextMessageId: genId,
pendingAssistantMessageId: connectingMsg.id,
jvmPlanContext: retryJVMPlanContext,
jvmDiagnosticPlanContext: retryJVMDiagnosticPlanContext,
});
@@ -319,6 +321,7 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
getCurrentJVMPlanContext,
getCurrentJVMDiagnosticPlanContext,
resetToolCallState,
updateAIChatMessage,
]);
useAIChatStreamSubscription({
@@ -433,8 +436,10 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
messages: allMessages,
tools: availableTools,
addAIChatMessage,
updateAIChatMessage,
setSending,
nextMessageId: genId,
pendingAssistantMessageId: connectingMsg.id,
jvmPlanContext: currentJVMPlanContext,
jvmDiagnosticPlanContext: currentJVMDiagnosticPlanContext,
unavailableContent: '❌ AI Service 未就绪',
@@ -459,6 +464,7 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
getCurrentJVMPlanContext,
getCurrentJVMDiagnosticPlanContext,
loadingModels,
updateAIChatMessage,
]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {

View File

@@ -78,6 +78,47 @@ describe('aiChatPayloadDispatch', () => {
expect(onNonStreamSuccess).toHaveBeenCalled();
});
it('settles the pending assistant message when falling back to non-stream send', async () => {
const AIChatSend = vi.fn().mockResolvedValue({
success: true,
content: 'done',
reasoning_content: 'thinking',
});
const addAIChatMessage = vi.fn();
const updateAIChatMessage = vi.fn();
const setSending = vi.fn();
(globalThis as any).window = {
go: {
aiservice: {
Service: { AIChatSend },
},
},
};
const result = await dispatchAIChatPayload({
sid: 'session-1',
messages: [{ role: 'user', content: 'hello' }],
tools: [],
addAIChatMessage,
updateAIChatMessage,
setSending,
nextMessageId: () => 'msg-send',
pendingAssistantMessageId: 'assistant-connecting',
});
expect(result).toBe('send');
expect(addAIChatMessage).not.toHaveBeenCalled();
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
content: 'done',
thinking: 'thinking',
reasoning_content: 'thinking',
loading: false,
phase: 'idle',
}));
expect(setSending).toHaveBeenCalledWith(false);
});
it('emits the unavailable message when the AI service is missing', async () => {
const addAIChatMessage = vi.fn();
const setSending = vi.fn();
@@ -101,6 +142,33 @@ describe('aiChatPayloadDispatch', () => {
expect(setSending).toHaveBeenCalledWith(false);
});
it('settles the pending assistant message when the AI service is missing', async () => {
const addAIChatMessage = vi.fn();
const updateAIChatMessage = vi.fn();
const setSending = vi.fn();
(globalThis as any).window = {};
const result = await dispatchAIChatPayload({
sid: 'session-1',
messages: [{ role: 'user', content: 'hello' }],
tools: [],
addAIChatMessage,
updateAIChatMessage,
setSending,
nextMessageId: () => 'msg-unavailable',
pendingAssistantMessageId: 'assistant-connecting',
});
expect(result).toBe('unavailable');
expect(addAIChatMessage).not.toHaveBeenCalled();
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
content: '❌ AI Service 未就绪',
loading: false,
phase: 'idle',
}));
expect(setSending).toHaveBeenCalledWith(false);
});
it('sanitizes thrown errors and preserves the raw error when the cleaned text changes', async () => {
const AIChatSend = vi.fn().mockRejectedValue(new Error('<html><title>502 Bad Gateway</title></html>'));
const addAIChatMessage = vi.fn();
@@ -131,4 +199,40 @@ describe('aiChatPayloadDispatch', () => {
}));
expect(setSending).toHaveBeenCalledWith(false);
});
it('settles the pending assistant message when streaming startup throws', async () => {
const AIChatStream = vi.fn().mockRejectedValue(new Error('<html><title>502 Bad Gateway</title></html>'));
const addAIChatMessage = vi.fn();
const updateAIChatMessage = vi.fn();
const setSending = vi.fn();
(globalThis as any).window = {
go: {
aiservice: {
Service: { AIChatStream },
},
},
};
const result = await dispatchAIChatPayload({
sid: 'session-1',
messages: [{ role: 'user', content: 'hello' }],
tools: [],
addAIChatMessage,
updateAIChatMessage,
setSending,
nextMessageId: () => 'msg-error',
pendingAssistantMessageId: 'assistant-connecting',
});
expect(result).toBe('error');
expect(addAIChatMessage).not.toHaveBeenCalled();
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
content: '❌ 发送失败: HTTP 502: 502 Bad Gateway',
rawError: '<html><title>502 Bad Gateway</title></html>',
loading: false,
phase: 'idle',
}));
expect(setSending).toHaveBeenCalledWith(false);
});
});

View File

@@ -16,8 +16,14 @@ interface DispatchAIChatPayloadOptions {
messages: any[];
tools: AIChatToolDefinition[];
addAIChatMessage: (sid: string, message: AIChatMessage) => void;
updateAIChatMessage?: (
sid: string,
messageId: string,
patch: Partial<AIChatMessage>,
) => void;
setSending: (sending: boolean) => void;
nextMessageId: () => string;
pendingAssistantMessageId?: string;
jvmPlanContext?: JVMAIPlanContext;
jvmDiagnosticPlanContext?: JVMDiagnosticPlanContext;
unavailableContent?: string;
@@ -27,13 +33,53 @@ interface DispatchAIChatPayloadOptions {
const getAIChatService = (): AIChatService | undefined =>
(window as any)?.go?.aiservice?.Service;
const settleAssistantMessage = ({
sid,
patch,
addAIChatMessage,
updateAIChatMessage,
nextMessageId,
pendingAssistantMessageId,
}: {
sid: string;
patch: Partial<AIChatMessage>;
addAIChatMessage: (sid: string, message: AIChatMessage) => void;
updateAIChatMessage?: (
sid: string,
messageId: string,
patch: Partial<AIChatMessage>,
) => void;
nextMessageId: () => string;
pendingAssistantMessageId?: string;
}) => {
const settledPatch: Partial<AIChatMessage> = {
...patch,
loading: false,
phase: 'idle',
};
if (pendingAssistantMessageId && updateAIChatMessage) {
updateAIChatMessage(sid, pendingAssistantMessageId, settledPatch);
return;
}
addAIChatMessage(sid, {
id: nextMessageId(),
role: 'assistant',
timestamp: Date.now(),
...settledPatch,
} as AIChatMessage);
};
export const dispatchAIChatPayload = async ({
sid,
messages,
tools,
addAIChatMessage,
updateAIChatMessage,
setSending,
nextMessageId,
pendingAssistantMessageId,
jvmPlanContext,
jvmDiagnosticPlanContext,
unavailableContent,
@@ -51,16 +97,20 @@ export const dispatchAIChatPayload = async ({
const rawError = result?.error || '未知错误';
const cleanError = sanitizeErrorMsg(rawError);
addAIChatMessage(sid, {
id: nextMessageId(),
role: 'assistant',
content: result?.success ? result.content : `${cleanError}`,
thinking: result?.success ? result.reasoning_content : undefined,
reasoning_content: result?.success ? result.reasoning_content : undefined,
rawError: !result?.success && cleanError !== rawError ? rawError : undefined,
timestamp: Date.now(),
jvmPlanContext,
jvmDiagnosticPlanContext,
settleAssistantMessage({
sid,
addAIChatMessage,
updateAIChatMessage,
nextMessageId,
pendingAssistantMessageId,
patch: {
content: result?.success ? result.content : `${cleanError}`,
thinking: result?.success ? result.reasoning_content : undefined,
reasoning_content: result?.success ? result.reasoning_content : undefined,
rawError: !result?.success && cleanError !== rawError ? rawError : undefined,
jvmPlanContext,
jvmDiagnosticPlanContext,
},
});
setSending(false);
if (result?.success) {
@@ -69,14 +119,19 @@ export const dispatchAIChatPayload = async ({
return 'send';
}
if (unavailableContent) {
addAIChatMessage(sid, {
id: nextMessageId(),
role: 'assistant',
content: unavailableContent,
timestamp: Date.now(),
jvmPlanContext,
jvmDiagnosticPlanContext,
const resolvedUnavailableContent = unavailableContent || (pendingAssistantMessageId ? '❌ AI Service 未就绪' : '');
if (resolvedUnavailableContent) {
settleAssistantMessage({
sid,
addAIChatMessage,
updateAIChatMessage,
nextMessageId,
pendingAssistantMessageId,
patch: {
content: resolvedUnavailableContent,
jvmPlanContext,
jvmDiagnosticPlanContext,
},
});
}
setSending(false);
@@ -84,14 +139,18 @@ export const dispatchAIChatPayload = async ({
} catch (error: any) {
const rawError = error?.message || String(error);
const cleanError = sanitizeErrorMsg(rawError);
addAIChatMessage(sid, {
id: nextMessageId(),
role: 'assistant',
content: `❌ 发送失败: ${cleanError}`,
rawError: cleanError !== rawError ? rawError : undefined,
timestamp: Date.now(),
jvmPlanContext,
jvmDiagnosticPlanContext,
settleAssistantMessage({
sid,
addAIChatMessage,
updateAIChatMessage,
nextMessageId,
pendingAssistantMessageId,
patch: {
content: `❌ 发送失败: ${cleanError}`,
rawError: cleanError !== rawError ? rawError : undefined,
jvmPlanContext,
jvmDiagnosticPlanContext,
},
});
setSending(false);
return 'error';

View File

@@ -217,8 +217,10 @@ export const useAIChatLocalTools = ({
messages: allMessages,
tools: chainTools,
addAIChatMessage: (sessionId, message) => useStore.getState().addAIChatMessage(sessionId, message),
updateAIChatMessage,
setSending,
nextMessageId,
pendingAssistantMessageId: chainConnectingMsg.id,
jvmPlanContext: inheritedJVMPlanContext,
jvmDiagnosticPlanContext: inheritedJVMDiagnosticPlanContext,
});