# 机器人开发

# 聊天机器人

聊天机器人是Yach为用户提供的组织内部使用的机器人,为组织数字化转型业务服务。开发者可通过本文所描述步骤进行机器人的自主开发和上架,组织内其它成员可通过方便快捷地在群内添加聊天机器人,并使用机器人的能力。

基于聊天机器人的outgoing机制,用户@机器人之后,Yach会将消息内容POST到开发者的消息接收地址。开发者解析出消息内容、发送者身份,根据企业的业务逻辑,组装响应的消息内容返回,Yach会将响应内容发送到群里。

# 聊天机器人使用流程

# 创建机器人

(1)需要 在知音楼工作台->流程中心 搜索 "申请知音楼聊天机器人" 流程申请创建聊天机器人。

chat-admin-01.png

(2)填写基本信息,包括机器人名称、机器人Logo、机器人简介、消息预览图、详细描述,点击下一步。填写基本信息注意事项:

填写内容 说明
机器人名称 请使用意图正确、文明的词汇,避免歧义、错别字及不合规词汇
机器人头像 建议大小为200像素*200像素,请使用清晰的图片,图片核心内容居中。尽量避免尺寸过小、过大或者内容不合规图片

chat-admin-02.png

(3)配置开发信息,注意事项如下:

填写内容 说明
服务器出口IP 填写本企业服务器的公网IP
消息接收地址 填写一个公网可访问的本企业HTTPS服务地址,用于接收POST过来的消息

chat-admin-03.png

(4)填写完成后,点击创建,即可成功创建机器人,如下图所示。

chat-admin-04.png

# 开发机器人

当用户@机器人时,Yach会通过机器人开发者的HTTPS服务地址,把消息内容发送出去,报文协议如下:

# HTTP HEADER

{
  "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
  "timestamp": "1577262236757",
  "sign":"xxxxxxxxxx"
}
参数 说明
timestamp 消息发送的时间戳,单位是毫秒,13位数字
sign 签名值

开发者需对header中的 timestamp和sign进行验证,以判断是否是来自Yach的合法请求,避免其他仿冒Yach调用开发者的HTTPS服务传送数据,具体验证逻辑如下:

  1. timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。

  2. sign 与开发者自己计算的结果不一致,则认为是非法的请求。

必须当timestamp和sign同时验证通过,才能认为是来自Yach的合法请求。

sign的计算方法:

header中的timestamp + "\n" + 机器人的appSecret 当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,得到最终的签名值。

签名计算代码示例(Java)

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;

public class Test {
    public static void main(String[] args) throws Exception {
        Long timestamp = 1577262236757L;
        String appSecret = "this is a secret";
        String stringToSign = timestamp + "\n" + appSecret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
        String sign = new String(Base64.encodeBase64(signData));
        System.out.println(sign);
    }
}

签名计算代码示例(Python)

#python 3.8
import hmac
import hashlib
import base64

