feat(chat): add configurable streaming output

This commit is contained in:
2026-07-17 13:34:27 +08:00
parent f7076569a7
commit 13fed0467a
11 changed files with 172 additions and 24 deletions

View File

@@ -15,16 +15,47 @@ const markdown = new MarkdownIt({ breaks: true, html: false, linkify: true });
const answer = computed(() => {
if (props.role === "user") return props.content;
// 部分模型会产生嵌套 think 标签,使用贪婪匹配移除完整思考区,再清理流式残留标签。
let content = props.content.replace(/<think>[\s\S]*<\/think>/gi, "");
content = content.replace(/<think>[\s\S]*$/i, "");
content = content.replace(/<\/?think>/gi, "");
return content.trim();
return stripStreamingReasoning(props.content).replace(/^\s+/, "");
});
const renderedContent = computed(() =>
props.role === "assistant" ? markdown.render(answer.value) : answer.value,
);
const hasAnswer = computed(() => answer.value.trim().length > 0);
const renderedContent = computed(() => {
if (props.role !== "assistant") return answer.value;
return hasAnswer.value ? markdown.render(answer.value) : "";
});
function stripStreamingReasoning(content: string) {
const lower = content.toLowerCase();
let visible = "";
let cursor = 0;
let reasoningDepth = 0;
while (cursor < content.length) {
const tagStart = content.indexOf("<", cursor);
if (tagStart === -1) {
if (reasoningDepth === 0) visible += content.slice(cursor);
break;
}
if (reasoningDepth === 0) visible += content.slice(cursor, tagStart);
const tail = lower.slice(tagStart);
const openTag = tail.match(/^<think(?:\s[^>]*)?>/);
if (openTag) {
reasoningDepth += 1;
cursor = tagStart + openTag[0].length;
continue;
}
const closeTag = tail.match(/^<\/think\s*>/);
if (closeTag) {
reasoningDepth = Math.max(0, reasoningDepth - 1);
cursor = tagStart + closeTag[0].length;
continue;
}
if ("<think".startsWith(tail) || "</think>".startsWith(tail)) break;
if (reasoningDepth === 0) visible += "<";
cursor = tagStart + 1;
}
return visible;
}
const displayTime = computed(() => {
// 消息服务返回的是 UTC 无时区字符串,补齐时区后再按用户本地时间展示。

View File

@@ -107,12 +107,12 @@ export async function streamChat(
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
const parts = buffer.split(/\r?\n\r?\n/);
buffer = parts.pop() ?? "";
for (const part of parts) {
const line = part.trim();
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
const dataLines = part.split(/\r?\n/).filter((line) => line.startsWith("data:"));
if (!dataLines.length) continue;
const payload = dataLines.map((line) => line.slice(5).trimStart()).join("\n").trim();
if (payload === "[DONE]") return;
const parsed = JSON.parse(payload) as { type?: string; content?: string; message?: string };
if (parsed.type === "queued" || parsed.type === "generating") {