|
|
@@ -4,12 +4,15 @@ import cn.hutool.core.util.StrUtil;
|
|
|
import cn.hutool.json.JSONUtil;
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.fs.common.annotation.Excel;
|
|
|
import com.fs.common.config.FSConfig;
|
|
|
import com.fs.common.core.domain.R;
|
|
|
import com.fs.common.core.redis.RedisCache;
|
|
|
import com.fs.company.domain.CompanyConfig;
|
|
|
+import com.fs.company.domain.CompanyUser;
|
|
|
import com.fs.company.mapper.CompanyConfigMapper;
|
|
|
+import com.fs.company.mapper.CompanyUserMapper;
|
|
|
import com.fs.config.ai.AiHostProper;
|
|
|
import com.fs.course.domain.FsUserCourseVideo;
|
|
|
import com.fs.course.mapper.FsCourseWatchLogMapper;
|
|
|
@@ -26,6 +29,7 @@ import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
|
|
|
import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
|
|
|
import com.fs.fastGpt.param.SendAIParam;
|
|
|
import com.fs.fastGpt.service.*;
|
|
|
+import com.fs.fastGpt.utils.TencentAudioUtils;
|
|
|
import com.fs.fastgptApi.param.ChatParam;
|
|
|
import com.fs.fastgptApi.result.ChatDetailTStreamFResult;
|
|
|
import com.fs.fastgptApi.result.KnowledgeBaseResult;
|
|
|
@@ -34,15 +38,27 @@ import com.fs.fastgptApi.service.Impl.AudioServiceImpl;
|
|
|
import com.fs.fastgptApi.util.AiImgUtil;
|
|
|
import com.fs.fastgptApi.util.EventLogUtils;
|
|
|
import com.fs.fastgptApi.vo.AudioVO;
|
|
|
+import com.fs.his.domain.FsInquiryOrderMsg;
|
|
|
import com.fs.his.domain.FsStoreOrder;
|
|
|
+import com.fs.his.domain.FsUser;
|
|
|
import com.fs.his.dto.ExpressInfoDTO;
|
|
|
+import com.fs.his.dto.PayloadDTO;
|
|
|
import com.fs.his.dto.TracesDTO;
|
|
|
import com.fs.his.enums.ShipperCodeEnum;
|
|
|
import com.fs.his.mapper.FsStoreMapper;
|
|
|
import com.fs.his.mapper.FsStoreOrderMapper;
|
|
|
+import com.fs.his.mapper.FsUserMapper;
|
|
|
import com.fs.his.service.IFsExpressService;
|
|
|
+import com.fs.his.service.IFsInquiryOrderMsgService;
|
|
|
import com.fs.his.service.IFsStoreOrderService;
|
|
|
+import com.fs.im.config.IMConfig;
|
|
|
+import com.fs.im.domain.ImChatMsg;
|
|
|
+import com.fs.im.domain.ImChatSession;
|
|
|
import com.fs.im.dto.OpenImMsgDTO;
|
|
|
+import com.fs.im.dto.OpenImResponseDTO;
|
|
|
+import com.fs.im.service.IImChatMsgService;
|
|
|
+import com.fs.im.service.IImChatSessionService;
|
|
|
+import com.fs.im.util.OpenIMUtil;
|
|
|
import com.fs.im.vo.OpenImMsgCallBackVO;
|
|
|
import com.fs.qw.domain.*;
|
|
|
import com.fs.qw.mapper.*;
|
|
|
@@ -61,6 +77,7 @@ import com.fs.utils.SensitiveDataUtils;
|
|
|
import com.fs.voice.utils.StringUtil;
|
|
|
import com.fs.wxwork.dto.*;
|
|
|
import com.fs.wxwork.service.WxWorkService;
|
|
|
+import com.google.gson.Gson;
|
|
|
import com.vdurmont.emoji.EmojiParser;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.ObjectUtils;
|
|
|
@@ -86,10 +103,25 @@ import java.util.regex.Pattern;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
import static com.fs.his.utils.PhoneUtil.decryptPhone;
|
|
|
+import static net.sf.jsqlparser.util.validation.metadata.NamedObject.user;
|
|
|
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
public class AiHookServiceImpl implements AiHookService {
|
|
|
+
|
|
|
+ /** OpenIM 内容类型:图片 */
|
|
|
+ private static final int IM_CONTENT_IMAGE = 102;
|
|
|
+ /** OpenIM 内容类型:语音 */
|
|
|
+ private static final int IM_CONTENT_VOICE = 103;
|
|
|
+ /** OpenIM 内容类型:视频 */
|
|
|
+ private static final int IM_CONTENT_VIDEO = 104;
|
|
|
+ /** IM AI 回复 Redis 缓存时长(分钟),与 sendFsUserIMAiMsg 内 5s 等待配合实现防抖 */
|
|
|
+ private static final int IM_REPLY_CACHE_MINUTES = 5;
|
|
|
+ /** sendFsUserIMAiMsg 内校验用的 reply 槽位,仅 slot 匹配时才继续请求 FastGPT */
|
|
|
+ private static final int IM_REPLY_SLOT = 1;
|
|
|
+ private static final String IM_REPLY_CACHE_KEY = "IM:reply:";
|
|
|
+ private static final String IM_MSG_CACHE_KEY = "IM:msg:";
|
|
|
+
|
|
|
@Autowired
|
|
|
private FastGptChatSessionMapper fastGptChatSessionMapper;
|
|
|
@Autowired
|
|
|
@@ -97,6 +129,12 @@ public class AiHookServiceImpl implements AiHookService {
|
|
|
@Autowired
|
|
|
private QwUserMapper qwUserMapper;
|
|
|
@Autowired
|
|
|
+ private IFsInquiryOrderMsgService fsInquiryOrderMsgService;
|
|
|
+ @Autowired
|
|
|
+ IMConfig imConfig;
|
|
|
+ @Autowired
|
|
|
+ private IImChatMsgService imChatMsgService;
|
|
|
+ @Autowired
|
|
|
private IFastGptRoleService roleService;
|
|
|
@Autowired
|
|
|
private QwExternalContactInfoMapper qwExternalContactInfoMapper;
|
|
|
@@ -107,6 +145,8 @@ public class AiHookServiceImpl implements AiHookService {
|
|
|
@Autowired
|
|
|
QwApiService qwApiService;
|
|
|
@Autowired
|
|
|
+ private IImChatSessionService imChatSessionService;
|
|
|
+ @Autowired
|
|
|
QwCompanyMapper qwCompanyMapper;
|
|
|
|
|
|
@Autowired
|
|
|
@@ -168,6 +208,10 @@ public class AiHookServiceImpl implements AiHookService {
|
|
|
private IFastGptChatReplaceTextService fastGptChatReplaceTextService;
|
|
|
@Autowired
|
|
|
private ICrmMsgService crmMsgService;
|
|
|
+ @Autowired
|
|
|
+ private FsUserMapper fsUserMapper;
|
|
|
+ @Autowired
|
|
|
+ private CompanyUserMapper companyUserMapper;
|
|
|
|
|
|
private static final String AI_REPLY = "AI_REPLY:";
|
|
|
private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
|
|
|
@@ -330,8 +374,844 @@ public class AiHookServiceImpl implements AiHookService {
|
|
|
}
|
|
|
return R.ok();
|
|
|
}
|
|
|
-
|
|
|
+ /**
|
|
|
+ * ai自动回复 (IM渠道 - 基于FsUser)
|
|
|
+ * 适用场景:没有qwUser/qwExternalContact,只有fsUser信息的IM回调
|
|
|
+ * 转人工时通过IM发送消息给销售
|
|
|
+ */
|
|
|
@Override
|
|
|
+ public R AiReply(OpenImMsgCallBackVO openImMsgDTO, FsUser fsUser) {
|
|
|
+ if (fsUser == null) {
|
|
|
+ return R.error("用户信息为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ CompanyUser companyUser = resolveCompanyUserForFsUser(fsUser);
|
|
|
+ if (companyUser == null) {
|
|
|
+ log.error("AiReply(FsUser): 未找到关联的销售, fsUserId={}", fsUser.getUserId());
|
|
|
+ return R.error("未找到关联的销售");
|
|
|
+ }
|
|
|
+ if (companyUser.getFastgptRoleId() == null) {
|
|
|
+ return R.error("销售未绑定AI角色,请先绑定角色");
|
|
|
+ }
|
|
|
+
|
|
|
+ FastGptRole role = roleService.selectFastGptRoleTypeByRoleId(companyUser.getFastgptRoleId());
|
|
|
+ ModeConfig config = resolveModeConfig(role);
|
|
|
+ if (config == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ ImContentParseResult parseResult = parseImMultimediaContent(openImMsgDTO, companyUser, fsUser.getUserId());
|
|
|
+ if (parseResult.hasError()) {
|
|
|
+ return parseResult.getError();
|
|
|
+ }
|
|
|
+ String content = processContent(parseResult.getContent());
|
|
|
+
|
|
|
+ String conversationId = buildImConversationId(openImMsgDTO);
|
|
|
+ ImChatSession imChatSession = getImChatSession(
|
|
|
+ openImMsgDTO, companyUser, conversationId, companyUser.getUserId(), fsUser.getNickName());
|
|
|
+
|
|
|
+ R aiResponse = dispatchDebouncedAiRequest(
|
|
|
+ openImMsgDTO, content, role, fsUser, conversationId, config, imChatSession, companyUser);
|
|
|
+ clearImReplyCache(conversationId);
|
|
|
+
|
|
|
+ return handleAiReplyResponse(aiResponse, companyUser, role, fsUser, openImMsgDTO, imChatSession);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 FsUser 关联的销售:优先 companyUserId,否则经 qwUserId 间接查找
|
|
|
+ */
|
|
|
+ private CompanyUser resolveCompanyUserForFsUser(FsUser fsUser) {
|
|
|
+ CompanyUser companyUser = null;
|
|
|
+ if (fsUser.getCompanyUserId() != null) {
|
|
|
+ companyUser = companyUserMapper.selectCompanyUserByUserId(fsUser.getCompanyUserId());
|
|
|
+ }
|
|
|
+ if (companyUser == null && fsUser.getQwUserId() != null) {
|
|
|
+ QwUser qwUser = qwUserMapper.selectQwUserById(fsUser.getQwUserId());
|
|
|
+ if (qwUser != null && qwUser.getCompanyUserId() != null) {
|
|
|
+ companyUser = companyUserMapper.selectCompanyUserByUserId(qwUser.getCompanyUserId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return companyUser;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载 FastGPT 模式配置;角色或 APPKey 缺失时返回 null(与历史行为一致,非 R.error)
|
|
|
+ */
|
|
|
+ private ModeConfig resolveModeConfig(FastGptRole role) {
|
|
|
+ if (role == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String modeConfigJson = role.getModeConfigJson();
|
|
|
+ if (StringUtils.isEmpty(modeConfigJson)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ ModeConfig config = JSONUtil.toBean(modeConfigJson, ModeConfig.class);
|
|
|
+ if (StringUtils.isEmpty(config.getAPPKey())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return config;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildImConversationId(OpenImMsgCallBackVO openImMsgDTO) {
|
|
|
+ return "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 IM 多媒体消息转为 AI 可理解的文本。
|
|
|
+ * 图片/视频走 OCR;语音走腾讯云 ASR;普通文本原样返回。
|
|
|
+ */
|
|
|
+ private ImContentParseResult parseImMultimediaContent(OpenImMsgCallBackVO openImMsgDTO,
|
|
|
+ CompanyUser companyUser, Long senderId) {
|
|
|
+ String content = openImMsgDTO.getContent();
|
|
|
+ int contentType = openImMsgDTO.getContentType();
|
|
|
+
|
|
|
+ if (contentType == IM_CONTENT_IMAGE || contentType == IM_CONTENT_VIDEO) {
|
|
|
+ JSONObject contentObject = new JSONObject(openImMsgDTO.getContent());
|
|
|
+ String url = contentType == IM_CONTENT_IMAGE
|
|
|
+ ? contentObject.getJSONObject("sourcePicture").getString("url")
|
|
|
+ : contentObject.getString("videoUrl");
|
|
|
+ String imageParse = aiImgUtil.getIMImageParse(url, companyUser, senderId);
|
|
|
+ if (imageParse == null) {
|
|
|
+ return ImContentParseResult.error(R.error("图片解析失败"));
|
|
|
+ }
|
|
|
+ // 视频帧未识别为表情包时,固定提示文案,避免 AI 误判
|
|
|
+ if (!imageParse.contains("表情包") && contentType == IM_CONTENT_VIDEO) {
|
|
|
+ content = "用户发送图片内容:" + "\"未被识别的表情包\"";
|
|
|
+ } else {
|
|
|
+ String emoticon = imgEmoticon(imageParse);
|
|
|
+ content = (emoticon == null || emoticon.isEmpty())
|
|
|
+ ? "用户发送图片内容:" + "\"" + imageParse + "\""
|
|
|
+ : emoticon;
|
|
|
+ }
|
|
|
+ } else if (contentType == IM_CONTENT_VOICE) {
|
|
|
+ JSONObject contentObject = new JSONObject(openImMsgDTO.getContent());
|
|
|
+ String url = contentObject.getString("sourceUrl");
|
|
|
+ if (StringUtils.isEmpty(url)) {
|
|
|
+ return ImContentParseResult.error(R.error("语音解析失败,未获取到语音地址!"));
|
|
|
+ }
|
|
|
+ // 腾讯云 ASR:先上传音频拿 TaskId,再轮询识别结果
|
|
|
+ String uploadStr = TencentAudioUtils.doRequest(url, 0L, 1);
|
|
|
+ if (uploadStr == null) {
|
|
|
+ return ImContentParseResult.error(R.error("语音解析失败!上传录音失败!"));
|
|
|
+ }
|
|
|
+ JSONObject uploadObject = new JSONObject(uploadStr);
|
|
|
+ if (!uploadObject.getJSONObject("Response").isNull("Error")) {
|
|
|
+ log.error("腾讯云语音上传失败!{}", uploadObject.getJSONObject("Response")
|
|
|
+ .getJSONObject("Error").getString("Message"));
|
|
|
+ return ImContentParseResult.error(R.error("语音解析失败!"));
|
|
|
+ }
|
|
|
+ Long taskId = uploadObject.getJSONObject("Response").getJSONObject("Data").getLong("TaskId");
|
|
|
+ String voiceText = TencentAudioUtils.doRequest("", taskId, 2);
|
|
|
+ if (voiceText == null) {
|
|
|
+ return ImContentParseResult.error(R.error("语音解析失败!获取语音内容失败!"));
|
|
|
+ }
|
|
|
+ content = voiceText;
|
|
|
+ }
|
|
|
+ return ImContentParseResult.success(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * IM 连发防抖:Redis 累加 reply 计数并缓存最新消息。
|
|
|
+ * sendFsUserIMAiMsg 内 sleep 5s 后校验 reply 是否仍为 slot=1,否则丢弃过期请求。
|
|
|
+ */
|
|
|
+ private R dispatchDebouncedAiRequest(OpenImMsgCallBackVO openImMsgDTO, String content,
|
|
|
+ FastGptRole role, FsUser fsUser, String conversationId,
|
|
|
+ ModeConfig config, ImChatSession imChatSession,
|
|
|
+ CompanyUser companyUser) {
|
|
|
+ String replyKey = IM_REPLY_CACHE_KEY + conversationId;
|
|
|
+ String msgKey = IM_MSG_CACHE_KEY + conversationId;
|
|
|
+
|
|
|
+ Integer replyCount = redisCache.getCacheObject(replyKey);
|
|
|
+ if (replyCount == null) {
|
|
|
+ redisCache.setCacheObject(replyKey, 1, IM_REPLY_CACHE_MINUTES, TimeUnit.MINUTES);
|
|
|
+ } else {
|
|
|
+ redisCache.setCacheObject(replyKey, replyCount + 1, IM_REPLY_CACHE_MINUTES, TimeUnit.MINUTES);
|
|
|
+ }
|
|
|
+ redisCache.setCacheObject(msgKey, content, IM_REPLY_CACHE_MINUTES, TimeUnit.MINUTES);
|
|
|
+ return sendFsUserIMAiMsg(IM_REPLY_SLOT, openImMsgDTO, content, role, fsUser,
|
|
|
+ conversationId, config, imChatSession, companyUser);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void clearImReplyCache(String conversationId) {
|
|
|
+ redisCache.deleteObject(IM_REPLY_CACHE_KEY + conversationId);
|
|
|
+ redisCache.deleteObject(IM_MSG_CACHE_KEY + conversationId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理 FastGPT 响应:成功则落库并下发 IM 消息,失败则通知销售
|
|
|
+ *
|
|
|
+ * @param aiResponse
|
|
|
+ * @param companyUser
|
|
|
+ * @param role
|
|
|
+ * @param fsUser
|
|
|
+ * @param openImMsgDTO
|
|
|
+ * @param imChatSession
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private R handleAiReplyResponse(R aiResponse, CompanyUser companyUser, FastGptRole role,
|
|
|
+ FsUser fsUser, OpenImMsgCallBackVO openImMsgDTO,
|
|
|
+ ImChatSession imChatSession) {
|
|
|
+ if (aiResponse.get("code").equals(200)) {
|
|
|
+ ChatDetailTStreamFResult result = (ChatDetailTStreamFResult) aiResponse.get("data");
|
|
|
+ if (result.getChoices().isEmpty()) {
|
|
|
+ return R.error("AI回复空消息");
|
|
|
+ }
|
|
|
+ addSaveImChatMsg(companyUser, role, result.getChoices().get(0).getMessage().getContent(), imChatSession);
|
|
|
+ dealWithFsUserResult(result, companyUser, fsUser, openImMsgDTO, fsUser.getUserId(), role, imChatSession);
|
|
|
+ return R.ok();
|
|
|
+ }
|
|
|
+ notifySalesViaIM(companyUser, fsUser, "AI报错", openImMsgDTO);
|
|
|
+ return R.error("AI请求失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** IM 多媒体解析结果:error 非空表示需中断主流程 */
|
|
|
+ private static final class ImContentParseResult {
|
|
|
+ private final R error;
|
|
|
+ private final String content;
|
|
|
+
|
|
|
+ private ImContentParseResult(R error, String content) {
|
|
|
+ this.error = error;
|
|
|
+ this.content = content;
|
|
|
+ }
|
|
|
+
|
|
|
+ static ImContentParseResult success(String content) {
|
|
|
+ return new ImContentParseResult(null, content);
|
|
|
+ }
|
|
|
+
|
|
|
+ static ImContentParseResult error(R error) {
|
|
|
+ return new ImContentParseResult(error, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean hasError() {
|
|
|
+ return error != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ R getError() {
|
|
|
+ return error;
|
|
|
+ }
|
|
|
+
|
|
|
+ String getContent() {
|
|
|
+ return content;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于 FsUser 调用 FastGPT。
|
|
|
+ * 等待 5s 后校验 reply 槽位:连发场景下仅最新请求继续,较早请求直接返回 error。
|
|
|
+ */
|
|
|
+ private R sendFsUserIMAiMsg(int replySlot, OpenImMsgCallBackVO openImMsgDTO, String content,
|
|
|
+ FastGptRole role, FsUser fsUser, String conversationId,
|
|
|
+ ModeConfig config, ImChatSession imChatSession, CompanyUser companyUser) {
|
|
|
+ String replyKey = IM_REPLY_CACHE_KEY + conversationId;
|
|
|
+ try {
|
|
|
+ Thread.sleep(5000);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.warn("IM AI 防抖等待被中断, conversationId={}", conversationId);
|
|
|
+ }
|
|
|
+ if (!Objects.equals(redisCache.getCacheObject(replyKey), replySlot)) {
|
|
|
+ return R.error();
|
|
|
+ }
|
|
|
+
|
|
|
+ ChatParam param = new ChatParam();
|
|
|
+ param.setChatId(imChatSession.getChatId());
|
|
|
+ param.setStream(false);
|
|
|
+ param.setDetail(true);
|
|
|
+ ChatParam.Variables variables = new ChatParam.Variables();
|
|
|
+ variables.setUid(openImMsgDTO.getRecvID());
|
|
|
+ variables.setName(fsUser.getNickName());
|
|
|
+ List<ChatParam.Message> messageList = new ArrayList<>();
|
|
|
+ param.setMessages(messageList);
|
|
|
+
|
|
|
+ addFsUserIMKeyWord(role, fsUser, messageList, content, openImMsgDTO, imChatSession);
|
|
|
+
|
|
|
+ R response = chatService.initiatingTakeChat(param, "http://129.28.170.206:3000/api", config.getAPPKey());
|
|
|
+ recordFsUserAiTokenUsage(response, content, fsUser, companyUser);
|
|
|
+
|
|
|
+ if (!Objects.equals(redisCache.getCacheObject(replyKey), replySlot)) {
|
|
|
+ return R.error();
|
|
|
+ }
|
|
|
+ addSaveImChatMsg(companyUser, role, param.getMessages().get(0).getContent(), imChatSession);
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 非知识库命中时记录 FastGPT token 消耗 */
|
|
|
+ private void recordFsUserAiTokenUsage(R response, String content, FsUser fsUser, CompanyUser companyUser) {
|
|
|
+ Object data = response.get("data");
|
|
|
+ if (data instanceof KnowledgeBaseResult) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ChatDetailTStreamFResult chatResult = (ChatDetailTStreamFResult) data;
|
|
|
+ EventLogUtils.recordEventTokenLog(content + "消耗token", fsUser.getUserId(), 2,
|
|
|
+ (long) chatResult.getUsage().getTotal_tokens(), 0, companyUser);
|
|
|
+ EventLogUtils.recordEventTokenLog(content + "输入token", fsUser.getUserId(), 2,
|
|
|
+ (long) chatResult.getUsage().getPrompt_tokens(), 1, companyUser);
|
|
|
+ EventLogUtils.recordEventTokenLog(content + "输出token", fsUser.getUserId(), 2,
|
|
|
+ (long) chatResult.getUsage().getCompletion_tokens(), 2, companyUser);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基于 FsUser 构建 AI 上下文(FastGptChatConversation JSON)
|
|
|
+ * userInfo 字段配置见 {@link FastGptRole#getUserInfo()},contactInfo 仍用于企微渠道
|
|
|
+ */
|
|
|
+ private void addFsUserIMKeyWord(FastGptRole role, FsUser fsUser, List<ChatParam.Message> messageList,
|
|
|
+ String content, OpenImMsgCallBackVO openImMsgDTO,
|
|
|
+ ImChatSession imChatSession) {
|
|
|
+
|
|
|
+ FastGptChatConversation conversation = new FastGptChatConversation();
|
|
|
+ conversation.setUserInfo(new com.alibaba.fastjson.JSONObject());
|
|
|
+ conversation.setHistory(new com.alibaba.fastjson.JSONArray());
|
|
|
+
|
|
|
+ if (role.getReminderWords() != null && !role.getReminderWords().isEmpty()) {
|
|
|
+ conversation.setAiInfo(role.getReminderWords());
|
|
|
+ }
|
|
|
+
|
|
|
+ fillFsUserInfo(conversation, role, fsUser, imChatSession);
|
|
|
+
|
|
|
+ com.alibaba.fastjson.JSONArray historyArray = buildFsUserImHistory(imChatSession, openImMsgDTO);
|
|
|
+ if (!historyArray.isEmpty()) {
|
|
|
+ conversation.setHistory(historyArray);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (content != null && !content.isEmpty()) {
|
|
|
+ conversation.setUserContent(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ ChatParam.Message message1 = new ChatParam.Message();
|
|
|
+ message1.setRole("user");
|
|
|
+ message1.setContent(new Gson().toJson(conversation));
|
|
|
+ messageList.add(message1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按角色 userInfo 配置(逗号分隔 Java 字段名),从 ImChatSession、FsUser 填充对话 userInfo
|
|
|
+ */
|
|
|
+ private void fillFsUserInfo(FastGptChatConversation conversation, FastGptRole role,
|
|
|
+ FsUser fsUser, ImChatSession imChatSession) {
|
|
|
+ String sessionUserInfo = imChatSession.getUserInfo();
|
|
|
+ String[] split = role.getUserInfo().split(",");
|
|
|
+ com.alibaba.fastjson.JSONObject userInfo = conversation.getUserInfo();
|
|
|
+ if(sessionUserInfo != null){
|
|
|
+ putBeanFields(userInfo, split, fsUser);
|
|
|
+ }else{
|
|
|
+ for (String name : split) {
|
|
|
+ if (name != null) {
|
|
|
+ userInfo.put(name,"");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void putBeanFields(com.alibaba.fastjson.JSONObject userInfo, String[] fieldNames, Object bean) {
|
|
|
+ if (bean == null || fieldNames == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Class<?> clazz = bean.getClass();
|
|
|
+ for (String rawName : fieldNames) {
|
|
|
+ if (rawName == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String name = rawName.trim();
|
|
|
+ if (name.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Field field = clazz.getDeclaredField(name);
|
|
|
+ field.setAccessible(true);
|
|
|
+ Object value = field.get(bean);
|
|
|
+ String jsonKey = resolveFieldJsonKey(field, name);
|
|
|
+ if (value != null) {
|
|
|
+ userInfo.put(jsonKey, value.toString());
|
|
|
+ } else if (!userInfo.containsKey(jsonKey)) {
|
|
|
+ userInfo.put(jsonKey, "");
|
|
|
+ }
|
|
|
+ } catch (NoSuchFieldException | IllegalAccessException ignored) {
|
|
|
+ // 当前 bean 无此字段,由其他数据源补充
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 有 @Excel 注解时用中文名作为 JSON key,与 AI 回复中的【用户状态信息】块一致 */
|
|
|
+ private String resolveFieldJsonKey(Field field, String fieldName) {
|
|
|
+ Excel excel = field.getAnnotation(Excel.class);
|
|
|
+ if (excel != null && StringUtils.isNotEmpty(excel.name())) {
|
|
|
+ return excel.name();
|
|
|
+ }
|
|
|
+ return fieldName;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建 IM 渠道历史对话(JSON 数组,role: user/ai)
|
|
|
+ */
|
|
|
+ private com.alibaba.fastjson.JSONArray buildFsUserImHistory(ImChatSession imChatSession,
|
|
|
+ OpenImMsgCallBackVO openImMsgDTO) {
|
|
|
+ com.alibaba.fastjson.JSONArray historyArray = new com.alibaba.fastjson.JSONArray();
|
|
|
+
|
|
|
+ if (imChatSession != null && imChatSession.getSessionId() != null) {
|
|
|
+ List<ImChatMsg> msgs = imChatMsgService.getListBySessionId(imChatSession.getSessionId());
|
|
|
+ if (msgs != null && !msgs.isEmpty()) {
|
|
|
+ String sessionUserId = imChatSession.getUserId();
|
|
|
+ for (ImChatMsg msg : msgs) {
|
|
|
+ String msgContent = StringUtils.isNotEmpty(msg.getShortContent())
|
|
|
+ ? msg.getShortContent() : msg.getContent();
|
|
|
+ boolean isUser;
|
|
|
+ if (msg.getUserType() != null) {
|
|
|
+ isUser = msg.getUserType() == 1;
|
|
|
+ } else {
|
|
|
+ isUser = sessionUserId != null && sessionUserId.equals(msg.getUserId());
|
|
|
+ }
|
|
|
+ if (!isUser && msgContent != null && msgContent.length() > 500) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ com.alibaba.fastjson.JSONObject msgObj = new com.alibaba.fastjson.JSONObject();
|
|
|
+ msgObj.put("role", isUser ? "user" : "ai");
|
|
|
+ msgObj.put("content", msgContent);
|
|
|
+ historyArray.add(msgObj);
|
|
|
+ }
|
|
|
+ return historyArray;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (openImMsgDTO == null) {
|
|
|
+ return historyArray;
|
|
|
+ }
|
|
|
+ List<FsInquiryOrderMsg> historyMsg = fsInquiryOrderMsgService.selectHistoryMsgByUserId(
|
|
|
+ stripPrefix(openImMsgDTO.getSendID(), "U"),
|
|
|
+ stripPrefix(openImMsgDTO.getRecvID(), "C"));
|
|
|
+ if (historyMsg == null || historyMsg.isEmpty()) {
|
|
|
+ return historyArray;
|
|
|
+ }
|
|
|
+ Collections.reverse(historyMsg);
|
|
|
+ for (FsInquiryOrderMsg inquiryMsg : historyMsg) {
|
|
|
+ String hisContent = inquiryMsg.getContent();
|
|
|
+ int contentType = inquiryMsg.getMsgContentType() != null ? inquiryMsg.getMsgContentType() : 1;
|
|
|
+ if (contentType != 1 && hisContent != null && hisContent.length() > 500) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ com.alibaba.fastjson.JSONObject msgObj = new com.alibaba.fastjson.JSONObject();
|
|
|
+ msgObj.put("role", "1".equals(inquiryMsg.getMsgType()) ? "user" : "ai");
|
|
|
+ msgObj.put("content", hisContent);
|
|
|
+ historyArray.add(msgObj);
|
|
|
+ }
|
|
|
+ return historyArray;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 AI 回复中的【用户状态信息】,回写 ImChatSession / FsUser(字段范围由 role.userInfo 配置)
|
|
|
+ */
|
|
|
+ private void updateImSessionUserInfoFromAi(String word, FastGptRole role,
|
|
|
+ ImChatSession imChatSession, FsUser fsUser) {
|
|
|
+ if (role == null || StringUtils.isEmpty(role.getUserInfo()) || StringUtils.isEmpty(word)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Pattern pattern = Pattern.compile("【用户状态信息(.*?)】", Pattern.DOTALL);
|
|
|
+ Matcher matcher = pattern.matcher(word);
|
|
|
+ if (!matcher.find()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String[] lines = matcher.group(1).trim().split("\n");
|
|
|
+ String[] fieldNames = role.getUserInfo().split(",");
|
|
|
+ boolean sessionUpdated = false;
|
|
|
+ boolean userUpdated = false;
|
|
|
+
|
|
|
+ if (imChatSession != null) {
|
|
|
+ sessionUpdated = applyAiUserInfoLines(lines, fieldNames, imChatSession);
|
|
|
+ }
|
|
|
+ if (fsUser != null) {
|
|
|
+ userUpdated = applyAiUserInfoLines(lines, fieldNames, fsUser);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sessionUpdated && imChatSession.getSessionId() != null) {
|
|
|
+ ImChatSession update = new ImChatSession();
|
|
|
+ update.setSessionId(imChatSession.getSessionId());
|
|
|
+ update.setStudy(imChatSession.getStudy());
|
|
|
+ update.setCourseStatus(imChatSession.getCourseStatus());
|
|
|
+ update.setCourseName(imChatSession.getCourseName());
|
|
|
+ update.setVideoName(imChatSession.getVideoName());
|
|
|
+ update.setTalk(imChatSession.getTalk());
|
|
|
+ update.setLastTalkTime(new Date());
|
|
|
+ imChatSessionService.updateImChatSession(update);
|
|
|
+ }
|
|
|
+ if (userUpdated && fsUser.getUserId() != null) {
|
|
|
+ fsUserMapper.updateFsUser(fsUser);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean applyAiUserInfoLines(String[] lines, String[] fieldNames, Object bean) {
|
|
|
+ boolean updated = false;
|
|
|
+ Class<?> clazz = bean.getClass();
|
|
|
+ for (String rawName : fieldNames) {
|
|
|
+ if (rawName == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String fieldName = rawName.trim();
|
|
|
+ if (fieldName.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Field field = clazz.getDeclaredField(fieldName);
|
|
|
+ field.setAccessible(true);
|
|
|
+ String jsonKey = resolveFieldJsonKey(field, fieldName);
|
|
|
+ for (String line : lines) {
|
|
|
+ String[] kv = splitUserInfoLine(line);
|
|
|
+ if (kv == null || kv.length != 2) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!kv[0].trim().equals(jsonKey)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String newValue = kv[1].trim();
|
|
|
+ if (newValue.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ("学习到的章节".equals(jsonKey) || "今日课程完成情况".equals(jsonKey)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (newValue.contains("delete")) {
|
|
|
+ if (newValue.contains(";")) {
|
|
|
+ newValue = newValue.replaceAll("delete.*?;", "");
|
|
|
+ } else {
|
|
|
+ newValue = " ";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Object oldValue = field.get(bean);
|
|
|
+ String oldStr = oldValue != null ? oldValue.toString().trim() : "";
|
|
|
+ if (!oldStr.equals(newValue)) {
|
|
|
+ field.set(bean, newValue);
|
|
|
+ updated = true;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (NoSuchFieldException | IllegalAccessException ignored) {
|
|
|
+ // 当前 bean 无此字段
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return updated;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String[] splitUserInfoLine(String line) {
|
|
|
+ if (line == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (line.contains(":")) {
|
|
|
+ return line.split(":", 2);
|
|
|
+ }
|
|
|
+ if (line.contains(":")) {
|
|
|
+ return line.split(":", 2);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理AI回复结果(基于FsUser版本)
|
|
|
+ */
|
|
|
+ private void dealWithFsUserResult(ChatDetailTStreamFResult result, CompanyUser companyUser,
|
|
|
+ FsUser fsUser, OpenImMsgCallBackVO openImMsgDTO, Long sender,
|
|
|
+ FastGptRole role, ImChatSession imChatSession) {
|
|
|
+ String contentKh = result.getChoices().get(0).getMessage().getContent();
|
|
|
+ String conversationId = "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
|
|
|
+ Gson gson = new Gson();
|
|
|
+ FastGptChatConversation fastGptChatConversation = gson.fromJson(contentKh, FastGptChatConversation.class);
|
|
|
+ String content = replace(fastGptChatConversation.getAiContent()).trim();
|
|
|
+
|
|
|
+ if (!content.isEmpty()) {
|
|
|
+ // 转人工关键词判断
|
|
|
+ List<String> collect = qwExternalContactMapper.selectChatGptChatArtificialWords().stream().map(m -> m.getContent()).collect(Collectors.toList());
|
|
|
+ if (collect.stream().anyMatch(contentKh::contains)) {
|
|
|
+ log.info(conversationId + " :触发转人工:" + contentKh);
|
|
|
+ markSessionAsHuman(imChatSession, openImMsgDTO);
|
|
|
+ // 通过IM通知销售
|
|
|
+ notifySalesViaIM(companyUser, fsUser, " 触发关键词", openImMsgDTO);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (result.isLongText()) {
|
|
|
+ sendIMMsg(content, companyUser, openImMsgDTO);
|
|
|
+ } else {
|
|
|
+ String sa = contentKh.replaceAll("】\n", "】").replaceAll("\n【", "【");
|
|
|
+ String nr = replace(sa);
|
|
|
+ String[] split = nr.split("\n");
|
|
|
+ if (split.length > 6) {
|
|
|
+ log.info(conversationId + " :消息过长,转人工");
|
|
|
+ markSessionAsHuman(imChatSession, openImMsgDTO);
|
|
|
+ notifySalesViaIM(companyUser, fsUser, " 回复长度异常", openImMsgDTO);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ List<String> countList = countString(content);
|
|
|
+ updateImSessionUserInfoFromAi(contentKh, role, imChatSession, fsUser);
|
|
|
+ for (String msg : countList) {
|
|
|
+ sendIMMsg(msg, companyUser, openImMsgDTO);
|
|
|
+ try {
|
|
|
+ Thread.sleep(10000);
|
|
|
+ } catch (InterruptedException ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // AI主动请求转人工
|
|
|
+ if (result.isArtificial()) {
|
|
|
+ log.info(conversationId + " :AI请求转人工:" + contentKh);
|
|
|
+ markSessionAsHuman(imChatSession, openImMsgDTO);
|
|
|
+ notifySalesViaIM(companyUser, fsUser, " AI请求人工协助", openImMsgDTO);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 标记会话为人工模式
|
|
|
+ */
|
|
|
+ private void markSessionAsHuman(ImChatSession imChatSession, OpenImMsgCallBackVO openImMsgDTO) {
|
|
|
+ ImChatSession updateSession = new ImChatSession();
|
|
|
+ updateSession.setSessionId(imChatSession.getSessionId());
|
|
|
+ String humanKey = "IM:SESSION:HUMAN:" + imChatSession.getChatId();
|
|
|
+ redisCache.setCacheObject(humanKey, "1", 10, TimeUnit.MINUTES);
|
|
|
+ updateSession.setPersonOrAi(1);
|
|
|
+ imChatSessionService.updateImChatSession(updateSession);
|
|
|
+ OpenIMUtil.editConversationEx(openImMsgDTO, imConfig, redisCache, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过IM通知销售转人工
|
|
|
+ */
|
|
|
+ private void notifySalesViaIM(CompanyUser companyUser, FsUser fsUser, String reason, OpenImMsgCallBackVO openImMsgDTO) {
|
|
|
+ log.info("IM转人工通知: fsUser={}, reason={}", fsUser.getNickName(), reason);
|
|
|
+ try {
|
|
|
+ // 向用户发送转人工提示
|
|
|
+ sendIMMsg("正在为您转接人工客服,请稍等...", companyUser, openImMsgDTO);
|
|
|
+
|
|
|
+ // 通过IM给销售发送转人工通知消息
|
|
|
+ OpenImMsgDTO notifyMsg = new OpenImMsgDTO();
|
|
|
+ OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
|
|
|
+ offlinePushInfo.setTitle("转人工通知");
|
|
|
+ offlinePushInfo.setDesc("客户" + fsUser.getNickName() + "需要人工服务");
|
|
|
+ offlinePushInfo.setIOSBadgeCount(true);
|
|
|
+
|
|
|
+ // 销售发送消息给用户,这里反过来:系统给销售发通知
|
|
|
+ // 发送者用系统ID,接收者用销售的IM ID(C + companyUserId)
|
|
|
+ String salesImId = "C" + companyUser.getUserId();
|
|
|
+ notifyMsg.setSendID(openImMsgDTO.getRecvID()); // 用原来的接收者(客服/销售ID)作为发送者
|
|
|
+ notifyMsg.setRecvID(salesImId); // 接收者是销售
|
|
|
+ notifyMsg.setContentType(101);
|
|
|
+ notifyMsg.setSenderPlatformID(openImMsgDTO.getSenderPlatformID());
|
|
|
+ notifyMsg.setSessionType(1); // 单聊
|
|
|
+
|
|
|
+ ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+ OpenImMsgDTO.Content msgContent = new OpenImMsgDTO.Content();
|
|
|
+ PayloadDTO payload = new PayloadDTO();
|
|
|
+ String notifyText = "【转人工通知】客户 " + fsUser.getNickName() + " 因" + reason + " 需要人工服务,请及时回复";
|
|
|
+ payload.setData(notifyText);
|
|
|
+ PayloadDTO.Extension extension = new PayloadDTO.Extension();
|
|
|
+ extension.setTitle("转人工通知");
|
|
|
+ payload.setExtension(extension);
|
|
|
+ OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
|
|
|
+ imData.setPayload(payload);
|
|
|
+ String imJson = objectMapper.writeValueAsString(imData);
|
|
|
+ msgContent.setData(imJson);
|
|
|
+ msgContent.setContent(notifyText);
|
|
|
+ notifyMsg.setContent(msgContent);
|
|
|
+ notifyMsg.setOfflinePushInfo(offlinePushInfo);
|
|
|
+
|
|
|
+ OpenIMUtil.openIMSendMsg(notifyMsg, imConfig, redisCache);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("通过IM通知销售转人工失败: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendIMMsg(String msg, CompanyUser companyUser, OpenImMsgCallBackVO openImMsgDTO) {
|
|
|
+ try {
|
|
|
+ if (msg == null || msg.trim().isEmpty()) {
|
|
|
+ log.info("输出为空格");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 违规词语替换
|
|
|
+ msg = replaceWords(msg);
|
|
|
+ OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
|
|
|
+ OpenImMsgDTO replyMsg = new OpenImMsgDTO();
|
|
|
+ if (null != companyUser && StringUtils.isNotEmpty(companyUser.getAvatar())) {
|
|
|
+ offlinePushInfo.setTitle(companyUser.getNickName());
|
|
|
+ replyMsg.setSenderFaceURL(companyUser.getAvatar());
|
|
|
+ }
|
|
|
+ // 初始化ObjectMapper对象,用于JSON序列化和反序列化
|
|
|
+ ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+ // 设置消息的发送者ID、接收者ID、内容类型等基础信息
|
|
|
+ // 将发送者和接收者交换一下
|
|
|
+ replyMsg.setSendID(openImMsgDTO.getRecvID());
|
|
|
+ replyMsg.setRecvID(openImMsgDTO.getSendID());
|
|
|
+ replyMsg.setContentType(101);
|
|
|
+ replyMsg.setSenderPlatformID(openImMsgDTO.getSenderPlatformID());
|
|
|
+ replyMsg.setSessionType(openImMsgDTO.getSessionType());
|
|
|
+ // 初始化消息内容对象
|
|
|
+ OpenImMsgDTO.Content content = new OpenImMsgDTO.Content();
|
|
|
+ // 初始化负载数据对象,并设置数据内容
|
|
|
+ PayloadDTO payload = new PayloadDTO();
|
|
|
+ payload.setData(msg);
|
|
|
+ PayloadDTO.Extension extension = new PayloadDTO.Extension();
|
|
|
+ extension.setTitle("快速回复"); // 可选标题
|
|
|
+ payload.setExtension(extension);
|
|
|
+ OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
|
|
|
+ imData.setPayload(payload);
|
|
|
+ String imJson = objectMapper.writeValueAsString(imData);
|
|
|
+ content.setData(imJson);
|
|
|
+ content.setContent(msg);
|
|
|
+ replyMsg.setContent(content);
|
|
|
+ offlinePushInfo.setDesc("快速回复");
|
|
|
+ offlinePushInfo.setIOSBadgeCount(true);
|
|
|
+ offlinePushInfo.setIOSPushSound("");
|
|
|
+ replyMsg.setOfflinePushInfo(offlinePushInfo);
|
|
|
+ // 发送消息
|
|
|
+ OpenImResponseDTO response = OpenIMUtil.openIMSendMsg(replyMsg, imConfig, redisCache);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("修改会话异常:{}", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void addIMKeyWord(String countInfo, Long extId, List<ChatParam.Message> messageList, String words, String content, OpenImMsgCallBackVO openImMsgDTO) {
|
|
|
+ String str = "";
|
|
|
+ // 这里获取后台的提示词进行匹配
|
|
|
+ QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(extId);
|
|
|
+ if (info == null) {
|
|
|
+ info = new QwExternalContactInfo();
|
|
|
+ }
|
|
|
+ if (info != null) {
|
|
|
+ str = "【用户状态信息\n";
|
|
|
+ Field[] fields = info.getClass().getDeclaredFields();
|
|
|
+ for (Field field : fields) {
|
|
|
+ field.setAccessible(true);
|
|
|
+ Excel annotation = field.getAnnotation(Excel.class);
|
|
|
+ if (annotation != null) {
|
|
|
+ String name = field.getName();
|
|
|
+ String fieldName = annotation.name();
|
|
|
+ String[] split = countInfo.split(",");
|
|
|
+ for (String zName : split) {
|
|
|
+ if (zName.equals(name)) {
|
|
|
+ Object value = null;
|
|
|
+ try {
|
|
|
+ value = field.get(info);
|
|
|
+ } catch (IllegalAccessException e) {
|
|
|
+ }
|
|
|
+ if (value != null) {
|
|
|
+ str += fieldName + ": " + value.toString() + "\n";
|
|
|
+ } else {
|
|
|
+ str += fieldName + ": \n";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ str += "】\n";
|
|
|
+
|
|
|
+ if (words != null && !"".equals(words)) {
|
|
|
+ str += "【你的角色信息:以下内容为你的信息状态的补充而非用户信息,相当于放在角色任务里面,问到了需要知晓,但是如果无关的时候请无视此段内容 " + "\"" + words + "\"" + "】\n";
|
|
|
+ }
|
|
|
+ // 无法查询IM历史消息智能查询本地消息
|
|
|
+ List<FsInquiryOrderMsg> historyMsg = fsInquiryOrderMsgService.selectHistoryMsgByUserId(openImMsgDTO.getSendID().replace("U", ""), openImMsgDTO.getRecvID().replace("C", ""));
|
|
|
+ //如果之前有消息 加下面这句话,没消息 跳过
|
|
|
+ if (!historyMsg.isEmpty()) {
|
|
|
+ Collections.reverse(historyMsg);
|
|
|
+ str += "【历史聊天内容:\n";
|
|
|
+ for (FsInquiryOrderMsg fsInquiryOrderMsg : historyMsg) {
|
|
|
+ int contentType = fsInquiryOrderMsg.getMsgContentType();
|
|
|
+ String hisContent = fsInquiryOrderMsg.getContent();
|
|
|
+ if (contentType != 1) {
|
|
|
+ if (hisContent != null && hisContent.length() > 150) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ str += ("1".equals(fsInquiryOrderMsg.getMsgType()) ? "用户:" : "AI:") + hisContent + "\n";
|
|
|
+ }
|
|
|
+ str += "】\n";
|
|
|
+ }
|
|
|
+ if (content != null && !"".equals(content)) {
|
|
|
+ str += "【用户说的话内容(之前的内容仅仅为背景,你知道即可,以下才是用户真实说的话的内容)\n" +
|
|
|
+ content + "\n" +
|
|
|
+ "】";
|
|
|
+ }
|
|
|
+ ChatParam.Message message1 = new ChatParam.Message();
|
|
|
+ message1.setRole("user");
|
|
|
+ message1.setContent(str);
|
|
|
+ messageList.add(message1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addSaveImChatMsg(CompanyUser companyUser, FastGptRole role, String content, ImChatSession imChatSession) {
|
|
|
+ ImChatMsg msg = new ImChatMsg();
|
|
|
+ msg.setSessionId(imChatSession.getSessionId());
|
|
|
+ msg.setUserId(String.valueOf(companyUser.getUserId()));
|
|
|
+ msg.setContent(content);
|
|
|
+ msg.setMsgType(2);
|
|
|
+ msg.setCompanyId(companyUser.getCompanyId());
|
|
|
+ msg.setRoleId(role.getRoleId());
|
|
|
+ msg.setCompanyUserId(companyUser.getUserId());
|
|
|
+ msg.setStatus(0);
|
|
|
+ msg.setNickName(companyUser.getNickName());
|
|
|
+ msg.setAvatar(companyUser.getAvatar());
|
|
|
+ msg.setCreateTime(new Date());
|
|
|
+ imChatMsgService.insertImChatMsg(msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ private ImChatSession getImChatSession(OpenImMsgCallBackVO openImMsgDTO, CompanyUser user, String conversationId, Long qwUserId, String name) {
|
|
|
+ ImChatSession session = imChatSessionService.selectImChatSessionByChatId(conversationId);
|
|
|
+ if (session == null) {
|
|
|
+ ImChatSession insert = new ImChatSession();
|
|
|
+ insert.setChatId(conversationId);
|
|
|
+ insert.setUserId(stripPrefix(openImMsgDTO.getSendID(), "U"));
|
|
|
+ insert.setKfId(stripPrefix(openImMsgDTO.getRecvID(), "C"));
|
|
|
+ insert.setCompanyId(user.getCompanyId());
|
|
|
+ insert.setNickName(user.getNickName());
|
|
|
+ insert.setAvatar(user.getAvatar());
|
|
|
+ insert.setCreateTime(new Date());
|
|
|
+ insert.setQwUserId(qwUserId);
|
|
|
+ insert.setUserName(name);
|
|
|
+ insert.setPersonOrAi(2);
|
|
|
+
|
|
|
+ imChatSessionService.insertImChatSession(insert);
|
|
|
+ return insert;
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean needUpdate = false;
|
|
|
+
|
|
|
+ if (session.getQwUserId() == null || !Objects.equals(session.getQwUserId(), qwUserId)) {
|
|
|
+ session.setQwUserId(qwUserId);
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Objects.equals(session.getUserName(), name)) {
|
|
|
+ session.setUserName(name);
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.isEmpty(session.getNickName())) {
|
|
|
+ session.setNickName(user.getNickName());
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.isEmpty(session.getUserName())) {
|
|
|
+ session.setUserName(name);
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+ if (session.getCompanyId() == null) {
|
|
|
+ session.setCompanyId(user.getCompanyId());
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+ if (StringUtils.isEmpty(session.getAvatar())) {
|
|
|
+ session.setAvatar(user.getAvatar());
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needUpdate) {
|
|
|
+ imChatSessionService.updateImChatSession(session);
|
|
|
+ }
|
|
|
+
|
|
|
+ return session;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String stripPrefix(String value, String prefix) {
|
|
|
+ if (StringUtils.isEmpty(value)) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ return value.startsWith(prefix) ? value.substring(1) : value;
|
|
|
+ }
|
|
|
+ /*@Override
|
|
|
public R AiReply(OpenImMsgCallBackVO openImMsgDTO, Long companyId) {
|
|
|
String content = openImMsgDTO.getContent();
|
|
|
if (content == null || content.isEmpty() || content.trim().isEmpty()) {
|
|
|
@@ -366,7 +1246,7 @@ public class AiHookServiceImpl implements AiHookService {
|
|
|
}
|
|
|
return R.error();
|
|
|
}
|
|
|
-
|
|
|
+*/
|
|
|
|
|
|
/** Ai回复 **/
|
|
|
@Async
|