基于 Laravel + Botman 轻松实现微信公众号聊天机器人
快速入门
Botman 是什么
开始之前,我们需要花一点篇幅先了解下 Botman 是什么。官方介绍如下:
Botman 是一个与框架无关的、可以在不同消息平台轻松实现聊天机器人的 PHP 库,这些消息平台包括但不限于 Slack、Telegram、Microsoft Bot Framework、Nexmo、HipChat、Facebook Messenger 以及微信等。
Botman Studio
关于 Botman 的详细使用可以参考官方文档,我们可以在已有项目中单独安装 Botman 库来使用,当然,如果你对 Laravel 熟悉的话,也可以通过官方提供的工作室项目 Botman Studio 来快速上手。本教程就是基于 Botman Studio 的,通过 Composer 来安装:
composer create-project --prefer-dist botman/studio dogchat
如上,我们新建了一个 dogchat
项目,通过这个聊天机器人,我们可以轻松获取各种狗狗的图片(后台基于 Dog API 驱动),只需要把我们的需求告诉机器人,它就会为我们自动返回。
项目初始化完成之后,就可以在浏览器中通过 http://dogchat.test
(假设你使用的是 Valet)访问项目首页:
点击 Tinker
链接(http://dogchat.test/botman/tinker
),即可进入 Web 版聊天机器人页面,我们在输入框中输入 Hi
并回车,内置机器人会返回 Hello!
回应:
这足以说明我们的系统已经可以正常运行了,下面我们就来定义更多的指令以完成更复杂的功能。
创建指令
从所有品种中返回随机图片
首先,我们创建一个 app/Services/DogService
服务类作为后端服务提供方:
<?php
namespace App\Services;
use Exception;
use GuzzleHttp\Client;
class DogService
{
// 获取随机狗狗图片的接口
const RANDOM_ENDPOINT = 'https://dog.ceo/api/breeds/image/random';
/**
* Guzzle client.
*
* @var Client
*/
protected $client;
/**
* DogService constructor
*/
public function __construct()
{
$this->client = new Client();
}
/**
* 获取并返回随机图片
*
* @return string
*/
public function random()
{
try {
// Decode the json response.
$response = json_decode(
// Make an API call an return the response body.
$this->client->get(self::RANDOM_ENDPOINT)->getBody()
);
// Return the image URL.
return $response->message;
} catch (Exception $e) {
// 如果出错,返回以下错误信息给用户
return 'An unexpected error occurred. Please try again later.';
}
}
}
接下来创建一个控制器 AllBreedsController
:
php artisan make:controller AllBreedsController
编写刚生成的 AllBreedsController
代码如下:
<?php
namespace App\Http\Controllers;
use App\Services\DogService;
use Illuminate\Http\Request;
class AllBreedsController extends Controller
{
/**
* Controller constructor
*
* @return void
*/
public function __construct()
{
$this->photos = new DogService();
}
/**
* Return a random dog image from all breeds.
*
* @return void
*/
public function random($bot)
{
// $this->photos->random() is basically the photo URL returned from the service.
// $bot->reply is what we will use to send a message back to the user.
$bot->reply($this->photos->random());
}
}
最后在注册以下路由到 routes/botman.php
:
$botman->hears('random', AllBreedsController::class . '@random');
这样,就可以在 http://dogchat.test/botman/tinker
页面测试 random
指令了:
从特定品种中返回随机图片
在上面创建的 AllBreedsController
中新增如下方法:
/**
* Return a random dog image from a given breed.
*
* @return void
*/
public function byBreed($bot, $name)
{
// Because we used a wildcard in the command definition, Botman will pass it to our method.
// Again, we let the service class handle the API call and we reply with the result we get back.
$bot->reply($this->photos->byBreed($name));
}
然后在 DogService
中新增 byBread
方法:
/**
* Fetch and return a random image from a given breed.
*
* @param string $breed
* @return string
*/
public function byBreed($breed)
{
try {
// We replace %s in our endpoint with the given breed name.
$endpoint = sprintf(self::BREED_ENDPOINT, $breed);
$response = json_decode(
$this->client->get($endpoint)->getBody()
);
return $response->message;
} catch (Exception $e) {
return "Sorry I couldn\"t get you any photos from $breed. Please try with a different breed.";
}
}
以及 BREED_ENDPOINT
常量:
// The endpoint we will hit to get a random image by a given breed name.
const BREED_ENDPOINT = 'https://dog.ceo/api/breed/%s/images/random';
最后在 routes/botman.php
中注册以下路由:
$botman->hears('b {breed}', AllBreedsController::class . '@byBreed');
在 http://dogchat.test/botman/tinker
页面中测试刚创建的指令:
通过给定品种+子品种返回随机图片
先生成一个新的控制器类 SubBreedController
:
php artisan make:controller SubBreedController
编写 SubBreedController
控制器代码如下:
<?php
namespace App\Http\Controllers;
use App\Services\DogService;
class SubBreedController extends Controller
{
/**
* Controller constructor
*
* @return void
*/
public function __construct()
{
$this->photos = new DogService();
}
/**
* Return a random dog image from all breeds.
*
* @return void
*/
public function random($bot, $breed, $subBreed)
{
$bot->reply($this->photos->bySubBreed($breed, $subBreed));
}
}
在 DogService
中新增常量:
// The endpoint we will hit to get a random image by a given breed name and its sub-breed.
const SUB_BREED_ENDPOINT = 'https://dog.ceo/api/breed/%s/%s/images/random';
以及 bySubBreed
方法:
/**
* Fetch and return a random image from a given breed and its sub-breed.
*
* @param string $breed
* @param string $subBreed
* @return string
*/
public function bySubBreed($breed, $subBreed)
{
try {
$endpoint = sprintf(self::SUB_BREED_ENDPOINT, $breed, $subBreed);
$response = json_decode(
$this->client->get($endpoint)->getBody()
);
return $response->message;
} catch (Exception $e) {
return "Sorry I couldn\"t get you any photos from $breed. Please try with a different breed.";
}
}
最后还是在 routes/botman.php
中注册以下路由:
$botman->hears('s {breed}:{subBreed}', SubBreedController::class . '@random');
在 http://dogchat.test/botman/tinker
页面中测试刚创建的指令:
返回提供操作选项的会话
一问一答有点乏味?Botman 还支持返回提供多个操作选项的会话。开始之前,先创建一个 DefaultConversation
以提供这个功能:
php artisan botman:make:conversation DefaultConversation
然后编写 DefaultConversation
代码如下:
<?php
namespace App\Http\Conversations;
use App\Services\DogService;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Incoming\Answer;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Outgoing\Question;
class DefaultConversation extends Conversation
{
/**
* 启动带操作选项会话的问题
*/
public function defaultQuestion()
{
// We first create our question and set the options and their values.
$question = Question::create('Huh - you woke me up. What do you need?')
->addButtons([
Button::create('Random dog photo')->value('random'),
Button::create('A photo by breed')->value('breed'),
Button::create('A photo by sub-breed')->value('sub-breed'),
]);
// We ask our user the question.
return $this->ask($question, function (Answer $answer) {
// Did the user click on an option or entered a text?
if ($answer->isInteractiveMessageReply()) {
// We compare the answer to our pre-defined ones and respond accordingly.
switch ($answer->getValue()) {
case 'random':
$this->say((new DogService())->random());
break;
case 'breed':
$this->askForBreedName();
break;
case 'sub-breed':
$this->askForSubBreed();
break;
}
}
});
}
/**
* Ask for the breed name and send the image.
*
* @return void
*/
public function askForBreedName()
{
$this->ask('What\'s the breed name?', function (Answer $answer) {
$name = $answer->getText();
$this->say((new DogService())->byBreed($name));
});
}
/**
* Ask for the breed name and send the image.
*
* @return void
*/
public function askForSubBreed()
{
$this->ask('What\'s the breed and sub-breed names? ex:hound:afghan', function (Answer $answer) {
$answer = explode(':', $answer->getText());
$this->say((new DogService())->bySubBreed($answer[0], $answer[1]));
});
}
/**
* Start the conversation
*
* @return void
*/
public function run()
{
// This is the boot method, it's what will be excuted first.
$this->defaultQuestion();
}
}
接下来修改默认的控制器 BotmanController
方法 startConversation
代码如下:
/**
* Loaded through routes/botman.php
* @param BotMan $bot
*/
public function startConversation(BotMan $bot)
{
$bot->startConversation(new DefaultConversation());
}
这样我们就可以在测试页面中测试了:
点击右侧会话选项就会返回相应的回复。
响应未注册指令
很多时候用户输入指令可能后端并未实现,所以我们需要创建一个指令对该类消息进行兜底处理。先创建一个用于处理未注册指令的控制器:
php artisan make:controller FallbackController
然后编写该控制器代码如下:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
class FallbackController extends Controller
{
/**
* Respond with a generic message.
*
* @param Botman $bot
* @return void
*/
public function index($bot)
{
$bot->reply('Sorry, I did not understand these commands. Try: \'Start Conversation\'');
}
}
最后在 routes/botman.php
中注册路由:
$botman->fallback(FallbackController::class . '@index');
在测试页面中测试任意未注册指令,返回如下:
在微信公众号中集成 Botman
上面测试指令都是基于 Botman 默认启用的 web
驱动,上面我们说了,Botman 还支持很多其他消息平台驱动,要查看支持的所有驱动,可以通过以下 Artisan 命令:
php artisan botman:list-drivers
返回结果如下:
对于国内用户,日常接触最频繁也是最刚需的肯定要数微信(WeChat)了,所以我们正好趁热打铁,将上述聊天机器人集成到微信公众号中,为此,需要先安装新的 wechat
驱动:
php artisan botman:install-driver wechat
安装完成后,会在 config/botman
下新增一个 wechat
配置文件,需要我们配置微信公众号的 app_id
、app_key
、以及 verification
配置项。
下面我们通过微信公众平台测试帐号,如果没有注册的话按照系统提示完成注册流程,扫描登录成功后进入管理页面可以看到如下信息:
其中 appID
对应配置项 app_id
,appsecret
对应配置项 app_key
,接口配置信息中的 Token
对应 verification
配置项,Token
根据你自己的偏好填写即可。
由于我们是本地开发,为了让微信公众号可以连接到本地我们可以通过 ngrok
工具(或者 valet share
命令)将本地项目分享到公网。 具体操作步骤如下:
cd dogchat
php artisan serve
ngrok http 8000
红色方框内就是为你分配的域名信息,将其填写到接口配置信息里的 URL
对应域名部分,点击提交按钮,提示配置成功,即可通过扫描该页面中的测试号二维码进行测试了:
以下是我的测试结果:

