diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae65a6b..2f2ca22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# 更新日志 / Changelog
+## [0.8.3.4](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.4) - 2026-04-12
+
+### 新增 (Added)
+
+- 无
+
+### 变更 (Changed)
+
+- ✨ **插件设置页面支持 View 展示**: 插件设置页面现在支持使用 View 进行展示
+ - 插件开发者可以通过 View 自定义设置页面的 UI 和交互
+ - 提供更灵活的设置页面展示方式,提升插件用户体验
+ - 兼容原有的设置方式,平滑过渡
+
+### 修复 (Fixed)
+
+- 无
+
+### 移除 (Removed)
+
+- 无
+
+***
+
## [0.8.3.3](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.3) - 2026-04-12
### 新增 (Added)
diff --git a/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj b/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj
index 4024a34..8adcb54 100644
--- a/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj
+++ b/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj
@@ -4,7 +4,7 @@
net10.0
enable
enable
- 4.0.0
+ 4.0.1
LanMountainDesktop.PluginSdk
true
LanMountainDesktop
diff --git a/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs b/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
index 0a41024..8af5b8d 100644
--- a/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
+++ b/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
public static class PluginSdkInfo
{
- public const string ApiVersion = "4.0.0";
+ public const string ApiVersion = "4.0.1";
public const string ManifestFileName = "plugin.json";
public const string PackageFileExtension = ".laapp";
public const string DataDirectoryName = "Data";
diff --git a/LanMountainDesktop.PluginSdk/README.md b/LanMountainDesktop.PluginSdk/README.md
index 4717217..2198e83 100644
--- a/LanMountainDesktop.PluginSdk/README.md
+++ b/LanMountainDesktop.PluginSdk/README.md
@@ -14,7 +14,7 @@ Official SDK package for LanMountainDesktop plugins.
```xml
-
+
```
diff --git a/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs b/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
index 22348d6..1f0d66c 100644
--- a/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
+++ b/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
@@ -9,5 +9,6 @@ public enum SettingsPageCategory
PluginCatalog = 35,
[Obsolete("Use PluginCatalog instead.")]
PluginMarket = 35,
- About = 40
+ About = 40,
+ Dev = 50
}
diff --git a/LanMountainDesktop.PluginTemplate/content/plugin.json b/LanMountainDesktop.PluginTemplate/content/plugin.json
index 5de5e7d..eeabb40 100644
--- a/LanMountainDesktop.PluginTemplate/content/plugin.json
+++ b/LanMountainDesktop.PluginTemplate/content/plugin.json
@@ -4,7 +4,7 @@
"description": "__PLUGIN_DESCRIPTION__",
"author": "__PLUGIN_AUTHOR__",
"version": "1.0.0",
- "apiVersion": "4.0.0",
+ "apiVersion": "4.0.1",
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
"sharedContracts": []
}
diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
index 120a418..7fb4155 100644
--- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
@@ -154,6 +154,10 @@ public sealed class AppSettingsSnapshot
public List DisabledPluginIds { get; set; } = [];
+ public bool IsDevModeEnabled { get; set; }
+
+ public string? DevPluginPath { get; set; }
+
#region Study Settings
public bool StudyEnabled { get; set; } = true;
diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs
index d1b4f07..5b41667 100644
--- a/LanMountainDesktop/Program.cs
+++ b/LanMountainDesktop/Program.cs
@@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.WebView.Desktop;
using LanMountainDesktop.DesktopHost;
using LanMountainDesktop.Models;
+using LanMountainDesktop.Plugins;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
@@ -19,6 +20,7 @@ public sealed class Program
public static void Main(string[] args)
{
AppLogger.Initialize();
+ DevPluginOptions.Parse(args);
RegisterGlobalExceptionLogging();
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
diff --git a/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs b/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs
index 043d18a..4056516 100644
--- a/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
+using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Plugins;
using LanMountainDesktop.Services.PluginMarket;
@@ -204,6 +205,10 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
string? pluginId,
bool isBuiltIn)
{
+ var isDevModeEnabled = _settingsFacade.Settings
+ .LoadSnapshot(SettingsScope.App)
+ .IsDevModeEnabled;
+
foreach (var pageType in assembly.GetTypes()
.Where(type => !type.IsAbstract && typeof(SettingsPageBase).IsAssignableFrom(type)))
{
@@ -214,6 +219,12 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
}
var category = isBuiltIn ? pageInfo.Category : SettingsPageCategory.Plugins;
+
+ if (category == SettingsPageCategory.Dev && !isDevModeEnabled)
+ {
+ continue;
+ }
+
var sortOrder = isBuiltIn ? pageInfo.SortOrder : 100 + pageInfo.SortOrder;
var title = ResolveLocalizedText(pageInfo.TitleLocalizationKey, pageInfo.Name);
var description = ResolveLocalizedText(pageInfo.DescriptionLocalizationKey, null);
diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
index 941e257..5a46eb9 100644
--- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs
+++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
@@ -3088,3 +3088,54 @@ public sealed class PluginGeneratedSettingsPageViewModel
public string? Description { get; }
}
+
+public sealed partial class DevSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private bool _isInitializing;
+
+ public DevSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _isInitializing = true;
+ LoadSettings();
+ _isInitializing = false;
+ }
+
+ [ObservableProperty]
+ private bool _isDevModeEnabled;
+
+ [ObservableProperty]
+ private string _devPluginPath = string.Empty;
+
+ partial void OnIsDevModeEnabledChanged(bool value)
+ {
+ if (_isInitializing) return;
+ SaveField(nameof(AppSettingsSnapshot.IsDevModeEnabled), value);
+ }
+
+ partial void OnDevPluginPathChanged(string value)
+ {
+ if (_isInitializing) return;
+ SaveField(nameof(AppSettingsSnapshot.DevPluginPath), value);
+ }
+
+ private void LoadSettings()
+ {
+ var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App);
+ IsDevModeEnabled = snapshot.IsDevModeEnabled;
+ DevPluginPath = snapshot.DevPluginPath ?? string.Empty;
+ }
+
+ private void SaveField(string key, T value)
+ {
+ var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App);
+ var property = typeof(AppSettingsSnapshot).GetProperty(key);
+ if (property is not null && property.CanWrite)
+ {
+ property.SetValue(snapshot, value);
+ }
+
+ _settingsFacade.Settings.SaveSnapshot(SettingsScope.App, snapshot, changedKeys: [key]);
+ }
+}
diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
index fc835ef..e9c5551 100644
--- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
@@ -36,7 +36,8 @@
+ Height="240"
+ PointerPressed="OnAboutHeroCardPointerPressed">
3)
+ {
+ _heroCardClickCount = 1;
+ }
+ else
+ {
+ _heroCardClickCount++;
+ }
+
+ _lastHeroCardClickTime = now;
+
+ var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
+ var snapshot = settingsFacade.Settings.LoadSnapshot(SettingsScope.App);
+
+ if (snapshot.IsDevModeEnabled)
+ {
+ if (_heroCardClickCount >= 3)
+ {
+ _heroCardClickCount = 0;
+ _ = ShowMessageAsync("开发者模式", "开发者模式已启用,无需重复操作。");
+ }
+
+ return;
+ }
+
+ var remaining = DevModeActivationClicks - _heroCardClickCount;
+
+ if (remaining <= 0)
+ {
+ _heroCardClickCount = 0;
+ PromptEnableDevMode(settingsFacade);
+ }
+ else if (remaining <= 2)
+ {
+ Debug.WriteLine($"[AboutSettingsPage] 再点击 {remaining} 次即可启用开发者模式。");
+ }
+ }
+
+ private async void PromptEnableDevMode(ISettingsFacadeService settingsFacade)
+ {
+ var dialog = new ContentDialog
+ {
+ Title = "启用开发者模式",
+ Content = "开发者模式提供了插件调试、热重载等高级功能,仅供开发和调试用途。\n\n" +
+ "请注意:开发者不对以非开发用途使用此功能造成的任何后果负责,也不接受以非开发用途使用时产生的 Bug 反馈。\n\n" +
+ "确定要启用开发者模式吗?",
+ PrimaryButtonText = "启用",
+ CloseButtonText = "取消",
+ DefaultButton = ContentDialogButton.Close
+ };
+
+ var result = await dialog.ShowAsync();
+ if (result != ContentDialogResult.Primary)
+ {
+ return;
+ }
+
+ var snapshot = settingsFacade.Settings.LoadSnapshot(SettingsScope.App);
+ snapshot.IsDevModeEnabled = true;
+ settingsFacade.Settings.SaveSnapshot(
+ SettingsScope.App,
+ snapshot,
+ changedKeys: [nameof(AppSettingsSnapshot.IsDevModeEnabled)]);
+
+ AppLogger.Info("DevMode", "Developer mode enabled via About page activation.");
+
+ _ = ShowMessageAsync("开发者模式", "已启用开发者模式。重新打开设置窗口即可看到开发者选项。");
+
+ if (HostContext is not null)
+ {
+ HostContext.RequestRestart("开发者模式已更改");
+ }
+ }
+
+ private static async Task ShowMessageAsync(string title, string message)
+ {
+ var dialog = new ContentDialog
+ {
+ Title = title,
+ Content = message,
+ CloseButtonText = "确定"
+ };
+ await dialog.ShowAsync();
+ }
}
diff --git a/LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml
new file mode 100644
index 0000000..65ba67c
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml.cs
new file mode 100644
index 0000000..fa16ae0
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml.cs
@@ -0,0 +1,30 @@
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.ViewModels;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+[SettingsPageInfo(
+ "dev",
+ "开发者",
+ SettingsPageCategory.Dev,
+ IconKey = "DeveloperBoard",
+ SortOrder = 0,
+ TitleLocalizationKey = "settings.dev.title",
+ DescriptionLocalizationKey = "settings.dev.description")]
+public partial class DevSettingsPage : SettingsPageBase
+{
+ public DevSettingsPage()
+ : this(new DevSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
+ {
+ }
+
+ public DevSettingsPage(DevSettingsPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = ViewModel;
+ InitializeComponent();
+ }
+
+ public DevSettingsPageViewModel ViewModel { get; }
+}
diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
index 6721481..4b426aa 100644
--- a/LanMountainDesktop/Views/SettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
@@ -734,8 +734,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
"Info" => Symbol.Info,
"ArrowSync" => Symbol.ArrowSync,
"Hourglass" => Symbol.Hourglass,
- "Alert" => Symbol.Alert, // 铃铛图标
- "Bell" => Symbol.Alert, // Bell也映射到Alert图标
+ "Alert" => Symbol.Alert,
+ "Bell" => Symbol.Alert,
+ "DeveloperBoard" => Symbol.DeveloperBoard,
+ "FolderLink" => Symbol.FolderLink,
+ "WindowConsole" => Symbol.WindowConsole,
_ => Symbol.Settings
};
}
diff --git a/LanMountainDesktop/plugins/DevPluginOptions.cs b/LanMountainDesktop/plugins/DevPluginOptions.cs
new file mode 100644
index 0000000..3be9a48
--- /dev/null
+++ b/LanMountainDesktop/plugins/DevPluginOptions.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using LanMountainDesktop.Services;
+
+namespace LanMountainDesktop.Plugins;
+
+public sealed class DevPluginOptions
+{
+ private static readonly string[] DevPluginPathArgs = ["--dev-plugin", "-dp"];
+ private static readonly string[] DevModeArgs = ["--dev-mode", "-dev"];
+ private static readonly string[] HotReloadArgs = ["--hot-reload", "-hr"];
+ private static readonly string EnvDevPluginPath = "LMD_DEV_PLUGIN";
+ private static readonly string EnvDevMode = "LMD_DEV_MODE";
+
+ public static DevPluginOptions Current { get; } = new();
+
+ public bool IsDevMode { get; private set; }
+
+ public string? DevPluginPath { get; private set; }
+
+ public bool EnableHotReload { get; private set; }
+
+ public IReadOnlyList DevPluginPaths { get; private set; } = Array.Empty();
+
+ private DevPluginOptions() { }
+
+ public static DevPluginOptions Parse(string[] args)
+ {
+ var options = Current;
+
+ options.IsDevMode = TryGetFlag(args, DevModeArgs) ||
+ string.Equals(Environment.GetEnvironmentVariable(EnvDevMode), "1", StringComparison.Ordinal) ||
+ string.Equals(Environment.GetEnvironmentVariable(EnvDevMode), "true", StringComparison.OrdinalIgnoreCase);
+
+ options.DevPluginPath = TryGetValue(args, DevPluginPathArgs) ??
+ Environment.GetEnvironmentVariable(EnvDevPluginPath)?.Trim();
+
+ options.EnableHotReload = TryGetFlag(args, HotReloadArgs);
+
+ if (!options.IsDevMode && !string.IsNullOrWhiteSpace(options.DevPluginPath))
+ {
+ options.IsDevMode = true;
+ }
+
+ options.DevPluginPaths = ResolveDevPluginPaths(options.DevPluginPath);
+
+ if (options.IsDevMode)
+ {
+ AppLogger.Info(
+ "DevPlugin",
+ $"Developer mode enabled. DevPluginPath='{options.DevPluginPath}'; EnableHotReload={options.EnableHotReload}; ResolvedPaths={options.DevPluginPaths.Count}.");
+ }
+
+ return options;
+ }
+
+ internal void ApplySettingsFromSnapshot(bool isDevMode, string? devPluginPath)
+ {
+ if (isDevMode && !IsDevMode)
+ {
+ IsDevMode = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(devPluginPath) && string.IsNullOrWhiteSpace(DevPluginPath))
+ {
+ DevPluginPath = devPluginPath;
+ }
+
+ var allPaths = new List(DevPluginPaths);
+ if (!string.IsNullOrWhiteSpace(devPluginPath))
+ {
+ foreach (var path in ResolveDevPluginPaths(devPluginPath))
+ {
+ if (!allPaths.Contains(path, StringComparer.OrdinalIgnoreCase))
+ {
+ allPaths.Add(path);
+ }
+ }
+ }
+
+ DevPluginPaths = allPaths;
+ }
+
+ private static IReadOnlyList ResolveDevPluginPaths(string? rawPath)
+ {
+ if (string.IsNullOrWhiteSpace(rawPath))
+ {
+ return Array.Empty();
+ }
+
+ var paths = rawPath.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ var resolved = new List();
+ foreach (var path in paths)
+ {
+ try
+ {
+ var fullPath = Path.GetFullPath(path);
+ if (Directory.Exists(fullPath) || File.Exists(fullPath))
+ {
+ resolved.Add(fullPath);
+ }
+ else
+ {
+ AppLogger.Warn("DevPlugin", $"Developer plugin path '{path}' does not exist. It will be skipped.");
+ }
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("DevPlugin", $"Failed to resolve developer plugin path '{path}': {ex.Message}");
+ }
+ }
+
+ return resolved;
+ }
+
+ private static bool TryGetFlag(string[] args, string[] names)
+ {
+ return args.Any(arg => names.Any(name => string.Equals(arg, name, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ private static string? TryGetValue(string[] args, string[] names)
+ {
+ for (var i = 0; i < args.Length - 1; i++)
+ {
+ if (names.Any(name => string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)))
+ {
+ return args[i + 1]?.Trim();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/LanMountainDesktop/plugins/PluginCatalogEntry.cs b/LanMountainDesktop/plugins/PluginCatalogEntry.cs
index 8fe99b5..7929089 100644
--- a/LanMountainDesktop/plugins/PluginCatalogEntry.cs
+++ b/LanMountainDesktop/plugins/PluginCatalogEntry.cs
@@ -5,7 +5,8 @@ namespace LanMountainDesktop.Services;
public enum PluginCatalogSourceKind
{
Package = 0,
- Manifest = 1
+ Manifest = 1,
+ DevPlugin = 2
}
public sealed record PluginCatalogEntry(
@@ -16,4 +17,5 @@ public sealed record PluginCatalogEntry(
bool IsLoaded,
string? ErrorMessage,
int SettingsPageCount,
- int WidgetCount);
+ int WidgetCount,
+ bool IsDevPlugin = false);
diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs
index 7bc2e25..68a85f2 100644
--- a/LanMountainDesktop/plugins/PluginLoader.cs
+++ b/LanMountainDesktop/plugins/PluginLoader.cs
@@ -146,7 +146,7 @@ public sealed class PluginLoader
try
{
Directory.CreateDirectory(dataDirectory);
- ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory);
+ ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory, _options.IsDevMode);
AppLogger.Info(
"PluginLoader",
$"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
@@ -721,13 +721,23 @@ public sealed class PluginLoader
private static void ValidatePluginRuntimeAssets(
PluginManifest manifest,
string assemblyPath,
- string pluginDirectory)
+ string pluginDirectory,
+ bool isDevMode)
{
var depsFilePath = Path.ChangeExtension(assemblyPath, ".deps.json");
if (!File.Exists(depsFilePath))
{
- throw new InvalidOperationException(
- $"Plugin '{manifest.Id}' targets API {PluginSdkInfo.ApiVersion} and must include '{Path.GetFileName(depsFilePath)}' next to its main assembly.");
+ if (isDevMode)
+ {
+ AppLogger.Warn(
+ "PluginLoader",
+ $"Plugin '{manifest.Id}' is missing '{Path.GetFileName(depsFilePath)}'. In developer mode this is allowed, but dependency resolution may fail at runtime.");
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"Plugin '{manifest.Id}' targets API {PluginSdkInfo.ApiVersion} and must include '{Path.GetFileName(depsFilePath)}' next to its main assembly.");
+ }
}
var runtimesDirectory = Path.Combine(pluginDirectory, "runtimes");
diff --git a/LanMountainDesktop/plugins/PluginLoaderOptions.cs b/LanMountainDesktop/plugins/PluginLoaderOptions.cs
index b4f4bf2..5fbb1b9 100644
--- a/LanMountainDesktop/plugins/PluginLoaderOptions.cs
+++ b/LanMountainDesktop/plugins/PluginLoaderOptions.cs
@@ -19,6 +19,8 @@ public sealed class PluginLoaderOptions
public string PackagedDataDirectoryName { get; init; } = PluginSdkInfo.PackagedDataDirectoryName;
+ public bool IsDevMode { get; init; }
+
public ISet SharedAssemblyNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase)
{
typeof(IPlugin).Assembly.GetName().Name!
diff --git a/LanMountainDesktop/plugins/PluginRuntimeService.cs b/LanMountainDesktop/plugins/PluginRuntimeService.cs
index 1be2753..c5cad4e 100644
--- a/LanMountainDesktop/plugins/PluginRuntimeService.cs
+++ b/LanMountainDesktop/plugins/PluginRuntimeService.cs
@@ -85,6 +85,7 @@ public sealed class PluginRuntimeService : IDisposable
Directory.CreateDirectory(PluginsDirectory);
ApplyPendingPluginDeletions();
UnloadInstalledPlugins();
+ MergeDevSettingsFromSnapshot();
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds();
@@ -108,19 +109,30 @@ public sealed class PluginRuntimeService : IDisposable
var selectedPluginIds = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates)
{
+ var isDevPlugin = candidate.SourceKind == PluginCatalogSourceKind.DevPlugin;
+
if (!selectedPluginIds.Add(candidate.Manifest.Id))
{
- var duplicateFailure = PluginLoadResult.Failure(
- candidate.SourcePath,
- candidate.Manifest,
- new InvalidOperationException(
- $"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
- _loadResults.Add(duplicateFailure);
- LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
- continue;
+ if (isDevPlugin)
+ {
+ AppLogger.Info(
+ "DevPlugin",
+ $"Developer plugin '{candidate.Manifest.Id}' overrides an already-registered plugin from '{candidate.SourcePath}'.");
+ }
+ else
+ {
+ var duplicateFailure = PluginLoadResult.Failure(
+ candidate.SourcePath,
+ candidate.Manifest,
+ new InvalidOperationException(
+ $"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
+ _loadResults.Add(duplicateFailure);
+ LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
+ continue;
+ }
}
- var isEnabled = !disabledPluginIds.Contains(candidate.Manifest.Id);
+ var isEnabled = isDevPlugin || !disabledPluginIds.Contains(candidate.Manifest.Id);
if (!isEnabled)
{
_catalog.Add(new PluginCatalogEntry(
@@ -172,6 +184,10 @@ public sealed class PluginRuntimeService : IDisposable
PluginsDirectory,
services: _hostServices,
hostProperties),
+ PluginCatalogSourceKind.DevPlugin => _loader.LoadFromManifest(
+ candidate.SourcePath,
+ services: _hostServices,
+ hostProperties),
_ => _loader.LoadFromManifest(
candidate.SourcePath,
services: _hostServices,
@@ -192,7 +208,8 @@ public sealed class PluginRuntimeService : IDisposable
true,
null,
loadResult.LoadedPlugin.SettingsSections.Count,
- loadResult.LoadedPlugin.DesktopComponents.Count));
+ loadResult.LoadedPlugin.DesktopComponents.Count,
+ IsDevPlugin: isDevPlugin));
AppLogger.Info(
"PluginRuntime",
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? ""}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? ""}'; SourceKind='{candidate.SourceKind}'; SettingsSections={loadResult.LoadedPlugin.SettingsSections.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}; Editors={loadResult.LoadedPlugin.DesktopComponentEditors.Count}.");
@@ -208,7 +225,8 @@ public sealed class PluginRuntimeService : IDisposable
false,
loadResult.Error?.Message,
0,
- 0));
+ 0,
+ IsDevPlugin: isDevPlugin));
LogPluginFailure("Load", loadResult, treatAsError: true);
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
}
@@ -229,6 +247,14 @@ public sealed class PluginRuntimeService : IDisposable
return false;
}
+ var catalogEntry = _catalog.FirstOrDefault(entry =>
+ string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
+ if (catalogEntry.IsDevPlugin && !isEnabled)
+ {
+ AppLogger.Warn("DevPlugin", $"Cannot disable developer plugin '{pluginId}'. Developer plugins are always enabled in dev mode.");
+ return false;
+ }
+
var snapshot = LoadAppSettingsSnapshot();
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
@@ -459,12 +485,74 @@ public sealed class PluginRuntimeService : IDisposable
}
}
+ DiscoverDevPluginCandidates(candidates, failures);
+
return candidates
- .OrderBy(candidate => candidate.SourceKind)
+ .OrderByDescending(candidate => candidate.SourceKind)
.ThenBy(candidate => candidate.SourcePath, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
+ private void DiscoverDevPluginCandidates(List candidates, List failures)
+ {
+ var devOptions = DevPluginOptions.Current;
+ if (!devOptions.IsDevMode || devOptions.DevPluginPaths.Count == 0)
+ {
+ return;
+ }
+
+ AppLogger.Info("DevPlugin", $"Scanning developer plugin paths. Count={devOptions.DevPluginPaths.Count}.");
+
+ foreach (var devPath in devOptions.DevPluginPaths)
+ {
+ if (File.Exists(devPath) && string.Equals(Path.GetExtension(devPath), PluginSdkInfo.PackageFileExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var manifest = ReadManifestFromPackage(devPath);
+ candidates.Add(new PluginCandidate(devPath, manifest, PluginCatalogSourceKind.DevPlugin));
+ AppLogger.Info("DevPlugin", $"Found developer plugin package. PluginId='{manifest.Id}'; Path='{devPath}'.");
+ }
+ catch (Exception ex)
+ {
+ var failure = PluginLoadResult.Failure(devPath, null, ex);
+ failures.Add(failure);
+ AppLogger.Warn("DevPlugin", $"Failed to read developer plugin package '{devPath}'.", ex);
+ }
+
+ continue;
+ }
+
+ if (Directory.Exists(devPath))
+ {
+ var manifestPath = Path.Combine(devPath, PluginSdkInfo.ManifestFileName);
+ if (File.Exists(manifestPath))
+ {
+ try
+ {
+ var manifest = PluginManifest.Load(manifestPath);
+ candidates.Add(new PluginCandidate(manifestPath, manifest, PluginCatalogSourceKind.DevPlugin));
+ AppLogger.Info("DevPlugin", $"Found developer plugin manifest. PluginId='{manifest.Id}'; Path='{manifestPath}'.");
+ }
+ catch (Exception ex)
+ {
+ var failure = PluginLoadResult.Failure(manifestPath, null, ex);
+ failures.Add(failure);
+ AppLogger.Warn("DevPlugin", $"Failed to load developer plugin manifest '{manifestPath}'.", ex);
+ }
+ }
+ else
+ {
+ AppLogger.Warn("DevPlugin", $"Developer plugin directory '{devPath}' does not contain '{PluginSdkInfo.ManifestFileName}'. Skipping.");
+ }
+
+ continue;
+ }
+
+ AppLogger.Warn("DevPlugin", $"Developer plugin path '{devPath}' is neither a file nor a directory. Skipping.");
+ }
+ }
+
private IEnumerable EnumerateCandidatePaths(string searchPattern)
{
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(PluginsDirectory), ".runtime"));
@@ -582,7 +670,8 @@ public sealed class PluginRuntimeService : IDisposable
private static PluginLoaderOptions CreateOptions()
{
- var options = new PluginLoaderOptions();
+ var devOptions = DevPluginOptions.Current;
+ var options = new PluginLoaderOptions { IsDevMode = devOptions.IsDevMode };
AddSharedAssembly(options, typeof(App).Assembly);
AddSharedAssembly(options, typeof(IServiceCollection).Assembly);
AddSharedAssembly(options, typeof(HostBuilderContext).Assembly);
@@ -614,6 +703,31 @@ public sealed class PluginRuntimeService : IDisposable
}
}
+ private void MergeDevSettingsFromSnapshot()
+ {
+ var devOptions = DevPluginOptions.Current;
+
+ try
+ {
+ var snapshot = LoadAppSettingsSnapshot();
+
+ if (snapshot.IsDevModeEnabled && !devOptions.IsDevMode)
+ {
+ devOptions.ApplySettingsFromSnapshot(isDevMode: true, devPluginPath: snapshot.DevPluginPath);
+ AppLogger.Info("DevPlugin", $"Developer mode enabled via settings. DevPluginPath='{snapshot.DevPluginPath}'.");
+ }
+ else if (!string.IsNullOrWhiteSpace(snapshot.DevPluginPath) && string.IsNullOrWhiteSpace(devOptions.DevPluginPath))
+ {
+ devOptions.ApplySettingsFromSnapshot(isDevMode: devOptions.IsDevMode, devPluginPath: snapshot.DevPluginPath);
+ AppLogger.Info("DevPlugin", $"Developer plugin path merged from settings. DevPluginPath='{snapshot.DevPluginPath}'.");
+ }
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("DevPlugin", "Failed to merge developer settings from snapshot.", ex);
+ }
+ }
+
private void CollectContributions(LoadedPlugin loadedPlugin)
{
_exportRegistry.ReplaceExports(loadedPlugin.Manifest.Id, loadedPlugin.ExportedServices);
@@ -826,6 +940,13 @@ public sealed class PluginRuntimeService : IDisposable
_settingsCatalogService.RemovePluginSections(pluginId);
}
+ private enum PluginCatalogSourceKind
+ {
+ Package = 0,
+ Manifest = 1,
+ DevPlugin = 2
+ }
+
private sealed record PluginCandidate(
string SourcePath,
PluginManifest Manifest,
diff --git a/README.md b/README.md
index c561a1c..fcf9986 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ dotnet new install LanMountainDesktop.PluginTemplate
dotnet new lmd-plugin -n MyPlugin
```
-- **Plugin SDK**: `LanMountainDesktop.PluginSdk` (API 4.0.0)
+- **Plugin SDK**: `LanMountainDesktop.PluginSdk` (API 4.0.1)
- **共享契约**: `LanMountainDesktop.Shared.Contracts`
- **迁移指南**: [PLUGIN_SDK_V4_MIGRATION.md](docs/PLUGIN_SDK_V4_MIGRATION.md)
diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md
index aea1a7a..be882e7 100644
--- a/docs/PRODUCT.md
+++ b/docs/PRODUCT.md
@@ -39,7 +39,7 @@
### 当前阶段
- 产品版本:`1.0.0`
-- Plugin SDK API 基线:`4.0.0`
+- Plugin SDK API 基线:`4.0.1`
- 当前重点:持续完善宿主体验、设置页体验、组件能力与插件生态
- 近期需求入口:以 `.trae/specs/` 中的 feature spec 为准
@@ -59,4 +59,4 @@
LanMountainDesktop is a cross-platform desktop enhancement product built with Avalonia UI and .NET 10. It targets students, office users, and customization-focused users who want a programmable desktop surface for information, tools, and plugin-driven extensions.
-This repository is the source of truth for the desktop host, plugin runtime, Plugin SDK, shared contracts, and core appearance/settings infrastructure. The current product version is `1.0.0`, and the active Plugin SDK baseline in this repository is `4.0.0`.
+This repository is the source of truth for the desktop host, plugin runtime, Plugin SDK, shared contracts, and core appearance/settings infrastructure. The current product version is `1.0.0`, and the active Plugin SDK baseline in this repository is `4.0.1`.