SSE 是服务器向客户端单向推送数据的轻量级解决方案 ,它的核心优势是基于 HTTP、实现简单、效率高,完美适配 AI 流式回复这类 "单向推送" 的业务场景。相比之下,轮询效率低,WebSocket 过于复杂,因此 SSE 成为你的最佳选择。
所以我们系统在做AI问答的时候,对接Dify 工作流的API,记录一下遇到的问题
javascript
async sendAIRequest(query) {
this.isStreaming = true;
this.source = new AbortController();
const signal = this.source.signal;
const requestData = {
query,
inputs: {},
response_mode: 'streaming',
user: API_CONFIG.user,
conversation_id: this.conversationId || undefined,
files: [],
auto_generate_name: true
};
try {
// 添加AI回复占位符
const assistantIndex = this.dialogList.push({
role: "assistant",
content: "",
loading: true,
files: []
}) - 1;
// ========== 改用 fetch API 发起请求 ==========
const response = await fetch(
`${API_CONFIG.baseURL}${API_CONFIG.chatPath}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_CONFIG.apiKey}`
},
body: JSON.stringify(requestData), // 转为 JSON 字符串
signal: signal // 关联 AbortController
}
);
// 检查响应是否成功
if (!response.ok) {
throw new Error(`HTTP 错误:${response.status} ${response.statusText}`);
}
// 确认响应体是 ReadableStream(SSE 流式响应)
if (!response.body) {
throw new Error("响应体不是流式数据");
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let isStreamEnd = false;
// 循环读取流数据
while (!isStreamEnd) {
const { done, value } = await reader.read();
if (done) break;
if (signal.aborted) break;
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 按 \n\n 分割完整的 SSE 事件块
const chunks = buffer.split('\n\n');
buffer = chunks.pop() || '';
// 遍历解析每个事件块
for (const chunk of chunks) {
const trimmedChunk = chunk.trim();
if (!trimmedChunk) continue;
if (trimmedChunk.startsWith('data: ')) {
const dataStr = trimmedChunk.slice(6);
if (!dataStr) continue;
try {
const eventData = JSON.parse(dataStr);
console.log('Dify SSE 事件:', eventData);
// 保存会话 ID
if (eventData.conversation_id) {
this.conversationId = eventData.conversation_id;
}
// 处理不同事件类型
switch (eventData.event) {
case 'message':
if (eventData.answer) {
this.dialogList[assistantIndex].content += eventData.answer;
this.dialogList[assistantIndex].loading = false;
this.scrollToDialogBottom();
this.$forceUpdate();
}
break;
case 'message_replace':
if (eventData.answer) {
this.dialogList[assistantIndex].content = eventData.answer;
this.dialogList[assistantIndex].loading = false;
this.scrollToDialogBottom();
this.$forceUpdate();
}
break;
case 'message_file':
if (eventData.url && eventData.type === 'image') {
this.dialogList[assistantIndex].content += `\n\n`;
this.dialogList[assistantIndex].files.push(eventData.url);
this.scrollToDialogBottom();
this.$forceUpdate();
}
break;
case 'message_end':
isStreamEnd = true;
this.dialogList[assistantIndex].loading = false;
break;
case 'error':
isStreamEnd = true;
this.dialogList[assistantIndex].content = `请求错误:${eventData.message || '未知错误'}`;
this.dialogList[assistantIndex].loading = false;
this.showTip(`AI 错误:${eventData.message}`, 'error');
break;
case 'workflow_started':
case 'node_started':
case 'workflow_finished':
case 'ping':
break;
default:
console.warn('未知事件类型:', eventData.event);
break;
}
} catch (e) {
console.error('解析 SSE 数据失败:', e, '原始数据:', dataStr);
}
}
}
}
// 最终处理无内容的情况
if (this.dialogList[assistantIndex]) {
this.dialogList[assistantIndex].loading = false;
if (!this.dialogList[assistantIndex].content) {
this.dialogList[assistantIndex].content = "暂无有效回复内容";
}
}
} catch (error) {
if (!signal.aborted) {
console.error('AI 请求失败:', error);
this.dialogList.push({
role: "assistant",
content: "抱歉,请求失败,请稍后重试。"
});
this.showTip("请求失败:" + (error.message || '网络错误'), "error");
}
} finally {
this.isStreaming = false;
this.source = null;
this.scrollToDialogBottom();
}
},
之所以 fetch 能正常工作而 Axios 不行 ,核心原因是:Axios 的设计初衷是统一浏览器和 Node.js 的请求逻辑,但浏览器与 Node.js 的「流式响应」实现体系完全不同,导致 Axios 在浏览器环境下无法暴露标准的 Web API ReadableStream ;而 fetch 是浏览器原生 API,天然适配浏览器的流式响应体系。