feat.文档更新

This commit is contained in:
lincube
2026-06-08 03:54:33 +08:00
parent 7db72fbcd0
commit 49af6601aa
247 changed files with 2939 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
# 🧭 阑山桌面插件开发文档导航
欢迎来到 **LanMountainDesktop阑山桌面** 插件开发文档!
这套文档将帮助你从零开始,一步步掌握插件开发的完整流程,最终发布你的作品到插件市场。
---
## 📖 文档概述
**目标读者:**
- 有一定 .NET/C# 基础的开发者
- 熟悉或愿意学习 Avalonia UI 框架的开发者
- 想要为阑山桌面扩展功能的创意开发者
**你能学到什么:**
- 🚀 快速搭建插件开发环境
- 🧩 创建桌面组件Widgets
- ⚙️ 集成设置页面
- 🎨 适配主题和外观
- 🐛 调试和故障排除
- 🚀 CI/CD 自动化构建
- 📦 发布到插件市场
---
## 🛤️ 推荐阅读路径
### 🌱 新手路径(从零开始)
如果你从未开发过阑山桌面插件,请按以下顺序阅读:
1. **[01-开发环境准备](01-快速开始/01-开发环境准备.md)** - 安装必要工具和模板
2. **[02-三分钟创建第一个插件](01-快速开始/02-三分钟创建第一个插件.md)** - 快速上手,建立信心
3. **[03-插件项目结构详解](01-快速开始/03-插件项目结构详解.md)** - 理解项目组成
4. **[04-调试运行指南](01-快速开始/04-调试运行指南.md)** - 学会调试技巧
5. **[01-插件生命周期](02-核心概念与原理/01-插件生命周期.md)** - 理解运行原理
6. **[02-桌面组件系统](02-核心概念与原理/02-桌面组件系统.md)** - 创建你的第一个组件
7. **[01-开发天气组件](04-实战案例/01-开发天气组件.md)** - 完整实战案例
**预计时间:** 2-3 小时即可开发出第一个可用插件
### 🚀 有经验路径(已有 .NET/Avalonia 基础)
如果你已有相关经验,可以跳过基础部分:
1. **[01-开发环境准备](01-快速开始/01-开发环境准备.md)** - 快速配置环境
2. **[02-核心概念与原理/](02-核心概念与原理/)** - 了解阑山桌面的特殊机制
3. **[03-API实践指南/](03-API实践指南/)** - 查阅具体 API 用法
4. **[04-实战案例/](04-实战案例/)** - 参考完整示例
---
## 🔍 快速问题索引
| 我想知道... | 查看文档 |
|------------|---------|
| 如何搭建开发环境? | [01-开发环境准备](01-快速开始/01-开发环境准备.md) |
| 如何创建第一个插件? | [02-三分钟创建第一个插件](01-快速开始/02-三分钟创建第一个插件.md) |
| plugin.json 各字段含义? | [03-插件项目结构详解](01-快速开始/03-插件项目结构详解.md) |
| 如何调试插件代码? | [04-调试运行指南](01-快速开始/04-调试运行指南.md) |
| 插件什么时候初始化?能做什么? | [01-插件生命周期](02-核心概念与原理/01-插件生命周期.md) |
| 什么是桌面组件?如何创建? | [02-桌面组件系统](02-核心概念与原理/02-桌面组件系统.md) |
| 如何添加设置页面? | [03-设置系统集成](02-核心概念与原理/03-设置系统集成.md) + [04-开发设置页面](04-实战案例/04-开发设置页面.md) |
| 如何适配暗色模式? | [04-外观与主题系统](02-核心概念与原理/04-外观与主题系统.md) |
| 插件之间如何通信? | [05-插件间通信](02-核心概念与原理/05-插件间通信.md) |
| 完整的组件开发示例? | [01-开发天气组件](04-实战案例/01-开发天气组件.md) |
| 如何排查插件不加载的问题? | [03-常见问题排查](05-调试与故障排除/03-常见问题排查.md) |
| 如何配置 GitHub Actions | [01-GitHub Actions入门](06-CI-CD与自动化/01-GitHub Actions入门.md) |
| 如何自动打包 .laapp | [03-自动打包与发布](06-CI-CD与自动化/03-自动打包与发布.md) |
| 如何发布到插件市场? | [03-发布到插件市场](07-发布与运营/03-发布到插件市场.md) |
---
## 📚 相关资源
### 官方资源
| 资源 | 位置 | 说明 |
|-----|------|------|
| **Plugin SDK 源码** | `LanMountainDesktop.PluginSdk/` | SDK 的完整源码和 XML 注释 |
| **插件模板** | `LanMountainDesktop.PluginTemplate/` | `dotnet new` 模板源码 |
| **共享契约** | `LanMountainDesktop.Shared.Contracts/` | 宿主与插件共享的类型定义 |
| **架构文档** | `docs/ARCHITECTURE.md` | 宿主应用架构说明 |
| **视觉规范** | `docs/VISUAL_SPEC.md` | UI 设计规范 |
| **圆角规范** | `docs/CORNER_RADIUS_SPEC.md` | 圆角设计系统 |
| **开发指南** | `docs/DEVELOPMENT.md` | 宿主开发指南 |
### 外部资源
| 资源 | 链接 | 说明 |
|-----|------|------|
| **示例插件仓库** | `LanMountainDesktop.SamplePlugin` | 官方示例插件(独立仓库) |
| **Avalonia UI 文档** | https://docs.avaloniaui.net/ | UI 框架官方文档 |
| **FluentAvalonia** | https://github.com/amwx/FluentAvalonia | 主题控件库 |
| **.NET 文档** | https://learn.microsoft.com/dotnet/ | .NET 官方文档 |
---
## 💡 获取帮助
如果在开发过程中遇到问题:
1. **查阅本文档** - 使用上方快速索引找到相关章节
2. **查看示例代码** - 参考 `LanMountainDesktop.PluginTemplate/content/` 中的模板代码
3. **阅读 SDK 源码** - `LanMountainDesktop.PluginSdk/` 中有详细的 XML 注释
4. **搜索 Issues** - 在 GitHub 仓库搜索是否有人遇到类似问题
5. **提交 Issue** - 如果确认是 bug欢迎提交 Issue
---
## 🎯 下一步
准备好开始了吗?点击 **[01-开发环境准备](01-快速开始/01-开发环境准备.md)** 开始你的插件开发之旅!
---
*最后更新2026年4月*

View File

