const fs = require('fs'); const path = require('path'); const https = require('https'); const http = require('http'); const { getDb } = require('../db'); const AI_CONFIG_PATH = path.join(__dirname, '..', 'data', 'ai_config.json'); const PROVIDER_PRESETS = { openrouter: { label: 'OpenRouter', base_url: 'https://openrouter.ai/api/v1', models: ['qwen/qwen3-coder:free', 'meta-llama/llama-3.3-70b-instruct:free', 'google/gemma-3-27b-it:free', 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o-mini'] }, openai: { label: 'OpenAI', base_url: 'https://api.openai.com/v1', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-mini'] }, anthropic: { label: 'Anthropic', base_url: 'https://api.anthropic.com/v1', models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] }, deepseek: { label: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', models: ['deepseek-chat', 'deepseek-reasoner'] }, groq: { label: 'Groq', base_url: 'https://api.groq.com/openai/v1', models: ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768'] }, ollama: { label: 'Ollama (local)', base_url: 'http://localhost:11434/v1', models: ['llama3.2', 'mistral', 'codellama', 'qwen2.5'] }, azure: { label: 'Azure OpenAI', base_url: '', models: ['gpt-4o', 'gpt-4o-mini'] }, custom: { label: 'Custom (OpenAI-compatible)', base_url: '', models: [] } }; function initAiSchema() { const db = getDb(); db.exec(` CREATE TABLE IF NOT EXISTS ai_providers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, provider_type TEXT NOT NULL DEFAULT 'openrouter', api_key TEXT DEFAULT '', base_url TEXT DEFAULT '', default_model TEXT DEFAULT '', is_active INTEGER DEFAULT 1, is_default INTEGER DEFAULT 0, extra_headers TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS integrations ( id TEXT PRIMARY KEY, name TEXT DEFAULT '', api_key TEXT DEFAULT '', webhook_url TEXT DEFAULT '', config TEXT DEFAULT '{}', status TEXT DEFAULT 'inactive', created_at TEXT DEFAULT (datetime('now')) ); `); const count = db.prepare('SELECT COUNT(*) as c FROM ai_providers').get().c; if (count === 0 && process.env.DEEPSEEK_API_KEY) { db.prepare(`INSERT INTO ai_providers (name, provider_type, api_key, base_url, default_model, is_default) VALUES (?, ?, ?, ?, ?, 1)`).run( 'OpenRouter Default', 'openrouter', process.env.DEEPSEEK_API_KEY, 'https://openrouter.ai/api/v1', 'qwen/qwen3-coder:free' ); } } let _aiSchemaReady = false; function ensureAiSchema() { if (_aiSchemaReady) return; initAiSchema(); _aiSchemaReady = true; } function getProviders() { ensureAiSchema(); return getDb().prepare('SELECT id, name, provider_type, base_url, default_model, is_active, is_default, created_at, updated_at FROM ai_providers ORDER BY is_default DESC, name').all(); } function getProvider(id) { ensureAiSchema(); return getDb().prepare('SELECT * FROM ai_providers WHERE id = ?').get(id); } function getDefaultProvider() { ensureAiSchema(); let p = getDb().prepare('SELECT * FROM ai_providers WHERE is_default = 1 AND is_active = 1 LIMIT 1').get(); if (!p) p = getDb().prepare('SELECT * FROM ai_providers WHERE is_active = 1 ORDER BY id LIMIT 1').get(); return p; } function saveProvider(data) { ensureAiSchema(); const db = getDb(); if (data.is_default) { db.prepare('UPDATE ai_providers SET is_default = 0').run(); } if (data.id) { db.prepare(`UPDATE ai_providers SET name=?, provider_type=?, api_key=?, base_url=?, default_model=?, is_active=?, is_default=?, updated_at=datetime('now') WHERE id=?`).run( data.name, data.provider_type, data.api_key || '', data.base_url || '', data.default_model || '', data.is_active ? 1 : 0, data.is_default ? 1 : 0, data.id ); syncAiConfigFile(); return data.id; } const r = db.prepare(`INSERT INTO ai_providers (name, provider_type, api_key, base_url, default_model, is_active, is_default) VALUES (?, ?, ?, ?, ?, ?, ?)`).run( data.name, data.provider_type, data.api_key || '', data.base_url || '', data.default_model || '', data.is_active !== false ? 1 : 0, data.is_default ? 1 : 0 ); syncAiConfigFile(); return r.lastInsertRowid; } function deleteProvider(id) { ensureAiSchema(); getDb().prepare('DELETE FROM ai_providers WHERE id = ?').run(id); syncAiConfigFile(); } function syncAiConfigFile() { ensureAiSchema(); const providers = getDb().prepare('SELECT * FROM ai_providers WHERE is_active = 1').all(); const defaultProvider = providers.find(p => p.is_default) || providers[0] || null; const config = { default_provider_id: defaultProvider?.id || null, providers: providers.map(p => ({ id: p.id, name: p.name, provider_type: p.provider_type, api_key: p.api_key, base_url: p.base_url || PROVIDER_PRESETS[p.provider_type]?.base_url || '', default_model: p.default_model, is_default: !!p.is_default })) }; const dir = path.dirname(AI_CONFIG_PATH); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2)); const leadgenConfig = { api_key: defaultProvider?.api_key || process.env.DEEPSEEK_API_KEY || '', base_url: defaultProvider?.base_url || 'https://openrouter.ai/api/v1', model: defaultProvider?.default_model || 'qwen/qwen3-coder:free', provider_type: defaultProvider?.provider_type || 'openrouter' }; fs.writeFileSync(path.join(dir, 'leadgen_config.json'), JSON.stringify(leadgenConfig, null, 2)); } function maskKey(key) { if (!key || key.length < 8) return key ? '••••••••' : ''; return key.slice(0, 6) + '••••' + key.slice(-4); } function chatCompletion(messages, options = {}) { return new Promise((resolve, reject) => { const provider = options.providerId ? getProvider(options.providerId) : getDefaultProvider(); if (!provider || !provider.api_key) { return reject(new Error('Geen actieve AI provider geconfigureerd. Ga naar /ai om een provider toe te voegen.')); } const preset = PROVIDER_PRESETS[provider.provider_type] || PROVIDER_PRESETS.custom; const baseUrl = provider.base_url || preset.base_url; const model = options.model || provider.default_model || preset.models[0]; let url; try { url = new URL('/chat/completions', baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'); } catch (e) { return reject(new Error('Ongeldige base URL: ' + baseUrl)); } const body = JSON.stringify({ model, messages, temperature: options.temperature ?? 0.7, max_tokens: options.max_tokens ?? 4096 }); const isHttps = url.protocol === 'https:'; const lib = isHttps ? https : http; const headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }; if (provider.provider_type === 'anthropic') { headers['x-api-key'] = provider.api_key; headers['anthropic-version'] = '2023-06-01'; } else { headers['Authorization'] = `Bearer ${provider.api_key}`; } if (provider.provider_type === 'openrouter') { headers['HTTP-Referer'] = process.env.BASE_URL || 'http://localhost:3000'; headers['X-Title'] = 'Mek-Tech Platform'; } const reqOpts = { hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname, method: 'POST', headers, timeout: 120000 }; const req = lib.request(reqOpts, (res) => { let data = ''; res.on('data', c => { data += c; }); res.on('end', () => { try { const parsed = JSON.parse(data); if (res.statusCode >= 400) { reject(new Error(parsed.error?.message || parsed.message || data.slice(0, 200))); } else { resolve(parsed); } } catch (e) { reject(new Error('AI response parse error: ' + data.slice(0, 200))); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('AI request timeout')); }); req.write(body); req.end(); }); } module.exports = { PROVIDER_PRESETS, initAiSchema: ensureAiSchema, getProviders, getProvider, getDefaultProvider, saveProvider, deleteProvider, syncAiConfigFile, maskKey, chatCompletion };