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