版本260406
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 模型包配置选项。
|
||||
/// </summary>
|
||||
public class ModelPackageOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 模型包基础路径。
|
||||
/// </summary>
|
||||
public string BasePath { get; set; } = "ModelPackages";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验结果。
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 警告信息。
|
||||
/// </summary>
|
||||
public string[] Warnings { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模型包应用服务实现。
|
||||
/// </summary>
|
||||
public class ModelPackageAppService : IModelPackageAppService
|
||||
{
|
||||
private readonly ILogger<ModelPackageAppService> _logger;
|
||||
private readonly ModelPackageOptions _options;
|
||||
private readonly string _basePath;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化模型包应用服务。
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <param name="options">模型包配置选项。</param>
|
||||
public ModelPackageAppService(
|
||||
ILogger<ModelPackageAppService> logger,
|
||||
IOptions<ModelPackageOptions> options)
|
||||
{
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
_basePath = _options.BasePath ?? "ModelPackages";
|
||||
|
||||
// 确保基础目录存在
|
||||
if (!Directory.Exists(_basePath))
|
||||
{
|
||||
Directory.CreateDirectory(_basePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<Guid>> 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<Guid>.Success(packageId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "构建模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<Guid>.FailWithTrace("BUILD_PACKAGE_FAILED", "构建模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ExportFileDto>> 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<ExportFileDto>.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<ExportFileDto>.Success(exportDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "导出模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ExportFileDto>.FailWithTrace("EXPORT_PACKAGE_FAILED", "导出模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ModelPackageDetailDto>> 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<ModelPackageDetailDto>.Fail("PACKAGE_NOT_FOUND", "模型包不存在。");
|
||||
}
|
||||
|
||||
var manifestPath = Path.Combine(packageDir, "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
return Result<ModelPackageDetailDto>.Fail("MANIFEST_NOT_FOUND", "模型包清单文件不存在。");
|
||||
}
|
||||
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken);
|
||||
var manifest = JsonSerializer.Deserialize<JsonElement>(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<ModelPackageDetailDto>.Success(package);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "获取模型包详情失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ModelPackageDetailDto>.FailWithTrace("GET_PACKAGE_DETAIL_FAILED", "获取模型包详情失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ModelPackageImportResultDto>> ImportPackageAsync(string packagePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("开始导入模型包: {PackagePath}", packagePath);
|
||||
|
||||
if (!File.Exists(packagePath))
|
||||
{
|
||||
return Result<ModelPackageImportResultDto>.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<ModelPackageImportResultDto>.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<ModelPackageImportResultDto>.Fail(validationResult.Code, validationResult.Message);
|
||||
}
|
||||
|
||||
var importResult = new ModelPackageImportResultDto
|
||||
{
|
||||
ModelPackageId = packageId,
|
||||
Status = ModelPackageImportStatus.Imported,
|
||||
Message = "导入成功",
|
||||
ImportedAtUtc = DateTime.UtcNow,
|
||||
ImportedBy = "CurrentUser"
|
||||
};
|
||||
|
||||
_logger.LogInformation("成功导入模型包: {PackageId}", packageId);
|
||||
return Result<ModelPackageImportResultDto>.Success(importResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "导入模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ModelPackageImportResultDto>.FailWithTrace("IMPORT_PACKAGE_FAILED", "导入模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ModelPackageValidationResultDto>> 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<ModelPackageValidationResultDto>.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<ModelPackageValidationResultDto>.Success(validationDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "校验模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ModelPackageValidationResultDto>.FailWithTrace("VALIDATE_PACKAGE_FAILED", "校验模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ModelPackageActivationResultDto>> 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<ModelPackageActivationResultDto>.Fail("PACKAGE_NOT_FOUND", "模型包不存在。");
|
||||
}
|
||||
|
||||
// 先校验模型包
|
||||
var validationResult = await ValidatePackageStructure(packageDir, cancellationToken);
|
||||
if (!validationResult.Succeeded)
|
||||
{
|
||||
return Result<ModelPackageActivationResultDto>.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<ModelPackageActivationResultDto>.Success(activationResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "激活模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ModelPackageActivationResultDto>.FailWithTrace("ACTIVATE_PACKAGE_FAILED", "激活模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ModelPackageDeactivationResultDto>> 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<ModelPackageDeactivationResultDto>.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<ModelPackageDeactivationResultDto>.Success(deactivationResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "停用模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ModelPackageDeactivationResultDto>.FailWithTrace("DEACTIVATE_PACKAGE_FAILED", "停用模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ModelPackageRollbackResultDto>> 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<ModelPackageRollbackResultDto>.Fail("TARGET_PACKAGE_NOT_FOUND", "目标模型包不存在。");
|
||||
}
|
||||
|
||||
// 停用当前激活的模型包
|
||||
await DeactivateAllPackagesAsync(cancellationToken);
|
||||
|
||||
// 激活目标模型包
|
||||
var activationResult = await ActivatePackageAsync(targetModelPackageId, cancellationToken);
|
||||
if (!activationResult.Succeeded)
|
||||
{
|
||||
return Result<ModelPackageRollbackResultDto>.Fail("ACTIVATION_FAILED", "回滚失败:无法激活目标模型包。");
|
||||
}
|
||||
|
||||
var rollbackResult = new ModelPackageRollbackResultDto
|
||||
{
|
||||
TargetModelPackageId = targetModelPackageId,
|
||||
Status = ModelPackageRollbackStatus.RolledBack,
|
||||
Message = "回滚成功",
|
||||
RolledBackAtUtc = DateTime.UtcNow,
|
||||
RolledBackBy = "CurrentUser"
|
||||
};
|
||||
|
||||
_logger.LogInformation("成功回滚到模型包: {PackageId}", targetModelPackageId);
|
||||
return Result<ModelPackageRollbackResultDto>.Success(rollbackResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "回滚模型包失败。TraceId: {TraceId}", traceId);
|
||||
return Result<ModelPackageRollbackResultDto>.FailWithTrace("ROLLBACK_PACKAGE_FAILED", "回滚模型包失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<IReadOnlyList<ModelPackageSummaryDto>>> GetImportedPackagesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("获取已导入的模型包列表");
|
||||
|
||||
var packages = new List<ModelPackageSummaryDto>();
|
||||
|
||||
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<JsonElement>(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<IReadOnlyList<ModelPackageSummaryDto>>.Success(packages);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var traceId = Guid.NewGuid().ToString("N");
|
||||
_logger.LogError(ex, "获取已导入的模型包列表失败。TraceId: {TraceId}", traceId);
|
||||
return Result<IReadOnlyList<ModelPackageSummaryDto>>.FailWithTrace("GET_IMPORTED_PACKAGES_FAILED", "获取已导入的模型包列表失败。", traceId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#region 辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 创建模型包清单。
|
||||
/// </summary>
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算文件校验和。
|
||||
/// </summary>
|
||||
private static async Task<Dictionary<string, string>> CalculateChecksums(string packageDir, CancellationToken cancellationToken)
|
||||
{
|
||||
var checksums = new Dictionary<string, string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算SHA256哈希。
|
||||
/// </summary>
|
||||
private static async Task<string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算目录大小。
|
||||
/// </summary>
|
||||
private static async Task<long> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验模型包结构。
|
||||
/// </summary>
|
||||
private async Task<Result<ValidationResult>> ValidatePackageStructure(string packageDir, CancellationToken cancellationToken)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
|
||||
// 检查必需文件
|
||||
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<ValidationResult>.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<JsonElement>(manifestJson);
|
||||
|
||||
// 检查必需字段
|
||||
var requiredFields = new[] { "schemaVersion", "packageCode", "packageName", "packageVersion", "createdAt" };
|
||||
foreach (var field in requiredFields)
|
||||
{
|
||||
if (!manifest.TryGetProperty(field, out _))
|
||||
{
|
||||
return Result<ValidationResult>.Fail($"INVALID_MANIFEST", $"manifest.json缺少必需字段: {field}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return Result<ValidationResult>.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<Dictionary<string, string>>(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<ValidationResult>.Fail($"CHECKSUM_MISMATCH", $"文件校验和不匹配: {kvp.Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result<ValidationResult>.Success(new ValidationResult { Warnings = warnings.ToArray() });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用其他模型包。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用所有模型包。
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user