宝塔管理服务器比较方便,管理网站和数据库,安装各种插件,比较灵活。如果能用宝塔安装云SRS,那可以在很多云和虚拟机都可能安装了。

说干就干,先去下载资料,宝塔插件的官方手册和官方论坛,还有官方Demo,可以直接下载下来后在你的宝塔中安装。

我跟着操作了一遍,发现还是有些地方没有说清楚,而官方Demo比较复杂,写得比较隐晦,就重新写了个教程。

Note: 目前插件开发,需要企业认证,认证通过后,才能开发第三方应用插件。

Develop Mode

记得一定要打开开发者模式,否则修改代码不生效:

bt宝塔软件面板 mysql 宝塔app插件怎么用_宝塔

Note: 开启开发者模式后,比如修改srs_cloud_main.py或者index.html后,会立刻生效,不用清理缓存或重启服务。

开启开发者模式后,我们就可以开始开发插件了。

HelloWorld

我们先做一个最简单的插件,只实现安装和卸载,请参考bt-plugin-example-install,下面是详细的解读。

宝塔插件的安装如下,假设插件为srs_cloud,请替换成你的插件名称:

  • 安装时,会调用install.sh脚本,参数是install,需要定义如何安装。
  • ZIP文件会解压到/www/server/panel/plugin/srs_cloud插件目录,调用脚本时文件已经存在了,可以使用插件自己的这些文件。
  • 会获取info.json的信息,关于插件的描述信息。
  • 卸载时,会调用install.sh脚本,参数是uninstall,需要定义如何卸载。

我们写一个简单的插件,目录为srs_cloud。插件描述信息info.json文件,内容如下:

{
  "title": "SRS音视频服务器",
  "name": "srs_cloud",
  "ps": "直播和WebRTC音视频能力,支持RTMP、WebRTC、HLS、HTTP- FLV和SRT等常用协议",
  "versions": "4.4",
  "checks": "/www/server/panel/plugin/srs_cloud",
  "author": "Winlin",
  "home": "https://github.com/ossrs/srs"
}

然后是install.sh脚本,安装只打印一条消息并等待3秒,卸载时直接删除插件目录:

#!/bin/bash

install_path=/www/server/panel/plugin/srs_cloud

Install() {
  echo 'Installing'; sleep 2;
  echo 'Install OK'; sleep 2;
}

Uninstall() {
 rm -rf $install_path
}

if [ "${1}" == 'install' ];then
 Install
elif [ "${1}" == 'uninstall' ];then
 Uninstall
else
 echo 'Error!'; exit 1;
fi

将目录srs_cloud打包成srs_cloud.zip,可以在第三方应用 > 导入插件,上传压缩包安装。安装过程会显示消息:

bt宝塔软件面板 mysql 宝塔app插件怎么用_linux_02

Note: 安装后,可以直接scp本地文件到服务器的插件目录,这样不用每次上传压缩包,直接刷新页面就可以看到效果,开发调试的周期很短。

安装后,就可以看到这个插件,可以卸载,如下图所示:

bt宝塔软件面板 mysql 宝塔app插件怎么用_宝塔_03

安装后,如果点设置发现会有错误提示,接下来我们开发插件的设置和配置面板。

Settings

接下来我们实现插件的设置面板,只显示一个简单的信息,请参考bt-plugin-example-settings,下面是详细的解读。

用户点击插件的设置按钮,或者点插件名称,会打开插件设置面板。定义如下:

  • 需要index.html页面,默认的模板页面。

我们先实现一个简单的UI,新增文件index.html我们先写一个非常简单的插件(更多样式可以参考demo插件),只显示一个导航和内容:

<div class="bt-form">
    <div class="bt-w-main">
        <div class="bt-w-menu"><p class="bgw">概览</p></div>
        <div class="bt-w-con pd15"><div class="plugin_body"></div></div>
    </div>
</div>
<script type="text/javascript">
    $('.plugin_body').html("<h1>Hello World!</h1>");
</script>

这样用户点击就会打开一个页面:

bt宝塔软件面板 mysql 宝塔app插件怎么用_linux_04

这是最简单的插件页面了,只是静态页面而已。我们接下来,添加后端API。

Backend Server

接下来我们实现插件的后端服务,前端调用后端服务并展示结果,请参考bt-plugin-example-backend,下面是详细的解读。

