Commit e24472f792ff6fd83e5f63f1915c813d8a572605

Authored by qianbao
1 parent 1cb691fc

添加向量库

src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java
@@ -6,6 +6,8 @@ import cn.hutool.core.date.DateUtil; @@ -6,6 +6,8 @@ import cn.hutool.core.date.DateUtil;
6 import cn.hutool.core.thread.ThreadUtil; 6 import cn.hutool.core.thread.ThreadUtil;
7 import cn.hutool.core.util.ObjectUtil; 7 import cn.hutool.core.util.ObjectUtil;
8 import cn.hutool.core.util.StrUtil; 8 import cn.hutool.core.util.StrUtil;
  9 +import com.google.common.reflect.TypeToken;
  10 +import com.google.gson.Gson;
9 import com.google.gson.JsonArray; 11 import com.google.gson.JsonArray;
10 import com.google.gson.JsonObject; 12 import com.google.gson.JsonObject;
11 import com.xly.milvus.service.MilvusService; 13 import com.xly.milvus.service.MilvusService;
@@ -30,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; @@ -30,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
30 import org.springframework.beans.factory.annotation.Value; 32 import org.springframework.beans.factory.annotation.Value;
31 import org.springframework.stereotype.Service; 33 import org.springframework.stereotype.Service;
32 34
  35 +import java.lang.reflect.Type;
