适用范围
云小蜜对焦钉钉企业内部机器人(内部群机器人、内部机器人应用)
功能对照说明
功能点 | 子功能 | 云小蜜 |
问答 | 纯文本 | 支持 |
富文本 | 支持 | |
卡片消息 | 支持,需要适配 | |
人工客服 | 暂时不支持 | |
知识详情页 | 暂时不支持,现在钉钉支持富文本的数据展示,不再需要知识详情页来辅助展示知识了。 | |
点赞点踩 | 支持 | |
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;
}
}
文档内容是否对您有帮助?