零基础入门Node.js开发:通过通义千问为曲靖M CMS集成文生图应用

在当前内容管理系统的开发中,为提升用户体验和丰富内容展示形式,我们计划为曲靖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技术的可能性。

发表回复