useApiRequest.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import { useCallback } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { SSE } from 'sse';
  4. import { getUserIdFromLocalStorage } from '../helpers/index.js';
  5. import {
  6. API_ENDPOINTS,
  7. MESSAGE_STATUS,
  8. DEBUG_TABS
  9. } from '../utils/constants';
  10. import {
  11. buildApiPayload,
  12. handleApiError
  13. } from '../utils/apiUtils';
  14. import {
  15. processThinkTags,
  16. processIncompleteThinkTags
  17. } from '../utils/messageUtils';
  18. export const useApiRequest = (
  19. setMessage,
  20. setDebugData,
  21. setActiveDebugTab,
  22. sseSourceRef
  23. ) => {
  24. const { t } = useTranslation();
  25. // 处理消息自动关闭逻辑的公共函数
  26. const applyAutoCollapseLogic = useCallback((message, isThinkingComplete = true) => {
  27. const shouldAutoCollapse = isThinkingComplete && !message.hasAutoCollapsed;
  28. return {
  29. isThinkingComplete,
  30. hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
  31. isReasoningExpanded: shouldAutoCollapse ? false : message.isReasoningExpanded,
  32. };
  33. }, []);
  34. // 流式消息更新
  35. const streamMessageUpdate = useCallback((textChunk, type) => {
  36. setMessage(prevMessage => {
  37. const lastMessage = prevMessage[prevMessage.length - 1];
  38. if (lastMessage.status === MESSAGE_STATUS.ERROR) {
  39. return prevMessage;
  40. }
  41. if (lastMessage.status === MESSAGE_STATUS.LOADING ||
  42. lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
  43. let newMessage = { ...lastMessage };
  44. if (type === 'reasoning') {
  45. newMessage = {
  46. ...newMessage,
  47. reasoningContent: (lastMessage.reasoningContent || '') + textChunk,
  48. status: MESSAGE_STATUS.INCOMPLETE,
  49. isThinkingComplete: false,
  50. };
  51. } else if (type === 'content') {
  52. const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent;
  53. const newContent = (lastMessage.content || '') + textChunk;
  54. let shouldCollapseFromThinkTag = false;
  55. let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
  56. if (lastMessage.isReasoningExpanded && newContent.includes('</think>')) {
  57. const thinkMatches = newContent.match(/<think>/g);
  58. const thinkCloseMatches = newContent.match(/<\/think>/g);
  59. if (thinkMatches && thinkCloseMatches &&
  60. thinkCloseMatches.length >= thinkMatches.length) {
  61. shouldCollapseFromThinkTag = true;
  62. thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
  63. }
  64. }
  65. // 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
  66. const isThinkingComplete = (lastMessage.reasoningContent && !lastMessage.isThinkingComplete) ||
  67. thinkingCompleteFromTags;
  68. const autoCollapseState = applyAutoCollapseLogic(lastMessage, isThinkingComplete);
  69. newMessage = {
  70. ...newMessage,
  71. content: newContent,
  72. status: MESSAGE_STATUS.INCOMPLETE,
  73. ...autoCollapseState,
  74. };
  75. }
  76. return [...prevMessage.slice(0, -1), newMessage];
  77. }
  78. return prevMessage;
  79. });
  80. }, [setMessage, applyAutoCollapseLogic]);
  81. // 完成消息
  82. const completeMessage = useCallback((status = MESSAGE_STATUS.COMPLETE) => {
  83. setMessage(prevMessage => {
  84. const lastMessage = prevMessage[prevMessage.length - 1];
  85. if (lastMessage.status === MESSAGE_STATUS.COMPLETE ||
  86. lastMessage.status === MESSAGE_STATUS.ERROR) {
  87. return prevMessage;
  88. }
  89. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  90. return [
  91. ...prevMessage.slice(0, -1),
  92. {
  93. ...lastMessage,
  94. status: status,
  95. ...autoCollapseState,
  96. }
  97. ];
  98. });
  99. }, [setMessage, applyAutoCollapseLogic]);
  100. // 非流式请求
  101. const handleNonStreamRequest = useCallback(async (payload) => {
  102. setDebugData(prev => ({
  103. ...prev,
  104. request: payload,
  105. timestamp: new Date().toISOString(),
  106. response: null
  107. }));
  108. setActiveDebugTab(DEBUG_TABS.REQUEST);
  109. try {
  110. const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
  111. method: 'POST',
  112. headers: {
  113. 'Content-Type': 'application/json',
  114. 'New-Api-User': getUserIdFromLocalStorage(),
  115. },
  116. body: JSON.stringify(payload),
  117. });
  118. if (!response.ok) {
  119. let errorBody = '';
  120. try {
  121. errorBody = await response.text();
  122. } catch (e) {
  123. errorBody = '无法读取错误响应体';
  124. }
  125. const errorInfo = handleApiError(
  126. new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`),
  127. response
  128. );
  129. setDebugData(prev => ({
  130. ...prev,
  131. response: JSON.stringify(errorInfo, null, 2)
  132. }));
  133. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  134. throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
  135. }
  136. const data = await response.json();
  137. setDebugData(prev => ({
  138. ...prev,
  139. response: JSON.stringify(data, null, 2)
  140. }));
  141. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  142. if (data.choices?.[0]) {
  143. const choice = data.choices[0];
  144. let content = choice.message?.content || '';
  145. let reasoningContent = choice.message?.reasoning_content || '';
  146. const processed = processThinkTags(content, reasoningContent);
  147. setMessage(prevMessage => {
  148. const newMessages = [...prevMessage];
  149. const lastMessage = newMessages[newMessages.length - 1];
  150. if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
  151. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  152. newMessages[newMessages.length - 1] = {
  153. ...lastMessage,
  154. content: processed.content,
  155. reasoningContent: processed.reasoningContent,
  156. status: MESSAGE_STATUS.COMPLETE,
  157. ...autoCollapseState,
  158. };
  159. }
  160. return newMessages;
  161. });
  162. }
  163. } catch (error) {
  164. console.error('Non-stream request error:', error);
  165. const errorInfo = handleApiError(error);
  166. setDebugData(prev => ({
  167. ...prev,
  168. response: JSON.stringify(errorInfo, null, 2)
  169. }));
  170. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  171. setMessage(prevMessage => {
  172. const newMessages = [...prevMessage];
  173. const lastMessage = newMessages[newMessages.length - 1];
  174. if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
  175. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  176. newMessages[newMessages.length - 1] = {
  177. ...lastMessage,
  178. content: t('请求发生错误: ') + error.message,
  179. status: MESSAGE_STATUS.ERROR,
  180. ...autoCollapseState,
  181. };
  182. }
  183. return newMessages;
  184. });
  185. }
  186. }, [setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic]);
  187. // SSE请求
  188. const handleSSE = useCallback((payload) => {
  189. setDebugData(prev => ({
  190. ...prev,
  191. request: payload,
  192. timestamp: new Date().toISOString(),
  193. response: null
  194. }));
  195. setActiveDebugTab(DEBUG_TABS.REQUEST);
  196. const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
  197. headers: {
  198. 'Content-Type': 'application/json',
  199. 'New-Api-User': getUserIdFromLocalStorage(),
  200. },
  201. method: 'POST',
  202. payload: JSON.stringify(payload),
  203. });
  204. sseSourceRef.current = source;
  205. let responseData = '';
  206. let hasReceivedFirstResponse = false;
  207. source.addEventListener('message', (e) => {
  208. if (e.data === '[DONE]') {
  209. source.close();
  210. sseSourceRef.current = null;
  211. setDebugData(prev => ({ ...prev, response: responseData }));
  212. completeMessage();
  213. return;
  214. }
  215. try {
  216. const payload = JSON.parse(e.data);
  217. responseData += e.data + '\n';
  218. if (!hasReceivedFirstResponse) {
  219. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  220. hasReceivedFirstResponse = true;
  221. }
  222. const delta = payload.choices?.[0]?.delta;
  223. if (delta) {
  224. if (delta.reasoning_content) {
  225. streamMessageUpdate(delta.reasoning_content, 'reasoning');
  226. }
  227. if (delta.content) {
  228. streamMessageUpdate(delta.content, 'content');
  229. }
  230. }
  231. } catch (error) {
  232. console.error('Failed to parse SSE message:', error);
  233. const errorInfo = `解析错误: ${error.message}`;
  234. setDebugData(prev => ({
  235. ...prev,
  236. response: responseData + `\n\nError: ${errorInfo}`
  237. }));
  238. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  239. streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
  240. completeMessage(MESSAGE_STATUS.ERROR);
  241. }
  242. });
  243. source.addEventListener('error', (e) => {
  244. console.error('SSE Error:', e);
  245. const errorMessage = e.data || t('请求发生错误');
  246. const errorInfo = handleApiError(new Error(errorMessage));
  247. errorInfo.readyState = source.readyState;
  248. setDebugData(prev => ({
  249. ...prev,
  250. response: responseData + '\n\nSSE Error:\n' + JSON.stringify(errorInfo, null, 2)
  251. }));
  252. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  253. streamMessageUpdate(errorMessage, 'content');
  254. completeMessage(MESSAGE_STATUS.ERROR);
  255. sseSourceRef.current = null;
  256. source.close();
  257. });
  258. source.addEventListener('readystatechange', (e) => {
  259. if (e.readyState >= 2 && source.status !== undefined && source.status !== 200) {
  260. const errorInfo = handleApiError(new Error('HTTP状态错误'));
  261. errorInfo.status = source.status;
  262. errorInfo.readyState = source.readyState;
  263. setDebugData(prev => ({
  264. ...prev,
  265. response: responseData + '\n\nHTTP Error:\n' + JSON.stringify(errorInfo, null, 2)
  266. }));
  267. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  268. source.close();
  269. streamMessageUpdate(t('连接已断开'), 'content');
  270. completeMessage(MESSAGE_STATUS.ERROR);
  271. }
  272. });
  273. try {
  274. source.stream();
  275. } catch (error) {
  276. console.error('Failed to start SSE stream:', error);
  277. const errorInfo = handleApiError(error);
  278. setDebugData(prev => ({
  279. ...prev,
  280. response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2)
  281. }));
  282. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  283. streamMessageUpdate(t('建立连接时发生错误'), 'content');
  284. completeMessage(MESSAGE_STATUS.ERROR);
  285. }
  286. }, [setDebugData, setActiveDebugTab, streamMessageUpdate, completeMessage, t, applyAutoCollapseLogic]);
  287. // 停止生成
  288. const onStopGenerator = useCallback(() => {
  289. if (sseSourceRef.current) {
  290. sseSourceRef.current.close();
  291. sseSourceRef.current = null;
  292. setMessage(prevMessage => {
  293. const lastMessage = prevMessage[prevMessage.length - 1];
  294. if (lastMessage.status === MESSAGE_STATUS.LOADING ||
  295. lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
  296. const processed = processIncompleteThinkTags(
  297. lastMessage.content || '',
  298. lastMessage.reasoningContent || ''
  299. );
  300. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  301. return [
  302. ...prevMessage.slice(0, -1),
  303. {
  304. ...lastMessage,
  305. status: MESSAGE_STATUS.COMPLETE,
  306. reasoningContent: processed.reasoningContent || null,
  307. content: processed.content,
  308. ...autoCollapseState,
  309. }
  310. ];
  311. }
  312. return prevMessage;
  313. });
  314. }
  315. }, [setMessage, applyAutoCollapseLogic]);
  316. // 发送请求
  317. const sendRequest = useCallback((payload, isStream) => {
  318. if (isStream) {
  319. handleSSE(payload);
  320. } else {
  321. handleNonStreamRequest(payload);
  322. }
  323. }, [handleSSE, handleNonStreamRequest]);
  324. return {
  325. sendRequest,
  326. onStopGenerator,
  327. streamMessageUpdate,
  328. completeMessage,
  329. };
  330. };