vue3 + thinkphp 接入 七牛云 DeepSeek-R1/V3 流式调用和非流式调用

示例

在这里插入图片描述

如何获取七牛云 Token API 密钥

https://eastern-squash-d44.notion.site/Token-API-1932c3f43aee80fa8bfafeb25f1163d8

后端

// 七牛云 DeepSeek API 地址
    private $deepseekUrl = 'https://api.qnaigc.com/v1/chat/completions';
    private $deepseekKey = '秘钥';
    
    // 流式调用
    public function qnDSchat()
    {
        // 禁用所有缓冲
        while (ob_get_level()) ob_end_clean();
    
        // 设置流式响应头(必须最先执行)
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache, must-revalidate');
        header('X-Accel-Buffering: no'); // 禁用Nginx缓冲
        header('Access-Control-Allow-Origin: *');
    
        // 获取用户输入
        $userMessage = input('get.content');
    
        // 构造API请求数据
        $data = [
            'model' => 'deepseek-v3', // 支持模型:"deepseek-r1"和"deepseek-v3"
            'messages' => [['role' => 'user', 'content' => $userMessage]],
            'stream' => true, // 启用流式响应
            'temperature' => 0.7
        ];
    
        // 初始化 cURL
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $this->deepseekUrl,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($data),
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->deepseekKey,
                'Content-Type: application/json',
                'Accept: text/event-stream'
            ],
            CURLOPT_WRITEFUNCTION => function($ch, $data) {
                // 解析七牛云返回的数据结构
                $lines = explode("\n", $data);
                foreach ($lines as $line) {
                    if (strpos($line, 'data: ') === 0) {
                        $payload = json_decode(substr($line, 6), true);
                        $content = $payload['choices'][0]['delta']['content'] ?? '';
                        
                        // 按SSE格式输出
                        echo "data: " . json_encode([
                            'content' => $content,
                            'finish_reason' => $payload['choices'][0]['finish_reason'] ?? null
                        ]) . "\n\n";
                        
                        ob_flush();
                        flush();
                    }
                }
                return strlen($data);
            },
            CURLOPT_RETURNTRANSFER => false,
            CURLOPT_TIMEOUT => 120
        ]);
    
        // 执行请求
        curl_exec($ch);
        curl_close($ch);
        exit();
    }
    
    

    // 非流式调用
    public function qnDSchat2()
    {
        $userMessage = input('post.content');
    
        // 构造API请求数据
        $data = [
            'model' => 'deepseek-v3', // 支持模型:"deepseek-r1"和"deepseek-v3"
            'messages' => [['role' => 'user', 'content' => $userMessage]],
            'temperature' => 0.7
        ];
    
        // 发起API请求
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $this->deepseekUrl,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($data),
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->deepseekKey,
                'Content-Type: application/json'
            ],
            CURLOPT_RETURNTRANSFER => true, // 获取返回结果
            CURLOPT_TIMEOUT => 120
        ]);
    
        // 执行请求并获取返回数据
        $response = curl_exec($ch);
        curl_close($ch);
    
        // 解析API返回结果
        $responseData = json_decode($response, true);
    
        // 根据实际的API响应格式返回数据
        return json([
            'content' => $responseData['choices'][0]['message']['content'] ?? '没有返回内容',
            'finish_reason' => $responseData['choices'][0]['finish_reason'] ?? null
        ]);
    }

前端

npm i markdown-it github-markdown-css
<template>
  <div class="chat-container">
    <div class="messages" ref="messagesContainer">
      <div class="default-questions">
        <div v-for="(question, index) in defaultQuestions" :key="index" @click="handleQuestionClick(question)"
          class="default-question">
          {{ question }}
        </div>
      </div>
      <div v-for="(message, index) in messages" :key="index" class="message"
        :class="{ 'user-message': message.role === 'user', 'ai-message': message.role === 'assistant' }">
        <div class="message-content">
          <!-- <span v-if="message.role === 'assistant' && message.isStreaming"></span> -->
          <div v-if="message.role === 'assistant'" v-html="message.content" class="markdown-body"></div>
          <div v-if="message.role === 'user'" v-text="message.content"></div>
        </div>
      </div>
      <div v-if="isLoading" class="orbit-spinner">
        <div class="orbit"></div>
        <div class="orbit"></div>
        <div class="orbit"></div>
      </div>
    </div>

    <div class="input-area">
      <textarea v-model="inputText" maxlength="9999" ref="inputRef"
        @keydown.enter.exact.prevent="sendMessage(inputText.trim())" placeholder="输入你的问题..."
        :disabled="isLoading"></textarea>
      <div class="input-icons">
        <button @click="sendMessage(inputText.trim())" :disabled="isLoading || !inputText.trim()" class="send-button">
          {{ isLoading ? '生成中...' : '发送' }}
        </button>
        <button @click="stopMessage" :disabled="!isLoading" class="stop-button">
          停止
        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, Ref, onMounted, onBeforeUnmount } from 'vue'
