Forráskód Böngészése

feat(ai): 添加 Dify聊天功能支持

zrd 1 hónapja
szülő
commit
1bb9ff8d81
13 módosított fájl, 1109 hozzáadás és 18 törlés
  1. 4 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java
  2. 16 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
  3. 1 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
  4. 117 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/chat/AppAiChatConversationController.java
  5. 167 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/chat/AppAiChatMessageController.java
  6. 12 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java
  7. 11 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java
  8. 42 5
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java
  9. 9 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java
  10. 197 6
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
  11. 90 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/vo/DifyFlowResponse.java
  12. 442 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/vo/KcbResponse.java
  13. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageServiceImpl.java

+ 4 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java

@@ -17,7 +17,10 @@ public class AiChatConversationUpdateMyReqVO {
 
     @Schema(description = "是否置顶", example = "true")
     private Boolean pinned;
-
+    /**
+     * dify对话id
+     */
+    private String difyConversationId;
     @Schema(description = "模型编号", example = "1")
     private Long modelId;
 

+ 16 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java

@@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
-import jakarta.validation.constraints.Size;
 import lombok.Data;
-import lombok.experimental.Accessors;
+
+import java.util.List;
 
 @Schema(description = "管理后台 - AI 聊天消息发送 Request VO")
 @Data
@@ -14,12 +14,26 @@ public class AiChatMessageSendReqVO {
     @Schema(description = "聊天对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     @NotNull(message = "聊天对话编号不能为空")
     private Long conversationId;
+    @Schema(description = "dify编码")
+    private String difyConversationId;
+    /**
+     * 事件
+     */
+    private String event;
 
     @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "帮我写个 Java 算法")
     @NotEmpty(message = "聊天内容不能为空")
     private String content;
+    @Schema(description = "令牌")
+    private String token;
 
     @Schema(description = "是否携带上下文", example = "true")
     private Boolean useContext;
+    
+    
+    /**
+     * 文件url
+     */
+    private List<String> fileUrls;
 
 }

+ 1 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java

@@ -28,6 +28,7 @@ public class AiChatMessageSendRespVO {
 
         @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
         private String content;
+        private String event;
 
         @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
         private List<Long> segmentIds;

+ 117 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/chat/AppAiChatConversationController.java

@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.ai.controller.app.chat;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "APP - AI 聊天对话")
+@RestController
+@RequestMapping("/ai/chat/conversation")
+@Validated
+public class AppAiChatConversationController {
+    
+    @Resource
+    private AiChatConversationService chatConversationService;
+    @Resource
+    private AiChatMessageService chatMessageService;
+    
+    @PostMapping("/create-my")
+    @Operation(summary = "创建【我的】聊天对话")
+    public CommonResult<Long> createChatConversationMy(@RequestBody @Valid AiChatConversationCreateMyReqVO createReqVO) {
+        return success(chatConversationService.createChatConversationMy(createReqVO, getLoginUserId()));
+    }
+    
+    @PostMapping("/create-dify")
+    @Operation(summary = "创建【我的】聊天对话")
+    public CommonResult<AiChatConversationDO> createChatConversationDify(@RequestBody @Valid AiChatConversationCreateMyReqVO createReqVO) {
+        return success(chatConversationService.createChatConversationDify(createReqVO, getLoginUserId()));
+    }
+    
+    @PutMapping("/update-my")
+    @Operation(summary = "更新【我的】聊天对话")
+    public CommonResult<Boolean> updateChatConversationMy(@RequestBody @Valid AiChatConversationUpdateMyReqVO updateReqVO) {
+        chatConversationService.updateChatConversationMy(updateReqVO, getLoginUserId());
+        return success(true);
+    }
+    
+    @GetMapping("/my-list")
+    @Operation(summary = "获得【我的】聊天对话列表")
+    public CommonResult<List<AiChatConversationRespVO>> getChatConversationMyList() {
+        List<AiChatConversationDO> list = chatConversationService.getChatConversationListByUserId(getLoginUserId());
+        return success(BeanUtils.toBean(list, AiChatConversationRespVO.class));
+    }
+    
+    @GetMapping("/get-my")
+    @Operation(summary = "获得【我的】聊天对话")
+    @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
+    public CommonResult<AiChatConversationRespVO> getChatConversationMy(@RequestParam("id") Long id) {
+        AiChatConversationDO conversation = chatConversationService.getChatConversation(id);
+        if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
+            conversation = null;
+        }
+        return success(BeanUtils.toBean(conversation, AiChatConversationRespVO.class));
+    }
+    
+    @DeleteMapping("/delete-my")
+    @Operation(summary = "删除聊天对话")
+    @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
+    public CommonResult<Boolean> deleteChatConversationMy(@RequestParam("id") Long id) {
+        chatConversationService.deleteChatConversationMy(id, getLoginUserId());
+        return success(true);
+    }
+    
+    @DeleteMapping("/delete-by-unpinned")
+    @Operation(summary = "删除未置顶的聊天对话")
+    public CommonResult<Boolean> deleteChatConversationMyByUnpinned() {
+        chatConversationService.deleteChatConversationMyByUnpinned(getLoginUserId());
+        return success(true);
+    }
+    
+    // ========== 对话管理 ==========
+    
+    @GetMapping("/page")
+    @Operation(summary = "获得对话分页", description = "用于【对话管理】菜单")
+    public CommonResult<PageResult<AiChatConversationRespVO>> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
+        PageResult<AiChatConversationDO> pageResult = chatConversationService.getChatConversationPage(pageReqVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty());
+        }
+        // 拼接关联数据
+        Map<Long, Integer> messageCountMap = chatMessageService.getChatMessageCountMap(
+                convertList(pageResult.getList(), AiChatConversationDO::getId));
+        return success(BeanUtils.toBean(pageResult, AiChatConversationRespVO.class,
+                conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0))));
+    }
+    
+    @Operation(summary = "管理员删除对话")
+    @DeleteMapping("/delete-by-admin")
+    @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
+    public CommonResult<Boolean> deleteChatConversationByAdmin(@RequestParam("id") Long id) {
+        chatConversationService.deleteChatConversationByAdmin(id);
+        return success(true);
+    }
+    
+}

