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: stringisStreaming?: 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 = trueif (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 {// 超过几秒没有新数据,重新检查indexsetTimeout(() => {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>
相关文章:
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 秘钥;// 流式调用pub…...
Linux应用之构建命令行解释器(bash进程)
目录 1.分析 2.打印输入提示符 3.读取并且处理输入字符串 4.创建子进程并切换 5.bash内部指令 6.完整代码 1.分析 当我们登录服务器的时候,命令行解释器就会自动加载出来。接下来我们就。在命令行中输入指令来达到我们想要的目的。 我们在命令行上输入的…...
php 系统命令执行及绕过
文章目录 php的基础概念php的基础语法1. PHP 基本语法结构2. PHP 变量3.输出数据4.数组5.超全局变量6.文件操作 php的命令执行可以执行命令的函数命令执行绕过利用代码中命令(如ls)执行命令替换过滤过滤特定字符串神技:利用base64编码解码的绕…...
保护大数据的最佳实践方案
在当今数字化时代,保障大数据安全的重要性再怎么强调也不为过。 随着科技的迅猛发展以及对数据驱动决策的依赖日益加深,企业必须将保护其宝贵信息置于首位。 我们将深入探讨保障大数据安全的流程,并讨论关键原则、策略、工具及技术…...
在高流量下保持WordPress网站的稳定和高效运行
随着流量的不断增加,网站的稳定和高效运行变得越来越重要,特别是使用WordPress搭建的网站。流量过高时,网站加载可能会变慢,甚至崩溃,直接影响用户体验和网站正常运营。因此,我们需要采取一些有效的措施&am…...
Redis7——基础篇(二)
前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。 基础篇: Redis(一) 接上期内容:上期完成了Redis环境的搭建。下面开始学习Redis常用命令…...
Docker 容器安装 Dify的两种方法
若 Windows 已安装 Docker,可借助 Docker 容器来安装 Dify: 一、方法一 1. 拉取 Dify 镜像 打开 PowerShell 或命令提示符(CMD),运行以下命令从 Docker Hub 拉取 Dify 的镜像(Docker Hub中找到该命令行&…...
golang常用库之-swaggo/swag根据注释生成接口文档
文章目录 golang常用库之-swaggo/swag库根据注释生成接口文档什么是swaggo/swag golang常用库之-swaggo/swag库根据注释生成接口文档 什么是swaggo/swag github:https://github.com/swaggo/swag 参考文档:https://golang.halfiisland.com/community/pk…...
docker中pull hello-world的时候出现报错
Windows下的docker中pull的时候出现下面的错误: PS C:\Users\xxx> docker pull hello-world Using default tag: latest Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connect…...
NPM环境搭建指南
NPM(Node Package Manager)是 Node.js 的包管理工具,堪称前端开发的基石。本文将手把手教你 在Mac、Windows、Linux三大系统上快速搭建NPM环境,并验证是否成功。 一、Mac系统安装NPM 方法1:通过Homebrew安装ÿ…...
【CSS进阶】常见的页面自适应的方法
在前端开发中,自适应布局(Responsive Design)是一种让网页能够适应不同屏幕尺寸、设备和分辨率的技术。常见的自适应布局方法包括 流式布局、弹性布局(Flexbox)、栅格布局(Grid)、媒体查询&…...
Linux系统配置阿里云yum源,安装docker
配置阿里云yum源 需要保证能够访问阿里云网站 可以先ping一下看看(阿里云可能禁ping,只要能够解析为正常的ip地址即可) ping mirrors.aliyun.com脚本 #!/bin/bash mkdir /etc/yum.repos.d/bak mv /etc/yum.repos.d/*.repo /etc/yum.repos…...
啥是CTF?新手如何入门CTF?网络安全零基础入门到精通实战教程!
CTF是啥 CTF 是 Capture The Flag 的简称,中文咱们叫夺旗赛,其本意是西方的一种传统运动。在比赛上两军会互相争夺旗帜,当有一方的旗帜已被敌军夺取,就代表了那一方的战败。在信息安全领域的 CTF 是说,通过各种攻击手…...
免费搭建个人网站
💡 全程零服务器、完全免费!我的个人站 guoshunfa.com ,正是基于此方案搭建,目前稳定运行。 ✅ vdoing不是基于最新的vuepress2,但是是我目前使用过最好用的主题,完全自动化,只需专心写博客。 …...
网络安全钓鱼邮件测试 网络安全 钓鱼
🍅 点击文末小卡片 ,免费获取网络安全全套资料,资料在手,涨薪更快 如今,网络安全是一个备受关注的话题,“网络钓鱼”这个词也被广泛使用。 即使您对病毒、恶意软件或如何在线保护自己一无所知,您…...
Rust编程语言入门教程(五)猜数游戏:生成、比较神秘数字并进行多次猜测
Rust 系列 🎀Rust编程语言入门教程(一)安装Rust🚪 🎀Rust编程语言入门教程(二)hello_world🚪 🎀Rust编程语言入门教程(三) Hello Cargo…...
haproxy实现MySQL服务器负载均衡
1.环境准备 准备好下面四台台服务器: 主机名IP角色open-Euler1192.168.121.150mysql-server1openEuler-2192.168.121.151mysql-server2openEuler-3192.168.121.152clientRocky8-1192.168.121.160haproxy 2.mysql服务器配置 1.下载mariadb #下载mariadb [rootop…...
Windows桌面系统管理5:Windows 10操作系统注册表
Windows桌面系统管理0:总目录-CSDN博客 Windows桌面系统管理1:计算机硬件组成及组装-CSDN博客 Windows桌面系统管理2:VMware Workstation使用和管理-CSDN博客 Windows桌面系统管理3:Windows 10操作系统部署与使用-CSDN博客 Wi…...
CSDN文章质量分查询系统【赠python爬虫、提分攻略】
CSDN文章质量分查询系统 https://www.csdn.net/qc 点击链接-----> CSDN文章质量分查询系统 <------点击链接 点击链接-----> https://www.csdn.net/qc <------点击链接 点击链接-----> CSDN文章质量分查询系统 <------点击链接 点击链…...
Mysql测试连接失败
解决方案 1 将mysql.exe(C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe)配置到系统环境变量 2 管理员权限启动cmd 输入 3 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; 4 FLUSH PRIVILEGES;...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...
23-Oracle 23 ai 区块链表(Blockchain Table)
小伙伴有没有在金融强合规的领域中遇见,必须要保持数据不可变,管理员都无法修改和留痕的要求。比如医疗的电子病历中,影像检查检验结果不可篡改行的,药品追溯过程中数据只可插入无法删除的特性需求;登录日志、修改日志…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
