feat(assetbundle): 完善官方资源解析与双目录发布

This commit is contained in:
2026-07-25 20:51:01 +08:00
parent 102b49b666
commit 3e9bb20d79
37 changed files with 4663 additions and 1333 deletions
+94 -13
View File
@@ -52,7 +52,7 @@ const BEIJING_UTC_OFFSET_SECONDS: u64 = 8 * 60 * 60;
const DAILY_FORCED_REFRESH_LOCAL_SECONDS: [u64; 3] = [3 * 60 * 60, 16 * 60 * 60, 18 * 60 * 60];
const DAILY_FORCED_REFRESH_LABEL: &str = "UTC+8 03:00, 16:00, 18:00";
const STARTUP_BANNER: &str = r#"
============================================================
=====================================================================================
____ _ _ _ _ _____ _ _ _ _
| __ )| |_ _ ___ / \ _ __ ___| |__ (_)_ _____|_ _|__ ___ | | | _(_) |_
| _ \| | | | |/ _ \/ _ \ | '__/ __| '_ \| \ \ / / _ \ | |/ _ \ / _ \| | |/ / | __|
@@ -61,7 +61,7 @@ const STARTUP_BANNER: &str = r#"
BlueArchiveToolkit
Official Resource Sync
============================================================
=====================================================================================
"#;
fn main() {
@@ -3104,6 +3104,7 @@ fn sync_command_rpc_method(options: &CliOptions, command_name: &str) -> Option<&
&& options.config.launcher_version == defaults.launcher_version
&& options.config.platforms.is_none()
&& options.config.output_root == defaults.output_root
&& options.config.localized_output_root == defaults.localized_output_root
&& options.config.snapshot_path.is_none()
&& options.config.curl_command == defaults.curl_command
&& options.config.curl_proxy == defaults.curl_proxy
@@ -3122,6 +3123,7 @@ fn sync_command_rpc_method(options: &CliOptions, command_name: &str) -> Option<&
}
}
#[cfg(test)]
fn refresh_should_use_daemon_rpc(options: &CliOptions, command_name: &str) -> bool {
sync_command_rpc_method(options, command_name) == Some(RPC_METHOD_REFRESH)
}
@@ -3456,7 +3458,14 @@ impl HumanReport for OfficialUpdateReport {
print_field("本地审计", format_bool(self.audit_local));
print_field("自动修复", format_bool(self.repair));
print_field("dry-run", format_bool(self.dry_run));
print_path_field("输出根目录", &self.output_root);
print_path_field("官方资源目录", &self.output_root);
print_path_field("汉化输出目录", &self.localized_output_root);
print_field("汉化发布状态", self.localized_release_status.as_str());
print_path_field("汉化 current", &self.localized_current_path);
print_optional_path_field(
"汉化 published",
self.localized_published_version_path.as_ref(),
);
print_path_field("active release", &self.active_resource_root);
print_path_field("current", &self.current_path);
print_path_field("version state", &self.version_state_path);
@@ -3464,6 +3473,7 @@ impl HumanReport for OfficialUpdateReport {
print_optional_path_field("published", self.published_version_path.as_ref());
print_path_field("snapshot", &self.snapshot_path);
print_path_field("manifest", &self.download_manifest);
print_optional_path_field("解析缓存", self.parse_cache_path.as_ref());
print_optional_path_field("写入 snapshot", self.snapshot_written.as_ref());
print_optional_path_field("bootstrap cache", self.bootstrap_cache_path.as_ref());
print_optional_field("bootstrap 命中", self.bootstrap_cache_hit.map(format_bool));
@@ -3478,6 +3488,14 @@ impl HumanReport for OfficialUpdateReport {
print_field("需修复", self.local_manifest_repair_needed_count);
print_field("官方 hash 校验", self.official_seed_hash_verified_count);
print_verification_summary(&self.verification_summary);
if let Some(summary) = self.parse_summary.as_ref() {
print_field("解析缓存条目", summary.cache_entry_count);
print_field("解析成功 bundle", summary.parsed_bundle_count);
print_field("解析复用", summary.skipped_unchanged_count);
print_field("解析不支持", summary.unsupported_count);
print_field("解析失败", summary.failed_count);
print_field("TextAsset", summary.text_asset_count);
}
print_field("catalog marker", self.addressables_marker_checked_count);
print_list("变更 endpoint", &self.changed_endpoint_urls, 8);
print_list("计划 URL", &self.download_urls, 8);
@@ -3868,11 +3886,21 @@ fn build_doctor_report(
let mut checks = vec![
path_check("state_dir", state_dir, "后台状态目录可用"),
path_check("output_root", &config.output_root, "资源输出目录可用"),
path_check(
"localized_output_root",
&config.localized_output_root,
"汉化输出目录可用",
),
safety_check(
"output_root_safety",
validate_output_root(&config.output_root),
"资源输出目录安全边界通过",
),
safety_check(
"localized_output_root_safety",
validate_output_root(&config.localized_output_root),
"汉化输出目录安全边界通过",
),
safety_check(
"state_dir_safety",
validate_runtime_state_dir(state_dir),
@@ -4524,6 +4552,8 @@ fn daemon_child_args(options: &CliOptions) -> Vec<String> {
}
args.push("--output".to_string());
args.push(config.output_root.to_string_lossy().to_string());
args.push("--localized-output".to_string());
args.push(config.localized_output_root.to_string_lossy().to_string());
if let Some(snapshot_path) = config.snapshot_path.as_ref() {
args.push("--snapshot".to_string());
args.push(snapshot_path.to_string_lossy().to_string());
@@ -4937,6 +4967,7 @@ fn stage_to_static(stage: &str) -> &'static str {
"download" => "download",
"snapshot" => "snapshot",
"publish" => "publish",
"parse" => "parse",
"finish" => "finish",
"watch" => "watch",
"daemon" => "daemon",
@@ -4968,6 +4999,7 @@ fn localized_stage(stage: &str) -> &str {
"dry-run" => "试运行",
"download" => "下载",
"publish" => "发布",
"parse" => "解析",
"resource" => "资源",
"finish" => "完成",
"watch" => "常驻",
@@ -5003,8 +5035,10 @@ const ENV_TEMPLATE: &str = r#"# BlueArchive Toolkit 配置文件(bat 首次启
# BAT_SKIP_ENV_FILE=1 bat
# ---- ----
# ./bat-resources
# ./bat-resources
BAT_OUTPUT=./bat-resources
# ./bat-localized
BAT_LOCALIZED_OUTPUT=./bat-localized
# app-version / connection-group / server-info 1
BAT_AUTO_DISCOVER=1
# bat.sock / / /tmp/bat-pid
@@ -5175,6 +5209,9 @@ fn apply_bat_env_overrides(
if let Some(v) = value("BAT_OUTPUT") {
options.config.output_root = PathBuf::from(v);
}
if let Some(v) = value("BAT_LOCALIZED_OUTPUT") {
options.config.localized_output_root = PathBuf::from(v);
}
if let Some(v) = value("BAT_STATE_DIR") {
options.state_dir = PathBuf::from(v);
}
@@ -5343,6 +5380,11 @@ fn parse_args_with_env(
options.config.output_root = PathBuf::from(next_option_value(&mut args, &flag)?);
options.output_explicit = true;
}
"--localized-output" => {
options.config.localized_output_root =
PathBuf::from(next_option_value(&mut args, &flag)?);
options.output_explicit = true;
}
"--state-dir" | "--pid-dir" => {
options.state_dir = PathBuf::from(next_option_value(&mut args, &flag)?);
}
@@ -5707,7 +5749,10 @@ fn print_usage(binary: &str) {
eprintln!("Sync:");
eprintln!(" --platforms <LIST> Platforms, e.g. Windows,Android");
eprintln!(
" --output <DIR> Resource publish root (default: ./bat-resources)"
" --output <DIR> Official resource publish root (default: ./bat-resources)"
);
eprintln!(
" --localized-output <DIR> Localized output root (default: ./bat-localized)"
);
eprintln!(" --snapshot <PATH> Override snapshot path (default: <output>/current/official-sync-snapshot.json)");
eprintln!(" --curl <PATH> curl executable (default: curl)");
@@ -5739,7 +5784,10 @@ fn print_usage(binary: &str) {
eprintln!();
eprintln!("Defaults:");
eprintln!(" platforms: Windows,Android");
eprintln!(" resource output: ./bat-resources (current -> versions/<id>, .staging/<id>)");
eprintln!(
" official resource output: ./bat-resources (current -> versions/<id>, .staging/<id>)"
);
eprintln!(" localized output: ./bat-localized (separate patch/export target)");
eprintln!(" daemon state: /tmp/bat-pid (bat.sock, bat.pid, bat-status.json, bat-daemon.log, bat-events.jsonl)");
eprintln!(" forced refresh: {DAILY_FORCED_REFRESH_LABEL}");
}
@@ -5820,6 +5868,7 @@ mod tests {
&["bat"],
&[
("BAT_OUTPUT", "/srv/bat"),
("BAT_LOCALIZED_OUTPUT", "/srv/bat-localized"),
("BAT_AUTO_DISCOVER", "1"),
("BAT_STATE_DIR", "/srv/state"),
("BAT_INTERVAL_SECONDS", "120"),
@@ -5827,6 +5876,10 @@ mod tests {
)
.unwrap();
assert_eq!(options.config.output_root, PathBuf::from("/srv/bat"));
assert_eq!(
options.config.localized_output_root,
PathBuf::from("/srv/bat-localized")
);
assert!(options.config.auto_discover);
assert_eq!(options.state_dir, PathBuf::from("/srv/state"));
assert_eq!(options.interval, Duration::from_secs(120));
@@ -5946,6 +5999,7 @@ mod tests {
keys.push(key);
}
assert!(keys.contains(&"BAT_OUTPUT".to_string()));
assert!(keys.contains(&"BAT_LOCALIZED_OUTPUT".to_string()));
assert!(keys.contains(&"BAT_AUTO_DISCOVER".to_string()));
}
@@ -6118,6 +6172,10 @@ mod tests {
assert!(options.banner);
assert_eq!(options.output_format, OutputFormat::Human);
assert_eq!(config.output_root, PathBuf::from("./bat-resources"));
assert_eq!(
config.localized_output_root,
PathBuf::from("./bat-localized")
);
assert_eq!(options.state_dir, PathBuf::from(DEFAULT_DAEMON_STATE_DIR));
}
@@ -6210,6 +6268,8 @@ mod tests {
"--daemon",
"--output",
"/tmp/daemon-output",
"--localized-output",
"/tmp/daemon-localized",
"--interval",
"30m",
])
@@ -6222,6 +6282,10 @@ mod tests {
options.config.output_root,
PathBuf::from("/tmp/daemon-output")
);
assert_eq!(
options.config.localized_output_root,
PathBuf::from("/tmp/daemon-localized")
);
assert_eq!(options.state_dir, PathBuf::from(DEFAULT_DAEMON_STATE_DIR));
assert_eq!(options.interval, Duration::from_secs(30 * 60));
assert!(options.quiet_up_to_date);
@@ -6246,11 +6310,19 @@ mod tests {
assert!(!status.progress);
assert!(!status.banner);
assert_eq!(status.config.output_root, PathBuf::from("./bat-resources"));
assert_eq!(
status.config.localized_output_root,
PathBuf::from("./bat-localized")
);
assert_eq!(status.state_dir, PathBuf::from(DEFAULT_DAEMON_STATE_DIR));
let stop = parse(&["bat", "stop", "--state-dir", "/tmp/custom-bat-pid"]).unwrap();
assert_eq!(stop.command, CliCommand::Stop);
assert_eq!(stop.config.output_root, PathBuf::from("./bat-resources"));
assert_eq!(
stop.config.localized_output_root,
PathBuf::from("./bat-localized")
);
assert_eq!(stop.state_dir, PathBuf::from("/tmp/custom-bat-pid"));
}
@@ -6312,6 +6384,8 @@ mod tests {
"Windows,Android",
"--output",
"/tmp/daemon-output",
"--localized-output",
"/tmp/daemon-localized",
"--state-dir",
"/tmp/daemon-state",
"--curl",
@@ -6336,6 +6410,9 @@ mod tests {
assert!(args
.windows(2)
.any(|pair| pair == ["--output", "/tmp/daemon-output"]));
assert!(args
.windows(2)
.any(|pair| pair == ["--localized-output", "/tmp/daemon-localized"]));
assert!(args
.windows(2)
.any(|pair| pair == ["--state-dir", "/tmp/daemon-state"]));
@@ -6579,8 +6656,10 @@ mod tests {
let temp = tempfile::TempDir::new().unwrap();
let state_dir = temp.path().join("state");
let output_root = temp.path().join("output");
let mut base_config = OfficialUpdateConfig::default();
base_config.output_root = output_root;
let base_config = OfficialUpdateConfig {
output_root,
..Default::default()
};
let (queue, _rx) = mpsc::channel::<TaskJob>();
let context = DaemonTaskContext {
registry: TaskRegistry::new(),
@@ -6662,11 +6741,13 @@ mod tests {
let control = new_daemon_control();
// 保留 rx 让 send 成功(不启动 worker,任务停留在 queued)。
let (queue, rx) = mpsc::channel::<TaskJob>();
let mut base_config = OfficialUpdateConfig::default();
base_config.force = true;
base_config.dry_run = true;
base_config.audit_local = false;
base_config.repair = false;
let base_config = OfficialUpdateConfig {
force: true,
dry_run: true,
audit_local: false,
repair: false,
..Default::default()
};
let context = DaemonTaskContext {
registry: TaskRegistry::new(),
queue,