+ 167 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/chat/AppAiChatMessageController.java

@@ -0,0 +1,167 @@
+package cn.iocoder.yudao.module.ai.controller.app.chat;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
+import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
+import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "APP - 聊天消息")
+@RestController
+@RequestMapping("/ai/chat/message")
+@Slf4j
+public class AppAiChatMessageController {
+    
+    @Resource
+    private AiChatMessageService chatMessageService;
+    @Resource
+    private AiChatConversationService chatConversationService;
+    @Resource
+    private AiChatRoleService chatRoleService;
+    @Resource
+    private AiKnowledgeSegmentService knowledgeSegmentService;
+    @Resource
+    private AiKnowledgeDocumentService knowledgeDocumentService;
+    
+    @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢")
+    @PostMapping("/send")
+    public CommonResult<AiChatMessageSendRespVO> sendMessage(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
+        return success(chatMessageService.sendMessage(sendReqVO, getLoginUserId()));
+    }
+    
+    @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
+    @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
+        return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
+    }
+    
+    @Operation(summary = "发送消息(流式)", description = "dify流式返回,响应较快")
+    @PostMapping(value = "/dify-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public Flux<CommonResult<AiChatMessageSendRespVO>> difyChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO, HttpServletRequest request) {
+        String token = SecurityFrameworkUtils.obtainAuthorization(request,
+                "Authorization", "token");
+        sendReqVO.setToken(token);
+        return chatMessageService.difyChatMessageStream(sendReqVO, getLoginUserId());
+    }
+    
+    @Operation(summary = "获得指定对话的消息列表")
+    @GetMapping("/list-by-conversation-id")
+    @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024")
+    public CommonResult<List<AiChatMessageRespVO>> getChatMessageListByConversationId(
+            @RequestParam("conversationId") Long conversationId) {
+        AiChatConversationDO conversation = chatConversationService.getChatConversation(conversationId);
+        if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
+            return success(Collections.emptyList());
+        }
+        // 1. 获取消息列表
+        List<AiChatMessageDO> messageList = chatMessageService.getChatMessageListByConversationId(conversationId);
+        if (CollUtil.isEmpty(messageList)) {
+            return success(Collections.emptyList());
+        }
+        
+        // 2. 拼接数据,主要是知识库段落信息
+        Map<Long, AiKnowledgeSegmentDO> segmentMap =
+                knowledgeSegmentService.getKnowledgeSegmentMap(convertListByFlatMap(messageList,
+                        message -> CollUtil.isEmpty(message.getSegmentIds()) ? null :
+                                message.getSegmentIds().stream()));
+        Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
+                convertList(segmentMap.values(), AiKnowledgeSegmentDO::getDocumentId));
+        List<AiChatMessageRespVO> messageVOList = BeanUtils.toBean(messageList, AiChatMessageRespVO.class);
+        for (int i = 0; i < messageList.size(); i++) {
+            AiChatMessageDO message = messageList.get(i);
+            if (CollUtil.isEmpty(message.getSegmentIds())) {
+                continue;
+            }
+            // 设置知识库段落信息
+            messageVOList.get(i).setSegments(convertList(message.getSegmentIds(), segmentId -> {
+                AiKnowledgeSegmentDO segment = segmentMap.get(segmentId);
+                if (segment == null) {
+                    return null;
+                }
+                AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
+                if (document == null) {
+                    return null;
+                }
+                return new AiChatMessageRespVO.KnowledgeSegment().setId(segment.getId()).setContent(segment.getContent())
+                        .setDocumentId(segment.getDocumentId()).setDocumentName(document.getName());
+            }));
+        }
+        return success(messageVOList);
+    }
+    
+    @Operation(summary = "删除消息")
+    @DeleteMapping("/delete")
+    @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
+    public CommonResult<Boolean> deleteChatMessage(@RequestParam("id") Long id) {
+        chatMessageService.deleteChatMessage(id, getLoginUserId());
+        return success(true);
+    }
+    
+    @Operation(summary = "删除指定对话的消息")
+    @DeleteMapping("/delete-by-conversation-id")
+    @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024")
+    public CommonResult<Boolean> deleteChatMessageByConversationId(@RequestParam("conversationId") Long conversationId) {
+        chatMessageService.deleteChatMessageByConversationId(conversationId, getLoginUserId());
+        return success(true);
+    }
+    
+    // ========== 对话管理 ==========
+    
+    @GetMapping("/page")
+    @Operation(summary = "获得消息分页", description = "用于【对话管理】菜单")
+    public CommonResult<PageResult<AiChatMessageRespVO>> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) {
+        PageResult<AiChatMessageDO> pageResult = chatMessageService.getChatMessagePage(pageReqVO);
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty());
+        }
+        // 拼接数据
+        Map<Long, AiChatRoleDO> roleMap = chatRoleService.getChatRoleMap(
+                convertSet(pageResult.getList(), AiChatMessageDO::getRoleId));
+        return success(BeanUtils.toBean(pageResult, AiChatMessageRespVO.class,
+                respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(),
+                        role -> respVO.setRoleName(role.getName()))));
+    }
+    
+    @Operation(summary = "管理员删除消息")
+    @DeleteMapping("/delete-by-admin")
+    @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
+    public CommonResult<Boolean> deleteChatMessageByAdmin(@RequestParam("id") Long id) {
+        chatMessageService.deleteChatMessageByAdmin(id);
+        return success(true);
+    }
+    
+}

