mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:15:14 +08:00
feat(official-sync): 配置校验与 server-info/marker 拉取接入错误码
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 <noreply@anthropic.com>
This commit is contained in:
@@ -756,9 +756,12 @@ impl OfficialResourcePullService {
|
|||||||
/// This is intended for small discovery inputs such as `server-info`,
|
/// This is intended for small discovery inputs such as `server-info`,
|
||||||
/// `TableCatalog.bytes`, `BundlePackingInfo.bytes`, and
|
/// `TableCatalog.bytes`, `BundlePackingInfo.bytes`, and
|
||||||
/// `MediaCatalog.bytes`.
|
/// `MediaCatalog.bytes`.
|
||||||
pub fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
|
pub fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, DownloadError> {
|
||||||
if !is_official_yostar_jp_url(url) {
|
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(
|
let output = run_curl_with_retry_with_proxy(
|
||||||
@@ -779,7 +782,13 @@ impl OfficialResourcePullService {
|
|||||||
command
|
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)
|
Ok(output.stdout)
|
||||||
}
|
}
|
||||||
@@ -1829,6 +1838,33 @@ mod tests {
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
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]
|
#[test]
|
||||||
fn read_download_manifest_at_handles_missing_and_bad_version() {
|
fn read_download_manifest_at_handles_missing_and_bad_version() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ use crate::path_security::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
build_official_pull_plan_for_platform_inventory, build_official_sync_plan,
|
build_official_pull_plan_for_platform_inventory, build_official_sync_plan,
|
||||||
changed_endpoint_urls, default_official_platforms, OfficialGameMainConfigBootstrapService,
|
changed_endpoint_urls, default_official_platforms, DownloadError,
|
||||||
OfficialLauncherBootstrapService, OfficialResourcePullPlan, OfficialResourcePullProgress,
|
OfficialGameMainConfigBootstrapService, OfficialLauncherBootstrapService,
|
||||||
OfficialResourcePullProgressKind, OfficialResourcePullService, YostarJpLauncherGameConfig,
|
OfficialResourcePullPlan, OfficialResourcePullProgress, OfficialResourcePullProgressKind,
|
||||||
YostarJpLauncherManifestUrl, YostarJpLauncherRemoteManifest,
|
OfficialResourcePullService, YostarJpLauncherGameConfig, YostarJpLauncherManifestUrl,
|
||||||
|
YostarJpLauncherRemoteManifest,
|
||||||
};
|
};
|
||||||
use bat_adapters::official::game_main_config::YostarJpGameMainConfig;
|
use bat_adapters::official::game_main_config::YostarJpGameMainConfig;
|
||||||
use bat_adapters::official::inventory::{
|
use bat_adapters::official::inventory::{
|
||||||
@@ -25,6 +26,7 @@ use bat_adapters::official::yostar_jp::{
|
|||||||
server_info_url, PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceEndpoint,
|
server_info_url, PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceEndpoint,
|
||||||
YostarJpResourceEndpointKind, YostarJpServerInfo, YostarJpSyncSnapshot,
|
YostarJpResourceEndpointKind, YostarJpServerInfo, YostarJpSyncSnapshot,
|
||||||
};
|
};
|
||||||
|
use bat_core::ErrorCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs::{self, OpenOptions};
|
||||||
@@ -939,7 +941,10 @@ impl OfficialUpdateService {
|
|||||||
.map(|bootstrap| bootstrap.launcher_metadata.game_latest_version.clone())
|
.map(|bootstrap| bootstrap.launcher_metadata.game_latest_version.clone())
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.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
|
let connection_group = config
|
||||||
.connection_group
|
.connection_group
|
||||||
@@ -950,7 +955,10 @@ impl OfficialUpdateService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.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(
|
progress(OfficialUpdateProgress::new(
|
||||||
"metadata",
|
"metadata",
|
||||||
@@ -970,20 +978,27 @@ impl OfficialUpdateService {
|
|||||||
load_server_info(source, &fetcher)?
|
load_server_info(source, &fetcher)?
|
||||||
} else {
|
} else {
|
||||||
let bootstrap = bootstrap.as_ref().ok_or_else(|| {
|
let bootstrap = bootstrap.as_ref().ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::Error::new(DownloadError::new(
|
||||||
"缺少服务器信息来源;请显式传入 --server-info-* 或启用 --auto-discover"
|
ErrorCode::MISSING_SERVER_INFO_SOURCE,
|
||||||
)
|
"缺少服务器信息来源;请显式传入 --server-info-* 或启用 --auto-discover",
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
let url = bootstrap
|
let url = bootstrap
|
||||||
.game_main_config
|
.game_main_config
|
||||||
.server_info_data_url
|
.server_info_data_url
|
||||||
.as_deref()
|
.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(
|
progress(OfficialUpdateProgress::new(
|
||||||
"server-info",
|
"server-info",
|
||||||
format!("拉取自动发现的服务器信息 {url}"),
|
format!("拉取自动发现的服务器信息 {url}"),
|
||||||
));
|
));
|
||||||
fetcher.fetch_bytes(url).map_err(anyhow::Error::msg)?
|
fetcher.fetch_bytes(url).map_err(anyhow::Error::new)?
|
||||||
};
|
};
|
||||||
|
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
@@ -1515,10 +1530,10 @@ fn load_server_info(
|
|||||||
OfficialServerInfoSource::LocalPath(path) => Ok(fs::read(path)?),
|
OfficialServerInfoSource::LocalPath(path) => Ok(fs::read(path)?),
|
||||||
OfficialServerInfoSource::OfficialFile(file_name) => {
|
OfficialServerInfoSource::OfficialFile(file_name) => {
|
||||||
let url = server_info_url(file_name).map_err(anyhow::Error::msg)?;
|
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) => {
|
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
|
let bytes = fetcher
|
||||||
.fetch_bytes(&endpoint.url)
|
.fetch_bytes(&endpoint.url)
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::new)?;
|
||||||
let value = String::from_utf8_lossy(&bytes).trim().to_string();
|
let value = String::from_utf8_lossy(&bytes).trim().to_string();
|
||||||
markers.push(OfficialEndpointMarkerSnapshot {
|
markers.push(OfficialEndpointMarkerSnapshot {
|
||||||
kind: endpoint.kind,
|
kind: endpoint.kind,
|
||||||
@@ -2594,7 +2609,7 @@ fn fetch_seed_catalogs(
|
|||||||
));
|
));
|
||||||
let bytes = fetcher
|
let bytes = fetcher
|
||||||
.fetch_bytes(&endpoint.url)
|
.fetch_bytes(&endpoint.url)
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::new)?;
|
||||||
catalogs.insert(endpoint, bytes)?;
|
catalogs.insert(endpoint, bytes)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2816,6 +2831,59 @@ fn process_exists(_pid: u32) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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::<DownloadError>()
|
||||||
|
.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::<DownloadError>()
|
||||||
|
.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::<DownloadError>()
|
||||||
|
.expect("配置校验错误应为类型化 DownloadError");
|
||||||
|
assert_eq!(coded.code(), ErrorCode::MISSING_SERVER_INFO_SOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
fn fixture_base_snapshot() -> YostarJpSyncSnapshot {
|
fn fixture_base_snapshot() -> YostarJpSyncSnapshot {
|
||||||
YostarJpSyncSnapshot {
|
YostarJpSyncSnapshot {
|
||||||
connection_group_name: "Prod-Audit".to_string(),
|
connection_group_name: "Prod-Audit".to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user