using System.Security.Cryptography;
using System.Text.Json;
using Fleet.Core;
using Fleet.Server;
using MoShiro.Protocol;
namespace MoShiro.Backend.Application;
public sealed record DesiredTaskSet(long Version, TaskSet Document);
///
/// 为每个 Agent 计算期望状态():有效任务集 + 允许拨入的拨号方公钥。
/// 只有内容真的变了才递增版本号,所以断线重连后 Agent 收到的永远是当前版本的完整快照。
///
public sealed class TaskPlanner : IDisposable
{
public const string DialTargetsVersionKey = "dial_targets_version";
private readonly ITaskStore _tasks;
private readonly IAgentStore _agents;
private readonly ISettingsStore _settings;
private readonly LinkService _links;
private readonly TrustService _trust;
private readonly SemaphoreSlim _gate = new(1, 1);
public TaskPlanner(ITaskStore tasks, IAgentStore agents, ISettingsStore settings, LinkService links, TrustService trust)
{
_trust = trust;
_links = links;
_tasks = tasks;
_agents = agents;
_settings = settings;
}
/// pending、disabled 或不存在的 Agent 返回 null(不下发状态)。
public async ValueTask GetDesiredAsync(NodeId agentId, CancellationToken ct)
{
AgentRecord? agent = await _agents.GetAsync(agentId, ct);
if (agent is null || agent.Status != NodeStatus.Active)
{
return null;
}
IReadOnlyList all = await _tasks.ListAsync(ct);
IReadOnlyList nodes = await _agents.ListAsync(ct);
LinkSnapshot links = await _links.SnapshotAsync(ct);
var document = new TaskSet();
foreach (EffectiveTask task in EffectiveTasks.Compute(all, agent, filterByAgentKinds: true))
{
document.Tasks.Add(new TaskSpec
{
Id = task.Id,
Scope = task.Scope,
Kind = task.Kind,
Target = task.Target,
Params = new Dictionary(task.Params),
Schedule = task.Schedule,
Enabled = true,
});
}
if (agent.Listens)
{
foreach (AgentRecord dialer in nodes.Where(n => n.Status == NodeStatus.Active && n.TlsPublicKey is { Length: > 0 } && links.IsLinked(n, agent)))
{
document.Peers.Add(new PeerSpec { NodeId = dialer.Id, PublicKey = Convert.ToBase64String(dialer.TlsPublicKey!) });
}
}
IReadOnlyList chain = await _trust.ChainAsync(ct);
document.TrustChain = chain.Count == 0 ? null : [.. chain];
string hash = Convert.ToHexStringLower(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(document, MoShiroJsonContext.Default.TaskSet)));
await _gate.WaitAsync(ct);
try
{
// 重新读取,避免并发的两次计算各自递增
AgentRecord current = await _agents.GetAsync(agentId, ct) ?? agent;
string? storedHash = await _agents.GetTaskHashAsync(agentId, ct);
long version = current.TaskVersion;
if (storedHash != hash)
{
version++;
await _agents.SetTaskVersionAsync(agentId, version, hash, ct);
}
document.Version = version;
return new DesiredTaskSet(version, document);
}
finally
{
_gate.Release();
}
}
public void Dispose() => _gate.Dispose();
public ValueTask BumpDialTargetsVersionAsync(CancellationToken ct) => _settings.IncrementAsync(DialTargetsVersionKey, ct);
}