278 lines
11 KiB
JSON
278 lines
11 KiB
JSON
{
|
||
"name": "NMU warning mail OCR and forward",
|
||
"nodes": [
|
||
{
|
||
"parameters": {
|
||
"content": "Настройте credentials для IMAP/Exchange ящика disp5@appm.ru.\n\nФильтры workflow:\n- From: iaocms@sevmeteo.ru\n- Subject: Предупреждение о НМУ\n\nЛокальная модель:\n- POST http://10.1.1.1:4000/v1/chat/completions\n- model: QWEN3.5-9B\n\nSMTP рассылка:\n- 10.0.3.22:25\n- без авторизации\n\nВ узле Build Forward Email замените список получателей NMU_RECIPIENTS.",
|
||
"height": 320,
|
||
"width": 420
|
||
},
|
||
"id": "b3d55a4f-6f6c-4e58-840c-7770e5019111",
|
||
"name": "README",
|
||
"type": "n8n-nodes-base.stickyNote",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
-760,
|
||
-280
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"mailbox": "INBOX",
|
||
"downloadAttachments": true,
|
||
"options": {
|
||
"customEmailConfig": "UNSEEN"
|
||
}
|
||
},
|
||
"id": "e73a579a-3a21-4684-96b0-e1d817208b57",
|
||
"name": "IMAP Email Trigger",
|
||
"type": "n8n-nodes-base.emailReadImap",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
-760,
|
||
160
|
||
],
|
||
"credentials": {
|
||
"imap": {
|
||
"id": "",
|
||
"name": "disp5@appm.ru IMAP"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const FROM = 'iaocms@sevmeteo.ru';\nconst SUBJECT = 'Предупреждение о НМУ';\n\nreturn $input.all().map((item) => {\n const json = item.json || {};\n const fromText = String(json.from?.value?.[0]?.address || json.from?.text || json.from || '').toLowerCase();\n const subject = String(json.subject || '');\n const binaryKeys = Object.keys(item.binary || {});\n\n const matched = fromText.includes(FROM) && subject.trim() === SUBJECT && binaryKeys.length > 0;\n\n return {\n json: {\n ...json,\n nmu_should_process: matched,\n nmu_filter_reason: matched ? 'matched' : `from=${fromText}; subject=${subject}; attachments=${binaryKeys.length}`,\n nmu_binary_keys: binaryKeys\n },\n binary: item.binary\n };\n});"
|
||
},
|
||
"id": "6ea5d2c9-ed64-4e35-bab2-492482df028e",
|
||
"name": "Filter NMU Message",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
-520,
|
||
160
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"conditions": {
|
||
"boolean": [
|
||
{
|
||
"value1": "={{ $json.nmu_should_process }}",
|
||
"value2": true
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"id": "86befd17-05e7-46c3-b16d-c5b67b820fe3",
|
||
"name": "Should Process?",
|
||
"type": "n8n-nodes-base.if",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
-280,
|
||
160
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const item = $input.first();\nconst binary = item.binary || {};\nconst imageKey = Object.keys(binary).find((key) => {\n const mime = String(binary[key].mimeType || '').toLowerCase();\n return mime.startsWith('image/');\n});\n\nif (!imageKey) {\n throw new Error('В письме нет вложения-изображения. Если приходят PDF/TIFF, добавьте конвертацию перед OCR.');\n}\n\nconst attachment = binary[imageKey];\nconst mimeType = attachment.mimeType || 'image/jpeg';\nconst dataUrl = `data:${mimeType};base64,${attachment.data}`;\n\nconst openaiBody = {\n model: 'QWEN3.5-9B',\n temperature: 0,\n messages: [\n {\n role: 'system',\n content: 'Ты извлекаешь данные из скана официального письма. Верни только валидный JSON без markdown, пояснений и комментариев.'\n },\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: 'Распознай документ на изображении. Нужно извлечь: дату письма рядом с исходящим номером, исходящий номер после символа №, основной текст предупреждения НМУ и полный распознанный текст. Верни только валидный JSON в формате {\"date\":\"ДД.ММ.ГГГГ или null\",\"outgoing_number\":\"строка или null\",\"warning_text\":\"строка\",\"full_text\":\"строка\"}. Если поле не найдено, верни null. Не добавляй markdown.'\n },\n {\n type: 'image_url',\n image_url: {\n url: dataUrl\n }\n }\n ]\n }\n ]\n};\n\nreturn [{\n json: {\n message_id: item.json.messageId || item.json.message_id || null,\n original_subject: item.json.subject || '',\n original_from: item.json.from || '',\n attachment_key: imageKey,\n attachment_file_name: attachment.fileName || imageKey,\n attachment_mime_type: mimeType,\n openai_body: JSON.stringify(openaiBody)\n },\n binary: item.binary\n}];"
|
||
},
|
||
"id": "753f7228-2e78-4192-b82a-e3f6da3cbe9d",
|
||
"name": "Prepare Qwen Vision Request",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
-20,
|
||
40
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"method": "POST",
|
||
"url": "http://10.1.1.1:4000/v1/chat/completions",
|
||
"sendHeaders": true,
|
||
"headerParameters": {
|
||
"parameters": [
|
||
{
|
||
"name": "Content-Type",
|
||
"value": "application/json"
|
||
}
|
||
]
|
||
},
|
||
"sendBody": true,
|
||
"contentType": "raw",
|
||
"rawContentType": "application/json",
|
||
"body": "={{ $json.openai_body }}",
|
||
"options": {
|
||
"timeout": 180000
|
||
}
|
||
},
|
||
"id": "c8992117-91e8-4855-9f71-089d2db07d6d",
|
||
"name": "OCR via QWEN3.5-9B",
|
||
"type": "n8n-nodes-base.httpRequest",
|
||
"typeVersion": 4.2,
|
||
"position": [
|
||
240,
|
||
40
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "function extractJson(text) {\n const clean = String(text || '').trim()\n .replace(/^```json\\s*/i, '')\n .replace(/^```\\s*/i, '')\n .replace(/```$/i, '')\n .trim();\n\n try {\n return JSON.parse(clean);\n } catch (error) {\n const match = clean.match(/\\{[\\s\\S]*\\}/);\n if (match) return JSON.parse(match[0]);\n throw error;\n }\n}\n\nconst response = $json;\nconst content = response.choices?.[0]?.message?.content || response.choices?.[0]?.text || response.content || '';\nconst parsed = extractJson(content);\n\nconst fullText = String(parsed.full_text || '');\nconst dateMatch = fullText.match(/\\b\\d{2}\\.\\d{2}\\.\\d{4}\\b/);\nconst numberMatch = fullText.match(/№\\s*([А-ЯA-Z0-9][А-ЯA-Z0-9\\/-]*)/i);\n\nconst date = parsed.date || dateMatch?.[0] || null;\nconst outgoingNumber = parsed.outgoing_number || numberMatch?.[1] || null;\n\nreturn [{\n json: {\n date,\n outgoing_number: outgoingNumber,\n warning_text: parsed.warning_text || null,\n full_text: parsed.full_text || content,\n raw_model_content: content\n }\n}];"
|
||
},
|
||
"id": "6f2e9dcc-dfc3-4838-8c42-8988fc433559",
|
||
"name": "Parse OCR JSON",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
500,
|
||
40
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"jsCode": "const NMU_RECIPIENTS = [\n 'recipient1@appm.ru',\n 'recipient2@appm.ru'\n];\n\nconst date = $json.date || 'дата не распознана';\nconst number = $json.outgoing_number || 'номер не распознан';\nconst warningText = $json.warning_text || $json.full_text || 'Текст предупреждения не распознан.';\n\nconst subject = `Предупреждение о НМУ от ${date} № ${number}`;\nconst text = [\n `Дата письма: ${date}`,\n `Исходящий номер: ${number}`,\n '',\n 'Текст предупреждения:',\n warningText,\n '',\n 'Полный распознанный текст:',\n $json.full_text || ''\n].join('\\n');\n\nreturn [{\n json: {\n to_email: NMU_RECIPIENTS.join(','),\n from_email: 'disp5@appm.ru',\n subject,\n text,\n date: $json.date,\n outgoing_number: $json.outgoing_number\n }\n}];"
|
||
},
|
||
"id": "af0f0426-8e65-4040-ba56-3450c1d223f0",
|
||
"name": "Build Forward Email",
|
||
"type": "n8n-nodes-base.code",
|
||
"typeVersion": 2,
|
||
"position": [
|
||
760,
|
||
40
|
||
]
|
||
},
|
||
{
|
||
"parameters": {
|
||
"fromEmail": "={{ $json.from_email }}",
|
||
"toEmail": "={{ $json.to_email }}",
|
||
"subject": "={{ $json.subject }}",
|
||
"emailFormat": "text",
|
||
"text": "={{ $json.text }}",
|
||
"options": {}
|
||
},
|
||
"id": "3ff0b578-b265-4267-944f-8cb95e4a262a",
|
||
"name": "Send SMTP Email",
|
||
"type": "n8n-nodes-base.emailSend",
|
||
"typeVersion": 2.1,
|
||
"position": [
|
||
1020,
|
||
40
|
||
],
|
||
"credentials": {
|
||
"smtp": {
|
||
"id": "",
|
||
"name": "SMTP 10.0.3.22 no auth"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"parameters": {
|
||
"content": "Если узел HTTP Request вернет ошибку про image_url/base64, значит локальный OpenAI-compatible сервер не принимает data URL. Тогда нужен промежуточный HTTP-доступ к файлу или отдельный OCR endpoint, который принимает multipart/form-data.",
|
||
"height": 180,
|
||
"width": 360
|
||
},
|
||
"id": "f0631818-7c64-43dd-b901-08af5eb3de92",
|
||
"name": "Qwen note",
|
||
"type": "n8n-nodes-base.stickyNote",
|
||
"typeVersion": 1,
|
||
"position": [
|
||
160,
|
||
-220
|
||
]
|
||
}
|
||
],
|
||
"pinData": {},
|
||
"connections": {
|
||
"IMAP Email Trigger": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Filter NMU Message",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Filter NMU Message": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Should Process?",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Should Process?": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Prepare Qwen Vision Request",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
],
|
||
[]
|
||
]
|
||
},
|
||
"Prepare Qwen Vision Request": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "OCR via QWEN3.5-9B",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"OCR via QWEN3.5-9B": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Parse OCR JSON",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Parse OCR JSON": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Build Forward Email",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
},
|
||
"Build Forward Email": {
|
||
"main": [
|
||
[
|
||
{
|
||
"node": "Send SMTP Email",
|
||
"type": "main",
|
||
"index": 0
|
||
}
|
||
]
|
||
]
|
||
}
|
||
},
|
||
"active": false,
|
||
"settings": {
|
||
"executionOrder": "v1",
|
||
"saveManualExecutions": true,
|
||
"callerPolicy": "workflowsFromSameOwner"
|
||
},
|
||
"versionId": "68b03930-1a27-48e3-8a17-2c61433e4800",
|
||
"meta": {
|
||
"templateCredsSetupCompleted": false
|
||
},
|
||
"id": "NMU_QWEN_LOCAL_OCR",
|
||
"tags": []
|
||
}
|