← 所有文章
教程

Webhook 媒体相册聚合:合并展示照片,不丢失子消息

通过 webhook 接收媒体相册时,应先独立保存每条子消息,再用相册标识构建聚合视图。不要按相册 ID 对子消息去重,否则同一相册里的不同照片会被丢弃。如果载荷没有提供预期总数,静默等待一段时间可以作为开始处理的条件,但不能证明所有照片都已到达。

要点

  • 消息身份用于逐条存储,相册身份用于关联,不能混用。
  • 两种身份都应包含接收账号及会话范围。
  • 持久化子消息后再确认投递,聚合放在后台执行。
  • 定时器触发后仍保留相册状态,让迟到项可以补入。

相册 ID 不是消息 ID

Telegram 官方 Bot API 文档将 Message.media_group_id 定义为可选字段,标识该消息所属、在当前聊天内唯一的媒体组。它描述消息之间的关联,不代替每条消息自身的身份。

UnifyPort 使用另一套契约。标准 webhook 参考记录了可选的 data.message.album,包含 id、index 和可选的 total。相册子项仍有自己的 data.message.id。文档中的相册示例来自 WhatsApp;不能因此承诺所有平台都会提供相册元数据,也不能假定 Telegram 原始字段会出现在标准载荷里。

如果还在实现附件提取,请先看媒体附件映射指南。聚合是在附件处理之外增加一个展示层,不是新的下载协议。

身份建议范围用途
普通事件 ID工作区及事件流识别同一事件的重复投递
子消息 ID工作区、平台、消息账号、会话保存或更新一条消息
相册 ID工作区、平台、消息账号、会话关联多个子项
附件 URL所属子消息记录定位媒体,不作为聚合键

例如,一个假设的三张照片提交中,B 到达两次、C 到达一次,只代表保存了两个不同子项,而不是三个。HTTP 请求次数不能代表相册完整度。

先保存子消息,再生成视图

以下 JavaScript 从已验证的 UnifyPort 事件生成应用存储键。返回属性是本地应用设计,不是新增 API 字段;这段代码也不负责验签或持久化。

function albumKeys(workspaceKey, event) {
  if (event.type !== 'message.received') return null;
  const conversation = event.data?.conversation;
  const message = event.data?.message;
  if (!event.provider || !event.account_id ||
      !conversation?.id || !message?.id) {
    throw new Error('Missing message identity');
  }

  const scope = [workspaceKey, event.provider,
    event.account_id, conversation.id];
  return {
    childKey: JSON.stringify([...scope, message.id]),
    albumKey: message.album?.id
      ? JSON.stringify([...scope, message.album.id])
      : null
  };
}

建议按以下事务流程接入:

  1. 按投递契约验证原始请求。启用 signing_secret 后,对时间戳、句点及原始请求体组合验证 HMAC-SHA256,并检查时间戳新鲜度。
  2. 保存事件,以带范围的消息身份建立唯一约束并写入或更新子消息。保留文字说明、附件、sent_at 和已有相册元数据。
  3. 将子消息关联到对应相册。没有相册 ID 就按独立消息保留,不根据相同文字或相邻到达时间猜测归属。
  4. 提交入站记录与持久化任务后再返回 2xx;由 worker 更新视图并单独下载媒体。

使用同一事务或持久化 outbox,避免进程崩溃后出现“消息已存、聚合任务未建”。各子项的文字说明都应保存,不要让最后一次投递覆盖唯一的相册说明。

决定何时处理,但不夸大完整性

以下是应用状态建议,不是平台事件或接口字段。

观察应用处理
total 一致,且不同子项数量已达到该值标记预期数量已达到,媒体就绪状态另算
没有 total按本地设定的静默等待时间生成暂定快照
total 冲突或 index 重复保留所有子消息,标记元数据待核查
处理后又收到新子项更新视图,执行明确的迟到项策略
同一子项再次投递幂等更新,不增加不同子项计数

同一相册的视图更新应串行执行,或使用事务版本检查,否则多个 worker 可能读到同一旧状态并重复创建后续任务。保存每次处理决定及实际包含的子消息身份。迟到项到达时,明确选择刷新界面、补充分析或人工复核,不要自动再次向客户发消息。

保留收到的 album.index 用于展示,但不要凭单个示例推断文档未说明的索引起点。索引缺失或冲突不能成为丢弃子项的理由。等待时长只是延迟策略,不是平台的“相册完成”信号。

媒体就绪状态单独处理

即使消息记录数量齐全,也可能仍有文件不可用。UnifyPort 文档说明附件 URL 是临时地址,超大文件也可能不含 URL。应及时创建下载任务,并分别展示附件成功或失败状态。

下载链接失效恢复指南解释了为什么不能直接用 Telegram Bot API 的文件刷新方法处理 UnifyPort 附件 URL。不要为了等待相册完整而无限推迟保存已经可用的文件。

UnifyPort 是非官方接口,统一事件流不代表各平台元数据完全一致。产品不提供 REST 消息历史读取 API,也不保证重放遗漏载荷。需要原生机器人契约时应保留官方 Bot API,不要在同一个解析器中混用两套结构。

验收与常见问题

建议测试重复子项、乱序、不同会话里的同名相册、缺失总数、冲突元数据,以及处理后迟到的照片。这些是建议测试,不是已执行结果。

可以用 album.id 对消息去重吗?

不可以。多个不同消息会共用它。子消息存储应使用带范围的消息 ID。

total 达到预期是否代表文件已下载?

不是。它在提供时描述分组预期大小,文件持久化是另一项状态。

没有 total 的相册应该丢弃吗?

不应丢弃。保留子消息,按明确的应用策略处理暂定视图,并允许后续补入。

下一步与参考资料

先依据标准事件参考完成子消息存储,再启用相册级自动化。

参考资料核对日期:2026-09-29。

UnifyPort API

让消息接入变成一条稳定的产品管线。

先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。