From e3fe965f108e748a0bfa13f6f122d650d73575d9 Mon Sep 17 00:00:00 2001 From: luoqian <2769838458@qq.com> Date: Thu, 21 May 2026 15:52:36 +0800 Subject: [PATCH] init --- .config/dotnet-tools.json | 13 + .gitignore | 30 + .vscode/settings.json | 7 + .../Authentication/ApiAuthEndpointService.cs | 160 + Avalonia-API/Authentication/JwtOptions.cs | 33 + .../Authentication/JwtTokenService.cs | 55 + .../Authentication/RefreshTokenService.cs | 124 + Avalonia-API/Avalonia-API.csproj | 31 + Avalonia-API/Avalonia-API.csproj.user | 9 + Avalonia-API/Avalonia-API.http | 6 + .../Configuration/ServicesConfiguration.cs | 75 + .../Extensions/UnifiedEndpointExtensions.cs | 225 + Avalonia-API/Program.cs | 60 + Avalonia-API/Properties/launchSettings.json | 23 + Avalonia-API/appsettings.Development.json | 8 + Avalonia-API/appsettings.json | 24 + Avalonia-Common/Avalonia-Common.csproj | 18 + Avalonia-Common/Core/ApiResponse.cs | 205 + .../Infrastructure/LoggingConfiguration.cs | 167 + Avalonia-EFCore/Avalonia-EFCore.csproj | 28 + Avalonia-EFCore/Database/AppDataContext.cs | 50 + .../Database/AppDataContextFactory.cs | 104 + Avalonia-EFCore/Database/AppDbContext.cs | 115 + .../Database/DatabaseConfiguration.cs | 93 + .../Database/DatabaseExtensions.cs | 85 + Avalonia-EFCore/Database/DatabaseManager.cs | 224 + .../Database/DatabaseProviderRegistry.cs | 59 + .../Database/ProviderAppDataContexts.cs | 30 + ...6_AutoMigration_20260520162543.Designer.cs | 175 + ...0520082626_AutoMigration_20260520162543.cs | 103 + ...6_AutoMigration_20260520163216.Designer.cs | 181 + ...0520083306_AutoMigration_20260520163216.cs | 30 + .../MySQL/MySqlAppDataContextModelSnapshot.cs | 178 + ...7_AutoMigration_20260520162543.Designer.cs | 184 + ...0520082617_AutoMigration_20260520162543.cs | 97 + ...4_AutoMigration_20260520163216.Designer.cs | 190 + ...0520083254_AutoMigration_20260520163216.cs | 30 + .../PostgreSqlAppDataContextModelSnapshot.cs | 187 + .../20260514000100_InitialCreate.Designer.cs | 94 + .../SQLite/20260514000100_InitialCreate.cs | 62 + ...5_AutoMigration_20260515152037.Designer.cs | 113 + ...0515072045_AutoMigration_20260515152037.cs | 30 + ...7_AutoMigration_20260515165835.Designer.cs | 173 + ...0515085847_AutoMigration_20260515165835.cs | 54 + ...0_AutoMigration_20260520163216.Designer.cs | 179 + ...0520083230_AutoMigration_20260520163216.cs | 30 + .../SqliteAppDataContextModelSnapshot.cs | 176 + ...7_AutoMigration_20260520162543.Designer.cs | 184 + ...0520082607_AutoMigration_20260520162543.cs | 96 + ...2_AutoMigration_20260520163216.Designer.cs | 190 + ...0520083242_AutoMigration_20260520163216.cs | 30 + .../SqlServerAppDataContextModelSnapshot.cs | 187 + .../Models/ApiRefreshTokenEntity.cs | 79 + Avalonia-EFCore/Models/UserEntity.cs | 69 + Avalonia-EFCore/Models/WeatherForecast.cs | 28 + .../Models/WeatherForecastEntity.cs | 59 + Avalonia-PC/.github/copilot-instructions.md | 4 + Avalonia-PC/App.axaml | 15 + Avalonia-PC/App.axaml.cs | 37 + Avalonia-PC/Assets/avalonia-logo.ico | Bin 0 -> 175875 bytes .../DefaultPcThirdPartyAuthorizationClient.cs | 50 + .../Authentication/PcAuthEndpointService.cs | 94 + Avalonia-PC/Authentication/PcAuthService.cs | 60 + .../Authentication/PcGlobalTokenService.cs | 227 + Avalonia-PC/Avalonia-PC.csproj | 48 + Avalonia-PC/Avalonia-PC.csproj.user | 9 + Avalonia-PC/Avalonia-PC.slnx | 11 + Avalonia-PC/Program.cs | 97 + Avalonia-PC/Properties/launchSettings.json | 11 + Avalonia-PC/ViewLocator.cs | 53 + Avalonia-PC/ViewModels/MainWindowViewModel.cs | 13 + Avalonia-PC/ViewModels/ViewModelBase.cs | 12 + Avalonia-PC/Views/MainWindow.BridgeScript.cs | 345 ++ Avalonia-PC/Views/MainWindow.Routes.cs | 38 + Avalonia-PC/Views/MainWindow.axaml | 23 + Avalonia-PC/Views/MainWindow.axaml.cs | 684 +++ Avalonia-PC/app.manifest | 18 + Avalonia-PC/www/api.js | 51 + Avalonia-PC/www/index.html | 62 + Avalonia-Services/Avalonia-Services.csproj | 24 + Avalonia-Services/Core/EndpointPrinter.cs | 70 + .../Core/GlobalExceptionFilter.cs | 107 + Avalonia-Services/Core/IAuthService.cs | 39 + Avalonia-Services/Core/IEndpointFilter.cs | 48 + .../Core/ServiceEndpointCollection.cs | 364 ++ .../Core/ServiceEndpointContext.cs | 79 + Avalonia-Services/Endpoints/AppEndpoints.cs | 147 + Avalonia-Services/Endpoints/AuthEndpoints.cs | 61 + .../Extensions/DesktopEndpointAdapter.cs | 233 + .../Services/AuthService/AuthContracts.cs | 98 + .../AuthService/AuthEndpointServices.cs | 59 + .../Services/WeatherForecastService.cs | 30 + Avalonia-Web-VUE/.editorconfig | 8 + Avalonia-Web-VUE/.gitattributes | 1 + Avalonia-Web-VUE/.gitignore | 39 + Avalonia-Web-VUE/.oxlintrc.json | 10 + Avalonia-Web-VUE/CHANGELOG.md | 13 + Avalonia-Web-VUE/README.md | 48 + Avalonia-Web-VUE/avalonia-web-vue.esproj | 11 + Avalonia-Web-VUE/env.d.ts | 1 + Avalonia-Web-VUE/eslint.config.ts | 23 + Avalonia-Web-VUE/index.html | 13 + Avalonia-Web-VUE/package-lock.json | 4880 +++++++++++++++++ Avalonia-Web-VUE/package.json | 40 + Avalonia-Web-VUE/public/favicon.ico | Bin 0 -> 4286 bytes Avalonia-Web-VUE/src/App.vue | 47 + Avalonia-Web-VUE/src/api/env.ts | 15 + Avalonia-Web-VUE/src/api/http.ts | 84 + Avalonia-Web-VUE/src/api/index.ts | 8 + Avalonia-Web-VUE/src/assets/base.css | 86 + Avalonia-Web-VUE/src/assets/logo.svg | 1 + Avalonia-Web-VUE/src/assets/main.css | 35 + .../src/components/HelloWorld.vue | 41 + .../src/components/TheWelcome.vue | 95 + .../src/components/WelcomeItem.vue | 87 + .../src/components/icons/IconCommunity.vue | 7 + .../components/icons/IconDocumentation.vue | 7 + .../src/components/icons/IconEcosystem.vue | 7 + .../src/components/icons/IconSupport.vue | 7 + .../src/components/icons/IconTooling.vue | 19 + Avalonia-Web-VUE/src/main.ts | 6 + Avalonia-Web-VUE/src/shims-vue.d.ts | 6 + Avalonia-Web-VUE/tsconfig.app.json | 18 + Avalonia-Web-VUE/tsconfig.json | 11 + Avalonia-Web-VUE/tsconfig.node.json | 27 + Avalonia-Web-VUE/vite.config.ts | 10 + README.md | 0 package-scripts/installer/Avalonia-PC.iss | 59 + package-scripts/package-pc.bat | 32 + package-scripts/package-pc.ps1 | 171 + scripts/add-migration.bat | 2 + scripts/add-migration.cmd | 2 + scripts/add-migration.ps1 | 92 + scripts/find-missing-csharp-docs.bat | 6 + scripts/find-missing-csharp-docs.ps1 | 358 ++ scripts/missing-csharp-docs.after.json | 23 + scripts/missing-csharp-docs.txt | 1 + scripts/生成注释提示词.txt | 60 + 138 files changed, 15087 insertions(+) create mode 100644 .config/dotnet-tools.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Avalonia-API/Authentication/ApiAuthEndpointService.cs create mode 100644 Avalonia-API/Authentication/JwtOptions.cs create mode 100644 Avalonia-API/Authentication/JwtTokenService.cs create mode 100644 Avalonia-API/Authentication/RefreshTokenService.cs create mode 100644 Avalonia-API/Avalonia-API.csproj create mode 100644 Avalonia-API/Avalonia-API.csproj.user create mode 100644 Avalonia-API/Avalonia-API.http create mode 100644 Avalonia-API/Configuration/ServicesConfiguration.cs create mode 100644 Avalonia-API/Extensions/UnifiedEndpointExtensions.cs create mode 100644 Avalonia-API/Program.cs create mode 100644 Avalonia-API/Properties/launchSettings.json create mode 100644 Avalonia-API/appsettings.Development.json create mode 100644 Avalonia-API/appsettings.json create mode 100644 Avalonia-Common/Avalonia-Common.csproj create mode 100644 Avalonia-Common/Core/ApiResponse.cs create mode 100644 Avalonia-Common/Infrastructure/LoggingConfiguration.cs create mode 100644 Avalonia-EFCore/Avalonia-EFCore.csproj create mode 100644 Avalonia-EFCore/Database/AppDataContext.cs create mode 100644 Avalonia-EFCore/Database/AppDataContextFactory.cs create mode 100644 Avalonia-EFCore/Database/AppDbContext.cs create mode 100644 Avalonia-EFCore/Database/DatabaseConfiguration.cs create mode 100644 Avalonia-EFCore/Database/DatabaseExtensions.cs create mode 100644 Avalonia-EFCore/Database/DatabaseManager.cs create mode 100644 Avalonia-EFCore/Database/DatabaseProviderRegistry.cs create mode 100644 Avalonia-EFCore/Database/ProviderAppDataContexts.cs create mode 100644 Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/MySQL/20260520082626_AutoMigration_20260520162543.cs create mode 100644 Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/MySQL/20260520083306_AutoMigration_20260520163216.cs create mode 100644 Avalonia-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs create mode 100644 Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/PostgreSQL/20260520082617_AutoMigration_20260520162543.cs create mode 100644 Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/PostgreSQL/20260520083254_AutoMigration_20260520163216.cs create mode 100644 Avalonia-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260514000100_InitialCreate.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260515072045_AutoMigration_20260515152037.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260515085847_AutoMigration_20260515165835.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/20260520083230_AutoMigration_20260520163216.cs create mode 100644 Avalonia-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs create mode 100644 Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/SqlServer/20260520082607_AutoMigration_20260520162543.cs create mode 100644 Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.Designer.cs create mode 100644 Avalonia-EFCore/Migrations/SqlServer/20260520083242_AutoMigration_20260520163216.cs create mode 100644 Avalonia-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs create mode 100644 Avalonia-EFCore/Models/ApiRefreshTokenEntity.cs create mode 100644 Avalonia-EFCore/Models/UserEntity.cs create mode 100644 Avalonia-EFCore/Models/WeatherForecast.cs create mode 100644 Avalonia-EFCore/Models/WeatherForecastEntity.cs create mode 100644 Avalonia-PC/.github/copilot-instructions.md create mode 100644 Avalonia-PC/App.axaml create mode 100644 Avalonia-PC/App.axaml.cs create mode 100644 Avalonia-PC/Assets/avalonia-logo.ico create mode 100644 Avalonia-PC/Authentication/DefaultPcThirdPartyAuthorizationClient.cs create mode 100644 Avalonia-PC/Authentication/PcAuthEndpointService.cs create mode 100644 Avalonia-PC/Authentication/PcAuthService.cs create mode 100644 Avalonia-PC/Authentication/PcGlobalTokenService.cs create mode 100644 Avalonia-PC/Avalonia-PC.csproj create mode 100644 Avalonia-PC/Avalonia-PC.csproj.user create mode 100644 Avalonia-PC/Avalonia-PC.slnx create mode 100644 Avalonia-PC/Program.cs create mode 100644 Avalonia-PC/Properties/launchSettings.json create mode 100644 Avalonia-PC/ViewLocator.cs create mode 100644 Avalonia-PC/ViewModels/MainWindowViewModel.cs create mode 100644 Avalonia-PC/ViewModels/ViewModelBase.cs create mode 100644 Avalonia-PC/Views/MainWindow.BridgeScript.cs create mode 100644 Avalonia-PC/Views/MainWindow.Routes.cs create mode 100644 Avalonia-PC/Views/MainWindow.axaml create mode 100644 Avalonia-PC/Views/MainWindow.axaml.cs create mode 100644 Avalonia-PC/app.manifest create mode 100644 Avalonia-PC/www/api.js create mode 100644 Avalonia-PC/www/index.html create mode 100644 Avalonia-Services/Avalonia-Services.csproj create mode 100644 Avalonia-Services/Core/EndpointPrinter.cs create mode 100644 Avalonia-Services/Core/GlobalExceptionFilter.cs create mode 100644 Avalonia-Services/Core/IAuthService.cs create mode 100644 Avalonia-Services/Core/IEndpointFilter.cs create mode 100644 Avalonia-Services/Core/ServiceEndpointCollection.cs create mode 100644 Avalonia-Services/Core/ServiceEndpointContext.cs create mode 100644 Avalonia-Services/Endpoints/AppEndpoints.cs create mode 100644 Avalonia-Services/Endpoints/AuthEndpoints.cs create mode 100644 Avalonia-Services/Extensions/DesktopEndpointAdapter.cs create mode 100644 Avalonia-Services/Services/AuthService/AuthContracts.cs create mode 100644 Avalonia-Services/Services/AuthService/AuthEndpointServices.cs create mode 100644 Avalonia-Services/Services/WeatherForecastService.cs create mode 100644 Avalonia-Web-VUE/.editorconfig create mode 100644 Avalonia-Web-VUE/.gitattributes create mode 100644 Avalonia-Web-VUE/.gitignore create mode 100644 Avalonia-Web-VUE/.oxlintrc.json create mode 100644 Avalonia-Web-VUE/CHANGELOG.md create mode 100644 Avalonia-Web-VUE/README.md create mode 100644 Avalonia-Web-VUE/avalonia-web-vue.esproj create mode 100644 Avalonia-Web-VUE/env.d.ts create mode 100644 Avalonia-Web-VUE/eslint.config.ts create mode 100644 Avalonia-Web-VUE/index.html create mode 100644 Avalonia-Web-VUE/package-lock.json create mode 100644 Avalonia-Web-VUE/package.json create mode 100644 Avalonia-Web-VUE/public/favicon.ico create mode 100644 Avalonia-Web-VUE/src/App.vue create mode 100644 Avalonia-Web-VUE/src/api/env.ts create mode 100644 Avalonia-Web-VUE/src/api/http.ts create mode 100644 Avalonia-Web-VUE/src/api/index.ts create mode 100644 Avalonia-Web-VUE/src/assets/base.css create mode 100644 Avalonia-Web-VUE/src/assets/logo.svg create mode 100644 Avalonia-Web-VUE/src/assets/main.css create mode 100644 Avalonia-Web-VUE/src/components/HelloWorld.vue create mode 100644 Avalonia-Web-VUE/src/components/TheWelcome.vue create mode 100644 Avalonia-Web-VUE/src/components/WelcomeItem.vue create mode 100644 Avalonia-Web-VUE/src/components/icons/IconCommunity.vue create mode 100644 Avalonia-Web-VUE/src/components/icons/IconDocumentation.vue create mode 100644 Avalonia-Web-VUE/src/components/icons/IconEcosystem.vue create mode 100644 Avalonia-Web-VUE/src/components/icons/IconSupport.vue create mode 100644 Avalonia-Web-VUE/src/components/icons/IconTooling.vue create mode 100644 Avalonia-Web-VUE/src/main.ts create mode 100644 Avalonia-Web-VUE/src/shims-vue.d.ts create mode 100644 Avalonia-Web-VUE/tsconfig.app.json create mode 100644 Avalonia-Web-VUE/tsconfig.json create mode 100644 Avalonia-Web-VUE/tsconfig.node.json create mode 100644 Avalonia-Web-VUE/vite.config.ts create mode 100644 README.md create mode 100644 package-scripts/installer/Avalonia-PC.iss create mode 100644 package-scripts/package-pc.bat create mode 100644 package-scripts/package-pc.ps1 create mode 100644 scripts/add-migration.bat create mode 100644 scripts/add-migration.cmd create mode 100644 scripts/add-migration.ps1 create mode 100644 scripts/find-missing-csharp-docs.bat create mode 100644 scripts/find-missing-csharp-docs.ps1 create mode 100644 scripts/missing-csharp-docs.after.json create mode 100644 scripts/missing-csharp-docs.txt create mode 100644 scripts/生成注释提示词.txt 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 0000000000000000000000000000000000000000..f7da8bb5863b7cecec2adcdebd948fe2f9418d0c GIT binary patch literal 175875 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 + /// 第三方授权客户端占位实现。接入真实第三方接口时替换此服务即可。 + /// + 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 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 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 定位源码成员。 +- 逐文件修改。 +- 不要新建额外的注释生成脚本。 +- 不要生成新的工具脚本。 +- 可以使用现有脚本重新扫描验证。 +- 最终直接完成代码修改。