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
};
}
建议按以下事务流程接入:
- 按投递契约验证原始请求。启用
signing_secret后,对时间戳、句点及原始请求体组合验证 HMAC-SHA256,并检查时间戳新鲜度。 - 保存事件,以带范围的消息身份建立唯一约束并写入或更新子消息。保留文字说明、附件、
sent_at和已有相册元数据。 - 将子消息关联到对应相册。没有相册 ID 就按独立消息保留,不根据相同文字或相邻到达时间猜测归属。
- 提交入站记录与持久化任务后再返回 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。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。