Implement chat queue fallback
This commit is contained in:
@@ -84,12 +84,30 @@ async function send(message: string) {
|
||||
messages.value.push(userMessage, assistantMessage);
|
||||
loading.value = true;
|
||||
activeAbortController.value = new AbortController();
|
||||
let hasContent = false;
|
||||
await scrollToBottom();
|
||||
try {
|
||||
await streamChat(activeSessionId.value, message, async (chunk) => {
|
||||
assistantMessage.content += chunk;
|
||||
await scrollToBottom();
|
||||
}, activeAbortController.value.signal);
|
||||
await streamChat(
|
||||
activeSessionId.value,
|
||||
message,
|
||||
async (chunk) => {
|
||||
if (!hasContent) {
|
||||
assistantMessage.content = "";
|
||||
hasContent = true;
|
||||
}
|
||||
assistantMessage.content += chunk;
|
||||
await scrollToBottom();
|
||||
},
|
||||
async (statusMessage, statusType) => {
|
||||
if (statusType === "queued") {
|
||||
assistantMessage.content = statusMessage || "当前请求较多,正在排队中。";
|
||||
} else if (statusType === "generating" && !hasContent) {
|
||||
assistantMessage.content = "";
|
||||
}
|
||||
await scrollToBottom();
|
||||
},
|
||||
activeAbortController.value.signal,
|
||||
);
|
||||
assistantMessage.streaming = false;
|
||||
await refreshSessions(activeSessionId.value);
|
||||
user.value = await api.profile();
|
||||
|
||||
@@ -18,24 +18,36 @@ const assistantParts = computed(() => {
|
||||
if (props.role === "user") {
|
||||
return { reasoning: "", answer: props.content };
|
||||
}
|
||||
const matched = props.content.match(/<think>([\s\S]*?)<\/think>/i);
|
||||
if (matched) {
|
||||
return {
|
||||
reasoning: matched[1].trim(),
|
||||
answer: props.content.replace(matched[0], "").trim(),
|
||||
};
|
||||
}
|
||||
const opening = props.content.match(/<think>([\s\S]*)$/i);
|
||||
if (opening) {
|
||||
return {
|
||||
reasoning: opening[1].trim(),
|
||||
answer: props.content.slice(0, opening.index).trim(),
|
||||
};
|
||||
}
|
||||
return { reasoning: "", answer: props.content };
|
||||
});
|
||||
|
||||
const renderedReasoning = computed(() => markdown.render(assistantParts.value.reasoning || ""));
|
||||
const content = props.content;
|
||||
let reasoning = "";
|
||||
let answer = content;
|
||||
|
||||
// 匹配所有完整的 <think>...</think>(全局匹配,支持多个)
|
||||
const thinkRegex = /<think>([\s\S]*?)<\/think>/gi;
|
||||
const matches = Array.from(answer.matchAll(thinkRegex));
|
||||
|
||||
if (matches.length > 0) {
|
||||
reasoning = matches.map((m) => m[1].trim()).join("\n\n");
|
||||
answer = answer.replace(thinkRegex, "").trim();
|
||||
}
|
||||
|
||||
// 处理未闭合的 <think>(流式输出时可能还没收到 </think>)
|
||||
const openThink = answer.match(/<think>([\s\S]*)$/i);
|
||||
if (openThink) {
|
||||
reasoning += (reasoning ? "\n\n" : "") + openThink[1].trim();
|
||||
answer = answer.slice(0, openThink.index).trim();
|
||||
}
|
||||
|
||||
// 兜底:如果 answer 为空但 reasoning 有内容,说明模型可能把全部内容放在 think 里了
|
||||
// 此时把 reasoning 当 answer 显示,不显示 thinking 面板
|
||||
if (!answer && reasoning) {
|
||||
answer = reasoning;
|
||||
reasoning = "";
|
||||
}
|
||||
|
||||
return { reasoning, answer };
|
||||
});
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (props.role === "user") return props.content;
|
||||
@@ -48,10 +60,9 @@ const renderedContent = computed(() => {
|
||||
<div class="avatar">{{ role === "assistant" ? "AI" : "我" }}</div>
|
||||
<div class="bubble">
|
||||
<template v-if="role === 'assistant'">
|
||||
<details v-if="assistantParts.reasoning" class="reasoning-panel" :open="streaming">
|
||||
<summary>{{ streaming ? "思考中" : "思考过程" }}</summary>
|
||||
<div class="markdown-content reasoning-content" v-html="renderedReasoning"></div>
|
||||
</details>
|
||||
<div v-if="streaming && assistantParts.reasoning" class="thinking-indicator">
|
||||
<span class="thinking-dots">思考中</span>
|
||||
</div>
|
||||
<div class="content markdown-content" v-html="renderedContent"></div>
|
||||
</template>
|
||||
<div v-else class="content">{{ renderedContent }}</div>
|
||||
|
||||
@@ -49,6 +49,7 @@ export async function streamChat(
|
||||
sessionId: number,
|
||||
message: string,
|
||||
onChunk: (chunk: string) => void,
|
||||
onStatus?: (message: string, type: string) => void,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const token = getToken();
|
||||
@@ -79,7 +80,14 @@ export async function streamChat(
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const payload = line.slice(5).trim();
|
||||
if (payload === "[DONE]") return;
|
||||
const parsed = JSON.parse(payload) as { content?: string };
|
||||
const parsed = JSON.parse(payload) as { type?: string; content?: string; message?: string };
|
||||
if (parsed.type === "queued" || parsed.type === "generating") {
|
||||
onStatus?.(parsed.message || "", parsed.type);
|
||||
continue;
|
||||
}
|
||||
if (parsed.type === "error") {
|
||||
throw new Error(parsed.message || "AI 回复失败");
|
||||
}
|
||||
if (parsed.content) onChunk(parsed.content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,31 @@ button:disabled {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.thinking-indicator {
|
||||
margin-bottom: 10px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
background: #f2f7f5;
|
||||
color: #5d7169;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.thinking-dots::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
margin-left: 4px;
|
||||
border-radius: 50%;
|
||||
background: #0f8b6f;
|
||||
animation: thinking-dot 1.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes thinking-dot {
|
||||
0%, 100% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.composer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
Reference in New Issue
Block a user