Dify不仅提供iframe形式直接嵌入网页,还支持通过标准RESTful API实现深度集成。相比iframe方案,API访问具有以下显著优势:
1)完全自主控制请求参数和响应处理逻辑,可自定义UI交互样式;
2)支持与企业内部系统(如CRM/ERP/OA)深度对接,实现业务数据与AI能力的闭环;
3)提供细粒度权限管理和流量控制,满足企业级安全要求;
4)兼容多种数据格式(JSON/XML),适配不同开发框架。这种集成方式特别适合需要高度定制化或涉及敏感数据的业务场景

在这里插入图片描述
以聊天助手为例,调用API,制作了一个聊天助手
Dify中API调用提供了两个模式一个流式模式、一个阻塞模式
在这里插入图片描述

开发之前要明确以哪种模式调用API,聊天式的应用,以流式展示较好,那么需要有一种机制来处理这种流式对话,因为我是在vue项目开发的使用的是microsoft/fetch-event-source来处理流式对话

插件安装

npm install @microsoft/fetch-event-source

在vue的项目中的views文件夹下新建一个文件夹,在文件夹下新建ChatService.ts文件

import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ref, type Ref } from 'vue'

// 定义 Message 类型,这是对话框中的两个角色
export interface Message {
  id: string
  role: 'user' | 'assistant'
  content: string
  timestamp: number
}

export const useChatStream = () => {
  const controller = new AbortController()
  //创建一个 AbortController 实例,它是一个浏览器提供的 API,用来控制和中断异步操作(如 fetch 请求)。
  const isStreaming = ref(false)//是否开始流式
  const error = ref<Error | null>(null)//错误信息返回

  const streamChat = async (messages: Ref<Message[]>, input: string) => {
    // 添加用户消息,先是用户提问
    const userMessage: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: input,
      timestamp: Date.now()
    }
    messages.value = [...messages.value, userMessage]

    // 初始化AI助手消息(内容为空,当发送消息是开始可能需要思考一段时间才会有有效的消息传递过来)
    const assistantMessage: Message = {
      id: `temp_${Date.now()}`,
      role: 'assistant',
      content: '正在思考中...',
      timestamp: Date.now()
    }
    messages.value = [...messages.value, assistantMessage]
    isStreaming.value = true
// ‘/v1/chat-messages’是chat类应用地址,应用区分是通过Authorization值来区分,这个值获取是从对应应用API访问页面中获取到的
//body中的参数在DIfy中有相应的解释,记得 response_mode设置为streaming
    try {
      await fetchEventSource('http://192.168.92.136:8088/v1/chat-messages', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer app-XFgY43nb1CST9NUSAZjJdaT1'
        },
        body: JSON.stringify({
          inputs: { type: "chat" },
          query: input,
          response_mode: "streaming",
          user: "current_user"
        }),
        signal: controller.signal,

        // 连接建立时的回调
        async onopen(response) {
          if (!response.ok) throw new Error('服务器开小差了,状态码:' + response.status)
        },
//onmessage中就是获取到的流式数据,onmessage中如何处理需要根据返回的流式数据格式,dify中的处理是相同的,所有应用都是用一个逻辑即可
        onmessage(ev) {
          let data;
          try {
            data = JSON.parse(ev.data);
          } catch (e) {
            console.error('JSON 解析失败:', ev.data);
            return;
          }

          // 如果是第一个响应数据,则替换“正在思考中”的提示
          const lastMsg = messages.value[messages.value.length - 1];
          if (lastMsg.content === '正在思考中...' && data.answer) {
            lastMsg.content = data.answer;
          } else {
            lastMsg.content += data.answer || '';
          }

          // 更新消息列表
          messages.value = [...messages.value.slice(0, -1), lastMsg];

          if (data.event === 'message_end') {
            lastMsg.id = `msg_${Date.now()}`;
            isStreaming.value = false;
          }


        },
//这个返回的报错信息
        onclose() {
          const lastMsg = messages.value[messages.value.length - 1]
          lastMsg.id = `msg_${Date.now()}`
          isStreaming.value = false
        },

        onerror(err) {
          console.log(err, 'err....');
          error.value = new Error(`流式传输异常:${err}`)
        }
      })
    } catch (err) {
      error.value = err as Error
      isStreaming.value = false
    }
  }

  return {
    streamChat,
    isStreaming,
    error,
    cancel: () => controller.abort()
  }
}

获取应用API密钥
在这里插入图片描述
返回的流式数据格式
在这里插入图片描述
处理文件创建好之后,需要自己创建一个vue文件来实现聊天对话,在同一目录下创建difyApi.vue

