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:
2026-06-14 20:59:41 +08:00
commit efe8e165cf
62 changed files with 7671 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
// triplet-extract.ts — LLM 三元組萃取 + 寫入工具函數
// 萃取=純 LLM(不碰 DB);寫入=走基本盤 API(零 SQL / 零 D1)。
// 供 block-ingest.ts 和 block-process.ts 共用
import type { KbdbClient } from '../lib/kbdb-client';
import { createTriplet, queryTriplets } from './triplet-crud';
const EXTRACT_PROMPT = `你是知識萃取助手,從文章中萃取知識三元組。
只萃取:性格特質、關鍵經歷精華、核心觀點信念、說話方式與風格。
禁止:具體年份日期、純事實統計、流水帳年表。
格式:[{"subject":"...","predicate":"2-6字","object":"15-50字","confidence":0.8}]
直接輸出 JSON Array,第一字元 [,最後字元 ]。不要其他文字。`;
export interface LLMTriplet {
subject: string;
predicate: string;
object: string;
confidence: number;
}
/** Workers AI 萃取三元組,每段獨立呼叫,單段失敗不中斷 */
export async function extractTripletsViaLLM(ai: Ai, chunks: string[]): Promise<LLMTriplet[]> {
const results: LLMTriplet[] = [];
for (const chunk of chunks) {
if (!chunk.trim()) continue;
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const raw = await (ai as any).run('@cf/meta/llama-3.3-70b-instruct-fp8-fast', {
messages: [
{ role: 'system', content: EXTRACT_PROMPT },
{ role: 'user', content: `萃取三元組:\n${chunk}` },
],
max_tokens: 512,
temperature: 0.1,
});
const text = (typeof raw === 'string' ? raw : (raw?.response ?? ''))
.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
const m = text.match(/\[[\s\S]*\]/);
if (!m) continue;
const parsed = JSON.parse(m[0]);
if (!Array.isArray(parsed)) continue;
for (const t of parsed) {
const s = String(t?.subject ?? '').trim();
const p = String(t?.predicate ?? '').trim();
const o = String(t?.object ?? '').trim();
if (s && p && o) results.push({ subject: s, predicate: p, object: o, confidence: Number(t?.confidence) || 0.8 });
}
} catch { /* 單段失敗跳過 */ }
}
return results;
}
/** 寫入一條三元組(走基本盤 API),已存在回傳 false,新寫入回傳 true。
* 查重 + 寫入全走 KbdbClient → triplet-crud,零 SQL / 零 D1。 */
export async function writeTripletToDb(
client: KbdbClient,
t: { subject: string; predicate: string; object: string; confidence?: number },
owner: string | null,
): Promise<boolean> {
// 查重:以 S-P-O 三欄精確比對(queryTriplets 取 template record 後在插件層 filter
const { count } = await queryTriplets(client, {
subject: t.subject,
predicate: t.predicate,
object: t.object,
limit: 1,
});
if (count > 0) return false;
await createTriplet(client, {
subject: t.subject,
predicate: t.predicate,
object: t.object,
confidence: t.confidence ?? 0.8,
owner_id: owner ?? undefined,
});
return true;
}