fix: 完成下载并发与翻译交接链路

This commit is contained in:
2026-08-02 22:34:23 +08:00
parent d533c88108
commit 8b64cc94f3
19 changed files with 2923 additions and 138 deletions
+101 -17
View File
@@ -29,6 +29,9 @@ use crate::path_security::{
read_file_no_symlink, validate_output_root, write_file_atomic, STATE_FILE_MODE,
};
use crate::release_flow::ReleaseFlowStatusCode;
use crate::translation_tasks::{
build_translation_handoff, sync_translation_task_repository_at, write_translation_handoff_at,
};
use crate::{
build_official_pull_plan_for_platform_inventory, build_official_sync_plan,
changed_endpoint_urls, default_official_platforms, DownloadError,
@@ -42,15 +45,17 @@ use crate::{
read_parse_cache_at, OfficialParseCacheService, OfficialParseConfig, OfficialParseSummary,
};
use crate::{FileSystemCasRepository, SqliteResourceRepository};
use crate::{DEFAULT_DOWNLOAD_CONCURRENCY, MAX_DOWNLOAD_CONCURRENCY, MIN_DOWNLOAD_CONCURRENCY};
use bat_adapters::official::game_main_config::YostarJpGameMainConfig;
use bat_adapters::official::inventory::{
YostarJpPlatformCatalogInventory, YostarJpPlatformDownloadInventory,
};
use bat_adapters::official::launcher::YostarJpLauncherManifestFile;
use bat_adapters::official::yostar_jp::{
server_info_url, PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceEndpoint,
PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceEndpoint,
YostarJpResourceEndpointKind, YostarJpServerInfo, YostarJpSyncSnapshot,
};
use bat_adapters::official::{
OfficialResourceBackend, PlatformCatalogInput, YostarJpBackend,
YostarJpPlatformDownloadInventory,
};
use bat_core::ErrorCode;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -115,6 +120,10 @@ pub struct OfficialUpdateConfig {
pub curl_command: PathBuf,
/// Proxy selection used by all official `curl` transfers.
pub curl_proxy: CurlProxyConfig,
/// Maximum number of resource downloads executed concurrently.
///
/// `8` is the default; values are accepted only in `1..=256`.
pub download_concurrency: usize,
/// Unzip command used when a metadata change requires GameMainConfig parsing.
pub unzip_command: PathBuf,
/// Dry run reports decisions and optional plan URLs without writing sync state.
@@ -151,6 +160,7 @@ impl Default for OfficialUpdateConfig {
snapshot_path: None,
curl_command: PathBuf::from("curl"),
curl_proxy: CurlProxyConfig::default(),
download_concurrency: DEFAULT_DOWNLOAD_CONCURRENCY,
unzip_command: PathBuf::from("unzip"),
dry_run: false,
plan: false,
@@ -844,6 +854,8 @@ pub struct OfficialUpdateReport {
pub crowdin_textunit_queue_path: Option<PathBuf>,
/// Incremental TextUnit task queue summary.
pub textunit_task_summary: Option<OfficialTextUnitTaskSummary>,
/// Versioned translation worker handoff view.
pub translation_handoff_path: Option<PathBuf>,
/// Snapshot path written after success.
pub snapshot_written: Option<PathBuf>,
/// Launcher bootstrap artifact written for this run.
@@ -1221,7 +1233,8 @@ impl OfficialUpdateService {
&active_resource_root,
&config.curl_command,
)
.with_proxy_config(config.curl_proxy.clone());
.with_proxy_config(config.curl_proxy.clone())
.with_max_concurrency(config.download_concurrency);
let snapshot_path = snapshot_path_for(config, &active_resource_root);
let bootstrap_cache_path = config.bootstrap_cache_path();
@@ -1668,6 +1681,7 @@ impl OfficialUpdateService {
textunit_task_queue_path: None,
crowdin_textunit_queue_path: None,
textunit_task_summary: None,
translation_handoff_path: None,
snapshot_written: None,
launcher_bootstrap_artifact_path: None,
repository_import_enabled: config.import_repository,
@@ -1846,7 +1860,8 @@ impl OfficialUpdateService {
&publish_plan.staging_path,
&config.curl_command,
)
.with_proxy_config(config.curl_proxy.clone());
.with_proxy_config(config.curl_proxy.clone())
.with_max_concurrency(config.download_concurrency);
let pruned_stale_resource_count = staging_fetcher
.prune_stale_manifest_entries(&pull_plan)
.map_err(anyhow::Error::msg)?;
@@ -2201,6 +2216,7 @@ fn run_post_sync_textunit_queue_if_needed(
&& is_crowdin_textunit_queue_current(resource_root, &queue)
.map_err(anyhow::Error::msg)?
{
sync_translation_task_repository(resource_root, &queue)?;
progress(OfficialUpdateProgress::new(
"textunit",
format!(
@@ -2212,7 +2228,8 @@ fn run_post_sync_textunit_queue_if_needed(
));
report.textunit_task_queue_path = Some(task_queue_path);
report.crowdin_textunit_queue_path = Some(crowdin_queue_path);
report.textunit_task_summary = Some(queue.summary);
report.textunit_task_summary = Some(queue.summary.clone());
write_translation_handoff_for_release(resource_root, &queue, report, progress)?;
return Ok(());
}
progress(OfficialUpdateProgress::new(
@@ -2227,6 +2244,19 @@ fn run_post_sync_textunit_queue_if_needed(
run_post_sync_textunit_queue(resource_root, report, progress, should_cancel)
}
fn sync_translation_task_repository(
resource_root: &Path,
queue: &crate::official_textunit_queue::OfficialTextUnitTaskQueue,
) -> anyhow::Result<()> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime
.block_on(sync_translation_task_repository_at(resource_root, queue))
.map_err(|error| anyhow::anyhow!("{error}"))?;
Ok(())
}
fn run_post_sync_textunit_queue(
resource_root: &Path,
report: &mut OfficialUpdateReport,
@@ -2257,6 +2287,11 @@ fn run_post_sync_textunit_queue(
),
));
apply_textunit_queue_report(report, queue_report);
if let Some(queue) =
read_textunit_task_queue_at(resource_root).map_err(anyhow::Error::msg)?
{
write_translation_handoff_for_release(resource_root, &queue, report, progress)?;
}
}
Err(error) => {
progress(OfficialUpdateProgress::new(
@@ -2277,6 +2312,48 @@ fn apply_textunit_queue_report(
report.textunit_task_summary = Some(queue_report.summary);
}
fn write_translation_handoff_for_release(
resource_root: &Path,
queue: &crate::official_textunit_queue::OfficialTextUnitTaskQueue,
report: &mut OfficialUpdateReport,
progress: &mut dyn FnMut(OfficialUpdateProgress),
) -> anyhow::Result<()> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let tasks = runtime.block_on(async {
let repository = crate::translation_tasks::SqliteTranslationTaskRepository::new(
crate::translation_tasks::SqliteTranslationTaskRepository::repository_path(
resource_root,
),
)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
repository
.sync_queue(queue)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
repository
.list(&crate::official_textunit_queue::OfficialTextUnitTaskQuery::default())
.await
.map_err(|error| anyhow::anyhow!("{error}"))
})?;
let handoff = build_translation_handoff(queue, &tasks);
write_translation_handoff_at(resource_root, &handoff).map_err(anyhow::Error::msg)?;
let path = resource_root.join(crate::translation_tasks::TRANSLATION_HANDOFF_FILE);
report.translation_handoff_path = Some(path.clone());
progress(OfficialUpdateProgress::new(
"textunit",
format!(
"翻译 handoff 已发布:任务={} provider_run={} {}",
handoff.units.len(),
handoff.provider_runs.len(),
path.display()
),
));
Ok(())
}
fn run_post_sync_repository_import(
config: &OfficialUpdateConfig,
resource_root: &Path,
@@ -2370,8 +2447,9 @@ fn build_pull_plan(
should_cancel: &mut dyn FnMut() -> bool,
) -> anyhow::Result<OfficialResourcePullPlan> {
check_shutdown_requested(should_cancel)?;
let discovery = server_info
.discovery_plan(connection_group, app_version, platforms)
let backend = YostarJpBackend;
let discovery = backend
.discovery_plan(server_info, connection_group, app_version, platforms)
.map_err(anyhow::Error::msg)?;
progress(OfficialUpdateProgress::new(
"plan",
@@ -2402,7 +2480,9 @@ fn load_server_info(
match source {
OfficialServerInfoSource::LocalPath(path) => Ok(fs::read(path)?),
OfficialServerInfoSource::OfficialFile(file_name) => {
let url = server_info_url(file_name).map_err(anyhow::Error::msg)?;
let url = YostarJpBackend
.server_info_url(file_name)
.map_err(anyhow::Error::msg)?;
fetcher.fetch_bytes(&url).map_err(anyhow::Error::new)
}
OfficialServerInfoSource::OfficialUrl(url) => {
@@ -2489,6 +2569,7 @@ fn waiting_for_official_resources_report(
textunit_task_queue_path: None,
crowdin_textunit_queue_path: None,
textunit_task_summary: None,
translation_handoff_path: None,
snapshot_written: None,
launcher_bootstrap_artifact_path: None,
repository_import_enabled: config.import_repository,
@@ -2894,6 +2975,12 @@ pub fn diff_extended_snapshot(
}
fn validate_update_paths(config: &OfficialUpdateConfig) -> Result<(), String> {
if !(MIN_DOWNLOAD_CONCURRENCY..=MAX_DOWNLOAD_CONCURRENCY).contains(&config.download_concurrency)
{
return Err(format!(
"下载并发数必须在 {MIN_DOWNLOAD_CONCURRENCY}..={MAX_DOWNLOAD_CONCURRENCY} 范围内"
));
}
validate_output_root(&config.output_root)?;
validate_output_root(&config.localized_output_root)?;
validate_separate_output_roots(&config.output_root, &config.localized_output_root)?;
@@ -4047,17 +4134,14 @@ fn build_inventory_from_seed_catalogs(
anyhow::anyhow!("官方发现结果缺少 {} MediaCatalog.bytes", platform.as_str())
})?;
platform_catalogs.push(YostarJpPlatformCatalogInventory::from_catalog_bytes(
*platform,
platform_catalogs.push(PlatformCatalogInput {
platform: *platform,
bundle_packing_info,
media_catalog,
));
});
}
Ok(YostarJpPlatformDownloadInventory::from_catalog_bytes(
table_catalog,
platform_catalogs,
))
Ok(YostarJpBackend.inventory(table_catalog, &platform_catalogs))
}
#[derive(Debug, Default)]