创业者最怕什么?不是没有好想法,而是想法还在 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. 1.在 n8n 画布中,添加一个新的 Webhook 节点。

Image

  1. 2.将 HTTP Method 设置为 GET。
  2. 3.设置响应模式:找到 Respond 选项,确保其设置为 Using ‘Respond to Webhook’ Node。

💡

说明:这个设置至关重要。它告诉触发器节点:请不要立即或在工作流结束时自动回复,而是等待一个专门的‘Respond to Webhook’节点来构造和发送响应。

这种模式给了我们最大的灵活性,让我们可以在工作流的任何位置、任何时机向前端返回内容,是构建复杂应用的基础。

Image

  1. 4.复制 Production URL,稍后我们将在浏览器中访问它。

2、第二步:配置 Respond to Webhook 节点

  1. 1.添加一个 Respond to Webhook 节点,并将其连接到 Webhook 节点之后。

Image

Image

  1. 2.在节点配置中,找到 Respond With 选项,将其从默认的 First Incoming Item 修改为 Text。

Image

  1. 3.在下方的 Response Body 输入框中,粘贴以下基础的 HTML 代码:

    Image

    <span style="color: #61aeee;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;!DOCTYPE <span style="color: #c678dd;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">html<span leaf="">&gt;<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. 1.在 Respond to Webhook 节点配置中,点击底部的 Options 展开选项,然后选择 Add Option -> Response Headers。

Image

  1. 2.在出现的 Headers 区域,点击 Add Response Header。

Image

  1. 3.在 Name 字段中输入 Content-Type。在 Value 字段中输入 html。

Image

完成以上配置后,激活你的工作流。现在,在浏览器中访问你第一步复制的 Webhook URL,你将看到一个由 n8n 驱动的、活生生的网页!

Image

通过这简单的三步,我们已经成功地让 n8n 扮演了一个 Web 服务器的角色。这为我们后续在 n8n 中构建更复杂的前端应用打下了坚实的基础。

图片

二、让你的网页“活”起来 - 前后端通信

图片

图片

在上一节,我们已经成功迈出了第一步:让 n8n 充当 Web 服务器,返回了一个精美的 “Hello, World!” 静态页面。这是一个了不起的开始,但一个静态页面无法与用户交互,它的内容是固定的。

那么,我们如何才能更进一步,构建一个能接收用户输入、进行复杂处理、并动态返回结果的真实应用呢?

答案是:前后端通信。这正是我们将 n8n 从一个页面展示工具,升级为全功能应用开发平台的关键。为了让这个过程更有趣、更贴近真实场景,我们将构建一个AI 智能生活助理。

📌

教学目标:  通过从零开始剖析这个“智能生活助理”工作流,你将掌握在 n8n 中构建全栈应用的核心模式:使用一个工作流提供前端界面(UI),界面上的 JavaScript 再调用另一个工作流作为后端应用程序接口(API),后端 API 甚至可以集成强大的 AI Agent 来处理复杂任务。

Image

一)设计思路:分工明确的前后端

和专业的 Web 开发一样,我们将任务拆分为“前端”和“后端”两个部分,它们都由 n8n 的工作流来承载:

  1. 1.前端界面工作流 (GET Webhook):

    Image

  • 职责:它的任务非常纯粹,就是当用户通过浏览器访问时,提供一个美观、易用的 HTML 操作界面。
  • 实现:由 触发器:提供UI界面 节点(设置为 GET 请求)和一个包含完整 HTML/CSS/JS 代码的 响应:返回UI界面HTML 节点构成。
  1. 2.后端 API 工作流 (POST Webhook):
  • 职责:这是应用的“大脑”和“动力核心”。它作为一个 API 接口,负责接收前端发送的用户问题,利用 AI Agent 进行智能分析和处理,调用高德地图等外部工具获取信息,最后将处理好的结果返回给前端。
  • 实现: 由 触发器:接收生成指令 节点(设置为 POST 请求)触发,后续连接着一整套复杂的 AI Agent 逻辑。

Image

这种模式完美复刻了现代 Web 应用的开发思想,只不过,我们的前端和后端都运行在 n8n 里!

二)实战:搭建你的智能生活助理应用

现在,让我们深入这份工作流,一步步揭示这个智能应用是如何构建的。

1、剖析前端界面 (GET 工作流)

