portal-auth P1(#24 #25):KBDB library filter 地基(不 merge,待總管審) (#50)

This commit was merged in pull request #50.
This commit is contained in:
Leo
2026-07-14 03:50:06 +00:00
parent 18c0846af0
commit 7f409646e5
6 changed files with 274 additions and 14 deletions
+17 -4
View File
@@ -13,6 +13,14 @@ import { embedEnabled, embedOnWrite, semanticSearch } from '../embed';
export const entryRoutes = new Hono<{ Bindings: Bindings }>();
// library 多值參數(逗號分隔,portal-auth P1design §3.3)。空值/全空白 → undefined(=不過濾,
// 行為與未帶參數一字不變——向後相容硬驗收)。
function parseLibraryParam(raw: string | undefined): string[] | undefined {
if (!raw) return undefined;
const libs = raw.split(',').map((s) => s.trim()).filter(Boolean);
return libs.length > 0 ? libs : undefined;
}
// POST /entries — create (entry_type=block/value/project/workflow/...)
entryRoutes.post('/', async (c) => {
const body = await c.req.json().catch(() => null);
@@ -27,6 +35,7 @@ entryRoutes.post('/', async (c) => {
// 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)
// e.g. filter by library(多值逗號分隔,portal-auth P1: ?library=finance,hr(未標記舊資料歸 general
// e.g. keyword filter: ?q=遷移 或 ?search=遷移(別名,Arcrun#3 發現①:caller 實測時打的是 search=
// 舊版完全不接這個 filter;q 與 search 兩個名字都認,避免同一個坑再踩一次)。
// count = 本頁筆數(受 limit 影響);total = 符合條件全部筆數(不受 limit 影響,見 total 欄位)。
@@ -37,6 +46,7 @@ entryRoutes.get('/', async (c) => {
parent_id: c.req.query('parent_id') || undefined,
page_name: c.req.query('page_name') || undefined,
source: c.req.query('source') || undefined,
library: parseLibraryParam(c.req.query('library')),
q: c.req.query('q') || c.req.query('search') || undefined,
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
@@ -44,23 +54,26 @@ entryRoutes.get('/', async (c) => {
return c.json({ success: true, entries, count: entries.length, total });
});
// GET /entries/search?q=...&owner_id=...&source=...&entry_type=...&mode=keyword|semantic
// GET /entries/search?q=...&owner_id=...&source=...&entry_type=...&library=...&mode=keyword|semantic
// - mode=keyword(預設):D1 LIKEbase,永遠可用)。
// - mode=semantic:需 embed 模組開(Vectorize+AI binding)。未開 → 降級 keyword + capability_hint 告知缺能力(#7 發現閉環)。
// - entry_typebase 通用 filtercaller 傳任意 type,如 workflowbase 不寫死語意,workflow-discovery Q4)。
// - library:多值庫 filter(逗號分隔,portal-auth P1)。keyword 走 json_extractNULL→general
// semantic 走 Vectorize $in。未帶=全庫(行為不變)。
entryRoutes.get('/search', async (c) => {
const q = c.req.query('q');
if (!q) return c.json({ success: false, error: 'q required' }, 400);
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 library = parseLibraryParam(c.req.query('library'));
const mode = c.req.query('mode') === 'semantic' ? 'semantic' : 'keyword';
if (mode === 'semantic') {
const hits = await semanticSearch(c.env, q, { owner_id, source, entry_type });
const hits = await semanticSearch(c.env, q, { owner_id, source, entry_type, library });
if (hits === null) {
// 模組沒開:誠實降級 keyword + 告知「叫 CC 幫你開 vectorize」(不假裝有語義)。
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type);
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type, undefined, library);
return c.json({
success: true,
entries,
@@ -78,7 +91,7 @@ entryRoutes.get('/search', async (c) => {
return c.json({ success: true, entries, count: entries.length, mode: 'semantic' });
}
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type);
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type, undefined, library);
return c.json({ success: true, entries, count: entries.length, mode: 'keyword' });
});