本文定义 @mtproto-relay/bridge 的平台适配器接口契约。平台适配器只负责对接具体 IM 协议;消息入库、Telegram ID 分配、媒体分片与 MTProto TL 对象构造均由 Bridge 处理。
Bridge 通过 ctx.imPlatform 提供适配器注册表,每个 Cordis 配置项的 id 即为该平台账号的唯一 platformId:
import type { Context } from 'cordis'
import { resolvePlatformPluginId } from '@mtproto-relay/bridge'
export const inject = ['imPlatform']
export function apply(ctx: Context, config: Config) {
const id = resolvePlatformPluginId(ctx)
ctx.imPlatform.register(new QQPlatform(config), id)
}同一个适配器包可在 app.yml 中挂载多次,每个条目对应一个独立的平台账号和虚拟手机号:
- id: qq-primary
name: '@mtproto-relay/platform-qqnt'
- id: qq-secondary
name: '@mtproto-relay/platform-qqnt'适配器注册后,Bridge 调用 getAccount() 获取当前登录用户资料,创建或更新持久化会话,并调用 subscribe() 监听实时事件。subscribe() 回调返回的 Promise 具有背压语义:适配器必须等待该 Promise 完成后再推进上游游标,防止进程退出导致消息丢失。
- 用户、会话、消息、媒体与消息组 ID 均为不透明字符串(opaque string),禁止强转为数字或定长哈希。
- 用户按
(platformId, platformUserId)唯一标识,数据库自增主键即为其 Telegramuser_id。 IMMessage.id为逻辑消息 ID,sourceIds包含该逻辑消息对应的全部平台物理消息 ID(例如多图合并为一条逻辑消息时)。- Bridge 按
(platformSessionId, conversationId, sourceId)幂等入库。
interface IMConversation<L = unknown> {
id: string
kind: 'direct' | 'group' | 'channel'
title: string
parentId?: string
spaceId?: string
avatar?: IMMedia<L>
selfRole?: 'owner' | 'administrator' | 'member' | 'guest'
selfPermissions?: IMConversationPermissions
metadata?: JsonObject
}- QQ/微信群使用
group(Bridge 统一投影为 Telegram Megagroup)。 - Discord 服务器放入
spaceId,频道/子线程使用channel,父频道或分类放入parentId(当parentId指向同一会话下的另一个channel时,自动投影为 Forum Topic)。 - 实时消息事件必须携带完整的
IMConversation对象,以便在未拉取会话列表时直接入库。 - 成员列表通过
getConversationMembers()按游标分页,角色分为owner | administrator | member | guest,具体权限通过IMConversationPermissions显式声明。
IMMedia<L> 的 locator 是适配器自定义的强类型凭据,Bridge 仅在落库时将其序列化为 JSON,并在调用 downloadMedia(session, media, options) 时原样传回。
图片可附带 strippedThumbnail: Uint8Array(Telegram photoStrippedSize 格式),Bridge 会将其直接内联进消息,无需客户端额外请求缩略图。
sendMessage(session, conversation, input, options) 接收有序的 parts(支持文字、图片、文件、贴纸混排),返回带平台最终消息 ID 的 IMMessage。若平台消息正文原生为 Markdown(如 QQ 官方机器人),适配器除了提供渲染后的 parts,还可在 content.richMarkdown 中附上原始 Markdown,由 Bridge 转换为 Telegram 富文本(Article)消息块以支持表格、列表与多级标题。
系统提示使用 content.serviceAction:
{ type: 'custom', text }:通用灰条提示。{ type: 'phone-call' }:通话记录提示。{ type: 'members-joined', text, members, actor?, viaInviteLink? }:结构化入群通知,投影为可点击成员姓名的原生入群消息。
QQ 灰条特例:QQ 灰条与伴随的普通消息共用同一个
msgSeq。Bridge 会将指向灰条的回复重定向到同序号的正文消息,且删除灰条时仅在本地隐藏,绝不向 QQ 发起撤回请求(避免误撤回同序号的正文消息)。
getDialogs()与getHistory()每次调用只返回一页数据,严禁在单次调用内自动循环拉取后续游标。- 若平台支持标记已读,声明
capabilities.readState.markRead = true并实现markRead();若支持其他端已读同步推送,声明capabilities.readState.events = true并推送{ type: 'read', conversationId, upToMessageId }。
type IMEvent =
| { type: 'message', conversation: IMConversation, message: IMMessage }
| { type: 'message-edit', eventId: string, conversation: IMConversation, message: IMMessage }
| { type: 'message-delete', eventId: string, conversation: IMConversation, messageIds: string[], timestamp: number }
| { type: 'message-reactions', eventId: string, conversation: IMConversation, target: IMReactionTarget, context: IMReactionContext, timestamp: number }
| { type: 'conversation', conversation: IMConversation }
| { type: 'read', conversationId: string, upToMessageId: string }- 撤回采用 Tombstone 机制:消息从会话与历史中隐藏(或按配置标记为已撤回),但平台 ID 与 Telegram ID 映射永久保留。
capabilities.messageActions声明撤回、编辑(native | delete-and-resend | unsupported)与转发能力。- 支持「戳一戳」的平台声明
capabilities.poke = { maxCount }并实现sendPoke()。
支持 crossgram.prepareMediaUpload 的客户端在上传分片前会先提交文件哈希(MD5、SHA-1、前 10 MiB MD5 及分块 SHA-1 状态)。适配器实现 prepareMediaUpload():
- 秒传命中:返回不含
sink的结果,客户端跳过所有upload.saveFilePart分片上传。 - 流式直传:返回带
sink的上传计划,Bridge 在内存中重排乱序分片(上限 32 个分片 / 16 MiB)并直接按序写入sink(如 QQ Highway 1 MiB 分块直传),全程不落盘临时文件。 - 回退模式:未预检的客户端上传分片暂存至磁盘,发送时通过
media.source.stream()边读边传。
适配器必须实现 downloadMedia(session, media, { offset, limit, signal }) 支持字节区间读取。若平台支持客户端直连 CDN,可额外实现 resolveDirectDownload() 返回带过期时间的 HTTP(S) URL。
贴纸目录独立于 IMPlatform,通过 ctx.imSticker.register(provider, id) 注册:
- 提供
listPacks()、getPack()、getSticker()、openAsset()与可选的prepareSend()。 - QQ 收藏表情作为独立的
qq-favorites贴纸包暴露,客户端收藏/取消收藏直接映射为 QQ 收藏夹操作。
- 标准 Emoji 回应声明
presentation: { type: 'emoji', emoticon },复用 Bridge 内置的 Telegram 动画资源。 - 平台自定义回应声明
presentation: { type: 'custom', alt, resource },由downloadReactionResource()提供图片字节。 setMessageReactions(session, target, reactionKeys)接收消息最终选中的回应 Key 列表。