commit e3fe965f108e748a0bfa13f6f122d650d73575d9 Author: luoqian <2769838458@qq.com> Date: Thu May 21 15:52:36 2026 +0800 init diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..9755770 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.7", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15db508 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +################################################################################ +# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。 +################################################################################ + +/Avalonia-PC/bin +/Avalonia-PC/.vs +/Avalonia-PC/obj +/Avalonia-API/bin +/Avalonia-API/obj +/Avalonia-Services/bin +/Avalonia-Services/obj +/Avalonia-Web-VUE/.vscode +/Avalonia-Web-VUE/obj +/Avalonia-Web-VUE/node_modules +/Avalonia-Web-VUE/dist +/Avalonia-Web-VUE/.vscode +/avalonia-web-react/obj +/avalonia-web-react/obj +/avalonia-web-react/node_modules +/avalonia-web-react/dist +/Avalonia-EFCore/bin +/Avalonia-EFCore/obj +/Avalonia-Common/bin +/Avalonia-Common/obj +/Avalonia-API/logs +/Avalonia-API/avalonia-api.db +/Avalonia-API/avalonia-api.db-shm +/Avalonia-API/avalonia-api.db-wal +/package-output +/package-scripts/tools diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7c24122 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "chat.tools.terminal.autoApprove": { + "ForEach-Object": true, + "dotnet list": true, + "dotnet build": true + } +} \ No newline at end of file diff --git a/Avalonia-API/Authentication/ApiAuthEndpointService.cs b/Avalonia-API/Authentication/ApiAuthEndpointService.cs new file mode 100644 index 0000000..8c97530 --- /dev/null +++ b/Avalonia-API/Authentication/ApiAuthEndpointService.cs @@ -0,0 +1,160 @@ +using Avalonia_Common.Core; +using Avalonia_EFCore.Database; +using Avalonia_EFCore.Models; +using Avalonia_Services.Core; +using Avalonia_Services.Services.AuthService; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace Avalonia_API.Authentication +{ + /// + /// API 鉴权端点服务,实现 , + /// 处理登录、刷新 Token 和登出操作,使用 JWT 与 Refresh Token 机制。 + /// + public sealed class ApiAuthEndpointService( + AppDataContext db, + JwtTokenService jwtTokenService, + RefreshTokenService refreshTokenService) : IApiAuthEndpointService + { + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + /// + /// 处理用户登录请求。根据账号(邮箱或用户名)查找或创建用户, + /// 生成 JWT Access Token 和 Refresh Token 并返回。 + /// + /// 服务端点上下文,包含请求体、请求头等信息。 + /// 包含 AccessToken、RefreshToken 及过期时间的认证响应。 + public async Task LoginAsync(ServiceEndpointContext ctx) + { + var request = Deserialize(ctx.Body); + if (string.IsNullOrWhiteSpace(request?.Account)) + { + ctx.StatusCode = 400; + return ResponseHelper.Failure(400, "账号不能为空"); + } + + var user = await db.Users.FirstOrDefaultAsync( + x => x.Email == request.Account || x.Name == request.Account); + + if (user is null) + { + user = new UserEntity + { + Name = request.Account, + Email = request.Account.Contains('@') ? request.Account : null, + }; + db.Users.Add(user); + await db.SaveChangesAsync(); + } + + var roles = NormalizeRoles(request.Roles); + var accessToken = jwtTokenService.CreateAccessToken(user, roles); + var refreshToken = await refreshTokenService.CreateAsync( + user.Id, + ctx.GetHeader("User-Agent"), + GetRemoteIpAddress(ctx)); + + return ResponseHelper.Ok(new AuthTokenResponse( + accessToken.Token, + refreshToken.Token, + accessToken.ExpiresAt, + refreshToken.Entity.ExpiresAt, + roles), "登录成功"); + } + + /// + /// 使用 Refresh Token 轮换新的 Access Token 和 Refresh Token。 + /// 旧的 Refresh Token 会被撤销并替换。 + /// + /// 服务端点上下文,包含请求体中的 RefreshToken。 + /// 新的 Token 对;若 Refresh Token 无效则返回 401 错误。 + public async Task RefreshAsync(ServiceEndpointContext ctx) + { + var request = Deserialize(ctx.Body); + var rotated = await refreshTokenService.RotateAsync( + request?.RefreshToken, + ctx.GetHeader("User-Agent"), + GetRemoteIpAddress(ctx)); + + if (rotated is null) + { + ctx.StatusCode = 401; + return ResponseHelper.Failure(401, "刷新 token 无效或已过期"); + } + + var user = await db.Users.FindAsync(rotated.Value.Entity.UserId); + if (user is null) + { + ctx.StatusCode = 401; + return ResponseHelper.Failure(401, "用户不存在"); + } + + var roles = new[] { "Admin" }; + var accessToken = jwtTokenService.CreateAccessToken(user, roles); + + return ResponseHelper.Ok(new AuthTokenResponse( + accessToken.Token, + rotated.Value.Token, + accessToken.ExpiresAt, + rotated.Value.Entity.ExpiresAt, + roles), "刷新成功"); + } + + /// + /// 处理用户登出请求,撤销指定的 Refresh Token。 + /// + /// 服务端点上下文,包含请求体中的 RefreshToken。 + /// 登出成功的响应。 + public async Task LogoutAsync(ServiceEndpointContext ctx) + { + var request = Deserialize(ctx.Body); + await refreshTokenService.RevokeAsync(request?.RefreshToken); + return ResponseHelper.Succeed("退出成功"); + } + + /// + /// 将 JSON 请求体反序列化为指定类型。 + /// + /// 目标类型。 + /// JSON 请求体字符串,可为空。 + /// 反序列化后的对象;若 body 为空则返回默认值。 + private static T? Deserialize(string? body) + { + return string.IsNullOrWhiteSpace(body) + ? default + : JsonSerializer.Deserialize(body, JsonOptions); + } + + /// + /// 从上下文的 Items 中提取 ASP.NET Core HttpContext,并获取客户端远程 IP 地址。 + /// + /// 服务端点上下文。 + /// 客户端 IP 地址字符串;若无法获取则返回 null。 + private static string? GetRemoteIpAddress(ServiceEndpointContext ctx) + { + return ctx.Items.TryGetValue("HttpContext", out var value) && value is HttpContext httpContext + ? httpContext.Connection.RemoteIpAddress?.ToString() + : null; + } + + /// + /// 规范化角色数组:去空白、去重(忽略大小写),为空时默认返回 Admin 角色。 + /// + /// 原始角色数组,可为 null。 + /// 规范化后的角色数组。 + private static string[] NormalizeRoles(string[]? roles) + { + var normalized = roles? + .Where(role => !string.IsNullOrWhiteSpace(role)) + .Select(role => role.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + return normalized is { Length: > 0 } ? normalized : ["Admin"]; + } + } +} diff --git a/Avalonia-API/Authentication/JwtOptions.cs b/Avalonia-API/Authentication/JwtOptions.cs new file mode 100644 index 0000000..ad72a1a --- /dev/null +++ b/Avalonia-API/Authentication/JwtOptions.cs @@ -0,0 +1,33 @@ +namespace Avalonia_API.Authentication +{ + /// + /// JWT 鉴权配置选项,从 appsettings.json 的 Jwt 节绑定。 + /// + public sealed class JwtOptions + { + /// + /// 获取或设置 Token 签发者。 + /// + public string Issuer { get; set; } = "Avalonia-API"; + + /// + /// 获取或设置 Token 受众。 + /// + public string Audience { get; set; } = "Avalonia-Client"; + + /// + /// 获取或设置签名密钥(至少 32 字节)。 + /// + public string SigningKey { get; set; } = "change-this-development-signing-key-at-least-32-bytes"; + + /// + /// 获取或设置 Access Token 有效期(分钟),默认 60 分钟。 + /// + public int AccessTokenMinutes { get; set; } = 60; + + /// + /// 获取或设置 Refresh Token 有效期(天),默认 30 天。 + /// + public int RefreshTokenDays { get; set; } = 30; + } +} diff --git a/Avalonia-API/Authentication/JwtTokenService.cs b/Avalonia-API/Authentication/JwtTokenService.cs new file mode 100644 index 0000000..a76d501 --- /dev/null +++ b/Avalonia-API/Authentication/JwtTokenService.cs @@ -0,0 +1,55 @@ +using Avalonia_EFCore.Models; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace Avalonia_API.Authentication +{ + /// + /// JWT Token 服务,负责创建包含用户声明和角色的 Access Token。 + /// + public sealed class JwtTokenService(IOptions options) + { + /// + /// JWT 配置选项。 + /// + private readonly JwtOptions _options = options.Value; + + /// + /// 创建包含用户声明和角色的 JWT Access Token。 + /// + /// 用户实体。 + /// 角色集合。 + /// 包含 Token 字符串和过期时间的元组。 + public (string Token, DateTime ExpiresAt) CreateAccessToken(UserEntity user, IReadOnlyCollection roles) + { + var expiresAt = DateTime.UtcNow.AddMinutes(_options.AccessTokenMinutes); + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new(ClaimTypes.NameIdentifier, user.Id.ToString()), + new(ClaimTypes.Name, user.Name ?? user.Email ?? $"user-{user.Id}"), + new("auth_type", "api-jwt"), + }; + + foreach (var role in roles.Where(role => !string.IsNullOrWhiteSpace(role)).Distinct(StringComparer.OrdinalIgnoreCase)) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SigningKey)); + var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); + var jwt = new JwtSecurityToken( + issuer: _options.Issuer, + audience: _options.Audience, + claims: claims, + notBefore: DateTime.UtcNow, + expires: expiresAt, + signingCredentials: credentials); + + return (new JwtSecurityTokenHandler().WriteToken(jwt), expiresAt); + } + } +} diff --git a/Avalonia-API/Authentication/RefreshTokenService.cs b/Avalonia-API/Authentication/RefreshTokenService.cs new file mode 100644 index 0000000..7b8e2f9 --- /dev/null +++ b/Avalonia-API/Authentication/RefreshTokenService.cs @@ -0,0 +1,124 @@ +using Avalonia_EFCore.Database; +using Avalonia_EFCore.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using System.Security.Cryptography; +using System.Text; + +namespace Avalonia_API.Authentication +{ + /// + /// Refresh Token 服务,负责创建、查找、撤销和轮换 Refresh Token, + /// Token 原文经 SHA256 哈希后存入数据库以保证安全性。 + /// + public sealed class RefreshTokenService(AppDataContext db, IOptions options) + { + /// + /// JWT 配置选项。 + /// + private readonly JwtOptions _options = options.Value; + + /// + /// 创建一个新的 Refresh Token,生成随机 Token 原文并存储其哈希到数据库。 + /// + /// 关联的用户 ID。 + /// 创建设备标识(如 User-Agent)。 + /// 客户端 IP 地址。 + /// 取消令牌。 + /// 包含 Token 原文和实体记录的元组。 + public async Task<(string Token, ApiRefreshTokenEntity Entity)> CreateAsync( + int userId, + string? device, + string? ipAddress, + CancellationToken cancellationToken = default) + { + var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + var entity = new ApiRefreshTokenEntity + { + UserId = userId, + TokenHash = HashToken(token), + ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenDays), + Device = device, + IpAddress = ipAddress, + }; + + db.ApiRefreshTokens.Add(entity); + await db.SaveChangesAsync(cancellationToken); + return (token, entity); + } + + /// + /// 查找有效的 Refresh Token 实体。Token 原文会被哈希后查询数据库, + /// 仅返回未过期且未被撤销的 Token。 + /// + /// Refresh Token 原文。 + /// 取消令牌。 + /// 有效的 Token 实体;若无效或不存在则返回 null。 + public async Task FindActiveAsync(string? token, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(token)) + { + return null; + } + + var hash = HashToken(token); + var entity = await db.ApiRefreshTokens.FirstOrDefaultAsync(x => x.TokenHash == hash, cancellationToken); + return entity?.IsActive == true ? entity : null; + } + + /// + /// 撤销指定的 Refresh Token,将其 RevokedAt 设为当前时间。 + /// + /// 要撤销的 Refresh Token 原文。 + /// 取消令牌。 + public async Task RevokeAsync(string? token, CancellationToken cancellationToken = default) + { + var entity = await FindActiveAsync(token, cancellationToken); + if (entity is null) + { + return; + } + + entity.RevokedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } + + /// + /// 轮换 Refresh Token:撤销旧的并创建新的,将新 Token 的哈希关联到旧记录。 + /// + /// 旧的 Refresh Token 原文。 + /// 当前设备标识。 + /// 当前客户端 IP 地址。 + /// 取消令牌。 + /// 新的 Token 对;若旧 Token 无效则返回 null。 + public async Task<(string Token, ApiRefreshTokenEntity Entity)?> RotateAsync( + string? token, + string? device, + string? ipAddress, + CancellationToken cancellationToken = default) + { + var current = await FindActiveAsync(token, cancellationToken); + if (current is null) + { + return null; + } + + var next = await CreateAsync(current.UserId, device, ipAddress, cancellationToken); + current.RevokedAt = DateTime.UtcNow; + current.ReplacedByTokenHash = next.Entity.TokenHash; + await db.SaveChangesAsync(cancellationToken); + return next; + } + + /// + /// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。 + /// + /// Token 原文。 + /// SHA256 哈希后的十六进制字符串。 + private static string HashToken(string token) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); + return Convert.ToHexString(bytes); + } + } +} diff --git a/Avalonia-API/Avalonia-API.csproj b/Avalonia-API/Avalonia-API.csproj new file mode 100644 index 0000000..8bbb938 --- /dev/null +++ b/Avalonia-API/Avalonia-API.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + Avalonia_API + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/Avalonia-API/Avalonia-API.csproj.user b/Avalonia-API/Avalonia-API.csproj.user new file mode 100644 index 0000000..983ecfc --- /dev/null +++ b/Avalonia-API/Avalonia-API.csproj.user @@ -0,0 +1,9 @@ + + + + http + + + ProjectDebugger + + \ No newline at end of file diff --git a/Avalonia-API/Avalonia-API.http b/Avalonia-API/Avalonia-API.http new file mode 100644 index 0000000..3dd7f88 --- /dev/null +++ b/Avalonia-API/Avalonia-API.http @@ -0,0 +1,6 @@ +@Avalonia_API_HostAddress = http://localhost:5206 + +GET {{Avalonia_API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Avalonia-API/Configuration/ServicesConfiguration.cs b/Avalonia-API/Configuration/ServicesConfiguration.cs new file mode 100644 index 0000000..f8f8746 --- /dev/null +++ b/Avalonia-API/Configuration/ServicesConfiguration.cs @@ -0,0 +1,75 @@ +using Avalonia_API.Authentication; +using Avalonia_EFCore.Database; +using Avalonia_Services.Core; +using Avalonia_Services.Endpoints; +using Avalonia_Services.Services; +using Avalonia_Services.Services.AuthService; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace Avalonia_API.Configuration +{ + /// + /// API 项目服务配置扩展类,负责注册数据库、鉴权、业务服务和统一端点。 + /// + public static class ServicesConfiguration + { + /// + /// 注册统一端点及其依赖的服务(含数据库)。 + /// 所有业务端点定义在 Avalonia-Services/Endpoints/AppEndpoints.cs。 + /// + public static IServiceCollection AddUnifiedApiServices(this IServiceCollection services, IConfiguration configuration) + { + // ---- 数据库 ---- + // 从 appsettings.json 读取 DatabaseConfiguration 节 + // 注册默认数据库提供程序(SQLite / MySQL / PostgreSQL / SqlServer) + DatabaseProviderRegistry.RegisterDefaults(); + + var databaseConfig = configuration + .GetSection(nameof(DatabaseConfiguration)) + .Get() + ?? DatabaseConfiguration.ForSQLite("app.db"); + + // 注册 AppDataContext(共享数据上下文) + services.AddAppDatabase(databaseConfig); + + // ---- 业务服务 ---- + services.AddScoped(); + + // ---- API 鉴权 ---- + var jwtSection = configuration.GetSection("Jwt"); + services.Configure(jwtSection); + var jwtOptions = jwtSection.Get() ?? new JwtOptions(); + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtOptions.Issuer, + ValidAudience = jwtOptions.Audience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)), + ClockSkew = TimeSpan.FromMinutes(1), + }; + }); + services.AddAuthorization(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // ---- 统一端点 ---- + var endpointBuilder = new ServiceEndpointBuilder(); + AppEndpoints.Configure(endpointBuilder); + AuthEndpoints.ConfigureApi(endpointBuilder); + var endpoints = endpointBuilder.Build(); + services.AddSingleton(endpoints); + + return services; + } + } +} diff --git a/Avalonia-API/Extensions/UnifiedEndpointExtensions.cs b/Avalonia-API/Extensions/UnifiedEndpointExtensions.cs new file mode 100644 index 0000000..234541f --- /dev/null +++ b/Avalonia-API/Extensions/UnifiedEndpointExtensions.cs @@ -0,0 +1,225 @@ +using Avalonia_Services.Core; +using Microsoft.AspNetCore.Authorization; +using AspNetCoreFilterContext = Microsoft.AspNetCore.Http.EndpointFilterInvocationContext; +using AspNetCoreFilterDelegate = Microsoft.AspNetCore.Http.EndpointFilterDelegate; +// 解决与 ASP.NET Core 同名类型的冲突 +using UnifiedFilter = Avalonia_Services.Core.IEndpointFilter; + +namespace Avalonia_API.Extensions +{ + /// + /// 将 Avalonia-Services 的统一端点映射到 ASP.NET Core Minimal API。 + /// 支持鉴权、过滤器、中间件的完整 ASP.NET Core 管道。 + /// + public static class UnifiedEndpointExtensions + { + /// + /// 将 ServiceEndpointCollection 中的所有端点注册到 ASP.NET Core 路由。 + /// + public static IEndpointRouteBuilder MapUnifiedEndpoints( + this IEndpointRouteBuilder routeBuilder, + ServiceEndpointCollection endpoints, + IServiceProvider serviceProvider) + { + var apiGroup = routeBuilder.MapGroup("/"); + + foreach (var endpoint in endpoints.ForHost(EndpointHostTarget.Api)) + { + var routeHandlerBuilder = MapEndpoint(apiGroup, endpoint, serviceProvider); + + // 全局过滤器 → ASP.NET Core Endpoint Filters + foreach (var globalFilter in endpoints.GlobalFilters) + { + routeHandlerBuilder.AddEndpointFilter( + async (context, next) => await ConvertFilterAsync(globalFilter, context, next)); + } + + // 端点专属过滤器 + foreach (var filter in endpoint.Filters) + { + routeHandlerBuilder.AddEndpointFilter( + async (context, next) => await ConvertFilterAsync(filter, context, next)); + } + + // 鉴权(使用 ASP.NET Core 原生鉴权机制) + if (endpoint.RequireAuthorization) + { + if (endpoint.Roles.Count > 0) + { + routeHandlerBuilder.RequireAuthorization(new AuthorizeAttribute + { + Roles = string.Join(',', endpoint.Roles), + }); + } + else if (!string.IsNullOrEmpty(endpoint.Policy)) + { + routeHandlerBuilder.RequireAuthorization(endpoint.Policy); + } + else + { + routeHandlerBuilder.RequireAuthorization(); + } + } + + if (!string.IsNullOrEmpty(endpoint.Name)) + { + routeHandlerBuilder.WithName(endpoint.Name); + } + + if (!string.IsNullOrEmpty(endpoint.OpenApiTag)) + { + routeHandlerBuilder.WithTags(endpoint.OpenApiTag); + } + + if (!string.IsNullOrEmpty(endpoint.OpenApiDescription)) + { + routeHandlerBuilder.WithDescription(endpoint.OpenApiDescription); + } + + if (!string.IsNullOrWhiteSpace(endpoint.OpenApiSummary)) + { + routeHandlerBuilder.WithSummary(endpoint.OpenApiSummary); + } + + if (endpoint.OpenApiRequestType is not null) + { + routeHandlerBuilder.Accepts(endpoint.OpenApiRequestType, "application/json"); + } + + if (endpoint.OpenApiResponseType is not null) + { + routeHandlerBuilder.Produces(200, endpoint.OpenApiResponseType, "application/json"); + } + } + + return routeBuilder; + } + + /// + /// 根据端点的 HTTP 方法(GET/POST/PUT/DELETE)将其映射到 ASP.NET Core 路由。 + /// + /// 路由组。 + /// 统一端点定义。 + /// 服务提供程序。 + /// 路由处理器构建器,用于叠加过滤器等配置。 + private static RouteHandlerBuilder MapEndpoint( + IEndpointRouteBuilder group, + ServiceEndpoint endpoint, + IServiceProvider serviceProvider) + { + var handler = CreateAspNetCoreHandler(endpoint.Handler, serviceProvider); + + return endpoint.HttpMethod.ToUpperInvariant() switch + { + "GET" => group.MapGet(endpoint.Pattern, handler), + "POST" => group.MapPost(endpoint.Pattern, handler), + "PUT" => group.MapPut(endpoint.Pattern, handler), + "DELETE" => group.MapDelete(endpoint.Pattern, handler), + _ => group.MapGet(endpoint.Pattern, handler), + }; + } + + /// + /// 创建适配 ASP.NET Core 的委托处理器,将统一处理器包装为 ASP.NET Core 可识别的委托。 + /// + /// 统一端点处理器。 + /// 服务提供程序。 + /// ASP.NET Core 兼容的委托。 + private static Delegate CreateAspNetCoreHandler( + Func> unifiedHandler, + IServiceProvider serviceProvider) + { + return async (HttpContext httpContext) => + { + var ctx = await BuildContextFromHttpContext(httpContext); + ctx.Items["ServiceProvider"] = serviceProvider; + ctx.Items["User"] = httpContext.User; + + var result = await unifiedHandler(ctx); + + // 同步响应状态 + httpContext.Response.StatusCode = ctx.StatusCode; + foreach (var kvp in ctx.ResponseHeaders) + { + httpContext.Response.Headers[kvp.Key] = kvp.Value; + } + + return result is not null ? Results.Json(result) : Results.Ok(); + }; + } + + /// + /// 从 ASP.NET Core 的 HttpContext 构建统一的 ServiceEndpointContext, + /// 提取路径、方法、请求头、查询参数和请求体。 + /// + /// ASP.NET Core 的 HttpContext。 + /// 构建好的统一端点上下文。 + private static async Task BuildContextFromHttpContext(HttpContext httpContext) + { + var ctx = new ServiceEndpointContext + { + Path = httpContext.Request.Path.Value ?? "/", + Method = httpContext.Request.Method, + StatusCode = 200, + }; + + foreach (var header in httpContext.Request.Headers) + { + ctx.Headers[header.Key] = header.Value.ToString(); + } + + foreach (var query in httpContext.Request.Query) + { + ctx.Query[query.Key] = query.Value.ToString(); + } + + if (httpContext.Request.ContentLength > 0) + { + using var reader = new StreamReader(httpContext.Request.Body); + ctx.Body = await reader.ReadToEndAsync(); + } + + ctx.Items["HttpContext"] = httpContext; + ctx.Items["User"] = httpContext.User; + + return ctx; + } + + /// + /// 将统一过滤器转换为 ASP.NET Core 端点过滤器, + /// 在调用统一过滤器前后桥接上下文和状态。 + /// + /// 统一过滤器。 + /// ASP.NET Core 过滤器调用上下文。 + /// ASP.NET Core 过滤器管道中的下一个委托。 + /// 过滤器执行结果,可能包含短路响应体。 + private static async ValueTask ConvertFilterAsync( + UnifiedFilter unifiedFilter, + AspNetCoreFilterContext aspContext, + AspNetCoreFilterDelegate aspNext) + { + var httpContext = aspContext.HttpContext; + var ctx = httpContext.Items["UnifiedContext"] as ServiceEndpointContext + ?? await BuildContextFromHttpContext(httpContext); + + httpContext.Items["UnifiedContext"] = ctx; + + await unifiedFilter.InvokeAsync(ctx, async (c) => + { + httpContext.Response.StatusCode = c.StatusCode; + foreach (var kvp in c.ResponseHeaders) + { + httpContext.Response.Headers[kvp.Key] = kvp.Value; + } + await aspNext(aspContext); + }); + + if (ctx.ResponseBody is not null) + { + return Results.Json(ctx.ResponseBody, statusCode: ctx.StatusCode); + } + + return null!; + } + } +} diff --git a/Avalonia-API/Program.cs b/Avalonia-API/Program.cs new file mode 100644 index 0000000..506a365 --- /dev/null +++ b/Avalonia-API/Program.cs @@ -0,0 +1,60 @@ +using Avalonia_API.Configuration; +using Avalonia_API.Extensions; +using Avalonia_Common.Infrastructure; +using Avalonia_EFCore.Database; +using Avalonia_Services.Core; +using Serilog; + +// 初始化日志系统 +Log.Logger = LoggingConfiguration.CreateDefaultLogger(logDir: "logs"); +Log.Information("Avalonia-API 正在启动..."); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + // 使用 Serilog 作为日志提供程序 + builder.Host.UseSerilog(); + + // Add services to the container. + builder.Services.AddControllers(); + builder.Services.AddOpenApi(); + + // 注册统一端点及业务服务(入口在 Avalonia-Services/Endpoints/AppEndpoints.cs) + builder.Services.AddUnifiedApiServices(builder.Configuration); + + var app = builder.Build(); + + // 初始化数据库(自动迁移 + 种子数据) + app.Services.InitializeDatabase(); + + var endpoints = app.Services.GetRequiredService(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "Avalonia API v1"); + options.RoutePrefix = "swagger"; + }); + } + + app.UseHttpsRedirection(); + app.UseAuthentication(); + app.UseAuthorization(); + + // 将统一端点映射到 ASP.NET Core 路由 + app.MapUnifiedEndpoints(endpoints, app.Services); + + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Avalonia-API 启动失败"); +} +finally +{ + Log.CloseAndFlush(); +} diff --git a/Avalonia-API/Properties/launchSettings.json b/Avalonia-API/Properties/launchSettings.json new file mode 100644 index 0000000..00c28bd --- /dev/null +++ b/Avalonia-API/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7165;http://localhost:5206", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Avalonia-API/appsettings.Development.json b/Avalonia-API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Avalonia-API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Avalonia-API/appsettings.json b/Avalonia-API/appsettings.json new file mode 100644 index 0000000..91235d1 --- /dev/null +++ b/Avalonia-API/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Jwt": { + "Issuer": "Avalonia-API", + "Audience": "Avalonia-Client", + "SigningKey": "change-this-development-signing-key-at-least-32-bytes", + "AccessTokenMinutes": 60, + "RefreshTokenDays": 30 + }, + "DatabaseConfiguration": { + "Provider": "MySQL", + "ConnectionString": "Server=127.0.0.1;Port=3306;Database=avalonia-api;Uid=root;Pwd=123456;Max Pool Size=100;Min Pool Size=5;AllowZeroDateTime=True;AllowLoadLocalInfile=true;SslMode=Required", + "AutoMigrate": true, + "RecreateDatabase": false, + "EnableDetailedLog": false, + "Timeout": 30 + } +} diff --git a/Avalonia-Common/Avalonia-Common.csproj b/Avalonia-Common/Avalonia-Common.csproj new file mode 100644 index 0000000..583405c --- /dev/null +++ b/Avalonia-Common/Avalonia-Common.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + Avalonia_Common + enable + enable + + + + + + + + + + + diff --git a/Avalonia-Common/Core/ApiResponse.cs b/Avalonia-Common/Core/ApiResponse.cs new file mode 100644 index 0000000..404e9c8 --- /dev/null +++ b/Avalonia-Common/Core/ApiResponse.cs @@ -0,0 +1,205 @@ +using System.Text.Json.Serialization; + +namespace Avalonia_Common.Core +{ + /// + /// 统一 API 返回格式。 + /// 所有接口的返回都包装为此格式,确保前端收到一致的数据结构。 + /// + /// 业务数据类型 + public class ApiResponse + { + /// 是否成功 + [JsonPropertyName("success")] + public bool Success { get; set; } + + /// HTTP 状态码 + [JsonPropertyName("code")] + public int Code { get; set; } + + /// 消息(成功时可为 null,失败时包含错误描述) + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// 业务数据 + [JsonPropertyName("data")] + public T? Data { get; set; } + + /// 时间戳 + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.Now; + + /// 请求追踪 ID(用于排查问题) + [JsonPropertyName("traceId")] + public string? TraceId { get; set; } + + // ---- 快捷工厂方法 ---- + + /// 成功返回(有数据) + public static ApiResponse Ok(T data, string? message = null) + { + return new ApiResponse + { + Success = true, + Code = 200, + Message = message, + Data = data, + }; + } + + /// 失败返回 + public static ApiResponse Fail(int code, string message, T? data = default) + { + return new ApiResponse + { + Success = false, + Code = code, + Message = message, + Data = data, + }; + } + + /// 400 参数错误 + public static ApiResponse BadRequest(string message = "参数错误") + => Fail(400, message); + + /// 401 未授权 + public static ApiResponse Unauthorized(string message = "未授权") + => Fail(401, message); + + /// 403 无权限 + public static ApiResponse Forbidden(string message = "无权限") + => Fail(403, message); + + /// 404 未找到 + public static ApiResponse NotFound(string message = "资源不存在") + => Fail(404, message); + + /// 500 服务器内部错误 + public static ApiResponse ServerError(string message = "服务器内部错误") + => Fail(500, message); + } + + /// + /// 无数据的统一返回格式(object? 版本)。 + /// + public class ApiResponse : ApiResponse + { + /// 成功返回(无数据) + public static ApiResponse Succeed(string? message = null) + { + return new ApiResponse + { + Success = true, + Code = 200, + Message = message, + Data = null, + }; + } + + /// 失败返回 + public static ApiResponse Failure(int code, string message) + { + return new ApiResponse + { + Success = false, + Code = code, + Message = message, + Data = null, + }; + } + } + + /// + /// 分页返回格式 + /// + public class PagedResponse + { + /// + /// 获取或设置操作是否成功。 + /// + [JsonPropertyName("success")] + public bool Success { get; set; } = true; + + /// + /// 获取或设置业务状态码,默认 200。 + /// + [JsonPropertyName("code")] + public int Code { get; set; } = 200; + + /// + /// 获取或设置分页数据项列表。 + /// + [JsonPropertyName("items")] + public List Items { get; set; } = new(); + + /// + /// 获取或设置数据总条数。 + /// + [JsonPropertyName("total")] + public int Total { get; set; } + + /// + /// 获取或设置当前页码,从 1 开始。 + /// + [JsonPropertyName("page")] + public int Page { get; set; } = 1; + + /// + /// 获取或设置每页条数,默认 20。 + /// + [JsonPropertyName("pageSize")] + public int PageSize { get; set; } = 20; + + /// + /// 获取总页数(根据 Total 和 PageSize 自动计算)。 + /// + [JsonPropertyName("totalPages")] + public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0; + + /// + /// 从数据列表和分页参数创建分页响应。 + /// + /// 当前页数据项。 + /// 数据总条数。 + /// 当前页码。 + /// 每页条数。 + /// 分页响应实例。 + public static PagedResponse From(List items, int total, int page, int pageSize) + { + return new PagedResponse + { + Items = items, + Total = total, + Page = page, + PageSize = pageSize, + }; + } + } + + /// + /// 端点返回辅助方法 —— 在 AppEndpoints 中快捷构建统一响应。 + /// + public static class ResponseHelper + { + /// 成功返回 + public static ApiResponse Ok(T data, string? message = null) + => ApiResponse.Ok(data, message); + + /// 成功返回(无数据) + public static ApiResponse Succeed(string? message = null) + => ApiResponse.Succeed(message); + + /// 失败返回 + public static ApiResponse Fail(int code, string message, T? data = default) + => ApiResponse.Fail(code, message, data); + + /// 失败返回(无数据) + public static ApiResponse Failure(int code, string message) + => ApiResponse.Failure(code, message); + + /// 分页返回 + public static PagedResponse Paged(List items, int total, int page, int pageSize) + => PagedResponse.From(items, total, page, pageSize); + } +} diff --git a/Avalonia-Common/Infrastructure/LoggingConfiguration.cs b/Avalonia-Common/Infrastructure/LoggingConfiguration.cs new file mode 100644 index 0000000..12df6fe --- /dev/null +++ b/Avalonia-Common/Infrastructure/LoggingConfiguration.cs @@ -0,0 +1,167 @@ +using Serilog; +using Serilog.Events; + +namespace Avalonia_Common.Infrastructure +{ + /// + /// Serilog 日志配置 —— 可在 Avalonia-API 和 Avalonia-PC 中共享。 + /// + public static class LoggingConfiguration + { + /// + /// 默认日志目录 + /// + private static readonly string DefaultLogDir = Path.Combine(AppContext.BaseDirectory, "logs"); + + /// + /// 创建控制台日志记录器(开发环境)。 + /// + public static ILogger CreateConsoleLogger( + LogEventLevel minimumLevel = LogEventLevel.Debug) + { + return new LoggerConfiguration() + .MinimumLevel.Is(minimumLevel) + .Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + } + + /// + /// 创建控制台 + 文件日志记录器。 + /// + /// 最低日志级别 + /// 日志目录,默认 ./logs + /// 保留天数 + public static ILogger CreateDefaultLogger( + LogEventLevel minimumLevel = LogEventLevel.Information, + string? logDir = null, + int retainedDays = 30) + { + logDir ??= DefaultLogDir; + + return new LoggerConfiguration() + .MinimumLevel.Is(minimumLevel) + //.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + //.MinimumLevel.Override("System", LogEventLevel.Warning) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithThreadId() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File( + path: Path.Combine(logDir, "log-.txt"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedDays, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}", + encoding: System.Text.Encoding.UTF8) + .CreateLogger(); + } + + /// + /// 创建只写文件的日志记录器(桌面应用静默模式)。 + /// + public static ILogger CreateFileOnlyLogger( + LogEventLevel minimumLevel = LogEventLevel.Information, + string? logDir = null, + int retainedDays = 30) + { + logDir ??= DefaultLogDir; + + return new LoggerConfiguration() + .MinimumLevel.Is(minimumLevel) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.File( + path: Path.Combine(logDir, "app-.txt"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedDays, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}", + encoding: System.Text.Encoding.UTF8) + .CreateLogger(); + } + } + + /// + /// 静态日志访问器 —— 全局静态入口,方便在没有 DI 的场景下使用。 + /// + public static class AppLog + { + /// + /// 保存全局日志记录器实例。 + /// + private static ILogger? _logger; + + /// + /// 初始化全局日志记录器。 + /// + public static void Initialize(ILogger logger) + { + _logger = logger; + Log.Logger = logger; + } + + /// + /// 获取全局日志记录器。若未初始化则回退到 Serilog.Log.Logger。 + /// + public static ILogger Logger => _logger ?? Log.Logger; + + /// + /// 写入 Debug 级别日志。 + /// + /// 消息模板。 + /// 属性值。 + public static void Debug(string messageTemplate, params object?[] propertyValues) + => Logger.Debug(messageTemplate, propertyValues); + + /// + /// 写入 Information 级别日志。 + /// + /// 消息模板。 + /// 属性值。 + public static void Information(string messageTemplate, params object?[] propertyValues) + => Logger.Information(messageTemplate, propertyValues); + + /// + /// 写入 Warning 级别日志。 + /// + /// 消息模板。 + /// 属性值。 + public static void Warning(string messageTemplate, params object?[] propertyValues) + => Logger.Warning(messageTemplate, propertyValues); + + /// + /// 写入 Error 级别日志。 + /// + /// 消息模板。 + /// 属性值。 + public static void Error(string messageTemplate, params object?[] propertyValues) + => Logger.Error(messageTemplate, propertyValues); + + /// + /// 写入 Error 级别日志,并附带异常信息。 + /// + /// 异常对象。 + /// 消息模板。 + /// 属性值。 + public static void Error(Exception exception, string messageTemplate, params object?[] propertyValues) + => Logger.Error(exception, messageTemplate, propertyValues); + + /// + /// 写入 Fatal 级别日志。 + /// + /// 消息模板。 + /// 属性值。 + public static void Fatal(string messageTemplate, params object?[] propertyValues) + => Logger.Fatal(messageTemplate, propertyValues); + + /// + /// 写入 Fatal 级别日志,并附带异常信息。 + /// + /// 异常对象。 + /// 消息模板。 + /// 属性值。 + public static void Fatal(Exception exception, string messageTemplate, params object?[] propertyValues) + => Logger.Fatal(exception, messageTemplate, propertyValues); + } +} diff --git a/Avalonia-EFCore/Avalonia-EFCore.csproj b/Avalonia-EFCore/Avalonia-EFCore.csproj new file mode 100644 index 0000000..bc55af8 --- /dev/null +++ b/Avalonia-EFCore/Avalonia-EFCore.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + Avalonia_EFCore + enable + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Avalonia-EFCore/Database/AppDataContext.cs b/Avalonia-EFCore/Database/AppDataContext.cs new file mode 100644 index 0000000..e8c5f09 --- /dev/null +++ b/Avalonia-EFCore/Database/AppDataContext.cs @@ -0,0 +1,50 @@ +using Avalonia_EFCore.Models; +using Microsoft.EntityFrameworkCore; + +namespace Avalonia_EFCore.Database +{ + /// + /// 应用数据库上下文 —— 继承自 Avalonia-EFCore 的 AppDbContext。 + /// 所有业务实体在此注册 DbSet。 + /// 这是 Avalonia-API 和 Avalonia-PC 共用的具体数据上下文。 + /// + public class AppDataContext(DatabaseConfiguration dbConfig) : AppDbContext(dbConfig) + { + /// 天气预报数据 + public DbSet WeatherForecasts => Set(); + + /// 用户数据 + public DbSet Users => Set(); + + /// API refresh token 数据 + public DbSet ApiRefreshTokens => Set(); + + /// + /// 配置实体映射,包括主键、索引和属性约束。 + /// + /// 模型构建器。 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk-weather-forecast"); + entity.Property(e => e.Summary).HasMaxLength(200); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk-user"); + entity.Property(e => e.Email).HasMaxLength(200); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk-api-refresh-token"); + entity.HasIndex(e => e.TokenHash).IsUnique().HasDatabaseName("idx-api-refresh-token-hash"); + entity.HasIndex(e => e.UserId).HasDatabaseName("idx-api-refresh-token-user-id"); + }); + } + } +} diff --git a/Avalonia-EFCore/Database/AppDataContextFactory.cs b/Avalonia-EFCore/Database/AppDataContextFactory.cs new file mode 100644 index 0000000..114941f --- /dev/null +++ b/Avalonia-EFCore/Database/AppDataContextFactory.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore.Design; + +namespace Avalonia_EFCore.Database +{ + /// + /// 设计时 DbContext 工厂,用于 EF Core 迁移工具生成迁移代码。 + /// + public class AppDataContextFactory : IDesignTimeDbContextFactory + { + /// + /// 创建用于设计时的 AppDataContext 实例,默认使用 SQLite 提供程序。 + /// + /// 命令行参数。 + /// 配置好的数据上下文实例。 + public AppDataContext CreateDbContext(string[] args) + { + return new AppDataContext(DesignTimeDatabaseConfiguration.Create(args)); + } + } + + /// + /// SQLite 迁移设计时工厂。 + /// + public sealed class SqliteAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public SqliteAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.SQLite)); + } + + /// + /// SQL Server 迁移设计时工厂。 + /// + public sealed class SqlServerAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public SqlServerAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.SqlServer)); + } + + /// + /// PostgreSQL 迁移设计时工厂。 + /// + public sealed class PostgreSqlAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public PostgreSqlAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.PostgreSQL)); + } + + /// + /// MySQL 迁移设计时工厂。 + /// + public sealed class MySqlAppDataContextFactory : IDesignTimeDbContextFactory + { + /// + public MySqlAppDataContext CreateDbContext(string[] args) + => new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.MySQL)); + } + + internal static class DesignTimeDatabaseConfiguration + { + public static DatabaseConfiguration Create(string[] args, DatabaseProvider defaultProvider = DatabaseProvider.SQLite) + { + DatabaseProviderRegistry.RegisterDefaults(); + + var provider = GetProvider(args) ?? defaultProvider; + return provider switch + { + DatabaseProvider.SQLite => DatabaseConfiguration.ForSQLite("avalonia-api.db"), + DatabaseProvider.SqlServer => DatabaseConfiguration.ForSqlServer("(localdb)\\MSSQLLocalDB", "AvaloniaApi"), + DatabaseProvider.PostgreSQL => DatabaseConfiguration.ForPostgreSQL("localhost", "avalonia_api", "postgres", "postgres"), + DatabaseProvider.MySQL => DatabaseConfiguration.ForMySQL("localhost", "avalonia_api", "root", "root"), + _ => DatabaseConfiguration.ForSQLite("avalonia-api.db"), + }; + } + + private static DatabaseProvider? GetProvider(string[] args) + { + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + string? value = null; + + if (arg.Equals("--provider", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) + { + value = args[i + 1]; + } + else if (arg.StartsWith("--provider=", StringComparison.OrdinalIgnoreCase)) + { + value = arg["--provider=".Length..]; + } + + if (!string.IsNullOrWhiteSpace(value) + && Enum.TryParse(value, ignoreCase: true, out var provider)) + { + return provider; + } + } + + return null; + } + } +} diff --git a/Avalonia-EFCore/Database/AppDbContext.cs b/Avalonia-EFCore/Database/AppDbContext.cs new file mode 100644 index 0000000..48d283c --- /dev/null +++ b/Avalonia-EFCore/Database/AppDbContext.cs @@ -0,0 +1,115 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia_EFCore.Database +{ + /// + /// 应用数据库上下文基类 —— 自动根据 DatabaseConfiguration 选择数据库提供程序。 + /// 所有业务 DbContext 继承此类即可获得多数据库支持。 + /// + public abstract class AppDbContext(DatabaseConfiguration dbConfig) : DbContext + { + /// + /// 数据库配置。 + /// + private readonly DatabaseConfiguration _dbConfig = dbConfig; + + /// + /// 配置数据库提供程序和连接选项。 + /// + /// 选项构建器。 + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (optionsBuilder.IsConfigured) return; + + ConfigureProvider(optionsBuilder, _dbConfig); + + if (_dbConfig.EnableDetailedLog) + { + optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information); + } + + // 启用详细的 EF Core 错误信息 + optionsBuilder.EnableDetailedErrors(); + optionsBuilder.EnableSensitiveDataLogging(_dbConfig.EnableDetailedLog); + } + + /// + /// 根据配置选择数据库提供程序。 + /// 使用注册模式,由宿主项目注册具体的提供程序实现。 + /// + public static void ConfigureProvider(DbContextOptionsBuilder optionsBuilder, DatabaseConfiguration config) + { + if (DatabaseProviderRegistry.TryGet(config.Provider, out var configurator)) + { + configurator(optionsBuilder, config.ConnectionString, config.Timeout); + } + else + { + throw new NotSupportedException( + $"数据库提供程序 {config.Provider} 未注册。" + + $"请在宿主项目中安装对应的 EF Core NuGet 包并调用 DatabaseProviderRegistry.Register()。"); + } + + optionsBuilder.EnableDetailedErrors(); + optionsBuilder.EnableSensitiveDataLogging(config.EnableDetailedLog); + + if (config.EnableDetailedLog) + { + optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information); + } + } + + /// + /// 保存时自动设置时间戳。 + /// + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + SetTimestamps(); + return base.SaveChanges(acceptAllChangesOnSuccess); + } + + /// + /// 异步保存更改,自动设置时间戳。 + /// + /// 是否在成功时接受所有更改。 + /// 取消令牌。 + /// 受影响的行数。 + public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + SetTimestamps(); + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + + /// + /// 自动设置新增或修改实体的 CreatedAt 和 UpdatedAt 时间戳。 + /// + private void SetTimestamps() + { + var entries = ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); + + foreach (var entry in entries) + { + var entity = entry.Entity; + + // 使用反射设置 CreatedAt / UpdatedAt(如果存在) + var createdAtProp = entity.GetType().GetProperty("CreatedAt"); + var updatedAtProp = entity.GetType().GetProperty("UpdatedAt"); + + if (entry.State == EntityState.Added && createdAtProp != null) + { + createdAtProp.SetValue(entity, DateTime.UtcNow); + } + + if (updatedAtProp != null) + { + updatedAtProp.SetValue(entity, DateTime.UtcNow); + } + } + } + } +} diff --git a/Avalonia-EFCore/Database/DatabaseConfiguration.cs b/Avalonia-EFCore/Database/DatabaseConfiguration.cs new file mode 100644 index 0000000..0f36c53 --- /dev/null +++ b/Avalonia-EFCore/Database/DatabaseConfiguration.cs @@ -0,0 +1,93 @@ +namespace Avalonia_EFCore.Database +{ + /// + /// 支持的数据库提供程序类型。 + /// + public enum DatabaseProvider + { + /// SQLite(本地文件数据库,无需安装,跨平台) + SQLite, + + /// MySQL / MariaDB + MySQL, + + /// PostgreSQL + PostgreSQL, + + /// SQL Server + SqlServer + } + + /// + /// 数据库连接配置 —— 在 appsettings.json 中配置。 + /// + public class DatabaseConfiguration + { + /// 数据库提供程序 + public DatabaseProvider Provider { get; set; } = DatabaseProvider.SQLite; + + /// 连接字符串 + public string ConnectionString { get; set; } = "Data Source=app.db"; + + /// 是否在启动时自动执行迁移 + public bool AutoMigrate { get; set; } = true; + + /// + /// 是否在迁移前删除并重建当前连接指向的数据库。 + /// 仅用于切换数据库类型或本地开发重建库;生产环境默认必须保持 false。 + /// + public bool RecreateDatabase { get; set; } = false; + + /// 是否启用详细日志(会打印 SQL 语句) + public bool EnableDetailedLog { get; set; } = false; + + /// 连接超时(秒) + public int Timeout { get; set; } = 30; + + // ---- 快捷构建方法 ---- + + /// SQLite 本地数据库 + public static DatabaseConfiguration ForSQLite(string dataSource = "app.db") + { + return new DatabaseConfiguration + { + Provider = DatabaseProvider.SQLite, + ConnectionString = $"Data Source={dataSource}", + AutoMigrate = true, + }; + } + + /// MySQL 数据库 + public static DatabaseConfiguration ForMySQL(string server, string database, string user, string password, uint port = 3306) + { + return new DatabaseConfiguration + { + Provider = DatabaseProvider.MySQL, + ConnectionString = $"Server={server};Port={port};Database={database};User={user};Password={password};", + }; + } + + /// PostgreSQL 数据库 + public static DatabaseConfiguration ForPostgreSQL(string host, string database, string username, string password, int port = 5432) + { + return new DatabaseConfiguration + { + Provider = DatabaseProvider.PostgreSQL, + ConnectionString = $"Host={host};Port={port};Database={database};Username={username};Password={password};", + }; + } + + /// SQL Server 数据库 + public static DatabaseConfiguration ForSqlServer(string server, string database, string? user = null, string? password = null) + { + var connStr = string.IsNullOrEmpty(user) + ? $"Server={server};Database={database};Trusted_Connection=True;TrustServerCertificate=True;" + : $"Server={server};Database={database};User Id={user};Password={password};TrustServerCertificate=True;"; + return new DatabaseConfiguration + { + Provider = DatabaseProvider.SqlServer, + ConnectionString = connStr, + }; + } + } +} diff --git a/Avalonia-EFCore/Database/DatabaseExtensions.cs b/Avalonia-EFCore/Database/DatabaseExtensions.cs new file mode 100644 index 0000000..c09bc61 --- /dev/null +++ b/Avalonia-EFCore/Database/DatabaseExtensions.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Avalonia_EFCore.Database +{ + /// + /// 数据库服务注册扩展 —— 在 Program.cs 中一行配置数据库。 + /// + public static class DatabaseExtensions + { + /// + /// 注册数据库上下文及相关服务。 + /// + /// 继承自 AppDbContext 的业务 DbContext + public static IServiceCollection AddAppDatabase( + this IServiceCollection services, + DatabaseConfiguration config) + where TContext : AppDbContext + { + // 注册配置 + services.AddSingleton(config); + + if (typeof(TContext) == typeof(AppDataContext)) + { + services.AddProviderAppDataContext(config); + services.AddScoped>(); + + return services; + } + + // 注册 DbContext + services.AddDbContext(options => + { + AppDbContext.ConfigureProvider(options, config); + }); + + // 注册数据库管理器 + services.AddScoped>(); + + return services; + } + + private static void AddProviderAppDataContext(this IServiceCollection services, DatabaseConfiguration config) + { + switch (config.Provider) + { + case DatabaseProvider.SQLite: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + case DatabaseProvider.SqlServer: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + case DatabaseProvider.PostgreSQL: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + case DatabaseProvider.MySQL: + services.AddDbContext(options => + AppDbContext.ConfigureProvider(options, config)); + break; + default: + throw new NotSupportedException($"数据库提供程序 {config.Provider} 未注册。"); + } + } + + /// + /// 初始化数据库(在应用启动时调用一次)。 + /// + public static IServiceProvider InitializeDatabase( + this IServiceProvider serviceProvider, + Action? seeder = null) + where TContext : AppDbContext + { + using var scope = serviceProvider.CreateScope(); + var dbManager = scope.ServiceProvider.GetRequiredService>(); + + // 同步等待初始化(启动时阻塞) + dbManager.InitializeAsync(seeder).GetAwaiter().GetResult(); + + return serviceProvider; + } + } +} diff --git a/Avalonia-EFCore/Database/DatabaseManager.cs b/Avalonia-EFCore/Database/DatabaseManager.cs new file mode 100644 index 0000000..22f2187 --- /dev/null +++ b/Avalonia-EFCore/Database/DatabaseManager.cs @@ -0,0 +1,224 @@ +using Avalonia_Common.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia_EFCore.Database +{ + /// + /// 数据库管理器 —— 负责连接测试、自动迁移、种子数据、版本检查。 + /// 在应用启动时调用,确保数据库结构与应用代码同步。 + /// + public class DatabaseManager where TContext : AppDbContext + { + /// + /// 数据库上下文实例。 + /// + private readonly TContext _context; + /// + /// 数据库配置。 + /// + private readonly DatabaseConfiguration _config; + /// + /// DI 服务提供程序(可选,用于种子数据中解析服务)。 + /// + private readonly IServiceProvider? _serviceProvider; + + /// + /// 初始化数据库管理器。 + /// + /// 数据库上下文。 + /// 数据库配置。 + /// 可选的 DI 容器。 + public DatabaseManager(TContext context, DatabaseConfiguration config, IServiceProvider? serviceProvider = null) + { + _context = context; + _config = config; + _serviceProvider = serviceProvider; + } + + /// + /// 初始化数据库:测试连接 → 自动迁移 → 种子数据。 + /// + public async Task InitializeAsync(Action? seeder = null) + { + AppLog.Information( + "正在初始化数据库 Provider={Provider}, AppVersion={AppVersion}", + _config.Provider, + GetApplicationVersion()); + + // 1. 自动迁移(如果启用)。MigrateAsync 会按迁移历史顺序执行全部待处理迁移, + // 支持用户从较旧软件版本直接升级到当前版本。 + if (_config.AutoMigrate) + { + if (_config.RecreateDatabase) + { + AppLog.Warning( + "RecreateDatabase=true,将删除并重建当前连接指向的数据库。Provider={Provider}", + _config.Provider); + + await _context.Database.EnsureDeletedAsync(); + } + + await MigrateAsync(); + } + else + { + var canConnect = await CanConnectAsync(); + if (!canConnect) + { + throw new InvalidOperationException( + $"无法连接到数据库 [{_config.Provider}],请检查连接字符串和数据库服务状态。"); + } + } + + // 2. 种子数据 + if (seeder != null) + { + seeder(_context, _serviceProvider); + await _context.SaveChangesAsync(); + } + } + + /// + /// 测试数据库连接是否正常。 + /// + public async Task CanConnectAsync() + { + try + { + return await _context.Database.CanConnectAsync(); + } + catch + { + return false; + } + } + + /// + /// 执行待处理的迁移。 + /// 使用 EF Core 原生迁移机制,自动检测并应用 Schema 变更。 + /// + public async Task MigrateAsync() + { + try + { + var appliedMigrations = (await _context.Database.GetAppliedMigrationsAsync()).ToList(); + var pendingMigrations = await _context.Database.GetPendingMigrationsAsync(); + + if (pendingMigrations.Any()) + { + if (appliedMigrations.Count == 0) + { + AppLog.Information( + "未检测到已应用迁移,将按当前 Provider={Provider} 从 0 构建完整表结构", + _config.Provider); + } + + AppLog.Information( + "当前已应用 {AppliedCount} 个迁移,检测到 {PendingCount} 个待执行迁移: {Migrations}", + appliedMigrations.Count, + pendingMigrations.Count(), + string.Join(", ", pendingMigrations)); + + await _context.Database.MigrateAsync(); + + AppLog.Information("数据库迁移完成({Count} 个迁移已应用)", pendingMigrations.Count()); + } + else + { + AppLog.Information("数据库已是最新版本,无需迁移"); + } + } + catch (Exception ex) + { + AppLog.Error(ex, "数据库迁移失败"); + throw; + } + } + + /// + /// 获取当前应用程序的版本号,优先读取 AssemblyInformationalVersion,回退到 AssemblyVersion。 + /// + /// 应用程序版本字符串。 + private static string GetApplicationVersion() + { + var assembly = Assembly.GetEntryAssembly() ?? typeof(TContext).Assembly; + return assembly + .GetCustomAttribute() + ?.InformationalVersion + ?? assembly.GetName().Version?.ToString() + ?? "unknown"; + } + + /// + /// 获取数据库当前版本信息。 + /// + public async Task GetVersionInfoAsync() + { + var appliedMigrations = await _context.Database.GetAppliedMigrationsAsync(); + var pendingMigrations = await _context.Database.GetPendingMigrationsAsync(); + + return new DatabaseVersionInfo + { + Provider = _config.Provider.ToString(), + AppliedMigrations = appliedMigrations.ToList(), + PendingMigrations = pendingMigrations.ToList(), + IsLatest = !pendingMigrations.Any(), + CanConnect = await CanConnectAsync(), + }; + } + + /// + /// 生成从指定迁移到最新版本的 SQL 脚本(用于生产环境审计)。 + /// + public string GenerateMigrationScript(string? fromMigration = null) + { + var migrator = _context.GetService(); + return fromMigration is null + ? migrator.GenerateScript() + : migrator.GenerateScript(fromMigration); + } + + /// + /// 确保数据库已创建(不执行迁移,适用于简单场景)。 + /// + public bool EnsureCreated() + { + return _context.Database.EnsureCreated(); + } + } + + /// + /// 数据库版本信息 DTO。 + /// + public class DatabaseVersionInfo + { + /// + /// 获取或设置数据库提供程序名称。 + /// + public string Provider { get; set; } = string.Empty; + /// + /// 获取或设置已应用的迁移列表。 + /// + public List AppliedMigrations { get; set; } = new(); + /// + /// 获取或设置待应用的迁移列表。 + /// + public List PendingMigrations { get; set; } = new(); + /// + /// 获取或设置是否为最新版本。 + /// + public bool IsLatest { get; set; } + /// + /// 获取或设置数据库是否可连接。 + /// + public bool CanConnect { get; set; } + } +} diff --git a/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs b/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs new file mode 100644 index 0000000..33e4477 --- /dev/null +++ b/Avalonia-EFCore/Database/DatabaseProviderRegistry.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; + +namespace Avalonia_EFCore.Database +{ + /// + /// 数据库提供程序注册表 —— 统一注册所有支持的提供程序配置委托。 + /// 具体使用哪个提供程序由各宿主项目决定: + /// Avalonia-API:从 appsettings.json 的 DatabaseConfiguration 节读取; + /// Avalonia-PC :固定使用 SQLite。 + /// + public static class DatabaseProviderRegistry + { + /// + /// 提供程序配置委托:optionsBuilder, connectionString, timeout → void + /// + public delegate void ProviderConfigurator(DbContextOptionsBuilder optionsBuilder, string connectionString, int timeout); + + /// + /// 保存已注册的数据库提供程序及其配置委托。 + /// + private static readonly Dictionary _providers = new(); + + /// + /// 注册一个数据库提供程序。 + /// + public static void Register(DatabaseProvider provider, ProviderConfigurator configurator) + { + _providers[provider] = configurator; + } + + /// + /// 尝试获取注册的提供程序配置。 + /// + public static bool TryGet(DatabaseProvider provider, out ProviderConfigurator configurator) + { + return _providers.TryGetValue(provider, out configurator!); + } + + /// + /// 注册所有内置提供程序的默认配置(四个包均已内置在 Avalonia-EFCore 中)。 + /// 注册完成后由调用方根据自身需求选择具体的 。 + /// + public static void RegisterDefaults() + { + Register(DatabaseProvider.SQLite, (opts, cs, timeout) => + opts.UseSqlite(cs, o => o.CommandTimeout(timeout))); + + Register(DatabaseProvider.SqlServer, (opts, cs, timeout) => + opts.UseSqlServer(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); })); + + Register(DatabaseProvider.PostgreSQL, (opts, cs, timeout) => + opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); })); + + Register(DatabaseProvider.MySQL, (opts, cs, timeout) => + opts.UseMySQL(cs, o => o.CommandTimeout(timeout))); + } + } +} + diff --git a/Avalonia-EFCore/Database/ProviderAppDataContexts.cs b/Avalonia-EFCore/Database/ProviderAppDataContexts.cs new file mode 100644 index 0000000..13bf6cd --- /dev/null +++ b/Avalonia-EFCore/Database/ProviderAppDataContexts.cs @@ -0,0 +1,30 @@ +namespace Avalonia_EFCore.Database +{ + /// + /// SQLite 专用 DbContext,用于隔离 SQLite 迁移集。 + /// + public sealed class SqliteAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } + + /// + /// SQL Server 专用 DbContext,用于隔离 SQL Server 迁移集。 + /// + public sealed class SqlServerAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } + + /// + /// PostgreSQL 专用 DbContext,用于隔离 PostgreSQL 迁移集。 + /// + public sealed class PostgreSqlAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } + + /// + /// MySQL 专用 DbContext,用于隔离 MySQL 迁移集。 + /// + public sealed class MySqlAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig) + { + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs new file mode 100644 index 0000000..fd0442c --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs @@ -0,0 +1,175 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + [Migration("20260520082626_AutoMigration_20260520162543")] + partial class AutoMigration_20260520162543 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs new file mode 100644 index 0000000..49f8086 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs @@ -0,0 +1,103 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using MySql.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + /// + public partial class AutoMigration_20260520162543 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + userid = table.Column(name: "user-id", type: "int", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "varchar(128)", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false), + expiresat = table.Column(name: "expires-at", type: "datetime(6)", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "datetime(6)", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "varchar(128)", maxLength: 128, nullable: true), + device = table.Column(type: "varchar(200)", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "varchar(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "用户主键") + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + name = table.Column(type: "varchar(100)", maxLength: 100, nullable: true, comment: "用户名称"), + email = table.Column(type: "varchar(200)", maxLength: 200, nullable: true, comment: "用户邮箱"), + phonenumber = table.Column(name: "phone-number", type: "varchar(50)", maxLength: 50, nullable: true, comment: "电话号码"), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.id); + }, + comment: "用户实体,演示数据库 CRUD 操作") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "天气预报主键") + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + date = table.Column(type: "date", nullable: false, comment: "预报日期"), + temperaturec = table.Column(name: "temperature-c", type: "int", nullable: false, comment: "摄氏温度"), + summary = table.Column(type: "varchar(200)", maxLength: 200, nullable: true, comment: "天气摘要"), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.id); + }, + comment: "天气预报数据实体") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + + migrationBuilder.DropTable( + name: "user"); + + migrationBuilder.DropTable( + name: "weather-forecast"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..cca9450 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,181 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + [Migration("20260520083306_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..6d9aca2 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "varchar(200)", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..3ab0fa9 --- /dev/null +++ b/Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs @@ -0,0 +1,178 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + partial class MySqlAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs new file mode 100644 index 0000000..4d16e82 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs @@ -0,0 +1,184 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + [Migration("20260520082617_AutoMigration_20260520162543")] + partial class AutoMigration_20260520162543 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs new file mode 100644 index 0000000..507419f --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs @@ -0,0 +1,97 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AutoMigration_20260520162543 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + userid = table.Column(name: "user-id", type: "integer", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "character varying(128)", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false), + expiresat = table.Column(name: "expires-at", type: "timestamp with time zone", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "timestamp with time zone", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "character varying(128)", maxLength: 128, nullable: true), + device = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "character varying(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token"); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column(type: "integer", nullable: false, comment: "用户主键") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: true, comment: "用户名称"), + email = table.Column(type: "character varying(200)", maxLength: 200, nullable: true, comment: "用户邮箱"), + phonenumber = table.Column(name: "phone-number", type: "character varying(50)", maxLength: 50, nullable: true, comment: "电话号码"), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.id); + }, + comment: "用户实体,演示数据库 CRUD 操作"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + id = table.Column(type: "integer", nullable: false, comment: "天气预报主键") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + date = table.Column(type: "date", nullable: false, comment: "预报日期"), + temperaturec = table.Column(name: "temperature-c", type: "integer", nullable: false, comment: "摄氏温度"), + summary = table.Column(type: "character varying(200)", maxLength: 200, nullable: true, comment: "天气摘要"), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.id); + }, + comment: "天气预报数据实体"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + + migrationBuilder.DropTable( + name: "user"); + + migrationBuilder.DropTable( + name: "weather-forecast"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..eb576d5 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + [Migration("20260520083254_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..b0f17c3 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "character varying(200)", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..9655547 --- /dev/null +++ b/Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs @@ -0,0 +1,187 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + partial class PostgreSqlAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs new file mode 100644 index 0000000..1dacf6c --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260514000100_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .HasComment("用户主键") + .HasColumnName("id") + .ValueGeneratedOnAdd(); + + b.Property("CreatedAt") + .HasComment("创建时间") + .HasColumnName("created-at"); + + b.Property("Email") + .HasComment("用户邮箱") + .HasColumnName("email") + .HasMaxLength(200); + + b.Property("Name") + .HasComment("用户名称") + .HasColumnName("name") + .HasMaxLength(100); + + b.Property("UpdatedAt") + .HasComment("更新时间") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .HasComment("天气预报主键") + .HasColumnName("id") + .ValueGeneratedOnAdd(); + + b.Property("CreatedAt") + .HasComment("创建时间") + .HasColumnName("created-at"); + + b.Property("Date") + .HasComment("预报日期") + .HasColumnName("date"); + + b.Property("Summary") + .HasComment("天气摘要") + .HasColumnName("summary") + .HasMaxLength(200); + + b.Property("TemperatureC") + .HasComment("摄氏温度") + .HasColumnName("temperature-c"); + + b.Property("UpdatedAt") + .HasComment("更新时间") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs new file mode 100644 index 0000000..589d107 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + /// + /// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。 + /// + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + Id = table.Column(name: "id", nullable: false, comment: "用户主键") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("Sqlite:Autoincrement", true) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(name: "name", maxLength: 100, nullable: true, comment: "用户名称"), + Email = table.Column(name: "email", maxLength: 200, nullable: true, comment: "用户邮箱"), + CreatedAt = table.Column(name: "created-at", nullable: false, comment: "创建时间"), + UpdatedAt = table.Column(name: "updated-at", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.Id); + }, + comment: "用户实体,演示数据库 CRUD 操作"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + Id = table.Column(name: "id", nullable: false, comment: "天气预报主键") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("Sqlite:Autoincrement", true) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Date = table.Column(name: "date", nullable: false, comment: "预报日期"), + TemperatureC = table.Column(name: "temperature-c", nullable: false, comment: "摄氏温度"), + Summary = table.Column(name: "summary", maxLength: 200, nullable: true, comment: "天气摘要"), + CreatedAt = table.Column(name: "created-at", nullable: false, comment: "创建时间"), + UpdatedAt = table.Column(name: "updated-at", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.Id); + }, + comment: "天气预报数据实体"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "weather-forecast"); + migrationBuilder.DropTable(name: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs new file mode 100644 index 0000000..3425bda --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs @@ -0,0 +1,113 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260515072045_AutoMigration_20260515152037")] + partial class AutoMigration_20260515152037 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs new file mode 100644 index 0000000..42d50b7 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + /// + public partial class AutoMigration_20260515152037 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "phone-number", + table: "user", + type: "TEXT", + maxLength: 50, + nullable: true, + comment: "电话号码"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "phone-number", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs new file mode 100644 index 0000000..2cf45c8 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs @@ -0,0 +1,173 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260515085847_AutoMigration_20260515165835")] + partial class AutoMigration_20260515165835 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs new file mode 100644 index 0000000..3388890 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + /// + public partial class AutoMigration_20260515165835 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + userid = table.Column(name: "user-id", type: "INTEGER", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "TEXT", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "TEXT", nullable: false), + expiresat = table.Column(name: "expires-at", type: "TEXT", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "TEXT", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "TEXT", maxLength: 128, nullable: true), + device = table.Column(type: "TEXT", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "TEXT", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..13eb309 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,179 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260520083230_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..01ff29d --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "TEXT", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..6438beb --- /dev/null +++ b/Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs @@ -0,0 +1,176 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + partial class SqliteAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs new file mode 100644 index 0000000..cb50f7f --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs @@ -0,0 +1,184 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + [Migration("20260520082607_AutoMigration_20260520162543")] + partial class AutoMigration_20260520162543 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs new file mode 100644 index 0000000..cb9d496 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs @@ -0,0 +1,96 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + /// + public partial class AutoMigration_20260520162543 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api-refresh-token", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + userid = table.Column(name: "user-id", type: "int", nullable: false), + tokenhash = table.Column(name: "token-hash", type: "nvarchar(128)", maxLength: 128, nullable: false), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false), + expiresat = table.Column(name: "expires-at", type: "datetime2", nullable: false), + revokedat = table.Column(name: "revoked-at", type: "datetime2", nullable: true), + replacedbytokenhash = table.Column(name: "replaced-by-token-hash", type: "nvarchar(128)", maxLength: 128, nullable: true), + device = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + ipaddress = table.Column(name: "ip-address", type: "nvarchar(64)", maxLength: 64, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk-api-refresh-token", x => x.id); + }, + comment: "API refresh token"); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "用户主键") + .Annotation("SqlServer:Identity", "1, 1"), + name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true, comment: "用户名称"), + email = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "用户邮箱"), + phonenumber = table.Column(name: "phone-number", type: "nvarchar(50)", maxLength: 50, nullable: true, comment: "电话号码"), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-user", x => x.id); + }, + comment: "用户实体,演示数据库 CRUD 操作"); + + migrationBuilder.CreateTable( + name: "weather-forecast", + columns: table => new + { + id = table.Column(type: "int", nullable: false, comment: "天气预报主键") + .Annotation("SqlServer:Identity", "1, 1"), + date = table.Column(type: "date", nullable: false, comment: "预报日期"), + temperaturec = table.Column(name: "temperature-c", type: "int", nullable: false, comment: "摄氏温度"), + summary = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "天气摘要"), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false, comment: "创建时间"), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false, comment: "更新时间") + }, + constraints: table => + { + table.PrimaryKey("pk-weather-forecast", x => x.id); + }, + comment: "天气预报数据实体"); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-hash", + table: "api-refresh-token", + column: "token-hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-api-refresh-token-user-id", + table: "api-refresh-token", + column: "user-id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api-refresh-token"); + + migrationBuilder.DropTable( + name: "user"); + + migrationBuilder.DropTable( + name: "weather-forecast"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs new file mode 100644 index 0000000..a594598 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + [Migration("20260520083242_AutoMigration_20260520163216")] + partial class AutoMigration_20260520163216 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs new file mode 100644 index 0000000..907c311 --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + /// + public partial class AutoMigration_20260520163216 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password-hash", + table: "user", + type: "nvarchar(200)", + maxLength: 200, + nullable: true, + comment: "密码哈希值"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password-hash", + table: "user"); + } + } +} diff --git a/Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs new file mode 100644 index 0000000..955a92c --- /dev/null +++ b/Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs @@ -0,0 +1,187 @@ +// +using System; +using Avalonia_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Avalonia_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + partial class SqlServerAppDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Avalonia-EFCore/Models/ApiRefreshTokenEntity.cs b/Avalonia-EFCore/Models/ApiRefreshTokenEntity.cs new file mode 100644 index 0000000..ef03000 --- /dev/null +++ b/Avalonia-EFCore/Models/ApiRefreshTokenEntity.cs @@ -0,0 +1,79 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Avalonia_EFCore.Models +{ + /// + /// API refresh token。只保存哈希,不保存明文 token。 + /// + [Comment("API refresh token")] + [Table("api-refresh-token")] + public class ApiRefreshTokenEntity + { + /// + /// 获取或设置主键 ID(自增)。 + /// + [Key] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + /// + /// 获取或设置关联的用户 ID。 + /// + [Column("user-id")] + public int UserId { get; set; } + + /// + /// 获取或设置 Token 的 SHA256 哈希值,用于安全存储和查询。 + /// + [Column("token-hash")] + [MaxLength(128)] + public string TokenHash { get; set; } = string.Empty; + + /// + /// 获取或设置 Token 创建时间。 + /// + [Column("created-at")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// 获取或设置 Token 过期时间。 + /// + [Column("expires-at")] + public DateTime ExpiresAt { get; set; } + + /// + /// 获取或设置 Token 撤销时间,null 表示尚未撤销。 + /// + [Column("revoked-at")] + public DateTime? RevokedAt { get; set; } + + /// + /// 获取或设置替换此 Token 的新 Token 哈希值(轮换时设置)。 + /// + [Column("replaced-by-token-hash")] + [MaxLength(128)] + public string? ReplacedByTokenHash { get; set; } + + /// + /// 获取或设置创建设备标识(如 User-Agent)。 + /// + [Column("device")] + [MaxLength(200)] + public string? Device { get; set; } + + /// + /// 获取或设置创建时的客户端 IP 地址。 + /// + [Column("ip-address")] + [MaxLength(64)] + public string? IpAddress { get; set; } + + /// + /// 获取 Token 是否有效(未被撤销且未过期)。 + /// + public bool IsActive => RevokedAt is null && ExpiresAt > DateTime.UtcNow; + } +} diff --git a/Avalonia-EFCore/Models/UserEntity.cs b/Avalonia-EFCore/Models/UserEntity.cs new file mode 100644 index 0000000..89b2326 --- /dev/null +++ b/Avalonia-EFCore/Models/UserEntity.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Avalonia_EFCore.Models +{ + /// + /// 用户实体 —— 演示数据库 CRUD 操作。 + /// + [Comment("用户实体,演示数据库 CRUD 操作")] + [Table("user")] + public class UserEntity + { + /// + /// 获取或设置用户主键 ID(自增)。 + /// + [Key] + [Comment("用户主键")] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// 获取或设置用户名称。 + /// + [Comment("用户名称")] + [Column("name")] + [MaxLength(100)] + public string? Name { get; set; } + + /// + /// 获取或设置用户密码哈希值。 + /// + [Comment("密码哈希值")] + [Column("password-hash")] + [MaxLength(200)] + public string? PasswordHash { get; set; } + + /// + /// 获取或设置用户邮箱。 + /// + [Comment("用户邮箱")] + [Column("email")] + [MaxLength(200)] + public string? Email { get; set; } + + /// + /// 获取或设置用户电话号码。 + /// + [Comment("电话号码")] + [Column("phone-number")] + [MaxLength(50)] + public string? PhoneNumber { get; set; } + + /// + /// 获取或设置用户创建时间。 + /// + [Comment("创建时间")] + [Column("created-at")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// 获取或设置用户最后更新时间。 + /// + [Comment("更新时间")] + [Column("updated-at")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Avalonia-EFCore/Models/WeatherForecast.cs b/Avalonia-EFCore/Models/WeatherForecast.cs new file mode 100644 index 0000000..c2547b0 --- /dev/null +++ b/Avalonia-EFCore/Models/WeatherForecast.cs @@ -0,0 +1,28 @@ +namespace Avalonia_EFCore.Models +{ + /// + /// 天气预报数据模型(内存/DTO 用,非数据库实体)。 + /// + public class WeatherForecast + { + /// + /// 获取或设置预报日期。 + /// + public DateOnly Date { get; set; } + + /// + /// 获取或设置摄氏温度。 + /// + public int TemperatureC { get; set; } + + /// + /// 获取华氏温度(根据摄氏温度自动计算)。 + /// + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + /// + /// 获取或设置天气摘要。 + /// + public string? Summary { get; set; } + } +} diff --git a/Avalonia-EFCore/Models/WeatherForecastEntity.cs b/Avalonia-EFCore/Models/WeatherForecastEntity.cs new file mode 100644 index 0000000..c5a3772 --- /dev/null +++ b/Avalonia-EFCore/Models/WeatherForecastEntity.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Avalonia_EFCore.Models +{ + /// + /// 天气预报数据实体。 + /// + [Comment("天气预报数据实体")] + [Table("weather-forecast")] + public class WeatherForecastEntity + { + /// + /// 获取或设置天气预报主键 ID(自增)。 + /// + [Key] + [Comment("天气预报主键")] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// 获取或设置预报日期。 + /// + [Comment("预报日期")] + [Column("date")] + public DateOnly Date { get; set; } + + /// + /// 获取或设置摄氏温度。 + /// + [Comment("摄氏温度")] + [Column("temperature-c")] + public int TemperatureC { get; set; } + + /// + /// 获取或设置天气摘要。 + /// + [Comment("天气摘要")] + [Column("summary")] + [MaxLength(200)] + public string? Summary { get; set; } + + /// + /// 获取或设置记录创建时间。 + /// + [Comment("创建时间")] + [Column("created-at")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// 获取或设置记录最后更新时间。 + /// + [Comment("更新时间")] + [Column("updated-at")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/Avalonia-PC/.github/copilot-instructions.md b/Avalonia-PC/.github/copilot-instructions.md new file mode 100644 index 0000000..a0ea5b3 --- /dev/null +++ b/Avalonia-PC/.github/copilot-instructions.md @@ -0,0 +1,4 @@ +# Copilot Instructions + +## 项目指南 +- 用户偏好:仅修改明确要求的内容,不要做额外改动(如未请求的 ViewModel DI 注册)。 \ No newline at end of file diff --git a/Avalonia-PC/App.axaml b/Avalonia-PC/App.axaml new file mode 100644 index 0000000..97a2bd1 --- /dev/null +++ b/Avalonia-PC/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Avalonia-PC/App.axaml.cs b/Avalonia-PC/App.axaml.cs new file mode 100644 index 0000000..8c1845b --- /dev/null +++ b/Avalonia-PC/App.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia_PC.ViewModels; +using Avalonia_PC.Views; +using Microsoft.Extensions.DependencyInjection; + +namespace Avalonia_PC +{ + /// + /// Avalonia 应用程序入口类,负责初始化 XAML 资源和设置主窗口。 + /// + public partial class App : Application + { + /// + /// 加载 Avalonia XAML 资源。 + /// + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + /// + /// 框架初始化完成后设置主窗口和数据上下文。 + /// + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = Program.Services.GetRequiredService(); + desktop.MainWindow.DataContext = new MainWindowViewModel(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/Avalonia-PC/Assets/avalonia-logo.ico b/Avalonia-PC/Assets/avalonia-logo.ico new file mode 100644 index 0000000..f7da8bb Binary files /dev/null and b/Avalonia-PC/Assets/avalonia-logo.ico differ diff --git a/Avalonia-PC/Authentication/DefaultPcThirdPartyAuthorizationClient.cs b/Avalonia-PC/Authentication/DefaultPcThirdPartyAuthorizationClient.cs new file mode 100644 index 0000000..0ff5ff4 --- /dev/null +++ b/Avalonia-PC/Authentication/DefaultPcThirdPartyAuthorizationClient.cs @@ -0,0 +1,50 @@ +using Avalonia_Services.Services.AuthService; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia_PC.Authentication +{ + /// + /// 第三方授权客户端占位实现。接入真实第三方接口时替换此服务即可。 + /// + public sealed class DefaultPcThirdPartyAuthorizationClient : IPcThirdPartyAuthorizationClient + { + /// + /// 验证第三方授权码是否有效。默认实现将 "invalid" 视为授权丢失,其余视为有效。 + /// + /// 第三方授权码。 + /// 取消令牌。 + /// 授权检查结果。 + public Task ValidateAuthorizationCodeAsync( + string authorizationCode, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(authorizationCode) || + string.Equals(authorizationCode, "invalid", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(ThirdPartyAuthCheckResult.AuthorizationLost); + } + + return Task.FromResult(ThirdPartyAuthCheckResult.Valid); + } + + /// + /// 刷新第三方授权。默认实现总是返回 TemporaryFailure,表示暂时无法刷新。 + /// + /// 授权引用标识。 + /// 取消令牌。 + /// 授权检查结果。 + public Task RefreshAuthorizationAsync( + string authorizationReference, + CancellationToken cancellationToken = default) + { + if (string.Equals(authorizationReference, "invalid", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(ThirdPartyAuthCheckResult.AuthorizationLost); + } + + return Task.FromResult(ThirdPartyAuthCheckResult.TemporaryFailure); + } + } +} diff --git a/Avalonia-PC/Authentication/PcAuthEndpointService.cs b/Avalonia-PC/Authentication/PcAuthEndpointService.cs new file mode 100644 index 0000000..270b01b --- /dev/null +++ b/Avalonia-PC/Authentication/PcAuthEndpointService.cs @@ -0,0 +1,94 @@ +using Authentication; +using Avalonia_Common.Core; +using Avalonia_Services.Core; +using Avalonia_Services.Services.AuthService; +using System; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Avalonia_PC.Authentication +{ + /// + /// PC 端鉴权端点服务,实现 , + /// 处理授权码登录、Token 刷新和登出操作。 + /// + public sealed class PcAuthEndpointService(PcGlobalTokenService tokenService) : IPcAuthEndpointService + { + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + /// + public async Task AuthorizeAsync(ServiceEndpointContext ctx) + { + var request = Deserialize(ctx.Body); + var token = await tokenService.AuthorizeAsync(request?.AuthorizationCode); + if (token is null) + { + ctx.StatusCode = 401; + return ResponseHelper.Failure(401, "授权失败"); + } + + return ResponseHelper.Ok(token, "授权成功"); + } + + /// + public async Task RefreshAsync(ServiceEndpointContext ctx) + { + var request = Deserialize(ctx.Body); + var token = request?.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization")); + var refreshed = await tokenService.RefreshAsync(token); + if (refreshed is null) + { + ctx.StatusCode = 401; + return ResponseHelper.Failure(401, "授权已失效"); + } + + return ResponseHelper.Ok(refreshed, "刷新成功"); + } + + /// + public Task LogoutAsync(ServiceEndpointContext ctx) + { + var request = Deserialize(ctx.Body); + var token = request?.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization")); + tokenService.Logout(token); + return Task.FromResult(ResponseHelper.Succeed("退出成功")); + } + + /// + /// 将 JSON 请求体反序列化为指定类型。 + /// + /// 目标类型。 + /// JSON 请求体字符串,可为空。 + /// 反序列化后的对象;若 body 为空则返回默认值。 + private static T? Deserialize(string? body) + { + return string.IsNullOrWhiteSpace(body) + ? default + : JsonSerializer.Deserialize(body, JsonOptions); + } + + /// + /// 从 Authorization 头中提取 Bearer Token。 + /// + /// Authorization 头的值。 + /// 提取的 Token 字符串;若无法提取则返回 null。 + private static string? ExtractBearerToken(string? authorization) + { + if (string.IsNullOrWhiteSpace(authorization)) + { + return null; + } + + /// + /// Bearer Token 的前缀常量。 + /// + const string prefix = "Bearer "; + return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) + ? authorization[prefix.Length..].Trim() + : authorization.Trim(); + } + } +} diff --git a/Avalonia-PC/Authentication/PcAuthService.cs b/Avalonia-PC/Authentication/PcAuthService.cs new file mode 100644 index 0000000..0624539 --- /dev/null +++ b/Avalonia-PC/Authentication/PcAuthService.cs @@ -0,0 +1,60 @@ +using Authentication; +using Avalonia_Services.Core; +using System; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Avalonia_PC.Authentication +{ + /// + /// PC 端鉴权服务,基于全局 Token 验证用户身份,实现 。 + /// + public sealed class PcAuthService(PcGlobalTokenService tokenService) : IAuthService + { + /// + public async Task AuthenticateAsync(ServiceEndpointContext context) + { + var token = ExtractBearerToken(context.GetHeader("Authorization")); + if (!await tokenService.ValidateAsync(token)) + { + return null; + } + + var identity = new ClaimsIdentity( + [ + new Claim(ClaimTypes.NameIdentifier, "pc-local"), + new Claim(ClaimTypes.Name, "PC授权用户"), + new Claim(ClaimTypes.Role, "SuperAdmin"), + new Claim(ClaimTypes.Role, "Admin"), + new Claim("auth_type", "pc-global-token"), + ], + "pc-global-token"); + + return new ClaimsPrincipal(identity); + } + + /// + public Task AuthorizeAsync(ClaimsPrincipal user, string policy) + { + return Task.FromResult(user.Identity?.IsAuthenticated == true); + } + + /// + /// 从 Authorization 头中提取 Bearer Token。 + /// + /// Authorization 头的值。 + /// 提取的 Token 字符串;若无法提取则返回 null。 + private static string? ExtractBearerToken(string? authorization) + { + if (string.IsNullOrWhiteSpace(authorization)) + { + return null; + } + + string prefix = "Bearer "; + return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) + ? authorization[prefix.Length..].Trim() + : authorization.Trim(); + } + } +} diff --git a/Avalonia-PC/Authentication/PcGlobalTokenService.cs b/Avalonia-PC/Authentication/PcGlobalTokenService.cs new file mode 100644 index 0000000..27e8bb8 --- /dev/null +++ b/Avalonia-PC/Authentication/PcGlobalTokenService.cs @@ -0,0 +1,227 @@ +using Avalonia_Services.Services.AuthService; +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Authentication +{ + /// + /// PC 端全局 Token 服务,管理全局唯一的访问 Token。 + /// 支持授权码登录、Token 刷新、有效性验证和登出, + /// 在第三方授权暂时失败时使用缩短有效期的临时 Token。 + /// + public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient) + { + /// + /// 超级管理员角色集合。 + /// + private static readonly string[] SuperRoles = ["SuperAdmin", "Admin"]; + /// + /// 线程同步锁。 + /// + private readonly object _syncRoot = new(); + /// + /// 当前 Token 状态。 + /// + private PcTokenState? _current; + + /// + /// 正常 Token 有效期(8 小时)。 + /// + private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8); + /// + /// 第三方暂时失败时的 Token 有效期(20 分钟)。 + /// + private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20); + /// + /// 第三方暂时失败的最长容忍窗口(24 小时),超出后清除 Token。 + /// + private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24); + + /// + /// 使用授权码进行登录授权,验证成功后颁发全局 Token。 + /// + /// 第三方授权码。 + /// 取消令牌。 + /// Token 响应;若授权码无效则返回 null。 + public async Task AuthorizeAsync(string? authorizationCode, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(authorizationCode)) + { + return null; + } + + var result = await thirdPartyClient.ValidateAuthorizationCodeAsync(authorizationCode, cancellationToken); + if (result != ThirdPartyAuthCheckResult.Valid) + { + return null; + } + + return IssueToken(authorizationCode, NormalLifetime, resetTemporaryFailureWindow: true); + } + + /// + /// 刷新当前 Token,向第三方验证授权引用是否仍然有效。 + /// 根据第三方返回结果决定是续期、降级为临时 Token 还是清除。 + /// + /// 当前 Token。 + /// 取消令牌。 + /// 新的 Token 响应;若授权丢失则返回 null。 + public async Task RefreshAsync(string? token, CancellationToken cancellationToken = default) + { + PcTokenState? current; + lock (_syncRoot) + { + current = IsCurrentToken(token) ? _current : null; + } + + if (current is null) + { + return null; + } + + var result = await thirdPartyClient.RefreshAuthorizationAsync(current.AuthorizationReference, cancellationToken); + return result switch + { + ThirdPartyAuthCheckResult.Valid => IssueToken(current.AuthorizationReference, NormalLifetime, resetTemporaryFailureWindow: true), + ThirdPartyAuthCheckResult.AuthorizationLost => ClearAndReturnNull(), + ThirdPartyAuthCheckResult.TemporaryFailure => RefreshAfterTemporaryFailure(current), + _ => null, + }; + } + + /// + /// 验证 Token 是否有效,若已过期则尝试自动刷新。 + /// + /// 要验证的 Token。 + /// 取消令牌。 + /// Token 是否有效。 + public async Task ValidateAsync(string? token, CancellationToken cancellationToken = default) + { + PcTokenState? current; + lock (_syncRoot) + { + if (!IsCurrentToken(token)) + { + return false; + } + + current = _current; + if (current is not null && current.ExpiresAt > DateTime.UtcNow) + { + return true; + } + } + + return await RefreshAsync(token, cancellationToken) is not null; + } + + /// + /// 登出并清除当前 Token。 + /// + /// 要清除的 Token。 + public void Logout(string? token) + { + lock (_syncRoot) + { + if (IsCurrentToken(token)) + { + _current = null; + } + } + } + + /// + /// 颁发新的全局 Token。 + /// + /// 授权引用标识。 + /// Token 有效期。 + /// 是否重置暂时失败窗口。 + /// 包含 Token 和过期时间的响应。 + private PcTokenResponse IssueToken(string authorizationReference, TimeSpan lifetime, bool resetTemporaryFailureWindow) + { + var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + var now = DateTime.UtcNow; + var state = new PcTokenState( + HashToken(token), + authorizationReference, + now.Add(lifetime), + resetTemporaryFailureWindow ? null : _current?.TemporaryFailureStartedAt ?? now); + + lock (_syncRoot) + { + _current = state; + } + + return new PcTokenResponse(token, state.ExpiresAt, SuperRoles); + } + + /// + /// 在第三方暂时失败时刷新 Token。若超出最大容忍窗口则清除 Token。 + /// + /// 当前 Token 状态。 + /// 新的临时 Token 响应;若超出容忍窗口则返回 null。 + private PcTokenResponse? RefreshAfterTemporaryFailure(PcTokenState current) + { + var startedAt = current.TemporaryFailureStartedAt ?? DateTime.UtcNow; + if (DateTime.UtcNow - startedAt > MaxTemporaryFailureWindow) + { + return ClearAndReturnNull(); + } + + return IssueToken(current.AuthorizationReference, TemporaryFailureLifetime, resetTemporaryFailureWindow: false); + } + + /// + /// 清除当前 Token 并返回 null。 + /// + /// 始终返回 null。 + private PcTokenResponse? ClearAndReturnNull() + { + lock (_syncRoot) + { + _current = null; + } + + return null; + } + + /// + /// 检查给定 Token 是否与当前持有的 Token 匹配。 + /// + /// 要检查的 Token。 + /// 是否匹配。 + private bool IsCurrentToken(string? token) + { + return !string.IsNullOrWhiteSpace(token) && + _current is not null && + string.Equals(_current.TokenHash, HashToken(token), StringComparison.Ordinal); + } + + /// + /// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。 + /// + /// Token 原文。 + /// SHA256 哈希后的十六进制字符串。 + private static string HashToken(string token) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); + return Convert.ToHexString(bytes); + } + + /// + /// 保存当前全局 Token 的内部状态。 + /// + /// Token 的 SHA256 哈希值。 + /// 授权引用标识。 + /// 过期时间。 + /// 第三方暂时失败的起始时间。 + private sealed record PcTokenState( + string TokenHash, + string AuthorizationReference, + DateTime ExpiresAt, + DateTime? TemporaryFailureStartedAt); + } +} diff --git a/Avalonia-PC/Avalonia-PC.csproj b/Avalonia-PC/Avalonia-PC.csproj new file mode 100644 index 0000000..9be16cf --- /dev/null +++ b/Avalonia-PC/Avalonia-PC.csproj @@ -0,0 +1,48 @@ + + + WinExe + net10.0 + enable + app.manifest + Assets\avalonia-logo.ico + true + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + None + All + + + + + + + + + + + + diff --git a/Avalonia-PC/Avalonia-PC.csproj.user b/Avalonia-PC/Avalonia-PC.csproj.user new file mode 100644 index 0000000..bc9c889 --- /dev/null +++ b/Avalonia-PC/Avalonia-PC.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Avalonia-PC + + \ No newline at end of file diff --git a/Avalonia-PC/Avalonia-PC.slnx b/Avalonia-PC/Avalonia-PC.slnx new file mode 100644 index 0000000..a181409 --- /dev/null +++ b/Avalonia-PC/Avalonia-PC.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Avalonia-PC/Program.cs b/Avalonia-PC/Program.cs new file mode 100644 index 0000000..2283dd5 --- /dev/null +++ b/Avalonia-PC/Program.cs @@ -0,0 +1,97 @@ +using Authentication; +using Avalonia; +using Avalonia_Common.Infrastructure; +using Avalonia_EFCore.Database; +using Avalonia_PC.Authentication; +using Avalonia_PC.Views; +using Avalonia_Services.Core; +using Avalonia_Services.Endpoints; +using Avalonia_Services.Services; +using Avalonia_Services.Services.AuthService; +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using System; + +namespace Avalonia_PC +{ + /// + /// 桌面应用程序入口类,负责配置 DI 容器、初始化数据库和启动 Avalonia 框架。 + /// + internal sealed class Program + { + /// + /// 获取全局 DI 服务提供程序。 + /// + public static IServiceProvider Services { get; private set; } = null!; + + /// + /// 应用程序主入口点。 + /// + /// 命令行参数。 + [STAThread] + public static void Main(string[] args) + { + // 初始化日志系统 + AppLog.Initialize(LoggingConfiguration.CreateDefaultLogger(logDir: "logs")); + + AppLog.Information("Avalonia-PC 正在启动..."); + + ConfigureServices(); + + // 初始化数据库(自动迁移 + 种子数据) + Services.InitializeDatabase(); + +#if DEBUG + // 开启 WebView2 远程调试,启动后在 Edge 中访问 edge://inspect 调试网页 + Environment.SetEnvironmentVariable( + "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", + "--remote-debugging-port=9222 --auto-open-devtools-for-tabs"); +#endif + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + /// + /// 配置 DI 容器,注册数据库、业务服务、鉴权服务和统一端点。 + /// + private static void ConfigureServices() + { + var services = new ServiceCollection(); + + // ---- 数据库 ---- + // 注册默认数据库提供程序(SQLite / MySQL / PostgreSQL / SqlServer) + DatabaseProviderRegistry.RegisterDefaults(); + + // 桌面端固定使用 SQLite 本地数据库 + services.AddAppDatabase(DatabaseConfiguration.ForSQLite("app.db")); + + // ---- 业务服务 ---- + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // ---- 端点注册 ---- + var endpointBuilder = new ServiceEndpointBuilder(); + AppEndpoints.Configure(endpointBuilder); + AuthEndpoints.ConfigurePc(endpointBuilder); + var endpoints = endpointBuilder.Build(); + services.AddSingleton(endpoints); + + // 注册 Window + services.AddTransient(sp => new MainWindow(sp)); + + Services = services.BuildServiceProvider(); + } + + /// + /// 构建 Avalonia 应用程序(供可视化设计器使用,请勿删除)。 + /// + /// Avalonia 应用构建器。 + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); + } +} diff --git a/Avalonia-PC/Properties/launchSettings.json b/Avalonia-PC/Properties/launchSettings.json new file mode 100644 index 0000000..05300e7 --- /dev/null +++ b/Avalonia-PC/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Avalonia-PC": { + "commandName": "Project" + }, + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/Avalonia-PC/ViewLocator.cs b/Avalonia-PC/ViewLocator.cs new file mode 100644 index 0000000..dfc998d --- /dev/null +++ b/Avalonia-PC/ViewLocator.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia_PC.ViewModels; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia_PC +{ + /// + /// Given a view model, returns the corresponding view if possible. + /// + [RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] + /// + /// 视图定位器,根据 ViewModel 类型自动查找对应的 View, + /// 实现 IDataTemplate 以支持 Avalonia 的数据模板机制。 + /// + public class ViewLocator : IDataTemplate + { + /// + /// 根据 ViewModel 实例构建对应的 View 控件。 + /// 约定:将 ViewModels 命名空间中的 ViewModel 替换为 Views 命名空间中的同名 View。 + /// + /// ViewModel 实例。 + /// 对应的 View 控件;若未找到则返回 TextBlock 显示错误信息。 + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + /// + /// 判断数据对象是否为 ViewModel 类型(以 "ViewModel" 结尾)。 + /// + /// 要判断的数据对象。 + /// 是否为 ViewModel。 + public bool Match(object? data) + { + return data is ViewModelBase; + } + } +} diff --git a/Avalonia-PC/ViewModels/MainWindowViewModel.cs b/Avalonia-PC/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..50bf8e5 --- /dev/null +++ b/Avalonia-PC/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,13 @@ +namespace Avalonia_PC.ViewModels +{ + /// + /// 主窗口的 ViewModel,提供问候语等绑定属性。 + /// + public partial class MainWindowViewModel : ViewModelBase + { + /// + /// 获取问候语文本。 + /// + public string Greeting { get; } = "Welcome to Avalonia!"; + } +} diff --git a/Avalonia-PC/ViewModels/ViewModelBase.cs b/Avalonia-PC/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..e1414cd --- /dev/null +++ b/Avalonia-PC/ViewModels/ViewModelBase.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Avalonia_PC.ViewModels +{ + /// + /// ViewModel 基类,继承自 CommunityToolkit.Mvvm 的 ObservableObject, + /// 提供属性变更通知功能。 + /// + public abstract class ViewModelBase : ObservableObject + { + } +} diff --git a/Avalonia-PC/Views/MainWindow.BridgeScript.cs b/Avalonia-PC/Views/MainWindow.BridgeScript.cs new file mode 100644 index 0000000..41210d3 --- /dev/null +++ b/Avalonia-PC/Views/MainWindow.BridgeScript.cs @@ -0,0 +1,345 @@ +namespace Avalonia_PC.Views +{ + /// + /// MainWindow 的分部类,定义注入 WebView2 的 JavaScript Bridge 脚本。 + /// + public partial class MainWindow + { + private const string BridgeScript = """ +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; + const NativeXMLHttpRequest = window.XMLHttpRequest; + + const sendAppBridgeRequest = ({ requestUrl, method, headers, body, timeoutMs = 30000 }) => { + 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}`)); + }, timeoutMs); + + 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, + headers, + body + })); + + return responsePromise; + }; + + 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(); + } + + return sendAppBridgeRequest({ + requestUrl, + method: init?.method ?? request?.method ?? 'GET', + headers, + body: body ?? null, + timeoutMs: 30000 + }); + }; + + /// + /// WebView2 Bridge 中的 XMLHttpRequest 替代实现,将 app:// 请求拦截并转为 C# 调用。 + /// + class BridgeXMLHttpRequest { + constructor() { + this._native = new NativeXMLHttpRequest(); + this._isAppRequest = false; + this._requestUrl = ''; + this._method = 'GET'; + this._headers = {}; + this._responseHeaders = {}; + this._responseHeadersRaw = ''; + this._aborted = false; + + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.response = null; + this.responseText = ''; + this.responseType = ''; + this.responseURL = ''; + this.timeout = 0; + this.withCredentials = false; + + this.onreadystatechange = null; + this.onload = null; + this.onerror = null; + this.ontimeout = null; + this.onabort = null; + this.onloadend = null; + + this.upload = { + addEventListener: () => {}, + removeEventListener: () => {} + }; + + this._native.onreadystatechange = () => { + if (this._isAppRequest) { + return; + } + + this.readyState = this._native.readyState; + this.status = this._native.status; + this.statusText = this._native.statusText; + this.responseURL = this._native.responseURL ?? ''; + this.response = this._native.response; + this.responseText = this._native.responseText ?? ''; + this._raiseReadyStateChange(); + }; + + this._native.onload = event => { + if (!this._isAppRequest && typeof this.onload === 'function') { + this.onload(event); + } + }; + + this._native.onerror = event => { + if (!this._isAppRequest && typeof this.onerror === 'function') { + this.onerror(event); + } + }; + + this._native.ontimeout = event => { + if (!this._isAppRequest && typeof this.ontimeout === 'function') { + this.ontimeout(event); + } + }; + + this._native.onabort = event => { + if (!this._isAppRequest && typeof this.onabort === 'function') { + this.onabort(event); + } + }; + + this._native.onloadend = event => { + if (!this._isAppRequest && typeof this.onloadend === 'function') { + this.onloadend(event); + } + }; + } + + open(method, url, async = true, user, password) { + const requestUrl = typeof url === 'string' || url instanceof URL + ? url.toString() + : `${url ?? ''}`; + + this._requestUrl = requestUrl; + this._method = method ?? 'GET'; + this._isAppRequest = requestUrl.startsWith('app://'); + this._headers = {}; + this._responseHeaders = {}; + this._responseHeadersRaw = ''; + this._aborted = false; + + if (!this._isAppRequest) { + this._native.open(method, url, async, user, password); + return; + } + + this.readyState = 1; + this._raiseReadyStateChange(); + } + + setRequestHeader(name, value) { + if (!this._isAppRequest) { + this._native.setRequestHeader(name, value); + return; + } + + this._headers[name] = value; + } + + getAllResponseHeaders() { + if (!this._isAppRequest) { + return this._native.getAllResponseHeaders(); + } + + return this._responseHeadersRaw; + } + + getResponseHeader(name) { + if (!this._isAppRequest) { + return this._native.getResponseHeader(name); + } + + return this._responseHeaders[name.toLowerCase()] ?? null; + } + + overrideMimeType(mimeType) { + if (!this._isAppRequest && typeof this._native.overrideMimeType === 'function') { + this._native.overrideMimeType(mimeType); + } + } + + abort() { + if (!this._isAppRequest) { + this._native.abort(); + return; + } + + this._aborted = true; + if (typeof this.onabort === 'function') { + this.onabort(); + } + if (typeof this.onloadend === 'function') { + this.onloadend(); + } + } + + async send(body = null) { + if (!this._isAppRequest) { + this._native.send(body); + return; + } + + let requestBody = body; + if (requestBody && typeof requestBody !== 'string') { + requestBody = await new Response(requestBody).text(); + } + + try { + const response = await sendAppBridgeRequest({ + requestUrl: this._requestUrl, + method: this._method, + headers: this._headers, + body: requestBody ?? null, + timeoutMs: this.timeout > 0 ? this.timeout : 30000 + }); + + if (this._aborted) { + return; + } + + this.status = response.status; + this.statusText = response.statusText; + this.responseURL = this._requestUrl; + + this._responseHeaders = {}; + this._responseHeadersRaw = ''; + response.headers.forEach((value, key) => { + this._responseHeaders[key.toLowerCase()] = value; + this._responseHeadersRaw += `${key}: ${value}\r\n`; + }); + + const text = await response.text(); + this.responseText = text; + this.response = this.responseType === 'json' + ? (text ? JSON.parse(text) : null) + : text; + + this.readyState = 4; + this._raiseReadyStateChange(); + + if (typeof this.onload === 'function') { + this.onload(); + } + if (typeof this.onloadend === 'function') { + this.onloadend(); + } + } catch (error) { + if (this._aborted) { + return; + } + + this.status = 0; + this.statusText = ''; + this.readyState = 4; + this._raiseReadyStateChange(); + + const errorMessage = error?.message ?? ''; + if (errorMessage.includes('Timed out waiting') && typeof this.ontimeout === 'function') { + this.ontimeout(error); + } else if (typeof this.onerror === 'function') { + this.onerror(error); + } + + if (typeof this.onloadend === 'function') { + this.onloadend(); + } + } + } + + _raiseReadyStateChange() { + if (typeof this.onreadystatechange === 'function') { + this.onreadystatechange(); + } + } + } + + window.XMLHttpRequest = BridgeXMLHttpRequest; +} +"""; + } +} diff --git a/Avalonia-PC/Views/MainWindow.Routes.cs b/Avalonia-PC/Views/MainWindow.Routes.cs new file mode 100644 index 0000000..856c3fd --- /dev/null +++ b/Avalonia-PC/Views/MainWindow.Routes.cs @@ -0,0 +1,38 @@ +using Avalonia_Services.Core; +using Avalonia_Services.Endpoints; +using Avalonia_Services.Extensions; +using Avalonia_Services.Services; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia_PC.Views +{ + /// + /// MainWindow 的分部类,负责路由注册和统一端点适配。 + /// + public partial class MainWindow + { + /// + /// 统一端点适配器(替代原来的 _routes 字典)。 + /// 所有端点在 Avalonia-Services/AppEndpoints.cs 中统一定义。 + /// + private DesktopEndpointAdapter _endpointAdapter = null!; + + /// + /// 服务容器,通过构造函数注入。 + /// + private IServiceProvider _services = null!; + + /// + /// 从 DI 获取统一端点集合并构建桌面适配器。 + /// + private void RegisterRoutes() + { + // 从 DI 获取已构建的端点集合 + var endpointCollection = _services.GetRequiredService(); + _endpointAdapter = endpointCollection.CreateAdapter(_services); + } + } +} diff --git a/Avalonia-PC/Views/MainWindow.axaml b/Avalonia-PC/Views/MainWindow.axaml new file mode 100644 index 0000000..dee33b6 --- /dev/null +++ b/Avalonia-PC/Views/MainWindow.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/Avalonia-PC/Views/MainWindow.axaml.cs b/Avalonia-PC/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..88cd6dd --- /dev/null +++ b/Avalonia-PC/Views/MainWindow.axaml.cs @@ -0,0 +1,684 @@ +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 +{ + /// + /// 主窗口,承载 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) + { + 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; + } + + await _webView.InvokeScript(BridgeScript); + } + + #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) + { + 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 + { + 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 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 + } +} diff --git a/Avalonia-PC/app.manifest b/Avalonia-PC/app.manifest new file mode 100644 index 0000000..b02c8a5 --- /dev/null +++ b/Avalonia-PC/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/Avalonia-PC/www/api.js b/Avalonia-PC/www/api.js new file mode 100644 index 0000000..f14fc86 --- /dev/null +++ b/Avalonia-PC/www/api.js @@ -0,0 +1,51 @@ +// api.js - 跨端统一 API 调用层 + +const isWebView2 = () => { + return window.isWebView2 === true; +}; + +const getBaseUrl = () => { + if (isWebView2()) { + return "app://api/"; + } + + return "https://your-production-api.com/api/"; +}; + +async function callApi(endpoint, options = {}) { + const url = getBaseUrl() + endpoint; + const fetchOptions = { + method: options.method || "GET", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...(options.body && { body: JSON.stringify(options.body) }) + }; + + const token = localStorage.getItem("authToken"); + if (token) { + fetchOptions.headers.Authorization = `Bearer ${token}`; + } + + try { + const response = await fetch(url, fetchOptions); + const data = await response.json(); + console.log(data) + + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`); + } + + return data; + } catch (err) { + console.error(`API call failed: ${endpoint}`, err); + throw err; + } +} + +window.api = { + getUser: () => callApi("getUser?t=1"), + processData: (input) => callApi("processData", { method: "POST", body: { input } }), + wData: (input) => callApi("wData", { method: "POST", body: { input } }), +}; diff --git a/Avalonia-PC/www/index.html b/Avalonia-PC/www/index.html new file mode 100644 index 0000000..fe5428f --- /dev/null +++ b/Avalonia-PC/www/index.html @@ -0,0 +1,62 @@ + + + + + 跨端测试 + + +

