feat: KBDB-graph 插件獨立 — 全面改寫成走基本盤 API(API-as-Wall)
按 leo 鐵律(2026-06-14)把插件從「直接 SQL 操作基本盤表」改寫成 「只透過基本盤 arcrun/kbdb HTTP API 讀寫」。零建表、零 migration、零 SQL。 - 新增 src/lib/kbdb-client.ts:唯一對外通道,封裝 entries/templates/records API - 新增 src/lib/templates.ts:triplet/entity template 定義(替代建表) - 改寫 21 個違規 action(triplet/graph/entity/search)→ 走 client,圖在插件層記憶體組裝 - 移除所有 migrations、D1/Vectorize/AI 綁定;embedding/語意搜尋歸基本盤 optional 模組 - index.ts 只掛 triplets/graph/entities/search 路由;基本盤路由歸 arcrun/kbdb - 測試改走 mock client(純 node);裁剪 CLAUDE.md 只留 graph 插件 + 鐵律 - 修正 SDD design.md「讀現狀推翻鐵律」的錯誤判斷(共用 D1 → API-as-Wall) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
|
||||
import type { Bindings } from '../types';
|
||||
import {
|
||||
getPendingAliases,
|
||||
confirmPendingAlias,
|
||||
rejectPendingAlias,
|
||||
} from '../actions/entity-pending';
|
||||
import { listTripletEntities } from '../actions/triplet-entities';
|
||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||
|
||||
const entityRoutes = new OpenAPIHono<{ Bindings: Bindings }>();
|
||||
|
||||
// GET / (列出 entities)
|
||||
const listEntitiesRoute = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
request: {
|
||||
query: z.object({
|
||||
limit: z.string().optional(),
|
||||
offset: z.string().optional(),
|
||||
q: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: { description: 'List of entities' },
|
||||
},
|
||||
tags: ['Entities'],
|
||||
});
|
||||
|
||||
entityRoutes.openapi(listEntitiesRoute, async (c) => {
|
||||
const limit = parseInt(c.req.query('limit') || '100');
|
||||
const offset = parseInt(c.req.query('offset') || '0');
|
||||
const q = c.req.query('q') || '';
|
||||
|
||||
const result = await listTripletEntities(makeKbdbClient(c.env), { limit, offset, q: q || undefined });
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// GET /pending (列出待確認)
|
||||
const listPendingRoute = createRoute({
|
||||
method: 'get',
|
||||
path: '/pending',
|
||||
responses: {
|
||||
200: { description: 'List of pending aliases' },
|
||||
},
|
||||
tags: ['Entities'],
|
||||
});
|
||||
|
||||
entityRoutes.openapi(listPendingRoute, async (c) => {
|
||||
const limit = parseInt(c.req.query('limit') || '100');
|
||||
const pending = await getPendingAliases(makeKbdbClient(c.env), limit);
|
||||
return c.json(pending);
|
||||
});
|
||||
|
||||
entityRoutes.post('/pending/:id/confirm', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
await confirmPendingAlias(makeKbdbClient(c.env), id);
|
||||
return c.json({ success: true, action: 'confirmed', id });
|
||||
});
|
||||
|
||||
entityRoutes.post('/pending/:id/reject', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const newEntity = await rejectPendingAlias(makeKbdbClient(c.env), id);
|
||||
return c.json({ success: true, action: 'rejected', newEntity });
|
||||
});
|
||||
|
||||
export { entityRoutes };
|
||||
@@ -0,0 +1,49 @@
|
||||
// 圖遍歷路由入口
|
||||
// 僅驗證參數,呼叫 actions(actions 走基本盤 API,圖在插件層組裝)
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import { traverseGraph, queryRelation } from '../actions/graph-traverse';
|
||||
import { getNodeEdges, getNeighbors } from '../actions/graph-nodes';
|
||||
import { findShortestPath } from '../actions/graph-path';
|
||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||
|
||||
const graphRoutes = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
graphRoutes.get('/traverse', async (c) => {
|
||||
const start = c.req.query('start');
|
||||
if (!start) return c.json({ error: 'start parameter required' }, 400);
|
||||
const depth = Number(c.req.query('depth') ?? 2);
|
||||
const results = await traverseGraph(makeKbdbClient(c.env), start, depth);
|
||||
return c.json({ start, depth, nodes: results, nodeCount: results.length });
|
||||
});
|
||||
|
||||
graphRoutes.get('/relation', async (c) => {
|
||||
const from = c.req.query('from');
|
||||
const to = c.req.query('to');
|
||||
if (!from || !to) return c.json({ error: 'from and to parameters required' }, 400);
|
||||
const relations = await queryRelation(makeKbdbClient(c.env), from, to);
|
||||
return c.json({ from, to, relations, count: relations.length });
|
||||
});
|
||||
|
||||
// GET /graph/neighbors/:name — 合併原 nodes/:name 的 edges + neighbors
|
||||
graphRoutes.get('/neighbors/:name', async (c) => {
|
||||
const name = decodeURIComponent(c.req.param('name'));
|
||||
const client = makeKbdbClient(c.env);
|
||||
const [edges, neighbors] = await Promise.all([
|
||||
getNodeEdges(client, name),
|
||||
getNeighbors(client, name),
|
||||
]);
|
||||
return c.json({ node: name, edges, neighbors, edgeCount: edges.length, neighborCount: neighbors.length });
|
||||
});
|
||||
|
||||
// GET /graph/path — 取代 /shortest-path,行為不變
|
||||
graphRoutes.get('/path', async (c) => {
|
||||
const from = c.req.query('from');
|
||||
const to = c.req.query('to');
|
||||
if (!from || !to) return c.json({ error: 'from and to parameters required' }, 400);
|
||||
const result = await findShortestPath(makeKbdbClient(c.env), from, to);
|
||||
return c.json({ from, to, ...result });
|
||||
});
|
||||
|
||||
export { graphRoutes };
|
||||
@@ -0,0 +1,43 @@
|
||||
// 搜尋路由入口 — 僅驗證參數,呼叫 actions
|
||||
//
|
||||
// 插件層只做基本盤 keyword 搜尋(D1 LIKE,走 GET /entries/search)。
|
||||
// 語意搜尋 / embedding 屬基本盤 optional embed 模組,不在插件 → 已移除 /search/embed。
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import type { Bindings, Variables } from '../types';
|
||||
import { keywordSearch } from '../actions/search-query';
|
||||
import { suggestKnowledge } from '../actions/search-suggest';
|
||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||
|
||||
const searchRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
||||
|
||||
const UnifiedSearchSchema = z.object({
|
||||
query: z.string().min(1),
|
||||
type: z.enum(['keyword', 'suggest']).optional().default('keyword'),
|
||||
topK: z.number().min(1).max(20).optional(),
|
||||
owner_id: z.string().optional(),
|
||||
});
|
||||
|
||||
// 統一搜尋入口:POST /search
|
||||
searchRoutes.post('/', async (c) => {
|
||||
const parsed = UnifiedSearchSchema.safeParse(await c.req.json());
|
||||
if (!parsed.success) return c.json({ error: parsed.error.flatten() }, 400);
|
||||
const { query, type, topK, owner_id } = parsed.data;
|
||||
|
||||
// Namespace 讀取過濾:partner token 只能搜到自己 namespace 的資料
|
||||
const namespace = c.get('namespace');
|
||||
const effectiveOwner = namespace ?? owner_id;
|
||||
|
||||
const client = makeKbdbClient(c.env);
|
||||
|
||||
if (type === 'suggest') {
|
||||
const result = await suggestKnowledge(client, query, topK, effectiveOwner);
|
||||
return c.json(result);
|
||||
}
|
||||
|
||||
const matches = await keywordSearch(client, query, { limit: topK, owner_id: effectiveOwner });
|
||||
return c.json({ matches, count: matches.length, mode: 'keyword' });
|
||||
});
|
||||
|
||||
export { searchRoutes };
|
||||
@@ -0,0 +1,100 @@
|
||||
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
|
||||
import type { Bindings, Variables } from '../types';
|
||||
import { createTriplet, queryTriplets } from '../actions/triplet-crud';
|
||||
import { getTripletStats } from '../actions/triplet-stats';
|
||||
import { makeKbdbClient } from '../lib/kbdb-client';
|
||||
|
||||
const tripletRoutes = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>();
|
||||
|
||||
const TripletSchema = z.object({
|
||||
id: z.string(),
|
||||
subject: z.string(),
|
||||
predicate: z.string(),
|
||||
object: z.string(),
|
||||
owner_id: z.string().optional(),
|
||||
});
|
||||
|
||||
// GET /triplets/stats (統計)
|
||||
const statsRoute = createRoute({
|
||||
method: 'get',
|
||||
path: '/stats',
|
||||
responses: {
|
||||
200: { description: 'Statistics of the knowledge graph' },
|
||||
},
|
||||
tags: ['Triplets'],
|
||||
});
|
||||
|
||||
tripletRoutes.openapi(statsRoute, async (c) => {
|
||||
const stats = await getTripletStats(makeKbdbClient(c.env));
|
||||
return c.json(stats, 200);
|
||||
});
|
||||
|
||||
// GET /triplets (查詢)
|
||||
const queryRoute = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
request: {
|
||||
query: z.object({
|
||||
subject: z.string().optional(),
|
||||
predicate: z.string().optional(),
|
||||
object: z.string().optional(),
|
||||
limit: z.string().optional().describe('Maximum number of results (default 50)'),
|
||||
offset: z.string().optional().describe('Pagination offset'),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({ triplets: z.array(TripletSchema), count: z.number() }),
|
||||
},
|
||||
},
|
||||
description: 'Query results from KBDB Graph',
|
||||
},
|
||||
},
|
||||
tags: ['Triplets'],
|
||||
});
|
||||
|
||||
tripletRoutes.openapi(queryRoute, async (c) => {
|
||||
const query = c.req.valid('query');
|
||||
const limit = query.limit ? parseInt(query.limit, 10) : undefined;
|
||||
const offset = query.offset ? parseInt(query.offset, 10) : undefined;
|
||||
|
||||
const results = await queryTriplets(makeKbdbClient(c.env), { ...query, limit, offset });
|
||||
return c.json(results as any, 200);
|
||||
});
|
||||
|
||||
// POST /triplets (建立)
|
||||
const createRouteDefinition = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
subject: z.string().min(1),
|
||||
predicate: z.string().min(1),
|
||||
object: z.string().min(1),
|
||||
owner_id: z.string().optional(),
|
||||
source_block_id: z.string().optional(),
|
||||
confidence: z.number().optional(),
|
||||
clusters: z.array(z.string()).optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: 'Triplet created successfully' },
|
||||
},
|
||||
tags: ['Triplets'],
|
||||
});
|
||||
|
||||
tripletRoutes.openapi(createRouteDefinition, async (c) => {
|
||||
const data = c.req.valid('json');
|
||||
await createTriplet(makeKbdbClient(c.env), data);
|
||||
return c.json({ ok: true }, 201);
|
||||
});
|
||||
|
||||
export { tripletRoutes };
|
||||
Reference in New Issue
Block a user