const express = require('express'); const fs = require('fs'); const cors = require('cors'); const path = require('path'); const { initDatabase } = require('./lib/db'); const db = initDatabase(); console.log('Database initialized successfully'); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors({ origin: ['http://localhost:5173', 'http://localhost:3000', 'chrome-extension://*', 'https://ark.xiaohongshu.com', 'https://www.xiaohongshu.com'], credentials: true, })); app.use((req, res, next) => { const start = Date.now(); res.on("finish", () => { const ms = Date.now() - start; if (req.path.startsWith("/api/")) console.log(new Date().toISOString() + " " + req.method + " " + req.path + " " + res.statusCode + " " + ms + "ms"); }); next(); }); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // Read configured image save path from database let imagesRoot = path.join(__dirname, 'images'); try { const savePathRow = db.prepare("SELECT value FROM configs WHERE key = 'image_save_path'").get(); if (savePathRow && savePathRow.value && savePathRow.value.trim()) { imagesRoot = savePathRow.value.trim(); } } catch (e) { /* config table may not exist yet */ } console.log('Image save path:', imagesRoot); if (!fs.existsSync(imagesRoot)) fs.mkdirSync(imagesRoot, { recursive: true }); app.use('/images', express.static(imagesRoot)); app.use('/api/images', express.static(imagesRoot)); const webDist = path.join(__dirname, '..', 'web', 'dist'); app.use(express.static(webDist)); const createShopRoutes = require('./routes/shops'); const createProductRoutes = require('./routes/products'); const createNoteRoutes = require('./routes/notes'); const createConfigRoutes = require('./routes/configs'); const createTaskRoutes = require('./routes/tasks'); const createGenerateRoutes = require('./routes/generate'); const createLicenseRoutes = require('./routes/license'); const { requireLicense } = require('./lib/licenseAuth'); app.use('/api/license', createLicenseRoutes()); app.use('/api', requireLicense({ allowPaths: ['/health'] })); app.use('/api/shops', createShopRoutes(db)); app.use('/api/products', createProductRoutes(db)); app.use('/api/notes', createNoteRoutes(db)); app.use('/api/configs', createConfigRoutes(db)); app.use('/api/tasks', createTaskRoutes(db)); app.use('/api/generate', createGenerateRoutes(db)); app.use('/api/notes/generate', createGenerateRoutes(db)); app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.get('/api/stats', (req, res) => { const shopCount = db.prepare('SELECT COUNT(*) as cnt FROM shops').get().cnt; const productCount = db.prepare('SELECT COUNT(*) as cnt FROM products').get().cnt; const noteCount = db.prepare('SELECT COUNT(*) as cnt FROM notes').get().cnt; const publishedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE use_status = 'used'").get().cnt; const pendingReviewCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'generated'").get().cnt; const failedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'failed' OR use_status = 'failed'").get().cnt; res.json({ shops: shopCount, products: productCount, notes: noteCount, published: publishedCount, pendingReview: pendingReviewCount, failed: failedCount }); }); // SPA fallback app.use((req, res, next) => { if (req.path.startsWith('/api/')) { return res.status(404).json({ error: 'API endpoint not found' }); } res.sendFile(path.join(webDist, 'index.html'), (err) => { if (err) res.status(404).json({ error: 'Frontend not built. Run: cd web && npm run build' }); }); }); //定时释放超时的 publishing 任务(每5分钟检查一次) setInterval(() => { try { const stale = db.prepare( "UPDATE notes SET status = 'approved', use_status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')" ).run(); if (stale.changes > 0) { console.log(`[定时任务] 释放了 ${stale.changes} 条超时的 publishing 笔记`); } // 同步清理 publish_tasks 表 const staleTasks = db.prepare( "UPDATE publish_tasks SET status = 'pending', claimed_by = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE status IN ('claimed', 'executing') AND claimed_at < datetime('now', '-10 minutes')" ).run(); if (staleTasks.changes > 0) { console.log(`[定时任务] 释放了 ${staleTasks.changes} 条超时的 publish_tasks`); } } catch (e) { console.error('[定时任务] 释放超时任务失败:', e.message); } }, 5 * 60 * 1000); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); console.log(`API: http://localhost:${PORT}/api`); });