diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index 34627b95..b3f37ec2 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -279,12 +279,14 @@ function config(array|string $key = null, mixed $default = null) return $config->get($key, $default); } -function bot(): ZM\Context\BotContext +function bot(string $bot_id = '', string $platform = ''): ZM\Context\BotContext { - if (container()->has(ZM\Context\BotContext::class)) { - return container()->get(ZM\Context\BotContext::class); - } - return new \ZM\Context\BotContext('', ''); + return BotMap::getBotContext($bot_id, $platform); +} + +function bot_connect(int $flag, int $fd) +{ + return BotMap::getConnectContext($flag, $fd); } /** @@ -297,7 +299,7 @@ function kv(string $name = ''): Psr\SimpleCache\CacheInterface { global $kv_class; if (!$kv_class) { - $kv_class = config('global.kv.use', \LightCache::class); + $kv_class = config('global.kv.use', LightCache::class); } /* @phpstan-ignore-next-line */ return is_a($kv_class, KVInterface::class, true) ? $kv_class::open($name) : new $kv_class($name); diff --git a/src/ZM/Container/ContainerRegistrant.php b/src/ZM/Container/ContainerRegistrant.php index 1a630cad..ab2161a8 100644 --- a/src/ZM/Container/ContainerRegistrant.php +++ b/src/ZM/Container/ContainerRegistrant.php @@ -27,6 +27,9 @@ class ContainerRegistrant 'bot.event' => DI\get(OneBotEvent::class), ]); + // 不用依赖注入可能会更好一点,而且方便其他开发者排查问题(因为挺多开发者在非机器人事件里面用 bot() 的,会让依赖注入报错,而且他们自己也看不懂 + // 而且我想让 BotContext 对象成为无状态无数据的对象,一切东西都从 container 和 BotMap 获取,它就是用作调用方法而已 + /* if (isset($event->self['platform'])) { self::addServices([ BotContext::class => DI\autowire($bot_context)->constructor( @@ -35,6 +38,7 @@ class ContainerRegistrant ), ]); } + */ } /** diff --git a/src/ZM/Context/BotConnectContext.php b/src/ZM/Context/BotConnectContext.php new file mode 100644 index 00000000..a7bf5baf --- /dev/null +++ b/src/ZM/Context/BotConnectContext.php @@ -0,0 +1,31 @@ +fd; + } + + public function getFlag(): int + { + return $this->flag; + } +} diff --git a/src/ZM/Context/BotContext.php b/src/ZM/Context/BotContext.php index bfefd094..304e3b7c 100644 --- a/src/ZM/Context/BotContext.php +++ b/src/ZM/Context/BotContext.php @@ -7,15 +7,13 @@ namespace ZM\Context; use DI\DependencyException; use DI\NotFoundException; use OneBot\Driver\Coroutine\Adaptive; -use OneBot\Driver\Event\Http\HttpRequestEvent; -use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\V12\Object\ActionResponse; use OneBot\V12\Object\MessageSegment; use OneBot\V12\Object\OneBotEvent; use ZM\Context\Trait\BotActionTrait; use ZM\Exception\OneBot12Exception; use ZM\Exception\WaitTimeoutException; -use ZM\Plugin\OneBot12Adapter; +use ZM\Plugin\OneBot\OneBot12Adapter; use ZM\Schedule\Timer; use ZM\Utils\MessageUtil; @@ -26,7 +24,7 @@ class BotContext implements ContextInterface /** @var array> 记录机器人的上下文列表 */ private static array $bots = []; - /** @var string[] 记录当前上下文绑定的机器人 */ + /** @var null|string[] 记录当前上下文绑定的机器人 */ private array $self; /** @var array 如果是 BotCommand 匹配的上下文,这里会存放匹配到的参数 */ @@ -35,11 +33,9 @@ class BotContext implements ContextInterface /** @var bool 用于标记当前上下文会话是否已经调用过 reply() 方法 */ private bool $replied = false; - public function __construct(string $bot_id, string $platform, null|WebSocketMessageEvent|HttpRequestEvent $event = null) + public function __construct(string $bot_id, string $platform) { $this->self = ['user_id' => $bot_id, 'platform' => $platform]; - self::$bots[$bot_id][$platform] = $this; - $this->base_event = $event; } /** diff --git a/src/ZM/Context/Trait/BotActionTrait.php b/src/ZM/Context/Trait/BotActionTrait.php index d6a374b1..2b5298ea 100644 --- a/src/ZM/Context/Trait/BotActionTrait.php +++ b/src/ZM/Context/Trait/BotActionTrait.php @@ -4,36 +4,28 @@ 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; use OneBot\V12\Object\Action; use OneBot\V12\Object\ActionResponse; use OneBot\V12\Object\MessageSegment; use ZM\Annotation\AnnotationHandler; use ZM\Annotation\OneBot\BotAction; +use ZM\Context\BotConnectContext; use ZM\Exception\OneBot12Exception; +use ZM\Plugin\OneBot\BotMap; use ZM\Utils\MessageUtil; trait BotActionTrait { - /** - * @var array 一个记录 echo 对应协程 ID 的列表,用于恢复协程 - */ - protected static array $coroutine_list = []; - - protected null|WebSocketMessageEvent|HttpRequestEvent $base_event; - /** * @internal 只允许内部调用 * @param ActionResponse $response 尝试调用看看有没有协程等待的 */ public static function tryResume(ActionResponse $response): void { - if (($co = Adaptive::getCoroutine()) !== null && isset(static::$coroutine_list[$response->echo ?? ''])) { - $co->resume(static::$coroutine_list[$response->echo ?? ''], $response); + if (($co = Adaptive::getCoroutine()) !== null && isset(BotMap::$bot_coroutines[$response->echo ?? ''])) { + $co->resume(BotMap::$bot_coroutines[$response->echo ?? ''], $response); } } @@ -48,7 +40,7 @@ trait BotActionTrait $message = MessageUtil::convertToArr($message); $params['message'] = $message; $params['detail_type'] = $detail_type; - return $this->sendAction(Utils::camelToSeparator(__FUNCTION__), $params, $this->self); + return $this->sendAction(Utils::camelToSeparator(__FUNCTION__), $params, $this->getSelf()); } /** @@ -58,6 +50,9 @@ trait BotActionTrait */ public function sendAction(string $action, array $params = [], ?array $self = null): bool|ActionResponse { + if ($self === null && $this->self !== null) { + $self = $this->self; + } // 声明 Action 对象 $a = new Action($action, $params, ob_uuidgen(), $self); // 调用事件在回复之前的回调 @@ -70,29 +65,29 @@ trait BotActionTrait return false; } - // 调用机器人连接发送 Action,首先试试看是不是 WebSocket - if ($this->base_event instanceof WebSocketMessageEvent) { - logger()->debug('使用传入的 base_event 发送消息'); - $result = $this->base_event->send(json_encode($a->jsonSerialize())); - } - if (!isset($result) && container()->has('ws.message.event')) { - logger()->debug('使用容器的 Event 发送消息'); - $result = container()->get('ws.message.event')->send(json_encode($a->jsonSerialize())); - } - // 如果是 HTTP WebHook 的形式,那么直接调用 Response - if (!isset($result) && $this->base_event instanceof HttpRequestEvent) { - $response = HttpFactory::createResponse(headers: ['Content-Type' => 'application/json'], body: json_encode([$a->jsonSerialize()])); - $this->base_event->withResponse($response); - $result = true; - } - if (!isset($result) && container()->has('http.request.event')) { - $response = HttpFactory::createResponse(headers: ['Content-Type' => 'application/json'], body: json_encode([$a->jsonSerialize()])); - container()->get('http.request.event')->withResponse($response); - $result = true; + // 获取机器人的 BotMap 对应连接(前提是当前上下文有 self) + if ($self !== null) { + $fd_map = BotMap::getBotFd($self['user_id'], $self['platform']); + if ($fd_map === null) { + logger()->error("机器人 [{$self['platform']}:{$self['user_id']}] 没有连接或未就绪,无法发送数据"); + return false; + } + $result = ws_socket($fd_map[0])->send(json_encode($a->jsonSerialize()), $fd_map[1]); + } elseif ($this instanceof BotConnectContext) { + // self 为空,说明可能是发送的元动作,需要通过 fd 来查找对应的 connect 连接 + $flag = $this->getFlag(); + $fd = $this->getFd(); + $result = ws_socket($flag)->send(json_encode($a->jsonSerialize()), $fd); + } elseif (method_exists($this, 'emitSendAction')) { + $result = $this->emitSendAction($a); + } else { + logger()->error('未匹配到任何机器人连接'); + return false; } + // 如果开启了协程,并且成功发送,那就进入协程等待,挂起等待结果返回一个 ActionResponse 对象 if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) { - static::$coroutine_list[$a->echo] = $co->getCid(); + BotMap::$bot_coroutines[$a->echo] = $co->getCid(); $response = $co->suspend(); if ($response instanceof ActionResponse) { return $response; diff --git a/src/ZM/Plugin/OneBot/BotMap.php b/src/ZM/Plugin/OneBot/BotMap.php new file mode 100644 index 00000000..91460275 --- /dev/null +++ b/src/ZM/Plugin/OneBot/BotMap.php @@ -0,0 +1,133 @@ +> 机器人上下文对象列表 + */ + private static array $bot_status = []; + + /** + * @var array> 机器人上下文缓存对象,避免重复创建 + */ + private static array $bot_ctx_cache = []; + + /** + * 机器人对应连接 fd + * 例如:{ "qq": { "123456": [1,2] } } + * + * @var array> 机器人对应连接 fd + */ + private static array $bot_fds = []; + + public static function getConnectContext(int $flag, int $fd): BotConnectContext + { + return new BotConnectContext($flag, $fd); + } + + /** + * 注册机器人 + * + * @param int|string $bot_id 机器人 ID + * @param string $platform 机器人平台 + * @param bool $status 机器人状态 + * @param int $fd 绑定的反向 ws 连接的客户端对应 fd + * @param int $flag fd 所在 server 监听端口 + */ + public static function registerBotWithFd(string|int $bot_id, string $platform, bool $status, int $fd, int $flag): bool + { + logger()->debug('正在注册机器人:' . "{$platform}:{$bot_id}, fd:{$fd}, flag:{$flag}"); + self::$bot_fds[$platform][strval($bot_id)] = [$flag, $fd]; + self::$bot_status[$platform][strval($bot_id)] = $status; + return true; + } + + /** + * 获取所有机器人对应的 fd + * + * @return array> + */ + public static function getBotFds(): array + { + return self::$bot_fds; + } + + public static function getBotFd(string|int $bot_id, string $platform): ?array + { + return self::$bot_fds[$platform][$bot_id] ?? null; + } + + public static function unregisterBot(string|int $bot_id, string $platform): void + { + logger()->debug('取消注册 bot: ' . $bot_id); + unset(self::$bot_fds[$platform][$bot_id], self::$bot_status[$platform][$bot_id], self::$bot_ctx_cache[$platform][$bot_id]); + } + + public static function unregisterBotByFd(int $flag, int $fd): void + { + $unreg_list = []; + foreach (self::$bot_fds as $platform => $bots) { + foreach ($bots as $bot_id => $bot_fd) { + if ($bot_fd[0] === $flag && $bot_fd[1] = $fd) { + $unreg_list[] = [$platform, $bot_id]; + } + } + } + foreach ($unreg_list as $item) { + self::unregisterBot($item[1], $item[0]); + } + } + + public static function getBotContext(string|int $bot_id = '', string $platform = ''): BotContext + { + if (isset(self::$bot_ctx_cache[$platform][$bot_id])) { + return self::$bot_ctx_cache[$platform][$bot_id]; + } + // 如果传入的是空,说明需要通过 cid 来获取事件绑定的机器人,并且机器人没有 + if ($bot_id === '' && $platform === '') { + if (!container()->has(OneBotEvent::class)) { + throw new OneBot12Exception('无法在不指定机器人平台、机器人 ID 的情况下在非机器人事件回调内获取机器人上下文'); + } + $event = container()->get(OneBotEvent::class); + if (($event->self['platform'] ?? null) === null) { + throw new OneBot12Exception('无法在不包含机器人 ID 的事件回调内获取机器人上下文'); + } + // 有,那就通过事件本身的 self 字段来获取一下 + $self = $event->self; + return self::$bot_ctx_cache[$self['platform']][$self['user_id']] = new BotContext($self['user_id'], $self['platform']); + } + // 传入的 platform 为空,但 ID 不为空,那么就模糊搜索一个平台的 ID 下的机器人 ID 返回 + if ($platform === '') { + foreach (self::$bot_fds as $platform => $bot_ids) { + foreach ($bot_ids as $id => $fd_map) { + if ($id === $bot_id) { + return self::$bot_ctx_cache[$platform][$id] = new BotContext($id, $platform); + } + } + } + throw new OneBot12Exception('未找到 ID 为 ' . $bot_id . ' 的机器人'); + } + if (!isset(self::$bot_fds[$platform][$bot_id])) { + throw new OneBot12Exception('未找到 ' . $platform . ' 平台下 ID 为 ' . $bot_id . ' 的机器人'); + } + return self::$bot_ctx_cache[$platform][$bot_id] = new BotContext($bot_id, $platform); + } +} diff --git a/src/ZM/Plugin/OneBot12Adapter.php b/src/ZM/Plugin/OneBot/OneBot12Adapter.php similarity index 91% rename from src/ZM/Plugin/OneBot12Adapter.php rename to src/ZM/Plugin/OneBot/OneBot12Adapter.php index bc8ffba0..4f18d4fa 100644 --- a/src/ZM/Plugin/OneBot12Adapter.php +++ b/src/ZM/Plugin/OneBot/OneBot12Adapter.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace ZM\Plugin; +namespace ZM\Plugin\OneBot; use Choir\Http\HttpFactory; use OneBot\Driver\Coroutine\Adaptive; use OneBot\Driver\Event\StopException; +use OneBot\Driver\Event\WebSocket\WebSocketCloseEvent; use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent; use OneBot\V12\Exception\OneBotException; @@ -28,6 +29,7 @@ use ZM\Context\BotContext; use ZM\Exception\InterruptException; use ZM\Exception\OneBot12Exception; use ZM\Exception\WaitTimeoutException; +use ZM\Plugin\ZMPlugin; use ZM\Utils\ConnectionUtil; use ZM\Utils\MessageUtil; @@ -49,19 +51,20 @@ class OneBot12Adapter extends ZMPlugin */ private static array $context_prompt_queue = []; - public function __construct(string $submodule = '', ?AnnotationParser $parser = null) + public function __construct(string $submodule = 'onebot12', ?AnnotationParser $parser = null) { switch ($submodule) { - case '': case 'onebot12': // 处理所有 OneBot 12 的反向 WS 握手事件 $this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseOpen']); $this->addEvent(WebSocketMessageEvent::class, [$this, 'handleWSReverseMessage']); + $this->addEvent(WebSocketCloseEvent::class, [$this, 'handleWSReverseClose']); // 在 BotEvent 内处理 BotCommand $this->addBotEvent(BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand'])); // 在 BotEvent 内处理需要等待回复的 CommandArgument $this->addBotEvent(BotEvent::make(type: 'message', level: 49)->on([$this, 'handleCommandArgument'])); $this->addBotEvent(BotEvent::make(type: 'message', level: 50)->on([$this, 'handleContextPrompt'])); + $this->addBotEvent(BotEvent::make(type: 'meta', detail_type: 'status_update', level: 50)->on([$this, 'handleStatusUpdate'])); // 处理和声明所有 BotCommand 下的 CommandArgument $parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']); // 不需要给列表写入 CommandArgument @@ -165,6 +168,44 @@ class OneBot12Adapter extends ZMPlugin $this->callBotCommand($ctx, $command); } + /** + * [CALLBACK] 处理 status_update 事件,更新 BotMap + * + * @param OneBotEvent $event 机器人事件 + */ + public function handleStatusUpdate(OneBotEvent $event, WebSocketMessageEvent $message_event): void + { + $status = $event->get('status'); + $old = BotMap::getBotFds(); + if (($status['good'] ?? false) === true) { + foreach (($status['bots'] ?? []) as $bot) { + BotMap::registerBotWithFd( + bot_id: $bot['self']['user_id'], + platform: $bot['self']['platform'], + status: $bot['good'] ?? false, + fd: $message_event->getFd(), + flag: $message_event->getSocketFlag() + ); + if (isset($old[$bot['self']['platform']][$bot['self']['user_id']])) { + unset($old[$bot['self']['platform']][$bot['self']['user_id']]); + } + logger()->error("[{$bot['self']['platform']}.{$bot['self']['user_id']}] 已接入,状态:" . (($bot['good'] ?? false) ? 'OK' : 'Not OK')); + } + } else { + logger()->debug('该实现状态目前不是正常的,不处理 bots 列表'); + $old = []; + } + foreach ($old as $platform => $bot_ids) { + if (empty($bot_ids)) { + continue; + } + foreach ($bot_ids as $id => $flag_fd) { + logger()->debug("[{$platform}.{$id}] 已断开!"); + BotMap::unregisterBot($id, $platform); + } + } + } + /** * [CALLBACK] 处理需要等待回复的 CommandArgument * @@ -339,6 +380,14 @@ class OneBot12Adapter extends ZMPlugin // 绑定容器 ContainerRegistrant::registerOBEventServices($obj); + if ($obj->getSelf() !== null) { + $bot_id = $obj->self['user_id']; + $platform = $obj->self['platform']; + if (BotMap::getBotFd($bot_id, $platform) === null) { + BotMap::registerBotWithFd($bot_id, $platform, true, $event->getFd(), $event->getSocketFlag()); + } + container()->set(BotContext::class, bot($obj->self['user_id'], $obj->self['platform'])); + } // 调用 BotEvent 事件 $handler = new AnnotationHandler(BotEvent::class); @@ -387,6 +436,17 @@ class OneBot12Adapter extends ZMPlugin } } + public function handleWSReverseClose(WebSocketCloseEvent $event) + { + // 忽略非 OneBot 12 的消息 + $impl = ConnectionUtil::getConnection($event->getFd())['impl'] ?? null; + if ($impl === null) { + return; + } + // 在关闭连接的时候 + BotMap::unregisterBotByFd($event->getSocketFlag(), $event->getFd()); + } + /** * 根据事件匹配规则 *