From 3f5d2a8da76ac97b11f9478fbb9b258051f2c8fd Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Thu, 23 Jul 2026 11:54:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(sync):=20=E4=BF=AE=E5=A4=8D=E5=AE=98?= =?UTF-8?q?=E6=96=B9=E8=B5=84=E6=BA=90=E5=B9=B6=E5=8F=91=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E4=B8=8E=E5=AA=92=E4=BD=93=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit daemon 子进程现会透传下载并发配置,下载进度按已完成数量单调上报。MediaCatalog 改为使用官方相对路径生成全局媒体 URL,覆盖 GameData、Prologue 等目录并补齐 jpg 资源,避免叶子文件名误拼媒体根目录导致 403。同步更新测试、smoke 断言和运行文档。 --- CHANGELOG.md | 2 +- CURRENT_STATUS.md | 4 +- DOCS_INDEX.md | 2 +- README.md | 2 +- adapters/src/official/inventory.rs | 134 +++++++++++++++--- adapters/src/official/yostar_jp.rs | 11 +- .../architecture/official-resource-backend.md | 6 +- docs/guides/official-full-pull-smoke.md | 6 +- docs/guides/official-resource-test-pull.md | 4 +- docs/reports/CURRENT_GAPS.md | 4 +- infrastructure/src/bin/bat_official_sync.rs | 7 + infrastructure/src/official_download.rs | 44 +++--- infrastructure/src/official_pull.rs | 10 +- infrastructure/src/official_update.rs | 6 +- .../official_game_main_config_bootstrap.rs | 10 +- scripts/official-full-pull-smoke.sh | 4 +- 16 files changed, 182 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f649e27..645af88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ - 新增官方同步路径安全边界:拒绝危险输出目录和 snapshot 路径逃逸,下载目标、manifest、daemon PID/status/log/control 文件不跟随 symlink,daemon 状态文件默认使用 `0600` 权限 - 新增官方资源原子发布布局:非 dry-run 下载先进入 `.staging/`,校验和 manifest/snapshot 写入完成后发布到 `versions/`,再原子切换 `current` symlink - 新增 daemon 可观测性:`bat-events.jsonl` 结构化 JSONL 日志、日志轮转、status 中的当前下载进度、最后成功时间、下次检查时间和最后错误摘要 -- 新增运行时下载与校验 progress log:总体下载进度、单文件开始/完成状态、官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验摘要 +- 新增运行时下载与校验 progress log:下载已完成计数、单文件开始/完成状态、官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验摘要 - 新增官方资源同步生产部署模板:release binary symlink 路径、systemd unit、运行用户、日志位置、升级和回滚流程 - 新增真实官方网络全量拉取 smoke:`scripts/official-full-pull-smoke.sh`、`make official-smoke` 和 `docs/guides/official-full-pull-smoke.md` - 新增官方资源下载校验:官方 URL 拒绝、`.part` 续传、重试、本地 size+BLAKE3、官方 seed `.hash` 校验 diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md index b67ca12..5f96f57 100644 --- a/CURRENT_STATUS.md +++ b/CURRENT_STATUS.md @@ -19,7 +19,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口: 3. 默认平台为 `Windows + Android`。 4. 能生成官方全量 pull plan,执行真实下载,维护 release 内的 `official-download-manifest.json`。 5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;所有 `.zip` 在下载验收、复用、本地 audit/verify 时做 ZIP 结构校验;官方 seed `.hash` 使用标准 `xxHash32(seed=0)` 强校验(早期实现的非标准 avalanche 常量已修正)。 -6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair、失败 staging 恢复复用、403/404/5xx 分类重试(重试带指数退避)、下载 quarantine 诊断,以及旧 launcher 包官方 primary/backup CDN 切换。下载支持多线程:并行仅作用于实际网络下载(默认并发 4,可经 `--download-concurrency`/`BAT_DOWNLOAD_CONCURRENCY` 配置为 `1..=256`),manifest/quarantine 簿记与 seed `.hash` 校验保持串行,`fail-fast` 与「不发布不完整资源」不变量不变。 +6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair、失败 staging 恢复复用、403/404/5xx 分类重试(重试带指数退避)、下载 quarantine 诊断,以及旧 launcher 包官方 primary/backup CDN 切换。下载支持多线程:并行仅作用于实际网络下载(默认并发 4,可经 `--download-concurrency`/`BAT_DOWNLOAD_CONCURRENCY` 配置为 `1..=256`),daemon 子进程会透传该并发配置;manifest/quarantine 簿记与 seed `.hash` 校验保持串行,`fail-fast` 与「不发布不完整资源」不变量不变。下载进度按已完成数量单调上报,不再使用 plan 序号计算百分比。 7. 支持 curl 传输层本地代理:默认自动检测 `HTTPS_PROXY` / `ALL_PROXY` / `HTTP_PROXY` 及小写环境变量(带凭据的代理推荐用环境变量配置),也可用 `--proxy ` 显式指定或 `--no-proxy` 强制直连;代理决策会写入 progress log、daemon log 和 `bat doctor` 诊断输出。代理凭据不落世界可读位置:日志/`status` 脱敏,传给 curl 经 `ALL_PROXY` 环境变量而非 argv,`--daemon` 下经环境变量下传后台子进程、不进子进程 argv 或 `bat-status.json`,复用凭据存于 `bat-proxy.secret`(`0600`)且 `clean-stable` 会清除。 8. `bat --watch` 可常驻运行,`bat --daemon` 可后台运行并用 `bat status` / `bat stop` / `bat restart` / `bat reload` / `bat logs` 管理;daemon 使用 `bat.sock` Unix socket JSON-RPC 作为 live 控制通道,PID/状态/日志文件作为快照和 fallback,`bat-events.jsonl` 记录带轮转的结构化事件日志,`bat-control.lock` 串行化控制命令;正常检查默认每 1 小时一次;远端和本地一致时默认静默,失败后默认 60 秒快速重试;CLI 默认向 stdout 输出人类可读摘要,向 stderr 输出 ASCII banner、progress log、失败分类和 quarantine 状态,需要机器输出时使用 `--json --no-progress`。 9. 远端 snapshot 未变化但输出目录为空时,会按首次运行执行全量拉取;官方 seed `.hash` 校验失败时会清理对应 manifest 条目,避免失败产物被后续本地 audit 误判为可复用。 @@ -92,7 +92,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口: - Addressables JSON catalog 的 path、hash、size、address、dependencies、metadata 解析。 - 真实形态 Addressables fixture/golden 测试。 - 当前 catalog、上一个版本 catalog、结构变化 catalog 的离线回归 fixture。 -- 官方日服 `server-info`、URL 规则、平台 discovery 和 inventory 枚举。 +- 官方日服 `server-info`、URL 规则、平台 discovery 和 inventory 枚举;`MediaCatalog.bytes` 使用官方相对路径生成媒体 URL,覆盖 `GameData/`、`Prologue/` 下的 zip/mp4/png/jpg/ogg/wav 等媒体资源,避免把叶子文件名误拼到媒体根目录。 待完成: diff --git a/DOCS_INDEX.md b/DOCS_INDEX.md index a96470d..9e95690 100644 --- a/DOCS_INDEX.md +++ b/DOCS_INDEX.md @@ -99,7 +99,7 @@ - 文档整理和路线图重制。 - Rust 官方资源同步闭环:`bat`、`--auto-discover`、`--watch`、`--daemon`、Unix socket JSON-RPC 后台控制、`status`、`stop`、`restart`、`reload`、`refresh`、`logs`、`verify`、`repair`、`doctor`、`clean-stable`、北京时间固定强制刷新、snapshot、manifest audit/repair、官方 seed `.hash` 校验。 - 真实官方网络全量拉取 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`,默认写入 `/tmp` 隔离目录并输出本地运行报告。 -- `bat` 运行时 progress log 已覆盖总体下载进度、单文件下载进度和校验结果摘要。 +- `bat` 运行时 progress log 已覆盖下载已完成计数、单文件下载进度和校验结果摘要。 - Addressables 当前真实形态 fixture/golden 覆盖。 - SQLite Resource Repository 和可选无状态 `bat-ffi` JSON 兼容接口。 diff --git a/README.md b/README.md index 147c6c1..32887af 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ cargo run -p bat-infrastructure --bin bat -- clean-stable 下载失败会按 curl exit 和 HTTP 状态分类:403/404/普通 4xx 视为不可重试,5xx、429、DNS、连接、超时、中断和网络类错误会按尝试次数重试。某个 URL 最终失败后会写入 `/current` 或 staging 下的 `official-download-quarantine.json`,stderr progress、daemon status 和 `bat-events.jsonl` 会记录失败类型、HTTP 状态、是否可重试、尝试次数和 quarantine 状态;同步会中断并阻止发布不完整资源。旧 launcher 包下载路径会在官方 primary CDN 失败后切换官方 backup CDN。 -CLI 默认启动时会向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并继续把阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、总体下载进度、单文件下载进度、校验结果摘要、snapshot 和 publish;命令结果默认以人类可读摘要写到 stdout。需要给上层程序保留稳定结构化输出时加 `--json --no-progress`,只想关闭横幅但保留日志时可加 `--no-banner`。 +CLI 默认启动时会向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并继续把阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、下载已完成计数、单文件下载进度、校验结果摘要、snapshot 和 publish;命令结果默认以人类可读摘要写到 stdout。需要给上层程序保留稳定结构化输出时加 `--json --no-progress`,只想关闭横幅但保留日志时可加 `--no-banner`。 真实官方网络全量拉取 smoke 已固化为可重复命令,默认使用 `/tmp/bat-official-smoke-/` 隔离目录,不会写入已有客户端、生产目录或开发机人工维护资源目录: diff --git a/adapters/src/official/inventory.rs b/adapters/src/official/inventory.rs index 3ae2db8..6788520 100644 --- a/adapters/src/official/inventory.rs +++ b/adapters/src/official/inventory.rs @@ -10,7 +10,7 @@ pub struct YostarJpDownloadInventory { pub bundle_patch_pack_names: Vec, /// Table file names from `TableCatalog.bytes`. pub table_file_names: Vec, - /// Media file names from `MediaCatalog.bytes`. + /// Media file relative paths from `MediaCatalog.bytes`. pub media_file_names: Vec, } @@ -21,7 +21,7 @@ pub struct YostarJpPlatformCatalogInventory { pub platform: PatchPlatform, /// Patch-pack zip names from this platform's `BundlePackingInfo.bytes`. pub bundle_patch_pack_names: Vec, - /// Media file names from this platform's `MediaCatalog.bytes`. + /// Media file relative paths from this platform's `MediaCatalog.bytes`. pub media_file_names: Vec, } @@ -35,10 +35,7 @@ impl YostarJpPlatformCatalogInventory { Self { platform, bundle_patch_pack_names: extract_full_patch_pack_names(bundle_packing_info), - media_file_names: extract_file_names( - media_catalog, - &["zip", "mp4", "png", "ogg", "wav"], - ), + media_file_names: extract_media_file_paths(media_catalog), } } @@ -172,10 +169,7 @@ impl YostarJpDownloadInventory { Self { bundle_patch_pack_names: extract_full_patch_pack_names(bundle_packing_info), table_file_names: extract_table_file_names(table_catalog), - media_file_names: extract_file_names( - media_catalog, - &["zip", "mp4", "png", "ogg", "wav"], - ), + media_file_names: extract_media_file_paths(media_catalog), } } @@ -327,6 +321,20 @@ fn extract_file_names(data: &[u8], extensions: &[&str]) -> Vec { names.into_iter().collect() } +fn extract_media_file_paths(data: &[u8]) -> Vec { + let mut paths = BTreeSet::new(); + + for string in extract_printable_strings(data, 4) { + for path in + candidate_relative_paths(&string, &["zip", "mp4", "png", "jpg", "jpeg", "ogg", "wav"]) + { + paths.insert(path); + } + } + + paths.into_iter().collect() +} + fn extract_printable_strings(data: &[u8], min_len: usize) -> Vec { let mut strings = Vec::new(); let mut current = Vec::new(); @@ -375,6 +383,32 @@ fn candidate_file_names(value: &str, extensions: &[&str]) -> Vec { names } +fn candidate_relative_paths(value: &str, extensions: &[&str]) -> Vec { + let mut paths = Vec::new(); + let bytes = value.as_bytes(); + + for extension in extensions { + let suffix = format!(".{extension}"); + let mut search_from = 0; + + while let Some(relative_index) = value[search_from..].find(&suffix) { + let extension_start = search_from + relative_index; + let start = filename_start(bytes, extension_start); + let end = extension_start + suffix.len(); + let candidate = &value[start..end]; + let candidate = candidate.replace('\\', "/"); + + if is_plausible_relative_path(&candidate) { + paths.push(candidate); + } + + search_from = end; + } + } + + paths +} + fn filename_start(bytes: &[u8], mut index: usize) -> usize { while index > 0 { let byte = bytes[index - 1]; @@ -409,6 +443,28 @@ fn is_plausible_file_name(name: &str) -> bool { .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')) } +fn is_plausible_relative_path(path: &str) -> bool { + if path.is_empty() + || path.starts_with('/') + || path.starts_with('.') + || path.contains("..") + || path.contains(':') + || path.contains('=') + || !path.contains('/') + { + return false; + } + + path.split('/').all(|segment| { + !segment.is_empty() + && segment != "." + && segment != ".." + && segment + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')) + }) +} + fn unique_platforms(platforms: &[PatchPlatform]) -> Vec { platforms .iter() @@ -441,7 +497,7 @@ mod tests { let inventory = YostarJpDownloadInventory::from_catalog_bytes( b"prefix FullPatch_000.zip noise FullPatch_114.zip suffix", b"GameData\\Table\\ExcelDB.db\0ExcelDB.db\0rawdata/table/excel/ignored.bytes\0Battle.zip\0Battle.zip8", - b"audio/voc_jp/jp_airi/jp_airi\0GameData\\Audio\\VOC_JP\\JP_Airi.zip8\0JP_Akane.zip", + b"audio/voc_jp/jp_airi/jp_airi\0GameData\\Audio\\VOC_JP\\JP_Airi.zip8\0audio/voc_jp/jp_akane/jp_akane\0GameData\\Audio\\VOC_JP\\JP_Akane.zip", ); assert_eq!( @@ -457,7 +513,10 @@ mod tests { ); assert_eq!( inventory.media_file_names, - vec!["JP_Airi.zip".to_string(), "JP_Akane.zip".to_string(),] + vec![ + "GameData/Audio/VOC_JP/JP_Airi.zip".to_string(), + "GameData/Audio/VOC_JP/JP_Akane.zip".to_string(), + ] ); } @@ -482,7 +541,7 @@ mod tests { let inventory = YostarJpDownloadInventory::from_catalog_bytes( b"FullPatch_000.zip FullPatch_001.zip", b"ExcelDB.db ExcelDB.db Battle.zip Battle.zip", - b"JP_Airi.zip JP_Akane.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi.zip GameData\\Audio\\VOC_JP\\JP_Akane.zip", ); let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap(); @@ -496,7 +555,7 @@ mod tests { .any(|url| url.ends_with("/TableBundles/ExcelDB.db"))); assert!(urls .iter() - .any(|url| url.ends_with("/MediaResources-Windows/JP_Airi.zip"))); + .any(|url| url.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip"))); } #[test] @@ -504,7 +563,7 @@ mod tests { let inventory = YostarJpDownloadInventory::from_catalog_bytes( b"FullPatch_000.zip", b"ExcelDB.db ExcelDB.db", - b"JP_Airi.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi.zip", ); let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap(); @@ -524,10 +583,10 @@ mod tests { .any(|url| url.ends_with("/TableBundles/ExcelDB.db"))); assert!(urls .iter() - .any(|url| url.ends_with("/MediaResources-Windows/JP_Airi.zip"))); + .any(|url| url.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip"))); assert!(urls .iter() - .any(|url| url.ends_with("/MediaResources/JP_Airi.zip"))); + .any(|url| url.ends_with("/MediaResources/GameData/Audio/VOC_JP/JP_Airi.zip"))); } #[test] @@ -538,12 +597,12 @@ mod tests { YostarJpPlatformCatalogInventory::from_catalog_bytes( PatchPlatform::Windows, b"FullPatch_000.zip", - b"JP_Airi_Win.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip", ), YostarJpPlatformCatalogInventory::from_catalog_bytes( PatchPlatform::Android, b"FullPatch_001.zip", - b"JP_Airi_Android.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip", ), ], ); @@ -561,10 +620,11 @@ mod tests { .any(|url| url.ends_with("/Android_PatchPack/FullPatch_001.zip"))); assert!(urls .iter() - .any(|url| url.ends_with("/MediaResources-Windows/JP_Airi_Win.zip"))); + .any(|url| url + .ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi_Win.zip"))); assert!(urls .iter() - .any(|url| url.ends_with("/MediaResources/JP_Airi_Android.zip"))); + .any(|url| url.ends_with("/MediaResources/GameData/Audio/VOC_JP/JP_Airi_Android.zip"))); assert!(!urls .iter() .any(|url| url.ends_with("/Android_PatchPack/FullPatch_000.zip"))); @@ -573,6 +633,30 @@ mod tests { .any(|url| url.ends_with("/Windows_PatchPack/FullPatch_001.zip"))); } + #[test] + fn media_catalog_uses_download_relative_path_not_leaf_name() { + let inventory = YostarJpPlatformDownloadInventory::from_catalog_bytes( + b"ExcelDB.db ExcelDB.db", + vec![YostarJpPlatformCatalogInventory::from_catalog_bytes( + PatchPlatform::Windows, + b"FullPatch_000.zip", + b"scenario/event/10000_title_sound\0Prologue\\Scenario\\Event\\10000_Title_Sound.ogg\0 10000_Title_Sound.ogg", + )], + ); + let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap(); + + let urls = inventory + .direct_download_urls_for_platforms(&root, &[PatchPlatform::Windows]) + .unwrap(); + + assert!(urls.iter().any(|url| { + url.ends_with("/MediaResources-Windows/Prologue/Scenario/Event/10000_Title_Sound.ogg") + })); + assert!(!urls + .iter() + .any(|url| url.ends_with("/MediaResources-Windows/10000_Title_Sound.ogg"))); + } + #[test] #[ignore = "requires BAT_REAL_OFFICIAL_BUNDLE_PACKING_INFO, BAT_REAL_OFFICIAL_TABLE_CATALOG, BAT_REAL_OFFICIAL_MEDIA_CATALOG"] fn extracts_realistic_counts_from_official_shape() { @@ -591,7 +675,7 @@ mod tests { assert_eq!(inventory.bundle_patch_pack_names.len(), 142); assert!(inventory.table_file_names.len() < 1000); - assert_eq!(inventory.media_file_names.len(), 1887); + assert!(inventory.media_file_names.len() >= 4000); assert!(inventory .bundle_patch_pack_names .iter() @@ -603,6 +687,10 @@ mod tests { assert!(inventory .media_file_names .iter() - .any(|name| name == "JP_Airi.zip")); + .any(|name| name == "GameData/Audio/VOC_JP/JP_Airi.zip")); + assert!(inventory + .media_file_names + .iter() + .any(|name| name.ends_with(".jpg"))); } } diff --git a/adapters/src/official/yostar_jp.rs b/adapters/src/official/yostar_jp.rs index 4acef9d..174af3e 100644 --- a/adapters/src/official/yostar_jp.rs +++ b/adapters/src/official/yostar_jp.rs @@ -646,8 +646,8 @@ impl YostarJpResourceRoot { /// Returns an official media archive URL. /// - /// The argument is the `Media.FileName` field from `MediaCatalog.bytes`, - /// for example `JP_Airi.zip`. + /// The argument is the downloadable relative path from `MediaCatalog.bytes`, + /// for example `GameData/Audio/VOC_JP/JP_Airi.zip`. pub fn media_file(&self, platform: PatchPlatform, file_name: &str) -> Result { validate_relative_path(file_name, "media file")?; Ok(format!( @@ -1134,8 +1134,9 @@ mod tests { "https://prod-clientpatch.bluearchiveyostar.com/r93_dctuo3tcd029wwxnvb55/MediaResources-Windows/Catalog/MediaCatalog.bytes" ); assert_eq!( - root.media_file(PatchPlatform::Windows, "JP_Airi.zip").unwrap(), - "https://prod-clientpatch.bluearchiveyostar.com/r93_dctuo3tcd029wwxnvb55/MediaResources-Windows/JP_Airi.zip" + root.media_file(PatchPlatform::Windows, "GameData/Audio/VOC_JP/JP_Airi.zip") + .unwrap(), + "https://prod-clientpatch.bluearchiveyostar.com/r93_dctuo3tcd029wwxnvb55/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip" ); } @@ -1211,7 +1212,7 @@ mod tests { ); assert!(root.table_bundle("text=jp/ExcelDB.db").is_err()); assert!(root - .media_file(PatchPlatform::Windows, "/JP_Airi.zip") + .media_file(PatchPlatform::Windows, "/GameData/Audio/VOC_JP/JP_Airi.zip") .is_err()); assert!(root .bundle_patch_pack(PatchPlatform::Windows, "../FullPatch_000.zip") diff --git a/docs/architecture/official-resource-backend.md b/docs/architecture/official-resource-backend.md index 6744320..025415b 100644 --- a/docs/architecture/official-resource-backend.md +++ b/docs/architecture/official-resource-backend.md @@ -68,7 +68,7 @@ ### 3.2 枚举完整资源清单 -资源清单不是“猜几个文件”,而是从官方 catalog 字节里提取完整文件名列表。 +资源清单不是“猜几个文件”,而是从官方 catalog 字节里提取完整文件名或相对路径列表。 当前做法: @@ -77,7 +77,7 @@ 3. 读取 `TableCatalog.bytes`。 4. 提取所有表资源名,例如 `ExcelDB.db`。 5. 读取 `MediaCatalog.bytes`。 -6. 提取所有媒体资源名,例如 `JP_Airi.zip`。 +6. 提取所有媒体下载相对路径,例如 `GameData/Audio/VOC_JP/JP_Airi.zip`、`Prologue/Scenario/Event/10000_Title_Sound.ogg`。 然后对 verified platforms 生成完整 URL 集: @@ -246,7 +246,7 @@ Linux 生产路径: - pull plan 会同时包含 discovery URLs 和 content URLs - 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL - `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策 -- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖代理决策、总体下载进度、单文件开始/完成状态、下载中断失败分类和校验结果摘要;支持 `--proxy` / `--no-proxy` 控制 curl 传输代理,支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC 控制、`status`、`stop`、`restart`、`reload`、`logs`、`refresh --force`、`verify`、`repair`、`doctor`、`clean-stable`,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度、失败类型、HTTP 状态和调度状态 +- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖代理决策、下载已完成计数、单文件开始/完成状态、下载中断失败分类和校验结果摘要;支持 `--proxy` / `--no-proxy` 控制 curl 传输代理,支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC 控制、`status`、`stop`、`restart`、`reload`、`logs`、`refresh --force`、`verify`、`repair`、`doctor`、`clean-stable`,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度、失败类型、HTTP 状态和调度状态 - curl 失败分类和重试策略已覆盖 404 不重试、5xx 重试耗尽后 quarantine、launcher primary CDN 失败后切换 official backup CDN - `official-version-state.json` 已覆盖当前完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,重新拉取或成功发布后清理同版本失败记录,同版本失败 staging 会在路径安全且未发布时复用,`bat status` 会暴露版本状态摘要和最近历史失败原因 - 资源导入链路已覆盖 CAS 写入、`ResourceRepository` 索引、AssetBundle UnityFS 摘要,以及 TextAsset/Table/Media 分类 diff --git a/docs/guides/official-full-pull-smoke.md b/docs/guides/official-full-pull-smoke.md index 3b2623f..d689552 100644 --- a/docs/guides/official-full-pull-smoke.md +++ b/docs/guides/official-full-pull-smoke.md @@ -69,10 +69,10 @@ cargo build --release -p bat-infrastructure --bin bat 脚本会在关键步骤后自动检查: -- 首次全量拉取 stderr log 包含总体下载进度、单文件进度和校验结果。 +- 首次全量拉取 stderr log 包含下载已完成计数、单文件进度和校验结果。 - 二次运行 stdout JSON 包含 `update_status=up_to_date`。 - repair stdout JSON 包含 `command=repair` 和 `status=completed`。 -- repair stderr log 包含总体下载进度、单文件进度和校验结果。 +- repair stderr log 包含下载已完成计数、单文件进度和校验结果。 - repair 后 verify stdout JSON 包含 `healthy=true`。 ## 环境变量 @@ -96,4 +96,4 @@ cargo build --release -p bat-infrastructure --bin bat - `03-second-up-to-date.stdout.json` 中 `update_status` 为 `up_to_date`。 - `04-repair-after-damage.stdout.json` 中 repair 完成,且有重新下载或修复行为。 - `05-verify-after-repair.stdout.json` 中 `healthy` 为 `true`。 -- `02-first-full-pull.stderr.log` 和 `04-repair-after-damage.stderr.log` 中包含下载总体进度、单文件进度和校验结果日志。 +- `02-first-full-pull.stderr.log` 和 `04-repair-after-damage.stderr.log` 中包含下载已完成计数、单文件进度和校验结果日志。 diff --git a/docs/guides/official-resource-test-pull.md b/docs/guides/official-resource-test-pull.md index c07ba49..e69e4d2 100644 --- a/docs/guides/official-resource-test-pull.md +++ b/docs/guides/official-resource-test-pull.md @@ -308,7 +308,7 @@ cargo run -p bat-infrastructure --bin bat -- \ --error-retry 60s ``` -默认平台是 `Windows,Android`,无需显式传 `--platforms`;只有要覆盖默认平台时才传。`--interval` 是正常检查周期,默认 `1h`;watch/daemon 模式还会在每天北京时间(UTC+8)`03:00`、`16:00`、`18:00` 强制执行一次自动刷新,该轮会注入 `force=true`,并且会中断普通 interval 的 sleep。`--error-retry` 是下载、发现或校验失败后的重试周期,默认 `60s`,也可以用 `--error-retry-seconds 60`。CLI 默认启动时向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并把阶段进度日志写到 stderr,包括自动发现、proxy、server-info、marker、catalog、audit、download、snapshot 和 publish 阶段;download 阶段会输出总体下载进度和单文件开始/完成状态,audit 阶段会输出官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验结果摘要。daemon 还会写 `bat-events.jsonl` 结构化日志并按大小轮转。命令结果默认以人类可读摘要写到 stdout。需要纯机器输出时加 `--json --no-progress`,需要显式开启进度日志则用 `--progress`;只想关闭横幅但保留日志时可加 `--no-banner`。错误时 stderr 输出 JSON error,watch 模式下错误 JSON 的 `next_retry_seconds` 使用失败重试周期;如果未关闭 progress,错误 JSON 前可能已有 banner 和进度日志。普通错误 exit `1`,资源目录锁冲突 exit `75`,`verify` 或 `doctor` 发现问题也返回非 0。 +默认平台是 `Windows,Android`,无需显式传 `--platforms`;只有要覆盖默认平台时才传。`--interval` 是正常检查周期,默认 `1h`;watch/daemon 模式还会在每天北京时间(UTC+8)`03:00`、`16:00`、`18:00` 强制执行一次自动刷新,该轮会注入 `force=true`,并且会中断普通 interval 的 sleep。`--error-retry` 是下载、发现或校验失败后的重试周期,默认 `60s`,也可以用 `--error-retry-seconds 60`。CLI 默认启动时向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并把阶段进度日志写到 stderr,包括自动发现、proxy、server-info、marker、catalog、audit、download、snapshot 和 publish 阶段;download 阶段会输出已完成计数和单文件开始/完成状态,已完成计数在并发下载时保持单调不倒退,audit 阶段会输出官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验结果摘要。daemon 还会写 `bat-events.jsonl` 结构化日志并按大小轮转。命令结果默认以人类可读摘要写到 stdout。需要纯机器输出时加 `--json --no-progress`,需要显式开启进度日志则用 `--progress`;只想关闭横幅但保留日志时可加 `--no-banner`。错误时 stderr 输出 JSON error,watch 模式下错误 JSON 的 `next_retry_seconds` 使用失败重试周期;如果未关闭 progress,错误 JSON 前可能已有 banner 和进度日志。普通错误 exit `1`,资源目录锁冲突 exit `75`,`verify` 或 `doctor` 发现问题也返回非 0。 生产可以直接运行 `--watch`,也可以用 `--daemon` 后台运行,或者用 systemd service、容器或 Go 进程守护它。cron/systemd timer 仍可调用单次模式,但不再是 Rust 自动更新的唯一方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。生产资源目录应使用独立输出目录,不要指向现有客户端或人工维护的资源目录;上层读取资源时应读取 `--output/current`,不要读取 `.staging` 或 `versions` 中未切换的目录。非 dry-run 每轮会创建 `--output/.official-sync.lock`,防止并发写同一资源目录;live daemon 还会阻止前台写命令直接修改它正在管理的同一目录。 @@ -325,7 +325,7 @@ scripts/official-full-pull-smoke.sh make official-smoke ``` -默认输出在 `/tmp/bat-official-smoke-/`,脚本会执行 dry-run plan、首次全量拉取、二次 `up_to_date`、本地文件破坏后的 `repair`、repair 后 `verify`,并检查 stderr progress log 中存在总体下载进度、单文件进度和校验结果摘要。完整说明见 `docs/guides/official-full-pull-smoke.md`。 +默认输出在 `/tmp/bat-official-smoke-/`,脚本会执行 dry-run plan、首次全量拉取、二次 `up_to_date`、本地文件破坏后的 `repair`、repair 后 `verify`,并检查 stderr progress log 中存在下载已完成计数、单文件进度和校验结果摘要。完整说明见 `docs/guides/official-full-pull-smoke.md`。 ## 7. 例外输入 diff --git a/docs/reports/CURRENT_GAPS.md b/docs/reports/CURRENT_GAPS.md index 0737f4d..8be9507 100644 --- a/docs/reports/CURRENT_GAPS.md +++ b/docs/reports/CURRENT_GAPS.md @@ -426,7 +426,7 @@ - 新增 `make official-smoke` 统一入口。 - 新增 `docs/guides/official-full-pull-smoke.md`,记录目标、命令、输出结构、环境变量、安全边界和成功判定。 - smoke 流程覆盖 dry-run plan、首次全量拉取、二次 `up_to_date`、人工破坏 active release 文件后的 `repair`、repair 后 `verify`。 -- 脚本会检查二次 `up_to_date`、repair 完成、verify `healthy=true`,并检查首次拉取和 repair 的 stderr log 中存在总体下载进度、单文件进度和校验结果日志。 +- 脚本会检查二次 `up_to_date`、repair 完成、verify `healthy=true`,并检查首次拉取和 repair 的 stderr log 中存在下载已完成计数、单文件进度和校验结果日志。 - 运行报告 `SMOKE_REPORT.md` 记录实际输出目录、active release、文件数量、release 大小和被破坏文件;大型官方资源文件保留在隔离输出目录,不纳入 Git。 验收: @@ -470,7 +470,7 @@ ## 6. 当前关闭顺序建议 -1. issue #17 验收并关闭或更新范围:多线程下载与指数退避实现已合入,但 GitHub issue 仍 open。 +1. issue #17/#20/#21/#22 验收并关闭或更新范围:多线程下载与指数退避实现已合入,daemon 子进程并发参数透传和单调进度上报已补齐,但 GitHub issue 仍 open。 2. issue #2 / G-007:继续扩大 Addressables 可校验字段和结构变体覆盖。 3. issue #3 / G-005:把 UnityFS 基础摘要推进到 `bat-assetbundle` 引擎级解析。 4. G-011:官方同步结果接入 CAS + ResourceRepository 用户级工作流。 diff --git a/infrastructure/src/bin/bat_official_sync.rs b/infrastructure/src/bin/bat_official_sync.rs index 7df6be6..2d2b03e 100644 --- a/infrastructure/src/bin/bat_official_sync.rs +++ b/infrastructure/src/bin/bat_official_sync.rs @@ -4471,6 +4471,8 @@ fn daemon_child_args(options: &CliOptions) -> Vec { } args.push("--curl".to_string()); args.push(config.curl_command.to_string_lossy().to_string()); + args.push("--download-concurrency".to_string()); + args.push(config.download_concurrency.to_string()); match config.curl_proxy.mode() { CurlProxyMode::Auto if options.proxy_option_explicit => { args.push("--proxy".to_string()); @@ -6324,6 +6326,8 @@ mod tests { "http://127.0.0.1:7890", "--unzip", "/usr/bin/unzip", + "--download-concurrency", + "40", "--interval", "30m", "--error-retry", @@ -6346,6 +6350,9 @@ mod tests { assert!(args .windows(2) .any(|pair| pair == ["--platforms", "Windows,Android"])); + assert!(args + .windows(2) + .any(|pair| pair == ["--download-concurrency", "40"])); // 代理凭据不得进入子进程 argv:只放不含凭据的 flag,URL 经环境变量下传。 assert!(args.contains(&PROXY_FROM_ENV_FLAG.to_string())); assert!(!args.iter().any(|arg| arg.contains("127.0.0.1:7890"))); diff --git a/infrastructure/src/official_download.rs b/infrastructure/src/official_download.rs index b291a98..787e2c5 100644 --- a/infrastructure/src/official_download.rs +++ b/infrastructure/src/official_download.rs @@ -118,7 +118,12 @@ impl OfficialResourcePullProgressKind { pub struct OfficialResourcePullProgress { /// Progress event kind. pub kind: OfficialResourcePullProgressKind, - /// One-based URL index in the pull plan. + /// Monotonic completed item count for the whole pull. + /// + /// `Started` events report the currently completed count before the URL + /// finishes; `Finished` events report the count after completion. This is + /// intentionally not the URL's plan position, because concurrent downloads + /// finish out of plan order and status percentages must not move backward. pub index: usize, /// Total URL count in the pull plan. pub total: usize, @@ -633,6 +638,7 @@ impl OfficialResourcePullService { .collect(); let mut download_results: Vec>> = (0..planned.len()).map(|_| None).collect(); + let mut completed_count = 0usize; if !download_indices.is_empty() { let concurrency = self.download_concurrency.clamp(1, MAX_DOWNLOAD_CONCURRENCY); @@ -680,7 +686,7 @@ impl OfficialResourcePullService { WorkerMessage::Started { plan_index } => { let item = &planned[plan_index]; progress(OfficialResourcePullProgress::started( - plan_index + 1, + completed_count, total, item.url.clone(), )); @@ -711,8 +717,9 @@ impl OfficialResourcePullService { ))); continue; } + completed_count += 1; progress(OfficialResourcePullProgress::finished( - plan_index + 1, + completed_count, total, item.url.clone(), pull_result.status, @@ -736,7 +743,7 @@ impl OfficialResourcePullService { let item = &planned[plan_index]; self.record_quarantine_entry(&item.url, &item.destination, error)?; progress(OfficialResourcePullProgress::failed( - plan_index + 1, + completed_count, total, item.url.clone(), error, @@ -768,12 +775,13 @@ impl OfficialResourcePullService { let result = if let Some(existing) = &item.existing { self.clear_quarantine_entry(&item.url)?; progress(OfficialResourcePullProgress::started( - plan_index + 1, + completed_count, total, item.url.clone(), )); + completed_count += 1; progress(OfficialResourcePullProgress::finished( - plan_index + 1, + completed_count, total, item.url.clone(), existing.status, @@ -2083,7 +2091,7 @@ exit 22 YostarJpDownloadInventory::from_catalog_bytes( b"FullPatch_000.zip", b"ExcelDB.db ExcelDB.db", - b"JP_Airi.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi.zip", ) } @@ -2143,10 +2151,10 @@ done printf '%s' '{android_media_hash}' > "$out" ;; */MediaResources-Windows/Catalog/MediaCatalog.bytes) - printf '%s' 'JP_Airi_Win.zip' > "$out" + printf '%s' 'GameData\Audio\VOC_JP\JP_Airi_Win.zip' > "$out" ;; */MediaResources/Catalog/MediaCatalog.bytes) - printf '%s' 'JP_Airi_Android.zip' > "$out" + printf '%s' 'GameData\Audio\VOC_JP\JP_Airi_Android.zip' > "$out" ;; *.zip) printf '\120\113\003\004\024\000\000\000\000\000\000\000\000\000\000\000\000\000\002\000\000\000\002\000\000\000\010\000\000\000file.txtok\120\113\001\002\024\000\024\000\000\000\000\000\000\000\000\000\000\000\000\000\002\000\000\000\002\000\000\000\010\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000file.txt\120\113\005\006\000\000\000\000\001\000\001\000\066\000\000\000\050\000\000\000\000\000' > "$out" @@ -2162,8 +2170,8 @@ fi table_catalog_hash = xxhash32(b"ExcelDB.db"), windows_bundle_hash = xxhash32(b"FullPatch_000.zip"), android_bundle_hash = xxhash32(b"FullPatch_001.zip"), - windows_media_hash = xxhash32(b"JP_Airi_Win.zip"), - android_media_hash = xxhash32(b"JP_Airi_Android.zip"), + windows_media_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip"), + android_media_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip"), ) } @@ -2868,7 +2876,10 @@ exit 22 assert_eq!(xxhash32(b""), 46947589); assert_eq!(xxhash32(b"ExcelDB.db"), 2044170421); assert_eq!(xxhash32(b"FullPatch_000.zip"), 4038880697); - assert_eq!(xxhash32(b"JP_Airi_Win.zip"), 1416778215); + assert_eq!( + xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip"), + 1865771294 + ); assert_eq!(xxhash32(b"ExcelDB.db ExcelDB.db"), 2147704569); } @@ -3053,11 +3064,12 @@ exit 22 assert_eq!(started.len(), expected_urls); assert_eq!(finished.len(), expected_urls); - // 每个 started 的 total 一致;index 覆盖 1..=N 且不重复。 - let mut indices: Vec = started.iter().map(|event| event.index).collect(); - indices.sort_unstable(); - assert_eq!(indices, (1..=expected_urls).collect::>()); + // 每个 started 的 total 一致;index 表示已完成数量,不能超过总数。 + assert!(started.iter().all(|event| event.index <= expected_urls)); assert!(started.iter().all(|event| event.total == expected_urls)); + assert!(events.windows(2).all(|pair| pair[0].index <= pair[1].index)); + let finished_indices: Vec = finished.iter().map(|event| event.index).collect(); + assert_eq!(finished_indices, (1..=expected_urls).collect::>()); // started 与 finished 覆盖同一组 URL;finished 均为已下载。 let started_urls: HashSet<_> = started.iter().map(|event| event.url.clone()).collect(); diff --git a/infrastructure/src/official_pull.rs b/infrastructure/src/official_pull.rs index 787d9a2..f79d5f5 100644 --- a/infrastructure/src/official_pull.rs +++ b/infrastructure/src/official_pull.rs @@ -186,7 +186,7 @@ mod tests { YostarJpDownloadInventory::from_catalog_bytes( b"FullPatch_000.zip", b"ExcelDB.db ExcelDB.db", - b"JP_Airi.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi.zip", ) } @@ -197,12 +197,12 @@ mod tests { YostarJpPlatformCatalogInventory::from_catalog_bytes( PatchPlatform::Windows, b"FullPatch_000.zip", - b"JP_Airi_Win.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip", ), YostarJpPlatformCatalogInventory::from_catalog_bytes( PatchPlatform::Android, b"FullPatch_001.zip", - b"JP_Airi_Android.zip", + b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip", ), ], ) @@ -229,11 +229,11 @@ mod tests { assert!(all_urls[0].ends_with("TableCatalog.bytes")); assert!(all_urls .iter() - .any(|url| url.ends_with("/MediaResources-Windows/JP_Airi.zip"))); + .any(|url| url.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip"))); assert_eq!( all_urls .iter() - .filter(|url| url.ends_with("/MediaResources/JP_Airi.zip")) + .filter(|url| url.ends_with("/MediaResources/GameData/Audio/VOC_JP/JP_Airi.zip")) .count(), 1 ); diff --git a/infrastructure/src/official_update.rs b/infrastructure/src/official_update.rs index e49f684..dda4b53 100644 --- a/infrastructure/src/official_update.rs +++ b/infrastructure/src/official_update.rs @@ -1654,7 +1654,7 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new( "download", format!( - "下载进度:总体 {}/{} ({:.1}%);单文件开始 URL={}", + "下载进度:已完成 {}/{} ({:.1}%);单文件开始 URL={}", event.index, event.total, download_progress_percent(event.index, event.total), @@ -1667,7 +1667,7 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda OfficialUpdateProgress::new( "download", format!( - "下载进度:总体 {}/{} ({:.1}%);单文件完成 状态={} 文件字节={} 本轮传输字节={} URL={}", + "下载进度:已完成 {}/{} ({:.1}%);单文件完成 状态={} 文件字节={} 本轮传输字节={} URL={}", event.index, event.total, download_progress_percent(event.index, event.total), @@ -1682,7 +1682,7 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda OfficialResourcePullProgressKind::Failed => OfficialUpdateProgress::new( "download", format!( - "下载中断:总体 {}/{} ({:.1}%);失败类型={} HTTP={} 可重试={} 尝试次数={} quarantine={};本轮跳过该 URL 且不会发布不完整资源 URL={}", + "下载中断:已完成 {}/{} ({:.1}%);失败类型={} HTTP={} 可重试={} 尝试次数={} quarantine={};本轮跳过该 URL 且不会发布不完整资源 URL={}", event.index, event.total, download_progress_percent(event.index, event.total), diff --git a/infrastructure/tests/official_game_main_config_bootstrap.rs b/infrastructure/tests/official_game_main_config_bootstrap.rs index 9792315..1823a36 100644 --- a/infrastructure/tests/official_game_main_config_bootstrap.rs +++ b/infrastructure/tests/official_game_main_config_bootstrap.rs @@ -231,7 +231,7 @@ fn official_update_first_run_pulls_without_pre_audit() { .any(|event| { event.stage == "decision" && event.message.contains("首次拉取=true") })); assert!(events.iter().any(|event| { event.stage == "download" - && event.message.contains("下载进度:总体") + && event.message.contains("下载进度:已完成") && event.message.contains("单文件") })); assert!(events.iter().any(|event| { @@ -561,7 +561,7 @@ elif [[ "$url" == "{addressables_root}/Windows_PatchPack/BundlePackingInfo.bytes elif [[ "$url" == "{addressables_root}/Windows_PatchPack/BundlePackingInfo.hash" ]]; then emit_text "{windows_bundle_catalog_hash}" elif [[ "$url" == "{addressables_root}/MediaResources-Windows/Catalog/MediaCatalog.bytes" ]]; then - emit_text "JP_Airi_Win.zip" + emit_text "GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip" elif [[ "$url" == "{addressables_root}/MediaResources-Windows/Catalog/MediaCatalog.hash" ]]; then emit_text "{windows_media_catalog_hash}" elif [[ "$url" == "{addressables_root}/Android_PatchPack/BundlePackingInfo.bytes" ]]; then @@ -569,7 +569,7 @@ elif [[ "$url" == "{addressables_root}/Android_PatchPack/BundlePackingInfo.bytes elif [[ "$url" == "{addressables_root}/Android_PatchPack/BundlePackingInfo.hash" ]]; then emit_text "{android_bundle_catalog_hash}" elif [[ "$url" == "{addressables_root}/MediaResources/Catalog/MediaCatalog.bytes" ]]; then - emit_text "JP_Airi_Android.zip" + emit_text "GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip" elif [[ "$url" == "{addressables_root}/MediaResources/Catalog/MediaCatalog.hash" ]]; then emit_text "{android_media_catalog_hash}" elif [[ -n "$output" && "$url" == "{addressables_root}/"* ]]; then @@ -601,9 +601,9 @@ fi addressables_root = TEST_ADDRESSABLES_ROOT, table_catalog_hash = xxhash32(b"ExcelDB.db ExcelDB.db"), windows_bundle_catalog_hash = xxhash32(b"FullPatch_000.zip"), - windows_media_catalog_hash = xxhash32(b"JP_Airi_Win.zip"), + windows_media_catalog_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip"), android_bundle_catalog_hash = xxhash32(b"FullPatch_001.zip"), - android_media_catalog_hash = xxhash32(b"JP_Airi_Android.zip"), + android_media_catalog_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip"), ) } diff --git a/scripts/official-full-pull-smoke.sh b/scripts/official-full-pull-smoke.sh index df5f7f0..1a347d5 100755 --- a/scripts/official-full-pull-smoke.sh +++ b/scripts/official-full-pull-smoke.sh @@ -85,7 +85,7 @@ run_step "02-first-full-pull" \ --progress \ --no-banner -require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "下载进度:总体" "first pull did not emit total download progress" +require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "下载进度:已完成" "first pull did not emit completed download progress" require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "单文件" "first pull did not emit single-file download progress" require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "校验结果:" "first pull did not emit verification summary" @@ -129,7 +129,7 @@ run_step "04-repair-after-damage" \ require_contains "${REPORT_DIR}/04-repair-after-damage.stdout.json" '"command": "repair"' "repair command report missing" require_contains "${REPORT_DIR}/04-repair-after-damage.stdout.json" '"status": "completed"' "repair command did not complete" -require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "下载进度:总体" "repair did not emit total download progress" +require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "下载进度:已完成" "repair did not emit completed download progress" require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "单文件" "repair did not emit single-file download progress" require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "校验结果:" "repair did not emit verification summary"