timestamp = '1577262236757'
app_secret = 'this is a secret'
app_secret_enc = app_secret.encode('utf-8')
string_to_sign = '{}\n{}'.format(timestamp, app_secret)
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(app_secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = base64.b64encode(hmac_code).decode('utf-8')
print(sign)
#python 2.7
import hmac
import hashlib
import base64

timestamp = '1577262236757'
app_secret = 'this is a secret'
app_secret_enc = bytes(app_secret).encode('utf-8')
string_to_sign = '{}\n{}'.format(timestamp, app_secret)
string_to_sign_enc = bytes(string_to_sign).encode('utf-8')
hmac_code = hmac.new(app_secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = base64.b64encode(hmac_code)
print(sign)

签名计算代码示例(PHP)

<?php
$data = sprintf("%s\n%s", $timestamp, $app_secret);
$sign = base64_encode(hash_hmac('sha256', $data, $app_secret, true));
var_dump($sign);

# HTTP BODY(下面参数会提交给机器人开发者)

    "msgtype": "text",
    "content": "我就是我, 是不一样的烟火",
    "appID":"1234567890",
    "msgId": "XXXX",
    "createAt": 1487561654123,
    "conversationType": "2",
    "conversationId": "XXXX",
    "conversationTitle": "Yach群标题",
    "senderId": "XXXX",
    "senderNick": "星星",
    "senderCorpId": "XXXX",
    "chatbotUserId":"XXXX",
    "chatbotUserName":"XXXX",
    "replyMsgType": "回复的消息类型(text、image、audio、video)",
    "replyMsgId": "回复的加密消息ID",
    "replyContent": "回复的内容",
    "atUsers":[
       {
         "yachId":"XXXX"
       }
    ],
    "userJson":{
        "yachId":"XXXX",
        "workCode":"XXXX",
        "name":"用户姓名",
        "deptName":"部门名称"
    },
    "extra":{
    },
    "remark":{
    }
参数 是否必填 类型 说明
msgtype string 目前支持text(文本消息)、reply(回复消息)、welcome(欢迎语)、image(图片)、audio(语音)、file(文件)、video(视频)、artificial(人工)、appraise(评价)、add_group(加入群聊)、start_new_session(开启新会话,此功能在申请聊天机器人时需要说明)
content string 消息文本(当msgtype=file时,content是加密传输的,解密逻辑参考msgId、conversationId字段)
appID string 应用ID
msgId string 加密的消息ID(使用机器人的AppKey作为密钥来解密)
createAt string 消息的时间戳,单位毫秒
conversationType string 1-单聊、2-群聊
conversationId string 加密的会话ID(使用机器人的AppKey作为密钥来解密)
conversationTitle string 会话标题(群聊时才有)
senderId string 加密的发送者ID(使用机器人的AppKey作为密钥来解密)
senderNick string 发送者昵称
senderCorpId string 发送者当前群的企业corpId(企业内部群有)
chatbotUserId string 加密的机器人ID(使用机器人的AppKey作为密钥来解密)
chatbotUserName string 机器人名称
replyMsgType string 回复的消息类型,回复的消息才有该字段
replyMsgId string 回复的加密消息ID,回复的消息才有该字段(使用机器人的AppKey作为密钥来解密)
replyContent string 回复的内容,回复的消息才有该字段
atUsers json 被@人的信息,yachId:加密的发送者YachID,示例:[{"yachId":“111111”},{"yachId":“22222222”}]
userJson json 用户信息(用户yachId、用户工号、用户名、部门名称)
extra json 扩展字段(用于回传接入方参数)
remark json 备注字段,默认空
originName json 当msgtype=file或video时,表示文件或视频的原始名称,默认空

# msgtype部分值的特别说明

  • add_group 当聊天机器人加入到群聊后,开发者的接口会收到一条 msgtype 为 add_group的消息,此时开发者可以根据参数判断当前聊天机器人被加入了哪个群聊

  • start_new_session 当聊天机器人开启新会话的能力后,如果用户点击开启新会话,开发者接口会收到一条 msgtype 为 start_new_session 的消息,同时会向客户端发送一条开启新会话的提示,开发者收到该类型消息后需要处理业务自己的多轮会话消息逻辑,如果没有多轮会话逻辑,则申请机器人时不需要开启此功能

# HTTP响应格式(JSON格式)

开发者可以根据自己的业务需要,选择回复一段消息到群中,目前支持text、markdown、action_card这3种消息类型。 如果消息处理时间较长的话可以选择采用异步调用接口发送或者采用流式输出的方式发送消息。 流式发送需要先同步回复 empty 类型消息,然后异步调用接口:聊天机器人异步推送消息进行流式消息发送。

text类型

{
     "msgtype": "text",
     "text": {
         "content": "你就是你, @150XXXXXXXX 璀璨的@XXXXXXXX 烟火"
     },
     "at": {
         "atMobiles": [
             "150XXXXXXXX"
         ],
         "isAtAll": false
     }
 }
参数 是否必填 类型 说明
msgtype string text
content string 消息文本【如果需要支持@的话,可以参考上面示例】
atMobiles list 被@人的手机号列表,只有text、markdown类型的消息支持@
isAtAll boolean @所有人是true,否则为false

markdown类型

{
     "msgtype": "markdown",
     "markdown": {
         "title":"北京天气",
         "text": "#### 北京天气 \n> 18度,东南风1级,空气良98,相对温度78%\n> ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n> ###### 10点20分发布 [天气](https://www.zhiyinlou.com) \n"
     },
     "at": {
         "atMobiles": [
             "150XXXXXXXX"
         ],
         "isAtAll": false
     }
}
参数 是否必填 类型 说明
msgtype string markdown
title string 首屏会话透出的展示内容
text string markdown格式的消息内容
# 当输入完关键词,想立刻跳转到新页面(如表单或评价页面,pc_slide=true表示PC端侧边栏打开,回复格式如下:
{
    "msgtype": "custom",
    "custom": {
		"type": "1", // 类型(1:跳转链接)
		"body": {
			"url": "yach://yach.zhiyinlou.com/session/webview?url=encode编码后的链接"
		}
	}
}
# 当不想回复消息到群里时,回复格式如下:
{
    "msgtype": "empty"
}

# 关于HTTP BODY里面加密参数如何解密的说明(用于使用InComing模式的接入方,使用机器人的AppKey作为密钥,使用AES-128模式来解密)

解密代码示例(PHP)

<?php
    /**
     * 加密方法
     * @param string $plainText 原始数据
     * @param string $key 密钥(是开放平台分配给接入方的AppKey)
     * @param string $cipher 加密模式(默认:AES128位,ECB模式加密数据)
     * @param string $iv A non-NULL Initialization Vector
     * @return string
     */
    public static function encrypt($plainText, $key, $cipher = 'AES-128-ECB', $iv = '')
    {
        if (in_array($cipher, openssl_get_cipher_methods())) {
            return openssl_encrypt($plainText, $cipher, $key, 0, $iv);
        }
        throw new RuntimeException("The only supported ciphers are openssl_cipher list.");
    }

    /**
         * 解密方法
         * @param string $payload 加密后的密文
         * @param string $key 密钥(是开放平台分配给接入方的appKey)
         * @param string $cipher 加密模式(默认:AES128位,ECB模式加密数据)
         * @param string $iv A non-NULL Initialization Vector
         * @return string
         */
    public static function decrypt($payload, $key, $cipher = 'AES-128-ECB', $iv = '')
    {
        if (in_array($cipher, openssl_get_cipher_methods())) {
            return openssl_decrypt($payload, $cipher, $key, 0, $iv);
        }
        throw new RuntimeException("The only supported ciphers are openssl_cipher list.");
    }

解密代码示例(Python)

import base64
from Crypto.Cipher import AES

# import conf


class AESCipher:
    def __init__(self, key):
        self.key = key
        self.key = key + (16-len(key)) * chr(0)  # AES-128-ECB
        self.BLOCK_SIZE = 16  # Bytes
        self.pad = lambda s: s + (self.BLOCK_SIZE - len(s) % self.BLOCK_SIZE) * chr(self.BLOCK_SIZE - len(s) % self.BLOCK_SIZE)
        self.unpad = lambda s: s[:-ord(s[len(s) - 1:])]

    def encrypt(self, raw):
        raw = self.pad(raw)
        cipher = AES.new(self.key, AES.MODE_ECB)
        return base64.b64encode(cipher.encrypt(raw)).decode('utf-8')

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        cipher = AES.new(self.key, AES.MODE_ECB)
        return self.unpad(cipher.decrypt(enc)).decode('utf-8')

    def yach_solution(self, byte_s):
        return base64.b64encode(byte_s).decode('utf-8')


if __name__ == '__main__':
    key = 'testappSecret'
    aes = AESCipher(key=key)

    raw_s = "test-encrypt-string"
    en_text = aes.encrypt(raw_s)
    print(en_text)
    de_text = aes.decrypt(en_text)
    print(de_text)

上面加解密的例子参考:

plainText = 'test-encrypt-string'

key = 'testappSecret'

加密后的字符串:xuISUSOQ2wQafzVeDjZnLAY0lWzuQrgI797nffqftlg=

# 发布机器人

# 发布

新创建的机器人,共两种状态,分别是:

状态 说明
已发布 点击上线即发布
已下线 企业成员在该企业内部群的机器人列表中,选不到这个机器人

企业成员使用路径:进入要使用机器人的群-【群设置】-【智能群助手】,在聊天机器人列表中即可找到。

# 自定义机器人

# 获取自定义机器人webhook

步骤一,在机器人管理页面选择“自定义”机器人,输入机器人名字,同时可以为机器人设置机器人头像。

步骤二,完成机器人设置后,复制出机器人的Webhook地址、密钥,可用于向这个群发送消息,格式如下: https://yach-oapi.zhiyinlou.com/robot/send?access_token=XXXX&timestamp=XXXX&sign=XXXX

# 注意:请保管好此Webhook 地址,不要公布在外部网站上,泄露后有安全风险。

# 安全设置

安全设置目前只有2种方式:

(1)方式一:加签

第一步,把timestamp+"\n"+密钥当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,最后再把签名参数再进行urlEncode,得到最终的签名(需要使用UTF-8字符集)。

参数 说明
timestamp 当前时间戳,单位是毫秒,13位数字,与请求调用时间误差不能超过1小时
secret 密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串

签名计算代码示例(Java)

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import java.net.URLEncoder;

public class Test {
    public static void main(String[] args) throws Exception {
        Long timestamp = System.currentTimeMillis();
        String secret = "this is secret";

        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
        String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");
        System.out.println(sign);
    }
}

签名计算代码示例(Python)

#python 3.7
import time
import hmac
import hashlib
import base64
import urllib.request

timestamp = int(round(time.time() * 1000))
secret = 'this is secret'
secret_enc = secret.encode('utf-8')
string_to_sign = '{}\n{}'.format(timestamp, secret)
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.request.quote(base64.b64encode(hmac_code))
print(timestamp)
print(sign)
#python 2.7
import time
import hmac
import hashlib
import base64
import urllib

timestamp = long(round(time.time() * 1000))
secret = 'this is secret'
secret_enc = bytes(secret).encode('utf-8')
string_to_sign = '{}\n{}'.format(timestamp, secret)
string_to_sign_enc = bytes(string_to_sign).encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.quote_plus(base64.b64encode(hmac_code))
print(timestamp)
print(sign)

签名计算代码示例(PHP)

<?php
$secret = 'this is secret';
$timestamp = (float)sprintf("%.0f", microtime(true) * 1000);
$data = sprintf("%s\n%s", $timestamp, $secret);
$sign = urlencode(base64_encode(hash_hmac('sha256', $data, $secret, true)));

var_dump($timestamp, $sign);

签名计算代码示例(Shell)

#!/bin/bash

export LANG="en_US.UTF-8"

# 设置 secret 值(请在此处输入 secret 值,也就是SEC开头的那个密钥)
secret=""
# 设置 Webhook (请在此处输入知音楼机器人的 Webhook 地址)
yach_robot_path=""

# URL Encode 函数
function urlencode() {
	local LANG=C
	local length="${#1}"
	i=0
	while :
	do
	[ $length -gt $i ]&&{
		local c="${1:$i:1}"
		case $c in
		[a-zA-Z0-9.~_-]) printf "$c" ;;
		*) printf '%%%02X' "'$c" ;;
		esac
		}||break
	let i++
	done
}

