ai-chat-ui/src/services/api.ts

353 lines
9.2 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.

/**
* Chat UI API 服务
* 所有端点都是固定的,后端需要实现这些端点
*/
import { getAuthHeaders } from './request';
// API 端点定义(固定)
const API_ENDPOINTS = {
// 发送消息(流式)
CHAT_STREAM: "/api/chat-ui/chat",
// 发送消息(非流式)
CHAT: "/api/chat-ui/chat",
// 获取对话历史
CONVERSATIONS: "/api/chat-ui/conversations",
// 获取单个对话
CONVERSATION: "/api/chat-ui/conversations/:id",
// 删除对话
DELETE_CONVERSATION: "/api/chat-ui/conversations/:id",
// 上传文件
UPLOAD: "/api/chat-ui/upload",
// 获取模型列表
MODELS: "/api/chat-ui/models",
// 停止生成
STOP: "/api/chat-ui/stop",
};
// 请求类型定义
export interface ChatMessage {
role: "user" | "assistant" | "system";
content: string;
images?: string[];
files?: string[];
}
export interface ChatRequest {
conversationId?: string;
message: string;
images?: string[];
files?: string[]; // 非图片附件 URL 列表
model?: string;
temperature?: number;
maxTokens?: number;
systemPrompt?: string;
stream?: boolean;
// 历史对话消息(用于上下文记忆)
history?: { role: string; content: string }[];
// 扩展选项
deepSearch?: boolean;
webSearch?: boolean;
deepThinking?: boolean;
}
export interface ChatResponse {
id: string;
conversationId: string;
content: string;
model: string;
createdAt: number;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
export interface ModelInfo {
id: string;
name: string;
description: string;
maxTokens: number;
provider: string;
supports_thinking: boolean;
supports_web_search: boolean;
supports_vision: boolean;
supports_files: boolean;
}
export interface UploadResult {
url: string;
name: string;
size?: number;
mimeType?: string;
}
// 流式响应块类型
export interface StreamChunk {
type: "content" | "reasoning";
text: string;
}
// API 调用类
class ChatApi {
private baseUrl: string;
constructor(baseUrl = "") {
this.baseUrl = baseUrl;
}
/**
* 流式对话
*/
async *streamChat(
request: ChatRequest,
signal?: AbortSignal,
): AsyncGenerator<StreamChunk> {
// 构建消息数组,考虑是否包含图片
let userContent;
if (request.images && request.images.length > 0) {
// 如果有图片则构建内容数组针对阿里云DashScope API的格式
userContent = [{ type: "text", text: request.message }];
// 添加图片URL到内容中阿里云格式
request.images.forEach((imageUrl) => {
userContent.push({
type: "image_url",
image_url: imageUrl, // 注意:阿里云格式不需要嵌套对象
});
});
} else {
// 没有图片时,使用简单的文本
userContent = request.message;
}
// 将前端简化的请求翻译为 OpenAI 兼容的规范请求体
// 构建 messages 数组system + 历史消息 + 当前用户消息
const systemMessage = {
role: "system",
content:
request.systemPrompt ||
"你是一个智能助手,可以分析用户发送的文字,文件或图片内容,并进行回答。",
};
const currentUserMessage = {
role: "user",
content: userContent,
};
const allMessages =
request.history && request.history.length > 0
? [systemMessage, ...request.history, currentUserMessage]
: [systemMessage, currentUserMessage];
const openAiRequest = {
model: request.model || "glm-4-flash",
messages: allMessages,
stream: true,
temperature: request.temperature,
max_tokens: request.maxTokens,
files: request.files || [],
// 扩展参数传递给我们的 Python 后端进行特殊处理
deepSearch: request.deepSearch,
webSearch: request.webSearch,
deepThinking: request.deepThinking,
};
const response = await fetch(
`${this.baseUrl}${API_ENDPOINTS.CHAT_STREAM}`,
{
method: "POST",
headers: {
...getAuthHeaders(),
Accept: "text/event-stream",
},
body: JSON.stringify(openAiRequest),
signal,
},
);
if (!response.ok) {
const error = await response.text();
throw new Error(error || `HTTP ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
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 lines = buffer.split("\n");
// 保留最后一行未完整的 JSON
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim() === "" || line.includes("[DONE]")) continue;
const match = line.match(/^data:\s*(.+)$/);
if (match) {
try {
const data = JSON.parse(match[1]);
// 检查是否有完成原因,如果是完成则跳出
const finishReason = data.choices?.[0]?.finish_reason;
if (finishReason && finishReason !== "null") {
break;
}
const delta = data.choices?.[0]?.delta;
// 处理深度思考内容reasoning_content
const reasoningContent = delta?.reasoning_content;
if (reasoningContent) {
yield { type: "reasoning", text: reasoningContent };
}
// 处理普通内容
const content = delta?.content;
if (content) {
yield { type: "content", text: content };
}
} catch (e) {
console.warn("JSON解析错误", e, line);
}
}
}
}
}
/**
* 非流式对话
*/
async chat(request: ChatRequest): Promise<ChatResponse> {
// 构建消息数组,考虑是否包含图片
let userContent;
if (request.images && request.images.length > 0) {
// 如果有图片,则构建内容数组
userContent = [{ type: "text", text: request.message }];
// 添加图片URL到内容中
request.images.forEach((imageUrl) => {
userContent.push({
type: "image_url",
image_url: { url: imageUrl },
});
});
} else {
// 没有图片时,使用简单的文本
userContent = request.message;
}
const requestBody = {
...request,
message: userContent,
};
const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.CHAT}`, {
method: "POST",
headers: getAuthHeaders(),
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || `HTTP ${response.status}`);
}
return response.json();
}
/**
* 停止对话
*/
async stopChat(messageId?: string) {
await fetch(`${this.baseUrl}${API_ENDPOINTS.STOP}/${messageId}`, {
method: "POST",
headers: getAuthHeaders(),
});
}
/**
* 获取模型列表
*/
async getModels(): Promise<ModelInfo[]> {
try {
const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.MODELS}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`获取模型列表失败: HTTP ${response.status}`);
}
const data = await response.json();
// 后端返回格式: { object: "list", data: [...] }
return data.data || [];
} catch (error) {
console.error("获取模型列表失败:", error);
// 返回默认模型列表作为降级
return [
{
id: "glm-4.6",
name: "智普 GLM-4.6",
description: "最强大的模型",
maxTokens: 200000,
provider: "Zhipu",
supports_thinking: true,
supports_web_search: true,
supports_vision: false,
supports_files: false,
},
{
id: "deepseek-chat",
name: "DeepSeek Chat",
description: "DeepSeek 对话模型",
maxTokens: 64000,
provider: "DeepSeek",
supports_thinking: false,
supports_web_search: false,
supports_vision: false,
supports_files: false,
},
];
}
}
/**
* 上传文件
*/
async uploadFile(file: File): Promise<UploadResult> {
const formData = new FormData();
formData.append("file", file);
// 获取认证 headers但不包含 Content-Type让浏览器为 FormData 自动设置)
const authHeaders = getAuthHeaders();
const { 'Content-Type': _, ...headersWithoutContentType } = authHeaders;
const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.UPLOAD}`, {
method: "POST",
headers: headersWithoutContentType,
body: formData,
});
if (!response.ok) {
throw new Error(`上传失败: HTTP ${response.status}`);
}
return response.json();
}
}
// 导出单例
export const chatApi = new ChatApi();
// 导出类用于自定义配置
export { ChatApi, API_ENDPOINTS };
// 导出端点常量(供调试使用)
// export {API_ENDPOINTS}