vue3+thinkphp接入七牛云DeepSeek-R1/V3流式调用和非流式调用
- 创业
- 2025-09-02 04:36:02

如何获取七牛云 Token API 密钥
eastern-squash-d44.notion.site/Token-API-1932c3f43aee80fa8bfafeb25f1163d8
后端 // 七牛云 DeepSeek API 地址 private $deepseekUrl = ' api.qnaigc /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 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"> <!-- 使用v-html渲染解析后的Markdown内容 --> <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="loading-indicator"> <div class="dot-flashing"></div> </div> </div> <div class="input-area"> <textarea v-model="inputText" maxlength="9999" ref="inputRef" @keydown.enter.exact.prevent="sendMessage" placeholder="输入你的问题..." :disabled="isLoading"></textarea> <div class="input-icons"> <button @click="sendMessage" :disabled="isLoading || !inputText.trim()" class="send-button"> {{ isLoading ? '生成中...' : '发送' }} </button> </div> </div> </div> </template> <script setup lang="ts"> import { ref, nextTick, Ref } from 'vue' // import { qnDeepseekChat } from '@/api/qnDeepseek' import MarkdownIt from 'markdown-it' import 'github-markdown-css' interface ChatMessage { role: 'user' | 'assistant' content: string isStreaming?: boolean } 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) // 回复是否结束 // 滚动到底部 const scrollToBottom = () => { nextTick(() => { if (messagesContainer.value) { messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight } }) } // 流式接收处理 const processStreamResponse = async (userMessage: any) => { const aiMessage: ChatMessage = { role: 'assistant', content: '', isStreaming: true }; stopReceived.value = false; messages.value.push(aiMessage); const eventSource = new EventSource(` api.ecom20200909 /Other/qnDSchat?content=${encodeURIComponent(userMessage)}`); let buffer = ''; let index = 0; const md = new MarkdownIt(); const typeWriter = () => { if(stopReceived.value) { // 如果接收数据完成,则不用打字机形式一点点显示,而是把剩余数据全部显示完 aiMessage.content = md.render(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)); messages.value[messages.value.length - 1] = { ...aiMessage }; index = toRenderLength; // 更新index为实际处理的长度 setTimeout(typeWriter, 30); // 控制打字速度,30ms显示最多1个字符 scrollToBottom() } else { // 超过几秒没有新数据,重新检查index setTimeout(() => { 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.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.close(); } } catch (error) { console.error('Parse error:', error); } }; eventSource.onerror = (e: Event) => { console.error('EventSource failed:', e); isLoading.value = false; aiMessage.content = md.render(buffer) + '\n[模型服务过载,请稍后再试.]'; aiMessage.isStreaming = false; messages.value[messages.value.length - 1] = { ...aiMessage }; scrollToBottom() eventSource.close(); }; }; // 流式调用 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 { await processStreamResponse(userMessage); // 启动流式处理 } catch (error) { console.error('Error:', error); messages.value.push({ role: 'assistant', content: '⚠️ 请求失败,请稍后再试' }); } finally { scrollToBottom(); } }; // 非流式调用 // 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; /* resize: none; */ height: 100%; max-height: 180px; /* Maximum height of 6 rows */ background-color: #f1f1f1; font-size: 14px; } /* 移除获取焦点时的边框 */ textarea:focus { outline: none; /* 移除outline */ border: 1px solid #ddd; /* 保持原边框样式,不改变 */ } .input-icons { display: flex; align-items: center; } .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; } .loading-indicator { padding: 12px; text-align: center; } .dot-flashing { position: relative; width: 10px; height: 10px; border-radius: 5px; background-color: #42b983; animation: dotFlashing 1s infinite linear alternate; } @keyframes dotFlashing { 0% { opacity: 0.2; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-5px); } 100% { opacity: 0.2; transform: translateY(0); } } @keyframes blink { 50% { opacity: 0; } } </style>vue3+thinkphp接入七牛云DeepSeek-R1/V3流式调用和非流式调用由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“vue3+thinkphp接入七牛云DeepSeek-R1/V3流式调用和非流式调用”