前言

此文章为PHP实现基于Mirai的QQ机器人的一部分。

申请API

该聊天机器人需要使用聊天API,通过将机器人接收到的消息转发给该接口,接口响应消息,来实现聊天机器人的效果。

免费的聊天API有:

图灵机器人

腾讯智能闲聊

青云客智能聊天机器人API

其中图灵机器人需要实名认证,并且免费额度100/天,听说会更加智能(没感觉)

腾讯AI开放平台的智能闲聊API接口调用额度无限制,响应速度快。

青云客似乎也是无限制。但是感觉大厂会稳点,所以选用的腾讯AI开放平台的智能闲聊API接口服务。

QQ登录腾讯智能闲聊,点击应用管理->创建应用,应用类型选机器人,其他的随意,创建完成后保存得到的APPID和APPKEY,在后面会使用。

2021-08-14更新:腾讯的API说没就没,不愧是你。直接使用青云客的API就好了,200次/10分钟一般也够用。

实例

在Web服务器监听目录下新建文件Bot.class.php(以下代码需要PHP7以上),内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<?php

namespace Mirai;

use Exception;

/**
* Mirai QQ机器人
* @param string $_url MiraiHTTP接口地址
* @param string $_authKey Mirai认证Key
* @param string $_sessionKey Mirai会话Key
*/
class Bot
{
private $_url;
private $_authKey;
private $_sessionKey;

/**
* 构造函数
* @param string $url MiraiHTTP接口地址
* @param string $authKey Mirai认证Key
*/
function __construct(string $url, string $authKey)
{
$this->_url = $url;
$this->_authKey = array('authKey' => $authKey);
}


/**
* cURL获取数据
* @param string $url 发送请求的链接
* @param int $ifPost 是否为post请求(1||0)
* @param mixed $postFields post的数据
* @param string $cookie 发送请求携带的cookie
* @param mixed $cookieFile cookie文件
* @param int $ifHeader 是否获取响应头信息(1||0)
* @throws Exception 请求失败
* @return mixed 响应结果
*/
public function httpRequest(string $url, int $ifPost = 0, $postFields = '', string $cookie = '', $cookieFile = '', int $ifHeader = 0)
{
// 模拟http请求header头
$header = array(
"Connection: Keep-Alive",
"Accept: text/html, application/xhtml+xml, */*",
"Pragma: no-cache",
"Accept-Language: zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3",
"User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)"
);

// 初始化一个cURL会话
$ch = curl_init();

// 设置cURL传输选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, $ifHeader);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
$ifPost && curl_setopt($ch, CURLOPT_POST, $ifPost);
$ifPost && curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$cookie && curl_setopt($ch, CURLOPT_COOKIE, $cookie); // 发送cookie变量
$cookieFile && curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile); // 发送cookie文件
$cookieFile && curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile); // 写入cookie到文件
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 允许执行的最长秒数
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// 执行cURL会话
$result = curl_exec($ch);

// 失败则抛出异常
if ($result === false) {
throw new Exception('Sending request to ' . $url . ' failed!');
}
// 关闭 cURL 会话
curl_close($ch);

// 释放$ch
unset($ch);

return $result;
}


/**
* 进行认证
* @throws Exception 认证失败
* @return bool
*/
public function auth(): bool
{
$url = $this->_url . '/auth';
$postData = json_encode($this->_authKey);
$response = json_decode($this->httpRequest($url, 1, $postData));
if ($response !== false && $response->code === 0) {
$this->_sessionKey = $response->session;
return true;
} else {
throw new Exception('Mirai authentication failed!');
}
}

/**
* 校验Session
* @param int $qq Session将要绑定的Bot的qq号
* @throws Exception 校验Session失败
* @return bool
*/
public function verify(int $qq): bool
{
$url = $this->_url . '/verify';
$postData = json_encode(
array(
'sessionKey' => $this->_sessionKey,
'qq' => $qq
)
);
$response = json_decode($this->httpRequest($url, 1, $postData));
if ($response !== false && $response->code === 0) {
return true;
} else {
throw new Exception('Validation session failed!' . 'The qq is ' . $qq . '.');
}
}

