新增了客户端打包成sdk的功能和服务端docker镜像打包的功能,并修复了一些问题

This commit is contained in:
2026-07-07 09:48:01 +00:00
parent cd3dca042d
commit 9e545cb328
50 changed files with 5251 additions and 875 deletions
+60
View File
@@ -12,6 +12,7 @@
#include <QCryptographicHash>
#include <QDir>
#include <QJsonDocument>
#include <QSaveFile>
#include <QApplication>
#include <algorithm>
#include "ConfigHelper.h"
@@ -325,6 +326,52 @@ bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString&
return true;
}
bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& stagingDir)
{
m_offlineError.clear();
QFile package(packagePath);
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = "无法打开离线更新包"; return false; }
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = "离线包格式标识无效"; return false; }
const QByteArray lengthBytes = package.read(8);
if (lengthBytes.size() != 8) { m_offlineError = "离线包头不完整"; return false; }
quint64 headerSize = 0;
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = "离线包头长度无效"; return false; }
QJsonParseError error;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = "离线包头 JSON 无效"; return false; }
const QJsonObject wrapper = wrapperDoc.object();
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = "离线包 RSA 签名无效"; return false; }
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = "离线包签名元数据无效"; return false; }
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = "Manifest 摘要与包签名不一致"; return false; }
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = "离线 Manifest 无效"; return false; }
manifest.insert("signature", wrapper.value("manifest_signature").toString());
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = "包信息与 Manifest 身份不一致"; return false; }
if (!verifyManifestSignature()) { m_offlineError = "离线 Manifest RSA 签名无效"; return false; }
const qint64 payloadStart = 16 + qint64(headerSize);
const QJsonArray entries = packageMeta.value("files").toArray();
for (const QJsonValue& value : entries) {
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = "离线包包含不安全路径或越界数据: " + path; return false; }
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
if (stagingDir.isEmpty()) continue;
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = "无法准备离线文件: " + path; return false; }
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = "无法创建暂存文件: " + path; return false; }
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = "离线文件读取失败: " + path; return false; } hash.addData(block); remaining -= block.size(); }
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = "离线文件 Hash 或写入失败: " + path; return false; }
}
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = "离线包文件数量与 Manifest 不一致"; return false; }
return true;
}
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
{
QString url = m_serverAddr + "/api/v1/update/download-url";
@@ -640,6 +687,18 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
return true;
}
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
const QString& version, bool success)
{
QJsonArray files;
for (const FileDownloadItem& item : m_fileItems)
files.append(QJsonObject{{"path", item.path}, {"size", item.size}});
QJsonObject body{{"app_id", appId}, {"channel", channel}, {"version", version},
{"result", success ? "success" : "fail"}, {"files", files}};
m_http.postRequest(m_serverAddr + "/api/v1/update/download-report", body,
[](int code, const QJsonObject&) { qDebug() << "Download result report returned code:" << code; });
}
void UpdaterLogic::reportResult(const QString& deviceId,
const QString& fromVer,
const QString& toVer,
@@ -648,6 +707,7 @@ void UpdaterLogic::reportResult(const QString& deviceId,
QString url = m_serverAddr + "/api/v1/update/report";
QJsonObject body;
body["app_id"] = ConfigHelper::instance().getValue("App", "app_id");
body["device_id"] = deviceId;
body["from_version"] = fromVer;
body["to_version"] = toVer;