Files
community/Ink Canvas/Helpers/AutoUpdateHelper.cs
T
2025-08-25 09:00:45 +08:00

1634 lines
84 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers
{
internal class AutoUpdateHelper
{
// 定义超时时间为10秒
private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(10);
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static string statusFilePath;
// 线路组结构体(包含版本、下载、日志地址)
public class UpdateLineGroup
{
public string GroupName { get; set; } // 组名
public string VersionUrl { get; set; } // 版本检测地址
public string DownloadUrlFormat { get; set; } // 下载地址格式(带{0}占位符)
public string LogUrl { get; set; } // 更新日志地址
}
// 通道-线路组映射
public static readonly Dictionary<UpdateChannel, List<UpdateLineGroup>> ChannelLineGroups = new Dictionary<UpdateChannel, List<UpdateLineGroup>>
{
{ UpdateChannel.Release, new List<UpdateLineGroup>
{
new UpdateLineGroup
{
GroupName = "GitHub主线",
VersionUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "bgithub备用",
VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "kkgithub线路",
VersionUrl = "https://kkgithub.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://kkgithub.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://kkgithub.com/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community/InkCanvasForClass.CE.{0}.zip",
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Release/InkCanvasForClass.CE.{0}.zip",
}
}
},
{ UpdateChannel.Beta, new List<UpdateLineGroup>
{
new UpdateLineGroup
{
GroupName = "GitHub主线",
VersionUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "bgithub备用",
VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "kkgithub线路",
VersionUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://kkgithub.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community-beta/InkCanvasForClass.CE.{0}.zip",
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Beta/InkCanvasForClass.CE.{0}.zip",
}
}
}
};
// 区块任务结构体(移到类体内)
private class BlockTask
{
public int Index;
public long Start;
public long End;
public int RetryCount;
}
// 检测URL延迟
private static async Task<long> GetUrlDelay(string url)
{
try
{
// 检测是否为Windows 7
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (isWindows7)
{
// Windows 7使用特殊配置
using (var handler = new HttpClientHandler())
{
// 配置HttpClientHandler以支持Windows 7
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient(handler))
{
client.Timeout = TimeSpan.FromSeconds(5);
var sw = Stopwatch.StartNew();
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
sw.Stop();
if (resp.IsSuccessStatusCode)
return sw.ElapsedMilliseconds;
}
}
}
else
{
// 其他Windows版本使用标准配置
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(5);
var sw = Stopwatch.StartNew();
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
sw.Stop();
if (resp.IsSuccessStatusCode)
return sw.ElapsedMilliseconds;
}
}
}
catch { }
return -1;
}
// 检测线路组延迟,返回最快组(保持向后兼容)
private static async Task<UpdateLineGroup> GetFastestLineGroup(UpdateChannel channel)
{
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
return availableGroups.Count > 0 ? availableGroups[0] : null;
}
// 获取所有可用线路组,按延迟排序
public static async Task<List<UpdateLineGroup>> GetAvailableLineGroupsOrdered(UpdateChannel channel)
{
var groups = ChannelLineGroups[channel];
var availableGroups = new List<(UpdateLineGroup group, long delay)>();
LogHelper.WriteLogToFile($"AutoUpdate | 开始检测通道 {channel} 下所有线路组延迟...");
foreach (var group in groups)
{
// 跳过"智教联盟"和"inkeys"线路组,不参与延迟检测和排序
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 跳过{group.GroupName}线路组延迟检测");
continue;
}
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})");
var delay = await GetUrlDelay(group.VersionUrl);
if (delay >= 0)
{
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 延迟: {delay}ms");
availableGroups.Add((group, delay));
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 不可用", LogHelper.LogType.Warning);
}
}
// 按延迟排序,延迟最小的排在前面
var orderedGroups = availableGroups
.OrderBy(x => x.delay)
.Select(x => x.group)
.ToList();
// 将"智教联盟"线路组插入到最前面(如果存在)
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
if (zhiJiaoGroup != null)
{
orderedGroups.Insert(0, zhiJiaoGroup);
LogHelper.WriteLogToFile("AutoUpdate | 智教联盟线路组已插入到首位");
}
// 将"inkeys"线路组插入到第二位(如果存在)
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
if (inkeysGroup != null)
{
orderedGroups.Insert(1, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已插入到第二位");
}
if (orderedGroups.Count > 0)
{
LogHelper.WriteLogToFile($"AutoUpdate | 找到 {orderedGroups.Count} 个可用线路组,按延迟排序:");
for (int i = 0; i < orderedGroups.Count; i++)
{
LogHelper.WriteLogToFile($"AutoUpdate | {i + 1}. {orderedGroups[i].GroupName}");
}
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
}
return orderedGroups;
}
// 获取远程版本号
private static async Task<string> GetRemoteVersion(string fileUrl)
{
// 检测是否为Windows 7
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (isWindows7)
{
// Windows 7使用特殊配置
using (var handler = new HttpClientHandler())
{
try
{
// 配置HttpClientHandler以支持Windows 7
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (HttpClient client = new HttpClient(handler))
{
client.Timeout = RequestTimeout;
LogHelper.WriteLogToFile($"AutoUpdate | 发送HTTP请求到: {fileUrl}");
var downloadTask = client.GetAsync(fileUrl);
var timeoutTask = Task.Delay(RequestTimeout);
var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
if (completedTask == timeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时 ({RequestTimeout.TotalSeconds}秒)", LogHelper.LogType.Error);
return null;
}
HttpResponseMessage response = await downloadTask;
LogHelper.WriteLogToFile($"AutoUpdate | HTTP响应状态: {response.StatusCode}");
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
content = content.Trim();
// 如果内容包含HTML(可能是GitHub页面而不是原始内容),尝试提取版本号
if (content.Contains("<html") || content.Contains("<!DOCTYPE"))
{
LogHelper.WriteLogToFile("AutoUpdate | 收到HTML内容而不是原始版本号 - 尝试提取版本");
int startPos = content.IndexOf("<table");
if (startPos > 0)
{
int endPos = content.IndexOf("</table>", startPos);
if (endPos > startPos)
{
string tableContent = content.Substring(startPos, endPos - startPos);
var match = Regex.Match(tableContent, @"(\d+\.\d+\.\d+(\.\d+)?)");
if (match.Success)
{
content = match.Groups[1].Value;
LogHelper.WriteLogToFile($"AutoUpdate | 从HTML提取版本: {content}");
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 无法从HTML内容提取版本");
return null;
}
}
}
}
LogHelper.WriteLogToFile($"AutoUpdate | 响应内容: {content}");
return content;
}
}
catch (HttpRequestException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | HTTP请求错误: {ex.Message}", LogHelper.LogType.Error);
}
catch (TaskCanceledException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时: {ex.Message}", LogHelper.LogType.Error);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 错误: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
}
// 其他Windows版本使用标准配置
using (HttpClient client = new HttpClient())
{
try
{
client.Timeout = RequestTimeout;
LogHelper.WriteLogToFile($"AutoUpdate | 发送HTTP请求到: {fileUrl}");
var downloadTask = client.GetAsync(fileUrl);
var timeoutTask = Task.Delay(RequestTimeout);
var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
if (completedTask == timeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时 ({RequestTimeout.TotalSeconds}秒)", LogHelper.LogType.Error);
return null;
}
HttpResponseMessage response = await downloadTask;
LogHelper.WriteLogToFile($"AutoUpdate | HTTP响应状态: {response.StatusCode}");
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
content = content.Trim();
// 如果内容包含HTML(可能是GitHub页面而不是原始内容),尝试提取版本号
if (content.Contains("<html") || content.Contains("<!DOCTYPE"))
{
LogHelper.WriteLogToFile("AutoUpdate | 收到HTML内容而不是原始版本号 - 尝试提取版本");
int startPos = content.IndexOf("<table");
if (startPos > 0)
{
int endPos = content.IndexOf("</table>", startPos);
if (endPos > startPos)
{
string tableContent = content.Substring(startPos, endPos - startPos);
var match = Regex.Match(tableContent, @"(\d+\.\d+\.\d+(\.\d+)?)");
if (match.Success)
{
content = match.Groups[1].Value;
LogHelper.WriteLogToFile($"AutoUpdate | 从HTML提取版本: {content}");
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 无法从HTML内容提取版本");
return null;
}
}
}
}
LogHelper.WriteLogToFile($"AutoUpdate | 响应内容: {content}");
return content;
}
catch (HttpRequestException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | HTTP请求错误: {ex.Message}", LogHelper.LogType.Error);
}
catch (TaskCanceledException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时: {ex.Message}", LogHelper.LogType.Error);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 错误: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
}
// 通过GitHub API获取指定版本的Release信息
private static async Task<(string version, string downloadUrl, string releaseNotes, DateTime? releaseTime)> GetGithubReleaseByVersion(string targetVersion, UpdateChannel channel)
{
try
{
string apiUrl = channel == UpdateChannel.Beta
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
var response = await client.GetStringAsync(apiUrl);
var releases = JArray.Parse(response);
foreach (var release in releases)
{
string version = release["tag_name"]?.ToString();
if (version == targetVersion || version == $"v{targetVersion}" || version == $"V{targetVersion}")
{
string releaseNotes = release["body"]?.ToString();
string downloadUrl = release["assets"]?.First?["browser_download_url"]?.ToString();
// 解析发布时间
DateTime? releaseTime = null;
if (release["published_at"] != null && DateTime.TryParse(release["published_at"].ToString(), out DateTime parsedTime))
{
releaseTime = parsedTime;
}
return (version, downloadUrl, releaseNotes, releaseTime);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取版本 {targetVersion} 失败: {ex.Message}", LogHelper.LogType.Warning);
}
return (null, null, null, null);
}
// 通过GitHub API获取最新Release信息
private static async Task<(string version, string downloadUrl, string releaseNotes, DateTime? releaseTime)> GetLatestGithubRelease(UpdateChannel channel)
{
try
{
string apiUrl = channel == UpdateChannel.Beta
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases/latest"
: "https://api.github.com/repos/InkCanvasForClass/community/releases/latest";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
var response = await client.GetStringAsync(apiUrl);
var json = JObject.Parse(response);
string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString();
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
// 解析发布时间
DateTime? releaseTime = null;
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
{
releaseTime = parsedTime;
}
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes, releaseTime);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取失败: {ex.Message}", LogHelper.LogType.Warning);
}
return (null, null, null, null);
}
// 主要的更新检测方法(优先检测延迟,失败时自动切换线路组)
// 仅检测新版本时用GitHub API,实际下载时只用线路组
public static async Task<(string remoteVersion, UpdateLineGroup lineGroup, string releaseNotes)> CheckForUpdates(UpdateChannel channel = UpdateChannel.Release, bool alwaysGetRemote = false, bool isVersionFix = false)
{
try
{
// 记录更新检查时间
DeviceIdentifier.RecordUpdateCheck();
string localVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
LogHelper.WriteLogToFile($"AutoUpdate | 本地版本: {localVersion}");
LogHelper.WriteLogToFile($"AutoUpdate | 设备ID: {DeviceIdentifier.GetDeviceId()}");
LogHelper.WriteLogToFile($"AutoUpdate | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
LogHelper.WriteLogToFile("AutoUpdate | 优先通过GitHub Releases API检测...");
// 1. 优先通过GitHub Releases API获取
var (apiVersion, _, apiReleaseNotes, apiReleaseTime) = await GetLatestGithubRelease(channel);
if (!string.IsNullOrEmpty(apiVersion))
{
Version local = new Version(localVersion);
Version remote = new Version(apiVersion.TrimStart('v', 'V'));
if (remote > local || alwaysGetRemote)
{
LogHelper.WriteLogToFile($"AutoUpdate | 通过GitHub Releases API发现新版本: {apiVersion}");
// 检查是否应该根据用户优先级推送更新(版本修复功能不受限制)
if (!isVersionFix)
{
DateTime releaseTime = apiReleaseTime ?? DateTime.Now;
// 尝试获取当前版本的发布时间
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(apiVersion, releaseTime, true, currentVersionReleaseTime); // 明确标记为自动更新
if (!shouldPush)
{
var priority = DeviceIdentifier.GetUpdatePriority();
var daysBetweenVersions = currentVersionReleaseTime.HasValue
? (releaseTime - currentVersionReleaseTime.Value).TotalDays
: (DateTime.Now - releaseTime).TotalDays;
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级({priority}),暂不推送更新 {apiVersion},版本间隔: {daysBetweenVersions:F1} 天");
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, group, apiReleaseNotes); // 返回null表示不推送
}
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 版本修复模式,跳过分级策略检查");
}
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级,推送更新 {apiVersion}");
// 只返回版本号和日志,不返回直链
var availableGroup = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (apiVersion, availableGroup, apiReleaseNotes);
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 当前版本已是最新 (GitHub Releases API)");
var availableGroup = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, availableGroup, apiReleaseNotes);
}
}
// 2. 回退到原有txt方案
LogHelper.WriteLogToFile("AutoUpdate | GitHub Releases API获取失败,回退到txt方案...");
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
if (availableGroups.Count == 0)
{
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
return (null, null, null);
}
foreach (var group in availableGroups)
{
LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用线路组获取版本信息: {group.GroupName}");
string remoteVersion = await GetRemoteVersion(group.VersionUrl);
if (remoteVersion != null)
{
LogHelper.WriteLogToFile($"AutoUpdate | 成功从线路组 {group.GroupName} 获取远程版本: {remoteVersion}");
Version local = new Version(localVersion);
Version remote = new Version(remoteVersion);
if (remote > local || alwaysGetRemote)
{
LogHelper.WriteLogToFile($"AutoUpdate | 发现新版本或强制获取: {remoteVersion}");
// 检查是否应该根据用户优先级推送更新(版本修复功能不受限制)
if (!isVersionFix)
{
// 尝试获取当前版本的发布时间
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(remoteVersion, DateTime.Now, true, currentVersionReleaseTime); // 明确标记为自动更新
if (!shouldPush)
{
var priority = DeviceIdentifier.GetUpdatePriority();
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级({priority}),暂不推送更新 {remoteVersion}");
return (null, group, null); // 返回null表示不推送
}
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 版本修复模式,跳过分级策略检查");
}
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级,推送更新 {remoteVersion}");
return (remoteVersion, group, null);
}
LogHelper.WriteLogToFile("AutoUpdate | 当前版本已是最新");
return (null, group, null);
}
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 获取版本失败,尝试下一个线路组", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均无法获取版本信息", LogHelper.LogType.Error);
return (null, null, null);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | CheckForUpdates错误: {ex.Message}", LogHelper.LogType.Error);
return (null, null, null);
}
}
// 使用指定线路组下载新版
public static async Task<bool> DownloadSetupFile(string version, UpdateLineGroup group)
{
return await DownloadSetupFileWithFallback(version, new List<UpdateLineGroup> { group });
}
// 获取智教联盟真实下载地址
private static async Task<string> GetZhijiaoRealDownloadUrl(string url)
{
try
{
using (var handler = new HttpClientHandler { AllowAutoRedirect = false })
using (var client = new HttpClient(handler))
{
client.Timeout = RequestTimeout;
var resp = await client.GetAsync(url);
// 优先取Location头
if (resp.StatusCode == HttpStatusCode.Found || resp.StatusCode == HttpStatusCode.Redirect || resp.StatusCode == HttpStatusCode.MovedPermanently)
{
if (resp.Headers.Location != null)
{
var realUrl = resp.Headers.Location.ToString();
if (realUrl.Contains(" ")) realUrl = realUrl.Replace(" ", "%20");
return realUrl;
}
}
// 有些服务器直接返回真实地址在内容里
var content = await resp.Content.ReadAsStringAsync();
if (Uri.IsWellFormedUriString(content.Trim(), UriKind.Absolute))
{
var realUrl = content.Trim();
if (realUrl.Contains(" ")) realUrl = realUrl.Replace(" ", "%20");
return realUrl;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 获取智教联盟真实下载地址失败: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
// 使用多线路组下载新版(支持自动切换)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
{
try
{
statusFilePath = Path.Combine(updatesFolderPath, $"DownloadV{version}Status.txt");
if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true")
{
LogHelper.WriteLogToFile("AutoUpdate | 安装包已下载");
progressCallback?.Invoke(100, "已下载完成");
return true;
}
// 确保更新目录存在
if (!Directory.Exists(updatesFolderPath))
{
Directory.CreateDirectory(updatesFolderPath);
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新目录: {updatesFolderPath}");
}
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
LogHelper.WriteLogToFile($"AutoUpdate | 目标文件路径: {zipFilePath}");
SaveDownloadStatus(false);
// 优先尝试“智教联盟”线路组
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
if (zhiJiaoGroup != null || inkeysGroup != null)
{
var priorityGroups = new List<UpdateLineGroup>();
if (zhiJiaoGroup != null)
{
priorityGroups.Add(zhiJiaoGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试智教联盟线路组");
}
if (inkeysGroup != null)
{
priorityGroups.Add(inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
}
groups = priorityGroups.Concat(groups.Where(g => g.GroupName != "智教联盟" && g.GroupName != "inkeys")).ToList();
}
// 依次尝试每个线路组
foreach (var group in groups)
{
string url = string.Format(group.DownloadUrlFormat, version);
// 智教联盟需要先获取真实下载地址
if (group.GroupName == "智教联盟")
{
LogHelper.WriteLogToFile($"AutoUpdate | 获取智教联盟真实下载地址: {url}");
var realUrl = await GetZhijiaoRealDownloadUrl(url);
if (string.IsNullOrEmpty(realUrl))
{
LogHelper.WriteLogToFile("AutoUpdate | 智教联盟真实下载地址获取失败,跳过", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, "智教联盟真实下载地址获取失败,跳过");
continue;
}
url = realUrl;
LogHelper.WriteLogToFile($"AutoUpdate | 智教联盟真实下载地址: {url}");
}
// inkeys线路组直接使用下载地址,无需特殊处理
else if (group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 使用inkeys线路组下载地址: {url}");
}
LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}");
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
if (downloadSuccess)
{
SaveDownloadStatus(true);
LogHelper.WriteLogToFile($"AutoUpdate | 从线路组 {group.GroupName} 下载成功");
progressCallback?.Invoke(100, "下载完成");
return true;
}
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 下载失败,尝试下一个线路组", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组下载均失败", LogHelper.LogType.Error);
progressCallback?.Invoke(0, "所有线路组下载均失败");
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载更新时出错: {ex.Message}", LogHelper.LogType.Error);
if (ex.InnerException != null)
{
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error);
}
SaveDownloadStatus(false);
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
}
// 下载文件的具体实现
public static async Task<bool> DownloadFile(string fileUrl, string destinationPath, Action<double, string> progressCallback = null)
{
LogHelper.WriteLogToFile($"AutoUpdate | 正在尝试多线程下载: {fileUrl}");
int maxRetry = 3;
// 降低并发数,减少网络压力
int[] threadOptions = { 32, 16, 8, 4, 1 };
// 检查服务器是否支持Range分块下载
bool supportRange = false;
long totalSize = -1;
try
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
var req = new HttpRequestMessage(HttpMethod.Head, fileUrl);
req.Headers.Range = new RangeHeaderValue(0, 0);
var resp = await client.SendAsync(req);
if (resp.StatusCode == HttpStatusCode.PartialContent)
{
supportRange = true;
if (resp.Content.Headers.ContentRange != null && resp.Content.Headers.ContentRange.Length.HasValue)
{
totalSize = resp.Content.Headers.ContentRange.Length.Value;
}
else if (resp.Content.Headers.ContentLength.HasValue)
{
totalSize = resp.Content.Headers.ContentLength.Value;
}
}
else if (resp.StatusCode == HttpStatusCode.OK)
{
supportRange = false;
if (resp.Content.Headers.ContentLength.HasValue)
{
totalSize = resp.Content.Headers.ContentLength.Value;
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 检查Range支持时异常: {ex.Message}", LogHelper.LogType.Warning);
}
if (!supportRange)
{
LogHelper.WriteLogToFile("AutoUpdate | 服务器不支持分块下载,自动降级为单线程下载");
progressCallback?.Invoke(0, "服务器不支持分块下载,自动降级为单线程下载");
return await DownloadSingleThread(fileUrl, destinationPath, totalSize, progressCallback);
}
foreach (int threadCount in threadOptions)
{
LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用 {threadCount} 线程下载");
progressCallback?.Invoke(0, $"尝试使用 {threadCount} 线程下载");
if (totalSize <= 0)
{
totalSize = await GetContentLength(fileUrl);
}
if (totalSize <= 0)
{
progressCallback?.Invoke(0, "无法获取文件大小,取消下载");
return false;
}
// 根据文件大小动态调整分块大小,避免分块过小
int minBlockSize = 32 * 1024; // 最小32KB
int blockSize = Math.Max(minBlockSize, (int)Math.Ceiling((double)totalSize / threadCount));
int blockCount = (int)Math.Ceiling((double)totalSize / blockSize);
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小: {totalSize}, 分块数: {blockCount}, 分块大小: {blockSize}");
var blockQueue = new ConcurrentQueue<BlockTask>();
var finishedBlocks = new ConcurrentDictionary<int, bool>();
long[] blockDownloaded = new long[blockCount];
for (int i = 0; i < blockCount; i++)
{
long start = i * blockSize;
long end = Math.Min(start + blockSize - 1, totalSize - 1);
blockQueue.Enqueue(new BlockTask { Index = i, Start = start, End = end, RetryCount = 0 });
}
CancellationTokenSource cts = new CancellationTokenSource();
var tasks = new List<Task>();
for (int t = 0; t < threadCount; t++)
{
tasks.Add(Task.Run(async () =>
{
while (blockQueue.TryDequeue(out var block))
{
bool success = false;
string tempPath = destinationPath + $".part{block.Index}";
for (int retry = block.RetryCount; retry < maxRetry && !success; retry++)
{
try
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
req.Headers.Range = new RangeHeaderValue(block.Start, block.End);
// 增加连接超时设置
client.Timeout = TimeSpan.FromSeconds(30);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var lastReadTime = DateTime.UtcNow;
bool dataReceived = false;
using (var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, downloadCts.Token))
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 响应状态: {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192];
int read;
long blockDownloadedBytes = 0;
while (true)
{
var readTask = stream.ReadAsync(buffer, 0, buffer.Length, downloadCts.Token);
var timeoutTask = Task.Delay(20000, downloadCts.Token); // 增加到20秒超时
var completed = await Task.WhenAny(readTask, timeoutTask);
if (completed == timeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 20秒无数据,线程超时重试", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index} 20秒无数据,线程超时重试");
downloadCts.Cancel();
break;
}
read = await readTask;
if (read <= 0) break;
await fs.WriteAsync(buffer, 0, read, downloadCts.Token);
blockDownloadedBytes += read;
blockDownloaded[block.Index] = blockDownloadedBytes;
lastReadTime = DateTime.UtcNow;
dataReceived = true;
// 合并所有块进度
long totalDownloaded = blockDownloaded.Sum();
double percent = (double)totalDownloaded / totalSize * 100;
progressCallback?.Invoke(percent, $"多线程下载中({threadCount}线程): {percent:F1}%");
}
}
}
if (!dataReceived)
{
throw new IOException("分块下载超时无数据");
}
// 验证分块大小是否正确
var fileInfo = new FileInfo(tempPath);
long expectedSize = block.End - block.Start + 1;
if (fileInfo.Length != expectedSize)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}大小不匹配,期望:{expectedSize},实际:{fileInfo.Length}", LogHelper.LogType.Warning);
throw new IOException($"分块{block.Index}大小不匹配");
}
}
success = true;
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
}
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}");
// 清理可能损坏的分块文件
if (File.Exists(tempPath))
{
try { File.Delete(tempPath); } catch { }
}
// 增加重试间隔,避免频繁重试
await Task.Delay(2000 * (retry + 1));
}
}
if (success)
{
finishedBlocks[block.Index] = true;
}
else if (block.RetryCount + 1 < maxRetry)
{
// 失败但未超最大重试,重新入队
block.RetryCount++;
blockQueue.Enqueue(block);
}
else
{
// 超过最大重试,取消所有任务
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}超过最大重试次数,取消下载", LogHelper.LogType.Error);
cts.Cancel();
break;
}
}
}));
}
await Task.WhenAll(tasks);
if (cts.IsCancellationRequested || finishedBlocks.Count != blockCount)
{
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,完成分块数: {finishedBlocks.Count}/{blockCount}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,完成分块数: {finishedBlocks.Count}/{blockCount}");
// 清理分块文件
for (int i = 0; i < blockCount; i++)
{
string tempPath = destinationPath + $".part{i}";
if (File.Exists(tempPath)) File.Delete(tempPath);
}
if (threadCount == threadOptions.Last())
{
// 已经是最后一次尝试,降级为单线程
LogHelper.WriteLogToFile("AutoUpdate | 所有多线程尝试失败,降级为单线程下载");
progressCallback?.Invoke(0, "所有多线程尝试失败,降级为单线程下载");
return await DownloadSingleThread(fileUrl, destinationPath, totalSize, progressCallback);
}
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,尝试降级为{threadOptions[Array.IndexOf(threadOptions, threadCount) + 1]}线程");
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,尝试降级为{threadOptions[Array.IndexOf(threadOptions, threadCount) + 1]}线程");
continue;
}
// 合并所有块
try
{
using (var output = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
for (int i = 0; i < blockCount; i++)
{
string tempPath = destinationPath + $".part{i}";
if (!File.Exists(tempPath))
{
throw new FileNotFoundException($"分块文件不存在: {tempPath}");
}
using (var input = new FileStream(tempPath, FileMode.Open, FileAccess.Read))
{
await input.CopyToAsync(output);
}
File.Delete(tempPath);
}
}
progressCallback?.Invoke(100, $"多线程下载完成({threadCount}线程)");
LogHelper.WriteLogToFile($"AutoUpdate | 多线程下载完成({threadCount}线程)");
// 文件大小校验
FileInfo fileInfo = new FileInfo(destinationPath);
if (fileInfo.Length != totalSize)
{
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小校验失败,本地:{fileInfo.Length},服务器:{totalSize}", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, "文件大小校验失败,已删除损坏文件");
return false;
}
// ZIP文件完整性校验
if (destinationPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
try
{
ZipFile.OpenRead(destinationPath).Dispose();
}
catch
{
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压测试失败,文件可能已损坏", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, "ZIP文件解压测试失败,已删除损坏文件");
return false;
}
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 合并分块文件时出错: {ex.Message}", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, $"合并分块文件时出错: {ex.Message}");
return false;
}
}
return false;
}
// 单线程下载方法
private static async Task<bool> DownloadSingleThread(string fileUrl, string destinationPath, long totalSize, Action<double, string> progressCallback = null)
{
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始单线程下载: {fileUrl}");
progressCallback?.Invoke(0, "开始单线程下载");
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.Timeout = TimeSpan.FromMinutes(10); // 单线程下载设置更长的超时时间
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
{
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192];
int read;
long downloaded = 0;
var lastProgressUpdate = DateTime.UtcNow;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fs.WriteAsync(buffer, 0, read);
downloaded += read;
// 限制进度更新频率,避免UI卡顿
if (DateTime.UtcNow - lastProgressUpdate > TimeSpan.FromMilliseconds(500))
{
if (totalSize > 0)
{
double percent = (double)downloaded / totalSize * 100;
progressCallback?.Invoke(percent, $"单线程下载中: {percent:F1}%");
}
lastProgressUpdate = DateTime.UtcNow;
}
}
}
}
}
progressCallback?.Invoke(100, "单线程下载完成");
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载完成");
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
progressCallback?.Invoke(0, $"单线程下载失败: {ex.Message}");
return false;
}
}
// 获取文件总大小
private static async Task<long> GetContentLength(string fileUrl)
{
try
{
using (var client = new HttpClient())
{
var req = new HttpRequestMessage(HttpMethod.Head, fileUrl);
var resp = await client.SendAsync(req);
if (resp.IsSuccessStatusCode && resp.Content.Headers.ContentLength.HasValue)
return resp.Content.Headers.ContentLength.Value;
}
}
catch { }
return -1;
}
// 保存下载状态
private static void SaveDownloadStatus(bool isSuccess)
{
try
{
if (statusFilePath == null) return;
string directory = Path.GetDirectoryName(statusFilePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(statusFilePath, isSuccess.ToString());
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 保存下载状态时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
// 安装新版本应用
public static void InstallNewVersionApp(string version, bool isInSilence)
{
try
{
// 在更新前备份设置文件
try
{
if (MainWindow.Settings.Advanced.IsAutoBackupBeforeUpdate)
{
string backupDir = Path.Combine(App.RootPath, "Backups");
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
LogHelper.WriteLogToFile($"创建备份目录: {backupDir}");
}
string backupFileName = $"Settings_BeforeUpdate_v{version}_{DateTime.Now:yyyyMMdd_HHmmss}.json";
string backupPath = Path.Combine(backupDir, backupFileName);
string settingsJson = JsonConvert.SerializeObject(MainWindow.Settings, Formatting.Indented);
File.WriteAllText(backupPath, settingsJson);
LogHelper.WriteLogToFile($"更新前自动备份设置成功: {backupPath}");
}
else
{
LogHelper.WriteLogToFile("更新前自动备份功能已禁用,跳过备份");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新前自动备份设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
LogHelper.WriteLogToFile($"AutoUpdate | 检查ZIP文件: {zipFilePath}");
if (!File.Exists(zipFilePath))
{
LogHelper.WriteLogToFile($"AutoUpdate | ZIP文件未找到: {zipFilePath}", LogHelper.LogType.Error);
return;
}
FileInfo fileInfo = new FileInfo(zipFilePath);
if (fileInfo.Length == 0)
{
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件为空,无法继续", LogHelper.LogType.Error);
return;
}
LogHelper.WriteLogToFile($"AutoUpdate | ZIP文件大小: {fileInfo.Length} 字节");
string currentAppDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
int currentProcessId = Process.GetCurrentProcess().Id;
string appPath = Assembly.GetExecutingAssembly().Location;
LogHelper.WriteLogToFile($"AutoUpdate | 当前应用程序目录: {currentAppDir}");
LogHelper.WriteLogToFile($"AutoUpdate | 当前进程ID: {currentProcessId}");
LogHelper.WriteLogToFile($"AutoUpdate | 静默更新模式: {isInSilence}");
string batchFilePath = Path.Combine(Path.GetTempPath(), "UpdateICC_" + Guid.NewGuid().ToString().Substring(0, 8) + ".bat");
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新批处理文件: {batchFilePath}");
StringBuilder batchContent = new StringBuilder();
batchContent.AppendLine("@echo off");
batchContent.AppendLine("echo Set objShell = CreateObject(\"WScript.Shell\") > \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("echo objShell.Run \"cmd /c \"\"\" ^& WScript.Arguments(0) ^& \"\"\"\", 0, True >> \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("echo Wscript.Sleep 100 >> \"%temp%\\hideme.vbs\"");
string updateBatPath = Path.Combine(Path.GetTempPath(), "ICCUpdate_" + Guid.NewGuid().ToString().Substring(0, 8) + ".bat");
batchContent.AppendLine($"echo @echo off > \"{updateBatPath}\"");
batchContent.AppendLine($"echo set PROC_ID={currentProcessId} >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo :CHECK_PROCESS >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo tasklist /fi \"PID eq %PROC_ID%\" ^| find \"%PROC_ID%\" ^> nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% == 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo timeout /t 1 /nobreak ^> nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto CHECK_PROCESS >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo timeout /t 1 /nobreak > nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Application closed, starting update process... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo timeout /t 2 /nobreak ^> nul >> \"{updateBatPath}\"");
string extractPath = Path.Combine(updatesFolderPath, $"Extract_{version}");
batchContent.AppendLine($"echo echo Extracting update files... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if exist \"{extractPath}\" rd /s /q \"{extractPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo mkdir \"{extractPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo powershell -command \"Expand-Archive -Path '{zipFilePath.Replace("'", "''")}' -DestinationPath '{extractPath.Replace("'", "''")}' -Force\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% neq 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Copying updated files to application directory... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo xcopy /s /y /e \"{extractPath}\\*\" \"{currentAppDir}\\\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% neq 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Cleaning up temporary files... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if exist \"{extractPath}\" rd /s /q \"{extractPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if exist \"{zipFilePath}\" del /f /q \"{zipFilePath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Update completed successfully! >> \"{updateBatPath}\"");
if (isInSilence)
{
batchContent.AppendLine($"echo echo 自动启动应用程序... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo start \"\" \"{appPath}\" >> \"{updateBatPath}\"");
}
else
{
batchContent.AppendLine($"echo taskkill /F /IM \"InkCanvasForClass.exe\" >nul 2>nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo :: 检查应用程序是否已经在运行 >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo tasklist /FI \"IMAGENAME eq InkCanvasForClass.exe\" | find /i \"InkCanvasForClass.exe\" > nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% neq 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo 启动应用程序... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo start \"\" \"{appPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) else ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo 应用程序已经在运行,不再重复启动 >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
}
batchContent.AppendLine($"echo exit /b 0 >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto EXIT >> \"{updateBatPath}\"");
if (isInSilence)
{
batchContent.AppendLine($"echo :ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Update failed! >> \"%temp%\\icc_update_error.log\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo exit /b 1 >> \"{updateBatPath}\"");
}
else
{
batchContent.AppendLine($"echo :ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo start \"\" cmd /c \"echo Update failed! ^& pause\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo exit /b 1 >> \"{updateBatPath}\"");
}
batchContent.AppendLine($"echo :EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo del \"{updateBatPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo exit >> \"{updateBatPath}\"");
batchContent.AppendLine($"wscript \"%temp%\\hideme.vbs\" \"{updateBatPath}\"");
batchContent.AppendLine("del \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("exit");
File.WriteAllText(batchFilePath, batchContent.ToString());
LogHelper.WriteLogToFile("AutoUpdate | 创建更新批处理文件完成");
Process.Start(new ProcessStartInfo
{
FileName = batchFilePath,
CreateNoWindow = true,
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
LogHelper.WriteLogToFile("AutoUpdate | 启动更新批处理进程(隐藏窗口)");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 准备更新安装时出错: {ex.Message}", LogHelper.LogType.Error);
if (ex.InnerException != null)
{
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error);
}
}
}
// 获取远程内容的通用方法
public static async Task<string> GetRemoteContent(string fileUrl)
{
// 检测是否为Windows 7
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (isWindows7)
{
// Windows 7使用特殊配置
using (var handler = new HttpClientHandler())
{
try
{
// 配置HttpClientHandler以支持Windows 7
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (HttpClient client = new HttpClient(handler))
{
client.Timeout = RequestTimeout;
LogHelper.WriteLogToFile($"AutoUpdate | 发送HTTP请求到: {fileUrl}");
var downloadTask = client.GetAsync(fileUrl);
var timeoutTask = Task.Delay(RequestTimeout);
var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
if (completedTask == timeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时 ({RequestTimeout.TotalSeconds}秒)", LogHelper.LogType.Error);
return null;
}
HttpResponseMessage response = await downloadTask;
LogHelper.WriteLogToFile($"AutoUpdate | HTTP响应状态: {response.StatusCode}");
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
return content;
}
}
catch (HttpRequestException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | HTTP请求错误: {ex.Message}", LogHelper.LogType.Error);
}
catch (TaskCanceledException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时: {ex.Message}", LogHelper.LogType.Error);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 错误: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
}
// 其他Windows版本使用标准配置
using (HttpClient client = new HttpClient())
{
try
{
client.Timeout = RequestTimeout;
LogHelper.WriteLogToFile($"AutoUpdate | 发送HTTP请求到: {fileUrl}");
var downloadTask = client.GetAsync(fileUrl);
var timeoutTask = Task.Delay(RequestTimeout);
var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
if (completedTask == timeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时 ({RequestTimeout.TotalSeconds}秒)", LogHelper.LogType.Error);
return null;
}
HttpResponseMessage response = await downloadTask;
LogHelper.WriteLogToFile($"AutoUpdate | HTTP响应状态: {response.StatusCode}");
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
return content;
}
catch (HttpRequestException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | HTTP请求错误: {ex.Message}", LogHelper.LogType.Error);
}
catch (TaskCanceledException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 请求超时: {ex.Message}", LogHelper.LogType.Error);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 错误: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
}
// 使用指定线路组获取更新日志
public static async Task<string> GetUpdateLogWithLineGroup(UpdateLineGroup group)
{
return await GetRemoteContent(group.LogUrl);
}
// 获取更新日志(自动选择最快线路组)
public static async Task<string> GetUpdateLog(UpdateChannel channel = UpdateChannel.Release)
{
var group = await GetFastestLineGroup(channel);
if (group == null) return "# 无法获取更新日志\n\n所有线路均不可用。";
return await GetUpdateLogWithLineGroup(group);
}
// 删除更新文件夹
public static void DeleteUpdatesFolder()
{
try
{
if (Directory.Exists(updatesFolderPath))
{
foreach (string file in Directory.GetFiles(updatesFolderPath, "*", SearchOption.AllDirectories))
{
try { File.Delete(file); } catch { }
}
foreach (string dir in Directory.GetDirectories(updatesFolderPath))
{
try { Directory.Delete(dir, true); } catch { }
}
try { Directory.Delete(updatesFolderPath, true); } catch { }
}
}
catch { }
}
// 版本修复方法,强制下载并安装指定通道的最新版本
public static async Task<bool> FixVersion(UpdateChannel channel = UpdateChannel.Release)
{
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本,版本修复模式)
var (remoteVersion, group, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
return false;
}
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本远程版本: {remoteVersion}");
// 无论版本是否为最新,都下载远程版本
bool downloadResult = await DownloadSetupFile(remoteVersion, group);
if (!downloadResult)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时下载更新失败", LogHelper.LogType.Error);
return false;
}
// 执行安装,非静默模式
InstallNewVersionApp(remoteVersion, false);
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() =>
{
Application.Current.Shutdown();
});
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | FixVersion错误: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
// 获取所有GitHub历史版本(Release
public static async Task<List<(string version, string downloadUrl, string releaseNotes)>> GetAllGithubReleases(UpdateChannel channel = UpdateChannel.Release)
{
var result = new List<(string, string, string)>();
try
{
string apiUrl = channel == UpdateChannel.Beta
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
var response = await client.GetStringAsync(apiUrl);
var arr = JArray.Parse(response);
foreach (var item in arr)
{
string version = item["tag_name"]?.ToString();
string releaseNotes = item["body"]?.ToString();
string downloadUrl = item["assets"]?.First?["browser_download_url"]?.ToString();
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
result.Add((version, downloadUrl, releaseNotes));
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 获取历史版本失败: {ex.Message}", LogHelper.LogType.Error);
}
return result;
}
// 测试Windows 7 TLS连接的方法
public static async Task<bool> TestWindows7TlsConnection()
{
try
{
// 检测是否为Windows 7
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (!isWindows7)
{
LogHelper.WriteLogToFile("AutoUpdate | 当前系统不是Windows 7,跳过TLS连接测试");
return true; // 非Windows 7系统直接返回成功
}
LogHelper.WriteLogToFile("AutoUpdate | 开始测试Windows 7 TLS连接...");
// 测试GitHub连接
var testUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt";
using (var handler = new HttpClientHandler())
{
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient(handler))
{
client.Timeout = TimeSpan.FromSeconds(10);
var response = await client.GetAsync(testUrl);
if (response.IsSuccessStatusCode)
{
LogHelper.WriteLogToFile("AutoUpdate | Windows 7 TLS连接测试成功");
return true;
}
LogHelper.WriteLogToFile($"AutoUpdate | Windows 7 TLS连接测试失败,状态码: {response.StatusCode}", LogHelper.LogType.Error);
return false;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | Windows 7 TLS连接测试异常: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 获取指定版本的发布时间
/// </summary>
/// <param name="version">版本号</param>
/// <param name="channel">更新通道</param>
/// <returns>版本发布时间,如果获取失败则返回null</returns>
public static async Task<DateTime?> GetVersionReleaseTime(string version, UpdateChannel channel = UpdateChannel.Release)
{
try
{
var (_, _, _, releaseTime) = await GetGithubReleaseByVersion(version, channel);
return releaseTime;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 获取版本 {version} 发布时间失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
}
/// <summary>
/// 启动手动指定版本的多线路多线程下载并自动安装(用于历史版本回滚等场景)
/// </summary>
public static async Task<bool> StartManualDownloadAndInstall(string version, UpdateChannel channel, Action<double, string> progressCallback = null)
{
try
{
// 先检测并排序所有可用线路组
var groups = await GetAvailableLineGroupsOrdered(channel);
bool downloadSuccess = await DownloadSetupFileWithFallback(version, groups, progressCallback);
if (!downloadSuccess)
{
LogHelper.WriteLogToFile($"AutoUpdate | 手动下载版本{version}失败");
return false;
}
LogHelper.WriteLogToFile($"AutoUpdate | 手动安装版本: {version}");
InstallNewVersionApp(version, false);
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() =>
{
Application.Current.Shutdown();
});
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 手动下载或安装异常: {ex.Message}", LogHelper.LogType.Error);
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
}
}
internal class AutoUpdateWithSilenceTimeComboBox
{
public static ObservableCollection<string> Hours { get; set; } = new ObservableCollection<string>();
public static ObservableCollection<string> Minutes { get; set; } = new ObservableCollection<string>();
public static void InitializeAutoUpdateWithSilenceTimeComboBoxOptions(ComboBox startTimeComboBox, ComboBox endTimeComboBox)
{
for (int hour = 0; hour <= 23; ++hour)
{
Hours.Add(hour.ToString("00"));
}
for (int minute = 0; minute <= 59; minute += 20)
{
Minutes.Add(minute.ToString("00"));
}
startTimeComboBox.ItemsSource = Hours.SelectMany(h => Minutes.Select(m => $"{h}:{m}"));
endTimeComboBox.ItemsSource = Hours.SelectMany(h => Minutes.Select(m => $"{h}:{m}"));
}
public static bool CheckIsInSilencePeriod(string startTime, string endTime)
{
if (startTime == endTime) return true;
DateTime currentTime = DateTime.Now;
DateTime StartTime = DateTime.ParseExact(startTime, "HH:mm", null);
DateTime EndTime = DateTime.ParseExact(endTime, "HH:mm", null);
if (StartTime <= EndTime)
{ // 单日时间段
return currentTime >= StartTime && currentTime <= EndTime;
} // 跨越两天的时间段
return currentTime >= StartTime || currentTime <= EndTime;
}
}
}