基于 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
http://dogchat.test
Tinkerhttp://dogchat.test/botman/tinkerHiHello!

这足以说明我们的系统已经可以正常运行了,下面我们就来定义更多的指令以完成更复杂的功能。

创建指令

从所有品种中返回随机图片

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/tinkerrandom

从特定品种中返回随机图片

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));
}
DogServicebyBread
/**
 * 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

返回提供操作选项的会话

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();
    }
}
BotmanControllerstartConversation
/**
 * 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

web
php artisan botman:list-drivers

返回结果如下:

wechat
php artisan botman:install-driver wechat
config/botmanwechatapp_idapp_keyverification

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

appIDapp_idappsecretapp_keyTokenverificationToken
ngrokvalet share
cd dogchat
php artisan serve
ngrok http 8000
URL

以下是我的测试结果:

wechat