/** * 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; qs?: Record; } export interface ActionParams { sheet?: string; recordId?: string; values?: Record; slots?: string[]; description?: string; query?: string; mode?: 'keyword' | 'semantic'; filters?: { library?: string; source?: string; entry_type?: string }; } function need(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 = { 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 = { 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 端不存在,薄殼不拼裝)`); } }