674e1b4fa2
leo 逐行複核 fix/embed-backfill-d68 後點出的破口+二次裁決(票上全文見 Leo/Arcrun#85): 一、向量化優先序(今天寫的立刻/本週在跑的先跑/有查詢紀錄的庫優先/半年前慢慢跑)表達 不出來——策略要能從外面(工作流)指定,不能焊死在資料層。新增 embed.ts 的 `SelectionCriteria`(owner_id/source/library/since/until),backfillEmbeddings 與 reconcileEmbedGeneration 共用同一套形狀;「按庫」那一層現在有資料可用即可運作(見下)。 二、世代核對(reconcileEmbedGeneration)不打 AI 但逐筆寫 D1,47 萬筆候選 ≈ 4.7 倍 D1 100,000 rows/日免費額度,先前零保護。新增 actions/maintenance-quota.ts(單一 entries 列/日的共用計數器,精神同 execution-log.ts/embed.ts 既有慣例,不新增表)。 三、leo 二度裁決:「標庫」與「時間分層」其實是一件事,判定標準要從第一天同時容納兩者, 不能先做一半再回頭改。新增 actions/library-backfill.ts 的 backfillEntryLibraryTags—— 呼叫端(ingest/daemon/Arcrun#87)決定要貼哪個庫、用 page_names(Gitea 原稿卡名精準 點名,leo 定案的正解)或 source_prefix/page_name_prefix 過渡 fallback 篩選候選,base 只負責安全、節流地寫入。owner_id 刻意必填(leo 點出「補錯 owner 等於白做」——實查卡片 掛在 owner_id=bfezv28v,換成 'leo' 查卻是空的)。 D69:reconcile 與標庫 backfill 共用同一顆「今天還剩多少 D1 寫入額度」計數器(不共用的話 其中一個會把另一個的閘繞過去);新增 POST /entries/backfill-library + GET .../status, 擴充 POST /embed/backfill 與 /embed/reconcile 吃 library/since/until 參數。 測試:92 → 39 個新增/擴充案例覆蓋 since/until/library 篩選、reconcile 額度真的擋 (含「拿掉 cap 會變紅」反向驗證)、標庫 backfill 冪等/owner_id 必填/page_names 精準比對、 以及兩個操作共用同一顆額度計數器的跨模組驗證(雙向:先 reconcile 耗盡再標庫、反之亦然)。 kbdb 全套 192 個測試綠燈,tsc --noEmit 除既有 auth.test.ts 舊缺陷外無新增錯誤。 紅線:未併 main、未部署、未動任何實例的 is_embedded 旗標(只在本地 SQLite 測試治具跑過)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
6.2 KiB
TypeScript
111 lines
6.2 KiB
TypeScript
// 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;
|