mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling. Key changes: - LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension. - LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations. - Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment. - LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form. - SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy). - AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly. - Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative. These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
This commit is contained in:
@@ -3,6 +3,7 @@ using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -111,7 +112,7 @@ internal sealed class LauncherClient
|
||||
WorkingDirectory = Path.GetDirectoryName(launcherPath) ?? AppContext.BaseDirectory,
|
||||
Arguments = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"--source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
||||
$"plugin install --source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
||||
};
|
||||
|
||||
return Process.Start(startInfo);
|
||||
@@ -130,7 +131,17 @@ internal sealed class LauncherClient
|
||||
|
||||
private static string ResolveLauncherPath()
|
||||
{
|
||||
return Path.Combine(AppContext.BaseDirectory, "Launcher", LauncherExecutableName);
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string value)
|
||||
|
||||
@@ -1236,7 +1236,31 @@ internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsServi
|
||||
repository,
|
||||
publication,
|
||||
sources,
|
||||
[]);
|
||||
BuildCapabilities(entry));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PluginCapabilityInfo> BuildCapabilities(AirAppMarketPluginEntry entry)
|
||||
{
|
||||
if (entry.Capabilities is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var capabilities = new List<PluginCapabilityInfo>();
|
||||
capabilities.AddRange(entry.Capabilities.SharedContracts.Select(contract =>
|
||||
new PluginCapabilityInfo(contract.Id, contract.Version, contract.AssemblyName)));
|
||||
capabilities.AddRange(entry.Capabilities.DesktopComponents.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
capabilities.AddRange(entry.Capabilities.SettingsSections.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
capabilities.AddRange(entry.Capabilities.Exports.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
capabilities.AddRange(entry.Capabilities.MessageTypes.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
|
||||
return capabilities
|
||||
.DistinctBy(capability => $"{capability.Id}@{capability.Version}@{capability.AssemblyName}")
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PluginPackageSourceInfo> BuildPackageSources(AirAppMarketPluginEntry entry)
|
||||
|
||||
@@ -112,6 +112,9 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
||||
? entry.PackageSources
|
||||
: entry.Publication?.PackageSources ?? [];
|
||||
var firstPackageSourceUrl = resolvedPackageSources.FirstOrDefault()?.Url ?? entry.DownloadUrl;
|
||||
var existingManifest = entry.Manifest;
|
||||
var existingCompatibility = entry.Compatibility;
|
||||
var existingPublication = entry.Publication;
|
||||
|
||||
return new AirAppMarketPluginEntry
|
||||
{
|
||||
@@ -142,9 +145,13 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
||||
{
|
||||
MinHostVersion = FirstNonEmpty(
|
||||
template?.MinHostVersion,
|
||||
existingCompatibility?.MinHostVersion,
|
||||
entry.MinHostVersion),
|
||||
PluginApiVersion = FirstNonEmpty(
|
||||
resolvedManifest?.ApiVersion,
|
||||
existingCompatibility?.PluginApiVersion,
|
||||
existingCompatibility?.ApiVersion,
|
||||
existingManifest?.ApiVersion,
|
||||
entry.ApiVersion)
|
||||
?? string.Empty
|
||||
}
|
||||
@@ -162,19 +169,24 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
||||
},
|
||||
Publication = entry.Publication,
|
||||
Capabilities = entry.Capabilities,
|
||||
Id = FirstNonEmpty(resolvedManifest?.Id, entry.Id, entry.PluginId) ?? entry.PluginId,
|
||||
Name = FirstNonEmpty(resolvedManifest?.Name, entry.Name) ?? string.Empty,
|
||||
Description = FirstNonEmpty(resolvedManifest?.Description, entry.Description) ?? string.Empty,
|
||||
Author = FirstNonEmpty(resolvedManifest?.Author, entry.Author) ?? string.Empty,
|
||||
Version = FirstNonEmpty(resolvedManifest?.Version, entry.Version) ?? string.Empty,
|
||||
ApiVersion = FirstNonEmpty(resolvedManifest?.ApiVersion, entry.ApiVersion) ?? string.Empty,
|
||||
MinHostVersion = FirstNonEmpty(template?.MinHostVersion, entry.MinHostVersion) ?? string.Empty,
|
||||
Id = FirstNonEmpty(resolvedManifest?.Id, existingManifest?.Id, entry.Id, entry.PluginId) ?? entry.PluginId,
|
||||
Name = FirstNonEmpty(resolvedManifest?.Name, existingManifest?.Name, entry.Name) ?? string.Empty,
|
||||
Description = FirstNonEmpty(resolvedManifest?.Description, existingManifest?.Description, entry.Description) ?? string.Empty,
|
||||
Author = FirstNonEmpty(resolvedManifest?.Author, existingManifest?.Author, entry.Author) ?? string.Empty,
|
||||
Version = FirstNonEmpty(resolvedManifest?.Version, existingManifest?.Version, entry.Version) ?? string.Empty,
|
||||
ApiVersion = FirstNonEmpty(
|
||||
resolvedManifest?.ApiVersion,
|
||||
existingCompatibility?.PluginApiVersion,
|
||||
existingCompatibility?.ApiVersion,
|
||||
existingManifest?.ApiVersion,
|
||||
entry.ApiVersion) ?? string.Empty,
|
||||
MinHostVersion = FirstNonEmpty(template?.MinHostVersion, existingCompatibility?.MinHostVersion, entry.MinHostVersion) ?? string.Empty,
|
||||
DownloadUrl = FirstNonEmpty(firstPackageSourceUrl, entry.DownloadUrl) ?? string.Empty,
|
||||
Sha256 = entry.Sha256,
|
||||
PackageSizeBytes = entry.PackageSizeBytes,
|
||||
Sha256 = FirstNonEmpty(existingPublication?.Sha256, entry.Sha256) ?? string.Empty,
|
||||
PackageSizeBytes = existingPublication?.PackageSizeBytes > 0 ? existingPublication.PackageSizeBytes : entry.PackageSizeBytes,
|
||||
IconUrl = FirstNonEmpty(template?.IconUrl, repository.IconUrl, entry.IconUrl) ?? string.Empty,
|
||||
ReleaseTag = entry.ReleaseTag,
|
||||
ReleaseAssetName = entry.ReleaseAssetName,
|
||||
ReleaseTag = FirstNonEmpty(existingPublication?.ReleaseTag, entry.ReleaseTag) ?? string.Empty,
|
||||
ReleaseAssetName = FirstNonEmpty(existingPublication?.ReleaseAssetName, entry.ReleaseAssetName) ?? string.Empty,
|
||||
ProjectUrl = FirstNonEmpty(template?.ProjectUrl, repository.ProjectUrl, entry.ProjectUrl) ?? string.Empty,
|
||||
ReadmeUrl = FirstNonEmpty(template?.ReadmeUrl, repository.ReadmeUrl, entry.ReadmeUrl) ?? string.Empty,
|
||||
HomepageUrl = FirstNonEmpty(template?.HomepageUrl, repository.HomepageUrl, entry.HomepageUrl) ?? string.Empty,
|
||||
@@ -191,9 +203,9 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
||||
.ToList()
|
||||
?? entry.SharedContracts,
|
||||
PackageSources = resolvedPackageSources,
|
||||
Md5 = entry.Md5,
|
||||
PublishedAt = entry.PublishedAt,
|
||||
UpdatedAt = entry.UpdatedAt,
|
||||
Md5 = FirstNonEmpty(existingPublication?.Md5, entry.Md5) ?? string.Empty,
|
||||
PublishedAt = existingPublication?.PublishedAt ?? entry.PublishedAt,
|
||||
UpdatedAt = existingPublication?.UpdatedAt ?? entry.UpdatedAt,
|
||||
ReleaseNotes = FirstNonEmpty(template?.ReleaseNotes, repository.ReleaseNotes, entry.ReleaseNotes) ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
@@ -366,7 +366,17 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
|
||||
private static string ResolveLauncherPath()
|
||||
{
|
||||
return Path.Combine(AppContext.BaseDirectory, "Launcher", LauncherExecutableName);
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||
}
|
||||
|
||||
private static void TryDeleteFile(string path)
|
||||
|
||||
@@ -646,6 +646,8 @@ internal sealed class AirAppMarketPluginCompatibilityEntry
|
||||
{
|
||||
public string MinHostVersion { get; init; } = string.Empty;
|
||||
|
||||
public string ApiVersion { get; init; } = string.Empty;
|
||||
|
||||
public string PluginApiVersion { get; init; } = string.Empty;
|
||||
|
||||
public AirAppMarketPluginCompatibilityEntry ValidateAndNormalize(string sourceName)
|
||||
@@ -656,9 +658,13 @@ internal sealed class AirAppMarketPluginCompatibilityEntry
|
||||
MinHostVersion,
|
||||
nameof(MinHostVersion),
|
||||
sourceName),
|
||||
ApiVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||
AirAppMarketIndexDocument.NormalizeValue(PluginApiVersion) ?? ApiVersion,
|
||||
nameof(ApiVersion),
|
||||
sourceName),
|
||||
PluginApiVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||
PluginApiVersion,
|
||||
nameof(PluginApiVersion),
|
||||
AirAppMarketIndexDocument.NormalizeValue(PluginApiVersion) ?? ApiVersion,
|
||||
nameof(ApiVersion),
|
||||
sourceName)
|
||||
};
|
||||
}
|
||||
@@ -742,6 +748,8 @@ internal sealed class AirAppMarketPluginPackageSourceEntry
|
||||
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public PluginPackageSourceKind SourceKind { get; init; } = PluginPackageSourceKind.ReleaseAsset;
|
||||
|
||||
public AirAppMarketPluginPackageSourceEntry ValidateAndNormalize(string sourceName, string pluginId)
|
||||
@@ -755,9 +763,11 @@ internal sealed class AirAppMarketPluginPackageSourceEntry
|
||||
$"Market index '{sourceName}' declares invalid package source kind '{normalizedKind}' for plugin '{pluginId}'.");
|
||||
}
|
||||
|
||||
var normalizedPath = AirAppMarketIndexDocument.NormalizeValue(Path);
|
||||
var normalizedUrl = AirAppMarketIndexDocument.NormalizeValue(Url)
|
||||
?? normalizedPath
|
||||
?? throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing package source url for plugin '{pluginId}'.");
|
||||
$"Market index '{sourceName}' is missing package source url/path for plugin '{pluginId}'.");
|
||||
EnsurePackageSourceUrl(normalizedUrl, sourceName, pluginId);
|
||||
|
||||
return new AirAppMarketPluginPackageSourceEntry
|
||||
@@ -770,6 +780,7 @@ internal sealed class AirAppMarketPluginPackageSourceEntry
|
||||
_ => normalizedKind
|
||||
},
|
||||
Url = normalizedUrl,
|
||||
Path = normalizedPath ?? string.Empty,
|
||||
SourceKind = sourceKind
|
||||
};
|
||||
}
|
||||
@@ -1240,6 +1251,7 @@ internal sealed class AirAppMarketPluginEntry
|
||||
{
|
||||
return compatibility is not null &&
|
||||
(!string.IsNullOrWhiteSpace(compatibility.MinHostVersion) ||
|
||||
!string.IsNullOrWhiteSpace(compatibility.ApiVersion) ||
|
||||
!string.IsNullOrWhiteSpace(compatibility.PluginApiVersion));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user