我们一般需要页面从后端服务请求数据,比如查看我们的服务器是否正常等。定义如下:

  • 需要srs_cloud_main.py,每个函数就是API。

新建文件srs_cloud_main.py,新增一个简单的helloworld函数,它的代码如下:

#!/usr/bin/python
# coding: utf-8

import sys, os, json

os.chdir("/www/server/panel")
sys.path.append("class/")
import public

class srs_cloud_main:
    def __init__(self):
        pass

    def helloworld(self, args):
        return public.returnMsg(True, "Hello API World!")

关于后端API,请注意:

  • 必须返回固定的格式,我们使用public.returnMsg函数返回,一个是status,一个是msg两个字段,是不能随便返回的,会报错。
  • 我们这里没有传参数,args为空,注意不能传action、s、name、a等参数,会报错。

现在我们有了后端API,在前端index.html中就可以调用这个API了,同样也必须是固定的格式,我们修改index.html代码如下:

<div class="bt-form">
    <div class="bt-w-main">
        <div class="bt-w-menu"><p class="bgw">概览</p></div>
        <div class="bt-w-con pd15"><div class="plugin_body"></div></div>
    </div>
</div>
<script type="text/javascript">
function plugin_request(plugin_name, function_name, args, callback, timeout) {
    $.ajax({
        type:'POST',
        url: '/plugin?action=a&s=' + function_name + '&name=' + plugin_name,
        data: args,
        timeout:timeout || 3600 * 1000,
        success: function(res) {
            if (!callback) return layer.msg(res.msg, { icon: res.status ? 1 : 2 });
            return callback(res);
        }
    });
}

plugin_request('srs_cloud', 'helloworld', { p: 100}, function (res) {
    $('.plugin_body').html(`<pre>${JSON.stringify(res, null, 2)}</pre>`);
});
</script>

打开插件的设置页面,就会调用这个API,从后端AP获取数据:

bt宝塔软件面板 mysql 宝塔app插件怎么用_bt宝塔软件面板 mysql_05

这样,我们就可以写非常复杂的功能了。接下来,我们传参数和解析响应值。

Request Args

接下来我们实现请求的参数,以及复杂的响应解析,请参考bt-plugin-example-args,下面是详细的解读。

我们先让msg变成一个JSON对象,修改srs_cloud_main.py如下:

def helloworld(self, args):
    return public.returnMsg(True, json.dumps({'r0': 100, 'r1': 'Hello API World!'}))

同时,我们在前端收到响应时,再解析一次msg对象:

return callback({...res, msg: JSON.parse(res.msg)});

这样我们获取到的就是整个JSON对象了:

bt宝塔软件面板 mysql 宝塔app插件怎么用_bt宝塔软件面板 mysql_06

我们可以在args中传一些参数,比如我们加了born和category两个参数:

plugin_request('srs_cloud', 'helloworld', {born:2013, category:'media'}, function (res) {
    $('.plugin_body').html(`<pre>${JSON.stringify(res, null, 2)}</pre>`);
});

同时修改后端的代码,读取这两个参数并返回:

def helloworld(self, args):
    return public.returnMsg(True, json.dumps({
        'r0': 100, 'r1': args.born, 'r2': args.category,
    }))

运行结果如下:

bt宝塔软件面板 mysql 宝塔app插件怎么用_linux_07

注意:插件的后端API,不能随便传参数,有几个参数是不能传的,比如action、s、name、a,会出各种奇怪的错误,比如我们传个name:

plugin_request('srs_cloud', 'helloworld', { name: 'srs'}, function (res) {
    $('.plugin_body').html(`<h3>${JSON.stringify(res)}</h3>`);
});

会发现调用错误,显示的错误是:

Traceback (most recent call last):
  File "class/panelPlugin.py", line 2683, in a
    p = Plugin(get.name)
  File "class/pluginAuth.py", line 80, in panel.class.pluginAuth.Plugin.__init__
  File "class/pluginAuth.py", line 229, in panel.class.pluginAuth.Plugin.__l1Ol11l0O001
  File "class/public.py", line 3411, in get_plugin_main_object
    return plugin_obj,is_php
UnboundLocalError: local variable 'plugin_obj' referenced before assignment

看起来是插件对象问题,实际上是name这个参数不能传。

后面就是如何定义更多的API,实现自己的复杂业务逻辑了。下面是一些其他话题,如果感兴趣也可以参考。

Request Promise

