151 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { http } from "@/api/http";
import type { Conversation, ConversationSummary } from "@/types/conversation";
import type { ApiResponse } from "@/types/api";
export interface ConversationStreamEvent {
type: "start" | "semantic" | "delta" | "done" | "error";
content?: string;
message?: string;
data?: Conversation | Record<string, unknown>;
}
export function fetchConversations(limit = 50, search?: string) {
return http.get<ApiResponse<ConversationSummary[]>>("/v1/conversations", {
params: { limit, search },
timeout: 30_000,
});
}
export function createConversation(title?: string) {
return http.post<ApiResponse<Conversation>>("/v1/conversations", {
title,
});
}
export function directConversationQuery(kind: "supplier" | "material" | "cases" | "solution", query: string) {
return http.post<ApiResponse<Record<string, unknown>>>("/v1/conversations/direct-query", { kind, query });
}
export function fetchConversation(id: number, offset = 0) {
return http.get<ApiResponse<Conversation>>(`/v1/conversations/${id}`, {
params: { limit: 20, offset },
timeout: 60_000,
});
}
export function deleteConversation(id: number) {
return http.delete<ApiResponse<null>>(`/v1/conversations/${id}`);
}
export function sendConversationMessage(
id: number,
content: string,
messageType: "text" | "image" | "file" | "audio" = "text",
) {
return http.post<ApiResponse<Conversation>>(
`/v1/conversations/${id}/messages`,
{ content, messageType },
);
}
export function withdrawConversationMessage(
conversationId: number,
messageId: number,
) {
return http.delete<ApiResponse<Conversation>>(
`/v1/conversations/${conversationId}/messages/${messageId}`,
);
}
export async function streamConversationMessage(
id: number,
content: string,
onEvent: (event: ConversationStreamEvent) => void,
messageType: "text" | "image" | "file" | "audio" = "text",
signal?: AbortSignal,
) {
const token = localStorage.getItem("risk_prediction_token");
const controller = new AbortController();
const abortFromCaller = () => controller.abort();
if (signal?.aborted) controller.abort();
signal?.addEventListener("abort", abortFromCaller, { once: true });
const timeout = window.setTimeout(() => controller.abort(), 180_000);
let response: Response;
try {
response = await fetch(
`${import.meta.env.VITE_API_BASE_URL || "/api"}/v1/conversations/${id}/stream`,
{
method: "POST",
headers: {
Accept: "text/event-stream",
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ content, messageType }),
signal: controller.signal,
},
);
} catch (reason) {
if (signal?.aborted) throw new Error("已停止生成", { cause: reason });
throw reason;
}
if (response.status === 401) {
localStorage.removeItem("risk_prediction_token");
window.dispatchEvent(new CustomEvent("risk-auth-expired"));
}
if (!response.ok || !response.body)
throw new Error(`流式请求失败(HTTP ${response.status})`);
try {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let completed = false;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const raw = line.slice(5).trim();
if (!raw) continue;
let event: ConversationStreamEvent;
try {
event = JSON.parse(raw) as ConversationStreamEvent;
} catch {
throw new Error("AI 流式响应格式错误,请重试");
}
onEvent(event);
if (event.type === "done") completed = true;
}
}
if (buffer.startsWith("data:")) {
const raw = buffer.slice(5).trim();
if (raw) {
let event: ConversationStreamEvent;
try {
event = JSON.parse(raw) as ConversationStreamEvent;
} catch {
throw new Error("AI 流式响应格式错误,请重试");
}
onEvent(event);
if (event.type === "done") completed = true;
}
}
if (!completed) throw new Error("AI 流式连接中断,请重试");
} catch (reason) {
if (signal?.aborted) {
throw new Error("已停止生成", { cause: reason });
}
if (controller.signal.aborted) {
throw new Error("AI 响应超时,请稍后重试", { cause: reason });
}
throw reason;
} finally {
window.clearTimeout(timeout);
signal?.removeEventListener("abort", abortFromCaller);
}
}