useApiRequest.js 13 KB

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