适用范围
云小蜜对焦钉钉企业内部机器人(内部群机器人、内部机器人应用)
功能对照说明
| 功能点 | 子功能 | 云小蜜 | 
| 问答 | 纯文本 | 支持 | 
| 富文本 | 支持 | |
| 卡片消息 | 支持,需要适配 | |
| 人工客服 | 暂时不支持 | |
| 知识详情页 | 暂时不支持,现在钉钉支持富文本的数据展示,不再需要知识详情页来辅助展示知识了。 | |
| 点赞点踩 | 支持 | |
| FAQ知识库 | 支持 | |
| 多轮对话 | 支持 | |
| 消息推送 | 暂时不支持 | |
| 渠道 | 网页渠道 | 支持 | 
| 微信渠道 | 暂时不支持 | |
| 数据看板 | 支持,效果和功能更完善 | 
前期准备
1. 准备一个域名
*必需:域名是用来接收机器人消息的,没有域名则无法接收机器人消息,也就无法进行后续处理。
温馨提醒:
域名购买网址:https://wanwang.aliyun.com/
域名要求:http/https可用,注:遵循钉钉开放平台要求即可
钉钉开放平台说明:https://open.dingtalk.com/document/orgapp/enterprise-created-chatbot
2. 在钉钉开放平台创建机器人
*必需:https://open-dev.dingtalk.com/fe/app#/corp/robot


说明:
- 消息推送到钉钉的服务端出口IP,如果出口IP不在列表中,会被钉钉拒绝处理。 
- 用户问机器人时,钉钉平台会把对应的消息作为参数,调用这个服务地址。 
云小蜜配置
- 创建机器人 
- 绑定FAQ类目/创建多轮对话 
- 发布机器人 
- 获取机器人ID,对应的是Chat API的InstanceId参数 