+ 12 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java

@@ -6,8 +6,12 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
+import java.io.Serial;
 import java.time.LocalDateTime;
 
 /**
@@ -27,7 +31,9 @@ import java.time.LocalDateTime;
 public class AiChatConversationDO extends BaseDO {
 
     public static final String TITLE_DEFAULT = "新对话";
-
+    @Serial
+    private static final long serialVersionUID = -5171421480235501728L;
+    
     /**
      * ID 编号,自增
      */
@@ -47,6 +53,10 @@ public class AiChatConversationDO extends BaseDO {
      * 默认由系统自动生成,可用户手动修改
      */
     private String title;
+    /**
+     * dify对话id
+     */
+    private String difyConversationId;
     /**
      * 是否置顶
      */

+ 11 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java

@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
 
 import java.util.List;
@@ -24,6 +23,15 @@ public interface AiChatConversationService {
      * @return 编号
      */
     Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId);
+    
+    /**
+     * 创建聊天对话dify
+     *
+     * @param createReqVO 创建req vo
+     * @param userId      用户id
+     * @return {@link Long }
+     */
+    AiChatConversationDO createChatConversationDify(AiChatConversationCreateMyReqVO createReqVO, Long userId);
 
     /**
      * 更新【我的】聊天对话
@@ -32,6 +40,8 @@ public interface AiChatConversationService {
      * @param userId 用户编号
      */
     void updateChatConversationMy(AiChatConversationUpdateMyReqVO updateReqVO, Long userId);
+    
+    void updateConversation(AiChatConversationUpdateMyReqVO updateReqVO);
 
     /**
      * 获得【我的】聊天对话列表

+ 42 - 5
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java

@@ -4,26 +4,33 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
-import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
+import cn.iocoder.yudao.module.ai.service.chat.vo.KcbResponse;
 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
-import cn.iocoder.yudao.module.ai.service.model.AiModelService;
 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
+import cn.iocoder.yudao.module.ai.service.model.AiModelService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import java.time.LocalDateTime;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -40,7 +47,8 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSAT
 @Validated
 @Slf4j
 public class AiChatConversationServiceImpl implements AiChatConversationService {
-
+    @Value("${dify.base-url}")
+    private String baseUrl;
     @Resource
     private AiChatConversationMapper chatConversationMapper;
 
@@ -78,6 +86,29 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
         chatConversationMapper.insert(conversation);
         return conversation.getId();
     }
+    
+    @Override
+    public AiChatConversationDO createChatConversationDify(AiChatConversationCreateMyReqVO createReqVO, Long userId) {
+        // 1.1 获得 AiChatRoleDO 聊天角色
+        
+        // 2. 创建 AiChatConversationDO 聊天对话
+        AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
+                .setModelId(99L).setModel("DIFY")
+                .setTemperature(1D).setMaxTokens(4086).setMaxContexts(48000);
+        
+        conversation.setTitle(AiChatConversationDO.TITLE_DEFAULT);
+        //获取开场白
+        String apiKey = DictFrameworkUtils.parseDictDataValue("ai_key", "多轮对话");
+        Map<String, String> header = new HashMap<>(4);
+        header.put("Authorization", "Bearer " + apiKey);
+        
+        header.put("Content-Type", "application/json");
+        String responseBody = HttpUtils.get(baseUrl + "v1/parameters", header);
+        KcbResponse kcbResponse = JSONUtil.toBean(responseBody, KcbResponse.class);
+        conversation.setSystemMessage(kcbResponse.getOpening_statement());
+        chatConversationMapper.insert(conversation);
+        return conversation;
+    }
 
     @Override
     public void updateChatConversationMy(AiChatConversationUpdateMyReqVO updateReqVO, Long userId) {
@@ -107,7 +138,12 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
         }
         chatConversationMapper.updateById(updateObj);
     }
-
+    
+    @Override
+    public void updateConversation(AiChatConversationUpdateMyReqVO updateReqVO) {
+        chatConversationMapper.updateById(BeanUtils.toBean(updateReqVO, AiChatConversationDO.class));
+    }
+    
     @Override
     public List<AiChatConversationDO> getChatConversationListByUserId(Long userId) {
         return chatConversationMapper.selectListByUserId(userId);
@@ -147,7 +183,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
         Assert.equals(model.getType(), AiModelTypeEnum.CHAT.getType(), "模型类型不正确:" + model);
         throw exception(CHAT_CONVERSATION_MODEL_ERROR);
     }
-
+    
+    @Override
     public AiChatConversationDO validateChatConversationExists(Long id) {
         AiChatConversationDO conversation = chatConversationMapper.selectById(id);
         if (conversation == null) {

+ 9 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java

@@ -35,6 +35,15 @@ public interface AiChatMessageService {
      * @param userId 用户编号
      * @return 发送结果
      */
+    Flux<CommonResult<AiChatMessageSendRespVO>> difyChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId);
+    
+    /**
+     * 发送聊天消息流
+     *
+     * @param sendReqVO 发送请求vo
+     * @param userId    用户id
+     * @return {@link Flux }<{@link CommonResult }<{@link AiChatMessageSendRespVO }>>
+     */
     Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId);
 
     /**

+ 197 - 6
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java

@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.ai.service.chat;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
@@ -21,6 +25,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.module.ai.service.chat.vo.DifyFlowResponse;
 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
@@ -39,8 +44,10 @@ import org.springframework.ai.chat.model.ChatResponse;
 import org.springframework.ai.chat.model.StreamingChatModel;
 import org.springframework.ai.chat.prompt.ChatOptions;
 import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.reactive.function.client.WebClient;
 import reactor.core.publisher.Flux;
 
 import java.time.LocalDateTime;
@@ -70,10 +77,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
     private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 <Reference></Reference> 标记中的内容作为本次对话的参考:\n\n" +
             "%s\n\n" + // 多个 <Reference></Reference> 的拼接
             "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。";
-
+    private final WebClient webClient;
     @Resource
     private AiChatMessageMapper chatMessageMapper;
-
     @Resource
     private AiChatConversationService chatConversationService;
     @Resource
@@ -86,7 +92,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
     private AiKnowledgeDocumentService knowledgeDocumentService;
     @Resource
     private AiToolService toolService;
-
+    
+    public AiChatMessageServiceImpl(WebClient.Builder webClientBuilder) {
+        this.webClient = webClientBuilder.baseUrl("http://42.194.163.46:9502").build();
+    }
+    
+    @Override
     @Transactional(rollbackFor = Exception.class)
     public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
         // 1.1 校验对话存在
@@ -134,7 +145,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
                 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
                         .setContent(newContent).setSegments(segments));
     }
-
+    
     @Override
     public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO,
             Long userId) {
@@ -199,7 +210,129 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
                     new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
         }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
     }
-
+    
+    @Override
+    public Flux<CommonResult<AiChatMessageSendRespVO>> difyChatMessageStream(AiChatMessageSendReqVO sendReqVO,
+                                                                             Long userId) {
+        // 1.1 校验对话存在
+        AiChatConversationDO conversation = chatConversationService
+                .validateChatConversationExists(sendReqVO.getConversationId());
+        Map<String, Object> inputs = new HashMap<>();
+        if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
+            throw exception(CHAT_CONVERSATION_NOT_EXISTS);
+        }
+        // 1.2 校验模型
+        AiModelDO model = new AiModelDO();
+        List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>();
+        model.setModel("dify");
+        model.setId(99L);
+        // 3. 插入 user 发送消息
+        AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
+                userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
+                null);
+        String apiKey = DictFrameworkUtils.parseDictDataValue("ai_key", "多轮对话");
+        JSONObject requestBody = new JSONObject();
+        if (StrUtil.isNotBlank(conversation.getDifyConversationId())) {
+            requestBody.set("conversation_id", conversation.getDifyConversationId());
+        }
+        String type = "";
+        if (userId == 1L) {
+            inputs.put("type", "系统客服");
+            type = "系统客服";
+        } else if (userId == 2L) {
+            inputs.put("type", "贷款专家");
+            type = "贷款专家";
+        } else if (userId == 3L) {
+            inputs.put("type", "律师专家");
+            type = "律师专家";
+        } else {
+            inputs.put("type", "其他");
+            type = "其他";
+        }
+        
+        inputs.put("token", sendReqVO.getToken());
+        
+        
+        requestBody.set("response_mode", "streaming");
+        requestBody.set("user", SecurityFrameworkUtils.getLoginUserId());
+        // 如果difyConversationId为0 则为 开场白
+        
+        
+        if (CollUtil.isNotEmpty(sendReqVO.getFileUrls())) {
+            inputs.put("fileUrls", StrUtil.join(",", sendReqVO.getFileUrls()));
+            if (CollUtil.isNotEmpty(sendReqVO.getFileUrls())) {
+                List<Map<String, Object>> docs = new ArrayList<>();
+                for (String image : sendReqVO.getFileUrls()) {
+                    Map<String, Object> variableValue = new HashMap<>();
+                    variableValue.put("transfer_method", "remote_url");
+                    variableValue.put("url", image);
+                    if (StrUtil.containsAny(image, ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx")) {
+                        variableValue.put("type", "document");
+                    } else {
+                        variableValue.put("type", "image");
+                    }
+                    
+                    docs.add(variableValue);
+                }
+                requestBody.set("files", docs);
+            }
+        }
+        
+        requestBody.set("inputs", inputs);
+        requestBody.set("query", sendReqVO.getContent());
+        AiChatMessageSendRespVO result = new AiChatMessageSendRespVO();
+        result.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class));
+        // 使用 WebClient 发起非阻塞请求
+        Flux<AiChatMessageSendReqVO> flux = sendMessageToDify(apiKey, requestBody,
+                sendReqVO);
+        
+        // 3.2 流式返回
+        StringBuffer contentBuffer = new StringBuffer();
+        StringBuffer difyId = new StringBuffer();
+        AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
+                userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
+                knowledgeSegments);
+        return flux.map(chunk -> {
+            String newContent = chunk.getContent();
+            contentBuffer.append(newContent);
+            if (chunk.getEvent().equals("message_end")) {
+                difyId.append(chunk.getDifyConversationId());
+            }
+            assistantMessage.setContent(newContent);
+            AiChatMessageSendRespVO.Message receive = BeanUtils.toBean(assistantMessage,
+                    AiChatMessageSendRespVO.Message.class);
+            receive.setEvent(chunk.getEvent());
+            result.setReceive(receive);
+            // 响应结果
+            return success(result);
+        }).doOnComplete(() -> {
+            // 忽略租户,因为 Flux 异步无法透传租户
+            assistantMessage.setContent(contentBuffer.toString());
+            // 1.2 保存消息
+            //更新 创建时间 方便排序
+            assistantMessage.setCreateTime(LocalDateTime.now());
+            TenantUtils.executeIgnore(() ->
+                    chatMessageMapper.updateById(assistantMessage));
+            AiChatConversationUpdateMyReqVO updateReqVO = new AiChatConversationUpdateMyReqVO();
+            updateReqVO.setId(conversation.getId());
+            updateReqVO.setDifyConversationId(difyId.toString());
+            updateReqVO.setTitle(StrUtil.subPre(sendReqVO.getContent(), 20));
+            TenantUtils.executeIgnore(() ->
+                    chatConversationService.updateConversation(updateReqVO));
+            
+        }).doOnError(throwable -> {
+            log.error("[generateWriteCon-tent][generateReqVO({}) 发生异常]", null, throwable);
+            // 忽略租户,因为 Flux 异步无法透传租户
+            
+        }).onErrorResume(error -> Flux.just(null));
+        // 4.1 插入 assistant 接收消息
+        
+        
+        // 4.2 构建 Prompt,并进行调用
+        
+    }
+    
+    
     private List<AiKnowledgeSegmentSearchRespBO> recallKnowledgeSegment(String content,
             AiChatConversationDO conversation) {
         // 1. 查询聊天角色
@@ -219,7 +352,65 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         }
         return knowledgeSegments;
     }
-
+    
+    /**
+     * 向dify发送消息
+     *
+     * @param apiKey      api密钥
+     * @param requestBody 请求正文
+     * @param message     信息
+     * @return {@link Flux }<{@link AiChatMessageSendReqVO }>
+     */
+    public Flux<AiChatMessageSendReqVO> sendMessageToDify(String apiKey, JSONObject requestBody,
+                                                          AiChatMessageSendReqVO message) {
+        return webClient.post()
+                .uri("/v1/chat-messages") // 假设的API路径,根据实际情况修改
+                .headers(httpHeaders -> {
+                    httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+                    httpHeaders.setBearerAuth(apiKey);
+                })
+                .bodyValue(requestBody)
+                .retrieve()
+                .bodyToFlux(DifyFlowResponse.class)
+                .map(difyResponse -> convertToKeFuMessage(difyResponse, message))
+                .filter(this::shouldInclude)
+                .doOnComplete(() -> {
+                
+                })
+                .doOnTerminate(() -> System.out.println("请求结束"));
+    }
+    
+    /**
+     * 应包括
+     *
+     * @param streamResponse 流响应
+     * @return boolean
+     */
+    private boolean shouldInclude(AiChatMessageSendReqVO streamResponse) {
+        // 示例:只要message节点的数据和message_end节点的数据
+        if (streamResponse.getEvent().equals("message")
+                || streamResponse.getEvent().equals("message_end")) {
+            return true;
+        }
+        return false;
+    }
+    
+    private AiChatMessageSendReqVO convertToKeFuMessage(DifyFlowResponse response, AiChatMessageSendReqVO message) {
+        // 实现 DifyResponse 到 KeFuMessageRespVO 的转换逻辑
+        // 设置其他必要的字段...
+        StringBuffer contentBuffer = new StringBuffer();
+        if (StrUtil.equals("message", response.getEvent())) {
+            message.setContent(response.getAnswer());
+            message.setDifyConversationId(response.getConversation_id());
+            contentBuffer.append(response.getAnswer());
+        } else if (StrUtil.equals("message_end", response.getEvent())) {
+        
+        } else {
+        
+        }
+        message.setEvent(response.getEvent());
+        return message;
+    }
     private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
             List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
             AiModelDO model, AiChatMessageSendReqVO sendReqVO) {

+ 90 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/vo/DifyFlowResponse.java

@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.ai.service.chat.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class DifyFlowResponse implements Serializable {
+    
+    
+    @Serial
+    private static final long serialVersionUID = -8136474488568106769L;
+    /**
+     * event : message
+     * task_id : 4513bd53-fd94-47af-abeb-759846469dc9
+     * id : 6c068b57-afc2-4dbd-b1d6-49ef5e55202e
+     * message_id : 6c068b57-afc2-4dbd-b1d6-49ef5e55202e
+     * conversation_id : 50a4839b-6bf7-4741-b86b-81c02d0f5de3
+     * mode : advanced-chat
+     * answer :
+     * <p>
+     * 您好!我是 AI 小优,您专属的 AI助手,您来说,我来办
+     * 生成个人数据资产
+     * metadata : {"usage":{"prompt_tokens":0,"prompt_unit_price":"0.0","prompt_price_unit":"0.0","prompt_price":"0
+     * .0","completion_tokens":0,"completion_unit_price":"0.0","completion_price_unit":"0.0","completion_price":"0
+     * .0","total_tokens":0,"total_price":"0.0","currency":"USD","latency":0}}
+     * created_at : 1747733207
+     */
+    
+    private String event;
+    private String task_id;
+    private String id;
+    private String message_id;
+    private String conversation_id;
+    private String mode;
+    private String answer;
+    private MetadataBean metadata;
+    private int created_at;
+    
+    
+    @Data
+    public static class MetadataBean implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 8939728861612692110L;
+        /**
+         * usage : {"prompt_tokens":0,"prompt_unit_price":"0.0","prompt_price_unit":"0.0","prompt_price":"0.0",
+         * "completion_tokens":0,"completion_unit_price":"0.0","completion_price_unit":"0.0","completion_price":"0
+         * .0","total_tokens":0,"total_price":"0.0","currency":"USD","latency":0}
+         */
+        
+        private UsageBean usage;
+        
+        public UsageBean getUsage() {return usage;}
+        
+        public void setUsage(UsageBean usage) {this.usage = usage;}
+        
+        public static class UsageBean {
+            /**
+             * prompt_tokens : 0
+             * prompt_unit_price : 0.0
+             * prompt_price_unit : 0.0
+             * prompt_price : 0.0
+             * completion_tokens : 0
+             * completion_unit_price : 0.0
+             * completion_price_unit : 0.0
+             * completion_price : 0.0
+             * total_tokens : 0
+             * total_price : 0.0
+             * currency : USD
+             * latency : 0.0
+             */
+            
+            private int prompt_tokens;
+            private String prompt_unit_price;
+            private String prompt_price_unit;
+            private String prompt_price;
+            private int completion_tokens;
+            private String completion_unit_price;
+            private String completion_price_unit;
+            private String completion_price;
+            private int total_tokens;
+            private String total_price;
+            private String currency;
+            private double latency;
+            
+            
+        }
+    }
+}

