/** * 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 { // 构建消息数组,考虑是否包含图片 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 { // 构建消息数组,考虑是否包含图片 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 { 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 { 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(); } /** * 删除附件(从 OSS 删除) */ async deleteAttachment(url: string): Promise { const response = await fetch( `${this.baseUrl}/api/chat-ui/attachment?url=${encodeURIComponent(url)}`, { method: "DELETE", headers: getAuthHeaders(), } ); if (!response.ok) { throw new Error(`删除附件失败: HTTP ${response.status}`); } } } // 导出单例 export const chatApi = new ChatApi(); // 导出类用于自定义配置 export { ChatApi, API_ENDPOINTS }; // 导出端点常量(供调试使用) // export {API_ENDPOINTS}