/** * 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[]; // 非图片附件 URL 列表 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 { // 构建消息数组,考虑是否包含图片 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, files: request.files || [], // 传递文件 URL 列表给后端 // 扩展参数传递给我们的 Python 后端进行特殊处理 deepSearch: request.deepSearch, webSearch: request.webSearch, deepThinking: request.deepThinking, }; 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 { // 构建消息数组,考虑是否包含图片 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 { 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 { 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}