假設現在有一篇主文和兩則預排回覆,AI 都寫好了。畫面看起來只差按下「發」。
但對工作流來說,「發」這個字太模糊了。它可能代表我看過預覽、同意某個版本,也可能只代表平台建立了一個還在處理的容器。等到貼文真的公開,又會拿到另一個識別碼。若第二則回覆要接在第一則下面,還得等第一則發布完成,才能知道要填哪一個父識別碼。
Day 24 處理的是內部架構:固定流程、單一代理或雙代理該怎麼選。當時的答案是證據不足,所以先維持固定流程。但不管內部用了哪種架構,都不會因此取得外部發文權。
今天就拿「一篇主文加兩則回覆」這組合成串文當例子,把送出前後的狀態攤開。這次不接正式帳號、不讀 token,也沒有真的發布。要看的不是平台成效,而是一條內容產線怎麼知道自己走到哪裡,又該在哪裡停下來。
我先把正常路徑拆成六格:
預覽 → 批准 → 建立 → 等待 → 發布 → 回覆
預覽讓我看到準備送出的主文、回覆順序、媒材與目標。這一步只能讀資料,不能碰外部服務。
批准要回答的是:「我同意送出哪一版,以及同意做哪些動作?」如果文字或圖片在批准後又改過,舊批准就不該繼續沿用。
建立會把內容交給平台,取得一個容器識別碼。它只證明平台收到了待處理物件,不代表內容已經公開。等待階段要查遠端狀態,確認容器可以繼續。正式發布後,平台才會回傳內容識別碼。預排回覆則要拿這些新識別碼,逐則建立正確的父子關係。
把這六格分開後,published: true 就顯得太粗了。它沒有告訴我哪一版被批准、平台建立了什麼、等待時看到哪個狀態,也沒有留下下一則回覆應該接到哪裡。
下面直接拿這組合成串文往下走。
這裡有一個很好的反例。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_PROGRESS、FINISHED、PUBLISHED、ERROR 與 EXPIRED。Day 25 的控制器採取一個簡單政策:看到 IN_PROGRESS 就在次數上限內繼續查;只有 FINISHED 可以進入發布;ERROR 或 EXPIRED 立刻停止。
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,沒有繼續發布,也沒有自動重試。這個限制正好帶到下一個問題。
Day 25 保存了 request_id 和 intent_id,仍然不能宣稱重送一定安全。
RFC 9110 的冪等方法章節說明,重複相同請求和執行一次應具有相同預期效果,也限制客戶端任意重試非冪等方法。Google AIP-155示範服務如何用呼叫端提供的 request ID 支援除重與稽核。Stripe 的冪等文件則明確定義同一鍵如何重放第一次結果,以及同一鍵遇到不同參數時怎麼拒絕。
三份文件都有自己的服務契約,不能直接搬成 Threads 的保證。這次核對的 Threads 官方資料沒有承諾 Idempotency-Key 或恰好一次發布。本地多存一個 ID,可以幫控制器辨認意圖,卻不能替外部平台補出沒有寫進契約的行為。
這也是 Day 25 刻意停下來的地方。
今天完成的是已知的正常路徑,以及幾個能在外部呼叫前拒絕的錯誤。如果平台其實已經發布,回應卻在途中遺失,系統就不能只看本地紀錄決定重送。主文成功、其中一則回覆失敗,也不能假裝整批回滾。
Day 26 會接著處理這個外部結果不確定的問題:先對帳還是重試?哪些已成功步驟要保留?哪一段可以接續,哪一段需要補償?