從外部系統把工作交棒進來(給外部系統的開發團隊)¶
你有一套既有的系統,使用者在上面做完一輪分析,想在這個平台上讓 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. 把使用者送過去¶
請在檔案都上傳完之後才跳轉,這樣使用者一到就看得到完整的東西, 不會看到一個半空的工作區。
五個地雷¶
地雷一:limit 不帶,就等於全撈¶
這邊的 limit 預設值是一個哨兵值(約 42.9 億),不是頁大小。
你忘了帶 limit=100,這支 API 會安靜地把整張表撈給你 —— 不會報錯,只會越來越慢。
每一次都要明確帶 limit。
地雷二:排序用 created_time 會讓收斂失效¶
清單有上限,所以掉出範圍的 item 使用者就看不到 —— 看不到就會再開一個新的, 於是同一題又散開了,正是這整套機制要防的事。
用 updated_time 排序可以大幅減輕這個問題:每交棒一次,那個 item 就會回到最前面。
所以「同一題被交棒好幾次」這個主要情境,第二次以後一定找得到第一次的落點。
不過有個限制要先告訴你,免得你以為它比實際更聰明:只有「記錄編號」那一步會更新時間戳。 上傳檔案不會,使用者在平台上跟 AI 對話、改檔案也不會。 所以一個「不是從你們那邊交棒過來、但有人天天在用」的 item, 仍然會慢慢沉下去。對你們的流程影響不大(你們碰得到的都是交棒過的), 但如果哪天使用者反映「我明明一直在用的案子找不到」,原因就在這裡。
地雷三:permission 不帶,item 生下來只有自己看得到¶
建立 item 時如果沒有明確帶 permission,它預設是私人的。
你不會收到任何錯誤,但你的使用者的同事在清單上看不到這個 item,
於是同事會自己再開一個 —— 同一題又裂成兩個,而且沒有任何徵兆。
所以建立時請明確帶:
但請先讓你們的資安/主管知道 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 筆):
次要排序 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)給維運,設定在:
身分沿用共用登入,所以你不需要、也不應該在參數裡帶使用者 id —— 從瀏覽器打過來,這邊自己就知道是誰。
編號格式¶
external_refs 的每個值請用 <系統代號>:<紀錄編號>,例如 legacy-rca:12345。
平台把它當成不透明字串:只比對,永不解析。<系統代號> 的作用只是讓不同來源系統的編號
不會互撞,你自己取一個穩定的名字即可。