87f6de7c9f
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>
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
/**
|
||
* 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.1;D91/D92 正是拼裝出來的)。
|
||
*
|
||
* 對外的門=cypher-executor 的 /kbdb/* proxy(X-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 端不存在,薄殼不拼裝)`);
|
||
}
|
||
}
|