This commit is contained in:
lincube
2026-03-24 23:15:32 +08:00
parent b83cfb47b0
commit 26ff11b16b
7 changed files with 1619 additions and 233 deletions

View File

@@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
using LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services.Settings
{
public enum WallpaperMediaType
{
@@ -66,10 +69,272 @@ public sealed record UpdateSettingsState(
long? PendingUpdatePublishedAtUtcMs,
long? LastUpdateCheckUtcMs);
public sealed record PluginManagementSettingsState(IReadOnlyList<string> DisabledPluginIds);
public enum PluginPackageSourceKind
{
ReleaseAsset = 0,
RawFallback = 1,
WorkspaceLocal = 2
}
public sealed record PluginCatalogSourceInfo(
string Id,
string Name,
string? Description,
string? SourceUrl,
string? CachePath,
bool IsOfficial,
int Priority);
public sealed record PluginCatalogSharedContractInfo(
string Id,
string Version,
string AssemblyName);
public sealed record PluginCapabilityInfo(
string Id,
string? Version,
string? AssemblyName);
public sealed record PluginPackageSourceInfo(
PluginPackageSourceKind Kind,
string Url,
string Sha256,
long PackageSizeBytes);
public sealed record PluginCatalogManifestInfo(
string Id,
string Name,
string Description,
string Author,
string Version,
string ApiVersion,
string EntranceAssembly,
IReadOnlyList<PluginCatalogSharedContractInfo> SharedContracts);
public sealed record PluginCatalogCompatibilityInfo(
string MinHostVersion,
string ApiVersion);
public sealed record PluginCatalogRepositoryInfo(
string IconUrl,
string ProjectUrl,
string ReadmeUrl,
string HomepageUrl,
string RepositoryUrl,
IReadOnlyList<string> Tags,
string ReleaseNotes);
public sealed record PluginCatalogPublicationInfo(
string ReleaseTag,
string ReleaseAssetName,
DateTimeOffset PublishedAt,
DateTimeOffset UpdatedAt,
long PackageSizeBytes,
string Sha256,
string? Md5);
public sealed record PluginCatalogItemInfo(
PluginCatalogManifestInfo Manifest,
PluginCatalogCompatibilityInfo Compatibility,
PluginCatalogRepositoryInfo Repository,
PluginCatalogPublicationInfo Publication,
IReadOnlyList<PluginPackageSourceInfo> PackageSources,
IReadOnlyList<PluginCapabilityInfo> Capabilities)
{
public string Id => Manifest.Id;
public string Name => Manifest.Name;
public string Description => Manifest.Description;
public string Author => Manifest.Author;
public string Version => Manifest.Version;
public string ApiVersion => Manifest.ApiVersion;
public string MinHostVersion => Compatibility.MinHostVersion;
public string DownloadUrl => PackageSources.FirstOrDefault()?.Url ?? string.Empty;
public string Sha256 => Publication.Sha256;
public long PackageSizeBytes => Publication.PackageSizeBytes;
public string IconUrl => Repository.IconUrl;
public string ProjectUrl => Repository.ProjectUrl;
public string ReadmeUrl => Repository.ReadmeUrl;
public string HomepageUrl => Repository.HomepageUrl;
public string RepositoryUrl => Repository.RepositoryUrl;
public IReadOnlyList<string> Tags => Repository.Tags;
public IReadOnlyList<PluginCatalogSharedContractInfo> SharedContracts => Manifest.SharedContracts;
public IReadOnlyList<PluginCatalogDependencyInfo> Dependencies =>
Manifest.SharedContracts
.Select(contract => new PluginCatalogDependencyInfo(
contract.Id,
contract.Version,
contract.AssemblyName))
.ToArray();
public DateTimeOffset PublishedAt => Publication.PublishedAt;
public DateTimeOffset UpdatedAt => Publication.UpdatedAt;
public string ReleaseTag => Publication.ReleaseTag;
public string ReleaseAssetName => Publication.ReleaseAssetName;
public string ReleaseNotes => Repository.ReleaseNotes;
public static implicit operator PluginMarketPluginInfo(PluginCatalogItemInfo item)
{
return new PluginMarketPluginInfo(
item.Id,
item.Name,
item.Description,
item.Author,
item.Version,
item.ApiVersion,
item.MinHostVersion,
item.DownloadUrl,
item.ReleaseTag,
item.ReleaseAssetName,
item.IconUrl,
item.ReadmeUrl,
item.HomepageUrl,
item.RepositoryUrl,
item.Tags.ToArray(),
item.Dependencies.Select(dependency => new PluginMarketDependencyInfo(
dependency.Id,
dependency.Version,
dependency.AssemblyName)).ToArray(),
item.PublishedAt,
item.UpdatedAt);
}
public static implicit operator PluginCatalogItemInfo(PluginMarketPluginInfo plugin)
{
return new PluginCatalogItemInfo(
new PluginCatalogManifestInfo(
plugin.Id,
plugin.Name,
plugin.Description,
plugin.Author,
plugin.Version,
plugin.ApiVersion,
string.Empty,
plugin.Dependencies
.Select(dependency => new PluginCatalogSharedContractInfo(
dependency.Id,
dependency.Version,
dependency.AssemblyName))
.ToArray()),
new PluginCatalogCompatibilityInfo(
plugin.MinHostVersion,
plugin.ApiVersion),
new PluginCatalogRepositoryInfo(
plugin.IconUrl,
plugin.RepositoryUrl,
plugin.ReadmeUrl,
plugin.HomepageUrl,
plugin.RepositoryUrl,
plugin.Tags,
string.Empty),
new PluginCatalogPublicationInfo(
plugin.ReleaseTag,
plugin.ReleaseAssetName,
plugin.PublishedAt,
plugin.UpdatedAt,
0,
string.Empty,
null),
string.IsNullOrWhiteSpace(plugin.DownloadUrl)
? []
: [
new PluginPackageSourceInfo(
string.IsNullOrWhiteSpace(plugin.ReleaseTag)
? PluginPackageSourceKind.RawFallback
: PluginPackageSourceKind.ReleaseAsset,
plugin.DownloadUrl,
string.Empty,
0)
],
[]);
}
}
public sealed record PluginCatalogIndexResult(
bool Success,
IReadOnlyList<PluginCatalogItemInfo> Plugins,
IReadOnlyList<PluginCatalogSourceInfo> Sources,
string? Source,
string? SourceLocation,
string? WarningMessage,
string? ErrorMessage)
{
public static implicit operator PluginMarketIndexResult(PluginCatalogIndexResult result)
{
return new PluginMarketIndexResult(
result.Success,
result.Plugins.Select(plugin => (PluginMarketPluginInfo)plugin).ToArray(),
result.Source,
result.SourceLocation,
result.WarningMessage,
result.ErrorMessage);
}
}
public sealed record PluginInstallDiagnostic(
string Code,
string Message,
string? Details = null);
public sealed record PluginCatalogInstallResult(
bool Success,
string? PluginId,
string? PluginName,
PluginManifest? InstalledManifest,
IReadOnlyList<PluginInstallDiagnostic> Diagnostics,
string? ErrorMessage)
{
public static implicit operator PluginMarketInstallResult(PluginCatalogInstallResult result)
{
return new PluginMarketInstallResult(
result.Success,
result.PluginId,
result.PluginName,
result.ErrorMessage);
}
}
public sealed record PluginCatalogDependencyInfo(
string Id,
string Version,
string AssemblyName)
{
public static implicit operator PluginMarketDependencyInfo(PluginCatalogDependencyInfo dependency)
{
return new PluginMarketDependencyInfo(
dependency.Id,
dependency.Version,
dependency.AssemblyName);
}
}
[Obsolete("Use PluginCatalogSharedContractInfo and PluginCatalogItemInfo instead.")]
public sealed record PluginMarketDependencyInfo(
string Id,
string Version,
string AssemblyName);
[Obsolete("Use PluginCatalogItemInfo instead.")]
public sealed record PluginMarketPluginInfo(
string Id,
string Name,
@@ -89,6 +354,8 @@ public sealed record PluginMarketPluginInfo(
IReadOnlyList<PluginMarketDependencyInfo> Dependencies,
DateTimeOffset PublishedAt,
DateTimeOffset UpdatedAt);
[Obsolete("Use PluginCatalogIndexResult instead.")]
public sealed record PluginMarketIndexResult(
bool Success,
IReadOnlyList<PluginMarketPluginInfo> Plugins,
@@ -96,12 +363,39 @@ public sealed record PluginMarketIndexResult(
string? SourceLocation,
string? WarningMessage,
string? ErrorMessage);
[Obsolete("Use PluginCatalogInstallResult instead.")]
public sealed record PluginMarketInstallResult(
bool Success,
string? PluginId,
string? PluginName,
string? ErrorMessage);
public interface IPluginCatalogSourceProvider
{
Task<PluginCatalogIndexResult> LoadCatalogAsync(CancellationToken cancellationToken = default);
}
public interface IPluginCatalogService : IPluginCatalogSourceProvider
{
Task<PluginCatalogInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
}
public interface IPackageSourceResolver
{
IReadOnlyList<PluginPackageSourceInfo> ResolveSources(PluginCatalogItemInfo item);
}
public interface IPluginCompatibilityEvaluator
{
PluginInstallDiagnostic? Evaluate(PluginCatalogItemInfo item, Version? hostVersion);
}
public interface IPluginInstallOrchestrator
{
Task<PluginCatalogInstallResult> InstallAsync(PluginCatalogItemInfo item, CancellationToken cancellationToken = default);
}
public interface IGridSettingsService
{
GridSettingsState Get();
@@ -223,10 +517,17 @@ public interface IPluginManagementSettingsService
bool DeleteInstalledPlugin(string pluginId);
}
public interface IPluginMarketSettingsService
public interface IPluginCatalogSettingsService : IPluginCatalogSourceProvider
{
new Task<PluginCatalogIndexResult> LoadCatalogAsync(CancellationToken cancellationToken = default);
Task<PluginCatalogInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
}
[Obsolete("Use IPluginCatalogSettingsService instead.")]
public interface IPluginMarketSettingsService : IPluginCatalogSettingsService
{
Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default);
Task<PluginMarketInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
new Task<PluginMarketInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
}
public interface IApplicationInfoService
@@ -252,6 +553,20 @@ public interface ISettingsFacadeService
ILauncherCatalogService LauncherCatalog { get; }
ILauncherPolicyService LauncherPolicy { get; }
IPluginManagementSettingsService PluginManagement { get; }
IPluginCatalogSettingsService PluginCatalog { get; }
[Obsolete("Use PluginCatalog instead.")]
IPluginMarketSettingsService PluginMarket { get; }
IApplicationInfoService ApplicationInfo { get; }
}
}
namespace LanMountainDesktop.Services.PluginMarket
{
internal enum PluginPackageSourceKind
{
ReleaseAsset = 0,
RawFallback = 1,
WorkspaceLocal = 2
}
}

View File

@@ -870,14 +870,41 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
_installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot);
}
public async Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default)
public Task<PluginCatalogIndexResult> LoadCatalogAsync(CancellationToken cancellationToken = default)
{
var result = await _indexService.LoadAsync(cancellationToken);
return LoadCatalogCoreAsync(cancellationToken);
}
async Task<PluginMarketIndexResult> IPluginMarketSettingsService.LoadIndexAsync(CancellationToken cancellationToken)
{
return await LoadCatalogCoreAsync(cancellationToken).ConfigureAwait(false);
}
public Task<PluginCatalogInstallResult> InstallAsync(
string pluginId,
CancellationToken cancellationToken = default)
{
return InstallCatalogCoreAsync(pluginId, cancellationToken);
}
async Task<PluginMarketInstallResult> IPluginMarketSettingsService.InstallAsync(
string pluginId,
CancellationToken cancellationToken)
{
return await InstallCatalogCoreAsync(pluginId, cancellationToken).ConfigureAwait(false);
}
private async Task<PluginCatalogIndexResult> LoadCatalogCoreAsync(CancellationToken cancellationToken = default)
{
var result = await _indexService.LoadAsync(cancellationToken).ConfigureAwait(false);
var sources = BuildCatalogSources(result.Source?.ToString(), result.SourceLocation, result.WarningMessage);
if (!result.Success || result.Document is null)
{
return new PluginMarketIndexResult(
_cachedPlugins.Clear();
return new PluginCatalogIndexResult(
false,
[],
sources,
result.Source?.ToString(),
result.SourceLocation,
result.WarningMessage,
@@ -889,81 +916,189 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
.Select(entry =>
{
_cachedPlugins[entry.Id] = entry;
return new PluginMarketPluginInfo(
entry.Id,
entry.Name,
entry.Description,
entry.Author,
entry.Version,
entry.ApiVersion,
entry.MinHostVersion,
entry.DownloadUrl,
entry.ReleaseTag,
entry.ReleaseAssetName,
entry.IconUrl,
entry.ReadmeUrl,
entry.HomepageUrl,
entry.RepositoryUrl,
entry.Tags,
entry.SharedContracts
.Select(contract => new PluginMarketDependencyInfo(
contract.Id,
contract.Version,
contract.AssemblyName))
.ToArray(),
entry.PublishedAt,
entry.UpdatedAt);
return MapCatalogItem(entry);
})
.ToArray();
return new PluginMarketIndexResult(
return new PluginCatalogIndexResult(
true,
plugins,
sources,
result.Source?.ToString(),
result.SourceLocation,
result.WarningMessage,
null);
}
public async Task<PluginMarketInstallResult> InstallAsync(
private async Task<PluginCatalogInstallResult> InstallCatalogCoreAsync(
string pluginId,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(pluginId))
{
return new PluginMarketInstallResult(false, null, null, "Plugin id is required.");
return new PluginCatalogInstallResult(
false,
null,
null,
null,
[new PluginInstallDiagnostic("invalid_request", "Plugin id is required.")],
"Plugin id is required.");
}
if (_installService is null || _pluginRuntimeService is null)
{
return new PluginMarketInstallResult(
return new PluginCatalogInstallResult(
false,
pluginId,
null,
null,
[new PluginInstallDiagnostic("runtime_unavailable", "Plugin runtime is unavailable.")],
"Plugin runtime is unavailable.");
}
if (!_cachedPlugins.TryGetValue(pluginId, out var entry))
{
var load = await LoadIndexAsync(cancellationToken);
var load = await LoadCatalogCoreAsync(cancellationToken).ConfigureAwait(false);
if (!load.Success)
{
return new PluginMarketInstallResult(false, pluginId, null, load.ErrorMessage);
return new PluginCatalogInstallResult(
false,
pluginId,
null,
null,
[new PluginInstallDiagnostic("catalog_load_failed", load.ErrorMessage ?? "Failed to load the plugin catalog.")],
load.ErrorMessage);
}
if (!_cachedPlugins.TryGetValue(pluginId, out entry))
{
return new PluginMarketInstallResult(false, pluginId, null, "Plugin was not found in market index.");
return new PluginCatalogInstallResult(
false,
pluginId,
null,
null,
[new PluginInstallDiagnostic("not_found", "Plugin was not found in the official catalog.")],
"Plugin was not found in the official catalog.");
}
}
var result = await _installService.InstallAsync(entry, cancellationToken);
var result = await _installService.InstallAsync(entry, cancellationToken).ConfigureAwait(false);
if (!result.Success)
{
return new PluginMarketInstallResult(false, entry.Id, entry.Name, result.ErrorMessage);
return new PluginCatalogInstallResult(
false,
entry.Id,
entry.Name,
null,
[new PluginInstallDiagnostic("install_failed", result.ErrorMessage ?? "Plugin install failed.")],
result.ErrorMessage);
}
return new PluginMarketInstallResult(true, result.Manifest?.Id ?? entry.Id, result.Manifest?.Name ?? entry.Name, null);
return new PluginCatalogInstallResult(
true,
result.Manifest?.Id ?? entry.Id,
result.Manifest?.Name ?? entry.Name,
result.Manifest,
[],
null);
}
private static PluginCatalogItemInfo MapCatalogItem(AirAppMarketPluginEntry entry)
{
var manifest = new PluginCatalogManifestInfo(
entry.Id,
entry.Name,
entry.Description,
entry.Author,
entry.Version,
entry.ApiVersion,
string.Empty,
entry.SharedContracts
.Select(contract => new PluginCatalogSharedContractInfo(
contract.Id,
contract.Version,
contract.AssemblyName))
.ToArray());
var compatibility = new PluginCatalogCompatibilityInfo(
entry.MinHostVersion,
entry.ApiVersion);
var repository = new PluginCatalogRepositoryInfo(
entry.IconUrl,
entry.ProjectUrl,
entry.ReadmeUrl,
entry.HomepageUrl,
entry.RepositoryUrl,
entry.Tags.ToArray(),
entry.ReleaseNotes);
var publication = new PluginCatalogPublicationInfo(
entry.ReleaseTag,
entry.ReleaseAssetName,
entry.PublishedAt,
entry.UpdatedAt,
entry.PackageSizeBytes,
entry.Sha256,
null);
var sources = BuildPackageSources(entry);
return new PluginCatalogItemInfo(
manifest,
compatibility,
repository,
publication,
sources,
[]);
}
private static IReadOnlyList<PluginPackageSourceInfo> BuildPackageSources(AirAppMarketPluginEntry entry)
{
if (string.IsNullOrWhiteSpace(entry.DownloadUrl))
{
return [];
}
var sourceKind = entry.HasReleaseDownloadMetadata
? PluginPackageSourceKind.ReleaseAsset
: PluginPackageSourceKind.RawFallback;
return
[
new PluginPackageSourceInfo(
sourceKind,
entry.DownloadUrl,
entry.Sha256,
entry.PackageSizeBytes)
];
}
private static IReadOnlyList<PluginCatalogSourceInfo> BuildCatalogSources(
string? sourceId,
string? sourceLocation,
string? warningMessage)
{
if (string.IsNullOrWhiteSpace(sourceId) && string.IsNullOrWhiteSpace(sourceLocation))
{
return [];
}
var normalizedSourceId = string.IsNullOrWhiteSpace(sourceId)
? "plugin-catalog"
: sourceId.Trim();
return
[
new PluginCatalogSourceInfo(
normalizedSourceId,
normalizedSourceId,
string.IsNullOrWhiteSpace(warningMessage) ? null : warningMessage.Trim(),
string.IsNullOrWhiteSpace(sourceLocation) ? null : sourceLocation.Trim(),
null,
true,
0)
];
}
public void Dispose()
@@ -1054,6 +1189,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
_pluginManagementSettingsService = new PluginManagementSettingsService(Settings, pluginRuntimeService);
PluginManagement = _pluginManagementSettingsService;
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
PluginCatalog = _pluginMarketSettingsService;
PluginMarket = _pluginMarketSettingsService;
ApplicationInfo = new ApplicationInfoService();
}
@@ -1086,6 +1222,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
public IPluginManagementSettingsService PluginManagement { get; }
public IPluginCatalogSettingsService PluginCatalog { get; }
public IPluginMarketSettingsService PluginMarket { get; }
public IApplicationInfoService ApplicationInfo { get; }