我们把plugin_request修改下,改成现代js的写法,用promise的方式实现:

async function srs_cloud_request(function_name, args, timeout) {
    return new Promise((resolve, reject) => {
      $.ajax({
        type: 'POST',
        url: `/plugin?action=a&s=${function_name}&name=srs_cloud`,
        data: args,
        timeout: timeout || 3600 * 1000,
        success: function (res, status, xhr) {
          return resolve(JSON.parse(res.msg));
        },
        error: reject,
      });
    });
  }

后续我们用到这个函数时,都使用Promise方式的函数,会让整个逻辑非常简单。

Pycharm Development

宝塔定义了Python可以用到一些API,在/www/server/panel/class/public.py中,因此可以看到srs_cloud_main.py中导入这个文件:

os.chdir("/www/server/panel")
sys.path.append("class/")
import public

如果在本机开发,这些函数就会提示错误,所以我们可以把服务器的文件下载下来,配置Pycharm的开发环境,这样就更容易知道可以调用哪些API了。

我们在服务器上把整个宝塔都打包下载下来,然后在Pycharm的设置中,添加这个路径就可以:

bt宝塔软件面板 mysql 宝塔app插件怎么用_bt宝塔软件面板 mysql_08

bt宝塔软件面板 mysql 宝塔app插件怎么用_html_09

bt宝塔软件面板 mysql 宝塔app插件怎么用_html_10

我们在Pycharm中打开demo,可以看到public的函数都可以找到了:

bt宝塔软件面板 mysql 宝塔app插件怎么用_html_11

这样开发效率是很高的。

Template

Demo中的demo_main.py,定义了一些函数,比如demo_main.index和demo_main.get_logs,然后他们的备注说是模板。

这个是指,可以在宝塔中打开对应插件的页面,这样会请求对应的API,并且绑定到对应的模板中。比如,安装demo插件后,打开:

http://your_server_ip:8888/demo/index.html

展示的就是一个由demo_main.index和templates/index.html渲染出来的页面:

bt宝塔软件面板 mysql 宝塔app插件怎么用_bt宝塔软件面板 mysql_12

注意官方手册中有个说明:

Note: 插件动态路由不继承面板会话权限,需开发者自己在插件(_check 方法)中做好访问权限的控制,请谨慎使用。

这个弄得很晕,其实并不是必要的,很多插件包括官方的插件,都没有用这种方式。

Config

在Demo中还有个config.json的操作函数,具体没看到怎么用法,猜测可能是有便捷的方式能让插件读取和写入配置吧。

Install

关于install.sh特别说明几点:

  1. 就算exit 1也会提示安装成功,所以不能依赖脚本来做复杂的任务,比如终止安装和回滚等。
  2. 如果要检测依赖,比如需要nginx,那可以在html和py中检测,加一个API,如果没有就提示用户安装。
  3. 依赖的文件一般都放在插件的压缩包中,除非是一些公共的比如git/make/docker等,可以直接安装了。

宝塔的管理逻辑是简化安装,重点是在设置页面再处理。

比如PostgreSQL管理器这个插件,就会在插件配置中的版本管理安装数据库。第一次打开它时,会提示没有数据库,要安装,这时候才去安装和管理不同的版本。如下图所示:

bt宝塔软件面板 mysql 宝塔app插件怎么用_宝塔_13

这样可以做比较复杂的安装和回滚逻辑,比如选择不同的版本安装。

JS API: Message

前端也有一些API可以使用,比如弹一个消息出来等等。

下面是一个例子,在index.html中写如下代码:

<div class="bt-form">
    <div class="bt-w-main"></div>
</div>
<script type="text/javascript">
    layer.msg('HelloWorld', {icon: 1});
</script>

这样会弹出消息,过几秒就会消失,如下图所示:

bt宝塔软件面板 mysql 宝塔app插件怎么用_html_14

这个API的定义如下:

layer.msg(msg: String, {icon: Integer});
  • msg: 弹出的消息内容。
  • icon: 整形数字,定义如下:
  • 0是警告,
  • 1是成功消息,
  • 2是失败消息,
  • 3是黄色问号,
  • 4一把灰色的锁,
  • 5是红色哭脸,
  • 6是绿色笑脸,
  • 7是黄色感叹号,
  • 16是转圈圈的图标

例如,弹出成功消息:

layer.msg('HelloWorld', {icon: 1});

有时候需要弹窗确认,还有个弹窗的API可用。

