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 import cn.hutool.core.thread.ThreadUtil;
7 7 import cn.hutool.core.util.ObjectUtil;
8 8 import cn.hutool.core.util.StrUtil;
  9 +import com.google.common.reflect.TypeToken;
  10 +import com.google.gson.Gson;
9 11 import com.google.gson.JsonArray;
10 12 import com.google.gson.JsonObject;
11 13 import com.xly.milvus.service.MilvusService;
... ... @@ -30,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
30 32 import org.springframework.beans.factory.annotation.Value;
31 33 import org.springframework.stereotype.Service;
32 34  
  35 +import java.lang.reflect.Type;
33 36 import java.math.BigDecimal;
34 37 import java.time.LocalDate;
35 38 import java.time.LocalDateTime;
... ... @@ -324,7 +327,11 @@ public class MilvusServiceImpl implements MilvusService {
324 327 for (int i = 0; i < vectorList.size(); i++) {
325 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 335 // 3. 创建 Milvus FloatVec 对象
329 336 FloatVec floatVec = new FloatVec(floatArray);
330 337 log.info("查询向量库条件{}",milvusFilter);
... ... @@ -449,7 +456,11 @@ public class MilvusServiceImpl implements MilvusService {
449 456 for (SearchResp.SearchResult result : resultList) {
450 457 // 获取实体字段数据
451 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 465 Float score = result.getScore();
455 466 if (score != null) {
... ... @@ -463,6 +474,15 @@ public class MilvusServiceImpl implements MilvusService {
463 474 log.info("处理完成,共 {} 条搜索结果", results.size());
464 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 487 * 从实体对象构建Milvus插入数据
468 488 */
... ...
src/main/resources/templates/chat.html
... ... @@ -466,7 +466,7 @@
466 466 let brandsid= "1111111111";
467 467 let subsidiaryid= "1111111111";
468 468 let usertype= "sysadmin";
469   - let authorization="1EDB99C9BF070115F7A57AC43D8CB09F0B8C49F979DAB63A2AEA84B372B2B42BF3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D35611629BD9166D2BBFC3B7AF31FDF60A31A297DF9BF51740C90173D4CC922B3538155B7ADAEE71E899235DC1122F426";
  469 + let authorization="CE444885A9BCFDDE1FD793F8A0931301E9D7DE6CEDD9DE4B83ECE2219C7829A8F3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D4CAE6F9AC893752209A98011A981375391D4466816B7D3D1AF306E28B989121C538155B7ADAEE71E899235DC1122F426";
470 470 let hrefLock = window.location.origin+"/xlyAi";
471 471  
472 472 const CONFIG = {
... ... @@ -479,6 +479,8 @@
479 479 };
480 480  
481 481 let chatHistory = [];
  482 + let audioQueue = [];
  483 + let isPlaying = false;
482 484  
483 485 let currentModel = 'general';
484 486 const md = window.markdownit({
... ... @@ -555,6 +557,9 @@
555 557 doMessage(input, message, button);
556 558 }
557 559  
  560 + // ======================
  561 + // 🔥 已修复:完整 fetch 流式交互
  562 + // ======================
558 563 // ============================
559 564 // 核心:按序号 0,1,2... 顺序获取 + 播放
560 565 // ===========================
... ... @@ -594,7 +599,6 @@
594 599 checkPiece();
595 600 }
596 601  
597   - // 修改 doMessage 函数,改为调用新的流式接口
598 602 async function doMessage(input, message, button) {
599 603 addMessage(message, 'user');
600 604 showTypingIndicator();
... ... @@ -603,9 +607,6 @@
603 607 const requestData = {
604 608 text: message,
605 609 userid: userid,
606   - username: username,
607   - brandsid: brandsid,
608   - subsidiaryid: subsidiaryid,
609 610 usertype: usertype,
610 611 authorization: authorization,
611 612 voice: "zh-CN-XiaoxiaoNeural",
... ... @@ -614,173 +615,53 @@
614 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 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 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 662 } catch (error) {
781 663 console.error('错误:', error);
782 664 hideTypingIndicator();
783   - $(`#temp-${Date.now()}`).remove();
784 665 addMessage("服务异常,请重试", 'ai');
785 666 } finally {
786 667 input.prop('disabled', false);
... ... @@ -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 705 async function handleNormalResponse(requestData) {
795 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 708 method: 'POST',
798 709 headers: CONFIG.headers,
799 710 body: JSON.stringify(requestData)
... ... @@ -801,8 +712,6 @@
801 712 if (!response.ok) {
802 713 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
803 714 }
804   - // 流式响应不需要在这里处理,由 doMessage 处理
805   - return response;
806 715 } catch (error) {
807 716 hideTypingIndicator();
808 717 throw error;
... ... @@ -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 723 function getCurrentTime() {
824 724 const now = new Date();
825 725 return now.getHours().toString().padStart(2, '0') + ':' +
... ... @@ -1041,4 +941,4 @@
1041 941 });
1042 942 </script>
1043 943 </body>
1044   -</html>
1045 944 \ No newline at end of file
  945 +</html>
... ...