import MarkdownIt from 'markdown-it'
import 'github-markdown-css'
// import { marked } from 'marked';

interface ChatMessage {
  role: 'user' | 'assistant'
  content: string
  isStreaming?: boolean
}

const eventSource: Ref = ref(null)
const messages = ref<ChatMessage[]>([])
const inputText = ref('')
const isLoading = ref(false)
const messagesContainer = ref<HTMLElement | null>(null)
const inputRef: Ref = ref(null)
const stopReceived: Ref = ref(true)

let aiMessage: ChatMessage = {
  role: 'assistant',
  content: '',
  isStreaming: true
};

const defaultQuestions = ref([
  "中医有哪些治疗方法?",
  "中医有哪些经典著作?",
  "中医有哪些传统方剂?",
  "中医有哪些养生方法?",
])

onMounted(() => {
  setTimeout(() => {
    inputRef.value?.focus()
  }, 1000)
})

onBeforeUnmount(() => {
  stopMessage();
});

const scrollToBottom = () => {
  nextTick(() => {
    if (messagesContainer.value) {
      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
  })
}

const stopMessage = () => {
  stopReceived.value = true
  if (eventSource.value) {
    eventSource.value.close();
  }
}

// 流式接收处理
const processStreamResponse = async (userMessage: any) => {
  aiMessage = {
    role: 'assistant',
    content: '',
    isStreaming: true
  };
  stopReceived.value = false;
  messages.value.push(aiMessage);
  eventSource.value = new EventSource(`后端请求地址/qnDSchat?content=${encodeURIComponent(userMessage)}`);
  let buffer = '';
  let index = 0;
  const md = new MarkdownIt();
  const typeWriter = () => {
    if (stopReceived.value) {
      // 如果接收数据完成,则不用打字机形式一点点显示,而是把剩余数据全部显示完
      aiMessage.content = md.render(buffer); // 渲染剩余的所有内容
      // aiMessage.content = marked(buffer);
      aiMessage.isStreaming = false;
      messages.value[messages.value.length - 1] = { ...aiMessage };
      isLoading.value = false;
      nextTick(() => {
        inputRef.value?.focus();
      });
      scrollToBottom();
      return
    }
    // 确保不会超出buffer的长度
    const toRenderLength = Math.min(index + 1, buffer.length);
    if (index < buffer.length) {
      aiMessage.content = md.render(buffer.substring(0, toRenderLength));
      // aiMessage.content = marked(buffer.substring(0, toRenderLength));
      messages.value[messages.value.length - 1] = { ...aiMessage };
      index = toRenderLength; // 更新index为实际处理的长度
      setTimeout(typeWriter, 30); // 控制打字速度,30ms显示最多1个字符
      scrollToBottom()
    } else {
      // 超过几秒没有新数据,重新检查index
      setTimeout(() => {
        if (!stopReceived.value || index < buffer.length) {
          typeWriter(); // 如果还没有收到停止信号并且还有未处理的数据,则继续处理
        } else {
          aiMessage.isStreaming = false;
          messages.value[messages.value.length - 1] = { ...aiMessage };
          isLoading.value = false;
          nextTick(() => {
            inputRef.value?.focus();
          });
          scrollToBottom();
        }
      }, 2000);
    }
  };
  eventSource.value.onmessage = (e: MessageEvent) => {
    try {
      const data = JSON.parse(e.data);
      const newContent = data.choices[0].delta.content;
      if (newContent) {
        buffer += newContent; // 将新内容添加到缓冲区
        if (index === 0) {
          typeWriter();
        }
      }
      if (data.choices[0].finish_reason === 'stop') {
        stopReceived.value = true;
        eventSource.value.close();
      }
    } catch (error) {
      console.error('Parse error:', error);
    }
  };
  eventSource.value.onerror = (e: Event) => {
    console.error('EventSource failed:', e);
    isLoading.value = false;
    aiMessage.content = md.render(buffer) + '\n[模型服务过载,请稍后再试.]';
    // aiMessage.content = marked(buffer) + '\n[模型服务过载,请稍后再试.]';
    aiMessage.isStreaming = false;
    messages.value[messages.value.length - 1] = { ...aiMessage };
    scrollToBottom()
    eventSource.value.close();
  };
};

// 流式调用
const sendMessage = async (question?: any) => {
  let userMessage = question || inputText.value.trim();
  if (!userMessage || isLoading.value) return;
  inputText.value = '';
  messages.value.push({
    role: 'user',
    content: userMessage
  });
  isLoading.value = true;
  scrollToBottom();
  try {
    await processStreamResponse(userMessage);
  } catch (error) {
    console.error('Error:', error);
    messages.value.push({
      role: 'assistant',
      content: '⚠️ 请求失败,请稍后再试'
    });
    isLoading.value = false;
    nextTick(() => {
      inputRef.value?.focus();
    });
  } finally {
    scrollToBottom();
  }
};

const handleQuestionClick = (question: string) => {
  sendMessage(question);
}

// 非流式调用
// const sendMessage = async () => {
//   if (!inputText.value.trim() || isLoading.value) return

//   const userMessage = inputText.value.trim()
//   inputText.value = ''

//   // 添加用户消息
//   messages.value.push({
//     role: 'user',
//     content: userMessage
//   })

//   isLoading.value = true
//   scrollToBottom()

//   try {
//     // 调用后端接口
//     const response = await qnDeepseekChat(userMessage)

//     // 解析 AI 的回复并添加到消息中
//     const md = new MarkdownIt();
//     const markdownContent = response.content || '没有返回内容';
//     const htmlContent = md.render(markdownContent);

//     messages.value.push({
//       role: 'assistant',
//       content: htmlContent
//     })
//   } catch (error) {
//     messages.value.push({
//       role: 'assistant',
//       content: '⚠️ 请求失败,请稍后再试'
//     })
//   } finally {
//     isLoading.value = false
//     nextTick(() => {
//       inputRef.value?.focus()
//     })
//     scrollToBottom()
//   }
// }

</script>

<style scoped>
.chat-container {
  max-width: 800px;
  margin: 0 auto;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  background: #f5f5f5;
}

.message {
  margin-bottom: 20px;
}

.message-content {
  max-width: 100%;
  padding: 12px 20px;
  border-radius: 12px;
  display: inline-block;
  position: relative;
  font-size: 16px;
}

.user-message {
  text-align: right;
}

.user-message .message-content {
  background: #42b983;
  color: white;
  margin-left: auto;
}

.ai-message .message-content {
  background: white;
  border: 1px solid #ddd;
}

.input-area {
  padding: 12px 20px;
  background: #f1f1f1;
  border-top: 1px solid #ddd;
  display: flex;
  gap: 10px;
  align-items: center;
  min-height: 100px;
}

textarea {
  flex: 1;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 20px;
  height: 100%;
  max-height: 180px;
  background-color: #f1f1f1;
  font-size: 14px;
}

textarea:focus {
  outline: none;
  border: 1px solid #ddd;
}

.input-icons {
  display: flex;
  align-items: center;
  flex-direction: column;
}

.send-button {
  padding: 8px 16px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  transition: opacity 0.2s;
  font-size: 14px;
}

.send-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.stop-button {
  padding: 8px 16px;
  background: #b94a42;
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  transition: opacity 0.2s;
  font-size: 14px;
  margin-top: 5px;
}

.stop-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.default-questions {
  padding: 10px;
  margin-bottom: 10px;
  background-color: #f0f0f0;
  border-radius: 8px;
}

.default-question {
  padding: 8px;
  margin: 4px;
  cursor: pointer;
  background-color: #fff;
  border-radius: 5px;
  transition: background-color .3s ease;
}

.default-question:hover {
  background-color: #e0e0e0;
}

.orbit-spinner,
.orbit-spinner * {
  box-sizing: border-box;
}

.orbit-spinner {
  height: 55px;
  width: 55px;
  border-radius: 50%;
  perspective: 800px;
}

.orbit-spinner .orbit {
  position: absolute;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  border-radius: 50%;
}

.orbit-spinner .orbit:nth-child(1) {
  left: 0%;
  top: 0%;
  animation: orbit-spinner-orbit-one-animation 1200ms linear infinite;
  border-bottom: 3px solid #ff1d5e;
}

.orbit-spinner .orbit:nth-child(2) {
  right: 0%;
  top: 0%;
  animation: orbit-spinner-orbit-two-animation 1200ms linear infinite;
  border-right: 3px solid #ff1d5e;
}

.orbit-spinner .orbit:nth-child(3) {
  right: 0%;
  bottom: 0%;
  animation: orbit-spinner-orbit-three-animation 1200ms linear infinite;
  border-top: 3px solid #ff1d5e;
}

@keyframes orbit-spinner-orbit-one-animation {
  0% {
    transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
  }
}

@keyframes orbit-spinner-orbit-two-animation {
  0% {
    transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
  }
}

@keyframes orbit-spinner-orbit-three-animation {
  0% {
    transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
  }

  100% {
    transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
  }
}

::v-deep .markdown-body h1,
::v-deep .markdown-body h2,
::v-deep .markdown-body h3,
::v-deep .markdown-body h4,
::v-deep .markdown-body h5,
::v-deep .markdown-body h6 {
  margin: 0 !important;
}

::v-deep .markdown-body p,
::v-deep .markdown-body blockquote,
::v-deep .markdown-body ul,
::v-deep .markdown-body ol,
::v-deep .markdown-body dl,
::v-deep .markdown-body table,
::v-deep .markdown-body pre,
::v-deep .markdown-body details {
  margin: 0 !important;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/972167.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

「软件设计模式」桥接模式(Bridge Pattern)

深入解析桥接模式&#xff1a;解耦抽象与实现的艺术 一、模式思想&#xff1a;正交维度的优雅解耦 桥接模式&#xff08;Bridge Pattern&#xff09;通过分离抽象&#xff08;Abstraction&#xff09;与实现&#xff08;Implementation&#xff09;&#xff0c;使二者可以独立…

Vue2项目,商城系统

Vue2商城系统项目 商城系统 包含功能: 下单平台&#xff0c;登录&#xff0c;购物车 纯前端无后台、无数据库 &#xff01;&#xff01; 纯前端无后台、无数据库 &#xff01;&#xff01; vue2 setup语法糖写法 本项目主要使用技术&#xff1a; - 基于vue2的项目框…

百度千帆平台对接DeepSeek官方文档

目录 第一步&#xff1a;注册账号&#xff0c;开通千帆服务 第二步&#xff1a;创建应用&#xff0c;获取调用秘钥 第三步&#xff1a;调用模型&#xff0c;开启AI对话 方式一&#xff1a;通过API直接调用 方式二&#xff1a;使用SDK快速调用 方式三&#xff1a;在千帆大模…

Breakout Tool

思科 CML 使用起来还是很麻烦的&#xff0c;很多操作对于习惯了 secure crt 或者 putty 等工具的网络工程师都不友好。 Breakout Tool 提供对远程实验室中虚拟机控制台与图形界面的本地化接入能力&#xff0c;其核心特性如下&#xff1a; Console 访问&#xff1a;基于 Telnet…

若依Flowable工作流版本监听器使用方法

1.前言 本文详细介绍如何在若依Flowable工作流版本&#xff08;RuoYi-Vue-Flowable&#xff09;中配置执行监听器和任务监听器。是以我二次开发的代码为基础&#xff0c;介绍如何配置监听器&#xff0c;已解决源码在新增或删除监听器出现的问题&#xff0c;如果需要二次开发的…

深度集成DeepSeek大模型:WebSocket流式聊天实现

目录 5分钟快速接入DeepSeek大模型&#xff1a;WebSocket实时聊天指南创建应用开发后端代码 (Python/Node.js)结语 5分钟快速接入DeepSeek大模型&#xff1a;WebSocket实时聊天指南 创建应用 访问DeepSeek官网 前往 DeepSeek官网。如果还没有账号&#xff0c;需要先注册一个。…

python进阶篇-面向对象

1.对象的定义 1.1 什么是对象 面向过程&#xff1a;将程序流程化 对象&#xff1a;就是“容器“&#xff0c;是用来存储数据和功能的&#xff0c;是数据和功能的集合体。 面向对象和面向过程没有优劣之分&#xff0c;它们只是使用的场景不同罢了。 1.2 为什么要有对象 有…

Spring Boot(8)深入理解 @Autowired 注解:使用场景与实战示例

搞个引言 在 Spring 框架的开发中&#xff0c;依赖注入&#xff08;Dependency Injection&#xff0c;简称 DI&#xff09;是它的一个核心特性&#xff0c;它能够让代码更加模块化、可测试&#xff0c;并且易于维护。而 Autowired 注解作为 Spring 实现依赖注入的关键工具&…

用deepseek学大模型04-机器学习建模过程

https://metaso.cn/s/7tEmBHr 目前已经学完深度学习的数学基础&#xff0c;给出机器学习建模的基本过程 好的&#xff0c;我现在需要回答用户的问题&#xff1a;已经学完深度学习的数学基础&#xff0c;现在要给出机器学习建模的基本过程。我需要结合我搜索到的资料来构建一个详…

【网络编程】网络编程基础:TCP/UDP 协议

一、什么是网络&#xff1f; 网络是信息传输&#xff0c;接收和共享的虚拟世界&#xff0c;通过把网络上的信息汇聚在一起&#xff0c;将这些资源进行共享。 初衷&#xff1a;知识共享。这里不得不提到Internet 的历史&#xff0d;它其实是“冷战”的产物&#xff1a; 1957年…

【算法】双指针(上)

目录 双指针 左右指针(对撞指针) 快慢指针 移动零 双指针解题 复写零 暴力解题 双指针解题(快慢指针) 快乐数 双指针解题(快慢指针) 盛最多水的容器 暴力解题(会超时) 双指针解题(左右指针) 有效三角形的个数 暴力解题 双指针解题(左右指针) 双指针 常见的双指…

CES Asia 2025:构建长效价值运营体系,赋能科技产业新发展

CES Asia 2025作为亚洲消费电子技术领域的盛会&#xff0c;将带来诸多令人瞩目的创新与变革。其中&#xff0c;亮点四——增加长效价值运营体系备受关注&#xff0c;为展会的参展企业和整个科技产业发展注入了新动力。 展会将推出365天在线供需对接平台&#xff0c;打破了传统…

【亚马逊开发者账号02】终审问题SA+review_Pre-review+Doc.xlsx

1.终审问题 你好感谢您在此过程中的回复和协作。所有想要构建具有受限 SP-API 角色的公开可用应用程序的开发人员都必须与我们的解决方案架构师团队一起完成架构审核。 这将需要详细说明应用程序的数据流、个人身份信息 &#xff08;PII&#xff09; 的数据保护控制&#xff0…

DeepSeek-R1论文阅读及蒸馏模型部署

DeepSeek-R1论文阅读及蒸馏模型部署 文章目录 DeepSeek-R1论文阅读及蒸馏模型部署摘要Abstract一、DeepSeek-R1论文1. 论文摘要2. 引言3. DeepSeek-R1-Zero的方法3.1 强化学习算法3.2 奖励建模3.3 训练模版3.4 DeepSeek-R1-Zero的性能、自进化过程和顿悟时刻 4. DeepSeek-R1&am…

地理探测器数据准备及驱动因素分析

地理探测器 地理探测器是一种用于分析空间数据的工具&#xff0c;主要用于检测和量化地理现象的空间异质性。它通过分析变量在不同区域内的分布特征及其相互关系&#xff0c;帮助我们理解自然和社会现象的空间分布规律以及背后可能的驱动因素。地理探测器主要由以下几个部分组…

【数据结构】(10) 排序算法

一、排序算法 冒泡排序在C语言部分学过&#xff0c;堆排序上一章学过&#xff0c;还剩五种常见排序算法。以下默认从小到大排序。 稳定性&#xff1a;相同元素在排序过后&#xff0c;前后相对位置依旧不变。一个本身稳定的排序&#xff0c;可以改成不稳定的&#xff1b…

机器学习实战(1): 入门——什么是机器学习

机器学习入门——什么是机器学习&#xff1f; 欢迎来到“机器学习实战”系列的第一篇博文&#xff01;在这一集中&#xff0c;我们将带你了解机器学习的基本概念、主要类型以及它在现实生活中的应用。无论你是初学者还是有一定经验的开发者&#xff0c;这篇文章都会为你打下坚…

HTML【详解】input 标签

input 标签主要用于接收用户的输入&#xff0c;随 type 属性值的不同&#xff0c;变换其具体功能。 通用属性 属性属性值功能name字符串定义输入字段的名称&#xff0c;在表单提交时&#xff0c;服务器通过该名称来获取对应的值disabled布尔值禁用输入框&#xff0c;使其无法被…

《TSP6K数据集进行交通场景解析》学习笔记

paper&#xff1a;2303.02835 GitHub&#xff1a;PengtaoJiang/TSP6K: The official PyTorch code for "Traffic Scene Parsing through the TSP6K Dataset". 目录 摘要 1、介绍 2、相关工作 2.1 场景解析数据集 2.2 场景解析方法 2.3 实例分割方法 2.4 无监…

Tomcat下载,安装,配置终极版(2024)

Tomcat下载&#xff0c;安装&#xff0c;配置终极版&#xff08;2024&#xff09; 1. Tomcat下载和安装 进入Apache Tomcat官网&#xff0c;我们可以看到这样一个界面。 现在官网目前最新版是Tomcat11&#xff0c;我用的是Java17&#xff0c;在这里我们选择Tomcat10即可。Tom…