前言

KnockersTaskBlocks 完成結構化文件、互動式 HTML跨職能流程圖後,下一個需求是讓 Codex 或其他自動化程式把研究結果寫進 Kanboard 任務。

Kanboard 原生 JSON-RPC 已能建立任務、上傳附件與建立任務關聯,但 Task Document 當時只能由登入後的網頁編輯器透過 Session 與 CSRF 儲存。外部 Agent 即使擁有有效 API Key,也無法用正式介面讀取或寫入內容區塊。

如果改成 SSH 進主機呼叫內部 Model,或讓 Agent 操作瀏覽器表單,雖然可以完成一次任務,卻繞開權限、Revision、Validation 與穩定錯誤合約。這篇記錄我如何把 Task Document 接到 Kanboard 原生 JSON-RPC,同時避免 AI 覆蓋使用者剛完成的修改。

API 不應該建立第二套儲存流程

最危險的做法,是為了 API 方便而直接寫入外掛資料表。

網頁儲存原本已經處理:

  • 專案是否啟用 Task Blocks。
  • 使用者是否能讀取或修改任務。
  • Block migration 與完整 schema validation。
  • 圖片、附件與連結 ownership。
  • Markdown Compatibility Content。
  • Revision history 與保留上限。
  • Optimistic locking 與 database transaction。

如果 JSON-RPC 重新實作一次,兩條路徑很快就會出現差異。API 可能接受網頁不允許的區塊,也可能忘記更新相容內容或繞過自訂角色限制。

因此 JSON-RPC Procedure 只是一個薄 Adapter,真正儲存仍呼叫和網頁編輯器相同的 Task Document service。

只提供三個方法

外部介面刻意保持小型:

getKnockersTaskDocument
saveKnockersTaskDocument
appendKnockersTaskBlocks

get 回傳目前 revision、Canonical Document、Compatibility Content、支援的 block definitions 與 guardrails。

save 完整取代 Task Document,適合已經讀取最新文件並完成合併的 Client。

append 只把新區塊加入文件尾端,不允許藉此修改、刪除或重排既有區塊。它讓 AI 可以安全追加研究筆記,又不需要每個整合都自行實作 document merge。

原生任務仍由 Kanboard 的 createTask 建立,附件和連結也先走原生 procedure。外掛 API 不複製已經存在的大型任務建立介面,只處理自己的 Task Document aggregate。

為什麼每次寫入都需要 expected_revision?

假設使用者和 Agent 同時讀到 Revision 4:

使用者 ──讀取 Revision 4──→ 修改標題
Agent  ──讀取 Revision 4──→ 產生研究內容

使用者 ──儲存成功────────→ Revision 5
Agent  ──仍以 Revision 4 儲存

如果 API 只做最後寫入者勝出,Agent 的完整文件會直接覆蓋使用者變更,而且兩邊都以為自己成功。

因此寫入必須帶上讀取時取得的 expected_revision

{
  "jsonrpc": "2.0",
  "method": "saveKnockersTaskDocument",
  "id": 1,
  "params": {
    "task_id": 42,
    "expected_revision": 4,
    "document": {
      "schemaVersion": 1,
      "blocks": []
    }
  }
}

資料庫更新條件同時比對 Task ID 與 Revision。受影響列數不是一筆時,server 回傳 revision_conflict,不產生部分寫入,也不增加歷史版本。

Client 接到衝突後要重新讀取 Revision 5,理解最新變更,再決定合併或放棄。不能把 retry 寫成原封不動重送,否則 optimistic locking 只剩形式。

Append 仍然需要 Revision

只追加區塊看起來不會改動既有內容,但仍可能和其他操作產生順序與限制衝突。

例如使用者已經加入新段落、另一個 Agent 也正在追加內容,或文件已接近 block 數與 byte limit。Server 需要在同一個 Canonical Document 上確認 Revision、合併新區塊,再執行完整 validation 與 transaction。

Append 的流程是:

  1. 讀取目前 Canonical Document。
  2. 比對 expected_revision
  3. 保留既有 block JSON 與順序。
  4. 將新區塊加入尾端。
  5. 執行完整文件 validation。
  6. 產生 Compatibility Content。
  7. 儲存新 Revision 與 history。

它不是直接對資料表做 array append,而是受限制的 server-side merge。

沿用 Kanboard 的身分與權限

JSON-RPC 使用 Kanboard 原生 HTTP Basic username 與 API Key 驗證,外掛不簽發也不保存第二組憑證。

讀取需要使用者能看見任務所屬專案;寫入則需要和網頁編輯器相同的實際更新權限。自訂角色如果只能修改被指派任務,API 也必須得到同樣結果。

為了避免權限漂移,Web Controller 與 JSON-RPC Adapter 共用同一個 Task Document authorization module。這個模組只回答任務是否存在、專案是否啟用,以及目前使用者能否讀寫,不執行 persistence。

Agent 不能自行宣告附件屬於哪個任務

Task Document 的圖片、附件與內部連結只保存原生資源 ID。API Client 必須先透過 Kanboard procedure 建立資源,再把取得的 ID 放入區塊。

Server 仍會重新確認:

  • File 是否真的屬於目前任務。
  • Internal Link 是否是這筆任務的原生關聯。
  • External Link 是否由目前任務建立。
  • HTML 與流程圖是否符合既有大小和結構限制。

Client 傳入一個數字 ID,不代表它有權引用該資源。Ownership 必須由 server 從資料庫關係判斷。

錯誤需要能讓自動化判斷

人類看到錯誤畫面後可以自行理解,Agent 則需要穩定合約。可預期的 domain failure 使用:

{
  "ok": false,
  "error": {
    "code": "revision_conflict",
    "message": "Document changed before save."
  }
}

主要錯誤包含:

  • task_not_found
  • task_blocks_disabled
  • revision_conflict
  • validation_failed

Authentication 與 authorization failure 維持 Kanboard 標準 JSON-RPC access denied,不把 private exception、stack trace 或資源存在性放進回應。

這不是把 AI 模型裝進 Kanboard

這次外掛只提供穩定的資料介面,不負責:

  • 呼叫模型供應商。
  • 保存 Prompt 或模型回應。
  • 管理 AI 憑證。
  • 自動解決衝突。
  • 讓 Agent 任意 Patch 資料庫欄位。

AI 仍然在外部執行,Kanboard 只提供受權限、版本與 validation 約束的讀寫能力。這讓不同 Agent、Script 或整合服務都可以使用相同合約,而不把產品綁在單一模型上。

如何驗證 API 沒有走捷徑?

最高層驗收不是直接呼叫 PHP class,而是對真正的 jsonrpc.php 發出請求,確認:

  • 方法完成註冊並可被找到。
  • 原生 API Key 驗證生效。
  • 不同專案角色得到正確結果。
  • 空文件以 Revision 0 和標準 envelope 回傳。
  • Replace 與 Append 都產生 Compatibility Content。
  • Revision history 和保留策略正常。
  • 衝突與無效資料不產生部分寫入。
  • API 建立的區塊能在任務頁正常呈現。
  • 憑證沒有進入 fixture、response 或 log。

這個 API 最重要的設計不是 method 數量,而是沒有建立特權寫入路徑。Agent 和人類編輯器最後都經過同一套授權、validation、projection、Revision 與 transaction。

下一篇將處理讀取的另一端:Kanboard 單一任務公開分享開發實錄:Capability URL、輪替與撤銷