108 lines
4.8 KiB
JavaScript
108 lines
4.8 KiB
JavaScript
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`);
|
||
});
|
||
|