添加全局 Copilot 聊天框组件

This commit is contained in:
2026-03-23 18:03:24 +08:00
parent 55362bef8f
commit accf6ad254
4 changed files with 394 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
import { apiFetch } from "@/lib/apiFetch";
import { config } from "@config/config";
export type StreamEvent =
| { type: "token"; conversationId: string; content: string }
| { type: "done"; conversationId: string }
| { type: "error"; conversationId?: string; message: string; detail?: string };
type StreamOptions = {
message: string;
conversationId?: string;
signal?: AbortSignal;
onEvent: (event: StreamEvent) => void;
};
const parseEventBlock = (block: string): { event?: string; data?: string } => {
const lines = block.split("\n");
let event: string | undefined;
const dataLines: string[] = [];
for (const line of lines) {
if (line.startsWith("event:")) {
event = line.slice("event:".length).trim();
} else if (line.startsWith("data:")) {
dataLines.push(line.slice("data:".length).trim());
}
}
return {
event,
data: dataLines.length ? dataLines.join("\n") : undefined,
};
};
export const streamCopilotChat = async ({
message,
conversationId,
signal,
onEvent,
}: StreamOptions) => {
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, {
method: "POST",
signal,
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({
message,
conversation_id: conversationId,
}),
});
if (!response.ok || !response.body) {
const detail = await response.text();
onEvent({
type: "error",
message: "stream request failed",
detail,
});
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const blocks = buffer.split("\n\n");
buffer = blocks.pop() ?? "";
for (const block of blocks) {
const { event, data } = parseEventBlock(block);
if (!event || !data) continue;
try {
const parsed = JSON.parse(data) as {
conversationId?: string;
content?: string;
message?: string;
detail?: string;
};
if (event === "token") {
onEvent({
type: "token",
conversationId: parsed.conversationId ?? "",
content: parsed.content ?? "",
});
} else if (event === "done") {
onEvent({
type: "done",
conversationId: parsed.conversationId ?? "",
});
} else if (event === "error") {
onEvent({
type: "error",
conversationId: parsed.conversationId,
message: parsed.message ?? "unknown error",
detail: parsed.detail,
});
}
} catch {
onEvent({
type: "error",
message: "invalid SSE data payload",
detail: data,
});
}
}
}
};