處理 UnifyPort 批次送達與已讀回條
一筆 UnifyPort 送達或已讀回條可能涵蓋多則訊息。有批次欄位時,應處理 data.message.ids 中的每個 ID;單則回條則使用 data.message.id。將回條與訊息內容分開儲存,即使回條先到也不會遺失。傳送請求獲接受、收件人回條,以及團隊收件匣的已讀狀態,是三種不同證據,不能共用一個「成功」標記。
重點摘要
- 按工作空間、通路、訊息帳號及對話比對回條,不能只看訊息 ID。
- 通路回報的回條時間與 webhook 抵達時間分別儲存。
- 晚到的送達回條不能清除已經觀察到的已讀證據。
- 缺少確認代表未知,不代表可以重新傳送。
先確認批次範圍,再更新畫面
標準事件契約將 message.delivered 定義為訊息送達收件人的裝置,將 message.read 定義為收件人已讀回條。批次欄位是 data.message.ids;只更新單數 id 會漏掉其餘訊息。
| 證據 | 能確認什麼 | 不能確認什麼 |
|---|---|---|
傳送回應 status: accepted | 請求已被接受 | 送達或已讀 |
message.delivered | 指定訊息已送達 | 收件人已閱讀 |
message.read | 指定訊息已讀 | 客服案件已解決或所有群組成員已讀 |
| 對話本機已讀狀態 | 已連線帳號的聊天清單狀態 | 收件人回條 |
最後一項屬於獨立的共用收件匣已讀/未讀流程。不要呼叫對話已讀操作來修復缺少的外寄訊息回條。
以下是依據已讀回條文件縮減的說明範例,並非實際客戶事件:
{
"id": "evt_2f9c1a4b7e",
"type": "message.read",
"provider": "whatsapp",
"account_id": "acc_8c21d0",
"occurred_at": "2026-06-08T12:35:10Z",
"data": {
"conversation": { "id": "8613912345678", "type": "user" },
"sender": { "id": "8613912345678", "type": "user" },
"message": {
"id": "wamid.HBgM",
"ids": ["wamid.HBgM", "wamid.HBgN"],
"read_at": "2026-06-08T12:35:10Z"
}
}
}
這筆回條涵蓋清單中的兩則訊息,並不證明更早的訊息都已讀。即使 ID 看起來像數字,也不能自行推導連續範圍。
先儲存觀察紀錄,再產生狀態標記
建議分成兩層:持久化的已驗簽事件紀錄,以及依訊息整理的回條檢視。這是應用程式的儲存設計,不是額外的 UnifyPort 欄位。
- 將兩個回條事件名稱加入訂閱,並保留其他必要事件與簽章設定。
- 按webhook 投遞契約,使用
signing_secret驗證「時間戳記、一個點、原始請求本文」的 HMAC-SHA256,並檢查簽章時間戳記是否過舊。 - 持久化已驗證事件後才回傳 2xx。事件紀錄與持久化工作應以原子方式提交;也可以將檢視更新與去重紀錄放入同一交易。
- 將批次回條展開成逐訊息觀察紀錄。保留工作空間脈絡、
provider、account_id、data.conversation.id、事件 ID,以及可用的傳送者身分。 - 分別儲存
occurred_at、通路的read_at或delivered_at,以及應用程式記錄的抵達時間。回條時間也會鏡像至data層的同名欄位。缺少的時間保持未知,不能拿抵達時間當作通路時間。
以下 JavaScript 只從已驗簽事件擷取訊息範圍,不是完整接收器或資料庫實作:
function receiptMessageIds(event) {
if (!['message.delivered', 'message.read'].includes(event.type)) return [];
const message = event.data?.message;
const ids = message?.ids === undefined ? [message?.id] : message.ids;
if (!Array.isArray(ids) || ids.length === 0 ||
ids.some(id => typeof id !== 'string' || id.length === 0)) {
throw new Error('Receipt scope needs review');
}
return [...new Set(ids)];
}
批次欄位無效時,不要默默改用單數 ID。應保留事件供調查,而不是依猜測的範圍套用更新。
合併證據,不製造送達結論
獨立保留送達與已讀觀察。已讀回條先到時,可以顯示「已觀察到已讀」,但不要編造 delivered_at。後來的送達事件只補充證據,不能把畫面降回「已送達」。重複事件也不應產生重複通知。
若訊息內容尚未存入,先依完整訊息識別保留回條,等到比對成功再附加。不能只憑回條建立收件訊息或增加收件匣計數。可用 occurred_at 與事件 ID 建立確定性順序,但不要採用「最後抵達的狀態覆蓋全部」的邏輯。
傳送時,將回應中的 message_id 與 provider_ref 和本機操作一起儲存。關聯回條前,確認哪個識別碼對應實際觀察到的通路訊息;不同欄位名稱並不保證值永遠相等。無法比對的回條應進入核對佇列,不要依時間就近附加到某則訊息。保留可用的傳送者身分,但不要將一次觀察擴大為「所有人已讀」。
媒體傳送疑難排解指南處理的是來源檔案與請求接受階段。回條處理位於另一層,逾時或沒有回條都不能自動觸發重送。
能力邊界與驗收情境
UnifyPort 使用非官方介面。通路事件矩陣描述解析器對應能力,不保證每個帳號或部署都產生回條。在畫面承諾送達標記前,應核對所選通路,包括 LINE。沒有回條不能證明未讀、未送達或遭封鎖;遺漏的事件也不保證重播。
建議測試:涵蓋兩則訊息的批次回條、單則回條、重複投遞、已讀先於送達、回條先於內容、不同帳號使用相同訊息 ID、無效批次欄位,以及一直沒有回條。確認只有明確列出的 ID 被更新,且沒有任何情境自動重送。這些是待執行測試,不是既有測試結果。
常見問題
應處理 id 還是 ids?
有有效且非空的批次 ids 時使用它;否則使用單數 id。無效範圍應交由調查流程處理。
收到已讀回條後,可以補填 delivered_at 嗎?
不可以。顯示已讀證據即可,不要製造通路送達時間。
沒有回條能把傳送標記為失敗嗎?
單憑這一點不行。保留「確認未知」,檢查通路能力、訂閱、接收流程及識別碼比對。
下一步與參考資料
從送達回條文件開始,先實作有明確範圍的觀察紀錄儲存,再加入畫面狀態。
產品資料核對日期:2026-11-06。
讓訊息接入變成一條穩定的產品管線。
先用統一 API 跑通傳送,再用標準事件把所有入站訊息接回業務系統。