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