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 +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> |