const express = require('express'); const axios = require('axios'); const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER } = require('../database'); const { EVENT_TYPES } = require('../constants/eventTypes'); const router = express.Router(); function maskApiKey(apiKey) { if (!apiKey || apiKey.length < 8) { return '****'; } return apiKey.substring(0, 4) + '****' + apiKey.substring(apiKey.length - 4); } function createAIRouter(db, manager, requireAdminKey, requireServerOrAdminKey) { router.get('/config', requireAdminKey, (req, res) => { try { const config = db.getAIConfig(); if (!config) { return res.status(404).json({ detail: 'AI configuration not found' }); } res.json({ apiUrl: config.apiUrl, modelId: config.modelId, apiKey: maskApiKey(config.apiKey), enabled: config.enabled, systemPrompt: config.systemPrompt, createdAt: config.createdAt, updatedAt: config.updatedAt }); } catch (error) { res.status(500).json({ detail: error.message }); } }); router.post('/config', requireAdminKey, (req, res) => { try { const { api_url, model_id, api_key, enabled = true, system_prompt = null } = req.body; if (!api_url || !model_id || !api_key) { return res.status(400).json({ detail: 'api_url, model_id, and api_key are required' }); } const config = db.saveAIConfig({ apiUrl: api_url, modelId: model_id, apiKey: api_key, enabled, systemPrompt: system_prompt }); res.status(201).json({ apiUrl: config.apiUrl, modelId: config.modelId, apiKey: maskApiKey(config.apiKey), enabled: config.enabled, systemPrompt: config.systemPrompt, createdAt: config.createdAt, updatedAt: config.updatedAt }); } catch (error) { res.status(500).json({ detail: error.message }); } }); router.patch('/config', requireAdminKey, (req, res) => { try { const existing = db.getAIConfig(); if (!existing) { return res.status(404).json({ detail: 'AI configuration not found' }); } const { api_url, model_id, api_key, enabled = true, system_prompt = null } = req.body; // Only update fields that are provided (allow partial updates) const newConfig = { apiUrl: api_url !== undefined ? api_url : existing.apiUrl, modelId: model_id !== undefined ? model_id : existing.modelId, enabled: enabled !== undefined ? enabled : existing.enabled, systemPrompt: system_prompt !== undefined ? system_prompt : existing.systemPrompt, apiKey: existing.apiKey // Default to existing key }; // Only update apiKey if a non-empty string is provided // If api_key is provided but empty string, we assume user wants to keep existing key (frontend logic) // If user wants to clear key, they should probably delete config or we need explicit null handling. // For now, secure default is: if api_key is present and not empty, update it. if (api_key && api_key.trim() !== '') { newConfig.apiKey = api_key; } const config = db.saveAIConfig(newConfig); res.json({ apiUrl: config.apiUrl, modelId: config.modelId, apiKey: maskApiKey(config.apiKey), enabled: config.enabled, systemPrompt: config.systemPrompt, createdAt: config.createdAt, updatedAt: config.updatedAt }); } catch (error) { console.error('[AI Config Update Error]', error); res.status(500).json({ detail: error.message }); } }); router.delete('/config', requireAdminKey, (req, res) => { try { const deleted = db.deleteAIConfig(); if (!deleted) { return res.status(404).json({ detail: 'AI configuration not found' }); } res.status(204).send(); } catch (error) { res.status(500).json({ detail: error.message }); } }); router.post('/config/test', requireAdminKey, async (req, res) => { let config = null; try { config = db.getAIConfig(); if (!config) { return res.status(404).json({ detail: 'AI configuration not found' }); } const response = await axios.post( config.apiUrl, { model: config.modelId, messages: [ { role: 'user', content: 'Hello, this is a test message. Please respond with "OK".' } ], max_tokens: 10 }, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }, timeout: 30000 } ); res.json({ success: true, message: 'Connection successful', model: response.data.model || config.modelId, response: response.data.choices?.[0]?.message?.content || 'OK' }); } catch (error) { // Enhanced Error Logging console.error('[AI Connection Test Failed]'); if (config) { console.error('URL:', config.apiUrl); console.error('Model:', config.modelId); } else { console.error('Config is undefined or null'); } if (error.response) { console.error('Status:', error.response.status); console.error('Data:', JSON.stringify(error.response.data, null, 2)); } else { console.error('Error:', error.message); } const errorMessage = error.response?.data?.error?.message || error.message; res.status(400).json({ success: false, message: 'Connection failed', error: errorMessage, details: error.response?.data }); } }); router.post('/chat', requireServerOrAdminKey, async (req, res) => { try { const config = db.getAIConfig(); if (!config) { return res.status(404).json({ detail: 'AI configuration not found' }); } if (!config.enabled) { return res.status(403).json({ detail: 'AI feature is disabled' }); } const { message, player_name, server_id } = req.body; if (!message) { return res.status(400).json({ detail: 'message is required' }); } const isAdmin = req.apiKey.keyType === KEY_TYPE_ADMIN; const targetServerId = isAdmin ? (server_id || req.apiKey.serverId) : req.apiKey.serverId; const messages = []; if (config.systemPrompt) { messages.push({ role: 'system', content: config.systemPrompt }); } messages.push({ role: 'user', content: message }); const response = await axios.post( config.apiUrl, { model: config.modelId, messages: messages }, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }, timeout: 60000 } ); const reply = response.data.choices?.[0]?.message?.content || ''; const usage = response.data.usage || null; const chatEvent = { event_type: EVENT_TYPES.AI_CHAT, server_name: targetServerId || 'unknown', timestamp: new Date().toISOString(), data: { player_name: player_name || 'unknown', message: message, reply: reply, model: config.modelId } }; db.logEvent(chatEvent, req.apiKey.id); if (manager) { manager.broadcastToAll({ type: 'minecraft_event', event: chatEvent, source_key_id_prefix: req.apiKey.id.substring(0, 8) }); } res.json({ success: true, reply: reply, model: response.data.model || config.modelId, usage: usage }); } catch (error) { const errorMessage = error.response?.data?.error?.message || error.message; console.error('[AI Chat Error]', errorMessage); res.status(500).json({ success: false, detail: 'AI request failed', error: errorMessage }); } }); return router; } module.exports = createAIRouter;