aiChatService.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * AI Chat Service
  3. *
  4. * Integrates with external AI platform API for chat completions.
  5. * Supports both streaming and non-streaming responses.
  6. *
  7. * API Documentation:
  8. * Configuration is read from the frontend environment.
  9. * - Supports text, file_url, and image_url content types
  10. *
  11. * @module services/aiChatService
  12. */
  13. import { fetchWithTimeout } from '../utils/fetchWithTimeout';
  14. /**
  15. * Message content types
  16. */
  17. export type MessageContentType = 'text' | 'file_url' | 'image_url';
  18. /**
  19. * Message content item (for multi-modal messages)
  20. */
  21. export interface MessageContent {
  22. type: MessageContentType;
  23. text?: string;
  24. name?: string;
  25. url?: string;
  26. }
  27. /**
  28. * Chat message
  29. */
  30. export interface ChatMessage {
  31. role: 'user' | 'assistant' | 'system';
  32. content: string | MessageContent[];
  33. }
  34. /**
  35. * Chat completion request
  36. */
  37. export interface ChatCompletionRequest {
  38. /** Custom chat/conversation ID (e.g., user ID) */
  39. chatId: string;
  40. /** Whether to return streaming response */
  41. stream?: boolean;
  42. /** Whether to return intermediate process details */
  43. detail?: boolean;
  44. /** Chat messages */
  45. messages: ChatMessage[];
  46. }
  47. /**
  48. * Chat completion response (non-streaming)
  49. */
  50. export interface ChatCompletionResponse {
  51. /** Response message content */
  52. content: string;
  53. /** Knowledge base references (if any) */
  54. references?: {
  55. title: string;
  56. url: string;
  57. snippet: string;
  58. }[];
  59. /** Process details (if detail=true) */
  60. details?: unknown;
  61. }
  62. /**
  63. * Configuration for AI Chat API
  64. */
  65. interface AIChatConfig {
  66. apiUrl: string;
  67. apiKey: string;
  68. }
  69. /**
  70. * Get AI Chat configuration from environment variables
  71. */
  72. const getAIChatConfig = (): AIChatConfig => {
  73. const isDevelopment = import.meta.env.DEV;
  74. return {
  75. apiUrl: isDevelopment
  76. ? import.meta.env.VITE_AI_API_URL
  77. : import.meta.env.VITE_AI_PROXY_URL,
  78. apiKey: isDevelopment ? import.meta.env.VITE_AI_API_KEY : '',
  79. };
  80. };
  81. const getAuthorizationHeaders = (apiKey: string): Record<string, string> => (
  82. apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
  83. );
  84. /**
  85. * Send a chat completion request to AI platform
  86. *
  87. * @param request - Chat completion request
  88. * @returns Promise resolving to AI response
  89. * @throws {Error} When API call fails
  90. */
  91. export const sendChatCompletion = async (
  92. request: ChatCompletionRequest
  93. ): Promise<ChatCompletionResponse> => {
  94. try {
  95. const config = getAIChatConfig();
  96. if (!config.apiUrl) {
  97. if (import.meta.env.PROD) {
  98. throw new Error('AI 服务未配置,请先配置服务端代理');
  99. }
  100. return mockChatCompletion(request);
  101. }
  102. const response = await fetchWithTimeout(config.apiUrl, {
  103. method: 'POST',
  104. headers: {
  105. 'Content-Type': 'application/json',
  106. ...getAuthorizationHeaders(config.apiKey),
  107. },
  108. body: JSON.stringify({
  109. chatId: request.chatId,
  110. stream: request.stream ?? false,
  111. detail: request.detail ?? false,
  112. messages: request.messages,
  113. }),
  114. });
  115. if (!response.ok) {
  116. const errorText = await response.text();
  117. throw new Error(`AI API error (${response.status}): ${errorText}`);
  118. }
  119. const data = await response.json();
  120. // Extract content from response
  121. // The actual response format may vary, adjust based on API documentation
  122. const content = data.choices?.[0]?.message?.content || data.content || data.response || '';
  123. return {
  124. content,
  125. references: data.references,
  126. details: data.details,
  127. };
  128. } catch (error) {
  129. throw new Error(
  130. 'AI对话失败: ' + (error instanceof Error ? error.message : '未知错误'),
  131. { cause: error }
  132. );
  133. }
  134. };
  135. /**
  136. * Send a streaming chat completion request
  137. *
  138. * @param request - Chat completion request
  139. * @param onChunk - Callback for each chunk of streamed content
  140. * @returns Promise resolving when stream completes
  141. * @throws {Error} When API call fails
  142. */
  143. export const sendStreamingChatCompletion = async (
  144. request: ChatCompletionRequest,
  145. onChunk: (chunk: string) => void
  146. ): Promise<void> => {
  147. try {
  148. const config = getAIChatConfig();
  149. if (!config.apiUrl) {
  150. if (import.meta.env.PROD) {
  151. throw new Error('AI 服务未配置,请先配置服务端代理');
  152. }
  153. const mockResponse = await mockChatCompletion(request);
  154. for (const chunk of mockResponse.content.split(' ')) {
  155. await new Promise((resolve) => setTimeout(resolve, 50));
  156. onChunk(chunk + ' ');
  157. }
  158. return;
  159. }
  160. const response = await fetchWithTimeout(config.apiUrl, {
  161. method: 'POST',
  162. headers: {
  163. 'Content-Type': 'application/json',
  164. ...getAuthorizationHeaders(config.apiKey),
  165. },
  166. body: JSON.stringify({
  167. ...request,
  168. stream: true,
  169. }),
  170. });
  171. if (!response.ok) {
  172. const errorText = await response.text();
  173. throw new Error(`AI API error (${response.status}): ${errorText}`);
  174. }
  175. // Process streaming response
  176. const reader = response.body?.getReader();
  177. if (!reader) {
  178. throw new Error('Response body is not readable');
  179. }
  180. const decoder = new TextDecoder();
  181. let buffer = '';
  182. while (true) {
  183. const { done, value } = await reader.read();
  184. if (done) break;
  185. buffer += decoder.decode(value, { stream: true });
  186. const lines = buffer.split('\n');
  187. buffer = lines.pop() || '';
  188. for (const line of lines) {
  189. if (line.trim() === '' || line.startsWith(':')) continue;
  190. if (line.startsWith('data: ')) {
  191. const data = line.slice(6);
  192. if (data === '[DONE]') continue;
  193. try {
  194. const json = JSON.parse(data);
  195. const content = json.choices?.[0]?.delta?.content || '';
  196. if (content) {
  197. onChunk(content);
  198. }
  199. } catch {
  200. // Failed to parse SSE data, skip this line
  201. }
  202. }
  203. }
  204. }
  205. } catch (error) {
  206. throw new Error(
  207. 'AI流式对话失败: ' + (error instanceof Error ? error.message : '未知错误'),
  208. { cause: error }
  209. );
  210. }
  211. };
  212. /**
  213. * Mock chat completion for testing/fallback
  214. * Used when API key is not configured or API is unavailable
  215. *
  216. * @param request - Chat completion request
  217. * @returns Mock AI response
  218. */
  219. const mockChatCompletion = async (
  220. request: ChatCompletionRequest
  221. ): Promise<ChatCompletionResponse> => {
  222. // Simulate network delay
  223. await new Promise((resolve) => setTimeout(resolve, 800));
  224. // Get the last user message
  225. const lastMessage = request.messages[request.messages.length - 1];
  226. const userContent =
  227. typeof lastMessage.content === 'string'
  228. ? lastMessage.content
  229. : lastMessage.content.find((c) => c.type === 'text')?.text || '';
  230. const lowerContent = userContent.toLowerCase();
  231. // Mock different responses based on content
  232. if (lowerContent.includes('生成') && (lowerContent.includes('报告') || lowerContent.includes('文档'))) {
  233. let reportType = '报告';
  234. if (lowerContent.includes('地质')) {
  235. reportType = '地质报告';
  236. } else if (lowerContent.includes('技术')) {
  237. reportType = '技术报告';
  238. } else if (lowerContent.includes('分析')) {
  239. reportType = '分析报告';
  240. }
  241. return {
  242. content: `好的,我已经为您准备了一份${reportType}。正在生成文档...`,
  243. };
  244. }
  245. if (lowerContent.includes('你好') || lowerContent.includes('hello')) {
  246. return {
  247. content: '您好!我是AI助手,很高兴为您服务。您可以让我帮您生成各种报告和文档。',
  248. };
  249. }
  250. // Default response
  251. return {
  252. content: `我理解您说的是:"${userContent}"。这是一个模拟响应(后端 AI 服务未配置)。`,
  253. };
  254. };
  255. /**
  256. * Helper: Create a simple text message
  257. */
  258. export const createTextMessage = (role: 'user' | 'assistant', text: string): ChatMessage => ({
  259. role,
  260. content: text,
  261. });
  262. /**
  263. * Helper: Create a multi-modal message with text and files
  264. */
  265. export const createMultiModalMessage = (
  266. role: 'user',
  267. contents: MessageContent[]
  268. ): ChatMessage => ({
  269. role,
  270. content: contents,
  271. });
  272. /**
  273. * Export the AI chat service
  274. */
  275. export const aiChatService = {
  276. sendChatCompletion,
  277. sendStreamingChatCompletion,
  278. createTextMessage,
  279. createMultiModalMessage,
  280. };
  281. export default aiChatService;