/** * Execution Logger — 執行結果寫入 KBDB(fire-and-forget) * * KV 額度事故修復(總管交辦,2026-08-07):舊版寫 ANALYTICS_KV(Workers KV), * key = stats:{workflowId}:{timestamp}(註解寫「避免覆蓋」)⇒ 只增不減、永不覆蓋。 * 封測者 Evan 處理約 690 個檔案,KV 免費層 write 上限 1,000/日被打爆(實測 1,070 write)。 * * KBDB 鐵律(leo 2026-06-14):KBDB=API-as-Wall,零 SQL——任何存取一律走 KBDB 的 HTTP API, * 不准直接對它的 D1 下 SQL。本檔因此**不直連任何 D1**,改 fire-and-forget POST * `{KBDB_BASE_URL}/execution-log/record`(連法/認證頭完全比照既有 recordRecipeStats * 慣例,見 webhook-handlers.ts;儲存/降級實作在 kbdb/src/actions/execution-log.ts)。 * * leo 兩條判準: * ① 執行紀錄是稽核資料 → 搬去 D1(entries 表,rows written 100,000/日,額度是 KV 的 100 倍)。 * ② 不是 n8n、不靠 Execution 計費 → 少記:不留每節點輸入輸出,只留時間/workflow/verdict/ * duration/錯誤訊息/(可得的)目標;成功記最少,失敗多記一點(截斷長度不對稱,見 KBDB 端)。 * * A2 自我降級(門檻與降級邏輯全在 KBDB 端,見 execution-log.ts):D1 額度仍與知識卡共用, * 超過門檻 KBDB 會回報 mode='skip'/'log_failure_only',但**這件事對呼叫端透明**—— * 本函式不管 KBDB 決定寫或不寫,一律 fire-and-forget、永不 throw,workflow 執行不受影響。 */ import type { Bindings, GraphNode } from '../types'; import { kbdbBase } from '../routes/kbdb-proxy'; export interface ExecutionVerdict { workflow_id: string; verdict: 'success' | 'failed'; duration_ms: number; message: string; target?: string; } /** * 從觸發時的 trigger context 擷取這次處理的目標(page_name / path),供「哪些檔沒進去」 * 這種問題答得出來。只認這兩個 key(少記,不做窮舉式欄位挖掘/猜測)。 */ function extractTarget(input?: Record): string | undefined { if (!input) return undefined; const raw = input.page_name ?? input.path; if (raw === undefined || raw === null) return undefined; return typeof raw === 'string' ? raw : JSON.stringify(raw); } /** * 寫入執行結果至 KBDB(fire-and-forget,不阻擋主流程)。 * 由 c.executionCtx.waitUntil() 包裹呼叫。 * * @param nodes 保留參數相容既有呼叫端簽名(原本用來算 component_ids);「不記每節點」 * 是本次修復的明確要求(少記),此參數現不使用。 * @param input 觸發時的 trigger context(可選)——只用來抓 page_name / path 當 target, * 不整包送出(少記:不留每節點輸入輸出,這裡也不例外)。 * @param apiKey 觸發者的租戶(可選,/execute 舊路徑無租戶概念)。 */ export async function writeExecutionVerdict( env: Bindings, workflowId: string, nodes: GraphNode[], verdict: 'success' | 'failed', durationMs: number, message: string, input?: Record, apiKey?: string, ): Promise { void nodes; // 少記:不再從節點算 component_ids,保留參數只為呼叫端相容 try { const { base, headers } = kbdbBase(env); await fetch(`${base}/execution-log/record`, { method: 'POST', headers, body: JSON.stringify({ workflow_id: workflowId, owner_id: apiKey ?? null, verdict, duration_ms: Math.max(0, Math.round(durationMs)), message: message ?? '', target: extractTarget(input) ?? null, }), }); } catch { // fire-and-forget:任何錯誤(含 KBDB 端額度打滿、網路失敗)都吞掉、不影響主流程 } }