mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
🐛 fix(ai): 复用待响应气泡承载发送失败状态
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user