diff --git a/contact-center/app/.gitignore b/contact-center/app/.gitignore index e1dd3832..b7453245 100644 --- a/contact-center/app/.gitignore +++ b/contact-center/app/.gitignore @@ -1,10 +1,6 @@ # dev profile src/main/resources/application-dev.properties -# ignore channel views within plugins -src/main/resources/templates/admin/channel/* -!src/main/resources/templates/admin/channel/im - # ignore app views within plugins src/main/resources/templates/apps/callout src/main/resources/templates/apps/callcenter diff --git a/contact-center/app/src/main/java/com/chatopera/cc/.gitignore b/contact-center/app/src/main/java/com/chatopera/cc/.gitignore index 6acddf6c..e69de29b 100644 --- a/contact-center/app/src/main/java/com/chatopera/cc/.gitignore +++ b/contact-center/app/src/main/java/com/chatopera/cc/.gitignore @@ -1,3 +0,0 @@ -plugins/* -!plugins/chatbot -!plugins/README.md \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChannelController.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChannelController.java new file mode 100644 index 00000000..e9c5ddfd --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChannelController.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.plugins.messenger; + +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.controller.Handler; +import com.chatopera.cc.model.FbMessenger; +import com.chatopera.cc.model.Organ; +import com.chatopera.cc.model.SNSAccount; +import com.chatopera.cc.persistence.repository.FbMessengerRepository; +import com.chatopera.cc.persistence.repository.OrganRepository; +import com.chatopera.cc.persistence.repository.SNSAccountRepository; +import com.chatopera.cc.proxy.OrganProxy; +import com.chatopera.cc.util.Menu; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Controller +@RequestMapping("/admin/messenger") +public class MessengerChannelController extends Handler { + private final static Logger logger = LoggerFactory.getLogger(MessengerChannelController.class); + + @Autowired + private FbMessengerRepository fbMessengerRepository; + + @Autowired + private OrganRepository organRepository; + + @Autowired + private OrganProxy organProxy; + + @Autowired + private SNSAccountRepository snsAccountRepository; + + private Map getOwnOrgan(HttpServletRequest request) { + return organProxy.findAllOrganByParentAndOrgi(super.getOrgan(request), super.getOrgi(request)); + + } + + @RequestMapping("/index") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView index(ModelMap map, HttpServletRequest request) { + Map organs = getOwnOrgan(request); + List fbMessengers = fbMessengerRepository.findByOrganIn(organs.keySet()); + Organ currentOrgan = super.getOrgan(request); + map.addAttribute("fbMessengers", fbMessengers); + map.addAttribute("organs", organs); + map.addAttribute("organ", currentOrgan); + return request(super.createView("/admin/channel/messenger/index")); + } + + @RequestMapping("/add") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView add(ModelMap map, HttpServletRequest request) { + Organ currentOrgan = super.getOrgan(request); + map.addAttribute("organ", currentOrgan); + return request(super.createView("/admin/channel/messenger/add")); + } + + @RequestMapping("/save") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView save(ModelMap map, HttpServletRequest request, @Valid FbMessenger fbMessenger) { + String msg = "save_ok"; + Organ currentOrgan = super.getOrgan(request); + FbMessenger fbMessengerOne = fbMessengerRepository.findOneByPageId(fbMessenger.getPageId()); + if (fbMessengerOne != null) { + msg = "save_no_PageId"; + } else { + fbMessenger.setId(MainUtils.getUUID()); + fbMessenger.setOrgan(currentOrgan.getId()); + + if (fbMessenger.getStatus() == null) { + fbMessenger.setStatus("disabled"); + } + fbMessenger.setCreatetime(new Date()); + fbMessenger.setUpdatetime(new Date()); + fbMessenger.setAiid(null); + fbMessengerRepository.save(fbMessenger); + + SNSAccount snsAccount = new SNSAccount(); + snsAccount.setId(MainUtils.genID()); + snsAccount.setCreatetime(new Date()); + snsAccount.setOrgi(super.getOrgi(request)); + snsAccount.setName(fbMessenger.getName()); + snsAccount.setOrgan(currentOrgan.getId()); + snsAccount.setSnsid(fbMessenger.getPageId()); + snsAccount.setSnstype(MainContext.ChannelType.MESSENGER.toString()); + snsAccountRepository.save(snsAccount); + } + return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg)); + } + + @RequestMapping("/edit") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView edit(ModelMap map, HttpServletRequest request, @Valid String id) { + FbMessenger fbMessenger = fbMessengerRepository.findOne(id); + + Organ fbOrgan = organRepository.getOne(fbMessenger.getOrgan()); + map.addAttribute("organ", fbOrgan); + map.addAttribute("fb", fbMessenger); + + return request(super.createView("/admin/channel/messenger/edit")); + } + + @RequestMapping("/update") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView update(ModelMap map, HttpServletRequest request, @Valid FbMessenger fbMessenger) { + String msg = "update_ok"; + FbMessenger oldMessenger = fbMessengerRepository.findOne(fbMessenger.getId()); + oldMessenger.setName(fbMessenger.getName()); + if (fbMessenger.getStatus() != null) { + oldMessenger.setStatus(fbMessenger.getStatus()); + } else { + oldMessenger.setStatus(Constants.MESSENGER_CHANNEL_DISABLED); + } + + oldMessenger.setToken(fbMessenger.getToken()); + oldMessenger.setVerifyToken(fbMessenger.getVerifyToken()); + oldMessenger.setUpdatetime(new Date()); + + fbMessengerRepository.save(oldMessenger); + + return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg)); + } + + @RequestMapping("/delete") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView delete(ModelMap map, HttpServletRequest request, @Valid String id) { + String msg = "delete_ok"; + FbMessenger fbMessenger = fbMessengerRepository.getOne(id); + fbMessengerRepository.delete(id); + + snsAccountRepository.findBySnsid(fbMessenger.getPageId()).ifPresent(snsAccount -> { + snsAccountRepository.delete(snsAccount); + }); + + return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg)); + } + + @RequestMapping("/setting") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView setting(ModelMap map, HttpServletRequest request, @Valid String id) { + FbMessenger fbMessenger = fbMessengerRepository.findOne(id); + Organ fbOrgan = organRepository.getOne(fbMessenger.getOrgan()); + + map.mergeAttributes(fbMessenger.parseConfigMap()); + map.addAttribute("organ", fbOrgan); + map.addAttribute("fb", fbMessenger); + + return request(super.createView("/admin/channel/messenger/setting")); + } + + @RequestMapping(value = "/setting/save", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + @Menu(type = "admin", subtype = "messenger") + public ModelAndView saveSetting(ModelMap map, HttpServletRequest request, @Valid String id, @RequestBody MultiValueMap formData) { + String msg = "update_ok"; + + FbMessenger fbMessenger = fbMessengerRepository.findOne(id); + if (fbMessenger != null) { + fbMessenger.setConfigMap(formData.toSingleValueMap()); + fbMessengerRepository.save(fbMessenger); + } + + return request(super.createView("redirect:/admin/messenger/index.html?msg=" + msg)); + } + + @RequestMapping("/setStatus") + @Menu(type = "admin", subtype = "messenger") + @ResponseBody + public String setStatus(ModelMap map, HttpServletRequest request, @Valid String id, @Valid String status) { + FbMessenger fbMessenger = fbMessengerRepository.findOne(id); + fbMessenger.setStatus(status); + fbMessengerRepository.save(fbMessenger); + return "ok"; + } + +} + diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChannelMessager.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChannelMessager.java new file mode 100644 index 00000000..a0655d20 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChannelMessager.java @@ -0,0 +1,90 @@ +package com.chatopera.cc.plugins.messenger; + +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.model.AgentUser; +import com.chatopera.cc.model.OnlineUser; +import com.chatopera.cc.peer.PeerContext; +import com.chatopera.cc.persistence.repository.OnlineUserRepository; +import com.chatopera.cc.socketio.message.ChatMessage; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Messenger 消息处理 + */ +@Component +public class MessengerChannelMessager implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(MessengerChannelMessager.class); + + @Autowired + private OnlineUserRepository onlineUserRes; + + @Autowired + private MessengerMessageProxy messengerMessageProxy; + + @Override + public void apply(final PeerContext ctx, final Functional next) { + if ((!ctx.isSent()) && ctx.getChannel() == MainContext.ChannelType.MESSENGER) { + AgentUser agentUser = ctx.getMessage().getAgentUser(); + + final OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi( + agentUser.getUserid(), agentUser.getOrgi()); + + if (onlineUser != null) { + handle(ctx, onlineUser); + } else { + logger.info( + "[apply] can not online user or its contactsid with agentUserId {}, userid {} and orgi {}", + agentUser.getId(), agentUser.getUserid(), agentUser.getOrgi()); + } + } + next.apply(); + } + + /** + * 处理消息体 + * + * @param ctx + * @param onlineUser + */ + private void handle(final PeerContext ctx, final OnlineUser onlineUser) { + if (ctx.getMessage().getChannelMessage() instanceof ChatMessage) { + final ChatMessage chatMessage = (ChatMessage) ctx.getMessage().getChannelMessage(); + logger.info( + "[apply] chat message type {}, content {}", chatMessage.getMsgtype(), + chatMessage.getMessage()); + if (StringUtils.equals(chatMessage.getMsgtype(), MainContext.MediaType.TEXT.toString())) { + Document document = Jsoup.parse(chatMessage.getMessage()); + Elements pngs = document.select("img[src]"); + if (pngs.size() > 0) { + for (Element element : pngs) { + String imgUrl = element.attr("src"); + if (StringUtils.isNotBlank(imgUrl)) { + messengerMessageProxy.sendImage(onlineUser.getAppid(), onlineUser.getUserid(), imgUrl); + } + } + } else { + messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), document.text()); + } + } else if (StringUtils.equals(chatMessage.getMsgtype(), MainContext.MediaType.IMAGE.toString())) { + messengerMessageProxy.sendImage(onlineUser.getAppid(), onlineUser.getUserid(), chatMessage.getMessage()); + } + logger.info("[apply] message is sent."); + ctx.setSent(true); + } else { + if (StringUtils.isNotBlank(ctx.getMessage().getMessage())) { + messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), ctx.getMessage().getMessage()); + } + } + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChatbot.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChatbot.java new file mode 100644 index 00000000..af5d328c --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChatbot.java @@ -0,0 +1,149 @@ +package com.chatopera.cc.plugins.messenger; + +import com.chatopera.bot.exception.ChatbotException; +import com.chatopera.cc.acd.ACDServiceRouter; +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.exception.CSKefuException; +import com.chatopera.cc.model.*; +import com.chatopera.cc.persistence.repository.*; +import com.chatopera.cc.plugins.chatbot.ChatbotProxy; +import com.chatopera.cc.proxy.OnlineUserProxy; +import com.chatopera.cc.socketio.message.ChatMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.net.MalformedURLException; +import java.util.Date; +import java.util.Optional; + +@Component +public class MessengerChatbot { + private final static Logger logger = LoggerFactory.getLogger(MessengerChatbot.class); + + @Autowired + private ChatbotProxy chatbotProxy; + + @Autowired + private AgentUserRepository agentUserRes; + + @Autowired + private ChatMessageRepository chatMessageRes; + + @Autowired + private SNSAccountRepository snsAccountRes; + + @Autowired + private AgentServiceRepository agentServiceRes; + + @Autowired + private MessengerMessageProxy messengerMessageProxy; + + @Autowired + private OnlineUserRepository onlineUserRes; + + public boolean receiveMessage(String chatbotId, String fromId, String snsID, OnlineUser onlineUser, MainContext.MediaType msgType, String msg) throws ChatbotException, MalformedURLException { + logger.info("[receiveMessage] message: chatbotId {},fromId {},toId {},msg {} ", chatbotId, fromId, snsID, msg); + // 在线客服访客咨询记录 + Date now = new Date(); + Optional optionalSNSAccount = snsAccountRes.findBySnsid(snsID); + SNSAccount snsAccount = optionalSNSAccount.get(); + CousultInvite invite = OnlineUserProxy.consult(onlineUser.getAppid(), Constants.SYSTEM_ORGI); + AgentUser agentUser = agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI).orElseGet(() -> { + AgentUser au = new AgentUser( + onlineUser.getUserid(), + MainContext.ChannelType.MESSENGER.toString(), + onlineUser.getId(), + onlineUser.getUsername(), + Constants.SYSTEM_ORGI, + onlineUser.getAppid()); + + au.setServicetime(now); + au.setCreatetime(now); + au.setUpdatetime(now); + au.setLogindate(now); + au.setSessionid(onlineUser.getSessionid()); + au.setRegion(onlineUser.getRegion()); + au.setUsername(onlineUser.getUsername()); + au.setSkill(snsAccount.getOrgan()); + au.setAppid(snsAccount.getSnsid()); + au.setOrgi(Constants.SYSTEM_ORGI); + au.setNickname(onlineUser.getUsername()); + au.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString()); + + // 聊天机器人处理的请求 + au.setOpttype(MainContext.OptType.CHATBOT.toString()); + au.setAgentno(chatbotId); // 聊天机器人ID + au.setAgentname(invite != null ? invite.getAiname() : "机器人客服"); + au.setCity(onlineUser.getCity()); + au.setProvince(onlineUser.getProvince()); + au.setCountry(onlineUser.getCountry()); + AgentService agentService = ACDServiceRouter.getAcdChatbotService().processChatbotService( + invite != null ? invite.getAiname() : "机器人客服", au, Constants.SYSTEM_ORGI); + au.setAgentserviceid(agentService.getId()); + // 标记为机器人坐席 + au.setChatbotops(true); + // 保存到MySQL + agentUserRes.save(au); + + return au; + }); + + if (agentUser.isChatbotops()) { + ChatMessage data = new ChatMessage(); + data.setMessage(msg); + data.setUserid(fromId); + data.setUsession(fromId); // 绑定唯一用户 + data.setSessionid(agentUser.getSessionid()); + data.setMessage(MainUtils.processEmoti(data.getMessage())); // 处理表情 + data.setTouser(chatbotId); + data.setUsername(agentUser.getUsername()); + data.setAiid(chatbotId); + data.setAgentserviceid(agentUser.getAgentserviceid()); + data.setChannel(agentUser.getChannel()); + data.setOrgi(Constants.SYSTEM_ORGI); + data.setContextid(agentUser.getAgentserviceid()); // 一定要设置 ContextID + data.setCalltype(MainContext.CallType.IN.toString()); + data.setMsgtype(msgType.toString()); + + chatMessageRes.save(data); + + if (MainContext.MediaType.TEXT == msgType) { + // 发送消息给Bot + chatbotProxy.publishMessage(data, Constants.CHATBOT_EVENT_TYPE_CHAT); + } + + return true; + } else { + return false; + } + } + + public void switchManualCustomerService(String fromId) { + agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI).ifPresent(agentUser -> { + if (agentUser.isChatbotops()) { + Date now = new Date(); + AgentService agentService = agentServiceRes.findOne(agentUser.getAgentserviceid()); + agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + agentService.setEndtime(now); + agentServiceRes.save(agentService); + agentUser.setAgentserviceid(null); + agentUser.setChatbotops(false); + agentUser.setAgentno(null); + agentUserRes.save(agentUser); + + snsAccountRes.findBySnsid(agentUser.getAppid()).ifPresent(snsAccount -> { + OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(fromId, Constants.SYSTEM_ORGI); + try { + messengerMessageProxy.scheduleMessengerAgentUser(agentUser, onlineUser, snsAccount, Constants.SYSTEM_ORGI); + } catch (CSKefuException e) { + e.printStackTrace(); + } + }); + } + }); + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChatbotMessager.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChatbotMessager.java new file mode 100644 index 00000000..1d601833 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerChatbotMessager.java @@ -0,0 +1,119 @@ +package com.chatopera.cc.plugins.messenger; + +import com.alibaba.fastjson.JSONObject; +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.model.FbMessenger; +import com.chatopera.cc.model.OnlineUser; +import com.chatopera.cc.persistence.repository.FbMessengerRepository; +import com.chatopera.cc.persistence.repository.OnlineUserRepository; +import com.chatopera.cc.plugins.chatbot.ChatbotConstants; +import com.chatopera.cc.plugins.chatbot.ChatbotContext; +import com.chatopera.cc.socketio.message.ChatMessage; +import com.chatopera.compose4j.Functional; +import com.chatopera.compose4j.Middleware; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public class MessengerChatbotMessager implements Middleware { + private final static Logger logger = LoggerFactory.getLogger(MessengerChatbotMessager.class); + + private final static String EVALUATION_YES_REPLY = "evaluationYesReply"; + private final static String EVALUATION_NO_REPLY = "evaluationNoReply"; + + @Autowired + private MessengerMessageProxy messengerMessageProxy; + + @Autowired + private OnlineUserRepository onlineUserRes; + + @Autowired + private FbMessengerRepository fbMessengerRepository; + + private final Map messengerConfig = new HashMap() { + { + put("transferManualService", "转人工"); + put("suggestQuestion", "您是否想问以下问题"); + put("evaluationAsk", "以上答案是否对您有帮助"); + put("evaluationYes", "是"); + put("evaluationNo", "否"); + put(EVALUATION_YES_REPLY, "感谢您的反馈,我们会做的更好!"); + put(EVALUATION_NO_REPLY, "感谢您的反馈,机器人在不断的学习!"); + } + }; + + @Override + public void apply(final ChatbotContext ctx, final Functional next) { + ChatMessage resp = ctx.getResp(); + if (MainContext.ChannelType.MESSENGER.toString().equals(resp.getChannel())) { + + final OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi( + resp.getUserid(), Constants.SYSTEM_ORGI); + + Map configMap = messengerConfig; + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(onlineUser.getAppid()); + if (fbMessenger != null && StringUtils.isNotBlank(fbMessenger.getConfig())) { + configMap = (Map) JSONObject.parse(fbMessenger.getConfig()); + } + + if (StringUtils.isNotBlank(resp.getExpmsg())) { + String jsonStr = processGenericTemplate(resp.getExpmsg(), configMap); + messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), JSONObject.parseObject(jsonStr), fbMessenger); + } else { + messengerMessageProxy.send(onlineUser.getAppid(), onlineUser.getUserid(), processTextTemplate(resp.getMessage(), configMap)); + } + } + next.apply(); + } + + /** + * 替换文本消息 + * + * @param template + * @param params + * @return + */ + public static String processTextTemplate(String template, Map params) { + if (StringUtils.equals(template, ChatbotConstants.PROVIDER_FEEDBACK_EVAL_POSITIVE_REPLY_PLACEHOLDER)) { + if (params.containsKey(EVALUATION_YES_REPLY) && StringUtils.isNotBlank(params.get(EVALUATION_YES_REPLY))) { + return params.get(EVALUATION_YES_REPLY); + } + } else if (StringUtils.equals(template, ChatbotConstants.PROVIDER_FEEDBACK_EVAL_NEGATIVE_REPLY_PLACEHOLDER)) { + if (params.containsKey(EVALUATION_NO_REPLY) && StringUtils.isNotBlank(params.get(EVALUATION_NO_REPLY))) { + return params.get(EVALUATION_NO_REPLY); + } + } else if (StringUtils.equals(template, "${leaveMeAlone}")) { + return ""; + } + return template; + } + + /** + * 替换模版消息 + * + * @param template + * @param params + * @return + */ + public static String processGenericTemplate(String template, Map params) { + Matcher m = Pattern.compile("\\$\\{\\w+\\}").matcher(template); + + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String param = m.group(); + Object value = params.get(param.substring(2, param.length() - 1)); + m.appendReplacement(sb, value == null ? "" : value.toString()); + } + m.appendTail(sb); + return sb.toString(); + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerEventSubscription.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerEventSubscription.java new file mode 100644 index 00000000..e53394a7 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerEventSubscription.java @@ -0,0 +1,65 @@ +package com.chatopera.cc.plugins.messenger; + +import com.alibaba.fastjson.JSONObject; +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.model.FbMessenger; +import com.chatopera.cc.model.FbOTN; +import com.chatopera.cc.model.FbOtnFollow; +import com.chatopera.cc.persistence.repository.FbMessengerRepository; +import com.chatopera.cc.persistence.repository.FbOTNFollowRepository; +import com.chatopera.cc.persistence.repository.FbOTNRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +@Component +public class MessengerEventSubscription { + private final static Logger logger = LoggerFactory.getLogger(MessengerEventSubscription.class); + + @Autowired + private FbMessengerRepository fbMessengerRepository; + + @Autowired + private FbOTNRepository otnRepository; + + @Autowired + private FbOTNFollowRepository otnFollowRepository; + + @Autowired + private MessengerMessageProxy messengerMessageProxy; + + @JmsListener(destination = Constants.INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN, containerFactory = "jmsListenerContainerQueue") + public void onPublish(final String jsonStr) { + JSONObject payload = JSONObject.parseObject(jsonStr); + String otnId = payload.getString("otnId"); + Date sendtime = payload.getTimestamp("sendtime"); + + FbOTN otn = otnRepository.getOne(otnId); + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(otn.getPageId()); + if (fbMessenger != null && otn != null) { + if (otn.getStatus().equals("create") && otn.getSendtime() != null && otn.getSendtime().equals(sendtime)) { + otn.setStatus("sending"); + otnRepository.save(otn); + } + + if (otn.getStatus().equals("sending")) { + List follows = otnFollowRepository.findByOtnId(otn.getId()); + for (FbOtnFollow f : follows) { + if (f.getSendtime() == null) { + messengerMessageProxy.sendOtnText(fbMessenger.getToken(), f.getOtnToken(), otn.getOtnMessage()); + f.setSendtime(new Date()); + otnFollowRepository.save(f); + } + } + + otn.setStatus("finish"); + otnRepository.save(otn); + } + } + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerMessageProxy.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerMessageProxy.java new file mode 100644 index 00000000..52274550 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerMessageProxy.java @@ -0,0 +1,540 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.plugins.messenger; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.chatopera.bot.exception.ChatbotException; +import com.chatopera.cc.acd.ACDAgentService; +import com.chatopera.cc.acd.ACDVisitorDispatcher; +import com.chatopera.cc.acd.basic.ACDComposeContext; +import com.chatopera.cc.acd.basic.ACDMessageHelper; +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.cache.Cache; +import com.chatopera.cc.exception.CSKefuException; +import com.chatopera.cc.model.*; +import com.chatopera.cc.peer.PeerSyncIM; +import com.chatopera.cc.persistence.blob.JpaBlobHelper; +import com.chatopera.cc.persistence.repository.*; +import com.chatopera.cc.socketio.message.ChatMessage; +import com.chatopera.cc.socketio.message.Message; +import com.chatopera.cc.util.HttpClientUtil; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Component +public class MessengerMessageProxy { + + private final static Logger logger = LoggerFactory.getLogger(MessengerMessageProxy.class); + + private final String FACEBOOK_MESSAGES_API = "https://graph.facebook.com/v2.6/me/messages"; + + @Autowired + private OnlineUserRepository onlineUserRes; + + @Autowired + private AgentUserRepository agentUserRes; + + @Autowired + private SNSAccountRepository snsAccountRes; + + @Autowired + private ACDMessageHelper acdMessageHelper; + + @Autowired + private ACDVisitorDispatcher acdVisitorDispatcher; + + @Autowired + private AgentServiceRepository agentServiceRes; + + @Autowired + private Cache cache; + + @Autowired + private PeerSyncIM peerSyncIM; + + @Autowired + private FbMessengerRepository fbMessengerRepository; + + @Autowired + private ChatbotRepository chatbotRes; + + @Autowired + private MessengerChatbot messengerChatbot; + + @Autowired + private FbOTNRepository otnRepository; + + @Autowired + private FbOTNFollowRepository otnFollowRepository; + + @Autowired + private ACDAgentService acdAgentService; + + @Autowired + private ChatMessageRepository chatMessageRes; + + @Autowired + private JpaBlobHelper jpaBlobHelper; + + @Autowired + private StreamingFileRepository streamingFileRes; + + @Value("${uk.im.server.host}") + private String host; + + public void acceptOTNReq(String fromId, String toId, String otnToken, String ref) { + FbOTN otn = otnRepository.findOne(ref); + if (otn != null) { + FbOtnFollow follow = new FbOtnFollow(); + follow.setId(MainUtils.getUUID()); + follow.setOtnId(ref); + follow.setUserId(fromId); + follow.setPageId(toId); + follow.setOtnToken(otnToken); + follow.setCreatetime(new Date()); + follow.setUpdatetime(new Date()); + + otnFollowRepository.save(follow); + + otnRepository.incOneSubNumById(otn.getId()); + + sendOTNContent(toId, fromId, JSONObject.parseObject(otn.getSuccessMessage())); + } + } + + public void acceptMeLink(String fromId, String toId, String ref) { + FbOTN otn = otnRepository.findOne(ref); + if (otn != null) { + if (StringUtils.isNotBlank(otn.getPreSubMessage())) { + Object obj = JSON.parse(otn.getPreSubMessage()); + if (obj instanceof JSONObject) { + JSONObject json = (JSONObject) obj; + sendOTNContent(toId, fromId, json); + } else if (obj instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) obj; + sendOTNContent(toId, fromId, jsonArray.getJSONObject(0)); + sendOTNContent(toId, fromId, jsonArray.getJSONObject(1)); + } + otnRepository.incOneMelinkNumById(otn.getId()); + } + + if (StringUtils.isNotBlank(otn.getSubMessage())) { + sendOTNReq(toId, fromId, otn.getSubMessage(), ref); + } + } + } + + public void accept(String fromId, String toId, MainContext.MediaType msgType, String msg) throws CSKefuException, ChatbotException, MalformedURLException { + Optional optionalSNSAccount = snsAccountRes.findBySnsid(toId); + + if (!optionalSNSAccount.isPresent()) { + logger.warn("[handle] SnsAccount is null."); + return; + } + + SNSAccount snsAccount = optionalSNSAccount.get(); + + Date now = new Date(); + + OnlineUser onlineUser = onlineUserRes.findOneByUseridAndOrgi(fromId, Constants.SYSTEM_ORGI); + if (onlineUser == null) { + onlineUser = new OnlineUser(); + } + + if (StringUtils.isBlank(onlineUser.getUserid())) { + Map profile = getPersonName(toId, fromId); + + // 创建新的Onlineuser + onlineUser.setId(MainUtils.getUUID()); + onlineUser.setUpdatetime(now); + onlineUser.setUsername(profile.get("name")); + onlineUser.setHeadimgurl(profile.get("profile_pic")); + onlineUser.setCreatetime(now); + onlineUser.setLogintime(now); + onlineUser.setChannel(MainContext.ChannelType.MESSENGER.toString()); + onlineUser.setAppid(snsAccount.getSnsid()); + onlineUser.setStatus(MainContext.OnlineUserStatusEnum.ONLINE.toString()); + onlineUser.setOrgi(Constants.SYSTEM_ORGI); + onlineUser.setUserid(fromId); + onlineUser.setUsertype(MainContext.OnlineUserType.MESSENGER.toString()); + onlineUserRes.save(onlineUser); + } else if (cache.existBlackEntityByUserIdAndOrgi(onlineUser.getUserid(), Constants.SYSTEM_ORGI)) { + // 检查该访客是否被拉黑 + logger.info("[handle] online user {} is in black list.", onlineUser.getId()); + return; + } + + Chatbot c = chatbotRes.findBySnsAccountIdentifierAndOrgi(toId, Constants.SYSTEM_ORGI); + if (c != null && c.isEnabled()) { + + if (!StringUtils.equals(Constants.CHATBOT_HUMAN_FIRST, c.getWorkmode())) { + Boolean sendService = messengerChatbot.receiveMessage(c.getId(), fromId, toId, onlineUser, msgType, msg); + if (sendService) { + return; + } + } + } else if (c != null && !c.isEnabled() && StringUtils.equals(Constants.CHATBOT_CHATBOT_ONLY, c.getWorkmode())) { + return; + } else { + agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI).ifPresent(p -> { + if (p.isChatbotops()) { + messengerChatbot.switchManualCustomerService(fromId); + } + }); + } + + /** + * 得到OnlineUser后获取AgentUser + * 因为AgentUser是和OnlineUser关联的 + */ + // 一个OnlineUser可以对应多个agentUser, 此处获得 + AgentUser agentUser = agentUserRes.findOneByUseridAndStatusNotAndChannelAndOrgi(fromId, MainContext.AgentUserStatusEnum.END.toString(), MainContext.ChannelType.MESSENGER.toString(), Constants.SYSTEM_ORGI) + .orElseGet(() -> new AgentUser()); + + AgentService agentService; + + if (StringUtils.isBlank(agentUser.getAgentserviceid())) { + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(toId); + if (fbMessenger != null && StringUtils.equals(fbMessenger.getStatus(), Constants.MESSENGER_CHANNEL_DISABLED)) { + return; + } + + // 没有加载到进行中的AgentUser,创建一个新的 + agentService = scheduleMessengerAgentUser( + agentUser, onlineUser, snsAccount, Constants.SYSTEM_ORGI).orElseThrow( + () -> new CSKefuException("Can not resolve AgentService Object.")); + } else { + agentService = agentServiceRes.findOne(agentUser.getAgentserviceid()); + } + + + /** + * 给客服发送消息 + */ + if (agentUser != null && agentService != null) { + ChatMessage chatMessage = new ChatMessage(); + Message outMessage = new Message(); + + chatMessage.setMessage(msg); + chatMessage.setOrgi(Constants.SYSTEM_ORGI); + chatMessage.setUsername(agentUser.getName()); + chatMessage.setCalltype(MainContext.CallType.IN.toString()); + if (StringUtils.isNotBlank(agentUser.getAgentno())) { + chatMessage.setTouser(agentUser.getUserid()); + } + chatMessage.setChannel(MainContext.ChannelType.MESSENGER.toString()); + chatMessage.setUsession(agentUser.getUserid()); + chatMessage.setId(MainUtils.getUUID()); + chatMessage.setContextid(agentUser.getContextid()); + chatMessage.setUserid(agentUser.getUserid()); + chatMessage.setUsession(agentUser.getUserid()); + chatMessage.setAgentserviceid(agentUser.getAgentserviceid()); + chatMessage.setUsername(agentUser.getUsername()); + + chatMessage.setMsgtype(msgType.toString()); + + outMessage.setMessageType(chatMessage.getMsgtype()); + outMessage.setMessage(msg); + outMessage.setAttachmentid(chatMessage.getAttachmentid()); + outMessage.setCalltype(MainContext.CallType.IN.toString()); + outMessage.setContextid(agentUser.getContextid()); + outMessage.setAgentUser(agentUser); + + outMessage.setChannelMessage(chatMessage); + outMessage.setCreatetime(Constants.DISPLAY_DATE_FORMATTER.format( + chatMessage.getCreatetime())); + + outMessage.setMessage(chatMessage.getMessage()); + + outMessage.setChannelMessage(chatMessage); + outMessage.setAgentUser(agentUser); + outMessage.setAgentService(agentService); + outMessage.setCalltype( + MainContext.CallType.IN.toString()); + outMessage.setCreatetime( + Constants.DISPLAY_DATE_FORMATTER.format( + now)); + + + // Notify customer service to refresh the page + if (StringUtils.isNotBlank(agentService.getAgentno())) { + peerSyncIM.send(MainContext.ReceiverType.AGENT, + MainContext.ChannelType.MESSENGER, + agentUser.getAppid(), + MainContext.MessageType.MESSAGE, + agentService.getAgentno(), outMessage, true); + } else { + chatMessageRes.save((chatMessage)); + } + + } else { + logger.info("[handle] agent user not found"); + } + } + + /** + * 创建新的AgentUser + * + * @param onlineUser 访客 + * @param snsAccount 社交信息账号 + * @param orgi 租户ID + * @return + */ + public Optional scheduleMessengerAgentUser( + final AgentUser agentUser, + final OnlineUser onlineUser, + final SNSAccount snsAccount, + final String orgi) throws CSKefuException { + if (agentUser == null) { + throw new CSKefuException("Invalid param for agentUser, should not be null."); + } + + String channel = MainContext.ChannelType.MESSENGER.toString(); + Date now = new Date(); + agentUser.setUsername(onlineUser.getUsername()); + agentUser.setSkill(snsAccount.getOrgan()); + agentUser.setOrgi(orgi); + agentUser.setNickname(onlineUser.getUsername()); + agentUser.setUserid(onlineUser.getUserid()); + agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + agentUser.setLogindate(now); + agentUser.setServicetime(now); + agentUser.setCreatetime(now); + agentUser.setUpdatetime(now); + agentUser.setSessiontimes(System.currentTimeMillis() - now.getTime()); + agentUser.setChannel(channel); + agentUser.setAppid(snsAccount.getSnsid()); + agentUserRes.save(agentUser); + + // 为访客安排坐席 + ACDComposeContext ctx = acdMessageHelper.getComposeContextWithAgentUser( + agentUser, false, MainContext.ChatInitiatorType.USER.toString()); + ctx.setOnlineUserHeadimgUrl(onlineUser.getHeadimgurl()); + + acdVisitorDispatcher.enqueue(ctx); + acdAgentService.notifyAgentUserProcessResult(ctx); + + + return Optional.ofNullable(ctx.getAgentService()); + } + + public Map getPersonName(String pageId, String psid) { + Map result = new HashMap<>(); + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(pageId); + if (fbMessenger != null) { + Map searchParams = new HashMap<>(); + searchParams.put("access_token", fbMessenger.getToken()); + searchParams.put("fields", "locale,first_name,last_name,profile_pic"); + try { + String res = HttpClientUtil.doGet("https://graph.facebook.com/" + psid, searchParams); + JSONObject json = JSONObject.parseObject(res); + String firstName = json.getString("first_name"); + String lastName = json.getString("last_name"); + + result.put("profile_pic", json.getString("profile_pic")); + + saveProfilePic(result, json); + + if (StringUtils.isNotBlank(firstName) && StringUtils.isNotBlank(lastName)) { + result.put("name", firstName + " " + lastName); + } + } catch (IOException e) { + logger.error("[messenger] 详情获取异常", e); + } + } + + return result; + } + + private void saveProfilePic(Map result, JSONObject json) { + try { + String profile_pic = json.getString("profile_pic"); + + if (StringUtils.isBlank(profile_pic)) { + return; + } + + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpGet httpGet = new HttpGet(profile_pic); + + HttpResponse response = httpClient.execute(httpGet); + //获取Http响应的码 200 + int startCode = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (startCode == 200 && entity != null) { + InputStream input = entity.getContent(); + long size = entity.getContentLength(); + String fileid = MainUtils.getUUID(); + StreamingFile sf = new StreamingFile(); + sf.setId(fileid); + sf.setName(fileid); + sf.setMime(entity.getContentType().getValue()); + sf.setData(jpaBlobHelper.createBlob(input, size)); + streamingFileRes.save(sf); + String fileURL = "/res/image.html?id=" + fileid; + result.put("profile_pic", fileURL); + } + } catch (IOException exception) { + + } + } + + public void sendOTNContent(String fromId, String toId, JSONObject json) { + if (json.getString("type").equals("image")) { + sendImage(fromId, toId, json.getString("url")); + } else if (json.getString("type").equals("text") && StringUtils.isNotBlank(json.getString("content"))) { + send(fromId, toId, json.getString("content")); + } + } + + public void sendOtnText(String fbToken, String otnToken, String msg) { + logger.info("send messenger to otnToken:{} msg:{}", otnToken, msg); + + JSONObject inputJson = JSONObject.parseObject(msg); + + try { + JSONObject json = new JSONObject(); + JSONObject recipient = new JSONObject(); + recipient.put("one_time_notif_token", otnToken); + JSONObject message = new JSONObject(); + if (inputJson.getString("type").equals("image")) { + JSONObject attachment = new JSONObject(); + attachment.put("type", "image"); + JSONObject payload = new JSONObject(); + payload.put("url", "https://" + host + inputJson.getString("url")); + attachment.put("payload", payload); + message.put("attachment", attachment); + json.put("recipient", recipient); + json.put("message", message); + } else { + message.put("text", inputJson.getString("content")); + message.put("metadata", "DEVELOPER_DEFINED_METADATA"); + json.put("recipient", recipient); + json.put("message", message); + } + + String result = HttpClientUtil.doPost(FACEBOOK_MESSAGES_API + "?access_token=" + fbToken, json.toJSONString()); + logger.info(result); + } catch (IOException e) { + logger.error("[messenger] 发送消息异常", e); + } + } + + public void send(String fromId, String toId, JSONObject message) { + send(fromId, toId, message, null); + } + + public void send(String fromId, String toId, JSONObject message, FbMessenger fbMessenger) { + if (fbMessenger == null) { + fbMessenger = fbMessengerRepository.findOneByPageId(fromId); + } + + if (fbMessenger != null) { + try { + JSONObject json = new JSONObject(); + JSONObject recipient = new JSONObject(); + recipient.put("id", toId); + json.put("recipient", recipient); + json.put("message", message); + + String result = HttpClientUtil.doPost(FACEBOOK_MESSAGES_API + "?access_token=" + fbMessenger.getToken(), json.toJSONString()); + logger.info(result); + } catch (IOException e) { + logger.error("[messenger] 发送消息异常", e); + } + } + } + + public void sendImage(String fromId, String toId, String imageUrl) { + logger.info("send messenger fromId:{} toId:{} image:{}", fromId, toId, imageUrl); + + JSONObject message = new JSONObject(); + JSONObject attachment = new JSONObject(); + attachment.put("type", "image"); + JSONObject payload = new JSONObject(); + if (StringUtils.indexOf(imageUrl, "http") > -1) { + payload.put("url", imageUrl); + } else { + payload.put("url", "https://" + host + imageUrl); + } + attachment.put("payload", payload); + message.put("attachment", attachment); + + send(fromId, toId, message); + } + + public void send(String fromId, String toId, String msg) { + logger.info("send messenger fromId:{} toId:{} msg:{}", fromId, toId, msg); + + JSONObject message = new JSONObject(); + message.put("text", StringEscapeUtils.unescapeHtml(msg)); + + send(fromId, toId, message); + } + + public void sendOTNReq(String fromId, String toId, String title, String ref) { + logger.info("send messenger fromId:{} toId:{} title:{}", fromId, toId, title); + + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(fromId); + if (fbMessenger != null) { + try { + JSONObject json = new JSONObject(); + JSONObject recipient = new JSONObject(); + recipient.put("id", toId); + JSONObject message = new JSONObject(); + JSONObject attachment = new JSONObject(); + attachment.put("type", "template"); + JSONObject payload = new JSONObject(); + payload.put("template_type", "one_time_notif_req"); + payload.put("title", title); + payload.put("payload", ref); + attachment.put("payload", payload); + message.put("attachment", attachment); + json.put("recipient", recipient); + json.put("message", message); + + String result = HttpClientUtil.doPost(FACEBOOK_MESSAGES_API + "?access_token=" + fbMessenger.getToken(), json.toJSONString()); + logger.info(result); + } catch (IOException e) { + logger.error("[messenger] 发送消息异常", e); + } + } + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerOTNController.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerOTNController.java new file mode 100644 index 00000000..872ae56b --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerOTNController.java @@ -0,0 +1,203 @@ +package com.chatopera.cc.plugins.messenger; + +import com.alibaba.fastjson.JSONObject; +import com.chatopera.cc.activemq.BrokerPublisher; +import com.chatopera.cc.basic.Constants; +import com.chatopera.cc.basic.MainUtils; +import com.chatopera.cc.controller.Handler; +import com.chatopera.cc.controller.api.request.RestUtils; +import com.chatopera.cc.exception.CSKefuException; +import com.chatopera.cc.model.*; +import com.chatopera.cc.persistence.repository.FbMessengerRepository; +import com.chatopera.cc.persistence.repository.FbOTNRepository; +import com.chatopera.cc.proxy.AgentProxy; +import com.chatopera.cc.proxy.OrganProxy; +import com.chatopera.cc.util.Menu; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Controller +@RequestMapping("/apps/messenger/otn") +public class MessengerOTNController extends Handler { + @Autowired + private FbMessengerRepository fbMessengerRepository; + + @Autowired + private FbOTNRepository otnRepository; + + @Autowired + private OrganProxy organProxy; + + @Autowired + private AgentProxy agentProxy; + + @Autowired + private BrokerPublisher brokerPublisher; + + private Map getOwnOrgan(HttpServletRequest request) { + return organProxy.findAllOrganByParentAndOrgi(super.getOrgan(request), super.getOrgi(request)); + } + + @RequestMapping("/index") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView index(ModelMap map, HttpServletRequest request, @Valid String queryPageId) { + Map organs = getOwnOrgan(request); + List fbMessengers = fbMessengerRepository.findByOrganIn(organs.keySet()); + List pageIds = fbMessengers.stream().map(p -> p.getPageId()).collect(Collectors.toList()); + + map.addAttribute("fbMessengers", fbMessengers); + + if (StringUtils.isNotBlank(queryPageId)) { + map.addAttribute("queryPageId", queryPageId); + pageIds = Arrays.asList(queryPageId); + } + + Page otns = otnRepository.findByPageIdIn(pageIds, new PageRequest(super.getP(request), super.getPs(request), Sort.Direction.DESC, "createtime")); + map.addAttribute("otns", otns); + return request(super.createView("/admin/channel/messenger/otn/index")); + } + + @RequestMapping("/add") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView add(ModelMap map, HttpServletRequest request) { + Map organs = getOwnOrgan(request); + List fbMessengers = fbMessengerRepository.findByOrganIn(organs.keySet()); + map.addAttribute("fbMessengers", fbMessengers); + return request(super.createView("/admin/channel/messenger/otn/add")); + } + + @RequestMapping("/save") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView save(ModelMap map, @Valid FbOTN otn, HttpServletRequest request) { + String msg = "save_ok"; + otn.setId(MainUtils.getUUID()); + otn.setCreatetime(new Date()); + otn.setUpdatetime(new Date()); + otn.setSubNum(0); + otn.setMelinkNum(0); + + otn.setStatus("create"); + otnRepository.save(otn); + + if (otn.getSendtime() != null) { + delaySend(otn); + } + + return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg)); + } + + @RequestMapping("/edit") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView edit(ModelMap map, @Valid String id, HttpServletRequest request) { + map.addAttribute("otn", otnRepository.getOne(id)); + return request(super.createView("/admin/channel/messenger/otn/edit")); + } + + @RequestMapping("/update") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView update(ModelMap map, @Valid FbOTN otn, HttpServletRequest request) { + String msg = "update_ok"; + FbOTN oldOtn = otnRepository.findOne(otn.getId()); + if (oldOtn != null) { + Date oldSendtime = oldOtn.getSendtime(); + + oldOtn.setName(otn.getName()); + oldOtn.setPreSubMessage(otn.getPreSubMessage()); + oldOtn.setSubMessage(otn.getSubMessage()); + oldOtn.setOtnMessage(otn.getOtnMessage()); + oldOtn.setSuccessMessage(otn.getSuccessMessage()); + oldOtn.setSendtime(otn.getSendtime()); + oldOtn.setUpdatetime(new Date()); + otnRepository.save((oldOtn)); + + if (otn.getSendtime() != null && !otn.getSendtime().equals(oldSendtime)) { + delaySend(otn); + } + } + + return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg)); + } + + private void delaySend(FbOTN otn) { + long delaySeconds = (otn.getSendtime().getTime() - new Date().getTime()) / 1000; + JSONObject payload = new JSONObject(); + payload.put("otnId", otn.getId()); + payload.put("sendtime", otn.getSendtime()); + brokerPublisher.send(Constants.INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN, payload.toJSONString(), false, (int) delaySeconds); + } + + @RequestMapping("/send") + @Menu(type = "admin", subtype = "faceb ook") + public ModelAndView send(ModelMap map, @Valid String id, HttpServletRequest request) { + String msg = "send_ok"; + + FbOTN otn = otnRepository.getOne(id); + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(otn.getPageId()); + if (fbMessenger != null && otn != null && otn.getStatus().equals("create")) { + otn.setStatus("sending"); + otn.setSendtime(new Date()); + otnRepository.save(otn); + JSONObject payload = new JSONObject(); + payload.put("otnId", otn.getId()); + brokerPublisher.send(Constants.INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN, payload.toJSONString()); + } + + return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg)); + } + + @RequestMapping("/image/upload") + @Menu(type = "admin", subtype = "image", access = false) + public ResponseEntity upload( + ModelMap map, + HttpServletRequest request, + @RequestParam(value = "imgFile", required = false) MultipartFile multipart + ) throws IOException { + final User logined = super.getUser(request); + JSONObject result = new JSONObject(); + HttpHeaders headers = RestUtils.header(); + if (multipart != null && multipart.getOriginalFilename().lastIndexOf(".") > 0) { + try { + StreamingFile sf = agentProxy.saveFileIntoMySQLBlob(logined, multipart); + result.put("error", 0); + result.put("url", sf.getFileUrl()); + } catch (CSKefuException e) { + result.put("error", 1); + result.put("message", "请选择文件"); + } + } else { + result.put("error", 1); + result.put("message", "请选择图片文件"); + } + return new ResponseEntity<>(result.toString(), headers, HttpStatus.OK); + } + + @RequestMapping("/delete") + @Menu(type = "admin", subtype = "messenger") + public ModelAndView delete(ModelMap map, HttpServletRequest request, @Valid String id) { + String msg = "delete_ok"; + otnRepository.delete(id); + + return request(super.createView("redirect:/apps/messenger/otn/index.html?msg=" + msg)); + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerPluginConfigurer.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerPluginConfigurer.java new file mode 100644 index 00000000..65088d06 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerPluginConfigurer.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.plugins.messenger; + +import com.chatopera.cc.basic.plugins.AbstractPluginConfigurer; +import com.chatopera.cc.basic.plugins.PluginRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + + +/** + * 定义Plugin存在 + */ +@Configuration +public class MessengerPluginConfigurer extends AbstractPluginConfigurer { + private final static Logger logger = LoggerFactory.getLogger(MessengerPluginConfigurer.class); + private final static String pluginName = "Messenger 渠道"; + private final static String pluginId = "messenger"; + + @Autowired + private PluginRegistry pluginRegistry; + + @PostConstruct + public void setup() { + pluginRegistry.addPlugin(this); + } + + @Override + public String getPluginId() { + return pluginId; + } + + /** + * 获取消息服务的Bean的名字 + * 当该方法存在时,加载到消息处理的调用栈 PeerSyncIM 中 + * + * @return + */ + @Override + public String getPluginName() { + return pluginName; + } + + @Override + public String getIOEventHandler() { + return null; + } + + @Override + public boolean isModule() { + return true; + } +} diff --git a/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerWebhookChannelController.java b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerWebhookChannelController.java new file mode 100644 index 00000000..9d1a0966 --- /dev/null +++ b/contact-center/app/src/main/java/com/chatopera/cc/plugins/messenger/MessengerWebhookChannelController.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2019 Chatopera Inc, + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.chatopera.cc.plugins.messenger; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONPath; +import com.chatopera.bot.exception.ChatbotException; +import com.chatopera.cc.basic.MainContext; +import com.chatopera.cc.controller.Handler; +import com.chatopera.cc.exception.CSKefuException; +import com.chatopera.cc.model.FbMessenger; +import com.chatopera.cc.persistence.repository.FbMessengerRepository; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.net.MalformedURLException; + +@Controller +@RequestMapping("/messenger") +public class MessengerWebhookChannelController extends Handler { + private final static Logger logger = LoggerFactory.getLogger(MessengerWebhookChannelController.class); + + @Autowired + private FbMessengerRepository fbMessengerRepository; + + @Autowired + private MessengerMessageProxy messengerMessageProxy; + + @Autowired + private MessengerChatbot messengerChatbot; + + @RequestMapping(value = "/webhook/{pageId}", method = RequestMethod.GET) + @ResponseBody + public String get(@PathVariable("pageId") String pageId, @RequestParam("hub.challenge") String challenge, @RequestParam("hub.verify_token") String verify_token) { + logger.info("[get] verify token pageid {}", pageId); + FbMessenger fbMessenger = fbMessengerRepository.findOneByPageId(pageId); + String result = "not allow!"; + + if (fbMessenger != null && fbMessenger.getVerifyToken().equals(verify_token)) { + result = challenge; + } + + return result; + } + + @RequestMapping(value = "/webhook/{pageId}", method = RequestMethod.POST) + @ResponseBody + public String post(@PathVariable("pageId") String pageId, @RequestBody JSONObject jsonParam) { + logger.info("[post] pageId {}, payload {}", pageId, jsonParam.toString()); + // NOTE: 此处 PathVariable pageId 不安全,不建议在函数内使用,详见 #1190 + // https://gitlab.chatopera.com/cskefu/cskefu.io/issues/1190 + // 该值可使用 recipientId 更为准确,recipientId 就是实际的 pageId + if (jsonParam.getString("object").equals("page")) { + JSONArray entries = jsonParam.getJSONArray("entry"); + for (int i = 0; i < entries.size(); i++) { + JSONObject entry = entries.getJSONObject(i); + JSONArray messaging = entry.getJSONArray("messaging"); + for (int j = 0; j < messaging.size(); j++) { + try { + messageHandler(messaging.getJSONObject(j)); + } catch (Exception e) { + logger.error("[messenger] 接收消息异常", e); + } + } + } + } + + return ""; + } + + private void messageHandler(JSONObject messagingEvent) throws ChatbotException, CSKefuException, MalformedURLException { + String senderId = (String) JSONPath.eval(messagingEvent, "$.sender.id"); + String recipientId = (String) JSONPath.eval(messagingEvent, "$.recipient.id"); + String referralType = (String) JSONPath.eval(messagingEvent, "$.referral.type"); + String optinType = (String) JSONPath.eval(messagingEvent, "$.optin.type"); + String postbackPayload = (String) JSONPath.eval(messagingEvent, "$.postback.payload"); + JSONObject quickRepliesPayload = (JSONObject) JSONPath.eval(messagingEvent, "$.message.quick_reply"); + + if (StringUtils.equals(referralType, "OPEN_THREAD")) { + String ref = (String) JSONPath.eval(messagingEvent, "$.referral.ref"); + messengerMessageProxy.acceptMeLink(senderId, recipientId, ref); + return; + } else if (StringUtils.equals(optinType, "one_time_notif_req")) { + String otnToken = (String) JSONPath.eval(messagingEvent, "$.optin.one_time_notif_token"); + String ref = (String) JSONPath.eval(messagingEvent, "$.optin.payload"); + messengerMessageProxy.acceptOTNReq(senderId, recipientId, otnToken, ref); + return; + } else if (StringUtils.isNotBlank(postbackPayload)) { + if (StringUtils.equals(postbackPayload, "TRANSFER_LABOR")) { + messengerChatbot.switchManualCustomerService(senderId); + } else if (StringUtils.equals(postbackPayload, "FAQ_LIST")) { + String msg = (String) JSONPath.eval(messagingEvent, "$.postback.title"); + messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, msg); + } else if (StringUtils.equals(postbackPayload, "startChatopera")) { + messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, "__kickoff"); + } else { + messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, postbackPayload); + } + return; + } else if (quickRepliesPayload != null) { + String msg = (String) JSONPath.eval(messagingEvent, "$.message.quick_reply.payload"); + messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, msg); + return; + } + + String msg = (String) JSONPath.eval(messagingEvent, "$.message.text"); + JSONArray attachments = (JSONArray) JSONPath.eval(messagingEvent, "$.message.attachments"); + + if (StringUtils.isNotBlank(msg)) { + messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.TEXT, msg); + } else { + for (Object att : attachments) { + + String type = (String) JSONPath.eval(att, "$.type"); + String url = (String) JSONPath.eval(att, "$.payload.url"); + + if (StringUtils.equals(type, "image")) { + messengerMessageProxy.accept(senderId, recipientId, MainContext.MediaType.IMAGE, url); + } + } + } + } +} + diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/add.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/add.pug new file mode 100644 index 00000000..efbf0e4b --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/add.pug @@ -0,0 +1,59 @@ +.uk-layui-form(style="margin:20px auto") + form.layui-form(action="/admin/messenger/save.html", method="post") + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | 技能组 + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", value="#{organ.name}", disabled="disabled") + i.csfont(style='position: absolute;right: 3px;top: 8px;font-size: 20px;color: #e6e6e6')  + .layui-form-mid.layui-word-aux + |    该渠道的人工服务由哪个技能组接待,默认为当前技能组,在右上角切换 + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | 名称 + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", placeholder="北美玩家页") + .layui-form-mid.layui-word-aux + |    使用字符串命名该渠道 + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | Page ID + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="pageId", required="", lay-verify="required", autocomplete="off", placeholder="e.g. 1541840459") + .layui-form-mid.layui-word-aux + |    Facebook Messenger 设置页面获得 Page Id + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | Access Token + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="password", name="token", required="", lay-verify="required", autocomplete="off", placeholder="*********") + .layui-form-mid.layui-word-aux + |    Facebook Messenger 设置页面获得 Access Token + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | Verify Token + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="verifyToken", required="", lay-verify="required", autocomplete="off", placeholder="e.g. uN26itM4Ec") + .layui-form-mid.layui-word-aux + |    自定义,并且回填到 Facebook Messenger 设置页面 + .layui-form-button + .layui-button-block + button.layui-btn(lay-submit="", lay-filter="formDemo") 提交 + button.layui-btn.layui-btn-original(type="reset") 重置 +script. + layui.use('form', function () { + var form = layui.form(); + form.render(); //更新全部 + }); + layui.use('element', function () { + var element = layui.element(); + }); diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/edit.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/edit.pug new file mode 100644 index 00000000..a7772a00 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/edit.pug @@ -0,0 +1,61 @@ +.uk-layui-form(style="margin:20px auto") + form.layui-form(action="/admin/messenger/update.html", method="post") + input(type="hidden", name="id", value=fb.id) + input(type="hidden", name="status", value=fb.status) + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | 技能组 + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", value=organ.name, disabled="disabled") + i.csfont(style='position: absolute;right: 3px;top: 8px;font-size: 20px;color: #e6e6e6')  + .layui-form-mid.layui-word-aux + |    该渠道的人工服务由哪个技能组接待,默认为当前技能组,在右上角切换 + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | 名称 + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="name", required="", lay-verify="required", autocomplete="off", value=fb.name) + .layui-form-mid.layui-word-aux + |    使用字符串命名该渠道 + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | Page ID + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", disabled ,type="text", name="pageId", required="", lay-verify="required", autocomplete="off", value=fb.pageId) + .layui-form-mid.layui-word-aux + |    Facebook Messenger 设置页面获得 Page Id + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | Access Token + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="password", name="token", required="", lay-verify="required", autocomplete="off", value=fb.token) + .layui-form-mid.layui-word-aux + |    Facebook Messenger 设置页面获得 Access Token + .layui-form-item + .layui-inline + label.layui-form-label(style="width:150px;") + | Verify Token + font(color='red') * + .layui-input-inline + input.layui-input(style="width: 200px;", type="text", name="verifyToken", required="", lay-verify="required", autocomplete="off", value=fb.verifyToken) + .layui-form-mid.layui-word-aux + |    自定义,并且回填到 Facebook Messenger 设置页面 + .layui-form-button + .layui-button-block + button.layui-btn(lay-submit="", lay-filter="formDemo") 提交 + button.layui-btn.layui-btn-original(type="reset") 重置 +script. + layui.use('form', function () { + var form = layui.form(); + form.render(); //更新全部 + }); + layui.use('element', function () { + var element = layui.element(); + }); diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/head.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/head.pug new file mode 100644 index 00000000..7dd82ca1 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/head.pug @@ -0,0 +1,17 @@ +.row(style="border-bottom: 10px solid #EFEFEF;padding:10px;") + .col-lg-8 + .ukefu-measure + .ukefu-bt + .layui-icon.ukewo-btn.ukefu-content-ind + img(src="/images/messenger.png", style="width:60px;height:60px;") + .ukefu-bt-text + .ukefu-bt-text-title(style="font-weight:400;font-size:24px;border-bottom:1px solid #dedede;") #{fb.name} + span(style="font-size:15px;color:#AAAAAA;")  接入渠道创建时间:#{pugHelper.formatDate('yyyy-MM-dd HH:mm:ss', fb.createtime)} + + .ukefu-bt-text-content(style="") Facebook Page Id: #{fb.pageId} +.ukefu-tab-title(style="margin-left: 0px;") + ul.tab-title + .ukefu-tab-title(style="margin-left: 0px;") + ul.tab-title + li(class={'layui-this': subtype == 'messenger'}) + a(href="/admin/messenger/setting.html?id=" + fb.id) 基本设置 \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/index.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/index.pug new file mode 100644 index 00000000..001aacc4 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/index.pug @@ -0,0 +1,176 @@ +extends /admin/include/layout.pug + +block content + .row + input#copyInput(type="hidden") + #me-config.col-lg-12 + h1.site-h1(style="background-color:#FFFFFF;") + | Messenger 列表 (#{size(fbMessengers)}) + span(style="float:right;") + .layui-btn-group + if organ.skill == true + button.layui-btn.layui-btn-small.green(href="/admin/messenger/add.html", data-toggle="ajax", data-width="800", data-height="400", data-title="创建 Messenger 渠道") 创建渠道 + else + button.layui-btn.layui-btn-disabled.layui-btn-small.layui-btn-warm(data-toggle="tooltip" title="当前组织机构非技能组,不支持创建 Messenger 渠道,使用右上角菜单切换") 创建渠道 + button.layui-btn.layui-btn-small.layui-btn-normal(onclick='openExternalUrlWithBlankTarget("https://docs.chatopera.com/products/cskefu/channels/messenger/index.html")') 文档中心 + .row(style="padding:5px;") + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux Messenger 是 Facebook 提供给企业、商铺、消费者之间相互连接的即时通信工具,与 Facebook 粉丝页、Instagram 和网页聊天等多渠道快速发起对话,春松客服支持与 Messenger 集成,如有疑问,阅读文档中心,获得详细使用指南。 + .col-lg-12 + table.layui-table(lay-skin="line", style="table-layout: fixed; word-break: break-all") + thead + tr + th(style="width:100px") 名称 + th 技能组 + th Page ID + th(style="width:150px") 创建时间 + th + | 状态 + i.layui-icon(style="color:gray" data-toggle="tooltip" title="处于关闭状态,只对该渠道支持 Facebook OTN 功能;在开启状态,即支持客服功能也支持 Facebook OTN 功能")  + th Webhook + th(style="width:250px;white-space:nowrap;", nowrap="nowrap") 操作 + tbody + for item in fbMessengers + tr + td(title="#{item.name}", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= item.name + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= organs[item.organ].name + td(title="#{item.pageId}", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= item.pageId + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;")= pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", item.createtime) + td(style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + .fbStatus.layui-unselect.layui-form-switch.checkStatus.lay-filter(style="margin-top: 0px;width: 50px;", data-id=item.id, class={ + 'layui-form-onswitch': item.status != 'disabled', + 'layui-form-offswitch': item.status == 'disabled' + }) + i.checkStatusI + td(style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + button.layui-btn.layui-btn-small.webhook(type="button", data-pageid=item.pageId) 复制 + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + a(href="/admin/messenger/setting.html?id=" + item.id, data-toggle="load", data-target="#me-config") + i.layui-icon  + | 设置 + a(href="/admin/messenger/edit.html?id=" + item.id, style="margin-left:10px;", data-toggle="ajax", data-width="800", data-height="400", data-title="编辑 Messenger 渠道") + i.layui-icon  + | 编辑 + a(href="https://www.facebook.com/" + item.pageId, style="margin-left:10px;", target="_blank") + i.layui-icon  + | 粉丝页 + a(href="/admin/messenger/delete.html?id=" + item.id, style="margin-left:10px;", data-toggle="tip", title="请确认是否删除?") + i.layui-icon(style="color:red;") ဆ + | 删除 + + style. + .fbStatus.layui-form-onswitch .checkStatusI { + left: 38px; + } + + .fbStatus.layui-form-onswitch:before { + content: '开启'; + color: #ffffff; + } + + .fbStatus.layui-form-offswitch:before { + content: '关闭'; + color: #aaaaaa; + padding-left: 18px; + } + + script. + // 打开链接 + function openExternalUrlWithBlankTarget(externalUrl) { + window.open(externalUrl, "_blank"); + } + + layui.use('layer', function () { + var layer = layui.layer; + var msg = '#{msg}' + if (msg == 'save_ok') + layer.msg('渠道添加成功', {icon: 1, time: 1000}) + else if (msg == 'update_ok') + layer.msg('保存成功', {icon: 1, time: 1000}) + else if (msg == 'save_no_PageId') + layer.msg('Page ID已存在', {icon: 2, time: 3000}) + }); + + $('.fbStatus').click(function () { + var isOpen = $(this).hasClass('layui-form-onswitch') + var id = $(this).data('id'); + var status = !isOpen ? 'enabled' : 'disabled'; + + var that = this; + + $.post('setStatus.html', {id, status}).success(function (msg) { + if (msg == 'ok') { + if (isOpen) { + $(that).removeClass('layui-form-onswitch'); + $(that).addClass('layui-form-offswitch'); + } else { + $(that).removeClass('layui-form-offswitch'); + $(that).addClass('layui-form-onswitch'); + } + } + }) + }) + + $('.webhook').each(function (i, n) { + var htmlOld = $(n).attr("data-pageId"); + var webhook = window.location.protocol + "//" + window.location.host + "/messenger/webhook/" + htmlOld; + $(n).on('click', function () { + Clipboard.copy(webhook); + }) + }); + window.Clipboard = (function (window, document, navigator) { + var textArea, + copy; + + // 判断是不是ios端 + function isOS() { + return navigator.userAgent.match(/ipad|iphone/i); + } + + //创建文本元素 + function createTextArea(text) { + textArea = document.createElement('textArea'); + textArea.value = text; + document.body.appendChild(textArea); + } + + //选择内容 + function selectText() { + var range, + selection; + if (isOS()) { + range = document.createRange(); + range.selectNodeContents(textArea); + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + textArea.setSelectionRange(0, 999999); + } else { + textArea.select(); + } + } + + //复制到剪贴板 + function copyToClipboard() { + try { + if (document.execCommand("Copy")) { + layer.msg('webhook已复制到剪切板', {icon: 1, time: 1000, offset: 'rt'}) + } else { + layer.msg('webhook复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'}) + } + } catch (err) { + layer.msg('webhook复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'}) + } + document.body.removeChild(textArea); + } + + copy = function (text) { + createTextArea(text); + selectText(); + copyToClipboard(); + }; + return { + copy: copy + }; + })(window, document, navigator); diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/add.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/add.pug new file mode 100644 index 00000000..3ed9d6e5 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/add.pug @@ -0,0 +1,133 @@ +.row + .col-lg-12 + .ukefu-customer-div.setting-wrapper + .box.default-box + .box-header + h1.site-h1 新建OTN +.row + .col-lg-12 + .uk-layui-form(style="height: calc(100% + 10px);") + form.layui-form(action="/apps/messenger/otn/save.html", method="post") + input#preSubMessage(type="hidden", name="preSubMessage", value=otn.preSubMessage) + .layui-colla-item + h2.layui-colla-title 基本信息 + .layui-colla-content.layui-show + .layui-form-item + .layui-inline + label.layui-form-label 名称: + .layui-input-inline + input.layui-input(type='text', name='name', required, lay-verify='required', autocomplete='off') + .layui-inline + font(color='red') *(必填项) + .layui-inline + label.layui-form-label(style='width:60px;line-height: 35px;') 渠道: + .layui-input-inline(style='width:218px;margin-right:0px;padding-top:9px;') + select(name='pageId', required, lay-verify='required') + option(value="") 请选择渠道... + if fbMessengers + for m in fbMessengers + option(value=m.pageId)= m.name + .layui-inline + font(color='red') *(必填项) + .layui-colla-item + h2.layui-colla-title 订阅邀请信息(2000字符) + .layui-colla-content.layui-show + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 访客点击链接后发送订阅邀请信息给访客 + .layui-form-item + .layui-inline(style="width: 380px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") + | 订阅邀请前消息1 + //font(color='red') *(必填项) + .layui-input-inline + #preSubMessage1.messageBox(name="preSubMessage1") + .layui-inline(style="width: 380px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") + | 订阅邀请前消息2 + //font(color='red') *(必填项) + .layui-input-inline + #preSubMessage2.messageBox(name="preSubMessage2") + .layui-form-item + .layui-inline(style="width: 320px;height: 209px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") + | 订阅邀请 + font(color='red') *(必填项,60字符) + .layui-input-inline + textarea(name="subMessage", required="required", lay-verify="maxSubMessage", style="margin: 0px; width: 310px; height: 170px;resize:none;border: 1px solid #ccc") + + .layui-colla-item + h2.layui-colla-title 订阅成功提醒(2000字符) + .layui-colla-content.layui-show + .layui-input-inline + .messageBox(name="successMessage") + + .layui-colla-item + h2.layui-colla-title 推送信息(2000字符) + .layui-colla-content.layui-show + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 推送时间到达时,此内容通过OTN推送给访客。推送时间可以不设置,后续手动推送。 + .layui-form-item + .layui-inline(style="width: 380px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") OTN内容 + .layui-input-inline + .messageBox(name="otnMessage") + .layui-inline(style="width: 320px;height: 209px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") 推送时间 + .layui-input-inline + input#begin.layui-input.ukefu-input(name='sendtime', style="width:310px") + + .layui-button-block(style="width: 700px;") + button.layui-btn(lay-submit="", lay-filter="formDemo") 提交 + button.layui-btn.layui-btn-original(type="reset", onclick="location.reload()") 取消 + +script. + layui.use('form', function () { + var form = layui.form(); + form.render(); //更新全部 + + form.verify({ + maxSubMessage: function (value) { + if (value.length > 50) { + return '长度大于60!请重新输入'; + } + } + }); + + form.on('submit(formDemo)', function () { + + }); + }); + + layui.use('laydate', function () { + var laydate = layui.laydate; + document.getElementById('begin').onclick = function () { + var date = { + min: laydate.now(), + format: 'YYYY-MM-DD hh:mm:ss', + istoday: false, + istime: true + }; + date.elem = this; + laydate(date); + } + }) + + layui.use('element', function () { + var element = layui.element(); + }); + + $(".messageBox").otnContent(); + + function preSubMessageChange() { + var preSubMessage1 = JSON.parse($('#preSubMessage1 input').val() || '{}'); + var preSubMessage2 = JSON.parse($('#preSubMessage2 input').val() || '{}'); + + $('#preSubMessage').val(JSON.stringify([preSubMessage1, preSubMessage2])) + } + + $('#preSubMessage1').change(preSubMessageChange); + $('#preSubMessage2').change(preSubMessageChange); + + diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/edit.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/edit.pug new file mode 100644 index 00000000..f38fc448 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/edit.pug @@ -0,0 +1,131 @@ +.row + .col-lg-12 + .ukefu-customer-div.setting-wrapper + .box.default-box + .box-header + h1.site-h1 编辑OTN + +.row + .col-lg-12 + .uk-layui-form(style="height: calc(100% + 10px);") + form.layui-form(action="/apps/messenger/otn/update.html", method="post") + input(type="hidden", name="pageId", value=pageId) + input(type="hidden", name="id", value=otn.id) + input#preSubMessage(type="hidden", name="preSubMessage", value=otn.preSubMessage) + .layui-colla-item + h2.layui-colla-title 基本信息 + .layui-colla-content.layui-show + .layui-form-item + .layui-inline + label.layui-form-label 名称: + .layui-input-inline + input.layui-input(type='text', name='name', required, lay-verify='required', autocomplete='off', value=otn.name) + .layui-inline + font(color='red') *(必填项) + .layui-inline + label.layui-form-label(style='width:60px;line-height: 35px;') 渠道: + .layui-input-inline(style='width:218px;margin-right:0px;padding-top:9px;') + p(style="padding: 10px 0;")= otn.fbMessenger.name + .layui-colla-item + h2.layui-colla-title 订阅邀请信息(2000字符) + .layui-colla-content.layui-show + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 访客点击链接后发送订阅邀请信息给访客 + .layui-form-item + .layui-inline(style="width: 380px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") + | 订阅邀请前消息1 + //font(color='red') *(必填项) + .layui-input-inline + #preSubMessage1.messageBox(name="preSubMessage1") + .layui-inline(style="width: 380px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") + | 订阅邀请前消息2 + //font(color='red') *(必填项) + .layui-input-inline + #preSubMessage2.messageBox(name="preSubMessage2") + .layui-form-item + .layui-inline(style="width: 320px;height: 209px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") + | 订阅邀请 + font(color='red') *(必填项,65字符) + .layui-input-inline + textarea(name="subMessage", required="required", lay-verify="maxSubMessage", style="margin: 0px; width: 310px; height: 170px;resize:none;border: 1px solid #ccc")= otn.subMessage + + .layui-colla-item + h2.layui-colla-title 订阅成功提醒(2000字符) + .layui-colla-content.layui-show + .layui-input-inline + .messageBox(name="successMessage",value=otn.successMessage) + + .layui-colla-item + h2.layui-colla-title 推送信息(2000字符) + .layui-colla-content.layui-show + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 推送时间到达时,此内容通过OTN推送给访客。推送时间可以不设置,后续手动推送。 + .layui-form-item + .layui-inline(style="width: 380px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") OTN内容 + .layui-input-inline + .messageBox(name="otnMessage",value=otn.otnMessage) + .layui-inline(style="width: 320px;height: 209px;") + label.layui-form-label(style="float: none;text-align: left;width:200px") 推送时间 + .layui-input-inline + input#begin.layui-input.ukefu-input(name='sendtime', style="width:310px", value=pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", otn.sendtime)) + + .layui-button-block(style="width: 700px;") + button.layui-btn(lay-submit="", lay-filter="formDemo") 提交 + button.layui-btn.layui-btn-original(type="reset", onclick="location.reload()") 取消 + +script. + layui.use('form', function () { + var form = layui.form(); + form.render(); //更新全部 + + form.verify({ + maxSubMessage: function (value) { + if (value.length > 50) { + return '长度大于60!请重新输入'; + } + } + }); + + form.on('submit(formDemo)', function () { + + }); + }); + layui.use('element', function () { + var element = layui.element(); + }); + + layui.use('laydate', function () { + var laydate = layui.laydate; + document.getElementById('begin').onclick = function () { + var date = { + min: laydate.now(), + format: 'YYYY-MM-DD hh:mm:ss', + istoday: false, + istime: true + }; + date.elem = this; + laydate(date); + } + }) + + var preSubMessage = JSON.parse($('#preSubMessage').val() || '[{},{}]'); + $('#preSubMessage1').attr('value', JSON.stringify(preSubMessage[0])); + $('#preSubMessage2').attr('value', JSON.stringify(preSubMessage[1])); + + $(".messageBox").otnContent(); + + function preSubMessageChange() { + var preSubMessage1 = JSON.parse($('#preSubMessage1 input').val() || '{}'); + var preSubMessage2 = JSON.parse($('#preSubMessage2 input').val() || '{}'); + + $('#preSubMessage').val(JSON.stringify([preSubMessage1, preSubMessage2])) + } + + $('#preSubMessage1').change(preSubMessageChange); + $('#preSubMessage2').change(preSubMessageChange); diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/index.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/index.pug new file mode 100644 index 00000000..c91ee168 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/otn/index.pug @@ -0,0 +1,179 @@ +extends /apps/include/layout.pug + +block append head + script(src="/js/otnContent.js") + +block content + .layui-side.layui-bg-black + .layui-side-scroll + include /apps/marketing/left.pug + #otn-edit-content.layui-body + .layui-side-scroll + .row + input#copyInput(type="hidden") + .col-lg-12 + .ukefu-customer-div.setting-wrapper + .box.default-box + .box-header + h1.site-h1 + | Messenger OTN 列表   + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 请勿使用负载字段发送密码、用户凭证、可识别用户身份的信息(即,姓名或邮箱等可单独用于联系用户或识别其身份的信息)或其他敏感信息(如健康状况、财务、支付或持卡人数据,或根据适用法律定义为敏感信息的其他类别的信息 + .row(style="padding:5px;") + h1(style="width: 100%;") + span(style="padding: 0 5px;") Messenger   + select#queryPageId(name='queryPageId') + option(value) 请选择渠道... + for m in fbMessengers + option(value=m.pageId, selected=m.pageId == queryPageId)= m.name + span(style="float:right;") + button.layui-btn.layui-btn-small.green(href="/apps/messenger/otn/add.html", data-toggle="load", data-target="#otn-edit-content") + | 创建OTN + .row(style="padding:5px;") + .col-lg-12 + table.layui-table(lay-skin="line", style="table-layout: fixed; word-break: break-all") + thead + tr + th 名称 + th Messenger + th(style="width:100px") 点击 + th(style="width:100px") 订阅 + th(style="width:80px") 创建时间 + th(style="width:80px") 发送时间 + th(style="width:60px") 状态 + th(style="width:60px") 分享链接 + th(style="width:180px;white-space:nowrap;", nowrap="nowrap") 操作 + tbody + - var state = {'create' :'新建','sending':'发送中','finish':'已发送'} + for item in otns.content + tr + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + | #{item.name} + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + | #{item.fbMessenger.name} + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + | #{item.melinkNum} + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + | #{item.subNum} + td + | #{pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", item.createtime)} + td + | #{item.sendtime ? pugHelper.formatDate("yyyy-MM-dd HH:mm:ss", item.sendtime) : ''} + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + | #{state[item.status]} + td(style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + button.layui-btn.layui-btn-small.webhook(type="button", data-id=item.id, data-page=item.pageId) 复制 + td(title="", style="text-overflow: ellipsis;white-space: nowrap;overflow: hidden;") + if(item.status == 'create') + a(href="/apps/messenger/otn/send.html?id=" + item.id, style="margin-left:10px;", data-toggle="tip", title="请确认是否发送?") + i.layui-icon  + | 发送 + a(href="/apps/messenger/otn/edit.html?id=" + item.id, data-toggle="load", data-target="#otn-edit-content") + i.layui-icon  + | 编辑 + a(href="/apps/messenger/otn/delete.html?id=" + item.id, style="margin-left:10px;", data-toggle="tip", title="请确认是否删除?") + i.layui-icon(style="color:red;") ဆ + | 删除 + .row(style='padding:5px;') + .col-lg-12#page(style='text-align:center;') + + script. + layui.use(['laypage', 'layer'], function () { + var laypage = layui.laypage, + layer = layui.layer; + laypage({ + cont: 'page', + pages: #{ otns.totalPages }, //总页数 + curr: #{ otns.number + 1 }, + groups: 5, //连续显示分页数 + jump: function (data, first) { + if (!first) { + location.href = "/apps/messenger/otn/index.html?p=" + data.curr; + } + } + }); + }); + + layui.use('layer', function () { + var layer = layui.layer; + var msg = '#{msg}' + if (msg == 'save_ok') + layer.msg('OTN创建成功', {icon: 1, time: 1000}) + else if (msg == 'save_no_PageId') + layer.msg('Page ID已存在', {icon: 2, time: 3000}) + else if (msg == 'send_ok') + layer.msg('发送成功', {icon: 1, time: 3000}) + + }); + + $(function () { + $('.webhook').each(function (i, n) { + var id = $(n).data("id"); + var pageId = $(n).data("page"); + var webhook = "https://m.me/" + pageId + "?ref=" + id; + $(n).on('click', function () { + Clipboard.copy(webhook); + }) + }); + + $('#queryPageId').change(function () { + location.search = "queryPageId=" + $(this).val(); + }) + }) + + window.Clipboard = (function (window, document, navigator) { + var textArea, + copy; + + // 判断是不是ios端 + function isOS() { + return navigator.userAgent.match(/ipad|iphone/i); + } + + //创建文本元素 + function createTextArea(text) { + textArea = document.createElement('textArea'); + textArea.value = text; + document.body.appendChild(textArea); + } + + //选择内容 + function selectText() { + var range, + selection; + if (isOS()) { + range = document.createRange(); + range.selectNodeContents(textArea); + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + textArea.setSelectionRange(0, 999999); + } else { + textArea.select(); + } + } + + //复制到剪贴板 + function copyToClipboard() { + try { + if (document.execCommand("Copy")) { + layer.msg('me link 已复制到剪切板', {icon: 1, time: 1000, offset: 'rt'}) + } else { + layer.msg('me link 复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'}) + } + } catch (err) { + layer.msg('me link 复制失败请手动复制', {icon: 2, time: 1000, offset: 'rt'}) + } + document.body.removeChild(textArea); + } + + copy = function (text) { + createTextArea(text); + selectText(); + copyToClipboard(); + }; + return { + copy: copy + }; + })(window, document, navigator); diff --git a/contact-center/app/src/main/resources/templates/admin/channel/messenger/setting.pug b/contact-center/app/src/main/resources/templates/admin/channel/messenger/setting.pug new file mode 100644 index 00000000..ada5e708 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/channel/messenger/setting.pug @@ -0,0 +1,86 @@ +block content + include head + + .layui-tab + .layui-tab-content + .layui-tab-item.layui-show + form.layui-form(method='post', action="/admin/messenger/setting/save.html") + input(type='hidden' name='id' value=fb.id) + input.layui-input(type='checkbox', name='status', lay-skin='switch', lay-text="开启|关闭") + .ukefu-customer-div.setting-wrapper + .box.default-box + .box-header + h3.box-title 机器人客服 + .box-body(style="padding-top:5px;") + .row: .col-lg-8 + .ukefu-webim-prop + .ukefu-webim-tl(style="clear:both;") 1、转人工按钮文本 + .box-item + .row + .col-lg-8 + p 默认显示信息:转人工 + p(style="color:#888888;font-size:13px;margin-top:10px;") 访客在 Messenger 对话窗口,机器人客服转人工客服提示按钮文本 + .col-lg-4 + input.layui-input(type='text', name='transferManualService', lay-verify='required', value=(transferManualService ? transferManualService : "转人工")) + + .ukefu-webim-prop + .ukefu-webim-tl(style="clear:both;") 2、推荐问题提示文本 + .box-item + .row + .col-lg-8 + p 默认显示信息:您是否想问以下问题 + p(style="color:#888888;font-size:13px;margin-top:10px;") 机器人客服提示建议问题的文本 + .col-lg-4 + input.layui-input(type='text', name='suggestQuestion', lay-verify='required', value=(suggestQuestion ? suggestQuestion : "您是否想问以下问题")) + + + .ukefu-webim-prop + .ukefu-webim-tl(style="clear:both;") 3、询问是否有帮助文本 + .box-item + .row + .col-lg-8 + p 默认显示信息:以上答案是否对您有帮助 + p(style="color:#888888;font-size:13px;margin-top:10px;") 机器人客服询问回答是否有帮助的文本 + .col-lg-4 + input.layui-input(type='text', name='evaluationAsk', lay-verify='required', value=(evaluationAsk ? evaluationAsk : "以上答案是否对您有帮助")) + .row: br + .row + .col-lg-8 + p 有帮助按钮文本 + p(style="color:#888888;font-size:13px;margin-top:10px;") 访客得到的上一条机器人回答有帮助 + .col-lg-4 + input.layui-input(type='text', name='evaluationYes', lay-verify='required', value=(evaluationYes ? evaluationYes : "是")) + .row: br + .row + .col-lg-8 + p 有帮助后回复文本 + p(style="color:#888888;font-size:13px;margin-top:10px;") 访客反馈反馈有帮助后机器人回复,设置为空则不回复 + .col-lg-4 + input.layui-input(type='text', name='evaluationYesReply', lay-verify='required', value=(evaluationYesReply ? evaluationYesReply : "感谢您的反馈,我们会做的更好!")) + + .row: br + .row + .col-lg-8 + p 无帮助按钮文本 + p(style="color:#888888;font-size:13px;margin-top:10px;") 访客得到的上一条机器人回答没帮助 + .col-lg-4 + input.layui-input(type='text', name='evaluationNo', lay-verify='required', value=(evaluationNo ? evaluationNo : "否")) + .row: br + .row + .col-lg-8 + p 无帮助后回复文本 + p(style="color:#888888;font-size:13px;margin-top:10px;") 访客反馈反馈没帮助后机器人回复,设置为空则不回复 + .col-lg-4 + input.layui-input(type='text', name='evaluationNoReply', lay-verify='required', value=(evaluationNoReply ? evaluationNoReply : "感谢您的反馈,机器人在不断的学习!")) + + .row + .col-lg-3 + .col-lg-9 + .layui-form-item + .layui-input-block + button.layui-btn(lay-submit, lay-filter='formDemo') 保存 + button.layui-btn.layui-btn-original(type='reset', onclick="location.reload()") 取消 + script. + layui.use('form', function () { + var form = layui.form; + });