This commit is contained in:
lincube
2026-03-20 22:37:37 +08:00
parent 20cd6041a7
commit 33baaa579d
92 changed files with 1149 additions and 2752 deletions

View File

@@ -156,7 +156,7 @@ public sealed class PluginLoader
var pluginType = ResolvePluginType(assembly);
plugin = CreatePluginInstance(pluginType);
AppLogger.Info("PluginLoader", $"Plugin instance created. PluginId='{manifest.Id}'; PluginType='{pluginType.FullName}'.");
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties);
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties, services);
var serviceCollection = CreateServiceCollection(runtimeContext, services);
var hostBuilderContext = CreateHostBuilderContext(runtimeContext);
@@ -297,13 +297,15 @@ public sealed class PluginLoader
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
IReadOnlyDictionary<string, object?>? properties)
IReadOnlyDictionary<string, object?>? properties,
IServiceProvider? hostServices)
{
return new PluginRuntimeContext(
manifest,
pluginDirectory,
dataDirectory,
CreateReadOnlyProperties(properties));
CreateReadOnlyProperties(properties),
BuildAppearanceSnapshot(hostServices));
}
private ServiceCollection CreateServiceCollection(
@@ -313,6 +315,7 @@ public sealed class PluginLoader
var services = new ServiceCollection();
services.AddSingleton(runtimeContext);
services.AddSingleton<IPluginRuntimeContext>(runtimeContext);
services.AddSingleton<IPluginAppearanceContext>(runtimeContext.Appearance);
services.AddSingleton(runtimeContext.Manifest);
services.AddSingleton<IReadOnlyDictionary<string, object?>>(runtimeContext.Properties);
services.AddSingleton<IPluginMessageBus, PluginMessageBus>();
@@ -332,6 +335,33 @@ public sealed class PluginLoader
return services;
}
private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices)
{
var defaultSnapshot = new PluginAppearanceSnapshot(
GlobalCornerRadiusScale: 1d,
CornerRadiusTokens: new PluginCornerRadiusTokens(6, 10, 14, 18, 24, 30, 36),
ThemeVariant: "Unknown");
if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService)
{
return defaultSnapshot;
}
try
{
var hostSnapshot = appearanceThemeService.GetCurrent();
return new PluginAppearanceSnapshot(
GlobalCornerRadiusScale: Math.Max(0d, hostSnapshot.GlobalCornerRadiusScale),
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(hostSnapshot.CornerRadiusTokens),
ThemeVariant: hostSnapshot.IsNightMode ? "Dark" : "Light");
}
catch (Exception ex)
{
AppLogger.Warn("PluginLoader", "Failed to resolve host appearance snapshot for plugin runtime context.", ex);
return defaultSnapshot;
}
}
private static void RegisterHostService<TService>(IServiceCollection services, IServiceProvider? hostServices)
where TService : class
{
@@ -730,12 +760,14 @@ public sealed class PluginLoader
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
IReadOnlyDictionary<string, object?> properties)
IReadOnlyDictionary<string, object?> properties,
PluginAppearanceSnapshot appearanceSnapshot)
{
Manifest = manifest;
PluginDirectory = pluginDirectory;
DataDirectory = dataDirectory;
Properties = properties;
Appearance = new PluginAppearanceContext(appearanceSnapshot);
Services = NullServiceProvider.Instance;
}
@@ -749,6 +781,8 @@ public sealed class PluginLoader
public IReadOnlyDictionary<string, object?> Properties { get; }
public IPluginAppearanceContext Appearance { get; }
public T? GetService<T>()
{
return (T?)Services.GetService(typeof(T));

View File

@@ -90,16 +90,30 @@ internal static class AirAppMarketDefaults
private static string? TryResolveWorkspacePath(string repositoryName, string relativePath)
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
while (current is not null && current.Exists)
{
var candidate = Path.Combine(current.FullName, repositoryName);
if (Directory.Exists(candidate))
var solutionPath = Path.Combine(current.FullName, "LanMountainDesktop.slnx");
if (File.Exists(solutionPath))
{
var candidatePath = Path.GetFullPath(Path.Combine(candidate, relativePath));
var workspaceRoot = current.Parent;
if (workspaceRoot is null)
{
return null;
}
var candidateRepositoryPath = Path.Combine(workspaceRoot.FullName, repositoryName);
if (!Directory.Exists(candidateRepositoryPath))
{
return null;
}
var candidatePath = Path.GetFullPath(Path.Combine(candidateRepositoryPath, relativePath));
if (File.Exists(candidatePath))
{
return candidatePath;
}
return null;
}
current = current.Parent;

View File

@@ -1,53 +1,26 @@
# 宿主侧插件运行时 / Host Plugin Runtime
## 中文
本目录保存阑山桌面宿主侧插件运行时实现。
### 主要职责
- 发现、安装和替换 `.laapp` 插件包
- 加载插件程序集和共享契约
- 接入插件设置页、桌面组件与市场界面
-`3.0.0` API 基线插件构建插件作用域的 `IServiceCollection` / `ServiceProvider`
- 在激活前解析共享契约缓存,并暴露显式插件导出
### 与 LanAirApp 的分工
- `LanAirApp` 负责官方市场索引、开发文档、校验工具和镜像样例
- 本目录负责宿主运行时发现、安装、加载和界面接入
- 权威示例插件是独立仓库 `LanMountainDesktop.SamplePlugin``LanAirApp` 中的样例目录只是镜像模板
### 市场安装顺序
1. 宿主读取官方 `LanAirApp/airappmarket/index.json`
2. 若条目同时包含 `releaseTag``releaseAssetName`,优先解析 GitHub Release 资产
3. 若 Release 解析失败,则回退到仓库根目录 `.laapp`
4. 插件详情始终读取插件仓库根目录 `README.md`
5. 市场安装为暂存安装,重启后生效
## English
# Host Plugin Runtime
This directory contains the host-side plugin runtime for LanMountainDesktop.
### Responsibilities
## Responsibilities
- discover, install, and replace `.laapp` packages
- load plugin assemblies and shared contracts
- integrate plugin settings pages, desktop components, and market UI
- build a plugin-scoped `IServiceCollection` / `ServiceProvider` for API `3.0.0` plugins
- resolve shared contract caches before activation and expose explicit plugin exports
- Discover, install, replace, and stage `.laapp` plugin packages
- Load plugin assemblies and shared contracts
- Integrate plugin settings sections, desktop components, and market UI
- Build plugin-scoped `IServiceCollection` / `ServiceProvider` for API `4.x` plugins
- Resolve shared contracts before activation and expose explicit plugin exports
### Relationship with LanAirApp
## Relationship with LanAirApp
- `LanAirApp` owns the official market index, developer docs, validation tools, and mirrored sample templates
- this directory owns host-side discovery, installation, loading, and UI integration
- the authoritative sample plugin lives in the standalone `LanMountainDesktop.SamplePlugin` repository; the `LanAirApp` sample directory is only a mirror/template copy
- `LanAirApp` is a standalone repository and owns market metadata plus developer ecosystem materials
- This host runtime only consumes market metadata and plugin packages
- The host no longer maintains an embedded `LanAirApp/` mirror inside this repository
- Workspace debugging resolves market files from sibling path `..\\LanAirApp\\...`
### Market install order
## Market Install Flow
1. The host reads the official `LanAirApp/airappmarket/index.json`
2. If an entry contains both `releaseTag` and `releaseAssetName`, the host first resolves the exact GitHub Release asset
3. If Release resolution fails, the host falls back to the repository-root `.laapp`
4. Plugin details always come from the plugin repository root `README.md`
5. Market installs are staged and take effect after restart
1. Host reads the official market index
2. If both `releaseTag` and `releaseAssetName` are present, host resolves the exact GitHub Release asset first
3. If release resolution fails, host falls back to repository-root `.laapp`
4. Plugin detail text is read from plugin repository root `README.md`
5. Installation is staged and becomes effective after restart