add AIAgent
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/** 门店智能助手聊天请求。 */
|
||||
export interface GaoAiChatRequest {
|
||||
message: string;
|
||||
conversationId?: string;
|
||||
/** GLOBAL_MANAGER 兼任店长时使用当前选中门店。 */
|
||||
storeId?: string;
|
||||
}
|
||||
|
||||
/** 智能助手 SSE 事件。 */
|
||||
export interface GaoAiStreamEvent {
|
||||
event: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
/** 智能助手 HTTP 错误。 */
|
||||
export class GaoAiHttpError extends Error {
|
||||
readonly status: number;
|
||||
readonly code?: string;
|
||||
|
||||
constructor(status: number, message: string, code?: string) {
|
||||
super(message);
|
||||
this.name = "GaoAiHttpError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/** 智能助手流式响应错误。 */
|
||||
export class GaoAiStreamError extends Error {
|
||||
readonly code?: string;
|
||||
|
||||
constructor(message: string, code?: string) {
|
||||
super(message);
|
||||
this.name = "GaoAiStreamError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const chatPath = "/gao/ai/chat";
|
||||
|
||||
/**
|
||||
* 发起门店智能助手聊天请求。
|
||||
*
|
||||
* @param request 聊天请求
|
||||
* @param token 当前登录令牌
|
||||
* @param signal 请求取消信号
|
||||
*/
|
||||
export async function chat(request: GaoAiChatRequest, token: string, signal: AbortSignal): Promise<Response> {
|
||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL as string | undefined;
|
||||
const url = new URL(chatPath, apiBaseURL || window.location.origin);
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "text/event-stream",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
if (token) {
|
||||
// 系统现有接口使用 Token;Authorization 兼容智能助手实施协议。
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
headers.Token = token;
|
||||
}
|
||||
|
||||
return await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(request),
|
||||
credentials: "include",
|
||||
signal
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并解析 SSE 流。
|
||||
*
|
||||
* @param response 聊天响应
|
||||
* @param onEvent 事件处理函数
|
||||
*/
|
||||
export async function readSseStream(
|
||||
response: Response,
|
||||
onEvent: (event: GaoAiStreamEvent) => Promise<void>
|
||||
): Promise<void> {
|
||||
if (!response.body) {
|
||||
throw new GaoAiStreamError("智能助手没有返回可读取的数据流", "EMPTY_STREAM");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
buffer = await consumeSseBuffer(buffer, onEvent, false);
|
||||
}
|
||||
|
||||
buffer += decoder.decode();
|
||||
await consumeSseBuffer(buffer, onEvent, true);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async function consumeSseBuffer(
|
||||
buffer: string,
|
||||
onEvent: (event: GaoAiStreamEvent) => Promise<void>,
|
||||
isFinal: boolean
|
||||
): Promise<string> {
|
||||
const normalizedBuffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
||||
const blockList = normalizedBuffer.split("\n\n");
|
||||
const remain = isFinal ? "" : blockList.pop() || "";
|
||||
for (const block of blockList) {
|
||||
const event = parseSseBlock(block);
|
||||
if (event) {
|
||||
await onEvent(event);
|
||||
}
|
||||
}
|
||||
if (isFinal && remain.trim()) {
|
||||
const event = parseSseBlock(remain);
|
||||
if (event) {
|
||||
await onEvent(event);
|
||||
}
|
||||
}
|
||||
return remain;
|
||||
}
|
||||
|
||||
function parseSseBlock(block: string): GaoAiStreamEvent | undefined {
|
||||
let event = "message";
|
||||
const dataLineList: string[] = [];
|
||||
for (const line of block.split("\n")) {
|
||||
if (line.startsWith("event:")) {
|
||||
event = line.slice(6).trim() || "message";
|
||||
} else if (line.startsWith("data:")) {
|
||||
const data = line.slice(5);
|
||||
dataLineList.push(data.startsWith(" ") ? data.slice(1) : data);
|
||||
}
|
||||
}
|
||||
if (!dataLineList.length) {
|
||||
return undefined;
|
||||
}
|
||||
return { event, data: dataLineList.join("\n") };
|
||||
}
|
||||
|
||||
/** 将非 2xx 响应转换为不携带原始响应正文的错误。 */
|
||||
export async function ensureChatResponse(response: Response): Promise<void> {
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
let code: string | undefined;
|
||||
let message = "智能助手请求失败,请稍后重试";
|
||||
try {
|
||||
const payload = await response.json() as {
|
||||
code?: unknown;
|
||||
message?: unknown;
|
||||
error?: unknown;
|
||||
};
|
||||
if (typeof payload.code === "string") {
|
||||
code = payload.code;
|
||||
}
|
||||
if (typeof payload.message === "string" && payload.message.trim()) {
|
||||
message = payload.message.trim();
|
||||
} else if (typeof payload.error === "string" && payload.error.trim()) {
|
||||
message = payload.error.trim();
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON 错误响应使用上面的安全默认文案。
|
||||
}
|
||||
throw new GaoAiHttpError(response.status, message, code);
|
||||
}
|
||||
|
||||
/** 解析 SSE 数据,解析失败时返回原始文本。 */
|
||||
export function parseSseData(data: string): unknown {
|
||||
if (data === "[DONE]") {
|
||||
return data;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(data) as unknown;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user