From 94b20bf2bb1fbb446c6ad7829699e50df2d1fe71 Mon Sep 17 00:00:00 2001 From: nikelaluo <2135665716@qq.com> Date: Tue, 14 Jul 2026 01:37:06 +0000 Subject: [PATCH] feat(client): improve SDK packaging and registry config --- .editorconfig | 6 + .gitignore | 19 +- Bootstrap/main.cpp | 46 +- CMakeLists.txt | 211 +- CMakePresets.json | 57 + Common/CMakeLists.txt | 27 +- Common/ConfigHelper.cpp | 664 +++- Common/ConfigHelper.h | 47 +- Common/DeviceIdentityHelper.cpp | 15 +- Common/FileHelper.cpp | 12 +- Common/HttpHelper.h | 4 +- Common/LocalStateHelper.cpp | 62 +- Common/LocalStateHelper.h | 2 +- Common/PolicyHelper.cpp | 34 +- Common/TicketHelper.cpp | 2 +- Docs/ReadMe.txt | 47 + Docs/客户端部署说明.md | 299 ++ Docs/第三方依赖说明.md | 117 + Launcher/CMakeLists.txt | 27 +- Launcher/UpdateLogic.cpp | 83 +- Launcher/UpdateLogic.h | 30 +- Launcher/main.cpp | 352 +- MainApp/CMakeLists.txt | 39 +- MainApp/main.cpp | 15 +- READMe.txt | 27 - SDK_README.md | 188 -- Updater/CMakeLists.txt | 26 +- Updater/UpdaterLogic.cpp | 63 +- Updater/main.cpp | 500 +-- config/manifest_public_key.pem | 18 +- i18n/update-client.qrc | 5 + i18n/update-client_zh_CN.qm | Bin 0 -> 18431 bytes i18n/update-client_zh_CN.ts | 874 +++++ pdf_requirements.txt | 2943 ----------------- scripts/ReadMe.txt | 33 + install-sdk.ps1 => scripts/install-sdk.ps1 | 0 .../package-client.ps1 | 37 +- package-sdk.ps1 => scripts/package-sdk.ps1 | 69 +- 38 files changed, 3040 insertions(+), 3960 deletions(-) create mode 100644 .editorconfig create mode 100644 CMakePresets.json create mode 100644 Docs/ReadMe.txt create mode 100644 Docs/客户端部署说明.md create mode 100644 Docs/第三方依赖说明.md delete mode 100644 READMe.txt delete mode 100644 SDK_README.md create mode 100644 i18n/update-client.qrc create mode 100644 i18n/update-client_zh_CN.qm create mode 100644 i18n/update-client_zh_CN.ts delete mode 100644 pdf_requirements.txt create mode 100644 scripts/ReadMe.txt rename install-sdk.ps1 => scripts/install-sdk.ps1 (100%) rename package-client.ps1 => scripts/package-client.ps1 (91%) rename package-sdk.ps1 => scripts/package-sdk.ps1 (77%) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e1db451 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +charset = utf-8-bom +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore index bf5e161..a90c815 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,12 @@ Desktop.ini *.log *~ -# Private requirement / handover documents -*.pdf -*.doc -*.docx +# Private requirement / handover documents +*.pdf +*.doc +*.docx +private/ +pdf_requirements.txt # C/C++ generated artifacts *.obj @@ -48,10 +50,11 @@ CMakeSettings.json CMakeUserPresets.json Testing/ -# Third-party/business binary drops and generated packages -App/ -dist/ -*.zip +# Third-party/business binary drops and generated packages +thirdparty/ +App/ +dist/ +*.zip *.tar *.tar.gz *.tgz diff --git a/Bootstrap/main.cpp b/Bootstrap/main.cpp index f2879e6..a1739c9 100644 --- a/Bootstrap/main.cpp +++ b/Bootstrap/main.cpp @@ -112,12 +112,12 @@ static bool launchUpdater(const std::wstring& updater, const std::vector paths; std::string line; while (std::getline(input, line)) { - if (!line.empty() && line.back() == '\r') line.pop_back(); - if (line.size() < 3 || line[1] != '\t' || (line[0] != 'C' && line[0] != 'D')) { - MessageBoxW(nullptr, L"更新计划操作格式无效。", L"更新接管失败", MB_ICONERROR); - return 3; - } - const fs::path relative(fromUtf8(line.substr(2))); - if (!safeRelative(relative)) { - MessageBoxW(nullptr, L"更新计划包含不安全路径。", L"更新接管失败", MB_ICONERROR); - return 3; - } + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.size() < 3 || line[1] != '\t' || (line[0] != 'C' && line[0] != 'D')) { + MessageBoxW(nullptr, L"The update plan operation format is invalid.", L"Update Handoff Failed", MB_ICONERROR); + return 3; + } + const fs::path relative(fromUtf8(line.substr(2))); + if (!safeRelative(relative)) { + MessageBoxW(nullptr, L"The update plan contains an unsafe path.", L"Update Handoff Failed", MB_ICONERROR); + return 3; + } items.push_back({static_cast(line[0]), relative}); paths.push_back(relative); } @@ -180,12 +180,12 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) if (!success) rolledBack = rollback(installDir, backupDir, paths); } - const std::wstring result = success ? L"success" : (rolledBack ? L"rolledback" : L"rollback-failed"); - if (!launchUpdater(updater, updateArgs, result)) { - std::wstring message = L"无法重新启动 Updater.exe。"; - if (!failedPath.empty()) message += L"\n失败文件:" + failedPath.wstring(); - MessageBoxW(nullptr, message.c_str(), L"更新接管失败", MB_ICONERROR); - return 4; - } + const std::wstring result = success ? L"success" : (rolledBack ? L"rolledback" : L"rollback-failed"); + if (!launchUpdater(updater, updateArgs, result)) { + std::wstring message = L"Cannot restart Updater.exe."; + if (!failedPath.empty()) message += L"\nFailed file: " + failedPath.wstring(); + MessageBoxW(nullptr, message.c_str(), L"Update Handoff Failed", MB_ICONERROR); + return 4; + } return (success || rolledBack) ? 0 : 5; } diff --git a/CMakeLists.txt b/CMakeLists.txt index 2318ef2..012a495 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,70 +1,141 @@ -cmake_minimum_required(VERSION 3.20) -project(ClientAll LANGUAGES C CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(MSVC) - add_compile_options(/utf-8) -endif() -# Qt全局配置 -set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_PREFIX_PATH "C:\\Qt\\5.15.2\\msvc2019_64" ${CMAKE_PREFIX_PATH}) -find_package(Qt5 REQUIRED COMPONENTS Core Network Gui Widgets) -# ========== OpenSSL 手动配置(完全抛弃find_package) ========== -set(OPENSSL_ROOT_DIR "C:/Program Files/OpenSSL-Win64") -set(OPENSSL_INC "${OPENSSL_ROOT_DIR}/include") -set(OPENSSL_LIB_DEBUG "${OPENSSL_ROOT_DIR}/lib/VC/x64/MDd") -set(OPENSSL_LIB_RELEASE "${OPENSSL_ROOT_DIR}/lib/VC/x64/MD") -# 全局宏,所有子项目统一启用OpenSSL -add_compile_definitions(HAVE_OPENSSL=1) -# ========================================================== -# 统一输出目录 -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/lib) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin) -# Bootstrap 不依赖 Qt,可在 Updater 退出后替换 Updater 与 Qt 运行库 -add_subdirectory(Bootstrap) -# 先编译公共库 -add_subdirectory(Common) -# 再编译三个业务程序 -add_subdirectory(Launcher) -add_subdirectory(Updater) -add_subdirectory(MainApp) -# 默认启动项目 -set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Launcher) -# Copy client.ini and config directory to output bin folder -set(APP_CONFIG_SOURCE_FILE "${CMAKE_SOURCE_DIR}/config/app_config.example.json") -set(CONFIG_SOURCE_DIR "${CMAKE_SOURCE_DIR}/config") -set(MANIFEST_PUBLIC_KEY_FILE "${CONFIG_SOURCE_DIR}/manifest_public_key.pem") -set(INI_TARGET_FOLDER "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") -# app_config.json 包含运行时版本状态;如果存在旧 client.ini,则交给客户端首次启动迁移 -file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}") -file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}/config") -if(NOT EXISTS "${INI_TARGET_FOLDER}/config/app_config.json" AND NOT EXISTS "${INI_TARGET_FOLDER}/client.ini") - configure_file("${APP_CONFIG_SOURCE_FILE}" "${INI_TARGET_FOLDER}/config/app_config.json" COPYONLY) -endif() -foreach(RUNTIME_RESOURCE local_state.json version_policy.dat) - if(NOT EXISTS "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}") - configure_file("${CONFIG_SOURCE_DIR}/${RUNTIME_RESOURCE}" "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}" COPYONLY) - endif() -endforeach() -# 编译阶段同步静态配置资源 -add_custom_target(copy_client_resources ALL - COMMAND ${CMAKE_COMMAND} -E make_directory ${INI_TARGET_FOLDER} - COMMAND ${CMAKE_COMMAND} -E make_directory ${INI_TARGET_FOLDER}/config - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${MANIFEST_PUBLIC_KEY_FILE} ${INI_TARGET_FOLDER}/config/manifest_public_key.pem - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${MANIFEST_PUBLIC_KEY_FILE} ${INI_TARGET_FOLDER}/manifest_public_key.pem - COMMENT "Copy client config directory and manifest public key to output bin folder" -) -add_dependencies(Launcher copy_client_resources) -add_dependencies(Updater copy_client_resources) -add_dependencies(MainApp copy_client_resources) -# Install rules for packaging -if(EXISTS ${CONFIG_SOURCE_DIR}) - install(DIRECTORY ${CONFIG_SOURCE_DIR} DESTINATION bin/config) -endif() -if(EXISTS ${MANIFEST_PUBLIC_KEY_FILE}) - install(FILES ${MANIFEST_PUBLIC_KEY_FILE} DESTINATION bin) -endif() \ No newline at end of file +cmake_minimum_required(VERSION 3.20) +project(ClientAll LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(MSVC) + # Source files should be saved as UTF-8 with BOM; compile source and + # execution charsets as UTF-8 instead of setting encodings in code. + add_compile_options(/utf-8) +endif() + +# Qt global config. Qt is intentionally not hard-coded here; configure it with +# CMake-recognized environment variables such as CMAKE_PREFIX_PATH or Qt5_DIR. +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) +find_package(Qt5 REQUIRED COMPONENTS Core Network Gui Widgets) + +get_target_property(QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION) +get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY) +find_program(QT_LRELEASE_EXECUTABLE NAMES lrelease lrelease.exe HINTS "${QT_BIN_DIR}" REQUIRED) +find_program(QT_LUPDATE_EXECUTABLE NAMES lupdate lupdate.exe HINTS "${QT_BIN_DIR}" REQUIRED) + +set(TRANSLATION_TS "${CMAKE_SOURCE_DIR}/i18n/update-client_zh_CN.ts") +set(TRANSLATION_QM "${CMAKE_SOURCE_DIR}/i18n/update-client_zh_CN.qm") +set(TRANSLATION_SCAN_DIRS + "${CMAKE_SOURCE_DIR}/Common" + "${CMAKE_SOURCE_DIR}/Launcher" + "${CMAKE_SOURCE_DIR}/Updater" + "${CMAKE_SOURCE_DIR}/MainApp" +) + +add_custom_command( + OUTPUT "${TRANSLATION_QM}" + COMMAND "${QT_LRELEASE_EXECUTABLE}" "${TRANSLATION_TS}" -qm "${TRANSLATION_QM}" + DEPENDS "${TRANSLATION_TS}" + COMMENT "Compile Qt translation: update-client_zh_CN.qm" + VERBATIM +) +add_custom_target(update_client_translations ALL + DEPENDS "${TRANSLATION_QM}" +) + +add_custom_target(update_client_lupdate + COMMAND "${QT_LUPDATE_EXECUTABLE}" + ${TRANSLATION_SCAN_DIRS} + -extensions cpp,h + -no-obsolete + -ts "${TRANSLATION_TS}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Update Qt translation source: update-client_zh_CN.ts" + VERBATIM +) + +# Local-only third-party dependencies. The thirdparty directory is ignored by Git. +# Override OpenSSL when it is not copied into thirdparty: +# -DSIMCAE_OPENSSL_ROOT= +set(THIRDPARTY_DIR "${CMAKE_SOURCE_DIR}/thirdparty" CACHE PATH "Local third-party dependency root") +set(SIMCAE_OPENSSL_ROOT "${THIRDPARTY_DIR}/OpenSSL-Win64" CACHE PATH "OpenSSL Win64 root") + +# OpenSSL manual config. The project expects the Windows OpenSSL layout. +if(NOT EXISTS "${SIMCAE_OPENSSL_ROOT}/include/openssl") + message(FATAL_ERROR + "OpenSSL not found: ${SIMCAE_OPENSSL_ROOT}\n" + "Copy OpenSSL-Win64 to thirdparty/OpenSSL-Win64, or configure with " + "-DSIMCAE_OPENSSL_ROOT=." + ) +endif() +set(OPENSSL_INC "${SIMCAE_OPENSSL_ROOT}/include") +set(OPENSSL_LIB_DEBUG "${SIMCAE_OPENSSL_ROOT}/lib/VC/x64/MDd") +set(OPENSSL_LIB_RELEASE "${SIMCAE_OPENSSL_ROOT}/lib/VC/x64/MD") +foreach(OPENSSL_LIB_DIR IN ITEMS "${OPENSSL_LIB_DEBUG}" "${OPENSSL_LIB_RELEASE}") + if(NOT EXISTS "${OPENSSL_LIB_DIR}") + message(FATAL_ERROR "OpenSSL library directory not found: ${OPENSSL_LIB_DIR}") + endif() +endforeach() + +add_compile_definitions(HAVE_OPENSSL=1) + +# 统一输出目录 +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin) + +# Bootstrap 不依赖 Qt,可在 Updater 退出后替换 Updater 与 Qt 运行库 +add_subdirectory(Bootstrap) + +# 先编译公共库 +add_subdirectory(Common) + +# 再编译三个业务程序 +add_subdirectory(Launcher) +add_subdirectory(Updater) +add_subdirectory(MainApp) + +# 默认启动项目 +set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Launcher) + +# Copy config templates and static resources to output bin folder. +set(APP_CONFIG_SOURCE_FILE "${CMAKE_SOURCE_DIR}/config/app_config.example.json") +set(CONFIG_SOURCE_DIR "${CMAKE_SOURCE_DIR}/config") +set(MANIFEST_PUBLIC_KEY_FILE "${CONFIG_SOURCE_DIR}/manifest_public_key.pem") +set(INI_TARGET_FOLDER "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + +# app_config.json 包含运行时版本状态;如果存在旧 client.ini,则交给客户端首次启动迁移 +file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}") +file(MAKE_DIRECTORY "${INI_TARGET_FOLDER}/config") +if(NOT EXISTS "${INI_TARGET_FOLDER}/config/app_config.json" AND NOT EXISTS "${INI_TARGET_FOLDER}/client.ini") + configure_file("${APP_CONFIG_SOURCE_FILE}" "${INI_TARGET_FOLDER}/config/app_config.json" COPYONLY) +endif() +foreach(RUNTIME_RESOURCE local_state.json version_policy.dat) + if(EXISTS "${CONFIG_SOURCE_DIR}/${RUNTIME_RESOURCE}" AND NOT EXISTS "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}") + configure_file("${CONFIG_SOURCE_DIR}/${RUNTIME_RESOURCE}" "${INI_TARGET_FOLDER}/config/${RUNTIME_RESOURCE}" COPYONLY) + endif() +endforeach() + +# 编译阶段同步静态配置资源 +add_custom_target(copy_client_resources ALL + COMMAND ${CMAKE_COMMAND} -E make_directory ${INI_TARGET_FOLDER} + COMMAND ${CMAKE_COMMAND} -E make_directory ${INI_TARGET_FOLDER}/config + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${MANIFEST_PUBLIC_KEY_FILE} ${INI_TARGET_FOLDER}/config/manifest_public_key.pem + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${MANIFEST_PUBLIC_KEY_FILE} ${INI_TARGET_FOLDER}/manifest_public_key.pem + COMMENT "Copy client config directory and manifest public key to output bin folder" +) +add_dependencies(Launcher copy_client_resources) +add_dependencies(Updater copy_client_resources) +add_dependencies(MainApp copy_client_resources) +add_dependencies(Launcher update_client_translations) +add_dependencies(Updater update_client_translations) +add_dependencies(MainApp update_client_translations) + +# Install rules for packaging +if(EXISTS "${APP_CONFIG_SOURCE_FILE}") + install(FILES "${APP_CONFIG_SOURCE_FILE}" DESTINATION bin/config RENAME app_config.json) +endif() +if(EXISTS "${MANIFEST_PUBLIC_KEY_FILE}") + install(FILES "${MANIFEST_PUBLIC_KEY_FILE}" DESTINATION bin/config) + install(FILES "${MANIFEST_PUBLIC_KEY_FILE}" DESTINATION bin) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..8d77981 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "windows-x64-base", + "displayName": "Windows x64 Base", + "description": "Windows x64 build with Visual Studio 2022.", + "hidden": true, + "generator": "Visual Studio 17 2022", + "architecture": { + "value": "x64", + "strategy": "set" + }, + "binaryDir": "${sourceDir}/out/build/${presetName}", + "installDir": "${sourceDir}/out/install/${presetName}", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "x64-debug", + "displayName": "x64 Debug", + "description": "Debug build for Windows x64.", + "inherits": "windows-x64-base", + "cacheVariables": { + "CMAKE_CONFIGURATION_TYPES": "Debug", + "CMAKE_INSTALL_CONFIG_NAME": "Debug" + } + }, + { + "name": "x64-release", + "displayName": "x64 Release", + "description": "Release build for Windows x64.", + "inherits": "windows-x64-base", + "cacheVariables": { + "CMAKE_CONFIGURATION_TYPES": "Release", + "CMAKE_INSTALL_CONFIG_NAME": "Release" + } + } + ], + "buildPresets": [ + { + "name": "x64-debug", + "displayName": "x64 Debug", + "configurePreset": "x64-debug", + "configuration": "Debug" + }, + { + "name": "x64-release", + "displayName": "x64 Release", + "configurePreset": "x64-release", + "configuration": "Release" + } + ] +} diff --git a/Common/CMakeLists.txt b/Common/CMakeLists.txt index 4ce9dd7..6e76e53 100644 --- a/Common/CMakeLists.txt +++ b/Common/CMakeLists.txt @@ -1,7 +1,6 @@ -project(Common LANGUAGES C CXX) -find_package(Qt5 REQUIRED COMPONENTS Core Network Widgets) - -set(SRC +project(Common LANGUAGES C CXX) + +set(SRC HttpHelper.h HttpHelper.cpp FileHelper.h @@ -28,11 +27,15 @@ target_include_directories(Common ) # 链接库、库目录通过INTERFACE传递给所有依赖Common的exe -target_link_directories(Common INTERFACE - $<$:${OPENSSL_LIB_DEBUG}> - $<$:${OPENSSL_LIB_RELEASE}> -) -target_link_libraries(Common - PRIVATE Qt5::Core Qt5::Network Qt5::Widgets - INTERFACE libssl.lib libcrypto.lib -) \ No newline at end of file +target_link_directories(Common INTERFACE + $<$:${OPENSSL_LIB_DEBUG}> + $<$>:${OPENSSL_LIB_RELEASE}> +) +target_link_libraries(Common + PRIVATE Qt5::Core Qt5::Network Qt5::Widgets + INTERFACE libssl.lib libcrypto.lib +) + +if(WIN32) + target_link_libraries(Common INTERFACE shell32) +endif() diff --git a/Common/ConfigHelper.cpp b/Common/ConfigHelper.cpp index 30dbc2a..e01d3f1 100644 --- a/Common/ConfigHelper.cpp +++ b/Common/ConfigHelper.cpp @@ -1,27 +1,410 @@ -#include "ConfigHelper.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "ConfigHelper.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_WIN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +namespace +{ +const QString kElevatedConfigSetFlag = QStringLiteral("--simcae-config-set"); +const QString kElevatedFileWriteFlag = QStringLiteral("--simcae-file-write"); +const QString kElevatedFileRemoveFlag = QStringLiteral("--simcae-file-remove"); +const QString kConfigPathPrefix = QStringLiteral("--config-path-b64="); +const QString kConfigKeyPrefix = QStringLiteral("--config-key-b64="); +const QString kConfigValuePrefix = QStringLiteral("--config-value-b64="); +const QString kFilePathPrefix = QStringLiteral("--file-path-b64="); +const QString kFileDataPrefix = QStringLiteral("--file-data-b64="); +const QString kRegistryOrganization = QStringLiteral("Marsco"); +const QString kRegistryApplication = QStringLiteral("UpdateClientSDK"); +const QString kRegistryInstallationsGroup = QStringLiteral("installations"); +const QString kRegistryConfigGroup = QStringLiteral("config"); +const QString kRegistryMetaGroup = QStringLiteral("_meta"); +const QString kRegistrySourceHashKey = QStringLiteral("source_sha256"); +const QString kRegistrySourcePathKey = QStringLiteral("source_path"); +const QString kRegistryRuntimeRootKey = QStringLiteral("runtime_root"); +const QString kRegistryImportedAtKey = QStringLiteral("imported_at_utc"); + +QString encodeArgument(const QString& value) +{ + return QString::fromLatin1(value.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); +} + +QString encodeBytes(const QByteArray& value) +{ + return QString::fromLatin1(value.toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); +} + +QString decodeArgument(const QString& value) +{ + return QString::fromUtf8(QByteArray::fromBase64(value.toLatin1(), + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); +} + +QByteArray decodeBytes(const QString& value) +{ + return QByteArray::fromBase64(value.toLatin1(), + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); +} + +QString findArgumentValue(const QStringList& arguments, const QString& prefix) +{ + for (const QString& argument : arguments) + if (argument.startsWith(prefix)) + return argument.mid(prefix.size()); + return QString(); +} + +bool writeBytesToFile(const QString& path, const QByteArray& bytes, QString* errorMessage) +{ + QDir dir(QFileInfo(path).path()); + if (!dir.exists() && !dir.mkpath(".")) + { + if (errorMessage) + *errorMessage = QString("Cannot create directory: %1").arg(dir.path()); + return false; + } + + QSaveFile output(path); + if (!output.open(QIODevice::WriteOnly)) + { + if (errorMessage) + *errorMessage = QString("Cannot open file for writing: %1").arg(output.errorString()); + return false; + } + if (output.write(bytes) != bytes.size()) + { + if (errorMessage) + *errorMessage = QString("Cannot write full file: %1").arg(output.errorString()); + output.cancelWriting(); + return false; + } + if (!output.commit()) + { + if (errorMessage) + *errorMessage = QString("Cannot commit file: %1").arg(output.errorString()); + return false; + } + if (errorMessage) + errorMessage->clear(); + return true; +} + +bool removeFile(const QString& path, QString* errorMessage) +{ + if (!QFile::exists(path)) + { + if (errorMessage) + errorMessage->clear(); + return true; + } + if (QFile::remove(path)) + { + if (errorMessage) + errorMessage->clear(); + return true; + } + if (errorMessage) + *errorMessage = QString("Cannot remove file: %1").arg(path); + return false; +} + +bool writeConfigValueToFile(const QString& configPath, const QString& key, + const QString& value, QString* errorMessage) +{ + QFile input(configPath); + QJsonObject config; + if (input.exists()) + { + if (!input.open(QIODevice::ReadOnly)) + { + if (errorMessage) + *errorMessage = QString("Cannot open config for reading: %1").arg(input.errorString()); + return false; + } + QJsonParseError parseError; + const QByteArray raw = input.readAll(); + input.close(); + const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) + { + if (errorMessage) + *errorMessage = QString("Invalid config JSON: %1").arg(parseError.errorString()); + return false; + } + config = document.object(); + } + + config.insert(key, value); + QDir dir(QFileInfo(configPath).path()); + if (!dir.exists() && !dir.mkpath(".")) + { + if (errorMessage) + *errorMessage = QString("Cannot create config directory: %1").arg(dir.path()); + return false; + } + + const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented); + return writeBytesToFile(configPath, payload, errorMessage); +} + +#ifdef Q_OS_WIN +QString windowsErrorMessage(DWORD errorCode) +{ + if (errorCode == ERROR_CANCELLED) + return QCoreApplication::translate("ConfigHelper", "The user canceled the administrator permission confirmation."); + return QCoreApplication::translate("ConfigHelper", "Windows error %1").arg(errorCode); +} + +bool runElevatedSelfCommand(const QStringList& arguments, const QString& targetPath, + const QString& originalError, QString* errorMessage) +{ + const QMessageBox::StandardButton choice = QMessageBox::question( + nullptr, + QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"), + QCoreApplication::translate("ConfigHelper", "The current installation directory requires administrator permission to save configuration.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.") + .arg(QDir::toNativeSeparators(targetPath), originalError), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Ok); + if (choice != QMessageBox::Ok) + { + if (errorMessage) + *errorMessage = QCoreApplication::translate("ConfigHelper", "The user canceled the administrator permission request."); + return false; + } + + const QString executable = QCoreApplication::applicationFilePath(); + if (executable.isEmpty() || !QFileInfo::exists(executable)) + { + if (errorMessage) + *errorMessage = QStringLiteral("Cannot locate current executable for elevated config write."); + return false; + } + + const QString parameters = arguments.join(QLatin1Char(' ')); + const QString workingDir = QFileInfo(executable).absolutePath(); + + std::wstring verb = L"runas"; + std::wstring file = executable.toStdWString(); + std::wstring params = parameters.toStdWString(); + std::wstring directory = workingDir.toStdWString(); + + SHELLEXECUTEINFOW info{}; + info.cbSize = sizeof(info); + info.fMask = SEE_MASK_NOCLOSEPROCESS; + info.lpVerb = verb.c_str(); + info.lpFile = file.c_str(); + info.lpParameters = params.c_str(); + info.lpDirectory = directory.c_str(); + info.nShow = SW_HIDE; + + if (!ShellExecuteExW(&info)) + { + const DWORD err = GetLastError(); + if (errorMessage) + *errorMessage = QStringLiteral("Cannot request administrator permission: %1").arg(windowsErrorMessage(err)); + return false; + } + + WaitForSingleObject(info.hProcess, INFINITE); + DWORD exitCode = 1; + GetExitCodeProcess(info.hProcess, &exitCode); + CloseHandle(info.hProcess); + if (exitCode != 0) + { + if (errorMessage) + *errorMessage = QStringLiteral("Elevated config write failed with exit code %1.").arg(exitCode); + return false; + } + + if (errorMessage) + errorMessage->clear(); + return true; +} + +bool writeConfigValueWithElevation(const QString& configPath, const QString& key, + const QString& value, const QString& originalError, + QString* errorMessage) +{ + const QStringList arguments{ + kElevatedConfigSetFlag, + kConfigPathPrefix + encodeArgument(configPath), + kConfigKeyPrefix + encodeArgument(key), + kConfigValuePrefix + encodeArgument(value) + }; + return runElevatedSelfCommand(arguments, configPath, originalError, errorMessage); +} + +bool writeBytesWithElevation(const QString& path, const QByteArray& bytes, + const QString& originalError, QString* errorMessage) +{ + const QStringList arguments{ + kElevatedFileWriteFlag, + kFilePathPrefix + encodeArgument(path), + kFileDataPrefix + encodeBytes(bytes) + }; + return runElevatedSelfCommand(arguments, path, originalError, errorMessage); +} + +bool removeFileWithElevation(const QString& path, const QString& originalError, QString* errorMessage) +{ + const QStringList arguments{ + kElevatedFileRemoveFlag, + kFilePathPrefix + encodeArgument(path) + }; + return runElevatedSelfCommand(arguments, path, originalError, errorMessage); +} +#endif +} + +ConfigHelper& ConfigHelper::instance() +{ + static ConfigHelper obj; + return obj; +} + +int ConfigHelper::runElevatedWriteCommandIfRequested() +{ + const QStringList arguments = QCoreApplication::arguments(); + if (arguments.contains(kElevatedConfigSetFlag)) + { + const QString configPath = decodeArgument(findArgumentValue(arguments, kConfigPathPrefix)); + const QString key = decodeArgument(findArgumentValue(arguments, kConfigKeyPrefix)); + const QString value = decodeArgument(findArgumentValue(arguments, kConfigValuePrefix)); + if (configPath.isEmpty() || key.isEmpty()) + { + qWarning() << "Elevated config write arguments are incomplete."; + return 2; + } + + QString errorMessage; + if (!writeConfigValueToFile(configPath, key, value, &errorMessage)) + { + qWarning() << "Elevated config write failed:" << errorMessage; + return 3; + } + return 0; + } + + if (arguments.contains(kElevatedFileWriteFlag)) + { + const QString path = decodeArgument(findArgumentValue(arguments, kFilePathPrefix)); + const QByteArray bytes = decodeBytes(findArgumentValue(arguments, kFileDataPrefix)); + if (path.isEmpty()) + { + qWarning() << "Elevated file write arguments are incomplete."; + return 2; + } + + QString errorMessage; + if (!writeBytesToFile(path, bytes, &errorMessage)) + { + qWarning() << "Elevated file write failed:" << errorMessage; + return 3; + } + return 0; + } + + if (arguments.contains(kElevatedFileRemoveFlag)) + { + const QString path = decodeArgument(findArgumentValue(arguments, kFilePathPrefix)); + if (path.isEmpty()) + { + qWarning() << "Elevated file remove arguments are incomplete."; + return 2; + } + + QString errorMessage; + if (!removeFile(path, &errorMessage)) + { + qWarning() << "Elevated file remove failed:" << errorMessage; + return 3; + } + return 0; + } + + return -1; +} + +bool ConfigHelper::writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data, + QString* errorMessage) +{ + QString localError; + if (writeBytesToFile(path, data, &localError)) + { + if (errorMessage) + errorMessage->clear(); + return true; + } + +#ifdef Q_OS_WIN + if (data.size() > 24 * 1024) + { + if (errorMessage) + *errorMessage = QString("File is too large for elevated inline write: %1 bytes. Original error: %2") + .arg(data.size()).arg(localError); + return false; + } + return writeBytesWithElevation(path, data, localError, errorMessage); +#else + if (errorMessage) + *errorMessage = localError; + return false; +#endif +} + +bool ConfigHelper::removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage) +{ + QString localError; + if (removeFile(path, &localError)) + { + if (errorMessage) + errorMessage->clear(); + return true; + } + +#ifdef Q_OS_WIN + return removeFileWithElevation(path, localError, errorMessage); +#else + if (errorMessage) + *errorMessage = localError; + return false; +#endif +} -ConfigHelper& ConfigHelper::instance() -{ - static ConfigHelper obj; - return obj; -} - -ConfigHelper::ConfigHelper() -{ - m_configPath = QApplication::applicationDirPath() + "/config/app_config.json"; - migrateLegacyIniIfNeeded(); - qDebug() << "Loading app config path:" << m_configPath; - qDebug() << "File exists?" << QFile::exists(m_configPath); -} +ConfigHelper::ConfigHelper() +{ + m_configPath = QApplication::applicationDirPath() + "/config/app_config.json"; + m_registryInstallId = QString::fromLatin1(QCryptographicHash::hash( + QDir::cleanPath(QApplication::applicationDirPath()).toUtf8(), + QCryptographicHash::Sha256).toHex()); + migrateLegacyIniIfNeeded(); + syncRegistryFromConfigFileIfChanged(); + qDebug() << "Loading app config path:" << m_configPath; + qDebug() << "File exists?" << QFile::exists(m_configPath); + qDebug() << "Registry installation id:" << m_registryInstallId; +} QString ConfigHelper::configPath() const { @@ -57,80 +440,167 @@ QString ConfigHelper::runtimeRelativePath() const return relative.trimmed(); } -QString ConfigHelper::lastError() const -{ - return m_error; -} - -QString ConfigHelper::getValue(const QString& section, const QString& key) const -{ - Q_UNUSED(section); - QFile file(m_configPath); - if (!file.open(QIODevice::ReadOnly)) - return QString(); - - QJsonParseError error; - const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error); - if (error.error != QJsonParseError::NoError || !document.isObject()) - return QString(); - - return document.object().value(key).toVariant().toString(); -} - -bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value) -{ - Q_UNUSED(section); - m_error.clear(); - - QFile input(m_configPath); - QJsonObject config; - if (input.exists()) - { - if (!input.open(QIODevice::ReadOnly)) - { - m_error = QString("Cannot open config for reading: %1").arg(input.errorString()); - return false; - } - QJsonParseError parseError; - const QByteArray raw = input.readAll(); - input.close(); - const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError); - if (parseError.error != QJsonParseError::NoError || !document.isObject()) - { - m_error = QString("Invalid config JSON: %1").arg(parseError.errorString()); - return false; - } - config = document.object(); - } - - config.insert(key, value); - QDir dir(QFileInfo(m_configPath).path()); - if (!dir.exists() && !dir.mkpath(".")) - { - m_error = QString("Cannot create config directory: %1").arg(dir.path()); - return false; - } - - QSaveFile output(m_configPath); - if (!output.open(QIODevice::WriteOnly)) - { - m_error = QString("Cannot open config for writing: %1").arg(output.errorString()); - return false; - } - const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented); - if (output.write(payload) != payload.size()) - { - m_error = QString("Cannot write full config file: %1").arg(output.errorString()); - output.cancelWriting(); - return false; - } - if (!output.commit()) - { - m_error = QString("Cannot commit config file: %1").arg(output.errorString()); - return false; - } - return true; -} +QString ConfigHelper::lastError() const +{ + return m_error; +} + +void ConfigHelper::enterRegistryGroup(QSettings& settings) const +{ + settings.beginGroup(kRegistryInstallationsGroup); + settings.beginGroup(m_registryInstallId); +} + +bool ConfigHelper::syncRegistryFromConfigFileIfChanged() +{ + QFile file(m_configPath); + if (!file.exists()) + return true; + + if (!file.open(QIODevice::ReadOnly)) + { + m_error = QStringLiteral("Cannot open config for reading: %1").arg(file.errorString()); + return false; + } + + QJsonParseError parseError; + const QByteArray raw = file.readAll(); + const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) + { + m_error = QStringLiteral("Invalid config JSON: %1").arg(parseError.errorString()); + return false; + } + + const QByteArray normalizedConfig = QJsonDocument(document.object()).toJson(QJsonDocument::Compact); + const QString sourceHash = QString::fromLatin1( + QCryptographicHash::hash(normalizedConfig, QCryptographicHash::Sha256).toHex()); + + QSettings settings(QSettings::NativeFormat, QSettings::UserScope, + kRegistryOrganization, kRegistryApplication); + enterRegistryGroup(settings); + settings.beginGroup(kRegistryMetaGroup); + const QString previousHash = settings.value(kRegistrySourceHashKey).toString(); + settings.endGroup(); + + if (previousHash == sourceHash) + return true; + + const bool configChangedAfterPreviousImport = !previousHash.isEmpty(); + + settings.beginGroup(kRegistryConfigGroup); + settings.remove(QString()); + const QJsonObject config = document.object(); + for (auto it = config.constBegin(); it != config.constEnd(); ++it) + settings.setValue(it.key(), it.value().toVariant()); + settings.endGroup(); + + settings.beginGroup(kRegistryMetaGroup); + settings.setValue(kRegistrySourceHashKey, sourceHash); + settings.setValue(kRegistrySourcePathKey, QDir::toNativeSeparators(QFileInfo(m_configPath).absoluteFilePath())); + settings.setValue(kRegistryRuntimeRootKey, QDir::toNativeSeparators(QApplication::applicationDirPath())); + settings.setValue(kRegistryImportedAtKey, QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); + settings.endGroup(); + settings.sync(); + + if (settings.status() != QSettings::NoError) + { + m_error = QStringLiteral("Cannot sync config to registry."); + return false; + } + + m_error.clear(); + qDebug() << "Synced app_config.json to registry installation id:" << m_registryInstallId; + + if (configChangedAfterPreviousImport) + { + const QString configDir = QFileInfo(m_configPath).absolutePath(); + const QStringList staleFiles{ + QDir(configDir).filePath(QStringLiteral("client_identity.dat")), + QDir(configDir).filePath(QStringLiteral("version_policy.dat")) + }; + for (const QString& staleFile : staleFiles) + { + QString removeError; + if (!ConfigHelper::removeFileWithElevationIfNeeded(staleFile, &removeError)) + { + m_error = QStringLiteral("Cannot remove stale runtime file after config change: %1. %2") + .arg(staleFile, removeError); + return false; + } + } + qDebug() << "Removed stale client identity and policy after app_config.json changed."; + } + return true; +} + +bool ConfigHelper::readRegistryValue(const QString& key, QString* value) const +{ + QSettings settings(QSettings::NativeFormat, QSettings::UserScope, + kRegistryOrganization, kRegistryApplication); + enterRegistryGroup(settings); + settings.beginGroup(kRegistryConfigGroup); + if (!settings.contains(key)) + { + settings.endGroup(); + return false; + } + if (value) + *value = settings.value(key).toString(); + settings.endGroup(); + return true; +} + +bool ConfigHelper::writeRegistryValue(const QString& key, const QString& value) +{ + QSettings settings(QSettings::NativeFormat, QSettings::UserScope, + kRegistryOrganization, kRegistryApplication); + enterRegistryGroup(settings); + settings.beginGroup(kRegistryConfigGroup); + settings.setValue(key, value); + settings.endGroup(); + settings.sync(); + + if (settings.status() != QSettings::NoError) + { + m_error = QStringLiteral("Cannot write config value to registry: %1").arg(key); + return false; + } + + m_error.clear(); + return true; +} + +QString ConfigHelper::readFileValue(const QString& key) const +{ + QFile file(m_configPath); + if (!file.open(QIODevice::ReadOnly)) + return QString(); + + QJsonParseError error; + const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) + return QString(); + + return document.object().value(key).toVariant().toString(); +} + +QString ConfigHelper::getValue(const QString& section, const QString& key) const +{ + Q_UNUSED(section); + QString value; + if (readRegistryValue(key, &value)) + return value; + return readFileValue(key); +} + +bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value) +{ + Q_UNUSED(section); + m_error.clear(); + + return writeRegistryValue(key, value); +} bool ConfigHelper::migrateLegacyIniIfNeeded() { diff --git a/Common/ConfigHelper.h b/Common/ConfigHelper.h index f604701..35d1025 100644 --- a/Common/ConfigHelper.h +++ b/Common/ConfigHelper.h @@ -1,22 +1,35 @@ -#pragma once -#include - -class ConfigHelper -{ -public: - static ConfigHelper& instance(); - QString getValue(const QString& section, const QString& key) const; +#pragma once +#include +#include + +class QSettings; + +class ConfigHelper +{ +public: + static ConfigHelper& instance(); + static int runElevatedWriteCommandIfRequested(); + static bool writeFileWithElevationIfNeeded(const QString& path, const QByteArray& data, + QString* errorMessage = nullptr); + static bool removeFileWithElevationIfNeeded(const QString& path, QString* errorMessage = nullptr); + QString getValue(const QString& section, const QString& key) const; bool setValue(const QString& section, const QString& key, const QString& value); QString configPath() const; QString installRoot() const; QString runtimeRoot() const; QString updateRoot() const; - QString runtimeRelativePath() const; - QString lastError() const; - -private: - ConfigHelper(); - bool migrateLegacyIniIfNeeded(); - QString m_configPath; - QString m_error; -}; + QString runtimeRelativePath() const; + QString lastError() const; + +private: + ConfigHelper(); + bool migrateLegacyIniIfNeeded(); + void enterRegistryGroup(QSettings& settings) const; + bool syncRegistryFromConfigFileIfChanged(); + bool readRegistryValue(const QString& key, QString* value) const; + bool writeRegistryValue(const QString& key, const QString& value); + QString readFileValue(const QString& key) const; + QString m_configPath; + QString m_registryInstallId; + QString m_error; +}; diff --git a/Common/DeviceIdentityHelper.cpp b/Common/DeviceIdentityHelper.cpp index 1c1a18b..94ed261 100644 --- a/Common/DeviceIdentityHelper.cpp +++ b/Common/DeviceIdentityHelper.cpp @@ -8,12 +8,11 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #ifdef HAVE_OPENSSL #include #include @@ -45,5 +44,5 @@ bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token ConfigHelper& config=ConfigHelper::instance(); QString installation=config.getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!config.setValue("Device","installation_id",installation)){m_error=QString("cannot save installation id to %1: %2").arg(config.configPath(),config.lastError());return false;}} QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}}; - QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error=QString("cannot save device credential to %1: %2").arg(path,out.errorString());return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true; -} + QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;bool timeoutOk=false;int timeoutMs=ConfigHelper::instance().getValue("Update","request_timeout_ms").toInt(&timeoutOk);if(!timeoutOk||timeoutMs<1000)timeoutMs=5000;QTimer timer;timer.setSingleShot(true);QObject::connect(&timer,&QTimer::timeout,[&](){if(reply&&reply->isRunning())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);QString writeError;if(!ConfigHelper::writeFileWithElevationIfNeeded(path,bytes,&writeError)){m_error=QString("cannot save device credential to %1: %2").arg(path,writeError);return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true; +} diff --git a/Common/FileHelper.cpp b/Common/FileHelper.cpp index 5aaf239..5f4526e 100644 --- a/Common/FileHelper.cpp +++ b/Common/FileHelper.cpp @@ -13,11 +13,11 @@ bool FileHelper::copyFileOverwrite(const QString &src, const QString &dst) { if (QFile::exists(dst)) { - if (!QFile::remove(dst)) - { - qDebug() << "无法删除旧文件:" << dst; - return false; - } + if (!QFile::remove(dst)) + { + qDebug() << "Cannot remove old file:" << dst; + return false; + } } return QFile::copy(src, dst); } @@ -39,4 +39,4 @@ bool FileHelper::killProcess(const QString &exeName) process.start("taskkill /f /im " + exeName); process.waitForFinished(1000); return !isProcessRunning(exeName); -} \ No newline at end of file +} diff --git a/Common/HttpHelper.h b/Common/HttpHelper.h index 3bef462..52dcc29 100644 --- a/Common/HttpHelper.h +++ b/Common/HttpHelper.h @@ -11,7 +11,7 @@ class HttpHelper { public: - // 每次调用独立创建manager,不做成成员变量 + // Create an independent manager for each call instead of keeping it as a member. static void postRequest(const QString& url, const QJsonObject& jsonBody, std::function callback); -}; \ No newline at end of file +}; diff --git a/Common/LocalStateHelper.cpp b/Common/LocalStateHelper.cpp index fc9ae79..b4eccfc 100644 --- a/Common/LocalStateHelper.cpp +++ b/Common/LocalStateHelper.cpp @@ -1,8 +1,7 @@ -#include "LocalStateHelper.h" -#include -#include -#include -#include +#include "LocalStateHelper.h" +#include "ConfigHelper.h" +#include +#include LocalStateHelper::LocalStateHelper(const QString& baseDir) : m_baseDir(baseDir) @@ -13,19 +12,12 @@ LocalStateHelper::LocalStateHelper(const QString& baseDir) bool LocalStateHelper::loadState(const QString& relativePath) { m_filePath = m_baseDir + "/" + relativePath; - QFile file(m_filePath); - if (!file.exists()) - { - QDir dir(QFileInfo(m_filePath).path()); - if (!dir.exists() && !dir.mkpath(".")) - { - m_error = QString("Cannot create state directory: %1").arg(dir.path()); - return false; - } - - m_state = QJsonObject{ - {"max_policy_seq", 0}, - {"last_success_run_at", QString()}, + QFile file(m_filePath); + if (!file.exists()) + { + m_state = QJsonObject{ + {"max_policy_seq", 0}, + {"last_success_run_at", QString()}, {"last_online_verified_at", QString()}, {"last_success_version", QString()} }; @@ -60,22 +52,24 @@ bool LocalStateHelper::loadState(const QString& relativePath) return true; } -bool LocalStateHelper::saveState() const -{ - if (!m_loaded) - return false; - - QFile file(m_filePath); - if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) - { - return false; - } - - QJsonDocument doc(m_state); - file.write(doc.toJson(QJsonDocument::Indented)); - file.close(); - return true; -} +bool LocalStateHelper::saveState() const +{ + if (!m_loaded) + { + m_error = "State is not loaded"; + return false; + } + + QJsonDocument doc(m_state); + QString writeError; + if (!ConfigHelper::writeFileWithElevationIfNeeded(m_filePath, doc.toJson(QJsonDocument::Indented), &writeError)) + { + m_error = QString("Cannot save state file: %1").arg(writeError); + return false; + } + m_error.clear(); + return true; +} bool LocalStateHelper::isLoaded() const { diff --git a/Common/LocalStateHelper.h b/Common/LocalStateHelper.h index 86d2999..03b63c9 100644 --- a/Common/LocalStateHelper.h +++ b/Common/LocalStateHelper.h @@ -27,7 +27,7 @@ public: private: QString m_baseDir; QString m_filePath; - QString m_error; + mutable QString m_error; QJsonObject m_state; bool m_loaded = false; }; diff --git a/Common/PolicyHelper.cpp b/Common/PolicyHelper.cpp index 64b0917..fc64aa4 100644 --- a/Common/PolicyHelper.cpp +++ b/Common/PolicyHelper.cpp @@ -1,11 +1,11 @@ -#include "PolicyHelper.h" -#include -#include -#include -#include -#include -#include -#include +#include "PolicyHelper.h" +#include "ConfigHelper.h" +#include +#include +#include +#include +#include +#include #ifdef HAVE_OPENSSL #include #include @@ -31,13 +31,17 @@ bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& si return isValid(); } -bool PolicyHelper::savePolicy(const QString& relativePath) const -{ - QSaveFile file(m_baseDir + "/" + relativePath); - if (!file.open(QIODevice::WriteOnly)) return false; - const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented); - return file.write(bytes) == bytes.size() && file.commit(); -} +bool PolicyHelper::savePolicy(const QString& relativePath) const +{ + const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented); + QString writeError; + if (!ConfigHelper::writeFileWithElevationIfNeeded(m_baseDir + "/" + relativePath, bytes, &writeError)) { + m_error = "Cannot save policy file: " + writeError; + return false; + } + m_error.clear(); + return true; +} QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const { diff --git a/Common/TicketHelper.cpp b/Common/TicketHelper.cpp index 04b4fbe..5222536 100644 --- a/Common/TicketHelper.cpp +++ b/Common/TicketHelper.cpp @@ -64,7 +64,7 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex return false; } const QByteArray raw = file.readAll(); file.close(); - QFile::remove(consumingPath); // 一次性消费;无论成功失败都不能重放。 + QFile::remove(consumingPath); // Consume once; it must not be replayed regardless of success or failure. QJsonParseError parseError; const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError); if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { diff --git a/Docs/ReadMe.txt b/Docs/ReadMe.txt new file mode 100644 index 0000000..f9aff77 --- /dev/null +++ b/Docs/ReadMe.txt @@ -0,0 +1,47 @@ +客户端文档入口 +============== + +本目录用于保存 update-client 客户端子仓库的说明文档。 + +建议阅读顺序: + +1. 客户端部署说明.md + 面向接入和部署,说明 SDK 是什么、怎么放到业务软件目录、app_config.json 怎么填、如何打包。 + +2. 第三方依赖说明.md + 面向编译环境,说明 Qt、OpenSSL、thirdparty/ 和 CMake 环境变量怎么配置。 + +3. 本 ReadMe.txt + 记录客户端配置文件和打包脚本的简要说明。 + + +客户端配置说明 +============== + +config/app_config.json 是部署配置源文件。Launcher / Updater / MainApp 启动时会把它同步到当前 Windows 用户的注册表,后续运行配置优先从注册表读取。 +如果 config/app_config.json 内容被修改,下一次启动时会按解析后的 JSON 内容 SHA256 判断变化并重新导入注册表。 +注册表位置:HKEY_CURRENT_USER\Software\Marsco\UpdateClientSDK\installations\<安装目录SHA256>\config。 +如果检测到 app_config.json 发生变化,SDK 会删除 config/client_identity.dat 和 config/version_policy.dat,避免继续使用旧授权身份或旧策略;目录不可写时会弹出管理员权限确认框。 +首次运行时如果该文件不存在且发现旧 client.ini,会自动迁移。 + +接入新软件时通常需要修改: + +1. app_id、app_name、channel、current_version。 +2. api_base_url、client_token、license_key、launch_token。 +3. main_executable:团队业务主程序文件名。 +4. launcher_executable、updater_executable、bootstrap_executable。 +5. health_check_timeout_ms:升级后等待业务程序健康确认的毫秒数,最小 1000。 + +完整格式参考 update-client/config/app_config.example.json。 +运行时生成的 client_identity.dat、local_state.json 等文件不得打入通用 SDK 模板。app_config.json 可以作为部署模板,但不要把某台机器运行后产生的临时状态混进去。 + +Windows 发布打包: + +1. 使用 Release 配置编译全部客户端程序。 +2. 先完成当前版本在线校验,确认 out/bin/update/manifest_cache 中存在对应的签名 Manifest。 +3. 准备一份实际 app_config.json,确认其中包含正确的 License Key、当前版本和业务程序名。 +4. 在 PowerShell 执行: + powershell -ExecutionPolicy Bypass -File .\scripts\package-client.ps1 -ConfigFile .\config\app_config.json +5. 输出位于 dist/UpdateClient 和 dist/UpdateClient.zip。 + +脚本会拒绝 Debug DLL、PDB、嵌套重复主程序和缺少签名 Manifest 的发布源目录。 diff --git a/Docs/客户端部署说明.md b/Docs/客户端部署说明.md new file mode 100644 index 0000000..4482b29 --- /dev/null +++ b/Docs/客户端部署说明.md @@ -0,0 +1,299 @@ +# UpdateClientSDK 客户端部署说明 + +本文按“维护者打包 SDK -> 你接入业务软件 -> 联调测试 -> 生成最终客户端包”的顺序说明。你拿到这份文档后,按章节一步一步做即可。 + +## 一、这个 SDK 是什么 + +UpdateClientSDK 是“独立更新器 SDK / 升级运行时 SDK”。它不是传统的 `include + lib` 形态,而是把自动升级能力做成一组独立程序,让业务软件通过这些程序完成检查更新、下载、安装、回滚和启动保护。 + +SDK 核心程序: + +- `Launcher.exe`:用户入口。检查版本、验证授权和策略,决定直接启动业务主程序或进入升级流程。 +- `Updater.exe`:下载、校验、备份、安装、健康确认、提交或回滚。 +- `Bootstrap.exe`:处理运行中可能被占用的 EXE/DLL 替换。 +- `config/app_config.json`:部署配置源文件。启动时会同步到当前 Windows 用户的注册表,运行时优先读注册表。 +- `config/manifest_public_key.pem`:Manifest 签名公钥,用来验证服务端发布包没有被篡改。 + +## 二、维护者:生成 SDK 包 + +这一节是 SDK 维护者操作。接入方通常只需要拿到 `UpdateClientSDK.zip`。 + +打包前确认: + +1. 已在 Windows 上用 Release 配置编译完成,输出目录里有 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe`。 +2. `config/manifest_public_key.pem` 和服务端使用的私钥是一对。 +3. SDK Word 接入说明已经放在 `update-client` 根目录或 `update-client/Docs` 目录下,文件名包含 `SDK`,例如 `SimCAE自动升级SDK接入说明_v0.1.docx`。打包脚本会把它复制到 SDK 根目录,方便接入方一打开压缩包就能看到。 +4. 如果业务软件本身已经带 Qt DLL,通常不要把 SDK 的 Qt 运行库打进去,避免 Qt 版本混用。 + +在 Windows PowerShell 中执行: + +```powershell +cd C:\Users\admin\Desktop\update-client + +.\scripts\package-sdk.ps1 ` + -SourceDir .\out\bin ` + -OutputDir .\dist\UpdateClientSDK ` + -ZipFile .\dist\UpdateClientSDK.zip ` + -SdkVersion 0.1.0 +``` + +生成结果: + +```text +dist/ + UpdateClientSDK/ + SimCAE自动升级SDK接入说明_v0.1.docx + sdk_manifest.json + bin/ + Launcher.exe + Updater.exe + Bootstrap.exe + ... + config/ + app_config.json + manifest_public_key.pem + scripts/ + install-sdk.ps1 + package-client.ps1 + package-sdk.ps1 + UpdateClientSDK.zip +``` + +把 `dist/UpdateClientSDK.zip` 发给接入方即可。 + +可选参数: + +- `-IncludeDemoMainApp`:把仓库里的 Demo 主程序 `MainApp.exe` 也打进 SDK,方便演示。 +- `-IncludeQtRuntime`:把 Qt 运行库也打进 SDK。只有业务软件本身不带 Qt 时才建议使用。 + +## 三、你:把 SDK 放进业务软件目录 + +假设业务软件目录是: + +```text +D:\SimCAE\ + bin\ + SimCAE.exe + Qt5Core.dll + ... + Licenses\ + installerResources\ +``` + +推荐把 SDK 放到 `bin` 目录,和 `SimCAE.exe` 同级;后台发布新版本时仍选择整个 `D:\SimCAE\` 作为发布根目录。 + +先解压 SDK: + +```powershell +Expand-Archive D:\交付\UpdateClientSDK.zip -DestinationPath D:\SimCAE_SDK -Force +``` + +再安装到业务软件的 `bin` 目录: + +```powershell +cd D:\SimCAE\bin + +D:\SimCAE_SDK\scripts\install-sdk.ps1 ` + -SdkRoot D:\SimCAE_SDK ` + -ReleaseDir . +``` + +安装后目录应类似: + +```text +D:\SimCAE\ + bin\ + Launcher.exe + Updater.exe + Bootstrap.exe + SimCAE.exe + config\ + app_config.json + manifest_public_key.pem + Qt5Core.dll + ... + Licenses\ + installerResources\ +``` + +如果你需要重新覆盖 `config/app_config.json`,执行安装脚本时加 `-OverwriteConfig`: + +```powershell +D:\SimCAE_SDK\scripts\install-sdk.ps1 ` + -SdkRoot D:\SimCAE_SDK ` + -ReleaseDir D:\SimCAE\bin ` + -OverwriteConfig +``` + +注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应复用 SimCAE 的 `Qt5*.dll`、`platforms/`、`imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则可能出现“无法定位程序输入点”一类错误。 + +## 四、你:生成并填写 app_config.json + +最推荐的方式是在服务端管理后台生成客户端配置: + +1. 浏览器打开服务端管理后台,例如 `http://服务器IP:8000/`。 +2. 登录后台。 +3. 创建或选择应用,例如 `app_id=simcae`。 +4. 创建 License。 +5. 在“客户端配置生成”区域选择应用、渠道、License 和主程序名。 +6. 点击生成配置。 +7. 复制生成的 JSON,覆盖 `D:\SimCAE\bin\config\app_config.json`。 + +配置同步规则: + +- `app_config.json` 是部署配置源文件,适合交付、复制、人工修改。 +- Launcher / Updater / MainApp 启动时会计算 `app_config.json` 解析后的 JSON 内容 SHA256;如果 JSON 内容和上次导入时不同,就把文件里的配置重新写入当前 Windows 用户的注册表。 +- 后续运行时优先从注册表读取配置,不再每次直接读 JSON。 +- 运行过程中产生的动态值,例如首次输入的 `license_key`、服务端返回的 `device_id`、升级后的 `current_version`,会写入注册表。 +- 如果你手动修改了 `app_config.json`,下一次启动会重新导入并覆盖注册表里的同名字段。 +- 如果检测到 `app_config.json` 确实发生变化,SDK 会同时删除 `config/client_identity.dat` 和 `config/version_policy.dat`,避免继续使用旧 License、旧设备身份或旧版本策略;如果安装目录不可写,会弹出管理员权限确认框。 +- 注册表按安装目录隔离;同一台电脑上多个安装目录不会互相覆盖配置。 +- 注册表位置为 `HKEY_CURRENT_USER\Software\Marsco\UpdateClientSDK\installations\<安装目录SHA256>\config`。 + +关键字段说明: + +- `app_id`:服务端应用 ID,要和管理后台里的应用一致。 +- `app_name`:应用显示名称。 +- `channel`:发布渠道,例如 `stable`、`beta`、`dev`。 +- `current_version`:客户端当前初始版本,必须和后台已发布的版本一致。 +- `client_protocol`:客户端协议号,当前建议为 `3`。 +- `launch_token`:本机启动票据 HMAC 密钥。服务端生成配置时会填默认值;正式部署建议按项目统一修改。 +- `license_key`:管理后台创建 License 后生成的授权码。为空时首次启动 `Launcher.exe` 会弹窗让用户输入并保存到注册表;如果授权错误或过期,也会提示重新输入。 +- `api_base_url`:服务端 API 地址,例如 `http://192.168.229.128:8000`。 +- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,必须和服务端一致。 +- `device_id`:设备 ID。一般可以留空,首次启动时 SDK 会向服务端登记并写入注册表。 +- `install_root`:被更新的安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`。 +- `main_executable`:业务主程序相对 `Launcher.exe` 所在目录的路径。SDK 放在 `bin` 且主程序也在 `bin` 时填 `SimCAE.exe`。 +- `launcher_executable`、`updater_executable`、`bootstrap_executable`:通常不用改。 +- `platform`、`arch`:当前为 `windows`、`x64`。 + +典型配置: + +```json +{ + "app_id": "simcae", + "app_name": "SimCAE", + "channel": "stable", + "current_version": "1.0.0", + "client_protocol": "3", + "launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes", + "license_key": "MARSCO-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "api_base_url": "http://192.168.229.128:8000", + "client_token": "SimCAEClientToken2026", + "request_timeout_ms": "5000", + "temp_folder": "update_temp", + "device_id": "", + "install_root": "..", + "main_executable": "SimCAE.exe", + "launcher_executable": "Launcher.exe", + "updater_executable": "Updater.exe", + "bootstrap_executable": "Bootstrap.exe", + "health_check_timeout_ms": "15000", + "platform": "windows", + "arch": "x64" +} +``` + +## 五、你:业务主程序需要配合什么 + +当前安全模式下,业务主程序需要配合两件事: + +1. 接收 `--ticket-file=` 参数,验证并消费一次性启动票据。 +2. 如果收到 `--health-file=` 参数,启动成功后向该路径写入 `ok\n`,让 Updater 确认新版本可用。 + +接入位置: + +```text +main / WinMain 开头,创建主窗口之前 +``` + +当前仓库里的 `update-client/MainApp/main.cpp` 是接入示例,已经实现: + +- 启动票据校验。 +- 本地 License/设备身份校验。 +- 本地策略校验。 +- Manifest 完整性校验。 +- 健康标记写入。 + +真正接入业务软件时,把这些启动检查逻辑移植到业务主程序。用户入口应改成 `Launcher.exe`,不要让用户直接双击 `SimCAE.exe`。 + +## 六、你:准备服务端数据 + +首次联调前,服务端至少要准备这些内容: + +1. 创建应用,例如 `simcae`。 +2. 创建渠道,例如 `stable`。 +3. 创建 License。 +4. 发布一个初始版本,例如 `1.0.0`。 +5. 生成客户端配置,并写入 `bin\config\app_config.json`。客户端下次启动时会自动同步到注册表。 + +为什么必须先发布初始版本:Launcher 启动业务主程序前会做 Manifest 完整性校验。这个 Manifest 是服务端发布版本时生成并签名的清单,用来证明当前本地文件属于一个可信版本。如果没有发布过 `current_version` 对应版本,客户端会提示签名 Manifest 缓存缺失。 + +后台发布版本时,选择整个安装根目录,例如 `D:\SimCAE\`,不要只选择 `D:\SimCAE\bin`。这样服务端会把 `bin/SimCAE.exe`、`Licenses/`、`installerResources/` 等完整结构写进 Manifest。 + +## 七、你:运行和联调 + +基础联调步骤: + +1. 确认服务端正在运行。 +2. 确认 `bin\config\app_config.json` 中 `api_base_url`、`client_token`、`license_key`、`current_version` 正确。 +3. 双击 `bin\Launcher.exe`。 +4. 首次启动时如果 `license_key` 为空,按弹窗输入后台创建的 License;SDK 会把它保存到注册表。 +5. 成功进入业务主程序后,回到后台查看设备、升级日志、下载日志。 +6. 在后台发布更高版本,例如从 `1.0.0` 发布到 `1.0.1`。 +7. 再次启动 `Launcher.exe`,验证升级、健康确认和回滚逻辑。 + +联调目录建议: + +```text +D:\SimCAE_Release\ +C:\Users\<你的用户名>\Desktop\SimCAE_Release\ +``` + +如果安装在 `C:\Program Files\...` 这类默认不可写目录,普通配置值会写入当前用户注册表,不需要修改 `app_config.json`。但 `client_identity.dat`、`local_state.json`、`version_policy.dat` 等本地状态文件仍位于安装目录下;当这些文件需要写入且目录不可写时,SDK 会弹出 Windows 管理员权限确认框,用户点击“是”后会继续保存。 + +注意:这次提权主要覆盖小型配置/状态文件的写入和删除。更新缓存、离线包暂存、升级替换 EXE/DLL 等大文件操作仍建议放在可写目录;如果最终产品必须完整安装到 `C:\Program Files\SimCAE\bin` 并在普通用户下自动升级,后续建议把运行时状态迁移到 `ProgramData` / `AppData`,或让 Updater/Bootstrap 在替换安装目录文件时走管理员权限。 + +## 八、维护者:生成某个产品的最终客户端包 + +SDK 是给接入方开发使用的。最终给用户安装或分发时,可以从已经联调过的 Release 目录生成最终客户端包。 + +在 Windows PowerShell 中执行: + +```powershell +cd C:\Users\admin\Desktop\update-client + +.\scripts\package-client.ps1 ` + -SourceDir .\out\bin ` + -ConfigFile .\config\app_config.json ` + -OutputDir .\dist\UpdateClient ` + -ZipFile .\dist\UpdateClient.zip +``` + +`package-client.ps1` 会检查: + +- 配置文件必填字段是否完整。 +- 主程序、Launcher、Updater、Bootstrap 是否存在。 +- 是否混入 Debug DLL、PDB、ILK。 +- 当前版本是否已有签名 Manifest 缓存。 +- 是否存在重复主程序。 + +生成结果: + +```text +dist/ + UpdateClient/ + UpdateClient.zip +``` + +## 九、常见错误 + +1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。 +2. 首次启动保存配置/状态文件失败:如果目录不可写,SDK 会弹出管理员权限确认框;用户取消或当前账号没有管理员权限时仍会失败。 +3. 首次启动设备登记失败:检查 `api_base_url`、`client_token`、`license_key`、服务端 License 状态。 +4. 提示 License 错误或过期:在后台确认 License 是否存在、是否被禁用或删除、是否超过最大设备数。 +5. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配。 +6. 提示 signed manifest cache missing:先在后台发布一次 `current_version` 对应版本,并让客户端拿到该版本 Manifest。 +7. 升级后回滚:检查业务程序是否在 `health_check_timeout_ms` 内写入健康标记。 +8. 发布失败提示主程序不在根目录:服务端 `.env` 中 `RELEASE_MAIN_EXECUTABLE` 默认是 `bin/SimCAE.exe`,发布目录要选择包含 `bin/SimCAE.exe` 的安装根目录。 +9. 启动时提示 `无法定位程序输入点 ... Qt5*.dll`:通常是 Qt DLL 被不同版本覆盖或混用。恢复业务软件原始 Qt DLL,并重新打包 SDK;SimCAE 场景下不要使用 `-IncludeQtRuntime`。 diff --git a/Docs/第三方依赖说明.md b/Docs/第三方依赖说明.md new file mode 100644 index 0000000..124603c --- /dev/null +++ b/Docs/第三方依赖说明.md @@ -0,0 +1,117 @@ +# 第三方依赖说明 + +`thirdparty/` 是本机依赖目录,已经被 `.gitignore` 忽略,不会提交到 Git。 + +当前客户端构建依赖: + +1. Qt 5.15.2 msvc2019_64 +2. OpenSSL-Win64 + +## 1. Qt 配置 + +Qt 路径由本机环境变量提供。你需要在 Windows 环境变量里配置 Qt 路径,让 CMake 的 `find_package(Qt5 ...)` 能找到 Qt。 + +推荐配置用户环境变量 `CMAKE_PREFIX_PATH`: + +```powershell +[Environment]::SetEnvironmentVariable("CMAKE_PREFIX_PATH", "C:\Qt\5.15.2\msvc2019_64", "User") +``` + +设置完成后,重新打开 PowerShell 或 Visual Studio。 + +如果只想对当前 PowerShell 窗口临时生效: + +```powershell +$env:CMAKE_PREFIX_PATH="C:\Qt\5.15.2\msvc2019_64" +``` + +也可以配置更精确的 `Qt5_DIR`: + +```powershell +[Environment]::SetEnvironmentVariable("Qt5_DIR", "C:\Qt\5.15.2\msvc2019_64\lib\cmake\Qt5", "User") +``` + +`CMAKE_PREFIX_PATH` 和 `Qt5_DIR` 二选一即可,推荐使用 `CMAKE_PREFIX_PATH`。 + +一般不需要把 `C:\Qt\5.15.2\msvc2019_64\bin` 加入 `Path`。项目构建后会通过 `windeployqt` 复制运行所需的 Qt DLL。 + +## 2. OpenSSL 配置 + +OpenSSL 默认放在: + +```text +thirdparty/OpenSSL-Win64 +``` + +推荐目录结构: + +```text +thirdparty/ + OpenSSL-Win64/ + include/ + openssl/ + lib/ + VC/ + x64/ + MD/ + MDd/ +``` + +复制命令示例: + +```powershell +cd C:\Users\admin\Desktop\update-client +mkdir thirdparty +Copy-Item "C:\Program Files\OpenSSL-Win64" ".\thirdparty\OpenSSL-Win64" -Recurse +``` + +如果 OpenSSL 不放在 `thirdparty/`,可以在配置 CMake 时手动指定: + +```powershell +cmake -S . -B out\build\x64-Debug ` + -G "Visual Studio 17 2022" ` + -A x64 ` + -DSIMCAE_OPENSSL_ROOT="C:\Program Files\OpenSSL-Win64" +``` + +## 3. 重新配置和编译 + +如果之前配置过 CMake,建议先删除旧缓存: + +```powershell +cd C:\Users\admin\Desktop\update-client +Remove-Item out\build -Recurse -Force +``` + +重新配置: + +```powershell +cmake -S . -B out\build\x64-Debug ` + -G "Visual Studio 17 2022" ` + -A x64 +``` + +编译: + +```powershell +cmake --build out\build\x64-Debug --config Debug +``` + +## 4. 提交注意事项 + +不要提交下面这些内容: + +```text +thirdparty/ +.vs/ +out/ +build/ +dist/ +App/ +*.exe +*.dll +*.lib +*.pdb +``` + +这些都属于本机依赖、构建产物或打包产物,不应该进 Git。 diff --git a/Launcher/CMakeLists.txt b/Launcher/CMakeLists.txt index 2063e02..b9b31c8 100644 --- a/Launcher/CMakeLists.txt +++ b/Launcher/CMakeLists.txt @@ -3,20 +3,19 @@ project(LauncherDemo LANGUAGES C CXX) # C++标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Qt 自动处理ui/moc/rcc -set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTORCC ON) -# 依赖Qt模块:网络、基础核心、窗口 -set(CMAKE_PREFIX_PATH "C:\\Qt\\5.15.2\\msvc2019_64" ${CMAKE_PREFIX_PATH}) -find_package(Qt5 REQUIRED COMPONENTS Core Network Gui Widgets) -# 所有源码文件 -set(SRC_LIST - main.cpp - UpdateLogic.h - UpdateLogic.cpp -) +# Qt 自动处理ui/moc/rcc +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +# 所有源码文件 +set(SRC_LIST + main.cpp + UpdateLogic.h + UpdateLogic.cpp + ${CMAKE_SOURCE_DIR}/i18n/update-client.qrc +) # 生成可执行程序 add_executable(Launcher ${SRC_LIST}) if(WIN32) diff --git a/Launcher/UpdateLogic.cpp b/Launcher/UpdateLogic.cpp index 20b68f2..b3ae6ae 100644 --- a/Launcher/UpdateLogic.cpp +++ b/Launcher/UpdateLogic.cpp @@ -1,10 +1,12 @@ #include "UpdateLogic.h" #include "ConfigHelper.h" #include -#include -#include -#include "PolicyHelper.h" -#include "LocalStateHelper.h" +#include +#include +#include +#include +#include "PolicyHelper.h" +#include "LocalStateHelper.h" UpdateLogic::UpdateLogic(QObject* parent) : QObject(parent) @@ -81,8 +83,8 @@ void UpdateLogic::checkUpdate() }); } -void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId) -{ +void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId) +{ QString url = m_serverAddr + "/api/v1/update/download-url"; QJsonObject body; body["app_id"] = appId; @@ -97,10 +99,69 @@ void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, c qDebug() << "\n========== File Download Url =========="; qDebug() << resp["files"].toArray(); }); -} - -void UpdateLogic::reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success) -{ +} + +bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, const QString& version, + int versionId, const QString& cacheDir) +{ + m_error.clear(); + if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) { + m_error = "manifest identity is incomplete"; + return false; + } + + QJsonObject response; + int statusCode = 0; + QJsonObject body; + body["app_id"] = appId; + body["channel"] = channel; + body["version"] = version; + body["version_id"] = versionId; + m_http.postRequest(m_serverAddr + "/api/v1/update/manifest", body, + [&](int code, const QJsonObject& resp) { + statusCode = code; + response = resp; + }); + + if (statusCode != 200) { + m_error = QString("manifest request failed (HTTP %1)").arg(statusCode); + return false; + } + + const QJsonObject manifest = response.value("manifest").toObject(); + const QString manifestText = response.value("manifest_text").toString(); + if (manifest.isEmpty() || manifestText.isEmpty()) { + m_error = "manifest response is incomplete"; + return false; + } + if (manifest.value("app_id").toString() != appId + || manifest.value("channel").toString() != channel + || manifest.value("version").toString() != version) { + m_error = "manifest identity does not match current version"; + return false; + } + + QDir dir(cacheDir); + if (!dir.exists() && !dir.mkpath(".")) { + m_error = "cannot create manifest cache directory"; + return false; + } + + QJsonObject wrapper; + wrapper["manifest"] = manifest; + wrapper["manifest_text"] = manifestText; + QSaveFile file(dir.filePath("manifest_" + version + ".json")); + const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented); + if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { + m_error = "cannot save signed manifest cache"; + return false; + } + qDebug() << "Current version manifest cached to" << file.fileName(); + return true; +} + +void UpdateLogic::reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success) +{ QString url = m_serverAddr + "/api/v1/update/report"; QJsonObject body; body["app_id"] = m_appId; @@ -114,4 +175,4 @@ void UpdateLogic::reportUpdateResult(const QString& deviceId, const QString& fro qDebug() << "\n========== Update Report Result =========="; qDebug() << resp; }); -} \ No newline at end of file +} diff --git a/Launcher/UpdateLogic.h b/Launcher/UpdateLogic.h index 3f2ea84..2b35e4f 100644 --- a/Launcher/UpdateLogic.h +++ b/Launcher/UpdateLogic.h @@ -10,15 +10,18 @@ class UpdateLogic : public QObject public: explicit UpdateLogic(QObject* parent = nullptr); - void checkUpdate(); - void getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId); - void reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success); - - bool getNeedUpdate() const { return m_needUpdate; } - QString getLatestVersion() const { return m_latestVer; } - QJsonObject getCheckResult() const { return m_checkResp; } - bool isNetworkOk() const { return m_networkOk; } - int lastStatusCode() const { return m_lastStatusCode; } + void checkUpdate(); + void getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId); + void reportUpdateResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success); + bool cacheManifest(const QString& appId, const QString& channel, const QString& version, + int versionId, const QString& cacheDir); + + bool getNeedUpdate() const { return m_needUpdate; } + QString getLatestVersion() const { return m_latestVer; } + QJsonObject getCheckResult() const { return m_checkResp; } + bool isNetworkOk() const { return m_networkOk; } + int lastStatusCode() const { return m_lastStatusCode; } + QString errorString() const { return m_error; } QString getAppId() const { return m_appId; } QString getChannel() const { return m_channel; } @@ -32,7 +35,8 @@ private: bool m_needUpdate = false; bool m_networkOk = false; - int m_lastStatusCode = 0; - QString m_latestVer; - QJsonObject m_checkResp; -}; \ No newline at end of file + int m_lastStatusCode = 0; + QString m_latestVer; + QJsonObject m_checkResp; + QString m_error; +}; diff --git a/Launcher/main.cpp b/Launcher/main.cpp index 29bd3b7..9b2a99e 100644 --- a/Launcher/main.cpp +++ b/Launcher/main.cpp @@ -1,13 +1,13 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "UpdateLogic.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "UpdateLogic.h" #include "../Common/ConfigHelper.h" #include "../Common/PolicyHelper.h" #include "../Common/LocalStateHelper.h" @@ -17,15 +17,20 @@ #include #include -int main(int argc, char* argv[]) -{ - SetConsoleOutputCP(65001); - QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); - QApplication app(argc, argv); - QApplication::setApplicationName("Marsco Launcher"); - - QProgressDialog progress("正在检查软件更新...", QString(), 0, 0); - progress.setWindowTitle("Marsco 软件启动器"); +int main(int argc, char* argv[]) +{ + QApplication app(argc, argv); + QApplication::setApplicationName("Marsco Launcher"); + QTranslator translator; + if (translator.load(":/i18n/update-client_zh_CN.qm")) + app.installTranslator(&translator); + + const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested(); + if (elevatedWriteExitCode >= 0) + return elevatedWriteExitCode; + + QProgressDialog progress(QCoreApplication::translate("Launcher", "Checking for software updates..."), QString(), 0, 0); + progress.setWindowTitle(QCoreApplication::translate("Launcher", "Marsco Launcher")); progress.setCancelButton(nullptr); progress.setWindowModality(Qt::ApplicationModal); progress.setMinimumDuration(0); @@ -37,50 +42,57 @@ int main(int argc, char* argv[]) ConfigHelper& config = ConfigHelper::instance(); const auto isLicenseError = [](const QString& errorText) { const QString text = errorText.toLower(); - return text.contains("license") - || errorText.contains("授权") - || errorText.contains("过期") - || errorText.contains("设备数量已达到上限") - || errorText.contains("已绑定其他授权"); - }; - const auto clearDeviceCredential = [&]() { - QFile::remove(QDir(appDir).filePath("config/client_identity.dat")); - config.setValue("Update", "device_id", QString()); - }; + return text.contains("license") + || text.contains("authorization") + || text.contains("expired") + || text.contains("device limit") + || text.contains("bound to another license"); + }; + const auto clearDeviceCredential = [&]() { + ConfigHelper::removeFileWithElevationIfNeeded(QDir(appDir).filePath("config/client_identity.dat")); + config.setValue("Update", "device_id", QString()); + }; QString licenseKey = config.getValue("License", "license_key").trimmed(); auto promptAndSaveLicense = [&](const QString& reason) -> QString { QString promptReason = reason; while (true) { progress.close(); - bool accepted = false; - const QString appName = config.getValue("App", "app_name").trimmed(); - const QString prompt = promptReason.trimmed().isEmpty() - ? QString("请输入%1授权 License:").arg(appName.isEmpty() ? "软件" : appName) - : QString("%1\n\n请重新输入%2授权 License:").arg(promptReason, appName.isEmpty() ? "软件" : appName); - licenseKey = QInputDialog::getText( - nullptr, - "输入 License", - prompt, + bool accepted = false; + const QString appName = config.getValue("App", "app_name").trimmed(); + const QString prompt = promptReason.trimmed().isEmpty() + ? QCoreApplication::translate("Launcher", "Please enter the License for %1:") + .arg(appName.isEmpty() ? QCoreApplication::translate("Launcher", "the application") : appName) + : QCoreApplication::translate("Launcher", "%1\n\nPlease enter a new License for %2:") + .arg(promptReason, appName.isEmpty() ? QCoreApplication::translate("Launcher", "the application") : appName); + licenseKey = QInputDialog::getText( + nullptr, + QCoreApplication::translate("Launcher", "Enter License"), + prompt, QLineEdit::Normal, QString(), &accepted - ).trimmed(); - if (!accepted) { - QMessageBox::information(nullptr, "需要 License", "首次启动需要输入管理员提供的 License。"); - return QString(); - } - if (licenseKey.isEmpty()) { - QMessageBox::warning(nullptr, "License 不能为空", "请粘贴管理员在后台创建的 License。"); - promptReason.clear(); - continue; - } - if (!config.setValue("License", "license_key", licenseKey)) { - QMessageBox::critical(nullptr, "保存 License 失败", - QString("无法写入配置文件:%1\n%2").arg(config.configPath(), config.lastError())); - return QString(); - } - progress.show(); - progress.setLabelText("正在验证 License..."); + ).trimmed(); + if (!accepted) { + QMessageBox::information(nullptr, + QCoreApplication::translate("Launcher", "License Required"), + QCoreApplication::translate("Launcher", "A License provided by the administrator is required for the first launch.")); + return QString(); + } + if (licenseKey.isEmpty()) { + QMessageBox::warning(nullptr, + QCoreApplication::translate("Launcher", "License Cannot Be Empty"), + QCoreApplication::translate("Launcher", "Please paste the License created in the admin page.")); + promptReason.clear(); + continue; + } + if (!config.setValue("License", "license_key", licenseKey)) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Failed to Save License"), + QCoreApplication::translate("Launcher", "Cannot write the configuration file:\n%1\n%2").arg(config.configPath(), config.lastError())); + return QString(); + } + progress.show(); + progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying License...")); QApplication::processEvents(); return licenseKey; } @@ -101,12 +113,14 @@ int main(int argc, char* argv[]) } const QString error = identity.errorString(); if (!isLicenseError(error)) { - progress.close(); - QMessageBox::critical(nullptr, "设备身份验证失败", error); - return -1; - } - clearDeviceCredential(); - licenseKey = promptAndSaveLicense(QString("当前 License 无法使用:%1").arg(error)); + progress.close(); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Device Authentication Failed"), + error); + return -1; + } + clearDeviceCredential(); + licenseKey = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current License cannot be used: %1").arg(error)); if (licenseKey.isEmpty()) return 0; } @@ -116,49 +130,61 @@ int main(int argc, char* argv[]) const bool needUpdate = logic.getNeedUpdate(); const bool networkOk = logic.isNetworkOk(); const QString latestVer = logic.getLatestVersion(); - const QJsonObject response = logic.getCheckResult(); - const int targetVersionId = response.value("version_id").toInt(); - const QString appId = logic.getAppId(); - const QString channel = logic.getChannel(); + const QJsonObject response = logic.getCheckResult(); + const int targetVersionId = response.value("version_id").toInt(); + const QString appId = logic.getAppId(); + const QString channel = logic.getChannel(); + const QString launchToken = config.getValue("App", "launch_token"); + const QString currentVersion = config.getValue("App", "current_version"); if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403) { progress.close(); const QJsonValue detail = response.value("detail"); const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString(); - const QString displayMessage = message.isEmpty() ? "设备或 License 授权无效。" : message; - if (isLicenseError(displayMessage)) { - clearDeviceCredential(); - const QString newLicense = promptAndSaveLicense(QString("当前授权被服务端拒绝:%1").arg(displayMessage)); - if (!newLicense.isEmpty()) { - progress.close(); - QMessageBox::information(nullptr, "License 已保存", "请重新启动 Launcher 完成设备授权和更新检查。"); - } - return 0; - } - QMessageBox::critical(nullptr, "授权被拒绝", displayMessage); + const QString displayMessage = message.isEmpty() + ? QCoreApplication::translate("Launcher", "The device or License authorization is invalid.") + : message; + if (isLicenseError(displayMessage)) { + clearDeviceCredential(); + const QString newLicense = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current authorization was rejected by the server: %1").arg(displayMessage)); + if (!newLicense.isEmpty()) { + progress.close(); + QMessageBox::information(nullptr, + QCoreApplication::translate("Launcher", "License Saved"), + QCoreApplication::translate("Launcher", "Please restart Launcher to complete device authorization and update checking.")); + } + return 0; + } + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Authorization Rejected"), + displayMessage); return -1; } - const QString launchToken = config.getValue("App", "launch_token"); - const QString currentVersion = config.getValue("App", "current_version"); - const auto configuredName = [&](const QString& key, const QString& fallback) { - const QString value = config.getValue("Runtime", key).trimmed(); - return value.isEmpty() ? fallback : value; + const auto configuredName = [&](const QString& key, const QString& fallback) { + const QString value = config.getValue("Runtime", key).trimmed(); + return value.isEmpty() ? fallback : value; }; const QString mainExecutable = configuredName("main_executable", "MainApp.exe"); const QString updaterExecutable = configuredName("updater_executable", "Updater.exe"); - const QString mainAppPath = QDir(appDir).filePath(mainExecutable); - const QString updaterPath = QDir(appDir).filePath(updaterExecutable); - const auto importOfflinePackage = [&]() { - const QString package = QFileDialog::getOpenFileName(nullptr, "选择离线更新包", QString(), "Marsco 离线更新包 (*.upd)"); - return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)}); - }; + const QString mainAppPath = QDir(appDir).filePath(mainExecutable); + const QString updaterPath = QDir(appDir).filePath(updaterExecutable); + const auto importOfflinePackage = [&]() { + const QString package = QFileDialog::getOpenFileName(nullptr, + QCoreApplication::translate("Launcher", "Select Offline Update Package"), + QString(), + QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)")); + return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)}); + }; const QString deviceId = config.getValue("Update", "device_id"); - if (QCoreApplication::arguments().contains("--import-offline")) { - progress.close(); - if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包。"); - return 0; - } + if (QCoreApplication::arguments().contains("--import-offline")) { + progress.close(); + if (!importOfflinePackage()) + QMessageBox::information(nullptr, + QCoreApplication::translate("Launcher", "Offline Update"), + QCoreApplication::translate("Launcher", "No offline update package was selected.")); + return 0; + } const auto startMainApp = [&]() { QString ticketPath; @@ -174,28 +200,33 @@ int main(int argc, char* argv[]) return started; }; - progress.setLabelText("正在验证本地运行策略..."); + progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying local runtime policy...")); QApplication::processEvents(); PolicyHelper policy(appDir); if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid()) { progress.close(); - QMessageBox::critical(nullptr, "无法启动", QString("本地版本策略无效:%1").arg(policy.errorString())); - return -1; + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Cannot Start"), + QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString())); + return -1; } if (policy.isExpired()) { progress.close(); - QMessageBox::critical(nullptr, "无法启动", "本地版本策略已过期,请连接网络或联系管理员。"); - return -1; + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Cannot Start"), + QCoreApplication::translate("Launcher", "The local version policy has expired. Please connect to the network or contact the administrator.")); + return -1; } if ((!policy.allowRun() || !policy.isVersionAllowed(currentVersion)) && !(networkOk && needUpdate)) { progress.close(); - QMessageBox::critical(nullptr, "当前版本不可运行", - policy.message().isEmpty() ? QString("当前版本 %1 已被管理员停用。").arg(currentVersion) - : policy.message()); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Current Version Cannot Run"), + policy.message().isEmpty() ? QCoreApplication::translate("Launcher", "The current version %1 has been disabled by the administrator.").arg(currentVersion) + : policy.message()); return -1; } @@ -203,32 +234,39 @@ int main(int argc, char* argv[]) if (!state.loadState()) { progress.close(); - QMessageBox::critical(nullptr, "无法启动", QString("无法读取本地状态:%1").arg(state.errorString())); - return -1; + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Cannot Start"), + QCoreApplication::translate("Launcher", "Cannot read the local state: %1").arg(state.errorString())); + return -1; } if (state.isPolicySeqRolledBack(policy.policySeq())) { progress.close(); - QMessageBox::critical(nullptr, "安全检查失败", "检测到版本策略序列回退,已阻止启动。"); - return -1; + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Security Check Failed"), + QCoreApplication::translate("Launcher", "A version policy sequence rollback was detected. Startup has been blocked.")); + return -1; } if (state.isSystemTimeRewound()) { progress.close(); - QMessageBox::critical(nullptr, "安全检查失败", "检测到系统时间可能被回拨,已阻止启动。"); - return -1; + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Security Check Failed"), + QCoreApplication::translate("Launcher", "A possible system time rollback was detected. Startup has been blocked.")); + return -1; } - if (networkOk && needUpdate) - { - progress.close(); - const bool rollbackOperation = response.value("action").toString() == "rollback_allowed"; - const QString dialogTitle = rollbackOperation ? "版本回退" - : (policy.forceUpdate() ? "必须更新" : "发现新版本"); - const QString prompt = policy.message().isEmpty() - ? QString(rollbackOperation ? "管理员提供了版本 %1 作为回退目标,是否现在降级?" - : "发现新版本 %1,是否现在更新?").arg(latestVer) - : policy.message() + QString("\n目标版本:%1").arg(latestVer); + if (networkOk && needUpdate) + { + progress.close(); + const bool rollbackOperation = response.value("action").toString() == "rollback_allowed"; + const QString dialogTitle = rollbackOperation ? QCoreApplication::translate("Launcher", "Version Rollback") + : (policy.forceUpdate() ? QCoreApplication::translate("Launcher", "Update Required") : QCoreApplication::translate("Launcher", "New Version Available")); + const QString prompt = policy.message().isEmpty() + ? (rollbackOperation + ? QCoreApplication::translate("Launcher", "The administrator provided version %1 as the rollback target. Downgrade now?").arg(latestVer) + : QCoreApplication::translate("Launcher", "Version %1 is available. Update now?").arg(latestVer)) + : policy.message() + QCoreApplication::translate("Launcher", "\nTarget version: %1").arg(latestVer); bool accepted = true; if (policy.forceUpdate()) { QMessageBox::information(nullptr, dialogTitle, prompt); @@ -237,44 +275,74 @@ int main(int argc, char* argv[]) QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; } if (!accepted) { - if (!startMainApp()) { - QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath)); - return -1; - } + if (!startMainApp()) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Startup Failed"), + QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); + return -1; + } return 0; } const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)}; - if (!QProcess::startDetached(updaterPath, updaterArgs)) - { - QMessageBox::critical(nullptr, "更新器启动失败", QString("无法启动更新器:%1").arg(updaterPath)); - return -1; + if (!QProcess::startDetached(updaterPath, updaterArgs)) + { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Updater Startup Failed"), + QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath)); + return -1; } - return 0; - } - - if (!networkOk) { - progress.close(); - if (QMessageBox::question(nullptr, "服务器不可用", "当前无法连接更新服务器。是否导入离线更新包?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { - if (!importOfflinePackage()) QMessageBox::information(nullptr, "离线更新", "未选择离线更新包或无法启动更新器。"); - return 0; + return 0; + } + + if (networkOk && !needUpdate && targetVersionId > 0 && latestVer == currentVersion) + { + progress.setLabelText(QCoreApplication::translate("Launcher", "Caching signed version manifest...")); + QApplication::processEvents(); + const QString manifestCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("manifest_cache"); + if (!logic.cacheManifest(appId, channel, currentVersion, targetVersionId, manifestCacheDir)) + { + progress.close(); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Manifest Cache Failed"), + QCoreApplication::translate("Launcher", "Cannot cache the signed manifest for the current version: %1").arg(logic.errorString())); + return -1; + } + } + + if (!networkOk) { + progress.close(); + if (QMessageBox::question(nullptr, + QCoreApplication::translate("Launcher", "Server Unavailable"), + QCoreApplication::translate("Launcher", "Cannot connect to the update server. Import an offline update package?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { + if (!importOfflinePackage()) + QMessageBox::information(nullptr, + QCoreApplication::translate("Launcher", "Offline Update"), + QCoreApplication::translate("Launcher", "No offline update package was selected, or the updater could not be started.")); + return 0; } progress.show(); } - if (!networkOk && !policy.isOfflineAllowed()) - { - progress.close(); - QMessageBox::critical(nullptr, "网络不可用", "无法连接更新服务器,且当前策略不允许离线启动。"); - return -1; - } - - progress.setLabelText(networkOk ? "当前已是最新版本,正在启动..." : "当前处于离线模式,正在启动..."); + if (!networkOk && !policy.isOfflineAllowed()) + { + progress.close(); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Network Unavailable"), + QCoreApplication::translate("Launcher", "Cannot connect to the update server, and the current policy does not allow offline startup.")); + return -1; + } + + progress.setLabelText(networkOk + ? QCoreApplication::translate("Launcher", "The application is up to date. Starting...") + : QCoreApplication::translate("Launcher", "Offline mode is active. Starting...")); QApplication::processEvents(); if (!startMainApp()) { progress.close(); - QMessageBox::critical(nullptr, "启动失败", QString("无法启动主程序:%1").arg(mainAppPath)); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Launcher", "Startup Failed"), + QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); return -1; } progress.close(); diff --git a/MainApp/CMakeLists.txt b/MainApp/CMakeLists.txt index 7182c39..849de21 100644 --- a/MainApp/CMakeLists.txt +++ b/MainApp/CMakeLists.txt @@ -1,21 +1,22 @@ -project(MainApp) -add_executable(MainApp - main.cpp - MainWindow.h - MainWindow.cpp - ../Common/ConfigHelper.h -) -target_include_directories(MainApp - PUBLIC ${CMAKE_SOURCE_DIR}/Common - PRIVATE ${OPENSSL_INC} -) -target_link_libraries(MainApp PRIVATE Common Qt5::Core Qt5::Gui Qt5::Widgets) - -if(WIN32) - get_target_property(QT_WINDEPLOYQT_EXE Qt5::qmake IMPORTED_LOCATION) - get_filename_component(QT_BIN_PATH "${QT_WINDEPLOYQT_EXE}" DIRECTORY) - set(WINDEPLOYQT "${QT_BIN_PATH}/windeployqt.exe") - add_custom_command(TARGET MainApp POST_BUILD +project(MainApp) +add_executable(MainApp + main.cpp + MainWindow.h + MainWindow.cpp + ../Common/ConfigHelper.h + ${CMAKE_SOURCE_DIR}/i18n/update-client.qrc +) +target_include_directories(MainApp + PUBLIC ${CMAKE_SOURCE_DIR}/Common + PRIVATE ${OPENSSL_INC} +) +target_link_libraries(MainApp PRIVATE Common Qt5::Core Qt5::Gui Qt5::Widgets) + +if(WIN32) + get_target_property(QT_WINDEPLOYQT_EXE Qt5::qmake IMPORTED_LOCATION) + get_filename_component(QT_BIN_PATH "${QT_WINDEPLOYQT_EXE}" DIRECTORY) + set(WINDEPLOYQT "${QT_BIN_PATH}/windeployqt.exe") + add_custom_command(TARGET MainApp POST_BUILD COMMAND ${WINDEPLOYQT} $,--debug,--release> $ - ) + ) endif() diff --git a/MainApp/main.cpp b/MainApp/main.cpp index 6077195..e2a511a 100644 --- a/MainApp/main.cpp +++ b/MainApp/main.cpp @@ -1,11 +1,10 @@ -#include -#include +#include #include -#include #include #include #include #include +#include #include "MainWindow.h" #include "../Common/ConfigHelper.h" #include "../Common/PolicyHelper.h" @@ -16,10 +15,14 @@ int main(int argc, char* argv[]) { - SetConsoleOutputCP(936); - QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK")); - QApplication a(argc, argv); + QTranslator translator; + if (translator.load(":/i18n/update-client_zh_CN.qm")) + a.installTranslator(&translator); + + const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested(); + if (elevatedWriteExitCode >= 0) + return elevatedWriteExitCode; qDebug() << Qt::endl << "entered main app" << Qt::endl; diff --git a/READMe.txt b/READMe.txt deleted file mode 100644 index 39fd4fd..0000000 --- a/READMe.txt +++ /dev/null @@ -1,27 +0,0 @@ -客户端配置说明 -============== - -统一从程序目录下的 config/app_config.json 读取配置。 -首次运行时如果该文件不存在且发现旧 client.ini,会自动迁移。 - -接入新软件时通常需要修改: - -1. app_id、app_name、channel、current_version。 -2. api_base_url、client_token、license_key、launch_token。 -3. main_executable:团队业务主程序文件名。 -4. launcher_executable、updater_executable、bootstrap_executable。 -5. health_check_timeout_ms:升级后等待业务程序健康确认的毫秒数,最小 1000。 - -完整格式参考 config/app_config.example.json。 -运行时生成的 app_config.json、client_identity.dat、local_state.json 等文件不得打入通用 SDK 模板。 - -Windows 发布打包: - -1. 使用 Release 配置编译全部客户端程序。 -2. 先完成当前版本在线校验,确认 out/bin/update/manifest_cache 中存在对应的签名 Manifest。 -3. 准备一份实际 app_config.json,确认其中包含正确的 License Key、当前版本和业务程序名。 -4. 在 PowerShell 执行: - powershell -ExecutionPolicy Bypass -File .\package-client.ps1 -ConfigFile .\config\app_config.json -5. 输出位于 dist/UpdateClient 和 dist/UpdateClient.zip。 - -脚本会拒绝 Debug DLL、PDB、嵌套重复主程序和缺少签名 Manifest 的发布源目录。 diff --git a/SDK_README.md b/SDK_README.md deleted file mode 100644 index cd6ba3a..0000000 --- a/SDK_README.md +++ /dev/null @@ -1,188 +0,0 @@ -# UpdateClientSDK 接入说明 - -这个 SDK 是“独立更新器 SDK / 升级运行时 SDK”。它不是传统的 `include + lib` 形态,而是把自动升级能力作为一组独立程序交给业务软件使用。 - -SDK 核心程序: - -- `Launcher.exe`:用户入口。检查版本、验证策略,决定启动业务主程序或启动 Updater。 -- `Updater.exe`:下载、校验、备份、安装、健康确认、提交或回滚。 -- `Bootstrap.exe`:替换运行中可能被占用的 EXE/DLL。 -- `config/app_config.json`:接入方配置。 -- `config/manifest_public_key.pem`:验证服务端签名用的公钥。 - -## 接入方需要做什么 - -假设接入的软件叫 `YourApp.exe`。 - -1. 在服务端管理后台创建应用,例如 `app_id=your_app_id`。 -2. 创建或确认渠道,例如 `stable`。 -3. 创建 License,把生成的 `license_key` 填到客户端配置。 -4. 把业务软件完整安装目录作为发布根目录,例如 `SimCAE\`,其中主程序位于 `bin\SimCAE.exe`。 -5. 把 SDK 的 `Launcher.exe`、`Updater.exe`、`Bootstrap.exe` 放到 `SimCAE\bin\` 目录,和 `SimCAE.exe` 同级。不要覆盖 SimCAE 自带的 `Qt5*.dll` 和 Qt 插件目录。 -6. 把 `config/app_config.example.json` 复制成 `SimCAE\bin\config\app_config.json` 并修改字段。 -7. 用户入口改成 `Launcher.exe`,不要直接双击业务主程序。 -8. 在管理后台发布新版本时,选择包含业务主程序和 SDK 运行时的干净 Release 根目录。 - -## 安装目录写权限要求 - -当前 SDK 会在 `Launcher.exe` 所在目录下写入运行时状态文件。SDK 放在 `SimCAE\bin` 时,这些文件实际位于 `SimCAE\bin` 下,例如: - -```text -config/app_config.json -config/client_identity.dat -config/local_state.json -config/version_policy.dat -update/ -update_temp/ -``` - -所以联调和普通运行时,`Launcher.exe` 所在目录必须允许当前 Windows 用户写入。不要直接把联调目录放在 `C:\Program Files\...` 后用普通用户启动;该目录默认禁止普通程序写文件,会导致首次启动报错,例如 `cannot save installation id`。 - -推荐联调目录: - -```text -D:\SimCAE_Release\ -C:\Users\<你的用户名>\Desktop\SimCAE_Release\ -``` - -如果最终产品必须安装到 `C:\Program Files\SimCAE\bin`,需要额外设计管理员提权、Windows 服务,或把运行时状态迁移到 `ProgramData` / `AppData`。当前交付版本默认按“安装目录可写”的模式工作。 - -## SimCAE 目录结构建议 - -SimCAE 当前安装目录是根目录下有 `bin/`、`Licenses/`、`installerResources/` 等子目录。SDK 推荐放在 `bin/` 目录,和 `SimCAE.exe` 同级;后台发布时仍选择整个安装根目录: - -```text -SimCAE/ - bin/ - Launcher.exe - Updater.exe - Bootstrap.exe - SimCAE.exe - config/ - app_config.json - manifest_public_key.pem - update/ - manifest_cache/ - Qt5Core.dll - ... - Licenses/ - installerResources/ - maintenancetool.exe -``` - -这种模式下,后台“发布新版本”时选择整个 `SimCAE/` 目录,服务端会检查 `bin/SimCAE.exe` 是否存在,并把整个安装结构写入 Manifest。客户端配置里 `install_root` 填 `..`,表示被更新的安装根目录是 `bin` 的上一级;升级事务、下载缓存和 Manifest 缓存仍放在 `SimCAE\bin\update`。 - -注意:SimCAE 自己已经带有 Qt 运行库。SDK 的 Launcher/Updater 应使用和 SimCAE 兼容的 Qt 编译,并复用 SimCAE 的 `Qt5*.dll`、`platforms/`、`imageformats/` 等目录。不要把另一套 Qt DLL 覆盖到 `SimCAE\bin`,否则会出现“无法定位程序输入点”一类错误。 - -## app_config.json 关键字段 - -```json -{ - "app_id": "simcae", - "app_name": "SimCAE", - "channel": "stable", - "current_version": "1.0.0", - "client_protocol": "3", - "launch_token": "SimCAE_Launch_Token_2026_ChangeMe_32Bytes", - "license_key": "", - "api_base_url": "http://YOUR_SERVER_IP:8000", - "client_token": "SimCAEClientToken2026", - "request_timeout_ms": "5000", - "temp_folder": "update_temp", - "device_id": "", - "install_root": "..", - "main_executable": "SimCAE.exe", - "launcher_executable": "Launcher.exe", - "updater_executable": "Updater.exe", - "bootstrap_executable": "Bootstrap.exe", - "health_check_timeout_ms": "15000", - "platform": "windows", - "arch": "x64" -} -``` - -字段说明: - -- `app_id`:服务端应用 ID,默认填 `simcae`;如果后台创建了别的 App ID,这里同步修改。 -- `channel`:发布渠道,例如 `stable`、`beta`、`dev`。 -- `current_version`:当前客户端初始版本。 -- `client_protocol`:客户端协议号,当前建议为 `3`。 -- `api_base_url`:服务端 API 地址,例如 `http://192.168.229.128:8000`;服务器 IP 无法提前知道,所以这里需要按现场地址修改。 -- `client_token`:服务端 `.env` 中的 `CLIENT_API_TOKEN`,默认交付包已填 `SimCAEClientToken2026`。 -- `license_key`:管理后台创建 License 后返回的密钥;模板里先留空,创建授权后再填。 -- `launch_token`:本机启动票据 HMAC 密钥,模板已给默认值,可试跑;正式交付建议改成你自己的 32 字符以上随机字符串。 -- `install_root`:安装根目录相对 `Launcher.exe` 所在目录的位置。SDK 放在 `bin` 时填 `..`。 -- `main_executable`:业务主程序相对 `Launcher.exe` 所在目录的路径,SimCAE 默认填 `SimCAE.exe`。 -- `health_check_timeout_ms`:升级后等待业务程序写健康标记的时间。 - -## 业务主程序需要配合什么 - -当前安全模式下,业务主程序需要配合两件事: - -1. 接收 `--ticket-file=` 参数,验证并消费一次性启动票据。 -2. 如果收到 `--health-file=` 参数,启动成功后向该路径写入 `ok\n`,让 Updater 确认新版本可用。 - -当前仓库里的 `client/MainApp/main.cpp` 是接入示例,已经实现了: - -- 启动票据校验。 -- 本地 License/设备身份校验。 -- 本地策略校验。 -- Manifest 完整性校验。 -- 健康标记写入。 - -如果第三方业务程序暂时不想改代码,可以先使用当前 `MainApp.exe` 作为 Demo 验证 SDK 包;真正接入时建议把这些启动检查逻辑移植到业务主程序。 - -## 如何生成 SDK 包 - -在 Windows PowerShell 中执行: - -```powershell -cd client -.\package-sdk.ps1 ` - -SourceDir .\out\bin ` - -OutputDir .\dist\UpdateClientSDK ` - -ZipFile .\dist\UpdateClientSDK.zip ` - -SdkVersion 0.1.0 -``` - -默认生成的 SDK 不包含 Qt 运行库,避免覆盖业务软件自带的 Qt。只有在接入的软件本身不带 Qt,且你确认要让 SDK 自带一套 Qt 运行库时,才额外添加 `-IncludeQtRuntime`。 - -生成结果: - -```text -dist/UpdateClientSDK/ - SimCAE自动升级SDK接入说明_v0.1.docx - sdk_manifest.json - bin/ - config/ - app_config.json - manifest_public_key.pem - scripts/ -``` - -把 `UpdateClientSDK.zip` 发给接入方即可。 - -## 如何生成某个产品的最终客户端包 - -SDK 是给开发者接入用的,最终给用户安装/分发时,可以使用: - -```powershell -cd client -.\package-client.ps1 ` - -SourceDir .\out\bin ` - -ConfigFile .\config\app_config.json ` - -OutputDir .\dist\UpdateClient ` - -ZipFile .\dist\UpdateClient.zip -``` - -`package-client.ps1` 会检查配置和必需文件,并生成具体产品的客户端包。 - -## 常见错误 - -1. 直接启动业务主程序提示 ticket 错误:应从 `Launcher.exe` 启动。 -2. 首次启动提示 `cannot save installation id`:当前目录不可写,常见于 `C:\Program Files\...`;请换到可写目录联调,或用管理员权限/提权方案。 -3. 首次启动设备登记失败:检查 `api_base_url`、`client_token`、`license_key`、服务端 License 状态。 -4. 策略或 Manifest 验签失败:检查 `config/manifest_public_key.pem` 是否和服务端私钥匹配。 -5. 升级后回滚:检查业务程序是否在 `health_check_timeout_ms` 内写入健康标记。 -6. 发布失败提示主程序不在根目录:选择发布目录时要选择业务主程序所在目录,而不是上层或下层目录。 -7. 启动时提示 `无法定位程序输入点 ... Qt5*.dll`:通常是 Qt DLL 被不同版本覆盖或混用。恢复业务软件原始 Qt DLL,并重新打包 SDK;SimCAE 场景下不要使用 `-IncludeQtRuntime`。 diff --git a/Updater/CMakeLists.txt b/Updater/CMakeLists.txt index f95d815..0e77f5a 100644 --- a/Updater/CMakeLists.txt +++ b/Updater/CMakeLists.txt @@ -1,20 +1,18 @@ # 强制所有编译、设计时生成均使用x64,禁止Win32 set(CMAKE_GENERATOR_PLATFORM x64 CACHE STRING "强制x64平台,禁用Win32") set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCH x64) -# 关闭VS自动Win32设计时预生成 -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF) -# 清除32位Strawberry Perl路径干扰 -list(REMOVE_ITEM CMAKE_INCLUDE_PATH "D:/softwaresInstallDir/strawberry-perl-5.22.1.3-32bit/c/include") -list(REMOVE_ITEM CMAKE_LIBRARY_PATH "D:/softwaresInstallDir/strawberry-perl-5.22.1.3-32bit/c/lib") - -project(Updater) -set(SRC - main.cpp - UpdaterLogic.h - UpdaterLogic.cpp - UpdateTransaction.h - UpdateTransaction.cpp -) +# 关闭VS自动Win32设计时预生成 +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF) + +project(Updater) +set(SRC + main.cpp + UpdaterLogic.h + UpdaterLogic.cpp + UpdateTransaction.h + UpdateTransaction.cpp + ${CMAKE_SOURCE_DIR}/i18n/update-client.qrc +) add_executable(Updater ${SRC}) if(WIN32) diff --git a/Updater/UpdaterLogic.cpp b/Updater/UpdaterLogic.cpp index 7390b8d..58091ee 100644 --- a/Updater/UpdaterLogic.cpp +++ b/Updater/UpdaterLogic.cpp @@ -1,8 +1,9 @@ #include "UpdaterLogic.h" #include #include -#include -#include +#include +#include +#include #include #include #include @@ -341,45 +342,45 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& { 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(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; } + if (!package.open(QIODevice::ReadOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot open the offline update package"); return false; } + if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package format identifier is invalid"); return false; } + const QByteArray lengthBytes = package.read(8); + if (lengthBytes.size() != 8) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header is incomplete"); return false; } + quint64 headerSize = 0; + for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast(lengthBytes[i])) << (i * 8); + if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header length is invalid"); return false; } + QJsonParseError error; + const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error); + if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header JSON is invalid"); 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; } + if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package RSA signature is invalid"); return false; } + const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object(); + if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package signature metadata is invalid"); return false; } + if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The manifest digest does not match the package signature"); return false; } + QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object(); + if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest is invalid"); 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; } + if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Package information does not match manifest identity"); return false; } + if (!verifyManifestSignature()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest RSA signature is invalid"); 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(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; } + if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package contains an unsafe path or out-of-range data: %1").arg(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 = QCoreApplication::translate("UpdaterLogic", "Cannot prepare offline file: %1").arg(path); return false; } + QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot create staged file: %1").arg(path); return false; } + QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size; + while (remaining > 0) { const QByteArray block = package.read(qMin(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Failed to read offline file: %1").arg(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 = QCoreApplication::translate("UpdaterLogic", "Offline file hash verification or write failed: %1").arg(path); return false; } + } + if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package file count does not match the manifest"); return false; } return true; } @@ -514,7 +515,7 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat if (existingSize > 0 && httpStatus == 200) { - // 服务端忽略 Range,当前文件是“旧片段 + 完整响应”,必须安全重下。 + // The server ignored Range; the current file is "old fragment + full response" and must be redownloaded safely. qDebug() << "Server ignored Range; restart full download:" << savePath; QFile::remove(partPath); } diff --git a/Updater/main.cpp b/Updater/main.cpp index ce3cbbe..41e1daf 100644 --- a/Updater/main.cpp +++ b/Updater/main.cpp @@ -1,19 +1,18 @@ -#include -#include -#include +#include +#include #include #include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include "UpdaterLogic.h" +#include +#include +#include +#include +#include +#include +#include "UpdaterLogic.h" #include "UpdateTransaction.h" #include "FileHelper.h" #include "ConfigHelper.h" @@ -21,35 +20,44 @@ #include "PolicyHelper.h" #include -int main(int argc, char* argv[]) -{ - SetConsoleOutputCP(65001); - QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); - QApplication app(argc, argv); - QApplication::setApplicationName("Marsco Updater"); - - UpdaterLogic logic; +int main(int argc, char* argv[]) +{ + QApplication app(argc, argv); + QApplication::setApplicationName("Marsco Updater"); + QTranslator translator; + if (translator.load(":/i18n/update-client_zh_CN.qm")) + app.installTranslator(&translator); + + const int elevatedWriteExitCode = ConfigHelper::runElevatedWriteCommandIfRequested(); + if (elevatedWriteExitCode >= 0) + return elevatedWriteExitCode; + + UpdaterLogic logic; QString offlinePackagePath; QString appId; QString channel; QString targetVersion; int targetVersionId = 0; - if (argc >= 2 && QString(argv[1]).startsWith("--offline-package=")) { - offlinePackagePath = QString(argv[1]).mid(QString("--offline-package=").size()); - if (!logic.loadOfflinePackage(offlinePackagePath)) { - QMessageBox::critical(nullptr, "离线包无效", logic.offlineError()); - return -1; - } + if (argc >= 2 && QString(argv[1]).startsWith("--offline-package=")) { + offlinePackagePath = QString(argv[1]).mid(QString("--offline-package=").size()); + if (!logic.loadOfflinePackage(offlinePackagePath)) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Invalid Offline Package"), + logic.offlineError()); + return -1; + } const QJsonObject packageManifest = logic.getManifest(); appId = packageManifest.value("app_id").toString(); channel = packageManifest.value("channel").toString(); targetVersion = packageManifest.value("version").toString(); targetVersionId = packageManifest.value("manifest_seq").toInt(); - } else { - if (argc < 5) { - QMessageBox::critical(nullptr, "更新器参数错误", "更新器缺少在线更新参数或离线更新包。"); - return -1; - } + } else { + if (argc < 5) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Updater Argument Error"), + QCoreApplication::translate("Updater", "The updater is missing online update arguments or an offline update package.")); + return -1; + } appId = argv[1]; channel = argv[2]; targetVersion = argv[3]; targetVersionId = QString(argv[4]).toInt(); } QString bootstrapResult; @@ -64,48 +72,58 @@ int main(int argc, char* argv[]) ConfigHelper& config = ConfigHelper::instance(); const QString targetDir = config.installRoot(); const QString updateDir = config.updateRoot(); - QDir().mkpath(updateDir); - if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) { - QMessageBox::critical(nullptr, "离线包不适用", "更新包的应用或渠道与本机配置不一致。"); - return -1; - } + QDir().mkpath(updateDir); + if (appId != config.getValue("App", "app_id") || channel != config.getValue("App", "channel")) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Offline Package Not Applicable"), + QCoreApplication::translate("Updater", "The update package application or channel does not match the local configuration.")); + return -1; + } QString fromVersion = config.getValue("App", "current_version"); if (!offlinePackagePath.isEmpty()) { PolicyHelper offlinePolicy(runtimeDir); - if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired() - || !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) { - QMessageBox::critical(nullptr, "离线更新被拒绝", "本地签名策略已过期、禁止离线更新或不允许目标版本。"); - return -1; - } + if (!offlinePolicy.loadPolicy() || !offlinePolicy.isValid() || offlinePolicy.isExpired() + || !offlinePolicy.isOfflineAllowed() || !offlinePolicy.isVersionAllowed(targetVersion)) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Offline Update Rejected"), + QCoreApplication::translate("Updater", "The local signed policy has expired, forbids offline updates, or does not allow the target version.")); + return -1; + } if (QVersionNumber::compare(QVersionNumber::fromString(targetVersion), QVersionNumber::fromString(fromVersion)) < 0 - && !offlinePolicy.allowRollback()) { - QMessageBox::critical(nullptr, "禁止降级", "当前签名策略不允许安装较低版本的离线包。"); - return -1; - } + && !offlinePolicy.allowRollback()) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Downgrade Forbidden"), + QCoreApplication::translate("Updater", "The current signed policy does not allow installing an offline package with a lower version.")); + return -1; + } } QString deviceId = config.getValue("Update", "device_id"); if (deviceId.isEmpty()) deviceId = "unknown_device"; - if (!resumingFromBootstrap) { - QString restoredVersion; - QString recoveryError; - if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) { - QMessageBox::critical(nullptr, "更新恢复失败", - QString("检测到上次更新未完成,但无法恢复旧版本:%1\n请不要继续运行软件,并联系管理员。").arg(recoveryError)); - return -1; - } - if (!restoredVersion.isEmpty() && restoredVersion != fromVersion) { - if (!config.setValue("App", "current_version", restoredVersion)) { - QMessageBox::critical(nullptr, "更新恢复失败", "旧文件已经恢复,但无法恢复版本状态,请检查配置目录写入权限。"); - return -1; - } + if (!resumingFromBootstrap) { + QString restoredVersion; + QString recoveryError; + if (!UpdateTransaction::recoverInterrupted(targetDir, updateDir, &restoredVersion, &recoveryError)) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Update Recovery Failed"), + QCoreApplication::translate("Updater", "An unfinished update was detected, but the old version could not be restored: %1\nDo not continue running the software. Please contact the administrator.") + .arg(recoveryError)); + return -1; + } + if (!restoredVersion.isEmpty() && restoredVersion != fromVersion) { + if (!config.setValue("App", "current_version", restoredVersion)) { + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Update Recovery Failed"), + QCoreApplication::translate("Updater", "The old files were restored, but the version state could not be restored. Please check write permissions for the configuration directory.")); + return -1; + } fromVersion = restoredVersion; } } - QProgressDialog progress("正在准备更新...", QString(), 0, 100); - progress.setWindowTitle(QString("正在更新到 %1").arg(targetVersion)); + QProgressDialog progress(QCoreApplication::translate("Updater", "Preparing update..."), QString(), 0, 100); + progress.setWindowTitle(QCoreApplication::translate("Updater", "Updating to %1").arg(targetVersion)); progress.setCancelButton(nullptr); progress.setWindowModality(Qt::ApplicationModal); progress.setMinimumDuration(0); @@ -131,10 +149,11 @@ int main(int argc, char* argv[]) return QString::number(bytes) + " B"; }; QObject::connect(&logic, &UpdaterLogic::downloadProgress, - [&](qint64 received, qint64 total, const QString& path, double bytesPerSecond) { - const int value = total > 0 ? 30 + static_cast(30 * received / total) : 60; - const QString fileText = path.isEmpty() ? QString("正在准备下载...") - : QString("正在下载:%1").arg(path); + [&](qint64 received, qint64 total, const QString& path, double bytesPerSecond) { + const int value = total > 0 ? 30 + static_cast(30 * received / total) : 60; + const QString fileText = path.isEmpty() + ? QCoreApplication::translate("Updater", "Preparing download...") + : QCoreApplication::translate("Updater", "Downloading: %1").arg(path); progress.setValue(value); progress.setLabelText(QString("%1\n%2 / %3 · %4/s") .arg(fileText, formatBytes(received), formatBytes(total), @@ -193,31 +212,32 @@ int main(int argc, char* argv[]) }; return QFile::exists(bootstrapPath) && QProcess::startDetached(bootstrapPath, args); }; - const auto delegateRollback = [&](const QString& title, const QString& reason) { - FileHelper::killProcess(mainExecutable); - transaction.markRollbackRequired(reason); - progress.setLabelText("正在将回滚工作移交给 Bootstrap..."); - QApplication::processEvents(); + const auto delegateRollback = [&](const QString& title, const QString& reason) { + FileHelper::killProcess(mainExecutable); + transaction.markRollbackRequired(reason); + progress.setLabelText(QCoreApplication::translate("Updater", "Delegating rollback to Bootstrap...")); + QApplication::processEvents(); if (launchBootstrap("rollback")) { progress.close(); return 0; } - reportUpdateResult(false); - progress.close(); - QMessageBox::critical(nullptr, title, - reason + "\n\n无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。"); - return -1; - }; + reportUpdateResult(false); + progress.close(); + QMessageBox::critical(nullptr, title, + reason + QCoreApplication::translate("Updater", "\n\nCannot start Bootstrap to perform rollback. Do not continue running the software. Please contact the administrator.")); + return -1; + }; if (resumingFromBootstrap) { QString resumeError; if (!transaction.resumeExisting(&resumeError)) { - reportUpdateResult(false); - progress.close(); - QMessageBox::critical(nullptr, "事务续办失败", - QString("无法读取 Bootstrap 更新事务:%1").arg(resumeError)); - return -1; - } + reportUpdateResult(false); + progress.close(); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Transaction Resume Failed"), + QCoreApplication::translate("Updater", "Cannot read the Bootstrap update transaction: %1").arg(resumeError)); + return -1; + } fromVersion = transaction.fromVersion(); if (QFile::exists(QDir(transaction.backupDir()).filePath(".offline_mode"))) offlinePackagePath = "__bootstrap_resumed_offline__"; @@ -225,40 +245,54 @@ int main(int argc, char* argv[]) const bool stateOk = config.setValue("App", "current_version", fromVersion); transaction.markRolledBack(); reportUpdateResult(false); - if (stateOk) launchMainApp(); - progress.close(); - QMessageBox::warning(nullptr, "更新已回滚", - stateOk ? "新版本安装或启动失败,已自动恢复并启动旧版本。" - : "旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。"); - return stateOk ? 0 : -1; - } - if (bootstrapResult != "success") { - reportUpdateResult(false); - progress.close(); - QMessageBox::critical(nullptr, "自动回滚失败", - "Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。"); - return -1; - } - } else if (!transaction.initialize()) { - return fail("更新准备失败", "无法创建更新事务目录或保存事务状态。", "transaction_init_failed"); - } else if (!offlinePackagePath.isEmpty()) { - QFile marker(QDir(transaction.backupDir()).filePath(".offline_mode")); - if (!marker.open(QIODevice::WriteOnly) || marker.write("offline\n") != 8) - return fail("更新准备失败", "无法保存离线事务标记。", "offline_marker_failed"); - } - - setProgress(resumingFromBootstrap ? 72 : 10, offlinePackagePath.isEmpty() ? "正在获取并验证版本清单..." : "正在验证离线更新包..."); + if (stateOk) launchMainApp(); + progress.close(); + QMessageBox::warning(nullptr, + QCoreApplication::translate("Updater", "Update Rolled Back"), + stateOk + ? QCoreApplication::translate("Updater", "The new version failed to install or start. The old version has been restored and started automatically.") + : QCoreApplication::translate("Updater", "The old files were restored, but the old version number could not be written back. Please check configuration directory permissions.")); + return stateOk ? 0 : -1; + } + if (bootstrapResult != "success") { + reportUpdateResult(false); + progress.close(); + QMessageBox::critical(nullptr, + QCoreApplication::translate("Updater", "Automatic Rollback Failed"), + QCoreApplication::translate("Updater", "Bootstrap could not fully restore the old version. Do not continue running the software. Please contact the administrator.")); + return -1; + } + } else if (!transaction.initialize()) { + return fail(QCoreApplication::translate("Updater", "Update Preparation Failed"), + QCoreApplication::translate("Updater", "Cannot create the update transaction directory or save the transaction state."), + "transaction_init_failed"); + } else if (!offlinePackagePath.isEmpty()) { + QFile marker(QDir(transaction.backupDir()).filePath(".offline_mode")); + if (!marker.open(QIODevice::WriteOnly) || marker.write("offline\n") != 8) + return fail(QCoreApplication::translate("Updater", "Update Preparation Failed"), + QCoreApplication::translate("Updater", "Cannot save the offline transaction marker."), + "offline_marker_failed"); + } + + setProgress(resumingFromBootstrap ? 72 : 10, + offlinePackagePath.isEmpty() + ? QCoreApplication::translate("Updater", "Fetching and verifying version manifest...") + : QCoreApplication::translate("Updater", "Verifying offline update package...")); const QString manifestCacheDir = QDir(updateDir).filePath("manifest_cache"); if (resumingFromBootstrap) { - if (!logic.loadManifestCache(manifestCacheDir, targetVersion)) - return delegateRollback("清单缓存失败", "Bootstrap 安装后无法读取签名 Manifest 缓存。"); + if (!logic.loadManifestCache(manifestCacheDir, targetVersion)) + return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"), + QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation.")); } else if (offlinePackagePath.isEmpty()) { logic.getManifest(appId, channel, targetVersion, targetVersionId); } if (!logic.verifyManifestSignature()) { - if (resumingFromBootstrap) - return delegateRollback("安全验证失败", "Bootstrap 安装后无法重新验证版本清单签名。"); - return fail("安全验证失败", "版本清单签名无效,更新已停止。请联系管理员。", "manifest_signature_invalid"); + if (resumingFromBootstrap) + return delegateRollback(QCoreApplication::translate("Updater", "Security Verification Failed"), + QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation.")); + return fail(QCoreApplication::translate("Updater", "Security Verification Failed"), + QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."), + "manifest_signature_invalid"); } QStringList obsoletePaths; if (!resumingFromBootstrap && fromVersion != targetVersion) { @@ -272,47 +306,64 @@ int main(int argc, char* argv[]) qDebug() << "No valid signed previous manifest cache; obsolete deletion skipped for safety"; } } - if (!logic.saveManifestCache(manifestCacheDir)) { - if (resumingFromBootstrap) - return delegateRollback("清单缓存失败", "无法保存新版本 Manifest 缓存。"); - return fail("清单缓存失败", "无法保存新版本 Manifest 缓存,更新已停止。", "manifest_cache_failed"); - } - - if (!resumingFromBootstrap) { - setProgress(25, offlinePackagePath.isEmpty() ? "正在获取安全下载地址..." : "正在准备离线包文件..."); - if (offlinePackagePath.isEmpty()) - logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId); - if (logic.getFileList().isEmpty()) - return fail("没有可更新文件", "服务器没有返回任何版本文件,更新已停止。", "empty_file_list"); + if (!logic.saveManifestCache(manifestCacheDir)) { + if (resumingFromBootstrap) + return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"), + QCoreApplication::translate("Updater", "Cannot save the new version manifest cache.")); + return fail(QCoreApplication::translate("Updater", "Manifest Cache Failed"), + QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."), + "manifest_cache_failed"); + } + + if (!resumingFromBootstrap) { + setProgress(25, + offlinePackagePath.isEmpty() + ? QCoreApplication::translate("Updater", "Fetching secure download URLs...") + : QCoreApplication::translate("Updater", "Preparing offline package files...")); + if (offlinePackagePath.isEmpty()) + logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId); + if (logic.getFileList().isEmpty()) + return fail(QCoreApplication::translate("Updater", "No Files to Update"), + QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."), + "empty_file_list"); QStorageInfo storage(updateDir); storage.refresh(); const qint64 requiredBytes = logic.estimateAdditionalDiskBytes(targetDir, obsoletePaths); - const qint64 availableBytes = storage.bytesAvailable(); - if (!storage.isValid() || !storage.isReady() || availableBytes < requiredBytes) { - return fail("磁盘空间不足", - QString("更新至少需要 %1 可用空间,安装盘当前仅剩 %2。\n" - "所需空间已包含下载文件、旧版本备份和安全余量。") - .arg(formatBytes(requiredBytes), formatBytes(qMax(0, availableBytes))), - "disk_space_insufficient"); - } - - const QString stagingDir = transaction.stagingDir(); - setProgress(30, QString(offlinePackagePath.isEmpty() ? "正在下载并校验 %1 个版本文件..." : "正在提取并校验 %1 个离线文件...").arg(logic.getFileList().size())); - if (!offlinePackagePath.isEmpty()) { - if (!logic.loadOfflinePackage(offlinePackagePath, stagingDir)) - return fail("离线包提取失败", logic.offlineError(), "offline_package_invalid"); - } else { - if (!logic.downloadAllFiles(stagingDir, targetDir)) { - logic.reportDownloadResult(appId, channel, targetVersion, false); - return fail("下载失败", "部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。", "download_failed"); - } - logic.reportDownloadResult(appId, channel, targetVersion, true); - } - - setProgress(58, "正在校验完整版本文件..."); - if (!logic.validateLocalFiles(stagingDir, targetDir)) - return fail("文件校验失败", "暂存文件与版本清单不一致,更新已停止。", "staging_verify_failed"); + const qint64 availableBytes = storage.bytesAvailable(); + if (!storage.isValid() || !storage.isReady() || availableBytes < requiredBytes) { + return fail(QCoreApplication::translate("Updater", "Insufficient Disk Space"), + QCoreApplication::translate("Updater", + "The update requires at least %1 of free space, but the installation drive currently has only %2.\n" + "The required space includes downloaded files, old version backups, and a safety margin.") + .arg(formatBytes(requiredBytes), formatBytes(qMax(0, availableBytes))), + "disk_space_insufficient"); + } + + const QString stagingDir = transaction.stagingDir(); + setProgress(30, offlinePackagePath.isEmpty() + ? QCoreApplication::translate("Updater", "Downloading and verifying %1 version files...").arg(logic.getFileList().size()) + : QCoreApplication::translate("Updater", "Extracting and verifying %1 offline files...").arg(logic.getFileList().size())); + if (!offlinePackagePath.isEmpty()) { + if (!logic.loadOfflinePackage(offlinePackagePath, stagingDir)) + return fail(QCoreApplication::translate("Updater", "Offline Package Extraction Failed"), + logic.offlineError(), + "offline_package_invalid"); + } else { + if (!logic.downloadAllFiles(stagingDir, targetDir)) { + logic.reportDownloadResult(appId, channel, targetVersion, false); + return fail(QCoreApplication::translate("Updater", "Download Failed"), + QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network and try again."), + "download_failed"); + } + logic.reportDownloadResult(appId, channel, targetVersion, true); + } + + setProgress(58, QCoreApplication::translate("Updater", "Verifying complete version files...")); + if (!logic.validateLocalFiles(stagingDir, targetDir)) + return fail(QCoreApplication::translate("Updater", "File Verification Failed"), + QCoreApplication::translate("Updater", "The staged files do not match the version manifest. The update has stopped."), + "staging_verify_failed"); QStringList changedPaths; QDir stagingRoot(stagingDir); @@ -322,89 +373,118 @@ int main(int argc, char* argv[]) const QString runtimePrefix = config.runtimeRelativePath(); const QString bootstrapManifestPath = QDir::fromNativeSeparators( runtimePrefix.isEmpty() ? bootstrapExecutable : runtimePrefix + "/" + bootstrapExecutable); - for (const QString& path : changedPaths) { - if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0) - return fail("Bootstrap 无法自更新", QString("本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。").arg(bootstrapManifestPath), "bootstrap_self_update_blocked"); - } - if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths)) - return fail("事务记录失败", "无法保存已校验或待删除文件列表,更新已停止。", "transaction_record_failed"); - - setProgress(66, "正在关闭主程序..."); - if (!FileHelper::killProcess(mainExecutable)) - return fail("无法关闭主程序", QString("%1 仍在运行,请手动关闭后重试。").arg(mainExecutable), "mainapp_close_failed"); - - setProgress(72, QString("正在备份 %1 个待变更文件(其中删除 %2 个)...") - .arg(changedPaths.size() + obsoletePaths.size()).arg(obsoletePaths.size())); - if (!transaction.backupCurrentFiles()) - return fail("备份失败", "无法备份当前版本文件,尚未安装新版本。请检查磁盘空间和目录权限。", "backup_failed"); - - const QString planFile = bootstrapPlanFile(); - QSaveFile plan(planFile); - if (!plan.open(QIODevice::WriteOnly)) - return fail("接管准备失败", "无法创建 Bootstrap 文件计划。", "bootstrap_plan_failed"); + for (const QString& path : changedPaths) { + if (path.compare(bootstrapManifestPath, Qt::CaseInsensitive) == 0) + return fail(QCoreApplication::translate("Updater", "Bootstrap Cannot Self-Update"), + QCoreApplication::translate("Updater", "This version contains a new %1. Please upgrade this component with the installer, then publish the business version again.") + .arg(bootstrapManifestPath), + "bootstrap_self_update_blocked"); + } + if (!transaction.recordVerifiedFiles(changedPaths, obsoletePaths)) + return fail(QCoreApplication::translate("Updater", "Transaction Recording Failed"), + QCoreApplication::translate("Updater", "Cannot save the list of verified or pending deletion files. The update has stopped."), + "transaction_record_failed"); + + setProgress(66, QCoreApplication::translate("Updater", "Closing the main application...")); + if (!FileHelper::killProcess(mainExecutable)) + return fail(QCoreApplication::translate("Updater", "Cannot Close Main Application"), + QCoreApplication::translate("Updater", "%1 is still running. Please close it manually and try again.").arg(mainExecutable), + "mainapp_close_failed"); + + setProgress(72, QCoreApplication::translate("Updater", "Backing up %1 files to be changed, including %2 files to be deleted...") + .arg(changedPaths.size() + obsoletePaths.size()).arg(obsoletePaths.size())); + if (!transaction.backupCurrentFiles()) + return fail(QCoreApplication::translate("Updater", "Backup Failed"), + QCoreApplication::translate("Updater", "Cannot back up current version files. The new version has not been installed. Please check disk space and directory permissions."), + "backup_failed"); + + const QString planFile = bootstrapPlanFile(); + QSaveFile plan(planFile); + if (!plan.open(QIODevice::WriteOnly)) + return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"), + QCoreApplication::translate("Updater", "Cannot create the Bootstrap file plan."), + "bootstrap_plan_failed"); const auto writePlanItem = [&](char operation, const QString& path) { const QByteArray line = QByteArray(1, operation) + '\t' + path.toUtf8() + '\n'; return plan.write(line) == line.size(); }; for (const QString& path : changedPaths) { - if (!writePlanItem('C', path)) { - plan.cancelWriting(); - return fail("接管准备失败", "无法写入 Bootstrap 复制计划。", "bootstrap_plan_failed"); - } - } - for (const QString& path : obsoletePaths) { - if (!writePlanItem('D', path)) { - plan.cancelWriting(); - return fail("接管准备失败", "无法写入 Bootstrap 删除计划。", "bootstrap_plan_failed"); - } - } - if (!plan.commit() || !transaction.markAwaitingBootstrap()) - return fail("接管准备失败", "无法提交 Bootstrap 文件计划或事务状态。", "bootstrap_plan_failed"); - - setProgress(78, "正在将安装工作移交给 Bootstrap..."); - if (!launchBootstrap("install")) - return fail("Bootstrap 启动失败", QString("无法启动独立更新接管程序:%1").arg(bootstrapPath), "bootstrap_start_failed"); + if (!writePlanItem('C', path)) { + plan.cancelWriting(); + return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"), + QCoreApplication::translate("Updater", "Cannot write the Bootstrap copy plan."), + "bootstrap_plan_failed"); + } + } + for (const QString& path : obsoletePaths) { + if (!writePlanItem('D', path)) { + plan.cancelWriting(); + return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"), + QCoreApplication::translate("Updater", "Cannot write the Bootstrap deletion plan."), + "bootstrap_plan_failed"); + } + } + if (!plan.commit() || !transaction.markAwaitingBootstrap()) + return fail(QCoreApplication::translate("Updater", "Bootstrap Handoff Preparation Failed"), + QCoreApplication::translate("Updater", "Cannot commit the Bootstrap file plan or transaction state."), + "bootstrap_plan_failed"); + + setProgress(78, QCoreApplication::translate("Updater", "Delegating installation to Bootstrap...")); + if (!launchBootstrap("install")) + return fail(QCoreApplication::translate("Updater", "Bootstrap Startup Failed"), + QCoreApplication::translate("Updater", "Cannot start the standalone update handoff program: %1").arg(bootstrapPath), + "bootstrap_start_failed"); progress.close(); return 0; } - setProgress(82, "正在校验 Bootstrap 安装结果..."); - if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir)) - return delegateRollback("安装校验失败", "新版本文件安装后校验未通过。"); - for (const QString& path : transaction.obsoletePaths()) { - if (QFile::exists(QDir(targetDir).filePath(path))) - return delegateRollback("废弃文件清理失败", QString("废弃文件仍然存在:%1").arg(path)); - } - - setProgress(89, "正在保存新版本状态..."); - if (!config.setValue("App", "current_version", targetVersion) - || config.getValue("App", "current_version") != targetVersion) - return delegateRollback("状态保存失败", "无法保存当前版本号。"); - - const QString healthFile = transaction.healthFile(); - QFile::remove(healthFile); - setProgress(94, "正在启动新版本并等待健康确认..."); - if (!launchMainApp(healthFile)) - return delegateRollback("启动失败", QString("%1 无法启动。").arg(mainExecutable)); + setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result...")); + if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir)) + return delegateRollback(QCoreApplication::translate("Updater", "Installation Verification Failed"), + QCoreApplication::translate("Updater", "New version files failed verification after installation.")); + for (const QString& path : transaction.obsoletePaths()) { + if (QFile::exists(QDir(targetDir).filePath(path))) + return delegateRollback(QCoreApplication::translate("Updater", "Obsolete File Cleanup Failed"), + QCoreApplication::translate("Updater", "An obsolete file still exists: %1").arg(path)); + } + + setProgress(89, QCoreApplication::translate("Updater", "Saving new version state...")); + if (!config.setValue("App", "current_version", targetVersion) + || config.getValue("App", "current_version") != targetVersion) + return delegateRollback(QCoreApplication::translate("Updater", "State Save Failed"), + QCoreApplication::translate("Updater", "Cannot save the current version number.")); + + const QString healthFile = transaction.healthFile(); + QFile::remove(healthFile); + setProgress(94, QCoreApplication::translate("Updater", "Starting the new version and waiting for health confirmation...")); + if (!launchMainApp(healthFile)) + return delegateRollback(QCoreApplication::translate("Updater", "Startup Failed"), + QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable)); QElapsedTimer healthTimer; healthTimer.start(); while (healthTimer.elapsed() < healthCheckTimeoutMs && !QFile::exists(healthFile)) { QApplication::processEvents(); QThread::msleep(100); - } - if (!QFile::exists(healthFile)) - return delegateRollback("启动确认失败", QString("新版本在 %1 毫秒内没有完成启动健康确认。").arg(healthCheckTimeoutMs)); - - setProgress(99, "正在提交更新事务..."); - if (!transaction.commit()) - return delegateRollback("事务提交失败", "新版本已经启动,但无法提交更新事务。"); + } + if (!QFile::exists(healthFile)) + return delegateRollback(QCoreApplication::translate("Updater", "Startup Confirmation Failed"), + QCoreApplication::translate("Updater", "The new version did not complete startup health confirmation within %1 milliseconds.") + .arg(healthCheckTimeoutMs)); + + setProgress(99, QCoreApplication::translate("Updater", "Committing update transaction...")); + if (!transaction.commit()) + return delegateRollback(QCoreApplication::translate("Updater", "Transaction Commit Failed"), + QCoreApplication::translate("Updater", "The new version has started, but the update transaction could not be committed.")); QFile::remove(healthFile); QFile::remove(bootstrapPlanFile()); - reportUpdateResult(true); - progress.setValue(100); - progress.close(); - QMessageBox::information(nullptr, "更新完成", QString("软件已成功更新到 %1,并通过启动健康检查。").arg(targetVersion)); + reportUpdateResult(true); + progress.setValue(100); + progress.close(); + QMessageBox::information(nullptr, + QCoreApplication::translate("Updater", "Update Complete"), + QCoreApplication::translate("Updater", "The software has been successfully updated to %1 and passed the startup health check.") + .arg(targetVersion)); return 0; } diff --git a/config/manifest_public_key.pem b/config/manifest_public_key.pem index ae08914..990937d 100644 --- a/config/manifest_public_key.pem +++ b/config/manifest_public_key.pem @@ -1,9 +1,9 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMROESbT36XU4d3YjuwU -WAC2h5p3btFu/IAeF3bVtHMovIA4ZXXKYsiq5FycDnDzyD86ou3B7PP7uHhVhn2l -Uru7QElYRfEfQAFSU5dErc+SZo+oT170cgq1ePPD/YleKPRqFAL221Tbh5pusHcZ -Ocujit2qrfg5f4rIEzWu7kBKVSJ6WChkjetEL6OZ43ClkDyUGebUuaQ9dv39YVlv -Fp2pfARi7/7djMMncLVaFU2AAIuSy3jgQg65DOFRVPSr1P/rRfuqU55ZmqAmmZIs -F/KVpne6zLFBXrxf5rNTBch+nxX+hcE7M+K0PJA5Ie669qRQFRwxoJlGpSOvMefQ -TwIDAQAB ------END PUBLIC KEY----- +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArX1FSi06eP8XhX4B3Oy6 +FzfkTe3FBIg6OjWpT1541LivRTRt9HXZ30nKuIs5itXBzBQPMU8hsfR0MD9nfPG/ +NlajfZzqbyxAJlUMeThJiwtyz98wv3VE1hS2Bwuc3mbmtfU0zl/MoPdWrluL3x3C +bt0ylL00rLBuvjgG21zDflVqj3w9CwpKHFsNrSYoI6vOFX9YrRZ9tKcPEkSRPqts +411QkAQLtMiOMIIhNe5aFIL7doCnglx32hJeHNCTUSxjM9VyHUTEj8wKN/6Gy5iP +YZGRafeiJZ32RRIHyssereAg3Xe/3scd+tbFNNYP9xrRhRgDacmkSCBodnbJd4Qc +rQIDAQAB +-----END PUBLIC KEY----- diff --git a/i18n/update-client.qrc b/i18n/update-client.qrc new file mode 100644 index 0000000..8019ce3 --- /dev/null +++ b/i18n/update-client.qrc @@ -0,0 +1,5 @@ + + + update-client_zh_CN.qm + + \ No newline at end of file diff --git a/i18n/update-client_zh_CN.qm b/i18n/update-client_zh_CN.qm new file mode 100644 index 0000000000000000000000000000000000000000..e24568e2ef1b617b1dabf74a77f45a1d67ed8025 GIT binary patch literal 18431 zcmcIr3v^WFwf^&%JcgM`j1V9OIKc#B3?YOFic|>!ib^KQObFyc66bN|(M--v=aG=q7fvPQOWz|Ol${_XZzuWSkVMGj5IJ$`UP5N= zAZPdY5R!WhIsXVeZyU+VT6mt2+1*(sb#;VHpOban8(u;t?8|B^z6GHzTjWA=ovJwiy) zglyY4;P;8P?5W4#yt6v8g|SJ5EUw9({{+ZY@nW`XH^^~?n0?2`BZOS)&Hknn_|*(& z-}SQ!_}<&u_wRwvOIKz;dk*wrc`y6T)zb;7a%8`|_g+G#Ez17nzd8uHeq#2yTh0@5 z<=*Ubr?Uvjejq3JFnn)PO-^g-Dfql2=cxhs`@Cg2&;J?d&pDd&&V9LrT>a~u)2H4c zB&R;-^I?#$#F|@haDA-hmZr{Llge>}T?tv0G zNAc?__b^Yydp2pnQmi7=*_o(Ii58+(- z&skO<1v}3Ek)?Mf{JiEi%Qqs=gIpoYzRP|9a(~_O-5ZiXXNu*>FX8j4*H~UV@_%6G z>nv}7^KbBT)bdda&|8>m`RK@LLYh|OWmj~7oPoT;pCG;G^NKDn0XY}t&By_L%zQNO z#_v}VQams3Yxjd)NL$`F>Yd051@!jk z4;~)@yPeJ7_$ko2_Kp0V_f03n{&N1_JC1;!KgoaZp@X3RSb=Tb3t;aL3X1NA^A$`g zXnblJA%%|>EZ^V&y}e)1@#lL%4?ipTBI;GHY`3XX@eqK1~VbG8Lp28)TorGMz zt8iTazAyZ|aJ}_B*yVx3C$9#-oBl-M^V=_j^JEvk?rS7uX?Nit_rmwedkRkv0pIFj zYspcGkhwu?X%Re^`<%6M+9RO1Ve5iz@Ut*x9XbGfC(pL-zQPZ39LEKe_H?c z>C5o^=?Nh}JXiT|69&HQ1buZ)_+As(w`}=@AMS?V>whxg$H(CBGmcDnde{Mam^I@uKp)e?L0TOi++@7b%?!q01p?2DS- z2RlD(zv*}I`_*^aU4Mn=mlFHIUyXp?KDR#~hUaHA*gv?_3H~_KF?9&|T(;0L|5>ox zm*zTF{_o3R=RJ;9e|QSw<~fIRT_e~#!hX&<2vcIQNj_nD-dun-3lDzq}XnLxtn`ue!ir-*J4NGaclaRkW}Y z{1!Cb?%xUepH|eF3wFHz^`gG>Nzls&MR$J$^c%{H{(b&Lh-+ujYd-}!7Y&EuP|xMS zgN|>JZJRe9Nal5{4L0vL0I+zetd?@{RKNrvwi3aP; zbzPRK_AYe4(ZAJsN7{m@3)R^dANpJ;l8pX)PT$N%x4h~y2&s=B4%KscUA3K4pEY8R~{W`%yyf0L{Jma`*& z5I@^{+I{lYb6XE}9`pWX%+`Hs-?e>SlvgFomz#uG6f%f6A_|G2L{jV*lHq<)h({w4 zuLq)J(324SViMv}^w$aQq$i$C#e@)i>J=e&ypgD{5B_4*P9>8BO}eo&CHCxcP3}Cl zn`DsV#wI~FFvtVpIV2>|_aHX@0isyZ1GXOW{$}K;V)fFdR5BEehlg22yTq^1BO1?9 z*5_MOz3I|*Yd4N`*zq)y#l4!;;eQITnav(wC@ckqL^vpcHThBcJyJM;0aRzApaSvT z$m?hhg^aD)BT3OD)3Q+#0Utm~Fqxwy16xoglt&jdi)BJS5KE+vB@4y{QrwB>-wQ@p z9Wvf^%=bXxw0JmZcOB>qjoF+NQ4BP#)85`R`uNb#MxNF=*E%jnRFXiu0_19R;Z)4; zNrJnHao}1jcqEqmxG1vy{ZYurs0}C!(Lo^^2t>jXJjFcFM4z=Nk5SV4?7-ZPS?fM; z7g0hyf!i`RL(%z)amZLF+|VD3#z7QGkwZR23}VC+6t6d0s}w~UJ?nlWSi7b^{^-qF zIvdT98OKErJ3mB{CxYio(u!rII1wqLik;UEPw3gDqgN%Q`|RRe~lMPE@*xq&J>c-)-WbQH1u+wCHv2^m9J<=k9F8WgfMGTFg+7(4-;LQ}t4 z*|tJru4*vqp%f!B^o!v8yQ0p|P-?TK zOb$lleZp$VGXTElfx2(_`AEM%Ym+Kp3YB2VmxCD&nWL!7CR8Qv_0?7e&Q-_V`$vl$ip4G(|Oz zR=GBKKhl4-l8!ZrZCirE_GebN%cqlq5$~M<4cAIL>YKNN;Az~ zvqsM-hEN{1H+atuFj+*Fu0fY2ZSqS1i5Fio+Ue6mh^|0A7{Q#LVZ5_QxUsZUN!a?CjBCLIfsqut8R!6~#n8+q!M-A>^q|DQ*$) zOgK5jic!Xt;?M?*;#1Sy)cFPO56rcMHU>A0E@oq}Oc(AYZi=xk9PI_DT8;1yw+!<$ zB|_N`u+wqgfoTGg7mf;q*7eRsi1KlSFlIyGl{mbndSA=PgDS|isslVu*Xr^9*7SqI%opCXtZ^o3d{`>mcueeTMZQ?4UjP4pt%9Z zK!ExML05@Et*>P;GXenAx+zty!%?%46-T&A7%;dGNeo5-ese@shHO~>f+ik5iCv72 z_|(oqDW>Z>2e>j2V#k{vQ z38a|_p1h?-mE!5ecg2+|=5vv`9JEjzxI(pO2Wy6<0Z$}s)}Gc9ZdJBUB3*@ zz*!b{IaoKnrMGt6a+V!j#n*#;8Bpg(mfICPwzwgNBe*)AUjfsq`yNTIy&y3BB?+mW zI=ChhO!QK!mK61%v;ORiGNlz@ZtQ~I7w+DODN{*1MRK0WjKCbMiW_o9DSI-mj0iI2 zj_FwX5FZ;1t?j8N0@w%#h7iWf<~6N-v^__1s3k=1Xir^Ltw@t%wu`3#g2R-9+0K-% zkj6kPxuw$?YCx6FDuFs3!9vc$ip{Z6oggdIJWUl~lwqz^lwZsVx#Z!CG0o>9=2RXi zN`g148a(6ooj9@OZ6lmxDd5&9NdkgRLhZrLuY~;&+M#tL4tm^OssseQq7{jNeS1P285j6y6G6K$*)>Pw}F#~vSay`_Z*!yvZ zW6w*Yud7HdX7)`a!;uI-L0aetj*!Di0VXUdm{<(4u?CE=Jwa&0X;^DGm-gJ(n}Z#) z^QEsYShseKzF<@|!K}fXh*EfhQ*4+FAY*X@O#c%+o;4)f*u>3g?W;f+4b~tw8nd;o zK>gh8?Xru<(dStDz)|VA&3!irlY*53Y#u_8&{J%HqaF>`3f>e;XHkIW19C)YfI0%O zD5OV}-TKe+2;1hrLPG}btN}Drq3w?1?A|Bv*is=fjS{cM^wgE_JjbFQ+TEJo+ud)0 zLUAKf&_3Xt;}Z7y>0e>G?5r6p)Mc9$O97;zcLS6FmqFVBl8M5+6gCYWDTobCSn@?u ze*98ZgXSB4fPFY=WdlQvWCbXd=2&**1YFXoR`bAgvv5aW*!)-Y4<(J1r=wR6Y5<>s zIV{jHr)a3!d=G3q(EegqzDAVFG>w&jo1s?~+Tvo&!}c+mBrG?R&`?^Yq-h(0sYbRN z&FJBY*KHS*=?J)}R5zUFo_6Oz3PW7>Xqv`UZvW5AcMd!YCMPxlR4h16wp%mJLM=G= zYf8gfg9(=)8T2MT^`gFQTVDn#19pTAHKUTSt=aI*z+1z^drx<4$K4_B^;9|WR+%wf z6@Vvlgv%2Eb^ba55elwU_{RvAaI{NT2=eMrjhnFBf-PX8Pe{Ok6>1!GV(Fz|dMlSO z=G6u~o9|WULGx;dCmlWDD)E*&M_mVSEUP-rweo48E$oL>j@2n`e42w}5gY--G$QVi z64;x=wi1d9Gz$hLSEC@%R(u`@U-5I!kHGoC1#`44ia!4s4_#dUKaiCgl$zk0bCWPtkC=^?Sv*@u<`T zkZkv(Qbq7h@^i`iw+2=_z9 z02g3QGwgUYK!>p?5{@DYacV?zQac%vb|fDyX4j?oqJ5zQDW-fPZmA|lXqk=LQi8Cc_a@1)=GnZZW7EyIGy zQ?HJo&`hfxG-ilc>1I}CYcq;pVE}wy6oU9sTTO0B(bT!hw@I zTOv4C%y0pQ2uKrGl<5O-~$GMC0DD-!Dpra$n$_uMlZ;}PqnNinhTPN@QKyTV z6=ITIxzH+tYk{X`Gk2mRBjkXSd({7$`ZY^*^aX`}0tfjpY~;pV?OMfm$eP~6hMGV- zYMfxo3T)jwX$@mr_)OjadxWho7@ikyP+yE27ab1Ro%S6+Qhaza&`AXXVP6<;+bu(A z?M7&A@M9|))osB!qxxYwMpd>Esx^dG2N$_s)jV9ziZZH^;WktTtnkWq{Zf^|ufaVe zw=#7iAwZYtGEk6}%#e^e%NVn|BXS^E=st;g5t%B-ErH!2nvHhttCgLeM%ZFLzL6}+ zJdMT6Tlv(6n!Xt>mPwg=bO%#6Mwf<41z-Hgsup-m0qDt)PHc>n)4n@fweg*6E022d`C$j*Z!b!-! zXiSAO(}Z|J0dK~eI5H$O$$yGqcsiY6m!|2l8kkY6G@QfcQAg%7xORYtQzJFjCz;4Y z7l)YQG=?u%7Kh^r_NBZ3;>ttk<<*DoPuyfBX(Dq-1G$PUAVNBhaK4E>J#IJ*^G(qm zg^{=>q;M0)^B%x&IndlIo8~lJwLlYa=|L#%-4WCAbq6NsWadHWOf)uvMi%}(0~>GX zy}eEBc?_@2>hya(o&TVQ8H02tKqiBpFs(Vb#14s`NHWB370I`D%}i=T#>hxxgUcuz z2-;@6RLn2^W(d1+txq)(VnfeCKhiM01en4m8azmS0Y&p*H8yJ48m4dEVW!-5TdNZx z!@w36FRE<0z5@)w8#lR+HNRgcr%G0{m0KA+s0PEJOb=|n2BGJbv#&9sHsB71%g1(V z^;rm~P%ex*n7Yurg18<=iB*#NP*V8{9J@qmT&&gNg)YEf2}M2@tO3h9=SWj^#{W5gmuwG?TbX;&4&0onR?mjj03QEQ|59;1SC1V>1zuN_9aATfih)f3ee|T3ms4pYj&F?!)Bes%IS< zE=3WP^6a3yZ?u}q!Y`eoQ`0GTBIWami3D35Ft3KP7{xq*wF~Y@!4(i%*7V5(3#w5` zvjSg-F&0*S2Py}Fx6|$Y5qE4|aWr%IKmAGc*- zOV3^K@=K{C>~=dH!P?*=bU-GSD)QS=6gjYYfl+O-F+Jg;OcEo2KIBW_X(?Ha&_s?= zJAM>s^QY@CkuRLmEXag#AdTdje&u$nqwaD}N^8}QvH?~-p3{6_aFx25As#8jx^ z`Yg6M#f?eFc5E73yY{@II?&^)1Et?sg10o_osuM47{-`^xCkx^#}(6=L*1ST{y1Fp zC2OkX+w#wfF>fia;BJfR-1H(BGh`gEKG1iv0Gxr?cvW9EF|bPY zL)Qt%fmdrX-Jmzxu$KEZA_{u%MKd`bJ!5(^!H=radoq15gN?}3cVbxelZ8}|jaAoo z#(NQAS+KUR0_8=&5f9UAk>GQ^btdAU<4I4F18d|}x#JXwoRVy|gB3KlR1MqUekN7vHTbSl@p zlo>F0T=r@f0F6|Hze1(yhv*;=IpTUBV+gagj{fCo`WPYfIt`C33hT_;LOOuJ);yi| zGLz*42sx9;70rw+UB2K-nAdLIB$lu7+4X3|=)79JF<6{{1LzrPft*E|3x%ZnmZYG!e1Awg=mkj22z zQ)L+dkrCYmkB@34ml0fVC9HQ7a64S^mLlvm;nC2l5*6!g2$LWz8Rgj_lqrT{S?84b zFtgm2*8jOV>U02G!&Vtz17greu}5Q@JfjLrctMW2w+`ZP@Fl=&p>SJ{y+vcRPNk8d zf9dCWcJ${ zc3KQWF}CLwbN|)3&?2+*W2|FW2%tmf#!0X@x3js*OPCO$#l1zagMLwC#ClU^G-Tws zfExi6a>^IH%~{A+?P>(O-Uy!AN}PB>U$xnwZvg0(46{Q&P`}aL>O@p~K^F4-zN--| z3Pnh9_-eBg$b>0PUX08oFJ{W=YTUc2=X)B7P5n?N91*1;IKvmoJdqK{AZ>3(&(koN TXC}Fl5QKg)=|S(hAea9KpQd+1 literal 0 HcmV?d00001 diff --git a/i18n/update-client_zh_CN.ts b/i18n/update-client_zh_CN.ts new file mode 100644 index 0000000..7ebdcb7 --- /dev/null +++ b/i18n/update-client_zh_CN.ts @@ -0,0 +1,874 @@ + + + + + ConfigHelper + + + The user canceled the administrator permission confirmation. + 用户取消了管理员权限确认。 + + + + Windows error %1 + Windows 错误 %1 + + + + Administrator Permission Required + 需要管理员权限 + + + + The current installation directory requires administrator permission to save configuration. + +Target file: %1 +Reason: %2 + +Click OK, then choose Yes in the Windows permission confirmation dialog. + 当前安装目录需要管理员权限才能保存配置。 + +目标文件:%1 +原因:%2 + +点击“确定”后,请在 Windows 权限确认窗口中选择“是”。 + + + + The user canceled the administrator permission request. + 用户取消了管理员权限请求。 + + + + Launcher + + + Checking for software updates... + 正在检查软件更新... + + + + Marsco Launcher + Marsco 软件启动器 + + + + Please enter the License for %1: + 请输入 %1 的授权 License: + + + + + the application + 软件 + + + + %1 + +Please enter a new License for %2: + %1 + +请重新输入 %2 的授权 License: + + + + Enter License + 输入 License + + + + License Required + 需要 License + + + + A License provided by the administrator is required for the first launch. + 首次启动需要输入管理员提供的 License。 + + + + License Cannot Be Empty + License 不能为空 + + + + Please paste the License created in the admin page. + 请粘贴管理员在后台创建的 License。 + + + + Failed to Save License + 保存 License 失败 + + + + Cannot write the configuration file: +%1 +%2 + 无法写入配置文件: +%1 +%2 + + + + Verifying License... + 正在验证 License... + + + + Device Authentication Failed + 设备身份验证失败 + + + + The current License cannot be used: %1 + 当前 License 无法使用:%1 + + + + The device or License authorization is invalid. + 设备或 License 授权无效。 + + + + The current authorization was rejected by the server: %1 + 当前授权被服务端拒绝:%1 + + + + License Saved + License 已保存 + + + + Please restart Launcher to complete device authorization and update checking. + 请重新启动 Launcher 完成设备授权和更新检查。 + + + + Authorization Rejected + 授权被拒绝 + + + + Select Offline Update Package + 选择离线更新包 + + + + Marsco offline update package (*.upd) + Marsco 离线更新包 (*.upd) + + + + + Offline Update + 离线更新 + + + + No offline update package was selected. + 未选择离线更新包。 + + + + Verifying local runtime policy... + 正在验证本地运行策略... + + + + + + Cannot Start + 无法启动 + + + + The local version policy is invalid: %1 + 本地版本策略无效:%1 + + + + The local version policy has expired. Please connect to the network or contact the administrator. + 本地版本策略已过期,请连接网络或联系管理员。 + + + + Current Version Cannot Run + 当前版本不可运行 + + + + The current version %1 has been disabled by the administrator. + 当前版本 %1 已被管理员停用。 + + + + Cannot read the local state: %1 + 无法读取本地状态:%1 + + + + + Security Check Failed + 安全检查失败 + + + + A version policy sequence rollback was detected. Startup has been blocked. + 检测到版本策略序列回退,已阻止启动。 + + + + A possible system time rollback was detected. Startup has been blocked. + 检测到系统时间可能被回拨,已阻止启动。 + + + + Version Rollback + 版本回退 + + + + Update Required + 必须更新 + + + + New Version Available + 发现新版本 + + + + The administrator provided version %1 as the rollback target. Downgrade now? + 管理员提供了版本 %1 作为回退目标,是否现在降级? + + + + Version %1 is available. Update now? + 发现新版本 %1,是否现在更新? + + + + +Target version: %1 + +目标版本:%1 + + + + + Startup Failed + 启动失败 + + + + + Cannot start the main application: %1 + 无法启动主程序:%1 + + + + Updater Startup Failed + 更新器启动失败 + + + + Cannot start the updater: %1 + 无法启动更新器:%1 + + + + Caching signed version manifest... + 正在缓存签名版本清单... + + + + Manifest Cache Failed + 清单缓存失败 + + + + Cannot cache the signed manifest for the current version: %1 + 无法缓存当前版本的签名清单:%1 + + + + Server Unavailable + 服务器不可用 + + + + Cannot connect to the update server. Import an offline update package? + 当前无法连接更新服务器。是否导入离线更新包? + + + + No offline update package was selected, or the updater could not be started. + 未选择离线更新包或无法启动更新器。 + + + + Network Unavailable + 网络不可用 + + + + Cannot connect to the update server, and the current policy does not allow offline startup. + 无法连接更新服务器,且当前策略不允许离线启动。 + + + + The application is up to date. Starting... + 当前已是最新版本,正在启动... + + + + Offline mode is active. Starting... + 当前处于离线模式,正在启动... + + + + Updater + + + Invalid Offline Package + 离线包无效 + + + + Updater Argument Error + 更新器参数错误 + + + + The updater is missing online update arguments or an offline update package. + 更新器缺少在线更新参数或离线更新包。 + + + + Offline Package Not Applicable + 离线包不适用 + + + + The update package application or channel does not match the local configuration. + 更新包的应用或渠道与本机配置不一致。 + + + + Offline Update Rejected + 离线更新被拒绝 + + + + The local signed policy has expired, forbids offline updates, or does not allow the target version. + 本地签名策略已过期、禁止离线更新或不允许目标版本。 + + + + Downgrade Forbidden + 禁止降级 + + + + The current signed policy does not allow installing an offline package with a lower version. + 当前签名策略不允许安装较低版本的离线包。 + + + + + Update Recovery Failed + 更新恢复失败 + + + + An unfinished update was detected, but the old version could not be restored: %1 +Do not continue running the software. Please contact the administrator. + 检测到上次更新未完成,但无法恢复旧版本:%1 +请不要继续运行软件,并联系管理员。 + + + + The old files were restored, but the version state could not be restored. Please check write permissions for the configuration directory. + 旧文件已经恢复,但无法恢复版本状态,请检查配置目录写入权限。 + + + + Preparing update... + 正在准备更新... + + + + Updating to %1 + 正在更新到 %1 + + + + Preparing download... + 正在准备下载... + + + + Downloading: %1 + 正在下载:%1 + + + + Delegating rollback to Bootstrap... + 正在将回滚工作移交给 Bootstrap... + + + + + +Cannot start Bootstrap to perform rollback. Do not continue running the software. Please contact the administrator. + + +无法启动 Bootstrap 执行回滚。请不要继续运行软件,并联系管理员。 + + + + Transaction Resume Failed + 事务续办失败 + + + + Cannot read the Bootstrap update transaction: %1 + 无法读取 Bootstrap 更新事务:%1 + + + + Update Rolled Back + 更新已回滚 + + + + The new version failed to install or start. The old version has been restored and started automatically. + 新版本安装或启动失败,已自动恢复并启动旧版本。 + + + + The old files were restored, but the old version number could not be written back. Please check configuration directory permissions. + 旧文件已经恢复,但旧版本号写回失败,请检查配置目录权限。 + + + + Automatic Rollback Failed + 自动回滚失败 + + + + Bootstrap could not fully restore the old version. Do not continue running the software. Please contact the administrator. + Bootstrap 无法完整恢复旧版本。请不要继续运行软件,并联系管理员。 + + + + + Update Preparation Failed + 更新准备失败 + + + + Cannot create the update transaction directory or save the transaction state. + 无法创建更新事务目录或保存事务状态。 + + + + Cannot save the offline transaction marker. + 无法保存离线事务标记。 + + + + Fetching and verifying version manifest... + 正在获取并验证版本清单... + + + + Verifying offline update package... + 正在验证离线更新包... + + + + + + Manifest Cache Failed + 清单缓存失败 + + + + Cannot read the signed manifest cache after Bootstrap installation. + Bootstrap 安装后无法读取签名 Manifest 缓存。 + + + + + Security Verification Failed + 安全验证失败 + + + + Cannot reverify the manifest signature after Bootstrap installation. + Bootstrap 安装后无法重新验证版本清单签名。 + + + + The version manifest signature is invalid. The update has stopped. Please contact the administrator. + 版本清单签名无效,更新已停止。请联系管理员。 + + + + Cannot save the new version manifest cache. + 无法保存新版本 Manifest 缓存。 + + + + Cannot save the new version manifest cache. The update has stopped. + 无法保存新版本 Manifest 缓存,更新已停止。 + + + + Fetching secure download URLs... + 正在获取安全下载地址... + + + + Preparing offline package files... + 正在准备离线包文件... + + + + No Files to Update + 没有可更新文件 + + + + The server did not return any version files. The update has stopped. + 服务器没有返回任何版本文件,更新已停止。 + + + + Insufficient Disk Space + 磁盘空间不足 + + + + The update requires at least %1 of free space, but the installation drive currently has only %2. +The required space includes downloaded files, old version backups, and a safety margin. + 更新至少需要 %1 可用空间,安装盘当前仅剩 %2。 +所需空间已包含下载文件、旧版本备份和安全余量。 + + + + Downloading and verifying %1 version files... + 正在下载并校验 %1 个版本文件... + + + + Extracting and verifying %1 offline files... + 正在提取并校验 %1 个离线文件... + + + + Offline Package Extraction Failed + 离线包提取失败 + + + + Download Failed + 下载失败 + + + + Some files failed to download or failed SHA-256 verification. Please check the network and try again. + 部分文件下载失败或 SHA-256 校验未通过,请检查网络后重试。 + + + + Verifying complete version files... + 正在校验完整版本文件... + + + + File Verification Failed + 文件校验失败 + + + + The staged files do not match the version manifest. The update has stopped. + 暂存文件与版本清单不一致,更新已停止。 + + + + Bootstrap Cannot Self-Update + Bootstrap 无法自更新 + + + + This version contains a new %1. Please upgrade this component with the installer, then publish the business version again. + 本次版本包含新的 %1。请使用安装包升级该组件,再重新发布业务版本。 + + + + Transaction Recording Failed + 事务记录失败 + + + + Cannot save the list of verified or pending deletion files. The update has stopped. + 无法保存已校验或待删除文件列表,更新已停止。 + + + + Closing the main application... + 正在关闭主程序... + + + + Cannot Close Main Application + 无法关闭主程序 + + + + %1 is still running. Please close it manually and try again. + %1 仍在运行,请手动关闭后重试。 + + + + Backing up %1 files to be changed, including %2 files to be deleted... + 正在备份 %1 个待变更文件(其中删除 %2 个)... + + + + Backup Failed + 备份失败 + + + + Cannot back up current version files. The new version has not been installed. Please check disk space and directory permissions. + 无法备份当前版本文件,尚未安装新版本。请检查磁盘空间和目录权限。 + + + + + + + Bootstrap Handoff Preparation Failed + 接管准备失败 + + + + Cannot create the Bootstrap file plan. + 无法创建 Bootstrap 文件计划。 + + + + Cannot write the Bootstrap copy plan. + 无法写入 Bootstrap 复制计划。 + + + + Cannot write the Bootstrap deletion plan. + 无法写入 Bootstrap 删除计划。 + + + + Cannot commit the Bootstrap file plan or transaction state. + 无法提交 Bootstrap 文件计划或事务状态。 + + + + Delegating installation to Bootstrap... + 正在将安装工作移交给 Bootstrap... + + + + Bootstrap Startup Failed + Bootstrap 启动失败 + + + + Cannot start the standalone update handoff program: %1 + 无法启动独立更新接管程序:%1 + + + + Verifying Bootstrap installation result... + 正在校验 Bootstrap 安装结果... + + + + Installation Verification Failed + 安装校验失败 + + + + New version files failed verification after installation. + 新版本文件安装后校验未通过。 + + + + Obsolete File Cleanup Failed + 废弃文件清理失败 + + + + An obsolete file still exists: %1 + 废弃文件仍然存在:%1 + + + + Saving new version state... + 正在保存新版本状态... + + + + State Save Failed + 状态保存失败 + + + + Cannot save the current version number. + 无法保存当前版本号。 + + + + Starting the new version and waiting for health confirmation... + 正在启动新版本并等待健康确认... + + + + Startup Failed + 启动失败 + + + + %1 cannot be started. + %1 无法启动。 + + + + Startup Confirmation Failed + 启动确认失败 + + + + The new version did not complete startup health confirmation within %1 milliseconds. + 新版本在 %1 毫秒内没有完成启动健康确认。 + + + + Committing update transaction... + 正在提交更新事务... + + + + Transaction Commit Failed + 事务提交失败 + + + + The new version has started, but the update transaction could not be committed. + 新版本已经启动,但无法提交更新事务。 + + + + Update Complete + 更新完成 + + + + The software has been successfully updated to %1 and passed the startup health check. + 软件已成功更新到 %1,并通过启动健康检查。 + + + + UpdaterLogic + + + Cannot open the offline update package + 无法打开离线更新包 + + + + The offline package format identifier is invalid + 离线包格式标识无效 + + + + The offline package header is incomplete + 离线包头不完整 + + + + The offline package header length is invalid + 离线包头长度无效 + + + + The offline package header JSON is invalid + 离线包头 JSON 无效 + + + + The offline package RSA signature is invalid + 离线包 RSA 签名无效 + + + + The offline package signature metadata is invalid + 离线包签名元数据无效 + + + + The manifest digest does not match the package signature + Manifest 摘要与包签名不一致 + + + + The offline manifest is invalid + 离线 Manifest 无效 + + + + Package information does not match manifest identity + 包信息与 Manifest 身份不一致 + + + + The offline manifest RSA signature is invalid + 离线 Manifest RSA 签名无效 + + + + The offline package contains an unsafe path or out-of-range data: %1 + 离线包包含不安全路径或越界数据:%1 + + + + Cannot prepare offline file: %1 + 无法准备离线文件:%1 + + + + Cannot create staged file: %1 + 无法创建暂存文件:%1 + + + + Failed to read offline file: %1 + 离线文件读取失败:%1 + + + + Offline file hash verification or write failed: %1 + 离线文件 Hash 或写入失败:%1 + + + + The offline package file count does not match the manifest + 离线包文件数量与 Manifest 不一致 + + + diff --git a/pdf_requirements.txt b/pdf_requirements.txt deleted file mode 100644 index 7367fb3..0000000 --- a/pdf_requirements.txt +++ /dev/null @@ -1,2943 +0,0 @@ -MARSCO_内部 - -软件自动升级与版本控制系统开发设计文档 - -v0.1 - -角色 - -编写 - -校对 - -校正 - -姓名 - -待填写 - -待填写 - -待填写 - -日期 - -待填写 - -待填写 - -待填写 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - - - - - - MARSCO_内部 - -修订记录 - -版本 - -日期 - -修订章节 - -修订内容 - -v0.1 - -2026-06- - -技术选型 - -明确客户端与服务端技术栈边界。 - -修订人 - -待填写 - -23 - -v0.1 - -2026-06- - -总体架构 - -梳理客户端、服务端和对象存储交互关 - -待填写 - -23 - -系。 - -v0.1 - -2026-06- - -客户端目录结 - -将目录树整理为正式表格。 - -待填写 - -23 - -构 - -v0.1 - -2026-06- - -本地配置文件 - -补充 app_config、client_identity、 - -待填写 - -23 - -设计 - -version_policy、local_state 字段说 - -明。 - -v0.1 - -2026-06- - -Manifest 设计 补充全量清单、签名和文件完整性规则。 待填写 - -23 - -v0.1 - -2026-06- - -启动票据设计 明确票据用途、校验规则和安全边界。 - -待填写 - -23 - -v0.1 - -2026-06- - -客户端启动逻 - -新增 Visio 流程图和步骤表。 - -待填写 - -23 - -辑 - -v0.1 - -2026-06- - -REST API 设计 明确设备身份接口非用户登录,补充管理 - -待填写 - -23 - -员登录仅用于后台。 - -v0.1 - -2026-06- - -数据库设计 - -补充应用、通道、版本、策略、设备、授 - -待填写 - -23 - -权和审计日志表。 - -v0.1 - -2026-06- - -对象存储目录 - -将对象存储目录整理为正式表格。 - -待填写 - -23 - -设计 - -v0.1 - -2026-06- - -Hash 校验策略 明确更新可增量、校验必须全量。 - -待填写 - -23 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -v0.1 - -2026-06- - -签名策略 - -补充签名算法、key_id 和稳定序列化要 - -待填写 - -23 - -求。 - -v0.1 - -2026-06- - -限流策略 - -将限流规则整理为表格。 - -待填写 - -23 - -v0.1 - -2026-06- - -回滚机制 - -补充事务状态机和失败恢复规则。 - -待填写 - -23 - -v0.1 - -2026-06- - -Demo 开发里程 - -按阶段整理开发计划。 - -待填写 - -23 - -碑 - -v0.1 - -2026-06- - -Demo 验收用例 补充升级、回滚、禁用、预览版、离线等 - -待填写 - -23 - -验收用例。 - -v0.1 - -2026-06- - -后续扩展方向 补充 License、灰度、多平台、日志上传 - -待填写 - -23 - -等扩展方向。 - -v0.1 - -2026-06- - -设计范围与登 - -明确本期默认可用且不做普通用户登录校 - -待填写 - -23 - -录边界 - -验;管理员登录仅用于后台。 - -v0.1 - -2026-06- - -本期范围 - -明确普通用户登录本期不做,默认可用; - -待填写 - -23 - -后续可扩展权限和授权校验。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - - MARSCO_内部 - -目录 - -1 设计范围与登录边界 ............................................................................................................. 6 - -2 技术选型 ............................................................................................................................. 6 - -2.1 客户端技术栈 ..................................................................................................................... 6 - -2.2 服务端技术栈 ..................................................................................................................... 7 - -2.3 文件存储 ............................................................................................................................ 8 - -3 总体架构 .............................................................................................................................. 8 - -4 客户端目录结构 .................................................................................................................... 9 - -5 本地配置文件设计 .............................................................................................................. 11 - -5.1 app_config.json ................................................................................................................ 11 - -5.2 client_identity.dat ............................................................................................................. 11 - -5.3 version_policy.dat ............................................................................................................ 11 - -5.4 local_state.json ................................................................................................................ 12 - -6 Manifest 设计 .................................................................................................................... 13 - -7 启动票据设计 ..................................................................................................................... 14 - -8 客户端启动逻辑 .................................................................................................................. 15 - -8.1 Launcher 启动逻辑 ........................................................................................................... 16 - -8.2 MainApp 启动逻辑 ............................................................................................................ 18 - -8.3 Updater 升级逻辑 ............................................................................................................. 20 - -9 REST API 设计 .................................................................................................................... 21 - -9.1 检查更新接口 ................................................................................................................... 21 - -9.2 获取下载链接接口 ............................................................................................................ 24 - -9.3 升级结果上报接口 ............................................................................................................ 25 - -9.4 设备身份签发接口(非用户登录) .................................................................................... 26 - -9.5 管理后台管理员登录接口(非本期普通客户端用户登录) ................................................. 27 - -9.6 管理后台接口摘要 ............................................................................................................ 28 - -10 数据库设计 ....................................................................................................................... 29 - -10.1 应用表 apps ................................................................................................................... 29 - -10.2 版本通道表 channels ...................................................................................................... 29 - -10.3 版本表 versions .............................................................................................................. 30 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -10.4 版本文件表 version_files ................................................................................................. 31 - -10.5 版本策略表 version_policies ............................................................................................ 32 - -10.6 设备表 devices ............................................................................................................... 33 - -10.7 授权表 licenses .............................................................................................................. 33 - -10.8 下载日志表 download_logs ............................................................................................. 34 - -10.9 升级日志表 upgrade_logs ................................................................................................ 35 - -10.10 管理员表 admin_users .................................................................................................. 35 - -10.11 管理员操作日志表 admin_audit_logs ............................................................................. 36 - -11 对象存储目录设计 ............................................................................................................. 37 - -12 Hash 校验策略 ................................................................................................................. 38 - -13 签名策略 .......................................................................................................................... 38 - -14 限流策略 .......................................................................................................................... 41 - -15 回滚机制 .......................................................................................................................... 42 - -16 Demo 开发里程碑 ............................................................................................................. 45 - -16.1 第一阶段:基础框架 ....................................................................................................... 45 - -16.2 第二阶段:服务端基础能力 ............................................................................................ 45 - -16.3 第三阶段:在线更新 ....................................................................................................... 45 - -16.4 第四阶段:版本策略 ....................................................................................................... 46 - -16.5 第五阶段:离线能力 ....................................................................................................... 46 - -16.6 第六阶段:可靠性与安全 ................................................................................................ 46 - -17 Demo 验收用例 ................................................................................................................ 46 - -18 后续扩展方向 ................................................................................................................... 47 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - - - MARSCO_内部 - -1 设计范围与登录边界 - -本设计文档用于说明软件自动升级与版本控制系统第一版 Demo - -的技术方案。设计范围包括客户端启动器、主程序启动校验、独立升 - -级器、版本策略、manifest、签名校验、下载授权、离线运行、回滚机 - -制、管理后台和基础服务端接口。 - -本 期 普 通 客 户 端 用 户 默 认 可 以 使 用 软 件 , 不 需 要 登 录 账 号 。 - -Launcher 启动检查、MainApp 启动校验、Updater 下载与替换、离线 - -启动、离线更新包导入、版本禁用和降级控制均不做普通用户登录校 - -验,只使用设备身份、版本策略、manifest、启动票据和签名校验。 - -本期客户端侧不实现普通用户登录。所有客户端请求使用设备身 - -份凭证进行识别,设备身份凭证不代表用户账号,也不产生用户会话。 - -后续版本可以增加普通用户登录能力,用于用户权限校验、用户 - -授权状态判断,以及判断该用户是否可以使用该软件。该能力属于后 - -续扩展,不纳入本期 Demo 必做范围。 - -后续如增加普通用户登录,建议放在 Launcher 启动检查阶段或 - -MainApp 首次进入业务前,用于用户级授权状态判断。该能力不替代 - -设备身份、版本策略、manifest、启动票据和签名校验。 - -管理后台管理员登录属于本期范围,仅用于保护后台版本发布、 - -策略修改、设备管理、离线包生成和日志查看等管理操作。该登录不 - -是普通客户端用户登录,普通客户端用户不调用管理后台管理员登录 - -接口,也不持有后台访问令牌。 -2 技术选型 -2.1 客户端技术栈 - -客户端采用 Qt + C++ 开发。 - -客户端程序包括: - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -a) Launcher.exe - -软件启动器。 - -b) MainApp.exe - -主业务程序,用于 Demo 演示主软件启动、版本显示和 DLL 加载。 - -c) Updater.exe - -独立升级器,负责下载、校验、替换、回滚和重启。 - -d) demo_plugin.dll - -Demo 插件 DLL,用于验证 DLL 替换能力。 - -e) UpdateHelper.exe - -可选辅助程序,用于后续处理 Updater 自身更新。 - -客户端推荐使用 Qt 模块: - -a) QNetworkAccessManager:HTTP API 请求和文件下载。 - -b) QJsonDocument:JSON 解析。 - -c) QFile、QDir:文件操作。 - -d) QCryptographicHash:SHA-256 hash 计算。 - -e) QProcess:启动和关闭外部进程。 - -f) QTimer:定时检查版本。 - -g) QLockFile:防止重复启动升级器。 - -h) QSaveFile:安全写入本地配置文件。 - -i) QSettings:本地配置管理。 - -j) QTemporaryDir:临时目录管理。 - -2.2 服务端技术栈 - -Demo 阶段建议使用: - -a) Python FastAPI。 - -b) SQLite。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -c) MinIO。 - -d) Redis 可选,用于限流。 - -正式阶段可选: - -a) Python FastAPI + PostgreSQL + Redis + MinIO / OSS。 - -b) Go + PostgreSQL + Redis + MinIO / OSS。 - -接口采用 HTTP REST JSON 方式约定。 - -2.3 文件存储 - -Demo 阶段使用 MinIO。 - -正式阶段可使用: - -a) 阿里云 OSS。 - -b) 腾讯云 COS。 - -c) 华为云 OBS。 - -d) AWS S3。 - -e) 私有化 MinIO。 - -f) 企业内网对象存储。 - -版本文件不建议直接存入数据库。数据库只存文件元数据、hash、 - -大小、存储路径和版本关系。 -3 总体架构 - -系统总体架构如下: - -客户端: - -a) Launcher.exe - -b) MainApp.exe - -c) Updater.exe - -d) 本地身份凭证 - -e) 本地版本策略缓存 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -f) 本地 manifest 缓存 - -g) staging 下载目录 - -h) backup 备份目录 - -i) - -logs 日志目录 - -服务端: - -a) API 服务。 - -b) 管理后台。 - -c) 数据库。 - -d) 对象存储。 - -e) Redis 限流模块。 - -f) 签名服务模块。 - -交互流程: - -a) 客户端请求服务端 API。 - -b) 服务端验证设备身份。 - -c) 服务端返回版本策略。 - -d) 客户端根据策略判断是否允许运行或升级。 - -e) 需要升级时,客户端请求下载链接。 - -f) 服务端生成短期对象存储下载 URL。 - -g) 客户端下载文件并校验。 - -h) 客户端替换文件并上报结果。 - -4 客户端目录结构 - -建议客户端目录如下: - -目录或文件 - -DemoApp/ - -Launcher.exe - -MainApp.exe - -用途 - -客户端安装根目录 - -默认启动入口,负责版本准入检查和启动票据生成 - -主业务程序 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -Updater.exe - -UpdateHelper.exe - -独立升级程序 - -可选辅助程序,用于处理 Updater 自身更新 - -plugins/demo_plugin.dll - -Demo 插件 DLL,用于验证 DLL 替换能力 - -bin/ - -config/app_config.json - -运行依赖目录 - -客户端基础配置 - -config/client_identity.dat - -本地设备身份凭证 - -config/version_policy.dat - -本地版本策略缓存 - -config/local_state.json - -本地运行状态和防回滚状态 - -update/manifest_cache/ - -manifest 缓存目录 - -update/staging/ - -update/backup/ - -update/logs/ - -data/ - -cache/ - -logs/ - -说明: - -更新文件下载临时目录 - -升级前备份目录 - -升级日志目录 - -用户数据目录,不纳入 hash 校验 - -缓存目录 - -运行日志目录 - -a) config/client_identity.dat:本地设备身份凭证。 - -b) config/version_policy.dat:本地版本策略。 - -c) update/manifest_cache:缓存 manifest。 - -d) update/staging:下载新文件的临时目录。 - -e) update/backup:升级前备份目录。 - -f) update/logs:升级日志。 - -g) plugins:插件 DLL 目录,需要进行白名单检查。 - -h) data:用户数据目录,不纳入 hash 校验。 - -i) - -logs:运行日志目录,不纳入 hash 校验。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -5 本地配置文件设计 -5.1 app_config.json - -字段 - -app_id - -app_name - -channel - -api_base_url - -platform - -arch - -current_version -5.2 client_identity.dat - -示例值 - -marsco_demo_app - -Marsco Demo App - -stable - -https://api.example.com/api/v1 - -windows - -x64 - -1.0.0 - -该文件由服务端签发,客户端只验证,不自行伪造。 - -字段 - -client_id - -device_id - -license_id - -app_id - -channel - -issued_at - -valid_until - -signature -5.3 version_policy.dat - -字段 - -app_id - -client_id - -示例值 - -client_xxx - -device_xxx - -license_xxx - -marsco_demo_app - -stable - -2026-06-23T00:00:00Z - -2026-12-31T00:00:00Z - -base64_signature - -示例值 - -marsco_demo_app - -client_xxx - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -1024 - -stable - -1.0.0 - -True - -False - -False - -True - -2026-06-23T00:00:00Z - -2026-07-23T00:00:00Z - -1.0.2 - -1.0.0 - -当前版本允许使用 - -base64_signature - -policy_seq - -channel - -current_version - -allow_run - -force_update - -allow_rollback - -offline_allowed - -issued_at - -valid_until - -latest_version - -min_supported_version - -message - -signature -5.4 local_state.json - -用于记录本地状态,防止策略回滚和系统时间回拨。 - -字段 - -max_policy_seq - -last_success_run_at - -last_online_verified_at - -last_success_version - -示例值 - -1024 - -2026-06-23T10:00:00Z - -2026-06-23T09:50:00Z - -1.0.0 - -离线时间和策略回滚判断规则: - -a) max_policy_seq 只能递增,不能被旧策略覆盖。 - -b) 新读取的 version_policy.policy_seq 小于 max_policy_seq 时,客户 - -端应拒绝启动。 - -c) 当前系统时间早于 last_success_run_at 时,视为疑似时间回拨。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -d) 当前系统时间早于 last_online_verified_at 时,视为疑似时间回拨。 - -e) 离线启动时必须同时满足:策略签名正确、policy_seq 未倒退、 - -offline_allowed 为 true、当前时间未超过 valid_until、未检测到 - -明显时间回拨。 - -f) 联网校验成功后更新 last_online_verified_at 和本地缓存策略。 - -g) 每次成功启动后更新 last_success_run_at。 - -6 Manifest 设计 - -manifest 必须是当前版本的全量受控文件清单。 - -字段 - -app_id - -version - -channel - -platform - -arch - -manifest_seq - -created_at - -files - -signature - -规则: - -示例值 - -marsco_demo_app - -1.0.2 - -stable - -windows - -x64 - -3001 - -2026-06-23T00:00:00Z - -[{"path": "Launcher.exe", "sha256": "aaa", "size": - -123456, "executable": true}, {"path": "MainApp.exe", - -"sha256": "bbb", "size": 223456, "executable": true}, - -{"path": "Updater.exe", "sha256": "ccc", "size": 323456, - -"executable": true}, {"path": "plugins/demo_plugin.dll", - -"sha256": "ddd", "size": 423456, "executable": false}] - -base64_signature - -a) manifest 中列出的文件必须存在。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -b) manifest 中列出的文件 hash 必须一致。 - -c) 受控目录下未在 manifest 中出现的 exe、dll 文件应视为异常。 - -d) manifest 必须由服务端私钥签名。 - -e) 客户端使用内置公钥验证 manifest。 - -f) manifest 应支持版本号、通道、平台、架构。 - -7 启动票据设计 - -Launcher 启动 MainApp 时生成短期启动票据。 - -推荐方式: - -a) Launcher 生成 ticket 文件。 - -b) ticket 文件存放到系统临时目录。 - -c) Launcher 启动 MainApp,并传入 ticket 文件路径。 - -d) MainApp 读取并验证 ticket。 - -e) MainApp 验证成功后删除 ticket 文件。 - -启动参数示例: - -项目 - -启动参数 - -示例 - -MainApp.exe --ticket- - -ticket 内容: - -字段 - -payload - -file=C:\Users\xxx\AppData\Local\Temp\marsco_ticket_xxx.json - -示例值 - -{"app_id": "marsco_demo_app", "client_id": - -"client_xxx", "device_id": "device_xxx", "version": - -"1.0.0", "nonce": "uuid_xxx", "issued_at": "2026-06- - -23T10:00:00Z", "expires_at": "2026-06-23T10:01:00Z"} - -signature - -base64_signature - -校验规则: - -a) 签名正确。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -b) 未过期。 - -c) app_id 一致。 - -d) client_id 一致。 - -e) device_id 一致。 - -f) version 一致。 - -g) nonce 未重复使用。 - -Demo 阶段可由 Launcher 生成本地签名票据。正式阶段需要区分 - -启动票据和服务端版本策略,真正的版本准入仍以服务端签名策略为 - -准。 - -启动票据安全边界: - -a) 启动票据只用于防止普通用户直接双击 MainApp 绕过 Launcher。 - -b) 启动票据不能替代 version_policy、manifest 和 client_identity 的 - -服务端签名校验。 - -c) 客户端不得保存服务端私钥。 - -d) Demo 阶段的本地票据签名密钥只用于本机进程间启动校验,不 - -用于签发版本策略、manifest 或设备身份。 - -e) 即使启动票据校验通过,MainApp 仍必须验证本地版本策略签名、 - -有效期、版本号和核心文件 hash。 - -8 客户端启动逻辑 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 8.1 Launcher 启动逻辑 - -MARSCO_内部 - -图 1 Launcher 启动准入流程 - -流程步骤: - -步骤 - -1 - -2 - -判断或处理 - -结果 - -启动 Launcher,读取 - -获得本地配置和设备身份 - -app_config 和 client_identity - -验证 client_identity 签名 - -身份无效时拒绝继续 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - 3 - -4 - -5 - -6 - -7 - -8 - -9 - -MARSCO_内部 - -尝试请求 /update/check - -联网成功时进入在线策略校验 - -验证服务端返回策略签名,更新 - -获得最新运行策略 - -version_policy - -联网失败时读取本地 - -进入离线策略校验 - -version_policy - -验证本地策略签名、 - -离线策略无效时拒绝启动 - -valid_until、offline_allowed - -检查本地核心文件 hash - -核心文件异常时进入更新或提示 - -判断是否允许运行 - -允许时生成启动票据并启动 - -MainApp - -不允许运行或强制升级 - -启动 Updater 或提示用户更新 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 8.2 MainApp 启动逻辑 - -MARSCO_内部 - -图 2 MainApp 启动校验流程 - -流程步骤: - -步骤 - -判断或处理 - -结果 - -1 - -2 - -3 - -启动 MainApp,检查启动参数中 - -无票据时提示通过启动器启动并 - -是否存在 ticket-file - -退出 - -读取 ticket - -获得启动票据内容 - -验证 ticket 签名和有效期 - -票据无效时拒绝启动 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - 4 - -5 - -6 - -7 - -8 - -MARSCO_内部 - -验证 version_policy - -策略无效时拒绝启动 - -验证当前版本是否允许运行 - -禁用版本或过期策略进入拦截 - -验证核心 DLL hash - -核心文件异常时拒绝启动 - -校验通过 - -进入主界面 - -主界面运行期间 - -启动定时器,定期检查更新 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -8.3 Updater 升级逻辑 - -图 3 Updater 升级与回滚流程 - -流程步骤: - -步骤 - -1 - -判断或处理 - -结果 - -启动 Updater,读取升级参数 - -获得目标版本和升级上下文 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - - 2 - -3 - -4 - -5 - -6 - -7 - -8 - -9 - -10 - -11 -9 REST API 设计 -9.1 检查更新接口 - -方法 - -POST - -接口: - -请求: - -字段 - -app_id - -client_id - -device_id - -MARSCO_内部 - -请求 /update/check,获取目标 - -获得版本清单 - -版本 manifest - -验证 manifest 签名 - -签名无效时停止升级 - -扫描本地文件并对比 manifest - -生成待下载文件列表 - -全量 hash - -请求临时下载 URL - -获得短期下载地址 - -下载文件到 staging 并校验 - -下载文件可用于替换 - -hash - -通知 MainApp 退出并等待完全退 - -进入文件替换窗口 - -出 - -备份旧文件到 backup - -形成可回滚状态 - -替换新文件并执行安装后全量校 - -校验通过后启动 MainApp - -验 - -上报升级成功 - -任一步骤失败 - -服务端记录升级结果 - -执行回滚并上报失败原因 - -路径 - -/api/v1/update/check - -示例值 - -marsco_demo_app - -client_xxx - -device_xxx - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 license_id - -current_version - -target_version - -channel - -platform - -arch - -operation - -返回: - -字段 - -allow_run - -action - -message - -current_version - -latest_version - -channel - -force_update - -allow_rollback - -policy_seq - -MARSCO_内部 - -license_xxx - -1.0.0 - -stable - -windows - -x64 - -check - -示例值 - -True - -optional_update - -发现新版本 - -1.0.0 - -1.0.2 - -stable - -False - -False - -1025 - -policy_valid_until - -2026-07-23T00:00:00Z - -manifest_xxx - -https://download.example.com/xxx - -base64_signature - -manifest_id - -manifest_url - -signature - -action 可选值: - -字段/取值 - -allow - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - MARSCO_内部 - -optional_update - -force_update - -blocked - -rollback_allowed - -rollback_denied - -license_invalid - -device_invalid - -policy_expired - -operation 可选值: - -字段/取值 - -check - -upgrade - -downgrade - -当 operation 为 downgrade 时,客户端必须传入 target_version。服务 - -端需要判断目标版本是否存在、是否属于当前通道、是否允许降级、 - -是否满足最低支持版本和数据兼容规则。 - -降级检查返回示例: - -字段 - -allow_run - -action - -message - -current_version - -latest_version - -target_version - -channel - -示例值 - -True - -rollback_allowed - -允许降级到 1.0.0 - -1.0.2 - -1.0.2 - -1.0.0 - -stable - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 force_update - -allow_rollback - -policy_seq - -MARSCO_内部 - -False - -True - -1030 - -policy_valid_until - -2026-07-23T00:00:00Z - -manifest_id - -manifest_url - -manifest_1_0_0 - -https://download.example.com/manifest-1.0.0.json - -signature -9.2 获取下载链接接口 - -base64_signature - -方法 - -POST - -接口: - -请求: - -字段 - -app_id - -client_id - -device_id - -license_id - -version - -channel - -files - -返回: - -字段 - -expires_in - -files - -路径 - -/api/v1/update/download-url - -示例值 - -marsco_demo_app - -client_xxx - -device_xxx - -license_xxx - -1.0.2 - -stable - -["MainApp.exe", "plugins/demo_plugin.dll"] - -示例值 - -600 - -[{"path": "MainApp.exe", "url": - -"https://oss.example.com/temp-url-1", "sha256": - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -"bbb", "size": 223456}, {"path": - -"plugins/demo_plugin.dll", "url": - -"https://oss.example.com/temp-url-2", "sha256": - -"ddd", "size": 423456}] - -9.3 升级结果上报接口 - -方法 - -POST - -接口: - -请求: - -字段 - -app_id - -client_id - -device_id - -license_id - -from_version - -to_version - -channel - -result - -error_code - -message - -started_at - -finished_at - -result 可选: - -字段/取值 - -success - -路径 - -/api/v1/update/report - -示例值 - -marsco_demo_app - -client_xxx - -device_xxx - -license_xxx - -1.0.0 - -1.0.2 - -stable - -success - -upgrade success - -2026-06-23T10:00:00Z - -2026-06-23T10:05:00Z - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - MARSCO_内部 - -download_failed - -hash_failed - -signature_failed - -replace_failed - -rollback_success - -rollback_failed - -permission_failed - -disk_space_not_enough - -mainapp_restart_failed -9.4 设备身份签发接口(非用户登录) - -该接口用于首次签发或刷新设备身份凭证,不是客户端用户登录 - -接口。 - -Demo 阶 段 可 不 暴 露 该 接 口 , 由 测 试 数 据 或 预 置 文 件 生 成 - -client_identity.dat。正式阶段如需启用,也应定位为设备激活/身份签 - -发能力,而不是用户名密码登录。 - -接口: - -方法 - -POST - -请求: - -字段 - -app_id - -activation_code - -device_fingerprint - -platform - -arch - -路径 - -/api/v1/device/activate - -示例值 - -marsco_demo_app - -XXXX-XXXX-XXXX - -device_hash_xxx - -windows - -x64 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 返回: - -字段 - -client_id - -device_id - -license_id - -channel - -identity_token - -MARSCO_内部 - -示例值 - -client_xxx - -device_xxx - -license_xxx - -stable - -{"valid_until": "2026-12-31T00:00:00Z", "signature": - -"base64_signature"} - -Demo 阶段可以简化为预置 client_id,不做复杂激活流程。 - -9.5 管理后台管理员登录接口(非本期普通客户端用户登录) - -该接口仅用于管理后台管理员登录,与本期普通客户端用户无关。 - -普通客户端用户在本期客户端不登录,不调用该接口,也不持有该接 - -口返回的后台访问令牌。 - -后续如增加普通用户登录,应单独设计客户端用户登录接口、用 - -户授权模型和用户权限校验逻辑,不复用管理后台管理员登录接口。 - -Demo 阶段可使用单管理员账号。正式阶段可扩展为多管理员、角 - -色权限、双因素认证或企业单点登录。 - -方法 - -POST - -接口: - -请求: - -字段 - -username - -password - -返回: - -路径 - -/api/v1/admin/login - -示例值 - -admin - -password - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -示例值 - -jwt_or_session_token - -Bearer - -7200 - -字段 - -access_token - -token_type - -expires_in - -要求: - -a) 密码不得明文存储。 - -b) 登录失败应记录审计日志。 - -c) 管理后台接口必须校验管理员 token。 - -d) Demo 阶段可不实现复杂角色权限,但必须区分未登录和已登录 - -管理员;该状态只存在于管理后台,不存在于本期普通客户端。 - -9.6 管理后台接口摘要 - -第一版 Demo 至少需要以下后台接口: - -方法 - -POST - -POST - -POST - -POST - -POST - -POST - -POST - -POST - -POST - -GET - -GET - -GET - -路径 - -/api/v1/admin/apps - -/api/v1/admin/channels - -/api/v1/admin/versions - -/api/v1/admin/versions/{version_id}/files - -/api/v1/admin/versions/{version_id}/publish - -/api/v1/admin/policies - -/api/v1/admin/policies/{policy_id}/force-update - -/api/v1/admin/policies/{policy_id}/block - -/api/v1/admin/policies/{policy_id}/rollback - -/api/v1/admin/download-logs - -/api/v1/admin/upgrade-logs - -/api/v1/admin/audit-logs - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -后台接口规则: - -a) 所有后台接口都需要管理员 token。 - -b) 发布版本时生成 manifest、计算文件 hash 并写入版本文件表。 - -c) 修改强制升级、禁用、降级策略时必须生成新的 policy_seq。 - -d) 每次关键操作必须写入管理员操作记录。 - -e) Demo 阶段可使用简化管理页面或 API 调试页面完成后台操作。 - -10 数据库设计 -10.1 应用表 apps - -字段: - -字段/取值 - -id - -app_id - -app_name - -description - -created_at - -updated_at -10.2 版本通道表 channels - -字段: - -字段/取值 - -id - -app_id - -channel_code - -channel_name - -enabled - -created_at - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -updated_at - -示例: - -字段/取值 - -stable / 正式版 - -preview / 预览版 - -internal / 内测版 - -customer_a / 客户专版 -10.3 版本表 versions - -字段: - -字段/取值 - -id - -app_id - -channel_code - -version - -version_name - -status - -is_latest - -release_notes - -offline_allowed - -offline_valid_days - -allow_rollback - -rollback_targets - -force_update - -created_at - -published_at - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -updated_at - -status 可选: - -字段/取值 - -draft - -published - -deprecated - -force_update - -blocked - -archived - -说明: - -a) allow_rollback 表示该版本是否允许作为降级目标。 - -b) rollback_targets 可为空;为空时表示不允许主动降级到其他版本。 - -c) 如果需要精确控制降级目标,rollback_targets 记录允许降级到的 - -版本列表,或拆分为独立版本关系表。 - -10.4 版本文件表 version_files - -字段: - -字段/取值 - -id - -version_id - -relative_path - -storage_key - -sha256 - -size - -file_type - -is_required - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -created_at -10.5 版本策略表 version_policies - -字段: - -字段/取值 - -id - -app_id - -channel_code - -version - -policy_seq - -allow_run - -force_update - -allow_rollback - -rollback_targets - -offline_allowed - -valid_until - -min_supported_version - -latest_version - -message - -created_at - -updated_at - -说明: - -a) 每次策略变更必须递增 policy_seq。 - -b) 客户端记录已见过的最大 policy_seq,检测到倒退时拒绝使用。 - -c) allow_rollback 表示当前策略是否允许降级。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -d) rollback_targets 用于限制允许降级的目标版本;未命中目标版本 - -时返回 rollback_denied。 - -10.6 设备表 devices - -字段: - -字段/取值 - -id - -client_id - -device_id - -license_id - -app_id - -channel_code - -status - -first_seen_at - -last_seen_at - -created_at - -updated_at - -status 可选: - -字段/取值 - -active - -disabled - -suspicious - -expired -10.7 授权表 licenses - -字段: - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -字段/取值 - -id - -license_id - -customer_name - -app_id - -channel_code - -max_devices - -valid_until - -status - -created_at - -updated_at -10.8 下载日志表 download_logs - -字段: - -字段/取值 - -id - -client_id - -device_id - -license_id - -app_id - -version - -channel_code - -file_path - -file_size - -ip - -user_agent - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -result - -created_at -10.9 升级日志表 upgrade_logs - -字段: - -字段/取值 - -id - -client_id - -device_id - -license_id - -app_id - -channel_code - -from_version - -to_version - -result - -error_code - -message - -started_at - -finished_at - -created_at -10.10 管理员表 admin_users - -字段: - -字段/取值 - -id - -username - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -password_hash - -status - -last_login_at - -created_at - -updated_at - -status 可选: - -字段/取值 - -active - -disabled - -规则: - -a) 密码只保存 hash,不保存明文。 - -b) Demo 阶段可预置一个管理员账号。 - -c) 正式阶段可扩展角色、权限、双因素认证和单点登录。 - -10.11 管理员操作日志表 admin_audit_logs - -字段: - -字段/取值 - -id - -admin_user_id - -action - -target_type - -target_id - -before_data - -after_data - -ip - -user_agent - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -created_at - -需要记录的关键操作: - -a) 登录成功和登录失败。 - -b) 创建或发布版本。 - -c) 上传版本文件。 - -d) 修改强制升级策略。 - -e) 禁用或恢复版本。 - -f) 修改降级策略。 - -g) 生成离线更新包。 - -h) 禁用或恢复设备。 - -11 对象存储目录设计 - -建议对象存储目录如下: - -对象路径 - -updates/{app_id}/ - -内容 - -应用根目录 - -updates/{app_id}/{channel}/ - -版本通道目录,例如 stable、 - -updates/{app_id}/{channel}/{version}/ - -preview - -单个版本目录 - -updates/{app_id}/{channel}/{version}/manifest.json - -当前版本 manifest - -updates/{app_id}/{channel}/{version}/manifest.sig - -manifest 签名 - -updates/{app_id}/{channel}/{version}/files/ - -版本文件目录 - -updates/{app_id}/{channel}/{version}/files/Launcher.exe - -Launcher 可执行文件 - -updates/{app_id}/{channel}/{version}/files/MainApp.exe - -主程序可执行文件 - -updates/{app_id}/{channel}/{version}/files/Updater.exe - -升级器可执行文件 - -updates/{app_id}/{channel}/{version}/files/plugins/demo_plugin.dll Demo 插件 DLL - -updates/{app_id}/{channel}/{version}/offline/ - -离线更新包目录 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -updates/{app_id}/{channel}/{version}/offline/{package}.upd - -离线更新包 - -规则: - -a) 已发布版本目录不可覆盖。 - -b) 如果要修复,应发布新版本号。 - -c) manifest 与 files 必须保持一致。 - -d) 离线包由同一套 manifest 生成。 - -e) 对象存储 bucket 正式环境应设置为私有。 - -12 Hash 校验策略 - -系统采用 SHA-256 作为文件 hash 算法。 - -规则: - -a) 服务端发布时计算所有受控文件 hash。 - -b) manifest 包含全量受控文件 hash。 - -c) 客户端升级前扫描本地文件。 - -d) 客户端只下载缺失或 hash 不一致的文件。 - -e) 下载完成后校验下载文件 hash。 - -f) 文件替换完成后执行全量 hash 校验。 - -g) 启动时执行核心文件 hash 校验。 - -h) 插件目录执行白名单检查。 - -结论: - -更新可以增量,校验必须全量。 - -13 签名策略 - -签名采用非对称签名机制。Demo 阶段建议使用 Ed25519;如果现 - -有技术栈不方便支持 Ed25519,可使用 RSA-2048 / SHA-256,但同一 - -套 Demo 中必须固定一种算法。 - -规则: - -a) 服务端保存私钥。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -b) 客户端内置公钥。 - -c) version_policy 必须签名。 - -d) manifest 必须签名。 - -e) client_identity 必须签名。 - -f) 离线更新包必须签名。 - -g) 客户端验证签名失败时拒绝启动或拒绝安装。 - -客户端不得保存服务端私钥。 - -签名数据规则: - -a) 被签名对象必须包含 signature_alg 和 key_id。 - -b) 计算签名前必须移除 signature 字段。 - -c) JSON 签名载荷必须使用稳定序列化规则:字段按字典序排序、无 - -多余空白、UTF-8 编码。 - -d) 时间字段统一使用 UTC ISO-8601 格式。 - -e) 客户端根据 key_id 选择内置公钥进行验签。 - -f) 公钥轮换时,客户端可同时内置当前公钥和上一个公钥。 - -g) 签名失败返回或记录 signature_failed。 - -version_policy 签名字段至少包括: - -字段/取值 - -app_id - -client_id - -policy_seq - -channel - -current_version - -allow_run - -force_update - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -allow_rollback - -rollback_targets - -offline_allowed - -issued_at - -valid_until - -latest_version - -min_supported_version - -message - -signature_alg - -key_id - -manifest 签名字段至少包括: - -字段/取值 - -app_id - -version - -channel - -platform - -arch - -manifest_seq - -created_at - -files - -signature_alg - -key_id - -client_identity 签名字段至少包括: - -字段/取值 - -client_id - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -device_id - -license_id - -app_id - -channel - -issued_at - -valid_until - -signature_alg - -key_id -14 限流策略 - -服务端需要支持多维度限流。 - -建议限流维度: - -a) client_id。 - -b) device_id。 - -c) license_id。 - -d) IP。 - -e) app_id。 - -f) version。 - -g) file_path。 - -建议限流规则: - -维度 - -client_id - -device_id - -license_id - -IP - -超限后返回: - -限制 - -10 分钟内最多请求 10 次下载链接 - -1 小时内最多下载 3 次完整包 - -24 小时内最多下载指定额度 - -10 分钟内最多请求 200 次 API - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -示例值 - -RATE_LIMITED - -请求过于频繁,请稍后再试。 - -字段 - -code - -message -15 回滚机制 - -升级失败时必须支持回滚。 - -流程: - -a) 下载文件到 staging。 - -b) 校验 hash。 - -c) 备份当前版本到 backup。 - -d) 替换文件。 - -e) 替换完成后执行全量校验。 - -f) 启动 MainApp 验证。 - -g) 如果失败,使用 backup 回滚。 - -h) 回滚后再次校验旧版本文件。 - -i) 上报回滚结果。 - -升级事务状态文件: - -Updater 应 在 update/ 目 录 下 维 护 事 务 状 态 文 件 , 例 如 - -update/upgrade_state.json。 - -字段: - -字段 - -transaction_id - -from_version - -to_version - -status - -manifest_id - -示例值 - -uuid_xxx - -1.0.0 - -1.0.1 - -prepared - -manifest_xxx - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -2026-06-23T10:00:00Z - -2026-06-23T10:01:00Z - -update/staging/uuid_xxx - -update/backup/uuid_xxx - -started_at - -updated_at - -staging_dir - -backup_dir - -error_code - -message - -事务状态: - -字段/取值 - -idle - -prepared - -downloading - -downloaded - -verified - -waiting_mainapp_exit - -backed_up - -replacing - -replaced - -post_verify - -committed - -rollback_required - -rolling_back - -rolled_back - -failed - -状态规则: - -a) 下载前进入 prepared。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 - - MARSCO_内部 - -b) 下载过程中进入 downloading。 - -c) 下载完成且 hash 校验通过后进入 verified。 - -d) MainApp 完全退出后才能进入 backed_up。 - -e) 备份完成后才能进入 replacing。 - -f) 替换完成后进入 post_verify 并执行全量 hash 校验。 - -g) 全量校验成功、MainApp 可启动后进入 committed。 - -h) 替换后校验失败、启动失败或异常中断时进入 rollback_required。 - -i) 回滚完成并校验旧版本成功后进入 rolled_back。 - -j) 回滚失败进入 failed,并提示用户重新安装或联系管理员。 - -异常恢复规则: - -a) Updater 启动时先读取 upgrade_state.json。 - -b) 如果状态为 downloading、downloaded 或 verified,可以清理 staging - -后重新下载。 - -c) 如果状态为 backed_up、replacing、replaced 或 post_verify,必须 - -优先尝试回滚。 - -d) 如果状态为 rollback_required 或 rolling_back,继续执行回滚。 - -e) 如果状态为 committed 或 rolled_back,可清理旧事务状态。 - -f) 清理 staging 和 backup 前必须确认事务已提交或已成功回滚。 - -需要处理的失败类型: - -a) 下载失败。 - -b) hash 失败。 - -c) 签名失败。 - -d) 文件占用。 - -e) 权限不足。 - -f) 磁盘空间不足。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -g) 替换失败。 - -h) 启动失败。 - -i) 回滚失败。 -16 Demo 开发里程碑 -16.1 第一阶段:基础框架 - -目标: - -a) 完成 Launcher、MainApp、Updater 三个 Qt 程序。 - -b) MainApp 能显示当前版本。 - -c) MainApp 能加载 demo_plugin.dll。 - -d) Launcher 能启动 MainApp。 - -e) MainApp 直接启动时能拒绝进入。 - -16.2 第二阶段:服务端基础能力 - -目标: - -a) 搭建 FastAPI 或 Go API。 - -b) 搭建 SQLite 数据库。 - -c) 搭建 MinIO。 - -d) 实现应用、版本、通道基础数据。 - -e) 实现 /update/check。 - -f) 实现 manifest 返回。 -16.3 第三阶段:在线更新 - -目标: - -a) Updater 下载 manifest。 - -b) Updater 校验 manifest 签名。 - -c) Updater 对比本地 hash。 - -d) Updater 下载差异文件。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -e) Updater 替换 MainApp 和 DLL。 - -f) Updater 自动重启 MainApp。 - -16.4 第四阶段:版本策略 - -目标: - -a) 支持强制升级。 - -b) 支持版本禁用。 - -c) 支持正式版和预览版。 - -d) 支持动态新增通道。 - -e) 支持允许降级和禁止降级。 - -16.5 第五阶段:离线能力 - -目标: - -a) 本地缓存 version_policy。 - -b) 支持离线有效期。 - -c) 离线凭证未过期时允许启动。 - -d) 离线凭证过期时禁止启动。 - -e) 支持离线更新包导入。 -16.6 第六阶段:可靠性与安全 - -目标: - -a) 支持升级失败回滚。 - -b) 支持 policy_seq 防回滚。 - -c) 支持系统时间回拨检测。 - -d) 支持插件白名单检查。 - -e) 支持下载限流。 - -f) 支持升级日志和下载日志。 - -17 Demo 验收用例 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -a) 通过 Launcher 正常启动 MainApp。 - -b) 直接启动 MainApp 被拒绝。 - -c) MainApp 显示主程序版本和 DLL 版本。 - -d) 后台发布 1.0.1。 - -e) 客户端从 1.0.0 升级到 1.0.1。 - -f) 升级后 DLL 版本变化。 - -g) 升级后 MainApp 自动重启。 - -h) 手动篡改 DLL 后启动失败。 - -i) 手动篡改 version_policy 后启动失败。 - -j) 后台设置 1.0.0 强制升级后客户端无法继续使用。 - -k) 后台禁用 1.0.0 后客户端拒绝启动。 - -l) 后台开放预览版后 preview 客户端获取 preview 更新。 - -m) 后台关闭降级通道后客户端无法降级。 - -n) 离线凭证未过期时可以启动。 - -o) 离线凭证过期后拒绝启动。 - -p) 模拟升级失败后成功回滚。 - -18 后续扩展方向 - -后续可扩展: - -a) 完整 License 系统。 - -b) 客户专版分发。 - -c) 灰度发布。 - -d) 多平台支持。 - -e) Windows 服务模式更新。 - -f) 管理员权限提权。 - -g) 代码签名证书。 - -h) 自动错误收集。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 MARSCO_内部 - -i) 客户端日志上传。 - -j) 增量二进制差分更新。 - -k) 多语言界面。 - -l) 企业内网镜像站点。 - -MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄MARSCO内部资料请勿外泄 \ No newline at end of file diff --git a/scripts/ReadMe.txt b/scripts/ReadMe.txt new file mode 100644 index 0000000..c1539d3 --- /dev/null +++ b/scripts/ReadMe.txt @@ -0,0 +1,33 @@ +客户端脚本说明 +============== + +本目录保存 update-client 的辅助脚本。项目根目录只保留源码、CMake 入口、Docs 和配置模板,脚本统一放在这里。 + +脚本列表: + +1. package-sdk.ps1 + 生成给其他软件接入用的 UpdateClientSDK 包。 + +2. package-client.ps1 + 生成某个具体产品的最终客户端发布包。 + +3. install-sdk.ps1 + 将 SDK 运行时复制到业务软件 Release 目录。 + +推荐在 update-client 根目录执行: + +```powershell +.\scripts\package-sdk.ps1 ` + -SourceDir .\out\bin ` + -OutputDir .\dist\UpdateClientSDK ` + -ZipFile .\dist\UpdateClientSDK.zip ` + -SdkVersion 0.1.0 +``` + +```powershell +.\scripts\package-client.ps1 ` + -SourceDir .\out\bin ` + -ConfigFile .\config\app_config.json ` + -OutputDir .\dist\UpdateClient ` + -ZipFile .\dist\UpdateClient.zip +``` diff --git a/install-sdk.ps1 b/scripts/install-sdk.ps1 similarity index 100% rename from install-sdk.ps1 rename to scripts/install-sdk.ps1 diff --git a/package-client.ps1 b/scripts/package-client.ps1 similarity index 91% rename from package-client.ps1 rename to scripts/package-client.ps1 index 943c6d8..3e0dec2 100644 --- a/package-client.ps1 +++ b/scripts/package-client.ps1 @@ -1,16 +1,27 @@ -param( - [string]$SourceDir = "$PSScriptRoot/out/bin", - [Parameter(Mandatory = $true)] - [string]$ConfigFile, - [string]$OutputDir = "$PSScriptRoot/dist/UpdateClient", - [string]$ZipFile = "$PSScriptRoot/dist/UpdateClient.zip" -) - -$ErrorActionPreference = "Stop" - -$source = (Resolve-Path $SourceDir).Path -$config = (Resolve-Path $ConfigFile).Path -$settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json +param( + [string]$SourceDir = "", + [Parameter(Mandatory = $true)] + [string]$ConfigFile, + [string]$OutputDir = "", + [string]$ZipFile = "" +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = Split-Path -Parent $PSScriptRoot +if ([string]::IsNullOrWhiteSpace($SourceDir)) { + $SourceDir = Join-Path $RepoRoot "out/bin" +} +if ([string]::IsNullOrWhiteSpace($OutputDir)) { + $OutputDir = Join-Path $RepoRoot "dist/UpdateClient" +} +if ([string]::IsNullOrWhiteSpace($ZipFile)) { + $ZipFile = Join-Path $RepoRoot "dist/UpdateClient.zip" +} + +$source = (Resolve-Path $SourceDir).Path +$config = (Resolve-Path $ConfigFile).Path +$settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json function Normalize-RelativePath([string]$PathValue) { return ($PathValue -replace '\\', '/').Trim('/') diff --git a/package-sdk.ps1 b/scripts/package-sdk.ps1 similarity index 77% rename from package-sdk.ps1 rename to scripts/package-sdk.ps1 index c71ae36..7a70ffb 100644 --- a/package-sdk.ps1 +++ b/scripts/package-sdk.ps1 @@ -1,17 +1,31 @@ -param( - [string]$SourceDir = "$PSScriptRoot/out/bin", - [string]$OutputDir = "$PSScriptRoot/dist/UpdateClientSDK", - [string]$ZipFile = "$PSScriptRoot/dist/UpdateClientSDK.zip", - [string]$SdkVersion = "0.1.0", - [string]$ExampleConfig = "$PSScriptRoot/config/app_config.example.json", - [switch]$IncludeDemoMainApp, - [switch]$IncludeQtRuntime -) - -$ErrorActionPreference = "Stop" - -$source = (Resolve-Path $SourceDir).Path -$exampleConfigPath = (Resolve-Path $ExampleConfig).Path +param( + [string]$SourceDir = "", + [string]$OutputDir = "", + [string]$ZipFile = "", + [string]$SdkVersion = "0.1.0", + [string]$ExampleConfig = "", + [switch]$IncludeDemoMainApp, + [switch]$IncludeQtRuntime +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = Split-Path -Parent $PSScriptRoot +if ([string]::IsNullOrWhiteSpace($SourceDir)) { + $SourceDir = Join-Path $RepoRoot "out/bin" +} +if ([string]::IsNullOrWhiteSpace($OutputDir)) { + $OutputDir = Join-Path $RepoRoot "dist/UpdateClientSDK" +} +if ([string]::IsNullOrWhiteSpace($ZipFile)) { + $ZipFile = Join-Path $RepoRoot "dist/UpdateClientSDK.zip" +} +if ([string]::IsNullOrWhiteSpace($ExampleConfig)) { + $ExampleConfig = Join-Path $RepoRoot "config/app_config.example.json" +} + +$source = (Resolve-Path $SourceDir).Path +$exampleConfigPath = (Resolve-Path $ExampleConfig).Path $requiredFiles = @("Launcher.exe", "Updater.exe", "Bootstrap.exe") foreach ($name in $requiredFiles) { @@ -21,11 +35,11 @@ foreach ($name in $requiredFiles) { } } -$publicKeyCandidates = @( - (Join-Path $source "config/manifest_public_key.pem"), - (Join-Path $source "manifest_public_key.pem"), - (Join-Path $PSScriptRoot "config/manifest_public_key.pem") -) +$publicKeyCandidates = @( + (Join-Path $source "config/manifest_public_key.pem"), + (Join-Path $source "manifest_public_key.pem"), + (Join-Path $RepoRoot "config/manifest_public_key.pem") +) $publicKey = $publicKeyCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $publicKey) { throw "manifest_public_key.pem is missing. Prepare the public key that matches the server signing private key." @@ -86,13 +100,16 @@ Get-ChildItem $source -Force | Where-Object { Copy-Item $exampleConfigPath (Join-Path $configDir "app_config.json") -Force Copy-Item $publicKey (Join-Path $configDir "manifest_public_key.pem") -Force -$wordGuideSource = Get-ChildItem $PSScriptRoot -File -Filter "*.docx" | Where-Object { - $_.Name -like "*SDK*.docx" -and $_.Name -notlike "~$*" -} | Sort-Object Name | Select-Object -First 1 -if (-not $wordGuideSource) { - throw "SDK integration Word guide is missing. Expected a *SDK*.docx file in the client directory." -} -Copy-Item $wordGuideSource.FullName (Join-Path $OutputDir $wordGuideSource.Name) -Force +$wordGuideSource = @($RepoRoot, (Join-Path $RepoRoot "Docs")) | + Where-Object { Test-Path $_ } | + ForEach-Object { Get-ChildItem $_ -File -Filter "*.docx" } | + Where-Object { $_.Name -like "*SDK*.docx" -and $_.Name -notlike "~$*" } | + Sort-Object Name | + Select-Object -First 1 +if (-not $wordGuideSource) { + throw "SDK integration Word guide is missing. Expected a *SDK*.docx file in the repository root or Docs directory." +} +Copy-Item $wordGuideSource.FullName (Join-Path $OutputDir $wordGuideSource.Name) -Force Copy-Item (Join-Path $PSScriptRoot "package-client.ps1") (Join-Path $scriptsDir "package-client.ps1") -Force Copy-Item (Join-Path $PSScriptRoot "package-sdk.ps1") (Join-Path $scriptsDir "package-sdk.ps1") -Force