Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions PCL.Core/App/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public static partial class Config
/// 动画帧率上限。
/// </summary>
[ConfigItem<int>("UiAniFPS", 59)] public partial int AnimationFpsLimit { get; set; }

/// <summary>
/// 隐藏的公告
/// </summary>
[ConfigItem<string>("HiddenAnnouncementList", "[]")] public partial string HiddenAnnouncement { get; set; }
}

/// <summary>
Expand Down
102 changes: 102 additions & 0 deletions PCL.Core/App/Essentials/Announcement/AnnouncementService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using PCL.Core.App.Essentials.Announcement.Models;
using PCL.Core.App.IoC;
using PCL.Core.IO.Net.Http;
using PCL.Core.Logging;
using PCL.Core.UI;

namespace PCL.Core.App.Essentials.Announcement;

[LifecycleScope("announcement","公告")]
[LifecycleService(LifecycleState.Running)]
public partial class AnnouncementService
{

private static readonly string[] _AllowScheme = ["http", "https", "minecraft" ];
private static readonly string[] _AnnouncementServerList = Secrets.AnnouncementServerList;

private static List<string> _ignored =
JsonSerializer.Deserialize<string[]>(Config.System.HiddenAnnouncement)?.ToList() ?? [];

[LifecycleStart]
private static async Task _Start()
{
// 可能会出现公告服务比配置服务晚关闭的情况
Lifecycle.StateChanged += state =>
{
if (state == LifecycleState.Closing) Config.System.HiddenAnnouncement = JsonSerializer.Serialize(_ignored);
};
try
{
foreach (var source in _AnnouncementServerList)
{
var response = await HttpRequest.GetJsonAsync<List<AnnouncementDetails>>(source)
.ConfigureAwait(false);
if (response is null) continue;

// 对忽略的公告进行检查1,以确保仍然处于公告列表内

var invalid = _ignored.Except(response.Select(a => a.Id)).ToList();
_ignored.RemoveAll(invalid.Contains);

var announcements = response.OrderByDescending(a => a.Priority).Where(a =>
{
var isNotAfterValid = DateTimeOffset.TryParse(a.SkipOn.NotAfter, out var notAfter);
var isNotBeforeValid = DateTimeOffset.TryParse(a.SkipOn.NotBefore, out var notBefore);
var localTime = DateTimeOffset.Now;
if (isNotAfterValid && localTime > notAfter) return false;
if (isNotBeforeValid && localTime < notBefore) return false;
var currentVersion = new Version(Basics.VersionName.Split("-")[0]);
var max = new Version(a.SkipOn.MaxVersion ?? "999.999.999");
var min = new Version(a.SkipOn.MinVersion ?? "0.0.0");

// [min,max]
return currentVersion >= min && currentVersion <= max;

});
foreach (var detail in announcements)
{
Context.Debug(MsgBoxWrapper.ShowWithCustomButtons(
detail.Details, $"{detail.Title} ({detail.ReleaseDate})", _GetSelectTheme(detail.Level),
false,
detail.Buttons.Select(operation => new MsgBoxButtonInfo(operation.ButtonText,
Comment on lines +65 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: 某一个公告服务器失败会中止后续服务器的处理。

因为 foreach (var source in _AnnouncementServerList) 循环被包裹在一个统一的 try/catch (HttpRequestException) 中,只要任意一个服务器抛出异常,就会阻止后续服务器被处理。请针对每个服务器单独处理 HttpRequestException(例如,在每次迭代外包一层,或者在 GetJsonAsync 内部捕获),以避免单个失败的端点阻塞其它端点。

建议实现方式:

                foreach (var detail in announcements)
                {
                    Context.Debug(MsgBoxWrapper.ShowWithCustomButtons(
                        detail.Details, $"{detail.Title} ({detail.ReleaseDate})", _GetSelectTheme(detail.Level),
                        false,
                        detail.Buttons.Select(operation => new MsgBoxButtonInfo(operation.ButtonText,
                            OnClick: _GetSelectCallback(operation.Operation, operation.Argument))).ToArray()).ToString());
                }
            }
        }
    }

    private static Action _GetSelectCallback(string operation, string arguments) => operation switch

要完整实现你在审查意见中建议的“按服务器分别处理错误”,可以:

  1. 在该方法前面找到 foreach (var source in _AnnouncementServerList) 循环。
  2. 将每个服务器的 HTTP 调用(例如获取该 source 公告的 GetJsonAsync/HttpClient 调用)包裹在各自的 try/catch (HttpRequestException ex) 块中。
  3. 在每个服务器自身的 catch 中,使用 Context.Error("加载公告失败", ex, ActionLevel.HintErr);(或带上服务器/来源标识的变体)记录失败日志,然后使用 continue; 继续处理下一个服务器,而不是中止整个循环。
  4. 移除或避免任何仍然包裹整个 _AnnouncementServerList 循环的外层 try/catch (HttpRequestException),这样单个失败的端点就不会阻止后续端点被处理。
Original comment in English

suggestion: A failure on one announcement server aborts processing of subsequent servers.

Because the foreach (var source in _AnnouncementServerList) loop is inside a single try/catch (HttpRequestException), an exception from any server will prevent later servers from being processed. Please handle HttpRequestException per server (e.g., around each iteration or inside GetJsonAsync) so one failing endpoint doesn’t block the rest.

Suggested implementation:

                foreach (var detail in announcements)
                {
                    Context.Debug(MsgBoxWrapper.ShowWithCustomButtons(
                        detail.Details, $"{detail.Title} ({detail.ReleaseDate})", _GetSelectTheme(detail.Level),
                        false,
                        detail.Buttons.Select(operation => new MsgBoxButtonInfo(operation.ButtonText,
                            OnClick: _GetSelectCallback(operation.Operation, operation.Argument))).ToArray()).ToString());
                }
            }
        }
    }

    private static Action _GetSelectCallback(string operation, string arguments) => operation switch

To fully implement per-server error handling as suggested in your review comment, you should:

  1. Locate the foreach (var source in _AnnouncementServerList) loop earlier in this method.
  2. Wrap the HTTP call for each server (e.g., the GetJsonAsync/HttpClient call that fetches announcements for that source) in its own try/catch (HttpRequestException ex) block.
  3. Inside that per-server catch, log the failure using Context.Error("加载公告失败", ex, ActionLevel.HintErr); (or a variant that includes the server/source identifier) and then continue; to proceed to the next server instead of aborting the loop.
  4. Remove or avoid any remaining outer try/catch (HttpRequestException) that wraps the entire _AnnouncementServerList loop, so that one failing endpoint does not prevent subsequent servers from being processed.

OnClick: _GetSelectCallback(operation.Operation, operation.Argument))).ToArray()).ToString());
}
}
}
catch (HttpRequestException ex)
{
Context.Error("加载公告失败", ex, ActionLevel.HintErr);
}
}

