包包baobaolin.com
date
entry
014
topic
integration
rev

Threads API 的兩階段發布:為什麼串文不能批次送

建容器和發布是兩個呼叫,中間要等;而串接下一篇需要的是「已發布的 post ID」,不是容器 ID。三個限制疊起來,一串十篇的 thread 就成了一條無法平行化、也沒有交易保護的鏈。

Threads 的發文 API 是兩階段的。先建一個「媒體容器」,再把容器發布出去:

# 1. 建容器
curl -s -X POST "https://graph.threads.net/v1.0/$USER_ID/threads" \
  --data-urlencode "media_type=TEXT" \
  --data-urlencode "text=第一串的內容" \
  --data-urlencode "access_token=$TOKEN"
# → {"id":"<CONTAINER_ID>"}

# 2. 發布
curl -s -X POST "https://graph.threads.net/v1.0/$USER_ID/threads_publish" \
  --data-urlencode "creation_id=$CONTAINER_ID" \
  --data-urlencode "access_token=$TOKEN"
# → {"id":"<POST_ID>"}

分兩階段是有道理的:附圖的容器需要伺服器端去下載那張圖、驗證、轉檔。這件事不會在你的 POST 回來之前做完,所以 API 給你一個容器 ID,讓你稍後再確認。

三個限制疊在一起

單獨看每一條都很合理,疊起來就變成一條嚴格序列:

  1. 建完容器不能立刻發布。要留時間給伺服器處理,實務上至少三十秒
  2. reply_to_id 吃的是 post ID,不是容器 ID。用容器 ID 會失敗
  3. 因此 不能先把所有容器建好再一起發——第二串的容器需要第一串發布之後才拿得到的 ID

於是每一串都要走完整條路,才能開始下一串:

建容器 → 等 30 秒 → 發布 → 拿到 post ID → 下一串帶著它建容器 → ...
curl -s -X POST "https://graph.threads.net/v1.0/$USER_ID/threads" \
  --data-urlencode "media_type=TEXT" \
  --data-urlencode "text=第二串的內容" \
  --data-urlencode "reply_to_id=$PREV_POST_ID" \
  --data-urlencode "access_token=$TOKEN"

十串的 thread,光是等待就三百秒起跳。這不是效能問題——是它決定了你的腳本必須長成什麼樣子。

真正的問題:這條鏈沒有交易

一個五分鐘的迴圈,中間任何一步都可能失敗:token 過期、圖床暫時掛掉、rate limit、網路斷。

關鍵在於失敗的時候,前面已經發出去的串不會回滾。第六串失敗時,前五串已經公開在使用者的動態上,帶著一個沒有結尾的段落。這跟資料庫的批次作業完全不同——那裡失敗可以整批回滾,這裡不行,因為每一次 publish 都是對外可見的事實。

所以腳本的形狀不能是「跑一次,成功或失敗」,而要是一個可以續跑的狀態機:

  • 每發布成功一串,就把 post ID 寫進一個檔案(或任何能存活到下次執行的地方)
  • 重跑時從記錄的最後一串繼續,不要從頭
  • 失敗時把「已經發到第幾串」印出來——這是重跑唯一需要的資訊
STATE=.thread-state
LAST_ID=$(tail -n1 "$STATE" 2>/dev/null | cut -f2)
START=$(( $(wc -l < "$STATE" 2>/dev/null || echo 0) + 1 ))

for i in $(seq "$START" "$TOTAL"); do
  ... 建容器 / 等待 / 發布 ...
  printf '%s\t%s\n' "$i" "$POST_ID" >> "$STATE"   # 成功才記錄
done

這跟金流回調要冪等是同一個問題的另一面:那邊處理的是「同一件事被送兩次」,這邊處理的是「一連串事情做到一半」。兩者都源於同一個事實——外部系統不提供交易,你得自己在邊界上補。

圖片必須先公開存在

API 不吃本地檔案。附圖的容器要給一個公開可存取的 URL,伺服器會自己去抓:

curl -s -X POST "https://graph.threads.net/v1.0/$USER_ID/threads" \
  --data-urlencode "media_type=IMAGE" \
  --data-urlencode "image_url=https://example.com/pic.jpg" \
  --data-urlencode "text=圖說" \
  --data-urlencode "access_token=$TOKEN"

限制:JPEG 或 PNG、8 MB 以內、寬度 320–1440 px、sRGB。

這個設計有一個容易被忽略的後果:你的圖床變成發文流程的相依項。如果那個圖床當下不可用,容器建得出來但發布會失敗——而失敗訊息通常只說媒體處理失敗,不會說是抓不到圖。免費圖床尤其要留意,它們關閉服務的方式往往是安靜地停掉上傳。

幾個小一點的坑:文字上限 500 字元,超過要拆串;空行要用 --data-urlencode 傳才會保留,直接串進 URL 的話 \n\n 會不見。

Token 六十天後會過期

長期 token 的效期是六十天。這件事單獨看沒什麼,放進「排程自動發文」的情境就變成一顆定時炸彈——它會在你早就忘記這個腳本存在的時候壞掉,而且症狀是安靜的:排程跑了、沒有東西發出去。

兩個成本很低的處置:

  • 把 token 的取得日期記下來,並在到期前排一個提醒
  • 腳本開頭先驗證 token 還活著再開始跑迴圈,失敗就明確報錯——這跟改 DNS 前先驗 token 是同一個習慣

另外,token 本身是憑證,不該放進 .env 之外還跟著專案目錄到處跑。同一篇文章裡的判準適用:只要它出現在會被複製的地方,就當成會外流。

如果只記得一件事

任何「建立 → 等待 → 確認」形狀的 API,都要當成狀態機來寫,不能當成一個函式呼叫。 因為中間那個等待代表工作在別人的伺服器上進行,而別人的伺服器不會為了你的迴圈維持一致性。你能做的只有:每完成一步就記下來,然後讓重跑能接得上。

修訂紀錄

  1. 首次發布