From efda3f44086733d1318f1b19c6c6a75b5d3c930f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 26 Dec 2022 03:19:53 +0800 Subject: [PATCH] enhancement for BotCommand and CommandArgument --- src/Globals/global_defines_app.php | 2 +- src/Globals/global_functions.php | 48 ++ src/ZM/Annotation/AnnotationMap.php | 2 +- src/ZM/Annotation/OneBot/BotCommand.php | 46 +- src/ZM/Annotation/OneBot/CommandArgument.php | 21 +- src/ZM/Context/BotContext.php | 41 +- src/ZM/Exception/WaitTimeoutException.php | 16 + src/ZM/Plugin/OneBot12Adapter.php | 464 ++++++++++++++++-- .../Utils/CodeGenerator/PluginGenerator.php | 2 +- src/ZM/Utils/MessageUtil.php | 49 ++ 10 files changed, 625 insertions(+), 66 deletions(-) create mode 100644 src/ZM/Exception/WaitTimeoutException.php diff --git a/src/Globals/global_defines_app.php b/src/Globals/global_defines_app.php index 95803edd..719229c7 100644 --- a/src/Globals/global_defines_app.php +++ b/src/Globals/global_defines_app.php @@ -15,7 +15,7 @@ const ZM_VERSION_ID = Framework::VERSION_ID; const ZM_VERSION = Framework::VERSION; /** 机器人用的,用于判断二元语意 */ -const TRUE_LIST = ['yes', 'y', 'true', 'on', '是', '对', true]; +const TRUE_LIST = ['yes', 'ok', 'y', 'true', 'on', '是', '对', true]; const FALSE_LIST = ['no', 'n', 'false', 'off', '否', '错', false]; /** 定义多进程的全局变量 */ diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index 01b65dc5..b3c0385f 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -103,6 +103,54 @@ function is_assoc_array(array $array): bool return !empty($array) && array_keys($array) !== range(0, count($array) - 1); } +/** + * 格式匹配 + */ +function match_pattern(string $pattern, string $subject): bool +{ + $pattern = str_replace(['\*', '\\\\.*'], ['.*', '\*'], preg_quote($pattern, '/')); + $pattern = '/^' . $pattern . '$/i'; + return preg_match($pattern, $subject) === 1; +} + +/** + * 匹配参数 + * + * @return array|false 成功时返回匹配到的参数数组,失败时返回false + */ +function match_args(string $pattern, string $subject) +{ + $result = []; + if (match_pattern($pattern, $subject)) { + if (mb_strpos($pattern, '*') === false) { + return []; + } + $exp = explode('*', $pattern); + $i = 0; + foreach ($exp as $k => $v) { + if (empty($v) && $k === 0) { + continue; + } + if (empty($v) && $k === count($exp) - 1) { + $subject .= '^EOL'; + $v = '^EOL'; + } + $cur_var = ''; + $ori = $i; + while (($a = mb_substr($subject, $i, mb_strlen($v))) !== $v && !empty($a)) { + $cur_var .= mb_substr($subject, $i, 1); + ++$i; + } + if ($i !== $ori || $k === 1 || $k === count($exp) - 1) { + $result[] = $cur_var; + } + $i += mb_strlen($v); + } + return $result; + } + return false; +} + /** * 构建消息段的助手函数 * diff --git a/src/ZM/Annotation/AnnotationMap.php b/src/ZM/Annotation/AnnotationMap.php index 6fcf452d..114ff7e7 100644 --- a/src/ZM/Annotation/AnnotationMap.php +++ b/src/ZM/Annotation/AnnotationMap.php @@ -35,7 +35,7 @@ class AnnotationMap public static function loadAnnotationByParser(AnnotationParser $parser): void { // 生成后加入到全局list中 - self::$_list = array_merge(self::$_list, $parser->generateAnnotationList()); + self::$_list = array_merge_recursive(self::$_list, $parser->generateAnnotationList()); self::$_map = $parser->getAnnotationMap(); } diff --git a/src/ZM/Annotation/OneBot/BotCommand.php b/src/ZM/Annotation/OneBot/BotCommand.php index e29c7b9f..ae275472 100644 --- a/src/ZM/Annotation/OneBot/BotCommand.php +++ b/src/ZM/Annotation/OneBot/BotCommand.php @@ -28,34 +28,32 @@ class BotCommand extends AnnotationBase implements Level * @param string[] $alias */ public function __construct( - public $name = '', - public $match = '', - public $pattern = '', - public $regex = '', - public $start_with = '', - public $end_with = '', - public $keyword = '', - public $alias = [], - public $detail_type = '', - public $user_id = '', - public $group_id = '', - public $level = 20 + public string $name = '', + public string $match = '', + public string $pattern = '', + public string $regex = '', + public string $start_with = '', + public string $end_with = '', + public string $keyword = '', + public array $alias = [], + public string $detail_type = '', + public string $prefix = '', + public int $level = 20 ) { } public static function make( - $name = '', - $match = '', - $pattern = '', - $regex = '', - $start_with = '', - $end_with = '', - $keyword = '', - $alias = [], - $message_type = '', - $user_id = '', - $group_id = '', - $level = 20 + string $name = '', + string $match = '', + string $pattern = '', + string $regex = '', + string $start_with = '', + string $end_with = '', + string $keyword = '', + array $alias = [], + string $detail_type = '', + string $prefix = '', + int $level = 20 ): BotCommand { return new static(...func_get_args()); } diff --git a/src/ZM/Annotation/OneBot/CommandArgument.php b/src/ZM/Annotation/OneBot/CommandArgument.php index a43a5f2b..43684644 100644 --- a/src/ZM/Annotation/OneBot/CommandArgument.php +++ b/src/ZM/Annotation/OneBot/CommandArgument.php @@ -23,13 +23,14 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation public string $type = 'string'; /** - * @param string $name 参数名称(可以是中文) - * @param string $description 参数描述(默认为空) - * @param bool $required 参数是否必需,如果是必需,为true(默认为false) - * @param string $prompt 当参数为必需且缺失时,返回给用户的提示输入的消息(默认为"请输入$name") - * @param string $default 当required为false时,未匹配到参数将自动使用default值(默认为空) - * @param int $timeout prompt超时时间(默认为60秒) - * @throws InvalidArgumentException|ZMKnownException + * @param string $name 参数名称(可以是中文) + * @param string $description 参数描述(默认为空) + * @param bool $required 参数是否必需,如果是必需,为true(默认为false) + * @param string $prompt 当参数为必需且缺失时,返回给用户的提示输入的消息(默认为"请输入$name") + * @param null|array|\Closure|float|int|string $default 当required为false时,未匹配到参数将自动使用default值(默认为空) + * @param int $timeout prompt超时时间(默认为60秒) + * @throws InvalidArgumentException + * @throws ZMKnownException */ public function __construct( /** @@ -40,7 +41,7 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation string $type = 'string', public bool $required = false, public string $prompt = '', - public string $default = '', + public string|\Closure|array|int|float|null $default = '', public int $timeout = 60, public int $error_prompt_policy = 1 ) { @@ -97,6 +98,10 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation if (array_key_exists($type, $table)) { return $table[$type]; } + // 说明是自定义类型 + if (mb_strpos($type, '.') === 0 && mb_strlen($type) >= 2) { + return $type; + } throw new ZMKnownException(zm_internal_errcode('E00077') . 'Invalid argument type: ' . $type . ', only support any, string, number and bool !'); } } diff --git a/src/ZM/Context/BotContext.php b/src/ZM/Context/BotContext.php index 1df911c1..3502dbd7 100644 --- a/src/ZM/Context/BotContext.php +++ b/src/ZM/Context/BotContext.php @@ -24,6 +24,10 @@ class BotContext implements ContextInterface private array $self; + private array $params = []; + + private bool $replied = false; + public function __construct(string $bot_id, string $platform) { $this->self = ['user_id' => $bot_id, 'platform' => $platform]; @@ -55,6 +59,7 @@ class BotContext implements ContextInterface throw new OneBot12Exception('bot()->reply() can only be used in message event.'); } $msg = (is_string($message) ? [new MessageSegment('text', ['text' => $message])] : ($message instanceof MessageSegment ? [$message] : $message)); + $this->replied = true; return $this->sendMessage($msg, $event->detail_type, $event->jsonSerialize()); } throw new OneBot12Exception('bot()->reply() can only be used in message event.'); @@ -65,8 +70,7 @@ class BotContext implements ContextInterface */ public function hasReplied(): bool { - // TODO: 完成是否已经回复的记录和返回 - return false; + return $this->replied; } /** @@ -93,6 +97,39 @@ class BotContext implements ContextInterface return $this->sendAction(Utils::camelToSeparator(__FUNCTION__), $params, $this->self); } + /** + * 设置该消息下解析出来的参数列表 + * + * @param array $params 参数列表 + */ + public function setParams(array $params): void + { + $this->params = $params; + } + + /** + * 获取单个参数值,不存在则返回 null + * + * @param int|string $name 参数名称或索引 + */ + public function getParam(string|int $name): mixed + { + return $this->params[$name] ?? null; + } + + public function getParamString(string|int $name): ?string + { + return MessageUtil::getAltMessage($this->params[$name] ?? null); + } + + /** + * 获取所有参数 + */ + public function getParams(): array + { + return $this->params; + } + public function getEchoAction(mixed $echo): ?Action { return self::$echo_id_list[$echo] ?? null; diff --git a/src/ZM/Exception/WaitTimeoutException.php b/src/ZM/Exception/WaitTimeoutException.php new file mode 100644 index 00000000..6c0595a5 --- /dev/null +++ b/src/ZM/Exception/WaitTimeoutException.php @@ -0,0 +1,16 @@ +module = $module; + } +} diff --git a/src/ZM/Plugin/OneBot12Adapter.php b/src/ZM/Plugin/OneBot12Adapter.php index 30657007..81dc28fa 100644 --- a/src/ZM/Plugin/OneBot12Adapter.php +++ b/src/ZM/Plugin/OneBot12Adapter.php @@ -10,8 +10,11 @@ use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent; use OneBot\V12\Exception\OneBotException; use OneBot\V12\Object\ActionResponse; +use OneBot\V12\Object\MessageSegment; use OneBot\V12\Object\OneBotEvent; use OneBot\V12\Validator; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use ZM\Annotation\AnnotationHandler; use ZM\Annotation\AnnotationMap; use ZM\Annotation\AnnotationParser; @@ -21,10 +24,24 @@ use ZM\Annotation\OneBot\BotEvent; use ZM\Annotation\OneBot\CommandArgument; use ZM\Container\ContainerRegistrant; use ZM\Context\BotContext; +use ZM\Exception\InterruptException; +use ZM\Exception\OneBot12Exception; +use ZM\Exception\WaitTimeoutException; use ZM\Utils\ConnectionUtil; +use ZM\Utils\MessageUtil; class OneBot12Adapter extends ZMPlugin { + /** + * 缓存待询问参数的队列 + * + * 0: 代表 OneBotEvent 对象,即用于判断是否为同一会话环境 + * 1: \Generator 生成器,协程,不多讲 + * 2: BotCommand 注解对象 + * 3: match_result(array)匹配到一半的结果 + */ + private static array $prompt_queue = []; + public function __construct(string $submodule = '', ?AnnotationParser $parser = null) { parent::__construct(__DIR__); @@ -35,8 +52,9 @@ class OneBot12Adapter extends ZMPlugin $this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseOpen']); $this->addEvent(WebSocketMessageEvent::class, [$this, 'handleWSReverseMessage']); // 在 BotEvent 内处理 BotCommand - // $cmd_event = BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand']); - // $this->addBotEvent($cmd_event); + $this->addBotEvent(BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand'])); + // 在 BotEvent 内处理需要等待回复的 CommandArgument + $this->addBotEvent(BotEvent::make(type: 'message', level: 20)->on([$this, 'handlePrompt'])); // 处理和声明所有 BotCommand 下的 CommandArgument $parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']); // 不需要给列表写入 CommandArgument @@ -77,30 +95,98 @@ class OneBot12Adapter extends ZMPlugin } /** - * 调用 BotCommand 注解的方法 + * [CALLBACK] 调用 BotCommand 注解的方法 * - * @param BotEvent $event BotEvent 事件 - * @param BotContext $ctx 机器人环境上下文 + * @param BotContext $ctx 机器人环境上下文 + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws \Throwable + * @throws OneBot12Exception */ - public function handleBotCommand(BotEvent $event, BotContext $ctx) + public function handleBotCommand(BotContext $ctx) { - $handler = new AnnotationHandler(BotCommand::class); - $handler->setReturnCallback(function ($result) use ($ctx) { - if (is_string($result)) { - $ctx->reply($result); - return; - } - try { - Validator::validateMessageSegment($result); - $ctx->reply($result); - } catch (\Throwable) { - } - if ($ctx->hasReplied()) { - AnnotationHandler::interrupt(); - } - }); + if ($ctx->hasReplied()) { + return; + } // 匹配消息 $match_result = $this->matchBotCommand($ctx->getEvent()); + if ($match_result === null) { + return; + } + // 匹配成功,检查 CommandArguments + /** @var BotCommand $command */ + $command = $match_result[0]; + $arguments = $this->matchCommandArguments($match_result[1], $command); + // 返回的是生成器,说明有需要询问的参数 + if ($arguments instanceof \Generator) { + /** @var null|CommandArgument $argument */ + $argument = $arguments->current(); + if ($argument === null) { + // 是 null 表明返回了空生成器,说明参数都已经匹配完毕 + $ctx->setParams($arguments->getReturn()); + $this->callBotCommand($ctx, $command); + return; + } + $message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt); + $ctx->reply([$message]); + // 然后将此事件放入等待队列 + self::$prompt_queue[] = [$ctx->getEvent(), $arguments, $command, $match_result]; + return; + } + $ctx->setParams($arguments); + // 调用方法 + $this->callBotCommand($ctx, $command); + } + + /** + * [CALLBACK] 处理需要等待回复的 CommandArgument + * + * @param BotContext $ctx 机器人上下文 + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws OneBot12Exception + * @throws \Throwable + */ + public function handlePrompt(BotContext $ctx) + { + // 需要先从队列里找到定义当前会话的 prompt + // 定义一个会话的标准是:事件的 detail_type,user_id,[group_id],[guild_id,channel_id] 全部相同 + $new_event = $ctx->getEvent(); + foreach (self::$prompt_queue as $k => $v) { + /** @var OneBotEvent $old_event */ + $old_event = $v[0]; + if ($old_event->detail_type !== $new_event->detail_type) { + continue; + } + $matched = match ($old_event->detail_type) { + 'private' => $new_event->getUserId() === $old_event->getUserId(), + 'group' => $new_event->getGroupId() === $old_event->getGroupId(), + 'guild' => $new_event->getGuildId() === $old_event->getGuildId() && $new_event->getChannelId() === $old_event->getChannelId(), + default => false, + }; + if (!$matched) { + continue; + } + array_splice(self::$prompt_queue, $k, 1); + // 找到了,开始处理 + /** @var \Generator $arguments */ + $arguments = $v[1]; + $new_arguments = $arguments->send($new_event->getMessage()); + if ($new_arguments !== null || $arguments->valid()) { + // 还有需要询问的参数 + /** @var CommandArgument $argument */ + $argument = $arguments->current(); + $message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt); + $ctx->reply([$message]); + // 然后将此事件放入等待队列 + self::$prompt_queue[] = [$ctx->getEvent(), $arguments, $v[2], $v[3]]; + } else { + // 所有参数都已经获取到了,调用方法 + $ctx->setParams($arguments->getReturn()); + $this->callBotCommand($ctx, $v[2]); + } + return; + } } /** @@ -118,8 +204,10 @@ class OneBot12Adapter extends ZMPlugin } /** - * 接入和认证反向 WS 的连接 + * [CALLBACK] 接入和认证反向 WS 的连接 + * * @throws StopException + * @throws \JsonException */ public function handleWSReverseOpen(WebSocketOpenEvent $event): void { @@ -151,7 +239,7 @@ class OneBot12Adapter extends ZMPlugin } /** - * 处理 WebSocket 消息 + * [CALLBACK] 处理 WebSocket 消息 * * @param WebSocketMessageEvent $event 事件对象 * @throws OneBotException @@ -177,7 +265,7 @@ class OneBot12Adapter extends ZMPlugin // 如果含有 type,detail_type 字段,表明是 event try { $obj = new OneBotEvent($body); - } catch (OneBotException $e) { + } catch (OneBotException) { logger()->debug('收到非 OneBot 12 标准的消息,已忽略'); return; } @@ -192,7 +280,11 @@ class OneBot12Adapter extends ZMPlugin && ($event->sub_type === null || $event->sub_type === $obj->sub_type) && ($event->detail_type === null || $event->detail_type === $obj->detail_type); }); - $handler->handleAll($obj); + try { + $handler->handleAll($obj); + } catch (WaitTimeoutException $e) { + bot()->reply($e->getMessage()); + } } elseif (isset($body['status'], $body['retcode'])) { // 如果含有 status,retcode 字段,表明是 action 的 response $resp = new ActionResponse(); @@ -212,11 +304,325 @@ class OneBot12Adapter extends ZMPlugin } } - private function matchBotCommand(OneBotEvent $event): array + /** + * 根据事件匹配规则 + * + * @param OneBotEvent $event 事件对象 + */ + public function matchBotCommand(OneBotEvent $event): ?array { + /** @var BotCommand[] $ls */ $ls = AnnotationMap::$_list[BotCommand::class] ?? []; - $msg = $event->getMessageString(); - // TODO: 还没写完匹配 BotCommand - return []; + $msgs = $event->getMessage(); + $head = ''; + $cmd_explode = []; + $full_str = ''; + foreach ($msgs as $segment) { + /** @param \MessageSegment $segment */ + if ($segment->type !== 'text') { + if ($head === '') { + continue; + } + $cmd_explode[] = $segment; + continue; + } + // 当没识别到命令头的时候,就当作命令头识别 + if ($head === '') { + $text = $segment->data['text']; + $full_str .= $text; + // 切分字符串 + $nlp = MessageUtil::splitMessage(str_replace("\r", '', $text)); + // 啥也没有,分个锤子 + if (empty($nlp)) { + continue; + } + // 先预留一个给分组而配置的空间 + $cmd_explode = $nlp; + $head = $nlp[0]; + } else { + $full_str .= $segment->data['text']; + // 如果已经识别到了命令头,就当作命令体识别 + $nlp = MessageUtil::splitMessage(str_replace("\r", '', $segment->data['text'])); + if (empty($nlp)) { + continue; + } + $cmd_explode = array_merge($cmd_explode, $nlp); + } + // 先匹配 + } + if ($head === '') { + return null; + } + // 遍历所有 BotCommand 注解 + foreach ($ls as $v) { + /** @var BotCommand $v */ + // 测试 deatil_type + if ($v->detail_type !== '' && $v->detail_type !== $event->detail_type) { + continue; + } + // 测试 prefix + if ($v->prefix !== '' && mb_strpos($full_str, $v->prefix) !== 0) { + continue; + } + // 测试 match + if ($v->match !== '' && $v->match === $head) { + array_shift($cmd_explode); + return [$v, $cmd_explode, $full_str]; + } + // 测试 alias + if ($v->match !== '' && $v->alias !== [] && in_array($head, $v->alias, true)) { + array_shift($cmd_explode); + return [$v, $cmd_explode, $full_str]; + } + // 测试 pattern + if ($v->pattern !== '' && ($args = match_args($v->pattern, $full_str)) !== false) { + return [$v, $args, $full_str]; + } + // 测试 regex + if ($v->regex !== '' && preg_match('/' . $v->regex . '/u', $full_str, $match) !== 0) { + return [$v, $match, $full_str]; + } + // 测试 start_with + if ($v->start_with !== '' && mb_strpos($full_str, $v->start_with) === 0) { + return [$v, [mb_substr($full_str, mb_strlen($v->start_with))], $full_str]; + } + // 测试 end_with + if ($v->end_with !== '' && mb_substr($full_str, 0 - mb_strlen($v->end_with)) === $v->end_with) { + return [$v, [mb_substr($full_str, 0, 0 - mb_strlen($v->end_with))], $full_str]; + } + // 测试 keyword + if ($v->keyword !== '' && mb_strpos($full_str, $v->keyword) !== false) { + return [$v, explode($v->keyword, $full_str), $full_str]; + } + } + + return null; + } + + /** + * 根据匹配结果和 CommandArgument 进行匹配 + * + * @param array $match_result 匹配结果的引用 + * @param BotCommand $cmd 注解对象 + * @return array|\Generator 返回 array 时为匹配结果,返回 Generator 时为等待结果 + * @throws WaitTimeoutException + */ + private function matchCommandArguments(array $match_result, BotCommand $cmd): array|\Generator + { + $arguments = []; + /** @var CommandArgument $argument */ + foreach ($cmd->getArguments() as $argument) { + switch ($argument->type) { + case 'string': + case 'any': + case 'str': + $cnt = count($match_result); + for ($k = 0; $k < $cnt; ++$k) { + $v = $match_result[$k]; + if (is_string($v)) { + array_splice($match_result, $k, 1); + $arguments[$argument->name] = $v; + break 2; + } + } + if ($argument->required) { + // 不够用,且是必需的,就询问用户(这里可能还需要考虑没有协程环境怎么处理) + $g = yield $argument; + foreach ($g as $v) { + if ($v instanceof MessageSegment && $v->type === 'text') { + $arguments[$argument->name] = $v->data['text']; + break 2; + } + if (is_string($v)) { + $arguments[$argument->name] = $v; + break 2; + } + } + if ($argument->error_prompt_policy === 1) { + $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt; + $clone_argument = clone $argument; + $clone_argument->prompt = $prompt; + $g = yield $clone_argument; + foreach ($g as $v) { + if ($v instanceof MessageSegment && $v->type === 'text') { + $arguments[$argument->name] = $v->data['text']; + break 2; + } + if (is_string($v)) { + $arguments[$argument->name] = $v; + break 2; + } + } + } + throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt()); + } else { + // 非必需,就使用缺省值 + $arguments[$argument->name] = $argument->default; + } + break; + case 'number': + $cnt = count($match_result); + // 遍历现存的参数列表,找到第一个数字 + for ($k = 0; $k < $cnt; ++$k) { + $v = $match_result[$k]; + if (is_numeric($v)) { + array_splice($match_result, $k, 1); + $arguments[$argument->name] = $v / 1; + break 2; + } + } + // 找不到就看看是不是必需的,如果不是必需的,且缺省值是数字,那么就顶上 + if (!$argument->required && is_numeric($argument->default)) { + $arguments[$argument->name] = $argument->default / 1; + break; + } + // 到这里还没找到,就说明需要询问用户了 + $g = yield $argument; + foreach ($g as $v) { + if (is_numeric($v)) { + $arguments[$argument->name] = $v / 1; + break 2; + } + if ($v instanceof MessageSegment && $v->type === 'text') { + if (is_numeric($v->data['text'])) { + $arguments[$argument->name] = $v->data['text'] / 1; + break 2; + } + } + } + if ($argument->error_prompt_policy === 1) { + $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt; + $clone_argument = clone $argument; + $clone_argument->prompt = $prompt; + $g = yield $clone_argument; + foreach ($g as $v) { + if (is_numeric($v)) { + $arguments[$argument->name] = $v / 1; + break 2; + } + if ($v instanceof MessageSegment && $v->type === 'text') { + if (is_numeric($v->data['text'])) { + $arguments[$argument->name] = $v->data['text'] / 1; + break 2; + } + } + } + } + throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt()); + case 'bool': + // 先遍历参数,找到具有布尔值参数的语言 + $cnt = count($match_result); + for ($k = 0; $k < $cnt; ++$k) { + $v = $match_result[$k]; + // 看看有没有true值 + if (in_array(strtolower($v), TRUE_LIST, true)) { + array_splice($match_result, $k, 1); + $arguments[$argument->name] = true; + break 2; + } + // 看看有没有false值 + if (in_array(strtolower($v), FALSE_LIST, true)) { + array_splice($match_result, $k, 1); + $arguments[$argument->name] = false; + break 2; + } + } + // 如果不是必需的,那就使用缺省值 + if (!$argument->required) { + $arguments[$argument->name] = in_array($argument->default === '' ? true : 'true', TRUE_LIST); + break; + } + // 到这里还没找到,就说明需要询问用户了 + $g = yield $argument; + if (in_array(strtolower($g), TRUE_LIST, true)) { + $arguments[$argument->name] = true; + } elseif (in_array(strtolower($g), FALSE_LIST, true)) { + $arguments[$argument->name] = false; + } else { + if ($argument->error_prompt_policy === 1) { + $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt; + $clone_argument = clone $argument; + $clone_argument->prompt = $prompt; + $g = yield $clone_argument; + if (in_array(strtolower($g), TRUE_LIST, true)) { + $arguments[$argument->name] = true; + } elseif (in_array(strtolower($g), FALSE_LIST, true)) { + $arguments[$argument->name] = false; + } else { + throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt()); + } + } else { + throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt()); + } + } + break; + default: + // 其他类型,处理富文本 + $msg_type = mb_substr($argument->type, 1); + $cnt = count($match_result); + for ($k = 0; $k < $cnt; ++$k) { + $v = $match_result[$k]; + if ($v instanceof MessageSegment && $v->type === $msg_type) { + array_splice($match_result, $k, 1); + $arguments[$argument->name] = $v; + break 2; + } + } + // 如果不是必需的,那就使用缺省值 + if (!$argument->required && is_array($argument->default)) { + // 生成一个消息段的段 + $a = [new MessageSegment($msg_type, $argument->default)]; + $arguments[$argument->name] = $a; + break; + } + // 到这里还没找到,就说明需要询问用户了 + $g = yield $argument; + foreach ($g as $v) { + if ($v instanceof MessageSegment && $v->type === $msg_type) { + $arguments[$argument->name] = $v; + break 2; + } + } + if ($argument->error_prompt_policy === 1) { + $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt; + $clone_argument = clone $argument; + $clone_argument->prompt = $prompt; + $g = yield $clone_argument; + foreach ($g as $v) { + if ($v instanceof MessageSegment && $v->type === $msg_type) { + $arguments[$argument->name] = $v; + break 2; + } + } + } + throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt()); + } + } + $arguments['.unnamed'] = $match_result; + return $arguments; + } + + /** + * @throws InterruptException + * @throws \Throwable + */ + private function callBotCommand(BotContext $ctx, BotCommand $cmd) + { + $handler = new AnnotationHandler(BotCommand::class); + $handler->setReturnCallback(function ($result) use ($ctx) { + if (is_string($result)) { + $ctx->reply($result); + return; + } + try { + Validator::validateMessageSegment($result); + $ctx->reply($result); + } catch (\Throwable) { + } + if ($ctx->hasReplied()) { + AnnotationHandler::interrupt(); + } + }); + $handler->handle($cmd); } } diff --git a/src/ZM/Utils/CodeGenerator/PluginGenerator.php b/src/ZM/Utils/CodeGenerator/PluginGenerator.php index 28e16c9c..6fd34ea5 100644 --- a/src/ZM/Utils/CodeGenerator/PluginGenerator.php +++ b/src/ZM/Utils/CodeGenerator/PluginGenerator.php @@ -67,7 +67,7 @@ class PluginGenerator '{class}' => $this->convertClassName(), ]; $main_php = str_replace(array_keys($replace), array_values($replace), $template); - file_put_contents(zm_dir($plugin_base_dir . '/src/PluginMain.php'), $main_php); + file_put_contents(zm_dir($plugin_base_dir . '/src/' . $this->convertClassName() . '.php'), $main_php); // 写入 composer.json $composer_json = [ 'autoload' => [ diff --git a/src/ZM/Utils/MessageUtil.php b/src/ZM/Utils/MessageUtil.php index 72aab1f1..5214b9de 100644 --- a/src/ZM/Utils/MessageUtil.php +++ b/src/ZM/Utils/MessageUtil.php @@ -64,6 +64,11 @@ class MessageUtil public static function convertToArr(MessageSegment|\Stringable|array|string $message) { if (is_array($message)) { + foreach ($message as $k => $v) { + if (is_string($v)) { + $message[$k] = new MessageSegment('text', ['text' => $v]); + } + } return $message; } if ($message instanceof MessageSegment) { @@ -74,4 +79,48 @@ class MessageUtil } return new MessageSegment('text', ['text' => $message]); } + + /** + * 分割消息字符串 + * + * @param array $includes 需要进行切割的字符串,默认包含空格及制表符(\t) + */ + public static function splitMessage(string $msg, array $includes = [' ', "\t"]): array + { + $msg = trim($msg); + foreach ($includes as $v) { + $msg = str_replace($v, "\n", $msg); + } + $msg_seg = explode("\n", $msg); + $ls = []; + foreach ($msg_seg as $v) { + if (empty(trim($v))) { + continue; + } + $ls[] = trim($v); + } + return $ls; + } + + public static function getAltMessage(null|array|string|MessageSegment $message): string + { + if ($message === null) { + return ''; + } + if (is_string($message)) { + return $message; + } + if ($message instanceof MessageSegment) { + $message = [$message]; + } + $message_string = ''; + foreach ($message as $segment) { + if ($segment->type === 'text') { + $message_string .= $segment->data['text']; + } else { + $message_string .= '[富文本:' . $segment->type . ']'; + } + } + return $message_string; + } }