Merge branch 'main' into fix/merge-main-into-batch-t173
# Conflicts: # console-ui/public/portal/index.html # cypher-executor/src/routes/health.ts # cypher-executor/src/routes/portal.ts # registry/components/kbdb_upsert_block/component.contract.yaml # registry/examples/km-wiki-ingest/workflow.yaml
This commit is contained in:
@@ -3,6 +3,7 @@ import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import {
|
||||
createEntry,
|
||||
deprecateEntriesByLibrary,
|
||||
getEntry,
|
||||
listEntries,
|
||||
updateEntry,
|
||||
@@ -32,6 +33,49 @@ entryRoutes.post('/', async (c) => {
|
||||
return c.json({ success: true, entry });
|
||||
});
|
||||
|
||||
// GET /entries/libraries?owner_id=... — 這個租戶的資料裡實際出現過哪些庫(distinct)。
|
||||
// t52(leo 2026-07-26:地端幾個資料夾=雲端幾個庫):庫由 ingest 蓋章決定,這裡直接從
|
||||
// 資料反查,讓「蓋了章的庫」一定看得到,不必依賴任何登記動作。未蓋章的舊資料=general。
|
||||
// 註冊在 '/' 之前——Hono 路由先到先比,放後面會被 '/:id' 之類的樣式吃掉。
|
||||
entryRoutes.get('/libraries', async (c) => {
|
||||
const owner = c.req.query('owner_id') || '';
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT DISTINCT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library
|
||||
FROM entries
|
||||
WHERE (?1 = '' OR owner_id = ?1)
|
||||
AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'
|
||||
ORDER BY library`,
|
||||
)
|
||||
.bind(owner)
|
||||
.all<{ library: string }>();
|
||||
const libraries = (rows.results ?? []).map((r) => r.library).filter(Boolean);
|
||||
return c.json({ success: true, libraries, count: libraries.length });
|
||||
});
|
||||
|
||||
// GET /entries/library-stats?owner_id=... — 每個庫的知識卡數(distinct page_name,非 block 數)。
|
||||
// t142(2026-07-29):政府驗收用——一眼看出每個庫有幾張卡(page 粒度,不是 block 粒度,
|
||||
// 一張卡通常對應 3-5 個 block;不含 deprecated entries)。
|
||||
// 只計 entry_type='block' 的條目,因為 block 才對應知識卡的一個段落(page_name 標記所屬頁面)。
|
||||
entryRoutes.get('/library-stats', async (c) => {
|
||||
const owner = c.req.query('owner_id') || '';
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT
|
||||
COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library,
|
||||
COUNT(DISTINCT page_name) AS card_count
|
||||
FROM entries
|
||||
WHERE (?1 = '' OR owner_id = ?1)
|
||||
AND entry_type = 'block'
|
||||
AND page_name IS NOT NULL
|
||||
AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'
|
||||
GROUP BY library
|
||||
ORDER BY library`,
|
||||
)
|
||||
.bind(owner)
|
||||
.all<{ library: string; card_count: number }>();
|
||||
const stats = (rows.results ?? []).map((r) => ({ library: r.library, card_count: r.card_count }));
|
||||
return c.json({ success: true, stats });
|
||||
});
|
||||
|
||||
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source, q/search)
|
||||
// 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
|
||||
@@ -142,6 +186,18 @@ entryRoutes.get('/:id', async (c) => {
|
||||
return c.json({ success: true, entry });
|
||||
});
|
||||
|
||||
// PATCH /entries/deprecate-by-library — body {owner_id, library}。
|
||||
// t135:把某租戶某庫的所有 entries 標 deprecated,讓庫從 auto 清單消失。
|
||||
// 此路由必須在 '/:id' 之前,否則 'deprecate-by-library' 會被當成 id 參數。
|
||||
entryRoutes.patch('/deprecate-by-library', async (c) => {
|
||||
const body = (await c.req.json().catch(() => null)) as { owner_id?: string; library?: string } | null;
|
||||
const ownerId = String(body?.owner_id ?? '').trim();
|
||||
const library = String(body?.library ?? '').trim();
|
||||
if (!ownerId || !library) return c.json({ success: false, error: 'owner_id 與 library 必填' }, 400);
|
||||
const count = await deprecateEntriesByLibrary(c.env.DB, ownerId, library);
|
||||
return c.json({ success: true, deprecated_count: count });
|
||||
});
|
||||
|
||||
// PATCH /entries/:id
|
||||
entryRoutes.patch('/:id', async (c) => {
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Records route — structured records (entry_values composed by a template).
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import { createRecord, getRecord, searchByTemplate, updateRecord } from '../actions/record-crud';
|
||||
import { createRecord, deleteRecord, getRecord, searchByTemplate, updateRecord } from '../actions/record-crud';
|
||||
|
||||
export const recordRoutes = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
@@ -19,6 +19,38 @@ recordRoutes.post('/', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /records/triplet-stats?owner_id=... — 每個庫的三元組(關聯)數。
|
||||
// t142(2026-07-29):政府驗收——顯示每個庫整理出幾條知識關聯。
|
||||
// 計法:依 triplet 型 record 的 'library' slot 值分組計數。無 library slot 的舊三元組歸 general。
|
||||
// 使用子查詢先取 distinct triplet record IDs(針對 owner),再 LEFT JOIN library slot,
|
||||
// 避免 N+1(全部一次 SQL 完成,不逐筆 getRecord)。
|
||||
recordRoutes.get('/triplet-stats', async (c) => {
|
||||
const owner = c.req.query('owner_id') || '';
|
||||
// 子查詢:找到屬於這個 owner 的所有 triplet records;LEFT JOIN library slot 取庫名
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT
|
||||
COALESCE(NULLIF(lib_e.content, ''), 'general') AS library,
|
||||
COUNT(*) AS triplet_count
|
||||
FROM (
|
||||
SELECT DISTINCT ev.record_id
|
||||
FROM entry_values ev
|
||||
JOIN templates t ON ev.template_id = t.id
|
||||
JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE t.name = 'triplet'
|
||||
AND (?1 = '' OR e.owner_id = ?1)
|
||||
) AS tr
|
||||
LEFT JOIN entry_values lev
|
||||
ON lev.record_id = tr.record_id AND lev.slot_name = 'library'
|
||||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')
|
||||
ORDER BY library`,
|
||||
)
|
||||
.bind(owner)
|
||||
.all<{ library: string; triplet_count: number }>();
|
||||
const stats = (rows.results ?? []).map((r) => ({ library: r.library, triplet_count: r.triplet_count }));
|
||||
return c.json({ success: true, stats });
|
||||
});
|
||||
|
||||
// GET /records/by-template/:template — list records of a template
|
||||
recordRoutes.get('/by-template/:template', async (c) => {
|
||||
const records = await searchByTemplate(c.env.DB, c.req.param('template'), c.req.query('owner_id') || undefined);
|
||||
@@ -47,3 +79,10 @@ recordRoutes.patch('/:recordId', async (c) => {
|
||||
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /records/:recordId — 刪除一筆 record 及其底層 entries。
|
||||
recordRoutes.delete('/:recordId', async (c) => {
|
||||
const found = await deleteRecord(c.env.DB, c.req.param('recordId'));
|
||||
if (!found) return c.json({ success: false, error: 'not found' }, 404);
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user