/** * Kneron Toolchain Web GUI Task Scheduler * Express.js服務器,代理所有API調用 */ const express = require('express'); const cors = require('cors'); const multer = require('multer'); const axios = require('axios'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const morgan = require('morgan'); const compression = require('compression'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 4000; // 配置API服務 const API_SERVICES = { onnx: { url: process.env.ONNX_SERVICE_URL || 'http://localhost:5001', apiKey: process.env.ONNX_API_KEY || 'onnx-secret-key' }, bie: { url: process.env.BIE_SERVICE_URL || 'http://localhost:5002', apiKey: process.env.BIE_API_KEY || 'bie-secret-key' }, nef: { url: process.env.NEF_SERVICE_URL || 'http://localhost:5003', apiKey: process.env.NEF_API_KEY || 'nef-secret-key' } }; // 中間件配置 app.use(helmet()); app.use(compression()); app.use(morgan('combined')); // CORS配置 - 只允許前端訪問 app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:3000', credentials: true })); // 請求限流 const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分鐘 max: 100, // 限制每個IP每15分鐘最多100個請求 message: 'Too many requests from this IP, please try again later.' }); app.use('/api', limiter); // 解析JSON和URL編碼 app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // 文件上傳配置 const storage = multer.memoryStorage(); const upload = multer({ storage: storage, limits: { fileSize: 500 * 1024 * 1024 // 500MB } }); // 健康檢查 app.get('/health', (req, res) => { res.json({ service: 'task-scheduler', status: 'healthy', timestamp: new Date().toISOString(), services: Object.keys(API_SERVICES) }); }); // API代理函數 async function proxyRequest(service, endpoint, method = 'GET', data = null, files = null) { try { const serviceConfig = API_SERVICES[service]; if (!serviceConfig) { throw new Error(`Service ${service} not found`); } const url = `${serviceConfig.url}${endpoint}`; const headers = { 'X-API-Key': serviceConfig.apiKey, 'Content-Type': 'application/json' }; let response; if (method === 'GET') { response = await axios.get(url, { headers }); } else if (method === 'POST') { if (files) { // 處理文件上傳 const formData = new FormData(); Object.keys(files).forEach(key => { formData.append(key, files[key].buffer, files[key].originalname); }); response = await axios.post(url, formData, { headers: { 'X-API-Key': serviceConfig.apiKey, 'Content-Type': 'multipart/form-data' } }); } else { response = await axios.post(url, data, { headers }); } } else if (method === 'DELETE') { response = await axios.delete(url, { headers }); } return response.data; } catch (error) { console.error(`Proxy request failed for ${service}:`, error.message); throw error; } } // ONNX API代理 app.post('/api/onnx/upload', upload.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file provided' }); } const result = await proxyRequest('onnx', '/api/onnx/upload', 'POST', null, { file: req.file }); res.json(result); } catch (error) { console.error('ONNX upload error:', error.message); res.status(500).json({ error: error.message }); } }); app.post('/api/onnx/process', async (req, res) => { try { const result = await proxyRequest('onnx', '/api/onnx/process', 'POST', req.body); res.json(result); } catch (error) { console.error('ONNX process error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/onnx/tasks/:taskId/status', async (req, res) => { try { const result = await proxyRequest('onnx', `/api/onnx/tasks/${req.params.taskId}/status`); res.json(result); } catch (error) { console.error('ONNX status error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/onnx/tasks/:taskId/result', async (req, res) => { try { const result = await proxyRequest('onnx', `/api/onnx/tasks/${req.params.taskId}/result`); // 如果是文件下載,設置適當的響應頭 if (result.headers && result.headers['content-type']) { res.set('Content-Type', result.headers['content-type']); res.set('Content-Disposition', result.headers['content-disposition']); res.send(result.data); } else { res.json(result); } } catch (error) { console.error('ONNX result error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/onnx/tasks', async (req, res) => { try { const result = await proxyRequest('onnx', '/api/onnx/tasks'); res.json(result); } catch (error) { console.error('ONNX tasks list error:', error.message); res.status(500).json({ error: error.message }); } }); app.delete('/api/onnx/tasks/:taskId', async (req, res) => { try { const result = await proxyRequest('onnx', `/api/onnx/tasks/${req.params.taskId}`, 'DELETE'); res.json(result); } catch (error) { console.error('ONNX cancel error:', error.message); res.status(500).json({ error: error.message }); } }); // BIE API代理 app.post('/api/bie/process', async (req, res) => { try { const result = await proxyRequest('bie', '/api/bie/process', 'POST', req.body); res.json(result); } catch (error) { console.error('BIE process error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/bie/tasks/:taskId/status', async (req, res) => { try { const result = await proxyRequest('bie', `/api/bie/tasks/${req.params.taskId}/status`); res.json(result); } catch (error) { console.error('BIE status error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/bie/tasks/:taskId/result', async (req, res) => { try { const result = await proxyRequest('bie', `/api/bie/tasks/${req.params.taskId}/result`); if (result.headers && result.headers['content-type']) { res.set('Content-Type', result.headers['content-type']); res.set('Content-Disposition', result.headers['content-disposition']); res.send(result.data); } else { res.json(result); } } catch (error) { console.error('BIE result error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/bie/tasks', async (req, res) => { try { const result = await proxyRequest('bie', '/api/bie/tasks'); res.json(result); } catch (error) { console.error('BIE tasks list error:', error.message); res.status(500).json({ error: error.message }); } }); app.delete('/api/bie/tasks/:taskId', async (req, res) => { try { const result = await proxyRequest('bie', `/api/bie/tasks/${req.params.taskId}`, 'DELETE'); res.json(result); } catch (error) { console.error('BIE cancel error:', error.message); res.status(500).json({ error: error.message }); } }); // NEF API代理 app.post('/api/nef/process', async (req, res) => { try { const result = await proxyRequest('nef', '/api/nef/process', 'POST', req.body); res.json(result); } catch (error) { console.error('NEF process error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/nef/tasks/:taskId/status', async (req, res) => { try { const result = await proxyRequest('nef', `/api/nef/tasks/${req.params.taskId}/status`); res.json(result); } catch (error) { console.error('NEF status error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/nef/tasks/:taskId/result', async (req, res) => { try { const result = await proxyRequest('nef', `/api/nef/tasks/${req.params.taskId}/result`); if (result.headers && result.headers['content-type']) { res.set('Content-Type', result.headers['content-type']); res.set('Content-Disposition', result.headers['content-disposition']); res.send(result.data); } else { res.json(result); } } catch (error) { console.error('NEF result error:', error.message); res.status(500).json({ error: error.message }); } }); app.get('/api/nef/tasks', async (req, res) => { try { const result = await proxyRequest('nef', '/api/nef/tasks'); res.json(result); } catch (error) { console.error('NEF tasks list error:', error.message); res.status(500).json({ error: error.message }); } }); app.delete('/api/nef/tasks/:taskId', async (req, res) => { try { const result = await proxyRequest('nef', `/api/nef/tasks/${req.params.taskId}`, 'DELETE'); res.json(result); } catch (error) { console.error('NEF cancel error:', error.message); res.status(500).json({ error: error.message }); } }); // 完整工作流程 app.post('/api/workflow/complete', async (req, res) => { try { const { onnx_file_id, model_id, version, platform, data_dir } = req.body; // 1. 處理ONNX優化 const onnxResult = await proxyRequest('onnx', '/api/onnx/process', 'POST', { file_id: onnx_file_id, model_id, version, platform }); // 2. 處理BIE分析 const bieResult = await proxyRequest('bie', '/api/bie/process', 'POST', { onnx_file_id, model_id, version, platform, data_dir }); // 3. 處理NEF編譯 const nefResult = await proxyRequest('nef', '/api/nef/process', 'POST', { bie_file_id: bieResult.task_id, // 假設BIE結果包含文件ID model_id, version, platform }); res.json({ success: true, workflow_id: `workflow_${Date.now()}`, steps: { onnx: onnxResult, bie: bieResult, nef: nefResult }, message: 'Complete workflow submitted successfully' }); } catch (error) { console.error('Workflow error:', error.message); res.status(500).json({ error: error.message }); } }); // 錯誤處理中間件 app.use((error, req, res, next) => { console.error('Server error:', error); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong' }); }); // 404處理 app.use('*', (req, res) => { res.status(404).json({ error: 'Endpoint not found' }); }); // 啟動服務器 app.listen(PORT, () => { console.log(`🚀 Task scheduler running on port ${PORT}`); console.log(`📡 Proxying to services:`); Object.keys(API_SERVICES).forEach(service => { console.log(` ${service}: ${API_SERVICES[service].url}`); }); }); module.exports = app;