Commit 11f61c6c7c007ca2267a119b1784a41cf5052d94

Authored by qianbao
1 parent cff9d5c5

添加向量库

... ... @@ -40,6 +40,7 @@
40 40 <json-schema.version>1.17.2</json-schema.version>
41 41 <!-- 向量数据 -->
42 42 <milvus.version>2.6.15</milvus.version>
  43 + <tess4j.version>5.18.0</tess4j.version>
43 44 </properties>
44 45  
45 46 <dependencies>
... ... @@ -106,6 +107,13 @@
106 107 </dependency>
107 108  
108 109 <dependency>
  110 + <groupId>net.sourceforge.tess4j</groupId>
  111 + <artifactId>tess4j</artifactId>
  112 + <version>${tess4j.version}</version>
  113 + <scope>compile</scope>
  114 + </dependency>
  115 +
  116 + <dependency>
109 117 <groupId>org.springframework.boot</groupId>
110 118 <artifactId>spring-boot-starter-thymeleaf</artifactId>
111 119 </dependency>
... ...
src/main/java/com/xly/agent/ChatiAgent.java
... ... @@ -4,6 +4,7 @@ import dev.langchain4j.service.MemoryId;
4 4 import dev.langchain4j.service.SystemMessage;
5 5 import dev.langchain4j.service.UserMessage;
6 6 import dev.langchain4j.service.V;
  7 +import reactor.core.publisher.Flux;