这个工作流为我们的应用提供了“面子”。

  • UI 界面:响应:返回UI界面HTML 节点中的代码构建了一个非常完善的用户界面,包括一个用于自然语言输入的大文本框、一些方便操作的快捷示例标签,以及一个用于动态展示结果的区域。
  • 核心交互 (JavaScript): 在  标签内,handleQuery 函数是关键。当用户点击“开始查询”按钮时,它会:
  1. a.获取输入框(queryInput)中的文本内容。
  2. b.调用 fetch 函数,向后端的 API 地址 (/webhook/smart-assistant) 发送一个 POST 请求。
  3. c.请求的 body 是一个 JSON 对象,格式为 { “query”: “用户输入的内容” }。
  4. d.等待后端返回结果,并将其动态地渲染到结果区域(resultDisplay)中。

2、探秘智能后端 (POST 工作流)

如果说前端是应用的“面子”,那么这个由 AI Agent 驱动的后端就是应用的“里子”。

  1. 1.接收指令:触发器:接收生成指令 节点准确地接收到前端发来的 POST 请求和包含用户问题的 JSON 数据。
  2. 2.组装 AI Agent: 这个应用的核心是 AI Agent。你可以把它理解为一个拥有“大脑”和“工具”的智能机器人。
  • 大脑 (LLM):DeepSeek Chat Model 节点扮演着“大脑”的角色,它赋予了 Agent 理解、推理和生成语言的能力。

  • 工具 (Tools):HTTP Streamable 节点被配置成了一个高德地图的工具,它像是 Agent 的“手臂”,让 Agent 有能力去获取现实世界的地理和天气信息。

    💡

    注意: 该节点需要你提前在 n8n 的环境变量中配置好名为 AMAP_API_KEY 的高德 API 密钥。

  • 中枢 (Agent 节点):智能助理核心 节点是 Agent 的灵魂。它将“大脑”和“工具”连接在一起,并根据我们为它设定的行动纲领 (System Prompt) 来工作。

  1. 3.返回结果

Agent 完成思考和工具调用后,会生成一段完美的答复。返回最终JSON结果 节点会将这段答复包装成 JSON 格式,响应给正在等待的前端页面。

Image

3、完整流程回顾

现在,让我们把前后端串起来,完整地看一遍用户提问“明天北京天气如何?”时发生了什么:

  1. 1.用户在浏览器中输入问题,点击查询。

Image

  1. 2.前端 JavaScript 将 {“query”: “明天北京天气如何?"} 发送到后端 API。

Image

  1. 3.后端 Agent 的“大脑”分析问题,根据 System Prompt 里的规则,识别出这是一个天气查询任务,决定使用“高德地图工具”。

Image

  1. 4.Agent 调用工具,传入“北京” 等参数,工具返回了实时的天气数据(如:晴,15-25度)。

Image

  1. 5.Agent 的“大脑”再次工作,将这些枯燥的数据,按照 System Prompt 规定的模板,润色成一段生动、友好的天气预报。

Image

  1. 6.后端将这段生成好的文本返回给前端。

Image

  1. 7.前端 JavaScript 接收到结果,并将其显示在页面上。

Image

三)关键代码深度解读:连接前后端的“魔法”

我们已经了解了智能助理应用的整体架构。现在,让我们聚焦于最核心的部分:前端界面是如何将用户的问题,准确无误地发送给后端 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="">&lt;<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="">&gt;<span leaf="">  
<span leaf="">  <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;<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="">&gt;<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="">&lt;<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="">    &gt;<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">textarea<span leaf="">&gt;<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="">&lt;<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="">&gt;<span leaf="">  
<span leaf="">      <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;<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="">&gt;<span leaf="">🔍 开始查询<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">span<span leaf="">&gt;<span leaf="">  
<span leaf="">      <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;<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="">&gt;<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">&gt;<span leaf="">  
<span leaf="">    <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">button<span leaf="">&gt;<span leaf="">  
<span leaf="">    <span leaf="">  
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">form<span leaf="">&gt;<span leaf="">  
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">&gt;<span leaf="">  
<span leaf="">  
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;<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="">&gt;<span leaf="">  
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;<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="">&gt;<span leaf="">  
<span leaf="">    <span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;<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="">&gt;<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">&gt;<span leaf="">  
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">&gt;<span leaf="">  
<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">&lt;/<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">div<span leaf="">&gt;
  • 讲解要点:

  • 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="">&lt;<span style="color: #e06c75;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">script<span leaf="">&gt;<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 知识卡片生成器

图片

图片

Image

Image

Image

一)拥抱最前沿的 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、模块一:智能提示词工程

