按需加载 WhatsApp 更早消息:历史请求与异步回调实现
通过 UnifyPort 加载 WhatsApp 更早消息时,应先订阅 conversation.history,再用已保存消息构造 before 锚点发起请求。HTTP 响应不会返回消息正文:202 和 status: accepted 仅表示请求已被接受。可用历史会异步推送。因此,应实现尽力而为的**“加载更早消息”**按钮,而不是宣称能够导出完整档案,或自动判断历史已全部加载的分页循环。
要点
- 当前操作适用于
provider=whatsapp私聊,不适用于群组、频道或whatsapp-protocol。 - 发起请求前,先完成事件订阅与持久化接收。
- 使用真实原生内容消息的 ID、发送时间和方向推进锚点。
- 批次可能重复、延迟、多次到达或完全不到达;没有回调不代表完成。
- 历史消息用于补充时间线,不应触发新消息自动回复。
区分请求响应与历史结果
请求会话历史参考 定义了 POST /v1/accounts/{account_id}/conversations/history/request。它发起异步推送,不是同步返回已存消息的 REST 读取接口。
| 观察结果 | 可以确认什么 | 不能确认什么 |
|---|---|---|
HTTP 202、data.status: accepted | 请求已被接受 | 消息已经送达或操作已经完成 |
HTTP request_id | 可用于排查该次 HTTP 请求 | 任务 ID 或回调关联键 |
conversation.history 携带 data.history.source: on_demand | 收到了按需历史批次 | 这是唯一或最后一个批次 |
| 空批次或较短批次 | 本批次包含的内容 | 可用历史已经结束 |
| HTTP 超时 | 客户端未收到明确响应 | 请求没有产生作用 |
消息账号运行时恢复指南 解决的是恢复连接。这与补充历史不同:无论重新连接还是请求历史,都不能保证补齐中断期间遗漏的全部消息。
先准备接收器,再开放按钮
将 conversation.history 加入端点订阅,同时保留收件箱已有的必要事件。实时消息仍由 message.received 接收。更新现有端点时,按照 webhook 配置参考 操作,不要意外清空 signing_secret。
遵循 推送契约:以 X-Device-Timestamp、一个点和原始请求体组成签名输入,用 HMAC-SHA256 验证 X-Device-Signature。检查时间戳新鲜度,再持久化经过验证的投递,最后返回 2xx。
历史事件需要单独的去重分支。WhatsApp HistorySync 不同分块可能复用顶层事件 ID,因此不能仅因事件 ID 已出现过,就丢弃整个历史批次。在工作区内,应按 provider、account_id、data.conversation.id 和每条 data.messages[].id 合并消息。
不要将这些历史内容送入实时自动回复触发器。今天收到一条旧问题,不代表客户今天重新提问。
构造有效的 before 锚点
从同一消息账号、同一私聊中选择已观察到的消息。锚点需要 message_id、sent_at 和 direction。不能根据会话标题、接收时间或电话号码猜测这些值。
以下是符合文档结构的示例请求体;示例标识应替换为实际保存的值:
{
"conversation_id": "100000000000002@lid",
"before": {
"message_id": "MSG_HISTORY_ANCHOR_001",
"sent_at": "2026-09-28T03:00:00Z",
"direction": "inbound"
},
"limit": 50
}
使用后端保存的 X-Api-Key 调用上述 POST 操作。account_id 必须是一个非空路径段,不含空白或斜杠,也不能包含编码后的斜杠。不要把会话标识放入账号路径段。
继续请求时,从已收到的消息中选择时间最早、锚点字段完整的原生内容消息。排除 type: call 合成记录。下面的 JavaScript 只从已选择并验证的消息构造锚点,不是完整接收器或自动分页任务:
function beforeFromMessage(message) {
if (!message || message.type === 'call' ||
typeof message.id !== 'string' || !message.id ||
typeof message.sent_at !== 'string' ||
!Number.isFinite(Date.parse(message.sent_at)) ||
!['inbound', 'outbound'].includes(message.direction)) {
throw new Error('Select a native content message with complete anchor fields');
}
return {
message_id: message.id,
sent_at: message.sent_at,
direction: message.direction
};
}
注意,type 不会被复制到 before。如果没有有效锚点,应禁用请求并解释原因,而不是使用合成通话记录或虚构更早时间。
在收件箱中表达不确定性
以下是建议的应用行为,不是新增 API 状态:
- **请求前:**保存账号、会话、锚点和本地请求时间。按会话串行处理操作员请求,减少重叠操作。
- **接受后:**显示“请求已接受,等待可用历史”。实时消息继续独立接收。
- **每次收到批次:**按消息粒度幂等合并。保留实时编辑状态和删除屏障,避免旧历史覆盖新状态。
- **收到更早内容后:**允许操作员明确发起下一次请求,使用最早的合格锚点。若没有更早的合格锚点,应显示“尚未收到更早锚点”,而不是“全部历史已加载”。
- **超时或没有回调:**保留不确定状态,继续接受延迟批次。不要自动重发相同请求。
文档没有定义下一页游标或完成状态。account.history.synced 是 HistorySync 单个批次或分块的汇总,不能证明某次按需请求已经结束,更不能证明档案完整。不要把它解释成任务完成事件。
历史中的引用关系也不等于发送能力。历史消息不包含 reply_token;引用回复指南 说明了为什么不能用父消息 ID 替代它。同样,不可用的附件应明确显示不可用,而不是显示成已经下载的文件。
错误处理与验收检查
400 可能是 invalid_request、provider_invalid_request(包括合成通话锚点),或 unsupported_conversation_type。其他 provider 返回 501 unsupported_by_provider。应修正适用范围或输入,而不是进入重试循环。保留 HTTP request_id 用于排查,但不要用它关联回调任务。
上线前,建议测试重复批次、共享同一事件 ID 的不同分块、HTTP 超时后才到达的回调、实时消息与历史交错,以及缺少方向的锚点。这些是建议测试,不是已经取得的结果。
UnifyPort 提供非官方接口。此能力用于尽力补充会话上下文,不提供完整备份、保证重放或任意账号访问。仍需维护自己的授权消息存储与保留策略。
常见问题
202 是否说明更早消息已经获取成功?
不是。它只说明请求被接受。可用消息会通过异步历史事件到达。
能否一直请求,直到返回较短批次?
不能把短批次作为结束条件。批次大小不证明历史完整,自动请求还可能与延迟结果重叠。
能用于 WhatsApp 群组、LINE 或 Zalo 吗?
此操作仅针对 provider=whatsapp 私聊。统一 webhook 结构不代表各渠道拥有相同的历史请求能力。
下一步与参考资料
先按 请求会话历史参考 实现接收器和不确定状态,再启用收件箱按钮。
官方参考核对日期:2026-09-30。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。