Files
wx-win-agent/node-agent/WxAgent.Tray/Program.cs
T

645 lines
29 KiB
C#

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<ServiceOptions>(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<T>(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();
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 = "远程读取授权(Agent 连接后自动授予已验证账号和会话)"
};
var reportingEnabled = new CheckBox
{
Left = 18, Top = 442, Width = 620,
Text = "使用 Agent 连接授权读取(无需单独确认)",
Checked = true,
Enabled = false
};
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 = "连接成功即授权当前已验证账号的通讯录、会话和消息读取;下方旧范围仅为兼容显示。"
};
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 = false;
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 = remoteEnabled.Checked
? reporting with { Enabled = true, ConfigVersion = checked(Math.Max(1, reporting.ConfigVersion) + 1) }
: reporting;
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;
}
}