diff --git a/README.md b/README.md index 258b8d6..2ac2ef0 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,152 @@ -# go-cqhttp-adapter-plugin - -炸毛框架用于接入 go-cqhttp(OneBot 11)的适配器插件。 - -## 功能 - -该插件将 gocq 的反向 WebSocket 接入信息全部转换为 OneBot 12 标准,安装该插件后几乎无需修改任何代码即可接入。 - -如果你想在其他项目上使用,也可以单独使用内部的 Converter 相关类进行转换。 - -## 安装 - -```bash -# Composer 安装稳定版 -composer require zhamao/go-cqhttp-adapter-plugin - -# GitHub 安装 Nightly 版 -./zhamao plugin:install https://github.com/zhamao-robot/go-cqhttp-adapter-plugin.git -``` - -## 转换注意事项 - -由于 OneBot 11 和 OneBot 12 有较多差异,而这些差异也导致两者不能无损相互转换。 -例如 OneBot 11 中未规定要求文件分片上传和下载的动作,那么在使用本插件时也无法使用这些动作。 - -由于 OneBot 11 的实现较多,而且和 OneBot 11 本身相差较大,该插件也时重点针对 go-cqhttp 进行适配转换,这也是插件不叫 onebot-11-adapter 的原因。 - -本插件下方列举的事件转换规则仅为自身的一些转换细节处理方案,不代表 OneBot 11 和 OneBot 12 的事件定义。 -但是本插件的转换规则也是 OneBot 11 和 OneBot 12 事件定义的一个参考,如果你想了解 OneBot 11 和 12 的差异,也可以阅读下方的转换规则。 - -## 事件转换规则(11 转 12) - -下面是 `post_type` 转换规则: - -- 字段名 `post_type` 转换为 `type`。 -- 如果 `post_type` 值为 `meta_event`,转换为 `meta`。 -- 如果 `post_type` 值为 `message_sent`,转换为 `message`。 - -下面是 `XXX_type` 转换规则: - -- 如果 `post_type` 为 `message` 或 `message_sent`,将字段名 `message_type` 转换为 `detail_type`。 -- 如果 `post_type` 为 `meta_event`,将字段名 `meta_event_type` 转换为 `detail_type`。 -- 如果 `post_type` 为 `notice`,将字段名 `notice_type` 转换为 `detail_type`。 -- 如果 `post_type` 为 `request`,将字段名 `request_type` 转换为 `detail_type`。 - -下面是 `self_id` 转换规则: - -- 如果存在 `self_id` 字段,将该字段删除,替换为 `"self" => ["user_id" => $user_id, "platform" => "qq"]`,其中 `$user_id` 的值等于 `self_id` 的字符串值。 -- 如果不存在 `self_id` 字段,将该字段删除,替换为和上方一样的格式,`$user_id` 的值等于该连接请求握手时 `X-Self-ID` 的值。 - -下面是 `user_id`、`group_id`、`guild_id`、`channel_id`、`message_id` 转换规则: - -- 以上列举的值都取字符串值,即转换为字符串。 - -下面是其他字段的一些转换规则: - -- 如果 `post_type` 为 `message` 或 `message_sent`,则将 `raw_message` 转换为 `alt_message`。 -- go-cqhttp 的消息事件中默认带有 `sender`,将其字段名转换为 `qq.sender`,内部参数不变。 -- go-cqhttp 未提供事件的 ID,因此该插件会自动生成一个随机的 UUID 作为事件 ID。 - -下面是消息事件(`message`)的一些转换规则: - -- `message` 字段如果为字符串,会先将 CQ 码无损转换为 OneBot 11 的等价消息段,然后再将消息段转换为 OneBot 12 标准的消息段。 -- CQ 码的转换基本按照 OneBot 11 的规范进行,但是由于 OneBot 11 的规范不完整,因此可能会有一些不同。例如,CQ 码在解码参数时未考虑参数内带有等号,该适配器仅会获取第一个等号前的参数,后面的等号会被当作值的一部分。 -- 转换为 OneBot 12 的消息段时,仅保留 `type` 和 `data`,如果存在其他字段,将丢弃。 -- `at` 类型,如果参数 `qq` 为 `all`,则类型转换为 `mention_all`,否则类型转换为 `mention` 同时 `qq` 字段名转换为 `user_id`。 -- `video`、`image`、`record` 类型,将参数 `file` 转换为 `file_id`。 -- `record` 类型转换为 `voice`。 -- `location` 类型,将参数 `lat` 转换为 `latitude`,将参数 `lon` 转换为 `longitude`。 -- `reply` 类型,将 `id` 转换为 `message_id`,如果存在参数 `qq`,将 `qq` 转换为 `user_id`。 -- 其他类型,类型字段名称统一添加前缀 `qq.`,例如 `forward` 转换为 `qq.forward`。 - -下面是通知事件(`notice`)的一些转换规则: - -| go-cqhttp 的 `notice_type` | OneBot 12 的 `detail_type` | 描述 | -|---------------------------|---------------------------|-----------| -| `friend_recall` | `private_message_delete` | 撤回一条私聊消息 | -| `friend_add` | `friend_increase` | 添加好友的通知事件 | -| `group_increase` | `group_member_increase` | 群成员增加 | -| `group_decrease` | `group_member_decrease` | 群成员减少 | -| `group_recall` | `group_message_delete` | 群消息撤回 | -| 除上述外的其他通知事件 | `qq.` 前缀加上原名称 | | - -- `friend_recall` 转换后,仅保留 `message_id` 和 `user_id` 字段。 -- `friend_add` 转换后,仅保留 `user_id` 字段。 -- `group_increase` 转换后,如果 `sub_type` 值为 `approve` 或空,则转换为 `join`;如果为 `invite` 则不变,如果是其他,则加上前缀 `qq.`。 -- `group_increase` 转换后,仅保留 `sub_type`、`group_id`、`user_id``operator_id` 且转换为字符串值。 -- `group_decrease` 转换后,如果 `sub_type` 值为 `kick` 或 `kick_me`,则转换为 `kick`;如果为 `leave` 则不变,如果是其他,则加上前缀 `qq.`。如果想判断是否为 `kick_me`,可以使用判断 `user_id` 和 `operator_id` 是否相同。 -- `group_decrease` 转换后,仅保留 `sub_type`、`group_id`、`user_id``operator_id` 且转换为字符串值。 -- `group_recall` 转换后,如果 `user_id` 与 `operator_id` 相同,则 `sub_type` 值设定为 `recall`,否则为 `delete`(分别代表自己撤回和被撤回)。 -- `group_recall` 转换后,仅保留 `message_id`、`group_id`、`user_id`、`operator_id` 且转换为字符串值。 -- 其他通知类事件加前缀,其他字段除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。 - -下面是请求事件(`request`)的一些转换规则: - -- 由于 OneBot 12 标准未规定任何 `request` 事件,故所有 `request` 事件均将 `request_type` 加上 `qq.` 前缀,并转换名称为 `detail_type`。 -- 其他字段除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。 - -下面是元事件(`meta`)的一些转换规则: - -- `meta_event_type` 转换为 `detail_type`。 -- 目前 go-cqhttp 仅有两种元事件,其中 `meta_event_type` 分别为 `lifecycle` 和 `heartbeat`。 -- 如果 `meta_event_type` 为 `lifecycle`,`sub_type` 为 `connect`,则将 `detail_type` 转换为 `connect`。 -- 如果按照上面的规则转换为 `connect`,其中 OneBot 12 转换后的 `version` 内容为:`['impl' => 'go-cqhttp', 'version' => $user_agent, 'onebot_version' => '12']`。其中 `$user_agent` 为与 gocq 建立连接时对端发送过来的 `User-Agent` 值。 -- 如果 `meta_event_type` 为 `heartbeat`,则参数仅保留 `interval`,类型名称不变。 -- 其他 `meta_evet_type`,不添加前缀,假设实现端有一个 `xxx` 元事件,则此处转换为 `detail_type` 依旧是 `xxx`。 -- 其他元事件的其他字段,除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。 - -## 动作转换规则(12 转 11) - -- `send_message` 动作根据参数 `detail_type` 的值,转换为对应的 `send_xxx_msg` 动作,但目前仅支持私聊和群组类型。 -- 如果 `detail_type` 为 `group`,将保留 `group_id` 参数,并将 OneBot 12 消息段转换为 OneBot 11 消息段发送。 -- 如果 `detail_type` 为 `private`,将保留 `user_id` 参数,如果传入的 Action 对象存在 `group_id` 参数也将会保留,然后将 OneBot 12 消息段转换为 OneBot 11 消息段发送。 -- `delete_message` 动作名称变更为 `delete_msg`,保留 `message_id` 参数。 -- `get_self_info` 动作名称变更为 `get_login_info`。 -- `get_user_info` 动作名称变更为 `get_stranger_info`,保留 `user_id` 字段。 -- `get_friend_list`、`get_group_info`、`get_group_list`、`get_group_member_info`、`get_group_member_list`、`set_group_name` 动作名称和参数均不变。 -- `leave_group` 动作名称变更为 `set_group_leave`,参数不变。 -- `upload_file` 动作名称变更为 `download_file`,并且仅支持 URL 方式上传文件。 -- `get_status` 名称和参数(好像也没有请求参数,不管了)均保持不变。 -- `get_version` 动作名称变更为 `get_version_info`。 -- 其他动作如果开头使用 `qq.` 前缀,则将其前缀去除,参数保持不变。 -- `echo` 回响字段保持不变。 - -## 动作响应转换规则(11 转 12) - -> 动作响应的转换在插件内部对动作请求做了缓存,通过 echo 字段进行匹配,从而确定响应对应的动作请求。 - -- `status`、`retcode`、`echo` 字段保持不变。 -- `msg` 字段如果存在则转换为 `message` 字段。 -- 如果请求的动作是 `send_xxx_msg`,则响应中的 `message_id` 字段保持不变。 -- 如果请求动作是 `get_stranger_info`,响应中的 `user_id` 保持不变,`nickname` 转换为 `user_name`,同时也将 `user_displayname` 设置为 `user_name` 相同的值,`user_remark` 设置为空。 -- 如果请求动作是 `get_login_info`,响应中的 `user_id` 保持不变,`nickname` 转换为 `user_name`,同时也将 `user_displayname` 设置为 `user_name` 相同的值。 -- 如果请求动作是 `get_friend_list`,每个用户元素的 `user_id` 保持不变,`nickname` 转换为 `user_name`,同时也将 `user_displayname` 设置为 `user_name` 相同的值,`user_remark` 设置为空。 -- 如果请求动作是 `get_group_info` 或 `get_group_list`,参数保持不变。 -- 如果请求动作是 `get_group_member_info`,现有参数转换规则同 `get_stranger_info` 的参数转换规则,其他参数的参数名添加前缀 `qq.`,值保持不变。 -- 如果请求动作是 `get_group_member_list`,每个用户元素的参数转换规则同 `get_group_member_info`。 -- 如果请求动作是 `download_file`,响应中的 `file` 字段转换为 `file_id`。 -- 如果请求动作是 `set_group_name`、`set_group_leave`,参数保持不变。 -- 如果请求动作是 `get_status`,仅保留 `good` 字段,OB12 的 `bots` 字段值为 `[['impl' => 'go-cqhttp', 'version' => $user_agent, 'onebot_version' => '12']]`。 -- 如果请求动作是 `get_version_info`,`app_name` 转换为 `impl`,`app_version` 转换为 `version`,`onebot_version` 设置为 12,其他值丢弃。 - -## 其他不兼容项 - -转换器不支持转换以下 OneBot 12 动作到 OneBot 11 API: - -- `get_latest_events` -- `get_supported_actions` -- 所有二级群组动作,例如 `get_guild_info` 等 -- `upload_file_fragmented` -- `get_file` -- `get_file_fragmented` +# go-cqhttp-adapter-plugin + +炸毛框架用于接入 go-cqhttp(OneBot 11)的适配器插件。 + +## 功能 + +该插件将 gocq 的反向 WebSocket 接入信息全部转换为 OneBot 12 标准,安装该插件后几乎无需修改任何代码即可接入。 + +如果你想在其他项目上使用,也可以单独使用内部的 Converter 相关类进行转换。 + +## 安装 + +```bash +# Composer 安装稳定版 +composer require zhamao/go-cqhttp-adapter-plugin + +# GitHub 安装 Nightly 版 +./zhamao plugin:install https://github.com/zhamao-robot/go-cqhttp-adapter-plugin.git +``` + +## 转换注意事项 + +由于 OneBot 11 和 OneBot 12 有较多差异,而这些差异也导致两者不能无损相互转换。 +例如 OneBot 11 中未规定要求文件分片上传和下载的动作,那么在使用本插件时也无法使用这些动作。 + +由于 OneBot 11 的实现较多,而且和 OneBot 11 本身相差较大,该插件也时重点针对 go-cqhttp 进行适配转换,这也是插件不叫 onebot-11-adapter 的原因。 + +本插件下方列举的事件转换规则仅为自身的一些转换细节处理方案,不代表 OneBot 11 和 OneBot 12 的事件定义。 +但是本插件的转换规则也是 OneBot 11 和 OneBot 12 事件定义的一个参考,如果你想了解 OneBot 11 和 12 的差异,也可以阅读下方的转换规则。 + +## 事件转换规则(11 转 12) + +下面是 `post_type` 转换规则: + +- 字段名 `post_type` 转换为 `type`。 +- 如果 `post_type` 值为 `meta_event`,转换为 `meta`。 +- 如果 `post_type` 值为 `message_sent`,转换为 `message`。 + +下面是 `XXX_type` 转换规则: + +- 如果 `post_type` 为 `message` 或 `message_sent`,将字段名 `message_type` 转换为 `detail_type`。 +- 如果 `post_type` 为 `meta_event`,将字段名 `meta_event_type` 转换为 `detail_type`。 +- 如果 `post_type` 为 `notice`,将字段名 `notice_type` 转换为 `detail_type`。 +- 如果 `post_type` 为 `request`,将字段名 `request_type` 转换为 `detail_type`。 + +下面是 `self_id` 转换规则: + +- 如果存在 `self_id` 字段,将该字段删除,替换为 `"self" => ["user_id" => $user_id, "platform" => "qq"]`,其中 `$user_id` 的值等于 `self_id` 的字符串值。 +- 如果不存在 `self_id` 字段,将该字段删除,替换为和上方一样的格式,`$user_id` 的值等于该连接请求握手时 `X-Self-ID` 的值。 + +下面是 `user_id`、`group_id`、`guild_id`、`channel_id`、`message_id` 转换规则: + +- 以上列举的值都取字符串值,即转换为字符串。 + +下面是其他字段的一些转换规则: + +- 如果 `post_type` 为 `message` 或 `message_sent`,则将 `raw_message` 转换为 `alt_message`。 +- go-cqhttp 的消息事件中默认带有 `sender`,将其字段名转换为 `qq.sender`,内部参数不变。 +- go-cqhttp 未提供事件的 ID,因此该插件会自动生成一个随机的 UUID 作为事件 ID。 + +下面是消息事件(`message`)的一些转换规则: + +- `message` 字段如果为字符串,会先将 CQ 码无损转换为 OneBot 11 的等价消息段,然后再将消息段转换为 OneBot 12 标准的消息段。 +- CQ 码的转换基本按照 OneBot 11 的规范进行,但是由于 OneBot 11 的规范不完整,因此可能会有一些不同。例如,CQ 码在解码参数时未考虑参数内带有等号,该适配器仅会获取第一个等号前的参数,后面的等号会被当作值的一部分。 +- 转换为 OneBot 12 的消息段时,仅保留 `type` 和 `data`,如果存在其他字段,将丢弃。 +- `at` 类型,如果参数 `qq` 为 `all`,则类型转换为 `mention_all`,否则类型转换为 `mention` 同时 `qq` 字段名转换为 `user_id`。 +- `video`、`image`、`record` 类型,将参数 `file` 转换为 `file_id`。 +- `record` 类型转换为 `voice`。 +- `location` 类型,将参数 `lat` 转换为 `latitude`,将参数 `lon` 转换为 `longitude`。 +- `reply` 类型,将 `id` 转换为 `message_id`,如果存在参数 `qq`,将 `qq` 转换为 `user_id`。 +- 其他类型,类型字段名称统一添加前缀 `qq.`,例如 `forward` 转换为 `qq.forward`。 + +下面是通知事件(`notice`)的一些转换规则: + +| go-cqhttp 的 `notice_type` | OneBot 12 的 `detail_type` | 描述 | +|---------------------------|---------------------------|-----------| +| `friend_recall` | `private_message_delete` | 撤回一条私聊消息 | +| `friend_add` | `friend_increase` | 添加好友的通知事件 | +| `group_increase` | `group_member_increase` | 群成员增加 | +| `group_decrease` | `group_member_decrease` | 群成员减少 | +| `group_recall` | `group_message_delete` | 群消息撤回 | +| 除上述外的其他通知事件 | `qq.` 前缀加上原名称 | | + +- `friend_recall` 转换后,仅保留 `message_id` 和 `user_id` 字段。 +- `friend_add` 转换后,仅保留 `user_id` 字段。 +- `group_increase` 转换后,如果 `sub_type` 值为 `approve` 或空,则转换为 `join`;如果为 `invite` 则不变,如果是其他,则加上前缀 `qq.`。 +- `group_increase` 转换后,仅保留 `sub_type`、`group_id`、`user_id``operator_id` 且转换为字符串值。 +- `group_decrease` 转换后,如果 `sub_type` 值为 `kick` 或 `kick_me`,则转换为 `kick`;如果为 `leave` 则不变,如果是其他,则加上前缀 `qq.`。如果想判断是否为 `kick_me`,可以使用判断 `user_id` 和 `operator_id` 是否相同。 +- `group_decrease` 转换后,仅保留 `sub_type`、`group_id`、`user_id``operator_id` 且转换为字符串值。 +- `group_recall` 转换后,如果 `user_id` 与 `operator_id` 相同,则 `sub_type` 值设定为 `recall`,否则为 `delete`(分别代表自己撤回和被撤回)。 +- `group_recall` 转换后,仅保留 `message_id`、`group_id`、`user_id`、`operator_id` 且转换为字符串值。 +- 其他通知类事件加前缀,其他字段除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。 + +下面是请求事件(`request`)的一些转换规则: + +- 由于 OneBot 12 标准未规定任何 `request` 事件,故所有 `request` 事件均将 `request_type` 加上 `qq.` 前缀,并转换名称为 `detail_type`。 +- 其他字段除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。 + +下面是元事件(`meta`)的一些转换规则: + +- `meta_event_type` 转换为 `detail_type`。 +- 目前 go-cqhttp 仅有两种元事件,其中 `meta_event_type` 分别为 `lifecycle` 和 `heartbeat`。 +- 如果 `meta_event_type` 为 `lifecycle`,`sub_type` 为 `connect`,则将 `detail_type` 转换为 `connect`。 +- 如果按照上面的规则转换为 `connect`,其中 OneBot 12 转换后的 `version` 内容为:`['impl' => 'go-cqhttp', 'version' => $user_agent, 'onebot_version' => '12']`。其中 `$user_agent` 为与 gocq 建立连接时对端发送过来的 `User-Agent` 值。 +- 如果 `meta_event_type` 为 `heartbeat`,则参数仅保留 `interval`,类型名称不变。 +- 其他 `meta_evet_type`,不添加前缀,假设实现端有一个 `xxx` 元事件,则此处转换为 `detail_type` 依旧是 `xxx`。 +- 其他元事件的其他字段,除 `post_type`、`notice_type`、`sub_type`、`request_type`、`meta_event_type`、`time` 和一系列 `xxx_id` 外,均保留,并加上 `qq.` 前缀,值不变。 + +## 动作转换规则(12 转 11) + +- `send_message` 动作根据参数 `detail_type` 的值,转换为对应的 `send_xxx_msg` 动作,但目前仅支持私聊和群组类型。 +- 如果 `detail_type` 为 `group`,将保留 `group_id` 参数,并将 OneBot 12 消息段转换为 OneBot 11 消息段发送。 +- 如果 `detail_type` 为 `private`,将保留 `user_id` 参数,如果传入的 Action 对象存在 `group_id` 参数也将会保留,然后将 OneBot 12 消息段转换为 OneBot 11 消息段发送。 +- `delete_message` 动作名称变更为 `delete_msg`,保留 `message_id` 参数。 +- `get_self_info` 动作名称变更为 `get_login_info`。 +- `get_user_info` 动作名称变更为 `get_stranger_info`,保留 `user_id` 字段。 +- `get_friend_list`、`get_group_info`、`get_group_list`、`get_group_member_info`、`get_group_member_list`、`set_group_name` 动作名称和参数均不变。 +- `leave_group` 动作名称变更为 `set_group_leave`,参数不变。 +- `upload_file` 动作名称变更为 `download_file`,并且仅支持 URL 方式上传文件。 +- `get_status` 名称和参数(好像也没有请求参数,不管了)均保持不变。 +- `get_version` 动作名称变更为 `get_version_info`。 +- 其他动作如果开头使用 `qq.` 前缀,则将其前缀去除,参数保持不变。 +- `echo` 回响字段保持不变。 + +## 动作响应转换规则(11 转 12) + +> 动作响应的转换在插件内部对动作请求做了缓存,通过 echo 字段进行匹配,从而确定响应对应的动作请求。 + +- `status`、`retcode`、`echo` 字段保持不变。 +- `msg` 字段如果存在则转换为 `message` 字段。 +- 如果请求的动作是 `send_xxx_msg`,则响应中的 `message_id` 字段保持不变。 +- 如果请求动作是 `get_stranger_info`,响应中的 `user_id` 保持不变,`nickname` 转换为 `user_name`,同时也将 `user_displayname` 设置为 `user_name` 相同的值,`user_remark` 设置为空。 +- 如果请求动作是 `get_login_info`,响应中的 `user_id` 保持不变,`nickname` 转换为 `user_name`,同时也将 `user_displayname` 设置为 `user_name` 相同的值。 +- 如果请求动作是 `get_friend_list`,每个用户元素的 `user_id` 保持不变,`nickname` 转换为 `user_name`,同时也将 `user_displayname` 设置为 `user_name` 相同的值,`user_remark` 设置为空。 +- 如果请求动作是 `get_group_info` 或 `get_group_list`,参数保持不变。 +- 如果请求动作是 `get_group_member_info`,现有参数转换规则同 `get_stranger_info` 的参数转换规则,其他参数的参数名添加前缀 `qq.`,值保持不变。 +- 如果请求动作是 `get_group_member_list`,每个用户元素的参数转换规则同 `get_group_member_info`。 +- 如果请求动作是 `download_file`,响应中的 `file` 字段转换为 `file_id`。 +- 如果请求动作是 `set_group_name`、`set_group_leave`,参数保持不变。 +- 如果请求动作是 `get_status`,仅保留 `good` 字段,OB12 的 `bots` 字段值为 `[['impl' => 'go-cqhttp', 'version' => $user_agent, 'onebot_version' => '12']]`。 +- 如果请求动作是 `get_version_info`,`app_name` 转换为 `impl`,且 impl 值后附加 `(go-cqhttp-adapter converted)`,`app_version` 转换为 `version`,`onebot_version` 设置为 12,其他值丢弃。 + +## 其他不兼容项 + +转换器不支持转换以下 OneBot 12 动作到 OneBot 11 API: + +- `get_latest_events` +- `get_supported_actions` +- 所有二级群组动作,例如 `get_guild_info` 等 +- `upload_file_fragmented` +- `get_file` +- `get_file_fragmented` diff --git a/composer.json b/composer.json index 17f43ad..abbd1dc 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,19 @@ { - "name": "zhamao/go-cqhttp-adapter-plugin", - "license": "AGPL-3.0", - "autoload": { - "psr-4": { - "GocqAdapter\\": "src/" + "name": "zhamao/go-cqhttp-adapter-plugin", + "license": "AGPL-3.0", + "autoload": { + "psr-4": { + "GocqAdapter\\": "src/" + } + }, + "require": { + "php": "~8.0 || ~8.1 || ~8.2" + }, + "require-dev": { + "zhamao/framework": "dev-main" + }, + "minimum-stability": "dev", + "extra": { + "zm-plugin-version": "1.0.0" } - }, - "require": { - "php": "~8.0 || ~8.1" - }, - "require-dev": { - "zhamao/framework": "dev-main" - }, - "minimum-stability": "dev" } diff --git a/src/GoActionTrait.php b/src/GoActionTrait.php new file mode 100644 index 0000000..80cf51e --- /dev/null +++ b/src/GoActionTrait.php @@ -0,0 +1,80 @@ +self !== null) { + $self = $this->self; + } + // 声明 Action 对象 + $a = new Action($action, $params, ob_uuidgen(), $self); + // 调用事件在回复之前的回调 + $handler = new AnnotationHandler(BotAction::class); + container()->set(Action::class, $a); + $handler->setRuleCallback(fn (BotAction $act) => ($act->action === '' || $act->action === $action) && !$act->need_response); + $handler->handleAll(); + // 被阻断时候,就不发送了 + if ($handler->getStatus() === AnnotationHandler::STATUS_INTERRUPTED) { + return false; + } + + // 从这里开始,gocq 需要做一个 12 -> 11 的转换 + $action_array = GocqActionConverter::getInstance()->convertAction12To11($a); + // 将这个 action 提取出来需要记忆的 echo + GocqAdapter::$action_hold_list[$a->echo] = $action_array; + // 获取机器人的 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($action_array), $fd_map[1]); + } elseif ($this instanceof GoBotConnectContext) { + // self 为空,说明可能是发送的元动作,需要通过 fd 来查找对应的 connect 连接 + $flag = $this->getFlag(); + $fd = $this->getFd(); + $result = ws_socket($flag)->send(json_encode($action_array), $fd); + } elseif (method_exists($this, 'emitSendAction')) { + $result = $this->emitSendAction($a); + } else { + logger()->error('未匹配到任何机器人连接'); + return false; + } + + // 如果开启了协程,并且成功发送,那就进入协程等待,挂起等待结果返回一个 ActionResponse 对象 + if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) { + BotMap::$bot_coroutines[$a->echo] = $co->getCid(); + $response = $co->suspend(); + if ($response instanceof ActionResponse) { + $handler = new AnnotationHandler(BotAction::class); + $handler->setRuleCallback(fn(BotAction $act) => ($act->action === '' || $act->action === $action) && $act->need_response); + container()->set(ActionResponse::class, $response); + $handler->handleAll(); + return $response; + } + return false; + } + if (isset($result)) { + return $result; + } + // 到这里表明你调用时候不在 WS 或 HTTP 上下文 + throw new OneBot12Exception('No bot connection found.'); + } +} diff --git a/src/GoBotConnectContext.php b/src/GoBotConnectContext.php new file mode 100644 index 0000000..9ee517a --- /dev/null +++ b/src/GoBotConnectContext.php @@ -0,0 +1,10 @@ +set(Action::class, $a); - $handler->setRuleCallback(fn (BotAction $act) => $act->action === '' || $act->action === $action && !$act->need_response); - $handler->handleAll($a); - // 被阻断时候,就不发送了 - if ($handler->getStatus() === AnnotationHandler::STATUS_INTERRUPTED) { - return false; - } - - // 从这里开始,gocq 需要做一个 12 -> 11 的转换 - $action_array = GocqActionConverter::getInstance()->convertAction12To11($a); - // 将这个 action 提取出来需要记忆的 echo - GocqAdapter::$action_hold_list[$a->echo] = $action_array; - - // 调用机器人连接发送 Action - if ($this->base_event instanceof WebSocketMessageEvent) { - $result = $this->base_event->send(json_encode($action_array)); - } - if (!isset($result) && container()->has('ws.message.event')) { - $result = container()->get('ws.message.event')->send(json_encode($action_array)); - } - // 如果开启了协程,并且成功发送,那就进入协程等待,挂起等待结果返回一个 ActionResponse 对象 - if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) { - static::$coroutine_list[$a->echo] = $co->getCid(); - $response = $co->suspend(); - if ($response instanceof ActionResponse) { - return $response; - } - return false; - } - if (isset($result)) { - return $result; - } - // 到这里表明你调用时候不在 WS 或 HTTP 上下文 - throw new OneBot12Exception('No bot connection found.'); - } + use GoActionTrait; } diff --git a/src/GocqActionConverter.php b/src/GocqActionConverter.php index 757fcd3..8fbb000 100644 --- a/src/GocqActionConverter.php +++ b/src/GocqActionConverter.php @@ -82,13 +82,17 @@ public function convertAction12To11(Action $action): array 'thread_count' => $action->params['thread_count'] ?? 1, ]; break; + case 'get_version': + $act = 'get_version_info'; + $params = $action->params; + break; default: // qq. 开头的动作,一律当作 gocq 的其他事件,这时候 params 原封不动发出 if (str_starts_with($action->action, 'qq.')) { $act = substr($action->action, 3); $params = $action->params; } else { - throw new OneBot12Exception('Current action cannot send to gocq'); + throw new OneBot12Exception('Current action cannot send to gocq: ' . $action->action); } break; } @@ -178,6 +182,13 @@ public function convertActionResponse11To12(array $response, array $action): Act 'file_id' => $response['data']['file'], ]; break; + case 'get_version_info': + $response_obj->data = [ + 'impl' => $response['data']['app_name'] . ' (go-cqhttp-adapter converted)', + 'version' => $response['data']['app_version'], + 'onebot_version' => '12', + ]; + break; case 'set_group_name': case 'set_group_leave': default: @@ -195,6 +206,9 @@ private function parseSegments12To11(array $message): array { $msgs = []; foreach ($message as $v) { + if (is_array($v)) { + $v = segment($v['type'], $v['data'] ?? []); + } $msgs[] = GocqSegmentConverter::getInstance()->parseSegment12To11($v); } return $msgs; diff --git a/src/GocqAdapter.php b/src/GocqAdapter.php index d3777da..815ba70 100644 --- a/src/GocqAdapter.php +++ b/src/GocqAdapter.php @@ -8,17 +8,22 @@ 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 ZM\Annotation\AnnotationHandler; use ZM\Annotation\Framework\BindEvent; use ZM\Annotation\Framework\Init; +use ZM\Annotation\Middleware\Middleware; use ZM\Annotation\OneBot\BotActionResponse; use ZM\Annotation\OneBot\BotEvent; use ZM\Annotation\OneBot\CommandArgument; use ZM\Container\ContainerRegistrant; use ZM\Context\BotContext; +use ZM\Exception\OneBot12Exception; use ZM\Exception\WaitTimeoutException; +use ZM\Middleware\WebSocketFilter; +use ZM\Plugin\OneBot\BotMap; use ZM\Utils\ConnectionUtil; class GocqAdapter @@ -47,29 +52,26 @@ public function handleWSReverseOpen(WebSocketOpenEvent $event): void { logger()->info('连接到 ob11'); $request = $event->getRequest(); - ob_dump($request); // 判断是不是 Gocq 或 OneBot 11 标准的连接。OB11 标准必须带有 X-Client-Role 和 X-Self-ID 两个头。 if ($request->getHeaderLine('X-Client-Role') === 'Universal' && $request->getHeaderLine('X-Self-ID') !== '') { logger()->info('检测到 OneBot 11 反向 WS 连接 ' . $request->getHeaderLine('User-Agent')); - $info = ['gocq_impl' => 'go-cqhttp', 'self_id' => $request->getHeaderLine('X-Self-ID')]; + $info = ['gocq_impl' => 'go-cqhttp', 'self_id' => $request->getHeaderLine('X-Self-ID'), 'onebot-version' => 11]; // TODO: 验证 Token ConnectionUtil::setConnection($event->getFd(), $info); logger()->info('已接入 go-cqhttp 的反向 WS 连接,连接 ID 为 ' . $event->getFd()); + BotMap::setCustomConnectContext($event->getSocketFlag(), $event->getFd(), new GoBotConnectContext($event->getSocketFlag(), $event->getFd())); } } /** - * @throws OneBotException + * @param WebSocketMessageEvent $event + * @throws \Throwable + * @throws OneBot12Exception */ #[BindEvent(WebSocketMessageEvent::class)] + #[Middleware(WebSocketFilter::class, ['gocq_impl' => 'go-cqhttp'])] public function handleWSReverseMessage(WebSocketMessageEvent $event): void { - // 忽略非 gocq 的消息 - $impl = ConnectionUtil::getConnection($event->getFd())['gocq_impl'] ?? null; - if ($impl === null) { - return; - } - // 解析 Frame 到 UTF-8 JSON $body = $event->getFrame()->getData(); $body = json_decode($body, true); @@ -79,6 +81,7 @@ public function handleWSReverseMessage(WebSocketMessageEvent $event): void } if (isset($body['post_type'], $body['self_id'])) { + // 获取转换后的对象 $ob12 = self::getConverter($event->getFd(), strval($body['self_id']))->convertEvent($body); if ($ob12 === null) { logger()->debug('收到了不支持的 Event,丢弃此事件'); @@ -94,8 +97,10 @@ public function handleWSReverseMessage(WebSocketMessageEvent $event): void } // 绑定容器 - ContainerRegistrant::registerOBEventServices($obj, GoBotContext::class); - + ContainerRegistrant::registerOBEventServices($obj); + BotMap::registerBotWithFd($obj->self['user_id'], $obj->self['platform'], true, $event->getFd(), $event->getSocketFlag()); + BotMap::setCustomContext($obj->self['user_id'], $obj->self['platform'], GoBotContext::class); + container()->set(BotContext::class, bot()); // 调用 BotEvent 事件 $handler = new AnnotationHandler(BotEvent::class); $handler->setRuleCallback(function (BotEvent $event) use ($obj) { @@ -104,7 +109,7 @@ public function handleWSReverseMessage(WebSocketMessageEvent $event): void && ($event->detail_type === null || $event->detail_type === $obj->detail_type); }); try { - $handler->handleAll($obj); + $handler->handleAll(); } catch (WaitTimeoutException $e) { // 这里是处理 prompt() 下超时的情况的 if ($e->getTimeoutPrompt() === null) { @@ -125,14 +130,17 @@ public function handleWSReverseMessage(WebSocketMessageEvent $event): void $origin_action = self::$action_hold_list[$body['echo']]; unset(self::$action_hold_list[$body['echo']]); $resp = GocqActionConverter::getInstance()->convertActionResponse11To12($body, $origin_action); + ContainerRegistrant::registerOBActionResponseServices($resp); // 调用 BotActionResponse 事件 $handler = new AnnotationHandler(BotActionResponse::class); $handler->setRuleCallback(function (BotActionResponse $event) use ($resp) { - return $event->retcode === null || $event->retcode === $resp->retcode; + return ($event->retcode === null || $event->retcode === $resp->retcode) + && ($event->status === null || $event->status === $resp->status); }); - $handler->handleAll($resp); + container()->set(ActionResponse::class, $resp); + $handler->handleAll(); // 如果有协程,并且该 echo 记录在案的话,就恢复协程 BotContext::tryResume($resp); diff --git a/src/GocqEventConverter.php b/src/GocqEventConverter.php index daf1698..0c48df2 100644 --- a/src/GocqEventConverter.php +++ b/src/GocqEventConverter.php @@ -103,7 +103,7 @@ public function convertMessageSegment(string|array $message): array $message[$k] = ['type' => $type, 'data' => $data]; } } - return $message; + return json_decode(json_encode($message), true); } /**