钉钉、飞书、企微三家 IM 的 API 风格天差地别,但站在「AI 分身」视角,它们要做的事高度一致:收消息、发消息、标已读、拉历史。把这份一致性抽出来,差异留在适配器内部,是多平台支持能成立的前提。

上下文隔离是第一个坑

每个账号一个会话数据库,靠 ContextVar 在请求链路里传递。问题在于:守护线程不会自动继承父线程的 ContextVar,导致跨线程取到的 DB 是错的。修复方式是显式把 context 在 spawn 时 copy 进子线程。

平台门控不能挂在单例路由上

ToolRouter 是三平台共享的单实例。如果在 router 上判断「这个工具属于哪个平台」,必然错判。正确的归属判断必须挂在 orchestrator / agent 上,因为那是 per-account 的上下文。

三个最反直觉的边界 case

异常归属规则

根异常类在 src/exceptions.py,具体子类由各族 owner 模块单一持有(LLM 异常归 llm/、IM 异常归 im_adapter/)。在本模块另立同名具体类是历史事故源头,现在已在代码评审里卡死。

这类抽象没有银弹,真正稳健靠的是把每条平台差异都变成一桩可复盘的工程事件。