- 在AI迅速发展的现在,身为前端工程师也要学习AI,跟上技术潮流,保持自身竞争力。
- 本文会分享一个DIFY工作流,完成了 市场分析->生成PPT->总结内容和标题->发送邮件 的流程,使用的技术:node写后端接口、dify制作工作流、marp将md转换成PPT。当然,这样的流程也存在缺陷,后续也换了其他的技术,接下来会一一说明。
1、DIFY部署
使用DIFY官网提供的云端DIFY存在很多的限制,例如只有单工作空间、工作空间最多10个项目、知识库容量限制等。所以我们可以在自己的服务器上部署DIFY,这样私有化部署,数据的安全性也得到了提高。DIFY的部署、为什么会选择DIFY,这些问题在上一篇文章中有详细描述。
2、引入大模型
dify支持的大模型非常多,我使用的是kimi联网搜索版,kimi每人有15的免费额度,像豆包、文心、智谱等都有免费的额度,测试的时候可以先用免费的。
调用kimi接口有两点注意事项,一是使用网络搜索需要自己加上tools $web_search,二是如果需要kimi输出json格式的数据,需要指定一下response_format
node接口示例如下:
const openai = require('openai'); // 需要安装 openai 库
const { kimiConfig } = require('../config/kimi');
const client = new openai.OpenAI({
apiKey: kimiConfig.apiKey,
baseURL: kimiConfig.baseURL,
});
const tools = [
{
type: 'builtin_function',
function: {
name: '$web_search',
},
},
];
class KimiReqService {
constructor() {
this.client = new openai.OpenAI({
apiKey: kimiConfig.apiKey,
baseURL: kimiConfig.baseURL,
});
}
async kimiReq(content, isJSON = false) {
let finishReason = null;
let tool_result = null;
let finalResponse = '';
const messages = [
{
role: 'system',
content:
'你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。',
},
{ role: 'user', content: content },
];
while (finishReason === null || finishReason === 'tool_calls') {
const completion = await client.chat.completions.create({
model: 'moonshot-v1-auto',
messages: messages,
temperature: 0.3,
tools: tools,
response_format: isJSON ? { type: 'json_object' } : { type: 'text' },
});
const choice = completion.choices[0];
finishReason = choice.finish_reason;
if (finishReason === 'tool_calls') {
messages.push(choice.message);
for (const toolCall of choice.message.tool_calls) {
const tool_call_name = toolCall.function.name;
const tool_call_arguments = JSON.parse(toolCall.function.arguments);
if (tool_call_name === '$web_search') {
tool_result = tool_call_arguments;
} else {
tool_result = 'no tool found';
}
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
name: tool_call_name,
content: JSON.stringify(tool_result),
});
}
} else {
finalResponse = choice.message.content;
}
}
return finalResponse;
}
}
module.exports = new KimiReqService();
3、DIFY HTTP节点调用注意事项
POST请求的form-data使用下来貌似有bug(有成功的朋友可以说一声),所以我选择用JSON传递参数。大模型输出的body也是string类型,我们需要对输出结果进行转换,然后才可以直接调用。
4、PPT生成
PPT生成,我使用的是Marp,如果有用过的朋友会知道,Marp是基于 Markdown 的开源幻灯片制作工具,可以轻松将 Markdown 文档转换为精美的幻灯片,输出格式可以是HTML、PPTX、PDF。当然Marp有一个致命的缺点就是,他输出的PPT是不可编辑的,和直接编辑PPT有本质的区别。(为了生成可编辑的PPTX文件,后续换了一个PPTist的方案,开发完成后会输出文档,欢迎一起交流。)
所以,接着之前的工作流,这一步会将得到的分析内容,接下来需要转换成md格式,方便Marp去输出PPT(提示词需要多调试调试)。
MARP使用示例:
const path = require('path');
const fs = require('fs').promises;
const { execSync } = require('child_process');
class generateService {
async generatePPT(content, type = 'pptx') {
try {
const result = await this.marpPPT(content, type);
return result;
} catch (error) {
console.error('生成PPT错误:', error);
throw error;
}
}
async marpPPT(content, type = 'pptx') {
const timestamp = Date.now();
const mdFileName = `slides_${timestamp}.md`;
const outputFileName = `slides_${timestamp}.${type}`;
// 设置目录路径
const baseDir = process.cwd();
const mdDir = path.join(baseDir, 'public/markdown');
const outputDir = path.join(baseDir, 'public/ppt');
let mdPath = null;
let outputPath = null;
try {
// 创建目录
await fs.mkdir(mdDir, { recursive: true });
await fs.mkdir(outputDir, { recursive: true });
mdPath = path.join(mdDir, mdFileName);
outputPath = path.join(outputDir, outputFileName);
// 写入markdown内容并等待完成
await fs.writeFile(mdPath, content, 'utf8');
console.log('Markdown文件已创建:', mdPath);
// 确认文件存在
const stats = await fs.stat(mdPath);
console.log('Markdown文件大小:', stats.size);
// 使用 execSync 执行命令
try {
const cmd = `npx --no @marp-team/marp-cli "${mdPath}" --pptx -o "${outputPath}" --chrome --no-sandbox --disable-setuid-sandbox`;
console.log('执行命令:', cmd);
const output = execSync(cmd, {
cwd: baseDir,
timeout: 600000,
stdio: 'pipe',
});
console.log('命令执行输出:', output.toString());
} catch (execError) {
console.error('命令执行错误:', execError);
console.error('错误输出:', execError.stderr?.toString());
throw new Error(`PPT转换失败: ${execError.message}`);
}
// 确认输出文件存在
await fs.access(outputPath);
console.log('PPT文件已生成:', outputPath);
// 读取生成的文件
const fileContent = await fs.readFile(outputPath);
console.log('PPT文件大小:', fileContent.length);
return {
mdPath,
filePath: outputPath,
fileContent,
};
} catch (error) {
console.error('详细错误:', error);
throw new Error(`生成PPT失败: ${error.message}`);
}
}
async cleanUp(path, mdPath) {
if (path) {
fs.unlink(path).catch((e) => console.error('清理PPT文件失败:', e));
}
if (mdPath) {
fs.unlink(mdPath).catch((e) => console.error('清理MD文件失败:', e));
}
}
}
module.exports = new generateService();
5、发送邮件
DIFY有发送邮件的工具,但是这个工具不支持邮件名有_ 等特殊符号的,所以只能用nodemailer手搓一个发送邮件的接口。在发送之前,需要大模型总结一下邮件的标题和正文内容,最后标题、正文、附件PPT一同发送至目标邮箱。
const sendEmailService = require("../service/email");
const path = require("path");
const emailController = async (req, res) => {
if (!Array.isArray(req.body.emails)) {
return res.json({
success: false,
message: "emails 必须是数组",
});
}
const body = {
emails: req.body.emails.join(","),
subject: req.body.subject || "主题",
text: req.body.text || "正文",
attachments: req.body.attachments || [
{
filename: "test.txt", // 附件文件名
path: path.join(__dirname, "../../public/ppt/test.txt"), // 附件路径
},
],
};
// 邮件选项
const mailOptions = {
from: "1342980924@qq.com", // 发件人地址
to: body.emails, // 收件人地址
subject: body.subject, // 邮件主题
text: body.text, // 邮件正文
attachments: body.attachments,
};
try {
await sendEmailService(mailOptions);
return res.json({
success: true,
message: "邮件发送成功",
});
} catch (error) {
return res.json({
success: false,
message: error,
});
}
};
module.exports = {
emailController,
};
//services
const nodemailer = require("nodemailer");
const { smtpConfig } = require("../config/email");
class EmailService {
constructor() {
this.transporter = nodemailer.createTransport(smtpConfig);
}
async sendEmail(mailOptions) {
return new Promise((resolve, reject) => {
this.transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return reject("发送失败: " + error);
}
return resolve("邮件发送成功: " + info.respons);
});
});
}
}
module.exports = new EmailService();
6、总结
这是一个前后端和工作流结合开发的尝试,要到生产环境中跑起来还有许多的问题,例如:
- 开头分析的结果可以再跑一个工作流输出更详细、更准确的内容;
- 大模型输出md文档也许会有问题导致PPT不能正确分页;
- 大模型输出内容过少,PPT留白太多;
- Marp接口部署在服务器上需要正确配置无头浏览器;
- dify部署对服务器要求较高;
- dify社区版还有很多bug等。