Major Features: - ✨ Multi-LLM provider support (DeepSeek, Ollama, OpenAI, Custom) - 🤖 Admin panel LLM configuration management UI - 🔄 Dynamic provider switching without restart - 🧪 Built-in API connection testing - 🔒 Secure API key management Backend Changes: - Add routes/llmConfig.js: Complete LLM config CRUD API - Update routes/analyze.js: Use database LLM configuration - Update server.js: Add LLM config routes - Add scripts/add-deepseek-config.js: DeepSeek setup script Frontend Changes: - Update src/pages/AdminPage.jsx: Add LLM Config tab + modal - Update src/services/api.js: Add LLM config API methods - Provider presets for DeepSeek, Ollama, OpenAI - Test connection feature in config modal Configuration: - Update .env.example: Add DeepSeek API configuration - Update package.json: Add llm:add-deepseek script Documentation: - Add docs/LLM_CONFIGURATION_GUIDE.md: Complete guide - Add DEEPSEEK_INTEGRATION.md: Integration summary - Quick setup instructions for DeepSeek API Endpoints: - GET /api/llm-config: List all configurations - GET /api/llm-config/active: Get active configuration - POST /api/llm-config: Create configuration - PUT /api/llm-config/🆔 Update configuration - PUT /api/llm-config/:id/activate: Activate configuration - DELETE /api/llm-config/🆔 Delete configuration - POST /api/llm-config/test: Test API connection Database: - Uses existing llm_configs table - Only one config active at a time - Fallback to Ollama if no database config Security: - Admin-only access to LLM configuration - API keys never returned in GET requests - Audit logging for all config changes - Cannot delete active configuration DeepSeek Model: - Model: deepseek-chat - High-quality 5 Why analysis - Excellent Chinese language support - Cost-effective pricing 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
219 lines
6.1 KiB
JavaScript
219 lines
6.1 KiB
JavaScript
import express from 'express';
|
|
import cors from 'cors';
|
|
import helmet from 'helmet';
|
|
import session from 'express-session';
|
|
import rateLimit from 'express-rate-limit';
|
|
import dotenv from 'dotenv';
|
|
import { testConnection } from './config.js';
|
|
import { sessionConfig, securityConfig, serverConfig } from './config.js';
|
|
import { notFoundHandler, errorHandler } from './middleware/errorHandler.js';
|
|
|
|
// Routes
|
|
import authRoutes from './routes/auth.js';
|
|
import analyzeRoutes from './routes/analyze.js';
|
|
import adminRoutes from './routes/admin.js';
|
|
import llmConfigRoutes from './routes/llmConfig.js';
|
|
|
|
// 載入環境變數
|
|
dotenv.config();
|
|
|
|
const app = express();
|
|
const PORT = serverConfig.port;
|
|
|
|
// ============================================
|
|
// Middleware Setup
|
|
// ============================================
|
|
|
|
// Security Headers
|
|
app.use(helmet({
|
|
contentSecurityPolicy: false, // 暫時關閉 CSP 以便開發
|
|
}));
|
|
|
|
// CORS
|
|
app.use(cors({
|
|
origin: [`http://localhost:${serverConfig.clientPort}`, 'http://localhost:5173'],
|
|
credentials: true
|
|
}));
|
|
|
|
// Body Parser
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
|
|
// Session
|
|
app.use(session({
|
|
secret: sessionConfig.secret,
|
|
resave: sessionConfig.resave,
|
|
saveUninitialized: sessionConfig.saveUninitialized,
|
|
cookie: sessionConfig.cookie,
|
|
name: '5why.sid'
|
|
}));
|
|
|
|
// Rate Limiting
|
|
const limiter = rateLimit({
|
|
windowMs: securityConfig.rateLimitWindowMs,
|
|
max: securityConfig.rateLimitMax,
|
|
message: {
|
|
success: false,
|
|
error: '請求過於頻繁,請稍後再試'
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false
|
|
});
|
|
|
|
app.use('/api/', limiter);
|
|
|
|
// Request Logging (Development)
|
|
if (process.env.NODE_ENV === 'development') {
|
|
app.use((req, res, next) => {
|
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Routes
|
|
// ============================================
|
|
|
|
// Health Check
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
message: 'Server is running',
|
|
timestamp: new Date().toISOString(),
|
|
environment: process.env.NODE_ENV || 'development'
|
|
});
|
|
});
|
|
|
|
// Database Health Check
|
|
app.get('/health/db', async (req, res) => {
|
|
try {
|
|
const isConnected = await testConnection();
|
|
res.json({
|
|
status: isConnected ? 'ok' : 'error',
|
|
database: isConnected ? 'connected' : 'disconnected'
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
status: 'error',
|
|
database: 'error',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// API Routes
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/analyze', analyzeRoutes);
|
|
app.use('/api/admin', adminRoutes);
|
|
app.use('/api/llm-config', llmConfigRoutes);
|
|
|
|
// Root Endpoint
|
|
app.get('/', (req, res) => {
|
|
res.json({
|
|
message: '5 Why Root Cause Analyzer API',
|
|
version: '1.0.0',
|
|
endpoints: {
|
|
health: '/health',
|
|
auth: {
|
|
login: 'POST /api/auth/login',
|
|
logout: 'POST /api/auth/logout',
|
|
me: 'GET /api/auth/me'
|
|
},
|
|
analyze: {
|
|
create: 'POST /api/analyze',
|
|
history: 'GET /api/analyze/history',
|
|
detail: 'GET /api/analyze/:id',
|
|
translate: 'POST /api/analyze/translate'
|
|
},
|
|
admin: {
|
|
dashboard: 'GET /api/admin/dashboard',
|
|
users: 'GET /api/admin/users',
|
|
analyses: 'GET /api/admin/analyses',
|
|
auditLogs: 'GET /api/admin/audit-logs'
|
|
},
|
|
llmConfig: {
|
|
list: 'GET /api/llm-config',
|
|
active: 'GET /api/llm-config/active',
|
|
create: 'POST /api/llm-config',
|
|
update: 'PUT /api/llm-config/:id',
|
|
activate: 'PUT /api/llm-config/:id/activate',
|
|
delete: 'DELETE /api/llm-config/:id',
|
|
test: 'POST /api/llm-config/test'
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// Error Handling
|
|
// ============================================
|
|
|
|
// 404 Handler
|
|
app.use(notFoundHandler);
|
|
|
|
// Global Error Handler
|
|
app.use(errorHandler);
|
|
|
|
// ============================================
|
|
// Server Startup
|
|
// ============================================
|
|
|
|
async function startServer() {
|
|
try {
|
|
console.log('\n╔════════════════════════════════════════════╗');
|
|
console.log('║ 5 Why Analyzer - Server Starting... ║');
|
|
console.log('╚════════════════════════════════════════════╝\n');
|
|
|
|
// Test database connection
|
|
console.log('📡 Testing database connection...');
|
|
const dbConnected = await testConnection();
|
|
|
|
if (!dbConnected) {
|
|
console.warn('⚠️ Warning: Database connection failed');
|
|
console.warn(' Server will start but database features will not work');
|
|
}
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log('\n✅ Server is running!');
|
|
console.log(` URL: http://localhost:${PORT}`);
|
|
console.log(` Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
console.log(` Database: ${dbConnected ? 'Connected ✓' : 'Disconnected ✗'}`);
|
|
console.log(`\n📚 API Documentation: http://localhost:${PORT}/`);
|
|
console.log(`🔍 Health Check: http://localhost:${PORT}/health`);
|
|
console.log('\n💡 Press Ctrl+C to stop the server\n');
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Failed to start server:');
|
|
console.error(' Error:', error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Handle uncaught exceptions
|
|
process.on('uncaughtException', (error) => {
|
|
console.error('Uncaught Exception:', error);
|
|
process.exit(1);
|
|
});
|
|
|
|
// Handle unhandled promise rejections
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
process.exit(1);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', () => {
|
|
console.log('\n📴 SIGTERM received. Shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('\n📴 SIGINT received. Shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|
|
|
|
// Start the server
|
|
startServer();
|