| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- /**
- * 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(/<think>/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('<think>') > -1;
- const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
- const cleaned = (piece || '').replace(/<think>/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('<think>') > -1;
- const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
- const cleaned = piece.replace(/<think>/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('<think>') > -1;
- const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
- const cleaned = piece.replace(/<think>/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('<think>') > -1;
- const hasClose = typeof piece === 'string' && piece.indexOf('</think>') > -1;
- const cleaned = (piece || '').replace(/<think>/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<boolean>} 返回网络是否可用
- */
- 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
- };
|