3、n8n工作流初体验—定制个人名片
3、n8n工作流初体验—定制个人名片
3、n8n工作流初体验—定制个人名片 3、n8n工作流初体验—定制个人名片 Modified September 28, 2025 常见坑 • 忘了把 binary 带回(HTML 节点就取不到图片) • headline 为空:回退为 name|location 以保证不空 Code block JavaScript Copy const items = $input.all(); function esc(s){ return String(s??'').replace(/&/g,'&').replace(/</g,'<').replace(/ /g,'>').replace(/\"/g,'"'); } function cut(s,n){ s=String(s||''); return s.length n ? s.slice(0,n)+'…' : s; } const baseList = $items('清洗与规范化', 0) || []; for (let i=0;i<items.length;i++){ const it = items[i]; const ai = it.json.output ?? it.json ?? {}; const base = (baseList[i] && baseList[i].json) ? baseList[i].json : {}; const headline = esc(cut(ai.headline || , 32)); const subtitle = esc(cut(ai.subtitle || (Array.isArray(base.tags)&&base.tags[0] || '自我介绍'), 36)); const tags = (Array.isArray(ai.tags)&&ai.tags.length ? ai.tags : base.tags || []) .slice(0,8).map(t= esc(cut(t,12))); const offers = (Array.isArray(ai.offers)&&ai.offers.length ? ai.offers : base.offers || []) .slice(0,8).map(t= esc(cut(t,22))); const motto = esc(cut((ai.motto ?? base.motto ?? ''), 36)); const namePart = (headline||'student').split(/\||||·|,|,/)[0].trim(); const baseName = namePart.replace(/\s+/g,''); const ts = new Date().toISOString().replace(/[ :TZ.]/g,'').slice(0,14); const slug = (base.slug || (baseName||'student')+' '+ts); const initials = (namePart || '名片').slice(0,2); // 联系方式继承清洗后的 const wechat = esc(base.wechat || ''); const phone = esc(base.phone || ''); const email = esc(base.email || ''); const site = esc(base.site || ''); // ——关键:把 binary 和 base64 一起带回去—— const fromClean = baseList[i] || {}; it.binary = fromClean.binary || {}; // 给 HTML 节点 attachBinaryToJson 使用 const avatar base64 = base.avatar base64 || ''; const qr base64 = base.qr base64 || ''; it.json = { headline, subtitle, tags, offers, motto, slug, initials, wechat, phone, email, site, avatar base64, qr base64 }; } return items; 6. Code 节点:生成HTML 作用 把所有字段渲染成最终的名片网页: • 头像:圆形显示,取 binary.avatar(支持多别名/url/base64) • 二维码:裁切放大(只露中间码),不遮住右下角 • 语义组件:标题/副标题/标签/能力/联系方式/格言/页脚 • 产出 html 字符串,并写入 binary.data 作为文件内容 代码 科幻风格模板 JavaScript Copy const items = $input.all(); / 工具函数 / const SEP = /[,,、;;\n\r\t]+/; function esc(s) { return String(s ?? '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/ /g, '>') .replace(/"/g, '"'); } function toStr(v) { return String(v ?? '').trim(); } function limit(s, n) { s = toStr(s); return s.length n ? s.slice(0, n) + '…' : s; } function ensureList(v, maxN, maxLen) { let arr = Array.isArray(v) ? v : toStr(v).split(SEP); return arr.map(x = esc(limit(x, maxLen))).filter(Boolean).slice(0, maxN); } function initialsFrom(name) { name = toStr(name); if (!name) return '名片'; const zh = name.match(/[\u4e00 \u9fff]/g); if (zh && zh.length) return (zh[0] + (zh[1] || '')).slice(0, 2); // 中文前2字 const parts = name.split(/\s+/).filter(Boolean); if (parts.length = 2) return (parts[0][0] + parts[1][0]).toUpperCase(); // 英文首字母 return name.slice(0, 2).toUpperCase(); } function safeSlug(name) { const base = toStr(name).replace(/\s+/g, '').replace(/[\\/: ?"< | %&{}$!@ data:${o.mimeType || fallbackMime};base64,${o.data} ${key} base64 ${a} base64 ${key} mime ${aliases[0]} mime data:${mime};base64,${clean} data:${bin.mimeType || fallbackMime};base64,${bin.data} data:${bin.mimeType};base64,${bin.data} ${nm}|${lc} <span class="chip" ${t}</span <li <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria hidden="true" <path d="M20 6L9 17l 5 5" stroke=" 34d399" stroke width="2.2" stroke linecap="round" stroke linejoin="round"/ </svg <span ${t}</span </li <!doctype html <html lang="zh CN" <head <meta charset="utf 8"/ <meta name="viewport" content="width=device width, initial scale=1"/ <title ${esc(rawHeadline)}</title <style @font face{ font family:'CNWeb'; font style:normal; font weight:400; font display:block; src:url('https://fastly.jsdelivr.net/gh/lxgw/LxgwWenKai/release/LXGWWenKai Regular.woff2') format('woff2'); unicode range:U+0000 00FF, U+4E00 9FFF; } @font face{ font family:'CNWeb'; font style:normal; font weight:700; font display:block; src:url('https://fastly.jsdelivr.net/gh/lxgw/LxgwWenKai/release/LXGWWenKai Bold.woff2') format('woff2'); unicode range:U+0000 00FF, U+4E00 9FFF; } :root{ card w: 820px; bg: 0b1222; grid: 131b2f; card: 0e1a31bf; card2: 0b1529bf; text: e9eef7; muted: 9fb1c9; accent: 5dd2b3; chip: 80caff; beam1: 1dd6c8; beam2: 7c4dff; qr size: 168px; / 显示尺寸:可在 150 184 之间微调 / qr zoom: 1.28; / 放大倍率:数值越大裁得越狠 / qr focus: 78%; / 取图中心的垂直焦点,往下偏一点裁掉上部介绍 / } {box sizing:border box} html,body{height:100%} html,body, { font family:'CNWeb','Noto Sans SC','PingFang SC','Microsoft YaHei', apple system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans serif !important; webkit font smoothing:antialiased; text rendering:optimizeLegibility; 女生模板 XML Copy const items = $input.all(); / 工具函数(与原版一致,便于无缝替换) / const SEP = /[,,、;;\n\r\t]+/; function esc(s){return String(s??'').replace(/&/g,'&').replace(/</g,'<').replace(/ /g,'>').replace(/"/g,'"')} function toStr(v){return String(v??'').trim()} function limit(s,n){s=toStr(s);return s.length n?s.slice(0,n)+'…':s} function ensureList(v,maxN,maxLen){let arr=Array.isArray(v)?v:toStr(v).split(SEP);return arr.map(x= esc(limit(x,maxLen))).filter(Boolean).slice(0,maxN)} function initialsFrom(name){ name=toStr(name); if(!name) return '名片'; const zh=name.match(/[\u4e00 \u9fff]/g); if(zh&&zh.length) return (zh[0]+(zh[1]||'')).slice(0,2); const parts=name.split(/\s+/).filter(Boolean); if(parts.length =2) return (parts[0][0]+parts[1][0]).toUpperCase(); return name.slice(0,2).toUpperCase(); } function safeSlug(name){ const base=toStr(name).replace(/\s+/g,'').replace(/[\\/: ?"< | %&{}$!@ data:${o.mimeType||fallbackMime};base64,${o.data} ${key} base64 ${a} base64 ${key} mime ${aliases[0]} mime data:${mime};base64,${clean} data:${bin.mimeType||fallbackMime};base64,${bin.data} data:${bin.mimeType};base64,${bin.data} ${nm}|${lc}` : (nm||lc||''); } const parts=rawHeadline.split(/||\|/); const name=toStr(parts[0]); const loc=toStr(parts[1]); const titleName=esc(limit(name||rawHeadline,30)); const titleLoc =esc(limit(loc,22)); const subtitle =esc(limit(d.subtitle||'',36)); const tags =ensureList(d.tags,8,12); const offers =ensureList(d.offers,8,22); const motto =esc(limit(d.motto||'',36)); const initials =esc((d.initials && limit(d.initials,2))||initialsFrom(name||rawHeadline)); const avatarUrl=unifyImageSrc(d,'avatar',['头像','微信头像']); const qrUrl =unifyImageSrc(d,'qr',['微信二维码','二维码','qrcode']); 常见坑 • 忘了把 binary 带回(HTML 节点就取不到图片) • headline 为空:回退为 name|location 以保证不空 6. Code 节点:生成HTML 作用 把所有字段渲染成最终的名片网页: • 头像:圆形显示,取 binary.avatar(支持多别名/url/base64) • 二维码:裁切放大(只露中间码),不遮住右下角 • 语义组件:标题/副标题/标签/能力/联系方式/格言/页脚 • 产出 html 字符串,并写入 binary.data 作为文件内容 代码 继续问AI 迭代优化 关键点(二维码的三条“药方”已内置) • qr size 控显示尺寸(PC/移动分别调) • qr zoom 控裁切力度(越大越只露中间码) • qr focus 控焦点偏下,尽量裁掉“微信保存页”的上方介绍 • 外层 .qr box 作为“裁切视窗”,img 在其中放大+下移 • image rendering: pixelated; 放大不糊 • footer 通过 padding right 预留二维码空间,避免被遮挡 检查点 • 这个节点的 JSON 里应包含 slug 与 html • 这个节点的 Binary 里应有 data(就是 HTML 文件内容) • 打开 html 预览:头像显示、二维码只露纯码、右下角不被遮挡 7. 保存HTML文件节点(Write Binary File) 作用 把上一步产出的 binary.data 落到磁盘。 关键设置 • File name: /data/files/1/{{$json.slug}}.html(或你自定义的路径) • 注意:/data/files 就是我们之前在docker部署的时候docker内的文件夹目录,映射我们本地电脑的目录 • Binary Property:默认 data(即第 6 步里写入的键) 检查点 • 节点运行后,日志显示写入成功 • 到服务器对应目录能看到 .html 文件 8. 串联关系(从左到右) 表单 → 清洗与规范化 → 准备AI输入(文本) → AI Agent(大脑/模型/解析器) → 解析AI输出(约束+转义) → 生成HTML → 写文件 9. 常见问题排查清单 • AI 报 422 / 反序列化失败: 1)检查“准备AI输入(文本)”是否只输出纯文本; 2)Agent 提示词是否明确“只输出严格 JSON”; 3)Parser 的 JSON Schema 是否与提示词一致。 • 头像/二维码不显示: 1)第 2 个节点节输出里 binary.avatar/qr 是否存在; 2)第 5 个节点是否把 binary 带回; 3)第 6 个节点的 unifyImageSrc 是否有拿到 data url(可以 console.log 验证)。 • 二维码太小/露出周边文字: 在第 6 节 CSS 里微调: ◦ qr size(先 168 184px 尝试) ◦ qr zoom(1.35 1.5) ◦ qr focus(76% 82%) ◦ qr size(先 168 184px 尝试) ◦ qr zoom(1.35 1.5) ◦ qr focus(76% 82%) 上述我们已经手把手带着大家来进行实际操作,感受到n8n的魅力,它非常的开放,这个名片的想法也只是我突然想到的,然后跟AI聊天,在n8n中一步步实现的,大家后续系统学完我们的课程,也大概率可以独立做出属于自己的智能体,给自己提效或者帮助别人解决痛点。 和AI交流的详情:https://chatgpt.com/share/68a4659f 7cac 800f a8a0 8da67678512d 中间迭代的部分效果过程。 即使是强如ChatGPT5 thinking也没办法直接把我们的需求生成最终可执行的n8n工作流,这个过程还是需要我们来进行优化,所以这也算是一个壁垒,有一定的门槛。 小石学长后续会给大家系统的分享n8n中关键节点,让大家把基础打好,后续遇到类似的问题更快解决。 📖 大家先看一下效果,通过工作流,用AI智能体定制你的个人独家名片。 大家先看一下效果,通过工作流,用AI智能体定制你的个人独家名片。 最终版还支持切换主题,共有十种精选模板。 我们只要在工作流表单中输入我们的相关信息即可,下图是最终搭建好的工作流整体框架。 而这些,需要我们对n8n一些常用节点有基础,不然在具体实现过程中只是一个想法,很难去把需求落地。 我们带着大家从0 1开始搭建,一起先感受一下,听不懂也没关系,主要是用 n8n 智能体做出实际的东西。 1. 表单节点 在n8n工作流页面右边新建form表单。 选择触发器下面的这个节点。 这个节点主要是用于n8n启动的时候收集我们的基本信息,下面的参数供参考。 针对微信二维码和头像这两个字段重点说明一下 两个图片字段必须把 Field Type 设为 File,否则图片不会进入 binary。 表单提交后,在节点右侧的 Binary 标签页应能看到两张图;如果看不到,说明类型没配成文件或浏览器没有真正 最好把文件名称改成这样 上传 【微信二维码保存方式】 【微信头像保存方式】 2. Code 节点:清洗与规范化 在n8n工作流页面搜索code 页面详情 作用 把表单的原始输入清洗成标准结构,尤其是: • 统一中英文键名:名称/坐标/标签/我能提供/格言/微信号/… → name/location/tags/offers/motto/wechat/... • 将文件统一放到 binary.avatar / binary.qr(支持多别名、原始文件名模糊匹配) • 生成 slug(输出文件名后缀) 为什么重要 后续所有节点都依赖这里的统一字段;尤其是二进制图片,这里不归一,后面就很容易“有图但取不到”。 代码【这块大家暂时看不懂也没关系 结合了AI编程 后续可以慢慢深入学习】 检查点 • 节点输出的 JSON 里应有:name / location / tags[] / offers[] / motto / wechat / phone / email / site • 节点输出的 Binary 里应有:avatar 与 qr(看不到就是表单没配成文件,或字段名没匹配上) 3. Code 节点:准备AI输入(文本) 作用 给 LLM(AI Agent)准备纯文本输入,去掉任何 binary/base64,防止被当作“图片消息”发送导致报错。 它做了什么 • 兼容中文/英文键名,裁切列表长度(标签 ≤5、能力 ≤6) • 只输出文本字段:{ name, location, tags, offers, motto } 代码 检查点 • 这个节点的输出 不应有 Binary 标签页 • JSON 里能看到 5 个字段,数组长度符合限制 4. AI Agent 节点(文案生成链路) 这是 3 个子组件的组合:Agent(提示词) → ChatModel(模型) → Parser(结构化输出)。 1) AI Agent(提示词) 作用:组织提示词,把第 3 节产出的字段喂给模型。 提示词详情 要点: • 提示词只包含纯文本字段,不插入任何图片/二进制 • 模板中用: ◦ 名称:{{ $json.name || '' }} ◦ 坐标:{{ $json.location || '' }} ◦ 标签:{{ ($json.tags || []).slice(0,5).join(',') }} ◦ 我能提供:{{ ($json.offers || []).slice(0,6).join(',') }} ◦ 格言:{{ $json.motto || '' }} ◦ 名称:{{ $json.name || '' }} ◦ 坐标:{{ $json.location || '' }} ◦ 标签:{{ ($json.tags || []).slice(0,5).join(',') }} ◦ 我能提供:{{ ($json.offers || []).slice(0,6).join(',') }} ◦ 格言:{{ $json.motto || '' }} • 提醒模型 只输出严格 JSON,不要 markdown 包裹 2) ChatModel(真正干活的“大脑”) 作用:承接 Agent 的提示,产出文案草稿(标题/副标题/再加工后的 tags/offers/motto)。 要点:无需改动路由;只要保证输入是纯文本即可。 这里我用deepseek,大家也可以用国内外的AI模型,可以本地部署或者买线上API,买线上API要去对应的平台花钱获取。 比如DeepseekAPIKey获取:https://platform.deepseek.com/api keys 在这里进行充值即可,10元可以用很久 我这用了两个月了还有一大半的钱 把上面页面的API复制下来,在Deepseek这个节点里面新增凭证 把APIKey在这里面复制,然后保存即可 然后我们可以看到这里有chat模型和reasoner模型,就是对应网页端的普通模型和推理模型,这里我们算是简单任务,普通的chat模型就能满足需求,且速度更快。 3) Parser(输出解析器) 前提:在AI Agent节点要勾选需要特定输出格式 作用:把模型的自然语言解析为严格 JSON: 要点:如果解析失败,90% 是提示词没让模型输出纯 JSON 或模型生成了多余文本;回到 Agent 检查提示词。 5. Code 节点:解析AI输出(约束+转义) 作用 把解析器给的 JSON 与第 2 节的“清洗结果”合并,做安全裁切与转义,补全联系信息,并把图片从清洗节点带回来(给下一步 HTML 使用)。 关键点 • 读取 AI Agent 的输出(headline/subtitle/tags/offers/motto),并与 清洗与规范化 同序合并 • 把 baseList[i].binary 原样放回当前 item,确保后面能拿到 binary.avatar / binary.qr • 统一输出:headline / subtitle / tags[] / offers[] / motto / initials / wechat / phone / email / site / (binary) 因为觉得如果每次只能生成一个样式的模板,感觉功能还是太简单且不方便了,于是设置了一个可以切换主题的功能。 如果二维码缩放过大,可以把 qr zoom:1.36 这个数值调小一些