在自动化工作流程中,n8n和Notion的集成是一种常见需求。然而,当处理较长文本内容时(如AI生成的文章、报告或内容),这个集成过程中会遇到一些技术挑战。本文记录了我在尝试将AI生成的长文本内容通过n8n发送到Notion时遇到的问题和探索的解决方案。
问题背景
在我的工作流中,我使用了以下组件:
- AI Agent节点:生成较长的文本内容(如求职信)
- Notion节点:尝试将生成的内容保存到Notion数据库中
主要挑战出现在Notion集成部分。Notion API对单个文本块的内容长度有限制(2000字符),而AI生成的内容往往超过这个限制。错误提示为:
Bad request - please check your parameters body failed validation: body.children[0].paragraph.rich_text[0].text.content.length should be ≤ 2000, instead was 3728
尝试的解决方案
1. 文本分段处理
我首先尝试在AI Agent和Notion节点之间添加Function/Code节点,将长文本分割成短段落:
// 获取AI Agent的输出文本 const fullText = items[0].json.output; // 按自然段落分割文本 const paragraphs = fullText.split("\n\n"); // 创建多个输出项,每项对应一个Notion段落 const returnItems = []; paragraphs.forEach((paragraph, index) => { if (paragraph.trim().length > 0) { // 确保段落不超过2000字符 if (paragraph.length > 2000) { // 如果超过,进一步分割 let startIdx = 0; while (startIdx < paragraph.length) { const chunk = paragraph.substring(startIdx, Math.min(startIdx + 1900, paragraph.length)); returnItems.push({ json: { index: returnItems.length, content: chunk, type: "paragraph" } }); startIdx += 1900; } } else { // 如果段落长度合适,直接添加 returnItems.push({ json: { index: index, content: paragraph, type: "paragraph" } }); } } }); return returnItems;
这个方法成功地将文本分成了多个段落块,但接下来的问题是如何将这些块正确地传递给Notion。
2. 尝试创建页面并添加块
理论上,正确的工作流应该是:
- 先创建Notion页面(使用第一段作为标题)
- 然后使用"Append Block"功能将其余段落添加到该页面
这个过程中,我尝试了多种方法并遇到了一系列技术挑战:
方案一:数组中的内容对象
我最初的数据格式是一个包含多个对象的数组,每个对象有
index
、content
和type
属性:[ { "index": 0, "content": "**xxx**", "type": "paragraph" }, { "index": 1, "content": "**abc** ", "type": "paragraph" }, // ...更多对象 ]
我尝试使用表达式
{{ $('Code3').item.json.map(item => item.content) }}
来提取所有内容,但收到错误:body failed validation: body.children[0].paragraph.rich_text[0].text.content should be defined, instead was `undefined`.
问题分析:表达式返回一个数组,而Notion API期望一个字符串作为
content
值。方案二:预格式化的Notion块对象
我尝试了另一种数据格式,直接预先构建符合Notion API格式的块对象:
[ { "title": "** wenjiaqi.top \nMunich, August 2024", "blocks": [ { "type": "paragraph", "paragraph": { "rich_text": [ { "type": "text", "text": { "content": "**wenjiaqi.top \nMunich, August 2024 " } } ] } }, // ...更多块 ] } ]
使用表达式
{{ $('Code2').first().json.blocks }}
尝试直接传递预格式化的块,但收到错误:body failed validation: body.children[0].paragraph.rich_text[0].text.content should be a string, instead was [{"type":"paragraph","paragraph":{"rich_text":[{"type":....
问题分析:虽然
blocks
数组包含了正确格式的Notion块,但Notion API期望这些块包含在一个children
属性中。所以我尝试在notion模块之前加上code,以变成指定格式,但仍不行。
方法3: 使用HTTP Request节点直接调用Notion API
由于Notion节点限制,我转向使用HTTP Request节点直接调用Notion API:
- 首先创建页面并获取页面ID
- 然后使用另一个HTTP请求添加块
尝试的请求格式:
URL: https://api.notion.com/v1/blocks/[页面ID]/children 方法: POST 头部: { "Notion-Version": "2022-06-28", "Content-Type": "application/json", "Authorization": "Bearer [token]" } 请求体: { "children": [块数组] }
遇到的问题:
- URL格式错误:必须使用正确的页面ID,而不是页面名称或URL
- 认证问题:需要设置正确的Notion API令牌
- JSON引用问题:难以在HTTP节点中正确引用复杂的JSON结构
- 收到错误
Cannot POST /[页面名称]
,表明URL格式不正确
方法4: 使用多个节点循环处理每个段落
我尝试了一种逐段添加的方法:
- 修改Function/Code节点,让其输出多个项而非一个数组:
// 返回多个项而非单个数组 return paragraphs.map(para => ({ json: { content: para } }));
- 设置一个IF节点,区分第一段(用于创建页面)和其他段落(用于添加块)
- 对于每个段落项,分别通过Notion节点处理
但最终结果是创建了多个独立页面,而不是将多个段落添加到同一页面中。这看起来是n8n处理数组输出的方式导致的问题,每个数组项都被视为独立的工作流执行实例。
实际结果与发现
经过多次尝试,我未能成功实现将AI生成的长文本作为多个块添加到单个Notion页面的功能。下面是详细的问题分析:
1. n8n与Notion节点的设计限制
Create Page功能的问题
- 大型JSON处理限制:当尝试将整个分段后的内容数组传递给Notion节点时,n8n无法正确处理这种复杂结构。
- 错误处理不明确:节点返回模糊的错误消息,如"Bad request - please check your parameters",难以诊断具体问题。
- 文档不足:n8n对于如何正确格式化Notion块的指导不足,特别是对于复杂的分段内容。
Append Block功能的行为异常
- 数组处理逻辑:当Function/Code节点返回数组项时,n8n将每个项视为独立的工作流执行,导致创建多个Notion页面而非在一个页面中添加多个块。
- 设计缺陷:这可能是一个未实现的功能或设计缺陷。理论上,应该支持在单个页面中添加多个块,但实际行为与预期不符。
- 节点配置限制:在UI中,无法像某些其他集成那样配置"集合模式"(批量处理vs单项处理)。
2. 表达式引用限制
复杂JSON结构的引用问题
- 路径解析错误:尝试引用复杂嵌套JSON结构(如
notionBlocks
数组)时,n8n经常无法正确解析。
- 类型不匹配:当成功引用结构时,往往是类型不匹配,例如将整个JSON对象传递给期望字符串的字段。
- 实际表现:表达式如
{{ $('Code2').json.blocks }}
返回undefined或格式错误。
数组引用的特殊挑战
- 索引访问不可靠:使用
{{ $node["Code"].json.notionBlocks[0] }}
等表达式时,索引访问有时无法正确工作。
- 无数组循环机制:在Notion节点中,没有内置机制循环处理数组中的每个项目并将它们作为单一请求的一部分。
3. Notion API格式要求严格
API调用格式问题
- 格式精确匹配要求:Notion API要求极其精确的JSON结构,任何轻微偏差都会导致错误。
- 嵌套结构复杂:块格式包含多层嵌套(children → block → paragraph → rich_text → text → content),在n8n表达式中难以正确构建。
HTTP请求实现的困难
- URL格式混淆:尝试HTTP Request节点时,API URL格式容易混淆(页面ID vs 页面名称/URL)。
- 认证复杂性:需要正确设置Bearer令牌和Notion-Version头部,增加了配置复杂性。
- 调试困难:API错误消息不总是清晰指出问题所在,如收到"The resource you are requesting could not be found"而非具体指出URL格式错误。
4. 工作流设计挑战
数据流管理问题
- 状态保持困难:在多步骤工作流中,保持页面ID等状态信息变得复杂。
- 错误处理有限:n8n提供的错误处理机制在处理复杂的API集成时不够灵活。
测试和调试限制
- 节点输入/输出检查有限:难以详细检查节点间传递的完整数据结构。
- 表达式调试工具有限:缺乏强大的表达式调试工具,增加了排查表达式问题的难度。
突破性解决方案:利用n8n的.all()方法
经过多次尝试和调试,我最终发现了一个有效的解决方案,关键在于正确理解n8n的数据访问方法。
突破点:使用.all()方法访问完整数据
在Code4节点中,我尝试了这个方法,它成功了:
return $('Code3').all().map(item => ({ json: { content: item.json.content, type: item.json.type, index: item.json.index } }));
虽然输出看起来与之前尝试的方法几乎一样(只是key的顺序不同),但这个方法成功地将所有段落添加到了Notion中。
n8n中不同数据访问方法的关键差异
理解下面这些数据访问方法的差异是解决问题的关键:
访问方法 | 说明 | 返回内容 |
$('NodeName').item | 访问当前处理的单个项 | 当前正在处理的项对象 |
$('NodeName').json | 访问当前处理项的json属性 | 当前项的json数据 |
$('NodeName').item.json | 等同于.json | 当前项的json数据 |
$('NodeName').all() | 获取所有项的完整数组 | 包含节点所有输出项的数组 |
$('NodeName').first() | 获取第一个项 | 节点的第一个输出项 |
$('NodeName').last() | 获取最后一个项 | 节点的最后一个输出项 |
为什么.all()方法有效?
- 完整数据访问:
.all()
方法返回节点的所有输出项作为一个数组,而不仅仅是当前正在处理的项。
- 突破执行上下文限制:在默认情况下,
$('NodeName')
不带任何方法调用时只引用当前正在处理的项,而.all()
突破了这个限制,无论节点的执行模式如何,都会返回完整的数据集。
- 正确的数据结构处理:通过
.map()
处理.all()
返回的数组,我们能够为每个项创建正确格式的输出,保留n8n期望的数据结构。
看似相同实则不同的数据结构
虽然不同方法返回的JSON表示可能看起来相似,但内部的JavaScript对象结构可能有很大不同:
- 引用vs值:直接访问可能返回引用,而
.all()
总是返回一个新的数组副本。
- 内部元数据:n8n的数据项可能包含额外的内部元数据,虽然这些在JSON表示中不可见。
- 原型链差异:不同访问方法返回的对象可能有不同的原型链,影响它们的行为。
实际应用分析
在我的Notion集成中:
- 通过
.all()
访问所有段落数据
- 使用
.map()
为每个段落创建正确的格式
- 返回格式化后的数组,让n8n正确处理这些项
完整的正确代码:
// 获取所有段落数据并正确格式化 return $('Code3').all().map(item => ({ json: { content: item.json.content, type: item.json.type, index: item.json.index } }));
n8n数据访问的最佳实践
基于这次经验,以下是一些处理n8n数据的关键最佳实践:
1. 根据需求选择正确的访问方法
- 单项处理:使用
.item
或默认引用(无方法)
- 所有项处理:使用
.all()
- 特定项处理:使用
.first()
、.last()
或.itemAt(index)
2. 理解执行上下文的影响
- "Run Once"模式:适合处理整个数组,但默认只访问第一个项
- "Run Once for Each Item"模式:为每个项单独执行,但无法方便地访问其他项
- 使用
.all()
可以在任何模式下访问完整数据,突破这些限制
3. 保持正确的输出结构
- 确保始终返回包含
json
属性的对象或对象数组
- 使用
.map()
而不是.forEach()
处理数组,以保留结构
- 避免嵌套过深的复杂对象,可能导致引用问题
4. 有效的调试方法
// 检查节点输出类型和结构 const nodeOutput = $('NodeName'); console.log('输出类型:', typeof nodeOutput); console.log('是数组?', Array.isArray(nodeOutput)); console.log('有.all方法?', typeof nodeOutput.all === 'function'); console.log('示例数据:', JSON.stringify(nodeOutput).slice(0, 200)); // 比较不同访问方法 const directAccess = $('NodeName').json; const allMethod = $('NodeName').all()[0]?.json; console.log('直接访问vs .all():', JSON.stringify(directAccess) === JSON.stringify(allMethod) ? '相同' : '不同');
结论与经验总结
这次深入排查n8n和Notion集成问题的过程,揭示了n8n数据处理的一些关键细节,特别是不同数据访问方法之间微妙但重要的差异。
关键经验:
- 深入理解工具特性:n8n的数据访问机制(特别是
.all()
方法)是突破复杂集成瓶颈的关键。
- 看似微小的差异可能至关重要:看似相似的JSON输出可能隐藏着底层数据结构的重要差异。
- 系统性调试:使用
console.log
探索数据结构是解决复杂问题的有力工具。
- 耐心与坚持:有时解决方案可能出人意料地简单,但需要多次尝试才能发现。
通过这种方法,我成功实现了将AI生成的长文本内容分段添加到Notion的功能,避开了各种API限制和数据结构复杂性。希望这些经验能帮助其他在n8n和API集成中遇到类似挑战的开发者。
有用的表达式技巧
在处理n8n表达式时,以下是一些处理空值的有用技巧:
// 使用逻辑或处理空值 {{ $input.item.json.title || "" }} // 使用空值合并运算符 {{ $input.item.json.title ?? "" }} // 使用三元运算符 {{ $input.item.json.title ? $input.item.json.title : "" }} // 处理可能的非字符串值 {{ typeof $input.item.json.title === 'object' ? JSON.stringify($input.item.json.title) : ($input.item.json.title || "") }} // 安全访问深层嵌套属性 {{ $input?.item?.json?.complex?.nested?.property || "默认值" }} // 安全地访问数组 {{ Array.isArray($input.item.json.items) ? $input.item.json.items[0] : null }} // 结合.all()方法和表达式 {{ $('NodeName').all().filter(item => item.json.type === "important").map(item => item.json.content).join(", ") }}
这些表达式在处理动态数据时特别有用,可以避免因空值或意外数据类型导致的错误,同时结合
.all()
等方法可以实现更复杂的数据处理。