Files
MoShiro/backend/src/MoShiro.Backend.Application/Tasks/TaskPlanner.cs
T
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

102 lines
3.7 KiB
C#

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);
/// <summary>
/// 为每个 Agent 计算期望状态(<see cref="TaskSet"/>):有效任务集 + 允许拨入的拨号方公钥。
/// 只有内容真的变了才递增版本号,所以断线重连后 Agent 收到的永远是当前版本的完整快照。
/// </summary>
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;
}
/// <summary>pending、disabled 或不存在的 Agent 返回 null(不下发状态)。</summary>
public async ValueTask<DesiredTaskSet?> GetDesiredAsync(NodeId agentId, CancellationToken ct)
{
AgentRecord? agent = await _agents.GetAsync(agentId, ct);
if (agent is null || agent.Status != NodeStatus.Active)
{
return null;
}
IReadOnlyList<TaskRecord> all = await _tasks.ListAsync(ct);
IReadOnlyList<AgentRecord> 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<string, JsonElement>(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<Fleet.Core.SignedStatementDto> 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<long> BumpDialTargetsVersionAsync(CancellationToken ct) => _settings.IncrementAsync(DialTargetsVersionKey, ct);
}