using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using BetterGenshinImpact.Core.Script.Utils; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.View; using Microsoft.Extensions.Logging; namespace BetterGenshinImpact.Core.Script.Dependence; /// /// HTML遮罩层 - JS脚本依赖类 /// 提供窗口管理与消息通信功能 /// public class HtmlMask : IDisposable { private static readonly JsonSerializerOptions _jsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; /// /// 通信消息结构 /// public class Message { public string Url { get; set; } = ""; public JsonElement? Data { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? RequestId { get; set; } } /// /// 脚本到HTML的待推送队列 /// private static readonly ConcurrentDictionary> _toHtmlQueues = new(); /// /// HTML到脚本的消息队列 /// private static readonly ConcurrentDictionary> _fromHtmlQueues = new(); /// /// JS到HTML请求的等待句柄,用于request-response匹配 /// private static readonly ConcurrentDictionary> _jsPendingRequests = new(); /// /// requestId到windowId的映射,用于窗口关闭时取消对应的pending请求 /// private static readonly ConcurrentDictionary _requestWindowMap = new(); private readonly string _workDir; private readonly List _openedWindows = []; private readonly object _openedWindowsLock = new(); private bool _disposed; public HtmlMask(string workDir) { _workDir = workDir; } #region 窗口管理 /// /// 显示HTML遮罩窗口 /// public string Show(string url, string? id = null) { try { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("URL不能为空"); string finalUrl; if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { finalUrl = url; } else { // 禁止 file:// 绝对路径,仅允许脚本目录下的相对路径 string absPath = ScriptUtils.NormalizePath(_workDir, url); finalUrl = new Uri(absPath).AbsoluteUri; } string windowId = HtmlMaskWindow.Show(finalUrl, id, _workDir); _toHtmlQueues[windowId] = new ConcurrentQueue(); _fromHtmlQueues[windowId] = new ConcurrentQueue(); lock (_openedWindowsLock) { _openedWindows.Add(windowId); } return windowId; } catch (Exception ex) { TaskControl.Logger.LogError(ex, "打开HTML遮罩失败: {Url}", url); throw; } } /// /// 关闭指定窗口 /// public bool Close(string id) { lock (_openedWindowsLock) { _openedWindows.Remove(id); } CleanupQueues(id); return HtmlMaskWindow.Close(id); } /// /// 关闭所有由本实例打开的窗口 /// public void CloseAll() { List windows; lock (_openedWindowsLock) { windows = [.. _openedWindows]; _openedWindows.Clear(); } foreach (var windowId in windows) { CleanupQueues(windowId); HtmlMaskWindow.Close(windowId); } } /// /// 获取所有窗口ID /// public string[] GetWindowIds() => HtmlMaskWindow.GetWindowIds(); /// /// 窗口是否存在 /// public bool Exists(string id) => HtmlMaskWindow.Exists(id); /// /// 设置窗口的点击穿透模式 /// /// 窗口ID /// true=点击穿透,false=可交互 public void SetClickThrough(string windowId, bool enabled) { HtmlMaskWindow.SetClickThrough(windowId, enabled); } /// /// 获取窗口的点击穿透状态 /// /// 窗口ID /// true=点击穿透,false=可交互 public bool GetClickThrough(string windowId) { return HtmlMaskWindow.GetClickThrough(windowId); } /// /// 切换窗口的点击穿透模式 /// /// 窗口ID public void ToggleClickThrough(string windowId) { HtmlMaskWindow.ToggleClickThrough(windowId); } #endregion #region 消息通信 /// /// 发送消息到HTML(单向推送) /// public void Send(string windowId, string url, string jsonData) { if (!HtmlMaskWindow.Exists(windowId) || !_toHtmlQueues.TryGetValue(windowId, out var queue)) throw new InvalidOperationException($"HTML遮罩窗口不存在或已关闭: {windowId}"); queue.Enqueue(new Message { Url = url, Data = ParseData(jsonData) }); HtmlMaskWindow.NotifyFlush(windowId); } /// /// 发送请求到HTML并等待响应 /// /// 目标窗口ID /// 接口路径 /// JSON数据 /// 超时毫秒,0表示无限等待 public async Task Request(string windowId, string url, string jsonData, int timeoutMs = 0) { var requestId = Guid.NewGuid().ToString("N"); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _jsPendingRequests[requestId] = tcs; _requestWindowMap[requestId] = windowId; try { if (!HtmlMaskWindow.Exists(windowId) || !_toHtmlQueues.TryGetValue(windowId, out var queue)) throw new InvalidOperationException($"HTML遮罩窗口不存在或已关闭: {windowId}"); queue.Enqueue(new Message { Url = url, Data = ParseData(jsonData), RequestId = requestId }); HtmlMaskWindow.NotifyFlush(windowId); if (timeoutMs > 0) { using var cts = new System.Threading.CancellationTokenSource(timeoutMs); await using var registration = cts.Token.Register(() => { if (_jsPendingRequests.TryRemove(requestId, out var pending)) pending.TrySetResult(null!); }); return await tcs.Task; } return await tcs.Task; } catch (OperationCanceledException) { return null; } finally { _jsPendingRequests.TryRemove(requestId, out _); _requestWindowMap.TryRemove(requestId, out _); } } /// /// 等待接收来自HTML的一条消息 /// /// 窗口ID /// 超时毫秒,0表示无限等待 public async Task Receive(string windowId, int timeoutMs = 0) { if (!_fromHtmlQueues.TryGetValue(windowId, out var queue)) return null; var sw = System.Diagnostics.Stopwatch.StartNew(); while (true) { if (queue.TryDequeue(out var message)) return JsonSerializer.Serialize(message, _jsonOptions); if (_disposed || !_fromHtmlQueues.ContainsKey(windowId) || !HtmlMaskWindow.Exists(windowId)) return null; if (timeoutMs > 0 && sw.ElapsedMilliseconds > timeoutMs) return null; await Task.Delay(50); } } /// /// 轮询来自HTML的消息(非阻塞) /// public string? Poll(string windowId) { if (_fromHtmlQueues.TryGetValue(windowId, out var queue) && queue.TryDequeue(out var message)) { return JsonSerializer.Serialize(message, _jsonOptions); } return null; } /// /// 批量获取来自HTML的所有消息 /// public string PollAll(string windowId) { var messages = new List(); if (_fromHtmlQueues.TryGetValue(windowId, out var queue)) { while (queue.TryDequeue(out var message)) { messages.Add(message); } } return JsonSerializer.Serialize(messages, _jsonOptions); } #endregion #region 内部方法 /// /// 将待推送队列中的消息通过回调逐一发出 /// internal static void FlushPendingMessages(string windowId, Action postAction) { if (_toHtmlQueues.TryGetValue(windowId, out var queue)) { while (queue.TryDequeue(out var msg)) { postAction(JsonSerializer.Serialize(msg, _jsonOptions)); } } } /// /// HTML端发来的消息入队,如果是JS请求的响应则直接resolve /// internal static void SendFromHtml(string windowId, string url, string data, string? requestId = null) { // 匹配JS端pending的request if (requestId != null && _jsPendingRequests.TryRemove(requestId, out var tcs)) { var parsed = ParseData(data); tcs.TrySetResult(parsed != null ? parsed.Value.GetRawText() : "null"); return; } // 普通消息入队 if (_fromHtmlQueues.TryGetValue(windowId, out var queue)) { queue.Enqueue(new Message { Url = url, Data = ParseData(data), RequestId = requestId }); } } private static JsonElement? ParseData(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; try { using var doc = JsonDocument.Parse(json); return doc.RootElement.Clone(); } catch { // 不是合法JSON,作为纯文本包装 return JsonSerializer.SerializeToElement(json, _jsonOptions); } } private static void CleanupQueues(string windowId) { _toHtmlQueues.TryRemove(windowId, out _); _fromHtmlQueues.TryRemove(windowId, out _); // 取消该窗口关联的待响应请求 foreach (var kvp in _requestWindowMap) { if (kvp.Value == windowId && _jsPendingRequests.TryRemove(kvp.Key, out var tcs)) { tcs.TrySetCanceled(); } } } #endregion public void Dispose() { if (_disposed) return; _disposed = true; CloseAll(); } }