feat: add tray app and Setup.exe installer

This commit is contained in:
2026-09-08 07:50:04 +08:00
parent 49faf16b83
commit c5314350bf
9 changed files with 382 additions and 130 deletions
+7
View File
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WxAgent.Service", "src\WxAg
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WxAgent.Service.Tests", "tests\WxAgent.Service.Tests\WxAgent.Service.Tests.csproj", "{99273C3C-2BCE-431A-AEFA-EFBDEE5ECAFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WxAgent.Tray", "src\WxAgent.Tray\WxAgent.Tray.csproj", "{FCDBB182-E1EB-44C4-8DB8-20BF2913E929}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -52,6 +54,10 @@ Global
{99273C3C-2BCE-431A-AEFA-EFBDEE5ECAFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99273C3C-2BCE-431A-AEFA-EFBDEE5ECAFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99273C3C-2BCE-431A-AEFA-EFBDEE5ECAFB}.Release|Any CPU.Build.0 = Release|Any CPU
{FCDBB182-E1EB-44C4-8DB8-20BF2913E929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCDBB182-E1EB-44C4-8DB8-20BF2913E929}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCDBB182-E1EB-44C4-8DB8-20BF2913E929}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCDBB182-E1EB-44C4-8DB8-20BF2913E929}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{EDC68F82-2F67-40A5-B6CA-B4D26349E988} = {11E9B433-75E3-446D-B8FF-958A0F541BA5}
@@ -60,5 +66,6 @@ Global
{1F24FB68-6917-4984-89FC-5F9C64E2965D} = {CE7C3E9D-F9B0-4A30-AA6F-A645D75888D9}
{8280C39E-328E-4321-8D96-238405B5F19D} = {11E9B433-75E3-446D-B8FF-958A0F541BA5}
{99273C3C-2BCE-431A-AEFA-EFBDEE5ECAFB} = {CE7C3E9D-F9B0-4A30-AA6F-A645D75888D9}
{FCDBB182-E1EB-44C4-8DB8-20BF2913E929} = {11E9B433-75E3-446D-B8FF-958A0F541BA5}
EndGlobalSection
EndGlobal
+2 -7
View File
@@ -17,14 +17,9 @@
WxAgent.Host.exe serve --config C:\Users\USERNAME\wx-agent\service.json
```
需要安装为当前用户登录后自动启动、创建桌面快捷方式并打开控制台时,使用发布目录中的脚本:
需要正常安装、登录后自动启动、创建桌面/开始菜单快捷方式并常驻托盘时,运行发布包中的 `WxAgent-Setup.exe`。安装器会创建当前用户的登录启动项,不需要 PowerShell 或管理员权限;首次启动托盘程序时生成本地管理 Token,并在一次性窗口中提供复制按钮。
```powershell
Set-ExecutionPolicy -Scope Process Bypass
.\scripts\windows\Install-WxAgent.ps1 -InstallRoot C:\Users\USERNAME\wx-agent -Start -OpenBrowser -PreventAutoLock
```
脚本不会覆盖已有 `credentials.json` 或 `service.json`;首次安装且没有凭据时生成一次本地管理 Token,仅在终端显示、不写入明文文件。`-PreventAutoLock` 会关闭当前电源计划的自动息屏/睡眠/休眠和屏保,并设置 Windows 锁屏策略;不阻止用户手动锁定。默认只监听 `127.0.0.1:5088`。外部监听必须同时设置 `AllowExternal=true`、明确 IP/端口和精确 `AllowedHosts`/`AllowedOrigins`;自行配置防火墙,服务不会自动开放端口。HTTP 不加密 Token、Cookie、消息或附件,不直接暴露公网。
服务运行后可在托盘菜单中打开控制台、打开 `service.json`、停止/启动服务或重新加载配置。若需要保持微信 UI 会话不自动锁屏,在安装器的可选项中启用“防止自动锁屏”;该设置不阻止用户手动锁定。默认只监听 `127.0.0.1:5088`。外部监听必须同时设置 `AllowExternal=true`、明确 IP/端口和精确 `AllowedHosts`/`AllowedOrigins`;自行配置防火墙,服务不会自动开放端口。HTTP 不加密 Token、Cookie、消息或附件,不直接暴露公网。
浏览器访问 `/`,输入 Token 登录。HTTP/MCP 客户端使用:
+4 -1
View File
@@ -55,7 +55,8 @@ WxAgent.sln
├── src/
│ ├── WxAgent.Core/ # net8.0,模型、解析、去重、状态机、业务用例
│ ├── WxAgent.Windows/ # net8.0-windows,FlaUI/UIA/Win32 实现
│ └── WxAgent.Host/ # net8.0-windows,Desktop Agent 和 CLI
│ ├── WxAgent.Host/ # net8.0-windows,Desktop Agent 和 CLI
│ └── WxAgent.Tray/ # net8.0-windows,托盘常驻、配置和服务生命周期
└── tests/
└── WxAgent.Core.Tests/ # net8.0,可在 Linux 执行
```
@@ -127,6 +128,8 @@ dotnet publish src/WxAgent.Host \
```text
src/WxAgent.Host/bin/Release/net8.0-windows10.0.19041.0/win-x64/publish/
src/WxAgent.Tray/bin/Release/net8.0-windows10.0.19041.0/win-x64/publish/
installer/WxAgent.iss → WxAgent-Setup.exe
```
### 5.3 Windows 真机验证流程
+48
View File
@@ -0,0 +1,48 @@
#define AppVersion "0.1.0"
#define PublishRoot "..\artifacts\installer-publish"
[Setup]
AppId={{8B2EAF78-2B11-4E55-9C39-7A6E4E7A4B7D}
AppName=WxAgent
AppVersion={#AppVersion}
AppPublisher=WxAgent
DefaultDirName={localappdata}\WxAgent
DefaultGroupName=WxAgent
OutputDir=..\artifacts\setup
OutputBaseFilename=WxAgent-Setup
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=lowest
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
CloseApplications=yes
RestartApplications=no
Uninstallable=yes
[Files]
Source: "{#PublishRoot}\WxAgent.Tray.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#PublishRoot}\WxAgent.Host.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#PublishRoot}\wwwroot\*"; DestDir: "{app}\wwwroot"; Flags: ignoreversion recursesubdirs createallsubdirs
[Tasks]
Name: "autostart"; Description: "登录 Windows 后自动启动 WxAgent"; GroupDescription: "启动选项:"; Flags: checkedonce
Name: "preventautolock"; Description: "防止自动息屏、睡眠、休眠和锁屏"; GroupDescription: "会话选项:"; Flags: unchecked
[Icons]
Name: "{autoprograms}\WxAgent"; Filename: "{app}\WxAgent.Tray.exe"; Parameters: "--config ""{app}\service.json"""; WorkingDir: "{app}"
Name: "{userdesktop}\WxAgent"; Filename: "{app}\WxAgent.Tray.exe"; Parameters: "--config ""{app}\service.json"""; WorkingDir: "{app}"
Name: "{userstartup}\WxAgent"; Filename: "{app}\WxAgent.Tray.exe"; Parameters: "--config ""{app}\service.json"""; WorkingDir: "{app}"; Tasks: autostart
[Run]
Filename: "{sys}\schtasks.exe"; Parameters: "/delete /tn ""WxAgent"" /f"; Flags: runhidden waituntilterminated
Filename: "{sys}\taskkill.exe"; Parameters: "/IM WxAgent.Host.exe /F"; Flags: runhidden waituntilterminated
Filename: "{sys}\taskkill.exe"; Parameters: "/IM WxAgent.Tray.exe /F"; Flags: runhidden waituntilterminated
Filename: "{app}\WxAgent.Tray.exe"; Parameters: "--prevent-auto-lock --exit"; WorkingDir: "{app}"; Description: "配置会话防止自动锁屏"; Flags: runhidden waituntilterminated; Tasks: preventautolock
Filename: "{app}\WxAgent.Tray.exe"; Parameters: "--config ""{app}\service.json"""; WorkingDir: "{app}"; Description: "启动 WxAgent 托盘程序"; Flags: nowait postinstall skipifsilent
[UninstallRun]
Filename: "{sys}\schtasks.exe"; Parameters: "/delete /tn ""WxAgent"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "DeleteWxAgentTask"
Filename: "{sys}\taskkill.exe"; Parameters: "/IM WxAgent.Tray.exe /F"; Flags: runhidden; RunOnceId: "StopWxAgentTray"
Filename: "{sys}\taskkill.exe"; Parameters: "/IM WxAgent.Host.exe /F"; Flags: runhidden; RunOnceId: "StopWxAgentHost"
-120
View File
@@ -1,120 +0,0 @@
[CmdletBinding()]
param(
[string]$InstallRoot = (Join-Path $env:USERPROFILE 'wx-agent'),
[string]$TaskName = 'WxAgent',
[switch]$Start,
[switch]$OpenBrowser,
[switch]$PreventAutoLock,
[string]$Token
)
$ErrorActionPreference = 'Stop'
$exe = Join-Path $InstallRoot 'WxAgent.Host.exe'
$config = Join-Path $InstallRoot 'service.json'
$credentials = Join-Path $InstallRoot 'credentials.json'
$data = Join-Path $InstallRoot 'data'
$url = 'http://127.0.0.1:5088/'
if (-not (Test-Path $exe)) { throw "WxAgent.Host.exe was not found under $InstallRoot. Publish/copy the application first." }
if ($TaskName -notmatch '^[A-Za-z0-9 ._-]{1,64}$') { throw 'TaskName contains unsupported characters.' }
New-Item -ItemType Directory -Force -Path $data | Out-Null
if ($PreventAutoLock) {
powercfg /change monitor-timeout-ac 0
powercfg /change standby-timeout-ac 0
powercfg /change hibernate-timeout-ac 0
powercfg /change monitor-timeout-dc 0
powercfg /change standby-timeout-dc 0
powercfg /change hibernate-timeout-dc 0
powercfg /setacvalueindex SCHEME_CURRENT SUB_NONE CONSOLELOCK 0
powercfg /setdcvalueindex SCHEME_CURRENT SUB_NONE CONSOLELOCK 0
powercfg /setactive SCHEME_CURRENT
New-Item -Path 'HKCU:\Control Panel\Desktop' -Force | Out-Null
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value 0
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeout -Value 0
New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' -Force | Out-Null
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization' -Name NoLockScreen -Type DWord -Value 1
}
function Write-Utf8Json([string]$Path, $Value) {
$utf8 = New-Object System.Text.UTF8Encoding($false)
[IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 8), $utf8)
}
function To-Hex([byte[]]$Bytes) { [BitConverter]::ToString($Bytes).Replace('-', '') }
$generatedToken = $null
if (-not (Test-Path $credentials)) {
if ([string]::IsNullOrWhiteSpace($Token)) {
$bytes = New-Object byte[] 32
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
$generatedToken = (To-Hex $bytes).ToLowerInvariant()
$Token = $generatedToken
}
$sha = [Security.Cryptography.SHA256]::Create()
try { $hash = To-Hex ($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Token))).ToUpperInvariant() }
finally { $sha.Dispose() }
Write-Utf8Json $credentials @(@{
PrincipalId = 'local-admin'
TokenSha256 = $hash
Permissions = @('read', 'content', 'manage')
AccountIds = @()
})
}
if (-not (Test-Path $config)) {
Write-Utf8Json $config @{
ListenUrl = 'http://127.0.0.1:5088'
AllowExternal = $false
AllowedHosts = @('127.0.0.1:5088', 'localhost:5088', '[::1]:5088')
AllowedOrigins = @('http://127.0.0.1:5088', 'http://localhost:5088', 'http://[::1]:5088')
CredentialFile = $credentials
DataDirectory = $data
QueueCapacity = 100
ListenerSession = '文件传输助手'
EnableListenerEvents = $false
}
}
$user = $env:USERNAME
$action = New-ScheduledTaskAction -Execute $exe -Argument ('serve --config "{0}"' -f $config) -WorkingDirectory $InstallRoot
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user
$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
$desktop = [Environment]::GetFolderPath('Desktop')
$shell = New-Object -ComObject WScript.Shell
$startShortcut = Join-Path $desktop 'WxAgent-Start.lnk'
$link = $shell.CreateShortcut($startShortcut)
$link.TargetPath = Join-Path $env:SystemRoot 'System32\schtasks.exe'
$link.Arguments = "/run /tn `"$TaskName`""
$link.WorkingDirectory = $InstallRoot
$link.Description = 'Start WxAgent Desktop Agent'
$link.Save()
$openShortcut = Join-Path $desktop 'WxAgent.lnk'
$link = $shell.CreateShortcut($openShortcut)
$link.TargetPath = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$command = "Start-ScheduledTask -TaskName '$TaskName'; Start-Sleep -Seconds 2; Start-Process '$url'"
$link.Arguments = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$command`""
$link.WorkingDirectory = $InstallRoot
$link.Description = 'Start WxAgent and open the web console'
$link.Save()
$urlShortcut = Join-Path $desktop 'WxAgent-Web.url'
[IO.File]::WriteAllText($urlShortcut, "[InternetShortcut]`r`nURL=$url`r`n", [Text.Encoding]::ASCII)
if ($Start) { Start-ScheduledTask -TaskName $TaskName }
if ($OpenBrowser) { Start-Process $url }
[pscustomobject]@{
TaskName = $TaskName
InstallRoot = $InstallRoot
Url = $url
Started = [bool]$Start
PreventAutoLock = [bool]$PreventAutoLock
TokenGenerated = [bool]$generatedToken
Token = $generatedToken
} | Format-List
+1 -1
View File
@@ -4,7 +4,7 @@ using WxAgent.Windows;
namespace WxAgent.Host;
internal sealed class WindowsAgentBackend(ServiceOptions options, AccountBindingStore bindings) : IAgentBackend, IAgentEventSource
public sealed class WindowsAgentBackend(ServiceOptions options, AccountBindingStore bindings) : IAgentBackend, IAgentEventSource
{
private readonly SemaphoreSlim bindingGate = new(1, 1);
-1
View File
@@ -16,6 +16,5 @@
<ItemGroup>
<ProjectReference Include="..\WxAgent.Windows\WxAgent.Windows.csproj" />
<ProjectReference Include="..\WxAgent.Service\WxAgent.Service.csproj" />
<None Include="..\..\scripts\windows\Install-WxAgent.ps1" Link="Install-WxAgent.ps1" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always" />
</ItemGroup>
</Project>
+296
View File
@@ -0,0 +1,296 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Windows.Forms;
using Microsoft.AspNetCore.Builder;
using WxAgent.Host;
using WxAgent.Service;
namespace WxAgent.Tray;
internal static class Program
{
[STAThread]
private static void Main(string[] args)
{
if (args.Contains("--prevent-auto-lock", StringComparer.OrdinalIgnoreCase))
{
PowerPolicy.Apply();
if (args.Contains("--exit", StringComparer.OrdinalIgnoreCase)) 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;
}
}
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(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 = SystemIcons.Application,
Text = "WxAgent",
Visible = true,
ContextMenuStrip = menu
};
trayIcon.DoubleClick += (_, _) => OpenConsole();
try
{
EnsureConfiguration();
StartServiceAsync().GetAwaiter().GetResult();
}
catch (Exception exception)
{
SetStatus($"启动失败: {exception.Message}");
trayIcon.ShowBalloonTip(5000, "WxAgent 启动失败", exception.Message, ToolTipIcon.Error);
}
}
private ToolStripMenuItem StatusItem => (ToolStripMenuItem)trayIcon.ContextMenuStrip!.Items[0];
private void EnsureConfiguration()
{
var directory = Path.GetDirectoryName(configPath)!;
Directory.CreateDirectory(directory);
if (!File.Exists(configPath))
{
WriteJson(configPath, new ServiceOptions
{
CredentialFile = Path.Combine(directory, "credentials.json"),
DataDirectory = Path.Combine(directory, "data")
});
}
var loaded = ReadOptions();
options = loaded;
Directory.CreateDirectory(loaded.DataDirectory);
if (!File.Exists(loaded.CredentialFile))
{
var token = CreateCredentials(loaded.CredentialFile);
ShowToken(token);
}
}
private ServiceOptions ReadOptions() =>
JsonSerializer.Deserialize<ServiceOptions>(File.ReadAllText(configPath), ServiceHost.Json)
?? throw new InvalidDataException("service.json is empty.");
private async Task StartServiceAsync()
{
if (service is not null) return;
var loaded = ReadOptions();
loaded.Validate();
options = loaded;
service = ServiceHost.Build(loaded, new WindowsAgentBackend(loaded, new AccountBindingStore(loaded)));
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)
{
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)
{
SetStatus($"配置错误: {exception.Message}");
trayIcon.ShowBalloonTip(5000, "WxAgent 配置错误", exception.Message, ToolTipIcon.Error);
}
}
private void OpenConsole()
{
var url = (options?.ListenUrl.TrimEnd('/') ?? "http://127.0.0.1:5088") + "/";
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
private void OpenConfig()
{
EnsureConfiguration();
Process.Start(new ProcessStartInfo("notepad.exe", $"\"{configPath}\"") { UseShellExecute = true });
}
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 string CreateCredentials(string path)
{
var bytes = RandomNumberGenerator.GetBytes(32);
var token = Convert.ToHexString(bytes).ToLowerInvariant();
WriteJson(path, new[]
{
new ServiceCredential("local-admin", ServiceOptions.HashToken(token), ["read", "content", "manage"], [])
});
return token;
}
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));
}
private static void ShowToken(string token)
{
using var dialog = new Form
{
Text = "WxAgent 首次配置",
Width = 640,
Height = 210,
StartPosition = FormStartPosition.CenterScreen,
MinimizeBox = false,
MaximizeBox = false,
FormBorderStyle = FormBorderStyle.FixedDialog
};
var message = new Label { Left = 18, Top = 18, Width = 585, Height = 48, Text = "首次启动已生成本地管理 Token。请复制并妥善保存;Token 不会写入明文文件。" };
var box = new TextBox { Left = 18, Top = 76, Width = 585, ReadOnly = true, Text = token };
var copy = new Button { Left = 418, Top = 120, Width = 88, Text = "复制" };
var close = new Button { Left = 515, Top = 120, Width = 88, Text = "继续" };
copy.Click += (_, _) => { Clipboard.SetText(token); copy.Text = "已复制"; };
close.Click += (_, _) => dialog.Close();
dialog.Controls.AddRange([message, box, copy, close]);
dialog.AcceptButton = close;
dialog.ShowDialog();
}
}
+24
View File
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UseWindowsForms>true</UseWindowsForms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<PublishTrimmed>false</PublishTrimmed>
<AssemblyName>WxAgent.Tray</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WxAgent.Host\WxAgent.Host.csproj" />
<ProjectReference Include="..\WxAgent.Service\WxAgent.Service.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\WxAgent.Service\wwwroot\**\*" Link="wwwroot\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always" />
</ItemGroup>
</Project>