mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-18 05:04:51 +08:00
253 lines
9.7 KiB
PHP
253 lines
9.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
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\Utils\MessageUtil;
|
||
|
||
class BotContext implements ContextInterface
|
||
{
|
||
use BotActionTrait;
|
||
|
||
/** @var array<string, array<string, BotContext>> 记录机器人的上下文列表 */
|
||
private static array $bots = [];
|
||
|
||
/** @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)
|
||
{
|
||
$this->self = ['user_id' => $bot_id, 'platform' => $platform];
|
||
self::$bots[$bot_id][$platform] = $this;
|
||
$this->base_event = $event;
|
||
}
|
||
|
||
/**
|
||
* 获取机器人事件对象
|
||
*
|
||
* @throws DependencyException
|
||
* @throws NotFoundException
|
||
*/
|
||
public function getEvent(): OneBotEvent
|
||
{
|
||
return container()->get('bot.event');
|
||
}
|
||
|
||
/**
|
||
* 快速回复机器人消息文本
|
||
*
|
||
* @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, int $reply_mode = ZM_REPLY_NONE)
|
||
{
|
||
if (container()->has('bot.event')) {
|
||
// 这里直接使用当前上下文的事件里面的参数,不再重新挨个获取怎么发消息的参数
|
||
/** @var OneBotEvent $event */
|
||
$event = container()->get('bot.event');
|
||
|
||
// reply 的条件是必须 type=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.');
|
||
}
|
||
|
||
/**
|
||
* 在当前会话等待用户一条消息
|
||
* 如果是私聊,就在对应的机器人私聊环境下等待
|
||
* 如果是单级群组,就在对应的群组下等待当前消息人的消息
|
||
* 如果是多级群组,则等待最小级下当前消息人的消息
|
||
*
|
||
* @param array|MessageSegment|string|\Stringable $prompt 等待前发送的消息文本
|
||
* @param int $timeout 等待超时时间(单位为秒,默认为 600 秒)
|
||
* @param array|MessageSegment|string|\Stringable $timeout_prompt 超时后提示的消息内容
|
||
* @param bool $return_string 是否只返回 text 格式的字符串消息(默认为 false)
|
||
* @param int $option prompt 功能的选项参数
|
||
* @throws DependencyException
|
||
* @throws NotFoundException
|
||
* @throws OneBot12Exception
|
||
* @throws WaitTimeoutException
|
||
*/
|
||
public function prompt(string|\Stringable|MessageSegment|array $prompt = '', int $timeout = 600, string|\Stringable|MessageSegment|array $timeout_prompt = '', bool $return_string = false, int $option = ZM_PROMPT_NONE): array|string
|
||
{
|
||
if (!container()->has('bot.event')) {
|
||
throw new OneBot12Exception('bot()->prompt() can only be used in message event');
|
||
}
|
||
/** @var OneBotEvent $event */
|
||
$event = container()->get('bot.event');
|
||
if ($event->type !== 'message') {
|
||
throw new OneBot12Exception('bot()->prompt() can only be used in message event');
|
||
}
|
||
// 开始等待输入
|
||
logger()->debug('Waiting user for prompt...');
|
||
if ($prompt !== '') {
|
||
$prompt = $this->applyPromptMode($option, $prompt, $event);
|
||
$reply_resp = $this->reply($prompt);
|
||
}
|
||
if (($co = Adaptive::getCoroutine()) === null) {
|
||
throw new OneBot12Exception('Coroutine is not supported yet, prompt() not works');
|
||
}
|
||
$cid = $co->getCid();
|
||
OneBot12Adapter::addContextPrompt($cid, $event);
|
||
$co->create(function () use ($cid, $timeout) {
|
||
Adaptive::sleep($timeout);
|
||
if (OneBot12Adapter::isContextPromptExists($cid)) {
|
||
Adaptive::getCoroutine()->resume($cid, '');
|
||
}
|
||
});
|
||
$result = $co->suspend();
|
||
OneBot12Adapter::removeContextPrompt($cid);
|
||
if ($result === '') {
|
||
throw new WaitTimeoutException(
|
||
$this,
|
||
$timeout_prompt,
|
||
prompt_response: isset($reply_resp) && $reply_resp instanceof ActionResponse ? $reply_resp : null,
|
||
user_event: $event,
|
||
prompt_option: $option
|
||
);
|
||
}
|
||
if ($result instanceof OneBotEvent && $result->type === 'message') {
|
||
return $return_string ? $result->getMessageString() : $result->getMessage();
|
||
}
|
||
throw new OneBot12Exception('Internal error for resuming prompt: unknown type ' . gettype($result));
|
||
}
|
||
|
||
/**
|
||
* 返回是否已经调用过回复了
|
||
*/
|
||
public function hasReplied(): bool
|
||
{
|
||
return $this->replied;
|
||
}
|
||
|
||
/**
|
||
* 获取其他机器人的上下文操作对象
|
||
*
|
||
* @param string $bot_id 机器人的 self.user_id 对应的 ID
|
||
* @param string $platform 机器人的 self.platform 对应的 platform
|
||
* @throws OneBot12Exception
|
||
*/
|
||
public function getBot(string $bot_id, string $platform = ''): BotContext
|
||
{
|
||
if (isset(self::$bots[$bot_id])) {
|
||
if ($platform === '') {
|
||
$one = current(self::$bots[$bot_id]);
|
||
if ($one instanceof BotContext) {
|
||
return $one;
|
||
}
|
||
} elseif (isset(self::$bots[$bot_id][$platform])) {
|
||
return self::$bots[$bot_id][$platform];
|
||
}
|
||
}
|
||
// 到这里说明没找到对应的机器人,抛出异常
|
||
throw new OneBot12Exception('Bot not found.');
|
||
}
|
||
|
||
/**
|
||
* 设置该消息下解析出来的参数列表
|
||
*
|
||
* @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 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()]));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 匹配更改 prompt reply 的特殊格式
|
||
*
|
||
* @param int $option prompt 模式
|
||
* @param array|MessageSegment|string|\Stringable $prompt 消息或消息段
|
||
* @param OneBotEvent $event 事件对象
|
||
* @return array 消息段
|
||
*/
|
||
private function applyPromptMode(int $option, array|string|\Stringable|MessageSegment $prompt, OneBotEvent $event): array
|
||
{
|
||
// 先格式化消息
|
||
if ($prompt instanceof MessageSegment) {
|
||
$prompt = [$prompt];
|
||
} elseif (is_string($prompt) || $prompt instanceof \Stringable) {
|
||
$prompt = [strval($prompt)];
|
||
}
|
||
// 然后这里只验证 MENTION 和 QUOTE
|
||
if (($option & ZM_PROMPT_MENTION_USER) === ZM_PROMPT_MENTION_USER) {
|
||
$prompt = [new MessageSegment('mention', ['user_id' => $event->getUserId()]), ...$prompt];
|
||
}
|
||
if (($option & ZM_PROMPT_QUOTE_USER) === ZM_PROMPT_QUOTE_USER) {
|
||
$prompt = [new MessageSegment('reply', ['message_id' => $event->getMessageId(), 'user_id' => $event->getUserId()]), ...$prompt];
|
||
}
|
||
return $prompt;
|
||
}
|
||
}
|