simpler-robot icon indicating copy to clipboard operation
simpler-robot copied to clipboard

统一的文件接口

Open NoMathExpectation opened this issue 1 year ago • 7 comments

建议描述

我想给机器人开发一个下载、处理文件并上传的功能。但是似乎框架没有提供统一文件处理的接口,而且各个平台对文件的实现也比较独立,对每个平台分别处理也比较费劲。希望能够有一个统一的文件接口,可以用来下载和上传文件。

NoMathExpectation avatar Jul 09 '24 16:07 NoMathExpectation

框架有提供针对 "资源(Resource)" 的抽象,它在JVM平台下或许可以视为某种针对文件(例如 File, Path 等)的抽象。 目前来看的话,Resource 在某些方面是够用的,只是可能避免不了编写平台代码。

不知道具体的操作文件的需求是怎样的?如果只是需要能够做到读取文件的二进制数据,Resource.data 就可以满足; 上传的话,如果是使用 Ktor (v2.x),simbot-common-ktor-inputfile 模块提供了一个很简单的包装类型 InputFile ; 如果需要解析 Resource 并针对不同类型做处理(比如区分 File、ByteArray等)可以考虑使用 ResourceResolverJvmResourceResolver 用类似访问器的形式进行处理。只不过如果是多平台项目,需要编写平台代码(主要是JVM平台)

但如果是说像 kotlinx.io 或者 Okio 那样针对文件系统的文件本身的抽象,那的确没有...,如果是这种的可以考虑直接使用 kotlinx.io 或 Okio

ForteScarlet avatar Jul 09 '24 17:07 ForteScarlet

你好,谢谢你的解答。我感觉我可能表述得有问题,造成了误解。我里面的平台指的是机器人对接的平台,例如kook、onebot、qq频道等这样的对接方式。我希望能有一个统一的接口,这样就可以只需写一次代码就可以处理从对接的平台下载文件(或者获取下载地址)和上传并发送文件到对接的平台,这样会方便许多。

NoMathExpectation avatar Jul 10 '24 02:07 NoMathExpectation

🤔以图片消息为例,如果要发送一个图片文件,一般来讲各个组件都会支持解析并使用 OfflineImage 类型,而 OfflineImage 类型可以通过 Resource 构建,用统一的方式发送应当是比较简单的。

而对于获取、下载收到的图片消息,的确不够统一,不过它们大多数都围绕着 RemoteImageUrlAwareImage 进行实现。 各组件的图片接收方式有不小的差异,比如:

  • KOOK、QQ频道中,图片是与其他"附件"类型元素在一起的,是附件的一种类别,Kook组件对附件类型进行了区分实现并实现了 UrlAwareImage,而QQ频道则暂时还没。
  • Telegram中,图片仅有ID,不会直接提供URL,需要再次通过网络接口查询。且一张图片有多个"尺寸",尺寸们又有各自的ID。
  • OneBot中,发送和接收的真实结构体是同一个,因此消息元素的类型目前也只有一个,因此暂时无法区分Offline还是Remote,不过它实现了 UrlAwareImage

不过随着更新,接收到的图片元素都会逐步实现类似 UrlAwareImage 或下文会提到的类似 UrlAwareMessage 的接口(如果这个图片元素能获取到url的话)。

对于其他类型的跟文件相关的内容,比如KOOK的文件附件,Telegram的语音音乐、文件附件等,的确没有一个统一的、可用于下载的便捷抽象。 但是不同平台的API差异使得它们很难被抽象成一个即可以顾及所有平台、又十分便于使用的类型,最常见的一种情况就是有的平台会直接给你资源的URL,而有的平台必须通过ID进行查询,很是头疼😢

许会考虑提供类似 UrlAwareMessage 的接口用来描述这个消息元素是否能够得到 URL 链接地址,或者能否通过手段查询到链接。但其他更好的方案我们就没什么头绪了 😣

ForteScarlet avatar Jul 10 '24 05:07 ForteScarlet

有关于文件操作的部分,我个人觉得可以试着这么实现:

  • 文件下载:设计一个消息元素接口FileMessage,提供一个方法返回文件二进制数据,例如kotlinx.ioSource,毕竟获取文件的需求本质上是需要读文件的数据;
  • 文件上传:设计一个行为对象接口FileSendSupport,可以实现方法传入二进制数据来构造FileMessage,然后只能通过这种方式去构造文件消息并插入消息链中,防止在不支持的平台上使用上传文件的功能。

