Harden Paperless and school intake n8n workflows
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Token 4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d"
|
||||
"value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer sk-a95bd142d43d175b6476ebc862e81aa1f6ef34b1153f839698d07541d2f55308"
|
||||
"value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,7 +125,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Token 4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d"
|
||||
"value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
@@ -188,7 +188,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const data = $input.first().json;\n\nconst PAPERLESS_BASE_URL = 'https://paperless.paccoco.com';\nconst PAPERLESS_TOKEN = '4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d';\n\nconst headers = {\n Authorization: `Token ${PAPERLESS_TOKEN}`,\n 'Content-Type': 'application/json',\n};\n\nfunction normalizeTagName(tag) {\n return String(tag || '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9 _-]/gi, '')\n .replace(/\\s+/g, ' ')\n .slice(0, 64);\n}\n\nconst suggestedTags = Array.isArray(data.suggested_tags)\n ? [...new Set(data.suggested_tags.map(normalizeTagName).filter(Boolean))].slice(0, 5)\n : [];\n\nif (!data.documentId) {\n throw new Error('No documentId found; cannot apply tags.');\n}\n\nif (suggestedTags.length === 0) {\n return [{\n json: {\n ...data,\n tag_apply_status: 'skipped_no_suggested_tags',\n applied_tag_names: [],\n applied_tag_ids: []\n }\n }];\n}\n\nasync function request(method, url, body) {\n const options = {\n method,\n url,\n headers,\n json: true,\n };\n\n if (body !== undefined) {\n options.body = body;\n }\n\n return await this.helpers.httpRequest(options);\n}\n\n// Fetch all existing tags. Handles pagination.\nconst existingTags = [];\nlet tagsUrl = `${PAPERLESS_BASE_URL}/api/tags/?page_size=100`;\n\nwhile (tagsUrl) {\n const page = await request.call(this, 'GET', tagsUrl);\n const results = Array.isArray(page.results) ? page.results : (Array.isArray(page) ? page : []);\n existingTags.push(...results);\n\n if (page.next) {\n tagsUrl = page.next.startsWith('http') ? page.next : `${PAPERLESS_BASE_URL}${page.next}`;\n } else {\n tagsUrl = null;\n }\n}\n\nconst tagsByName = new Map(\n existingTags.map(tag => [normalizeTagName(tag.name), tag])\n);\n\nconst appliedTags = [];\n\nfor (const tagName of suggestedTags) {\n let tag = tagsByName.get(tagName);\n\n if (!tag) {\n tag = await request.call(this, 'POST', `${PAPERLESS_BASE_URL}/api/tags/`, {\n name: tagName,\n });\n tagsByName.set(tagName, tag);\n }\n\n if (tag?.id) {\n appliedTags.push(tag);\n }\n}\n\n// Fetch current document so we merge tags instead of overwriting existing ones.\nconst doc = await request.call(this, 'GET', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`);\nconst currentTagIds = Array.isArray(doc.tags) ? doc.tags : [];\nconst appliedTagIds = appliedTags.map(tag => tag.id);\nconst mergedTagIds = [...new Set([...currentTagIds, ...appliedTagIds])];\n\nawait request.call(this, 'PATCH', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`, {\n tags: mergedTagIds,\n});\n\nreturn [{\n json: {\n ...data,\n tag_apply_status: 'updated',\n applied_tag_names: appliedTags.map(tag => tag.name),\n applied_tag_ids: appliedTagIds,\n final_tag_ids: mergedTagIds,\n }\n}];"
|
||||
"jsCode": "const data = $input.first().json;\n\nconst PAPERLESS_BASE_URL = ($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '');\nconst PAPERLESS_TOKEN = $env.PAPERLESS_API_TOKEN;\nif (!PAPERLESS_TOKEN) throw new Error('PAPERLESS_API_TOKEN is not set.');\n\nconst headers = {\n Authorization: `Token ${PAPERLESS_TOKEN}`,\n 'Content-Type': 'application/json',\n};\n\nfunction normalizeTagName(tag) {\n return String(tag || '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9 _-]/gi, '')\n .replace(/\\s+/g, ' ')\n .slice(0, 64);\n}\n\nconst suggestedTags = Array.isArray(data.suggested_tags)\n ? [...new Set(data.suggested_tags.map(normalizeTagName).filter(Boolean))].slice(0, 5)\n : [];\n\nif (!data.documentId) {\n throw new Error('No documentId found; cannot apply tags.');\n}\n\nif (suggestedTags.length === 0) {\n return [{\n json: {\n ...data,\n tag_apply_status: 'skipped_no_suggested_tags',\n applied_tag_names: [],\n applied_tag_ids: []\n }\n }];\n}\n\nasync function request(method, url, body) {\n const options = { method, url, headers, json: true };\n if (body !== undefined) options.body = body;\n return await this.helpers.httpRequest(options);\n}\n\nconst existingTags = [];\nlet tagsUrl = `${PAPERLESS_BASE_URL}/api/tags/?page_size=100`;\nwhile (tagsUrl) {\n const page = await request.call(this, 'GET', tagsUrl);\n const results = Array.isArray(page.results) ? page.results : (Array.isArray(page) ? page : []);\n existingTags.push(...results);\n tagsUrl = page.next ? (page.next.startsWith('http') ? page.next : `${PAPERLESS_BASE_URL}${page.next}`) : null;\n}\n\nconst tagsByName = new Map(existingTags.map(tag => [normalizeTagName(tag.name), tag]));\nconst appliedTags = [];\nfor (const tagName of suggestedTags) {\n let tag = tagsByName.get(tagName);\n if (!tag) {\n tag = await request.call(this, 'POST', `${PAPERLESS_BASE_URL}/api/tags/`, { name: tagName });\n tagsByName.set(tagName, tag);\n }\n if (tag?.id) appliedTags.push(tag);\n}\n\nconst doc = await request.call(this, 'GET', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`);\nconst currentTagIds = Array.isArray(doc.tags) ? doc.tags : [];\nconst appliedTagIds = appliedTags.map(tag => tag.id);\nconst mergedTagIds = [...new Set([...currentTagIds, ...appliedTagIds])];\n\nawait request.call(this, 'PATCH', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`, { tags: mergedTagIds });\n\nreturn [{\n json: {\n ...data,\n tag_apply_status: 'updated',\n applied_tag_names: appliedTags.map(tag => tag.name),\n applied_tag_ids: appliedTagIds,\n final_tag_ids: mergedTagIds,\n }\n}];"
|
||||
},
|
||||
"id": "b0d5423b-dc83-4adf-98c6-d95afa3e5c1b",
|
||||
"name": "Apply Suggested Tags to Paperless",
|
||||
@@ -409,4 +409,4 @@
|
||||
"name": "Version 199d2609",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Token 4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d"
|
||||
"value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer sk-a95bd142d43d175b6476ebc862e81aa1f6ef34b1153f839698d07541d2f55308"
|
||||
"value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,7 +125,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Token 4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d"
|
||||
"value": "={{'Token ' + $env.PAPERLESS_API_TOKEN}}"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
@@ -188,7 +188,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const data = $input.first().json;\n\nconst PAPERLESS_BASE_URL = 'https://paperless.paccoco.com';\nconst PAPERLESS_TOKEN = '4fb34ba35b4ab76a0715e8c56faf00a31de4fb5d';\n\nconst headers = {\n Authorization: `Token ${PAPERLESS_TOKEN}`,\n 'Content-Type': 'application/json',\n};\n\nfunction normalizeTagName(tag) {\n return String(tag || '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9 _-]/gi, '')\n .replace(/\\s+/g, ' ')\n .slice(0, 64);\n}\n\nconst suggestedTags = Array.isArray(data.suggested_tags)\n ? [...new Set(data.suggested_tags.map(normalizeTagName).filter(Boolean))].slice(0, 5)\n : [];\n\nif (!data.documentId) {\n throw new Error('No documentId found; cannot apply tags.');\n}\n\nif (suggestedTags.length === 0) {\n return [{\n json: {\n ...data,\n tag_apply_status: 'skipped_no_suggested_tags',\n applied_tag_names: [],\n applied_tag_ids: []\n }\n }];\n}\n\nasync function request(method, url, body) {\n const options = {\n method,\n url,\n headers,\n json: true,\n };\n\n if (body !== undefined) {\n options.body = body;\n }\n\n return await this.helpers.httpRequest(options);\n}\n\n// Fetch all existing tags. Handles pagination.\nconst existingTags = [];\nlet tagsUrl = `${PAPERLESS_BASE_URL}/api/tags/?page_size=100`;\n\nwhile (tagsUrl) {\n const page = await request.call(this, 'GET', tagsUrl);\n const results = Array.isArray(page.results) ? page.results : (Array.isArray(page) ? page : []);\n existingTags.push(...results);\n\n if (page.next) {\n tagsUrl = page.next.startsWith('http') ? page.next : `${PAPERLESS_BASE_URL}${page.next}`;\n } else {\n tagsUrl = null;\n }\n}\n\nconst tagsByName = new Map(\n existingTags.map(tag => [normalizeTagName(tag.name), tag])\n);\n\nconst appliedTags = [];\n\nfor (const tagName of suggestedTags) {\n let tag = tagsByName.get(tagName);\n\n if (!tag) {\n tag = await request.call(this, 'POST', `${PAPERLESS_BASE_URL}/api/tags/`, {\n name: tagName,\n });\n tagsByName.set(tagName, tag);\n }\n\n if (tag?.id) {\n appliedTags.push(tag);\n }\n}\n\n// Fetch current document so we merge tags instead of overwriting existing ones.\nconst doc = await request.call(this, 'GET', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`);\nconst currentTagIds = Array.isArray(doc.tags) ? doc.tags : [];\nconst appliedTagIds = appliedTags.map(tag => tag.id);\nconst mergedTagIds = [...new Set([...currentTagIds, ...appliedTagIds])];\n\nawait request.call(this, 'PATCH', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`, {\n tags: mergedTagIds,\n});\n\nreturn [{\n json: {\n ...data,\n tag_apply_status: 'updated',\n applied_tag_names: appliedTags.map(tag => tag.name),\n applied_tag_ids: appliedTagIds,\n final_tag_ids: mergedTagIds,\n }\n}];"
|
||||
"jsCode": "const data = $input.first().json;\n\nconst PAPERLESS_BASE_URL = ($env.PAPERLESS_BASE_URL || 'https://paperless.paccoco.com').replace(/\\/$/, '');\nconst PAPERLESS_TOKEN = $env.PAPERLESS_API_TOKEN;\nif (!PAPERLESS_TOKEN) throw new Error('PAPERLESS_API_TOKEN is not set.');\n\nconst headers = {\n Authorization: `Token ${PAPERLESS_TOKEN}`,\n 'Content-Type': 'application/json',\n};\n\nfunction normalizeTagName(tag) {\n return String(tag || '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9 _-]/gi, '')\n .replace(/\\s+/g, ' ')\n .slice(0, 64);\n}\n\nconst suggestedTags = Array.isArray(data.suggested_tags)\n ? [...new Set(data.suggested_tags.map(normalizeTagName).filter(Boolean))].slice(0, 5)\n : [];\n\nif (!data.documentId) {\n throw new Error('No documentId found; cannot apply tags.');\n}\n\nif (suggestedTags.length === 0) {\n return [{\n json: {\n ...data,\n tag_apply_status: 'skipped_no_suggested_tags',\n applied_tag_names: [],\n applied_tag_ids: []\n }\n }];\n}\n\nasync function request(method, url, body) {\n const options = { method, url, headers, json: true };\n if (body !== undefined) options.body = body;\n return await this.helpers.httpRequest(options);\n}\n\nconst existingTags = [];\nlet tagsUrl = `${PAPERLESS_BASE_URL}/api/tags/?page_size=100`;\nwhile (tagsUrl) {\n const page = await request.call(this, 'GET', tagsUrl);\n const results = Array.isArray(page.results) ? page.results : (Array.isArray(page) ? page : []);\n existingTags.push(...results);\n tagsUrl = page.next ? (page.next.startsWith('http') ? page.next : `${PAPERLESS_BASE_URL}${page.next}`) : null;\n}\n\nconst tagsByName = new Map(existingTags.map(tag => [normalizeTagName(tag.name), tag]));\nconst appliedTags = [];\nfor (const tagName of suggestedTags) {\n let tag = tagsByName.get(tagName);\n if (!tag) {\n tag = await request.call(this, 'POST', `${PAPERLESS_BASE_URL}/api/tags/`, { name: tagName });\n tagsByName.set(tagName, tag);\n }\n if (tag?.id) appliedTags.push(tag);\n}\n\nconst doc = await request.call(this, 'GET', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`);\nconst currentTagIds = Array.isArray(doc.tags) ? doc.tags : [];\nconst appliedTagIds = appliedTags.map(tag => tag.id);\nconst mergedTagIds = [...new Set([...currentTagIds, ...appliedTagIds])];\n\nawait request.call(this, 'PATCH', `${PAPERLESS_BASE_URL}/api/documents/${data.documentId}/`, { tags: mergedTagIds });\n\nreturn [{\n json: {\n ...data,\n tag_apply_status: 'updated',\n applied_tag_names: appliedTags.map(tag => tag.name),\n applied_tag_ids: appliedTagIds,\n final_tag_ids: mergedTagIds,\n }\n}];"
|
||||
},
|
||||
"id": "b0d5423b-dc83-4adf-98c6-d95afa3e5c1b",
|
||||
"name": "Apply Suggested Tags to Paperless",
|
||||
@@ -409,4 +409,4 @@
|
||||
"name": "Version 199d2609",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nconst body = $json.body || $json;\nlet id = body.document_id || body.id || body.doc_id || body.pk || body.document;\nif (!id || (typeof id === 'string' && id.includes('{'))) {\n id = null;\n}\nreturn [{ json: { document_id: id ? String(id) : null, received_at: new Date().toISOString(), webhook_payload_keys: Object.keys(body) } }];\n"
|
||||
"jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || {};\nconst query = input.query || {};\nconst params = input.params || {};\nconst headers = input.headers || {};\nconst candidates = [\n body.document_id,\n body.id,\n body.doc_id,\n body.pk,\n body.document_id?.id,\n body.document?.id,\n body.document,\n query.document_id,\n query.id,\n query.doc_id,\n params.document_id,\n params.id,\n headers['x-paperless-document-id'],\n input.document_id,\n input.id\n].filter(v => v !== undefined && v !== null && v !== '');\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: String(documentId), received_at: new Date().toISOString(), webhook_payload_keys: Object.keys(body) } }];\n"
|
||||
},
|
||||
"id": "extract-doc-id",
|
||||
"name": "Extract Document ID",
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ model: $env.PAPERLESS_TRIAGE_MODEL || 'gpt-4o-mini', temperature: 0.1, response_format: { type: 'json_object' }, messages: $json.llm_messages }) }}",
|
||||
"jsonBody": "={{ JSON.stringify({ model: $env.PAPERLESS_TRIAGE_MODEL || 'medium', temperature: 0.1, response_format: { type: 'json_object' }, messages: $json.llm_messages }) }}",
|
||||
"options": {
|
||||
"timeout": 60000
|
||||
}
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\n// Requires n8n Code node filesystem access: NODE_FUNCTION_ALLOW_BUILTIN=fs,path\nconst fs = require('fs');\nconst path = require('path');\nconst queuePath = $env.PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH || '/data/paperless_review.json';\nconst maxItems = Number($env.PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS || 200);\nfs.mkdirSync(path.dirname(queuePath), { recursive: true });\nlet queue = [];\ntry { queue = JSON.parse(fs.readFileSync(queuePath, 'utf8')); } catch (_) { queue = []; }\nif (!Array.isArray(queue)) queue = [];\nconst item = $json.review_item;\nqueue = [item, ...queue.filter(x => String(x.id) !== String(item.id))].slice(0, maxItems);\nconst tmp = queuePath + '.tmp';\nfs.writeFileSync(tmp, JSON.stringify(queue, null, 2) + '\n', 'utf8');\nfs.renameSync(tmp, queuePath);\nreturn [{ json: { ...$json, queue_path: queuePath, queue_count: queue.length } }];\n"
|
||||
"jsCode": "\n// Requires n8n Code node filesystem access: NODE_FUNCTION_ALLOW_BUILTIN=fs,path\nconst fs = require('fs');\nconst path = require('path');\nconst queuePath = $env.PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH || '/data/paperless_review.json';\nconst maxItems = Number($env.PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS || 200);\nfs.mkdirSync(path.dirname(queuePath), { recursive: true });\nlet queue = [];\ntry { queue = JSON.parse(fs.readFileSync(queuePath, 'utf8')); } catch (_) { queue = []; }\nif (!Array.isArray(queue)) queue = [];\nconst item = $json.review_item;\nqueue = [item, ...queue.filter(x => String(x.id) !== String(item.id))].slice(0, maxItems);\nconst tmp = queuePath + '.tmp';\nfs.writeFileSync(tmp, JSON.stringify(queue, null, 2) + '\\n', 'utf8');\nfs.renameSync(tmp, queuePath);\nreturn [{ json: { ...$json, queue_path: queuePath, queue_count: queue.length } }];\n"
|
||||
},
|
||||
"id": "write-queue",
|
||||
"name": "Write Doris Review Queue JSON",
|
||||
@@ -313,11 +313,6 @@
|
||||
"node": "Apply Safe Updates Enabled?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Urgent Notification Configured?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || input;\nconst candidates = [\n body.document_id,\n body.id,\n body.doc_id,\n body.pk,\n body.document_id?.id,\n body.document?.id,\n body.document\n].filter(v => v !== undefined && v !== null);\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: documentId } }];"
|
||||
"jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || {};\nconst query = input.query || {};\nconst params = input.params || {};\nconst headers = input.headers || {};\nconst candidates = [\n body.document_id,\n body.id,\n body.doc_id,\n body.pk,\n body.document_id?.id,\n body.document?.id,\n body.document,\n query.document_id,\n query.id,\n query.doc_id,\n params.document_id,\n params.id,\n headers['x-paperless-document-id'],\n input.document_id,\n input.id\n].filter(v => v !== undefined && v !== null && v !== '');\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: documentId } }];"
|
||||
},
|
||||
"id": "normalize-paperless-webhook",
|
||||
"name": "Normalize Paperless Webhook",
|
||||
@@ -152,13 +152,13 @@
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer sk-a95bd142d43d175b6476ebc862e81aa1f6ef34b1153f839698d07541d2f55308"
|
||||
"value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"model\": \"medium\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You write concise, useful summaries for school assignments stored in Paperless. Return ONLY valid JSON with this exact shape: {\\\"summary\\\": string, \\\"course_context\\\": [string], \\\"suggested_tags\\\": [string]}. Summary should be 2-4 sentences, name the argument or subject clearly, and avoid fluff. course_context should contain short bullets like assignment type, score if known, or class framing when inferable from the provided metadata. suggested_tags should be 3-6 lowercase tags focused on subject matter, not generic words. No markdown, no code fences.\"\n },\n {\n \"role\": \"user\",\n \"content\": {{ JSON.stringify('Title: ' + ($json.title || '') + '\\nClass: ' + ($json.class_name || '') + '\\nAssignment: ' + ($json.assignment_name || '') + '\\nSubmission kind: ' + ($json.submission_kind || '') + '\\nOriginal filename: ' + ($json.original_filename || '') + '\\n\\nDocument content:\\n' + ($json.content || '')).slice(1, -1) }}\n }\n ],\n \"max_tokens\": 500,\n \"temperature\": 0.2,\n \"response_format\": { \"type\": \"json_object\" }\n}",
|
||||
"jsonBody": "={\n \"model\": \"medium\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You write concise, useful summaries for school assignments stored in Paperless. Return ONLY valid JSON with this exact shape: {\\\"summary\\\": string, \\\"course_context\\\": [string], \\\"suggested_tags\\\": [string]}. Summary should be 2-4 sentences, name the argument or subject clearly, and avoid fluff. course_context should contain short bullets like assignment type, score if known, or class framing when inferable from the provided metadata. suggested_tags should be 3-6 lowercase tags focused on subject matter, not generic words. No markdown, no code fences.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"={{ JSON.stringify('Title: ' + ($json.title || '') + '\\nClass: ' + ($json.class_name || '') + '\\nAssignment: ' + ($json.assignment_name || '') + '\\nSubmission kind: ' + ($json.submission_kind || '') + '\\nOriginal filename: ' + ($json.original_filename || '') + '\\n\\nDocument content:\\n' + ($json.content || '')).slice(1, -1) }}\"\n }\n ],\n \"max_tokens\": 500,\n \"temperature\": 0.2,\n \"response_format\": { \"type\": \"json_object\" }\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "school-ai-summary",
|
||||
@@ -329,4 +329,4 @@
|
||||
"meta": null,
|
||||
"pinData": null,
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || input;\nconst candidates = [\n body.document_id,\n body.id,\n body.doc_id,\n body.pk,\n body.document_id?.id,\n body.document?.id,\n body.document\n].filter(v => v !== undefined && v !== null);\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: documentId } }];"
|
||||
"jsCode": "\nconst input = $input.first().json || {};\nconst body = input.body || {};\nconst query = input.query || {};\nconst params = input.params || {};\nconst headers = input.headers || {};\nconst candidates = [\n body.document_id,\n body.id,\n body.doc_id,\n body.pk,\n body.document_id?.id,\n body.document?.id,\n body.document,\n query.document_id,\n query.id,\n query.doc_id,\n params.document_id,\n params.id,\n headers['x-paperless-document-id'],\n input.document_id,\n input.id\n].filter(v => v !== undefined && v !== null && v !== '');\nlet raw = candidates[0];\nif (typeof raw === 'object' && raw !== null) raw = raw.id ?? raw.document_id ?? raw.pk ?? null;\nif (typeof raw === 'string' && /\\{.+\\}/.test(raw)) {\n throw new Error('Received unresolved template string instead of document id.');\n}\nconst documentId = Number(raw);\nif (!Number.isFinite(documentId) || documentId <= 0) {\n throw new Error('Could not resolve Paperless document id from webhook payload.');\n}\nreturn [{ json: { document_id: documentId } }];"
|
||||
},
|
||||
"id": "normalize-paperless-webhook",
|
||||
"name": "Normalize Paperless Webhook",
|
||||
@@ -152,13 +152,13 @@
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer sk-a95bd142d43d175b6476ebc862e81aa1f6ef34b1153f839698d07541d2f55308"
|
||||
"value": "={{'Bearer ' + $env.LITELLM_API_KEY}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"model\": \"medium\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You write concise, useful summaries for school assignments stored in Paperless. Return ONLY valid JSON with this exact shape: {\\\"summary\\\": string, \\\"course_context\\\": [string], \\\"suggested_tags\\\": [string]}. Summary should be 2-4 sentences, name the argument or subject clearly, and avoid fluff. course_context should contain short bullets like assignment type, score if known, or class framing when inferable from the provided metadata. suggested_tags should be 3-6 lowercase tags focused on subject matter, not generic words. No markdown, no code fences.\"\n },\n {\n \"role\": \"user\",\n \"content\": {{ JSON.stringify('Title: ' + ($json.title || '') + '\\nClass: ' + ($json.class_name || '') + '\\nAssignment: ' + ($json.assignment_name || '') + '\\nSubmission kind: ' + ($json.submission_kind || '') + '\\nOriginal filename: ' + ($json.original_filename || '') + '\\n\\nDocument content:\\n' + ($json.content || '')).slice(1, -1) }}\n }\n ],\n \"max_tokens\": 500,\n \"temperature\": 0.2,\n \"response_format\": { \"type\": \"json_object\" }\n}",
|
||||
"jsonBody": "={\n \"model\": \"medium\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You write concise, useful summaries for school assignments stored in Paperless. Return ONLY valid JSON with this exact shape: {\\\"summary\\\": string, \\\"course_context\\\": [string], \\\"suggested_tags\\\": [string]}. Summary should be 2-4 sentences, name the argument or subject clearly, and avoid fluff. course_context should contain short bullets like assignment type, score if known, or class framing when inferable from the provided metadata. suggested_tags should be 3-6 lowercase tags focused on subject matter, not generic words. No markdown, no code fences.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"={{ JSON.stringify('Title: ' + ($json.title || '') + '\\nClass: ' + ($json.class_name || '') + '\\nAssignment: ' + ($json.assignment_name || '') + '\\nSubmission kind: ' + ($json.submission_kind || '') + '\\nOriginal filename: ' + ($json.original_filename || '') + '\\n\\nDocument content:\\n' + ($json.content || '')).slice(1, -1) }}\"\n }\n ],\n \"max_tokens\": 500,\n \"temperature\": 0.2,\n \"response_format\": { \"type\": \"json_object\" }\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "school-ai-summary",
|
||||
@@ -329,4 +329,4 @@
|
||||
"meta": null,
|
||||
"pinData": null,
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
| 14 | Paperless Inbox Reminder | Scheduled (Monday 9 AM) | Queries Paperless for untagged documents; sends Gotify reminder with oldest items listed, or a ✅ clear if inbox is empty |
|
||||
| 15 | n8n Self-Health Monitor | Scheduled (every 15 min) | Checks n8n execution history for failures in the past hour; deduplicates via Postgres; pushes Gotify alert with workflow name and error summary |
|
||||
| 16 | NFS Mount Watchdog | Scheduled (every 5 min) | Probes NFS mount points on PD; if missing: runs mount script, re-probes, restarts affected containers (Plex, Audiobookshelf, Immich); notifies via Gotify on heal or escalation |
|
||||
| 17 | Paperless Intake Triage → Doris Review Queue | `POST /webhook/paperless/intake-triage` | Fetches a Paperless document by `document_id`, runs conservative AI triage, writes a Doris Dashboard review queue JSON file, and keeps safe auto-apply disabled unless explicitly enabled |
|
||||
| 18 | Telegram School Intake → Postgres → Paperless v1.2 | `POST /webhook/school/intake/upload` | Accepts multipart schoolwork uploads plus class/assignment metadata, uploads into Paperless with deterministic `intake_id` in the filename/title, and returns the resulting task/document context |
|
||||
| 19 | Paperless School Intake Metadata Enrichment v1.1 | `POST /webhook/school/intake/paperless-enrich` | Handles Paperless post-consumption webhooks, resolves metadata from deterministic filename/title, then applies deterministic title/tags and optional correspondent mapping |
|
||||
|
||||
@@ -79,6 +80,10 @@ Two steps required — both the `.env` file AND the `docker-compose.yaml` `envir
|
||||
| `PAPERLESS_TAG_SUBMISSION_KIND_MAP_JSON` | 19 | JSON object mapping submission kind → Paperless tag ID |
|
||||
| `PAPERLESS_DOCUMENT_TYPE_PAPER_KIND_MAP_JSON` | 19 | JSON object mapping paper kind → Paperless document type ID |
|
||||
| `PAPERLESS_CORRESPONDENT_CLASS_MAP_JSON` | 19 | JSON object mapping class name → Paperless correspondent ID |
|
||||
| `PAPERLESS_TRIAGE_MODEL` | 17 | Optional LiteLLM model override; defaults to `medium` |
|
||||
| `PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH` | 17 | Writable queue path inside the n8n container, e.g. `/data/paperless-triage/paperless_review.json` |
|
||||
| `PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS` | 17 | Optional queue size cap; defaults to `200` |
|
||||
| `PAPERLESS_TRIAGE_APPLY_SAFE` | 17 | Keep `false` unless you intentionally want title-only safe auto-apply enabled |
|
||||
| `N8N_API_KEY` | 15 | n8n → Settings → API → Create API Key |
|
||||
| `LITELLM_API_KEY` | 01, 04, 08 | LiteLLM virtual key (already in .env) |
|
||||
|
||||
@@ -97,6 +102,10 @@ Two steps required — both the `.env` file AND the `docker-compose.yaml` `envir
|
||||
PAPERLESS_TAG_SUBMISSION_KIND_MAP_JSON: ${PAPERLESS_TAG_SUBMISSION_KIND_MAP_JSON}
|
||||
PAPERLESS_DOCUMENT_TYPE_PAPER_KIND_MAP_JSON: ${PAPERLESS_DOCUMENT_TYPE_PAPER_KIND_MAP_JSON}
|
||||
PAPERLESS_CORRESPONDENT_CLASS_MAP_JSON: ${PAPERLESS_CORRESPONDENT_CLASS_MAP_JSON}
|
||||
PAPERLESS_TRIAGE_MODEL: ${PAPERLESS_TRIAGE_MODEL}
|
||||
PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH: ${PAPERLESS_TRIAGE_REVIEW_QUEUE_PATH}
|
||||
PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS: ${PAPERLESS_TRIAGE_REVIEW_MAX_ITEMS}
|
||||
PAPERLESS_TRIAGE_APPLY_SAFE: ${PAPERLESS_TRIAGE_APPLY_SAFE}
|
||||
N8N_API_KEY: ${N8N_API_KEY}
|
||||
```
|
||||
|
||||
@@ -146,17 +155,18 @@ curl -X PUT http://10.5.1.6:6333/collections/knowledge_base \
|
||||
curl http://10.5.1.6:11434/api/pull -d '{"name": "nomic-embed-text"}'
|
||||
```
|
||||
|
||||
### Paperless Webhook (required for workflows 03 and 05)
|
||||
### Paperless Webhook (required for workflows 03, 05, 17, and 19)
|
||||
|
||||
In Paperless-NGX, set up post-consumption webhooks to POST to:
|
||||
```
|
||||
https://n8n.paccoco.com/webhook/paperless/new-document
|
||||
https://n8n.paccoco.com/webhook/paperless/rag-ingest
|
||||
https://n8n.paccoco.com/webhook/paperless/intake-triage
|
||||
https://n8n.paccoco.com/webhook/school/intake/paperless-enrich
|
||||
```
|
||||
with body: `{"document_id": <id>}`
|
||||
|
||||
You can trigger all three from the same Paperless event. Workflow 03 handles AI summary/tagging, workflow 05 handles RAG ingest, and workflow 19 re-applies deterministic school metadata for intake-managed documents.
|
||||
You can trigger all four from the same Paperless event. Workflow 03 handles general AI summary/tagging, workflow 05 handles RAG ingest, workflow 17 feeds the Doris Dashboard review queue, and workflow 19 re-applies deterministic school metadata for intake-managed documents.
|
||||
|
||||
### Grafana Webhook (already configured)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user