8
n8n 中文网amn8n.com

解析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![${alt||''}](${src})\\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 Logo](https://rss.imagecdn.realsimple.dev/Logos/RealSimpleLogo-nobg-Light.png) [**_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)可能需要您自行付费。

工作流信息
难度等级
中级
节点数量10
分类2
节点类型6
难度说明

适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

作者
外部链接
在 n8n.io 查看

分享此工作流