// // Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team) // Copyright (C) 2021-2025 MaaAssistantArknights Contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License v3.0 only as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY // #nullable enable using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using MaaWpfGui.Constants; using MaaWpfGui.Constants.Enums; using MaaWpfGui.Extensions; using MaaWpfGui.Helper; using MaaWpfGui.Main; using MaaWpfGui.Models; using MaaWpfGui.Services; using MaaWpfGui.States; using MaaWpfGui.Utilities; using MaaWpfGui.ViewModels.UserControl.Settings; using Microsoft.VisualBasic.FileIO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Semver; using Serilog; using Stylet; using SearchOption = System.IO.SearchOption; namespace MaaWpfGui.ViewModels.UI; /// /// The view model of version update. /// public class VersionUpdateViewModel : Screen { private readonly RunningState _runningState; /// /// Initializes a new instance of the class. /// public VersionUpdateViewModel() { _runningState = RunningState.Instance; } private static readonly ILogger _logger = Log.ForContext(); private static string AddContributorLink(string text) { /* // "@ " -> "@ " // "`@`" -> "`@`" // "@MistEO" -> "[@MistEO](https://github.com/MistEO)" // "[@MistEO]" -> "[@MistEO]" */ return Regex.Replace(text, @"([^\[`]|^)@([^\s]+)", "$1[@$2](https://github.com/$2)"); } private readonly string _curVersion = Marshal.PtrToStringAnsi(MaaService.AsstGetVersion()) ?? "0.0.1"; private string _latestVersion = string.Empty; private string _updateTag = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionName, string.Empty); /// /// Gets or sets the update tag. /// public string UpdateTag { get => _updateTag; set { SetAndNotify(ref _updateTag, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionName, value); } } private string _updateInfo = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdateBody, string.Empty); // private static readonly MarkdownPipeline s_markdownPipeline = new MarkdownPipelineBuilder().UseXamlSupportedExtensions().Build(); /// /// Gets or sets the update info. /// public string UpdateInfo { get { try { return AddContributorLink(_updateInfo); } catch { return _updateInfo; } } set { SetAndNotify(ref _updateInfo, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdateBody, value); } } private string _updateUrl = string.Empty; /// /// Gets or sets the update URL. /// public string UpdateUrl { get => _updateUrl; set => SetAndNotify(ref _updateUrl, value); } private bool _isFirstBootAfterUpdate = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdateIsFirstBoot, bool.FalseString)); /// /// Gets or sets a value indicating whether it is the first boot after updating. /// public bool IsFirstBootAfterUpdate { get => _isFirstBootAfterUpdate; set { SetAndNotify(ref _isFirstBootAfterUpdate, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdateIsFirstBoot, value.ToString()); } } private string _updatePackageName = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdatePackage, string.Empty); /// /// Gets or sets the name of the update package. /// public string UpdatePackageName { get => _updatePackageName; set { SetAndNotify(ref _updatePackageName, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdatePackage, value); } } /// /// Gets the OS architecture. /// private static string OsArchitecture => RuntimeInformation.OSArchitecture.ToString().ToLower(); /// /// Gets a value indicating whether the OS is arm. /// public static bool IsArm => OsArchitecture.StartsWith("arm"); /* private const string RequestUrl = "repos/MaaAssistantArknights/MaaRelease/releases"; private const string StableRequestUrl = "repos/MaaAssistantArknights/MaaAssistantArknights/releases/latest"; private const string MaaReleaseRequestUrlByTag = "repos/MaaAssistantArknights/MaaRelease/releases/tags/"; private const string InfoRequestUrl = "repos/MaaAssistantArknights/MaaAssistantArknights/releases/tags/"; */ private const string MaaUpdateApi = "version/summary.json"; private JObject? _latestJson; private JObject? _assetsObject; private string? _mirrorcDownloadUrl; private string? _mirrorcVersionName; private string? _mirrorcReleaseNote; /// /// 检查是否有已下载的更新包 /// /// 操作成功返回 ,反之则返回 public bool CheckAndUpdateNow() { if (UpdateTag == string.Empty || UpdatePackageName == string.Empty || !File.Exists(UpdatePackageName)) { return false; } { using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionZipFileFoundTitle")); toast.AppendContentText(LocalizationHelper.GetString("NewVersionZipFileFoundDescDecompressing")) .AppendContentText(UpdateTag) .ShowUpdateVersion(row: 2); } string curDir = Directory.GetCurrentDirectory(); string extractDir = Path.Combine(curDir, "NewVersionExtract"); // 新版本解压的路径 string oldFileDir = Path.Combine(curDir, ".old"); // 解压 try { if (Directory.Exists(extractDir)) { Directory.Delete(extractDir, true); } ZipFile.ExtractToDirectory(UpdatePackageName, extractDir); } catch (InvalidDataException) { File.Delete(UpdatePackageName); { using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionZipFileBrokenTitle")); toast.AppendContentText(LocalizationHelper.GetString("NewVersionZipFileBrokenDescFilename") + UpdatePackageName) .AppendContentText(LocalizationHelper.GetString("NewVersionZipFileBrokenDescDeleted")) .ShowUpdateVersion(); } return false; } string removeListFile = Path.Combine(extractDir, "removelist.txt"); string mirrorChyanChangeFile = Path.Combine(extractDir, "changes.json"); bool isOTAPackage = File.Exists(removeListFile) || File.Exists(mirrorChyanChangeFile); string[] removeList = []; if (File.Exists(removeListFile)) { removeList = File.ReadAllLines(removeListFile); } if (File.Exists(mirrorChyanChangeFile)) { try { string json = File.ReadAllText(mirrorChyanChangeFile); var jObject = JObject.Parse(json); removeList = jObject["deleted"]?.ToObject() ?? []; } catch (Exception e) { _logger.Error("parse mirrorChyan changes.json error: {EMessage}", e.Message); } } if (removeList.Length > 0) { foreach (string file in removeList) { string path = Path.Combine(curDir, file); if (!File.Exists(path)) { continue; } string moveTo = Path.Combine(oldFileDir, file); if (File.Exists(moveTo)) { DeleteFileWithBackup(moveTo); } else { var dir = Path.GetDirectoryName(moveTo); if (dir != null) { Directory.CreateDirectory(dir); } } try { File.Move(path, moveTo); } catch (Exception e) { _logger.Error("move file error, path: {Path}, moveTo: {MoveTo}, error: {EMessage}", path, moveTo, e.Message); throw; } } } else if (!isOTAPackage) { List deleteTasks = []; foreach (var dir in Directory.GetDirectories(extractDir)) { deleteTasks.Add(Task.Run(() => { try { FileSystem.DeleteDirectory(dir.Replace(extractDir, curDir), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); } catch { _logger.Error("delete directory error, dir: {Dir}", dir); } })); } Task.WaitAll([.. deleteTasks]); } Directory.CreateDirectory(oldFileDir); foreach (var dir in Directory.GetDirectories(extractDir, "*", SearchOption.AllDirectories)) { Directory.CreateDirectory(dir.Replace(extractDir, curDir)); Directory.CreateDirectory(dir.Replace(extractDir, oldFileDir)); } // 复制新版本的所有文件到当前路径下 foreach (var file in Directory.GetFiles(extractDir, "*", SearchOption.AllDirectories)) { var fileName = Path.GetFileName(file); // ReSharper disable once StringLiteralTypo if (fileName == "removelist.txt") { continue; } string curFileName = file.Replace(extractDir, curDir); try { if (File.Exists(curFileName)) { string moveTo = file.Replace(extractDir, oldFileDir); if (File.Exists(moveTo)) { DeleteFileWithBackup(moveTo); } File.Move(curFileName, moveTo); } File.Move(file, curFileName); } catch (Exception e) { _logger.Error("move file error, file name: {File}, error: {EMessage}", file, e.Message); throw; } } // 操作完了,把解压的文件删了 Directory.Delete(extractDir, true); File.Delete(UpdatePackageName); // 保存更新信息,下次启动后会弹出已更新完成的提示 UpdatePackageName = string.Empty; IsFirstBootAfterUpdate = true; return true; static void DeleteFileWithBackup(string filePath) { try { File.Delete(filePath); } catch (Exception e) { _logger.Error("delete file error, filePath: {FilePath}, error: {EMessage}, try to backup.", filePath, e.Message); int index = 0; string currentDate = DateTime.Now.ToString("yyyyMMddHHmm"); string backupFilePath = $"{filePath}.{currentDate}.{index}"; while (File.Exists(backupFilePath)) { index++; backupFilePath = $"{filePath}.{currentDate}.{index}"; } try { File.Move(filePath, backupFilePath); } catch (Exception e1) { _logger.Error("move file error, path: {FilePath}, moveTo: {BackupFilePath}, error: {E1Message}", filePath, backupFilePath, e1.Message); throw; } } } } public enum CheckUpdateRetT { /// /// 操作成功 /// // ReSharper disable once InconsistentNaming OK, /// /// 未知错误 /// UnknownError, /// /// 无需更新 /// NoNeedToUpdate, /// /// 调试版本无需更新 /// NoNeedToUpdateDebugVersion, /// /// 已经是最新版 /// AlreadyLatest, /// /// 网络错误 /// NetworkError, /// /// 获取信息失败 /// FailedToGetInfo, /// /// 新版正在构建中 /// NewVersionIsBeingBuilt, /// /// 只更新了游戏资源 /// OnlyGameResourceUpdated, /// /// NoMirrorChyanCdk /// NoMirrorChyanCdk, } public enum AppUpdateSource { /// /// Maa API /// MaaApi, /// /// MirrorChyan /// MirrorChyan, } private bool _doNotShowUpdate = Convert.ToBoolean(ConfigurationHelper.GetGlobalValue(ConfigurationKeys.VersionUpdateDoNotShowUpdate, bool.FalseString)); /// /// Gets or sets a value indicating whether to show the update. /// public bool DoNotShowUpdate { get => _doNotShowUpdate; set { SetAndNotify(ref _doNotShowUpdate, value); ConfigurationHelper.SetGlobalValue(ConfigurationKeys.VersionUpdateDoNotShowUpdate, value.ToString()); } } /// /// 如果是在更新后第一次启动,显示ReleaseNote弹窗,否则检查更新并下载更新包。 /// /// Task public async Task ShowUpdateOrDownload() { if (IsFirstBootAfterUpdate) { IsFirstBootAfterUpdate = false; if (!DoNotShowUpdate) { Instances.WindowManager.ShowWindow(this); } } else { if (!SettingsViewModel.VersionUpdateSettings.StartupUpdateCheck) { return; } if (!IsDebugVersion()) { if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan" && string.IsNullOrEmpty(SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk)) { _ = Task.Run(() => MessageBoxHelper.Show( LocalizationHelper.GetString("MirrorChyanSelectedButNoCdk"), "cdk is empty!", MessageBoxButton.OK, MessageBoxImage.Warning, ok: LocalizationHelper.GetString("Ok"))); } await VersionUpdateAndAskToRestartAsync(); await ResourceUpdater.ResourceUpdateAndReloadAsync(); } else { // await ResourceUpdater.CheckAndDownloadResourceUpdate(); // 跑个空任务避免 async warning await Task.Run(() => { }); } } } /// /// 检查更新并下载更新包,如果成功则提示重启。 /// /// Task public async Task VersionUpdateAndAskToRestartAsync() { if (SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates) { return; } var ret = await CheckAndDownloadVersionUpdate(); if (ret == CheckUpdateRetT.OK) { _ = AskToRestart(); } } /// /// 检查更新,并下载更新包。 /// /// 操作成功返回 ,反之则返回 public async Task CheckAndDownloadVersionUpdate() { try { SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = true; var (checkRet, source) = await CheckUpdate(); if (checkRet != CheckUpdateRetT.OK) { return checkRet; } return source switch { AppUpdateSource.MaaApi => await HandleUpdateFromMaaApi(), AppUpdateSource.MirrorChyan => await HandleUpdateFromMirrorChyan(), _ => CheckUpdateRetT.UnknownError, }; } finally { SettingsViewModel.VersionUpdateSettings.IsCheckingForUpdates = false; } } private async Task HandleUpdateFromMaaApi() { // 保存新版本的信息 var name = _latestJson?["name"]?.ToString(); UpdateTag = string.IsNullOrEmpty(name) ? (_latestJson?["tag_name"]?.ToString() ?? string.Empty) : name; SettingsViewModel.VersionUpdateSettings.NewVersionFoundInfo = $"{LocalizationHelper.GetString("NewVersionFoundTitle")}: {UpdateTag}"; var body = _latestJson?["body"]?.ToString() ?? string.Empty; if (string.IsNullOrEmpty(body)) { var curHash = ComparableHash(_curVersion); var latestHash = ComparableHash(_latestVersion); if (curHash != null && latestHash != null) { body = $"**Full Changelog**: [{curHash} -> {latestHash}](https://github.com/MaaAssistantArknights/MaaAssistantArknights/compare/{curHash}...{latestHash})"; } } UpdateInfo = body; UpdateUrl = _latestJson?["html_url"]?.ToString() ?? string.Empty; bool otaFound = _assetsObject != null; bool goDownload = otaFound && SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage; ShowUpdateInfo(otaFound, LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"), true); UpdatePackageName = _assetsObject?["name"]?.ToString() ?? string.Empty; if (!goDownload || string.IsNullOrWhiteSpace(UpdatePackageName)) { OutputDownloadProgress(string.Empty); return CheckUpdateRetT.NoNeedToUpdate; } if (_assetsObject == null) { return CheckUpdateRetT.FailedToGetInfo; } string? rawUrl = _assetsObject["browser_download_url"]?.ToString(); var urls = new List(); if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "Github" && !SettingsViewModel.VersionUpdateSettings.ForceGithubGlobalSource) { var mirrors = _assetsObject["mirrors"]?.ToObject>(); if (mirrors != null) { urls.AddRange(mirrors); } } // 负载均衡 // var rand = new Random(); // urls = urls.OrderBy(_ => rand.Next()).ToList(); if (rawUrl != null) { urls.Add(rawUrl); } _logger.Information("Start test legacy download urls"); // run latency test parallel var tasks = urls.ConvertAll(url => Instances.HttpService.HeadAsync(new Uri(url))); var latencies = await Task.WhenAll(tasks); var proxy = ConfigurationHelper.GetValue(ConfigurationKeys.UpdateProxy, string.Empty); var hasProxy = !string.IsNullOrEmpty(proxy); // select the fastest mirror _logger.Information("Selecting the fastest mirror:"); var selected = 0; for (int i = 0; i < latencies.Length; i++) { // ReSharper disable once StringLiteralTypo var isInChina = urls[i].Contains("s3.maa-org.net") || urls[i].Contains("maa-ota.annangela.cn"); if (latencies[i] < 0) { _logger.Warning("\turl: {CDNUrl} not available", urls[i]); continue; } _logger.Information("\turl: {CDNUrl}, legacy: {1:0.00}ms", urls[i], latencies[i]); if (hasProxy && isInChina) { // 如果设置了代理,国内镜像的延迟加上一个固定值 latencies[i] += 6480; } if (latencies[selected] < 0 || (latencies[i] >= 0 && latencies[i] < latencies[selected])) { selected = i; } } if (latencies[selected] < 0) { _logger.Error("All mirrors are not available"); OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle")); return CheckUpdateRetT.NetworkError; } _logger.Information("Selected mirror: {CDNUrl}", urls[selected]); var downloaded = await DownloadGithubAssets(urls[selected], _assetsObject); if (downloaded) { OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadCompletedTitle")); } else { OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle")); { using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle")); toast.AppendContentText(LocalizationHelper.GetString("NewVersionDownloadFailedDesc")) .AddButton(LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"), ToastNotification.GetActionTagForOpenWeb(UpdateUrl)) .Show(); } return CheckUpdateRetT.NoNeedToUpdate; } return CheckUpdateRetT.OK; string? ComparableHash(string version) { if (IsStdVersion(version) || IsBetaVersion(version)) { return version; } if (!SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out var semVersion) || !IsNightlyVersion(semVersion)) { return null; } // v4.6.6-1.g{Hash} // v4.6.7-beta.2.8.g{Hash} var commitHash = semVersion.PrereleaseIdentifiers[^1].ToString(); if (commitHash.StartsWith('g')) { commitHash = commitHash.Remove(0, 1); } return commitHash; } } private void ShowUpdateInfo(bool otaFound, string? text, bool globalSource) { bool goDownload = otaFound && SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage; using var toast = new ToastNotification((otaFound ? LocalizationHelper.GetString("NewVersionFoundTitle") : LocalizationHelper.GetString("NewVersionFoundButNoPackageTitle")) + " : " + UpdateTag); if (goDownload) { OutputDownloadProgress(LocalizationHelper.GetString("NewVersionDownloadPreparing"), false, globalSource); toast.AppendContentText(globalSource ? LocalizationHelper.GetString("NewVersionFoundDescDownloadingWithGlobalSource") : LocalizationHelper.GetString("NewVersionFoundDescDownloadingWithMirrorChyan")); } if (!otaFound) { toast.AppendContentText(LocalizationHelper.GetString("NewVersionFoundButNoPackageDesc")); } int count = 0; foreach (var line in UpdateInfo.Split('\n')) { if (line.StartsWith('#') || string.IsNullOrWhiteSpace(line)) { continue; } toast.AppendContentText(line); if (++count >= 10) { break; } } if (!string.IsNullOrEmpty(text)) { toast.AddButton(text, ToastNotification.GetActionTagForOpenWeb(globalSource ? UpdateUrl : MaaUrls.MirrorChyanManualUpdate)); } toast.ShowUpdateVersion(); } private async Task HandleUpdateFromMirrorChyan() { if (string.IsNullOrEmpty(_mirrorcDownloadUrl)) { return CheckUpdateRetT.FailedToGetInfo; } UpdateTag = _mirrorcVersionName ?? string.Empty; UpdateInfo = _mirrorcReleaseNote ?? string.Empty; SettingsViewModel.VersionUpdateSettings.NewVersionFoundInfo = $"{LocalizationHelper.GetString("NewVersionFoundTitle")}: {UpdateTag}"; bool goDownload = SettingsViewModel.VersionUpdateSettings.AutoDownloadUpdatePackage; ShowUpdateInfo(true, LocalizationHelper.GetString("NewVersionFoundButtonGoWebpage"), false); if (!goDownload) { OutputDownloadProgress(string.Empty); return CheckUpdateRetT.NoNeedToUpdate; } UpdatePackageName = "MirrorChyanApp" + _mirrorcVersionName + ".zip"; var downloaded = await DownloadFromMirrorChyan(_mirrorcDownloadUrl, UpdatePackageName); if (downloaded) { OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadCompletedTitle")); } else { OutputDownloadProgress(downloading: false, output: LocalizationHelper.GetString("NewVersionDownloadFailedTitle")); { using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionDownloadFailedTitle")); toast.AppendContentText(LocalizationHelper.GetString("NewVersionDownloadFailedDesc")) .Show(); } return CheckUpdateRetT.NoNeedToUpdate; } AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanFirstUse); return CheckUpdateRetT.OK; } public async Task AskToRestart() { if (SettingsViewModel.VersionUpdateSettings.AutoInstallUpdatePackage) { await Bootstrapper.RestartAfterIdleAsync(); return; } await _runningState.UntilIdleAsync(10000); var result = MessageBoxHelper.Show( LocalizationHelper.GetString("NewVersionDownloadCompletedDesc"), LocalizationHelper.GetString("NewVersionDownloadCompletedTitle"), MessageBoxButton.OKCancel, MessageBoxImage.Question, ok: LocalizationHelper.GetString("Ok"), cancel: LocalizationHelper.GetString("ManualRestart")); if (result == MessageBoxResult.OK) { Bootstrapper.ShutdownAndRestartWithoutArgs(); } } /// /// 检查更新。 /// /// 检查到更新返回 ,反之则返回 private async Task<(CheckUpdateRetT Ret, AppUpdateSource? Source)> CheckUpdate() { // 调试版不检查更新 if (IsDebugVersion()) { return (CheckUpdateRetT.NoNeedToUpdateDebugVersion, null); } if (SettingsViewModel.VersionUpdateSettings.UpdateSource == "MirrorChyan") { try { var ret = await CheckUpdateByMirrorChyan(); if (ret is CheckUpdateRetT.OK or CheckUpdateRetT.AlreadyLatest) { return (ret, AppUpdateSource.MirrorChyan); } } catch (Exception ex) { _logger.Error(ex, "Failed to check update by MirrorChyan, rollback to maaApi"); } } try { var ret = await CheckUpdateByMaaApi(); return (ret, AppUpdateSource.MaaApi); } catch (Exception ex) { _logger.Error(ex, "Failed to check update by Maa API."); return (CheckUpdateRetT.FailedToGetInfo, AppUpdateSource.MaaApi); } } private async Task CheckUpdateByMaaApi() { JObject json = await Instances.MaaApiService.RequestMaaApiWithCache(MaaUpdateApi); if (json is null) { _logger.Error("Failed to get update info from Maa API."); return CheckUpdateRetT.FailedToGetInfo; } string versionType = SettingsViewModel.VersionUpdateSettings.VersionType switch { VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta => "beta", VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly => "alpha", _ => "stable", }; var latestVersion = json[versionType]?["version"]?.ToString(); latestVersion ??= string.Empty; if (!NeedToUpdate(latestVersion)) { return CheckUpdateRetT.AlreadyLatest; } return await GetVersionDetailsByMaaApi(versionType); } private async Task GetVersionDetailsByMaaApi(string versionType) { var json = await Instances.MaaApiService.RequestMaaApiWithCache($"version/{versionType}.json"); if (json is null) { return CheckUpdateRetT.FailedToGetInfo; } string? latestVersion = json["version"]?.ToString(); if (string.IsNullOrEmpty(latestVersion)) { return CheckUpdateRetT.FailedToGetInfo; } if (!NeedToUpdate(latestVersion)) { return CheckUpdateRetT.AlreadyLatest; } _latestVersion = latestVersion; _latestJson = json["details"] as JObject; if (_latestJson == null) { return CheckUpdateRetT.FailedToGetInfo; } _assetsObject = null; JObject? fullPackage = null; var curVersionLower = _curVersion.ToLower(); var latestVersionLower = _latestVersion.ToLower(); foreach (var curAssets in ((JArray?)_latestJson["assets"])!) { string? name = curAssets["name"]?.ToString().ToLower(); if (name == null) { continue; } if (IsArm ^ name.Contains("arm")) { continue; } if (!name.Contains("win")) { continue; } if (name.Contains($"maa-{latestVersionLower}-")) { fullPackage = curAssets as JObject; } // ReSharper disable once InvertIf if (name.Contains("ota") && name.Contains($"{curVersionLower}_{latestVersionLower}")) { _assetsObject = curAssets as JObject; break; } } if (_assetsObject == null && fullPackage != null) { _assetsObject = fullPackage; _logger.Warning("No OTA package found, but full package found."); using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionNoOtaPackage")); toast.Show(30); Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NewVersionNoOtaPackage"), UiLogColor.Warning); } return CheckUpdateRetT.OK; } private async Task CheckUpdateByMirrorChyan() { var cdk = SettingsViewModel.VersionUpdateSettings.MirrorChyanCdk.Trim(); var arch = IsArm ? "arm64" : "x64"; string channel = SettingsViewModel.VersionUpdateSettings.VersionType switch { VersionUpdateSettingsUserControlModel.UpdateVersionType.Beta => "beta", VersionUpdateSettingsUserControlModel.UpdateVersionType.Nightly => "alpha", _ => "stable", }; var spid = HardwareInfoUtility.GetMachineGuid().StableHash(); var url = $"{MaaUrls.MirrorChyanAppUpdate}?current_version={_curVersion}&cdk={cdk}&user_agent=MaaWpfGui&os=win&arch={arch}&channel={channel}&sp_id={spid}"; HttpResponseMessage? response = null; try { response = await Instances.HttpService.GetAsync(new(url), uriPartial: UriPartial.Path); } catch (Exception e) { _logger.Error(e, "Failed to send GET request to {Uri}", new Uri(url).GetLeftPart(UriPartial.Path)); _logger.Information("current_version: {CurVersion}, cdk: {Mask}, arch: {Arch}, channel: {Channel}", _curVersion, cdk.Mask(), arch, channel); } if (response is null) { _logger.Error("mirrorc failed"); return CheckUpdateRetT.NetworkError; } var jsonStr = await response.Content.ReadAsStringAsync(); _logger.Information("{JsonStr}", jsonStr); JObject? data = null; try { data = (JObject?)JsonConvert.DeserializeObject(jsonStr); } catch (Exception ex) { _logger.Error(ex, "Failed to deserialize json"); } if (data is null) { return CheckUpdateRetT.UnknownError; } var mirrorChyanCdkExpired = data["data"]?["cdk_expired_time"]?.ToObject(); if (mirrorChyanCdkExpired.HasValue) { SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkExpiredTime = mirrorChyanCdkExpired.Value; SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = false; } else { SettingsViewModel.VersionUpdateSettings.MirrorChyanCdkFetchFailed = true; } var errorCode = data["code"]?.ToObject() ?? MirrorChyanErrorCode.Undivided; if (errorCode != MirrorChyanErrorCode.Success) { switch (errorCode) { case MirrorChyanErrorCode.KeyExpired: ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkExpired")); break; case MirrorChyanErrorCode.KeyInvalid: ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkInvalid")); AchievementTrackerHelper.Instance.Unlock(AchievementIds.MirrorChyanCdkError); break; case MirrorChyanErrorCode.ResourceQuotaExhausted: ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkQuotaExhausted")); break; case MirrorChyanErrorCode.KeyMismatched: ToastNotification.ShowDirect(LocalizationHelper.GetString("MirrorChyanCdkMismatched")); break; case MirrorChyanErrorCode.InvalidParams: case MirrorChyanErrorCode.ResourceNotFound: case MirrorChyanErrorCode.InvalidOs: case MirrorChyanErrorCode.InvalidArch: case MirrorChyanErrorCode.InvalidChannel: case MirrorChyanErrorCode.Undivided: ToastNotification.ShowDirect(data["msg"]?.ToString() ?? LocalizationHelper.GetString("GameResourceFailed")); break; } return CheckUpdateRetT.UnknownError; } var version = data["data"]?["version_name"]?.ToString(); if (string.IsNullOrEmpty(version)) { return CheckUpdateRetT.UnknownError; } if (!NeedToUpdate(version)) { return CheckUpdateRetT.AlreadyLatest; } if (data["data"]?["update_type"]?.ToObject() == "full") { using var toast = new ToastNotification(LocalizationHelper.GetString("NewVersionNoOtaPackage")); toast.Show(30); _logger.Warning("No OTA package found, but full package found."); Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("NewVersionNoOtaPackage"), UiLogColor.Warning); } // 到这里已经确定有新版本了 _logger.Information("New version found: {Version}", version); _mirrorcVersionName = version; _mirrorcReleaseNote = data["data"]?["release_note"]?.ToString(); if (string.IsNullOrEmpty(cdk)) { return CheckUpdateRetT.NoMirrorChyanCdk; } _mirrorcDownloadUrl = data["data"]?["url"]?.ToString(); return CheckUpdateRetT.OK; } private bool NeedToUpdate(string latestVersion) { if (IsDebugVersion()) { return false; } bool curParsed = SemVersion.TryParse(_curVersion, SemVersionStyles.AllowLowerV, out var curVersionObj); bool latestPared = SemVersion.TryParse(latestVersion, SemVersionStyles.AllowLowerV, out var latestVersionObj); if (curParsed && latestPared && curVersionObj != null && latestVersionObj != null) { return curVersionObj.CompareSortOrderTo(latestVersionObj) < 0; } return string.CompareOrdinal(_curVersion, latestVersion) < 0; } /// /// 获取 GitHub Assets 对象对应的文件 /// /// 下载链接 /// Github Assets 对象 /// 操作成功返回 true,反之则返回 false private static async Task DownloadGithubAssets(string url, JObject assetsObject) { try { return await Instances.HttpService.DownloadFileAsync( new(url), assetsObject["name"]!.ToString(), assetsObject["content_type"]?.ToString()) .ConfigureAwait(false); } catch (Exception) { return false; } } private static async Task DownloadFromMirrorChyan(string url, string filename) { try { return await Instances.HttpService.DownloadFileAsync( new(url), filename) .ConfigureAwait(false); } catch (Exception) { return false; } } private static ObservableCollection? _logItemViewModels; public static void OutputDownloadProgress(long value = 0, long maximum = 1, int len = 0, double ts = 1) { string progress = $"[{value / 1048576.0:F}MiB/{maximum / 1048576.0:F}MiB ({value * 100.0 / maximum:F}%)"; double speedInKiBPerSecond = len / ts / 1024.0; var speedDisplay = speedInKiBPerSecond >= 1024 ? $"{speedInKiBPerSecond / 1024.0:F} MiB/s" : $"{speedInKiBPerSecond:F} KiB/s"; OutputDownloadProgress(progress + $" {speedDisplay}"); } private static bool _globalSource = true; public static void OutputDownloadProgress(string output, bool downloading = true, bool? globalSource = null) { globalSource ??= _globalSource; _globalSource = globalSource.Value; if (_logItemViewModels == null) { try { _logItemViewModels = Instances.TaskQueueViewModel.LogItemViewModels; } catch { return; } if (_logItemViewModels == null) { return; } } string fullText; if (downloading) { string key = globalSource.Value ? "NewVersionFoundDescDownloadingWithGlobalSource" : "NewVersionFoundDescDownloadingWithMirrorChyan"; fullText = LocalizationHelper.GetString(key) + "\n" + output; } else { fullText = output; } var log = new LogItemViewModel(fullText, UiLogColor.Download); Execute.OnUIThread(() => { if (_logItemViewModels.Count > 0 && _logItemViewModels[0].Color == UiLogColor.Download) { if (!string.IsNullOrEmpty(output)) { _logItemViewModels[0] = log; } else { _logItemViewModels.RemoveAt(0); } } else if (!string.IsNullOrEmpty(output)) { _logItemViewModels.Clear(); _logItemViewModels.Add(log); } }); } public bool IsDebugVersion(string? version = null) { // return false; version ??= _curVersion; // match case 1: DEBUG VERSION // match case 2: v{Major}.{Minor}.{Patch}-{CommitDistance}-g{CommitHash} // match case 3: {CommitHash} return Regex.IsMatch(version, @"^(.*DEBUG.*|v\d+(\.\d+){1,3}-\d+-g[0-9a-f]{6,}|[^v][0-9a-f]{6,})$"); } public bool IsStdVersion(string? version = null) { // 正式版:vX.X.X // DevBuild (CI):yyyy-MM-dd-HH-mm-ss-{CommitHash[..7]} // DevBuild (Local):yyyy-MM-dd-HH-mm-ss-{CommitHash[..7]}-Local // Release (Local Commit):v.{CommitHash[..7]}-Local // Release (Local Tag):{Tag}-Local // Debug (Local):DEBUG VERSION // Script Compiled:c{CommitHash[..7]} version ??= _curVersion; if (IsDebugVersion(version)) { return false; } if (version.StartsWith('c') || version.StartsWith("20") || version.Contains("Local")) { return false; } if (!SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out var semVersion)) { return false; } return !semVersion.IsPrerelease; } public bool IsBetaVersion(string? version = null) { version ??= _curVersion; if (IsDebugVersion(version)) { return false; } if (version.StartsWith('c') || version.StartsWith("20") || version.Contains("Local")) { return false; } if (!SemVersion.TryParse(version, SemVersionStyles.AllowLowerV, out var semVersion)) { return false; } return semVersion.IsPrerelease && !IsNightlyVersion(semVersion); } public static bool IsNightlyVersion(SemVersion version) { if (!version.IsPrerelease) { return false; } // ReSharper disable once CommentTypo // v{Major}.{Minor}.{Patch}-{Prerelease}.{CommitDistance}.g{CommitHash} // v4.6.7-beta.2.1.g1234567 // v4.6.8-5.g1234567 var lastId = version.PrereleaseIdentifiers.LastOrDefault().ToString(); return lastId.StartsWith('g') && lastId.Length >= 7; } }