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); }