From eac1edd455a0db2e2ac34444e235ed3816567afe Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Fri, 17 Jul 2026 10:20:08 -0700 Subject: [PATCH] =?UTF-8?q?feat(official-sync):=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E4=B8=8E=20server-info/marker=20=E6=8B=89?= =?UTF-8?q?=E5=8F=96=E6=8E=A5=E5=85=A5=E9=94=99=E8=AF=AF=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit catalog.refresh 真机 e2e 暴露的缺口:任务配置校验失败落 900001,而 输入域码表(100xxx)早已定义却无使用点。本次接入: - 配置校验四处类型化(经 anyhow::Error::new 保留 DownloadError, task worker 现有 downcast 直接归类): 缺少应用版本 → MISSING_APP_VERSION(100001)、 缺少连接组 → MISSING_CONNECTION_GROUP(100002)、 缺少服务器信息来源 → MISSING_SERVER_INFO_SOURCE(100003)、 GameMainConfig 缺 ServerInfoDataUrl → LAUNCHER_RESPONSE_INVALID (600004,launcher 链内容缺陷)。 - fetch_bytes 改返回 DownloadError:非官方 URL → NON_OFFICIAL_URL、 curl 失败保留准确网络域码。server-info / endpoint marker / 种子 catalog 拉取(5 处调用)由 anyhow::Error::msg 改 Error::new 保留 类型——发现阶段的网络失败此前全部落 internal,现携带 3xx 网络码。 测试:3 个配置校验错误码 downcast 断言 + fetch_bytes 双拒绝映射 (非官方 URL / 404)。真机 e2e:无 auto-discover 的 daemon 上 catalog.refresh → task.status 报 BAT-ERR-100001 missing_app_version (domain=input),修复前为 900001。全量 fmt / clippy --workspace --all-targets -D warnings / test --workspace 全绿。 Co-Authored-By: Claude Fable 5 --- infrastructure/src/official_download.rs | 42 ++++++++++- infrastructure/src/official_update.rs | 98 +++++++++++++++++++++---- 2 files changed, 122 insertions(+), 18 deletions(-) diff --git a/infrastructure/src/official_download.rs b/infrastructure/src/official_download.rs index 6fc0528..fc1feaa 100644 --- a/infrastructure/src/official_download.rs +++ b/infrastructure/src/official_download.rs @@ -756,9 +756,12 @@ impl OfficialResourcePullService { /// This is intended for small discovery inputs such as `server-info`, /// `TableCatalog.bytes`, `BundlePackingInfo.bytes`, and /// `MediaCatalog.bytes`. - pub fn fetch_bytes(&self, url: &str) -> Result, String> { + pub fn fetch_bytes(&self, url: &str) -> Result, DownloadError> { if !is_official_yostar_jp_url(url) { - return Err(format!("拒绝拉取非官方 URL:{url}")); + return Err(DownloadError::new( + bat_core::ErrorCode::NON_OFFICIAL_URL, + format!("拒绝拉取非官方 URL:{url}"), + )); } let output = run_curl_with_retry_with_proxy( @@ -779,7 +782,13 @@ impl OfficialResourcePullService { command }, ) - .map_err(|error| format!("curl 拉取失败 {url}:{error}"))?; + .map_err(|error| { + // 保留 curl 失败的准确网络域码;进程类失败归 INTERNAL。 + let code = error + .final_error_code() + .unwrap_or(bat_core::ErrorCode::INTERNAL); + DownloadError::new(code, format!("curl 拉取失败 {url}:{error}")) + })?; Ok(output.stdout) } @@ -1829,6 +1838,33 @@ mod tests { use std::fs; use tempfile::TempDir; + #[test] + fn fetch_bytes_maps_rejections_to_error_codes() { + // 非官方 URL:安全边界拒绝。 + let service = OfficialResourcePullService::new("/tmp/unused"); + let error = service + .fetch_bytes("https://evil.example/server_info.json") + .unwrap_err(); + assert_eq!(error.code(), bat_core::ErrorCode::NON_OFFICIAL_URL); + + // curl 404:保留网络域码。 + let out_dir = TempDir::new().unwrap(); + let bin_dir = TempDir::new().unwrap(); + let curl_path = bin_dir.path().join("curl"); + write_shell_script( + &curl_path, + r#"#!/bin/sh +printf 'curl: (22) The requested URL returned error: 404\n' >&2 +exit 22 +"#, + ); + let service = OfficialResourcePullService::with_curl_command(out_dir.path(), &curl_path); + let error = service + .fetch_bytes("https://prod-clientpatch.bluearchiveyostar.com/r93_token/missing.bytes") + .unwrap_err(); + assert_eq!(error.code(), bat_core::ErrorCode::HTTP_NOT_FOUND); + } + #[test] fn read_download_manifest_at_handles_missing_and_bad_version() { let temp = TempDir::new().unwrap(); diff --git a/infrastructure/src/official_update.rs b/infrastructure/src/official_update.rs index 3f95843..8d5ec34 100644 --- a/infrastructure/src/official_update.rs +++ b/infrastructure/src/official_update.rs @@ -12,10 +12,11 @@ use crate::path_security::{ }; use crate::{ build_official_pull_plan_for_platform_inventory, build_official_sync_plan, - changed_endpoint_urls, default_official_platforms, OfficialGameMainConfigBootstrapService, - OfficialLauncherBootstrapService, OfficialResourcePullPlan, OfficialResourcePullProgress, - OfficialResourcePullProgressKind, OfficialResourcePullService, YostarJpLauncherGameConfig, - YostarJpLauncherManifestUrl, YostarJpLauncherRemoteManifest, + changed_endpoint_urls, default_official_platforms, DownloadError, + OfficialGameMainConfigBootstrapService, OfficialLauncherBootstrapService, + OfficialResourcePullPlan, OfficialResourcePullProgress, OfficialResourcePullProgressKind, + OfficialResourcePullService, YostarJpLauncherGameConfig, YostarJpLauncherManifestUrl, + YostarJpLauncherRemoteManifest, }; use bat_adapters::official::game_main_config::YostarJpGameMainConfig; use bat_adapters::official::inventory::{ @@ -25,6 +26,7 @@ use bat_adapters::official::yostar_jp::{ server_info_url, PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceEndpoint, YostarJpResourceEndpointKind, YostarJpServerInfo, YostarJpSyncSnapshot, }; +use bat_core::ErrorCode; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::{self, OpenOptions}; @@ -939,7 +941,10 @@ impl OfficialUpdateService { .map(|bootstrap| bootstrap.launcher_metadata.game_latest_version.clone()) }) .ok_or_else(|| { - anyhow::anyhow!("缺少应用版本;请显式传入 --app-version 或启用 --auto-discover") + anyhow::Error::new(DownloadError::new( + ErrorCode::MISSING_APP_VERSION, + "缺少应用版本;请显式传入 --app-version 或启用 --auto-discover", + )) })?; let connection_group = config .connection_group @@ -950,7 +955,10 @@ impl OfficialUpdateService { }) }) .ok_or_else(|| { - anyhow::anyhow!("缺少连接组;请显式传入 --connection-group 或启用 --auto-discover") + anyhow::Error::new(DownloadError::new( + ErrorCode::MISSING_CONNECTION_GROUP, + "缺少连接组;请显式传入 --connection-group 或启用 --auto-discover", + )) })?; progress(OfficialUpdateProgress::new( "metadata", @@ -970,20 +978,27 @@ impl OfficialUpdateService { load_server_info(source, &fetcher)? } else { let bootstrap = bootstrap.as_ref().ok_or_else(|| { - anyhow::anyhow!( - "缺少服务器信息来源;请显式传入 --server-info-* 或启用 --auto-discover" - ) + anyhow::Error::new(DownloadError::new( + ErrorCode::MISSING_SERVER_INFO_SOURCE, + "缺少服务器信息来源;请显式传入 --server-info-* 或启用 --auto-discover", + )) })?; let url = bootstrap .game_main_config .server_info_data_url .as_deref() - .ok_or_else(|| anyhow::anyhow!("官方 GameMainConfig 中没有 ServerInfoDataUrl"))?; + .ok_or_else(|| { + // 官方 GameMainConfig 缺少必需字段:launcher 链内容缺陷。 + anyhow::Error::new(DownloadError::new( + ErrorCode::LAUNCHER_RESPONSE_INVALID, + "官方 GameMainConfig 中没有 ServerInfoDataUrl", + )) + })?; progress(OfficialUpdateProgress::new( "server-info", format!("拉取自动发现的服务器信息 {url}"), )); - fetcher.fetch_bytes(url).map_err(anyhow::Error::msg)? + fetcher.fetch_bytes(url).map_err(anyhow::Error::new)? }; progress(OfficialUpdateProgress::new( @@ -1515,10 +1530,10 @@ fn load_server_info( OfficialServerInfoSource::LocalPath(path) => Ok(fs::read(path)?), OfficialServerInfoSource::OfficialFile(file_name) => { let url = server_info_url(file_name).map_err(anyhow::Error::msg)?; - fetcher.fetch_bytes(&url).map_err(anyhow::Error::msg) + fetcher.fetch_bytes(&url).map_err(anyhow::Error::new) } OfficialServerInfoSource::OfficialUrl(url) => { - fetcher.fetch_bytes(url).map_err(anyhow::Error::msg) + fetcher.fetch_bytes(url).map_err(anyhow::Error::new) } } } @@ -1547,7 +1562,7 @@ fn collect_endpoint_markers( )); let bytes = fetcher .fetch_bytes(&endpoint.url) - .map_err(anyhow::Error::msg)?; + .map_err(anyhow::Error::new)?; let value = String::from_utf8_lossy(&bytes).trim().to_string(); markers.push(OfficialEndpointMarkerSnapshot { kind: endpoint.kind, @@ -2594,7 +2609,7 @@ fn fetch_seed_catalogs( )); let bytes = fetcher .fetch_bytes(&endpoint.url) - .map_err(anyhow::Error::msg)?; + .map_err(anyhow::Error::new)?; catalogs.insert(endpoint, bytes)?; } @@ -2816,6 +2831,59 @@ fn process_exists(_pid: u32) -> bool { mod tests { use super::*; + #[test] + fn missing_app_version_carries_input_error_code() { + // 未启用 auto-discover 且未传 app-version:配置校验失败应携带 + // 输入域错误码(100001),供任务层 downcast 归类。 + let temp = tempfile::TempDir::new().unwrap(); + let config = OfficialUpdateConfig { + output_root: temp.path().join("out"), + dry_run: true, + plan: true, + ..OfficialUpdateConfig::default() + }; + let error = OfficialUpdateService::new().run(&config).unwrap_err(); + let coded = error + .downcast_ref::() + .expect("配置校验错误应为类型化 DownloadError"); + assert_eq!(coded.code(), ErrorCode::MISSING_APP_VERSION); + } + + #[test] + fn missing_connection_group_carries_input_error_code() { + let temp = tempfile::TempDir::new().unwrap(); + let config = OfficialUpdateConfig { + output_root: temp.path().join("out"), + app_version: Some("1.70.0".to_string()), + dry_run: true, + plan: true, + ..OfficialUpdateConfig::default() + }; + let error = OfficialUpdateService::new().run(&config).unwrap_err(); + let coded = error + .downcast_ref::() + .expect("配置校验错误应为类型化 DownloadError"); + assert_eq!(coded.code(), ErrorCode::MISSING_CONNECTION_GROUP); + } + + #[test] + fn missing_server_info_source_carries_input_error_code() { + let temp = tempfile::TempDir::new().unwrap(); + let config = OfficialUpdateConfig { + output_root: temp.path().join("out"), + app_version: Some("1.70.0".to_string()), + connection_group: Some("Prod-Audit".to_string()), + dry_run: true, + plan: true, + ..OfficialUpdateConfig::default() + }; + let error = OfficialUpdateService::new().run(&config).unwrap_err(); + let coded = error + .downcast_ref::() + .expect("配置校验错误应为类型化 DownloadError"); + assert_eq!(coded.code(), ErrorCode::MISSING_SERVER_INFO_SOURCE); + } + fn fixture_base_snapshot() -> YostarJpSyncSnapshot { YostarJpSyncSnapshot { connection_group_name: "Prod-Audit".to_string(),