vue 对接 Dify 官方 SSE 流式响应

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![图片](${eventData.url})`;
                  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,天然适配浏览器的流式响应体系。

相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax