mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 12:44:51 +08:00
add plugin loader support
This commit is contained in:
parent
52a195aca2
commit
cd2bb1b570
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,7 @@
|
||||
/tmp/
|
||||
/temp/
|
||||
/site/
|
||||
/plugins/
|
||||
|
||||
# 框架审计文件
|
||||
audit.log
|
||||
|
||||
@ -71,7 +71,8 @@ $config['plugin'] = [
|
||||
|
||||
/* 内部默认启用的插件 */
|
||||
$config['native_plugin'] = [
|
||||
'onebot12' => true,
|
||||
'onebot12' => true, // OneBot v12 协议支持
|
||||
'onebot12-ban-other-ws' => true, // OneBot v12 协议支持,禁止其他 WebSocket 连接
|
||||
];
|
||||
|
||||
/* 静态文件读取器 */
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$plugin = new ZMPlugin(__DIR__);
|
||||
$plugin = new \ZM\ZMApplication(__DIR__);
|
||||
|
||||
/*
|
||||
* 发送 "测试 123",回复 "你好,123"
|
||||
@ -17,8 +17,4 @@ $route1 = Route::make('/index233')->on(fn () => '<h1>Hello world</h1>');
|
||||
$plugin->addBotCommand($cmd1);
|
||||
$plugin->addHttpRoute($route1);
|
||||
|
||||
return [
|
||||
'plugin-name' => 'pasd',
|
||||
'version' => '1.0.0',
|
||||
'plugin' => $plugin,
|
||||
];
|
||||
$plugin->run();
|
||||
|
||||
@ -24,10 +24,10 @@ $app->enablePlugins([
|
||||
'd',
|
||||
]);
|
||||
// BotCommand 事件构造
|
||||
$cmd = \ZM\Annotation\OneBot\BotCommand::make('test')->withMethod(function () {
|
||||
$cmd = BotCommand::make('test')->on(function () {
|
||||
ctx()->reply('test ok');
|
||||
});
|
||||
$event = \ZM\Annotation\OneBot\BotEvent::make('message')->withMethod(function () {
|
||||
$event = BotEvent::make(type: 'message')->on(function () {
|
||||
});
|
||||
$app->addBotEvent($event);
|
||||
$app->addBotCommand($cmd);
|
||||
|
||||
@ -13,3 +13,9 @@ class_alias(\ZM\Annotation\OneBot\BotEvent::class, 'BotEvent');
|
||||
class_alias(\ZM\Annotation\OneBot\CommandArgument::class, 'CommandArgument');
|
||||
class_alias(\ZM\Annotation\Closed::class, 'Closed');
|
||||
class_alias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin');
|
||||
|
||||
// 下面是 OneBot 相关类的全局别称
|
||||
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');
|
||||
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketCloseEvent::class, 'WebSocketCloseEvent');
|
||||
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketMessageEvent::class, 'WebSocketMessageEvent');
|
||||
class_alias(\OneBot\Driver\Event\Http\HttpRequestEvent::class, 'HttpRequestEvent');
|
||||
|
||||
@ -4,19 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Framework\Setup;
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Middleware\TimerMiddleware;
|
||||
|
||||
class Hello123
|
||||
{
|
||||
#[Setup]
|
||||
public function onRequest()
|
||||
{
|
||||
echo "OK\n";
|
||||
}
|
||||
|
||||
#[Route('/route', request_method: ['GET'])]
|
||||
#[Middleware(TimerMiddleware::class)]
|
||||
public function route()
|
||||
|
||||
@ -6,11 +6,9 @@ namespace ZM\Annotation;
|
||||
|
||||
abstract class AnnotationBase implements \IteratorAggregate
|
||||
{
|
||||
public string $method = '';
|
||||
/** @var array|\Closure|string 方法名或闭包 */
|
||||
public \Closure|string|array $method = '';
|
||||
|
||||
/**
|
||||
* @var \Closure|string
|
||||
*/
|
||||
public $class = '';
|
||||
|
||||
public array $group = [];
|
||||
|
||||
@ -127,14 +127,18 @@ class AnnotationHandler
|
||||
// 由于3.0有额外的插件模式支持,所以注解就不再提供独立的闭包函数调用支持了
|
||||
// 提取要调用的目标类和方法名称
|
||||
$class = $v->class;
|
||||
$target_class = new $class();
|
||||
$target_method = $v->method;
|
||||
if ($class !== '') {
|
||||
$target_class = new $class();
|
||||
$callback = [$target_class, $target_method];
|
||||
} else {
|
||||
$callback = $target_method;
|
||||
}
|
||||
// 先执行规则,失败就返回false
|
||||
if ($rule_callback !== null && !$rule_callback($v)) {
|
||||
$this->status = self::STATUS_RULE_FAILED;
|
||||
return false;
|
||||
}
|
||||
$callback = [$target_class, $target_method];
|
||||
try {
|
||||
$this->return_val = middleware()->process($callback, ...$args);
|
||||
} catch (InterruptException $e) {
|
||||
|
||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation;
|
||||
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
|
||||
/**
|
||||
* 注解全局存取位置
|
||||
*/
|
||||
@ -36,4 +38,20 @@ class AnnotationMap
|
||||
self::$_list = array_merge(self::$_list, $parser->generateAnnotationList());
|
||||
self::$_map = $parser->getAnnotationMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序所有的注解
|
||||
*/
|
||||
public static function sortAnnotationList(): void
|
||||
{
|
||||
foreach (self::$_list as $class => $annotations) {
|
||||
if (is_a($class, Level::class, true)) {
|
||||
usort(self::$_list[$class], function ($a, $b) {
|
||||
$left = $a->getLevel(); /** @phpstan-ignore-line */
|
||||
$right = $b->getLevel(); /* @phpstan-ignore-line */
|
||||
return $left > $right ? -1 : ($left == $right ? 0 : 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ use Koriym\Attributes\DualReader;
|
||||
use ZM\Annotation\Http\Controller;
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\Interfaces\ErgodicAnnotation;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Store\FileSystem;
|
||||
use ZM\Utils\HttpUtil;
|
||||
@ -67,7 +66,7 @@ class AnnotationParser
|
||||
* @param string $class_name 注解类名
|
||||
* @param callable $callback 回调函数
|
||||
*/
|
||||
public function addSpecialParser(string $class_name, callable $callback)
|
||||
public function addSpecialParser(string $class_name, callable $callback): void
|
||||
{
|
||||
$this->special_parsers[$class_name][] = $callback;
|
||||
}
|
||||
@ -160,14 +159,11 @@ class AnnotationParser
|
||||
}
|
||||
|
||||
// 预处理3:调用自定义解析器
|
||||
foreach (($this->special_parsers[get_class($vs)] ?? []) as $parser) {
|
||||
$result = $parser($vs);
|
||||
if ($result === true) {
|
||||
continue 2;
|
||||
}
|
||||
if ($result === false) {
|
||||
continue 3;
|
||||
}
|
||||
if (($a = $this->parseSpecial($vs)) === true) {
|
||||
continue;
|
||||
}
|
||||
if ($a === false) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,14 +187,11 @@ class AnnotationParser
|
||||
}
|
||||
|
||||
// 预处理3.3:调用自定义解析器
|
||||
foreach (($this->special_parsers[get_class($method_anno)] ?? []) as $parser) {
|
||||
$result = $parser($method_anno);
|
||||
if ($result === true) {
|
||||
continue 2;
|
||||
}
|
||||
if ($result === false) {
|
||||
continue 3;
|
||||
}
|
||||
if (($a = $this->parseSpecial($method_anno, $methods_annotations)) === true) {
|
||||
continue;
|
||||
}
|
||||
if ($a === false) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// 如果上方没有解析或返回了 true,则添加到注解解析列表中
|
||||
@ -240,12 +233,20 @@ class AnnotationParser
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($o as $k => $v) {
|
||||
$this->sortByLevel($o, $k);
|
||||
}
|
||||
return $o;
|
||||
}
|
||||
|
||||
public function parseSpecial($annotation, $same_method_annotations = null): ?bool
|
||||
{
|
||||
foreach (($this->special_parsers[get_class($annotation)] ?? []) as $parser) {
|
||||
$result = $parser($annotation, $same_method_annotations);
|
||||
if (is_bool($result)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加解析的路径
|
||||
*
|
||||
@ -258,26 +259,6 @@ class AnnotationParser
|
||||
$this->path_list[] = [$path, $indoor_name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序注解列表
|
||||
*
|
||||
* @param array $events 需要排序的
|
||||
* @param string $class_name 排序的类名
|
||||
* @param string $prefix 前缀
|
||||
* @internal 用于 level 排序
|
||||
*/
|
||||
public function sortByLevel(array &$events, string $class_name, string $prefix = '')
|
||||
{
|
||||
if (is_a($class_name, Level::class, true)) {
|
||||
$class_name .= $prefix;
|
||||
usort($events[$class_name], function ($a, $b) {
|
||||
$left = $a->getLevel();
|
||||
$right = $b->getLevel();
|
||||
return $left > $right ? -1 : ($left == $right ? 0 : 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解析器调用的时间(秒)
|
||||
*/
|
||||
@ -297,14 +278,16 @@ class AnnotationParser
|
||||
/**
|
||||
* 添加注解路由
|
||||
*/
|
||||
private function addRouteAnnotation(Route $vss): void
|
||||
private function addRouteAnnotation(Route $vss, ?array $same_method_annotations = null)
|
||||
{
|
||||
// 拿到所属方法的类上面有没有控制器的注解
|
||||
$prefix = '';
|
||||
foreach (($this->annotation_tree[$vss->class]['methods_annotations'][$vss->method] ?? []) as $annotation) {
|
||||
if ($annotation instanceof Controller) {
|
||||
$prefix = $annotation->prefix;
|
||||
break;
|
||||
if ($same_method_annotations !== null) {
|
||||
foreach ($same_method_annotations as $annotation) {
|
||||
if ($annotation instanceof Controller) {
|
||||
$prefix = $annotation->prefix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$tail = trim($vss->route, '/');
|
||||
@ -314,5 +297,6 @@ class AnnotationParser
|
||||
$route->setMethods($vss->request_method);
|
||||
|
||||
HttpUtil::getRouteCollection()->add(md5($route_name), $route);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation\OneBot;
|
||||
|
||||
use Attribute;
|
||||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
@ -114,6 +113,12 @@ class BotCommand extends AnnotationBase implements Level
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withArgumentObject(CommandArgument $argument): BotCommand
|
||||
{
|
||||
$this->arguments[] = $argument;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLevel(): int
|
||||
{
|
||||
return $this->level;
|
||||
|
||||
@ -43,7 +43,7 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation
|
||||
* @param string $name 参数名称(可以是中文)
|
||||
* @param string $description 参数描述(默认为空)
|
||||
* @param bool $required 参数是否必需,如果是必需,为true(默认为false)
|
||||
* @param string $prompt 当参数为必需时,返回给用户的提示输入的消息(默认为"请输入$name")
|
||||
* @param string $prompt 当参数为必需且缺失时,返回给用户的提示输入的消息(默认为"请输入$name")
|
||||
* @param string $default 当required为false时,未匹配到参数将自动使用default值(默认为空)
|
||||
* @param int $timeout prompt超时时间(默认为60秒)
|
||||
* @throws InvalidArgumentException|ZMKnownException
|
||||
|
||||
@ -40,11 +40,23 @@ class WSEventListener
|
||||
|
||||
public function onWebSocketMessage(WebSocketMessageEvent $event): void
|
||||
{
|
||||
// 调用注解
|
||||
$handler = new AnnotationHandler(BindEvent::class);
|
||||
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketMessageEvent::class, true));
|
||||
$handler->handleAll($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function onWebSocketClose(WebSocketCloseEvent $event): void
|
||||
{
|
||||
logger()->info('关闭连接: ' . $event->getFd());
|
||||
// 调用注解
|
||||
$handler = new AnnotationHandler(BindEvent::class);
|
||||
$handler->setRuleCallback(fn ($x) => is_a($x->event_class, WebSocketCloseEvent::class, true));
|
||||
$handler->handleAll($event);
|
||||
|
||||
ConnectionUtil::removeConnection($event->getFd());
|
||||
resolve(ContainerServicesProvider::class)->cleanup();
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ use ZM\Plugin\PluginManager;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
use ZM\Store\Database\DBException;
|
||||
use ZM\Store\Database\DBPool;
|
||||
use ZM\Store\FileSystem;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
class WorkerEventListener
|
||||
@ -29,7 +30,7 @@ class WorkerEventListener
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function onWorkerStart999()
|
||||
public function onWorkerStart999(): void
|
||||
{
|
||||
// 自注册一下,刷新当前进程的logger进程banner
|
||||
ob_logger_register(ob_logger());
|
||||
@ -120,7 +121,7 @@ class WorkerEventListener
|
||||
{
|
||||
logger()->debug('Loading user sources');
|
||||
|
||||
// 首先先加载 source 普通插件,相当于内部模块,不算插件的一种
|
||||
// 首先先加载 source 模式的代码,相当于内部模块,不算插件的一种
|
||||
$parser = new AnnotationParser();
|
||||
$composer = ZMUtil::getComposerMetadata();
|
||||
// 合并 dev 和 非 dev 的 psr-4 加载目录
|
||||
@ -135,22 +136,49 @@ class WorkerEventListener
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 然后加载插件目录下的插件
|
||||
PluginManager::addPlugin([
|
||||
'name' => 'onebot12-adapter',
|
||||
'plugin' => new OneBot12Adapter(),
|
||||
]);
|
||||
// 首先加载内置插件
|
||||
$native_plugins = config('global.native_plugin');
|
||||
foreach ($native_plugins as $name => $enable) {
|
||||
if (!$enable) {
|
||||
continue;
|
||||
}
|
||||
match ($name) {
|
||||
'onebot12' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(parser: $parser)]),
|
||||
'onebot12-ban-other-ws' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(submodule: $name)]),
|
||||
};
|
||||
}
|
||||
|
||||
// 然后加载插件目录的插件
|
||||
if (config('global.plugin.enable')) {
|
||||
$load_dir = config('global.plugin.load_dir');
|
||||
if (empty($load_dir)) {
|
||||
$load_dir = SOURCE_ROOT_DIR . '/plugins';
|
||||
} elseif (FileSystem::isRelativePath($load_dir)) {
|
||||
$load_dir = SOURCE_ROOT_DIR . '/' . $load_dir;
|
||||
}
|
||||
$load_dir = zm_dir($load_dir);
|
||||
|
||||
$count = PluginManager::addPluginsFromDir($load_dir);
|
||||
logger()->info('Loaded ' . $count . ' user plugins');
|
||||
|
||||
// 启用并初始化插件
|
||||
PluginManager::enablePlugins($parser);
|
||||
}
|
||||
|
||||
// 解析所有注册路径的文件,获取注解
|
||||
$parser->parseAll();
|
||||
// 将Parser解析后的注解注册到全局的 AnnotationMap
|
||||
AnnotationMap::loadAnnotationByParser($parser);
|
||||
// 排序所有的
|
||||
AnnotationMap::sortAnnotationList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分发调用 Init 注解
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function dispatchInit()
|
||||
private function dispatchInit(): void
|
||||
{
|
||||
$handler = new AnnotationHandler(Init::class);
|
||||
$handler->setRuleCallback(function (Init $anno) {
|
||||
@ -166,7 +194,7 @@ class WorkerEventListener
|
||||
*
|
||||
* @throws DBException
|
||||
*/
|
||||
private function initConnectionPool()
|
||||
private function initConnectionPool(): void
|
||||
{
|
||||
// 清空 MySQL 的连接池
|
||||
foreach (DBPool::getAllPools() as $name => $pool) {
|
||||
|
||||
@ -11,7 +11,7 @@ class ZMKnownException extends ZMException
|
||||
{
|
||||
public function __construct($err_code, $message = '', $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(zm_internal_errcode($err_code) . $message, $code, $previous);
|
||||
parent::__construct(zm_internal_errcode($err_code) . $message, '', $code, $previous);
|
||||
if ($err_code === 'E99999') {
|
||||
$code = 0;
|
||||
// 这也太懒了吧
|
||||
|
||||
@ -5,19 +5,79 @@ declare(strict_types=1);
|
||||
namespace ZM\Plugin;
|
||||
|
||||
use Choir\Http\HttpFactory;
|
||||
use OneBot\Driver\Event\StopException;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\OneBot\BotCommand;
|
||||
use ZM\Annotation\OneBot\CommandArgument;
|
||||
use ZM\Utils\ConnectionUtil;
|
||||
|
||||
class OneBot12Adapter extends ZMPlugin
|
||||
{
|
||||
public function __construct()
|
||||
public function __construct(string $submodule = '', ?AnnotationParser $parser = null)
|
||||
{
|
||||
parent::__construct(__DIR__);
|
||||
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseInput']);
|
||||
switch ($submodule) {
|
||||
case '':
|
||||
case 'onebot12':
|
||||
// 处理所有 OneBot 12 的反向 WS 握手事件
|
||||
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseInput']);
|
||||
// 处理和声明所有 BotCommand 下的 CommandArgument
|
||||
$parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']);
|
||||
// 不需要给列表写入 CommandArgument
|
||||
$parser->addSpecialParser(CommandArgument::class, [$this, 'parseCommandArgument']);
|
||||
break;
|
||||
case 'onebot12-ban-other-ws':
|
||||
// 禁止其他类型的 WebSocket 客户端接入
|
||||
$this->addEvent(WebSocketOpenEvent::class, [$this, 'handleUnknownWSReverseInput'], 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 BotCommand 假设含有 CommandArgument 的话,就注册到参数列表中
|
||||
*
|
||||
* @param BotCommand $command 命令对象
|
||||
* @param null|array $same_method_annotations 同一个方法的所有注解
|
||||
*/
|
||||
public function parseBotCommand(BotCommand $command, ?array $same_method_annotations = null): ?bool
|
||||
{
|
||||
if ($same_method_annotations === null) {
|
||||
return null;
|
||||
}
|
||||
foreach ($same_method_annotations as $v) {
|
||||
if ($v instanceof CommandArgument) {
|
||||
$command->withArgumentObject($v);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略解析记录 CommandArgument 注解
|
||||
*/
|
||||
public function parseCommandArgument(): ?bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StopException
|
||||
*/
|
||||
public function handleUnknownWSReverseInput(WebSocketOpenEvent $event)
|
||||
{
|
||||
// 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头
|
||||
$line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
|
||||
if ($line[0] !== '12') {
|
||||
logger()->warning('不允许接入除 OneBot 12 以外的 WebSocket Client');
|
||||
$event->withResponse(HttpFactory::createResponse(403, 'Forbidden'));
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接入和认证反向 WS 的连接
|
||||
* @throws StopException
|
||||
*/
|
||||
public function handleWSReverseInput(WebSocketOpenEvent $event): void
|
||||
{
|
||||
@ -39,7 +99,7 @@ class OneBot12Adapter extends ZMPlugin
|
||||
if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token,鉴权失败
|
||||
logger()->warning('OneBot 12 反向 WS 连接鉴权失败,拒绝接入');
|
||||
$event->withResponse(HttpFactory::createResponse(401, 'Unauthorized'));
|
||||
return;
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,37 +4,206 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Plugin;
|
||||
|
||||
use ZM\Annotation\AnnotationMap;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Framework\BindEvent;
|
||||
use ZM\Annotation\OneBot\BotEvent;
|
||||
use ZM\Exception\PluginException;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
private const DEFAULT_META = [
|
||||
'name' => '<anonymous>',
|
||||
'version' => 'dev',
|
||||
'dir' => '',
|
||||
'object' => null,
|
||||
'entry_file' => null,
|
||||
'autoload' => null,
|
||||
'dependencies' => [],
|
||||
];
|
||||
|
||||
/** @var array|string[] 缺省的自动加载插件的入口文件 */
|
||||
public static array $default_entries = [
|
||||
'main.php',
|
||||
'entry.php',
|
||||
'index.php',
|
||||
];
|
||||
|
||||
/** @var array 插件信息列表 */
|
||||
private static array $plugins = [];
|
||||
|
||||
/**
|
||||
* 传入插件父目录,扫描插件目录下的所有插件并注册添加
|
||||
*
|
||||
* @param string $dir 插件目录
|
||||
* @return int 返回添加插件的数量
|
||||
* @throws PluginException
|
||||
*/
|
||||
public static function addPluginsFromDir(string $dir): int
|
||||
{
|
||||
// 遍历插件目录
|
||||
$list = FileSystem::scanDirFiles($dir, false, false, true);
|
||||
$cnt = 0;
|
||||
foreach ($list as $item) {
|
||||
// 必须是目录形式的插件
|
||||
if (!is_dir($item)) {
|
||||
continue;
|
||||
}
|
||||
$plugin_meta = self::DEFAULT_META;
|
||||
$plugin_meta['dir'] = $item;
|
||||
|
||||
// 看看有没有插件信息文件
|
||||
$info_file = $item . '/zmplugin.json';
|
||||
$main_file = '';
|
||||
// 如果有的话,就从插件信息文件中找到插件信息
|
||||
if (is_file($info_file)) {
|
||||
$info = json_decode(file_get_contents($info_file), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logger()->error('插件信息文件解析失败: ' . json_last_error_msg());
|
||||
continue;
|
||||
}
|
||||
// 设置名称(如果有)
|
||||
$plugin_meta['name'] = $info['name'] ?? ('<anonymous:' . pathinfo($item, PATHINFO_BASENAME) . '>');
|
||||
// 设置版本(如果有)
|
||||
if (isset($info['version'])) {
|
||||
$plugin_meta['version'] = $info['version'];
|
||||
}
|
||||
// 设置了入口文件,则遵循这个入口文件
|
||||
if (isset($info['main'])) {
|
||||
$main_file = FileSystem::isRelativePath($info['main']) ? ($item . '/' . $info['main']) : $info['main'];
|
||||
} else {
|
||||
$main_file = self::matchDefaultEntry($item);
|
||||
}
|
||||
|
||||
// 检查有没有 composer.json 和 vendor/autoload.php 自动加载,如果有的话,那就写上去
|
||||
$composer_file = $item . '/composer.json';
|
||||
if (is_file(zm_dir($composer_file))) {
|
||||
// composer.json 存在,那么就加载这个插件
|
||||
$composer = json_decode(file_get_contents($composer_file), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logger()->error('插件 composer.json 文件解析失败: ' . json_last_error_msg());
|
||||
continue;
|
||||
}
|
||||
if (isset($composer['autoload']['psr-4']) && is_assoc_array($composer['autoload']['psr-4'])) {
|
||||
$plugin_meta['autoload'] = $composer['autoload']['psr-4'];
|
||||
}
|
||||
}
|
||||
|
||||
// 主文件存在,则加载
|
||||
if (is_file(zm_dir($main_file))) {
|
||||
// 如果入口文件存在,那么就加载这个插件
|
||||
$plugin_meta['entry_file'] = $main_file;
|
||||
}
|
||||
|
||||
// composer.json 不存在,那么就忽略这个插件,并报 warning
|
||||
if (!is_file(zm_dir($composer_file)) && $plugin_meta['entry_file'] === null) {
|
||||
logger()->warning('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载');
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$plugin_meta['name'] = '<unnamed:' . pathinfo($item, PATHINFO_BASENAME) . '>';
|
||||
// 到这里,说明没有 zmplugin.json 这个文件,那么我们就直接匹配
|
||||
$main_file = self::matchDefaultEntry($item);
|
||||
if (is_file(zm_dir($main_file))) {
|
||||
// 如果入口文件存在,那么就加载这个插件
|
||||
$plugin_meta['entry_file'] = $main_file;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 到这里,说明插件信息收集齐了,只需要加载就行了
|
||||
self::addPlugin($plugin_meta);
|
||||
++$cnt;
|
||||
}
|
||||
return $cnt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加插件到全局注册中
|
||||
*
|
||||
* @throws PluginException
|
||||
*/
|
||||
public static function addPlugin(array $meta = []): void
|
||||
{
|
||||
// 首先检测 meta 是否存在 plugin 对象
|
||||
if (isset($meta['plugin'])) {
|
||||
// 存在的话,说明是单例插件,调用对象内的方法注册事件就行了
|
||||
$meta['type'] = 'instant';
|
||||
self::$plugins[$meta['name']] = $meta;
|
||||
if (!isset($meta['name'])) {
|
||||
throw new PluginException('Plugin must have a name!');
|
||||
}
|
||||
logger()->debug('Adding plugin: ' . $meta['name']);
|
||||
|
||||
self::$plugins[$meta['name']] = $meta;
|
||||
|
||||
// 存在直接声明的对象,那么直接初始化
|
||||
if (isset($meta['object']) && $meta['object'] instanceof ZMPlugin) {
|
||||
return;
|
||||
}
|
||||
if (isset($meta['dir'])) {
|
||||
// 不存在的话,说明是多文件插件,是设置了 zmplugin.json 的目录,此目录为自动加载的
|
||||
$meta['type'] = 'dir';
|
||||
self::$plugins[$meta['name']] = $meta;
|
||||
|
||||
// 存在入口文件(单文件),从单文件加载
|
||||
if (isset($meta['entry_file']) && is_file(zm_dir($meta['entry_file']))) {
|
||||
$zmplugin = self::$plugins[$meta['name']]['object'] = require $meta['entry_file'];
|
||||
if (!$zmplugin instanceof ZMPlugin) {
|
||||
unset(self::$plugins[$meta['name']]);
|
||||
throw new PluginException('插件 ' . $meta['name'] . ' 的入口文件 ' . $meta['entry_file'] . ' 必须返回一个 ZMPlugin 对象');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 两者都不存在的话,说明是错误的插件
|
||||
throw new PluginException('plugin meta must have plugin or dir');
|
||||
|
||||
// 存在自动加载,检测 vendor/autoload.php 是否存在,如果存在,那么就加载
|
||||
if (isset($meta['autoload'], $meta['dir']) && $meta['dir'] !== '' && is_file($meta['dir'] . '/vendor/autoload.php')) {
|
||||
require_once $meta['dir'] . '/vendor/autoload.php';
|
||||
return;
|
||||
}
|
||||
// 如果都不存在,那是不可能的事情,抛出一个谁都没见过的异常
|
||||
unset(self::$plugins[$meta['name']]);
|
||||
throw new PluginException('插件 ' . $meta['name'] . ' 无法加载,因为没有入口文件,也没有自动加载文件和内建 Composer');
|
||||
}
|
||||
|
||||
public static function getPlugins(): array
|
||||
public static function enablePlugins(AnnotationParser $parser): void
|
||||
{
|
||||
return self::$plugins;
|
||||
foreach (self::$plugins as $name => $plugin) {
|
||||
if (!isset($plugin['internal'])) {
|
||||
logger()->info('Enabling plugin: ' . $name);
|
||||
}
|
||||
if (isset($plugin['object']) && $plugin['object'] instanceof ZMPlugin) {
|
||||
$obj = $plugin['object'];
|
||||
// 将 Event 加入事件监听
|
||||
foreach ($obj->getEvents() as $event) {
|
||||
$bind = new BindEvent($event[0], $event[2]);
|
||||
$bind->on($event[1]);
|
||||
AnnotationMap::$_list[BindEvent::class][] = $bind;
|
||||
}
|
||||
// 将 Routes 加入事件监听
|
||||
foreach ($obj->getRoutes() as $route) {
|
||||
$parser->parseSpecial($route);
|
||||
}
|
||||
// 将 BotEvents 加入事件监听
|
||||
foreach ($obj->getBotEvents() as $event) {
|
||||
AnnotationMap::$_list[BotEvent::class][] = $event;
|
||||
}
|
||||
// 将 BotCommand 加入事件监听
|
||||
foreach ($obj->getBotCommands() as $cmd) {
|
||||
$parser->parseSpecial($cmd);
|
||||
}
|
||||
} elseif (isset($plugin['autoload'], $plugin['dir'])) {
|
||||
foreach ($plugin['autoload'] as $k => $v) {
|
||||
$parser->addRegisterPath($plugin['dir'] . '/' . $v . '/', trim($k, '\\'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function matchDefaultEntry(string $dir): string
|
||||
{
|
||||
$main = '';
|
||||
// 没有设置入口文件,则遍历默认入口文件列表
|
||||
foreach (self::$default_entries as $entry) {
|
||||
$main_file = $dir . '/' . $entry;
|
||||
if (is_file(zm_dir($main_file))) {
|
||||
$main = $main_file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $main;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ namespace ZM;
|
||||
|
||||
use ZM\Command\Server\ServerStartCommand;
|
||||
use ZM\Exception\SingletonViolationException;
|
||||
use ZM\Plugin\PluginManager;
|
||||
use ZM\Plugin\ZMPlugin;
|
||||
|
||||
class ZMApplication extends ZMPlugin
|
||||
@ -43,6 +44,7 @@ class ZMApplication extends ZMPlugin
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
PluginManager::addPlugin(['name' => 'native-app', 'object' => $this]);
|
||||
(new Framework($this->args))->init()->start();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user