client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent 33b7d57498
commit 9abf7f8213
60 changed files with 10025 additions and 0 deletions
+38
View File
@@ -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>
+100
View File
@@ -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();
}
}
}
+19
View File
@@ -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>
+37
View File
@@ -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;
}
}
+20
View File
@@ -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>
+102
View File
@@ -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();
}
}
+26
View File
@@ -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>
+67
View File
@@ -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));
}
}