feat: AIOps智能助手流式输出(SSE代理,逐字渲染+思考中光标+断连安全持久化)

This commit is contained in:
attacker committed 2026-06-18 05:07:28 +08:00
1 parent 139bddc9d9
commit 5d19878b28
5 files changed
+362 -31

No files matched your search

+72
View File
@@ -673,6 +673,78 @@ export const aiApi = {
// AI 对话(messages 含历史 + 当前用户消息)
chat: (messages: { role: 'user' | 'assistant'; content: string }[]) =>
http.post<ChatResult>('/aiops/chat', { messages }),
// AI 流式对话(SSE)。逐块回调 onChunk,返回完整内容。错误抛出 Error。
// 使用裸 fetch 绕过 http.ts(后者会 await json() 一次性消费) // Logan.Li
chatStream: async (
messages: { role: 'user' | 'assistant'; content: string }[],
onChunk: (delta: string) => void,
signal?: AbortSignal,
): Promise<string> => {
const token = useUserStore.getState().token;
const res = await fetch('/api/v1/aiops/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ messages }),
signal,
});
if (res.status === 401) {
useUserStore.getState().logout();
window.location.href = '/login';
throw new Error('登录已过期,请重新登录');
}
if (!res.body) throw new Error('流式响应不可用');
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let fullContent = '';
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE 事件之间以 \n\n 分隔
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (const evt of events) {
const lines = evt.split('\n');
let eventType = 'message';
let dataLines: string[] = [];
for (const ln of lines) {
if (ln.startsWith('event:')) eventType = ln.slice(6).trim();
else if (ln.startsWith('data:')) dataLines.push(ln.slice(5).trim());
}
const data = dataLines.join('');
if (!data) continue;
if (eventType === 'error') {
// {code, message}
try {
const obj = JSON.parse(data);
throw new Error(obj.message || 'LLM 流式响应错误');
} catch (parseErr) {
if (parseErr instanceof Error && parseErr.message) throw parseErr;
throw new Error('LLM 流式响应错误');
}
}
if (eventType === 'done') {
// 服务端结束(可能含 model 字段)
continue;
}
// message:data 是 {"content":"..."}
try {
const obj = JSON.parse(data);
if (typeof obj.content === 'string') {
fullContent += obj.content;
onChunk(obj.content);
}
} catch {
// 非 JSON 数据忽略(如 [DONE] 或注释)
}
}
}
return fullContent;
},
// 获取当前用户对话历史(按时间正序,最近 100 条)
getHistory: () => http.get<{ list: ChatHistoryItem[]; total: number }>('/aiops/chat-history'),
// 清空当前用户对话历史