# 图吧工具箱插件开发文档(API v2)
## 设计规则
内置工具入口是珍贵资源,不推荐创建多个简单的内置工具。**默认一个工具承载多个相关功能**:例如 Windows 优化只有一个工具入口,在自定义页面内组织清理、系统健康、网络、电源等分组或选项卡。不要把每一个简单按钮分别注册成工具。
支持多个独立插件项目,但仅当需求属于独立领域或用户明确要求独立入口时才拆分。此规则已写入 AI 开发技能和工作区说明。
API v2 为重新设计的接口,不兼容 API v1。SDK 程序集版本为 2.0.0.0,旧插件须使用当前 SDK 与清单重新编译。MSIX 版本不显示侧边栏插件入口,但「内置工具 → 插件」仍可打开插件页并加载已安装插件;MSIX 与便携版使用各自的数据目录,插件需在对应版本中安装。
## 注册自定义 XAML 工具
SDK 以 NuGet 包 `TubaWinUi3.PluginSdk` 分发(`net10.0`,无 WinUI 依赖)。AI 开发工作区优先用 `PackageReference` 从 nuget.org 还原;离线或宿主 SDK 尚未发布时,回退到工作区 `sdk/` 副本(宿主维护)。普通插件可通过 XAML 与宿主控件接口完成开发,不必引用宿主或 WinUI 包。
```csharp
using TubaWinUi3.PluginSdk;
public sealed class Entry : ITubaPlugin
{
public void Register(IPluginHost host) => host.RegisterTool(new PluginToolDefinition
{
Id = "maintenance",
Name = "电脑维护",
Category = "系统优化",
Description = "在一个页面中集中提供维护功能",
IconPath = "assets/icon.svg",
Tags = ["系统", "维护"],
Xaml = """
""",
InitializeAsync = async view =>
{
view.SetValue("Note", await view.Services.GetSettingAsync("note", view.CancellationToken) ?? "");
view.BindClick("Save", async context =>
{
await context.Services.SetSettingAsync("note", view.GetValue("Note"), context.CancellationToken);
view.SetValue("Result", "已保存");
});
view.BindClick("Hardware", async context =>
await context.Services.NavigateAsync(PluginDestination.Hardware, context.CancellationToken));
}
});
}
```
工具全局 ID 由宿主生成:`plugin:<小写包ID>:<小写工具ID>`。只在同步 `Register` 期间注册;宿主先暂存再整体提交,注册失败不替换旧版本。不要在注册阶段创建窗口、联网、执行系统操作或后台常驻任务。工具接入内置列表、分类、搜索、收藏和快捷方式。`IPluginHost.Language` 提供宿主界面语言(`zh-CN` / `en-US`),可在注册时生成本地化的工具名称与说明;分类使用内置分类原名(如 `游戏工具`),显示层由宿主本地化。
### PluginToolDefinition
| 字段 | 含义 |
| --- | --- |
| Id / Name | 包内稳定 ID、显示名称;ID 不含路径 |
| Category / Description | 分类和功能说明 |
| IconPath / Glyph | 包内 SVG/PNG/JPEG/BMP 路径与备用字体图标 |
| Tags | 搜索标签 |
| Xaml | 内联的 WinUI XAML 字符串 |
| XamlPath | 包内独立 XAML 文件路径,宿主读取,最大 2 MB |
| CreateContent | 高级接口:在 UI 线程创建并返回一个新的 WinUI FrameworkElement |
| ExecuteAsync | 极少数不需要页面的独立即时操作 |
| InitializeAsync | 页面加载一次后初始化控件、读取配置与绑定事件 |
| ShowInSidebar / ShowInSettings | 为同一工具添加可选侧边栏/设置入口,默认 false |
`Xaml`、`XamlPath`、`CreateContent`、`ExecuteAsync` 必须且只能选一个。每次打开都创建新的页面实例。`ShowInSidebar` 与 `ShowInSettings` 是同一工具的附加导航位置,不另注册工具;默认不要占用侧边栏。
XAML 必须有一个 FrameworkElement 根和 WinUI 默认 xmlns。可以使用 Grid、ScrollViewer、TabView、列表、输入、按钮和资源样式等标准 WinUI 控件。使用 ThemeResource 跟随主题与高对比度,给交互控件添加自动化名称和 ID。运行时 XAML 不支持 x:Class、x:Bind 和 Click="方法名" 等代码隐藏绑定;通过 InitializeAsync/BindClick 接线,或在 CreateContent 中用 C# 直接构建 UI。引用插件自定义 CLR 控件、编译 XAML/PRI 的发现机制不由宿主自动配置;需要这些功能时使用 CreateContent 并自行验证依赖。
自定义 UI 在标准工具页头下完整展示,支持应用内页面或用户设置的独立窗口。页面离开或窗口关闭时取消页面令牌、解绑宿主按钮并执行 OnClosed 清理。后台任务必须响应取消,宿主无法强制终止忽略取消的同进程代码。
## 视图 API:PluginViewContext
| API | 用途 |
| --- | --- |
| Root / FindElement(name) | 原生对象,供引用 WinUI 的高级插件进行类型转换 |
| BindClick(name, callback) | 绑定 Button/ToggleButton 点击;ToggleSwitch 自动接到 Toggled;同页异步动作串行 |
| BindValueChanged(name, callback) | 绑定 ToggleSwitch/ToggleButton、TextBox、ComboBox、NumberBox、Slider 值变化;初始化与 SetValue 不触发回调 |
| GetValue / SetValue | 文本、开关、数字、下拉选项与内容 |
| SetEnabled / SetVisible | 更新控件可用性和显示状态 |
| SetImage(name, relativePath) | 将包内 SVG/图片设置到 Image 或 ImageIcon |
| RunOnUiThreadAsync | 后台任务需要访问控件时回到 UI 线程 |
| OnClosed(cleanup) | 退订自建事件、停止计时器、释放插件自有资源 |
| Host / Services / CancellationToken | 对话框、宿主能力和页面生命周期 |
GetValue 在 UI 线程读取;TextBox/TextBlock 使用字符串,ToggleSwitch/ToggleButton 使用 true/false,NumberBox/Slider/ProgressBar 使用 invariant culture 数字,下拉使用 ComboBoxItem.Tag,未设 Tag 时使用 Content。SetValue/SetEnabled/SetVisible/SetImage 可调度到 UI 线程;需要等待更新完成时用 RunOnUiThreadAsync。
开关不是按钮,不要将 ToggleSwitch 强转 ButtonBase。推荐使用 `BindValueChanged` 并在回调读取当前值;按钮提交式的设置页仍使用 `BindClick`。宿主会在异步执行期间禁用所有已绑定控件,结束后恢复原可用状态;关闭时退订事件。初始化/程序赋值不执行设置写入,防止读取状态时意外修改系统。`ReportProgress("")` 或空白消息关闭宿主进度条。
### SDK 2.1 宿主服务
`Services.GetService()` 只返回公开 SDK 服务,未知类型返回 null,支持能力检测;不向插件暴露内部页面、静态服务类或原始 NavigationView。
| 服务 | 能力 |
| --- | --- |
| IPluginBackgroundService | GetAsync、SetAsync、SetOpacityAsync;修改工具箱背景与透明度,传 null 清除 |
| IPluginNavigationService | GetItemsAsync、UpdateAsync、SetOrderAsync、ResetAsync;导航名称、字形/SVG 图标、显隐、排序及恢复默认 |
| IPluginHostInfoService | GetInfo;宿主版本、API 版本、语言、架构、MSIX 身份 |
| IPluginDiagnosticsService | ReadJsonAsync;Hardware、Sensors、Disks、CpuZ 只读快照;不释放宿主共享监控器 |
```csharp
var background = context.Services.GetService()
?? throw new NotSupportedException("宿主需要更新");
await background.SetAsync("assets/wallpaper.png", 0.25, context.CancellationToken);
// await background.SetAsync(null, ct: context.CancellationToken); // 清除工具箱背景
var navigation = context.Services.GetService()!;
var items = await navigation.GetItemsAsync(context.CancellationToken);
var hardware = items.FirstOrDefault(i => i.Id == "hardware");
if (hardware is not null)
await navigation.UpdateAsync(new(hardware.Id, Title: "硬件中心"), context.CancellationToken);
await navigation.SetOrderAsync(new[] { "hardware", "home" }, context.CancellationToken);
// await navigation.ResetAsync(ct: context.CancellationToken); // 恢复默认导航
var info = context.Services.GetService()?.GetInfo();
var diagnostics = context.Services.GetService();
// 用户要求诊断时再调用:
// var json = await diagnostics!.ReadJsonAsync(PluginDiagnosticKind.Sensors, context.CancellationToken);
```
背景服务仅作用于工具箱,不设置 Windows 桌面壁纸。支持 PNG/JPEG/BMP/WebP,必须是真实的本地图片,包内相对路径遵循包资源路径规则,也可传用户选择的绝对路径。宿主复制到背景图库,插件更新、卸载不影响已选背景。透明度必须为有限的 0–1 数值,0 为完全透明。
### 完全访问(fullTrust)
需要直接操作宿主界面、导航、页面实例或任意宿主设置时,在 `tubatools.json` 声明:
```json
{
"apiVersion": 2,
"fullTrust": true,
"assembly": "plugin/MyPlugin.dll",
"entryType": "MyPlugin.Entry",
"name": "我的工具箱增强"
}
```
开启后 `context.Services.GetService()` 返回宿主访问服务:
| 成员 | 能力 |
| --- | --- |
| MainWindow / RootVisual | 主窗口与根视觉元素;用 `VisualTreeHelper` 可遍历、修改任意已存在的界面 |
| NavigationView / NavigationFrame / CurrentPage | 原始导航控件、Frame 和当前页面实例 |
| RunOnUiThreadAsync | 回到宿主 UI 线程执行界面修改 |
| NavigateAsync / NavigateBackAsync | 导航到任意页面类型(宿主页面或插件自己的 Page 子类) |
| GetSettingAsync / SetSettingAsync | 读写**任意宿主设置键**(不是插件的隔离设置),含删除 |
| ThemeChanged / LanguageChanged | 宿主主题、语言变化事件 |
需要强类型访问时,插件项目可引用安装目录里的 `TubaWinUi3.dll`(**不要打包该 DLL**),然后直接使用 `TubaWinUi3.Plugins.PluginHostBridge`:
```xml
$(AppDir)\TubaWinUi3.dll
false
```
`PluginHostBridge` 暴露 `MainWindow`、`Navigation`、`NavigationFrame`、`Root`、`CurrentPage`、`Dispatcher`、`HostAssembly`/`FindType`、`GetSetting/SetSetting`、`Navigate/NavigateBack`;插件也可以直接调用宿主里 public 的静态服务类(`ToolCatalog`、`AppSettings`、`FavoritesService` 等)。未公开的宿主成员可以通过 `HostAssembly` 反射访问,但不保证跨版本兼容。
注意事项:
- fullTrust 不是安全沙箱。插件与工具箱同进程、同权限运行,本来就可以用反射访问宿主;这个开关只是把常用入口正式化。只给可信插件开启。
- 强类型入口和宿主内部成员绑定,宿主升级后插件需要按目标版本重新编译;直接修改视觉树还会受列表虚拟化、页面重建影响,优先使用公开的导航/设置入口。
- 修改界面必须在宿主 UI 线程完成(`RunOnUiThreadAsync` 或 `PluginHostBridge.Dispatcher`)。
- MSIX 版本同样支持 fullTrust 插件;数据目录仍在包 LocalState。
### 启动钩子(IPluginStartup)
需要“安装后立即生效、用户不用先打开工具页”的插件,可以让入口同时实现 `IPluginStartup`:
```csharp
public sealed class MyPlugin : ITubaPlugin, IPluginStartup
{
public void Register(IPluginHost host) { /* 注册工具 */ }
public ValueTask StartupAsync(IPluginStartupContext context, CancellationToken ct)
{
// 主窗口已经完成首次加载;此时 NavigationView、页面和 App.Current.Resources 已可用。
var host = context.HostAccess;
return ValueTask.CompletedTask;
}
public ValueTask ShutdownAsync(CancellationToken ct) => ValueTask.CompletedTask;
}
```
宿主在主窗口首次 `Loaded` 后调用 `StartupAsync`,此时主窗口、NavigationView、页面和 `App.Current.Resources` 已可用;不要在更早的阶段移动视觉树。停用或卸载时调用 `ShutdownAsync`。启动上下文提供 `PluginId`、`DataDirectory` 和插件隔离设置读写;`HostAccess` 只在清单声明 `fullTrust: true` 时返回,未声明时为 null。典型用法是主题包、全局快捷键、常驻监控;普通工具页插件不需要实现这个接口。
示例:`plugins/Tubatools.ClassicTheme` 在启动钩子里把主窗口、侧栏、标题栏和卡片资源替换为经典蓝色渐变,并在停用/卸载时还原。
导航以返回的稳定 Id 定位:home、hardware、builtin、benchmark、plugins、ai-assistant、settings;分类为 `category:分类原名`,插件工具为 `tool:工具全局ID`。UpdateAsync 的 null 属性保持原覆盖值,空字符串清除该属性覆盖;IconPath 可传包内相对 SVG/位图或本地绝对路径,图片复制后保存。显示名、图标、排序与显隐不改变原跳转目标;设置仍处于页脚,不参与 SetOrderAsync。先列出的菜单排到前面,未列出的保持当前相对顺序。ResetAsync 可按 ID 恢复,省略 ID 则全部恢复;全局自定义在重启后保留,应为用户提供恢复入口。新工具页面仍通过 RegisterTool 注册并按需设置 ShowInSidebar,不能注入未注册的任意宿主页面。
FindElement 使用当前 XAML 根的名字作用域,不能直接查找另一个 DataTemplate/ControlTemplate 作用域中的实例;这些情况请通过原生 Root 自行定位。图片等包资源不要假设 ms-appx:/// 指向插件包:使用 SetImage 或 Services.GetAssetPath 得到真实路径。
## 执行与宿主 API
按钮回调接收 `PluginExecutionContext`:
- CancellationToken:本次动作取消;页面关闭也会取消。
- ShowMessageAsync(title, text):原生可滚动消息对话框。
- ConfirmAsync(title, text):明确确认/取消,默认不选确认。
- ReportProgress(text):页面状态提示,允许后台上报。
- Services:下表能力。
初始化在 UI 线程开始,重计算使用 Task.Run,I/O 使用异步 API。初始化取消通过离开页面;按钮任务另有宿主「停止」。错误在页面显示,避免异常扩散。对系统修改、清理、重置、写盘等应告知影响并使用 ConfirmAsync,不能从一句「优化一下」推断对所有操作的授权。
| IPluginServices | 含义 |
| --- | --- |
| PluginId / PackageDirectory | 包身份与当前版本包目录,包目录按只读资源使用 |
| DataDirectory | 插件私有可写目录,跨版本保留 |
| GetAssetPath(relative) | 校验并解析包内资源,不允许路径逃逸 |
| GetSettingAsync / SetSettingAsync | 插件隔离的字符串配置,null 删除;并发写入串行、原子落盘 |
| GetToolsAsync | 获取当前真实可用工具的 ID、名称、分类和说明 |
| RunToolAsync(id) | 按查询所得 ID 调用内置/目录工具,沿用原启动流程;不能递归启动自身 |
| NavigateAsync(destination) | Home、Hardware、BuiltinTools、Settings、Plugins、AiAssistant |
| GetAppearance / ApplyAppearanceAsync | 查询/修改工具箱主题、侧边栏或顶部导航、紧凑模式,修改会持久化 |
| OpenUriAsync | 打开 HTTP、HTTPS 或 ms-settings 地址 |
| CopyTextAsync | 复制文本到系统剪贴板 |
| PickOpenFileAsync | 原生单文件选择,取消返回 null |
| PickFolderAsync | 原生文件夹选择(与内置工具同一 Win32/WinRT 选择器),取消返回 null |
外观修改应由用户主动选择并确认。宿主不提供任意内部 AppSettings 键写入;插件配置写入自己的配置区。目录工具可能需要下载或管理员授权,调用沿用原界面的行为;不能保证第三方 GUI 工具支持自动化或被插件取消。
高级 CreateContent 插件可引用正确版本的 WinUI,返回新的 FrameworkElement;SDK/WinUI/WinRT 类型共享宿主身份,不要打包框架 DLL 副本。代码 UI 必须自行维护事件与生命周期,建议在 InitializeAsync 中通过 OnClosed 登记清理。
## 图标与清单元信息
`tubatools.json` 位于 NuGet 包根:
```json
{
"apiVersion": 2,
"assembly": "plugin/MyPlugin.dll",
"entryType": "MyPlugin.Entry",
"name": "电脑维护套件",
"description": "集中组织清理、健康检查和网络维护",
"author": "实际作者",
"website": "https://example.com/project",
"icon": "assets/icon.svg"
}
```
assembly、icon、IconPath、XamlPath 为包内相对路径,不允许绝对路径或目录逃逸。工具 IconPath 优先,其次清单 icon,最后 Glyph。SVG 建议使用明确 viewBox、纯路径/形状与本地颜色;WinUI 的 SVG 子集不保证支持所有滤镜、外部资源或 CSS。支持 SVG、PNG、JPG/JPEG、BMP,单个图标最大 2 MB,SVG 禁止 DTD。为原生 SVG 保留透明背景,不需要栅格化。
图标会用于内置工具、分类/收藏/搜索卡片、页头和可选侧边栏;设置入口与已安装插件列表也显示图标。桌面快捷方式仍使用备用 Glyph 生成 ico。插件管理页读取清单名称、描述、作者、主页并始终保留真实包 ID 与版本。NuGet 官方发现页使用 NuGet 的作者和描述,不在安装前执行插件读取信息。
## 项目、资源与打包
完整可运行示例:`samples/HelloPlugin`。它只注册一个工具,包含配置、文件选择、剪贴板、外观、工具列表、硬件导航和取消示例,并附带 SVG。
外部插件先添加 SDK NuGet 包(仓库内示例直接引用 SDK 项目):
```powershell
dotnet add package TubaWinUi3.PluginSdk --version 2.1.0
```
```xml
net10.0
enable
enable
true
Tubatools.MyPlugin
2.0.0
ActualAuthor
说明实际功能
Tubatools
false
```
SDK 通过 NuGet 包分发,仓库外的插件按上面的 `PackageReference` 还原即可;`PrivateAssets="all"` + `ExcludeAssets="runtime"` 让它只参与编译,不复制 SDK 程序集、不打进插件包(运行时由宿主提供)。仓库内的示例与官方插件直接引用 SDK 项目,产物内容与 NuGet 方式一致。自己的托管/原生依赖也须打包并保持布局,宿主不还原 NuGet 依赖,不运行安装脚本或 MSBuild targets。禁止打包 SDK、宿主、WinUI、.NET 框架副本。涉及原生库时说明支持架构并分别验证。
仓库中构建示例:
```powershell
dotnet pack samples/HelloPlugin/HelloPlugin.csproj -c Release -o artifacts/plugins
```
AI 工作区从根目录显式指定具体 csproj:`dotnet pack MyPlugin/MyPlugin.csproj -c Release -o artifacts/`。编译需要 .NET 10 SDK:先用 `dotnet --list-sdks` 检查,缺少时用 `winget install --id Microsoft.DotNet.SDK.10 -e --source winget --accept-package-agreements --accept-source-agreements` 安装(或从 手动安装)。成功后宿主自动校验和动态加载本轮新包,不需要手动加载按钮,也无需先上传 NuGet。每次修改递增版本,保持 PackageId、工具 ID 和分类以保留收藏;新版本注册失败保留旧版本。已打开的旧页面继续使用旧代码和资源直到关闭。
## 来源、安装与存储
发现来源为 nuget.org 官方 V3 服务中完整 Tubatools 标签的稳定版本。支持搜索、翻页、安装、更新、停用与卸载。MSIX 下侧边栏不展示插件入口,安装与管理仍通过「内置工具 → 插件」完成;插件存储位于包 LocalState,与便携版隔离。旧社区工具不恢复。
官方内置工具 `Steam 工具箱` 已迁移为插件参考实现:`plugins/Tubatools.SteamToolbox`(`Tubatools.SteamToolbox.nupkg`)。它保留了原页面全部布局与功能(收藏/账号/搜索排序/多存储库分组/挂卡登录/二维码/Steam Guard/任务计时/账号游戏库),并演示了第三方依赖打包(SteamKit2、QRCoder)和 `CreateContent` 代码构建界面的完整用法。构建与发包命令:
```powershell
dotnet pack plugins/Tubatools.SteamToolbox/Tubatools.SteamToolbox.csproj -c Release -o artifacts/plugins
```
「上传到 NuGet」只打开官方上传页并展示本文档,工具箱不保存上传凭据、不代上传。完整的新手发布流程见下节「发布到 NuGet(小白版,统一网页上传)」。插件与工具箱同进程、同权限,标签不代表审核;包校验和路径约束不构成代码沙箱。
包限制:压缩 100 MB、解压 300 MB、5000 文件,校验 nuspec 身份与标签、API v2 清单,拒绝路径逃逸/符号链接。首次安装动态加载;AI 本地新版本支持热替换。NuGet 商店更新需重启后使用新版本;停用/卸载会立即从工具列表隐藏,已加载程序集与后台代码在重启后完全释放,不强制卸载已打开页面使用的程序集。
安装记录与版本资源:`/Plugins/installed.json` 与 `/Plugins/<包ID>/<版本>/`。
插件可写数据与配置:`/PluginData/<包ID>/`,独立于版本目录,升级和卸载不会自动删除。坏配置不会被静默覆盖。
验证建议:一个入口内多功能布局、SVG 与透明度、事件绑定、输入读写、配置隔离/升级保留、动作取消/关闭清理、错误提示、热替换与回滚,以及 MSIX 下从内置工具进入插件页、包 LocalState 数据隔离。独立窗口与主窗口都需实际验证。
## 发布到 NuGet(小白版,统一网页上传)
> 这一节写给第一次发布插件的人。按顺序做即可;每一步都说明“在做什么、会看到什么”。
>
> 插件本机测试通过后,**建议直接发布到 NuGet**:这是让别人用起来最简单的方式,不需要服务器、命令行或 API Key,网页点几下就行。发布后包里的 DLL、XAML、图片和 README 都能被任何人下载;先确认没有密钥、密码和私人路径。工具箱只从 nuget.org 搜索带 `Tubatools` 标签的稳定版包,没有私有源安装入口。
### 1. 测试通过后,鼓励发布
- 已在本机完成 `dotnet pack`,并实际安装、启用、打开页面、重启、停用/卸载测试过。
- 测试通过后,**建议直接发布到 NuGet**:别人不用拿到你的电脑,在图吧工具箱的「内置工具 → 插件」里搜名字就能安装。不要做完只留在本地。
- 发布前确认用户同意公开;公开后包内代码、XAML、图片和 README 都可下载,先检查没有密钥、密码、私人路径。
- `PackageId` 唯一且以后不改;`PackageTags` 包含 `Tubatools`;版本是稳定版(不要 `-beta`/`-preview`,插件页只搜稳定版)。
- 包内没有宿主 SDK、WinUI、.NET 框架 DLL。
### 2. 注册和检查包名
1. 打开 ,用 Microsoft/GitHub 账号登录,完成邮箱验证。
2. 打开 `https://www.nuget.org/packages/` 检查是否被占用;被占用就换一个唯一 ID,例如 `Tubatools.你的昵称.插件名`。
- `PackageId` 就是插件在商店里的“门牌号”,也是安装目录和升级身份,发布后不要改。
### 3. 填写 NuGet 包信息
在 csproj 的 `` 里至少写:
```xml
Tubatools.MyPlugin
1.0.0
你的昵称
一句话说明插件做什么
Tubatools
MIT
README.md
```
建议再加图标和 README(NuGet 页面会更像样):
```xml
icon.png
```
- `PackageTags` 必须包含 `Tubatools`(大小写不敏感;不要写全角);图吧工具箱的搜索就是按这个标签找包。
- `Version` 每次发布递增;同一个版本不能传第二次。
- README 用中文写:插件做什么、怎么用、需要什么权限、怎么卸载;不要写密钥或私人路径。
- 图标建议 128×128 PNG;`PackageIcon` 指向的文件必须真的打进包。
- 插件安装后显示的名称/简介来自包内 `tubatools.json` 的 `name`/`description`;NuGet 商店页面显示的是 csproj 的 `Description`/`Authors`,两边都写清楚。
- 包内清单和资源的完整要求见前文「图标与清单元信息」与「项目、资源与打包」。
- 不想加 README/图标就删掉对应属性和 `` 行;`PackageReadmeFile`/`PackageIcon` 指向的文件必须真实存在,否则打包会报错。
### 4. 打包并检查
```powershell
# AI 工作区用 artifacts/ 顶层,宿主自动加载只扫描这一层
dotnet pack <项目.csproj> -c Release -o artifacts/
```
仓库里的示例可以用 `artifacts/plugins/`;上传时按实际生成的 `.nupkg` 路径操作。确认 `.nupkg` 里至少有:
- 根目录 `tubatools.json`,`apiVersion` 为 2
- `plugin/<入口>.dll` 和 `plugin/<入口>.deps.json`
- `assets/` 下的图标、XAML
- 没有 `TubaWinUi3.PluginSdk.dll`、`TubaWinUi3.dll`、`Microsoft.*.dll`、`WinRT.Runtime.dll`
### 5. 上传(统一用网页版)
1. 打开 (工具箱插件页的「上传到 NuGet」按钮也是打开这里,不会代传)。
2. 登录后选择 `.nupkg`,NuGet 会读出包信息;核对 `PackageId` 和 `Version`。
3. 按页面提示提交/发布;首次发布的新包 ID 按页面要求确认。
4. 上传成功不等于立刻能搜到:等几分钟到十几分钟,让 NuGet 完成索引。
### 6. 验证并告诉别人怎么装
- 打开 `https://www.nuget.org/packages/`,确认版本、作者、README 和标签。
- 把包页面链接或 `PackageId` 发给朋友;对方在工具箱里:
**内置工具 → 插件 → 搜索插件名或 PackageId → 安装 → 确认“插件会以工具箱权限运行”的提示**。
- MSIX 版同样从「内置工具 → 插件」进入;MSIX 与便携版数据目录隔离,需要在对应版本中安装。
- 分享时给 NuGet 页面链接即可,不需要传 `.nupkg` 文件。
### 7. 更新版本
- 改代码 → 递增 ``(同一版本不能传第二次)→ 重新 `dotnet pack` → **网页上传同一个 PackageId**。
- 不要改 `PackageId`;它是插件身份,也是用户收藏、配置和更新关系的依据。
- NuGet 页面显示新版本后,用户在插件页点“更新到 x.y.z”;通常重启工具箱后使用新版本。
### 8. 常见错误
| 现象 | 原因/处理 |
| --- | --- |
| 登录/上传页报错 | 账号未验证、登录过期或没有该 PackageId 的权限;重新登录并完成邮箱验证 |
| 409 | 这个版本已存在;递增 ``,不要复用 |
| 400 | Authors/Description/README/Icon 等元信息不完整,或 README/图标文件没打进包;看 NuGet 的详细提示 |
| 图吧工具箱搜不到 | 还在索引;或不是稳定版;或 `PackageTags` 没有 `Tubatools`;或包不是公开的 |
| 安装后不显示/不加载 | 检查 `tubatools.json` 是否在包根、`apiVersion` 是否 2、`assembly`/`entryType` 是否正确、入口 DLL/deps.json 是否在 `plugin/` |
| 包太大 | 宿主限制压缩 100 MB、解压 300 MB、5000 个文件;删掉 SDK/宿主/WinUI/框架副本 |
| 想下架 | 在 NuGet 包页面 Unlist(从搜索隐藏,已安装用户仍保留);不要随意删除;PackageId 是插件身份 |
### 9. 关于“只想给朋友用”
当前内置插件页只从 **nuget.org 公开源** 搜索带 `Tubatools` 标签的稳定包,没有私有源/白名单安装入口。只想小范围使用就不要上传公开包;可以把源码和 `.nupkg` 直接给朋友,或让对方在开发环境中加载。不要为了“私有”把密钥或包放进公开仓库。
### 10. 发布前检查清单
- [ ] 本机实际测试通过(安装、启用、页面、重启、停用/卸载)
- [ ] 插件测试通过后已主动建议发布(用户不想公开则尊重选择)
- [ ] 用户明确同意公开上传
- [ ] `PackageId` 唯一且以后不改
- [ ] `Version` 已递增,且是稳定版
- [ ] `PackageTags` 包含 `Tubatools`
- [ ] `tubatools.json` 在包根、`apiVersion=2`
- [ ] 入口 DLL/deps.json/资源齐全
- [ ] 没有 SDK/宿主/WinUI/框架 DLL
- [ ] 没有密钥、密码、私人路径
- [ ] 已通过网页上传,NuGet 页面已验证,已告诉用户如何安装/更新
## 本次交付验证(2026-11-07)
- 主项目 x64 构建通过,含新版 PRI;无窗口 CLI help 返回 ok=true。
- 43 项相关回归通过;另一次包含真实组件交互、API v2、真实 DLL 加载和本地化的 29 项复核通过。覆盖 API v1 拒绝、XAML/图标资源解析、整批注册回滚、配置隔离/并发/坏文件保护、多个插件并存与热替换。
- 原生预览实际加载生产 PluginXamlView 与 PluginToolPage:SVG 显示、输入与保存、异步动作停止、关闭重开恢复配置、离开/返回后再次保存均通过。预览中的全局工具/外观服务使用替身,未在用户工具箱执行系统变更。
- 已执行 Release pack,产物 artifacts/plugins/Tubatools.Hello.2.0.0.nupkg,实际核对清单、SVG、入口 DLL 与 deps.json;未上传 NuGet。
- 当前工具箱主窗口中的设置/侧边栏入口、真实 MSIX 安装和 ARM64 实机仍需单独验收。原生预览已关闭,未关闭用户的工具箱实例。