using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OrpaonVision.Core.Results; using OrpaonVision.Core.Training.Contracts; using OrpaonVision.Core.Training.Contracts.Commands; using OrpaonVision.Core.Training; using System.IO; using System.IO.Compression; using System.Text.Json; using System.Security.Cryptography; using System.Text; namespace OrpaonVision.ConfigApp.Infrastructure.Services; /// /// 模型包配置选项。 /// public class ModelPackageOptions { /// /// 模型包基础路径。 /// public string BasePath { get; set; } = "ModelPackages"; } /// /// 校验结果。 /// public class ValidationResult { /// /// 警告信息。 /// public string[] Warnings { get; set; } = Array.Empty(); } /// /// 模型包应用服务实现。 /// public class ModelPackageAppService : IModelPackageAppService { private readonly ILogger _logger; private readonly ModelPackageOptions _options; private readonly string _basePath; private readonly IRuleValidationService _ruleValidationService; /// /// 初始化模型包应用服务。 /// /// 日志记录器。 /// 配置选项。 /// 规则校验服务。 public ModelPackageAppService( ILogger logger, IOptions options, IRuleValidationService ruleValidationService) { _logger = logger; _options = options.Value; _basePath = _options.BasePath ?? "ModelPackages"; _ruleValidationService = ruleValidationService; // 确保基础目录存在 if (!Directory.Exists(_basePath)) { Directory.CreateDirectory(_basePath); } } /// public async Task> BuildPackageAsync(BuildModelPackageCommand command, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始构建模型包: {Name}", command.Name); var packageId = Guid.NewGuid(); var packageDir = Path.Combine(_basePath, packageId.ToString("N")); // 创建模型包目录 Directory.CreateDirectory(packageDir); // 创建manifest.json var manifest = CreateManifest(command, packageId); var manifestPath = Path.Combine(packageDir, "manifest.json"); await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }), cancellationToken); // 复制模型文件 var modelDir = Path.Combine(packageDir, "model"); Directory.CreateDirectory(modelDir); var modelFileName = Path.GetFileName(command.ModelFilePath); var targetModelPath = Path.Combine(modelDir, modelFileName); File.Copy(command.ModelFilePath, targetModelPath, true); // 创建metadata目录和文件 var metadataDir = Path.Combine(packageDir, "metadata"); Directory.CreateDirectory(metadataDir); // 创建类别映射文件 var classMapping = new { classes = new[] { new { id = 1, name = "part" } } }; var classMappingPath = Path.Combine(metadataDir, "class-mapping.json"); await File.WriteAllTextAsync(classMappingPath, JsonSerializer.Serialize(classMapping, new JsonSerializerOptions { WriteIndented = true }), cancellationToken); // 创建阈值文件 var thresholds = new { confidence = 0.5f, nms = 0.4f }; var thresholdsPath = Path.Combine(metadataDir, "thresholds.json"); await File.WriteAllTextAsync(thresholdsPath, JsonSerializer.Serialize(thresholds, new JsonSerializerOptions { WriteIndented = true }), cancellationToken); // 创建产品兼容性文件 var compatibility = new { productTypes = new[] { new { code = command.ProductTypeId.ToString(), name = "Product Type" } }, ruleVersions = new[] { "1.0.0" } }; var compatibilityPath = Path.Combine(metadataDir, "product-compatibility.json"); await File.WriteAllTextAsync(compatibilityPath, JsonSerializer.Serialize(compatibility, new JsonSerializerOptions { WriteIndented = true }), cancellationToken); // 计算文件校验和 var checksums = await CalculateChecksums(packageDir, cancellationToken); var checksumsPath = Path.Combine(packageDir, "checksums.sha256"); await File.WriteAllTextAsync(checksumsPath, JsonSerializer.Serialize(checksums, new JsonSerializerOptions { WriteIndented = true }), cancellationToken); _logger.LogInformation("成功构建模型包: {PackageId}", packageId); return Result.Success(packageId); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "构建模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("BUILD_PACKAGE_FAILED", "构建模型包失败。", traceId, ex.Message); } } /// public async Task> ExportPackageAsync(ExportModelPackageCommand command, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始导出模型包: {PackageId}", command.ModelPackageId); var packageDir = Path.Combine(_basePath, command.ModelPackageId.ToString("N")); if (!Directory.Exists(packageDir)) { return Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在。"); } var exportFileName = $"model_package_{DateTime.UtcNow:yyyyMMddHHmmss}.ovpkg"; var exportPath = Path.Combine(_basePath, "exports", exportFileName); // 确保导出目录存在 Directory.CreateDirectory(Path.GetDirectoryName(exportPath)!); // 创建ZIP包 using (var archive = ZipFile.Open(exportPath, ZipArchiveMode.Create)) { foreach (var file in Directory.EnumerateFiles(packageDir, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(packageDir, file); archive.CreateEntryFromFile(file, relativePath); } } var fileInfo = new FileInfo(exportPath); var exportDto = new ExportFileDto { FileName = exportFileName, FilePath = exportPath, FileSizeBytes = fileInfo.Length, CreatedAtUtc = DateTime.UtcNow }; _logger.LogInformation("成功导出模型包: {ExportPath}", exportPath); return Result.Success(exportDto); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "导出模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("EXPORT_PACKAGE_FAILED", "导出模型包失败。", traceId, ex.Message); } } /// public async Task> GetDetailAsync(Guid modelPackageId, CancellationToken cancellationToken = default) { try { _logger.LogInformation("获取模型包详情: {PackageId}", modelPackageId); var packageDir = Path.Combine(_basePath, modelPackageId.ToString("N")); if (!Directory.Exists(packageDir)) { return Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在。"); } var manifestPath = Path.Combine(packageDir, "manifest.json"); if (!File.Exists(manifestPath)) { return Result.Fail("MANIFEST_NOT_FOUND", "模型包清单文件不存在。"); } var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken); var manifest = JsonSerializer.Deserialize(manifestJson); var package = new ModelPackageDetailDto { ModelPackageId = modelPackageId, Name = manifest.GetProperty("packageName").GetString() ?? "", VersionNo = manifest.GetProperty("packageVersion").GetString() ?? "", Description = manifest.GetProperty("description").GetString() ?? "", CreatedAtUtc = DateTime.Parse(manifest.GetProperty("createdAt").GetString() ?? DateTime.UtcNow.ToString("O")), CreatedBy = manifest.GetProperty("createdBy").GetString() ?? "", Status = "Imported", IsActive = false, // 默认未激活 PackageSizeBytes = await CalculateDirectorySize(packageDir, cancellationToken) }; return Result.Success(package); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "获取模型包详情失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("GET_PACKAGE_DETAIL_FAILED", "获取模型包详情失败。", traceId, ex.Message); } } /// public async Task> ImportPackageAsync(string packagePath, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始导入模型包: {PackagePath}", packagePath); if (!File.Exists(packagePath)) { return Result.Fail("PACKAGE_FILE_NOT_FOUND", "模型包文件不存在。"); } var packageId = Guid.NewGuid(); var extractDir = Path.Combine(_basePath, packageId.ToString("N")); // 解压模型包 using (var archive = ZipFile.OpenRead(packagePath)) { Directory.CreateDirectory(extractDir); foreach (var entry in archive.Entries) { var targetPath = Path.Combine(extractDir, entry.FullName); // 确保目标路径在提取目录内(防止路径遍历攻击) if (!targetPath.StartsWith(extractDir)) { return Result.Fail("INVALID_PACKAGE_STRUCTURE", "模型包包含无效的文件路径。"); } var targetDir = Path.GetDirectoryName(targetPath); if (!string.IsNullOrEmpty(targetDir)) { Directory.CreateDirectory(targetDir); } entry.ExtractToFile(targetPath, overwrite: true); } } // 校验模型包结构 var validationResult = await ValidatePackageStructure(extractDir, cancellationToken); if (!validationResult.Succeeded) { // 清理失败的导入 if (Directory.Exists(extractDir)) { Directory.Delete(extractDir, true); } return Result.Fail(validationResult.Code, validationResult.Message); } // 校验规则快照包(如果存在) var ruleSnapshotPath = Path.Combine(extractDir, "rules", "snapshot.ovpkg"); if (File.Exists(ruleSnapshotPath)) { _logger.LogInformation("发现规则快照包,开始校验: {RuleSnapshotPath}", ruleSnapshotPath); var ruleValidationResult = await _ruleValidationService.ValidateRuleSnapshotPackageAsync(ruleSnapshotPath, cancellationToken); if (!ruleValidationResult.Succeeded) { // 清理失败的导入 if (Directory.Exists(extractDir)) { Directory.Delete(extractDir, true); } return Result.Fail(ruleValidationResult.Code, $"规则快照包校验失败: {ruleValidationResult.Message}"); } if (!ruleValidationResult.Data!.IsValid) { var errors = string.Join("; ", ruleValidationResult.Data.Errors); _logger.LogWarning("规则快照包校验发现问题: {Errors}", errors); // 可以选择是否将规则校验失败视为导入失败,这里仅记录警告 } } var importResult = new ModelPackageImportResultDto { ModelPackageId = packageId, Status = ModelPackageImportStatus.Imported, Message = "导入成功", ImportedAtUtc = DateTime.UtcNow, ImportedBy = "CurrentUser" }; _logger.LogInformation("成功导入模型包: {PackageId}", packageId); return Result.Success(importResult); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "导入模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("IMPORT_PACKAGE_FAILED", "导入模型包失败。", traceId, ex.Message); } } /// public async Task> ValidatePackageAsync(Guid modelPackageId, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始校验模型包: {PackageId}", modelPackageId); var packageDir = Path.Combine(_basePath, modelPackageId.ToString("N")); if (!Directory.Exists(packageDir)) { return Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在。"); } var validationResult = await ValidatePackageStructure(packageDir, cancellationToken); var validationDto = new ModelPackageValidationResultDto { ModelPackageId = modelPackageId, Status = validationResult.Succeeded ? ModelPackageValidationStatus.Passed : ModelPackageValidationStatus.Failed, Message = validationResult.Succeeded ? "校验通过" : validationResult.Message, ValidatedAtUtc = DateTime.UtcNow, ValidatedBy = "CurrentUser" }; _logger.LogInformation("模型包校验完成: {PackageId}, IsValid: {IsValid}", modelPackageId, validationResult.Succeeded); return Result.Success(validationDto); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "校验模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("VALIDATE_PACKAGE_FAILED", "校验模型包失败。", traceId, ex.Message); } } /// public async Task> ActivatePackageAsync(Guid modelPackageId, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始激活模型包: {PackageId}", modelPackageId); var packageDir = Path.Combine(_basePath, modelPackageId.ToString("N")); if (!Directory.Exists(packageDir)) { return Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在。"); } // 先校验模型包 var validationResult = await ValidatePackageStructure(packageDir, cancellationToken); if (!validationResult.Succeeded) { return Result.Fail("PACKAGE_VALIDATION_FAILED", "模型包校验失败,无法激活。"); } // 检查是否已有激活的模型包(同一机种) await DeactivateOtherPackagesAsync(modelPackageId, cancellationToken); // 创建激活状态文件 var activationFile = Path.Combine(packageDir, "activation.json"); var activationInfo = new { isActive = true, activatedAt = DateTime.UtcNow.ToString("O"), activatedBy = "CurrentUser" // TODO: 从当前用户上下文获取 }; await File.WriteAllTextAsync(activationFile, JsonSerializer.Serialize(activationInfo, new JsonSerializerOptions { WriteIndented = true }), cancellationToken); var activationResult = new ModelPackageActivationResultDto { ModelPackageId = modelPackageId, Status = ModelPackageActivationStatus.Activated, Message = "激活成功", ActivatedAtUtc = DateTime.UtcNow, ActivatedBy = "CurrentUser" }; _logger.LogInformation("成功激活模型包: {PackageId}", modelPackageId); return Result.Success(activationResult); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "激活模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("ACTIVATE_PACKAGE_FAILED", "激活模型包失败。", traceId, ex.Message); } } /// public async Task> DeactivatePackageAsync(Guid modelPackageId, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始停用模型包: {PackageId}", modelPackageId); var packageDir = Path.Combine(_basePath, modelPackageId.ToString("N")); if (!Directory.Exists(packageDir)) { return Result.Fail("PACKAGE_NOT_FOUND", "模型包不存在。"); } var activationFile = Path.Combine(packageDir, "activation.json"); if (File.Exists(activationFile)) { File.Delete(activationFile); } var deactivationResult = new ModelPackageDeactivationResultDto { ModelPackageId = modelPackageId, Status = ModelPackageDeactivationStatus.Deactivated, Message = "停用成功", DeactivatedAtUtc = DateTime.UtcNow, DeactivatedBy = "CurrentUser" }; _logger.LogInformation("成功停用模型包: {PackageId}", modelPackageId); return Result.Success(deactivationResult); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "停用模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("DEACTIVATE_PACKAGE_FAILED", "停用模型包失败。", traceId, ex.Message); } } /// public async Task> RollbackToPackageAsync(Guid targetModelPackageId, CancellationToken cancellationToken = default) { try { _logger.LogInformation("开始回滚到模型包: {PackageId}", targetModelPackageId); var targetPackageDir = Path.Combine(_basePath, targetModelPackageId.ToString("N")); if (!Directory.Exists(targetPackageDir)) { return Result.Fail("TARGET_PACKAGE_NOT_FOUND", "目标模型包不存在。"); } // 停用当前激活的模型包 await DeactivateAllPackagesAsync(cancellationToken); // 激活目标模型包 var activationResult = await ActivatePackageAsync(targetModelPackageId, cancellationToken); if (!activationResult.Succeeded) { return Result.Fail("ACTIVATION_FAILED", "回滚失败:无法激活目标模型包。"); } var rollbackResult = new ModelPackageRollbackResultDto { TargetModelPackageId = targetModelPackageId, Status = ModelPackageRollbackStatus.RolledBack, Message = "回滚成功", RolledBackAtUtc = DateTime.UtcNow, RolledBackBy = "CurrentUser" }; _logger.LogInformation("成功回滚到模型包: {PackageId}", targetModelPackageId); return Result.Success(rollbackResult); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "回滚模型包失败。TraceId: {TraceId}", traceId); return Result.FailWithTrace("ROLLBACK_PACKAGE_FAILED", "回滚模型包失败。", traceId, ex.Message); } } /// public async Task>> GetImportedPackagesAsync(CancellationToken cancellationToken = default) { try { _logger.LogInformation("获取已导入的模型包列表"); var packages = new List(); if (Directory.Exists(_basePath)) { foreach (var packageDir in Directory.GetDirectories(_basePath)) { var packageIdStr = Path.GetFileName(packageDir); if (Guid.TryParse(packageIdStr, out var packageId)) { var manifestPath = Path.Combine(packageDir, "manifest.json"); if (File.Exists(manifestPath)) { var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken); var manifest = JsonSerializer.Deserialize(manifestJson); var activationFile = Path.Combine(packageDir, "activation.json"); var isActive = File.Exists(activationFile); packages.Add(new ModelPackageSummaryDto { ModelPackageId = packageId, Name = manifest.GetProperty("packageName").GetString() ?? "", VersionNo = manifest.GetProperty("packageVersion").GetString() ?? "", CreatedAtUtc = DateTime.Parse(manifest.GetProperty("createdAt").GetString() ?? DateTime.UtcNow.ToString("O")), IsActive = isActive }); } } } } return Result>.Success(packages); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError(ex, "获取已导入的模型包列表失败。TraceId: {TraceId}", traceId); return Result>.FailWithTrace("GET_IMPORTED_PACKAGES_FAILED", "获取已导入的模型包列表失败。", traceId, ex.Message); } } #region 辅助方法 /// /// 创建模型包清单。 /// private static object CreateManifest(BuildModelPackageCommand command, Guid packageId) { return new { schemaVersion = "1.0", packageCode = $"PKG-{command.ProductTypeId}-1.0", packageName = command.Name, packageVersion = "1.0", packageType = "runtime-model-package", createdAt = DateTime.UtcNow.ToString("O"), createdBy = command.CreatedBy, description = command.Description, model = new { modelVersionCode = command.ModelVersionId.ToString(), modelName = command.Name, modelVersion = "1.0", framework = "YOLO", runtimeFormat = "ONNX", inputSize = "640x640", labelSetCode = "default", labelSetVersion = "1.0", trainingJobCode = command.TrainingJobId.ToString(), recommendedConfidence = 0.5f }, compatibility = new { compatibilityVersion = command.CompatibilityVersion, applicableProductTypes = new[] { command.ProductTypeId.ToString() }, recommendedRuleBindings = new[] { "1.0.0" } }, files = new { manifest = "manifest.json", checksums = "checksums.sha256", model = "model/model.onnx", classMapping = "metadata/class-mapping.json", thresholds = "metadata/thresholds.json", productCompatibility = "metadata/product-compatibility.json" }, extensions = new object() }; } /// /// 计算文件校验和。 /// private static async Task> CalculateChecksums(string packageDir, CancellationToken cancellationToken) { var checksums = new Dictionary(); foreach (var file in Directory.EnumerateFiles(packageDir, "*", SearchOption.AllDirectories)) { if (Path.GetFileName(file) == "checksums.sha256") continue; // 跳过校验和文件本身 var relativePath = Path.GetRelativePath(packageDir, file).Replace("\\", "/"); var hash = await ComputeSHA256Hash(file, cancellationToken); checksums[relativePath] = hash; } return checksums; } /// /// 计算SHA256哈希。 /// private static async Task ComputeSHA256Hash(string filePath, CancellationToken cancellationToken) { using var sha256 = SHA256.Create(); await using var stream = File.OpenRead(filePath); var hash = await sha256.ComputeHashAsync(stream, cancellationToken); return Convert.ToHexString(hash).ToLowerInvariant(); } /// /// 计算目录大小。 /// private static async Task CalculateDirectorySize(string directory, CancellationToken cancellationToken) { long size = 0; foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) { var fileInfo = new FileInfo(file); size += fileInfo.Length; } return size; } /// /// 校验模型包结构。 /// private async Task> ValidatePackageStructure(string packageDir, CancellationToken cancellationToken) { var warnings = new List(); // 检查必需文件 var requiredFiles = new[] { "manifest.json", "checksums.sha256", "model/model.onnx", "metadata/class-mapping.json", "metadata/thresholds.json", "metadata/product-compatibility.json" }; foreach (var requiredFile in requiredFiles) { var filePath = Path.Combine(packageDir, requiredFile); if (!File.Exists(filePath)) { return Result.Fail($"MISSING_REQUIRED_FILE", $"缺少必需文件: {requiredFile}"); } } // 校验manifest.json格式 try { var manifestPath = Path.Combine(packageDir, "manifest.json"); var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken); var manifest = JsonSerializer.Deserialize(manifestJson); // 检查必需字段 var requiredFields = new[] { "schemaVersion", "packageCode", "packageName", "packageVersion", "createdAt" }; foreach (var field in requiredFields) { if (!manifest.TryGetProperty(field, out _)) { return Result.Fail($"INVALID_MANIFEST", $"manifest.json缺少必需字段: {field}"); } } } catch (JsonException ex) { return Result.Fail($"INVALID_MANIFEST_FORMAT", $"manifest.json格式错误: {ex.Message}"); } // 校验文件完整性 var checksumsPath = Path.Combine(packageDir, "checksums.sha256"); if (File.Exists(checksumsPath)) { var checksumsJson = await File.ReadAllTextAsync(checksumsPath, cancellationToken); var checksums = JsonSerializer.Deserialize>(checksumsJson); if (checksums != null) { foreach (var kvp in checksums) { var filePath = Path.Combine(packageDir, kvp.Key.Replace("/", Path.DirectorySeparatorChar.ToString())); if (File.Exists(filePath)) { var actualHash = await ComputeSHA256Hash(filePath, cancellationToken); if (actualHash != kvp.Value) { return Result.Fail($"CHECKSUM_MISMATCH", $"文件校验和不匹配: {kvp.Key}"); } } } } } return Result.Success(new ValidationResult { Warnings = warnings.ToArray() }); } /// /// 停用其他模型包。 /// private async Task DeactivateOtherPackagesAsync(Guid currentPackageId, CancellationToken cancellationToken) { if (!Directory.Exists(_basePath)) return; foreach (var packageDir in Directory.GetDirectories(_basePath)) { var packageIdStr = Path.GetFileName(packageDir); if (Guid.TryParse(packageIdStr, out var packageId) && packageId != currentPackageId) { var activationFile = Path.Combine(packageDir, "activation.json"); if (File.Exists(activationFile)) { File.Delete(activationFile); } } } await Task.CompletedTask; } /// /// 停用所有模型包。 /// private async Task DeactivateAllPackagesAsync(CancellationToken cancellationToken) { if (!Directory.Exists(_basePath)) return; foreach (var packageDir in Directory.GetDirectories(_basePath)) { var activationFile = Path.Combine(packageDir, "activation.json"); if (File.Exists(activationFile)) { File.Delete(activationFile); } } await Task.CompletedTask; } #endregion }