iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0

假設現在有一篇主文和兩則預排回覆,AI 都寫好了。畫面看起來只差按下「發」。

但對工作流來說,「發」這個字太模糊了。它可能代表我看過預覽、同意某個版本,也可能只代表平台建立了一個還在處理的容器。等到貼文真的公開,又會拿到另一個識別碼。若第二則回覆要接在第一則下面,還得等第一則發布完成,才能知道要填哪一個父識別碼。

Day 24 處理的是內部架構:固定流程、單一代理或雙代理該怎麼選。當時的答案是證據不足,所以先維持固定流程。但不管內部用了哪種架構,都不會因此取得外部發文權。

今天就拿「一篇主文加兩則回覆」這組合成串文當例子,把送出前後的狀態攤開。這次不接正式帳號、不讀 token,也沒有真的發布。要看的不是平台成效,而是一條內容產線怎麼知道自己走到哪裡,又該在哪裡停下來。

一個「發」字,其實藏了六種狀態

我先把正常路徑拆成六格:

預覽 → 批准 → 建立 → 等待 → 發布 → 回覆

預覽讓我看到準備送出的主文、回覆順序、媒材與目標。這一步只能讀資料,不能碰外部服務。

批准要回答的是:「我同意送出哪一版,以及同意做哪些動作?」如果文字或圖片在批准後又改過,舊批准就不該繼續沿用。

建立會把內容交給平台,取得一個容器識別碼。它只證明平台收到了待處理物件,不代表內容已經公開。等待階段要查遠端狀態,確認容器可以繼續。正式發布後,平台才會回傳內容識別碼。預排回覆則要拿這些新識別碼,逐則建立正確的父子關係。

把這六格分開後,published: true 就顯得太粗了。它沒有告訴我哪一版被批准、平台建立了什麼、等待時看到哪個狀態,也沒有留下下一則回覆應該接到哪裡。

下面直接拿這組合成串文往下走。

API 可以一步發布,責任還是要分開

這裡有一個很好的反例。Meta 官方 Threads Postman collection列出純文字的 auto_publish_text=true,代表部分內容可以在建立時直接發布。

所以這篇不能寫成「Threads 強制所有貼文都要建立、等待、再發布」。平台確實提供比較短的操作路徑。

不過,API 少一次呼叫,沒有替內容產線回答版本和授權問題。程式還是要知道人工看過哪一版、圖片有沒有跟著換、這次批准是否包含發布回覆,以及平台最後回傳哪個遠端物件。

老實說,這裡最容易混淆:平台 API 的步驟數,和工作流的責任數,不是同一件事。純文字可以一步發布,不代表「準備內容」「批准內容」和「改變外部狀態」適合塞進同一個函式。

Day 25 保留兩階段,是為了讓每一段都能被觀察和拒絕,不是宣稱所有貼文都只能這樣發。

真正被批准的,是一份鎖定版本的請求

一句「可以發」對人很自然,對程式卻不夠。

假設我批准的是第 7 版文字,後來只換了一張圖。畫面看起來仍是同一篇文章,但實際要送出的媒材已經不同。如果批准資料只記一個 approved: true,控制器沒有辦法判斷這次變更是否仍在原本範圍內。

Day 25 因此把發布請求整理成 publication-request.v1。下面只保留最重要的欄位:

{
  "request_id": "pubreq-day25-001",
  "intent_id": "synthetic-day25-normal-path",
  "revision": 7,
  "draft_sha256": "sha256:...",
  "media_manifest_sha256": "sha256:...",
  "target_platform": "fake_threads",
  "requested_capabilities": [
    "create_container",
    "publish_content",
    "publish_reply"
  ]
}

revision 鎖定草稿版本,兩份摘要分別綁住文字和媒材。request_id 辨認這一份請求,intent_id 則表達這一次發布意圖。兩篇內容可以完全相同,卻是兩次真的想發布;反過來說,同一個意圖重送,也不該因為呼叫兩次就多出兩篇文章。

