useApiRequest.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import { useCallback } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { SSE } from 'sse.js';
  4. import {
  5. API_ENDPOINTS,
  6. MESSAGE_STATUS,
  7. DEBUG_TABS
  8. } from '../constants/playground.constants';
  9. import {
  10. getUserIdFromLocalStorage,
  11. handleApiError,
  12. processThinkTags,
  13. processIncompleteThinkTags
  14. } from '../helpers';
  15. export const useApiRequest = (
  16. setMessage,
  17. setDebugData,
  18. setActiveDebugTab,
  19. sseSourceRef,
  20. saveMessages
  21. ) => {
  22. const { t } = useTranslation();
  23. // 处理消息自动关闭逻辑的公共函数
  24. const applyAutoCollapseLogic = useCallback((message, isThinkingComplete = true) => {
  25. const shouldAutoCollapse = isThinkingComplete && !message.hasAutoCollapsed;
  26. return {
  27. isThinkingComplete,
  28. hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
  29. isReasoningExpanded: shouldAutoCollapse ? false : message.isReasoningExpanded,
  30. };
  31. }, []);
  32. // 流式消息更新
  33. const streamMessageUpdate = useCallback((textChunk, type) => {
  34. setMessage(prevMessage => {
  35. const lastMessage = prevMessage[prevMessage.length - 1];
  36. if (!lastMessage) return prevMessage;
  37. if (lastMessage.role !== 'assistant') return prevMessage;
  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. const updatedMessages = [
  91. ...prevMessage.slice(0, -1),
  92. {
  93. ...lastMessage,
  94. status: status,
  95. ...autoCollapseState,
  96. }
  97. ];
  98. // 在消息完成时保存,传入更新后的消息列表
  99. if (status === MESSAGE_STATUS.COMPLETE || status === MESSAGE_STATUS.ERROR) {
  100. setTimeout(() => saveMessages(updatedMessages), 0);
  101. }
  102. return updatedMessages;
  103. });
  104. }, [setMessage, applyAutoCollapseLogic, saveMessages]);
  105. // 非流式请求
  106. const handleNonStreamRequest = useCallback(async (payload) => {
  107. setDebugData(prev => ({
  108. ...prev,
  109. request: payload,
  110. timestamp: new Date().toISOString(),
  111. response: null
  112. }));
  113. setActiveDebugTab(DEBUG_TABS.REQUEST);
  114. try {
  115. const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
  116. method: 'POST',
  117. headers: {
  118. 'Content-Type': 'application/json',
  119. 'New-Api-User': getUserIdFromLocalStorage(),
  120. },
  121. body: JSON.stringify(payload),
  122. });
  123. if (!response.ok) {
  124. let errorBody = '';
  125. try {
  126. errorBody = await response.text();
  127. } catch (e) {
  128. errorBody = '无法读取错误响应体';
  129. }
  130. const errorInfo = handleApiError(
  131. new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`),
  132. response
  133. );
  134. setDebugData(prev => ({
  135. ...prev,
  136. response: JSON.stringify(errorInfo, null, 2)
  137. }));
  138. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  139. throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
  140. }
  141. const data = await response.json();
  142. setDebugData(prev => ({
  143. ...prev,
  144. response: JSON.stringify(data, null, 2)
  145. }));
  146. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  147. if (data.choices?.[0]) {
  148. const choice = data.choices[0];
  149. let content = choice.message?.content || '';
  150. let reasoningContent = choice.message?.reasoning_content || '';
  151. const processed = processThinkTags(content, reasoningContent);
  152. setMessage(prevMessage => {
  153. const newMessages = [...prevMessage];
  154. const lastMessage = newMessages[newMessages.length - 1];
  155. if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
  156. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  157. newMessages[newMessages.length - 1] = {
  158. ...lastMessage,
  159. content: processed.content,
  160. reasoningContent: processed.reasoningContent,
  161. status: MESSAGE_STATUS.COMPLETE,
  162. ...autoCollapseState,
  163. };
  164. }
  165. return newMessages;
  166. });
  167. }
  168. } catch (error) {
  169. console.error('Non-stream request error:', error);
  170. const errorInfo = handleApiError(error);
  171. setDebugData(prev => ({
  172. ...prev,
  173. response: JSON.stringify(errorInfo, null, 2)
  174. }));
  175. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  176. setMessage(prevMessage => {
  177. const newMessages = [...prevMessage];
  178. const lastMessage = newMessages[newMessages.length - 1];
  179. if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
  180. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  181. newMessages[newMessages.length - 1] = {
  182. ...lastMessage,
  183. content: t('请求发生错误: ') + error.message,
  184. status: MESSAGE_STATUS.ERROR,
  185. ...autoCollapseState,
  186. };
  187. }
  188. return newMessages;
  189. });
  190. }
  191. }, [setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic]);
  192. // SSE请求
  193. const handleSSE = useCallback((payload) => {
  194. setDebugData(prev => ({
  195. ...prev,
  196. request: payload,
  197. timestamp: new Date().toISOString(),
  198. response: null
  199. }));
  200. setActiveDebugTab(DEBUG_TABS.REQUEST);
  201. const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
  202. headers: {
  203. 'Content-Type': 'application/json',
  204. 'New-Api-User': getUserIdFromLocalStorage(),
  205. },
  206. method: 'POST',
  207. payload: JSON.stringify(payload),
  208. });
  209. sseSourceRef.current = source;
  210. let responseData = '';
  211. let hasReceivedFirstResponse = false;
  212. source.addEventListener('message', (e) => {
  213. if (e.data === '[DONE]') {
  214. source.close();
  215. sseSourceRef.current = null;
  216. setDebugData(prev => ({ ...prev, response: responseData }));
  217. completeMessage();
  218. return;
  219. }
  220. try {
  221. const payload = JSON.parse(e.data);
  222. responseData += e.data + '\n';
  223. if (!hasReceivedFirstResponse) {
  224. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  225. hasReceivedFirstResponse = true;
  226. }
  227. const delta = payload.choices?.[0]?.delta;
  228. if (delta) {
  229. if (delta.reasoning_content) {
  230. streamMessageUpdate(delta.reasoning_content, 'reasoning');
  231. }
  232. if (delta.content) {
  233. streamMessageUpdate(delta.content, 'content');
  234. }
  235. }
  236. } catch (error) {
  237. console.error('Failed to parse SSE message:', error);
  238. const errorInfo = `解析错误: ${error.message}`;
  239. setDebugData(prev => ({
  240. ...prev,
  241. response: responseData + `\n\nError: ${errorInfo}`
  242. }));
  243. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  244. streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
  245. completeMessage(MESSAGE_STATUS.ERROR);
  246. }
  247. });
  248. source.addEventListener('error', (e) => {
  249. console.error('SSE Error:', e);
  250. const errorMessage = e.data || t('请求发生错误');
  251. const errorInfo = handleApiError(new Error(errorMessage));
  252. errorInfo.readyState = source.readyState;
  253. setDebugData(prev => ({
  254. ...prev,
  255. response: responseData + '\n\nSSE Error:\n' + JSON.stringify(errorInfo, null, 2)
  256. }));
  257. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  258. streamMessageUpdate(errorMessage, 'content');
  259. completeMessage(MESSAGE_STATUS.ERROR);
  260. sseSourceRef.current = null;
  261. source.close();
  262. });
  263. source.addEventListener('readystatechange', (e) => {
  264. if (e.readyState >= 2 && source.status !== undefined && source.status !== 200) {
  265. const errorInfo = handleApiError(new Error('HTTP状态错误'));
  266. errorInfo.status = source.status;
  267. errorInfo.readyState = source.readyState;
  268. setDebugData(prev => ({
  269. ...prev,
  270. response: responseData + '\n\nHTTP Error:\n' + JSON.stringify(errorInfo, null, 2)
  271. }));
  272. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  273. source.close();
  274. streamMessageUpdate(t('连接已断开'), 'content');
  275. completeMessage(MESSAGE_STATUS.ERROR);
  276. }
  277. });
  278. try {
  279. source.stream();
  280. } catch (error) {
  281. console.error('Failed to start SSE stream:', error);
  282. const errorInfo = handleApiError(error);
  283. setDebugData(prev => ({
  284. ...prev,
  285. response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2)
  286. }));
  287. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  288. streamMessageUpdate(t('建立连接时发生错误'), 'content');
  289. completeMessage(MESSAGE_STATUS.ERROR);
  290. }
  291. }, [setDebugData, setActiveDebugTab, streamMessageUpdate, completeMessage, t, applyAutoCollapseLogic]);
  292. // 停止生成
  293. const onStopGenerator = useCallback(() => {
  294. // 如果仍有活动的 SSE 连接,首先关闭
  295. if (sseSourceRef.current) {
  296. sseSourceRef.current.close();
  297. sseSourceRef.current = null;
  298. }
  299. // 无论是否存在 SSE 连接,都尝试处理最后一条正在生成的消息
  300. setMessage(prevMessage => {
  301. if (prevMessage.length === 0) return prevMessage;
  302. const lastMessage = prevMessage[prevMessage.length - 1];
  303. if (lastMessage.status === MESSAGE_STATUS.LOADING ||
  304. lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
  305. const processed = processIncompleteThinkTags(
  306. lastMessage.content || '',
  307. lastMessage.reasoningContent || ''
  308. );
  309. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  310. const updatedMessages = [
  311. ...prevMessage.slice(0, -1),
  312. {
  313. ...lastMessage,
  314. status: MESSAGE_STATUS.COMPLETE,
  315. reasoningContent: processed.reasoningContent || null,
  316. content: processed.content,
  317. ...autoCollapseState,
  318. }
  319. ];
  320. // 停止生成时也保存,传入更新后的消息列表
  321. setTimeout(() => saveMessages(updatedMessages), 0);
  322. return updatedMessages;
  323. }
  324. return prevMessage;
  325. });
  326. }, [setMessage, applyAutoCollapseLogic, saveMessages]);
  327. // 发送请求
  328. const sendRequest = useCallback((payload, isStream) => {
  329. if (isStream) {
  330. handleSSE(payload);
  331. } else {
  332. handleNonStreamRequest(payload);
  333. }
  334. }, [handleSSE, handleNonStreamRequest]);
  335. return {
  336. sendRequest,
  337. onStopGenerator,
  338. streamMessageUpdate,
  339. completeMessage,
  340. };
  341. };