XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

引入项目独立 NuGet 包引用功能

本次提交实现了工具包 manifest.json 对 nugetPackages 字段的支持,允许每个工具独立声明所需 NuGet 包及版本。新增了类型定义、校验规则和唯一性检查。Code Studio 设计器界面支持可视化管理包引用,实时校验输入。运行/编译时自动注入声明的 NuGet 包,支持覆盖内置包版本。完善了相关文档和示例,补充了序列化、校验、注入等单元测试,提升了工具包的扩展性和可维护性。

ed5f0d2
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

12 个文件 +1004 -17
Added XFEToolBox.Client.Wpf.Test/ConsoleFoldBlockStyleTests.cs +94 -0
@@ -0,0 +1,94 @@
1 using System.Collections;
2 using System.Reflection;
3 using System.Windows;
4 using System.Windows.Controls;
5 using System.Windows.Media;
6 using System.Windows.Shapes;
7 using XFEToolBox.Client.Utilities;
8
9 namespace XFEToolBox.Client.Wpf.Test;
10
11 public static class ConsoleFoldBlockStyleTests
12 {
13 [Test]
14 public static void FoldBlockUsesCompactDedicatedButtonAndTogglesItsState()
15 {
16 RunSta(() =>
17 {
18 var converterType = typeof(BufferedConsoleRenderer).Assembly.GetType(
19 "XFEToolBox.Client.Utilities.DecoratedTextConverter",
20 throwOnError: true)!;
21 var convertMethod = converterType.GetMethod(
22 "ConvertToInlineList",
23 BindingFlags.Public | BindingFlags.Static,
24 binder: null,
25 types: [typeof(string), typeof(Color)],
26 modifiers: null)!;
27 const string markup =
28 "[foldblock color: white #9898e7 title: 分析对象:ConsoleShowcaseObject text: 第一行\n第二行]";
29 var converted = (IEnumerable)convertMethod.Invoke(null, [markup, Colors.White])!;
30 var foldGrid = converted.Cast<object>().OfType<Grid>().Single();
31
32 Ensure(foldGrid.ColumnDefinitions.Count == 3, "折叠块标题没有使用独立的按钮列。");
33 Ensure(Math.Abs(foldGrid.ColumnDefinitions[1].Width.Value - 34) < double.Epsilon,
34 "折叠按钮列没有保持紧凑宽度。");
35
36 var titleBorder = (Border)foldGrid.Children[0];
37 var buttonBorder = (Border)foldGrid.Children[1];
38 var contentBorder = (Border)foldGrid.Children[2];
39 var button = (Button)buttonBorder.Child;
40 var chevron = button.Content as Path;
41
42 Ensure(button.Style is not null, "折叠按钮没有使用专用样式。");
43 Ensure(button.MinWidth == 0 && Math.Abs(button.Width - 34) < double.Epsilon,
44 "折叠按钮仍受全局 Button 最小宽度影响。");
45 Ensure(button.Padding == new Thickness(0), "折叠按钮仍保留了全局 Button 内边距。");
46 Ensure(chevron?.RenderTransform is RotateTransform,
47 "折叠按钮没有使用可旋转的 Fluent 折线图标。");
48 Ensure(GetBrushColor(titleBorder.Background) == Color.FromRgb(0x98, 0x98, 0xE7),
49 "折叠块标题背景色不正确。");
50 Ensure(GetBrushColor(buttonBorder.Background) == GetBrushColor(titleBorder.Background),
51 "折叠按钮与标题没有形成统一色块。");
52 Ensure(contentBorder.Visibility == Visibility.Collapsed && Equals(button.ToolTip, "展开详情"),
53 "折叠块初始状态不正确。");
54
55 button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
56 Ensure(contentBorder.Visibility == Visibility.Visible && Equals(button.ToolTip, "折叠详情"),
57 "点击后折叠块没有展开。");
58 Ensure(Math.Abs(((RotateTransform)chevron!.RenderTransform).Angle - 180) < double.Epsilon,
59 "展开时折线图标没有旋转。");
60
61 button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
62 Ensure(contentBorder.Visibility == Visibility.Collapsed && Equals(button.ToolTip, "展开详情"),
63 "再次点击后折叠块没有收起。");
64 Ensure(Math.Abs(((RotateTransform)chevron.RenderTransform).Angle) < double.Epsilon,
65 "收起时折线图标没有复位。");
66 });
67 }
68
69 private static Color GetBrushColor(Brush brush)
70 => brush is SolidColorBrush solidColorBrush
71 ? solidColorBrush.Color
72 : throw new InvalidOperationException("预期使用纯色画刷。");
73
74 private static void RunSta(Action action)
75 {
76 Exception? failure = null;
77 var thread = new Thread(() =>
78 {
79 try { action(); }
80 catch (Exception exception) { failure = exception; }
81 }) { IsBackground = true };
82 thread.SetApartmentState(ApartmentState.STA);
83 thread.Start();
84 Ensure(thread.Join(TimeSpan.FromSeconds(10)), "折叠块 WPF 回归测试超时。");
85 if (failure is not null)
86 throw new InvalidOperationException(failure.Message, failure);
87 }
88
89 private static void Ensure(bool condition, string message)
90 {
91 if (!condition)
92 throw new InvalidOperationException(message);
93 }
94 }
Added XFEToolBox.Client.Wpf.Test/ToolNuGetPackageTests.cs +125 -0
@@ -0,0 +1,125 @@
1 using System.IO;
2 using System.Text.Json;
3 using XFEToolBox.Client.Utilities;
4 using XFEToolBox.Core.Tools;
5
6 namespace XFEToolBox.Client.Wpf.Test;
7
8 public static class ToolNuGetPackageTests
9 {
10 [Test]
11 public static void ManifestRoundTripsProjectNuGetPackages()
12 {
13 const string json = """
14 {
15 "packageFormatVersion": 1,
16 "id": "xfestudio.package-test",
17 "name": "Package Test",
18 "version": "1.0.0",
19 "description": "test",
20 "author": "XFEstudio",
21 "nugetPackages": [
22 { "id": "XFEExtension.NetCore.XFEConsole", "version": "2.6.0" }
23 ],
24 "entry": {
25 "viewXaml": "Code/Main.xaml",
26 "viewClass": "Test.Main",
27 "viewCodeBehind": "Code/Main.xaml.cs"
28 }
29 }
30 """;
31
32 var manifest = JsonSerializer.Deserialize<ToolPackageManifest>(
33 json,
34 new JsonSerializerOptions(JsonSerializerDefaults.Web))
35 ?? throw new InvalidOperationException("manifest 反序列化失败。 ");
36
37 Ensure(manifest.NuGetPackages.Length == 1, "项目 NuGet 包没有从 manifest 读出。 ");
38 Ensure(manifest.NuGetPackages[0].Id == "XFEExtension.NetCore.XFEConsole", "NuGet 包 ID 读取错误。 ");
39 Ensure(manifest.NuGetPackages[0].Version == "2.6.0", "NuGet 包版本读取错误。 ");
40
41 var serialized = JsonSerializer.Serialize(manifest, new JsonSerializerOptions(JsonSerializerDefaults.Web));
42 Ensure(serialized.Contains("\"nugetPackages\"", StringComparison.Ordinal),
43 "manifest 序列化没有使用 nugetPackages 字段。 ");
44 }
45
46 [Test]
47 public static void PackageRulesRequireSafeIdsAndPinnedVersions()
48 {
49 Ensure(ToolNuGetPackageRules.IsValidPackageId("XFEExtension.NetCore.XFEConsole"), "合法包 ID 被拒绝。 ");
50 Ensure(ToolNuGetPackageRules.IsValidExactVersion("2.6.0"), "合法稳定版本被拒绝。 ");
51 Ensure(ToolNuGetPackageRules.IsValidExactVersion("2.6.0-beta.1"), "合法预发布版本被拒绝。 ");
52 Ensure(!ToolNuGetPackageRules.IsValidPackageId("bad<package"), "可注入 XML 的包 ID 被接受。 ");
53 Ensure(!ToolNuGetPackageRules.IsValidExactVersion("[2.0,3.0)"), "版本范围不应被接受。 ");
54 Ensure(!ToolNuGetPackageRules.IsValidExactVersion("2.*"), "浮动版本不应被接受。 ");
55 }
56
57 [Test]
58 public static void RuntimeProjectIncludesEachToolPackageAndAllowsToolkitOverride()
59 {
60 var project = ToolProjectRunService.CreateProjectFile(
61 Path.GetTempPath(),
62 "ToolPackageReferenceTest",
63 "XFEToolBox",
64 [
65 new ToolNuGetPackageReference
66 {
67 Id = "XFEExtension.NetCore.XFEConsole",
68 Version = "2.6.0"
69 },
70 new ToolNuGetPackageReference
71 {
72 Id = "CommunityToolkit.Mvvm",
73 Version = "8.4.1"
74 }
75 ]);
76
77 Ensure(project.Contains("PackageReference Include=\"XFEExtension.NetCore.XFEConsole\" Version=\"2.6.0\"", StringComparison.Ordinal),
78 "运行时项目没有注入工具自己的 NuGet 包。 ");
79 Ensure(project.Contains("PackageReference Include=\"CommunityToolkit.Mvvm\" Version=\"8.4.1\"", StringComparison.Ordinal),
80 "项目无法覆盖内置 CommunityToolkit.Mvvm 版本。 ");
81 Ensure(Count(project, "PackageReference Include=\"CommunityToolkit.Mvvm\"") == 1,
82 "运行时项目生成了重复的 CommunityToolkit.Mvvm 引用。 ");
83
84 EnsureThrows<InvalidDataException>(() => ToolProjectRunService.CreateProjectFile(
85 Path.GetTempPath(),
86 "DuplicatePackageTest",
87 "XFEToolBox",
88 [
89 new ToolNuGetPackageReference { Id = "Example.Package", Version = "1.0.0" },
90 new ToolNuGetPackageReference { Id = "example.package", Version = "1.0.1" }
91 ]),
92 "大小写不同的重复包没有被拒绝。 ");
93 }
94
95 private static int Count(string value, string fragment)
96 {
97 var count = 0;
98 var start = 0;
99 while ((start = value.IndexOf(fragment, start, StringComparison.Ordinal)) >= 0)
100 {
101 count++;
102 start += fragment.Length;
103 }
104 return count;
105 }
106
107 private static void Ensure(bool condition, string message)
108 {
109 if (!condition)
110 throw new InvalidOperationException(message);
111 }
112
113 private static void EnsureThrows<TException>(Action action, string message) where TException : Exception
114 {
115 try
116 {
117 action();
118 }
119 catch (TException)
120 {
121 return;
122 }
123 throw new InvalidOperationException(message);
124 }
125 }
Modified XFEToolBox.Core/Tools/ToolPackageManifest.cs +40 -0
@@ -1,3 +1,6 @@
1 using System.Text.Json.Serialization;
2 using System.Text.RegularExpressions;
3
1 4 namespace XFEToolBox.Core.Tools;
2 5
3 6 /// <summary>
@@ -38,6 +41,13 @@ public sealed class ToolPackageManifest
38 41
39 42 public string? ReleaseNotes { get; init; }
40 43
44 /// <summary>
45 /// Exact NuGet package references restored when this tool is compiled.
46 /// Package versions are intentionally pinned so a published tool remains reproducible.
47 /// </summary>
48 [JsonPropertyName("nugetPackages")]
49 public ToolNuGetPackageReference[] NuGetPackages { get; init; } = [];
50
41 51 /// <summary>
42 52 /// Requires the host to display elevation state and launch this tool through Windows UAC.
43 53 /// Users cannot override this requirement with a per-tool preference.
@@ -59,6 +69,36 @@ public sealed class ToolPackageManifest
59 69 public string[] RequestedPermissions { get; init; } = [];
60 70 }
61 71
72 public sealed class ToolNuGetPackageReference
73 {
74 public required string Id { get; init; }
75
76 public required string Version { get; init; }
77 }
78
79 public static partial class ToolNuGetPackageRules
80 {
81 public const int MaximumPackageCount = 64;
82 public const int MaximumPackageIdLength = 100;
83 public const int MaximumVersionLength = 64;
84
85 public static bool IsValidPackageId(string? value)
86 => !string.IsNullOrWhiteSpace(value)
87 && value.Length <= MaximumPackageIdLength
88 && PackageIdRegex().IsMatch(value);
89
90 public static bool IsValidExactVersion(string? value)
91 => !string.IsNullOrWhiteSpace(value)
92 && value.Length <= MaximumVersionLength
93 && ExactVersionRegex().IsMatch(value);
94
95 [GeneratedRegex("^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$")]
96 private static partial Regex PackageIdRegex();
97
98 [GeneratedRegex("^[0-9]+(?:\\.[0-9]+){0,3}(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$")]
99 private static partial Regex ExactVersionRegex();
100 }
101
62 102 public sealed class ToolWindowManifest
63 103 {
64 104 public const double DefaultWidth = 760;
Modified XFEToolBox.Server.Core/Services/ToolPackageValidator.cs +18 -0
@@ -140,6 +140,7 @@ public sealed partial class ToolPackageValidator(ToolPackageValidationOptions op
140 140 throw new ToolPackageValidationException("minimumHostVersion 必须是有效的 SemVer 版本。");
141 141 if (manifest.Tags is null || manifest.Tags.Length > 20 || manifest.Tags.Any(tag => string.IsNullOrWhiteSpace(tag) || tag.Length > 40))
142 142 throw new ToolPackageValidationException("标签最多 20 个,且每个标签长度为 1-40 个字符。");
143 ValidateNuGetPackages(manifest.NuGetPackages);
143 144 if (manifest.RequestedPermissions is null || manifest.RequestedPermissions.Length > 32 || manifest.RequestedPermissions.Any(permission => string.IsNullOrWhiteSpace(permission) || permission.Length > 64))
144 145 throw new ToolPackageValidationException("请求的权限列表不合法。");
145 146
@@ -160,6 +161,23 @@ public sealed partial class ToolPackageValidator(ToolPackageValidationOptions op
160 161 }
161 162 }
162 163
164 private static void ValidateNuGetPackages(IReadOnlyCollection<ToolNuGetPackageReference>? packages)
165 {
166 if (packages is null || packages.Count > ToolNuGetPackageRules.MaximumPackageCount)
167 throw new ToolPackageValidationException($"NuGet 包最多允许 {ToolNuGetPackageRules.MaximumPackageCount} 个。");
168
169 var packageIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
170 foreach (var package in packages)
171 {
172 if (package is null || !ToolNuGetPackageRules.IsValidPackageId(package.Id))
173 throw new ToolPackageValidationException("NuGet 包 ID 不合法。");
174 if (!ToolNuGetPackageRules.IsValidExactVersion(package.Version))
175 throw new ToolPackageValidationException($"NuGet 包 {package.Id} 必须使用精确版本,例如 1.2.3 或 1.2.3-beta.1。");
176 if (!packageIds.Add(package.Id))
177 throw new ToolPackageValidationException($"NuGet 包不能重复:{package.Id}。");
178 }
179 }
180
163 181 private void ValidateXamlFiles(ZipArchive archive)
164 182 {
165 183 foreach (var entry in archive.Entries.Where(entry =>
Modified XFEToolBox/Utilities/DecoratedTextConverter.cs +78 -14
@@ -3,7 +3,9 @@ using System.Text.RegularExpressions;
3 3 using System.Windows;
4 4 using System.Windows.Controls;
5 5 using System.Windows.Documents;
6 using System.Windows.Input;
6 7 using System.Windows.Media;
8 using System.Windows.Shapes;
7 9 using System.Windows.Threading;
8 10
9 11 namespace XFEToolBox.Client.Utilities;
@@ -99,19 +101,20 @@ public partial class DecoratedTextConverter
99 101 {
100 102 Margin = new Thickness(3)
101 103 };
102 foldGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new(220) });
103 foldGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new(20) });
104 foldGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new(300) });
105 foldGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new(34) });
104 106 foldGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new(1, GridUnitType.Star) });
105 107 foldGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
106 108 foldGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
107 109 var titleTextBlock = new TextBlock
108 110 {
109 Margin = new Thickness(5, 2, 5, 2),
111 Margin = new Thickness(11, 3, 11, 3),
110 112 Text = foldBlockDecSpan.Title,
111 113 TextTrimming = TextTrimming.CharacterEllipsis,
112 114 Foreground = new SolidColorBrush(foldBlockDecSpan.Color),
113 115 VerticalAlignment = VerticalAlignment.Center,
114 HorizontalAlignment = HorizontalAlignment.Center
116 HorizontalAlignment = HorizontalAlignment.Left,
117 FontWeight = FontWeights.SemiBold
115 118 };
116 119 var titleBorder = new Border
117 120 {
@@ -120,22 +123,46 @@ public partial class DecoratedTextConverter
120 123 Child = titleTextBlock
121 124 };
122 125 foldGrid.Children.Add(titleBorder);
123 var buttonText = new TextBlock
126 var chevronRotation = new RotateTransform();
127 var chevron = new Path
124 128 {
125 Text = "▼",
126 FontSize = 18
129 Data = Geometry.Parse("M 1,3 L 6,8 L 11,3"),
130 Stroke = new SolidColorBrush(foldBlockDecSpan.Color),
131 StrokeThickness = 1.8,
132 StrokeStartLineCap = PenLineCap.Round,
133 StrokeEndLineCap = PenLineCap.Round,
134 StrokeLineJoin = PenLineJoin.Round,
135 Width = 12,
136 Height = 10,
137 Stretch = Stretch.None,
138 RenderTransformOrigin = new Point(0.5, 0.5),
139 RenderTransform = chevronRotation
127 140 };
128 141 var button = new Button
129 142 {
130 MinWidth = 32,
143 Style = CreateFoldButtonStyle(),
144 Width = 34,
145 MinWidth = 0,
131 146 Height = 30,
132 147 Padding = new Thickness(0),
133 Content = buttonText
148 Margin = new Thickness(0),
149 Background = Brushes.Transparent,
150 BorderBrush = new SolidColorBrush(Color.FromArgb(
151 54,
152 foldBlockDecSpan.Color.R,
153 foldBlockDecSpan.Color.G,
154 foldBlockDecSpan.Color.B)),
155 BorderThickness = new Thickness(1, 0, 0, 0),
156 Cursor = Cursors.Hand,
157 FocusVisualStyle = null,
158 ToolTip = "展开详情",
159 Content = chevron
134 160 };
135 161 var foldButtonBorder = new Border
136 162 {
137 Background = new SolidColorBrush(foldBlockDecSpan.Color),
163 Background = new SolidColorBrush(foldBlockDecSpan.BackgroundColor),
138 164 CornerRadius = new CornerRadius(0, 5, 5, 0),
165 ClipToBounds = true,
139 166 Child = button
140 167 };
141 168 Grid.SetColumn(foldButtonBorder, 1);
@@ -169,19 +196,21 @@ public partial class DecoratedTextConverter
169 196 {
170 197 contentBorder.Visibility = Visibility.Collapsed;
171 198 titleBorder.CornerRadius = new(5, 0, 0, 5);
172 buttonText.Text = "▼";
199 foldButtonBorder.CornerRadius = new(0, 5, 5, 0);
200 chevronRotation.Angle = 0;
201 button.ToolTip = "展开详情";
173 202 }
174 203 else
175 204 {
176 205 contentBorder.Visibility = Visibility.Visible;
177 206 titleBorder.CornerRadius = new(5, 0, 0, 0);
178 buttonText.Text = "▲";
207 foldButtonBorder.CornerRadius = new(0, 5, 0, 0);
208 chevronRotation.Angle = 180;
209 button.ToolTip = "折叠详情";
179 210 }
180 211 };
181 212 Grid.SetColumnSpan(contentBorder, 3);
182 213 Grid.SetRow(contentBorder, 1);
183 Grid.SetRow(contentBorder, 1);
184 Grid.SetColumnSpan(contentBorder, 3);
185 214 foldGrid.Children.Add(contentBorder);
186 215 inLineList.Add(new Span(new Run("\n")));
187 216 inLineList.Add(foldGrid);
@@ -205,6 +234,41 @@ public partial class DecoratedTextConverter
205 234 }
206 235 return inLineList;
207 236 }
237
238 private static Style CreateFoldButtonStyle()
239 {
240 var surface = new FrameworkElementFactory(typeof(Border), "Surface");
241 surface.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty));
242 surface.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Control.BorderBrushProperty));
243 surface.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Control.BorderThicknessProperty));
244
245 var content = new FrameworkElementFactory(typeof(ContentPresenter));
246 content.SetValue(ContentPresenter.ContentProperty, new TemplateBindingExtension(ContentControl.ContentProperty));
247 content.SetValue(ContentPresenter.ContentTemplateProperty, new TemplateBindingExtension(ContentControl.ContentTemplateProperty));
248 content.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center);
249 content.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
250 surface.AppendChild(content);
251
252 var template = new ControlTemplate(typeof(Button)) { VisualTree = surface };
253 var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
254 hoverTrigger.Setters.Add(new Setter(
255 Border.BackgroundProperty,
256 new SolidColorBrush(Color.FromArgb(38, 255, 255, 255)),
257 "Surface"));
258 template.Triggers.Add(hoverTrigger);
259 var pressedTrigger = new Trigger { Property = Button.IsPressedProperty, Value = true };
260 pressedTrigger.Setters.Add(new Setter(
261 Border.BackgroundProperty,
262 new SolidColorBrush(Color.FromArgb(68, 255, 255, 255)),
263 "Surface"));
264 template.Triggers.Add(pressedTrigger);
265
266 var style = new Style(typeof(Button));
267 style.Setters.Add(new Setter(Control.TemplateProperty, template));
268 style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Center));
269 style.Setters.Add(new Setter(Control.VerticalContentAlignmentProperty, VerticalAlignment.Center));
270 return style;
271 }
208 272 /// <summary>
209 273 /// 转为行内组件列表
210 274 /// </summary>
Modified XFEToolBox/Utilities/ToolProjectRunService.cs +32 -3
@@ -126,7 +126,11 @@ internal static class ToolProjectRunService
126 126 var projectPath = Path.Combine(runtimeRoot, "ToolRuntime.csproj");
127 127 var entryPath = Path.Combine(runtimeRoot, "RuntimeEntry.g.cs");
128 128 var toolIconPath = ResolveToolIconPath(preparedWorkspaceRoot, manifest.Icon);
129 await File.WriteAllTextAsync(projectPath, CreateProjectFile(preparedWorkspaceRoot, assemblyName, hostAssemblyName), new UTF8Encoding(false), cancellationToken);
129 await File.WriteAllTextAsync(
130 projectPath,
131 CreateProjectFile(preparedWorkspaceRoot, assemblyName, hostAssemblyName, manifest.NuGetPackages),
132 new UTF8Encoding(false),
133 cancellationToken);
130 134 await File.WriteAllTextAsync(
131 135 entryPath,
132 136 CreateRuntimeEntry(manifest, hostAssemblyName, windowTitle, toolIconPath, requiresElevatedProcess),
@@ -252,7 +256,11 @@ internal static class ToolProjectRunService
252 256 }
253 257 }
254 258
255 private static string CreateProjectFile(string workspaceRoot, string assemblyName, string hostAssemblyName)
259 internal static string CreateProjectFile(
260 string workspaceRoot,
261 string assemblyName,
262 string hostAssemblyName,
263 IReadOnlyCollection<ToolNuGetPackageReference>? nugetPackages)
256 264 {
257 265 var root = EscapeXml(Path.GetFullPath(workspaceRoot).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
258 266 var coreAssembly = EscapeXml(typeof(ToolPackageManifest).Assembly.Location);
@@ -263,6 +271,27 @@ internal static class ToolProjectRunService
263 271 var xfeExtensionAssembly = EscapeXml(Path.Combine(
264 272 Path.GetDirectoryName(clientAssemblyPath)!,
265 273 "XFEExtension.NetCore.dll"));
274 var packageReferences = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
275 {
276 ["CommunityToolkit.Mvvm"] = "8.4.2"
277 };
278 if (nugetPackages?.Count > ToolNuGetPackageRules.MaximumPackageCount)
279 throw new InvalidDataException($"NuGet 包最多允许 {ToolNuGetPackageRules.MaximumPackageCount} 个。");
280 var customPackageIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
281 foreach (var package in nugetPackages ?? [])
282 {
283 if (!ToolNuGetPackageRules.IsValidPackageId(package.Id)
284 || !ToolNuGetPackageRules.IsValidExactVersion(package.Version))
285 throw new InvalidDataException($"NuGet 包引用不合法:{package.Id} {package.Version}。");
286 if (!customPackageIds.Add(package.Id))
287 throw new InvalidDataException($"NuGet 包不能重复:{package.Id}。");
288 packageReferences[package.Id.Trim()] = package.Version.Trim();
289 }
290 var packageReferenceXml = string.Join(
291 Environment.NewLine,
292 packageReferences
293 .OrderBy(package => package.Key, StringComparer.OrdinalIgnoreCase)
294 .Select(package => $" <PackageReference Include=\"{EscapeXml(package.Key)}\" Version=\"{EscapeXml(package.Value)}\" />"));
266 295 return $$"""
267 296 <Project Sdk="Microsoft.NET.Sdk">
268 297 <PropertyGroup>
@@ -288,7 +317,7 @@ internal static class ToolProjectRunService
288 317 Exclude="{{root}}\bin\**;{{root}}\obj\**" Link="Source\%(RecursiveDir)%(Filename)%(Extension)" />
289 318 </ItemGroup>
290 319 <ItemGroup>
291 <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
320 {{packageReferenceXml}}
292 321 <Reference Include="XFEToolBox.Core"><HintPath>{{coreAssembly}}</HintPath><Private>true</Private></Reference>
293 322 <Reference Include="XFEToolBox.Client.Core"><HintPath>{{clientCoreAssembly}}</HintPath><Private>true</Private></Reference>
294 323 <Reference Include="XFEToolBox.WpfCore"><HintPath>{{wpfCoreAssembly}}</HintPath><Private>true</Private></Reference>
Modified XFEToolBox/Utilities/ToolProjectWorkspaceService.cs +1 -0
@@ -67,6 +67,7 @@ internal static class ToolProjectWorkspaceService
67 67 "icon": "{{DefaultToolIconRelativePath}}",
68 68 "category": "开发工具",
69 69 "tags": [ "WPF" ],
70 "nugetPackages": [],
70 71 "requiresAdministrator": false,
71 72 "entry": {
72 73 "viewXaml": "Code/Views/MainPage.xaml",
Modified XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml +52 -0
@@ -695,6 +695,58 @@
695 695 </StackPanel>
696 696 </Border>
697 697
698 <Border Margin="0,12,0,0" Padding="16" Background="White" BorderBrush="#E4E3EF" BorderThickness="1" CornerRadius="14">
699 <StackPanel>
700 <TextBlock Text="NuGet 包" Foreground="#45455A" FontSize="12.5" FontWeight="SemiBold"/>
701 <TextBlock Text="为当前工具独立添加包引用。保存、编译和发布都会使用精确版本;请只引用可信包。" Foreground="#9696A8" FontSize="9.3" Margin="0,4,0,14"/>
702 <Grid>
703 <Grid.ColumnDefinitions>
704 <ColumnDefinition Width="*"/>
705 <ColumnDefinition Width="14"/>
706 <ColumnDefinition Width="180"/>
707 <ColumnDefinition Width="10"/>
708 <ColumnDefinition Width="Auto"/>
709 </Grid.ColumnDefinitions>
710 <StackPanel>
711 <TextBlock Text="包 ID" Style="{StaticResource ManifestFieldLabel}"/>
712 <controls:TextEditor x:Name="ManifestNuGetPackageIdBox" Style="{StaticResource ManifestTextEditor}" HintText="例如 XFEExtension.NetCore.XFEConsole"/>
713 </StackPanel>
714 <StackPanel Grid.Column="2">
715 <TextBlock Text="精确版本" Style="{StaticResource ManifestFieldLabel}"/>
716 <controls:TextEditor x:Name="ManifestNuGetPackageVersionBox" Style="{StaticResource ManifestTextEditor}" HintText="例如 2.6.0"/>
717 </StackPanel>
718 <Button Grid.Column="4" Content="添加 / 更新" MinWidth="92" Height="38" Margin="0,19,0,0" Padding="13,0"
719 Click="AddManifestNuGetPackageButton_Click"/>
720 </Grid>
721 <TextBlock x:Name="ManifestNuGetPackageValidationText" Text="使用精确版本可确保本地编译与发布后的工具保持一致。"
722 Foreground="#858598" FontSize="9" Margin="2,8,0,0" TextWrapping="Wrap"/>
723 <ItemsControl x:Name="ManifestNuGetPackagesItems" Margin="0,11,0,0">
724 <ItemsControl.ItemTemplate>
725 <DataTemplate>
726 <Border Margin="0,0,0,7" Padding="12,9" Background="#F7F7FC" BorderBrush="#E0DFF0" BorderThickness="1" CornerRadius="10">
727 <Grid>
728 <Grid.ColumnDefinitions>
729 <ColumnDefinition Width="*"/>
730 <ColumnDefinition Width="Auto"/>
731 <ColumnDefinition Width="Auto"/>
732 </Grid.ColumnDefinitions>
733 <StackPanel>
734 <TextBlock Text="{Binding Id}" Foreground="#4C4C63" FontSize="10.5" FontWeight="SemiBold"/>
735 <TextBlock Text="项目独立引用" Foreground="#9999AA" FontSize="8.5" Margin="0,2,0,0"/>
736 </StackPanel>
737 <Border Grid.Column="1" Padding="9,4" Margin="12,0" Background="#ECECFA" CornerRadius="8" VerticalAlignment="Center">
738 <TextBlock Text="{Binding Version}" Foreground="#6868A6" FontSize="9.5" FontFamily="Consolas"/>
739 </Border>
740 <Button Grid.Column="2" Content="移除" Tag="{Binding}" MinWidth="58" Height="30" Padding="9,0"
741 Click="RemoveManifestNuGetPackageButton_Click"/>
742 </Grid>
743 </Border>
744 </DataTemplate>
745 </ItemsControl.ItemTemplate>
746 </ItemsControl>
747 </StackPanel>
748 </Border>
749
698 750 <Border Margin="0,12,0,0" Padding="16" Background="White" BorderBrush="#E4E3EF" BorderThickness="1" CornerRadius="14">
699 751 <StackPanel>
700 752 <TextBlock Text="权限与格式" Foreground="#45455A" FontSize="12.5" FontWeight="SemiBold"/>
Modified XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml.cs +74 -0
@@ -89,6 +89,7 @@ public partial class ToolCodeEditorWindow : Window
89 89 private readonly ObservableCollection<EditorExplorerItem> _files = [];
90 90 private readonly ObservableCollection<EditorExplorerItem> _explorerItems = [];
91 91 private readonly ObservableCollection<string> _manifestTags = [];
92 private readonly ObservableCollection<ToolNuGetPackageReference> _manifestNuGetPackages = [];
92 93 private List<string> _explorerOrder = [];
93 94 private ICollectionView? _fileView;
94 95 private string _workspaceRoot = string.Empty;
@@ -131,6 +132,7 @@ public partial class ToolCodeEditorWindow : Window
131 132 _fileView.Filter = FileMatchesFilter;
132 133 FileList.ItemsSource = _fileView;
133 134 ManifestTagsItems.ItemsSource = _manifestTags;
135 ManifestNuGetPackagesItems.ItemsSource = _manifestNuGetPackages;
134 136 SaveButton.IsEnabled = false;
135 137 EditorThemeButton.IsEnabled = false;
136 138 PreviewButton.IsEnabled = false;
@@ -1488,6 +1490,9 @@ public partial class ToolCodeEditorWindow : Window
1488 1490 ManifestIconBox.Text = manifest.Icon ?? string.Empty;
1489 1491 ManifestCategoryBox.Text = manifest.Category;
1490 1492 ReplaceManifestTags(manifest.Tags ?? []);
1493 ReplaceManifestNuGetPackages(manifest.NuGetPackages ?? []);
1494 ManifestNuGetPackageIdBox.Text = string.Empty;
1495 ManifestNuGetPackageVersionBox.Text = string.Empty;
1491 1496 ManifestMinimumHostVersionBox.Text = manifest.MinimumHostVersion ?? string.Empty;
1492 1497 ManifestReleaseNotesBox.Text = manifest.ReleaseNotes ?? string.Empty;
1493 1498 ManifestRequiresAdministratorCheckBox.IsChecked = manifest.RequiresAdministrator;
@@ -1571,6 +1576,7 @@ public partial class ToolCodeEditorWindow : Window
1571 1576 Icon = NullIfWhiteSpace(ManifestIconBox.Text),
1572 1577 Category = ManifestCategoryBox.Text.Trim(),
1573 1578 Tags = _manifestTags.ToArray(),
1579 NuGetPackages = _manifestNuGetPackages.ToArray(),
1574 1580 MinimumHostVersion = NullIfWhiteSpace(ManifestMinimumHostVersionBox.Text),
1575 1581 ReleaseNotes = NullIfWhiteSpace(ManifestReleaseNotesBox.Text),
1576 1582 RequiresAdministrator = ManifestRequiresAdministratorCheckBox.IsChecked == true,
@@ -1751,6 +1757,74 @@ public partial class ToolCodeEditorWindow : Window
1751 1757 return null;
1752 1758 }
1753 1759
1760 private void ReplaceManifestNuGetPackages(IEnumerable<ToolNuGetPackageReference> packages)
1761 {
1762 _manifestNuGetPackages.Clear();
1763 foreach (var package in packages.Where(package => package is not null))
1764 {
1765 _manifestNuGetPackages.Add(new ToolNuGetPackageReference
1766 {
1767 Id = package.Id?.Trim() ?? string.Empty,
1768 Version = package.Version?.Trim() ?? string.Empty
1769 });
1770 }
1771 }
1772
1773 private void AddManifestNuGetPackageButton_Click(object sender, RoutedEventArgs e)
1774 {
1775 var packageId = ManifestNuGetPackageIdBox.Text.Trim();
1776 var packageVersion = ManifestNuGetPackageVersionBox.Text.Trim();
1777 if (!ToolNuGetPackageRules.IsValidPackageId(packageId))
1778 {
1779 ManifestNuGetPackageValidationText.Text = $"包 ID 只能包含字母、数字、点、短横线和下划线,且不超过 {ToolNuGetPackageRules.MaximumPackageIdLength} 个字符。";
1780 return;
1781 }
1782 if (!ToolNuGetPackageRules.IsValidExactVersion(packageVersion))
1783 {
1784 ManifestNuGetPackageValidationText.Text = "请输入精确版本,例如 2.6.0 或 2.6.0-beta.1;不支持 * 和版本范围。";
1785 return;
1786 }
1787
1788 var existingIndex = -1;
1789 for (var index = 0; index < _manifestNuGetPackages.Count; index++)
1790 {
1791 if (!_manifestNuGetPackages[index].Id.Equals(packageId, StringComparison.OrdinalIgnoreCase))
1792 continue;
1793 existingIndex = index;
1794 break;
1795 }
1796
1797 var package = new ToolNuGetPackageReference { Id = packageId, Version = packageVersion };
1798 if (existingIndex >= 0)
1799 {
1800 _manifestNuGetPackages[existingIndex] = package;
1801 ManifestNuGetPackageValidationText.Text = $"已更新 {packageId} 的版本。";
1802 }
1803 else
1804 {
1805 if (_manifestNuGetPackages.Count >= ToolNuGetPackageRules.MaximumPackageCount)
1806 {
1807 ManifestNuGetPackageValidationText.Text = $"每个工具最多添加 {ToolNuGetPackageRules.MaximumPackageCount} 个 NuGet 包。";
1808 return;
1809 }
1810 _manifestNuGetPackages.Add(package);
1811 ManifestNuGetPackageValidationText.Text = $"已添加 {packageId}。";
1812 }
1813
1814 ManifestNuGetPackageIdBox.Text = string.Empty;
1815 ManifestNuGetPackageVersionBox.Text = string.Empty;
1816 MarkManifestDesignerChanged();
1817 }
1818
1819 private void RemoveManifestNuGetPackageButton_Click(object sender, RoutedEventArgs e)
1820 {
1821 if (sender is not Button { Tag: ToolNuGetPackageReference package })
1822 return;
1823 _manifestNuGetPackages.Remove(package);
1824 ManifestNuGetPackageValidationText.Text = $"已移除 {package.Id}。";
1825 MarkManifestDesignerChanged();
1826 }
1827
1754 1828 private void LoadManifestPermissions(IEnumerable<string> permissions)
1755 1829 {
1756 1830 var selected = permissions
Added XfeTestArtifacts/test-results.json +444 -0
@@ -0,0 +1,444 @@
1 {
2 "startedAt": "2026-08-28T20:51:19.0815569+00:00",
3 "duration": "00:00:07.7970135",
4 "results": [
5 {
6 "id": "XFEToolBox.Client.Wpf.Test.ConsoleFoldBlockStyleTests.FoldBlockUsesCompactDedicatedButtonAndTogglesItsState#0",
7 "displayName": "ConsoleFoldBlockStyleTests.FoldBlockUsesCompactDedicatedButtonAndTogglesItsState",
8 "outcome": 0,
9 "bodyDuration": "00:00:00.2909623",
10 "totalDuration": "00:00:00.2917335",
11 "attempts": 1,
12 "message": null,
13 "stackTrace": null,
14 "output": "",
15 "isLegacySingleRun": false,
16 "typeName": "XFEToolBox.Client.Wpf.Test.ConsoleFoldBlockStyleTests",
17 "methodName": "FoldBlockUsesCompactDedicatedButtonAndTogglesItsState"
18 },
19 {
20 "id": "XFEToolBox.Client.Wpf.Test.InstallerTests.InstallerRejectsNestedAndTraversingPackages#0",
21 "displayName": "InstallerTests.InstallerRejectsNestedAndTraversingPackages",
22 "outcome": 0,
23 "bodyDuration": "00:00:00.0168395",
24 "totalDuration": "00:00:00.0170013",
25 "attempts": 1,
26 "message": null,
27 "stackTrace": null,
28 "output": "",
29 "isLegacySingleRun": false,
30 "typeName": "XFEToolBox.Client.Wpf.Test.InstallerTests",
31 "methodName": "InstallerRejectsNestedAndTraversingPackages"
32 },
33 {
34 "id": "XFEToolBox.Client.Wpf.Test.InstallerTests.InstallerRestoresExistingFilesWhenAnOverwriteFails#0",
35 "displayName": "InstallerTests.InstallerRestoresExistingFilesWhenAnOverwriteFails",
36 "outcome": 0,
37 "bodyDuration": "00:00:02.8856297",
38 "totalDuration": "00:00:02.8858505",
39 "attempts": 1,
40 "message": null,
41 "stackTrace": null,
42 "output": "",
43 "isLegacySingleRun": false,
44 "typeName": "XFEToolBox.Client.Wpf.Test.InstallerTests",
45 "methodName": "InstallerRestoresExistingFilesWhenAnOverwriteFails"
46 },
47 {
48 "id": "XFEToolBox.Client.Wpf.Test.InstallerTests.InstallerStagesAndAppliesAValidPackage#0",
49 "displayName": "InstallerTests.InstallerStagesAndAppliesAValidPackage",
50 "outcome": 0,
51 "bodyDuration": "00:00:00.0725230",
52 "totalDuration": "00:00:00.0725958",
53 "attempts": 1,
54 "message": null,
55 "stackTrace": null,
56 "output": "",
57 "isLegacySingleRun": false,
58 "typeName": "XFEToolBox.Client.Wpf.Test.InstallerTests",
59 "methodName": "InstallerStagesAndAppliesAValidPackage"
60 },
61 {
62 "id": "XFEToolBox.Client.Wpf.Test.Program.ActivityCenterTracksProgressAndCancellation#0",
63 "displayName": "Program.ActivityCenterTracksProgressAndCancellation",
64 "outcome": 0,
65 "bodyDuration": "00:00:00.0039646",
66 "totalDuration": "00:00:00.0040477",
67 "attempts": 1,
68 "message": null,
69 "stackTrace": null,
70 "output": "",
71 "isLegacySingleRun": false,
72 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
73 "methodName": "ActivityCenterTracksProgressAndCancellation"
74 },
75 {
76 "id": "XFEToolBox.Client.Wpf.Test.Program.BufferedConsoleRendererKeepsUiTreeBoundedUnderLoad#0",
77 "displayName": "Program.BufferedConsoleRendererKeepsUiTreeBoundedUnderLoad#0",
78 "outcome": 0,
79 "bodyDuration": "00:00:00.5758979",
80 "totalDuration": "00:00:00.5761352",
81 "attempts": 1,
82 "message": null,
83 "stackTrace": null,
84 "output": "WPF \u63A7\u5236\u53F0\u7AEF\u5230\u7AEF\u541E\u5410\uFF1A115,705 \u6761/\u79D2\uFF1BUI \u6700\u5927\u8C03\u5EA6\u5EF6\u8FDF 20.5 ms\uFF1B\u4FDD\u7559 7,968 \u884C\uFF0C\u4EC5 63 \u4E2A\u6587\u672C\u5757\u3002\r\n",
85 "isLegacySingleRun": true,
86 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
87 "methodName": "BufferedConsoleRendererKeepsUiTreeBoundedUnderLoad"
88 },
89 {
90 "id": "XFEToolBox.Client.Wpf.Test.Program.FramelessMaximizedWindowStaysInsideMonitorWorkArea#0",
91 "displayName": "Program.FramelessMaximizedWindowStaysInsideMonitorWorkArea",
92 "outcome": 0,
93 "bodyDuration": "00:00:00.4037253",
94 "totalDuration": "00:00:00.4039765",
95 "attempts": 1,
96 "message": null,
97 "stackTrace": null,
98 "output": "",
99 "isLegacySingleRun": false,
100 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
101 "methodName": "FramelessMaximizedWindowStaysInsideMonitorWorkArea"
102 },
103 {
104 "id": "XFEToolBox.Client.Wpf.Test.Program.LauncherHotkeyParserNormalizesAndRejectsUnsafeGestures#0",
105 "displayName": "Program.LauncherHotkeyParserNormalizesAndRejectsUnsafeGestures",
106 "outcome": 0,
107 "bodyDuration": "00:00:00.0010884",
108 "totalDuration": "00:00:00.0012193",
109 "attempts": 1,
110 "message": null,
111 "stackTrace": null,
112 "output": "",
113 "isLegacySingleRun": false,
114 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
115 "methodName": "LauncherHotkeyParserNormalizesAndRejectsUnsafeGestures"
116 },
117 {
118 "id": "XFEToolBox.Client.Wpf.Test.Program.LauncherRankingUsesStableMatchOrderAndPersonalizationBoosts#0",
119 "displayName": "Program.LauncherRankingUsesStableMatchOrderAndPersonalizationBoosts",
120 "outcome": 0,
121 "bodyDuration": "00:00:00.0628954",
122 "totalDuration": "00:00:00.0630476",
123 "attempts": 1,
124 "message": null,
125 "stackTrace": null,
126 "output": "",
127 "isLegacySingleRun": false,
128 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
129 "methodName": "LauncherRankingUsesStableMatchOrderAndPersonalizationBoosts"
130 },
131 {
132 "id": "XFEToolBox.Client.Wpf.Test.Program.PinnedAndRecentConfigurationRecoverFromDuplicatesAndDamage#0",
133 "displayName": "Program.PinnedAndRecentConfigurationRecoverFromDuplicatesAndDamage",
134 "outcome": 0,
135 "bodyDuration": "00:00:00.0456199",
136 "totalDuration": "00:00:00.0459442",
137 "attempts": 1,
138 "message": null,
139 "stackTrace": null,
140 "output": "",
141 "isLegacySingleRun": false,
142 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
143 "methodName": "PinnedAndRecentConfigurationRecoverFromDuplicatesAndDamage"
144 },
145 {
146 "id": "XFEToolBox.Client.Wpf.Test.Program.QuickAccessToolCardLoadsItsCatalogIcon#0",
147 "displayName": "Program.QuickAccessToolCardLoadsItsCatalogIcon",
148 "outcome": 0,
149 "bodyDuration": "00:00:00.0933325",
150 "totalDuration": "00:00:00.0934134",
151 "attempts": 1,
152 "message": null,
153 "stackTrace": null,
154 "output": "",
155 "isLegacySingleRun": false,
156 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
157 "methodName": "QuickAccessToolCardLoadsItsCatalogIcon"
158 },
159 {
160 "id": "XFEToolBox.Client.Wpf.Test.Program.SingleInstanceForwardsCommandsThroughItsNamedPipe#0",
161 "displayName": "Program.SingleInstanceForwardsCommandsThroughItsNamedPipe",
162 "outcome": 0,
163 "bodyDuration": "00:00:00.0100075",
164 "totalDuration": "00:00:00.0100862",
165 "attempts": 1,
166 "message": null,
167 "stackTrace": null,
168 "output": "",
169 "isLegacySingleRun": false,
170 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
171 "methodName": "SingleInstanceForwardsCommandsThroughItsNamedPipe"
172 },
173 {
174 "id": "XFEToolBox.Client.Wpf.Test.Program.TabAndNavigationOutlinesStayInsideTheirLayoutBounds#0",
175 "displayName": "Program.TabAndNavigationOutlinesStayInsideTheirLayoutBounds",
176 "outcome": 0,
177 "bodyDuration": "00:00:00.4342620",
178 "totalDuration": "00:00:00.4343900",
179 "attempts": 1,
180 "message": null,
181 "stackTrace": null,
182 "output": "",
183 "isLegacySingleRun": false,
184 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
185 "methodName": "TabAndNavigationOutlinesStayInsideTheirLayoutBounds"
186 },
187 {
188 "id": "XFEToolBox.Client.Wpf.Test.Program.TimePickerIncrementOneKeepsTheWholeScrollTrackUsable#0",
189 "displayName": "Program.TimePickerIncrementOneKeepsTheWholeScrollTrackUsable",
190 "outcome": 0,
191 "bodyDuration": "00:00:00.9141555",
192 "totalDuration": "00:00:00.9142123",
193 "attempts": 1,
194 "message": null,
195 "stackTrace": null,
196 "output": "",
197 "isLegacySingleRun": false,
198 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
199 "methodName": "TimePickerIncrementOneKeepsTheWholeScrollTrackUsable"
200 },
201 {
202 "id": "XFEToolBox.Client.Wpf.Test.Program.WorkshopProjectCardLoadsItsManifestPreviewIcon#0",
203 "displayName": "Program.WorkshopProjectCardLoadsItsManifestPreviewIcon",
204 "outcome": 0,
205 "bodyDuration": "00:00:00.0294264",
206 "totalDuration": "00:00:00.0295885",
207 "attempts": 1,
208 "message": null,
209 "stackTrace": null,
210 "output": "",
211 "isLegacySingleRun": false,
212 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
213 "methodName": "WorkshopProjectCardLoadsItsManifestPreviewIcon"
214 },
215 {
216 "id": "XFEToolBox.Client.Wpf.Test.Program.XamlCodeViewerRendersDistinctSyntaxTokens#0",
217 "displayName": "Program.XamlCodeViewerRendersDistinctSyntaxTokens",
218 "outcome": 0,
219 "bodyDuration": "00:00:00.0413805",
220 "totalDuration": "00:00:00.0415977",
221 "attempts": 1,
222 "message": null,
223 "stackTrace": null,
224 "output": "",
225 "isLegacySingleRun": false,
226 "typeName": "XFEToolBox.Client.Wpf.Test.Program",
227 "methodName": "XamlCodeViewerRendersDistinctSyntaxTokens"
228 },
229 {
230 "id": "XFEToolBox.Client.Wpf.Test.RecentUsageIconCacheTests.RecentUsageIconsAreFrozenReusedAndBounded#0",
231 "displayName": "RecentUsageIconCacheTests.RecentUsageIconsAreFrozenReusedAndBounded",
232 "outcome": 0,
233 "bodyDuration": "00:00:00.0051891",
234 "totalDuration": "00:00:00.0053736",
235 "attempts": 1,
236 "message": null,
237 "stackTrace": null,
238 "output": "",
239 "isLegacySingleRun": false,
240 "typeName": "XFEToolBox.Client.Wpf.Test.RecentUsageIconCacheTests",
241 "methodName": "RecentUsageIconsAreFrozenReusedAndBounded"
242 },
243 {
244 "id": "XFEToolBox.Client.Wpf.Test.SettingsControlsTests.RemoteInputsAreOnlyVisibleWhileRemoteModeIsEnabled#0",
245 "displayName": "SettingsControlsTests.RemoteInputsAreOnlyVisibleWhileRemoteModeIsEnabled",
246 "outcome": 0,
247 "bodyDuration": "00:00:00.4803482",
248 "totalDuration": "00:00:00.4804578",
249 "attempts": 1,
250 "message": null,
251 "stackTrace": null,
252 "output": "",
253 "isLegacySingleRun": false,
254 "typeName": "XFEToolBox.Client.Wpf.Test.SettingsControlsTests",
255 "methodName": "RemoteInputsAreOnlyVisibleWhileRemoteModeIsEnabled"
256 },
257 {
258 "id": "XFEToolBox.Client.Wpf.Test.ToolNuGetPackageTests.ManifestRoundTripsProjectNuGetPackages#0",
259 "displayName": "ToolNuGetPackageTests.ManifestRoundTripsProjectNuGetPackages",
260 "outcome": 0,
261 "bodyDuration": "00:00:00.0028672",
262 "totalDuration": "00:00:00.0030161",
263 "attempts": 1,
264 "message": null,
265 "stackTrace": null,
266 "output": "",
267 "isLegacySingleRun": false,
268 "typeName": "XFEToolBox.Client.Wpf.Test.ToolNuGetPackageTests",
269 "methodName": "ManifestRoundTripsProjectNuGetPackages"
270 },
271 {
272 "id": "XFEToolBox.Client.Wpf.Test.ToolNuGetPackageTests.PackageRulesRequireSafeIdsAndPinnedVersions#0",
273 "displayName": "ToolNuGetPackageTests.PackageRulesRequireSafeIdsAndPinnedVersions",
274 "outcome": 0,
275 "bodyDuration": "00:00:00.0018497",
276 "totalDuration": "00:00:00.0019044",
277 "attempts": 1,
278 "message": null,
279 "stackTrace": null,
280 "output": "",
281 "isLegacySingleRun": false,
282 "typeName": "XFEToolBox.Client.Wpf.Test.ToolNuGetPackageTests",
283 "methodName": "PackageRulesRequireSafeIdsAndPinnedVersions"
284 },
285 {
286 "id": "XFEToolBox.Client.Wpf.Test.ToolNuGetPackageTests.RuntimeProjectIncludesEachToolPackageAndAllowsToolkitOverride#0",
287 "displayName": "ToolNuGetPackageTests.RuntimeProjectIncludesEachToolPackageAndAllowsToolkitOverride",
288 "outcome": 0,
289 "bodyDuration": "00:00:00.0083083",
290 "totalDuration": "00:00:00.0084044",
291 "attempts": 1,
292 "message": null,
293 "stackTrace": null,
294 "output": "",
295 "isLegacySingleRun": false,
296 "typeName": "XFEToolBox.Client.Wpf.Test.ToolNuGetPackageTests",
297 "methodName": "RuntimeProjectIncludesEachToolPackageAndAllowsToolkitOverride"
298 },
299 {
300 "id": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests.AdministratorLaunchUsesShellRunAsAndTheCompiledAppHost#0",
301 "displayName": "ToolRuntimeProcessStartInfoFactoryTests.AdministratorLaunchUsesShellRunAsAndTheCompiledAppHost",
302 "outcome": 0,
303 "bodyDuration": "00:00:00.0002925",
304 "totalDuration": "00:00:00.0003602",
305 "attempts": 1,
306 "message": null,
307 "stackTrace": null,
308 "output": "",
309 "isLegacySingleRun": false,
310 "typeName": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests",
311 "methodName": "AdministratorLaunchUsesShellRunAsAndTheCompiledAppHost"
312 },
313 {
314 "id": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests.AdministratorPreferenceSurvivesSerializationAndIsCaseInsensitive#0",
315 "displayName": "ToolRuntimeProcessStartInfoFactoryTests.AdministratorPreferenceSurvivesSerializationAndIsCaseInsensitive",
316 "outcome": 0,
317 "bodyDuration": "00:00:00.0050723",
318 "totalDuration": "00:00:00.0051211",
319 "attempts": 1,
320 "message": null,
321 "stackTrace": null,
322 "output": "",
323 "isLegacySingleRun": false,
324 "typeName": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests",
325 "methodName": "AdministratorPreferenceSurvivesSerializationAndIsCaseInsensitive"
326 },
327 {
328 "id": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests.StandardLaunchKeepsStartupDiagnosticsEnabled#0",
329 "displayName": "ToolRuntimeProcessStartInfoFactoryTests.StandardLaunchKeepsStartupDiagnosticsEnabled",
330 "outcome": 0,
331 "bodyDuration": "00:00:00.0000878",
332 "totalDuration": "00:00:00.0001338",
333 "attempts": 1,
334 "message": null,
335 "stackTrace": null,
336 "output": "",
337 "isLegacySingleRun": false,
338 "typeName": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests",
339 "methodName": "StandardLaunchKeepsStartupDiagnosticsEnabled"
340 },
341 {
342 "id": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests.UserAndManifestAdministratorModesAreBothEffective#0",
343 "displayName": "ToolRuntimeProcessStartInfoFactoryTests.UserAndManifestAdministratorModesAreBothEffective",
344 "outcome": 0,
345 "bodyDuration": "00:00:00.0001573",
346 "totalDuration": "00:00:00.0002414",
347 "attempts": 1,
348 "message": null,
349 "stackTrace": null,
350 "output": "",
351 "isLegacySingleRun": false,
352 "typeName": "XFEToolBox.Client.Wpf.Test.ToolRuntimeProcessStartInfoFactoryTests",
353 "methodName": "UserAndManifestAdministratorModesAreBothEffective"
354 },
355 {
356 "id": "XFEToolBox.Client.Wpf.Test.UnifiedControlDefaultsTests.DataGridGeneratedColumnsReceiveUnifiedStylesWithoutOverwritingExplicitStyles#0",
357 "displayName": "UnifiedControlDefaultsTests.DataGridGeneratedColumnsReceiveUnifiedStylesWithoutOverwritingExplicitStyles",
358 "outcome": 0,
359 "bodyDuration": "00:00:00.0062280",
360 "totalDuration": "00:00:00.0062767",
361 "attempts": 1,
362 "message": null,
363 "stackTrace": null,
364 "output": "",
365 "isLegacySingleRun": false,
366 "typeName": "XFEToolBox.Client.Wpf.Test.UnifiedControlDefaultsTests",
367 "methodName": "DataGridGeneratedColumnsReceiveUnifiedStylesWithoutOverwritingExplicitStyles"
368 },
369 {
370 "id": "XFEToolBox.Client.Wpf.Test.UnifiedControlDefaultsTests.ProgressRingDefaultsToZeroAndInactive#0",
371 "displayName": "UnifiedControlDefaultsTests.ProgressRingDefaultsToZeroAndInactive",
372 "outcome": 0,
373 "bodyDuration": "00:00:00.0189634",
374 "totalDuration": "00:00:00.0191593",
375 "attempts": 1,
376 "message": null,
377 "stackTrace": null,
378 "output": "",
379 "isLegacySingleRun": false,
380 "typeName": "XFEToolBox.Client.Wpf.Test.UnifiedControlDefaultsTests",
381 "methodName": "ProgressRingDefaultsToZeroAndInactive"
382 },
383 {
384 "id": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests.BackgroundDecodedPngCanBeDisplayedByAnimatedImageBehavior#0",
385 "displayName": "WebImageSourceLoaderTests.BackgroundDecodedPngCanBeDisplayedByAnimatedImageBehavior",
386 "outcome": 0,
387 "bodyDuration": "00:00:00.3904849",
388 "totalDuration": "00:00:00.3905703",
389 "attempts": 1,
390 "message": null,
391 "stackTrace": null,
392 "output": "",
393 "isLegacySingleRun": false,
394 "typeName": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests",
395 "methodName": "BackgroundDecodedPngCanBeDisplayedByAnimatedImageBehavior"
396 },
397 {
398 "id": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests.LargeSvgLoadingStaysOffTheUiThreadAndSharesItsCachedResult#0",
399 "displayName": "WebImageSourceLoaderTests.LargeSvgLoadingStaysOffTheUiThreadAndSharesItsCachedResult",
400 "outcome": 0,
401 "bodyDuration": "00:00:00.9502277",
402 "totalDuration": "00:00:00.9504780",
403 "attempts": 1,
404 "message": null,
405 "stackTrace": null,
406 "output": "",
407 "isLegacySingleRun": false,
408 "typeName": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests",
409 "methodName": "LargeSvgLoadingStaysOffTheUiThreadAndSharesItsCachedResult"
410 },
411 {
412 "id": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests.WebImageLoaderDecodesSvgIcoAndSvgDataUris#0",
413 "displayName": "WebImageSourceLoaderTests.WebImageLoaderDecodesSvgIcoAndSvgDataUris",
414 "outcome": 0,
415 "bodyDuration": "00:00:00.0303629",
416 "totalDuration": "00:00:00.0305853",
417 "attempts": 1,
418 "message": null,
419 "stackTrace": null,
420 "output": "",
421 "isLegacySingleRun": false,
422 "typeName": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests",
423 "methodName": "WebImageLoaderDecodesSvgIcoAndSvgDataUris"
424 },
425 {
426 "id": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests.WebImageLoaderRejectsOversizedAndNonImageContent#0",
427 "displayName": "WebImageSourceLoaderTests.WebImageLoaderRejectsOversizedAndNonImageContent",
428 "outcome": 0,
429 "bodyDuration": "00:00:00.0008585",
430 "totalDuration": "00:00:00.0009633",
431 "attempts": 1,
432 "message": null,
433 "stackTrace": null,
434 "output": "",
435 "isLegacySingleRun": false,
436 "typeName": "XFEToolBox.Client.Wpf.Test.WebImageSourceLoaderTests",
437 "methodName": "WebImageLoaderRejectsOversizedAndNonImageContent"
438 }
439 ],
440 "total": 31,
441 "passed": 31,
442 "failed": 0,
443 "skipped": 0
444 }
Added XfeTestArtifacts/test-results.xml +36 -0
Modified docs/tool-packages.md +10 -0
@@ -117,6 +117,9 @@ base64-generator.xfetool
117 117 "tags": ["base64", "编码"],
118 118 "minimumHostVersion": "0.2.0",
119 119 "releaseNotes": "首个版本。",
120 "nugetPackages": [
121 { "id": "Example.Package", "version": "1.2.3" }
122 ],
120 123 "requiresAdministrator": false,
121 124 "entry": {
122 125 "viewXaml": "Code/Views/MainPage.xaml",
@@ -155,6 +158,7 @@ base64-generator.xfetool
155 158 | `tags` | `string[]` | `[]` | 最多 20 项,每项 1–40 字符 |
156 159 | `minimumHostVersion` | `string?` | `null` | 非空时必须是 SemVer;当前服务端会校验格式,但客户端尚未据此阻止运行 |
157 160 | `releaseNotes` | `string?` | `null` | 当前版本说明 |
161 | `nugetPackages` | `ToolNuGetPackageReference[]` | `[]` | 当前项目独立使用的 NuGet 包;最多 64 项,包 ID 不区分大小写且不能重复,版本必须是精确版本 |
158 162 | `requiresAdministrator` | `bool` | `false` | 为 `true` 时工具卡片显示 UAC 盾牌,宿主强制通过 Windows UAC 以管理员身份启动;用户不能在工具配置中关闭 |
159 163 | `entry` | `ToolEntryManifest` | 必填 | 入口视图配置,见下表 |
160 164 | `window` | `ToolWindowManifest` | 默认对象 | 独立宿主窗口配置,见下表 |
@@ -164,6 +168,12 @@ Code Studio 可视化设计器提供的通用权限名称为:`FileSystem`、`N
164 168
165 169 `requiresAdministrator` 也可以在代码工坊的 `manifest.json · 可视化配置` →“权限与格式”中勾选。该字段属于工具作者声明的强制策略,与用户在工具卡片“工具配置”中的可选管理员模式不同;任一项启用都会以管理员身份启动,但清单强制策略不能被用户覆盖。
166 170
171 ### `nugetPackages`
172
173 每个项目可以在 Code Studio 的 `manifest.json · 可视化配置` →“NuGet 包”中独立添加、更新或移除包。运行和生成验证时,工具箱会把这些引用写入该工具自己的临时 `.csproj`,再使用标准 `dotnet restore/build` 流程解析依赖;发布到 `.xfetool` 后,包引用仍保存在清单中。
174
175 包 ID 仅允许字母、数字、点、短横线和下划线,长度不超过 100;`version` 必须固定为 `1.2.3`、`1.2.3-beta.1` 这类精确版本,不接受 `*`、`[1.0,2.0)` 等浮动版本或范围。项目显式引用 `CommunityToolkit.Mvvm` 时可以覆盖工具箱内置的默认版本。NuGet 包可能携带构建目标并在还原/编译阶段运行,因此只应添加可信来源的包。
176
167 177 ### `entry`
168 178
169 179 | JSON 字段 | C# 类型 | 必填 | 说明 |