# MIGRATION — workflow 真相源 WEBHOOKS KV → KBDB API(entry_type=workflow) > 分支:`feat/workflow-store-kbdb` SDD:`system-dev/docs/3-specs/arcrun/kbdb-base/design.md` §8.3/§8.4 > 總管裁定(2026-07-06):① upsert 走 KBDB base `PUT /entries`;② cron 掃 KBDB、不保留 KV cron-idx; > ③ 一 workflow = 一筆 `entry_type=workflow`,`content=description`(保 embed),graph+config+cron_expr 進 `metadata_json`。 > > **本 PR 不 merge、不部署、不動 production 那 7 筆 KV workflow。** data 搬遷是下一階段(leo 寫入閘)。 --- ## 1. 這個 PR 改了什麼 ### KBDB(新增能力) - `kbdb/src/actions/entry-crud.ts`:新增 `upsertEntry(db, input)` — by `(owner_id, page_name, entry_type)` read-then-write(無 UNIQUE 約束、表不變鐵律;命中最舊一筆更新,重跑收斂)。 - `kbdb/src/routes/entries.ts`:新增 `PUT /entries`(upsert,回 `{ success, entry, created }`;content 變動時 embed-on-write)。 ### cypher-executor(workflow store + 六路 + 兩路切雙軌) - **新檔** `cypher-executor/src/lib/workflow-store.ts`:`getWorkflow / listWorkflows / putWorkflow / deleteWorkflow / listCronWorkflows`,全走 KBDB base API(`kbdbBase` = KBDB_BASE_URL + 選用 KBDB_INTERNAL_TOKEN),owner_id=namespace 租戶隔離。禁直連 D1/SQL。 - `routes/webhooks-named.ts`: - register:寫 `putWorkflow`(KBDB 為主)+ 暫雙寫 KV;移除 `writeWorkflowSearchEntry`(上收進 putWorkflow, 修掉每次 POST 堆重複 entry 的 bug);移除 KV cron-idx 維護。 - trigger / list / delete:讀先 KBDB、miss fallback KV;list = KBDB ∪ KV(同名 KBDB 為準);delete 兩邊都刪。 - `backfill-search-entries`:升級為 **KV→KBDB 完整遷移入口**(putWorkflow upsert,冪等可重跑)。 - 移除 `migrate-cron-index` 端點(KV cron-idx 已退場)。 - `scheduled.ts`:cron 掃描改 `listCronWorkflows`(掃 KBDB、記憶體濾 cron_expr);不再讀 KV cron-idx。 - `lib/component-loader.ts`(trigger_workflow 子流程)、`routes/executions.ts`(擁有權檢查):切雙軌讀。 - **刪除** `lib/cron-index.ts`(KV cron-idx 模型退場)。 ### 綠燈 - `kbdb` tsc 0 error、vitest 6/6 綠。 - `cypher-executor` tsc 0 error、vitest 41/42 綠(1 個 `executor.test.ts > 不存在的零件` 失敗為 **pre-existing**,main 上同樣失敗,與本 PR 無關——component-not-found 錯誤訊息措辭斷言,非 workflow 路徑)。 --- ## 2. 部署後驗證計畫(對真端點 curl,禁 miniflare 假綠) > 前提:部署到 **leo21c**(auto 模式本輪不做;下述為部署後的驗收步驟)。 > 端點:cypher `https://arcrun-cypher-executor.leo21c.workers.dev`、KBDB `https://arcrun-kbdb.leo21c.workers.dev`。 > **全程只用測試 namespace(如 `ak_wfmigtest`),不碰現有 7 筆 production workflow。** 設 `NS=ak_wfmigtest`、`CY=https://arcrun-cypher-executor.leo21c.workers.dev`、`KB=https://arcrun-kbdb.leo21c.workers.dev`。 1. **KBDB upsert 端點本身** ``` curl -XPUT $KB/entries -H 'content-type: application/json' \ -d '{"entry_type":"workflow","owner_id":"'$NS'","page_name":"t1","content":"desc A","metadata_json":"{\"graph\":{\"id\":\"g\"}}"}' # → { success:true, created:true } # 再打一次(改 content:"desc B")→ { success:true, created:false }(同一 entry 更新,非新增) curl "$KB/entries?entry_type=workflow&owner_id=$NS&page_name=t1" # → 恰 1 筆、content="desc B" ``` 2. **register(部署)→ 恰一筆、redeploy 不重複** ``` curl -XPOST $CY/webhooks/named -H "X-Arcrun-API-Key: $NS" -H 'content-type: application/json' \ -d '{"name":"wf_a","description":"測試工作流","graph":{"id":"wf_a","nodes":[]}}' curl "$KB/entries?entry_type=workflow&owner_id=$NS&page_name=wf_a" # → 恰 1 筆 # 同名再 POST 一次(改 description)→ 仍恰 1 筆、content 更新(驗 upsert 冪等,無重複 entry bug) ``` 3. **list / trigger / delete 走 KBDB** ``` curl $CY/webhooks/named -H "X-Arcrun-API-Key: $NS" # → 含 wf_a,欄位齊 curl -XPOST $CY/webhooks/named/wf_a/trigger -H "X-Arcrun-API-Key: $NS" -d '{}' # → 執行(讀到 KBDB graph) curl -XDELETE $CY/webhooks/named/wf_a -H "X-Arcrun-API-Key: $NS" # → {deleted:true} curl "$KB/entries?entry_type=workflow&owner_id=$NS&page_name=wf_a" # → 0 筆 ``` 4. **cron 掃 KBDB**:部署一個首節點為 cron 的 workflow(cron_expr 每分鐘),等 1–2 分鐘看 `wrangler tail` `[scheduled] scanned N KBDB cron workflows, k triggered` 有觸發。 5. **雙軌 fallback(不寫 KBDB 的舊 KV workflow 仍可 trigger)**:手動塞一筆 `{NS}:wf:legacy` 進 WEBHOOKS KV (只 KV、不進 KBDB),打 `/webhooks/named/legacy/trigger` → 應 fallback KV 成功執行;`GET /webhooks/named` 應同時列出 KBDB 的與這筆 KV-only 的(union)。 **驗收標準**:上述每步 HTTP status + 回傳體符合預期;特別是「redeploy 同名恆為 1 筆」(upsert 生效) 與「cron 從 KBDB 觸發」。缺任一即不算綠,回報而非假綠(mindset §7)。 --- ## 3. 剩餘 data 搬遷步驟(下一階段,leo 寫入閘,本輪別做) 現有 7 筆 production workflow 仍只在 WEBHOOKS KV。雙軌讀讓它們仍可 trigger,但: - **cron 掃描只看 KBDB** → 若這 7 筆裡有 cron workflow,未搬遷前它們的排程會**停止觸發**(⚠️ 高優先)。 - semantic 搜尋也只看 KBDB entry。 **搬遷程序(冪等、可重跑)**: 1. 逐 namespace 呼叫 `POST /workflows/backfill-search-entries`(帶該租戶 `X-Arcrun-API-Key`)。 它 list KV 的 `{ns}:wf:*`、對每筆 `putWorkflow`(upsert)遷進 KBDB。回傳 `backfilled` / `needs_description`。 2. 驗證:`GET $KB/entries?entry_type=workflow&owner_id={ns}` 每筆都有對應 entry(graph 在 metadata)。 3. `needs_description` 清單交操盤 CC re-deploy 時據實補描述(不自動編造)。 4. 全部驗穩後,才由 leo 決定**拆掉 register 的 KV 雙寫 + trigger/list/delete 的 KV fallback**(另一次閘)。 拆除前 KV 是安全網,不要急著拿掉。 > 誰有幾筆、哪些是 cron,需先盤點(`WEBHOOKS.list` 或 console)再排搬遷順序(cron 的先搬)。 --- ## 4. 回滾 本 PR 不動 production 資料,且 register 仍雙寫 KV、讀有 KV fallback → 直接 revert 分支即可回到「KV 為真相源」。 KBDB 端只**新增** `PUT /entries`(不改既有端點行為),revert cypher 側即停用;KBDB 側可留(無副作用)。 唯一不可逆的取捨:本 PR 刪了 `cron-index.ts` 與 `migrate-cron-index` 端點(KV cron-idx 退場); 如需回滾 cron 到 KV,從 git 歷史取回即可。