{ "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": [] }