add AIAgent

This commit is contained in:
Timi
2026-09-16 01:39:44 +08:00
parent c29436f845
commit a6f9fabcec
11 changed files with 1266 additions and 5 deletions
+183
View File
@@ -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;
}
}