Просмотр исходного кода

1.企微聊天初版(未接入其他)

jzp 9 часов назад
Родитель
Сommit
265c4515fe

+ 104 - 0
fs-ai-api/src/main/java/com/fs/ai/rag/controller/LlmController.java

@@ -0,0 +1,104 @@
+package com.fs.ai.rag.controller;
+
+import cn.hutool.http.HttpRequest;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/ai/llm")
+public class LlmController {
+
+    @Value("${ai.rag.llm-url}")
+    private String llmUrl;
+
+    @Value("${ai.rag.llm-api-key:}")
+    private String apiKey;
+
+    @Value("${ai.rag.llm-model}")
+    private String model;
+
+    @PostMapping("/chat")
+    public R chat(@RequestBody Map<String, Object> request) {
+        String modelName = (String) request.getOrDefault("model", model);
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> messages = (List<Map<String, String>>) request.get("messages");
+        double temperature = request.get("temperature") != null
+                ? ((Number) request.get("temperature")).doubleValue() : 0.7;
+        int maxTokens = request.get("max_tokens") != null
+                ? ((Number) request.get("max_tokens")).intValue() : 2048;
+
+        if (messages == null || messages.isEmpty()) {
+            return R.error().put("msg", "messages 不能为空");
+        }
+
+        // 构建 OpenAI 兼容请求体
+        JSONObject reqBody = new JSONObject();
+        reqBody.put("model", modelName);
+
+        JSONArray msgArray = new JSONArray();
+        for (Map<String, String> msg : messages) {
+            JSONObject msgObj = new JSONObject();
+            msgObj.put("role", msg.get("role"));
+            msgObj.put("content", msg.get("content"));
+            msgArray.add(msgObj);
+        }
+        reqBody.put("messages", msgArray);
+        reqBody.put("temperature", temperature);
+        reqBody.put("max_tokens", maxTokens);
+
+        log.info("LLM 请求: model={}, messages.size={}", modelName, messages.size());
+
+        try {
+            String result = HttpRequest.post(llmUrl)
+                    .header("Authorization", "Bearer " + apiKey)
+                    .header("Content-Type", "application/json")
+                    .body(reqBody.toJSONString())
+                    .timeout(60000)
+                    .execute()
+                    .body();
+
+            JSONObject resp = JSON.parseObject(result);
+
+            // 检查是否有错误
+            if (resp.containsKey("error")) {
+                log.error("LLM 返回错误: {}", resp);
+                return R.error().put("msg", resp.getJSONObject("error").getString("message"));
+            }
+
+            JSONArray choices = resp.getJSONArray("choices");
+            if (choices == null || choices.isEmpty()) {
+                return R.error().put("msg", "LLM 返回为空");
+            }
+
+            String content = choices.getJSONObject(0)
+                    .getJSONObject("message")
+                    .getString("content");
+
+            JSONObject data = new JSONObject();
+            data.put("content", content);
+            data.put("model", resp.getString("model"));
+            data.put("usage", resp.getJSONObject("usage"));
+
+            log.info("LLM 回复成功: content.length={}, usage={}",
+                    content != null ? content.length() : 0, resp.getJSONObject("usage"));
+
+            return R.ok().put("data", data);
+
+        } catch (Exception e) {
+            log.error("LLM 调用异常: {}", e.getMessage(), e);
+            return R.error().put("msg", "LLM 调用失败: " + e.getMessage());
+        }
+    }
+}

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAdminController.java

@@ -1,4 +1,4 @@
-package com.fs.company.controller;
+package com.fs.company.controller.workflow;
 
 import com.fs.company.service.workflow.evolution.EvolutionEngine;
 import com.fs.company.service.workflow.evolution.UserNodeOptimizer;

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/workflow/PayCallbackController.java

@@ -1,4 +1,4 @@
-package com.fs.company.controller;
+package com.fs.company.controller.workflow;
 
 import com.fs.company.service.workflow.pay.PayService;
 import org.slf4j.Logger;

