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