client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user