client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<Application
|
||||
x:Class="EveryPublish.Client.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace EveryPublish.Client;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _window;
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
var startMinimized = args.Arguments.Contains("--minimized");
|
||||
_window = new MainWindow();
|
||||
if (!startMinimized)
|
||||
{
|
||||
_window.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<RootNamespace>EveryPublish.Client</RootNamespace>
|
||||
<AssemblyName>EveryPublish.Client</AssemblyName>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x64</Platforms>
|
||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.2.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240802000" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" />
|
||||
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.0.131" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
<Window
|
||||
x:Class="EveryPublish.Client.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="EveryPublish 客户端">
|
||||
<NavigationView x:Name="NavView"
|
||||
PaneDisplayMode="Top"
|
||||
IsSettingsVisible="False"
|
||||
SelectionChanged="NavView_SelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<NavigationViewItem Content="状态" Tag="status">
|
||||
<NavigationViewItem.Icon><SymbolIcon Symbol="Home"/></NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="配对" Tag="pair">
|
||||
<NavigationViewItem.Icon><SymbolIcon Symbol="Link"/></NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="挑战" Tag="challenges">
|
||||
<NavigationViewItem.Icon><SymbolIcon Symbol="Permissions"/></NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem Content="设置" Tag="settings">
|
||||
<NavigationViewItem.Icon><SymbolIcon Symbol="Setting"/></NavigationViewItem.Icon>
|
||||
</NavigationViewItem>
|
||||
</NavigationView.MenuItems>
|
||||
<Frame x:Name="ContentFrame"/>
|
||||
</NavigationView>
|
||||
</Window>
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using EveryPublish.Client.Pages;
|
||||
using EveryPublish.Client.Services;
|
||||
using H.NotifyIcon;
|
||||
|
||||
namespace EveryPublish.Client;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private readonly string _dataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"EveryPublish");
|
||||
private readonly CoreClient _core;
|
||||
private readonly Watchdog _watchdog;
|
||||
private readonly ClientContext _context;
|
||||
private TaskbarIcon? _tray;
|
||||
private bool _exiting;
|
||||
|
||||
/// <summary>供设置页「退出客户端」调用</summary>
|
||||
public static Action? ExitAction;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Directory.CreateDirectory(_dataDir);
|
||||
_core = new CoreClient(_dataDir);
|
||||
_watchdog = new Watchdog(_core, _dataDir, OnCoreFatal);
|
||||
_context = new ClientContext(_core, _dataDir, _watchdog);
|
||||
ExitAction = ExitApp;
|
||||
AppWindow.Closing += OnClosing;
|
||||
InitTray();
|
||||
NavView.SelectedItem = NavView.MenuItems[0];
|
||||
Navigate("status");
|
||||
_watchdog.Start();
|
||||
}
|
||||
|
||||
private void OnCoreFatal()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = "核心进程异常",
|
||||
Content = "agent-core 连续重启失败,请检查安装目录或重新安装。",
|
||||
CloseButtonText = "知道了",
|
||||
XamlRoot = Content.XamlRoot,
|
||||
};
|
||||
await dialog.ShowAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitTray()
|
||||
{
|
||||
try
|
||||
{
|
||||
_tray = new TaskbarIcon { ToolTipText = "EveryPublish 客户端" };
|
||||
var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "app.ico");
|
||||
if (File.Exists(iconPath))
|
||||
{
|
||||
_tray.IconSource = new BitmapImage(new Uri(iconPath));
|
||||
}
|
||||
// H.NotifyIcon 2.x:左/右键命令打开主窗口(退出入口在设置页)
|
||||
_tray.LeftClickCommand = new SimpleCommand(ShowWindow);
|
||||
_tray.RightClickCommand = new SimpleCommand(ShowWindow);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 托盘不可用不影响主功能
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowWindow()
|
||||
{
|
||||
AppWindow.Show();
|
||||
Activate();
|
||||
}
|
||||
|
||||
private void OnClosing(AppWindow sender, AppWindowClosingEventArgs args)
|
||||
{
|
||||
if (!_exiting)
|
||||
{
|
||||
args.Cancel = true;
|
||||
AppWindow.Hide(); // 隐藏到托盘(WinUI 3 无 Window.Hide,用 AppWindow.Hide)
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApp()
|
||||
{
|
||||
_exiting = true;
|
||||
_tray?.Dispose();
|
||||
_watchdog.Dispose();
|
||||
_core.Dispose();
|
||||
Close();
|
||||
}
|
||||
|
||||
private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is NavigationViewItem item && item.Tag is string tag)
|
||||
{
|
||||
Navigate(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private void Navigate(string tag)
|
||||
{
|
||||
Type page = tag switch
|
||||
{
|
||||
"pair" => typeof(PairPage),
|
||||
"challenges" => typeof(ChallengesPage),
|
||||
"settings" => typeof(SettingsPage),
|
||||
_ => typeof(StatusPage),
|
||||
};
|
||||
ContentFrame.Navigate(page, _context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EveryPublish.Client.Models;
|
||||
|
||||
/// <summary>agent-core 写入的 local.json(随机端口 + token)</summary>
|
||||
public class LocalInfo
|
||||
{
|
||||
[JsonPropertyName("port")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[JsonPropertyName("token")]
|
||||
public string Token { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>核心状态(/status 返回)</summary>
|
||||
public class CoreStatus
|
||||
{
|
||||
[JsonPropertyName("online")]
|
||||
public bool Online { get; set; }
|
||||
|
||||
[JsonPropertyName("session")]
|
||||
public string Session { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("server")]
|
||||
public string Server { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("deviceId")]
|
||||
public long DeviceId { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>挑战(/challenges 返回,与 shared/proto.Challenge 一致)</summary>
|
||||
public class ChallengeItem
|
||||
{
|
||||
[JsonPropertyName("challengeId")]
|
||||
public string ChallengeId { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("accountId")]
|
||||
public string AccountId { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("platform")]
|
||||
public string Platform { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("kind")]
|
||||
public string Kind { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("qrToken")]
|
||||
public string QrToken { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("expiresAt")]
|
||||
public long ExpiresAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# EveryPublish 客户端打包脚本(Windows 上执行)
|
||||
# 产出 dist/EveryPublish-Client-<版本>-win-x64.zip:WinUI 壳 + agent-core.exe 同目录。
|
||||
param(
|
||||
[string]$Version = "0.2.0"
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot # client/ui
|
||||
$dist = Join-Path $root "..\dist"
|
||||
New-Item -ItemType Directory -Force -Path $dist | Out-Null
|
||||
|
||||
Write-Host "[1/5] 生成图标 app.ico ..."
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$assets = Join-Path $root "Assets"
|
||||
New-Item -ItemType Directory -Force -Path $assets | Out-Null
|
||||
$bmp = New-Object System.Drawing.Bitmap 32, 32
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.Clear([System.Drawing.Color]::FromArgb(0, 110, 255))
|
||||
$font = New-Object System.Drawing.Font("Arial", 16, [System.Drawing.FontStyle]::Bold)
|
||||
$g.DrawString("E", $font, [System.Drawing.Brushes]::White, 7, 5)
|
||||
$g.Dispose()
|
||||
$icon = [System.Drawing.Icon]::FromHandle($bmp.GetHicon())
|
||||
$fs = [System.IO.File]::Create((Join-Path $assets "app.ico"))
|
||||
$icon.Save($fs)
|
||||
$fs.Close()
|
||||
|
||||
Write-Host "[2/5] 交叉编译 agent-core.exe (Go) ..."
|
||||
Push-Location (Join-Path $root "..\core")
|
||||
$env:GOOS = "windows"
|
||||
$env:GOARCH = "amd64"
|
||||
go build -o (Join-Path $dist "agent-core.exe") .
|
||||
Pop-Location
|
||||
|
||||
Write-Host "[3/5] dotnet publish WinUI 壳 ..."
|
||||
$publishDir = Join-Path $dist "publish"
|
||||
Push-Location $root
|
||||
dotnet publish EveryPublish.Client.csproj -c Release -r win-x64 -p:Platform=x64 -o $publishDir
|
||||
Pop-Location
|
||||
|
||||
Write-Host "[4/5] 组装产物 ..."
|
||||
Copy-Item (Join-Path $dist "agent-core.exe") $publishDir -Force
|
||||
Copy-Item -Recurse -Force $assets (Join-Path $publishDir "Assets")
|
||||
|
||||
Write-Host "[5/5] 压缩 ..."
|
||||
$zip = Join-Path $dist "EveryPublish-Client-$Version-win-x64.zip"
|
||||
if (Test-Path $zip) { Remove-Item $zip -Force }
|
||||
Compress-Archive -Path (Join-Path $publishDir "*") -DestinationPath $zip -Force
|
||||
Write-Host "OK: $zip" -ForegroundColor Green
|
||||
@@ -0,0 +1,31 @@
|
||||
# EveryPublish Windows 构建前环境自检(PowerShell)
|
||||
Write-Host "=== EveryPublish Windows 构建自检 ===" -ForegroundColor Cyan
|
||||
|
||||
function Check($name, $cmd) {
|
||||
try {
|
||||
$out = Invoke-Expression $cmd 2>&1 | Out-String
|
||||
$first = ($out.Trim() -split "`n")[0]
|
||||
Write-Host "[OK] $name : $first" -ForegroundColor Green
|
||||
return $true
|
||||
} catch {
|
||||
Write-Host "[FAIL] $name 未安装或不可用" -ForegroundColor Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$ok = $true
|
||||
$ok = (Check "dotnet SDK" "dotnet --version") -and $ok
|
||||
$ok = (Check "Go 工具链" "go version") -and $ok
|
||||
|
||||
# 版本校验:.NET 需 8.x,Go 需 1.23+
|
||||
$dotnetVer = (dotnet --version 2>$null)
|
||||
if ($dotnetVer -and -not $dotnetVer.StartsWith("8.")) {
|
||||
Write-Host "[WARN] dotnet 版本 $dotnetVer,建议 .NET 8 SDK" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (-not $ok) {
|
||||
Write-Host "请先安装依赖(见 client/ui/README.md)后重试" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "环境就绪:可运行 Packaging\build-installer.ps1 打包" -ForegroundColor Green
|
||||
@@ -0,0 +1,38 @@
|
||||
<Page
|
||||
x:Class="EveryPublish.Client.Pages.ChallengesPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Grid Margin="32" RowSpacing="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0" Spacing="8">
|
||||
<TextBlock Text="二次验证挑战" Style="{StaticResource TitleTextBlockStyle}"/>
|
||||
<TextBlock Text="平台要求扫码/验证码时在此处理;超时可在网页端一键重发。" Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Spacing="12">
|
||||
<ListView x:Name="ChallengeList" SelectionChanged="ChallengeList_SelectionChanged">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Padding="8" Spacing="4">
|
||||
<TextBlock FontWeight="SemiBold">
|
||||
<Run Text="平台:"/><Run Text="{Binding Platform}"/>
|
||||
<Run Text=" 类型:"/><Run Text="{Binding Kind}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Prompt}" TextWrapping="Wrap" Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<TextBlock x:Name="EmptyText" Text="暂无待处理挑战" Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
|
||||
<StackPanel x:Name="SolvePanel" Spacing="8" Visibility="Collapsed" MaxWidth="420">
|
||||
<Image x:Name="QrImage" MaxHeight="220" MaxWidth="220" Visibility="Collapsed" HorizontalAlignment="Left"/>
|
||||
<TextBlock x:Name="SolveHint" Text="验证码 / 确认值"/>
|
||||
<TextBox x:Name="ValueBox" PlaceholderText="输入收到的验证码"/>
|
||||
<Button x:Name="SolveButton" Content="提交" Style="{StaticResource AccentButtonStyle}" Click="Solve_Click" HorizontalAlignment="Left"/>
|
||||
<TextBlock x:Name="SolveResult" Text="" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using EveryPublish.Client.Models;
|
||||
using EveryPublish.Client.Services;
|
||||
|
||||
namespace EveryPublish.Client.Pages;
|
||||
|
||||
public sealed partial class ChallengesPage : Page
|
||||
{
|
||||
private ClientContext _ctx = null!;
|
||||
private DispatcherTimer? _timer;
|
||||
private ChallengeItem? _selected;
|
||||
|
||||
public ChallengesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
||||
_timer.Tick += async (s, e) => await RefreshAsync();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
_ctx = (ClientContext)e.Parameter;
|
||||
_ctx.Core.ReloadLocalJson();
|
||||
_timer?.Start();
|
||||
_ = RefreshAsync();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
_timer?.Stop();
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
var list = await _ctx.Core.GetChallengesAsync();
|
||||
ChallengeList.ItemsSource = list;
|
||||
EmptyText.Visibility = list.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (_selected == null)
|
||||
{
|
||||
SolvePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ChallengeList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_selected = ChallengeList.SelectedItem as ChallengeItem;
|
||||
if (_selected == null)
|
||||
{
|
||||
SolvePanel.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
SolvePanel.Visibility = Visibility.Visible;
|
||||
SolveHint.Text = _selected.Kind switch
|
||||
{
|
||||
"qr" => "扫码挑战(请在对应平台 App 扫码;完成后点击「我已确认」提交)",
|
||||
_ => "输入收到的验证码 / 确认值",
|
||||
};
|
||||
// 二维码展示:qrUrl 为 http URL 或 file:// 本地路径
|
||||
var qr = !string.IsNullOrEmpty(_selected.QrUrl) ? _selected.QrUrl : _selected.QrToken;
|
||||
if (!string.IsNullOrEmpty(qr) && (qr.StartsWith("http") || qr.StartsWith("file://")))
|
||||
{
|
||||
QrImage.Source = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(new Uri(qr));
|
||||
QrImage.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
QrImage.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
ValueBox.Text = "";
|
||||
SolveResult.Text = "";
|
||||
}
|
||||
|
||||
private async void Solve_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_selected == null) return;
|
||||
var value = ValueBox.Text.Trim();
|
||||
if (_selected.Kind == "qr" && string.IsNullOrEmpty(value))
|
||||
{
|
||||
value = "confirmed";
|
||||
}
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
SolveResult.Text = "请输入验证码";
|
||||
return;
|
||||
}
|
||||
SolveButton.IsEnabled = false;
|
||||
var (ok, message) = await _ctx.Core.SolveAsync(_selected.ChallengeId, value);
|
||||
SolveButton.IsEnabled = true;
|
||||
SolveResult.Text = ok ? "已提交,等待服务器确认" : message;
|
||||
if (ok)
|
||||
{
|
||||
_selected = null;
|
||||
await RefreshAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Page
|
||||
x:Class="EveryPublish.Client.Pages.PairPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32" Spacing="16" MaxWidth="480">
|
||||
<TextBlock Text="设备配对" Style="{StaticResource TitleTextBlockStyle}"/>
|
||||
<InfoBar IsOpen="True" Severity="Informational" IsClosable="False">
|
||||
<InfoBar.Message>
|
||||
<TextBlock Text="1. 登录网页控制台 →「设置 → Agent 设备」生成配对码;2. 在下方输入配对码完成绑定。"/>
|
||||
</InfoBar.Message>
|
||||
</InfoBar>
|
||||
<TextBox x:Name="CodeBox" Header="配对码" PlaceholderText="6 位配对码(5 分钟内有效)"/>
|
||||
<TextBox x:Name="NameBox" Header="设备名(选填)" PlaceholderText="例如:办公室电脑-01"/>
|
||||
<Button x:Name="PairButton" Content="配对" Style="{StaticResource AccentButtonStyle}" Click="Pair_Click" HorizontalAlignment="Left"/>
|
||||
<TextBlock x:Name="PairResult" Text="" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Page>
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using EveryPublish.Client.Services;
|
||||
|
||||
namespace EveryPublish.Client.Pages;
|
||||
|
||||
public sealed partial class PairPage : Page
|
||||
{
|
||||
private ClientContext _ctx = null!;
|
||||
|
||||
public PairPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
_ctx = (ClientContext)e.Parameter;
|
||||
_ctx.Core.ReloadLocalJson();
|
||||
}
|
||||
|
||||
private async void Pair_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var code = CodeBox.Text.Trim();
|
||||
var name = NameBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
PairResult.Text = "请输入配对码";
|
||||
return;
|
||||
}
|
||||
PairButton.IsEnabled = false;
|
||||
PairResult.Text = "配对中...";
|
||||
var (ok, message) = await _ctx.Core.PairAsync(code, name);
|
||||
PairButton.IsEnabled = true;
|
||||
PairResult.Text = ok ? "配对成功,正在连接服务器(可到「状态」页查看)" : message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Page
|
||||
x:Class="EveryPublish.Client.Pages.SettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32" Spacing="16" MaxWidth="520">
|
||||
<TextBlock Text="设置" Style="{StaticResource TitleTextBlockStyle}"/>
|
||||
<TextBox x:Name="ServerBox" Header="服务器地址" PlaceholderText="http://127.0.0.1:8090"/>
|
||||
<ToggleSwitch x:Name="AutoStartToggle" Header="开机自启" OnContent="开" OffContent="关"/>
|
||||
<ToggleSwitch x:Name="FakeAllToggle" Header="模拟执行(无真实账号的安装联调)" OnContent="开" OffContent="关"/>
|
||||
<TextBlock><Run Text="数据目录:" FontWeight="SemiBold"/><Run x:Name="DataDirText" Text="..."/></TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="保存并重启核心" Style="{StaticResource AccentButtonStyle}" Click="Save_Click"/>
|
||||
<Button Content="打开数据目录" Click="OpenDir_Click"/>
|
||||
<Button Content="退出客户端" Click="Exit_Click"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="ResultText" Text="" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Page>
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.System;
|
||||
using EveryPublish.Client.Services;
|
||||
|
||||
namespace EveryPublish.Client.Pages;
|
||||
|
||||
public sealed partial class SettingsPage : Page
|
||||
{
|
||||
private ClientContext _ctx = null!;
|
||||
|
||||
public SettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
_ctx = (ClientContext)e.Parameter;
|
||||
DataDirText.Text = _ctx.DataDir;
|
||||
ServerBox.Text = ReadServer();
|
||||
AutoStartToggle.IsOn = AutoStart.IsEnabled();
|
||||
FakeAllToggle.IsOn = ReadBool("fakeAll");
|
||||
}
|
||||
|
||||
private bool ReadBool(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(_ctx.DataDir, "config.json");
|
||||
if (!File.Exists(path)) return false;
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (doc.RootElement.TryGetProperty(key, out var v)) return v.GetBoolean();
|
||||
}
|
||||
catch { /* 忽略 */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
private string ReadServer()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(_ctx.DataDir, "config.json");
|
||||
if (!File.Exists(path)) return "http://127.0.0.1:8090";
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (doc.RootElement.TryGetProperty("server", out var s)) return s.GetString() ?? "";
|
||||
}
|
||||
catch { /* 忽略 */ }
|
||||
return "http://127.0.0.1:8090";
|
||||
}
|
||||
|
||||
private void WriteServer(string server)
|
||||
{
|
||||
var path = Path.Combine(_ctx.DataDir, "config.json");
|
||||
var doc = File.Exists(path)
|
||||
? JsonSerializer.Deserialize<JsonElement>(File.ReadAllText(path))
|
||||
: JsonDocument.Parse("{}").RootElement;
|
||||
using var json = JsonDocument.Parse(doc.GetRawText());
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json.RootElement.GetRawText()) ?? new();
|
||||
dict["server"] = JsonSerializer.SerializeToElement(server);
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
private void Save_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var server = ServerBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(server))
|
||||
{
|
||||
ResultText.Text = "服务器地址不能为空";
|
||||
return;
|
||||
}
|
||||
WriteServer(server);
|
||||
WriteBool("fakeAll", FakeAllToggle.IsOn);
|
||||
AutoStart.SetEnabled(AutoStartToggle.IsOn);
|
||||
_ctx.Watchdog.Restart();
|
||||
ResultText.Text = "已保存,核心进程已重启";
|
||||
}
|
||||
|
||||
private void WriteBool(string key, bool value)
|
||||
{
|
||||
var path = Path.Combine(_ctx.DataDir, "config.json");
|
||||
var dict = File.Exists(path)
|
||||
? JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText(path)) ?? new()
|
||||
: new();
|
||||
dict[key] = JsonSerializer.SerializeToElement(value);
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
private async void OpenDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Directory.CreateDirectory(_ctx.DataDir);
|
||||
await Launcher.LaunchFolderPathAsync(_ctx.DataDir);
|
||||
}
|
||||
|
||||
private void Exit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MainWindow.ExitAction?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Page
|
||||
x:Class="EveryPublish.Client.Pages.StatusPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32" Spacing="16">
|
||||
<TextBlock Text="运行状态" Style="{StaticResource TitleTextBlockStyle}"/>
|
||||
<InfoBar IsOpen="True" Severity="Informational" IsClosable="False">
|
||||
<InfoBar.Message>
|
||||
<TextBlock Text="客户端后台常驻运行;关闭窗口将最小化到托盘,不会退出。"/>
|
||||
</InfoBar.Message>
|
||||
</InfoBar>
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="连接状态:" VerticalAlignment="Center"/>
|
||||
<Ellipse x:Name="OnlineDot" Width="10" Height="10" Fill="Gray" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="OnlineText" Text="..." VerticalAlignment="Center" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<TextBlock><Run Text="服务器:" FontWeight="SemiBold"/><Run x:Name="ServerText" Text="..."/></TextBlock>
|
||||
<TextBlock><Run Text="设备:" FontWeight="SemiBold"/><Run x:Name="DeviceText" Text="..."/></TextBlock>
|
||||
<TextBlock><Run Text="会话:" FontWeight="SemiBold"/><Run x:Name="SessionText" Text="..."/></TextBlock>
|
||||
</StackPanel>
|
||||
<Button x:Name="OpenWebButton" Content="打开网页控制台" Click="OpenWeb_Click" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Page>
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.System;
|
||||
using EveryPublish.Client.Services;
|
||||
|
||||
namespace EveryPublish.Client.Pages;
|
||||
|
||||
public sealed partial class StatusPage : Page
|
||||
{
|
||||
private ClientContext _ctx = null!;
|
||||
private DispatcherTimer? _timer;
|
||||
|
||||
public StatusPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
|
||||
_timer.Tick += async (s, e) => await RefreshAsync();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
_ctx = (ClientContext)e.Parameter;
|
||||
_ctx.Core.ReloadLocalJson();
|
||||
_timer?.Start();
|
||||
_ = RefreshAsync();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
_timer?.Stop();
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
_ctx.Core.ReloadLocalJson();
|
||||
var status = await _ctx.Core.GetStatusAsync();
|
||||
if (status == null)
|
||||
{
|
||||
OnlineText.Text = "核心进程未就绪";
|
||||
OnlineDot.Fill = new SolidColorBrush(Microsoft.UI.Colors.Gray);
|
||||
return;
|
||||
}
|
||||
ServerText.Text = status.Server;
|
||||
DeviceText.Text = status.DeviceId > 0 ? "#" + status.DeviceId + "(" + status.Name + ")" : "未配对";
|
||||
SessionText.Text = status.Session;
|
||||
if (status.Online)
|
||||
{
|
||||
OnlineText.Text = "在线";
|
||||
OnlineDot.Fill = new SolidColorBrush(Microsoft.UI.Colors.LimeGreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnlineText.Text = status.DeviceId > 0 ? "离线(自动重连中)" : "未配对";
|
||||
OnlineDot.Fill = new SolidColorBrush(Microsoft.UI.Colors.Orange);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OpenWeb_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var status = await _ctx.Core.GetStatusAsync();
|
||||
var url = status?.Server ?? "http://127.0.0.1:8090";
|
||||
await Launcher.LaunchUriAsync(new Uri(url));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# EveryPublish Windows 客户端(WinUI 3 壳)
|
||||
|
||||
## 结构
|
||||
|
||||
- 本目录:WinUI 3(C#/.NET 8)外壳——托盘、配对引导、挑战窗口、状态页、设置,
|
||||
spawn 并看门狗守护 `agent-core.exe`(../core,Go)。
|
||||
- 壳与 core 的桥:`data/local.json`(core 启动写随机端口 + token),壳经 127.0.0.1 HTTP 控制。
|
||||
|
||||
## Windows 构建(需 Windows 10/11 + .NET 8 SDK)
|
||||
|
||||
1. 安装 .NET 8 SDK(https://dotnet.microsoft.com/download/dotnet/8.0)与 Go 1.23+。
|
||||
2. 首次构建前还原依赖:`dotnet restore EveryPublish.Client.csproj`(需网络)。
|
||||
3. 一键打包:`powershell -ExecutionPolicy Bypass -File Packaging\build-installer.ps1`。
|
||||
产出 `../dist/EveryPublish-Client-0.2.0-win-x64.zip`。
|
||||
4. 解压运行 `EveryPublish.Client.exe`(无需安装 .NET 运行时与 WindowsAppSDK,已自包含)。
|
||||
|
||||
## 手动验证清单(D9 验收)
|
||||
|
||||
1. 首次启动:窗口显示「未配对」;托盘图标出现。
|
||||
2. 网页端生成配对码 → 客户端「配对」页输入 → 配对成功 → 状态页「在线」。
|
||||
3. 网页端新建账号并绑定 → 客户端「挑战」页出现挑战 → 提交验证码 → 网页端账号变「已绑定」。
|
||||
4. 网页端新建任务 → 审核通过 → 客户端状态页保持在线 → 网页端任务变「发布成功」。
|
||||
5. 关闭窗口 → 最小化到托盘(进程仍运行);托盘「退出」才真正退出。
|
||||
6. 设置页开启「开机自启」→ 注销/重启后客户端自动运行。
|
||||
|
||||
## 已知注意
|
||||
|
||||
- H.NotifyIcon.WinUI 锁定 2.0.131(NuGet 已验证存在):托盘左/右键打开主窗口(2.x 无 WPF 式 ContextMenu),退出入口在「设置 → 退出客户端」。
|
||||
- 构建前可先跑 `Packaging\precheck.ps1` 自检 .NET 8 SDK 与 Go 工具链。
|
||||
- 若出现 WinUI 编译错误,多为 XAML 控件 API 细节,按报错微调即可(工程结构已就位)。
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EveryPublish.Client.Services;
|
||||
|
||||
/// <summary>开机自启(HKCU Run)</summary>
|
||||
public static class AutoStart
|
||||
{
|
||||
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string ValueName = "EveryPublishClient";
|
||||
|
||||
public static bool IsEnabled()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(RunKey);
|
||||
return key?.GetValue(ValueName) != null;
|
||||
}
|
||||
|
||||
public static void SetEnabled(bool enabled)
|
||||
{
|
||||
using var key = Registry.CurrentUser.CreateSubKey(RunKey);
|
||||
if (key == null) return;
|
||||
if (enabled)
|
||||
{
|
||||
var exe = Environment.ProcessPath ?? "";
|
||||
key.SetValue(ValueName, "\"" + exe + "\" --minimized");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue(ValueName, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace EveryPublish.Client.Services;
|
||||
|
||||
/// <summary>页面共享上下文(导航参数)</summary>
|
||||
public class ClientContext
|
||||
{
|
||||
public CoreClient Core { get; }
|
||||
public string DataDir { get; }
|
||||
public Watchdog Watchdog { get; }
|
||||
|
||||
public ClientContext(CoreClient core, string dataDir, Watchdog watchdog)
|
||||
{
|
||||
Core = core;
|
||||
DataDir = dataDir;
|
||||
Watchdog = watchdog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using EveryPublish.Client.Models;
|
||||
|
||||
namespace EveryPublish.Client.Services;
|
||||
|
||||
/// <summary>agent-core 本机控制客户端(读 local.json → 127.0.0.1 + token 鉴权)</summary>
|
||||
public class CoreClient : IDisposable
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _dataDir;
|
||||
private string _base = "";
|
||||
private string _token = "";
|
||||
|
||||
public CoreClient(string dataDir)
|
||||
{
|
||||
_dataDir = dataDir;
|
||||
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
||||
ReloadLocalJson();
|
||||
}
|
||||
|
||||
/// <summary>重新读取 data/local.json(端口/token,core 每次启动随机)</summary>
|
||||
public void ReloadLocalJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(_dataDir, "local.json");
|
||||
if (!File.Exists(path)) return;
|
||||
var info = JsonSerializer.Deserialize<LocalInfo>(File.ReadAllText(path));
|
||||
if (info == null || info.Port <= 0 || string.IsNullOrEmpty(info.Token)) return;
|
||||
_base = "http://127.0.0.1:" + info.Port;
|
||||
_token = info.Token;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// core 尚未写入 local.json,稍后重试
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequestMessage NewRequest(HttpMethod method, string path)
|
||||
{
|
||||
var req = new HttpRequestMessage(method, _base + path);
|
||||
req.Headers.Add("X-Agent-Token", _token);
|
||||
return req;
|
||||
}
|
||||
|
||||
public async Task<CoreStatus?> GetStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var resp = await _http.SendAsync(NewRequest(HttpMethod.Get, "/status"));
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
return await resp.Content.ReadFromJsonAsync<CoreStatus>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ChallengeItem>> GetChallengesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var resp = await _http.SendAsync(NewRequest(HttpMethod.Get, "/challenges"));
|
||||
if (!resp.IsSuccessStatusCode) return new List<ChallengeItem>();
|
||||
var doc = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
if (doc.TryGetProperty("list", out var list))
|
||||
{
|
||||
return list.Deserialize<List<ChallengeItem>>() ?? new List<ChallengeItem>();
|
||||
}
|
||||
return new List<ChallengeItem>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<ChallengeItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<(bool Ok, string Message)> PairAsync(string code, string name)
|
||||
{
|
||||
return PostAsync("/pair", new { code, name });
|
||||
}
|
||||
|
||||
public Task<(bool Ok, string Message)> SolveAsync(string challengeId, string value)
|
||||
{
|
||||
return PostAsync("/challenges/" + challengeId, new { value });
|
||||
}
|
||||
|
||||
private async Task<(bool Ok, string Message)> PostAsync(string path, object body)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var req = NewRequest(HttpMethod.Post, path);
|
||||
req.Content = JsonContent.Create(body);
|
||||
using var resp = await _http.SendAsync(req);
|
||||
var doc = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var ok = doc.TryGetProperty("code", out var c) && c.GetInt32() == 0;
|
||||
var msg = doc.TryGetProperty("message", out var m) ? m.GetString() ?? "" : "";
|
||||
return (ok, msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, "无法连接客户端核心:" + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_http.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace EveryPublish.Client.Services;
|
||||
|
||||
/// <summary>极简 ICommand 实现(托盘命令用,免 CommunityToolkit 依赖)</summary>
|
||||
public class SimpleCommand : ICommand
|
||||
{
|
||||
private readonly Action _action;
|
||||
|
||||
public SimpleCommand(Action action)
|
||||
{
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged { add { } remove { } }
|
||||
|
||||
public bool CanExecute(object? parameter) => true;
|
||||
|
||||
public void Execute(object? parameter) => _action();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace EveryPublish.Client.Services;
|
||||
|
||||
/// <summary>看门狗:spawn agent-core 子进程,5s 心跳检查,失败自动拉起,3 次失败告警</summary>
|
||||
public class Watchdog : IDisposable
|
||||
{
|
||||
private readonly CoreClient _core;
|
||||
private readonly string _dataDir;
|
||||
private readonly Action _onFatal;
|
||||
private Process? _proc;
|
||||
private Timer? _timer;
|
||||
private int _failCount;
|
||||
|
||||
public Watchdog(CoreClient core, string dataDir, Action onFatal)
|
||||
{
|
||||
_core = core;
|
||||
_dataDir = dataDir;
|
||||
_onFatal = onFatal;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Spawn();
|
||||
_timer = new Timer(_ => Check(), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
try { _proc?.Kill(); } catch { /* 忽略 */ }
|
||||
Spawn();
|
||||
}
|
||||
|
||||
private void Spawn()
|
||||
{
|
||||
var exe = Path.Combine(AppContext.BaseDirectory, "agent-core.exe");
|
||||
if (!File.Exists(exe))
|
||||
{
|
||||
_onFatal();
|
||||
return;
|
||||
}
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = exe,
|
||||
Arguments = "-dir \"" + _dataDir + "\"",
|
||||
WorkingDirectory = Path.GetDirectoryName(exe) ?? AppContext.BaseDirectory,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
_proc = Process.Start(psi);
|
||||
}
|
||||
|
||||
private void Check()
|
||||
{
|
||||
_core.ReloadLocalJson();
|
||||
var healthy = false;
|
||||
if (_proc != null && !_proc.HasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = _core.GetStatusAsync().GetAwaiter().GetResult();
|
||||
healthy = status != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
healthy = false;
|
||||
}
|
||||
}
|
||||
if (healthy)
|
||||
{
|
||||
_failCount = 0;
|
||||
return;
|
||||
}
|
||||
_failCount++;
|
||||
if (_failCount >= 3)
|
||||
{
|
||||
_onFatal();
|
||||
return;
|
||||
}
|
||||
Spawn();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
try { _proc?.Kill(); } catch { /* 忽略 */ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="EveryPublish.Client.app"/>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 / 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user