/** * AI问答API工具函数 */ // AI问答API基础配置 const AI_API_CONFIG = { baseURL: 'http://116.62.47.88:5003', timeout: 30000, // 30秒超时 headers: { 'Content-Type': 'application/json' } }; /** * 发送AI问答请求(真正的流式处理) * @param {string} question - 用户问题 * @param {Function} onChunk - 接收流式数据的回调函数 * @returns {Promise} 返回Promise对象 */ export const askAI = (question, onChunk = null) => { return new Promise((resolve, reject) => { // 参数验证 if (!question || typeof question !== 'string' || !question.trim()) { reject(new Error('问题不能为空')); return; } // 构建请求参数 const requestData = { question: question.trim() }; // 发送请求 uni.request({ url: `${AI_API_CONFIG.baseURL}/ask`, method: 'POST', header: { ...AI_API_CONFIG.headers, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }, data: requestData, timeout: AI_API_CONFIG.timeout, success: (response) => { // 检查响应状态 if (response.statusCode === 200) { // 记录响应数据用于调试 console.log('AI问答API响应数据:', response.data); console.log('响应数据类型:', typeof response.data); // 处理响应数据 let responseText = ''; // 如果是字符串,直接使用 if (typeof response.data === 'string') { responseText = response.data; console.log('响应是字符串,长度:', responseText.length); } else if (response.data && typeof response.data === 'object') { // 如果是对象,尝试提取文本内容 responseText = extractTextFromResponse(response.data); console.log('响应是对象,提取的文本:', responseText); } // 如果有回调函数,实现真正的流式效果 if (onChunk && typeof onChunk === 'function') { console.log('开始真正的流式解析'); // 立即开始解析,每解析到一个JSON对象就立即显示 const fullText = parseAndStreamRealTime(responseText, onChunk); // 返回完整响应 resolve({ success: true, data: fullText, statusCode: response.statusCode }); } else { // 直接解析完整文本 const parsedText = parseConcatenatedJson(responseText); console.log('解析后的文本:', parsedText); resolve({ success: true, data: parsedText, statusCode: response.statusCode }); } } else { // 服务器错误 reject({ success: false, error: `服务器响应错误 (${response.statusCode})`, statusCode: response.statusCode, data: response.data }); } }, fail: (error) => { // 网络错误或其他错误 console.error('AI问答API请求失败:', error); let errorMessage = '网络连接失败'; if (error.errMsg) { if (error.errMsg.includes('timeout')) { errorMessage = '请求超时,请稍后重试'; } else if (error.errMsg.includes('fail')) { errorMessage = '网络连接失败,请检查网络'; } } reject({ success: false, error: errorMessage, originalError: error }); } }); }); }; /** * 通过HTTP chunked实现真正的流式读取(H5下) * - 首选 fetch + ReadableStream * - 回退到 XMLHttpRequest + onprogress * @param {string} question 用户问题 * @param {(chunkText:string, fullText:string)=>void} onChunk 块回调 * @returns {Promise<{success:boolean,data?:string,error?:string}>} */ export const askAIStream = (question, onChunk) => { return new Promise(async (resolve, reject) => { if (!question || typeof question !== 'string' || !question.trim()) { reject(new Error('问题不能为空')); return; } const url = `${AI_API_CONFIG.baseURL}/ask`; const headers = { ...AI_API_CONFIG.headers, 'Accept': 'text/event-stream, text/plain, text/html, application/json;q=0.9, */*;q=0.8', 'Cache-Control': 'no-cache' }; const body = JSON.stringify({ question: question.trim() }); // 块处理:后端按 "\n\n" 分隔的 JSON 块,逐块解析并输出 chatText const emitByDoubleNewline = (state, emit) => { // 兼容 \r\n\r\n const delimiterRe = /\r?\n\r?\n/; const parts = state.buffer.split(delimiterRe); // 最后一段可能是不完整块,先不处理 for (let i = 0; i < parts.length - 1; i++) { const part = (parts[i] || '').trim(); if (!part) continue; try { const obj = JSON.parse(part); let piece = ''; if (obj && obj.response && typeof obj.response.chatText === 'string') { piece = obj.response.chatText; } // 去掉思考标签,前端只显示可见内容 if (piece) { const cleaned = piece.replace(//g, '').replace(/<\/think>/g, ''); emit(cleaned); state.fullText += cleaned; } } catch (e) { // 块级解析失败,忽略该块或原样输出 // 为安全起见这里选择忽略,避免把JSON原文渲染到UI } } // 缓存尾段(不完整) state.buffer = parts[parts.length - 1] || ''; }; // H5 fetch 流式 try { if (typeof window !== 'undefined' && window.fetch && typeof ReadableStream !== 'undefined') { const res = await fetch(url, { method: 'POST', headers, body }); if (!res.ok) { reject({ success: false, error: `服务器响应错误 (${res.status})` }); return; } const reader = res.body && res.body.getReader ? res.body.getReader() : null; if (!reader) { // 无 reader 时,退化为一次性读取 const text = await res.text(); if (onChunk) onChunk(text, text); resolve({ success: true, data: text }); return; } const decoder = new TextDecoder('utf-8'); let done = false; const state = { buffer: '', fullText: '', thinking: false }; while (!done) { const { value, done: readerDone } = await reader.read(); done = readerDone; if (value) { const chunkText = decoder.decode(value, { stream: !done }); state.buffer += chunkText; // 直接在此处解析并调用回调,保证 nowFull 不包含思考段的可见文本 const delimiterRe = /\r?\n\r?\n/; const parts = state.buffer.split(delimiterRe); for (let i = 0; i < parts.length - 1; i++) { const part = (parts[i] || '').trim(); if (!part) continue; try { const obj = JSON.parse(part); let piece = ''; if (obj && obj.response && typeof obj.response.chatText === 'string') { piece = obj.response.chatText; } const hasOpen = typeof piece === 'string' && piece.indexOf('') > -1; const hasClose = typeof piece === 'string' && piece.indexOf('') > -1; const cleaned = (piece || '').replace(//g, '').replace(/<\/think>/g, ''); const isThinkingPart = state.thinking || hasOpen; // 本块属于思考阶段 const addToFull = isThinkingPart ? '' : cleaned; const computedNowFull = state.fullText + addToFull; if (onChunk) onChunk(cleaned, computedNowFull, piece || ''); if (addToFull) state.fullText += addToFull; // 更新思考状态:如果遇到关闭标签则退出思考 if (hasOpen) state.thinking = true; if (hasClose) state.thinking = false; } catch (e) {} } state.buffer = parts[parts.length - 1] || ''; } } // 读完后,尝试解析剩余缓冲 if (state.buffer.trim()) { try { const obj = JSON.parse(state.buffer.trim()); let piece = ''; if (obj && obj.response && typeof obj.response.chatText === 'string') piece = obj.response.chatText; if (piece) { const hasOpen = typeof piece === 'string' && piece.indexOf('') > -1; const hasClose = typeof piece === 'string' && piece.indexOf('') > -1; const cleaned = piece.replace(//g, '').replace(/<\/think>/g, ''); const isThinkingPart = state.thinking || hasOpen; const addToFull = isThinkingPart ? '' : cleaned; onChunk && onChunk(cleaned, state.fullText + addToFull, piece); if (addToFull) state.fullText += addToFull; if (hasOpen) state.thinking = true; if (hasClose) state.thinking = false; } } catch (_) {} } resolve({ success: true, data: state.fullText }); return; } } catch (err) { console.warn('fetch流式失败,尝试XHR回退:', err); } // XHR 回退:利用 onprogress 增量读取 try { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); Object.keys(headers).forEach((k) => xhr.setRequestHeader(k, headers[k])); let lastLen = 0; const state = { buffer: '', fullText: '', thinking: false }; xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { // 完成时解析尾段 if (state.buffer.trim()) { try { const obj = JSON.parse(state.buffer.trim()); let piece = ''; if (obj && obj.response && typeof obj.response.chatText === 'string') piece = obj.response.chatText; if (piece) { const hasOpen = typeof piece === 'string' && piece.indexOf('') > -1; const hasClose = typeof piece === 'string' && piece.indexOf('') > -1; const cleaned = piece.replace(//g, '').replace(/<\/think>/g, ''); const isThinkingPart = state.thinking || hasOpen; const addToFull = isThinkingPart ? '' : cleaned; onChunk && onChunk(cleaned, state.fullText + addToFull, piece); if (addToFull) state.fullText += addToFull; if (hasOpen) state.thinking = true; if (hasClose) state.thinking = false; } } catch (_) {} } resolve({ success: true, data: state.fullText }); } else { reject({ success: false, error: `服务器响应错误 (${xhr.status})` }); } } }; xhr.onprogress = () => { const resp = xhr.responseText || ''; if (resp.length > lastLen) { const delta = resp.substring(lastLen); lastLen = resp.length; state.buffer += delta; // 解析增量并过滤思考段对全文累积的影响 const delimiterRe = /\r?\n\r?\n/; const parts = state.buffer.split(delimiterRe); for (let i = 0; i < parts.length - 1; i++) { const part = (parts[i] || '').trim(); if (!part) continue; try { const obj = JSON.parse(part); let piece = ''; if (obj && obj.response && typeof obj.response.chatText === 'string') { piece = obj.response.chatText; } const hasOpen = typeof piece === 'string' && piece.indexOf('') > -1; const hasClose = typeof piece === 'string' && piece.indexOf('') > -1; const cleaned = (piece || '').replace(//g, '').replace(/<\/think>/g, ''); const isThinkingPart = state.thinking || hasOpen; const addToFull = isThinkingPart ? '' : cleaned; const computedNowFull = state.fullText + addToFull; if (onChunk) onChunk(cleaned, computedNowFull, piece || ''); if (addToFull) state.fullText += addToFull; if (hasOpen) state.thinking = true; if (hasClose) state.thinking = false; } catch (e) {} } state.buffer = parts[parts.length - 1] || ''; } }; xhr.onerror = () => { reject({ success: false, error: '网络连接失败' }); }; xhr.send(body); } catch (err) { reject({ success: false, error: '请求发送失败' }); } }); }; /** * 从响应对象中提取文本内容 * @param {any} responseData - 响应数据 * @returns {string} 提取的文本内容 */ function extractTextFromResponse(responseData) { if (typeof responseData === 'string') { return responseData; } if (responseData && typeof responseData === 'object') { // 尝试提取各种可能的字段 if (responseData.response && responseData.response.chatText) { return responseData.response.chatText; } if (responseData.text) { return responseData.text; } if (responseData.content) { return responseData.content; } if (responseData.message) { return responseData.message; } // 如果是数组,尝试处理第一个元素 if (Array.isArray(responseData) && responseData.length > 0) { return extractTextFromResponse(responseData[0]); } } return ''; } /** * 解析拼接的JSON对象字符串 * @param {string} jsonString - 包含多个JSON对象的字符串 * @returns {string} 提取的完整文本 */ function parseConcatenatedJson(jsonString) { if (!jsonString || typeof jsonString !== 'string') { return ''; } try { // 尝试直接解析为JSON const parsed = JSON.parse(jsonString); if (parsed && parsed.response && parsed.response.chatText) { return parsed.response.chatText; } } catch (e) { // 如果不是单个JSON对象,尝试解析拼接的JSON console.log('尝试解析拼接的JSON对象'); } // 处理拼接的JSON对象 let fullText = ''; let currentIndex = 0; while (currentIndex < jsonString.length) { // 查找下一个JSON对象的开始位置 const startBrace = jsonString.indexOf('{', currentIndex); if (startBrace === -1) break; // 查找对应的结束位置 let braceCount = 0; let endBrace = -1; for (let i = startBrace; i < jsonString.length; i++) { if (jsonString[i] === '{') { braceCount++; } else if (jsonString[i] === '}') { braceCount--; if (braceCount === 0) { endBrace = i; break; } } } if (endBrace === -1) break; // 提取单个JSON对象 const jsonObj = jsonString.substring(startBrace, endBrace + 1); try { const parsed = JSON.parse(jsonObj); if (parsed && parsed.response && parsed.response.chatText) { fullText += parsed.response.chatText; } } catch (e) { console.error('解析JSON对象失败:', e, jsonObj); } // 移动到下一个位置 currentIndex = endBrace + 1; } return fullText; } /** * 实时解析并流式显示 * @param {string} jsonString - 包含多个JSON对象的字符串 * @param {Function} onChunk - 每解析到一个JSON对象就调用此回调 * @returns {string} 提取的完整文本 */ function parseAndStreamRealTime(jsonString, onChunk) { if (!jsonString || typeof jsonString !== 'string') { return ''; } let fullText = ''; let currentIndex = 0; while (currentIndex < jsonString.length) { // 查找下一个JSON对象的开始位置 const startBrace = jsonString.indexOf('{', currentIndex); if (startBrace === -1) break; // 查找对应的结束位置 let braceCount = 0; let endBrace = -1; for (let i = startBrace; i < jsonString.length; i++) { if (jsonString[i] === '{') { braceCount++; } else if (jsonString[i] === '}') { braceCount--; if (braceCount === 0) { endBrace = i; break; } } } if (endBrace === -1) break; // 提取单个JSON对象 const jsonObj = jsonString.substring(startBrace, endBrace + 1); try { const parsed = JSON.parse(jsonObj); if (parsed && parsed.response && parsed.response.chatText) { const chunkText = parsed.response.chatText; fullText += chunkText; // 立即调用回调函数,实现实时显示 console.log('实时解析到文本块:', chunkText, '当前完整文本:', fullText); onChunk(chunkText, fullText); } } catch (e) { console.error('解析JSON对象失败:', e, jsonObj); } // 移动到下一个位置 currentIndex = endBrace + 1; } return fullText; } /** * 格式化AI回复内容 * @param {any} content - AI回复的原始内容 * @returns {string} 格式化后的内容 */ export const formatAIResponse = (content) => { if (!content) { return '抱歉,我暂时无法回答您的问题。'; } // 如果是字符串,直接返回 if (typeof content === 'string') { return content.trim(); } // 如果是对象,尝试提取文本内容 if (typeof content === 'object') { // 处理response对象 - 这是API返回的主要格式 if (content.response && content.response.chatText) { return content.response.chatText.trim(); } // 处理其他可能的字段 if (content.text) { return content.text.trim(); } if (content.content) { return content.content.trim(); } if (content.message) { return content.message.trim(); } // 如果是数组,尝试处理第一个元素 if (Array.isArray(content) && content.length > 0) { return formatAIResponse(content[0]); } } return '抱歉,我暂时无法回答您的问题。'; }; /** * 检查网络连接状态 * @returns {Promise} 返回网络是否可用 */ export const checkNetworkStatus = () => { return new Promise((resolve) => { uni.getNetworkType({ success: (res) => { const isConnected = res.networkType !== 'none'; resolve(isConnected); }, fail: () => { resolve(false); } }); }); }; export default { askAI, formatAIResponse, checkNetworkStatus, askAIStream };