/**
* 释放Session
* @param int $qq 与该Session绑定Bot的QQ号码
* @throws Exception 释放Session失败
* @return bool
*/
public function release(int $qq): bool
{
$url = $this->_url . '/release';
$postData = json_encode(
array(
'sessionKey' => $this->_sessionKey,
'qq' => $qq
)
);
$response = json_decode($this->httpRequest($url, 1, $postData));
if ($response !== false && $response->code === 0) {
return true;
} else {
throw new Exception('Failed to release session! The qq is ' . $qq . '!');
}
}

/**
* 发送好友消息
* @param int $qq 发送消息目标好友的QQ号
* @param array $messageChain 消息链,消息对象构成的数组
* @param int $quote 回复消息的messageId
* @throws Exception 发送好友消息失败
* @return int messageId 可引用进行回复
*/
public function sendFriendMessage(int $qq, array $messageChain, $quote = null): int
{
$url = $this->_url . '/sendFriendMessage';
$postData = json_encode(
array(
'sessionKey' => $this->_sessionKey,
'target' => $qq,
'quote' => $quote,
'messageChain' => $messageChain
)
);
$response = json_decode($this->httpRequest($url, 1, $postData));
if ($response !== false && $response->code === 0) {
return $response->messageId;
} else {
throw new Exception('Failed to send friend message to qq:' . $qq . '!');
}
}
}

该类的方法实现了mirai-api-http所提供功能的一部分,包括认证身份、校验Session、释放Session和发送好友消息,重要参数和功能已在注释中说明。

然后在同目录下新建文件Chatter.class.php,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php

namespace Mirai;

use Exception;

/**
* 聊天机器人
* @param string $_url 接口地址
* @param string $_appId 应用标识(AppId)
* @param string $_appKey 应用Key(AppKey)
* @param int $_session 会话标识(应用内唯一)
*/
class Chatter
{
private $_url;
private $_appId;
private $_appKey;
private $_session;

/**
* 构造函数
* @param string $appId 应用标识(AppId)
* @param string $appKey 应用Key(AppKey)
* @param int $session 会话标识(应用内唯一)
*/
function __construct(string $appId, string $appKey, int $session = 23333)
{
$this->_url = 'https://api.ai.qq.com/fcgi-bin/nlp/nlp_textchat';
$this->_appId = $appId;
$this->_appKey = $appKey;
$this->_session = $session;
}

/**
* 根据接口请求参数和应用密钥计算请求签名
* @param array $params 接口请求数组
* @return string 签名结果
*/
public function getReqSign(array $params): string
{
// 字典升序排序
ksort($params);

// 拼按URL键值对
$str = '';
foreach ($params as $key => $value) {
if ($value !== '') {
$str .= $key . '=' . urlencode($value) . '&';
}
}

// 拼接app_key
$str .= 'app_key=' . $this->_appKey;

// MD5运算+转换大写,得到请求签名
$sign = strtoupper(md5($str));
return $sign;
}

/**
* 执行POST请求,并取回响应结果
* @param array $params 完整接口请求数组
* @return mixed 返回false表示失败,否则表示API成功返回的HTTP BODY部分
*/
public function doHttpPost(array $params)
{
$curl = curl_init();

$response = false;
do {
// 设置HTTP URL (API地址)
curl_setopt($curl, CURLOPT_URL, $this->_url);

// 设置HTTP HEADER (表单POST)
$head = array(
'Content-Type: application/x-www-form-urlencoded'
);
curl_setopt($curl, CURLOPT_HTTPHEADER, $head);

// 设置HTTP BODY (URL键值对)
$body = http_build_query($params);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);

// 调用API,获取响应结果
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_NOBODY, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($curl);
if ($response === false) {
$response = false;
break;
}

$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code != 200) {
$response = false;
break;
}
} while (0);

