/** * AI Chat Service * * Integrates with external AI platform API for chat completions. * Supports both streaming and non-streaming responses. * * API Documentation: * Configuration is read from the frontend environment. * - Supports text, file_url, and image_url content types * * @module services/aiChatService */ import { fetchWithTimeout } from '../utils/fetchWithTimeout'; /** * Message content types */ export type MessageContentType = 'text' | 'file_url' | 'image_url'; /** * Message content item (for multi-modal messages) */ export interface MessageContent { type: MessageContentType; text?: string; name?: string; url?: string; } /** * Chat message */ export interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string | MessageContent[]; } /** * Chat completion request */ export interface ChatCompletionRequest { /** Custom chat/conversation ID (e.g., user ID) */ chatId: string; /** Whether to return streaming response */ stream?: boolean; /** Whether to return intermediate process details */ detail?: boolean; /** Chat messages */ messages: ChatMessage[]; } /** * Chat completion response (non-streaming) */ export interface ChatCompletionResponse { /** Response message content */ content: string; /** Knowledge base references (if any) */ references?: { title: string; url: string; snippet: string; }[]; /** Process details (if detail=true) */ details?: unknown; } /** * Configuration for AI Chat API */ interface AIChatConfig { apiUrl: string; apiKey: string; } /** * Get AI Chat configuration from environment variables */ const getAIChatConfig = (): AIChatConfig => { const isDevelopment = import.meta.env.DEV; return { apiUrl: isDevelopment ? import.meta.env.VITE_AI_API_URL : import.meta.env.VITE_AI_PROXY_URL, apiKey: isDevelopment ? import.meta.env.VITE_AI_API_KEY : '', }; }; const getAuthorizationHeaders = (apiKey: string): Record => ( apiKey ? { Authorization: `Bearer ${apiKey}` } : {} ); /** * Send a chat completion request to AI platform * * @param request - Chat completion request * @returns Promise resolving to AI response * @throws {Error} When API call fails */ export const sendChatCompletion = async ( request: ChatCompletionRequest ): Promise => { try { const config = getAIChatConfig(); if (!config.apiUrl) { if (import.meta.env.PROD) { throw new Error('AI 服务未配置,请先配置服务端代理'); } return mockChatCompletion(request); } const response = await fetchWithTimeout(config.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...getAuthorizationHeaders(config.apiKey), }, body: JSON.stringify({ chatId: request.chatId, stream: request.stream ?? false, detail: request.detail ?? false, messages: request.messages, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`AI API error (${response.status}): ${errorText}`); } const data = await response.json(); // Extract content from response // The actual response format may vary, adjust based on API documentation const content = data.choices?.[0]?.message?.content || data.content || data.response || ''; return { content, references: data.references, details: data.details, }; } catch (error) { throw new Error( 'AI对话失败: ' + (error instanceof Error ? error.message : '未知错误'), { cause: error } ); } }; /** * Send a streaming chat completion request * * @param request - Chat completion request * @param onChunk - Callback for each chunk of streamed content * @returns Promise resolving when stream completes * @throws {Error} When API call fails */ export const sendStreamingChatCompletion = async ( request: ChatCompletionRequest, onChunk: (chunk: string) => void ): Promise => { try { const config = getAIChatConfig(); if (!config.apiUrl) { if (import.meta.env.PROD) { throw new Error('AI 服务未配置,请先配置服务端代理'); } const mockResponse = await mockChatCompletion(request); for (const chunk of mockResponse.content.split(' ')) { await new Promise((resolve) => setTimeout(resolve, 50)); onChunk(chunk + ' '); } return; } const response = await fetchWithTimeout(config.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...getAuthorizationHeaders(config.apiKey), }, body: JSON.stringify({ ...request, stream: true, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`AI API error (${response.status}): ${errorText}`); } // Process streaming response const reader = response.body?.getReader(); if (!reader) { throw new Error('Response body is not readable'); } const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim() === '' || line.startsWith(':')) continue; if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const json = JSON.parse(data); const content = json.choices?.[0]?.delta?.content || ''; if (content) { onChunk(content); } } catch { // Failed to parse SSE data, skip this line } } } } } catch (error) { throw new Error( 'AI流式对话失败: ' + (error instanceof Error ? error.message : '未知错误'), { cause: error } ); } }; /** * Mock chat completion for testing/fallback * Used when API key is not configured or API is unavailable * * @param request - Chat completion request * @returns Mock AI response */ const mockChatCompletion = async ( request: ChatCompletionRequest ): Promise => { // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 800)); // Get the last user message const lastMessage = request.messages[request.messages.length - 1]; const userContent = typeof lastMessage.content === 'string' ? lastMessage.content : lastMessage.content.find((c) => c.type === 'text')?.text || ''; const lowerContent = userContent.toLowerCase(); // Mock different responses based on content if (lowerContent.includes('生成') && (lowerContent.includes('报告') || lowerContent.includes('文档'))) { let reportType = '报告'; if (lowerContent.includes('地质')) { reportType = '地质报告'; } else if (lowerContent.includes('技术')) { reportType = '技术报告'; } else if (lowerContent.includes('分析')) { reportType = '分析报告'; } return { content: `好的,我已经为您准备了一份${reportType}。正在生成文档...`, }; } if (lowerContent.includes('你好') || lowerContent.includes('hello')) { return { content: '您好!我是AI助手,很高兴为您服务。您可以让我帮您生成各种报告和文档。', }; } // Default response return { content: `我理解您说的是:"${userContent}"。这是一个模拟响应(后端 AI 服务未配置)。`, }; }; /** * Helper: Create a simple text message */ export const createTextMessage = (role: 'user' | 'assistant', text: string): ChatMessage => ({ role, content: text, }); /** * Helper: Create a multi-modal message with text and files */ export const createMultiModalMessage = ( role: 'user', contents: MessageContent[] ): ChatMessage => ({ role, content: contents, }); /** * Export the AI chat service */ export const aiChatService = { sendChatCompletion, sendStreamingChatCompletion, createTextMessage, createMultiModalMessage, }; export default aiChatService;