xiaohongshufabu/server/index.js

106 lines
4.8 KiB
JavaScript
Raw Normal View History

2026-07-21 21:17:20 +08:00
const express = require('express');
const fs = require('fs');
const cors = require('cors');
const path = require('path');
const { initDatabase } = require('./lib/db');
2026-07-22 10:55:17 +08:00
const { getDefaultImagesRoot, getWebDistPath, resolveImagesRoot } = require('./lib/runtime-paths');
2026-07-21 21:17:20 +08:00
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
2026-07-22 10:55:17 +08:00
let imagesRoot = getDefaultImagesRoot();
2026-07-21 21:17:20 +08:00
try {
const savePathRow = db.prepare("SELECT value FROM configs WHERE key = 'image_save_path'").get();
2026-07-22 10:55:17 +08:00
imagesRoot = resolveImagesRoot(savePathRow?.value);
2026-07-21 21:17:20 +08:00
} 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));
2026-07-22 10:55:17 +08:00
const webDist = getWebDistPath();
2026-07-21 21:17:20 +08:00
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');
2026-07-22 10:07:46 +08:00
const createLicenseRoutes = require('./routes/license');
const { requireLicense } = require('./lib/licenseAuth');
app.use('/api/license', createLicenseRoutes());
app.use('/api', requireLicense({ allowPaths: ['/health'] }));
2026-07-21 21:17:20 +08:00
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;
2026-07-21 21:17:20 +08:00
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;
2026-07-21 21:17:20 +08:00
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')"
2026-07-21 21:17:20 +08:00
).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`);
});