initial commit
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "Phase 1.1: Nextcloud Lists Discovery",
|
||||
"description": "Discovers and maps Nextcloud task list IDs (Work, Personal, Shopping, Wishes) for environment configuration",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "Start",
|
||||
"type": "n8n-nodes-base.start",
|
||||
"typeVersion": 1,
|
||||
"position": [50, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "{{ $env.NC_HOST }}/remote.php/dav/lists/",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "{{ $env.NC_USERNAME }}",
|
||||
"password": "{{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"responseFormat": "text"
|
||||
},
|
||||
"name": "Fetch Nextcloud Lists (WebDAV)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [250, 50],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Parse XML response from WebDAV\nconst xmlText = $input.item.json.body || '';\n\n// Extract list names and IDs from XML\nconst listRegex = /<d:href>(\\/remote\\.php\\/dav\\/lists\\/[^/]+\\/)<\\/d:href>[\\s\\S]*?<d:displayname>([^<]+)<\\/d:displayname>/g;\nconst lists = [];\nlet match;\n\nwhile ((match = listRegex.exec(xmlText)) !== null) {\n const href = match[1];\n const displayname = match[2];\n const listId = href.split('/').filter(p => p)[3]; // Extract ID from path\n lists.push({\n name: displayname,\n id: listId,\n url: href\n });\n}\n\n// Identify special lists by name patterns\nconst result = {\n discovery_timestamp: new Date().toISOString(),\n lists_found: lists,\n mapping: {\n work: lists.find(l => l.name.toLowerCase().includes('work'))?.id || 'NOT_FOUND',\n personal: lists.find(l => l.name.toLowerCase().includes('personal'))?.id || 'NOT_FOUND',\n shopping: lists.find(l => l.name.toLowerCase().includes('shopping'))?.id || 'NOT_FOUND',\n wishes: lists.find(l => l.name.toLowerCase().includes('wishes') || l.name.toLowerCase().includes('want'))?.id || 'NOT_FOUND'\n }\n};\n\nreturn result;"
|
||||
},
|
||||
"name": "Parse WebDAV Response",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [450, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Generate .env snippet for configuration\nconst mapping = $input.item.json.mapping;\nconst timestamp = new Date().toISOString();\n\nconst envSnippet = `# NEXA Nextcloud Lists - Discovered on ${timestamp}\nNC_LIST_ID_WORK=${mapping.work}\nNC_LIST_ID_PERSONAL=${mapping.personal}\nNC_LIST_ID_SHOPPING=${mapping.shopping}\nNC_LIST_ID_WISHES=${mapping.wishes}`;\n\nreturn {\n env_snippet: envSnippet,\n discovered_lists: $input.item.json.lists_found,\n ready_for_deployment: Object.values(mapping).every(v => v !== 'NOT_FOUND')\n};"
|
||||
},
|
||||
"name": "Generate .env Configuration",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [650, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "{{ $env.MEMOS_HOST }}/api/v1/memos",
|
||||
"authentication": "headerAuth",
|
||||
"headerAuth": {
|
||||
"Authorization": "Bearer {{ $env.MEMOS_API_KEY }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "json",
|
||||
"body": "{\n \"content\": \"🔍 **Nexa Discovery Report**\\n\\nNextcloud Lists gefunden:\\n\\n- **Work:** {{ $input.item.json.env_snippet.split('=')[1].split('\\\\n')[0] }}\\n- **Personal:** {{ $input.item.json.env_snippet.split('=')[2].split('\\\\n')[0] }}\\n- **Shopping:** {{ $input.item.json.env_snippet.split('=')[3].split('\\\\n')[0] }}\\n- **Wishes:** {{ $input.item.json.env_snippet.split('=')[4].split('\\\\n')[0] }}\\n\\nBitte diese IDs in .env eintragen.\",\n \"visibility\": \"PRIVATE\"\n}"
|
||||
},
|
||||
"name": "Post Report to Memos",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [850, 50],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fromFile": true,
|
||||
"fileName": "discovery_result.json"
|
||||
},
|
||||
"name": "Output Discovery Result",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [1050, 50]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Nextcloud Lists (WebDAV)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Fetch Nextcloud Lists (WebDAV)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse WebDAV Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse WebDAV Response": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Generate .env Configuration",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Generate .env Configuration": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Post Report to Memos",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Output Discovery Result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router",
|
||||
"description": "Receives Memos, classifies context (work/personal), and routes to appropriate Nextcloud list via SAIA",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "memos"
|
||||
},
|
||||
"name": "Memos Webhook Trigger",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [50, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Extract and validate Memos webhook payload\nconst payload = $input.item.json;\n\n// Structure expected from Memos webhook\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n // Detect if it's a task (contains - [ ] or #todo)\n isTask: /(- \\[ \\]|-\\s\\[ \\])|(#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important|!important)/i.test(payload.memo?.content || payload.content || '')\n};\n\n// Ignore archived memos\nif (memo.archived) {\n throw new Error('Skipping archived memo');\n}\n\n// Only process if it contains task marker or #todo\nif (!memo.isTask) {\n throw new Error('Memo is not marked as task (missing - [ ] or #todo)');\n}\n\nreturn {\n memo: memo,\n shouldProcess: true,\n timestamp: new Date().toISOString()\n};"
|
||||
},
|
||||
"name": "Validate Memo Payload",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "{{ $env.SAIA_HOST }}/v1/chat/completions",
|
||||
"authentication": "headerAuth",
|
||||
"headerAuth": {
|
||||
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "json",
|
||||
"body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are Nexa, a context classifier. Analyze the following task memo and classify it as either 'work' or 'personal'. Consider:\\n- Work context: Professional projects, client work, coding, architecture, development\\n- Personal context: Household, shopping, hobbies, health, leisure\\n\\nRespond with ONLY valid JSON: {\\\"context\\\": \\\"work\\\" | \\\"personal\\\", \\\"urgency\\\": 1-5, \\\"summary\\\": \\\"one-line summary\\\", \\\"confidence\\\": 0.0-1.0}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3,\n \"max_tokens\": 200\n}"
|
||||
},
|
||||
"name": "SAIA: Classify Context",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [450, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Parse SAIA response\nconst response = $input.item.json;\nconst content = response.choices?.[0]?.message?.content || '{}';\n\nlet classification;\ntry {\n // Try to extract JSON from response\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch ? jsonMatch[0] : content);\n} catch (e) {\n console.error('Failed to parse SAIA response:', content);\n classification = { context: 'personal', confidence: 0 };\n}\n\nreturn {\n raw_response: content,\n classification: classification,\n timestamp: new Date().toISOString()\n};"
|
||||
},
|
||||
"name": "Parse Classification",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [650, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {\n "options": [\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "work"\n },\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "personal"\n }\n ]\n }\n },\n "name": "Route by Context",\n "type": "n8n-nodes-base.switch",
|
||||
n "typeVersion": 1,\n "position": [850, 50]\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-work\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Work List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 50],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-personal\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Personal List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 200],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "jsCode": "// Create feedback message based on routing decision\nconst context = $node[\\\"Parse Classification\\\"].json.classification.context;\nconst urgency = $node[\\\"Parse Classification\\\"].json.classification.urgency;\nconst listName = context === 'work' ? 'Work' : 'Personal';\nconst emoji = context === 'work' ? '💼' : '🏠';\n\nreturn {\n feedback_content: `${emoji} Task routed to ${listName} list (Urgency: ${urgency}/5)`,\n classification: $node[\\\"Parse Classification\\\"].json.classification,\n memo_id: $node[\\\"Validate Memo Payload\\\"].json.memo.id\n};"
|
||||
},
|
||||
"name": "Prepare Feedback",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [1250, 50]
|
||||
},\n {\n "parameters": {\n "method": "PATCH",\n "url": "{{ $env.MEMOS_HOST }}/api/v1/memos/{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}\",\n "authentication": "headerAuth",\n "headerAuth": {\n "Authorization": "Bearer {{ $env.MEMOS_API_KEY }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "json",\n "body": "{\n \\\"content\\\": \\\"{{ $node[\\\"Prepare Feedback\\\"].json.feedback_content }}\\n\\n_Processed by Nexa at {{ now().toLocaleString() }}_\\\"\n}"\n },\n "name": "Post Feedback to Memos",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1450, 50],\n "onError": "continueErrorOutput"\n }\n ],\n "connections": {\n "Memos Webhook Trigger": {\n "main": [\n [\n {\n "node": "Validate Memo Payload",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Validate Memo Payload": {\n "main": [\n [\n {\n "node": "SAIA: Classify Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "SAIA: Classify Context": {\n "main": [\n [\n {\n "node": "Parse Classification",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Parse Classification": {\n "main": [\n [\n {\n "node": "Route by Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Route by Context": {\n "main": [\n [ { "node": "Create Task - Work List", "type": "main", "index": 0 } ],\n [ { "node": "Create Task - Personal List", "type": "main", "index": 0 } ]\n ]\n },\n "Create Task - Work List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Create Task - Personal List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Prepare Feedback": {\n "main": [\n [\n {\n "node": "Post Feedback to Memos",\n "type": "main",\n "index": 0\n }\n ]\n ]\n }\n }\n}\n
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router",
|
||||
"description": "Receives Memos webhook, classifies context (work/personal) via SAIA, routes to appropriate Nextcloud list",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "memos"
|
||||
},
|
||||
"name": "Memos Webhook Trigger",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [50, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const payload = $input.item.json;\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n isTask: /(- \\[ \\]|#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important)/i.test(payload.memo?.content || payload.content || '')\n};\nif (memo.archived || !memo.isTask) {\n throw new Error('Memo not a task');\n}\nreturn { memo, timestamp: new Date().toISOString() };"
|
||||
},
|
||||
"name": "Validate Memo Payload",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.SAIA_HOST }}/v1/chat/completions",
|
||||
"authentication": "headerAuth",
|
||||
"headerAuth": {
|
||||
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "json",
|
||||
"body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Classify as work or personal. Respond: {context: work|personal, urgency: 1-5, summary: str}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3\n}"
|
||||
},
|
||||
"name": "SAIA: Classify Context",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [450, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const content = $input.item.json.choices?.[0]?.message?.content || '{}';\nlet classification;\ntry {\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch?.[0] || content);\n} catch (e) {\n classification = { context: 'personal', urgency: 2 };\n}\nreturn { classification, raw: content };"
|
||||
},
|
||||
"name": "Parse Classification",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [650, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": [
|
||||
{
|
||||
"condition": "nodeOutputProperty",
|
||||
"value1": "={{ $node[\"Parse Classification\"].json.classification.context }}",
|
||||
"value2": "work"
|
||||
},
|
||||
{
|
||||
"condition": "nodeOutputProperty",
|
||||
"value1": "={{ $node[\"Parse Classification\"].json.classification.context }}",
|
||||
"value2": "personal"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Route by Context",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 1,
|
||||
"position": [850, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "={{ $env.NC_USERNAME }}",
|
||||
"password": "={{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "raw",
|
||||
"body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR"
|
||||
},
|
||||
"name": "Create Task - Work List",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1050, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "={{ $env.NC_USERNAME }}",
|
||||
"password": "={{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "raw",
|
||||
"body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR"
|
||||
},
|
||||
"name": "Create Task - Personal List",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1050, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const context = $node[\"Parse Classification\"].json.classification.context;\nconst emoji = context === 'work' ? '💼' : '🏠';\nreturn { feedback: emoji + ' Task routed to ' + context + ' list', context };"
|
||||
},
|
||||
"name": "Prepare Feedback",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [1250, 50]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Memos Webhook Trigger": {
|
||||
"main": [[{ "node": "Validate Memo Payload", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Validate Memo Payload": {
|
||||
"main": [[{ "node": "SAIA: Classify Context", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"SAIA: Classify Context": {
|
||||
"main": [[{ "node": "Parse Classification", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Parse Classification": {
|
||||
"main": [[{ "node": "Route by Context", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Route by Context": {
|
||||
"main": [
|
||||
[{ "node": "Create Task - Work List", "type": "main", "index": 0 }],
|
||||
[{ "node": "Create Task - Personal List", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"Create Task - Work List": {
|
||||
"main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Create Task - Personal List": {
|
||||
"main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"name": "Phase 2.1: Email Butler - Nextcloud Mail Digest & Summarization",
|
||||
"description": "Reads private emails from Nextcloud Mail, summarizes daily digest, posts to Memos",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"triggerType": "on",
|
||||
"cronExpression": "0 7 * * *"
|
||||
},
|
||||
"name": "Daily Trigger (7 AM)",
|
||||
"type": "n8n-nodes-base.cronTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [50, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "={{ $env.NC_HOST }}/remote.php/dav/calendars/{{ $env.NC_USERNAME }}/personal/",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "={{ $env.NC_USERNAME }}",
|
||||
"password": "={{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"responseFormat": "json"
|
||||
},
|
||||
"name": "Discover Nextcloud Mail Account",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [250, 50],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "={{ $env.NC_USERNAME }}",
|
||||
"password": "={{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"responseFormat": "json",
|
||||
"headers": {
|
||||
"OCS-APIREQUEST": "true"
|
||||
}
|
||||
},
|
||||
"name": "Fetch Mail Accounts from Nextcloud",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [450, 50],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Extract mail account IDs\nconst accounts = $input.item.json.ocs?.data || [];\nif (!Array.isArray(accounts) || accounts.length === 0) {\n throw new Error('No mail accounts found in Nextcloud');\n}\n\n// Find personal account (first private account)\nconst personalAccount = accounts.find(a => a.name?.toLowerCase().includes('privat') || a.name?.toLowerCase().includes('personal')) || accounts[0];\n\nreturn {\n account_id: personalAccount.id,\n account_name: personalAccount.name,\n email: personalAccount.email,\n total_accounts: accounts.length\n};"
|
||||
},
|
||||
"name": "Extract Personal Mail Account",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [650, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts/{{ $node['Extract Personal Mail Account'].json.account_id }}/mailboxes",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "={{ $env.NC_USERNAME }}",
|
||||
"password": "={{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"responseFormat": "json",
|
||||
"headers": {
|
||||
"OCS-APIREQUEST": "true"
|
||||
}
|
||||
},
|
||||
"name": "Fetch Mailboxes",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [850, 50],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Get INBOX mailbox ID\nconst mailboxes = $input.item.json.ocs?.data || [];\nconst inbox = mailboxes.find(m => m.name === 'INBOX');\nif (!inbox) {\n throw new Error('INBOX not found');\n}\nreturn { mailbox_id: inbox.id };"
|
||||
},
|
||||
"name": "Get INBOX ID",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [1050, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts/{{ $node['Extract Personal Mail Account'].json.account_id }}/mailboxes/{{ $node['Get INBOX ID'].json.mailbox_id }}/messages",
|
||||
"authentication": "basicAuth",
|
||||
"basicAuth": {
|
||||
"user": "={{ $env.NC_USERNAME }}",
|
||||
"password": "={{ $env.NC_APP_PASSWORD }}"
|
||||
},
|
||||
"responseFormat": "json",
|
||||
"headers": {
|
||||
"OCS-APIREQUEST": "true"
|
||||
},
|
||||
"qs": "from={{ $now.subtract(1, 'day').format('YYYY-MM-DD') }}&limit=20"
|
||||
},
|
||||
"name": "Fetch Today's Messages",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1250, 50],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Prepare email list for summarization\nconst messages = $input.item.json.ocs?.data || [];\nconst emails = messages.map(msg => ({\n from: msg.from?.join(', ') || 'Unknown',\n subject: msg.subject || '(No Subject)',\n preview: msg.preview?.substring(0, 200) || msg.plainTextBody?.substring(0, 200) || '(No preview)',\n date: msg.dateInt ? new Date(msg.dateInt * 1000).toLocaleDateString('de-DE') : 'Unknown'\n}));\n\nconst emailList = emails.map((e, i) => `${i+1}. **${e.subject}** (von ${e.from})\\n ${e.preview}`).join('\\n\\n');\n\nreturn {\n total_emails: emails.length,\n email_list: emailList,\n emails_json: emails\n};"
|
||||
},
|
||||
"name": "Prepare Email Summary",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [1450, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.SAIA_HOST }}/v1/chat/completions",
|
||||
"authentication": "headerAuth",
|
||||
"headerAuth": {
|
||||
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "json",
|
||||
"body": "{\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Write a concise 3-5 line daily email digest summarizing key points and action items.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Summarize these {{ $node['Prepare Email Summary'].json.total_emails }} emails:\\n\\n{{ $node['Prepare Email Summary'].json.email_list }}\"\n }\n ],\n \"temperature\": 0.7,\n \"max_tokens\": 300\n}"
|
||||
},
|
||||
"name": "SAIA: Summarize Emails",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [1650, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const summary = $input.item.json.choices?.[0]?.message?.content || 'No emails today';\nconst timestamp = new Date().toLocaleTimeString('de-DE', {timeZone: 'Europe/Berlin'});\nreturn {\n summary,\n timestamp,\n email_count: $node['Prepare Email Summary'].json.total_emails\n};"
|
||||
},
|
||||
"name": "Extract Summary",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [1850, 50]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.MEMOS_HOST }}/api/v1/memos",
|
||||
"authentication": "headerAuth",
|
||||
"headerAuth": {
|
||||
"Authorization": "Bearer {{ $env.MEMOS_API_KEY }}"
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParametersUi": "json",
|
||||
"body": "{\n \"content\": \"📧 **Email Digest** ({{ $node['Extract Summary'].json.email_count }} Mails)\\n\\n{{ $node['Extract Summary'].json.summary }}\\n\\n_Generated at {{ $node['Extract Summary'].json.timestamp }}_\",\n \"visibility\": \"PRIVATE\"\n}"
|
||||
},
|
||||
"name": "Post Digest to Memos",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [2050, 50]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Daily Trigger (7 AM)": {
|
||||
"main": [[{ "node": "Fetch Mail Accounts from Nextcloud", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Fetch Mail Accounts from Nextcloud": {
|
||||
"main": [[{ "node": "Extract Personal Mail Account", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Extract Personal Mail Account": {
|
||||
"main": [[{ "node": "Fetch Mailboxes", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Fetch Mailboxes": {
|
||||
"main": [[{ "node": "Get INBOX ID", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Get INBOX ID": {
|
||||
"main": [[{ "node": "Fetch Today's Messages", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Fetch Today's Messages": {
|
||||
"main": [[{ "node": "Prepare Email Summary", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Prepare Email Summary": {
|
||||
"main": [[{ "node": "SAIA: Summarize Emails", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"SAIA: Summarize Emails": {
|
||||
"main": [[{ "node": "Extract Summary", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Extract Summary": {
|
||||
"main": [[{ "node": "Post Digest to Memos", "type": "main", "index": 0 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user