ai-api.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /**
  2. * AI问答API工具函数
  3. */
  4. // AI问答API基础配置
  5. const AI_API_CONFIG = {
  6. baseURL: 'http://116.62.47.88:5003',
  7. timeout: 30000, // 30秒超时
  8. headers: {
  9. 'Content-Type': 'application/json'
  10. }
  11. };
  12. /**
  13. * 发送AI问答请求(真正的流式处理)
  14. * @param {string} question - 用户问题
  15. * @param {Function} onChunk - 接收流式数据的回调函数
  16. * @returns {Promise} 返回Promise对象
  17. */
  18. export const askAI = (question, onChunk = null) => {
  19. return new Promise((resolve, reject) => {
  20. // 参数验证
  21. if (!question || typeof question !== 'string' || !question.trim()) {
  22. reject(new Error('问题不能为空'));
  23. return;
  24. }
  25. // 构建请求参数
  26. const requestData = {
  27. question: question.trim()
  28. };
  29. // 发送请求
  30. uni.request({
  31. url: `${AI_API_CONFIG.baseURL}/ask`,
  32. method: 'POST',
  33. header: {
  34. ...AI_API_CONFIG.headers,
  35. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
  36. },
  37. data: requestData,
  38. timeout: AI_API_CONFIG.timeout,
  39. success: (response) => {
  40. // 检查响应状态
  41. if (response.statusCode === 200) {
  42. // 记录响应数据用于调试
  43. console.log('AI问答API响应数据:', response.data);
  44. console.log('响应数据类型:', typeof response.data);
  45. // 处理响应数据
  46. let responseText = '';
  47. // 如果是字符串,直接使用
  48. if (typeof response.data === 'string') {
  49. responseText = response.data;
  50. console.log('响应是字符串,长度:', responseText.length);
  51. } else if (response.data && typeof response.data === 'object') {
  52. // 如果是对象,尝试提取文本内容
  53. responseText = extractTextFromResponse(response.data);
  54. console.log('响应是对象,提取的文本:', responseText);
  55. }
  56. // 如果有回调函数,实现真正的流式效果
  57. if (onChunk && typeof onChunk === 'function') {
  58. console.log('开始真正的流式解析');
  59. // 立即开始解析,每解析到一个JSON对象就立即显示
  60. const fullText = parseAndStreamRealTime(responseText, onChunk);
  61. // 返回完整响应
  62. resolve({
  63. success: true,
  64. data: fullText,
  65. statusCode: response.statusCode
  66. });
  67. } else {
  68. // 直接解析完整文本
  69. const parsedText = parseConcatenatedJson(responseText);
  70. console.log('解析后的文本:', parsedText);
  71. resolve({
  72. success: true,
  73. data: parsedText,
  74. statusCode: response.statusCode
  75. });
  76. }
  77. } else {
  78. // 服务器错误
  79. reject({
  80. success: false,
  81. error: `服务器响应错误 (${response.statusCode})`,
  82. statusCode: response.statusCode,
  83. data: response.data
  84. });
  85. }
  86. },
  87. fail: (error) => {
  88. // 网络错误或其他错误
  89. console.error('AI问答API请求失败:', error);
  90. let errorMessage = '网络连接失败';
  91. if (error.errMsg) {
  92. if (error.errMsg.includes('timeout')) {
  93. errorMessage = '请求超时,请稍后重试';
  94. } else if (error.errMsg.includes('fail')) {
  95. errorMessage = '网络连接失败,请检查网络';
  96. }
  97. }
  98. reject({
  99. success: false,
  100. error: errorMessage,
  101. originalError: error
  102. });
  103. }
  104. });
  105. });
  106. };
  107. /**
  108. * 通过HTTP chunked实现真正的流式读取(H5下)
  109. * - 首选 fetch + ReadableStream
  110. * - 回退到 XMLHttpRequest + onprogress
  111. * @param {string} question 用户问题
  112. * @param {(chunkText:string, fullText:string)=>void} onChunk 块回调
  113. * @returns {Promise<{success:boolean,data?:string,error?:string}>}
  114. */
  115. export const askAIStream = (question, onChunk) => {
  116. return new Promise(async (resolve, reject) => {
  117. if (!question || typeof question !== 'string' || !question.trim()) {
  118. reject(new Error('问题不能为空'));
  119. return;
  120. }
  121. const url = `${AI_API_CONFIG.baseURL}/ask`;
  122. const headers = {
  123. ...AI_API_CONFIG.headers,
  124. 'Accept': 'text/event-stream, text/plain, text/html, application/json;q=0.9, */*;q=0.8',
  125. 'Cache-Control': 'no-cache'
  126. };
  127. const body = JSON.stringify({ question: question.trim() });
  128. // 块处理:后端按 "\n\n" 分隔的 JSON 块,逐块解析并输出 chatText
  129. const emitByDoubleNewline = (state, emit) => {
  130. // 兼容 \r\n\r\n
  131. const delimiterRe = /\r?\n\r?\n/;
  132. const parts = state.buffer.split(delimiterRe);
  133. // 最后一段可能是不完整块,先不处理
  134. for (let i = 0; i < parts.length - 1; i++) {
  135. const part = (parts[i] || '').trim();
  136. if (!part) continue;
  137. try {
  138. const obj = JSON.parse(part);
  139. let piece = '';
  140. if (obj && obj.response && typeof obj.response.chatText === 'string') {
  141. piece = obj.response.chatText;
  142. }
  143. // 去掉思考标签,前端只显示可见内容
  144. if (piece) {
  145. const cleaned = piece.replace(/<think>/g, '').replace(/<\/think>/g, '');
  146. emit(cleaned);
  147. state.fullText += cleaned;
  148. }
  149. } catch (e) {
  150. // 块级解析失败,忽略该块或原样输出
  151. // 为安全起见这里选择忽略,避免把JSON原文渲染到UI
  152. }
  153. }
  154. // 缓存尾段(不完整)
  155. state.buffer = parts[parts.length - 1] || '';
  156. };
  157. // H5 fetch 流式
  158. try {
  159. if (typeof window !== 'undefined' && window.fetch && typeof ReadableStream !== 'undefined') {
  160. const res = await fetch(url, { method: 'POST', headers, body });
  161. if (!res.ok) {
  162. reject({ success: false, error: `服务器响应错误 (${res.status})` });
  163. return;
  164. }
  165. const reader = res.body && res.body.getReader ? res.body.getReader() : null;
  166. if (!reader) {
  167. // 无 reader 时,退化为一次性读取
  168. const text = await res.text();
  169. if (onChunk) onChunk(text, text);
  170. resolve({ success: true, data: text });
  171. return;
  172. }
  173. const decoder = new TextDecoder('utf-8');
  174. let done = false;
  175. const state = { buffer: '', fullText: '', thinking: false };
  176. while (!done) {
  177. const { value, done: readerDone } = await reader.read();
  178. done = readerDone;
  179. if (value) {
  180. const chunkText = decoder.decode(value, { stream: !done });
  181. state.buffer += chunkText;
  182. // 直接在此处解析并调用回调,保证 nowFull 不包含思考段的可见文本
  183. const delimiterRe = /\r?\n\r?\n/;
  184. const parts = state.buffer.split(delimiterRe);
  185. for (let i = 0; i < parts.length - 1; i++) {
  186. const part = (parts[i] || '').trim();
  187. if (!part) continue;
  188. try {
  189. const obj = JSON.parse(part);
  190. let piece = '';
  191. if (obj && obj.response && typeof obj.response.chatText === 'string') {
  192. piece = obj.response.chatText;
  193. }
  194. const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
  195. const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
  196. const cleaned = (piece || '').replace(/<think>/g, '').replace(/<\/think>/g, '');
  197. const isThinkingPart = state.thinking || hasOpen; // 本块属于思考阶段
  198. const addToFull = isThinkingPart ? '' : cleaned;
  199. const computedNowFull = state.fullText + addToFull;
  200. if (onChunk) onChunk(cleaned, computedNowFull, piece || '');
  201. if (addToFull) state.fullText += addToFull;
  202. // 更新思考状态:如果遇到关闭标签则退出思考
  203. if (hasOpen) state.thinking = true;
  204. if (hasClose) state.thinking = false;
  205. } catch (e) {}
  206. }
  207. state.buffer = parts[parts.length - 1] || '';
  208. }
  209. }
  210. // 读完后,尝试解析剩余缓冲
  211. if (state.buffer.trim()) {
  212. try {
  213. const obj = JSON.parse(state.buffer.trim());
  214. let piece = '';
  215. if (obj && obj.response && typeof obj.response.chatText === 'string') piece = obj.response.chatText;
  216. if (piece) {
  217. const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
  218. const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
  219. const cleaned = piece.replace(/<think>/g, '').replace(/<\/think>/g, '');
  220. const isThinkingPart = state.thinking || hasOpen;
  221. const addToFull = isThinkingPart ? '' : cleaned;
  222. onChunk && onChunk(cleaned, state.fullText + addToFull, piece);
  223. if (addToFull) state.fullText += addToFull;
  224. if (hasOpen) state.thinking = true;
  225. if (hasClose) state.thinking = false;
  226. }
  227. } catch (_) {}
  228. }
  229. resolve({ success: true, data: state.fullText });
  230. return;
  231. }
  232. } catch (err) {
  233. console.warn('fetch流式失败,尝试XHR回退:', err);
  234. }
  235. // XHR 回退:利用 onprogress 增量读取
  236. try {
  237. const xhr = new XMLHttpRequest();
  238. xhr.open('POST', url, true);
  239. Object.keys(headers).forEach((k) => xhr.setRequestHeader(k, headers[k]));
  240. let lastLen = 0;
  241. const state = { buffer: '', fullText: '', thinking: false };
  242. xhr.onreadystatechange = () => {
  243. if (xhr.readyState === 4) {
  244. if (xhr.status >= 200 && xhr.status < 300) {
  245. // 完成时解析尾段
  246. if (state.buffer.trim()) {
  247. try {
  248. const obj = JSON.parse(state.buffer.trim());
  249. let piece = '';
  250. if (obj && obj.response && typeof obj.response.chatText === 'string') piece = obj.response.chatText;
  251. if (piece) {
  252. const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
  253. const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
  254. const cleaned = piece.replace(/<think>/g, '').replace(/<\/think>/g, '');
  255. const isThinkingPart = state.thinking || hasOpen;
  256. const addToFull = isThinkingPart ? '' : cleaned;
  257. onChunk && onChunk(cleaned, state.fullText + addToFull, piece);
  258. if (addToFull) state.fullText += addToFull;
  259. if (hasOpen) state.thinking = true;
  260. if (hasClose) state.thinking = false;
  261. }
  262. } catch (_) {}
  263. }
  264. resolve({ success: true, data: state.fullText });
  265. } else {
  266. reject({ success: false, error: `服务器响应错误 (${xhr.status})` });
  267. }
  268. }
  269. };
  270. xhr.onprogress = () => {
  271. const resp = xhr.responseText || '';
  272. if (resp.length > lastLen) {
  273. const delta = resp.substring(lastLen);
  274. lastLen = resp.length;
  275. state.buffer += delta;
  276. // 解析增量并过滤思考段对全文累积的影响
  277. const delimiterRe = /\r?\n\r?\n/;
  278. const parts = state.buffer.split(delimiterRe);
  279. for (let i = 0; i < parts.length - 1; i++) {
  280. const part = (parts[i] || '').trim();
  281. if (!part) continue;
  282. try {
  283. const obj = JSON.parse(part);
  284. let piece = '';
  285. if (obj && obj.response && typeof obj.response.chatText === 'string') {
  286. piece = obj.response.chatText;
  287. }
  288. const hasOpen = typeof piece === 'string' && piece.indexOf('<think>') > -1;
  289. const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
  290. const cleaned = (piece || '').replace(/<think>/g, '').replace(/<\/think>/g, '');
  291. const isThinkingPart = state.thinking || hasOpen;
  292. const addToFull = isThinkingPart ? '' : cleaned;
  293. const computedNowFull = state.fullText + addToFull;
  294. if (onChunk) onChunk(cleaned, computedNowFull, piece || '');
  295. if (addToFull) state.fullText += addToFull;
  296. if (hasOpen) state.thinking = true;
  297. if (hasClose) state.thinking = false;
  298. } catch (e) {}
  299. }
  300. state.buffer = parts[parts.length - 1] || '';
  301. }
  302. };
  303. xhr.onerror = () => {
  304. reject({ success: false, error: '网络连接失败' });
  305. };
  306. xhr.send(body);
  307. } catch (err) {
  308. reject({ success: false, error: '请求发送失败' });
  309. }
  310. });
  311. };
  312. /**
  313. * 从响应对象中提取文本内容
  314. * @param {any} responseData - 响应数据
  315. * @returns {string} 提取的文本内容
  316. */
  317. function extractTextFromResponse(responseData) {
  318. if (typeof responseData === 'string') {
  319. return responseData;
  320. }
  321. if (responseData && typeof responseData === 'object') {
  322. // 尝试提取各种可能的字段
  323. if (responseData.response && responseData.response.chatText) {
  324. return responseData.response.chatText;
  325. }
  326. if (responseData.text) {
  327. return responseData.text;
  328. }
  329. if (responseData.content) {
  330. return responseData.content;
  331. }
  332. if (responseData.message) {
  333. return responseData.message;
  334. }
  335. // 如果是数组,尝试处理第一个元素
  336. if (Array.isArray(responseData) && responseData.length > 0) {
  337. return extractTextFromResponse(responseData[0]);
  338. }
  339. }
  340. return '';
  341. }
  342. /**
  343. * 解析拼接的JSON对象字符串
  344. * @param {string} jsonString - 包含多个JSON对象的字符串
  345. * @returns {string} 提取的完整文本
  346. */
  347. function parseConcatenatedJson(jsonString) {
  348. if (!jsonString || typeof jsonString !== 'string') {
  349. return '';
  350. }
  351. try {
  352. // 尝试直接解析为JSON
  353. const parsed = JSON.parse(jsonString);
  354. if (parsed && parsed.response && parsed.response.chatText) {
  355. return parsed.response.chatText;
  356. }
  357. } catch (e) {
  358. // 如果不是单个JSON对象,尝试解析拼接的JSON
  359. console.log('尝试解析拼接的JSON对象');
  360. }
  361. // 处理拼接的JSON对象
  362. let fullText = '';
  363. let currentIndex = 0;
  364. while (currentIndex < jsonString.length) {
  365. // 查找下一个JSON对象的开始位置
  366. const startBrace = jsonString.indexOf('{', currentIndex);
  367. if (startBrace === -1) break;
  368. // 查找对应的结束位置
  369. let braceCount = 0;
  370. let endBrace = -1;
  371. for (let i = startBrace; i < jsonString.length; i++) {
  372. if (jsonString[i] === '{') {
  373. braceCount++;
  374. } else if (jsonString[i] === '}') {
  375. braceCount--;
  376. if (braceCount === 0) {
  377. endBrace = i;
  378. break;
  379. }
  380. }
  381. }
  382. if (endBrace === -1) break;
  383. // 提取单个JSON对象
  384. const jsonObj = jsonString.substring(startBrace, endBrace + 1);
  385. try {
  386. const parsed = JSON.parse(jsonObj);
  387. if (parsed && parsed.response && parsed.response.chatText) {
  388. fullText += parsed.response.chatText;
  389. }
  390. } catch (e) {
  391. console.error('解析JSON对象失败:', e, jsonObj);
  392. }
  393. // 移动到下一个位置
  394. currentIndex = endBrace + 1;
  395. }
  396. return fullText;
  397. }
  398. /**
  399. * 实时解析并流式显示
  400. * @param {string} jsonString - 包含多个JSON对象的字符串
  401. * @param {Function} onChunk - 每解析到一个JSON对象就调用此回调
  402. * @returns {string} 提取的完整文本
  403. */
  404. function parseAndStreamRealTime(jsonString, onChunk) {
  405. if (!jsonString || typeof jsonString !== 'string') {
  406. return '';
  407. }
  408. let fullText = '';
  409. let currentIndex = 0;
  410. while (currentIndex < jsonString.length) {
  411. // 查找下一个JSON对象的开始位置
  412. const startBrace = jsonString.indexOf('{', currentIndex);
  413. if (startBrace === -1) break;
  414. // 查找对应的结束位置
  415. let braceCount = 0;
  416. let endBrace = -1;
  417. for (let i = startBrace; i < jsonString.length; i++) {
  418. if (jsonString[i] === '{') {
  419. braceCount++;
  420. } else if (jsonString[i] === '}') {
  421. braceCount--;
  422. if (braceCount === 0) {
  423. endBrace = i;
  424. break;
  425. }
  426. }
  427. }
  428. if (endBrace === -1) break;
  429. // 提取单个JSON对象
  430. const jsonObj = jsonString.substring(startBrace, endBrace + 1);
  431. try {
  432. const parsed = JSON.parse(jsonObj);
  433. if (parsed && parsed.response && parsed.response.chatText) {
  434. const chunkText = parsed.response.chatText;
  435. fullText += chunkText;
  436. // 立即调用回调函数,实现实时显示
  437. console.log('实时解析到文本块:', chunkText, '当前完整文本:', fullText);
  438. onChunk(chunkText, fullText);
  439. }
  440. } catch (e) {
  441. console.error('解析JSON对象失败:', e, jsonObj);
  442. }
  443. // 移动到下一个位置
  444. currentIndex = endBrace + 1;
  445. }
  446. return fullText;
  447. }
  448. /**
  449. * 格式化AI回复内容
  450. * @param {any} content - AI回复的原始内容
  451. * @returns {string} 格式化后的内容
  452. */
  453. export const formatAIResponse = (content) => {
  454. if (!content) {
  455. return '抱歉,我暂时无法回答您的问题。';
  456. }
  457. // 如果是字符串,直接返回
  458. if (typeof content === 'string') {
  459. return content.trim();
  460. }
  461. // 如果是对象,尝试提取文本内容
  462. if (typeof content === 'object') {
  463. // 处理response对象 - 这是API返回的主要格式
  464. if (content.response && content.response.chatText) {
  465. return content.response.chatText.trim();
  466. }
  467. // 处理其他可能的字段
  468. if (content.text) {
  469. return content.text.trim();
  470. }
  471. if (content.content) {
  472. return content.content.trim();
  473. }
  474. if (content.message) {
  475. return content.message.trim();
  476. }
  477. // 如果是数组,尝试处理第一个元素
  478. if (Array.isArray(content) && content.length > 0) {
  479. return formatAIResponse(content[0]);
  480. }
  481. }
  482. return '抱歉,我暂时无法回答您的问题。';
  483. };
  484. /**
  485. * 检查网络连接状态
  486. * @returns {Promise<boolean>} 返回网络是否可用
  487. */
  488. export const checkNetworkStatus = () => {
  489. return new Promise((resolve) => {
  490. uni.getNetworkType({
  491. success: (res) => {
  492. const isConnected = res.networkType !== 'none';
  493. resolve(isConnected);
  494. },
  495. fail: () => {
  496. resolve(false);
  497. }
  498. });
  499. });
  500. };
  501. export default {
  502. askAI,
  503. formatAIResponse,
  504. checkNetworkStatus,
  505. askAIStream
  506. };