基于 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_idapp_key、以及 verification 配置项。

下面我们通过微信公众平台测试帐号,如果没有注册的话按照系统提示完成注册流程,扫描登录成功后进入管理页面可以看到如下信息:

其中 appID 对应配置项 app_idappsecret 对应配置项 app_key,接口配置信息中的 Token 对应 verification 配置项,Token 根据你自己的偏好填写即可。

由于我们是本地开发,为了让微信公众号可以连接到本地我们可以通过 ngrok 工具(或者 valet share 命令)将本地项目分享到公网。 具体操作步骤如下:

cd dogchat
php artisan serve
ngrok http 8000

红色方框内就是为你分配的域名信息,将其填写到接口配置信息里的 URL 对应域名部分,点击提交按钮,提示配置成功,即可通过扫描该页面中的测试号二维码进行测试了:

以下是我的测试结果:

当然除了文本信息之外,wechat 还支持定位、音频、图片、视频等格式信息,后面有空我再分享下。

学院君 has written 848 articles

终身学习者,Laravel学院院长

One thought on “基于 Laravel + Botman 轻松实现微信公众号聊天机器人

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>