645 lines
29 KiB
C#
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;
|
|
}
|
|
}
|