33 import java.math.BigDecimal; 36 import java.math.BigDecimal;
34 import java.time.LocalDate; 37 import java.time.LocalDate;
35 import java.time.LocalDateTime; 38 import java.time.LocalDateTime;
@@ -324,7 +327,11 @@ public class MilvusServiceImpl implements MilvusService { @@ -324,7 +327,11 @@ public class MilvusServiceImpl implements MilvusService {
324 for (int i = 0; i < vectorList.size(); i++) { 327 for (int i = 0; i < vectorList.size(); i++) {
325 floatArray[i] = vectorList.get(i); 328 floatArray[i] = vectorList.get(i);
326 } 329 }
327 - 330 + if(ObjectUtil.isEmpty(fields)){
  331 + fields = new ArrayList<>();
  332 + }
  333 + fields.add("sSlaveId");
  334 + fields.add("metadata");
328 // 3. 创建 Milvus FloatVec 对象 335 // 3. 创建 Milvus FloatVec 对象
329 FloatVec floatVec = new FloatVec(floatArray); 336 FloatVec floatVec = new FloatVec(floatArray);
330 log.info("查询向量库条件{}",milvusFilter); 337 log.info("查询向量库条件{}",milvusFilter);
@@ -449,7 +456,11 @@ public class MilvusServiceImpl implements MilvusService { @@ -449,7 +456,11 @@ public class MilvusServiceImpl implements MilvusService {
449 for (SearchResp.SearchResult result : resultList) { 456 for (SearchResp.SearchResult result : resultList) {
450 // 获取实体字段数据 457 // 获取实体字段数据
451 Map<String, Object> entity = result.getEntity(); 458 Map<String, Object> entity = result.getEntity();
452 - Map<String, Object> metadata = (Map<String, Object>) entity.get("metadata"); 459 + Map<String,Object> metadata = new HashMap<>();
  460 + if(ObjectUtil.isNotEmpty(entity.get("metadata"))){
  461 + JsonObject obj = (JsonObject) entity.get("metadata");
  462 + metadata.putAll( jsonObjectToMap(obj));
  463 + }
453 // 获取相似度分数 464 // 获取相似度分数
454 Float score = result.getScore(); 465 Float score = result.getScore();
455 if (score != null) { 466 if (score != null) {
@@ -463,6 +474,15 @@ public class MilvusServiceImpl implements MilvusService { @@ -463,6 +474,15 @@ public class MilvusServiceImpl implements MilvusService {
463 log.info("处理完成,共 {} 条搜索结果", results.size()); 474 log.info("处理完成,共 {} 条搜索结果", results.size());
464 return results; 475 return results;
465 } 476 }
  477 +
  478 + /**
  479 + * JsonObject 转 Map<String, Object>
  480 + */
  481 + public static Map<String, Object> jsonObjectToMap(JsonObject jsonObject) {
  482 + Gson gson = new Gson();
  483 + Type type = new TypeToken<Map<String, Object>>(){}.getType();
  484 + return gson.fromJson(jsonObject, type);
  485 + }
466 /** 486 /**
467 * 从实体对象构建Milvus插入数据 487 * 从实体对象构建Milvus插入数据
468 */ 488 */
src/main/resources/templates/chat.html
@@ -466,7 +466,7 @@ @@ -466,7 +466,7 @@
466 let brandsid= "1111111111"; 466 let brandsid= "1111111111";
467 let subsidiaryid= "1111111111"; 467 let subsidiaryid= "1111111111";
468 let usertype= "sysadmin"; 468 let usertype= "sysadmin";
469 - let authorization="1EDB99C9BF070115F7A57AC43D8CB09F0B8C49F979DAB63A2AEA84B372B2B42BF3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D35611629BD9166D2BBFC3B7AF31FDF60A31A297DF9BF51740C90173D4CC922B3538155B7ADAEE71E899235DC1122F426"; 469 + let authorization="CE444885A9BCFDDE1FD793F8A0931301E9D7DE6CEDD9DE4B83ECE2219C7829A8F3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D4CAE6F9AC893752209A98011A981375391D4466816B7D3D1AF306E28B989121C538155B7ADAEE71E899235DC1122F426";
470 let hrefLock = window.location.origin+"/xlyAi"; 470 let hrefLock = window.location.origin+"/xlyAi";
471 471
472 const CONFIG = { 472 const CONFIG = {
@@ -479,6 +479,8 @@ @@ -479,6 +479,8 @@
479 }; 479 };
480 480
481 let chatHistory = []; 481 let chatHistory = [];
  482 + let audioQueue = [];
  483 + let isPlaying = false;
482 484
483 let currentModel = 'general'; 485 let currentModel = 'general';
484 const md = window.markdownit({ 486 const md = window.markdownit({
@@ -555,6 +557,9 @@ @@ -555,6 +557,9 @@
555 doMessage(input, message, button); 557 doMessage(input, message, button);
556 } 558 }
557 559
  560 + // ======================
  561 + // 🔥 已修复:完整 fetch 流式交互
  562 + // ======================
558 // ============================ 563 // ============================
559 // 核心:按序号 0,1,2... 顺序获取 + 播放 564 // 核心:按序号 0,1,2... 顺序获取 + 播放
560 // =========================== 565 // ===========================
@@ -594,7 +599,6 @@ @@ -594,7 +599,6 @@
594 checkPiece(); 599 checkPiece();
595 } 600 }
596 601
597 - // 修改 doMessage 函数,改为调用新的流式接口  
598 async function doMessage(input, message, button) { 602 async function doMessage(input, message, button) {
599 addMessage(message, 'user'); 603 addMessage(message, 'user');
600 showTypingIndicator(); 604 showTypingIndicator();
@@ -603,9 +607,6 @@ @@ -603,9 +607,6 @@
603 const requestData = { 607 const requestData = {
604 text: message, 608 text: message,
605 userid: userid, 609 userid: userid,
606 - username: username,  
607 - brandsid: brandsid,  
608 - subsidiaryid: subsidiaryid,  
609 usertype: usertype, 610 usertype: usertype,
610 authorization: authorization, 611 authorization: authorization,
611 voice: "zh-CN-XiaoxiaoNeural", 612 voice: "zh-CN-XiaoxiaoNeural",
@@ -614,173 +615,53 @@ @@ -614,173 +615,53 @@
614 voiceless: true 615 voiceless: true
615 }; 616 };
616 617
617 - // 创建临时消息元素用于流式追加内容  
618 - const tempMessageId = `temp-${Date.now()}`;  
619 - const messagesDiv = $('#chatMessages');  
620 - hideTypingIndicator();  
621 -  
622 - // 创建一个临时的AI消息容器  
623 - const messageHtml = `  
624 - <div class="message ai-message" id="${tempMessageId}">  
625 - <div class="message-bubble">  
626 - <div class="message-content"></div>  
627 - <div class="message-meta">  
628 - <span class="message-time">${getCurrentTime()}</span>  
629 - <div class="message-actions">  
630 - <button class="action-btn" onclick="copyMessage('${tempMessageId}')">复制</button>  
631 - <button class="action-btn" onclick="regenerateMessage('${tempMessageId}')">重新生成</button>  
632 - </div>  
633 - </div>  
634 - </div>  
635 - </div>  
636 - `;  
637 - messagesDiv.append(messageHtml);  
638 - scrollToBottom();  
639 -  
640 - let fullText = '';  
641 - let cacheKey = null;  
642 - let audioSize = 0;  
643 - let hasReceivedComplete = false;  
644 -  
645 - // 调用流式接口  
646 - const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/queryFlux`, { 618 + const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/query`, {
647 method: "POST", 619 method: "POST",
648 - headers: {  
649 - "Content-Type": "application/json;charset=UTF-8",  
650 - "Accept": "application/x-ndjson, application/json"  
651 - }, 620 + headers: { "Content-Type": "application/json;charset=UTF-8" },
652 body: JSON.stringify(requestData) 621 body: JSON.stringify(requestData)
653 }); 622 });
654 623
655 - if (!response.ok) {  
656 - throw new Error(`HTTP ${response.status}: ${response.statusText}`);  
657 - }  
658 -  
659 - const reader = response.body.getReader();  
660 - const decoder = new TextDecoder();  
661 - let buffer = '';  
662 -  
663 - while (true) {  
664 - const { done, value } = await reader.read();  
665 - if (done) break;  
666 -  
667 - buffer += decoder.decode(value, { stream: true });  
668 - const lines = buffer.split('\n');  
669 - buffer = lines.pop() || '';  
670 -  
671 - for (const line of lines) {  
672 - if (line.trim() === '') continue;  
673 -  
674 - try {  
675 - const data = JSON.parse(line);  
676 - console.log('收到数据:', data.message, data);  
677 -  
678 - // 根据消息类型处理  
679 - switch(data.message) {  
680 - case 'ERP_CHUNK':  
681 - // ERP文本片段 - 实时显示  
682 - if (data.processedText) {  
683 - fullText += data.processedText;  
684 - const messageContent = $(`#${tempMessageId} .message-content`);  
685 - // 直接显示HTML内容(因为后端返回的是HTML格式)  
686 - messageContent.html(fullText);  
687 - scrollToBottom();  
688 - }  
689 - break;  
690 -  
691 - case 'ERP_COMPLETE':  
692 - // ERP完成,更新完整文本  
693 - if (data.processedText) {  
694 - fullText = data.processedText;  
695 - const messageContent = $(`#${tempMessageId} .message-content`);  
696 - messageContent.html(fullText);  
697 - scrollToBottom();  
698 - }  
699 - hasReceivedComplete = true;  
700 - console.log('ERP完成,文本长度:', fullText.length);  
701 - break;  
702 -  
703 - case 'TTS_SEGMENT':  
704 - // TTS音频片段 - 处理音频  
705 - if (data.audioBase64) {  
706 - const blob = base64ToBlob(data.audioBase64);  
707 - const audio = new Audio(URL.createObjectURL(blob));  
708 - audio.play().catch(err => console.log('播放失败:', err));  
709 - }  
710 - if (data.cacheKey) {  
711 - cacheKey = data.cacheKey;  
712 - }  
713 - if (data.audioSize) {  
714 - audioSize = data.audioSize;  
715 - }  
716 - break;  
717 -  
718 - default:  
719 - // 兼容旧格式  
720 - if (data.processedText) {  
721 - fullText += data.processedText;  
722 - const messageContent = $(`#${tempMessageId} .message-content`);  
723 - messageContent.html(fullText);  
724 - scrollToBottom();  
725 - }  
726 - if (data.cacheKey) {  
727 - cacheKey = data.cacheKey;  
728 - }  
729 - break;  
730 - }  
731 -  
732 - // 如果是最后一包且还没有播放音频  
733 - if (data.last === true && cacheKey && audioSize > 0) {  
734 - playByIndex(cacheKey, 0, audioSize);  
735 - }  
736 -  
737 - } catch (e) {  
738 - console.error('解析流式数据失败:', e, line); 624 + const data = await response.json();
  625 + hideTypingIndicator();
  626 + const replyText = (data.processedText || "") + (data.systemText || "");
  627 + addMessage(replyText, 'ai');
  628 +
  629 + // ==============================================
  630 + // 👇 【关键】用 cacheKey 取音频(绝对不串音)
  631 + // ==============================================
  632 + const cacheKey = data.cacheKey;
  633 + if (!cacheKey) return;
  634 + const audioSize = data.audioSize; // 总分几段
  635 +
  636 + let retry = 0;
  637 + const checkAudio = async () => {
  638 + retry++;
  639 + if (retry > 20) return;
  640 +
  641 + try {
  642 + // ==============================================
  643 + // 👇 用 cacheKey 获取自己的音频(别人拿不到)
  644 + // ==============================================
  645 + const res = await fetch(`${CONFIG.backendUrl}/api/tts/audio?cacheKey=${encodeURIComponent(cacheKey)}`);
  646 + const audioData = await res.json();
  647 +
  648 + if (audioData.audioBase64) {
  649 + const blob = base64ToBlob(audioData.audioBase64);
  650 + const audio = new Audio(URL.createObjectURL(blob));
  651 + audio.play().catch(err => console.log('播放异常', err));
  652 + } else {
  653 + setTimeout(checkAudio, 800);
739 } 654 }
  655 + } catch (e) {
  656 + setTimeout(checkAudio, 800);
740 } 657 }
741 - }  
742 -  
743 - // 流式结束后,处理可能的音频播放  
744 - if (cacheKey && audioSize > 0) {  
745 - playByIndex(cacheKey, 0, audioSize);  
746 - }  
747 -  
748 - // 如果收到完整内容,直接显示,不需要额外处理  
749 - if (!hasReceivedComplete && fullText) {  
750 - const messageContent = $(`#${tempMessageId} .message-content`);  
751 - messageContent.html(fullText);  
752 - }  
753 -  
754 - // 更新最终消息ID  
755 - const finalMessageId = `msg-${Date.now()}`;  
756 - const finalMessage = $(`#${tempMessageId}`).clone();  
757 - finalMessage.attr('id', finalMessageId);  
758 - $(`#${tempMessageId}`).remove();  
759 - messagesDiv.append(finalMessage);  
760 -  
761 - // 更新按钮的事件绑定  
762 - $(`#${finalMessageId} .action-btn`).each(function() {  
763 - const onclick = $(this).attr('onclick');  
764 - if (onclick) {  
765 - $(this).attr('onclick', onclick.replace(tempMessageId, finalMessageId));  
766 - }  
767 - });  
768 -  
769 - // 处理消息中的可点击按钮(如果有)  
770 - $(`#${finalMessageId} .message-content [data-action]`).each(function() {  
771 - const action = $(this).attr('data-action');  
772 - const text = $(this).attr('data-text');  
773 - if (action === 'reset') {  
774 - $(this).on('click', function() {  
775 - reset(text);  
776 - });  
777 - }  
778 - }); 658 + };
  659 + setTimeout(checkAudio, 1200);
  660 + playByIndex(cacheKey, 0, audioSize);
779 661
780 } catch (error) { 662 } catch (error) {
781 console.error('错误:', error); 663 console.error('错误:', error);
782 hideTypingIndicator(); 664 hideTypingIndicator();
783 - $(`#temp-${Date.now()}`).remove();  
784 addMessage("服务异常,请重试", 'ai'); 665 addMessage("服务异常,请重试", 'ai');
785 } finally { 666 } finally {
786 input.prop('disabled', false); 667 input.prop('disabled', false);
@@ -790,10 +671,40 @@ @@ -790,10 +671,40 @@
790 } 671 }
791 } 672 }
792 673
793 - // 修改原有的 handleNormalResponse 函数(如果需要的话) 674 + // ==============================
  675 + // 👇 语音排队播放函数(保证顺序)
  676 + // ==============================
  677 + function playNextAudio() {
  678 + if (isPlaying || audioQueue.length === 0) return;
  679 +
  680 + isPlaying = true;
  681 + const base64 = audioQueue.shift();
  682 + const blob = base64ToBlob(base64);
  683 + const audio = new Audio(URL.createObjectURL(blob));
  684 +
  685 + audio.onended = () => {
  686 + isPlaying = false;
  687 + playNextAudio(); // 播放下一条
  688 + };
  689 +
  690 + audio.play().catch(err => {
  691 + isPlaying = false;
  692 + playNextAudio();
  693 + });
  694 + }
  695 +
  696 + function base64ToBlob(base64) {
  697 + const byteCharacters = atob(base64);
  698 + const byteNumbers = new Array(byteCharacters.length);
  699 + for (let i = 0; i < byteCharacters.length; i++) {
  700 + byteNumbers[i] = byteCharacters.charCodeAt(i);
  701 + }
  702 + return new Blob([new Uint8Array(byteNumbers)], { type: 'audio/mpeg' });
  703 + }
  704 +
794 async function handleNormalResponse(requestData) { 705 async function handleNormalResponse(requestData) {
795 try { 706 try {
796 - const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/queryFlux`, { 707 + const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/query`, {
797 method: 'POST', 708 method: 'POST',
798 headers: CONFIG.headers, 709 headers: CONFIG.headers,
799 body: JSON.stringify(requestData) 710 body: JSON.stringify(requestData)
@@ -801,8 +712,6 @@ @@ -801,8 +712,6 @@
801 if (!response.ok) { 712 if (!response.ok) {
802 throw new Error(`HTTP ${response.status}: ${response.statusText}`); 713 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
803 } 714 }
804 - // 流式响应不需要在这里处理,由 doMessage 处理  
805 - return response;  
806 } catch (error) { 715 } catch (error) {
807 hideTypingIndicator(); 716 hideTypingIndicator();
808 throw error; 717 throw error;
@@ -811,15 +720,6 @@ @@ -811,15 +720,6 @@
811 } 720 }
812 } 721 }
813 722
814 - function base64ToBlob(base64) {  
815 - const byteCharacters = atob(base64);  
816 - const byteNumbers = new Array(byteCharacters.length);  
817 - for (let i = 0; i < byteCharacters.length; i++) {  
818 - byteNumbers[i] = byteCharacters.charCodeAt(i);  
819 - }  
820 - return new Blob([new Uint8Array(byteNumbers)], { type: 'audio/mpeg' });  
821 - }  
822 -  
823 function getCurrentTime() { 723 function getCurrentTime() {
824 const now = new Date(); 724 const now = new Date();
825 return now.getHours().toString().padStart(2, '0') + ':' + 725 return now.getHours().toString().padStart(2, '0') + ':' +
@@ -1041,4 +941,4 @@ @@ -1041,4 +941,4 @@
1041 }); 941 });
1042 </script> 942 </script>
1043 </body> 943 </body>
1044 -</html>  
1045 \ No newline at end of file 944 \ No newline at end of file
  945 +</html>