namespace Zcbot.WindowsNode; internal sealed record SoftwareManagementSnapshot( string Id, string DisplayName, SoftwarePathKind PathKind, string RequiredRelativePath, bool LocationAvailable, string LocationText, string StatusText, bool RuntimeInstalled, bool HasActiveJobs); internal interface ISoftwareManagementService { Task> RefreshAsync( CancellationToken cancellationToken); void SaveLocation(string softwareId, string path); void ClearLocation(string softwareId); Task InstallRuntimeAsync( string softwareId, IProgress progress, CancellationToken cancellationToken); } internal sealed class SoftwareManagementService : ISoftwareManagementService { private readonly NodePaths paths; private readonly AdapterCatalog adapters; internal SoftwareManagementService(NodePaths paths) { this.paths = paths; adapters = AdapterCatalog.CreateDefault( new JobRepository(paths.JobsDirectory), paths); } public Task> RefreshAsync( CancellationToken cancellationToken) => Task.Run>( CreateSnapshots, cancellationToken); public void SaveLocation(string softwareId, string path) { var definition = Definition(softwareId); SoftwareLocationService.SaveConfiguredPath(definition, path); adapters.InvalidateRuntime(definition.RuntimeId); } public void ClearLocation(string softwareId) { var definition = Definition(softwareId); SoftwareLocationService.ClearConfiguredPath(definition); adapters.InvalidateRuntime(definition.RuntimeId); } public async Task InstallRuntimeAsync( string softwareId, IProgress progress, CancellationToken cancellationToken) { var definition = Definition(softwareId); var result = await ManagedRuntimeInstaller.InstallAsync( definition, progress, cancellationToken); adapters.InvalidateRuntime(definition.RuntimeId); return result; } private IReadOnlyList CreateSnapshots() { return SoftwareCatalog.Definitions.Select(definition => { var location = SoftwareLocationService.ResolveLocation(definition); var runtimePath = Path.Combine( paths.RootDirectory, "runtimes", definition.RuntimeId, "Scripts", "python.exe"); var runtimeInstalled = File.Exists(runtimePath); var matchingAdapters = adapters.All.Where(item => definition.RuntimeId.Equals(item.RuntimeId, StringComparison.Ordinal)).ToArray(); var hasActiveJobs = AdapterCatalog.HasActiveExecution || matchingAdapters.Any(item => item.HasActiveJobs); var status = BuildStatus( location, runtimeInstalled, matchingAdapters); return new SoftwareManagementSnapshot( definition.Id, definition.DisplayName, definition.PathKind, definition.RequiredRelativePath ?? string.Empty, location.Available, location.Path ?? "未检测到应用位置", status, runtimeInstalled, hasActiveJobs); }).ToArray(); } private static string BuildStatus( SoftwareLocation location, bool runtimeInstalled, IReadOnlyList matchingAdapters) { var runtime = runtimeInstalled ? "运行环境已安装" : "运行环境尚未安装"; if (!location.Available) { return $"{runtime} · {location.Detail}"; } if (!runtimeInstalled || matchingAdapters.Count == 0) { return $"{runtime} · {location.Source}"; } var probes = matchingAdapters.Select(item => item.DetectRuntime()).ToArray(); var health = probes.All(item => item.Health == "ready") ? "可用" : probes.Any(item => item.Health == "ready") ? "部分可用" : "需配置"; var versions = probes .Select(item => item.SoftwareVersion) .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.Ordinal) .ToArray(); var version = versions.Length == 0 ? string.Empty : $" · {string.Join(" / ", versions)}"; var detail = string.Join(";", probes .Where(item => item.Health != "ready") .Select(item => item.Detail) .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.Ordinal)); return string.IsNullOrWhiteSpace(detail) ? $"{runtime} · {health}{version} · {location.Source}" : $"{runtime} · {health}{version} · {detail}"; } private static SoftwareDefinition Definition(string softwareId) => SoftwareCatalog.Definitions.Single(item => item.Id.Equals(softwareId, StringComparison.Ordinal)); }