Initial commit: R0Installer source, patch generator, API backend and build scripts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 18:03:44 +08:00
commit 6124d6060c
33 changed files with 8389 additions and 0 deletions
+492
View File
@@ -0,0 +1,492 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace R0Installer
{
public class UpdateForm : Form
{
private Panel progressBar;
private Panel progressFill;
private Label titleLabel;
private Label statusLabel;
private Label percentLabel;
private Label versionLabel;
private string currentVersion;
private UpdateManager updateManager;
private readonly Color primaryGreen = Color.FromArgb(76, 175, 80);
private readonly Color darkBg = Color.FromArgb(18, 18, 18);
private readonly Color panelBg = Color.FromArgb(28, 28, 28);
private readonly Color textGray = Color.FromArgb(180, 180, 180);
private readonly Color textDark = Color.FromArgb(100, 100, 100);
private bool isDragging = false;
private Point dragStart;
private string installPath;
private bool updateConfirmed;
private string projectId;
private string updateTarget;
private string projectConfig;
private string updateConfig;
private string windowTitle;
private string[] processesToClose;
private string launchAfterUpdate;
private string holdFile;
public UpdateForm(string version, bool updateConfirmed, string projectId, string updateTarget)
{
this.currentVersion = version;
this.updateConfirmed = updateConfirmed;
this.projectId = projectId;
this.updateTarget = updateTarget;
this.installPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
LoadConfig();
InitializeComponent();
SetupUI();
this.Load += UpdateForm_Load;
}
private void LoadConfig()
{
projectConfig = ConfigManager.GetProjectConfigSync(projectId);
if (projectConfig != null)
{
updateConfig = ConfigManager.GetUpdateConfig(projectConfig, updateTarget);
}
if (updateConfig != null)
{
windowTitle = ConfigManager.ExtractJsonValue(updateConfig, "window_title");
launchAfterUpdate = ConfigManager.ExtractJsonValue(updateConfig, "launch_after_update");
holdFile = ConfigManager.ExtractJsonValue(updateConfig, "hold_file");
string procArray = ConfigManager.ExtractJsonArray(updateConfig, "processes_to_close");
if (!string.IsNullOrEmpty(procArray))
{
List<string> procs = ConfigManager.ParseJsonStringArray(procArray);
processesToClose = procs.ToArray();
}
this.updateManager = new UpdateManager(updateConfig, projectConfig);
}
else
{
windowTitle = "更新程序";
processesToClose = new string[0];
launchAfterUpdate = "";
holdFile = "";
string fallbackUpdateJson = "{\"check_api\":\"/v3/update/check?project=" + projectId + "&v={0}\",\"temp_filename_template\":\"update_v{version}.exe\"}";
string fallbackProjectJson = "{}";
this.updateManager = new UpdateManager(fallbackUpdateJson, fallbackProjectJson);
}
if (string.IsNullOrEmpty(windowTitle)) windowTitle = "更新程序";
if (processesToClose == null) processesToClose = new string[0];
this.updateManager.OnProgress = OnProgressUpdate;
this.updateManager.OnStatus = OnStatusUpdate;
}
private async void UpdateForm_Load(object sender, EventArgs e)
{
await StartUpdateProcess();
}
private void InitializeComponent()
{
this.SuspendLayout();
this.Text = windowTitle;
this.Size = new Size(500, 200);
this.StartPosition = FormStartPosition.CenterScreen;
this.FormBorderStyle = FormBorderStyle.None;
this.BackColor = darkBg;
this.DoubleBuffered = true;
try
{
this.Icon = Icon.ExtractAssociatedIcon(System.Reflection.Assembly.GetExecutingAssembly().Location);
}
catch { }
this.Region = CreateRoundedRegion(this.Width, this.Height, 12);
this.MouseDown += Form_MouseDown;
this.MouseMove += Form_MouseMove;
this.MouseUp += Form_MouseUp;
this.ResumeLayout(false);
}
private Region CreateRoundedRegion(int width, int height, int radius)
{
GraphicsPath path = new GraphicsPath();
path.AddArc(0, 0, radius * 2, radius * 2, 180, 90);
path.AddArc(width - radius * 2, 0, radius * 2, radius * 2, 270, 90);
path.AddArc(width - radius * 2, height - radius * 2, radius * 2, radius * 2, 0, 90);
path.AddArc(0, height - radius * 2, radius * 2, radius * 2, 90, 90);
path.CloseAllFigures();
return new Region(path);
}
private void SetupUI()
{
Label closeBtn = new Label();
closeBtn.Text = "×";
closeBtn.Font = new Font("Segoe UI", 14, FontStyle.Regular);
closeBtn.ForeColor = textGray;
closeBtn.Size = new Size(36, 36);
closeBtn.Location = new Point(this.Width - 40, 4);
closeBtn.TextAlign = ContentAlignment.MiddleCenter;
closeBtn.Cursor = Cursors.Hand;
closeBtn.Click += CloseBtn_Click;
closeBtn.MouseEnter += (s, e) => ((Label)s).ForeColor = Color.White;
closeBtn.MouseLeave += (s, e) => ((Label)s).ForeColor = textGray;
this.Controls.Add(closeBtn);
titleLabel = new Label();
titleLabel.Text = windowTitle.Replace("程序", "");
titleLabel.Font = new Font("Microsoft YaHei", 16, FontStyle.Bold);
titleLabel.ForeColor = primaryGreen;
titleLabel.AutoSize = true;
titleLabel.BackColor = Color.Transparent;
titleLabel.Location = new Point((this.Width - 180) / 2, 30);
this.Controls.Add(titleLabel);
versionLabel = new Label();
versionLabel.Text = "当前版本: " + currentVersion;
versionLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
versionLabel.ForeColor = textGray;
versionLabel.AutoSize = true;
versionLabel.Location = new Point(40, 75);
this.Controls.Add(versionLabel);
statusLabel = new Label();
statusLabel.Text = "正在检查更新...";
statusLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Regular);
statusLabel.ForeColor = textGray;
statusLabel.AutoSize = false;
statusLabel.Size = new Size(300, 25);
statusLabel.Location = new Point(40, 110);
this.Controls.Add(statusLabel);
percentLabel = new Label();
percentLabel.Text = "";
percentLabel.Font = new Font("Microsoft YaHei", 9, FontStyle.Bold);
percentLabel.ForeColor = primaryGreen;
percentLabel.AutoSize = false;
percentLabel.Size = new Size(160, 25);
percentLabel.TextAlign = ContentAlignment.MiddleRight;
percentLabel.Location = new Point(this.Width - 200, 110);
this.Controls.Add(percentLabel);
progressBar = new Panel();
progressBar.Size = new Size(this.Width - 80, 8);
progressBar.Location = new Point(40, 140);
progressBar.BackColor = panelBg;
this.Controls.Add(progressBar);
progressFill = new Panel();
progressFill.Size = new Size(0, 8);
progressFill.Location = new Point(0, 0);
progressFill.BackColor = primaryGreen;
progressBar.Controls.Add(progressFill);
this.Paint += UpdateForm_Paint;
}
private void UpdateForm_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen pen = new Pen(Color.FromArgb(50, 255, 255, 255), 1))
{
Rectangle rect = new Rectangle(0, 0, this.Width - 1, this.Height - 1);
using (GraphicsPath path = CreateRoundedPath(rect, 12))
{
g.DrawPath(pen, path);
}
}
}
private GraphicsPath CreateRoundedPath(Rectangle rect, int radius)
{
GraphicsPath path = new GraphicsPath();
path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90);
path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90);
path.AddArc(rect.Right - radius * 2, rect.Bottom - radius * 2, radius * 2, radius * 2, 0, 90);
path.AddArc(rect.X, rect.Bottom - radius * 2, radius * 2, radius * 2, 90, 90);
path.CloseAllFigures();
return path;
}
#region Window events
private void CloseBtn_Click(object sender, EventArgs e) { Application.Exit(); }
private void Form_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) { isDragging = true; dragStart = e.Location; }
}
private void Form_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Point diff = new Point(e.X - dragStart.X, e.Y - dragStart.Y);
this.Location = new Point(this.Location.X + diff.X, this.Location.Y + diff.Y);
}
}
private void Form_MouseUp(object sender, MouseEventArgs e) { isDragging = false; }
#endregion
#region Progress
private void OnProgressUpdate(int percent, string message, string speedText)
{
if (this.InvokeRequired) { this.Invoke(new Action<int, string, string>(OnProgressUpdate), percent, message, speedText); return; }
int targetWidth = (int)(progressBar.Width * percent / 100.0);
progressFill.Width = targetWidth;
if (message != null)
statusLabel.Text = message;
if (!string.IsNullOrEmpty(speedText))
percentLabel.Text = String.Format("{0}% {1}", percent, speedText);
else
percentLabel.Text = String.Format("{0}%", percent);
}
private void OnStatusUpdate(string message)
{
if (this.InvokeRequired) { this.Invoke(new Action<string>(OnStatusUpdate), message); return; }
statusLabel.Text = message;
}
private void UpdateVersionLabel(string newVersion)
{
if (this.InvokeRequired) { this.Invoke(new Action<string>(UpdateVersionLabel), newVersion); return; }
versionLabel.Text = String.Format("更新: {0} → {1}", currentVersion, newVersion);
}
#endregion
#region Update process
private async Task StartUpdateProcess()
{
try
{
OnStatusUpdate("正在获取更新信息...");
UpdateCheckResult result = await updateManager.CheckForUpdate(currentVersion);
if (result == null)
{
OnStatusUpdate("获取更新信息失败,请检查网络连接");
Console.WriteLine("ERROR");
await Task.Delay(3000);
Application.Exit();
return;
}
if (!updateConfirmed && !result.has_update)
{
OnStatusUpdate("当前已是最新版本");
Console.WriteLine("OK");
Application.Exit();
return;
}
bool hasHoldFile = !string.IsNullOrEmpty(holdFile) && holdFile != "null";
if (hasHoldFile)
{
CreateHoldFile();
}
OnStatusUpdate("正在关闭相关程序...");
CloseProcesses();
await Task.Delay(500);
UpdateVersionLabel(result.latest_version);
bool success = false;
bool isIncrementalUpdate = false;
switch (result.update_type)
{
case "full":
OnStatusUpdate("正在准备全量更新...");
success = await updateManager.PerformFullUpdate(result);
if (success)
{
OnProgressUpdate(100, "更新完成,正在启动安装程序...", "");
await Task.Delay(1000);
}
break;
case "incremental":
isIncrementalUpdate = true;
OnStatusUpdate("正在准备增量更新...");
success = await updateManager.PerformIncrementalUpdate(result.download_url, installPath);
if (success)
{
OnProgressUpdate(100, "更新完成!", "");
await Task.Delay(1000);
if (hasHoldFile) DeleteHoldFile();
LaunchMainProgram();
}
break;
case "multi_incremental":
isIncrementalUpdate = true;
OnStatusUpdate("正在准备增量更新...");
success = await updateManager.PerformMultiIncrementalUpdate(result.incremental_updates, installPath);
if (success)
{
OnProgressUpdate(100, "更新完成!", "");
await Task.Delay(1000);
if (hasHoldFile) DeleteHoldFile();
LaunchMainProgram();
}
break;
default:
OnStatusUpdate("未知的更新类型: " + result.update_type);
await Task.Delay(3000);
break;
}
if (!success)
{
OnStatusUpdate("更新失败,请稍后重试");
if (hasHoldFile && isIncrementalUpdate) DeleteHoldFile();
await Task.Delay(3000);
}
Application.Exit();
}
catch (Exception ex)
{
OnStatusUpdate("更新出错: " + ex.Message);
Debug.WriteLine("更新错误: " + ex.ToString());
Console.WriteLine("ERROR: " + ex.Message);
System.Threading.Thread.Sleep(5000);
Application.Exit();
}
}
private void CloseProcesses()
{
foreach (string name in processesToClose)
{
try
{
Process[] processes = Process.GetProcessesByName(name);
foreach (Process proc in processes)
{
try
{
proc.Kill();
proc.WaitForExit(5000);
}
catch (Exception ex)
{
Debug.WriteLine("关闭进程失败: " + ex.Message);
}
finally
{
proc.Dispose();
}
}
}
catch (Exception ex)
{
Debug.WriteLine("获取进程失败: " + ex.Message);
}
}
}
private string GetHoldFilePath()
{
return Path.Combine(Path.GetTempPath(), holdFile);
}
private void CreateHoldFile()
{
try
{
string holdPath = GetHoldFilePath();
File.WriteAllText(holdPath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
catch (Exception ex)
{
Debug.WriteLine("创建 hold 文件失败: " + ex.Message);
}
}
private void DeleteHoldFile()
{
try
{
string holdPath = GetHoldFilePath();
if (File.Exists(holdPath))
{
File.Delete(holdPath);
}
}
catch (Exception ex)
{
Debug.WriteLine("删除 hold 文件失败: " + ex.Message);
}
}
private void LaunchMainProgram()
{
if (string.IsNullOrEmpty(launchAfterUpdate)) return;
try
{
string mainExe = Path.Combine(installPath, launchAfterUpdate);
if (File.Exists(mainExe))
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = mainExe;
psi.UseShellExecute = true;
psi.WorkingDirectory = installPath;
Process.Start(psi);
}
else
{
Debug.WriteLine("程序未找到: " + mainExe);
}
}
catch (Exception ex)
{
Debug.WriteLine("启动程序失败: " + ex.Message);
}
}
#endregion
}
}