1
0
forked from BRT/arc
arc/android/dump/TaskDumpRecorder.cpp
trnsmkot c79acee002 Add OCR utilities and backend integration for enhanced screen dump analysis:
- Introduce `OcrUtils` with fuzzy-matching logic for text recognition on screenshots.
- Implement dual-pass OCR to handle light-on-dark and dark-on-light text cases.
- Replace hardcoded "Подтверждаю" button tap logic with OCR-based detection and fallback geometry.
- Update `TaskDumpRecorder` to utilize `NetworkService` for uploading screen dumps, images, and metadata to the backend.
- Add support for `/monitoring/dump` endpoint in `NetworkService`.
- Enhance UI functionality with new error handling and progress feedback during dump creation and upload.
2026-04-30 19:17:17 +07:00

295 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "TaskDumpRecorder.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QMetaObject>
#include <QPointer>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTextStream>
#include <QThread>
#include <QThreadStorage>
#include <qDebug>
#include "adb/AdbUtils.h"
#include "net/NetworkService.h"
namespace {
QThreadStorage<TaskDumpRecorder *> &tlsRecorder() {
static QThreadStorage<TaskDumpRecorder *> tls;
return tls;
}
QPointer<NetworkService> &networkServiceRef() {
static QPointer<NetworkService> ref;
return ref;
}
QString dumpsRoot() {
// Пишем в пользовательский temp — UAC/Program Files не мешают.
QString base = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
if (base.isEmpty()) base = QDir::tempPath();
return base + "/arc_dumps";
}
QString sanitize(const QString &raw, int maxLen = 40) {
static const QRegularExpression re(QStringLiteral("[^\\p{L}\\p{N}_-]+"));
QString s = raw;
s.replace(re, "_");
if (s.size() > maxLen) s = s.left(maxLen);
while (s.endsWith('_')) s.chop(1);
while (s.startsWith('_')) s = s.mid(1);
return s;
}
QString extractScreenTag(const QString &xml) {
static const QRegularExpression re(QStringLiteral("text=\"([^\"]{4,40})\""));
static const QRegularExpression noiseRe(QStringLiteral("^[0-9\\s,\\.\\-+₽$€%]+$"));
auto it = re.globalMatch(xml);
while (it.hasNext()) {
QString text = it.next().captured(1).trimmed();
if (text.isEmpty() || noiseRe.match(text).hasMatch()) continue;
QString tag = sanitize(text);
if (!tag.isEmpty()) return tag;
}
return QStringLiteral("screen");
}
bool zipFolder(const QString &folder, const QString &archivePath) {
#ifdef Q_OS_WIN
const QString cmd = QString(
"Compress-Archive -Path '%1\\*' -DestinationPath '%2' -Force"
).arg(QDir::toNativeSeparators(folder), QDir::toNativeSeparators(archivePath));
QProcess p;
p.start("powershell", {"-NoProfile", "-Command", cmd});
if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) {
qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString();
return false;
}
if (p.exitCode() != 0) {
qWarning() << "[TaskDumpRecorder] zip exit=" << p.exitCode()
<< "stderr:" << QString::fromUtf8(p.readAllStandardError()).trimmed();
return false;
}
return true;
#else
const QFileInfo fi(folder);
QProcess p;
p.setWorkingDirectory(fi.absolutePath());
p.start("zip", {"-rq", archivePath, fi.fileName()});
if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) {
qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString();
return false;
}
return p.exitCode() == 0;
#endif
}
QByteArray readFileBytes(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) return {};
QByteArray bytes = f.readAll();
f.close();
return bytes;
}
} // namespace
void TaskDumpRecorder::setNetworkService(NetworkService *service) {
networkServiceRef() = service;
}
TaskDumpRecorder *TaskDumpRecorder::current() {
return tlsRecorder().hasLocalData() ? tlsRecorder().localData() : nullptr;
}
void TaskDumpRecorder::begin(const QString &bank, const QString &taskKind,
const QString &adbDeviceId, const TaskDumpMeta &meta) {
if (auto *prev = current()) {
prev->finishInternal(QStringLiteral("interrupted (new task started before previous finished)"));
// Не вызываем `delete prev` — QThreadStorage<T*> сам удалит предыдущий
// указатель при замене через setLocalData.
}
auto *rec = new TaskDumpRecorder(bank, taskKind, adbDeviceId, meta);
tlsRecorder().setLocalData(rec);
}
void TaskDumpRecorder::finish(const QString &errorMessage) {
auto *rec = current();
if (!rec) return;
rec->finishInternal(errorMessage);
// setLocalData(nullptr) сам вызовет delete на текущем указателе.
tlsRecorder().setLocalData(nullptr);
}
TaskDumpRecorder::TaskDumpRecorder(const QString &bank, const QString &taskKind,
const QString &adbDeviceId, const TaskDumpMeta &meta)
: m_bank(sanitize(bank)),
m_taskKind(sanitize(taskKind)),
m_adbDeviceIdRaw(adbDeviceId),
m_adbDeviceIdSafe(sanitize(adbDeviceId)),
m_meta(meta) {
// Короткий суффикс для имени папки — чтобы визуально отличать задачи,
// но не раздувать путь. Берём materialId (первые 8 символов — UUID segment)
// + сумму, если есть.
QString folderTail;
if (!m_meta.materialId.isEmpty()) {
folderTail = sanitize(m_meta.materialId.left(8));
}
if (m_meta.amount != 0) {
const QString amt = QString::number(static_cast<qint64>(m_meta.amount));
folderTail = folderTail.isEmpty() ? amt : folderTail + "_" + amt;
}
const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
const QString sub = folderTail.isEmpty()
? QString("%1_%2").arg(ts, m_adbDeviceIdSafe)
: QString("%1_%2_%3").arg(ts, m_adbDeviceIdSafe, folderTail);
m_folder = QString("%1/%2_%3/%4")
.arg(dumpsRoot(), m_bank, m_taskKind, sub);
if (!QDir().mkpath(m_folder)) {
qWarning() << "[TaskDumpRecorder] failed to mkpath" << m_folder;
m_folder.clear();
return;
}
qDebug() << "[TaskDumpRecorder] begin:" << m_folder;
}
QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const {
QString s = QString("Bank: %1 | Task: %2 | Device: %3")
.arg(m_bank, m_taskKind, m_adbDeviceIdRaw);
if (!m_meta.materialId.isEmpty()) s += QString("\nMaterial ID: %1").arg(m_meta.materialId);
if (m_meta.amount != 0) s += QString("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
if (!m_meta.phone.isEmpty()) s += QString("\nPhone: %1").arg(m_meta.phone);
if (!m_meta.card.isEmpty()) s += QString("\nCard: %1").arg(m_meta.card);
s += QString("\nError: %1").arg(errorMessage.left(800));
return s;
}
void TaskDumpRecorder::saveXml(int counter, const QString &xml) {
if (m_folder.isEmpty() || xml.isEmpty()) return;
const QString tag = extractScreenTag(xml);
const QString fname = QString("%1/%2_%3.xml")
.arg(m_folder,
QString("%1").arg(counter, 4, 10, QChar('0')),
tag);
QFile f(fname);
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
f.write(xml.toUtf8());
f.close();
} else {
qWarning() << "[TaskDumpRecorder] failed to write" << fname << f.errorString();
}
}
void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
if (m_finished) return;
m_finished = true;
if (m_folder.isEmpty()) return;
if (errorMessage.isEmpty()) {
QDir(m_folder).removeRecursively();
qDebug() << "[TaskDumpRecorder] success — removed" << m_folder;
return;
}
qWarning() << "[TaskDumpRecorder] task failed:" << errorMessage
<< "folder:" << m_folder;
// Финальный скриншот текущего экрана
if (!m_adbDeviceIdRaw.isEmpty()) {
const QByteArray png = AdbUtils::captureScreenshotBytes(m_adbDeviceIdRaw);
if (!png.isEmpty()) {
QFile f(m_folder + "/_final_screen.png");
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
f.write(png);
f.close();
}
}
}
// Метаданные
{
QFile ef(m_folder + "/_error.txt");
if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream ts(&ef);
ts.setEncoding(QStringConverter::Utf8);
ts << "bank: " << m_bank << "\n"
<< "task: " << m_taskKind << "\n"
<< "device: " << m_adbDeviceIdRaw << "\n";
if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n";
if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n";
if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n";
if (!m_meta.card.isEmpty()) ts << "card: " << m_meta.card << "\n";
ts << "time: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n"
<< "error: " << errorMessage << "\n";
ef.close();
}
}
const QString folder = m_folder;
const QString archivePath = m_folder + ".zip";
const QString text = buildCaption(errorMessage);
// Финальный скриншот шлём отдельным полем `image`, поэтому сразу читаем
// его в память, пока папка ещё цела.
QByteArray finalScreenshot;
{
const QString screenPath = folder + "/_final_screen.png";
if (QFile::exists(screenPath)) {
finalScreenshot = readFileBytes(screenPath);
}
}
QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot]() {
QByteArray archiveBytes;
if (zipFolder(folder, archivePath)) {
archiveBytes = readFileBytes(archivePath);
if (archiveBytes.isEmpty()) {
qWarning() << "[TaskDumpRecorder] zip is empty, sending text-only dump for" << folder;
}
} else {
qWarning() << "[TaskDumpRecorder] zip failed, sending text-only dump for" << folder;
}
QDir(folder).removeRecursively();
QFile::remove(archivePath);
NetworkService *svc = networkServiceRef().data();
if (!svc) {
qWarning() << "[TaskDumpRecorder] no NetworkService — dump dropped for" << folder
<< "(setNetworkService was not called at app startup?)";
return;
}
qDebug() << "[TaskDumpRecorder] dispatching dump to NetworkService"
<< "archive.size=" << archiveBytes.size()
<< "image.size=" << finalScreenshot.size()
<< "text.size=" << text.size();
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
Qt::QueuedConnection,
Q_ARG(QString, text),
Q_ARG(QByteArray, archiveBytes),
Q_ARG(QByteArray, finalScreenshot));
if (!dispatched) {
qCritical() << "[TaskDumpRecorder] invokeMethod(postMonitoringDump) FAILED — "
"slot not found or wrong signature for" << folder;
}
});
QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater);
bg->start();
}
TaskDumpRecorder::Scope::Scope(const QString &bank, const QString &taskKind,
const QString &adbDeviceId, const TaskDumpMeta &meta,
const QString *errorPtr)
: m_errorPtr(errorPtr) {
TaskDumpRecorder::begin(bank, taskKind, adbDeviceId, meta);
}
TaskDumpRecorder::Scope::~Scope() {
TaskDumpRecorder::finish(m_errorPtr ? *m_errorPtr : QString());
}