| import express from 'express'; |
| import cors from 'cors'; |
| import multer from 'multer'; |
| import fetch from 'node-fetch'; |
| import path from 'path'; |
| import { fileURLToPath } from 'url'; |
| import fs from 'fs'; |
| import extract from 'png-chunks-extract'; |
| import text from 'png-chunk-text'; |
|
|
| const __filename = fileURLToPath(import.meta.url); |
| const __dirname = path.dirname(__filename); |
|
|
| const app = express(); |
| const upload = multer({ storage: multer.memoryStorage() }); |
|
|
| |
| app.use(cors()); |
| app.use(express.json()); |
| app.use(express.static('public')); |
|
|
| |
| app.post('/api/chat', async (req, res) => { |
| try { |
| const { apiKey, model, messages, temperature, maxTokens, topP, repetitionPenalty } = req.body; |
|
|
| const response = await fetch( |
| 'https://www.gpt4novel.com/api/xiaoshuoai/ext/v1/chat/completions', |
| { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer ${apiKey}`, |
| }, |
| body: JSON.stringify({ |
| model: model, |
| messages: messages, |
| stream: true, |
| temperature: temperature || 0.7, |
| max_tokens: maxTokens || 800, |
| top_p: topP || 0.35, |
| repetition_penalty: repetitionPenalty || 1.05, |
| }), |
| } |
| ); |
|
|
| if (!response.ok) { |
| return res.status(response.status).json({ error: `API请求失败: ${response.status}` }); |
| } |
|
|
| |
| res.setHeader('Content-Type', 'text/event-stream'); |
| res.setHeader('Cache-Control', 'no-cache'); |
| res.setHeader('Connection', 'keep-alive'); |
|
|
| |
| response.body.on('data', (chunk) => { |
| res.write(chunk); |
| }); |
|
|
| response.body.on('end', () => { |
| res.end(); |
| }); |
|
|
| response.body.on('error', (error) => { |
| console.error('Stream error:', error); |
| res.end(); |
| }); |
|
|
| } catch (error) { |
| console.error('Chat error:', error); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| |
| app.get('/api/models', async (req, res) => { |
| try { |
| const apiKey = req.headers.authorization?.replace('Bearer ', ''); |
| |
| if (!apiKey) { |
| return res.status(401).json({ error: '需要API密钥' }); |
| } |
|
|
| |
| const models = [ |
| { id: 'nalang-max-0826-16k', name: 'Nalang Max 16K (推荐)', price: '$0.0004/1K tokens' }, |
| { id: 'nalang-max-0826-10k', name: 'Nalang Max 10K', price: '$0.0004/1K tokens' }, |
| { id: 'nalang-max-0826', name: 'Nalang Max 32K', price: '$0.0004/1K tokens' }, |
| { id: 'nalang-xl-0826-16k', name: 'Nalang XL 16K (推荐)', price: '$0.0003/1K tokens' }, |
| { id: 'nalang-xl-0826-10k', name: 'Nalang XL 10K', price: '$0.0003/1K tokens' }, |
| { id: 'nalang-xl-0826', name: 'Nalang XL 32K', price: '$0.0003/1K tokens' }, |
| { id: 'nalang-medium-0826', name: 'Nalang Medium 32K', price: '$0.0002/1K tokens' }, |
| { id: 'nalang-turbo-0826', name: 'Nalang Turbo 32K (推荐)', price: '$0.0001/1K tokens' }, |
| ]; |
|
|
| res.json({ models }); |
| } catch (error) { |
| console.error('Models error:', error); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| |
| app.post('/api/comfyui/prompt', async (req, res) => { |
| try { |
| const { comfyuiUrl, workflow, prompt } = req.body; |
|
|
| if (!comfyuiUrl || !workflow) { |
| return res.status(400).json({ error: '缺少必要参数' }); |
| } |
|
|
| |
| const modifiedWorkflow = JSON.parse(JSON.stringify(workflow)); |
| |
| |
| for (const nodeId in modifiedWorkflow) { |
| const node = modifiedWorkflow[nodeId]; |
| if (node.class_type === 'CLIPTextEncode' && node.inputs) { |
| node.inputs.text = prompt; |
| } |
| } |
|
|
| |
| const response = await fetch(`${comfyuiUrl}/prompt`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Accept': 'application/json', |
| }, |
| body: JSON.stringify({ |
| prompt: modifiedWorkflow, |
| }), |
| }); |
|
|
| if (!response.ok) { |
| const errorText = await response.text(); |
| console.error('ComfyUI错误响应:', errorText); |
| throw new Error(`ComfyUI请求失败: ${response.status} - ${errorText}`); |
| } |
|
|
| const data = await response.json(); |
| res.json(data); |
|
|
| } catch (error) { |
| console.error('ComfyUI error:', error); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| |
| app.get('/api/comfyui/history/:promptId', async (req, res) => { |
| try { |
| const { comfyuiUrl } = req.query; |
| const { promptId } = req.params; |
|
|
| |
| if (!comfyuiUrl) { |
| console.error('缺少ComfyUI URL参数'); |
| return res.status(400).json({ error: '缺少ComfyUI URL' }); |
| } |
|
|
| if (!promptId) { |
| console.error('缺少promptId参数'); |
| return res.status(400).json({ error: '缺少promptId' }); |
| } |
|
|
| console.log(`获取历史: ${comfyuiUrl}/history/${promptId}`); |
|
|
| const response = await fetch(`${comfyuiUrl}/history/${promptId}`, { |
| headers: { |
| 'Accept': 'application/json', |
| } |
| }); |
| |
| if (!response.ok) { |
| const errorText = await response.text(); |
| console.error(`ComfyUI历史请求失败: ${response.status}`, errorText); |
| throw new Error(`获取历史失败: ${response.status}`); |
| } |
|
|
| const data = await response.json(); |
| res.json(data); |
|
|
| } catch (error) { |
| console.error('History error:', error); |
| res.status(500).json({ |
| error: error.message, |
| details: '无法从ComfyUI获取生成历史,请检查ComfyUI是否正常运行' |
| }); |
| } |
| }); |
|
|
| |
| app.get('/api/comfyui/view', async (req, res) => { |
| try { |
| const { comfyuiUrl, filename, subfolder, type } = req.query; |
|
|
| if (!comfyuiUrl || !filename) { |
| return res.status(400).json({ error: '缺少必要参数' }); |
| } |
|
|
| |
| let imageUrl = `${comfyuiUrl}/view?filename=${encodeURIComponent(filename)}`; |
| if (subfolder) { |
| imageUrl += `&subfolder=${encodeURIComponent(subfolder)}`; |
| } |
| if (type) { |
| imageUrl += `&type=${encodeURIComponent(type)}`; |
| } |
|
|
| const response = await fetch(imageUrl); |
| |
| if (!response.ok) { |
| throw new Error(`获取图片失败: ${response.status}`); |
| } |
|
|
| |
| const contentType = response.headers.get('content-type') || 'image/png'; |
| res.setHeader('Content-Type', contentType); |
| res.setHeader('Access-Control-Allow-Origin', '*'); |
| |
| response.body.pipe(res); |
|
|
| } catch (error) { |
| console.error('View image error:', error); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| |
| app.get('/api/comfyui/test', async (req, res) => { |
| try { |
| const { url } = req.query; |
|
|
| if (!url) { |
| return res.status(400).json({ success: false, error: '缺少URL参数' }); |
| } |
|
|
| |
| const response = await fetch(`${url}/system_stats`, { |
| headers: { |
| 'Accept': 'application/json', |
| } |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`连接失败: HTTP ${response.status}`); |
| } |
|
|
| const data = await response.json(); |
| res.json({ |
| success: true, |
| data, |
| message: 'ComfyUI连接成功!' |
| }); |
|
|
| } catch (error) { |
| console.error('Test connection error:', error); |
| |
| |
| let errorMessage = error.message; |
| if (error.code === 'ECONNREFUSED') { |
| errorMessage = '无法连接到ComfyUI服务器,请检查:\n1. ComfyUI是否正在运行\n2. URL地址是否正确\n3. 端口号是否正确(默认8188)'; |
| } else if (error.message.includes('403')) { |
| errorMessage = 'ComfyUI拒绝连接(403),请在ComfyUI启动时添加 --listen 参数'; |
| } else if (error.message.includes('CORS')) { |
| errorMessage = 'CORS跨域错误,ComfyUI需要允许跨域访问'; |
| } |
| |
| res.json({ |
| success: false, |
| error: errorMessage, |
| details: error.message |
| }); |
| } |
| }); |
|
|
| |
| app.post('/api/parse-character-png', upload.single('file'), async (req, res) => { |
| try { |
| if (!req.file) { |
| return res.status(400).json({ error: '没有上传文件' }); |
| } |
|
|
| const buffer = req.file.buffer; |
| const chunks = extract(buffer); |
| |
| |
| let characterData = null; |
| |
| for (const chunk of chunks) { |
| if (chunk.name === 'tEXt') { |
| const textData = text.decode(chunk.data); |
| if (textData.keyword === 'chara' || textData.keyword === 'character') { |
| try { |
| |
| const decoded = Buffer.from(textData.text, 'base64').toString('utf-8'); |
| characterData = JSON.parse(decoded); |
| break; |
| } catch (e) { |
| console.error('解析角色数据失败:', e); |
| } |
| } |
| } |
| } |
|
|
| if (characterData) { |
| res.json({ success: true, character: characterData }); |
| } else { |
| res.status(400).json({ error: '未找到角色卡数据' }); |
| } |
|
|
| } catch (error) { |
| console.error('Parse PNG error:', error); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| |
| app.post('/api/generate-pdf', async (req, res) => { |
| try { |
| const { title, chapters, style } = req.body; |
|
|
| if (!title || !chapters) { |
| return res.status(400).json({ error: '缺少必要参数' }); |
| } |
|
|
| |
| const { default: PDFDocument } = await import('pdfkit'); |
| const { default: fetch } = await import('node-fetch'); |
|
|
| |
| const doc = new PDFDocument({ |
| size: 'A4', |
| margins: { top: 50, bottom: 50, left: 50, right: 50 } |
| }); |
|
|
| |
| res.setHeader('Content-Type', 'application/pdf'); |
| res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(title)}.pdf"`); |
|
|
| |
| doc.pipe(res); |
|
|
| |
| const styles = { |
| modern: { |
| titleFont: 'Helvetica-Bold', |
| titleSize: 32, |
| headingFont: 'Helvetica-Bold', |
| headingSize: 20, |
| bodyFont: 'Helvetica', |
| bodySize: 12, |
| lineHeight: 1.5 |
| }, |
| classic: { |
| titleFont: 'Times-Bold', |
| titleSize: 36, |
| headingFont: 'Times-Bold', |
| headingSize: 22, |
| bodyFont: 'Times-Roman', |
| bodySize: 13, |
| lineHeight: 1.6 |
| }, |
| magazine: { |
| titleFont: 'Helvetica-Bold', |
| titleSize: 40, |
| headingFont: 'Helvetica-Bold', |
| headingSize: 24, |
| bodyFont: 'Helvetica', |
| bodySize: 11, |
| lineHeight: 1.4 |
| }, |
| technical: { |
| titleFont: 'Courier-Bold', |
| titleSize: 28, |
| headingFont: 'Courier-Bold', |
| headingSize: 18, |
| bodyFont: 'Courier', |
| bodySize: 10, |
| lineHeight: 1.5 |
| } |
| }; |
|
|
| const currentStyle = styles[style] || styles.modern; |
|
|
| |
| doc.font(currentStyle.titleFont) |
| .fontSize(currentStyle.titleSize) |
| .text(title, 50, 300, { align: 'center' }); |
|
|
| doc.fontSize(14) |
| .font('Helvetica') |
| .text(`生成于 ${new Date().toLocaleDateString('zh-CN')}`, 50, 400, { align: 'center' }); |
|
|
| doc.addPage(); |
|
|
| |
| for (let i = 0; i < chapters.length; i++) { |
| const chapter = chapters[i]; |
|
|
| |
| if (i > 0) { |
| doc.addPage(); |
| } |
|
|
| doc.font(currentStyle.headingFont) |
| .fontSize(currentStyle.headingSize) |
| .text(chapter.title, { align: 'left' }); |
|
|
| doc.moveDown(1); |
|
|
| |
| doc.font(currentStyle.bodyFont) |
| .fontSize(currentStyle.bodySize); |
|
|
| |
| const cleanContent = chapter.content.replace(/\[IMAGE:.*?\]/g, ''); |
| const paragraphs = cleanContent.split('\n').filter(p => p.trim()); |
|
|
| for (const paragraph of paragraphs) { |
| doc.text(paragraph, { |
| align: 'justify', |
| lineGap: currentStyle.lineHeight * 2 |
| }); |
| doc.moveDown(0.5); |
| } |
|
|
| |
| if (chapter.images && chapter.images.length > 0) { |
| for (const imageUrl of chapter.images) { |
| try { |
| |
| const imageResponse = await fetch(imageUrl); |
| const imageBuffer = await imageResponse.buffer(); |
|
|
| doc.moveDown(1); |
|
|
| |
| const pageWidth = doc.page.width - 100; |
| doc.image(imageBuffer, 50, doc.y, { |
| fit: [pageWidth, 300], |
| align: 'center' |
| }); |
|
|
| doc.moveDown(1); |
| } catch (error) { |
| console.error('添加图片失败:', error); |
| } |
| } |
| } |
| } |
|
|
| |
| doc.end(); |
|
|
| } catch (error) { |
| console.error('PDF生成错误:', error); |
| res.status(500).json({ error: error.message }); |
| } |
| }); |
|
|
| const PORT = process.env.PORT || 7860; |
|
|
| app.listen(PORT, () => { |
| console.log(`服务器运行在端口 ${PORT}`); |
| }); |