7 8  
8 9 public interface ChatiAgent {
9 10 @SystemMessage("""
... ... @@ -16,4 +17,15 @@ public interface ChatiAgent {
16 17 """)
17 18 @UserMessage("用户说:{{userInput}}")
18 19 String chat(@MemoryId String userId, @V("userInput") String userInput);
  20 +
  21 + @SystemMessage("""
  22 + 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。
  23 + 要求:1. 不生硬、不说教,避免书面化表达;
  24 + 2. 主动接梗,适当延伸话题,不一问一答;
  25 + 3. 偶尔带点小幽默,保持轻松无压力的氛围;
  26 + 4. 回答简洁,符合日常聊天的语气,不啰嗦。
  27 + 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。
  28 + """)
  29 + @UserMessage("用户说:{{userInput}}")
  30 + Flux<String> chatStream(@MemoryId String userId, @V("userInput") String userInput);
19 31 }
... ...
src/main/java/com/xly/constant/ReturnTypeCode.java
... ... @@ -14,6 +14,7 @@ public enum ReturnTypeCode {
14 14  
15 15 // 成功
16 16 HTML("html", "html"),
  17 + STREAM("stream", "stream"),
17 18 MAKEDOWN("makedown", "makedown");
18 19  
19 20  
... ...
src/main/java/com/xly/entity/ToolMeta.java
... ... @@ -49,5 +49,6 @@ public class ToolMeta {
49 49 private LocalDateTime tMakeDate;
50 50 private String sVectorfiled;
51 51 private String sVectorjson;
  52 + private String sVectorfiledAll;
52 53  
53 54 }
... ...
src/main/java/com/xly/milvus/service/MilvusService.java
... ... @@ -54,5 +54,5 @@ public interface MilvusService {
54 54 * @return java.util.Map<java.lang.String,java.lang.Object>
55 55 * @Description 获取配置
56 56 **/
57   - Map<String,Object> getMilvusFiled(String sVectorfiled);
  57 + Map<String,Object> getMilvusFiled(String sVectorfiled,String sVectorfiledAll);
58 58 }
59 59 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java
... ... @@ -11,6 +11,7 @@ import com.google.gson.JsonObject;
11 11 import com.xly.milvus.service.MilvusService;
12 12 import com.xly.milvus.service.VectorizationService;
13 13 import com.xly.milvus.util.MapToJsonConverter;
  14 +import com.xly.milvus.util.MilvusTimeUtil;
14 15 import com.xly.service.DynamicExeDbService;
15 16 import com.xly.tts.bean.TTSResponseDTO;
16 17 import io.milvus.response.SearchResultsWrapper;
... ... @@ -33,6 +34,10 @@ import org.springframework.beans.factory.annotation.Value;
33 34 import org.springframework.stereotype.Service;
34 35  
35 36 import java.math.BigDecimal;
  37 +import java.time.LocalDate;
  38 +import java.time.LocalDateTime;
  39 +import java.time.ZoneId;
  40 +import java.time.format.DateTimeFormatter;
36 41 import java.util.*;
37 42 import java.util.regex.Matcher;
38 43 import java.util.regex.Pattern;
... ... @@ -49,7 +54,10 @@ public class MilvusServiceImpl implements MilvusService {
49 54 private final MilvusClientV2 milvusClient;
50 55 private final VectorizationService vectorizationService;
51 56 private final DynamicExeDbService dynamicExeDbService;
52   -
  57 + private static final long NULL_TIMESTAMP = -1L;
  58 + // 日期格式常量
  59 + private static final DateTimeFormatter ISO_FORMATTER =
  60 + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
53 61 // 或者从配置文件读取
54 62 @Value("${milvus.vector.dimension:384}")
55 63 private int VECTOR_DIM;
... ... @@ -90,13 +98,12 @@ public class MilvusServiceImpl implements MilvusService {
90 98 String sInputTabelName = reqMap.get("sInputTabelName").toString();
91 99 String sVectorfiled = reqMap.get("sVectorfiled").toString();
92 100 String sVectorjson = reqMap.get("sVectorjson").toString();
93   -
  101 + //创建集合
  102 + createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true);
94 103 String tUpdateDate = DateUtil.now();
95 104 String tUpdateDateUp = getUpdateDateUp(sInputTabelName);
96 105 //获取需要同步地数据
97 106 List<Map<String,Object>> data = getAddData(sInputTabelName,tUpdateDate, tUpdateDateUp);
98   - //创建集合
99   - createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true);
100 107 if(ObjectUtil.isNotEmpty(data)){
101 108 //插入数据
102 109 long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data);
... ... @@ -257,11 +264,12 @@ public class MilvusServiceImpl implements MilvusService {
257 264 * @Description 返回组装动态内容
258 265 **/
259 266 @Override
260   - public Map<String,Object> getMilvusFiled(String sVectorfiled){
261   - String[] sVectorfiledArray = sVectorfiled.split(",");
  267 + public Map<String,Object> getMilvusFiled(String sVectorfiled,String sVectorfiledAll){
262 268 List<String> sFileds = new ArrayList<>();
263 269 List<String> sFiledDescriptions = new ArrayList<>();
  270 + List<String> sFiledDescriptionsAll = new ArrayList<>();
264 271 List<Map<String,String>> titleList = new LinkedList<>();
  272 + String[] sVectorfiledArray = sVectorfiled.split(",");
265 273 for(String sVectorfiledOne : sVectorfiledArray){
266 274 Map<String,String> title = new HashMap<>();
267 275  
... ... @@ -278,9 +286,21 @@ public class MilvusServiceImpl implements MilvusService {
278 286 title.put("sTitle",sDescriptions);
279 287 titleList.add(title);
280 288 }
  289 + String[] sVectorfiledArrayAll = sVectorfiledAll.split(",");
  290 + for(String sVectorfiledOne : sVectorfiledArrayAll){
  291 + String[] sVectorfiledOneArray = sVectorfiledOne.split(":");
  292 + String sDescriptions = sVectorfiledOneArray[0];
  293 + String sName = sVectorfiledOneArray[1];
  294 + // 处理描述中可能包含的换行,保持缩进一致
  295 +// String formattedDesc = sDescriptions.replace("\n", "\n ");
  296 +// sFiledDescriptions.add(String.format(" - %s: %s", sName, formattedDesc));
  297 + String formattedDesc =String.format("%s: %s", sName, sDescriptions);
  298 + sFiledDescriptionsAll.add(formattedDesc);
  299 + }
281 300 Map<String,Object> rMap = new HashMap<>();
282 301 rMap.put("sMilvusFiled", String.join(",", sFileds));
283 302 rMap.put("sMilvusFiledDescription", String.join(",", sFiledDescriptions));
  303 + rMap.put("sMilvusFiledDescriptionAll", String.join(",", sFiledDescriptionsAll));
284 304 rMap.put("sFileds", sFileds);
285 305 rMap.put("title", titleList);
286 306 return rMap;
... ... @@ -430,17 +450,17 @@ public class MilvusServiceImpl implements MilvusService {
430 450 for (List<SearchResp.SearchResult> resultList : searchResults) {
431 451 // 遍历每个搜索结果
432 452 for (SearchResp.SearchResult result : resultList) {
433   - Map<String, Object> item = new HashMap<>();
  453 + // 获取实体字段数据
  454 + Map<String, Object> entity = result.getEntity();
  455 + Map<String, Object> metadata = (Map<String, Object>) entity.get("metadata");
434 456 // 获取相似度分数
435 457 Float score = result.getScore();
436 458 if (score != null) {
437   - item.put("score", score);
  459 + metadata.put("score", score);
438 460 }
439   - // 获取实体字段数据
440   - Map<String, Object> entity = result.getEntity();
441 461 // 将所有字段添加到结果中
442   - item.putAll(entity);
443   - results.add(item);
  462 +// item.putAll(entity);
  463 + results.add(metadata);
444 464 }
445 465 }
446 466 log.info("处理完成,共 {} 条搜索结果", results.size());
... ... @@ -548,27 +568,75 @@ public class MilvusServiceImpl implements MilvusService {
548 568 return;
549 569 }
550 570 // 基本类型直接添加
551   - if (value instanceof String) {
  571 + if(fieldName.startsWith("t")){
  572 + if(ObjectUtil.isNotEmpty(value)){
  573 + long date = MilvusTimeUtil.toTimestamp(value.toString());
  574 + row.addProperty(fieldName,date);
  575 + }else{
  576 + row.addProperty(fieldName,NULL_TIMESTAMP);
  577 + }
  578 + }else if (value instanceof String) {
552 579 row.addProperty(fieldName, (String) value);
553 580 } else if (value instanceof Number) {
554 581 row.addProperty(fieldName, (Number) value);
555 582 } else if (value instanceof Boolean) {
556 583 row.addProperty(fieldName, (Boolean) value);
557   - }
558   - // List / 数组类型
559   - else if (value instanceof List<?>) {
  584 + }else if (value instanceof List<?>) {
  585 + // List / 数组类型
560 586 JsonArray jsonArray = new JsonArray();
561 587 for (Object item : (List<?>) value) {
562 588 addJsonElement(jsonArray, item);
563 589 }
564 590 row.add(fieldName, jsonArray);
565   - }
566   - // 其他对象转字符串
567   - else {
  591 + } else {
  592 + // 其他对象转字符串
568 593 row.addProperty(fieldName, value.toString());
569 594 }
570 595 }
571 596 /**
  597 + * 解析字符串为时间戳
  598 + * 支持多种格式
  599 + */
  600 + private static long parseStringToTimestamp(String dateStr) {
  601 + if (dateStr == null || dateStr.trim().isEmpty()) {
  602 + return NULL_TIMESTAMP;
  603 + }
  604 +
  605 + try {
  606 + // 1. 尝试 ISO 8601 格式(如:2025-07-21T09:26:09)
  607 + if (dateStr.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}")) {
  608 + LocalDateTime ldt = LocalDateTime.parse(dateStr, ISO_FORMATTER);
  609 + return ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
  610 + }
  611 +
  612 + // 2. 尝试 yyyy-MM-dd HH:mm:ss 格式
  613 + if (dateStr.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
  614 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  615 + LocalDateTime ldt = LocalDateTime.parse(dateStr, formatter);
  616 + return ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
  617 + }
  618 +
  619 + // 3. 尝试 yyyy-MM-dd 格式(当天 00:00:00)
  620 + if (dateStr.matches("\\d{4}-\\d{2}-\\d{2}")) {
  621 + LocalDate ld = LocalDate.parse(dateStr);
  622 + return ld.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
  623 + }
  624 +
  625 + // 4. 尝试直接解析为时间戳数字(如果字符串全是数字)
  626 + if (dateStr.matches("\\d+")) {
  627 + return Long.parseLong(dateStr);
  628 + }
  629 +
  630 + // 5. 如果都不匹配,返回空值
  631 + System.err.println("无法解析的日期格式: " + dateStr);
  632 + return NULL_TIMESTAMP;
  633 +
  634 + } catch (Exception e) {
  635 + System.err.println("日期解析失败: " + dateStr + ", 错误: " + e.getMessage());
  636 + return NULL_TIMESTAMP;
  637 + }
  638 + }
  639 + /**
572 640 * 递归处理 List 元素(支持无限层嵌套 List)
573 641 */
574 642 private void addJsonElement(JsonArray jsonArray, Object item) {
... ... @@ -605,12 +673,11 @@ public class MilvusServiceImpl implements MilvusService {
605 673  
606 674 //是否删除集合 重新创建
607 675 if (bRset){
  676 + this.delAiMilvusVectorRecord(collectionName);
608 677 // 1. 删除旧集合
609 678 milvusClient.dropCollection(DropCollectionReq.builder()
610 679 .collectionName(collectionName)
611 680 .build());
612   - //删除对应的记录表
613   - delAiMilvusVectorRecord(collectionName);
614 681 }
615 682 // 检查集合是否存在
616 683 HasCollectionReq hasCollectionReq = HasCollectionReq.builder()
... ... @@ -777,9 +844,11 @@ public class MilvusServiceImpl implements MilvusService {
777 844 if(sKey.startsWith("d")){
778 845 return DataType.Double;
779 846 }else if(sKey.startsWith("i")){
780   - return DataType.Int64;
  847 + return DataType.Int32;
781 848 }else if(sKey.startsWith("b")){
782 849 return DataType.Bool;
  850 + }else if(sKey.startsWith("t")){
  851 + return DataType.Int64;
783 852 }else{
784 853 return DataType.VarChar;
785 854 }
... ... @@ -793,7 +862,14 @@ public class MilvusServiceImpl implements MilvusService {
793 862 * @Description 索引类型
794 863 **/
795 864 public IndexParam.IndexType indexField(String sKey) {
796   - return IndexParam.IndexType.TRIE;
  865 + if(sKey.startsWith("d") || sKey.startsWith("i")){
  866 + return IndexParam.IndexType.STL_SORT;
  867 + }else if(sKey.startsWith("t")){
  868 + return IndexParam.IndexType.STL_SORT;
  869 + }else{
  870 + return IndexParam.IndexType.TRIE;
  871 + }
  872 +
797 873 }
798 874  
799 875  
... ...
src/main/java/com/xly/milvus/util/MilvusTimeUtil.java 0 → 100644
  1 +package com.xly.milvus.util;
  2 +
  3 +import java.time.*;
  4 +import java.time.format.DateTimeFormatter;
  5 +import java.time.format.DateTimeParseException;
  6 +import java.util.*;
  7 +
  8 +public class MilvusTimeUtil {
  9 +
  10 + public static final long NULL_TIMESTAMP = -1L;
  11 +
  12 + // 多种 ISO 格式
  13 + private static final List<DateTimeFormatter> ISO_FORMATTERS = Arrays.asList(
  14 + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"), // 完整格式
  15 + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm"), // 缺少秒
  16 + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH"), // 只有小时
  17 + DateTimeFormatter.ISO_LOCAL_DATE_TIME, // 标准 ISO
  18 + DateTimeFormatter.ISO_OFFSET_DATE_TIME // 带时区
  19 + );
  20 +
  21 + /**
  22 + * 智能解析多种 ISO 格式
  23 + */
  24 + public static long toTimestamp(String isoString) {
  25 + if (isoString == null || isoString.trim().isEmpty()) {
  26 + return NULL_TIMESTAMP;
  27 + }
  28 +
  29 + // 尝试多种格式
  30 + for (DateTimeFormatter formatter : ISO_FORMATTERS) {
  31 + try {
  32 + LocalDateTime localDateTime = LocalDateTime.parse(isoString, formatter);
  33 + return localDateTime.atZone(ZoneId.systemDefault())
  34 + .toInstant()
  35 + .toEpochMilli();
  36 + } catch (DateTimeParseException e) {
  37 + // 继续尝试下一个格式
  38 + }
  39 + }
  40 +
  41 + // 特殊处理:如果只有日期
  42 + try {
  43 + LocalDate localDate = LocalDate.parse(isoString);
  44 + return localDate.atStartOfDay(ZoneId.systemDefault())
  45 + .toInstant()
  46 + .toEpochMilli();
  47 + } catch (DateTimeParseException e) {
  48 + // 忽略
  49 + }
  50 +
  51 + System.err.println("无法解析的日期格式: " + isoString);
  52 + return NULL_TIMESTAMP;
  53 + }
  54 +
  55 + /**
  56 + * 补全秒部分(将缺少秒的格式补全为 :00)
  57 + */
  58 + public static String fillSeconds(String isoString) {
  59 + if (isoString == null) return null;
  60 +
  61 + // 匹配 yyyy-MM-dd'T'HH:mm 格式
  62 + if (isoString.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}")) {
  63 + return isoString + ":00";
  64 + }
  65 +
  66 + // 匹配 yyyy-MM-dd'T'HH 格式
  67 + if (isoString.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}")) {
  68 + return isoString + ":00:00";
  69 + }
  70 +
  71 + return isoString;
  72 + }
  73 +}
0 74 \ No newline at end of file
... ...
src/main/java/com/xly/service/XlyErpService.java
... ... @@ -79,47 +79,137 @@ public class XlyErpService {
79 79  
80 80  
81 81 /**
82   - * 新的流式方法 - 返回 Flux<AiResponseDTO>
83   - * 每个AiResponseDTO包含一个文本片段
84   - */
85   - /**
86   - * 模拟的erpUserInputStream实现
87   - */
88   - public Flux<AiResponseDTO> erpUserInputStream(String userInput, String sUserId,
89   - String sUserName, String sBrandsId,
90   - String sSubsidiaryId, String sUserType,
91   - String authorization) {
92   - String requestId = UUID.randomUUID().toString();
93   -
94   - // 按句子分割
95   - String[] sentences = userInput.split("(?<=[。!?.!?])");
96   - int totalChunks = sentences.length;
97   -
98   - return Flux.range(0, totalChunks)
99   - .delayElements(Duration.ofMillis(200))
100   - .map(i -> {
101   - String sentence = sentences[i].trim();
102   - if (sentence.isEmpty()) return null;
103   -
104   - return AiResponseDTO.builder()
105   - .requestId(requestId)
106   - .code(200)
107   - .message("ERP_CHUNK")
108   - .status("PROCESSING")
109   - .textFragment(sentence)
110   - .chunkIndex(i)
111   - .totalChunks(totalChunks)
112   - .isLastChunk(i == totalChunks - 1)
113   - .progress((i + 1) * 100 / totalChunks)
114   - .timestamp(System.currentTimeMillis())
115   - .sSceneName("客服咨询")
116   - .sMethodName("chat")
117   - .sReturnType("MARKDOWN")
118   - .build();
119   - })
120   - .filter(Objects::nonNull);
121   - }
  82 + * @Author 钱豹
  83 + * @Date 19:18 2026/1/27
  84 + * @Param [userInput, userId, sUserType]
  85 + * @return reactor.core.publisher.Flux<AiResponseDTO>
  86 + * @Description 问答(流式返回)
  87 + **/
  88 + public Flux<AiResponseDTO> erpUserInputStream(String userInput,
  89 + String userId,
  90 + String sUserName,
  91 + String sBrandsId,
  92 + String sSubsidiaryId,
  93 + String sUserType,
  94 + String authorization) {
  95 + String sceneName = StrUtil.EMPTY;
  96 + String methodName = StrUtil.EMPTY;
  97 + UserSceneSession session=null;
  98 + try {
  99 + // 0. 预处理用户输入:去空格、转小写(方便匹配)
  100 + String input= InputPreprocessor.preprocessWithCommons(userInput);
  101 + // 1. 初始化用户场景会话(权限内场景)
  102 + session = userSceneSessionService.getUserSceneSession(userId,sUserName,sBrandsId,sSubsidiaryId,sUserType,authorization);
  103 + session.setAuthorization(authorization);
  104 + session.setSFunPrompts(null);
  105 + sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY;
  106 + methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():StrUtil.EMPTY;
  107 + // 2. 特殊指令:重置场景(无论是否已选,都可重置)
  108 + if (input.contains("重置") || input.contains("重新选择")) {
  109 + //清除记忆缓存
  110 + reSet(userId ,sUserName, sBrandsId ,sSubsidiaryId,sUserType,authorization,session);
  111 + return Flux.just(AiResponseDTO.builder()
  112 + .aiText(resetUserScene(session.getUserId(), session))
  113 + .sSceneName(sceneName)
  114 + .sMethodName(methodName)
  115 + .sReturnType(ReturnTypeCode.HTML.getCode())
  116 + .build());
  117 + }
  118 + //聊天只能体
  119 + if (session.getCurrentScene() != null
  120 + && Objects.equals(session.getCurrentScene().getSSceneNo(), "ChatZone"))
  121 + {
  122 + return getChatiAgentStream(input, session);
  123 + }
  124 + // 3. 未选场景:先展示场景选择界面,处理用户序号选择
  125 + if (!session.isSceneSelected() && ValiDataUtil.me().isPureNumber(input)){
  126 + // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示)
  127 + AiResponseDTO aiResponseDTO = handleSceneSelect(userId, input, session);
  128 + return Flux.just(aiResponseDTO);
  129 + }
  130 + // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续
  131 + ErpAiAgent aiAgent = createErpAiAgent(userId, input, session);
  132 + // 没有选择到场景,进闲聊模式
  133 + if (aiAgent == null){
  134 + return getChatiAgentStream (input,session);
  135 + }
  136 + String sResponMessage = StrUtil.EMPTY;
  137 + //用户输入添加方法(如果没有方法,动态SQL方法不需要)
  138 + if(!(ObjectUtil.isNotEmpty(session.getCurrentTool())
  139 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
  140 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo()))
  141 + ){
  142 + sResponMessage = aiAgent.chat(userId, input);
  143 + }
122 144  
  145 + if(ObjectUtil.isNotEmpty(session.getCurrentTool())
  146 + && !ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
  147 + ){
  148 + input = session.getCurrentTool().getSMethodName()+","+input;
  149 + }
  150 + //动态方法或返回需要提示的信息
  151 + if(ObjectUtil.isNotEmpty(session.getSFunPrompts())){
  152 + return Flux.just(AiResponseDTO.builder()
  153 + .aiText(session.getSFunPrompts())
  154 + .sSceneName(sceneName)
  155 + .sMethodName(methodName)
  156 + .sReturnType(ReturnTypeCode.HTML.getCode())
  157 + .build());
  158 + }
  159 +// 1.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体
  160 + if((ObjectUtil.isNotEmpty(session.getCurrentTool())
  161 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
  162 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo()))
  163 + ){
  164 + //查询是否走向量库 还是数据库查询
  165 + Boolean isAggregation = aiAgent.routeQuery(session.getUserId(), input);
  166 + if(!isAggregation){
  167 + //获取常量库内容
  168 + sResponMessage = getMilvus(session, input, aiAgent);
  169 + }else {
  170 + sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY, aiAgent);
  171 + }
  172 + return Flux.just(AiResponseDTO.builder()
  173 + .aiText(sResponMessage)
  174 + .sSceneName(sceneName)
  175 + .sMethodName(methodName)
  176 + .sReturnType(ReturnTypeCode.HTML.getCode())
  177 + .build());
  178 + } else if (ObjectUtil.isNotEmpty(session.getCurrentTool())) {
  179 + //2.处理工具参数采集结束后业务逻辑处理
  180 + //调用方法,参数缺失部分提示,就直接使用方法返回的
  181 + sResponMessage = dynamicToolProvider.doDynamicTool(session.getCurrentTool(),session);
  182 + return Flux.just(AiResponseDTO.builder()
  183 + .aiText(sResponMessage)
  184 + .sSceneName(sceneName)
  185 + .sMethodName(methodName)
  186 + .sReturnType(ReturnTypeCode.HTML.getCode())
  187 + .build());
  188 + }else if(session.getCurrentScene()== null ){
  189 + return Flux.just(AiResponseDTO.builder()
  190 + .aiText("当前场景:没有选择 退回当前场景 请输入 "+ CommonConstant.RESET + sResponMessage)
  191 + .sSceneName(sceneName)
  192 + .sMethodName(methodName)
  193 + .sReturnType(ReturnTypeCode.HTML.getCode())
  194 + .build());
  195 + }else{
  196 + return getChatiAgentStream (input, session);
  197 + }
  198 + } catch (Exception e) {
  199 + e.printStackTrace();
  200 + return Flux.just(AiResponseDTO.builder()
  201 + .aiText("系统异常:" + e.getMessage() + ",请稍后重试!")
  202 + .sSceneName(sceneName)
  203 + .sMethodName(methodName)
  204 + .sReturnType(ReturnTypeCode.HTML.getCode())
  205 + .build());
  206 + }finally {
  207 + //5.执行工具方法后,清除记忆
  208 + if(session !=null && session.getBCleanMemory()){
  209 + doCleanUserMemory(session,userId);
  210 + }
  211 + }
  212 + }
123 213  
124 214 /***
125 215 * @Author 钱豹
... ... @@ -278,15 +368,17 @@ public class XlyErpService {
278 368 try{
279 369 String sVectorfiled = session.getCurrentTool().getSVectorfiled();
280 370 String sInputTabelName = session.getCurrentTool().getSInputTabelName();
281   - Map<String,Object> rMap = milvusService.getMilvusFiled(sVectorfiled);
  371 + String sVectorfiledAll = session.getCurrentTool().getSVectorfiledAll();
  372 + Map<String,Object> rMap = milvusService.getMilvusFiled(sVectorfiled,sVectorfiledAll);
282 373 String sMilvusFiled = rMap.get("sMilvusFiled").toString();
283 374 String sMilvusFiledDescription = rMap.get("sMilvusFiledDescription").toString();
  375 + String sMilvusFiledDescriptionAll = rMap.get("sMilvusFiledDescriptionAll").toString();
284 376 List<String> fields = (List<String>) rMap.get("sFileds");
285 377 // List<Map<String, String>> title = (List<Map<String, String>>) rMap.get("title");
286 378 String milvusFilter = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription);
287 379 List<Map<String,Object>> data = milvusService.getDataToCollection(sInputTabelName, milvusFilter,userInput,100,fields);
288 380 //采用表格形式显示
289   - resultExplain = aiAgent.explainMilvusResult(session.getUserId(),userInput,sMilvusFiledDescription,JSONObject.toJSONString(data));
  381 + resultExplain = aiAgent.explainMilvusResult(session.getUserId(),userInput,sMilvusFiledDescriptionAll,JSONObject.toJSONString(data));
290 382 //buildMarkdownTableWithStream(data, title);
291 383 return resultExplain;
292 384 }catch (Exception e){
... ... @@ -778,6 +870,40 @@ public class XlyErpService {
778 870 return sb.toString();
779 871 }
780 872  
  873 + /**
  874 + * @Author 钱豹
  875 + * @Date 13:32 2026/2/6
  876 + * @Param [input, session]
  877 + * @return reactor.core.publisher.Flux<AiResponseDTO>
  878 + * @Description 获取智普通智能体(流式返回)
  879 + **/
  880 + private Flux<AiResponseDTO> getChatiAgentStream(String input, UserSceneSession session) {
  881 + String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())
  882 + ? session.getCurrentScene().getSSceneName() : StrUtil.EMPTY;
  883 + String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())
  884 + ? session.getCurrentTool().getSMethodName() : "随便聊聊";
  885 +
  886 + // 从缓存获取或创建ChatiAgent
  887 + ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId());
  888 + if (ObjectUtil.isEmpty(chatiAgent)) {
  889 + chatiAgent = AiServices.builder(ChatiAgent.class)
  890 + .chatLanguageModel(chatiModel)
  891 + .chatMemoryProvider(operableChatMemoryProvider)
  892 + .build();
  893 + UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent);
  894 + }
  895 +
  896 + // 调用流式聊天方法
  897 + return chatiAgent.chatStream(session.getUserId(), input)
  898 + .map(chunk -> AiResponseDTO.builder()
  899 + .sSceneName(sceneName)
  900 + .sMethodName(methodName)
  901 + .aiText(chunk)
  902 + .systemText(StrUtil.EMPTY)
  903 + .sReturnType(ReturnTypeCode.STREAM.getCode())
  904 + .build());
  905 + }
  906 +
781 907 /***
782 908 * @Author 钱豹
783 909 * @Date 13:32 2026/2/6
... ...
src/main/java/com/xly/tts/service/PythonTtsProxyService.java
... ... @@ -84,8 +84,6 @@ public class PythonTtsProxyService {
84 84  
85 85 /**
86 86 * 流式ERP + 流式TTS合成
87   - * 先流式输出ERP文本,完成后自动开始TTS合成
88   - * 使用现有TTSResponseDTO字段
89 87 */
90 88 public Flux<TTSResponseDTO> synthesizeStreamAiStream(TTSRequestDTO request) {
91 89 String userInput = request.getText();
... ... @@ -95,95 +93,121 @@ public class PythonTtsProxyService {
95 93 String sSubsidiaryId = request.getSubsidiaryid();
96 94 String sUserType = request.getUsertype();
97 95 String authorization = request.getAuthorization();
98   -
99 96 String requestId = UUID.randomUUID().toString();
100   - log.info("开始流式处理: requestId={}, userId={}", requestId, sUserId);
101 97  
102   - // 创建累积器(用于累积完整的AiResponseDTO)
103   - AiResponseAccumulator accumulator = new AiResponseAccumulator(requestId);
  98 + log.info("开始流式处理: requestId={}, userId={}", requestId, sUserId);
104 99  
105 100 // 1. 处理ERP流,将AiResponseDTO转换为TTSResponseDTO
106 101 Flux<TTSResponseDTO> erpStream = xlyErpService.erpUserInputStream(
107 102 userInput, sUserId, sUserName, sBrandsId,
108 103 sSubsidiaryId, sUserType, authorization
109 104 )
110   - .doOnNext(aiResponse -> {
111   - // 设置请求ID
112   - aiResponse.setRequestId(requestId);
113   - // 后台累积完整文本(为后续TTS做准备)
114   - accumulator.accumulate(aiResponse);
115   - log.debug("收到ERP片段: requestId={}, chunk={}/{}",
116   - requestId,
117   - aiResponse.getChunkIndex(),
118   - aiResponse.getTotalChunks());
119   - })
120 105 .map(aiResponse -> {
121 106 // 将AiResponseDTO转换为TTSResponseDTO
122   - // 使用processedText字段传递AI文本片段
123   - // 使用systemText字段传递系统文本片段
124   - return TTSResponseDTO.builder()
  107 + TTSResponseDTO dto = TTSResponseDTO.builder()
125 108 .code(200)
126   - .message("ERP_CHUNK") // message字段标记为ERP文本块
  109 + .message("ERP_CHUNK")
127 110 .requestId(requestId)
128   - .processedText(aiResponse.getTextFragment()) // 用processedText传递AI文本片段
129   - .systemText(aiResponse.getSystemTextFragment()) // 用systemText传递系统文本片段
  111 + .processedText(aiResponse.getAiText()) // 使用 aiText 字段传递文本
  112 + .systemText(aiResponse.getSystemText())
130 113 .sSceneName(aiResponse.getSSceneName())
131 114 .sMethodName(aiResponse.getSMethodName())
132 115 .sReturnType(aiResponse.getSReturnType())
133 116 .timestamp(System.currentTimeMillis())
134 117 .build();
  118 +
  119 + log.debug("发送ERP片段: requestId={}, text长度={}", requestId, aiResponse.getAiText() != null ? aiResponse.getAiText().length() : 0);
  120 + return dto;
  121 + });
  122 +
  123 + // 2. 收集完整的ERP响应(用于TTS)
  124 + List<String> textChunks = new ArrayList<>();
  125 + List<String> systemTextChunks = new ArrayList<>();
  126 + Flux<TTSResponseDTO> erpWithCollect = erpStream
  127 + .doOnNext(dto -> {
  128 + // 收集文本片段
  129 + if (dto.getProcessedText() != null) {
  130 + textChunks.add(dto.getProcessedText());
  131 + }
  132 + if (dto.getSystemText() != null) {
  133 + systemTextChunks.add(dto.getSystemText());
  134 + }
135 135 });
136 136  
137   - // 2. ERP完成后,发送完成标记,然后开始TTS合成
138   - return erpStream
  137 + // 3. ERP完成后,发送完成标记并开始TTS
  138 + return erpWithCollect
139 139 .concatWith(Flux.defer(() -> {
140   - // 获取完整的累积结果
141   - AiResponseDTO completeResponse = accumulator.getCompleteResponse();
  140 + // 合并所有文本片段
  141 + String fullText = String.join("", textChunks);
  142 + String fullSystemText = String.join("", systemTextChunks);
142 143  
143   - // 验证ERP结果
144   - if (StrUtil.isBlank(completeResponse.getAiText())) {
  144 + if (StrUtil.isBlank(fullText)) {
145 145 log.warn("ERP返回空文本: requestId={}", requestId);
146   - return Flux.error(new RuntimeException("ERP返回空文本"));
  146 + return Flux.just(TTSResponseDTO.error(requestId, 500, "ERP返回空文本"));
147 147 }
148 148  
149   - log.info("ERP流式处理完成,开始TTS合成: requestId={}, aiText长度={}",
150   - requestId, completeResponse.getAiText().length());
  149 + log.info("ERP流式处理完成,开始TTS合成: requestId={}, 文本长度={}",
  150 + requestId, fullText.length());
151 151  
152   - // 3. 发送ERP完成消息(使用完整文本)
153   - TTSResponseDTO erpCompleteDto = TTSResponseDTO.builder()
  152 + // 发送ERP完成消息
  153 + TTSResponseDTO completeDto = TTSResponseDTO.builder()
154 154 .code(200)
155   - .message("ERP_COMPLETE") // message标记完成
  155 + .message("ERP_COMPLETE")
156 156 .requestId(requestId)
157   - .processedText(completeResponse.getAiText()) // 完整AI文本
158   - .systemText(completeResponse.getSystemText()) // 完整系统文本
159   - .sSceneName(completeResponse.getSSceneName())
160   - .sMethodName(completeResponse.getSMethodName())
161   - .sReturnType(completeResponse.getSReturnType())
  157 +// .processedText(fullText)
  158 +// .systemText(fullSystemText)
162 159 .timestamp(System.currentTimeMillis())
163 160 .build();
  161 +// AiResponseDTO aiResponseDTO = AiResponseDTO.builder().aiText(fullText).systemText(fullSystemText);
164 162  
165   - // 4. 调用TTS合成(返回TTSResponseDTO流)
166   - Flux<TTSResponseDTO> ttsStream = synthesizeStreamAiNew(request, completeResponse)
167   - .doOnNext(ttsResponse -> {
168   - ttsResponse.setRequestId(requestId);
169   - ttsResponse.setMessage("TTS_SEGMENT"); // message标记为TTS音频段
170   - });
171   -
172   - // 先发送ERP完成消息,再发送TTS流
173   - return Flux.concat(Flux.just(erpCompleteDto), ttsStream);
  163 + // 调用TTS合成
  164 +// synthesizeStreamAi(request, aiResponseDTO);
  165 + // 先发送完成消息,再发送TTS流
  166 + return Flux.just(completeDto);
  167 + //Flux.concat(Flux.just(completeDto), ttsStream);
174 168 }))
175   - // 超时控制
176 169 .timeout(Duration.ofSeconds(120))
177   - // 错误处理
178 170 .onErrorResume(e -> {
179   - log.error("流式处理失败: requestId={}, error={}", requestId, e.getMessage());
  171 + log.error("流式处理失败: requestId={}", requestId, e);
180 172 return Flux.just(TTSResponseDTO.error(requestId, 500, e.getMessage()));
181 173 })
182   - // 日志记录
183 174 .doOnComplete(() -> log.info("流式处理完成: requestId={}", requestId))
184 175 .doOnCancel(() -> log.warn("流式处理取消: requestId={}", requestId));
185 176 }
186 177  
  178 +// /**
  179 +// * 调用TTS服务进行语音合成(流式返回)
  180 +// */
  181 +// private Flux<TTSResponseDTO> synthesizeTtsStream(TTSRequestDTO originalRequest,
  182 +// String text,
  183 +// String requestId) {
  184 +// // 构建TTS请求
  185 +// TTSRequestDTO ttsRequest = TTSRequestDTO.builder()
  186 +// .userid(originalRequest.getUserid())
  187 +// .text(text)
  188 +// .voice(originalRequest.getVoice())
  189 +// .rate(originalRequest.getRate())
  190 +// .volume(originalRequest.getVolume())
  191 +// .voiceless(originalRequest.getVoiceless())
  192 +// .build();
  193 +//
  194 +// // 调用Python TTS服务
  195 +// return webClient.post()
  196 +// .uri("http://python-service:5000/api/tts/synthesize")
  197 +// .contentType(MediaType.APPLICATION_JSON)
  198 +// .bodyValue(ttsRequest)
  199 +// .retrieve()
  200 +// .bodyToFlux(TTSResponseDTO.class)
  201 +// .doOnNext(ttsResponse -> {
  202 +// ttsResponse.setRequestId(requestId);
  203 +// ttsResponse.setMessage("TTS_SEGMENT");
  204 +// log.debug("发送TTS片段: requestId={}, audioSize={}",
  205 +// requestId,
  206 +// ttsResponse.getAudio() != null ? ttsResponse.getAudio().length() : 0);
  207 +// });
  208 +// }
  209 +
  210 +
187 211  
188 212  
189 213  
... ...
src/main/java/com/xly/web/TTSStreamController.java
... ... @@ -85,24 +85,18 @@ public class TTSStreamController {
85 85 return pythonTtsProxyService.synthesizeStreamAi(request);
86 86 }
87 87  
88   - /**
89   - * 流式合成语音(代理到Python服务)
90   - */
91 88 @PostMapping(value = "/stream/queryFlux",
92   - consumes = {MediaType.APPLICATION_JSON_VALUE},
93   - produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE,
94   - MediaType.APPLICATION_JSON_VALUE})
95   - public Flux<TTSResponseDTO> streamFlux(@Valid @RequestBody Mono<TTSRequestDTO> requestMono) {
96   - return requestMono.flatMapMany(request -> {
97   - log.info("处理请求: requestId={}, text长度={}", request.getUserid(), request.getText().length());
98   - return pythonTtsProxyService.synthesizeStreamAiStream(request);
99   - })
  89 + consumes = MediaType.APPLICATION_JSON_VALUE,
  90 + produces = {MediaType.APPLICATION_NDJSON_VALUE, MediaType.APPLICATION_JSON_VALUE})
  91 + public Flux<TTSResponseDTO> streamFlux(@Valid @RequestBody TTSRequestDTO request) {
  92 + return pythonTtsProxyService.synthesizeStreamAiStream(request)
100 93 .doOnSubscribe(sub -> log.debug("流式订阅开始"))
101 94 .doOnCancel(() -> log.debug("流式请求被取消"))
102 95 .doOnComplete(() -> log.debug("流式响应完成"))
103 96 .doOnError(e -> log.error("流式处理错误", e));
104 97 }
105 98  
  99 +
106 100 @GetMapping("/audio/piece")
107 101 public ResponseEntity<Map<String, String>> getPiece(
108 102 @RequestParam String cacheKey,
... ...
src/main/resources/templates/chat.html
... ... @@ -594,6 +594,7 @@
594 594 checkPiece();
595 595 }
596 596  
  597 + // 修改 doMessage 函数,改为调用新的流式接口
597 598 async function doMessage(input, message, button) {
598 599 addMessage(message, 'user');
599 600 showTypingIndicator();
... ... @@ -602,6 +603,9 @@
602 603 const requestData = {
603 604 text: message,
604 605 userid: userid,
  606 + username: username,
  607 + brandsid: brandsid,
  608 + subsidiaryid: subsidiaryid,
605 609 usertype: usertype,
606 610 authorization: authorization,
607 611 voice: "zh-CN-XiaoxiaoNeural",
... ... @@ -610,26 +614,173 @@
610 614 voiceless: true
611 615 };
612 616  
613   - const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/query`, {
  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`, {
614 647 method: "POST",
615   - headers: { "Content-Type": "application/json;charset=UTF-8" },
  648 + headers: {
  649 + "Content-Type": "application/json;charset=UTF-8",
  650 + "Accept": "application/x-ndjson, application/json"
  651 + },
616 652 body: JSON.stringify(requestData)
617 653 });
618 654  
619   - const data = await response.json();
620   - hideTypingIndicator();
621   - const replyText = (data.processedText || "") + (data.systemText || "");
622   - addMessage(replyText, 'ai');
  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 + }
623 736  
624   - const cacheKey = data.cacheKey;
625   - const audioSize = data.audioSize; // 总分几段
  737 + } catch (e) {
  738 + console.error('解析流式数据失败:', e, line);
  739 + }
  740 + }
  741 + }
