?Q&&k9f
zScz%GECfjoJUKsI$~1jG#0
z_kWocd$YD*7go%72cefAc%>bLqmp$3*BF3>`~GO^?!_JmvKz4>Z#?GNKG&IaT80s>
zae)$YyB5Dvn8B99%L(<%t@^!h-y#We3XVoA+IJziIhC?CuCUD{#)M>UGxP2W){o!bquQg`q&XaYYPIJvhGM5$Q|&~uHtQ0xQJjjSe>-Xr>`ttPejSfSWjj7
z;86PgHlcHKeDcoIlaH1{YnEHDp16jxdR)0n
zMWA7hIQXj$+U)dqGVXfK3V|hD+BelYIZ91>xePjJ@<|wbUMsu$%AmonA0NHNejd;`
z(n)k${uaV;VFY@F9Se1cVz}3Rs2Y!~7;e4poj4CEv<-N|sHJU?AU%zruGn$P1PVUp
zBn8XPa@P6=|MJ(oDt*9v@Qnzv=yR~HrbB@e(~xbTG?(k(B#YCQ#zuA&N-n8N^-%Y*
z*#nHhG2@PbuJT
z;5PGaY#g;-o8vC@9Iiy}>Nip}cpG3AYZ@BnllL&2w>W(yC@4;pNq=UwSCd<9%zE++
zQqg!eY%CtzWbxAez?>)#AysO7{h?kiMv~KFXX%;Q(q-IrE;<`F<3ey33@)sUOY|R#
zZ6#8B%G1*f=8o+ZU|*-Wjr1%5m(jdf&T8C7~@g*dVWpKo}>k#m#c+QmJG%78Vg
zn78C5Owl_+s>Vcsg^nX+H;3(I-@~apbFvM+FhRXKy}bsJ*eCiR>rDUc+5WtUrz^!W2O|E%)PzH
z?xcy5>Io;TNY36Ko;KQq+!>ydQ1nrIvc0oueLeJ@uISW6dPg)bkVrufN(9
z+S8;(pErEKVaFn4raH@YS$JmgOhn9j#p^4L7@%%Mq*+j~<*!A!7Y_gk^MOT3IcoM>
z!V5g&`Z8@9u@QQNLKd^`H#1q`m?)vpu6z77$5CeoC6V&E4rY
z?ByYs3u%JI!VB`rKCRnfr)Q}L>Q9;79ho5C{~A_vYQPa)o)|AjJb3>*X$_l$azKwyDgH5VSmB-}cD%3g
z5W@8hCp^y)_T3GNx1b4idl4rnRweV?^565_g65`lamKx-TZdEs7dq4HZl!<1dPl%5
z==CR~(B^fzYWKS+mcI&HbyI&;an|Gr$Zi+fP2;X>iL)=~Exq$?Q_2XOTfn^^J&TDc
zN^Vv9f5vu!K+DTmI{}w%-+Qi_x1~utN=j?XjJRKs!C&1ZFjXfmXUM~sL}7sh`J+_f
zwe-(~wa3nxbP>3F#jp!ADDT0c<8w_s6d`P}s@4m0ke*O4{9+`9&vAmk=AP%XM-g4s
z9$)2;c*d>BUpVzj(8arvojarW<%qagQKr4^D~`h*KD;1Y&5_RNFr;r)1e>^U-tn2~
z2RkP%%5Ii}ONF{!GQ`Cat#aS-1`}R6VUS+|Z)7Q25R%rP*;i)gjB^X*CbK-GbU*%C
zMJ-z1?Y{XHy@Jv6+{xMU$su*VqLK7fi+RZDnD6F0!aswsE>ppl5BAUJ0DeXh`Bz~8
z0DgkW>YTCR;nf8s04T8Uhewowf3i{l0AzkJ{{jsMuPU4K#Dks6c=%zdU%$tt%P}DV
z;29N3`G0i^{MhziFp%Iqq!j3+f{^=pUcwJCd-DQ-*nek$6X10f98W9QrlJbHh5Fh0
E8ziqH)&Kwi
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/EntController.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/EntController.java
index ef6ea9768f..ef2aa6312d 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/EntController.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/EntController.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.custom.controller.admin.ent;
+import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -7,9 +8,13 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.*;
+import cn.iocoder.yudao.module.custom.convert.ent.EntConvert;
import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntDO;
import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntInfoDO;
+import cn.iocoder.yudao.module.custom.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.custom.service.ent.EntService;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
+import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -23,10 +28,13 @@ import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.IMPORT;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - 企业")
@RestController
@@ -36,6 +44,8 @@ public class EntController {
@Resource
private EntService entService;
+ @Resource
+ private AdminUserService userService;
@PostMapping("/create")
@Operation(summary = "创建企业")
@@ -83,8 +93,16 @@ public class EntController {
@Operation(summary = "获得企业分页")
@PreAuthorize("@ss.hasPermission('custom:ent:query')")
public CommonResult> getEntPage(@Valid EntPageReqVO pageReqVO) {
+ // 获得企业分页列表
PageResult pageResult = entService.getEntPage(pageReqVO);
- return success(BeanUtils.toBean(pageResult, EntRespVO.class));
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(new PageResult<>(pageResult.getTotal()));
+ }
+ // 拼接用户名称
+ Map userMap = userService.getUserMap(
+ convertList(pageResult.getList(), EntDO::getUserId));
+ return success(new PageResult<>(EntConvert.INSTANCE.convertList(pageResult.getList(), userMap),
+ pageResult.getTotal()));
}
@PostMapping("/import-excel")
@@ -94,7 +112,14 @@ public class EntController {
@ApiAccessLog(operateType = IMPORT)
public CommonResult importEntExcel(@RequestParam("file") MultipartFile file) throws IOException {
List importList = ExcelUtils.read(file, EntImportReqVO.class);
- entService.importEntList(importList);
+ if (CollUtil.isEmpty(importList))
+ return error(ErrorCodeConstants.EMPTY_DATA);
+
+ // 拼接机构
+ Map userMap = userService.getUserMap(
+ convertList(importList, EntImportReqVO::getUserId));
+
+ entService.importEntList(EntConvert.INSTANCE.convertDeptList(importList, userMap));
return success("导入成功");
}
@@ -106,9 +131,12 @@ public class EntController {
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List list = entService.getEntPage(pageReqVO).getList();
+ // 拼接用户数据
+ Map userMap = userService.getUserMap(
+ convertList(list, EntDO::getUserId));
// 导出 Excel
ExcelUtils.write(response, "企业.xls", "数据", EntRespVO.class,
- BeanUtils.toBean(list, EntRespVO.class));
+ EntConvert.INSTANCE.convertList(list, userMap));
}
// ==================== 子表(企业信息) ====================
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java
index 31411f2ffe..8f4da417dc 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java
@@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 企业导入 Request VO")
@Data
@@ -22,10 +21,9 @@ public class EntImportReqVO {
private String remark;
@ExcelProperty("所属客户经理")
- private String manager;
+ private Long userId;
@ExcelProperty(value = "状态(0正常 1停用)")
- @NotNull(message = "状态(0正常 1停用)不能为空")
private Integer status;
}
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java
index bbb1477a40..9971584b40 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java
@@ -31,6 +31,9 @@ public class EntRespVO {
@NotNull(message = "所属客户经理不能为空")
private Long userId;
+ @Schema(description = "所属客户经理的名称")
+ private String userName;
+
@Schema(description = "状态(0正常 1停用)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
@ExcelProperty("状态(0正常 1停用)")
private Integer status;
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/convert/ent/EntConvert.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/convert/ent/EntConvert.java
new file mode 100644
index 0000000000..7dccf011dc
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/convert/ent/EntConvert.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.custom.convert.ent;
+
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntImportReqVO;
+import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntRespVO;
+import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface EntConvert {
+
+ EntConvert INSTANCE = Mappers.getMapper(EntConvert.class);
+
+ default List convertList(List list, Map userMap) {
+ return CollectionUtils.convertList(list, ent -> convert(ent, userMap.get(ent.getUserId())));
+ }
+
+ default EntRespVO convert(EntDO ent, AdminUserDO user) {
+ EntRespVO entVO = BeanUtils.toBean(ent, EntRespVO.class);
+ if (user != null) {
+ entVO.setUserName(user.getNickname());
+ }
+ return entVO;
+ }
+
+ default List convertDeptList(List importList, Map userMap) {
+ return CollectionUtils.convertList(importList, ent -> convertDept(ent, userMap.get(ent.getUserId())));
+ }
+
+
+
+ default EntDO convertDept(EntImportReqVO ent, AdminUserDO user) {
+ EntDO entVO = BeanUtils.toBean(ent, EntDO.class);
+ if (user != null) {
+ entVO.setDeptId(user.getDeptId());
+ }
+ return entVO;
+ }
+}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/enums/ErrorCodeConstants.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/enums/ErrorCodeConstants.java
index f594e27671..af3658b64d 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/enums/ErrorCodeConstants.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/enums/ErrorCodeConstants.java
@@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.custom.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-// TODO 待办:请将下面的错误码复制到 yudao-module-custom 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
-// ========== 企业 TODO 补充编号 ==========
public interface ErrorCodeConstants {
ErrorCode ENT_NOT_EXISTS = new ErrorCode(1_003_201_000, "企业不存在");
+
+ ErrorCode EMPTY_DATA = new ErrorCode(1_003_201_001, "Excel有效数据为空");
}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java
index 1b8fbe40b3..49d0d05d7c 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.custom.service.ent;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntImportReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntInfoPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntSaveReqVO;
@@ -72,7 +71,7 @@ public interface EntService {
* @param importReqVOList 导入信息列表
*/
- void importEntList(List importReqVOList) throws IOException;
+ void importEntList(List importReqVOList) throws IOException;
// ==================== 子表(企业信息) ====================
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java
index 326d967512..f4601b760b 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.custom.service.ent;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntImportReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntInfoPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntSaveReqVO;
@@ -15,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
-import java.io.IOException;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -156,8 +154,7 @@ public class EntServiceImpl implements EntService {
}
@Override
- public void importEntList(List importReqVOList) throws IOException {
- List entList = BeanUtils.toBean(importReqVOList, EntDO.class);
+ public void importEntList(List entList) {
entMapper.insertBatch(entList);
}
--
Gitee
From 17c98d13f8c2fb7162006499736ad5fa3833c387 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 15:47:41 +0800
Subject: [PATCH 28/50] =?UTF-8?q?=E4=BC=81=E4=B8=9A=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E6=A8=A1=E7=B3=8A=E6=A3=80=E7=B4=A2=EF=BC=9B=20=E5=B0=B1?=
=?UTF-8?q?=E6=98=AF=E5=AF=B9=E8=AF=9D=E5=BC=82=E6=AD=A5=E8=BE=93=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...71\347\233\256\350\257\264\346\230\216.md" | 61 ++++-
.../custom/api/fastgpt/vo/FlowResponse.java | 97 +++++++
.../controller/admin/api/ChatController.http | 2 +-
.../controller/admin/api/ChatController.java | 6 +-
.../custom/dal/mysql/ent/EntMapper.java | 4 +-
.../service/fastgpt/FastGptService.java | 238 +++++++++++++-----
6 files changed, 336 insertions(+), 72 deletions(-)
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java
diff --git "a/docs/\351\241\271\347\233\256\350\257\264\346\230\216.md" "b/docs/\351\241\271\347\233\256\350\257\264\346\230\216.md"
index 4b444539d4..af9f71a5be 100644
--- "a/docs/\351\241\271\347\233\256\350\257\264\346\230\216.md"
+++ "b/docs/\351\241\271\347\233\256\350\257\264\346\230\216.md"
@@ -9,4 +9,63 @@
3、重启项目
## 文件目录设置
-如果是本地存储,在 基础设施/文件管理/文件配置 下配置对应的目录
\ No newline at end of file
+如果是本地存储,在 基础设施/文件管理/文件配置 下配置对应的目录
+
+## 流式对话接口说明
+```
+event取值:
+answer: 返回给客户端的文本(最终会算作回答)
+fastAnswer: 指定回复返回给客户端的文本(最终会算作回答)
+toolCall: 执行工具
+toolParams: 工具参数
+toolResponse: 工具返回
+flowNodeStatus: 运行到的节点状态
+flowResponses: 节点完整响应
+updateVariables: 更新变量
+error: 报错
+
+event: flowNodeStatus
+data: {"status":"running","name":"知识库搜索"}
+
+event: fastAnswer
+data: {"role":"assistant","content":"\n好的,我这就为您查询待办事项,请稍候。\n用户是:\n机构是:"}
+
+event: workflowDuration
+data: {"durationSeconds":1.83}
+
+event: flowNodeStatus
+data: {"status":"running","name":"AI 对话"}
+
+event: answer
+data: {"role":"ai","content":"电影"}
+
+event: answer
+data: {"role":"ai","content":"《铃"}
+
+event: answer
+data: {"role":"ai","content":"芽之旅》"}
+
+event: answer
+data: {"role":"ai","content":"的导演是新"}
+
+event: answer
+data: {"role":"ai","content":"海诚。"}
+
+event: flowResponses
+data: {
+"qouteContent": "腾讯最近进军了机器人行业,这是一个值得关注的动态[690b16b7c483714cb4ab6d1b](CITE)。此外,腾讯在秋招期间招聘了1000人,使得企业规模扩大到了将近2万人[690b1a9cc483714cb4ab7170](CITE)。腾讯的总部位于深圳,目前公司规模已经达到了上万人[690b168dc483714cb4ab6ca0](CITE)。",
+"sourceList": [{
+"sourceName": "data (81).csv",
+"sourceId": "64fd3b6423aa1307b65896f6",
+"quoteList": [{
+"id": "654f2e49b64caef1d9431e8b",
+"q": "电影《铃芽之旅》的导演是谁?",
+"a": "电影《铃芽之旅》的导演是新海诚!"
+}]
+}]
+}
+
+event: answer
+data: [DONE]
+
+```
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java
new file mode 100644
index 0000000000..938911d48f
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java
@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.module.custom.api.fastgpt.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 最终节点结果解析
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class FlowResponse {
+
+ /** 搜索结果列表 */
+ private List sourceList;
+ /** 答复结果 */
+ private String content;
+
+
+ /**
+ * 引用的数据集合来源
+ */
+ @Data
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class SourceItem {
+ /** 知识库ID */
+ private String datasetId;
+ /** 集合ID */
+ private String collectionId;
+ /** 源名称 */
+ private String sourceName;
+ /** 具体引用的数据块 */
+ private List quoteList;
+
+ public SourceItem(String datasetId, String collectionId, String sourceName) {
+ this.datasetId = datasetId;
+ this.collectionId = collectionId;
+ this.sourceName = sourceName;
+ }
+
+ public void addQuoteItem(String id, String q, String a) {
+ if (quoteList == null) {
+ quoteList = new java.util.ArrayList<>();
+ }
+ QuoteItem newQuoteItem = new QuoteItem(id, q, a);
+ if(!quoteList.contains(newQuoteItem))
+ quoteList.add(new QuoteItem(id, q, a));
+ }
+ }
+
+
+ /**
+ * 引用的数据块
+ */
+ @Data
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class QuoteItem {
+ /** 数据ID */
+ private String id;
+ /** 问题文本 */
+ private String q;
+ /** 答案文本 */
+ private String a;
+
+ public QuoteItem(String id, String q, String a) {
+ this.id = id;
+ this.q = q;
+ this.a = a;
+ }
+ }
+
+ // 新增sourceItem
+ public void addSourceItem(String datasetId, String collectionId, String sourceName) {
+ SourceItem item = new SourceItem(datasetId, collectionId, sourceName);
+ addSourceItem(item);
+ }
+
+ public void addSourceItem(SourceItem sourceItem) {
+ if (sourceList == null) {
+ sourceList = new java.util.ArrayList<>();
+ }
+ if(!sourceList.contains(sourceItem))
+ sourceList.add(sourceItem);
+ }
+
+ public SourceItem getByCollectionId(String collectionId) {
+ if (sourceList != null && !sourceList.isEmpty())
+ for (SourceItem item : sourceList) {
+ if (item.getCollectionId().equals(collectionId)) {
+ return item;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
index 71bf5a970c..b9f58f74ba 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
@@ -1,5 +1,5 @@
### 流式对话接口测试
-POST {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_12345&dataId=12345&userContent=腾讯公司最近发生了什么
+GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_12345&dataId=12345&userContent=腾讯公司最近发生了什么
Content-Type: application/json
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
index 66b5f8ab11..49c6de1ac0 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
@@ -39,7 +39,7 @@ public class ChatController {
private final ChatService chatService;
@Operation(summary = "与fastgpt进行流式对话")
- @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Parameter(name = "category", description = "对话流程类型", required = true)
@Parameter(name = "chatId", description = "每一个新对话的唯一id", required = true)
@Parameter(name = "dataId", description = "每轮对话的id", required = true)
@@ -76,7 +76,9 @@ public class ChatController {
} catch (Exception e) {
log.error("保存助手回复失败", e);
}
- });
+ })
+ .onBackpressureBuffer() // 添加背压缓冲
+ .doOnNext(text -> log.info("发送 SSE 数据: {}", text)); // 添加日志
}
@Operation(summary = "获取用户对话记录列表")
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/mysql/ent/EntMapper.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/mysql/ent/EntMapper.java
index 9ef92d1f23..23460fd841 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/mysql/ent/EntMapper.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/mysql/ent/EntMapper.java
@@ -20,8 +20,8 @@ public interface EntMapper extends BaseMapperX {
default PageResult selectPage(EntPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX()
.likeIfPresent(EntDO::getName, reqVO.getName())
- .eqIfPresent(EntDO::getIndustry, reqVO.getIndustry())
- .eqIfPresent(EntDO::getRemark, reqVO.getRemark())
+ .likeIfPresent(EntDO::getIndustry, reqVO.getIndustry())
+ .likeIfPresent(EntDO::getRemark, reqVO.getRemark())
.eqIfPresent(EntDO::getStatus, reqVO.getStatus())
.orderByDesc(EntDO::getId));
}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index 05d849a92e..39d2dbc623 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.custom.api.fastgpt.FastGptAuthInterceptor;
import cn.iocoder.yudao.module.custom.api.fastgpt.FastGptDataApi;
import cn.iocoder.yudao.module.custom.api.fastgpt.vo.*;
+import com.alibaba.druid.support.json.JSONUtils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -168,6 +169,18 @@ public class FastGptService {
* @return FastGptDataApi实例
*/
private FastGptDataApi createTokenClient(String category, String tenantId) {
+ return createTokenClient(category, tenantId, false);
+ }
+
+ /**
+ * 创建临时API客户端
+ *
+ * @param category 认证token的类型
+ * @param tenantId 租户ID
+ * @param isStreaming 是否为流式传输
+ * @return FastGptDataApi实例
+ */
+ private FastGptDataApi createTokenClient(String category, String tenantId, boolean isStreaming) {
String url = getDictUrl(tenantId);
String token = getDictToken(tenantId, category);
@@ -175,13 +188,22 @@ public class FastGptService {
throw new IllegalArgumentException("fastgpt的URL和token不能为空");
}
- int readTimeout = 30;
int connectTimeout = 30;
- OkHttpClient httpClient = new OkHttpClient.Builder()
+ int readTimeout = isStreaming ? 300 : 30; // 流式传输使用5分钟,非流式使用30秒
+ int writeTimeout = isStreaming ? 300 : 30;
+
+ OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder()
.addInterceptor(new FastGptAuthInterceptor(token))
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
.readTimeout(readTimeout, TimeUnit.SECONDS)
- .build();
+ .writeTimeout(writeTimeout, TimeUnit.SECONDS);
+
+ // 只在流式传输时禁用缓存
+ if (isStreaming) {
+ httpClientBuilder.cache(null);
+ }
+
+ OkHttpClient httpClient = httpClientBuilder.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(url)
@@ -197,6 +219,18 @@ public class FastGptService {
return apiCache.computeIfAbsent(tenantId + "@" + category, key -> createTokenClient(category, tenantId));
}
+ /**
+ * 获取流式聊天客户端(不使用缓存,每次创建新实例以确保流式传输配置生效)
+ *
+ * @param category 认证类别
+ * @param tenantId 租户ID
+ * @return FastGptDataApi实例
+ */
+ private FastGptDataApi getStreamChatClient(String category, String tenantId) {
+ // 流式传输客户端不使用缓存,确保配置生效
+ return createTokenClient(category, tenantId, true);
+ }
+
private Map getDictMap() {
List dictGpt = dictDataCommonApi.getDictDataList("fastgpt");
return dictGpt.stream().collect(Collectors.toMap(DictDataRespDTO::getLabel, DictDataRespDTO::getValue));
@@ -215,16 +249,16 @@ public class FastGptService {
/**
* 发送对话请求
*
- * @param chatId 对话标志
- * @param content 对话内容
- * @param category 认证类别
- * @param dataId 数据ID
- * @param tenantId 租户ID
+ * @param chatId 对话标志
+ * @param content 对话内容
+ * @param category 认证类别
+ * @param dataId 数据ID
+ * @param tenantId 租户ID
* @param onComplete 流式输出完成后的回调函数,参数为完整的响应内容
* @return 流式响应
*/
public Flux sendStreamChatRequest(String chatId, String content, String category, String dataId, String tenantId,
- java.util.function.Consumer onComplete) {
+ java.util.function.Consumer onComplete) {
if (StrUtil.isEmpty(dataId)) dataId = UUID.randomUUID().toString();
@@ -240,43 +274,43 @@ public class FastGptService {
ChatCompletionRequest.ChatMessage message = new ChatCompletionRequest.ChatMessage(content);
request.setMessages(Collections.singletonList(message));
- // 使用 Retrofit 异步调用
- return Flux.create(sink ->
- getChatClient(category, tenantId).sendStreamChatRequest(request).enqueue(new retrofit2.Callback() {
- @Override
- public void onResponse(Call call, Response response) {
- if (response.isSuccessful()) {
- // 处理成功的响应
- // 处理流式响应
- ResponseBody responseBody = response.body();
- if (responseBody != null) {
- log.info("开始处理流式响应:");
-
- // 创建ObjectMapper用于解析JSON
- ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
- // 用于收集完整的响应内容
- StringBuilder fullResponse = new StringBuilder();
-
- try {
- // 逐行读取响应内容
- BufferedReader reader = new BufferedReader(responseBody.charStream());
- String line;
- int count = 0, done = 0;
-
- while ((line = reader.readLine()) != null && !sink.isCancelled()) {
- log.info("\n接收到响应行: {}", line);
-
- if(done == 1 && count++ < 4) {
- log.info("\n处理最后的汇总数据: {}", line);
- continue;
- }
+ // 使用 Flux.create 并在单独的线程中处理流式响应
+ return Flux.create(sink -> {
+ // 异步执行 Retrofit 调用,使用专门的流式传输客户端
+ getStreamChatClient(category, tenantId).sendStreamChatRequest(request).enqueue(new retrofit2.Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ ResponseBody responseBody = response.body();
+ if (responseBody != null) {
+ log.info("开始处理流式响应");
+
+ // 创建ObjectMapper用于解析JSON
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ // 用于收集完整的响应内容
+ StringBuilder fullResponse = new StringBuilder();
+
+ try {
+ // 逐行读取响应内容
+ BufferedReader reader = new BufferedReader(responseBody.charStream());
+ String line;
+ int count = 0, done = 0;
+
+ while ((line = reader.readLine()) != null && !sink.isCancelled()) {
+ log.info("接收到响应行: {}", line);
+
+ if (done == 1 && count++ < 4) {
+ log.info("处理最后的汇总数据: {}", line);
+ continue;
+ }
- if(count > 4) break;
+ if (count > 4) break;
- // 处理SSE格式的数据
- if (!line.trim().isEmpty() && line.startsWith("data: ")) {
+ // 处理SSE格式的数据
+ if (!line.trim().isEmpty()) {
+ if (line.startsWith("data: ")) {
String data = line.substring(6); // 移除"data: "前缀
// 检查是否为结束标记
@@ -300,47 +334,119 @@ public class FastGptService {
if (contentNode != null && contentNode.isTextual()) {
String text = contentNode.asText();
if (!text.isEmpty()) {
- log.info("\n接收到的内容片段: {}", text);
- sink.next(text); // 通过flux发送内容片段
- fullResponse.append(text); // 收集完整响应内容
+ log.info("接收到的内容片段: {}", text);
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next("{\"role\":\"assistant\",\"content\":\"" + text + "\"}");
+ }
+ fullResponse.append(text);
}
}
}
+ } else if (jsonNode.isArray() && jsonNode.has("moduleType")) {
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next(handleFlowResponses(jsonNode));
+ }
+ } else {
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next(data);
+ }
}
} catch (Exception e) {
log.warn("解析JSON数据失败: {}", data, e);
}
+ } else if (line.startsWith("event: ")) {
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next(line);
+ }
}
}
- } catch (Exception e) {
- log.warn("处理流式响应失败: {}", e.getMessage(), e);
}
+ reader.close();
+ responseBody.close();
+ } catch (Exception e) {
+ log.error("处理流式响应失败", e);
if (!sink.isCancelled()) {
- // 在流完成时调用回调函数
- if (onComplete != null) {
- try {
- onComplete.accept(fullResponse.toString());
- } catch (Exception e) {
- log.error("执行回调函数时发生错误", e);
- }
+ sink.error(e);
+ }
+ return;
+ }
+
+ if (!sink.isCancelled()) {
+ // 在流完成时调用回调函数
+ if (onComplete != null) {
+ try {
+ onComplete.accept(fullResponse.toString());
+ } catch (Exception e) {
+ log.error("执行回调函数时发生错误", e);
}
- sink.complete(); // 完成flux流
}
+ sink.complete();
}
- } else {
- // 处理错误响应
- sink.error(new RuntimeException("流式聊天调用失败: " + response.message()));
}
+ } else {
+ sink.error(new RuntimeException("流式聊天调用失败: " + response.message()));
}
+ }
- @Override
- public void onFailure(Call call, Throwable t) {
- // 处理网络故障等异常
- sink.error(t);
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ log.error("流式聊天请求失败", t);
+ sink.error(t);
+ }
+ });
+ }, reactor.core.publisher.FluxSink.OverflowStrategy.BUFFER);
+ }
+
+ private String handleFlowResponses(JsonNode jsonNode) {
+
+ // 解析数组类型的节点数据
+ FlowResponse flowResponse = new FlowResponse();
+
+ for (JsonNode item : jsonNode) {
+ String moduleType = item.has("moduleType") ? item.get("moduleType").asText() : "";
+
+ // 提取 chatNode 类型的数据
+ if ("chatNode".equals(moduleType) && item.has("historyPreview")) {
+ JsonNode historyPreview = item.get("historyPreview");
+ if (historyPreview.isArray() && !historyPreview.isEmpty()) {
+ JsonNode lastItem = historyPreview.get(historyPreview.size() - 1);
+ if (lastItem.has("obj") && "AI".equals(lastItem.get("obj").asText()) && lastItem.has("value")) {
+ flowResponse.setContent(lastItem.get("value").asText());
}
- })
- );
+ }
+ }
+
+ // 提取 datasetSearchNode 类型的数据
+ if ("datasetSearchNode".equals(moduleType) && item.has("quoteList")) {
+ JsonNode quoteListNode = item.get("quoteList");
+ if (quoteListNode.isArray()) {
+
+ for (JsonNode quote : quoteListNode) {
+ String collectionId = quote.has("collectionId") ? quote.get("collectionId").asText() : "";
+
+ FlowResponse.SourceItem sourceItem = null;
+ if (quote.has("datasetId") && !StrUtil.isEmpty(collectionId) && quote.has("sourceName")) {
+ sourceItem = new FlowResponse.SourceItem(quote.get("datasetId").asText(), quote.get("collectionId").asText(), quote.get("sourceName").asText());
+ flowResponse.addSourceItem(sourceItem);
+ }
+
+ if (quote.has("id") && quote.has("q") && quote.has("a")) {
+ sourceItem = flowResponse.getByCollectionId(collectionId);
+ if (sourceItem != null)
+ sourceItem.addQuoteItem(quote.get("id").asText(), quote.get("q").asText(), quote.get("a").asText());
+ }
+ }
+
+ }
+ }
+ }
+
+ return JSONUtils.toJSONString(flowResponse);
}
--
Gitee
From 3c22612355522a95eb99722c625eaeb4bc9ccea3 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 16:16:50 +0800
Subject: [PATCH 29/50] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=BC=82=E6=AD=A5?=
=?UTF-8?q?=E8=BE=93=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/fastgpt/FastGptService.java | 45 ++++++++++---------
1 file changed, 24 insertions(+), 21 deletions(-)
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index 39d2dbc623..f4114b8037 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -301,28 +301,36 @@ public class FastGptService {
while ((line = reader.readLine()) != null && !sink.isCancelled()) {
log.info("接收到响应行: {}", line);
- if (done == 1 && count++ < 4) {
- log.info("处理最后的汇总数据: {}", line);
- continue;
- }
-
- if (count > 4) break;
+ if (done == 1) count++;
+ if (done == 1 && count > 3) break;
// 处理SSE格式的数据
if (!line.trim().isEmpty()) {
if (line.startsWith("data: ")) {
String data = line.substring(6); // 移除"data: "前缀
-
- // 检查是否为结束标记
- if ("[DONE]".equals(data.trim())) {
- log.info("流式传输结束");
- done = 1;
- continue;
- }
-
try {
// 直接通过JSON路径提取content内容
JsonNode jsonNode = objectMapper.readTree(data);
+
+ // 已经发送过DONE,处理最后的flowResponse
+ if (done == 1) {
+ log.info("处理最后的汇总数据: {}", line);
+ if (jsonNode.isArray()) {
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next(handleFlowResponses(jsonNode));
+ }
+ }
+ break;
+ }
+
+ // 检查是否为结束标记
+ if ("[DONE]".equals(data.trim())) {
+ log.info("流式传输结束");
+ done = 1;
+ continue;
+ }
+
JsonNode choicesNode = jsonNode.get("choices");
if (choicesNode != null && choicesNode.isArray() && !choicesNode.isEmpty()) {
@@ -337,21 +345,16 @@ public class FastGptService {
log.info("接收到的内容片段: {}", text);
// 立即发送内容片段
if (!sink.isCancelled()) {
- sink.next("{\"role\":\"assistant\",\"content\":\"" + text + "\"}");
+ sink.next("data: {\"role\":\"assistant\",\"content\":\"" + text + "\"}");
}
fullResponse.append(text);
}
}
}
- } else if (jsonNode.isArray() && jsonNode.has("moduleType")) {
- // 立即发送内容片段
- if (!sink.isCancelled()) {
- sink.next(handleFlowResponses(jsonNode));
- }
} else {
// 立即发送内容片段
if (!sink.isCancelled()) {
- sink.next(data);
+ sink.next(line);
}
}
} catch (Exception e) {
--
Gitee
From 05ff9348732b39032ee096a72a5685afcd711b05 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 16:51:21 +0800
Subject: [PATCH 30/50] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=BC=82=E6=AD=A5?=
=?UTF-8?q?=E8=BE=93=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/admin/api/ChatController.http | 4 +--
.../controller/admin/api/ChatController.java | 9 ++++--
.../service/fastgpt/FastGptService.java | 31 ++++++++++---------
3 files changed, 25 insertions(+), 19 deletions(-)
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
index b9f58f74ba..c9e9fa9d7e 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
@@ -1,6 +1,6 @@
### 流式对话接口测试
-GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_12345&dataId=12345&userContent=腾讯公司最近发生了什么
-Content-Type: application/json
+GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_11&dataId=110705&userContent=腾讯公司最近发生了什么
+Content-Type: text/event-stream
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
index 49c6de1ac0..5c0be441aa 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
@@ -19,6 +19,7 @@ import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
import javax.validation.Valid;
@@ -45,7 +46,7 @@ public class ChatController {
@Parameter(name = "dataId", description = "每轮对话的id", required = true)
@Parameter(name = "userContent", description = "用户发送的内容", required = true)
@PreAuthorize("@ss.hasPermission('custom:chat:stream')")
- public Flux streamChat(
+ public Flux> streamChat(
@RequestParam String category,
@RequestParam String chatId,
@RequestParam String dataId,
@@ -77,8 +78,12 @@ public class ChatController {
log.error("保存助手回复失败", e);
}
})
+ .map(wrapper -> ServerSentEvent.builder()
+ .event(wrapper.getEvent())
+ .data(wrapper.getData())
+ .build())
.onBackpressureBuffer() // 添加背压缓冲
- .doOnNext(text -> log.info("发送 SSE 数据: {}", text)); // 添加日志
+ .doOnNext(event -> log.info("发送 SSE 事件: event={}, data={}", event.event(), event.data()));
}
@Operation(summary = "获取用户对话记录列表")
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index f4114b8037..93a8dc2501 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -257,7 +257,7 @@ public class FastGptService {
* @param onComplete 流式输出完成后的回调函数,参数为完整的响应内容
* @return 流式响应
*/
- public Flux sendStreamChatRequest(String chatId, String content, String category, String dataId, String tenantId,
+ public Flux sendStreamChatRequest(String chatId, String content, String category, String dataId, String tenantId,
java.util.function.Consumer onComplete) {
if (StrUtil.isEmpty(dataId)) dataId = UUID.randomUUID().toString();
@@ -297,12 +297,18 @@ public class FastGptService {
BufferedReader reader = new BufferedReader(responseBody.charStream());
String line;
int count = 0, done = 0;
+ String currentEvent = null;
while ((line = reader.readLine()) != null && !sink.isCancelled()) {
log.info("接收到响应行: {}", line);
if (done == 1) count++;
- if (done == 1 && count > 3) break;
+ if (done == 1 && count > 3) {
+ if (!sink.isCancelled()) {
+ sink.next(new SseMessageWrapper("finished", "[DONE]"));
+ }
+ break;
+ }
// 处理SSE格式的数据
if (!line.trim().isEmpty()) {
@@ -318,7 +324,8 @@ public class FastGptService {
if (jsonNode.isArray()) {
// 立即发送内容片段
if (!sink.isCancelled()) {
- sink.next(handleFlowResponses(jsonNode));
+ sink.next(new SseMessageWrapper("flowResponses", handleFlowResponses(jsonNode)));
+ sink.next(new SseMessageWrapper("finished", "[DONE]"));
}
}
break;
@@ -343,28 +350,22 @@ public class FastGptService {
String text = contentNode.asText();
if (!text.isEmpty()) {
log.info("接收到的内容片段: {}", text);
- // 立即发送内容片段
- if (!sink.isCancelled()) {
- sink.next("data: {\"role\":\"assistant\",\"content\":\"" + text + "\"}");
+ // 立即发送内容片段,使用从FastGPT收到的event
+ if (!sink.isCancelled() && currentEvent != null) {
+ String responseData = "{\"role\":\"assistant\",\"content\":\"" + text + "\"}";
+ sink.next(new SseMessageWrapper(currentEvent, responseData));
+ currentEvent = null;
}
fullResponse.append(text);
}
}
}
- } else {
- // 立即发送内容片段
- if (!sink.isCancelled()) {
- sink.next(line);
- }
}
} catch (Exception e) {
log.warn("解析JSON数据失败: {}", data, e);
}
} else if (line.startsWith("event: ")) {
- // 立即发送内容片段
- if (!sink.isCancelled()) {
- sink.next(line);
- }
+ currentEvent = line.substring(7);
}
}
}
--
Gitee
From 98c3a3b969aedf155cc933882857f5d52e798174 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 17:11:46 +0800
Subject: [PATCH 31/50] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=BC=82=E6=AD=A5?=
=?UTF-8?q?=E8=BE=93=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/fastgpt/FastGptService.java | 25 +++++++++++--------
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index 93a8dc2501..ab5089dc64 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -312,9 +312,18 @@ public class FastGptService {
// 处理SSE格式的数据
if (!line.trim().isEmpty()) {
- if (line.startsWith("data: ")) {
+ if (line.startsWith("event: ")) {
+ currentEvent = line.substring(7);
+ } else if (line.startsWith("data: ")) {
String data = line.substring(6); // 移除"data: "前缀
try {
+ // 检查是否为结束标记
+ if ("[DONE]".equals(data.trim())) {
+ log.info("流式传输结束");
+ done = 1;
+ continue;
+ }
+
// 直接通过JSON路径提取content内容
JsonNode jsonNode = objectMapper.readTree(data);
@@ -331,13 +340,6 @@ public class FastGptService {
break;
}
- // 检查是否为结束标记
- if ("[DONE]".equals(data.trim())) {
- log.info("流式传输结束");
- done = 1;
- continue;
- }
-
JsonNode choicesNode = jsonNode.get("choices");
if (choicesNode != null && choicesNode.isArray() && !choicesNode.isEmpty()) {
@@ -354,18 +356,19 @@ public class FastGptService {
if (!sink.isCancelled() && currentEvent != null) {
String responseData = "{\"role\":\"assistant\",\"content\":\"" + text + "\"}";
sink.next(new SseMessageWrapper(currentEvent, responseData));
- currentEvent = null;
}
fullResponse.append(text);
}
}
}
+ } else {
+ if (!sink.isCancelled() && currentEvent != null) {
+ sink.next(new SseMessageWrapper(currentEvent, data));
+ }
}
} catch (Exception e) {
log.warn("解析JSON数据失败: {}", data, e);
}
- } else if (line.startsWith("event: ")) {
- currentEvent = line.substring(7);
}
}
}
--
Gitee
From 0a71cefe749be9479146c1d02f8517e72280465e Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 17:26:31 +0800
Subject: [PATCH 32/50] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=BC=82=E6=AD=A5?=
=?UTF-8?q?=E8=BE=93=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../api/fastgpt/vo/ChatCompletionRequest.java | 5 +++++
.../custom/service/fastgpt/FastGptService.java | 1 +
.../service/fastgpt/SseMessageWrapper.java | 18 ++++++++++++++++++
3 files changed, 24 insertions(+)
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/SseMessageWrapper.java
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/ChatCompletionRequest.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/ChatCompletionRequest.java
index c00196be15..a3d991d54d 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/ChatCompletionRequest.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/ChatCompletionRequest.java
@@ -30,6 +30,11 @@ public class ChatCompletionRequest {
* 是否返回详细信息
*/
private Boolean detail;
+
+ /**
+ * 是否返回引用
+ */
+ private Boolean retainDatasetCite;
/**
* 聊天消息列表
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index ab5089dc64..26f22d72e2 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -268,6 +268,7 @@ public class FastGptService {
request.setResponseChatItemId(dataId);
request.setStream(true);
request.setDetail(true);
+ request.setRetainDatasetCite(true);
request.setVariables(new HashMap<>());
// 创建消息
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/SseMessageWrapper.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/SseMessageWrapper.java
new file mode 100644
index 0000000000..fa3b81e230
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/SseMessageWrapper.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.custom.service.fastgpt;
+
+import lombok.Data;
+
+/**
+ * SSE消息包装类
+ * 用于传递event和data信息
+ */
+@Data
+public class SseMessageWrapper {
+ private String event;
+ private String data;
+
+ public SseMessageWrapper(String event, String data) {
+ this.event = event;
+ this.data = data;
+ }
+}
--
Gitee
From 255e1502976beccbcd690b286339492e5e4c48ad Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 17:58:20 +0800
Subject: [PATCH 33/50] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=BC=82=E6=AD=A5?=
=?UTF-8?q?=E8=BE=93=E5=87=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../custom/api/fastgpt/vo/FlowResponse.java | 2 +-
.../controller/admin/api/ChatController.http | 5 +-
.../service/fastgpt/FastGptService.java | 189 ++++++++++--------
3 files changed, 109 insertions(+), 87 deletions(-)
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java
index 938911d48f..9813cdbdff 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/FlowResponse.java
@@ -80,7 +80,7 @@ public class FlowResponse {
if (sourceList == null) {
sourceList = new java.util.ArrayList<>();
}
- if(!sourceList.contains(sourceItem))
+ if(getByCollectionId(sourceItem.getCollectionId()) == null)
sourceList.add(sourceItem);
}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
index c9e9fa9d7e..1fc05f5463 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
@@ -1,6 +1,7 @@
### 流式对话接口测试
-GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_11&dataId=110705&userContent=腾讯公司最近发生了什么
-Content-Type: text/event-stream
+GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_12&dataId=110702&userContent=腾讯公司最近发生了什么
+Content-Type: application/json
+Accept: text/event-stream
Authorization: Bearer {{token}}
tenant-id: {{adminTenantId}}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index 26f22d72e2..a2deedd563 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -21,6 +21,9 @@ import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.util.*;
@@ -38,6 +41,7 @@ public class FastGptService {
private DictDataCommonApi dictDataCommonApi;
private static final Map apiCache = new ConcurrentHashMap<>();
+ private static final ExecutorService asyncExecutor = Executors.newFixedThreadPool(10);
/**
* 同步FAQ数据详情,创建场景(只有)的时候批量添加,文件夹id为数据集,faq为数据集下的数据;
@@ -294,106 +298,116 @@ public class FastGptService {
StringBuilder fullResponse = new StringBuilder();
try {
- // 逐行读取响应内容
- BufferedReader reader = new BufferedReader(responseBody.charStream());
- String line;
- int count = 0, done = 0;
- String currentEvent = null;
-
- while ((line = reader.readLine()) != null && !sink.isCancelled()) {
- log.info("接收到响应行: {}", line);
-
- if (done == 1) count++;
- if (done == 1 && count > 3) {
- if (!sink.isCancelled()) {
- sink.next(new SseMessageWrapper("finished", "[DONE]"));
- }
- break;
- }
-
- // 处理SSE格式的数据
- if (!line.trim().isEmpty()) {
- if (line.startsWith("event: ")) {
- currentEvent = line.substring(7);
- } else if (line.startsWith("data: ")) {
- String data = line.substring(6); // 移除"data: "前缀
- try {
- // 检查是否为结束标记
- if ("[DONE]".equals(data.trim())) {
- log.info("流式传输结束");
- done = 1;
- continue;
+ // 使用异步线程处理流式响应
+ asyncExecutor.submit(() -> {
+ try {
+ // 逐行读取响应内容
+ BufferedReader reader = new BufferedReader(responseBody.charStream());
+ String line;
+ int count = 0, done = 0;
+ String currentEvent = null;
+
+ while ((line = reader.readLine()) != null && !sink.isCancelled()) {
+ log.info("接收到响应行: {}", line);
+
+ if (done == 1) count++;
+ if (done == 1 && count > 3) {
+ if (!sink.isCancelled()) {
+ sink.next(new SseMessageWrapper("finished", "[DONE]"));
}
+ break;
+ }
- // 直接通过JSON路径提取content内容
- JsonNode jsonNode = objectMapper.readTree(data);
-
- // 已经发送过DONE,处理最后的flowResponse
- if (done == 1) {
- log.info("处理最后的汇总数据: {}", line);
- if (jsonNode.isArray()) {
- // 立即发送内容片段
- if (!sink.isCancelled()) {
- sink.next(new SseMessageWrapper("flowResponses", handleFlowResponses(jsonNode)));
- sink.next(new SseMessageWrapper("finished", "[DONE]"));
+ // 处理SSE格式的数据
+ if (!line.trim().isEmpty()) {
+ if (line.startsWith("event: ")) {
+ currentEvent = line.substring(7);
+ } else if (line.startsWith("data: ")) {
+ String data = line.substring(6); // 移除"data: "前缀
+ try {
+ // 检查是否为结束标记
+ if ("[DONE]".equals(data.trim())) {
+ log.info("流式传输结束");
+ done = 1;
+ continue;
}
- }
- break;
- }
- JsonNode choicesNode = jsonNode.get("choices");
-
- if (choicesNode != null && choicesNode.isArray() && !choicesNode.isEmpty()) {
- JsonNode firstChoice = choicesNode.get(0);
- JsonNode deltaNode = firstChoice.get("delta");
-
- if (deltaNode != null) {
- JsonNode contentNode = deltaNode.get("content");
- if (contentNode != null && contentNode.isTextual()) {
- String text = contentNode.asText();
- if (!text.isEmpty()) {
- log.info("接收到的内容片段: {}", text);
- // 立即发送内容片段,使用从FastGPT收到的event
- if (!sink.isCancelled() && currentEvent != null) {
- String responseData = "{\"role\":\"assistant\",\"content\":\"" + text + "\"}";
- sink.next(new SseMessageWrapper(currentEvent, responseData));
+ // 直接通过JSON路径提取content内容
+ JsonNode jsonNode = objectMapper.readTree(data);
+
+ // 已经发送过DONE,处理最后的flowResponse
+ if (done == 1) {
+ log.info("处理最后的汇总数据: {}", line);
+ if (jsonNode.isArray()) {
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next(new SseMessageWrapper("flowResponses", handleFlowResponses(jsonNode)));
+ sink.next(new SseMessageWrapper("finished", "[DONE]"));
}
- fullResponse.append(text);
}
+ break;
}
- }
- } else {
- if (!sink.isCancelled() && currentEvent != null) {
- sink.next(new SseMessageWrapper(currentEvent, data));
+
+ JsonNode choicesNode = jsonNode.get("choices");
+
+ if (choicesNode != null && choicesNode.isArray() && !choicesNode.isEmpty()) {
+ JsonNode firstChoice = choicesNode.get(0);
+ JsonNode deltaNode = firstChoice.get("delta");
+
+ if (deltaNode != null) {
+ JsonNode contentNode = deltaNode.get("content");
+ if (contentNode != null && contentNode.isTextual()) {
+ String text = contentNode.asText();
+ if (!text.isEmpty()) {
+ log.info("接收到的内容片段: {}", text);
+ // 立即发送内容片段,使用从FastGPT收到的event
+ if (!sink.isCancelled() && currentEvent != null) {
+ String responseData = "{\"role\":\"assistant\",\"content\":\"" + text + "\"}";
+ sink.next(new SseMessageWrapper(currentEvent, responseData));
+ }
+ fullResponse.append(text);
+ }
+ }
+ }
+ } else {
+ if (!sink.isCancelled() && currentEvent != null) {
+ sink.next(new SseMessageWrapper(currentEvent, data));
+ }
+ }
+ } catch (Exception e) {
+ log.warn("解析JSON数据失败: {}", data, e);
}
}
- } catch (Exception e) {
- log.warn("解析JSON数据失败: {}", data, e);
}
}
+
+ reader.close();
+ responseBody.close();
+ } catch (Exception e) {
+ log.error("异步处理流式响应失败", e);
+ if (!sink.isCancelled()) {
+ sink.error(e);
+ }
+ return;
}
- }
- reader.close();
- responseBody.close();
+ if (!sink.isCancelled()) {
+ // 在流完成时调用回调函数
+ if (onComplete != null) {
+ try {
+ onComplete.accept(fullResponse.toString());
+ } catch (Exception e) {
+ log.error("执行回调函数时发生错误", e);
+ }
+ }
+ sink.complete();
+ }
+ });
} catch (Exception e) {
- log.error("处理流式响应失败", e);
+ log.error("提交异步任务失败", e);
if (!sink.isCancelled()) {
sink.error(e);
}
- return;
- }
-
- if (!sink.isCancelled()) {
- // 在流完成时调用回调函数
- if (onComplete != null) {
- try {
- onComplete.accept(fullResponse.toString());
- } catch (Exception e) {
- log.error("执行回调函数时发生错误", e);
- }
- }
- sink.complete();
}
}
} else {
@@ -454,7 +468,14 @@ public class FastGptService {
}
}
- return JSONUtils.toJSONString(flowResponse);
+ // 使用Jackson ObjectMapper替代Druid的JSONUtils
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ return objectMapper.writeValueAsString(flowResponse);
+ } catch (Exception e) {
+ log.error("FlowResponse序列化失败", e);
+ return "{}"; // 返回默认值
+ }
}
--
Gitee
From ed9cceb02e7cbc9cc664f36f79ef576f33ca1347 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 18:00:14 +0800
Subject: [PATCH 34/50] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/StreamChatDialog.vue | 500 ++++++++++++++++++++++++++++++++++++++
1 file changed, 500 insertions(+)
create mode 100644 docs/StreamChatDialog.vue
diff --git a/docs/StreamChatDialog.vue b/docs/StreamChatDialog.vue
new file mode 100644
index 0000000000..ed99dc10e8
--- /dev/null
+++ b/docs/StreamChatDialog.vue
@@ -0,0 +1,500 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
--
Gitee
From f1cacd00fc031092cf1ee5337a34585d4fc26c72 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Fri, 7 Nov 2025 18:44:31 +0800
Subject: [PATCH 35/50] =?UTF-8?q?=E7=BB=88=E4=BA=8E=E5=8F=AF=E4=BB=A5?=
=?UTF-8?q?=E6=AD=A3=E5=B8=B8=E8=BF=9B=E8=A1=8C=E6=B5=81=E5=BC=8F=E8=BE=93?=
=?UTF-8?q?=E5=87=BA=E4=BA=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/StreamChatDialog.vue | 209 ++++++++++++++----
yudao-module-custom/README_API.md | 198 -----------------
.../custom/api/fastgpt/FastGptDataApi.java | 1 +
.../controller/admin/api/ChatController.http | 2 +-
.../service/fastgpt/FastGptService.java | 202 ++++++++---------
5 files changed, 263 insertions(+), 349 deletions(-)
delete mode 100644 yudao-module-custom/README_API.md
diff --git a/docs/StreamChatDialog.vue b/docs/StreamChatDialog.vue
index ed99dc10e8..284edfc60d 100644
--- a/docs/StreamChatDialog.vue
+++ b/docs/StreamChatDialog.vue
@@ -40,6 +40,16 @@
>
+
+
@@ -112,6 +122,7 @@ interface FormData {
interface Message {
role: 'user' | 'assistant'
content: string
+ sources?: any[]
}
const dialogVisible = ref(false)
@@ -209,6 +220,7 @@ const handleStop = () => {
const startStreamChat = async () => {
try {
streaming.value = true
+ // 添加空的助手消息
messages.value.push({
role: 'assistant',
content: ''
@@ -228,6 +240,9 @@ const startStreamChat = async () => {
userContent: formData.userContent
})
+ // 清空输入框
+ formData.userContent = ''
+
await fetchEventSource(`${baseUrl}/custom/chat/stream?${params.toString()}`, {
method: 'GET',
headers: {
@@ -236,47 +251,116 @@ const startStreamChat = async () => {
'tenant-id': localStorage.getItem('adminTenantId') || ''
},
signal: abortController.value.signal,
+ onopen: () => {
+ console.log('SSE连接已建立')
+ },
onmessage: (event) => {
try {
- console.log(event)
+ console.log('收到SSE事件:', event.event, event.data)
+
// 处理不同的事件类型
- if (event.event === 'source') {
- // 处理source事件
- const sourceData = JSON.parse(event.data)
- console.log('Received source data:', sourceData)
- // 这里可以处理引用信息
- } else if (event.event === 'answer') {
- // 处理answer事件
- if (event.data === '[DONE]') {
- // 流式传输结束
- streaming.value = false
- ElMessage.success('对话完成')
- return
+ if (event.event === 'flowNodeStatus') {
+ // 处理节点状态事件
+ try {
+ const statusData = JSON.parse(event.data)
+ console.log('节点状态:', statusData.name, '-', statusData.status)
+ // 可以在这里添加状态更新的UI反馈
+ } catch (e) {
+ console.warn('解析节点状态数据失败:', e)
}
-
- const answerData = JSON.parse(event.data)
- if (answerData.code === 0 && answerData.data) {
- // 更新最后一条消息的内容
- const lastMessage = messages.value[messages.value.length - 1]
- lastMessage.content += answerData.data.content || ''
- } else {
- ElMessage.error(`错误: ${answerData.msg || '未知错误'}`)
- streaming.value = false
+ } else if (event.event === 'flowResponses') {
+ // 处理引用信息和对话内容
+ try {
+ const responseData = JSON.parse(event.data)
+ console.log('收到响应数据:', responseData)
+
+ // 检查是否包含对话内容
+ if (typeof responseData === 'string') {
+ try {
+ const parsedContent = JSON.parse(responseData)
+ if (parsedContent.content) {
+ const lastMessage = messages.value[messages.value.length - 1]
+ lastMessage.content += parsedContent.content
+ scrollToBottom()
+ }
+ } catch (e) {
+ // 如果不是JSON格式,直接作为内容处理
+ const lastMessage = messages.value[messages.value.length - 1]
+ lastMessage.content += responseData
+ scrollToBottom()
+ }
+ } else if (responseData && typeof responseData === 'object') {
+ // 处理引用信息
+ const lastMessage = messages.value[messages.value.length - 1]
+ if (responseData.sources) {
+ lastMessage.sources = responseData.sources
+ }
+ }
+ } catch (e) {
+ console.warn('解析响应数据失败:', e)
}
+ } else if (event.event === 'chatMessage' || event.event === 'assistant') {
+ // 处理聊天消息事件
+ try {
+ const data = JSON.parse(event.data)
+ if (data && typeof data === 'object') {
+ const lastMessage = messages.value[messages.value.length - 1]
+ if (data.content) {
+ lastMessage.content += data.content
+ scrollToBottom()
+ } else if (data.text) {
+ lastMessage.content += data.text
+ scrollToBottom()
+ }
+ }
+ } catch (e) {
+ console.warn('解析聊天消息失败:', e)
+ }
+ } else if (event.event === 'finished') {
+ // 流式传输结束
+ streaming.value = false
+ ElMessage.success('对话完成')
+ return
+ } else if (event.data === '[DONE]') {
+ // 流式传输结束标记
+ streaming.value = false
+ ElMessage.success('对话完成')
+ return
} else {
- // 处理默认格式(兼容原有逻辑)
- const data = JSON.parse(event.data)
- if (data.code === 0 && data.data) {
- // 更新最后一条消息的内容
+ // 处理其他未知事件类型
+ console.log('未知事件类型:', event.event, '数据:', event.data)
+ try {
+ const data = JSON.parse(event.data)
+ if (data && typeof data === 'object') {
+ // 更新最后一条消息的内容
+ const lastMessage = messages.value[messages.value.length - 1]
+ if (data.content) {
+ lastMessage.content += data.content
+ scrollToBottom()
+ } else if (data.text) {
+ lastMessage.content += data.text
+ scrollToBottom()
+ } else {
+ // 直接追加原始数据
+ lastMessage.content += event.data
+ scrollToBottom()
+ }
+ } else {
+ // 直接追加原始数据
+ const lastMessage = messages.value[messages.value.length - 1]
+ lastMessage.content += event.data
+ scrollToBottom()
+ }
+ } catch (error) {
+ console.warn('解析流式数据失败,尝试直接追加:', error)
+ // 尝试直接追加原始数据
const lastMessage = messages.value[messages.value.length - 1]
- lastMessage.content += data.data.content || ''
- } else {
- ElMessage.error(`错误: ${data.msg || '未知错误'}`)
- streaming.value = false
+ lastMessage.content += event.data
+ scrollToBottom()
}
}
} catch (error) {
- console.error('解析消息失败:', error)
+ console.error('处理SSE事件失败:', error)
console.error('原始数据:', event.data)
}
},
@@ -287,6 +371,7 @@ const startStreamChat = async () => {
throw error
},
onclose: () => {
+ console.log('SSE连接已关闭')
streaming.value = false
abortController.value = null
}
@@ -389,6 +474,16 @@ const scrollToBottom = async () => {
}
}
+// 格式化消息内容(支持Markdown等)
+const formatMessageContent = (content: string) => {
+ // 基本的文本格式化
+ return content
+ .replace(/\n/g, '
')
+ .replace(/\*\*(.*?)\*\*/g, '$1') // 粗体
+ .replace(/\*(.*?)\*/g, '$1') // 斜体
+ .replace(/`(.*?)`/g, '$1') // 行内代码
+}
+
// 监听对话框关闭事件,自动重置状态
watch(dialogVisible, (newValue) => {
if (!newValue) {
@@ -397,12 +492,6 @@ watch(dialogVisible, (newValue) => {
}
})
-// 格式化消息内容(支持Markdown等)
-const formatMessageContent = (content: string) => {
- // 这里可以添加Markdown解析等格式化逻辑
- return content.replace(/\n/g, '
')
-}
-
// 处理输入中回车的定时器
let inputTimeout = ref()
@@ -464,6 +553,18 @@ defineExpose({
border-radius: 8px;
max-width: 80%;
line-height: 1.5;
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.message.user {
@@ -477,6 +578,25 @@ defineExpose({
align-self: flex-start;
}
+.message.assistant.streaming {
+ position: relative;
+}
+
+.cursor-blink {
+ display: inline-block;
+ width: 3px;
+ height: 1em;
+ background-color: var(--el-text-color-primary);
+ animation: cursorBlink 0.8s infinite;
+ vertical-align: bottom;
+ margin-left: 2px;
+}
+
+@keyframes cursorBlink {
+ 0%, 100% { opacity: 0; }
+ 50% { opacity: 1; }
+}
+
.message-content {
white-space: pre-wrap;
word-wrap: break-word;
@@ -484,6 +604,21 @@ defineExpose({
line-height: 1.6;
}
+.message-content :deep(code) {
+ background-color: var(--el-color-primary-light-8);
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+}
+
+.message-content :deep(strong) {
+ font-weight: bold;
+}
+
+.message-content :deep(em) {
+ font-style: italic;
+}
+
.dialog-footer {
padding: 0;
margin: 0;
diff --git a/yudao-module-custom/README_API.md b/yudao-module-custom/README_API.md
deleted file mode 100644
index 48822a08cf..0000000000
--- a/yudao-module-custom/README_API.md
+++ /dev/null
@@ -1,198 +0,0 @@
-# 对话模块 API 文档
-
-## 1. 流式对话接口
-
-### 请求信息
-- **URL**: `/custom/chat/stream`
-- **方法**: `POST`
-- **Content-Type**: `application/json`
-
-### 请求头
-- `tenant-id`: 租户ID
-- `login-user-id`: 用户ID
-
-### 请求参数
-```json
-{
- "chatId": "可选,如果为空会自动生成",
- "stream": true,
- "detail": false,
- "variables": {},
- "messages": [
- {
- "role": "user",
- "content": "你好,请介绍一下你自己"
- }
- ]
-}
-```
-
-### 请求参数说明
-- `chatId`: 对话会话ID,可选,如果为空会自动生成
-- `messages`: 消息列表,目前只支持一条消息
-- `messages[0].content`: 用户发送的消息内容
-
-### URL参数
-- `category`: 快速回复,
-- `dataId`: 数据ID
-
-### 响应
-**响应类型**: `Stream`
-**响应格式**: 每行一个数据块
-```
-data: {"choices":[{"delta":{"content":"你好"},"id":"chat123","model":"gpt-3.5-turbo"}}
-
-data: {"choices":[{"delta":{"content":",我是"},"id":"chat123","model":"gpt-3.5-turbo"}}
-
-data: {"choices":[{"delta":{"content":"AI助手。"},"id":"chat123","model":"gpt-3.5-turbo"}}
-
-data: [DONE]
-```
-
-### 响应说明
-- 每行以 `data: ` 开头
-- 流式响应结束时会发送 `data: [DONE]`
-- 处理时需要去掉 `data: ` 前缀并解析JSON
-
-## 2. 非流式对话接口
-
-### 请求信息
-- **URL**: `/custom/chat/chat`
-- **方法**: `POST`
-- **Content-Type**: `application/json`
-
-### 请求头
-- `tenant-id`: 租户ID
-- `login-user-id`: 用户ID
-
-### 请求参数
-同流式对话接口
-
-### 响应
-```json
-{
- "code": 200,
- "data": "完整回复内容",
- "msg": "成功"
-}
-```
-
-## 3. 获取对话记录列表
-
-### 请求信息
-- **URL**: `/custom/chat/list`
-- **方法**: `POST`
-- **Content-Type**: `application/json`
-
-### 请求头
-- `tenant-id`: 租户ID
-- `login-user-id`: 用户ID
-
-### 请求参数
-```json
-{
- "pageNum": 1,
- "pageSize": 10
-}
-```
-
-### 响应
-```json
-{
- "code": 200,
- "data": [
- {
- "id": 1,
- "chatId": "chat_123",
- "messageId": "msg_456",
- "content": "你好",
- "role": "user",
- "responseId": "",
- "model": "",
- "promptTokens": 0,
- "completionTokens": 0,
- "totalTokens": 0,
- "createTime": "2024-01-01T10:00:00"
- }
- ],
- "msg": "成功"
-}
-```
-
-## 4. 获取对话详情
-
-### 请求信息
-- **URL**: `/custom/chat/detail/{chatId}`
-- **方法**: `GET`
-
-### 请求头
-- `tenant-id`: 租户ID
-- `login-user-id`: 用户ID
-
-### 路径参数
-- `chatId`: 对话ID
-
-### 响应
-```json
-{
- "code": 200,
- "data": [
- {
- "id": 1,
- "chatId": "chat_123",
- "messageId": "msg_456",
- "content": "你好",
- "role": "user",
- "responseId": "",
- "model": "",
- "promptTokens": 10,
- "completionTokens": 20,
- "totalTokens": 30,
- "createTime": "2024-01-01T10:00:00"
- },
- {
- "id": 2,
- "chatId": "chat_123",
- "messageId": "msg_789",
- "content": "你好,我是AI助手",
- "role": "assistant",
- "responseId": "resp_123",
- "model": "gpt-3.5-turbo",
- "promptTokens": 10,
- "completionTokens": 20,
- "totalTokens": 30,
- "createTime": "2024-01-01T10:00:05"
- }
- ],
- "msg": "成功"
-}
-```
-
-## 5. 删除对话记录
-
-### 请求信息
-- **URL**: `/custom/chat/{chatId}`
-- **方法**: `DELETE`
-
-### 请求头
-- `tenant-id`: 租户ID
-- `login-user-id`: 用户ID
-
-### 路径参数
-- `chatId`: 对话ID
-
-### 响应
-```json
-{
- "code": 200,
- "data": true,
- "msg": "成功"
-}
-```
-
-## 注意事项
-
-1. 所有接口都需要传递租户ID和用户ID
-2. 流式接口会返回响应式流,需要特殊处理
-3. 对话记录会自动保存到数据库
-4. 删除操作实际上是软删除,只是将状态标记为已删除
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java
index 87e10e6670..4a898000cd 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java
@@ -111,6 +111,7 @@ public interface FastGptDataApi {
* @param request 聊天完成请求对象
* @return 流式响应
*/
+ @Streaming
@POST("api/v1/chat/completions")
@Headers({
"Content-Type: application/json",
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
index 1fc05f5463..c7e781d7f7 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.http
@@ -1,5 +1,5 @@
### 流式对话接口测试
-GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_12&dataId=110702&userContent=腾讯公司最近发生了什么
+GET {{baseUrl}}/custom/chat/stream?category=ent_chat&chatId=chat_132&dataId=6702&userContent=腾讯公司最近发生了什么
Content-Type: application/json
Accept: text/event-stream
Authorization: Bearer {{token}}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index a2deedd563..1fc649968f 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.custom.api.fastgpt.FastGptAuthInterceptor;
import cn.iocoder.yudao.module.custom.api.fastgpt.FastGptDataApi;
import cn.iocoder.yudao.module.custom.api.fastgpt.vo.*;
-import com.alibaba.druid.support.json.JSONUtils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -21,13 +20,13 @@ import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
import javax.annotation.Resource;
import java.io.BufferedReader;
+import java.io.InputStreamReader;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -262,7 +261,7 @@ public class FastGptService {
* @return 流式响应
*/
public Flux sendStreamChatRequest(String chatId, String content, String category, String dataId, String tenantId,
- java.util.function.Consumer onComplete) {
+ java.util.function.Consumer onComplete) {
if (StrUtil.isEmpty(dataId)) dataId = UUID.randomUUID().toString();
@@ -285,9 +284,8 @@ public class FastGptService {
getStreamChatClient(category, tenantId).sendStreamChatRequest(request).enqueue(new retrofit2.Callback() {
@Override
public void onResponse(Call call, Response response) {
- if (response.isSuccessful()) {
- ResponseBody responseBody = response.body();
- if (responseBody != null) {
+ if (response.isSuccessful() && response.body() != null) {
+ try (ResponseBody responseBody = response.body()) {
log.info("开始处理流式响应");
// 创建ObjectMapper用于解析JSON
@@ -297,117 +295,106 @@ public class FastGptService {
// 用于收集完整的响应内容
StringBuilder fullResponse = new StringBuilder();
- try {
- // 使用异步线程处理流式响应
- asyncExecutor.submit(() -> {
- try {
- // 逐行读取响应内容
- BufferedReader reader = new BufferedReader(responseBody.charStream());
- String line;
- int count = 0, done = 0;
- String currentEvent = null;
-
- while ((line = reader.readLine()) != null && !sink.isCancelled()) {
- log.info("接收到响应行: {}", line);
-
- if (done == 1) count++;
- if (done == 1 && count > 3) {
- if (!sink.isCancelled()) {
- sink.next(new SseMessageWrapper("finished", "[DONE]"));
- }
- break;
- }
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody.byteStream()))) {
+ // 逐行读取响应内容
+ String line;
+ int count = 0, done = 0;
+ String currentEvent = null;
- // 处理SSE格式的数据
- if (!line.trim().isEmpty()) {
- if (line.startsWith("event: ")) {
- currentEvent = line.substring(7);
- } else if (line.startsWith("data: ")) {
- String data = line.substring(6); // 移除"data: "前缀
- try {
- // 检查是否为结束标记
- if ("[DONE]".equals(data.trim())) {
- log.info("流式传输结束");
- done = 1;
- continue;
- }
+ while ((line = reader.readLine()) != null && !sink.isCancelled()) {
+ log.info("接收到响应行: {}", line);
- // 直接通过JSON路径提取content内容
- JsonNode jsonNode = objectMapper.readTree(data);
-
- // 已经发送过DONE,处理最后的flowResponse
- if (done == 1) {
- log.info("处理最后的汇总数据: {}", line);
- if (jsonNode.isArray()) {
- // 立即发送内容片段
- if (!sink.isCancelled()) {
- sink.next(new SseMessageWrapper("flowResponses", handleFlowResponses(jsonNode)));
- sink.next(new SseMessageWrapper("finished", "[DONE]"));
- }
- }
- break;
+ if (done == 1) count++;
+ if (done == 1 && count > 3) {
+ if (!sink.isCancelled()) {
+ sink.next(new SseMessageWrapper("finished", "[DONE]"));
+ }
+ break;
+ }
+
+ // 处理SSE格式的数据
+ if (!line.trim().isEmpty()) {
+ if (line.startsWith("event: ")) {
+ currentEvent = line.substring(7);
+ } else if (line.startsWith("data: ")) {
+ String data = line.substring(6); // 移除"data: "前缀
+ try {
+ // 检查是否为结束标记
+ if ("[DONE]".equals(data.trim())) {
+ log.info("流式传输结束");
+ done = 1;
+ continue;
+ }
+
+ // 直接通过JSON路径提取content内容
+ JsonNode jsonNode = objectMapper.readTree(data);
+
+ // 已经发送过DONE,处理最后的flowResponse
+ if (done == 1) {
+ log.info("处理最后的汇总数据: {}", line);
+ if (jsonNode.isArray()) {
+ // 立即发送内容片段
+ if (!sink.isCancelled()) {
+ sink.next(new SseMessageWrapper("flowResponses", handleFlowResponses(jsonNode)));
+ sink.next(new SseMessageWrapper("finished", "[DONE]"));
}
+ }
+ break;
+ }
- JsonNode choicesNode = jsonNode.get("choices");
-
- if (choicesNode != null && choicesNode.isArray() && !choicesNode.isEmpty()) {
- JsonNode firstChoice = choicesNode.get(0);
- JsonNode deltaNode = firstChoice.get("delta");
-
- if (deltaNode != null) {
- JsonNode contentNode = deltaNode.get("content");
- if (contentNode != null && contentNode.isTextual()) {
- String text = contentNode.asText();
- if (!text.isEmpty()) {
- log.info("接收到的内容片段: {}", text);
- // 立即发送内容片段,使用从FastGPT收到的event
- if (!sink.isCancelled() && currentEvent != null) {
- String responseData = "{\"role\":\"assistant\",\"content\":\"" + text + "\"}";
- sink.next(new SseMessageWrapper(currentEvent, responseData));
- }
- fullResponse.append(text);
- }
+ JsonNode choicesNode = jsonNode.get("choices");
+
+ if (choicesNode != null && choicesNode.isArray() && !choicesNode.isEmpty()) {
+ JsonNode firstChoice = choicesNode.get(0);
+ JsonNode deltaNode = firstChoice.get("delta");
+
+ if (deltaNode != null) {
+ JsonNode contentNode = deltaNode.get("content");
+ if (contentNode != null && contentNode.isTextual()) {
+ String text = contentNode.asText();
+ if (!text.isEmpty()) {
+ log.info("接收到的内容片段: {}", text);
+ // 立即发送内容片段,使用从FastGPT收到的event
+ if (!sink.isCancelled() && currentEvent != null) {
+ String responseData = "{\"role\":\"assistant\",\"content\":\"" + text + "\"}";
+ sink.next(new SseMessageWrapper(currentEvent, responseData));
}
- }
- } else {
- if (!sink.isCancelled() && currentEvent != null) {
- sink.next(new SseMessageWrapper(currentEvent, data));
+ fullResponse.append(text);
}
}
- } catch (Exception e) {
- log.warn("解析JSON数据失败: {}", data, e);
+ }
+ } else {
+ if (!sink.isCancelled() && currentEvent != null) {
+ sink.next(new SseMessageWrapper(currentEvent, data));
}
}
- }
- }
-
- reader.close();
- responseBody.close();
- } catch (Exception e) {
- log.error("异步处理流式响应失败", e);
- if (!sink.isCancelled()) {
- sink.error(e);
- }
- return;
- }
-
- if (!sink.isCancelled()) {
- // 在流完成时调用回调函数
- if (onComplete != null) {
- try {
- onComplete.accept(fullResponse.toString());
} catch (Exception e) {
- log.error("执行回调函数时发生错误", e);
+ log.warn("解析JSON数据失败: {}", data, e);
}
}
- sink.complete();
}
- });
+ }
+
+ reader.close();
+ responseBody.close();
} catch (Exception e) {
- log.error("提交异步任务失败", e);
+ log.error("异步处理流式响应失败", e);
if (!sink.isCancelled()) {
sink.error(e);
}
+ return;
+ }
+
+ if (!sink.isCancelled()) {
+ // 在流完成时调用回调函数
+ if (onComplete != null) {
+ try {
+ onComplete.accept(fullResponse.toString());
+ } catch (Exception e) {
+ log.error("执行回调函数时发生错误", e);
+ }
+ }
+ sink.complete();
}
}
} else {
@@ -432,17 +419,6 @@ public class FastGptService {
for (JsonNode item : jsonNode) {
String moduleType = item.has("moduleType") ? item.get("moduleType").asText() : "";
- // 提取 chatNode 类型的数据
- if ("chatNode".equals(moduleType) && item.has("historyPreview")) {
- JsonNode historyPreview = item.get("historyPreview");
- if (historyPreview.isArray() && !historyPreview.isEmpty()) {
- JsonNode lastItem = historyPreview.get(historyPreview.size() - 1);
- if (lastItem.has("obj") && "AI".equals(lastItem.get("obj").asText()) && lastItem.has("value")) {
- flowResponse.setContent(lastItem.get("value").asText());
- }
- }
- }
-
// 提取 datasetSearchNode 类型的数据
if ("datasetSearchNode".equals(moduleType) && item.has("quoteList")) {
JsonNode quoteListNode = item.get("quoteList");
--
Gitee
From 71424a8a7074ea1bdd5215201df1d00f80e2a56b Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Mon, 10 Nov 2025 14:35:32 +0800
Subject: [PATCH 36/50] =?UTF-8?q?=E4=BC=81=E4=B8=9A=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../module/custom/controller/admin/ent/vo/EntRespVO.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java
index 9971584b40..f5b3c530bc 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntRespVO.java
@@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
@Schema(description = "管理后台 - 企业 Response VO")
@Data
@@ -38,4 +39,7 @@ public class EntRespVO {
@ExcelProperty("状态(0正常 1停用)")
private Integer status;
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
}
--
Gitee
From 6be2aee554050d3fb0e03bf0e1bec52332bbd18f Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Mon, 10 Nov 2025 17:08:27 +0800
Subject: [PATCH 37/50] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BC=81=E4=B8=9A?=
=?UTF-8?q?=E5=92=8C=E4=BC=81=E4=B8=9A=E9=99=84=E4=BB=B6=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=88=B0fastgpt?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../custom/api/fastgpt/FastGptDataApi.java | 22 ++
.../api/fastgpt/vo/DatasetCreateRequest.java | 30 +++
.../custom/dal/dataobject/ent/EntDO.java | 4 +-
.../custom/dal/dataobject/ent/EntInfoDO.java | 6 +-
.../module/custom/job/EntInfoProcessJob.java | 190 ++++++++++--------
.../service/fastgpt/FastGptService.java | 50 ++++-
6 files changed, 211 insertions(+), 91 deletions(-)
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/DatasetCreateRequest.java
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java
index 4a898000cd..aff4aca4e2 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/FastGptDataApi.java
@@ -30,6 +30,28 @@ public interface FastGptDataApi {
@Body PushDataRequest request
);
+ /**
+ * 创建知识库
+ *
+ * @param request 创建知识库请求参数
+ * @return 创建的知识库信息
+ */
+ @POST("api/core/dataset/create")
+ Call> createDataset(
+ @Body DatasetCreateRequest request
+ );
+
+ /**
+ * 删除知识库
+ *
+ * @param datasetId 知识库ID
+ * @return 删除结果
+ */
+ @DELETE("api/core/dataset/delete")
+ Call> deleteDataset(
+ @Query("id") String datasetId
+ );
+
/**
* 删除知识库集合下的数据
*
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/DatasetCreateRequest.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/DatasetCreateRequest.java
new file mode 100644
index 0000000000..632d9252ca
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/api/fastgpt/vo/DatasetCreateRequest.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.custom.api.fastgpt.vo;
+
+import lombok.Data;
+
+/**
+ * 创建数据集请求
+ */
+@Data
+public class DatasetCreateRequest {
+ /** 父级ID,用于构建目录结构。通常可以为 null 或者直接不传 */
+ private String parentId;
+ /** 类型:dataset或folder,代表普通知识库和文件夹。不传则代表创建普通知识库 */
+ private String type = "dataset";
+ /** 知识库名(必填) */
+ private String name;
+ /** 介绍(可选) */
+ private String intro;
+ /** 头像地址(可选) */
+ private String avatar;
+ /** 向量模型(不能为空,用系统默认的) */
+ private String vectorModel = "default";
+ /** 文本处理模型(不能为空,用系统默认的) */
+ private String agentModel = "default";
+ /** 图片理解模型(建议传空,用系统默认的) */
+ private String vlmModel;
+
+ public String getAvatar() {
+ return avatar == null ? "folder".equals(type) ? "/imgs/files/folder.svg" : "" : avatar;
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntDO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntDO.java
index 987a63bfa0..a898806f2c 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntDO.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntDO.java
@@ -51,9 +51,9 @@ public class EntDO extends TenantBaseDO {
*/
private Integer status;
/**
- * 上传fastgpt后的Id
+ * 对应fastgpt的知识库id
*/
- private String fastgptId;
+ private String datasetId;
}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntInfoDO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntInfoDO.java
index f5fb2731ed..577de142a8 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntInfoDO.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/dal/dataobject/ent/EntInfoDO.java
@@ -31,7 +31,7 @@ public class EntInfoDO extends TenantBaseDO {
*/
private String name;
/**
- * 资料来源(0 文档,1 爬虫,2 录音, 3 待办事项)
+ * 资料来源(0 文档,1 爬虫,2 录音, 3 待办事项, 4 文本)
*/
private Integer source;
/**
@@ -51,4 +51,8 @@ public class EntInfoDO extends TenantBaseDO {
*/
private Integer status;
+ /**
+ * 上传fastgpt后的数据集id
+ */
+ private String collectionId;
}
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/job/EntInfoProcessJob.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/job/EntInfoProcessJob.java
index 1100b7a08b..007d50755f 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/job/EntInfoProcessJob.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/job/EntInfoProcessJob.java
@@ -30,6 +30,7 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
@Component
@@ -82,6 +83,9 @@ public class EntInfoProcessJob implements JobHandler {
return "处理异常:请前往基础设施 → 定时任务配置参数";
}
+ /* 这里将企业对应到fastgpt的知识库(datesetId)用于区分权限;
+ * 将企业信息对应到fastgpt的数据集(collectionId)自动拆块后就是一条条的数据*/
+
// 获取entInfo表中status=-1的记录
for (String t : tenants) {
EntInfoPageReqVO reqVO = new EntInfoPageReqVO();
@@ -93,9 +97,41 @@ public class EntInfoProcessJob implements JobHandler {
List pendingEntInfos = pages.getList();
+ // 获取去重的企业Id列表
+ List entIds = pendingEntInfos.stream().map(EntInfoDO::getEntId).distinct().collect(Collectors.toList());
+ List ents = entMapper.selectByIds(entIds);
+ String datasetId;
+ EntDO entDo;
+ for (EntDO ent : ents) {
+ if (ent.getDatasetId() == null || ent.getDatasetId().isEmpty()) {
+ datasetId = fastGptService.syncKnowledge(ent);
+ if (!StrUtil.isEmpty(datasetId)) {
+ ent.setDatasetId(datasetId);
+ entDo = new EntDO();
+ entDo.setId(ent.getId());
+ entDo.setDatasetId(datasetId);
+ entMapper.updateById(entDo);
+ }
+ }
+ }
+
+ Map entMap = ents.stream().collect(Collectors.toMap(EntDO::getId, ent -> ent));
+ String collectionId;
+ EntInfoDO entInfoDO;
for (EntInfoDO entInfo : pendingEntInfos) {
- // 处理逻辑将在后续步骤中实现
- processEntInfo(entInfo);
+ entDo = entMap.get(entInfo.getEntId());
+ // 保存数据集
+ collectionId = fastGptService.syncFaqCat(entDo.getTenantId().toString(), entInfo.getName(), entDo.getDatasetId());
+ if (collectionId != null && !collectionId.isEmpty()) {
+ entInfo.setCollectionId(collectionId);
+ entInfoDO = new EntInfoDO();
+ entInfoDO.setId(entInfoDO.getId());
+ entInfoDO.setCollectionId(collectionId);
+ entInfoMapper.updateById(entInfoDO);
+ }
+
+ // 保存数据集下的数据
+ processEntInfo(entInfo, entDo);
}
return "处理完成,共处理 " + pendingEntInfos.size() + " 条记录(分页大小:" + pageSize + ")";
@@ -104,27 +140,28 @@ public class EntInfoProcessJob implements JobHandler {
return "处理异常";
}
- private void processEntInfo(EntInfoDO entInfo) {
+ /**
+ * 处理企业信息,企业信息为数据集,内容为一条条的数据
+ *
+ * @param entInfo 企业信息
+ * @param entDO 企业
+ */
+ private void processEntInfo(EntInfoDO entInfo, EntDO entDO) {
+ String content = null;
// source=0时,读取文件内容
if (entInfo.getSource() == 0 && StrUtil.isNotEmpty(entInfo.getPath())) {
- try {
- String fastgptId;
- // 找到对应的企业,看是否已经创了集合
- EntDO entDO = entMapper.selectOne(EntDO::getId, entInfo.getEntId());
- if (entDO.getFastgptId() == null || entDO.getFastgptId().isEmpty()) {
- fastgptId = fastGptService.syncFaqCat(entDO.getTenantId().toString(), entDO.getName());
- if (!fastgptId.isEmpty()) {
- EntDO upEnt = new EntDO();
- upEnt.setId(entDO.getId());
- upEnt.setFastgptId(fastgptId);
- entMapper.updateById(upEnt);
- }
- } else fastgptId = entDO.getFastgptId();
+ content = extractFileContent(entInfo.getPath());
+ } else if(entInfo.getSource() == 4 && StrUtil.isNotEmpty(entInfo.getRemark())) {
+ content = entInfo.getRemark();
+ }
- String content = extractFileContent(entInfo.getPath());
- String time = String.valueOf(System.currentTimeMillis());
- // 将内容拆分成FAQ
- fastGptService.getJsonRequest(time, content, "document_analysis", entInfo.getId() + time, entDO.getTenantId().toString()).subscribe(response -> {
+ if (StrUtil.isEmpty(content)) return;
+
+ try {
+
+ String time = String.valueOf(System.currentTimeMillis());
+ // 将内容拆分成FAQ
+ fastGptService.getJsonRequest(time, content, "document_analysis", entInfo.getId() + time, entDO.getTenantId().toString()).subscribe(response -> {
/*{
"summary": "",
"todolist": "",
@@ -133,49 +170,49 @@ public class EntInfoProcessJob implements JobHandler {
"a": ""
}]
}*/
- PushDataRequest request = new PushDataRequest();
- JSONObject json = JSONUtil.parseObj(response);
- List list = json.getJSONArray("list").stream()
- .map(item -> JSONUtil.parseObj(item.toString()))
- .collect(Collectors.toList());
- list.forEach(item -> {
- request.addData(item.getStr("q"), item.getStr("a"));
- });
-
- // 提交到知识库
- boolean isSuccess = false;
- if (!request.getData().isEmpty()) {
- isSuccess = true;
- fastGptService.syncFaq(entDO.getTenantId().toString(), fastgptId, request.getData());
- }
+ PushDataRequest request = new PushDataRequest();
+ JSONObject json = JSONUtil.parseObj(response);
+ List list = json.getJSONArray("list").stream()
+ .map(item -> JSONUtil.parseObj(item.toString()))
+ .collect(Collectors.toList());
+ list.forEach(item -> {
+ request.addData(item.getStr("q"), item.getStr("a"));
+ });
- // 更新entInfo的附件内容字段
- if (json.getStr("summary") != null && !json.getStr("summary").isEmpty() && isSuccess) {
- EntInfoDO infoDO = new EntInfoDO();
- infoDO.setId(entInfo.getId());
- infoDO.setRemark(json.getStr("summary"));
- infoDO.setStatus(0);
- entInfoMapper.updateById(infoDO);
- }
+ // 提交到知识库
+ boolean isSuccess = false;
+ if (!request.getData().isEmpty()) {
+ isSuccess = true;
+ fastGptService.syncFaq(entDO.getTenantId().toString(), entInfo.getCollectionId(), request.getData());
+ }
- // 提取了待办事项直接入库
- if (json.getStr("todolist") != null && !json.getStr("todolist").isEmpty()) {
- EntInfoDO infoDO = new EntInfoDO();
- infoDO.setSource(3);
- infoDO.setStatus(2);
- infoDO.setEntId(entDO.getId());
- infoDO.setName(entDO.getName() + "-待办" + DateUtils.format(new Date(), "yyyyMMdd"));
- infoDO.setRemark(json.getStr("todolist"));
- entInfoMapper.insert(infoDO);
- }
+ // 更新entInfo的附件内容字段
+ if (json.getStr("summary") != null && !json.getStr("summary").isEmpty() && isSuccess) {
+ EntInfoDO infoDO = new EntInfoDO();
+ infoDO.setId(entInfo.getId());
+ infoDO.setRemark(json.getStr("summary"));
+ infoDO.setStatus(0);
+ entInfoMapper.updateById(infoDO);
+ }
- });
+ // 提取了待办事项直接入库
+ if (json.getStr("todolist") != null && !json.getStr("todolist").isEmpty()) {
+ EntInfoDO infoDO = new EntInfoDO();
+ infoDO.setSource(3);
+ infoDO.setStatus(2);
+ infoDO.setEntId(entDO.getId());
+ infoDO.setName(entDO.getName() + "-待办" + DateUtils.format(new Date(), "yyyyMMdd"));
+ infoDO.setRemark(json.getStr("todolist"));
+ entInfoMapper.insert(infoDO);
+ }
- } catch (IOException e) {
- // 处理文件读取异常
- log.error("文件读取异常:{}", e.getMessage(), e);
- }
+ });
+
+ } catch (Exception e) {
+ // 处理文件读取异常
+ log.error("处理文件异常:{}", e.getMessage(), e);
}
+
}
/**
@@ -183,32 +220,19 @@ public class EntInfoProcessJob implements JobHandler {
*
* @param filePath 文件路径
* @return 文件内容
- * @throws IOException IO异常
*/
- private String extractFileContent(String filePath) throws IOException {
- File file = new File(filePath);
-
- // 检查文件是否存在
- if (!file.exists()) {
- log.warn("文件不存在:{}", filePath);
- return "文件不存在:" + filePath;
- }
-
- // 检查文件是否可读
- if (!file.canRead()) {
- log.warn("文件无法读取:{}", filePath);
- return "文件无法读取:" + filePath;
- }
+ private String extractFileContent(String filePath) {
+ try {
+ File file = new File(filePath);
- // 检查文件大小
- if (file.length() > MAX_FILE_SIZE) {
- log.warn("文件过大,跳过处理:{},大小:{}MB", filePath, file.length() / (1024 * 1024));
- return "文件过大,超过10MB限制";
- }
+ // 检查文件是否存在
+ if (!file.exists()) {
+ log.warn("文件不存在:{}", filePath);
+ return "文件不存在:" + filePath;
+ }
- String fileName = file.getName().toLowerCase();
+ String fileName = file.getName().toLowerCase();
- try {
if (fileName.endsWith(".docx")) {
// 处理Word 2007+文档
return extractDocxContent(file);
@@ -220,10 +244,12 @@ public class EntInfoProcessJob implements JobHandler {
return FileUtil.readUtf8String(file);
} else if (fileName.endsWith(".wav") || fileName.endsWith(".mp3")) {
// 处理录音文件,使用funasr
- return "请配置funasr";
+ log.error("请配置funasr");
+ return "";
} else {
// 其他格式文件返回提示
- return "不支持的文件格式:" + fileName;
+ log.error("不支持的文件格式:{}", fileName);
+ return "";
}
} catch (Exception e) {
log.error("提取文件内容失败:{}", filePath, e);
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
index 1fc649968f..bcadedeefe 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/fastgpt/FastGptService.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
import cn.iocoder.yudao.module.custom.api.fastgpt.FastGptAuthInterceptor;
import cn.iocoder.yudao.module.custom.api.fastgpt.FastGptDataApi;
import cn.iocoder.yudao.module.custom.api.fastgpt.vo.*;
+import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntDO;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -25,8 +26,6 @@ import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -40,7 +39,6 @@ public class FastGptService {
private DictDataCommonApi dictDataCommonApi;
private static final Map apiCache = new ConcurrentHashMap<>();
- private static final ExecutorService asyncExecutor = Executors.newFixedThreadPool(10);
/**
* 同步FAQ数据详情,创建场景(只有)的时候批量添加,文件夹id为数据集,faq为数据集下的数据;
@@ -82,16 +80,15 @@ public class FastGptService {
* @param tenantId 租户ID
* @param name 数据集名称
*/
- public String syncFaqCat(String tenantId, String name) {
+ public String syncFaqCat(String tenantId, String name, String datasetId) {
String collectionId = "";
try {
- String sceneGptId = getDictToken(tenantId, "ent_knowledge");
// 将faq的目录作为数据集
log.info(" 创建空数据集...");
CollectionCreateRequest createRequest = new CollectionCreateRequest();
createRequest.setName(name);
- createRequest.setDatasetId(sceneGptId);
+ createRequest.setDatasetId(datasetId);
Response> response = getApiClient(tenantId).createEmptyCollection(createRequest).execute();
if (response.code() == 200 && response.body() != null) {
@@ -115,6 +112,47 @@ public class FastGptService {
return collectionId;
}
+ /**
+ * 同步知识库数据
+ *
+ * @param ent 企业信息
+ * @return String 知识库id
+ */
+ public String syncKnowledge(EntDO ent) {
+ String gptId = "", tenantId = ent.getTenantId().toString();
+
+ // 知识库所属租户文件夹的Id
+ String knowledgeFolder = getDictToken(tenantId, "ent_knowledge_folder");
+ if (knowledgeFolder == null || knowledgeFolder.isEmpty()) {
+ log.info("[syncKnowledge][ent_knowledge_folder参数为空]");
+ return "处理异常:请前往数据字典 → fastgpt配置参数 ent_knowledge_folder";
+ }
+
+ // 判断根据不同的操作类型进行操作
+ try {
+ log.info(" 创建企业知识库...");
+ DatasetCreateRequest createRequest = new DatasetCreateRequest();
+ createRequest.setName(ent.getName());
+ createRequest.setIntro("企业【" + createRequest.getName() + "】属于【" + ent.getIndustry() + "】行业。" + ent.getRemark());
+
+ Response> response = getApiClient(tenantId).createDataset(createRequest).execute();
+ if (response.code() == 200) {
+ if (response.body() != null) {
+ gptId = response.body().getData();
+ log.info("✓✓✓ 创建企业知识库调用成功,name:{}", ent.getName());
+ return gptId;
+ }
+
+ } else {
+ log.error("XXX HTTP状态码={},创建租户知识库调用失败:{}", response.code(), response.errorBody());
+ }
+ } catch (Exception e) {
+ log.error("知识库同步到fastgpt出错:", e);
+ }
+
+ return gptId;
+ }
+
/**
* 知识检索内容(异步版本)
*
--
Gitee
From 312ee5797a53125a07e72ddb91769293ca289626 Mon Sep 17 00:00:00 2001
From: AsyDong <814318774@qq.com>
Date: Tue, 11 Nov 2025 16:29:26 +0800
Subject: [PATCH 38/50] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=BE=85=E5=8A=9E?=
=?UTF-8?q?=E4=BA=8B=E9=A1=B9=E6=8E=A5=E5=8F=A3=E5=92=8C=E4=BC=81=E4=B8=9A?=
=?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/admin/api/ChatController.java | 45 ++++++++++++
.../admin/api/ThirdPartyController.java | 69 +++++++++++++++++++
.../admin/api/vo/ChatTodoRespVO.java | 68 ++++++++++++++++++
.../admin/api/vo/ThirdEntRespVO.java | 18 +++++
.../admin/ent/vo/EntImportReqVO.java | 2 +-
.../module/custom/service/ent/EntService.java | 13 ++++
.../custom/service/ent/EntServiceImpl.java | 23 +++++++
.../service/fastgpt/FastGptService.java | 9 ++-
.../custom/service/third/ThirdService.java | 15 ++++
.../service/third/ThirdServiceImpl.java | 48 +++++++++++++
10 files changed, 308 insertions(+), 2 deletions(-)
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ThirdPartyController.java
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ChatTodoRespVO.java
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ThirdEntRespVO.java
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/third/ThirdService.java
create mode 100644 yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/third/ThirdServiceImpl.java
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
index 5c0be441aa..810e44f9a9 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ChatController.java
@@ -7,8 +7,11 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.custom.controller.admin.api.vo.ChatRecordPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.api.vo.ChatRecordVO;
+import cn.iocoder.yudao.module.custom.controller.admin.api.vo.ChatTodoRespVO;
import cn.iocoder.yudao.module.custom.dal.dataobject.chat.ChatRecordDO;
+import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntInfoDO;
import cn.iocoder.yudao.module.custom.service.chat.ChatService;
+import cn.iocoder.yudao.module.custom.service.ent.EntService;
import cn.iocoder.yudao.module.custom.service.fastgpt.FastGptService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -24,6 +27,8 @@ import reactor.core.publisher.Flux;
import javax.validation.Valid;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -39,6 +44,8 @@ public class ChatController {
private final ChatService chatService;
+ private final EntService entService;
+
@Operation(summary = "与fastgpt进行流式对话")
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Parameter(name = "category", description = "对话流程类型", required = true)
@@ -136,4 +143,42 @@ public class ChatController {
return success(true);
}
+
+ /**
+ * 查询用户的待办事项
+ */
+ @Operation(summary = "查询用户的待办事项")
+ @GetMapping("/todo")
+ @PreAuthorize("@ss.hasPermission('custom:chat:stream')")
+ public CommonResult> getTodoList() {
+ Map> entInfoList = entService.getEntInfoList();
+ // 将entInfoList转换成List
+ List todoList = entInfoList.entrySet().stream()
+ .map(entry -> new ChatTodoRespVO(entry.getKey(), entry.getValue()))
+ .collect(Collectors.toList());
+
+ return success(todoList);
+ }
+
+
+ /**
+ * 用户点击完成待办事项
+ */
+ @Operation(summary = "用户点击完成待办事项")
+ @PostMapping("/todo/done")
+ @PreAuthorize("@ss.hasPermission('custom:chat:stream')")
+ public CommonResult doneTodoList(@RequestParam Long id) {
+ int i = entService.changeStatus(id);
+ return success(i > 0);
+ }
+
+ /**
+ * 查询用户权限范围内的企业列表
+ */
+ @Operation(summary = "查询用户权限范围内的企业列表")
+ @GetMapping("/ent")
+ @PreAuthorize("@ss.hasPermission('custom:chat:stream')")
+ public CommonResult> getEntList() {
+ return success(entService.getEntByUser());
+ }
}
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ThirdPartyController.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ThirdPartyController.java
new file mode 100644
index 0000000000..64329a884a
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/ThirdPartyController.java
@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.custom.controller.admin.api;
+
+import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.custom.controller.admin.api.vo.ThirdEntRespVO;
+import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntInfoPageReqVO;
+import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntInfoDO;
+import cn.iocoder.yudao.module.custom.service.ent.EntService;
+import cn.iocoder.yudao.module.custom.service.third.ThirdService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "第三方接口 - 企业信息")
+@RestController
+@RequestMapping("/custom/third")
+@Validated
+@Slf4j
+@RequiredArgsConstructor
+public class ThirdPartyController {
+
+ private final ThirdService thirdService;
+ private final DictDataCommonApi dictDataCommonApi;
+
+ @Operation(summary = "第三方获取企业列表")
+ @GetMapping("/ent/list")
+ public CommonResult> getEntList(
+ @RequestHeader("X-Tenant-Id") Long tenantId,
+ @RequestHeader("X-API-Key") String apiKey) {
+
+ // 验证API密钥和租户权限
+ if (!validateThirdPartyAccess(apiKey, tenantId)) {
+ return CommonResult.error(401, "未授权的访问");
+ }
+
+ try {
+ List result = thirdService.getEntList(tenantId);
+ return success(result);
+ } catch (Exception e) {
+ log.error("第三方获取企业列表失败,tenantId: {}", tenantId, e);
+ return CommonResult.error(500, "系统内部错误");
+ }
+ }
+
+ private boolean validateThirdPartyAccess(String apiKey, Long tenantId) {
+ // 检查API密钥和租户权限
+ List apiKeys = dictDataCommonApi.getDictDataList("third_api");
+ for (DictDataRespDTO apiKeyItem : apiKeys) {
+ if (apiKeyItem.getLabel().equals("key_" + tenantId) && apiKeyItem.getValue().equals(apiKey)) {
+ // 验证API密钥和租户权限
+ return true;
+ }
+ }
+
+ return false; // 简化示例
+ }
+}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ChatTodoRespVO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ChatTodoRespVO.java
new file mode 100644
index 0000000000..87eb6b3159
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ChatTodoRespVO.java
@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.custom.controller.admin.api.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntInfoDO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "对话记录 - 待办事项")
+@Data
+@ExcelIgnoreUnannotated
+public class ChatTodoRespVO {
+
+ @Schema(description = "企业名称", example = "腾讯")
+ private String name;
+
+ public ChatTodoRespVO(String key, List value) {
+ this.name = key;
+
+ for (EntInfoDO entInfoDO : value) {
+ this.addTodo(entInfoDO.getId(), entInfoDO.getRemark(),
+ entInfoDO.getStatus() == 3 ? "已办结" : entInfoDO.getStatus() == 2 ? "待办" : "其他", entInfoDO.getCreateTime());
+ }
+ }
+
+ @Data
+ public class Todo {
+
+ @Schema(description = "企业附件id", example = "123")
+ private Long id;
+
+ @Schema(description = "待办内容", example = "随便")
+ private String content;
+
+ @Schema(description = "待办状态", example = "已办结")
+ private String status;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+ public Todo(Long id, String content, String status, LocalDateTime createTime) {
+ this.id = id;
+ this.content = content;
+ this.status = status;
+ this.createTime = createTime;
+ }
+ }
+
+ @Schema(description = "待办列表")
+ private List todoList;
+
+ public void addTodo(Long id, String content, String status, LocalDateTime createTime) {
+ if (todoList == null) {
+ todoList = new java.util.ArrayList<>();
+ }
+ todoList.add(new Todo(id, content, status, createTime));
+ }
+
+ public ChatTodoRespVO() {
+ }
+
+ public ChatTodoRespVO(String name) {
+ this.name = name;
+ }
+}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ThirdEntRespVO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ThirdEntRespVO.java
new file mode 100644
index 0000000000..4bd536534d
--- /dev/null
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/api/vo/ThirdEntRespVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.custom.controller.admin.api.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class ThirdEntRespVO {
+
+ @Schema(description = "爬虫对应的collectionId", example = "123")
+ private String collectionId;
+
+ @Schema(description = "企业名称", example = "张三")
+ private String name;
+
+ @Schema(description = "企业官网地址")
+ private String domain;
+
+}
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java
index 8f4da417dc..ebe2db4732 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/controller/admin/ent/vo/EntImportReqVO.java
@@ -14,7 +14,7 @@ public class EntImportReqVO {
@NotEmpty(message = "企业名称不能为空")
private String name;
- @ExcelProperty("企业所属行业")
+ @ExcelProperty("企业官网地址")
private String industry;
@ExcelProperty("说明")
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java
index 49d0d05d7c..c38f491116 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntService.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.custom.service.ent;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.custom.controller.admin.api.vo.ThirdEntRespVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntInfoPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntSaveReqVO;
@@ -10,6 +11,7 @@ import cn.iocoder.yudao.module.custom.dal.dataobject.ent.EntInfoDO;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
/**
* 企业 Service 接口
@@ -107,4 +109,15 @@ public interface EntService {
void deleteEntInfo(Long id);
PageResult getEntInfoPage(@Valid EntInfoPageReqVO infoPageReqVO);
+
+ List getEntByUser();
+
+ Map> getEntInfoList();
+
+ /**
+ * 根据企业附件Id,办结待办事项
+ * @param id 企业附件Id
+ * @return
+ */
+ int changeStatus(Long id);
}
\ No newline at end of file
diff --git a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java
index f4601b760b..86321701c4 100644
--- a/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java
+++ b/yudao-module-custom/src/main/java/cn/iocoder/yudao/module/custom/service/ent/EntServiceImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.custom.service.ent;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.custom.controller.admin.api.vo.ThirdEntRespVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntInfoPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntPageReqVO;
import cn.iocoder.yudao.module.custom.controller.admin.ent.vo.EntSaveReqVO;
@@ -14,7 +15,11 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.custom.enums.ErrorCodeConstants.ENT_NOT_EXISTS;
@@ -153,9 +158,27 @@ public class EntServiceImpl implements EntService {
return entInfoMapper.selectPage(infoPageReqVO);
}
+ @Override
+ public List