JS API: Dialog

比如我们要用户确认操作,可以弹一个确认窗口出来。

下面是一个例子,在index.html中写如下代码:

<div class="bt-form">
    <div class="bt-w-main"></div>
</div>
<script type="text/javascript">
  const confirm = layer.open({
    icon: 0,
    closeBtn: 2,
    title: '提示',
    area: '400px',
    btn: ['确定', '取消'],
    content: '<div class="bt_conter bt-form">是否继续操作?</div>',
    yes: function (index, layero) {
      layer.close(confirm);
    }
  });
</script>

这样就会弹出一个确认窗口,如下图所示:

bt宝塔软件面板 mysql 宝塔app插件怎么用_bt宝塔软件面板 mysql_15

这个API的定义如下:

layer.open({
  icon: Integer, closeBtn: Integer, title: String, area: String, 
  btn: [String], Content: String, yes: Function,
});
  • icon: 具体定义参考JS API: Message中的详细说明。
  • btn: 按钮数组,0是确认,1是取消,如果为空则不显示任何按钮。
  • closeBtn: 0不显示关闭按钮,2显示关闭按钮。

有时候我们会依赖其他的基础软件,比如NGINX。我们可以在后端py中检测依赖的软件路径,若不存在则返回错误给前端,前端提示用户安装。

JS API: Close

弹出消息Message和对话框Dialog都是可以关闭的,接口是:layer.close(id)或者layer.closeAll()。

比如,我们在调用接口前弹出一个消息框,然后接口返回后我们关闭提示:

const tips = layer.msg('检测服务状态', {icon: 16});
    const services = await srs_cloud_request('serviceStatus');
    console.log(`services status is ${JSON.stringify(services)}`);
    layer.close(tips);

或者,我们如果要安装别的依赖软件,弹出来依赖的安装对话框,但是我们想把自己的安装窗口关闭掉,那么就可以关闭所有的窗口:

layer.closeAll();
await install_dependence();

这样就能避免在一个逻辑中做过多的判断了。

JS API: Software

宝塔提供了函数,可以安装特定的软件,比如NGINX,我们在前端index.html中写如下代码:

<div class="bt-form">
    <div class="bt-w-main">
        <div class="bt-w-con pd15">
            <div class="plugin_body">
                <a href="javascript:void(0);" class="btlink" onclick="bt.soft.install('nginx')">
                    安装NGINX
                </a>
            </div>
        </div>
    </div>
</div>

则会显示一个安装NGINX的链接,点击后会弹出来安装NGINX的对话框:

bt宝塔软件面板 mysql 宝塔app插件怎么用_linux_16

如果是强依赖NGINX,安装前不能点其他操作,可以配合前面的Confirm来弹出一个对话框,HTML如下:

<div class="bt-form">
    <div class="bt-w-main"></div>
</div>
<script type="text/javascript">
  const confirm = layer.open({
    icon: 0,
    closeBtn: 0,
    title: '请安装依赖',
    area: '400px',
    btn: [],
    content: `
        <a href="javascript:void(0);" class="btlink" οnclick="bt.soft.install('nginx')">
            安装NGINX
        </a>
    `,
    yes: function (index, layero) {
      layer.close(confirm);
    }
  });
</script>

我们配置了Confirm没有取消和关闭,效果如下:

bt宝塔软件面板 mysql 宝塔app插件怎么用_linux_17

这样用户做其他操作前,必须先安装依赖。

Example: Logging

有时候我们需要起一个任务,然后显示日志,并且会不断滚动刷新最新的日志,如下图所示:

bt宝塔软件面板 mysql 宝塔app插件怎么用_宝塔_18

用一个简单的例子,我们执行个脚本do_setup.sh,不断的打印日志:

#!/bin/bash
for ((;;)); do
  echo "installing $(date)"
  sleep 3
done

我们可以加一个API,如果没有启动脚本就启动这个脚本,并返回日志:

