適用范圍
云小蜜對焦釘釘企業內部機器人(內部群機器人、內部機器人應用)
功能對照說明
功能點 | 子功能 | 云小蜜 |
問答 | 純文本 | 支持 |
富文本 | 支持 | |
卡片消息 | 支持,需要適配 | |
人工客服 | 暫時不支持 | |
知識詳情頁 | 暫時不支持,現在釘釘支持富文本的數據展示,不再需要知識詳情頁來輔助展示知識了。 | |
點贊點踩 | 支持 | |
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;
}
}
文檔內容是否對您有幫助?