添加 HTTP 服务器配置和服务 (#1873)

This commit is contained in:
辉鸭蛋
2025-07-14 00:58:24 +08:00
committed by GitHub
parent 900b6aec84
commit daa4aea7a8
7 changed files with 445 additions and 1 deletions

View File

@@ -79,6 +79,9 @@ public partial class App : Application
services.AddNavigationViewPageProvider();
// App Host
services.AddHostedService<ApplicationHostService>();
// HTTP Server Service
services.AddHostedService<HttpServerService>();
// Page resolver service
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IUpdateService, UpdateService>();

View File

@@ -49,6 +49,8 @@
<PackageReference Include="Emoji.Wpf" Version="0.3.4" />
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
<PackageReference Include="Meziantou.Framework.Win32.CredentialManager" Version="1.7.4" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.4" />

View File

@@ -223,6 +223,11 @@ public partial class AllConfig : ObservableObject
/// </summary>
public HardwareAccelerationConfig HardwareAccelerationConfig { get; set; } = new();
/// <summary>
/// HTTP 服务器配置
/// </summary>
public HttpServerConfig HttpServerConfig { get; set; } = new();
[JsonIgnore]
public Action? OnAnyChangedAction { get; set; }
@@ -253,6 +258,7 @@ public partial class AllConfig : ObservableObject
PathingConditionConfig.PropertyChanged += OnAnyPropertyChanged;
DevConfig.PropertyChanged += OnAnyPropertyChanged;
HardwareAccelerationConfig.PropertyChanged += OnAnyPropertyChanged;
HttpServerConfig.PropertyChanged += OnAnyPropertyChanged;
}
public void OnAnyPropertyChanged(object? sender, EventArgs args)

View File

@@ -0,0 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace BetterGenshinImpact.Core.Config;
/// <summary>
/// HTTP 服务器配置
/// </summary>
[Serializable]
public partial class HttpServerConfig : ObservableObject
{
/// <summary>
/// 是否启用 HTTP 服务器
/// </summary>
[ObservableProperty]
private bool _enabled = false;
/// <summary>
/// HTTP 服务器端口
/// </summary>
[ObservableProperty]
private int _port = 30648;
/// <summary>
/// 是否启用 CORS
/// </summary>
[ObservableProperty]
private bool _enableCors = true;
/// <summary>
/// 是否启用 WebSocket 支持
/// </summary>
[ObservableProperty]
private bool _enableWebSocket = true;
/// <summary>
/// API 访问令牌(可选,用于安全验证)
/// </summary>
[ObservableProperty]
private string _accessToken = Guid.NewGuid().ToString("N");
// /// <summary>
// /// 是否启用 Swagger 文档
// /// </summary>
// [ObservableProperty]
// private bool _enableSwagger = true;
/// <summary>
/// 监听地址
/// </summary>
[ObservableProperty]
private string _host = "localhost";
}

View File

@@ -95,6 +95,6 @@ public static class MirrorChyanHelper
private static void OpenMirrorChyanWebsite()
{
Launcher.LaunchUriAsync(new Uri($"https://mirrorchyan.com/zh/get-start?source=bgi-{Global.Version}"));
Launcher.LaunchUriAsync(new Uri($"https://mirrorchyan.com/zh/get-start?source=bgi-desktop-{Global.Version}"));
}
}

View File

