跳轉到

從外部系統把工作交棒進來(給外部系統的開發團隊)

你有一套既有的系統,使用者在上面做完一輪分析,想在這個平台上讓 AI 接力往下做。 這頁講你要打哪幾支 API、以及三個會讓你安靜踩到的地雷。

平台這邊不需要為你新增任何端點。 下面用到的全部是現成的路由,你只要照順序打。

先講清楚這件事的形狀

一個真實問題,在你那邊常常被拆成好幾筆分析;在這邊我們希望它們收斂成同一個工作項目 (item),這樣 AI 才有完整的上下文可以接力。

哪幾筆算同一題,只有人知道 —— 你那邊沒有欄位可以把它們串起來,所以平台不會替你猜。 正確的作法是:讓使用者在你的畫面上挑要接到哪個既有 item,或開一個新的。

規則 意思
多對一 多筆你那邊的分析,可以收斂到同一個 item
同 item 不重複 同一筆分析已經進過某個 item,就不要再進一次
跨 item 不限制 同一筆分析要同時掛到兩個不同 item,是允許的

你要做的四件事

以下路徑都在 /api 底下。RCA app 的資料模型路徑是 rca-investigation; 其他 app 各有各的模型名稱,可以從 /api/openapi.json 查到。

1. 列出候選 item(給使用者挑)

GET /api/rca-investigation
      ?limit=100
      &sorts=[{"type":"meta","key":"updated_time","direction":"-"},
              {"type":"meta","key":"resource_id","direction":"+"}]

回來長這樣(外層是信封,data 是 item 本身):

[
  {
    "data": {
      "title": "烤箱溫度飄移",
      "external_refs": ["legacy-rca:12345", "legacy-rca:12346"],
      "severity": "P1",
      "status": "triaging"
    },
    "revision_info": {
      "resource_id": "rca-investigation:0f3c…",
      "revision_id": "rca-investigation:0f3c…:7"
    }
  }
]

兩個欄位你後面都會用到:

  • revision_info.resource_id 就是 item 的 id,第 2、3 步全部要用它。
  • revision_info.revision_id 是這一版的版本號,第 2a 步記錄編號時要帶(見地雷四)。

不要打 /api/rca-investigation/data 那支只回 data 本體、沒有 id, 你會做出一個畫得出清單、卻無法對使用者點的那一列做任何事的畫面。

「這筆分析是不是已經進過某個 item」你在自己的前端就能判斷完, 不用多打任何一支 API:把使用者這次要交棒的編號,拿去比對每筆 data.external_refs 就好。 已經進過的那個 item,就在畫面上標示或停用。

排序請照上面帶兩個 key:主要用 updated_time(最後被動過的排最前面, 理由見地雷二),次要用 resource_id。次要那個是為了讓「同一次查詢」的順序有定數 —— 兩筆時間戳一樣時不會每次重新整理都跳來跳去。

但它擋不住翻頁漏項,這點請務必看地雷五。

2a. 使用者挑了既有 item → 上傳檔案,再記一筆

上傳(原始位元組直接放 body,不是 multipart):

PUT /api/a/rca/items/{item_id}/files/legacy-rca-12346/readings.csv
Content-Type: application/octet-stream
<檔案內容>
→ 204 No Content

建議每一筆分析放進自己的資料夾,名字用 <系統代號>-<編號>/(上面例子的 legacy-rca-12346/)。 平台不強制,但同一個 item 會累積好幾筆交棒,沒有前綴的話兩筆分析只要有同名檔案就會互相覆蓋, 而且事後看不出哪個檔案屬於哪一筆。

路徑只有一個限制:不能有 .. 這一段(會回 400)。 檔名裡有點沒關係(report..final.csv 可以),只有整段是 .. 才擋。

其他回應碼:400 路徑含 ..404 item 不存在或你完全看不到它、 403 你看得到這個 item 但沒有寫入權限(或路徑落在唯讀的 .readonly/ 底下)、 410 item 已被刪除、413 單檔超過大小上限、507 工作區容量不足。

記錄這個 item 已經吸收了這筆分析。這一步請照下面的三個步驟做,不要只送一個 PATCH

① 先讀目前狀態
GET /api/rca-investigation/{item_id}
→ {"data": {"external_refs": [...]}, "revision_info": {"revision_id": "...:7"}}

