From 8cb6a323417197d941d9df8eed7fd52edaf0ee0f Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sat, 30 Apr 2022 20:02:44 +0800 Subject: [PATCH] refactor CommandHelpGenerator to CommandInfoUtil --- src/Module/Example/Hello.php | 9 +- src/ZM/Utils/CommandInfoUtil.php | 180 +++++++++++++++++++++++++++++ src/ZM/Utils/MessageUtil.php | 77 ------------ tests/ZM/Utils/MessageUtilTest.php | 5 +- 4 files changed, 191 insertions(+), 80 deletions(-) create mode 100644 src/ZM/Utils/CommandInfoUtil.php diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 02d97154..1746a9a9 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -13,6 +13,7 @@ use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnRequestEvent; +use ZM\Annotation\Swoole\OnStart; use ZM\API\CQ; use ZM\API\OneBotV11; use ZM\API\TuringAPI; @@ -24,6 +25,7 @@ use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Module\QQBot; use ZM\Requests\ZMRequest; +use ZM\Utils\CommandInfoUtil; use ZM\Utils\MessageUtil; use ZM\Utils\ZMUtil; @@ -232,7 +234,12 @@ class Hello #[CQCommand('帮助')] public function help(): string { - $helps = MessageUtil::generateCommandHelp(); + $util = resolve(CommandInfoUtil::class); + $commands = $util->get(); + $helps = []; + foreach ($commands as $command) { + $helps[] = $util->getHelp($command['id']); + } array_unshift($helps, '帮助:'); return implode("\n", $helps); } diff --git a/src/ZM/Utils/CommandInfoUtil.php b/src/ZM/Utils/CommandInfoUtil.php new file mode 100644 index 00000000..649d4314 --- /dev/null +++ b/src/ZM/Utils/CommandInfoUtil.php @@ -0,0 +1,180 @@ + 'string', 'call' => 'callable', 'descriptions' => ['string'], 'trigger' => ['string' => ['string']]]])] + public function get(): array + { + if (!$this->exists()) { + return $this->generate(); + } + return WorkerCache::get('commands'); + } + + /** + * 根据注解树生成命令信息 + */ + #[ArrayShape([['id' => 'string', 'call' => 'callable', 'descriptions' => ['string'], 'trigger' => ['string' => ['string']]]])] + public function generate(): array + { + if ($this->exists()) { + return $this->get(); + } + + return $this->generate0(); + } + + /** + * 重新生成命令信息 + */ + public function regenerate(): void + { + $this->generate0(); + } + + /** + * 获取命令帮助 + * + * @param string $command_id 命令ID,为 `class@method` 格式 + */ + public function getHelp(string $command_id): string + { + $command = $this->get()[$command_id]; + + $formats = [ + 'match' => '%s', + 'pattern' => '符合”%s“', + 'regex' => '匹配“%s”', + 'start_with' => '以”%s“开头', + 'end_with' => '以”%s“结尾', + 'keyword' => '包含“%s”', + 'alias' => '%s', + ]; + $triggers = []; + foreach ($command['triggers'] as $trigger => $conditions) { + if (count($conditions) === 0) { + continue; + } + if (isset($formats[$trigger])) { + $format = $formats[$trigger]; + } else { + Console::warning("未知的命令触发条件:{$trigger}"); + continue; + } + foreach ($conditions as $condition) { + $condition = sprintf($format, $condition); + $triggers[] = $condition; + } + } + $name = array_shift($triggers); + if (count($triggers) > 0) { + $name .= '(' . implode(',', $triggers) . ')'; + } + + if (empty($command['descriptions'])) { + $description = '作者很懒,啥也没说'; + } else { + $description = implode(';', $command['descriptions']); + } + + return "{$name}:{$description}"; + } + + /** + * 缓存命令信息 + */ + protected function save(array $helps): void + { + WorkerCache::set('commands', $helps); + } + + /** + * 根据注解树生成命令信息(内部) + */ + #[ArrayShape([['id' => 'string', 'call' => 'callable', 'descriptions' => ['string'], 'trigger' => ['string' => ['string']]]])] + protected function generate0(): array + { + $commands = []; + + foreach (EventManager::$events[CQCommand::class] as $annotation) { + // 正常来说不可能,但保险起见需要判断 + if (!$annotation instanceof CQCommand) { + continue; + } + + $id = "{$annotation->class}@{$annotation->method}"; + + try { + $reflection = new ReflectionMethod($annotation->class, $annotation->method); + } catch (ReflectionException $e) { + Console::warning('命令 ' . $id . ' 注解解析错误:' . $e->getMessage()); + continue; + } + + $doc = $reflection->getDocComment(); + if ($doc) { + // 匹配出不以@开头,且后接中文或任意非空格字符,并以换行符结尾的字符串,也就是命令描述 + preg_match_all('/\*\s((?!@)[\x{4e00}-\x{9fa5}\S]+)(\r\n|\r|\n)/u', $doc, $descriptions); + $descriptions = $descriptions[1]; + } + + $command = [ + 'id' => $id, + 'call' => [$annotation->class, $annotation->method], + 'descriptions' => $descriptions ?? [], + 'triggers' => [], + ]; + + if (empty($command['descriptions'])) { + Console::warning("命令没有描述信息:{$id}"); + } + + // 可能的触发条件,顺序会影响命令帮助的生成结果 + $possible_triggers = ['match', 'pattern', 'regex', 'start_with', 'end_with', 'keyword', 'alias']; + foreach ($possible_triggers as $trigger) { + if (isset($annotation->{$trigger}) && !empty($annotation->{$trigger})) { + // 部分触发条件可能存在多个 + if (is_iterable($annotation->{$trigger})) { + foreach ($annotation->{$trigger} as $item) { + $command['triggers'][$trigger][] = $item; + } + } else { + $command['triggers'][$trigger][] = $annotation->{$trigger}; + } + } + } + if (empty($command['triggers'])) { + Console::warning("命令没有触发条件:{$id}"); + continue; + } + + $commands[$id] = $command; + } + + $this->save($commands); + return $commands; + } +} diff --git a/src/ZM/Utils/MessageUtil.php b/src/ZM/Utils/MessageUtil.php index 16217602..028d7c5c 100644 --- a/src/ZM/Utils/MessageUtil.php +++ b/src/ZM/Utils/MessageUtil.php @@ -6,8 +6,6 @@ namespace ZM\Utils; use Exception; use Iterator; -use ReflectionException; -use ReflectionMethod; use ZM\Annotation\CQ\CommandArgument; use ZM\Annotation\CQ\CQCommand; use ZM\API\CQ; @@ -19,7 +17,6 @@ use ZM\Event\EventManager; use ZM\Event\EventMapIterator; use ZM\Exception\WaitTimeoutException; use ZM\Requests\ZMRequest; -use ZM\Store\WorkerCache; use ZM\Utils\Manager\WorkerManager; class MessageUtil @@ -258,80 +255,6 @@ class MessageUtil return $str; } - /** - * 根据注解树生成命令列表、帮助 - * - * @return array 帮助信息,每个元素对应一个命令的帮助信息,格式为:命令名(其他触发条件):命令描述 - */ - public static function generateCommandHelp(): array - { - try { - if ($cache = WorkerCache::get('command_help')) { - return $cache; - } - } catch (Exception $e) { - // 不做任何处理,尝试重新生成 - } - $helps = []; - foreach (EventManager::$events[CQCommand::class] as $annotation) { - if ($annotation instanceof CQCommand) { - try { - $reflection = new ReflectionMethod($annotation->class, $annotation->method); - } catch (ReflectionException $e) { - Console::warning('注解解析错误:' . $e->getMessage()); - continue; - } - $doc = $reflection->getDocComment(); - if ($doc) { - // 匹配出不以@开头,且后接中文或任意非空格字符,并以换行符结尾的字符串,也就是命令描述 - preg_match_all('/\*\s((?!@)[\x{4e00}-\x{9fa5}\S]+)(\r\n|\r|\n)/u', $doc, $matches); - // 多行描述用分号分隔 - $help = implode(';', $matches[1]); - if (empty($help)) { - Console::warning('命令 ' . $annotation->class . '::' . $annotation->method . ' 没有描述!'); - $help = '无描述'; - } - } else { - Console::warning('命令 ' . $annotation->class . '::' . $annotation->method . ' 没有描述!'); - $help = '无描述'; - } - - // 可以触发命令的参数 - $possible_keys = [ - 'match' => '%s', - 'pattern' => '符合”%s“', - 'regex' => '匹配“%s”', - 'start_with' => '以”%s“开头', - 'end_with' => '以”%s“结尾', - 'keyword' => '包含“%s”', - 'alias' => '%s', - ]; - $command_seg = []; - foreach ($possible_keys as $key => $help_format) { - // 如果定义了该参数,则添加到帮助信息中 - if (isset($annotation->{$key}) && !empty($annotation->{$key})) { - if (is_iterable($annotation->{$key})) { - foreach ($annotation->{$key} as $item) { - $command_seg[] = sprintf($help_format, $item); - } - } else { - $command_seg[] = sprintf($help_format, $annotation->{$key}); - } - } - } - // 第一个触发参数为主命令名 - $command = array_shift($command_seg); - if (count($command_seg) > 0) { - $command .= '(' . implode(',', $command_seg) . ')'; - } - $helps[] = sprintf('%s:%s', $command, $help); - } - } - // 放到跨进程缓存以供取用 - WorkerCache::set('command_helps', $helps); - return $helps; - } - /** * @throws WaitTimeoutException */ diff --git a/tests/ZM/Utils/MessageUtilTest.php b/tests/ZM/Utils/MessageUtilTest.php index 0b9f17a6..94b91435 100644 --- a/tests/ZM/Utils/MessageUtilTest.php +++ b/tests/ZM/Utils/MessageUtilTest.php @@ -9,6 +9,7 @@ use Throwable; use ZM\Annotation\CQ\CQCommand; use ZM\API\CQ; use ZM\Event\EventManager; +use ZM\Utils\CommandInfoUtil; use ZM\Utils\DataProvider; use ZM\Utils\MessageUtil; @@ -51,8 +52,8 @@ class MessageUtilTest extends TestCase $cmd->class = self::class; $cmd->method = __FUNCTION__; EventManager::addEvent(CQCommand::class, $cmd); - $help = MessageUtil::generateCommandHelp(); - $this->assertEquals('测试命令:无描述', $help[0]); + $help = resolve(CommandInfoUtil::class)->getHelp(self::class . '@' . __FUNCTION__); + $this->assertEquals('测试命令:无描述', $help); } /**