注意:因为当前deepseek服务器资源紧张, API 服务充值被停用,所以deepseek的接口是无法使用的,本篇文章的Kimi是可以正常使用的。
第一步:去官网申请API keys
deepseek api:DeepSeek 开放平台
kimi api Moonshot AI - 开放平台
注意!!!只有第一次创建的时候可以复制api key ,自己要保存好
点击创建,就可以获取api key了
第二步:配置环境变量
找到我们的.env.development 或者 .env文件
在文件里面设置我们的API keys 和 接口访问地址
# deepseek
VITE_APP_DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
VITE_APP_DEEPSEEK_API_KEY=your api keys
#kimi
VITE_APP_KIMI_API_URL=https://api.moonshot.cn/v1/chat/completions
VITE_APP_KIMI_API_KEY=your api keys
把自己的api keys替换上去就可以了,我用的构建工具是vite,所以环境变量的名称必须以VITE_APP开头
第三步:封装请求方法
在我们文件的src\http目录下创建aiRequest.ts
可以直接使用我封装的代码,代码都有注释我就不过多的阐述,大家可以自己看一下。
需要提醒的是,我的这个封装方法实现的是流式输出,打字机的动画效果,所以用的是fetch而不是axios封装。
我是做了Kimi和deepseek两个可以切换的请求。
// request.ts 修改后的请求文件
import { AxiosError } from 'axios';
import { ChatMessage } from '@/types/ai';
/** 历史消息长度 */
const MAX_HISTORY = 10;
/**
* 封装API请求,用于获取AI的流式响应
* @param messages - 历史消息列表
* @param aiType - AI类型,可选值为 'KIMI' 或 'DEEPSEEK'
* @param onStream - 流式数据回调函数,接收到新的数据块时调用
*/
const fetchAIResponse = async (
messages: ChatMessage[],
aiType: 'KIMI' | 'DEEPSEEK',
onStream?: (chunk: string) => void
) => {
const url = import.meta.env[`VITE_APP_${aiType}_API_URL`];
const key = import.meta.env[`VITE_APP_${aiType}_API_KEY`];
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${key}`
},
body: JSON.stringify({
model: aiType === 'KIMI' ? 'moonshot-v1-8k' : 'deepseek-chat',
messages: messages.slice(-MAX_HISTORY),
temperature: 0.7,
stream: true
})
});
// 检查响应状态是否正常
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取响应体的读取器
const reader = response.body?.getReader();
// 创建文本解码器
const decoder = new TextDecoder('utf-8');
// 用于存储未处理完的流式数据
let buffer = '';
// eslint-disable-next-line no-constant-condition
while (true) {
// 读取流式数据的下一个块
const { done, value } = await reader!.read();
if (done) break;
// 解码并追加到缓冲区
buffer += decoder.decode(value, { stream: true });
// 按行分割缓冲区的数据
const chunks = buffer.split('data: ');
// 保留未完成的行
buffer = chunks.pop() || '';
for (let i = 1; i < chunks.length; i++) {
const chunk = chunks[i].trim();
// 忽略空行和结束标记[DONE]
if (!chunk) continue;
try {
const json = JSON.parse(chunk);
const content = json.choices[0]?.delta?.content || '';
if (content) {
// 调用回调函数处理数据块
onStream?.(content);
}
} catch (e) {
console.error('Failed to parse JSON chunk:', chunk);
}
}
}
} catch (error) {
handleApiError(error as AxiosError<unknown, any>);
throw error;
}
};
/**
* 统一处理API请求错误
* @param error - Axios错误对象
*/
const handleApiError = (error: AxiosError) => {
let errorMsg = '请求失败,请稍后重试';
if (error.response) {
errorMsg = `API错误: ${error.response.status} ${error.response.data}`;
} else if (error.request) {
errorMsg = '网络连接异常,请检查网络';
}
ElMessage.error(errorMsg);
};
export default fetchAIResponse;
添加类型
因为我们用的是TS,所以我们还需要在src\types文件夹下创建ai.ts。
类型放在哪就看自己的项目要求,也可以直接写在请求文件里面,因为后面Vue文件里面也会用到,所以我就把他提出来了
export type MessageRole = 'user' | 'assistant' | 'system';
export interface ChatMessage {
id: string;
//角色
role: MessageRole;
//内容
content: string;
//时间戳
timestamp: number;
}
第四步:写vue文件
画页面
这是自己做路过简单的页面以及动画,大家可以根据自己的需要自行调整
<template>
<div class="chat-container">
<div class="message-area">
<div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]">
<div class="role-tag">{{ msg.role === 'user' ? '我' : '助手' }}</div>
<div class="content">{{ msg.content || '' }}</div>
<div class="timestamp">
{{ new Date(msg.timestamp).toLocaleTimeString() }}
</div>
</div>
<div v-if="isLoading" class="loading-indicator">思考中...</div>
</div>
<div class="input-area">
<el-input
v-model="inputMessage"
type="textarea"
:rows="2"
:disabled="isLoading"
placeholder="输入消息(Enter发送,Shift+Enter换行)"
@keyup.enter.prevent="handleKeyPress"
@keydown.enter.prevent
/>
<el-button type="primary" :loading="isLoading" class="send-button" @click="sendMessage"> 发送 </el-button>
</div>
</div>
</template>
<style scoped>
.chat-container {
display: flex;
padding: 20px;
margin: 0 auto;
max-width: 1200px;
height: 80vh;
background: #fff;
flex-direction: column;
}
.message-area {
flex: 1;
overflow-y: auto;
padding: 20px;
margin-bottom: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
}
.message {
display: flex;
padding: 12px 16px;
margin: 12px 0;
max-width: 75%;
border-radius: 12px;
flex-direction: column;
gap: 8px;
&.user {
align-self: flex-end;
background: #e8f4ff;
border: 1px solid #c2d9ff;
}
&.assistant {
align-self: flex-start;
background: #f6f6f7;
border: 1px solid #e4e4e7;
}
}
.role-tag {
font-size: 0.8em;
color: #666;
font-weight: 500;
}
.timestamp {
font-size: 0.75em;
text-align: right;
color: #999;
}
.input-area {
display: flex;
gap: 12px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
.send-button {
width: 70px;
height: 50px;
}
}
.loading-indicator {
padding: 12px;
text-align: center;
color: #666;
}
.message.assistant .content {
overflow: hidden;
white-space: pre-wrap;
animation:
typing 0.1s steps(4, end),
blink 0.75s step-end infinite;
}
@keyframes typing {
from {
max-width: 0;
}
to {
max-width: 100%;
}
}
@keyframes blink {
50% {
border-color: transparent;
}
}
</style>
TS逻辑
代码里面也有注释我就也不过多阐述了。
值得一提的是,在封装请求的时候,我们做了deepseek和Kimi的切换,我们只需要在调用fetchAIResponse请求的时候把第二个参数把 'KIMI' 换成 'DEEPSEEK'就可以了,如果有需要的话,大家可以在页面添加一个按钮做切换。
<script lang="ts" setup>
import { ChatMessage } from '@/types/ai';
import fetchAIResponse from '@/http/aiRequest';
/** 输入消息 */
const inputMessage = ref('');
/** 消息列表 */
const messages = ref<ChatMessage[]>([]);
/** 加载状态 */
const isLoading = ref(false);
/** 定时器 ID */
const currentStreamMessage = ref<ChatMessage | null>(null);
/**
* 消息处理函数,将新消息添加到消息列表
* @param message - 要添加的消息,不包含 id 和 timestamp
*/
const addMessage = (message: Omit<ChatMessage, 'id' | 'timestamp'>) => {
messages.value.push({
...message,
id: crypto.randomUUID(),
timestamp: Date.now()
});
};
/** 发送消息 */
async function sendMessage() {
// 如果输入为空或正在加载,则不执行操作
if (!inputMessage.value.trim() || isLoading.value) return;
// 清空输入框前保存用户输入的消息
const userMessage = inputMessage.value.trim();
inputMessage.value = '';
// 添加用户消息到消息列表
addMessage({
role: 'user',
content: userMessage
});
// 更新消息列表,触发响应式更新
const tempMessage: ChatMessage = {
id: crypto.randomUUID(),
role: 'assistant',
content: '',
timestamp: Date.now()
};
// 更新消息列表,触发响应式更新
messages.value = [...messages.value, tempMessage];
// 设置当前流式消息
currentStreamMessage.value = tempMessage;
try {
isLoading.value = true;
await fetchAIResponse(
messages.value.filter((msg) => msg.content),
'KIMI',
(chunk: string) => {
if (currentStreamMessage.value) {
currentStreamMessage.value.content += chunk;
// 触发 DOM 更新
requestAnimationFrame(autoScroll);
}
}
);
} catch (error) {
if (currentStreamMessage.value) {
currentStreamMessage.value.content += '(响应中断)';
}
} finally {
isLoading.value = false;
// 清空当前流式消息对象
currentStreamMessage.value = null;
}
}
/** 自动滚动函数 */
const autoScroll = () => {
const container = document.querySelector('.message-area');
if (container) {
requestAnimationFrame(() => {
const lastMessage = container.lastElementChild;
if (lastMessage) {
lastMessage.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
});
}
};
/** 设置键盘事件(设置 Shift+Enter换行,Enter发送)*/
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
};
</script>
五、总结配图流程
六、扩展实现方案
七、AI 对话框简易实现流程图
了解原理和流程后,根据官网的示例,也可以去实现别的AI