feat(kbdb): 執行紀錄保留期可設定(P7,leo 08-08 confirm)
背景:08-07 事故修復(60688c3)已把執行紀錄從 KV 搬到 KBDB/D1(entries 表,
API-as-Wall),解掉「稽核資料放在會揮發、被額度打斷的地方」這個結構性錯誤,
也順帶把 Evan 撞到的 1,070 次寫入牆退到 D1 額度層級。但那次修復留了一個誠實
的缺口:MCP list_recent_executions 的說明文字寫著「無固定保留期」——保留期
可設定這件事還沒做。本次補上。
P7 規格(system-dev/docs/3-specs/pending-changes.md「P7」,leo 08-08 confirm):
執行紀錄是稽核資料,預設保留 90 天(3 個月)過期即清;租戶可自訂天數,也可
設「不刪除」(企業稽核,leo:「我願意花很多錢保存,不要刪除」)。
實作(kbdb/src/actions/execution-log.ts,牆內):
- getRetentionDays/setRetentionDays:沿用 execution_log_usage 的 upsert 慣例,
單一 entries 列/租戶(entry_type='execution_log_retention_config'),零建表。
- cleanupExpiredLogs:分兩段掃——有自訂天數的租戶各自 cutoff;其餘(含無租戶)
套預設 90 天,排除「不刪除」與已處理過的租戶。每次呼叫界限刪除量
(CLEANUP_BATCH_LIMIT=500),長期多次呼叫可逐步清完累積量。
路由(kbdb/src/routes/execution-log.ts):GET/PUT /execution-log/retention、
POST /execution-log/cleanup,沿用既有的 Bearer token 全域守衛(fail-closed)。
清理觸發(cypher-executor/src/scheduled.ts):不新增排程基礎設施(wrangler.toml
[triggers] 是受保護檔案)——搭 cypher-executor 既有的每分鐘 cron tick 便車,
固定 UTC 02:30 那一分鐘 fire-and-forget 打一次 KBDB 的 cleanup 端點,一天一次,
不是輪詢。
Portal 薄殼(cypher-executor/src/routes/portal.ts):GET/PUT
/portal/admin/execution-log-retention(role=admin 閘),讓本實例的租戶
(portalTenant)能實際設定保留天數,不只是 KBDB 內部端點。
測試(kbdb/tests/execution-log.test.ts):新增 27 個測試(含原有測試共 27 通過
於本檔),真 SQLite 驗證 cutoff 邏輯、自訂天數隔離、「不刪除」永不清、壞資料
容錯、混合租戶情境、路由層 400/200。測試治具需要「插入指定 created_at 的過期
紀錄」這個正式寫入路徑刻意不開放的能力,做成 kbdb/src/actions/execution-log.ts
內匯出的 testInsert*/testCount* 函式(牆內執行 SQL),測試檔本身零原生 SQL。
量測(不是推論):youlin(yuga3bse)實例上,redeploy 後對 graph_neighbors
webhook 發送 1,200 次併發請求(超過 Evan 實測失敗的 1,070 次)——全部 HTTP 200;
ANALYTICS_KV 的 key 數量在請求前後維持 663 不變,證明新寫入路徑完全不碰 KV,
Evan 撞到的那道牆的成因已被物理移除,不只是延後。
讀取端驗證(真呼叫,非 curl):透過綁定 yuga3bse 的 MCP 連線實際呼叫
arcrun_list_recent_executions(回傳含本次量測寫入的 D1 紀錄)與
arcrun_get_execution_trace(正確回 404 not_found,非崩潰);portal 前端
(https://arcrun-rag-ui.youlin-hsieh-dev.workers.dev/portal)瀏覽器實際載入,
無 console 錯誤、無異常紅色橫幅。
舊資料:KV 裡既有的 stats:* 沿用 60688c3 的既有決定——不搬移,任其依現有 90
天 TTL 自然過期(那是統計快取不是真相源);新的 D1 execution_log 保留政策只
管新資料,不回溯處理。
部署:cypher-executor + kbdb 已手動部署到 youlin(yuga3bse,AI 測試場,leo
08-08 令),未動 prod(uncle6)。本次僅程式碼行為變更、無新增/修改 D1 binding、
無新表——三個既有 D1 binding(CREDENTIALS_DB×2+kbdb DB)維持原樣,未新增第四個。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1192,6 +1192,44 @@ portalRouter.get('/portal/admin/ai', (c) =>
|
||||
}),
|
||||
);
|
||||
|
||||
// GET /portal/admin/execution-log-retention — 讀本實例的執行紀錄保留期設定(P7,role=admin 閘)。
|
||||
// retention_days: number=自訂天數;null=已設「不刪除」(企業稽核);未設定過的租戶也回一個值
|
||||
// (KBDB 端會退回預設 90 天,見 kbdb/src/actions/execution-log.ts DEFAULT_RETENTION_DAYS)。
|
||||
portalRouter.get('/portal/admin/execution-log-retention', (c) =>
|
||||
run(c, async () => {
|
||||
const auth = await requirePortalAdmin(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const ownerId = portalTenant(c.env);
|
||||
const res = await kbdbFetch(c.env, `/execution-log/retention?owner_id=${encodeURIComponent(ownerId)}`);
|
||||
if (!res.ok) throw new KbdbError(`GET /execution-log/retention → ${res.status}`);
|
||||
const data = (await res.json()) as { retention_days?: number | null; default_days?: number };
|
||||
return c.json({ success: true, retention_days: data.retention_days ?? null, default_days: data.default_days ?? 90 });
|
||||
}),
|
||||
);
|
||||
|
||||
// PUT /portal/admin/execution-log-retention — 設定保留天數(P7,role=admin 閘)。
|
||||
// body: { retention_days: number|null }。null=不刪除(leo 08-07:「我願意花很多錢保存,
|
||||
// 不要刪除」,這是稽核用途的付費理由,不是成本負擔);正整數=自訂天數,覆蓋預設 90 天。
|
||||
portalRouter.put('/portal/admin/execution-log-retention', (c) =>
|
||||
run(c, async () => {
|
||||
const auth = await requirePortalAdmin(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const body = (await c.req.json().catch(() => null)) as { retention_days?: number | null } | null;
|
||||
const days = body?.retention_days;
|
||||
if (days !== null && days !== undefined && (typeof days !== 'number' || !Number.isFinite(days) || days <= 0)) {
|
||||
return c.json({ error: 'retention_days 必須是正整數,或 null(代表不刪除)' }, 400);
|
||||
}
|
||||
const ownerId = portalTenant(c.env);
|
||||
const res = await kbdbFetch(c.env, '/execution-log/retention', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ owner_id: ownerId, retention_days: days === undefined ? null : days }),
|
||||
});
|
||||
if (!res.ok) throw new KbdbError(`PUT /execution-log/retention → ${res.status}`);
|
||||
const data = (await res.json()) as { retention_days?: number | null };
|
||||
return c.json({ success: true, retention_days: data.retention_days ?? null });
|
||||
}),
|
||||
);
|
||||
|
||||
// DELETE /portal/admin/libraries/by-name/:name — 移除 auto 庫(只有資料章記、無登記簿 record)。
|
||||
// 語意:把該庫的所有 entries 標 deprecated → 資料不刪、重新 ingest 可還原。
|
||||
// ⚠️ 影響資料可搜性,要求 body.confirm 等於庫名才執行(二次確認)。
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 2. 在記憶體比對每筆 cron_expr 跟 event.scheduledTime(UTC 分鐘精度)
|
||||
* 3. 匹配才去讀完整 workflow record({apiKey}:wf:{name})
|
||||
* 4. 匹配 → executeWebhookGraph 跑(waitUntil 背景,不擋)
|
||||
* 5. 每天固定一分鐘(UTC 02:30)順便叫 KBDB 清一批過期執行紀錄(P7 保留期,見下方 §5)
|
||||
*
|
||||
* 8.P0 止血(SDD §8.2):原本每分鐘 WEBHOOKS.list('cron-idx:') = 1440 list/日 爆 KV 上限,
|
||||
* 改成單一固定 key 只 get 一次 → list 歸零。
|
||||
@@ -18,6 +19,7 @@ import type { Bindings } from './types';
|
||||
import { cronMatch } from './lib/cron-match';
|
||||
import { readCronIndex, parseCronEntryKey } from './lib/cron-index';
|
||||
import { executeWebhookGraph } from './actions/webhook-handlers';
|
||||
import { kbdbBase } from './routes/kbdb-proxy';
|
||||
|
||||
type StoredWorkflowRecord = {
|
||||
graph: Record<string, unknown>;
|
||||
@@ -73,4 +75,22 @@ export async function handleScheduled(
|
||||
);
|
||||
}
|
||||
console.log(`[scheduled] scanned ${entries.length} cron-idx entries, ${triggered} triggered`);
|
||||
|
||||
// §5 P7 保留期清理(2026-08-09):不新增排程基礎設施(wrangler.toml [triggers] 是受保護
|
||||
// 檔案,AI 不可編輯——見 InkStoneCo 頂層 pending-changes.md P9 段 L1 權限閘),改「搭便車」:
|
||||
// 這支 handler 本來就每分鐘醒一次(給上面的 cron workflow 用),挑固定一分鐘(UTC 02:30,
|
||||
// 避開整點/半點常見的 cron 表達式擁擠時段)順手打一次 fire-and-forget 給 KBDB 的
|
||||
// POST /execution-log/cleanup。頻率仍是「一天一次」,不是輪詢外部系統要狀態,是既有 tick
|
||||
// 順手打理自己的表。呼叫失敗不影響上面的 cron workflow 觸發(各自 try/catch,互不拖累)。
|
||||
if (now.getUTCHours() === 2 && now.getUTCMinutes() === 30) {
|
||||
const { base, headers } = kbdbBase(env);
|
||||
ctx.waitUntil(
|
||||
fetch(`${base}/execution-log/cleanup`, { method: 'POST', headers })
|
||||
.then(async (r) => {
|
||||
const body = await r.json().catch(() => null);
|
||||
console.log('[scheduled] execution-log cleanup', r.status, JSON.stringify(body));
|
||||
})
|
||||
.catch((e) => console.error('[scheduled] execution-log cleanup failed', e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user