626 742  
  743 + // 流式结束后,处理可能的音频播放
  744 + if (cacheKey && audioSize > 0) {
  745 + playByIndex(cacheKey, 0, audioSize);
  746 + }
627 747  
628   - playByIndex(cacheKey, 0, audioSize);
  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 + });
629 779  
630 780 } catch (error) {
631 781 console.error('错误:', error);
632 782 hideTypingIndicator();
  783 + $(`#temp-${Date.now()}`).remove();
633 784 addMessage("服务异常,请重试", 'ai');
634 785 } finally {
635 786 input.prop('disabled', false);
... ... @@ -639,18 +790,10 @@
639 790 }
640 791 }
641 792  
642   - function base64ToBlob(base64) {
643   - const byteCharacters = atob(base64);
644   - const byteNumbers = new Array(byteCharacters.length);
645   - for (let i = 0; i < byteCharacters.length; i++) {
646   - byteNumbers[i] = byteCharacters.charCodeAt(i);
647   - }
648   - return new Blob([new Uint8Array(byteNumbers)], { type: 'audio/mpeg' });
649   - }
650   -
  793 + // 修改原有的 handleNormalResponse 函数(如果需要的话)
651 794 async function handleNormalResponse(requestData) {
652 795 try {
653   - const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/query`, {
  796 + const response = await fetch(`${CONFIG.backendUrl}/api/tts/stream/queryFlux`, {
654 797 method: 'POST',
655 798 headers: CONFIG.headers,
656 799 body: JSON.stringify(requestData)
... ... @@ -658,6 +801,8 @@
658 801 if (!response.ok) {
659 802 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
660 803 }
  804 + // 流式响应不需要在这里处理,由 doMessage 处理
  805 + return response;
661 806 } catch (error) {
662 807 hideTypingIndicator();
663 808 throw error;
... ... @@ -666,6 +811,15 @@
666 811 }
667 812 }
668 813  
  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 +
669 823 function getCurrentTime() {
670 824 const now = new Date();
671 825 return now.getHours().toString().padStart(2, '0') + ':' +
... ...