useApiRequest.jsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import { useCallback } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { SSE } from 'sse.js';
  18. import {
  19. API_ENDPOINTS,
  20. MESSAGE_STATUS,
  21. DEBUG_TABS,
  22. } from '../../constants/playground.constants';
  23. import {
  24. getUserIdFromLocalStorage,
  25. handleApiError,
  26. processThinkTags,
  27. processIncompleteThinkTags,
  28. } from '../../helpers';
  29. export const useApiRequest = (
  30. setMessage,
  31. setDebugData,
  32. setActiveDebugTab,
  33. sseSourceRef,
  34. saveMessages,
  35. ) => {
  36. const { t } = useTranslation();
  37. // 处理消息自动关闭逻辑的公共函数
  38. const applyAutoCollapseLogic = useCallback(
  39. (message, isThinkingComplete = true) => {
  40. const shouldAutoCollapse =
  41. isThinkingComplete && !message.hasAutoCollapsed;
  42. return {
  43. isThinkingComplete,
  44. hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
  45. isReasoningExpanded: shouldAutoCollapse
  46. ? false
  47. : message.isReasoningExpanded,
  48. };
  49. },
  50. [],
  51. );
  52. // 流式消息更新
  53. const streamMessageUpdate = useCallback(
  54. (textChunk, type) => {
  55. setMessage((prevMessage) => {
  56. const lastMessage = prevMessage[prevMessage.length - 1];
  57. if (!lastMessage) return prevMessage;
  58. if (lastMessage.role !== 'assistant') return prevMessage;
  59. if (lastMessage.status === MESSAGE_STATUS.ERROR) {
  60. return prevMessage;
  61. }
  62. if (
  63. lastMessage.status === MESSAGE_STATUS.LOADING ||
  64. lastMessage.status === MESSAGE_STATUS.INCOMPLETE
  65. ) {
  66. let newMessage = { ...lastMessage };
  67. if (type === 'reasoning') {
  68. newMessage = {
  69. ...newMessage,
  70. reasoningContent:
  71. (lastMessage.reasoningContent || '') + textChunk,
  72. status: MESSAGE_STATUS.INCOMPLETE,
  73. isThinkingComplete: false,
  74. };
  75. } else if (type === 'content') {
  76. const shouldCollapseReasoning =
  77. !lastMessage.content && lastMessage.reasoningContent;
  78. const newContent = (lastMessage.content || '') + textChunk;
  79. let shouldCollapseFromThinkTag = false;
  80. let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
  81. if (
  82. lastMessage.isReasoningExpanded &&
  83. newContent.includes('</think>')
  84. ) {
  85. const thinkMatches = newContent.match(/<think>/g);
  86. const thinkCloseMatches = newContent.match(/<\/think>/g);
  87. if (
  88. thinkMatches &&
  89. thinkCloseMatches &&
  90. thinkCloseMatches.length >= thinkMatches.length
  91. ) {
  92. shouldCollapseFromThinkTag = true;
  93. thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
  94. }
  95. }
  96. // 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
  97. const isThinkingComplete =
  98. (lastMessage.reasoningContent &&
  99. !lastMessage.isThinkingComplete) ||
  100. thinkingCompleteFromTags;
  101. const autoCollapseState = applyAutoCollapseLogic(
  102. lastMessage,
  103. isThinkingComplete,
  104. );
  105. newMessage = {
  106. ...newMessage,
  107. content: newContent,
  108. status: MESSAGE_STATUS.INCOMPLETE,
  109. ...autoCollapseState,
  110. };
  111. }
  112. return [...prevMessage.slice(0, -1), newMessage];
  113. }
  114. return prevMessage;
  115. });
  116. },
  117. [setMessage, applyAutoCollapseLogic],
  118. );
  119. // 完成消息
  120. const completeMessage = useCallback(
  121. (status = MESSAGE_STATUS.COMPLETE) => {
  122. setMessage((prevMessage) => {
  123. const lastMessage = prevMessage[prevMessage.length - 1];
  124. if (
  125. lastMessage.status === MESSAGE_STATUS.COMPLETE ||
  126. lastMessage.status === MESSAGE_STATUS.ERROR
  127. ) {
  128. return prevMessage;
  129. }
  130. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  131. const updatedMessages = [
  132. ...prevMessage.slice(0, -1),
  133. {
  134. ...lastMessage,
  135. status: status,
  136. ...autoCollapseState,
  137. },
  138. ];
  139. // 在消息完成时保存,传入更新后的消息列表
  140. if (
  141. status === MESSAGE_STATUS.COMPLETE ||
  142. status === MESSAGE_STATUS.ERROR
  143. ) {
  144. setTimeout(() => saveMessages(updatedMessages), 0);
  145. }
  146. return updatedMessages;
  147. });
  148. },
  149. [setMessage, applyAutoCollapseLogic, saveMessages],
  150. );
  151. // 非流式请求
  152. const handleNonStreamRequest = useCallback(
  153. async (payload) => {
  154. setDebugData((prev) => ({
  155. ...prev,
  156. request: payload,
  157. timestamp: new Date().toISOString(),
  158. response: null,
  159. sseMessages: null, // 非流式请求清除 SSE 消息
  160. isStreaming: false,
  161. }));
  162. setActiveDebugTab(DEBUG_TABS.REQUEST);
  163. try {
  164. const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
  165. method: 'POST',
  166. headers: {
  167. 'Content-Type': 'application/json',
  168. 'New-Api-User': getUserIdFromLocalStorage(),
  169. },
  170. body: JSON.stringify(payload),
  171. });
  172. if (!response.ok) {
  173. let errorBody = '';
  174. try {
  175. errorBody = await response.text();
  176. } catch (e) {
  177. errorBody = '无法读取错误响应体';
  178. }
  179. const errorInfo = handleApiError(
  180. new Error(
  181. `HTTP error! status: ${response.status}, body: ${errorBody}`,
  182. ),
  183. response,
  184. );
  185. setDebugData((prev) => ({
  186. ...prev,
  187. response: JSON.stringify(errorInfo, null, 2),
  188. }));
  189. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  190. throw new Error(
  191. `HTTP error! status: ${response.status}, body: ${errorBody}`,
  192. );
  193. }
  194. const data = await response.json();
  195. setDebugData((prev) => ({
  196. ...prev,
  197. response: JSON.stringify(data, null, 2),
  198. }));
  199. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  200. if (data.choices?.[0]) {
  201. const choice = data.choices[0];
  202. let content = choice.message?.content || '';
  203. let reasoningContent = choice.message?.reasoning_content || choice.message?.reasoning || '';
  204. const processed = processThinkTags(content, reasoningContent);
  205. setMessage((prevMessage) => {
  206. const newMessages = [...prevMessage];
  207. const lastMessage = newMessages[newMessages.length - 1];
  208. if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
  209. const autoCollapseState = applyAutoCollapseLogic(
  210. lastMessage,
  211. true,
  212. );
  213. newMessages[newMessages.length - 1] = {
  214. ...lastMessage,
  215. content: processed.content,
  216. reasoningContent: processed.reasoningContent,
  217. status: MESSAGE_STATUS.COMPLETE,
  218. ...autoCollapseState,
  219. };
  220. }
  221. return newMessages;
  222. });
  223. }
  224. } catch (error) {
  225. console.error('Non-stream request error:', error);
  226. const errorInfo = handleApiError(error);
  227. setDebugData((prev) => ({
  228. ...prev,
  229. response: JSON.stringify(errorInfo, null, 2),
  230. }));
  231. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  232. setMessage((prevMessage) => {
  233. const newMessages = [...prevMessage];
  234. const lastMessage = newMessages[newMessages.length - 1];
  235. if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
  236. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  237. newMessages[newMessages.length - 1] = {
  238. ...lastMessage,
  239. content: t('请求发生错误: ') + error.message,
  240. status: MESSAGE_STATUS.ERROR,
  241. ...autoCollapseState,
  242. };
  243. }
  244. return newMessages;
  245. });
  246. }
  247. },
  248. [setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic],
  249. );
  250. // SSE请求
  251. const handleSSE = useCallback(
  252. (payload) => {
  253. setDebugData((prev) => ({
  254. ...prev,
  255. request: payload,
  256. timestamp: new Date().toISOString(),
  257. response: null,
  258. sseMessages: [], // 新增:存储 SSE 消息数组
  259. isStreaming: true, // 新增:标记流式状态
  260. }));
  261. setActiveDebugTab(DEBUG_TABS.REQUEST);
  262. const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
  263. headers: {
  264. 'Content-Type': 'application/json',
  265. 'New-Api-User': getUserIdFromLocalStorage(),
  266. },
  267. method: 'POST',
  268. payload: JSON.stringify(payload),
  269. });
  270. sseSourceRef.current = source;
  271. let responseData = '';
  272. let hasReceivedFirstResponse = false;
  273. let isStreamComplete = false; // 添加标志位跟踪流是否正常完成
  274. source.addEventListener('message', (e) => {
  275. if (e.data === '[DONE]') {
  276. isStreamComplete = true; // 标记流正常完成
  277. source.close();
  278. sseSourceRef.current = null;
  279. setDebugData((prev) => ({
  280. ...prev,
  281. response: responseData,
  282. sseMessages: [...(prev.sseMessages || []), '[DONE]'], // 添加 DONE 标记
  283. isStreaming: false,
  284. }));
  285. completeMessage();
  286. return;
  287. }
  288. try {
  289. const payload = JSON.parse(e.data);
  290. responseData += e.data + '\n';
  291. if (!hasReceivedFirstResponse) {
  292. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  293. hasReceivedFirstResponse = true;
  294. }
  295. // 新增:将 SSE 消息添加到数组
  296. setDebugData((prev) => ({
  297. ...prev,
  298. sseMessages: [...(prev.sseMessages || []), e.data],
  299. }));
  300. const delta = payload.choices?.[0]?.delta;
  301. if (delta) {
  302. if (delta.reasoning_content) {
  303. streamMessageUpdate(delta.reasoning_content, 'reasoning');
  304. }
  305. if (delta.reasoning) {
  306. streamMessageUpdate(delta.reasoning, 'reasoning');
  307. }
  308. if (delta.content) {
  309. streamMessageUpdate(delta.content, 'content');
  310. }
  311. }
  312. } catch (error) {
  313. console.error('Failed to parse SSE message:', error);
  314. const errorInfo = `解析错误: ${error.message}`;
  315. setDebugData((prev) => ({
  316. ...prev,
  317. response: responseData + `\n\nError: ${errorInfo}`,
  318. sseMessages: [...(prev.sseMessages || []), e.data], // 即使解析失败也保存原始数据
  319. isStreaming: false,
  320. }));
  321. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  322. streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
  323. completeMessage(MESSAGE_STATUS.ERROR);
  324. }
  325. });
  326. source.addEventListener('error', (e) => {
  327. // 只有在流没有正常完成且连接状态异常时才处理错误
  328. if (!isStreamComplete && source.readyState !== 2) {
  329. console.error('SSE Error:', e);
  330. const errorMessage = e.data || t('请求发生错误');
  331. const errorInfo = handleApiError(new Error(errorMessage));
  332. errorInfo.readyState = source.readyState;
  333. setDebugData((prev) => ({
  334. ...prev,
  335. response:
  336. responseData +
  337. '\n\nSSE Error:\n' +
  338. JSON.stringify(errorInfo, null, 2),
  339. }));
  340. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  341. streamMessageUpdate(errorMessage, 'content');
  342. completeMessage(MESSAGE_STATUS.ERROR);
  343. sseSourceRef.current = null;
  344. source.close();
  345. }
  346. });
  347. source.addEventListener('readystatechange', (e) => {
  348. // 检查 HTTP 状态错误,但避免与正常关闭重复处理
  349. if (
  350. e.readyState >= 2 &&
  351. source.status !== undefined &&
  352. source.status !== 200 &&
  353. !isStreamComplete
  354. ) {
  355. const errorInfo = handleApiError(new Error('HTTP状态错误'));
  356. errorInfo.status = source.status;
  357. errorInfo.readyState = source.readyState;
  358. setDebugData((prev) => ({
  359. ...prev,
  360. response:
  361. responseData +
  362. '\n\nHTTP Error:\n' +
  363. JSON.stringify(errorInfo, null, 2),
  364. }));
  365. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  366. source.close();
  367. streamMessageUpdate(t('连接已断开'), 'content');
  368. completeMessage(MESSAGE_STATUS.ERROR);
  369. }
  370. });
  371. try {
  372. source.stream();
  373. } catch (error) {
  374. console.error('Failed to start SSE stream:', error);
  375. const errorInfo = handleApiError(error);
  376. setDebugData((prev) => ({
  377. ...prev,
  378. response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2),
  379. }));
  380. setActiveDebugTab(DEBUG_TABS.RESPONSE);
  381. streamMessageUpdate(t('建立连接时发生错误'), 'content');
  382. completeMessage(MESSAGE_STATUS.ERROR);
  383. }
  384. },
  385. [
  386. setDebugData,
  387. setActiveDebugTab,
  388. streamMessageUpdate,
  389. completeMessage,
  390. t,
  391. applyAutoCollapseLogic,
  392. ],
  393. );
  394. // 停止生成
  395. const onStopGenerator = useCallback(() => {
  396. // 如果仍有活动的 SSE 连接,首先关闭
  397. if (sseSourceRef.current) {
  398. sseSourceRef.current.close();
  399. sseSourceRef.current = null;
  400. }
  401. // 无论是否存在 SSE 连接,都尝试处理最后一条正在生成的消息
  402. setMessage((prevMessage) => {
  403. if (prevMessage.length === 0) return prevMessage;
  404. const lastMessage = prevMessage[prevMessage.length - 1];
  405. if (
  406. lastMessage.status === MESSAGE_STATUS.LOADING ||
  407. lastMessage.status === MESSAGE_STATUS.INCOMPLETE
  408. ) {
  409. const processed = processIncompleteThinkTags(
  410. lastMessage.content || '',
  411. lastMessage.reasoningContent || '',
  412. );
  413. const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
  414. const updatedMessages = [
  415. ...prevMessage.slice(0, -1),
  416. {
  417. ...lastMessage,
  418. status: MESSAGE_STATUS.COMPLETE,
  419. reasoningContent: processed.reasoningContent || null,
  420. content: processed.content,
  421. ...autoCollapseState,
  422. },
  423. ];
  424. // 停止生成时也保存,传入更新后的消息列表
  425. setTimeout(() => saveMessages(updatedMessages), 0);
  426. return updatedMessages;
  427. }
  428. return prevMessage;
  429. });
  430. }, [setMessage, applyAutoCollapseLogic, saveMessages]);
  431. // 发送请求
  432. const sendRequest = useCallback(
  433. (payload, isStream) => {
  434. if (isStream) {
  435. handleSSE(payload);
  436. } else {
  437. handleNonStreamRequest(payload);
  438. }
  439. },
  440. [handleSSE, handleNonStreamRequest],
  441. );
  442. return {
  443. sendRequest,
  444. onStopGenerator,
  445. streamMessageUpdate,
  446. completeMessage,
  447. };
  448. };