Commit 11f61c6c7c007ca2267a119b1784a41cf5052d94

Authored by qianbao
1 parent cff9d5c5

添加向量库

@@ -40,6 +40,7 @@ @@ -40,6 +40,7 @@
40 <json-schema.version>1.17.2</json-schema.version> 40 <json-schema.version>1.17.2</json-schema.version>
41 <!-- 向量数据 --> 41 <!-- 向量数据 -->
42 <milvus.version>2.6.15</milvus.version> 42 <milvus.version>2.6.15</milvus.version>
  43 + <tess4j.version>5.18.0</tess4j.version>
43 </properties> 44 </properties>
44 45
45 <dependencies> 46 <dependencies>
@@ -106,6 +107,13 @@ @@ -106,6 +107,13 @@
106 </dependency> 107 </dependency>
107 108
108 <dependency> 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 <groupId>org.springframework.boot</groupId> 117 <groupId>org.springframework.boot</groupId>
110 <artifactId>spring-boot-starter-thymeleaf</artifactId> 118 <artifactId>spring-boot-starter-thymeleaf</artifactId>
111 </dependency> 119 </dependency>
src/main/java/com/xly/agent/ChatiAgent.java
@@ -4,6 +4,7 @@ import dev.langchain4j.service.MemoryId; @@ -4,6 +4,7 @@ import dev.langchain4j.service.MemoryId;
4 import dev.langchain4j.service.SystemMessage; 4 import dev.langchain4j.service.SystemMessage;
5 import dev.langchain4j.service.UserMessage; 5 import dev.langchain4j.service.UserMessage;
6 import dev.langchain4j.service.V; 6 import dev.langchain4j.service.V;
  7 +import reactor.core.publisher.Flux;
