perf: 优化 WebUI Chat 的流式传输性能

This commit is contained in:
Soulter
2025-04-09 15:22:35 +08:00
parent bafa473c8e
commit 5f0b8161b7
3 changed files with 67 additions and 43 deletions
@@ -8,6 +8,7 @@ import astrbot.core.message.components as Comp
from openai import AsyncOpenAI, AsyncAzureOpenAI
from openai.types.chat.chat_completion import ChatCompletion
# from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai._exceptions import NotFoundError, UnprocessableEntityError
from openai.lib.streaming.chat._completions import ChatCompletionStreamState
@@ -143,14 +144,19 @@ class ProviderOpenAIOfficial(Provider):
state = ChatCompletionStreamState()
async for chunk in stream:
state.handle_chunk(chunk)
try:
state.handle_chunk(chunk)
except Exception as e:
logger.warning("Saving chunk state error: " + str(e))
if len(chunk.choices) == 0:
continue
delta = chunk.choices[0].delta
# 处理文本内容
if delta.content:
completion_text = delta.content
llm_response.result_chain = MessageChain(chain=[Comp.Plain(completion_text)])
llm_response.result_chain = MessageChain(
chain=[Comp.Plain(completion_text)]
)
yield llm_response
final_completion = state.get_final_completion()
+1 -1
View File
@@ -190,7 +190,7 @@ class ChatRoute(Route):
# 丢弃
continue
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
await asyncio.sleep(0.15)
await asyncio.sleep(0.05)
if streaming and type != "end":
continue
+58 -40
View File
@@ -36,11 +36,11 @@ marked.setOptions({
</template>
<v-list-item-title class="conversation-title">新对话</v-list-item-title>
<v-list-item-subtitle class="timestamp">{{ formatDate(item.updated_at)
}}</v-list-item-subtitle>
}}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card>
<v-fade-transition>
<div class="no-conversations" v-if="conversations.length === 0">
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
@@ -57,7 +57,8 @@ marked.setOptions({
<v-chip class="status-chip" :color="status?.llm_enabled ? 'primary' : 'grey-lighten-2'"
variant="elevated" size="small">
<template v-slot:prepend>
<v-icon :icon="status?.llm_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'" size="x-small"></v-icon>
<v-icon :icon="status?.llm_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
size="x-small"></v-icon>
</template>
LLM 服务
</v-chip>
@@ -65,7 +66,8 @@ marked.setOptions({
<v-chip class="status-chip" :color="status?.stt_enabled ? 'success' : 'grey-lighten-2'"
variant="elevated" size="small">
<template v-slot:prepend>
<v-icon :icon="status?.stt_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'" size="x-small"></v-icon>
<v-icon :icon="status?.stt_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
size="x-small"></v-icon>
</template>
语音转文本
</v-chip>
@@ -295,50 +297,66 @@ export default {
const chunk = decoder.decode(value, { stream: true });
// data: {"type": "plain", "data": "helloworld"}
let chunk_json = JSON.parse(chunk.replace('data: ', ''));
// 可能有多行
if (chunk_json.type === 'heartbeat') {
continue; // 心跳包
}
if (chunk_json.type === 'error') {
console.error('Error received:', chunk_json.data);
continue;
}
let lines = chunk.split('\n\n');
if (chunk_json.type === 'image') {
let img = chunk_json.data.replace('[IMAGE]', '');
let bot_resp = {
type: 'bot',
message: `<img src="/api/chat/get_file?filename=${img}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
console.log('SSE数据:', lines);
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (!line) {
continue;
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'record') {
let audio = chunk_json.data.replace('[RECORD]', '');
let bot_resp = {
type: 'bot',
message: `<audio controls class="audio-player">
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
您的浏览器不支持音频播放。
</audio>`
console.log(line)
// data: {"type": "plain", "data": "helloworld"}
let chunk_json = JSON.parse(line.replace('data: ', ''));
if (chunk_json.type === 'heartbeat') {
continue; // 心跳包
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'plain') {
if (!in_streaming) {
message_obj = {
if (chunk_json.type === 'error') {
console.error('Error received:', chunk_json.data);
continue;
}
if (chunk_json.type === 'image') {
let img = chunk_json.data.replace('[IMAGE]', '');
let bot_resp = {
type: 'bot',
message: ref(chunk_json.data),
message: `<img src="/api/chat/get_file?filename=${img}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
}
this.messages.push(message_obj);
in_streaming = true;
} else {
message_obj.message.value += chunk_json.data;
this.messages.push(bot_resp);
} else if (chunk_json.type === 'record') {
let audio = chunk_json.data.replace('[RECORD]', '');
let bot_resp = {
type: 'bot',
message: `<audio controls class="audio-player">
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
您的浏览器不支持音频播放。
</audio>`
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'plain') {
if (!in_streaming) {
message_obj = {
type: 'bot',
message: ref(chunk_json.data),
}
this.messages.push(message_obj);
in_streaming = true;
} else {
message_obj.message.value += chunk_json.data;
}
} else if (chunk_json.type === 'end') {
in_streaming = false;
continue;
}
} else if (chunk_json.type === 'end') {
in_streaming = false;
continue;
this.scrollToBottom();
}
this.scrollToBottom();
}
},