② 如果編號已經在 data.external_refs 裡 → 什麼都不用做,結束
   (這一步讓整個動作可以安全重試,見地雷四)

③ 帶著剛剛讀到的版本號送出追加
PATCH /api/rca-investigation/{item_id}?expected_revision_id=<剛剛的 revision_id>
Content-Type: application/json

[{"op": "add", "path": "/external_refs/-", "value": "legacy-rca:12346"}]

→ 200  成功
→ 412  在你讀完到寫入之間有別人先寫了 → 回到 ① 重做(重試上限建議 20 次)

add/-不要把整份 external_refs 重送一次 —— 那會直接蓋掉別人的紀錄。

expected_revision_id 也可以改用 HTTP 標準的 If-Match 標頭,效果相同。

2b. 使用者要開新的 item

POST /api/a/rca/items
Content-Type: application/json

{
  "title": "烤箱溫度飄移",
  "permission": {"visibility": "public"},
  "severity": "P1"
}
→ {"resource_id": "rca-investigation:...", "seeded": ["/SOP.md", ...]}

拿到 resource_id 之後,接下來完全照 2a 做:先逐檔上傳,最後才記錄編號。

建立時請不要順手帶 external_refs 這個欄位在建立時就能填,但別用 —— 如果編號在檔案之前就寫進去,而你的程式在上傳途中掛掉(網路斷、瀏覽器被關), 就會留下一個宣稱已經收了這筆分析、實際上一個檔案都沒有的 item。 更糟的是使用者下次再按,畫面會因為編號已經在裡面而把它標成「已經進過了」, 於是這筆分析再也補不進去。 先傳檔、後記編號,中途掛掉就只是「還沒記錄」,重來一次就好 —— 檔案路徑一樣,覆蓋上去即可。

3. 把使用者送過去

https://<平台網址>/a/rca/{item_id}

請在檔案都上傳完之後才跳轉,這樣使用者一到就看得到完整的東西, 不會看到一個半空的工作區。

五個地雷

地雷一:limit 不帶,就等於全撈

這邊的 limit 預設值是一個哨兵值(約 42.9 億),不是頁大小。 你忘了帶 limit=100,這支 API 會安靜地把整張表撈給你 —— 不會報錯,只會越來越慢。 每一次都要明確帶 limit

地雷二:排序用 created_time 會讓收斂失效

清單有上限,所以掉出範圍的 item 使用者就看不到 —— 看不到就會再開一個新的, 於是同一題又散開了,正是這整套機制要防的事。

updated_time 排序可以大幅減輕這個問題:每交棒一次,那個 item 就會回到最前面。 所以「同一題被交棒好幾次」這個主要情境,第二次以後一定找得到第一次的落點。

不過有個限制要先告訴你,免得你以為它比實際更聰明:只有「記錄編號」那一步會更新時間戳。 上傳檔案不會,使用者在平台上跟 AI 對話、改檔案也不會。 所以一個「不是從你們那邊交棒過來、但有人天天在用」的 item, 仍然會慢慢沉下去。對你們的流程影響不大(你們碰得到的都是交棒過的), 但如果哪天使用者反映「我明明一直在用的案子找不到」,原因就在這裡。

地雷三:permission 不帶,item 生下來只有自己看得到

建立 item 時如果沒有明確帶 permission,它預設是私人的。 你不會收到任何錯誤,但你的使用者的同事在清單上看不到這個 item, 於是同事會自己再開一個 —— 同一題又裂成兩個,而且沒有任何徵兆。

所以建立時請明確帶:

"permission": {"visibility": "public"}

但請先讓你們的資安/主管知道 public 實際的意思:它不只是「大家看得到」, 而是大家都能動。實測任何登入者都可以對一個 public 的 item: 上傳/覆蓋檔案(204)、改標題(200)、甚至改掉負責人欄位(200); 只有刪除擋得住(403,僅限建立者)。跟 AI 對話、執行程式的權限也一樣是開的。

現階段這是刻意的選擇 —— 先讓收斂成立、上線好排查 —— 之後會收緊。 如果你們的分析內容含客訴、良率或供應商資訊,這件事要先講清楚再上線。

地雷四:單獨一個 PATCH 會安靜地掉紀錄

記錄編號那一步在伺服器端是「讀出來 → 改 → 寫回去」。兩個人幾乎同時把各自的分析 交棒到同一個 item,兩個請求都會回 200,但其中一筆編號就這樣消失了。