+ 442 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/vo/KcbResponse.java

@@ -0,0 +1,442 @@
+package cn.iocoder.yudao.module.ai.service.chat.vo;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class KcbResponse implements Serializable {
+    
+    
+    @Serial
+    private static final long serialVersionUID = 2673634716823991482L;
+    
+    
+    /**
+     * opening_statement :
+     * 您好!我是 AI 小优,您专属的 AI助手,您来说,我来办
+     * suggested_questions : ["生成个人数据资产"]
+     * suggested_questions_after_answer : {"enabled":true}
+     * speech_to_text : {"enabled":true}
+     * text_to_speech : {"enabled":true,"language":"","voice":""}
+     * retriever_resource : {"enabled":true}
+     * annotation_reply : {"enabled":false}
+     * more_like_this : {"enabled":false}
+     * user_input_form : [{"text-input":{"variable":"token","label":"令牌","type":"text-input","max_length":255,
+     * "required":true,"options":[]}}]
+     * sensitive_word_avoidance : {"enabled":false}
+     * file_upload : {"image":{"enabled":false,"number_limits":3,"transfer_methods":["local_file","remote_url"]}
+     * ,"enabled":true,"allowed_file_types":["image"],"allowed_file_extensions":[],
+     * "allowed_file_upload_methods":["remote_url","local_file"],"number_limits":3,"fileUploadConfig":{
+     * "file_size_limit":15,"batch_count_limit":5,"image_file_size_limit":10,"video_file_size_limit":100,
+     * "audio_file_size_limit":50,"workflow_file_upload_limit":10}}
+     * system_parameters : {"image_file_size_limit":10,"video_file_size_limit":100,"audio_file_size_limit":50,
+     * "file_size_limit":15,"workflow_file_upload_limit":10}
+     */
+    
+    private String opening_statement;
+    private SuggestedQuestionsAfterAnswerBean suggested_questions_after_answer;
+    private SpeechToTextBean speech_to_text;
+    private TextToSpeechBean text_to_speech;
+    private RetrieverResourceBean retriever_resource;
+    private AnnotationReplyBean annotation_reply;
+    private MoreLikeThisBean more_like_this;
+    private SensitiveWordAvoidanceBean sensitive_word_avoidance;
+    private FileUploadBean file_upload;
+    private SystemParametersBean system_parameters;
+    private List<String> suggested_questions;
+    private List<UserInputFormBean> user_input_form;
+    
+    public String getOpening_statement() {return opening_statement;}
+    
+    public void setOpening_statement(String opening_statement) {this.opening_statement = opening_statement;}
+    
+    public SuggestedQuestionsAfterAnswerBean getSuggested_questions_after_answer() {return suggested_questions_after_answer;}
+    
+    public void setSuggested_questions_after_answer(SuggestedQuestionsAfterAnswerBean suggested_questions_after_answer) {this.suggested_questions_after_answer = suggested_questions_after_answer;}
+    
+    public SpeechToTextBean getSpeech_to_text() {return speech_to_text;}
+    
+    public void setSpeech_to_text(SpeechToTextBean speech_to_text) {this.speech_to_text = speech_to_text;}
+    
+    public TextToSpeechBean getText_to_speech() {return text_to_speech;}
+    
+    public void setText_to_speech(TextToSpeechBean text_to_speech) {this.text_to_speech = text_to_speech;}
+    
+    public RetrieverResourceBean getRetriever_resource() {return retriever_resource;}
+    
+    public void setRetriever_resource(RetrieverResourceBean retriever_resource) {
+        this.retriever_resource =
+                retriever_resource;
+    }
+    
+    public AnnotationReplyBean getAnnotation_reply() {return annotation_reply;}
+    
+    public void setAnnotation_reply(AnnotationReplyBean annotation_reply) {this.annotation_reply = annotation_reply;}
+    
+    public MoreLikeThisBean getMore_like_this() {return more_like_this;}
+    
+    public void setMore_like_this(MoreLikeThisBean more_like_this) {this.more_like_this = more_like_this;}
+    
+    public SensitiveWordAvoidanceBean getSensitive_word_avoidance() {return sensitive_word_avoidance;}
+    
+    public void setSensitive_word_avoidance(SensitiveWordAvoidanceBean sensitive_word_avoidance) {this.sensitive_word_avoidance = sensitive_word_avoidance;}
+    
+    public FileUploadBean getFile_upload() {return file_upload;}
+    
+    public void setFile_upload(FileUploadBean file_upload) {this.file_upload = file_upload;}
+    
+    public SystemParametersBean getSystem_parameters() {return system_parameters;}
+    
+    public void setSystem_parameters(SystemParametersBean system_parameters) {
+        this.system_parameters =
+                system_parameters;
+    }
+    
+    public List<String> getSuggested_questions() {return suggested_questions;}
+    
+    public void setSuggested_questions(List<String> suggested_questions) {
+        this.suggested_questions =
+                suggested_questions;
+    }
+    
+    public List<UserInputFormBean> getUser_input_form() {return user_input_form;}
+    
+    public void setUser_input_form(List<UserInputFormBean> user_input_form) {this.user_input_form = user_input_form;}
+    
+    public static class SuggestedQuestionsAfterAnswerBean {
+        /**
+         * enabled : true
+         */
+        
+        private boolean enabled;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+    }
+    
+    public static class SpeechToTextBean {
+        /**
+         * enabled : true
+         */
+        
+        private boolean enabled;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+    }
+    
+    public static class TextToSpeechBean {
+        /**
+         * enabled : true
+         * language :
+         * voice :
+         */
+        
+        private boolean enabled;
+        private String language;
+        private String voice;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+        
+        public String getLanguage() {return language;}
+        
+        public void setLanguage(String language) {this.language = language;}
+        
+        public String getVoice() {return voice;}
+        
+        public void setVoice(String voice) {this.voice = voice;}
+    }
+    
+    public static class RetrieverResourceBean {
+        /**
+         * enabled : true
+         */
+        
+        private boolean enabled;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+    }
+    
+    public static class AnnotationReplyBean {
+        /**
+         * enabled : false
+         */
+        
+        private boolean enabled;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+    }
+    
+    public static class MoreLikeThisBean {
+        /**
+         * enabled : false
+         */
+        
+        private boolean enabled;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+    }
+    
+    public static class SensitiveWordAvoidanceBean {
+        /**
+         * enabled : false
+         */
+        
+        private boolean enabled;
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+    }
+    
+    public static class FileUploadBean {
+        /**
+         * image : {"enabled":false,"number_limits":3,"transfer_methods":["local_file","remote_url"]}
+         * enabled : true
+         * allowed_file_types : ["image"]
+         * allowed_file_extensions : []
+         * allowed_file_upload_methods : ["remote_url","local_file"]
+         * number_limits : 3
+         * fileUploadConfig : {"file_size_limit":15,"batch_count_limit":5,"image_file_size_limit":10,
+         * "video_file_size_limit":100,"audio_file_size_limit":50,"workflow_file_upload_limit":10}
+         */
+        
+        private ImageBean image;
+        private boolean enabled;
+        private int number_limits;
+        private FileUploadConfigBean fileUploadConfig;
+        private List<String> allowed_file_types;
+        private List<?> allowed_file_extensions;
+        private List<String> allowed_file_upload_methods;
+        
+        public ImageBean getImage() {return image;}
+        
+        public void setImage(ImageBean image) {this.image = image;}
+        
+        public boolean isEnabled() {return enabled;}
+        
+        public void setEnabled(boolean enabled) {this.enabled = enabled;}
+        
+        public int getNumber_limits() {return number_limits;}
+        
+        public void setNumber_limits(int number_limits) {this.number_limits = number_limits;}
+        
+        public FileUploadConfigBean getFileUploadConfig() {return fileUploadConfig;}
+        
+        public void setFileUploadConfig(FileUploadConfigBean fileUploadConfig) {
+            this.fileUploadConfig =
+                    fileUploadConfig;
+        }
+        
+        public List<String> getAllowed_file_types() {return allowed_file_types;}
+        
+        public void setAllowed_file_types(List<String> allowed_file_types) {
+            this.allowed_file_types =
+                    allowed_file_types;
+        }
+        
+        public List<?> getAllowed_file_extensions() {return allowed_file_extensions;}
+        
+        public void setAllowed_file_extensions(List<?> allowed_file_extensions) {
+            this.allowed_file_extensions =
+                    allowed_file_extensions;
+        }
+        
+        public List<String> getAllowed_file_upload_methods() {return allowed_file_upload_methods;}
+        
+        public void setAllowed_file_upload_methods(List<String> allowed_file_upload_methods) {this.allowed_file_upload_methods = allowed_file_upload_methods;}
+        
+        public static class ImageBean {
+            /**
+             * enabled : false
+             * number_limits : 3
+             * transfer_methods : ["local_file","remote_url"]
+             */
+            
+            private boolean enabled;
+            private int number_limits;
+            private List<String> transfer_methods;
+            
+            public boolean isEnabled() {return enabled;}
+            
+            public void setEnabled(boolean enabled) {this.enabled = enabled;}
+            
+            public int getNumber_limits() {return number_limits;}
+            
+            public void setNumber_limits(int number_limits) {this.number_limits = number_limits;}
+            
+            public List<String> getTransfer_methods() {return transfer_methods;}
+            
+            public void setTransfer_methods(List<String> transfer_methods) {this.transfer_methods = transfer_methods;}
+        }
+        
+        public static class FileUploadConfigBean {
+            /**
+             * file_size_limit : 15
+             * batch_count_limit : 5
+             * image_file_size_limit : 10
+             * video_file_size_limit : 100
+             * audio_file_size_limit : 50
+             * workflow_file_upload_limit : 10
+             */
+            
+            private int file_size_limit;
+            private int batch_count_limit;
+            private int image_file_size_limit;
+            private int video_file_size_limit;
+            private int audio_file_size_limit;
+            private int workflow_file_upload_limit;
+            
+            public int getFile_size_limit() {return file_size_limit;}
+            
+            public void setFile_size_limit(int file_size_limit) {this.file_size_limit = file_size_limit;}
+            
+            public int getBatch_count_limit() {return batch_count_limit;}
+            
+            public void setBatch_count_limit(int batch_count_limit) {this.batch_count_limit = batch_count_limit;}
+            
+            public int getImage_file_size_limit() {return image_file_size_limit;}
+            
+            public void setImage_file_size_limit(int image_file_size_limit) {
+                this.image_file_size_limit =
+                        image_file_size_limit;
+            }
+            
+            public int getVideo_file_size_limit() {return video_file_size_limit;}
+            
+            public void setVideo_file_size_limit(int video_file_size_limit) {
+                this.video_file_size_limit =
+                        video_file_size_limit;
+            }
+            
+            public int getAudio_file_size_limit() {return audio_file_size_limit;}
+            
+            public void setAudio_file_size_limit(int audio_file_size_limit) {
+                this.audio_file_size_limit =
+                        audio_file_size_limit;
+            }
+            
+            public int getWorkflow_file_upload_limit() {return workflow_file_upload_limit;}
+            
+            public void setWorkflow_file_upload_limit(int workflow_file_upload_limit) {this.workflow_file_upload_limit = workflow_file_upload_limit;}
+        }
+    }
+    
+    public static class SystemParametersBean {
+        /**
+         * image_file_size_limit : 10
+         * video_file_size_limit : 100
+         * audio_file_size_limit : 50
+         * file_size_limit : 15
+         * workflow_file_upload_limit : 10
+         */
+        
+        private int image_file_size_limit;
+        private int video_file_size_limit;
+        private int audio_file_size_limit;
+        private int file_size_limit;
+        private int workflow_file_upload_limit;
+        
+        public int getImage_file_size_limit() {return image_file_size_limit;}
+        
+        public void setImage_file_size_limit(int image_file_size_limit) {
+            this.image_file_size_limit =
+                    image_file_size_limit;
+        }
+        
+        public int getVideo_file_size_limit() {return video_file_size_limit;}
+        
+        public void setVideo_file_size_limit(int video_file_size_limit) {
+            this.video_file_size_limit =
+                    video_file_size_limit;
+        }
+        
+        public int getAudio_file_size_limit() {return audio_file_size_limit;}
+        
+        public void setAudio_file_size_limit(int audio_file_size_limit) {
+            this.audio_file_size_limit =
+                    audio_file_size_limit;
+        }
+        
+        public int getFile_size_limit() {return file_size_limit;}
+        
+        public void setFile_size_limit(int file_size_limit) {this.file_size_limit = file_size_limit;}
+        
+        public int getWorkflow_file_upload_limit() {return workflow_file_upload_limit;}
+        
+        public void setWorkflow_file_upload_limit(int workflow_file_upload_limit) {
+            this.workflow_file_upload_limit =
+                    workflow_file_upload_limit;
+        }
+    }
+    
+    public static class UserInputFormBean {
+        /**
+         * text-input : {"variable":"token","label":"令牌","type":"text-input","max_length":255,"required":true,
+         * "options":[]}
+         */
+        
+        @SerializedName("text-input")
+        private TextinputBean textinput;
+        
+        public TextinputBean getTextinput() {return textinput;}
+        
+        public void setTextinput(TextinputBean textinput) {this.textinput = textinput;}
+        
+        public static class TextinputBean {
+            /**
+             * variable : token
+             * label : 令牌
+             * type : text-input
+             * max_length : 255
+             * required : true
+             * options : []
+             */
+            
+            private String variable;
+            private String label;
+            private String type;
+            private int max_length;
+            private boolean required;
+            private List<?> options;
+            
+            public String getVariable() {return variable;}
+            
+            public void setVariable(String variable) {this.variable = variable;}
+            
+            public String getLabel() {return label;}
+            
+            public void setLabel(String label) {this.label = label;}
+            
+            public String getType() {return type;}
+            
+            public void setType(String type) {this.type = type;}
+            
+            public int getMax_length() {return max_length;}
+            
+            public void setMax_length(int max_length) {this.max_length = max_length;}
+            
+            public boolean isRequired() {return required;}
+            
+            public void setRequired(boolean required) {this.required = required;}
+            
+            public List<?> getOptions() {return options;}
+            
+            public void setOptions(List<?> options) {this.options = options;}
+        }
+    }
+}

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/kefu/KeFuMessageServiceImpl.java

@@ -264,7 +264,7 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
             message.setCreateTime(LocalDateTime.now());
             TenantUtils.executeIgnore(() ->
                     keFuMessageMapper.updateById(BeanUtils.toBean(message, KeFuMessageDO.class)));
-            if (StrUtil.isNotBlank(message.getDifyConversationId())) {
+            if (!StrUtil.equals(difyId.toString(), message.getDifyConversationId())) {
                 KeFuConversationRespVO updatePinnedReqVO = new KeFuConversationRespVO();
                 updatePinnedReqVO.setId(message.getConversationId());
                 updatePinnedReqVO.setDifyConversationId(difyId.toString());