CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
370 lines
15 KiB
C#
370 lines
15 KiB
C#
using System.IO;
|
||
using PCL.Core.App;
|
||
using PCL.Core.App.Configuration;
|
||
using PCL.Core.App.Localization;
|
||
using PCL.Core.Utils.OS;
|
||
|
||
namespace PCL
|
||
{
|
||
public class CustomEvent
|
||
{
|
||
public EventType Type { get; set; } = EventType.None;
|
||
public string Data { get; set; } = string.Empty;
|
||
|
||
public CustomEvent() { }
|
||
|
||
public CustomEvent(EventType type, string data)
|
||
{
|
||
Type = type;
|
||
Data = data;
|
||
}
|
||
|
||
public void Raise() => Raise(Type, Data);
|
||
|
||
/// <summary>
|
||
/// 静态入口:根据 EventType 分发给对应的 action 执行。
|
||
/// </summary>
|
||
public static void Raise(EventType type, string arg)
|
||
{
|
||
if (type == EventType.None) return;
|
||
ModBase.Log($"[Control] 执行自定义事件: {type}, {arg}");
|
||
|
||
try
|
||
{
|
||
if (ActionMap.TryGetValue(type, out var action))
|
||
action(arg, type);
|
||
else
|
||
ModMain.MyMsgBox(
|
||
Lang.Text("Event.Error.UnknownType", type.ToString()),
|
||
Lang.Text("Event.Error.Title"));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
Lang.Text("Event.Error.ExecutionFailed", type, arg),
|
||
ModBase.LogLevel.Msgbox,
|
||
userSummary: Lang.Text("Event.Error.ExecutionFailed", type, arg));
|
||
}
|
||
}
|
||
|
||
public static string GetCustomVariable(string name, string defaultValue = "") =>
|
||
States.CustomVariables.GetValueOrDefault(name, defaultValue);
|
||
|
||
/// <summary>
|
||
/// 将 arg 按 '|' 分割为参数数组,空串统一返回 [""]。
|
||
/// </summary>
|
||
private static string[] SplitArgs(string arg) => arg.Split('|');
|
||
|
||
/// <summary>
|
||
/// 将 \\n 替换为 Windows 换行符 \r\n。
|
||
/// </summary>
|
||
private static string FixNewlines(string s) => s.Replace("\\n", "\r\n");
|
||
|
||
/// <summary>
|
||
/// 禁止自定义主页写入的高危设置黑名单
|
||
/// </summary>
|
||
private static readonly HashSet<string> SecuritySensitiveSettingKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
"LaunchAdvanceRun",
|
||
"VersionAdvanceRun"
|
||
};
|
||
|
||
/// <summary>
|
||
/// EventType → 执行逻辑 的字典映射,O(1) 分发。
|
||
/// </summary>
|
||
private static readonly Dictionary<EventType, Action<string, EventType>> ActionMap = new()
|
||
{
|
||
[EventType.OpenUrl] = _OpenUrl,
|
||
[EventType.OpenFile] = _OpenFileOrCommand,
|
||
[EventType.ExecuteCommand] = _OpenFileOrCommand,
|
||
[EventType.LaunchGame] = _LaunchGame,
|
||
[EventType.CopyText] = _CopyText,
|
||
[EventType.RefreshHomepage] = _Refresh,
|
||
[EventType.RefreshPage] = _Refresh,
|
||
[EventType.DailyFortune] = _DailyFortune,
|
||
[EventType.ClearTrash] = _ClearTrash,
|
||
[EventType.ShowDialog] = _ShowDialog,
|
||
[EventType.ShowHint] = _ShowHint,
|
||
[EventType.SwitchPage] = _SwitchPage,
|
||
[EventType.ImportModpack] = _ModpackInstall,
|
||
[EventType.InstallModpack] = _ModpackInstall,
|
||
[EventType.DownloadFile] = _DownloadFile,
|
||
[EventType.ModifySetting] = _WriteSetting,
|
||
[EventType.WriteSetting] = _WriteSetting,
|
||
[EventType.ModifyVariable] = _WriteVariable,
|
||
[EventType.WriteVariable] = _WriteVariable,
|
||
[EventType.OpenHelp] = (_, __) => ModBase.OpenWebsite("https://docs.pclc.cc/ce"),
|
||
};
|
||
|
||
/// <summary>
|
||
/// 打开网页。校验 https?:// 前缀,非 file 协议。
|
||
/// </summary>
|
||
private static void _OpenUrl(string arg, EventType type)
|
||
{
|
||
arg = arg.Replace('\\', '/');
|
||
if (!arg.Contains("://") || arg.StartsWithF("file", true))
|
||
{
|
||
ModMain.MyMsgBox(Lang.Text("Event.Error.UrlRequired"), Lang.Text("Event.Error.Title"));
|
||
return;
|
||
}
|
||
HintService.Hint(Lang.Text("Event.OpenUrl.Opening", arg));
|
||
ModBase.RunInThread(() => ModBase.OpenWebsite(arg));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开文件 / 执行命令。复用 GetAbsoluteUrls 解析路径后 ProcessInterop.Start 启动。
|
||
/// </summary>
|
||
private static void _OpenFileOrCommand(string arg, EventType type)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
ModBase.RunInThread(() =>
|
||
{
|
||
try
|
||
{
|
||
var urls = GetAbsoluteUrls(args[0], type);
|
||
if (!EventSafetyConfirm($"{urls[0]}{(args.Length >= 2 ? " " + args[1] : "")}"))
|
||
return;
|
||
ProcessInterop.Start(urls[0], args.Length >= 2 ? args[1] : "");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
Lang.Text("Event.Error.ExecutionFailed", type, arg),
|
||
ModBase.LogLevel.Msgbox,
|
||
userSummary: Lang.Text("Event.Error.ExecutionFailed", type, arg));
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动游戏。支持 \current 指代当前选中实例,可选 ServerIp。
|
||
/// </summary>
|
||
private static void _LaunchGame(string arg, EventType type)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
if (args[0] == "\\current")
|
||
{
|
||
if (ModInstanceList.McMcInstanceSelected is null)
|
||
throw new InvalidOperationException(Lang.Text("Event.LaunchGame.SelectVersion"));
|
||
args[0] = ModInstanceList.McMcInstanceSelected.Name;
|
||
}
|
||
ModBase.RunInUi(() =>
|
||
{
|
||
var launchOptions = new ModLaunch.McLaunchOptions
|
||
{
|
||
ServerIp = args.Length >= 2 ? args[1] : null,
|
||
instance = new McInstance(args[0])
|
||
};
|
||
if (ModLaunch.McLaunchStart(launchOptions))
|
||
HintService.Hint(Lang.Text("Event.LaunchGame.Starting", args[0]));
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 复制文本到剪贴板。
|
||
/// </summary>
|
||
private static void _CopyText(string arg, EventType _) => ModBase.ClipboardSet(arg);
|
||
|
||
/// <summary>
|
||
/// 刷新主页 / 刷新当前页面。要求当前 pageRight 实现 IRefreshable。
|
||
/// </summary>
|
||
private static void _Refresh(string arg, EventType _)
|
||
{
|
||
if (ModMain.frmMain?.pageRight is IRefreshable refreshable)
|
||
{
|
||
ModBase.RunInUiWait(() => refreshable.Refresh());
|
||
if (string.IsNullOrEmpty(arg))
|
||
HintService.Hint(Lang.Text("Event.Refresh.Success"), HintType.Success);
|
||
}
|
||
else
|
||
HintService.Hint(Lang.Text("Event.Refresh.NotSupported"), HintType.Error);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 今日人品。
|
||
/// </summary>
|
||
private static void _DailyFortune(string _, EventType __) => PageToolsTest.Jrrp();
|
||
|
||
/// <summary>
|
||
/// 清理垃圾。异步执行 RubbishClear。
|
||
/// </summary>
|
||
private static void _ClearTrash(string _, EventType __) =>
|
||
ModBase.RunInThread(PageToolsTest.RubbishClear);
|
||
|
||
/// <summary>
|
||
/// 弹出消息框。参数:Title|Content[|ButtonText]。
|
||
/// </summary>
|
||
private static void _ShowDialog(string arg, EventType type)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
if (args.Length == 1)
|
||
throw new ArgumentException(Lang.Text("Event.Error.MissingArgs", type.ToString(), "Title|Content"));
|
||
ModMain.MyMsgBox(
|
||
FixNewlines(args[1]),
|
||
FixNewlines(args[0]),
|
||
args.Length > 2 ? args[2] : Lang.Text("Common.Action.Confirm"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 弹出提示条。参数:Message[|HintType](HintType = Info / Success / Error,兼容旧版 Finish / Critical)。
|
||
/// </summary>
|
||
private static void _ShowHint(string arg, EventType _)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
var hintType = args.Length == 1
|
||
? HintType.Info
|
||
: (HintType)Enum.Parse(typeof(HintType), NormalizeHintTypeName(args[1]), true);
|
||
HintService.Hint(FixNewlines(args[0]), hintType);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将旧版 HintType 名称映射到当前名称,保持对旧主页的兼容。
|
||
/// </summary>
|
||
private static string NormalizeHintTypeName(string name) => name.ToLowerInvariant() switch
|
||
{
|
||
"finish" => nameof(HintType.Success),
|
||
"critical" => nameof(HintType.Error),
|
||
_ => name
|
||
};
|
||
|
||
/// <summary>
|
||
/// 切换页面。参数:PageType[|PageSubType]。
|
||
/// </summary>
|
||
private static void _SwitchPage(string arg, EventType _)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
ModBase.RunInUi(() =>
|
||
{
|
||
var page = (FormMain.PageType)Enum.Parse(typeof(FormMain.PageType), args[0], true);
|
||
var sub = args.Length == 1
|
||
? FormMain.PageSubType.Default
|
||
: (FormMain.PageSubType)Enum.Parse(typeof(FormMain.PageSubType), args[1], true);
|
||
ModMain.frmMain?.PageChange(page, sub);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导入 / 安装整合包。触发 ModModpack.ModpackInstall()。
|
||
/// </summary>
|
||
private static void _ModpackInstall(string _, EventType __) =>
|
||
ModBase.RunInUi(ModModpack.ModpackInstall);
|
||
|
||
/// <summary>
|
||
/// 下载文件。参数:Url[|SavePath[|FileName]],校验 http/https 前缀并弹安全确认。
|
||
/// </summary>
|
||
private static void _DownloadFile(string arg, EventType _)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
args[0] = args[0].Replace('\\', '/');
|
||
if (!args[0].StartsWithF("http://", true) && !args[0].StartsWithF("https://", true))
|
||
{
|
||
ModMain.MyMsgBox(Lang.Text("Event.Error.DownloadUrlRequired"), Lang.Text("Event.Error.Title"));
|
||
return;
|
||
}
|
||
if (!EventSafetyConfirm(Lang.Text("Event.Download.Confirm", args[0])))
|
||
return;
|
||
|
||
try
|
||
{
|
||
PageToolsTest.StartCustomDownload(args[0],
|
||
args.Length >= 2 ? args[1] : ModBase.GetFileNameFromPath(args[0]),
|
||
args.Length >= 3 ? args[2] : null);
|
||
}
|
||
catch
|
||
{
|
||
PageToolsTest.StartCustomDownload(args[0], Lang.Text("Common.State.Unknown"));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入 / 修改设置。参数:SettingName|Value。
|
||
/// </summary>
|
||
private static void _WriteSetting(string arg, EventType type)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
if (args.Length == 1)
|
||
throw new ArgumentException(Lang.Text("Event.Error.MissingArgs", type.ToString(), "SettingName|Value"));
|
||
if (ConfigService.TryGetConfigItemNoType(args[0], out var item) && item.Source != ConfigSource.SharedEncrypt)
|
||
{
|
||
if (!CanWriteSettingFromCustomEvent(args[0]))
|
||
return;
|
||
item.SetValueNoType(args[1], ModInstanceList.McMcInstanceSelected?.PathInstance);
|
||
}
|
||
if (args.Length == 2)
|
||
HintService.Hint(Lang.Text("Event.Setting.Written", args[0], args[1]), HintType.Success);
|
||
}
|
||
private static bool CanWriteSettingFromCustomEvent(string key)
|
||
{
|
||
if (!SecuritySensitiveSettingKeys.Contains(key))
|
||
return true;
|
||
ModBase.Log($"[Control] 已阻止自定义事件写入高危设置:{key}", ModBase.LogLevel.Developer);
|
||
HintService.Hint(Lang.Text("Event.Safety.DangerousSettingBlocked", key), HintType.Error);
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入 / 修改自定义变量。参数:VariableName|Value。
|
||
/// </summary>
|
||
private static void _WriteVariable(string arg, EventType type)
|
||
{
|
||
var args = SplitArgs(arg);
|
||
if (args.Length == 1)
|
||
throw new ArgumentException(Lang.Text("Event.Error.MissingArgs", type.ToString(), "VariableName|Value"));
|
||
States.CustomVariables[args[0]] = args[1];
|
||
States.CustomVariables = States.CustomVariables; // 触发 ConfigPropertyChanged
|
||
if (args.Length == 2)
|
||
HintService.Hint(Lang.Text("Event.Variable.Written", args[0], args[1]), HintType.Success);
|
||
}
|
||
|
||
public static string[] GetAbsoluteUrls(string relativeUrl, EventType type)
|
||
{
|
||
relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\');
|
||
var pclDir = Path.Combine(Basics.ExecutableDirectory, "PCL");
|
||
|
||
if (relativeUrl.Contains(":\\"))
|
||
{
|
||
ModBase.Log($"[Control] 自定义事件中由绝对路径 {type}: {relativeUrl}");
|
||
return [relativeUrl, pclDir];
|
||
}
|
||
if (File.Exists(Path.Combine(pclDir, relativeUrl)))
|
||
{
|
||
var fullPath = Path.Combine(pclDir, relativeUrl);
|
||
var resolved = Path.GetFullPath(fullPath);
|
||
if (!resolved.StartsWith(pclDir, StringComparison.OrdinalIgnoreCase))
|
||
throw new UnauthorizedAccessException(Lang.Text("Event.Error.FileNotFound", relativeUrl));
|
||
ModBase.Log($"[Control] 自定义事件中由相对 PCL 文件夹的路径 {type}: {fullPath}");
|
||
return [fullPath, pclDir];
|
||
}
|
||
if (type is EventType.OpenFile or EventType.ExecuteCommand)
|
||
{
|
||
ModBase.Log($"[Control] 自定义事件中直接 {type}: {relativeUrl}");
|
||
return [relativeUrl, pclDir];
|
||
}
|
||
throw new FileNotFoundException(Lang.Text("Event.Error.FileNotFound", relativeUrl), relativeUrl);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 安全确认对话框。已由用户勾选"不再询问"时跳过。
|
||
/// </summary>
|
||
private static bool EventSafetyConfirm(string message)
|
||
{
|
||
if (States.Hint.HomepageCommand)
|
||
return true;
|
||
|
||
return ModMain.MyMsgBox(
|
||
Lang.Text("Event.Safety.ConfirmMessage", message),
|
||
Lang.Text("Event.Safety.Title"),
|
||
Lang.Text("Common.Action.Continue"),
|
||
Lang.Text("Event.Safety.ContinueAlways"),
|
||
Lang.Text("Common.Action.Cancel")) switch
|
||
{
|
||
1 => true,
|
||
2 => States.Hint.HomepageCommand = true,
|
||
_ => false,
|
||
};
|
||
}
|
||
}
|
||
}
|