curl_close($curl);
return $response;
}


/**
* 获取聊天信息
* @param string $chatMessage 发送给机器人接口的消息
* @return object 机器人返回的结果
*/
public function chat(string $chatMessage): object
{
$postData = array(
'app_id' => $this->_appId,
'session' => $this->_session,
'question' => $chatMessage,
'time_stamp' => strval(time()),
'nonce_str' => strval(rand()),
'sign' => ''
);

$postData['sign'] = $this->getReqSign($postData);
$response = json_decode($this->doHttpPost($postData));
if ($response->ret === 0) {
return $response->data;
} else {
throw new Exception('Failed to get chat message!');
}
}
}

该类的方法实现了腾讯智能闲聊API的接口鉴权和消息请求功能,重要参数和功能已在注释中说明。

之后在同目录下新建文件index.php,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace Mirai;

use Exception;

// 包含Bot和Chatter类
include_once("./Bot.class.php");
include_once("./Chatter.class.php");

// 本地调试模拟请求(与获取请求Body不能同时存在)
// $friendMessage = '{"type":"FriendMessage","messageChain":[{"type":"Source","id":16959,"time":1616033525},{"type":"Plain","text":"你好"}],"sender":{"id":消息发送者QQ,"nickname":"消息发送者昵称","remark":""}}';
// $message = json_decode($friendMessage);

// 获取请求的Body(服务器部署时使用,与"获取本地调试模拟请求"不能同时存在)
$message = json_decode(file_get_contents('php://input'));

// 判断是否为好友文本消息
if ($message->type === 'FriendMessage' && $message->messageChain[1]->type === 'Plain') {
// 实例化一个chatter
$chatter = new Chatter('应用标识(AppId)', '应用Key(AppKey)');
// 获得消息响应
try {
$responseMessage = $chatter->chat($message->messageChain[1]->text);
} catch (Exception $e) {
// 本地调试输出错误信息
// echo $e->getMessage() . ' At file:' . $e->getFile() . ' on line:' . $e->getLine() . '.';
// 发生异常时记录日志
error_log($e->getMessage() . ' At file:' . $e->getFile() . ' on line:' . $e->getLine() . '.');
}

// 实例化一个bot
$bot = new Bot('MiraiHTTPAPI地址', 'Mirai认证Key');

// 机器人QQ号
$botQQ = xxxxxxxxxx;
// 消息链
$messageChain = array(
array(
'type' => 'Plain',
'text' => $responseMessage->answer
)
);
// 发送消息的目标QQ
$targetQQ = $message->sender->id;

try {
// 进行认证
$bot->auth();
// 校验Session
$bot->verify($botQQ);
// 发送该消息
$bot->sendFriendMessage($targetQQ, $messageChain);
// 释放Session
$bot->release($botQQ);
} catch (Exception $e) {
// 本地调试输出错误信息
// echo $e->getMessage().' At file:'.$e->getFile().' on line:'.$e->getLine().'.';
// 发生异常时记录日志
error_log($e->getMessage() . ' At file:' . $e->getFile() . ' on line:' . $e->getLine() . '.');
}
}

以上代码获取了mirai-api-http上报的消息,并对消息类型进行了判断,通过后将消息转发到聊天机器人API,之后将获取的响应结果发送给目标QQ好友,就实现了与机器人(智能)聊天的效果。

部署到服务器

将该机器人部署到服务器,首先新建一个动态站点,将以上文件放入服务器端的Web服务器监听目录下,并将mirai-api-http配置文件中的”上报URL”改为文件所在站点的url。

例如使用nginx创建动态站点api.alsaces.cn,并将以上文件放入”站点目录/qqbot/v1/“下,将mirai-api-http配置文件中的上报URL改为http://api.alsaces.cn/qqbot/v1/,重启mirai。

当机器人收到消息时便会上报至该url,PHP处理接收的消息,合法则向mirai-api-http返回处理结果。

至此,聊天机器人部署完毕。