feat: KBDB self-hosted 查詢 + embed 模組 + thin-shell 收窄 + search_workflow(code done 待端到端)
按 issue 分段標明(檔 #5/#8 改動交疊處無法乾淨拆檔,故併一個 commit): #4 thin-shell §3.1 自力救濟階梯 + code-node 規則(純文檔/規則,code-node 零件未實作) #5 KBDB source filter(json_extract metadata_json 零建表)+ 能力對照;documents 聚合與 DELETE proxy 部分擱置等頂層 T8 #7 base embed 模組(kbdb/src/embed.ts)+ vectorize 開關(deploy/config/wrangler.toml 註解範本) + 語義查詢降級閉環(mode=semantic 未開→LIKE+capability_hint) #8 部分(workflow-discovery): - KBDB /entries/search 加 base 通用 entry_type filter(entry-crud/embed/route/kbdb-proxy 透傳) - /webhooks/named 強制 description(空→400,訊息要求操盤 AI 據實寫一句) - 部署雙寫 entry_type=workflow embeddable entry(waitUntil 非阻塞,供 search) - cypher GET /workflows/search + MCP u6u_search_workflows(優先語意、降級 hint) - cypher POST /workflows/backfill-search-entries(無 desc 列出不編造) - GET /webhooks/named 補回 description/created_at 欄位(為 list 來源收斂備) ⚠️ tsc 綠 = code done,非完成(mindset §7 禁假綠): - #7/#8 端到端待 leo21c 部署驗(Vectorize 需官方憑證、CC 跑不了) - #8 ①-a(MCP deploy 改打 /webhooks/named)未做、MCP deploy 那半仍 404 - #8 端到端(強制填擋空/語義命中/租戶隔離/降級 hint)未驗 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Entries route — atomic data + tree (project/workflow). Base, no embed/triplet.
|
||||
// Entries route — atomic data + tree (project/workflow). Base; embed is OPTIONAL (issue #7).
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import {
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
deleteEntry,
|
||||
searchEntries,
|
||||
} from '../actions/entry-crud';
|
||||
import { embedEnabled, embedOnWrite, semanticSearch } from '../embed';
|
||||
|
||||
export const entryRoutes = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
@@ -17,29 +18,63 @@ entryRoutes.post('/', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body || !body.entry_type) return c.json({ success: false, error: 'entry_type required' }, 400);
|
||||
const entry = await createEntry(c.env.DB, body);
|
||||
// embed-on-write (#7 / #5 第4點):模組開 + entry 標 embed:true 才做;fire-and-forget,不阻塞回應、失敗不致命。
|
||||
if (embedEnabled(c.env)) c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {}));
|
||||
return c.json({ success: true, entry });
|
||||
});
|
||||
|
||||
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name)
|
||||
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source)
|
||||
// e.g. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow
|
||||
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
|
||||
// e.g. filter by ingest source: ?source=logseq://vault/foo.md (issue #5.1)
|
||||
entryRoutes.get('/', async (c) => {
|
||||
const entries = await listEntries(c.env.DB, {
|
||||
entry_type: c.req.query('entry_type') || undefined,
|
||||
owner_id: c.req.query('owner_id') || undefined,
|
||||
parent_id: c.req.query('parent_id') || undefined,
|
||||
page_name: c.req.query('page_name') || undefined,
|
||||
source: c.req.query('source') || undefined,
|
||||
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
||||
offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
|
||||
});
|
||||
return c.json({ success: true, entries, count: entries.length });
|
||||
});
|
||||
|
||||
// GET /entries/search?q=...&owner_id=... — D1 LIKE keyword search (base)
|
||||
// GET /entries/search?q=...&owner_id=...&source=...&entry_type=...&mode=keyword|semantic
|
||||
// - mode=keyword(預設):D1 LIKE(base,永遠可用)。
|
||||
// - mode=semantic:需 embed 模組開(Vectorize+AI binding)。未開 → 降級 keyword + capability_hint 告知缺能力(#7 發現閉環)。
|
||||
// - entry_type:base 通用 filter(caller 傳任意 type,如 workflow;base 不寫死語意,workflow-discovery Q4)。
|
||||
entryRoutes.get('/search', async (c) => {
|
||||
const q = c.req.query('q');
|
||||
if (!q) return c.json({ success: false, error: 'q required' }, 400);
|
||||
const entries = await searchEntries(c.env.DB, q, c.req.query('owner_id') || undefined);
|
||||
const owner_id = c.req.query('owner_id') || undefined;
|
||||
const source = c.req.query('source') || undefined;
|
||||
const entry_type = c.req.query('entry_type') || undefined;
|
||||
const mode = c.req.query('mode') === 'semantic' ? 'semantic' : 'keyword';
|
||||
|
||||
if (mode === 'semantic') {
|
||||
const hits = await semanticSearch(c.env, q, { owner_id, source, entry_type });
|
||||
if (hits === null) {
|
||||
// 模組沒開:誠實降級 keyword + 告知「叫 CC 幫你開 vectorize」(不假裝有語義)。
|
||||
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type);
|
||||
return c.json({
|
||||
success: true,
|
||||
entries,
|
||||
count: entries.length,
|
||||
mode: 'keyword',
|
||||
requested_mode: 'semantic',
|
||||
capability_hint:
|
||||
'語義查詢需先開 vectorize(embed 模組)。叫 CC「幫我開語義查詢」即可(設 kbdb_embed:true + redeploy)。本次已降級關鍵字搜尋。',
|
||||
});
|
||||
}
|
||||
// hydrate vector hits → 完整 entry(保持回應形狀與 keyword 一致)。
|
||||
const entries = (await Promise.all(hits.map((h) => getEntry(c.env.DB, h.id)))).filter(
|
||||
(e): e is NonNullable<typeof e> => e !== null,
|
||||
);
|
||||
return c.json({ success: true, entries, count: entries.length, mode: 'semantic' });
|
||||
}
|
||||
|
||||
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type);
|
||||
return c.json({ success: true, entries, count: entries.length, mode: 'keyword' });
|
||||
});
|
||||
|
||||
@@ -55,11 +90,19 @@ entryRoutes.patch('/:id', async (c) => {
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
const entry = await updateEntry(c.env.DB, c.req.param('id'), body);
|
||||
if (!entry) return c.json({ success: false, error: 'not found' }, 404);
|
||||
// 內容改了 → 重 embed(保持向量新鮮)。embedOnWrite 內部自會檢查模組開 + entry 是否 embeddable。
|
||||
if (embedEnabled(c.env) && body.content !== undefined) {
|
||||
c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {}));
|
||||
}
|
||||
return c.json({ success: true, entry });
|
||||
});
|
||||
|
||||
// DELETE /entries/:id
|
||||
entryRoutes.delete('/:id', async (c) => {
|
||||
// 模組開 → 連帶刪向量(避免孤兒向量)。失敗不致命。
|
||||
if (embedEnabled(c.env)) {
|
||||
c.executionCtx.waitUntil(c.env.VECTORIZE!.deleteByIds([c.req.param('id')]).then(() => {}).catch(() => {}));
|
||||
}
|
||||
await deleteEntry(c.env.DB, c.req.param('id'));
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user