project-mirai / mirai-api-http

Mirai HTTP API (console) plugin
GNU Affero General Public License v3.0
1.66k stars 343 forks source link

[功能请求] 添加标准通用数据包的接口 #732

Open bunnyi116 opened 1 year ago

bunnyi116 commented 1 year ago

原因

在编写独立的Http与WebSocket适配器的API的时候,发现需要重复写的东西很多,因为Http请求接口路径问题,得每个为他编写方法,为了能统一调用适配器API方法,为此建议制定标准数据包格式接口以便调用。至于其他的适配器我也不清楚,没用过

这样做的好处

可以减少编写适配器的接口的工作量,例:http、ws通过适配器接口去实现一个ResponsePacket SendPacket(RequestPacket packet)的方法,然后将已实现的适配器派生对象放入一个适配器管理器中的列表中,然后适配器管理器的也实现ResponsePacket SendPacket(RequestPacket packet)方法,不过这个方法是遍历适配器列表进行发送(如果适配器模式允许发送的话则发送),之后我准备实现发送消息API,那我只需要在适配器管理器(或新建一个专门实现API类)中实现一个叫做SendFriendMessage方法,然后调用适配器管理器的SendPacket即可实现不同适配器一起发送。 无标题

实现(mirai-api-http)

  1. 在Http适配器中,添加一个POST请求接口,该POST请求为标准数据包格式。例:http://127.0.0.1/general
  2. 在服务端响应返回给客户端的数据包需要携带RestfulResult信息,也就是状态码和状态消息,如果是成功状态可以忽略

数据包格式

RequestPacket(请求数据包)

  1. syncId(同步标识,根据适配器需求而定,但标准中必须有,他可以为null不进行序列化)
  2. command(请求的命令也就是请求路径)
  3. data(请求的数据)

ResponsePacket(响应数据包)

  1. syncId(同步标识,根据适配器需求而定,但标准中必须有,他可以为null不进行序列化)
  2. code(状态代码,所有响应的数据包都需要实现,当成功状态时,序列化可以忽略,个人觉得不管成功与否都需要携带)
  3. msg(状态消息,所有响应的数据包都需要实现,当成功状态时,序列化可以忽略)
  4. data(响应的数据,如果响应没有数据,只是一个确认是否成功,那么它可以为null不进行序列化)
bunnyi116 commented 1 year ago

顺便提一句:这不仅仅针对http,也适用于其他适配器。

mirai-api-http有多种适配器且每个适配器都可以独立存在,为了开发者更好对接mirai-api-http,需要为开发者编写一个通用请求与响应的接口(标准的传出传入的数据包),让开发者更好的管理适配器API,避免重复编写API方法。

bunnyi116 commented 1 year ago

建议清单

  1. 制定一个标准的请求与响应数据包格式
  2. 当客户端连接服务端并认证成功后(我觉得http中 verify 与 bind 请求可以进行合并,有点多余)

    应当响应数据

    https://docs.mirai.mamoe.net/mirai-api-http/adapter/HttpAdapter.html#%E8%8E%B7%E5%8F%96%E4%BC%9A%E8%AF%9D%E4%BF%A1%E6%81%AF

    {
    "syncId": "", 
    "code": 0,
    "msg": "",
    "data": {
    "sessionKey": "YourSessionKey",
    "qq": {
      "id": 1234567890,
      "nickname": "",
      "remark": ""
    }
    }
    }

    不应该响应格式

    {
    "syncId": "",
    "data": {
    "code": 0,
    "session": "YourSessionKey"
    }
    }
  3. 【客户端】所有请求的数据都通过RequestPacket进行包装在发送
  4. 【服务端】所有响应的数据都通过ResponsePacket(RestfulResult)进行包装在发送
  5. 增加心跳机制,这个心跳包用于服务端和服务端互相感知连接状态,如果一定时间没有进行交互(比如:1分钟一次,然后更新上一次心跳的时间),那么双方就连接然后释放资源。
  6. 退出账号后再登录,之前的会话会丢失Bot实例,所以在登录账号成功后Bot触发上线事件后,请 mirai-api-http 更新当前会话列表对应账号的Bot实例,避免会话对应的Bot实例映射丢失,从而导致客户端接收不到信息。
  7. 为了更好的区分新的协议,可以在 /about 请求中,增加协议版本号字段(protocol version),采用整数形式,这样客户端就不需要进行版本字符串分割检查版本号,只需要根据协议版本号进行调整,比如v1 Api的版本协议版本就命名为1,故此类推
  8. Socket文件上传文件特殊接口。针对于socket处理长连接类型的适配器(WebSocket),会阻塞Socket的情况,可以考虑添加新连接,专门用于上传文件的连接接口,拿WebSocket举例,目前我有个ws主连接,然后准备上传一个文件\图片,客户端可以使用当前的会话key主动与ws服务端创建一个新连接,该连接通道为upload,然后传入参数文件名,文件字节大小,md5,等等...,然后通过二进制流直接传输,服务端根据文件的字节大小进行接收,当传输完成后,服务端给客户端一个成功响应。

数据包格式(这边我是以客户端视角命名)

