xiaohongshufabu/server/index.js
2026-07-22 10:07:46 +08:00

108 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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`);
});