diff --git a/.gitignore b/.gitignore
index 6bc9a117..44e49ace 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
/tmp/
/temp/
/site/
+/plugins/
# 框架审计文件
audit.log
diff --git a/config/global.php b/config/global.php
index fde39f51..7edb0b24 100644
--- a/config/global.php
+++ b/config/global.php
@@ -71,7 +71,8 @@ $config['plugin'] = [
/* 内部默认启用的插件 */
$config['native_plugin'] = [
- 'onebot12' => true,
+ 'onebot12' => true, // OneBot v12 协议支持
+ 'onebot12-ban-other-ws' => true, // OneBot v12 协议支持,禁止其他 WebSocket 连接
];
/* 静态文件读取器 */
diff --git a/instant-plugin-demo.php b/instant-plugin-demo.php
index 64b59060..fb36d158 100644
--- a/instant-plugin-demo.php
+++ b/instant-plugin-demo.php
@@ -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 () => '
Hello world
');
$plugin->addBotCommand($cmd1);
$plugin->addHttpRoute($route1);
-return [
- 'plugin-name' => 'pasd',
- 'version' => '1.0.0',
- 'plugin' => $plugin,
-];
+$plugin->run();
diff --git a/mybot.php b/mybot.php
index 5320ba6b..3cf25b77 100644
--- a/mybot.php
+++ b/mybot.php
@@ -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);
diff --git a/src/Globals/global_class_alias.php b/src/Globals/global_class_alias.php
index 9f4d1a1c..a20c0cb0 100644
--- a/src/Globals/global_class_alias.php
+++ b/src/Globals/global_class_alias.php
@@ -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');
diff --git a/src/Module/Example/Hello123.php b/src/Module/Example/Hello123.php
index 972b7690..605ff397 100644
--- a/src/Module/Example/Hello123.php
+++ b/src/Module/Example/Hello123.php
@@ -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()
diff --git a/src/ZM/Annotation/AnnotationBase.php b/src/ZM/Annotation/AnnotationBase.php
index 4a3e033c..bab99b50 100644
--- a/src/ZM/Annotation/AnnotationBase.php
+++ b/src/ZM/Annotation/AnnotationBase.php
@@ -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 = [];
diff --git a/src/ZM/Annotation/AnnotationHandler.php b/src/ZM/Annotation/AnnotationHandler.php
index a324044f..f82af3e6 100644
--- a/src/ZM/Annotation/AnnotationHandler.php
+++ b/src/ZM/Annotation/AnnotationHandler.php
@@ -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) {
diff --git a/src/ZM/Annotation/AnnotationMap.php b/src/ZM/Annotation/AnnotationMap.php
index 313ed4db..6fcf452d 100644
--- a/src/ZM/Annotation/AnnotationMap.php
+++ b/src/ZM/Annotation/AnnotationMap.php
@@ -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);
+ });
+ }
+ }
+ }
}
diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php
index ff052582..74353f99 100644
--- a/src/ZM/Annotation/AnnotationParser.php
+++ b/src/ZM/Annotation/AnnotationParser.php
@@ -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;
}
}
diff --git a/src/ZM/Annotation/OneBot/BotCommand.php b/src/ZM/Annotation/OneBot/BotCommand.php
index a614bf73..01b8165e 100644
--- a/src/ZM/Annotation/OneBot/BotCommand.php
+++ b/src/ZM/Annotation/OneBot/BotCommand.php
@@ -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;
diff --git a/src/ZM/Annotation/OneBot/CommandArgument.php b/src/ZM/Annotation/OneBot/CommandArgument.php
index f9654ab5..a578ae0c 100644
--- a/src/ZM/Annotation/OneBot/CommandArgument.php
+++ b/src/ZM/Annotation/OneBot/CommandArgument.php
@@ -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
diff --git a/src/ZM/Event/Listener/WSEventListener.php b/src/ZM/Event/Listener/WSEventListener.php
index 84d8193b..ede6d14c 100644
--- a/src/ZM/Event/Listener/WSEventListener.php
+++ b/src/ZM/Event/Listener/WSEventListener.php
@@ -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();
}
diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php
index 6f661def..233f1441 100644
--- a/src/ZM/Event/Listener/WorkerEventListener.php
+++ b/src/ZM/Event/Listener/WorkerEventListener.php
@@ -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) {
diff --git a/src/ZM/Exception/ZMKnownException.php b/src/ZM/Exception/ZMKnownException.php
index c514b9ef..d26a4019 100644
--- a/src/ZM/Exception/ZMKnownException.php
+++ b/src/ZM/Exception/ZMKnownException.php
@@ -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;
// 这也太懒了吧
diff --git a/src/ZM/Plugin/OneBot12Adapter.php b/src/ZM/Plugin/OneBot12Adapter.php
index ebea5da7..f34d95ba 100644
--- a/src/ZM/Plugin/OneBot12Adapter.php
+++ b/src/ZM/Plugin/OneBot12Adapter.php
@@ -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();
}
}
}
diff --git a/src/ZM/Plugin/PluginManager.php b/src/ZM/Plugin/PluginManager.php
index 6a53f156..e5c54a55 100644
--- a/src/ZM/Plugin/PluginManager.php
+++ b/src/ZM/Plugin/PluginManager.php
@@ -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' => '',
+ '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'] ?? ('');
+ // 设置版本(如果有)
+ 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'] = '';
+ // 到这里,说明没有 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;
}
}
diff --git a/src/ZM/ZMApplication.php b/src/ZM/ZMApplication.php
index 8f805f0a..6d811460 100644
--- a/src/ZM/ZMApplication.php
+++ b/src/ZM/ZMApplication.php
@@ -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();
}
}