# 执行函数
function run() {
	# 获取时间戳
	current=`date "+%Y-%m-%d %H:%M:%S"`  
    timeStamp=`date -d "$current" +%s`   
    #将current转换为时间戳,精确到毫秒  
    cur_timestamp=$((timeStamp*1000+`date "+%N"`/1000000)) 
    echo $cur_timestamp
	# 获得签名
	sign=`echo -n -e "$cur_timestamp\n$secret" | openssl dgst -sha256 -hmac $secret -binary | base64`
	echo "加密后签名:"$sign
	# 对签名进行 urlencode
	sign_urlencode=`urlencode $sign`
	echo "urlencode 后签名:"$sign_urlencode
	request_url=$yach_robot_path"&timestamp="$cur_timestamp"&sign="$sign_urlencode
	echo "最终请求的 URL:"$request_url

	curl -X POST \
	  $request_url  \
	  -H 'content-type: application/json' \
	  -d '{"msgtype": "text",
	        "text": {
	             "content": "你就是你, 璀璨的烟火"
	        }
		  }'
}

run

签名计算代码示例(NodeJs)

  import crypto from 'crypto'
  generateSign(timestamp: number) {
    let str = `${timestamp}\n${secret}`
    let hash = crypto
      .createHmac('SHA256', secret)
      .update(str, 'utf8')
      .digest('base64')

    hash = encodeURIComponent(hash)
    return hash
  }