@@ -0,0 +1,220 @@
# 01-开发环境准备
在开始开发阑山桌面插件之前,你需要准备好开发环境。本指南将带你完成所有必要的安装和配置。
---
## ✅ 系统要求
### 支持的操作系统
| 操作系统 | 版本要求 | 备注 |
|---------|---------|------|
| **Windows** | Windows 10 版本 1809 或更高 | 推荐开发平台 |
| **Windows** | Windows 11 | 最佳体验 |
| **Linux** | Ubuntu 20.04+ / Debian 10+ | 支持开发和运行 |
| **macOS** | macOS 12+ | 支持开发和运行 |
### 硬件要求
- **处理器**x64 或 ARM64 架构
- **内存**:至少 4GB RAM推荐 8GB
- **磁盘空间**:至少 2GB 可用空间
---
## 🛠️ 安装 .NET SDK
阑山桌面插件基于 **.NET 10** 开发,你需要安装对应版本的 SDK。
### 下载安装
1. 访问 [.NET 10 下载页面](https://dotnet.microsoft.com/download/dotnet/10.0)
2. 下载适合你操作系统的 SDK 安装包
3. 运行安装程序,按提示完成安装
### 验证安装
打开终端PowerShell、CMD 或 Bash运行以下命令
```powershell
# 检查 .NET SDK 版本
dotnet --version
```
**预期输出示例:**
```
10.0.100
```
⚠️ **如果版本低于 10.0**,请重新下载安装最新版 .NET 10 SDK。
---
## 💻 安装 IDE集成开发环境
你可以选择以下任一 IDE 进行开发:
### 选项 1Visual Studio 2022推荐 Windows 用户)
**优点:** 功能最全,调试体验最佳
1. 下载 [Visual Studio 2022](https://visualstudio.microsoft.com/vs/)
2. 安装时选择以下工作负载:
-**.NET 桌面开发**
-**Avalonia UI 开发**(可选,如需 Avalonia 设计器)
### 选项 2JetBrains Rider跨平台推荐
**优点:** 跨平台智能提示强大Avalonia 支持好
1. 下载 [Rider](https://www.jetbrains.com/rider/)
2. 安装后打开,会自动检测 .NET SDK
### 选项 3Visual Studio Code轻量级
**优点:** 免费,轻量,插件丰富
1. 下载 [VS Code](https://code.visualstudio.com/)
2. 安装以下扩展:
- **C# Dev Kit**Microsoft 官方)
- **Avalonia for VS Code**(可选)
---
## 📦 安装插件模板
阑山桌面提供了官方的 `dotnet new` 模板,帮助你快速创建插件项目。
### 安装模板
```powershell
# 安装最新版插件模板
dotnet new install LanMountainDesktop.PluginTemplate
```
**成功提示:**
```
模板名 短名称 语言 标签
------------------------------------- ---------- ---- ------------
LanMountainDesktop Plugin lmd-plugin C# LanMountainDesktop/Plugin
```
### 验证安装
```powershell
# 列出已安装的模板,查找 lmd-plugin
dotnet new list | findstr lmd
```
Linux/macOS
```bash
dotnet new list | grep lmd
```
---
## 🎮 获取宿主应用
插件需要在阑山桌面宿主中运行,你需要获取宿主应用:
### 方式 1下载 Release 版本(推荐)
1. 访问 GitHub Releases 页面
2. 下载最新版本的安装包(.exe / .deb / .dmg
3. 安装并运行阑山桌面
### 方式 2从源码构建
如果你想调试宿主或了解内部机制:
```powershell
# 克隆仓库
git clone https://github.com/your-org/LanMountainDesktop.git
cd LanMountainDesktop
# 还原依赖
dotnet restore
# 构建项目
dotnet build LanMountainDesktop.slnx -c Debug
# 运行宿主
dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
```
---
## 🔍 环境验证清单
在继续之前,请确认以下检查项都已完成:
| 检查项 | 验证命令 | 预期结果 |
|-------|---------|---------|
| ✅ .NET SDK 版本 | `dotnet --version` | 10.0.xxx |
| ✅ 模板已安装 | `dotnet new list \| findstr lmd` | 显示 lmd-plugin |
| ✅ IDE 可创建项目 | 在 IDE 中新建项目 | 能看到 C# 项目模板 |
| ✅ 宿主可运行 | 双击 LanMountainDesktop.exe | 应用正常启动 |
---
## ⚠️ 常见问题
### 问题 1dotnet 命令找不到
**现象:** 运行 `dotnet` 提示不是内部或外部命令
**解决:**
1. 确认 .NET SDK 已正确安装
2. 重启终端或 IDE
3. 检查环境变量 PATH 是否包含 `C:\Program Files\dotnet\`
### 问题 2模板安装失败
**现象:** `dotnet new install` 报错或卡住
**解决:**
1. 检查网络连接(需要访问 nuget.org
2. 尝试指定版本号:
```powershell
dotnet new install LanMountainDesktop.PluginTemplate::1.0.0
```
3. 清除模板缓存后重试:
```powershell
dotnet new uninstall LanMountainDesktop.PluginTemplate
dotnet new install LanMountainDesktop.PluginTemplate
```
### 问题 3SDK 版本不匹配
**现象:** 构建时提示 SDK 版本不符合 global.json 要求
**解决:**
1. 检查项目根目录的 `global.json` 文件
2. 安装对应版本的 .NET SDK
3. 或使用以下命令使用已安装的版本:
```powershell
dotnet new globaljson --sdk-version 10.0.100 --roll-forward latestFeature
```
---
## 🎯 下一步
环境准备完成!接下来:
👉 **[02-三分钟创建第一个插件](02-三分钟创建第一个插件.md)** - 立即开始创建你的第一个插件!
---
## 📚 参考资源
- [.NET 10 下载](https://dotnet.microsoft.com/download/dotnet/10.0)
- [Visual Studio 2022](https://visualstudio.microsoft.com/vs/)
- [JetBrains Rider](https://www.jetbrains.com/rider/)
- [VS Code](https://code.visualstudio.com/)
- [Avalonia UI 文档](https://docs.avaloniaui.net/)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,236 @@
# 02-三分钟创建第一个插件
本指南将帮助你在三分钟内创建并运行你的第一个阑山桌面插件。让我们开始吧!
---
## 🎯 目标
完成本指南后,你将:
- ✅ 创建一个可运行的插件项目
- ✅ 在宿主中成功加载插件
- ✅ 在插件列表中看到你的插件
---
## ⚡ 步骤一创建项目30秒
打开终端,运行以下命令:
```powershell
# 创建插件项目
dotnet new lmd-plugin -n MyFirstPlugin
# 进入项目目录
cd MyFirstPlugin
```
**成功标志:** 命令执行后没有报错,且生成了 `MyFirstPlugin` 文件夹。
---
## 📝 步骤二配置插件信息30秒
打开 `plugin.json` 文件,修改以下字段:
```json
{
"id": "com.yourname.myfirstplugin",
"name": "我的第一个插件",
"description": "这是一个测试插件",
"author": "你的名字",
"version": "1.0.0",
"apiVersion": "5.0.0",
"entranceAssembly": "MyFirstPlugin.dll",
"sharedContracts": []
}
```
⚠️ **重要提示:**
- `id` 必须是唯一的,建议使用反向域名格式(如 `com.yourname.pluginname`
- `apiVersion` 必须与 SDK 版本匹配
- 保存文件时使用 **UTF-8** 编码
---
## 🔨 步骤三构建项目30秒
在终端中运行:
```powershell
# 构建项目
dotnet build
```
**成功标志:** 看到类似以下的输出:
```
生成成功。
0 个警告
0 个错误
```
---
## 📦 步骤四找到插件包15秒
构建完成后,插件包位于:
```
MyFirstPlugin/
└── bin/
└── Debug/
└── net10.0/
└── MyFirstPlugin.laapp <-- 这就是插件包!
```
⚠️ **什么是 .laapp 文件?**
- `.laapp` 是阑山桌面的插件包格式
- 本质上是一个 ZIP 压缩包,包含插件 DLL 和资源文件
- 不要解压,直接安装即可
---
## 🚀 步骤五安装到宿主30秒
1. **启动阑山桌面**(如果尚未运行)
2. **打开设置**
- 右键点击桌面上的阑山桌面图标
- 选择「设置」
3. **进入插件管理**
- 在设置窗口左侧选择「插件」
4. **安装本地插件**
- 点击「安装本地插件」按钮
- 选择刚才生成的 `.laapp` 文件
- 点击「打开」
5. **重启宿主**
- 安装完成后,点击「重启」按钮
- 阑山桌面将重新启动
---
## ✅ 步骤六验证安装15秒
重启后,再次打开设置 → 插件:
🎉 **成功标志:**
- 在插件列表中看到「我的第一个插件」
- 状态显示为「已启用」
- 作者显示为你设置的名字
![插件列表示意图](此处应有截图位置)
---
## 📂 生成的项目结构
你的项目现在包含以下文件:
```
MyFirstPlugin/
├── plugin.json # 插件清单文件
├── MyFirstPlugin.csproj # 项目文件
├── Plugin.cs # 插件入口类
├── README.md # 项目说明
└── Localization/ # 本地化文件夹
├── zh-CN.json # 中文资源
└── en-US.json # 英文资源
```
---
## 🔍 查看插件代码
打开 `Plugin.cs`,你会看到:
```csharp
using LanMountainDesktop.PluginSdk;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MyFirstPlugin;
[PluginEntrance]
public sealed class Plugin : PluginBase
{
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 插件初始化代码
// 在这里注册组件、设置页面等
}
}
```
**关键点:**
- `[PluginEntrance]` 特性标记入口类
- 继承 `PluginBase` 基类
- `Initialize` 方法是插件的初始化入口
---
## 🎉 恭喜!
你已经成功创建并安装了第一个阑山桌面插件!
虽然这个插件目前还没有任何功能,但你已经掌握了:
- ✅ 使用模板创建项目
- ✅ 配置插件信息
- ✅ 构建插件包
- ✅ 安装到宿主
---
## 🚦 常见问题
### 问题 1构建失败提示找不到 SDK
**现象:** 错误信息包含 "SDK not found"
**解决:**
1. 确认已安装 .NET 10 SDK`dotnet --version`
2. 检查 `global.json` 中的版本要求
### 问题 2宿主提示插件安装失败
**现象:** 安装时弹出错误对话框
**排查步骤:**
1. 检查 `plugin.json` 是否为有效的 JSON 格式
2. 确认 `id` 字段唯一且合法(只能包含字母、数字、点号)
3. 确认 `apiVersion` 与 SDK 版本匹配
### 问题 3插件列表中不显示
**现象:** 安装后重启,但列表中没有
**排查步骤:**
1. 确认已点击「重启」按钮
2. 检查日志文件:`%LOCALAPPDATA%\LanMountainDesktop\logs\`
3. 确认 `.laapp` 文件完整未损坏
---
## 🎯 下一步
现在你的插件已经能运行了,接下来学习:
👉 **[03-插件项目结构详解](03-插件项目结构详解.md)** - 深入理解每个文件的作用
或者直接进入实战:
👉 **[02-桌面组件系统](../02-核心概念与原理/02-桌面组件系统.md)** - 创建你的第一个桌面组件!
---
## 💡 小贴士
- **快速重建**:修改代码后,只需运行 `dotnet build` 即可重新生成 `.laapp`
- **自动安装**:可以在 IDE 中配置构建后自动复制到宿主插件目录
- **日志调试**:使用 `ILogger` 记录日志,在 `%LOCALAPPDATA%\LanMountainDesktop\logs\` 查看
---
*最后更新2026年4月*

View File

@@ -0,0 +1,350 @@
# 03-插件项目结构详解
了解插件项目的每个文件和文件夹的作用,是开发高质量插件的基础。本文将详细解析插件项目的完整结构。
---
## 📂 项目结构概览
使用模板创建的插件项目结构如下:
```
MyPlugin/
├── plugin.json # 插件清单(必需)
├── MyPlugin.csproj # 项目文件(必需)
├── Plugin.cs # 入口类(必需)
├── README.md # 项目说明(推荐)
├── .gitignore # Git忽略文件可选
└── Localization/ # 本地化文件夹(可选)
├── zh-CN.json # 中文资源
└── en-US.json # 英文资源
```
---
## 📋 plugin.json - 插件清单
这是插件最重要的配置文件,定义了插件的元数据。
### 完整示例
```json
{
"id": "com.example.myplugin",
"name": "我的插件",
"description": "这是一个示例插件",
"author": "作者名称",
"version": "1.0.0",
"apiVersion": "5.0.0",
"entranceAssembly": "MyPlugin.dll",
"sharedContracts": [],
"website": "https://example.com",
"icon": "icon.png",
"tags": ["工具", "实用"]
}
```
### 字段详解
| 字段 | 必需 | 说明 | 示例 |
|-----|------|------|------|
| `id` | ✅ | 唯一标识符,反向域名格式 | `com.yourname.plugin` |
| `name` | ✅ | 显示名称 | `天气插件` |
| `description` | ✅ | 简短描述 | `显示实时天气信息` |
| `author` | ✅ | 作者名称 | `张三` |
| `version` | ✅ | 版本号(语义化版本) | `1.0.0` |
| `apiVersion` | ✅ | SDK API 版本 | `5.0.0` |
| `entranceAssembly` | ✅ | 入口程序集文件名 | `MyPlugin.dll` |
| `sharedContracts` | ✅ | 共享契约类型列表 | `[]` |
| `website` | ❌ | 项目网站 | `https://github.com/...` |
| `icon` | ❌ | 图标文件名 | `icon.png` |
| `tags` | ❌ | 标签数组 | `["天气", "工具"]` |
### 重要规则
⚠️ **id 字段规则:**
- 只能包含小写字母、数字、点号(`.`
- 必须全局唯一
- 建议使用反向域名格式:`com.yourname.pluginname`
- 一经发布不可更改
⚠️ **version 字段规则:**
- 使用语义化版本格式:`主版本.次版本.修订号`
- 示例:`1.0.0``2.1.3-beta`
⚠️ **apiVersion 字段规则:**
- 必须与引用的 SDK 版本兼容
- 当前最新版本:`5.0.0`
- 不兼容时宿主将拒绝加载插件
---
## 🔧 .csproj - 项目文件
定义了项目的构建配置和依赖项。
### 完整示例
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\**\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
```
### 关键配置项
| 配置项 | 说明 | 推荐值 |
|-------|------|--------|
| `TargetFramework` | 目标框架 | `net10.0` |
| `LangVersion` | C# 语言版本 | `latest` |
| `Nullable` | 可空引用类型 | `enable` |
### SDK 引用
```xml
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
```
⚠️ **版本必须匹配:**
- SDK 版本必须与 `plugin.json` 中的 `apiVersion` 兼容
- 建议使用最新稳定版
### 资源文件配置
确保 `plugin.json` 和本地化文件被正确复制到输出目录:
```xml
<ItemGroup>
<None Update="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\**\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
```
---
## 🚪 Plugin.cs - 入口类
插件的入口点,负责初始化逻辑。
### 基本结构
```csharp
using LanMountainDesktop.PluginSdk;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MyPlugin;
[PluginEntrance]
public sealed class Plugin : PluginBase
{
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 在这里注册组件、设置页面、服务等
// 示例:注册桌面组件
// services.AddPluginDesktopComponent<MyWidget>(...);
// 示例:注册设置页面
// services.AddPluginSettingsSection(...);
}
}
```
### 关键特性
| 特性/类 | 说明 |
|--------|------|
| `[PluginEntrance]` | 标记插件入口类,必须有且仅有一个 |
| `PluginBase` | 插件基类,提供基础功能和日志访问 |
| `Initialize` | 初始化方法,宿主启动时调用 |
### Initialize 方法参数
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
```
| 参数 | 类型 | 说明 |
|-----|------|------|
| `context` | `HostBuilderContext` | 宿主构建上下文,可访问配置 |
| `services` | `IServiceCollection` | 依赖注入服务集合,用于注册组件和服务 |
---
## 🌍 Localization - 本地化文件夹
存放多语言资源文件,支持插件的国际化。
### 文件夹结构
```
Localization/
├── zh-CN.json # 简体中文
├── zh-TW.json # 繁体中文
├── en-US.json # 英文(美国)
├── ja-JP.json # 日文
└── ko-KR.json # 韩文
```
### 资源文件格式
```json
{
"PluginName": "我的插件",
"Settings": {
"Title": "设置",
"RefreshInterval": "刷新间隔"
},
"Messages": {
"Loading": "加载中...",
"Error": "出错了:{0}"
}
}
```
### 在代码中使用
```csharp
// 获取本地化字符串
var localizer = serviceProvider.GetRequiredService<IStringLocalizer<MyPlugin>>();
var pluginName = localizer["PluginName"];
var message = localizer["Messages.Error", errorDetails];
```
### 支持的语言代码
| 语言 | 代码 |
|-----|------|
| 简体中文 | `zh-CN` |
| 繁体中文 | `zh-TW` |
| 英文 | `en-US` |
| 日文 | `ja-JP` |
| 韩文 | `ko-KR` |
---
## 📦 构建输出结构
运行 `dotnet build` 后,生成的输出结构:
```
bin/Debug/net10.0/
├── MyPlugin.dll # 插件程序集
├── MyPlugin.pdb # 调试符号
├── plugin.json # 插件清单(复制)
├── Localization/ # 本地化文件夹(复制)
│ └── zh-CN.json
├── MyPlugin.laapp # 插件包(由 SDK 自动生成)
└── ...(依赖项 DLL
```
### .laapp 包结构
`.laapp` 文件本质是一个 ZIP 压缩包,包含:
```
MyPlugin.laapp
├── plugin.json # 清单文件
├── MyPlugin.dll # 主程序集
├── Localization/ # 本地化资源
└── ...(其他依赖 DLL
```
---
## 🔗 与其他 .NET 项目的区别
| 特性 | 普通 .NET 应用 | 阑山桌面插件 |
|-----|---------------|-------------|
| 入口点 | `Program.cs``Main` | `Plugin.cs``Initialize` |
| 运行方式 | 独立运行 | 由宿主加载运行 |
| 依赖注入 | 自行配置 | 使用宿主提供的 `IServiceCollection` |
| 输出格式 | `.exe``.dll` | `.laapp` 包 |
| 资源访问 | 直接访问 | 通过 SDK API 访问宿主资源 |
| 热重载 | 支持 | 不支持(需重启宿主) |
---
## 🎯 最佳实践
### 项目组织建议
```
MyPlugin/
├── plugin.json
├── MyPlugin.csproj
├── Plugin.cs # 入口类(保持简洁)
├── README.md
├── .gitignore
├── Localization/ # 本地化资源
├── Services/ # 服务类文件夹
│ ├── WeatherService.cs
│ └── DataService.cs
├── Views/ # 视图文件夹
│ ├── WeatherWidget.axaml
│ ├── WeatherWidget.axaml.cs
│ └── SettingsPage.axaml
└── ViewModels/ # 视图模型文件夹
├── WeatherViewModel.cs
└── SettingsViewModel.cs
```
### 文件命名规范
| 类型 | 命名约定 | 示例 |
|-----|---------|------|
| 入口类 | `Plugin` | `Plugin.cs` |
| 组件视图 | `{Name}Widget` | `WeatherWidget.axaml` |
| 设置页面 | `{Name}SettingsPage` | `WeatherSettingsPage.axaml` |
| 服务类 | `{Name}Service` | `WeatherService.cs` |
| 视图模型 | `{Name}ViewModel` | `WeatherViewModel.cs` |
---
## 📚 参考资源
- [Plugin SDK 源码](../../LanMountainDesktop.PluginSdk/)
- [插件模板](../../LanMountainDesktop.PluginTemplate/content/)
- [02-桌面组件系统](../02-核心概念与原理/02-桌面组件系统.md)
- [03-设置系统集成](../02-核心概念与原理/03-设置系统集成.md)
---
## 🎯 下一步
理解了项目结构后,接下来学习:
👉 **[04-调试运行指南](04-调试运行指南.md)** - 掌握调试技巧
或者深入了解核心概念:
👉 **[01-插件生命周期](../02-核心概念与原理/01-插件生命周期.md)** - 理解插件运行机制
---
*最后更新2026年4月*

View File

@@ -0,0 +1,380 @@
# 04-调试运行指南
掌握插件调试技巧,能大幅提升开发效率。本文介绍阑山桌面插件的各种调试方法和常见问题排查。
---
## 🔄 调试方式概述
阑山桌面插件有两种主要调试方式:
| 方式 | 适用场景 | 优点 | 缺点 |
|-----|---------|------|------|
| **附加到进程** | 日常开发调试 | 不改变项目结构 | 每次需手动附加 |
| **独立调试** | 深度调试、单元测试 | 启动即调试 | 配置较复杂 |
---
## 🎯 方式一:附加到进程(推荐)
这是日常开发中最常用的调试方式。
### 步骤
1. **启动阑山桌面**
- 正常启动宿主应用(非调试模式)
- 确保你的插件已安装
2. **在 IDE 中打开插件项目**
- 使用 Visual Studio / Rider / VS Code 打开项目
3. **设置断点**
- 在你想要调试的代码行左侧点击,设置断点
- 常见断点位置:
- `Plugin.Initialize()` - 插件初始化
- 组件构造函数
- 设置页面加载方法
4. **附加到进程**
**Visual Studio**
- 菜单:`调试``附加到进程`
- 或快捷键:`Ctrl+Alt+P`
- 在列表中找到 `LanMountainDesktop.exe`
- 点击`附加`
**Rider**
- 菜单:`Run``Attach to Process`
- 或快捷键:`Ctrl+Alt+F5`
- 选择 `LanMountainDesktop.exe`
**VS Code**
-`Ctrl+Shift+D` 打开调试面板
- 点击`创建 launch.json 文件`
- 选择 `.NET Core Attach`
- 选择 `LanMountainDesktop` 进程
5. **触发调试**
- 在阑山桌面中操作,触发插件代码
- 例如:添加组件、打开设置页面等
- 程序会在断点处暂停
### 附加配置VS Code
创建 `.vscode/launch.json`
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "附加到阑山桌面",
"type": "coreclr",
"request": "attach",
"processName": "LanMountainDesktop"
}
]
}
```
---
## 🔧 方式二:独立调试
适用于深度调试或单元测试。
### 配置步骤
1. **修改 .csproj 临时引用宿主**
```xml
<ItemGroup>
<!-- 临时添加,仅用于调试 -->
<ProjectReference Include="..\LanMountainDesktop\LanMountainDesktop.csproj" />
</ItemGroup>
```
2. **创建调试启动配置**
**Visual Studio**
- 右键项目 → `属性` → `调试`
- 启动外部程序:选择 `LanMountainDesktop.exe`
- 工作目录:设为宿主输出目录
**VS Code launch.json**
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "启动阑山桌面(调试)",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/../LanMountainDesktop/bin/Debug/net10.0/LanMountainDesktop.exe",
"args": [],
"cwd": "${workspaceFolder}/../LanMountainDesktop/bin/Debug/net10.0",
"stopAtEntry": false
}
]
}
```
3. **启动调试**
- 按 `F5` 启动
- 宿主会以调试模式启动
- 插件代码中的断点会直接命中
⚠️ **注意:** 发布插件前务必移除临时引用!
---
## 📝 日志调试
当断点调试不方便时,日志是最有效的调试手段。
### 使用 ILogger
```csharp
using Microsoft.Extensions.Logging;
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.LogInformation("开始执行任务");
try
{
// 业务逻辑
_logger.LogDebug("处理数据: {Data}", data);
}
catch (Exception ex)
{
_logger.LogError(ex, "任务执行失败");
}
_logger.LogInformation("任务完成");
}
}
```
### 日志级别
| 级别 | 使用场景 |
|-----|---------|
| `LogTrace` | 最详细的跟踪信息 |
| `LogDebug` | 调试信息 |
| `LogInformation` | 一般信息 |
| `LogWarning` | 警告信息 |
| `LogError` | 错误信息 |
| `LogCritical` | 严重错误 |
### 查看日志文件
日志文件位置:
```
Windows: %LOCALAPPDATA%\LanMountainDesktop\logs\
Linux: ~/.local/share/LanMountainDesktop/logs/
macOS: ~/Library/Application Support/LanMountainDesktop/logs/
```
日志文件命名格式:
```
log-20240413.txt
log-20240413_001.txt
```
### 实时查看日志
**Windows PowerShell**
```powershell
Get-Content "$env:LOCALAPPDATA\LanMountainDesktop\logs\log-$(Get-Date -Format 'yyyyMMdd').txt" -Wait
```
**Linux/macOS**
```bash
tail -f ~/.local/share/LanMountainDesktop/logs/log-$(date +%Y%m%d).txt
```
---
## 🚫 热重载限制
⚠️ **重要:** 阑山桌面插件**不支持**热重载Hot Reload
### 原因
插件运行在独立的 `AssemblyLoadContext` 中,.NET 不支持卸载已加载的程序集。因此:
- 修改代码后必须重新构建
- 必须重启宿主才能加载新版本
- 无法使用 `dotnet watch`
### 高效开发流程
```
修改代码 → dotnet build → 重启宿主 → 测试
```
**加速技巧:**
1. **创建批处理脚本**`rebuild-and-run.ps1`
```powershell
dotnet build
Stop-Process -Name "LanMountainDesktop" -ErrorAction SilentlyContinue
Start-Process "C:\Path\To\LanMountainDesktop.exe"
```
2. **使用 Rider 的外部工具**
- 配置构建后自动复制 `.laapp` 到插件目录
---
## 🐛 常见问题排查
### 问题 1断点不命中
**可能原因:**
- 插件未重新构建
- PDB 符号文件未生成
- 附加到了错误的进程
**解决步骤:**
1. 确认已重新构建:`dotnet build`
2. 检查输出目录是否有 `.pdb` 文件
3. 确认附加的是 `LanMountainDesktop.exe`(不是 `LanMountainDesktop.dll`
4. 尝试清理重建:
```powershell
dotnet clean
dotnet build
```
### 问题 2插件不加载
**排查步骤:**
1. **检查日志**
```powershell
Get-Content "$env:LOCALAPPDATA\LanMountainDesktop\logs\log-$(Get-Date -Format 'yyyyMMdd').txt" | Select-String "MyPlugin"
```
2. **验证 plugin.json**
- JSON 格式是否有效
- `id` 是否合法(只含小写字母、数字、点号)
- `apiVersion` 是否与 SDK 版本匹配
3. **检查 .laapp 包**
- 用压缩软件打开,确认文件完整
- 确认 `plugin.json` 和 DLL 存在
### 问题 3依赖项找不到
**现象:** `FileNotFoundException` 或 `Could not load file or assembly`
**解决:**
1. 确保所有依赖项都复制到输出目录
2. 在 `.csproj` 中添加:
```xml
<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
```
### 问题 4调试时宿主卡顿
**原因:** 断点暂停导致 UI 线程阻塞
**解决:**
- 使用 `Debugger.Break()` 代替断点
- 或使用日志代替断点调试
---
## 💡 调试技巧
### 1. 条件断点
当需要在特定条件下暂停时使用:
**Visual Studio**
- 右键断点 → `条件`
- 输入条件表达式,如:`count > 10`
### 2. 日志点Tracepoint
不暂停程序,只输出日志:
**Visual Studio**
- 右键断点 → `操作`
- 勾选 `将消息输出到输出窗口`
- 输入消息模板:`变量值: {variableName}`
### 3. 异常设置
自动在抛出异常时中断:
**Visual Studio**
- `调试` → `窗口` → `异常设置`
- 勾选 `Common Language Runtime Exceptions`
### 4. 立即窗口
在调试时执行代码:
**Visual Studio**
- 快捷键:`Ctrl+Alt+I`
- 可查看变量值、调用方法
---
## 📊 性能调试
### 使用 Diagnostic Tools
**Visual Studio**
- 调试时自动显示 CPU 和内存使用情况
- `调试` → `窗口` → `诊断工具`
### 内存泄漏排查
```csharp
// 在可疑位置添加诊断代码
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var memory = GC.GetTotalMemory(true);
Debug.WriteLine($"内存使用: {memory / 1024 / 1024} MB");
```
---
## 🎯 下一步
掌握了调试技巧后,接下来学习核心概念:
👉 **[01-插件生命周期](../02-核心概念与原理/01-插件生命周期.md)** - 理解插件运行机制
或者查看实战案例:
👉 **[01-开发天气组件](../04-实战案例/01-开发天气组件.md)** - 完整开发流程
---
## 📚 参考资源
- [PluginBase 源码](../../LanMountainDesktop.PluginSdk/PluginBase.cs)
- [docs/DEVELOPMENT.md](../../docs/DEVELOPMENT.md)
- [Visual Studio 调试文档](https://docs.microsoft.com/visualstudio/debugger/)
- [Rider 调试文档](https://www.jetbrains.com/help/rider/Debugging.html)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,398 @@
# 01-插件生命周期
理解插件的生命周期,是开发稳定可靠插件的基础。本文详细讲解插件从加载到卸载的完整过程。
---
## 🔄 生命周期概览
```
┌─────────────────────────────────────────────────────────────┐
│ 阑山桌面启动 │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 1. 发现插件 │
│ - 扫描插件目录 │
│ - 解析 plugin.json │
│ - 验证 API 版本兼容性 │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. 加载插件 │
│ - 创建 AssemblyLoadContext │
│ - 加载插件 DLL │
│ - 查找入口类(带 [PluginEntrance] 特性) │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. 初始化Initialize
│ - 调用 Plugin.Initialize() │
│ - 注册组件、设置页面、服务 │
│ - ⚠️ 此时 UI 尚未完全就绪 │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. 运行中 │
│ - 组件被添加到桌面 │
│ - 用户与组件交互 │
│ - 设置页面被打开 │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. 停用/卸载 │
│ - 用户禁用插件 │
│ - 或关闭阑山桌面 │
│ - 释放资源(当前版本无显式卸载回调) │
└─────────────────────────────────────────────────────────────┘
```
---
## 📋 各阶段详解
### 阶段 1发现插件
**时机:** 阑山桌面启动时
**过程:**
1. 扫描 `%LOCALAPPDATA%\LanMountainDesktop\plugins\` 目录
2. 读取每个 `.laapp` 包中的 `plugin.json`
3. 验证 `apiVersion` 是否与宿主兼容
4. 检查 `id` 是否唯一
**可能失败的原因:**
- `plugin.json` 格式错误
- `apiVersion` 不兼容
- `id` 与其他插件冲突
---
### 阶段 2加载插件
**时机:** 发现成功后
**过程:**
1. 创建独立的 `AssemblyLoadContext`
2. 加载插件 DLL 及其依赖项
3. 查找带有 `[PluginEntrance]` 特性的类
4. 实例化插件入口类
**代码示例:**
```csharp
[PluginEntrance] // ← 这个特性标记入口类
public sealed class Plugin : PluginBase
{
// 插件实例在此阶段被创建
}
```
⚠️ **重要:** 此阶段**不要**执行耗时操作,只应进行简单的字段初始化。
---
### 阶段 3初始化Initialize
**时机:** 插件加载完成后
**这是插件开发中最重要的阶段!**
#### 方法签名
```csharp
public override void Initialize(
HostBuilderContext context, // 宿主构建上下文
IServiceCollection services) // 服务注册集合
```
#### 可执行的操作
**可以做的:**
- 注册桌面组件
- 注册设置页面
- 注册服务到依赖注入容器
- 读取配置
- 初始化资源
**不应该做的:**
- 访问 UI 元素UI 尚未就绪)
- 执行耗时阻塞操作
- 创建窗口或对话框
#### 典型初始化代码
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 1. 注册服务
services.AddSingleton<IWeatherService, WeatherService>();
// 2. 注册桌面组件
services.AddPluginDesktopComponent<WeatherWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "MyPlugin.Weather",
DisplayName = "天气",
IconKey = "Weather",
Category = "工具",
MinWidthCells = 4,
MinHeightCells = 3
});
// 3. 注册设置页面
services.AddPluginSettingsSection(
"myplugin-settings",
"天气设置",
section => section
.AddToggle("auto_refresh", "自动刷新", defaultValue: true)
.AddNumber("interval", "刷新间隔(分钟)", defaultValue: 30),
iconKey: "Settings");
}
```
---
### 阶段 4运行中
**时机:** 初始化完成后,直到插件被禁用或宿主关闭
**特点:**
- 组件可以被添加到桌面
- 用户可以与组件交互
- 设置页面可以被打开
- 定时器可以运行
#### 组件生命周期
```
用户添加组件
┌─────────────────┐
│ 创建组件实例 │ ← 调用构造函数
│ (Dependency │ 注入 IServiceProvider
│ Injection) │
└────────┬────────┘
┌─────────────────┐
│ 组件初始化 │ ← 可在此时加载数据
│ (Loaded事件) │
└────────┬────────┘
┌─────────────────┐
│ 渲染显示 │ ← 用户看到组件
└────────┬────────┘
┌────┴────┐
▼ ▼
用户交互 定时更新
│ │
└────┬────┘
┌─────────────────┐
│ 组件移除 │ ← 用户删除组件
│ (Unloaded事件) │ 或关闭宿主
└─────────────────┘
```
---
### 阶段 5停用/卸载
**时机:**
- 用户在设置中禁用插件
- 卸载插件
- 关闭阑山桌面
**当前限制:**
- SDK v5 暂无显式的卸载回调方法
- 资源释放依赖 .NET 垃圾回收
- 建议:
- 使用 `IDisposable` 模式管理资源
- 在组件卸载事件中清理资源
---
## ⏱️ 启动时序图
```
阑山桌面 插件系统 你的插件
│ │ │
│── 启动 ───────►│ │
│ │ │
│ │── 发现插件 ───►│
│ │ │ (读取 plugin.json)
│ │◄───────────────│
│ │ │
│ │── 加载 DLL ───►│
│ │ │ (AssemblyLoadContext)
│ │◄───────────────│
│ │ │
│ │── 创建实例 ───►│
│ │ │ (调用构造函数)
│ │◄───────────────│
│ │ │
│ │── Initialize ─►│
│ │ │ (注册组件/服务)
│ │◄───────────────│
│ │ │
│◄───────────────│ │
│ │ │
│── UI就绪 ─────►│ │
│ │ │
│ │── 用户添加组件 ─►│
│ │ │ (创建组件实例)
```
---
## 💡 最佳实践
### 1. Initialize 方法保持轻量
```csharp
// ✅ 好的做法:快速注册
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<IDataService, DataService>();
services.AddPluginDesktopComponent<MyWidget>(options);
}
// ❌ 避免:耗时操作
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 不要这样做!
var data = FetchDataFromInternet().Result; // 阻塞!
services.AddSingleton(data);
}
```
### 2. 延迟加载数据
```csharp
public class MyWidget : Border
{
private readonly IDataService _dataService;
public MyWidget(PluginDesktopComponentContext context)
{
_dataService = context.ServiceProvider.GetRequiredService<IDataService>();
// 在 Loaded 事件中加载数据,而不是构造函数
Loaded += async (_, _) =>
{
await LoadDataAsync();
};
}
private async Task LoadDataAsync()
{
var data = await _dataService.GetDataAsync();
// 更新 UI
}
}
```
### 3. 正确处理资源释放
```csharp
public class MyWidget : Border, IDisposable
{
private readonly Timer _timer;
private bool _disposed;
public MyWidget()
{
_timer = new Timer(OnTimerTick, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
// 在卸载时释放资源
Unloaded += (_, _) => Dispose();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_timer?.Dispose();
}
}
```
### 4. 避免循环依赖
```csharp
// ❌ 避免:服务之间相互依赖
public class ServiceA
{
public ServiceA(ServiceB b) { } // 循环依赖风险
}
public class ServiceB
{
public ServiceB(ServiceA a) { }
}
// ✅ 好的做法:使用接口解耦
public class ServiceA
{
public ServiceA(IServiceB b) { }
}
```
---
## 🐛 常见问题
### 问题 1Initialize 中访问 UI 报错
**现象:** `InvalidOperationException` 或空引用
**原因:** Initialize 在 UI 就绪前调用
**解决:** 延迟到组件创建后再访问 UI
### 问题 2服务注册顺序问题
**现象:** 依赖注入找不到服务
**原因:** 服务注册顺序不正确
**解决:** 先注册服务,再注册依赖这些服务的组件
### 问题 3插件加载慢
**现象:** 宿主启动变慢
**原因:** Initialize 中执行耗时操作
**解决:** 将耗时操作移到后台线程或延迟执行
---
## 📚 参考资源
- [PluginBase 源码](../../LanMountainDesktop.PluginSdk/PluginBase.cs)
- [IPlugin 接口](../../LanMountainDesktop.PluginSdk/IPlugin.cs)
- [02-桌面组件系统](02-桌面组件系统.md)
- [03-设置系统集成](03-设置系统集成.md)
---
## 🎯 下一步
理解生命周期后,学习如何创建桌面组件:
👉 **[02-桌面组件系统](02-桌面组件系统.md)** - 创建可视化组件
---
*最后更新2026年4月*

View File

@@ -0,0 +1,393 @@
# 02-桌面组件系统
桌面组件Desktop Component是阑山桌面插件的核心功能。本文详细讲解组件系统的工作原理和开发方法。
---
## 🎯 什么是桌面组件
桌面组件是显示在阑山桌面上的可视化元素,用户可以自由:
- 添加/删除组件
- 拖动调整位置
- 调整大小
- 配置属性
**常见组件示例:**
- 时钟组件 - 显示当前时间
- 天气组件 - 显示天气信息
- 日历组件 - 显示日期和日程
- 系统监控 - 显示 CPU/内存使用率
---
## 📐 网格系统
阑山桌面使用网格系统管理组件布局。
### 网格概念
```
┌─────────────────────────────────────────┐
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 2x2│ │ 2x2│ │ 2x2│ │ 2x2│ │
│ │ 格 │ │ 格 │ │ 格 │ │ 格 │ │
│ └────┘ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────────┐ ┌────┐ │
│ │ 2x2│ │ 4x2 │ │ 2x2│ │
│ │ 格 │ │ 格 │ │ 格 │ │
│ └────┘ └────────┘ └────┘ │
│ ┌────────┐ ┌────────┐ │
│ │ 4x3 │ │ 4x3 │ │
│ │ 格 │ │ 格 │ │
│ └────────┘ └────────┘ │
└─────────────────────────────────────────┘
每格大小:约 60-80 像素(根据 DPI 自动调整)
```
### 组件尺寸
组件尺寸以**格数**为单位:
| 属性 | 说明 | 示例 |
|-----|------|------|
| `MinWidthCells` | 最小宽度(格数) | 4 = 4格宽 |
| `MinHeightCells` | 最小高度(格数) | 3 = 3格高 |
**常见尺寸参考:**
| 组件类型 | 推荐尺寸 | 实际像素(约) |
|---------|---------|--------------|
| 小图标 | 2x2 | 120x120 |
| 天气卡片 | 4x3 | 240x180 |
| 时钟 | 4x4 | 240x240 |
| 日历 | 6x4 | 360x240 |
| 宽面板 | 8x3 | 480x180 |
---
## 🏗️ 创建组件
### 步骤 1创建组件类
组件是继承自 Avalonia 控件的类:
```csharp
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
namespace MyPlugin;
public class WeatherWidget : Border // 继承自 Border 或其他控件
{
public WeatherWidget(PluginDesktopComponentContext context)
{
// 组件初始化
InitializeComponent(context);
}
private void InitializeComponent(PluginDesktopComponentContext context)
{
// 设置背景
Background = new SolidColorBrush(Colors.Transparent);
// 设置圆角(使用宿主主题)
CornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component);
// 创建内容
var textBlock = new TextBlock
{
Text = "天气组件",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
Child = textBlock;
}
}
```
### 步骤 2注册组件
`Plugin.Initialize` 中注册:
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddPluginDesktopComponent<WeatherWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "MyPlugin.Weather", // 唯一标识
DisplayName = "天气", // 显示名称
IconKey = "Weather", // 图标Fluent 图标名)
Category = "工具", // 分类
MinWidthCells = 4, // 最小宽度(格)
MinHeightCells = 3, // 最小高度(格)
CornerRadiusPreset = PluginCornerRadiusPreset.Component // 圆角预设
});
}
```
### PluginDesktopComponentOptions 详解
| 属性 | 必需 | 说明 | 示例 |
|-----|------|------|------|
| `ComponentId` | ✅ | 唯一标识符 | `"MyPlugin.Weather"` |
| `DisplayName` | ✅ | 显示名称 | `"天气"` |
| `IconKey` | ✅ | 图标键名 | `"Weather"``"Clock"` |
| `Category` | ✅ | 分类 | `"工具"``"信息"` |
| `MinWidthCells` | ✅ | 最小宽度(格) | `4` |
| `MinHeightCells` | ✅ | 最小高度(格) | `3` |
| `CornerRadiusPreset` | ❌ | 圆角预设 | `PluginCornerRadiusPreset.Component` |
| `ResizeMode` | ❌ | 调整大小模式 | `PluginDesktopComponentResizeMode.Free` |
### 常用 Fluent 图标
| 图标键名 | 用途 |
|---------|------|
| `Weather` | 天气相关 |
| `Clock` | 时钟、时间 |
| `Calendar` | 日历、日期 |
| `Settings` | 设置 |
| `Home` | 主页 |
| `Search` | 搜索 |
| `Star` | 收藏 |
| `Heart` | 喜欢 |
| `Info` | 信息 |
| `Warning` | 警告 |
完整图标列表:[Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons)
---
## 🎨 组件外观
### 圆角设置
插件必须使用宿主提供的圆角系统,以保持视觉一致性:
```csharp
public WeatherWidget(PluginDesktopComponentContext context)
{
// 获取组件标准圆角
var cornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component);
CornerRadius = cornerRadius;
}
```
**可用的圆角预设:**
| 预设 | 用途 |
|-----|------|
| `Micro` | 微小元素 |
| `Xs` | 小元素 |
| `Sm` | 小卡片 |
| `Md` | 普通按钮/卡片 |
| `Lg` | 大面板 |
| `Xl` | 强调容器 |
| `Component` | **桌面组件标准** |
| `Default` | 自适应 |
### 背景与透明
```csharp
// 透明背景(推荐,让宿主壁纸透出)
Background = new SolidColorBrush(Colors.Transparent);
// 毛玻璃效果
Background = new SolidColorBrush(Color.Parse("#40FFFFFF"));
// 纯色背景
Background = new SolidColorBrush(Color.Parse("#FF2D2D2D"));
```
### 响应主题变化
```csharp
public WeatherWidget(PluginDesktopComponentContext context)
{
// 订阅主题变化
context.Appearance.AppearanceChanged += (_, _) =>
{
UpdateAppearance();
};
UpdateAppearance();
}
private void UpdateAppearance()
{
// 根据当前主题更新颜色
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
Foreground = new SolidColorBrush(isDark ? Colors.White : Colors.Black);
}
```
---
## 📏 尺寸与布局
### 获取实际尺寸
```csharp
public WeatherWidget(PluginDesktopComponentContext context)
{
// 订阅尺寸变化
SizeChanged += OnSizeChanged;
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
// 获取当前实际尺寸(像素)
var width = Bounds.Width;
var height = Bounds.Height;
// 根据尺寸调整内容
if (width < 200)
{
// 小尺寸模式
ShowCompactView();
}
else
{
// 完整模式
ShowFullView();
}
}
```
### 自适应布局
```csharp
private void UpdateLayout()
{
var width = Bounds.Width;
var height = Bounds.Height;
// 根据宽高比调整布局
if (width > height * 2)
{
// 宽屏模式 - 水平排列
_layout.Orientation = Orientation.Horizontal;
}
else
{
// 正常模式 - 垂直排列
_layout.Orientation = Orientation.Vertical;
}
}
```
---
## 🔄 组件生命周期事件
```csharp
public WeatherWidget(PluginDesktopComponentContext context)
{
// 组件加载完成(此时已添加到视觉树)
Loaded += OnLoaded;
// 组件卸载(用户删除或关闭宿主)
Unloaded += OnUnloaded;
// 尺寸变化
SizeChanged += OnSizeChanged;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
// 加载数据
await LoadDataAsync();
// 启动定时器
_timer = new Timer(OnTimerTick, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
}
private void OnUnloaded(object? sender, RoutedEventArgs e)
{
// 清理资源
_timer?.Dispose();
_httpClient?.Dispose();
}
```
---
## 💾 组件设置持久化
组件可以保存自己的设置:
```csharp
public class WeatherWidget : Border
{
private readonly IComponentSettingsAccessor _settings;
public WeatherWidget(PluginDesktopComponentContext context)
{
// 获取设置访问器
_settings = context.Settings;
// 读取设置
var city = _settings.GetValue<string>("city", defaultValue: "北京");
var autoRefresh = _settings.GetValue<bool>("auto_refresh", defaultValue: true);
// 保存设置
_settings.SetValue("city", "上海");
}
}
```
---
## 🐛 常见问题
### 问题 1组件不显示在库中
**排查:**
1. 确认已调用 `AddPluginDesktopComponent`
2. 检查 `ComponentId` 是否唯一
3. 确认组件类是 `public`
### 问题 2组件显示异常
**排查:**
1. 检查构造函数参数是否正确(需要 `PluginDesktopComponentContext`
2. 确认没有抛出未处理异常
3. 查看日志文件
### 问题 3圆角不生效
**原因:** 插件无法访问宿主 XAML 资源
**解决:** 使用代码设置圆角(见上文)
### 问题 4尺寸不正确
**排查:**
1. 检查 `MinWidthCells``MinHeightCells` 设置
2. 确认内容没有强制尺寸
---
## 📚 参考资源
- [PluginDesktopComponentOptions 源码](../../LanMountainDesktop.PluginSdk/PluginDesktopComponentOptions.cs)
- [04-外观与主题系统](04-外观与主题系统.md)
- [01-开发天气组件](../04-实战案例/01-开发天气组件.md)
---
## 🎯 下一步
学习如何添加设置页面:
👉 **[03-设置系统集成](03-设置系统集成.md)** - 让用户配置你的组件
---
*最后更新2026年4月*

View File

@@ -0,0 +1,353 @@
# 03-设置系统集成
设置系统允许插件在阑山桌面的设置窗口中添加自己的配置页面,让用户可以自定义插件行为。
---
## 🎯 设置系统概述
阑山桌面提供两种设置页面模式:
| 模式 | 适用场景 | 复杂度 | 灵活性 |
|-----|---------|--------|--------|
| **声明式设置** | 简单的键值配置 | 低 | 中 |
| **自定义设置页** | 复杂交互、自定义控件 | 中 | 高 |
---
## 📝 声明式设置
通过链式 API 声明配置项,宿主自动生成设置页面。
### 基本用法
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddPluginSettingsSection(
sectionId: "myplugin-settings", // 设置节唯一标识
displayName: "我的插件设置", // 显示名称
configure: section => // 配置设置项
{
section
.AddToggle("enabled", "启用插件", defaultValue: true)
.AddText("api_key", "API密钥", defaultValue: "")
.AddNumber("interval", "刷新间隔(秒)", defaultValue: 60, minimum: 10, maximum: 3600)
.AddSelect("theme", "主题", new[]
{
new SettingsOptionChoice("light", "浅色"),
new SettingsOptionChoice("dark", "深色"),
new SettingsOptionChoice("auto", "跟随系统")
}, defaultValue: "auto");
},
iconKey: "Settings"); // 图标
}
```
### 支持的设置类型
| 方法 | 类型 | 用途 | 示例 |
|-----|------|------|------|
| `AddToggle` | 布尔 | 开关选项 | 启用/禁用 |
| `AddText` | 字符串 | 文本输入 | API密钥、用户名 |
| `AddNumber` | 数值 | 数字输入 | 刷新间隔、数量 |
| `AddSelect` | 枚举 | 下拉选择 | 主题、语言 |
| `AddPath` | 路径 | 文件/文件夹选择 | 保存路径 |
| `AddList` | 列表 | 字符串列表 | 服务器地址列表 |
### 各类型详解
#### Toggle开关
```csharp
.AddToggle(
key: "auto_update", // 设置键
displayName: "自动更新", // 显示名称
defaultValue: true, // 默认值
description: "启动时检查更新" // 可选描述
)
```
#### Text文本
```csharp
.AddText(
key: "api_key",
displayName: "API密钥",
defaultValue: "",
placeholder: "请输入API密钥", // 占位符
isPassword: true // 密码输入(掩码显示)
)
```
#### Number数值
```csharp
.AddNumber(
key: "refresh_interval",
displayName: "刷新间隔",
defaultValue: 60,
minimum: 10, // 最小值
maximum: 3600, // 最大值
increment: 10 // 步进值
)
```
#### Select选择
```csharp
.AddSelect(
key: "display_mode",
displayName: "显示模式",
choices: new[]
{
new SettingsOptionChoice("compact", "紧凑"),
new SettingsOptionChoice("normal", "标准"),
new SettingsOptionChoice("detailed", "详细")
},
defaultValue: "normal"
)
```
#### Path路径
```csharp
.AddPath(
key: "save_location",
displayName: "保存位置",
defaultValue: "",
pathType: SettingsPathType.Folder, // Folder 或 File
dialogTitle: "选择保存文件夹"
)
```
---
## 🎨 自定义设置页
当声明式设置无法满足需求时,可以创建自定义设置页面。
### 步骤 1创建设置页类
```csharp
using LanMountainDesktop.PluginSdk;
using FluentAvalonia.UI.Controls;
namespace MyPlugin;
public class MySettingsPage : SettingsPageBase
{
private readonly IPluginSettingsService _settingsService;
public MySettingsPage(IPluginSettingsService settingsService)
{
_settingsService = settingsService;
InitializeComponent();
}
private void InitializeComponent()
{
// 创建页面内容
var panel = new StackPanel { Spacing = 16 };
// 添加自定义控件
var expander = new SettingsExpander
{
Header = "高级设置",
Description = "配置插件的高级选项"
};
var toggle = new ToggleSwitch
{
Content = "启用实验性功能"
};
expander.Items.Add(toggle);
panel.Children.Add(expander);
// 添加颜色选择器示例
var colorPicker = new ColorPicker
{
Header = "主题颜色"
};
panel.Children.Add(colorPicker);
Content = panel;
}
}
```
### 步骤 2注册自定义设置页
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddPluginSettingsSection<MySettingsPage>(
sectionId: "myplugin-advanced",
displayName: "高级设置",
iconKey: "Settings");
}
```
### 混合模式
可以同时使用声明式设置和自定义视图:
```csharp
services.AddPluginSettingsSection(
sectionId: "myplugin-settings",
displayName: "插件设置",
configure: section => section
.SetCustomView<MyCustomSettingsPage>() // 设置自定义视图
.AddToggle("enabled", "启用") // 同时声明设置项
.AddText("api_key", "API密钥"),
iconKey: "Settings");
```
---
## 💾 读取和保存设置
### 在服务中读取设置
```csharp
public class WeatherService
{
private readonly IPluginSettingsService _settings;
public WeatherService(IPluginSettingsService settings)
{
_settings = settings;
// 读取设置
var apiKey = _settings.GetValue<string>("api_key", "");
var autoRefresh = _settings.GetValue<bool>("auto_update", true);
var interval = _settings.GetValue<int>("refresh_interval", 60);
// 监听设置变化
_settings.SettingsChanged += (sender, e) =>
{
if (e.Key == "refresh_interval")
{
UpdateTimerInterval(e.NewValue);
}
};
}
}
```
### 在组件中读取设置
```csharp
public class WeatherWidget : Border
{
public WeatherWidget(PluginDesktopComponentContext context)
{
// 通过 context 获取设置
var settings = context.Settings;
var city = settings.GetValue<string>("city", "北京");
var unit = settings.GetValue<string>("temperature_unit", "celsius");
// 监听设置变化
context.Settings.SettingsChanged += (_, e) =>
{
if (e.Key == "city")
{
RefreshWeather(e.NewValue);
}
};
}
}
```
### 保存设置
```csharp
// 在设置页面中保存
private void SaveButton_Click(object? sender, RoutedEventArgs e)
{
_settingsService.SetValue("api_key", ApiKeyTextBox.Text);
_settingsService.SetValue("auto_update", AutoUpdateToggle.IsChecked ?? false);
// 设置会自动持久化,无需手动保存文件
}
```
---
## 🔔 设置变更通知
### 订阅变更事件
```csharp
public class MyService
{
public MyService(IPluginSettingsService settings)
{
settings.SettingsChanged += OnSettingsChanged;
}
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
Console.WriteLine($"设置变更: {e.Key}");
Console.WriteLine($"旧值: {e.OldValue}");
Console.WriteLine($"新值: {e.NewValue}");
// 根据变更的键执行相应操作
switch (e.Key)
{
case "refresh_interval":
UpdateRefreshTimer((int)e.NewValue);
break;
case "theme":
ApplyTheme((string)e.NewValue);
break;
}
}
}
```
---
## 🐛 常见问题
### 问题 1设置不保存
**排查:**
1. 确认 `sectionId` 唯一且合法
2. 检查设置键名是否正确
3. 查看日志是否有权限错误
### 问题 2设置页面不显示
**排查:**
1. 确认已调用 `AddPluginSettingsSection`
2. 检查 `sectionId` 是否唯一
3. 确认设置页类是 `public`
### 问题 3设置变更通知不触发
**原因:** 可能订阅的是不同实例
**解决:** 确保使用注入的 `IPluginSettingsService`
---
## 📚 参考资源
- [IPluginSettingsService 源码](../../LanMountainDesktop.PluginSdk/IPluginSettingsService.cs)
- [SettingsPageBase 源码](../../LanMountainDesktop.PluginSdk/SettingsPageBase.cs)
- [04-开发设置页面](../04-实战案例/04-开发设置页面.md)
---
## 🎯 下一步
学习外观系统:
👉 **[04-外观与主题系统](04-外观与主题系统.md)** - 适配宿主主题
---
*最后更新2026年4月*

View File

@@ -0,0 +1,308 @@
# 04-外观与主题系统
阑山桌面支持暗色/浅色主题切换,插件需要适配宿主的视觉风格,保持界面一致性。
---
## 🎨 主题系统概述
阑山桌面使用 Avalonia UI 的主题系统,支持:
- **浅色主题** - 明亮背景,深色文字
- **深色主题** - 深色背景,浅色文字
- **跟随系统** - 自动匹配 Windows/macOS 主题
---
## 🌗 检测当前主题
### 在组件中检测
```csharp
using Avalonia;
using Avalonia.Styling;
public class MyWidget : Border
{
public MyWidget(PluginDesktopComponentContext context)
{
// 检测当前主题
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
// 根据主题设置颜色
UpdateTheme(isDark);
// 监听主题变化
if (Application.Current != null)
{
Application.Current.ActualThemeVariantChanged += (_, _) =>
{
var newIsDark = Application.Current.ActualThemeVariant == ThemeVariant.Dark;
UpdateTheme(newIsDark);
};
}
}
private void UpdateTheme(bool isDark)
{
Background = new SolidColorBrush(
isDark ? Color.Parse("#FF1E1E1E") : Color.Parse("#FFFFFFFF"));
Foreground = new SolidColorBrush(
isDark ? Colors.White : Colors.Black);
}
}
```
---
## 📐 圆角系统
插件必须使用宿主提供的圆角系统,确保与内置组件视觉一致。
### 为什么插件不能使用 XAML 资源
插件运行在独立的 `AssemblyLoadContext` 中,无法直接访问宿主的资源字典。因此 `{DynamicResource DesignCornerRadiusComponent}` 在插件 XAML 中无效。
### 使用代码设置圆角
```csharp
public class MyWidget : Border
{
public MyWidget(PluginDesktopComponentContext context)
{
// 方法 1使用预设推荐
CornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component);
// 方法 2带最小/最大值限制
CornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component,
minimum: new CornerRadius(8),
maximum: new CornerRadius(24));
// 方法 3自定义基础值应用全局缩放
CornerRadius = context.Appearance.ResolveScaledCornerRadius(
baseRadius: 16,
minimum: 8,
maximum: 32);
}
}
```
### 圆角预设
| 预设 | 默认值 | 用途 |
|-----|-------|------|
| `Micro` | 6px | 微小元素 |
| `Xs` | 12px | 小元素、图标容器 |
| `Sm` | 14px | 小卡片 |
| `Md` | 20px | 普通按钮/卡片 |
| `Lg` | 28px | 大面板 |
| `Xl` | 32px | 强调容器 |
| `Island` | 36px | 大型容器 |
| `Component` | 18px | **桌面组件标准** |
| `Default` | 自适应 | 根据尺寸自动计算 |
### 内部元素圆角
组件内部的卡片、按钮应使用更小的圆角:
```csharp
// 组件根容器 - 使用 Component 预设
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Component);
// 内部卡片 - 使用 Md 预设
var innerCard = new Border
{
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Md),
Background = new SolidColorBrush(Colors.LightGray)
};
// 按钮 - 使用 Sm 预设
var button = new Button
{
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Sm)
};
```
---
## 🎨 颜色系统
### 推荐的颜色策略
```csharp
// 透明背景(推荐)- 让宿主壁纸透出
Background = new SolidColorBrush(Colors.Transparent);
// 毛玻璃效果
Background = new SolidColorBrush(Color.Parse(isDark ? "#40FFFFFF" : "#40000000"));
// 卡片背景
Background = new SolidColorBrush(Color.Parse(isDark ? "#FF2D2D2D" : "#FFFFFFFF"));
// 强调色(使用系统强调色)
var accentColor = Color.Parse("#FF0078D4"); // 阑山桌面主色调
```
### 文字颜色
```csharp
// 主要文字
Foreground = new SolidColorBrush(isDark ? Colors.White : Colors.Black);
// 次要文字
Foreground = new SolidColorBrush(isDark ? Color.Parse("#FFCCCCCC") : Color.Parse("#FF666666"));
// 禁用文字
Foreground = new SolidColorBrush(isDark ? Color.Parse("#FF666666") : Color.Parse("#FF999999"));
```
---
## 🔄 响应外观变化
### 订阅外观变化事件
```csharp
public class MyWidget : Border
{
public MyWidget(PluginDesktopComponentContext context)
{
// 订阅外观变化
context.Appearance.AppearanceChanged += (_, _) =>
{
UpdateAppearance();
};
// 初始化
UpdateAppearance();
}
private void UpdateAppearance()
{
// 重新应用圆角(用户可能调整了全局圆角设置)
var context = ...; // 获取 context
CornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component);
// 更新主题颜色
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
UpdateThemeColors(isDark);
}
}
```
---
## 🧩 使用 FluentAvalonia 控件
推荐使用 FluentAvalonia 控件库,它们自动适配主题:
```xml
<Window xmlns:ui="using:FluentAvalonia.UI.Controls">
<ui:SettingsExpander Header="设置项">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xE713;" />
</ui:SettingsExpander.IconSource>
</ui:SettingsExpander>
</Window>
```
### 常用 FluentAvalonia 控件
| 控件 | 用途 |
|-----|------|
| `SettingsExpander` | 设置项展开器 |
| `SettingsCard` | 设置卡片 |
| `ColorPicker` | 颜色选择器 |
| `NumberBox` | 数字输入框 |
| `ToggleSwitch` | 开关 |
---
## 💡 最佳实践
### 1. 始终使用透明背景
```csharp
// ✅ 好的做法
Background = new SolidColorBrush(Colors.Transparent);
// ❌ 避免硬编码背景色
Background = new SolidColorBrush(Colors.White);
```
### 2. 组件根容器必须使用 Component 圆角
```csharp
// ✅ 正确
CornerRadius = context.ResolveCornerRadius(PluginCornerRadiusPreset.Component);
// ❌ 错误 - 硬编码
CornerRadius = new CornerRadius(18);
```
### 3. 响应主题变化
```csharp
// ✅ 订阅变化事件
Application.Current.ActualThemeVariantChanged += OnThemeChanged;
// ❌ 只在构造函数中设置一次
```
### 4. 使用语义化颜色
```csharp
// ✅ 根据用途选择颜色
var primaryText = isDark ? Colors.White : Colors.Black;
var secondaryText = isDark ? Color.Parse("#FFCCCCCC") : Color.Parse("#FF666666");
// ❌ 避免随意使用颜色
var textColor = Color.Parse("#FF123456");
```
---
## 🐛 常见问题
### 问题 1圆角不生效
**原因:** 在 XAML 中使用 `{DynamicResource}`
**解决:** 在代码中设置圆角(见上文)
### 问题 2主题切换后颜色不对
**原因:** 没有订阅主题变化事件
**解决:** 添加 `ActualThemeVariantChanged` 事件处理
### 问题 3组件与内置组件风格不一致
**排查:**
1. 检查圆角是否使用 `PluginCornerRadiusPreset.Component`
2. 检查背景是否透明
3. 检查是否使用了 FluentAvalonia 控件
---
## 📚 参考资源
- [CORNER_RADIUS_SPEC.md](../../docs/CORNER_RADIUS_SPEC.md)
- [VISUAL_SPEC.md](../../docs/VISUAL_SPEC.md)
- [FluentAvalonia 文档](https://github.com/amwx/FluentAvalonia)
---
## 🎯 下一步
学习插件间通信:
👉 **[05-插件间通信](05-插件间通信.md)** - 与其他插件协作
---
*最后更新2026年4月*

View File

@@ -0,0 +1,375 @@
# 05-插件间通信
插件之间可以通过消息总线和服务导出进行通信,实现功能协作和数据共享。
---
## 🎯 通信方式概述
| 方式 | 适用场景 | 方向 |
|-----|---------|------|
| **消息总线** | 事件通知、广播 | 多对多 |
| **服务导出** | 功能共享、API 暴露 | 一对多 |
| **共享契约** | 数据交换 | 双向 |
---
## 📢 消息总线
使用 `IPluginMessageBus` 发布和订阅消息。
### 发布消息
```csharp
public class WeatherService
{
private readonly IPluginMessageBus _messageBus;
public WeatherService(IPluginMessageBus messageBus)
{
_messageBus = messageBus;
}
public async Task UpdateWeatherAsync()
{
var weather = await FetchWeatherAsync();
// 发布天气更新消息
_messageBus.Publish(new WeatherUpdatedMessage
{
City = weather.City,
Temperature = weather.Temperature,
Condition = weather.Condition
});
}
}
// 定义消息
public class WeatherUpdatedMessage
{
public string City { get; set; } = "";
public double Temperature { get; set; }
public string Condition { get; set; } = "";
}
```
### 订阅消息
```csharp
public class AnotherPluginService
{
public AnotherPluginService(IPluginMessageBus messageBus)
{
// 订阅天气更新消息
messageBus.Subscribe<WeatherUpdatedMessage>(OnWeatherUpdated);
}
private void OnWeatherUpdated(WeatherUpdatedMessage message)
{
Console.WriteLine($"收到天气更新: {message.City} {message.Temperature}°C");
// 根据天气更新自己的状态
UpdateDisplay(message);
}
}
```
### 取消订阅
```csharp
public class MyService : IDisposable
{
private readonly IPluginMessageBus _messageBus;
private readonly Guid _subscriptionId;
public MyService(IPluginMessageBus messageBus)
{
_messageBus = messageBus;
_subscriptionId = messageBus.Subscribe<WeatherUpdatedMessage>(OnWeatherUpdated);
}
public void Dispose()
{
// 取消订阅,避免内存泄漏
_messageBus.Unsubscribe<WeatherUpdatedMessage>(_subscriptionId);
}
}
```
---
## 🔌 服务导出
插件可以将服务导出,供其他插件使用。
### 导出服务
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 注册服务
services.AddSingleton<IWeatherService, WeatherService>();
// 导出服务供其他插件使用
services.AddPluginServiceExport<IWeatherService>(
serviceKey: "MyPlugin.WeatherService",
description: "提供天气查询服务");
}
// 定义服务接口
public interface IWeatherService
{
Task<WeatherInfo> GetCurrentWeatherAsync(string city);
Task<List<WeatherForecast>> GetForecastAsync(string city, int days);
}
// 实现服务
public class WeatherService : IWeatherService
{
public async Task<WeatherInfo> GetCurrentWeatherAsync(string city)
{
// 实现天气查询
}
public async Task<List<WeatherForecast>> GetForecastAsync(string city, int days)
{
// 实现天气预报
}
}
```
### 使用其他插件的服务
```csharp
public class MyWidget : Border
{
public MyWidget(PluginDesktopComponentContext context)
{
// 获取其他插件导出的服务
var weatherService = context.ServiceProvider
.GetExportedService<IWeatherService>("MyPlugin.WeatherService");
if (weatherService != null)
{
// 使用服务
LoadWeatherAsync(weatherService);
}
}
private async void LoadWeatherAsync(IWeatherService weatherService)
{
var weather = await weatherService.GetCurrentWeatherAsync("北京");
UpdateUI(weather);
}
}
```
### 服务导出选项
```csharp
services.AddPluginServiceExport<IWeatherService>(
serviceKey: "MyPlugin.WeatherService",
description: "提供天气查询服务",
version: "1.0.0",
isPublic: true); // 是否公开给其他插件
```
---
## 📦 共享契约
通过 `sharedContracts` 在插件间共享类型定义。
### 定义共享契约
```csharp
// 在共享类库项目中定义
namespace MyPlugin.Shared;
public interface IWeatherData
{
string City { get; }
double Temperature { get; }
string Condition { get; }
}
public class WeatherData : IWeatherData
{
public string City { get; set; } = "";
public double Temperature { get; set; }
public string Condition { get; set; } = "";
}
```
### 在 plugin.json 中声明
```json
{
"id": "com.example.weather",
"name": "天气插件",
"sharedContracts": [
"MyPlugin.Shared.IWeatherData",
"MyPlugin.Shared.WeatherData"
]
}
```
### 使用共享类型
```csharp
// 插件 A 发布数据
public class WeatherService
{
public WeatherData GetWeather()
{
return new WeatherData
{
City = "北京",
Temperature = 25.5,
Condition = "晴"
};
}
}
// 插件 B 接收数据
public class ConsumerService
{
public void ProcessWeather(IWeatherData weather)
{
Console.WriteLine($"{weather.City}: {weather.Temperature}°C");
}
}
```
---
## 🔒 安全考虑
### 服务导出安全
```csharp
// 只导出必要的接口,不暴露实现细节
public interface IPublicApi
{
Task<Data> GetDataAsync();
}
internal class InternalService : IPublicApi
{
// 内部实现细节不暴露
private readonly SecretKey _key;
public async Task<Data> GetDataAsync()
{
// 实现
}
}
```
### 消息验证
```csharp
private void OnMessageReceived(MyMessage message)
{
// 验证消息来源
if (message.SenderId != "TrustedPlugin")
{
return; // 忽略不信任来源的消息
}
// 处理消息
}
```
---
## 💡 最佳实践
### 1. 使用接口定义服务契约
```csharp
// ✅ 好的做法 - 定义接口
public interface IWeatherService { }
// ❌ 避免 - 直接导出实现类
services.AddPluginServiceExport<WeatherService>(...);
```
### 2. 处理服务不可用情况
```csharp
// ✅ 优雅处理服务缺失
var service = context.ServiceProvider
.GetExportedService<IWeatherService>("key");
if (service == null)
{
// 显示提示或降级处理
ShowServiceUnavailableMessage();
return;
}
```
### 3. 及时取消消息订阅
```csharp
// ✅ 在 Dispose 中取消订阅
public void Dispose()
{
_messageBus.Unsubscribe<MyMessage>(_subscriptionId);
}
```
### 4. 版本兼容性
```csharp
// 在服务导出中包含版本信息
services.AddPluginServiceExport<IWeatherService>(
serviceKey: "MyPlugin.WeatherService",
version: "2.0.0", // 语义化版本
description: "天气服务 v2");
```
---
## 🐛 常见问题
### 问题 1消息收不到
**排查:**
1. 确认消息类型完全一致(包括命名空间)
2. 检查订阅是否在消息发布之前
3. 确认没有取消订阅
### 问题 2服务找不到
**排查:**
1. 确认服务已导出(`AddPluginServiceExport`
2. 检查 `serviceKey` 是否正确
3. 确认依赖的插件已安装并启用
### 问题 3类型转换错误
**原因:** 共享契约类型不匹配
**解决:** 确保所有插件使用相同版本的共享契约程序集
---
## 📚 参考资源
- [IPluginMessageBus 源码](../../LanMountainDesktop.PluginSdk/IPluginMessageBus.cs)
- [IPluginExportRegistry 源码](../../LanMountainDesktop.PluginSdk/IPluginExportRegistry.cs)
- [Shared.Contracts](../../LanMountainDesktop.Shared.Contracts/)
---
## 🎯 下一步
查看实战案例:
👉 **[01-开发天气组件](../04-实战案例/01-开发天气组件.md)** - 完整插件开发流程
---
*最后更新2026年4月*

View File

@@ -0,0 +1,307 @@
# 01-PluginBase详解
`PluginBase` 是插件的基类,提供基础功能和生命周期管理。本文详细讲解其用法和扩展点。
---
## 🎯 PluginBase 概述
```csharp
public abstract class PluginBase : IPlugin
{
// 日志记录器
protected ILogger? Logger { get; }
// 初始化方法(必须实现)
public abstract void Initialize(HostBuilderContext context, IServiceCollection services);
}
```
---
## 📝 基本用法
### 最小实现
```csharp
using LanMountainDesktop.PluginSdk;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MyPlugin;
[PluginEntrance]
public sealed class Plugin : PluginBase
{
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 插件初始化逻辑
}
}
```
---
## 🔧 Initialize 方法详解
### 方法签名
```csharp
public abstract void Initialize(
HostBuilderContext context, // 宿主构建上下文
IServiceCollection services // 服务注册集合
);
```
### 参数说明
| 参数 | 类型 | 用途 |
|-----|------|------|
| `context` | `HostBuilderContext` | 访问宿主配置、环境信息 |
| `services` | `IServiceCollection` | 注册服务、组件、设置页面 |
### context 使用示例
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 访问配置
var configValue = context.Configuration["MySetting"];
// 判断运行环境
var isDevelopment = context.HostingEnvironment.IsDevelopment();
// 获取应用名称
var appName = context.HostingEnvironment.ApplicationName;
}
```
---
## 📝 日志记录
### 使用 Logger 属性
```csharp
[PluginEntrance]
public sealed class Plugin : PluginBase
{
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 记录日志
Logger?.LogInformation("插件初始化开始");
try
{
// 初始化逻辑
services.AddSingleton<IMyService, MyService>();
Logger?.LogInformation("插件初始化完成");
}
catch (Exception ex)
{
Logger?.LogError(ex, "插件初始化失败");
throw;
}
}
}
```
### 日志级别
```csharp
Logger?.LogTrace("详细跟踪信息");
Logger?.LogDebug("调试信息");
Logger?.LogInformation("一般信息");
Logger?.LogWarning("警告信息");
Logger?.LogError("错误信息");
Logger?.LogCritical("严重错误");
```
---
## 🔌 服务注册
### 注册单例服务
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 单例 - 整个应用生命周期只有一个实例
services.AddSingleton<IWeatherService, WeatherService>();
}
```
### 注册作用域服务
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 作用域 - 每个作用域一个实例
services.AddScoped<IDataContext, DataContext>();
}
```
### 注册瞬态服务
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 瞬态 - 每次请求都创建新实例
services.AddTransient<IValidator, Validator>();
}
```
### 带配置的服务注册
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<IWeatherService>(provider =>
{
var httpClient = provider.GetRequiredService<HttpClient>();
var logger = provider.GetRequiredService<ILogger<WeatherService>>();
var apiKey = context.Configuration["WeatherApiKey"];
return new WeatherService(httpClient, logger, apiKey);
});
}
```
---
## 🧩 完整示例
```csharp
using LanMountainDesktop.PluginSdk;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace WeatherPlugin;
[PluginEntrance]
public sealed class Plugin : PluginBase
{
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
Logger?.LogInformation("天气插件初始化开始");
try
{
// 1. 注册 HTTP 客户端
services.AddHttpClient("weather", client =>
{
client.BaseAddress = new Uri("https://api.weather.com/");
client.Timeout = TimeSpan.FromSeconds(30);
});
// 2. 注册服务
services.AddSingleton<IWeatherService, WeatherService>();
services.AddSingleton<ILocationService, LocationService>();
// 3. 注册桌面组件
services.AddPluginDesktopComponent<WeatherWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "WeatherPlugin.Widget",
DisplayName = "天气",
IconKey = "Weather",
Category = "信息",
MinWidthCells = 4,
MinHeightCells = 3
});
// 4. 注册设置页面
services.AddPluginSettingsSection(
"weather-settings",
"天气设置",
section => section
.AddText("api_key", "API密钥", isPassword: true)
.AddText("default_city", "默认城市", defaultValue: "北京")
.AddToggle("auto_refresh", "自动刷新", defaultValue: true)
.AddNumber("refresh_interval", "刷新间隔(分钟)",
defaultValue: 30, minimum: 5, maximum: 120),
iconKey: "Settings");
Logger?.LogInformation("天气插件初始化完成");
}
catch (Exception ex)
{
Logger?.LogError(ex, "天气插件初始化失败");
throw;
}
}
}
```
---
## 💡 最佳实践
### 1. 使用 try-catch 包装初始化逻辑
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
try
{
// 初始化逻辑
}
catch (Exception ex)
{
Logger?.LogError(ex, "初始化失败");
throw; // 重新抛出,让宿主知道初始化失败
}
}
```
### 2. 按依赖顺序注册服务
```csharp
// ✅ 先注册被依赖的服务
services.AddSingleton<IDataService, DataService>();
// 再注册依赖它们的服务
services.AddSingleton<IWeatherService, WeatherService>(); // 依赖 IDataService
// 最后注册组件
services.AddPluginDesktopComponent<WeatherWidget>(options);
```
### 3. 记录初始化过程
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
Logger?.LogInformation("开始初始化...");
Logger?.LogDebug("注册服务...");
services.AddSingleton<IMyService, MyService>();
Logger?.LogDebug("注册组件...");
services.AddPluginDesktopComponent<MyWidget>(options);
Logger?.LogInformation("初始化完成");
}
```
---
## 📚 参考资源
- [PluginBase 源码](../../LanMountainDesktop.PluginSdk/PluginBase.cs)
- [IPlugin 接口](../../LanMountainDesktop.PluginSdk/IPlugin.cs)
- [Microsoft.Extensions.DependencyInjection 文档](https://docs.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection)
---
## 🎯 下一步
学习组件注册 API
👉 **[02-组件注册与配置](02-组件注册与配置.md)**
---
*最后更新2026年4月*

View File

@@ -0,0 +1,182 @@
# 02-组件注册与配置
`AddPluginDesktopComponent` 是注册桌面组件的核心 API。本文详细讲解其用法和配置选项。
---
## 🎯 API 概览
```csharp
public static IServiceCollection AddPluginDesktopComponent<TComponent>(
this IServiceCollection services,
PluginDesktopComponentOptions options)
where TComponent : class, IControl
```
---
## 📋 基本用法
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
services.AddPluginDesktopComponent<MyWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "MyPlugin.MyWidget",
DisplayName = "我的组件",
IconKey = "Home",
Category = "工具",
MinWidthCells = 4,
MinHeightCells = 3
});
}
```
---
## 🔧 PluginDesktopComponentOptions
### 完整属性列表
| 属性 | 类型 | 必需 | 说明 |
|-----|------|------|------|
| `ComponentId` | `string` | ✅ | 唯一标识符 |
| `DisplayName` | `string` | ✅ | 显示名称 |
| `IconKey` | `string` | ✅ | 图标键名 |
| `Category` | `string` | ✅ | 分类 |
| `MinWidthCells` | `int` | ✅ | 最小宽度(格) |
| `MinHeightCells` | `int` | ✅ | 最小高度(格) |
| `CornerRadiusPreset` | `PluginCornerRadiusPreset` | ❌ | 圆角预设 |
| `ResizeMode` | `PluginDesktopComponentResizeMode` | ❌ | 调整大小模式 |
### ComponentId
```csharp
ComponentId = "MyPlugin.WeatherWidget"
```
- 必须唯一
- 建议使用 `插件ID.组件名` 格式
- 一经发布不可更改
### DisplayName
```csharp
DisplayName = "天气"
```
- 显示在组件库中
- 支持本地化(通过资源文件)
### IconKey
```csharp
IconKey = "Weather"
```
使用 [Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons) 的图标名。
### Category
```csharp
Category = "工具"
```
常用分类:
- `工具` - 实用工具
- `信息` - 信息展示
- `娱乐` - 娱乐相关
- `系统` - 系统监控
### MinWidthCells / MinHeightCells
```csharp
MinWidthCells = 4, // 4格宽约240像素
MinHeightCells = 3 // 3格高约180像素
```
---
## 🎨 圆角配置
```csharp
services.AddPluginDesktopComponent<MyWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "MyPlugin.Widget",
DisplayName = "我的组件",
IconKey = "Home",
Category = "工具",
MinWidthCells = 4,
MinHeightCells = 3,
CornerRadiusPreset = PluginCornerRadiusPreset.Component
});
```
---
## 📐 调整大小模式
```csharp
services.AddPluginDesktopComponent<MyWidget>(
new PluginDesktopComponentOptions
{
// ...
ResizeMode = PluginDesktopComponentResizeMode.Free
});
```
| 模式 | 说明 |
|-----|------|
| `Free` | 自由调整大小 |
| `Fixed` | 固定大小 |
| `AspectRatio` | 保持宽高比 |
---
## 🧩 完整示例
```csharp
public override void Initialize(HostBuilderContext context, IServiceCollection services)
{
// 天气组件
services.AddPluginDesktopComponent<WeatherWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "WeatherPlugin.Widget",
DisplayName = "天气",
IconKey = "Weather",
Category = "信息",
MinWidthCells = 4,
MinHeightCells = 3,
CornerRadiusPreset = PluginCornerRadiusPreset.Component,
ResizeMode = PluginDesktopComponentResizeMode.Free
});
// 时钟组件
services.AddPluginDesktopComponent<ClockWidget>(
new PluginDesktopComponentOptions
{
ComponentId = "ClockPlugin.Widget",
DisplayName = "时钟",
IconKey = "Clock",
Category = "工具",
MinWidthCells = 4,
MinHeightCells = 4,
CornerRadiusPreset = PluginCornerRadiusPreset.Component,
ResizeMode = PluginDesktopComponentResizeMode.AspectRatio
});
}
```
---
## 📚 参考资源
- [PluginDesktopComponentOptions 源码](../../LanMountainDesktop.PluginSdk/PluginDesktopComponentOptions.cs)
- [02-桌面组件系统](../02-核心概念与原理/02-桌面组件系统.md)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,147 @@
# 03-设置API详解
设置 API 允许插件添加配置页面和持久化用户设置。
---
## 🎯 API 概览
### 声明式设置
```csharp
services.AddPluginSettingsSection(
string sectionId,
string displayName,
Action<PluginSettingsSectionBuilder> configure,
string iconKey);
```
### 自定义设置页
```csharp
services.AddPluginSettingsSection<TPage>(
string sectionId,
string displayName,
string iconKey)
where TPage : SettingsPageBase;
```
---
## 📋 声明式设置详解
### 基本用法
```csharp
services.AddPluginSettingsSection(
"myplugin-settings",
"我的设置",
section => section
.AddToggle("enabled", "启用", defaultValue: true)
.AddText("name", "名称", defaultValue: ""),
iconKey: "Settings");
```
### 设置类型
#### Toggle开关
```csharp
.AddToggle(
key: "auto_update",
displayName: "自动更新",
defaultValue: true,
description: "启动时检查更新")
```
#### Text文本
```csharp
.AddText(
key: "api_key",
displayName: "API密钥",
defaultValue: "",
placeholder: "请输入",
isPassword: false)
```
#### Number数值
```csharp
.AddNumber(
key: "interval",
displayName: "刷新间隔",
defaultValue: 60,
minimum: 10,
maximum: 3600,
increment: 10)
```
#### Select选择
```csharp
.AddSelect(
key: "theme",
displayName: "主题",
choices: new[]
{
new SettingsOptionChoice("light", "浅色"),
new SettingsOptionChoice("dark", "深色")
},
defaultValue: "light")
```
#### Path路径
```csharp
.AddPath(
key: "save_path",
displayName: "保存路径",
defaultValue: "",
pathType: SettingsPathType.Folder,
dialogTitle: "选择文件夹")
```
---
## 🔧 读取和保存设置
### 使用 IPluginSettingsService
```csharp
public class MyService
{
private readonly IPluginSettingsService _settings;
public MyService(IPluginSettingsService settings)
{
_settings = settings;
// 读取
var value = _settings.GetValue<string>("key", "default");
// 保存
_settings.SetValue("key", "new value");
// 监听变化
_settings.SettingsChanged += (s, e) =>
{
if (e.Key == "key")
{
HandleChange(e.NewValue);
}
};
}
}
```
---
## 📚 参考资源
- [IPluginSettingsService 源码](../../LanMountainDesktop.PluginSdk/IPluginSettingsService.cs)
- [03-设置系统集成](../02-核心概念与原理/03-设置系统集成.md)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,106 @@
# 04-外观API详解
外观 API 提供圆角、主题等视觉相关的功能。
---
## 🎯 IPluginAppearanceContext
```csharp
public interface IPluginAppearanceContext
{
// 获取圆角值
CornerRadius ResolveCornerRadius(PluginCornerRadiusPreset preset);
// 获取带限制的圆角值
CornerRadius ResolveCornerRadius(
PluginCornerRadiusPreset preset,
CornerRadius? minimum,
CornerRadius? maximum);
// 获取缩放后的圆角值
CornerRadius ResolveScaledCornerRadius(
double baseRadius,
double? minimum,
double? maximum);
// 外观变化事件
event EventHandler? AppearanceChanged;
}
```
---
## 📐 圆角 API
### 获取圆角值
```csharp
public MyWidget(PluginDesktopComponentContext context)
{
// 使用预设
CornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component);
}
```
### 带限制的圆角
```csharp
var radius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component,
minimum: new CornerRadius(8),
maximum: new CornerRadius(24));
```
### 缩放圆角
```csharp
var radius = context.Appearance.ResolveScaledCornerRadius(
baseRadius: 16,
minimum: 8,
maximum: 32);
```
---
## 🎨 圆角预设
| 预设 | 值 | 用途 |
|-----|---|------|
| Micro | 6px | 微小元素 |
| Xs | 12px | 小元素 |
| Sm | 14px | 小卡片 |
| Md | 20px | 普通按钮 |
| Lg | 28px | 大面板 |
| Xl | 32px | 强调容器 |
| Island | 36px | 大型容器 |
| Component | 18px | 桌面组件 |
| Default | 自适应 | 自动计算 |
---
## 🔄 响应外观变化
```csharp
public MyWidget(PluginDesktopComponentContext context)
{
context.Appearance.AppearanceChanged += (_, _) =>
{
// 重新应用圆角
CornerRadius = context.Appearance.ResolveCornerRadius(
PluginCornerRadiusPreset.Component);
};
}
```
---
## 📚 参考资源
- [IPluginAppearanceContext 源码](../../LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs)
- [04-外观与主题系统](../02-核心概念与原理/04-外观与主题系统.md)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,73 @@
# 05-本地化支持
本地化 API 支持多语言资源管理。
---
## 🎯 资源文件
### 文件位置
```
Localization/
├── zh-CN.json # 简体中文
├── en-US.json # 英文
├── ja-JP.json # 日文
└── ko-KR.json # 韩文
```
### 资源格式
```json
{
"PluginName": "我的插件",
"Settings": {
"Title": "设置",
"Save": "保存"
},
"Messages": {
"Hello": "你好,{0}!",
"Error": "错误:{0}"
}
}
```
---
## 📝 使用本地化
### 注入 IStringLocalizer
```csharp
public class MyService
{
private readonly IStringLocalizer<MyService> _localizer;
public MyService(IStringLocalizer<MyService> localizer)
{
_localizer = localizer;
}
public void DoWork()
{
// 简单字符串
var name = _localizer["PluginName"];
// 带参数
var message = _localizer["Messages.Hello", "用户"];
// 嵌套键
var title = _localizer["Settings.Title"];
}
}
```
---
## 📚 参考资源
- [Microsoft.Extensions.Localization 文档](https://docs.microsoft.com/dotnet/api/microsoft.extensions.localization)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,215 @@
# 01-GitHub Actions入门
GitHub Actions 是自动化构建、测试和发布插件的强大工具。本文介绍如何为插件项目配置 CI/CD 流程。
---
## 🎯 什么是 GitHub Actions
GitHub Actions 是 GitHub 提供的持续集成/持续部署CI/CD服务可以
- ✅ 自动构建插件
- ✅ 运行单元测试
- ✅ 打包 .laapp 文件
- ✅ 自动发布到 GitHub Releases
---
## 📁 工作流文件位置
```
.github/workflows/
├── build.yml # 构建工作流
├── release.yml # 发布工作流
└── code-quality.yml # 代码质量检查
```
---
## 🚀 基础工作流示例
### 最简单的构建工作流
```yaml
# .github/workflows/build.yml
name: Build Plugin
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
build:
runs-on: windows-latest
steps:
# 1. 检出代码
- name: Checkout
uses: actions/checkout@v4
# 2. 设置 .NET
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
# 3. 还原依赖
- name: Restore
run: dotnet restore
# 4. 构建
- name: Build
run: dotnet build --configuration Release --no-restore
# 5. 上传构建产物
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: plugin-package
path: bin/Release/net10.0/*.laapp
```
---
## 📋 工作流详解
### 触发条件on
```yaml
on:
# 推送到指定分支时触发
push:
branches: [main, master, develop]
# 创建 Pull Request 时触发
pull_request:
branches: [main, master]
# 手动触发
workflow_dispatch:
# 定时触发每天凌晨2点
schedule:
- cron: '0 2 * * *'
# 创建标签时触发(用于发布)
push:
tags:
- 'v*'
```
### 运行环境runs-on
```yaml
jobs:
build:
runs-on: windows-latest # Windows 环境
# 或
runs-on: ubuntu-latest # Linux 环境
# 或
runs-on: macos-latest # macOS 环境
```
### 矩阵构建(多平台)
```yaml
jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
dotnet: ['10.0.x']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet }}
```
---
## 🔧 常用 Actions
### 检出代码
```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整历史(用于生成版本号)
```
### 设置 .NET
```yaml
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
```
### 上传产物
```yaml
- uses: actions/upload-artifact@v4
with:
name: plugin-package
path: bin/Release/net10.0/*.laapp
retention-days: 30 # 保留30天
```
### 下载产物
```yaml
- uses: actions/download-artifact@v4
with:
name: plugin-package
path: ./artifacts
```
---
## 💡 最佳实践
### 1. 缓存依赖
```yaml
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
```
### 2. 使用语义化版本
```yaml
- name: Get Version
id: version
run: |
VERSION=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
```
### 3. 条件执行
```yaml
- name: Deploy
if: github.ref == 'refs/heads/main' # 只在 main 分支执行
run: echo "Deploying..."
```
---
## 🎯 下一步
学习自动打包配置:
👉 **[02-配置自动构建](02-配置自动构建.md)**
---
*最后更新2026年4月*

View File

@@ -0,0 +1,130 @@
# 02-配置自动构建
配置 GitHub Actions 自动构建插件项目。
---
## 🎯 完整构建工作流
```yaml
# .github/workflows/build.yml
name: Build
on:
push:
branches: [main, master]
paths-ignore:
- '**.md'
- '.gitignore'
pull_request:
branches: [main, master]
env:
DOTNET_VERSION: '10.0.x'
CONFIGURATION: 'Release'
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration ${{ env.CONFIGURATION }} --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: plugin-${{ github.run_number }}
path: bin/${{ env.CONFIGURATION }}/net10.0/*.laapp
```
---
## 🔧 关键配置说明
### 路径过滤
```yaml
on:
push:
paths-ignore:
- '**.md' # 忽略文档修改
- '.gitignore' # 忽略 gitignore 修改
- 'docs/**' # 忽略 docs 文件夹
```
### 环境变量
```yaml
env:
DOTNET_VERSION: '10.0.x'
CONFIGURATION: 'Release'
PLUGIN_NAME: 'MyPlugin'
```
### 构建步骤
```yaml
steps:
# 1. 检出
- uses: actions/checkout@v4
# 2. 设置 .NET
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
# 3. 缓存
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ hashFiles('**/*.csproj') }}
# 4. 还原
- run: dotnet restore
# 5. 构建
- run: dotnet build -c ${{ env.CONFIGURATION }} --no-restore
# 6. 测试
- run: dotnet test --no-build
# 7. 上传
- uses: actions/upload-artifact@v4
with:
name: plugin
path: bin/Release/net10.0/*.laapp
```
---
## 📚 参考资源
- [GitHub Actions 文档](https://docs.github.com/actions)
- [.NET CI/CD 指南](https://docs.microsoft.com/dotnet/devops/github-actions-overview)
---
*最后更新2026年4月*

View File

@@ -0,0 +1,156 @@
# 03-自动打包与发布
配置 GitHub Actions 自动打包 .laapp 并发布到 GitHub Releases。
---
## 🎯 发布工作流
```yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
env:
DOTNET_VERSION: '10.0.x'
jobs:
build-and-release:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Get Version
id: version
run: |
$version = $env:GITHUB_REF -replace 'refs/tags/v', ''
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Package
run: |
$version = "${{ steps.version.outputs.VERSION }}"
Rename-Item -Path "bin/Release/net10.0/MyPlugin.laapp" -NewName "MyPlugin-$version.laapp"
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: bin/Release/net10.0/*.laapp
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
---
## 📋 发布流程
### 1. 创建标签
```bash
# 创建版本标签
git tag -a v1.0.0 -m "Release version 1.0.0"
# 推送标签到 GitHub
git push origin v1.0.0
```
### 2. 自动触发
推送标签后GitHub Actions 会自动:
1. 检出代码
2. 构建项目
3. 打包 .laapp
4. 创建 Release
5. 上传产物
### 3. 查看 Release
在 GitHub 仓库页面 → Releases 查看自动创建的发布。
---
## 🔧 高级配置
### 预发布版本
```yaml
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: bin/Release/net10.0/*.laapp
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
```
### 生成变更日志
```yaml
- name: Generate Changelog
id: changelog
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: .github/changelog-config.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
uses: softprops/action-gh-release@v1
with:
body: ${{ steps.changelog.outputs.changelog }}
files: bin/Release/net10.0/*.laapp
```
---
## 💡 最佳实践
### 版本号管理
```yaml
- name: Update Version
run: |
$version = "${{ github.ref_name }}" -replace '^v', ''
$json = Get-Content plugin.json | ConvertFrom-Json
$json.version = $version
$json | ConvertTo-Json | Set-Content plugin.json
```
### 多文件发布
```yaml
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
bin/Release/net10.0/*.laapp
README.md
LICENSE
```
---
## 🎯 下一步
学习多平台构建:
👉 **[04-多平台构建策略](04-多平台构建策略.md)**
---
*最后更新2026年4月*

View File

@@ -0,0 +1,92 @@
# 01-插件打包规范
了解 .laapp 包的规范和结构,确保插件能正确安装和运行。
---
## 📦 .laapp 文件格式
`.laapp` 是阑山桌面的插件包格式,本质上是一个 **ZIP 压缩包**
### 文件结构
```
MyPlugin.laapp
├── plugin.json # 插件清单(必需)
├── MyPlugin.dll # 主程序集(必需)
├── Localization/ # 本地化文件夹
│ ├── zh-CN.json
│ └── en-US.json
└── *.dll # 依赖项
```
---
## 📋 plugin.json 规范
### 必需字段
```json
{
"id": "com.example.myplugin",
"name": "我的插件",
"description": "插件描述",
"author": "作者名",
"version": "1.0.0",
"apiVersion": "5.0.0",
"entranceAssembly": "MyPlugin.dll",
"sharedContracts": []
}
```
### 字段验证规则
| 字段 | 规则 |
|-----|------|
| `id` | 小写字母、数字、点号,反向域名格式 |
| `version` | 语义化版本x.y.z |
| `apiVersion` | 必须与 SDK 版本兼容 |
| `entranceAssembly` | 必须与 DLL 文件名一致 |
---
## 🔨 构建配置
### .csproj 关键配置
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
</ItemGroup>
<!-- 确保资源文件复制到输出目录 -->
<ItemGroup>
<None Update="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\**\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
```
---
## ✅ 打包检查清单
- [ ] `plugin.json` 格式正确
- [ ] `id` 全局唯一
- [ ] `apiVersion` 与 SDK 版本匹配
- [ ] DLL 文件名与 `entranceAssembly` 一致
- [ ] 所有依赖项已包含
- [ ] 本地化文件完整
---
*最后更新2026年4月*

View File

@@ -0,0 +1,75 @@
# 02-版本管理策略
合理的版本管理是插件维护的基础。
---
## 🎯 语义化版本SemVer
版本格式:`主版本.次版本.修订号`
| 版本变化 | 说明 | 示例 |
|---------|------|------|
| 主版本Major | 破坏性变更 | 1.0.0 → 2.0.0 |
| 次版本Minor | 新功能,向后兼容 | 1.0.0 → 1.1.0 |
| 修订号Patch | Bug 修复 | 1.0.0 → 1.0.1 |
---
## 📋 版本示例
| 版本 | 含义 |
|-----|------|
| `1.0.0` | 首个正式版 |
| `1.1.0` | 新增功能 |
| `1.1.1` | 修复 Bug |
| `2.0.0-beta` | 2.0 测试版 |
| `2.0.0-rc1` | 2.0 候选版 |
---
## 🔄 版本更新流程
### 1. 更新版本号
```json
// plugin.json
{
"version": "1.1.0"
}
```
### 2. 更新 CHANGELOG.md
```markdown
## [1.1.0] - 2024-04-13
### 新增
- 添加天气预警功能
- 支持多城市管理
### 修复
- 修复定位失败问题
```
### 3. 创建 Git 标签
```bash
git add .
git commit -m "Release v1.1.0"
git tag -a v1.1.0 -m "Release version 1.1.0"
git push origin main --tags
```
---
## 💡 最佳实践
- 使用 GitHub Releases 管理版本
- 每个版本都写更新日志
- 测试版使用 `-beta``-alpha` 后缀
- 保持向后兼容,避免频繁主版本升级
---
*最后更新2026年4月*

View File

@@ -0,0 +1,71 @@
# 03-发布到插件市场
将插件发布到阑山桌面插件市场,让更多用户使用。
---
## 🎯 发布流程
### 1. 准备材料
- 插件包(.laapp
- 插件图标256x256 PNG
- 截图(至少 1 张)
- 详细描述
- 更新日志
### 2. 提交审核
1. 访问阑山桌面开发者门户
2. 登录开发者账号
3. 点击「提交新插件」
4. 填写插件信息
5. 上传 .laapp 文件
6. 提交审核
### 3. 审核标准
| 检查项 | 要求 |
|-------|------|
| 功能完整性 | 插件能正常运行 |
| 安全性 | 无恶意代码 |
| 用户体验 | 界面美观,操作流畅 |
| 文档完整 | 有基本使用说明 |
---
## 📋 元数据要求
### 必需信息
```json
{
"id": "com.example.plugin",
"name": "插件名称",
"description": "简短描述50字以内",
"author": "作者名",
"version": "1.0.0",
"tags": ["工具", "天气"],
"website": "https://github.com/..."
}
```
### 图标规范
- 格式PNG
- 尺寸256x256 像素
- 背景:透明
- 风格:与阑山桌面一致
---
## 🚀 发布后
- 关注用户反馈
- 及时修复 Bug
- 定期更新功能
- 维护更新日志
---
*最后更新2026年4月*

View File

@@ -0,0 +1,82 @@
# 04-更新与维护
插件发布后的持续维护和更新策略。
---
## 🔄 更新策略
### 热修复Hotfix
发现严重 Bug 时立即发布:
```bash
# 1. 修复 Bug
git checkout -b hotfix/critical-bug
# ... 修复代码 ...
# 2. 更新版本号(修订号+1
# plugin.json: "version": "1.0.1"
# 3. 提交并发布
git commit -m "Fix critical bug"
git tag -a v1.0.1 -m "Hotfix v1.0.1"
git push origin main --tags
```
### 功能更新
```bash
# 1. 开发新功能
git checkout -b feature/new-feature
# 2. 完成功能后合并到 main
# 3. 更新版本号(次版本+1
# plugin.json: "version": "1.1.0"
# 4. 发布
git tag -a v1.1.0 -m "Release v1.1.0"
git push origin main --tags
```
---
## 📊 维护清单
### 日常维护
- [ ] 监控用户反馈
- [ ] 查看崩溃报告
- [ ] 回复用户问题
- [ ] 更新依赖包
### 定期维护
- [ ] 每季度检查 SDK 更新
- [ ] 每年评估功能需求
- [ ] 定期更新文档
---
## 🐛 Bug 处理流程
1. **收集信息** - 复现步骤、环境信息
2. **定位问题** - 本地调试复现
3. **修复验证** - 修复后充分测试
4. **发布更新** - 按热修复流程发布
5. **通知用户** - 在 Release 中说明
---
## 💡 最佳实践
- 保持向后兼容
- 及时响应用户反馈
- 定期发布小更新
- 维护清晰的更新日志
- 废弃功能提前通知
---
*最后更新2026年4月*