<template>
  <div class="chat-container" ref="chatContainer">
    <!-- 问答区 -->
    <div class="chat-box" ref="chatBox">    
      <div v-for="msg in messages" :key="msg.id" :class="`message ${msg.role}`">
        <div class="avatar">
          <div class="timestamp">{{ formatTimestamp(msg.timestamp) }}</div>
          <img v-if="msg.role === 'user'" src="../../../assets/ai/user.png" alt="User Avatar" />
          <img v-else src="../../../assets/ai/qwen.png" alt="AI Avatar" />
        </div>
        <div class="content">
          <div class="text">{{ msg.content }}</div>
        </div>
      </div>
    </div>
    <!-- 发送区-->
    <div class="chatInput">
      <!-- 输入区域 -->
      <div class="input-content" contenteditable="true" placeholder="输入您的问题..." id="aiInput" ref="aiInput"
        @keydown.enter.exact.prevent="handleSubmit">
      </div>
      <!-- 发送按钮 -->
      <div class="send-wrapper">
        <el-icon class="send-folder">
          <FolderOpened />
        </el-icon>
        <el-icon class="send-picture">
          <Picture />
        </el-icon>
        <span class="separator">||</span>
        <el-icon class="send-icon" @click="handleSubmit" v-if="!isSending">
          <Promotion />
        </el-icon>
        <el-icon class="stop-icon" @click="cancels" v-else>
          <SwitchFilled />
        </el-icon>
      </div>
    </div>

    <!-- 错误提示 -->
    <div v-if="error" class="error">
      {{ error.message }}
      <button @click="error = null">×</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted, watch, nextTick } from "vue";
import { useChatStream, Message } from "./ChatService.ts";
import {
  Promotion,
  SwitchFilled,
  Picture,
  FolderOpened,
} from "@element-plus/icons-vue";

// 定义消息列表
const messages = ref<Message[]>([]); // 消息列表
const isSending = ref(false); // 正在发送中
const { streamChat, isStreaming, error, cancel } = useChatStream();
// 获取聊天框和输入框的引用
const chatBox = ref<HTMLElement>();
const aiInput = ref<HTMLElement | null>(null);
// 格式化时间戳函数
const formatTimestamp = (timestamp: number): string => {
  return new Date(timestamp).toLocaleTimeString();
};

const handleSubmit = () => {
  if (!aiInput.value || isStreaming.value || isSending.value) return; // 判断是否可以发送
  const inputContent = aiInput.value.innerText.trim(); // 获取输入框内容
  if (!inputContent) return; // 判断是否为空
  isSending.value = true; // 设置为发送中
  streamChat(messages, inputContent)
    .then(() => {
      isSending.value = false; // 发送完成
    })
    .catch(() => {
      isSending.value = false; // 发送失败
    });
  aiInput.value.innerText = ""; // 清空输入框
};
const cancels = () => {
  cancel();
  isSending.value = false; // 取消发送
  isStreaming.value = false; // 取消流式
};

// 监听消息列表变化,滚动到底部
watch(
  messages,
  async () => {
    await nextTick();
    setTimeout(() => {
      //回答区
      if (chatBox.value) {
        chatBox.value.scrollTop = chatBox.value.scrollHeight;
      }
    }, 100); // 100ms的延迟
    //发送区
    if (aiInput.value) {
      aiInput.value.scrollTop = aiInput.value.scrollHeight; // 滚动到输入框底部
    }
  },

  { deep: true }
);

// 取消请求
onUnmounted(() => {
  if (cancel) {
    cancel(); // 调用取消方法
  }
});

// 上传文件
const uploadFile = () => {
  // 实现文件上传逻辑
};
</script>

<style scoped>
.chat-container {
  display: flex;
  flex-direction: column;
  height: 88vh;
}

