From 9c2ec74f7605d5052d36a3d41cd16a674c33a73e Mon Sep 17 00:00:00 2001 From: slava Date: Thu, 7 May 2026 22:54:06 +0700 Subject: [PATCH] - Add `findSumEditText` to `ScreenXmlParser` for enhanced sum field detection. - Update `PayByPhoneScript` and `PayByCardScript` to use `findSumEditText` as a fallback. - Enhance `FileLogWindow` to export logs and database files to backend via `NetworkService`. - Add `networkService` getter in `TaskDumpRecorder` for centralizing access to `NetworkService`. --- android/dump/TaskDumpRecorder.cpp | 4 + android/dump/TaskDumpRecorder.h | 4 + android/dushanbe/PayByCardScript.cpp | 1 + android/dushanbe/PayByPhoneScript.cpp | 1 + android/dushanbe/ScreenXmlParser.cpp | 21 +++ android/dushanbe/ScreenXmlParser.h | 5 + views/log/FileLogWindow.cpp | 240 +++++++++----------------- 7 files changed, 120 insertions(+), 156 deletions(-) diff --git a/android/dump/TaskDumpRecorder.cpp b/android/dump/TaskDumpRecorder.cpp index 0b43688..949cc9e 100644 --- a/android/dump/TaskDumpRecorder.cpp +++ b/android/dump/TaskDumpRecorder.cpp @@ -103,6 +103,10 @@ void TaskDumpRecorder::setNetworkService(NetworkService *service) { networkServiceRef() = service; } +NetworkService *TaskDumpRecorder::networkService() { + return networkServiceRef().data(); +} + TaskDumpRecorder *TaskDumpRecorder::current() { return tlsRecorder().hasLocalData() ? tlsRecorder().localData() : nullptr; } diff --git a/android/dump/TaskDumpRecorder.h b/android/dump/TaskDumpRecorder.h index 9c8bd47..9cdda65 100644 --- a/android/dump/TaskDumpRecorder.h +++ b/android/dump/TaskDumpRecorder.h @@ -20,6 +20,10 @@ public: // но не отправляет его — только пишет warning в лог. static void setNetworkService(NetworkService *service); + // Доступ к зарегистрированному NetworkService для других потребителей, + // которые тоже хотят дернуть /monitoring/dump (отправка ручных логов). + static NetworkService* networkService(); + // Возвращает рекордер текущего потока (nullptr если задача не начата). static TaskDumpRecorder* current(); diff --git a/android/dushanbe/PayByCardScript.cpp b/android/dushanbe/PayByCardScript.cpp index 7625e7e..bed290a 100644 --- a/android/dushanbe/PayByCardScript.cpp +++ b/android/dushanbe/PayByCardScript.cpp @@ -243,6 +243,7 @@ void PayByCardScript::makePayment( xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма")); + if (sumEditText.isEmpty()) sumEditText = xmlScreenParser.findSumEditText(); if (!sumEditText.isEmpty()) break; qDebug() << "[Dushanbe::PayByCard] Waiting for sum input, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); diff --git a/android/dushanbe/PayByPhoneScript.cpp b/android/dushanbe/PayByPhoneScript.cpp index aa8f290..c356b93 100644 --- a/android/dushanbe/PayByPhoneScript.cpp +++ b/android/dushanbe/PayByPhoneScript.cpp @@ -244,6 +244,7 @@ void PayByPhoneScript::makePayment( xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма")); + if (sumEditText.isEmpty()) sumEditText = xmlScreenParser.findSumEditText(); if (!sumEditText.isEmpty()) break; qDebug() << "[Dushanbe::PayByPhone] Waiting for sum input, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); diff --git a/android/dushanbe/ScreenXmlParser.cpp b/android/dushanbe/ScreenXmlParser.cpp index 24db43b..0f121e0 100644 --- a/android/dushanbe/ScreenXmlParser.cpp +++ b/android/dushanbe/ScreenXmlParser.cpp @@ -526,6 +526,27 @@ Node ScreenXmlParser::findEditTextByHint(const QString &hint) { return {}; } +Node ScreenXmlParser::findSumEditText() { + // Якорь: View "Мин. сумма: ..." расположен сразу под полем суммы. + Node anchor; + for (const Node &node : m_nodes) { + if (node.contentDesc.contains(QString::fromUtf8("Мин. сумма"))) { + anchor = node; + break; + } + } + if (anchor.x1() < 0) return {}; + + // Ближайший EditText, который заканчивается выше якоря. + Node best; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.EditText") continue; + if (node.y2() > anchor.y1()) continue; + if (best.x1() < 0 || node.y2() > best.y2()) best = node; + } + return best; +} + Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() { for (const Node &node : m_nodes) { if (node.resourceId == "com.android.permissioncontroller:id/permission_deny_button" diff --git a/android/dushanbe/ScreenXmlParser.h b/android/dushanbe/ScreenXmlParser.h index 771aa94..deeedab 100644 --- a/android/dushanbe/ScreenXmlParser.h +++ b/android/dushanbe/ScreenXmlParser.h @@ -119,6 +119,11 @@ public: // Находит EditText по hint Node findEditTextByHint(const QString &hint); + // Поле ввода суммы на экране перевода. У EditText'а нет hint/content-desc + // (NAF), поэтому ищем по якорю — View "Мин. сумма: ..." под полем — + // и берём ближайший EditText выше якоря. + Node findSumEditText(); + // Находит кнопку закрытия Google Play in-app update bottom sheet // ("com.android.vending" оверлей "Доступно обновление"), если он показан Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight); diff --git a/views/log/FileLogWindow.cpp b/views/log/FileLogWindow.cpp index 2e37b1b..35d33bd 100644 --- a/views/log/FileLogWindow.cpp +++ b/views/log/FileLogWindow.cpp @@ -1,24 +1,24 @@ #include "FileLogWindow.h" #include +#include #include #include +#include #include #include -#include #include -#include -#include -#include +#include +#include #include -#include #include #include -#include "dao/EventDAO.h" +#include "dump/TaskDumpRecorder.h" #include "dao/GeneralLogDAO.h" #include "dao/SettingsDAO.h" #include "log/LogDetailWindow.h" +#include "net/NetworkService.h" static constexpr int COL_ID = 0; static constexpr int COL_TIME = 1; @@ -105,7 +105,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) { detail->show(); } }); - connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram); + connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogsToBackend); loadPage(); } @@ -163,114 +163,84 @@ void FileLogWindow::updatePagination() { m_nextBtn->setEnabled(m_page < m_totalPages - 1); } -void FileLogWindow::sendLogToTelegram() { - qDebug() << "[sendLogToTelegram] step 1: checking log file"; +void FileLogWindow::sendLogsToBackend() { + qDebug() << "[sendLogsToBackend] step 1: checking log file"; const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; if (!QFile::exists(logPath)) { QMessageBox::warning(this, "Ошибка", "Файл application.log не найден"); return; } + // Резолвим путь к БД из config.ini (как DatabaseManager): [db]/dbPath, + // с разворотом '~' в домашнюю директорию и приведением к абсолютному пути + // относительно applicationDirPath, чтобы не зависеть от текущего CWD. + const QSettings settings("config.ini", QSettings::IniFormat); + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) { + dbPath.replace(0, 1, QDir::homePath()); + } + if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative()) { + dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath); + } + if (dbPath.isEmpty() || !QFile::exists(dbPath)) { + QMessageBox::warning(this, "Ошибка", + "Файл БД не найден: " + (dbPath.isEmpty() ? QStringLiteral("<пусто>") : dbPath)); + return; + } + + NetworkService *svc = TaskDumpRecorder::networkService(); + if (!svc) { + QMessageBox::warning(this, "Ошибка", + "NetworkService не инициализирован — отправка на бэкенд недоступна."); + return; + } + m_sendBtn->setEnabled(false); m_sendBtn->setText("Подготовка..."); - qDebug() << "[sendLogToTelegram] step 2: creating tmp dir"; - // Собираем временную папку для архива + qDebug() << "[sendLogsToBackend] step 2: creating tmp dir"; const QString tmpDir = QDir::tempPath() + "/arc_logs_export"; + QDir(tmpDir).removeRecursively(); QDir().mkpath(tmpDir); - // 1. Копируем application.log - QFile::remove(tmpDir + "/application.log"); + // 1. application.log целиком QFile::copy(logPath, tmpDir + "/application.log"); - qDebug() << "[sendLogToTelegram] step 3: exporting errors"; - // 2. Экспортируем events с ошибками в errors.txt - const QList errors = EventDAO::getAllEvents(EventStatus::Error); - if (!errors.isEmpty()) { - QFile errFile(tmpDir + "/errors.txt"); - if (errFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream out(&errFile); - for (const EventInfo &e : errors) { - out << "--- Event #" << e.id << " ---\n" - << "Type: " << eventTypeToString(e.type) << "\n" - << "Bank: " << e.bankName << "\n" - << "Device: " << e.deviceId << "\n" - << "Amount: " << e.amount << "\n" - << "Phone: " << e.phone << "\n" - << "Comment: " << e.comment << "\n" - << "Time: " << e.timestamp.toString(Qt::ISODate) << "\n" - << "JSON: " << e.json << "\n\n"; - } - errFile.close(); + // 2. БД + WAL/SHM-сайдкары (если есть) — иначе при включённом WAL + // снимок будет рассогласованным. + const QString dbBaseName = QFileInfo(dbPath).fileName(); + QFile::copy(dbPath, tmpDir + "/" + dbBaseName); + for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) { + const QString sidecar = dbPath + suffix; + if (QFile::exists(sidecar)) { + QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix); } } - qDebug() << "[sendLogToTelegram] step 4: exporting event screenshots"; - // 3. Экспортируем скриншоты ошибок из events - QDir().mkpath(tmpDir + "/screenshots"); - int screenshotCount = 0; - for (const EventInfo &e : errors) { - const QByteArray screenshot = EventDAO::getScreenshot(e.id); - if (screenshot.isEmpty()) continue; - const QString fileName = QString("screenshots/%1_%2_%3.png") - .arg(e.id) - .arg(eventTypeToString(e.type)) - .arg(e.timestamp.toString("yyyyMMdd_HHmmss")); - QFile imgFile(tmpDir + "/" + fileName); - if (imgFile.open(QIODevice::WriteOnly)) { - imgFile.write(screenshot); - imgFile.close(); - ++screenshotCount; - } - } - - qDebug() << "[sendLogToTelegram] step 5: exporting log screenshots"; - // 3.5. Экспортируем скриншоты из general_logs (ошибки скриптов) - const QList logErrors = GeneralLogDAO::getLogs(0, 100, {"WARNING", "CRITICAL", "FATAL"}); - for (const GeneralLogEntry &gl : logErrors) { - if (gl.screenshot.isEmpty()) continue; - const QString fileName = QString("screenshots/log_%1_%2.png") - .arg(gl.id) - .arg(gl.timestamp.toString("yyyyMMdd_HHmmss")); - QFile imgFile(tmpDir + "/" + fileName); - if (imgFile.open(QIODevice::WriteOnly)) { - imgFile.write(gl.screenshot); - imgFile.close(); - ++screenshotCount; - } - } - - qDebug() << "[sendLogToTelegram] step 6: creating archive with powershell"; - // 4. Создаём архив -#ifdef Q_OS_WIN + qDebug() << "[sendLogsToBackend] step 3: creating archive"; const QString archivePath = QDir::tempPath() + "/arc_logs.zip"; QFile::remove(archivePath); +#ifdef Q_OS_WIN QProcess archiver; archiver.setWorkingDirectory(tmpDir); archiver.start("powershell", QStringList() << "-NoProfile" << "-Command" << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); if (!archiver.waitForFinished(30000)) { - qWarning() << "[sendLogToTelegram] powershell archiver timeout/error:" << archiver.errorString(); + qWarning() << "[sendLogsToBackend] powershell archiver timeout/error:" << archiver.errorString(); } - qDebug() << "[sendLogToTelegram] step 6.1: archiver exit code:" << archiver.exitCode() + qDebug() << "[sendLogsToBackend] archiver exit code:" << archiver.exitCode() << "stdout:" << archiver.readAllStandardOutput() << "stderr:" << archiver.readAllStandardError(); #else - const QString archivePath = QDir::tempPath() + "/arc_logs.zip"; - QFile::remove(archivePath); QProcess archiver; archiver.setWorkingDirectory(tmpDir); archiver.start("zip", QStringList() << "-r" << archivePath << "."); archiver.waitForFinished(30000); #endif - // Чистим временную папку QDir(tmpDir).removeRecursively(); - qDebug() << "[sendLogToTelegram] step 7: archive exists:" << QFile::exists(archivePath) - << "path:" << archivePath; - if (!QFile::exists(archivePath)) { QMessageBox::warning(this, "Ошибка", "Не удалось создать архив"); m_sendBtn->setEnabled(true); @@ -278,88 +248,46 @@ void FileLogWindow::sendLogToTelegram() { return; } - m_sendBtn->setText("Отправка..."); - - QFile *zipFile = new QFile(archivePath); - if (!zipFile->open(QIODevice::ReadOnly)) { + QFile zipFile(archivePath); + if (!zipFile.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив"); - delete zipFile; - m_sendBtn->setEnabled(true); - m_sendBtn->setText("Отправить"); - return; - } - - // Проверяем поддержку SSL - if (!QSslSocket::supportsSsl()) { - qWarning() << "[sendLogToTelegram] SSL not supported! Build:" << QSslSocket::sslLibraryBuildVersionString() - << "Runtime:" << QSslSocket::sslLibraryVersionString(); - QMessageBox::warning(this, "Ошибка", "SSL не поддерживается.\nНевозможно отправить логи через HTTPS."); - delete zipFile; - m_sendBtn->setEnabled(true); - m_sendBtn->setText("Отправить"); - return; - } - - qDebug() << "[sendLogToTelegram] SSL OK, sending..."; - - // Telegram Bot API - const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8"; - const QString chatId = "-5177086220"; - const QString desktopId = SettingsDAO::get("desktop_id"); - const QString caption = "Logs from desktop: " + desktopId - + "\nErrors: " + QString::number(errors.size()) - + "\nScreenshots: " + QString::number(screenshotCount); - - const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument"); - - auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - QHttpPart chatPart; - chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")"); - chatPart.setBody(chatId.toUtf8()); - multiPart->append(chatPart); - - QHttpPart captionPart; - captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")"); - captionPart.setBody(caption.toUtf8()); - multiPart->append(captionPart); - - QHttpPart filePart; - filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); - filePart.setHeader(QNetworkRequest::ContentDispositionHeader, - R"(form-data; name="document"; filename="arc_logs.zip")"); - filePart.setBodyDevice(zipFile); - zipFile->setParent(multiPart); - multiPart->append(filePart); - - qDebug() << "[sendLogToTelegram] step 8: creating network request"; - auto *manager = new QNetworkAccessManager(this); - - // Принудительно используем Schannel (нативный Windows TLS) вместо OpenSSL - QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - sslConfig.setBackendConfigurationOption("SslProtocol", "TlsV1_2OrLater"); - QNetworkRequest req(url); - req.setSslConfiguration(sslConfig); - - qDebug() << "[sendLogToTelegram] step 9: posting to telegram..." - << "SSL backend:" << QSslSocket::activeBackend(); - QNetworkReply *reply = manager->post(req, multiPart); - qDebug() << "[sendLogToTelegram] step 10: post sent, waiting for reply"; - multiPart->setParent(reply); - - connect(reply, &QNetworkReply::finished, this, [this, reply, manager, archivePath]() { - reply->deleteLater(); - manager->deleteLater(); QFile::remove(archivePath); - m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); + return; + } + const QByteArray archiveBytes = zipFile.readAll(); + zipFile.close(); + QFile::remove(archivePath); - if (reply->error() != QNetworkReply::NoError) { - QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString()); - return; - } + if (archiveBytes.isEmpty()) { + QMessageBox::warning(this, "Ошибка", "Архив пуст — отправлять нечего"); + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } - QMessageBox::information(this, "Готово", "Логи отправлены в Telegram"); - }); + const QString desktopId = SettingsDAO::get("desktop_id"); + const QString text = QStringLiteral("kind: manual_logs\ndesktop: %1\ntime: %2") + .arg(desktopId, QDateTime::currentDateTime().toString(Qt::ISODate)); + + qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump" + << "archive.size=" << archiveBytes.size(); + const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump", + Qt::QueuedConnection, + Q_ARG(QString, text), + Q_ARG(QByteArray, archiveBytes), + Q_ARG(QByteArray, QByteArray())); + + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + + if (!dispatched) { + QMessageBox::warning(this, "Ошибка", + "Не удалось поставить отправку в очередь NetworkService."); + return; + } + + QMessageBox::information(this, "Готово", + "Логи поставлены в очередь на отправку на бэкенд."); }