调用方式:
 async sendData(data: any) {
    try {
      let timestamp = +new Date()
      let sign = this.generateSign(timestamp)
      let url = `${this.baseUrl}?access_token=${token}&timestamp=${timestamp}&sign=${sign}`
      let result = await axois.post(url, data)
      return result.data
    } catch (error) {}
  }

第二步,把timestamp和第一步得到的签名值拼接到URL中。

参数 说明
timestamp 第一步使用到的时间戳
sign 第一步得到的签名值
https://yach-oapi.zhiyinlou.com/robot/send?access_token=XXXX&timestamp=XXXX&sign=XXXX

(2)方式二:自定义关键词

最多可以设置10个关键词,消息中至少包含其中1个关键词才可以发送成功。

例如:添加了一个自定义关键词:监控报警

则这个机器人所发送的消息,必须包含 监控报警 这个词,才能发送成功。

# 注意:校验不通过的消息将会发送失败,错误如下:

access_token参数不合法

{
    "code":401,
    "msg":"access_token参数不合法"
}

timestamp 无效

{
  "code":10002,
  "msg":"请求过期,请重新发起"
}

签名不匹配、不包含关键词、不包含 ip 白名单

{
  "code":180034,
  "msg":"机器人身份验证失败,请检查机器人配置"
}

