From e16402c9c116b7d482433b477d0cbe1f1ae6c0f6 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 11 May 2026 17:14:26 +0800 Subject: [PATCH 01/13] =?UTF-8?q?fix(ai):=20=E4=BF=AE=E5=A4=8DWebSocket?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=9B=9E=E8=B0=83=E5=8F=8A=E6=84=8F=E5=A4=96?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/js/ai-service.js | 60 +++++++++++++++++++++----- pages_tool/ai-chat/ai-chat-message.vue | 8 +++- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/common/js/ai-service.js b/common/js/ai-service.js index 9bda135..1b3a278 100644 --- a/common/js/ai-service.js +++ b/common/js/ai-service.js @@ -88,6 +88,16 @@ export default { return new Promise((resolve, reject) => { const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:]+)/); const domain = match ? match[1] : ''; + + // 修复 5:Domain 提取失败时提前拒绝 + if (!domain) { + const errorMsg = 'WebSocket 域名配置无效'; + console.error(errorMsg); + reject(new Error(errorMsg)); + if (onComplete) onComplete({ error: errorMsg }); + return; + } + const wssUrl = `wss://${domain}/ws/aikefu`; const socketTask = wx.connectSocket({ url: wssUrl, @@ -97,6 +107,29 @@ export default { let content = ''; let conversationId = ''; let isAuthenticated = false; + let closedNormally = false; + let settled = false; + + // 安全地拒绝(防止重复调用) + function safeReject(error) { + if (settled) return; + settled = true; + clearTimeout(timeout); + reject(error); + } + + // 安全地完成 + function safeResolve(result) { + if (settled) return; + settled = true; + clearTimeout(timeout); + resolve(result); + } + + // 安全地触发 onComplete + function safeComplete(result) { + if (onComplete) onComplete(result); + } socketTask.onOpen(() => { socketTask.send({ @@ -128,8 +161,8 @@ export default { } else if (data.type === 'auth_failed') { const errorMsg = '认证失败,请重新登录'; console.error(errorMsg, data); - reject(new Error(errorMsg)); - if (onComplete) onComplete({ error: errorMsg }); + safeComplete({ error: errorMsg }); + safeReject(new Error(errorMsg)); socketTask.close(); } @@ -146,10 +179,9 @@ export default { if (conversationId) { this.setConversationId(conversationId); } - if (onComplete) { - onComplete({ content, conversation_id: conversationId }); - } - resolve({ content, conversation_id: conversationId }); + closedNormally = true; + safeComplete({ content, conversation_id: conversationId }); + safeResolve({ content, conversation_id: conversationId }); socketTask.close(); } @@ -165,21 +197,27 @@ export default { socketTask.onError((err) => { const errorMsg = 'WebSocket 连接失败'; console.error(errorMsg, err); - reject(new Error(errorMsg)); - if (onComplete) onComplete({ error: errorMsg }); + safeComplete({ error: errorMsg }); + safeReject(new Error(errorMsg)); }); socketTask.onClose(() => { + if (!closedNormally && !settled) { + const errorMsg = 'WebSocket 连接意外关闭'; + console.error(errorMsg); + safeComplete({ error: errorMsg }); + safeReject(new Error(errorMsg)); + } }); const timeout = setTimeout(() => { if (!isAuthenticated || content === '') { console.warn('WebSocket 超时,强制关闭'); socketTask.close(); - reject(new Error('AI服务响应超时')); - if (onComplete) onComplete({ error: 'AI服务响应超时' }); + safeComplete({ error: 'AI服务响应超时' }); + safeReject(new Error('AI服务响应超时')); } - }, 10000); + }, 20000); }); // #endif }, diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue index ac1e8df..2eddd4d 100644 --- a/pages_tool/ai-chat/ai-chat-message.vue +++ b/pages_tool/ai-chat/ai-chat-message.vue @@ -1010,8 +1010,10 @@ async loadChatHistoryIfExist() { await aiService.sendHttpStream( userMessage, (chunk) => { - // 实时更新内容(不频繁滚动) + // 实时更新内容 streamMessage.content += chunk; + // 流式更新时滚动到底部显示最新内容(防抖) + this.scrollToBottom(); }, (completeResult) => { // 流结束回调 @@ -1054,8 +1056,10 @@ async loadChatHistoryIfExist() { await aiService.sendStreamMessage( userMessage, (chunk) => { - // 实时更新内容(不频繁滚动) + // 实时更新内容 streamMessage.content += chunk; + // 流式更新时滚动到底部显示最新内容(防抖) + this.scrollToBottom(); }, (completeResult) => { // 流结束回调 From 875cf0b42803bc0fa9381b80a4a4cf48100a83af Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 12 May 2026 08:30:26 +0800 Subject: [PATCH 02/13] =?UTF-8?q?feat(ai-chat):=20=E5=A2=9E=E5=BC=BAMarkdo?= =?UTF-8?q?wn=E8=A7=A3=E6=9E=90=E5=8F=8A=E8=B0=83=E6=95=B4=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=B3=A1=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .local.config.js | 2 +- pages_tool/ai-chat/ai-chat-message.vue | 198 +++++++++++++++++++++++-- 2 files changed, 184 insertions(+), 16 deletions(-) diff --git a/.local.config.js b/.local.config.js index 1902e69..4d86284 100644 --- a/.local.config.js +++ b/.local.config.js @@ -62,5 +62,5 @@ const localDevConfig = ({ uniacid: 2075, domain: 'https://xcx.aigc-quickapp.com/', }, -})['2811']; // 选择要使用的环境配置 +})['1']; // 选择要使用的环境配置 export default localDevConfig; \ No newline at end of file diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue index 2eddd4d..60ce73a 100644 --- a/pages_tool/ai-chat/ai-chat-message.vue +++ b/pages_tool/ai-chat/ai-chat-message.vue @@ -1126,20 +1126,190 @@ async loadChatHistoryIfExist() { } return `感谢您的咨询!关于"${userMessage}"的问题,我们的专业客服会尽快为您解答。您也可以查看我们的帮助文档获取更多信息。`; }, - - - // 解析Markdown + + + // 增强版Markdown解析(无第三方依赖) parseMarkdown(content) { if (!content) return '' - - // 简单的Markdown解析 + + // 辅助:内联格式处理 + const parseInline = (text) => { + return text + // 行内图片 + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + // 链接 + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + // 行内代码 + .replace(/`([^`]+)`/g, '$1') + // 粗体(优先匹配**) + .replace(/\*\*(.+?)\*\*/g, '$1') + // 斜体 + .replace(/\*(.+?)\*/g, '$1') + // 删除线 + .replace(/~~(.+?)~~/g, '$1') + // 手机号 + .replace(/\b(1[3-9]\d{9})\b/g, '$1') + // 400电话 + .replace(/\b(400[\d-]{7,10})\b/g, '$1') + } + + // 1. 转义HTML特殊字符 let html = content - .replace(/\*\*(.*?)\*\*/g, '$1') // 粗体 - .replace(/\*(.*?)\*/g, '$1') // 斜体 - .replace(/`(.*?)`/g, '$1') // 代码 - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // 链接 - .replace(/\n/g, '
') // 换行 - + .replace(/&/g, '&') + .replace(//g, '>') + + // 2. 提取代码块(保护其内容不被后续处理污染) + const codeBlocks = [] + html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (m, lang, code) => { + const idx = codeBlocks.length + const safe = code.replace(//g, '>') + codeBlocks.push( + `
` +
+          `${safe}
` + ) + return `%%CODEBLOCK_${idx}%%` + }) + + // 3. 按行处理,用
连接单换行,双换行分割段落块 + const lines = html.split('\n') + const out = [] + let inList = false + let buf = [] // 普通文本行缓冲区 + let inTable = false + let tableRows = [] // 表格行缓冲区 + + const closeList = () => { + if (inList === 'ul') { inList = false; return '' } + if (inList === 'ol') { inList = false; return '' } + return '' + } + const flushBuf = () => { + if (buf.length === 0) return + // 去掉首尾的空串,避免产生尾随
+ while (buf.length > 0 && buf[0] === '') buf.shift() + while (buf.length > 0 && buf[buf.length - 1] === '') buf.pop() + if (buf.length === 0) return + const content = buf.map(l => parseInline(l)).join('
') + out.push(`
${content}
`) + buf = [] + } + const flushTable = () => { + if (!inTable || tableRows.length === 0) return + const thead = tableRows[0].map(c => + `${parseInline(c)}` + ).join('') + const tbody = tableRows.slice(1).map(row => + '' + row.map(c => + `${parseInline(c)}` + ).join('') + '' + ).join('') + out.push( + `` + + `${thead}${tbody}
` + ) + tableRows = [] + inTable = false + } + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const trimmed = line.trim() + + // 代码块占位符 + if (/^%%CODEBLOCK_\d+%%$/.test(trimmed)) { + flushBuf(); flushTable(); out.push(closeList()) + out.push(line) + continue + } + + // 表格行(以 | 开头) + if (trimmed.charAt(0) === '|') { + flushBuf(); out.push(closeList()) + // 去除首尾空 cell + const rawCells = trimmed.split('|').filter((c, j, a) => j > 0 && j < a.length - 1) + if (rawCells.length === 0) continue + // 跳过分隔行 | --- | :--- | :---: | ---: | + const isSep = rawCells.every(c => /^[\s:\-|]+$/.test(c.trim())) + if (isSep) continue + if (!inTable) { inTable = true; tableRows = [] } + tableRows.push(rawCells.map(c => c.trim())) + continue + } + // 非表格行 → 刷新已积累的表格 + if (inTable) flushTable() + + // 标题 + const hMatch = line.match(/^(#{1,6})\s+(.+)$/) + if (hMatch) { + flushBuf(); out.push(closeList()) + const lv = hMatch[1].length + const size = ['', '40rpx', '36rpx', '32rpx', '28rpx', '26rpx', '24rpx'][lv] + const wgt = ['', '700', '700', '600', '600', '500', '500'][lv] + const mg = ['', '20rpx 0 10rpx', '16rpx 0 8rpx', '14rpx 0 6rpx', '12rpx 0 6rpx', '10rpx 0 4rpx', '8rpx 0 4rpx'][lv] + out.push(`${parseInline(hMatch[2])}`) + continue + } + + // 分隔线 + if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) { + flushBuf(); out.push(closeList()) + out.push('
') + continue + } + + // 引用 + const bqMatch = line.match(/^>\s?(.+)$/) + if (bqMatch) { + flushBuf(); out.push(closeList()) + out.push(`
${parseInline(bqMatch[1])}
`) + continue + } + + // 无序列表 + const ulMatch = line.match(/^[-*+]\s+(.+)$/) + if (ulMatch) { + flushBuf() + if (!inList) { out.push('