创建对话后,使用SSE协议发起对话
最近更新日期:2026-01-19 22:12:06
概述
本文档介绍在创建对话后,如何使用 SSE (Server-Sent Events) 协议与 AI 进行实时流式对话。
流程概览
- 获取角色列表 - 从服务器获取可用的 AI 角色列表
- 创建对话 - 选择角色后创建新对话,获取 chatID
- 发送消息 (SSE) - 使用 SSE 协议发送消息并接收流式响应
API 接口说明
1. 获取角色列表
请求
GET /product/zeroai/character/list请求头
Authorization: {User:Token}响应示例
json
{
"code": 200,
"data": {
"list": [
{
"id": "character_id_1",
"name": "角色名称"
}
]
}
}2. 创建对话
请求
POST /product/zeroai/chat/creat请求头
Authorization: {User:Token}
Content-Type: multipart/form-data请求参数 (FormData)
characterID: 角色 ID
响应示例
json
{
"code": 200,
"data": {
"id": "chat_id_123"
}
}3. 发送消息 (SSE 流式对话)
请求
GET /product/zeroai/chat/chat?chatID={chatID}&text={message}请求头
Authorization: {User:Token}SSE 数据格式
data: {"output":"回复内容","action":"动作","expression":"表情"}
data: {"output":"继续回复"}前端实现代码
完整示例代码
javascript
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next';
import axios from 'axios';
import config from '../../../js/config.js';
import { useCookies } from "vue3-cookies";
const { cookies } = useCookies();
// 角色列表
const characterList = ref([]);
const selectedCharacter = ref(null);
const loadingCharacters = ref(false);
// 聊天数据
const chatId = ref(null);
const chatHistory = ref([]);
const inputMessage = ref('');
const sendingMessage = ref(false);
const chatContainerRef = ref(null);
// 获取角色列表
const fetchCharacterList = async () => {
loadingCharacters.value = true;
try {
const { data } = await axios.get(`${config.apiBaseUrl}/product/zeroai/character/list`, {
headers: {
'Authorization': cookies.get('User:Token')
}
});
if (data.code === 200) {
if (Array.isArray(data.data)) {
characterList.value = data.data;
} else if (data.data.list && Array.isArray(data.data.list)) {
characterList.value = data.data.list;
}
} else {
MessagePlugin.error(data.message || '获取角色列表失败');
}
} catch (error) {
console.error('获取角色列表失败:', error);
MessagePlugin.error('获取角色列表失败');
} finally {
loadingCharacters.value = false;
}
};
// 创建对话
const createChat = async () => {
if (!selectedCharacter.value) {
MessagePlugin.warning('请先选择角色');
return;
}
try {
const formData = new FormData();
formData.append('characterID', selectedCharacter.value);
const { data } = await axios.post(`${config.apiBaseUrl}/product/zeroai/chat/creat`, formData, {
headers: {
'Authorization': cookies.get('User:Token')
}
});
if (data.code === 200) {
chatId.value = data.data.id;
chatHistory.value = [];
MessagePlugin.success('创建对话成功');
} else {
MessagePlugin.error(data.message || '创建对话失败');
}
} catch (error) {
console.error('创建对话失败:', error);
MessagePlugin.error('创建对话失败');
}
};
// 解析SSE数据
const parseSSEData = (text) => {
const lines = text.split('\n').filter(line => line.trim().startsWith('data: '));
let jsonString = '';
lines.forEach(line => {
jsonString += line.replace('data: ', '');
});
try {
return JSON.parse(jsonString);
} catch (e) {
console.error('解析SSE数据失败:', e);
return null;
}
};
// 发送消息
const sendMessage = async () => {
if (!chatId.value) {
MessagePlugin.warning('请先创建对话');
return;
}
if (!inputMessage.value.trim()) {
return;
}
// 添加用户消息到历史
chatHistory.value.push({
role: 'user',
content: inputMessage.value
});
const message = inputMessage.value;
inputMessage.value = '';
sendingMessage.value = true;
await scrollToBottom();
try {
const response = await fetch(`${config.apiBaseUrl}/product/zeroai/chat/chat?chatID=${chatId.value}&text=${encodeURIComponent(message)}`, {
method: 'GET',
headers: {
'Authorization': cookies.get('User:Token')
}
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let sseText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
sseText += decoder.decode(value, { stream: true });
// 解析并更新UI
const parsedData = parseSSEData(sseText);
if (parsedData && parsedData.output) {
// 移除之前的AI消息,重新渲染
const lastAIMessage = chatHistory.value[chatHistory.value.length - 1];
if (lastAIMessage && lastAIMessage.role === 'ai') {
chatHistory.value[chatHistory.value.length - 1] = {
role: 'ai',
content: parsedData.output
};
} else {
chatHistory.value.push({
role: 'ai',
content: parsedData.output
});
}
await scrollToBottom();
}
}
} catch (error) {
console.error('发送消息失败:', error);
MessagePlugin.error('发送消息失败');
} finally {
sendingMessage.value = false;
}
};
// 滚动到底部
const scrollToBottom = async () => {
await nextTick();
if (chatContainerRef.value) {
chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight;
}
};
// 监听角色选择变化
watch(selectedCharacter, (newVal) => {
if (newVal) {
createChat();
}
});
// 页面加载时获取角色列表
onMounted(() => {
fetchCharacterList();
});关键技术点
1. SSE 数据解析
SSE 流式数据以 data: 前缀格式发送,每行一个数据块。需要累积所有数据块后合并解析为 JSON。
javascript
const parseSSEData = (text) => {
const lines = text.split('\n').filter(line => line.trim().startsWith('data: '));
let jsonString = '';
lines.forEach(line => {
jsonString += line.replace('data: ', '');
});
try {
return JSON.parse(jsonString);
} catch (e) {
console.error('解析SSE数据失败:', e);
return null;
}
};2. Fetch API 读取流
使用 response.body.getReader() 获取流式读取器,通过 TextDecoder 解码数据。
javascript
const response = await fetch(url, { method: 'GET', headers });
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
// 处理流式数据
}3. 流式 UI 更新
在接收流式数据时,实时更新聊天历史,实现打字机效果。
javascript
const parsedData = parseSSEData(sseText);
if (parsedData && parsedData.output) {
const lastAIMessage = chatHistory.value[chatHistory.value.length - 1];
if (lastAIMessage && lastAIMessage.role === 'ai') {
// 更新最后一条 AI 消息
chatHistory.value[chatHistory.value.length - 1] = {
role: 'ai',
content: parsedData.output
};
} else {
// 添加新的 AI 消息
chatHistory.value.push({
role: 'ai',
content: parsedData.output
});
}
}4. 自动滚动到底部
使用 Vue 的 nextTick 确保 DOM 更新后再滚动。
javascript
const scrollToBottom = async () => {
await nextTick();
if (chatContainerRef.value) {
chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight;
}
};注意事项
- Token 认证:所有请求都需要携带
Authorization请求头 - URL 编码:发送消息时需要对消息内容进行
encodeURIComponent编码 - 流式数据处理:SSE 数据可能分多次发送,需要累积后统一解析
- 错误处理:需要处理网络错误和解析错误
- 并发控制:使用
sendingMessage标志防止重复发送