feat(n8n): KBDB community node 薄殼——對照表 那半的八個動作

leo 2026-08-15「有效才正式開發」下一格:紙上考卷驗過的動作集做成真的 n8n 節點。
詞彙照定案(sheet/field/record,InkStoneCo system-dev/wiki/kbdb-詞彙表.md)。

- 動作 ↔ API 映射收斂在 nodes/Kbdb/actions.ts(純函式),20 個 vitest 釘死
  ——就是動作對照表  那半的機器版;🔴/◐ 的動作刻意不存在,呼叫丟
  「沒有這個能力」(07-thin-shell §3.1:API 沒有的能力不拼裝)
- 對外走 cypher-executor /kbdb/* proxy(X-Arcrun-API-Key),不直連 kbdb worker
- Add a field 不掛:proxy 無 PATCH /kbdb/templates(實測 404);base 端
  PATCH /templates/:id 語意是 slots 整組覆蓋(本地實測),不是加一欄

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-15 19:33:55 +08:00
parent 576afa2364
commit 87f6de7c9f
9 changed files with 3504 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
/**
* KBDB 動作 → HTTP 請求的純映射(薄殼,rule 07)。
*
* 每個動作=**恰好一個** API 呼叫。這個檔案刻意是純函式,讓「動作對到哪支 API」
* 可以被單元測試釘死——它就是 `system-dev/docs/4-guides/kbdb-動作對照表.md`(頂層
* InkStoneCo)標 ✅ 那半的機器版。
*
* 🔴 禁止在這裡拼裝:對照表標 🔴/◐ 而 API 沒有的能力(Archive、關係整組、
* Add a field),這裡**沒有**對應動作——不是漏做,是「回報沒有能力」的實作方式
* (見 .claude/rules/07-thin-shell.md §3.1D91/D92 正是拼裝出來的)。
*
* 對外的門=cypher-executor 的 /kbdb/* proxyX-Arcrun-API-Key 租戶隔離)。
* KBDB worker 本體只收系統內部的 Bearer,外面打一定 401(設計,不是故障)。
*/
export interface KbdbRequest {
method: 'GET' | 'POST' | 'PATCH';
path: string;
body?: Record<string, unknown>;
qs?: Record<string, string>;
}
export interface ActionParams {
sheet?: string;
recordId?: string;
values?: Record<string, string>;
slots?: string[];
description?: string;
query?: string;
mode?: 'keyword' | 'semantic';
filters?: { library?: string; source?: string; entry_type?: string };
}
function need<T>(v: T | undefined | null, what: string): T {
if (v === undefined || v === null || (typeof v === 'string' && v === '')) {
throw new Error(`缺少必要參數:${what}`);
}
return v;
}
export function buildRequest(resource: string, operation: string, p: ActionParams): KbdbRequest {
const key = `${resource}.${operation}`;
switch (key) {
// ── record ─────────────────────────────────────────────
case 'record.append':
return {
method: 'POST',
path: '/kbdb/records',
body: { template: need(p.sheet, 'sheet'), values: need(p.values, 'values') },
};
case 'record.get':
return { method: 'GET', path: `/kbdb/records/${encodeURIComponent(need(p.recordId, 'recordId'))}` };
case 'record.getMany':
return { method: 'GET', path: `/kbdb/records/by-template/${encodeURIComponent(need(p.sheet, 'sheet'))}` };
case 'record.update':
return {
method: 'PATCH',
path: `/kbdb/records/${encodeURIComponent(need(p.recordId, 'recordId'))}`,
body: { values: need(p.values, 'values') },
};
// ── sheet ──────────────────────────────────────────────
case 'sheet.create': {
const slots = need(p.slots, 'fields');
if (!Array.isArray(slots) || slots.length === 0) throw new Error('fields 至少要有一欄');
const body: Record<string, unknown> = { name: need(p.sheet, 'sheet'), slots };
if (p.description) body.description = p.description;
return { method: 'POST', path: '/kbdb/templates', body };
}
case 'sheet.getSchema':
return { method: 'GET', path: `/kbdb/templates/${encodeURIComponent(need(p.sheet, 'sheet'))}` };
case 'sheet.list':
return { method: 'GET', path: '/kbdb/templates' };
// ── search ─────────────────────────────────────────────
case 'search.search': {
const qs: Record<string, string> = { q: need(p.query, 'query') };
if (p.mode) qs.mode = p.mode;
for (const k of ['library', 'source', 'entry_type'] as const) {
const v = p.filters?.[k];
if (v) qs[k] = v;
}
return { method: 'GET', path: '/kbdb/search', qs };
}
default:
// 誠實回報,不拼裝(07-thin-shell §3.1;對照表「怎麼用這張表」第 2 條)
throw new Error(`KBDB 目前沒有「${key}」這個能力(API 端不存在,薄殼不拼裝)`);
}
}