Spaces:
Running
Running
| /** | |
| * QUANTA Engine Server | |
| * RFQ-to-Revenue Proposal Orchestrator for ideaxWeb | |
| * Express API + Telegram webhook + Web dashboard | |
| */ | |
| const express = require('express'); | |
| const cors = require('cors'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const multer = require('multer'); | |
| const axios = require('axios'); | |
| // Skills | |
| const webScraper = require('./skills/web_scraper'); | |
| const leadQualifier = require('./skills/lead_qualifier'); | |
| const proposalGenerator = require('./skills/proposal_generator'); | |
| const pdfBuilder = require('./skills/pdf_builder'); | |
| const projectManager = require('./skills/project_manager'); | |
| const vinosBridge = require('./skills/vinos_bridge'); | |
| const app = express(); | |
| const PORT = process.env.PORT || 7860; | |
| const ADMIN_KEY = process.env.QUANTA_ADMIN_KEY || ''; | |
| const API_KEY = process.env.QUANTA_API_KEY || ''; | |
| const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || ''; | |
| const CHAT_ID = process.env.TELEGRAM_CHAT_ID || ''; | |
| // Middleware | |
| app.use(cors()); | |
| app.use(express.json({ limit: '10mb' })); | |
| app.use(express.urlencoded({ extended: true })); | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| // File upload config | |
| const upload = multer({ | |
| dest: path.join(__dirname, 'storage/uploads'), | |
| limits: { fileSize: 20 * 1024 * 1024 }, // 20MB | |
| fileFilter: (req, file, cb) => { | |
| const allowed = ['.pdf', '.doc', '.docx', '.png', '.jpg', '.jpeg', '.zip']; | |
| const ext = path.extname(file.originalname).toLowerCase(); | |
| cb(null, allowed.includes(ext)); | |
| }, | |
| }); | |
| // --- Auth Middleware --- | |
| function requireAdmin(req, res, next) { | |
| if (!ADMIN_KEY) return next(); // No key = open (dev mode) | |
| const auth = req.headers.authorization; | |
| if (auth === `Bearer ${ADMIN_KEY}`) return next(); | |
| res.status(401).json({ error: 'Unauthorized' }); | |
| } | |
| function requireAPIKey(req, res, next) { | |
| if (!API_KEY) return next(); | |
| const key = req.headers['x-api-key'] || req.query.api_key; | |
| if (key === API_KEY) return next(); | |
| res.status(401).json({ error: 'Invalid API key' }); | |
| } | |
| // ============================================ | |
| // WEB ROUTES | |
| // ============================================ | |
| app.get('/', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'index.html')); | |
| }); | |
| app.get('/proposal/:id', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'proposal.html')); | |
| }); | |
| app.get('/client/:token', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'client.html')); | |
| }); | |
| // ============================================ | |
| // API ROUTES — Health | |
| // ============================================ | |
| app.get('/api/health', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| engine: 'QUANTA', | |
| version: '1.0.0', | |
| uptime: process.uptime(), | |
| timestamp: new Date().toISOString(), | |
| }); | |
| }); | |
| // ============================================ | |
| // API ROUTES — RFQ & Proposals | |
| // ============================================ | |
| // Shared logic for Proposal Generation | |
| async function handleProposalGeneration(rfq_text, channel = 'web', competitor_domains = [], attribution = {}) { | |
| if (!rfq_text) throw new Error('rfq_text is required'); | |
| // Step 1: Qualify | |
| const qualification = await leadQualifier.qualify(rfq_text); | |
| // Step 2: Generate proposal | |
| const proposal = await proposalGenerator.generateProposal(qualification, { | |
| competitorDomains: competitor_domains, | |
| enrichWithVinOS: true, | |
| }); | |
| // Step 3: Create project | |
| const project = projectManager.createProject(qualification, proposal, attribution); | |
| // Step 4: Generate PDF | |
| try { | |
| const pdfPath = await pdfBuilder.generateProposalPDF(proposal); | |
| projectManager.updatePDFPath(project.id, pdfPath); | |
| } catch (e) { | |
| console.error('[Server] PDF generation failed:', e.message); | |
| } | |
| // Step 5: Notify | |
| if (CHAT_ID && BOT_TOKEN) { | |
| const summary = proposalGenerator.generateTelegramSummary(proposal); | |
| const clientUrl = projectManager.getClientURL(project.id); | |
| const notifyMsg = `${summary}\n\n🔗 Client link: ${clientUrl}\n🔑 PIN: ${project.client_pin}`; | |
| await _sendTelegram(CHAT_ID, notifyMsg); | |
| } | |
| // Response based on channel | |
| if (channel === 'telegram') { | |
| return { | |
| success: true, | |
| summary: proposalGenerator.generateTelegramSummary(proposal), | |
| project_id: project.id, | |
| client_url: projectManager.getClientURL(project.id), | |
| client_pin: project.client_pin, | |
| lqs: qualification.lqs, | |
| tier: qualification.recommended_tier, | |
| total: qualification.amounts?.total || 0, | |
| user_id: attribution.user_id, | |
| user_name: attribution.user_name | |
| }; | |
| } else { | |
| return { | |
| success: true, | |
| project, | |
| proposal: { sections: proposal.sections, generatedAt: proposal.generatedAt }, | |
| client_url: projectManager.getClientURL(project.id), | |
| client_pin: project.client_pin, | |
| }; | |
| } | |
| } | |
| app.post('/api/rfq', async (req, res) => { | |
| try { | |
| const { rfq_text, channel = 'web', competitor_domains = [], user_id, user_name } = req.body; | |
| const result = await handleProposalGeneration(rfq_text, channel, competitor_domains, { user_id, user_name }); | |
| res.json(result); | |
| } catch (e) { | |
| console.error('[Server] RFQ processing error:', e); | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // ============================================ | |
| // API ROUTES — Projects | |
| // ============================================ | |
| app.get('/api/projects', requireAdmin, (req, res) => { | |
| const { status, tier, limit } = req.query; | |
| const projects = projectManager.listProjects({ | |
| status, tier, limit: limit ? parseInt(limit) : undefined, | |
| }); | |
| res.json({ success: true, projects }); | |
| }); | |
| app.get('/api/projects/:id', (req, res) => { | |
| const project = projectManager.getProject(req.params.id); | |
| if (!project) return res.status(404).json({ error: 'Project not found' }); | |
| res.json({ success: true, project }); | |
| }); | |
| app.patch('/api/projects/:id', requireAdmin, (req, res) => { | |
| const { status, stage, milestone_id, milestone_status, note } = req.body; | |
| let result; | |
| if (status) result = projectManager.updateStatus(req.params.id, status); | |
| else if (stage) result = projectManager.updateStage(req.params.id, stage); | |
| else if (milestone_id && milestone_status) result = projectManager.updatePaymentMilestone(req.params.id, milestone_id, milestone_status); | |
| else if (note) result = projectManager.addNote(req.params.id, note); | |
| else return res.status(400).json({ error: 'Provide status, stage, milestone, or note' }); | |
| if (!result) return res.status(404).json({ error: 'Project not found' }); | |
| if (result.error) return res.status(400).json(result); | |
| res.json({ success: true, project: result }); | |
| }); | |
| app.get('/api/projects/:id/pdf', (req, res) => { | |
| const project = projectManager.getProject(req.params.id); | |
| if (!project) return res.status(404).json({ error: 'Project not found' }); | |
| const latest = project.proposal_versions[project.proposal_versions.length - 1]; | |
| if (!latest || !latest.pdfPath || !fs.existsSync(latest.pdfPath)) { | |
| return res.status(404).json({ error: 'PDF not generated yet' }); | |
| } | |
| res.download(latest.pdfPath, `proposal_${project.client_name.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`); | |
| }); | |
| app.post('/api/projects/:id/upload', upload.single('file'), (req, res) => { | |
| if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); | |
| projectManager.addNote(req.params.id, `File uploaded: ${req.file.originalname} (${req.file.size} bytes)`); | |
| res.json({ success: true, filename: req.file.originalname, path: req.file.path }); | |
| }); | |
| // ============================================ | |
| // API ROUTES — Client Portal | |
| // ============================================ | |
| app.post('/api/client/:token/verify', (req, res) => { | |
| const { pin } = req.body; | |
| if (!pin) return res.status(400).json({ error: 'PIN required' }); | |
| const result = projectManager.verifyClientPIN(req.params.token, pin); | |
| if (!result) return res.status(404).json({ error: 'Invalid access link' }); | |
| if (result.error) return res.status(403).json(result); | |
| // Return sanitized project data (no internal fields) | |
| res.json({ | |
| success: true, | |
| project: { | |
| id: result.id, | |
| client_name: result.client_name, | |
| project_name: result.project_name, | |
| status: result.status, | |
| onboarding_stage: result.onboarding_stage, | |
| recommended_tier: result.recommended_tier, | |
| amounts: result.amounts, | |
| payment_terms: result.payment_terms, | |
| timeline_weeks: result.timeline_weeks, | |
| key_dates: result.key_dates, | |
| checklist: result.checklist, | |
| proposal_versions: result.proposal_versions.map(v => ({ | |
| version: v.version, created: v.created, hasPDF: !!v.pdfPath, | |
| })), | |
| }, | |
| }); | |
| }); | |
| app.post('/api/client/:token/checklist', (req, res) => { | |
| const { checklist_id, done } = req.body; | |
| const project = projectManager.getProjectByToken(req.params.token); | |
| if (!project) return res.status(404).json({ error: 'Invalid access link' }); | |
| const result = projectManager.updateChecklist(project.id, checklist_id, done); | |
| if (!result) return res.status(404).json({ error: 'Failed' }); | |
| if (result.error) return res.status(400).json(result); | |
| res.json({ success: true }); | |
| }); | |
| // ============================================ | |
| // API ROUTES — Dashboard | |
| // ============================================ | |
| app.get('/api/dashboard', requireAdmin, (req, res) => { | |
| res.json({ success: true, data: projectManager.getDashboardData() }); | |
| }); | |
| // ============================================ | |
| // API ROUTES — Scraping | |
| // ============================================ | |
| app.post('/api/scrape/url', async (req, res) => { | |
| try { | |
| const { url, mode = 'auto' } = req.body; | |
| if (!url) return res.status(400).json({ error: 'url is required' }); | |
| const result = await webScraper.scrapeUrl(url, { mode }); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| app.post('/api/scrape/youtube', async (req, res) => { | |
| try { | |
| const { videoUrl } = req.body; | |
| if (!videoUrl) return res.status(400).json({ error: 'videoUrl is required' }); | |
| const result = await webScraper.scrapeYouTube(videoUrl); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| app.post('/api/scrape/social', async (req, res) => { | |
| try { | |
| const { platform, username } = req.body; | |
| if (!platform || !username) return res.status(400).json({ error: 'platform and username required' }); | |
| let result; | |
| switch (platform.toLowerCase()) { | |
| case 'instagram': case 'ig': result = await webScraper.scrapeInstagramProfile(username); break; | |
| case 'tiktok': case 'tt': result = await webScraper.scrapeTikTokProfile(username); break; | |
| case 'twitter': case 'x': result = await webScraper.scrapeTwitterProfile(username); break; | |
| default: return res.status(400).json({ error: `Unsupported platform: ${platform}` }); | |
| } | |
| res.json({ success: true, data: result }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| app.post('/api/scrape/pdf', upload.single('file'), async (req, res) => { | |
| try { | |
| if (!req.file) return res.status(400).json({ error: 'PDF file required' }); | |
| const text = await webScraper.extractPDF(req.file.path); | |
| // If channel=telegram or generate=true, proceed to proposal | |
| const { channel, generate, user_id, user_name } = req.body; | |
| if (channel === 'telegram' || generate === 'true') { | |
| const result = await handleProposalGeneration(text, channel || 'web', [], { user_id, user_name }); | |
| // Cleanup temp file | |
| try { fs.unlinkSync(req.file.path); } catch {} | |
| return res.json(result); | |
| } | |
| // Default: just return text | |
| try { fs.unlinkSync(req.file.path); } catch {} | |
| res.json({ success: true, data: text }); | |
| } catch (e) { | |
| console.error('[Server] PDF processing error:', e); | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| app.post('/api/scrape/audit', async (req, res) => { | |
| try { | |
| const { domain } = req.body; | |
| if (!domain) return res.status(400).json({ error: 'domain is required' }); | |
| const result = await webScraper.auditCompetitor(domain); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| // ============================================ | |
| // API ROUTES — Bridge (for VinOS calls) | |
| // ============================================ | |
| app.post('/api/bridge/rfq', requireAPIKey, async (req, res) => { | |
| try { | |
| const { rfq_text } = req.body; | |
| if (!rfq_text) return res.status(400).json({ error: 'rfq_text is required' }); | |
| const qualification = await leadQualifier.qualify(rfq_text); | |
| const proposal = await proposalGenerator.generateProposal(qualification); | |
| const project = projectManager.createProject(qualification, proposal); | |
| try { | |
| const pdfPath = await pdfBuilder.generateProposalPDF(proposal); | |
| projectManager.updatePDFPath(project.id, pdfPath); | |
| } catch {} | |
| res.json({ | |
| success: true, | |
| project_id: project.id, | |
| summary: proposalGenerator.generateTelegramSummary(proposal), | |
| client_url: projectManager.getClientURL(project.id), | |
| client_pin: project.client_pin, | |
| lqs: qualification.lqs, | |
| tier: qualification.recommended_tier, | |
| total: qualification.amounts.total, | |
| }); | |
| } catch (e) { | |
| res.status(500).json({ error: e.message }); | |
| } | |
| }); | |
| app.get('/api/bridge/project/:id', requireAPIKey, (req, res) => { | |
| const project = projectManager.getProject(req.params.id); | |
| if (!project) return res.status(404).json({ error: 'Not found' }); | |
| res.json({ success: true, project }); | |
| }); | |
| // ============================================ | |
| // TELEGRAM HELPERS | |
| // ============================================ | |
| async function _sendTelegram(chatId, text) { | |
| if (!BOT_TOKEN) return; | |
| try { | |
| await axios.post(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, { | |
| chat_id: chatId, | |
| text: text.substring(0, 4000), | |
| parse_mode: 'HTML', | |
| }, { timeout: 10000 }); | |
| } catch (e) { | |
| console.error('[Telegram] Send failed:', e.message); | |
| } | |
| } | |
| // ============================================ | |
| // START SERVER | |
| // ============================================ | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`[QUANTA] Engine running on port ${PORT}`); | |
| console.log(`[QUANTA] Dashboard: http://localhost:${PORT}`); | |
| console.log(`[QUANTA] Health: http://localhost:${PORT}/api/health`); | |
| }); | |
| module.exports = app; | |