feat: make Windows Agent configuration GUI-only
Build web service image / build (push) Successful in 40s

This commit is contained in:
2026-09-20 17:25:34 +08:00
parent c7c0ab273f
commit 7bf994de88
12 changed files with 82 additions and 137 deletions
+8 -22
View File
@@ -25,20 +25,8 @@ try
}
if (args[0] == "serve")
{
ValidateOptions(args, 1, ["--config"], []);
var configPath = Path.GetFullPath(GetRequiredOption(args, "--config"));
var serviceOptions = JsonSerializer.Deserialize<ServiceOptions>(
await File.ReadAllTextAsync(configPath, shutdown.Token), ServiceHost.ConfigurationJson)
?? throw new ArgumentException("A service configuration is required.");
await using var app = ServiceHost.Build(serviceOptions, new WindowsAgentBackend(new AccountBindingStore(serviceOptions), serviceOptions),
logPath: Path.Combine(Path.GetDirectoryName(configPath)!, "wxagent.log"));
await app.StartAsync(shutdown.Token);
try { await Task.Delay(Timeout.InfiniteTimeSpan, shutdown.Token); }
catch (OperationCanceledException) when (shutdown.IsCancellationRequested) { }
await app.StopAsync(CancellationToken.None);
return 0;
}
throw new WxAgentException(WxAgentErrorCode.InvalidArgument,
"Windows Agent must be started by double-clicking WxAgent.Tray.exe; configuration is GUI-only.");
var defaultTimeoutSeconds = ValidateCommandLine(args);
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(shutdown.Token);
@@ -1030,14 +1018,12 @@ static object ToMessageOutput(ChatMessageSnapshot message, bool includeContent)
static int CountNodes(UiNodeSnapshot node) => 1 + node.Children.Sum(CountNodes);
static void PrintHelp() => Console.WriteLine("""
WxAgent.Host commands:
serve --config <service.json>
remote auth show|set|clear --config <remote.json>
auth set options: --address <url> --token <token> | --token-file <path> --node <id>
[--server-ca-file <pem>] [--client-certificate-file <pem> --client-certificate-key-file <pem>]
remote status show --config <remote.json> [--data-dir <dir>]
remote probe run --config <remote.json> [--timeout 60]
remote reporting show|enable|disable|account-add|account-enable|account-disable|allow|deny --config <remote.json>
WxAgent.Host commands (diagnostics/read-only only; configuration is GUI-only):
remote auth show --config <remote.json>
remote status show --config <remote.json> [--data-dir <dir>]
remote probe run --config <remote.json> [--timeout 60]
remote reporting show --config <remote.json>
WxAgent.Tray.exe must be started by double-click; use its ... window for configuration.
doctor [--timeout 30]
diagnose --output <dir> [--baseline <ui-tree.json>] [--timeout 60]
inspect-ui --output <path> [--window-title <title>] [--timeout 30]
@@ -9,6 +9,8 @@ internal static class RemoteCliCommands
{
if (args.Length < 3)
throw Invalid("Remote commands require a group and action.");
if (IsConfigurationCommand(args))
throw Invalid("Windows Agent configuration is GUI-only; double-click WxAgent.Tray.exe and use 服务设置... .");
var configPath = Path.GetFullPath(Required(args, "--config"));
var group = args[1];
var action = args[2];
@@ -193,6 +195,13 @@ internal static class RemoteCliCommands
return new { saved = path, accountId = Mask(accountId), chatId = Mask(chatId), type, reporting.ConfigVersion };
}
private static bool IsConfigurationCommand(string[] args) => (args[1], args[2]) switch
{
("auth", "set" or "clear") => true,
("reporting", "enable" or "disable" or "account-add" or "account-enable" or "account-disable" or "allow" or "deny") => true,
_ => false
};
private static ReportingChatType ParseChatType(string value) => value.ToLowerInvariant() switch
{
"group" => ReportingChatType.Group,
@@ -25,6 +25,7 @@ public sealed class ServiceOptions
// Explicitly opt-in for a single, user-authorized Windows validation session.
// Production deployments remain read-only unless this local gate is enabled.
public bool EnableValidationOperations { get; init; }
public bool PreventAutoLock { get; init; }
// Kept only so older service.json files can be loaded and rewritten by the tray.
[JsonIgnore]
+33 -23
View File
@@ -15,22 +15,16 @@ internal static class Program
[STAThread]
private static void Main(string[] args)
{
if (args.Contains("--prevent-auto-lock", StringComparer.OrdinalIgnoreCase))
if (args.Length != 0)
{
PowerPolicy.Apply();
if (args.Contains("--exit", StringComparer.OrdinalIgnoreCase)) return;
MessageBox.Show("请直接双击 WxAgent.Tray.exe 运行;启动参数和命令行配置不受支持。", "WxAgent", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
ApplicationConfiguration.Initialize();
var configPath = GetOption(args, "--config") ?? Path.Combine(AppContext.BaseDirectory, "service.json");
using var context = new TrayApplicationContext(Path.GetFullPath(configPath));
Application.Run(context);
}
private static string? GetOption(string[] args, string name)
{
for (var index = 0; index < args.Length - 1; index++)
if (args[index] == name) return args[index + 1];
return null;
ApplicationConfiguration.Initialize();
var configPath = Path.Combine(AppContext.BaseDirectory, "service.json");
using var context = new TrayApplicationContext(configPath);
Application.Run(context);
}
}
@@ -104,7 +98,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
Visible = true,
ContextMenuStrip = menu
};
trayIcon.DoubleClick += (_, _) => OpenConsole();
trayIcon.DoubleClick += (_, _) => OpenConfig();
try
{
var firstRun = EnsureConfiguration();
@@ -178,7 +172,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
Remote = source.Remote,
Reporting = source.Reporting,
RemoteConfigurationFile = source.RemoteConfigurationFile,
EnableValidationOperations = source.EnableValidationOperations
EnableValidationOperations = source.EnableValidationOperations,
PreventAutoLock = source.PreventAutoLock
};
private ServiceOptions ReadOptions() =>
@@ -191,6 +186,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
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
{
@@ -363,7 +359,7 @@ internal static class ServiceSettingsEditor
{
Text = "WxAgent 服务设置",
Width = 620,
Height = 355,
Height = 405,
StartPosition = FormStartPosition.CenterScreen,
MinimizeBox = false,
MaximizeBox = false,
@@ -377,18 +373,30 @@ internal static class ServiceSettingsEditor
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 = 430,
Text = "启用本机验证写操作(仅授权测试机)",
Checked = current.EnableValidationOperations
};
var preventAutoLock = new CheckBox
{
Left = 145, Top = 187, Width = 430,
Text = "防止自动息屏、睡眠和锁屏",
Checked = current.PreventAutoLock
};
var note = new Label
{
Left = 18, Top = 170, Width = 570, Height = 72,
Text = "仅保留一个访问凭据,可直接输入自定义内容(不能为空或包含空白字符,不限制长度)。\n保存后新凭据立即生效,重新生成会使旧凭据失效。本机回环访问不需要凭据。"
Left = 18, Top = 220, Width = 570, Height = 48,
Text = "仅保留一个访问凭据,可直接输入自定义内容(不能为空或包含空白字符)。\n验证写操作只应在明确授权的测试机启用;本机回环访问不需要凭据。"
};
var data = new Label
{
Left = 18, Top = 246, Width = 570, Height = 24,
Left = 18, Top = 278, Width = 570, Height = 24,
Text = $"数据目录:{current.DataDirectory}", AutoEllipsis = true
};
var save = new Button { Left = 370, Top = 285, Width = 105, Text = "保存" };
var cancel = new Button { Left = 485, Top = 285, Width = 105, Text = "取消" };
var save = new Button { Left = 370, Top = 330, Width = 105, Text = "保存" };
var cancel = new Button { Left = 485, Top = 330, Width = 105, Text = "取消" };
copy.Click += (_, _) => { Clipboard.SetText(tokenBox.Text); copy.Text = "已复制"; };
regenerate.Click += (_, _) =>
{
@@ -415,7 +423,9 @@ internal static class ServiceSettingsEditor
DataDirectory = current.DataDirectory,
Remote = current.Remote,
Reporting = current.Reporting,
RemoteConfigurationFile = current.RemoteConfigurationFile
RemoteConfigurationFile = current.RemoteConfigurationFile,
EnableValidationOperations = validation.Checked,
PreventAutoLock = preventAutoLock.Checked
};
try { edited.Validate(); }
catch (Exception exception)
@@ -427,7 +437,7 @@ internal static class ServiceSettingsEditor
form.Close();
};
cancel.Click += (_, _) => form.Close();
form.Controls.AddRange([listenLabel, hostBox, portBox, external, tokenLabel, tokenBox, copy, regenerate, note, data, save, cancel]);
form.Controls.AddRange([listenLabel, hostBox, portBox, external, tokenLabel, tokenBox, copy, regenerate, validation, preventAutoLock, note, data, save, cancel]);
form.AcceptButton = save;
form.CancelButton = cancel;
form.ShowDialog();