From 0c8830133a746a87ad538252f23e7f4121eac597 Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 1 Jun 2026 17:28:26 +0800 Subject: [PATCH] =?UTF-8?q?feat.Publisher=E5=AE=8C=E6=95=B4=E5=8C=85?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/plonds-uploader.yml | 6 +- .trae/specs/plonds-client-service/spec.md | 170 ++++++++++++++++++ .../specs/plonds-comparator-redesign/spec.md | 5 + .../Publishing/PlondsPublishOptions.cs | 1 + .../Publishing/PlondsPublishResult.cs | 7 +- .../Plonds.Core/Publishing/PlondsPublisher.cs | 58 ++++-- .../Models/PlondsDownloadInfo.cs | 9 +- .../src/Plonds.Tool/Program.cs | 5 + 8 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 .trae/specs/plonds-client-service/spec.md diff --git a/.github/workflows/plonds-uploader.yml b/.github/workflows/plonds-uploader.yml index f24cd2f..42cb82e 100644 --- a/.github/workflows/plonds-uploader.yml +++ b/.github/workflows/plonds-uploader.yml @@ -75,9 +75,10 @@ jobs: set -euo pipefail rm -rf plonds-assets mkdir -p plonds-assets - gh release download "$RELEASE_TAG" -p changed.zip -p PLONDS.json -D plonds-assets --clobber + gh release download "$RELEASE_TAG" -p changed.zip -p PLONDS.json -p files-windows-x64.zip -D plonds-assets --clobber test -f plonds-assets/changed.zip test -f plonds-assets/PLONDS.json + test -f plonds-assets/files-windows-x64.zip jq -e . plonds-assets/PLONDS.json >/dev/null - name: Publish PLONDS assets to Rainyun S3 @@ -106,6 +107,7 @@ jobs: --repository "${{ github.repository }}" \ --manifest "$PWD/plonds-assets/PLONDS.json" \ --changed-zip "$PWD/plonds-assets/changed.zip" \ + --files-zip "$PWD/plonds-assets/files-windows-x64.zip" \ --work-dir "$PWD/plonds-publish-work" \ --s3-prefix "$PLONDS_S3_PREFIX" \ --s3-endpoint "$S3_ENDPOINT" \ @@ -116,7 +118,7 @@ jobs: --s3-public-base-url "$PUBLIC_BASE" \ --s3-public-base-key-prefix "$PLONDS_S3_PUBLIC_BASE_KEY_PREFIX" - jq -e '.downloads.github.changedZipUrl and .downloads.s3.changedFolderUrl' plonds-assets/PLONDS.json >/dev/null + jq -e '.downloads.github.changedZipUrl and .downloads.github.filesZipUrl and .downloads.s3.changedFolderUrl and .downloads.s3.filesFolderUrl' plonds-assets/PLONDS.json >/dev/null - name: Upload enriched PLONDS manifest to GitHub Release env: diff --git a/.trae/specs/plonds-client-service/spec.md b/.trae/specs/plonds-client-service/spec.md new file mode 100644 index 0000000..26eab57 --- /dev/null +++ b/.trae/specs/plonds-client-service/spec.md @@ -0,0 +1,170 @@ +# PLONDS Client Service 独立化设计 + +> 日期:2026-06-01 +> 状态:设计中 + +## 1. 目标 + +PLONDS 在应用内必须作为独立服务存在,负责分发发现、下载、校验和本地包准备。它不是现有 Update 模块的 provider,也不应把 S3/GitHub/source 选择逻辑混入 `LanMountainDesktop/Services/Update/`。 + +最终边界: + +- PLONDS 服务:寻找最新版本、选择下载源、下载 manifest 和包、校验文件、准备本地 staging。 +- 安装程序/安装网关:只消费 PLONDS 已准备好的本地安装输入,执行增量安装或完整安装。 +- UI:只展示 PLONDS 服务和安装程序返回的状态;完整包也失败后才处理错误。 + +## 2. 当前耦合点 + +当前需要拆离的耦合点: + +- `LanMountainDesktop/Services/Settings/SettingsDomainServices.cs` + - 直接持有 `PlondsStaticUpdateService` 与 `PlondsReleaseUpdateService` + - 在 `CheckForUpdatesCoreAsync` 中把 PLONDS 和 GitHub Update fallback 逻辑混在一起 +- `LanMountainDesktop/Services/Update/UpdateInstallGateway.cs` + - 直接判断 `UpdatePayloadKind.DeltaPlonds` + - 直接实例化 `PlondsUpdateApplier` +- `LanMountainDesktop/Services/Update/Plonds*.cs` + - PLONDS apply/parser/payload resolver 仍位于 Update 命名空间 + +## 3. Source 发现规则 + +PLONDS 客户端内置两个初始地址: + +1. S3 上的 PLONDS manifest 地址 +2. GitHub Release 上的 PLONDS manifest 地址 + +两个地址读取的是同一种 JSON 文件,当前文件名为 `PLONDS.json`。客户端每次检查增量更新时,会并行或顺序请求所有已知 source 的 `PLONDS.json`。 + +### 3.1 Source 扩展 + +`PLONDS.json` 可以声明额外 source。客户端读取到额外 source 后,应把它们加入下一轮寻找列表。 + +建议 manifest 扩展字段: + +```json +{ + "sources": [ + { + "id": "rainyun-s3", + "kind": "s3", + "manifestUrl": "https://example.com/plonds/1.2.3/PLONDS.json", + "priority": 100 + }, + { + "id": "github", + "kind": "github", + "manifestUrl": "https://github.com/owner/repo/releases/download/v1.2.3/PLONDS.json", + "priority": 50 + } + ] +} +``` + +规则: + +- `sources` 为空或缺失时,只使用内置 S3 + GitHub。 +- 新 source 不覆盖内置 source,除非 `id` 相同。 +- source 列表需要去重,按 `id` 和 `manifestUrl` 双重去重。 +- source 持久化到 PLONDS 自己的配置/缓存,不写入 Update 设置。 + +## 4. 版本选择规则 + +如果多个 source 返回的版本不一致,客户端选择 `currentVersion` 最高的 manifest。 + +规则: + +- 版本解析使用 `Version` 语义,忽略前导 `v`。 +- 版本相同时,优先选择下载可用性更高的 source。 +- 如果最高版本 manifest 下载包失败,可以尝试同版本的其他 source。 +- 不因为低版本 source 成功而降级,除非用户显式允许。 + +## 5. 下载与回退规则 + +PLONDS 服务优先走增量包: + +1. 下载所选 manifest。 +2. 下载 `changed.zip`。 +3. 校验 `changed.zip` 与 manifest 中的 hash/checksum。 +4. 解压或准备增量 staging。 +5. 交给安装程序执行增量安装。 + +如果增量流程失败,PLONDS 服务自动改用完整包: + +1. 下载 `Files.zip`。 +2. 校验 `Files.zip`。 +3. 解压或准备完整包 staging。 +4. 交给安装程序执行完整包安装。 + +如果完整包也失败,PLONDS 服务返回失败结果,由 UI 展示错误和重试入口。 + +## 6. 发布产物布局 + +Publisher 上传到 S3 的版本目录: + +```text +//PLONDS.json +//changed.zip +//-changed/** +//Files.zip +//-Files/** +``` + +说明: + +- `Files.zip` 是上传到 S3 时的完整包标准名。 +- `-Files/` 是 S3 上解压后的完整包目录。 +- GitHub Release 仍可保留平台原始文件名,例如 `files-windows-x64.zip`。 +- `PLONDS.json` 的 downloads 字段同时包含 GitHub 与 S3 的增量包、完整包位置。 + +## 7. 建议代码结构 + +```text +LanMountainDesktop/Services/Plonds/ + IPlondsService.cs + PlondsService.cs + Sources/ + IPlondsSource.cs + PlondsHttpManifestSource.cs + PlondsSourceRegistry.cs + Download/ + PlondsDownloader.cs + PlondsDownloadPlanner.cs + Verification/ + PlondsVerifier.cs + Staging/ + PlondsPackageStore.cs + PlondsPreparedPackage.cs + Models/ + PlondsClientManifest.cs + PlondsSourceDescriptor.cs + PlondsCheckResult.cs +``` + +后续如果要移植,优先把这棵目录或等价项目抽成独立库。 + +## 8. 与安装程序的交接契约 + +PLONDS 服务输出本地 prepared package: + +```csharp +public sealed record PlondsPreparedPackage( + Version Version, + PlondsPackageMode Mode, + string ManifestPath, + string? ChangedZipPath, + string? ChangedDirectory, + string? FilesZipPath, + string? FilesDirectory); +``` + +安装程序只接受这个结果,不参与 source 发现、下载和校验。 + +## 9. 实施顺序 + +1. Publisher 补齐完整包 S3 上传与 manifest downloads 字段。 +2. 新增 `Services/Plonds/` 客户端服务骨架和模型。 +3. 把 `PlondsStaticUpdateService` / `PlondsReleaseUpdateService` 合并迁移到独立 PLONDS source 体系。 +4. 把 `LanMountainDesktop/Services/Update/Plonds*.cs` 迁出 Update 命名空间。 +5. `UpdateSettingsService` 改为调用 `IPlondsService`,不再直接组合 S3/GitHub PLONDS fallback。 +6. 安装入口只接收 `PlondsPreparedPackage`。 +7. 添加单元测试覆盖 source 扩展、最高版本选择、增量失败转完整包、完整包失败交 UI。 diff --git a/.trae/specs/plonds-comparator-redesign/spec.md b/.trae/specs/plonds-comparator-redesign/spec.md index 10f9273..e5d7d5d 100644 --- a/.trae/specs/plonds-comparator-redesign/spec.md +++ b/.trae/specs/plonds-comparator-redesign/spec.md @@ -319,13 +319,18 @@ jobs: //PLONDS.json //changed.zip //-changed/** + //Files.zip + //-Files/** → 回写 PLONDS.json downloads 字段: downloads.github.releaseUrl downloads.github.manifestUrl downloads.github.changedZipUrl + downloads.github.filesZipUrl downloads.s3.manifestUrl downloads.s3.changedZipUrl downloads.s3.changedFolderUrl + downloads.s3.filesZipUrl + downloads.s3.filesFolderUrl - 将回写后的 PLONDS.json 重新上传到 GitHub Release ``` diff --git a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishOptions.cs b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishOptions.cs index 6630e23..938106f 100644 --- a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishOptions.cs +++ b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishOptions.cs @@ -5,6 +5,7 @@ public sealed record PlondsPublishOptions( string Repository, string ManifestPath, string ChangedZipPath, + string FilesZipPath, string WorkDir, string S3KeyPrefix, PlondsS3ClientOptions S3); diff --git a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishResult.cs b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishResult.cs index 7ca5fcf..cc67e04 100644 --- a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishResult.cs +++ b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublishResult.cs @@ -10,4 +10,9 @@ public sealed record PlondsPublishResult( string ChangedZipUrl, string ChangedFolderKey, string ChangedFolderUrl, - int ChangedFileCount); + string FilesZipKey, + string FilesZipUrl, + string FilesFolderKey, + string FilesFolderUrl, + int ChangedFileCount, + int FilesFileCount); diff --git a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublisher.cs b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublisher.cs index c3bcff4..37c680f 100644 --- a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublisher.cs +++ b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Core/Publishing/PlondsPublisher.cs @@ -21,12 +21,15 @@ public sealed class PlondsPublisher var repository = Require(options.Repository, nameof(options.Repository)); var manifestPath = Path.GetFullPath(Require(options.ManifestPath, nameof(options.ManifestPath))); var changedZipPath = Path.GetFullPath(Require(options.ChangedZipPath, nameof(options.ChangedZipPath))); + var filesZipPath = Path.GetFullPath(Require(options.FilesZipPath, nameof(options.FilesZipPath))); var workDir = Path.GetFullPath(Require(options.WorkDir, nameof(options.WorkDir))); var version = releaseTag.TrimStart('v', 'V'); var prefix = NormalizePrefix(options.S3KeyPrefix); var versionPrefix = $"{prefix}/{version}"; var changedFolderName = $"{version}-changed"; + var filesFolderName = $"{version}-Files"; var changedExtractRoot = Path.Combine(workDir, changedFolderName); + var filesExtractRoot = Path.Combine(workDir, filesFolderName); if (!File.Exists(manifestPath)) { @@ -38,26 +41,30 @@ public sealed class PlondsPublisher throw new FileNotFoundException("PLONDS changed.zip not found.", changedZipPath); } + if (!File.Exists(filesZipPath)) + { + throw new FileNotFoundException("PLONDS files zip not found.", filesZipPath); + } + var manifest = LoadManifest(manifestPath); PayloadUtilities.EnsureCleanDirectory(changedExtractRoot); ZipFile.ExtractToDirectory(changedZipPath, changedExtractRoot, overwriteFiles: true); + PayloadUtilities.EnsureCleanDirectory(filesExtractRoot); + ZipFile.ExtractToDirectory(filesZipPath, filesExtractRoot, overwriteFiles: true); var manifestKey = $"{versionPrefix}/PLONDS.json"; var changedZipKey = $"{versionPrefix}/changed.zip"; var changedFolderKey = $"{versionPrefix}/{changedFolderName}"; + var filesZipKey = $"{versionPrefix}/Files.zip"; + var filesFolderKey = $"{versionPrefix}/{filesFolderName}"; using var s3 = new PlondsS3Client(options.S3); - var changedFileCount = 0; - foreach (var filePath in Directory.EnumerateFiles(changedExtractRoot, "*", SearchOption.AllDirectories).OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) - { - var relativePath = PayloadUtilities.NormalizeRelativePath(Path.GetRelativePath(changedExtractRoot, filePath)); - var objectKey = $"{changedFolderKey}/{relativePath}"; - await s3.UploadFileAsync(new PlondsS3ObjectUpload(filePath, objectKey, ResolveContentType(filePath)), cancellationToken).ConfigureAwait(false); - changedFileCount++; - } + var changedFileCount = await UploadDirectoryAsync(s3, changedExtractRoot, changedFolderKey, cancellationToken).ConfigureAwait(false); + var filesFileCount = await UploadDirectoryAsync(s3, filesExtractRoot, filesFolderKey, cancellationToken).ConfigureAwait(false); await s3.UploadFileAsync(new PlondsS3ObjectUpload(changedZipPath, changedZipKey, "application/zip"), cancellationToken).ConfigureAwait(false); + await s3.UploadFileAsync(new PlondsS3ObjectUpload(filesZipPath, filesZipKey, "application/zip"), cancellationToken).ConfigureAwait(false); var updatedManifest = manifest with { @@ -66,7 +73,8 @@ public sealed class PlondsPublisher GitHub: new PlondsGitHubDownloadInfo( ReleaseUrl: $"https://github.com/{repository}/releases/tag/{releaseTag}", ManifestUrl: $"https://github.com/{repository}/releases/download/{releaseTag}/PLONDS.json", - ChangedZipUrl: $"https://github.com/{repository}/releases/download/{releaseTag}/changed.zip"), + ChangedZipUrl: $"https://github.com/{repository}/releases/download/{releaseTag}/changed.zip", + FilesZipUrl: $"https://github.com/{repository}/releases/download/{releaseTag}/{Path.GetFileName(filesZipPath)}"), S3: new PlondsS3DownloadInfo( Bucket: options.S3.Bucket, Prefix: versionPrefix, @@ -75,7 +83,11 @@ public sealed class PlondsPublisher ChangedZipKey: changedZipKey, ChangedZipUrl: s3.BuildPublicUrl(changedZipKey), ChangedFolderKey: changedFolderKey, - ChangedFolderUrl: s3.BuildPublicUrl(changedFolderKey))) + ChangedFolderUrl: s3.BuildPublicUrl(changedFolderKey), + FilesZipKey: filesZipKey, + FilesZipUrl: s3.BuildPublicUrl(filesZipKey), + FilesFolderKey: filesFolderKey, + FilesFolderUrl: s3.BuildPublicUrl(filesFolderKey))) }; File.WriteAllText(manifestPath, JsonSerializer.Serialize(updatedManifest, JsonOptions), new UTF8Encoding(false)); @@ -83,6 +95,7 @@ public sealed class PlondsPublisher await s3.EnsureObjectExistsAsync(manifestKey, cancellationToken).ConfigureAwait(false); await s3.EnsureObjectExistsAsync(changedZipKey, cancellationToken).ConfigureAwait(false); + await s3.EnsureObjectExistsAsync(filesZipKey, cancellationToken).ConfigureAwait(false); return new PlondsPublishResult( ReleaseTag: releaseTag, @@ -94,7 +107,30 @@ public sealed class PlondsPublisher ChangedZipUrl: s3.BuildPublicUrl(changedZipKey), ChangedFolderKey: changedFolderKey, ChangedFolderUrl: s3.BuildPublicUrl(changedFolderKey), - ChangedFileCount: changedFileCount); + FilesZipKey: filesZipKey, + FilesZipUrl: s3.BuildPublicUrl(filesZipKey), + FilesFolderKey: filesFolderKey, + FilesFolderUrl: s3.BuildPublicUrl(filesFolderKey), + ChangedFileCount: changedFileCount, + FilesFileCount: filesFileCount); + } + + private static async Task UploadDirectoryAsync( + PlondsS3Client s3, + string sourceDirectory, + string destinationKeyPrefix, + CancellationToken cancellationToken) + { + var count = 0; + foreach (var filePath in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories).OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + var relativePath = PayloadUtilities.NormalizeRelativePath(Path.GetRelativePath(sourceDirectory, filePath)); + var objectKey = $"{destinationKeyPrefix}/{relativePath}"; + await s3.UploadFileAsync(new PlondsS3ObjectUpload(filePath, objectKey, ResolveContentType(filePath)), cancellationToken).ConfigureAwait(false); + count++; + } + + return count; } private static PlondsManifest LoadManifest(string manifestPath) diff --git a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Shared/Models/PlondsDownloadInfo.cs b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Shared/Models/PlondsDownloadInfo.cs index c017f5b..d50a533 100644 --- a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Shared/Models/PlondsDownloadInfo.cs +++ b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Shared/Models/PlondsDownloadInfo.cs @@ -11,7 +11,8 @@ public sealed record PlondsDownloadInfo( public sealed record PlondsGitHubDownloadInfo( string ReleaseUrl, string ManifestUrl, - string ChangedZipUrl); + string ChangedZipUrl, + string FilesZipUrl); public sealed record PlondsS3DownloadInfo( string Bucket, @@ -21,4 +22,8 @@ public sealed record PlondsS3DownloadInfo( string ChangedZipKey, string ChangedZipUrl, string ChangedFolderKey, - string ChangedFolderUrl); + string ChangedFolderUrl, + string FilesZipKey, + string FilesZipUrl, + string FilesFolderKey, + string FilesFolderUrl); diff --git a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Tool/Program.cs b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Tool/Program.cs index 3667b59..f9cf3ab 100644 --- a/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Tool/Program.cs +++ b/PenguinLogisticsOnlineNetworkDistributionSystem/src/Plonds.Tool/Program.cs @@ -103,6 +103,7 @@ internal static class PlondsCli Repository: Require(options, "repository"), ManifestPath: Require(options, "manifest"), ChangedZipPath: Require(options, "changed-zip"), + FilesZipPath: Require(options, "files-zip"), WorkDir: Get(options, "work-dir", "plonds-publish-work") ?? "plonds-publish-work", S3KeyPrefix: Get(options, "s3-prefix", "lanmountain/update/plonds") ?? "lanmountain/update/plonds", S3: new PlondsS3ClientOptions( @@ -119,7 +120,10 @@ internal static class PlondsCli Console.WriteLine($" Manifest: {result.ManifestUrl}"); Console.WriteLine($" ChangedZip: {result.ChangedZipUrl}"); Console.WriteLine($" ChangedFolder: {result.ChangedFolderUrl}"); + Console.WriteLine($" FilesZip: {result.FilesZipUrl}"); + Console.WriteLine($" FilesFolder: {result.FilesFolderUrl}"); Console.WriteLine($" ChangedFileCount: {result.ChangedFileCount}"); + Console.WriteLine($" FilesFileCount: {result.FilesFileCount}"); return 0; } @@ -199,6 +203,7 @@ internal static class PlondsCli Console.WriteLine(" --repository GitHub repository"); Console.WriteLine(" --manifest PLONDS.json path"); Console.WriteLine(" --changed-zip changed.zip path"); + Console.WriteLine(" --files-zip Full files zip path"); Console.WriteLine(" --s3-endpoint S3-compatible endpoint"); Console.WriteLine(" --s3-region S3 signing region"); Console.WriteLine(" --s3-bucket S3 bucket");