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;...

Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
HTML前端开发:JavaScript 常用事件详解
作为前端开发的核心,JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例: 1. onclick - 点击事件 当元素被单击时触发(左键点击) button.onclick function() {alert("按钮被点击了!&…...

R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...
k8s从入门到放弃之HPA控制器
k8s从入门到放弃之HPA控制器 Kubernetes中的Horizontal Pod Autoscaler (HPA)控制器是一种用于自动扩展部署、副本集或复制控制器中Pod数量的机制。它可以根据观察到的CPU利用率(或其他自定义指标)来调整这些对象的规模,从而帮助应用程序在负…...
32单片机——基本定时器
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源 1、定…...