fix: 修复流式接口交互

This commit is contained in:
何嘉悦 2025-06-02 17:31:01 +08:00
parent c57c2711f8
commit ac8c09d53a
4 changed files with 66 additions and 193 deletions

View File

@ -32,7 +32,7 @@ export interface SendDTO {
/**
* id
*/
sessionId?: number;
sessionId?: string;
/**
*
*/

View File

@ -1,10 +1,11 @@
<!-- 每个回话对应的聊天内容 -->
<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 { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
import { useXStream } from 'vue-element-plus-x';
import { useRoute } from 'vue-router';
import { send } from '@/api/chat/index';
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 { useFilesStore } from '@/stores/modules/files';
import { useModelStore } from '@/stores/modules/model';
import { useUserStore } from '@/stores/modules/user';
type MessageItem = BubbleProps & {
key: number;
@ -21,82 +23,54 @@ type MessageItem = BubbleProps & {
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 chatStore = useChatStore();
const modelStore = useModelStore();
const filesStore = useFilesStore();
const userStore = useUserStore();
const inputValue = ref('');
const senderRef = ref<any>(null);
const senderRef = ref<InstanceType<typeof Sender> | null>(null);
const bubbleItems = ref<MessageItem[]>([]);
const bubbleListRef = ref<BubbleListInstance | null>(null);
// const processedIndex = ref(0);
const isLoading = ref(false);
watch(
() => route.params?.id,
async (_id_) => {
if (_id_) {
//
inputValue.value = '';
if (_id_ !== 'not_login') {
// id
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
bubbleItems.value = chatStore.chatMap[`${_id_}`] as MessageItem[];
//
setTimeout(() => {
bubbleListRef.value!.scrollToBottom();
}, 350);
return;
}
// id
if (chatStore.chatMap[`${_id_}`] && chatStore.chatMap[`${_id_}`].length) {
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
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,
}));
//
await chatStore.requestChatList(`${_id_}`);
//
bubbleItems.value = chatStore.chatMap[`${_id_}`] as MessageItem[];
//
setTimeout(() => {
bubbleListRef.value!.scrollToBottom();
}, 350);
return;
}
//
await chatStore.requestChatList(`${_id_}`);
//
bubbleItems.value = chatStore.chatMap[`${_id_}`].map((item: any) => ({
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(() => {
bubbleListRef.value!.scrollToBottom();
}, 350);
//
const v = localStorage.getItem('chatContent');
if (v) {
localStorage.removeItem('chatContent');
//
console.log('发送消息 v', v);
// setTimeout(() => {
// startSSE();
// }, 350);
setTimeout(() => {
startSSE(v);
}, 350);
localStorage.removeItem('chatContent');
}
}
},
@ -104,19 +78,10 @@ watch(
);
//
function handleDataChunk(chunk: string) {
if (chunk === ' [DONE]') {
console.log('数据接收完毕');
//
if (bubbleItems.value.length) {
bubbleItems.value[bubbleItems.value.length - 1].typing = false;
}
cancel();
return;
}
function handleDataChunk(chunk: AnyObject) {
try {
// console.log('New chunk:', JSON.parse(chunk))
const reasoningChunk = JSON.parse(chunk).choices[0].delta.reasoning_content;
// console.log('New chunk:', chunk);
const reasoningChunk = chunk.choices[0].delta.reasoning_content;
if (reasoningChunk) {
//
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) {
//
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) {
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;
}
// undefinedJSON
if (value === undefined) {
error = { path: currentPath, value, type: 'undefined' };
return;
}
// SymbolJSON
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;
}
// DateRegExp
error = { path: currentPath, value, type };
}
check(data);
return { valid: !error, error };
}
async function startSSE(chatContent: string) {
try {
//
console.log('chatContent', chatContent);
// console.log('chatContent', chatContent);
//
inputValue.value = '';
isLoading.value = true;
addMessage(chatContent, true);
addMessage('', false);
// BubbleList
bubbleListRef.value!.scrollToBottom();
bubbleListRef.value?.scrollToBottom();
const res = await send({
const res = send({
messages: bubbleItems.value
.filter((item: any) => item.role === 'user')
.map((item: any) => ({
role: item.role,
content: item.content,
})),
sessionId: Number(route.params?.id),
userId: 1,
sessionId: String(route.params?.id),
userId: userStore.userInfo?.userId,
model: modelStore.currentModelInfo.modelName ?? '',
});
console.log('res', res);
for await (const chunk of res) {
console.log('chunk', chunk);
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);
}
handleDataChunk(chunk.result as AnyObject);
}
// 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) {
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',
placement: isUser ? 'end' : 'start',
isMarkdown: !isUser,
variant: 'shadow',
shape: 'corner',
// maxWidth: '500px',
typing: isUser ? false : { step: 2, suffix: '❤️‍🔥', interval: 80 },
isFog: isUser ? false : { bgColor: '#FFFFFF' },
loading: !isUser,
content: message || '',
reasoning_content: '',
@ -326,12 +188,12 @@ watch(
(val) => {
if (val > 0) {
nextTick(() => {
senderRef.value.openHeader();
senderRef.value?.openHeader();
});
}
else {
nextTick(() => {
senderRef.value.closeHeader();
senderRef.value?.closeHeader();
});
}
},
@ -341,10 +203,6 @@ watch(
<template>
<div class="chat-with-id-container">
<div class="chat-warp">
<div v-if="error" class="error">
{{ error.message }}
</div>
<BubbleList ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 240px)">
<template #header="{ item }">
<Thinking

View File

@ -17,7 +17,22 @@ export const useChatStore = defineStore('chat', () => {
const chatMap = ref<Record<string, 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,
};
});
};
// 获取当前会话的聊天记录

View File

@ -3,7 +3,7 @@
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {};
export {}
/* prettier-ignore */
declare module 'vue' {