From 29ae105c644bcf03d7a990c0295eb811de37def6 Mon Sep 17 00:00:00 2001 From: slava Date: Mon, 1 Jun 2026 14:29:05 +0500 Subject: [PATCH] Add `appVersion` to `TaskDumpMeta` structure and update associated scripts for enhanced application version tracking in tasks and monitoring outputs. --- android/black/GetCardInfoScript.cpp | 2 +- android/black/GetProfileInfoScript.cpp | 2 +- android/black/LoginAndCheckAccountsScript.cpp | 2 +- android/dump/TaskDumpRecorder.cpp | 3 ++ android/dump/TaskDumpRecorder.h | 1 + android/dushanbe/GetCardInfoScript.cpp | 2 +- .../dushanbe/GetLastTransactionsScript.cpp | 29 +++++------ android/dushanbe/GetLastTransactionsScript.h | 14 ++++-- android/dushanbe/GetProfileInfoScript.cpp | 2 +- .../dushanbe/LoginAndCheckAccountsScript.cpp | 2 +- android/dushanbe/PayByCardScript.cpp | 2 +- android/dushanbe/PayByPhoneScript.cpp | 2 +- android/ozon/GetCardInfoScript.cpp | 2 +- android/ozon/GetLastTransactionsScript.cpp | 2 +- android/ozon/GetProfileInfoScript.cpp | 2 +- android/ozon/LoginAndCheckAccountsScript.cpp | 2 +- android/ozon/PayByCardScript.cpp | 2 +- android/ozon/PayByPhoneScript.cpp | 2 +- config.ini | 1 + services/DeviceScreener.cpp | 41 +++++++++++++++- services/DeviceScreener.h | 11 +++++ services/net/NetworkService.cpp | 49 +++++++++++++++++++ services/net/NetworkService.h | 9 ++++ 23 files changed, 152 insertions(+), 34 deletions(-) diff --git a/android/black/GetCardInfoScript.cpp b/android/black/GetCardInfoScript.cpp index dca0684..b30d8ec 100644 --- a/android/black/GetCardInfoScript.cpp +++ b/android/black/GetCardInfoScript.cpp @@ -37,7 +37,7 @@ void GetCardInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/black/GetProfileInfoScript.cpp b/android/black/GetProfileInfoScript.cpp index 6fa2a8d..f9044ab 100644 --- a/android/black/GetProfileInfoScript.cpp +++ b/android/black/GetProfileInfoScript.cpp @@ -35,7 +35,7 @@ void GetProfileInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/black/LoginAndCheckAccountsScript.cpp b/android/black/LoginAndCheckAccountsScript.cpp index 63e42ba..f07cf0d 100644 --- a/android/black/LoginAndCheckAccountsScript.cpp +++ b/android/black/LoginAndCheckAccountsScript.cpp @@ -36,7 +36,7 @@ void LoginAndCheckAccountsScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dump/TaskDumpRecorder.cpp b/android/dump/TaskDumpRecorder.cpp index 7f155d2..248798b 100644 --- a/android/dump/TaskDumpRecorder.cpp +++ b/android/dump/TaskDumpRecorder.cpp @@ -188,6 +188,8 @@ QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const { body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped()); if (!m_meta.profilePhone.isEmpty()) body += QStringLiteral("\nPhone: %1").arg(m_meta.profilePhone.toHtmlEscaped()); + if (!m_meta.appVersion.isEmpty()) + body += QStringLiteral("\nAppVersion: %1").arg(m_meta.appVersion.toHtmlEscaped()); if (m_meta.amount != 0) body += QStringLiteral("\nAmount: %1").arg(static_cast(m_meta.amount)); if (!m_meta.phone.isEmpty()) @@ -256,6 +258,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) { if (!m_meta.androidId.isEmpty()) ts << "android_id: " << m_meta.androidId << "\n"; if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n"; if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n"; + if (!m_meta.appVersion.isEmpty()) ts << "app_version: " << m_meta.appVersion << "\n"; if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n"; if (m_meta.amount != 0) ts << "amount: " << static_cast(m_meta.amount) << "\n"; if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n"; diff --git a/android/dump/TaskDumpRecorder.h b/android/dump/TaskDumpRecorder.h index 6e2ccea..cb25ca3 100644 --- a/android/dump/TaskDumpRecorder.h +++ b/android/dump/TaskDumpRecorder.h @@ -14,6 +14,7 @@ struct TaskDumpMeta { QString bankProfileId; // серверный UUID банковского профиля (детект в общих логах) QString profilePhone; // телефон банковского профиля (не получателя) QString androidId; // стабильный android_id устройства (в отличие от adbSerial не меняется) + QString appVersion; // версия банковского приложения (из pm dump versionName) }; class TaskDumpRecorder { diff --git a/android/dushanbe/GetCardInfoScript.cpp b/android/dushanbe/GetCardInfoScript.cpp index 40c7772..9df429c 100644 --- a/android/dushanbe/GetCardInfoScript.cpp +++ b/android/dushanbe/GetCardInfoScript.cpp @@ -36,7 +36,7 @@ void GetCardInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 08be599..98a6c79 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -278,7 +278,7 @@ void GetLastTransactionsScript::doStart() { } const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; - const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); @@ -407,12 +407,12 @@ void GetLastTransactionsScript::doStart() { // Проход идёт вниз от текущей позиции. Если цель не нашлась — возможно, // предыдущая (более новая) цель отсутствовала и увела список вниз, а эта // лежит выше. Тогда надёжно возвращаемся к началу Выписки и пробуем ещё раз. - bool found = searchOne(device, accountId, amount, time); - if (!found && m_error != "Interrupted") { + SearchResult res = searchOne(device, accountId, amount, time); + if (res == SearchResult::NotFound && m_error != "Interrupted") { scrollHistoryToTop(device); - found = searchOne(device, accountId, amount, time); + res = searchOne(device, accountId, amount, time); } - if (!found) { + if (res != SearchResult::Found) { if (m_error == "Interrupted") break; notFound << QString("amount=%1 time=%2") .arg(QString::number(amount, 'f', 2), time.toString(Qt::ISODate)); @@ -484,8 +484,9 @@ void GetLastTransactionsScript::scrollHistoryToTop(const DeviceInfo &device) { QThread::msleep(500); } -bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountId, - double searchAmount, const QDateTime &searchTime) { +GetLastTransactionsScript::SearchResult GetLastTransactionsScript::searchOne( + const DeviceInfo &device, int accountId, + double searchAmount, const QDateTime &searchTime) { // Скриншот списка транзакций — отправим при неудаче const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); @@ -511,7 +512,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI for (int scroll = 0; scroll <= maxScrolls; ++scroll) { if (interrupted()) { m_error = "Interrupted"; - return false; + return SearchResult::NotFound; } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); @@ -763,13 +764,13 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI const int tapX = txNode.x1() + 80; AdbUtils::makeTap(m_deviceId, tapX, txNode.y()); QThread::msleep(3000); - if (interrupted()) { m_error = "Interrupted"; return false; } + if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; } // Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж" OcrTransactionDetail detail; bool detailLoaded = false; for (int attempt = 0; attempt < 10; ++attempt) { - if (interrupted()) { m_error = "Interrupted"; return false; } + if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); // 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml): @@ -975,7 +976,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI } QThread::msleep(1000); - return true; // успех — транзакция найдена и отдана через transactionParsed + return SearchResult::Found; } // Окно поиска — только "сегодня + вчера". Список Выписки идёт сверху-вниз @@ -997,13 +998,13 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI " (newest visible=" << newestVisible.toString("dd.MM.yyyy") << "< cutoff=" << cutoff.toString("dd.MM.yyyy") << ") — stopping scroll"; - break; + return SearchResult::PastWindow; } } // Скроллим вниз — свайп внутри области транзакций if (scroll < maxScrolls) { - if (interrupted()) { m_error = "Interrupted"; return false; } + if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; } const int x = device.screenWidth / 2; // Определяем область контента из реальных Y позиций items int contentBottom = device.screenHeight * 2 / 3; @@ -1031,7 +1032,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI ? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot; if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); - return false; + return SearchResult::NotFound; } } // namespace Dushanbe diff --git a/android/dushanbe/GetLastTransactionsScript.h b/android/dushanbe/GetLastTransactionsScript.h index 12dfa90..ab26476 100644 --- a/android/dushanbe/GetLastTransactionsScript.h +++ b/android/dushanbe/GetLastTransactionsScript.h @@ -33,11 +33,15 @@ protected: void doStart() override; private: - // Поиск одной цели по сумме и времени на экране "Выписка". Возвращает true - // и эмитит transactionParsed, если нашли; false — если нет. Прерывание - // выставляет m_error="Interrupted". - bool searchOne(const DeviceInfo &device, int accountId, - double searchAmount, const QDateTime &searchTime); + enum class SearchResult { Found, NotFound, PastWindow }; + + // Поиск одной цели по сумме и времени на экране "Выписка". + // Found — нашли, transactionParsed эмитирован. + // NotFound — не нашли, список прокручен до maxScrolls. + // PastWindow — список старше окна today+yesterday, retry бессмысленен. + // Прерывание выставляет m_error="Interrupted" и возвращает NotFound. + SearchResult searchOne(const DeviceInfo &device, int accountId, + double searchAmount, const QDateTime &searchTime); // Возврат к началу списка "Выписка" между поисками целей. void scrollHistoryToTop(const DeviceInfo &device); diff --git a/android/dushanbe/GetProfileInfoScript.cpp b/android/dushanbe/GetProfileInfoScript.cpp index 0c916cd..80922aa 100644 --- a/android/dushanbe/GetProfileInfoScript.cpp +++ b/android/dushanbe/GetProfileInfoScript.cpp @@ -39,7 +39,7 @@ void GetProfileInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dushanbe/LoginAndCheckAccountsScript.cpp b/android/dushanbe/LoginAndCheckAccountsScript.cpp index b4283e3..24e042f 100644 --- a/android/dushanbe/LoginAndCheckAccountsScript.cpp +++ b/android/dushanbe/LoginAndCheckAccountsScript.cpp @@ -41,7 +41,7 @@ void LoginAndCheckAccountsScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dushanbe/PayByCardScript.cpp b/android/dushanbe/PayByCardScript.cpp index 70925b4..374f906 100644 --- a/android/dushanbe/PayByCardScript.cpp +++ b/android/dushanbe/PayByCardScript.cpp @@ -123,7 +123,7 @@ void PayByCardScript::doStart() { return; } - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); diff --git a/android/dushanbe/PayByPhoneScript.cpp b/android/dushanbe/PayByPhoneScript.cpp index 4534c8c..0784345 100644 --- a/android/dushanbe/PayByPhoneScript.cpp +++ b/android/dushanbe/PayByPhoneScript.cpp @@ -120,7 +120,7 @@ void PayByPhoneScript::doStart() { return; } - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index 129f77f..18255b3 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -42,7 +42,7 @@ void GetCardInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index 4c71177..97a14bb 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -109,7 +109,7 @@ void GetLastTransactionsScript::doStart() { } const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; - const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); if (!navigateToTransactionList(device, app)) { diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp index 8a3d12e..f9cff95 100644 --- a/android/ozon/GetProfileInfoScript.cpp +++ b/android/ozon/GetProfileInfoScript.cpp @@ -37,7 +37,7 @@ void GetProfileInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index 0c25617..ba0798e 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -40,7 +40,7 @@ void LoginAndCheckAccountsScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/ozon/PayByCardScript.cpp b/android/ozon/PayByCardScript.cpp index 0fd172a..50dfc6b 100644 --- a/android/ozon/PayByCardScript.cpp +++ b/android/ozon/PayByCardScript.cpp @@ -91,7 +91,7 @@ void PayByCardScript::doStart() { } const QString &adbId = device.adbSerial; - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp index eb56176..216896d 100644 --- a/android/ozon/PayByPhoneScript.cpp +++ b/android/ozon/PayByPhoneScript.cpp @@ -100,7 +100,7 @@ void PayByPhoneScript::doStart() { } const QString &adbId = device.adbSerial; - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id, app.appVersion}; TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); diff --git a/config.ini b/config.ini index 66e2813..17a76b9 100644 --- a/config.ini +++ b/config.ini @@ -23,6 +23,7 @@ pathMaterial=/api/v1/profile/material pathProfilePing=/api/v1/profile/ping pathMonitoringLog=/monitoring/log pathMonitoringDump=/monitoring/dump +pathMonitoringScreenshot=/monitoring/screenshot monitoringBase=http://142.93.102.40:8080 monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index 919f92d..9723707 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include "dao/MaterialDAO.h" #include "dao/SettingsDAO.h" #include "net/NetworkErrorUtils.h" +#include "net/NetworkService.h" DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { const QSettings settings("config.ini", QSettings::IniFormat); @@ -247,6 +249,28 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray reply->deleteLater(); } +void DeviceScreener::dispatchScreenshotToMonitoring(const QString &desktopId, + const QString &deviceId, + const QString &name, + const QByteArray &jpeg) { + if (jpeg.isEmpty()) return; + + // Отдельный поток + свой NetworkService, чтобы таймаут отправки не тормозил + // цикл сканера (паттерн LogArchiveSender::dispatch). + auto *thread = new QThread; + auto *svc = new NetworkService; + svc->moveToThread(thread); + QObject::connect(thread, &QThread::started, svc, + [svc, desktopId, deviceId, name, jpeg]() { + svc->postMonitoringScreenshot(desktopId, deviceId, name, jpeg); + emit svc->finished(); + }); + QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater); + thread->start(); +} + void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { // Находим первый активный bank_profile для этого устройства QJsonValue bankProfileId = QJsonValue::Null; @@ -392,8 +416,8 @@ void DeviceScreener::start() { qDebug() << "Device registered on API:" << device.id << "->" << apiId; } } else if (status == "device") { - // Обновляем не чаще раза в 60 секунд const qint64 now = QDateTime::currentSecsSinceEpoch(); + // Основной API обновляем не чаще раза в 60 секунд. if (now - m_lastApiUpdate.value(device.id, 0) >= 60) { device.apiId = saved.apiId; updateDeviceOnApi(device); @@ -402,6 +426,21 @@ void DeviceScreener::start() { } m_lastApiUpdate[device.id] = now; } + + // История экрана на сервере логов: проверяем не чаще раза в + // kMonitoringScreenIntervalSecs и шлём кадр только если экран + // изменился относительно последнего отправленного (дедуп по sha256). + if (!device.image.isEmpty() + && now - m_lastMonitoringScreen.value(device.id, 0) >= kMonitoringScreenIntervalSecs) { + m_lastMonitoringScreen[device.id] = now; + const QByteArray hash = + QCryptographicHash::hash(device.image, QCryptographicHash::Sha256); + if (m_lastSentScreenHash.value(device.id) != hash) { + m_lastSentScreenHash[device.id] = hash; + dispatchScreenshotToMonitoring(SettingsDAO::get("desktop_id"), + device.id, device.name, device.image); + } + } } } diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index 26da719..dfedbb7 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -15,6 +15,8 @@ public: ~DeviceScreener() override; static constexpr qint64 kOfflineDebounceSecs = 30; + // Как часто проверять/слать кадр истории экрана на сервер логов (с дедупом). + static constexpr qint64 kMonitoringScreenIntervalSecs = 30; public slots: void start(); @@ -37,6 +39,8 @@ private: QMap m_lastApiUpdate; // deviceId → unix timestamp последнего PATCH QSet m_onlineDevices; // дебаунс-онлайн (true online после dedup) QMap m_offlineSince; // deviceId → когда пропал из ADB (для дебаунса) + QMap m_lastMonitoringScreen; // deviceId → unix ts последней проверки кадра для истории экрана + QMap m_lastSentScreenHash; // deviceId → sha256 последнего кадра, отправленного в историю экрана static QList > readAdbDevices(); @@ -53,4 +57,11 @@ private: QString createDeviceOnApi(const DeviceInfo &device); void updateDeviceOnApi(const DeviceInfo &device); void postScreenshotToApi(const QString &apiId, const QByteArray &image); + + // Fire-and-forget отправка кадра истории экрана на сервер логов (monitoringBase). + // Создаёт временный NetworkService в отдельном потоке (паттерн LogArchiveSender). + static void dispatchScreenshotToMonitoring(const QString &desktopId, + const QString &deviceId, + const QString &name, + const QByteArray &jpeg); }; diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index bd03cca..056c10d 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -78,6 +78,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) { m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString(); m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString(); m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString(); + m_pathMonitoringScreenshot = settings.value("net/pathMonitoringScreenshot", "/monitoring/screenshot").toString(); m_monitoringBase = settings.value("net/monitoringBase", m_apiBase).toString(); m_monitoringToken = settings.value("net/monitoringToken").toString(); m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString(); @@ -1766,6 +1767,54 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a reply->deleteLater(); } +void NetworkService::postMonitoringScreenshot(const QString &desktopId, + const QString &deviceId, + const QString &name, + const QByteArray &jpeg) { + if (jpeg.isEmpty()) return; + + QNetworkRequest request(QUrl(m_monitoringBase + m_pathMonitoringScreenshot)); + if (!m_monitoringToken.isEmpty()) { + request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8()); + } + + QNetworkReply *reply = nullptr; + bool timedOut = true; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + addFormField(multiPart, "desktop_id", desktopId); + addFormField(multiPart, "device_id", deviceId); + addFormField(multiPart, "name", name); + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + imagePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"image\"; filename=\"screen.jpg\"") + ); + imagePart.setBody(jpeg); + multiPart->append(imagePart); + + reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + qWarning() << "[NetworkService] postMonitoringScreenshot timeout attempt" + << attempt << "/" << kMaxRequestAttempts; + reply->abort(); + } + + if (timedOut) { + qWarning() << "[NetworkService] postMonitoringScreenshot timeout after" + << kMaxRequestAttempts << "attempts"; + } else if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] postMonitoringScreenshot failed:" << reply->errorString() + << "\n response body:" << reply->readAll(); + } + reply->deleteLater(); +} + void NetworkService::start() { m_running = true; diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index f39adc5..91af1b9 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -117,6 +117,14 @@ public slots: const QByteArray &image, const QString &archiveFilename = QStringLiteral("task_dump.zip")); + // Отправляет JPEG-скриншот девайса на monitoringBase (/monitoring/screenshot, + // X-Monitoring-Token) для истории экрана. Q_INVOKABLE — вызывается через + // QueuedConnection из fire-and-forget потока DeviceScreener. + Q_INVOKABLE void postMonitoringScreenshot(const QString &desktopId, + const QString &deviceId, + const QString &name, + const QByteArray &jpeg); + void start(); void stop(); void disableAllProfilesOnServer(); @@ -165,6 +173,7 @@ private: QString m_pathTaskResult; QString m_pathMonitoringLog; QString m_pathMonitoringDump; + QString m_pathMonitoringScreenshot; QString m_monitoringBase; QString m_monitoringToken; QString m_pathCurrentProfile;