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;...
地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
用递归算法解锁「子集」问题 —— LeetCode 78题解析
文章目录 一、题目介绍二、递归思路详解:从决策树开始理解三、解法一:二叉决策树 DFS四、解法二:组合式回溯写法(推荐)五、解法对比 递归算法是编程中一种非常强大且常见的思想,它能够优雅地解决很多复杂的…...
李沐--动手学深度学习--GRU
1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...
