fix: 修复流式接口交互
This commit is contained in:
parent
c57c2711f8
commit
ac8c09d53a
@ -32,7 +32,7 @@ export interface SendDTO {
|
|||||||
/**
|
/**
|
||||||
* 会话id
|
* 会话id
|
||||||
*/
|
*/
|
||||||
sessionId?: number;
|
sessionId?: string;
|
||||||
/**
|
/**
|
||||||
* 是否开启流式对话
|
* 是否开启流式对话
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
<!-- 每个回话对应的聊天内容 -->
|
<!-- 每个回话对应的聊天内容 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AnyObject } from 'typescript-api-pro';
|
||||||
|
import type { Sender } from 'vue-element-plus-x';
|
||||||
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
||||||
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
||||||
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
||||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
||||||
import { useXStream } from 'vue-element-plus-x';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { send } from '@/api/chat/index';
|
import { send } from '@/api/chat/index';
|
||||||
import FilesSelect from '@/components/FilesSelect/index.vue';
|
import FilesSelect from '@/components/FilesSelect/index.vue';
|
||||||
@ -12,6 +13,7 @@ import ModelSelect from '@/components/ModelSelect/index.vue';
|
|||||||
import { useChatStore } from '@/stores/modules/chat';
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
import { useFilesStore } from '@/stores/modules/files';
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
import { useModelStore } from '@/stores/modules/model';
|
import { useModelStore } from '@/stores/modules/model';
|
||||||
|
import { useUserStore } from '@/stores/modules/user';
|
||||||
|
|
||||||
type MessageItem = BubbleProps & {
|
type MessageItem = BubbleProps & {
|
||||||
key: number;
|
key: number;
|
||||||
@ -21,82 +23,54 @@ type MessageItem = BubbleProps & {
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { cancel, error, isLoading } = useXStream();
|
|
||||||
|
|
||||||
// const BASE_URL = 'https://api.siliconflow.cn/v1/chat/completions';
|
|
||||||
// 仅供测试,请勿拿去测试其他付费模型
|
|
||||||
// const API_KEY = 'sk-vfjyscildobjnrijtcllnkhtcouidcxdgjxtldzqzeowrbga';
|
|
||||||
// const MODEL = 'THUDM/GLM-Z1-9B-0414';
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const modelStore = useModelStore();
|
const modelStore = useModelStore();
|
||||||
const filesStore = useFilesStore();
|
const filesStore = useFilesStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const inputValue = ref('');
|
const inputValue = ref('');
|
||||||
const senderRef = ref<any>(null);
|
const senderRef = ref<InstanceType<typeof Sender> | null>(null);
|
||||||
const bubbleItems = ref<MessageItem[]>([]);
|
const bubbleItems = ref<MessageItem[]>([]);
|
||||||
const bubbleListRef = ref<BubbleListInstance | null>(null);
|
const bubbleListRef = ref<BubbleListInstance | null>(null);
|
||||||
// const processedIndex = ref(0);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params?.id,
|
() => route.params?.id,
|
||||||
async (_id_) => {
|
async (_id_) => {
|
||||||
if (_id_) {
|
if (_id_) {
|
||||||
// 清空输入框
|
if (_id_ !== 'not_login') {
|
||||||
inputValue.value = '';
|
|
||||||
|
|
||||||
// 判断的当前会话id是否有聊天记录,有缓存则直接赋值展示
|
// 判断的当前会话id是否有聊天记录,有缓存则直接赋值展示
|
||||||
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
|
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
|
||||||
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
|
bubbleItems.value = chatStore.chatMap[`${_id_}`] as MessageItem[];
|
||||||
key: item.id,
|
|
||||||
avatar:
|
|
||||||
item.role === 'user'
|
|
||||||
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
|
|
||||||
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
|
||||||
content: item.content,
|
|
||||||
avatarSize: '32px',
|
|
||||||
role: item.role,
|
|
||||||
typing: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
bubbleListRef.value!.scrollToBottom();
|
bubbleListRef.value!.scrollToBottom();
|
||||||
}, 350);
|
}, 350);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无缓存则请求聊天记录
|
// 无缓存则请求聊天记录
|
||||||
await chatStore.requestChatList(`${_id_}`);
|
await chatStore.requestChatList(`${_id_}`);
|
||||||
// 请求聊天记录后,赋值回显,并滚动到底部
|
// 请求聊天记录后,赋值回显,并滚动到底部
|
||||||
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
|
bubbleItems.value = chatStore.chatMap[`${_id_}`] as MessageItem[];
|
||||||
key: item.id,
|
|
||||||
avatar:
|
|
||||||
item.role === 'user'
|
|
||||||
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
|
|
||||||
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
|
||||||
content: item.content,
|
|
||||||
avatarSize: '32px',
|
|
||||||
role: item.role,
|
|
||||||
typing: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
bubbleListRef.value!.scrollToBottom();
|
bubbleListRef.value!.scrollToBottom();
|
||||||
}, 350);
|
}, 350);
|
||||||
|
}
|
||||||
|
|
||||||
// 如果本地有发送内容 ,则直接发送
|
// 如果本地有发送内容 ,则直接发送
|
||||||
const v = localStorage.getItem('chatContent');
|
const v = localStorage.getItem('chatContent');
|
||||||
if (v) {
|
if (v) {
|
||||||
localStorage.removeItem('chatContent');
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
console.log('发送消息 v', v);
|
console.log('发送消息 v', v);
|
||||||
// setTimeout(() => {
|
setTimeout(() => {
|
||||||
// startSSE();
|
startSSE(v);
|
||||||
// }, 350);
|
}, 350);
|
||||||
|
|
||||||
|
localStorage.removeItem('chatContent');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -104,19 +78,10 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 封装数据处理逻辑
|
// 封装数据处理逻辑
|
||||||
function handleDataChunk(chunk: string) {
|
function handleDataChunk(chunk: AnyObject) {
|
||||||
if (chunk === ' [DONE]') {
|
|
||||||
console.log('数据接收完毕');
|
|
||||||
// 停止打字器状态
|
|
||||||
if (bubbleItems.value.length) {
|
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].typing = false;
|
|
||||||
}
|
|
||||||
cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
// console.log('New chunk:', JSON.parse(chunk))
|
// console.log('New chunk:', chunk);
|
||||||
const reasoningChunk = JSON.parse(chunk).choices[0].delta.reasoning_content;
|
const reasoningChunk = chunk.choices[0].delta.reasoning_content;
|
||||||
if (reasoningChunk) {
|
if (reasoningChunk) {
|
||||||
// 开始思考链状态
|
// 开始思考链状态
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'thinking';
|
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'thinking';
|
||||||
@ -126,7 +91,7 @@ function handleDataChunk(chunk: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedChunk = JSON.parse(chunk).choices[0].delta.content;
|
const parsedChunk = chunk.choices[0].delta.content;
|
||||||
if (parsedChunk) {
|
if (parsedChunk) {
|
||||||
// 结束 思考链状态
|
// 结束 思考链状态
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'end';
|
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'end';
|
||||||
@ -142,149 +107,51 @@ function handleDataChunk(chunk: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// watch(
|
|
||||||
// data,
|
|
||||||
// () => {
|
|
||||||
// for (let i = processedIndex.value; i < data.value.length; i++) {
|
|
||||||
// const chunk = data.value[i].data;
|
|
||||||
// handleDataChunk(chunk);
|
|
||||||
// processedIndex.value++;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// { deep: true },
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 封装错误处理逻辑
|
// 封装错误处理逻辑
|
||||||
function handleError(err: any) {
|
function handleError(err: any) {
|
||||||
console.error('Fetch error:', err);
|
console.error('Fetch error:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkJsonSerializable(data: any) {
|
|
||||||
const visited = new WeakSet();
|
|
||||||
let error: any = null;
|
|
||||||
|
|
||||||
function check(value: any, currentPath = 'root') {
|
|
||||||
if (error)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const type = typeof value;
|
|
||||||
|
|
||||||
// 处理基本类型(支持 JSON 的类型)
|
|
||||||
if (type === 'string' || type === 'number' || type === 'boolean' || value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 undefined(JSON 不支持)
|
|
||||||
if (value === undefined) {
|
|
||||||
error = { path: currentPath, value, type: 'undefined' };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理函数、Symbol(JSON 不支持)
|
|
||||||
if (type === 'function' || type === 'symbol') {
|
|
||||||
error = { path: currentPath, value, type };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理数组:递归检查每个元素
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((item, index) => {
|
|
||||||
check(item, `${currentPath}[${index}]`);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理对象:检查是否为普通对象(非特殊对象)
|
|
||||||
if (type === 'object' && value !== null) {
|
|
||||||
// 检测循环引用
|
|
||||||
if (visited.has(value)) {
|
|
||||||
error = { path: currentPath, value, type: 'circular_reference' };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
visited.add(value);
|
|
||||||
|
|
||||||
// 递归检查对象的每个属性
|
|
||||||
for (const [key, val] of Object.entries(value)) {
|
|
||||||
check(val, `${currentPath}.${key}`);
|
|
||||||
}
|
|
||||||
visited.delete(value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他不支持的类型(如 Date、RegExp 等)
|
|
||||||
error = { path: currentPath, value, type };
|
|
||||||
}
|
|
||||||
|
|
||||||
check(data);
|
|
||||||
return { valid: !error, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startSSE(chatContent: string) {
|
async function startSSE(chatContent: string) {
|
||||||
try {
|
try {
|
||||||
// 添加用户输入的消息
|
// 添加用户输入的消息
|
||||||
console.log('chatContent', chatContent);
|
// console.log('chatContent', chatContent);
|
||||||
|
// 清空输入框
|
||||||
|
inputValue.value = '';
|
||||||
|
isLoading.value = true;
|
||||||
addMessage(chatContent, true);
|
addMessage(chatContent, true);
|
||||||
addMessage('', false);
|
addMessage('', false);
|
||||||
|
|
||||||
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
||||||
bubbleListRef.value!.scrollToBottom();
|
bubbleListRef.value?.scrollToBottom();
|
||||||
|
|
||||||
const res = await send({
|
const res = send({
|
||||||
messages: bubbleItems.value
|
messages: bubbleItems.value
|
||||||
.filter((item: any) => item.role === 'user')
|
.filter((item: any) => item.role === 'user')
|
||||||
.map((item: any) => ({
|
.map((item: any) => ({
|
||||||
role: item.role,
|
role: item.role,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
})),
|
})),
|
||||||
sessionId: Number(route.params?.id),
|
sessionId: String(route.params?.id),
|
||||||
userId: 1,
|
userId: userStore.userInfo?.userId,
|
||||||
model: modelStore.currentModelInfo.modelName ?? '',
|
model: modelStore.currentModelInfo.modelName ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('res', res);
|
|
||||||
|
|
||||||
for await (const chunk of res) {
|
for await (const chunk of res) {
|
||||||
console.log('chunk', chunk);
|
handleDataChunk(chunk.result as AnyObject);
|
||||||
const resError = checkJsonSerializable(chunk.result);
|
|
||||||
console.log('resError', resError);
|
|
||||||
|
|
||||||
// 判断 json 序列化失败的情况
|
|
||||||
if (chunk.result && typeof chunk.result !== 'object') {
|
|
||||||
console.log('json 序列化失败');
|
|
||||||
handleDataChunk(chunk.result as string);
|
|
||||||
}
|
}
|
||||||
else if (chunk.result) {
|
|
||||||
const strChunk = JSON.stringify(chunk.result);
|
|
||||||
handleDataChunk(strChunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const response = await fetch(BASE_URL, {
|
|
||||||
// method: 'POST',
|
|
||||||
// headers: {
|
|
||||||
// 'Authorization': `Bearer ${API_KEY}`,
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// 'Accept': 'text/event-stream',
|
|
||||||
// },
|
|
||||||
// body: JSON.stringify({
|
|
||||||
// model: MODEL,
|
|
||||||
// messages: bubbleItems.value
|
|
||||||
// .filter((item: any) => item.role === 'user')
|
|
||||||
// .map((item: any) => ({
|
|
||||||
// role: item.role,
|
|
||||||
// content: item.content,
|
|
||||||
// })),
|
|
||||||
// stream: true,
|
|
||||||
// }),
|
|
||||||
// });
|
|
||||||
// const readableStream = response.body!;
|
|
||||||
// // 重置状态
|
|
||||||
// processedIndex.value = 0;
|
|
||||||
// await startStream({ readableStream });
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
handleError(err);
|
handleError(err);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
console.log('数据接收完毕');
|
||||||
|
// 停止打字器状态
|
||||||
|
if (bubbleItems.value.length) {
|
||||||
|
bubbleItems.value[bubbleItems.value.length - 1].typing = false;
|
||||||
|
}
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加消息 - 维护聊天记录
|
// 添加消息 - 维护聊天记录
|
||||||
@ -299,11 +166,6 @@ function addMessage(message: string, isUser: boolean) {
|
|||||||
role: isUser ? 'user' : 'system',
|
role: isUser ? 'user' : 'system',
|
||||||
placement: isUser ? 'end' : 'start',
|
placement: isUser ? 'end' : 'start',
|
||||||
isMarkdown: !isUser,
|
isMarkdown: !isUser,
|
||||||
variant: 'shadow',
|
|
||||||
shape: 'corner',
|
|
||||||
// maxWidth: '500px',
|
|
||||||
typing: isUser ? false : { step: 2, suffix: '❤️🔥', interval: 80 },
|
|
||||||
isFog: isUser ? false : { bgColor: '#FFFFFF' },
|
|
||||||
loading: !isUser,
|
loading: !isUser,
|
||||||
content: message || '',
|
content: message || '',
|
||||||
reasoning_content: '',
|
reasoning_content: '',
|
||||||
@ -326,12 +188,12 @@ watch(
|
|||||||
(val) => {
|
(val) => {
|
||||||
if (val > 0) {
|
if (val > 0) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
senderRef.value.openHeader();
|
senderRef.value?.openHeader();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
senderRef.value.closeHeader();
|
senderRef.value?.closeHeader();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -341,10 +203,6 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-with-id-container">
|
<div class="chat-with-id-container">
|
||||||
<div class="chat-warp">
|
<div class="chat-warp">
|
||||||
<div v-if="error" class="error">
|
|
||||||
{{ error.message }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BubbleList ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 240px)">
|
<BubbleList ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 240px)">
|
||||||
<template #header="{ item }">
|
<template #header="{ item }">
|
||||||
<Thinking
|
<Thinking
|
||||||
|
|||||||
@ -17,7 +17,22 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
const chatMap = ref<Record<string, ChatMessageVo[]>>({});
|
const chatMap = ref<Record<string, ChatMessageVo[]>>({});
|
||||||
|
|
||||||
const setChatMap = (id: string, data: ChatMessageVo[]) => {
|
const setChatMap = (id: string, data: ChatMessageVo[]) => {
|
||||||
chatMap.value[id] = data;
|
chatMap.value[id] = data?.map((item: ChatMessageVo) => {
|
||||||
|
const isUser = item.role === 'user';
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
key: item.id,
|
||||||
|
placement: isUser ? 'end' : 'start',
|
||||||
|
isMarkdown: !isUser,
|
||||||
|
// variant: 'shadow',
|
||||||
|
// shape: 'corner',
|
||||||
|
avatar: isUser
|
||||||
|
? 'https://avatars.githubusercontent.com/u/76239030?v=4'
|
||||||
|
: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
|
||||||
|
avatarSize: '32px',
|
||||||
|
typing: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取当前会话的聊天记录
|
// 获取当前会话的聊天记录
|
||||||
|
|||||||
2
types/components.d.ts
vendored
2
types/components.d.ts
vendored
@ -3,7 +3,7 @@
|
|||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {};
|
export {}
|
||||||
|
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user