using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Interop;
using Vanara.PInvoke;
using static Vanara.PInvoke.User32;
using BetterGenshinImpact.Core.Script.Dependence;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.Common;
using Microsoft.Extensions.Logging;
using Microsoft.Web.WebView2.Core;
namespace BetterGenshinImpact.View;
///
/// HTML遮罩窗口 - 仅用于显示,不可交互(点击穿透)
///
public partial class HtmlMaskWindow : Window
{
private static readonly ConcurrentDictionary _windows = new();
private const int MaxWindows = 5;
private readonly string _id;
private readonly string _workDir;
private readonly string _webView2DataPath;
private readonly string _pageUrl;
private bool _navigationCompleted;
///
/// 窗口唯一标识
///
public string MaskId => _id;
private HtmlMaskWindow(string url, string? id, string workDir)
{
_id = id ?? Guid.NewGuid().ToString("N");
_workDir = workDir;
_webView2DataPath = Path.Combine(workDir, "WebView2Data");
_pageUrl = url;
InitializeComponent();
Loaded += OnLoaded;
InitializeAsync(url);
}
#region 静态窗口管理
///
/// 显示HTML遮罩窗口
///
public static string Show(string url, string? id, string workDir)
{
return Application.Current.Dispatcher.Invoke(() =>
{
// 指定ID时先关闭已有窗口
if (id != null && _windows.TryGetValue(id, out var existing))
{
existing.Close();
}
if (_windows.Count >= MaxWindows)
{
throw new InvalidOperationException($"最多同时打开 {MaxWindows} 个HTML遮罩窗口");
}
var window = new HtmlMaskWindow(url, id, workDir);
string wid = window.MaskId;
_windows[wid] = window;
window.Closed += (_, _) =>
{
_windows.TryRemove(wid, out _);
window.DisposeWebView();
};
window.Show();
return wid;
});
}
///
/// 关闭指定窗口
///
public static bool Close(string id)
{
if (_windows.TryGetValue(id, out var window))
{
window.Dispatcher.Invoke(() => window.Close());
return true;
}
return false;
}
///
/// 关闭所有窗口
///
public static void CloseAll()
{
foreach (var kvp in _windows)
{
kvp.Value.Dispatcher.Invoke(() => kvp.Value.Close());
}
}
///
/// 隐藏所有窗口(保留生命,不关闭)
///
public static void HideAll()
{
foreach (var kvp in _windows)
{
kvp.Value.Dispatcher.Invoke(() => kvp.Value.Hide());
}
}
///
/// 显示所有窗口
///
public static void ShowAll()
{
foreach (var kvp in _windows)
{
kvp.Value.Dispatcher.Invoke(() =>
{
kvp.Value.Show();
kvp.Value.UpdatePosition();
});
}
}
///
/// 同步所有窗口位置
///
public static void UpdateAllPositions()
{
foreach (var kvp in _windows)
{
kvp.Value.UpdatePosition();
}
}
///
/// 获取所有窗口ID
///
public static string[] GetWindowIds()
{
return _windows.Keys.ToArray();
}
///
/// 窗口是否存在
///
public static bool Exists(string id)
{
return _windows.ContainsKey(id);
}
///
/// 通知窗口刷新待推送的消息
///
internal static void NotifyFlush(string windowId)
{
if (!_windows.TryGetValue(windowId, out var window)) return;
window.Dispatcher.BeginInvoke(() =>
{
// 页面还没加载完,消息留在队列中由 NavigationCompleted 统一推送
if (!window._navigationCompleted) return;
if (window.WebView.CoreWebView2 == null) return;
HtmlMask.FlushPendingMessages(windowId, json =>
{
window.WebView.CoreWebView2.PostWebMessageAsString(json);
});
});
}
#endregion
private void OnLoaded(object sender, RoutedEventArgs e)
{
SetClickThrough();
UpdatePosition();
}
private async void InitializeAsync(string url)
{
try
{
await WebView.EnsureCoreWebView2Async(
await CoreWebView2Environment.CreateAsync(null, _webView2DataPath));
WebView.DefaultBackgroundColor = System.Drawing.Color.Transparent;
WebView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
WebView.CoreWebView2.Settings.IsScriptEnabled = true;
WebView.CoreWebView2.Settings.IsWebMessageEnabled = true;
// 拦截网络请求,仅允许注册过的域名
WebView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
WebView.CoreWebView2.WebResourceRequested += OnWebResourceRequested;
// 注入 helper JS,提供 window.htmlMask.request / onMessage API
await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@"
window.htmlMask = {
_callbacks: {},
_seq: 0,
request: function(url, data) {
return new Promise(function(resolve, reject) {
var id = '__req_' + (++window.htmlMask._seq);
window.htmlMask._callbacks[id] = { resolve: resolve, reject: reject };
window.chrome.webview.postMessage(JSON.stringify({
url: url,
data: data || {},
requestId: id
}));
});
},
onMessage: null,
_dispatch: function(raw) {
try {
var msg = JSON.parse(raw);
if (msg.requestId && window.htmlMask._callbacks[msg.requestId]) {
window.htmlMask._callbacks[msg.requestId].resolve(msg);
delete window.htmlMask._callbacks[msg.requestId];
} else if (window.htmlMask.onMessage) {
var result = window.htmlMask.onMessage(msg);
if (msg.requestId && result !== undefined) {
Promise.resolve(result).then(function(data) {
window.chrome.webview.postMessage(JSON.stringify({
requestId: msg.requestId,
url: '/__response__',
data: data
}));
});
}
}
} catch(e) {
if (window.htmlMask.onMessage) window.htmlMask.onMessage(raw);
}
}
};
window.chrome.webview.addEventListener('message', function(e) {
window.htmlMask._dispatch(e.data);
});
");
// 监听HTML发来的消息,解析 url + data + requestId
WebView.CoreWebView2.WebMessageReceived += (_, e) =>
{
try
{
string raw = e.TryGetWebMessageAsString();
string messageUrl = "";
string data = raw;
string? requestId = null;
try
{
using var doc = JsonDocument.Parse(raw);
var root = doc.RootElement;
if (root.TryGetProperty("url", out var ep))
{
messageUrl = ep.GetString() ?? "";
data = root.TryGetProperty("data", out var d) ? d.GetRawText() : "{}";
}
if (root.TryGetProperty("requestId", out var rid))
{
requestId = rid.GetString();
}
}
catch { }
HtmlMask.SendFromHtml(_id, messageUrl, data, requestId);
}
catch { }
};
// 页面加载完成后推送队列中待发送的消息
WebView.CoreWebView2.NavigationStarting += (_, _) =>
{
_navigationCompleted = false;
};
WebView.CoreWebView2.NavigationCompleted += (_, _) =>
{
_navigationCompleted = true;
HtmlMask.FlushPendingMessages(_id, json =>
{
WebView.CoreWebView2.PostWebMessageAsString(json);
});
};
if (!string.IsNullOrEmpty(url))
{
WebView.Source = new Uri(url);
}
}
catch (Exception e)
{
TaskControl.Logger.LogError($"WebView2 初始化失败: {e.Message}");
Dispatcher.Invoke(() => Close());
}
}
///
/// 拦截网络请求,仅允许 file://、data:// 和注册过的域名
///
private void OnWebResourceRequested(object? sender, CoreWebView2WebResourceRequestedEventArgs e)
{
try
{
var uri = new Uri(e.Request.Uri);
// 允许数据URI
if (uri.Scheme == "data") return;
// 本地文件:必须在脚本目录内
if (uri.Scheme == "file")
{
var localPath = Uri.UnescapeDataString(uri.AbsolutePath);
var fullDir = Path.GetFullPath(_workDir);
var fullFile = Path.GetFullPath(localPath);
if (fullFile.StartsWith(fullDir, StringComparison.OrdinalIgnoreCase)) return;
TaskControl.Logger.LogWarning("拦截HTML遮罩越级文件访问: {Uri}", e.Request.Uri);
e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Blocked", "");
return;
}
// 仅允许页面自身的初始导航
if (string.Equals(uri.AbsoluteUri, _pageUrl, StringComparison.OrdinalIgnoreCase)) return;
// HTTP/HTTPS 请求:与 JS 脚本使用完全一致的权限校验
var currentProject = TaskContext.Instance().CurrentScriptProject;
if (currentProject?.AllowJsHTTP != true)
{
TaskControl.Logger.LogWarning("未启用JS HTTP权限,拦截HTML遮罩网络请求: {Uri}", e.Request.Uri);
e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Blocked", "");
return;
}
var allowedUrls = currentProject?.Project?.Manifest.HttpAllowedUrls ?? [];
if (allowedUrls.Length == 0)
{
TaskControl.Logger.LogWarning("未配置 http_allowed_urls,拦截HTML遮罩网络请求: {Uri}", e.Request.Uri);
e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Blocked", "");
return;
}
if (allowedUrls.Any(allowedUrl =>
{
var pattern = "^" + Regex.Escape(allowedUrl).Replace("\\*", ".*") + "$";
return Regex.IsMatch(e.Request.Uri, pattern, RegexOptions.IgnoreCase);
})) return;
TaskControl.Logger.LogWarning("URL不在允许列表中,拦截HTML遮罩网络请求: {Uri}", e.Request.Uri);
e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Blocked", "");
}
catch (Exception ex)
{
TaskControl.Logger.LogWarning(ex, "HTML遮罩资源请求拦截异常");
}
}
///
/// 更新窗口位置
///
public void UpdatePosition()
{
try
{
var gameHandle = TaskContext.Instance().GameHandle;
if (gameHandle == IntPtr.Zero) return;
var currentRect = SystemControl.GetCaptureRect(gameHandle);
if (currentRect.Width <= 0 || currentRect.Height <= 0) return;
Dispatcher.Invoke(() =>
{
Left = currentRect.Left;
Top = currentRect.Top;
Width = currentRect.Width;
Height = currentRect.Height;
});
}
catch (Exception ex)
{
TaskControl.Logger.LogDebug(ex, "HTML遮罩窗口位置更新失败");
}
}
///
/// 设置点击穿透
///
private void SetClickThrough()
{
var hwnd = new WindowInteropHelper(this).Handle;
var style = (int)GetWindowLong(hwnd, WindowLongFlags.GWL_EXSTYLE);
User32.SetWindowLong(hwnd, WindowLongFlags.GWL_EXSTYLE,
(IntPtr)(style | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT));
}
///
/// 释放 WebView2 资源,停止媒体播放
///
private void DisposeWebView()
{
try
{
WebView.Dispose();
}
catch { }
}
}