Spaces:
Sleeping
Sleeping
| 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; | |