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

291 lines
7.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 服务
* 所有端点都是固定的,后端需要实现这些端点
*/
// 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[];
model?: string;
temperature?: number;
maxTokens?: number;
systemPrompt?: string;
stream?: boolean;
// 扩展选项
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;
}
export interface UploadResult {
url: string;
name: string;
size?: number;
mimeType?: string;
}
// API 调用类
class ChatApi {
private baseUrl: string;
constructor(baseUrl = "") {
this.baseUrl = baseUrl;
}
/**
* 流式对话
*/
async *streamChat(
request: ChatRequest,
signal?: AbortSignal,
): AsyncGenerator<string> {
// 构建消息数组,考虑是否包含图片
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 兼容的规范请求体
const openAiRequest = {
model: request.model || "qwen-plus", // 可能需要指定支持视觉的模型
messages: [
{ role: "system", content: request.systemPrompt || "你是一个支持视觉理解的助手。" },
{
role: "user",
content: userContent
}
],
stream: true,
temperature: request.temperature,
max_tokens: request.maxTokens
};
const response = await fetch(
`${this.baseUrl}${API_ENDPOINTS.CHAT_STREAM}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
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 content = data.choices?.[0]?.delta?.content;
if (content) {
yield 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: {
"Content-Type": "application/json",
},
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: {
"Content-Type": "application/json",
},
});
}
/**
* 获取模型列表
*/
async getModels(): Promise<ModelInfo[]> {
return [
{
id: "qwen-max",
name: "通义千问 Max",
description: "最强大的模型",
maxTokens: 8192,
provider: "Aliyun",
},
{
id: "qwen-plus",
name: "通义千问 Plus",
description: "能力均衡",
maxTokens: 8192,
provider: "Aliyun",
},
];
}
/**
* 上传文件
*/
async uploadFile(file: File): Promise<UploadResult> {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.UPLOAD}`, {
method: "POST",
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}