diff --git a/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java b/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java index f411413..e09b5b3 100644 --- a/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java +++ b/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java @@ -6,6 +6,8 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.xly.milvus.service.MilvusService; @@ -30,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -324,7 +327,11 @@ public class MilvusServiceImpl implements MilvusService { for (int i = 0; i < vectorList.size(); i++) { floatArray[i] = vectorList.get(i); } - + if(ObjectUtil.isEmpty(fields)){ + fields = new ArrayList<>(); + } + fields.add("sSlaveId"); + fields.add("metadata"); // 3. 创建 Milvus FloatVec 对象 FloatVec floatVec = new FloatVec(floatArray); log.info("查询向量库条件{}",milvusFilter); @@ -449,7 +456,11 @@ public class MilvusServiceImpl implements MilvusService { for (SearchResp.SearchResult result : resultList) { // 获取实体字段数据 Map entity = result.getEntity(); - Map metadata = (Map) entity.get("metadata"); + Map metadata = new HashMap<>(); + if(ObjectUtil.isNotEmpty(entity.get("metadata"))){ + JsonObject obj = (JsonObject) entity.get("metadata"); + metadata.putAll( jsonObjectToMap(obj)); + } // 获取相似度分数 Float score = result.getScore(); if (score != null) { @@ -463,6 +474,15 @@ public class MilvusServiceImpl implements MilvusService { log.info("处理完成,共 {} 条搜索结果", results.size()); return results; } + + /** + * JsonObject 转 Map + */ + public static Map jsonObjectToMap(JsonObject jsonObject) { + Gson gson = new Gson(); + Type type = new TypeToken>(){}.getType(); + return gson.fromJson(jsonObject, type); + } /** * 从实体对象构建Milvus插入数据 */ diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html index 92c99b0..b29b9fe 100644 --- a/src/main/resources/templates/chat.html +++ b/src/main/resources/templates/chat.html @@ -466,7 +466,7 @@ let brandsid= "1111111111"; let subsidiaryid= "1111111111"; let usertype= "sysadmin"; - let authorization="1EDB99C9BF070115F7A57AC43D8CB09F0B8C49F979DAB63A2AEA84B372B2B42BF3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D35611629BD9166D2BBFC3B7AF31FDF60A31A297DF9BF51740C90173D4CC922B3538155B7ADAEE71E899235DC1122F426"; + let authorization="CE444885A9BCFDDE1FD793F8A0931301E9D7DE6CEDD9DE4B83ECE2219C7829A8F3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D4CAE6F9AC893752209A98011A981375391D4466816B7D3D1AF306E28B989121C538155B7ADAEE71E899235DC1122F426"; let hrefLock = window.location.origin+"/xlyAi"; const CONFIG = { @@ -479,6 +479,8 @@ }; let chatHistory = []; + let audioQueue = []; + let isPlaying = false; let currentModel = 'general'; const md = window.markdownit({ @@ -555,6 +557,9 @@ doMessage(input, message, button); } + // ====================== + // 🔥 已修复:完整 fetch 流式交互 + // ====================== // ============================ // 核心:按序号 0,1,2... 顺序获取 + 播放 // =========================== @@ -594,7 +599,6 @@ checkPiece(); } - // 修改 doMessage 函数,改为调用新的流式接口 async function doMessage(input, message, button) { addMessage(message, 'user'); showTypingIndicator(); @@ -603,9 +607,6 @@ const requestData = { text: message, userid: userid, - username: username, - brandsid: brandsid, - subsidiaryid: subsidiaryid, usertype: usertype, authorization: authorization, voice: "zh-CN-XiaoxiaoNeural", @@ -614,173 +615,53 @@ voiceless: true }; - // 创建临时消息元素用于流式追加内容 - const tempMessageId = `temp-${Date.now()}`; - const messagesDiv = $('#chatMessages'); - hideTypingIndicator(); - - // 创建一个临时的AI消息容器 - const messageHtml = ` -
-
-
-
- ${getCurrentTime()} -
- - -
-
-
-
- `; - messagesDiv.append(messageHtml); - scrollToBottom(); - - let fullText = ''; - let cacheKey = null; - let audioSize = 0; - let hasReceivedComplete = false; - - // 调用流式接口 - const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/queryFlux`, { + const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/query`, { method: "POST", - headers: { - "Content-Type": "application/json;charset=UTF-8", - "Accept": "application/x-ndjson, application/json" - }, + headers: { "Content-Type": "application/json;charset=UTF-8" }, body: JSON.stringify(requestData) }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.trim() === '') continue; - - try { - const data = JSON.parse(line); - console.log('收到数据:', data.message, data); - - // 根据消息类型处理 - switch(data.message) { - case 'ERP_CHUNK': - // ERP文本片段 - 实时显示 - if (data.processedText) { - fullText += data.processedText; - const messageContent = $(`#${tempMessageId} .message-content`); - // 直接显示HTML内容(因为后端返回的是HTML格式) - messageContent.html(fullText); - scrollToBottom(); - } - break; - - case 'ERP_COMPLETE': - // ERP完成,更新完整文本 - if (data.processedText) { - fullText = data.processedText; - const messageContent = $(`#${tempMessageId} .message-content`); - messageContent.html(fullText); - scrollToBottom(); - } - hasReceivedComplete = true; - console.log('ERP完成,文本长度:', fullText.length); - break; - - case 'TTS_SEGMENT': - // TTS音频片段 - 处理音频 - if (data.audioBase64) { - const blob = base64ToBlob(data.audioBase64); - const audio = new Audio(URL.createObjectURL(blob)); - audio.play().catch(err => console.log('播放失败:', err)); - } - if (data.cacheKey) { - cacheKey = data.cacheKey; - } - if (data.audioSize) { - audioSize = data.audioSize; - } - break; - - default: - // 兼容旧格式 - if (data.processedText) { - fullText += data.processedText; - const messageContent = $(`#${tempMessageId} .message-content`); - messageContent.html(fullText); - scrollToBottom(); - } - if (data.cacheKey) { - cacheKey = data.cacheKey; - } - break; - } - - // 如果是最后一包且还没有播放音频 - if (data.last === true && cacheKey && audioSize > 0) { - playByIndex(cacheKey, 0, audioSize); - } - - } catch (e) { - console.error('解析流式数据失败:', e, line); + const data = await response.json(); + hideTypingIndicator(); + const replyText = (data.processedText || "") + (data.systemText || ""); + addMessage(replyText, 'ai'); + + // ============================================== + // 👇 【关键】用 cacheKey 取音频(绝对不串音) + // ============================================== + const cacheKey = data.cacheKey; + if (!cacheKey) return; + const audioSize = data.audioSize; // 总分几段 + + let retry = 0; + const checkAudio = async () => { + retry++; + if (retry > 20) return; + + try { + // ============================================== + // 👇 用 cacheKey 获取自己的音频(别人拿不到) + // ============================================== + const res = await fetch(`${CONFIG.backendUrl}/api/tts/audio?cacheKey=${encodeURIComponent(cacheKey)}`); + const audioData = await res.json(); + + if (audioData.audioBase64) { + const blob = base64ToBlob(audioData.audioBase64); + const audio = new Audio(URL.createObjectURL(blob)); + audio.play().catch(err => console.log('播放异常', err)); + } else { + setTimeout(checkAudio, 800); } + } catch (e) { + setTimeout(checkAudio, 800); } - } - - // 流式结束后,处理可能的音频播放 - if (cacheKey && audioSize > 0) { - playByIndex(cacheKey, 0, audioSize); - } - - // 如果收到完整内容,直接显示,不需要额外处理 - if (!hasReceivedComplete && fullText) { - const messageContent = $(`#${tempMessageId} .message-content`); - messageContent.html(fullText); - } - - // 更新最终消息ID - const finalMessageId = `msg-${Date.now()}`; - const finalMessage = $(`#${tempMessageId}`).clone(); - finalMessage.attr('id', finalMessageId); - $(`#${tempMessageId}`).remove(); - messagesDiv.append(finalMessage); - - // 更新按钮的事件绑定 - $(`#${finalMessageId} .action-btn`).each(function() { - const onclick = $(this).attr('onclick'); - if (onclick) { - $(this).attr('onclick', onclick.replace(tempMessageId, finalMessageId)); - } - }); - - // 处理消息中的可点击按钮(如果有) - $(`#${finalMessageId} .message-content [data-action]`).each(function() { - const action = $(this).attr('data-action'); - const text = $(this).attr('data-text'); - if (action === 'reset') { - $(this).on('click', function() { - reset(text); - }); - } - }); + }; + setTimeout(checkAudio, 1200); + playByIndex(cacheKey, 0, audioSize); } catch (error) { console.error('错误:', error); hideTypingIndicator(); - $(`#temp-${Date.now()}`).remove(); addMessage("服务异常,请重试", 'ai'); } finally { input.prop('disabled', false); @@ -790,10 +671,40 @@ } } - // 修改原有的 handleNormalResponse 函数(如果需要的话) + // ============================== + // 👇 语音排队播放函数(保证顺序) + // ============================== + function playNextAudio() { + if (isPlaying || audioQueue.length === 0) return; + + isPlaying = true; + const base64 = audioQueue.shift(); + const blob = base64ToBlob(base64); + const audio = new Audio(URL.createObjectURL(blob)); + + audio.onended = () => { + isPlaying = false; + playNextAudio(); // 播放下一条 + }; + + audio.play().catch(err => { + isPlaying = false; + playNextAudio(); + }); + } + + function base64ToBlob(base64) { + const byteCharacters = atob(base64); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + return new Blob([new Uint8Array(byteNumbers)], { type: 'audio/mpeg' }); + } + async function handleNormalResponse(requestData) { try { - const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/queryFlux`, { + const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/query`, { method: 'POST', headers: CONFIG.headers, body: JSON.stringify(requestData) @@ -801,8 +712,6 @@ if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - // 流式响应不需要在这里处理,由 doMessage 处理 - return response; } catch (error) { hideTypingIndicator(); throw error; @@ -811,15 +720,6 @@ } } - function base64ToBlob(base64) { - const byteCharacters = atob(base64); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - return new Blob([new Uint8Array(byteNumbers)], { type: 'audio/mpeg' }); - } - function getCurrentTime() { const now = new Date(); return now.getHours().toString().padStart(2, '0') + ':' + @@ -1041,4 +941,4 @@ }); - \ No newline at end of file +