diff --git a/src/Globals/global_defines_app.php b/src/Globals/global_defines_app.php index 719229c7..6f70fe2f 100644 --- a/src/Globals/global_defines_app.php +++ b/src/Globals/global_defines_app.php @@ -31,6 +31,11 @@ const ZM_ERR_METHOD_NOT_FOUND = 1; // 找不到方法 const ZM_ERR_ROUTE_NOT_FOUND = 2; // 找不到路由 const ZM_ERR_ROUTE_METHOD_NOT_ALLOWED = 3; // 路由方法不允许 +/** 定义 BotContext 下 reply 回复的模式 */ +const ZM_REPLY_NONE = 0; // 默认回复,不带任何东西 +const ZM_REPLY_MENTION = 1; // 回复时 @ 该用户 +const ZM_REPLY_QUOTE = 2; // 回复时引用该消息 + const LOAD_MODE_VENDOR = 0; // 从 vendor 加载 const LOAD_MODE_SRC = 1; // 从 src 加载 diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index fd4bffdc..432b3c3b 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -15,6 +15,7 @@ use ZM\Store\Database\DBException; use ZM\Store\Database\DBQueryBuilder; use ZM\Store\Database\DBWrapper; use ZM\Store\KV\KVInterface; +use ZM\Store\KV\Redis\RedisWrapper; // 防止重复引用引发报错 if (function_exists('zm_internal_errcode')) { @@ -216,6 +217,16 @@ function sql_builder(string $name = ''): DBQueryBuilder return (new DBWrapper($name))->createQueryBuilder(); } +/** + * 获取 Redis 操作类 + * + * @param string $name 使用的 Redis 连接名称 + */ +function redis(string $name = 'default'): RedisWrapper +{ + return new RedisWrapper($name); +} + /** * 获取 / 设置配置项 * diff --git a/src/ZM/Context/BotContext.php b/src/ZM/Context/BotContext.php index c317d77c..faa0313c 100644 --- a/src/ZM/Context/BotContext.php +++ b/src/ZM/Context/BotContext.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace ZM\Context; +use DI\DependencyException; +use DI\NotFoundException; use OneBot\Driver\Event\Http\HttpRequestEvent; use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; -use OneBot\V12\Object\Action; use OneBot\V12\Object\MessageSegment; use OneBot\V12\Object\OneBotEvent; use ZM\Context\Trait\BotActionTrait; @@ -17,14 +18,16 @@ class BotContext implements ContextInterface { use BotActionTrait; + /** @var array> 记录机器人的上下文列表 */ private static array $bots = []; - private static array $echo_id_list = []; - + /** @var string[] 记录当前上下文绑定的机器人 */ private array $self; + /** @var array 如果是 BotCommand 匹配的上下文,这里会存放匹配到的参数 */ private array $params = []; + /** @var bool 用于标记当前上下文会话是否已经调用过 reply() 方法 */ private bool $replied = false; public function __construct(string $bot_id, string $platform, null|WebSocketMessageEvent|HttpRequestEvent $event = null) @@ -34,6 +37,12 @@ class BotContext implements ContextInterface $this->base_event = $event; } + /** + * 获取机器人事件对象 + * + * @throws DependencyException + * @throws NotFoundException + */ public function getEvent(): OneBotEvent { return container()->get('bot.event'); @@ -42,9 +51,10 @@ class BotContext implements ContextInterface /** * 快速回复机器人消息文本 * - * @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组 + * @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组 + * @param int $reply_mode 回复消息模式,默认为空,可选 ZM_REPLY_MENTION(at 用户)、ZM_REPLY_QUOTE(引用消息) */ - public function reply(\Stringable|MessageSegment|array|string $message) + public function reply(\Stringable|MessageSegment|array|string $message, int $reply_mode = ZM_REPLY_NONE) { if (container()->has('bot.event')) { // 这里直接使用当前上下文的事件里面的参数,不再重新挨个获取怎么发消息的参数 @@ -52,11 +62,13 @@ class BotContext implements ContextInterface $event = container()->get('bot.event'); // reply 的条件是必须 type=message - if ($event->getType() !== 'message') { + if ($event->type !== 'message') { 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; + // 判断规则 + $this->matchReplyMode($reply_mode, $msg, $event); return $this->sendMessage($msg, $event->detail_type, $event->jsonSerialize()); } throw new OneBot12Exception('bot()->reply() can only be used in message event.'); @@ -126,13 +138,25 @@ class BotContext implements ContextInterface return $this->params; } - public function getEchoAction(mixed $echo): ?Action - { - return self::$echo_id_list[$echo] ?? null; - } - public function getSelf(): array { return $this->self; } + + /** + * 匹配更改 reply 的特殊模式 + * + * @param int $reply_mode 回复模式 + * @param array $message_segments 消息段的引用 + * @param OneBotEvent $event 事件对象 + */ + private function matchReplyMode(int $reply_mode, array &$message_segments, OneBotEvent $event) + { + if (($reply_mode & ZM_REPLY_QUOTE) === ZM_REPLY_QUOTE) { + array_unshift($message_segments, new MessageSegment('reply', ['message_id' => $event->getMessageId(), 'user_id' => $event->getUserId()])); + } + if (($reply_mode & ZM_REPLY_MENTION) === ZM_REPLY_MENTION) { + array_unshift($message_segments, new MessageSegment('mention', ['user_id' => $event->getUserId()])); + } + } } diff --git a/src/ZM/Context/Trait/BotActionTrait.php b/src/ZM/Context/Trait/BotActionTrait.php index f8db6c0e..b39db513 100644 --- a/src/ZM/Context/Trait/BotActionTrait.php +++ b/src/ZM/Context/Trait/BotActionTrait.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace ZM\Context\Trait; use Choir\Http\HttpFactory; +use OneBot\Driver\Coroutine\Adaptive; use OneBot\Driver\Event\Http\HttpRequestEvent; use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\Util\Utils; @@ -18,9 +19,28 @@ use ZM\Utils\MessageUtil; trait BotActionTrait { + /** + * @var array 一个记录 echo 对应协程 ID 的列表,用于恢复协程 + */ + private static array $coroutine_list = []; + private null|WebSocketMessageEvent|HttpRequestEvent $base_event; /** + * @internal 只允许内部调用 + * @param ActionResponse $response 尝试调用看看有没有协程等待的 + */ + public static function tryResume(ActionResponse $response): void + { + if (($co = Adaptive::getCoroutine()) !== null && isset(self::$coroutine_list[$response->echo ?? ''])) { + $co->resume(self::$coroutine_list[$response->echo ?? ''], $response); + } + } + + /** + * 发送一条机器人消息 + * + * @param array|MessageSegment|string|\Stringable $message 消息内容,可以是消息段、字符串 * @throws \Throwable */ public function sendMessage(\Stringable|array|MessageSegment|string $message, string $detail_type, array $params = []): ActionResponse|bool @@ -40,7 +60,6 @@ trait BotActionTrait { // 声明 Action 对象 $a = new Action($action, $params, ob_uuidgen(), $self); - self::$echo_id_list[$a->echo] = $a; // 调用事件在回复之前的回调 $handler = new AnnotationHandler(BotAction::class); container()->set(Action::class, $a); @@ -51,7 +70,7 @@ trait BotActionTrait return false; } - // 调用机器人连接发送 Action + // 调用机器人连接发送 Action,首先试试看是不是 WebSocket if ($this->base_event instanceof WebSocketMessageEvent) { $result = $this->base_event->send(json_encode($a->jsonSerialize())); } @@ -69,13 +88,19 @@ trait BotActionTrait container()->get('http.request.event')->withResponse($response); $result = true; } + // 如果开启了协程,并且成功发送,那就进入协程等待,挂起等待结果返回一个 ActionResponse 对象 + if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) { + self::$coroutine_list[$a->echo] = $co->getCid(); + $response = $co->suspend(); + if ($response instanceof ActionResponse) { + return $response; + } + return false; + } if (isset($result)) { return $result; } - /* TODO: 协程支持 - if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) { - return $result ?? false; - }*/ + // 到这里表明你调用时候不在 WS 或 HTTP 上下文 throw new OneBot12Exception('No bot connection found.'); } } diff --git a/src/ZM/Plugin/OneBot12Adapter.php b/src/ZM/Plugin/OneBot12Adapter.php index 578be6a4..d353d55e 100644 --- a/src/ZM/Plugin/OneBot12Adapter.php +++ b/src/ZM/Plugin/OneBot12Adapter.php @@ -283,7 +283,8 @@ class OneBot12Adapter extends ZMPlugin $resp->retcode = $body['retcode']; $resp->status = $body['status']; $resp->message = $body['message'] ?? ''; - $resp->data = $body['data'] ?? null; + $resp->data = $body['data'] ?? []; + $resp->echo = $body['echo'] ?? null; ContainerRegistrant::registerOBActionResponseServices($resp); @@ -293,6 +294,9 @@ class OneBot12Adapter extends ZMPlugin return $event->retcode === null || $event->retcode === $resp->retcode; }); $handler->handleAll($resp); + + // 如果有协程,并且该 echo 记录在案的话,就恢复协程 + BotContext::tryResume($resp); } } diff --git a/src/ZM/Store/KV/Redis/RedisWrapper.php b/src/ZM/Store/KV/Redis/RedisWrapper.php new file mode 100644 index 00000000..9a55ce93 --- /dev/null +++ b/src/ZM/Store/KV/Redis/RedisWrapper.php @@ -0,0 +1,258 @@ +pool)->get(); + if (method_exists($pool, $name)) { + $result = $pool->{$name}(...$arguments); + } + RedisPool::pool($this->pool)->put($pool); + return $result ?? false; + } +}