zcbot/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs

375 lines
14 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

namespace Zcbot.WindowsNode;
internal sealed class ConfigurationForm : Form
{
private const int ContentWidth = 760;
private readonly TextBox server = CreateTextBox("http://127.0.0.1:8765");
private readonly TextBox nodeName = CreateTextBox(Environment.MachineName.ToLowerInvariant());
private readonly TextBox enrollmentCode = CreateTextBox(usePassword: true);
private readonly Button register = CreateButton("注册并连接", 128, primary: true);
private readonly Button reconnect = CreateButton("立即重连", 112, primary: true);
private readonly Button resetIdentity = CreateButton("清除本机身份并重新注册", 220);
private readonly CheckBox startAtLogin = new()
{
Text = "登录 Windows 后自动启动节点",
AutoSize = true,
Margin = new Padding(0, 4, 0, 8),
};
private readonly Label state = new()
{
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 13, FontStyle.Bold),
Margin = new Padding(0, 2, 0, 5),
};
private readonly Label detail = CreateBodyLabel();
private readonly Label identity = CreateBodyLabel();
private readonly Label capabilitySummary = CreateBodyLabel();
private readonly TableLayoutPanel registrationCard;
private bool changingStartup;
internal event Func<EnrollOptions, Task>? RegisterRequested;
internal event Action? ReconnectRequested;
internal event Action? ResetIdentityRequested;
internal ConfigurationForm()
{
Text = "zcbot Windows Node";
AutoScaleMode = AutoScaleMode.Dpi;
ClientSize = new Size(840, 680);
MinimumSize = new Size(820, 640);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 9);
FormBorderStyle = FormBorderStyle.Sizable;
BackColor = Color.FromArgb(241, 245, 249);
var shell = new TableLayoutPanel
{
Dock = DockStyle.Fill,
AutoScroll = true,
ColumnCount = 3,
RowCount = 1,
Padding = new Padding(0, 22, 0, 22),
BackColor = BackColor,
};
shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
shell.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ContentWidth));
shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
Controls.Add(shell);
var page = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Dock = DockStyle.Top,
ColumnCount = 1,
RowCount = 5,
BackColor = BackColor,
};
page.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
shell.Controls.Add(page, 1, 0);
var heading = new TableLayoutPanel
{
AutoSize = true,
Dock = DockStyle.Top,
ColumnCount = 1,
Margin = new Padding(4, 0, 4, 18),
};
heading.Controls.Add(new Label
{
Text = "zcbot Windows Node",
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 20, FontStyle.Bold),
ForeColor = Color.FromArgb(15, 23, 42),
});
heading.Controls.Add(CreateHint("连接本机科研软件与 zcbot 的受控执行节点"));
page.Controls.Add(heading);
var statusCard = CreateCard();
statusCard.Controls.Add(CreateSectionTitle("节点"));
statusCard.Controls.Add(state);
statusCard.Controls.Add(detail);
statusCard.Controls.Add(identity);
statusCard.Controls.Add(CreateDivider());
statusCard.Controls.Add(CreateCapabilityRow("Origin 绘图", "origin.plot@v1"));
statusCard.Controls.Add(capabilitySummary);
var resetActions = CreateActions();
resetActions.Controls.Add(reconnect);
resetActions.Controls.Add(resetIdentity);
statusCard.Controls.Add(resetActions);
page.Controls.Add(statusCard);
registrationCard = CreateCard();
registrationCard.Controls.Add(CreateSectionTitle("首次注册"));
registrationCard.Controls.Add(CreateHint(
"从 zcbot 管理后台生成注册码。注册码默认 10 分钟有效,成功注册一次后立即失效。"));
AddField(registrationCard, "zcbot 服务地址", server, "云端填写站点根地址;本机测试使用 http://127.0.0.1:8765");
AddField(registrationCard, "节点名称", nodeName, "必须与注册码限定的节点名称完全一致");
AddField(registrationCard, "一次性注册码", enrollmentCode, "格式类似 ZCN-…;仅用于首次注册,不会长期保存");
var registerActions = CreateActions();
registerActions.Controls.Add(register);
registrationCard.Controls.Add(registerActions);
page.Controls.Add(registrationCard);
var runtimeCard = CreateCard();
runtimeCard.Controls.Add(CreateSectionTitle("运行设置"));
runtimeCard.Controls.Add(startAtLogin);
runtimeCard.Controls.Add(CreateHint(
"关闭此窗口后,节点仍在系统托盘运行。右键托盘图标可以立即重连或退出。"));
page.Controls.Add(runtimeCard);
var securityNote = CreateHint(
"安全说明Node Token 由 Windows DPAPI 加密保存,不在界面显示,也不会写入日志。");
securityNote.Margin = new Padding(4, 8, 4, 0);
page.Controls.Add(securityNote);
register.Click += async (_, _) => await RegisterAsync();
reconnect.Click += (_, _) => ReconnectRequested?.Invoke();
resetIdentity.Click += (_, _) => ResetIdentity();
startAtLogin.CheckedChanged += (_, _) => ToggleStartup();
FormClosing += (_, eventArgs) =>
{
if (eventArgs.CloseReason == CloseReason.UserClosing)
{
eventArgs.Cancel = true;
Hide();
}
};
startAtLogin.Checked = StartupRegistration.IsEnabled;
ApplyStatus(NodeStatus.Create(NodeState.NotRegistered, "尚未注册"), null);
}
internal void ApplyStatus(NodeStatus status, NodeConfig? config)
{
state.Text = status.State switch
{
NodeState.Online => "● 在线",
NodeState.Connecting => "● 正在连接",
NodeState.NotRegistered => "● 尚未注册",
NodeState.AuthenticationRequired => "● 需要重新注册",
NodeState.Offline => "● 离线",
_ => "● 已停止",
};
state.ForeColor = status.State switch
{
NodeState.Online => Color.ForestGreen,
NodeState.Connecting => Color.DarkOrange,
NodeState.NotRegistered or NodeState.AuthenticationRequired => Color.Firebrick,
_ => Color.DimGray,
};
detail.Text = $"{status.Message} {status.ChangedAt:HH:mm:ss}";
identity.Text = config is null
? "尚未建立节点身份"
: $"节点:{config.NodeName}\nNode ID{config.NodeId}\n服务{config.ServerUrl}";
capabilitySummary.Text = config is null
? "注册后启用"
: FormatOriginStatus(OriginRuntimeProbe.Detect());
var registered = config is not null;
registrationCard.Visible = !registered;
reconnect.Visible = registered;
resetIdentity.Visible = registered;
register.Enabled = !registered && status.State != NodeState.Connecting;
if (registered)
{
server.Text = config!.ServerUrl.AbsoluteUri.TrimEnd('/');
nodeName.Text = config.NodeName;
enrollmentCode.Clear();
}
}
private static string FormatOriginStatus(OriginRuntimeStatus origin)
{
var version = string.IsNullOrWhiteSpace(origin.SoftwareVersion)
? "版本未知"
: $"版本 {origin.SoftwareVersion}";
var state = origin.Health == "ready" ? "可用" : "不可用";
return $"{state} · {version}\n{origin.Detail}";
}
private void ResetIdentity()
{
var answer = MessageBox.Show(
"这会删除本机加密身份,随后需要使用新注册码重新注册。请先在管理后台删除或禁用云端旧节点。是否继续?",
"清除本机节点身份", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (answer == DialogResult.Yes)
{
ResetIdentityRequested?.Invoke();
}
}
private async Task RegisterAsync()
{
if (RegisterRequested is null)
{
return;
}
try
{
register.Enabled = false;
detail.Text = "正在注册…";
var options = EnrollOptions.Parse([
"--server", server.Text, "--name", nodeName.Text, "--code", enrollmentCode.Text]);
await RegisterRequested(options);
}
catch (Exception exception) when (exception is NodeConfigurationException or HttpRequestException)
{
MessageBox.Show(exception.Message, "注册失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
register.Enabled = true;
}
}
private void ToggleStartup()
{
if (changingStartup)
{
return;
}
try
{
StartupRegistration.SetEnabled(startAtLogin.Checked);
}
catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
{
MessageBox.Show(exception.Message, "自动启动设置失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
changingStartup = true;
try
{
startAtLogin.Checked = StartupRegistration.IsEnabled;
}
finally
{
changingStartup = false;
}
}
}
private static TableLayoutPanel CreateCard()
{
var card = new TableLayoutPanel
{
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Dock = DockStyle.Top,
ColumnCount = 1,
Padding = new Padding(20, 17, 20, 17),
Margin = new Padding(0, 0, 0, 14),
BackColor = Color.White,
};
card.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
return card;
}
private static Label CreateSectionTitle(string text) => new()
{
Text = text,
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 11, FontStyle.Bold),
ForeColor = Color.FromArgb(30, 41, 59),
Margin = new Padding(0, 0, 0, 8),
};
private static Label CreateBodyLabel() => new()
{
AutoSize = true,
Dock = DockStyle.Fill,
ForeColor = Color.FromArgb(71, 85, 105),
MaximumSize = new Size(ContentWidth - 40, 0),
Margin = new Padding(0, 2, 0, 5),
};
private static Label CreateHint(string text) => new()
{
Text = text,
AutoSize = true,
Dock = DockStyle.Fill,
ForeColor = Color.FromArgb(100, 116, 139),
Margin = new Padding(0, 2, 0, 4),
};
private static Panel CreateDivider() => new()
{
Height = 1,
Dock = DockStyle.Top,
BackColor = Color.FromArgb(226, 232, 240),
Margin = new Padding(0, 10, 0, 12),
};
private static Panel CreateCapabilityRow(string title, string protocol)
{
var row = new Panel
{
AutoSize = false,
Height = 36,
Dock = DockStyle.Top,
BackColor = Color.FromArgb(239, 246, 255),
Margin = new Padding(0, 0, 0, 5),
Padding = new Padding(11, 7, 11, 7),
};
row.Controls.Add(new Label
{
Text = $"{title} · {protocol}",
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold),
ForeColor = Color.FromArgb(29, 78, 216),
});
return row;
}
private static TextBox CreateTextBox(string text = "", bool usePassword = false) => new()
{
Text = text,
UseSystemPasswordChar = usePassword,
AutoSize = false,
Height = 34,
BorderStyle = BorderStyle.FixedSingle,
Font = new Font("Segoe UI", 10),
};
private static Button CreateButton(string text, int width, bool primary = false)
{
var button = new Button
{
Text = text,
AutoSize = false,
Size = new Size(width, 36),
FlatStyle = FlatStyle.Flat,
BackColor = primary ? Color.FromArgb(37, 99, 235) : Color.White,
ForeColor = primary ? Color.White : Color.FromArgb(51, 65, 85),
Margin = new Padding(0, 0, 10, 0),
};
button.FlatAppearance.BorderColor = primary
? Color.FromArgb(37, 99, 235)
: Color.FromArgb(203, 213, 225);
return button;
}
private static FlowLayoutPanel CreateActions() => new()
{
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
Dock = DockStyle.Fill,
Margin = new Padding(0, 10, 0, 0),
};
private static void AddField(
TableLayoutPanel layout, string label, Control control, string hint)
{
layout.Controls.Add(new Label
{
Text = label,
AutoSize = true,
Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold),
ForeColor = Color.FromArgb(51, 65, 85),
Margin = new Padding(0, 8, 0, 4),
});
control.Dock = DockStyle.Top;
control.Margin = new Padding(0, 0, 0, 2);
layout.Controls.Add(control);
layout.Controls.Add(CreateHint(hint));
}
}