2026-07-21 21:17:20 +08:00
const express = require ( 'express' ) ;
const { v4 : uuidv4 } = require ( 'uuid' ) ;
const { generateImage } = require ( '../lib/generate-image' ) ;
function createNoteRoutes ( db ) {
const router = express . Router ( ) ;
function createPublishTask ( note ) {
// 已成功发布过的笔记不再创建新任务(防止重复发布)
const completed = db . prepare (
"SELECT id FROM publish_tasks WHERE note_id = ? AND status = 'completed'"
) . get ( note . id ) ;
if ( completed ) return ;
const existing = db . prepare (
"SELECT id FROM publish_tasks WHERE note_id = ? AND status IN ('pending', 'claimed', 'executing')"
) . get ( note . id ) ;
if ( existing ) return ;
db . prepare (
'INSERT INTO publish_tasks (id, note_id, shop_id) VALUES (?, ?, ?)'
) . run ( uuidv4 ( ) , note . id , note . shop _id ) ;
}
router . get ( '/next' , ( req , res ) => {
const { shop _id } = req . query ;
if ( ! shop _id ) return res . status ( 400 ) . json ( { error : 'shop_id is required' } ) ;
const note = db . prepare (
"SELECT n.*, p.title AS product_title, s.shop_name FROM notes n LEFT JOIN products p ON p.id = n.product_id LEFT JOIN shops s ON s.id = n.shop_id WHERE n.shop_id = ? AND n.status = 'approved' ORDER BY n.created_at ASC LIMIT 1"
) . get ( shop _id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'No pending notes' } ) ;
res . json ( {
... note ,
topics : JSON . parse ( note . topics || '[]' ) ,
image _paths : JSON . parse ( note . image _paths || '[]' ) ,
} ) ;
} ) ;
router . get ( '/next/task' , ( req , res ) => {
req . url = '/next' + ( req . url . includes ( '?' ) ? req . url . slice ( req . url . indexOf ( '?' ) ) : '' ) ;
return router . handle ( req , res ) ;
} ) ;
router . get ( '/' , ( req , res ) => {
const { shop _id , status , product _id } = req . query ;
let sql = 'SELECT n.*, p.title as product_title FROM notes n LEFT JOIN products p ON p.id = n.product_id WHERE 1=1' ;
const params = [ ] ;
if ( shop _id ) { sql += ' AND n.shop_id = ?' ; params . push ( shop _id ) ; }
if ( status ) { sql += ' AND n.status = ?' ; params . push ( status ) ; }
if ( product _id ) { sql += ' AND n.product_id = ?' ; params . push ( product _id ) ; }
sql += ' ORDER BY n.created_at DESC' ;
const notes = db . prepare ( sql ) . all ( ... params ) ;
const parsed = notes . map ( n => ( {
... n ,
topics : JSON . parse ( n . topics || '[]' ) ,
image _paths : JSON . parse ( n . image _paths || '[]' ) ,
} ) ) ;
res . json ( parsed ) ;
} ) ;
router . get ( '/export' , ( req , res ) => {
const { shop _id , status } = req . query ;
let sql = 'SELECT n.*, s.shop_name, p.title as product_title FROM notes n LEFT JOIN shops s ON s.id = n.shop_id LEFT JOIN products p ON p.id = n.product_id WHERE 1=1' ;
const params = [ ] ;
if ( shop _id ) { sql += ' AND n.shop_id = ?' ; params . push ( shop _id ) ; }
if ( status ) { sql += ' AND n.status = ?' ; params . push ( status ) ; }
sql += ' ORDER BY n.created_at DESC' ;
const notes = db . prepare ( sql ) . all ( ... params ) ;
const parsed = notes . map ( n => ( {
... n ,
topics : JSON . parse ( n . topics || '[]' ) ,
image _paths : JSON . parse ( n . image _paths || '[]' ) ,
} ) ) ;
res . json ( parsed ) ;
} ) ;
router . get ( '/:id' , ( req , res ) => {
const note = db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( req . params . id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
res . json ( {
... note ,
topics : JSON . parse ( note . topics || '[]' ) ,
image _paths : JSON . parse ( note . image _paths || '[]' ) ,
} ) ;
} ) ;
router . post ( '/' , ( req , res ) => {
const { product _id , shop _id , title , content , topics , image _paths } = req . body ;
if ( ! product _id || ! shop _id || ! title ) {
return res . status ( 400 ) . json ( { error : 'product_id, shop_id, and title are required' } ) ;
}
const id = uuidv4 ( ) ;
db . prepare (
'INSERT INTO notes (id, product_id, shop_id, title, content, topics, image_paths) VALUES (?, ?, ?, ?, ?, ?, ?)'
) . run ( id , product _id , shop _id , title , content || '' , JSON . stringify ( topics || [ ] ) , JSON . stringify ( image _paths || [ ] ) ) ;
res . status ( 201 ) . json ( db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( id ) ) ;
} ) ;
// Batch delete notes (POST because DELETE with body is not reliable)
router . post ( '/batch-delete' , ( req , res ) => {
const { ids } = req . body ;
if ( ! Array . isArray ( ids ) || ids . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'ids array required' } ) ;
}
const placeholders = ids . map ( ( ) => '?' ) . join ( ',' ) ;
const result = db . prepare ( 'DELETE FROM notes WHERE id IN (' + placeholders + ')' ) . run ( ... ids ) ;
res . json ( { deleted : result . changes } ) ;
} ) ;
// Batch delete notes (legacy DELETE route)
router . delete ( '/batch' , ( req , res ) => {
const { ids } = req . body ;
if ( ! Array . isArray ( ids ) || ids . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'ids array required' } ) ;
}
const placeholders = ids . map ( ( ) => '?' ) . join ( ',' ) ;
const result = db . prepare ( 'DELETE FROM notes WHERE id IN (' + placeholders + ')' ) . run ( ... ids ) ;
res . json ( { deleted : result . changes } ) ;
} ) ;
router . post ( '/batch/approve' , ( req , res ) => {
const { ids } = req . body ;
if ( ! Array . isArray ( ids ) || ids . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'ids array required' } ) ;
}
const placeholders = ids . map ( ( ) => '?' ) . join ( ',' ) ;
2026-07-22 09:34:55 +08:00
db . prepare ( 'UPDATE notes SET status = ?, use_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')' ) . run ( 'approved' , 'pending' , ... ids ) ;
2026-07-21 21:17:20 +08:00
const batchNotes = db . prepare ( 'SELECT * FROM notes WHERE id IN (' + placeholders + ')' ) . all ( ... ids ) ;
for ( const note of batchNotes ) {
createPublishTask ( note ) ;
}
res . json ( { updated : ids . length } ) ;
} ) ;
router . post ( '/batch/reject' , ( req , res ) => {
const { ids } = req . body ;
if ( ! Array . isArray ( ids ) || ids . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'ids array required' } ) ;
}
const placeholders = ids . map ( ( ) => '?' ) . join ( ',' ) ;
2026-07-22 09:34:55 +08:00
db . prepare ( 'UPDATE notes SET status = ?, use_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')' ) . run ( 'pending' , 'pending' , ... ids ) ;
2026-07-21 21:17:20 +08:00
res . json ( { updated : ids . length } ) ;
} ) ;
2026-07-22 09:34:55 +08:00
router . post ( '/batch/publish-reset' , ( req , res ) => {
const { ids } = req . body ;
if ( ! Array . isArray ( ids ) || ids . length === 0 ) {
return res . status ( 400 ) . json ( { error : 'ids array required' } ) ;
}
const placeholders = ids . map ( ( ) => '?' ) . join ( ',' ) ;
const resetNotes = db . prepare (
'SELECT * FROM notes WHERE id IN (' + placeholders + ") AND status = 'approved' AND use_status = 'failed'"
) . all ( ... ids ) ;
const reset = db . transaction ( ( ) => {
const result = db . prepare (
'UPDATE notes SET use_status = ?, error_message = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ") AND status = 'approved' AND use_status = 'failed'"
) . run ( 'pending' , '' , ... ids ) ;
for ( const note of resetNotes ) {
createPublishTask ( { ... note , use _status : 'pending' } ) ;
}
return result . changes ;
} ) ;
res . json ( { updated : reset ( ) } ) ;
} ) ;
2026-07-21 21:17:20 +08:00
router . put ( '/:id' , ( req , res ) => {
const note = db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( req . params . id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
const { title , content , topics , image _paths , status , image _prompt , worker _id } = req . body ;
2026-07-22 09:34:55 +08:00
const nextStatus = status || note . status ;
const nextUseStatus = nextStatus === 'approved' && note . status !== 'approved' ? 'pending' : note . use _status ;
db . prepare ( ` UPDATE notes SET title=?, content=?, topics=?, image_paths=?, status=?, use_status=?, image_prompt=?, worker_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=? ` )
2026-07-21 21:17:20 +08:00
. run (
title || note . title ,
content !== undefined ? content : note . content ,
topics ? JSON . stringify ( topics ) : note . topics ,
image _paths ? JSON . stringify ( image _paths ) : note . image _paths ,
2026-07-22 09:34:55 +08:00
nextStatus ,
nextUseStatus ,
2026-07-21 21:17:20 +08:00
image _prompt !== undefined ? image _prompt : note . image _prompt ,
worker _id !== undefined ? worker _id : note . worker _id ,
req . params . id
) ;
if ( status === 'approved' && note . status !== 'approved' ) {
createPublishTask ( { ... note , status : 'approved' } ) ;
}
res . json ( db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( req . params . id ) ) ;
} ) ;
router . post ( '/:id/rewrite' , async ( req , res ) => {
const note = db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( req . params . id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
const aiConfig = { } ;
for ( const k of [ 'ai_api_key' , 'ai_model' , 'ai_base_url' , 'ai_temperature' , 'prompt_template' ] ) {
const row = db . prepare ( 'SELECT value FROM configs WHERE key = ?' ) . get ( k ) ;
aiConfig [ k ] = row ? . value || '' ;
}
if ( ! aiConfig . ai _api _key ) {
return res . status ( 400 ) . json ( { error : 'AI API key not configured' } ) ;
}
const product = db . prepare ( 'SELECT * FROM products WHERE id = ?' ) . get ( note . product _id ) ;
const productTitle = product ? . title || note . title ;
2026-07-22 09:34:55 +08:00
db . prepare ( "UPDATE notes SET status='generating', use_status='pending', updated_at=CURRENT_TIMESTAMP WHERE id=?" ) . run ( req . params . id ) ;
2026-07-21 21:17:20 +08:00
// Cancel any pending publish tasks for this note
db . prepare ( "UPDATE publish_tasks SET status='failed', updated_at=CURRENT_TIMESTAMP WHERE note_id = ? AND status IN ('pending', 'claimed', 'executing')" ) . run ( req . params . id ) ;
try {
const promptTemplate = aiConfig . prompt _template || aiConfig . content _prompt _template || '为商品写一篇小红书笔记:{{title}}' ;
const prompt = promptTemplate . replace ( /\{\{title\}\}/g , productTitle ) . replace ( /\{title\}/g , productTitle ) ;
const temperature = parseFloat ( aiConfig . ai _temperature || '0.8' ) ;
const response = await fetch ( ( aiConfig . ai _base _url || 'https://api.deepseek.com' ) + '/chat/completions' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'Authorization' : 'Bearer ' + aiConfig . ai _api _key } ,
body : JSON . stringify ( {
model : aiConfig . ai _model || 'deepseek-chat' ,
messages : [ { role : 'user' , content : prompt } ] ,
temperature ,
max _tokens : 2000 ,
} ) ,
} ) ;
if ( ! response . ok ) throw new Error ( 'AI API error: ' + response . status ) ;
const data = await response . json ( ) ;
const generated = data . choices ? . [ 0 ] ? . message ? . content || '' ;
const lines = generated . split ( '\n' ) . filter ( l => l . trim ( ) ) ;
const titleLine = lines . find ( l => l . startsWith ( '#' ) ) || lines [ 0 ] || productTitle ;
const title = titleLine . replace ( /^#+\s*/ , '' ) . trim ( ) ;
const content = lines . slice ( lines . indexOf ( titleLine ) + 1 ) . join ( '\n' ) . trim ( ) || generated ;
const topicMatches = content . match ( /#[^\s#]+/g ) || [ ] ;
const topics = topicMatches . map ( t => t . replace ( '#' , '' ) . trim ( ) ) ;
// Generate new images
const imageResult = await generateImage ( db , req . params . id , productTitle ) ;
const imagePaths = imageResult . imagePaths || [ ] ;
const imagePrompt = imageResult . imagePrompt || '' ;
db . prepare ( "UPDATE notes SET title=?, content=?, topics=?, image_paths=?, image_prompt=?, status='generated', updated_at=CURRENT_TIMESTAMP WHERE id=?" )
. run ( title , content , JSON . stringify ( topics ) , JSON . stringify ( imagePaths ) , imagePrompt , req . params . id ) ;
res . json ( { id : req . params . id , title , content , topics , image _paths : imagePaths } ) ;
} catch ( error ) {
db . prepare ( "UPDATE notes SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE id=?" )
. run ( error . message , req . params . id ) ;
res . status ( 500 ) . json ( { error : error . message } ) ;
}
} ) ;
router . post ( '/:id/complete' , ( req , res ) => {
const note = db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( req . params . id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
2026-07-22 09:34:55 +08:00
db . prepare ( "UPDATE notes SET status='approved', use_status='used', publish_result=?, error_message='', published_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?" )
2026-07-21 21:17:20 +08:00
. run ( JSON . stringify ( req . body . result || { } ) , req . params . id ) ;
db . prepare ( "UPDATE publish_tasks SET status='completed', updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'" )
. run ( req . params . id ) ;
res . json ( { success : true } ) ;
} ) ;
router . post ( '/:id/fail' , ( req , res ) => {
const note = db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( req . params . id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
const { error _message } = req . body ;
2026-07-22 09:34:55 +08:00
db . prepare ( "UPDATE notes SET status='approved', use_status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE id=?" )
2026-07-21 21:17:20 +08:00
. run ( error _message || 'Unknown error' , req . params . id ) ;
db . prepare ( "UPDATE publish_tasks SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'" )
. run ( error _message || 'Unknown error' , req . params . id ) ;
res . json ( { success : true } ) ;
} ) ;
router . post ( '/batch' , ( req , res ) => {
const { product _id , shop _id , notes } = req . body ;
if ( ! product _id || ! shop _id || ! Array . isArray ( notes ) ) {
return res . status ( 400 ) . json ( { error : 'product_id, shop_id, and notes array required' } ) ;
}
const insert = db . prepare (
'INSERT INTO notes (id, product_id, shop_id, title, content, topics, image_paths) VALUES (?, ?, ?, ?, ?, ?, ?)'
) ;
const ids = [ ] ;
const batch = db . transaction ( ( ) => {
for ( const n of notes ) {
const id = uuidv4 ( ) ;
insert . run ( id , product _id , shop _id , n . title , n . content || '' , JSON . stringify ( n . topics || [ ] ) , JSON . stringify ( n . image _paths || [ ] ) ) ;
ids . push ( id ) ;
}
} ) ;
batch ( ) ;
res . status ( 201 ) . json ( { count : ids . length , ids } ) ;
} ) ;
router . delete ( '/:id' , ( req , res ) => {
const result = db . prepare ( 'DELETE FROM notes WHERE id = ?' ) . run ( req . params . id ) ;
if ( result . changes === 0 ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
res . json ( { success : true } ) ;
} ) ;
return router ;
}
module . exports = createNoteRoutes ;