← 所有文章
教程

按需加载 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 状态:

  1. **请求前:**保存账号、会话、锚点和本地请求时间。按会话串行处理操作员请求,减少重叠操作。
  2. **接受后:**显示“请求已接受,等待可用历史”。实时消息继续独立接收。
  3. **每次收到批次:**按消息粒度幂等合并。保留实时编辑状态和删除屏障,避免旧历史覆盖新状态。
  4. **收到更早内容后:**允许操作员明确发起下一次请求,使用最早的合格锚点。若没有更早的合格锚点,应显示“尚未收到更早锚点”,而不是“全部历史已加载”。
  5. **超时或没有回调:**保留不确定状态,继续接受延迟批次。不要自动重发相同请求。

文档没有定义下一页游标或完成状态。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。

UnifyPort API

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

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