class srs_cloud_main:
    __plugin_path = "/www/server/panel/plugin/srs_cloud"

    def installSrs(self, args):
        if 'start' not in args: args.start = 0
        if 'end' not in args: args.end = 0

        srs = public.ExecShell('ls /usr/lib/systemd/system/srs-cloud.service >/dev/null 2>&1 && echo -n ok')[0]
        running = public.ExecShell('ps aux |grep -v grep |grep srs_cloud |grep do_setup >/dev/null 2>&1 && echo -n ok')[0]

        [tail, wc] = ['', 0]
        if running != 'ok' and srs != 'ok':
            public.ExecShell('nohup bash {}/do_setup.sh >/tmp/srs_cloud_install.log 2>&1 &'.format(self.__plugin_path))
        elif os.path.exists('/tmp/srs_cloud_install.log'):
            tail = public.ExecShell('sed -n "{},{}p" /tmp/srs_cloud_install.log'.format(args.start, args.end))[0]
            wc = public.ExecShell('wc -l /tmp/srs_cloud_install.log')[0]

        return public.returnMsg(True, json.dumps({'srs': srs, 'running': running, 'wc': wc, 'tail': tail}))
  • 我们使用nohup来启动脚本,并使用ps查看脚本是否存在,若没有运行则运行这个脚本。
  • 我们将脚本的日志打印到了/tmp/srs_cloud_install.log,这样可以供前端查询。
  • 我们使用args.start和args.end,1代表第1行,前端可以从第一行开始获取,不断读取日志。
  • 我们会检测是否安装完成,也就是srs已经有了,避免脚本退出后不断重启脚本。

我们在前端展示这个日志,给它加上黑黢黢的一个样式:

<style>
    .tomcat_log {
        margin: 0px;
        width: 100%;
        height: 370px;
        background-color: #424251;
        overflow: auto;
        line-height: 22px;
        color: #fff;
        padding-left: 10px;
        font-family: arial;
        margin-top: 10px;
        outline: none;
    }
</style>
<textarea class="install_log tomcat_log"></textarea>

然后使用脚本调用API,启动任务和获取最新日志:

<script type="text/javascript">
  const installing = async () => {
    let pos = 1;
    for (; ;) {
      const pid = await srs_cloud_request('installSrs', {start: pos, end: pos + 100});

      if (pid.srs) return;
      pos += pid.tail ? (pid.tail.match(/\n/g)?.length || 0) : 0;

      if (pid.tail) {
        const elem = $('.install_log').val($('.install_log').val() + pid.tail);
        elem.get(0).scrollTop = elem.get(0).scrollHeight;
      }

      await new Promise(r0 => setTimeout(r0, 3000));
    }
  };

  installing();
</script>

Note: 函数srs_cloud_request的定义,请参考之前的说明。

每隔一定时间会重复获取日志,这样就实现了日志的刷新。由于我们使用start和end准确读取日志的文件,所以也不会漏掉日志。

Example: Website

若我们插件需要创建Web站点,可以调用panelSite的函数创建,比如SRS自动创建了一个站点,如下图所示:

bt宝塔软件面板 mysql 宝塔app插件怎么用_API_19

除了/www/server/panel/class/public.py,宝塔还提供了/www/server/panel/class/panelSite.py,以及其他的函数可以直接调用。比如,创建站点:

bt宝塔软件面板 mysql 宝塔app插件怎么用_宝塔_20

如果猜不到函数名是什么,则可以在创建站点的页面中,看页面发起了什么请求,比如:

bt宝塔软件面板 mysql 宝塔app插件怎么用_宝塔_21

后端的Python函数可以这么写:

class srs_cloud_main:
    __site = 'srs.cloud.local'
    def createSrsSite(self, args):
        site = panelSite().AddSite(Params(
            webname = json.dumps({"domain": self.__site, "domainlist": [], "count": 0}),
            type = 'PHP',
            port = '80',
            ps = self.__site,
            path = os.path.join(public.get_site_path(), self.__site),
            type_id = 0,
            version = '00',
            ftp = 'false',
            sql = 'false',
            codeing = 'utf8',
        ))
        if 'status' in site:
            return site
        return public.returnMsg(True, json.dumps(site))

class Params(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
    def __iter__(self):
        return self.__dict__.__iter__();
    def __getitem__(self, y):
        return self.__dict__.__getitem__(y)

其中有几个地方要注意的:

  • 我们定义了一个Params的对象,将字典转换成对象,因为panelSite.AddSite(get)的参数是一个对象而不是字典。
  • 参数的类型别弄错了,几乎都是字符串的,特别是webname是一个JSON字符串。
  • 网站的路径,可以通过public.get_site_path()获取,不要写死了,可能会变的。
  • 注意返回值若创建失败是有status,而成功则自己再需要打包下。

还可以调用防火墙对象,开放对应端口。