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;...
机器人视觉动作生成中的RFG去噪技术解析
1. 机器人视觉动作生成中的去噪技术概述在机器人视觉动作生成领域,去噪技术正成为提升模型性能的关键突破口。这项技术的核心挑战在于如何从噪声数据中准确预测出未来动作序列,同时保持计算效率以满足实时控制的需求。传统方法通常采用单步去噪策略&…...
YOLO11语义分割注意力机制改进:全网首发--使用CASAB多层注入增强多尺度特征筛选(方案3)
1. 工程简介 🚀 本工程基于 Ultralytics 框架扩展,面向语义分割与 YOLO 系列模型改进实验。核心特点是通过切换 yaml 配置文件,即可快速完成不同网络结构的训练、对比与验证,无需为每个模型单独编写训练脚本。 当前已支持的主要…...
如何在单页中初始化多个 Jodit 富文本编辑器实例
本文详解如何在同一个 HTML 页面中为多个 <textarea> 元素独立、正确地初始化 Jodit 编辑器,避免因复用单一实例导致的冲突或失效问题,并提供可直接运行的代码示例与关键注意事项。 本文详解如何在同一个 html 页面中为多个 元素独立、正确地…...
全能纯净影音播放器,通吃所有格式——PotPlayer
文章目录全能纯净影音播放器,通吃所有格式——PotPlayer核心定位官方安全下载渠道极简安装与基础配置(一步到位)1. 安装2.以下是我的常用配置推荐,按需使用核心功能全流程实操高频刚需应用场景全能纯净影音播放器,通吃…...
5个技巧掌握After Effects动画导出:Bodymovin插件完全指南
5个技巧掌握After Effects动画导出:Bodymovin插件完全指南 【免费下载链接】bodymovin-extension Bodymovin UI extension panel 项目地址: https://gitcode.com/gh_mirrors/bod/bodymovin-extension 作为一名动画设计师或前端开发者,你是否曾为A…...
AI智能体浏览器自动化实战:绕过反爬虫与验证码的终极方案
1. 项目概述:为AI智能体赋予“真实浏览器之手”如果你正在使用Claude Code、Cursor、OpenClaw这类AI编程助手,并且尝试过让它们帮你自动完成一些网页操作——比如抓取商品价格、监控新闻动态、或者自动填写表单——那你大概率经历过这样的挫败࿱…...
SPI、I2C、UART怎么选?一个实际项目中的通信协议选型踩坑与避坑指南
SPI、I2C、UART通信协议选型实战:从理论到避坑指南 在嵌入式系统设计中,通信协议的选择往往决定了整个项目的成败。作为一名经历过多次"踩坑"的工程师,我深刻体会到协议选型不仅仅是技术参数的对比,更需要考虑实际工程环…...
FlicFlac音频格式转换引擎:模块化架构与高性能编解码器集成深度解析
FlicFlac音频格式转换引擎:模块化架构与高性能编解码器集成深度解析 【免费下载链接】FlicFlac Tiny portable audio converter for Windows (WAV FLAC MP3 OGG APE M4A AAC) 项目地址: https://gitcode.com/gh_mirrors/fl/FlicFlac 在数字化音频处理领域&a…...
Dev Container配置效率暴跌87%?揭秘头部金融企业如何用自定义Dockerfile+devcontainer.json双引擎重构开发流水线(企业级配置模板首次公开)
更多请点击: https://intelliparadigm.com 第一章:Dev Container配置效率暴跌87%?真相溯源与企业级影响评估 近期多家中大型研发团队反馈,VS Code Remote-Containers 扩展在加载自定义 devcontainer.json 后,初始化耗…...
手把手教你自定义Synopsys AXI VIP的延迟参数,搞定那些烦人的超时错误
深度定制Synopsys AXI VIP:解决超时错误的实战指南 当仿真日志中频繁出现"ready/valid握手超时"的红色警告时,每个验证工程师的血压都会瞬间飙升。这些看似简单的延迟参数问题,往往会让整个验证进程陷入停滞。本文将带您深入Synops…...