# 使用自定义机器人

(1)获取到Webhook地址后,用户可以向这个地址发起HTTP POST 请求,即可实现给该Yach群发送消息。注意,发起POST请求时,必须将字符集编码设置成UTF-8。

(2)当前自定义机器人支持文本 (text)、图片 (image)、markdown(markdown)消息类型,大家可以根据自己的使用场景选择合适的消息类型,达到最好的展示样式。

(3)当前机器人尚不支持应答机制 (该机制指的是群里成员在聊天@机器人的时候,Yach回调指定的服务地址,即Outgoing机器人)。

# 消息发送频率限制:

每个机器人每分钟最多发送60条。如果超过60条,会限流1分钟。消息发送太频繁会严重影响群成员的使用体验,大量发消息的场景 (譬如系统监控报警) 可以将这些信息进行整合,通过markdown消息以摘要的形式发送到群里。

# 测试自定义机器人

通过下面方法,可以快速验证自定义机器人是否可以正常工作:

使用命令行工具curl。 为避免出错,请将以下命令直接复制到命令行,再将xxxxxxxx替换为真实access_token;若测试出错,请检查复制的命令是否和测试命令一致,多特殊字符会报错

curl 'https://yach-oapi.zhiyinlou.com/robot/send?access_token=xxxxxxxx&timestamp=XXX&sign=XXX' \
   -H 'Content-Type: application/json' \
   -d '{"msgtype": "text",
        "text": {
             "content": "你就是你, 璀璨的烟火"
        }
     }'

PHP程序测试:

function request_by_curl($remote_server, $post_string) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $remote_server);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array ('Content-Type: application/json;charset=utf-8'));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // 线下环境不用开启curl证书验证, 未调通情况可尝试添加该代码
    // curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    // curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
    $data = curl_exec($ch);
    curl_close($ch);
    return $data;
}

$webhook = "https://yach-oapi.zhiyinlou.com/robot/send?access_token=xxxxxx&timestamp=XXX&sign=XXX";
$message="我就是我, 是不一样的烟火";
$data = array ('msgtype' => 'text','text' => array ('content' => $message));
$data_string = json_encode($data);

$result = request_by_curl($webhook, $data_string);
echo $result;

# 消息类型及数据格式

请参见:消息类型与数据格式

# 联系方式

如果接入遇到问题,可以进群进行咨询,群二维码如下:

上次更新: 7/18/2024, 5:07:19 PM
foo