{
  "name": "Postagem Redes \u2014 Portal: A\u00e7\u00f5es",
  "description": null,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "content": "## Produ\u00e7\u00e3o controlada\n\n**Uma fila, quatro redes, tr\u00eas provedores de IA.**\n\n- A\u00e7\u00f5es do portal sempre exigem revis\u00e3o humana.\n- A fila s\u00f3 dispara quando SOCIAL_PUBLISH_ENABLED=true.\n- Cada entrega \u00e9 reservada antes da chamada externa; falhas recebem at\u00e9 3 tentativas com espera exponencial.\n- Credenciais ficam somente no cofre do n8n, nunca neste workflow.",
        "height": 270,
        "width": 550,
        "color": 6
      },
      "name": "Orquestrador de publica\u00e7\u00e3o",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -656,
        -464
      ]
    },
    {
      "parameters": {
        "content": "## 01 \u00b7 Portal e IA\n\nA sugest\u00e3o nunca publica conte\u00fado.\n\nOpenAI \u00e9 o provedor prim\u00e1rio; Gemini e Ollama s\u00f3 entram quando a vari\u00e1vel de fallback correspondente estiver habilitada.",
        "height": 180,
        "width": 510,
        "color": 4
      },
      "name": "01 \u00b7 Portal e IA",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        192,
        -464
      ]
    },
    {
      "parameters": {
        "content": "## 02 \u00b7 Fila, reserva e valida\u00e7\u00e3o\n\nCada destino recebe um `dispatchId` antes da chamada externa. A trava global e as vari\u00e1veis por rede s\u00e3o verificadas antes de publicar.",
        "height": 185,
        "width": 560,
        "color": 5
      },
      "name": "02 \u00b7 Fila protegida",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -656,
        352
      ]
    },
    {
      "parameters": {
        "content": "## 03 \u00b7 APIs oficiais\n\nCada faixa segue a regra pr\u00f3pria da plataforma. HTTP Request fica apenas onde n\u00e3o h\u00e1 opera\u00e7\u00e3o nativa completa no n8n.",
        "height": 165,
        "width": 500,
        "color": 2
      },
      "name": "03 \u00b7 Publicadores por rede",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1552,
        128
      ]
    },
    {
      "parameters": {
        "content": "## 04 \u00b7 Resultado por entrega\n\nSucesso e falha s\u00e3o gravados no estado e no Ledger nativo. Falhas t\u00eam at\u00e9 tr\u00eas tentativas com espera exponencial.",
        "height": 170,
        "width": 520,
        "color": 3
      },
      "name": "04 \u00b7 Resultado e recupera\u00e7\u00e3o",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2704,
        512
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "postagem-redes-api",
        "responseMode": "responseNode",
        "options": {}
      },
      "name": "Webhook do portal",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -624,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst item=$input.first(); const body=item.json.body||item.json||{}; const action=safeText(body.action,30); const operator=safeText(body.operator,80); if(!operator) throw new Error('Informe o nome do respons\u00e1vel.');\nconst now=new Date().toISOString(); const {state,items}=scan();\nif(action==='generate'){\n const id=safeText(body.contentId,130); const content=items.find(x=>x.id===id); if(!content) throw new Error('Conte\u00fado n\u00e3o encontrado. Atualize a p\u00e1gina.');\n if(String($env.SOCIAL_AI_ENABLED||'').toLowerCase()!=='true') return [{json:{ok:false,route:'respond',message:'A IA est\u00e1 desativada. Configure SOCIAL_AI_ENABLED=true e a credencial OpenAI no workflow Portal: A\u00e7\u00f5es.'}}];\n const brief=safeText(body.brief??content.brief,1600); const prompt=['Voc\u00ea \u00e9 redator de marketing B2B industrial em portugu\u00eas do Brasil.','Crie apenas uma sugest\u00e3o para revis\u00e3o humana; n\u00e3o invente especifica\u00e7\u00f5es, certifica\u00e7\u00f5es, clientes, pre\u00e7os ou resultados.','Retorne JSON v\u00e1lido sem markdown com as chaves: baseCaption (string), variants (objeto com instagram, facebook, linkedin e xThread; xThread \u00e9 array de 1 a 4 strings), hashtags (array at\u00e9 12), reviewNotes (array at\u00e9 5).','Adapte tom e tamanho \u00e0 rede. Inclua CTA discreto quando fizer sentido.','T\u00edtulo interno: '+content.title,'Brief: '+brief,'Legenda atual: '+content.caption,'Quantidade de slides: '+content.slides.length].join('\\n');\n return [{json:{route:'ai',contentId:id,operator,brief,aiPrompt:prompt}}];\n}\nif(action==='upload'){\n const title=safeText(body.title,120); const caption=safeText(body.caption,5000); if(!title||!caption) throw new Error('T\u00edtulo e legenda s\u00e3o obrigat\u00f3rios.');\n let order=[]; try{order=JSON.parse(body.order||'[]')}catch{} const rank=new Map(order.map((key,index)=>[String(key),index]));\n const entries=Object.entries(item.binary||{}).filter(([,file])=>['image/png','image/jpeg','image/webp'].includes(String(file.mimeType||'').toLowerCase())); if(entries.length<1||entries.length>10) throw new Error('Envie de 1 a 10 imagens PNG, JPG ou WEBP.');\n entries.sort(([a],[b])=>(rank.get(a)??999)-(rank.get(b)??999)||a.localeCompare(b));\n const base='rapido-'+Date.now()+'-'+slug(title).slice(0,24); const dir=path.join(INPUT,base); fs.mkdirSync(dir,{recursive:false}); try{for(let index=0;index<entries.length;index++){const [binaryName,file]=entries[index];const ext=file.mimeType==='image/png'?'.png':file.mimeType==='image/webp'?'.webp':'.jpg';const buffer=await this.helpers.getBinaryDataBuffer(0,binaryName);fs.writeFileSync(path.join(dir,String(index+1).padStart(2,'0')+ext),buffer)}}catch(error){try{fs.rmSync(dir,{recursive:true,force:true})}catch{}throw new Error('N\u00e3o foi poss\u00edvel gravar as imagens enviadas. Tente novamente.')} fs.writeFileSync(path.join(dir,'Texto.txt'),caption,'utf8'); const id=slug(base); state.records[id]={title,caption,status:'pendente',networks:[],deliveries:{},updatedAt:now,audit:[{at:now,operator,action:'criou conte\u00fado r\u00e1pido',comment:entries.length+' slide(s) organizado(s) antes do envio.'}]}; saveState(state); return [{json:{ok:true,route:'respond',message:'Publica\u00e7\u00e3o criada e enviada para aprova\u00e7\u00e3o.',id}}];\n}\nconst id=safeText(body.contentId,130); const content=items.find(x=>x.id===id); if(!content) throw new Error('Conte\u00fado n\u00e3o encontrado. Atualize a p\u00e1gina.'); const previous=state.records[id]||{};\nif(action==='reorder'){\n let requested=[]; try{requested=JSON.parse(body.slides||'[]')}catch{} if(!Array.isArray(requested)||requested.length!==content.slides.length||new Set(requested).size!==content.slides.length||requested.some(file=>!content.slides.includes(file))) throw new Error('A nova ordem de slides \u00e9 inv\u00e1lida. Atualize a p\u00e1gina e tente novamente.');\n const dir=path.join(INPUT,content.folder); const token=Date.now()+'-'+Math.random().toString(36).slice(2,8); const moves=requested.map((file,index)=>({from:path.join(dir,file),temp:path.join(dir,'.reordenar-'+token+'-'+index+path.extname(file)),to:path.join(dir,String(index+1).padStart(2,'0')+path.extname(file))}));\n try{moves.forEach(move=>fs.renameSync(move.from,move.temp));moves.forEach(move=>fs.renameSync(move.temp,move.to))}catch(error){try{moves.forEach(move=>{if(fs.existsSync(move.temp))fs.renameSync(move.temp,move.from)})}catch{}throw new Error('N\u00e3o foi poss\u00edvel reorganizar os slides. Nenhuma decis\u00e3o foi salva.');}\n state.records[id]={...previous,title:content.title,caption:content.caption,brief:content.brief||'',status:content.status,networks:content.networks||[],scheduleAt:content.scheduleAt||'',updatedAt:now,audit:[...(previous.audit||[]),{at:now,operator,action:'reorganizou slides',comment:'Ordem do carrossel atualizada no portal.'}]}; saveState(state); return [{json:{ok:true,route:'respond',message:'Ordem do carrossel atualizada.'}}];\n}\nif(action!=='save') throw new Error('A\u00e7\u00e3o n\u00e3o reconhecida.'); const title=safeText(body.title,120); const caption=safeText(body.caption,5000); const brief=safeText(body.brief,1600); const status=safeText(body.status,30); if(!title) throw new Error('Informe o t\u00edtulo interno da publica\u00e7\u00e3o.'); if(!allowedStatuses.has(status)||['publicado','parcial','falhou'].includes(status)) throw new Error('Status inv\u00e1lido.'); if(!caption) throw new Error('A legenda n\u00e3o pode ficar vazia.'); if(status==='agendado'&&!safeText(body.scheduleAt,40)) throw new Error('Informe data e hora para o agendamento.'); const rawNetworks=body.networks??[]; let networks=[]; if(Array.isArray(rawNetworks)) networks=rawNetworks; else if(typeof rawNetworks==='string'){const normalized=rawNetworks.trim(); try{networks=JSON.parse(normalized)}catch{networks=normalized.split(',')}} else networks=[rawNetworks]; if(!Array.isArray(networks)) networks=[networks]; networks=[...new Set(networks.map(x=>safeText(x,20)).filter(x=>['instagram','facebook','linkedin','x'].includes(x)))]; if((status==='aprovado'||status==='agendado')&&!networks.length) throw new Error('Selecione pelo menos uma rede.'); const comment=safeText(body.comment,1000); const deliveries={}; for(const network of networks){const prior=previous.deliveries?.[network]||{}; deliveries[network]={status:(status==='aprovado'||status==='agendado')?'queued':'draft',attempts:0,queuedAt:(status==='aprovado'||status==='agendado')?now:'',lastRemoteId:'',lastPermalink:'',lastError:'',...((prior.status==='published'&&previous.caption===caption&&previous.title===title)?prior:{})};} state.records[id]={...previous,title,caption,brief,status,networks,deliveries,scheduleAt:status==='agendado'?safeText(body.scheduleAt,40):'',updatedAt:now,audit:[...(previous.audit||[]),{at:now,operator,action:status,comment,networks}]}; saveState(state); return [{json:{ok:true,route:'respond',message:status==='aprovado'?'Conte\u00fado aprovado e adicionado \u00e0 fila de homologa\u00e7\u00e3o.':status==='agendado'?'Conte\u00fado aprovado e agendado para a fila.':'Atualiza\u00e7\u00e3o salva com sucesso.'}}];"
      },
      "name": "Processar a\u00e7\u00e3o do portal",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -384,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "ai",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Gerar IA"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "respond",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Responder"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none",
          "ignoreCase": false
        }
      },
      "name": "Roteador da a\u00e7\u00e3o",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        -128,
        0
      ]
    },
    {
      "parameters": {
        "modelId": {
          "mode": "id",
          "value": "={{ $env.SOCIAL_AI_MODEL || \"gpt-5-mini\" }}"
        },
        "responses": {
          "values": [
            {
              "content": "={{ $json.aiPrompt }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {
          "instructions": "Retorne somente JSON v\u00e1lido conforme o pedido; n\u00e3o use markdown.",
          "maxTokens": 1800
        }
      },
      "name": "OpenAI \u00b7 sugest\u00e3o prim\u00e1ria",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.3,
      "position": [
        144,
        -144
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ String($env.SOCIAL_AI_GEMINI_FALLBACK_ENABLED || \"\").toLowerCase() === \"true\" }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Fallback Gemini habilitado?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        400,
        -80
      ]
    },
    {
      "parameters": {
        "modelId": {
          "mode": "id",
          "value": "={{ $env.SOCIAL_GEMINI_MODEL || \"gemini-3.5-flash\" }}"
        },
        "messages": {
          "values": [
            {
              "content": "={{ $(\"Processar a\u00e7\u00e3o do portal\").item.json.aiPrompt }}"
            }
          ]
        },
        "jsonOutput": true,
        "builtInTools": {},
        "options": {
          "systemMessage": "Retorne somente JSON v\u00e1lido conforme o pedido; n\u00e3o use markdown.",
          "maxOutputTokens": 1800
        }
      },
      "name": "Gemini \u00b7 fallback",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1.2,
      "position": [
        640,
        -160
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ String($env.SOCIAL_AI_OLLAMA_FALLBACK_ENABLED || \"\").toLowerCase() === \"true\" }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Fallback Ollama habilitado?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        912,
        -64
      ]
    },
    {
      "parameters": {
        "modelId": {
          "mode": "id",
          "value": "={{ $env.SOCIAL_OLLAMA_MODEL || \"llama3.2\" }}"
        },
        "messages": {
          "values": [
            {
              "content": "={{ $(\"Processar a\u00e7\u00e3o do portal\").item.json.aiPrompt }}"
            }
          ]
        },
        "options": {
          "system": "Retorne somente JSON v\u00e1lido conforme o pedido; n\u00e3o use markdown.",
          "temperature": 0.3,
          "num_predict": 1800
        }
      },
      "name": "Ollama \u00b7 fallback local",
      "type": "@n8n/n8n-nodes-langchain.ollama",
      "typeVersion": 1,
      "position": [
        1152,
        -160
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst request=$items('Processar')[0]?.json||{}; const id=safeText(request.contentId,130); const operator=safeText(request.operator,80)||'Sistema'; const {state,items}=scan(); const content=items.find(x=>x.id===id); if(!content) throw new Error('Conte\u00fado n\u00e3o encontrado ao salvar a sugest\u00e3o.');\n const candidate=$json.output_text||$json.text||$json.content||$json.output?.flatMap(x=>x.content||[]).map(x=>x.text||'').join('')||$json.candidates?.[0]?.content?.parts?.map(x=>x.text||'').join('')||''; let draft; try{draft=JSON.parse(candidate)}catch{throw new Error('A IA n\u00e3o retornou um JSON v\u00e1lido. Nenhuma legenda foi alterada.')}\nconst text=v=>safeText(v,5000); const variants=draft&&typeof draft.variants==='object'?draft.variants:{}; const normalized={baseCaption:text(draft.baseCaption),variants:{instagram:text(variants.instagram),facebook:text(variants.facebook),linkedin:text(variants.linkedin),xThread:Array.isArray(variants.xThread)?variants.xThread.map(v=>text(v,280)).filter(Boolean).slice(0,4):[]},hashtags:Array.isArray(draft.hashtags)?draft.hashtags.map(v=>text(v,80)).filter(Boolean).slice(0,12):[],reviewNotes:Array.isArray(draft.reviewNotes)?draft.reviewNotes.map(v=>text(v,240)).filter(Boolean).slice(0,5):[],generatedAt:new Date().toISOString(),model:safeText($json.model||$json.model_id||$env.SOCIAL_AI_MODEL||'OpenAI',80)}; if(!normalized.baseCaption) throw new Error('A IA n\u00e3o retornou uma legenda-base v\u00e1lida. Nenhuma altera\u00e7\u00e3o foi salva.');\nconst previous=state.records[id]||{}; const now=new Date().toISOString(); state.records[id]={...previous,brief:safeText(request.brief,1600),aiDraft:normalized,updatedAt:now,audit:[...(previous.audit||[]),{at:now,operator,action:'gerou sugest\u00e3o IA',comment:'Rascunho salvo para revis\u00e3o humana; a legenda atual n\u00e3o foi substitu\u00edda.'}]}; saveState(state); return [{json:{ok:true,route:'respond',message:'Sugest\u00e3o da IA salva para revis\u00e3o. A legenda atual n\u00e3o foi alterada.',draft:normalized}}];"
      },
      "name": "Validar e salvar sugest\u00e3o IA",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        -144
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "return [{json:{ok:false,route:'respond',message:'N\u00e3o foi poss\u00edvel gerar a sugest\u00e3o. Revise as credenciais OpenAI/Gemini/Ollama ou tente novamente.'}}];"
      },
      "name": "Registrar indisponibilidade da IA",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1424,
        16
      ]
    },
    {
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Cache-Control",
                "value": "no-store"
              }
            ]
          }
        }
      },
      "name": "Responder ao portal",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1696,
        0
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes"
            }
          ]
        }
      },
      "name": "Schedule Trigger \u00b7 a cada 5 minutos",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -720,
        624
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst enabled=String($env.SOCIAL_PUBLISH_ENABLED||'').toLowerCase()==='true'; if(!enabled) return [];\nconst now=new Date(); const nowIso=now.toISOString(); const {state,items}=scan(); const outgoing=[]; const publicMediaBase=String($env.SOCIAL_PUBLIC_MEDIA_BASE_URL||'').replace(/\\/+$/,''); const requireSignature=String($env.SOCIAL_MEDIA_REQUIRE_SIGNED_URLS||'').toLowerCase()==='true'; const signingSecret=String($env.SOCIAL_MEDIA_SIGNING_SECRET||'');\nfunction signedAssetUrl(contentId,file){if(!publicMediaBase) return ''; const base=publicMediaBase+'?id='+encodeURIComponent(contentId)+'&file='+encodeURIComponent(file); if(!requireSignature) return base; if(!signingSecret) throw new Error('SOCIAL_MEDIA_SIGNING_SECRET \u00e9 obrigat\u00f3rio quando SOCIAL_MEDIA_REQUIRE_SIGNED_URLS=true.'); const exp=Math.floor(Date.now()/1000)+7200; const sig=crypto.createHmac('sha256',signingSecret).update(contentId+':'+file+':'+exp).digest('base64url'); return base+'&exp='+exp+'&sig='+encodeURIComponent(sig)}\nfor(const content of items){\n const due=content.status==='aprovado'||(content.status==='agendado'&&content.scheduleAt&&new Date(content.scheduleAt)<=now); if(!due) continue;\n for(const network of content.networks||[]){ const delivery=state.records[content.id]?.deliveries?.[network]; if(!delivery) continue;\n  const retryAt=delivery.retryAt?new Date(delivery.retryAt):null; const eligible=['queued','retry'].includes(delivery.status)&&(!retryAt||retryAt<=now); if(!eligible) continue;\n  delivery.status='dispatching'; delivery.attempts=Number(delivery.attempts||0)+1; delivery.dispatchId=crypto.randomUUID(); delivery.lastAttemptAt=nowIso; delivery.lastError='';\n  const variant=network==='x'?(content.aiDraft?.variants?.xThread||[]):safeText(content.aiDraft?.variants?.[network]||content.caption,5000);\n  const assetUrls=Object.fromEntries((content.slides||[]).map(file=>[file,signedAssetUrl(content.id,file)])); outgoing.push({json:{contentId:content.id,folder:content.folder,title:content.title,caption:content.caption,brief:content.brief||'',network,slides:content.slides,assetVersion:content.assetVersion,variant,dispatchId:delivery.dispatchId,attempt:delivery.attempts,publicMediaBase,assetUrls}});\n }\n if(outgoing.length) break;\n}\nif(outgoing.length){for(const payload of outgoing){const record=state.records[payload.json.contentId]; record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:'reservou publica\u00e7\u00e3o',comment:'Entrega '+payload.json.network+' reservada com idempot\u00eancia.',network:payload.json.network,dispatchId:payload.json.dispatchId}]} saveState(state)}\nreturn outgoing;"
      },
      "name": "Reservar entregas eleg\u00edveis",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -384,
        624
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst config=$env; const now=new Date().toISOString(); const outputs=[]; const blocked=[];\nfor(const [inputIndex,inputItem] of $input.all().entries()){\n  const payload=inputItem.json; const network=safeText(payload.network,30);\n  const requirements={instagram:['SOCIAL_META_ENABLED','SOCIAL_META_INSTAGRAM_ACCOUNT_ID','SOCIAL_PUBLIC_MEDIA_BASE_URL'],facebook:['SOCIAL_META_ENABLED','SOCIAL_META_PAGE_ID','SOCIAL_PUBLIC_MEDIA_BASE_URL'],linkedin:['SOCIAL_LINKEDIN_ENABLED','SOCIAL_LINKEDIN_ORGANIZATION_URN'],x:['SOCIAL_X_ENABLED']}[network]||[];\n  const missing=requirements.filter(key=>!safeText(config[key],400));\n  const featureFlag={instagram:'SOCIAL_META_ENABLED',facebook:'SOCIAL_META_ENABLED',linkedin:'SOCIAL_LINKEDIN_ENABLED',x:'SOCIAL_X_ENABLED'}[network];\n  if(featureFlag&&String(config[featureFlag]||'').trim().toLowerCase()!=='true'&&!missing.includes(featureFlag)) missing.unshift(featureFlag);\n  if(!missing.length){outputs.push({json:payload,pairedItem:{item:inputIndex}});continue;}\n  blocked.push({payload,network,missing});\n}\nif(blocked.length){\n  const {state}=scan(); let changed=false;\n  for(const entry of blocked){\n    const delivery=state.records?.[entry.payload.contentId]?.deliveries?.[entry.network];\n    if(delivery&&delivery.dispatchId===entry.payload.dispatchId){\n      delivery.status='blocked'; delivery.retryAt=''; delivery.lastError='Configura\u00e7\u00e3o pendente: '+entry.missing.join(', ');\n      const record=state.records[entry.payload.contentId]; reconcileRecordStatus(record); record.updatedAt=now;\n      record.audit=[...(record.audit||[]),{at:now,operator:'Sistema',action:'bloqueou publica\u00e7\u00e3o',comment:delivery.lastError,network:entry.network,dispatchId:entry.payload.dispatchId}];\n      changed=true;\n    }\n  }\n  if(changed) saveState(state);\n}\nreturn outputs;"
      },
      "name": "Pr\u00e9-validar publica\u00e7\u00e3o",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        96,
        624
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.network }}",
                    "rightValue": "instagram",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "1df2c2e8-7ebf-4630-a643-ac813db4271e"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Instagram"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.network }}",
                    "rightValue": "facebook",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "2453b5e4-a6ba-4f7a-9619-d940c84383ea"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Facebook"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.network }}",
                    "rightValue": "linkedin",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "07b2e0a8-8ff8-4ca0-b10d-487ffe2e51b6"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "LinkedIn"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.network }}",
                    "rightValue": "x",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "107cc222-a1e4-44bf-b91e-5d132d3859f0"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "X / thread"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none",
          "ignoreCase": false
        }
      },
      "name": "Roteador por rede",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        352,
        624
      ]
    },
    {
      "parameters": {
        "jsCode": "const parent = $('Roteador por rede').item.json;\n\nif (!Array.isArray(parent.slides) || parent.slides.length < 2 || parent.slides.length > 10) {\n  throw new Error('O carrossel do Instagram deve ter entre 2 e 10 imagens.');\n}\n\nif (!parent.publicMediaBase || !String(parent.publicMediaBase).startsWith('https://')) {\n  throw new Error('Instagram requer uma URL p\u00fablica HTTPS para as imagens.');\n}\n\nreturn parent.slides.map((file, index) => {\n  const url = parent.assetUrls?.[file];\n\n  if (!url || !String(url).startsWith('https://')) {\n    throw new Error(`N\u00e3o foi poss\u00edvel gerar a URL segura da imagem ${file}.`);\n  }\n\n  return {\n    json: {\n      ...parent,\n      file,\n      url,\n      slideIndex: index,\n    },\n    pairedItem: { item: 0 },\n  };\n});"
      },
      "name": "Instagram \u00b7 preparar slides",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        624,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_INSTAGRAM_ACCOUNT_ID+\"/media\" }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "image_url",
              "value": "={{ $json.url }}"
            },
            {
              "name": "is_carousel_item",
              "value": "true"
            }
          ]
        },
        "options": {
          "timeout": 60000
        }
      },
      "name": "Instagram \u00b7 criar containers",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        864,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const parent=$('Pr\u00e9-validar publica\u00e7\u00e3o').first().json; const children=$input.all().map(item=>item.json.id).filter(Boolean); if(children.length!==parent.slides.length) throw new Error('A Meta n\u00e3o retornou todos os containers do carrossel.'); return [{json:{...parent,children},pairedItem:{item:0}}];"
      },
      "name": "Instagram \u00b7 reunir containers",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_INSTAGRAM_ACCOUNT_ID+\"/media\" }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "media_type",
              "value": "CAROUSEL"
            },
            {
              "name": "children",
              "value": "={{ $json.children.join(\",\") }}"
            },
            {
              "name": "caption",
              "value": "={{ $json.variant || $json.caption }}"
            }
          ]
        },
        "options": {
          "timeout": 60000
        }
      },
      "name": "Instagram \u00b7 criar carrossel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1344,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "amount": 30
      },
      "name": "Instagram \u00b7 aguardar processamento",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1584,
        352
      ]
    },
    {
      "parameters": {
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$(\"Instagram \u00b7 criar carrossel\").item.json.id+\"?fields=status_code\" }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
            }
          ]
        },
        "options": {
          "timeout": 60000
        }
      },
      "name": "Instagram \u00b7 consultar status do carrossel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1824,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const status=String($json.status_code||'').toUpperCase(); if(status!=='FINISHED') throw new Error('O carrossel do Instagram ainda n\u00e3o terminou de processar (status: '+(status||'indispon\u00edvel')+'). A entrega ser\u00e1 tentada novamente.'); return $input.all();"
      },
      "name": "Instagram \u00b7 validar processamento",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2064,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_INSTAGRAM_ACCOUNT_ID+\"/media_publish\" }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "creation_id",
              "value": "={{ $(\"Instagram \u00b7 criar carrossel\").item.json.id }}"
            }
          ]
        },
        "options": {
          "timeout": 60000
        }
      },
      "name": "Instagram \u00b7 publicar carrossel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2304,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const parent = $('Roteador por rede').item.json;\n\nif (!Array.isArray(parent.slides) || parent.slides.length < 1) {\n  throw new Error('A publica\u00e7\u00e3o do Facebook precisa ter pelo menos uma imagem.');\n}\n\nif (!parent.publicMediaBase || !String(parent.publicMediaBase).startsWith('https://')) {\n  throw new Error('Facebook requer uma URL p\u00fablica HTTPS para as imagens neste fluxo.');\n}\n\nreturn parent.slides.map((file, index) => {\n  const url = parent.assetUrls?.[file];\n\n  if (!url || !String(url).startsWith('https://')) {\n    throw new Error(`N\u00e3o foi poss\u00edvel gerar a URL segura da imagem ${file}.`);\n  }\n\n  return {\n    json: {\n      ...parent,\n      file,\n      url,\n      slideIndex: index,\n    },\n    pairedItem: { item: 0 },\n  };\n});"
      },
      "name": "Facebook \u00b7 preparar slides",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        624,
        560
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_PAGE_ID+\"/photos\" }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ \"Bearer \"+$(\"Facebook \u00b7 token da P\u00e1gina\").first().json.access_token }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $json.url }}"
            },
            {
              "name": "published",
              "value": "false"
            }
          ]
        },
        "options": {
          "timeout": 60000
        }
      },
      "name": "Facebook \u00b7 enviar fotos n\u00e3o publicadas",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        864,
        560
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const parent=$('Pr\u00e9-validar publica\u00e7\u00e3o').first().json; const media=$input.all().map(item=>item.json.id).filter(Boolean).map(media_fbid=>({media_fbid})); if(media.length!==parent.slides.length) throw new Error('A Meta n\u00e3o retornou todas as fotos n\u00e3o publicadas do Facebook.'); return [{json:{...parent,attachedMedia:media},pairedItem:{item:0}}];"
      },
      "name": "Facebook \u00b7 reunir IDs das fotos",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        560
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_PAGE_ID+\"/feed\" }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ \"Bearer \"+$(\"Facebook \u00b7 token da P\u00e1gina\").first().json.access_token }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ message: $json.variant || $json.caption, attached_media: $json.attachedMedia }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Facebook \u00b7 publicar carrossel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1344,
        560
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const parent=$input.first().json; if(!parent.slides.length) throw new Error('N\u00e3o h\u00e1 imagens para enviar ao LinkedIn.'); return parent.slides.map((file,index)=>({json:{...parent,file,filePath:'/files/postagem-redes/entrada/'+parent.folder+'/'+file},pairedItem:{item:0}}));"
      },
      "name": "LinkedIn \u00b7 preparar arquivos",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        624,
        784
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "fileSelector": "={{ $json.filePath }}",
        "options": {
          "dataPropertyName": "data"
        }
      },
      "name": "LinkedIn \u00b7 ler imagem local",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        864,
        784
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "={{ $json.value.uploadUrl }}",
        "sendBody": true,
        "contentType": "raw",
        "options": {
          "timeout": 60000
        }
      },
      "name": "LinkedIn \u00b7 enviar bin\u00e1rio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1344,
        784
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const parent=$('Pr\u00e9-validar publica\u00e7\u00e3o').first().json; const images=$('Inicializar upload LinkedIn').all().map(item=>item.json.value?.image||item.json.image||item.json.id).filter(Boolean); if(images.length!==parent.slides.length) throw new Error('O LinkedIn n\u00e3o retornou URNs para todas as imagens.'); return [{json:{...parent,images},pairedItem:{item:0}}];"
      },
      "name": "LinkedIn \u00b7 reunir URNs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1584,
        784
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const payload=$input.first().json; const thread=Array.isArray(payload.variant)?payload.variant.filter(Boolean):[]; const chunks=(thread.length?thread:[payload.caption]).map(value=>String(value).trim()).filter(Boolean).slice(0,4); if(!chunks.length) throw new Error('N\u00e3o h\u00e1 texto para a thread do X.'); return [{json:{...payload,chunks},pairedItem:{item:0}}];"
      },
      "name": "X \u00b7 adaptar sequ\u00eancia",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        624,
        1024
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "fileSelector": "={{ \"/files/postagem-redes/entrada/\"+$json.folder+\"/\"+$json.slides[0] }}",
        "options": {
          "dataPropertyName": "data"
        }
      },
      "name": "X \u00b7 ler primeira imagem",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        864,
        1024
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.x.com/2/media/upload",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "twitterOAuth2Api",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "media",
              "inputDataFieldName": "data"
            },
            {
              "name": "media_category",
              "value": "tweet_image"
            },
            {
              "name": "media_type",
              "value": "image/png"
            }
          ]
        },
        "options": {
          "timeout": 60000
        }
      },
      "name": "X \u00b7 enviar m\u00eddia v2",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1104,
        1024
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[0] }}",
        "additionalFields": {
          "attachments": "={{ $(\"X \u00b7 enviar m\u00eddia v2\").item.json.data.id }}"
        }
      },
      "name": "X \u00b7 publicar post inicial",
      "type": "n8n-nodes-base.twitter",
      "typeVersion": 2,
      "position": [
        1344,
        1024
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks.length >= 2 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "X \u00b7 h\u00e1 resposta 2?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1584,
        992
      ]
    },
    {
      "parameters": {
        "text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[1] }}",
        "additionalFields": {
          "inReplyToStatusId": {
            "mode": "id",
            "value": "={{ $json.data.id }}"
          }
        }
      },
      "name": "X \u00b7 publicar resposta 2",
      "type": "n8n-nodes-base.twitter",
      "typeVersion": 2,
      "position": [
        1824,
        944
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks.length >= 3 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "X \u00b7 h\u00e1 resposta 3?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2064,
        992
      ]
    },
    {
      "parameters": {
        "text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[2] }}",
        "additionalFields": {
          "inReplyToStatusId": {
            "mode": "id",
            "value": "={{ $json.data.id }}"
          }
        }
      },
      "name": "X \u00b7 publicar resposta 3",
      "type": "n8n-nodes-base.twitter",
      "typeVersion": 2,
      "position": [
        2304,
        944
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks.length >= 4 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "X \u00b7 h\u00e1 resposta 4?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2544,
        992
      ]
    },
    {
      "parameters": {
        "text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[3] }}",
        "additionalFields": {
          "inReplyToStatusId": {
            "mode": "id",
            "value": "={{ $json.data.id }}"
          }
        }
      },
      "name": "X \u00b7 publicar resposta 4",
      "type": "n8n-nodes-base.twitter",
      "typeVersion": 2,
      "position": [
        2784,
        944
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "dataTableId": {
          "mode": "name",
          "value": "Postagem Redes - Ledger"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "content_id": "={{ $json.contentId }}",
            "network": "={{ $json.network }}",
            "status": "={{ $json.ok ? \"published\" : \"failed\" }}",
            "remote_id": "={{ $json.remoteId || \"\" }}",
            "permalink": "={{ $json.permalink || \"\" }}",
            "error": "={{ $json.error || \"\" }}",
            "dispatch_id": "={{ $json.dispatchId }}",
            "occurred_at": "={{ $now.toISO() }}"
          }
        },
        "options": {
          "optimizeBulk": true
        }
      },
      "name": "Registrar no Data Table \u00b7 Ledger",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        3152,
        1040
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='instagram'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date().toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; if(!delivery||delivery.dispatchId!==payload.dispatchId) throw new Error('A confirma\u00e7\u00e3o recebida n\u00e3o corresponde \u00e0 entrega reservada.'); const remoteId=safeText($json.id||$json.data?.id||$json.postId||$json.value?.id||'',200); const permalink=safeText($json.permalink_url||$json.permalink||'',1000); delivery.status='published'; delivery.publishedAt=now; delivery.retryAt=''; delivery.lastRemoteId=remoteId; delivery.lastPermalink=permalink; delivery.lastError=''; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=now; record.audit=[...(record.audit||[]),{at:now,operator:'Sistema',action:'publicou conte\u00fado',comment:'Confirma\u00e7\u00e3o recebida da API.',network:payload.network,dispatchId:payload.dispatchId,remoteId}]; saveState(state); return [{json:{...payload,ok:true,remoteId,permalink}}];"
      },
      "name": "Registrar publica\u00e7\u00e3o \u00b7 instagram",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        832
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='instagram'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date(); const nowIso=now.toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; const providerError=$json.error||{}; const raw=safeText(providerError.description||providerError.message||providerError.context?.data?.detail||providerError.context?.data?.title||$json.description||$json.message||'Falha sem detalhe retornado pela rede.',500); if(delivery&&delivery.dispatchId===payload.dispatchId){const attempts=Number(delivery.attempts||1); const canRetry=payload.network!=='x'&&attempts<3; delivery.status=canRetry?'retry':'failed'; delivery.retryAt=canRetry?new Date(now.getTime()+Math.min(60,2**attempts)*60000).toISOString():''; delivery.lastError=raw; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:canRetry?'agendou nova tentativa':payload.network==='x'?'registrou falha do X sem nova tentativa':'falhou publica\u00e7\u00e3o',comment:raw,network:payload.network,dispatchId:payload.dispatchId}]; saveState(state)} return [{json:{...payload,ok:false,error:raw}}];"
      },
      "name": "Registrar falha \u00b7 instagram",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        944
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='facebook'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date().toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; if(!delivery||delivery.dispatchId!==payload.dispatchId) throw new Error('A confirma\u00e7\u00e3o recebida n\u00e3o corresponde \u00e0 entrega reservada.'); const remoteId=safeText($json.id||$json.data?.id||$json.postId||$json.value?.id||'',200); const permalink=safeText($json.permalink_url||$json.permalink||'',1000); delivery.status='published'; delivery.publishedAt=now; delivery.retryAt=''; delivery.lastRemoteId=remoteId; delivery.lastPermalink=permalink; delivery.lastError=''; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=now; record.audit=[...(record.audit||[]),{at:now,operator:'Sistema',action:'publicou conte\u00fado',comment:'Confirma\u00e7\u00e3o recebida da API.',network:payload.network,dispatchId:payload.dispatchId,remoteId}]; saveState(state); return [{json:{...payload,ok:true,remoteId,permalink}}];"
      },
      "name": "Registrar publica\u00e7\u00e3o \u00b7 facebook",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1056
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='facebook'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date(); const nowIso=now.toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; const providerError=$json.error||{}; const raw=safeText(providerError.description||providerError.message||providerError.context?.data?.detail||providerError.context?.data?.title||$json.description||$json.message||'Falha sem detalhe retornado pela rede.',500); if(delivery&&delivery.dispatchId===payload.dispatchId){const attempts=Number(delivery.attempts||1); const canRetry=payload.network!=='x'&&attempts<3; delivery.status=canRetry?'retry':'failed'; delivery.retryAt=canRetry?new Date(now.getTime()+Math.min(60,2**attempts)*60000).toISOString():''; delivery.lastError=raw; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:canRetry?'agendou nova tentativa':payload.network==='x'?'registrou falha do X sem nova tentativa':'falhou publica\u00e7\u00e3o',comment:raw,network:payload.network,dispatchId:payload.dispatchId}]; saveState(state)} return [{json:{...payload,ok:false,error:raw}}];"
      },
      "name": "Registrar falha \u00b7 facebook",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1168
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='linkedin'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date().toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; if(!delivery||delivery.dispatchId!==payload.dispatchId) throw new Error('A confirma\u00e7\u00e3o recebida n\u00e3o corresponde \u00e0 entrega reservada.'); const remoteId=safeText($json.id||$json.data?.id||$json.postId||$json.value?.id||'',200); const permalink=safeText($json.permalink_url||$json.permalink||'',1000); delivery.status='published'; delivery.publishedAt=now; delivery.retryAt=''; delivery.lastRemoteId=remoteId; delivery.lastPermalink=permalink; delivery.lastError=''; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=now; record.audit=[...(record.audit||[]),{at:now,operator:'Sistema',action:'publicou conte\u00fado',comment:'Confirma\u00e7\u00e3o recebida da API.',network:payload.network,dispatchId:payload.dispatchId,remoteId}]; saveState(state); return [{json:{...payload,ok:true,remoteId,permalink}}];"
      },
      "name": "Registrar publica\u00e7\u00e3o \u00b7 linkedin",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1280
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='linkedin'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date(); const nowIso=now.toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; const providerError=$json.error||{}; const raw=safeText(providerError.description||providerError.message||providerError.context?.data?.detail||providerError.context?.data?.title||$json.description||$json.message||'Falha sem detalhe retornado pela rede.',500); if(delivery&&delivery.dispatchId===payload.dispatchId){const attempts=Number(delivery.attempts||1); const canRetry=payload.network!=='x'&&attempts<3; delivery.status=canRetry?'retry':'failed'; delivery.retryAt=canRetry?new Date(now.getTime()+Math.min(60,2**attempts)*60000).toISOString():''; delivery.lastError=raw; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:canRetry?'agendou nova tentativa':payload.network==='x'?'registrou falha do X sem nova tentativa':'falhou publica\u00e7\u00e3o',comment:raw,network:payload.network,dispatchId:payload.dispatchId}]; saveState(state)} return [{json:{...payload,ok:false,error:raw}}];"
      },
      "name": "Registrar falha \u00b7 linkedin",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1392
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='x'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date().toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; if(!delivery||delivery.dispatchId!==payload.dispatchId) throw new Error('A confirma\u00e7\u00e3o recebida n\u00e3o corresponde \u00e0 entrega reservada.'); const remoteId=safeText($json.id||$json.data?.id||$json.postId||$json.value?.id||'',200); const permalink=safeText($json.permalink_url||$json.permalink||'',1000); delivery.status='published'; delivery.publishedAt=now; delivery.retryAt=''; delivery.lastRemoteId=remoteId; delivery.lastPermalink=permalink; delivery.lastError=''; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=now; record.audit=[...(record.audit||[]),{at:now,operator:'Sistema',action:'publicou conte\u00fado',comment:'Confirma\u00e7\u00e3o recebida da API.',network:payload.network,dispatchId:payload.dispatchId,remoteId}]; saveState(state); return [{json:{...payload,ok:true,remoteId,permalink}}];"
      },
      "name": "Registrar publica\u00e7\u00e3o \u00b7 x",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1504
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst platform='x'; const payload=($json&&$json.contentId&&$json.dispatchId)?$json:(()=>{const {state,items}=scan();const matches=items.filter(content=>{const delivery=state.records?.[content.id]?.deliveries?.[platform];return delivery&&delivery.status==='dispatching'&&delivery.dispatchId});if(matches.length!==1)throw new Error('N\u00e3o foi poss\u00edvel identificar com seguran\u00e7a a entrega '+platform+' em despacho.');const content=matches[0];const delivery=state.records[content.id].deliveries[platform];return {contentId:content.id,network:platform,dispatchId:delivery.dispatchId};})(); const now=new Date(); const nowIso=now.toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; const providerError=$json.error||{}; const raw=safeText(providerError.description||providerError.message||providerError.context?.data?.detail||providerError.context?.data?.title||$json.description||$json.message||'Falha sem detalhe retornado pela rede.',500); if(delivery&&delivery.dispatchId===payload.dispatchId){const attempts=Number(delivery.attempts||1); const canRetry=payload.network!=='x'&&attempts<3; delivery.status=canRetry?'retry':'failed'; delivery.retryAt=canRetry?new Date(now.getTime()+Math.min(60,2**attempts)*60000).toISOString():''; delivery.lastError=raw; const record=state.records[payload.contentId]; reconcileRecordStatus(record); record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:canRetry?'agendou nova tentativa':payload.network==='x'?'registrou falha do X sem nova tentativa':'falhou publica\u00e7\u00e3o',comment:raw,network:payload.network,dispatchId:payload.dispatchId}]; saveState(state)} return [{json:{...payload,ok:false,error:raw}}];"
      },
      "name": "Registrar falha \u00b7 x",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1616
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n  const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n  if(!statuses.length) return record?.status||'pendente';\n  if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n  const published=statuses.filter(status=>status==='published').length;\n  record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n  return record.status;\n}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst payload=$json; const now=new Date(); const nowIso=now.toISOString(); const {state}=scan(); const delivery=state.records?.[payload.contentId]?.deliveries?.[payload.network]; const providerError=$json.error||{}; const raw=safeText(providerError.description||providerError.message||providerError.context?.data?.detail||providerError.context?.data?.title||$json.description||$json.message||'Falha sem detalhe retornado pela rede.',500); if(delivery&&delivery.dispatchId===payload.dispatchId){const attempts=Number(delivery.attempts||1); const canRetry=attempts<3; delivery.status=canRetry?'retry':'failed'; delivery.retryAt=canRetry?new Date(now.getTime()+Math.min(60,2**attempts)*60000).toISOString():''; delivery.lastError=raw; const record=state.records[payload.contentId]; record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:canRetry?'agendou nova tentativa':'falhou publica\u00e7\u00e3o',comment:raw,network:payload.network,dispatchId:payload.dispatchId}]; saveState(state)} return [{json:{...payload,ok:false,error:raw}}];"
      },
      "name": "Registrar falha \u00b7 pr\u00e9-valida\u00e7\u00e3o",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        1824
      ]
    },
    {
      "parameters": {
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_PAGE_ID+\"?fields=access_token\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "facebookGraphApiOAuth2Api",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Instagram \u00b7 token da P\u00e1gina",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        480,
        352
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_PAGE_ID+\"?fields=access_token\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "facebookGraphApiOAuth2Api",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Facebook \u00b7 token da P\u00e1gina",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        480,
        560
      ],
      "onError": "continueErrorOutput"
    }
  ],
  "connections": {
    "Webhook do portal": {
      "main": [
        [
          {
            "node": "Processar a\u00e7\u00e3o do portal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Processar a\u00e7\u00e3o do portal": {
      "main": [
        [
          {
            "node": "Roteador da a\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Roteador da a\u00e7\u00e3o": {
      "main": [
        [
          {
            "node": "OpenAI \u00b7 sugest\u00e3o prim\u00e1ria",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder ao portal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI \u00b7 sugest\u00e3o prim\u00e1ria": {
      "main": [
        [
          {
            "node": "Validar e salvar sugest\u00e3o IA",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fallback Gemini habilitado?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fallback Gemini habilitado?": {
      "main": [
        [
          {
            "node": "Gemini \u00b7 fallback",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fallback Ollama habilitado?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini \u00b7 fallback": {
      "main": [
        [
          {
            "node": "Validar e salvar sugest\u00e3o IA",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fallback Ollama habilitado?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fallback Ollama habilitado?": {
      "main": [
        [
          {
            "node": "Ollama \u00b7 fallback local",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar indisponibilidade da IA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ollama \u00b7 fallback local": {
      "main": [
        [
          {
            "node": "Validar e salvar sugest\u00e3o IA",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar indisponibilidade da IA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar e salvar sugest\u00e3o IA": {
      "main": [
        [
          {
            "node": "Responder ao portal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar indisponibilidade da IA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar indisponibilidade da IA": {
      "main": [
        [
          {
            "node": "Responder ao portal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger \u00b7 a cada 5 minutos": {
      "main": [
        [
          {
            "node": "Reservar entregas eleg\u00edveis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reservar entregas eleg\u00edveis": {
      "main": [
        [
          {
            "node": "Pr\u00e9-validar publica\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pr\u00e9-validar publica\u00e7\u00e3o": {
      "main": [
        [
          {
            "node": "Roteador por rede",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 pr\u00e9-valida\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Roteador por rede": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 token da P\u00e1gina",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Facebook \u00b7 token da P\u00e1gina",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "LinkedIn \u00b7 preparar arquivos",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "X \u00b7 adaptar sequ\u00eancia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 preparar slides": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 criar containers",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 criar containers": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 reunir containers",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 reunir containers": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 criar carrossel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 criar carrossel": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 aguardar processamento",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 aguardar processamento": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 consultar status do carrossel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 consultar status do carrossel": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 validar processamento",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 validar processamento": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 publicar carrossel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 publicar carrossel": {
      "main": [
        [
          {
            "node": "Registrar publica\u00e7\u00e3o \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Facebook \u00b7 preparar slides": {
      "main": [
        [
          {
            "node": "Facebook \u00b7 enviar fotos n\u00e3o publicadas",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 facebook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Facebook \u00b7 enviar fotos n\u00e3o publicadas": {
      "main": [
        [
          {
            "node": "Facebook \u00b7 reunir IDs das fotos",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 facebook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Facebook \u00b7 reunir IDs das fotos": {
      "main": [
        [
          {
            "node": "Facebook \u00b7 publicar carrossel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 facebook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Facebook \u00b7 publicar carrossel": {
      "main": [
        [
          {
            "node": "Registrar publica\u00e7\u00e3o \u00b7 facebook",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 facebook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn \u00b7 preparar arquivos": {
      "main": [
        [
          {
            "node": "LinkedIn \u00b7 ler imagem local",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 linkedin",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn \u00b7 ler imagem local": {
      "main": [
        [],
        [
          {
            "node": "Registrar falha \u00b7 linkedin",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn \u00b7 enviar bin\u00e1rio": {
      "main": [
        [
          {
            "node": "LinkedIn \u00b7 reunir URNs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 linkedin",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn \u00b7 reunir URNs": {
      "main": [
        [],
        [
          {
            "node": "Registrar falha \u00b7 linkedin",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 adaptar sequ\u00eancia": {
      "main": [
        [
          {
            "node": "X \u00b7 ler primeira imagem",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 ler primeira imagem": {
      "main": [
        [
          {
            "node": "X \u00b7 enviar m\u00eddia v2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 enviar m\u00eddia v2": {
      "main": [
        [
          {
            "node": "X \u00b7 publicar post inicial",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 publicar post inicial": {
      "main": [
        [
          {
            "node": "X \u00b7 h\u00e1 resposta 2?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 h\u00e1 resposta 2?": {
      "main": [
        [
          {
            "node": "X \u00b7 publicar resposta 2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar publica\u00e7\u00e3o \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 publicar resposta 2": {
      "main": [
        [
          {
            "node": "X \u00b7 h\u00e1 resposta 3?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 h\u00e1 resposta 3?": {
      "main": [
        [
          {
            "node": "X \u00b7 publicar resposta 3",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar publica\u00e7\u00e3o \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 publicar resposta 3": {
      "main": [
        [
          {
            "node": "X \u00b7 h\u00e1 resposta 4?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 h\u00e1 resposta 4?": {
      "main": [
        [
          {
            "node": "X \u00b7 publicar resposta 4",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar publica\u00e7\u00e3o \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "X \u00b7 publicar resposta 4": {
      "main": [
        [
          {
            "node": "Registrar publica\u00e7\u00e3o \u00b7 x",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 x",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar publica\u00e7\u00e3o \u00b7 instagram": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar falha \u00b7 instagram": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar publica\u00e7\u00e3o \u00b7 facebook": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar falha \u00b7 facebook": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar publica\u00e7\u00e3o \u00b7 linkedin": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar falha \u00b7 linkedin": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar publica\u00e7\u00e3o \u00b7 x": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar falha \u00b7 x": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar falha \u00b7 pr\u00e9-valida\u00e7\u00e3o": {
      "main": [
        [
          {
            "node": "Registrar no Data Table \u00b7 Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Instagram \u00b7 token da P\u00e1gina": {
      "main": [
        [
          {
            "node": "Instagram \u00b7 preparar slides",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 instagram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Facebook \u00b7 token da P\u00e1gina": {
      "main": [
        [
          {
            "node": "Facebook \u00b7 preparar slides",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Registrar falha \u00b7 facebook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "availableInMCP": false,
    "binaryMode": "separate"
  },
  "nodeGroups": [],
  "activeVersionId": "215f1f82-636e-4444-8e1b-879ff19c1144",
  "versionCounter": 46,
  "triggerCount": 2,
  "sourceWorkflowId": null,
  "versionMetadata": {
    "name": "Version 215f1f82",
    "description": ""
  },
  "active": false
}