解析POML
中级
这是一个Engineering, Multimodal AI领域的自动化工作流,包含 10 个节点。主要使用 Set, Code, ManualTrigger, Agent, LmChatOpenAi 等节点。 使用零依赖将POML转换为AI就绪提示和聊天消息
前置要求
- •OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "",
"meta": {
"instanceId": "",
"templateCredsSetupCompleted": false
},
"name": "解析POML",
"tags": [
{
"id": "ahEMnscqU2YYEOGK",
"name": "Developer",
"createdAt": "2025-08-19T16:32:11.755Z",
"updatedAt": "2025-08-19T16:32:11.755Z"
},
{
"id": "KvVY10Qqey3YJmAq",
"name": "Real Simple Solutions",
"createdAt": "2024-12-16T19:05:05.815Z",
"updatedAt": "2024-12-16T19:05:05.815Z"
}
],
"nodes": [
{
"id": "e67f2a4a-df0c-4b7f-b267-f831e117d74d",
"name": "设置变量",
"type": "n8n-nodes-base.set",
"position": [
256,
176
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e268b89e-df54-471f-bf56-b2d09d36e79c",
"name": "poml",
"type": "string",
"value": "<poml> <task>You are given various potential options or approaches for a project. Convert these into a well-structured research plan.</task> <stepwise-instructions> <list listStyle=\"decimal\"> <item>Identifies Key Objectives <list listStyle=\"dash\"> <item>Clarify what questions each option aims to answer</item> <item>Detail the data/info needed for evaluation</item> </list> </item> <item>Describes Research Methods <list listStyle=\"dash\"> <item>Outline how you’ll gather and analyze data</item> <item>Mention tools or methodologies for each approach</item> </list> </item> <item>Provides Evaluation Criteria <list listStyle=\"dash\"> <item>Metrics, benchmarks, or qualitative factors to compare options </item> <item>Criteria for success or viability</item> </list> </item> <item>Specifies Expected Outcomes <list listStyle=\"dash\"> <item>Possible findings or results </item> <item>Next steps or actions following the research</item> </list> </item> </list> Produce a methodical plan focusing on clear, practical steps. </stepwise-instructions> </poml>"
},
{
"id": "98ac0a36-75ae-485b-896e-d6658810d293",
"name": "context",
"type": "object",
"value": "{ \"context\": { \"project\": { \"name\": \"Discovery Framework\" }, \"audience\": \"internal research team\", \"timeframe\": \"10 business days\", \"methods\": { \"csv\": \"Literature review, SME interviews, Data scraping\" }, \"success\": \"Clear comparison matrix, ranked recommendation, and next steps.\" } }"
},
{
"id": "9fbc3de4-2a0b-4c83-a997-02a428cf2bb6",
"name": "speakerMode",
"type": "boolean",
"value": true
},
{
"id": "a55e0e74-be4a-460b-8088-4834fc4ba7bf",
"name": "listStyle",
"type": "string",
"value": "dash"
},
{
"id": "21b92729-99b1-4ed1-83c3-3f12eaae1be6",
"name": "componentSpec",
"type": "object",
"value": "{ \"components\": { \"ai-msg\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"Audio\": [\"src\", \"base64\", \"alt\", \"type\", \"position\", \"syntax\"], \"audio\": [\"src\", \"base64\", \"alt\", \"type\", \"position\", \"syntax\"], \"b\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"br\": [\"newLineCount\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"code\": [\"inline\", \"lang\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"conversation\": [\"messages\", \"selectedMessages\"], \"cp\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"dataObj\": [\"syntax\", \"data\", \"className\", \"speaker\", \"writerOptions\"], \"Document\": [\"src\", \"buffer\", \"base64\", \"parser\", \"multimedia\", \"selectedPages\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"example\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"chat\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"examples\": [\"caption\", \"captionSerialized\", \"chat\", \"introducer\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"folder\": [\"syntax\", \"src\", \"data\", \"filter\", \"maxDepth\", \"showContent\"], \"h\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"Header\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"hint\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"i\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"Image\": [\"src\", \"alt\", \"base64\", \"type\", \"position\", \"maxWidth\", \"maxHeight\", \"resize\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"img\": [\"src\", \"alt\", \"base64\", \"type\", \"position\", \"maxWidth\", \"maxHeight\", \"resize\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"input\": [\"caption\", \"captionSerialized\", \"speaker\", \"captionStyle\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"name\", \"type\", \"writerOptions\"], \"introducer\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"item\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"list\": [\"listStyle\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"msg-content\": [\"content\"], \"obj\": [\"syntax\", \"data\", \"className\", \"speaker\", \"writerOptions\"], \"Object\": [\"syntax\", \"data\", \"className\", \"speaker\", \"writerOptions\"], \"output\": [\"caption\", \"captionSerialized\", \"speaker\", \"captionStyle\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"name\", \"type\", \"writerOptions\"], \"output-format\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"p\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"poml\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"qa\": [\"questionCaption\", \"answerCaption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"role\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"s\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"section\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"span\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"stepwise-instructions\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"strike\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"system-msg\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"table\": [\"syntax\", \"records\", \"columns\", \"src\", \"parser\", \"selectedColumns\", \"selectedRecords\", \"maxRecords\", \"maxColumns\", \"className\", \"speaker\", \"writerOptions\"], \"task\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"text\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"Tree\": [\"syntax\", \"items\", \"showContent\"], \"u\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"user-msg\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"webpage\": [\"url\", \"src\", \"buffer\", \"base64\", \"extractText\", \"selector\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"] } } "
},
{
"id": "45d939a2-4aef-4a53-a090-fe61c99c5bf7",
"name": "attributeSpec",
"type": "array",
"value": "[ { \"attribute\": \"original-end-index\", \"applies_to\": [\"*\"], \"type\": \"integer\", \"description\": \"The end offset of the element corresponding to the current one in the original document\" }, { \"attribute\": \"original-start-index\", \"applies_to\": [\"*\"], \"type\": \"integer\", \"description\": \"The start offset of the element corresponding to the current one in the original document\" }, { \"attribute\": \"speaker\", \"applies_to\": [\"*\"], \"type\": \"ai/human/system\", \"description\": \"The speaker of the current content\" }, { \"attribute\": \"name\", \"applies_to\": [\"any\"], \"type\": \"string\", \"description\": \"An optional identifier for the data.\" }, { \"attribute\": \"type\", \"applies_to\": [\"any\"], \"type\": \"string\", \"description\": \"The data type of the value ('string', 'intege...lean', 'array', 'object', 'buffer', 'null', or 'undefined').\" }, { \"attribute\": \"blank-line\", \"applies_to\": [\"code\"], \"type\": \"boolean\", \"description\": \"Inserts a blank line before and after the code block if inline = false.\" }, { \"attribute\": \"inline\", \"applies_to\": [\"code\"], \"type\": \"boolean\", \"description\": \"Indicates whether the code is inline (true) or a block element (false).\" }, { \"attribute\": \"lang\", \"applies_to\": [\"code\"], \"type\": \"string\", \"description\": \"Specifies the programming language or syntax highlighting mode.\" }, { \"attribute\": \"markup-lang\", \"applies_to\": [\"env\"], \"type\": \"string\", \"description\": \"The specific markup language, required only if presentation = 'markup'.\" }, { \"attribute\": \"presentation\", \"applies_to\": [\"env\"], \"type\": \"string\", \"description\": \"The output style or format mode ('markup', 'serialize', 'free', or 'multimedia').\" }, { \"attribute\": \"serializer\", \"applies_to\": [\"env\"], \"type\": \"string\", \"description\": \"The name of the serializer, required only if presentation = 'serialize'.\" }, { \"attribute\": \"writer-options\", \"applies_to\": [\"env\"], \"type\": \"object\", \"description\": \"Optional parameters passed to the writer constructor for customizing output.\" }, { \"attribute\": \"level\", \"applies_to\": [\"h\"], \"type\": \"integer\", \"description\": \"Indicates the heading level. Typically ranges from 1 (highest level) to 6 (lowest level).\" }, { \"attribute\": \"alt\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"Alternative text describing the image.\" }, { \"attribute\": \"base64\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"The base64-encoded image data.\" }, { \"attribute\": \"position\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"The placement of the image relative to text, such as 'here', 'top', or 'bottom'.\" }, { \"attribute\": \"type\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"The image MIME type (e.g., 'image/jpeg', 'image/png').\" }, { \"attribute\": \"list-style\", \"applies_to\": [\"list\"], \"type\": \"string\", \"description\": \"The style of the list bullets or enumeration (e.g., 'star', 'dash', 'decimal').\" }, { \"attribute\": \"count\", \"applies_to\": [\"nl\"], \"type\": \"integer\", \"description\": \"Specifies how many newline characters to insert.\" }, { \"attribute\": \"data\", \"applies_to\": [\"obj\"], \"type\": \"object\", \"description\": \"A valid JSON object containing the structured data.\" }, { \"attribute\": \"blank-line\", \"applies_to\": [\"p\"], \"type\": \"boolean\", \"description\": \"Inserts a blank line before and after the paragraph when true.\" } ] "
}
]
}
},
"typeVersion": 3.4
},
{
"id": "be281179-0d5e-4937-bee1-b9bcfa09c4b7",
"name": "解析_POML",
"type": "n8n-nodes-base.code",
"position": [
576,
176
],
"parameters": {
"jsCode": "// n8n Code node (Function) — POML → Prompt/Messages (zero-deps, schema-driven)\n//\n// Supported tags (subset):\n// <poml|text|p>, <h level=N>, <b>/<strong>, <i>/<em>,\n// <list list-style=...> <item>, <code inline|lang|blank-line>,\n// <img alt|src|base64|type>, <audio alt|src>, <br newLineCount=N>,\n// <table records|columns>, <Conversation>, <SystemMessage>/<HumanMessage>/<AiMessage>,\n// <system-msg>/<user-msg>/<ai-msg>\n// Unknown tags render their children.\n//\n// How to pass your JSON specs:\n// - Set node A → set \"componentSpec\" = (your components JSON object)\n// - Set node B → set \"attributeSpec\" = (your attributes JSON object or array)\n// This node will auto-detect common shapes.\n\nfunction get(obj, path, fallback='') {\n if (!path) return fallback;\n const segs = String(path).split('.').map(s => s.trim()).filter(Boolean);\n let cur = obj;\n for (const s of segs) {\n if (cur && Object.prototype.hasOwnProperty.call(cur, s)) cur = cur[s];\n else return fallback;\n }\n return (cur == null ? fallback : cur);\n}\n\nfunction substitute(str, ctx) {\n if (!str) return '';\n return String(str).replace(/\\{\\{\\s*([a-zA-Z0-9_.$[\\]-]+)\\s*\\}\\}/g, (_, p1) => {\n const v = get(ctx, p1, '');\n return (v == null ? '' : String(v));\n });\n}\n\n// Minimal XML-ish tokenizer → AST\nfunction parsePoml(input) {\n const src = String(input);\n const root = { type: 'element', name: 'root', attrs: {}, children: [] };\n const stack = [root];\n let i = 0;\n const attrRe = /([:@a-zA-Z_][\\w:.-]*)\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s\"'>/]+))/g;\n\n while (i < src.length) {\n const lt = src.indexOf('<', i);\n if (lt < 0) { const text = src.slice(i); if (text) stack[stack.length-1].children.push({ type:'text', text }); break; }\n if (lt > i) { const text = src.slice(i, lt); if (text) stack[stack.length-1].children.push({ type:'text', text }); i = lt; }\n\n if (src.startsWith('<!--', i)) { const end = src.indexOf('-->', i+4); i = (end >= 0 ? end+3 : src.length); continue; }\n\n const closeMatch = src.slice(i).match(/^<\\/\\s*([a-zA-Z_][\\w:.-]*)\\s*>/);\n if (closeMatch) {\n const tag = closeMatch[1].toLowerCase();\n for (let j = stack.length - 1; j >= 0; j--) {\n if (stack[j].name.toLowerCase() === tag) { stack.length = j; break; }\n }\n i += closeMatch[0].length;\n continue;\n }\n\n const openMatch = src.slice(i).match(/^<\\s*([a-zA-Z_][\\w:.-]*)([^>]*)>/);\n if (openMatch) {\n const tag = openMatch[1];\n let rest = openMatch[2] || '';\n const selfClosing = /\\/\\s*$/.test(rest);\n if (selfClosing) rest = rest.replace(/\\/\\s*$/, '');\n const attrs = {};\n attrRe.lastIndex = 0;\n let m;\n while ((m = attrRe.exec(rest))) {\n const key = m[1];\n const val = m[3] ?? m[4] ?? m[5] ?? '';\n attrs[key] = val;\n }\n const el = { type:'element', name: tag, attrs, children: [] };\n stack[stack.length-1].children.push(el);\n i += openMatch[0].length;\n if (!selfClosing) stack.push(el);\n continue;\n }\n\n // literal '<'\n stack[stack.length-1].children.push({ type:'text', text:'<' });\n i += 1;\n }\n return root.children;\n}\n\n// Spec normalization — accepts multiple shapes\nfunction normalizeComponentSpec(spec) {\n const map = {};\n if (!spec) return map;\n // shape A: { components: { tag: [attrs...] } }\n if (spec.components && typeof spec.components === 'object') {\n for (const [k, v] of Object.entries(spec.components)) map[k.toLowerCase()] = new Set((v||[]).map(a => String(a).toLowerCase()));\n return map;\n }\n // shape B: { tag: [attrs...] }\n for (const [k, v] of Object.entries(spec)) {\n if (Array.isArray(v)) map[k.toLowerCase()] = new Set(v.map(a => String(a).toLowerCase()));\n }\n return map;\n}\n\nfunction normalizeAttributeSpec(spec) {\n // return { attrNameLower: { type: 'boolean'|'integer'|'string'|..., ... } }\n const out = {};\n if (!spec) return out;\n if (Array.isArray(spec)) {\n for (const a of spec) {\n const name = (a && (a.attribute || a.name || a.key)) ? String(a.attribute || a.name || a.key).toLowerCase() : null;\n if (name) out[name] = a;\n }\n return out;\n }\n if (typeof spec === 'object') {\n for (const [k, v] of Object.entries(spec)) out[String(k).toLowerCase()] = (typeof v === 'object' ? v : { type: String(v) });\n }\n return out;\n}\n\n// hyphen/camel resolver with type coercion\nfunction readAttr(attrs, name, attrSpecItem, fallback) {\n const keys = [name, name.replace(/-([a-z])/g, (_,c)=>c.toUpperCase()), name.replace(/[A-Z]/g, c=>'-'+c.toLowerCase())];\n let raw; for (const k of keys) if (attrs[k] !== undefined) { raw = attrs[k]; break; }\n if (raw === undefined) return fallback;\n\n const t = (attrSpecItem && attrSpecItem.type || '').toLowerCase();\n if (t === 'boolean') return /^(true|1|yes)$/i.test(String(raw));\n if (t === 'integer' || t === 'number') { const n = Number.parseInt(raw,10); return Number.isFinite(n) ? n : fallback; }\n if (t === 'ai/human/system') return String(raw).toLowerCase();\n // object/array as JSON\n if (t === 'object' || t === 'array') {\n if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return fallback; } }\n return raw;\n }\n return raw; // string / enum / unknown\n}\n\nconst md = {\n fence(code, lang='', addBlank=false){return `${addBlank?'\\n':''}\\`\\`\\`${lang}\\n${String(code).replace(/\\s+$/,'')}\\n\\`\\`\\`${addBlank?'\\n':''}`;},\n};\n\nconst trimLines = (s) => String(s).replace(/[ \\t]+\\n/g, '\\n');\n\nfunction compileNodes(nodes, ctx, roleCtx, options) {\n let out = '';\n for (const n of nodes) out += compileNode(n, ctx, roleCtx, options);\n return out;\n}\n\nfunction compileNode(node, ctx, roleCtx, options) {\n if (node.type === 'text') return substitute(node.text, ctx);\n\n const name = node.name.toLowerCase();\n const attrs = node.attrs || {};\n const attrSpec = options.attrSpec || {};\n const componentMap = options.componentMap || {};\n\n // (Optional) component attribute whitelist (non-fatal)\n const allow = componentMap[name];\n if (allow) {\n for (const k of Object.keys(attrs)) {\n if (!allow.has(String(k).toLowerCase())) {\n // drop unknown attrs silently (or collect warnings in options.warnings)\n // delete attrs[k];\n }\n }\n }\n\n // speaker override\n const speakerAttr = (readAttr(attrs, 'speaker', attrSpec['speaker'] || { type:'ai/human/system' }, '') || '').toLowerCase();\n let nextRoleCtx = roleCtx;\n if (speakerAttr === 'system') nextRoleCtx = 'system';\n if (speakerAttr === 'human') nextRoleCtx = 'user';\n if (speakerAttr === 'ai') nextRoleCtx = 'assistant';\n\n switch (name) {\n case 'poml':\n case 'text':\n case 'p':\n return trimLines(compileNodes(node.children, ctx, nextRoleCtx, options));\n\n case 'h':\n case 'header': {\n let level = readAttr(attrs, 'level', attrSpec['level'] || { type:'integer' }, 1);\n level = Math.min(6, Math.max(1, level || 1));\n const inner = trimLines(compileNodes(node.children, ctx, nextRoleCtx, options)).trim();\n return `\\n\\n${'#'.repeat(level)} ${inner}\\n\\n`;\n }\n\n case 'b':\n case 'bold':\n case 'strong': {\n const inner = compileNodes(node.children, ctx, nextRoleCtx, options).trim();\n return `**${inner}**`;\n }\n\n case 'i':\n case 'italic':\n case 'em': {\n const inner = compileNodes(node.children, ctx, nextRoleCtx, options).trim();\n return `_${inner}_`;\n }\n\n case 'list':\n case 'ul': {\n const style = readAttr(attrs, 'list-style', attrSpec['list-style'] || { type:'string' }, options.listStyle || 'dash')\n || readAttr(attrs, 'listStyle', attrSpec['list-style'] || { type:'string' }, options.listStyle || 'dash');\n const bullet = style==='decimal' ? '1.' : style==='star' ? '*' : style==='plus' ? '+' : style==='latin' ? 'a.' : '-';\n const lines = [];\n for (const ch of node.children) {\n if (ch.type === 'element' && (ch.name.toLowerCase() === 'item' || ch.name.toLowerCase() === 'li')) {\n const t = trimLines(compileNodes(ch.children, ctx, nextRoleCtx, options)).replace(/\\n+/g, ' ').trim();\n if (t) lines.push(`${bullet} ${t}`);\n }\n }\n return `\\n${lines.join('\\n')}\\n`;\n }\n\n case 'item':\n case 'li': {\n const t = trimLines(compileNodes(node.children, ctx, nextRoleCtx, options)).replace(/\\n+/g, ' ').trim();\n return `- ${t}\\n`;\n }\n\n case 'code': {\n const inline = readAttr(attrs, 'inline', attrSpec['inline'] || { type:'boolean' }, true);\n const lang = readAttr(attrs, 'lang', attrSpec['lang'] || { type:'string' }, '');\n const blankA = readAttr(attrs, 'blank-line', attrSpec['blank-line'] || { type:'boolean' }, false);\n const blankB = readAttr(attrs, 'blankLine', attrSpec['blank-line'] || { type:'boolean' }, false);\n const addBlank = Boolean(blankA || blankB);\n const body = compileNodes(node.children, ctx, nextRoleCtx, options);\n return inline ? '`' + body.replace(/`/g, '\\\\`') + '`' : md.fence(body, lang, addBlank);\n }\n\n case 'img':\n case 'image': {\n const alt = readAttr(attrs, 'alt', attrSpec['alt'] || { type:'string' }, '');\n let src = readAttr(attrs, 'src', attrSpec['src'] || { type:'string' }, '');\n const b64 = readAttr(attrs, 'base64', attrSpec['base64'] || { type:'string' }, '');\n const mime = readAttr(attrs, 'type', attrSpec['type'] || { type:'string' }, 'application/octet-stream');\n if (!src && b64) src = `data:${mime};base64,${b64}`;\n return src ? `\\n\\n\\n\\n` : (alt ? `\\n\\n${alt}\\n\\n` : '');\n }\n\n case 'audio': {\n const alt = readAttr(attrs, 'alt', attrSpec['alt'] || { type:'string' }, '');\n const src = readAttr(attrs, 'src', attrSpec['src'] || { type:'string' }, '');\n return `\\n\\n[Audio: ${alt || src || 'audio'}]\\n\\n`;\n }\n\n case 'br': {\n const n = Math.max(1, readAttr(attrs, 'newLineCount', attrSpec['newLineCount'] || { type:'integer' }, 1));\n return '\\n'.repeat(n);\n }\n\n case 'table': {\n let records = readAttr(attrs, 'records', attrSpec['records'] || { type:'object' }, null);\n if (typeof records === 'string') { try { records = JSON.parse(records); } catch {} }\n const columns = readAttr(attrs, 'columns', attrSpec['columns'] || { type:'object' }, null);\n if (!Array.isArray(records) || records.length === 0) return '';\n let headers;\n if (Array.isArray(columns) && columns.length) headers = columns.map(String);\n else if (typeof records[0] === 'object' && !Array.isArray(records[0])) headers = Object.keys(records[0]);\n else headers = records[0].map((_,i)=>`col${i+1}`);\n const rows = records.map(r => Array.isArray(r) ? r : headers.map(h => r[h]));\n const head = `| ${headers.join(' | ')} |`;\n const sep = `| ${headers.map(()=> '---').join(' | ')} |`;\n const body = rows.map(row => `| ${row.map(v => String(v ?? '')).join(' | ')} |`).join('\\n');\n return `\\n${head}\\n${sep}\\n${body}\\n`;\n }\n\n // Message components (speakerMode aware)\n case 'systemmessage':\n case 'humanmessage':\n case 'aimessage': {\n const role = name === 'systemmessage' ? 'system' : name === 'humanmessage' ? 'user' : 'assistant';\n const txt = trimLines(compileNodes(node.children, ctx, role, options)).trim();\n if (options.speakerMode) { options.messages.push({ role, content: txt }); return ''; }\n return `\\n\\n**${role.toUpperCase()}:**\\n${txt}\\n`;\n }\n\n case 'system-msg':\n case 'user-msg':\n case 'ai-msg': {\n const role = name === 'system-msg' ? 'system' : name === 'user-msg' ? 'user' : 'assistant';\n const txt = trimLines(compileNodes(node.children, ctx, role, options)).trim();\n if (options.speakerMode) { options.messages.push({ role, content: txt }); return ''; }\n return `\\n\\n**${role.toUpperCase()}:**\\n${txt}\\n`;\n }\n\n case 'conversation':\n return trimLines(compileNodes(node.children, ctx, nextRoleCtx, options));\n\n default:\n // Unknown tag → render children\n return trimLines(compileNodes(node.children, ctx, nextRoleCtx, options));\n }\n}\n\n// ---- main per-item loop\nconst items = $input.all();\nconst out = [];\n\nfor (const item of items) {\n try {\n const i = item.json || {};\n const poml = i.poml;\n if (typeof poml !== 'string' || !poml.trim()) {\n out.push({ json: { error: 'json.poml (string) is required' } });\n continue;\n }\n\n const componentMap = normalizeComponentSpec(i.componentSpec);\n const attrSpec = normalizeAttributeSpec(i.attributeSpec);\n\n const ctx = i.context || {};\n const ast = parsePoml(poml);\n const options = {\n listStyle: i.listStyle || 'dash',\n speakerMode: Boolean(i.speakerMode),\n messages: [],\n componentMap,\n attrSpec,\n };\n\n if (options.speakerMode) {\n compileNodes(ast, ctx, 'system', options); // messages collected\n if (options.messages.length === 0) {\n const txt = trimLines(compileNodes(ast, ctx, 'user', { ...options, speakerMode: false })).trim();\n if (txt) options.messages.push({ role: 'user', content: txt });\n }\n const prompt = options.messages.map(m => `<<${m.role.toUpperCase()}>>\\n${m.content}`).join('\\n\\n');\n out.push({ json: { prompt, messages: options.messages } });\n } else {\n const prompt = trimLines(compileNodes(ast, ctx, 'system', options)).trim();\n out.push({ json: { prompt } });\n }\n } catch (err) {\n out.push({ json: { error: err?.message || String(err) } });\n }\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "8680ce53-e4da-475d-b694-cc2ebb235369",
"name": "AI 代理",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
784,
176
],
"parameters": {
"text": "={{ $json.prompt }}",
"options": {},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "c78e40de-b82a-43a9-8557-15262c79c74c",
"name": "OpenAI 聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
656,
384
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "0NBgNTGVJWuQAji0",
"name": "SNPT - OpenAi account 3"
}
},
"typeVersion": 1.2
},
{
"id": "21df62e0-4746-405e-bd1f-2410e25adb17",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
-768
],
"parameters": {
"color": 7,
"width": 768,
"height": 864,
"content": "# **README**"
},
"typeVersion": 1
},
{
"id": "b0937fbd-1741-4582-8048-fee463d00fa9",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
-768
],
"parameters": {
"color": 7,
"width": 784,
"height": 864,
"content": "## 快速开始(复制粘贴)"
},
"typeVersion": 1
},
{
"id": "e0787480-85c0-4254-8466-aefd93db87f6",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
480
],
"parameters": {
"color": 7,
"width": 1072,
"height": 736,
"content": "## More Examples\n\n**Input item (trimmed):**\n\n```json\n{\n \"poml\": \"<poml> <task>You are given various potential options or approaches for a project. Convert these into a well-structured research plan.</task> <stepwise-instructions> <list listStyle=\\\"decimal\\\"> <item>Identifies Key Objectives <list listStyle=\\\"dash\\\"><item>Clarify what questions each option aims to answer</item><item>Detail the data/info needed for evaluation</item></list></item> … </list> Produce a methodical plan focusing on clear, practical steps. </stepwise-instructions> </poml>\",\n \"context\": {},\n \"speakerMode\": true,\n \"listStyle\": \"dash\",\n \"componentSpec\": { \"...\": \"as above\" },\n \"attributeSpec\": [ \"... as above ...\" ]\n}\n```\n\n**Output item (shape):**\n\n```json\n{\n \"prompt\": \"<<USER>>\\nYou are given various potential options or approaches for a project. Convert these into a well-structured research plan.\\n1. Identifies Key Objectives - Clarify what questions each option aims to answer - Detail the data/info needed for evaluation\\n1. Describes Research Methods …\\n Produce a methodical plan focusing on clear, practical steps.\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"You are given various potential options … steps.\" }\n ]\n}\n```\n\n> Note: For `decimal` lists, Markdown renders `1.` on each line but auto-numbers in viewers — this is intentional."
},
"typeVersion": 1
},
{
"id": "34004c9c-c711-453d-94a5-e9381204fc10",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
112
],
"parameters": {
"color": 3,
"width": 768,
"height": 352,
"content": "## Limitations (by design)\n* No external libraries (template-safe).\n* No POML file includes, scraping, or environment stylesheets.\n* Attribute-level `{{ }}` substitution is **not** enabled by default.\n* The parser is a pragmatic XML-ish tokenizer, not a full POML spec engine.\n\n## Troubleshooting\n* **“json.poml is required”** → Provide a string in the `poml` field.\n* **Got only one `user` message** → Add `<system-msg>`, `<user-msg>`, `<ai-msg>` tags **and** set `speakerMode: true`.\n* **Tables not rendering** → Ensure `records`/`columns` are **valid JSON strings** in the tag attributes.\n* **Unexpected token / syntax** → Ensure you pasted the **entire** Code node (switch/cases included), not partial snippets."
},
"typeVersion": 1
},
{
"id": "4dbba9f0-d648-4e7d-8891-21e68aaf0ed7",
"name": "便签4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
112
],
"parameters": {
"width": 288,
"height": 352,
"content": "## Credits\n\nAuthored by  [**_Real Simple Solutions_**](https://realsimple.dev) as an n8n **template-library-friendly** POML compiler (no dependencies). For full POML feature parity, Contact us for the template with the use Microsoft’s official SDK in a custom workflow.\n\n### View more of our [templates here](https://joeperes.gumroad.com/)"
},
"typeVersion": 1
},
{
"id": "afcd4577-5357-469a-b31b-f13465fea17b",
"name": "‘Execute workflow’",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-32,
176
],
"parameters": {},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "eaac3e3e-35f8-4170-83ac-752a68e3d883",
"connections": {
"Parse_POML": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Set_Variables": {
"main": [
[
{
"node": "Parse_POML",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"‘Execute workflow’": {
"main": [
[
{
"node": "Set_Variables",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
中级 - 工程, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
从GitHub仓库加载提示词并自动填充n8n表达式
从GitHub获取动态提示词并自动填充到n8n表达式提示中
If
Set
Code
+7
17 节点RealSimple Solutions
工程
转录评估器
使用DeepGram和GPT-4o的音频对话分析与可视化
Set
Code
Html
+15
54 节点RealSimple Solutions
人工智能
转录评估器 V2
🔊 浏览器录音音频转录与AI分析(使用Deepgram和GPT-4o)
Set
Code
Html
+15
54 节点RealSimple Solutions
人工智能
🎓 使用并行处理优化速度关键工作流(扇出-扇入)
🎓 使用并行处理(扇出/扇入)优化速度关键工作流
If
Set
Code
+10
34 节点Lucas Peyrin
工程
内容生成器 v3
AI驱动博客自动化:使用GPT-4生成并发布SEO文章至WordPress和Twitter
If
Set
Code
+25
144 节点Jay Emp0
内容创作
支持文本、语音、图像和PDF的AI驱动WhatsApp聊天机器人(RAG)
支持文本、语音、图像和PDF的AI驱动WhatsApp聊天机器人(RAG)
Set
Code
Switch
+15
35 节点NovaNode
工程