/** * Brain-API-Client fuer die App. * * Die App hat keinen direkten HTTP-Zugriff aufs Brain (nur via RVS). Wir * tunneln alle Memory-Operationen ueber den generischen brain_request / * brain_response RVS-Channel den die Bridge implementiert. * * Pattern: pro Call eine eindeutige requestId, Listener wartet auf passende * brain_response, Promise loest auf / wird abgelehnt bei status>=400. */ import rvs from './rvs'; type AnyJson = any; interface PendingRequest { resolve: (data: AnyJson) => void; reject: (err: Error) => void; timer: ReturnType; expectBinary?: boolean; } const pending = new Map(); let installed = false; function _ensureListener() { if (installed) return; installed = true; rvs.onMessage((msg: any) => { if (!msg || msg.type !== 'brain_response') return; const p = msg.payload || {}; const reqId: string = p.requestId || ''; const handler = pending.get(reqId); if (!handler) return; pending.delete(reqId); clearTimeout(handler.timer); const status: number = Number(p.status || 0); if (status >= 200 && status < 300) { if (handler.expectBinary) { handler.resolve({ base64: p.base64 || '', contentType: p.contentType || '' }); } else { handler.resolve(p.json !== undefined ? p.json : (p.text !== undefined ? p.text : null)); } } else { const detail = (p.json && p.json.detail) || p.text || `HTTP ${status}`; handler.reject(new Error(`Brain ${status}: ${detail}`)); } }); } let _nextId = 0; function _newRequestId(): string { _nextId += 1; return `brain_${Date.now().toString(36)}_${_nextId}`; } /** Mini-Query-String-Builder ohne URLSearchParams (Hermes-Polyfill kennt * kein URLSearchParams.set, crasht). Akzeptiert object mit string/number/ * bool-Values; undefined/null/leere Strings werden ausgelassen. */ function _qs(params: Record): string { const parts: string[] = []; for (const [k, v] of Object.entries(params)) { if (v === undefined || v === null || v === '') continue; parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`); } return parts.length ? `?${parts.join('&')}` : ''; } interface SendOpts { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; body?: AnyJson; bodyBase64?: string; contentType?: string; expectBinary?: boolean; timeoutMs?: number; } function _send(path: string, opts: SendOpts = {}): Promise { _ensureListener(); return new Promise((resolve, reject) => { const requestId = _newRequestId(); const timer = setTimeout(() => { if (pending.delete(requestId)) { reject(new Error(`Brain-Timeout fuer ${path}`)); } }, opts.timeoutMs || 30000); pending.set(requestId, { resolve, reject, timer, expectBinary: opts.expectBinary }); rvs.send('brain_request' as any, { requestId, method: opts.method || 'GET', path, ...(opts.body !== undefined ? { body: opts.body } : {}), ...(opts.bodyBase64 ? { bodyBase64: opts.bodyBase64 } : {}), ...(opts.contentType ? { contentType: opts.contentType } : {}), }); }); } // ── Typen ──────────────────────────────────────────────────────────── export interface MemoryAttachment { name: string; mime: string; size: number; path: string; } export interface Memory { id: string; type: string; title: string; content: string; pinned: boolean; category: string; source: string; tags: string[]; created_at: string; updated_at: string; conversation_id?: string | null; score?: number | null; attachments?: MemoryAttachment[]; } /** Trigger-Manifest wie aus Brain `/triggers/list` zurueckkommt. */ export interface Trigger { name: string; type: 'timer' | 'watcher' | string; active: boolean; author?: string; message: string; fires_at?: string; // ISO, nur timer condition?: string; // nur watcher check_interval_sec?: number; // nur watcher throttle_sec?: number; // nur watcher fire_count?: number; last_fired_at?: string | null; last_checked_at?: string | null; created_at?: string; updated_at?: string; } // ── Memory CRUD ────────────────────────────────────────────────────── export const brainApi = { /** Einzelne Memory holen (mit allen Feldern inkl. Anhaenge) */ getMemory(id: string): Promise { return _send(`/memory/get/${encodeURIComponent(id)}`); }, /** Liste aller Memories, optional nach Type gefiltert. */ listMemories(opts: { type?: string; limit?: number } = {}): Promise { const qs = _qs({ type: opts.type, limit: opts.limit || 500 }); return _send(`/memory/list${qs}`); }, /** Volltext-Substring-Suche. */ searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise { const qs = _qs({ q, type: opts.type, include_pinned: opts.includePinned !== false, k: opts.k || 50, }); return _send(`/memory/search-text${qs}`); }, /** Semantische Suche (Embedder). */ searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise { const qs = _qs({ q, type: opts.type, include_pinned: opts.includePinned !== false, k: opts.k || 10, score_threshold: opts.threshold ?? 0.30, }); return _send(`/memory/search${qs}`); }, /** Memory anlegen. */ saveMemory(body: { type: string; title: string; content: string; pinned?: boolean; category?: string; tags?: string[]; }): Promise { return _send('/memory/save', { method: 'POST', body: { source: 'app', ...body }, }); }, /** Memory aktualisieren (Patch — nur uebergebene Felder werden geaendert). */ updateMemory(id: string, body: Partial>): Promise { return _send(`/memory/update/${encodeURIComponent(id)}`, { method: 'PATCH', body, }); }, /** Memory loeschen. */ deleteMemory(id: string): Promise<{ deleted: string }> { return _send(`/memory/delete/${encodeURIComponent(id)}`, { method: 'DELETE', timeoutMs: 15000, }); }, // ── Anhaenge ──────────────────────────────────────────────────────── /** Datei als Anhang an die Memory haengen (Base64-Upload). */ uploadAttachment(memoryId: string, name: string, base64: string): Promise { return _send(`/memory/${encodeURIComponent(memoryId)}/attachments`, { method: 'POST', body: { name, data_base64: base64 }, timeoutMs: 120000, }); }, /** Anhang loeschen. */ deleteAttachment(memoryId: string, filename: string): Promise { return _send( `/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`, { method: 'DELETE' }, ); }, /** Anhang-Bytes holen (fuer Vorschau / Download). Liefert Base64. */ getAttachmentBytes(memoryId: string, filename: string): Promise<{ base64: string; contentType: string }> { return _send( `/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`, { expectBinary: true, timeoutMs: 60000 }, ); }, // ── Triggers ──────────────────────────────────────────────────────── /** Liste aller Trigger (aktive + inaktive). */ listTriggers(): Promise { return _send('/triggers/list'); }, /** Einzelnen Trigger holen (inkl. fire_count, last_fired_at, ...). */ getTrigger(name: string): Promise { return _send(`/triggers/${encodeURIComponent(name)}`); }, /** Verfuegbare Condition-Variablen + Funktionen (fuer Watcher-Editor). */ getTriggerConditions(): Promise<{ variables: any[]; functions: any[] }> { return _send('/triggers/conditions'); }, /** Trigger-Logs (last N Feuerungen). */ getTriggerLogs(name: string, limit: number = 50): Promise { return _send(`/triggers/${encodeURIComponent(name)}/logs?limit=${limit}`); }, /** Timer anlegen. fires_at = ISO timestamp (UTC). */ createTimer(body: { name: string; fires_at: string; message: string; author?: string }): Promise { return _send('/triggers/timer', { method: 'POST', body: { author: 'app', ...body }, }); }, /** Watcher anlegen. */ createWatcher(body: { name: string; condition: string; message: string; check_interval_sec?: number; throttle_sec?: number; author?: string; }): Promise { return _send('/triggers/watcher', { method: 'POST', body: { author: 'app', ...body }, }); }, /** Trigger patchen (active/message/condition/throttle/interval/fires_at). */ updateTrigger(name: string, body: Partial<{ active: boolean; message: string; condition: string; throttle_sec: number; check_interval_sec: number; fires_at: string; }>): Promise { return _send(`/triggers/${encodeURIComponent(name)}`, { method: 'PATCH', body, }); }, /** Trigger loeschen. */ deleteTrigger(name: string): Promise<{ deleted: string }> { return _send(`/triggers/${encodeURIComponent(name)}`, { method: 'DELETE', timeoutMs: 15000, }); }, }; export default brainApi;