#include "TaskDumpRecorder.h" #include #include #include #include #include #include #include #include #include #include #include "adb/AdbUtils.h" namespace { QThreadStorage &tlsRecorder() { static QThreadStorage tls; return tls; } constexpr const char *kBotToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8"; constexpr const char *kChatId = "-5177086220"; QString dumpsRoot() { return QCoreApplication::applicationDirPath() + "/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 runSync(const QString &program, const QStringList &args, int timeoutMs = 180000) { QProcess p; p.start(program, args); if (!p.waitForStarted(5000)) { qWarning() << "[TaskDumpRecorder] failed to start:" << program << args << p.errorString(); return false; } if (!p.waitForFinished(timeoutMs)) { qWarning() << "[TaskDumpRecorder] timeout:" << program << args; p.kill(); return false; } if (p.exitCode() != 0) { qWarning() << "[TaskDumpRecorder]" << program << "exit=" << p.exitCode() << "stderr:" << QString::fromUtf8(p.readAllStandardError()).trimmed(); return false; } return true; } 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)); return runSync("powershell", {"-NoProfile", "-Command", cmd}); #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 } void uploadToTelegram(const QString &archivePath, const QString &caption) { const QString url = QString("https://api.telegram.org/bot%1/sendDocument").arg(kBotToken); const QStringList args = { "-sS", "--max-time", "120", "-F", QString("chat_id=%1").arg(kChatId), "-F", QString("document=@%1").arg(archivePath), "-F", QString("caption=%1").arg(caption), url }; runSync("curl", args, 150000); } } // namespace 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 сам удалит предыдущий // указатель при замене через 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(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(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(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 caption = buildCaption(errorMessage); QThread *bg = QThread::create([folder, archivePath, caption]() { if (zipFolder(folder, archivePath)) { uploadToTelegram(archivePath, caption); } else { qWarning() << "[TaskDumpRecorder] zip failed, skipping Telegram upload for" << folder; } QDir(folder).removeRecursively(); QFile::remove(archivePath); }); 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()); }