这些只是我个人的一点想法,如果有考虑不周的地方请指正。

NoMathExpectation avatar Jul 10 '24 14:07 NoMathExpectation

对于文件下载:作为消息元素之一接收到的文件内容的形式比较有限,大概率是一个ID或URL(目前大部分组件的情况),小概率是完整数据(比如base64,但是还没遇到过这样的)。 如果提供获取文件二进制数据内容的接口或者功能,则需要考虑的情况我认为主要有这么几个:

  • 通过网络获取文件并下载,需要一个HTTP客户端,以及可能需要认证信息(比如bot的token)。在组件中,HTTP客户端一般在具体的Bot实现中,而消息元素通常不与某个具体的Bot相关联,且通常需要支持序列化,因此可能会导致序列化后丢失认证信息(或者说bot的信息,如果它需要的话)。
  • 直接获取文件的二进制数据,不适合较大文件。比如频道、Telegram的附件之类的,直接读到内存作为 ByteArray 并非明智之举。但是在多平台的情况下,外加simbot核心库并不依赖 Ktor 、kotlinx.io 尚未稳定的情况下,似乎除了直接获取 ByteArray 以外没有什么更好的抽象方案,就像 Resource 中的唯一一个抽象API也是 data(): ByteArray 一样。

不过,如果不考虑大文件的隐患、序列化的信息丢失隐患,提供直接获取二进制数据功能感觉可以考虑 👌,提供某种消息元素的接口,就像 UrlAwareMessage 那样 。


对于文件上传:其实类似于上传、构造独特 FileMessage 的这种API是有的,比如KOOK组件中的 KookBot.uploadAssetKookBot.uploadAssetImage。这里主要的问题是:"上传文件"这种API很难抽象化,不同的组件之间对于文件的API差异并不小,包括上传、发送的流程以及它们所需的参数,比如:

  • KOOK中,先通过 CreateAssetApi 提前上传,得到文件ID,然后再作为消息内容的一部分发送。需要类型、名称参数。
  • QQ频道中,似乎不能发送附件,只能发送图片。图片通过 MessageSendApifile_image 参数直接上传,没有提前上传的流程(倒是也可以用图片链接,链接需要审核备案),以表单的格式发送。没有额外针对图片文件的参数。
  • OneBot11中,图片可选地格式很多:本地文件路径(如果支持的话)、base64、链接(如果支持的话)。有很多可选参数,比如 typecacheproxytimeout 等。
  • Telegram中,发送图片也是直接通过 SendPhotoApi 上传,类似QQ频道,好像没有预上传的功能。需要的参数有很多,captionparseModehasSpoiler 等等。
  • Discord似乎也有很多参数,且没有预上传功能。

在参数方面,可以说它们之间毫无共通性 —— 除了大部分都需要表单格式,以及都有一个图片。

我们抽象了 OfflineImage 作为发送图片的消息元素,并尽可能地在所有组件中都支持解析它。因为图片发送的场景很常见,这是可以预估的。因此其实可以牵强地说是有抽象的“图片上传”功能地。

OfflineImage 可以通过 ResourceByteArray 构建,也符合 "传入二进制数据来构造消息元素" 的行为。

而抽象“文件上传”就有些困难了:

  1. 难以预估使用场景。比如QQ频道不支持上传附件、KOOK需要提前上传、而Telegram则有很多种文件需要用的地方:音频、文件、视频等等。
  2. 难以预估参数。不同的文件上传API所需的参数都不同,比如上面提到的OneBot和Telegram。

因此对于发送文件消息(图片除外),我们仍倾向于提供独特的消息元素类型(比如 Telegram 组件中的 TelegramAudio),而对于其他文件操作行为(比如曾经的 mirai 组件中的群文件API),它与消息本身无关,则会单独针对性的提供一套API,

除非...🤔能找到一个完美的抽象方案——不需要关心特定平台的实现,却能安全地满足任何特定平台的任何参数需求。

ForteScarlet avatar Jul 10 '24 16:07 ForteScarlet

对于文件对象序列化信息丢失以及文件过大的问题,可以使文件对象序列化时退化为一个不包含认证信息,只有获取文件所需的信息(例如是哪个环境的文件、 id 或 url 等)的对象,然后反序列化之后需要使用特定方法才能重新转化为可获取数据的文件对象,等用户真正需要数据调用获取数据的接口时再去与平台对接下载数据。

