n8n 全栈开发:从自动化工具到全功能应用开发平台 --知识铺
创业者最怕什么?不是没有好想法,而是想法还在 PPT 里,竞争对手的产品已经上线了。
🤔
在你的工作中,是否遇到过这样的情况?有了绝妙的想法,但传统开发流程让你眼睁睁错失了最佳时机?
传统产品开发流程往往需要数月时间:提出想法 → 市场调研 → 排定开发周期 → 协调前后端资源… 当严格走完这一整套流程后,市场可能早已变了天,精心打造的产品最终只是一场"自嗨"。
那么,有没有一种方法,能让我们以“周”甚至“天”为单位,快速构建出一个包含核心功能的产品原型(MVP),并立刻将其投向市场进行验证呢?
提到“快速开发”,很多人会想到 n8n。在大多数人的认知里,n8n 是一个极其强大的后端自动化工具,是连接不同 API 的“超级胶水”。但它的能力,真的仅限于此吗?
今天,我们将彻底打破 n8n 只能做后端自动化的固有认知,一起见证并亲手实践,如何用 n8n 从零开始构建一个既有前端交互界面、又有后端强大逻辑的完整应用。
📌
可复制代码:n8n 全栈开发:从自动化工具到全功能应用开发平台
一、让 n8n 成为你的 Web 服务器 - 响应HTML
一)Webhook:不止是数据的搬运工
在 n8n 的生态中,Webhook 节点是我们最常打交道的“门户”。通常,我们用它来接收外部请求、触发工作流、或者对外提供一个返回 JSON 数据的 API 接口。在这些场景下,Webhook 扮演的是一个高效的“数据搬运工”。
但它的能力远不止于此。
如果我们换一个思路,既然 Webhook 能响应数据,那它是否也能直接响应一个完整的、可交互的网页呢?答案是肯定的。这正是我们将 n8n 从一个纯后端工具,转变为一个具备全栈能力的关键一步。
二)核心原理:Content-Type 的魔力
要让浏览器将一串文本渲染成一个网页,而不是当作普通字符或数据来下载,关键在于我们如何告知浏览器。这个“告知”的动作,就是通过设置 HTTP 响应头(Headers)中的 Content-Type 字段来完成的。
- 当 Content-Type 为 application/json 时,浏览器知道它收到的是一份 JSON 数据。
- 而当我们明确地将 Content-Type 设置为 html 时,浏览器就会立刻明白:“好的,这是一份 HTML 文档,我需要用渲染引擎把它解析成一个网页来展示。”
理解了这一点,我们在 n8n 中实现“后端页面渲染”的路径就非常清晰了。
三)实战:三步构建你的第一个 n8n 网页
现在,我们来动手实践,让 n8n 返回一个经典的“Hello, World!”网页。
1、第一步:创建 Webhook 触发器
- 1.在 n8n 画布中,添加一个新的 Webhook 节点。
- 2.将 HTTP Method 设置为 GET。
- 3.设置响应模式:找到 Respond 选项,确保其设置为 Using ‘Respond to Webhook’ Node。
💡
说明:这个设置至关重要。它告诉触发器节点:请不要立即或在工作流结束时自动回复,而是等待一个专门的‘Respond to Webhook’节点来构造和发送响应。
这种模式给了我们最大的灵活性,让我们可以在工作流的任何位置、任何时机向前端返回内容,是构建复杂应用的基础。
- 4.复制 Production URL,稍后我们将在浏览器中访问它。
2、第二步:配置 Respond to Webhook 节点
- 1.添加一个 Respond to Webhook 节点,并将其连接到 Webhook 节点之后。
- 2.在节点配置中,找到 Respond With 选项,将其从默认的 First Incoming Item 修改为 Text。
-
3.在下方的 Response Body 输入框中,粘贴以下基础的 HTML 代码:
<span style="color: #61aeee;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><!DOCTYPE <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">html<span leaf="">><span leaf="">
<html lang=“zh-CN”>
<head>
<meta charset=“UTF-8”>
<meta name=“viewport” content=“width=device-width, initial-scale=1.0”>
<title>Hello World</title>
<style>
body {
font-family: ‘Arial’, sans-serif;
background: linear-gradient(135deg, #667eea0%, #764ba2100%);
margin: 0;
padding: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 60px40px;
box-shadow: 08px32pxrgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: fadeInUp 1s ease-out;
}
h1 {
color: white;
font-size: 3.5rem;
margin: 0;
text-shadow: 2px2px4pxrgba(0, 0, 0, 0.3);
animation: bounce 2s infinite;
}
p {
color: rgba(255, 255, 255, 0.8);
font-size: 1.2rem;
margin-top: 20px;
animation: fadeIn 2s ease-in;
}
.emoji {
font-size: 4rem;
animation: rotate 3s linear infinite;
display: inline-block;
margin: 20px0;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
h1 {
font-size: 2.5rem;
}
.container {
padding: 40px20px;
margin: 20px;
}
}
</style>
</head>
<body>
<div class=“container”>
<div class=“emoji”>🌍</div>
<h1>Hello World!</h1>
<p>欢迎来到我的第一个网页</p>
</div>
<script>
// 简单的交互效果
document.addEventListener(‘click’, function() {
const container = document.querySelector(’.container’);
container.style.transform = ‘scale(1.05)’;
setTimeout(() => {
container.style.transform = ‘scale(1)’;
}, 200);
});
// 控制台输出
console.log(‘Hello World from JavaScript!’);
</script>
</body>
</html>
```
3、第三步:设置正确的响应头 (关键步骤)
- 1.在 Respond to Webhook 节点配置中,点击底部的 Options 展开选项,然后选择 Add Option -> Response Headers。
- 2.在出现的 Headers 区域,点击 Add Response Header。
- 3.在 Name 字段中输入 Content-Type。在 Value 字段中输入 html。
完成以上配置后,激活你的工作流。现在,在浏览器中访问你第一步复制的 Webhook URL,你将看到一个由 n8n 驱动的、活生生的网页!
通过这简单的三步,我们已经成功地让 n8n 扮演了一个 Web 服务器的角色。这为我们后续在 n8n 中构建更复杂的前端应用打下了坚实的基础。
二、让你的网页“活”起来 - 前后端通信
在上一节,我们已经成功迈出了第一步:让 n8n 充当 Web 服务器,返回了一个精美的 “Hello, World!” 静态页面。这是一个了不起的开始,但一个静态页面无法与用户交互,它的内容是固定的。
那么,我们如何才能更进一步,构建一个能接收用户输入、进行复杂处理、并动态返回结果的真实应用呢?
答案是:前后端通信。这正是我们将 n8n 从一个页面展示工具,升级为全功能应用开发平台的关键。为了让这个过程更有趣、更贴近真实场景,我们将构建一个AI 智能生活助理。
📌
教学目标: 通过从零开始剖析这个“智能生活助理”工作流,你将掌握在 n8n 中构建全栈应用的核心模式:使用一个工作流提供前端界面(UI),界面上的 JavaScript 再调用另一个工作流作为后端应用程序接口(API),后端 API 甚至可以集成强大的 AI Agent 来处理复杂任务。
一)设计思路:分工明确的前后端
和专业的 Web 开发一样,我们将任务拆分为“前端”和“后端”两个部分,它们都由 n8n 的工作流来承载:
-
1.前端界面工作流 (GET Webhook):
- 职责:它的任务非常纯粹,就是当用户通过浏览器访问时,提供一个美观、易用的 HTML 操作界面。
- 实现:由 触发器:提供UI界面 节点(设置为 GET 请求)和一个包含完整 HTML/CSS/JS 代码的 响应:返回UI界面HTML 节点构成。
- 2.后端 API 工作流 (POST Webhook):
- 职责:这是应用的“大脑”和“动力核心”。它作为一个 API 接口,负责接收前端发送的用户问题,利用 AI Agent 进行智能分析和处理,调用高德地图等外部工具获取信息,最后将处理好的结果返回给前端。
- 实现: 由 触发器:接收生成指令 节点(设置为 POST 请求)触发,后续连接着一整套复杂的 AI Agent 逻辑。
这种模式完美复刻了现代 Web 应用的开发思想,只不过,我们的前端和后端都运行在 n8n 里!
二)实战:搭建你的智能生活助理应用
现在,让我们深入这份工作流,一步步揭示这个智能应用是如何构建的。
1、剖析前端界面 (GET 工作流)
这个工作流为我们的应用提供了“面子”。
- UI 界面:响应:返回UI界面HTML 节点中的代码构建了一个非常完善的用户界面,包括一个用于自然语言输入的大文本框、一些方便操作的快捷示例标签,以及一个用于动态展示结果的区域。
- 核心交互 (JavaScript): 在 标签内,handleQuery 函数是关键。当用户点击“开始查询”按钮时,它会:
- a.获取输入框(queryInput)中的文本内容。
- b.调用 fetch 函数,向后端的 API 地址 (/webhook/smart-assistant) 发送一个 POST 请求。
- c.请求的 body 是一个 JSON 对象,格式为 { “query”: “用户输入的内容” }。
- d.等待后端返回结果,并将其动态地渲染到结果区域(resultDisplay)中。
2、探秘智能后端 (POST 工作流)
如果说前端是应用的“面子”,那么这个由 AI Agent 驱动的后端就是应用的“里子”。
- 1.接收指令:触发器:接收生成指令 节点准确地接收到前端发来的 POST 请求和包含用户问题的 JSON 数据。
- 2.组装 AI Agent: 这个应用的核心是 AI Agent。你可以把它理解为一个拥有“大脑”和“工具”的智能机器人。
-
大脑 (LLM):DeepSeek Chat Model 节点扮演着“大脑”的角色,它赋予了 Agent 理解、推理和生成语言的能力。
-
工具 (Tools):HTTP Streamable 节点被配置成了一个高德地图的工具,它像是 Agent 的“手臂”,让 Agent 有能力去获取现实世界的地理和天气信息。
💡
注意: 该节点需要你提前在 n8n 的环境变量中配置好名为 AMAP_API_KEY 的高德 API 密钥。
-
中枢 (Agent 节点):智能助理核心 节点是 Agent 的灵魂。它将“大脑”和“工具”连接在一起,并根据我们为它设定的行动纲领 (System Prompt) 来工作。
- 3.返回结果
Agent 完成思考和工具调用后,会生成一段完美的答复。返回最终JSON结果 节点会将这段答复包装成 JSON 格式,响应给正在等待的前端页面。
3、完整流程回顾
现在,让我们把前后端串起来,完整地看一遍用户提问“明天北京天气如何?”时发生了什么:
- 1.用户在浏览器中输入问题,点击查询。
- 2.前端 JavaScript 将 {“query”: “明天北京天气如何?"} 发送到后端 API。
- 3.后端 Agent 的“大脑”分析问题,根据 System Prompt 里的规则,识别出这是一个天气查询任务,决定使用“高德地图工具”。
- 4.Agent 调用工具,传入“北京” 等参数,工具返回了实时的天气数据(如:晴,15-25度)。
- 5.Agent 的“大脑”再次工作,将这些枯燥的数据,按照 System Prompt 规定的模板,润色成一段生动、友好的天气预报。
- 6.后端将这段生成好的文本返回给前端。
- 7.前端 JavaScript 接收到结果,并将其显示在页面上。
三)关键代码深度解读:连接前后端的“魔法”
我们已经了解了智能助理应用的整体架构。现在,让我们聚焦于最核心的部分:前端界面是如何将用户的问题,准确无误地发送给后端 AI Agent 的?这背后的“魔法”,就藏在 响应:返回 UI 界面 HTML 节点中的 HTML 和 JavaScript 代码里。
1、HTML 结构
我们需要关注的是那些负责接收用户输入和展示结果的关键元素。它们通过 id 属性被命名,以便 JavaScript 能够轻松地找到并操作它们。
HTML
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">class<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"query-panel"<span leaf="">><span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">form<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"queryForm"<span leaf="">><span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">textarea<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"queryInput"<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">class<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"query-input"<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">placeholder<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"例如:明天去上海出差帮我查下天气..."<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">required<span leaf="">
<span leaf=""> ><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">textarea<span leaf="">><span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">button<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">type<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"submit"<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">class<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"query-btn"<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"submitBtn"<span leaf="">><span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">span<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"btnText"<span leaf="">><span leaf="">🔍 开始查询<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">span<span leaf="">><span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"loadingSpinner"<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">class<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"loading-spinner hidden"<span leaf="">><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">><span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">button<span leaf="">><span leaf="">
<span leaf=""> <span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">form<span leaf="">><span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">><span leaf="">
<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">class<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"result-panel"<span leaf="">><span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">class<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"result-display hidden"<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"resultDisplay"<span leaf="">><span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">id<span leaf="">=<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"resultItems"<span leaf="">><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">><span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">><span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""></<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">>
-
讲解要点:
-
id=“queryForm”:整个表单,JavaScript 会监听它的“提交”事件。
-
id=“queryInput”:用户输入问题的文本框。JavaScript 将从这里获取用户的原始问题。
-
id=“submitBtn”:查询按钮。
-
id=“resultDisplay” 和 id=“resultItems”:右侧的结果展示区域。当后端返回结果后,JavaScript 将在这里填充内容。
2、JavaScript fetch:前后端通信的“桥梁”
这是整个前后端交互的核心代码。它构建了一个网络请求,将前端数据发送到后端,并负责接收返回的答案。
<span leaf="">// 在 <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf=""><<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">script<span leaf="">><span leaf=""> 标签内找到 handleQuery 函数<span leaf="">
<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 处理查询<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">async<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">function<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">handleQuery<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">query<span leaf="">) {<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">try<span leaf=""> {<span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 1. 显示加载动画,提升用户体验<span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">showLoading<span leaf="">();<span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 2. 【核心】使用 fetch 发送异步网络请求<span leaf="">
<span leaf=""> <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> response = <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">await<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">fetch<span leaf="">(<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'/webhook/smart-assistant'<span leaf="">, {<span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 2a. 请求方法:POST,用于发送数据<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">method<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'POST'<span leaf="">,<span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 2b. 请求头:告诉后端我们发送的是 JSON 格式的数据<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">headers<span leaf="">: {<span leaf="">
<span leaf=""> <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'Content-Type'<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'application/json'<span leaf="">,<span leaf="">
<span leaf=""> },<span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 2c. 请求体:将用户的问题包装成 JSON 字符串<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">body<span leaf="">: <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">JSON<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">stringify<span leaf="">({<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">query<span leaf="">: query,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">timestamp<span leaf="">: <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">new<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Date<span leaf="">().<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">toISOString<span leaf="">()<span leaf="">
<span leaf=""> })<span leaf="">
<span leaf=""> });<span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 3. 解析后端返回的 JSON 格式的响应数据<span leaf="">
<span leaf=""> <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> data = <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">await<span leaf=""> response.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">json<span leaf="">();<span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 4. 将获取到的数据显示在结果区域<span leaf="">
<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">showResult<span leaf="">(data);<span leaf="">
<span leaf=""> <span leaf="">
<span leaf=""> } <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">catch<span leaf=""> (error) {<span leaf="">
<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// ...错误处理<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf="">}
讲解要点:
-
fetch(’/webhook/smart-assistant’, …):这是指令的核心。
-
第一个参数/webhook/smart-assistant 就是我们后端 API 工作流的 Webhook 路径。前端就是通过这个地址,精确地找到了我们的后端服务。
-
第二个参数{…} 是一个配置对象,定义了这个请求的一切细节。
-
method: ‘POST’:明确告诉后端,我们是来“提交”数据的,而不是简单地“获取”页面。这与后端 Webhook 节点上的设置完全对应。
-
headers: { ‘Content-Type’: ‘application/json’ }:这就像是写信时在信封上注明“内有文件”。它告诉后端服务器,我们信封(body)里的内容是 JSON 格式的,请用对应的方式解析。
-
body: JSON.stringify({ query: query }):这是请求的“货物”。我们把用户输入的文本 query 打包成一个 JavaScript 对象,然后用 JSON.stringify 将它转换成标准的 JSON 字符串,以便在网络中传输。
-
const data = await response.json():当后端处理完毕并返回结果后,我们用 .json() 方法来解析响应,将其从 JSON 字符串变回 JavaScript 对象,方便我们后续使用。
恭喜你!通过剖析这个完整的案例,你已经掌握了 n8n 全栈应用开发的精髓。我们从一个只能“展示”的 Hello World 页面,一跃构建了一个能够“思考”和“行动”的 AI 智能助理。
三、终极实战:用 Nano 构建 AI 知识卡片生成器
一)拥抱最前沿的 AI 技术
在之前的章节中,我们已经掌握了 n8n 全栈应用的开发模式,并构建了一个通用的 AI 智能助理。现在,让我们将目光投向更远方,探索如何将 n8n 与 AI 领域最新、最专业的模型相结合。
最近,Google 推出的 Nano 新一代生图模型,以其惊人的图像质量和效率,在技术圈引起了轰动。一个成熟的开发者,不仅要会使用通用工具,更要懂得如何利用这些“特种兵”模型来解决特定问题。
在本章,我们将挑战一个激动人心的任务:将前沿的 Nano 模型集成到 n8n 工作流中,打造一个垂直领域的、前后端一体化的“AI 教育知识卡片”生成器。这不仅是对我们已有技能的终极考验,更是你向 AI 应用开发前沿迈出的坚实一步。
📌
什么是 AI 知识卡片生成器?
想象一下,作为一名教师或内容创作者,你只需要输入一个关键词(如"AIGC”),选择目标受众(如"大学生"),指定视觉风格(如"涂鸦风格"),系统就能自动为你生成一套图文并茂的精美教学卡片。整个过程完全自动化,从概念理解到视觉设计,从内容生成到排版美化,一气呵成。
这就是我们要构建的 AI 知识卡片生成器的核心能力。
二)后端工作流深度剖析
与前面的案例不同,这次我们从后端开始,深入探索这个复杂系统的运作机理。
1、整体架构设计
我们的知识卡片生成器采用模块化设计,整个后端工作流可以分解为五个核心模块:
<span leaf="">用户输入 → 模块1:智能提示词工程 → 模块2:前沿AI图像生成 <span leaf="">
<span leaf=""> ↓<span leaf="">
<span leaf="">模块5:批量处理控制 ← 模块4:动态图像合成 ← 模块3:多模态AI协作
让我们逐一深入这些模块。
2、模块一:智能提示词工程
1)核心理念
在 AI 图像生成领域,提示词(Prompt)的质量直接决定了生成图像的效果。一个优秀的提示词工程系统,应该能够将用户的简单输入,转化为专业、详细、符合目标模型要求的生成指令。
2)技术实现
关键节点:生成生图提示词 (Google Gemini)
这个节点承担着整个系统的"大脑"角色。让我们看看它是如何工作的:
<span leaf="">I want you to help me generate image prompt <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">for<span leaf=""> teaching and instruction, the image style is {{ $json.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Style<span leaf=""> }},the topic is {{ $json.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Keywords<span leaf=""> }} , the count is {{ $json[<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'Count of cards'<span leaf="">] }},the audience is {{ $json.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Audience<span leaf=""> }}.<span leaf="">
<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Note<span leaf="">: <span leaf="">
<span leaf="">- ouput the prompt directly, <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">do<span leaf=""> not add any other explanation words.<span leaf="">
<span leaf="">- generate prompt must include keyword and images counts<span leaf="">
<span leaf="">
<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">For<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">example<span leaf="">:<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Generate<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">3<span leaf=""> doodle-style images to explain the concept <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">of<span leaf=""> <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">AIGC<span leaf=""> to junior students. <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">The<span leaf=""> images should have a consistent colorful, thick-pencil hand-drawn style, be rich <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">in<span leaf=""> information, feature <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">English<span leaf=""> text, use solid color backgrounds, have outlines around the cards, and include uniform titles, similar to a <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">PowerPoint<span leaf=""> presentation.<span leaf="">
工程要点:
- 1.参数动态注入:使用 n8n 的表达式语法 {{ $json.Style }},将前面节点处理好的标准化参数动态注入到提示词中。
- 2.示例引导:通过具体示例,引导 AI 理解我们期望的输出格式和质量标准。
- 3.约束明确:明确告知 AI 不要添加解释,直接输出可用的提示词,确保后续节点能够正确处理。
3)数据预处理
在进入提示词生成之前,我们需要一个关键的预处理步骤:
节点:规范化输入参数
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 核心代码片段<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">get<span leaf=""> = (<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">k, d<span leaf="">) => (body?.[k] !== <span style="color: #56b6c2;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">undefined<span leaf=""> ? body[k] : (src[k] !== <span style="color: #56b6c2;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">undefined<span leaf=""> ? src[k] : d));<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">sanitize<span leaf=""> = (<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">s<span leaf="">) => (s == <span style="color: #56b6c2;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">null<span leaf=""> ? <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">''<span leaf=""> : <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">String<span leaf="">(s)).<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">trim<span leaf="">();<span leaf="">
<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// count 限制 1~6,默认 3<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> rawCount = <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">get<span leaf="">(<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'count'<span leaf="">, <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">3<span leaf="">);<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> n = <span style="color: #e6c07b;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">parseInt<span leaf="">(rawCount, <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">10<span leaf="">);<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> count = <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Math<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">max<span leaf="">(<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">1<span leaf="">, <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Math<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">min<span leaf="">(<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">6<span leaf="">, <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Number<span leaf="">.<span style="color: #e6c07b;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">isFinite<span leaf="">(n) ? n : <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">3<span leaf="">));<span leaf="">
<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 供后续节点引用的"规范化字段"<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> normalized = {<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Keywords<span leaf="">: <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">sanitize<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">get<span leaf="">(<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'keywords'<span leaf="">, <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">''<span leaf="">)),<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Audience<span leaf="">: <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">sanitize<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">get<span leaf="">(<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'audience'<span leaf="">, <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">''<span leaf="">)),<span leaf="">
<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'Count of cards'<span leaf="">: count,<span leaf="">
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Style<span leaf="">: <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">sanitize<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">get<span leaf="">(<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'style'<span leaf="">, <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'Doodle-style'<span leaf="">)),<span leaf="">
<span leaf="">};
这个预处理步骤解决了什么问题?
- 数据清洗:去除空白字符,统一数据格式
- 参数验证:确保数量在合理范围内(1-6张)
- 兼容性处理:统一字段命名,方便后续节点引用
- 默认值处理:为缺失参数提供合理默认值
3、模块二:前沿 AI 模型集成
1)Google Nano 模型的优势
Google Nano 相比传统的图像生成模型,具有以下突出优势:
- 更高的图像质量:在细节表现和色彩还原上显著优于同类模型
- 更快的生成速度:优化的架构设计,生成时间大幅缩短
- 更好的指令理解:对复杂提示词的理解能力更强
- 更稳定的输出:生成结果的一致性和可预测性更好
2)技术实现细节
关键节点:调用Gemini生成图片
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// HTTP Request 节点配置<span leaf="">
<span leaf="">{<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">method<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"POST"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">authentication<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"genericCredentialType"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">genericAuthType<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"httpHeaderAuth"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">sendBody<span leaf="">: <span style="color: #56b6c2;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">true<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">specifyBody<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"json"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">jsonBody<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">`{<span leaf="">
<span leaf=""> "contents": [<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> "parts": [<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> "text": {{ $json.content.parts[0].text.toJsonString() }}<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> ]<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> ]<span leaf="">
<span leaf=""> }`<span leaf="">
<span leaf="">}
工程重点:
- 1.API 端点选择:使用 gemini-2.5-flash-image-preview 模型,这是专门优化过的图像生成版本。
- 2.认证处理:通过 httpHeaderAuth 方式传递 API 密钥,确保安全性。
- 3.错误处理:设置 onError: “continueErrorOutput”,让工作流能够优雅地处理 API 调用失败的情况。
3)图像数据提取
关键节点:从API响应提取图片
这个节点的代码展示了处理复杂 API 响应的专业技巧:
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 兼容四种常见路径,收集出当前 item 里的所有 data URL<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">function<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">collectUrls<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">j<span leaf="">) {<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 1) 自己构造的数组:images: [{type:'image_url', image_url:{url:'data:...'}}, ...]<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">if<span leaf=""> (<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Array<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">isArray<span leaf="">(j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">images<span leaf="">)) {<span leaf="">
<span leaf=""> <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">images<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">map<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">x<span leaf=""> =><span leaf=""> x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">image_url<span leaf="">?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf=""> ?? x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf="">).<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Boolean<span leaf="">);<span leaf="">
<span leaf=""> }<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 2) OpenRouter 风格:choices[0].message.images<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">if<span leaf=""> (<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Array<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">isArray<span leaf="">(j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">choices<span leaf="">?.[<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">0<span leaf="">]?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">message<span leaf="">?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">images<span leaf="">)) {<span leaf="">
<span leaf=""> <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">choices<span leaf="">[<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">0<span leaf="">].<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">message<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">images<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">map<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">x<span leaf=""> =><span leaf=""> x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">image_url<span leaf="">?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf=""> ?? x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf="">).<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Boolean<span leaf="">);<span leaf="">
<span leaf=""> }<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 3) content 混合数组:[{type:'text'}, {type:'image_url', image_url:{url:'data:...'}} ...]<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">if<span leaf=""> (<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Array<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">isArray<span leaf="">(j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">choices<span leaf="">?.[<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">0<span leaf="">]?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">message<span leaf="">?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">content<span leaf="">)) {<span leaf="">
<span leaf=""> <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">choices<span leaf="">[<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">0<span leaf="">].<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">message<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">content<span leaf="">
<span leaf=""> .<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">x<span leaf=""> =><span leaf=""> x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">type<span leaf=""> === <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'image_url'<span leaf="">)<span leaf="">
<span leaf=""> .<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">map<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">x<span leaf=""> =><span leaf=""> x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">image_url<span leaf="">?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf=""> ?? x?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">url<span leaf="">)<span leaf="">
<span leaf=""> .<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Boolean<span leaf="">);<span leaf="">
<span leaf=""> }<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 4) Gemini 风格: candidates[0].content.parts<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">if<span leaf=""> (<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Array<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">isArray<span leaf="">(j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">candidates<span leaf="">?.[<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">0<span leaf="">]?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">content<span leaf="">?.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">parts<span leaf="">)) {<span leaf="">
<span leaf=""> <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> j.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">candidates<span leaf="">[<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">0<span leaf="">].<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">content<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">parts<span leaf="">
<span leaf=""> .<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">p<span leaf=""> =><span leaf=""> p.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">inlineData<span leaf=""> && p.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">inlineData<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">data<span leaf="">)<span leaf="">
<span leaf=""> .<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">map<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">p<span leaf=""> =><span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">`data:<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">${p.inlineData.mimeType}<span leaf="">;base64,<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">${p.inlineData.data}<span leaf="">`<span leaf="">)<span leaf="">
<span leaf=""> .<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">Boolean<span leaf="">);<span leaf="">
<span leaf=""> }<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> [];<span leaf="">
<span leaf="">}
这个函数的精妙之处在于:
- 多格式兼容:支持不同 AI 服务商的响应格式
- 防御性编程:使用可选链操作符 ?. 避免访问不存在的属性
- 数据清洗:使用 filter(Boolean) 过滤掉空值
- 标准化输出:统一转换为 data URL 格式
4、模块三:多模态 AI 协作
1)什么是多模态 AI?
多模态 AI 是指能够理解和处理多种类型数据(文本、图像、音频等)的人工智能系统。在我们的知识卡片生成器中,我们创新性地使用了"看图说话"技术,让 AI 为生成的图像自动撰写描述文字。
2)视觉理解与文字生成
关键节点:生成图片中文描述
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">jsonBody<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">`{<span leaf="">
<span leaf=""> "contents": [<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> "parts": [<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> "text": "用中文描述这张图片的主要内容,针对{{ $('提取表单参数').item.json.audience }}受众。要求:1)总长度不超过100个字 2)每行不超过50个字,超过请换行 3)使用\\\\n换行符分隔 4)直接输出内容,无需解释"<span leaf="">
<span leaf=""> },<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> "inline_data": {<span leaf="">
<span leaf=""> "mime_type": "image/png",<span leaf="">
<span leaf=""> "data": "{{ $json.data }}"<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> ]<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> ],<span leaf="">
<span leaf=""> "generationConfig": {<span leaf="">
<span leaf=""> "responseMimeType": "application/json",<span leaf="">
<span leaf=""> "responseSchema": {<span leaf="">
<span leaf=""> "type": "OBJECT",<span leaf="">
<span leaf=""> "properties": {<span leaf="">
<span leaf=""> "storyboards": {<span leaf="">
<span leaf=""> "type": "ARRAY",<span leaf="">
<span leaf=""> "items": {<span leaf="">
<span leaf=""> "type": "OBJECT",<span leaf="">
<span leaf=""> "properties": { <span leaf="">
<span leaf=""> "chineseprompt": { "type": "STRING" } <span leaf="">
<span leaf=""> },<span leaf="">
<span leaf=""> "required": ["chineseprompt"],<span leaf="">
<span leaf=""> "propertyOrdering": ["chineseprompt"]<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> },<span leaf="">
<span leaf=""> "required": ["storyboards"],<span leaf="">
<span leaf=""> "propertyOrdering": ["storyboards"]<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf="">}`
技术亮点:
- 1.结构化输出:使用 responseSchema 确保 AI 返回结构化的 JSON 数据,而不是自由文本。
- 2.受众适配:通过引用前面节点的受众信息,让描述文字符合目标群体的理解水平。
- 3.格式控制:明确规定字数限制和换行规则,确保生成的文字能够很好地适配卡片布局。
- 4.多模态输入:同时传递文字指令和图像数据,实现真正的多模态处理。
5、模块四:动态图像合成
1)图像处理管道
知识卡片的最终呈现需要将 AI 生成的图像与 AI 生成的文字有机结合。这个过程涉及多个图像处理步骤:
步骤1:创建画布背景
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 创建卡片背景 节点配置<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operation<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"multiStep"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">dataPropertyName<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"bottom"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operations<span leaf="">: {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operations<span leaf="">: [<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operation<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"create"<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">backgroundColor<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"<a class="wx_topic_link" topic-id="mg0iihe5-4lb33q" style="color: #576B95 !important;" data-topic="1" href="javascript:;">#f5f5f5</a>"<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">width<span leaf="">: <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">1024<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">height<span leaf="">: <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">1366<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 3:4 比例,适合移动端显示<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> ]<span leaf="">
<span leaf="">}
步骤2:合成原始图像
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 添加描述文字到卡片 节点配置<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operations<span leaf="">: {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operations<span leaf="">: [<span leaf="">
<span leaf=""> {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operation<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"composite"<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">dataPropertyNameComposite<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"data"<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">positionY<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"=-{{ $binary[\"data\"].data.height }}"<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 动态计算位置<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf=""> ]<span leaf="">
<span leaf="">}
步骤3:添加文字描述
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 图像:添加描述文字到卡片 节点配置<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">operation<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"text"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">dataPropertyName<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"bottom"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">text<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"={{ $('提取图片描述文本').first().json[\"图片提示词\"].storyboards[0].chineseprompt }}"<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">fontSize<span leaf="">: <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">36<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">positionX<span leaf="">: <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">20<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">positionY<span leaf="">: <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">1104<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">lineLength<span leaf="">: <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">25<span leaf="">,<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">options<span leaf="">: {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">font<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">"=/usr/share/fonts/chinese/chinese.msyh.ttf"<span leaf=""> <span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 中文字体支持<span leaf="">
<span leaf="">}
2)技术挑战与解决方案
① 中文字体处理
📌
挑战:n8n 默认不支持中文字体渲染
中文下载:https://font.download/font/microsoft-yahei
首先在宿主机准备字体文件,常用的中文字体:
<span leaf="">/host/fonts/<span leaf="">
<span leaf="">├── NotoSansCJK-Regular.ttc<span leaf="">
<span leaf="">├── SimHei.ttf<span leaf="">
<span leaf="">├── Microsoft-YaHei.ttf<span leaf="">
<span leaf="">└── SourceHanSans-Regular.otf
Docker 启动时挂载字体目录
<span leaf="">docker run -d \<span leaf="">
<span leaf=""> -v /host/fonts:/usr/share/fonts/chinese \<span leaf="">
<span leaf=""> -v /host/fonts:/container/fonts \<span leaf="">
<span leaf=""> your-container-name
在节点中指定字体路径,Font Name or ID 中可以使用
这个方案应该能完美解决你的中文显示问题!你准备使用哪种中文字体?
-
挑战:不同图像的尺寸可能不同
-
解决:使用表达式动态计算位置 positionY: “=-{{ $binary[\“data\”].data.height }}”
-
挑战:确保文字不会超出卡片边界
-
解决:设置 lineLength: 25 控制每行字数
6、模块五:批量处理与工作流控制
1)循环处理机制
由于用户可能需要生成多张卡片,我们需要一个能够批量处理的机制:
关键节点:逐一处理图片 (Split in Batches)
这个节点的作用是将多张图片分解为单个图片,然后分别进入图像处理管道。每张图片都会经过:
<span leaf="">单张图片 → 转Base64 → AI描述生成 → 文字提取 → 画布创建 → 图像合成 → 文字添加 → 回到循环控制
2)数据流合并
关键节点:收集所有成品卡片
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 筛选出包含bottom binary数据的items<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> bottomImages = $input.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">all<span leaf="">().<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">filter<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">item<span leaf=""> =><span leaf=""> {<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> item.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">binary<span leaf=""> && item.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">binary<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">bottom<span leaf="">;<span leaf="">
<span leaf="">});<span leaf="">
<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 转换为前端期望的JSON格式<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> jsonResults = bottomImages.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">map<span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">(<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">item, idx<span leaf="">) =><span leaf=""> {<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> binary = item.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">binary<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">bottom<span leaf="">;<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> base64Data = binary.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">data<span leaf="">.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">toString<span leaf="">(<span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'base64'<span leaf="">);<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">const<span leaf=""> fileName = binary.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">fileName<span leaf=""> || <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">`image_<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">${idx + <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">1<span leaf="">}<span leaf="">.png`<span leaf="">;<span leaf="">
<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">data_url<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">`data:<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">${binary.mimeType}<span leaf="">;base64,<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">${base64Data}<span leaf="">`<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">output_filename<span leaf="">: fileName,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">mime<span leaf="">: binary.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">mimeType<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">chunks<span leaf="">: [base64Data],<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">index<span leaf="">: idx,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">total<span leaf="">: bottomImages.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">length<span leaf="">
<span leaf=""> };<span leaf="">
<span leaf="">});<span leaf="">
<span leaf="">
<span style="color: #5c6370;font-style: italic;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">// 返回包含final_results的对象,供webhook返回<span leaf="">
<span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">return<span leaf=""> [{<span leaf="">
<span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">json<span leaf="">: {<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">final_results<span leaf="">: jsonResults,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">total_count<span leaf="">: bottomImages.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">length<span leaf="">,<span leaf="">
<span leaf=""> <span style="color: #d19a66;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">status<span leaf="">: <span style="color: #98c379;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">'completed'<span leaf="">
<span leaf=""> }<span leaf="">
<span leaf="">}];
这个收集节点的精妙设计:
- 1.智能筛选:只收集处理完成的图片(含有 bottom binary 数据)
- 2.格式标准化:转换为前端期望的统一格式
- 3.元数据完整:包含文件名、索引、总数等完整信息
- 4.状态管理:返回处理状态信息
三)API 密钥管理最佳实践
在处理多个 AI 服务时,密钥管理是一个重要话题:
安全建议:
- 永远不要在代码中硬编码 API 密钥
- 使用 n8n 的凭据管理系统
- 定期轮换密钥
- 监控 API 使用量,设置预算警报
四)最佳实践总结
通过构建这个 AI 知识卡片生成器,我们掌握了以下核心能力:
1、技术能力
- 1.前沿 AI 模型集成:学会了如何在 n8n 中调用和管理最新的 AI 服务
- 2.多模态 AI 协作:实现了图像生成与文字描述的智能协作
- 3.复杂数据流管理:掌握了批量处理、数据合并等高级技巧
- 4.图像处理自动化:学会了在 n8n 中进行复杂的图像编辑操作
2、工程能力
- 1.系统架构设计:模块化设计思想,清晰的数据流向
- 2.错误处理机制:完整的容错和恢复策略
- 3.性能优化:合理的并发控制和资源管理
3、业务理解
- 1.用户需求分析:深入理解教育内容创作的痛点
- 2.产品功能设计:从用户体验出发的功能规划
- 3.商业价值创造:技术如何转化为实际的业务价值
这个案例不仅展示了 n8n 的强大能力,更重要的是展现了如何将前沿技术转化为实用的解决方案。在 AI 时代,掌握这样的全栈开发能力,将让你在激烈的技术竞争中占据有利位置。
四、总结:为什么 n8n 是 POC 神器?
在课程的最后,回到开篇提出的问题。
- 速度与敏捷:我们没有编写太多传统的后端代码,没有部署服务器,没有配置复杂的开发环境。只通过在浏览器中拖拽节点和编写少量前端代码,就在极短的时间内完成了一个包含前后端、多次 AI 调用、复杂图像处理的全功能应用。
- 全栈能力:我们证明了 n8n 不仅仅是 API 的“胶水”。它能担任 Web 服务器,也能构建复杂的后端逻辑,是名副其实的全栈开发平台。
- 快速验证:现在,你可以把这个应用的 URL 直接发给你的潜在用户或老板,让他们立即体验核心功能,并收集反馈。从一个想法到一个可交互的原型,我们只用了一个 n8n 工作流的时间。
这就是 n8n 的力量:它将产品创新的周期从“月”压缩到“天”,让你能以最快的速度抓住市场的脉搏。
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/ai001/post/20251010/n8n-%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91%E4%BB%8E%E8%87%AA%E5%8A%A8%E5%8C%96%E5%B7%A5%E5%85%B7%E5%88%B0%E5%85%A8%E5%8A%9F%E8%83%BD%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E5%B9%B3%E5%8F%B0/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com