.chat-box {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
  border: 2px solid #6e4bfa;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(95, 162, 249, 0.05);
}
.welcome {
  background: linear-gradient(90deg, #335bff, #ca37ce);
  border: 1px solid #9379fe;
  border-radius: 12px;
  font-size: 24px;
  color: #fff;
}

.chat-box::-webkit-scrollbar {
  width: 8px; /* 设置滚动条宽度 */
}

.chat-box::-webkit-scrollbar-track {
  background: #f1f1f1; /* 设置滚动条轨道背景颜色 */
  border-radius: 4px; /* 设置滚动条轨道圆角 */
}

.chat-box::-webkit-scrollbar-thumb {
  background: #a5d6a7; /* 设置滚动条滑块背景颜色 */
  border-radius: 4px; /* 设置滚动条滑块圆角 */
}

.chat-box::-webkit-scrollbar-thumb:hover {
  background: #81c784; /* 设置滚动条滑块悬停时的背景颜色 */
}

.message {
  display: flex;
  margin-bottom: 10px;
}

.message.user {
  flex-direction: row-reverse; /* 用户消息内容靠左,头像在右侧 */
}

.message.ai {
  justify-content: flex-start; /* AI消息靠左 */
}

.message.user .avatar {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.message.user .avatar .timestamp {
  margin-bottom: 5px; /* 调整时间戳与头像之间的间距 */
}

.message.user .avatar img {
  margin-right: 10px; /* 用户头像在内容右侧 */
}

.message.ai .avatar img {
  margin-right: 10px; /* AI头像在内容左侧 */
}

.avatar img {
  width: 40px;
  height: 40px;
  border-radius: 50%;
}

.content {
  max-width: 70%;
  padding: 10px;
  border-radius: 10px;
  position: relative;
}

.timestamp {
  font-size: 12px;
  color: #999;
}

.text {
  margin-top: 1px;
}

.message.user .content {
  background-color: #96f499; /* 绿色背景 */
  color: #000; /* 黑色文字 */
  border: 1px solid #fff; /* 浅绿色边框 */
  align-self: flex-start; /* 用户消息内容靠左 */
  margin-top: 20px;
  margin-right: 7px;
  position: relative; /* 添加相对定位 */
  padding-right: 15px; /* 为三角形留出空间 */
}

.message.user .content::before {
  content: "";
  position: absolute;
  top: 10px; /* 调整三角形的位置 */
  right: -10px; /* 三角形在内容区域的右侧 */
  border-width: 8px;
  border-style: solid;
  border-color: #96f499 transparent transparent #96f499; /* 三角形颜色与背景色一致 */
}

.message.assistant .content {
  background-color: #fff; /* 白色背景 */
  color: #000; /* 黑色文字 */
  border: 1px solid #a5d6a7; /* 浅绿色边框 */
  align-self: flex-start;
  margin-top: 20px;
  position: relative; /* 添加相对定位 */
  padding-left: 15px; /* 为三角形留出空间 */
}

.message.assistant .content::before {
  content: "";
  position: absolute;
  top: 10px; /* 调整三角形的位置 */
  left: -8px; /* 三角形在内容区域的左侧 */
  border-width: 8px;
  border-style: solid;
  border-color: #fff #fff transparent transparent; /* 三角形颜色与背景色一致 */
}
</style>
<style scoped>
.chatInput {
  position: relative;
  border: 2px solid #6e4bfa;
  border-radius: 12px;
  background: #ffffff;
  min-height: 88px; /* 标准输入高度 */
  padding: 12px 16px;
  box-shadow: 0 2px 8px rgba(28, 31, 35, 0.05);
}

/deep/.input-content {
  min-height: 32px;
  max-height: 200px;
  overflow-y: auto;
  padding-right: 96px; /* 为按钮预留空间 */
  font: 16px/22px "PingFang SC", sans-serif;
  color: rgb(11, 11, 11) !important;
  outline: none;
  text-align: left; /* 左对齐 */
}
/deep/.input-content[contenteditable="false"] {
  background-color: #f0f0f0;
  color: #999;
}

/deep/.input-content:empty::before {
  content: attr(placeholder);
  color: #464649; /* 占位符色值 */
}

/* 发送按钮定位系统 */
.send-wrapper {
  position: absolute;
  right: 10px;
  bottom: 6px;
  z-index: 10;
}
.separator {
  display: inline-block;
  vertical-align: middle;
  font-size: 20px; /* 与图标保持一致的字体大小 */
  line-height: 1; /* 调整行高以对齐 */
  margin-right: 8px;
  margin-left: 8px;
  transform: translateY(-8px);
  color: #81c784;
}
.send-folder {
  cursor: pointer; /* 改变鼠标指针为小手 */
  transition: color 0.3s ease; /* 添加过渡效果 */
  font-size: 25px;
  color: #3e503f;
  margin-right: 5px;
}
.send-picture {
  cursor: pointer; /* 改变鼠标指针为小手 */
  transition: color 0.3s ease; /* 添加过渡效果 */
  font-size: 25px;
  color: #3e503f;
}
.send-icon {
  cursor: pointer; /* 改变鼠标指针为小手 */
  transition: color 0.3s ease; /* 添加过渡效果 */
  font-size: 28px;
  color: #81c784;
}
.send-icon:hover {
  color: #2b83ff; /* 鼠标悬停时的颜色 */
}
.stop-icon {
  cursor: pointer; /* 改变鼠标指针为小手 */
  transition: color 0.3s ease; /* 添加过渡效果 */
  font-size: 28px;
  color: #3e423e;
  margin-right: 8px;
}
</style>

在这里插入图片描述
通过以上代码实现了一个简单的聊天助手,如果想实现其他比较复杂的功能就需要自己多加深入的耕耘了,总的来说Dify作为ANGENT先驱架构还是很好用的,也算是体验上时代的潮流了。。。。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