ErisPulse适配器开发经验分享

ErisPulse适配器开发经验分享

这段时间搞了好几个适配器,踩了不少坑。随便聊聊适配器开发的经验吧

适配器是啥

适配器是ErisPulse和各个消息平台之间的桥梁,每个平台的API都不一样,事件格式也千奇百怪。适配器的任务就是把这些"方言"翻译成ErisPulse能听懂的"普通话"(OneBot12标准),然后再把ErisPulse的"普通话"翻译回各个平台的"方言"。

这样用户写的模块就可以在所有平台上跑了,一次编写,全平台运行

SendDSL的设计思路

链式调用的好处

想法是让调用更顺手,提供一定的语法糖让调用事半功倍:

1await adapter.Send.To("group", "123").Text("Hello")

也是可以这样,使用一些修饰方法的:

1await adapter.Send.To("group", "123").At("456").Reply("msg_id").Text("回复@的消息")

修饰方法和发送方法的区别

这点设计的时候想了挺久。

修饰方法(At、Reply、AtAll)返回self,这样才能继续链式调用。发送方法(Text、Image这些)返回asyncio.Task,这样用户就能自行选择是否等待结果之类的了

其实这也有技术债上的影子(,,, 如果现在设计的话,会在最终添加一个 .send() 总方法再进行链式的最终发送,而不是一个最终发送方法 保持兼容性的话,就只能这样了

 1class Send:
 2    # 修饰方法返回 self
 3    def At(self, user_id: str):
 4        self._at_users.append(user_id)
 5        return self
 6    
 7    def Reply(self, msg_id: str):
 8        self._reply_to = msg_id
 9        return self
10    
11    # 发送方法返回 Task
12    def Text(self, text: str):
13        return asyncio.create_task(
14            self._adapter.call_api(
15                endpoint="/send",
16                content=text,
17                recvId=self._target_id,
18                recvType=self._target_type
19            )
20        )

为什么返回Task而不是直接await

这个也是考虑了很久。

如果发送方法直接await,那每次调用都会阻塞,返回Task的话,用户可以选择:

 1# 不等结果,后台发送
 2adapter.Send.To("user", "123").Text("Hello")
 3
 4# 或者等结果
 5await adapter.Send.To("user", "123").Text("Hello")
 6
 7# 或者先保存Task,稍后等待
 8task = adapter.Send.To("user", "123").Text("Hello")
 9# ... 做其他事 ...
10result = await task

开发过程中的踩坑经历

WebSocket事件提交的异步链路

这个坑真的踩了好几次www

最开始做的时候,以为事件提交就是同步的,结果发现整个事件链路都是异步的。如果不注意,事件提交的顺序可能会乱,甚至丢失。

正确的做法是每次事件提交都用Task:

1async def _ws_handler(self, websocket):
2    async for message in websocket:
3        # 转换事件
4        event = self.convert(message)
5        
6        # 提交事件到框架
7        asyncio.create_task(
8            self.sdk.adapter.emit(event)
9        )

或者每个ws_handler都用Task

这样每个事件都在独立的Task里处理,互不干扰,也不会阻塞WebSocket的接收循环。框架会保证事件的顺序处理

多账号管理的麻烦事

这个是OneBot11适配器搞出来的。

OneBot11支持多个Bot实例,每个Bot有自己的账号ID。适配器要能指定用哪个Bot发消息:

1# 用默认Bot发
2await adapter.Send.To("group", "123").Text("Hello")
3
4# 用指定Bot发
5await adapter.Send.Using("bot1").To("group", "123").Text("Hello")

一开始以为就加个Using方法就行,结果发现call_api方法也要知道用哪个Bot调用API,配置也要分开管理,连日志都要标明是哪个Bot的。

最后搞了个复杂的配置结构,才勉强搞定。

参考现有适配器

云湖、Telegram、OneBot11这几个适配器的代码都是开源的,遇到问题可以看看是怎么处理的。

特别是OneBot11适配器,配置和账号管理比较复杂,可以参考一下实现,但ob11的适配器是只有ws的,没有webhook。webhook的适配器实现确实简单了一些,也可以参考云湖的实现,如果是pulling的话,可以参考Telegram的实现

最后

评论