AWS Builders' Library在討論安全重試時,也特別區分呼叫端意圖與只看參數是否相同的判斷。Day 25 借用這個責任切法,但還沒有宣稱 Threads 會替我們除重。

批准資料再綁回同一個 request_id、請求摘要與 revision,並明列允許的能力:

{
  "decision": "approved",
  "request_id": "pubreq-day25-001",
  "request_digest": "sha256:...",
  "valid_for_revision": 7,
  "granted_capabilities": [
    "create_container",
    "publish_content",
    "publish_reply"
  ],
  "fixture_kind": "synthetic-approval-no-real-review"
}

最後一個欄位故意寫得很白。這是合成批准,沒有真人審閱、身分驗證或簽章,不能冒充 Ci 已經同意正式發文。

控制器會在外部呼叫前檢查請求、摘要、版本與能力。只批准 create_container,不能自動擴張成也能發布主文和回覆。NIST SP 800-53 的 AC-6談的是最小權限:只授予完成任務所需的能力。本文把這個原則做成三個可檢查欄位,但它們是本地教學政策,不是 Threads 的原生權限名稱。

建立成功,離「已發布」還有一段路

在本文的兩階段路徑中,建立後取得容器識別碼,不能據此認定內容已公開。系統只知道遠端物件已建立,接下來還要查狀態。

Meta 的 Threads API 疑難排解文件列出 IN_PROGRESSFINISHEDPUBLISHEDERROREXPIRED。Day 25 的控制器採取一個簡單政策:看到 IN_PROGRESS 就在次數上限內繼續查;只有 FINISHED 可以進入發布;ERROREXPIRED 立刻停止。

PUBLISHED 不在 Day 25 的正常發布前路徑裡。若狀態查詢已經看到它,代表外部平台可能早就完成發布,控制器不能再次呼叫發布。它要停下來,把遠端結果交給 Day 26 對帳。

這裡的等待不是「睡五秒,猜它應該好了」。Google 的 Long-running Operations把非同步工作表示成有名稱、完成狀態、結果或錯誤的資源。套回內容產線,就是保存容器識別碼,再查它目前處於哪個狀態。

現有發文程式也有相同的控制流形狀:先檢查主文和回覆,純預覽會在讀取 token 前返回;正式路徑則分開建立容器、等待就緒與發布。多圖貼文還可能先建立多個子容器,所以文章裡的一格「建立」,不等於只呼叫一次 API。

這些程式碼能支持流程怎麼拆,不能反過來證明每次正式發布都成功。

每一步都要留下一張能接續的收據

正式發布後,平台會回傳另一個內容識別碼。它不能證明文章內容正確,也不能證明觸及會變好,但至少能回答:哪一個遠端物件對應這一次操作?

如果工作流只留下 success: true,下一個步驟仍然不知道該用哪個識別碼。Day 25 的逐步收據因此保存:

  • sequence:執行順序。
  • step:目前完成哪個步驟。
  • item_id:主文或哪一則回覆。
  • remote_ref:這一步取得的遠端識別碼。
  • parent_remote_ref:回覆要接在哪個已發布項目下。
  • reason_code:為什麼前進或停止。
  • next_step_eligible:現有證據是否足以進入下一步。

Stripe 的 Request IDs 文件示範的是請求層追查;Threads 回傳的容器或內容識別碼則是資源參照。兩者不是同一種 ID,也都不能單獨證明整串發布完成。真正有用的是把「哪次請求」「哪個活動」「產生哪個遠端物件」串起來。

W3C PROV-DM用實體、活動、代理與衍生關係描述來源脈絡。這個觀念可以幫助我們整理 request、approval、container 和 published item 的關係,但來源紀錄不會自動變成簽章,也不會自動證明內容為真。

