From 6b60059c00eeb6a9048f267e65a5aba888b69858 Mon Sep 17 00:00:00 2001
From: Claude Code
+
{{-- /outer --}}
+
+{{ \MailHelper::getMessageMarker($headers['Message-ID']) }}
+
+
+
diff --git a/freescout-templates/reply_fancy.blade.php b/freescout-templates/reply_fancy.blade.php
new file mode 100644
index 0000000..ae8bb4d
--- /dev/null
+++ b/freescout-templates/reply_fancy.blade.php
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+@php
+ $reply_separator = \MailHelper::getHashedReplySeparator($headers['Message-ID']);
+ $is_rtl = \Helper::isLocaleRtl();
+ $is_forwarded = !empty($threads[0]) ? $threads[0]->isForwarded() : false;
+@endphp
+
+{{-- Outer background table --}}
+
+
+
+{{-- Email card --}}
+{{-- /email card --}}
+
+
{{-- /outer --}}
+
+
+
diff --git a/n8n-workflows/workflow-a-http.json b/n8n-workflows/workflow-a-http.json
index 99b77d4..64542bc 100644
--- a/n8n-workflows/workflow-a-http.json
+++ b/n8n-workflows/workflow-a-http.json
@@ -30,42 +30,25 @@
}
},
{
- "id": "uuid-extract-data",
- "name": "Extract Conversation Data",
- "type": "n8n-nodes-base.set",
- "typeVersion": 3,
+ "id": "uuid-split-out",
+ "name": "Split Array into Items",
+ "type": "n8n-nodes-base.splitOut",
+ "typeVersion": 1,
"position": [650, 200],
"parameters": {
- "options": {},
- "assignments": {
- "assignments": [
- {
- "name": "ticket_id",
- "value": "={{ $json.id }}",
- "type": "number"
- },
- {
- "name": "ticket_number",
- "value": "={{ $json.number }}",
- "type": "number"
- },
- {
- "name": "subject",
- "value": "={{ $json.subject }}",
- "type": "string"
- },
- {
- "name": "problem_text",
- "value": "={{ ($json.threads_text || 'No description provided').substring(0, 2000) }}",
- "type": "string"
- },
- {
- "name": "customer_email",
- "value": "={{ $json.customer_email }}",
- "type": "string"
- }
- ]
- }
+ "fieldToSplitOut": "data",
+ "options": {}
+ }
+ },
+ {
+ "id": "uuid-extract-data",
+ "name": "Extract Conversation Data",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [850, 200],
+ "parameters": {
+ "mode": "runOnceForEachItem",
+ "jsCode": "const item = $input.item.json;\n// HTML-Tags entfernen damit die AI lesbaren Text bekommt\nconst rawText = item.threads_text || 'Keine Beschreibung vorhanden';\nconst plainText = rawText\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/\\s+/g, ' ')\n .trim()\n .substring(0, 2000);\nreturn { json: {\n ticket_id: item.id,\n ticket_number: item.number,\n subject: item.subject,\n customer_email: item.customer_email,\n problem_text: plainText\n}};"
}
},
{
@@ -73,7 +56,7 @@
"name": "LiteLLM AI Analysis",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
- "position": [850, 200],
+ "position": [1050, 200],
"parameters": {
"url": "http://llm.eks-ai.apps.asgard.eks-lnx.fft-it.de/v1/chat/completions",
"method": "POST",
@@ -82,60 +65,54 @@
},
"sendBody": true,
"specifyBody": "json",
- "jsonBody": "{\"model\":\"gpt-oss_120b_128k-gpu\",\"messages\":[{\"role\":\"system\",\"content\":\"Du bist ein IT-Support-Assistent. Analysiere das folgende IT-Support-Ticket und gib eine strukturierte JSON-Antwort mit folgenden Feldern: kategorie (z.B. Hardware, Software, Netzwerk, Zugriff), lösung_typ (BARAMUNDI_JOB, AUTOMATISCHE_ANTWORT, oder ESKALATION), vertrauen (Dezimal zwischen 0.0 und 1.0 - wie sicher bist du bei dieser Lösung), baramundi_job (Name des Jobs falls BARAMUNDI_JOB), antwort_text (Die Antwort an den Nutzer), begründung (Kurze Erklärung deiner Analyse)\"},{\"role\":\"user\",\"content\":\"Ticket-Nummer: {{$json.ticket_number}}\\nBetreff: {{$json.subject}}\\nProblembeschreibung:\\n{{$json.problem_text}}\\n\\nBitte antworte NUR mit gültiger JSON in dieser Struktur: {\\\"kategorie\\\": \\\"...\\\", \\\"lösung_typ\\\": \\\"...\\\", \\\"vertrauen\\\": 0.75, \\\"baramundi_job\\\": \\\"...\\\", \\\"antwort_text\\\": \\\"...\\\", \\\"begründung\\\": \\\"...\\\"}\"}],\"temperature\":0.7,\"max_tokens\":1000}"
+ "jsonBody": "={{ JSON.stringify({model: 'gpt-oss_120b_128k-gpu', messages: [{role: 'system', content: 'Du bist ein IT-Support-Assistent. Analysiere das folgende IT-Support-Ticket und gib eine strukturierte JSON-Antwort mit folgenden Feldern: kategorie (z.B. Hardware, Software, Netzwerk, Zugriff), lösung_typ (BARAMUNDI_JOB, AUTOMATISCHE_ANTWORT, oder ESKALATION), vertrauen (Dezimal zwischen 0.0 und 1.0 - wie sicher bist du bei dieser Lösung), baramundi_job (Name des Jobs falls BARAMUNDI_JOB), antwort_text (Die Antwort an den Nutzer), begründung (Kurze Erklärung deiner Analyse)'}, {role: 'user', content: 'Ticket-Nummer: ' + $json.ticket_number + '\\nBetreff: ' + $json.subject + '\\nProblembeschreibung:\\n' + $json.problem_text + '\\n\\nBitte antworte NUR mit gültiger JSON in dieser Struktur: {\"kategorie\": \"...\", \"lösung_typ\": \"...\", \"vertrauen\": 0.75, \"baramundi_job\": \"...\", \"antwort_text\": \"...\", \"begründung\": \"...\"}'}], temperature: 0.7, max_tokens: 1000}) }}"
}
},
{
"id": "uuid-parse-response",
"name": "Parse AI Response",
- "type": "n8n-nodes-base.set",
- "typeVersion": 3,
- "position": [1050, 200],
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [1250, 200],
"parameters": {
- "options": {},
- "assignments": {
- "assignments": [
- {
- "name": "response_text",
- "value": "={{ $json.choices?.[0]?.message?.content || '{}' }}",
- "type": "string"
- },
- {
- "name": "ai_response",
- "value": "={{ (function() { try { return JSON.parse($json.response_text); } catch(e) { return {kategorie: 'unknown', lösung_typ: 'ESKALATION', vertrauen: 0.3}; } })() }}",
- "type": "object"
- },
- {
- "name": "vertrauen",
- "value": "={{ $json.ai_response?.vertrauen || 0.3 }}",
- "type": "number"
- }
- ]
- }
+ "mode": "runOnceForEachItem",
+ "jsCode": "const content = $input.item.json.choices[0].message.content;\nconst extractData = $('Extract Conversation Data').item.json;\nconst ticketId = extractData.ticket_id !== undefined ? extractData.ticket_id : extractData.id;\nlet vertrauen = 0.1;\nlet loesung_typ = 'UNBEKANNT';\nlet kategorie = '';\nlet antwort_text = '';\nlet baramundi_job = '';\ntry {\n const parsed = JSON.parse(content);\n vertrauen = typeof parsed.vertrauen === 'number' ? parsed.vertrauen : 0.1;\n loesung_typ = parsed['lösung_typ'] || parsed.loesung_typ || 'UNBEKANNT';\n kategorie = parsed.kategorie || '';\n antwort_text = parsed.antwort_text || '';\n baramundi_job = parsed.baramundi_job || '';\n} catch(e) { vertrauen = 0.1; }\n// Human-readable for Freescout textarea\nconst lines = [loesung_typ + ' | Vertrauen: ' + vertrauen + ' | Kategorie: ' + kategorie];\nif (baramundi_job) lines.push('Baramundi-Job: ' + baramundi_job);\nlines.push('---');\nlines.push(antwort_text);\nconst display_text = lines.join(' | ');\n// SQL-safe: Quotes escapen, Zeilenumbrüche als ¶ (Pilcrow) erhalten damit\n// Workflow B die Struktur der KI-Antwort wiederherstellen kann.\nconst ai_content_sql = display_text.replace(/'/g, \"''\").replace(/\\r/g, '').replace(/\\n/g, '¶');\nconst ai_json_sql = content.replace(/'/g, \"''\").replace(/[\\n\\r]/g, ' ');\nreturn { json: { vertrauen, ticket_id: ticketId, ai_content: content, ai_content_sql, ai_json_sql } };"
}
},
{
"id": "uuid-check-confidence",
"name": "Check Confidence >= 0.6",
- "type": "n8n-nodes-base.switch",
- "typeVersion": 1,
- "position": [1250, 200],
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2,
+ "position": [1450, 200],
"parameters": {
- "options": [
- {
- "condition": "numberGreaterThanOrEqual",
- "value1": "={{ $json.vertrauen }}",
- "value2": 0.6
- }
- ]
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "loose"
+ },
+ "conditions": [
+ {
+ "id": "cond-confidence",
+ "leftValue": "={{ $json.vertrauen }}",
+ "rightValue": 0.6,
+ "operator": {
+ "type": "number",
+ "operation": "gte"
+ }
+ }
+ ],
+ "combinator": "and"
+ }
}
},
{
- "id": "uuid-save-to-db",
- "name": "Save Suggestion to Freescout DB",
+ "id": "uuid-save-ai-suggestion",
+ "name": "Save AI Suggestion (field 6)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
- "position": [1450, 100],
+ "position": [1650, 100],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
@@ -144,7 +121,41 @@
},
"sendBody": true,
"specifyBody": "json",
- "jsonBody": "{\"query\":\"INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES ({{$json.ticket_id}}, 6, '{{$json.ai_response | json.stringify}}') ON DUPLICATE KEY UPDATE value = VALUES(value); INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES ({{$json.ticket_id}}, 7, 'PENDING') ON DUPLICATE KEY UPDATE value = VALUES(value); INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES ({{$json.ticket_id}}, 8, '1') ON DUPLICATE KEY UPDATE value = VALUES(value);\"}"
+ "jsonBody": "={{ JSON.stringify({query: \"INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES (\" + $json.ticket_id + \", 6, '\" + $json.ai_content_sql + \"') ON DUPLICATE KEY UPDATE value = VALUES(value)\"}) }}"
+ }
+ },
+ {
+ "id": "uuid-save-status-pending",
+ "name": "Save Status PENDING (field 7)",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4,
+ "position": [1650, 200],
+ "parameters": {
+ "url": "http://host.docker.internal:4000/query/freescout",
+ "method": "POST",
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ JSON.stringify({query: \"INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES (\" + $('Parse AI Response').item.json.ticket_id + \", 7, '0') ON DUPLICATE KEY UPDATE value = VALUES(value)\"}) }}"
+ }
+ },
+ {
+ "id": "uuid-save-processed-flag",
+ "name": "Save Processed Flag (field 8)",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4,
+ "position": [1650, 300],
+ "parameters": {
+ "url": "http://host.docker.internal:4000/query/freescout",
+ "method": "POST",
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "sendBody": true,
+ "specifyBody": "json",
+ "jsonBody": "={{ JSON.stringify({query: \"INSERT INTO conversation_custom_field (conversation_id, custom_field_id, value) VALUES (\" + $('Parse AI Response').item.json.ticket_id + \", 8, '1') ON DUPLICATE KEY UPDATE value = VALUES(value)\"}) }}"
}
},
{
@@ -152,19 +163,22 @@
"name": "Skip - Low Confidence",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
- "position": [1450, 350],
+ "position": [1650, 350],
"parameters": {
+ "mode": "manual",
"options": {},
"assignments": {
"assignments": [
{
+ "id": "assign-skipped",
"name": "skipped",
"value": true,
"type": "boolean"
},
{
+ "id": "assign-reason",
"name": "reason",
- "value": "Confidence {{$json.vertrauen}} < 0.6",
+ "value": "={{ 'Confidence ' + $json.vertrauen + ' < 0.6' }}",
"type": "string"
}
]
@@ -175,68 +189,48 @@
"connections": {
"Trigger": {
"main": [
- [
- {
- "node": "Get Unprocessed Conversations",
- "index": 0
- }
- ]
+ [{"node": "Get Unprocessed Conversations", "index": 0}]
]
},
"Get Unprocessed Conversations": {
"main": [
- [
- {
- "node": "Extract Conversation Data",
- "index": 0
- }
- ]
+ [{"node": "Split Array into Items", "index": 0}]
+ ]
+ },
+ "Split Array into Items": {
+ "main": [
+ [{"node": "Extract Conversation Data", "index": 0}]
]
},
"Extract Conversation Data": {
"main": [
- [
- {
- "node": "LiteLLM AI Analysis",
- "index": 0
- }
- ]
+ [{"node": "LiteLLM AI Analysis", "index": 0}]
]
},
"LiteLLM AI Analysis": {
"main": [
- [
- {
- "node": "Parse AI Response",
- "index": 0
- }
- ]
+ [{"node": "Parse AI Response", "index": 0}]
]
},
"Parse AI Response": {
"main": [
- [
- {
- "node": "Check Confidence >= 0.6",
- "index": 0
- }
- ]
+ [{"node": "Check Confidence >= 0.6", "index": 0}]
]
},
"Check Confidence >= 0.6": {
"main": [
- [
- {
- "node": "Save Suggestion to Freescout DB",
- "index": 0
- }
- ],
- [
- {
- "node": "Skip - Low Confidence",
- "index": 0
- }
- ]
+ [{"node": "Save AI Suggestion (field 6)", "index": 0}],
+ [{"node": "Skip - Low Confidence", "index": 0}]
+ ]
+ },
+ "Save AI Suggestion (field 6)": {
+ "main": [
+ [{"node": "Save Status PENDING (field 7)", "index": 0}]
+ ]
+ },
+ "Save Status PENDING (field 7)": {
+ "main": [
+ [{"node": "Save Processed Flag (field 8)", "index": 0}]
]
}
},
diff --git a/n8n-workflows/workflow-b-http.json b/n8n-workflows/workflow-b-http.json
index dbdb4c3..b794530 100644
--- a/n8n-workflows/workflow-b-http.json
+++ b/n8n-workflows/workflow-b-http.json
@@ -7,7 +7,7 @@
"name": "Trigger",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
- "position": [250, 200],
+ "position": [250, 300],
"parameters": {
"cronExpression": "*/2 * * * *"
}
@@ -17,97 +17,78 @@
"name": "Get Approved Conversations",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
- "position": [450, 200],
+ "position": [450, 300],
"parameters": {
"url": "http://host.docker.internal:4000/query/freescout",
"method": "POST",
- "headers": {
- "Content-Type": "application/json"
- },
+ "headers": { "Content-Type": "application/json" },
"sendBody": true,
"specifyBody": "json",
- "jsonBody": "{\"query\":\"SELECT c.id, c.number, c.subject, c.customer_email, ccf.value as ai_suggestion FROM conversations c JOIN conversation_custom_field ccf ON c.id = ccf.conversation_id WHERE ccf.custom_field_id = 7 AND ccf.value = 'APPROVED' LIMIT 10\"}"
+ "jsonBody": "{\"query\":\"SELECT c.id as ticket_id, c.number as ticket_number, c.subject, c.customer_email, ccf6.value as ai_suggestion_raw, ccf7.value as approval_status FROM conversations c JOIN conversation_custom_field ccf7 ON c.id = ccf7.conversation_id AND ccf7.custom_field_id = 7 LEFT JOIN conversation_custom_field ccf6 ON c.id = ccf6.conversation_id AND ccf6.custom_field_id = 6 WHERE ccf7.value = '1' LIMIT 10\"}"
}
},
{
- "id": "uuid-split-approved",
- "name": "Split Results",
- "type": "n8n-nodes-base.splitInBatches",
- "typeVersion": 3,
- "position": [650, 200],
+ "id": "uuid-check-empty",
+ "name": "Any Approved?",
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2,
+ "position": [650, 300],
"parameters": {
- "batchSize": 1,
- "options": {}
- }
- },
- {
- "id": "uuid-extract-approved",
- "name": "Extract & Parse Suggestion",
- "type": "n8n-nodes-base.set",
- "typeVersion": 3,
- "position": [850, 200],
- "parameters": {
- "options": {},
- "assignments": {
- "assignments": [
+ "conditions": {
+ "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" },
+ "conditions": [
{
- "name": "ticket_id",
- "value": "={{ $json.id }}",
- "type": "number"
- },
- {
- "name": "ticket_number",
- "value": "={{ $json.number }}",
- "type": "number"
- },
- {
- "name": "subject",
- "value": "={{ $json.subject }}",
- "type": "string"
- },
- {
- "name": "customer_email",
- "value": "={{ $json.customer_email }}",
- "type": "string"
- },
- {
- "name": "ai_suggestion_raw",
- "value": "={{ typeof $json.ai_suggestion === 'string' ? $json.ai_suggestion : JSON.stringify($json.ai_suggestion) }}",
- "type": "string"
- },
- {
- "name": "ai_suggestion",
- "value": "={{ typeof $json.ai_suggestion === 'string' ? JSON.parse($json.ai_suggestion) : $json.ai_suggestion }}",
- "type": "object"
- },
- {
- "name": "solution_type",
- "value": "={{ $json.ai_suggestion.lösung_typ || 'UNKNOWN' }}",
- "type": "string"
+ "id": "cond-has-data",
+ "leftValue": "={{ $json.data.length }}",
+ "rightValue": 0,
+ "operator": { "type": "number", "operation": "gt" }
}
- ]
+ ],
+ "combinator": "and"
}
}
},
{
- "id": "uuid-decide-solution",
- "name": "Decide Solution Type",
- "type": "n8n-nodes-base.switch",
+ "id": "uuid-split-approved",
+ "name": "Split into Items",
+ "type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
+ "position": [850, 200],
+ "parameters": {
+ "fieldToSplitOut": "data",
+ "options": {}
+ }
+ },
+ {
+ "id": "uuid-parse-suggestion",
+ "name": "Parse Suggestion",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
"position": [1050, 200],
"parameters": {
- "options": [
- {
- "condition": "equal",
- "value1": "={{ $json.solution_type }}",
- "value2": "BARAMUNDI_JOB"
- },
- {
- "condition": "equal",
- "value1": "={{ $json.solution_type }}",
- "value2": "AUTOMATISCHE_ANTWORT"
- }
- ]
+ "mode": "runOnceForEachItem",
+ "jsCode": "const item = $input.item.json;\nconst ticketId = item.ticket_id;\nconst raw = item.ai_suggestion_raw || '';\n\nlet loesung_typ = 'ESKALATION';\nlet baramundi_job = '';\nlet antwort_text = '';\n\n// Lösung-Typ aus dem ersten Segment extrahieren\nconst firstPart = raw.split('|')[0].trim();\nif (firstPart === 'BARAMUNDI_JOB' || firstPart === 'AUTOMATISCHE_ANTWORT' || firstPart === 'ESKALATION') {\n loesung_typ = firstPart;\n}\n\n// Baramundi-Job Name extrahieren\nconst jobMatch = raw.match(/Baramundi-Job:\\s*([^|\\n]+)/);\nif (jobMatch) baramundi_job = jobMatch[1].trim();\n\n// Antworttext nach '--- |' extrahieren\nconst sepIdx = raw.indexOf('--- |');\nif (sepIdx !== -1) {\n antwort_text = raw.substring(sepIdx + 5).trim();\n}\n\n// Fallback: gesamter raw-Text wenn antwort_text leer\nif (!antwort_text && loesung_typ === 'AUTOMATISCHE_ANTWORT') {\n // Versuche nach '---' ohne Pipe zu suchen\n const sepIdx2 = raw.indexOf('---');\n if (sepIdx2 !== -1) {\n antwort_text = raw.substring(sepIdx2 + 3).replace(/^\\s*\\|\\s*/, '').trim();\n }\n // Letzter Fallback: alle Segmente nach dem 4. Pipe-Zeichen\n if (!antwort_text) {\n const parts = raw.split('|');\n if (parts.length > 3) {\n antwort_text = parts.slice(3).join('|').trim();\n }\n }\n}\n\nreturn { json: {\n ticket_id: ticketId,\n ticket_number: item.ticket_number,\n subject: item.subject,\n customer_email: item.customer_email,\n loesung_typ,\n baramundi_job,\n antwort_text,\n raw_suggestion: raw\n}};"
+ }
+ },
+ {
+ "id": "uuid-is-baramundi",
+ "name": "Is Baramundi Job?",
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2,
+ "position": [1250, 200],
+ "parameters": {
+ "conditions": {
+ "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" },
+ "conditions": [
+ {
+ "id": "cond-baramundi",
+ "leftValue": "={{ $json.loesung_typ }}",
+ "rightValue": "BARAMUNDI_JOB",
+ "operator": { "type": "string", "operation": "equals" }
+ }
+ ],
+ "combinator": "and"
+ }
}
},
{
@@ -115,7 +96,7 @@
"name": "Execute Baramundi Job",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
- "position": [1250, 50],
+ "position": [1450, 100],
"parameters": {
"url": "https://baramundi-api.example.com/api/jobs",
"method": "POST",
@@ -125,77 +106,106 @@
},
"sendBody": true,
"specifyBody": "json",
- "jsonBody": "{\"job_name\":\"{{$json.ai_suggestion.baramundi_job}}\",\"ticket_id\":{{$json.ticket_id}},\"target_system\":\"IT\",\"description\":\"{{$json.subject}}\"}"
+ "jsonBody": "={{ JSON.stringify({job_name: $json.baramundi_job, ticket_id: $json.ticket_id, description: $json.subject}) }}"
}
},
{
- "id": "uuid-send-email",
- "name": "Send Email Reply",
- "type": "n8n-nodes-base.httpRequest",
- "typeVersion": 4,
- "position": [1250, 150],
+ "id": "uuid-is-auto-reply",
+ "name": "Is Auto Reply?",
+ "type": "n8n-nodes-base.if",
+ "typeVersion": 2,
+ "position": [1450, 300],
"parameters": {
- "url": "http://host.docker.internal:4000/query/freescout",
- "method": "POST",
- "headers": {
- "Content-Type": "application/json"
- },
- "sendBody": true,
- "specifyBody": "json",
- "jsonBody": "{\"query\":\"INSERT INTO threads (conversation_id, customer_id, user_id, type, status, body, created_at, updated_at) VALUES ({{$json.ticket_id}}, (SELECT customer_id FROM conversations WHERE id = {{$json.ticket_id}} LIMIT 1), NULL, 'customer', 'active', '{{$json.ai_suggestion.antwort_text | replace(\\\"'\\\", \\\"''\\\")}}', NOW(), NOW())\"}"
- }
- },
- {
- "id": "uuid-mark-escalation",
- "name": "Mark for Manual Review",
- "type": "n8n-nodes-base.set",
- "typeVersion": 3,
- "position": [1250, 270],
- "parameters": {
- "options": {},
- "assignments": {
- "assignments": [
+ "conditions": {
+ "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" },
+ "conditions": [
{
- "name": "action",
- "value": "Manual escalation required",
- "type": "string"
+ "id": "cond-autoreply",
+ "leftValue": "={{ $json.loesung_typ }}",
+ "rightValue": "AUTOMATISCHE_ANTWORT",
+ "operator": { "type": "string", "operation": "equals" }
}
- ]
+ ],
+ "combinator": "and"
}
}
},
{
- "id": "uuid-update-status",
- "name": "Update Status to EXECUTED",
- "type": "n8n-nodes-base.httpRequest",
- "typeVersion": 4,
+ "id": "uuid-prepare-email",
+ "name": "Prepare Email Body",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
"position": [1450, 200],
"parameters": {
- "url": "http://host.docker.internal:4000/query/freescout",
- "method": "POST",
- "headers": {
- "Content-Type": "application/json"
- },
- "sendBody": true,
- "specifyBody": "json",
- "jsonBody": "{\"query\":\"UPDATE conversation_custom_field SET value = 'EXECUTED' WHERE conversation_id = {{$json.ticket_id}} AND custom_field_id = 7\"}"
+ "mode": "runOnceForEachItem",
+ "jsCode": "const p = $('Parse Suggestion').item.json;\nconst rawBody = p.antwort_text || p.raw_suggestion || 'Bitte wenden Sie sich an den IT-Support.';\n\n// 1. Markdown entfernen + Struktur wiederherstellen\nlet clean = rawBody\n .replace(/¶/g, '\\n') // ¶-Platzhalter → echte Newlines (neue Tickets)\n .replace(/\\*\\*/g, '')\n .replace(/\\*/g, '')\n .replace(/_{2}/g, '')\n .replace(/ \\| /g, '\\n') // Pipe-Trennzeichen → Zeilenumbrüche\n // Fallback für alte Tickets (flacher Text ohne ¶): Struktur per Regex\n .replace(/ (\\d{1,2}\\.) ([A-ZÄÖÜ])/g, '\\n\\n$1 $2') // \" 1. Windows\" → Absatz\n .replace(/\\. - ([A-ZÄÖÜ])/g, '.\\n- $1') // \". - Öffnen\" → Aufzählung\n .replace(/ (Mit freundlichen)/g, '\\n\\n$1') // Grußformel\n .replace(/ (Sollten Sie)/g, '\\n\\n$1') // Abschlussatz\n .replace(/[ \\t]{2,}/g, ' ') // horizontale Mehrfach-Spaces normalisieren\n .trim();\n\n// 2. Text → HTML konvertieren\n// Absätze: doppelte Newlines →
+
+
+{{-- Email card --}}
+{{-- /email card --}}
+
\n// Nummerierten Listen erkennen und als
' + lines.join('
') + '