import Setting from './scanner/models/Setting.js'; /* * Anbindung an OpenAI fuer das CRM (bisher nutzte nur der Scanner ein Modell). * * Der Schluessel wird NICHT zum zweiten Mal gepflegt: es ist derselbe, den der * Scanner benutzt - Einstellungen -> Scanner, gespeichert als `openai_api_key` * in der Scanner-Tabelle `settings`. Ein zweiter Schluessel an zweiter Stelle * waere eine Quelle fuer den Fall "die eine Funktion geht, die andere nicht, * und keiner weiss warum". * * Bewusst ohne das `openai`-Paket: gebraucht wird ein einziger Aufruf, und * `fetch` ist in Node eingebaut. Der Scanner behaelt sein SDK - dort haengt * mehr daran. * * Der Schluessel bleibt auf dem Server. Der Browser spricht nie mit OpenAI. */ const ENDPOINT = 'https://api.openai.com/v1/chat/completions'; const TIMEOUT_MS = 120_000; /** Dasselbe Modell wie im Scanner - was dort ausreicht, reicht hier auch. */ export const AI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini'; class AiError extends Error { constructor(message, status = 502) { super(message); this.status = status; } } /** Der Schluessel des Scanners, ersatzweise aus der Umgebung. */ export async function aiKey() { const fromDb = await Setting.getVal('openai_api_key'); return (fromDb || process.env.OPENAI_API_KEY || '').trim(); } export async function aiConfigured() { return Boolean(await aiKey()); } /** * Ein Aufruf an das Modell, Antwort als JSON. * * @param {object} options * @param {string} options.system * @param {string} options.user * @param {number} [options.maxTokens] */ export async function askModelJson({ system, user, maxTokens }) { const key = await aiKey(); if (!key) { throw new AiError( 'Es ist kein OpenAI-Schlüssel hinterlegt (Einstellungen → Scanner).', 503 ); } const body = { model: AI_MODEL, messages: [ ...(system ? [{ role: 'system', content: system }] : []), { role: 'user', content: user }, ], response_format: { type: 'json_object' }, }; if (maxTokens) body.max_tokens = maxTokens; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); let response; try { response = await fetch(ENDPOINT, { method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal, }); } catch (err) { if (err?.name === 'AbortError') throw new AiError('Die KI hat nicht rechtzeitig geantwortet.', 504); throw new AiError('OpenAI ist gerade nicht erreichbar.', 502); } finally { clearTimeout(timer); } const payload = await response.json().catch(() => null); if (!response.ok) { // Die Meldung von OpenAI ist meist praezise ("model not found", "quota") - // sie hilft mehr als ein eigener Text, enthaelt aber nie den Schluessel. throw new AiError(payload?.error?.message || `OpenAI antwortete mit HTTP ${response.status}.`, 502); } const text = payload?.choices?.[0]?.message?.content; if (!text) throw new AiError('Die KI hat keine Antwort geliefert.', 502); try { return JSON.parse(text); } catch { throw new AiError('Die KI hat kein gültiges JSON geliefert.', 502); } } export { AiError };