This commit is contained in:
2026-05-21 15:52:36 +08:00
commit e3fe965f10
138 changed files with 15087 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0add-migration.ps1" %*
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0add-migration.ps1" %*
+92
View File
@@ -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*\{(?<body>.*?)\n\s*\}", "Singleline")
$downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?<body>.*?)\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."
+6
View File
@@ -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%
+358
View File
@@ -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+(?<name>[A-Za-z_][A-Za-z0-9_]*)') {
return $Matches["name"]
}
}
"Delegate" {
if ($Declaration -match '\b(?<name>[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+(?<name>[A-Za-z_][A-Za-z0-9_]*)') {
return $Matches["name"]
}
}
"Constructor" {
if ($Declaration -match '\b(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*\(') {
return $Matches["name"]
}
}
"Method" {
$matches = [regex]::Matches($Declaration, '\s(?<name>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(?<name>[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(?<name>[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)"
}
+23
View File
@@ -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) {"
}
]
+1
View File
@@ -0,0 +1 @@
+60
View File
@@ -0,0 +1,60 @@
你是一个资深 C# 工程师。现在我会给你一个 missing-csharp-docs.txt 文件,里面列出了项目中缺少 XML 文档注释的 C# 类型、方法、属性、字段、构造函数、接口成员等。
请根据这个 txt 文件逐项读取对应源码文件,并直接修改源码,为缺少注释的成员补全中文 XML 文档注释。
要求如下:
1. 注释必须是中文。
2. 使用标准 C# XML 文档注释格式。
3. 类、接口、record、struct、enum 使用:
/// <summary>
/// ...
/// </summary>
4. 方法、构造函数必须尽量补全:
/// <summary>
/// ...
/// </summary>
/// <param name="xxx">...</param>
/// <returns>...</returns>
/// <exception cref="...">...</exception>
5. 如果方法没有参数,不要生成 <param>。
6. 如果方法返回 void、Task 或构造函数,不要生成无意义的 <returns>。
7. 如果方法返回 Task<T>、ValueTask<T>、T、IEnumerable<T> 等有实际返回值的类型,需要生成 <returns>,说明返回内容。
8. 如果方法体中明确 throw 了异常,或声明逻辑明显可能抛出特定异常,可以补充 <exception>;不确定时不要乱写。
9. 属性使用:
/// <summary>
/// 获取或设置...
/// </summary>
如果是只读属性,写“获取...”;如果是计算属性,说明它计算或表示的含义。
10. 字段使用:
/// <summary>
/// 保存/定义/指示...
/// </summary>
11. 枚举成员也要加中文 summary,说明每个枚举值的含义。
12. 接口方法必须在 interface 中写完整注释,包括 summary、param、returns。
13. 具体实现类如果实现了已有注释的接口方法,优先使用:
/// <inheritdoc />
不要在实现类重复写一大段相同注释。
14. 如果实现类方法不是接口实现,或者接口中没有对应注释,则在实现类中写完整注释。
15. 不要只根据方法名机械生成注释,要结合方法体、参数、返回值、调用逻辑和业务语义来写。
16. 不要改业务逻辑,不要改方法签名,不要改格式以外的代码。
17. XML 注释放在 attribute 之前,例如:
/// <summary>
/// 用户 ID。
/// </summary>
[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 定位源码成员。
- 逐文件修改。
- 不要新建额外的注释生成脚本。
- 不要生成新的工具脚本。
- 可以使用现有脚本重新扫描验证。
- 最终直接完成代码修改。