返回提交历史
Added
XFEToolBox.Client.Wpf.Test/ConsoleFoldBlockStyleTests.cs
+94
-0
Added
XFEToolBox.Client.Wpf.Test/ToolNuGetPackageTests.cs
+125
-0
Modified
XFEToolBox.Core/Tools/ToolPackageManifest.cs
+40
-0
Modified
XFEToolBox.Server.Core/Services/ToolPackageValidator.cs
+18
-0
Modified
XFEToolBox/Utilities/DecoratedTextConverter.cs
+78
-14
Modified
XFEToolBox/Utilities/ToolProjectRunService.cs
+32
-3
Modified
XFEToolBox/Utilities/ToolProjectWorkspaceService.cs
+1
-0
Modified
XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml
+52
-0
Modified
XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml.cs
+74
-0
Added
XfeTestArtifacts/test-results.json
+444
-0
Added
XfeTestArtifacts/test-results.xml
+36
-0
Modified
docs/tool-packages.md
+10
-0
XFEstudio/XFEToolBox
引入项目独立 NuGet 包引用功能
本次提交实现了工具包 manifest.json 对 nugetPackages 字段的支持,允许每个工具独立声明所需 NuGet 包及版本。新增了类型定义、校验规则和唯一性检查。Code Studio 设计器界面支持可视化管理包引用,实时校验输入。运行/编译时自动注入声明的 NuGet 包,支持覆盖内置包版本。完善了相关文档和示例,补充了序列化、校验、注入等单元测试,提升了工具包的扩展性和可维护性。
ed5f0d2
代码差异
12 个文件
+1004
-17
@@ -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
}
@@ -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
}
@@ -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;
@@ -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 =>
@@ -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>
@@ -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>
@@ -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",
@@ -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"/>
@@ -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
@@ -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
}
@@ -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# 类型 | 必填 | 说明 |