mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
feat.开发者调试工具
This commit is contained in:
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
|||||||
# 更新日志 / Changelog
|
# 更新日志 / 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
|
## [0.8.3.3](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.3) - 2026-04-12
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>4.0.0</Version>
|
<Version>4.0.1</Version>
|
||||||
<PackageId>LanMountainDesktop.PluginSdk</PackageId>
|
<PackageId>LanMountainDesktop.PluginSdk</PackageId>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<Authors>LanMountainDesktop</Authors>
|
<Authors>LanMountainDesktop</Authors>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
|
|||||||
|
|
||||||
public static class PluginSdkInfo
|
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 ManifestFileName = "plugin.json";
|
||||||
public const string PackageFileExtension = ".laapp";
|
public const string PackageFileExtension = ".laapp";
|
||||||
public const string DataDirectoryName = "Data";
|
public const string DataDirectoryName = "Data";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Official SDK package for LanMountainDesktop plugins.
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="4.0.0" />
|
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="4.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ public enum SettingsPageCategory
|
|||||||
PluginCatalog = 35,
|
PluginCatalog = 35,
|
||||||
[Obsolete("Use PluginCatalog instead.")]
|
[Obsolete("Use PluginCatalog instead.")]
|
||||||
PluginMarket = 35,
|
PluginMarket = 35,
|
||||||
About = 40
|
About = 40,
|
||||||
|
Dev = 50
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "__PLUGIN_DESCRIPTION__",
|
"description": "__PLUGIN_DESCRIPTION__",
|
||||||
"author": "__PLUGIN_AUTHOR__",
|
"author": "__PLUGIN_AUTHOR__",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"apiVersion": "4.0.0",
|
"apiVersion": "4.0.1",
|
||||||
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
|
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
|
||||||
"sharedContracts": []
|
"sharedContracts": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public List<string> DisabledPluginIds { get; set; } = [];
|
public List<string> DisabledPluginIds { get; set; } = [];
|
||||||
|
|
||||||
|
public bool IsDevModeEnabled { get; set; }
|
||||||
|
|
||||||
|
public string? DevPluginPath { get; set; }
|
||||||
|
|
||||||
#region Study Settings
|
#region Study Settings
|
||||||
|
|
||||||
public bool StudyEnabled { get; set; } = true;
|
public bool StudyEnabled { get; set; } = true;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Avalonia;
|
|||||||
using Avalonia.WebView.Desktop;
|
using Avalonia.WebView.Desktop;
|
||||||
using LanMountainDesktop.DesktopHost;
|
using LanMountainDesktop.DesktopHost;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Plugins;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ public sealed class Program
|
|||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
AppLogger.Initialize();
|
AppLogger.Initialize();
|
||||||
|
DevPluginOptions.Parse(args);
|
||||||
RegisterGlobalExceptionLogging();
|
RegisterGlobalExceptionLogging();
|
||||||
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
|
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Plugins;
|
using LanMountainDesktop.Plugins;
|
||||||
using LanMountainDesktop.Services.PluginMarket;
|
using LanMountainDesktop.Services.PluginMarket;
|
||||||
@@ -204,6 +205,10 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
|
|||||||
string? pluginId,
|
string? pluginId,
|
||||||
bool isBuiltIn)
|
bool isBuiltIn)
|
||||||
{
|
{
|
||||||
|
var isDevModeEnabled = _settingsFacade.Settings
|
||||||
|
.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App)
|
||||||
|
.IsDevModeEnabled;
|
||||||
|
|
||||||
foreach (var pageType in assembly.GetTypes()
|
foreach (var pageType in assembly.GetTypes()
|
||||||
.Where(type => !type.IsAbstract && typeof(SettingsPageBase).IsAssignableFrom(type)))
|
.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;
|
var category = isBuiltIn ? pageInfo.Category : SettingsPageCategory.Plugins;
|
||||||
|
|
||||||
|
if (category == SettingsPageCategory.Dev && !isDevModeEnabled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var sortOrder = isBuiltIn ? pageInfo.SortOrder : 100 + pageInfo.SortOrder;
|
var sortOrder = isBuiltIn ? pageInfo.SortOrder : 100 + pageInfo.SortOrder;
|
||||||
var title = ResolveLocalizedText(pageInfo.TitleLocalizationKey, pageInfo.Name);
|
var title = ResolveLocalizedText(pageInfo.TitleLocalizationKey, pageInfo.Name);
|
||||||
var description = ResolveLocalizedText(pageInfo.DescriptionLocalizationKey, null);
|
var description = ResolveLocalizedText(pageInfo.DescriptionLocalizationKey, null);
|
||||||
|
|||||||
@@ -3088,3 +3088,54 @@ public sealed class PluginGeneratedSettingsPageViewModel
|
|||||||
|
|
||||||
public string? Description { get; }
|
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<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
|
IsDevModeEnabled = snapshot.IsDevModeEnabled;
|
||||||
|
DevPluginPath = snapshot.DevPluginPath ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveField<T>(string key, T value)
|
||||||
|
{
|
||||||
|
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
<StackPanel Classes="about-page-container">
|
<StackPanel Classes="about-page-container">
|
||||||
<Border x:Name="AboutHeroCard"
|
<Border x:Name="AboutHeroCard"
|
||||||
Classes="about-hero-card"
|
Classes="about-hero-card"
|
||||||
Height="240">
|
Height="240"
|
||||||
|
PointerPressed="OnAboutHeroCardPointerPressed">
|
||||||
<Image Source="/Assets/about_banner.png"
|
<Image Source="/Assets/about_banner.png"
|
||||||
Stretch="Uniform"
|
Stretch="Uniform"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.ViewModels;
|
using LanMountainDesktop.ViewModels;
|
||||||
|
|
||||||
@@ -19,6 +25,10 @@ namespace LanMountainDesktop.Views.SettingsPages;
|
|||||||
public partial class AboutSettingsPage : SettingsPageBase
|
public partial class AboutSettingsPage : SettingsPageBase
|
||||||
{
|
{
|
||||||
private const double HeroAspectRatio = 9d / 16d;
|
private const double HeroAspectRatio = 9d / 16d;
|
||||||
|
private const int DevModeActivationClicks = 5;
|
||||||
|
|
||||||
|
private int _heroCardClickCount;
|
||||||
|
private DateTime _lastHeroCardClickTime = DateTime.MinValue;
|
||||||
|
|
||||||
public AboutSettingsPage()
|
public AboutSettingsPage()
|
||||||
: this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
: this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||||
@@ -60,4 +70,94 @@ public partial class AboutSettingsPage : SettingsPageBase
|
|||||||
|
|
||||||
AboutHeroCard.Height = targetHeight;
|
AboutHeroCard.Height = targetHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAboutHeroCardPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var elapsed = now - _lastHeroCardClickTime;
|
||||||
|
|
||||||
|
if (elapsed.TotalSeconds > 3)
|
||||||
|
{
|
||||||
|
_heroCardClickCount = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_heroCardClickCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastHeroCardClickTime = now;
|
||||||
|
|
||||||
|
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||||
|
var snapshot = settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(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<AppSettingsSnapshot>(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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
89
LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml
Normal file
89
LanMountainDesktop/Views/SettingsPages/DevSettingsPage.axaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||||
|
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||||
|
x:Class="LanMountainDesktop.Views.SettingsPages.DevSettingsPage"
|
||||||
|
x:DataType="vm:DevSettingsPageViewModel">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Classes="settings-page-container settings-page-animated">
|
||||||
|
|
||||||
|
<ui:InfoBar IsOpen="True"
|
||||||
|
IsClosable="False"
|
||||||
|
Severity="Warning"
|
||||||
|
Title="开发者模式"
|
||||||
|
Message="开发者模式仅供开发和调试用途。开发者不对以非开发用途使用此功能造成的任何后果负责。"
|
||||||
|
Margin="0,0,0,16" />
|
||||||
|
|
||||||
|
<ui:SettingsExpander Header="启用开发者模式"
|
||||||
|
Description="启用后可使用插件调试、开发者插件路径等高级功能">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="DeveloperBoard" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<ToggleSwitch IsChecked="{Binding IsDevModeEnabled}" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<Separator Classes="settings-separator" />
|
||||||
|
|
||||||
|
<ui:SettingsExpander Header="开发者插件路径"
|
||||||
|
Description="指定开发中的插件目录路径,无需打包即可直接加载。多个路径用分号分隔。">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="FolderLink" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<TextBox Text="{Binding DevPluginPath}"
|
||||||
|
Watermark="C:\path\to\plugin\bin\Debug\net10.0"
|
||||||
|
Width="360"
|
||||||
|
MinWidth="200" />
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<Separator Classes="settings-separator" />
|
||||||
|
|
||||||
|
<ui:SettingsExpander Header="命令行参数"
|
||||||
|
Description="也可以通过命令行参数或环境变量指定开发者插件路径">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="WindowConsole" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<StackPanel Margin="0,8,0,0" Spacing="8">
|
||||||
|
<TextBlock Text="命令行参数:" FontWeight="SemiBold" />
|
||||||
|
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="12,8">
|
||||||
|
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||||
|
FontSize="12"
|
||||||
|
Text="--dev-plugin <path> 或 -dp <path>"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="环境变量:" FontWeight="SemiBold" Margin="0,8,0,0" />
|
||||||
|
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="12,8">
|
||||||
|
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||||
|
FontSize="12"
|
||||||
|
Text="LMD_DEV_PLUGIN=<path>"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="其他参数:" FontWeight="SemiBold" Margin="0,8,0,0" />
|
||||||
|
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="12,8">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||||
|
FontSize="12"
|
||||||
|
Text="--dev-mode / -dev 启用开发者模式" />
|
||||||
|
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||||
|
FontSize="12"
|
||||||
|
Text="--hot-reload / -hr 启用热重载(预留)" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -734,8 +734,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
|||||||
"Info" => Symbol.Info,
|
"Info" => Symbol.Info,
|
||||||
"ArrowSync" => Symbol.ArrowSync,
|
"ArrowSync" => Symbol.ArrowSync,
|
||||||
"Hourglass" => Symbol.Hourglass,
|
"Hourglass" => Symbol.Hourglass,
|
||||||
"Alert" => Symbol.Alert, // 铃铛图标
|
"Alert" => Symbol.Alert,
|
||||||
"Bell" => Symbol.Alert, // Bell也映射到Alert图标
|
"Bell" => Symbol.Alert,
|
||||||
|
"DeveloperBoard" => Symbol.DeveloperBoard,
|
||||||
|
"FolderLink" => Symbol.FolderLink,
|
||||||
|
"WindowConsole" => Symbol.WindowConsole,
|
||||||
_ => Symbol.Settings
|
_ => Symbol.Settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
136
LanMountainDesktop/plugins/DevPluginOptions.cs
Normal file
136
LanMountainDesktop/plugins/DevPluginOptions.cs
Normal file
@@ -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<string> DevPluginPaths { get; private set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
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<string>(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<string> ResolveDevPluginPaths(string? rawPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawPath))
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths = rawPath.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
var resolved = new List<string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ namespace LanMountainDesktop.Services;
|
|||||||
public enum PluginCatalogSourceKind
|
public enum PluginCatalogSourceKind
|
||||||
{
|
{
|
||||||
Package = 0,
|
Package = 0,
|
||||||
Manifest = 1
|
Manifest = 1,
|
||||||
|
DevPlugin = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PluginCatalogEntry(
|
public sealed record PluginCatalogEntry(
|
||||||
@@ -16,4 +17,5 @@ public sealed record PluginCatalogEntry(
|
|||||||
bool IsLoaded,
|
bool IsLoaded,
|
||||||
string? ErrorMessage,
|
string? ErrorMessage,
|
||||||
int SettingsPageCount,
|
int SettingsPageCount,
|
||||||
int WidgetCount);
|
int WidgetCount,
|
||||||
|
bool IsDevPlugin = false);
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ public sealed class PluginLoader
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(dataDirectory);
|
Directory.CreateDirectory(dataDirectory);
|
||||||
ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory);
|
ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory, _options.IsDevMode);
|
||||||
AppLogger.Info(
|
AppLogger.Info(
|
||||||
"PluginLoader",
|
"PluginLoader",
|
||||||
$"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
|
$"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
|
||||||
@@ -721,13 +721,23 @@ public sealed class PluginLoader
|
|||||||
private static void ValidatePluginRuntimeAssets(
|
private static void ValidatePluginRuntimeAssets(
|
||||||
PluginManifest manifest,
|
PluginManifest manifest,
|
||||||
string assemblyPath,
|
string assemblyPath,
|
||||||
string pluginDirectory)
|
string pluginDirectory,
|
||||||
|
bool isDevMode)
|
||||||
{
|
{
|
||||||
var depsFilePath = Path.ChangeExtension(assemblyPath, ".deps.json");
|
var depsFilePath = Path.ChangeExtension(assemblyPath, ".deps.json");
|
||||||
if (!File.Exists(depsFilePath))
|
if (!File.Exists(depsFilePath))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
if (isDevMode)
|
||||||
$"Plugin '{manifest.Id}' targets API {PluginSdkInfo.ApiVersion} and must include '{Path.GetFileName(depsFilePath)}' next to its main assembly.");
|
{
|
||||||
|
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");
|
var runtimesDirectory = Path.Combine(pluginDirectory, "runtimes");
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public sealed class PluginLoaderOptions
|
|||||||
|
|
||||||
public string PackagedDataDirectoryName { get; init; } = PluginSdkInfo.PackagedDataDirectoryName;
|
public string PackagedDataDirectoryName { get; init; } = PluginSdkInfo.PackagedDataDirectoryName;
|
||||||
|
|
||||||
|
public bool IsDevMode { get; init; }
|
||||||
|
|
||||||
public ISet<string> SharedAssemblyNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
public ISet<string> SharedAssemblyNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
typeof(IPlugin).Assembly.GetName().Name!
|
typeof(IPlugin).Assembly.GetName().Name!
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
Directory.CreateDirectory(PluginsDirectory);
|
Directory.CreateDirectory(PluginsDirectory);
|
||||||
ApplyPendingPluginDeletions();
|
ApplyPendingPluginDeletions();
|
||||||
UnloadInstalledPlugins();
|
UnloadInstalledPlugins();
|
||||||
|
MergeDevSettingsFromSnapshot();
|
||||||
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
|
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
|
||||||
|
|
||||||
var disabledPluginIds = GetDisabledPluginIds();
|
var disabledPluginIds = GetDisabledPluginIds();
|
||||||
@@ -108,19 +109,30 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var candidate in candidates)
|
foreach (var candidate in candidates)
|
||||||
{
|
{
|
||||||
|
var isDevPlugin = candidate.SourceKind == PluginCatalogSourceKind.DevPlugin;
|
||||||
|
|
||||||
if (!selectedPluginIds.Add(candidate.Manifest.Id))
|
if (!selectedPluginIds.Add(candidate.Manifest.Id))
|
||||||
{
|
{
|
||||||
var duplicateFailure = PluginLoadResult.Failure(
|
if (isDevPlugin)
|
||||||
candidate.SourcePath,
|
{
|
||||||
candidate.Manifest,
|
AppLogger.Info(
|
||||||
new InvalidOperationException(
|
"DevPlugin",
|
||||||
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
|
$"Developer plugin '{candidate.Manifest.Id}' overrides an already-registered plugin from '{candidate.SourcePath}'.");
|
||||||
_loadResults.Add(duplicateFailure);
|
}
|
||||||
LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
|
else
|
||||||
continue;
|
{
|
||||||
|
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)
|
if (!isEnabled)
|
||||||
{
|
{
|
||||||
_catalog.Add(new PluginCatalogEntry(
|
_catalog.Add(new PluginCatalogEntry(
|
||||||
@@ -172,6 +184,10 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
PluginsDirectory,
|
PluginsDirectory,
|
||||||
services: _hostServices,
|
services: _hostServices,
|
||||||
hostProperties),
|
hostProperties),
|
||||||
|
PluginCatalogSourceKind.DevPlugin => _loader.LoadFromManifest(
|
||||||
|
candidate.SourcePath,
|
||||||
|
services: _hostServices,
|
||||||
|
hostProperties),
|
||||||
_ => _loader.LoadFromManifest(
|
_ => _loader.LoadFromManifest(
|
||||||
candidate.SourcePath,
|
candidate.SourcePath,
|
||||||
services: _hostServices,
|
services: _hostServices,
|
||||||
@@ -192,7 +208,8 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
true,
|
true,
|
||||||
null,
|
null,
|
||||||
loadResult.LoadedPlugin.SettingsSections.Count,
|
loadResult.LoadedPlugin.SettingsSections.Count,
|
||||||
loadResult.LoadedPlugin.DesktopComponents.Count));
|
loadResult.LoadedPlugin.DesktopComponents.Count,
|
||||||
|
IsDevPlugin: isDevPlugin));
|
||||||
AppLogger.Info(
|
AppLogger.Info(
|
||||||
"PluginRuntime",
|
"PluginRuntime",
|
||||||
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; SourceKind='{candidate.SourceKind}'; SettingsSections={loadResult.LoadedPlugin.SettingsSections.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}; Editors={loadResult.LoadedPlugin.DesktopComponentEditors.Count}.");
|
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; 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,
|
false,
|
||||||
loadResult.Error?.Message,
|
loadResult.Error?.Message,
|
||||||
0,
|
0,
|
||||||
0));
|
0,
|
||||||
|
IsDevPlugin: isDevPlugin));
|
||||||
LogPluginFailure("Load", loadResult, treatAsError: true);
|
LogPluginFailure("Load", loadResult, treatAsError: true);
|
||||||
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
|
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
|
||||||
}
|
}
|
||||||
@@ -229,6 +247,14 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
return false;
|
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 snapshot = LoadAppSettingsSnapshot();
|
||||||
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
|
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
|
||||||
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
|
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -459,12 +485,74 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DiscoverDevPluginCandidates(candidates, failures);
|
||||||
|
|
||||||
return candidates
|
return candidates
|
||||||
.OrderBy(candidate => candidate.SourceKind)
|
.OrderByDescending(candidate => candidate.SourceKind)
|
||||||
.ThenBy(candidate => candidate.SourcePath, StringComparer.OrdinalIgnoreCase)
|
.ThenBy(candidate => candidate.SourcePath, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DiscoverDevPluginCandidates(List<PluginCandidate> candidates, List<PluginLoadResult> 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<string> EnumerateCandidatePaths(string searchPattern)
|
private IEnumerable<string> EnumerateCandidatePaths(string searchPattern)
|
||||||
{
|
{
|
||||||
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(PluginsDirectory), ".runtime"));
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(PluginsDirectory), ".runtime"));
|
||||||
@@ -582,7 +670,8 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
|
|
||||||
private static PluginLoaderOptions CreateOptions()
|
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(App).Assembly);
|
||||||
AddSharedAssembly(options, typeof(IServiceCollection).Assembly);
|
AddSharedAssembly(options, typeof(IServiceCollection).Assembly);
|
||||||
AddSharedAssembly(options, typeof(HostBuilderContext).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)
|
private void CollectContributions(LoadedPlugin loadedPlugin)
|
||||||
{
|
{
|
||||||
_exportRegistry.ReplaceExports(loadedPlugin.Manifest.Id, loadedPlugin.ExportedServices);
|
_exportRegistry.ReplaceExports(loadedPlugin.Manifest.Id, loadedPlugin.ExportedServices);
|
||||||
@@ -826,6 +940,13 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
_settingsCatalogService.RemovePluginSections(pluginId);
|
_settingsCatalogService.RemovePluginSections(pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum PluginCatalogSourceKind
|
||||||
|
{
|
||||||
|
Package = 0,
|
||||||
|
Manifest = 1,
|
||||||
|
DevPlugin = 2
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record PluginCandidate(
|
private sealed record PluginCandidate(
|
||||||
string SourcePath,
|
string SourcePath,
|
||||||
PluginManifest Manifest,
|
PluginManifest Manifest,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ dotnet new install LanMountainDesktop.PluginTemplate
|
|||||||
dotnet new lmd-plugin -n MyPlugin
|
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`
|
- **共享契约**: `LanMountainDesktop.Shared.Contracts`
|
||||||
- **迁移指南**: [PLUGIN_SDK_V4_MIGRATION.md](docs/PLUGIN_SDK_V4_MIGRATION.md)
|
- **迁移指南**: [PLUGIN_SDK_V4_MIGRATION.md](docs/PLUGIN_SDK_V4_MIGRATION.md)
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
### 当前阶段
|
### 当前阶段
|
||||||
|
|
||||||
- 产品版本:`1.0.0`
|
- 产品版本:`1.0.0`
|
||||||
- Plugin SDK API 基线:`4.0.0`
|
- Plugin SDK API 基线:`4.0.1`
|
||||||
- 当前重点:持续完善宿主体验、设置页体验、组件能力与插件生态
|
- 当前重点:持续完善宿主体验、设置页体验、组件能力与插件生态
|
||||||
- 近期需求入口:以 `.trae/specs/` 中的 feature spec 为准
|
- 近期需求入口:以 `.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.
|
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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user