Image

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. 1.参数动态注入:使用 n8n 的表达式语法 {{ $json.Style }},将前面节点处理好的标准化参数动态注入到提示词中。
  2. 2.示例引导:通过具体示例,引导 AI 理解我们期望的输出格式和质量标准。
  3. 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="">) =&gt; (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="">) =&gt; (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 模型集成

Image

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. 1.API 端点选择:使用 gemini-2.5-flash-image-preview 模型,这是专门优化过的图像生成版本。
  2. 2.认证处理:通过 httpHeaderAuth 方式传递 API 密钥,确保安全性。
  3. 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=""> =&gt;<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=""> =&gt;<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=""> =&gt;<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=""> =&gt;<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=""> =&gt;<span leaf=""> p.<span style="color: inherit;background: inherit;font-family: inherit;font-size: inherit;line-height: inherit;"><span leaf="">inlineData<span leaf=""> &amp;&amp; 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=""> =&gt;<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. 1.结构化输出:使用 responseSchema 确保 AI 返回结构化的 JSON 数据,而不是自由文本。
  2. 2.受众适配:通过引用前面节点的受众信息,让描述文字符合目标群体的理解水平。
  3. 3.格式控制:明确规定字数限制和换行规则,确保生成的文字能够很好地适配卡片布局。
  4. 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 中可以使用

Image

这个方案应该能完美解决你的中文显示问题!你准备使用哪种中文字体?

  • 挑战:不同图像的尺寸可能不同

  • 解决:使用表达式动态计算位置 positionY: “=-{{ $binary[\“data\”].data.height }}”

  • 挑战:确保文字不会超出卡片边界

  • 解决:设置 lineLength: 25 控制每行字数

6、模块五:批量处理与工作流控制

1)循环处理机制

由于用户可能需要生成多张卡片,我们需要一个能够批量处理的机制:

关键节点:逐一处理图片 (Split in Batches)

Image

这个节点的作用是将多张图片分解为单个图片,然后分别进入图像处理管道。每张图片都会经过:

<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=""> =&gt;<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=""> &amp;&amp; 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="">) =&gt;<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. 1.智能筛选:只收集处理完成的图片(含有 bottom binary 数据)
  2. 2.格式标准化:转换为前端期望的统一格式
  3. 3.元数据完整:包含文件名、索引、总数等完整信息
  4. 4.状态管理:返回处理状态信息

三)API 密钥管理最佳实践

在处理多个 AI 服务时,密钥管理是一个重要话题:

Image

安全建议:

  • 永远不要在代码中硬编码 API 密钥
  • 使用 n8n 的凭据管理系统
  • 定期轮换密钥
  • 监控 API 使用量,设置预算警报

四)最佳实践总结

通过构建这个 AI 知识卡片生成器,我们掌握了以下核心能力:

1、技术能力

  1. 1.前沿 AI 模型集成:学会了如何在 n8n 中调用和管理最新的 AI 服务
  2. 2.多模态 AI 协作:实现了图像生成与文字描述的智能协作
  3. 3.复杂数据流管理:掌握了批量处理、数据合并等高级技巧
  4. 4.图像处理自动化:学会了在 n8n 中进行复杂的图像编辑操作

2、工程能力

  1. 1.系统架构设计:模块化设计思想,清晰的数据流向
  2. 2.错误处理机制:完整的容错和恢复策略
  3. 3.性能优化:合理的并发控制和资源管理

3、业务理解

  1. 1.用户需求分析:深入理解教育内容创作的痛点
  2. 2.产品功能设计:从用户体验出发的功能规划
  3. 3.商业价值创造:技术如何转化为实际的业务价值

这个案例不仅展示了 n8n 的强大能力,更重要的是展现了如何将前沿技术转化为实用的解决方案。在 AI 时代,掌握这样的全栈开发能力,将让你在激烈的技术竞争中占据有利位置。

图片

四、总结:为什么 n8n 是 POC 神器?

图片

图片

在课程的最后,回到开篇提出的问题。

  • 速度与敏捷:我们没有编写太多传统的后端代码,没有部署服务器,没有配置复杂的开发环境。只通过在浏览器中拖拽节点和编写少量前端代码,就在极短的时间内完成了一个包含前后端、多次 AI 调用、复杂图像处理的全功能应用。
  • 全栈能力:我们证明了 n8n 不仅仅是 API 的“胶水”。它能担任 Web 服务器,也能构建复杂的后端逻辑,是名副其实的全栈开发平台。
  • 快速验证:现在,你可以把这个应用的 URL 直接发给你的潜在用户或老板,让他们立即体验核心功能,并收集反馈。从一个想法到一个可交互的原型,我们只用了一个 n8n 工作流的时间。

这就是 n8n 的力量:它将产品创新的周期从“月”压缩到“天”,让你能以最快的速度抓住市场的脉搏。