private static Action _GetSelectCallback(string operation, string arguments) => operation switch
{
"OpenWebSite" => () =>
{
if (arguments.Length == 0) throw new ArgumentException("Uri is missing");
if (_AllowScheme.All(s => new Uri(arguments).Scheme != s))
throw new InvalidOperationException("This uri contains a unsupported scheme.");
Process.Start(new ProcessStartInfo(arguments){ UseShellExecute = true });

},
"StopShow" => () =>
{
_ignored.Add(arguments);
},
_ => static () => { }
};

private static MsgBoxTheme _GetSelectTheme(AnnouncementLevel level) => level switch
{
AnnouncementLevel.Medium => MsgBoxTheme.Warning,
AnnouncementLevel.Highest => MsgBoxTheme.Error,
_ => MsgBoxTheme.Info
};
}
56 changes: 56 additions & 0 deletions PCL.Core/App/Essentials/Announcement/Models/AnnouncementDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace PCL.Core.App.Essentials.Announcement.Models;

public record AnnouncementDetails
{
/// <summary>
/// 公告标题
/// </summary>
[JsonPropertyName("title")]
public required string Title { get; init; }

/// <summary>
/// 公告内容
/// </summary>
[JsonPropertyName("details")]
public required string Details { get; init; }

/// <summary>
/// 该公告的优先级,值越高优先级越高
/// </summary>
[JsonPropertyName("priority")]
public int Priority { get; init; }

/// <summary>
/// 公告 ID
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }

/// <summary>
/// 该公告的等级,决定弹窗应该用什么样式
/// </summary>
[JsonPropertyName("level")]
public required AnnouncementLevel Level { get; init; }

/// <summary>
/// 该公告的发布日期
/// </summary>
[JsonPropertyName("date")]
public required string ReleaseDate { get; init; }

/// <summary>
/// 显示条件
/// </summary>
[JsonPropertyName("skip")]
public required AnnouncementSkipCondition SkipOn { get; init; }

/// <summary>
/// 弹窗按钮信息
/// </summary>
[JsonPropertyName("buttons")]
public required IEnumerable<AnnouncementOperation> Buttons { get; init; }
}
17 changes: 17 additions & 0 deletions PCL.Core/App/Essentials/Announcement/Models/AnnouncementLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace PCL.Core.App.Essentials.Announcement.Models;

public enum AnnouncementLevel
{
/// <summary>
/// 最低的等级,属于可看可不看的那种
/// </summary>
Lowest,
/// <summary>
/// 用户应该稍微有点了解的公告
/// </summary>
Medium,
/// <summary>
/// 必须让用户知道并理解的公告内容
/// </summary>
Highest
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace PCL.Core.App.Essentials.Announcement.Models;

public record AnnouncementOperation
{
/// <summary>
/// 按钮文本
/// </summary>
[JsonPropertyName("text")]
public required string ButtonText { get; init; }

/// <summary>
/// 按下后的操作
/// </summary>
[JsonPropertyName("exec")]
public required string Operation { get; init; }

/// <summary>
/// 参数列表
/// </summary>
[JsonPropertyName("argument")]
public required string Argument { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace PCL.Core.App.Essentials.Announcement.Models;

public class AnnouncementSkipCondition
{
[JsonPropertyName("min")]
public string? MinVersion { get; init; }
[JsonPropertyName("max")]
public string? MaxVersion { get; init; }
[JsonPropertyName("notAfter")]
public string? NotAfter { get; init; }
[JsonPropertyName("notBefore")]
public string? NotBefore { get; init; }

}
6 changes: 6 additions & 0 deletions PCL.Core/App/Secrets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ public static class Secrets
/// 当前版本的 Git 提交 SHA
/// </summary>
public static string CommitHash { get; } = EnvironmentInterop.GetSecret("GITHUB_SHA", readEnvDebugOnly: true).ReplaceNullOrEmpty();

/// <summary>
/// 公告服务器地址
/// </summary>
public static string[] AnnouncementServerList { get; } = EnvironmentInterop
.GetSecret("ANNOUNCEMENT_SERVER", readEnvDebugOnly: true).ReplaceNullOrEmpty().Split("|");
}
Loading