// Embed module admin route — backfill existing entries (issue #7 / mira-dissolve T2.4 缺口). // // 背景:embed 原本只有「寫入即嵌」(embedOnWrite),對「開 Vectorize binding 之前就寫入」或 // 「embed-on-write 當時漏掉」的既有 entry 沒有回填路徑 → is_embedded=0 且永遠補不回。 // 本 route = 把 embed.ts 既有 embedText/VECTORIZE.upsert 邏輯包成可重複呼叫的批次補嵌端點。 // // 鐵律對齊:embedding 屬 base optional 模組;模組未開(無 VECTORIZE+AI)→ 誠實回 409,不假裝(mindset §7)。 // base 對內容語意無知:只認通用 metadata.embed===true 旗標,不知 triplet/wiki(解耦)。 import { Hono } from 'hono'; import type { Bindings } from '../types'; import { embedEnabled, backfillEmbeddings, backfillStatus, embedSelfTest, reconcileEmbedGeneration } from '../embed'; export const embedRoutes = new Hono<{ Bindings: Bindings }>(); const OFF_HINT = '語義補嵌需先開 embed 模組(Vectorize+AI binding)。叫 CC「幫我開語義查詢」(設 kbdb_embed:true + redeploy 注入 binding)後再呼叫本端點。'; // POST /embed/backfill — batch-embed existing embeddable entries with is_embedded=0. // body(皆選填):{ limit?:1-100(預設25), owner_id?, source?, library?, since?, until?, reindex?, offset? }。 // 冪等:重跑不會重複嵌(已 is_embedded=1 的不再入選;upsert 同 id 冪等)。 // 分批:單次最多 limit 筆;回傳 remaining>0 表示還有 → 重複呼叫直到 remaining=0。 // reindex:true(Arcrun#11):改重推「所有 embeddable」既有向量(含 is_embedded=1), // 讓事後建立的 Vectorize metadata index 收錄它們(否則帶過濾語意查詢回 0);配 offset 分頁。 // library/since/until(Arcrun#85,2026-08-11):「挑哪一批」從外面指定——時間分層 // (今天/本週/半年前)與庫分層(有查詢紀錄的庫優先)共用同一套 SelectionCriteria, // 由呼叫端(工作流)決定這次要補的是哪一批,不是資料層焊死單一排序(見 embed.ts 檔頭說明)。 // 模組未開 → 409 + capability_hint(不假綠)。 embedRoutes.post('/backfill', async (c) => { if (!embedEnabled(c.env)) { return c.json( { success: false, error: 'embed module not enabled (need VECTORIZE + AI bindings)', capability_hint: OFF_HINT }, 409, ); } const body = (await c.req.json().catch(() => ({}))) as { limit?: number | string; owner_id?: string; source?: string; library?: string; since?: number | string; until?: number | string; reindex?: boolean; offset?: number | string; }; const result = await backfillEmbeddings(c.env, { limit: body.limit !== undefined ? Number(body.limit) : undefined, owner_id: body.owner_id || undefined, source: body.source || undefined, library: body.library || undefined, since: body.since !== undefined ? Number(body.since) : undefined, until: body.until !== undefined ? Number(body.until) : undefined, // reindex(Arcrun#11):重推既有向量讓事後建立的 Vectorize metadata index 收錄(見 embed.ts)。 reindex: body.reindex === true, offset: body.offset !== undefined ? Number(body.offset) : undefined, }); return c.json({ success: true, ...result }); }); // GET /embed/backfill/status?owner_id=&source= — 待補嵌 / 已補嵌計數(回報 + 判斷是否清零用)。 embedRoutes.get('/backfill/status', async (c) => { const status = await backfillStatus(c.env, { owner_id: c.req.query('owner_id') || undefined, source: c.req.query('source') || undefined, }); return c.json({ success: true, ...status }); }); // POST /embed/reconcile — 世代核對(D68 配套修復,2026-08-11;D69 額度節流同日補上): // 對「is_embedded=1 但 content_hash 非現行模型」的候選,問現行 Vectorize index 是否真的收錄; // 真的在 → 補標 content_hash(不打 AI);不在 → 重置 is_embedded=0,回到正常 /embed/backfill 佇列。 // 解「從備份整批灌回、帶著對已退役索引的 is_embedded=1,永遠不被 backfill 碰到」這個坑。 // body(皆選填):{ limit?:1-200(預設50), owner_id?, library?, since?, until? }。重複呼叫直到 remaining=0。 // D69:每筆候選最多消耗一次 D1 row write,與 POST /entries/backfill-library 共用同一顆每日 // 「背景維護 D1 寫入」額度(見 actions/maintenance-quota.ts)——額度用完會誠實回 // quota_exceeded:true 並停手,不會把當天 D1 免費額度燒穿(2026-08-11 leo 逐行複核找到的破口)。 embedRoutes.post('/reconcile', async (c) => { if (!embedEnabled(c.env)) { return c.json( { success: false, error: 'embed module not enabled (need VECTORIZE + AI bindings)', capability_hint: OFF_HINT }, 409, ); } const body = (await c.req.json().catch(() => ({}))) as { limit?: number | string; owner_id?: string; library?: string; since?: number | string; until?: number | string; }; const result = await reconcileEmbedGeneration(c.env, { limit: body.limit !== undefined ? Number(body.limit) : undefined, owner_id: body.owner_id || undefined, library: body.library || undefined, since: body.since !== undefined ? Number(body.since) : undefined, until: body.until !== undefined ? Number(body.until) : undefined, }); return c.json({ success: true, ...result }); }); // GET /embed/selftest?owner_id= — 語義自我檢查(檢修孔,2026-08-07): // 挑一筆已嵌入的卡片,拿它自己的內容查自己,只回布林診斷(不回卡片內容、不回 entry id)。 // 計數(backfill/status)看不出「嵌了但查不到」這種故障模式(Arcrun#11 撞過的真實案例), // 本端點端到端驗證 index 真的可用。模組未開仍誠實回 enabled:false(不 409,讓檢修孔 // 永遠能拿到一個可解讀的結論,不必先判斷該不該打這支)。 embedRoutes.get('/selftest', async (c) => { const result = await embedSelfTest(c.env, { owner_id: c.req.query('owner_id') || undefined }); return c.json({ success: true, ...result }); }); export default embedRoutes;