|
|
@@ -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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|