然后文件上传,可以只提供一个默认的 API,接受各个平台普遍使用的设置,产生一个平台默认实现的 FileMessage。如果用户需要自定义平台设置的话仍然可以自行调用平台独立的 API 去获得自定义的对象。而对于没有提前上传、上传即发送的平台,可以设计 lazy 的文件对象,在发送时检查是否已经上传,如果没有再去实际上传对象;或者是调用创建文件消息对象的方法时上传文件,然后在消息发送中去忽略包含的文件消息对象。

有关于各个平台文件参数不统一的部分,或许可以使用 map ?就像 HTTP 的 Header 一样,在框架里定义一套文件的标准属性,例如 attr["name"] = "foo" 代表文件名为 foo ,这样不同平台可以读取写入标准属性中,支持的平台可以尽量去支持实现这些属性,而不支持的平台可以忽略不支持的属性。然后上一段的文件上传 API 就可以改为接受这些属性去构造一个文件消息对象了,这样也可以兼顾各个平台的参数了。

个人意见,可能考虑不周,敬请指教。

NoMathExpectation avatar Jul 11 '24 09:07 NoMathExpectation

序列化导致信息丢失的情况,如果一定要携带认证信息(比如里面藏了一个bot),那么退化不可避免,只不过这个退化不可逆,因为如果要作为一个统一的抽象,就不可能预知需要什么东西来恢复它。其次还有一个问题就是消息元素的设计理应都是不可变、无状态且始终如一的,会退化的情况必然会违背这个原则,不过...倒也不是不能接受的违背就是了。因退化而导致无法再获取到其内容的情况可以接受,向用户 抛出异常 来作为提示。但"使用特定方法恢复"这一点感觉就难以抽象并统一了 🤔,这种特定方法是不可揣摩的。

然后文件上传,可以只提供一个默认的 API,接受各个平台普遍使用的设置,产生一个平台默认实现的 FileMessage。如果用户需要自定义平台设置的话仍然可以自行调用平台独立的 API 去获得自定义的对象。而对于没有提前上传、上传即发送的平台,可以设计 lazy 的文件对象,在发送时检查是否已经上传,如果没有再去实际上传对象;或者是调用创建文件消息对象的方法时上传文件,然后在消息发送中去忽略包含的文件消息对象。

这其实也就是 OfflineImage 的做法,它就是标准库中提供的一种统一的上传发送图片文件的方式。一个问题是除了图片以外,文件 这个概念太过松散。它可能是音频,可能是视频,可能是附件,只是一个 '发送文件' 似乎无法理解它的含义。比如在KOOK或Telegram中,发送了一个图片格式的文件,那么它到底是发送这张图片,还是发送一个图片附件?

有关于各个平台文件参数不统一的部分,或许可以使用 map ?就像 HTTP 的 Header 一样

这其实就是我们最想避免的情况 😞,也是为什么上一条回复中我强调了能够“安全地满足”。Map固然灵活,但它既不安全,也容易出错。比如:

  • Map中值的类型不可预估。如果约束为字符串,那么就存在类型转化的隐患;如果约束为 Any,那么...感觉好像比字符串还麻烦。
  • 难以描述结构体。properties的那种格式确实可以一定程度上用来描述结构体,但是会很复杂。数组结构则会更加麻烦。
  • 如果出现了上面这两条的情况,而为了解决它们选择:
    • 抛出异常来处理。那么出现异常的情况就会大大增加。作为Map的“弱类型”,不会有人提示它哪里写错了,而只能通过不断的试错来排除问题——不论是文档还是注释,都远不如编译器来的靠谱。
    • 增加容错性,遇到不可解析的内容则跳过。这会增加出现预期外行为的概率。本以为应该这样,但是因为解析失败而跳过,最终变成了那样。

如果想要确保绝对准确的使用Map中的键或值,目前想想办法倒是也有:由组件提供常量值以供使用。但是...这跟直接使用一个具体的强类型就没区别了。

这也是为什么 DeleteSupport.delete 这个API的参数我们选择使用了 ...options 的方式而不是类似于 map 的方式的原因。

印象里(记不清了)在 simbot3 时代是有一些类似使用 map 或者不可靠类型作为参数来达到兼容目的的API的,最终的结果要么就是复杂程度越来越高,要么就是为了更全面的抽象而导致重载或相似API越堆越大 😢

ForteScarlet avatar Jul 11 '24 09:07 ForteScarlet