Commit e24472f792ff6fd83e5f63f1915c813d8a572605
1 parent
1cb691fc
添加向量库
Showing
2 changed files
with
98 additions
and
178 deletions
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> | ... | ... |