這不是小事:使用者是靠這份清單判斷「這筆進過了沒」, 掉了一筆就等於「還沒進過」→ 同一份分析被交棒第二次 → 又多一個 item。

所以第 2a 步要照那三個步驟做:先讀版本號 → 帶著版本號寫 → 遇到 412 就重來

重試是安全的,因為步驟 ② 會先檢查編號在不在。這點很重要 —— add 這個寫法本身不是冪等的,你如果逾時了直接重送同一個 PATCH(很自然的作法), 就會把同一個編號記兩次。有步驟 ② 擋著,整個動作重做幾次都沒關係。

還有一段我們自己還沒解決的殘餘風險,先告訴你:平台底層的版本檢查是 「先比對、再寫入」,中間沒有交易保護,所以在多台伺服器的正式環境下, 即使照上面做,仍有很小的機率兩個寫入都通過檢查而掉一筆。 重試把窗口縮得很小但沒有完全關掉。 目前的建議是:跳轉過去之後如果使用者發現編號沒記到,再按一次就好 —— 因為步驟 ② 的關係,重按不會產生重複。我們正在往上游追這個問題。

地雷五:用 offset 往下翻頁會漏掉 item(這條先前我們寫錯了)

先更正一件事:這份文件之前說「帶上 resource_id 次要排序,翻頁就不會跳過 item」。 那是錯的,我們實測後把它收回。

原因是 updated_time 會變。你翻第二頁的那一刻,如果有人剛好交棒到一個原本在第二頁的 item, 它會跳到第一頁的位置,把整個視窗往後推 —— 於是有一筆從第一頁滑到第二頁的空隙裡,兩頁都看不到它。 實測(每頁 2 筆、共 4 筆):

原本順序        : d, c, b, a
第一頁 offset=0 : d, c
   ← 這時有人交棒到 a
第二頁 offset=2 : c, b        ← c 重複出現,而 a 從頭到尾沒出現過

次要排序 resource_id 解決的是「時間戳相同時順序不定」,不是這個。這兩件事常被混為一談。

怎麼辦:

  • 主要情境不需要翻頁。 你要找的是「最近交棒過的案子」,那一定在第一頁 —— 每交棒一次它就回到最前面。limit=100 對這個用途綽綽有餘,這也是我們建議的預設做法。
  • 真的要往下翻,就整個換成不會變的排序鍵重新查: sorts=[{"type":"meta","key":"created_time","direction":"-"},{"type":"meta","key":"resource_id","direction":"+"}]created_time 是建立時間、永遠不動,所以翻幾頁都不會漏。 代價是順序變成「依建立時間」,最近交棒的不會浮上來 —— 所以請當成「瀏覽全部」的模式, 跟第一頁的「最近在動的」分開看待。

不要試圖在同一次翻頁流程裡混用兩種排序,那會同時拿到兩者的缺點。

一件請你不要做的事

不要拿 external_refs 當查詢條件(例如想問「這個編號被哪些 item 收過」)。

這個欄位刻意沒有建索引,所以對它下條件時,條件會在進資料庫之前就被丟掉: 你會拿到 200,然後是一份空清單。不會報錯,不會有任何徵兆。

而空清單正好是這裡最糟的答案。你問的是「這個編號被哪些 item 收過」, 拿到空的,你的畫面就會顯示「沒有人收過」→ 使用者於是開一個新的 item → 同一題又多一份。這正是整套機制要防的事,卻由一個看起來很正常的查詢造成。

正確做法就是第 1 步:撈一頁回去,在你自己的前端比對。

平台這邊要配合的一件事

你的網頁要能打這邊的 API,我們必須先把你的網域加進白名單,否則瀏覽器會在請求送出前就擋掉。 請把你的來源網址(例如 https://legacy-rca.corp)給維運,設定在:

server:
  cors_allowed_origins:
    - "https://legacy-rca.corp"

身分沿用共用登入,所以你不需要、也不應該在參數裡帶使用者 id —— 從瀏覽器打過來,這邊自己就知道是誰。

編號格式

external_refs 的每個值請用 <系統代號>:<紀錄編號>,例如 legacy-rca:12345

平台把它當成不透明字串:只比對,永不解析。<系統代號> 的作用只是讓不同來源系統的編號 不會互撞,你自己取一個穩定的名字即可。