当然除了文本信息之外,wechat
还支持定位、音频、图片、视频等格式信息,后面有空我再分享下。
3 Comments
你好, 在微信填写token 之后提交的时候一直配置失败呢
你好,我在输入random的时候,报错了,
{ "message": "", "exception": "Illuminate\Container\EntryNotFoundException", "file": "D:\items\dogchat\vendor\botman\botman\src\Container\LaravelContainer.php", "line": 37, "trace": [ { "file": "D:\items\dogchat\vendor\botman\botman\src\BotMan.php", "line": 704, "function": "get", "class": "BotMan\BotMan\Container\LaravelContainer", "type": "->" }, { "file": "D:\items\dogchat\vendor\botman\botman\src\BotMan.php", "line": 454, "function": "getCallable", "class": "BotMan\BotMan\BotMan", "type": "->" }, { "file": "D:\items\dogchat\vendor\botman\botman\src\BotMan.php", "line": 421, "function": "callMatchingMessages", "class": "BotMan\BotMan\BotMan", "type": "->" }, { "file": "D:\items\dogchat\app\Http\Controllers\BotManController.php", "line": 19, "function": "listen", "class": "BotMan\BotMan\BotMan", "type": "->" }, { "function": "handle", "class": "App\Http\Controllers\BotManController", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Controller.php", "line": 54, "function": "call_user_func_array" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\ControllerDispatcher.php", "line": 45, "function": "callAction", "class": "Illuminate\Routing\Controller", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Route.php", "line": 219, "function": "dispatch", "class": "Illuminate\Routing\ControllerDispatcher", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Route.php", "line": 176, "function": "runController", "class": "Illuminate\Routing\Route", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Router.php", "line": 682, "function": "run", "class": "Illuminate\Routing\Route", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 30, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Router", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Middleware\SubstituteBindings.php", "line": 41, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Routing\Middleware\SubstituteBindings", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken.php", "line": 75, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Foundation\Http\Middleware\VerifyCsrfToken", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\View\Middleware\ShareErrorsFromSession.php", "line": 49, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\View\Middleware\ShareErrorsFromSession", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Session\Middleware\StartSession.php", "line": 63, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Session\Middleware\StartSession", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse.php", "line": 37, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Cookie\Middleware\EncryptCookies.php", "line": 66, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Cookie\Middleware\EncryptCookies", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 104, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Router.php", "line": 684, "function": "then", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Router.php", "line": 659, "function": "runRouteWithinStack", "class": "Illuminate\Routing\Router", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Router.php", "line": 625, "function": "runRoute", "class": "Illuminate\Routing\Router", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Router.php", "line": 614, "function": "dispatchToRoute", "class": "Illuminate\Routing\Router", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php", "line": 175, "function": "dispatch", "class": "Illuminate\Routing\Router", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 30, "function": "Illuminate\Foundation\Http\{closure}", "class": "Illuminate\Foundation\Http\Kernel", "type": "->" }, { "file": "D:\items\dogchat\vendor\fideloper\proxy\src\TrustProxies.php", "line": 57, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Fideloper\Proxy\TrustProxies", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\TransformsRequest.php", "line": 31, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Foundation\Http\Middleware\TransformsRequest", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\TransformsRequest.php", "line": 31, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Foundation\Http\Middleware\TransformsRequest", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\ValidatePostSize.php", "line": 27, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Foundation\Http\Middleware\ValidatePostSize", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode.php", "line": 62, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 163, "function": "handle", "class": "Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php", "line": 53, "function": "Illuminate\Pipeline\{closure}", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php", "line": 104, "function": "Illuminate\Routing\{closure}", "class": "Illuminate\Routing\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php", "line": 150, "function": "then", "class": "Illuminate\Pipeline\Pipeline", "type": "->" }, { "file": "D:\items\dogchat\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php", "line": 116, "function": "sendRequestThroughRouter", "class": "Illuminate\Foundation\Http\Kernel", "type": "->" }, { "file": "D:\items\dogchat\public\index.php", "line": 55, "function": "handle", "class": "Illuminate\Foundation\Http\Kernel", "type": "->" } ] } 能看下么,感谢