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')); // API代理 - 处理LLM聊天请求 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}` }); } // 设置SSE响应头 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 }); } }); // ComfyUI代理 - 提交工作流 app.post('/api/comfyui/prompt', async (req, res) => { try { const { comfyuiUrl, workflow, prompt } = req.body; if (!comfyuiUrl || !workflow) { return res.status(400).json({ error: '缺少必要参数' }); } // 在工作流中查找并替换CLIP文本提示词 const modifiedWorkflow = JSON.parse(JSON.stringify(workflow)); // 遍历工作流节点,找到CLIPTextEncode节点 for (const nodeId in modifiedWorkflow) { const node = modifiedWorkflow[nodeId]; if (node.class_type === 'CLIPTextEncode' && node.inputs) { node.inputs.text = prompt; } } // 发送到ComfyUI 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 }); } }); // ComfyUI - 获取生成的图片 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是否正常运行' }); } }); // 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: '缺少必要参数' }); } // 构建ComfyUI图片URL 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 }); } }); // ComfyUI - 测试连接 app.get('/api/comfyui/test', async (req, res) => { try { const { url } = req.query; if (!url) { return res.status(400).json({ success: false, error: '缺少URL参数' }); } // 尝试连接到ComfyUI 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 }); } }); // 解析PNG角色卡 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); // 查找tEXt chunk中的character数据 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 { // Base64解码 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 }); } }); // PDF生成API 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'); // 创建PDF文档 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); // 移除IMAGE标记 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); } } } } // 完成PDF 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}`); });