目前這種逐步收據只存在 Day 25 的合成稽核。現有正式腳本主要把結果印到標準輸出,不能倒過來寫成它早就具備相同的持久化紀錄。

為什麼兩則回覆不能全部一起送

回到開頭那組主文加兩則回覆。

Meta 的建立回覆文件使用 reply_to_id 指定父內容,再發布回覆容器並取得新的識別碼。Day 25 選擇讓每一則預排回覆接在上一則下面,因此合成結果會形成這條鏈:

main     → fake-post-0001 → parent null
reply-1  → fake-post-0002 → parent fake-post-0001
reply-2  → fake-post-0003 → parent fake-post-0002

reply-2 需要 fake-post-0002,而這個值要等 reply-1 發布後才會出現。它們之間有資料依賴,不能為了看起來比較快就完全平行送出。

若產品設計是讓所有回覆都直接掛在主文下面,根貼文識別碼一取得,後面的回覆就不再依賴彼此。這不是哪個架構永遠比較好,而是資料關係不同,控制流也要跟著變。

先用假客戶端把錯誤路徑走完

正式帳號不適合拿來測舊批准、能力不足和容器錯誤。這次用的純記憶體假客戶端只做三件事:建立合成容器、回傳預設狀態,以及把已就緒容器換成合成貼文識別碼。

它不連網、不讀 token,也不負責重試、對帳或補償。

六組固定合成案例得到以下結果:

案例 結果 假客戶端呼叫
只有預覽 previewed 0
缺少批准 blocked 0
批准綁到舊摘要或版本 blocked 0
批准能力不足 blocked 0
容器回傳錯誤 stopped 2
主文加兩則回覆完成 completed 9

六組結果 6/6 對上作者定義的答案,合計 11 次假客戶端呼叫。完整案例建立三個合成容器,並發布三個合成項目。

這些數字只能驗控制器分支和收據。6/6 不是正式平台成功率,11 次也不是效能基準。這次參考實作宣告的真實模型、網路與正式發布呼叫都是 0。

前四個案例反而最值得看。純預覽、缺批准、舊批准與能力不足,全都在假客戶端之前停止。若權限檢查放到建立容器之後,即使程式最後顯示「禁止發布」,外部狀態其實已經被改動。

容器錯誤案例只做一次建立和一次狀態查詢,接著停在 container_error,沒有繼續發布,也沒有自動重試。這個限制正好帶到下一個問題。

request_id 不是防止重複發布的魔法

Day 25 保存了 request_idintent_id,仍然不能宣稱重送一定安全。

RFC 9110 的冪等方法章節說明,重複相同請求和執行一次應具有相同預期效果,也限制客戶端任意重試非冪等方法。Google AIP-155示範服務如何用呼叫端提供的 request ID 支援除重與稽核。Stripe 的冪等文件則明確定義同一鍵如何重放第一次結果,以及同一鍵遇到不同參數時怎麼拒絕。

三份文件都有自己的服務契約,不能直接搬成 Threads 的保證。這次核對的 Threads 官方資料沒有承諾 Idempotency-Key 或恰好一次發布。本地多存一個 ID,可以幫控制器辨認意圖,卻不能替外部平台補出沒有寫進契約的行為。

這也是 Day 25 刻意停下來的地方。

今天完成的是已知的正常路徑,以及幾個能在外部呼叫前拒絕的錯誤。如果平台其實已經發布,回應卻在途中遺失,系統就不能只看本地紀錄決定重送。主文成功、其中一則回覆失敗,也不能假裝整批回滾。

Day 26 會接著處理這個外部結果不確定的問題:先對帳還是重試?哪些已成功步驟要保留?哪一段可以接續,哪一段需要補償?

參考資料


上一篇
Day 24|多一個 AI,真的會做得更好嗎?
下一篇
Day 26|發到一半失敗,系統怎麼知道哪一步已經成功?
系列文
情報進來,內容發出去:30 天拆一條每天真的在跑的 AI 內容產線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言