From 04805bd3c0f8e9fe99bfaee37f465a95f65340de Mon Sep 17 00:00:00 2001 From: GaoMengGu Date: Tue, 9 Jun 2026 12:51:36 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=92=8C=E4=BB=93=E5=BA=93=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Packages/RepositoryArchiveSynchronizer.cs | 73 +++++++++--- src/PlugHub.StaticValidation/Program.cs | 7 +- src/PlugHub.Wpf/RevitUiTheme.cs | 107 ++++++++++++++++++ 3 files changed, 169 insertions(+), 18 deletions(-) diff --git a/src/PlugHub.Framework/Packages/RepositoryArchiveSynchronizer.cs b/src/PlugHub.Framework/Packages/RepositoryArchiveSynchronizer.cs index e4a5100..230d907 100644 --- a/src/PlugHub.Framework/Packages/RepositoryArchiveSynchronizer.cs +++ b/src/PlugHub.Framework/Packages/RepositoryArchiveSynchronizer.cs @@ -52,8 +52,8 @@ public bool Sync(PackageRepositoryConfiguration repository, string cacheDirector try { - var archiveUrl = WithCacheBust(ArchiveUrl(address, repository)); - DownloadArchive(archiveUrl, repository, archivePath); + var archiveUrl = ArchiveDownloadUrl(address, repository); + DownloadArchive(archiveUrl, address, repository, archivePath); ValidateArchiveFile(archivePath, archiveUrl); ExtractArchive(archivePath, stagingDirectory); } @@ -88,23 +88,24 @@ public bool Sync(PackageRepositoryConfiguration repository, string cacheDirector private bool ShouldUseGiteeApiFallback(RepositoryAddress address, PackageRepositoryConfiguration repository, Exception exception) { if (!string.Equals(address.Provider, "gitee", StringComparison.OrdinalIgnoreCase)) return false; - if (!string.Equals(repository.Visibility, "private", StringComparison.OrdinalIgnoreCase)) return false; - if (string.IsNullOrWhiteSpace(_credentialService.ResolveApiKey(repository))) return false; + if (RepositoryRequiresToken(repository) && string.IsNullOrWhiteSpace(_credentialService.ResolveApiKey(repository))) return false; if (exception is InvalidDataException) return true; var webException = exception as WebException; var response = webException?.Response as HttpWebResponse; if (response == null) return false; - return response.StatusCode == HttpStatusCode.Forbidden + return response.StatusCode == HttpStatusCode.BadRequest + || response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized - || response.StatusCode == HttpStatusCode.NotFound; + || response.StatusCode == HttpStatusCode.NotFound + || response.StatusCode == HttpStatusCode.MethodNotAllowed; } private void SyncGiteeRepositoryViaApi(RepositoryAddress address, PackageRepositoryConfiguration repository, string stagingDirectory) { var apiKey = _credentialService.ResolveApiKey(repository); - if (string.IsNullOrWhiteSpace(apiKey)) + if (RepositoryRequiresToken(repository) && string.IsNullOrWhiteSpace(apiKey)) { throw new InvalidOperationException("Private Gitee repository requires an access token."); } @@ -188,10 +189,15 @@ private Dictionary ReadJsonObject(Uri uri) private static Uri GiteeApiUrl(RepositoryAddress address, string apiPath, string apiKey, string extraQuery) { - var query = "access_token=" + Uri.EscapeDataString(apiKey.Trim()); + var queryParts = new List(); + if (!string.IsNullOrWhiteSpace(apiKey)) + { + queryParts.Add("access_token=" + Uri.EscapeDataString(apiKey.Trim())); + } + if (!string.IsNullOrWhiteSpace(extraQuery)) { - query += "&" + extraQuery.TrimStart('&', '?'); + queryParts.Add(extraQuery.TrimStart('&', '?')); } return new Uri("https://gitee.com/api/v5/repos/" @@ -200,8 +206,7 @@ private static Uri GiteeApiUrl(RepositoryAddress address, string apiPath, string + Uri.EscapeDataString(address.Name) + "/" + apiPath - + "?" - + query); + + (queryParts.Count == 0 ? string.Empty : "?" + string.Join("&", queryParts))); } private static string EscapePath(string path) @@ -223,6 +228,17 @@ private static string StringValue(Dictionary source, string key) return Convert.ToString(value) ?? string.Empty; } + private Uri ArchiveDownloadUrl(RepositoryAddress address, PackageRepositoryConfiguration repository) + { + var archiveUrl = ArchiveUrl(address, repository); + return ShouldAppendArchiveCacheBust(address) ? WithCacheBust(archiveUrl) : archiveUrl; + } + + private static bool ShouldAppendArchiveCacheBust(RepositoryAddress address) + { + return string.Equals(address.Provider, "github", StringComparison.OrdinalIgnoreCase); + } + private Uri ArchiveUrl(RepositoryAddress address, PackageRepositoryConfiguration repository) { var gitRef = string.IsNullOrWhiteSpace(repository.Ref) ? "main" : repository.Ref.Trim(); @@ -246,7 +262,7 @@ private Uri ArchiveUrl(RepositoryAddress address, PackageRepositoryConfiguration return new Uri(url); } - private void DownloadArchive(Uri archiveUrl, PackageRepositoryConfiguration repository, string archivePath) + private void DownloadArchive(Uri archiveUrl, RepositoryAddress address, PackageRepositoryConfiguration repository, string archivePath) { var request = (HttpWebRequest)WebRequest.Create(archiveUrl); request.Method = "GET"; @@ -257,8 +273,8 @@ private void DownloadArchive(Uri archiveUrl, PackageRepositoryConfiguration repo request.UserAgent = ArchiveDownloadUserAgent; var apiKey = _credentialService.ResolveApiKey(repository); - if (string.Equals(repository.Provider, "github", StringComparison.OrdinalIgnoreCase) - && string.Equals(repository.Visibility, "private", StringComparison.OrdinalIgnoreCase) + if (string.Equals(address.Provider, "github", StringComparison.OrdinalIgnoreCase) + && RepositoryRequiresToken(repository) && !string.IsNullOrWhiteSpace(apiKey)) { request.Headers["Authorization"] = "Bearer " + apiKey.Trim(); @@ -286,6 +302,11 @@ private static Uri WithCacheBust(Uri uri) return new Uri(uri + separator + "plughubCacheBust=" + DateTime.UtcNow.Ticks.ToString(System.Globalization.CultureInfo.InvariantCulture)); } + private static bool RepositoryRequiresToken(PackageRepositoryConfiguration repository) + { + return string.Equals(repository.Visibility, "private", StringComparison.OrdinalIgnoreCase); + } + private static void ExtractArchive(string archivePath, string targetDirectory) { using (var archive = ZipFile.OpenRead(archivePath)) @@ -480,8 +501,8 @@ private RepositoryAddress(string provider, string owner, string name) return null; } - var expectedHost = string.Equals(provider, "gitee", StringComparison.OrdinalIgnoreCase) ? "gitee.com" : "github.com"; - if (!string.Equals(uri.Host, expectedHost, StringComparison.OrdinalIgnoreCase)) + var hostProvider = ProviderFromHost(uri.Host); + if (string.IsNullOrWhiteSpace(hostProvider)) { return null; } @@ -490,7 +511,7 @@ private RepositoryAddress(string provider, string owner, string name) .Trim('/') .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); return segments.Length >= 2 - ? new RepositoryAddress(provider, segments[0], StripRepositorySuffix(segments[1])) + ? new RepositoryAddress(hostProvider, segments[0], StripRepositorySuffix(segments[1])) : null; } @@ -508,6 +529,24 @@ private static string StripRepositorySuffix(string value) : value; } + private static string ProviderFromHost(string host) + { + var normalized = (host ?? string.Empty).Trim().TrimEnd('.'); + if (string.Equals(normalized, "github.com", StringComparison.OrdinalIgnoreCase) + || string.Equals(normalized, "www.github.com", StringComparison.OrdinalIgnoreCase)) + { + return "github"; + } + + if (string.Equals(normalized, "gitee.com", StringComparison.OrdinalIgnoreCase) + || string.Equals(normalized, "www.gitee.com", StringComparison.OrdinalIgnoreCase)) + { + return "gitee"; + } + + return string.Empty; + } + private static string StripUrlUserInfo(string url) { if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var uri) || string.IsNullOrEmpty(uri.UserInfo)) diff --git a/src/PlugHub.StaticValidation/Program.cs b/src/PlugHub.StaticValidation/Program.cs index 4a984c1..49993d0 100644 --- a/src/PlugHub.StaticValidation/Program.cs +++ b/src/PlugHub.StaticValidation/Program.cs @@ -1909,6 +1909,9 @@ private static void ValidateRevitWpfUiDesignSpecification() Require(theme.Contains("class RevitUiPalette") && theme.Contains("class RevitUiTheme"), "Revit WPF UI must centralize theme tokens in RevitUiTheme."); Require(theme.Contains("UIThemeManager") && theme.Contains("AppsUseLightTheme"), "Revit WPF UI theme detection must prefer Revit host theme and fall back to Windows app theme."); Require(theme.Contains("ButtonStyle") && theme.Contains("TabItem") && theme.Contains("DataGridRow"), "Revit WPF UI theme must provide shared styles for buttons, tabs, and grids."); + Require(theme.Contains("resources.Add(typeof(ComboBoxItem), ComboBoxItemStyle(palette))") && theme.Contains("ComboBoxItemTemplate"), "Revit WPF UI theme must explicitly style ComboBox dropdown items instead of leaving selected items on system colors."); + Require(theme.Contains("ComboBoxItem.IsHighlightedProperty") && theme.Contains("Selector.IsSelectedProperty") && theme.Contains("Control.BackgroundProperty, palette.SelectionBrush") && theme.Contains("Control.ForegroundProperty, palette.TextBrush"), "ComboBox dropdown hover and selected states must keep readable themed foreground/background colors."); + Require(theme.Contains("TabItemTemplate(palette)") && theme.Contains("ControlTemplate(typeof(TabItem))") && theme.Contains("RootBorder") && theme.Contains("Control.BorderBrushProperty, palette.AccentBrush"), "selected settings tabs must use an explicit template so WPF system colors cannot turn the selected tab white."); Require(theme.Contains("MenuItemTemplate") && theme.Contains("PART_Popup") && theme.Contains("SubmenuArrow"), "context menus must use a compact MenuItem template without the default icon slot."); Require(settingsWindow.Contains("RevitUiTheme.Apply(this)") && statusWindow.Contains("RevitUiTheme.Apply(this)"), "settings and status windows must share the Revit WPF theme."); Require(settingsWindow.Contains("BuildAboutTab") && settingsWindow.Contains("tabs.Items.Add(BuildAboutTab())"), "settings window must include an About tab."); @@ -2103,7 +2106,9 @@ private static void ValidatePackageSourceAndReleaseBehavior() Require(repositoryBrowser.Contains("RepositoryArchiveSynchronizer") && repositoryBrowser.Contains("_archiveSynchronizer.Sync"), "repository browsing must delegate remote cache refresh to the HTTP archive synchronizer."); Require(repositoryArchiveSynchronizer.Contains("HttpWebRequest") && repositoryArchiveSynchronizer.Contains("ZipFile.OpenRead") && repositoryArchiveSynchronizer.Contains("ExtractArchive"), "repository archive synchronizer must download and extract repository zip archives."); Require(repositoryArchiveSynchronizer.Contains("ArchiveDownloadUserAgent") && repositoryArchiveSynchronizer.Contains("curl/8.0.1") && !repositoryArchiveSynchronizer.Contains("request.UserAgent = \"PlugHub\""), "repository archive downloads must use a Gitee-compatible user agent accepted by archive endpoints."); - Require(repositoryArchiveSynchronizer.Contains("WithCacheBust(ArchiveUrl(address, repository))") && repositoryArchiveSynchronizer.Contains("RequestCachePolicy(RequestCacheLevel.Reload)"), "repository source sync must bypass stale HTTP/archive cache before replacing the local repository cache."); + Require(repositoryArchiveSynchronizer.Contains("ProviderFromHost(uri.Host)") && repositoryArchiveSynchronizer.Contains("new RepositoryAddress(hostProvider"), "absolute repository URLs must infer GitHub or Gitee from the URL host instead of failing when the provider field is stale."); + Require(repositoryArchiveSynchronizer.Contains("ArchiveDownloadUrl(address, repository)") && repositoryArchiveSynchronizer.Contains("ShouldAppendArchiveCacheBust") && repositoryArchiveSynchronizer.Contains("RequestCachePolicy(RequestCacheLevel.Reload)"), "repository source sync must bypass stale GitHub HTTP/archive cache without adding unsupported cache-bust query parameters to Gitee archive URLs."); + Require(repositoryArchiveSynchronizer.Contains("HttpStatusCode.BadRequest") && repositoryArchiveSynchronizer.Contains("RepositoryRequiresToken(repository)") && repositoryArchiveSynchronizer.Contains("SyncGiteeRepositoryViaApi(address, repository, stagingDirectory)"), "public Gitee archive failures must fall back to the Gitee API file download path instead of surfacing a raw 400 response."); Require(repositoryArchiveSynchronizer.Contains("ValidateArchiveFile") && repositoryArchiveSynchronizer.IndexOf("ValidateArchiveFile(archivePath, archiveUrl)", StringComparison.Ordinal) < repositoryArchiveSynchronizer.IndexOf("ExtractArchive(archivePath, stagingDirectory)", StringComparison.Ordinal), "repository archive synchronizer must validate downloaded zip content before extraction."); Require(repositoryArchiveSynchronizer.Contains("Downloaded repository archive is not a zip file") && repositoryArchiveSynchronizer.Contains("Check repository URL, ref, and credentials"), "repository archive synchronizer must report a clear URL/ref diagnostic for non-zip responses."); Require(repositoryArchiveSynchronizer.Contains("EnsureHttpsResponse(response.ResponseUri)") && repositoryArchiveSynchronizer.IndexOf("EnsureHttpsResponse(response.ResponseUri)", StringComparison.Ordinal) < repositoryArchiveSynchronizer.IndexOf("source.CopyTo(target)", StringComparison.Ordinal), "repository archive downloads must reject redirects away from HTTPS before writing archive bytes."); diff --git a/src/PlugHub.Wpf/RevitUiTheme.cs b/src/PlugHub.Wpf/RevitUiTheme.cs index 8713d6e..973bd64 100644 --- a/src/PlugHub.Wpf/RevitUiTheme.cs +++ b/src/PlugHub.Wpf/RevitUiTheme.cs @@ -106,6 +106,7 @@ private static ResourceDictionary CreateResources(RevitUiPalette palette) resources.Add(typeof(Button), ButtonStyle(palette)); resources.Add(typeof(TextBox), TextBoxStyle(palette)); resources.Add(typeof(ComboBox), ComboBoxStyle(palette)); + resources.Add(typeof(ComboBoxItem), ComboBoxItemStyle(palette)); resources.Add(typeof(CheckBox), CheckBoxStyle(palette)); resources.Add(typeof(TabControl), TabControlStyle(palette)); resources.Add(typeof(TabItem), TabItemStyle(palette)); @@ -181,6 +182,68 @@ private static Style ComboBoxStyle(RevitUiPalette palette) return style; } + private static Style ComboBoxItemStyle(RevitUiPalette palette) + { + var style = new Style(typeof(ComboBoxItem)); + style.Setters.Add(new Setter(Control.BackgroundProperty, Brushes.Transparent)); + style.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + style.Setters.Add(new Setter(Control.PaddingProperty, new Thickness(6, 3, 6, 3))); + style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)); + style.Setters.Add(new Setter(Control.VerticalContentAlignmentProperty, VerticalAlignment.Center)); + style.Setters.Add(new Setter(FrameworkElement.MinHeightProperty, 26.0)); + style.Setters.Add(new Setter(Control.TemplateProperty, ComboBoxItemTemplate(palette))); + + var highlighted = new Trigger { Property = ComboBoxItem.IsHighlightedProperty, Value = true }; + highlighted.Setters.Add(new Setter(Control.BackgroundProperty, palette.SelectionBrush)); + highlighted.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + style.Triggers.Add(highlighted); + + var selected = new Trigger { Property = Selector.IsSelectedProperty, Value = true }; + selected.Setters.Add(new Setter(Control.BackgroundProperty, palette.SelectionBrush)); + selected.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + style.Triggers.Add(selected); + + var disabled = new Trigger { Property = UIElement.IsEnabledProperty, Value = false }; + disabled.Setters.Add(new Setter(Control.ForegroundProperty, palette.DisabledTextBrush)); + style.Triggers.Add(disabled); + return style; + } + + private static ControlTemplate ComboBoxItemTemplate(RevitUiPalette palette) + { + var root = new FrameworkElementFactory(typeof(Border)); + root.Name = "RootBorder"; + root.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty)); + root.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Control.PaddingProperty)); + + var content = new FrameworkElementFactory(typeof(ContentPresenter)); + content.SetValue(ContentPresenter.RecognizesAccessKeyProperty, true); + content.SetValue(FrameworkElement.HorizontalAlignmentProperty, new TemplateBindingExtension(Control.HorizontalContentAlignmentProperty)); + content.SetValue(FrameworkElement.VerticalAlignmentProperty, new TemplateBindingExtension(Control.VerticalContentAlignmentProperty)); + content.SetBinding(ContentPresenter.ContentProperty, new Binding("Content") { RelativeSource = RelativeSource.TemplatedParent }); + content.SetBinding(ContentPresenter.ContentTemplateProperty, new Binding("ContentTemplate") { RelativeSource = RelativeSource.TemplatedParent }); + content.SetBinding(ContentPresenter.ContentStringFormatProperty, new Binding("ContentStringFormat") { RelativeSource = RelativeSource.TemplatedParent }); + root.AppendChild(content); + + var template = new ControlTemplate(typeof(ComboBoxItem)) { VisualTree = root }; + + var highlighted = new Trigger { Property = ComboBoxItem.IsHighlightedProperty, Value = true }; + highlighted.Setters.Add(new Setter(Border.BackgroundProperty, palette.SelectionBrush, "RootBorder")); + highlighted.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + template.Triggers.Add(highlighted); + + var selected = new Trigger { Property = Selector.IsSelectedProperty, Value = true }; + selected.Setters.Add(new Setter(Border.BackgroundProperty, palette.SelectionBrush, "RootBorder")); + selected.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + template.Triggers.Add(selected); + + var disabled = new Trigger { Property = UIElement.IsEnabledProperty, Value = false }; + disabled.Setters.Add(new Setter(UIElement.OpacityProperty, 0.58, "RootBorder")); + disabled.Setters.Add(new Setter(Control.ForegroundProperty, palette.DisabledTextBrush)); + template.Triggers.Add(disabled); + return template; + } + private static Style CheckBoxStyle(RevitUiPalette palette) { var style = new Style(typeof(CheckBox)); @@ -204,6 +267,12 @@ private static Style TabItemStyle(RevitUiPalette palette) style.Setters.Add(new Setter(Control.ForegroundProperty, palette.MutedTextBrush)); style.Setters.Add(new Setter(Control.BorderBrushProperty, palette.BorderBrush)); style.Setters.Add(new Setter(Control.PaddingProperty, new Thickness(12, 5, 12, 5))); + style.Setters.Add(new Setter(Control.TemplateProperty, TabItemTemplate(palette))); + + var hover = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true }; + hover.Setters.Add(new Setter(Control.BackgroundProperty, palette.ControlHoverBackground)); + hover.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + style.Triggers.Add(hover); var selected = new Trigger { Property = Selector.IsSelectedProperty, Value = true }; selected.Setters.Add(new Setter(Control.BackgroundProperty, palette.PanelBackground)); @@ -213,6 +282,44 @@ private static Style TabItemStyle(RevitUiPalette palette) return style; } + private static ControlTemplate TabItemTemplate(RevitUiPalette palette) + { + var root = new FrameworkElementFactory(typeof(Border)); + root.Name = "RootBorder"; + root.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty)); + root.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Control.BorderBrushProperty)); + root.SetValue(Border.BorderThicknessProperty, new Thickness(1, 1, 1, 0)); + root.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Control.PaddingProperty)); + + var header = new FrameworkElementFactory(typeof(ContentPresenter)); + header.Name = "HeaderHost"; + header.SetValue(ContentPresenter.RecognizesAccessKeyProperty, true); + header.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center); + header.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); + header.SetBinding(ContentPresenter.ContentProperty, new Binding("Header") { RelativeSource = RelativeSource.TemplatedParent }); + header.SetBinding(ContentPresenter.ContentTemplateProperty, new Binding("HeaderTemplate") { RelativeSource = RelativeSource.TemplatedParent }); + header.SetBinding(ContentPresenter.ContentStringFormatProperty, new Binding("HeaderStringFormat") { RelativeSource = RelativeSource.TemplatedParent }); + root.AppendChild(header); + + var template = new ControlTemplate(typeof(TabItem)) { VisualTree = root }; + + var hover = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true }; + hover.Setters.Add(new Setter(Border.BackgroundProperty, palette.ControlHoverBackground, "RootBorder")); + hover.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + template.Triggers.Add(hover); + + var selected = new Trigger { Property = Selector.IsSelectedProperty, Value = true }; + selected.Setters.Add(new Setter(Border.BackgroundProperty, palette.PanelBackground, "RootBorder")); + selected.Setters.Add(new Setter(Border.BorderBrushProperty, palette.AccentBrush, "RootBorder")); + selected.Setters.Add(new Setter(Control.ForegroundProperty, palette.TextBrush)); + template.Triggers.Add(selected); + + var disabled = new Trigger { Property = UIElement.IsEnabledProperty, Value = false }; + disabled.Setters.Add(new Setter(UIElement.OpacityProperty, 0.58, "RootBorder")); + template.Triggers.Add(disabled); + return template; + } + private static Style DataGridStyle(RevitUiPalette palette) { var style = new Style(typeof(DataGrid));