开发接入
- 和机器人问答后,钉钉平台会调用上述配置的消息接收地址,继而获取到用户的query和webhook回调地址 
- 构建请求云小蜜ChatAPI的请求参数,并请求得到ChatAPI的出参 
- 根据钉钉消息协议把云小蜜Chat的响应转换参数,通过webhook调用发送到钉钉平台,钉钉平台会通过机器人回复给用户。 
代码示例
机器人回调参数
{
	"atUsers": [{
		"dingtalkId": "$:LWCP_v1:$oUB47jFNK0bPR4e2lMpbUHsBiGCf6T"
	}],
	"chatbotCorpId": "dinge6f87ffe2e6d911f5bf40eda33b7ba0",
	"chatbotUserId": "$:LWCP_v1:$oUB47jFNK0bPR4e2lMpbUHsBidCf6T",
	"conversationId": "cid+ECvHAqZrIbIabqczkImg==",
	"conversationTitle": "云钉小蜜机器人测试",
	"conversationType": 2,
	"createAt": "169226429002",
	"isAdmin": true,
	"isInAtList": true,
	"msgId": "msgE7malneyJVy6xyDIC+l6zQ==",
	"msgtype": "text",
	"senderCorpId": "dinge6f87ffe2e6d911f5bf40eda33b7ba0",
	"senderId": "$:LWCP_v1:$+a/f9rESEqLnp3cAfE8G0Q==",
	"senderNick": "克霖 | KE",
	"senderStaffId": "04076552577297403",
	"sessionWebhook": "https://oapi.dingtalk.com/robot/sendBySession?session=5d200a7961856d31239e49f083438c",
	"sessionWebhookExpiredTime": 1692269670800,
	"text": {
		"content": "杭州公积金怎么查询?"
	}
}调用云小蜜Chat API
public static Client createClient() throws Exception {
 Config config = new Config()
 // 必填,您的 AccessKey ID
 .setAccessKeyId(ALIYUN_ACCESS_KEY_ID)
 // 必填,您的 AccessKey Secret
 .setAccessKeySecret(ALIYUN_ACCESS_KEY_SECRET);
 // Endpoint 请参考 https://api.aliyun.com/product/Chatbot
 config.endpoint = "chatbot.cn-shanghai.aliyuncs.com";
 return new Client(config);
}
public ChatResponseBody doChat(String query, String sessionId) throws Exception {
 Client client = createClient();
 // "cid+ECvHAqZrIbFIabqczkImg=="
 sessionId = sessionId.replace("+", StringUtils.EMPTY).replaceAll("==", StringUtils.EMPTY);
 ChatRequest chatRequest = new ChatRequest();
 chatRequest.setUtterance(query);
 chatRequest.setInstanceId(CLOUD_BOT_INSTANCE_ID);
 chatRequest.setSessionId(sessionId);
 RuntimeOptions runtime = new RuntimeOptions();
 ChatResponse chatResponse = client.chatWithOptions(chatRequest, runtime);
 ChatResponseBody body = chatResponse.getBody();
 log.info("doChat response:{}", JSON.toJSONString(body));
 return body;
}
调用云小蜜点赞点踩API
@Data
@Accessors(chain = true)
public static class FeedbackContext {
 private String messageId;
 private String feedback;
}
public void doFeedback(FeedbackContext feedbackContext) throws Exception {
 Client client = createClient();
 FeedbackRequest feedbackRequest = new FeedbackRequest();
 feedbackRequest.setMessageId(feedbackContext.messageId);
 feedbackRequest.setFeedback(feedbackContext.feedback);
 FeedbackResponse response = client.feedback(feedbackRequest);
 log.info("doFeedback response:{}", JSON.toJSONString(response.getBody()));
}协议转换
ChatResponseBody chatResponseBody = doChat(query, request.getConversationId());
String answer = "无答案";
ChatResponseBodyMessages message = chatResponseBody.getMessages().get(0);
if ("Text".equals(message.answerType)) {
 ChatResponseBodyMessagesText text = message.getText();
 answer = text.getContent();
 // 富文本
 if ("RICH_TEXT".equals(text.getContentType())) {
 answer = htmlTansToMarkdown(answer);
 msgType = "markdown";
 }
}
if ("Knowledge".equals(message.answerType)) {
 ChatResponseBodyMessagesKnowledge knowledge = message.getKnowledge();
 answer = htmlTansToMarkdown(knowledge.getContent());
 title = knowledge.getTitle();
 if ("RICH_TEXT".equals(knowledge.getContentType())) {
 msgType = "markdown";
 title = knowledge.getTitle();
 }
 // 关联知识处理
 List<ChatResponseBodyMessagesKnowledgeRelatedKnowledges> relatedKnowledges
 = knowledge.getRelatedKnowledges();
 if (CollectionUtils.isNotEmpty(relatedKnowledges)) {
 msgType = "markdown";
 answer = appendRelevanceAnswer(knowledge.getRelatedKnowledges(), answer);
 }
}
if ("Recommend".equals(message.answerType)) {
 List<ChatResponseBodyMessagesRecommends> recommends = message.getRecommends();
 answer = getRecommendText(message.getTitle(), recommends);
 msgType = "markdown";
 title = message.getTitle();
}
// 有帮助,无帮助
if (enableFeedback) {
 answer += String.format("\n\n--- \n\n该答复是否有帮助? [%s](%s) [%s](%s)",
 "有帮助", getContentLinkSendMsgUrl(GOOD_FEEDBACK_TEXT,
 new FeedbackContext().setFeedback("good").setMessageId(chatResponseBody.getMessageId())),
 "无帮助", getContentLinkSendMsgUrl(BAD_FEEDBACK_TEXT,
 new FeedbackContext().setFeedback("bad").setMessageId(chatResponseBody.getMessageId())));
 msgType = "markdown";
}
sendMessageWebhook(title, answer, request.getSessionWebhook(), msgType);回调消息到钉钉
public void sendMessageWebhook(String title, String answer, String webhook, String msgType) throws Exception {
 log.info("sendMessageWebhook answer:{}, webhook:{}, msgType:{}", answer, webhook, msgType);
 DingTalkClient client = new DefaultDingTalkClient(webhook);
 OapiRobotSendRequest request = new OapiRobotSendRequest();
 request.setMsgtype(msgType);
 OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
 text.setContent(answer);
 if ("markdown".equals(msgType)) {
 Markdown markdown = new Markdown();
 markdown.setText(answer);
 markdown.setTitle(title);
 request.setMarkdown(markdown);
 } else {
 request.setText(text);
 }
 OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
 request.setAt(at);
 OapiRobotSendResponse response = client.execute(request);
 log.info("sendMessageWebhook response:{}", response.getBody());
}完整代码
依赖项
<dependency>
 <groupId>com.aliyun</groupId>
 <artifactId>chatbot20220408</artifactId>
 <version>1.0.9</version>
</dependency>
<dependency>
 <groupId>io.github.furstenheim</groupId>
 <artifactId>copy_down</artifactId>
 <version>1.1</version>
