From be17c0c8ee38ca21d82e302fcbfd8fb51fe0d5dd Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 30 Oct 2026 16:15:56 +0800 Subject: [PATCH] Cache only approved delivery assets without reconciling existing article mappings --- .../Commands/SyncIllustrationDeliveries.php | 25 +++++++++++++++++-- app/Services/LocalIllustrations.php | 12 +++++++++ docs/maintenance/ILLUSTRATION_DELIVERY.md | 2 +- tests/Feature/IllustrationDeliveryTest.php | 5 ++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/Console/Commands/SyncIllustrationDeliveries.php b/app/Console/Commands/SyncIllustrationDeliveries.php index 2b495dc5..441a1e39 100644 --- a/app/Console/Commands/SyncIllustrationDeliveries.php +++ b/app/Console/Commands/SyncIllustrationDeliveries.php @@ -39,9 +39,30 @@ public function handle(): int return self::SUCCESS; } - if ($this->call('illustrations:sync-local') !== 0) { - throw new \RuntimeException('sync_failed'); + $snapshots = []; + $images = []; + foreach ($pending as $batch) { + foreach ($batch['assets'] ?? [] as $asset) { + $pool = $asset['pool'] ?? ''; + if (! in_array($pool, config('illustrations.categories'), true)) { + throw new \RuntimeException('invalid_pool'); + } + if (! isset($snapshots[$pool])) { + $snapshots[$pool] = IllustrationPool::sourceSnapshot($pool); + } + $match = null; + foreach ($snapshots[$pool]['items'] as $image) { + if (($image['asset_id'] ?? '') === ($asset['asset_id'] ?? null)) { + $match = $image; + } + } + if (! $match) { + throw new \RuntimeException('delivery_asset_unavailable'); + } + $images[$asset['asset_id']] = $match; + } } + app(LocalIllustrations::class)->cacheDeliveryAssets(array_values($images)); $bytes = file_get_contents(config('illustrations.local_manifest')); $manifest = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR); foreach ($pending as $batch) { diff --git a/app/Services/LocalIllustrations.php b/app/Services/LocalIllustrations.php index 22bcee6e..8fe34c40 100644 --- a/app/Services/LocalIllustrations.php +++ b/app/Services/LocalIllustrations.php @@ -115,6 +115,18 @@ private function copy(array $image, array $assets): array 'url' => config('illustrations.local_url_prefix').'/'.$sha.'.'.$ext, 'source_url' => $source]; } + /** Delivery intake copies only approved batch assets; it does not reconcile existing articles. */ + public function cacheDeliveryAssets(array $images): void + { + $this->locked(function () use ($images) { + $state = $this->read(); + foreach ($images as $image) { + $state['assets'][$image['asset_id']] = $this->copy($image, $state['assets']); + } + $this->save($state); + }); + } + public function storeSelection(string $pool, string $slug, array $image): string { return $this->locked(function () use ($pool, $slug, $image) { diff --git a/docs/maintenance/ILLUSTRATION_DELIVERY.md b/docs/maintenance/ILLUSTRATION_DELIVERY.md index b32744d9..accfe426 100644 --- a/docs/maintenance/ILLUSTRATION_DELIVERY.md +++ b/docs/maintenance/ILLUSTRATION_DELIVERY.md @@ -2,7 +2,7 @@ # 插画交付接收 协议与画师操作唯一正文:[Resource交付说明](/Users/hellcat/Documents/GemCodes/resource.loc/v2/docs/ILLUSTRATION_DELIVERY.md)。 -本项目新增 `illustrations:sync-deliveries`,开关 `ILLUSTRATION_DELIVERY_ENABLED=false` 默认关闭。VPS-003正式目录运行fna-illustration-delivery.timer(nginx,每分钟)。复用已有消费者凭据和 `illustrations:sync-local`:先同步并验证所有交付资产已在清单/静态目录中,哈希一致才ACK;失败下轮重试,不重分配既有文章、不删除图片。没有新批次时只查交付队列,不下载图池。页面请求不调用该任务。 +本项目新增 `illustrations:sync-deliveries`,开关 `ILLUSTRATION_DELIVERY_ENABLED=false` 默认关闭。VPS-003正式目录运行fna-illustration-delivery.timer(nginx,每分钟)。复用已有消费者凭据和LocalIllustrations下载/原子清单能力:只缓存本批资产,不执行全量旧文章同步;先验证所有交付资产已在清单/静态目录中,哈希一致才ACK;失败下轮重试,不重分配既有文章、不删除图片。没有新批次时只查交付队列,不下载图池。页面请求不调用该任务。 Resource持有唯一批次状态,FNA不复制完整提示词或私有原稿。Resource未完成的同步回执不影响已有静态图片。新增图片只扩充后续文章的候选池。 diff --git a/tests/Feature/IllustrationDeliveryTest.php b/tests/Feature/IllustrationDeliveryTest.php index 364eb06d..7dcb6ae4 100644 --- a/tests/Feature/IllustrationDeliveryTest.php +++ b/tests/Feature/IllustrationDeliveryTest.php @@ -30,7 +30,7 @@ protected function setUp(): void config(['illustrations.enabled' => true, 'illustrations.local_enabled' => true, 'illustrations.delivery_enabled' => true, 'illustrations.base_url' => 'https://resource-v2.gemcook.net', 'illustrations.credential_file' => $this->dir.'/credential.json', 'illustrations.local_directory' => $this->dir.'/public', 'illustrations.local_manifest' => $this->dir.'/manifest.json']); $this->sha = hash('sha256', 'verified-copy'); file_put_contents($this->dir.'/public/'.$this->sha.'.webp', 'verified-copy'); - file_put_contents($this->dir.'/manifest.json', json_encode(['version' => 1, 'assets' => [$this->asset => ['sha256' => $this->sha, 'mime' => 'image/webp']], 'selections' => []])); + file_put_contents($this->dir.'/manifest.json', json_encode(['version' => 1, 'assets' => [$this->asset => ['sha256' => $this->sha, 'mime' => 'image/webp', 'bytes' => strlen('verified-copy'), 'url' => '/images/illustrations/'.$this->sha.'.webp']], 'selections' => ['freenode' => ['old-article' => 'old-asset']]])); Http::preventStrayRequests(); Artisan::all(); \Illuminate\Support\Facades\Schema::create('articles', function ($t) { @@ -52,7 +52,7 @@ private function fake(int $sync = 0, int $ack = 200): void { Http::fake([ - 'https://resource-v2.gemcook.net/api/v2/pools/*' => Http::response(['data' => ['items' => [], 'selections' => [], 'unavailable' => [], 'next' => null]], $sync ? 503 : 200), + 'https://resource-v2.gemcook.net/api/v2/pools/*' => Http::response(['data' => ['items' => [['asset_id' => $this->asset, 'url' => 'https://resource-v2.gemcook.net/media/'.$this->id]], 'selections' => [], 'unavailable' => [], 'next' => null]], $sync ? 503 : 200), 'https://resource-v2.gemcook.net/api/v2/illustration-deliveries' => Http::response(['data' => [['id' => $this->id, 'state' => 'awaiting_sync', 'fingerprint' => str_repeat('b', 64), 'assets' => [['asset_id' => $this->asset, 'sha256' => $this->sha, 'pool' => 'freenode']]]]]), 'https://resource-v2.gemcook.net/api/v2/illustration-deliveries/*/ack' => Http::response(['data' => ['state' => 'completed']], $ack), ]); @@ -66,6 +66,7 @@ public function test_verified_copy_is_acknowledged_and_lost_ack_can_retry(): voi Http::preventStrayRequests(); $this->fake(); $this->artisan('illustrations:sync-deliveries')->assertExitCode(0); + $this->assertSame('old-asset', json_decode(file_get_contents($this->dir.'/manifest.json'), true)['selections']['freenode']['old-article']); Http::assertSent(fn ($r) => str_ends_with($r->url(), '/ack') && $r['fingerprint'] === str_repeat('b', 64) && $r['manifest_sha256'] === hash_file('sha256', $this->dir.'/manifest.json')); }