GitHub Actions
Sync from GitHub (excluding README)
138f7fd
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;