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
+31
View File
@@ -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);
}
}
}
+16
View File
@@ -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;
}
}
+117
View File
@@ -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();
}
}
+21
View File
@@ -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();
}
+91
View File
@@ -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 { /* 忽略 */ }
}
}