n8n 中二进制数据丢失的修复指南:用 Merge 节点与 n8n-mcp 在 JSON 变换后重新挂载 $binary
【免费下载链接】n8n-mcpA MCP for Claude Desktop / Claude Code / Windsurf / Cursor to build n8n workflows for you项目地址: https://gitcode.com/GitHub_Trending/n8/n8n-mcp
导读
在 n8n 工作流中,一个最隐蔽的故障模式是:数据项同时携带json与binary两个槽位,经过只处理 JSON 的节点(Edit Fields、Code、IF)后,binary槽位被静默丢弃,而下游三、五步之后的邮件节点再也找不到附件可挂——全程无报错、无校验警告,只有一个"文件神秘消失"的结果。本篇指南以 n8n-mcp 仓库中的 MERGE_FOR_CONTEXT.md 为核心,讲解"分流 + 按位置合并(Merge: combineByPosition)"的修复模式,并给出通过n8n_update_partial_workflow与get_node在 AI 助手场景下实际接线、配置与验证的完整方案。读完你将掌握:识别 binary 剥离点、设计 bypass 分支、正确配置 Merge 节点、以及用执行记录而非校验来确认文件真的存活。
问题:$json与$binary是两个独立槽位
n8n 的每个数据项(item)都有两个互不相干的顶层键:json存放结构化数据,binary存放文件字节。这一点在技能总览 README.md 与 BINARY_BASICS.md 中反复强调:重写json的变换不会自动携带binary,反之亦然。
{ "json": { "customerId": 42, "status": "sent" }, "binary": { "invoice": { "data": "<base64-encoded bytes>", "mimeType": "application/pdf", "fileName": "invoice-42.pdf", "fileExtension": "pdf", "fileSize": "12 kB" } } }binary内部的键名(上例中的invoice)称为binary property name,data是多数节点使用的默认名;文件处理节点通过binaryPropertyName参数指向这个键,生产方命名槽位,消费方按名引用,名字对不上就是"文件没挂上"最常见的根因之一。
问题由此而来:只处理 JSON 的节点在重建输出时不会保留binary槽位。一个典型的场景链是:
[下载 PDF 的 HTTP Request] → [Edit Fields 改字段] → [IF 判断] → [Send Email 挂附件]PDF 字节在 HTTP Request 输出时位于$binary.data;Edit Fields 重写了$json,binary 槽被丢弃;IF 继续路由无 binary 的项;最后邮件节点拿着binaryPropertyName去找一个已经不存在的槽位——附件为空。没有错误,没有警告,只有缺失的文件。这正是 MERGE_FOR_CONTEXT.md 开头所描述的"common, maddening bug"。
核心模式:在源头分流,让 binary 走一条不碰它的旁路
修复思路不是去修复变换节点,而是在源头把数据流拆成两支,最后再合回来:
[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐ │ (binary stripped here) │ │ ├─→ [Merge: combineByPosition] ─→ [Email: attach] │ │ └──────────────────────────────────┘ (bypass — binary passes through unchanged)- 变换支(transform branch):承担全部 JSON 工作(改字段、跑逻辑、做判断),binary 在这支上丢失也无妨——这支只负责贡献 JSON。
- 旁路支(bypass branch):承载原始数据项(binary 完好无损),不需要任何节点,直接把连接线从源头拉到 Merge 即可。
- 合并结果:JSON 来自变换支,binary 来自旁路支,二者重新拼回同一个 item。
这里的 Merge 节点与 n8n-node-configuration 技能(data/skills/n8n-node-configuration/README.md)中讲解的是同一个节点,只是在本场景中专门用于重新挂载 binary。这也是技能总览中"Binary is silently stripped by JSON-only transforms — pass it through or Merge it back"这条核心规则的落地操作。
用 n8n-mcp 接线:n8n_update_partial_workflow
在 Claude Desktop / Claude Code / Windsurf / Cursor 等 AI 编码环境中,n8n-mcp 暴露了n8n_update_partial_workflow工具用于增量 diff 式改工作流。当前工作流里源节点已经接好了变换支,你只需补充旁路连接和 Merge 节点:
{ "operations": [ { "type": "addNode", "node": { "name": "Merge", "type": "n8n-nodes-base.merge", "parameters": { "mode": "combine", "combineBy": "combineByPosition" } }}, { "type": "addConnection", "source": "Edit Fields", "target": "Merge", "targetInput": 0 }, { "type": "addConnection", "source": "Source", "target": "Merge", "targetInput": 1 }, { "type": "addConnection", "source": "Merge", "target": "Send Email" } ] }该工具的完整契约(20 种 diff 操作、原子模式、自动清理、自动消毒等)记录在 n8n-update-partial-workflow.ts 中。与本文直接相关的要点:
addNode要求提供节点的name、type、position;示例中为简洁省略了position,实际调用时应补上坐标(如"position": [800, 300])。addConnection按source → target建连,targetInput指定目标节点的输入口索引。- 依赖关系上注意操作顺序:必须先
addNode再加连接,因为连接的校验依赖节点已存在(工具文档 pitfall 中明确"must add node before connecting to it")。 - 建议在每次调用带上
intent参数(如"Re-attach binary after Edit Fields strips it"),这是该工具的明确最佳实践。
版本差异:先get_node确认参数形状
Merge 节点的参数名在不同版本间发生过漂移:mode、combineBy、combineByPosition的拼写,以及numberOfInputs的表达方式,在不同 n8n 版本中可能不同。原文档给出的原则是:"原理稳定,字段名会动(The principle is stable; the field names move)"。
因此在提交结构前,务必用 n8n-mcp 的get_node工具针对用户实际使用的 n8n 版本确认当前形状:
get_node({nodeType: "nodes-base.merge", detail: "standard"})get_node支持minimal / standard / full三种详情粒度与docs / search_properties / versions / compare等模式,完整用法见 get-node.ts。detail="standard"覆盖大多数需求;若要确认numberOfInputs这类字段的准确拼写与默认值,可用mode="search_properties"配合propertyQuery,或用mode="versions"对比版本间变化。
两个必踩的接线细节
- Merge 默认只有 2 个输入口。若你接 3 条以上分支,必须把输入口数量调到与实际分支数一致,否则多余的分支会被静默丢弃。
- 连接输入索引从 0 开始。上面的旁路支落在
targetInput: 1(第二个输入口),变换支在targetInput: 0。接错索引会导致配错对或分支整体丢失。
配置 Merge:位置合并是默认正确选择
用于重新挂载 binary 时,应当使用按位置合并(position-based combination)。四种模式对比如下:
| 模式 | 行为 | 可用于 binary 重挂? |
|---|---|---|
combineByPosition | 把输入 1 的第 N 项与输入 2 的第 N 项配对 | ✅ 是 |
combineBySql/combineByFields | 按键做 join | 仅当两支共享连接键 |
combineAll | 笛卡尔积(N×M 项) | ❌ 否——项数爆炸 |
append | 输入首尾相接拼接 | ❌ 否——不做配对 |
选择combineByPosition的理由很直接:它把 item 数保持在 N,且把每个变换后的 JSON 项与对应的、携带 binary 的原始项配对。要保证配对正确,两支输出的项顺序与数量必须一致——当两支共享同一个源时天然满足。
仓库中其他技能对 Merge 模式的告诫也佐证了这一选择:比如 ai_agent_workflow.md 指出并行 Agent 汇聚时combineAll做笛卡尔积并可能因输入到达时间不同产生 0 输出,这正是"模式选错导致结果爆炸/为空"的同类教训。
为什么它能生效
Merge 节点合并它配对的两个 item 的全部内容——既包括json,也包括binary。当一个输入持有你想要的 JSON、另一个输入持有你想要的 binary 时,合并后的 item 就同时携带两者。binary 之所以存活,是因为它走的那条分支从未被任何节点触碰过。
这也解释了为什么旁路支"不需要任何节点":任何额外的变换节点都可能是新的剥离点,最稳妥的旁路就是一条从源头直连 Merge 的裸连接。
更便宜的替代:在变换节点上直接透传
如果变换节点自己就能保留 binary,那么应该优先用它——一个节点能解决的事,不必上三个节点的分流合并:
Edit Fields (Set):启用
includeOtherFields,让节点把未提及的字段以及 binary 槽位一起带过去。Code 节点:在返回的 item 中显式带上
binary: $input.item.binary(详细读写配方见 BINARY_BASICS.md):// Code node, "Run Once for Each Item" const buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName) const text = buffer.toString('utf-8'); return [{ json: { ...$json, length: buffer.length }, binary: $input.item.binary, // ← 不返回 binary,文件就在这个节点上消失 }];注意
getBinaryDataBuffer会正确处理 n8n 的内存/文件系统两种存储模式,不要自己去 base64 解码$binary.<key>.data。IF / Filter:这类节点是"路由"而非"重建",通常会在透传的 item 上保留 binary——但不要假设,要在执行记录里验证。
只有两种情况下才回到 Merge:变换节点确实无法携带 binary,或 JSON 与 binary 来自完全不同的上游节点。
什么时候 Merge 也不够用
如果链路中有多个剥离点,在每个节点处都做"分流 + 合并"会变成一场维护噩梦——工作量与脆弱性都失控。两条更优路线:
- 尽早上传(Upload early):字节一产生就推到对象存储,把 URL/键作为普通 JSON 字段传遍整条链(JSON 在任何变换下都安然无恙),只在真正需要字节的节点处重新拉取。这也是大文件的正确做法——二进制槽位进入 n8n 执行数据库,几十 MB 起步就会拖慢实例,100 MB 以上必须外置存储(详见 BINARY_BASICS.md 的尺寸指导表)。
- 把 binary 处理推进子工作流:把文件交给一个专门做二进制处理的子工作流,让它返回最终结果。关键陷阱在于Execute Workflow Trigger 的输入模式:默认的 typed-input 模式只携带具名 JSON 字段、会丢弃
$binary;若子工作流必须直接接收字节,要改用 passthrough 输入模式。
一旦剥离点超过两三个,"尽早上传"或"子工作流"通常比让长链上每个节点都对 binary 保持忠诚更省事、更不易碎。
合并后如何验证:执行记录,而非校验
被合并却仍然丢失的 binary不会在校验中暴露——validate_workflow看不到 binary 槽是否存活,这是一个静默失败。唯一可靠的检查手段是执行记录:
- 用
n8n_test_workflow运行工作流,再用n8n_executions拉取本次执行。 - 看 Merge 节点的输出:确认合并后的 item 同时具备变换支的
json和旁路支的binary。 - 若 binary 缺失:检查 Merge 模式(有些模式并不按你预期的方式配对),并确认旁路支在进入 Merge 前确实带着 binary。
在 BINARY_BASICS.md 中有同样的判断标准:执行记录中binary槽位最后出现在哪个节点、又在下一个节点消失,那里就是需要插入透传或 Merge 的位置。即使 base64 内容过大无法完整渲染,槽位的存在性、元数据(名称、mime 类型、大小)也足以判断。
常见错误速查表
| 错误 | 症状 | 修复 |
|---|---|---|
| 发现剥离太晚 | 原始 binary 已经没了 | 开发时在每个节点后检查执行记录 |
| "合并"单源链但没有旁路 | 没有可合并的对象,binary 仍缺失 | 在源头分流,让 binary 走旁路支 |
该用combineByPosition却用了combineAll | N×M 项而非 N 项 | 刻意选择模式 |
| 旁路支接错输入索引 | 配对错误,或分支被丢弃 | 连接索引 0 基;用n8n_get_workflow核对 |
| 忘记把 Merge 输入口调到 2 以上 | 第三条分支静默丢弃 | 输入口数量与实际接线分支一致 |
补充一个与本主题直接相关的 n8n-mcp 工具细节:n8n_update_partial_workflow的addConnection对 IF/Switch 这类多输出节点支持语义参数(branch="true"/"false"、case=N),但 Merge 是普通多输入节点,必须用 0 基的targetInput精确指定输入口——这恰好呼应了上表"输入索引"一行的常见坑。接线完成后,可用n8n_get_workflow(mode="structure")核对连接结构是否符合预期(见 n8n-update-partial-workflow.ts 的返回值说明)。
小结
Binary 丢失的根因在 n8n 的数据模型里是结构性的:$json与$binary并行独立,JSON 变换天然不携带文件字节。应对顺序应当是:
- 优先透传——Edit Fields 开
includeOtherFields、Code 节点显式返回binary; - 透传不了就用 Merge 按位置合并——源头分流,旁路保 binary,
combineByPosition配对; - 剥离点太多就换架构——尽早上传走 URL,或把 binary 工作推给子工作流;
- 验证永远靠执行记录——
n8n_test_workflow+n8n_executions看 binary 槽位是否跨过 Merge。
记住 n8n 二进制技能(README.md)的那句话:两个槽位,并排而行。数据乘$json,文件乘$binary——而一旦文件穿越 AI 工具边界或抵达聊天界面,它将以 URL 而非字节的形式旅行。
【免费下载链接】n8n-mcpA MCP for Claude Desktop / Claude Code / Windsurf / Cursor to build n8n workflows for you项目地址: https://gitcode.com/GitHub_Trending/n8/n8n-mcp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考