Files
MyGoNavi/frontend/src/components/ai/useAIChatStreamSubscription.ts
2026-06-09 03:05:00 +08:00

321 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef } from 'react';
import type { MutableRefObject } from 'react';
import { EventsOn, EventsOff } from '../../../wailsjs/runtime';
import { useStore } from '../../store';
import type {
AIChatMessage,
AIToolCall,
JVMAIPlanContext,
JVMDiagnosticPlanContext,
} from '../../types';
import { sanitizeErrorMsg } from '../../utils/aiChatRuntime';
import { toAIRequestMessage } from '../../utils/aiMessagePayload';
import type { AIChatToolDefinition } from '../../utils/aiToolRegistry';
interface AIChatStreamChunk {
content?: string;
thinking?: string;
reasoning_content?: string;
tool_calls?: AIToolCall[];
done?: boolean;
error?: string;
}
interface UseAIChatStreamSubscriptionOptions {
sid: string;
sending: boolean;
setSending: (sending: boolean) => void;
availableTools: AIChatToolDefinition[];
addAIChatMessage: (sid: string, message: AIChatMessage) => void;
updateAIChatMessage: (
sid: string,
messageId: string,
patch: Partial<AIChatMessage>,
) => void;
buildSystemContextMessages: (
overrideJVMPlanContext?: JVMAIPlanContext,
overrideJVMDiagnosticPlanContext?: JVMDiagnosticPlanContext,
) => any[] | Promise<any[]>;
executeLocalTools: (toolCalls: AIToolCall[], currentAsstMsgId: string) => void | Promise<void>;
generateTitleForSession: (sid: string) => void | Promise<void>;
nextMessageId: () => string;
nudgeCountRef: MutableRefObject<number>;
pendingJVMPlanContextRef: MutableRefObject<JVMAIPlanContext | undefined>;
pendingJVMDiagnosticPlanContextRef: MutableRefObject<JVMDiagnosticPlanContext | undefined>;
}
export const useAIChatStreamSubscription = ({
sid,
sending,
setSending,
availableTools,
addAIChatMessage,
updateAIChatMessage,
buildSystemContextMessages,
executeLocalTools,
generateTitleForSession,
nextMessageId,
nudgeCountRef,
pendingJVMPlanContextRef,
pendingJVMDiagnosticPlanContextRef,
}: UseAIChatStreamSubscriptionOptions) => {
const sendingRef = useRef(sending);
useEffect(() => {
sendingRef.current = sending;
}, [sending]);
useEffect(() => {
const eventName = `ai:stream:${sid}`;
let assistantMsgId = '';
let isFirstCompletion = false;
// 缓冲高频 token避免把流式吞吐直接转成同步重绘风暴
const streamBuffer = { thinking: '', reasoningContent: '', content: '' };
let flushPending = false;
const flushStreamBuffer = () => {
if (!assistantMsgId) return;
const current = useStore.getState().aiChatHistory[sid];
const existing = current?.find((message) => message.id === assistantMsgId);
if (!existing) return;
const updates: Partial<AIChatMessage> = {};
if (streamBuffer.thinking) {
updates.thinking = (existing.thinking || '') + streamBuffer.thinking;
updates.phase = 'thinking';
streamBuffer.thinking = '';
}
if (streamBuffer.reasoningContent) {
updates.reasoning_content = (existing.reasoning_content || '') + streamBuffer.reasoningContent;
streamBuffer.reasoningContent = '';
}
if (streamBuffer.content) {
updates.content = (existing.content || '') + streamBuffer.content;
updates.phase = 'generating';
streamBuffer.content = '';
}
if (Object.keys(updates).length > 0) {
updateAIChatMessage(sid, assistantMsgId, updates);
}
flushPending = false;
};
const handler = (data: AIChatStreamChunk) => {
if (!assistantMsgId) {
const history = useStore.getState().aiChatHistory[sid] || [];
const lastMsg = history[history.length - 1];
if (lastMsg && lastMsg.role === 'assistant' && lastMsg.loading && lastMsg.phase === 'connecting') {
assistantMsgId = lastMsg.id;
updateAIChatMessage(sid, assistantMsgId, { content: '' });
}
}
if (data.error) {
const cleanErr = sanitizeErrorMsg(data.error);
const rawErr = cleanErr !== data.error ? data.error : undefined;
if (assistantMsgId) {
updateAIChatMessage(sid, assistantMsgId, {
content: `❌ 错误: ${cleanErr}`,
phase: 'idle',
loading: false,
rawError: rawErr,
});
} else {
addAIChatMessage(sid, {
id: nextMessageId(),
role: 'assistant',
phase: 'idle',
content: `❌ 错误: ${cleanErr}`,
rawError: rawErr,
timestamp: Date.now(),
jvmPlanContext: pendingJVMPlanContextRef.current,
jvmDiagnosticPlanContext: pendingJVMDiagnosticPlanContextRef.current,
});
}
assistantMsgId = '';
setSending(false);
return;
}
if (data.tool_calls && data.tool_calls.length > 0) {
if (assistantMsgId) {
updateAIChatMessage(sid, assistantMsgId, { tool_calls: data.tool_calls, phase: 'tool_calling' });
} else {
assistantMsgId = nextMessageId();
addAIChatMessage(sid, {
id: assistantMsgId,
role: 'assistant',
phase: 'tool_calling',
content: '',
tool_calls: data.tool_calls,
timestamp: Date.now(),
loading: true,
jvmPlanContext: pendingJVMPlanContextRef.current,
jvmDiagnosticPlanContext: pendingJVMDiagnosticPlanContextRef.current,
});
}
}
const displayThinking = data.thinking || data.reasoning_content || '';
if (displayThinking || data.reasoning_content) {
if (!assistantMsgId) {
assistantMsgId = nextMessageId();
addAIChatMessage(sid, {
id: assistantMsgId,
role: 'assistant',
phase: 'thinking',
content: '',
thinking: displayThinking || undefined,
reasoning_content: data.reasoning_content || undefined,
timestamp: Date.now(),
loading: true,
jvmPlanContext: pendingJVMPlanContextRef.current,
jvmDiagnosticPlanContext: pendingJVMDiagnosticPlanContextRef.current,
});
if (sendingRef.current) setSending(false);
} else {
streamBuffer.thinking += displayThinking;
if (data.reasoning_content) {
streamBuffer.reasoningContent += data.reasoning_content;
}
if (sendingRef.current) setSending(false);
}
}
if (data.content) {
if (!assistantMsgId) {
assistantMsgId = nextMessageId();
addAIChatMessage(sid, {
id: assistantMsgId,
role: 'assistant',
phase: 'generating',
content: data.content,
timestamp: Date.now(),
loading: true,
jvmPlanContext: pendingJVMPlanContextRef.current,
jvmDiagnosticPlanContext: pendingJVMDiagnosticPlanContextRef.current,
});
setSending(false);
const currentHistory = useStore.getState().aiChatHistory[sid] || [];
if (currentHistory.length <= 1) isFirstCompletion = true;
} else {
streamBuffer.content += data.content;
if (sendingRef.current) setSending(false);
}
}
if (streamBuffer.thinking || streamBuffer.reasoningContent || streamBuffer.content) {
if (!flushPending) {
flushPending = true;
requestAnimationFrame(flushStreamBuffer);
}
}
if (data.done) {
if (streamBuffer.thinking || streamBuffer.reasoningContent || streamBuffer.content) {
flushStreamBuffer();
}
const doneAssistantId = assistantMsgId;
const doneIsFirst = isFirstCompletion;
assistantMsgId = '';
setTimeout(() => {
const currentMsgs = useStore.getState().aiChatHistory[sid] || [];
for (const msg of currentMsgs) {
if (msg.id !== doneAssistantId && msg.loading && msg.phase === 'connecting') {
updateAIChatMessage(sid, msg.id, { loading: false, phase: 'idle' });
}
}
if (doneAssistantId) {
const current = useStore.getState().aiChatHistory[sid];
const existing = current?.find((message) => message.id === doneAssistantId);
if (existing && existing.tool_calls && existing.tool_calls.length > 0) {
nudgeCountRef.current = 0;
setTimeout(() => executeLocalTools(existing.tool_calls!, doneAssistantId), 50);
return;
}
if (
existing &&
nudgeCountRef.current < 2 &&
/(?:让我|我先|我来|现在|接下来|下面).*(?:查询|查找|获取|查看|检查|调用)|(?:获取|查询|查找|查看).*(?:信息|字段|列表|数据)[:]?\s*$/.test(existing.content || '')
) {
nudgeCountRef.current += 1;
updateAIChatMessage(sid, doneAssistantId, { loading: false, phase: 'idle' });
(async () => {
try {
const currentHistory = useStore.getState().aiChatHistory[sid] || [];
const messagesPayload = currentHistory.map(toAIRequestMessage);
const sysMessages = await buildSystemContextMessages(
existing.jvmPlanContext,
existing.jvmDiagnosticPlanContext,
);
messagesPayload.push({
role: 'user',
content: '请直接使用 function call 调用工具执行操作,不要只用文字描述计划。',
});
const allMsg = [...sysMessages, ...messagesPayload];
const service = (window as any).go?.aiservice?.Service;
if (service?.AIChatStream) {
await service.AIChatStream(sid, allMsg, availableTools);
}
} catch (error) {
console.error('Nudge failed', error);
setSending(false);
}
})();
return;
}
if (doneIsFirst) generateTitleForSession(sid);
const hasContent = !!existing?.content?.trim();
const hasThinking = !!existing?.thinking?.trim();
const hasTools = !!existing?.tool_calls?.length;
if (!hasContent && !hasThinking && !hasTools) {
updateAIChatMessage(sid, doneAssistantId, {
content: '❌ 模型未能成功响应任何内容,可能遭遇频控、上下文超载或理解拒绝。',
loading: false,
phase: 'idle',
});
} else {
updateAIChatMessage(sid, doneAssistantId, { loading: false, phase: 'idle' });
}
} else {
addAIChatMessage(sid, {
id: nextMessageId(),
role: 'assistant',
content: '❌ 请求中断:未收到任何具体回复。',
timestamp: Date.now(),
loading: false,
});
}
setSending(false);
}, 50);
}
};
EventsOn(eventName, handler);
return () => {
EventsOff(eventName);
};
}, [
addAIChatMessage,
availableTools,
buildSystemContextMessages,
executeLocalTools,
generateTitleForSession,
nextMessageId,
nudgeCountRef,
pendingJVMDiagnosticPlanContextRef,
pendingJVMPlanContextRef,
setSending,
sid,
updateAIChatMessage,
]);
};