2026-07-21 21:17:20 +08:00
const express = require ( 'express' ) ;
const { v4 : uuidv4 } = require ( 'uuid' ) ;
const { generateImage } = require ( '../lib/generate-image' ) ;
function createGenerateRoutes ( db ) {
const router = express . Router ( ) ;
2026-07-22 10:07:46 +08:00
const runningJobs = new Set ( ) ;
2026-07-21 21:17:20 +08:00
async function callAI ( prompt , maxTokens = 2000 ) {
const aiConfig = { } ;
const keys = [ 'ai_api_key' , 'ai_base_url' , 'ai_model' , 'ai_temperature' ] ;
2026-07-22 10:07:46 +08:00
for ( const key of keys ) {
const row = db . prepare ( 'SELECT value FROM configs WHERE key = ?' ) . get ( key ) ;
aiConfig [ key ] = row ? . value || '' ;
2026-07-21 21:17:20 +08:00
}
if ( ! aiConfig . ai _api _key ) throw new Error ( 'AI API key not configured. Please set it in Settings.' ) ;
2026-07-22 10:07:46 +08:00
const response = await fetch ( ` ${ aiConfig . ai _base _url || 'https://api.deepseek.com' } /chat/completions ` , {
2026-07-21 21:17:20 +08:00
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 : parseFloat ( aiConfig . ai _temperature || '0.8' ) ,
max _tokens : maxTokens ,
} ) ,
} ) ;
if ( ! response . ok ) {
const err = await response . text ( ) ;
throw new Error ( ` AI API error: ${ response . status } ${ err } ` ) ;
}
const data = await response . json ( ) ;
return data . choices ? . [ 0 ] ? . message ? . content || '' ;
}
async function generateTitle ( productTitle ) {
const titleRow = db . prepare ( 'SELECT value FROM configs WHERE key = ?' ) . get ( 'title_prompt_template' ) ;
const fallbackRow = db . prepare ( 'SELECT value FROM configs WHERE key = ?' ) . get ( 'prompt_template' ) ;
const template = titleRow ? . value || fallbackRow ? . value || '为商品写一个小红书标题:{{title}}' ;
const prompt = template . replace ( /\{\{title\}\}/g , productTitle ) ;
const generated = await callAI ( prompt , 500 ) ;
const title = generated . replace ( /^#+\s*/ , '' ) . replace ( /[\n\r]/g , '' ) . trim ( ) ;
return title || productTitle ;
}
async function generateContent ( productTitle ) {
const contentRow = db . prepare ( 'SELECT value FROM configs WHERE key = ?' ) . get ( 'content_prompt_template' ) ;
const fallbackRow = db . prepare ( 'SELECT value FROM configs WHERE key = ?' ) . get ( 'prompt_template' ) ;
const template = contentRow ? . value || fallbackRow ? . value || '为商品写一篇小红书正文:{{title}}' ;
const prompt = template . replace ( /\{\{title\}\}/g , productTitle ) ;
const generated = await callAI ( prompt , 2000 ) ;
const topicMatches = generated . match ( /#[^\s#]+/g ) || [ ] ;
2026-07-22 10:07:46 +08:00
const topics = topicMatches . map ( ( topic ) => topic . replace ( '#' , '' ) . trim ( ) ) ;
2026-07-21 21:17:20 +08:00
return { content : generated . trim ( ) , topics } ;
}
async function generateText ( productTitle ) {
const title = await generateTitle ( productTitle ) ;
const { content , topics } = await generateContent ( productTitle ) ;
return { title , content , topics } ;
}
async function createNoteForProduct ( product , options = { } ) {
const existing = db . prepare (
"SELECT id, title, content, topics, image_paths, image_prompt FROM notes WHERE product_id = ? AND status = 'generating' ORDER BY created_at DESC LIMIT 1"
) . get ( product . id ) ;
if ( existing ) {
return {
noteId : existing . id ,
title : existing . title ,
content : existing . content ,
topics : JSON . parse ( existing . topics || '[]' ) ,
imagePaths : JSON . parse ( existing . image _paths || '[]' ) ,
imagePrompt : existing . image _prompt || '' ,
} ;
}
const noteId = uuidv4 ( ) ;
2026-07-22 10:07:46 +08:00
db . prepare ( "INSERT INTO notes (id, product_id, shop_id, title, content, status) VALUES (?, ?, ?, ?, '', 'generating')" )
. run ( noteId , product . id , product . shop _id , product . title ) ;
2026-07-21 21:17:20 +08:00
const { title , content , topics } = await generateText ( product . title ) ;
const imageResult = await generateImage ( db , noteId , product . title , options ) ;
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 , noteId ) ;
2026-07-22 10:07:46 +08:00
db . prepare ( "UPDATE products SET status='generated', updated_at=CURRENT_TIMESTAMP WHERE id=?" ) . run ( product . id ) ;
2026-07-21 21:17:20 +08:00
return { noteId , title , content , topics , imagePaths , imagePrompt } ;
}
2026-07-22 10:07:46 +08:00
function updateJob ( jobId , fields ) {
const keys = Object . keys ( fields ) ;
if ( keys . length === 0 ) return ;
const setSql = keys . map ( ( key ) => ` ${ key } = ? ` ) . join ( ', ' ) ;
const values = keys . map ( ( key ) => fields [ key ] ) ;
values . push ( jobId ) ;
db . prepare ( ` UPDATE generation_jobs SET ${ setSql } , updated_at=CURRENT_TIMESTAMP WHERE id = ? ` ) . run ( ... values ) ;
}
async function runGenerationJob ( jobId , productIds , countPerProduct , options = { } ) {
if ( runningJobs . has ( jobId ) ) return ;
runningJobs . add ( jobId ) ;
try {
updateJob ( jobId , { status : 'running' , message : '开始生成' } ) ;
let completedNotes = 0 ;
let failedNotes = 0 ;
for ( const productId of productIds ) {
const product = db . prepare ( 'SELECT * FROM products WHERE id = ?' ) . get ( productId ) ;
if ( ! product ) {
failedNotes += countPerProduct ;
completedNotes += countPerProduct ;
updateJob ( jobId , {
completed _notes : completedNotes ,
failed _notes : failedNotes ,
current _product _id : productId ,
current _product _title : '' ,
message : '商品不存在' ,
} ) ;
continue ;
}
db . prepare ( "UPDATE products SET status='generating', updated_at=CURRENT_TIMESTAMP WHERE id=?" ) . run ( product . id ) ;
updateJob ( jobId , {
current _product _id : product . id ,
current _product _title : product . title ,
message : ` 正在生成: ${ product . title } ` ,
} ) ;
for ( let index = 0 ; index < countPerProduct ; index += 1 ) {
try {
await createNoteForProduct ( product , options ) ;
} catch ( error ) {
failedNotes += 1 ;
} finally {
completedNotes += 1 ;
updateJob ( jobId , {
completed _notes : completedNotes ,
failed _notes : failedNotes ,
message : ` 进度 ${ completedNotes } / ${ productIds . length * countPerProduct } ` ,
} ) ;
}
}
db . prepare ( "UPDATE products SET status='generated', updated_at=CURRENT_TIMESTAMP WHERE id=?" ) . run ( product . id ) ;
}
updateJob ( jobId , {
status : 'completed' ,
message : failedNotes > 0 ? ` 完成,失败 ${ failedNotes } 条 ` : '全部完成' ,
} ) ;
} catch ( error ) {
updateJob ( jobId , { status : 'failed' , message : error . message } ) ;
} finally {
runningJobs . delete ( jobId ) ;
}
}
function enqueueGeneration ( jobId , productIds , countPerProduct , options ) {
setImmediate ( ( ) => {
runGenerationJob ( jobId , productIds , countPerProduct , options ) ;
} ) ;
}
router . get ( '/jobs' , ( req , res ) => {
const { shop _id } = req . query ;
let sql = 'SELECT * FROM generation_jobs WHERE 1=1' ;
const params = [ ] ;
if ( shop _id ) {
sql += ' AND shop_id = ?' ;
params . push ( shop _id ) ;
}
sql += ' ORDER BY created_at DESC LIMIT 20' ;
res . json ( db . prepare ( sql ) . all ( ... params ) ) ;
} ) ;
router . post ( '/note' , ( req , res ) => {
const { product _id , shop _id , count _per _product , image _count , image _size , image _prompt _template } = req . body ;
2026-07-21 21:17:20 +08:00
if ( ! product _id || ! shop _id ) {
return res . status ( 400 ) . json ( { error : 'product_id and shop_id required' } ) ;
}
const product = db . prepare ( 'SELECT * FROM products WHERE id = ?' ) . get ( product _id ) ;
if ( ! product ) return res . status ( 404 ) . json ( { error : 'Product not found' } ) ;
2026-07-22 10:07:46 +08:00
const repeatCount = Math . max ( 1 , parseInt ( count _per _product || '1' , 10 ) || 1 ) ;
const jobId = uuidv4 ( ) ;
db . prepare (
'INSERT INTO generation_jobs (id, shop_id, status, total_products, total_notes, message) VALUES (?, ?, ?, ?, ?, ?)'
) . run ( jobId , shop _id , 'pending' , 1 , repeatCount , '等待开始' ) ;
enqueueGeneration ( jobId , [ product _id ] , repeatCount , { image _count , image _size , image _prompt _template } ) ;
res . status ( 201 ) . json ( { job _id : jobId , status : 'pending' } ) ;
2026-07-21 21:17:20 +08:00
} ) ;
router . post ( '/image' , async ( req , res ) => {
const { note _id } = req . body ;
if ( ! note _id ) return res . status ( 400 ) . json ( { error : 'note_id required' } ) ;
const note = db . prepare ( 'SELECT * FROM notes WHERE id = ?' ) . get ( note _id ) ;
if ( ! note ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
try {
const imageResult = await generateImage ( db , note _id , note . title ) ;
const imagePaths = imageResult . imagePaths || [ ] ;
const existingPaths = JSON . parse ( note . image _paths || '[]' ) ;
const allPaths = [ ... existingPaths , ... imagePaths ] ;
db . prepare ( "UPDATE notes SET image_paths=?, image_prompt=?, updated_at=CURRENT_TIMESTAMP WHERE id=?" )
. run ( JSON . stringify ( allPaths ) , imageResult . imagePrompt || note . image _prompt || '' , note _id ) ;
res . json ( { image _paths : imagePaths , total : allPaths . length } ) ;
} catch ( error ) {
res . status ( 500 ) . json ( { error : error . message } ) ;
}
} ) ;
async function batchGenerate ( req , res ) {
2026-07-22 10:07:46 +08:00
const { shop _id , product _ids , count _per _product , image _count , image _size , image _prompt _template } = req . body ;
2026-07-21 21:17:20 +08:00
if ( ! shop _id ) return res . status ( 400 ) . json ( { error : 'shop_id required' } ) ;
2026-07-22 10:07:46 +08:00
let sql = 'SELECT * FROM products WHERE shop_id = ?' ;
2026-07-21 21:17:20 +08:00
const params = [ shop _id ] ;
if ( Array . isArray ( product _ids ) && product _ids . length > 0 ) {
sql += ` AND id IN ( ${ product _ids . map ( ( ) => '?' ) . join ( ',' ) } ) ` ;
params . push ( ... product _ids ) ;
}
const products = db . prepare ( sql ) . all ( ... params ) ;
if ( products . length === 0 ) return res . json ( { count : 0 , message : 'No pending products' } ) ;
2026-07-22 10:07:46 +08:00
const repeatCount = Math . max ( 1 , parseInt ( count _per _product || '1' , 10 ) || 1 ) ;
const jobId = uuidv4 ( ) ;
db . prepare (
'INSERT INTO generation_jobs (id, shop_id, status, total_products, total_notes, message) VALUES (?, ?, ?, ?, ?, ?)'
) . run ( jobId , shop _id , 'pending' , products . length , products . length * repeatCount , '等待开始' ) ;
enqueueGeneration ( jobId , products . map ( ( product ) => product . id ) , repeatCount , { image _count , image _size , image _prompt _template } ) ;
res . status ( 201 ) . json ( { job _id : jobId , total : products . length , total _notes : products . length * repeatCount } ) ;
2026-07-21 21:17:20 +08:00
}
router . post ( '/' , batchGenerate ) ;
router . post ( '/batch' , batchGenerate ) ;
return router ;
}
module . exports = createGenerateRoutes ;