7 8
8 public interface ChatiAgent { 9 public interface ChatiAgent {
9 @SystemMessage(""" 10 @SystemMessage("""
@@ -16,4 +17,15 @@ public interface ChatiAgent { @@ -16,4 +17,15 @@ public interface ChatiAgent {
16 """) 17 """)
17 @UserMessage("用户说:{{userInput}}") 18 @UserMessage("用户说:{{userInput}}")
18 String chat(@MemoryId String userId, @V("userInput") String userInput); 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,6 +14,7 @@ public enum ReturnTypeCode {
14 14
15 // 成功 15 // 成功
16 HTML("html", "html"), 16 HTML("html", "html"),
  17 + STREAM("stream", "stream"),
17 MAKEDOWN("makedown", "makedown"); 18 MAKEDOWN("makedown", "makedown");
18 19
19 20
src/main/java/com/xly/entity/ToolMeta.java
@@ -49,5 +49,6 @@ public class ToolMeta { @@ -49,5 +49,6 @@ public class ToolMeta {
49 private LocalDateTime tMakeDate; 49 private LocalDateTime tMakeDate;
50 private String sVectorfiled; 50 private String sVectorfiled;
51 private String sVectorjson; 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,5 +54,5 @@ public interface MilvusService {
54 * @return java.util.Map<java.lang.String,java.lang.Object> 54 * @return java.util.Map<java.lang.String,java.lang.Object>
55 * @Description 获取配置 55 * @Description 获取配置
56 **/ 56 **/
57 - Map<String,Object> getMilvusFiled(String sVectorfiled); 57 + Map<String,Object> getMilvusFiled(String sVectorfiled,String sVectorfiledAll);
58 } 58 }
59 \ No newline at end of file 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,6 +11,7 @@ import com.google.gson.JsonObject;
11 import com.xly.milvus.service.MilvusService; 11 import com.xly.milvus.service.MilvusService;
12 import com.xly.milvus.service.VectorizationService; 12 import com.xly.milvus.service.VectorizationService;
13 import com.xly.milvus.util.MapToJsonConverter; 13 import com.xly.milvus.util.MapToJsonConverter;
  14 +import com.xly.milvus.util.MilvusTimeUtil;
14 import com.xly.service.DynamicExeDbService; 15 import com.xly.service.DynamicExeDbService;
15 import com.xly.tts.bean.TTSResponseDTO; 16 import com.xly.tts.bean.TTSResponseDTO;
16 import io.milvus.response.SearchResultsWrapper; 17 import io.milvus.response.SearchResultsWrapper;
@@ -33,6 +34,10 @@ import org.springframework.beans.factory.annotation.Value; @@ -33,6 +34,10 @@ import org.springframework.beans.factory.annotation.Value;
33 import org.springframework.stereotype.Service; 34 import org.springframework.stereotype.Service;
34 35
35 import java.math.BigDecimal; 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 import java.util.*; 41 import java.util.*;
37 import java.util.regex.Matcher; 42 import java.util.regex.Matcher;
38 import java.util.regex.Pattern; 43 import java.util.regex.Pattern;
@@ -49,7 +54,10 @@ public class MilvusServiceImpl implements MilvusService { @@ -49,7 +54,10 @@ public class MilvusServiceImpl implements MilvusService {
49 private final MilvusClientV2 milvusClient; 54 private final MilvusClientV2 milvusClient;
50 private final VectorizationService vectorizationService; 55 private final VectorizationService vectorizationService;
51 private final DynamicExeDbService dynamicExeDbService; 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 @Value("${milvus.vector.dimension:384}") 62 @Value("${milvus.vector.dimension:384}")
55 private int VECTOR_DIM; 63 private int VECTOR_DIM;
@@ -90,13 +98,12 @@ public class MilvusServiceImpl implements MilvusService { @@ -90,13 +98,12 @@ public class MilvusServiceImpl implements MilvusService {
90 String sInputTabelName = reqMap.get("sInputTabelName").toString(); 98 String sInputTabelName = reqMap.get("sInputTabelName").toString();
91 String sVectorfiled = reqMap.get("sVectorfiled").toString(); 99 String sVectorfiled = reqMap.get("sVectorfiled").toString();
92 String sVectorjson = reqMap.get("sVectorjson").toString(); 100 String sVectorjson = reqMap.get("sVectorjson").toString();
93 - 101 + //创建集合
  102 + createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true);
94 String tUpdateDate = DateUtil.now(); 103 String tUpdateDate = DateUtil.now();
95 String tUpdateDateUp = getUpdateDateUp(sInputTabelName); 104 String tUpdateDateUp = getUpdateDateUp(sInputTabelName);
96 //获取需要同步地数据 105 //获取需要同步地数据
97 List<Map<String,Object>> data = getAddData(sInputTabelName,tUpdateDate, tUpdateDateUp); 106 List<Map<String,Object>> data = getAddData(sInputTabelName,tUpdateDate, tUpdateDateUp);
98 - //创建集合  
99 - createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true);  
100 if(ObjectUtil.isNotEmpty(data)){ 107 if(ObjectUtil.isNotEmpty(data)){
101 //插入数据 108 //插入数据
102 long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data); 109 long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data);
@@ -257,11 +264,12 @@ public class MilvusServiceImpl implements MilvusService { @@ -257,11 +264,12 @@ public class MilvusServiceImpl implements MilvusService {
257 * @Description 返回组装动态内容 264 * @Description 返回组装动态内容
258 **/ 265 **/
259 @Override 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 List<String> sFileds = new ArrayList<>(); 268 List<String> sFileds = new ArrayList<>();
263 List<String> sFiledDescriptions = new ArrayList<>(); 269 List<String> sFiledDescriptions = new ArrayList<>();
  270 + List<String> sFiledDescriptionsAll = new ArrayList<>();
264 List<Map<String,String>> titleList = new LinkedList<>(); 271 List<Map<String,String>> titleList = new LinkedList<>();
  272 + String[] sVectorfiledArray = sVectorfiled.split(",");
265 for(String sVectorfiledOne : sVectorfiledArray){ 273 for(String sVectorfiledOne : sVectorfiledArray){
266 Map<String,String> title = new HashMap<>(); 274 Map<String,String> title = new HashMap<>();
267 275
@@ -278,9 +286,21 @@ public class MilvusServiceImpl implements MilvusService { @@ -278,9 +286,21 @@ public class MilvusServiceImpl implements MilvusService {
278 title.put("sTitle",sDescriptions); 286 title.put("sTitle",sDescriptions);
279 titleList.add(title); 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 Map<String,Object> rMap = new HashMap<>(); 300 Map<String,Object> rMap = new HashMap<>();
282 rMap.put("sMilvusFiled", String.join(",", sFileds)); 301 rMap.put("sMilvusFiled", String.join(",", sFileds));
283 rMap.put("sMilvusFiledDescription", String.join(",", sFiledDescriptions)); 302 rMap.put("sMilvusFiledDescription", String.join(",", sFiledDescriptions));
  303 + rMap.put("sMilvusFiledDescriptionAll", String.join(",", sFiledDescriptionsAll));
284 rMap.put("sFileds", sFileds); 304 rMap.put("sFileds", sFileds);
285 rMap.put("title", titleList); 305 rMap.put("title", titleList);
286 return rMap; 306 return rMap;
@@ -430,17 +450,17 @@ public class MilvusServiceImpl implements MilvusService { @@ -430,17 +450,17 @@ public class MilvusServiceImpl implements MilvusService {
430 for (List<SearchResp.SearchResult> resultList : searchResults) { 450 for (List<SearchResp.SearchResult> resultList : searchResults) {
431 // 遍历每个搜索结果 451 // 遍历每个搜索结果
432 for (SearchResp.SearchResult result : resultList) { 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 Float score = result.getScore(); 457 Float score = result.getScore();
436 if (score != null) { 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 log.info("处理完成,共 {} 条搜索结果", results.size()); 466 log.info("处理完成,共 {} 条搜索结果", results.size());
@@ -548,27 +568,75 @@ public class MilvusServiceImpl implements MilvusService { @@ -548,27 +568,75 @@ public class MilvusServiceImpl implements MilvusService {
548 return; 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 row.addProperty(fieldName, (String) value); 579 row.addProperty(fieldName, (String) value);
553 } else if (value instanceof Number) { 580 } else if (value instanceof Number) {
554 row.addProperty(fieldName, (Number) value); 581 row.addProperty(fieldName, (Number) value);
555 } else if (value instanceof Boolean) { 582 } else if (value instanceof Boolean) {
556 row.addProperty(fieldName, (Boolean) value); 583 row.addProperty(fieldName, (Boolean) value);
557 - }  
558 - // List / 数组类型  
559 - else if (value instanceof List<?>) { 584 + }else if (value instanceof List<?>) {
  585 + // List / 数组类型
560 JsonArray jsonArray = new JsonArray(); 586 JsonArray jsonArray = new JsonArray();
561 for (Object item : (List<?>) value) { 587 for (Object item : (List<?>) value) {
562 addJsonElement(jsonArray, item); 588 addJsonElement(jsonArray, item);
563 } 589 }
564 row.add(fieldName, jsonArray); 590 row.add(fieldName, jsonArray);
565 - }  
566 - // 其他对象转字符串  
567 - else { 591 + } else {
  592 + // 其他对象转字符串
568 row.addProperty(fieldName, value.toString()); 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 * 递归处理 List 元素(支持无限层嵌套 List) 640 * 递归处理 List 元素(支持无限层嵌套 List)
573 */ 641 */
574 private void addJsonElement(JsonArray jsonArray, Object item) { 642 private void addJsonElement(JsonArray jsonArray, Object item) {
@@ -605,12 +673,11 @@ public class MilvusServiceImpl implements MilvusService { @@ -605,12 +673,11 @@ public class MilvusServiceImpl implements MilvusService {
605 673
606 //是否删除集合 重新创建 674 //是否删除集合 重新创建
607 if (bRset){ 675 if (bRset){
  676 + this.delAiMilvusVectorRecord(collectionName);
608 // 1. 删除旧集合 677 // 1. 删除旧集合
609 milvusClient.dropCollection(DropCollectionReq.builder() 678 milvusClient.dropCollection(DropCollectionReq.builder()
610 .collectionName(collectionName) 679 .collectionName(collectionName)
611 .build()); 680 .build());
612 - //删除对应的记录表  
613 - delAiMilvusVectorRecord(collectionName);  
614 } 681 }
615 // 检查集合是否存在 682 // 检查集合是否存在
616 HasCollectionReq hasCollectionReq = HasCollectionReq.builder() 683 HasCollectionReq hasCollectionReq = HasCollectionReq.builder()
@@ -777,9 +844,11 @@ public class MilvusServiceImpl implements MilvusService { @@ -777,9 +844,11 @@ public class MilvusServiceImpl implements MilvusService {
777 if(sKey.startsWith("d")){ 844 if(sKey.startsWith("d")){
778 return DataType.Double; 845 return DataType.Double;
779 }else if(sKey.startsWith("i")){ 846 }else if(sKey.startsWith("i")){
780 - return DataType.Int64; 847 + return DataType.Int32;
781 }else if(sKey.startsWith("b")){ 848 }else if(sKey.startsWith("b")){
782 return DataType.Bool; 849 return DataType.Bool;
  850 + }else if(sKey.startsWith("t")){
  851 + return DataType.Int64;
783 }else{ 852 }else{
784 return DataType.VarChar; 853 return DataType.VarChar;
785 } 854 }
@@ -793,7 +862,14 @@ public class MilvusServiceImpl implements MilvusService { @@ -793,7 +862,14 @@ public class MilvusServiceImpl implements MilvusService {
793 * @Description 索引类型 862 * @Description 索引类型
794 **/ 863 **/
795 public IndexParam.IndexType indexField(String sKey) { 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 \ No newline at end of file 74 \ No newline at end of file
src/main/java/com/xly/service/XlyErpService.java
@@ -79,47 +79,137 @@ public class XlyErpService { @@ -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 * @Author 钱豹 215 * @Author 钱豹
@@ -278,15 +368,17 @@ public class XlyErpService { @@ -278,15 +368,17 @@ public class XlyErpService {
278 try{ 368 try{
279 String sVectorfiled = session.getCurrentTool().getSVectorfiled(); 369 String sVectorfiled = session.getCurrentTool().getSVectorfiled();
280 String sInputTabelName = session.getCurrentTool().getSInputTabelName(); 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 String sMilvusFiled = rMap.get("sMilvusFiled").toString(); 373 String sMilvusFiled = rMap.get("sMilvusFiled").toString();
283 String sMilvusFiledDescription = rMap.get("sMilvusFiledDescription").toString(); 374 String sMilvusFiledDescription = rMap.get("sMilvusFiledDescription").toString();
  375 + String sMilvusFiledDescriptionAll = rMap.get("sMilvusFiledDescriptionAll").toString();
284 List<String> fields = (List<String>) rMap.get("sFileds"); 376 List<String> fields = (List<String>) rMap.get("sFileds");
285 // List<Map<String, String>> title = (List<Map<String, String>>) rMap.get("title"); 377 // List<Map<String, String>> title = (List<Map<String, String>>) rMap.get("title");
286 String milvusFilter = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription); 378 String milvusFilter = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription);
287 List<Map<String,Object>> data = milvusService.getDataToCollection(sInputTabelName, milvusFilter,userInput,100,fields); 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 //buildMarkdownTableWithStream(data, title); 382 //buildMarkdownTableWithStream(data, title);
291 return resultExplain; 383 return resultExplain;
292 }catch (Exception e){ 384 }catch (Exception e){
@@ -778,6 +870,40 @@ public class XlyErpService { @@ -778,6 +870,40 @@ public class XlyErpService {
778 return sb.toString(); 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 * @Author 钱豹 908 * @Author 钱豹
783 * @Date 13:32 2026/2/6 909 * @Date 13:32 2026/2/6
src/main/java/com/xly/tts/service/PythonTtsProxyService.java
@@ -84,8 +84,6 @@ public class PythonTtsProxyService { @@ -84,8 +84,6 @@ public class PythonTtsProxyService {
84 84
85 /** 85 /**
86 * 流式ERP + 流式TTS合成 86 * 流式ERP + 流式TTS合成
87 - * 先流式输出ERP文本,完成后自动开始TTS合成  
88 - * 使用现有TTSResponseDTO字段  
89 */ 87 */
90 public Flux<TTSResponseDTO> synthesizeStreamAiStream(TTSRequestDTO request) { 88 public Flux<TTSResponseDTO> synthesizeStreamAiStream(TTSRequestDTO request) {
91 String userInput = request.getText(); 89 String userInput = request.getText();
@@ -95,95 +93,121 @@ public class PythonTtsProxyService { @@ -95,95 +93,121 @@ public class PythonTtsProxyService {
95 String sSubsidiaryId = request.getSubsidiaryid(); 93 String sSubsidiaryId = request.getSubsidiaryid();
96 String sUserType = request.getUsertype(); 94 String sUserType = request.getUsertype();
97 String authorization = request.getAuthorization(); 95 String authorization = request.getAuthorization();
98 -  
99 String requestId = UUID.randomUUID().toString(); 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 // 1. 处理ERP流,将AiResponseDTO转换为TTSResponseDTO 100 // 1. 处理ERP流,将AiResponseDTO转换为TTSResponseDTO
106 Flux<TTSResponseDTO> erpStream = xlyErpService.erpUserInputStream( 101 Flux<TTSResponseDTO> erpStream = xlyErpService.erpUserInputStream(
107 userInput, sUserId, sUserName, sBrandsId, 102 userInput, sUserId, sUserName, sBrandsId,
108 sSubsidiaryId, sUserType, authorization 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 .map(aiResponse -> { 105 .map(aiResponse -> {
121 // 将AiResponseDTO转换为TTSResponseDTO 106 // 将AiResponseDTO转换为TTSResponseDTO
122 - // 使用processedText字段传递AI文本片段  
123 - // 使用systemText字段传递系统文本片段  
124 - return TTSResponseDTO.builder() 107 + TTSResponseDTO dto = TTSResponseDTO.builder()
125 .code(200) 108 .code(200)
126 - .message("ERP_CHUNK") // message字段标记为ERP文本块 109 + .message("ERP_CHUNK")
127 .requestId(requestId) 110 .requestId(requestId)
128 - .processedText(aiResponse.getTextFragment()) // 用processedText传递AI文本片段  
129 - .systemText(aiResponse.getSystemTextFragment()) // 用systemText传递系统文本片段 111 + .processedText(aiResponse.getAiText()) // 使用 aiText 字段传递文本
  112 + .systemText(aiResponse.getSystemText())
130 .sSceneName(aiResponse.getSSceneName()) 113 .sSceneName(aiResponse.getSSceneName())
131 .sMethodName(aiResponse.getSMethodName()) 114 .sMethodName(aiResponse.getSMethodName())
132 .sReturnType(aiResponse.getSReturnType()) 115 .sReturnType(aiResponse.getSReturnType())
133 .timestamp(System.currentTimeMillis()) 116 .timestamp(System.currentTimeMillis())
134 .build(); 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 .concatWith(Flux.defer(() -> { 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 log.warn("ERP返回空文本: requestId={}", requestId); 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 .code(200) 154 .code(200)
155 - .message("ERP_COMPLETE") // message标记完成 155 + .message("ERP_COMPLETE")
156 .requestId(requestId) 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 .timestamp(System.currentTimeMillis()) 159 .timestamp(System.currentTimeMillis())
163 .build(); 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 .timeout(Duration.ofSeconds(120)) 169 .timeout(Duration.ofSeconds(120))
177 - // 错误处理  
178 .onErrorResume(e -> { 170 .onErrorResume(e -> {
179 - log.error("流式处理失败: requestId={}, error={}", requestId, e.getMessage()); 171 + log.error("流式处理失败: requestId={}", requestId, e);
180 return Flux.just(TTSResponseDTO.error(requestId, 500, e.getMessage())); 172 return Flux.just(TTSResponseDTO.error(requestId, 500, e.getMessage()));
181 }) 173 })
182 - // 日志记录  
183 .doOnComplete(() -> log.info("流式处理完成: requestId={}", requestId)) 174 .doOnComplete(() -> log.info("流式处理完成: requestId={}", requestId))
184 .doOnCancel(() -> log.warn("流式处理取消: requestId={}", requestId)); 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,24 +85,18 @@ public class TTSStreamController {
85 return pythonTtsProxyService.synthesizeStreamAi(request); 85 return pythonTtsProxyService.synthesizeStreamAi(request);
86 } 86 }
87 87
88 - /**  
89 - * 流式合成语音(代理到Python服务)  
90 - */  
91 @PostMapping(value = "/stream/queryFlux", 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 .doOnSubscribe(sub -> log.debug("流式订阅开始")) 93 .doOnSubscribe(sub -> log.debug("流式订阅开始"))
101 .doOnCancel(() -> log.debug("流式请求被取消")) 94 .doOnCancel(() -> log.debug("流式请求被取消"))
102 .doOnComplete(() -> log.debug("流式响应完成")) 95 .doOnComplete(() -> log.debug("流式响应完成"))
103 .doOnError(e -> log.error("流式处理错误", e)); 96 .doOnError(e -> log.error("流式处理错误", e));
104 } 97 }
105 98
  99 +
106 @GetMapping("/audio/piece") 100 @GetMapping("/audio/piece")
107 public ResponseEntity<Map<String, String>> getPiece( 101 public ResponseEntity<Map<String, String>> getPiece(
108 @RequestParam String cacheKey, 102 @RequestParam String cacheKey,
src/main/resources/templates/chat.html
@@ -594,6 +594,7 @@ @@ -594,6 +594,7 @@
594 checkPiece(); 594 checkPiece();
595 } 595 }
596 596
  597 + // 修改 doMessage 函数,改为调用新的流式接口
597 async function doMessage(input, message, button) { 598 async function doMessage(input, message, button) {
598 addMessage(message, 'user'); 599 addMessage(message, 'user');
599 showTypingIndicator(); 600 showTypingIndicator();
@@ -602,6 +603,9 @@ @@ -602,6 +603,9 @@
602 const requestData = { 603 const requestData = {
603 text: message, 604 text: message,
604 userid: userid, 605 userid: userid,
  606 + username: username,
  607 + brandsid: brandsid,
  608 + subsidiaryid: subsidiaryid,
605 usertype: usertype, 609 usertype: usertype,
606 authorization: authorization, 610 authorization: authorization,
607 voice: "zh-CN-XiaoxiaoNeural", 611 voice: "zh-CN-XiaoxiaoNeural",
@@ -610,26 +614,173 @@ @@ -610,26 +614,173 @@
610 voiceless: true 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 method: "POST", 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 body: JSON.stringify(requestData) 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 } catch (error) { 780 } catch (error) {
631 console.error('错误:', error); 781 console.error('错误:', error);
632 hideTypingIndicator(); 782 hideTypingIndicator();
  783 + $(`#temp-${Date.now()}`).remove();
633 addMessage("服务异常,请重试", 'ai'); 784 addMessage("服务异常,请重试", 'ai');
634 } finally { 785 } finally {
635 input.prop('disabled', false); 786 input.prop('disabled', false);
@@ -639,18 +790,10 @@ @@ -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 async function handleNormalResponse(requestData) { 794 async function handleNormalResponse(requestData) {
652 try { 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 method: 'POST', 797 method: 'POST',
655 headers: CONFIG.headers, 798 headers: CONFIG.headers,
656 body: JSON.stringify(requestData) 799 body: JSON.stringify(requestData)
@@ -658,6 +801,8 @@ @@ -658,6 +801,8 @@
658 if (!response.ok) { 801 if (!response.ok) {
659 throw new Error(`HTTP ${response.status}: ${response.statusText}`); 802 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
660 } 803 }
  804 + // 流式响应不需要在这里处理,由 doMessage 处理
  805 + return response;
661 } catch (error) { 806 } catch (error) {
662 hideTypingIndicator(); 807 hideTypingIndicator();
663 throw error; 808 throw error;
@@ -666,6 +811,15 @@ @@ -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 function getCurrentTime() { 823 function getCurrentTime() {
670 const now = new Date(); 824 const now = new Date();
671 return now.getHours().toString().padStart(2, '0') + ':' + 825 return now.getHours().toString().padStart(2, '0') + ':' +