demo / server.js
wkplhc's picture
Update server.js
0eb57a2 verified
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}`);
});