+ 2 - 1
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -371,7 +371,8 @@ public class QwMsgController {
                         final String finalContent = content;
                         if (2000000000000000L-receiver>0){
                             log.info("id:{}, 客户发送", id);
-                            aiHookService.qwHookNotifyAiReply(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
+                            //aiHookService.qwHookNotifyAiReply(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
+                            aiHookService.qwHookNotifyAiReplyByLobster(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
                         }else {
                             log.info("销售发送");
                             aiHookService.qwHookNotifyAddMsgNew(id,receiver,content,wxWorkMsgResp.getUuid(),1);

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/ai/AiProviderManager.java

@@ -167,7 +167,7 @@ public class AiProviderManager {
         public final int maxTokens;
         public final double temperature;
 
-        ProviderConfig(String code, String name, String endpoint, String key,
+        public ProviderConfig(String code, String name, String endpoint, String key,
                       String model, int maxTokens, double temperature) {
             this.providerCode = code;
             this.providerName = name;

+ 3 - 0
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -12,6 +12,9 @@ public interface AiHookService {
     /** ai回复**/
     R qwHookNotifyAiReply(Long qwUserID, Long sender,String count,String uid,Integer type,Long tenantId);
 
+    /** 龙虾ai回复**/
+    R qwHookNotifyAiReplyByLobster(Long qwUserID, Long sender,String count,String uid,Integer type,Long tenantId);
+
     /** 转人工 **/
     void artificial(QwHookVO vo);
 

+ 964 - 1
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -76,6 +76,7 @@ import org.jetbrains.annotations.Nullable;
 import org.json.JSONObject;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
@@ -204,6 +205,42 @@ public class AiHookServiceImpl implements AiHookService {
     @Qualifier("redisTemplateForInteger")
     RedisTemplate<String, Integer> redisForInteger;
 
+    /** 豆包大模型网关(直接调用) **/
+    @Autowired
+    private com.fs.company.service.ai.AiModelGateway aiModelGateway;
+
+    /** AI知识库服务(Qdrant向量检索) **/
+    @Autowired(required = false)
+    private com.fs.company.service.AiKnowledgeBaseService aiKnowledgeBaseService;
+
+    /** AI API 基础地址 **/
+    @Value("${ai.api.base-url:http://localhost:9009}")
+    private String aiApiBaseUrl;
+
+    /** 龙虾工作流任务查询(按 send_time 选取当前阶段任务) **/
+    @Autowired(required = false)
+    private com.fs.company.mapper.CompanyWorkflowLobsterTaskMapper lobsterTaskMapper;
+
+    /** 龙虾工作流节点查询(读取提示词配置) **/
+    @Autowired(required = false)
+    private com.fs.company.mapper.CompanyWorkflowLobsterNodeMapper lobsterNodeMapper;
+
+    /** 质量评分服务(8维度评分体系) **/
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.QualityScoringService qualityScoringService;
+
+    /** 龙虾节点执行日志(保存评分结果) **/
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterNodeExecutionLogMapper executionLogMapper;
+
+    /** 龙虾聊天会话(多渠道聚合) **/
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterChatSessionMapper lobsterChatSessionMapper;
+
+    /** 龙虾聊天消息(多渠道聚合) **/
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterChatMsgMapper lobsterChatMsgMapper;
+
     /** Ai半小时未回复提醒 **/
     /**
      *
@@ -398,6 +435,713 @@ public class AiHookServiceImpl implements AiHookService {
 
     @Autowired
     private DataSource dataSource;
+
+    /** 龙虾Ai回复 **/
+    @Override
+    public R qwHookNotifyAiReplyByLobster(Long qwUserId, Long sender,String qwContent,String uid,Integer type,Long tenantId) {
+        if (qwContent==null||qwContent.isEmpty()){
+            return R.ok();
+        }
+        if (qwContent.trim().isEmpty()){
+            return R.ok();
+        }
+
+        log.info("数据:{}", qwUserId);
+        QwUser user = qwUserMapper.selectQwUserById(qwUserId);
+        String corpId = user.getCorpId();
+        //查询接收人
+        if(user==null){
+            log.error("查询接收人为空");
+            return R.ok();
+        }
+
+
+        if(user.getAiStatus() == 1){
+            log.error("ai已下线:{}",user);
+            return R.ok();
+        }
+
+        Long serverId = user.getServerId();
+        log.info("服务器id"+serverId);
+        if (serverId == null) {
+            log.error("服务id为空");
+            return R.ok();
+        }
+
+        WxWorkVid2UserIdDTO wxWorkVid2UserIdDTO = new WxWorkVid2UserIdDTO();
+        wxWorkVid2UserIdDTO.setUser_id(Arrays.asList(sender));
+        wxWorkVid2UserIdDTO.setUuid(uid);
+        WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> WxWorkVid2UserIdRespDTO = wxWorkService.Vid2UserId(wxWorkVid2UserIdDTO,serverId);
+        List<WxWorkVid2UserIdRespDTO> data = WxWorkVid2UserIdRespDTO.getData();
+        if (data==null|| data.isEmpty()){
+
+            log.error("未获取到extId"+wxWorkVid2UserIdDTO);
+            return R.ok();
+        }
+        com.fs.wxwork.dto.WxWorkVid2UserIdRespDTO dto = data.get(0);
+        QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(dto.getOpenid(), corpId,user.getQwUserId());
+        try {
+            if (qwExternalContacts == null){
+                String url = OpenQwConfig.baseApi + "/getOpenExternalUserid?externalUserid=" + dto.getOpenid() + "&corpId=" + corpId + "&qwUserId=" + user.getQwUserId() + "&tenantId=" + tenantId;
+                String result = HttpUtil.createPost(url)
+                        .execute()
+                        .body();
+                R r = com.alibaba.fastjson.JSONObject.parseObject(result, R.class);
+                String openExternalUserid = r.get("openExternalUserid").toString();
+                log.info("openExternalUserid"+openExternalUserid);
+                if(StringUtils.isNotBlank(openExternalUserid)){
+                    qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(openExternalUserid, corpId,user.getQwUserId());
+                    log.info("corpId:{},userId:{},查询结果{}",corpId,user.getQwUserId(),qwExternalContacts);
+                }
+            }
+        } catch (Exception e) {
+            log.error("未查询到外部联系人id:{},报错{}",e,dto.getOpenid());
+        }
+        if (qwExternalContacts==null){
+            log.error("没有外部联系人" + "user:" + user);
+            return R.ok();
+        }
+        if(qwExternalContacts.getType()==2){
+
+            return R.ok();
+        }
+        // 添加脱敏逻辑
+        if(qwExternalContacts.getType() == 1){
+            FastGptChatSession fastGptChatSession= getFastGptSession(qwExternalContacts,user,dto);
+            // ── 龙虾聊天会话同步(find-or-create)──
+            Long lobsterSessionId = null;
+            if (lobsterChatSessionMapper != null) {
+                try {
+                    com.fs.company.domain.LobsterChatSession lobsterSession = lobsterChatSessionMapper.selectBySourceAndChannel(
+                        user.getCompanyId(), dto.getOpenid(), "QW");
+                    if (lobsterSession == null) {
+                        com.fs.company.domain.LobsterChatSession newLobsterSession = new com.fs.company.domain.LobsterChatSession();
+                        newLobsterSession.setCompanyId(user.getCompanyId());
+                        newLobsterSession.setContactId(qwExternalContacts.getId());
+                        newLobsterSession.setChannelType("QW");
+                        newLobsterSession.setChannelSourceId(dto.getOpenid());
+                        newLobsterSession.setChannelSourceType("qw_user");
+                        newLobsterSession.setUserId(dto.getOpenid());
+                        newLobsterSession.setExternalUserId(dto.getOpenid());
+                        newLobsterSession.setStatus(1);
+                        lobsterChatSessionMapper.insert(newLobsterSession);
+                        lobsterSessionId = newLobsterSession.getSessionId();
+                        log.info("[Lobster] 创建chat_session: id={}, companyId={}, contactId={}", lobsterSessionId, user.getCompanyId(), qwExternalContacts.getId());
+                    } else {
+                        lobsterSessionId = lobsterSession.getSessionId();
+                    }
+                } catch (Exception e) {
+                    log.warn("[Lobster] chat_session同步失败: {}", e.getMessage());
+                }
+            }
+            if (qwContent.contains("验证请求") || qwContent.contains("联系人验证请求") || qwContent.contains("我已经添加了你")){
+                return R.ok();
+            }
+            if(type == 104||type == 101){
+                String imageParse = aiImgUtil.getImageParse(qwContent,user,sender);
+                if (imageParse==null){
+                    return R.ok();
+                }
+                if(!imageParse.contains("表情包") && type == 104){
+                    qwContent="用户发送图片内容:"+"\"未被识别的表情包\"";
+                }else{
+                    String img = imgEmoticon(imageParse);
+                    if (img==null|| img.isEmpty()){
+                        qwContent="用户发送图片内容:"+"\""+imageParse+"\"";
+                    }else {
+                        qwContent=img;
+                    }
+                }
+            }else {
+                log.info("不是图片"+type);
+            }
+
+            if("==语音转换失败==".equals(qwContent)){
+                log.error("语音转换失败转人工:"+qwExternalContacts.getName() + ",uid" + uid);
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 语音转换失败转人工",qwExternalContacts.getId(),sender);
+                return R.ok();
+            }
+
+            //对用户处理的内容做处理,去除手机号替换
+            String maskedContent = processContent(qwContent);
+            String contentEmj = replaceWxEmo(maskedContent);
+            if(!contentEmj.contains("表情包")){
+                if(!contentEmj.isEmpty()){
+                    addSaveAiMsg(1,1,contentEmj,user,fastGptChatSession.getSessionId(),0L,qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+                    // ── 龙虾聊天消息同步(用户消息)──
+                    if (lobsterChatMsgMapper != null && lobsterSessionId != null) {
+                        try {
+                            com.fs.company.domain.LobsterChatMsg lobsterUserMsg = new com.fs.company.domain.LobsterChatMsg();
+                            lobsterUserMsg.setSessionId(lobsterSessionId);
+                            lobsterUserMsg.setCompanyId(user.getCompanyId());
+                            lobsterUserMsg.setChannelType("QW");
+                            lobsterUserMsg.setContent(contentEmj);
+                            lobsterUserMsg.setMsgType(1);
+                            lobsterUserMsg.setSendType(1);
+                            lobsterUserMsg.setStatus(0);
+                            lobsterChatMsgMapper.insert(lobsterUserMsg);
+                        } catch (Exception e) {
+                            log.warn("[Lobster] 用户chat_msg同步失败: {}", e.getMessage());
+                        }
+                    }
+                    //通过用户发送的对话去查询用户是否为新客,是就删除sop,否就不做处理
+                    cleanNewUserDialogue(user, qwExternalContacts);
+                    //用户是未回复状态
+                    if(qwExternalContacts.getIsReply() == 0){
+                        qwExternalContactMapper.updateQwExternalContactIsRePlyById(qwExternalContacts.getId());
+                    }
+                }else {
+                    contentEmj ="用户发送表情:"+qwContent;
+                    if (type==16){
+                        return R.ok();
+                    }
+                }
+            }
+
+
+            //判断是否转人工
+            if (fastGptChatSession.getIsArtificial()==1){
+                log.error("转人工了,sessionId:" + fastGptChatSession.getSessionId());
+                return R.ok();
+            }
+            //获取是用户是否发送消息
+            Integer reply = (Integer) redisForObject.opsForValue().get("reply:" + fastGptChatSession.getSessionId());
+            Integer replyI=1;
+            //用户正在发送消息 不发
+            if (reply!=null&&reply!=0){
+                //更新用户发送消息次数
+                redisForObject.opsForValue().set("reply:" + fastGptChatSession.getSessionId(),reply+1,5, TimeUnit.MINUTES);
+                //获取用户之前发送的消息
+                String msg = (String) redisForObject.opsForValue().get("msg:" + fastGptChatSession.getSessionId());
+                if (!msg.isEmpty()){
+                    //更新用户发送消息内容
+                    redisForObject.opsForValue().set("msg:" + fastGptChatSession.getSessionId(),msg+","+contentEmj,5,TimeUnit.MINUTES);
+                }
+                //本次跳过
+                log.info("正在对话");
+                return R.ok();
+            }
+            //用户首次发送消息
+            redisForObject.opsForValue().set("reply:" + fastGptChatSession.getSessionId(),1,5,TimeUnit.MINUTES);
+            redisForObject.opsForValue().set("msg:" + fastGptChatSession.getSessionId(),contentEmj,5,TimeUnit.MINUTES);
+            log.info("等待");
+            //R r= sendAiMsg(replyI,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts,sender);
+            R r= sendAiMsgNew(replyI,fastGptChatSession,user,qwExternalContacts,sender);
+            EventLogUtils.recordEventLog(sender,1L,1,user);
+            EventLogUtils.recordEventLog(sender,1L,2,user);
+            log.info("数据:{}", r);
+            //完成对话 删除消息记录
+            redisForObject.delete("reply:" + fastGptChatSession.getSessionId());
+            redisForObject.delete("msg:" + fastGptChatSession.getSessionId());
+            if(!r.get("code").equals(200)){
+                //判断消息是否需要重发的依据
+                log.error("ai报错转人工:"+qwExternalContacts.getName());
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai报错转人工",qwExternalContacts.getId(),sender);
+                return R.ok();
+            }
+            @SuppressWarnings("unchecked")
+            Map<String, Object> resultMap = r;
+            if (resultMap.isEmpty()) {
+                return R.ok();
+            }
+            String content = (String) resultMap.get("content");
+            if (content == null || content.isEmpty()) {
+                return R.ok();
+            }
+            //存聊天记录(纯文本)
+            addSaveAiMsg(1,2,content,user,fastGptChatSession.getSessionId(),0L,qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+
+            // ── 龙虾聊天消息同步(AI回复)──
+            if (lobsterChatMsgMapper != null && lobsterSessionId != null) {
+                try {
+                    com.fs.company.domain.LobsterChatMsg lobsterAiMsg = new com.fs.company.domain.LobsterChatMsg();
+                    lobsterAiMsg.setSessionId(lobsterSessionId);
+                    lobsterAiMsg.setCompanyId(user.getCompanyId());
+                    lobsterAiMsg.setChannelType("QW");
+                    lobsterAiMsg.setContent(content);
+                    lobsterAiMsg.setMsgType(1);
+                    lobsterAiMsg.setSendType(2);
+                    lobsterAiMsg.setStatus(0);
+                    lobsterChatMsgMapper.insert(lobsterAiMsg);
+                    // 更新会话最后消息
+                    String lastMsg = content.length() > 200 ? content.substring(0, 200) : content;
+                    lobsterChatSessionMapper.updateLastMsg(lobsterSessionId, lastMsg);
+                } catch (Exception e) {
+                    log.warn("[Lobster] AI chat_msg同步失败: {}", e.getMessage());
+                }
+            }
+
+            Boolean isLongText = (Boolean) resultMap.get("isLongText");
+            Boolean isArtificial = (Boolean) resultMap.get("isArtificial");
+
+            if (Boolean.TRUE.equals(isArtificial)) {
+                log.error("ai请求人工:0L"+ qwExternalContacts.getName());
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai请求人工协助",qwExternalContacts.getId(),sender);
+                return R.ok();
+            }
+            if (Boolean.TRUE.equals(isLongText)){
+                //新增用户信息
+                //addUserInfoNew(content, qwExternalContacts.getId(),fastGptChatSession);
+                //发送图片消息
+                //sendImgMsg(content,sender,uid,serverId);
+                sendAiMsgByLobster(content,sender,uid,serverId);
+            }else {
+                List<String> countList = countString(content);
+                //新增用户信息
+                //addUserInfoNew(content, qwExternalContacts.getId(),fastGptChatSession);
+                //发送图片消息
+                //sendImgMsg(content,sender,uid,serverId);
+                for (String msg : countList) {
+                    sendAiMsgByLobster(msg,sender,uid,serverId);
+                    try {
+                        Thread.sleep(500);
+                    } catch (InterruptedException e) {
+
+                    }
+                    Integer replyH = (Integer) redisForObject.opsForValue().get("reply:" + fastGptChatSession.getSessionId());
+                    //用户正在发送消息 后面的消息不发了
+                    if (replyH!=null&&replyH!=0){
+                        return R.ok();
+                    }
+                }
+            }
+        }
+        return R.ok();
+    }
+
+    private R sendAiMsgNew(Integer i, FastGptChatSession fastGptChatSession, QwUser user, QwExternalContact qwExternalContacts, Long sender) {
+        //等待0.5秒
+        try {
+            Thread.sleep(500);
+        } catch (InterruptedException e) {
+            log.warn("sendAiMsgNew 等待被中断");
+        }
+        //获取现在的次数
+        Integer reply = (Integer) redisForObject.opsForValue().get("reply:" + fastGptChatSession.getSessionId());
+        if (!Objects.equals(reply, i)) {
+            //次数变动 重新等待
+            return sendAiMsgNew(reply, fastGptChatSession, user, qwExternalContacts, sender);
+        }
+
+        //获取用户消息
+        String msgC = (String) redisForObject.opsForValue().get("msg:" + fastGptChatSession.getSessionId());
+        if (msgC == null || msgC.isEmpty()) {
+            return R.error("消息内容为空");
+        }
+
+        //组装prompt(含向量知识库 + 聊天记录)
+        String prompt = addPromptWordByLobster(msgC, qwExternalContacts, fastGptChatSession, user);
+
+        //保存AI接收到的消息(用于记录)
+        addSaveAiMsg(2, 1, msgC, user, fastGptChatSession.getSessionId(), 0L, qwExternalContacts, fastGptChatSession.getUserId(), null, null, null);
+
+        //再次检查次数是否变动
+        Integer reply2 = (Integer) redisForObject.opsForValue().get("reply:" + fastGptChatSession.getSessionId());
+        if (!Objects.equals(reply2, i)) {
+            return sendAiMsgNew(reply, fastGptChatSession, user, qwExternalContacts, sender);
+        }
+
+        //调用豆包大模型(固定参数)
+        com.fs.company.service.ai.AiProviderManager.ProviderConfig doubaoCfg =
+            new com.fs.company.service.ai.AiProviderManager.ProviderConfig(
+                "doubao", "豆包",
+                "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
+                "36417501-c96a-4d86-816c-5d8cc5b8ce09",
+                "ep-20260327095239-c2pqw", 4096, 0.7);
+        com.fs.company.service.llm.ModelResponse modelResp;
+        try {
+            modelResp = aiModelGateway.chatWithConfig(prompt, null, doubaoCfg);
+        } catch (Exception e) {
+            log.error("豆包大模型调用失败: {}", e.getMessage());
+            return R.error("AI调用失败");
+        }
+
+        String aiContent = modelResp.getContent();
+        if (aiContent == null || aiContent.isEmpty()) {
+            return R.error("AI返回空内容");
+        }
+
+        // ── 质量评分(8维度龙虾评分体系:相关性/专业性/完整性/自然度/合规性/知识库一致性/目标对齐性/拟人度,满分160)──
+        int qualityScore = 0;
+        java.util.Map<String, Integer> dimensionScores = null;
+        try {
+            if (qualityScoringService != null) {
+                com.fs.company.service.workflow.QualityScoringService.ScoringResult scoringResult =
+                    qualityScoringService.scoreWithRetry(
+                        user.getCompanyId(), aiContent, msgC, null, null, null);
+                qualityScore = scoringResult.isRegenerated()
+                    ? scoringResult.getSecondScore() : scoringResult.getFirstScore();
+                dimensionScores = scoringResult.getDimensionScores();
+                log.info("AI回复质量评分: total={}, passed={}, regenerated={}",
+                    qualityScore, scoringResult.isFinalPassed(), scoringResult.isRegenerated());
+
+                // 保存龙虾节点执行日志(含8维度评分)
+                if (executionLogMapper != null) {
+                    com.fs.company.domain.LobsterNodeExecutionLog execLog =
+                        new com.fs.company.domain.LobsterNodeExecutionLog();
+                    execLog.setCompanyId(user.getCompanyId());
+                    execLog.setNodeCode("ai_chat");
+                    execLog.setNodeType("AI_CHAT");
+                    execLog.setNodeName("多轮对话交互");
+                    execLog.setInputContent(msgC);
+                    execLog.setOutputContent(aiContent);
+                    execLog.setAiModel("doubao");
+                    execLog.setStatus("SUCCESS");
+                    execLog.setQualityScore(qualityScore);
+                    execLog.setTokenUsage(modelResp.getTotalTokens());
+                    execLog.setDurationMs(null);
+                    execLog.setDelFlag(0);
+                    executionLogMapper.insert(execLog);
+                    log.debug("龙虾节点执行日志已保存: logId={}, qualityScore={}", execLog.getId(), qualityScore);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("AI回复质量评分失败(不影响主流程): {}", e.getMessage());
+        }
+
+        //保存AI回复消息(完整原始回复)
+        addSaveAiMsg(2, 2, aiContent, user, fastGptChatSession.getSessionId(), 0L, qwExternalContacts, fastGptChatSession.getUserId(), modelResp.getPromptTokens(), modelResp.getCompletionTokens(), modelResp.getTotalTokens());
+
+        //判断是否长文本(>500字)
+        boolean isLongText = aiContent.length() > 500;
+
+        //判断是否转人工(含转人工关键词)
+        boolean isArtificial = false;
+        java.util.List<FastgptChatArtificialWords> artificialWords = qwExternalContactMapper.selectChatGptChatArtificialWords();
+        if (artificialWords != null) {
+            for (FastgptChatArtificialWords w : artificialWords) {
+                if (w.getContent() != null && aiContent.contains(w.getContent())) {
+                    isArtificial = true;
+                    break;
+                }
+            }
+        }
+
+        Map<String, Object> resultMap = new HashMap<>();
+        resultMap.put("content", aiContent);
+        resultMap.put("promptTokens", modelResp.getPromptTokens());
+        resultMap.put("completionTokens", modelResp.getCompletionTokens());
+        resultMap.put("totalTokens", modelResp.getTotalTokens());
+        resultMap.put("isLongText", isLongText);
+        resultMap.put("isArtificial", isArtificial);
+        resultMap.put("qualityScore", qualityScore);
+        if (dimensionScores != null) {
+            resultMap.put("dimensionScores", dimensionScores);
+        }
+
+        return R.ok(resultMap);
+    }
+
+    /**
+     * 从 company_workflow_lobster_node 表读取提示词,按客户创建时间选择
+     *
+     * nodeConfig JSON 格式示例:
+     * {
+     *   "systemPrompt": "基础提示词...",
+     *   "timePrompts": [
+     *     {"maxDays": 7,  "prompt": "新客户提示词..."},
+     *     {"maxDays": 30, "prompt": "普通客户提示词..."},
+     *     {"prompt": "老客户提示词..."}
+     *   ]
+     * }
+     * 若未配置 timePrompts,直接使用 systemPrompt 或 messageTemplate
+     */
+    private String getSystemPromptFromNode(Long companyId, QwExternalContact qwExternalContacts) {
+        // 获取客户创建天数
+        long customerDays = 999;
+        QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(qwExternalContacts.getId());
+        if (info != null && info.getCreateTime() != null) {
+            long diffMs = System.currentTimeMillis() - info.getCreateTime().getTime();
+            customerDays = diffMs / (1000L * 60 * 60 * 24);
+        }
+
+        // 查询公司绑定的龙虾工作流任务(按 send_time 比较当前时间选取阶段任务)
+        com.fs.company.domain.CompanyWorkflowLobsterTask matchedTask = null;
+        if (lobsterTaskMapper != null && companyId != null) {
+            try {
+                com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.fs.company.domain.CompanyWorkflowLobsterTask> taskWrapper =
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+                taskWrapper.eq(com.fs.company.domain.CompanyWorkflowLobsterTask::getCompanyId, companyId)
+                           .eq(com.fs.company.domain.CompanyWorkflowLobsterTask::getDelFlag, 0)
+                           .orderByDesc(com.fs.company.domain.CompanyWorkflowLobsterTask::getSendTime);
+                java.util.List<com.fs.company.domain.CompanyWorkflowLobsterTask> tasks = lobsterTaskMapper.selectList(taskWrapper);
+                if (tasks != null && !tasks.isEmpty()) {
+                    java.time.LocalDateTime now = java.time.LocalDateTime.now();
+                    // 找到第一个 sendTime <= now 的任务;若无则取最后一个(往前取最早的任务)
+                    for (com.fs.company.domain.CompanyWorkflowLobsterTask t : tasks) {
+                        if (t.getSendTime() != null && !t.getSendTime().isAfter(now)) {
+                            matchedTask = t;
+                            break;
+                        }
+                    }
+                    if (matchedTask == null) {
+                        // 所有任务 sendTime 都在未来 → 取最早的那个(往前取)
+                        matchedTask = tasks.get(tasks.size() - 1);
+                    }
+                    log.debug("匹配到的龙虾工作流任务: id={}, sendTime={}, lobsterNodeId={}",
+                        matchedTask.getId(), matchedTask.getSendTime(), matchedTask.getLobsterNodeId());
+                }
+            } catch (Exception e) {
+                log.warn("查询龙虾工作流任务失败: {}", e.getMessage());
+            }
+        }
+
+        // 查 ai_chat 节点配置(优先通过任务的 lobsterNodeId 直查,降级用 templateId+nodeCode)
+        com.fs.company.domain.CompanyWorkflowLobsterNode aiChatNode = null;
+        if (lobsterNodeMapper != null && matchedTask != null) {
+            try {
+                // 优先:通过任务的 lobsterNodeId 直接查询节点
+                if (matchedTask.getLobsterNodeId() != null) {
+                    aiChatNode = lobsterNodeMapper.selectById(matchedTask.getLobsterNodeId());
+                }
+                // 降级:通过 templateId + nodeCode 查询
+                if (aiChatNode == null && matchedTask.getTemplateId() != null) {
+                    com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.fs.company.domain.CompanyWorkflowLobsterNode> wrapper =
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+                    wrapper.eq(com.fs.company.domain.CompanyWorkflowLobsterNode::getWorkflowId, matchedTask.getTemplateId())
+                           .eq(com.fs.company.domain.CompanyWorkflowLobsterNode::getNodeCode, "ai_chat")
+                           .eq(com.fs.company.domain.CompanyWorkflowLobsterNode::getDelFlag, 0);
+                    java.util.List<com.fs.company.domain.CompanyWorkflowLobsterNode> nodes = lobsterNodeMapper.selectList(wrapper);
+                    if (nodes != null && !nodes.isEmpty()) {
+                        aiChatNode = nodes.get(0);
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("查询AI对话节点失败: {}", e.getMessage());
+            }
+        }
+
+        // 解析 nodeConfig 按时间选提示词
+        String customerName = qwExternalContacts.getName() != null ? qwExternalContacts.getName() : "客户";
+        StringBuilder prompt = new StringBuilder();
+        prompt.append("【系统指令】\n");
+
+        if (aiChatNode != null && aiChatNode.getNodeConfig() != null && !aiChatNode.getNodeConfig().isEmpty()) {
+            try {
+                com.alibaba.fastjson.JSONObject config = com.alibaba.fastjson.JSON.parseObject(aiChatNode.getNodeConfig());
+                com.alibaba.fastjson.JSONArray timePrompts = config.getJSONArray("timePrompts");
+
+                if (timePrompts != null && !timePrompts.isEmpty()) {
+                    // 按客户创建天数匹配 timePrompts(改进版:支持 minDays 显式区间 + 占位符 + 空隙降级)
+                    String matchedPrompt = null;
+                    String phaseName = null;
+                    int previousMax = -1;
+
+                    for (int i = 0; i < timePrompts.size(); i++) {
+                        com.alibaba.fastjson.JSONObject tp = timePrompts.getJSONObject(i);
+                        Integer maxDays = tp.getInteger("maxDays");
+                        Integer minDays = tp.getInteger("minDays");
+
+                        if (maxDays == null) {
+                            // 兜底条目(无上限),若还没匹配到则使用
+                            if (matchedPrompt == null) {
+                                matchedPrompt = tp.getString("prompt");
+                                phaseName = tp.getString("phase");
+                            }
+                            break;
+                        }
+
+                        // 实际区间: [min或上条max+1, maxDays]
+                        int actualMin = (minDays != null) ? minDays : (previousMax + 1);
+                        if (customerDays >= actualMin && customerDays <= maxDays) {
+                            matchedPrompt = tp.getString("prompt");
+                            phaseName = tp.getString("phase");
+                            break;
+                        }
+                        previousMax = maxDays;
+                    }
+
+                    // 仍未匹配且超出所有区间 → 使用最后一条(最近阶段)
+                    if (matchedPrompt == null && !timePrompts.isEmpty()) {
+                        com.alibaba.fastjson.JSONObject last = timePrompts.getJSONObject(timePrompts.size() - 1);
+                        matchedPrompt = last.getString("prompt");
+                        phaseName = last.getString("phase");
+                    }
+
+                    if (matchedPrompt != null) {
+                        // 占位符替换
+                        matchedPrompt = matchedPrompt
+                            .replace("{客户姓名}", customerName)
+                            .replace("{添加天数}", String.valueOf(customerDays))
+                            .replace("{阶段}", phaseName != null ? phaseName : "");
+                        prompt.append(matchedPrompt).append("\n");
+
+                        if (phaseName != null) {
+                            prompt.append("- 当前阶段:").append(phaseName).append(",客户姓名=").append(customerName).append(",添加天数=").append(customerDays).append("天\n\n");
+                        } else {
+                            prompt.append("- 客户信息:姓名=").append(customerName).append(",添加天数=").append(customerDays).append("天\n\n");
+                        }
+                        return prompt.toString();
+                    }
+                }
+
+                // 无 timePrompts,使用 systemPrompt
+                String sysPrompt = config.getString("systemPrompt");
+                if (sysPrompt != null && !sysPrompt.isEmpty()) {
+                    prompt.append(sysPrompt).append("\n");
+                    prompt.append("- 客户信息:姓名=").append(customerName).append("\n\n");
+                    return prompt.toString();
+                }
+            } catch (Exception e) {
+                log.warn("解析nodeConfig提示词失败: {}", e.getMessage());
+            }
+
+            // 降级:使用 messageTemplate
+            if (aiChatNode.getMessageTemplate() != null && !aiChatNode.getMessageTemplate().isEmpty()) {
+                prompt.append(aiChatNode.getMessageTemplate()).append("\n");
+                prompt.append("- 客户信息:姓名=").append(customerName).append("\n\n");
+                return prompt.toString();
+            }
+        }
+
+        // 最终兜底:使用默认提示词
+        prompt.append("你是一个专业的客服助手,请根据以下信息回复客户问题:\n");
+        prompt.append("- 客户信息:姓名=").append(customerName).append("\n\n");
+        return prompt.toString();
+    }
+
+    /**
+     * 组装豆包大模型 prompt(含向量知识库 + 聊天记录)
+     *
+     * @param currentMsg           当前用户消息
+     * @param qwExternalContacts   客户信息
+     * @param fastGptChatSession   会话信息
+     * @param user                 企微用户(提供companyId等)
+     * @return 组装好的完整 prompt 字符串
+     */
+    private String addPromptWordByLobster(String currentMsg, QwExternalContact qwExternalContacts, FastGptChatSession fastGptChatSession, QwUser user) {
+        StringBuilder prompt = new StringBuilder();
+
+        // ── 一、系统指令(从 company_workflow_lobster_node 动态读取,按客户创建时间选择) ──
+        String systemPrompt = getSystemPromptFromNode(user.getCompanyId(), qwExternalContacts);
+        prompt.append(systemPrompt);
+
+        // ── 二、聊天历史 + 知识库检索(Qdrant向量 + 关键词双路召回,参照 addPromptWordNew)──
+        Long extId = qwExternalContacts.getId();
+        List<FastGptChatMsg> msgs = fastGptChatMsgService.selectFastGptChatMsgByMsgSessionIdAndExtId(
+            fastGptChatSession.getSessionId(), extId);
+
+        // 构建增强检索上下文(最近6条对话 + 当前消息)
+        String contextQuery = currentMsg;
+        if (msgs != null && !msgs.isEmpty()) {
+            Collections.reverse(msgs);
+            msgs.remove(msgs.size() - 1);
+            StringBuilder contextBuilder = new StringBuilder();
+            int historyCount = 0;
+            for (FastGptChatMsg msg : msgs) {
+                Integer sendType = msg.getSendType();
+                String content = msg.getContent();
+                if (sendType != null && sendType != 1 && content != null && content.length() > 500) continue;
+                if (content != null && !content.trim().isEmpty() && historyCount < 6) {
+                    contextBuilder.insert(0, (sendType != null && sendType == 1 ? "用户:" : "AI:") + content + "\n");
+                    historyCount++;
+                }
+            }
+            if (contextBuilder.length() > 0 && currentMsg != null && !currentMsg.trim().isEmpty()) {
+                contextQuery = contextBuilder.toString().trim() + "\n用户:" + currentMsg;
+            }
+        }
+
+        // Qdrant双路召回(向量语义 + 关键词全文)
+        if (aiKnowledgeBaseService != null && currentMsg != null && !currentMsg.trim().isEmpty()) {
+            String searchQuery = contextQuery;
+            log.info("知识库检索查询文本 | original={} | contextQuery={}", currentMsg, searchQuery);
+            try {
+                com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.fs.company.domain.AiKnowledgeBase> lqw =
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+                lqw.eq(com.fs.company.domain.AiKnowledgeBase::getDelFlag, 0);
+                java.util.List<com.fs.company.domain.AiKnowledgeBase> kbList = aiKnowledgeBaseService.list(lqw);
+
+                if (kbList != null && !kbList.isEmpty()) {
+                    // 向量生成(只一次)
+                    java.util.List<Float> vector = createEmbedding(searchQuery);
+                    // 关键词提取
+                    java.util.List<String> ctxKeywords = extractKeywords(searchQuery);
+                    java.util.List<String> curKeywords = extractKeywords(currentMsg);
+                    java.util.Set<String> allKeywords = new java.util.LinkedHashSet<>(curKeywords);
+                    allKeywords.addAll(ctxKeywords);
+                    // 去重合并(同一 pointId 保留最高分)
+                    java.util.Map<String, java.util.Map<String, Object>> mergedRefMap = new java.util.LinkedHashMap<>();
+                    for (com.fs.company.domain.AiKnowledgeBase kb : kbList) {
+                        String collectionName = kb.getCollectionName();
+                        if (collectionName == null || collectionName.trim().isEmpty()) continue;
+                        // 路一:向量语义召回
+                        if (vector != null && !vector.isEmpty()) {
+                            java.util.List<java.util.Map<String, Object>> hits = searchQdrant(collectionName, vector, KB_VECTOR_TOP_K, KB_SCORE_THRESHOLD);
+                            mergeRefItems(mergedRefMap, hits, kb, "embedding");
+                        }
+                        // 路二:Payload 关键词全文检索召回
+                        if (!allKeywords.isEmpty()) {
+                            for (String keyword : allKeywords) {
+                                java.util.List<java.util.Map<String, Object>> hits = searchQdrantByPayload(collectionName, keyword, KB_KEYWORD_TOP_K);
+                                mergeRefItems(mergedRefMap, hits, kb, "fullText");
+                            }
+                        }
+                    }
+                    // 按 score 降序、截取 topN
+                    java.util.List<java.util.Map<String, Object>> knowledgeBase = mergedRefMap.values().stream()
+                        .sorted((a, b) -> Double.compare(toScore(b.get("score")), toScore(a.get("score"))))
+                        .limit(KB_FINAL_TOP_N)
+                        .collect(java.util.stream.Collectors.toList());
+
+                    if (!knowledgeBase.isEmpty()) {
+                        prompt.append("【知识库参考】\n");
+                        int idx = 1;
+                        for (java.util.Map<String, Object> item : knowledgeBase) {
+                            Object q = item.get("q"), a = item.get("a");
+                            StringBuilder kbText = new StringBuilder();
+                            if (q != null && !q.toString().trim().isEmpty())
+                                kbText.append("Q: ").append(q).append("\n");
+                            if (a != null && !a.toString().trim().isEmpty())
+                                kbText.append("A: ").append(a);
+                            if (kbText.length() > 0) {
+                                prompt.append(idx++).append(". ").append(kbText).append("\n");
+                            }
+                        }
+                        if (idx > 1) prompt.append("\n");
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("向量知识库检索失败: {}", e.getMessage());
+            }
+        }
+
+        // ── 三、历史对话记录 ──
+        if (msgs != null && !msgs.isEmpty()) {
+            prompt.append("【历史对话】\n");
+            // 反转后最新的在前,移除最后一条(即当前正在处理的消息,参照 addPromptWordNew 的处理方式)
+            Collections.reverse(msgs);
+            msgs.remove(msgs.size() - 1);
+            // 取最近20条,再反转以时间正序展示
+            int maxHistory = Math.min(msgs.size(), 20);
+            List<FastGptChatMsg> recentMsgs = msgs.subList(0, maxHistory);
+            Collections.reverse(recentMsgs);
+            for (FastGptChatMsg msg : recentMsgs) {
+                String content = msg.getContent();
+                if (content == null || content.trim().isEmpty()) continue;
+                Integer sendType = msg.getSendType();
+                // AI消息超过500字直接跳过(参照 addPromptWordNew 的处理方式)
+                if (sendType != null && sendType != 1) {
+                    if (content.length() > 500) continue;
+                }
+                // 用户消息截断到300字
+                if (content.length() > 300) {
+                    content = content.substring(0, 300) + "...";
+                }
+                String roleLabel = (sendType != null && sendType == 1) ? "user" : "ai";
+                prompt.append(roleLabel).append(": ").append(content).append("\n");
+            }
+            prompt.append("\n");
+        }
+
+        // ── 四、当前用户消息 ──
+        prompt.append("【当前用户消息】\n");
+        prompt.append(currentMsg).append("\n\n");
+        prompt.append("请根据以上信息,生成专业的客服回复:");
+
+        return prompt.toString();
+    }
+
     /** Ai回复 **/
     @Override
     public R qwHookNotifyAiReply(Long qwUserId, Long sender,String qwContent,String uid,Integer type,Long tenantId) {
@@ -1129,6 +1873,22 @@ public class AiHookServiceImpl implements AiHookService {
 
     @Autowired
     QwSopLogsMapper qwSopLogsMapper;
+    private void sendAiMsgByLobster(String content, Long sendId , String uuid,Long serverId) {
+        if (content == null || content.trim().isEmpty()){
+            System.out.println("输出为空格");
+            return;
+        }
+        WxWorkSendTextMsgDTO wxWorkSendTextMsgDTO = new WxWorkSendTextMsgDTO();
+        wxWorkSendTextMsgDTO.setSend_userid(sendId);
+        wxWorkSendTextMsgDTO.setUuid(uuid);
+        wxWorkSendTextMsgDTO.setContent(replaceWords(content));
+        wxWorkSendTextMsgDTO.setIsRoom(false);
+        WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> wxWorkSendTextMsgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(wxWorkSendTextMsgDTO,serverId);
+        WxWorkSendTextMsgRespDTO data = wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getData();
+
+
+    }
+
     private void sendAiMsg(String content, Long sendId , String uuid,Long serverId) {
         if (content == null || content.trim().isEmpty()){
             System.out.println("输出为空格");
@@ -1378,7 +2138,9 @@ public class AiHookServiceImpl implements AiHookService {
             fastGptChatSession = new FastGptChatSession();
             String chatId = UUID.randomUUID().toString();
             fastGptChatSession.setChatId(chatId);
-            fastGptChatSession.setKfId(user.getFastGptRoleId().toString());
+            if(user.getFastGptRoleId() != null){
+                fastGptChatSession.setKfId(user.getFastGptRoleId().toString());
+            }
             fastGptChatSession.setStatus(1);
             fastGptChatSession.setRemindCount(0);
             fastGptChatSession.setRemindStatus(0);
@@ -2362,4 +3124,205 @@ public class AiHookServiceImpl implements AiHookService {
         return wxWorkService.downloadWeChatFile(weChatFileDTO, serverId);
     }
 
+    //========向量知识库检索参数阈值(调优入口)========
+    /** 向量召回每个 collection 取 topK */
+    private static final int KB_VECTOR_TOP_K = 5;
+    /** 关键词召回每个 keyword 取 topK */
+    private static final int KB_KEYWORD_TOP_K = 5;
+    /** 向量召回分数阈值 */
+    private static final double KB_SCORE_THRESHOLD = 0.4D;
+    /** 最终输出到 prompt 的最大条数 */
+    private static final int KB_FINAL_TOP_N = 10;
+
+    /** 合并检索结果到去重 map(同一 pointId 保留最高分) */
+    private void mergeRefItems(java.util.Map<String, java.util.Map<String, Object>> merged,
+                               java.util.List<java.util.Map<String, Object>> hits,
+                               com.fs.company.domain.AiKnowledgeBase kb,
+                               String searchMode) {
+        if (hits == null || hits.isEmpty()) return;
+        for (java.util.Map<String, Object> hit : hits) {
+            java.util.Map<String, Object> ref = toFastGptRefItem(hit, kb, searchMode);
+            if (ref == null) continue;
+            String dedupKey = buildDedupKey(ref);
+            java.util.Map<String, Object> exist = merged.get(dedupKey);
+            if (exist == null || toScore(ref.get("score")) > toScore(exist.get("score"))) {
+                merged.put(dedupKey, ref);
+            }
+        }
+    }
+
+    /** 将单条 Qdrant 检索结果转换为引用项 */
+    @SuppressWarnings("unchecked")
+    private java.util.Map<String, Object> toFastGptRefItem(java.util.Map<String, Object> hit,
+                                                           com.fs.company.domain.AiKnowledgeBase kb,
+                                                           String searchMode) {
+        if (hit == null) return null;
+        Object payloadObj = hit.get("payload");
+        if (!(payloadObj instanceof java.util.Map)) return null;
+        java.util.Map<?, ?> payload = (java.util.Map<?, ?>) payloadObj;
+        Object qObj = payload.get("q");
+        Object aObj = payload.get("a");
+        if (qObj == null && aObj == null) return null;
+
+        java.util.Map<String, Object> ref = new java.util.LinkedHashMap<>();
+        Object id = hit.get("id");
+        ref.put("id", id == null ? "" : id.toString());
+        ref.put("q", qObj == null ? "" : qObj.toString());
+        ref.put("a", aObj == null ? "" : aObj.toString());
+        ref.put("sourceName", kb.getName() == null ? "" : kb.getName());
+        ref.put("sourceId", kb.getId() == null ? "" : kb.getId().toString());
+        ref.put("datasetId", kb.getCollectionId() == null ? "" : kb.getCollectionId());
+        ref.put("collectionId", kb.getCollectionName() == null ? "" : kb.getCollectionName());
+
+        double scoreVal = toScore(hit.get("score"));
+        java.util.List<java.util.Map<String, Object>> scoreArr = new java.util.ArrayList<>(1);
+        java.util.Map<String, Object> scoreItem = new java.util.LinkedHashMap<>();
+        scoreItem.put("type", searchMode);
+        scoreItem.put("value", scoreVal);
+        scoreItem.put("index", 0);
+        scoreArr.add(scoreItem);
+        ref.put("score", scoreArr);
+        return ref;
+    }
+
+    /** 以 collectionId+pointId 构建去重键 */
+    private String buildDedupKey(java.util.Map<String, Object> ref) {
+        Object collectionId = ref.get("collectionId");
+        Object id = ref.get("id");
+        if (id != null && !id.toString().isEmpty()) return collectionId + ":" + id;
+        return collectionId + ":" + ref.get("q") + ":" + ref.get("a");
+    }
+
+    /** 提取分数,兼容 Number 与 FastGPT score 数组结构 */
+    @SuppressWarnings("unchecked")
+    private double toScore(Object scoreObj) {
+        if (scoreObj instanceof Number) return ((Number) scoreObj).doubleValue();
+        if (scoreObj instanceof java.util.List) {
+            double max = 0D;
+            for (Object o : (java.util.List<?>) scoreObj) {
+                if (o instanceof java.util.Map) {
+                    Object v = ((java.util.Map<?, ?>) o).get("value");
+                    if (v instanceof Number) {
+                        double dv = ((Number) v).doubleValue();
+                        if (dv > max) max = dv;
+                    }
+                }
+            }
+            return max;
+        }
+        return 0D;
+    }
+
+    /** 提取关键词 */
+    private java.util.List<String> extractKeywords(String text) {
+        java.util.List<String> keywords = new java.util.ArrayList<>();
+        if (text == null || text.trim().isEmpty()) return keywords;
+        java.util.regex.Matcher durationMatcher = java.util.regex.Pattern.compile("\\d+\\s*[天日周月年]").matcher(text);
+        while (durationMatcher.find()) keywords.add(durationMatcher.group().replaceAll("\\s+", ""));
+        String[] productWords = {"套餐", "方案", "服务", "产品", "价格", "费用", "优惠", "活动", "会员", "课程", "项目"};
+        for (String word : productWords) {
+            if (text.contains(word)) keywords.add(word);
+        }
+        return keywords;
+    }
+
+    /** Payload 关键词全文检索 */
+    @SuppressWarnings("unchecked")
+    private java.util.List<java.util.Map<String, Object>> searchQdrantByPayload(String collectionName, String keyword, int limit) {
+        try {
+            com.alibaba.fastjson.JSONObject req = new com.alibaba.fastjson.JSONObject();
+            req.put("collectionName", collectionName);
+            req.put("vector", java.util.Collections.nCopies(1024, 0.0f));
+            req.put("topK", limit);
+            req.put("scoreThreshold", 0.0);
+            com.alibaba.fastjson.JSONObject filter = new com.alibaba.fastjson.JSONObject();
+            com.alibaba.fastjson.JSONArray should = new com.alibaba.fastjson.JSONArray();
+            com.alibaba.fastjson.JSONObject qMatch = new com.alibaba.fastjson.JSONObject();
+            qMatch.put("key", "q");
+            com.alibaba.fastjson.JSONObject qMatchValue = new com.alibaba.fastjson.JSONObject();
+            qMatchValue.put("value", keyword);
+            qMatch.put("match", qMatchValue);
+            should.add(qMatch);
+            com.alibaba.fastjson.JSONObject aMatch = new com.alibaba.fastjson.JSONObject();
+            aMatch.put("key", "a");
+            com.alibaba.fastjson.JSONObject aMatchValue = new com.alibaba.fastjson.JSONObject();
+            aMatchValue.put("value", keyword);
+            aMatch.put("match", aMatchValue);
+            should.add(aMatch);
+            filter.put("should", should);
+            req.put("filter", filter);
+
+            String url = aiApiBaseUrl + "/qdrant/point/search";
+            String result = HttpUtil.post(url, req.toJSONString());
+            com.alibaba.fastjson.JSONObject resp = com.alibaba.fastjson.JSONObject.parseObject(result);
+            Integer code = resp.getInteger("code");
+            if (code == null || code != 200) return null;
+            Object dataObj = resp.get("data");
+            if (dataObj instanceof java.util.List) {
+                java.util.List<java.util.Map<String, Object>> results = new java.util.ArrayList<>();
+                for (Object item : (java.util.List<?>) dataObj) {
+                    if (item instanceof java.util.Map) results.add((java.util.Map<String, Object>) item);
+                }
+                return results;
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("Payload关键词搜索失败 | collectionName={} | keyword={}", collectionName, keyword, e);
+            return null;
+        }
+    }
+
+    /** 生成 Embedding 向量 */
+    @SuppressWarnings("unchecked")
+    private java.util.List<Float> createEmbedding(String text) {
+        try {
+            com.alibaba.fastjson.JSONObject req = new com.alibaba.fastjson.JSONObject();
+            req.put("text", text);
+            String url = aiApiBaseUrl + "/ai/embedding/create";
+            String result = HttpUtil.post(url, req.toJSONString());
+            com.alibaba.fastjson.JSONObject resp = com.alibaba.fastjson.JSONObject.parseObject(result);
+            Integer code = resp.getInteger("code");
+            if (code == null || code != 200) return null;
+            com.alibaba.fastjson.JSONArray embeddingArray = resp.getJSONArray("data");
+            if (embeddingArray == null || embeddingArray.isEmpty()) return null;
+            java.util.List<Float> vector = new java.util.ArrayList<>();
+            for (Object item : embeddingArray) {
+                if (item instanceof Number) vector.add(((Number) item).floatValue());
+            }
+            return vector;
+        } catch (Exception e) {
+            log.error("生成Embedding向量失败 | text={}", text, e);
+            return null;
+        }
+    }
+
+    /** 向量语义检索 */
+    @SuppressWarnings("unchecked")
+    private java.util.List<java.util.Map<String, Object>> searchQdrant(String collectionName, java.util.List<Float> vector, int topK, double scoreThreshold) {
+        try {
+            com.alibaba.fastjson.JSONObject req = new com.alibaba.fastjson.JSONObject();
+            req.put("collectionName", collectionName);
+            req.put("vector", vector);
+            req.put("topK", topK);
+            req.put("scoreThreshold", scoreThreshold);
+            String url = aiApiBaseUrl + "/qdrant/point/search";
+            String result = HttpUtil.post(url, req.toJSONString());
+            com.alibaba.fastjson.JSONObject resp = com.alibaba.fastjson.JSONObject.parseObject(result);
+            Integer code = resp.getInteger("code");
+            if (code == null || code != 200) return null;
+            Object dataObj = resp.get("data");
+            if (dataObj instanceof java.util.List) {
+                java.util.List<java.util.Map<String, Object>> results = new java.util.ArrayList<>();
+                for (Object item : (java.util.List<?>) dataObj) {
+                    if (item instanceof java.util.Map) results.add((java.util.Map<String, Object>) item);
+                }
+                return results;
+            }
+            return null;
+        } catch (Exception e) {
+            log.error("Qdrant搜索失败 | collectionName={}", collectionName, e);
+            return null;
+        }
+    }
+
 }

+ 1 - 1
fs-service/src/main/java/com/fs/oms/service/impl/OmsProductServiceImpl.java

@@ -24,7 +24,7 @@ import com.fs.oms.mapper.OmsProductMapper;
 import com.fs.oms.param.OmsProductAddEditParam;
 import com.fs.oms.service.IOmsProductService;
 import com.fs.oms.vo.OmsProductExportVO;
-import com.fs.oms.vo.OmsProductListVO;;
+import com.fs.oms.vo.OmsProductListVO;
 import com.fs.oms.vo.OmsDetailVO;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;

+ 2 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1367,8 +1367,8 @@ public class QwUserServiceImpl implements IQwUserService
         WxWorkSetCallbackUrlDTO wxWorkSetCallbackUrlDTO = new WxWorkSetCallbackUrlDTO();
 
         System.out.println("回调地址"+"http://newsaasqwapimsg.ylrzcloud.com/msg/callback/"+serverId + "/"+loginParam.getTenantId());
-        wxWorkSetCallbackUrlDTO.setUrl("http://newsaasqwapimsg.ylrzcloud.com/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
-//        wxWorkSetCallbackUrlDTO.setUrl("http://t66e9dea.natappfree.cc/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
+//        wxWorkSetCallbackUrlDTO.setUrl("http://newsaasqwapimsg.ylrzcloud.com/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
+        wxWorkSetCallbackUrlDTO.setUrl("http://cn-hk-bgp-4.ofalias.net:55081/msg/callback/"+serverId+ "/"+loginParam.getTenantId());
         wxWorkSetCallbackUrlDTO.setUuid(data.getUuid());
         wxWorkService.SetCallbackUrl(wxWorkSetCallbackUrlDTO,serverId);
 

+ 1 - 1
fs-service/src/main/resources/mapper/fastGpt/FastGptChatSessionMapper.xml

@@ -29,7 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectFastGptChatSessionVo">
-        select session_id,remind_time,is_reply,last_time,remind_status,remind_count,qw_ext_id,qw_user_id,chat_id,is_artificial, user_id, kf_id, create_time, update_time, status, company_id, is_look, user_type, nick_name, avatar,userInfo from fastgpt_chat_session
+        select session_id,remind_time,is_reply,last_time,remind_status,remind_count,qw_ext_id,qw_user_id,chat_id,is_artificial, user_id, kf_id, create_time, update_time, status, company_id, is_look, user_type, nick_name, avatar,user_info from fastgpt_chat_session
     </sql>
 
     <select id="selectFastGptChatSessionList" resultMap="FastGptChatSessionResult">