钉钉开放平台集成云小蜜开发指导

适用范围

云小蜜对焦钉钉企业内部机器人(内部群机器人、内部机器人应用)

功能对照说明

功能点

子功能

云小蜜

问答

纯文本

支持

富文本

支持

卡片消息

支持,需要适配

人工客服

暂时不支持

知识详情页

暂时不支持,现在钉钉支持富文本的数据展示,不再需要知识详情页来辅助展示知识了。

点赞点踩

支持

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

image.pngimage.png

说明:

  1. 消息推送到钉钉的服务端出口IP,如果出口IP不在列表中,会被钉钉拒绝处理。

  2. 用户问机器人时,钉钉平台会把对应的消息作为参数,调用这个服务地址。

云小蜜配置

  1. 创建机器人

  2. 绑定FAQ类目/创建多轮对话

  3. 发布机器人

  4. 获取机器人ID,对应的是Chat API的InstanceId参数

72A8D9D4-FDDE-4E07-A479-4DF221D35F27.png

开发接入

  1. 和机器人问答后,钉钉平台会调用上述配置的消息接收地址,继而获取到用户的query和webhook回调地址

  2. 构建请求云小蜜ChatAPI的请求参数,并请求得到ChatAPI的出参

  1. 根据钉钉消息协议把云小蜜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;
 }
}