using Avalonia.Controls; using FileShare_Common.Infrastructure; 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; using FileShare_Services.Services.FileLibrary; using Microsoft.Extensions.DependencyInjection; namespace FileShare_PC.Views { /// /// 主窗口,承载 WebView2 控件并管理前后端 Bridge 通信。 /// public partial class MainWindow : Window { /// /// 自定义协议方案名称。 /// private const string AppScheme = "app"; /// /// 在线模式下的前端启动 URL。 /// private const string? OnlineStartupUrl = "http://localhost:51240"; //private const string? OnlineStartupUrl = null; /// /// 离线模式下的前端本地文件路径,为空则使用在线模式。 /// private const string? LocalStartupPath = null; private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; /// /// WebView2 原生控件实例。 /// private NativeWebView? _webView; /// /// 标记 WebView 事件是否已绑定。 /// private bool _eventsAttached; /// /// WebView 适配器对象。 /// private object? _webViewAdapter; /// /// 本地 HTTP 服务器实例(离线模式)。 /// private HttpListener? _localHttpServer; /// /// 本地 HTTP 服务器的取消令牌源。 /// private CancellationTokenSource? _localHttpServerCts; /// /// 本地 HTTP 服务器的基础 URL。 /// private string? _localHttpBaseUrl; /// /// 本地 HTTP 服务器的根目录路径。 /// private string? _localHttpRoot; #region 生命周期与 WebView 事件 /// /// 初始化窗口并注册生命周期事件。 /// public MainWindow(IServiceProvider services) { _services = services; InitializeComponent(); Opened += OnOpened; Closed += OnClosed; RegisterRoutes(); } /// /// 窗口打开后初始化 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) { AppLog.Error(ex, "Bridge AppRequest 处理失败"); 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 localHtmlPath = GetConfiguredLocalStartupPath(); var localRoot = !string.IsNullOrWhiteSpace(localHtmlPath) ? Path.GetDirectoryName(localHtmlPath) : null; if (!string.IsNullOrWhiteSpace(localRoot)) { await EnsureLocalHttpServerStartedAsync(localRoot); } var onlineUrl = GetConfiguredOnlineStartupUrl(); if (onlineUrl is not null) { _webView.Source = onlineUrl; return; } if (string.IsNullOrWhiteSpace(localHtmlPath) || !File.Exists(localHtmlPath)) { return; } 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; } await _webView.InvokeScript(BridgeScript); if (!string.IsNullOrWhiteSpace(_localHttpBaseUrl)) { var mediaOriginLiteral = JsonSerializer.Serialize(_localHttpBaseUrl.TrimEnd('/')); await _webView.InvokeScript($"window.__pcMediaOrigin = {mediaOriginLiteral}"); } } #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("请求地址不能为空。")); if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = 200; response.StatusMessage = "OK"; response.Body = JsonSerializer.Serialize(new { success = true }); return response; } // 使用统一端点适配器处理请求 var (normalizedPath, queryParams) = ParseRequestUri(uri); var routeResult = await _endpointAdapter.HandleRequestAsync( path: normalizedPath, method: method ?? "GET", body: body, headers: headers, query: queryParams); if (routeResult.IsMatched) { response.StatusCode = routeResult.StatusCode; response.StatusMessage = routeResult.StatusMessage; response.Body = BuildSuccessResponseBody(routeResult.Data); foreach (var kvp in routeResult.ResponseHeaders) { response.Headers[kvp.Key] = kvp.Value; } 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) { AppLog.Error(ex, "本地 HTTP 请求处理失败"); response.StatusCode = 500; response.StatusMessage = "Internal Server Error"; response.Body = JsonSerializer.Serialize(new { success = false, error = ex.Message }); return response; } } /// /// 从 URI 解析规范化路径和查询参数(供统一端点适配器使用)。 /// private static (string normalizedPath, Dictionary query) ParseRequestUri(Uri uri) { 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 (normalizedPath, query); } /// /// 统一构建成功响应体,保持前后端响应结构一致。 /// private static string BuildSuccessResponseBody(object? data) { return JsonSerializer.Serialize(new { success = true, data }); } /// /// 解析查询字符串为忽略大小写的字典。 /// 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; } /// /// 创建桥接响应的默认 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 (Exception ex) { AppLog.Information("解析 Bridge 请求 ID 失败: {Error}", ex.Message); 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 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 (Exception ex) when (ex is not OperationCanceledException) { AppLog.Error(ex, "本地 HTTP 服务循环异常退出"); } } /// /// 处理本地静态资源请求并返回文件内容。 /// private async Task HandleLocalHttpRequest(HttpListenerContext context, string wwwRoot) { try { if (await TryHandleLocalMediaStreamAsync(context) || await TryHandleLocalThumbnailAsync(context)) { return; } 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 (Exception ex) { AppLog.Error(ex, "本地静态文件请求处理失败"); try { context.Response.StatusCode = 500; context.Response.Close(); } catch (Exception closeEx) { AppLog.Warning("关闭 500 响应失败: {Error}", closeEx.Message); } } } /// /// Handle media element requests using the same stream path as FileShare-API. /// private async Task TryHandleLocalMediaStreamAsync(HttpListenerContext context) { var request = context.Request; var response = context.Response; var segments = (request.Url?.AbsolutePath ?? string.Empty) .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (segments.Length != 4 || !string.Equals(segments[0], "api", StringComparison.OrdinalIgnoreCase) || !string.Equals(segments[1], "files", StringComparison.OrdinalIgnoreCase) || !string.Equals(segments[3], "stream", StringComparison.OrdinalIgnoreCase)) { return false; } AddLocalMediaHeaders(response); if (string.Equals(request.HttpMethod, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = (int)HttpStatusCode.NoContent; response.Close(); return true; } if (!string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase) && !string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; response.Close(); return true; } if (!int.TryParse(segments[2], out var id) || id <= 0) { response.StatusCode = (int)HttpStatusCode.NotFound; response.Close(); return true; } using var scope = _services.CreateScope(); var fileStreamService = scope.ServiceProvider.GetService(); var fileResponse = fileStreamService is null ? null : await fileStreamService.GetFileStreamAsync(id); if (fileResponse is null || !File.Exists(fileResponse.FilePath)) { response.StatusCode = (int)HttpStatusCode.NotFound; response.Close(); return true; } var length = new FileInfo(fileResponse.FilePath).Length; var start = 0L; var end = length > 0 ? length - 1 : 0; var isRange = TryParseByteRange(request.Headers["Range"], length, out start, out end); if (!string.IsNullOrWhiteSpace(request.Headers["Range"]) && !isRange) { response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable; response.Headers["Content-Range"] = $"bytes */{length}"; response.Close(); return true; } var contentLength = length == 0 ? 0 : end - start + 1; response.StatusCode = isRange ? (int)HttpStatusCode.PartialContent : (int)HttpStatusCode.OK; response.ContentType = fileResponse.ContentType; response.ContentLength64 = contentLength; response.Headers["Accept-Ranges"] = "bytes"; response.Headers["Cache-Control"] = "public, max-age=3600"; response.Headers["Content-Disposition"] = $"inline; filename=\"{Uri.EscapeDataString(fileResponse.FileName)}\""; response.Headers["Last-Modified"] = fileResponse.LastModified.ToUniversalTime().ToString("R"); if (isRange) { response.Headers["Content-Range"] = $"bytes {start}-{end}/{length}"; } if (string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase) || contentLength == 0) { response.Close(); return true; } await using var input = File.Open(fileResponse.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); input.Seek(start, SeekOrigin.Begin); await CopyRangeAsync(input, response.OutputStream, contentLength); response.OutputStream.Close(); return true; } private async Task TryHandleLocalThumbnailAsync(HttpListenerContext context) { var request = context.Request; var response = context.Response; var segments = (request.Url?.AbsolutePath ?? string.Empty) .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (segments.Length != 3 || !string.Equals(segments[0], "api", StringComparison.OrdinalIgnoreCase) || !string.Equals(segments[1], "thumbnails", StringComparison.OrdinalIgnoreCase)) { return false; } AddLocalMediaHeaders(response); if (!string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase) && !string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; response.Close(); return true; } if (!int.TryParse(segments[2], out var id) || id <= 0) { response.StatusCode = (int)HttpStatusCode.NotFound; response.Close(); return true; } using var scope = _services.CreateScope(); var thumbnailService = scope.ServiceProvider.GetService(); var thumbnail = thumbnailService is null ? null : await thumbnailService.GetThumbnailAsync(id); if (thumbnail is null || !File.Exists(thumbnail.FilePath)) { response.StatusCode = (int)HttpStatusCode.NotFound; response.Close(); return true; } var fileInfo = new FileInfo(thumbnail.FilePath); response.StatusCode = (int)HttpStatusCode.OK; response.ContentType = thumbnail.ContentType; response.ContentLength64 = fileInfo.Length; response.Headers["Cache-Control"] = "public, max-age=3600"; response.Headers["Content-Disposition"] = $"inline; filename=\"{Uri.EscapeDataString(thumbnail.FileName)}\""; response.Headers["Last-Modified"] = thumbnail.LastModified.ToUniversalTime().ToString("R"); if (string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase) || fileInfo.Length == 0) { response.Close(); return true; } await using var input = File.Open(thumbnail.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); await input.CopyToAsync(response.OutputStream); response.OutputStream.Close(); return true; } /// /// 为媒体流响应添加 CORS 和 Range 请求头,允许浏览器跨域访问和分段请求。 /// /// HTTP 响应对象。 private static void AddLocalMediaHeaders(HttpListenerResponse response) { response.Headers["Access-Control-Allow-Origin"] = "*"; response.Headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS"; response.Headers["Access-Control-Allow-Headers"] = "Range, Content-Type, Authorization"; response.Headers["Access-Control-Expose-Headers"] = "Accept-Ranges, Content-Length, Content-Range"; } /// /// 解析 HTTP Range 请求头中的 bytes=start-end 格式,支持两端省略和后缀长度(如 bytes=-500)。 /// /// Range 请求头值。 /// 资源总字节数。 /// 解析出的起始字节偏移。 /// 解析出的结束字节偏移。 /// 解析成功返回 true,否则 false。 private static bool TryParseByteRange(string? value, long length, out long start, out long end) { start = 0; end = length > 0 ? length - 1 : 0; if (length <= 0 || string.IsNullOrWhiteSpace(value) || !value.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase)) { return false; } var range = value["bytes=".Length..].Split(',', 2)[0].Trim(); var separatorIndex = range.IndexOf('-'); if (separatorIndex < 0) { return false; } var startValue = range[..separatorIndex]; var endValue = range[(separatorIndex + 1)..]; if (string.IsNullOrWhiteSpace(startValue)) { if (!long.TryParse(endValue, out var suffixLength) || suffixLength <= 0) { return false; } start = Math.Max(0, length - suffixLength); end = length - 1; return true; } if (!long.TryParse(startValue, out start) || start < 0 || start >= length) { return false; } if (!string.IsNullOrWhiteSpace(endValue) && (!long.TryParse(endValue, out end) || end < start)) { return false; } end = Math.Min(end, length - 1); return true; } /// /// 从输入流读取指定字节数并写入输出流,用于实现 HTTP Range 分段响应。 /// /// 源文件流。 /// HTTP 响应输出流。 /// 需要传输的剩余字节数。 private static async Task CopyRangeAsync(Stream input, Stream output, long bytesRemaining) { var buffer = new byte[64 * 1024]; while (bytesRemaining > 0) { var readLength = (int)Math.Min(buffer.Length, bytesRemaining); var read = await input.ReadAsync(buffer.AsMemory(0, readLength)); if (read <= 0) { break; } await output.WriteAsync(buffer.AsMemory(0, read)); bytesRemaining -= read; } } /// /// 根据后缀返回静态资源 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 (Exception ex) { AppLog.Warning("停止本地 HTTP 服务时出错: {Error}", ex.Message); } finally { _localHttpServerCts?.Dispose(); _localHttpServerCts = null; _localHttpServer = null; _localHttpBaseUrl = null; _localHttpRoot = null; } } #endregion #region DTO / 路由上下文模型 /// /// Bridge 通信响应 DTO,用于序列化返回给前端的数据。 /// private sealed class AppResponse { /// /// 获取或设置响应类型标识。 /// public string Kind { get; set; } = string.Empty; /// /// 获取或设置请求 ID(对应前端请求)。 /// public string? Id { get; set; } /// /// 获取或设置 HTTP 状态码。 /// public int StatusCode { get; set; } /// /// 获取或设置状态描述文本。 /// public string StatusMessage { get; set; } = string.Empty; /// /// 获取或设置响应体 JSON 字符串。 /// public string Body { get; set; } = string.Empty; /// /// 获取或设置响应头字典。 /// public Dictionary Headers { get; set; } = new(); } #endregion } }