using Avalonia.Controls; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Avalonia_PC.Views { public partial class MainWindow : Window { private const string AppScheme = "app"; //private const string? OnlineStartupUrl = "https://re.laitool.cn"; private const string? OnlineStartupUrl = null; private const string? LocalStartupPath = null; private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; private NativeWebView? _webView; private bool _eventsAttached; private object? _webViewAdapter; private HttpListener? _localHttpServer; private CancellationTokenSource? _localHttpServerCts; private string? _localHttpBaseUrl; private string? _localHttpRoot; #region 生命周期与 WebView 事件 /// /// 初始化窗口并注册生命周期事件。 /// public MainWindow() { InitializeComponent(); Opened += OnOpened; Closed += OnClosed; } /// /// 窗口打开后初始化 WebView、挂载事件并加载入口页面。 /// private async void OnOpened(object? sender, EventArgs e) { if (_eventsAttached) { return; } _webView = this.FindControl("WebView"); if (_webView is null) { return; } _eventsAttached = true; _webView.NavigationCompleted += OnNavigationCompleted; _webView.WebMessageReceived += OnWebMessageReceived; _webView.AdapterCreated += OnAdapterCreated; await LoadInitialContentAsync(); } /// /// WebView 适配器创建后缓存实例,用于后续打开开发者工具。 /// private void OnAdapterCreated(object? sender, WebViewAdapterEventArgs e) { _webViewAdapter = e.GetType().GetProperty("Adapter")?.GetValue(e); } /// /// 窗口关闭时解绑事件并释放本地资源。 /// private void OnClosed(object? sender, EventArgs e) { if (_webView is not null) { _webView.NavigationCompleted -= OnNavigationCompleted; _webView.WebMessageReceived -= OnWebMessageReceived; _webView.AdapterCreated -= OnAdapterCreated; } _webViewAdapter = null; StopLocalHttpServer(); } /// /// 页面导航完成后注入 JS 桥接脚本。 /// private async void OnNavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e) { await InjectBridgeScriptAsync(); } #endregion #region 前端桥接与页面加载 /// /// 接收前端消息并进行分发(打开调试工具 / 处理 app 请求)。 /// private async void OnWebMessageReceived(object? sender, WebMessageReceivedEventArgs e) { var messageJson = e.Body; if (string.IsNullOrWhiteSpace(messageJson)) { return; } AppResponse? response = null; try { using var document = JsonDocument.Parse(messageJson); var root = document.RootElement; if (!root.TryGetProperty("kind", out var kindProperty)) { return; } var kind = kindProperty.GetString(); if (string.Equals(kind, "app-open-devtools", StringComparison.OrdinalIgnoreCase)) { TryOpenDevTools(); return; } if (!string.Equals(kind, "app-request", StringComparison.OrdinalIgnoreCase)) { return; } response = await HandleAppRequestAsync(root); } catch (Exception ex) { response = new AppResponse { Kind = "app-response", Id = TryGetRequestId(messageJson), StatusCode = 500, StatusMessage = "Internal Server Error", Body = JsonSerializer.Serialize(new { success = false, error = ex.Message }), Headers = CreateJsonHeaders(), }; } if (_webView is not null && response is not null) { var responseJson = JsonSerializer.Serialize(response, BridgeJsonSerializerOptions); var responseJsonLiteral = JsonSerializer.Serialize(responseJson); await _webView.InvokeScript($"window.__dispatchAppResponse({responseJsonLiteral})"); } } /// /// 加载初始页面:优先在线地址,其次本地路径(通过本地 HTTP 服务托管)。 /// private async Task LoadInitialContentAsync() { if (_webView is null) { return; } var onlineUrl = GetConfiguredOnlineStartupUrl(); if (onlineUrl is not null) { StopLocalHttpServer(); _webView.Source = onlineUrl; return; } var localHtmlPath = GetConfiguredLocalStartupPath(); if (string.IsNullOrWhiteSpace(localHtmlPath) || !File.Exists(localHtmlPath)) { return; } var localRoot = Path.GetDirectoryName(localHtmlPath); if (string.IsNullOrWhiteSpace(localRoot)) { return; } await EnsureLocalHttpServerStartedAsync(localRoot); if (string.IsNullOrWhiteSpace(_localHttpBaseUrl)) { _webView.Source = new Uri(localHtmlPath); return; } _webView.Source = new Uri(new Uri(_localHttpBaseUrl), Path.GetFileName(localHtmlPath)); } /// /// 向页面注入桥接脚本,接管 app:// 请求并回传到 C# 处理。 /// private async Task InjectBridgeScriptAsync() { if (_webView is null) { return; } const string script = """ if (!window.__appBridgeInstalled) { window.__appBridgeInstalled = true; window.isWebView2 = true; const pending = new Map(); const tryOpenDevTools = () => { window.invokeCSharpAction(JSON.stringify({ kind: 'app-open-devtools' })); }; window.__dispatchAppResponse = function(jsonStr) { const payload = JSON.parse(jsonStr); const responseId = payload.id ?? payload.Id; const entry = pending.get(responseId); if (!entry) return; pending.delete(responseId); entry.resolve(new Response(payload.body ?? payload.Body ?? '', { status: payload.statusCode ?? payload.StatusCode ?? 200, statusText: payload.statusMessage ?? payload.StatusMessage ?? 'OK', headers: payload.headers ?? payload.Headers ?? { 'Content-Type': 'application/json' } })); }; const nativeFetch = window.fetch ? window.fetch.bind(window) : null; document.addEventListener('keydown', event => { if (event.key === 'F12' || (event.ctrlKey && event.shiftKey && (event.key === 'I' || event.key === 'i'))) { event.preventDefault(); tryOpenDevTools(); } }, true); document.addEventListener('contextmenu', event => { if (event.shiftKey) { event.preventDefault(); tryOpenDevTools(); } }, true); window.fetch = async (input, init) => { const request = input instanceof Request ? input : null; const requestUrl = typeof input === 'string' || input instanceof URL ? input.toString() : request?.url; if (!requestUrl || !requestUrl.startsWith('app://')) { if (!nativeFetch) throw new Error('window.fetch is not available.'); return nativeFetch(input, init); } const combinedHeaders = new Headers(request?.headers); if (init?.headers) { new Headers(init.headers).forEach((value, key) => combinedHeaders.set(key, value)); } const headers = {}; combinedHeaders.forEach((value, key) => headers[key] = value); let body = init?.body; if (body === undefined && request) { body = await request.clone().text(); } if (body && typeof body !== 'string') { body = await new Response(body).text(); } const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`; const responsePromise = new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { pending.delete(id); reject(new Error(`Timed out waiting for ${requestUrl}`)); }, 30000); pending.set(id, { resolve: response => { clearTimeout(timeoutId); resolve(response); }, reject: error => { clearTimeout(timeoutId); reject(error); } }); }); window.invokeCSharpAction(JSON.stringify({ kind: 'app-request', id, url: requestUrl, method: init?.method ?? request?.method ?? 'GET', headers, body: body ?? null })); return responsePromise; }; } """; await _webView.InvokeScript(script); } #endregion #region 请求分发与通用响应 /// /// 解析前端请求消息并转发到统一请求处理入口。 /// private async Task HandleAppRequestAsync(JsonElement request) { var id = request.TryGetProperty("id", out var idProperty) ? idProperty.GetString() : null; var url = request.TryGetProperty("url", out var urlProperty) ? urlProperty.GetString() : null; var method = request.TryGetProperty("method", out var methodProperty) ? methodProperty.GetString() : "GET"; var body = request.TryGetProperty("body", out var bodyProperty) ? bodyProperty.GetString() : null; var headers = ExtractHeaders(request); return await HandleAppRequestAsync(id, url, method, body, headers); } /// /// 统一请求处理:构建上下文、处理 OPTIONS、按前缀分发并封装标准响应。 /// private async Task HandleAppRequestAsync( string? id, string? rawUrl, string? method, string? body, Dictionary headers) { var response = new AppResponse { Kind = "app-response", Id = id, StatusCode = 200, StatusMessage = "OK", Headers = CreateJsonHeaders(), }; try { var uri = new Uri(rawUrl ?? throw new InvalidOperationException("请求地址不能为空。")); var requestContext = CreateRouteRequestContext(uri, body); var authorization = GetAuthorizationHeader(headers); _ = authorization; if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = 200; response.StatusMessage = "OK"; response.Body = JsonSerializer.Serialize(new { success = true }); return response; } var routeResult = await DispatchByPrefixAsync(requestContext); if (routeResult.IsMatched) { response.StatusCode = routeResult.StatusCode; response.StatusMessage = routeResult.StatusMessage; response.Body = BuildSuccessResponseBody(routeResult.Data); return response; } response.StatusCode = 404; response.StatusMessage = "Not Found"; response.Body = JsonSerializer.Serialize(new { success = false, error = "API not found" }); return response; } catch (Exception ex) { response.StatusCode = 500; response.StatusMessage = "Internal Server Error"; response.Body = JsonSerializer.Serialize(new { success = false, error = ex.Message }); return response; } } /// /// 按请求前缀分发处理器(例如 api、sys、admin 等)。 /// private async Task DispatchByPrefixAsync(RouteRequestContext requestContext) { if (requestContext.PathSegments.Length > 0 && string.Equals(requestContext.PathSegments[0], "api", StringComparison.OrdinalIgnoreCase)) { return await HandleApiPrefixAsync(requestContext); } return RouteDispatchResult.NotMatched(); } /// /// 处理 api 前缀下的具体业务路由。 /// private static async Task HandleApiPrefixAsync(RouteRequestContext requestContext) { if (string.Equals(requestContext.NormalizedPath, "api/getUser", StringComparison.OrdinalIgnoreCase)) { var user = await GetUserFromDatabaseAsync(); return RouteDispatchResult.Success(user); } if (string.Equals(requestContext.NormalizedPath, "api/processData", StringComparison.OrdinalIgnoreCase) || (requestContext.PathSegments.Length > 1 && string.Equals(requestContext.PathSegments[1], "processData", StringComparison.OrdinalIgnoreCase))) { var input = ExtractInput(requestContext); var result = await ProcessDataAsync(input); return RouteDispatchResult.Success(result); } return RouteDispatchResult.NotMatched(); } /// /// 统一构建成功响应体,保持前后端响应结构一致。 /// private static string BuildSuccessResponseBody(object? data) { return JsonSerializer.Serialize(new { success = true, data }); } /// /// 从 URI 解析路径段、查询参数和 body,构建路由上下文。 /// private static RouteRequestContext CreateRouteRequestContext(Uri uri, string? body) { var host = uri.Host ?? string.Empty; var absolutePath = uri.AbsolutePath ?? string.Empty; var combinedPath = $"{host}/{absolutePath}"; var pathSegments = combinedPath .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(Uri.UnescapeDataString) .ToArray(); var normalizedPath = string.Join('/', pathSegments); var query = ParseQueryParameters(uri.Query); return new RouteRequestContext { NormalizedPath = normalizedPath, PathSegments = pathSegments, Query = query, Body = body, }; } /// /// 解析查询字符串为忽略大小写的字典。 /// private static Dictionary ParseQueryParameters(string? queryString) { var query = new Dictionary(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(queryString)) { return query; } var raw = queryString.TrimStart('?'); foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries)) { var separatorIndex = pair.IndexOf('='); if (separatorIndex < 0) { query[Uri.UnescapeDataString(pair)] = string.Empty; continue; } var key = Uri.UnescapeDataString(pair[..separatorIndex]); var value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]); query[key] = value; } return query; } /// /// 按 body -> query -> path 的优先级提取业务输入参数。 /// private static string ExtractInput(RouteRequestContext requestContext) { if (!string.IsNullOrWhiteSpace(requestContext.Body)) { using var jsonDocument = JsonDocument.Parse(requestContext.Body); if (jsonDocument.RootElement.TryGetProperty("input", out var inputProperty)) { return inputProperty.GetString() ?? string.Empty; } } if (requestContext.Query.TryGetValue("input", out var inputFromQuery) && !string.IsNullOrWhiteSpace(inputFromQuery)) { return inputFromQuery; } if (requestContext.PathSegments.Length > 2) { return string.Join('/', requestContext.PathSegments.Skip(2)); } return string.Empty; } /// /// 创建桥接响应的默认 JSON/CORS 头。 /// private static Dictionary CreateJsonHeaders() => new() { ["Content-Type"] = "application/json; charset=utf-8", ["Access-Control-Allow-Origin"] = "*", ["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS", ["Access-Control-Allow-Headers"] = "Content-Type, Authorization", }; /// /// 从前端请求消息中提取请求头。 /// private static Dictionary ExtractHeaders(JsonElement request) { if (!request.TryGetProperty("headers", out var headersElement) || headersElement.ValueKind != JsonValueKind.Object) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var property in headersElement.EnumerateObject()) { headers[property.Name] = property.Value.GetString() ?? string.Empty; } return headers; } /// /// 获取授权头,供鉴权逻辑扩展使用。 /// private static string? GetAuthorizationHeader(Dictionary headers) { return headers.FirstOrDefault( entry => string.Equals(entry.Key, "Authorization", StringComparison.OrdinalIgnoreCase)).Value; } /// /// 在异常情况下尝试提取请求 id,确保前端可收到对应错误响应。 /// private static string? TryGetRequestId(string messageJson) { try { using var document = JsonDocument.Parse(messageJson); return document.RootElement.TryGetProperty("id", out var idProperty) ? idProperty.GetString() : null; } catch { return null; } } #endregion #region 页面地址配置与本地静态服务 /// /// 获取在线启动地址配置(仅允许 http/https)。 /// private static Uri? GetConfiguredOnlineStartupUrl() { if (string.IsNullOrWhiteSpace(OnlineStartupUrl)) { return null; } if (!Uri.TryCreate(OnlineStartupUrl, UriKind.Absolute, out var uri)) { return null; } return uri.Scheme is "http" or "https" ? uri : null; } /// /// 获取本地启动文件路径,未配置时默认使用输出目录 www/index.html。 /// private static string? GetConfiguredLocalStartupPath() { if (!string.IsNullOrWhiteSpace(LocalStartupPath)) { return Path.GetFullPath(LocalStartupPath); } return Path.Combine(AppContext.BaseDirectory, "www", "index.html"); } /// /// 确保本地 HTTP 静态服务已启动;根目录变化时会重启。 /// private async Task EnsureLocalHttpServerStartedAsync(string localRoot) { if (!string.IsNullOrWhiteSpace(_localHttpBaseUrl) && _localHttpServer is not null && string.Equals(_localHttpRoot, localRoot, StringComparison.OrdinalIgnoreCase)) { return; } StopLocalHttpServer(); var port = GetAvailableTcpPort(); var prefix = $"http://127.0.0.1:{port}/"; _localHttpServerCts = new CancellationTokenSource(); _localHttpServer = new HttpListener(); _localHttpServer.Prefixes.Add(prefix); _localHttpServer.Start(); _localHttpBaseUrl = prefix; _localHttpRoot = localRoot; _ = Task.Run(() => RunLocalHttpServerLoopAsync(_localHttpServer, _localHttpServerCts.Token, localRoot)); } /// /// 本地静态服务主循环,持续接收并分发请求。 /// private static async Task RunLocalHttpServerLoopAsync(HttpListener listener, CancellationToken cancellationToken, string wwwRoot) { try { while (!cancellationToken.IsCancellationRequested) { var context = await listener.GetContextAsync(); _ = Task.Run(() => HandleLocalHttpRequest(context, wwwRoot), cancellationToken); } } catch { } } /// /// 处理本地静态资源请求并返回文件内容。 /// private static async Task HandleLocalHttpRequest(HttpListenerContext context, string wwwRoot) { try { var relativePath = context.Request.Url?.AbsolutePath.TrimStart('/') ?? string.Empty; if (string.IsNullOrWhiteSpace(relativePath)) { relativePath = "index.html"; } relativePath = relativePath.Replace('/', Path.DirectorySeparatorChar); var fullPath = Path.GetFullPath(Path.Combine(wwwRoot, relativePath)); var fullRoot = Path.GetFullPath(wwwRoot); if (!fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase) || !File.Exists(fullPath)) { context.Response.StatusCode = 404; context.Response.Close(); return; } context.Response.ContentType = GetContentType(fullPath); await using var input = File.OpenRead(fullPath); context.Response.ContentLength64 = input.Length; await input.CopyToAsync(context.Response.OutputStream); context.Response.OutputStream.Close(); } catch { try { context.Response.StatusCode = 500; context.Response.Close(); } catch { } } } /// /// 根据后缀返回静态资源 Content-Type。 /// private static string GetContentType(string filePath) { return Path.GetExtension(filePath).ToLowerInvariant() switch { ".html" => "text/html; charset=utf-8", ".js" => "application/javascript; charset=utf-8", ".css" => "text/css; charset=utf-8", ".json" => "application/json; charset=utf-8", _ => "application/octet-stream", }; } /// /// 获取一个可用本地端口,用于启动本地静态服务。 /// private static int GetAvailableTcpPort() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } /// /// 尝试打开 WebView 开发者工具(兼容不同适配器方法名)。 /// private void TryOpenDevTools() { if (_webViewAdapter is null) { return; } var adapterType = _webViewAdapter.GetType(); var method = adapterType.GetMethod("OpenDevTools", BindingFlags.Public | BindingFlags.Instance) ?? adapterType.GetMethod("ShowDevTools", BindingFlags.Public | BindingFlags.Instance); method?.Invoke(_webViewAdapter, null); } /// /// 停止并释放本地静态服务资源。 /// private void StopLocalHttpServer() { try { _localHttpServerCts?.Cancel(); _localHttpServer?.Stop(); _localHttpServer?.Close(); } catch { } finally { _localHttpServerCts?.Dispose(); _localHttpServerCts = null; _localHttpServer = null; _localHttpBaseUrl = null; _localHttpRoot = null; } } #endregion #region 业务示例方法 /// /// 示例:模拟读取用户数据。 /// private static async Task GetUserFromDatabaseAsync() { await Task.Delay(100); return new { id = 1, name = "张三", email = "zhangsan@example.com" }; } /// /// 示例:模拟处理输入数据。 /// private static async Task ProcessDataAsync(string? input) { await Task.Delay(200); return $"Processed: {input?.ToUpperInvariant()}"; } #endregion #region DTO / 路由上下文模型 private sealed class AppResponse { public string Kind { get; set; } = string.Empty; public string? Id { get; set; } public int StatusCode { get; set; } public string StatusMessage { get; set; } = string.Empty; public string Body { get; set; } = string.Empty; public Dictionary Headers { get; set; } = new(); } private sealed class RouteRequestContext { public string NormalizedPath { get; init; } = string.Empty; public string[] PathSegments { get; init; } = []; public Dictionary Query { get; init; } = new(StringComparer.OrdinalIgnoreCase); public string? Body { get; init; } } private sealed class RouteDispatchResult { public bool IsMatched { get; init; } public int StatusCode { get; init; } = 200; public string StatusMessage { get; init; } = "OK"; public object? Data { get; init; } public static RouteDispatchResult Success(object? data) { return new RouteDispatchResult { IsMatched = true, Data = data, }; } public static RouteDispatchResult NotMatched() { return new RouteDispatchResult { IsMatched = false, }; } } #endregion } }