在当前内容管理系统的开发中,为提升用户体验和丰富内容展示形式,我们计划为曲靖M内容管理系统(CMS)集成一个文本生成图像的应用。然而,在尝试直接从前端调用阿里云百炼大模型API进行文本到图像的应用时,遇到了跨域资源共享(CORS)的问题。为了解决这一挑战,并且由于对Node.js并不熟悉,我决定借助通义千问的帮助来编写一个基于Node.js的服务端代理程序。
一、为何选择集成文生图功能?
集成文本生成图像的功能可以极大地增强曲靖M CMS的内容创作灵活性。无论是自动生成文章配图、用户个性化头像还是其他创意用途,这项技术都能提供强大的支持,使内容更加生动和吸引人。
二、面临的挑战及解决方案
跨域问题:前端直接调用API时会遇到CORS限制,导致请求失败。为克服这个问题,最佳实践是在服务端创建一个代理程序,所有来自前端的请求都先发送给这个代理,再由代理与外部API交互。
缺乏Node.js经验:考虑到服务端逻辑需要快速实现且易于维护,选择了Node.js作为开发语言。尽管没有接触过Node.js,但通过向通义千问提问并遵循其指导,能够迅速搭建起所需的应用程序。
三、构建Node.js应用的具体步骤
准备工作:包括安装Node.js、初始化项目以及获取阿里云API Key等,如前文所述。
编写代理服务器代码:
1、利用Express框架简化HTTP服务器的创建过程。
2、使用axios库处理对外部API的请求。
3、在路由处理函数中转发前端传来的数据至阿里云API,并将响应结果返回给前端。
MVC 架构
Model: 负责处理数据和业务逻辑。
View: 处理用户界面显示(在这个项目中没有详细展示)。
Controller: 处理用户请求并调用模型方法返回结果。
输入验证
使用 Joi 库来验证输入数据,确保数据格式正确。
在 app/aigc/validators/text2image.js 中定义了两个验证函数:
validateGenerateImageInput: 验证生成图像时传入的数据。
validateGetTaskStatusParams: 验证获取任务状态时传入的路径参数。
自动路由
使用自定义路由机制来动态加载控制器和方法。
customRouter.js 根据请求路径自动解析模块、控制器和方法,并传递参数。
├── app/
│ ├── aigc/
│ │ ├── controllers/
│ │ │ └── text2image.js
│ │ ├── models/
│ │ │ └── text2image.js
│ │ └── validators/
│ │ └── text2image.js
│ ├── index/
│ │ └── controllers/
│ │ └── index.js
├── middlewares/
│ ├── corsMiddleware.js
│ └── errorHandler.js
├── routes/
│ ├── customRouter.js
│ └── validators/
│ └── routeValidator.js
├── utils/
│ └── logger.js
├── .env
├── config/
│ └── config.js
├── main.js
└── package.json
.env
API_KEY=YOUR_API_KEY
MODEL=wanx-poster-generation-v1
PORT=8030
DEFAULT_MODULE=index
DEFAULT_CONTROLLER=index
DEFAULT_METHOD=index
VIDEO_GENERATION_SYNTHESIS_MODEL=wanx2.1-t2v-plus
API_URL=https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis
TASK_API_URL=https://dashscope.aliyuncs.com/api/v1/tasks/
config/config.js
require('dotenv').config();
module.exports = {
apiKey: process.env.API_KEY,
model: process.env.MODEL,
port: process.env.PORT || 8030,
defaultModule: process.env.DEFAULT_MODULE || 'index',
defaultController: process.env.DEFAULT_CONTROLLER || 'index',
defaultMethod: process.env.DEFAULT_METHOD || 'index',
apiUrl: process.env.API_URL,
taskApiUrl: process.env.TASK_API_URL,
videoGenerationSynthesisApiUrl:process.env.VIDEO_GENERATION_SYNTHESIS_API_URL,
videoGenerationTaskApiUrl:process.env.VIDEO_GENERATION_TASK_API_URL,
videoGenerationSynthesisModel:process.env.VIDEO_GENERATION_SYNTHESIS_MODEL
};
app/aigc/controllers/text2image.js
const text2imageModel = require('../models/text2image');
const text2imageValidator = require('../validators/text2image');
const logger = require('../../../utils/logger');
const config = require('../../../config/config'); // 引入配置文件
// 控制器函数:获取任务状态
exports.getTaskStatus = async (req, res, next) => {
const taskId = req.params.task_id; // 获取任务 ID
// 验证传入的任务 ID 是否符合预期
try {
await text2imageValidator.validateGetTaskStatusParams({ task_id: taskId });
} catch (validationError) {
logger.error('Validation Error:', validationError.details.map(detail => detail.message));
return res.status(400).json({ errors: validationError.details.map(detail => detail.message) });
}
try {
const taskStatus = await text2imageModel.getTaskStatus(taskId);
res.status(200).json(taskStatus); // 返回任务状态
} catch (error) {
next(error); // 将错误传递给错误处理中间件
}
};
// 控制器函数:生成图像
exports.generateImage = async (req, res, next) => {
const input = req.body; // 获取请求体中的数据
// 验证传入的数据结构是否符合预期
try {
await text2imageValidator.validateGenerateImageInput(input);
} catch (validationError) {
logger.error('Validation Error:', validationError.details.map(detail => detail.message));
return res.status(400).json({ errors: validationError.details.map(detail => detail.message) });
}
const data = {
'model': config.model, // 使用配置文件中的模型名称
'parameters': {},
'input': {
'generate_mode': input.generate_mode || 'generate', // 设置生成模式,默认值为“generate”
'title': input.title || '', // 设置标题,必选
'sub_title': input.sub_title || '', // 设置副标题
'body_text': input.body_text || '', // 设置主体文本
'prompt_text_zh': input.prompt_text_zh || '', // 设置中文提示文本
'wh_ratios': input.wh_ratios || '竖版', // 设置宽高比,默认值为“竖版”
'lora_name': input.lora_name || '', // 设置 LoRA 名称,默认值为为空
'lora_weight': input.lora_weight || 0.8, // 设置 LoRA 权重,默认值为 0.8
'ctrl_ratio': input.ctrl_ratio || 0.7, // 设置控制比率,默认值为 0.7
'ctrl_step': input.ctrl_step || 0.7, // 设置控制步数,默认值为 0.7
'generate_num': input.generate_num || 1 // 设置生成数量,默认值为 1
}
};
try {
const generatedImage = await text2imageModel.generateImage(data);
res.status(200).json(generatedImage); // 返回生成的图像数据
} catch (error) {
next(error); // 将错误传递给错误处理中间件
}
};
app/aigc/models/text2image.js
const axios = require('axios');
const logger = require('../../../utils/logger');
const config = require('../../../config/config'); // 引入配置文件
// 获取任务状态
async function getTaskStatus(taskId) {
const url = `${config.taskApiUrl}${taskId}`;
const headers = {
'Content-Type': 'application/json', // 设置内容类型为 JSON
'Authorization': `Bearer ${config.apiKey}` // 设置授权头
};
try {
const response = await axios.get(url, { headers });
return response.data;
} catch (error) {
logger.error('Error fetching task status:', error);
throw error;
}
}
// 生成图像
async function generateImage(data) {
const url = config.apiUrl; // 使用配置文件中的 URL
const headers = {
'X-DashScope-Async': 'enable', // 设置异步请求头
'Content-Type': 'application/json', // 设置内容类型为 JSON
'Authorization': `Bearer ${config.apiKey}` // 设置授权头
};
try {
const response = await axios.post(url, data, { headers }); // 发送 POST 请求到 DashScope AIGC 服务
return response.data;
} catch (error) {
logger.error('Error generating image:', error);
throw error;
}
}
module.exports = {
getTaskStatus,
generateImage
};
app/aigc/validators/text2image.js
const Joi = require('joi');
// 验证生成图像的输入
const validateGenerateImageInput = (data) => {
const schema = Joi.object({
generate_mode: Joi.string().valid('generate', 'sr', 'hrf').default('generate'),
title: Joi.string().max(30).required(),
sub_title: Joi.string().max(30).allow('').optional(),
body_text: Joi.string().max(50).allow('').optional(),
prompt_text_zh: Joi.string().max(50).required(),
wh_ratios: Joi.string().valid('竖版', '横版').default('竖版'),
lora_name: Joi.string().valid('2D插画1', '2D插画2', '浩瀚星云', '浓郁色彩', '光线粒子', '透明玻璃', '剪纸工艺', '折纸工艺', '中国水墨', '中国刺绣', '真实场景', '2D卡通', '儿童水彩', '赛博背景', '浅蓝抽象', '深蓝抽象', '抽象点线', '童话油画').default(''),
lora_weight: Joi.number().min(0).max(1).default(0.8),
ctrl_ratio: Joi.number().min(0).max(1).default(0.7),
ctrl_step: Joi.number().min(0).max(1).default(0.7),
generate_num: Joi.number().integer().min(1).max(4).default(1)
});
return schema.validate(data);
};
// 验证获取任务状态的路径参数
const validateGetTaskStatusParams = (params) => {
const schema = Joi.object({
task_id: Joi.string().alphanum().length(24).required() // 假设 task_id 是一个 24 字符长的字母数字字符串
});
return schema.validate(params);
};
module.exports = {
validateGenerateImageInput,
validateGetTaskStatusParams
};
app/index/controllers/index.js
// 默认控制器方法
exports.index = (req, res, next) => {
res.status(200).json({ message: 'Welcome to the default endpoint' });
};
middlewares/corsMiddleware.js
const cors = require('cors');
const corsOptions = {
origin: [
'http://localhost:9527',
/^https:\/\/.*\.mp\.qujingm\.com$/
],
optionsSuccessStatus: 200 // 一些旧版本浏览器支持的成功状态码
};
module.exports = cors(corsOptions);
middlewares/errorHandler.js
const logger = require('../utils/logger');
function errorHandler(err, req, res, next) {
logger.error('Unhandled Error:', err.stack);
res.status(err.status || 500).json({
message: err.message || 'Internal Server Error'
});
}
module.exports = errorHandler;
routes/customRouter.js
const fs = require('fs');
const path = require('path');
const config = require('../config/config'); // 引入配置文件
const routeValidator = require('./validators/routeValidator');
const logger = require('../utils/logger');
module.exports = async (req, res, next) => {
const pathSegments = req.path.split('/').filter(segment => segment.length > 0);
let moduleName, controllerName, methodName, params;
if (pathSegments.length === 0) {
// 默认路径 "/"
moduleName = config.defaultModule;
controllerName = config.defaultController;
methodName = config.defaultMethod;
params = {};
} else if (pathSegments.length >= 3 && pathSegments.length % 2 === 1) {
[moduleName, controllerName, methodName] = pathSegments.slice(0, 3);
params = {};
for (let i = 3; i < pathSegments.length; i += 2) {
const paramName = pathSegments[i];
const paramValue = pathSegments[i + 1];
params[paramName] = paramValue;
}
} else {
return res.status(400).json({ message: 'Invalid request path' });
}
// 验证路径参数
try {
await routeValidator.validatePathParams({
moduleName: moduleName || '',
controllerName: controllerName || '',
methodName: methodName || '',
paramNames: Object.keys(params),
paramValues: Object.values(params)
});
} catch (validationError) {
logger.error('Validation Error:', validationError.details.map(detail => detail.message));
return res.status(400).json({ errors: validationError.details.map(detail => detail.message) });
}
const controllerPath = path.join(__dirname, `../app/${moduleName}/controllers/${controllerName}.js`);
if (!fs.existsSync(controllerPath)) {
return res.status(404).json({ message: 'Controller not found' });
}
const controller = require(controllerPath);
if (!controller[methodName]) {
return res.status(404).json({ message: 'Method not found in controller' });
}
req.params = { ...req.params, ...params };
controller[methodName](req, res, next);
};
routes/validators/routeValidator.js
const Joi = require('joi');
// 验证路径参数
const validatePathParams = (params) => {
const schema = Joi.object({
moduleName: Joi.string().pattern(/^[a-zA-Z]+$/), // 模块名称必须是字母组成
controllerName: Joi.string().pattern(/^[a-zA-Z]+$/), // 控制器名称必须是字母组成
methodName: Joi.string().pattern(/^[a-zA-Z]+$/), // 方法名称必须是字母组成
paramNames: Joi.array().items(Joi.string().pattern(/^[a-zA-Z_]+$/)), // 参数名必须是字母或下划线组成
paramValues: Joi.array().items(Joi.string()) // 参数值可以是任意字符串
});
return schema.validate(params);
};
module.exports = {
validatePathParams
};
utils/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;
main.js
const express = require('express'); // 导入 Express 模块
const corsMiddleware = require('./middlewares/corsMiddleware'); // 引入 CORS 中间件
const errorHandler = require('./middlewares/errorHandler'); // 引入错误处理中间件
const customRouter = require('./routes/customRouter'); // 引入自定义路由处理器
const config = require('./config/config'); // 引入配置文件
const app = express(); // 创建 Express 应用实例
app.use(express.json()); // 使用中间件来解析 JSON 格式的请求体
// 使用 CORS 中间件
app.use(corsMiddleware);
// 使用自定义路由处理器
app.use(customRouter);
// 使用错误处理中间件
app.use(errorHandler);
// 启动服务器并监听指定端口
app.listen(config.port, () => {
console.log(`Server is running on http://localhost:${config.port}`); // 输出启动信息
});
package.json
{
"name": "曲靖M AIGC",
"version": "1.0.0",
"description": "曲靖M AIGC",
"main": "main.js",
"scripts": {
"start": "node main.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.8.2",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"joi": "^17.13.3",
"winston": "^3.17.0"
}
}
启动应用:
node main.js
测试应用:
在浏览器中访问 http://localhost:8030 并检查是否能正常访问。然后通过 Nginx 访问 http://your_domain.com 并检查是否还能复现 502 Bad Gateway 错误。
四、总结
通过集成文本生成图像的功能到曲靖M CMS中,不仅解决了前端调用API时遇到的跨域问题,还提升了系统的内容创造能力。对于不熟悉Node.js的开发者来说,借助AI助手如通义千问的支持,可以高效地完成从零开始的学习和应用开发工作。希望本文能为你提供有价值的参考,并鼓励你探索更多关于Node.js和AI技术的可能性。