mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
16 KiB
16 KiB
插件开发指南
为 LanMountainDesktop 开发自定义插件
目录
快速开始
安装插件模板
# 安装官方插件模板
dotnet new install LanMountainDesktop.PluginTemplate
# 查看可用模板
dotnet new list | findstr lmd
创建新插件
# 创建插件项目
dotnet new lmd-plugin -n MyAwesomePlugin
# 进入项目目录
cd MyAwesomePlugin
# 还原依赖
dotnet restore
# 构建插件
dotnet build
项目结构
MyAwesomePlugin/
├── MyAwesomePlugin.csproj # 项目文件
├── Plugin.cs # 插件入口
├── Components/ # 组件目录
│ └── MyComponent.cs
├── Views/ # 视图目录
│ └── MyComponentView.axaml
├── ViewModels/ # 视图模型
│ └── MyComponentViewModel.cs
├── Settings/ # 设置页
│ └── MySettingsPage.axaml
└── plugin.json # 插件清单
插件架构
插件 SDK 版本
当前 SDK 版本: 5.0.0
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
<PackageReference Include="LanMountainDesktop.Shared.Contracts" Version="5.0.0" />
插件清单 (plugin.json)
{
"Id": "com.example.myawesomeplugin",
"Name": "My Awesome Plugin",
"Version": "1.0.0",
"Author": "Your Name",
"Description": "A plugin that does awesome things",
"MinHostVersion": "1.0.0",
"Dependencies": [],
"Permissions": [
"FileSystem.Read",
"Network.Access"
]
}
核心接口
IPlugin - 插件入口接口:
public interface IPlugin
{
string Id { get; }
string Name { get; }
string Version { get; }
Task InitializeAsync(IPluginContext context);
Task ShutdownAsync();
}
IPluginContext - 插件上下文:
public interface IPluginContext
{
string PluginDirectory { get; }
IServiceProvider Services { get; }
ILogger Logger { get; }
ISettingsService Settings { get; }
}
创建插件
1. 实现插件入口
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Shared.Contracts;
namespace MyAwesomePlugin;
public class Plugin : IPlugin
{
public string Id => "com.example.myawesomeplugin";
public string Name => "My Awesome Plugin";
public string Version => "1.0.0";
private IPluginContext? _context;
public async Task InitializeAsync(IPluginContext context)
{
_context = context;
// 注册组件
var componentRegistry = context.Services.GetService<IComponentRegistry>();
componentRegistry?.RegisterComponent<MyComponent>();
// 注册设置页
var settingsRegistry = context.Services.GetService<ISettingsPageRegistry>();
settingsRegistry?.RegisterPage<MySettingsPage>("我的插件设置");
// 初始化逻辑
context.Logger.LogInformation("Plugin initialized");
await Task.CompletedTask;
}
public async Task ShutdownAsync()
{
// 清理资源
_context?.Logger.LogInformation("Plugin shutting down");
await Task.CompletedTask;
}
}
2. 配置项目文件
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- 插件元数据 -->
<PluginId>com.example.myawesomeplugin</PluginId>
<PluginName>My Awesome Plugin</PluginName>
<PluginVersion>1.0.0</PluginVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
<PackageReference Include="LanMountainDesktop.Shared.Contracts" Version="5.0.0" />
<PackageReference Include="Avalonia" Version="12.0.1" />
</ItemGroup>
<!-- 复制 plugin.json 到输出目录 -->
<ItemGroup>
<None Update="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
插件生命周期
生命周期阶段
1. 发现 (Discovery)
↓
2. 加载 (Load)
├─ 加载程序集
├─ 验证依赖
└─ 创建插件实例
↓
3. 初始化 (Initialize)
├─ 调用 InitializeAsync()
├─ 注册组件
├─ 注册设置页
└─ 初始化服务
↓
4. 运行 (Running)
├─ 组件渲染
├─ 事件处理
└─ 服务调用
↓
5. 关闭 (Shutdown)
├─ 调用 ShutdownAsync()
├─ 清理资源
└─ 卸载程序集
生命周期钩子
public class Plugin : IPlugin
{
// 插件加载后立即调用
public async Task InitializeAsync(IPluginContext context)
{
// 注册组件、服务、设置页
// 初始化资源
}
// 插件卸载前调用
public async Task ShutdownAsync()
{
// 保存状态
// 释放资源
// 取消订阅
}
}
添加组件
1. 定义组件类
using LanMountainDesktop.PluginSdk.Components;
using LanMountainDesktop.Shared.Contracts;
namespace MyAwesomePlugin.Components;
[Component(
Id = "com.example.myawesomeplugin.mycomponent",
Name = "我的组件",
Description = "一个很棒的组件",
Category = "工具",
Icon = "avares://MyAwesomePlugin/Assets/icon.png"
)]
public class MyComponent : ComponentBase
{
public override string Id => "com.example.myawesomeplugin.mycomponent";
public override string Name => "我的组件";
// 组件设置
private string _message = "Hello, World!";
public string Message
{
get => _message;
set => SetProperty(ref _message, value);
}
// 组件初始化
public override Task InitializeAsync()
{
// 加载设置
Message = Settings.GetValue("Message", "Hello, World!");
return Task.CompletedTask;
}
// 组件更新 (定时调用)
public override Task UpdateAsync()
{
// 更新组件数据
return Task.CompletedTask;
}
}
2. 创建组件视图
MyComponentView.axaml:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyAwesomePlugin.ViewModels"
x:Class="MyAwesomePlugin.Views.MyComponentView"
x:DataType="vm:MyComponentViewModel">
<Border Background="{DynamicResource CardBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="16">
<StackPanel Spacing="8">
<TextBlock Text="{Binding Component.Name}"
FontSize="18"
FontWeight="Bold" />
<TextBlock Text="{Binding Component.Message}"
TextWrapping="Wrap" />
<Button Content="点击我"
Command="{Binding ClickCommand}" />
</StackPanel>
</Border>
</UserControl>
MyComponentView.axaml.cs:
using Avalonia.Controls;
namespace MyAwesomePlugin.Views;
public partial class MyComponentView : UserControl
{
public MyComponentView()
{
InitializeComponent();
}
}
3. 创建视图模型
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyAwesomePlugin.ViewModels;
public partial class MyComponentViewModel : ObservableObject
{
[ObservableProperty]
private MyComponent _component;
public MyComponentViewModel(MyComponent component)
{
_component = component;
}
[RelayCommand]
private void Click()
{
Component.Message = "按钮被点击了!";
}
}
4. 注册组件
public async Task InitializeAsync(IPluginContext context)
{
var componentRegistry = context.Services.GetService<IComponentRegistry>();
// 注册组件
componentRegistry?.RegisterComponent<MyComponent>(
componentFactory: () => new MyComponent(),
viewFactory: (component) => new MyComponentView
{
DataContext = new MyComponentViewModel((MyComponent)component)
}
);
}
添加设置页
1. 创建设置页视图
MySettingsPage.axaml:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyAwesomePlugin.Settings.MySettingsPage">
<StackPanel Spacing="16" Margin="24">
<TextBlock Text="我的插件设置"
FontSize="24"
FontWeight="Bold" />
<StackPanel Spacing="8">
<TextBlock Text="消息内容:" />
<TextBox x:Name="MessageTextBox"
Watermark="输入消息..." />
</StackPanel>
<Button Content="保存"
Click="SaveButton_Click" />
</StackPanel>
</UserControl>
MySettingsPage.axaml.cs:
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.PluginSdk;
namespace MyAwesomePlugin.Settings;
public partial class MySettingsPage : UserControl
{
private readonly ISettingsService _settings;
public MySettingsPage(ISettingsService settings)
{
InitializeComponent();
_settings = settings;
// 加载设置
MessageTextBox.Text = _settings.GetValue("Message", "Hello, World!");
}
private void SaveButton_Click(object? sender, RoutedEventArgs e)
{
// 保存设置
_settings.SetValue("Message", MessageTextBox.Text);
// 显示提示
// TODO: 显示保存成功提示
}
}
2. 注册设置页
public async Task InitializeAsync(IPluginContext context)
{
var settingsRegistry = context.Services.GetService<ISettingsPageRegistry>();
settingsRegistry?.RegisterPage(
title: "我的插件",
category: "插件",
pageFactory: () => new MySettingsPage(context.Settings)
);
}
使用服务
可用服务
ILogger - 日志服务:
context.Logger.LogInformation("信息日志");
context.Logger.LogWarning("警告日志");
context.Logger.LogError("错误日志");
ISettingsService - 设置服务:
// 读取设置
var value = context.Settings.GetValue("Key", "DefaultValue");
// 写入设置
context.Settings.SetValue("Key", "NewValue");
// 监听设置变化
context.Settings.SettingChanged += (sender, e) =>
{
if (e.Key == "Key")
{
// 设置已变更
}
};
INotificationService - 通知服务:
var notificationService = context.Services.GetService<INotificationService>();
notificationService?.ShowNotification(
title: "通知标题",
message: "通知内容",
type: NotificationType.Information
);
IHttpClientFactory - HTTP 客户端:
var httpFactory = context.Services.GetService<IHttpClientFactory>();
var httpClient = httpFactory?.CreateClient();
var response = await httpClient.GetStringAsync("https://api.example.com/data");
打包和发布
1. 构建插件
dotnet build -c Release
2. 打包为 .laapp
# 使用官方打包脚本
pwsh ./scripts/Pack-PluginPackages.ps1 -PluginProject ./MyAwesomePlugin/MyAwesomePlugin.csproj
# 或手动打包
cd MyAwesomePlugin/bin/Release/net10.0
zip -r MyAwesomePlugin-1.0.0.laapp *
3. 测试插件
# 安装插件
LanMountainDesktop.Launcher.exe plugin install MyAwesomePlugin-1.0.0.laapp
# 启动应用测试
LanMountainDesktop.Launcher.exe launch
应用内插件市场不会调用 Launcher 安装插件。市场安装会把 .laapp 下载到当前用户的 pending 队列,并在下一次 Host 启动、插件发现前应用;上面的 Launcher 命令仅作为本地维护/兼容入口保留。
4. 发布插件
选项 1: GitHub Release
- 创建 GitHub 仓库
- 上传
.laapp文件到 Release - 用户可以手动下载安装
选项 2: 插件市场 (如果可用)
- 提交插件到官方市场
- 等待审核
- 用户可以在应用内浏览和安装
最佳实践
性能优化
- 避免阻塞 UI 线程:
// 错误
public override Task UpdateAsync()
{
Thread.Sleep(1000); // 阻塞!
return Task.CompletedTask;
}
// 正确
public override async Task UpdateAsync()
{
await Task.Delay(1000);
}
- 使用异步 API:
// 使用 async/await
var data = await httpClient.GetStringAsync(url);
- 缓存数据:
private string? _cachedData;
private DateTime _cacheTime;
public async Task<string> GetDataAsync()
{
if (_cachedData != null && DateTime.Now - _cacheTime < TimeSpan.FromMinutes(5))
return _cachedData;
_cachedData = await FetchDataAsync();
_cacheTime = DateTime.Now;
return _cachedData;
}
资源管理
- 实现 IDisposable:
public class MyComponent : ComponentBase, IDisposable
{
private HttpClient? _httpClient;
public void Dispose()
{
_httpClient?.Dispose();
}
}
- 取消订阅事件:
public override Task ShutdownAsync()
{
context.Settings.SettingChanged -= OnSettingChanged;
return Task.CompletedTask;
}
错误处理
- 捕获异常:
public override async Task UpdateAsync()
{
try
{
await FetchDataAsync();
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "Failed to fetch data");
// 显示错误提示给用户
}
}
- 验证输入:
public void SetUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URL cannot be empty", nameof(url));
if (!Uri.TryCreate(url, UriKind.Absolute, out _))
throw new ArgumentException("Invalid URL format", nameof(url));
_url = url;
}
本地化
- 使用资源文件:
// Resources/Strings.resx
// Name: ComponentName, Value: My Component
public override string Name => Resources.Strings.ComponentName;
- 支持多语言:
<!-- Resources/Strings.zh-CN.resx -->
<data name="ComponentName" xml:space="preserve">
<value>我的组件</value>
</data>
安全性
- 验证用户输入:
// 防止路径遍历
var safePath = Path.GetFullPath(Path.Combine(pluginDirectory, userInput));
if (!safePath.StartsWith(pluginDirectory))
throw new SecurityException("Invalid path");
- 使用 HTTPS:
// 强制使用 HTTPS
if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
throw new SecurityException("Only HTTPS URLs are allowed");
示例插件
查看官方示例插件:
- 天气组件 - 显示天气信息
- 倒计时组件 - 倒计时功能
- RSS 阅读器 - 订阅和显示 RSS 源
仓库: https://github.com/YourOrg/LanMountainDesktop.SamplePlugin
相关文档
Public IPC Extension
Plugins can now contribute external IPC capabilities through the Host public IPC entry point.
Recommended registration styles:
services.AddPluginPublicIpc<IMyPluginPublicService, MyPluginPublicService>(
objectId: "default",
notifyIds: ["lanmountain.plugin.my-plugin.status.changed"]);
Or use the advanced contributor model:
public sealed class MyPluginPublicIpcContributor : IPluginPublicIpcContributor
{
public void ConfigurePublicIpc(IPluginPublicIpcBuilder builder)
{
builder.AddService<IMyPluginPublicService>(
objectId: "default",
notifyIds: ["lanmountain.plugin.my-plugin.status.changed"]);
}
}
Additional notes:
- Public IPC contracts must be interfaces marked with
[IpcPublic]. - External .NET clients can reference the plugin contract assembly and create strong-typed proxies through the Host public pipe.
- Plugins can inject
IExternalIpcNotificationPublisherto push live events outward through routed notifications.