WebView2 自定义协议演示

+ + + +

+
+    
+    
+
+
diff --git a/Avalonia-Services/Avalonia-Services.csproj b/Avalonia-Services/Avalonia-Services.csproj
new file mode 100644
index 0000000..19a6a29
--- /dev/null
+++ b/Avalonia-Services/Avalonia-Services.csproj
@@ -0,0 +1,24 @@
+
+
+  
+    net10.0
+    Avalonia_Services
+    enable
+    enable
+  
+
+  
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+    
+  
+
+
diff --git a/Avalonia-Services/Core/EndpointPrinter.cs b/Avalonia-Services/Core/EndpointPrinter.cs
new file mode 100644
index 0000000..e022a74
--- /dev/null
+++ b/Avalonia-Services/Core/EndpointPrinter.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Linq;
+
+namespace Avalonia_Services.Core
+{
+    /// 
+    /// 端点列表打印工具 —— 在应用启动时输出所有已注册的拦截接口。
+    /// 类似 Swagger 的接口清单效果。
+    /// 
+    public static class EndpointPrinter
+    {
+        /// 
+        /// 打印所有已注册端点到控制台。
+        /// 
+        public static void PrintEndpoints(
+            ServiceEndpointCollection collection,
+            string? title = null,
+            EndpointHostTarget host = EndpointHostTarget.All)
+        {
+            title ??= "API Endpoints";
+            var endpoints = collection.ForHost(host).ToList();
+
+            var maxMethodLen = endpoints.Count > 0
+                ? endpoints.Max(e => e.HttpMethod.Length)
+                : 4;
+            var maxPathLen = endpoints.Count > 0
+                ? endpoints.Max(e => e.Pattern.Length)
+                : 8;
+
+            var totalWidth = maxMethodLen + maxPathLen + 5;
+            var separator = new string('─', Math.Max(totalWidth, 50));
+
+            Console.WriteLine();
+            Console.WriteLine($"╔═ {title} ═{new string('═', Math.Max(0, totalWidth - title.Length - 3))}╗");
+            Console.WriteLine($"║ {"Method".PadRight(maxMethodLen)} │ {"Path".PadRight(maxPathLen)} │ Auth ║");
+            Console.WriteLine($"╟{separator}╢");
+
+            foreach (var ep in endpoints.OrderBy(e => e.Pattern))
+            {
+                var auth = ep.RequireAuthorization
+                    ? (ep.Roles.Count > 0 ? string.Join(",", ep.Roles) : ep.Policy ?? "✓")
+                    : "—";
+                var methodColor = ep.HttpMethod switch
+                {
+                    "GET" => ConsoleColor.Green,
+                    "POST" => ConsoleColor.Blue,
+                    "PUT" => ConsoleColor.Yellow,
+                    "DELETE" => ConsoleColor.Red,
+                    _ => ConsoleColor.Gray,
+                };
+
+                var savedColor = Console.ForegroundColor;
+
+                Console.Write("║ ");
+                Console.ForegroundColor = methodColor;
+                Console.Write(ep.HttpMethod.PadRight(maxMethodLen));
+                Console.ForegroundColor = savedColor;
+                Console.Write(" │ ");
+                Console.Write(ep.Pattern.PadRight(maxPathLen));
+                Console.Write(" │ ");
+                Console.Write(auth.PadRight(4));
+                Console.WriteLine(" ║");
+            }
+
+            Console.WriteLine($"╚{separator}╝");
+            Console.WriteLine($"  Total: {endpoints.Count} endpoint(s)");
+            Console.WriteLine();
+        }
+    }
+}
diff --git a/Avalonia-Services/Core/GlobalExceptionFilter.cs b/Avalonia-Services/Core/GlobalExceptionFilter.cs
new file mode 100644
index 0000000..a8f0825
--- /dev/null
+++ b/Avalonia-Services/Core/GlobalExceptionFilter.cs
@@ -0,0 +1,107 @@
+using Avalonia_Common.Core;
+using System;
+using System.Threading.Tasks;
+
+namespace Avalonia_Services.Core
+{
+    /// 
+    /// 全局异常拦截过滤器 —— 自动包裹所有端点处理器,无需在每个方法中写 try-catch。
+    /// 所有未捕获异常会被转为统一的 ApiResponse 错误格式。
+    /// 
+    public sealed class GlobalExceptionFilter : IEndpointFilter
+    {
+        /// 
+        /// 是否在错误响应中包含异常详情。
+        /// 
+        private readonly bool _includeDetails;
+
+        /// 
+        /// 初始化全局异常过滤器。
+        /// 
+        /// 是否在响应中包含异常详情(开发环境建议 true,生产环境 false)
+        public GlobalExceptionFilter(bool includeDetails = false)
+        {
+            _includeDetails = includeDetails;
+        }
+
+        /// 
+        /// 执行过滤器逻辑:包裹下一个委托,捕获所有未处理异常并转换为统一错误响应。
+        /// 
+        /// 请求上下文。
+        /// 管道中的下一个委托。
+        public async Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
+        {
+            try
+            {
+                await next(context);
+            }
+            catch (OperationCanceledException)
+            {
+                // 取消操作不视为错误
+                context.StatusCode = 499;
+                context.StatusMessage = "Client Closed Request";
+                context.ResponseBody = ApiResponse.Fail(499, "请求已取消");
+            }
+            catch (UnauthorizedAccessException ex)
+            {
+                context.StatusCode = 401;
+                context.StatusMessage = "Unauthorized";
+                context.ResponseBody = ApiResponse.Unauthorized(
+                    _includeDetails ? ex.Message : "未授权访问");
+            }
+            catch (InvalidOperationException ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase))
+            {
+                context.StatusCode = 404;
+                context.StatusMessage = "Not Found";
+                context.ResponseBody = ApiResponse.NotFound(
+                    _includeDetails ? ex.Message : "资源不存在");
+            }
+            catch (ArgumentException ex)
+            {
+                context.StatusCode = 400;
+                context.StatusMessage = "Bad Request";
+                context.ResponseBody = ApiResponse.BadRequest(
+                    _includeDetails ? ex.Message : "参数错误");
+            }
+            catch (Exception ex)
+            {
+                // 记录完整日志(无论是否返回详情)
+                LogException(context, ex);
+
+                context.StatusCode = 500;
+                context.StatusMessage = "Internal Server Error";
+                context.ResponseBody = ApiResponse.ServerError(
+                    _includeDetails ? ex.Message : "服务器内部错误,请联系管理员");
+
+                // 可选:在开发环境附加堆栈信息
+                if (_includeDetails)
+                {
+                    // 通过 Items 传递额外调试信息
+                    context.Items["ExceptionDetail"] = ex.ToString();
+                }
+            }
+        }
+
+        /// 
+        /// 记录异常日志,优先使用 Serilog,不可用时回退到 Console。
+        /// 
+        /// 请求上下文。
+        /// 异常对象。
+        private static void LogException(ServiceEndpointContext context, Exception ex)
+        {
+            try
+            {
+                // 使用 Serilog(如果已配置)
+                Serilog.Log.Error(ex,
+                    "全局异常拦截 | {Method} {Path} | {ExceptionType}: {Message}",
+                    context.Method, context.Path, ex.GetType().Name, ex.Message);
+            }
+            catch
+            {
+                // Serilog 不可用时回退到 Console
+                Console.Error.WriteLine(
+                    $"[ERROR] {context.Method} {context.Path} | {ex.GetType().Name}: {ex.Message}");
+            }
+        }
+    }
+}
diff --git a/Avalonia-Services/Core/IAuthService.cs b/Avalonia-Services/Core/IAuthService.cs
new file mode 100644
index 0000000..2569b67
--- /dev/null
+++ b/Avalonia-Services/Core/IAuthService.cs
@@ -0,0 +1,39 @@
+using System.Security.Claims;
+
+namespace Avalonia_Services.Core
+{
+    /// 
+    /// 鉴权服务抽象 —— 各宿主按自己的方式实现(JWT / Cookie / Token 等)。
+    /// 
+    public interface IAuthService
+    {
+        /// 
+        /// 验证请求并返回用户主体;返回 null 表示未授权。
+        /// 
+        Task AuthenticateAsync(ServiceEndpointContext context);
+
+        /// 
+        /// 检查当前用户是否有指定权限。
+        /// 
+        Task AuthorizeAsync(ClaimsPrincipal user, string policy);
+    }
+
+    /// 
+    /// 无需鉴权的默认实现(开发/公开 API 场景)。
+    /// 
+    public sealed class AnonymousAuthService : IAuthService
+    {
+        /// 
+        public Task AuthenticateAsync(ServiceEndpointContext context)
+        {
+            // 匿名用户,始终通过
+            var identity = new ClaimsIdentity("anonymous");
+            return Task.FromResult(new ClaimsPrincipal(identity));
+        }
+        /// 
+        public Task AuthorizeAsync(ClaimsPrincipal user, string policy)
+        {
+            return Task.FromResult(true);
+        }
+    }
+}
diff --git a/Avalonia-Services/Core/IEndpointFilter.cs b/Avalonia-Services/Core/IEndpointFilter.cs
new file mode 100644
index 0000000..81c4920
--- /dev/null
+++ b/Avalonia-Services/Core/IEndpointFilter.cs
@@ -0,0 +1,48 @@
+using System.Threading.Tasks;
+
+namespace Avalonia_Services.Core
+{
+    /// 
+    /// 端点过滤器抽象 —— 在请求处理前后执行逻辑。
+    /// 类似于 ASP.NET Core 的 IEndpointFilter,但可在任何宿主中使用。
+    /// 
+    public interface IEndpointFilter
+    {
+        /// 
+        /// 过滤器执行方法。
+        /// 调用 next(ctx) 继续管道;不调用则短路。
+        /// 
+        Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next);
+    }
+
+    /// 
+    /// 过滤器管道中的下一个委托。
+    /// 
+    public delegate Task EndpointFilterDelegate(ServiceEndpointContext context);
+
+    /// 
+    /// 用于包装匿名过滤器的简单实现。
+    /// 
+    internal sealed class AnonymousEndpointFilter : IEndpointFilter
+    {
+        /// 
+        /// 匿名过滤器的委托实现。
+        /// 
+        private readonly Func _filter;
+
+        /// 
+        /// 使用匿名函数创建过滤器。
+        /// 
+        /// 过滤器委托。
+        public AnonymousEndpointFilter(Func filter)
+        {
+            _filter = filter;
+        }
+
+        /// 
+        public Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
+        {
+            return _filter(context, next);
+        }
+    }
+}
diff --git a/Avalonia-Services/Core/ServiceEndpointCollection.cs b/Avalonia-Services/Core/ServiceEndpointCollection.cs
new file mode 100644
index 0000000..170d19e
--- /dev/null
+++ b/Avalonia-Services/Core/ServiceEndpointCollection.cs
@@ -0,0 +1,364 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Avalonia_Services.Core
+{
+    /// 
+    /// 端点挂载的宿主目标。
+    /// 
+    [Flags]
+    public enum EndpointHostTarget
+    {
+        /// 挂载到 Avalonia-API(ASP.NET Core Web API)。
+        Api = 1,
+        /// 挂载到 Avalonia-PC(桌面 WebView)。
+        Pc = 2,
+        /// 同时挂载到 API 和 PC。
+        All = Api | Pc,
+    }
+
+    /// 
+    /// 单个端点定义。
+    /// 
+    public class ServiceEndpoint
+    {
+        /// 路由路径,如 "api/wData"
+        public string Pattern { get; init; } = string.Empty;
+
+        /// HTTP 方法(GET/POST/PUT/DELETE)
+        public string HttpMethod { get; init; } = "GET";
+
+        /// 端点名称(用于 OpenAPI / 日志)
+        public string? Name { get; set; }
+
+        /// OpenAPI 分组标签。
+        public string? OpenApiTag { get; set; }
+
+        /// OpenAPI 摘要。
+        public string? OpenApiSummary { get; set; }
+
+        /// OpenAPI 描述。
+        public string? OpenApiDescription { get; set; }
+
+        /// OpenAPI 请求体类型。
+        public Type? OpenApiRequestType { get; set; }
+
+        /// OpenAPI 200 响应数据类型。
+        public Type? OpenApiResponseType { get; set; }
+
+        /// 端点处理器
+        public Func> Handler { get; init; } = _ => Task.FromResult(null);
+
+        /// 该端点专属的过滤器(按顺序执行)
+        public List Filters { get; init; } = new();
+
+        /// 是否需要鉴权
+        public bool RequireAuthorization { get; set; }
+
+        /// 鉴权策略名
+        public string? Policy { get; set; }
+
+        /// 允许访问该端点的角色。多个角色满足任意一个即可。
+        public List Roles { get; } = new();
+
+        /// 端点挂载的宿主。默认 API 和 PC 都挂载。
+        public EndpointHostTarget HostTarget { get; set; } = EndpointHostTarget.All;
+
+        /// 
+        /// 设置端点名称(Fluent API)。
+        /// 
+        public ServiceEndpoint WithName(string name)
+        {
+            Name = name;
+            return this;
+        }
+
+        /// 
+        /// 设置端点的 OpenAPI 元数据(标签、摘要、描述、请求/响应类型)。
+        /// 
+        /// OpenAPI 分组标签。
+        /// 简要摘要。
+        /// 详细描述。
+        /// 请求体类型。
+        /// 成功响应类型。
+        /// 当前端点实例(Fluent API)。
+        public ServiceEndpoint WithOpenApi(
+            string tag,
+            string summary,
+            string? description = null,
+            Type? requestType = null,
+            Type? responseType = null)
+        {
+            OpenApiTag = tag;
+            OpenApiSummary = summary;
+            OpenApiDescription = description;
+            OpenApiRequestType = requestType;
+            OpenApiResponseType = responseType;
+            return this;
+        }
+
+        /// 
+        /// 标记端点需要登录。
+        /// 
+        public ServiceEndpoint RequireAuth()
+        {
+            RequireAuthorization = true;
+            return this;
+        }
+
+        /// 
+        /// 标记端点需要指定角色。多个角色满足任意一个即可。
+        /// 
+        public ServiceEndpoint RequireRoles(params string[] roles)
+        {
+            RequireAuthorization = true;
+            Roles.Clear();
+            Roles.AddRange(roles.Where(role => !string.IsNullOrWhiteSpace(role)).Select(role => role.Trim()));
+            return this;
+        }
+
+        /// 
+        /// 只挂载到 Avalonia-API。
+        /// 
+        public ServiceEndpoint ApiOnly()
+        {
+            HostTarget = EndpointHostTarget.Api;
+            return this;
+        }
+
+        /// 
+        /// 只挂载到 Avalonia-PC。
+        /// 
+        public ServiceEndpoint PcOnly()
+        {
+            HostTarget = EndpointHostTarget.Pc;
+            return this;
+        }
+
+        /// 
+        /// 判断端点是否支持指定的宿主目标。
+        /// 
+        /// 要检查的宿主目标。
+        /// 是否支持。
+        public bool SupportsHost(EndpointHostTarget host)
+        {
+            return (HostTarget & host) != 0;
+        }
+    }
+
+    /// 
+    /// 端点集合 —— 所有端点的注册中心。在 Avalonia-Services 中统一配置。
+    /// 
+    public class ServiceEndpointCollection
+    {
+        /// 所有已注册的端点
+        public List Endpoints { get; } = new();
+
+        /// 
+        /// 获取指定宿主目标的所有端点。
+        /// 
+        /// 宿主目标。
+        /// 匹配的端点集合。
+        public IEnumerable ForHost(EndpointHostTarget host)
+        {
+            return Endpoints.Where(endpoint => endpoint.SupportsHost(host));
+        }
+
+        /// 作用于所有端点的全局过滤器
+        public List GlobalFilters { get; } = new();
+
+        /// 
+        /// 注册一个端点。
+        /// 
+        public ServiceEndpoint MapGet(string pattern, Func> handler)
+        {
+            return AddEndpoint(pattern, "GET", handler);
+        }
+
+        /// 
+        /// 注册一个带服务依赖注入的 GET 端点。
+        /// 
+        /// 服务类型。
+        /// 路由路径。
+        /// 接受服务实例和上下文的处理器。
+        /// 已注册的端点实例。
+        public ServiceEndpoint MapGet(
+            string pattern,
+            Func> handler)
+            where TService : notnull
+        {
+            return MapGet(pattern, CreateServiceHandler(handler));
+        }
+
+        /// 
+        /// 注册一个 POST 端点。
+        /// 
+        public ServiceEndpoint MapPost(string pattern, Func> handler)
+        {
+            return AddEndpoint(pattern, "POST", handler);
+        }
+
+        /// 
+        /// 注册一个带服务依赖注入的 POST 端点。
+        /// 
+        /// 服务类型。
+        /// 路由路径。
+        /// 接受服务实例和上下文的处理器。
+        /// 已注册的端点实例。
+        public ServiceEndpoint MapPost(
+            string pattern,
+            Func> handler)
+            where TService : notnull
+        {
+            return MapPost(pattern, CreateServiceHandler(handler));
+        }
+
+        /// 
+        /// 注册一个 PUT 端点。
+        /// 
+        public ServiceEndpoint MapPut(string pattern, Func> handler)
+        {
+            return AddEndpoint(pattern, "PUT", handler);
+        }
+
+        /// 
+        /// 注册一个带服务依赖注入的 PUT 端点。
+        /// 
+        /// 服务类型。
+        /// 路由路径。
+        /// 接受服务实例和上下文的处理器。
+        /// 已注册的端点实例。
+        public ServiceEndpoint MapPut(
+            string pattern,
+            Func> handler)
+            where TService : notnull
+        {
+            return MapPut(pattern, CreateServiceHandler(handler));
+        }
+
+        /// 
+        /// 注册一个 DELETE 端点。
+        /// 
+        public ServiceEndpoint MapDelete(string pattern, Func> handler)
+        {
+            return AddEndpoint(pattern, "DELETE", handler);
+        }
+
+        /// 
+        /// 注册一个带服务依赖注入的 DELETE 端点。
+        /// 
+        /// 服务类型。
+        /// 路由路径。
+        /// 接受服务实例和上下文的处理器。
+        /// 已注册的端点实例。
+        public ServiceEndpoint MapDelete(
+            string pattern,
+            Func> handler)
+            where TService : notnull
+        {
+            return MapDelete(pattern, CreateServiceHandler(handler));
+        }
+
+        /// 
+        /// 添加全局过滤器(作用于所有端点)。
+        /// 
+        public ServiceEndpointCollection AddGlobalFilter(IEndpointFilter filter)
+        {
+            GlobalFilters.Add(filter);
+            return this;
+        }
+
+        /// 
+        /// 通过匿名函数添加全局过滤器。
+        /// 
+        public ServiceEndpointCollection AddGlobalFilter(Func filter)
+        {
+            GlobalFilters.Add(new AnonymousEndpointFilter(filter));
+            return this;
+        }
+
+        /// 
+        /// 内部方法,创建端点并添加到集合。
+        /// 
+        /// 路由路径。
+        /// HTTP 方法。
+        /// 端点处理器。
+        /// 已创建的端点实例。
+        private ServiceEndpoint AddEndpoint(string pattern, string method, Func> handler)
+        {
+            var endpoint = new ServiceEndpoint
+            {
+                Pattern = pattern,
+                HttpMethod = method,
+                Handler = handler,
+            };
+            Endpoints.Add(endpoint);
+            return endpoint;
+        }
+
+        /// 
+        /// 创建自动从 DI 解析服务实例并调用处理器的委托包装。
+        /// 
+        /// 服务类型。
+        /// 接受服务实例和上下文的处理器。
+        /// 包装后的处理器委托。
+        private static Func> CreateServiceHandler(
+            Func> handler)
+            where TService : notnull
+        {
+            return async ctx =>
+            {
+                var serviceProvider = ctx.Items["ServiceProvider"] as IServiceProvider
+                    ?? throw new InvalidOperationException("ServiceProvider 未注入。");
+
+                await using var scope = serviceProvider.CreateAsyncScope();
+                var service = scope.ServiceProvider.GetRequiredService();
+                return await handler(service, ctx);
+            };
+        }
+    }
+
+    /// 
+    /// 构建器 —— 提供 Fluent API 来配置所有端点。
+    /// 
+    public class ServiceEndpointBuilder
+    {
+        /// 
+        /// 端点集合
+        /// 
+        public ServiceEndpointCollection Endpoints { get; } = new();
+
+        /// 
+        /// 鉴权服务(默认匿名)
+        /// 
+        public IAuthService AuthService { get; set; } = new AnonymousAuthService();
+
+        /// 
+        /// 配置端点(在此方法中调用 endpoints.MapGet 等)。
+        /// 
+        public ServiceEndpointBuilder ConfigureEndpoints(Action configure)
+        {
+            configure(Endpoints);
+            return this;
+        }
+
+        /// 
+        /// 设置鉴权服务。
+        /// 
+        public ServiceEndpointBuilder UseAuthService(IAuthService authService)
+        {
+            AuthService = authService;
+            return this;
+        }
+
+        /// 
+        /// 构建最终的端点集合。
+        /// 
+        public ServiceEndpointCollection Build()
+        {
+            return Endpoints;
+        }
+    }
+}
diff --git a/Avalonia-Services/Core/ServiceEndpointContext.cs b/Avalonia-Services/Core/ServiceEndpointContext.cs
new file mode 100644
index 0000000..94be302
--- /dev/null
+++ b/Avalonia-Services/Core/ServiceEndpointContext.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+
+namespace Avalonia_Services.Core
+{
+    /// 
+    /// 抽象的请求上下文,屏蔽不同宿主(ASP.NET Core / Desktop WebView)的差异。
+    /// 
+    public class ServiceEndpointContext
+    {
+        /// 
+        /// 请求路径,例如 "api/wData"
+        /// 
+        public string Path { get; init; } = string.Empty;
+
+        /// 
+        /// HTTP 方法(GET, POST, PUT, DELETE 等)
+        /// 
+        public string Method { get; init; } = "GET";
+
+        /// 
+        /// 请求头
+        /// 
+        public Dictionary Headers { get; init; } = new(StringComparer.OrdinalIgnoreCase);
+
+        /// 
+        /// 请求体(原始字符串)
+        /// 
+        public string? Body { get; set; }
+
+        /// 
+        /// 查询参数
+        /// 
+        public Dictionary Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
+
+        /// 
+        /// 响应状态码
+        /// 
+        public int StatusCode { get; set; } = 200;
+
+        /// 
+        /// 响应状态描述
+        /// 
+        public string StatusMessage { get; set; } = "OK";
+
+        /// 
+        /// 响应头
+        /// 
+        public Dictionary ResponseHeaders { get; set; } = new(StringComparer.OrdinalIgnoreCase)
+        {
+            ["Content-Type"] = "application/json; charset=utf-8"
+        };
+
+        /// 
+        /// 响应体
+        /// 
+        public object? ResponseBody { get; set; }
+
+        /// 
+        /// 存储在请求生命周期中的任意数据(由中间件/过滤器使用)
+        /// 
+        public Dictionary Items { get; init; } = new();
+
+        /// 
+        /// 获取请求头值
+        /// 
+        public string? GetHeader(string key)
+        {
+            return Headers.TryGetValue(key, out var value) ? value : null;
+        }
+
+        /// 
+        /// 设置响应头
+        /// 
+        public void SetResponseHeader(string key, string value)
+        {
+            ResponseHeaders[key] = value;
+        }
+    }
+}
diff --git a/Avalonia-Services/Endpoints/AppEndpoints.cs b/Avalonia-Services/Endpoints/AppEndpoints.cs
new file mode 100644
index 0000000..f2e9dee
--- /dev/null
+++ b/Avalonia-Services/Endpoints/AppEndpoints.cs
@@ -0,0 +1,147 @@
+using Avalonia_Common.Core;
+using Avalonia_EFCore.Database;
+using Avalonia_EFCore.Models;
+using Avalonia_Services.Core;
+using Avalonia_Services.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace Avalonia_Services.Endpoints
+{
+    /// 
+    /// 统一端点配置 —— 所有业务端点在此定义一次。
+    /// 这是 Avalonia-API 和 Avalonia-PC 的唯一入口。
+    /// 
+    public static class AppEndpoints
+    {
+        /// 
+        /// 配置所有业务端点。调用方传入 builder,按需叠加鉴权、过滤器等。
+        /// 
+        /// 端点构建器
+        /// 是否在错误响应中包含异常详情(开发环境 true)
+        public static ServiceEndpointBuilder Configure(ServiceEndpointBuilder builder, bool includeDetails = false)
+        {
+            // ---- 全局异常拦截(自动捕获所有端点中未处理的异常) ----
+            builder.Endpoints.AddGlobalFilter(new GlobalExceptionFilter(includeDetails));
+
+            builder.ConfigureEndpoints(endpoints =>
+            {
+                // ---- 全局日志过滤器(记录每个请求) ----
+                endpoints.AddGlobalFilter(async (ctx, next) =>
+                {
+                    Serilog.Log.Debug("→ {Method} {Path}", ctx.Method, ctx.Path);
+                    await next(ctx);
+                    Serilog.Log.Debug("← {Method} {Path} | {StatusCode}", ctx.Method, ctx.Path, ctx.StatusCode);
+                });
+
+                // ---- 业务端点注册 ----
+                // 天气预报(从数据库读取)
+                endpoints.MapGet("api/wData", GetWeatherForecastsAsync)
+                    .WithOpenApi("Weather", "获取天气预报信息。")
+                    .WithName("GetWeatherForecast");
+
+                // 获取用户(演示从数据库查询)
+                endpoints.MapGet("api/getUser", GetUserFromDatabaseAsync)
+                    .WithName("GetUser");
+
+                // 处理数据(POST — 演示参数处理)
+                endpoints.MapPost("api/processData", ProcessDataAsync)
+                    .WithName("ProcessData");
+
+                // ---- 需要鉴权的端点示例 ----
+                // endpoints.MapGet("api/admin/dashboard", AdminDashboardAsync)
+                //     .WithName("AdminDashboard")
+                //     .RequireAuthorization = true
+                //     .Policy = "AdminOnly";
+
+            });
+
+            return builder;
+        }
+
+        #region 业务处理方法
+
+        /// 
+        /// 从数据库查询天气预报(优先数据库,回退到内存生成)。
+        /// 
+        private static async Task GetWeatherForecastsAsync(ServiceEndpointContext ctx)
+        {
+            var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
+
+            // 尝试从数据库读取
+            if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db)
+            {
+                var dbForecasts = await db.WeatherForecasts
+                    .OrderByDescending(f => f.Date)
+                    .Take(5)
+                    .ToListAsync();
+
+                if (dbForecasts.Count > 0)
+                {
+                    return ResponseHelper.Ok(dbForecasts, "获取天气预报成功(来自数据库)");
+                }
+            }
+
+            // 回退:内存生成(数据库为空时)
+            var service = sp?.GetService(typeof(WeatherForecastService)) as WeatherForecastService
+                ?? new WeatherForecastService();
+
+            var forecasts = service.GetWeatherForecasts();
+            return ResponseHelper.Ok(forecasts, "获取天气预报成功(内存生成)");
+        }
+
+        /// 
+        /// 从数据库获取用户信息(演示数据库查询),若无数据则返回演示用户。
+        /// 
+        /// 服务端点上下文。
+        /// 用户信息。
+        private static async Task GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
+        {
+            var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
+
+            // 尝试从数据库读取用户
+            if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db)
+            {
+                var users = await db.Set().Take(1).ToListAsync();
+                if (users.Count > 0)
+                {
+                    return ResponseHelper.Ok(users[0], "获取用户成功(来自数据库)");
+                }
+            }
+
+            // 回退:演示数据
+            await Task.Delay(100);
+            var user = new { id = 1, name = "张三", email = "zhangsan@example.com" };
+            return ResponseHelper.Ok(user);
+        }
+
+        /// 
+        /// 处理前端发送的数据(POST 演示),将数据存入数据库或转为大写返回。
+        /// 
+        /// 服务端点上下文。
+        /// 处理结果。
+        private static async Task ProcessDataAsync(ServiceEndpointContext ctx)
+        {
+            var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
+
+            // 演示:将收到的数据存入数据库
+            var input = ctx.Body ?? string.Empty;
+            if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db && !string.IsNullOrWhiteSpace(input))
+            {
+                var forecast = new WeatherForecastEntity
+                {
+                    Date = DateOnly.FromDateTime(DateTime.Now),
+                    TemperatureC = 20,
+                    Summary = input,
+                };
+                db.WeatherForecasts.Add(forecast);
+                await db.SaveChangesAsync();
+                return ResponseHelper.Ok(forecast, "数据已存入数据库");
+            }
+
+            await Task.Delay(200);
+            return ResponseHelper.Ok(new { input, processed = input.ToUpperInvariant() });
+        }
+
+        #endregion
+    }
+}
diff --git a/Avalonia-Services/Endpoints/AuthEndpoints.cs b/Avalonia-Services/Endpoints/AuthEndpoints.cs
new file mode 100644
index 0000000..37d2159
--- /dev/null
+++ b/Avalonia-Services/Endpoints/AuthEndpoints.cs
@@ -0,0 +1,61 @@
+using Avalonia_Services.Core;
+using Avalonia_Services.Services.AuthService;
+
+namespace Avalonia_Services.Endpoints
+{
+    /// 
+    /// 认证端点统一入口。端点定义在这里,宿主项目只提供对应实现。
+    /// 
+    public static class AuthEndpoints
+    {
+        /// 
+        /// 配置 API 端鉴权端点(登录、刷新、登出)。
+        /// 
+        /// 端点构建器。
+        public static void ConfigureApi(ServiceEndpointBuilder builder)
+        {
+            builder.ConfigureEndpoints(endpoints =>
+            {
+                endpoints.MapPost("api/auth/login", (service, ctx) => service.LoginAsync(ctx))
+                    .WithName("ApiLogin")
+                    .WithOpenApi("Auth", "API 登录,返回 access token 和 refresh token。", "", typeof(ApiLoginRequest), typeof(AuthTokenResponse))
+                    .ApiOnly();
+
+                endpoints.MapPost("api/auth/refresh", (service, ctx) => service.RefreshAsync(ctx))
+                    .WithName("ApiRefresh")
+                    .WithOpenApi("Auth", "API refresh token 轮换。", "", typeof(ApiRefreshTokenRequest), typeof(AuthTokenResponse))
+                    .ApiOnly();
+
+                endpoints.MapPost("api/auth/logout", (service, ctx) => service.LogoutAsync(ctx))
+                    .WithName("ApiLogout")
+                    .WithOpenApi("Auth", "API 退出登录并吊销 refresh token。", "", typeof(ApiLogoutRequest))
+                    .ApiOnly();
+            });
+        }
+
+        /// 
+        /// 配置 PC 端鉴权端点(授权码登录、刷新、登出)。
+        /// 
+        /// 端点构建器。
+        public static void ConfigurePc(ServiceEndpointBuilder builder)
+        {
+            builder.ConfigureEndpoints(endpoints =>
+            {
+                endpoints.MapPost("api/pc/auth/authorize", (service, ctx) => service.AuthorizeAsync(ctx))
+                    .WithName("PcAuthorize")
+                    .WithOpenApi("Auth", "PC 授权码登录,生成本地全局 token。", "", typeof(PcAuthorizeRequest), typeof(PcTokenResponse))
+                    .PcOnly();
+
+                endpoints.MapPost("api/pc/auth/refresh", (service, ctx) => service.RefreshAsync(ctx))
+                    .WithName("PcRefresh")
+                    .WithOpenApi("Auth", "PC 全局 token 刷新。", "", typeof(PcRefreshRequest), typeof(PcTokenResponse))
+                    .PcOnly();
+
+                endpoints.MapPost("api/pc/auth/logout", (service, ctx) => service.LogoutAsync(ctx))
+                    .WithName("PcLogout")
+                    .WithOpenApi("Auth", "PC 退出登录。", "", typeof(PcLogoutRequest))
+                    .PcOnly();
+            });
+        }
+    }
+}
diff --git a/Avalonia-Services/Extensions/DesktopEndpointAdapter.cs b/Avalonia-Services/Extensions/DesktopEndpointAdapter.cs
new file mode 100644
index 0000000..edb4291
--- /dev/null
+++ b/Avalonia-Services/Extensions/DesktopEndpointAdapter.cs
@@ -0,0 +1,233 @@
+using Avalonia_Services.Core;
+
+namespace Avalonia_Services.Extensions
+{
+    /// 
+    /// Desktop (Avalonia-PC) 端点适配器。
+    /// 将统一端点转换为桌面端可用的路由处理器,支持过滤器和鉴权管道。
+    /// 
+    public class DesktopEndpointAdapter
+    {
+        /// 
+        /// 统一端点集合。
+        /// 
+        private readonly ServiceEndpointCollection _endpoints;
+        /// 
+        /// 鉴权服务。
+        /// 
+        private readonly IAuthService _authService;
+        /// 
+        /// DI 服务提供程序。
+        /// 
+        private readonly IServiceProvider _serviceProvider;
+
+        /// 
+        /// 匹配后的路由结果(与原有 RouteDispatchResult 兼容)。
+        /// 
+        public class RouteResult
+        {
+            /// 
+            /// 获取是否匹配到路由。
+            /// 
+            public bool IsMatched { get; init; }
+            /// 
+            /// 获取 HTTP 状态码。
+            /// 
+            public int StatusCode { get; init; } = 200;
+            /// 
+            /// 获取状态描述文本。
+            /// 
+            public string StatusMessage { get; init; } = "";
+            /// 
+            /// 获取响应数据。
+            /// 
+            public object? Data { get; init; }
+            /// 
+            /// 获取响应头字典。
+            /// 
+            public Dictionary ResponseHeaders { get; init; } = new();
+
+            /// 
+            /// 创建成功响应结果。
+            /// 
+            /// 响应数据。
+            /// 端点上下文。
+            /// 路由结果。
+            public static RouteResult Success(object? data, ServiceEndpointContext ctx)
+            {
+                return new RouteResult
+                {
+                    IsMatched = true,
+                    StatusCode = ctx.StatusCode,
+                    StatusMessage = ctx.StatusMessage,
+                    Data = data,
+                    ResponseHeaders = new Dictionary(ctx.ResponseHeaders, StringComparer.OrdinalIgnoreCase),
+                };
+            }
+
+            /// 
+            /// 创建 404 未找到响应。
+            /// 
+            /// 表示未匹配的路由结果。
+            public static RouteResult NotFound() => new()
+            {
+                IsMatched = false,
+                StatusCode = 404,
+                StatusMessage = "Not Found",
+            };
+        }
+
+        /// 
+        /// 初始化桌面端点适配器。
+        /// 
+        /// 端点集合。
+        /// 鉴权服务。
+        /// DI 服务提供程序。
+        public DesktopEndpointAdapter(
+            ServiceEndpointCollection endpoints,
+            IAuthService authService,
+            IServiceProvider serviceProvider)
+        {
+            _endpoints = endpoints;
+            _authService = authService;
+            _serviceProvider = serviceProvider;
+        }
+
+        /// 
+        /// 处理来自前端(WebView2 Bridge)的请求。
+        /// 
+        /// 规范化路径,如 "api/wData"
+        /// HTTP 方法
+        /// 请求体字符串
+        /// 请求头字典
+        /// 查询参数字典
+        public async Task HandleRequestAsync(
+            string path,
+            string method,
+            string? body,
+            Dictionary? headers = null,
+            Dictionary? query = null)
+        {
+            // 查找匹配的端点(忽略大小写 + 方法匹配)
+            var endpoint = _endpoints.Endpoints.FirstOrDefault(e =>
+                e.SupportsHost(EndpointHostTarget.Pc) &&
+                string.Equals(e.Pattern, path, StringComparison.OrdinalIgnoreCase) &&
+                string.Equals(e.HttpMethod, method, StringComparison.OrdinalIgnoreCase));
+
+            if (endpoint is null)
+            {
+                return RouteResult.NotFound();
+            }
+
+            // 构建上下文
+            var ctx = new ServiceEndpointContext
+            {
+                Path = path,
+                Method = method,
+                Body = body,
+                Headers = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase),
+                Query = query ?? new Dictionary(StringComparer.OrdinalIgnoreCase),
+                Items = { ["ServiceProvider"] = _serviceProvider },
+            };
+
+            try
+            {
+                // 1. 鉴权检查
+                if (endpoint.RequireAuthorization)
+                {
+                    var user = await _authService.AuthenticateAsync(ctx);
+                    if (user is null)
+                    {
+                        ctx.StatusCode = 401;
+                        ctx.StatusMessage = "Unauthorized";
+                        ctx.ResponseBody = new { success = false, error = "Unauthorized" };
+                        return RouteResult.Success(ctx.ResponseBody, ctx);
+                    }
+
+                    if (endpoint.Roles.Count > 0)
+                    {
+                        var authorized = await _authService.AuthorizeAsync(user, $"roles:{string.Join(',', endpoint.Roles)}");
+                        if (!authorized)
+                        {
+                            ctx.StatusCode = 403;
+                            ctx.StatusMessage = "Forbidden";
+                            ctx.ResponseBody = new { success = false, error = "Forbidden" };
+                            return RouteResult.Success(ctx.ResponseBody, ctx);
+                        }
+                    }
+                    else if (!string.IsNullOrEmpty(endpoint.Policy))
+                    {
+                        var authorized = await _authService.AuthorizeAsync(user, endpoint.Policy);
+                        if (!authorized)
+                        {
+                            ctx.StatusCode = 403;
+                            ctx.StatusMessage = "Forbidden";
+                            ctx.ResponseBody = new { success = false, error = "Forbidden" };
+                            return RouteResult.Success(ctx.ResponseBody, ctx);
+                        }
+                    }
+
+                    ctx.Items["User"] = user;
+                }
+
+                // 2. 构建过滤管道:全局过滤器 → 端点过滤器 → 处理器
+                var pipeline = BuildPipeline(endpoint);
+
+                // 3. 执行管道
+                await pipeline(ctx);
+
+                return RouteResult.Success(ctx.ResponseBody, ctx);
+            }
+            catch (Exception ex)
+            {
+                ctx.StatusCode = 500;
+                ctx.StatusMessage = "Internal Server Error";
+                ctx.ResponseBody = new { success = false, error = ex.Message };
+                return RouteResult.Success(ctx.ResponseBody, ctx);
+            }
+        }
+
+        /// 
+        /// 构建过滤管道(全局过滤器 + 端点过滤器 → 端点处理器)。
+        /// 
+        private EndpointFilterDelegate BuildPipeline(ServiceEndpoint endpoint)
+        {
+            // 最内层:端点处理器
+            EndpointFilterDelegate handler = async (ctx) =>
+            {
+                ctx.ResponseBody = await endpoint.Handler(ctx);
+            };
+
+            // 先包裹端点专属过滤器(后注册的先执行)
+            var filters = new List();
+            filters.AddRange(_endpoints.GlobalFilters);
+            filters.AddRange(endpoint.Filters);
+
+            for (int i = filters.Count - 1; i >= 0; i--)
+            {
+                var filter = filters[i];
+                var next = handler;
+                handler = (ctx) => filter.InvokeAsync(ctx, next);
+            }
+
+            return handler;
+        }
+    }
+
+    /// 
+    /// Desktop 端的辅助扩展。不依赖 IServiceCollection(由宿主项目自行完成 DI 注册)。
+    /// 
+    public static class DesktopServiceExtensions
+    {
+        /// 
+        /// 快速构建 DesktopEndpointAdapter(用于非 DI 场景如 MainWindow)。
+        /// 
+        public static DesktopEndpointAdapter CreateAdapter(
+            this ServiceEndpointCollection endpoints,
+            IServiceProvider serviceProvider)
+        {
+            var auth = (serviceProvider.GetService(typeof(IAuthService)) as IAuthService) ?? new AnonymousAuthService();
+            return new DesktopEndpointAdapter(endpoints, auth, serviceProvider);
+        }
+    }
+}
diff --git a/Avalonia-Services/Services/AuthService/AuthContracts.cs b/Avalonia-Services/Services/AuthService/AuthContracts.cs
new file mode 100644
index 0000000..5e7abe5
--- /dev/null
+++ b/Avalonia-Services/Services/AuthService/AuthContracts.cs
@@ -0,0 +1,98 @@
+namespace Avalonia_Services.Services.AuthService
+{
+    /// 
+    /// API 登录请求。
+    /// 
+    /// 账号(邮箱或用户名)。
+    /// 密码。
+    /// 请求的角色列表。
+    public sealed record ApiLoginRequest(string? Account, string? Password, string[]? Roles = null);
+
+    /// 
+    /// API Refresh Token 请求。
+    /// 
+    /// 刷新令牌。
+    public sealed record ApiRefreshTokenRequest(string? RefreshToken);
+
+    /// 
+    /// API 登出请求。
+    /// 
+    /// 要撤销的刷新令牌。
+    public sealed record ApiLogoutRequest(string? RefreshToken);
+
+    /// 
+    /// 认证 Token 响应,包含 Access Token 和 Refresh Token 及其过期时间。
+    /// 
+    /// 访问令牌。
+    /// 刷新令牌。
+    /// 访问令牌过期时间。
+    /// 刷新令牌过期时间。
+    /// 用户角色列表。
+    public sealed record AuthTokenResponse(
+        string AccessToken,
+        string RefreshToken,
+        DateTime AccessTokenExpiresAt,
+        DateTime RefreshTokenExpiresAt,
+        string[] Roles);
+
+    /// 
+    /// PC 端授权码登录请求。
+    /// 
+    /// 第三方授权码。
+    public sealed record PcAuthorizeRequest(string? AuthorizationCode);
+
+    /// 
+    /// PC 端 Token 刷新请求。
+    /// 
+    /// 当前 Token。
+    public sealed record PcRefreshRequest(string? Token);
+
+    /// 
+    /// PC 端登出请求。
+    /// 
+    /// 要清除的 Token。
+    public sealed record PcLogoutRequest(string? Token);
+
+    /// 
+    /// PC 端 Token 响应。
+    /// 
+    /// 访问令牌。
+    /// 过期时间。
+    /// 用户角色列表。
+    public sealed record PcTokenResponse(string Token, DateTime ExpiresAt, string[] Roles);
+
+    /// 
+    /// 第三方授权检查结果。
+    /// 
+    public enum ThirdPartyAuthCheckResult
+    {
+        /// 授权有效。
+        Valid,
+        /// 授权已丢失。
+        AuthorizationLost,
+        /// 暂时性失败。
+        TemporaryFailure,
+    }
+
+    /// 
+    /// 第三方授权客户端接口,用于验证和刷新第三方授权。
+    /// 
+    public interface IPcThirdPartyAuthorizationClient
+    {
+        /// 
+        /// 验证第三方授权码是否有效。
+        /// 
+        /// 第三方授权码。
+        /// 取消令牌。
+        /// 授权检查结果。
+        Task ValidateAuthorizationCodeAsync(string authorizationCode, CancellationToken cancellationToken = default);
+
+        /// 
+        /// 刷新第三方授权。
+        /// 
+        /// 授权引用标识。
+        /// 取消令牌。
+        /// 授权检查结果。
+        Task RefreshAuthorizationAsync(string authorizationReference, CancellationToken cancellationToken = default);
+    }
+}
diff --git a/Avalonia-Services/Services/AuthService/AuthEndpointServices.cs b/Avalonia-Services/Services/AuthService/AuthEndpointServices.cs
new file mode 100644
index 0000000..8218001
--- /dev/null
+++ b/Avalonia-Services/Services/AuthService/AuthEndpointServices.cs
@@ -0,0 +1,59 @@
+using Avalonia_Services.Core;
+using System.Threading.Tasks;
+
+namespace Avalonia_Services.Services.AuthService
+{
+    /// 
+    /// API 鉴权端点服务接口,定义登录、刷新 Token 和登出操作。
+    /// 
+    public interface IApiAuthEndpointService
+    {
+        /// 
+        /// 处理用户登录请求。
+        /// 
+        /// 服务端点上下文。
+        /// 包含 Token 的认证响应。
+        Task LoginAsync(ServiceEndpointContext ctx);
+
+        /// 
+        /// 使用 Refresh Token 刷新 Access Token。
+        /// 
+        /// 服务端点上下文。
+        /// 新的 Token 对。
+        Task RefreshAsync(ServiceEndpointContext ctx);
+
+        /// 
+        /// 处理用户登出请求。
+        /// 
+        /// 服务端点上下文。
+        /// 登出结果。
+        Task LogoutAsync(ServiceEndpointContext ctx);
+    }
+
+    /// 
+    /// PC 端鉴权端点服务接口,定义授权码登录、Token 刷新和登出操作。
+    /// 
+    public interface IPcAuthEndpointService
+    {
+        /// 
+        /// 使用授权码进行登录授权。
+        /// 
+        /// 服务端点上下文。
+        /// 包含 Token 的认证响应。
+        Task AuthorizeAsync(ServiceEndpointContext ctx);
+
+        /// 
+        /// 刷新当前 Token。
+        /// 
+        /// 服务端点上下文。
+        /// 新的 Token 响应。
+        Task RefreshAsync(ServiceEndpointContext ctx);
+
+        /// 
+        /// 处理用户登出请求。
+        /// 
+        /// 服务端点上下文。
+        /// 登出结果。
+        Task LogoutAsync(ServiceEndpointContext ctx);
+    }
+}
diff --git a/Avalonia-Services/Services/WeatherForecastService.cs b/Avalonia-Services/Services/WeatherForecastService.cs
new file mode 100644
index 0000000..4ed23af
--- /dev/null
+++ b/Avalonia-Services/Services/WeatherForecastService.cs
@@ -0,0 +1,30 @@
+using Avalonia_EFCore.Models;
+
+namespace Avalonia_Services.Services
+{
+    /// 
+    /// 天气预报服务,随机生成未来 5 天的天气预报数据。
+    /// 
+    public class WeatherForecastService
+    {
+        private static readonly string[] Summaries =
+        [
+            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
+        ];
+
+        /// 
+        /// 生成未来 5 天的随机天气预报数据。
+        /// 
+        /// 天气预报数据集合。
+        public IEnumerable GetWeatherForecasts()
+        {
+            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
+            {
+                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
+                TemperatureC = Random.Shared.Next(-20, 55),
+                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
+            })
+            .ToArray();
+        }
+    }
+}
diff --git a/Avalonia-Web-VUE/.editorconfig b/Avalonia-Web-VUE/.editorconfig
new file mode 100644
index 0000000..3b510aa
--- /dev/null
+++ b/Avalonia-Web-VUE/.editorconfig
@@ -0,0 +1,8 @@
+[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
+charset = utf-8
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+max_line_length = 100
diff --git a/Avalonia-Web-VUE/.gitattributes b/Avalonia-Web-VUE/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/Avalonia-Web-VUE/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/Avalonia-Web-VUE/.gitignore b/Avalonia-Web-VUE/.gitignore
new file mode 100644
index 0000000..cd68f14
--- /dev/null
+++ b/Avalonia-Web-VUE/.gitignore
@@ -0,0 +1,39 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
+
+.eslintcache
+
+# Cypress
+/cypress/videos/
+/cypress/screenshots/
+
+# Vitest
+__screenshots__/
+
+# Vite
+*.timestamp-*-*.mjs
diff --git a/Avalonia-Web-VUE/.oxlintrc.json b/Avalonia-Web-VUE/.oxlintrc.json
new file mode 100644
index 0000000..d5648b9
--- /dev/null
+++ b/Avalonia-Web-VUE/.oxlintrc.json
@@ -0,0 +1,10 @@
+{
+  "$schema": "./node_modules/oxlint/configuration_schema.json",
+  "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
+  "env": {
+    "browser": true
+  },
+  "categories": {
+    "correctness": "error"
+  }
+}
diff --git a/Avalonia-Web-VUE/CHANGELOG.md b/Avalonia-Web-VUE/CHANGELOG.md
new file mode 100644
index 0000000..b05a67d
--- /dev/null
+++ b/Avalonia-Web-VUE/CHANGELOG.md
@@ -0,0 +1,13 @@
+此文件解释 Visual Studio 如何创建项目。
+
+以下工具用于生成此项目:
+- create-vite
+
+以下为生成此项目的步骤:
+- 使用 create-vite: `npm init --yes vue@latest avalonia-web -- --eslint  --typescript ` 创建 vue 项目。
+- 正在使用端口更新 `vite.config.ts`。
+- 为基本类型添加 `shims-vue.d.ts`。
+- 创建项目文件 (`avalonia-web.esproj`)。
+- 创建 `launch.json` 以启用调试。
+- 向解决方案添加项目。
+- 写入此文件。
diff --git a/Avalonia-Web-VUE/README.md b/Avalonia-Web-VUE/README.md
new file mode 100644
index 0000000..f61b56b
--- /dev/null
+++ b/Avalonia-Web-VUE/README.md
@@ -0,0 +1,48 @@
+# avalonia-web
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+
+## Recommended Browser Setup
+
+- Chromium-based browsers (Chrome, Edge, Brave, etc.):
+  - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
+  - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
+- Firefox:
+  - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
+  - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vite.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```
diff --git a/Avalonia-Web-VUE/avalonia-web-vue.esproj b/Avalonia-Web-VUE/avalonia-web-vue.esproj
new file mode 100644
index 0000000..a73336f
--- /dev/null
+++ b/Avalonia-Web-VUE/avalonia-web-vue.esproj
@@ -0,0 +1,11 @@
+
+  
+    npm run dev
+    .\
+    Vitest
+    
+    false
+    
+    $(MSBuildProjectDirectory)\dist
+  
+
\ No newline at end of file
diff --git a/Avalonia-Web-VUE/env.d.ts b/Avalonia-Web-VUE/env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/Avalonia-Web-VUE/env.d.ts
@@ -0,0 +1 @@
+/// 
diff --git a/Avalonia-Web-VUE/eslint.config.ts b/Avalonia-Web-VUE/eslint.config.ts
new file mode 100644
index 0000000..0713270
--- /dev/null
+++ b/Avalonia-Web-VUE/eslint.config.ts
@@ -0,0 +1,23 @@
+import { globalIgnores } from 'eslint/config'
+import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
+import pluginVue from 'eslint-plugin-vue'
+import pluginOxlint from 'eslint-plugin-oxlint'
+
+// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
+// import { configureVueProject } from '@vue/eslint-config-typescript'
+// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
+// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
+
+export default defineConfigWithVueTs(
+  {
+    name: 'app/files-to-lint',
+    files: ['**/*.{vue,ts,mts,tsx}'],
+  },
+
+  globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
+
+  ...pluginVue.configs['flat/essential'],
+  vueTsConfigs.recommended,
+
+  ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
+)
diff --git a/Avalonia-Web-VUE/index.html b/Avalonia-Web-VUE/index.html
new file mode 100644
index 0000000..9e5fc8f
--- /dev/null
+++ b/Avalonia-Web-VUE/index.html
@@ -0,0 +1,13 @@
+
+
+  
+    
+    
+    
+    Vite App
+  
+  
+    
+ + + diff --git a/Avalonia-Web-VUE/package-lock.json b/Avalonia-Web-VUE/package-lock.json new file mode 100644 index 0000000..10203be --- /dev/null +++ b/Avalonia-Web-VUE/package-lock.json @@ -0,0 +1,4880 @@ +{ + "name": "avalonia-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "avalonia-web", + "version": "0.0.0", + "dependencies": { + "axios": "^1.15.2", + "vue": "^3.5.32" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.12.2", + "@vitejs/plugin-vue": "^6.0.6", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.9.1", + "eslint": "^10.2.1", + "eslint-plugin-oxlint": "~1.60.0", + "eslint-plugin-vue": "~10.8.0", + "jiti": "^2.6.1", + "npm-run-all2": "^8.0.4", + "oxlint": "~1.60.0", + "typescript": "~6.0.0", + "vite": "^8.0.8", + "vite-plugin-vue-devtools": "^8.1.1", + "vue-tsc": "^3.2.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.60.0.tgz", + "integrity": "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.60.0.tgz", + "integrity": "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.60.0.tgz", + "integrity": "sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.60.0.tgz", + "integrity": "sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.60.0.tgz", + "integrity": "sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.60.0.tgz", + "integrity": "sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.60.0.tgz", + "integrity": "sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.60.0.tgz", + "integrity": "sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.60.0.tgz", + "integrity": "sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.60.0.tgz", + "integrity": "sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.60.0.tgz", + "integrity": "sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.60.0.tgz", + "integrity": "sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.60.0.tgz", + "integrity": "sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.60.0.tgz", + "integrity": "sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.60.0.tgz", + "integrity": "sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.60.0.tgz", + "integrity": "sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.60.0.tgz", + "integrity": "sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.60.0.tgz", + "integrity": "sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.60.0.tgz", + "integrity": "sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node24": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz", + "integrity": "sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.33", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.33", + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.10", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.1.1.tgz", + "integrity": "sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1", + "@vue/devtools-shared": "^8.1.1" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz", + "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.56.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.56.0", + "vue-eslint-parser": "^10.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.7.tgz", + "integrity": "sha512-Gn4q/tRxbpVGLEuARQ43p3YELlNAFgRUVCgW9U5Cr+5q4vfD2bWDWpl3ABbJMXUt5xlE1dF8dkigg2aUq7JYYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.1.2", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/language-core/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/runtime-core": "3.5.33", + "@vue/shared": "3.5.33", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-oxlint": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-oxlint/-/eslint-plugin-oxlint-1.60.0.tgz", + "integrity": "sha512-9RUD23k7ablez1qg7JWnyPYPOlbucDDqaDr+qNUi0TbIQCPqIPCLzfllgqKF9lOxlg+l17H8hISErmarvm2J1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.3.1" + }, + "peerDependencies": { + "oxlint": "~1.60.0" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.0", + "semver": "^7.6.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.0.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-all2": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz", + "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^20.5.0 || >=22.0.0", + "npm": ">= 10" + } + }, + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-all2/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm-run-all2/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/oxlint": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.60.0.tgz", + "integrity": "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.60.0", + "@oxlint/binding-android-arm64": "1.60.0", + "@oxlint/binding-darwin-arm64": "1.60.0", + "@oxlint/binding-darwin-x64": "1.60.0", + "@oxlint/binding-freebsd-x64": "1.60.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", + "@oxlint/binding-linux-arm-musleabihf": "1.60.0", + "@oxlint/binding-linux-arm64-gnu": "1.60.0", + "@oxlint/binding-linux-arm64-musl": "1.60.0", + "@oxlint/binding-linux-ppc64-gnu": "1.60.0", + "@oxlint/binding-linux-riscv64-gnu": "1.60.0", + "@oxlint/binding-linux-riscv64-musl": "1.60.0", + "@oxlint/binding-linux-s390x-gnu": "1.60.0", + "@oxlint/binding-linux-x64-gnu": "1.60.0", + "@oxlint/binding-linux-x64-musl": "1.60.0", + "@oxlint/binding-openharmony-arm64": "1.60.0", + "@oxlint/binding-win32-arm64-msvc": "1.60.0", + "@oxlint/binding-win32-ia32-msvc": "1.60.0", + "@oxlint/binding-win32-x64-msvc": "1.60.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.18.0" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unplugin-utils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.1.tgz", + "integrity": "sha512-9qTpOmZ2vHpvlI9hdVXAQ1Ry4I8GcBArU7aPi0qfIaV7fQIXy0L1nb6X4mFY2Gw0dYshHuLbIl0Ulb572SCjsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.1.1", + "@vue/devtools-kit": "^8.1.1", + "@vue/devtools-shared": "^8.1.1", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^5.3.2" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect/node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect/node_modules/vite-dev-rpc/node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.4.0.tgz", + "integrity": "sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-sfc": "3.5.33", + "@vue/runtime-dom": "3.5.33", + "@vue/server-renderer": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-tsc": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.7.tgz", + "integrity": "sha512-zc1tL3HoQni1zGTGrwBVRQb7rGP5SWdu/m4rGB6JcnAC5MT5LFZIxF7Y+EJEnt4hGF23d60rXH7gRjHGb5KQQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.7" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Avalonia-Web-VUE/package.json b/Avalonia-Web-VUE/package.json new file mode 100644 index 0000000..3812385 --- /dev/null +++ b/Avalonia-Web-VUE/package.json @@ -0,0 +1,40 @@ +{ + "name": "avalonia-web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "run-s lint:*", + "lint:oxlint": "oxlint . --fix", + "lint:eslint": "eslint . --fix --cache" + }, + "dependencies": { + "axios": "^1.15.2", + "vue": "^3.5.32" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.12.2", + "@vitejs/plugin-vue": "^6.0.6", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.9.1", + "eslint": "^10.2.1", + "eslint-plugin-oxlint": "~1.60.0", + "eslint-plugin-vue": "~10.8.0", + "jiti": "^2.6.1", + "npm-run-all2": "^8.0.4", + "oxlint": "~1.60.0", + "typescript": "~6.0.0", + "vite": "^8.0.8", + "vite-plugin-vue-devtools": "^8.1.1", + "vue-tsc": "^3.2.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/Avalonia-Web-VUE/public/favicon.ico b/Avalonia-Web-VUE/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/Avalonia-Web-VUE/public/favicon.ico differ diff --git a/Avalonia-Web-VUE/src/App.vue b/Avalonia-Web-VUE/src/App.vue new file mode 100644 index 0000000..2d653b9 --- /dev/null +++ b/Avalonia-Web-VUE/src/App.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/Avalonia-Web-VUE/src/api/env.ts b/Avalonia-Web-VUE/src/api/env.ts new file mode 100644 index 0000000..5885dcb --- /dev/null +++ b/Avalonia-Web-VUE/src/api/env.ts @@ -0,0 +1,15 @@ +// 扩展 Window 接口,声明 C# 桥接注入的全局属性 +declare global { + interface Window { + /** 由 C# BridgeScript 注入,标记当前运行在 WebView2 环境中 */ + isWebView2?: boolean + /** 由 WebView2 宿主注入,用于向 C# 发送消息 */ + invokeCSharpAction?: (message: string) => void + } +} + +// 判断当前是否运行在 WebView2 环境中 +// 参考 www/index.html 中的判断逻辑 +export const isWebView2 = (): boolean => + window.isWebView2 === true || + typeof window.invokeCSharpAction === 'function' diff --git a/Avalonia-Web-VUE/src/api/http.ts b/Avalonia-Web-VUE/src/api/http.ts new file mode 100644 index 0000000..64fb725 --- /dev/null +++ b/Avalonia-Web-VUE/src/api/http.ts @@ -0,0 +1,84 @@ +import axios from 'axios' +import { isWebView2 } from './env' + +// WebView2 自定义协议前缀 +const WEBVIEW2_BASE = 'app://api/' + +// 普通浏览器 HTTP API 地址,按需修改 +const HTTP_BASE = 'http://localhost:5000/api/' + +// ─── axios 实例 ──────────────────────────────────────────────────────────────── + +const http = axios.create({ + headers: { 'Content-Type': 'application/json' }, +}) + +// 请求拦截器:仅在浏览器环境下注入鉴权 Token +// WebView2 本地运行,不需要鉴权 +http.interceptors.request.use((config) => { + if (!isWebView2()) { + const token = localStorage.getItem('authToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + } + return config +}) + +// 响应拦截器:统一解包 C# 返回的 { success, data/error } 结构 +// C# BuildSuccessResponseBody 固定格式:{ "success": true, "data": ... } +// 错误格式:{ "success": false, "error": "..." } +// WebView2 桥接和 HTTP 两个环境返回结构相同,拦截器可统一处理 +http.interceptors.response.use( + (response) => { + const payload = response.data as { success: boolean; data?: unknown; error?: string } + if (payload?.success === false) { + return Promise.reject(new Error(payload.error ?? '请求失败')) + } + return (payload?.data ?? payload) as never + }, + (error) => { + const msg: string = + error.response?.data?.error ?? + error.response?.data?.message ?? + error.message ?? + '网络错误' + return Promise.reject(new Error(msg)) + }, +) + +// ─── 统一请求方法 ────────────────────────────────────────────────────────────── + +interface RequestOptions { + method?: string + headers?: Record + body?: unknown +} + +export async function request(endpoint: string, options: RequestOptions = {}): Promise { + const url = (isWebView2() ? WEBVIEW2_BASE : HTTP_BASE) + endpoint + + // WebView2:直接走桥接 fetch(桥接脚本已完整覆盖 window.fetch) + if (isWebView2()) { + const res = await fetch(url, { + method: options.method ?? 'GET', + headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) }, + body: options.body !== undefined ? JSON.stringify(options.body) : undefined, + }) + const payload = await res.json() as { success: boolean; data?: T; error?: string } + // eslint-disable-next-line no-debugger + debugger + if (payload?.success === false) { + throw new Error(payload.error ?? '请求失败') + } + return (payload?.data ?? payload) as T + } + + // 普通浏览器:走 axios(拦截器处理鉴权和响应解包) + return http.request({ + url, + method: options.method ?? 'GET', + headers: options.headers, + data: options.body, + }) as Promise +} diff --git a/Avalonia-Web-VUE/src/api/index.ts b/Avalonia-Web-VUE/src/api/index.ts new file mode 100644 index 0000000..e383fdb --- /dev/null +++ b/Avalonia-Web-VUE/src/api/index.ts @@ -0,0 +1,8 @@ +import { request } from './http' + +// 业务接口定义,新增接口在此处添加一行即可 +export const api = { + getUser: () => request('getUser'), + processData: (input: string) => request('processData', { method: 'POST', body: { input } }), + wData: (input: string) => request('wData', { method: 'POST', body: { input } }), +} diff --git a/Avalonia-Web-VUE/src/assets/base.css b/Avalonia-Web-VUE/src/assets/base.css new file mode 100644 index 0000000..8816868 --- /dev/null +++ b/Avalonia-Web-VUE/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/Avalonia-Web-VUE/src/assets/logo.svg b/Avalonia-Web-VUE/src/assets/logo.svg new file mode 100644 index 0000000..7565660 --- /dev/null +++ b/Avalonia-Web-VUE/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/Avalonia-Web-VUE/src/assets/main.css b/Avalonia-Web-VUE/src/assets/main.css new file mode 100644 index 0000000..36fb845 --- /dev/null +++ b/Avalonia-Web-VUE/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/Avalonia-Web-VUE/src/components/HelloWorld.vue b/Avalonia-Web-VUE/src/components/HelloWorld.vue new file mode 100644 index 0000000..a2eabd1 --- /dev/null +++ b/Avalonia-Web-VUE/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/Avalonia-Web-VUE/src/components/TheWelcome.vue b/Avalonia-Web-VUE/src/components/TheWelcome.vue new file mode 100644 index 0000000..8b731d9 --- /dev/null +++ b/Avalonia-Web-VUE/src/components/TheWelcome.vue @@ -0,0 +1,95 @@ + + + diff --git a/Avalonia-Web-VUE/src/components/WelcomeItem.vue b/Avalonia-Web-VUE/src/components/WelcomeItem.vue new file mode 100644 index 0000000..6d7086a --- /dev/null +++ b/Avalonia-Web-VUE/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/Avalonia-Web-VUE/src/components/icons/IconCommunity.vue b/Avalonia-Web-VUE/src/components/icons/IconCommunity.vue new file mode 100644 index 0000000..2dc8b05 --- /dev/null +++ b/Avalonia-Web-VUE/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/Avalonia-Web-VUE/src/components/icons/IconDocumentation.vue b/Avalonia-Web-VUE/src/components/icons/IconDocumentation.vue new file mode 100644 index 0000000..6d4791c --- /dev/null +++ b/Avalonia-Web-VUE/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/Avalonia-Web-VUE/src/components/icons/IconEcosystem.vue b/Avalonia-Web-VUE/src/components/icons/IconEcosystem.vue new file mode 100644 index 0000000..c3a4f07 --- /dev/null +++ b/Avalonia-Web-VUE/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/Avalonia-Web-VUE/src/components/icons/IconSupport.vue b/Avalonia-Web-VUE/src/components/icons/IconSupport.vue new file mode 100644 index 0000000..7452834 --- /dev/null +++ b/Avalonia-Web-VUE/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/Avalonia-Web-VUE/src/components/icons/IconTooling.vue b/Avalonia-Web-VUE/src/components/icons/IconTooling.vue new file mode 100644 index 0000000..660598d --- /dev/null +++ b/Avalonia-Web-VUE/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/Avalonia-Web-VUE/src/main.ts b/Avalonia-Web-VUE/src/main.ts new file mode 100644 index 0000000..0ac3a5f --- /dev/null +++ b/Avalonia-Web-VUE/src/main.ts @@ -0,0 +1,6 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/Avalonia-Web-VUE/src/shims-vue.d.ts b/Avalonia-Web-VUE/src/shims-vue.d.ts new file mode 100644 index 0000000..3e9cfd4 --- /dev/null +++ b/Avalonia-Web-VUE/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/Avalonia-Web-VUE/tsconfig.app.json b/Avalonia-Web-VUE/tsconfig.app.json new file mode 100644 index 0000000..c0f2d86 --- /dev/null +++ b/Avalonia-Web-VUE/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + // Extra safety for array and object lookups, but may have false positives. + "noUncheckedIndexedAccess": true, + + // Path mapping for cleaner imports. + "paths": { + "@/*": ["./src/*"] + }, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + } +} diff --git a/Avalonia-Web-VUE/tsconfig.json b/Avalonia-Web-VUE/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/Avalonia-Web-VUE/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/Avalonia-Web-VUE/tsconfig.node.json b/Avalonia-Web-VUE/tsconfig.node.json new file mode 100644 index 0000000..c9b2bad --- /dev/null +++ b/Avalonia-Web-VUE/tsconfig.node.json @@ -0,0 +1,27 @@ +// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping. +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + // Most tools use transpilation instead of Node.js's native type-stripping. + // Bundler mode provides a smoother developer experience. + "module": "preserve", + "moduleResolution": "bundler", + + // Include Node.js types and avoid accidentally including other `@types/*` packages. + "types": ["node"], + + // Disable emitting output during `vue-tsc --build`, which is used for type-checking only. + "noEmit": true, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + } +} diff --git a/Avalonia-Web-VUE/vite.config.ts b/Avalonia-Web-VUE/vite.config.ts new file mode 100644 index 0000000..ddceca2 --- /dev/null +++ b/Avalonia-Web-VUE/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import plugin from '@vitejs/plugin-vue'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [plugin()], + server: { + port: 51552, + } +}) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/package-scripts/installer/Avalonia-PC.iss b/package-scripts/installer/Avalonia-PC.iss new file mode 100644 index 0000000..4e51cde --- /dev/null +++ b/package-scripts/installer/Avalonia-PC.iss @@ -0,0 +1,59 @@ +#ifndef AppName +#define AppName "Avalonia-PC" +#endif +#ifndef AppVersion +#define AppVersion "1.0.0" +#endif +#ifndef AppPublisher +#define AppPublisher "QiCheng" +#endif +#ifndef AppExeName +#define AppExeName "Avalonia-PC.exe" +#endif +#ifndef SourceDir +#define SourceDir "..\..\package-output\publish\Avalonia-PC\win-x64" +#endif +#ifndef OutputDir +#define OutputDir "..\..\package-output\installer" +#endif +#ifndef RepoRoot +#define RepoRoot "..\.." +#endif +#ifndef ChineseLanguageFile +#define ChineseLanguageFile "compiler:Default.isl" +#endif + +[Setup] +AppId={{7E41DD4C-FBF3-4C65-8D9F-4F2D794BC284} +AppName={#AppName} +AppVersion={#AppVersion} +AppPublisher={#AppPublisher} +DefaultDirName={autopf}\{#AppName} +DefaultGroupName={#AppName} +OutputDir={#OutputDir} +OutputBaseFilename={#AppName}-Setup-{#AppVersion}-win-x64 +SetupIconFile={#RepoRoot}\Avalonia-PC\Assets\avalonia-logo.ico +Compression=lzma2 +SolidCompression=yes +WizardStyle=modern +PrivilegesRequired=admin +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +DisableProgramGroupPage=yes +UninstallDisplayIcon={app}\{#AppExeName} + +[Languages] +Name: "chinesesimp"; MessagesFile: "{#ChineseLanguageFile}" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}" +Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/package-scripts/package-pc.bat b/package-scripts/package-pc.bat new file mode 100644 index 0000000..10ff627 --- /dev/null +++ b/package-scripts/package-pc.bat @@ -0,0 +1,32 @@ +@echo off +setlocal + +cd /d "%~dp0.." + +set "APP_VERSION=1.0.0" +set "APP_NAME=Avalonia-PC" +set "APP_PUBLISHER=QiCheng" + +echo Packaging %APP_NAME% %APP_VERSION% for Windows PC... +echo. + +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0package-pc.ps1" -Version "%APP_VERSION%" -AppName "%APP_NAME%" -Publisher "%APP_PUBLISHER%" -SingleFile -InstallInnoSetupIfMissing + +set "EXIT_CODE=%ERRORLEVEL%" +echo. + +if "%EXIT_CODE%"=="0" ( + echo Done. + echo Installer output: %CD%\package-output\installer +) else if "%EXIT_CODE%"=="2" ( + echo Publish completed, but installer was not created because Inno Setup 6 is not installed. + echo This BAT can download Inno Setup into package-scripts\tools. Run it again and allow network access. + echo. + echo Publish output: %CD%\package-output\publish\Avalonia-PC +) else ( + echo Packaging failed. Exit code: %EXIT_CODE% +) + +echo. +pause +exit /b %EXIT_CODE% diff --git a/package-scripts/package-pc.ps1 b/package-scripts/package-pc.ps1 new file mode 100644 index 0000000..67ba6bf --- /dev/null +++ b/package-scripts/package-pc.ps1 @@ -0,0 +1,171 @@ +[CmdletBinding()] +param( + [string]$Configuration = "Release", + [string]$Runtime = "win-x64", + [string]$Version = "1.0.0", + [string]$AppName = "Avalonia-PC", + [string]$Publisher = "QiCheng", + [bool]$SelfContained = $true, + [switch]$SingleFile, + [switch]$InstallInnoSetupIfMissing, + [switch]$SkipInstaller +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$projectPath = Join-Path $repoRoot "Avalonia-PC\Avalonia-PC.csproj" +$installerScript = Join-Path $PSScriptRoot "installer\Avalonia-PC.iss" +$buildStamp = Get-Date -Format "yyyyMMddHHmmss" +$publishDir = Join-Path $repoRoot "package-output\publish\Avalonia-PC\$Runtime-$buildStamp" +$installerDir = Join-Path $repoRoot "package-output\installer" +$appExeName = "Avalonia-PC.exe" +$toolsDir = Join-Path $PSScriptRoot "tools" +$innoSetupDir = Join-Path $toolsDir "InnoSetup6" +$innoSetupInstaller = Join-Path $toolsDir "downloads\innosetup-6.7.2.exe" +$innoSetupDownloadUrl = "https://github.com/jrsoftware/issrc/releases/download/is-6_7_2/innosetup-6.7.2.exe" +$chineseSimplifiedLanguageFile = Join-Path $innoSetupDir "Languages\ChineseSimplified.isl" +$chineseSimplifiedLanguageUrl = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/main/ChineseSimplified.isl" + +function Find-InnoSetupCompiler { + $localCompiler = Join-Path $innoSetupDir "ISCC.exe" + if (Test-Path $localCompiler) { + return $localCompiler + } + + $command = Get-Command "iscc" -ErrorAction SilentlyContinue + if ($command) { + return $command.Source + } + + $candidates = @( + "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", + "$env:ProgramFiles\Inno Setup 6\ISCC.exe", + "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" + ) + + foreach ($candidate in $candidates) { + if ($candidate -and (Test-Path $candidate)) { + return $candidate + } + } + + return $null +} + +function Install-LocalInnoSetup { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $innoSetupInstaller), $innoSetupDir | Out-Null + + if (-not (Test-Path $innoSetupInstaller)) { + Write-Host "Downloading Inno Setup 6 to: $innoSetupInstaller" + Invoke-WebRequest -Uri $innoSetupDownloadUrl -OutFile $innoSetupInstaller + } + + Write-Host "Installing local Inno Setup 6 to: $innoSetupDir" + & $innoSetupInstaller /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CURRENTUSER /DIR="$innoSetupDir" + if ($LASTEXITCODE -ne 0) { + throw "Inno Setup local install failed with exit code $LASTEXITCODE" + } +} + +function Ensure-ChineseSimplifiedLanguageFile { + if (Test-Path $chineseSimplifiedLanguageFile) { + return + } + + $languageDir = Split-Path -Parent $chineseSimplifiedLanguageFile + New-Item -ItemType Directory -Force -Path $languageDir | Out-Null + + Write-Host "Downloading Inno Setup Chinese language file to: $chineseSimplifiedLanguageFile" + Invoke-WebRequest -Uri $chineseSimplifiedLanguageUrl -OutFile $chineseSimplifiedLanguageFile +} + +if (-not (Test-Path $projectPath)) { + throw "Project file not found: $projectPath" +} + +if (-not (Test-Path $installerScript)) { + throw "Installer script not found: $installerScript" +} + +New-Item -ItemType Directory -Force -Path $publishDir, $installerDir | Out-Null + +Write-Host "Publishing $AppName ($Configuration, $Runtime)..." + +$publishArgs = @( + "publish", + $projectPath, + "-c", $Configuration, + "-r", $Runtime, + "--self-contained", $SelfContained.ToString().ToLowerInvariant(), + "-o", $publishDir, + "/p:Version=$Version", + "/p:PublishSingleFile=$($SingleFile.IsPresent.ToString().ToLowerInvariant())", + "/p:IncludeNativeLibrariesForSelfExtract=true", + "/p:PublishTrimmed=false", + "/p:DebugType=None", + "/p:DebugSymbols=false" +) + +dotnet @publishArgs +if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed with exit code $LASTEXITCODE" +} + +Get-ChildItem -Path $publishDir -Filter "*.pdb" -Recurse -File | Remove-Item -Force + +$publishedExe = Join-Path $publishDir $appExeName +if (-not (Test-Path $publishedExe)) { + throw "Publish completed, but executable was not found: $publishedExe" +} + +Write-Host "Publish output: $publishDir" + +$localInnoCompiler = Join-Path $innoSetupDir "ISCC.exe" + +if ($SkipInstaller) { + Write-Host "SkipInstaller was specified. Installer package was not created." + exit 0 +} + +if ($InstallInnoSetupIfMissing -and -not (Test-Path $localInnoCompiler)) { + Install-LocalInnoSetup +} + +$iscc = Find-InnoSetupCompiler +if (-not $iscc) { + if ($InstallInnoSetupIfMissing) { + Install-LocalInnoSetup + $iscc = Find-InnoSetupCompiler + } + + if (-not $iscc) { + Write-Warning "Inno Setup compiler (ISCC.exe) was not found. Rerun package-scripts\package-pc.bat and let it download Inno Setup into package-scripts\tools." + Write-Host "The publish output is ready at: $publishDir" + exit 2 + } +} + +Write-Host "Building installer with Inno Setup..." +Write-Host "Using Inno Setup compiler: $iscc" +Ensure-ChineseSimplifiedLanguageFile + +$isccArgs = @( + "/DAppName=$AppName", + "/DAppVersion=$Version", + "/DAppPublisher=$Publisher", + "/DAppExeName=$appExeName", + "/DSourceDir=$publishDir", + "/DOutputDir=$installerDir", + "/DRepoRoot=$repoRoot", + "/DChineseLanguageFile=$chineseSimplifiedLanguageFile", + $installerScript +) + +& $iscc @isccArgs +if ($LASTEXITCODE -ne 0) { + throw "Inno Setup failed with exit code $LASTEXITCODE" +} + +$setupFile = Join-Path $installerDir "$AppName-Setup-$Version-$Runtime.exe" +Write-Host "Installer created: $setupFile" diff --git a/scripts/add-migration.bat b/scripts/add-migration.bat new file mode 100644 index 0000000..c5ce612 --- /dev/null +++ b/scripts/add-migration.bat @@ -0,0 +1,2 @@ +@echo off +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0add-migration.ps1" %* diff --git a/scripts/add-migration.cmd b/scripts/add-migration.cmd new file mode 100644 index 0000000..c5ce612 --- /dev/null +++ b/scripts/add-migration.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0add-migration.ps1" %* diff --git a/scripts/add-migration.ps1 b/scripts/add-migration.ps1 new file mode 100644 index 0000000..2df4c3f --- /dev/null +++ b/scripts/add-migration.ps1 @@ -0,0 +1,92 @@ +param( + [string]$Name, + [ValidateSet("SQLite", "SqlServer", "PostgreSQL", "MySQL", "All")] + [string]$Provider = "All", + [string]$Project = "Avalonia-EFCore/Avalonia-EFCore.csproj", + [string]$StartupProject = "Avalonia-API/Avalonia-API.csproj", + [string]$OutputDir = "Migrations" +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +Set-Location $repoRoot + +if ([string]::IsNullOrWhiteSpace($Name)) { + $Name = "AutoMigration_{0}" -f (Get-Date -Format "yyyyMMddHHmmss") +} + +Write-Host "Restoring local dotnet tools..." +dotnet tool restore +if ($LASTEXITCODE -ne 0) { + throw "dotnet tool restore failed." +} + +function Get-ContextName([string]$providerName) { + switch ($providerName) { + "SQLite" { return "SqliteAppDataContext" } + "SqlServer" { return "SqlServerAppDataContext" } + "PostgreSQL" { return "PostgreSqlAppDataContext" } + "MySQL" { return "MySqlAppDataContext" } + default { throw "Unsupported provider '$providerName'." } + } +} + +function Add-ProviderMigration([string]$providerName) { + $context = Get-ContextName $providerName + $providerOutputDir = Join-Path $OutputDir $providerName + + Write-Host "Generating migration '$Name' for $providerName..." + dotnet tool run dotnet-ef migrations add $Name ` + --project $Project ` + --startup-project $StartupProject ` + --context $context ` + --output-dir $providerOutputDir + if ($LASTEXITCODE -ne 0) { + throw "dotnet ef migrations add failed for $providerName." + } + + $migrationDir = Join-Path (Split-Path $Project -Parent) $providerOutputDir + $migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" | + Where-Object { $_.Name -notlike "*.Designer.cs" } | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + + if ($null -eq $migrationFile) { + throw "Migration file was not found for '$Name' ($providerName)." + } + + $content = Get-Content $migrationFile.FullName -Raw + $upMatch = [regex]::Match($content, "protected override void Up\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline") + $downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline") + + $upBody = if ($upMatch.Success) { $upMatch.Groups["body"].Value.Trim() } else { "" } + $downBody = if ($downMatch.Success) { $downMatch.Groups["body"].Value.Trim() } else { "" } + + if ([string]::IsNullOrWhiteSpace($upBody) -and [string]::IsNullOrWhiteSpace($downBody)) { + Write-Host "No model changes were detected for $providerName. Removing empty migration '$Name'..." + dotnet tool run dotnet-ef migrations remove --force ` + --project $Project ` + --startup-project $StartupProject ` + --context $context + if ($LASTEXITCODE -ne 0) { + throw "dotnet ef migrations remove failed for $providerName." + } + return + } + + Write-Host "Migration generated for ${providerName}:" + Write-Host " $($migrationFile.FullName)" +} + +$providers = if ($Provider -eq "All") { + @("SQLite", "SqlServer", "PostgreSQL", "MySQL") +} else { + @($Provider) +} + +foreach ($providerName in $providers) { + Add-ProviderMigration $providerName +} + +Write-Host "Review the migration files, then start the app. Startup will apply the migration set matching DatabaseConfiguration.Provider." diff --git a/scripts/find-missing-csharp-docs.bat b/scripts/find-missing-csharp-docs.bat new file mode 100644 index 0000000..93949b8 --- /dev/null +++ b/scripts/find-missing-csharp-docs.bat @@ -0,0 +1,6 @@ +@echo off +setlocal + +set SCRIPT_DIR=%~dp0 +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%find-missing-csharp-docs.ps1" %* +exit /b %ERRORLEVEL% diff --git a/scripts/find-missing-csharp-docs.ps1 b/scripts/find-missing-csharp-docs.ps1 new file mode 100644 index 0000000..de476e4 --- /dev/null +++ b/scripts/find-missing-csharp-docs.ps1 @@ -0,0 +1,358 @@ +param( + [string]$Path = ".", + [string]$OutputPath = "scripts/missing-csharp-docs.txt", + [switch]$IncludeMigrations, + [switch]$IncludeGenerated, + [switch]$Json +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$scanRoot = Resolve-Path (Join-Path $repoRoot $Path) + +$excludedDirectories = @( + "\bin\", + "\obj\", + "\.git\", + "\.vs\", + "\node_modules\", + "\dist\", + "\logs\" +) + +if (-not $IncludeMigrations) { + $excludedDirectories += "\Migrations\" +} + +$memberRegexes = @( + @{ + Kind = "Type" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|abstract|sealed|partial|readonly|unsafe|file)\s+)*(?:class|interface|struct|enum|record(?:\s+(?:class|struct))?)\s+[A-Za-z_][A-Za-z0-9_]*' + }, + @{ + Kind = "Delegate" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|new|unsafe|partial)\s+)*delegate\s+' + }, + @{ + Kind = "Event" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|new|unsafe)\s+)*event\s+' + }, + @{ + Kind = "Property" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|new|readonly|required|unsafe)\s+)+(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*\s*\{\s*(?:get|set|init)\b' + }, + @{ + Kind = "InterfaceProperty" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*\s*\{\s*(?:get|set|init)\b' + }, + @{ + Kind = "Constructor" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|unsafe)\s+)+[A-Za-z_][A-Za-z0-9_]*\s*\(' + }, + @{ + Kind = "Method" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|async|extern|new|unsafe|partial)\s+)+(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+(?:operator\s*[^\s\(]+|[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\(' + }, + @{ + Kind = "InterfaceMethod" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*(?:<[^>]+>)?\s*\([^;{}]*\)\s*;' + }, + @{ + Kind = "Field" + Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|readonly|const|volatile|new|unsafe)\s+)+(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*(?:\s*=\s*[^;]+)?\s*;' + } +) + +function Test-IsExcludedFile { + param([System.IO.FileInfo]$File) + + $fullName = $File.FullName + foreach ($directory in $excludedDirectories) { + if ($fullName.Contains($directory)) { + return $true + } + } + + if (-not $IncludeGenerated) { + if ($File.Name -like "*.g.cs" -or + $File.Name -like "*.g.i.cs" -or + $File.Name -like "*.Designer.cs" -or + $File.Name -like "*.AssemblyInfo.cs") { + return $true + } + } + + return $false +} + +function Get-RelativePath { + param( + [string]$BasePath, + [string]$TargetPath + ) + + $baseFullPath = [System.IO.Path]::GetFullPath($BasePath) + if (-not $baseFullPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { + $baseFullPath += [System.IO.Path]::DirectorySeparatorChar + } + + $targetFullPath = [System.IO.Path]::GetFullPath($TargetPath) + $baseUri = New-Object System.Uri($baseFullPath) + $targetUri = New-Object System.Uri($targetFullPath) + $relativeUri = $baseUri.MakeRelativeUri($targetUri) + return [System.Uri]::UnescapeDataString($relativeUri.ToString()).Replace("/", [System.IO.Path]::DirectorySeparatorChar) +} + +function Remove-LineNoise { + param([string]$Line) + + $lineWithoutStrings = [regex]::Replace($Line, '@?"(?:[^"\\]|\\.|"")*"', '""') + return [regex]::Replace($lineWithoutStrings, '//.*$', '') +} + +function Get-PreviousCodeLineIndex { + param( + [string[]]$Lines, + [int]$StartIndex + ) + + for ($i = $StartIndex; $i -ge 0; $i--) { + $trimmed = $Lines[$i].Trim() + if ([string]::IsNullOrWhiteSpace($trimmed)) { + continue + } + + if ($trimmed.StartsWith("[") -and $trimmed.EndsWith("]")) { + continue + } + + return $i + } + + return -1 +} + +function Test-HasXmlDoc { + param( + [string[]]$Lines, + [int]$DeclarationIndex + ) + + $previousIndex = Get-PreviousCodeLineIndex -Lines $Lines -StartIndex ($DeclarationIndex - 1) + return $previousIndex -ge 0 -and $Lines[$previousIndex].TrimStart().StartsWith("///") +} + +function Get-DeclarationText { + param( + [string[]]$Lines, + [int]$StartIndex + ) + + $parts = New-Object System.Collections.Generic.List[string] + $maxIndex = [Math]::Min($Lines.Length - 1, $StartIndex + 8) + + for ($i = $StartIndex; $i -le $maxIndex; $i++) { + $clean = Remove-LineNoise $Lines[$i] + if ([string]::IsNullOrWhiteSpace($clean)) { + continue + } + + $parts.Add($clean.Trim()) + $joined = ($parts -join " ") + if ($joined -match '[\{;\}=]\s*$' -or $joined.Contains("=>")) { + break + } + } + + return ($parts -join " ") +} + +function Test-IsInsideInterface { + param( + [string[]]$Lines, + [int]$Index + ) + + $scopeStack = New-Object System.Collections.Generic.List[string] + $pendingInterface = $false + + for ($i = 0; $i -lt $Index; $i++) { + $line = Remove-LineNoise $Lines[$i] + if ($line -match '\binterface\s+[A-Za-z_][A-Za-z0-9_]*') { + $pendingInterface = $true + } + + foreach ($char in $line.ToCharArray()) { + if ($char -eq "{") { + if ($pendingInterface) { + $scopeStack.Add("interface") + $pendingInterface = $false + } else { + $scopeStack.Add("block") + } + } elseif ($char -eq "}") { + if ($scopeStack.Count -gt 0) { + $scopeStack.RemoveAt($scopeStack.Count - 1) + } + } + } + } + + return $scopeStack.Contains("interface") +} + +function Get-MemberName { + param( + [string]$Kind, + [string]$Declaration + ) + + switch ($Kind) { + "Type" { + if ($Declaration -match '\b(?:class|interface|struct|enum|record(?:\s+(?:class|struct))?)\s+(?[A-Za-z_][A-Za-z0-9_]*)') { + return $Matches["name"] + } + } + "Delegate" { + if ($Declaration -match '\b(?[A-Za-z_][A-Za-z0-9_]*)\s*\(') { + return $Matches["name"] + } + } + "Event" { + if ($Declaration -match '\bevent\s+[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s]*\s+(?[A-Za-z_][A-Za-z0-9_]*)') { + return $Matches["name"] + } + } + "Constructor" { + if ($Declaration -match '\b(?[A-Za-z_][A-Za-z0-9_]*)\s*\(') { + return $Matches["name"] + } + } + "Method" { + $matches = [regex]::Matches($Declaration, '\s(?operator\s*[^\s\(]+|[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\(') + if ($matches.Count -gt 0) { + return $matches[$matches.Count - 1].Groups["name"].Value + } + } + "InterfaceMethod" { + $matches = [regex]::Matches($Declaration, '\s(?[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\(') + if ($matches.Count -gt 0) { + return $matches[$matches.Count - 1].Groups["name"].Value + } + } + default { + if ($Declaration -match '\b(?[A-Za-z_][A-Za-z0-9_]*)\s*(?:[=;\{])') { + return $Matches["name"] + } + } + } + + return "" +} + +function Test-IsEnumMember { + param( + [string[]]$Lines, + [int]$Index + ) + + $line = Remove-LineNoise $Lines[$Index] + if ($line -notmatch '^\s*[A-Za-z_][A-Za-z0-9_]*(?:\s*=\s*[^,]+)?\s*,?\s*$') { + return $false + } + + for ($i = $Index - 1; $i -ge 0; $i--) { + $previous = Remove-LineNoise $Lines[$i] + if ($previous -match '\benum\s+[A-Za-z_][A-Za-z0-9_]*') { + return $true + } + + if ($previous.Contains("{") -or $previous.Contains("}")) { + return $false + } + } + + return $false +} + +$files = Get-ChildItem -Path $scanRoot -Recurse -File -Filter "*.cs" | + Where-Object { -not (Test-IsExcludedFile $_) } | + Sort-Object FullName + +$results = New-Object System.Collections.Generic.List[object] + +foreach ($file in $files) { + $lines = Get-Content $file.FullName -Encoding UTF8 + $relativePath = Get-RelativePath -BasePath $repoRoot -TargetPath $file.FullName + + for ($i = 0; $i -lt $lines.Length; $i++) { + $line = $lines[$i] + $trimmed = $line.Trim() + + if ([string]::IsNullOrWhiteSpace($trimmed) -or + $trimmed.StartsWith("///") -or + $trimmed.StartsWith("//") -or + $trimmed.StartsWith("#") -or + $trimmed.StartsWith("[") -or + $trimmed -in @("{", "}", "};")) { + continue + } + + $declaration = Get-DeclarationText -Lines $lines -StartIndex $i + $matchedKind = $null + + foreach ($entry in $memberRegexes) { + if ($declaration -cmatch $entry.Pattern) { + if (($entry.Kind -eq "InterfaceMethod" -or $entry.Kind -eq "InterfaceProperty") -and + -not (Test-IsInsideInterface -Lines $lines -Index $i)) { + continue + } + + $matchedKind = $entry.Kind + break + } + } + + if ($null -eq $matchedKind -and (Test-IsEnumMember -Lines $lines -Index $i)) { + $matchedKind = "EnumMember" + } + + if ($null -eq $matchedKind) { + continue + } + + if (Test-HasXmlDoc -Lines $lines -DeclarationIndex $i) { + continue + } + + $results.Add([pscustomobject]@{ + File = $relativePath + Line = $i + 1 + Kind = $matchedKind + Name = Get-MemberName -Kind $matchedKind -Declaration $declaration + Declaration = $declaration + }) + } +} + +if ($Json) { + $output = $results | ConvertTo-Json -Depth 4 +} else { + $output = $results | Format-Table File, Line, Kind, Name, Declaration -AutoSize | Out-String -Width 240 +} + +if (-not [string]::IsNullOrWhiteSpace($OutputPath)) { + $resolvedOutputPath = Join-Path $repoRoot $OutputPath + $outputDirectory = Split-Path $resolvedOutputPath -Parent + if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) { + New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null + } + + Set-Content -Path $resolvedOutputPath -Value $output -Encoding UTF8 + Write-Host "Missing XML documentation report written to $resolvedOutputPath" + Write-Host "Total missing items: $($results.Count)" +} else { + $output + Write-Host "Total missing items: $($results.Count)" +} diff --git a/scripts/missing-csharp-docs.after.json b/scripts/missing-csharp-docs.after.json new file mode 100644 index 0000000..6cba1b5 --- /dev/null +++ b/scripts/missing-csharp-docs.after.json @@ -0,0 +1,23 @@ +[ + { + "File": "Avalonia-API\\Authentication\\JwtTokenService.cs", + "Line": 20, + "Kind": "Method", + "Name": "CreateAccessToken", + "Declaration": "public (string Token, DateTime ExpiresAt) CreateAccessToken(UserEntity user, IReadOnlyCollection\u003cstring\u003e roles) {" + }, + { + "File": "Avalonia-API\\Authentication\\RefreshTokenService.cs", + "Line": 21, + "Kind": "Method", + "Name": "CreateAsync", + "Declaration": "public async Task\u003c(string Token, ApiRefreshTokenEntity Entity)\u003e CreateAsync( int userId, string? device, string? ipAddress, CancellationToken cancellationToken = default) {" + }, + { + "File": "Avalonia-API\\Authentication\\RefreshTokenService.cs", + "Line": 78, + "Kind": "Method", + "Name": "RotateAsync", + "Declaration": "public async Task\u003c(string Token, ApiRefreshTokenEntity Entity)?\u003e RotateAsync( string? token, string? device, string? ipAddress, CancellationToken cancellationToken = default) {" + } +] diff --git a/scripts/missing-csharp-docs.txt b/scripts/missing-csharp-docs.txt new file mode 100644 index 0000000..e02abfc --- /dev/null +++ b/scripts/missing-csharp-docs.txt @@ -0,0 +1 @@ + diff --git a/scripts/生成注释提示词.txt b/scripts/生成注释提示词.txt new file mode 100644 index 0000000..ac7a132 --- /dev/null +++ b/scripts/生成注释提示词.txt @@ -0,0 +1,60 @@ +你是一个资深 C# 工程师。现在我会给你一个 missing-csharp-docs.txt 文件,里面列出了项目中缺少 XML 文档注释的 C# 类型、方法、属性、字段、构造函数、接口成员等。 + +请根据这个 txt 文件逐项读取对应源码文件,并直接修改源码,为缺少注释的成员补全中文 XML 文档注释。 + +要求如下: + +1. 注释必须是中文。 +2. 使用标准 C# XML 文档注释格式。 +3. 类、接口、record、struct、enum 使用: + /// + /// ... + /// +4. 方法、构造函数必须尽量补全: + /// + /// ... + /// + /// ... + /// ... + /// ... +5. 如果方法没有参数,不要生成 。 +6. 如果方法返回 void、Task 或构造函数,不要生成无意义的 。 +7. 如果方法返回 Task、ValueTask、T、IEnumerable 等有实际返回值的类型,需要生成 ,说明返回内容。 +8. 如果方法体中明确 throw 了异常,或声明逻辑明显可能抛出特定异常,可以补充 ;不确定时不要乱写。 +9. 属性使用: + /// + /// 获取或设置... + /// + 如果是只读属性,写“获取...”;如果是计算属性,说明它计算或表示的含义。 +10. 字段使用: + /// + /// 保存/定义/指示... + /// +11. 枚举成员也要加中文 summary,说明每个枚举值的含义。 +12. 接口方法必须在 interface 中写完整注释,包括 summary、param、returns。 +13. 具体实现类如果实现了已有注释的接口方法,优先使用: + /// + 不要在实现类重复写一大段相同注释。 +14. 如果实现类方法不是接口实现,或者接口中没有对应注释,则在实现类中写完整注释。 +15. 不要只根据方法名机械生成注释,要结合方法体、参数、返回值、调用逻辑和业务语义来写。 +16. 不要改业务逻辑,不要改方法签名,不要改格式以外的代码。 +17. XML 注释放在 attribute 之前,例如: + /// + /// 用户 ID。 + /// + [Column("user-id")] + public int UserId { get; set; } +18. 如果成员前已经有 XML 注释,不要重复添加。 +19. 如果 txt 中的行号因为代码变化不准确,要通过成员名称和声明内容定位实际源码位置。 +20. 修改完成后,重新运行已有的注释扫描脚本确认缺失项为 0。 +21. 最后运行相关 dotnet build 验证没有语法错误。 +22. 最后给我总结:修改了哪些文件、补了多少处注释、扫描结果、构建结果。 + +执行方式: +- 直接读取 missing-csharp-docs.txt。 +- 按 txt 中列出的 File、Line、Kind、Name、Declaration 定位源码成员。 +- 逐文件修改。 +- 不要新建额外的注释生成脚本。 +- 不要生成新的工具脚本。 +- 可以使用现有脚本重新扫描验证。 +- 最终直接完成代码修改。