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' ) ;
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' ) ;
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 ;
2026-07-22 09:34:55 +08:00
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 ;
2026-07-22 09:34:55 +08:00
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 (
2026-07-22 09:34:55 +08:00
"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 ` ) ;
} ) ;
2026-07-22 10:07:46 +08:00