@@ -0,0 +1,244 @@
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.Service.Interface;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace BetterGenshinImpact.Service;
/// <summary>
/// HTTP 服务器服务
/// </summary>
public class HttpServerService : IHostedService, IDisposable
{
private readonly ILogger<HttpServerService> _logger;
private readonly IServiceProvider _serviceProvider;
private WebApplication? _webApp;
private readonly HttpServerConfig _config;
private CancellationTokenSource? _cancellationTokenSource;
public HttpServerService(
ILogger<HttpServerService> logger,
IConfigService configService,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
_config = configService.Get().HttpServerConfig;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_config.Enabled)
{
_logger.LogInformation("HTTP 服务器未启用");
return;
}
_cancellationTokenSource = new CancellationTokenSource();
await StartWebServer();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_webApp != null)
{
await _webApp.StopAsync(cancellationToken);
await _webApp.DisposeAsync();
_webApp = null;
}
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_logger.LogInformation("HTTP 服务器已停止");
}
private async Task StartWebServer()
{
try
{
var builder = WebApplication.CreateBuilder();
// 配置 Kestrel 服务器
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(_config!.Port);
});
// 添加服务
builder.Services.AddCors();
if (_config!.EnableWebSocket)
{
builder.Services.AddSignalR();
}
// if (_config.EnableSwagger)
// {
// builder.Services.AddEndpointsApiExplorer();
// // builder.Services.AddSwaggerGen();
// }
// 添加现有服务的引用
builder.Services.AddSingleton(_serviceProvider.GetService<IConfigService>()!);
_webApp = builder.Build();
// 配置中间件
if (_config.EnableCors)
{
_webApp.UseCors(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
}
// if (_config.EnableSwagger)
// {
// // _webApp.UseSwagger();
// // _webApp.UseSwaggerUI();
// }
// 配置路由
ConfigureRoutes(_webApp);
if (_config.EnableWebSocket)
{
_webApp.MapHub<BgiHub>("/bgi-hub");
}
// 启动服务器
await _webApp.StartAsync(_cancellationTokenSource!.Token);
var url = $"http://{_config.Host}:{_config.Port}";
_logger.LogInformation("HTTP 服务器已启动: {Url}", url);
}
catch (Exception ex)
{
_logger.LogError(ex, "启动 HTTP 服务器失败");
throw;
}
}
private void ConfigureRoutes(WebApplication app)
{
// 健康检查
app.MapGet("/health", () => new { status = "healthy", timestamp = DateTime.UtcNow })
.WithTags("System")
.WithSummary("健康检查");
// 获取应用状态
app.MapGet("/api/status", () =>
{
})
.WithTags("Application")
.WithSummary("获取应用状态");
// 获取配置
app.MapGet("/api/config", (IConfigService configService) =>
{
var config = configService.Get();
return Results.Json(config, ConfigService.JsonOptions);
})
.WithTags("Configuration")
.WithSummary("获取应用配置");
// 更新配置(仅部分配置)
app.MapPost("/api/config", async (HttpContext context, IConfigService configService) =>
{
try
{
var jsonDoc = await JsonDocument.ParseAsync(context.Request.Body);
var config = configService.Get();
configService.Save();
return Results.Ok(new { message = "配置更新成功" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
})
.WithTags("Configuration")
.WithSummary("更新应用配置");
// 启动/停止任务
app.MapPost("/api/tasks/{action}", (string action) =>
{
})
.WithTags("Tasks")
.WithSummary("启动或停止任务");
// 获取任务列表(脚本)
app.MapGet("/api/scripts", () =>
{
// 这里可以返回可用的脚本列表
// 具体实现根据你的脚本管理逻辑
return Results.Ok(new { scripts = new List<object>() });
})
.WithTags("Scripts")
.WithSummary("获取可用脚本列表");
// // 静态文件服务(可选)
// app.MapGet("/", () => Results.Redirect("/swagger"));
}
public void Dispose()
{
_webApp?.DisposeAsync().AsTask().Wait();
_cancellationTokenSource?.Dispose();
}
}
/// <summary>
/// SignalR Hub用于 WebSocket 通信
/// </summary>
public class BgiHub : Hub
{
private readonly ILogger<BgiHub> _logger;
public BgiHub(ILogger<BgiHub> logger)
{
_logger = logger;
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation("WebSocket 客户端已连接: {ConnectionId}", Context.ConnectionId);
await Groups.AddToGroupAsync(Context.ConnectionId, "BgiClients");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("WebSocket 客户端已断开: {ConnectionId}", Context.ConnectionId);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "BgiClients");
await base.OnDisconnectedAsync(exception);
}
/// <summary>
/// 向所有连接的客户端发送消息
/// </summary>
public async Task BroadcastMessage(string message, object? data = null)
{
await Clients.Group("BgiClients").SendAsync("ReceiveMessage", new
{
message,
data,
timestamp = DateTime.UtcNow
});
}
}

View File

@@ -477,6 +477,142 @@
<ui:ToggleSwitch Margin="0,0,36,0" IsChecked="{Binding Config.CommonConfig.ExitToTray, Mode=TwoWay}" />
</ui:CardControl>
<!-- HTTP服务器设置 -->
<ui:CardExpander Margin="0,0,0,12" ContentPadding="0" Icon="{ui:SymbolIcon Globe24}">
<ui:CardExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="BetterGI HTTP 服务器设置"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="可供外部程序调用,配置修改需重启后生效"
TextWrapping="Wrap" />
<ui:ToggleSwitch Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Margin="0,0,24,0"
IsChecked="{Binding Config.HttpServerConfig.Enabled, Mode=TwoWay}" />
</Grid>
</ui:CardExpander.Header>
<StackPanel>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="端口号"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="HTTP服务器监听的端口号范围1-65535"
TextWrapping="Wrap" />
<ui:TextBox Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
MinWidth="90"
Margin="0,0,36,0"
Text="{Binding Config.HttpServerConfig.Port, Mode=TwoWay,
ValidatesOnNotifyDataErrors=True}" />
</Grid>
<!--<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="启用跨域请求(CORS)"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="允许来自不同域名的Web应用访问API"
TextWrapping="Wrap" />
<ui:ToggleSwitch Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Margin="0,0,36,0"
IsChecked="{Binding Config.HttpServerConfig.EnableCors, Mode=TwoWay}" />
</Grid>-->
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="API访问令牌"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="可选的安全验证令牌,留空则不启用验证"
TextWrapping="Wrap" />
<ui:TextBox Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
MinWidth="170"
Margin="0,0,36,0"
Text="{Binding Config.HttpServerConfig.AccessToken, Mode=TwoWay}" />
</Grid>
<!--<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="启用WebSocket支持"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="启用WebSocket实时双向通信功能"
TextWrapping="Wrap" />
<ui:ToggleSwitch Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Margin="0,0,36,0"
IsChecked="{Binding Config.HttpServerConfig.EnableWebSocket, Mode=TwoWay}" />
</Grid>-->
</StackPanel>
</ui:CardExpander>
<ui:TextBlock Margin="0,0,0,8"
FontTypography="BodyStrong"