Files
tyx_AI_xhs/supervisor/Dada.Supervisor/SupervisorForm.cs
T

320 lines
15 KiB
C#

using System.Diagnostics;
namespace Dada.Supervisor;
internal sealed class SupervisorForm : Form
{
private readonly NotifyIcon trayIcon;
private readonly Label statusTitle;
private readonly Label statusDetail;
private readonly Label apiStatus;
private readonly Label workerStatus;
private readonly Label storageStatus;
private readonly FlowLayoutPanel actions;
private readonly ToolStripMenuItem openMenuItem;
private readonly ToolStripMenuItem diagnosticsMenuItem;
private readonly ToolStripMenuItem restartMenuItem;
private readonly string correlationId = $"SUP-{Guid.NewGuid():N}"[..12].ToUpperInvariant();
private SupervisorState state;
internal SupervisorForm(SupervisorState initialState = SupervisorState.Starting)
{
state = initialState;
AutoScaleMode = AutoScaleMode.Dpi;
BackColor = Color.FromArgb(246, 247, 249);
ClientSize = new Size(480, 480);
Font = new Font("Segoe UI", 9F);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "Dada";
var header = new Panel { BackColor = Color.FromArgb(24, 28, 34), Dock = DockStyle.Top, Height = 126, Padding = new Padding(28, 20, 28, 16) };
var brand = new Label { AutoSize = true, Font = new Font("Segoe UI Semibold", 12F), ForeColor = Color.White, Location = new Point(28, 18), Text = "DADA" };
statusTitle = new Label { AutoEllipsis = true, Font = new Font("Microsoft YaHei UI", 15F, FontStyle.Bold), ForeColor = Color.White, Location = new Point(28, 49), Size = new Size(420, 32) };
statusDetail = new Label { Font = new Font("Microsoft YaHei UI", 9F), ForeColor = Color.FromArgb(181, 188, 198), Location = new Point(28, 82), Size = new Size(420, 38) };
header.Controls.AddRange([brand, statusTitle, statusDetail]);
var componentPanel = new TableLayoutPanel
{
BackColor = Color.White,
ColumnCount = 2,
Dock = DockStyle.Top,
Height = 172,
Padding = new Padding(28, 20, 28, 12),
RowCount = 3,
};
componentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 62));
componentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 38));
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 33));
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 33));
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 34));
apiStatus = AddStatusRow(componentPanel, 0, "本机 API", ApiStatus(initialState));
workerStatus = AddStatusRow(componentPanel, 1, "后台 Worker", WorkerStatus(initialState));
storageStatus = AddStatusRow(componentPanel, 2, "本地数据", StorageStatus(initialState));
actions = new FlowLayoutPanel
{
BackColor = Color.FromArgb(246, 247, 249),
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
Padding = new Padding(24, 24, 20, 16),
WrapContents = true,
};
Controls.Add(actions);
Controls.Add(componentPanel);
Controls.Add(header);
var menu = new ContextMenuStrip();
openMenuItem = new ToolStripMenuItem("打开 Dada", null, (_, _) => OpenProduct());
menu.Items.Add(openMenuItem);
menu.Items.Add("运行状态", null, (_, _) => RestoreWindow());
diagnosticsMenuItem = new ToolStripMenuItem("打开诊断", null, (_, _) => ShowDiagnostics());
menu.Items.Add(diagnosticsMenuItem);
restartMenuItem = new ToolStripMenuItem("重新启动服务", null, (_, _) =>
{
SetState(SupervisorState.Starting);
RestartRequested?.Invoke();
});
menu.Items.Add(restartMenuItem);
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("退出 Dada", null, (_, _) => Close());
trayIcon = new NotifyIcon
{
ContextMenuStrip = menu,
Icon = SystemIcons.Application,
Text = TrayText(initialState),
Visible = !SystemInformation.UserInteractive ? false : true,
};
trayIcon.DoubleClick += (_, _) => RestoreWindow();
FormClosing += (_, _) => trayIcon.Visible = false;
Resize += (_, _) =>
{
if (WindowState == FormWindowState.Minimized) Hide();
};
SetState(initialState);
}
internal event Action? RestartRequested;
internal IReadOnlyList<string> TrayMenuLabels => trayIcon.ContextMenuStrip?.Items
.OfType<ToolStripMenuItem>()
.Select(item => item.Text ?? string.Empty)
.ToArray() ?? [];
internal void SetState(SupervisorState nextState)
{
state = nextState;
(statusTitle.Text, statusDetail.Text) = nextState switch
{
SupervisorState.Starting => ("正在启动本机服务", "请稍候,Dada 正在检查 API、Worker 和本地数据。"),
SupervisorState.Ready => ("Dada 已就绪", "本机服务运行正常。"),
SupervisorState.StartupFailed => ("Dada 启动失败", $"服务未能启动 · {DateTimeOffset.Now:HH:mm:ss} · 关联 ID {correlationId}\n诊断信息已移除敏感内容。"),
SupervisorState.PortInUse => ("Dada 无法使用固定端口启动", "固定端口 43121 已被占用,Dada 不会改用其他端口。"),
SupervisorState.ApiDegraded => ("本机 API 不可用", "网页暂时无法使用,可打开诊断或重新启动服务。"),
SupervisorState.WorkerDegraded => ("后台服务暂时不可用", "可继续查看、下载、编辑和删除;暂不能创建新任务。"),
SupervisorState.StorageFull => ("本地存储空间已满", "可继续查看和导出;清理空间后再创建新内容。"),
SupervisorState.StorageUnavailable => ("本地数据暂时不可用", "请恢复已配置的数据目录,然后重试。"),
SupervisorState.DiagnosticsRunning => ("正在运行诊断", "正在检查本机服务状态。"),
SupervisorState.DiagnosticsReady => ("诊断已完成", "可复制已脱敏的诊断结果。"),
_ => throw new ArgumentOutOfRangeException(nameof(nextState)),
};
trayIcon.Text = TrayText(nextState);
SetStatusLabel(apiStatus, ApiStatus(nextState));
SetStatusLabel(workerStatus, WorkerStatus(nextState));
SetStatusLabel(storageStatus, StorageStatus(nextState));
var allowed = SupervisorActions.For(nextState);
openMenuItem.Enabled = allowed.Contains(SupervisorAction.OpenProduct);
diagnosticsMenuItem.Enabled = allowed.Contains(SupervisorAction.OpenDiagnostics);
restartMenuItem.Visible = allowed.Contains(SupervisorAction.RestartServices);
RenderActions();
}
internal void RestoreWindow()
{
Show();
WindowState = FormWindowState.Normal;
Activate();
}
protected override void Dispose(bool disposing)
{
if (disposing) trayIcon.Dispose();
base.Dispose(disposing);
}
private void RenderActions()
{
actions.SuspendLayout();
actions.Controls.Clear();
foreach (var action in SupervisorActions.For(state))
{
var button = new Button
{
AutoSize = false,
BackColor = action is SupervisorAction.Retry or SupervisorAction.OpenProduct ? Color.FromArgb(28, 32, 38) : Color.White,
FlatStyle = FlatStyle.Flat,
ForeColor = action is SupervisorAction.Retry or SupervisorAction.OpenProduct ? Color.White : Color.FromArgb(33, 37, 43),
Height = 38,
Margin = new Padding(4),
Text = ActionLabel(action),
Width = action switch
{
SupervisorAction.OpenChrome => 132,
SupervisorAction.OpenEdge => 122,
SupervisorAction.OpenDiagnostics => 122,
SupervisorAction.CheckExistingInstance => 132,
SupervisorAction.RestartServices => 122,
_ => 112,
},
};
button.FlatAppearance.BorderColor = Color.FromArgb(215, 219, 225);
button.Click += (_, _) => InvokeAction(action);
actions.Controls.Add(button);
}
actions.ResumeLayout();
}
private void InvokeAction(SupervisorAction action)
{
switch (action)
{
case SupervisorAction.OpenProduct:
OpenProduct();
break;
case SupervisorAction.OpenChrome:
SupportedBrowserLauncher.OpenChrome(LoopbackEndpoint.ProductUri);
break;
case SupervisorAction.OpenEdge:
SupportedBrowserLauncher.OpenEdge(LoopbackEndpoint.ProductUri);
break;
case SupervisorAction.OpenDiagnostics:
case SupervisorAction.CheckExistingInstance:
ShowDiagnostics();
break;
case SupervisorAction.Retry:
case SupervisorAction.RestartServices:
SetState(SupervisorState.Starting);
RestartRequested?.Invoke();
break;
case SupervisorAction.Exit:
Close();
break;
}
}
internal static bool OpenProductInSupportedBrowser() => SupportedBrowserLauncher.OpenDefaultSupported(LoopbackEndpoint.ProductUri);
private static void OpenProduct() => OpenProductInSupportedBrowser();
private void ShowDiagnostics()
{
using var diagnostics = new DiagnosticsForm(state);
diagnostics.ShowDialog(this);
}
private static Label AddStatusRow(TableLayoutPanel panel, int row, string label, string status)
{
panel.Controls.Add(new Label { Anchor = AnchorStyles.Left, AutoSize = true, Font = new Font("Microsoft YaHei UI", 10F), Text = label }, 0, row);
var statusLabel = new Label { Anchor = AnchorStyles.Right, AutoSize = true, Font = new Font("Microsoft YaHei UI", 9F), Text = status };
SetStatusLabel(statusLabel, status);
panel.Controls.Add(statusLabel, 1, row);
return statusLabel;
}
private static void SetStatusLabel(Label label, string status)
{
label.Text = status;
label.ForeColor = status is "可用" or "正常" ? Color.FromArgb(28, 122, 76) : Color.FromArgb(180, 86, 32);
}
private static string ApiStatus(SupervisorState value) => value is SupervisorState.ApiDegraded or SupervisorState.StartupFailed or SupervisorState.PortInUse ? "不可用" : value == SupervisorState.Starting ? "启动中" : "正常";
private static string WorkerStatus(SupervisorState value) => value is SupervisorState.WorkerDegraded or SupervisorState.StartupFailed or SupervisorState.PortInUse ? "不可用" : value == SupervisorState.Starting ? "启动中" : "正常";
private static string TrayText(SupervisorState value) => value switch
{
SupervisorState.Starting => "Dada - 正在启动",
SupervisorState.Ready => "Dada - 运行正常",
SupervisorState.ApiDegraded => "Dada - API 不可用",
SupervisorState.WorkerDegraded => "Dada - Worker 不可用",
SupervisorState.StorageFull => "Dada - 存储空间已满",
SupervisorState.StorageUnavailable => "Dada - 本地数据不可用",
SupervisorState.PortInUse => "Dada - 固定端口被占用",
SupervisorState.StartupFailed => "Dada - 启动失败",
_ => "Dada - 运行诊断",
};
private static string StorageStatus(SupervisorState value) => value switch
{
SupervisorState.StorageFull => "已满",
SupervisorState.StorageUnavailable => "不可用",
_ => "可用",
};
private static string ActionLabel(SupervisorAction action) => action switch
{
SupervisorAction.OpenProduct => "打开 Dada",
SupervisorAction.OpenChrome => "使用 Chrome 打开",
SupervisorAction.OpenEdge => "使用 Edge 打开",
SupervisorAction.OpenDiagnostics => "打开诊断",
SupervisorAction.CheckExistingInstance => "检查已有 Dada",
SupervisorAction.Retry => "重试",
SupervisorAction.RestartServices => "重新启动服务",
SupervisorAction.Exit => "退出 Dada",
_ => throw new ArgumentOutOfRangeException(nameof(action)),
};
}
internal sealed class DiagnosticsForm : Form
{
internal string CopyPayload { get; }
internal DiagnosticsForm(SupervisorState state)
{
BackColor = Color.White;
ClientSize = new Size(760, 440);
Font = new Font("Microsoft YaHei UI", 9F);
StartPosition = FormStartPosition.CenterParent;
Text = "Dada 运行诊断";
var title = new Label { AutoSize = true, Font = new Font("Microsoft YaHei UI", 15F, FontStyle.Bold), Location = new Point(28, 24), Text = "运行诊断" };
var detail = new Label { ForeColor = Color.FromArgb(91, 97, 105), Location = new Point(30, 62), Size = new Size(700, 42), Text = "诊断仅包含服务状态、固定端口、目录可用性和凭据是否已配置,不包含凭据值或用户内容。" };
var checks = new ListView { Location = new Point(30, 116), Size = new Size(700, 250), View = View.Details, FullRowSelect = true };
checks.Columns.Add("组件", 170);
checks.Columns.Add("状态", 110);
checks.Columns.Add("检查结果", 390);
var report = DiagnosticReport.Create(
state,
[
state == SupervisorState.StorageUnavailable
? DiagnosticCheck.Fail("log_writable", "log_write_failed")
: DiagnosticCheck.Pass("log_writable", "log_ready"),
DiagnosticCheck.Pass("fixed_port", "loopback_only"),
],
new DiagnosticStorageSummary(0, 0, 0),
[]);
CopyPayload = System.Text.Json.JsonSerializer.Serialize(report, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
checks.Items.Add(new ListViewItem(["Supervisor", "正常", StateLabel(state)]));
checks.Items.Add(new ListViewItem(["固定端口", "43121", "仅绑定 127.0.0.1"]));
checks.Items.Add(new ListViewItem(["凭据", "已脱敏", "仅显示是否已配置"]));
var copy = new Button { Location = new Point(592, 382), Size = new Size(138, 36), Text = "复制脱敏结果" };
copy.Click += (_, _) => Clipboard.SetText(CopyPayload);
Controls.AddRange([title, detail, checks, copy]);
}
private static string StateLabel(SupervisorState state) => state switch
{
SupervisorState.Starting => "正在启动",
SupervisorState.Ready => "运行正常",
SupervisorState.StartupFailed => "启动失败",
SupervisorState.PortInUse => "固定端口被占用",
SupervisorState.ApiDegraded => "API 不可用",
SupervisorState.WorkerDegraded => "Worker 不可用",
SupervisorState.StorageFull => "存储空间已满",
SupervisorState.StorageUnavailable => "本地数据不可用",
SupervisorState.DiagnosticsRunning => "诊断中",
SupervisorState.DiagnosticsReady => "诊断完成",
_ => "未知",
};
}