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;...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...
NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...
【实施指南】Android客户端HTTPS双向认证实施指南
🔐 一、所需准备材料 证书文件(6类核心文件) 类型 格式 作用 Android端要求 CA根证书 .crt/.pem 验证服务器/客户端证书合法性 需预置到Android信任库 服务器证书 .crt 服务器身份证明 客户端需持有以验证服务器 客户端证书 .crt 客户端身份…...
Easy Excel
Easy Excel 一、依赖引入二、基本使用1. 定义实体类(导入/导出共用)2. 写 Excel3. 读 Excel 三、常用注解说明(完整列表)四、进阶:自定义转换器(Converter) 其它自定义转换器没生效 Easy Excel在…...
2025年全国I卷数学压轴题解答
第19题第3问: b b b 使得存在 t t t, 对于任意的 x x x, 5 cos x − cos ( 5 x t ) < b 5\cos x-\cos(5xt)<b 5cosx−cos(5xt)<b, 求 b b b 的最小值. 解: b b b 的最小值 b m i n min t max x g ( x , t ) b_{min}\min_{t} \max_{x} g(x,t) bmi…...
Life:Internship finding
1. 前言 fishwheel writes this Blog to 记录自分自身在研二下找实习的经历。When 写这篇 Blog 的时候我的最后一搏也挂掉了,只能启用保底方案了。When I 打开我的邮箱时,发现里面有 nearly 100 多封与之相关的邮件,顿时感到有些心凉&#x…...
