Commit 11f61c6c7c007ca2267a119b1784a41cf5052d94
1 parent
cff9d5c5
添加向量库
Showing
11 changed files
with
617 additions
and
148 deletions
pom.xml
| @@ -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
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') + ':' + |