feat: 添加批量和目录转换功能

批量转换服务:
- BatchConversionService: 批量转换服务实现
- ConvertDirectoryAsync: 目录转换(递归所有子目录)
- ConvertFilesAsync: 多文件批量转换
- 保持原始目录结构
- 自动生成批量报告

CLI 工具增强:
- --batch/-b: 启用批量转换模式
- --recursive/-r: 递归处理子目录
- --verbose: 显示详细信息
- convert 命令自动检测目录/文件模式

批量转换结果:
- BatchConversionResult: 批量转换结果
- ConvertedFileInfo: 成功文件详情
- FailedFileInfo: 失败文件详情
- 统计:总数/成功/失败/耗时

测试覆盖:
- ConvertDirectoryAsync_ValidDirectory: 目录转换测试
- ConvertFilesAsync_MultipleFiles: 多文件测试

总计:40 个测试全部通过 

使用示例:
# 转换整个目录
dotnet run --project CodePlay.CLI -- convert -s CSharp -t Java -i ./src -o ./output-java -b

# 递归转换(默认)
dotnet run --project CodePlay.CLI -- convert -s CSharp -t Java -i ./src -b -r true

# 详细输出
dotnet run --project CodePlay.CLI -- convert -s CSharp -t Java -i ./src -b --verbose
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
This commit is contained in:
monkeycode-ai
2026-06-03 10:31:34 +00:00
parent cc6be55677
commit 00570c129a
5 changed files with 635 additions and 102 deletions
@@ -0,0 +1,195 @@
using CodePlay.Core.Models;
using CodePlay.Core.Common;
namespace CodePlay.Core.Services;
public interface IBatchConversionService
{
Task<BatchConversionResult> ConvertDirectoryAsync(string sourceDirectory, string targetDirectory,
LanguageType sourceLanguage, LanguageType targetLanguage, ConversionOptions? options = null,
CancellationToken cancellationToken = default);
Task<BatchConversionResult> ConvertFilesAsync(IEnumerable<string> sourceFiles, string targetDirectory,
LanguageType sourceLanguage, LanguageType targetLanguage, ConversionOptions? options = null,
CancellationToken cancellationToken = default);
}
public class BatchConversionService : IBatchConversionService
{
private readonly ConversionService _conversionService;
private readonly IReportStorageService _reportStorageService;
public BatchConversionService(ConversionService conversionService, IReportStorageService reportStorageService)
{
_conversionService = conversionService;
_reportStorageService = reportStorageService;
}
public async Task<BatchConversionResult> ConvertDirectoryAsync(string sourceDirectory, string targetDirectory,
LanguageType sourceLanguage, LanguageType targetLanguage, ConversionOptions? options = null,
CancellationToken cancellationToken = default)
{
var result = new BatchConversionResult
{
SourceDirectory = sourceDirectory,
TargetDirectory = targetDirectory,
SourceLanguage = sourceLanguage,
TargetLanguage = targetLanguage,
StartedAt = DateTime.UtcNow
};
if (!Directory.Exists(sourceDirectory))
{
result.Success = false;
result.ErrorMessage = $"Source directory not found: {sourceDirectory}";
return result;
}
var fileExtension = GetFileExtension(sourceLanguage);
var sourceFiles = Directory.GetFiles(sourceDirectory, $"*{fileExtension}", SearchOption.AllDirectories);
return await ConvertFilesAsync(sourceFiles, targetDirectory, sourceLanguage, targetLanguage, options, cancellationToken);
}
public async Task<BatchConversionResult> ConvertFilesAsync(IEnumerable<string> sourceFiles, string targetDirectory,
LanguageType sourceLanguage, LanguageType targetLanguage, ConversionOptions? options = null,
CancellationToken cancellationToken = default)
{
var result = new BatchConversionResult
{
TargetDirectory = targetDirectory,
SourceLanguage = sourceLanguage,
TargetLanguage = targetLanguage,
StartedAt = DateTime.UtcNow,
TotalFiles = sourceFiles.Count()
};
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
var targetExtension = GetFileExtension(targetLanguage);
foreach (var sourceFile in sourceFiles)
{
if (cancellationToken.IsCancellationRequested)
{
result.CancelledAt = DateTime.UtcNow;
break;
}
try
{
var relativePath = Path.GetRelativePath(Path.GetDirectoryName(sourceFile)!, sourceFile);
var targetFileName = Path.ChangeExtension(relativePath, targetExtension);
var targetFilePath = Path.Combine(targetDirectory, targetFileName);
var targetFileDir = Path.GetDirectoryName(targetFilePath)!;
if (!Directory.Exists(targetFileDir))
{
Directory.CreateDirectory(targetFileDir);
}
var sourceCode = await File.ReadAllTextAsync(sourceFile, cancellationToken);
var conversionResult = await _conversionService.ConvertAsync(
sourceCode, sourceLanguage, targetLanguage, options);
if (conversionResult.Success)
{
await File.WriteAllTextAsync(targetFilePath, conversionResult.TransformedCode, cancellationToken);
result.SuccessfulFiles++;
result.ConvertedFiles.Add(new ConvertedFileInfo
{
SourceFile = sourceFile,
TargetFile = targetFilePath,
LinesConverted = conversionResult.Report?.LinesConverted ?? 0,
ClassesConverted = conversionResult.Report?.ClassesConverted ?? 0,
MethodsConverted = conversionResult.Report?.MethodsConverted ?? 0,
Warnings = conversionResult.Warnings.Count,
Issues = conversionResult.Report?.Issues.Count ?? 0
});
if (conversionResult.Report != null)
{
conversionResult.Report.ProjectId = "batch-" + DateTime.UtcNow.ToString("yyyyMMdd");
await _reportStorageService.SaveReportAsync(conversionResult.Report);
}
}
else
{
result.FailedFiles++;
result.FailedFileList.Add(new FailedFileInfo
{
SourceFile = sourceFile,
ErrorMessage = conversionResult.ErrorMessage ?? "Unknown error"
});
}
}
catch (Exception ex)
{
result.FailedFiles++;
result.FailedFileList.Add(new FailedFileInfo
{
SourceFile = sourceFile,
ErrorMessage = ex.Message
});
}
}
result.CompletedAt = DateTime.UtcNow;
result.Duration = result.CompletedAt - result.StartedAt;
result.Success = result.FailedFiles == 0;
return result;
}
private string GetFileExtension(LanguageType language)
{
return language switch
{
LanguageType.CSharp => ".cs",
LanguageType.Java => ".java",
LanguageType.CPlusPlus => ".cpp",
_ => throw new ArgumentException($"Unsupported language: {language}")
};
}
}
public class BatchConversionResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string SourceDirectory { get; set; } = string.Empty;
public string TargetDirectory { get; set; } = string.Empty;
public LanguageType SourceLanguage { get; set; }
public LanguageType TargetLanguage { get; set; }
public int TotalFiles { get; set; }
public int SuccessfulFiles { get; set; }
public int FailedFiles { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public DateTime? CancelledAt { get; set; }
public TimeSpan? Duration { get; set; }
public List<ConvertedFileInfo> ConvertedFiles { get; set; } = new();
public List<FailedFileInfo> FailedFileList { get; set; } = new();
}
public class ConvertedFileInfo
{
public string SourceFile { get; set; } = string.Empty;
public string TargetFile { get; set; } = string.Empty;
public int LinesConverted { get; set; }
public int ClassesConverted { get; set; }
public int MethodsConverted { get; set; }
public int Warnings { get; set; }
public int Issues { get; set; }
}
public class FailedFileInfo
{
public string SourceFile { get; set; } = string.Empty;
public string? TargetFile { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
}
@@ -30,6 +30,26 @@ public class ConversionService
_converters[(source, target)] = converter;
}
/// <summary>
/// 转换代码 (简化版)
/// </summary>
public async Task<ConversionResult> ConvertAsync(
string sourceCode,
LanguageType sourceLanguage,
LanguageType targetLanguage,
ConversionOptions? options = null)
{
var request = new ConversionRequest
{
SourceCode = sourceCode,
SourceLanguage = sourceLanguage,
TargetLanguage = targetLanguage,
Options = options
};
return await ConvertAsync(request, CancellationToken.None);
}
/// <summary>
/// 转换代码
/// </summary>