</dependency>
<dependency>
 <groupId>com.aliyun</groupId>
 <artifactId>alibaba-dingtalk-service-sdk</artifactId>
 <version>2.0.0</version>
</dependency>
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>fastjson</artifactId>
 <version>1.2.68.noneautotype</version>
</dependency>
<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <version>1.16.22</version>
</dependency>问答
import com.aliyun.chatbot20220408.Client;
import com.aliyun.chatbot20220408.models.ChatRequest;
import com.aliyun.chatbot20220408.models.ChatResponse;
import com.aliyun.chatbot20220408.models.ChatResponseBody;
import com.aliyun.chatbot20220408.models.ChatResponseBody.ChatResponseBodyMessages;
import com.aliyun.chatbot20220408.models.ChatResponseBody.ChatResponseBodyMessagesKnowledge;
import com.aliyun.chatbot20220408.models.ChatResponseBody.ChatResponseBodyMessagesKnowledgeRelatedKnowledges;
import com.aliyun.chatbot20220408.models.ChatResponseBody.ChatResponseBodyMessagesRecommends;
import com.aliyun.chatbot20220408.models.ChatResponseBody.ChatResponseBodyMessagesText;
import com.aliyun.chatbot20220408.models.FeedbackRequest;
import com.aliyun.chatbot20220408.models.FeedbackResponse;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.request.OapiRobotSendRequest.Markdown;
import com.dingtalk.api.response.OapiRobotSendResponse;
import io.github.furstenheim.CopyDown;
import io.github.furstenheim.Options;
import io.github.furstenheim.OptionsBuilder;
@Controller
@Slf4j
public class ChatController {
 private static final boolean enableFeedback = true;
 private static final String ALIYUN_ACCESS_KEY_ID = "";
 private static final String ALIYUN_ACCESS_KEY_SECRET = "";
 private static final String CLOUD_BOT_INSTANCE_ID = "chatbot-cn-xxxxx";
 private static final String BAD_FEEDBACK_TEXT = "评价:无帮助";
 private static final String GOOD_FEEDBACK_TEXT = "评价:有帮助";
 @PostMapping("/dingme/chat")
 public @ResponseBody
 String chat(@RequestBody DingtalkChatRequest request) {
 try {
 log.info("chat request params:{}", JSON.toJSONString(request));
 String query = null;
 String msgType = "text";
 String title;
 switch (request.getMsgtype()) {
 // 纯文本
 case "text":
 query = JSON.parseObject(JSON.toJSONString(request.getText()),
 DingtalkChatRequest.RequestText.class).getContent();
 break;
 default:
 sendMessageWebhook("", "这个问题我还不会哦!", request.getSessionWebhook(), "text");
 }
 query = StringUtils.trimToNull(query);
 if (StringUtils.isBlank(query)) {
 return "query empty";
 }
 if (feedback(request, query)) {
 return "feedback";
 }
 title = query;
 ChatResponseBody chatResponseBody = doChat(query, request.getConversationId());
 String answer = "无答案";
 ChatResponseBodyMessages message = chatResponseBody.getMessages().get(0);
 if ("Text".equals(message.answerType)) {
 ChatResponseBodyMessagesText text = message.getText();
 answer = text.getContent();
 // 富文本
 if ("RICH_TEXT".equals(text.getContentType())) {
 answer = htmlTansToMarkdown(answer);
 msgType = "markdown";
 }
 }
 if ("Knowledge".equals(message.answerType)) {
 ChatResponseBodyMessagesKnowledge knowledge = message.getKnowledge();
 answer = htmlTansToMarkdown(knowledge.getContent());
 title = knowledge.getTitle();
 if ("RICH_TEXT".equals(knowledge.getContentType())) {
 msgType = "markdown";
 title = knowledge.getTitle();
 }
 // 关联知识处理
 List<ChatResponseBodyMessagesKnowledgeRelatedKnowledges> relatedKnowledges
 = knowledge.getRelatedKnowledges();
 if (CollectionUtils.isNotEmpty(relatedKnowledges)) {
 msgType = "markdown";
 answer = appendRelevanceAnswer(knowledge.getRelatedKnowledges(), answer);
 }
 }
 if ("Recommend".equals(message.answerType)) {
 List<ChatResponseBodyMessagesRecommends> recommends = message.getRecommends();
 answer = getRecommendText(message.getTitle(), recommends);
 msgType = "markdown";
 title = message.getTitle();
 }
 // 有帮助,无帮助
 if (enableFeedback) {
 answer += String.format("\n\n--- \n\n该答复是否有帮助? [%s](%s) [%s](%s)",
 "有帮助", getContentLinkSendMsgUrl(GOOD_FEEDBACK_TEXT,
 new FeedbackContext().setFeedback("good").setMessageId(chatResponseBody.getMessageId())),
 "无帮助", getContentLinkSendMsgUrl(BAD_FEEDBACK_TEXT,
 new FeedbackContext().setFeedback("bad").setMessageId(chatResponseBody.getMessageId())));
 msgType = "markdown";
 }
 sendMessageWebhook(title, answer, request.getSessionWebhook(), msgType);
 } catch (Exception e) {
 log.error("unknown error, request:{}", JSON.toJSONString(request), e);
 }
 return "empty";
 }
 private boolean feedback(DingtalkChatRequest request, String query) throws Exception {
 if (enableFeedback && request.getContext() != null) {
 if (BAD_FEEDBACK_TEXT.equals(query) || GOOD_FEEDBACK_TEXT.equals(query)) {
 String ctx;
 if (request.getContext() instanceof String) {
 ctx = (String)request.getContext();
 } else {
 ctx = JSON.toJSONString(request.getContext());
 }
 FeedbackContext feedbackContext = JSON.parseObject(ctx, FeedbackContext.class);
 doFeedback(feedbackContext);
 sendMessageWebhook(null, "感谢您的评价!", request.getSessionWebhook(), "text");
 return true;
 }
 }
 return false;
 }
 private static final String RELATED_QUESTION_SEPARATE_LINE = " \n\n---";
 private static final String RELATED_QUESTION_PROMPT = " \n是否还想了解";
 private static final String RELATED_QUESTION_FORMAT = " \n[%s](dtmd://dingtalkclient/sendMessage?content=%s)";
 public static String htmlTansToMarkdown(String htmlStr) {
 OptionsBuilder optionsBuilder = OptionsBuilder.anOptions();
 Options options = optionsBuilder
 // more options
 .build();
 CopyDown converter = new CopyDown(options);
 String convert = converter.convert(htmlStr);
 // 兼容从钉钉上导入的知识,没有带https前缀的问题
 return convert.replaceAll("\\(//knowledgecloud.oss-cn-hangzhou.aliyuncs.com",
 "\\(https://knowledgecloud.oss-cn-hangzhou.aliyuncs.com");
 }
 //组装关联问题
 private String appendRelevanceAnswer(List<ChatResponseBodyMessagesKnowledgeRelatedKnowledges> answers,
 String cardText) {
 if (CollectionUtils.isEmpty(answers)) {
 return cardText;
 }
 StringBuilder recommendString = new StringBuilder(cardText + " \n\n");
 recommendString.append(RELATED_QUESTION_SEPARATE_LINE);
 recommendString.append(RELATED_QUESTION_PROMPT);
 for (ChatResponseBodyMessagesKnowledgeRelatedKnowledges childKnowledge : answers) {
 recommendString.append(String.format(RELATED_QUESTION_FORMAT, childKnowledge.getTitle(),
 encodeContentBlank(childKnowledge.getTitle())));
 }
 return recommendString.toString();
 }
 public static String getRecommendText(String title, List<ChatResponseBodyMessagesRecommends> recommends) {
 StringBuilder builder = new StringBuilder(" \n #### " + title + " \n");
 for (ChatResponseBodyMessagesRecommends recommend : recommends) {
 // 推荐的title
 String label = recommend.getTitle().replaceAll("[\r\n]", "");
 String encodeRemTitle = encodeContentBlank(recommend.getTitle());
 builder.append("> [").append(label).append("]");
 builder.append("(dtmd://dingtalkclient/sendMessage?content=");
 builder.append(encodeRemTitle);
 builder.append(") \n\n");
 }
 return builder.toString();
 }
 private static String encodeContentBlank(String content) {
 try {
 String remContent = content.replaceAll("/[\\r\\n]/g", "");
 // 解决空格问题
 return URLEncoder.encode(remContent, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
 } catch (UnsupportedEncodingException e) {
 return content;
 }
 }
 public static final String DING_TALK_CLIENT_SEND_MSG_URL = "dtmd://dingtalkclient/sendMessage?content=";
 public static final String JUMP_CONTEXT_REPLACE = "&context=";
 public static String getContentLinkSendMsgUrl(String content, Object context) {
 StringBuilder askUrl = new StringBuilder();
 String encodeRemContent = encodeContentBlank(content);
 askUrl.append(DING_TALK_CLIENT_SEND_MSG_URL);
 askUrl.append(encodeRemContent);
 if (context != null) {
 String encodeContext = encodeUtf8(JSONObject.toJSONString(context));
 askUrl.append(JUMP_CONTEXT_REPLACE);
 askUrl.append(encodeContext);
 }
 return askUrl.toString();
 }
 private static String encodeUtf8(String content) {
 try {
 return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
 } catch (UnsupportedEncodingException e) {
 return content;
 }
 }
 public static Client createClient() throws Exception {
 Config config = new Config()
 // 必填,您的 AccessKey ID
 .setAccessKeyId(ALIYUN_ACCESS_KEY_ID)
 // 必填,您的 AccessKey Secret
 .setAccessKeySecret(ALIYUN_ACCESS_KEY_SECRET);
 // Endpoint 请参考 https://api.aliyun.com/product/Chatbot
 config.endpoint = "chatbot.cn-shanghai.aliyuncs.com";
 return new Client(config);
 }
 public void sendMessageWebhook(String title, String answer, String webhook, String msgType) throws Exception {
 log.info("sendMessageWebhook answer:{}, webhook:{}, msgType:{}", answer, webhook, msgType);
 DingTalkClient client = new DefaultDingTalkClient(webhook);
 OapiRobotSendRequest request = new OapiRobotSendRequest();
 request.setMsgtype(msgType);
 OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
 text.setContent(answer);
 if ("markdown".equals(msgType)) {
 Markdown markdown = new Markdown();
 markdown.setText(answer);
 markdown.setTitle(title);
 request.setMarkdown(markdown);
 } else {
 request.setText(text);
 }
 OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
 request.setAt(at);
 OapiRobotSendResponse response = client.execute(request);
 log.info("sendMessageWebhook response:{}", response.getBody());
 }
 public ChatResponseBody doChat(String query, String sessionId) throws Exception {
 Client client = createClient();
 // "cid+ECvHAqZrIbFIabqczkImg=="
 sessionId = sessionId.replace("+", StringUtils.EMPTY).replaceAll("==", StringUtils.EMPTY);
 ChatRequest chatRequest = new ChatRequest();
 chatRequest.setUtterance(query);
 chatRequest.setInstanceId(CLOUD_BOT_INSTANCE_ID);
 chatRequest.setSessionId(sessionId);
 RuntimeOptions runtime = new RuntimeOptions();
 ChatResponse chatResponse = client.chatWithOptions(chatRequest, runtime);
 ChatResponseBody body = chatResponse.getBody();
 log.info("doChat response:{}", JSON.toJSONString(body));
 return body;
 }
 @Data
 @Accessors(chain = true)
 public static class FeedbackContext {
 private String messageId;
 private String feedback;
 }
 public void doFeedback(FeedbackContext feedbackContext) throws Exception {
 Client client = createClient();
 FeedbackRequest feedbackRequest = new FeedbackRequest();
 feedbackRequest.setMessageId(feedbackContext.messageId);
 feedbackRequest.setFeedback(feedbackContext.feedback);
 FeedbackResponse response = client.feedback(feedbackRequest);
 log.info("doFeedback response:{}", JSON.toJSONString(response.getBody()));
 }
}
参数对象
import lombok.Data;
@Data
public class DingtalkChatRequest {
 private String msgtype;
 private Object content;
 private Object text;
 private String msgId;
 private String createAt;
 private Integer conversationType;
 private String conversationId;
 private String conversationTitle;
 private String senderId;
 private String senderNick;
 private String senderRole;
 private String senderCorpId;
 private String senderStaffId;
 private Boolean isAdmin;
 private String chatbotCorpId;
 private String chatbotUserId;
 private String source;
 private String replyMsgId;
 private Integer isCustom;
 private Object context;
 private Object customerContext;
 private String originalMsgId;
 private Object atUsers;
 private String atUserDingtalkIds;
 private String atUserStaffIds;
 private String sessionWebhook;
 private Long sessionWebhookExpiredTime;
 private Boolean isInAtList;
 private String senderContactStaffId;
 @Data
 public static class RequestText{
 private String content;
 }
}该文章对您有帮助吗?