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,163 @@
|
||||
// KBDB 基本盤 API client — 唯一對外通道(API-as-Wall)
|
||||
//
|
||||
// 鐵律:插件不碰表、零 SQL。所有讀寫只透過基本盤 HTTP API(arcrun/kbdb)。
|
||||
// 這裡只發 fetch,絕無 .prepare / D1。base URL 由 KBDB_BASE_URL env var 注入(可留空於本地測試以 mock 替代)。
|
||||
//
|
||||
// 對齊基本盤真實契約(讀 arcrun/kbdb/src 2026-06-14):
|
||||
// 欄位用 entry_type / owner_id(非 type / user_id);回應包在 { success, ... }。
|
||||
|
||||
export type BaseEntry = {
|
||||
id: string;
|
||||
content: string | null;
|
||||
entry_type: string;
|
||||
owner_id: string | null;
|
||||
parent_id?: string | null;
|
||||
page_name?: string | null;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
};
|
||||
|
||||
export type BaseRecord = {
|
||||
record_id: string;
|
||||
template: string;
|
||||
values: Record<string, string>;
|
||||
};
|
||||
|
||||
export type CreateEntryInput = {
|
||||
content: string | null;
|
||||
entry_type: string;
|
||||
owner_id?: string;
|
||||
parent_id?: string;
|
||||
page_name?: string;
|
||||
};
|
||||
|
||||
/** 基本盤 API client。所有方法 = 一個 HTTP 呼叫,零 SQL。 */
|
||||
export class KbdbClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly token?: string,
|
||||
) {
|
||||
if (!baseUrl) {
|
||||
throw new Error('KBDB_BASE_URL 未設定:插件需指向基本盤 API(不可直連 D1)');
|
||||
}
|
||||
}
|
||||
|
||||
private async req<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
||||
|
||||
const res = await fetch(this.baseUrl.replace(/\/$/, '') + path, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
|
||||
const json = (await res.json().catch(() => null)) as any;
|
||||
if (!res.ok || (json && json.success === false)) {
|
||||
const msg = json?.error ?? `${res.status} ${res.statusText}`;
|
||||
throw new Error(`[kbdb-base] ${method} ${path}: ${msg}`);
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
|
||||
// --- entries ---
|
||||
|
||||
async createEntry(input: CreateEntryInput): Promise<BaseEntry> {
|
||||
const { entry } = await this.req<{ entry: BaseEntry }>('POST', '/entries', input);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async getEntry(id: string): Promise<BaseEntry | null> {
|
||||
try {
|
||||
const { entry } = await this.req<{ entry: BaseEntry }>('GET', `/entries/${encodeURIComponent(id)}`);
|
||||
return entry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async listEntries(filters: {
|
||||
entry_type?: string;
|
||||
owner_id?: string;
|
||||
parent_id?: string;
|
||||
page_name?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {}): Promise<BaseEntry[]> {
|
||||
const { entries } = await this.req<{ entries: BaseEntry[] }>('GET', '/entries' + qs(filters));
|
||||
return entries ?? [];
|
||||
}
|
||||
|
||||
/** 基本盤 D1 LIKE keyword 搜尋(語意搜尋屬 optional embed 模組,base 沒有)。 */
|
||||
async searchEntries(q: string, owner_id?: string): Promise<BaseEntry[]> {
|
||||
const { entries } = await this.req<{ entries: BaseEntry[] }>(
|
||||
'GET',
|
||||
'/entries/search' + qs({ q, owner_id }),
|
||||
);
|
||||
return entries ?? [];
|
||||
}
|
||||
|
||||
async updateEntry(id: string, patch: Partial<CreateEntryInput>): Promise<BaseEntry | null> {
|
||||
try {
|
||||
const { entry } = await this.req<{ entry: BaseEntry }>('PATCH', `/entries/${encodeURIComponent(id)}`, patch);
|
||||
return entry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEntry(id: string): Promise<void> {
|
||||
await this.req('DELETE', `/entries/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
// --- templates(= 替代建表;插件要新類型只能建 template) ---
|
||||
|
||||
async ensureTemplate(name: string, slots: string[], description?: string): Promise<void> {
|
||||
const existing = await this.req<{ id?: string } | { error: string }>(
|
||||
'GET',
|
||||
`/templates/${encodeURIComponent(name)}`,
|
||||
).catch(() => null);
|
||||
if (existing && (existing as any).id) return;
|
||||
await this.req('POST', '/templates', { name, slots, description, created_by: 'kbdb-graph' });
|
||||
}
|
||||
|
||||
// --- records(= template 實例,填 slot) ---
|
||||
|
||||
async createRecord(template: string, values: Record<string, string>, owner_id?: string): Promise<string> {
|
||||
const { record } = await this.req<{ record: { record_id: string } }>('POST', '/records', {
|
||||
template,
|
||||
values,
|
||||
owner_id,
|
||||
});
|
||||
return record.record_id;
|
||||
}
|
||||
|
||||
async getRecord(recordId: string): Promise<BaseRecord | null> {
|
||||
try {
|
||||
const { record } = await this.req<{ record: BaseRecord }>('GET', `/records/${encodeURIComponent(recordId)}`);
|
||||
return record;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async listRecordsByTemplate(template: string, owner_id?: string): Promise<BaseRecord[]> {
|
||||
const { records } = await this.req<{ records: BaseRecord[] }>(
|
||||
'GET',
|
||||
`/records/by-template/${encodeURIComponent(template)}` + qs({ owner_id }),
|
||||
);
|
||||
return records ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
function qs(params: Record<string, string | number | undefined>): string {
|
||||
const parts = Object.entries(params)
|
||||
.filter(([, v]) => v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
||||
return parts.length ? `?${parts.join('&')}` : '';
|
||||
}
|
||||
|
||||
/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。 */
|
||||
export function makeKbdbClient(env: { KBDB_BASE_URL?: string; KBDB_INTERNAL_TOKEN?: string }): KbdbClient {
|
||||
return new KbdbClient(env.KBDB_BASE_URL ?? '', env.KBDB_INTERNAL_TOKEN);
|
||||
}
|
||||
Reference in New Issue
Block a user