From fc0288d13dacf70309aa8c09adafefd9ced42ea9 Mon Sep 17 00:00:00 2001 From: dddennnisss Date: Wed, 15 Jul 2026 12:00:27 +0700 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20TrueConf=20Chatbot=20=D0=9A?= =?UTF-8?q?=D0=9B=D0=95=D0=92=D0=95=D0=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 36 + bot_log.txt | 1099 +++++++++++++++++++++++++++++ config/.env | 14 + config/config.py | 219 ++++++ config/test_pw.py | 42 ++ docker-compose.yml | 18 + lk/__init__.py | 0 lk/documents_service.py | 115 +++ lk/handlers.py | 682 ++++++++++++++++++ lk/info_service.py | 378 ++++++++++ lk/logs/lk_errors.log | 1 + lk/payslip_service.py | 322 +++++++++ lk/test_lk_payslip.py | 213 ++++++ lk/test_sql.py | 77 ++ logs/menu_stats.csv | 18 + main.py | 181 +++++ photo_bot/__init__.py | 0 photo_bot/ad_helper.py | 49 ++ photo_bot/ad_photo_bot.py | 181 +++++ photo_bot/check_ad_photo.py | 83 +++ photo_bot/handlers.py | 242 +++++++ photo_bot/photo_bot_log.txt | 317 +++++++++ photo_bot/photo_processor.py | 127 ++++ requirements.txt | 160 +++++ search_bot/handlers.py | 685 ++++++++++++++++++ search_bot/indexer.py | 173 +++++ search_bot/parse_pdf.py | 70 ++ search_bot/smart_indexer.py | 650 +++++++++++++++++ send_message.py | 54 ++ service_desk/__init__.py | 0 service_desk/handlers.py | 396 +++++++++++ service_desk/logs/sd_workflow.log | 67 ++ show_stats.py | 106 +++ transcription_bot/handlers.py | 127 ++++ trash_api.py | 36 + utils/__init__.py | 0 utils/embedding_client.py | 26 + utils/menu.py | 79 +++ utils/otp_service.py | 133 ++++ utils/states.py | 47 ++ utils/stats_logger.py | 24 + utils/texts.py | 571 +++++++++++++++ 42 files changed, 7818 insertions(+) create mode 100644 Dockerfile create mode 100644 bot_log.txt create mode 100644 config/.env create mode 100644 config/config.py create mode 100644 config/test_pw.py create mode 100644 docker-compose.yml create mode 100644 lk/__init__.py create mode 100644 lk/documents_service.py create mode 100644 lk/handlers.py create mode 100644 lk/info_service.py create mode 100644 lk/logs/lk_errors.log create mode 100644 lk/payslip_service.py create mode 100644 lk/test_lk_payslip.py create mode 100644 lk/test_sql.py create mode 100644 logs/menu_stats.csv create mode 100644 main.py create mode 100644 photo_bot/__init__.py create mode 100644 photo_bot/ad_helper.py create mode 100644 photo_bot/ad_photo_bot.py create mode 100644 photo_bot/check_ad_photo.py create mode 100644 photo_bot/handlers.py create mode 100644 photo_bot/photo_bot_log.txt create mode 100644 photo_bot/photo_processor.py create mode 100644 requirements.txt create mode 100644 search_bot/handlers.py create mode 100644 search_bot/indexer.py create mode 100644 search_bot/parse_pdf.py create mode 100644 search_bot/smart_indexer.py create mode 100644 send_message.py create mode 100644 service_desk/__init__.py create mode 100644 service_desk/handlers.py create mode 100644 service_desk/logs/sd_workflow.log create mode 100644 show_stats.py create mode 100644 transcription_bot/handlers.py create mode 100644 trash_api.py create mode 100644 utils/__init__.py create mode 100644 utils/embedding_client.py create mode 100644 utils/menu.py create mode 100644 utils/otp_service.py create mode 100644 utils/states.py create mode 100644 utils/stats_logger.py create mode 100644 utils/texts.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65248b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Установка системных зависимостей +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + libpq-dev \ + libffi-dev \ + libssl-dev \ + libxml2-dev \ + libxslt1-dev \ + libglib2.0-0 \ + libgl1 \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +# Копирование зависимостей +COPY requirements.txt . + +# Установка Python-зависимостей +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir \ + --extra-index-url https://download.pytorch.org/whl/cpu \ + -r requirements.txt + +# Директории для логов (создаются при запуске через volumes) +# Код проекта монтируется через volumes в docker-compose.yml + +# Переменные окружения +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Команда запуска +CMD ["python3", "main.py"] diff --git a/bot_log.txt b/bot_log.txt new file mode 100644 index 0000000..27966cf --- /dev/null +++ b/bot_log.txt @@ -0,0 +1,1099 @@ +2026-07-02 18:38:57,757 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.05 MB +2026-07-02 18:38:58,429 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.55 MB +2026-07-02 18:38:59,025 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.01 MB +2026-07-02 18:38:59,029 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.01 MB +2026-07-02 18:39:03,628 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.01 MB +2026-07-02 18:39:06,098 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.30 MB +2026-07-02 18:39:06,203 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.11 MB +2026-07-02 18:39:06,624 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.45 MB +2026-07-02 18:39:06,626 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.45 MB +2026-07-02 18:39:09,396 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.60 MB +2026-07-02 18:39:11,768 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.68 MB +2026-07-02 18:39:11,902 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.55 MB +2026-07-02 18:39:12,341 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.88 MB +2026-07-02 18:39:12,343 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.88 MB +2026-07-02 18:39:15,198 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.73 MB +2026-07-02 18:39:17,776 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.88 MB +2026-07-02 18:39:17,874 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.26 MB +2026-07-02 18:39:18,291 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.57 MB +2026-07-02 18:39:18,294 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.57 MB +2026-07-02 18:39:21,233 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.44 MB +2026-07-02 18:39:24,263 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.12 MB +2026-07-02 18:39:24,366 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.90 MB +2026-07-02 18:39:24,761 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.24 MB +2026-07-02 18:39:24,763 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.24 MB +2026-07-02 18:39:27,636 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.11 MB +2026-07-02 18:39:31,373 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.29 MB +2026-07-02 18:39:31,474 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.14 MB +2026-07-02 18:39:31,923 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.49 MB +2026-07-02 18:39:31,925 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.49 MB +2026-07-02 18:39:34,877 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.64 MB +2026-07-02 18:39:40,252 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.25 MB +2026-07-02 18:39:40,359 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.12 MB +2026-07-02 18:39:40,791 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.46 MB +2026-07-02 18:39:40,795 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.46 MB +2026-07-02 18:39:43,617 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.92 MB +2026-07-02 18:39:52,209 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.24 MB +2026-07-02 18:39:52,323 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.05 MB +2026-07-02 18:39:52,798 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.39 MB +2026-07-02 18:39:52,800 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.39 MB +2026-07-02 18:39:55,649 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.62 MB +2026-07-02 18:58:37,181 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.12 MB +2026-07-02 18:58:38,345 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.92 MB +2026-07-02 18:58:38,915 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.38 MB +2026-07-02 18:58:38,917 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.38 MB +2026-07-02 18:58:42,314 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.52 MB +2026-07-02 18:58:43,259 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 398.06 MB +2026-07-02 18:58:43,259 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-02 18:58:43,438 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-02 18:58:43,439 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-02 18:58:43,440 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-02 18:58:43,462 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-02 18:58:43,557 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-02 18:58:43,557 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-02 18:58:43,557 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-02 18:58:43,558 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-02 18:58:43,558 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-02 18:58:43,558 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-02 18:58:53,503 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-02 18:59:04,143 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018744056, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8546, position='0'), content=TextContent(text='dsa', parse_mode='html'), message_id='3dd0fc50-5e6f-4e8e-8fa8-d036ff976ae3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:04,143 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:04,143 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018744056, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8546, position='0'), content=TextContent(text='dsa', parse_mode='html'), message_id='3dd0fc50-5e6f-4e8e-8fa8-d036ff976ae3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:04,144 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:04,144 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018744056, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8546, position='0'), content=TextContent(text='dsa', parse_mode='html'), message_id='3dd0fc50-5e6f-4e8e-8fa8-d036ff976ae3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:04,144 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:04,144 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018744056, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8546, position='0'), content=TextContent(text='dsa', parse_mode='html'), message_id='3dd0fc50-5e6f-4e8e-8fa8-d036ff976ae3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:04,144 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:04,144 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018744056, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8546, position='0'), content=TextContent(text='dsa', parse_mode='html'), message_id='3dd0fc50-5e6f-4e8e-8fa8-d036ff976ae3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:04,144 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:04,144 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018744056, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8546, position='0'), content=TextContent(text='dsa', parse_mode='html'), message_id='3dd0fc50-5e6f-4e8e-8fa8-d036ff976ae3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:04,145 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:04,145 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:48,063 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018787979, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8548, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1aec10c5-9e48-4911-84e0-1966a11091a0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:48,064 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:48,064 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018787979, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8548, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1aec10c5-9e48-4911-84e0-1966a11091a0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:48,064 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:48,064 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018787979, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8548, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1aec10c5-9e48-4911-84e0-1966a11091a0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:48,064 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:48,064 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018787979, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8548, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1aec10c5-9e48-4911-84e0-1966a11091a0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:48,064 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:48,064 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018787979, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8548, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1aec10c5-9e48-4911-84e0-1966a11091a0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:48,065 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:48,065 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018787979, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8548, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1aec10c5-9e48-4911-84e0-1966a11091a0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:48,065 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:48,065 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:49,600 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018789507, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8550, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='75ef5b83-3a33-4f8f-a81e-291ca899c1e7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:49,601 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:49,601 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018789507, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8550, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='75ef5b83-3a33-4f8f-a81e-291ca899c1e7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:49,601 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:49,601 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018789507, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8550, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='75ef5b83-3a33-4f8f-a81e-291ca899c1e7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:49,602 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:49,602 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018789507, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8550, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='75ef5b83-3a33-4f8f-a81e-291ca899c1e7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:49,602 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:49,602 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018789507, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8550, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='75ef5b83-3a33-4f8f-a81e-291ca899c1e7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:49,602 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:49,602 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018789507, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8550, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='75ef5b83-3a33-4f8f-a81e-291ca899c1e7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:49,603 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:49,603 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:51,981 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018791892, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8553, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='b6b974ba-c6e3-4170-8317-3a5beda48871', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:51,981 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:51,981 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018791892, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8553, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='b6b974ba-c6e3-4170-8317-3a5beda48871', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:51,982 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:51,982 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018791892, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8553, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='b6b974ba-c6e3-4170-8317-3a5beda48871', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:51,982 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:51,982 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018791892, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8553, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='b6b974ba-c6e3-4170-8317-3a5beda48871', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:51,982 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:51,982 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018791892, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8553, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='b6b974ba-c6e3-4170-8317-3a5beda48871', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:51,983 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:51,983 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018791892, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8553, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='b6b974ba-c6e3-4170-8317-3a5beda48871', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:51,983 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:51,983 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:53,139 [INFO] httpx - HTTP Request: GET https://mobileapp.sibcem.ru/MobDriveService/MobDriveService/MobService.asmx/SendSMS?_number=%2B79235086996&_message=%D0%9A%D0%BE%D0%B4+%D0%B0%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8+%D0%A1%D0%B8%D0%B1%D1%86%D0%B5%D0%BC+%3A+3085 "HTTP/1.1 200 OK" +2026-07-02 18:59:53,141 [INFO] utils.otp_service - Сырой ответ OTP-шлюза: +Сообщение успешно отправлено в MAX на номер *******6996. +2026-07-02 18:59:58,384 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018798299, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8556, position='0'), content=TextContent(text='3085', parse_mode='html'), message_id='7f620a97-7821-49ed-be57-2fb0c6715878', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:58,385 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:58,386 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018798299, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8556, position='0'), content=TextContent(text='3085', parse_mode='html'), message_id='7f620a97-7821-49ed-be57-2fb0c6715878', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:58,386 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:58,387 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018798299, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8556, position='0'), content=TextContent(text='3085', parse_mode='html'), message_id='7f620a97-7821-49ed-be57-2fb0c6715878', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:58,387 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:58,387 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018798299, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8556, position='0'), content=TextContent(text='3085', parse_mode='html'), message_id='7f620a97-7821-49ed-be57-2fb0c6715878', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:58,387 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:58,387 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018798299, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8556, position='0'), content=TextContent(text='3085', parse_mode='html'), message_id='7f620a97-7821-49ed-be57-2fb0c6715878', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:58,388 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:58,388 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018798299, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8556, position='0'), content=TextContent(text='3085', parse_mode='html'), message_id='7f620a97-7821-49ed-be57-2fb0c6715878', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 18:59:58,388 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 18:59:58,388 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:59,164 [DEBUG] fontTools.ttLib.ttFont - Reading 'maxp' table from disk +2026-07-02 18:59:59,165 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'maxp' table +2026-07-02 18:59:59,165 [DEBUG] fontTools.subset.timer - Took 0.001s to load 'maxp' +2026-07-02 18:59:59,165 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'maxp' +2026-07-02 18:59:59,165 [INFO] fontTools.subset - maxp pruned +2026-07-02 18:59:59,167 [DEBUG] fontTools.ttLib.ttFont - Reading 'cmap' table from disk +2026-07-02 18:59:59,167 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'cmap' table +2026-07-02 18:59:59,168 [DEBUG] fontTools.ttLib.ttFont - Reading 'post' table from disk +2026-07-02 18:59:59,168 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'post' table +2026-07-02 18:59:59,177 [DEBUG] fontTools.subset.timer - Took 0.010s to load 'cmap' +2026-07-02 18:59:59,177 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'cmap' +2026-07-02 18:59:59,178 [INFO] fontTools.subset - cmap pruned +2026-07-02 18:59:59,178 [INFO] fontTools.subset - fpgm dropped +2026-07-02 18:59:59,178 [INFO] fontTools.subset - prep dropped +2026-07-02 18:59:59,178 [INFO] fontTools.subset - cvt dropped +2026-07-02 18:59:59,178 [INFO] fontTools.subset - kern dropped +2026-07-02 18:59:59,178 [DEBUG] fontTools.subset.timer - Took 0.000s to load 'post' +2026-07-02 18:59:59,178 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'post' +2026-07-02 18:59:59,178 [INFO] fontTools.subset - post pruned +2026-07-02 18:59:59,180 [INFO] fontTools.subset - GPOS dropped +2026-07-02 18:59:59,180 [INFO] fontTools.subset - GSUB dropped +2026-07-02 18:59:59,180 [DEBUG] fontTools.ttLib.ttFont - Reading 'glyf' table from disk +2026-07-02 18:59:59,181 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'glyf' table +2026-07-02 18:59:59,181 [DEBUG] fontTools.ttLib.ttFont - Reading 'loca' table from disk +2026-07-02 18:59:59,181 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'loca' table +2026-07-02 18:59:59,181 [DEBUG] fontTools.ttLib.ttFont - Reading 'head' table from disk +2026-07-02 18:59:59,181 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'head' table +2026-07-02 18:59:59,522 [DEBUG] fontTools.subset.timer - Took 0.342s to load 'glyf' +2026-07-02 18:59:59,523 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'glyf' +2026-07-02 18:59:59,523 [INFO] fontTools.subset - glyf pruned +2026-07-02 18:59:59,525 [DEBUG] fontTools.subset.timer - Took 0.001s to close glyph list over 'cmap' +2026-07-02 18:59:59,525 [INFO] fontTools.subset - Added gid0 to subset +2026-07-02 18:59:59,525 [INFO] fontTools.subset - Closing glyph list over 'glyf': 42 glyphs before +2026-07-02 18:59:59,525 [INFO] fontTools.subset - Glyph names: ['.notdef', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni041C', 'uni041D', 'uni0420', 'uni0423', 'uni0430', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0445', 'uni0447', 'uni044B', 'zero'] +2026-07-02 18:59:59,526 [INFO] fontTools.subset - Glyph IDs: [0, 3, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 935, 937, 945, 946, 949, 952, 965, 967, 968, 969, 970, 971, 972, 973, 974, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 988, 992] +2026-07-02 18:59:59,527 [INFO] fontTools.subset - Closed glyph list over 'glyf': 56 glyphs after +2026-07-02 18:59:59,527 [INFO] fontTools.subset - Glyph names: ['.notdef', 'B', 'H', 'M', 'P', 'a', 'breve', 'c', 'comma', 'dotlessi', 'e', 'eight', 'five', 'four', 'nine', 'o', 'one', 'p', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni041C', 'uni041D', 'uni0420', 'uni0423', 'uni0430', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0445', 'uni0447', 'uni044B', 'uni044C', 'x', 'y', 'zero'] +2026-07-02 18:59:59,527 [INFO] fontTools.subset - Glyph IDs: [0, 3, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 37, 43, 48, 51, 68, 70, 72, 82, 83, 91, 92, 243, 666, 935, 937, 945, 946, 949, 952, 965, 967, 968, 969, 970, 971, 972, 973, 974, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 988, 992, 993] +2026-07-02 18:59:59,527 [DEBUG] fontTools.subset.timer - Took 0.002s to close glyph list over 'glyf' +2026-07-02 18:59:59,530 [INFO] fontTools.subset - Retaining 56 glyphs +2026-07-02 18:59:59,530 [INFO] fontTools.subset - head subsetting not needed +2026-07-02 18:59:59,530 [INFO] fontTools.subset - hhea subsetting not needed +2026-07-02 18:59:59,530 [INFO] fontTools.subset - maxp subsetting not needed +2026-07-02 18:59:59,531 [INFO] fontTools.subset - OS/2 subsetting not needed +2026-07-02 18:59:59,531 [DEBUG] fontTools.ttLib.ttFont - Reading 'hmtx' table from disk +2026-07-02 18:59:59,531 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hmtx' table +2026-07-02 18:59:59,531 [DEBUG] fontTools.ttLib.ttFont - Reading 'hhea' table from disk +2026-07-02 18:59:59,531 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hhea' table +2026-07-02 18:59:59,536 [DEBUG] fontTools.subset.timer - Took 0.005s to subset 'hmtx' +2026-07-02 18:59:59,536 [INFO] fontTools.subset - hmtx subsetted +2026-07-02 18:59:59,538 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'cmap' +2026-07-02 18:59:59,538 [INFO] fontTools.subset - cmap subsetted +2026-07-02 18:59:59,538 [INFO] fontTools.subset - loca subsetting not needed +2026-07-02 18:59:59,538 [DEBUG] fontTools.subset.timer - Took 0.000s to subset 'post' +2026-07-02 18:59:59,538 [INFO] fontTools.subset - post subsetted +2026-07-02 18:59:59,538 [INFO] fontTools.subset - gasp subsetting not needed +2026-07-02 18:59:59,538 [INFO] fontTools.subset - FFTM NOT subset; don't know how to subset +2026-07-02 18:59:59,538 [DEBUG] fontTools.ttLib.ttFont - Reading 'GDEF' table from disk +2026-07-02 18:59:59,539 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'GDEF' table +2026-07-02 18:59:59,541 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'GDEF' +2026-07-02 18:59:59,541 [INFO] fontTools.subset - GDEF subsetted +2026-07-02 18:59:59,541 [INFO] fontTools.subset - name subsetting not needed +2026-07-02 18:59:59,543 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'glyf' +2026-07-02 18:59:59,543 [INFO] fontTools.subset - glyf subsetted +2026-07-02 18:59:59,543 [DEBUG] fontTools.subset.timer - Took 0.000s to subset GlyphOrder +2026-07-02 18:59:59,543 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'head' +2026-07-02 18:59:59,543 [INFO] fontTools.subset - head pruned +2026-07-02 18:59:59,543 [DEBUG] fontTools.ttLib.ttFont - Reading 'OS/2' table from disk +2026-07-02 18:59:59,544 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'OS/2' table +2026-07-02 18:59:59,544 [INFO] fontTools.subset - OS/2 Unicode ranges pruned: [0, 9] +2026-07-02 18:59:59,544 [INFO] fontTools.subset - OS/2 CodePage ranges pruned: [0] +2026-07-02 18:59:59,545 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'glyf' +2026-07-02 18:59:59,545 [INFO] fontTools.subset - glyf pruned +2026-07-02 18:59:59,545 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'GDEF' +2026-07-02 18:59:59,546 [INFO] fontTools.subset - GDEF pruned +2026-07-02 18:59:59,546 [DEBUG] fontTools.ttLib.ttFont - Reading 'name' table from disk +2026-07-02 18:59:59,546 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'name' table +2026-07-02 18:59:59,546 [DEBUG] fontTools.ttLib.ttFont - Reading 'gasp' table from disk +2026-07-02 18:59:59,546 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'gasp' table +2026-07-02 18:59:59,546 [DEBUG] fontTools.ttLib.ttFont - Reading 'FFTM' table from disk +2026-07-02 18:59:59,546 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'FFTM' table +2026-07-02 18:59:59,547 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'name' +2026-07-02 18:59:59,547 [INFO] fontTools.subset - name pruned +2026-07-02 18:59:59,558 [DEBUG] fontTools.ttLib.ttFont - Reading 'maxp' table from disk +2026-07-02 18:59:59,558 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'maxp' table +2026-07-02 18:59:59,558 [DEBUG] fontTools.subset.timer - Took 0.000s to load 'maxp' +2026-07-02 18:59:59,558 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'maxp' +2026-07-02 18:59:59,559 [INFO] fontTools.subset - maxp pruned +2026-07-02 18:59:59,559 [DEBUG] fontTools.ttLib.ttFont - Reading 'cmap' table from disk +2026-07-02 18:59:59,559 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'cmap' table +2026-07-02 18:59:59,560 [DEBUG] fontTools.ttLib.ttFont - Reading 'post' table from disk +2026-07-02 18:59:59,560 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'post' table +2026-07-02 18:59:59,569 [DEBUG] fontTools.subset.timer - Took 0.010s to load 'cmap' +2026-07-02 18:59:59,569 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'cmap' +2026-07-02 18:59:59,569 [INFO] fontTools.subset - cmap pruned +2026-07-02 18:59:59,570 [INFO] fontTools.subset - fpgm dropped +2026-07-02 18:59:59,570 [INFO] fontTools.subset - prep dropped +2026-07-02 18:59:59,570 [INFO] fontTools.subset - cvt dropped +2026-07-02 18:59:59,570 [INFO] fontTools.subset - kern dropped +2026-07-02 18:59:59,570 [DEBUG] fontTools.subset.timer - Took 0.000s to load 'post' +2026-07-02 18:59:59,570 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'post' +2026-07-02 18:59:59,570 [INFO] fontTools.subset - post pruned +2026-07-02 18:59:59,570 [INFO] fontTools.subset - GPOS dropped +2026-07-02 18:59:59,570 [INFO] fontTools.subset - GSUB dropped +2026-07-02 18:59:59,570 [DEBUG] fontTools.ttLib.ttFont - Reading 'glyf' table from disk +2026-07-02 18:59:59,571 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'glyf' table +2026-07-02 18:59:59,571 [DEBUG] fontTools.ttLib.ttFont - Reading 'loca' table from disk +2026-07-02 18:59:59,571 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'loca' table +2026-07-02 18:59:59,571 [DEBUG] fontTools.ttLib.ttFont - Reading 'head' table from disk +2026-07-02 18:59:59,571 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'head' table +2026-07-02 18:59:59,578 [DEBUG] fontTools.subset.timer - Took 0.007s to load 'glyf' +2026-07-02 18:59:59,578 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'glyf' +2026-07-02 18:59:59,578 [INFO] fontTools.subset - glyf pruned +2026-07-02 18:59:59,579 [DEBUG] fontTools.subset.timer - Took 0.001s to close glyph list over 'cmap' +2026-07-02 18:59:59,580 [INFO] fontTools.subset - Added gid0 to subset +2026-07-02 18:59:59,580 [INFO] fontTools.subset - Closing glyph list over 'MATH': 58 glyphs before +2026-07-02 18:59:59,580 [INFO] fontTools.subset - Glyph names: ['.notdef', 'colon', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0410', 'uni0412', 'uni0414', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044F', 'zero'] +2026-07-02 18:59:59,581 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 933, 935, 937, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 996] +2026-07-02 18:59:59,581 [DEBUG] fontTools.ttLib.ttFont - Reading 'MATH' table from disk +2026-07-02 18:59:59,581 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'MATH' table +2026-07-02 18:59:59,583 [INFO] fontTools.subset - Closed glyph list over 'MATH': 64 glyphs after +2026-07-02 18:59:59,583 [INFO] fontTools.subset - Glyph names: ['.notdef', 'colon', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0410', 'uni0412', 'uni0414', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044F', 'uni239B', 'uni239C', 'uni239D', 'uni239E', 'uni239F', 'uni23A0', 'zero'] +2026-07-02 18:59:59,584 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 933, 935, 937, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 996, 3507, 3508, 3509, 3510, 3511, 3512] +2026-07-02 18:59:59,584 [DEBUG] fontTools.subset.timer - Took 0.004s to close glyph list over 'MATH' +2026-07-02 18:59:59,584 [INFO] fontTools.subset - Closing glyph list over 'glyf': 64 glyphs before +2026-07-02 18:59:59,584 [INFO] fontTools.subset - Glyph names: ['.notdef', 'colon', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0410', 'uni0412', 'uni0414', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044F', 'uni239B', 'uni239C', 'uni239D', 'uni239E', 'uni239F', 'uni23A0', 'zero'] +2026-07-02 18:59:59,584 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 933, 935, 937, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 996, 3507, 3508, 3509, 3510, 3511, 3512] +2026-07-02 18:59:59,584 [INFO] fontTools.subset - Closed glyph list over 'glyf': 79 glyphs after +2026-07-02 18:59:59,584 [INFO] fontTools.subset - Glyph names: ['.notdef', 'A', 'B', 'C', 'H', 'M', 'O', 'T', 'a', 'breve', 'c', 'colon', 'comma', 'dotlessi', 'e', 'eight', 'five', 'four', 'nine', 'o', 'one', 'p', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0410', 'uni0412', 'uni0414', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044F', 'uni239B', 'uni239C', 'uni239D', 'uni239E', 'uni239F', 'uni23A0', 'y', 'zero'] +2026-07-02 18:59:59,585 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 36, 37, 38, 43, 48, 50, 55, 68, 70, 72, 82, 83, 92, 243, 666, 933, 935, 937, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 996, 3507, 3508, 3509, 3510, 3511, 3512] +2026-07-02 18:59:59,585 [DEBUG] fontTools.subset.timer - Took 0.001s to close glyph list over 'glyf' +2026-07-02 18:59:59,589 [INFO] fontTools.subset - Retaining 79 glyphs +2026-07-02 18:59:59,589 [INFO] fontTools.subset - head subsetting not needed +2026-07-02 18:59:59,589 [INFO] fontTools.subset - hhea subsetting not needed +2026-07-02 18:59:59,589 [INFO] fontTools.subset - maxp subsetting not needed +2026-07-02 18:59:59,590 [INFO] fontTools.subset - OS/2 subsetting not needed +2026-07-02 18:59:59,590 [DEBUG] fontTools.ttLib.ttFont - Reading 'hmtx' table from disk +2026-07-02 18:59:59,590 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hmtx' table +2026-07-02 18:59:59,590 [DEBUG] fontTools.ttLib.ttFont - Reading 'hhea' table from disk +2026-07-02 18:59:59,590 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hhea' table +2026-07-02 18:59:59,594 [DEBUG] fontTools.subset.timer - Took 0.004s to subset 'hmtx' +2026-07-02 18:59:59,594 [INFO] fontTools.subset - hmtx subsetted +2026-07-02 18:59:59,596 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'cmap' +2026-07-02 18:59:59,596 [INFO] fontTools.subset - cmap subsetted +2026-07-02 18:59:59,596 [INFO] fontTools.subset - loca subsetting not needed +2026-07-02 18:59:59,597 [DEBUG] fontTools.subset.timer - Took 0.000s to subset 'post' +2026-07-02 18:59:59,597 [INFO] fontTools.subset - post subsetted +2026-07-02 18:59:59,597 [INFO] fontTools.subset - gasp subsetting not needed +2026-07-02 18:59:59,597 [INFO] fontTools.subset - FFTM NOT subset; don't know how to subset +2026-07-02 18:59:59,597 [DEBUG] fontTools.subset.timer - Took 0.000s to subset 'MATH' +2026-07-02 18:59:59,597 [INFO] fontTools.subset - MATH subsetted +2026-07-02 18:59:59,597 [DEBUG] fontTools.ttLib.ttFont - Reading 'GDEF' table from disk +2026-07-02 18:59:59,597 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'GDEF' table +2026-07-02 18:59:59,599 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'GDEF' +2026-07-02 18:59:59,599 [INFO] fontTools.subset - GDEF subsetted +2026-07-02 18:59:59,600 [INFO] fontTools.subset - name subsetting not needed +2026-07-02 18:59:59,604 [DEBUG] fontTools.subset.timer - Took 0.004s to subset 'glyf' +2026-07-02 18:59:59,604 [INFO] fontTools.subset - glyf subsetted +2026-07-02 18:59:59,604 [DEBUG] fontTools.subset.timer - Took 0.000s to subset GlyphOrder +2026-07-02 18:59:59,604 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'head' +2026-07-02 18:59:59,605 [INFO] fontTools.subset - head pruned +2026-07-02 18:59:59,605 [DEBUG] fontTools.ttLib.ttFont - Reading 'OS/2' table from disk +2026-07-02 18:59:59,605 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'OS/2' table +2026-07-02 18:59:59,605 [INFO] fontTools.subset - OS/2 Unicode ranges pruned: [0, 9] +2026-07-02 18:59:59,605 [INFO] fontTools.subset - OS/2 CodePage ranges pruned: [0] +2026-07-02 18:59:59,606 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'glyf' +2026-07-02 18:59:59,606 [INFO] fontTools.subset - glyf pruned +2026-07-02 18:59:59,607 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'GDEF' +2026-07-02 18:59:59,607 [INFO] fontTools.subset - GDEF pruned +2026-07-02 18:59:59,607 [DEBUG] fontTools.ttLib.ttFont - Reading 'name' table from disk +2026-07-02 18:59:59,607 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'name' table +2026-07-02 18:59:59,607 [DEBUG] fontTools.ttLib.ttFont - Reading 'gasp' table from disk +2026-07-02 18:59:59,608 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'gasp' table +2026-07-02 18:59:59,608 [DEBUG] fontTools.ttLib.ttFont - Reading 'FFTM' table from disk +2026-07-02 18:59:59,608 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'FFTM' table +2026-07-02 18:59:59,608 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'name' +2026-07-02 18:59:59,608 [INFO] fontTools.subset - name pruned +2026-07-02 19:00:02,569 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018802470, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8560, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='a620e4aa-bb04-442d-81eb-e5f395b44e28', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:02,570 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:02,570 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018802470, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8560, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='a620e4aa-bb04-442d-81eb-e5f395b44e28', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:02,570 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:02,570 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018802470, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8560, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='a620e4aa-bb04-442d-81eb-e5f395b44e28', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:02,570 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:02,570 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018802470, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8560, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='a620e4aa-bb04-442d-81eb-e5f395b44e28', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:02,570 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:02,570 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018802470, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8560, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='a620e4aa-bb04-442d-81eb-e5f395b44e28', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:02,571 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:02,571 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018802470, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8560, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='a620e4aa-bb04-442d-81eb-e5f395b44e28', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:02,571 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:02,571 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:03,646 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018803555, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8562, position='0'), content=TextContent(text='dsfds', parse_mode='html'), message_id='65a56b05-bf12-4d36-a9ec-c4669f55e87c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:03,646 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:03,646 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018803555, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8562, position='0'), content=TextContent(text='dsfds', parse_mode='html'), message_id='65a56b05-bf12-4d36-a9ec-c4669f55e87c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:03,647 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:03,647 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018803555, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8562, position='0'), content=TextContent(text='dsfds', parse_mode='html'), message_id='65a56b05-bf12-4d36-a9ec-c4669f55e87c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:03,647 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:03,647 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018803555, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8562, position='0'), content=TextContent(text='dsfds', parse_mode='html'), message_id='65a56b05-bf12-4d36-a9ec-c4669f55e87c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:03,647 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:03,647 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018803555, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8562, position='0'), content=TextContent(text='dsfds', parse_mode='html'), message_id='65a56b05-bf12-4d36-a9ec-c4669f55e87c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:03,647 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:03,647 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018803555, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8562, position='0'), content=TextContent(text='dsfds', parse_mode='html'), message_id='65a56b05-bf12-4d36-a9ec-c4669f55e87c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:03,647 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:03,647 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:15,573 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018815482, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8564, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='3ce67663-f9c6-424c-b342-d982800330e1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:15,573 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:15,573 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018815482, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8564, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='3ce67663-f9c6-424c-b342-d982800330e1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:15,573 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:15,574 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018815482, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8564, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='3ce67663-f9c6-424c-b342-d982800330e1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:15,574 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:15,574 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018815482, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8564, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='3ce67663-f9c6-424c-b342-d982800330e1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:15,574 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:15,574 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018815482, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8564, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='3ce67663-f9c6-424c-b342-d982800330e1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:15,574 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:15,574 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018815482, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8564, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='3ce67663-f9c6-424c-b342-d982800330e1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:15,574 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:15,574 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:47,619 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018847474, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8566, position='0'), content=AttachmentContent(file_name='2025-11-10_15-32-32_Conference_Communigate фидбек.mp4', file_size=13228907, file_id='a793174a679f38fb609e9f8b0c9b48f8bc9b8419', mimetype='video/mp4', ready_state=None), message_id='5d90710d-74fd-4aac-8196-4f475c3c4969', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:47,620 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:47,620 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018847474, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8566, position='0'), content=AttachmentContent(file_name='2025-11-10_15-32-32_Conference_Communigate фидбек.mp4', file_size=13228907, file_id='a793174a679f38fb609e9f8b0c9b48f8bc9b8419', mimetype='video/mp4', ready_state=None), message_id='5d90710d-74fd-4aac-8196-4f475c3c4969', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:47,620 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:47,620 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018847474, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8566, position='0'), content=AttachmentContent(file_name='2025-11-10_15-32-32_Conference_Communigate фидбек.mp4', file_size=13228907, file_id='a793174a679f38fb609e9f8b0c9b48f8bc9b8419', mimetype='video/mp4', ready_state=None), message_id='5d90710d-74fd-4aac-8196-4f475c3c4969', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:47,620 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:47,620 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018847474, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8566, position='0'), content=AttachmentContent(file_name='2025-11-10_15-32-32_Conference_Communigate фидбек.mp4', file_size=13228907, file_id='a793174a679f38fb609e9f8b0c9b48f8bc9b8419', mimetype='video/mp4', ready_state=None), message_id='5d90710d-74fd-4aac-8196-4f475c3c4969', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:47,620 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:47,620 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018847474, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8566, position='0'), content=AttachmentContent(file_name='2025-11-10_15-32-32_Conference_Communigate фидбек.mp4', file_size=13228907, file_id='a793174a679f38fb609e9f8b0c9b48f8bc9b8419', mimetype='video/mp4', ready_state=None), message_id='5d90710d-74fd-4aac-8196-4f475c3c4969', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:47,620 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:47,620 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018847474, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8566, position='0'), content=AttachmentContent(file_name='2025-11-10_15-32-32_Conference_Communigate фидбек.mp4', file_size=13228907, file_id='a793174a679f38fb609e9f8b0c9b48f8bc9b8419', mimetype='video/mp4', ready_state=None), message_id='5d90710d-74fd-4aac-8196-4f475c3c4969', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:00:47,620 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:00:47,620 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:47,926 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/a793174a679f38fb609e9f8b0c9b48f8bc9b8419?token=5d7cf2935cfad1a009e8358ae7e00a4d "HTTP/1.1 200 OK" +2026-07-02 19:00:48,278 [INFO] httpx - HTTP Request: POST http://172.16.80.250:8000/ "HTTP/1.0 200 OK" +2026-07-02 19:01:06,812 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018866721, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8569, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='52b6e9da-8cbc-45cb-84ed-363965782070', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:06,813 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:06,813 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018866721, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8569, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='52b6e9da-8cbc-45cb-84ed-363965782070', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:06,813 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:06,813 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018866721, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8569, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='52b6e9da-8cbc-45cb-84ed-363965782070', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:06,813 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:06,814 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018866721, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8569, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='52b6e9da-8cbc-45cb-84ed-363965782070', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:06,814 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:06,814 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018866721, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8569, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='52b6e9da-8cbc-45cb-84ed-363965782070', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:06,814 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:06,814 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018866721, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8569, position='0'), content=TextContent(text='0', parse_mode='html'), message_id='52b6e9da-8cbc-45cb-84ed-363965782070', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:06,814 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:06,814 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:10,480 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018870386, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8571, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='3a3fa817-780e-448f-95a2-cdee766231a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:10,481 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:10,481 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018870386, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8571, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='3a3fa817-780e-448f-95a2-cdee766231a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:10,481 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:10,482 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018870386, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8571, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='3a3fa817-780e-448f-95a2-cdee766231a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:10,482 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:10,482 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018870386, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8571, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='3a3fa817-780e-448f-95a2-cdee766231a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:10,482 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:10,482 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018870386, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8571, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='3a3fa817-780e-448f-95a2-cdee766231a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:10,482 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:10,483 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018870386, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8571, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='3a3fa817-780e-448f-95a2-cdee766231a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:10,483 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:10,483 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:23,000 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882893, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8573, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='9d2fcfb0-27d7-4ffc-a15f-6e8fe4ce27d5', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,002 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,002 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882893, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8573, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='9d2fcfb0-27d7-4ffc-a15f-6e8fe4ce27d5', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,002 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,002 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882893, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8573, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='9d2fcfb0-27d7-4ffc-a15f-6e8fe4ce27d5', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,002 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,002 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882893, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8573, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='9d2fcfb0-27d7-4ffc-a15f-6e8fe4ce27d5', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,002 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,002 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882893, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8573, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='9d2fcfb0-27d7-4ffc-a15f-6e8fe4ce27d5', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,002 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,002 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882893, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8573, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='9d2fcfb0-27d7-4ffc-a15f-6e8fe4ce27d5', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,002 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,003 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:23,003 [INFO] sd_workflow - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает принтер', is_attachment=False, inline_attachments=0 +2026-07-02 19:01:23,017 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882895, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8574, position='0'), content=AttachmentContent(file_name='image_2026.07.03_02-01-22.webp', file_size=144432, file_id='4adb9e2d3a3bd1cd756874caf46122fb5f0606eb', mimetype='image/webp', ready_state=None), message_id='4fb7ccc2-3d1d-4eb7-8896-3e64fd807ec0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,017 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,017 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882895, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8574, position='0'), content=AttachmentContent(file_name='image_2026.07.03_02-01-22.webp', file_size=144432, file_id='4adb9e2d3a3bd1cd756874caf46122fb5f0606eb', mimetype='image/webp', ready_state=None), message_id='4fb7ccc2-3d1d-4eb7-8896-3e64fd807ec0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,017 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,017 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882895, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8574, position='0'), content=AttachmentContent(file_name='image_2026.07.03_02-01-22.webp', file_size=144432, file_id='4adb9e2d3a3bd1cd756874caf46122fb5f0606eb', mimetype='image/webp', ready_state=None), message_id='4fb7ccc2-3d1d-4eb7-8896-3e64fd807ec0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,017 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,017 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882895, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8574, position='0'), content=AttachmentContent(file_name='image_2026.07.03_02-01-22.webp', file_size=144432, file_id='4adb9e2d3a3bd1cd756874caf46122fb5f0606eb', mimetype='image/webp', ready_state=None), message_id='4fb7ccc2-3d1d-4eb7-8896-3e64fd807ec0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,017 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,017 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882895, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8574, position='0'), content=AttachmentContent(file_name='image_2026.07.03_02-01-22.webp', file_size=144432, file_id='4adb9e2d3a3bd1cd756874caf46122fb5f0606eb', mimetype='image/webp', ready_state=None), message_id='4fb7ccc2-3d1d-4eb7-8896-3e64fd807ec0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,017 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,017 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783018882895, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8574, position='0'), content=AttachmentContent(file_name='image_2026.07.03_02-01-22.webp', file_size=144432, file_id='4adb9e2d3a3bd1cd756874caf46122fb5f0606eb', mimetype='image/webp', ready_state=None), message_id='4fb7ccc2-3d1d-4eb7-8896-3e64fd807ec0', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-02 19:01:23,018 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-02 19:01:23,018 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:23,018 [INFO] sd_workflow - 🚀 [SD Debug] msg.type=MessageType.ATTACHMENT, content_type=AttachmentContent, msg_text='', is_attachment=True, inline_attachments=1 +2026-07-02 19:01:23,018 [INFO] sd_workflow - 📎 [SD] Received 1 more files. Total queue: 1 +2026-07-02 19:01:23,018 [INFO] sd_workflow - 🔄 [SD] Timer reset due to user activity. Waiting another 2.0s +2026-07-02 19:01:23,021 [INFO] sd_workflow - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-02 19:01:25,024 [INFO] sd_workflow - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:25,229 [INFO] sd_workflow - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-02 19:01:25,621 [INFO] sd_workflow - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает принтер'}], 'temperature': 0.0} +2026-07-02 19:01:34,650 [INFO] httpx - HTTP Request: POST http://172.16.80.47:8080/v1/chat/completions "HTTP/1.1 200 OK" +2026-07-02 19:01:34,653 [INFO] sd_workflow - 📝 [Subject Gen] Text: не работает принтер... -> Subject: Не работает принтер +2026-07-02 19:01:35,019 [INFO] httpx - HTTP Request: POST https://servicedesk.sibcem.ru:8080/api/v3/requests "HTTP/1.1 201 " +2026-07-02 19:01:35,020 [INFO] sd_workflow - 🎫 [Ticket Created] ID: 23108 +2026-07-02 19:01:35,021 [INFO] sd_workflow - 📎 [Upload] Uploading 1 file(s) to ticket #23108 +2026-07-02 19:01:35,159 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/4adb9e2d3a3bd1cd756874caf46122fb5f0606eb?token=5d7cf2935cfad1a009e8358ae7e00a4d "HTTP/1.1 200 OK" +2026-07-02 19:01:35,328 [INFO] httpx - HTTP Request: PUT https://servicedesk.sibcem.ru:8080/api/v3/requests/23108/upload "HTTP/1.1 200 " +2026-07-02 19:01:35,328 [INFO] sd_workflow - ✅ [Upload] All queued file(s) uploaded +2026-07-03 01:33:49,454 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783042429284, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8577, position='0'), content=TextContent(text='аываыв', parse_mode='html'), message_id='a5b80885-e3e3-4c18-8a9a-c159f41b77ac', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 01:33:49,455 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 01:33:49,456 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783042429284, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8577, position='0'), content=TextContent(text='аываыв', parse_mode='html'), message_id='a5b80885-e3e3-4c18-8a9a-c159f41b77ac', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 01:33:49,456 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 01:33:49,456 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783042429284, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8577, position='0'), content=TextContent(text='аываыв', parse_mode='html'), message_id='a5b80885-e3e3-4c18-8a9a-c159f41b77ac', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 01:33:49,456 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 01:33:49,456 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783042429284, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8577, position='0'), content=TextContent(text='аываыв', parse_mode='html'), message_id='a5b80885-e3e3-4c18-8a9a-c159f41b77ac', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 01:33:49,456 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 01:33:49,456 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783042429284, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8577, position='0'), content=TextContent(text='аываыв', parse_mode='html'), message_id='a5b80885-e3e3-4c18-8a9a-c159f41b77ac', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 01:33:49,457 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 01:33:49,457 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783042429284, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8577, position='0'), content=TextContent(text='аываыв', parse_mode='html'), message_id='a5b80885-e3e3-4c18-8a9a-c159f41b77ac', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 01:33:49,457 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 01:33:49,457 [INFO] __main__ - ⏳ Сессия пользователя ds.krivochenko@tcs.sibcem.ru сброшена по таймауту бездействия (1800 сек). +2026-07-03 01:33:49,457 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 09:27:27,096 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783070846882, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8579, position='0'), content=TextContent(text='Ир', parse_mode='html'), message_id='fb40fe81-1104-4cf6-93f2-67175abcc387', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 09:27:27,097 [INFO] chat_bot - [router:0x7faadca20890] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 09:27:27,097 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783070846882, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8579, position='0'), content=TextContent(text='Ир', parse_mode='html'), message_id='fb40fe81-1104-4cf6-93f2-67175abcc387', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 09:27:27,097 [INFO] chat_bot - [router:0x7faad0ca7210] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 09:27:27,098 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783070846882, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8579, position='0'), content=TextContent(text='Ир', parse_mode='html'), message_id='fb40fe81-1104-4cf6-93f2-67175abcc387', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 09:27:27,098 [INFO] chat_bot - [router:0x7faadd635510] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 09:27:27,098 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783070846882, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8579, position='0'), content=TextContent(text='Ир', parse_mode='html'), message_id='fb40fe81-1104-4cf6-93f2-67175abcc387', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 09:27:27,098 [INFO] chat_bot - [router:0x7faacd805110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 09:27:27,098 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783070846882, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8579, position='0'), content=TextContent(text='Ир', parse_mode='html'), message_id='fb40fe81-1104-4cf6-93f2-67175abcc387', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 09:27:27,098 [INFO] chat_bot - [router:0x7faaa5236850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 09:27:27,098 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783070846882, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8579, position='0'), content=TextContent(text='Ир', parse_mode='html'), message_id='fb40fe81-1104-4cf6-93f2-67175abcc387', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 09:27:27,098 [INFO] chat_bot - [router:0x7faadca50b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 09:27:27,098 [INFO] __main__ - ⏳ Сессия пользователя ds.krivochenko@tcs.sibcem.ru сброшена по таймауту бездействия (1800 сек). +2026-07-03 09:27:27,099 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:54:09,716 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.92 MB +2026-07-03 14:54:09,821 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.55 MB +2026-07-03 14:54:10,305 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 104.85 MB +2026-07-03 14:54:10,307 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 104.85 MB +2026-07-03 14:54:13,582 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 366.86 MB +2026-07-03 14:54:14,189 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 396.84 MB +2026-07-03 14:54:14,190 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-03 14:54:14,384 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-03 14:54:14,385 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-03 14:54:14,386 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-03 14:54:14,403 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-03 14:54:14,498 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 14:54:14,499 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 14:54:14,500 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 14:54:14,500 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 14:54:14,501 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 14:54:14,501 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 14:54:24,454 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-03 14:55:29,672 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090529591, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8582, position='0'), content=TextContent(text='jj', parse_mode='html'), message_id='1fc06395-80e7-4273-a19f-66cf17db77a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:29,674 [INFO] chat_bot - [router:0x7692adc3fb50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:29,674 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090529591, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8582, position='0'), content=TextContent(text='jj', parse_mode='html'), message_id='1fc06395-80e7-4273-a19f-66cf17db77a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:29,675 [INFO] chat_bot - [router:0x76929e9c5ed0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:29,675 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090529591, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8582, position='0'), content=TextContent(text='jj', parse_mode='html'), message_id='1fc06395-80e7-4273-a19f-66cf17db77a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:29,675 [INFO] chat_bot - [router:0x7692ad6de390] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:29,675 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090529591, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8582, position='0'), content=TextContent(text='jj', parse_mode='html'), message_id='1fc06395-80e7-4273-a19f-66cf17db77a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:29,675 [INFO] chat_bot - [router:0x76929e9fac50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:29,676 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090529591, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8582, position='0'), content=TextContent(text='jj', parse_mode='html'), message_id='1fc06395-80e7-4273-a19f-66cf17db77a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:29,676 [INFO] chat_bot - [router:0x769276f3f690] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:29,676 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090529591, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8582, position='0'), content=TextContent(text='jj', parse_mode='html'), message_id='1fc06395-80e7-4273-a19f-66cf17db77a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:29,676 [INFO] chat_bot - [router:0x769275430b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:29,676 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:32,597 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090532519, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8584, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='d1649514-45a8-408f-ac8f-59a89b835f82', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:32,597 [INFO] chat_bot - [router:0x7692adc3fb50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:32,597 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090532519, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8584, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='d1649514-45a8-408f-ac8f-59a89b835f82', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:32,597 [INFO] chat_bot - [router:0x76929e9c5ed0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:32,597 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090532519, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8584, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='d1649514-45a8-408f-ac8f-59a89b835f82', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:32,598 [INFO] chat_bot - [router:0x7692ad6de390] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:32,598 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090532519, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8584, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='d1649514-45a8-408f-ac8f-59a89b835f82', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:32,598 [INFO] chat_bot - [router:0x76929e9fac50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:32,598 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090532519, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8584, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='d1649514-45a8-408f-ac8f-59a89b835f82', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:32,598 [INFO] chat_bot - [router:0x769276f3f690] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:32,598 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090532519, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8584, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='d1649514-45a8-408f-ac8f-59a89b835f82', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:32,598 [INFO] chat_bot - [router:0x769275430b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:32,598 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:44,533 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090544455, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8586, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='fa77b701-d84c-445d-9f8a-e0edb1de3f4e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:44,533 [INFO] chat_bot - [router:0x7692adc3fb50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:44,533 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090544455, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8586, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='fa77b701-d84c-445d-9f8a-e0edb1de3f4e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:44,533 [INFO] chat_bot - [router:0x76929e9c5ed0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:44,533 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090544455, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8586, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='fa77b701-d84c-445d-9f8a-e0edb1de3f4e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:44,534 [INFO] chat_bot - [router:0x7692ad6de390] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:44,534 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090544455, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8586, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='fa77b701-d84c-445d-9f8a-e0edb1de3f4e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:44,534 [INFO] chat_bot - [router:0x76929e9fac50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:44,534 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090544455, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8586, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='fa77b701-d84c-445d-9f8a-e0edb1de3f4e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:44,534 [INFO] chat_bot - [router:0x769276f3f690] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:44,534 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090544455, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8586, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='fa77b701-d84c-445d-9f8a-e0edb1de3f4e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 14:55:44,534 [INFO] chat_bot - [router:0x769275430b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 14:55:44,534 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:44,534 [INFO] sd_workflow - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает интернет', is_attachment=False, inline_attachments=0 +2026-07-03 14:55:44,565 [INFO] sd_workflow - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-03 14:55:46,568 [INFO] sd_workflow - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:46,820 [INFO] sd_workflow - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-03 14:55:47,278 [INFO] sd_workflow - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает интернет'}], 'temperature': 0.0} +2026-07-03 14:55:55,266 [INFO] httpx - HTTP Request: POST http://172.16.80.47:8080/v1/chat/completions "HTTP/1.1 200 OK" +2026-07-03 14:55:55,269 [INFO] sd_workflow - 📝 [Subject Gen] Text: не работает интернет... -> Subject: Не работает доступ в интернет +2026-07-03 14:55:55,503 [INFO] httpx - HTTP Request: POST https://servicedesk.sibcem.ru:8080/api/v3/requests "HTTP/1.1 201 " +2026-07-03 14:55:55,504 [INFO] sd_workflow - 🎫 [Ticket Created] ID: 23132 +2026-07-03 14:55:55,504 [ERROR] custom_config - Ошибка: sd_ticket_created() missing 2 required positional arguments: 'subject' and 'description' +Traceback (most recent call last): + File "/app/service_desk/handlers.py", line 257, in _create_ticket_and_attach_files + sd_ticket_created(ticket_id), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +TypeError: sd_ticket_created() missing 2 required positional arguments: 'subject' and 'description' +2026-07-03 14:55:55,504 [ERROR] sd_workflow - ❌ [SD Error] sd_ticket_created() missing 2 required positional arguments: 'subject' and 'description' +2026-07-03 14:59:57,364 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.08 MB +2026-07-03 14:59:57,469 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.03 MB +2026-07-03 14:59:57,898 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.36 MB +2026-07-03 14:59:57,900 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.36 MB +2026-07-03 15:00:00,919 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.70 MB +2026-07-03 15:00:01,582 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.56 MB +2026-07-03 15:00:01,583 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-03 15:00:01,735 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-03 15:00:01,736 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-03 15:00:01,736 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-03 15:00:01,755 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-03 15:00:01,847 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:00:01,848 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:00:01,848 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:00:01,849 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:00:01,849 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:00:01,849 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:00:11,798 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-03 15:00:29,652 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090829568, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8590, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='20ca8086-14e7-4152-8b6a-d87e140b06e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:29,653 [INFO] chat_bot - [router:0x74125bdb96d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:29,653 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090829568, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8590, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='20ca8086-14e7-4152-8b6a-d87e140b06e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:29,653 [INFO] chat_bot - [router:0x74124cfd3f10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:29,653 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090829568, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8590, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='20ca8086-14e7-4152-8b6a-d87e140b06e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:29,653 [INFO] chat_bot - [router:0x74124cfa9bd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:29,653 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090829568, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8590, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='20ca8086-14e7-4152-8b6a-d87e140b06e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:29,653 [INFO] chat_bot - [router:0x74124caf9b90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:29,653 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090829568, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8590, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='20ca8086-14e7-4152-8b6a-d87e140b06e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:29,653 [INFO] chat_bot - [router:0x7412245ce2d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:29,653 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090829568, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8590, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='20ca8086-14e7-4152-8b6a-d87e140b06e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:29,654 [INFO] chat_bot - [router:0x74125cd3e310] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:29,654 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:00:34,387 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090834296, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8592, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='307e2bd4-2558-44cd-95c7-9a702f273a7c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:34,388 [INFO] chat_bot - [router:0x74125bdb96d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:34,388 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090834296, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8592, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='307e2bd4-2558-44cd-95c7-9a702f273a7c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:34,388 [INFO] chat_bot - [router:0x74124cfd3f10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:34,388 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090834296, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8592, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='307e2bd4-2558-44cd-95c7-9a702f273a7c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:34,388 [INFO] chat_bot - [router:0x74124cfa9bd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:34,388 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090834296, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8592, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='307e2bd4-2558-44cd-95c7-9a702f273a7c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:34,388 [INFO] chat_bot - [router:0x74124caf9b90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:34,388 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090834296, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8592, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='307e2bd4-2558-44cd-95c7-9a702f273a7c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:34,388 [INFO] chat_bot - [router:0x7412245ce2d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:34,388 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783090834296, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8592, position='0'), content=TextContent(text='не работает интернет', parse_mode='html'), message_id='307e2bd4-2558-44cd-95c7-9a702f273a7c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:00:34,389 [INFO] chat_bot - [router:0x74125cd3e310] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:00:34,389 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:00:34,389 [INFO] sd_workflow - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает интернет', is_attachment=False, inline_attachments=0 +2026-07-03 15:00:34,398 [INFO] sd_workflow - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-03 15:00:36,401 [INFO] sd_workflow - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:00:36,644 [INFO] sd_workflow - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-03 15:00:37,078 [INFO] sd_workflow - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает интернет'}], 'temperature': 0.0} +2026-07-03 15:00:38,733 [INFO] httpx - HTTP Request: POST http://172.16.80.47:8080/v1/chat/completions "HTTP/1.1 200 OK" +2026-07-03 15:00:38,739 [INFO] sd_workflow - 📝 [Subject Gen] Text: не работает интернет... -> Subject: Не работает доступ в интернет +2026-07-03 15:00:39,149 [INFO] httpx - HTTP Request: POST https://servicedesk.sibcem.ru:8080/api/v3/requests "HTTP/1.1 201 " +2026-07-03 15:00:39,151 [INFO] sd_workflow - 🎫 [Ticket Created] ID: 23133 +2026-07-03 15:06:58,655 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.91 MB +2026-07-03 15:06:58,761 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.04 MB +2026-07-03 15:06:59,161 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.37 MB +2026-07-03 15:06:59,165 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.37 MB +2026-07-03 15:07:01,993 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.56 MB +2026-07-03 15:07:02,673 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.57 MB +2026-07-03 15:07:02,674 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-03 15:07:02,861 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-03 15:07:02,862 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-03 15:07:02,862 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-03 15:07:02,881 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-03 15:07:02,975 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:07:02,976 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:07:02,976 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:07:02,976 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:07:02,977 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:07:02,977 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-03 15:07:12,955 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-03 15:07:19,765 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091239654, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8596, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='b313507a-329b-4128-bc63-e38da0e85da6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:19,765 [INFO] chat_bot - [router:0x7b301bbb2a50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:19,765 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091239654, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8596, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='b313507a-329b-4128-bc63-e38da0e85da6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:19,765 [INFO] chat_bot - [router:0x7b300cdcf410] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:19,766 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091239654, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8596, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='b313507a-329b-4128-bc63-e38da0e85da6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:19,766 [INFO] chat_bot - [router:0x7b300cde6b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:19,766 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091239654, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8596, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='b313507a-329b-4128-bc63-e38da0e85da6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:19,766 [INFO] chat_bot - [router:0x7b300c8f4050] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:19,766 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091239654, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8596, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='b313507a-329b-4128-bc63-e38da0e85da6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:19,766 [INFO] chat_bot - [router:0x7b2fe43be6d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:19,766 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091239654, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8596, position='0'), content=TextContent(text='3', parse_mode='html'), message_id='b313507a-329b-4128-bc63-e38da0e85da6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:19,766 [INFO] chat_bot - [router:0x7b2fde8e4e10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:19,766 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:07:25,799 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091245694, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8598, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='1017486f-b3d8-4de3-9fad-30a9c221de3a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:25,799 [INFO] chat_bot - [router:0x7b301bbb2a50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:25,799 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091245694, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8598, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='1017486f-b3d8-4de3-9fad-30a9c221de3a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:25,799 [INFO] chat_bot - [router:0x7b300cdcf410] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:25,799 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091245694, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8598, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='1017486f-b3d8-4de3-9fad-30a9c221de3a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:25,800 [INFO] chat_bot - [router:0x7b300cde6b10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:25,800 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091245694, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8598, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='1017486f-b3d8-4de3-9fad-30a9c221de3a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:25,800 [INFO] chat_bot - [router:0x7b300c8f4050] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:25,800 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091245694, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8598, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='1017486f-b3d8-4de3-9fad-30a9c221de3a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:25,800 [INFO] chat_bot - [router:0x7b2fe43be6d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:25,800 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783091245694, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8598, position='0'), content=TextContent(text='не работает принтер', parse_mode='html'), message_id='1017486f-b3d8-4de3-9fad-30a9c221de3a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-03 15:07:25,800 [INFO] chat_bot - [router:0x7b2fde8e4e10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-03 15:07:25,800 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:07:25,800 [INFO] sd_workflow - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает принтер', is_attachment=False, inline_attachments=0 +2026-07-03 15:07:25,807 [INFO] sd_workflow - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-03 15:07:27,810 [INFO] sd_workflow - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:07:28,053 [INFO] sd_workflow - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-03 15:07:28,704 [INFO] sd_workflow - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает принтер'}], 'temperature': 0.0} +2026-07-03 15:07:30,658 [INFO] httpx - HTTP Request: POST http://172.16.80.47:8080/v1/chat/completions "HTTP/1.1 200 OK" +2026-07-03 15:07:30,662 [INFO] sd_workflow - 📝 [Subject Gen] Text: не работает принтер... -> Subject: Не работает принтер +2026-07-03 15:07:30,832 [INFO] httpx - HTTP Request: POST https://servicedesk.sibcem.ru:8080/api/v3/requests "HTTP/1.1 201 " +2026-07-03 15:07:30,833 [INFO] sd_workflow - 🎫 [Ticket Created] ID: 23134 +2026-07-04 03:02:16,767 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.76 MB +2026-07-04 03:02:16,868 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.83 MB +2026-07-04 03:02:17,327 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.16 MB +2026-07-04 03:02:17,330 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.16 MB +2026-07-04 03:02:20,017 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.17 MB +2026-07-04 03:02:20,785 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.14 MB +2026-07-04 03:02:20,785 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-04 03:02:20,820 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 500 Internal Server Error" +2026-07-04 03:02:23,081 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.04 MB +2026-07-04 03:02:23,228 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.80 MB +2026-07-04 03:02:23,652 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.12 MB +2026-07-04 03:02:23,655 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.12 MB +2026-07-04 03:02:26,395 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 366.81 MB +2026-07-04 03:02:27,146 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 396.66 MB +2026-07-04 03:02:27,146 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-04 03:02:27,164 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 500 Internal Server Error" +2026-07-04 03:02:29,753 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.16 MB +2026-07-04 03:02:29,869 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.02 MB +2026-07-04 03:02:30,812 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.36 MB +2026-07-04 03:02:30,815 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.36 MB +2026-07-04 03:02:33,634 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.65 MB +2026-07-04 03:02:34,212 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.70 MB +2026-07-04 03:02:34,213 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-04 03:02:39,109 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-04 03:02:39,110 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-04 03:02:39,110 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-04 03:02:39,152 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-04 03:02:41,634 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-04 03:02:41,635 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-04 03:02:41,635 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-04 03:02:41,635 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-04 03:02:41,635 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-04 03:02:41,636 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-04 03:02:59,128 [ERROR] __main__ - Не удалось отправить стартовое сообщение администратору: Request to sendMessage timed out after 10.0s +2026-07-04 06:25:49,704 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783146349724, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8602, position='0'), content=TextContent(text='fsd', parse_mode='html'), message_id='05e27cb7-b1d8-4669-9de4-693e44a44726', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-04 06:25:49,705 [INFO] chat_bot - [router:0x76843bd9db10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-04 06:25:49,705 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783146349724, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8602, position='0'), content=TextContent(text='fsd', parse_mode='html'), message_id='05e27cb7-b1d8-4669-9de4-693e44a44726', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-04 06:25:49,705 [INFO] chat_bot - [router:0x76842cfcb650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-04 06:25:49,705 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783146349724, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8602, position='0'), content=TextContent(text='fsd', parse_mode='html'), message_id='05e27cb7-b1d8-4669-9de4-693e44a44726', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-04 06:25:49,705 [INFO] chat_bot - [router:0x768435b15610] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-04 06:25:49,705 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783146349724, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8602, position='0'), content=TextContent(text='fsd', parse_mode='html'), message_id='05e27cb7-b1d8-4669-9de4-693e44a44726', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-04 06:25:49,705 [INFO] chat_bot - [router:0x76842caf49d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-04 06:25:49,705 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783146349724, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8602, position='0'), content=TextContent(text='fsd', parse_mode='html'), message_id='05e27cb7-b1d8-4669-9de4-693e44a44726', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-04 06:25:49,705 [INFO] chat_bot - [router:0x7684045a49d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-04 06:25:49,705 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783146349724, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8602, position='0'), content=TextContent(text='fsd', parse_mode='html'), message_id='05e27cb7-b1d8-4669-9de4-693e44a44726', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-04 06:25:49,706 [INFO] chat_bot - [router:0x76843ce02390] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-04 06:25:49,706 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:22:57,108 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.93 MB +2026-07-08 02:22:57,234 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.99 MB +2026-07-08 02:22:57,676 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.32 MB +2026-07-08 02:22:57,679 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.32 MB +2026-07-08 02:23:00,944 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.09 MB +2026-07-08 02:23:01,642 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.01 MB +2026-07-08 02:23:01,642 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-08 02:23:01,852 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-08 02:23:01,853 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-08 02:23:01,853 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-08 02:23:01,870 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-08 02:23:01,955 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:23:01,955 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:23:01,955 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:23:01,956 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:23:01,956 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:23:01,956 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:23:11,943 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-08 02:23:16,603 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477396464, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8605, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='f74783e6-7b96-4cd3-849e-81b47a635327', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:16,604 [INFO] chat_bot - [router:0x75206a2b5a50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:16,604 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477396464, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8605, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='f74783e6-7b96-4cd3-849e-81b47a635327', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:16,604 [INFO] chat_bot - [router:0x75205afc2c50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:16,604 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477396464, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8605, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='f74783e6-7b96-4cd3-849e-81b47a635327', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:16,604 [INFO] chat_bot - [router:0x752069d6abd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:16,604 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477396464, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8605, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='f74783e6-7b96-4cd3-849e-81b47a635327', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:16,604 [INFO] chat_bot - [router:0x75205afebc10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:16,605 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477396464, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8605, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='f74783e6-7b96-4cd3-849e-81b47a635327', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:16,605 [INFO] chat_bot - [router:0x7520310d3010] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:16,605 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477396464, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8605, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='f74783e6-7b96-4cd3-849e-81b47a635327', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:16,605 [INFO] chat_bot - [router:0x75206401ce90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:16,605 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:23:18,886 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477398749, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8607, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='87d3cf6e-efd7-476c-8482-1fc5c088c922', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:18,887 [INFO] chat_bot - [router:0x75206a2b5a50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:18,887 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477398749, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8607, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='87d3cf6e-efd7-476c-8482-1fc5c088c922', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:18,887 [INFO] chat_bot - [router:0x75205afc2c50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:18,887 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477398749, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8607, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='87d3cf6e-efd7-476c-8482-1fc5c088c922', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:18,887 [INFO] chat_bot - [router:0x752069d6abd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:18,887 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477398749, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8607, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='87d3cf6e-efd7-476c-8482-1fc5c088c922', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:18,887 [INFO] chat_bot - [router:0x75205afebc10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:18,888 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477398749, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8607, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='87d3cf6e-efd7-476c-8482-1fc5c088c922', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:18,888 [INFO] chat_bot - [router:0x7520310d3010] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:18,888 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477398749, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8607, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='87d3cf6e-efd7-476c-8482-1fc5c088c922', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:18,888 [INFO] chat_bot - [router:0x75206401ce90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:18,888 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:23:20,867 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477400729, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8610, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='4e586af7-833b-476b-b359-f69b34208d0d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:20,867 [INFO] chat_bot - [router:0x75206a2b5a50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:20,868 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477400729, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8610, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='4e586af7-833b-476b-b359-f69b34208d0d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:20,868 [INFO] chat_bot - [router:0x75205afc2c50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:20,868 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477400729, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8610, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='4e586af7-833b-476b-b359-f69b34208d0d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:20,868 [INFO] chat_bot - [router:0x752069d6abd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:20,868 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477400729, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8610, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='4e586af7-833b-476b-b359-f69b34208d0d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:20,868 [INFO] chat_bot - [router:0x75205afebc10] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:20,868 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477400729, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8610, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='4e586af7-833b-476b-b359-f69b34208d0d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:20,868 [INFO] chat_bot - [router:0x7520310d3010] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:20,868 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783477400729, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8610, position='0'), content=TextContent(text='2', parse_mode='html'), message_id='4e586af7-833b-476b-b359-f69b34208d0d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:23:20,868 [INFO] chat_bot - [router:0x75206401ce90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:23:20,868 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:23:21,553 [INFO] utils.otp_service - ✅ CARD_ID для ds.krivochenko@tcs.sibcem.ru: 65955 +2026-07-08 02:23:21,594 [ERROR] utils.otp_service - Ошибка получения телефона из БД для CARD_ID 65955: (208, b"Invalid object name 'PR_CARD'.DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: Check messages from the SQL Server\n") +2026-07-08 02:23:21,594 [ERROR] utils.otp_service - Ошибка получения телефона для ds.krivochenko@tcs.sibcem.ru: Не удалось извлечь номер телефона из БД BossCopy: (208, b"Invalid object name 'PR_CARD'.DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: Check messages from the SQL Server\n") +2026-07-08 02:34:26,385 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.66 MB +2026-07-08 02:34:26,502 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.73 MB +2026-07-08 02:34:26,937 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.07 MB +2026-07-08 02:34:26,938 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.07 MB +2026-07-08 02:34:30,251 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.30 MB +2026-07-08 02:34:30,948 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.37 MB +2026-07-08 02:34:30,949 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-08 02:34:31,104 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-08 02:34:31,106 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-08 02:34:31,106 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-08 02:34:31,124 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-08 02:34:31,218 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:34:31,219 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:34:31,219 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:34:31,219 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:34:31,219 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:34:31,219 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 02:34:41,179 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-08 02:35:11,231 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478111069, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8614, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='18ac6862-7bac-420c-ba9b-224974f27165', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:11,232 [INFO] chat_bot - [router:0x71ba88586850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:11,232 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478111069, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8614, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='18ac6862-7bac-420c-ba9b-224974f27165', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:11,232 [INFO] chat_bot - [router:0x71ba792d6590] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:11,232 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478111069, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8614, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='18ac6862-7bac-420c-ba9b-224974f27165', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:11,232 [INFO] chat_bot - [router:0x71ba84f1e4d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:11,232 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478111069, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8614, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='18ac6862-7bac-420c-ba9b-224974f27165', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:11,232 [INFO] chat_bot - [router:0x71ba5f823710] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:11,232 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478111069, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8614, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='18ac6862-7bac-420c-ba9b-224974f27165', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:11,233 [INFO] chat_bot - [router:0x71ba4bccad50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:11,233 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478111069, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8614, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='18ac6862-7bac-420c-ba9b-224974f27165', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:11,233 [INFO] chat_bot - [router:0x71ba88586790] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:11,233 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:13,168 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478113000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8616, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='403738da-f933-4f7b-9c2f-f3b84f515841', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:13,168 [INFO] chat_bot - [router:0x71ba88586850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:13,169 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478113000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8616, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='403738da-f933-4f7b-9c2f-f3b84f515841', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:13,169 [INFO] chat_bot - [router:0x71ba792d6590] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:13,169 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478113000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8616, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='403738da-f933-4f7b-9c2f-f3b84f515841', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:13,169 [INFO] chat_bot - [router:0x71ba84f1e4d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:13,169 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478113000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8616, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='403738da-f933-4f7b-9c2f-f3b84f515841', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:13,169 [INFO] chat_bot - [router:0x71ba5f823710] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:13,169 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478113000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8616, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='403738da-f933-4f7b-9c2f-f3b84f515841', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:13,169 [INFO] chat_bot - [router:0x71ba4bccad50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:13,169 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478113000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8616, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='403738da-f933-4f7b-9c2f-f3b84f515841', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:13,170 [INFO] chat_bot - [router:0x71ba88586790] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:13,170 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:14,849 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478114679, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8619, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1d9c63a5-1a9f-419b-a4b0-f35831cc429d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:14,850 [INFO] chat_bot - [router:0x71ba88586850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:14,850 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478114679, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8619, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1d9c63a5-1a9f-419b-a4b0-f35831cc429d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:14,850 [INFO] chat_bot - [router:0x71ba792d6590] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:14,850 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478114679, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8619, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1d9c63a5-1a9f-419b-a4b0-f35831cc429d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:14,850 [INFO] chat_bot - [router:0x71ba84f1e4d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:14,850 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478114679, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8619, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1d9c63a5-1a9f-419b-a4b0-f35831cc429d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:14,851 [INFO] chat_bot - [router:0x71ba5f823710] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:14,851 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478114679, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8619, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1d9c63a5-1a9f-419b-a4b0-f35831cc429d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:14,851 [INFO] chat_bot - [router:0x71ba4bccad50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:14,851 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478114679, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8619, position='0'), content=TextContent(text='1', parse_mode='html'), message_id='1d9c63a5-1a9f-419b-a4b0-f35831cc429d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:14,851 [INFO] chat_bot - [router:0x71ba88586790] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:14,851 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:15,501 [INFO] utils.otp_service - ✅ CARD_ID для ds.krivochenko@tcs.sibcem.ru: 65955 +2026-07-08 02:35:15,516 [INFO] utils.otp_service - ✅ Телефон для CARD_ID 65955: 9235086996 +2026-07-08 02:35:15,942 [INFO] httpx - HTTP Request: GET https://mobileapp.sibcem.ru/MobDriveService/MobDriveService/MobService.asmx/SendSMS?_number=9235086996&_message=%D0%9A%D0%BE%D0%B4+%D0%B0%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8+%D0%A1%D0%B8%D0%B1%D1%86%D0%B5%D0%BC+%3A+1249 "HTTP/1.1 200 OK" +2026-07-08 02:35:15,942 [INFO] utils.otp_service - Сырой ответ OTP-шлюза: +Сообщение успешно отправлено в MAX на номер *******6996. +2026-07-08 02:35:20,961 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478120790, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8622, position='0'), content=TextContent(text='1249', parse_mode='html'), message_id='16f9c9cb-c2ed-4866-a463-4519725953c4', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:20,961 [INFO] chat_bot - [router:0x71ba88586850] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:20,961 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478120790, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8622, position='0'), content=TextContent(text='1249', parse_mode='html'), message_id='16f9c9cb-c2ed-4866-a463-4519725953c4', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:20,962 [INFO] chat_bot - [router:0x71ba792d6590] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:20,962 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478120790, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8622, position='0'), content=TextContent(text='1249', parse_mode='html'), message_id='16f9c9cb-c2ed-4866-a463-4519725953c4', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:20,962 [INFO] chat_bot - [router:0x71ba84f1e4d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:20,962 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478120790, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8622, position='0'), content=TextContent(text='1249', parse_mode='html'), message_id='16f9c9cb-c2ed-4866-a463-4519725953c4', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:20,963 [INFO] chat_bot - [router:0x71ba5f823710] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:20,963 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478120790, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8622, position='0'), content=TextContent(text='1249', parse_mode='html'), message_id='16f9c9cb-c2ed-4866-a463-4519725953c4', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:20,963 [INFO] chat_bot - [router:0x71ba4bccad50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:20,963 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783478120790, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8622, position='0'), content=TextContent(text='1249', parse_mode='html'), message_id='16f9c9cb-c2ed-4866-a463-4519725953c4', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 02:35:20,963 [INFO] chat_bot - [router:0x71ba88586790] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 02:35:20,964 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:21,748 [DEBUG] fontTools.ttLib.ttFont - Reading 'maxp' table from disk +2026-07-08 02:35:21,748 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'maxp' table +2026-07-08 02:35:21,748 [DEBUG] fontTools.subset.timer - Took 0.001s to load 'maxp' +2026-07-08 02:35:21,748 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'maxp' +2026-07-08 02:35:21,749 [INFO] fontTools.subset - maxp pruned +2026-07-08 02:35:21,750 [DEBUG] fontTools.ttLib.ttFont - Reading 'cmap' table from disk +2026-07-08 02:35:21,750 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'cmap' table +2026-07-08 02:35:21,751 [DEBUG] fontTools.ttLib.ttFont - Reading 'post' table from disk +2026-07-08 02:35:21,751 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'post' table +2026-07-08 02:35:21,761 [DEBUG] fontTools.subset.timer - Took 0.011s to load 'cmap' +2026-07-08 02:35:21,761 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'cmap' +2026-07-08 02:35:21,761 [INFO] fontTools.subset - cmap pruned +2026-07-08 02:35:21,761 [INFO] fontTools.subset - fpgm dropped +2026-07-08 02:35:21,761 [INFO] fontTools.subset - prep dropped +2026-07-08 02:35:21,761 [INFO] fontTools.subset - cvt dropped +2026-07-08 02:35:21,761 [INFO] fontTools.subset - kern dropped +2026-07-08 02:35:21,761 [DEBUG] fontTools.subset.timer - Took 0.000s to load 'post' +2026-07-08 02:35:21,761 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'post' +2026-07-08 02:35:21,761 [INFO] fontTools.subset - post pruned +2026-07-08 02:35:21,762 [INFO] fontTools.subset - GPOS dropped +2026-07-08 02:35:21,762 [INFO] fontTools.subset - GSUB dropped +2026-07-08 02:35:21,762 [DEBUG] fontTools.ttLib.ttFont - Reading 'glyf' table from disk +2026-07-08 02:35:21,763 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'glyf' table +2026-07-08 02:35:21,763 [DEBUG] fontTools.ttLib.ttFont - Reading 'loca' table from disk +2026-07-08 02:35:21,763 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'loca' table +2026-07-08 02:35:21,763 [DEBUG] fontTools.ttLib.ttFont - Reading 'head' table from disk +2026-07-08 02:35:21,763 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'head' table +2026-07-08 02:35:21,769 [DEBUG] fontTools.subset.timer - Took 0.006s to load 'glyf' +2026-07-08 02:35:21,769 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'glyf' +2026-07-08 02:35:21,769 [INFO] fontTools.subset - glyf pruned +2026-07-08 02:35:21,770 [DEBUG] fontTools.subset.timer - Took 0.001s to close glyph list over 'cmap' +2026-07-08 02:35:21,771 [INFO] fontTools.subset - Added gid0 to subset +2026-07-08 02:35:21,771 [INFO] fontTools.subset - Closing glyph list over 'glyf': 44 glyphs before +2026-07-08 02:35:21,771 [INFO] fontTools.subset - Glyph names: ['.notdef', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni0418', 'uni041D', 'uni0420', 'uni0423', 'uni0430', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0445', 'uni0447', 'uni044B', 'uni044C', 'uni044E', 'zero'] +2026-07-08 02:35:21,772 [INFO] fontTools.subset - Glyph IDs: [0, 3, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 935, 937, 941, 946, 949, 952, 965, 967, 968, 969, 970, 971, 972, 973, 974, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 988, 992, 993, 995] +2026-07-08 02:35:21,772 [INFO] fontTools.subset - Closed glyph list over 'glyf': 56 glyphs after +2026-07-08 02:35:21,772 [INFO] fontTools.subset - Glyph names: ['.notdef', 'B', 'H', 'P', 'a', 'breve', 'c', 'comma', 'dotlessi', 'e', 'eight', 'five', 'four', 'nine', 'o', 'one', 'p', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni0418', 'uni041D', 'uni0420', 'uni0423', 'uni0430', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0445', 'uni0447', 'uni044B', 'uni044C', 'uni044E', 'x', 'y', 'zero'] +2026-07-08 02:35:21,772 [INFO] fontTools.subset - Glyph IDs: [0, 3, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 37, 43, 51, 68, 70, 72, 82, 83, 91, 92, 243, 666, 935, 937, 941, 946, 949, 952, 965, 967, 968, 969, 970, 971, 972, 973, 974, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 988, 992, 993, 995] +2026-07-08 02:35:21,773 [DEBUG] fontTools.subset.timer - Took 0.002s to close glyph list over 'glyf' +2026-07-08 02:35:21,775 [INFO] fontTools.subset - Retaining 56 glyphs +2026-07-08 02:35:21,776 [INFO] fontTools.subset - head subsetting not needed +2026-07-08 02:35:21,776 [INFO] fontTools.subset - hhea subsetting not needed +2026-07-08 02:35:21,776 [INFO] fontTools.subset - maxp subsetting not needed +2026-07-08 02:35:21,776 [INFO] fontTools.subset - OS/2 subsetting not needed +2026-07-08 02:35:21,776 [DEBUG] fontTools.ttLib.ttFont - Reading 'hmtx' table from disk +2026-07-08 02:35:21,776 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hmtx' table +2026-07-08 02:35:21,776 [DEBUG] fontTools.ttLib.ttFont - Reading 'hhea' table from disk +2026-07-08 02:35:21,776 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hhea' table +2026-07-08 02:35:22,127 [DEBUG] fontTools.subset.timer - Took 0.351s to subset 'hmtx' +2026-07-08 02:35:22,127 [INFO] fontTools.subset - hmtx subsetted +2026-07-08 02:35:22,129 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'cmap' +2026-07-08 02:35:22,129 [INFO] fontTools.subset - cmap subsetted +2026-07-08 02:35:22,129 [INFO] fontTools.subset - loca subsetting not needed +2026-07-08 02:35:22,129 [DEBUG] fontTools.subset.timer - Took 0.000s to subset 'post' +2026-07-08 02:35:22,129 [INFO] fontTools.subset - post subsetted +2026-07-08 02:35:22,129 [INFO] fontTools.subset - gasp subsetting not needed +2026-07-08 02:35:22,129 [INFO] fontTools.subset - FFTM NOT subset; don't know how to subset +2026-07-08 02:35:22,129 [DEBUG] fontTools.ttLib.ttFont - Reading 'GDEF' table from disk +2026-07-08 02:35:22,129 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'GDEF' table +2026-07-08 02:35:22,132 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'GDEF' +2026-07-08 02:35:22,132 [INFO] fontTools.subset - GDEF subsetted +2026-07-08 02:35:22,132 [INFO] fontTools.subset - name subsetting not needed +2026-07-08 02:35:22,133 [DEBUG] fontTools.subset.timer - Took 0.001s to subset 'glyf' +2026-07-08 02:35:22,133 [INFO] fontTools.subset - glyf subsetted +2026-07-08 02:35:22,134 [DEBUG] fontTools.subset.timer - Took 0.000s to subset GlyphOrder +2026-07-08 02:35:22,134 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'head' +2026-07-08 02:35:22,134 [INFO] fontTools.subset - head pruned +2026-07-08 02:35:22,134 [DEBUG] fontTools.ttLib.ttFont - Reading 'OS/2' table from disk +2026-07-08 02:35:22,134 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'OS/2' table +2026-07-08 02:35:22,135 [INFO] fontTools.subset - OS/2 Unicode ranges pruned: [0, 9] +2026-07-08 02:35:22,135 [INFO] fontTools.subset - OS/2 CodePage ranges pruned: [0] +2026-07-08 02:35:22,135 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'glyf' +2026-07-08 02:35:22,136 [INFO] fontTools.subset - glyf pruned +2026-07-08 02:35:22,136 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'GDEF' +2026-07-08 02:35:22,136 [INFO] fontTools.subset - GDEF pruned +2026-07-08 02:35:22,136 [DEBUG] fontTools.ttLib.ttFont - Reading 'name' table from disk +2026-07-08 02:35:22,136 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'name' table +2026-07-08 02:35:22,136 [DEBUG] fontTools.ttLib.ttFont - Reading 'gasp' table from disk +2026-07-08 02:35:22,136 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'gasp' table +2026-07-08 02:35:22,136 [DEBUG] fontTools.ttLib.ttFont - Reading 'FFTM' table from disk +2026-07-08 02:35:22,136 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'FFTM' table +2026-07-08 02:35:22,137 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'name' +2026-07-08 02:35:22,137 [INFO] fontTools.subset - name pruned +2026-07-08 02:35:22,150 [DEBUG] fontTools.ttLib.ttFont - Reading 'maxp' table from disk +2026-07-08 02:35:22,150 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'maxp' table +2026-07-08 02:35:22,150 [DEBUG] fontTools.subset.timer - Took 0.000s to load 'maxp' +2026-07-08 02:35:22,150 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'maxp' +2026-07-08 02:35:22,150 [INFO] fontTools.subset - maxp pruned +2026-07-08 02:35:22,151 [DEBUG] fontTools.ttLib.ttFont - Reading 'cmap' table from disk +2026-07-08 02:35:22,151 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'cmap' table +2026-07-08 02:35:22,152 [DEBUG] fontTools.ttLib.ttFont - Reading 'post' table from disk +2026-07-08 02:35:22,152 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'post' table +2026-07-08 02:35:22,162 [DEBUG] fontTools.subset.timer - Took 0.011s to load 'cmap' +2026-07-08 02:35:22,162 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'cmap' +2026-07-08 02:35:22,162 [INFO] fontTools.subset - cmap pruned +2026-07-08 02:35:22,162 [INFO] fontTools.subset - fpgm dropped +2026-07-08 02:35:22,162 [INFO] fontTools.subset - prep dropped +2026-07-08 02:35:22,163 [INFO] fontTools.subset - cvt dropped +2026-07-08 02:35:22,163 [INFO] fontTools.subset - kern dropped +2026-07-08 02:35:22,163 [DEBUG] fontTools.subset.timer - Took 0.000s to load 'post' +2026-07-08 02:35:22,163 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'post' +2026-07-08 02:35:22,163 [INFO] fontTools.subset - post pruned +2026-07-08 02:35:22,163 [INFO] fontTools.subset - GPOS dropped +2026-07-08 02:35:22,163 [INFO] fontTools.subset - GSUB dropped +2026-07-08 02:35:22,163 [DEBUG] fontTools.ttLib.ttFont - Reading 'glyf' table from disk +2026-07-08 02:35:22,163 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'glyf' table +2026-07-08 02:35:22,163 [DEBUG] fontTools.ttLib.ttFont - Reading 'loca' table from disk +2026-07-08 02:35:22,164 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'loca' table +2026-07-08 02:35:22,164 [DEBUG] fontTools.ttLib.ttFont - Reading 'head' table from disk +2026-07-08 02:35:22,164 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'head' table +2026-07-08 02:35:22,170 [DEBUG] fontTools.subset.timer - Took 0.007s to load 'glyf' +2026-07-08 02:35:22,170 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'glyf' +2026-07-08 02:35:22,171 [INFO] fontTools.subset - glyf pruned +2026-07-08 02:35:22,172 [DEBUG] fontTools.subset.timer - Took 0.001s to close glyph list over 'cmap' +2026-07-08 02:35:22,172 [INFO] fontTools.subset - Added gid0 to subset +2026-07-08 02:35:22,172 [INFO] fontTools.subset - Closing glyph list over 'MATH': 59 glyphs before +2026-07-08 02:35:22,172 [INFO] fontTools.subset - Glyph names: ['.notdef', 'colon', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni0418', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044E', 'uni044F', 'zero'] +2026-07-08 02:35:22,173 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 935, 937, 941, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 995, 996] +2026-07-08 02:35:22,173 [DEBUG] fontTools.ttLib.ttFont - Reading 'MATH' table from disk +2026-07-08 02:35:22,173 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'MATH' table +2026-07-08 02:35:22,176 [INFO] fontTools.subset - Closed glyph list over 'MATH': 65 glyphs after +2026-07-08 02:35:22,176 [INFO] fontTools.subset - Glyph names: ['.notdef', 'colon', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni0418', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044E', 'uni044F', 'uni239B', 'uni239C', 'uni239D', 'uni239E', 'uni239F', 'uni23A0', 'zero'] +2026-07-08 02:35:22,176 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 935, 937, 941, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 995, 996, 3507, 3508, 3509, 3510, 3511, 3512] +2026-07-08 02:35:22,176 [DEBUG] fontTools.subset.timer - Took 0.004s to close glyph list over 'MATH' +2026-07-08 02:35:22,176 [INFO] fontTools.subset - Closing glyph list over 'glyf': 65 glyphs before +2026-07-08 02:35:22,176 [INFO] fontTools.subset - Glyph names: ['.notdef', 'colon', 'comma', 'eight', 'five', 'four', 'nine', 'one', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni0418', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044E', 'uni044F', 'uni239B', 'uni239C', 'uni239D', 'uni239E', 'uni239F', 'uni23A0', 'zero'] +2026-07-08 02:35:22,176 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 935, 937, 941, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 995, 996, 3507, 3508, 3509, 3510, 3511, 3512] +2026-07-08 02:35:22,177 [INFO] fontTools.subset - Closed glyph list over 'glyf': 79 glyphs after +2026-07-08 02:35:22,177 [INFO] fontTools.subset - Glyph names: ['.notdef', 'B', 'C', 'H', 'M', 'O', 'T', 'a', 'breve', 'c', 'colon', 'comma', 'dotlessi', 'e', 'eight', 'five', 'four', 'nine', 'o', 'one', 'p', 'parenleft', 'parenright', 'percent', 'period', 'seven', 'six', 'space', 'three', 'two', 'uni0412', 'uni0414', 'uni0418', 'uni041B', 'uni041C', 'uni041D', 'uni041E', 'uni041F', 'uni0421', 'uni0422', 'uni0424', 'uni0427', 'uni0430', 'uni0431', 'uni0432', 'uni0433', 'uni0434', 'uni0435', 'uni0436', 'uni0437', 'uni0438', 'uni0439', 'uni043A', 'uni043B', 'uni043C', 'uni043D', 'uni043E', 'uni043F', 'uni0440', 'uni0441', 'uni0442', 'uni0443', 'uni0444', 'uni0446', 'uni0447', 'uni0449', 'uni044B', 'uni044C', 'uni044D', 'uni044E', 'uni044F', 'uni239B', 'uni239C', 'uni239D', 'uni239E', 'uni239F', 'uni23A0', 'y', 'zero'] +2026-07-08 02:35:22,177 [INFO] fontTools.subset - Glyph IDs: [0, 3, 8, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 37, 38, 43, 48, 50, 55, 68, 70, 72, 82, 83, 92, 243, 666, 935, 937, 941, 944, 945, 946, 947, 948, 950, 951, 953, 956, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 987, 988, 990, 992, 993, 994, 995, 996, 3507, 3508, 3509, 3510, 3511, 3512] +2026-07-08 02:35:22,177 [DEBUG] fontTools.subset.timer - Took 0.001s to close glyph list over 'glyf' +2026-07-08 02:35:22,180 [INFO] fontTools.subset - Retaining 79 glyphs +2026-07-08 02:35:22,181 [INFO] fontTools.subset - head subsetting not needed +2026-07-08 02:35:22,181 [INFO] fontTools.subset - hhea subsetting not needed +2026-07-08 02:35:22,181 [INFO] fontTools.subset - maxp subsetting not needed +2026-07-08 02:35:22,181 [INFO] fontTools.subset - OS/2 subsetting not needed +2026-07-08 02:35:22,181 [DEBUG] fontTools.ttLib.ttFont - Reading 'hmtx' table from disk +2026-07-08 02:35:22,181 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hmtx' table +2026-07-08 02:35:22,181 [DEBUG] fontTools.ttLib.ttFont - Reading 'hhea' table from disk +2026-07-08 02:35:22,181 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'hhea' table +2026-07-08 02:35:22,186 [DEBUG] fontTools.subset.timer - Took 0.005s to subset 'hmtx' +2026-07-08 02:35:22,187 [INFO] fontTools.subset - hmtx subsetted +2026-07-08 02:35:22,189 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'cmap' +2026-07-08 02:35:22,189 [INFO] fontTools.subset - cmap subsetted +2026-07-08 02:35:22,189 [INFO] fontTools.subset - loca subsetting not needed +2026-07-08 02:35:22,189 [DEBUG] fontTools.subset.timer - Took 0.000s to subset 'post' +2026-07-08 02:35:22,189 [INFO] fontTools.subset - post subsetted +2026-07-08 02:35:22,189 [INFO] fontTools.subset - gasp subsetting not needed +2026-07-08 02:35:22,190 [INFO] fontTools.subset - FFTM NOT subset; don't know how to subset +2026-07-08 02:35:22,190 [DEBUG] fontTools.subset.timer - Took 0.000s to subset 'MATH' +2026-07-08 02:35:22,190 [INFO] fontTools.subset - MATH subsetted +2026-07-08 02:35:22,190 [DEBUG] fontTools.ttLib.ttFont - Reading 'GDEF' table from disk +2026-07-08 02:35:22,190 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'GDEF' table +2026-07-08 02:35:22,192 [DEBUG] fontTools.subset.timer - Took 0.002s to subset 'GDEF' +2026-07-08 02:35:22,192 [INFO] fontTools.subset - GDEF subsetted +2026-07-08 02:35:22,192 [INFO] fontTools.subset - name subsetting not needed +2026-07-08 02:35:22,195 [DEBUG] fontTools.subset.timer - Took 0.003s to subset 'glyf' +2026-07-08 02:35:22,195 [INFO] fontTools.subset - glyf subsetted +2026-07-08 02:35:22,195 [DEBUG] fontTools.subset.timer - Took 0.000s to subset GlyphOrder +2026-07-08 02:35:22,196 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'head' +2026-07-08 02:35:22,196 [INFO] fontTools.subset - head pruned +2026-07-08 02:35:22,196 [DEBUG] fontTools.ttLib.ttFont - Reading 'OS/2' table from disk +2026-07-08 02:35:22,196 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'OS/2' table +2026-07-08 02:35:22,196 [INFO] fontTools.subset - OS/2 Unicode ranges pruned: [0, 9] +2026-07-08 02:35:22,196 [INFO] fontTools.subset - OS/2 CodePage ranges pruned: [0] +2026-07-08 02:35:22,198 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'glyf' +2026-07-08 02:35:22,198 [INFO] fontTools.subset - glyf pruned +2026-07-08 02:35:22,198 [DEBUG] fontTools.subset.timer - Took 0.000s to prune 'GDEF' +2026-07-08 02:35:22,198 [INFO] fontTools.subset - GDEF pruned +2026-07-08 02:35:22,198 [DEBUG] fontTools.ttLib.ttFont - Reading 'name' table from disk +2026-07-08 02:35:22,198 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'name' table +2026-07-08 02:35:22,199 [DEBUG] fontTools.ttLib.ttFont - Reading 'gasp' table from disk +2026-07-08 02:35:22,199 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'gasp' table +2026-07-08 02:35:22,199 [DEBUG] fontTools.ttLib.ttFont - Reading 'FFTM' table from disk +2026-07-08 02:35:22,199 [DEBUG] fontTools.ttLib.ttFont - Decompiling 'FFTM' table +2026-07-08 02:35:22,199 [DEBUG] fontTools.subset.timer - Took 0.001s to prune 'name' +2026-07-08 02:35:22,199 [INFO] fontTools.subset - name pruned +2026-07-08 06:53:05,485 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.06 MB +2026-07-08 06:53:05,608 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.82 MB +2026-07-08 06:53:06,025 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.16 MB +2026-07-08 06:53:06,027 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.16 MB +2026-07-08 06:53:09,508 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 366.92 MB +2026-07-08 06:53:10,253 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 396.86 MB +2026-07-08 06:53:10,254 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-08 06:53:10,425 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-08 06:53:10,426 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-08 06:53:10,426 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-08 06:53:10,447 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-08 06:53:10,545 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 06:53:10,546 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 06:53:10,546 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 06:53:10,546 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 06:53:10,546 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 06:53:10,547 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-08 06:53:15,131 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783493595060, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8626, position='0'), content=TextContent(text='выф', parse_mode='html'), message_id='f65a60ad-32c1-4fe2-a290-33294443c058', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 06:53:15,132 [INFO] chat_bot - [router:0x7140dda89cd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 06:53:15,133 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783493595060, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8626, position='0'), content=TextContent(text='выф', parse_mode='html'), message_id='f65a60ad-32c1-4fe2-a290-33294443c058', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 06:53:15,133 [INFO] chat_bot - [router:0x7140ce7c0f50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 06:53:15,133 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783493595060, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8626, position='0'), content=TextContent(text='выф', parse_mode='html'), message_id='f65a60ad-32c1-4fe2-a290-33294443c058', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 06:53:15,133 [INFO] chat_bot - [router:0x7140dda79dd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 06:53:15,133 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783493595060, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8626, position='0'), content=TextContent(text='выф', parse_mode='html'), message_id='f65a60ad-32c1-4fe2-a290-33294443c058', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 06:53:15,133 [INFO] chat_bot - [router:0x7140a7242f50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 06:53:15,134 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783493595060, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8626, position='0'), content=TextContent(text='выф', parse_mode='html'), message_id='f65a60ad-32c1-4fe2-a290-33294443c058', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 06:53:15,134 [INFO] chat_bot - [router:0x7140a487b110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 06:53:15,134 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783493595060, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8626, position='0'), content=TextContent(text='выф', parse_mode='html'), message_id='f65a60ad-32c1-4fe2-a290-33294443c058', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-08 06:53:15,134 [INFO] chat_bot - [router:0x7140da5cf5d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-08 06:53:15,134 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 06:53:20,448 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-13 01:48:17,526 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783907297426, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8629, position='0'), content=TextContent(text='Ррр', parse_mode='html'), message_id='22e9fb6a-0741-4fef-a1ff-43d1e4b3c579', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-13 01:48:17,527 [INFO] chat_bot - [router:0x7140dda89cd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-13 01:48:17,527 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783907297426, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8629, position='0'), content=TextContent(text='Ррр', parse_mode='html'), message_id='22e9fb6a-0741-4fef-a1ff-43d1e4b3c579', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-13 01:48:17,527 [INFO] chat_bot - [router:0x7140ce7c0f50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-13 01:48:17,527 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783907297426, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8629, position='0'), content=TextContent(text='Ррр', parse_mode='html'), message_id='22e9fb6a-0741-4fef-a1ff-43d1e4b3c579', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-13 01:48:17,527 [INFO] chat_bot - [router:0x7140dda79dd0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-13 01:48:17,527 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783907297426, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8629, position='0'), content=TextContent(text='Ррр', parse_mode='html'), message_id='22e9fb6a-0741-4fef-a1ff-43d1e4b3c579', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-13 01:48:17,528 [INFO] chat_bot - [router:0x7140a7242f50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-13 01:48:17,528 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783907297426, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8629, position='0'), content=TextContent(text='Ррр', parse_mode='html'), message_id='22e9fb6a-0741-4fef-a1ff-43d1e4b3c579', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-13 01:48:17,529 [INFO] chat_bot - [router:0x7140a487b110] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-13 01:48:17,529 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1783907297426, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8629, position='0'), content=TextContent(text='Ррр', parse_mode='html'), message_id='22e9fb6a-0741-4fef-a1ff-43d1e4b3c579', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-13 01:48:17,529 [INFO] chat_bot - [router:0x7140da5cf5d0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-13 01:48:17,529 [INFO] __main__ - ⏳ Сессия пользователя ds.krivochenko@tcs.sibcem.ru сброшена по таймауту бездействия (1800 сек). +2026-07-13 01:48:17,529 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-14 06:27:44,122 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.14 MB +2026-07-14 06:27:44,328 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.72 MB +2026-07-14 06:27:46,012 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.10 MB +2026-07-14 06:27:46,134 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.96 MB +2026-07-14 06:27:47,876 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.55 MB +2026-07-14 06:27:47,994 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.75 MB +2026-07-14 06:27:49,928 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 69.23 MB +2026-07-14 06:27:50,048 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.98 MB +2026-07-14 06:27:52,352 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.84 MB +2026-07-14 06:27:52,472 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.86 MB +2026-07-14 06:27:55,634 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.19 MB +2026-07-14 06:27:55,757 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.62 MB +2026-07-14 06:28:00,584 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.76 MB +2026-07-14 06:28:00,703 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.76 MB +2026-07-14 06:28:08,652 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.16 MB +2026-07-14 06:28:08,768 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.08 MB +2026-07-14 06:28:23,115 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.08 MB +2026-07-14 06:28:23,249 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.76 MB +2026-07-14 06:28:50,440 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.74 MB +2026-07-14 06:28:50,558 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.75 MB +2026-07-14 06:29:43,321 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.25 MB +2026-07-14 06:29:43,440 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.54 MB +2026-07-14 06:30:44,915 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.79 MB +2026-07-14 06:30:45,033 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.77 MB +2026-07-14 06:31:46,472 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.57 MB +2026-07-14 06:31:46,588 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 99.28 MB +2026-07-14 06:32:48,053 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.73 MB +2026-07-14 06:32:48,173 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.24 MB +2026-07-14 06:33:49,610 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.22 MB +2026-07-14 06:33:49,725 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.91 MB +2026-07-14 06:34:51,300 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.83 MB +2026-07-14 06:34:51,424 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.41 MB +2026-07-14 06:35:48,535 [INFO] __main__ - 📊 [RAM MONITOR] Старт скрипта (базовый вес): 68.90 MB +2026-07-14 06:35:48,652 [INFO] __main__ - 📊 [RAM MONITOR] После импорта photo_bot: 98.88 MB +2026-07-14 06:35:49,094 [INFO] __main__ - 📊 [RAM MONITOR] После импорта service_desk: 105.21 MB +2026-07-14 06:35:49,097 [INFO] __main__ - 📊 [RAM MONITOR] После импорта transcription_router (Виспер): 105.21 MB +2026-07-14 06:35:54,249 [INFO] __main__ - 📊 [RAM MONITOR] После импорта search_router: 367.07 MB +2026-07-14 06:35:55,289 [INFO] __main__ - 📊 [RAM MONITOR] После импорта lk_router (Личный кабинет): 397.20 MB +2026-07-14 06:35:55,289 [INFO] __main__ - Инициализация Dispatcher и подключение роутеров... +2026-07-14 06:35:55,484 [INFO] httpx - HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-07-14 06:35:55,485 [INFO] __main__ - Бот запущен и готов к работе! +2026-07-14 06:35:55,485 [INFO] __main__ - Подключение к серверу TrueConf... +2026-07-14 06:35:55,503 [INFO] httpx - HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-07-14 06:35:55,610 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-14 06:35:55,611 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-14 06:35:55,611 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-14 06:35:55,611 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-14 06:35:55,611 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-14 06:35:55,611 [INFO] chat_bot - 📥 Incoming event: ChangedFileUploadLimits(extensions=Extensions(mode='block', list=['exe', 'com', 'bat', 'cmd', 'ps1', 'vbs', 'js', 'jse', 'wsf', 'wsh', 'msi', 'msp', 'scr', 'pif', 'cpl', 'dll', 'hta', 'docm', 'xlsm', 'pptm', 'dotm', 'xltm', 'potm', 'docb', 'xlb', 'lnk', 'url', 'reg', 'inf', 'scf', 'chm', 'hlp', 'jar', 'apk', 'app', 'ws', 'iso', 'img', 'vhd', 'vhdx', 'dmg']), max_size=None) +2026-07-14 06:36:05,583 [INFO] __main__ - Стартовое сообщение успешно отправлено в чат 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-07-14 06:36:37,815 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1784010997624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8632, position='0'), content=TextContent(text='rfed', parse_mode='html'), message_id='30a96b4f-3048-46cc-9813-e587ecc16a14', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-14 06:36:37,815 [INFO] chat_bot - [router:0x725bd0ab4390] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-14 06:36:37,815 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1784010997624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8632, position='0'), content=TextContent(text='rfed', parse_mode='html'), message_id='30a96b4f-3048-46cc-9813-e587ecc16a14', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-14 06:36:37,816 [INFO] chat_bot - [router:0x725bc210bd50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-14 06:36:37,816 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1784010997624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8632, position='0'), content=TextContent(text='rfed', parse_mode='html'), message_id='30a96b4f-3048-46cc-9813-e587ecc16a14', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-14 06:36:37,816 [INFO] chat_bot - [router:0x725bcd53e750] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-14 06:36:37,816 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1784010997624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8632, position='0'), content=TextContent(text='rfed', parse_mode='html'), message_id='30a96b4f-3048-46cc-9813-e587ecc16a14', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-14 06:36:37,816 [INFO] chat_bot - [router:0x725bc180c950] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-14 06:36:37,816 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1784010997624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8632, position='0'), content=TextContent(text='rfed', parse_mode='html'), message_id='30a96b4f-3048-46cc-9813-e587ecc16a14', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-14 06:36:37,816 [INFO] chat_bot - [router:0x725b992ea810] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-14 06:36:37,816 [INFO] chat_bot - 📥 Incoming event: Message(timestamp=1784010997624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=8632, position='0'), content=TextContent(text='rfed', parse_mode='html'), message_id='30a96b4f-3048-46cc-9813-e587ecc16a14', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-07-14 06:36:37,816 [INFO] chat_bot - [router:0x725bd0ab6f50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-07-14 06:36:37,817 [INFO] sd_workflow - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru diff --git a/config/.env b/config/.env new file mode 100644 index 0000000..8e09fd6 --- /dev/null +++ b/config/.env @@ -0,0 +1,14 @@ +# --- TRUECONF --- +TC_SERVER=tcs.sibcem.ru + + +# --- УВЕДОМЛЕНИЯ (через запятую) --- +ALERTS_DISABLED_USERS=ds.krivochenko@sibcem.ru +ALERTS_ALL_TICKETS=ds.krivochenko@sibcem.ru + + + +PW_ID_TC=bot_tcs +PW_ID_SD=sdesktoken +PW_ID_AD=AD +PW_ID_SQL=boss_base diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..290e52f --- /dev/null +++ b/config/config.py @@ -0,0 +1,219 @@ +# /opt/trueconf_bot/config/config.py +import os +import sys + +# Автоматическое определение корневой директории проекта (на уровень выше папки config) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# ========================================================= +# ФУНКЦИЯ ЧТЕНИЯ .env +# ========================================================= +# Загружает переменные окружения из локального файла .env (если он существует) +ENV_PATH = os.path.join(os.path.dirname(__file__), ".env") + +def load_env(filepath): + try: + with open(filepath, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + if "=" in line: + key, value = line.split("=", 1) + os.environ[key.strip()] = value.strip(' "\'\r\n') + except FileNotFoundError: + print(f"⚠️ Файл {filepath} не найден. Используются системные переменные.") + +load_env(ENV_PATH) + +# Карта соответствия: Имя переменной в боте -> Название карточки в сейфе (из локального .env) +# ИСПРАВЛЕНО: Перенесено выше для корректной передачи списка в модуль ретраев +CREDENTIALS_MAP = { + "TC_PASSWORD": os.getenv("PW_ID_TC", "").strip(' "\'\r\n'), # Получит "bot_tcs" + "SD_TOKEN": os.getenv("PW_ID_SD", "").strip(' "\'\r\n'), # Получит "sdesktoken" + "AD_PASSWORD": os.getenv("PW_ID_AD", "").strip(' "\'\r\n'), # Получит "AD" + "DB_PASSWORD": os.getenv("PW_ID_SQL", "").strip(' "\'\r\n'), # Получит "boss_base" +} + +# ========================================================= +# ИНТЕГРАЦИЯ С PASSWORK (Динамическая загрузка паролей) +# ========================================================= +PASSWORK_DIR = "/opt/passwork" +if PASSWORK_DIR not in sys.path: + sys.path.insert(0, PASSWORK_DIR) + +try: + from passwork import get_passwork_secrets + print("🔐 Обращение к модулю Passwork и выгрузка сейфа...") + + # ИСПРАВЛЕНО: Пробрасываем список целевых карточек для потокобезопасного ожидания сессии + required_to_wait = [name for name in CREDENTIALS_MAP.values() if name] + passwork_pool = get_passwork_secrets(required_cards=required_to_wait) +except Exception as e: + print(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Не удалось подключить модуль Passwork: {e}") + sys.exit(1) + +print("🔐 Распределение секретов и логинов по модулям бота...") +for var_name, card_name in CREDENTIALS_MAP.items(): + if not card_name: + print(f"⚠️ Внимание: Название карточки для {var_name} не найдено в .env") + continue + + clean_card_name = card_name.strip(' "\'\r\n') + card_data = passwork_pool.get(clean_card_name) + + if card_data is None: + print(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Карточка с именем '{clean_card_name}' не найдена в сейфе Passwork!") + sys.exit(1) + + # Динамически заполняем пароль/токен в глобальную область видимости config.py + globals()[var_name] = card_data["password"].strip(' "\'\r\n') + + # ИСПРАВЛЕНО: Переведено на жесткое globals() назначение для исключения NameError во внешних модулях + if var_name == "TC_PASSWORD": + globals()["TC_LOGIN"] = card_data.get("login", "").strip(' "\'\r\n') + elif var_name == "AD_PASSWORD": + globals()["AD_USER"] = card_data.get("login", "").strip(' "\'\r\n') + elif var_name == "DB_PASSWORD": + globals()["DB_USER"] = card_data.get("login", "").strip(' "\'\r\n') + + print(f" ✓ {var_name} и логин успешно загружены из карточки '{clean_card_name}'") + +# ========================================================= +# 1. ГЛОБАЛЬНЫЕ ФЛАГИ МОДУЛЕЙ +# ========================================================= +ENABLE_SERVICE_DESK = True # Модуль создания заявок в техподдержку +ENABLE_PHOTO_BOT = True # Модуль обработки фото/сканов +ENABLE_TRANSCRIPTION = True # Модуль расшифровки голосовых сообщений (Whisper) +ENABLE_SEARCH_BOT = True # Модуль интеллектуального поиска по регламентам (RAG) +ENABLE_LK = True # Личный кабинет сотрудника (отпуска, расчетные листки, справки) + +# ========================================================= +# 2. КОНТРОЛЬ ДОСТУПА +# ========================================================= +ENABLE_WHITELIST = True # Включить режим закрытого бета-тестирования +ALLOWED_USERS = [ # Список TrueConf ID разрешенных пользователей + "ds.krivochenko@tcs.sibcem.ru", + "mm.norenberg@tcs.sibcem.ru", + "a.kirpichev@tcs.sibcem.ru" +] + +# ========================================================= +# 3. НАСТРОЙКИ СВЯЗИ С КОРПОРАТИВНЫМИ СИСТЕМАМИ +# ========================================================= +TC_SERVER = os.getenv("TC_SERVER", "") # Адрес сервера TrueConf + +AD_SERVER = "ldap://172.16.20.20" # Адрес контроллера домена Active Directory +AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru" # Базовый путь поиска пользователей в AD + +SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru" # Сервер MS SQL для работы Личного кабинета +SQL_DB_NAME = "BossCopy" # База данных с информацией по кадрам и ЗП + +SD_URL = "https://servicedesk.sibcem.ru:8080" # URL портала ManageEngine ServiceDesk Plus +DEFAULT_REQUESTER = "ai@sibcem.ru" # E-mail по умолчанию для заявок + +SMTP_SERVER = "srvkem-mail.sibcem.ru" # Corporate почтовый сервер +SMTP_PORT = 25 # Порт SMTP +DEFAULT_EMAIL_FROM = "ai@sibcem.ru" # Адрес отправителя системных писем бота +EMAIL_COOLDOWN_SEC = 10.0 # Кулдаун отправки критических алертов администратору + +# Списки рассылок для системных уведомлений (берутся из .env, разделяются запятыми) +ALERTS_SUPPORT_EMAILS = [e.strip() for e in os.getenv("ALERTS_SUPPORT_EMAILS", "ds.krivochenko@sibcem.ru").split(",") if e.strip()] +ALERTS_DISABLED_USERS = [e.strip() for e in os.getenv("ALERTS_DISABLED_USERS", "").split(",") if e.strip()] +ALERTS_ALL_TICKETS = [e.strip() for e in os.getenv("ALERTS_ALL_TICKETS", "").split(",") if e.strip()] + +# ========================================================= +# 4. ВНЕШНИЕ МИКРОСЕРВИСЫ И ТАЙМАУТЫ СЕТИ +# ========================================================= +TRASH_API_URL = os.getenv("TRASH_API_URL", "http://127.0.0.1:8000/upload") +TRANSCRIPTION_API_URL = "http://172.16.80.250:8000/" +API_HTTP_TIMEOUT = 30.0 + +# ========================================================= +# 5. ИИ: ЯЗЫКОВАЯ МОДЕЛЬ (Генерация текста) +# ========================================================= +# ИСПРАВЛЕНО: Добавлен суффикс .gguf для однозначной адресации модели в llama-server +OLLAMA_URL = "http://172.16.80.47:8080/v1/chat/completions" +OLLAMA_MODEL = "gemma-4-E4B-it-Q4_K_M.gguf" +AI_HTTP_TIMEOUT = 180.0 +OLLAMA_TEMPERATURE = 0.0 +OLLAMA_NUM_CTX = 8192 +OLLAMA_NUM_PREDICT = 1500 + +# --- Настройки для генерации темы (Lightweight model) --- +OLLAMA_SUBJECT_URL = "http://172.16.80.47:8080/v1/chat/completions" +OLLAMA_SUBJECT_MODEL = "gemma-4-E4B-it-Q4_K_M" + +# ========================================================= +# 6. ИИ: RAG И ВЕКТОРНЫЙ ПОИСК (Работа с документами) +# ========================================================= +QDRANT_HOST = "localhost" +QDRANT_PORT = 6333 +QDRANT_COLLECTION = "sibcem_enterprise_docs" + +# --- НАСТРОЙКИ ВНЕШНЕГО EMBEDDING API (Векторизация) --- +EMBEDDING_API_URL = "http://127.0.0.1:8010/embed" +EMBEDDING_MODEL_NAME = "bge-m3" +EMBEDDING_API_TOKEN = os.getenv("EMBEDDING_API_TOKEN", "").strip(' "\'\r\n') + +# Пути к ресурсам, формируемые динамически от корня папки бота +SEARCH_DOCS_PATH = os.path.join(BASE_DIR, "enterprise_docs") +LOGS_PATH = os.path.join(BASE_DIR, "search_bot", "logs") + +# --- НАСТРОЙКИ РЕРАНКЕРА (Кросс-энкодер для переоценки релевантности) --- +ENABLE_RERANKER = True +RERANKER_MODE = "REMOTE_LLAMACPP" + +# Настройки для режима "REMOTE_LLAMACPP" +# ИСПРАВЛЕНО: Добавлен суффикс .gguf для точного роутинга на бэкенде llama-server (порт 8081) +RERANKER_API_URL = "http://172.16.80.47:8081/v1/rerank" +RERANKER_MODEL_NAME = "bge-reranker-v2-m3-Q8_0.gguf" + +# Настройки для режима "LOCAL" (в оперативной памяти) +RERANK_MODEL_PATH = "/opt/models/bge-reranker" + +# --- НАСТРОЙКИ ФИЛЬТРАЦИИ И ЛИМИТОВ (Поиск) --- +QDRANT_LIMIT = 10 +RERANK_TOP_K = 10 +MAX_AI_CONTEXTS = 6 +MAX_CONTEXT_LENGTH = 15000 +MAX_SINGLE_CHUNK_LENGTH = 1500 + +QDRANT_SCORE_THRESHOLD = 0.45 +RERANK_SCORE_THRESHOLD = -3.5 +TABLE_SCORE_THRESHOLD = 0.35 +QDRANT_CHUNK_WINDOW = 2 +QDRANT_SCROLL_LIMIT = 50 + +# ========================================================= +# 7. НАСТРОЙКИ СПРАВОК И ТЕСТИРОВАНИЯ (Личный кабинет) +# ========================================================= +DOCUMENTS_TEST_MODE = False +DOCUMENTS_TARGET_EMAIL = "ds.krivochenko@sibcem.ru" + +# ========================================================= +# 8. ОТЛАДКА И АНАЛИТИКА +# ========================================================= +ENABLE_DEEP_PROFILING = True +ENABLE_QDRANT_DEBUG = True +LOG_RAW_CHUNKS = True +DEBUG_TEXT_LIMIT = 250 + +# ========================================================= +# 9. ПОДСИСТЕМА МОНИТОРИНГА И СТАТУСА БОТА (HEARTBEAT) +# ========================================================= +ADMIN_CHAT_ID = "744bad93e58c4c498e15468562c6a2da3b610e9c" +ENABLE_HEARTBEAT_MONITOR = True +HEARTBEAT_CHECK_INTERVAL_SEC = 10.0 +HEARTBEAT_MAX_FAILURES = 3 + +# ========================================================= +# 10. БЕЗОПАСНОСТЬ И ДВУХФАКТОРНАЯ АВТОРИЗАЦИЯ (2FA / OTP) +# ========================================================= +# Срок действия одноразового OTP-кода (180 секунд = 3 минуты) +OTP_TTL_SEC = 180 +# Максимальное количество попыток ввода OTP-кода до срабатывания защиты +OTP_MAX_ATTEMPTS = 3 +# Время полной аппаратной блокировки меню расчетных листов (1800 секунд = 30 минут) +OTP_BLOCK_DURATION_SEC = 1800 +# Глобальный таймаут неактивности пользователя в любом подменю (1800 секунд = 30 минут) +SESSION_TIMEOUT = 1800 diff --git a/config/test_pw.py b/config/test_pw.py new file mode 100644 index 0000000..d1a5aec --- /dev/null +++ b/config/test_pw.py @@ -0,0 +1,42 @@ +# /opt/trueconf_bot/config/test_pw.py +import sys +import os + +PASSWORK_MODULE_PATH = "/opt/passwork" +if PASSWORK_MODULE_PATH not in sys.path: + sys.path.insert(0, PASSWORK_MODULE_PATH) + +try: + from passwork import get_passwork_secrets + print("✅ Модуль Пассворка успешно обнаружен и импортирован!") +except ImportError as e: + print(f"❌ Не удалось импортировать модуль. Ошибка: {e}") + sys.exit(1) + +def run_test(): + print("\n📡 Запрашиваю актуальный пул секретов из текущего сейфа...") + + # Модуль возвращает динамический словарь всего сейфа + secrets = get_passwork_secrets() + + if not secrets: + print("❌ Сейф пуст или доступы в /opt/passwork/.env невалидны.") + return + + print("🎉 Связь установлена! Данные из текущего сейфа успешно собраны.") + print("========================================================================") + print(" 🔍 ТЕКУЩЕЕ СОДЕРЖИМОЕ СЕЙФА (ДИНАМИЧЕСКИЙ ВЫВОД):") + print("========================================================================") + + # Никакого хардкода: перебираем то, что реально пришло из Пассворка + for index, (card_name, credentials) in enumerate(secrets.items(), 1): + print(f"📦 [Запись {index}]") + print(f" Название карточки: {card_name}") + print(f" Логин в системе: {credentials['login']}") + print(f" Актуальный пароль: {credentials['password']}") + print("------------------------------------------------------------------------") + + print(f"💡 Всего в текущем сейфе обнаружено карточек: {len(secrets)}\n") + +if __name__ == "__main__": + run_test() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4025d3a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + bot: + image: trueconf-bot:latest + container_name: trueconf-bot + volumes: + # Код проекта (покрывает всё внутри) + - .:/app:rw + # Конфигурация + - ./.env:/app/.env:rw + # Passwork (секреты) + - /opt/passwork:/opt/passwork:rw + network_mode: host + restart: always + deploy: + resources: + limits: + memory: 2G + cpus: '2' diff --git a/lk/__init__.py b/lk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lk/documents_service.py b/lk/documents_service.py new file mode 100644 index 0000000..8300329 --- /dev/null +++ b/lk/documents_service.py @@ -0,0 +1,115 @@ +# /opt/trueconf_bot/lk/documents_service.py +import smtplib +import logging +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email import encoders +from datetime import datetime + +import config.config as config + +logger = logging.getLogger(__name__) + +def get_user_info_for_docs(cursor, card_id: str) -> dict: + """Извлекает кадровые данные сотрудника по CARD_ID для оформления бланков справок""" + current_date = datetime.now().strftime('%Y-%m-%d') + + query = """ + select e.TAB_N, a.NAME, d.NAME, hr.FULL_NAME, hr.EMAIL, f.SNAME, pr.EMAIL as workBookEmail + from (select e.TAB_N, e.FIRM_ID, c.F_NAME, c.L_NAME, e.ID, c.M_NAME + from UOV_SELFSERVICE_PR_EMP e + join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID + where c.ID = %s + group by e.ID, e.FIRM_ID, c.F_NAME, c.L_NAME, c.M_NAME, e.TAB_N ) as e + join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID + join UOV_SELFSERVICE_ST_APPOINT a on a.ID = t.APPOINT_ID + join UOV_SELFSERVICE_HR_DEPT d on d.ID = t.DEPT_ID + join UOV_SELFSERVICE_HR_S_USER_SL hr on hr.FIRM_ID = e.FIRM_ID + join UOV_SELFSERVICE_HR_FIRM f on e.FIRM_ID=f.ID + left join UOV_SELFSERVICE_HR_S_USER_PR pr on (pr.DEPT_ISOL_ID = d.DEPT_ISOL_ID or pr.FIRM_ID = f.ID) + where ((t.D_FROM <= %s and t.D_TO >= %s) + or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) + and e.FIRM_ID = f.ID + """ + cursor.execute(query, (card_id, current_date, current_date, current_date)) + row = cursor.fetchone() + + if not row: + raise Exception("Не удалось найти кадровые данные сотрудника для заказа справки.") + + return { + 'tab_n': str(row[0]) if row[0] else "Не указан", + 'job': str(row[1]) if row[1] else "", + 'dept': str(row[2]) if row[2] else "", + 'hr_fio': str(row[3]) if row[3] else "", + 'hr_email': str(row[4]) if row[4] else "", + 'firm': str(row[5]) if row[5] else "", + 'wb_email': str(row[6]) if row[6] else "" + } + +def generate_mail_message(doc_name: str, quantity: int, user_info: dict, period: str = "", attachment_path: str = "") -> bool: + """Формирует и отправляет HTML-письмо заявки на справку в отдел кадров СибЦема""" + + # Извлекаем оригинальные адреса кадровиков из структуры БД + orig_to = user_info.get('hr_email', '').strip() + # Копия уходит на ящик трудовых книжек только если заказывают пункт 7 + orig_cc = user_info.get('wb_email', '').strip() if doc_name == "Копия трудовой книжки" else "" + + to_email = orig_to + cc_list = [orig_cc] if orig_cc else [] + + # Если включен DOCUMENTS_TEST_MODE, жестко перехватываем поток писем + if getattr(config, 'DOCUMENTS_TEST_MODE', False): + test_email = getattr(config, 'DOCUMENTS_TARGET_EMAIL', 'mailonline@sibcem.ru') + to_email = test_email + cc_list = [] + + subject = f"Запрос документа {doc_name} из Личного кабинета" + period_str = f" {period}" if period else "" + + messageBody = ( + f"Необходимо оформить документ {doc_name} в количестве {quantity} шт. " + f"на сотрудника предприятия {user_info['firm']} с табельным номером - " + f"\"{user_info['tab_n']}\"{period_str}.

" + f"Данное письмо отправлено автоматически, отвечать на него не стоит." + ) + + message = MIMEMultipart() + message['From'] = getattr(config, 'DEFAULT_EMAIL_FROM', 'mailonline@sibcem.ru') + message['To'] = to_email + message['Subject'] = subject + + if cc_list: + message['Cc'] = ", ".join(cc_list) + + message.attach(MIMEText(messageBody, 'html', 'utf-8')) + + if attachment_path: + try: + with open(attachment_path, 'rb') as attachment: + part = MIMEBase("application", "octet-stream") + part.set_payload(attachment.read()) + encoders.encode_base64(part) + part.add_header("Content-Disposition", f"attachment; filename={attachment_path}") + message.attach(part) + except Exception as e: + logger.error(f"Не удалось прикрепить файл к письму: {e}") + + recipients = [to_email] + cc_list + smtp_server = getattr(config, 'SMTP_SERVER', 'srvkem-mail.sibcem.ru') + smtp_port = int(getattr(config, 'SMTP_PORT', 25)) + + try: + server = smtplib.SMTP(smtp_server, smtp_port) + server.starttls() + # Если в конфигурации Passwork лежат логин/пароль для SMTP — авторизуемся + if getattr(config, 'SMTP_USER', None): + server.login(config.SMTP_USER, config.SMTP_PASSWORD) + + server.sendmail(message['From'], recipients, message.as_string()) + server.quit() + return True + except Exception as e: + logger.error(f"Ошибка SMTP при отправке письма: {e}") + return False \ No newline at end of file diff --git a/lk/handlers.py b/lk/handlers.py new file mode 100644 index 0000000..8766d6b --- /dev/null +++ b/lk/handlers.py @@ -0,0 +1,682 @@ +from utils.texts import * +# /opt/trueconf_bot/lk/handlers.py +import re +import os +import csv +import logging +import asyncio +import time +import smtplib +from datetime import datetime, timedelta +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from ldap3 import Server, Connection, ALL +from ldap3.utils.conv import escape_filter_chars +from trueconf import Router, Message +from trueconf.types import FSInputFile + +import config.config as config +import lk.payslip_service as ps +import lk.documents_service as ds +from utils.states import get_state, set_state, clear_state + +# 🔌 Импортируем централизованную функцию сбора статистики из main +from utils.stats_logger import log_menu_stats + +# 🔌 Импортируем функции безопасности из изолированного утилитного модуля 2FA +from utils.otp_service import generate_otp_code, send_otp_via_api, get_user_phone_from_ad + +# 🔌 Импортируем вынесенную бизнес-логику запросов по CARD_ID +from lk.info_service import ( + get_vacation_records, + get_rewards_text, + get_sick_leaves_text, + get_tabel_report_text, + get_lk_profile_by_card_id, + get_missions_records, + get_transfers_records +) + +logger = logging.getLogger(__name__) + +# ========================================================= +# ГЛОБАЛЬНЫЙ МАРШРУТИЗАТОР ЦЕЛЬНЫХ VK-ЭМОДЗИ И СЕССИЙ ПАГИНАЦИИ +# ========================================================= +EMOJI_DIGITS = { + "0": "0⃣", + "1": "1⃣", + "2": "2⃣", + "3": "3⃣", + "4": "4⃣", + "5": "5⃣", + "6": "6⃣", + "7": "7⃣", + "8": "8⃣", + "9": "9⃣", + "10": "🔟" +} + +# Список из 7 справок строго по структуру портала +DOC_TYPES = { + "1": "Справка о доходах физического лица (2-НДФЛ)", + "2": "Справка об удержаниях за ДМС", + "3": "Справка о средней заработной плате", + "4": "Справка о получении единовременного пособия по рождению ребенка", + "5": "Справка о получении ежемесячного пособия и назначении отпуска по уходу за ребенком", + "6": "Справка об удержаниях по исполнительным листам", + "7": "Копия трудовой книжки" +} + +LK_DOCS_MENU_TEXT = ( + "📄 Выберите тип документа:\n\n" + + "\n".join([f"{EMOJI_DIGITS[k]} — {v}" for k, v in DOC_TYPES.items()]) + + f"\n\n{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню" +) + +USER_COOLDOWN_SEC = 1.5 +EMAIL_COOLDOWN_SEC = 10.0 + +user_last_request = {} +last_email_time = 0.0 +user_docs_session = {} +user_page_sessions = {} + +# 🔒 Репозиторий временных блокировок 2FA (хранит user_id: timestamp_разблокировки) +user_payslip_blocks = {} + +router = Router() + +# ========================================================= +# СИСТЕМНЫЕ ФУНКЦИИ И ОШИБКИ +# ========================================================= +def log_and_notify_error(user_id: str, action: str, error_details: str): + global last_email_time + log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "lk", "logs") + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, "lk_errors.log") + current_time = time.time() + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"[{timestamp}] User: {user_id} | Action: {action} | Error: {error_details}\n" + + with open(log_file, "a", encoding="utf-8") as f: + f.write(log_entry) + + if current_time - last_email_time < EMAIL_COOLDOWN_SEC: + return + last_email_time = current_time + + try: + msg = MIMEMultipart() + msg['From'] = str(getattr(config, 'DEFAULT_EMAIL_FROM', 'bot@noreply.com')) + to_emails = getattr(config, 'ALERTS_SUPPORT_EMAILS', 'admin@example.com') + msg['To'] = ", ".join(to_emails) if isinstance(to_emails, list) else str(to_emails) + msg['Subject'] = f"TrueConf Bot Error: ЛК - {action}" + body = f"Обнаружена ошибка в ЛК.\nВремя: {timestamp}\nПользователь: {user_id}\nДействие: {action}\nДетали:\n{error_details}" + msg.attach(MIMEText(body, 'plain', 'utf-8')) + import lk.info_service as info_service + with smtplib.SMTP(getattr(config, 'SMTP_SERVER', 'localhost'), int(getattr(config, 'SMTP_PORT', 25))) as server: + server.send_message(msg) + except Exception: + pass + +async def handle_lk_error(msg: Message, user_id: str, action: str, error_details: str): + await asyncio.to_thread(log_and_notify_error, user_id, action, error_details) + error_msg = lk_error_handling(EMOJI_DIGITS) + await msg.answer(error_msg, parse_mode="html") + +# ========================================================= +# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2 +# ========================================================= +def get_card_id_from_ad(user_id: str) -> str: + try: + server = Server(config.AD_SERVER, get_info=ALL) + conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True) + safe_user_id = escape_filter_chars(user_id) + short_username = user_id.split('@')[0] + search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))" + + conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2']) + + if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value: + return str(conn.entries[0].extensionAttribute2.value).strip() + raise Exception("Поле extensionAttribute2 не заполнено в AD.") + except Exception as e: raise Exception(f"AD Card Query Error: {e}") + +# ========================================================= +# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА +# ========================================================= +async def send_paginated_page(msg: Message, user_id: str): + session = user_page_sessions.get(user_id) + if not session: return + + current_page = session["current_page"] + per_page = session["per_page"] + items = session["items"] + + total_items = len(items) + total_pages = (total_items + per_page - 1) // per_page if total_items > 0 else 1 + + start_idx = current_page * per_page + end_idx = start_idx + per_page + page_items = items[start_idx:end_idx] + + if session["type"] == "vacation": + body_text = session["static_header"] + "\n📜 История отпусков (полный архив):\n" + body_text += "\n".join(page_items) + "\n" if page_items else "• История отпусков пуста\n" + elif session["type"] == "missions": + body_text = "🧳 Информация о командировках (архив):\n\n" + body_text += "\n\n".join(page_items) + "\n" if page_items else "• Записи о командировках отсутствуют\n" + elif session["type"] == "transfers": + body_text = "🔄 История кадровых перемещений:\n\n" + body_text += "\n\n".join(page_items) + "\n" if page_items else "• Записи о перемещениях отсутствуют\n" + + body_text += f"\n📄 Страница {current_page + 1} из {total_pages}\n\n" + + nav_lines = [] + if current_page > 0: nav_lines.append(f"{EMOJI_DIGITS['7']} — Предыдущая страница") + if current_page < total_pages - 1: nav_lines.append(f"{EMOJI_DIGITS['8']} — Следующая страница") + nav_lines.append(f"{EMOJI_DIGITS['9']} — Назад") + nav_lines.append(f"{EMOJI_DIGITS['0']} — В главное меню") + + body_text += "\n".join(nav_lines) + await msg.answer(body_text, parse_mode="html") + +# ========================================================= +# ДИНАМИЧЕСКАЯ СБОРКА ГЛАВНОГО МЕНЮ ЛК +# ========================================================= +async def send_dynamic_lk_main_menu(msg: Message, user_id: str): + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + profile = await asyncio.to_thread(get_lk_profile_by_card_id, card_id) + + header = ( + f"👤 Личный кабинет\n\n" + f"🏢 Компания: {profile['company']}\n" + f"📂 Отдел: {profile['dept']}\n" + f"💼 Должность: {profile['job']}\n" + f"⏳ Страховой стаж: {profile['stag_o']}\n" + f"📈 Стаж на предприятии: {profile['stag_p']}\n\n" + ) + except Exception as e: + logger.error(f"Не удалось собрать динамический заголовок ЛК: {e}") + header = "👤 Личный кабинет\n\n" + + menu_text = lk_main_menu_text(header, EMOJI_DIGITS) + await msg.answer(menu_text, parse_mode="html") + +async def send_tabel_months_menu(msg: Message, user_id: str): + now = datetime(2026, 6, 3) + prev_1 = now.replace(day=1) - timedelta(days=1) + prev_2 = prev_1.replace(day=1) - timedelta(days=1) + months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"} + + menu_text = lk_tabel_months_menu_text(prev_1, prev_2, months_ru, EMOJI_DIGITS) + set_state(user_id, "LK_TABEL_MONTHS") + await msg.answer(menu_text, parse_mode="html") + +def wrapper_get_months(card_id: str) -> list: + import lk.info_service as info_service + conn = info_service.get_db_connection() + cursor = conn.cursor() + try: return ps.get_latest_payslip_months(cursor, card_id) + finally: conn.close() + +def wrapper_get_payslip(card_id: str, target_cmonth: int): + import lk.info_service as info_service + conn = info_service.get_db_connection() + cursor = conn.cursor() + try: return ps.build_payslip_data(cursor, card_id, target_cmonth) + except Exception as e: raise Exception(f"Payslip Build Error: {e}") + finally: conn.close() + +async def send_payslip_months_menu(msg: Message, user_id: str, card_id: str): + try: + months_data = await asyncio.to_thread(wrapper_get_months, card_id) + months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"} + menu_text = lk_payslip_months_menu_text(months_data, months_ru, EMOJI_DIGITS) + set_state(user_id, "LK_PAYSLIP_MONTHS") + await msg.answer(menu_text, parse_mode="html") + except Exception as e: raise Exception(str(e)) + +async def build_confirmation_text(user_id: str, doc: dict) -> str: + confirm_txt = f"📝 Подтверждение заказа:\n\nДокумент: {doc['doc_name']}\nКоличество: {doc['quantity']} шт." + if doc.get('period'): confirm_txt += f"\nУточнение: {doc['period']}" + if getattr(config, 'DOCUMENTS_TEST_MODE', False): + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + import lk.info_service as info_service + with info_service.get_db_connection() as conn: + cursor = conn.cursor() + user_info = await asyncio.to_thread(ds.get_user_info_for_docs, cursor, card_id) + + orig_to = user_info.get('hr_email', '').strip() + orig_cc = user_info.get('wb_email', '').strip() if doc['doc_id'] == "7" else "" + requested_recipients = orig_to + if orig_cc: requested_recipients += f" (Копия: {orig_cc})" + test_email = getattr(config, 'DOCUMENTS_TARGET_EMAIL', 'mailonline@sibcem.ru') + confirm_txt += f"\n\n⚠️ Тест-режим.\nКому: {requested_recipients}\nЗамена: {test_email}" + except Exception as e: confirm_txt += f"\n\n⚠️ Тест-режим.\n(Ошибка БД: {e})" + confirm_txt += f"\n\n{EMOJI_DIGITS['1']} — Да, отправить\n{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню" + return confirm_txt + +# ========================================================= +# ОСНОВНОЙ ОБРАБОТЧИК МАРШРУТОВ ЛК +# ========================================================= +@router.message() +async def lk_router_handler(msg: Message): + user_id = msg.from_user.id + current_time = time.time() + if current_time - user_last_request.get(user_id, 0) < USER_COOLDOWN_SEC: return + user_last_request[user_id] = current_time + + current_state = get_state(user_id) + nav_text = lk_nav_text(EMOJI_DIGITS) + + if not msg.text: return + cmd = msg.text.strip().lower() + + for raw_num, emoji_num in EMOJI_DIGITS.items(): + if cmd == emoji_num: + cmd = raw_num + break + + if current_state is None: + from utils.menu import MENU_MAP + if MENU_MAP.get(cmd) == "LK": + msg.handled = True + set_state(user_id, "LK_MODE") + await send_dynamic_lk_main_menu(msg, user_id) + return + return + + if current_state == "LK_MODE": + msg.handled = True + if cmd == "0": + log_menu_stats(user_id, "Личный кабинет", "Выход в главное меню") + clear_state(user_id) + from utils.menu import MENU_TEXT + await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "1": + block_expiry = user_payslip_blocks.get(user_id, 0) + if time.time() < block_expiry: + rem_minutes = int((block_expiry - time.time()) // 60) + 1 + await msg.answer( + f"❌ Доступ временно заблокирован.\n" + f"Вы трижды ввели неверный код подтверждения.\n" + f"Доступ к расчетным листам ограничен на {int(getattr(config, 'OTP_BLOCK_DURATION_SEC', 1800) // 60)} минут.\n\n" + f"⏱ Осталось подождать: {rem_minutes} мин.{nav_text}", + parse_mode="html" + ) + return + + log_menu_stats(user_id, "Личный кабинет", "Расчётный лист") + await msg.answer(lk_payslip_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + await send_payslip_months_menu(msg, user_id, card_id) + except Exception as e: await handle_lk_error(msg, user_id, "Меню расчетных листков", str(e)) + elif cmd == "2": + log_menu_stats(user_id, "Личный кабинет", "Информация о днях отпуска") + await msg.answer(lk_vacation_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + static_header, history_lines = await asyncio.to_thread(get_vacation_records, card_id) + user_page_sessions[user_id] = {"type": "vacation", "current_page": 0, "per_page": 5, "static_header": static_header, "items": history_lines} + set_state(user_id, "LK_VACATION_VIEW") + await send_paginated_page(msg, user_id) + except Exception as e: await handle_lk_error(msg, user_id, "Справка по отпускам", str(e)) + elif cmd == "3": + log_menu_stats(user_id, "Личный кабинет", "Больничные") + await msg.answer(lk_sick_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + sick_info = await asyncio.to_thread(get_sick_leaves_text, card_id) + set_state(user_id, "LK_SICK_VIEW") + await msg.answer(sick_info + nav_text, parse_mode="html") + except Exception as e: await handle_lk_error(msg, user_id, "Просмотр больничных", str(e)) + elif cmd == "4": + log_menu_stats(user_id, "Личный кабинет", "Командировки") + await msg.answer(lk_missions_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + mission_lines = await asyncio.to_thread(get_missions_records, card_id) + user_page_sessions[user_id] = {"type": "missions", "current_page": 0, "per_page": 4, "items": mission_lines} + set_state(user_id, "LK_MISSIONS_VIEW") + await send_paginated_page(msg, user_id) + except Exception as e: await handle_lk_error(msg, user_id, "Просмотр командировок", str(e)) + elif cmd == "5": + log_menu_stats(user_id, "Личный кабинет", "Награды") + await msg.answer(lk_rewards_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + rewards_info = await asyncio.to_thread(get_rewards_text, card_id) + set_state(user_id, "LK_REWARDS_VIEW") + await msg.answer(rewards_info + nav_text, parse_mode="html") + except Exception as e: await handle_lk_error(msg, user_id, "Запрос наград", str(e)) + elif cmd == "6": + log_menu_stats(user_id, "Личный кабинет", "Переводы") + await msg.answer(lk_transfers_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + transfer_lines = await asyncio.to_thread(get_transfers_records, card_id) + user_page_sessions[user_id] = {"type": "transfers", "current_page": 0, "per_page": 4, "items": transfer_lines} + set_state(user_id, "LK_TRANSFERS_VIEW") + await send_paginated_page(msg, user_id) + except Exception as e: await handle_lk_error(msg, user_id, "Просмотр переводов", str(e)) + elif cmd == "7": + log_menu_stats(user_id, "Личный кабинет", "Табель учёта рабочего времени") + await send_tabel_months_menu(msg, user_id) + elif cmd == "8": + log_menu_stats(user_id, "Личный кабинет", "Справки") + set_state(user_id, "LK_DOCS_MENU") + await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html") + else: + # 🎯 ИСПРАВЛЕНО: Вместо спама копии меню ЛК выводим строгое предупреждение + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_TABEL_MONTHS": + msg.handled = True + if cmd == "0": + clear_state(user_id) + from utils.menu import MENU_TEXT + await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": + set_state(user_id, "LK_MODE") + await send_dynamic_lk_main_menu(msg, user_id) + elif cmd in ["1", "2"]: + log_menu_stats(user_id, "Личный кабинет", "Просмотр табеля за месяц") + await msg.answer(lk_tabel_loading(EMOJI_DIGITS), parse_mode="html") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + now = datetime(2026, 6, 3) + target_date = now.replace(day=1) - timedelta(days=1) if cmd == "1" else (now.replace(day=1) - timedelta(days=1)).replace(day=1) - timedelta(days=1) + tabel_report = await asyncio.to_thread(get_tabel_report_text, card_id, target_date.year, target_date.month) + set_state(user_id, "LK_TABEL_VIEW") + await msg.answer(tabel_report + nav_text, parse_mode="html") + except Exception as e: await handle_lk_error(msg, user_id, "Формирование табеля", str(e)) + else: + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state in ["LK_VACATION_VIEW", "LK_MISSIONS_VIEW", "LK_TRANSFERS_VIEW"]: + msg.handled = True + session = user_page_sessions.get(user_id) + if cmd == "0": + user_page_sessions.pop(user_id, None) + clear_state(user_id) + from utils.menu import MENU_TEXT + await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": + user_page_sessions.pop(user_id, None) + set_state(user_id, "LK_MODE") + await send_dynamic_lk_main_menu(msg, user_id) + elif cmd == "7" and session and session["current_page"] > 0: + session["current_page"] -= 1 + await send_paginated_page(msg, user_id) + elif cmd == "8" and session: + if session["current_page"] < (len(session["items"]) + session["per_page"] - 1) // session["per_page"] - 1: + session["current_page"] += 1 + await send_paginated_page(msg, user_id) + else: + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_PAYSLIP_AUTH": + msg.handled = True + session = user_page_sessions.get(user_id) + + if cmd == "0": + user_page_sessions.pop(user_id, None) + clear_state(user_id) + from utils.menu import MENU_TEXT + await msg.answer(MENU_TEXT, parse_mode="html") + return + elif cmd == "9": + user_page_sessions.pop(user_id, None) + set_state(user_id, "LK_PAYSLIP_MONTHS") + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + await send_payslip_months_menu(msg, user_id, card_id) + except Exception as e: await handle_lk_error(msg, user_id, "Возврат к меню месяцев", str(e)) + return + + if not session: + await msg.answer("⚠️ Сессия авторизации не найдена. Начните сначала.", parse_mode="html") + set_state(user_id, "LK_MODE") + await send_dynamic_lk_main_menu(msg, user_id) + return + + if time.time() > session.get("expires_at", 0): + log_menu_stats(user_id, "Личный кабинет", "Ошибка 2FA: Истекло время жизни кода") + user_page_sessions.pop(user_id, None) + set_state(user_id, "LK_MODE") + await msg.answer( + f"⏱ Время действия кода подтверждения изменилось или истекло.\n" + f"Пожалуйста, запросите расчетный лист заново.{nav_text}", + parse_mode="html" + ) + await send_dynamic_lk_main_menu(msg, user_id) + return + + if cmd == session.get("correct_code"): + log_menu_stats(user_id, "Личный кабинет", "Успешная проверка 2FA расчетного листа") + await msg.answer(lk_auth_success(EMOJI_DIGITS), parse_mode="html") + + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + target_cmonth = session["target_cmonth"] + + m_name = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"} + tmonth = (target_cmonth - 1) % 12 + 1 + tyear = (target_cmonth - 1) // 12 + + payslip_info, pdf_path = await asyncio.to_thread(wrapper_get_payslip, card_id, target_cmonth) + + set_state(user_id, "LK_PAYSLIP_VIEW") + await msg.answer_document( + FSInputFile(pdf_path, filename=f"Расчетный_лист_{m_name.get(tmonth, '00')}_{tyear}.pdf"), + caption=payslip_info + f"\n\n{EMOJI_DIGITS['9']} — Назад к выбору месяца\n{EMOJI_DIGITS['0']} — В главное меню", + parse_mode="html" + ) + if os.path.exists(pdf_path): os.remove(pdf_path) + except Exception as e: + await handle_lk_error(msg, user_id, "Просмотр детализации после 2FA", str(e)) + finally: + user_page_sessions.pop(user_id, None) + else: + session["attempts_left"] -= 1 + if session["attempts_left"] <= 0: + log_menu_stats(user_id, "Личный кабинет", "Ошибка 2FA: Превышен лимит попыток ввода, бан") + + block_duration = int(getattr(config, 'OTP_BLOCK_DURATION_SEC', 1800)) + user_payslip_blocks[user_id] = time.time() + block_duration + + user_page_sessions.pop(user_id, None) + set_state(user_id, "LK_MODE") + await msg.answer( + f"❌ Превышено максимальное количество попыток ввода кода.\n" + f"Доступ к финансовым документам заблокирован на {block_duration // 60} минут.{nav_text}", + parse_mode="html" + ) + await send_dynamic_lk_main_menu(msg, user_id) + else: + await msg.answer( + f"⚠️ Неверный код подтверждения.\n" + f"Осталось попыток: {session['attempts_left']}. Попробуйте еще раз или вернитесь назад.{nav_text}", + parse_mode="html" + ) + + elif current_state in ["LK_REWARDS_VIEW", "LK_PAYSLIP_MONTHS", "LK_SICK_VIEW", "LK_TABEL_VIEW", "LK_STUB_VIEW"]: + msg.handled = True + if cmd == "0": + clear_state(user_id) + from utils.menu import MENU_TEXT + await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": + if current_state == "LK_TABEL_VIEW": await send_tabel_months_menu(msg, user_id) + else: set_state(user_id, "LK_MODE"); await send_dynamic_lk_main_menu(msg, user_id) + elif current_state == "LK_PAYSLIP_MONTHS" and cmd.isdigit(): + try: + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + months_data = await asyncio.to_thread(wrapper_get_months, card_id) + idx = int(cmd) - 1 + + if 0 <= idx < len(months_data): + target_cmonth = months_data[idx] + + await msg.answer(lk_auth_processing(EMOJI_DIGITS), parse_mode="html") + user_phone = await asyncio.to_thread(get_user_phone_from_ad, user_id) + + otp_code = generate_otp_code(4) + sms_success, channel_info = await send_otp_via_api(user_phone, otp_code) + + if sms_success: + log_menu_stats(user_id, "Личный кабинет", f"Запрос кода 2FA для расчетного листа ({channel_info})") + masked_phone = f"{user_phone[:5]}***{user_phone[-4:]}" if len(user_phone) > 8 else user_phone + + otp_ttl = int(getattr(config, 'OTP_TTL_SEC', 180)) + max_attempts = int(getattr(config, 'OTP_MAX_ATTEMPTS', 3)) + + user_page_sessions[user_id] = { + "correct_code": otp_code, + "target_cmonth": target_cmonth, + "expires_at": time.time() + otp_ttl, + "attempts_left": max_attempts + } + + set_state(user_id, "LK_PAYSLIP_AUTH") + await msg.answer( + f"🔐 Двухфакторная авторизация\n\n" + f"Код подтверждения отправлен {channel_info} на номер {masked_phone}.\n" + f"Пожалуйста, введите 4-значный код из сообщения в ответ:\n\n" + f"Код действителен в течение {otp_ttl // 60} минут. У вас есть {max_attempts} попытки.{nav_text}", + parse_mode="html" + ) + else: + raise Exception(f"Внутренний каскадный шлюз недоступен: {channel_info}") + else: + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + except Exception as e: await handle_lk_error(msg, user_id, "Инициализация 2FA расчетного листа", str(e)) + else: + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_PAYSLIP_VIEW": + msg.handled = True + if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": + try: card_id = await asyncio.to_thread(get_card_id_from_ad, user_id); await send_payslip_months_menu(msg, user_id, card_id) + except Exception as e: await handle_lk_error(msg, user_id, "Возврат в меню месяцев", str(e)) + else: + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_DOCS_MENU": + msg.handled = True + if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": user_docs_session.pop(user_id, None); set_state(user_id, "LK_MODE"); await send_dynamic_lk_main_menu(msg, user_id) + elif cmd in DOC_TYPES: + user_docs_session[user_id] = {'doc_id': cmd, 'doc_name': DOC_TYPES[cmd]} + log_menu_stats(user_id, "Личный кабинет", f"Выбор справки: {DOC_TYPES[cmd]}") + set_state(user_id, "LK_DOCS_QUANTITY") + await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html") + else: + # 🎯 ИСПРАВЛЕНО: Заменен спам списка документов на чистую ошибку ввода + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_DOCS_QUANTITY": + msg.handled = True + if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html") + elif cmd.isdigit() and 0 < int(cmd) <= 5: + user_docs_session[user_id]['quantity'] = int(cmd) + doc_id = user_docs_session[user_id]['doc_id'] + + if doc_id in ["1", "2", "6"]: + set_state(user_id, "LK_DOCS_PERIOD") + current_year = datetime.now().year + years_text = lk_years_menu(current_year, EMOJI_DIGITS) + await msg.answer(years_text, parse_mode="html") + elif doc_id == "3": + set_state(user_id, "LK_DOCS_PERIOD") + await msg.answer(lk_docs_avg_salary_prompt(EMOJI_DIGITS), parse_mode="html") + elif doc_id == "5": + set_state(user_id, "LK_DOCS_PERIOD") + await msg.answer(lk_docs_child_age_prompt(EMOJI_DIGITS), parse_mode="html") + else: + user_docs_session[user_id]['period'] = "" + set_state(user_id, "LK_DOCS_CONFIRM") + confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id]) + await msg.answer(confirm_txt, parse_mode="html") + else: + # 🎯 ИСПРАВЛЕНО: Вместо кастомной подсказки выводим единый стандарт + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_DOCS_PERIOD": + msg.handled = True + if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": set_state(user_id, "LK_DOCS_QUANTITY"); await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html") + else: + doc_id = user_docs_session[user_id]['doc_id'] + if doc_id in ["1", "2", "6"]: + if cmd in ["1", "2", "3", "4", "5", "6", "7", "8"]: + user_docs_session[user_id]['period'] = f"за {datetime.now().year - (int(cmd) - 1)} год" + else: + # 🎯 ИСПРАВЛЕНО: Убран спам меню годов при абракадабре + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + return + elif doc_id == "5": + if cmd == "1": user_docs_session[user_id]['period'] = "до 1.5 лет" + elif cmd == "2": user_docs_session[user_id]['period'] = "до 3 лет" + else: + # 🎯 ИСПРАВЛЕНО: Убран спам меню возраста при некорректном вводе + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + return + elif doc_id == "3": + user_docs_session[user_id]['period'] = f"за {cmd} мес." + + set_state(user_id, "LK_DOCS_CONFIRM") + confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id]) + await msg.answer(confirm_txt, parse_mode="html") + + elif current_state == "LK_DOCS_CONFIRM": + msg.handled = True + if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": + doc_id = user_docs_session.get(user_id, {}).get('doc_id') + if doc_id in ["1", "2", "3", "5", "6"]: + set_state(user_id, "LK_DOCS_PERIOD") + if doc_id in ["1", "2", "6"]: + current_year = datetime.now().year + years_text = lk_years_menu(current_year, EMOJI_DIGITS) + await msg.answer(years_text, parse_mode="html") + elif doc_id == "3": await msg.answer(lk_docs_avg_salary_prompt(EMOJI_DIGITS), parse_mode="html") + elif doc_id == "5": await msg.answer(lk_docs_child_age_prompt(EMOJI_DIGITS), parse_mode="html") + else: set_state(user_id, "LK_DOCS_QUANTITY"); await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html") + elif cmd == "1": + await msg.answer("⏳ Отправка заявки...", parse_mode="html") + try: + doc = user_docs_session[user_id] + card_id = await asyncio.to_thread(get_card_id_from_ad, user_id) + + import lk.info_service as info_service + with info_service.get_db_connection() as conn: + cursor = conn.cursor() + user_info = await asyncio.to_thread(ds.get_user_info_for_docs, cursor, card_id) + + success = await asyncio.to_thread(ds.generate_mail_message, doc['doc_name'], doc['quantity'], user_info, doc.get('period', '')) + if success: + log_menu_stats(user_id, "Личный кабинет", f"Заказ справки: {doc['doc_name']}") + await msg.answer(f"✅ Ваша заявка успешно отправлена!\nСпециалисты свяжутся с вами по готовности документа.\n\n{EMOJI_DIGITS['9']} — Назад в список справок\n{EMOJI_DIGITS['0']} — В главное меню", parse_mode="html") + set_state(user_id, "LK_DOCS_SUCCESS") + else: raise Exception("Внутренняя ошибка SMTP при отправке письма") + except Exception as e: await handle_lk_error(msg, user_id, f"Заказ справки {user_docs_session.get(user_id, {}).get('doc_id')}", str(e)); set_state(user_id, "LK_MODE") + finally: user_docs_session.pop(user_id, None) + else: + # 🎯 ИСПРАВЛЕНО: Удален спам-текст подтверждения + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + + elif current_state == "LK_DOCS_SUCCESS": + msg.handled = True + if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html") + elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html") + else: + # 🎯 ИСПРАВЛЕНО: Унифицировано под общий шаблон ошибки + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") \ No newline at end of file diff --git a/lk/info_service.py b/lk/info_service.py new file mode 100644 index 0000000..5680b0f --- /dev/null +++ b/lk/info_service.py @@ -0,0 +1,378 @@ +# /opt/trueconf_bot/lk/info_service.py +import re +import logging +import calendar +import pymssql +from datetime import datetime + +import config.config as config + +logger = logging.getLogger(__name__) + +def get_db_connection(): + if not config.DB_USER or not config.DB_PASSWORD: + raise Exception("Пароли БД не загружены из Passwork.") + return pymssql.connect( + server=config.SQL_SERVER, + user=config.DB_USER, + password=config.DB_PASSWORD, + database=config.SQL_DB_NAME, + charset='cp1251' + ) + +def get_lk_profile_by_card_id(card_id: str) -> dict: + """Запрос 1: Получение информации о подразделении, должности и стаже по CARD_ID""" + try: + current_date = datetime.now().strftime('%Y-%m-%d') + conn = get_db_connection() + cursor = conn.cursor() + query = """ + select f.SNAME [Компания], d.NAME [Отдел], appoint.NAME [Должность], rtrim(ltrim(e.STAGO)) [Страховой стаж], rtrim(ltrim(e.STAGP)) [Стаж на предприятии] + from (select e.ID, e.FIRM_ID, st.STAGO, st.STAGP + from UOV_SELFSERVICE_PR_EMP e + join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID + join UOV_SELFSERVICE_PR_STAG_CUR st on st.CARD_ID=c.ID + where c.ID=%s + group by e.ID , e.FIRM_ID, st.STAGO, st.STAGP) as e + join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID + join UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID + join UOV_SELFSERVICE_HR_DEPT d on d.ID = t.DEPT_ID + join UOV_SELFSERVICE_ST_APPOINT appoint on appoint.ID = t.APPOINT_ID + where (t.D_FROM <= cast(%s as date) + and (t.D_TO >= cast(%s as date) + or t.D_TO = cast('1999-12-31' as date))) + group by f.SNAME, d.NAME, appoint.NAME, rtrim(ltrim(e.STAGO)), rtrim(ltrim(e.STAGP)) + """ + cursor.execute(query, (card_id, current_date, current_date)) + row = cursor.fetchone() + conn.close() + if not row: raise Exception(f"Запись сотрудника по CARD_ID {card_id} не найдена в БД.") + return { + 'company': str(row[0]).strip() if row[0] else "", + 'dept': str(row[1]).strip() if row[1] else "", + 'job': str(row[2]).strip() if row[2] else "", + 'stag_o': str(row[3]).strip() if row[3] else "0", + 'stag_p': str(row[4]).strip() if row[4] else "0" + } + except Exception as e: raise Exception(f"SQL Profile Error: {e}") + +def get_vacation_records(card_id: str) -> tuple: + """Возвращает статический заголовок (баланс + планы) и список строк истории отпусков""" + try: + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT TOP 1 e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,)) + row = cursor.fetchone() + if not row: + conn.close() + return "🌴 Информация об отпусках\n\nСотрудник не найден в базе данных.", [] + emp_id = row[0] + + # Балансы + q_balance = """ + SELECT comp.LEAVE_NAME, comp.DAYS FROM UO_SELFSERVICE_LEAVE_COMP as comp + INNER JOIN UOV_SELFSERVICE_PR_TRANS as trans ON trans.EMP_ID = comp.EMP_ID + WHERE GETDATE() BETWEEN trans.D_FROM and trans.D_TO AND comp.EMP_ID = %s + """ + cursor.execute(q_balance, (emp_id,)) + balances = cursor.fetchall() + + # Планы + q_plans = """ + SELECT e.D_FROM, e.DAYS + FROM UOV_SELFSERVICE_PR_LEAVE_PLAN e + WHERE e.EMP_ID = %s AND e.D_FROM >= CAST(GETDATE() AS DATE) + ORDER BY e.D_FROM ASC + """ + cursor.execute(q_plans, (emp_id,)) + plans = cursor.fetchall() + + # Архив всей истории + q_history = """ + SELECT e.D_FROM, e.D_TO, e.DAYS, CAST(e.NAME AS VARCHAR(250)), e.ADDITIONAL + FROM UOV_SELFSERVICE_PR_LEAVE_ALL e + WHERE e.EMP_ID = %s AND e.D_FROM < CAST(GETDATE() AS DATE) + ORDER BY e.D_FROM DESC + """ + cursor.execute(q_history, (emp_id,)) + history = cursor.fetchall() + conn.close() + + # Формируем неизменяемую верхнюю часть (шапку) справки + static_header = "🌴 Информация об отпусках\n\n🟢 Доступный остаток:\n" + if balances: + for b in balances: + b_name = str(b[0]).strip() if b[0] else "Ежегодный оплачиваемый отпуск" + static_header += f"• {int(b[1]) if b[1] else 0} дн. ({b_name})\n" + else: + static_header += "• 0 дн.\n" + + static_header += "\n⏳ Предстоящие отпуска:\n" + if plans: + for p in plans: + d_from = p[0].strftime('%d.%m.%Y') if hasattr(p[0], 'strftime') else str(p[0])[:10] + static_header += f"• {d_from}{int(p[1]) if p[1] else 0} дн.\n" + else: + static_header += "• Нет запланированных отпусков\n" + + # Нарезаем строки истории в плоский массив для пагинатора + history_lines = [] + if history: + for h in history: + h_from = h[0].strftime('%d.%m.%Y') if hasattr(h[0], 'strftime') else str(h[0])[:10] + h_to = h[1].strftime('%d.%m.%Y') if hasattr(h[1], 'strftime') else str(h[1])[:10] + h_days = int(h[2]) if h[2] else 0 + + if h[3] and str(h[3]).strip(): + type_str = str(h[3]).strip() + else: + type_str = "Дополнительный" if h[4] == 1 else "Основной" + + history_lines.append(f"• {h_from} — {h_to} : 🔹 {h_days} дн. ({type_str})") + + return static_header, history_lines + except Exception as e: + raise Exception(f"SQL Vacation Records Error: {e}") + +def get_sick_leaves_text(card_id: str) -> str: + """Запрашивает историю больничных листов строго по CARD_ID""" + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,)) + row = cursor.fetchone() + if not row: + conn.close() + return "📑 Ваши больничные листы:\n\nСотрудник не найден в базе данных." + emp_id = row[0] + + query = """ + SELECT e.D_FROM, e.D_TO + FROM UOV_SELFSERVICE_PR_ILL e + WHERE e.EMP_ID = %s + ORDER BY e.D_FROM DESC + """ + cursor.execute(query, (emp_id,)) + results = cursor.fetchall() + conn.close() + + if not results: + return "📑 Ваши больничные листы:\n\nНичего не найдено." + + response = "📑 Ваши больничные листы:\n\n" + for r in results: + d_from = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10] + d_to = r[1].strftime('%d.%m.%Y') if hasattr(r[1], 'strftime') else str(r[1])[:10] + + days_count = "" + if hasattr(r[0], 'strftime') and hasattr(r[1], 'strftime'): + delta_days = (r[1] - r[0]).days + 1 + days_count = f" — {delta_days} дн." + + response += f"🩺 {d_from} по {d_to}{days_count}\n" + return response + except Exception as e: + raise Exception(f"SQL Sick Leaves Error: {e}") + +def get_missions_records(card_id: str) -> list: + """Возвращает структурированный список pre-formatted текстовых блоков командировок""" + try: + conn = get_db_connection() + cursor = conn.cursor() + query = """ + SELECT mp.D_FROM, mp.D_TO, CAST(mp.NAME AS VARCHAR(250)), CAST(mp.CITY AS VARCHAR(250)) + FROM UOV_SELFSERVICE_PR_EMP e + JOIN UOV_SELFSERVICE_PR_MISSION m ON e.ID = m.EMP_ID + JOIN UOV_SELFSERVICE_PR_MISSION_P mp ON mp.MISSION_ID = m.ID + WHERE e.CARD_ID = %s + ORDER BY mp.D_FROM DESC + """ + cursor.execute(query, (card_id,)) + results = cursor.fetchall() + conn.close() + + mission_blocks = [] + for r in results: + d_from = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10] + d_to = r[1].strftime('%d.%m.%Y') if hasattr(r[1], 'strftime') else str(r[1])[:10] + firm_name = str(r[2]).strip() if r[2] else "Организация не указана" + city = str(r[3]).strip() if r[3] else "Город не указан" + + block = f"• 📅 {d_from}{d_to}\n 📍 г. {city}, {firm_name}" + mission_blocks.append(block) + + return mission_blocks + except Exception as e: + raise Exception(f"SQL Missions Records Error: {e}") + +def get_transfers_records(card_id: str) -> list: + """Возвращает структурированный список кадровых переходов сотрудника по его CARD_ID""" + try: + conn = get_db_connection() + cursor = conn.cursor() + query = """ + SELECT + tr.D_FROM, + CASE + WHEN tr.D_TO = CAST('1999-12-31' AS DATE) THEN CAST('2099-12-31' AS DATE) + ELSE tr.D_TO + END as D_TO, + CAST(d.NAME AS VARCHAR(250)) as DEPT, + CAST(a.NAME AS VARCHAR(250)) as APPOINT + FROM UOV_SELFSERVICE_PR_EMP e + JOIN UOV_SELFSERVICE_PR_TRANS tr ON tr.EMP_ID = e.ID + JOIN UOV_SELFSERVICE_HR_DEPT d ON d.ID = tr.DEPT_ID + JOIN UOV_SELFSERVICE_ST_APPOINT a ON a.ID = tr.APPOINT_ID + WHERE e.CARD_ID = %s + ORDER BY tr.D_FROM DESC + """ + cursor.execute(query, (card_id,)) + results = cursor.fetchall() + conn.close() + + transfer_blocks = [] + for r in results: + d_from = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10] + if r[1] and (r[1].year == 2099 or r[1].year == 1999): + d_to = "наст. время" + else: + d_to = r[1].strftime('%d.%m.%Y') if hasattr(r[1], 'strftime') else str(r[1])[:10] + + dept = str(r[2]).strip() if r[2] else "Не указан" + appoint = str(r[3]).strip() if r[3] else "Не указана" + + block = f"• ⏳ {d_from} — {d_to}\n 💼 Должность: {appoint}\n 📂 Отдел: {dept}" + transfer_blocks.append(block) + + return transfer_blocks + except Exception as e: + raise Exception(f"SQL Transfers Records Error: {e}") + +def get_rewards_text(card_id: str) -> str: + """Запрашивает историю наград по CARD_ID""" + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(""" + SELECT e.D, CAST(e.NAME AS VARCHAR(1000)) + FROM UOV_SELFSERVICE_PR_REW_ENC e + WHERE e.CARD_ID = %s + ORDER BY e.D + """, (card_id,)) + results = cursor.fetchall() + conn.close() + + if not results: + return "🏆 Ваши награды и поощрения:\n\nНичего не найдено." + + response = "🏆 Ваши награды и поощрения:\n\n" + for r in results: + date_str = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10] + response += f"🏅 {date_str} — {str(r[1]).strip()}\n" + return response + except Exception as e: + raise Exception(f"SQL Rewards Error: {e}") + +def get_tabel_report_text(card_id: str, year: int, month: int) -> str: + """Формирует отчет по табелю из UOV_SELFSERVICE_TB_TABEL на основе CARD_ID""" + try: + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,)) + row = cursor.fetchone() + if not row: + conn.close() + return "📋 Рабочий табель:\n\nСотрудник не найден в базе данных." + emp_id = row[0] + + query = """ + SELECT e.D, e.TDAY_ID, e.H + FROM UOV_SELFSERVICE_TB_TABEL e + WHERE e.EMP_ID = %s AND YEAR(e.D) = %s AND MONTH(e.D) = %s + ORDER BY e.D ASC + """ + cursor.execute(query, (emp_id, year, month)) + records = cursor.fetchall() + conn.close() + + if not records: + return f"📋 Рабочий табель за {month:02d}.{year} г.\n\nДанные отсутствуют." + + tabel_map = {r[0].day if hasattr(r[0], 'day') else int(str(r[0])[-2:]): (str(r[1]).strip(), float(r[2]) if r[2] is not None else 0.0) for r in records} + + total_days = 0 + total_hours = 0.0 + for code, hours in tabel_map.values(): + if code == 'Я': + total_days += 1 + total_hours += hours + + months_names = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"} + + report = ( + f"📅 Рабочий табель за {months_names.get(month, 'Указанный месяц')} {year} г.\n\n" + f"📊 Итого за месяц:\n" + f"• Отработано дней: {total_days}\n" + f"• Всего часов: {total_hours:.1f} ч.\n" + f"• Переработки / Недоработки: 0 ч.\n" + ) + + days_in_month = calendar.monthrange(year, month)[1] + weekday_ru = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] + current_week_days = [] + week_counter = 1 + + for day in range(1, days_in_month + 1): + cur_date = datetime(year, month, day) + wday_idx = cur_date.weekday() + + code, hours = tabel_map.get(day, ('В', 0.0)) + current_week_days.append((day, weekday_ru[wday_idx], code, hours)) + + if wday_idx == 6 or day == days_in_month: + w_start = f"{current_week_days[0][0]:02d}.{month:02d}" + w_end = f"{current_week_days[-1][0]:02d}.{month:02d}" + + report += f"\n🔹 Неделя {week_counter} ({w_start} — {w_end}):\n" + + idx = 0 + while idx < len(current_week_days): + d_num, d_name, c_code, c_hours = current_week_days[idx] + + if c_code == 'Я' and c_hours == 8.2: + run_start = d_name + run_end = d_name + count = 1 + while idx + 1 < len(current_week_days) and current_week_days[idx+1][2] == 'Я' and current_week_days[idx+1][3] == 8.2: + idx += 1 + run_end = current_week_days[idx][1] + count += 1 + range_str = f"{run_start} - {run_end}" if count > 1 else f"{run_start}" + report += f"• {range_str}: 🛠 Явка ({count} дн. по 8.2 ч.)\n" + + elif c_code == 'В' and c_hours == 0.0: + run_start = d_name + run_end = d_name + count = 1 + while idx + 1 < len(current_week_days) and current_week_days[idx+1][2] == 'В' and current_week_days[idx+1][3] == 0.0: + idx += 1 + run_end = current_week_days[idx][1] + count += 1 + range_str = f"{run_start} - {run_end}" if count > 1 else f"{run_start}" + report += f"• {range_str}: 🏖 Выходные (0 ч.)\n" + + else: + emoji = "🛠" if c_code == 'Я' else "⏱" if c_code in ['Б', 'БЛ'] else "🏖" + label = "Явка" if c_code == 'Я' else "Больничный" if c_code in ['Б', 'БЛ'] else "Отпуск" if c_code == 'ОТ' else "Выходной" + report += f"• {d_name}: {emoji} {label} ({c_hours} ч.)\n" + + idx += 1 + + current_week_days = [] + week_counter += 1 + + return report + except Exception as e: + raise Exception(f"SQL Tabel Parser Error: {e}") \ No newline at end of file diff --git a/lk/logs/lk_errors.log b/lk/logs/lk_errors.log new file mode 100644 index 0000000..98a8121 --- /dev/null +++ b/lk/logs/lk_errors.log @@ -0,0 +1 @@ +[2026-07-08 02:23:21] User: ds.krivochenko@tcs.sibcem.ru | Action: Инициализация 2FA расчетного листа | Error: Не удалось извлечь номер телефона: Не удалось извлечь номер телефона из БД BossCopy: (208, b"Invalid object name 'PR_CARD'.DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: Check messages from the SQL Server\n") diff --git a/lk/payslip_service.py b/lk/payslip_service.py new file mode 100644 index 0000000..cc4b41f --- /dev/null +++ b/lk/payslip_service.py @@ -0,0 +1,322 @@ +# /opt/trueconf_bot/lk/payslip_service.py +import calendar +import tempfile +from itertools import zip_longest +from weasyprint import HTML +import config.config as config + +# Перевод английских системных месяцев в русские наименования для бланка PDF +ENG_TO_RU_MONTHS = { + "January": "Январь", "February": "Февраль", "March": "Март", + "April": "Апрель", "May": "Май", "June": "Июнь", + "July": "Июль", "August": "Август", "September": "Сентябрь", + "October": "Октябрь", "November": "Ноябрь", "December": "Декабрь" +} + +def format_currency(summa): + try: + if summa is None: + return "0,00" + f_summa = float(summa) + return f"{f_summa:,.2f}".replace(",", " ").replace(".", ",") + except: + return str(summa) + +def translate_period(period_str): + if not period_str: + return "" + for eng, ru in ENG_TO_RU_MONTHS.items(): + if eng in period_str: + return period_str.replace(eng, ru) + return period_str + +def get_latest_payslip_months(cursor, card_id: str) -> list: + query = """ + SELECT DISTINCT TOP 6 (t.TYEAR * 12 + t.TMONTH) as CMONTH + FROM UOV_SELFSERVICE_PR_EMP e + JOIN UOV_SELFSERVICE_PR_CARD c on c.ID = e.CARD_ID + JOIN UOV_SELFSERVICE_SL_YTAX t on t.CARD_ID = e.CARD_ID + JOIN UOV_SELFSERVICE_PR_TRANS tr on tr.EMP_ID = e.ID + JOIN UOV_SELFSERVICE_ST_APPOINT a on a.ID = tr.APPOINT_ID + JOIN UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID + WHERE c.ID = %s + ORDER BY CMONTH DESC + """ + cursor.execute(query, (card_id,)) + months = cursor.fetchall() + if not months: + raise Exception("Отсутствуют расчетные периоды.") + return [m[0] for m in months] + +def create_payslip_pdf(month_name, target_tyear, + header_sys, header_wage, header_deduct, header_base, + earnings_rows, deductions_rows, noncash_rows, payout_rows, + total_earned, total_withheld, total_noncash, total_payouts, + debt_comp_start, debt_emp_start, debt_comp_end, debt_emp_end, net_pay): + tbody_html = "" + tbody_html += f""" + + 1. Начислено + 2. Удержано + + """ + + for earn, ded in zip_longest(earnings_rows, deductions_rows): + if earn: + earn_name = str(earn[0]).strip() + earn_days = f"{earn[1]:g}".replace('.', ',') if earn[1] else "" + earn_hours = f"{earn[2]:g}".replace('.', ',') if earn[2] else "" + earn_prc = f"{earn[3]:g}" if earn[3] else "" + earn_period = translate_period(earn[4] or "") + earn_summa = format_currency(earn[5]) + earn_html = f'{earn_name}{earn_days}{earn_hours}{earn_prc}{earn_period}{earn_summa}' + else: + earn_html = '' + + if ded: + ded_name = str(ded[0]).strip() + ded_prc = f"{ded[1]:.0f}" if ded[1] else "" + ded_period = translate_period(ded[2] or "") + ded_summa = format_currency(ded[3]) + ded_html = f'{ded_name}{ded_prc}{ded_period}{ded_summa}' + else: + ded_html = '' + + tbody_html += f"{earn_html}{ded_html}" + + tbody_html += f""" + + Всего начислено{format_currency(total_earned)} + Всего удержано{format_currency(total_withheld)} + + + 3. Доходы в неденежной форме + 4. Выплачено + + """ + + for nc, pay in zip_longest(noncash_rows, payout_rows): + if nc: + nc_name = str(nc[0]).strip() + nc_period = translate_period(nc[1] or "") + nc_summa = format_currency(nc[2]) + nc_html = f'{nc_name}{nc_period}{nc_summa}' + else: + nc_html = '' + + if pay: + pay_name = str(pay[0]).strip() + pay_period = translate_period(pay[1] or "") + pay_summa = format_currency(pay[2]) + pay_html = f'{pay_name}{pay_period}{pay_summa}' + else: + pay_html = '' + + tbody_html += f"{nc_html}{pay_html}" + + tbody_html += f""" + + Всего доходов в неденежной форме{format_currency(total_noncash)} + Всего выплачено{format_currency(total_payouts)} + + + Долг за предприятием на начало месяца{format_currency(debt_comp_start)} + Долг за предприятием на конец месяца{format_currency(debt_comp_end)} + + + Долг за работником на начало месяца{format_currency(debt_emp_start)} + Долг за работником на конец месяца{format_currency(debt_emp_end)} + + + + 5. Выплачено денежными средствами{format_currency(net_pay)} + + """ + + html_template = f""" + + + + + + + +

Расчетный лист за {month_name} {target_tyear} г.

+ + + + +
Система оплаты: {header_sys}Оклад: {format_currency(header_wage)}
Стандартные вычеты за месяц: {format_currency(header_deduct)}
Облагаемая база по ставке 13%: {format_currency(header_base)}
+ + + + + + + + + {tbody_html} + +
ВидДниЧасы%ПериодСуммаВид%ПериодСумма
+ + + """ + temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') + HTML(string=html_template).write_pdf(temp_pdf.name) + return temp_pdf.name + +def build_payslip_data(cursor, card_id: str, target_cmonth: int) -> tuple: + tyear = (target_cmonth - 1) // 12 + tmonth = (target_cmonth - 1) % 12 + 1 + _, last_day = calendar.monthrange(tyear, tmonth) + + date_start = f"{tyear}-{tmonth:02d}-01" + date_end = f"{tyear}-{tmonth:02d}-{last_day:02d}" + date_params = (date_start, date_start, date_end, date_end, date_end) + + months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", + 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"} + month_name = months_ru.get(tmonth, f"{tmonth:02d}") + + # FIRM_ID по c.ID + q_firm = """ + select e.FIRM_ID, f.SNAME from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e + join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s + group by e.ID, e.FIRM_ID ) as e + join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID + join UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID + where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) + group by e.FIRM_ID, f.SNAME + """ + cursor.execute(q_firm, (card_id, *date_params)) + firm_row = cursor.fetchone() + if not firm_row: + raise Exception("Не удалось определить FIRM_ID для сотрудника в указанном периоде.") + firm_id = firm_row[0] + + # ШАПКА по c.ID + q_header = """ + select s.NAME as [NAME], ROUND(t.WAGE, 2), isnull(sum(yt.u21 + yt.u30), 0), ROUND(isnull(sum(yt.n200 + yt.n5100 - yt.u21 - yt.u30 - yt.u42 - yt.u99), 0), 2) + from (select e.ID, e.FIRM_ID, e.CARD_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID + where c.ID = %s and e.FIRM_ID=%s group by e.ID, e.FIRM_ID, e.CARD_ID ) as e + join UOV_SELFSERVICE_pr_trans t on t.EMP_ID = e.ID join UOV_SELFSERVICE_hr_syspay s on s.ID = t.SYSPAY_ID + join UOV_SELFSERVICE_sl_ytax yt on yt.CARD_ID = e.CARD_ID and yt.FIRM_ID = e.FIRM_ID + where e.FIRM_ID = %s and yt.tmonth = %s and yt.tyear = %s + and ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) + and t.tp_abbr='CUR' group by s.NAME, t.WAGE, t.D_FROM order by t.D_FROM desc + """ + cursor.execute(q_header, (card_id, firm_id, firm_id, tmonth, tyear, *date_params)) + h_row = cursor.fetchone() + header_sys = str(h_row[0]) if h_row else "Неизвестная система" + header_wage = float(h_row[1]) if h_row and h_row[1] else 0.0 + header_deduct = float(h_row[2]) if h_row and h_row[2] else 0.0 + header_base = float(h_row[3]) if h_row and h_row[3] else 0.0 + + # НАЧИСЛЕНО по c.ID + q_earn = """ + select p.NAME AS [NAME], ROUND(l.days, 1), ROUND(l.hours, 1), CAST(l.prc as INT), convert(varchar, i.D_FROM,104)+'-'+convert(varchar, i.D_TO,104), ROUND(l.summa, 2) + from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e + join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID = l.PAY_ID join UOV_SELFSERVICE_pr_ill i on i.ID=l.ID_MAIN + join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID + where l.t_pay = 1 and l.pay_id not in (264,301,262) and l.n_dirm = 6 and l.cmonth = %s + UNION ALL + select max(p.NAME) AS [NAME], sum(l.days), ROUND(sum(l.hours), 1), CAST(l.prc as INT), (DateName(month, DateAdd(month, l.TMONTH, 0) - 1) +' '+ cast(l.TYEAR as varchar)), ROUND(isnull(sum(l.summa),0), 2) + from(select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e + join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID + join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID = l.PAY_ID + where l.t_pay = 1 and l.pay_id not in (264,301,262) and l.n_dirm <> 6 and l.cmonth = %s + group by l.emp_id,l.pay_id,l.tmonth,l.prc, case l.PAY_ID when 17 then l.id else 0 end, l.TYEAR order by 6 DESC + """ + cursor.execute(q_earn, (card_id, firm_id, *date_params, target_cmonth, card_id, firm_id, *date_params, target_cmonth)) + earnings_rows = cursor.fetchall() + + # УДЕРЖАНО по c.ID + q_deduct = """ + select max(p.NAME) AS [NAME], CAST(l.prc as INT), ISNULL(DATENAME(month, DATEADD(month, l.tmonth, 0) - 1) + ' ' + CAST(NULLIF(l.TYEAR, 0) AS varchar), ''), ROUND(isnull(sum(l.summa),0), 2) + from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e + JOIN UOV_SELFSERVICE_sl_lic l ON l.EMP_ID = e.ID JOIN UOV_SELFSERVICE_sl_pay p ON p.ID = l.PAY_ID + join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID + where l.t_pay=2 and l.pay_id not in (301,490) and l.pay_id not in (305, 308, 316, 328, 360, 364) and l.cmonth = %s + group by l.emp_id,l.pay_id,l.tmonth,l.prc, l.id, l.TYEAR order by 4 DESC + """ + cursor.execute(q_deduct, (card_id, firm_id, *date_params, target_cmonth)) + deductions_rows = cursor.fetchall() + + # ДОЛГИ по c.ID + q_debts = """ + select l.pay_id, ROUND(isnull(sum(l.summa),0), 2) from (select e.ID, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e + join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.id join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID + where l.cmonth = %s and l.pay_id in (264, 301, 490) group by l.pay_id + """ + cursor.execute(q_debts, (card_id, firm_id, *date_params, target_cmonth)) + debt_rows = cursor.fetchall() + debt_comp_start, debt_emp_start, debt_emp_end = 0.0, 0.0, 0.0 + for r in debt_rows: + if r[0] == 264: debt_comp_start = float(r[1]) + elif r[0] == 301: debt_emp_start = float(r[1]) + elif r[0] == 490: debt_emp_end = float(r[1]) + + # НЕ ДЕНЕЖНЫЕ по c.ID + q_noncash = """ + select t.NAME AS [NAME], (DateName(month, CAST(%s as date)) +' '+ DateName(YEAR, CAST(%s as date))), ROUND(isnull(p.summa, 0), 2) + from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e + join UOV_SELFSERVICE_sl_profit p on p.EMP_ID = e.ID join UOV_SELFSERVICE_sl_tprofit t on t.ID = p.TPROFIT_ID + join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans tr where ((tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO = '1999-12-31')) group by EMP_ID) tr2 on tr2.EMP_ID = e.ID + where p.cmonth = %s and t.id != 9999 + """ + cursor.execute(q_noncash, (date_start, date_start, card_id, firm_id, *date_params, target_cmonth)) + noncash_rows = cursor.fetchall() + + # ВЫПЛАЧЕНО по c.ID + q_payout = """ + select p.NAME AS [NAME], (DateName(month, DateAdd(month, l.TMONTH, 0) - 1) +' '+ cast(l.TYEAR as varchar)), ROUND(sum(isnull(l.summa, 0)), 2) + from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e + join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID =l.PAY_ID + join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans tr where ((tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO = '1999-12-31')) group by EMP_ID) tr2 on tr2.EMP_ID = e.ID + where l.t_pay = 2 and l.cmonth = %s and l.pay_id in (305, 308, 316, 328, 360, 364) group by p.NAME, l.TMONTH, l.TYEAR + """ + cursor.execute(q_payout, (card_id, firm_id, *date_params, target_cmonth)) + payout_rows = cursor.fetchall() + + total_earned = sum(float(r[5]) for r in earnings_rows if r[5]) + total_withheld = sum(float(r[3]) for r in deductions_rows if r[3]) + total_noncash = sum(float(r[2]) for r in noncash_rows if r[2]) + total_payouts = sum(float(r[2]) for r in payout_rows if r[2]) + + # Расчет долга по формуле СибЦема + debt_comp_end = total_earned - total_withheld - total_payouts + net_pay = total_earned - total_withheld + + response = f"💰 Расчетный листок за {month_name} {tyear} г.\n\n1. Начислено\n" + for row in earnings_rows: + time_spent = f" ({float(row[1]):g} дн. / {float(row[2]):g} ч.)" if (row[1] or row[2]) else "" + response += f"🔹 {str(row[0]).strip()}{time_spent}: {format_currency(row[5])} руб.\n" + response += f"Всего начислено: {format_currency(total_earned)} руб.\n\n2. Удержано\n" + for row in deductions_rows: + prc_str = f" ({int(row[1])}%)" if row[1] else "" + response += f"🔹 {str(row[0]).strip()}{prc_str}: {format_currency(row[3])} руб.\n" + response += f"Всего удержано: {format_currency(total_withheld)} руб.\n\n3. Доходы в неденежной форме: {format_currency(total_noncash)} руб.\n\n4. Выплачено\n" + for row in payout_rows: + response += f"🔹 {str(row[0]).strip()}: {format_currency(row[2])} руб.\n" + response += f"Всего выплачено: {format_currency(total_payouts)} руб.\n\nДолг за предприятием на конец месяца: {format_currency(debt_comp_end)} руб.\nДолг за работником на конец месяца: {format_currency(debt_emp_end)} руб.\n\n5. Выплачено денежными средствами: {format_currency(net_pay)} руб." + + pdf_path = create_payslip_pdf( + month_name, tyear, header_sys, header_wage, header_deduct, header_base, + earnings_rows, deductions_rows, noncash_rows, payout_rows, + total_earned, total_withheld, total_noncash, total_payouts, + debt_comp_start, debt_emp_start, debt_comp_end, debt_emp_end, net_pay + ) + return response, pdf_path \ No newline at end of file diff --git a/lk/test_lk_payslip.py b/lk/test_lk_payslip.py new file mode 100644 index 0000000..ec961f3 --- /dev/null +++ b/lk/test_lk_payslip.py @@ -0,0 +1,213 @@ +import re +from datetime import datetime + +import pymssql +from ldap3 import Server, Connection, ALL +from ldap3.utils.conv import escape_filter_chars + + +# ========================================================= +# НАСТРОЙКИ +# ========================================================= + +TEST_USER = "man.bogov@tcs.sibcem.ru" + +AD_SERVER = "ldap://172.16.20.20" +AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru" + +AD_USER = "sdesk-mail" +AD_PASSWORD = "fne?e!q.m8phcrGVAcqr" + +SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru" +SQL_DB_NAME = "BossCopy" + +DB_USER = "SIBCEM\bot_tcs" +DB_PASSWORD = "Bav:fX#UwH8%atv4" + + +# ========================================================= +# AD +# ========================================================= + +def get_phone_from_ad(user_id: str) -> str: + + server = Server(AD_SERVER, get_info=ALL) + + conn = Connection( + server, + user=AD_USER, + password=AD_PASSWORD, + auto_bind=True + ) + + safe_user_id = escape_filter_chars(user_id) + + short_username = user_id.split("@")[0] + + search_filter = ( + f"(&(objectClass=user)" + f"(|(mail={safe_user_id})" + f"(userPrincipalName={safe_user_id})" + f"(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})" + f"(sAMAccountName={escape_filter_chars(short_username)})))" + ) + + conn.search( + AD_BASE, + search_filter, + attributes=['mobile', 'telephoneNumber'] + ) + + if not conn.entries: + raise Exception("Пользователь не найден в AD") + + entry = conn.entries[0] + + raw_phone = ( + str(entry.mobile.value) + if 'mobile' in entry and entry.mobile.value + else str(entry.telephoneNumber.value) + if 'telephoneNumber' in entry and entry.telephoneNumber.value + else None + ) + + if not raw_phone: + raise Exception("Телефон не найден") + + clean_phone = re.sub(r'\D', '', raw_phone) + + if clean_phone.startswith("8") and len(clean_phone) == 11: + return "7" + clean_phone[1:] + + if len(clean_phone) == 10: + return "7" + clean_phone + + return clean_phone + + +# ========================================================= +# SQL +# ========================================================= + +def get_db_connection(): + + return pymssql.connect( + server=SQL_SERVER, + user=DB_USER, + password=DB_PASSWORD, + database=SQL_DB_NAME, + charset='cp1251' + ) + + +# ========================================================= +# MAIN +# ========================================================= + +def main(): + + print("=" * 80) + print("ТЕСТ РАСЧЕТНЫХ ЛИСТКОВ") + print("=" * 80) + + date = datetime.now().date() + + cmonth = date.year * 12 + date.month + + print(f"Дата: {date}") + print(f"CMONTH: {cmonth}") + + # ----------------------------------------------------- + + print("\n[1] Получение телефона из AD") + + phone_full = get_phone_from_ad(TEST_USER) + + print(f"Телефон из AD: {phone_full}") + + phone = phone_full[1:].strip() + + print(f"Телефон для SQL: {phone}") + + # ----------------------------------------------------- + + print("\n[2] Подключение к SQL") + + conn = get_db_connection() + + cursor = conn.cursor() + + # ----------------------------------------------------- + + print("\n[3] Поиск PR_CARD") + + query = """ + SELECT + ID, + PHONE + FROM UOV_SELFSERVICE_PR_CARD + WHERE PHONE = %s + """ + + print(query) + + cursor.execute(query, (phone,)) + + rows = cursor.fetchall() + + print(f"\nНайдено записей: {len(rows)}") + + if rows: + + for row in rows: + + print("-" * 60) + print(f"ID: {row[0]}") + print(f"PHONE: {row[1]}") + + else: + + print("\n❌ Запись не найдена") + + print("\n[4] Поиск похожих телефонов") + + query2 = """ + SELECT TOP 20 + ID, + PHONE + FROM UOV_SELFSERVICE_PR_CARD + WHERE + PHONE LIKE %s + OR RIGHT(RTRIM(PHONE), 10) = %s + """ + + phone_tail = phone[-10:] + + cursor.execute( + query2, + ( + f"%{phone_tail}%", + phone_tail + ) + ) + + rows2 = cursor.fetchall() + + print(f"\nПохожих записей: {len(rows2)}") + + for row in rows2: + + print("-" * 60) + print(f"ID: {row[0]}") + print(f"PHONE: {row[1]}") + + conn.close() + + print("\n") + print("=" * 80) + print("ГОТОВО") + print("=" * 80) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lk/test_sql.py b/lk/test_sql.py new file mode 100644 index 0000000..8cc1f2a --- /dev/null +++ b/lk/test_sql.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# /opt/trueconf_bot/test_secure_connect.py + +import sys +import os +import pymssql + +# Добавляем путь к проекту для корректных импортов конфигурации +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +try: + print("🔄 Загружаем конфигурацию и выгружаем секреты из Passwork...") + import config.config as config +except ImportError as e: + print(f"❌ Ошибка импорта config.py: {e}") + sys.exit(1) + +def main(): + # Технические параметры целевого сервера + SERVER = '172.16.20.203' + DATABASE = 'iskmdax_model' + + # Извлекаем данные из автоматически загруженного пула + USER = getattr(config, 'DB_USER', None) + PASSWORD = getattr(config, 'DB_PASSWORD', None) + + if not USER or not PASSWORD: + print("❌ Ошибка: Учетные данные для подключения к БД не найдены в Passwork!") + sys.exit(1) + + print(f"🔌 Устанавливаем ЗАЩИЩЕННОЕ соединение (encryption='require')...") + print(f" Сервер: {SERVER}") + print(f" База: {DATABASE}") + print(f" Логин: [ДАННЫЕ СКРЫТЫ]") + print("-" * 60) + + try: + conn = pymssql.connect( + server=SERVER, + user=USER, + password=PASSWORD, + database=DATABASE, + charset='cp1251', + encryption='require' # Строгое требование SSL/TLS + ) + cursor = conn.cursor() + print("✅ Защищенное TLS/SSL соединение с сервером успешно установлено!") + + # Проверочный SQL-запрос + query = "SELECT TOP(5) * FROM dbo.Layer;" + print(f"🏃 Выполняем тестовый запрос: {query}\n") + + cursor.execute(query) + rows = cursor.fetchall() + + if rows: + print(f"🎯 Успешно получено строк: {len(rows)}") + for idx, row in enumerate(rows, 1): + # Печатаем содержимое строки как есть + print(f" 🔹 Строка #{idx}: {row}") + else: + print("📭 Запрос выполнен успешно, но целевая таблица оказалась пустой.") + + conn.close() + print("\n🏁 Тест защищенного соединения завершен успешно!") + + except Exception as e: + print(f"\n❌ Ошибка подключения или выполнения запроса!") + error_msg = str(e) + if "Login failed" in error_msg or "18456" in error_msg: + print("Детали: Ошибка аутентификации (проверьте корректность учетных данных в Passwork).") + else: + print(f"Детали: {error_msg}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/logs/menu_stats.csv b/logs/menu_stats.csv new file mode 100644 index 0000000..04a2904 --- /dev/null +++ b/logs/menu_stats.csv @@ -0,0 +1,18 @@ +Timestamp;User_ID;Main_Menu;Sub_Menu +2026-07-02 18:59:49;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Расчётный лист +2026-07-02 18:59:53;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Запрос кода 2FA для расчетного листа (в MAX) +2026-07-02 18:59:58;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Успешная проверка 2FA расчетного листа +2026-07-02 19:00:15;ds.krivochenko@tcs.sibcem.ru;Speech-to-Text;Вход +2026-07-02 19:00:48;ds.krivochenko@tcs.sibcem.ru;Speech-to-Text;Отправка встречи на расшифровку (.mp4) +2026-07-02 19:01:06;ds.krivochenko@tcs.sibcem.ru;Speech-to-Text;Выход в главное меню +2026-07-02 19:01:10;ds.krivochenko@tcs.sibcem.ru;Service Desk;Вход +2026-07-02 19:01:35;ds.krivochenko@tcs.sibcem.ru;Service Desk;Создание заявки #23108 +2026-07-03 14:55:32;ds.krivochenko@tcs.sibcem.ru;Service Desk;Вход +2026-07-03 15:00:29;ds.krivochenko@tcs.sibcem.ru;Service Desk;Вход +2026-07-03 15:00:39;ds.krivochenko@tcs.sibcem.ru;Service Desk;Создание заявки #23133 +2026-07-03 15:07:19;ds.krivochenko@tcs.sibcem.ru;Service Desk;Вход +2026-07-03 15:07:30;ds.krivochenko@tcs.sibcem.ru;Service Desk;Создание заявки #23134 +2026-07-08 02:23:18;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Расчётный лист +2026-07-08 02:35:13;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Расчётный лист +2026-07-08 02:35:15;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Запрос кода 2FA для расчетного листа (в MAX) +2026-07-08 02:35:20;ds.krivochenko@tcs.sibcem.ru;Личный кабинет;Успешная проверка 2FA расчетного листа diff --git a/main.py b/main.py new file mode 100644 index 0000000..38ba8ff --- /dev/null +++ b/main.py @@ -0,0 +1,181 @@ +import sys +import asyncio +import logging +import os +import psutil +import time +from datetime import datetime + +# --- НАСТРОЙКА ЛОГИРОВАНИЯ --- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", + handlers=[ + logging.FileHandler("bot_log.txt", encoding="utf-8"), + logging.StreamHandler(sys.stdout) + ] +) +logging.getLogger('trueconf.client.chatbot').propagate = False +logger = logging.getLogger(__name__) + +# Добавляем путь к проекту в sys.path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from trueconf import Bot, Dispatcher, Router, Message +from config.config import * +from utils.states import set_state, clear_state, get_state +from utils import states + +# 🎯 ИСПРАВЛЕНО: Импортируем оба варианта меню (короткий и приветственный) +from utils.menu import MENU_TEXT, WELCOME_TEXT, MENU_MAP + +# ========================================================= +# 🔄 МОДУЛЬ АВТОМАТИЧЕСКОГО КОНТРОЛЯ СЕССИЙ И ТАЙМАУТОВ +# ========================================================= +USER_LAST_ACTIVITY = {} +USER_MENU_SHOWN = {} +DEFAULT_TIMEOUT = 1800 + +_orig_get_state = states.get_state + +def patched_get_state(user_id): + current_time = time.time() + last_activity = USER_LAST_ACTIVITY.get(user_id) + session_timeout = getattr(states.config, 'SESSION_TIMEOUT', DEFAULT_TIMEOUT) + if last_activity and (current_time - last_activity > session_timeout): + states.clear_state(user_id) + USER_MENU_SHOWN[user_id] = False + logger.info(f"⏳ Сессия пользователя {user_id} сброшена по таймауту бездействия ({session_timeout} сек).") + USER_LAST_ACTIVITY[user_id] = current_time + return _orig_get_state(user_id) + +states.get_state = patched_get_state +get_state = patched_get_state + +# ========================================================= +# 🔌 КОРПОРАТИВНЫЕ ШАБЛОНЫ ТЕКСТОВ КЛЕВЕР +# ========================================================= +from utils.texts import UNKNOWN_MAIN_CMD_TEXT +from utils.stats_logger import log_menu_stats + +# ========================================================= +# ФУНКЦИЯ МОНИТОРИНГА ОПЕРАТИВНОЙ ПАМЯТИ +# ========================================================= +def log_ram_usage(stage_name: str): + process = psutil.Process(os.getpid()) + ram_mb = process.memory_info().rss / 1024 / 1024 + logger.info(f"📊 [RAM MONITOR] {stage_name}: {ram_mb:.2f} MB") + +log_ram_usage("Старт скрипта (базовый вес)") + +from photo_bot.handlers import router as photo_router, PHOTO_MAIN_MENU_TEXT +log_ram_usage("После импорта photo_bot") + +from service_desk.handlers import router as sd_router, SD_MAIN_MENU_TEXT +log_ram_usage("После импорта service_desk") + +from transcription_bot.handlers import router as transcription_router, TRANSCRIPTION_MAIN_MENU_TEXT +log_ram_usage("После импорта transcription_router (Виспер)") + +from search_bot.handlers import router as search_router, SEARCH_MAIN_MENU_TEXT +log_ram_usage("После импорта search_router") + +from lk.handlers import router as lk_router +log_ram_usage("После импорта lk_router (Личный кабинет)") + +# ========================================================= +# ГЛАВНЫЙ РОУТЕР +# ========================================================= +main_router = Router() + +@main_router.message() +async def main_menu(msg: Message): + if ENABLE_WHITELIST and msg.from_user.id not in ALLOWED_USERS: + logger.info(f"Доступ закрыт (Whitelist ON): Игнорирование пользователя {msg.from_user.id}") + return + user_id = msg.from_user.id + if getattr(msg, "handled", False): + return + if get_state(user_id) is not None: + return + is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" + if is_attachment: + await msg.answer("⚠️ Сначала выберите режим работы из меню.", parse_mode="html") + await msg.answer(WELCOME_TEXT, parse_mode="html") + USER_MENU_SHOWN[user_id] = True + return + menu_shown = USER_MENU_SHOWN.get(user_id, False) + if msg.text: + cmd = msg.text.strip().lower() + action = MENU_MAP.get(cmd) + if action == "SD": + USER_MENU_SHOWN[user_id] = True + set_state(user_id, "SD_MODE") + log_menu_stats(user_id, "Service Desk", "Вход") + await msg.answer(SD_MAIN_MENU_TEXT, parse_mode="html") + return + elif action == "PHOTO": + USER_MENU_SHOWN[user_id] = True + set_state(user_id, "PHOTO_MODE") + log_menu_stats(user_id, "Photo Bot", "Вход") + await msg.answer(PHOTO_MAIN_MENU_TEXT, parse_mode="html") + return + elif action == "TRANSCRIPTION": + USER_MENU_SHOWN[user_id] = True + set_state(user_id, "TRANSCRIPTION_MODE") + log_menu_stats(user_id, "Speech-to-Text", "Вход") + await msg.answer(TRANSCRIPTION_MAIN_MENU_TEXT, parse_mode="html") + return + elif action == "SEARCH": + USER_MENU_SHOWN[user_id] = True + set_state(user_id, "SEARCH_MODE") + log_menu_stats(user_id, "Search Bot", "Вход") + await msg.answer(SEARCH_MAIN_MENU_TEXT, parse_mode="html") + return + elif action == "LK": + USER_MENU_SHOWN[user_id] = True + set_state(user_id, "LK_MODE") + log_menu_stats(user_id, "Личный кабинет", "Вход") + from lk.handlers import send_dynamic_lk_main_menu + await send_dynamic_lk_main_menu(msg, user_id) + return + else: + if not menu_shown: + USER_MENU_SHOWN[user_id] = True + await msg.answer(WELCOME_TEXT, parse_mode="html") + return + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + return + USER_MENU_SHOWN[user_id] = True + await msg.answer(WELCOME_TEXT, parse_mode="html") + +async def main(): + logger.info("Инициализация Dispatcher и подключение роутеров...") + dp = Dispatcher() + dp.include_router(photo_router) + dp.include_router(sd_router) + dp.include_router(transcription_router) + dp.include_router(search_router) + dp.include_router(lk_router) + dp.include_router(main_router) + bot = Bot.from_credentials(server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False) + states.tc_bot = bot + logger.info("Бот запущен и готов к работе!") + target_chat = globals().get('ADMIN_CHAT_ID', "533a9784afdfe80c8d80b971a732cd2520a71dc6") + async def send_startup_notification(bot_instance, chat_id): + await asyncio.sleep(10) + try: + await bot_instance.send_message(chat_id, "✅ Я - online! Запуск прошел успешно.", parse_mode="html") + logger.info(f"Стартовое сообщение успешно отправлено в чат {chat_id}") + except Exception as e: + logger.error(f"Не удалось отправить стартовое сообщение администратору: {e}") + asyncio.create_task(send_startup_notification(bot, target_chat)) + logger.info("Подключение к серверу TrueConf...") + await bot.run() + +if __name__ == "__main__": + try: + main_router.message() + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Бот остановлен вручную.") diff --git a/photo_bot/__init__.py b/photo_bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/photo_bot/ad_helper.py b/photo_bot/ad_helper.py new file mode 100644 index 0000000..6d80fd9 --- /dev/null +++ b/photo_bot/ad_helper.py @@ -0,0 +1,49 @@ +import logging +import os +import sys +from ldap3 import Server, Connection, ALL, MODIFY_REPLACE + +# Подтягиваем конфиг +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE + +logger = logging.getLogger(__name__) + +def upload_photo_to_ad(target_user: str, photo_path: str) -> dict: + """ + Читает готовое фото с диска и записывает его в атрибут thumbnailPhoto в AD. + """ + try: + # Читаем сырые байты картинки + with open(photo_path, "rb") as f: + photo_bytes = f.read() + + # Формируем фильтр для поиска + if '@' in target_user: + ldap_filter = f"(mail={target_user})" + else: + ldap_filter = f"(sAMAccountName={target_user})" + + # Подключаемся к AD + server = Server(AD_SERVER, get_info=ALL) + conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True) + + # Находим пользователя в дереве (нам нужен его точный путь - DN) + conn.search(search_base=AD_BASE, search_filter=ldap_filter, attributes=["cn"]) + + if not conn.entries: + return {"success": False, "error": f"Пользователь {target_user} не найден в AD."} + + user_dn = conn.entries[0].entry_dn + + # 🪄 МАГИЯ: Перезаписываем атрибут thumbnailPhoto нашими байтами + conn.modify(user_dn, {'thumbnailPhoto': [(MODIFY_REPLACE, [photo_bytes])]}) + + if conn.result['result'] == 0: + return {"success": True} + else: + return {"success": False, "error": str(conn.result['description'])} + + except Exception as e: + logger.error(f"Ошибка загрузки в AD для {target_user}: {e}") + return {"success": False, "error": "Сбой при связи с сервером Active Directory."} \ No newline at end of file diff --git a/photo_bot/ad_photo_bot.py b/photo_bot/ad_photo_bot.py new file mode 100644 index 0000000..c734cbe --- /dev/null +++ b/photo_bot/ad_photo_bot.py @@ -0,0 +1,181 @@ +import os +import asyncio +import logging +import sys + +# Добавляем путь к корню проекта +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from trueconf import Bot, Dispatcher, Router, Message +from trueconf.types import FSInputFile + +# Наши обработчики +from photo_processor import prepare_ad_photo +from ad_helper import upload_photo_to_ad + +# Настройки +from config.config import * + +# --- НАСТРОЙКА ЛОГИРОВАНИЯ --- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("photo_bot_log.txt", encoding="utf-8"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +router = Router() +tc_bot = None + +# 🧠 ПАМЯТЬ БОТА: Храним пути к фото, ожидающим подтверждения +# Пример: {'ds.krivochenko': '/tmp/ready_ds.krivochenko.jpg'} +pending_photos = {} + +@router.message() +async def handle_message(msg: Message): + global tc_bot + + # Определяем логин пользователя + user_login = msg.from_user.id.split('@')[0] if '@' in msg.from_user.id else msg.from_user.id + + # Проверяем, вложение ли это + is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" + + # ===================================================================== + # СЦЕНАРИЙ 1: Бот ждет ответа, и пользователь написал текст (Да/Нет) + # ===================================================================== + if not is_attachment and user_login in pending_photos and msg.text: + answer = msg.text.strip().lower() + + # Если согласен + if answer in ["да", "yes", "+", "загрузить", "давай", "конечно"]: + photo_path = pending_photos[user_login] + await msg.answer("⏳ Подключаюсь к Active Directory и загружаю фото...") + + # Запускаем скрипт загрузки в AD + upload_result = await asyncio.to_thread(upload_photo_to_ad, user_login, photo_path) + + if upload_result["success"]: + await msg.answer( + "🎉 **Ура! Фото успешно установлено в Active Directory!**\n\n" + "Оно появится в Outlook, TrueConf и других системах в течение нескольких часов (после синхронизации серверов)." + ) + else: + await msg.answer(f"❌ **Ошибка при загрузке:** {upload_result.get('error')}") + + # Убираем за собой файл и стираем пользователя из памяти + if os.path.exists(photo_path): + os.remove(photo_path) + del pending_photos[user_login] + return + + # Если отказался + elif answer in ["нет", "no", "-", "отмена"]: + photo_path = pending_photos[user_login] + await msg.answer("🚫 Загрузка отменена. Можете прислать мне другую фотографию!") + + if os.path.exists(photo_path): + os.remove(photo_path) + del pending_photos[user_login] + return + + # Если написал что-то непонятное, но мы ждем ответа + else: + await msg.answer("❓ Пожалуйста, напишите **Да** (чтобы загрузить фото в AD) или **Нет** (чтобы отменить). Либо просто пришлите новую фотографию.") + return + + # ===================================================================== + # СЦЕНАРИЙ 2: Пользователь прислал новую фотографию + # ===================================================================== + if is_attachment: + file_id = getattr(msg.content, "file_id", None) + + if not file_id: + await msg.answer("❌ Не удалось получить файл. Попробуйте еще раз.") + return + + ready_path = f"/tmp/ready_{user_login}.jpg" + raw_path = None + + await msg.answer("⏳ Обрабатываю ваше фото, пожалуйста, подождите...") + + try: + downloaded_file = await tc_bot.download_file_by_id(file_id) + raw_path = downloaded_file + + # Обрабатываем фото (с новым разрешением 240x240) + result = await asyncio.to_thread(prepare_ad_photo, raw_path, ready_path, 240) + + if result["success"]: + photo_document = FSInputFile(ready_path, filename=f"AD_{user_login}.jpg") + + # Отправляем фото и СРАЗУ задаем вопрос + await msg.answer_photo( + photo_document, + photo_document, + caption=( + f"✅ **Фото готово!**\n\n" + f"📐 Размер: 240x240 px\n" + f"⚖️ Вес: {result['size_kb']} KB\n\n" + f"❓ **Вам нравится результат? Загружаем в Active Directory?**\n" + f"Напишите мне в ответ **Да** или **Нет**." + ) + ) + + # 🧠 ЗАПОМИНАЕМ, что этот пользователь теперь в режиме ожидания ответа + pending_photos[user_login] = ready_path + logger.info(f"Фото подготовлено для {user_login}, ждем подтверждения.") + + else: + await msg.answer(f"❌ Ошибка обработки: {result.get('error')}") + + except Exception as e: + logger.error(f"Ошибка при работе с фото для {user_login}: {e}") + await msg.answer("❌ Произошла системная ошибка при обработке.") + + finally: + # ВАЖНО: Удаляем ТОЛЬКО оригинал от TrueConf (raw_path). + # Готовое фото (ready_path) оставляем на диске, оно ждет ответа "Да"! + if raw_path and os.path.exists(raw_path): + try: os.remove(raw_path) + except: pass + + return + + # ===================================================================== + # СЦЕНАРИЙ 3: Дефолтное приветствие (если просто написали текст) + # ===================================================================== + await msg.answer( + "👋 Привет! Я корпоративный фотограф для Active Directory.\n\n" + "📸 **Просто отправь мне фото**, и я автоматически:\n" + "1. Найду лицо на снимке и сделаю правильный кадр.\n" + "2. Улучшу резкость и сожму до стандарта 240x240 px.\n" + "3. Предложу загрузить результат прямо в твой профиль!" + ) + +async def main(): + global tc_bot + + logger.info(f"Запуск фото-бота на сервере {TC_SERVER}...") + + dp = Dispatcher() + dp.include_router(router) + + tc_bot = Bot.from_credentials( + server=TC_SERVER, + username=TC_LOGIN, + password=TC_PASSWORD, + dispatcher=dp, + verify_ssl=False + ) + + await tc_bot.run() + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Бот остановлен пользователем.") \ No newline at end of file diff --git a/photo_bot/check_ad_photo.py b/photo_bot/check_ad_photo.py new file mode 100644 index 0000000..26be3a8 --- /dev/null +++ b/photo_bot/check_ad_photo.py @@ -0,0 +1,83 @@ +import sys +import os +import io +import logging +from ldap3 import Server, Connection, ALL +from PIL import Image + +# Подтягиваем ваши настройки из конфига +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE + +logging.basicConfig(level=logging.INFO, format="%(message)s") + +def check_user_photo(target_user: str): + print(f"🔍 Ищем пользователя: {target_user}...") + + # Если передали email, ищем по почте. Если просто логин — по sAMAccountName + if '@' in target_user: + ldap_filter = f"(mail={target_user})" + else: + ldap_filter = f"(sAMAccountName={target_user})" + + try: + # Подключаемся к AD + server = Server(AD_SERVER, get_info=ALL) + conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True) + + # Ищем пользователя и просим вернуть атрибуты: имя (cn) и фото (thumbnailPhoto) + conn.search( + search_base=AD_BASE, + search_filter=ldap_filter, + attributes=["cn", "thumbnailPhoto"] + ) + + if not conn.entries: + print("❌ Пользователь с такими данными не найден в AD.") + return + + user = conn.entries[0] + user_name = user.cn.value if 'cn' in user else target_user + print(f"👤 Найден сотрудник: {user_name}") + + # Проверяем, есть ли вообще фото + if 'thumbnailPhoto' not in user or not user.thumbnailPhoto.value: + print("⚠️ У этого пользователя НЕТ фотографии в Active Directory.") + return + + # Достаем байты фотографии + photo_bytes = user.thumbnailPhoto.value + size_kb = len(photo_bytes) / 1024 + + # Читаем картинку прямо из памяти (без сохранения на диск) с помощью Pillow + image = Image.open(io.BytesIO(photo_bytes)) + width, height = image.size + + print("\n📊 --- ИНФОРМАЦИЯ О ФОТО В AD ---") + print(f"📐 Разрешение : {width}x{height} пикселей") + print(f"⚖️ Вес : {size_kb:.1f} KB") + print(f"🗂 Формат : {image.format}") + print("----------------------------------\n") + + # Бонус: сохраняем фото во временную папку, чтобы вы могли на него взглянуть + safe_name = target_user.replace('@', '_').replace('.', '_') + save_path = f"/tmp/{safe_name}_ad.jpg" + + with open(save_path, "wb") as f: + f.write(photo_bytes) + + print(f"💾 Оригинал из AD сохранен сюда: {save_path}") + print("Вы можете скачать его через WinSCP или открыть в TrueConf, чтобы оценить качество глазами.") + + except Exception as e: + print(f"❌ Ошибка при подключении к AD: {e}") + +if __name__ == "__main__": + # Замените на нужный email или логин (например, 'n.shmargalova' или 'n.shmargalova@sibcem.ru') + target = "n.shmargalova@sibcem.ru" + + # Если скрипт запущен с аргументом (python check_ad_photo.py логин), берем аргумент + if len(sys.argv) > 1: + target = sys.argv[1] + + check_user_photo(target) \ No newline at end of file diff --git a/photo_bot/handlers.py b/photo_bot/handlers.py new file mode 100644 index 0000000..776617f --- /dev/null +++ b/photo_bot/handlers.py @@ -0,0 +1,242 @@ +# /opt/trueconf_bot/photo_bot/handlers.py +import os +import asyncio +import logging +import httpx +import smtplib +import time +from datetime import datetime +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from PIL import Image +from trueconf import Router, Message +from trueconf.types import FSInputFile + +import config.config as config +from utils import states +from utils.menu import MENU_TEXT +from photo_bot.photo_processor import prepare_ad_photo +from utils.texts import ( + EMOJI_DIGITS, + PHOTO_MAIN_MENU_TEXT, + PHOTO_UNKNOWN_CMD_TEXT, + photo_error_handling, + PHOTO_UPLOADING_TEXT, + photo_success_sent, + photo_cancel_text, + PHOTO_PROCESSING_TEXT, + photo_preview_ready, + photo_error_processing, +) + +# 🔌 Импортируем централизованную функцию сбора статистики из main +from utils.stats_logger import log_menu_stats + +logger = logging.getLogger(__name__) +router = Router() + +# Словарь для временного хранения путей к фото пользователей +pending_photos = {} + +# ========================================================= +# СИСТЕМА ОБРАБОТКИ ОШИБОК И ЛОГИРОВАНИЯ +# ========================================================= +EMAIL_COOLDOWN_SEC = 10.0 +last_email_time = 0.0 + +def log_and_notify_photo_error(user_id: str, action: str, error_details: str): + """Записывает системную ошибку в лог-файл и отправляет email-алерт администраторам""" + global last_email_time + + log_dir = os.path.dirname(os.path.abspath(__file__)) + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, "photo_errors.log") + + current_time = time.time() + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"[{timestamp}] User: {user_id} | Action: {action} | Error: {error_details}\n" + + with open(log_file, "a", encoding="utf-8") as f: + f.write(log_entry) + + if current_time - last_email_time < EMAIL_COOLDOWN_SEC: + with open(log_file, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] ALERT: Email suppressed due to rate limiting.\n") + return + + last_email_time = current_time + + try: + msg = MIMEMultipart() + msg['From'] = str(getattr(config, 'DEFAULT_EMAIL_FROM', 'bot@noreply.com')) + + to_emails = getattr(config, 'ALERTS_SUPPORT_EMAILS', 'admin@example.com') + msg['To'] = ", ".join(to_emails) if isinstance(to_emails, list) else str(to_emails) + + msg['Subject'] = f"TrueConf Bot Error: Фото Профиль - {action}" + + body = ( + f"Обнаружена системная ошибка в модуле загрузки фото профиля.\n\n" + f"Время: {timestamp}\n" + f"Пользователь (ID): {user_id}\n" + f"Действие: {action}\n\n" + f"Детали ошибки:\n{error_details}\n\n" + f"(Уведомления об ошибках ограничены: не чаще 1 раза в {EMAIL_COOLDOWN_SEC} сек.)" + ) + msg.attach(MIMEText(body, 'plain', 'utf-8')) + + smtp_server = getattr(config, 'SMTP_SERVER', 'localhost') + smtp_port = int(getattr(config, 'SMTP_PORT', 25)) + + with smtplib.SMTP(smtp_server, smtp_port) as server: + smtp_user = getattr(config, 'SMTP_USER', None) + smtp_pass = getattr(config, 'SMTP_PASSWORD', None) + if smtp_user and smtp_pass: + server.login(smtp_user, smtp_pass) + server.send_message(msg) + except Exception as e: + with open(log_file, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] СБОЙ ОТПРАВКИ EMAIL: {e}\n") + + +async def handle_photo_error(msg: Message, user_id: str, action: str, error_details: str): + """Вызывает системное логирование и выдает заглушку пользователю""" + await asyncio.to_thread(log_and_notify_photo_error, user_id, action, error_details) + + await msg.answer(photo_error_handling(), parse_mode="html") + + +# ========================================================= +# ОСНОВНОЙ ОБРАБОТЧИК +# ========================================================= +@router.message() +async def photo_module_handler(msg: Message): + user_id = msg.from_user.id + + if states.get_state(user_id) != "PHOTO_MODE": + return + + # Помечаем сообщение как обработанное + msg.handled = True + + user_login = user_id.split('@')[0] if '@' in user_id else user_id + msg_text = getattr(msg.content, "text", "").strip() if hasattr(msg.content, "text") else "" + is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" + + cmd = msg_text.lower() + + # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ + for raw_num, emoji_num in EMOJI_DIGITS.items(): + if cmd == emoji_num: + cmd = raw_num + break + + # --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню с очисткой кэша) --- + if cmd in ["0", "9", "/start", "меню"]: + log_menu_stats(user_id, "Photo Bot", "Выход в главное меню") + states.clear_state(user_id) + if user_id in pending_photos: + if os.path.exists(pending_photos[user_id]): + os.remove(pending_photos[user_id]) + del pending_photos[user_id] + + await msg.answer(MENU_TEXT, parse_mode="html") + return + + # --- ПОДТВЕРЖДЕНИЕ ОТПРАВКИ (Кнопка 1 или 2) --- + if not is_attachment and user_id in pending_photos: + if cmd == "1": + photo_path = pending_photos[user_id] + final_filename = f"{user_login}.bmp" + + await msg.answer(PHOTO_UPLOADING_TEXT, parse_mode="html") + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + with open(photo_path, "rb") as f: + files = {'file': (final_filename, f, 'image/bmp')} + response = await client.post(config.TRASH_API_URL, files=files) + + if response.status_code == 200: + # ФИКСАЦИЯ СТАТИСТИКИ: Успешная выгрузка фото на утверждение + log_menu_stats(user_id, "Photo Bot", "Отправка фото на утверждение") + await msg.answer(photo_success_sent(), parse_mode="html") + + states.clear_state(user_id) + if os.path.exists(photo_path): + os.remove(photo_path) + del pending_photos[user_id] + + await asyncio.sleep(0.5) + await msg.answer(MENU_TEXT, parse_mode="html") + else: + error_msg = f"API вернул статус-код {response.status_code}" + await handle_photo_error(msg, user_id, "Отправка фото по API", error_msg) + except Exception as e: + await handle_photo_error(msg, user_id, "Подключение к API загрузки", str(e)) + return + + # --- ОТМЕНА ФОТО (Кнопка 2) --- + elif cmd == "2": + log_menu_stats(user_id, "Photo Bot", "Отмена загруженного фото") + photo_path = pending_photos[user_id] + if os.path.exists(photo_path): + os.remove(photo_path) + del pending_photos[user_id] + await msg.answer(photo_cancel_text(), parse_mode="html") + return + + # --- ИСПРАВЛЕНО: Защита от мусорного ввода при висящем превью --- + else: + await msg.answer(PHOTO_UNKNOWN_CMD_TEXT, parse_mode="html") + return + + # --- ПРИЕМ НОВОГО ФОТО --- + if is_attachment: + file_id = getattr(msg.content, "file_id", None) + ready_path = f"/tmp/{user_login}.bmp" + raw_path = None + + await msg.answer(PHOTO_PROCESSING_TEXT, parse_mode="html") + + try: + downloaded_file = await states.tc_bot.download_file_by_id(file_id) + raw_path = downloaded_file + result = await asyncio.to_thread(prepare_ad_photo, raw_path, ready_path, 96) + + if result["success"]: + # ФИКСАЦИЯ СТАТИСТИКИ: Факт успешной загрузки и обработки нового фото + log_menu_stats(user_id, "Photo Bot", "Загрузка и конвертация нового фото") + pending_photos[user_id] = ready_path + preview_path = f"/tmp/prev_{user_login}.jpg" + try: + Image.open(ready_path).save(preview_path, format="JPEG") + p_main = FSInputFile(preview_path, filename="preview.jpg") + p_thumb = FSInputFile(preview_path, filename="preview_thumb.jpg") + await msg.answer_photo(p_main, p_thumb) + except Exception as lib_err: + await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err)) + finally: + if os.path.exists(preview_path): + os.remove(preview_path) + + await asyncio.sleep(0.5) + await msg.answer( + photo_preview_ready(), + parse_mode="html" + ) + else: + error_text = result.get('error', 'Не удалось обработать фото.') + await msg.answer( + photo_error_processing(error_text), + parse_mode="html" + ) + except Exception as e: + await handle_photo_error(msg, user_id, "Скачивание и обработка файла", str(e)) + finally: + if raw_path and os.path.exists(raw_path): + try: + os.remove(raw_path) + except Exception as cleanup_err: + await asyncio.to_thread(log_and_notify_photo_error, user_id, "Очистка временных файлов", str(cleanup_err)) + return diff --git a/photo_bot/photo_bot_log.txt b/photo_bot/photo_bot_log.txt new file mode 100644 index 0000000..9469dad --- /dev/null +++ b/photo_bot/photo_bot_log.txt @@ -0,0 +1,317 @@ +2026-04-23 23:39:51,315 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-23 23:39:51,930 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-23 23:39:51,969 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-23 23:39:51,970 [INFO] ⏳ Attempting WebSocket connection... +2026-04-23 23:39:52,055 [INFO] ✅ WebSocket connected +2026-04-23 23:39:52,055 [INFO] 🚀 Starting authorization +2026-04-23 23:39:52,055 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-23 23:39:52,129 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/43099233 +2026-04-23 23:40:09,861 [INFO] 📥 Incoming event: Message(timestamp=1776962409746, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=60, position='0'), content=TextContent(text='Hdd', parse_mode='html'), message_id='35d354ed-b6f2-41cc-b3a5-b7691342ccad', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:40:09,861 [INFO] [router:0x77d568978a90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:40:09,861 [ERROR] Handler async_wrapper failed: handle_photo() missing 1 required positional argument: 'bot' +Traceback (most recent call last): + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 133, in _run + await handler(event, **kwargs) + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 83, in async_wrapper + await func(evt, **filtered_kwargs) +TypeError: handle_photo() missing 1 required positional argument: 'bot' +2026-04-23 23:40:17,711 [INFO] 📥 Incoming event: Message(timestamp=1776962417624, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=61, position='0'), content=TextContent(text='Ddd', parse_mode='html'), message_id='a50e58bb-0924-4875-a6cc-5bcefcd34c1d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:40:17,711 [INFO] [router:0x77d568978a90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:40:17,712 [ERROR] Handler async_wrapper failed: handle_photo() missing 1 required positional argument: 'bot' +Traceback (most recent call last): + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 133, in _run + await handler(event, **kwargs) + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 83, in async_wrapper + await func(evt, **filtered_kwargs) +TypeError: handle_photo() missing 1 required positional argument: 'bot' +2026-04-23 23:41:19,825 [INFO] 📥 Incoming event: Message(timestamp=1776962477100, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=62, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-41-16_7720.webp', file_size=31428, file_id='b9002e074a4774d35ecf1f8cef29b8a737c4f1d2', mimetype='image/webp', ready_state=None), message_id='74faff33-a25d-4aa6-bcaa-cfc510c4ed97', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:41:19,825 [INFO] [router:0x77d568978a90] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:41:19,825 [ERROR] Handler async_wrapper failed: handle_photo() missing 1 required positional argument: 'bot' +Traceback (most recent call last): + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 133, in _run + await handler(event, **kwargs) + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 83, in async_wrapper + await func(evt, **filtered_kwargs) +TypeError: handle_photo() missing 1 required positional argument: 'bot' +2026-04-23 23:41:33,296 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-23 23:41:33,297 [INFO] 🛑 ChatBot stopped +2026-04-23 23:41:35,735 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-23 23:41:35,967 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-23 23:41:35,985 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-23 23:41:35,986 [INFO] ⏳ Attempting WebSocket connection... +2026-04-23 23:41:35,998 [INFO] ✅ WebSocket connected +2026-04-23 23:41:35,998 [INFO] 🚀 Starting authorization +2026-04-23 23:41:35,998 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-23 23:41:36,067 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/2ee10857 +2026-04-23 23:41:46,197 [INFO] 📥 Incoming event: Message(timestamp=1776962503542, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=63, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-41-43_2380.webp', file_size=33566, file_id='79e26db73915e1eddea88a79646ca9cbfa71c87c', mimetype='image/webp', ready_state=None), message_id='3246d0e7-5721-42e2-952f-d5891f56ccf2', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:41:46,197 [INFO] [router:0x7d0b32188940] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:41:46,197 [ERROR] Handler async_wrapper failed: handle_photo() missing 1 required positional argument: 'bot' +Traceback (most recent call last): + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 133, in _run + await handler(event, **kwargs) + File "/home/tpxk-03/bot_env/lib/python3.10/site-packages/trueconf/dispatcher/router.py", line 83, in async_wrapper + await func(evt, **filtered_kwargs) +TypeError: handle_photo() missing 1 required positional argument: 'bot' +2026-04-23 23:44:41,946 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-23 23:44:41,947 [INFO] 🛑 ChatBot stopped +2026-04-23 23:44:44,556 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-23 23:44:44,775 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-23 23:44:44,797 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-23 23:44:44,798 [INFO] ⏳ Attempting WebSocket connection... +2026-04-23 23:44:44,816 [INFO] ✅ WebSocket connected +2026-04-23 23:44:44,817 [INFO] 🚀 Starting authorization +2026-04-23 23:44:44,817 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-23 23:44:44,893 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/ca26821d +2026-04-23 23:45:03,929 [INFO] 📥 Incoming event: Message(timestamp=1776962701339, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=64, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-45-01_0330.webp', file_size=34734, file_id='b7a33d9f17f2fd8c20e2f89ada14bb543ccd80a3', mimetype='image/webp', ready_state=None), message_id='a3c09710-24fc-4460-987b-7e747542fb89', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:45:03,929 [INFO] [router:0x7e0f72d68b20] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:45:03,929 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:45:04,992 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/b7a33d9f17f2fd8c20e2f89ada14bb543ccd80a3 "HTTP/1.1 200 OK" +2026-04-23 23:45:04,995 [ERROR] Ошибка при работе с фото для ds.krivochenko: a bytes-like object is required, not 'PosixPath' +2026-04-23 23:45:04,995 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:46:02,036 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-23 23:46:02,037 [INFO] 🛑 ChatBot stopped +2026-04-23 23:47:20,892 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-23 23:47:21,125 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-23 23:47:21,142 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-23 23:47:21,143 [INFO] ⏳ Attempting WebSocket connection... +2026-04-23 23:47:21,158 [INFO] ✅ WebSocket connected +2026-04-23 23:47:21,158 [INFO] 🚀 Starting authorization +2026-04-23 23:47:21,158 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-23 23:47:21,233 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/c35e07f3 +2026-04-23 23:47:33,755 [INFO] 📥 Incoming event: Message(timestamp=1776962851136, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=67, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-47-30_8460.webp', file_size=37408, file_id='5d8e96f6752a2affd774703ef203b78ff982a9fd', mimetype='image/webp', ready_state=None), message_id='784b7f5a-5d32-4eb6-8656-b43016d5c5a7', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:47:33,755 [INFO] [router:0x7fb664b70af0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:47:33,756 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:47:34,992 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/5d8e96f6752a2affd774703ef203b78ff982a9fd "HTTP/1.1 200 OK" +2026-04-23 23:47:35,114 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:47:58,711 [INFO] 📥 Incoming event: Message(timestamp=1776962875889, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=70, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-47-55_6050.webp', file_size=33936, file_id='dd85f0b2bc7245d872c19dfa8dbc333772e76c6e', mimetype='image/webp', ready_state=None), message_id='4251845f-f133-4ea0-9165-d10e7ef3707e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:47:58,711 [INFO] [router:0x7fb664b70af0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:47:58,712 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:47:59,997 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/dd85f0b2bc7245d872c19dfa8dbc333772e76c6e "HTTP/1.1 200 OK" +2026-04-23 23:48:00,316 [ERROR] Ошибка при работе с фото для ds.krivochenko: Message.answer_document() got an unexpected keyword argument 'document' +2026-04-23 23:48:00,316 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:50:34,153 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-23 23:50:34,153 [INFO] 🛑 ChatBot stopped +2026-04-23 23:50:37,003 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-23 23:50:37,225 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-23 23:50:37,245 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-23 23:50:37,247 [INFO] ⏳ Attempting WebSocket connection... +2026-04-23 23:50:37,263 [INFO] ✅ WebSocket connected +2026-04-23 23:50:37,263 [INFO] 🚀 Starting authorization +2026-04-23 23:50:37,263 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-23 23:50:37,338 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/df72df35 +2026-04-23 23:50:52,450 [INFO] 📥 Incoming event: Message(timestamp=1776963049877, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=73, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-50-49_5490.webp', file_size=55568, file_id='2225e5a353a97ca067863db453377cbe8163ebea', mimetype='image/webp', ready_state=None), message_id='faec2f35-2dd3-40d2-bdd1-8f7954873cf2', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:50:52,451 [INFO] [router:0x73c06af80b50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:50:52,451 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:50:53,531 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/2225e5a353a97ca067863db453377cbe8163ebea "HTTP/1.1 200 OK" +2026-04-23 23:50:53,683 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:51:09,117 [INFO] 📥 Incoming event: Message(timestamp=1776963066361, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=76, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-51-06_0440.webp', file_size=62052, file_id='72dbcf140bd407daf3dc8e564658615fc9aa00dd', mimetype='image/webp', ready_state=None), message_id='81b66791-daa2-447c-ab98-187d243c86a3', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:51:09,117 [INFO] [router:0x73c06af80b50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:51:09,117 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:51:10,538 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/72dbcf140bd407daf3dc8e564658615fc9aa00dd "HTTP/1.1 200 OK" +2026-04-23 23:51:10,774 [INFO] ✉️ Sending file to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:51:10,959 [INFO] Успешно обработано фото для ds.krivochenko +2026-04-23 23:51:37,361 [INFO] 📥 Incoming event: Message(timestamp=1776963094641, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=80, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-51-34_3470.webp', file_size=51088, file_id='363fe9a06d9962d9a08b67639f601de3f705ed74', mimetype='image/webp', ready_state=None), message_id='2f50a004-1d03-4b3d-babd-21b20ffbcbab', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:51:37,361 [INFO] [router:0x73c06af80b50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:51:37,361 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:51:38,541 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/363fe9a06d9962d9a08b67639f601de3f705ed74 "HTTP/1.1 200 OK" +2026-04-23 23:51:38,668 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:51:59,710 [INFO] 📥 Incoming event: Message(timestamp=1776963116930, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=83, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-51-56_6400.webp', file_size=51134, file_id='0aca112ece96137cf7ccef3a1f2aa0e2e7ac6716', mimetype='image/webp', ready_state=None), message_id='ee34d07b-c738-4953-851e-b2bb462f7188', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:51:59,711 [INFO] [router:0x73c06af80b50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:51:59,712 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:52:00,543 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/0aca112ece96137cf7ccef3a1f2aa0e2e7ac6716 "HTTP/1.1 200 OK" +2026-04-23 23:52:00,673 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:52:11,373 [INFO] 📥 Incoming event: Message(timestamp=1776963131192, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=86, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-52-10_9050.webp', file_size=70874, file_id='55b8822f54d91c9de4fc826a510c537befd4ebd5', mimetype='image/webp', ready_state=None), message_id='35ee4f25-d715-48ac-840d-cfbe5b27530d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:52:11,373 [INFO] [router:0x73c06af80b50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:52:11,374 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:52:12,543 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/55b8822f54d91c9de4fc826a510c537befd4ebd5 "HTTP/1.1 200 OK" +2026-04-23 23:52:12,660 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:52:26,048 [INFO] 📥 Incoming event: Message(timestamp=1776963143388, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=89, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-52-23_0840.webp', file_size=61968, file_id='31146c0fcf22afd27c0f6abe91962b08efeefd7e', mimetype='image/webp', ready_state=None), message_id='295a95ae-0118-4cd0-9a34-7e00c0d69495', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:52:26,048 [INFO] [router:0x73c06af80b50] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:52:26,049 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:52:27,547 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/31146c0fcf22afd27c0f6abe91962b08efeefd7e "HTTP/1.1 200 OK" +2026-04-23 23:52:27,677 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:54:49,657 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-23 23:54:49,657 [INFO] 🛑 ChatBot stopped +2026-04-23 23:54:51,948 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-23 23:54:52,321 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-23 23:54:52,346 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-23 23:54:52,347 [INFO] ⏳ Attempting WebSocket connection... +2026-04-23 23:54:52,364 [INFO] ✅ WebSocket connected +2026-04-23 23:54:52,364 [INFO] 🚀 Starting authorization +2026-04-23 23:54:52,364 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-23 23:54:52,433 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/60d64d63 +2026-04-23 23:55:04,223 [INFO] 📥 Incoming event: Message(timestamp=1776963301650, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=92, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-55-01_3280.webp', file_size=48884, file_id='ec564bb845d9ead8144a632a008ca361143ef326', mimetype='image/webp', ready_state=None), message_id='27865386-09b3-4d0d-bffa-7a077b831da9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:55:04,224 [INFO] [router:0x775ecb984ca0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:55:04,224 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:55:05,197 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/ec564bb845d9ead8144a632a008ca361143ef326 "HTTP/1.1 200 OK" +2026-04-23 23:55:05,405 [INFO] ✉️ Sending file to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:55:05,544 [INFO] Успешно обработано фото для ds.krivochenko +2026-04-23 23:56:02,536 [INFO] 📥 Incoming event: Message(timestamp=1776963359671, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=96, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-55-59_3880.webp', file_size=41150, file_id='3105ea960dbd509a3fd08ef9f09477053b31f6a5', mimetype='image/webp', ready_state=None), message_id='6dcfa1a4-452f-4999-b2e0-3385c04c754c', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:56:02,536 [INFO] [router:0x775ecb984ca0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:56:02,536 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:56:03,985 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/3105ea960dbd509a3fd08ef9f09477053b31f6a5 "HTTP/1.1 200 OK" +2026-04-23 23:56:04,090 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:56:19,904 [INFO] 📥 Incoming event: Message(timestamp=1776963377210, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=99, position='0'), content=AttachmentContent(file_name='image_2026.04.23_23-56-16_9780.webp', file_size=30108, file_id='dbd42aa40a3491e5c496c906722887cccb99b47d', mimetype='image/webp', ready_state=None), message_id='37dd0e1b-e000-4de3-a160-3d4dc3c11743', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-23 23:56:19,904 [INFO] [router:0x775ecb984ca0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-23 23:56:19,904 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:56:20,959 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/dbd42aa40a3491e5c496c906722887cccb99b47d "HTTP/1.1 200 OK" +2026-04-23 23:56:21,119 [INFO] ✉️ Sending file to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-23 23:56:21,254 [INFO] Успешно обработано фото для ds.krivochenko +2026-04-24 00:02:01,048 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-24 00:02:01,049 [INFO] 🛑 ChatBot stopped +2026-04-24 00:02:03,574 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-24 00:02:03,963 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-24 00:02:03,981 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-24 00:02:03,982 [INFO] ⏳ Attempting WebSocket connection... +2026-04-24 00:02:03,996 [INFO] ✅ WebSocket connected +2026-04-24 00:02:03,996 [INFO] 🚀 Starting authorization +2026-04-24 00:02:03,996 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-24 00:02:04,069 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/3aca404a +2026-04-24 00:02:18,536 [INFO] 📥 Incoming event: Message(timestamp=1776963735846, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=103, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-02-15_6030.webp', file_size=34002, file_id='75a04b613653999c91814222650aa3afad25acbd', mimetype='image/webp', ready_state=None), message_id='fbc6a5ad-14f0-45e3-974a-2c46d75beb6f', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:02:18,536 [INFO] [router:0x7099be182050] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:02:18,537 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:02:20,023 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/75a04b613653999c91814222650aa3afad25acbd "HTTP/1.1 200 OK" +2026-04-24 00:02:20,164 [INFO] ✉️ Sending file to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:02:20,293 [INFO] Успешно обработано фото для ds.krivochenko +2026-04-24 00:03:17,433 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-24 00:03:17,433 [INFO] 🛑 ChatBot stopped +2026-04-24 00:03:19,011 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-24 00:03:19,279 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-24 00:03:19,302 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-24 00:03:19,303 [INFO] ⏳ Attempting WebSocket connection... +2026-04-24 00:03:19,322 [INFO] ✅ WebSocket connected +2026-04-24 00:03:19,322 [INFO] 🚀 Starting authorization +2026-04-24 00:03:19,322 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-24 00:03:19,393 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/7686cbd0 +2026-04-24 00:03:30,444 [INFO] 📥 Incoming event: Message(timestamp=1776963807702, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=107, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-03-27_4020.webp', file_size=32534, file_id='46dea905d43af0fd4a0aeb939cb52a0d54a41eb1', mimetype='image/webp', ready_state=None), message_id='a5bab295-8a17-401c-8782-45dd3cfa07e9', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:03:30,444 [INFO] [router:0x7f763ab7dea0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:03:30,444 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:03:30,973 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/46dea905d43af0fd4a0aeb939cb52a0d54a41eb1 "HTTP/1.1 200 OK" +2026-04-24 00:03:31,141 [INFO] ✉️ Sending file to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:03:31,290 [INFO] Успешно обработано фото для ds.krivochenko +2026-04-24 00:04:57,657 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-24 00:04:57,657 [INFO] 🛑 ChatBot stopped +2026-04-24 00:04:59,521 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-24 00:04:59,897 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-24 00:04:59,920 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-24 00:04:59,921 [INFO] ⏳ Attempting WebSocket connection... +2026-04-24 00:04:59,938 [INFO] ✅ WebSocket connected +2026-04-24 00:04:59,938 [INFO] 🚀 Starting authorization +2026-04-24 00:04:59,938 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-24 00:05:00,020 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/dc28dd50 +2026-04-24 00:05:12,095 [INFO] 📥 Incoming event: Message(timestamp=1776963909484, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=111, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-05-09_1850.webp', file_size=36734, file_id='137bb1dc861f951934607e84df8d2e3067b0e81a', mimetype='image/webp', ready_state=None), message_id='4fad207a-1a3a-4f5e-8ce6-ada8460e856e', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:05:12,096 [INFO] [router:0x7bc5f7589de0] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:05:12,096 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:05:13,600 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/137bb1dc861f951934607e84df8d2e3067b0e81a "HTTP/1.1 200 OK" +2026-04-24 00:05:13,758 [ERROR] Ошибка при работе с фото для ds.krivochenko: Message.answer_photo() missing 1 required positional argument: 'preview' +2026-04-24 00:05:13,758 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:06:47,892 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-24 00:06:47,892 [INFO] 🛑 ChatBot stopped +2026-04-24 00:06:50,231 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-24 00:06:50,615 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-24 00:06:50,634 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-24 00:06:50,635 [INFO] ⏳ Attempting WebSocket connection... +2026-04-24 00:06:50,648 [INFO] ✅ WebSocket connected +2026-04-24 00:06:50,648 [INFO] 🚀 Starting authorization +2026-04-24 00:06:50,649 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-24 00:06:50,723 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/89b2d6f9 +2026-04-24 00:06:54,616 [INFO] 📥 Incoming event: Message(timestamp=1776964014581, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=114, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-06-54_0770.webp', file_size=36734, file_id='6b6d137bb96d86f9b064372b1d545ae5ca580e08', mimetype='image/webp', ready_state=None), message_id='45208bf9-4942-49ab-9db2-ac130fd48adf', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:06:54,616 [INFO] [router:0x757f5e571e70] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:06:54,616 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:06:56,676 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/6b6d137bb96d86f9b064372b1d545ae5ca580e08 "HTTP/1.1 200 OK" +2026-04-24 00:06:56,837 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:06:56,990 [INFO] Успешно обработано фото для ds.krivochenko +2026-04-24 00:10:28,416 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-24 00:10:28,416 [INFO] 🛑 ChatBot stopped +2026-04-24 00:21:37,851 [INFO] Запуск фото-бота на сервере tcs.sibcem.ru... +2026-04-24 00:21:38,237 [INFO] HTTP Request: POST https://tcs.sibcem.ru/bridge/api/client/v1/oauth/token "HTTP/1.1 201 Created" +2026-04-24 00:21:38,257 [INFO] HTTP Request: GET https://tcs.sibcem.ru/api/v4/server "HTTP/1.1 200 OK" +2026-04-24 00:21:38,258 [INFO] ⏳ Attempting WebSocket connection... +2026-04-24 00:21:38,269 [INFO] ✅ WebSocket connected +2026-04-24 00:21:38,270 [INFO] 🚀 Starting authorization +2026-04-24 00:21:38,270 [INFO] 🛠 Created AuthMethod with id=1 +2026-04-24 00:21:38,348 [INFO] 🔐 Authenticated as bot_tcs@tcs.sibcem.ru/1c46032d +2026-04-24 00:24:04,723 [INFO] 📥 Incoming event: Message(timestamp=1776965041929, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=118, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-24-01_6060.webp', file_size=28260, file_id='fc945836ef4f03ab001a2b8380ffe6743bfd7d71', mimetype='image/webp', ready_state=None), message_id='47af2156-718c-4b38-8c42-aeb26c002cf6', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:24:04,724 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:24:04,725 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:24:06,125 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/fc945836ef4f03ab001a2b8380ffe6743bfd7d71 "HTTP/1.1 200 OK" +2026-04-24 00:24:06,301 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:24:06,455 [INFO] Фото подготовлено для ds.krivochenko, ждем подтверждения. +2026-04-24 00:24:32,525 [INFO] 📥 Incoming event: Message(timestamp=1776965072292, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=122, position='0'), content=TextContent(text='Нет', parse_mode='html'), message_id='af22acbc-5b9f-4f03-aaab-8b537733f964', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:24:32,525 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:24:32,525 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:25:14,683 [INFO] 📥 Incoming event: Message(timestamp=1776965111899, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=124, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-25-11_5690.webp', file_size=25826, file_id='945182624c6424558092caf588aac83a071656b4', mimetype='image/webp', ready_state=None), message_id='40c604e8-d252-4227-92aa-cfe807e3ad10', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:25:14,683 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:25:14,683 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:25:16,148 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/945182624c6424558092caf588aac83a071656b4 "HTTP/1.1 200 OK" +2026-04-24 00:25:16,258 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:25:16,411 [INFO] Фото подготовлено для ds.krivochenko, ждем подтверждения. +2026-04-24 00:25:30,911 [INFO] 📥 Incoming event: Message(timestamp=1776965130661, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=128, position='0'), content=TextContent(text='Да', parse_mode='html'), message_id='5cc37cc7-74d9-46c2-98f7-712ab19e6fad', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:25:30,911 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:25:30,911 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:25:31,111 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:27:38,891 [INFO] 📥 Incoming event: Message(timestamp=1776965256125, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=131, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-27-35_8290.webp', file_size=39604, file_id='71c02d7294899f9b801d804dd5d2d8085f4ed300', mimetype='image/webp', ready_state=None), message_id='ebf9fab0-6c23-4231-8383-a628ff22831a', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:27:38,891 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:27:38,891 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:27:40,704 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/71c02d7294899f9b801d804dd5d2d8085f4ed300 "HTTP/1.1 200 OK" +2026-04-24 00:27:40,849 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:27:41,004 [INFO] Фото подготовлено для ds.krivochenko, ждем подтверждения. +2026-04-24 00:28:35,890 [INFO] 📥 Incoming event: Message(timestamp=1776965313221, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=135, position='0'), content=AttachmentContent(file_name='image_2026.04.24_00-28-32_9170.webp', file_size=30814, file_id='a50aaded753fd1d8a8cd7be18b53d19f8ff81f9a', mimetype='image/webp', ready_state=None), message_id='ac4db733-0418-4f59-8483-03180350d633', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:28:35,890 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:28:35,890 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:28:37,510 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/a50aaded753fd1d8a8cd7be18b53d19f8ff81f9a "HTTP/1.1 200 OK" +2026-04-24 00:28:37,662 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:28:37,814 [INFO] Фото подготовлено для ds.krivochenko, ждем подтверждения. +2026-04-24 00:29:02,445 [INFO] 📥 Incoming event: Message(timestamp=1776965342360, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=139, position='0'), content=TextContent(text='Да', parse_mode='html'), message_id='0a3128bc-ed28-48ac-acc1-dc8eabadfbf1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 00:29:02,445 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 00:29:02,445 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 00:29:02,672 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:08:28,256 [INFO] 📥 Incoming event: Message(timestamp=1776967707857, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=142, position='0'), content=AttachmentContent(file_name='image_2026.04.24_01-08-27_7210.webp', file_size=70874, file_id='3b9c34d0a4d5159302bbd2e664bbebc74bf7b1f8', mimetype='image/webp', ready_state=None), message_id='0b126ae1-9feb-4339-bee3-f9929a76da03', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 01:08:28,281 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 01:08:28,300 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:08:30,338 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/3b9c34d0a4d5159302bbd2e664bbebc74bf7b1f8 "HTTP/1.1 200 OK" +2026-04-24 01:08:30,613 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:08:43,132 [INFO] 📥 Incoming event: Message(timestamp=1776967722748, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=145, position='0'), content=AttachmentContent(file_name='image_2026.04.24_01-08-42_5490.webp', file_size=44960, file_id='192fb72c99e818b5a3cc8e096d057716aa0a8fee', mimetype='image/webp', ready_state=None), message_id='027b3201-9cda-496f-9569-c047ad05dc3d', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 01:08:43,132 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 01:08:43,132 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:08:44,085 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/192fb72c99e818b5a3cc8e096d057716aa0a8fee "HTTP/1.1 200 OK" +2026-04-24 01:08:44,329 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:08:55,062 [INFO] 📥 Incoming event: Message(timestamp=1776967734742, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=148, position='0'), content=AttachmentContent(file_name='image_2026.04.24_01-08-54_6370.webp', file_size=41150, file_id='cd7c52d43bb145cab9db9e5ece908c18a638e885', mimetype='image/webp', ready_state=None), message_id='dbbf3cab-aa5d-445d-ac4f-33e783bc4f20', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 01:08:55,063 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 01:08:55,063 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:08:56,594 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/cd7c52d43bb145cab9db9e5ece908c18a638e885 "HTTP/1.1 200 OK" +2026-04-24 01:08:56,751 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:09:06,471 [INFO] 📥 Incoming event: Message(timestamp=1776967746177, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=151, position='0'), content=AttachmentContent(file_name='image_2026.04.24_01-09-05_9600.webp', file_size=51088, file_id='64c8e4f93958d31c7d968f31187a57242c8c75a1', mimetype='image/webp', ready_state=None), message_id='eec9d2de-01d5-469e-8805-7a4ae22b3c59', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 01:09:06,471 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 01:09:06,471 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 01:09:08,453 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/64c8e4f93958d31c7d968f31187a57242c8c75a1 "HTTP/1.1 200 OK" +2026-04-24 01:09:08,592 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:08:20,569 [INFO] 📥 Incoming event: Message(timestamp=1777007297696, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=154, position='0'), content=AttachmentContent(file_name='image_2026.04.24_12-08-17_3930.webp', file_size=41550, file_id='ab6dd76dbccf29443043f9ad4a2259cdf9d88139', mimetype='image/webp', ready_state=None), message_id='146d6162-8b7d-4e2b-8d1c-cb79c859ff25', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 12:08:20,598 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 12:08:20,617 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:08:22,738 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/ab6dd76dbccf29443043f9ad4a2259cdf9d88139 "HTTP/1.1 200 OK" +2026-04-24 12:08:23,151 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:08:39,495 [INFO] 📥 Incoming event: Message(timestamp=1777007316582, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=157, position='0'), content=AttachmentContent(file_name='image_2026.04.24_12-08-36_2420.webp', file_size=46970, file_id='6828a553f6cdbc5d0dfa224da4aef9948d3309aa', mimetype='image/webp', ready_state=None), message_id='832d86ea-82ec-4851-9507-818ad67e44d1', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 12:08:39,495 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 12:08:39,495 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:08:41,467 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/6828a553f6cdbc5d0dfa224da4aef9948d3309aa "HTTP/1.1 200 OK" +2026-04-24 12:08:41,719 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:08:42,077 [INFO] Фото подготовлено для ds.krivochenko, ждем подтверждения. +2026-04-24 12:09:11,132 [INFO] 📥 Incoming event: Message(timestamp=1777007351000, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=161, position='0'), content=AttachmentContent(file_name='image_2026.04.24_12-09-10_5280.webp', file_size=50966, file_id='8b475f2a95701d32f2ca01cdc72a31ba1a775946', mimetype='image/webp', ready_state=None), message_id='f256a6b8-176c-4b5c-a6f0-9e25e856b829', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 12:09:11,132 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 12:09:11,132 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:09:13,945 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/8b475f2a95701d32f2ca01cdc72a31ba1a775946 "HTTP/1.1 200 OK" +2026-04-24 12:09:14,301 [INFO] ✉️ Sending photo to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:09:14,440 [INFO] Фото подготовлено для ds.krivochenko, ждем подтверждения. +2026-04-24 12:09:29,899 [INFO] 📥 Incoming event: Message(timestamp=1777007369786, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=165, position='0'), content=AttachmentContent(file_name='image_2026.04.24_12-09-29_3140.webp', file_size=54302, file_id='9399f7914e37463ff42adc2647b90aaf4644e47d', mimetype='image/webp', ready_state=None), message_id='d650aa68-fd7a-414a-ad23-7d2d58d9ea5b', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 12:09:29,899 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 12:09:29,899 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:09:31,446 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/9399f7914e37463ff42adc2647b90aaf4644e47d "HTTP/1.1 200 OK" +2026-04-24 12:09:31,543 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:09:45,818 [INFO] 📥 Incoming event: Message(timestamp=1777007385689, type=, author=EnvelopeAuthor(id='ds.krivochenko@tcs.sibcem.ru', type=), box=EnvelopeBox(id=168, position='0'), content=AttachmentContent(file_name='image_2026.04.24_12-09-45_2090.webp', file_size=43322, file_id='225a71a2869ae1ca8f7d5a544f3eaec4b9f50587', mimetype='image/webp', ready_state=None), message_id='717652b9-de3a-456d-ae14-b6bd6f40a450', chat_id='744bad93e58c4c498e15468562c6a2da3b610e9c', is_edited=False, reply_message_id=None) +2026-04-24 12:09:45,818 [INFO] [router:0x757ad5372650] matched handler=async_wrapper filters=[InstanceOfFilter] +2026-04-24 12:09:45,818 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-24 12:09:47,467 [INFO] HTTP Request: GET https://tcs.sibcem.ru/bridge/api/client/v1/files/225a71a2869ae1ca8f7d5a544f3eaec4b9f50587 "HTTP/1.1 200 OK" +2026-04-24 12:09:47,569 [INFO] ✉️ Sending message to 744bad93e58c4c498e15468562c6a2da3b610e9c +2026-04-26 19:36:24,516 [INFO] 🛑 Connect loop cancelled, performing cleanup. +2026-04-26 19:36:24,549 [INFO] 🛑 ChatBot stopped diff --git a/photo_bot/photo_processor.py b/photo_bot/photo_processor.py new file mode 100644 index 0000000..a6f292d --- /dev/null +++ b/photo_bot/photo_processor.py @@ -0,0 +1,127 @@ +import io +import cv2 +import os +import logging +import numpy as np +from PIL import Image, ImageFilter, ImageEnhance + +logger = logging.getLogger(__name__) + +def prepare_ad_photo(input_path: str, output_path: str, target_size: int = 96) -> dict: + try: + # 1. Загружаем изображение для OpenCV + if isinstance(input_path, bytes): + img = cv2.imdecode(np.frombuffer(input_path, np.uint8), cv2.IMREAD_COLOR) + else: + img = cv2.imread(input_path) + if img is None: + return {"success": False, "error": "Не удалось прочитать файл изображения."} + if isinstance(input_path, bytes): + img = cv2.imdecode(np.frombuffer(input_path, np.uint8), cv2.IMREAD_COLOR) + else: + img = cv2.imread(input_path) + if img is None: + return {"success": False, "error": "Не удалось прочитать файл изображения."} + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # 2. Загружаем каскад Хаара для поиска лица + cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' + face_cascade = cv2.CascadeClassifier(cascade_path) + + # 3. Ищем лица + # Оставляем оптимальное значение minNeighbors=8 для баланса + faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=8, minSize=(50, 50)) + + if len(faces) == 0: + return {"success": False, "error": "Лицо не найдено! Пожалуйста, сделайте фото анфас при хорошем освещении."} + + if len(faces) > 1: + faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True) + + x, y, w, h = faces[0] + + # ========================================================= + # АБСОЛЮТНАЯ ЗАЩИТА ОТ ГРАФИКИ (НОРМАЛИЗОВАННАЯ МАТЕМАТИКА) + # ========================================================= + # 1. Вырезаем центральную зону лица + face_roi_original = gray[y + int(h*0.2):y + int(h*0.8), x + int(w*0.2):x + int(w*0.8)] + + # 2. Сжимаем до 100x100 пикселей. + # Это уравнивает 4K-фотографии и мелкие картинки, делая метрики стабильными. + face_roi = cv2.resize(face_roi_original, (100, 100)) + + std_dev = np.std(face_roi) + laplacian_var = cv2.Laplacian(face_roi, cv2.CV_64F).var() + unique_colors = len(np.unique(face_roi)) + + logger.info(f"Анализ нормализованного лица: std_dev={std_dev:.2f}, laplacian={laplacian_var:.2f}, colors={unique_colors}") + + # ПРОВЕРКА 1: Плоские, замыленные картинки + if std_dev < 10.0 or laplacian_var < 30.0: + logger.warning(f"Заблокирован плоский рисунок/замыливание: std={std_dev:.1f}, lap={laplacian_var:.1f}") + return {"success": False, "error": "Изображение слишком размыто или имеет плоскую текстуру. Отправьте реальное фото."} + + # ПРОВЕРКА 2: Векторы, комиксы и жесткий 2D-арт (Умный порог резкости + цвета) + # У реального человека на хорошую камеру laplacian может доходить до 500-600, + # но при этом всегда будет богатая палитра цветов (шум матрицы). + # У мультиков высокая резкость всегда сопровождается бедной палитрой. + if laplacian_var > 850.0 or (laplacian_var > 400.0 and unique_colors < 150): + logger.warning(f"Заблокирован резкий 2D-арт: lap={laplacian_var:.1f}, colors={unique_colors}") + return {"success": False, "error": "Изображение похоже на графический арт или сильно отфильтровано. Пожалуйста, загрузите естественное фото."} + + # ПРОВЕРКА 3: 3D-рендеры и нейросети (Мало шума) + # На реальной фотографии (даже с хорошим светом) шум матрицы телефона + # даст минимум 110-120 уникальных оттенков на 10000 пикселей. + if unique_colors < 105: + logger.warning(f"Заблокирован 3D-рендер по палитре: unique_colors={unique_colors}") + return {"success": False, "error": "Изображение распознано как 3D-модель или рисунок (неестественная палитра). Ждем реальное фото!"} + # ========================================================= + + # 4. Кадрируем (делаем квадрат с отступами вокруг лица) + margin = int(w * 0.3) + y1 = max(0, y - int(margin * 1.5)) + y2 = min(img.shape[0], y + h + margin) + x1 = max(0, x - margin) + x2 = min(img.shape[1], x + w + margin) + + side = min(x2 - x1, y2 - y1) + center_x, center_y = (x1 + x2) // 2, (y1 + y2) // 2 + crop_x1 = max(0, center_x - side // 2) + crop_y1 = max(0, center_y - side // 2) + crop_x2 = crop_x1 + side + crop_y2 = crop_y1 + side + + # 5. Обработка через Pillow + pil_img = Image.open(io.BytesIO(input_path)) + + # ВАЖНО: Убираем альфа-канал (прозрачность), иначе BMP выдаст ошибку + if pil_img.mode in ('RGBA', 'P', 'LA'): + background = Image.new('RGB', pil_img.size, (255, 255, 255)) + background.paste(pil_img, mask=pil_img.split()[3] if pil_img.mode == 'RGBA' else None) + pil_img = background + elif pil_img.mode != 'RGB': + pil_img = pil_img.convert('RGB') + + cropped_img = pil_img.crop((crop_x1, crop_y1, crop_x2, crop_y2)) + + # Сжимаем до нужного размера (96x96) + final_img = cropped_img.resize((target_size, target_size), Image.Resampling.LANCZOS) + + # Добавляем немного резкости, чтобы лицо не мылилось при 96px + final_img = final_img.filter(ImageFilter.UnsharpMask(radius=1.5, percent=100, threshold=3)) + + # 6. Сохраняем в формате BMP + final_img.save(output_path, format="BMP") + + # 7. Проверяем финальный вес + size_kb = os.path.getsize(output_path) / 1024 + + if size_kb > 99: + return {"success": False, "error": f"Сбой: размер файла ({size_kb:.1f} KB) превысил лимит в 99 KB."} + + return {"success": True, "size_kb": round(size_kb, 1)} + + except Exception as e: + logger.error(f"Ошибка в photo_processor: {e}") + return {"success": False, "error": str(e)} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..738fe7e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,160 @@ +aiofiles==25.1.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.13.5 +aiosignal==1.4.0 +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.13.0 +async-property==0.2.2 +async-timeout==4.0.3 +attrs==26.1.0 +autocommand==2.2.2 +backports.tarfile==1.2.0 +blinker==1.9.0 +brotli==1.2.0 +certifi==2026.2.25 +cffi==2.0.0 +charset-normalizer==3.4.6 +click==8.3.1 +cryptography==46.0.7 +cssselect2==0.9.0 +dataclasses-json==0.6.7 +diskcache==5.6.3 +distro==1.9.0 +docx2txt==0.9 +exceptiongroup==1.3.1 +faiss-cpu==1.9.0.post1 +filelock==3.29.0 +filetype==1.2.0 +flask==3.1.3 +fonttools==4.63.0 +frozenlist==1.8.0 +fsspec==2026.3.0 +greenlet==3.2.5 +grpcio==1.80.0 +grpcio-tools==1.80.0 +h11==0.16.0 +h2==4.3.0 +hf-xet==1.4.3 +hpack==4.1.0 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.3 +huggingface-hub==0.36.2 +hyperframe==6.1.0 +idna==3.11 +importlib-metadata==8.0.0 +inflect==7.3.1 +itsdangerous==2.2.0 +jaraco.collections==5.1.0 +jaraco.context==5.3.0 +jaraco.functools==4.0.1 +jaraco.text==3.12.1 +jinja2==3.1.6 +jiter==0.14.0 +joblib==1.5.3 +jsonpatch==1.33 +jsonpointer==3.1.1 +langchain==1.2.17 +langchain-classic==1.0.7 +langchain-community==0.4.1 +langchain-core==1.3.3 +langchain-openai==1.2.1 +langchain-protocol==0.0.15 +langchain-qdrant==1.1.0 +langchain-text-splitters==1.1.2 +langgraph==1.1.10 +langgraph-checkpoint==4.0.3 +langgraph-prebuilt==1.0.13 +langgraph-sdk==0.3.14 +langsmith==0.8.3 +ldap3==2.9 +llama-cpp-python==0.3.30 +loguru==0.7.3 +lxml==6.1.0 +magic-filter==1.0.12 +markdown-it-py==4.0.0 +markupsafe==3.0.3 +marshmallow==3.26.2 +mashumaro==3.20 +mdurl==0.1.2 +more-itertools==10.3.0 +mpmath==1.3.0 +multidict==6.7.1 +mypy-extensions==1.1.0 +networkx==3.4.2 +numpy==1.26.4 +openai==2.36.0 +opencv-python-headless==4.11.0.86 +orjson==3.11.9 +ormsgpack==1.12.2 +packaging==26.2 +passwork==0.1.6 +passwork-python==0.2.0 +pbkdf2==1.3 +pillow==12.2.0 +platformdirs==4.2.2 +portalocker==3.2.0 +propcache==0.4.1 +protobuf==6.33.6 +psutil==7.2.2 +pyasn1==0.6.2 +pycparser==3.0 +pydantic==2.13.4 +pydantic-core==2.46.4 +pydantic-settings==2.14.0 +pydyf==0.12.1 +pygments==2.20.0 +pymssql==2.3.13 +pymupdf==1.26.0 +pymupdf4llm==0.0.24 +pyodbc==5.3.0 +pyphen==0.17.2 +python-docx==1.2.0 +python-dotenv==1.2.2 +python-magic==0.4.27 +python-trueconf-bot==1.4.1 +pyyaml==6.0.3 +qdrant-client==1.17.1 +rank-bm25==0.2.2 +regex==2026.4.4 +requests==2.32.5 +requests-toolbelt==1.0.0 +rich==15.0.0 +safetensors==0.7.0 +scikit-learn==1.7.2 +scipy==1.15.3 +sentence-transformers==2.6.1 +setuptools==79.0.1 +shellingham==1.5.4 +sniffio==1.3.1 +sqlalchemy==2.0.49 +sympy==1.13.1 +tenacity==9.1.4 +threadpoolctl==3.6.0 +tiktoken==0.11.0 +tinycss2==1.5.1 +tinyhtml5==2.1.0 +tokenizers==0.15.2 +tomli==2.0.1 +torch==2.2.2+cpu +tqdm==4.67.3 +transformers==4.39.3 +truststore==0.10.4 +typeguard==4.3.0 +typer==0.25.0 +typing-extensions==4.15.0 +typing-inspect==0.9.0 +typing-inspection==0.4.2 +urllib3==1.26.20 +uuid-utils==0.14.1 +weasyprint==68.1 +webencodings==0.5.1 +websockets==16.0 +werkzeug==3.1.6 +wheel==0.45.1 +xxhash==3.7.0 +yarl==1.23.0 +zipp==3.19.2 +zopfli==0.4.1 +zstandard==0.25.0 diff --git a/search_bot/handlers.py b/search_bot/handlers.py new file mode 100644 index 0000000..8145893 --- /dev/null +++ b/search_bot/handlers.py @@ -0,0 +1,685 @@ +# /opt/trueconf_bot/search_bot/handlers.py +import re +import os +import sys +import csv +import time +import math +import httpx +import smtplib +import asyncio +import torch +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime + +from trueconf import Router, Message +from sentence_transformers import CrossEncoder +from qdrant_client import QdrantClient +from qdrant_client.models import Filter, FieldCondition, MatchValue, Range + +from config.config import * +from utils import states +from utils.menu import MENU_TEXT +from utils.texts import SEARCH_MAIN_MENU_TEXT, SEARCH_UNKNOWN_CMD_TEXT, search_searching_text, search_not_found_text, search_feedback_thanks_text, search_feedback_info_text, search_unavailable_action + +# 🔌 Импортируем централизованную функцию сбора статистики из main +from utils.stats_logger import log_menu_stats + +# ========================================================= +# ГЛОБАЛЬНЫЙ МАРШРУТИЗАТОР ЦЕЛЬНЫХ VK-ЭМОДЗИ +# ========================================================= +EMOJI_DIGITS = { + "0": "0⃣", + "1": "1⃣", + "2": "2⃣", + "3": "3⃣", + "4": "4⃣", + "5": "5⃣", + "6": "6⃣", + "7": "7⃣", + "8": "8⃣", + "9": "9⃣", + "10": "🔟" +} + +# Глобальная константа для меню Поискового Бота + +# Универсальный стандарт сообщения об ошибке ввода + +# Проверка флага глубокого профилирования +ENABLE_DEEP_PROFILING = getattr(sys.modules['config.config'], 'ENABLE_DEEP_PROFILING', False) if 'config.config' in sys.modules else True + +# ========================================================= +# INIT +# ========================================================= +router = Router() + +# Подключаем Qdrant динамически из живых настроек config.py +live_config = sys.modules.get('config.config') +client = QdrantClient( + host=getattr(live_config, 'QDRANT_HOST', 'localhost'), + port=getattr(live_config, 'QDRANT_PORT', 6333) +) + +# Универсальная гибридная инициализация локального реранкера (если выбран LOCAL) +reranker = None +if getattr(live_config, 'ENABLE_RERANKER', False) and getattr(live_config, 'RERANKER_MODE', '') == "LOCAL": + try: + device = "cuda" if torch.cuda.is_available() else "cpu" + model_path = getattr(live_config, 'RERANK_MODEL_PATH', '/opt/models/bge-reranker') + reranker = CrossEncoder(model_path, max_length=512, device=device) + print(f"📦 Реранкинг: ЛОКАЛЬНЫЙ режим на {device.upper()}") + except Exception as e: + print(f"⚠️ Ошибка загрузки локальной модели реранкера: {e}") + +# ========================================================= +# EMAIL ALERT FUNCTION +# ========================================================= +last_email_time = 0.0 + +def send_alert_email(subject: str, body: str, is_critical_error: bool = False): + global last_email_time + current_config = sys.modules.get('config.config') + if not getattr(current_config, 'ALERTS_SUPPORT_EMAILS', []): + return + + current_time = time.time() + if is_critical_error and (current_time - last_email_time < EMAIL_COOLDOWN_SEC): + return + + if is_critical_error: + last_email_time = current_time + + msg = MIMEMultipart() + msg["From"] = getattr(current_config, 'DEFAULT_EMAIL_FROM', 'ai@sibcem.ru') + msg["To"] = ", ".join(getattr(current_config, 'ALERTS_SUPPORT_EMAILS', [])) + msg["Subject"] = subject + msg.attach(MIMEText(body, "plain", "utf-8")) + + try: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + server.send_message(msg) + except Exception as e: + print(f"⚠️ Ошибка отправки email-алерта: {e}") + +# ========================================================= +# SYSTEM ERRORS & LOGGING +# ========================================================= +def log_system_error(action: str, error_details: str): + os.makedirs(LOGS_PATH, exist_ok=True) + log_file = os.path.join(LOGS_PATH, "search_errors.log") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(log_file, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] Действие: {action} | Ошибка: {error_details}\n") + +async def handle_search_system_error(msg: Message, user_id: str, action: str, error_details: str): + await asyncio.to_thread(log_system_error, action, error_details) + email_body = f"Пользователь: {user_id}\nДействие: {action}\nДетали проблемы:\n{error_details}" + await asyncio.to_thread(send_alert_email, f"🚨 Критический сбой: Поиск Бот - {action}", email_body, True) + + await msg.answer(search_unavailable_action(EMOJI_DIGITS), parse_mode="html") + +def log_analytics(login: str, query: str, answer: str, is_success: bool, response_time: float, qdrant_debug: str, sources: set): + os.makedirs(LOGS_PATH, exist_ok=True) + csv_file = os.path.join(LOGS_PATH, "chat_history.csv") + text_log = os.path.join(LOGS_PATH, "chat_debug.log") + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + sources_str = ", ".join(sorted(sources)) if sources else "Нет" + file_exists = os.path.isfile(csv_file) + + time_str = f"{response_time:.2f}".replace(".", ",") + + with open(csv_file, mode="a", encoding="utf-8-sig", newline="") as f: + writer = csv.writer(f, delimiter=";") + if not file_exists: + writer.writerow(["Дата и Время", "Логин", "Вопрос", "Ответ", "Успешно?", "Время ответа (сек)", "Документы", "Ответ устроил?", "Текст отзыва"]) + + writer.writerow([ + timestamp, login, query.replace("\n", " ").strip(), + answer.replace("\n", " ").strip(), "Да" if is_success else "Нет", + time_str, sources_str, "Да", "" + ]) + + if not getattr(sys.modules['config.config'], 'ENABLE_QDRANT_DEBUG', False): + return + + with open(text_log, mode="a", encoding="utf-8") as f: + f.write(f"╔{'═'*78}╗\n") + f.write(f"║ ВРЕМЯ: {timestamp}\n") + f.write(f"║ ПОЛЬЗОВАТЕЛЬ: {login}\n") + f.write(f"║ ВОПРОС: {query}\n") + f.write(f"╔{'═'*78}╗\n") + f.write("║ ВЫВОД QDRANT / RERANKER:\n") + f.write(f"{qdrant_debug}\n") + f.write(f"╠{'═'*78}╣\n") + f.write("║ ПОЛНЫЙ ОТВЕТ ИИ / ОШИБКА:\n") + f.write(f"║ {answer.replace(chr(10), chr(10) + '║ ')}\n") + f.write(f"╠{'═'*78}╣\n") + f.write(f"║ ИСТОЧНИКИ: {sources_str}\n") + f.write(f"║ ВРЕМЯ ОБРАБОТКИ: {response_time:.2f} сек.\n") + f.write(f"╚{'═'*78}╝\n\n") + +def update_feedback_log(login: str, rating: str = None, feedback_text: str = None): + csv_file = os.path.join(LOGS_PATH, "chat_history.csv") + if not os.path.isfile(csv_file): + return + try: + with open(csv_file, mode="r", encoding="utf-8-sig") as f: + reader = list(csv.reader(f, delimiter=";")) + target_idx = -1 + for i in range(len(reader) - 1, 0, -1): + if reader[i][1] == login: + target_idx = i + break + if target_idx != -1: + while len(reader[target_idx]) < 9: + reader[target_idx].append("") + if rating is not None: + reader[target_idx][7] = rating + if feedback_text is not None: + reader[target_idx][8] = feedback_text.replace("\n", " ").strip() + with open(csv_file, mode="w", encoding="utf-8-sig", newline="") as f: + writer = csv.writer(f, delimiter=";") + writer.writerows(reader) + except Exception as e: + print(f"⚠️ Ошибка обновления отзыва: {e}") + +def get_last_query_and_answer(login: str): + csv_file = os.path.join(LOGS_PATH, "chat_history.csv") + if not os.path.isfile(csv_file): + return "Неизвестно", "Неизвестно" + try: + with open(csv_file, mode="r", encoding="utf-8-sig") as f: + reader = list(csv.reader(f, delimiter=";")) + for i in range(len(reader) - 1, 0, -1): + if reader[i][1] == login: + query = reader[i][2] if len(reader[i]) > 2 else "Неизвестно" + answer = reader[i][3] if len(reader[i]) > 3 else "Неизвестно" + return query, answer + except Exception as e: + pass + return "Неизвестно", "Неизвестно" + +# ========================================================= +# HELPERS +# ========================================================= +def truncate_context(text: str) -> str: + if len(text) <= MAX_SINGLE_CHUNK_LENGTH: + return text + return text[:MAX_SINGLE_CHUNK_LENGTH] + "\n...[обрезано]" + +def normalize_text(text: str) -> str: + return re.sub(r"\s+", " ", text.lower()).strip() + +def is_table_chunk(text: str) -> bool: + table_patterns = ["Контекст:", "Параметр:", "Значение:", "Col", "|"] + hits = sum(1 for p in table_patterns if p in text) + return hits >= 2 + +# ========================================================= +# AI REQUEST +# ========================================================= +async def ask_ai(query: str, context_texts: list) -> str: + 刻_texts = "\n\n".join(context_texts) + + current_config = sys.modules.get('config.config') + live_ollama_url = getattr(current_config, 'OLLAMA_URL', 'http://172.16.80.250:8080/v1/chat/completions') + live_ollama_model = getattr(current_config, 'OLLAMA_MODEL', 'gemma-4-E4B-it-Q4_K_M') + live_ollama_temp = getattr(current_config, 'OLLAMA_TEMPERATURE', 0.0) + live_ollama_predict = getattr(current_config, 'OLLAMA_NUM_PREDICT', 1500) + live_ai_timeout = getattr(current_config, 'AI_HTTP_TIMEOUT', 180.0) + + full_prompt = f"""Ты — строгий и точный корпоративный аналитик документов АО «ХК «Сибцем». +Твоя единственная задача — отвечать на вопросы пользователя, используя ИСКЛЮЧИТЕЛЬНО информацию из предоставленного контекста. + + +{刻_texts} + + +ИНСТРУКЦИИ И ПРАВИЛА: +1. Опирайся строго на текст внутри тегов . Категорически запрещено использовать внешние знания или додумывать факты. +2. Приоритет данных: Если в тексте есть таблицы, списки или структурированные блоки, в первую очередь бери сроки и параметры оттуда. +3. Внимательность: Всегда дочитывай абзац до конца, чтобы не пропустить исключения. +4. Косвенные ответы: Если в тексте прямо не описан процесс (например, "Как создать?"), но указано, на основании чего он выполняется (например, "создается по заявке"), ОБЯЗАТЕЛЬНО сообщи об этом пользователю. +5. Формат ответа: Пиши кратко, ясно и только по делу на русском языке. +6. ЕСЛИ ОТВЕТ НАЙДЕН: В конце ответа всегда с новой строки указывай источник в формате: "Источник: [название файла]". +7. ЕСЛИ ОТВЕТА НЕТ: Если информации в тегах недостаточно даже для косвенного ответа, выведи строго одну фразу: «В данных документах этот вопрос не регламентирован.». Источник указывать не нужно. + +ВОПРОС ПОЛЬЗОВАТЕЛЯ: +{query}""" + + safe_temp = 0.1 if live_ollama_temp <= 0.0 else live_ollama_temp + + payload = { + "model": live_ollama_model, + "messages": [ + {"role": "user", "content": full_prompt} + ], + "temperature": safe_temp, + "max_tokens": live_ollama_predict, + "stream": False + } + + async with httpx.AsyncClient(timeout=httpx.Timeout(live_ai_timeout)) as http_client: + response = await http_client.post(live_ollama_url, json=payload) + response.raise_for_status() + data = response.json() + return data["choices"][0]["message"]["content"].strip() + +# ========================================================= +# MAIN MESSAGE HANDLER +# ========================================================= +@router.message() +async def main_message_handler(msg: Message): + user_id = msg.from_user.id + current_state = states.get_state(user_id) + + current_config = sys.modules.get('config.config') + live_use_reranker = getattr(current_config, 'ENABLE_RERANKER', False) + live_reranker_mode = getattr(current_config, 'RERANKER_MODE', 'REMOTE_LLAMACPP') + live_reranker_url = getattr(current_config, 'RERANKER_API_URL', 'http://172.16.80.250:8080/v1/rerank') + live_reranker_model = getattr(current_config, 'RERANKER_MODEL_NAME', 'bge-reranker-v2-m3-Q8_0') + + msg_text = msg.text.strip() if msg.text else "" + cmd = msg_text.lower() + + # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ + for raw_num, emoji_num in EMOJI_DIGITS.items(): + if cmd == emoji_num: + cmd = raw_num + break + + # ----------------------------------------------------- + # ЛОГИКА ФИДБЕКА + # ----------------------------------------------------- + if current_state in ["AWAITING_FEEDBACK", "FEEDBACK_DONE"]: + msg.handled = True + if cmd in ["0", "9", "/start", "меню"]: + log_menu_stats(user_id, "Search Bot", "Выход в главное меню") + states.clear_state(user_id) + await msg.answer(MENU_TEXT, parse_mode="html") + return + elif cmd == "1": + log_menu_stats(user_id, "Search Bot", "Повторный ввод вопроса") + states.set_state(user_id, "SEARCH_MODE") + await msg.answer(f"🔍 Жду ваш следующий вопрос по регламентам!\n\nДля выхода отправьте {EMOJI_DIGITS['0']}.", parse_mode="html") + return + elif current_state == "AWAITING_FEEDBACK" and msg_text: + log_menu_stats(user_id, "Search Bot", "Отправлен развернутый отзыв") + update_feedback_log(str(user_id), feedback_text=msg_text) + states.set_state(user_id, "SEARCH_MODE") + last_query, last_answer = get_last_query_and_answer(str(user_id)) + send_alert_email( + subject="💬 Новый текстовый отзыв к боту-регламентов", + body=(f"Пользователь: {user_id}\n\nВОПРОС ПОЛЬЗОВАТЕЛЯ:\n{last_query}\n\nОТВЕТ БОТА:\n{last_answer}\n\nТЕКСТ ОТЗЫВА:\n{msg_text}") + ) + await msg.answer(search_feedback_thanks_text(EMOJI_DIGITS), parse_mode="html") + return + else: + await msg.answer(UNKNOWN_CMD_TEXT, parse_mode="html") + return + + # ----------------------------------------------------- + # ЛОГИКА ПОИСКА (SEARCH_MODE) + # ----------------------------------------------------- + if current_state == "SEARCH_MODE": + msg.handled = True + + if cmd in ["0", "9", "/start", "меню"]: + log_menu_stats(user_id, "Search Bot", "Выход в главное меню") + states.clear_state(user_id) + await msg.answer(MENU_TEXT, parse_mode="html") + return + + if cmd == "1": + log_menu_stats(user_id, "Search Bot", "Клик: Ответ не удовлетворил (1)") + states.set_state(user_id, "AWAITING_FEEDBACK") + update_feedback_log(str(user_id), rating="Нет") + last_query, last_answer = get_last_query_and_answer(str(user_id)) + send_alert_email( + subject="⚠️ Пользователя не устроил answer бота", + body=(f"Пользователь: {user_id}\n\nВОПРОС ПОЛЬЗОВАТЕЛЯ:\n{last_query}\n\nОТВЕТ БОТА:\n{last_answer}\n\nНажал '1' (Ответ не удовлетворил).") + ) + await msg.answer(search_feedback_info_text(EMOJI_DIGITS), parse_mode="html") + return + + if not msg_text: + await msg.answer(UNKNOWN_CMD_TEXT, parse_mode="html") + return + + # ФИКСАЦИЯ СТАТИСТИКИ: Начало семантического поиска + log_menu_stats(user_id, "Search Bot", "Выполнение поиска по регламентам") + + # ===================================================== + # ГЛУБОКОЕ ПРОФИЛИРОВАНИЕ (DEEP PROFILING) + # ===================================================== + start_time = time.time() + prof_log = [] + + def add_prof(msg_text): + if ENABLE_DEEP_PROFILING: + elapsed = time.time() - start_time + prof_log.append(f"[{elapsed:>6.2f}s] {msg_text}") + + add_prof(f"=== СТАРТ ЗАПРОСА: '{msg_text}' ===") + add_prof(f"[НАСТРОЙКИ] QDRANT_LIMIT={QDRANT_LIMIT}, RERANK_TOP_K={RERANK_TOP_K}, USE_RERANKER={live_use_reranker}, CHUNK_WINDOW={QDRANT_CHUNK_WINDOW}") + + await msg.answer(search_searching_text(), parse_mode="html") + + # ===================================================== + # ВЕКТОРИЗАЦИЯ И ПОИСК (API ЭМБЕДДИНГОВ) + # ===================================================== + try: + t_vec = time.time() + + payload_api = { + "model": getattr(current_config, 'EMBEDDING_MODEL_NAME', 'bge-m3'), + "input_type": "query", + "texts": [msg_text] + } + headers_api = {"Content-Type": "application/json"} + api_token = getattr(current_config, 'EMBEDDING_API_TOKEN', None) + if api_token: + headers_api["X-Embedding-Token"] = api_token + + async with httpx.AsyncClient(timeout=httpx.Timeout(API_HTTP_TIMEOUT)) as http_client: + api_resp = await http_client.post(EMBEDDING_API_URL, json=payload_api, headers=headers_api) + api_resp.raise_for_status() + query_vector = api_resp.json()["embeddings"][0] + + add_prof(f"[ВЕКТОРИЗАЦИЯ API] Текст преобразован в вектор через API за {time.time() - t_vec:.2f}s.") + + fetch_limit = QDRANT_LIMIT * 3 if live_use_reranker else QDRANT_LIMIT + + t_qdr = time.time() + search_results = client.query_points( + collection_name=QDRANT_COLLECTION, + query=query_vector, + limit=fetch_limit, + with_payload=True + ).points + add_prof(f"[QDRANT ПОИСК] Извлечено чанков: {len(search_results)} (запрашивали {fetch_limit}). Заняло {time.time() - t_qdr:.2f}s.") + + # --- УНИВЕРСАЛЬНЫЙ ГИБРИДНЫЙ БЛОК РЕРАНКИНГА --- + if live_use_reranker and search_results: + t_rerank = time.time() + logits = None + rerank_success = False + + # --------------------------------------------------------- + # ВАРИАНТ 1: Новый API Llama.cpp (Standard Jina API format) с БАТЧИНГОМ + # --------------------------------------------------------- + if live_reranker_mode == "REMOTE_LLAMACPP": + add_prof(f"[RERANKER] Вызов Llama.cpp API для {len(search_results)} чанков (Батчинг).") + + documents = [res.payload.get("page_content", "")[:1500] for res in search_results] + batch_size = 4 + logits = [0.0] * len(documents) + rerank_success = True + + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(API_HTTP_TIMEOUT)) as http_client: + for b_start in range(0, len(documents), batch_size): + b_docs = documents[b_start:b_start + batch_size] + payload = { + "model": live_reranker_model, + "query": msg_text, + "documents": b_docs, + "top_n": len(b_docs) + } + + response = await http_client.post(live_reranker_url, json=payload) + response.raise_for_status() + rerank_data = response.json() + + if "results" in rerank_data: + for item in rerank_data["results"]: + local_index = item.get("index") + score = item.get("relevance_score", 0.0) + if local_index is not None: + global_index = b_start + local_index + if 0 <= global_index < len(logits): + logits[global_index] = score + else: + add_prof(f"[RERANKER ERROR] Неверный формат ответа в батче {b_start}: {rerank_data}") + rerank_success = False + break + except Exception as e: + add_prof(f"[RERANKER CRASH] Ошибка Llama.cpp API (батч {b_start}): {e}") + rerank_success = False + + # --------------------------------------------------------- + # ВАРИАНТ 2: Старый самописный удаленный API (Legacy) + # --------------------------------------------------------- + elif live_reranker_mode == "REMOTE_CUSTOM": + add_prof(f"[RERANKER] Вызов старого кастомного API для {len(search_results)} чанков.") + passages = [res.payload.get("page_content", "") for res in search_results] + payload = { + "query": msg_text, + "passages": passages + } + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(API_HTTP_TIMEOUT)) as http_client: + response = await http_client.post(live_reranker_url, json=payload) + response.raise_for_status() + rerank_data = response.json() + + if rerank_data.get("status") == "success": + logits = rerank_data.get("scores", []) + rerank_success = True + else: + add_prof(f"[RERANKER ERROR] Сервер вернул ошибку: {rerank_data.get('detail')}") + except Exception as e: + add_prof(f"[RERANKER CRASH] Ошибка кастомного API: {e}") + + # --------------------------------------------------------- + # ВАРИАНТ 3: Локальный инференс (CrossEncoder) + # --------------------------------------------------------- + elif live_reranker_mode == "LOCAL" and reranker is not None: + add_prof(f"[RERANKER] Локальный инференс для {len(search_results)} пар.") + pairs = [[msg_text, res.payload.get("page_content", "")] for res in search_results] + try: + logits = reranker.predict(pairs) + rerank_success = True + except Exception as e: + add_prof(f"[RERANKER CRASH] Ошибка локального инференса: {e}") + + # --------------------------------------------------------- + # ПРИМЕНЕНИЕ РЕЗУЛЬТАТОВ + # --------------------------------------------------------- + if rerank_success and logits is not None: + for i, res in enumerate(search_results): + res.payload["original_qdrant_score"] = res.score + res.score = float(logits[i]) + + search_results = sorted(search_results, key=lambda r: r.score, reverse=True) + search_results = search_results[:RERANK_TOP_K] + add_prof(f"[RERANKER END] Успешно отобрано {len(search_results)} чанков за {time.time() - t_rerank:.2f}s.") + else: + add_prof("[RERANKER FALLBACK] Ошибка реранкинга. Падаем на базовые скоры Qdrant.") + search_results = sorted(search_results, key=lambda r: r.score, reverse=True) + search_results = search_results[:RERANK_TOP_K] + else: + search_results = sorted(search_results, key=lambda r: r.score, reverse=True) + + except Exception as e: + error_str = f"[ОШИБКА ПОИСКА]: {type(e).__name__} - {str(e)}" + add_prof(f"[КРИТИЧЕСКАЯ ОШИБКА] {error_str}") + log_analytics(str(user_id), msg_text, error_str, False, time.time() - start_time, error_str, set()) + await handle_search_system_error(msg, str(user_id), "Семантический поиск", error_str) + return + + if not search_results: + error_str = "Поиск вернул 0 результатов." + add_prof(error_str) + log_analytics(str(user_id), msg_text, error_str, False, time.time() - start_time, error_str, set()) + await handle_search_system_error(msg, str(user_id), "Проверка результатов Qdrant", error_str) + return + + # ===================================================== + # ФИЛЬТРАЦИЯ И СБОРКА КОНТЕКСТА + # ===================================================== + add_prof("[СБОРКА КОНТЕКСТА] Начат анализ найденных чанков...") + raw_contexts, sources, processed_windows, seen_chunk_hashes, qdrant_debug_lines = [], set(), set(), set(), [] + contexts_added, total_context_size = 0, 0 + query_lower = msg_text.lower() + query_words = set(w for w in re.findall(r"\w+", query_lower) if len(w) > 3) + + for i, res in enumerate(search_results, 1): + if contexts_added >= MAX_AI_CONTEXTS: + add_prof(f" -> ПРЕРЫВАНИЕ: Достигнут лимит MAX_AI_CONTEXTS ({MAX_AI_CONTEXTS}).") + break + + score = res.score + metadata = res.payload.get("metadata", {}) + filename = metadata.get("filename", "unknown") + source_path = metadata.get("source", "") + chunk_idx = metadata.get("chunk_index", -1) + chunk_text = res.payload.get("page_content", "") + chunk_text_lower = chunk_text.lower() + + chunk_words = set(w for w in re.findall(r"\w+", chunk_text_lower) if len(w) > 3) + keyword_overlap = len(query_words & chunk_words) + + add_prof(f" -> [{i}] Оценка чанка: '{filename}' (idx={chunk_idx}). Score: {score:.4f}. Пересечение: {keyword_overlap} слов.") + + if not live_use_reranker: + if keyword_overlap == 0: score -= 0.05 + elif keyword_overlap == 1: score += 0.03 + elif keyword_overlap >= 2: score += 0.10 + + if "хран" in query_lower and "хран" in filename.lower(): score += 0.10 + if "долгосроч" in query_lower and ("долгосроч" in chunk_text_lower or "срок хран" in chunk_text_lower or "архив" in chunk_text_lower): score += 0.15 + if "файл" in query_lower and ("файл" in chunk_text_lower or "электрон" in chunk_text_lower): score += 0.05 + + is_table = is_table_chunk(chunk_text) + + # --- УНИВЕРСАЛЬНАЯ ДИНАМИЧЕСКАЯ ФИЛЬТРАЦИЯ ОТ МУСОРА --- + if live_use_reranker: + effective_threshold = getattr(current_config, 'RERANK_SCORE_THRESHOLD', -3.5) + else: + effective_threshold = TABLE_SCORE_THRESHOLD if is_table else QDRANT_SCORE_THRESHOLD + if is_table: + add_prof(f" - Обнаружена таблица. Порог снижен до {effective_threshold}.") + + if score < effective_threshold: + add_prof(f" - ПРОПУСК: Итоговый score {score:.4f} ниже порога {effective_threshold}.") + qdrant_debug_lines.append(f"[SKIP] {score:.4f} < {effective_threshold} | {filename}") + continue + + min_idx, max_idx = max(0, chunk_idx - QDRANT_CHUNK_WINDOW), chunk_idx + QDRANT_CHUNK_WINDOW + + try: + t_scroll = time.time() + neighbor_records, _ = client.scroll( + collection_name=QDRANT_COLLECTION, + scroll_filter=Filter( + must=[ + FieldCondition(key="metadata.source", match=MatchValue(value=source_path)), + FieldCondition(key="metadata.chunk_index", range=Range(gte=min_idx, lte=max_idx)) + ] + ), + limit=QDRANT_SCROLL_LIMIT, + with_payload=True + ) + add_prof(f" - SCROLL соседей ({min_idx}-{max_idx}) занял {time.time() - t_scroll:.2f}s. Найдено соседей: {len(neighbor_records)}.") + except Exception as e: + add_prof(f" - SCROLL ERROR: {e}") + qdrant_debug_lines.append(f"[SCROLL ERROR] {e}") + continue + + if not neighbor_records: + continue + + neighbor_records.sort(key=lambda x: x.payload["metadata"]["chunk_index"]) + + merged_parts = [] + for p in neighbor_records: + idx = p.payload["metadata"].get("chunk_index", -1) + txt = p.payload.get("page_content", "") + if txt: + normalized_sub = normalize_text(txt)[:1200] + sub_hash = hash(normalized_sub) + if sub_hash in seen_chunk_hashes: + continue + + seen_chunk_hashes.add(sub_hash) + merged_parts.append(f"[Чанк {idx}]\n{truncate_context(txt)}") + + if not merged_parts: + continue + + merged_text = "\n\n".join(merged_parts) + final_context = f"ФАЙЛ: {filename}\n\n{merged_text}" + + if total_context_size + len(final_context) > MAX_CONTEXT_LENGTH: + add_prof(f" - ПРЕРЫВАНИЕ: Лимит контекста ({total_context_size + len(final_context)} > {MAX_CONTEXT_LENGTH} симв.)") + qdrant_debug_lines.append("[STOP] Достигнут лимит контекста") + break + + total_context_size += len(final_context) + raw_contexts.append(final_context) + sources.add(filename) + contexts_added += 1 + + add_prof(f" - ДОБАВЛЕНО В ИТОГ. Текущий размер промпта: {total_context_size} симв.") + + debug_preview = merged_text[:DEBUG_TEXT_LIMIT].replace("\n", " ") + orig_score = res.payload.get("original_qdrant_score", res.score) + qdrant_debug_lines.append(f"[{i}] orig={orig_score:.4f} | final={score:.4f} | overlap={keyword_overlap} | table={is_table} | file={filename} | chunk={chunk_idx}\npreview={debug_preview}...") + + qdrant_debug_full = "\n".join(qdrant_debug_lines) + add_prof(f"[СБОРКА КОНТЕКСТА ЗАВЕРШЕНА] Итого контекстов: {contexts_added}. Источников: {len(sources)}.") + + if not raw_contexts: + error_str = "Документы найдены Qdrant, но отсеяны фильтрами (score threshold)." + add_prof(f"[СТОП] {error_str}") + log_analytics(str(user_id), msg_text, "Не найдено", False, time.time() - start_time, qdrant_debug_full, set()) + send_alert_email(subject="📭 Бот не нашел релевантных документов", body=f"Пользователь: {user_id}\nВопрос: {msg_text}\nПричина: {error_str}") + await msg.answer(search_not_found_text(EMOJI_DIGITS), parse_mode="html") + + if ENABLE_DEEP_PROFILING: + with open(os.path.join(LOGS_PATH, "deep_profiling.log"), "a", encoding="utf-8") as f: + f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] USER: {user_id}\n" + "\n".join(prof_log) + "\n" + "="*80 + "\n") + return + + # ===================================================== + # AI REQUEST (LLM) + # ===================================================== + try: + add_prof(f"[LLM START] Подготовка запроса к {getattr(current_config, 'OLLAMA_MODEL', 'gemma')}.") + t_llm = time.time() + + ai_answer = await ask_ai(msg_text, raw_contexts) + + add_prof(f"[LLM END] Ответ получен за {time.time() - t_llm:.2f}s.") + + if not ai_answer or ai_answer.strip() == "": + add_prof(f"[LLM WARNING] Модель вернула пустую строку. Применяется фоллбэк.") + ai_answer = "В данных документах этот вопрос не регламентирован." + + is_success = "не регламентирован" not in ai_answer.lower() + + if not is_success: + send_alert_email( + subject="👁 Бот не смог найти answer в тексте (Не регламентирован)", + body=f"Пользователь: {user_id}\nВопрос: {msg_text}\n\nИИ ответил, что вопрос не регламентирован." + ) + + final_response = f"🤖 ОТВЕТ ИИ:\n\n{ai_answer}\n\n{EMOJI_DIGITS['1']} — Ответ не удовлетворил\n{EMOJI_DIGITS['0']} — Выход в меню" + + add_prof(f"[ИТОГ] Запрос полностью выполнен за {time.time() - start_time:.2f}s.") + log_analytics(str(user_id), msg_text, ai_answer, is_success, time.time() - start_time, qdrant_debug_full, sources) + await msg.answer(final_response, parse_mode="html") + + except Exception as e: + error_str = f"[ОШИБКА LLM]: {type(e).__name__} - {str(e)}" + add_prof(f"[FATAL ERROR] {error_str}") + qdrant_debug_full += f"\n\n[FATAL ERROR]\n{error_str}" + log_analytics(str(user_id), msg_text, error_str, False, time.time() - start_time, qdrant_debug_full, sources) + await handle_search_system_error(msg, str(user_id), "Запрос к серверу LLM", error_str) + + if ENABLE_DEEP_PROFILING: + with open(os.path.join(LOGS_PATH, "deep_profiling.log"), "a", encoding="utf-8") as f: + f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] USER: {user_id}\n" + "\n".join(prof_log) + "\n" + "="*80 + "\n") \ No newline at end of file diff --git a/search_bot/indexer.py b/search_bot/indexer.py new file mode 100644 index 0000000..951d461 --- /dev/null +++ b/search_bot/indexer.py @@ -0,0 +1,173 @@ +import os +import sys +import string +import pickle # Библиотека для сохранения фрагментов текста на диск + +# Загрузчики и разбивка текста +from langchain_community.document_loaders import DirectoryLoader, PyMuPDFLoader, Docx2txtLoader +from langchain_text_splitters import RecursiveCharacterTextSplitter + +# Эмбеддинги и векторная база +from langchain_community.embeddings import HuggingFaceEmbeddings +from langchain_community.vectorstores import FAISS + +# Ретриверы +from langchain_community.retrievers import BM25Retriever +from langchain_classic.retrievers import EnsembleRetriever + +# Цепочки +from langchain_classic.chains.combine_documents import create_stuff_documents_chain +from langchain_classic.chains import create_retrieval_chain + +# Базовые модули и LLM +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI + +# ========================================== +# НАСТРОЙКА ПУТЕЙ +# ========================================== +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_REGULATIONS_PATH = os.path.join(BASE_DIR, "search_bot", "Регламенты", "АОХКСибцем", "07 Служба Вице-президента по экономике и финансам", "ДИТ") +MODEL_PATH = os.path.join(BASE_DIR, "models", "mpnet_model") + +# Пути для сохранения базы данных +FAISS_DB_PATH = os.path.join(BASE_DIR, "search_bot", "faiss_index") +CHUNKS_PATH = os.path.join(BASE_DIR, "search_bot", "chunks.pkl") + +# ========================================== +# 1. ИНИЦИАЛИЗАЦИЯ ЭМБЕДДИНГОВ +# ========================================== +print("Загрузка модели эмбеддингов (оффлайн)...") +embedding = HuggingFaceEmbeddings( + model_name=MODEL_PATH, + model_kwargs={'device': 'cpu'}, + encode_kwargs={'normalize_embeddings': False} +) + +# ========================================== +# 2. ЗАГРУЗКА ИЛИ СОЗДАНИЕ БАЗЫ ДАННЫХ +# ========================================== +# Проверяем, существует ли уже сохраненная база +if os.path.exists(FAISS_DB_PATH) and os.path.exists(CHUNKS_PATH): + print("✅ Найдена сохраненная база данных! Загружаю с диска (это быстро)...") + + # Загружаем фрагменты текста для BM25 + with open(CHUNKS_PATH, 'rb') as f: + split_docs = pickle.load(f) + + # Загружаем векторы FAISS + vector_store = FAISS.load_local( + FAISS_DB_PATH, + embedding, + allow_dangerous_deserialization=True # Обязательно для локальных файлов + ) +else: + print("⚠️ Сохраненная база не найдена. Начинаю чтение и индексацию документов...\n") + + if not os.path.exists(BASE_REGULATIONS_PATH): + print("❌ ОШИБКА: Указанный путь к документам не существует.") + sys.exit(1) + + # Загружаем PDF и DOCX + pdf_loader = DirectoryLoader(BASE_REGULATIONS_PATH, glob="**/*.pdf", loader_cls=PyMuPDFLoader, show_progress=True) + docx_loader = DirectoryLoader(BASE_REGULATIONS_PATH, glob="**/*.docx", loader_cls=Docx2txtLoader, show_progress=True) + + docs = pdf_loader.load() + docx_loader.load() + if len(docs) == 0: + print("❌ В указанной папке нет файлов .pdf или .docx. Завершение.") + sys.exit(1) + + print(f"✅ Загружено страниц/файлов: {len(docs)}. Разбивка на фрагменты...") + text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200, separators=["\n\n", "\n", ".", " "]) + split_docs = text_splitter.split_documents(docs) + + print("Создаю векторную базу FAISS...") + vector_store = FAISS.from_documents(split_docs, embedding=embedding) + + # СОХРАНЕНИЕ НА ДИСК ДЛЯ БУДУЩИХ ЗАПУСКОВ + print("💾 Сохраняю базу данных на диск...") + vector_store.save_local(FAISS_DB_PATH) + with open(CHUNKS_PATH, 'wb') as f: + pickle.dump(split_docs, f) + print("✅ База успешно сохранена!") + +# ========================================== +# 3. НАСТРОЙКА РЕТРИВЕРОВ (ПОИСКА) +# ========================================== +# 3.1 Семантический поиск +embedding_retriever = vector_store.as_retriever(search_kwargs={"k": 4}) + +# 3.2 Лексический поиск +def tokenize(s): + return s.lower().translate(str.maketrans("", "", string.punctuation)).split(" ") + +bm25_retriever = BM25Retriever.from_documents( + documents=split_docs, + preprocess_func=tokenize, + k=5 +) + +# 3.3 Гибридный поиск (Ансамбль) +ensemble_retriever = EnsembleRetriever( + retrievers=[embedding_retriever, bm25_retriever], + weights=[0.4, 0.6] +) + +# ========================================== +# 4. ПОДКЛЮЧЕНИЕ К LLAMA.CPP +# ========================================== +print("Подключение к серверу llama.cpp...") +llm = ChatOpenAI( + base_url="http://127.0.0.1:8080/v1", + api_key="not-needed", + temperature=0.0, + max_tokens=1024 +) + +prompt = ChatPromptTemplate.from_template('''Ты — строгий корпоративный ИИ-помощник. +Твоя задача — отвечать на вопросы строго на основании текста предоставленных регламентов. +Внимательно изучи контекст. Если там есть ответ, сформулируй его четко и по делу. Обязательно указывай номер пункта или название документа, откуда взята информация. +Если в контексте НЕТ ответа на вопрос, не придумывай информацию, а выведи фразу: "В предоставленных регламентах нет информации по данному вопросу." + +Контекст: +{context} + +Вопрос пользователя: {input} + +Ответ:''' +) + +document_chain = create_stuff_documents_chain(llm=llm, prompt=prompt) +rag_chain = create_retrieval_chain(ensemble_retriever, document_chain) + +# ========================================== +# 5. ТЕСТИРОВАНИЕ СИСТЕМЫ +# ========================================== +print("\n🚀 Система готова к работе! Выполняем запросы...\n") + +# Тестовые вопросы по Положению № ПОЛ-177 +questions = [ + "Где работник обязан хранить электронные документы, связанные с производственной деятельностью?", + "Через какой срок удаляются электронные документы, если их не открывали, и можно ли их восстановить?", + "Кому разрешен доступ к сетевым ресурсам Soft/Distr?", + "Какие правила хранения и удаления установлены для медиафайлов в сетевой папке 'СВК'?", + "Разрешено ли самостоятельно предоставлять общий доступ к папкам на своем рабочем компьютере?", + "Что имеют право сделать сотрудники ДИТ или СИБ, если файл несет угрозу ресурсам?" +] + +for q in questions: + print(f"❓ Вопрос: {q}") + try: + response = rag_chain.invoke({'input': q}) + print(f"🤖 Ответ: {response['answer']}\n") + + # Распечатка источников (полезно для отладки) + print("🔍 Найденные источники:") + for i, doc in enumerate(response['context']): + source_name = doc.metadata.get('source', 'Неизвестный источник').split('/')[-1] # Берем только имя файла + print(f" [{i+1}] Файл: {source_name}") + print("\n" + "-" * 50 + "\n") + + except Exception as e: + print(f"❌ Ошибка при запросе к серверу llama.cpp: {e}") + print("-" * 50) \ No newline at end of file diff --git a/search_bot/parse_pdf.py b/search_bot/parse_pdf.py new file mode 100644 index 0000000..2abe9ed --- /dev/null +++ b/search_bot/parse_pdf.py @@ -0,0 +1,70 @@ +import os +import requests +import json + +# Адрес вашего сервера с поднятым Docker-контейнером Unstructured +API_URL = "http://172.16.80.250:9000/general/v0/general" + +def main(): + # 1. Ищем все файлы .pdf и .docx в текущей директории + target_files = [f for f in os.listdir('.') if f.lower().endswith(('.pdf', '.docx'))] + target_files.sort() # Для порядка + + if not target_files: + print("❌ В текущей папке не найдено ни одного PDF или DOCX файла.") + return + + print(f"📂 Найдено файлов для обработки: {len(target_files)}") + + # 2. Проходимся по всем найденным файлам + for target_file in target_files: + print(f"\n📄 Обрабатывается файл: {target_file}") + print(f"⏳ Отправляем на сервер {API_URL}... (это может занять время)") + + # Настройки запроса (параметры PDF просто игнорируются сервером для DOCX) + payload_data = { + "strategy": "hi_res", + "languages": ["rus"], + "pdf_infer_table_structure": "true" + } + + # Определяем правильный MIME-тип для файла + ext = target_file.lower().split('.')[-1] + if ext == 'pdf': + mime_type = "application/pdf" + else: + mime_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + + # 3. Открываем файл и делаем POST-запрос + try: + with open(target_file, "rb") as f: + files = { + "files": (target_file, f, mime_type) + } + + # Таймаут 300 секунд + response = requests.post(API_URL, files=files, data=payload_data, timeout=300) + response.raise_for_status() + + # 4. Получаем JSON и сохраняем его в файл + parsed_data = response.json() + output_filename = f"{os.path.splitext(target_file)[0]}_parsed.json" + + with open(output_filename, "w", encoding="utf-8") as out_f: + json.dump(parsed_data, out_f, ensure_ascii=False, indent=4) + + print(f"✅ Готово! Результат успешно сохранен в файл: {output_filename}") + + except requests.exceptions.ConnectionError: + print(f"❌ Ошибка соединения! Проверьте доступность {API_URL}") + except requests.exceptions.Timeout: + print("❌ Сервер слишком долго не отвечал.") + except requests.exceptions.HTTPError as e: + print(f"❌ Сервер вернул ошибку: {e}") + if response.text: + print(f"Детали ошибки от сервера: {response.text}") + except Exception as e: + print(f"❌ Произошла непредвиденная ошибка при обработке {target_file}: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/search_bot/smart_indexer.py b/search_bot/smart_indexer.py new file mode 100644 index 0000000..880dbda --- /dev/null +++ b/search_bot/smart_indexer.py @@ -0,0 +1,650 @@ +import os +import sys +import re +import time +import uuid +import json +import hashlib +import requests + +from glob import glob +from datetime import datetime + +# Импорт SentenceTransformer УДАЛЕН для разгрузки ОЗУ сервера +from qdrant_client import QdrantClient +from qdrant_client.models import ( + Distance, + VectorParams, + PointStruct, + PayloadSchemaType, + Filter, + FieldCondition, + MatchValue +) + +# ========================================================= +# ИМПОРТ ЦЕНТРАЛЬНОГО КОНФИГА БОТА +# ========================================================= +# Добавляем корневую папку бота в sys.path, чтобы скрипт увидел модуль config, +# даже если скрипт запускается из подпапки. +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import config.config as config + +# ========================================================= +# CONFIG (Берем данные из центрального config.py) +# ========================================================= +DOCS_DIR = config.SEARCH_DOCS_PATH +QDRANT_HOST = config.QDRANT_HOST +QDRANT_PORT = config.QDRANT_PORT +COLLECTION_NAME = config.QDRANT_COLLECTION + +# Формируем путь к реестру внутри папки логов, указанной в конфиге +REGISTRY_PATH = os.path.join(config.LOGS_PATH, "indexing_registry.json") + +# ========================================================= +# ЛОКАЛЬНЫЕ НАСТРОЙКИ ИНДЕКСАТОРА +# ========================================================= +UNSTRUCTURED_API_URL = "http://172.16.80.250:9000/general/v0/general" +BATCH_SIZE = 32 +RECREATE_COLLECTION = False +MIN_TEXT_LENGTH = 25 + +# ========================================================= +# INIT +# ========================================================= + +# Локальная инициализация нейросети убрана. Работаем через API. + +print("🔄 Подключение к Qdrant...") + +client = QdrantClient( + host=QDRANT_HOST, + port=QDRANT_PORT +) + +# ========================================================= +# REGISTRY & HASHING +# ========================================================= + +def get_file_hash(filepath): + """Вычисляет MD5 хэш физического файла""" + hasher = hashlib.md5() + with open(filepath, 'rb') as f: + buf = f.read() + hasher.update(buf) + return hasher.hexdigest() + +def load_registry(): + """Загружает текущее состояние проиндексированных файлов""" + if os.path.exists(REGISTRY_PATH): + with open(REGISTRY_PATH, 'r', encoding='utf-8') as f: + return json.load(f) + return {} + +def save_registry(registry): + """Сохраняет реестр на диск""" + os.makedirs(os.path.dirname(REGISTRY_PATH), exist_ok=True) + with open(REGISTRY_PATH, 'w', encoding='utf-8') as f: + json.dump(registry, f, ensure_ascii=False, indent=4) + +# ========================================================= +# HELPERS +# ========================================================= + +def sha256_text(text): + return hashlib.sha256( + text.encode("utf-8") + ).hexdigest() + +def clean_text(text): + if not text: + return "" + + text = re.sub( + r"\s+", + " ", + text + ) + + return text.strip() + +def is_page_number(text): + return bool( + re.match( + r"^Стр\.\s*\d+\s*из\s*\d+$", + text + ) + ) + +def is_header_noise(text): + patterns = [ + "АО «ХК «Сибцем»", + "Тип документа:", + "Ведущее подразделение:", + "Дата утверждения:", + "Редакция 1", + "Оглавление" + ] + + return any( + p in text + for p in patterns + ) + +def is_junk(item, text): + if not text: + return True + + if len(text) < MIN_TEXT_LENGTH: + return True + + if item["type"] in [ + "Header", + "Footer", + "Image" + ]: + return True + + if is_page_number(text): + return True + + if is_header_noise(text): + return True + + return False + +# ========================================================= +# TABLE PARSER +# ========================================================= + +def html_table_to_text(html): + html = re.sub( + r"\n+", + " ", + html + ) + + rows = re.findall( + r"(.*?)", + html, + re.I | re.S + ) + + parsed_rows = [] + + for row in rows: + cells = re.findall( + r"(.*?)", + row, + re.I | re.S + ) + + clean_cells = [] + + for cell in cells: + box = re.sub( + r"<[^>]+>", + " ", + cell + ) + box = clean_text(box) + clean_cells.append(box) + + if clean_cells: + parsed_rows.append( + clean_cells + ) + + if not parsed_rows: + return "" + + headers = parsed_rows[0] + + result_lines = [] + + for row_idx, row in enumerate(parsed_rows[1:], 1): + result_lines.append( + f"Запись {row_idx}:" + ) + + for col_idx, cell in enumerate(row): + if not cell: + continue + + if col_idx < len(headers): + header = headers[col_idx] + else: + header = f"Колонка {col_idx+1}" + + result_lines.append( + f"{header}: {cell}" + ) + + result_lines.append("") + + return "\n".join(result_lines).strip() + +# ========================================================= +# MERGE FRAGMENTS +# ========================================================= + +def merge_fragments(elements): + merged = [] + buffer = [] + buffer_page = None + buffer_section = None + + def flush_buffer(): + nonlocal buffer + + if not buffer: + return + + text = clean_text( + " ".join(buffer) + ) + + if text: + merged.append({ + "type": "MergedText", + "text": text, + "page_number": buffer_page, + "section_title": buffer_section + }) + + buffer = [] + + for el in elements: + text = clean_text( + el["text"] + ) + + if not text: + continue + + el_type = el["type"] + page = el["metadata"].get("page_number", 0) + section = el.get("section_title", "") + + # ========================================= + # TABLE + # ========================================= + if el_type == "Table": + flush_buffer() + + merged.append({ + "type": "Table", + "text": text, + "html": el["metadata"].get("text_as_html", ""), + "page_number": page, + "section_title": section + }) + continue + + # ========================================= + # SHORT TEXT + # ========================================= + if el_type in [ + "NarrativeText", + "ListItem", + "UncategorizedText" + ]: + if len(text) < 80: + buffer.append(text) + buffer_page = page + buffer_section = section + else: + flush_buffer() + merged.append({ + "type": "Text", + "text": text, + "page_number": page, + "section_title": section + }) + else: + flush_buffer() + + flush_buffer() + + return merged + +# ========================================================= +# QDRANT INIT & DELETE +# ========================================================= + +def init_collection(): + collections = [ + c.name + for c in client.get_collections().collections + ] + + if ( + RECREATE_COLLECTION + and COLLECTION_NAME in collections + ): + print("🗑 Удаление коллекции...") + client.delete_collection(COLLECTION_NAME) + collections = [ + c.name + for c in client.get_collections().collections + ] + + if COLLECTION_NAME not in collections: + print("⚙️ Создание коллекции...") + client.create_collection( + collection_name=COLLECTION_NAME, + vectors_config=VectorParams( + size=1024, + distance=Distance.COSINE + ) + ) + + indexes = [ + ("metadata.filename", PayloadSchemaType.KEYWORD), + ("metadata.section_title", PayloadSchemaType.KEYWORD), + ("metadata.page_number", PayloadSchemaType.INTEGER), + ("metadata.content_type", PayloadSchemaType.KEYWORD) + ] + + for field_name, field_schema in indexes: + client.create_payload_index( + collection_name=COLLECTION_NAME, + field_name=field_name, + field_schema=field_schema + ) + +def delete_file_from_qdrant(filename): + """Удаляет все векторы, привязанные к конкретному файлу""" + client.delete( + collection_name=COLLECTION_NAME, + points_selector=Filter( + must=[ + FieldCondition( + key="metadata.filename", + match=MatchValue(value=filename) + ) + ] + ) + ) + print(f" 🗑 Удалены старые данные файла {filename} из базы.") + +# ========================================================= +# PARSE DOCUMENT +# ========================================================= + +def parse_document(file_path): + filename = os.path.basename( + file_path + ) + + print(f"\n📄 Парсинг: {filename}") + + payload_data = { + "strategy": "hi_res", + "languages": ["rus"], + "pdf_infer_table_structure": "true" + } + + ext = filename.lower().split(".")[-1] + + if ext == "pdf": + mime_type = "application/pdf" + else: + mime_type = ( + "application/vnd.openxmlformats-officedocument." + "wordprocessingml.document" + ) + + with open(file_path, "rb") as f: + files = { + "files": ( + filename, + f, + mime_type + ) + } + + response = requests.post( + UNSTRUCTURED_API_URL, + files=files, + data=payload_data, + timeout=900 + ) + + response.raise_for_status() + + return response.json() + +# ========================================================= +# INDEX DOCUMENT +# ========================================================= + +def index_document(file_path, data): + filename = os.path.basename(file_path) + + cleaned = [] + current_title = "Общая информация" + + # ===================================================== + # CLEAN + # ===================================================== + for item in data: + raw_text = item.get("text", "") + text = clean_text(raw_text) + + if is_junk(item, text): + continue + + if item["type"] == "Title": + current_title = text + continue + + item["section_title"] = current_title + cleaned.append(item) + + # ===================================================== + # MERGE + # ===================================================== + merged = merge_fragments(cleaned) + points = [] + + # ===================================================== + # BUILD CHUNKS + # ===================================================== + for idx, chunk in enumerate(merged): + chunk_type = chunk["type"] + section_title = chunk["section_title"] + page_number = chunk["page_number"] + + # ================================================= + # TABLE / TEXT + # ================================================= + if chunk_type == "Table": + table_text = html_table_to_text(chunk["html"]) + content = ( + f"Раздел: {section_title}\n" + f"Тип: Таблица\n\n" + f"{table_text}" + ) + else: + content = ( + f"Раздел: {section_title}\n\n" + f"{chunk['text']}" + ) + + content = clean_text(content) + + if len(content) < 40: + continue + + # ================================================= + # VECTOR (Переведено на центральный Embedding API) + # ================================================= + try: + payload_api = { + "model": config.EMBEDDING_MODEL_NAME, # Отправляем "bge-m3" + "input_type": "passage", # Для сохранения документов используем тип passage + "texts": [content] + } + headers_api = {"Content-Type": "application/json"} + if hasattr(config, 'EMBEDDING_API_TOKEN') and config.EMBEDDING_API_TOKEN: + headers_api["X-Embedding-Token"] = config.EMBEDDING_API_TOKEN + + api_resp = requests.post(config.EMBEDDING_API_URL, json=payload_api, headers=headers_api) + api_resp.raise_for_status() + vector = api_resp.json()["embeddings"][0] + except Exception as e: + print(f"❌ Ошибка векторизации чанка {idx} через API: {e}") + continue # Пропускаем чанк, если центральный сервис недоступен + + chunk_hash = sha256_text( + filename + str(idx) + content + ) + + point_id = str( + uuid.uuid5( + uuid.NAMESPACE_DNS, + chunk_hash + ) + ) + + payload = { + "page_content": content, + "metadata": { + "filename": filename, + "source": file_path, + "chunk_index": idx, + "section_title": section_title, + "page_number": page_number, + "content_type": chunk_type, + "chunk_hash": chunk_hash + } + } + + points.append( + PointStruct( + id=point_id, + vector=vector, + payload=payload + ) + ) + + # ================================================= + # UPSERT BATCH + # ================================================= + if len(points) >= BATCH_SIZE: + client.upsert( + collection_name=COLLECTION_NAME, + points=points + ) + print(f" ⬆️ batch={len(points)}") + points = [] + + # ===================================================== + # FINAL UPSERT + # ===================================================== + if points: + client.upsert( + collection_name=COLLECTION_NAME, + points=points + ) + + print(f" ✅ Загружено chunks: {len(merged)}") + return len(merged) + +# ========================================================= +# MAIN +# ========================================================= + +def main(): + init_collection() + + target_files = [] + + for ext in ["*.pdf", "*.docx"]: + target_files.extend( + glob( + os.path.join(DOCS_DIR, "**", ext), + recursive=True + ) + ) + + # ФИЛЬТРАЦИЯ СКРЫТЫХ ВРЕМЕННЫХ ФАЙЛОВ WORD (~$) + target_files = [f for f in target_files if not os.path.basename(f).startswith("~$")] + + target_files.sort() + + if not target_files: + print("❌ PDF/DOCX файлы не найдены") + return + + print(f"📂 Найдено файлов для индексации: {len(target_files)}") + print(f"📡 Использование удаленной векторизации: {config.EMBEDDING_API_URL} ({config.EMBEDDING_MODEL_NAME})") + + # Загружаем реестр + registry = load_registry() + + # ===================================================== + # PROCESS + # ===================================================== + for file_path in target_files: + filename = os.path.basename(file_path) + current_hash = get_file_hash(file_path) + + file_info = registry.get(filename) + + # Проверка: Файл уже есть, хэш совпадает, статус был успешным + if file_info and file_info.get("hash") == current_hash and file_info.get("status") == "SUCCESS": + print(f"\n⏭️ Пропуск (не изменился): {filename}") + continue + + print(f"\n🔄 Обработка: {filename}") + + # Если файл есть в реестре со статусом SUCCESS, но хэш другой (файл обновили) + if file_info and file_info.get("status") == "SUCCESS": + print(f" ⚠️ Обнаружена новая версия файла! Обновляем индекс...") + delete_file_from_qdrant(filename) + + try: + data = parse_document(file_path) + chunks_count = index_document(file_path, data) + + # Запись успешного результата + registry[filename] = { + "hash": current_hash, + "status": "SUCCESS", + "chunks": chunks_count, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + save_registry(registry) + + # ПАУЗА ПРИ НЕДОСТУПНОСТИ СЕРВЕРА ПАРСИНГА + except requests.exceptions.ConnectionError: + print(f" ⚠️ Сервер парсинга документов недоступен. Ждем 60 секунд...") + time.sleep(60) + registry[filename] = { + "hash": current_hash, + "status": "ERROR", + "error_details": "Connection refused", + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + save_registry(registry) + + except Exception as e: + print(f" ❌ Ошибка обработки файла {filename}: {e}") + + # Запись ошибки в реестр + registry[filename] = { + "hash": current_hash, + "status": "ERROR", + "error_details": str(e), + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + save_registry(registry) + + print("\n🎉 ГОТОВО. База Qdrant полностью синхронизирована через API.") + +# ========================================================= + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/send_message.py b/send_message.py new file mode 100644 index 0000000..eb42ed0 --- /dev/null +++ b/send_message.py @@ -0,0 +1,54 @@ +import sys +import os +import asyncio + +# Импортируем только учетные данные для подключения +from config.config import TC_SERVER, TC_LOGIN, TC_PASSWORD +from trueconf import Bot + +async def send_proactive_message(target_user: str, message_text: str): + # Создаем объект бота + bot = Bot.from_credentials( + server=TC_SERVER, + username=TC_LOGIN, + password=TC_PASSWORD, + verify_ssl=False + ) + + # Запускаем фоновый цикл для поднятия WebSocket соединения + bot_task = asyncio.create_task(bot.run()) + + print("🔄 Подключение к серверу (ожидание 3 секунды)...") + await asyncio.sleep(3) # Даем боту время поднять коннект + + try: + print(f"✉️ Отправка сообщения пользователю {target_user}...") + + # Отправляем сообщение + await bot.send_message(target_user, text=message_text, parse_mode="html") + + print("✅ Сообщение успешно отправлено!") + + except Exception as e: + print(f"❌ Ошибка при отправке: {e}") + + finally: + print("🛑 Завершение скрипта...") + # Мгновенно убиваем процесс, чтобы не зависнуть на WebSockets + os._exit(0) + + +if __name__ == "__main__": + # Чтение аргументов из консоли + if len(sys.argv) >= 3: + user = sys.argv[1] + text = " ".join(sys.argv[2:]) + else: + # Для ручного теста (без аргументов) + user = "ds.krivochenko@tcs.sibcem.ru" # Убедитесь, что чат с ботом уже инициирован! + text = "🔔 Уведомление: Тестовое сообщение." + + try: + asyncio.run(send_proactive_message(user, text)) + except KeyboardInterrupt: + os._exit(0) \ No newline at end of file diff --git a/service_desk/__init__.py b/service_desk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service_desk/handlers.py b/service_desk/handlers.py new file mode 100644 index 0000000..8f533da --- /dev/null +++ b/service_desk/handlers.py @@ -0,0 +1,396 @@ +import logging +from logging.handlers import RotatingFileHandler +import os +import json +import asyncio +import httpx +import re +import smtplib +import urllib3 +import importlib.util +from email.message import EmailMessage +from ldap3 import Server, Connection, ALL +from trueconf import Router, Message + +# --- SD WORKFLOW LOGGING SETUP --- +log_dir = os.path.join(os.path.dirname(__file__), 'logs') +os.makedirs(log_dir, exist_ok=True) +sd_workflow_logger = logging.getLogger('sd_workflow') +sd_workflow_logger.setLevel(logging.INFO) +if not sd_workflow_logger.handlers: + handler = RotatingFileHandler(os.path.join(log_dir, 'sd_workflow.log'), maxBytes=10*1024*1024, backupCount=5) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + sd_workflow_logger.addHandler(handler) +# ---------------------------------- + +# Абсолютный импорт конфигурации +spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "config.py")) +custom_config = importlib.util.module_from_spec(spec) +spec.loader.exec_module(custom_config) +globals().update(vars(custom_config)) + +from utils import states +from utils.menu import MENU_TEXT +from utils.texts import ( + EMOJI_DIGITS, + SD_MAIN_MENU_TEXT, + SD_UNKNOWN_CMD_TEXT, + SD_TEXT_REQUIRED, + SD_CREATING, + sd_ticket_created, + sd_ticket_create_error, + sd_system_error, + sd_uploading_file, + sd_file_attached, + sd_file_upload_error, +) +from utils.texts import UNKNOWN_MAIN_CMD_TEXT +# 🔌 Импортируем централизованную функцию сбора статистики из main +from utils.stats_logger import log_menu_stats + +# Таймаут ожидания вложений после текста (секунды) +ATTACHMENT_WAIT_TIMEOUT = 2.0 + +SD_DISPATCHER_SYSTEM_PROMPT = ( + "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\n" + "ПРАВИЛА:\n" + "1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n" + "2. Отвечать на английском языке ЗАПРЕЩЕНО.\n" + "3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n" + "4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n" + "5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.") + +# Глушим системные предупреждения +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +logger = logging.getLogger(__name__) +router = Router() +sd_sessions = {} + +# ================================================ +# БИЗНЕС-ЛОГИКА +# ================================================ +def get_ad_user_sync(login: str): + try: + server = Server(AD_SERVER, get_info=ALL) + conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True) + conn.search(search_base=AD_BASE, search_filter=f"(sAMAccountName={login})", attributes=["displayName", "mail", "l", "userAccountControl"]) + if conn.entries: + user = conn.entries[0] + uac = user.userAccountControl.value if 'userAccountControl' in user else 0 + return { + "name": user.displayName.value if 'displayName' in user else login, + "mail": user.mail.value if 'mail' in user else None, + "city": user.l.value if 'l' in user else "Кемерово", + "is_disabled": bool(uac & 2) + } + except Exception as e: + logger.error(f"Ошибка LDAP: {e}") + return None + +async def generate_smart_subject(text: str) -> str: + import re, traceback, importlib.util, httpx + spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "config.py")) + custom_config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(custom_config) + OLLAMA_SUBJECT_URL = custom_config.OLLAMA_SUBJECT_URL + OLLAMA_SUBJECT_MODEL = custom_config.OLLAMA_SUBJECT_MODEL + def has_chinese(t): return bool(re.search(r'[\u4e00-\u9fff]', str(t))) + if not text or len(text.split()) < 2: return "Заявка из КЛЕВЕР" + + user_msg = f"Сформулируй краткую тему для следующей ИТ-заявки:\n{text}" + + messages = [ + {"role": "system", "content": SD_DISPATCHER_SYSTEM_PROMPT}, + {"role": "user", "content": user_msg} + ] + + try: + payload = {"model": OLLAMA_SUBJECT_MODEL, "messages": messages, "temperature": 0.0} + sd_workflow_logger.info(f"📝 [Subject Gen] Payload: {payload}") + endpoint = OLLAMA_SUBJECT_URL.replace("/api/generate", "/v1/chat/completions") + async with httpx.AsyncClient() as client: + response = await client.post(endpoint, json=payload, timeout=120.0) + if response.status_code == 200: + data = response.json() + if "choices" in data and len(data["choices"]) > 0: + res = data["choices"][0]["message"]["content"].strip(" .'\"`") + res = re.sub(r'^(тема|заголовок|subject):\s*', '', res, flags=re.IGNORECASE).strip() + if res and not has_chinese(res) and not any(m in res.upper() for m in ["СПАМ", "НЕ ПОНЯЛ", "Я ИСКУССТВЕННЫЙ", "Я МОДЕЛЬ"]): + return res[0].upper() + res[1:] if len(res) > 0 else "Заявка из КЛЕВЕР" + else: + sd_workflow_logger.info(f"📝 [Subject Gen Error] Validation Failed: Received '{res}'") + else: + sd_workflow_logger.info(f"📝 [Subject Gen Error] Empty choices in LLM response") + else: + sd_workflow_logger.info(f"📝 [Subject Gen Error] API Error: Status {response.status_code} - {response.text}") + except Exception as e: + sd_workflow_logger.error(f"📝 [Subject Gen Error] Exception: {e}\n{traceback.format_exc()}") + return "Заявка из КЛЕВЕР" + +async def create_ticket_in_sd(requester_email: str, subject: str, description: str, city: str): + headers = {"authtoken": SD_TOKEN, "Accept": "application/vnd.manageengine.sdp.v3+json"} + endpoint = f"{SD_URL}/api/v3/requests" + html_desc = f"

{description.replace(chr(10), '
')}



Создано через TrueConf КЛЕВЕР

" + async def _send_req(email, current_city): + payload = {"request": {"subject": subject, "description": html_desc, "requester": {"email_id": email}, "udf_fields": {"udf_pick_301": current_city}}} + async with httpx.AsyncClient(verify=False) as client: + return await client.post(endpoint, headers=headers, data={"input_data": json.dumps(payload)}, timeout=15.0) + resp = await _send_req(requester_email, city) + data = resp.json() if resp.status_code in [200, 201] else {} + if data.get("response_status", {}).get("status_code") != 2000: + resp = await _send_req(requester_email, "Кемерово") + data = resp.json() if resp.status_code in [200, 201] else {} + if data.get("response_status", {}).get("status_code") == 2000: + return data.get("request", {}).get("id") + return None + +async def process_and_upload_file(file_id: str, filename: str, ticket_id: str): + import io + downloaded_path = None + try: + if isinstance(filename, bytes) or (isinstance(filename, str) and filename.startswith("b'")): + safe_filename = f"attachment_{ticket_id}.dat" + else: + safe_filename = str(filename) + downloaded_file = await states.tc_bot.download_file_by_id(file_id) + if isinstance(downloaded_file, bytes): + file_obj = io.BytesIO(downloaded_file) + downloaded_path = None + else: + downloaded_path = str(downloaded_file) + file_obj = open(downloaded_path, 'rb') + try: + upload_url = f"{SD_URL}/api/v3/requests/{ticket_id}/upload" + async with httpx.AsyncClient(verify=False) as client: + attach_resp = await client.put( + upload_url, + headers={"authtoken": SD_TOKEN}, + files={'input_file': (safe_filename, file_obj, 'application/octet-stream')}, + timeout=30.0 + ) + return attach_resp.status_code in [200, 201] + finally: + if not isinstance(downloaded_file, bytes): + file_obj.close() + except Exception as e: + logger.error(f"Ошибка загрузки файла {filename}: {e}") + return False + finally: + if downloaded_path and os.path.exists(downloaded_path): + os.remove(downloaded_path) + +def _extract_text_from_content(content): + """Извлечение текста из msg.content.""" + if isinstance(content, dict): + return content.get("text", "").strip() + elif hasattr(content, "text"): + return str(content.text).strip() + return "" + +def _extract_attachments_from_content(content, is_attachment_type=False): + """Извлечение file_id и file_name из msg.content.""" + attachments = [] + if isinstance(content, dict): + atts = content.get("attachments") or content.get("files") + single_fid = content.get("file_id") + single_fname = content.get("file_name") + if atts: + if isinstance(atts, list): + for a in atts: + if isinstance(a, dict): + fid = a.get("file_id") or a.get("id") + fname = a.get("file_name") or a.get("name", "attachment") + if fid: attachments.append({"file_id": fid, "file_name": fname}) + elif isinstance(a, str): + attachments.append({"file_id": a, "file_name": "attachment"}) + elif isinstance(atts, dict): + fid = atts.get("file_id") or atts.get("id") + fname = atts.get("file_name") or atts.get("name", "attachment") + if fid: attachments.append({"file_id": fid, "file_name": fname}) + if single_fid: + attachments.append({"file_id": single_fid, "file_name": single_fname or "attachment"}) + elif is_attachment_type: + if hasattr(content, "file_id"): + fid = content.file_id + fname = getattr(content, "file_name", "attachment") + attachments.append({"file_id": fid, "file_name": fname}) + return attachments + +async def _create_ticket_and_attach_files(user_id, msg_text, session, msg, login): + """Логика генерации темы, отправки в SD и загрузки всех очередей вложений.""" + session["step"] = "creating_ticket" + try: + ad_user = await asyncio.to_thread(get_ad_user_sync, login) + sd_workflow_logger.info(f"👤 [AD Lookup] User: {login} -> Found: {ad_user is not None}") + sender_email = ad_user.get("mail") if ad_user else DEFAULT_REQUESTER + city = ad_user.get("city") if ad_user else "Кемерово" + subject = await generate_smart_subject(msg_text) + sd_workflow_logger.info(f"📝 [Subject Gen] Text: {msg_text[:50]}... -> Subject: {subject}") + + ticket_id = await create_ticket_in_sd(sender_email, subject, msg_text, city) + sd_workflow_logger.info(f"🎫 [Ticket Created] ID: {ticket_id}") + + if ticket_id: + session["ticket_id"] = ticket_id + + # 1. Загрузка основных вложений (накопленных до и во время таймаута) + queued_files = list(session.get("files_queue", [])) + if queued_files: + sd_workflow_logger.info(f"📎 [Upload] Uploading {len(queued_files)} file(s) to ticket #{ticket_id}") + for f in queued_files: + await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) + sd_workflow_logger.info(f"✅ [Upload] All queued file(s) uploaded") + + # 2. Загрузка вложений, прилетевших во время выполнения API-запросов (из "черной дыры") + post_files = list(session.get("post_create_queue", [])) + if post_files: + sd_workflow_logger.info(f"📎 [Upload Extra] Uploading {len(post_files)} late file(s) to ticket #{ticket_id}") + for f in post_files: + await process_and_upload_file(f['file_id'], f['file_name'], ticket_id) + + session["files_queue"] = [] + session["post_create_queue"] = [] + session["step"] = "ticket_created" + + await msg.answer( + sd_ticket_created(ticket_id, subject, msg_text), + parse_mode="html" + ) + log_menu_stats(user_id, "Service Desk", f"Создание заявки #{ticket_id}") + else: + session["step"] = "need_text" + await msg.answer(sd_ticket_create_error(), parse_mode="html") + except Exception as e: + logger.exception(f"Ошибка: {e}") + sd_workflow_logger.error(f"❌ [SD Error] {e}") + session["step"] = "need_text" + await msg.answer(sd_system_error(), parse_mode="html") + +async def _wait_for_attachments_and_create(user_id, session): + """Фоновый таймер с возможностью отмены (Debounce).""" + try: + await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT) + except asyncio.CancelledError: + return # Таймер сброшен, прерываем выполнение текущей таски + + if session.get("step") != "waiting_for_attachments": + return + + sd_workflow_logger.info(f"⏰ [SD] Timeout reached. Initiating ticket creation for user {user_id}") + msg = session.get("msg") + login = session.get("login") + if msg: + await _create_ticket_and_attach_files(user_id, session["msg_text"], session, msg, login) + +@router.message() +async def sd_module_handler(msg: Message): + user_id = msg.from_user.id + sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}") + if states.get_state(user_id) != "SD_MODE": + return + + msg_text = _extract_text_from_content(msg.content) + is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" + inline_attachments = _extract_attachments_from_content(msg.content, is_attachment) + + sd_workflow_logger.info(f"🚀 [SD Debug] msg.type={msg.type}, content_type={type(msg.content).__name__}, msg_text='{msg_text}', is_attachment={is_attachment}, inline_attachments={len(inline_attachments)}") + + msg.handled = True + login = user_id.split("@")[0] if "@" in user_id else user_id + cmd = msg_text.lower() + + # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ + for raw_num, emoji_num in EMOJI_DIGITS.items(): + if cmd == emoji_num: + cmd = raw_num + break + + # --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) --- + if cmd in ["0", "9", "/start", "меню"]: + log_menu_stats(user_id, "Service Desk", "Выход в главное меню") + states.clear_state(user_id) + sd_sessions.pop(user_id, None) + await msg.answer(MENU_TEXT, parse_mode="html") + return + + if user_id not in sd_sessions: + sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": []} + session = sd_sessions[user_id] + + # --- ШАГ 1: ОЖИДАНИЕ ТЕКСТА --- + if session["step"] == "need_text": + if not msg_text.strip() and not inline_attachments: + await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html") + return + + # Если только вложение без текста — сохраняем и ждем текст + if not msg_text.strip() and inline_attachments: + session["files_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text") + await msg.answer(SD_TEXT_REQUIRED, parse_mode="html") + return + + # Если пришел текст (с вложениями или без) + if inline_attachments: + session["files_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue") + + # Настраиваем параметры сессии для ожидания + session["step"] = "waiting_for_attachments" + session["msg_text"] = msg_text + session["msg"] = msg + session["user_id"] = user_id + session["login"] = login + + # Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота + await msg.answer(SD_CREATING, parse_mode="html") + + # Запускаем фоновый таймер с механизмом сброса (Debounce) + if "timer_task" in session and not session["timer_task"].done(): + session["timer_task"].cancel() + session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) + sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...") + return + + # --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) --- + if session["step"] == "waiting_for_attachments": + if inline_attachments: + session["files_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📎 [SD] Received {len(inline_attachments)} more files. Total queue: {len(session['files_queue'])}") + if msg_text.strip(): + session["msg_text"] = session["msg_text"] + " " + msg_text.strip() + sd_workflow_logger.info(f"📝 [SD] Additional text appended") + + # Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед) + if "timer_task" in session and not session["timer_task"].done(): + session["timer_task"].cancel() + session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session)) + sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s") + return + + # --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") --- + if session["step"] == "creating_ticket": + if inline_attachments: + if "post_create_queue" not in session: + session["post_create_queue"] = [] + session["post_create_queue"].extend(inline_attachments) + sd_workflow_logger.info(f"📥 [SD Hole-Fix] Captured {len(inline_attachments)} file(s) DURING ticket creation API call.") + return + + # --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ --- + if session["step"] == "ticket_created": + if inline_attachments: + ticket_id = session["ticket_id"] + for att in inline_attachments: + await msg.answer(sd_uploading_file(ticket_id), parse_mode="html") + ok = await process_and_upload_file(att['file_id'], att['file_name'], ticket_id) + sd_workflow_logger.info(f"📎 [Late File Upload] Name: {att['file_name']} -> Success: {ok}") + if ok: + log_menu_stats(user_id, "Service Desk", f"Добавление файла к заявке #{ticket_id}") + await msg.answer(sd_file_attached(), parse_mode="html") + else: + await msg.answer(sd_file_upload_error(), parse_mode="html") + elif msg_text.strip(): + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") + return \ No newline at end of file diff --git a/service_desk/logs/sd_workflow.log b/service_desk/logs/sd_workflow.log new file mode 100644 index 0000000..e970b95 --- /dev/null +++ b/service_desk/logs/sd_workflow.log @@ -0,0 +1,67 @@ +2026-07-02 18:59:04,145 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:48,065 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:49,603 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:51,983 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 18:59:58,388 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:02,571 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:03,647 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:15,574 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:00:47,620 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:06,814 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:10,483 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:23,003 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:23,003 - INFO - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает принтер', is_attachment=False, inline_attachments=0 +2026-07-02 19:01:23,018 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:23,018 - INFO - 🚀 [SD Debug] msg.type=MessageType.ATTACHMENT, content_type=AttachmentContent, msg_text='', is_attachment=True, inline_attachments=1 +2026-07-02 19:01:23,018 - INFO - 📎 [SD] Received 1 more files. Total queue: 1 +2026-07-02 19:01:23,018 - INFO - 🔄 [SD] Timer reset due to user activity. Waiting another 2.0s +2026-07-02 19:01:23,021 - INFO - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-02 19:01:25,024 - INFO - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-02 19:01:25,229 - INFO - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-02 19:01:25,621 - INFO - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает принтер'}], 'temperature': 0.0} +2026-07-02 19:01:34,653 - INFO - 📝 [Subject Gen] Text: не работает принтер... -> Subject: Не работает принтер +2026-07-02 19:01:35,020 - INFO - 🎫 [Ticket Created] ID: 23108 +2026-07-02 19:01:35,021 - INFO - 📎 [Upload] Uploading 1 file(s) to ticket #23108 +2026-07-02 19:01:35,328 - INFO - ✅ [Upload] All queued file(s) uploaded +2026-07-03 01:33:49,457 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 09:27:27,099 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:29,676 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:32,598 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:44,534 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:44,534 - INFO - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает интернет', is_attachment=False, inline_attachments=0 +2026-07-03 14:55:44,565 - INFO - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-03 14:55:46,568 - INFO - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-03 14:55:46,820 - INFO - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-03 14:55:47,278 - INFO - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает интернет'}], 'temperature': 0.0} +2026-07-03 14:55:55,269 - INFO - 📝 [Subject Gen] Text: не работает интернет... -> Subject: Не работает доступ в интернет +2026-07-03 14:55:55,504 - INFO - 🎫 [Ticket Created] ID: 23132 +2026-07-03 14:55:55,504 - ERROR - ❌ [SD Error] sd_ticket_created() missing 2 required positional arguments: 'subject' and 'description' +2026-07-03 15:00:29,654 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:00:34,389 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:00:34,389 - INFO - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает интернет', is_attachment=False, inline_attachments=0 +2026-07-03 15:00:34,398 - INFO - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-03 15:00:36,401 - INFO - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:00:36,644 - INFO - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-03 15:00:37,078 - INFO - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает интернет'}], 'temperature': 0.0} +2026-07-03 15:00:38,739 - INFO - 📝 [Subject Gen] Text: не работает интернет... -> Subject: Не работает доступ в интернет +2026-07-03 15:00:39,151 - INFO - 🎫 [Ticket Created] ID: 23133 +2026-07-03 15:07:19,766 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:07:25,800 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:07:25,800 - INFO - 🚀 [SD Debug] msg.type=MessageType.PLAIN_MESSAGE, content_type=TextContent, msg_text='не работает принтер', is_attachment=False, inline_attachments=0 +2026-07-03 15:07:25,807 - INFO - ⏳ [SD] Timer started: waiting 2.0s for potentially more attachments... +2026-07-03 15:07:27,810 - INFO - ⏰ [SD] Timeout reached. Initiating ticket creation for user ds.krivochenko@tcs.sibcem.ru +2026-07-03 15:07:28,053 - INFO - 👤 [AD Lookup] User: ds.krivochenko -> Found: True +2026-07-03 15:07:28,704 - INFO - 📝 [Subject Gen] Payload: {'model': 'gemma-4-E4B-it-Q4_K_M', 'messages': [{'role': 'system', 'content': "Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\nПРАВИЛА:\n1. Назови проблему и оборудование/программу (например: 'Самопроизвольное выключение ПК', 'Неисправность принтера', 'Сбой авторизации в почте').\n2. Отвечать на английском языке ЗАПРЕЩЕНО.\n3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ."}, {'role': 'user', 'content': 'Сформулируй краткую тему для следующей ИТ-заявки:\nне работает принтер'}], 'temperature': 0.0} +2026-07-03 15:07:30,662 - INFO - 📝 [Subject Gen] Text: не работает принтер... -> Subject: Не работает принтер +2026-07-03 15:07:30,833 - INFO - 🎫 [Ticket Created] ID: 23134 +2026-07-04 06:25:49,706 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:23:16,605 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:23:18,888 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:23:20,868 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:11,233 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:13,170 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:14,851 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 02:35:20,964 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-08 06:53:15,134 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-13 01:48:17,529 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru +2026-07-14 06:36:37,817 - INFO - 🚀 [SD Workflow Start] User: ds.krivochenko@tcs.sibcem.ru diff --git a/show_stats.py b/show_stats.py new file mode 100644 index 0000000..3c75704 --- /dev/null +++ b/show_stats.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# show_stats.py + +import os +import csv +from collections import defaultdict + +def main(): + # Динамически определяем корень папки, где лежит сам скрипт show_stats.py + base_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(base_dir, "logs", "menu_stats.csv") + + if not os.path.exists(log_file): + print(f"❌ Файл статистики еще не создан по пути: {log_file}") + print("💡 Сначала перейдите по пунктам меню в боте, чтобы накопились первые логи.") + return + + total_actions = 0 + main_menu_counts = defaultdict(int) + sub_menu_counts = defaultdict(lambda: defaultdict(int)) + cert_counts = defaultdict(lambda: defaultdict(int)) + + try: + with open(log_file, "r", encoding="utf-8-sig") as f: + reader = csv.reader(f, delimiter=";") + header = next(reader) + + for row in reader: + if len(row) < 4: + continue + timestamp, user_id, main_menu, sub_menu = row + + main_menu = main_menu.strip() + sub_menu = sub_menu.strip() + sub_menu_lower = sub_menu.lower() + + # ИСПРАВЛЕНО: Полностью исключаем фоновые логи безопасности 2FA и выходы из статистики кликов + if "выход" in sub_menu_lower or "2fa" in sub_menu_lower: + continue + + total_actions += 1 + main_menu_counts[main_menu] += 1 + + if sub_menu and sub_menu_lower != "вход": + if sub_menu_lower == "справки" or "справк" in sub_menu_lower: + parent_item = "Справки (Заказ документов)" + + if sub_menu_lower == "справки": + specific_cert = "📂 Открытие главного меню выбора" + elif "выбор справки:" in sub_menu_lower: + cert_title = sub_menu.split(":", 1)[1].strip() + specific_cert = f"👀 Просмотр/Выбор: {cert_title}" + elif "заказ справки:" in sub_menu_lower: + cert_title = sub_menu.split(":", 1)[1].strip() + specific_cert = f"🚀 Оформлен заказ: {cert_title}" + else: + specific_cert = sub_menu + + sub_menu_counts[main_menu][parent_item] += 1 + cert_counts[main_menu][specific_cert] += 1 + else: + sub_menu_counts[main_menu][sub_menu] += 1 + except Exception as e: + print(f"❌ Ошибка при чтении файла логов: {e}") + return + + if total_actions == 0: + print("📭 Файл статистики пуст после фильтрации выходов и логов 2FA.") + return + + print("=" * 70) + print(f"📊 СТАТИСТИКА ИСПОЛЬЗОВАНИЯ МЕНЮ БОТА (Всего переходов: {total_actions})") + print("=" * 70) + + sorted_main = sorted(main_menu_counts.items(), key=lambda x: x[1], reverse=True) + + for main_item, main_count in sorted_main: + main_pct = (main_count / total_actions) * 100 + print(f"\n📦 {main_item}") + print(f" └── Всего вызовов: {main_count} ({main_pct:.1f}% от общей активности)") + + if main_item in sub_menu_counts and sub_menu_counts[main_item]: + print(" └── Внутреннее распределение по подпунктам:") + + total_sub_clicks = sum(sub_menu_counts[main_item].values()) + sorted_subs = sorted(sub_menu_counts[main_item].items(), key=lambda x: x[1], reverse=True) + + for sub_item, sub_count in sorted_subs: + sub_pct = (sub_count / total_sub_clicks) * 100 + print(f" ├── {sub_item}: {sub_count} шт. ({sub_pct:.1f}%)") + + if sub_item == "Справки (Заказ документов)" and main_item in cert_counts: + total_certs = sum(cert_counts[main_item].values()) + sorted_certs = sorted(cert_counts[main_item].items(), key=lambda x: x[1], reverse=True) + + print(" │ └── 📑 Детализация по типам документов:") + for idx, (cert_title, cert_num) in enumerate(sorted_certs, 1): + # ИСПРАВЛЕНО: Локальный расчет процента перенесен строго внутрь итерации + cert_pct = (cert_num / total_certs) * 100 if total_certs > 0 else 0 + tree_prefix = "└──" if idx == len(sorted_certs) else "├──" + print(f" │ {tree_prefix} {cert_title}: {cert_num} шт. ({cert_pct:.1f}%)") + + print("\n" + "=" * 70) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/transcription_bot/handlers.py b/transcription_bot/handlers.py new file mode 100644 index 0000000..73cd146 --- /dev/null +++ b/transcription_bot/handlers.py @@ -0,0 +1,127 @@ +# /opt/trueconf_bot/transcription_bot/handlers.py +import io +import os +import asyncio +import logging +import httpx +from ldap3 import Server, Connection, ALL +from trueconf import Router, Message + +from config.config import * +from utils import states +from utils.menu import MENU_TEXT +from utils.texts import ( + EMOJI_DIGITS, + TRANSCRIPTION_MAIN_MENU_TEXT, + transcription_error_bad_extension, + TRANSCRIPTION_UPLOADING_TEXT, + transcription_success_received, + transcription_error_api, + transcription_error_system, + UNKNOWN_MAIN_CMD_TEXT, +) + +# 🔌 Импортируем централизованную функцию сбора статистики из main +from utils.stats_logger import log_menu_stats + +logger = logging.getLogger(__name__) +router = Router() + +# ✅ Полный список разрешенных расширений (Аудио, Видео, Проф. форматы) +ALLOWED_EXTENSIONS = [ + # Аудио + '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.oga', '.opus', '.wma', + '.aif', '.aiff', '.ac3', '.eac3', '.dts', '.thd', '.au', '.caf', + '.wv', '.ape', '.tta', '.wav', + # Видео + '.mp4', '.m4v', '.mkv', '.mka', '.mk3d', '.avi', '.mov', '.flv', + '.webm', '.wmv', '.ts', '.m2ts', '.mpg', '.mpeg', '.vob', + # Профессиональные и вещательные + '.mxf', '.gxf', '.nucap', + # Специфические + '.4xm', '.bik', '.smk', '.cak' +] + +# --- Функция получения Email из Active Directory --- +def get_user_email_sync(login: str) -> str: + try: + server = Server(AD_SERVER, get_info=ALL) + conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True) + conn.search(search_base=AD_BASE, search_filter=f"(sAMAccountName={login})", attributes=["mail"]) + if conn.entries and 'mail' in conn.entries[0] and conn.entries[0].mail.value: + return str(conn.entries[0].mail.value) + except Exception as e: + logger.error(f"Ошибка поиска email в AD: {e}") + return DEFAULT_REQUESTER + +@router.message() +async def transcription_handler(msg: Message): + user_id = msg.from_user.id + + # Работаем только если включен режим расшифровки + if states.get_state(user_id) != "TRANSCRIPTION_MODE": + return + + # Помечаем сообщение как обработанное + msg.handled = True + + user_login = user_id.split('@')[0] if '@' in user_id else user_id + msg_text = getattr(msg.content, "text", "").strip() if hasattr(msg.content, "text") else "" + is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT" + + cmd = msg_text.lower() + + # 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ + for raw_num, emoji_num in EMOJI_DIGITS.items(): + if cmd == emoji_num: + cmd = raw_num + break + + # --- ВЫХОД (Кнопки 0 и 9 приведены к единому стандарту) --- + if cmd in ["0", "9", "/start", "меню"]: + log_menu_stats(user_id, "Speech-to-Text", "Выход в главное меню") + states.clear_state(user_id) + await msg.answer(MENU_TEXT, parse_mode="html") + return + + # --- ОБРАБОТКА ФАЙЛА --- + if is_attachment: + file_name = getattr(msg.content, "file_name", "").lower() + file_id = getattr(msg.content, "file_id", None) + + # Проверка расширения + ext = os.path.splitext(file_name)[1] + if ext not in ALLOWED_EXTENSIONS: + await msg.answer(transcription_error_bad_extension(ext), parse_mode="html") + return + + await msg.answer(TRANSCRIPTION_UPLOADING_TEXT, parse_mode="html") + try: + # 1. Добываем почту из AD + user_email = await asyncio.to_thread(get_user_email_sync, user_login) + + # 2. Качаем файл (возвращает bytes) + downloaded_file = await states.tc_bot.download_file_by_id(file_id) + + # 3. Отправка на API + async with httpx.AsyncClient(timeout=120.0) as client: + files = {'file': (file_name, io.BytesIO(downloaded_file), 'application/octet-stream')} + payload = { + 'email': user_email, + 'generate_summary': 'true' + } + response = await client.post(TRANSCRIPTION_API_URL, files=files, data=payload) + + if response.status_code == 200: + log_menu_stats(user_id, "Speech-to-Text", f"Отправка встречи на расшифровку ({ext})") + await msg.answer(transcription_success_received(user_email), parse_mode="html") + else: + await msg.answer(transcription_error_api(response.status_code), parse_mode="html") + + except Exception as e: + logger.error(f"Transcription Upload Error: {e}") + await msg.answer(transcription_error_system(), parse_mode="html") + return + + # --- ЗАГЛУШКА НА НЕИЗВЕСТНЫЙ ТЕКСТ / СТАНДАРТНАЯ ОШИБКА ВВОДА --- + await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html") diff --git a/trash_api.py b/trash_api.py new file mode 100644 index 0000000..c0d5dfc --- /dev/null +++ b/trash_api.py @@ -0,0 +1,36 @@ +from flask import Flask, request, jsonify +import os + +app = Flask(__name__) + +# Папка назначения +UPLOAD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trash_api_uploads") + +# Проверяем наличие папки +if not os.path.exists(UPLOAD_DIR): + os.makedirs(UPLOAD_DIR, exist_ok=True) + +@app.route('/upload', methods=['POST']) +def upload_file(): + # Проверяем, есть ли файл в запросе + if 'file' not in request.files: + return jsonify({"status": "error", "message": "Файл не найден в запросе"}), 400 + + file = request.files['file'] + + # Если пользователь не выбрал файл + if file.filename == '': + return jsonify({"status": "error", "message": "Имя файла пустое"}), 400 + + if file: + file_path = os.path.join(UPLOAD_DIR, file.filename) + file.save(file_path) + return jsonify({ + "status": "success", + "filename": file.filename, + "path": file_path + }), 200 + +if __name__ == '__main__': + # Запускаем на всех интерфейсах (0.0.0.0) на порту 8000 + app.run(host='0.0.0.0', port=8000) \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/embedding_client.py b/utils/embedding_client.py new file mode 100644 index 0000000..c1ea2ba --- /dev/null +++ b/utils/embedding_client.py @@ -0,0 +1,26 @@ +import requests +import config.config as config + +def get_embedding(text, model_name="multilingual-e5-base"): + """ + Заменяет model.encode(text). Вместо локальной загрузки + отправляет запрос к API-серверу. + """ + url = f"http://127.0.0.1:8010/embed" + headers = {"Content-Type": "application/json"} + + # Если в конфиге задан токен, добавляем его + if hasattr(config, 'EMBEDDING_API_TOKEN') and config.EMBEDDING_API_TOKEN: + headers["X-Embedding-Token"] = config.EMBEDDING_API_TOKEN + + payload = { + "model": model_name, + "input_type": "query", # или "passage", зависит от того, что кодируешь + "texts": [text] + } + + response = requests.post(url, json=payload, headers=headers) + if response.status_code == 200: + return response.json()["embeddings"][0] + else: + raise Exception(f"Ошибка API эмбеддингов: {response.text}") \ No newline at end of file diff --git a/utils/menu.py b/utils/menu.py new file mode 100644 index 0000000..5109646 --- /dev/null +++ b/utils/menu.py @@ -0,0 +1,79 @@ +# /opt/trueconf_bot/utils/menu.py +import config.config as config + +MENU_MAP = {} +MENU_TEXT = "" +WELCOME_TEXT = "" # 💡 Добавили шаблон для развернутого приветствия + +def build_dynamic_menu(): + global MENU_MAP, MENU_TEXT, WELCOME_TEXT + MENU_MAP.clear() + + # Старый формат keycap-цифр (как в VK) + emoji_digits = { + 1: "1⃣", + 2: "2⃣", + 3: "3⃣", + 4: "4⃣", + 5: "5⃣", + 6: "6⃣", + 7: "7⃣", + 8: "8⃣", + 9: "9⃣", + 10: "🔟", + } + + dynamic_items = [] + counter = 1 + + # Безопасно проверяем флаги из конфига и наполняем массив пунктов + if getattr(config, 'ENABLE_LK', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "LK" + MENU_MAP[num_char] = "LK" + dynamic_items.append(f"{num_char} — 👤 Личный кабинет") + counter += 1 + + if getattr(config, 'ENABLE_TRANSCRIPTION', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "TRANSCRIPTION" + MENU_MAP[num_char] = "TRANSCRIPTION" + dynamic_items.append(f"{num_char} — 🎙 Расшифровка совещаний") + counter += 1 + + if getattr(config, 'ENABLE_SERVICE_DESK', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "SD" + MENU_MAP[num_char] = "SD" + dynamic_items.append(f"{num_char} — 🎫 Создать заявку в Service Desk") + counter += 1 + + if getattr(config, 'ENABLE_PHOTO_BOT', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "PHOTO" + MENU_MAP[num_char] = "PHOTO" + dynamic_items.append(f"{num_char} — Изменить фото профиля") + counter += 1 + + if getattr(config, 'ENABLE_SEARCH_BOT', True): + num_char = emoji_digits.get(counter, str(counter)) + MENU_MAP[str(counter)] = "SEARCH" + MENU_MAP[num_char] = "SEARCH" + dynamic_items.append(f"{num_char} — Поиск по регламентам") + counter += 1 + + if counter == 1: + dynamic_items.append("В данный момент бот находится на техническом обслуживании.") + + # 1️⃣ Сценарий Б: Короткий шаблон для возврата из подмодулей + MENU_TEXT = "🍀 Главное меню:\n\n" + "\n".join(dynamic_items) + + # 2️⃣ Сценарий А: Длинный приветственный шаблон для старта/таймаута (с исправленной грамматикой) + WELCOME_TEXT = ( + "🍀 Здравствуйте! Я КЛЕВЕР — ваш персональный цифровой ассистент.\n\n" + "С моей помощью вы можете быстро получить доступ к нижеперечисленным корпоративным сервисам.\n\n" + "👇 Пожалуйста, отправьте мне в чат цифру, соответствующую нужному пункту меню:\n\n" + ) + "\n".join(dynamic_items) + +# Gенерируем меню один раз при импорте файла +build_dynamic_menu() \ No newline at end of file diff --git a/utils/otp_service.py b/utils/otp_service.py new file mode 100644 index 0000000..d3bdf65 --- /dev/null +++ b/utils/otp_service.py @@ -0,0 +1,133 @@ +# /opt/trueconf_bot/utils/otp_service.py +import re +import secrets +import logging +import httpx +import urllib3 +from ldap3 import Server, Connection, ALL +from ldap3.utils.conv import escape_filter_chars +import config.config as config + +# Отключаем во внутреннем контуре предупреждения об SSL +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +logger = logging.getLogger(__name__) + +def generate_otp_code(length: int = 4) -> str: + """Генерирует криптографически безопасный цифровой код""" + return "".join(secrets.choice("0123456789") for _ in range(length)) + +async def send_otp_via_api(phone: str, code: str) -> tuple: + """ + Асинхронно отправляет код на шлюз apisms. + Парсит XML-ответ и определяет фактический канал доставки (MAX, Telegram, СМС). + Возвращает: (True/False, "канал_доставки или текст_ошибки") + """ + url = "https://mobileapp.sibcem.ru/MobDriveService/MobDriveService/MobService.asmx/SendSMS" + params = { + "_number": phone, + "_message": f"Код авторизации Сибцем : {code}" + } + try: + async with httpx.AsyncClient(verify=False) as client: + response = await client.get(url, params=params, timeout=40.0) + + if response.status_code in [200, 201]: + xml_content = response.text + logger.info(f"Сырой ответ OTP-шлюза: {xml_content}") + + # Извлекаем текст внутри тега ... + match = re.search(r']*>(.*?)', xml_content, re.DOTALL) + if match: + inner_text = match.group(1).strip() + inner_lower = inner_text.lower() + + # Умное определение канала на основе ключевых слов шлюза Сибцема + if "max" in inner_lower: + channel_info = "в MAX" + elif "telegram" in inner_lower or "телеграм" in inner_lower: + channel_info = "в Telegram" + elif "смс" in inner_lower or "sms" in inner_lower or "yeastar" in inner_lower or "шлюз" in inner_lower: + channel_info = "по СМС" + else: + channel_info = "по СМС" # Дефолтный фоллбэк, если текст изменится + else: + channel_info = "по СМС" + + return True, channel_info + + logger.error(f"❌ Шлюз API вернул ошибку {response.status_code}: {response.text}") + return False, f"Ошибка сервера шлюза ({response.status_code})" + + except Exception as e: + logger.error(f"❌ Критический сбой сети при отправке OTP на {phone}: {e}") + return False, str(e) + +def get_card_id_from_ad(user_id: str) -> str: + """Синхронно вытягивает CARD_ID сотрудника из Active Directory (extensionAttribute2)""" + try: + server = Server(config.AD_SERVER, get_info=ALL) + conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True) + safe_user_id = escape_filter_chars(user_id) + short_username = user_id.split('@')[0] + + search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))" + conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2']) + + if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value: + return str(conn.entries[0].extensionAttribute2.value).strip() + raise Exception("Поле extensionAttribute2 не заполнено в AD.") + except Exception as e: + logger.error(f"Ошибка получения CARD_ID из AD для {user_id}: {e}") + raise Exception(f"Не удалось извлечь CARD_ID из AD: {e}") + +def get_phone_from_db(card_id: str) -> str: + """Синхронно вытягивает телефон сотрудника из БД BossCopy (таблица UOV_SELFSERVICE_PR_CARD) по CARD_ID""" + import pymssql + try: + conn = pymssql.connect( + server=config.SQL_SERVER, + user=config.DB_USER, + password=config.DB_PASSWORD, + database=config.SQL_DB_NAME, + charset="cp1251", + ) + cursor = conn.cursor() + + # Телефон берётся из поля PHONE таблицы UOV_SELFSERVICE_PR_CARD + query = """ + SELECT PHONE FROM UOV_SELFSERVICE_PR_CARD WHERE ID = %s + """ + cursor.execute(query, (card_id,)) + row = cursor.fetchone() + conn.close() + + if not row or not row[0]: + raise Exception(f"Телефон не найден для CARD_ID {card_id} в UOV_SELFSERVICE_PR_CARD") + + phone = str(row[0]).strip() + # Очищаем номер от пробелов, дефисов, скобок + phone = phone.replace(" ", "").replace("-", "").replace("(", "").replace(")", "") + return phone + except Exception as e: + logger.error(f"Ошибка получения телефона из БД для CARD_ID {card_id}: {e}") + raise Exception(f"Не удалось извлечь номер телефона из БД BossCopy: {e}") + +def get_user_phone_from_ad(user_id: str) -> str: + """ + Синхронно вытягивает телефон сотрудника: + 1. Получает CARD_ID из Active Directory (extensionAttribute2) + 2. Получает телефон из БД BossCopy (таблица UOV_SELFSERVICE_PR_CARD) по CARD_ID + """ + try: + # Шаг 1: Получаем CARD_ID из AD + card_id = get_card_id_from_ad(user_id) + logger.info(f"✅ CARD_ID для {user_id}: {card_id}") + + # Шаг 2: Получаем телефон из БД по CARD_ID + phone = get_phone_from_db(card_id) + logger.info(f"✅ Телефон для CARD_ID {card_id}: {phone}") + + return phone + except Exception as e: + logger.error(f"Ошибка получения телефона для {user_id}: {e}") + raise Exception(f"Не удалось извлечь номер телефона: {e}") diff --git a/utils/states.py b/utils/states.py new file mode 100644 index 0000000..b097ca6 --- /dev/null +++ b/utils/states.py @@ -0,0 +1,47 @@ +# /opt/trueconf_bot/utils/states.py +import time +import config.config as config + +user_states = {} +tc_bot = None # Общая переменная для экземпляра бота + +# --- Константы состояний --- +PHOTO_MODE = "PHOTO_MODE" +SD_MODE = "SD_MODE" +TRANSCRIPTION_MODE = "TRANSCRIPTION_MODE" +SEARCH_MODE = "SEARCH_MODE" + +def set_state(user_id, state): + """Фиксирует новое состояние пользователя и обновляет метку времени активности""" + user_states[user_id] = { + "state": state, + "last_active": time.time() + } + +def get_state(user_id): + """ + Возвращает текущее состояние пользователя. + Если время неактивности превысило лимит из config.py, сессия сбрасывается. + """ + if user_id in user_states: + session = user_states[user_id] + + # Динамически забираем значение таймаута из конфига (дефолт — 1800 сек / 30 мин) + session_timeout = getattr(config, 'SESSION_TIMEOUT', 1800) + + # Проверяем, сколько секунд прошло с момента последнего сообщения + if time.time() - session["last_active"] > session_timeout: + # Время вышло! Аппаратно сносим сессию навигации + del user_states[user_id] + return None + + # Если время не вышло, обновляем таймер (пользователь проявил активность) + session["last_active"] = time.time() + return session["state"] + + return None + +def clear_state(user_id): + """Принудительно очищает состояние пользователя (выход в корень)""" + if user_id in user_states: + del user_states[user_id] \ No newline at end of file diff --git a/utils/stats_logger.py b/utils/stats_logger.py new file mode 100644 index 0000000..ca4e5d2 --- /dev/null +++ b/utils/stats_logger.py @@ -0,0 +1,24 @@ +# utils/stats_logger.py +import os +import csv +from datetime import datetime + +def log_menu_stats(user_id: str, main_menu: str, sub_menu: str = ""): + """Записывает действие пользователя в лог статистики динамически в зависимости от среды""" + try: + # Динамически определяем корень проекта (на один уровень выше папки utils) + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + log_dir = os.path.join(base_dir, "logs") + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, "menu_stats.csv") + + file_exists = os.path.isfile(log_file) + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + with open(log_file, "a", newline="", encoding="utf-8-sig") as f: + writer = csv.writer(f, delimiter=";") + if not file_exists: + writer.writerow(["Timestamp", "User_ID", "Main_Menu", "Sub_Menu"]) + writer.writerow([timestamp, user_id, main_menu, sub_menu]) + except Exception as e: + print(f"Ошибка записи статистики меню: {e}") \ No newline at end of file diff --git a/utils/texts.py b/utils/texts.py new file mode 100644 index 0000000..311a233 --- /dev/null +++ b/utils/texts.py @@ -0,0 +1,571 @@ +# /opt/trueconf_bot/utils/texts.py +# Все текстовые сообщения бота, отправляемые пользователю + +# ========================================================= +# GLOBAL CONSTANTS +# ========================================================= + +# --- Main Bot --- +# Общее для главного бота +UNKNOWN_MAIN_CMD_TEXT = ( + "⚠️ Неизвестная команда.\n\n" + "Пожалуйста, используйте только цифры, соответствующие нужным пунктам меню, и отправьте выбранную цифру мне в сообщении." +) + +# --- Emoji Digits --- +# Используются для навигации и выбора пунктов меню +EMOJI_DIGITS = { + "0": "0⃣", + "1": "1⃣", + "2": "2⃣", + "3": "3⃣", + "4": "4⃣", + "5": "5⃣", + "6": "6⃣", + "7": "7⃣", + "8": "8⃣", + "9": "9⃣", + "10": "🔟" +} + +# --- Footer Templates --- +# Шаблоны подвалов сообщений для навигации + +def _footer(emojis: dict) -> str: + """Стандартный футер с кнопками Назад и Меню""" + return f"\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _footer_main_only(emojis: dict) -> str: + """Футер только с кнопкой Меню""" + return f"\n\n{emojis['0']} — В главное меню" + + +# ========================================================= +# TRANSCRIPTION BOT +# ========================================================= +# Модуль для расшифровки аудио и видео файлов + +# --- Private functions (Internal logic) --- + +def _transcription_welcome(emojis: dict) -> str: + return ( + f"🎙 Добро пожаловать! Я — Ассистент расшифровки совещаний.\n\n" + f"Я помогу вам перевести аудио или видео в текст, а также подготовлю краткое резюме вашей встречи.\n\n" + f"📎 Для начала работы просто отправьте нужный файл прямо в этот чат.\n\n" + f"{emojis['0']} — В главное меню" + ) + +def _transcription_unknown(emojis: dict) -> str: + return ( + f"⚠️ Неизвестная команда.\n\n" + f"Пожалуйста, отправьте в сообщении мне нужную цифру соответствующего пункта меню в чат." + ) + +def _transcription_bad_extension(ext: str, emojis: dict) -> str: + return ( + f"❌ Ошибка: Недопустимый формат файла ({ext}).\n\n" + f"Пожалуйста, отправьте поддерживаемый аудио или видео файл.\n\n" + f"{_footer(emojis)}" + ) + +def _transcription_uploading() -> str: + return "⏳ Загружаю файл..." + +def _transcription_success(email: str, emojis: dict) -> str: + return ( + f"✅ Хорошо, файл принят в обработку.\n\n" + f"Как только всё будет готово, я отправлю вам результаты на почту ({email}).\n\n" + f"⏳ Обычно это занимает от 5 до 30 минут.\n\n" + f"{_footer(emojis)}" + ) + +def _transcription_api_error(status_code: int, emojis: dict) -> str: + return f"❌ Ошибка API сервера ({status_code}).{_footer(emojis)}" + +def _transcription_system_error(emojis: dict) -> str: + return f"❌ Произошла системная ошибка при передаче файла.{_footer(emojis)}" + +# --- Public constants and functions --- + +TRANSCRIPTION_MAIN_MENU_TEXT = _transcription_welcome(EMOJI_DIGITS) +TRANSCRIPTION_UNKNOWN_CMD_TEXT = _transcription_unknown(EMOJI_DIGITS) + +def transcription_error_bad_extension(ext: str) -> str: + return _transcription_bad_extension(ext, EMOJI_DIGITS) + +TRANSCRIPTION_UPLOADING_TEXT = _transcription_uploading() + +def transcription_success_received(email: str) -> str: + return _transcription_success(email, EMOJI_DIGITS) + +def transcription_error_api(status_code: int) -> str: + return _transcription_api_error(status_code, EMOJI_DIGITS) + +def transcription_error_system() -> str: + return _transcription_system_error(EMOJI_DIGITS) + + +# ========================================================= +# SERVICE DESK BOT +# ========================================================= +# Модуль для создания и управления заявками в Service Desk + +# --- Private functions (Internal logic) --- + +def _sd_welcome(emojis: dict) -> str: + return ( + f"🎫 Режим создания заявки в Service Desk\n\n" + f"• Опишите проблему текстом.\n" + f"• Можно прикрепить фото или документ.\n\n" + f"{emojis['0']} — В главное меню" + ) + +def _sd_unknown(emojis: dict) -> str: + return ( + f"⚠️ Неизвестная команда.\n" + f"Пожалуйста, используйте цифры меню из сообщения выше.\n\n" + f"{emojis['9']} — Назад\n" + f"{emojis['0']} — В главное меню" + ) + +def _sd_text_required(emojis: dict) -> str: + return f"⚠️ Сначала опишите проблему текстом." + +def _sd_creating() -> str: + return "⏳ Создаю заявку, пожалуйста, подождите..." + +def _sd_ticket_created(ticket_id: int, subject: str, description: str, emojis: dict) -> str: + clean_desc = description.replace("\n", " ").strip() + if len(clean_desc) > 150: + clean_desc = clean_desc[:147] + "..." + return ( + f"✅ Заявка #{ticket_id} создана!\n\n" + + f"📌 Тема: {subject}\n\n" + f"📝 Текст: {clean_desc}\n\n" + + f"Если нужно добавить скриншоты или документ — отправьте их сейчас.\n\n" + f"{_footer(emojis)}" + ) + +def sd_ticket_created(ticket_id: int, subject: str, description: str) -> str: + return _sd_ticket_created(ticket_id, subject, description, EMOJI_DIGITS) + +def _sd_system_error(emojis: dict) -> str: + return f"❌ Системная ошибка. Попробуйте позже.{_footer_main_only(emojis)}" + +def _sd_uploading_file(ticket_id: int, emojis: dict) -> str: + return f"⏳ Загружаю файл в заявку #{ticket_id}..." + +def _sd_file_attached(emojis: dict) -> str: + return f"✅ Файл успешно прикреплен!{_footer(emojis)}" + +def _sd_file_upload_error(emojis: dict) -> str: + return f"❌ Ошибка загрузки файла.{_footer_main_only(emojis)}" + +# --- Public constants and functions --- + +SD_MAIN_MENU_TEXT = _sd_welcome(EMOJI_DIGITS) +SD_UNKNOWN_CMD_TEXT = _sd_unknown(EMOJI_DIGITS) +SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS) +SD_CREATING = _sd_creating() + +def sd_ticket_create_error() -> str: + return _sd_ticket_create_error(EMOJI_DIGITS) + +def sd_system_error() -> str: + return _sd_system_error(EMOJI_DIGITS) + +def sd_uploading_file(ticket_id: int) -> str: + return _sd_uploading_file(ticket_id, EMOJI_DIGITS) + +def sd_file_attached() -> str: + return _sd_file_attached(EMOJI_DIGITS) + +def sd_file_upload_error() -> str: + return _sd_file_upload_error(EMOJI_DIGITS) + + +# ========================================================= +# PHOTO BOT +# ========================================================= +# Модуль для обновления фото профиля + +# --- Private functions (Internal logic) --- + +def _photo_welcome(emojis: dict) -> str: + return ( + f"🖼 Режим обновления фото профиля\n\n" + f"📸 Пришлите мне вашу фотографию.\n\n" + f"{emojis['0']} — В главное меню" + ) + +def _photo_unknown(emojis: dict) -> str: + return ( + f"⚠️ Неизвестная команда.\n" + f"Пожалуйста, используйте цифры меню из сообщения выше.\n\n" + f"{emojis['9']} — Назад\n" + f"{emojis['0']} — В главное меню" + ) + +def _photo_error_handling(emojis: dict) -> str: + return ( + f"⚠️ В данный момент выполнить это действие невозможно. " + f"Информация о проблеме передана ответственным.\n" + f"Приносим извинения за доставленные неудобства.\n\n" + f"{emojis['0']} — В главное меню" + ) + +def _photo_uploading() -> str: + return "⏳ Отправка..." + +def _photo_success_sent() -> str: + return "📩 Фото отправлено на утверждение." + +def _photo_cancel(emojis: dict) -> str: + return ( + f"🚫 Отменено. Пришлите другое фото.\n\n" + f"{emojis['0']} — В главное меню" + ) + +def _photo_processing() -> str: + return "⏳ Обработка..." + +def _photo_preview_ready(emojis: dict) -> str: + return ( + f"... Фото готово!\n\n" + f"{emojis['1']} — Отправить на утверждение\n" + f"{emojis['2']} — Попробовать другое\n\n" + f"{emojis['0']} — В главное меню" + ) + +def _photo_error_processing(error_text: str, emojis: dict) -> str: + return ( + f"❌ Ошибка: {error_text}\n\n" + f"Пожалуйста, отправьте другое фото.\n\n" + f"{emojis['0']} — В главное меню" + ) + +# --- Public constants and functions --- + +PHOTO_MAIN_MENU_TEXT = _photo_welcome(EMOJI_DIGITS) +PHOTO_UNKNOWN_CMD_TEXT = _photo_unknown(EMOJI_DIGITS) + +def photo_error_handling() -> str: + return _photo_error_handling(EMOJI_DIGITS) + +PHOTO_UPLOADING_TEXT = _photo_uploading() + +def photo_success_sent() -> str: + return _photo_success_sent() + +def photo_cancel_text() -> str: + return _photo_cancel(EMOJI_DIGITS) + +PHOTO_PROCESSING_TEXT = _photo_processing() + +def photo_preview_ready() -> str: + return _photo_preview_ready(EMOJI_DIGITS) + +def photo_error_processing(error_text: str) -> str: + return _photo_error_processing(error_text, EMOJI_DIGITS) + + +# ========================================================= +# SEARCH BOT +# ========================================================= +# Модуль поиска по регламентам + +# --- Private functions (Internal logic) --- + +def _search_welcome(emojis: dict) -> str: + return ( + f"📚 Поиск по регламентам\n\n" + f"Введите ваш вопрос (например: 'время хранения электронных документов' ).\n" + f"Бот найдет наиболее подходящие абзацы из нормативных документов.\n\n" + f"Для возврата в главное меню отправьте {emojis['0']}." + ) + +def _search_unknown(emojis: dict) -> str: + return ( + f"⚠️ Неизвестная команда.\n" + f"Пожалуйста, используйте цифры меню из сообщения выше.\n\n" + f"{emojis['9']} — Назад\n" + f"{emojis['0']} — В главное меню" + ) + +def _search_searching() -> str: + return "🔍 Ищу информацию в регламентах..." + +def _search_not_found(emojis: dict) -> str: + return ( + f"🤖 Точной информации в регламентах не найдено.\n\n" + f"Для выхода отправьте {emojis['0']}." + ) + +def _search_feedback_thanks(emojis: dict) -> str: + return ( + f"🙏 Спасибо за ваш отзыв! Мы передали его разработчикам.\n\n" + f"🔍 Можете сразу задать следующий вопрос по регламентам.\n\n" + f"Для выхода отправьте {emojis['0']}." + ) + +def _search_feedback_info(emojis: dict) -> str: + return ( + f"⚠️ Информация передана.\n\n" + f"Если желаете, напишите прямо сейчас текстовым сообщением, что конкретно вам не понравилось в ответе.\n\n" + f"{emojis['1']} — Задать ещё вопрос\n" + f"{emojis['0']} — Выйти в главное меню" + ) + +def _search_unavailable(emojis: dict) -> str: + return ( + "⚠️ В данный момент выполнить это действие невозможно. " + "Информация о проблеме передана ответственным.\n" + "Приносим извинения за доставленные неудобства.\n\n" + f"{emojis['0']} — В главное меню" + ) + +# --- Public constants and functions --- + +SEARCH_MAIN_MENU_TEXT = _search_welcome(EMOJI_DIGITS) +SEARCH_UNKNOWN_CMD_TEXT = _search_unknown(EMOJI_DIGITS) + +def search_searching_text() -> str: + return _search_searching() + +def search_not_found_text(emojis: dict) -> str: + return _search_not_found(emojis) + +def search_feedback_thanks_text(emojis: dict) -> str: + return _search_feedback_thanks(emojis) + +def search_feedback_info_text(emojis: dict) -> str: + return _search_feedback_info(emojis) + +def search_unavailable_action(emojis: dict) -> str: + return _search_unavailable(emojis) + + +# ========================================================= +# LK BOT +# ========================================================= +# Модуль Личного Кабинета (финансовые документы, табель и др.) + +# --- Constants --- + +DOC_TYPES = { + "1": "Справка о доходах физического лица (2-НДФЛ)", + "2": "Справка об удержаниях за ДМС", + "3": "Справка о средней заработной плате", + "4": "Справка о получении единовременного пособия по рождению ребенка", + "5": "Справка о получении ежемесячного пособия и назначении отпуска по уходу за ребенком", + "6": "Справка об удержаниях по исполнительным листам", + "7": "Копия трудовой книжки" +} + +# --- Private functions (Internal logic) --- + +def _lk_error_handling(emojis: dict) -> str: + return ( + "⚠️ В данный момент выполнить это действие невозможно. Информация о проблеме передана ответственным. Приносим извинения за доставленные неудобства.\n\n" + f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + ) + +def _lk_auth_processing(emojis: dict) -> str: + return "⏳ Проверка безопасности... Запрашиваю контакты авторизации..." + +def _lk_auth_success(emojis: dict) -> str: + return "⏳ Авторизация успешна. Загружаю детализацию расчетного листа..." + +def _lk_auth_2fa_expired(emojis: dict) -> str: + return f"⏱ Время действия кода подтверждения изменилось или истекло.\nПожалуйста, запросите расчетный лист заново.{emojis['9']} — Назад" + +def _lk_auth_2fa_wrong_code(emojis: dict, attempts_left: int) -> str: + return ( + f"⚠️ Неверный код подтверждения.\nОсталось попыток: {attempts_left}. Попробуйте еще раз или вернитесь назад.\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + ) + +def _lk_auth_2fa_blocked_max(emojis: dict, minutes: int) -> str: + return ( + f"❌ Превышено максимальное количество попыток ввода кода.\nДоступ к финансовым документам заблокирован на {minutes} минут.
\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + ) + +def _lk_payslip_loading(emojis: dict) -> str: + return "⏳ Формирую архив расчетных листков..." + +def _lk_vacation_loading(emojis: dict) -> str: + return "⏳ Формирую справку по отпусках..." + +def _lk_sick_loading(emojis: dict) -> str: + return "⏳ Загружаю историю больничных листов..." + +def _lk_missions_loading(emojis: dict) -> str: + return "⏳ Загружаю историю командировок..." + +def _lk_rewards_loading(emojis: dict) -> str: + return "⏳ Ищу информацию о ваших наградах..." + +def _lk_transfers_loading(emojis: dict) -> str: + return "⏳ Загружаю историю кадровых перемещений..." + +def _lk_tabel_loading(emojis: dict) -> str: + return "⏳ Рассчитываю баланс рабочего времени..." + +def _lk_auth_sending(emojis: dict) -> str: + return "⏳ Отправка заявки..." + +def _lk_success_submission(emojis: dict) -> str: + return ( + "✅ Ваша заявка успешно отправлена!\n" + "Специалисты свяжутся с вами по готовности документа.\n\n" + f"{emojis['9']} — Назад в список справок\n{emojis['0']} — В главное меню" + ) + +def _lk_main_menu_text(header: str, emojis: dict) -> str: + return ( + f"{header}\n" + f"{emojis['1']} — Расчётный лист\n" + f"{emojis['2']} — Информация о днях отпуска\n" + f"{emojis['3']} — Больничные\n" + f"{emojis['4']} — Командировки\n" + f"{emojis['5']} — Награды\n" + f"{emojis['6']} — Переводы\n" + f"{emojis['7']} — Табель учёта рабочего времени\n" + f"{emojis['8']} — Справки\n\n" + f"{emojis['0']} — Вернуться в главное меню" + ) + +def _lk_tabel_months_menu_text(prev_1, prev_2, months_ru, emojis: dict) -> str: + return ( + "⏱ Выберите месяц для просмотра табеля:\n\n" + f"{emojis['1']} — {months_ru[prev_1.month]} {prev_1.year} г.\n" + f"{emojis['2']} — {months_ru[prev_2.month]} {prev_2.year} г.\n\n" + f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + ) + +def _lk_payslip_months_menu_text(months_data: list, months_ru: dict, emojis: dict) -> str: + menu_text = "📅 Выберите месяц для просмотра:\n\n" + for idx, cmonth in enumerate(months_data, start=1): + month_name = months_ru.get((cmonth - 1) % 12 + 1, '00') + year = (cmonth - 1) // 12 + menu_text += f"{emojis[str(idx)]} — {month_name} {year} г.\n" + menu_text += f"\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + return menu_text + +def _lk_docs_quantity_prompt(emojis: dict) -> str: + return f"Укажите необходимое количество экземпляров (максимум — 5 шт.):\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_docs_year_prompt(emojis: dict) -> str: + return f"📅 Выберите год, за который требуется справка:\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_docs_avg_salary_prompt(emojis: dict) -> str: + return f"Укажите количество месяцев для расчета средней ЗП (например: 3):\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_docs_child_age_prompt(emojis: dict) -> str: + return f"👶 Выберите возраст ребенка (до скольки лет):\n\n{emojis['1']} — до 1.5 лет\n{emojis['2']} — до 3 лет\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_docs_period_prompt(year: int, emojis: dict) -> str: + return f"за {year} год\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_docs_child_age_options(emojis: dict) -> str: + return f"{emojis['1']} — до 1.5 лет\n{emojis['2']} — до 3 лет\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_docs_period_options(emojis: dict) -> str: + return f"{emojis['1']} — до 1.5 лет\n{emojis['2']} — до 3 лет\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +def _lk_nav_text(emojis: dict) -> str: + return f"\n\n{emojis['9']} — Назад\n{emojis['0']} — В главное меню" + +# --- Public constants and functions --- + +LK_DOCS_MENU_TEXT = ( + "\n".join([f"{EMOJI_DIGITS[k]} — {v}" for k, v in DOC_TYPES.items()]) + + f"\n\n{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню" +) +LK_UNKNOWN_CMD_TEXT = ( + "⚠️ Неизвестная команда.\n" + "Пожалуйста, выберите пункт меню, используя цифры или кнопки.
\n" + f"{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню" +) + +def lk_error_handling(emojis: dict) -> str: + return _lk_error_handling(emojis) + +def lk_auth_processing(emojis: dict) -> str: + return _lk_auth_processing(emojis) + +def lk_auth_success(emojis: dict) -> str: + return _lk_auth_success(emojis) + +def lk_auth_2fa_expired(emojis: dict) -> str: + return _lk_auth_2fa_expired(emojis) + +def lk_auth_2fa_wrong_code(emojis: dict, attempts_left: int) -> str: + return _lk_auth_2fa_wrong_code(emojis, attempts_left) + +def lk_auth_2fa_blocked_max(emojis: dict, minutes: int) -> str: + return _lk_auth_2fa_blocked_max(emojis, minutes) + +def lk_payslip_loading(emojis: dict) -> str: + return _lk_payslip_loading(emojis) + +def lk_vacation_loading(emojis: dict) -> str: + return _lk_vacation_loading(emojis) + +def lk_sick_loading(emojis: dict) -> str: + return _lk_sick_loading(emojis) + +def lk_missions_loading(emojis: dict) -> str: + return _lk_missions_loading(emojis) + +def lk_rewards_loading(emojis: dict) -> str: + return _lk_rewards_loading(emojis) + +def lk_transfers_loading(emojis: dict) -> str: + return _lk_transfers_loading(emojis) + +def lk_tabel_loading(emojis: dict) -> str: + return _lk_tabel_loading(emojis) + +def lk_auth_sending(emojis: dict) -> str: + return _lk_auth_sending(emojis) + +def lk_success_submission(emojis: dict) -> str: + return _lk_success_submission(emojis) + +def lk_main_menu_text(header: str, emojis: dict) -> str: + return _lk_main_menu_text(header, emojis) + +def lk_tabel_months_menu_text(prev_1, prev_2, months_ru, emojis: dict) -> str: + return _lk_tabel_months_menu_text(prev_1, prev_2, months_ru, emojis) + +def lk_payslip_months_menu_text(months_data: list, months_ru: dict, emojis: dict) -> str: + return _lk_payslip_months_menu_text(months_data, months_ru, emojis) + +def lk_docs_quantity_prompt(emojis: dict) -> str: + return _lk_docs_quantity_prompt(emojis) + +def lk_docs_year_prompt(emojis: dict) -> str: + return _lk_docs_year_prompt(emojis) + +def lk_docs_avg_salary_prompt(emojis: dict) -> str: + return _lk_docs_avg_salary_prompt(emojis) + +def lk_docs_child_age_prompt(emojis: dict) -> str: + return _lk_docs_child_age_prompt(emojis) + +def lk_docs_period_prompt(year: int, emojis: dict) -> str: + return _lk_docs_period_prompt(year, emojis) + +def lk_docs_child_age_options(emojis: dict) -> str: + return _lk_docs_child_age_options(emojis) + +def lk_docs_period_options(emojis: dict) -> str: + return _lk_docs_period_options(emojis) + +def lk_nav_text(emojis: dict) -> str: + return _lk_nav_text(emojis) + +def lk_years_menu(current_year: int, emojis: dict) -> str: + return lk_years_menu(current_year, emojis)