using System.Diagnostics; using System.Net; using System.Text; using System.Text.Json; using System.Windows.Forms; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Logging; using WxAgent.Core; using WxAgent.Host; using WxAgent.Service; namespace WxAgent.Tray; internal static class Program { [STAThread] private static void Main(string[] args) { if (args.Length != 0) { MessageBox.Show("请直接双击 WxAgent.Tray.exe 运行;启动参数和命令行配置不受支持。", "WxAgent", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } ApplicationConfiguration.Initialize(); var configPath = Path.Combine(AppContext.BaseDirectory, "service.json"); using var context = new TrayApplicationContext(configPath); Application.Run(context); } } internal static class PowerPolicy { public static void Apply() { foreach (var arguments in new[] { "/change monitor-timeout-ac 0", "/change standby-timeout-ac 0", "/change hibernate-timeout-ac 0", "/change monitor-timeout-dc 0", "/change standby-timeout-dc 0", "/change hibernate-timeout-dc 0", "/setacvalueindex SCHEME_CURRENT SUB_NONE CONSOLELOCK 0", "/setdcvalueindex SCHEME_CURRENT SUB_NONE CONSOLELOCK 0", "/setactive SCHEME_CURRENT" }) { using var process = Process.Start(new ProcessStartInfo("powercfg.exe", arguments) { CreateNoWindow = true, UseShellExecute = false }); process?.WaitForExit(); } using var desktop = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(@"Control Panel\Desktop"); desktop?.SetValue("ScreenSaveActive", "0"); desktop?.SetValue("ScreenSaveTimeout", "0"); try { using var policy = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Policies\Microsoft\Windows\Personalization"); policy?.SetValue("NoLockScreen", 1, Microsoft.Win32.RegistryValueKind.DWord); } catch (UnauthorizedAccessException) { } } } internal sealed class TrayApplicationContext : ApplicationContext { private readonly string configPath; private readonly Mutex instanceMutex; private readonly NotifyIcon trayIcon; private readonly ToolStripMenuItem serviceToggle; private readonly ToolStripMenuItem reload; private WebApplication? service; private ServiceOptions? options; private bool exiting; public TrayApplicationContext(string configPath) { this.configPath = configPath; instanceMutex = new Mutex(true, "Local\\WxAgent.Tray", out var created); if (!created) { instanceMutex.Dispose(); MessageBox.Show("WxAgent 已经在运行。", "WxAgent", MessageBoxButtons.OK, MessageBoxIcon.Information); throw new InvalidOperationException("Another WxAgent tray instance is already running."); } serviceToggle = new ToolStripMenuItem("启动服务", null, async (_, _) => await ToggleServiceAsync()); reload = new ToolStripMenuItem("重新加载配置", null, async (_, _) => await ReloadAsync()); var menu = new ContextMenuStrip(); var status = new ToolStripMenuItem("正在启动…") { Enabled = false }; menu.Items.Add(status); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("打开工作台", null, (_, _) => OpenConsole())); menu.Items.Add(new ToolStripMenuItem("复制工作台地址", null, (_, _) => CopyConsoleAddress())); menu.Items.Add(serviceToggle); menu.Items.Add(reload); menu.Items.Add(new ToolStripMenuItem("服务设置...", null, (_, _) => OpenConfig())); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add(new ToolStripMenuItem("退出", null, async (_, _) => await ExitAsync())); trayIcon = new NotifyIcon { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application, Text = "WxAgent", Visible = true, ContextMenuStrip = menu }; trayIcon.DoubleClick += (_, _) => OpenConfig(); try { var firstRun = EnsureConfiguration(); StartServiceAsync().GetAwaiter().GetResult(); if (firstRun) OpenConfig(); } catch (Exception exception) { LogError("Tray startup failed.", exception); SetStatus($"启动失败: {exception.Message}"); trayIcon.ShowBalloonTip(5000, "WxAgent 启动失败", exception.Message, ToolTipIcon.Error); } } private ToolStripMenuItem StatusItem => (ToolStripMenuItem)trayIcon.ContextMenuStrip!.Items[0]; private bool EnsureConfiguration() { var directory = Path.GetDirectoryName(configPath)!; Directory.CreateDirectory(directory); if (!File.Exists(configPath)) { var initialToken = ServiceOptions.GenerateToken(); var initial = new ServiceOptions { AccessToken = initialToken, CredentialFile = Path.Combine(directory, "credentials.json"), DataDirectory = Path.Combine(directory, "data") }; Directory.CreateDirectory(initial.DataDirectory); WriteJson(configPath, initial); WriteCredentials(initial.CredentialFile, initialToken, initial.EnableValidationOperations); options = initial; return true; } var loaded = ReadOptions(); var credentialFile = ResolvePath(loaded.CredentialFile, directory, "credentials.json"); var dataDirectory = ResolvePath(loaded.DataDirectory, directory, "data"); var token = ServiceOptions.IsValidAccessToken(loaded.AccessToken) ? loaded.AccessToken! : ServiceOptions.GenerateToken(); var normalized = WithConfiguration(loaded, token, credentialFile, dataDirectory); var changed = !ServiceOptions.IsValidAccessToken(loaded.AccessToken) || !string.Equals(loaded.CredentialFile, credentialFile, StringComparison.OrdinalIgnoreCase) || !string.Equals(loaded.DataDirectory, dataDirectory, StringComparison.OrdinalIgnoreCase); if (changed) WriteJson(configPath, normalized); Directory.CreateDirectory(normalized.DataDirectory); // Always rewrite one canonical record so old/malformed/multi-record files self-heal on tray startup. WriteCredentials(normalized.CredentialFile, token, normalized.EnableValidationOperations); options = normalized; return changed; } private static string ResolvePath(string? value, string baseDirectory, string fallback) { if (string.IsNullOrWhiteSpace(value)) return Path.Combine(baseDirectory, fallback); try { return Path.GetFullPath(value, baseDirectory); } catch (ArgumentException) { return Path.Combine(baseDirectory, fallback); } } private static ServiceOptions WithConfiguration(ServiceOptions source, string token, string credentialFile, string dataDirectory) => new() { ListenUrl = source.ListenUrl, AllowExternal = source.AllowExternal, AllowedHosts = source.AllowedHosts ?? [], AllowedOrigins = source.AllowedOrigins ?? [], AccessToken = token, CredentialFile = credentialFile, DataDirectory = dataDirectory, Remote = source.Remote, Reporting = source.Reporting, RemoteConfigurationFile = source.RemoteConfigurationFile, EnableValidationOperations = source.EnableValidationOperations, EnableListenerEvents = source.EnableListenerEvents, PreventAutoLock = source.PreventAutoLock, EnableDataSync = source.EnableDataSync, DataSyncIntervalSeconds = source.DataSyncIntervalSeconds, DataSyncBatchLimit = source.DataSyncBatchLimit, DataSyncOverlapRows = source.DataSyncOverlapRows, DataSyncQueueMaxItems = source.DataSyncQueueMaxItems, DataSyncQueueMaxBytes = source.DataSyncQueueMaxBytes }; private ServiceOptions ReadOptions() => JsonSerializer.Deserialize(File.ReadAllText(configPath), ServiceHost.ConfigurationJson) ?? throw new InvalidDataException("service.json is empty."); private async Task StartServiceAsync() { if (service is not null) return; var loaded = ReadOptions(); loaded.Validate(); options = loaded; if (loaded.PreventAutoLock) PowerPolicy.Apply(); service = ServiceHost.Build(loaded, new WindowsAgentBackend(new AccountBindingStore(loaded), loaded), logPath: GetLogPath()); try { await service.StartAsync(); SetStatus("服务运行中"); serviceToggle.Text = "停止服务"; reload.Enabled = true; } catch { await service.DisposeAsync(); service = null; throw; } } private async Task StopServiceAsync() { var current = service; service = null; if (current is null) return; try { await current.StopAsync(); } finally { await current.DisposeAsync(); } SetStatus("服务已停止"); serviceToggle.Text = "启动服务"; } private async Task ToggleServiceAsync() { try { if (service is null) await StartServiceAsync(); else await StopServiceAsync(); } catch (Exception exception) { LogError("Service toggle failed.", exception); SetStatus($"操作失败: {exception.Message}"); trayIcon.ShowBalloonTip(5000, "WxAgent", exception.Message, ToolTipIcon.Error); } } private async Task ReloadAsync() { try { await StopServiceAsync(); EnsureConfiguration(); await StartServiceAsync(); trayIcon.ShowBalloonTip(2500, "WxAgent", "配置已重新加载。", ToolTipIcon.Info); } catch (Exception exception) { LogError("Configuration reload failed.", exception); SetStatus($"配置错误: {exception.Message}"); trayIcon.ShowBalloonTip(5000, "WxAgent 配置错误", exception.Message, ToolTipIcon.Error); } } private string GetLogPath() => Path.Combine(Path.GetDirectoryName(configPath)!, "wxagent.log"); private void LogError(string message, Exception exception) => RuntimeLog.Append(GetLogPath(), LogLevel.Error, "WxAgent.Tray", message, exception); private string GetConsoleUrl() { if (!Uri.TryCreate(options?.ListenUrl ?? "http://127.0.0.1:5088", UriKind.Absolute, out var uri)) return "http://127.0.0.1:5088/"; var host = uri.Host; if (IPAddress.TryParse(host.Trim('[', ']'), out var address) && (address.Equals(IPAddress.Any) || address.Equals(IPAddress.IPv6Any))) host = "127.0.0.1"; var formattedHost = host.Contains(':') ? $"[{host.Trim('[', ']')}]" : host; return $"{uri.Scheme}://{formattedHost}:{uri.Port}/"; } private void OpenConsole() { Process.Start(new ProcessStartInfo(GetConsoleUrl()) { UseShellExecute = true }); } private void CopyConsoleAddress() { var url = GetConsoleUrl(); Clipboard.SetText(url); trayIcon.ShowBalloonTip(2000, "WxAgent", $"工作台地址已复制:{url}", ToolTipIcon.Info); } private void OpenConfig() { try { EnsureConfiguration(); var settings = options!; if (!string.IsNullOrWhiteSpace(settings.RemoteConfigurationFile)) { var remotePath = ResolvePath(settings.RemoteConfigurationFile, Path.GetDirectoryName(configPath)!, "remote.json"); var remoteConfiguration = RemoteNodeConfigurationStore.LoadAsync(remotePath).GetAwaiter().GetResult(); settings = WithRemoteForSettings(settings, remoteConfiguration.Remote, remoteConfiguration.Reporting); } if (ServiceSettingsEditor.Show(settings) is not { } edited) return; WriteCredentials(edited.CredentialFile, edited.AccessToken!, edited.EnableValidationOperations); WriteJson(configPath, edited); _ = ReloadAsync(); } catch (Exception exception) { LogError("Service settings save failed.", exception); trayIcon.ShowBalloonTip(5000, "WxAgent 服务设置失败", exception.Message, ToolTipIcon.Error); } } private static ServiceOptions WithRemoteForSettings(ServiceOptions source, RemoteAgentOptions? remote, ReportingConfig reporting) => new() { ListenUrl = source.ListenUrl, AllowExternal = source.AllowExternal, AllowedHosts = source.AllowedHosts ?? [], AllowedOrigins = source.AllowedOrigins ?? [], AccessToken = source.AccessToken, CredentialFile = source.CredentialFile, DataDirectory = source.DataDirectory, Remote = remote, Reporting = reporting, RemoteConfigurationFile = null, EnableValidationOperations = source.EnableValidationOperations, EnableListenerEvents = source.EnableListenerEvents, PreventAutoLock = source.PreventAutoLock, EnableDataSync = source.EnableDataSync, DataSyncIntervalSeconds = source.DataSyncIntervalSeconds, DataSyncBatchLimit = source.DataSyncBatchLimit, DataSyncOverlapRows = source.DataSyncOverlapRows, DataSyncQueueMaxItems = source.DataSyncQueueMaxItems, DataSyncQueueMaxBytes = source.DataSyncQueueMaxBytes }; private void SetStatus(string message) { StatusItem.Text = message.Length > 60 ? message[..60] : message; trayIcon.Text = message.Length > 63 ? message[..63] : message; } private async Task ExitAsync() { if (exiting) return; exiting = true; try { await StopServiceAsync(); } finally { trayIcon.Visible = false; trayIcon.Dispose(); instanceMutex.ReleaseMutex(); instanceMutex.Dispose(); ExitThread(); } } protected override void Dispose(bool disposing) { if (disposing && !exiting) { exiting = true; try { StopServiceAsync().GetAwaiter().GetResult(); } catch { } trayIcon.Visible = false; trayIcon.Dispose(); instanceMutex.ReleaseMutex(); instanceMutex.Dispose(); } base.Dispose(disposing); } private static void WriteCredentials(string path, string token, bool includeValidationWrite) { var temporary = path + ".tmp"; var permissions = includeValidationWrite ? new[] { "read", "content", "write", "manage" } : new[] { "read", "content", "manage" }; WriteJson(temporary, new[] { new ServiceCredential("local-admin", ServiceOptions.HashToken(token), permissions, []) }); File.Move(temporary, path, true); } private static void WriteJson(string path, T value) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, JsonSerializer.Serialize(value, ServiceHost.Json), new UTF8Encoding(false)); } } internal static class ServiceSettingsEditor { public static ServiceOptions? Show(ServiceOptions current) { if (!Uri.TryCreate(current.ListenUrl, UriKind.Absolute, out var uri)) throw new InvalidDataException("ListenUrl is invalid."); static string? Optional(TextBox box) => string.IsNullOrWhiteSpace(box.Text) ? null : box.Text.Trim(); static string[] Lines(TextBox box) => box.Lines .Select(line => line.Trim()) .Where(line => line.Length > 0) .Distinct(StringComparer.Ordinal) .ToArray(); var host = uri.Host.Trim('[', ']'); var token = current.AccessToken ?? ServiceOptions.GenerateToken(); var remote = current.Remote; var reporting = (current.Reporting ?? new ReportingConfig()).NormalizeAndValidate(); var reportingAccount = reporting.Accounts.FirstOrDefault(); var boundAccounts = new AccountBindingStore(current).ReadAll(); var reportingAccountId = reportingAccount?.AccountId ?? (boundAccounts.Count == 1 ? boundAccounts[0].AccountId : ""); ServiceOptions? result = null; using var form = new Form { Text = "WxAgent 服务设置", Width = 720, Height = 805, StartPosition = FormStartPosition.CenterScreen, MinimizeBox = false, MaximizeBox = false, FormBorderStyle = FormBorderStyle.FixedDialog }; var tabs = new TabControl { Left = 12, Top = 12, Width = 680, Height = 720 }; var localTab = new TabPage("本地服务"); var remoteTab = new TabPage("远程连接"); tabs.TabPages.Add(localTab); tabs.TabPages.Add(remoteTab); var listenLabel = new Label { Left = 18, Top = 18, Width = 120, Text = "监听地址" }; var hostBox = new TextBox { Left = 145, Top = 14, Width = 245, Text = host }; var portBox = new NumericUpDown { Left = 400, Top = 14, Width = 90, Minimum = 1, Maximum = 65535, Value = uri.Port }; var external = new CheckBox { Left = 145, Top = 52, Width = 500, Text = "允许远程机器连接(仅在可信内网启用)", Checked = current.AllowExternal }; var tokenLabel = new Label { Left = 18, Top = 91, Width = 120, Text = "访问凭据" }; var tokenBox = new TextBox { Left = 145, Top = 87, Width = 345, Text = token, MaxLength = 0 }; var copy = new Button { Left = 500, Top = 85, Width = 88, Text = "复制" }; var regenerate = new Button { Left = 145, Top = 123, Width = 105, Text = "重新生成" }; var validation = new CheckBox { Left = 145, Top = 158, Width = 500, Text = "启用本机验证写操作(仅授权测试机)", Checked = current.EnableValidationOperations }; var preventAutoLock = new CheckBox { Left = 145, Top = 187, Width = 500, Text = "防止自动息屏、睡眠和锁屏", Checked = current.PreventAutoLock }; var listenerEvents = new CheckBox { Left = 145, Top = 216, Width = 500, Text = "启用后台消息监听(会操作微信界面,默认关闭)", Checked = current.EnableListenerEvents }; var note = new Label { Left = 18, Top = 250, Width = 620, Height = 48, Text = "仅保留一个访问凭据,可直接输入自定义内容(不能为空或包含空白字符)。\n验证写操作只应在明确授权的测试机启用;本机回环访问不需要凭据。" }; var data = new Label { Left = 18, Top = 308, Width = 620, Height = 24, Text = $"数据目录:{current.DataDirectory}", AutoEllipsis = true }; localTab.Controls.AddRange([listenLabel, hostBox, portBox, external, tokenLabel, tokenBox, copy, regenerate, validation, preventAutoLock, listenerEvents, note, data]); var remoteEnabled = new CheckBox { Left = 18, Top = 16, Width = 620, Text = "启用远程连接(连接控制面)", Checked = remote is not null }; var remoteAddressLabel = new Label { Left = 18, Top = 54, Width = 125, Text = "控制面地址" }; var remoteAddressBox = new TextBox { Left = 150, Top = 50, Width = 485, Text = remote?.AuthAddress ?? "" }; var remoteNodeLabel = new Label { Left = 18, Top = 90, Width = 125, Text = "节点 ID" }; var remoteNodeBox = new TextBox { Left = 150, Top = 86, Width = 485, Text = remote?.NodeId ?? "" }; var remoteAccountLabel = new Label { Left = 18, Top = 126, Width = 125, Text = "活动账号 ID" }; var remoteAccountBox = new TextBox { Left = 150, Top = 122, Width = 485, Text = remote?.ActiveAccountId ?? "" }; var remoteTokenLabel = new Label { Left = 18, Top = 162, Width = 125, Text = "控制面令牌" }; var remoteTokenBox = new TextBox { Left = 150, Top = 158, Width = 485, Text = remote?.Token ?? "", UseSystemPasswordChar = true }; var remoteTokenFileLabel = new Label { Left = 18, Top = 198, Width = 125, Text = "令牌文件(可选)" }; var remoteTokenFileBox = new TextBox { Left = 150, Top = 194, Width = 485, Text = remote?.TokenFile ?? "" }; var allowInsecureHttp = new CheckBox { Left = 150, Top = 230, Width = 485, Text = "允许私有网络 HTTP(仅 10/172.16-31/192.168 网段)", Checked = remote?.AllowInsecureHttp == true }; var remoteServerCaLabel = new Label { Left = 18, Top = 266, Width = 125, Text = "服务端 CA(可选)" }; var remoteServerCaBox = new TextBox { Left = 150, Top = 262, Width = 485, Text = remote?.ServerCaFile ?? "" }; var remoteClientCertLabel = new Label { Left = 18, Top = 302, Width = 125, Text = "客户端证书(可选)" }; var remoteClientCertBox = new TextBox { Left = 150, Top = 298, Width = 485, Text = remote?.ClientCertificateFile ?? "" }; var remoteClientKeyLabel = new Label { Left = 18, Top = 338, Width = 125, Text = "客户端密钥(可选)" }; var remoteClientKeyBox = new TextBox { Left = 150, Top = 334, Width = 485, Text = remote?.ClientCertificateKeyFile ?? "" }; var remoteNote = new Label { Left = 18, Top = 372, Width = 620, Height = 40, Text = "控制面令牌与控制面 WXAGENT_NODE_TOKEN 一致;HTTP 仅允许私有 IP,公网或域名请使用 HTTPS。" }; var reportingTitle = new Label { Left = 18, Top = 416, Width = 620, Height = 24, Text = "远程读取白名单(不填写不会默认放行)" }; var reportingEnabled = new CheckBox { Left = 18, Top = 442, Width = 620, Text = "启用 Reporting 数据读取", Checked = reporting.Enabled }; var reportingAccountEnabled = new CheckBox { Left = 150, Top = 470, Width = 485, Text = "启用当前账号", Checked = reportingAccount?.Enabled ?? true }; var reportingAccountLabel = new Label { Left = 18, Top = 505, Width = 125, Text = "账号 ID" }; var reportingAccountBox = new TextBox { Left = 150, Top = 501, Width = 485, Text = reportingAccountId }; var groupChatsLabel = new Label { Left = 18, Top = 541, Width = 125, Text = "群聊白名单" }; var groupChatsBox = new TextBox { Left = 150, Top = 537, Width = 485, Height = 48, Multiline = true, AcceptsReturn = true, ScrollBars = ScrollBars.Vertical, Text = string.Join(Environment.NewLine, reportingAccount?.AllowedChats .Where(chat => chat.Type == ReportingChatType.Group).Select(chat => chat.ChatId) ?? []) }; var privateChatsLabel = new Label { Left = 18, Top = 593, Width = 125, Text = "私聊白名单" }; var privateChatsBox = new TextBox { Left = 150, Top = 589, Width = 485, Height = 48, Multiline = true, AcceptsReturn = true, ScrollBars = ScrollBars.Vertical, Text = string.Join(Environment.NewLine, reportingAccount?.AllowedChats .Where(chat => chat.Type == ReportingChatType.Private).Select(chat => chat.ChatId) ?? []) }; var reportingIdentityConfirmed = new CheckBox { Left = 150, Top = 645, Width = 485, Text = "确认上述 chatId 已与微信身份核对", Checked = reportingAccount is not null && reportingAccount.AllowedChats.Count > 0 && reportingAccount.AllowedChats.All(chat => chat.IdentityVerified) }; var reportingNote = new Label { Left = 18, Top = 674, Width = 620, Height = 36, Text = "每行一个 chatId;通讯录/会话读取只返回白名单范围。" }; remoteTab.Controls.AddRange([ remoteEnabled, remoteAddressLabel, remoteAddressBox, remoteNodeLabel, remoteNodeBox, remoteAccountLabel, remoteAccountBox, remoteTokenLabel, remoteTokenBox, remoteTokenFileLabel, remoteTokenFileBox, allowInsecureHttp, remoteServerCaLabel, remoteServerCaBox, remoteClientCertLabel, remoteClientCertBox, remoteClientKeyLabel, remoteClientKeyBox, remoteNote, reportingTitle, reportingEnabled, reportingAccountEnabled, reportingAccountLabel, reportingAccountBox, groupChatsLabel, groupChatsBox, privateChatsLabel, privateChatsBox, reportingIdentityConfirmed, reportingNote ]); var remoteInputs = new Control[] { remoteAddressBox, remoteNodeBox, remoteAccountBox, remoteTokenBox, remoteTokenFileBox, allowInsecureHttp, remoteServerCaBox, remoteClientCertBox, remoteClientKeyBox }; var reportingInputs = new Control[] { reportingAccountEnabled, reportingAccountBox, groupChatsBox, privateChatsBox, reportingIdentityConfirmed }; void SetRemoteEnabled() { foreach (var control in remoteInputs) control.Enabled = remoteEnabled.Checked; reportingEnabled.Enabled = remoteEnabled.Checked; var reportingInputsEnabled = remoteEnabled.Checked && reportingEnabled.Checked; foreach (var control in reportingInputs) control.Enabled = reportingInputsEnabled; } remoteEnabled.CheckedChanged += (_, _) => SetRemoteEnabled(); reportingEnabled.CheckedChanged += (_, _) => SetRemoteEnabled(); SetRemoteEnabled(); var save = new Button { Left = 470, Top = 740, Width = 105, Text = "保存" }; var cancel = new Button { Left = 585, Top = 740, Width = 105, Text = "取消" }; copy.Click += (_, _) => { Clipboard.SetText(tokenBox.Text); copy.Text = "已复制"; }; regenerate.Click += (_, _) => { if (MessageBox.Show("重新生成并保存后当前访问凭据会立即失效,继续吗?", "WxAgent", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == DialogResult.OK) tokenBox.Text = ServiceOptions.GenerateToken(); }; save.Click += (_, _) => { if (!IPAddress.TryParse(hostBox.Text.Trim().Trim('[', ']'), out var address)) { MessageBox.Show("监听地址必须是 IP 地址。", "WxAgent", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var formattedHost = address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 ? $"[{address}]" : address.ToString(); var remoteToken = Optional(remoteTokenBox); var remoteOptions = remoteEnabled.Checked ? new RemoteAgentOptions { AuthAddress = Optional(remoteAddressBox), Token = remoteToken, TokenFile = remoteToken is null ? Optional(remoteTokenFileBox) : null, ServerCaFile = Optional(remoteServerCaBox), ClientCertificateFile = Optional(remoteClientCertBox), ClientCertificateKeyFile = Optional(remoteClientKeyBox), NodeId = Optional(remoteNodeBox), ActiveAccountId = Optional(remoteAccountBox), AllowInsecureHttp = allowInsecureHttp.Checked } : null; var reportingOptions = reporting; if (remoteEnabled.Checked && reportingEnabled.Checked) { var accountId = Optional(reportingAccountBox); if (accountId is null) { MessageBox.Show("启用 Reporting 时必须填写账号 ID。", "WxAgent 设置无效", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var allowedChats = Lines(groupChatsBox).Select(chatId => new AllowedChat { Type = ReportingChatType.Group, ChatId = chatId, Enabled = true, IdentityVerified = reportingIdentityConfirmed.Checked }).Concat(Lines(privateChatsBox).Select(chatId => new AllowedChat { Type = ReportingChatType.Private, ChatId = chatId, Enabled = true, IdentityVerified = reportingIdentityConfirmed.Checked })).ToArray(); var account = new AccountReportingConfig { AccountId = accountId, Enabled = reportingAccountEnabled.Checked, AllowedChats = allowedChats }; var accounts = reporting.Accounts .Where(existing => !string.Equals(existing.AccountId, accountId, StringComparison.Ordinal)) .Append(account) .ToArray(); reportingOptions = reporting with { Enabled = true, ConfigVersion = checked(reporting.ConfigVersion + 1), Accounts = accounts }; } else if (remoteEnabled.Checked && !reportingEnabled.Checked) { reportingOptions = reporting with { Enabled = false }; } var edited = new ServiceOptions { ListenUrl = $"http://{formattedHost}:{portBox.Value}", AllowExternal = external.Checked, AllowedHosts = current.AllowedHosts, AllowedOrigins = current.AllowedOrigins, AccessToken = tokenBox.Text, CredentialFile = current.CredentialFile, DataDirectory = current.DataDirectory, Remote = remoteOptions, Reporting = reportingOptions, RemoteConfigurationFile = null, EnableValidationOperations = validation.Checked, EnableListenerEvents = listenerEvents.Checked, PreventAutoLock = preventAutoLock.Checked }; try { edited.Validate(); } catch (Exception exception) { MessageBox.Show(exception.Message, "WxAgent 设置无效", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } result = edited; form.Close(); }; cancel.Click += (_, _) => form.Close(); form.Controls.Add(tabs); form.Controls.Add(save); form.Controls.Add(cancel); form.AcceptButton = save; form.CancelButton = cancel; form.ShowDialog(); return result; } }