请求数据包RequestPacket

  1. syncId(用于同步,可为null不进行序列化,根据适配器情况而定)
  2. command(请求的命令,我觉得没必要有子命令,跟http请求路径相同就行)
  3. data(请求的数据)

响应数据包ResponsePacket(mirai-api-http中叫做RestfulResult)

  1. syncId(用于同步,可为null不进行序列化,根据适配器情况而定)
  2. code (必须携带,这里使用枚举类进行统一约束,序列化的时候转换成int就行)
  3. msg (这个其实可以省略为null不进行序列化,因为有状态代码了,这个可以当做一个附加状态信息)
  4. data (数据的对象,用于承载通用数据,如果没有数据,则为null不进行序列化)
bunnyi116 commented 1 year ago

syncId 我觉得服务端主动推送直接为空或null就行,不用-1配置保留字段,然后客户端请求API的时候,syncId的值长度必须大于0,然后服务端校验一下客户端的请求syncId的长度 <= 0 则服务端直接响应错误表示拒绝\syncId值不正确,避免客户端传入的syncId空或null值与服务端主动推送syncId冲突

ryoii commented 1 year ago

HTTP body部分的格式和 Websocket content 部分的格式是一样的,应该不存在说需要重复实现的问题

bunnyi116 commented 1 year ago

数据格式都差不多,就是不规范,有很多不同的数据结构。

然后我说重复的是请求接口,因为http接口不同方法单独实现,在接口管理上的时候是很麻烦的,需要一个通用的请求接口。不然http ws api需要很多单独方法,然后接口管理里面又要实现适配器api调用,换句话说套娃。

ryoii commented 1 year ago

不规范当然是有的,但已经发布的接口,数据格式不是说随随便便就直接改的

不同 adapter 之间请求和响应在实现上用的都是同一个实体,结构理论上都是一样的,不存在说这部分在不同 adapter 之间要重复实现。你所说的很多单独的、难以管理的方法,能不能提供一个例子出来

bunnyi116 commented 1 year ago
public interface IApi
{
    void GetFriendList();
    // 其他...
}

public interface IAdapter : IApi
{
    AdapterMode Mode { get; }
}

public enum AdapterMode
{
    Receive, Send
}

internal class HttpAdapter : IAdapter, IApi
{
    public AdapterMode Mode { get; }
    public void GetFriendList()
    {
        // http...
    }
    // 其他...
}

internal class WebSocketAdapter : IAdapter, IApi
{
    public AdapterMode Mode { get; }

    public void GetFriendList()
    {
        // ws...
    }
    // 其他...
}

internal class AdapterManager : IApi
{
    private readonly List<IAdapter> adapters = new List<IAdapter>();

    public void GetFriendList()
    {
        foreach (var adapter in adapters)
        {
            // 适配器模式(是否支持发送API)
            if (adapter.Mode == AdapterMode.Send)
            {
                try
                {
                    adapter.GetFriendList();
                    return;
                }
                catch (Exception) { }
            }
        }
    }

    // 其他...
}

这边需要适配器把全部的API编写出来,然后管理器也要全部编写出来。

如果通过一个通用的接口,让他们整合起来就方便许多,只要实现一个通用接口,然后通过请求通用的接口去统一实现

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MiraiBotClient;

public class RequestPacket
{
    // ....
}
public class ResponsePacket
{
    // ....
}

public interface IApi
{
    void GetFriendList();
}

public interface IAdapter
{
    AdapterMode Mode { get; }
    ResponsePacket SendPacket(RequestPacket requestPacket);
}

public enum AdapterMode
{
    Receive, Send
}

internal class HttpAdapter : IAdapter
{
    public AdapterMode Mode { get; }

    public ResponsePacket SendPacket(RequestPacket requestPacket)
    {
        throw new NotImplementedException();
    }
}

internal class WebSocketAdapter : IAdapter
{
    public AdapterMode Mode { get; }

    public ResponsePacket SendPacket(RequestPacket requestPacket)
    {
        throw new NotImplementedException();
    }
}

internal class AdapterManager
{
    private readonly List<IAdapter> adapters = new List<IAdapter>();

    public ResponsePacket SendPacket(RequestPacket requestPacket)
    {
        foreach (var adapter in adapters)
        {
            // 或者弄个优先级,自行设置
            if (adapter.Mode == AdapterMode.Send)
            {
                return adapter.SendPacket(requestPacket);
            }
        }
    }

}

internal class Api : IApi
{
    AdapterManager AdapterManager { get; }

    public Api(AdapterManager adapterManager)
    {
        AdapterManager = adapterManager;
    }

    public void GetFriendList()
    {
        var friendList = new RequestPacket();
        // ....
        AdapterManager.SendPacket(friendList);
    }

    // 其他...
}

可能比较简陋

ryoii commented 1 year ago

不同接口的逻辑不同,即使提供通用接口,我认为该重复的地方还是会重复的。

不同 IAdapter 只负责到参数的解包和封包,共同抽离处理逻辑可以适当减少重复代码。

当然通用接口是可以提供的,但修改现有的请求结构可能性不大。你可以看看 general-router分支的这个功能能不能满足你的需求