feat: add official sync startup banner

This commit is contained in:
2026-07-07 23:00:02 +08:00
parent ea0920e4e3
commit f704ec18c5
6 changed files with 55 additions and 6 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
4. 能生成官方全量 pull plan,执行真实下载,维护 `official-download-manifest.json` 4. 能生成官方全量 pull plan,执行真实下载,维护 `official-download-manifest.json`
5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;官方 seed `.hash` 使用 `xxHash32(seed=0)` 强校验。 5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;官方 seed `.hash` 使用 `xxHash32(seed=0)` 强校验。
6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair。 6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair。
7. `bat-official-sync --watch` 可常驻运行,正常检查默认每 1 小时一次;远端和本地一致时默认静默,失败后默认 60 秒快速重试;CLI 默认向 stderr 输出人类可读 progress log。 7. `bat-official-sync --watch` 可常驻运行,正常检查默认每 1 小时一次;远端和本地一致时默认静默,失败后默认 60 秒快速重试;CLI 默认向 stderr 输出 ASCII banner 和人类可读 progress log。
8. 非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一状态目录。 8. 非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一状态目录。
仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量下载 smoke test 尚未记录在仓库文档中。 仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量下载 smoke test 尚未记录在仓库文档中。
+1 -1
View File
@@ -82,7 +82,7 @@ cargo run -p bat-infrastructure --bin bat-official-sync -- \
--error-retry 60s --error-retry 60s
``` ```
CLI 默认把人类可读的阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、下载第 N/总数个 URL 等;成功后的稳定 JSON report 仍写到 stdout。需要给上层程序保留纯机器输出时可加 `--no-progress` CLI 默认启动时会向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并继续把人类可读的阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、下载第 N/总数个 URL 等;成功后的稳定 JSON report 仍写到 stdout。需要给上层程序保留纯机器输出时可加 `--no-progress`,只想关闭横幅但保留日志时可加 `--no-banner`
生产输出目录必须使用独立状态目录,不要指向已有客户端目录,也不要指向 `/home/wanye/D/BlueArchive` 这类人工维护或开发资源目录。 生产输出目录必须使用独立状态目录,不要指向已有客户端目录,也不要指向 `/home/wanye/D/BlueArchive` 这类人工维护或开发资源目录。
@@ -228,7 +228,7 @@ Linux 生产路径:
- pull plan 会同时包含 discovery URLs 和 content URLs - pull plan 会同时包含 discovery URLs 和 content URLs
- 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL - 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL
- `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策 - `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策
- `bat-official-sync` 默认向 stderr 输出人类可读 progress log,成功 JSON report 保持 stdout;支持 `--no-progress` 关闭进度日志,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,非 dry-run 使用 `.official-sync.lock` 防止并发写状态目录 - `bat-official-sync` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和人类可读 progress log,成功 JSON report 保持 stdout;支持 `--no-progress` 关闭人类输出,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,非 dry-run 使用 `.official-sync.lock` 防止并发写状态目录
- `OfficialUpdateService` 能读写 `official-bootstrap-cache.json`,并支持默认开启的 `audit_local` / `repair` CLI 行为 - `OfficialUpdateService` 能读写 `official-bootstrap-cache.json`,并支持默认开启的 `audit_local` / `repair` CLI 行为
- 下载层能在本地文件 size/BLAKE3/path 或 manifest 不匹配时重新下载 - 下载层能在本地文件 size/BLAKE3/path 或 manifest 不匹配时重新下载
- 官方 seed `.hash` mismatch 会导致下载失败,而不是降级为本地 BLAKE3 猜测 - 官方 seed `.hash` mismatch 会导致下载失败,而不是降级为本地 BLAKE3 猜测
+1 -1
View File
@@ -157,7 +157,7 @@ target/release/bat-official-sync
--watch --watch
``` ```
`--watch` 是 Rust 内部持久检查模式,正常情况下默认每 1 小时执行一次检查。远端和本地一致时默认静默;有远端变化或本地文件损坏时自动下载或 repair,并输出 JSON report。下载、发现或校验失败时默认 60 秒后重试,可显式加 `--error-retry 60s``--error-retry-seconds 60` 调整。默认进度日志写到 stderr,成功 JSON report 写到 stdout;如果由上层服务严格解析 stderr/stdout,可加 `--no-progress` `--watch` 是 Rust 内部持久检查模式,正常情况下默认每 1 小时执行一次检查。远端和本地一致时默认静默;有远端变化或本地文件损坏时自动下载或 repair,并输出 JSON report。下载、发现或校验失败时默认 60 秒后重试,可显式加 `--error-retry 60s``--error-retry-seconds 60` 调整。默认 ASCII banner 和进度日志写到 stderr,成功 JSON report 写到 stdout;如果由上层服务严格解析 stderr/stdout,可加 `--no-progress`,只想关闭横幅可加 `--no-banner`
### systemd service 示例 ### systemd service 示例
+1 -1
View File
@@ -229,7 +229,7 @@ cargo run -p bat-infrastructure --bin bat-official-sync -- \
--error-retry 60s --error-retry 60s
``` ```
`--interval` 是正常检查周期,默认 `1h``--error-retry` 是下载、发现或校验失败后的重试周期,默认 `60s`,也可以用 `--error-retry-seconds 60`。CLI 默认把人类可读的阶段进度日志写到 stderr,包括自动发现、server-info、marker、catalog、audit、download 和 snapshot 阶段;成功时 stdout 输出稳定 JSON report。需要纯机器输出时加 `--no-progress`,需要显式开启则用 `--progress`。错误时 stderr 输出 JSON errorwatch 模式下错误 JSON 的 `next_retry_seconds` 使用失败重试周期;如果未关闭 progress,错误 JSON 前可能已有进度日志。普通错误 exit `1`,状态目录锁冲突 exit `75` `--interval` 是正常检查周期,默认 `1h``--error-retry` 是下载、发现或校验失败后的重试周期,默认 `60s`,也可以用 `--error-retry-seconds 60`。CLI 默认启动时向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并把人类可读的阶段进度日志写到 stderr,包括自动发现、server-info、marker、catalog、audit、download 和 snapshot 阶段;成功时 stdout 输出稳定 JSON report。需要纯机器输出时加 `--no-progress`,需要显式开启则用 `--progress`;只想关闭横幅但保留日志时可加 `--no-banner`。错误时 stderr 输出 JSON errorwatch 模式下错误 JSON 的 `next_retry_seconds` 使用失败重试周期;如果未关闭 progress,错误 JSON 前可能已有 banner 和进度日志。普通错误 exit `1`,状态目录锁冲突 exit `75`
生产可以直接运行 `--watch`,也可以用 systemd service、容器或 Go 进程守护它。cron/systemd timer 仍可调用单次模式,但不再是 Rust 自动更新的唯一方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。生产目录应使用独立输出目录,不要指向现有客户端或人工维护的资源目录。非 dry-run 每轮会创建 `--output/.official-sync.lock`,防止并发写同一状态目录。 生产可以直接运行 `--watch`,也可以用 systemd service、容器或 Go 进程守护它。cron/systemd timer 仍可调用单次模式,但不再是 Rust 自动更新的唯一方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。生产目录应使用独立输出目录,不要指向现有客户端或人工维护的资源目录。非 dry-run 每轮会创建 `--output/.official-sync.lock`,防止并发写同一状态目录。
+50 -1
View File
@@ -13,6 +13,18 @@ const EXIT_ERROR: i32 = 1;
const EXIT_LOCKED: i32 = 75; const EXIT_LOCKED: i32 = 75;
const DEFAULT_WATCH_INTERVAL_SECONDS: u64 = 60 * 60; const DEFAULT_WATCH_INTERVAL_SECONDS: u64 = 60 * 60;
const DEFAULT_ERROR_RETRY_SECONDS: u64 = 60; const DEFAULT_ERROR_RETRY_SECONDS: u64 = 60;
const STARTUP_BANNER: &str = r#"
============================================================
____ _ _ _ _ _____ _ _ _ _
| __ )| |_ _ ___ / \ _ __ ___| |__ (_)_ _____|_ _|__ ___ | | | _(_) |_
| _ \| | | | |/ _ \/ _ \ | '__/ __| '_ \| \ \ / / _ \ | |/ _ \ / _ \| | |/ / | __|
| |_) | | |_| | __/ ___ \| | | (__| | | | |\ V / __/ | | (_) | (_) | | <| | |_
|____/|_|\__,_|\___/_/ \_\_| \___|_| |_|_| \_/ \___| |_|\___/ \___/|_|_|\_\_|\__|
BlueArchiveToolkit
Official Resource Sync
============================================================
"#;
fn main() { fn main() {
match run() { match run() {
@@ -41,6 +53,9 @@ fn main() {
fn run() -> anyhow::Result<()> { fn run() -> anyhow::Result<()> {
let options = parse_args()?; let options = parse_args()?;
if options.banner {
print_startup_banner();
}
if options.watch { if options.watch {
run_watch(options)?; run_watch(options)?;
} else { } else {
@@ -73,6 +88,7 @@ struct CliOptions {
quiet_up_to_date: bool, quiet_up_to_date: bool,
quiet_up_to_date_explicit: bool, quiet_up_to_date_explicit: bool,
progress: bool, progress: bool,
banner: bool,
} }
impl Default for CliOptions { impl Default for CliOptions {
@@ -85,6 +101,7 @@ impl Default for CliOptions {
quiet_up_to_date: false, quiet_up_to_date: false,
quiet_up_to_date_explicit: false, quiet_up_to_date_explicit: false,
progress: true, progress: true,
banner: true,
} }
} }
} }
@@ -175,6 +192,10 @@ impl ProgressLogger {
} }
} }
fn print_startup_banner() {
eprintln!("{STARTUP_BANNER}");
}
fn should_print_status(status: OfficialUpdateStatus, quiet_up_to_date: bool) -> bool { fn should_print_status(status: OfficialUpdateStatus, quiet_up_to_date: bool) -> bool {
!(quiet_up_to_date && status == OfficialUpdateStatus::UpToDate) !(quiet_up_to_date && status == OfficialUpdateStatus::UpToDate)
} }
@@ -290,9 +311,17 @@ fn parse_args_from(raw_args: impl IntoIterator<Item = String>) -> anyhow::Result
} }
"--progress" => { "--progress" => {
options.progress = true; options.progress = true;
options.banner = true;
} }
"--no-progress" => { "--no-progress" => {
options.progress = false; options.progress = false;
options.banner = false;
}
"--banner" => {
options.banner = true;
}
"--no-banner" => {
options.banner = false;
} }
"--help" | "-h" => { "--help" | "-h" => {
print_usage(&binary); print_usage(&binary);
@@ -334,7 +363,7 @@ fn print_usage(binary: &str) {
[--app-version 1.70.0] [--connection-group <name>] [--launcher-version 1.7.2] \ [--app-version 1.70.0] [--connection-group <name>] [--launcher-version 1.7.2] \
[--platforms Windows,Android] [--output official_update_output] [--snapshot official-sync-snapshot.json] \ [--platforms Windows,Android] [--output official_update_output] [--snapshot official-sync-snapshot.json] \
[--curl curl] [--unzip unzip] [--dry-run] [--plan] [--force] [--audit-local|--no-audit-local] [--repair|--no-repair] \ [--curl curl] [--unzip unzip] [--dry-run] [--plan] [--force] [--audit-local|--no-audit-local] [--repair|--no-repair] \
[--watch] [--interval 1h|60m|3600s] [--error-retry 60s] [--quiet-up-to-date|--no-quiet-up-to-date] [--progress|--no-progress]" [--watch] [--interval 1h|60m|3600s] [--error-retry 60s] [--quiet-up-to-date|--no-quiet-up-to-date] [--progress|--no-progress] [--banner|--no-banner]"
); );
} }
@@ -438,6 +467,7 @@ mod tests {
assert!(!options.quiet_up_to_date); assert!(!options.quiet_up_to_date);
assert!(!options.quiet_up_to_date_explicit); assert!(!options.quiet_up_to_date_explicit);
assert!(options.progress); assert!(options.progress);
assert!(options.banner);
} }
#[test] #[test]
@@ -520,12 +550,31 @@ mod tests {
fn parses_progress_flags() { fn parses_progress_flags() {
let options = parse(&["bat-official-sync"]).unwrap(); let options = parse(&["bat-official-sync"]).unwrap();
assert!(options.progress); assert!(options.progress);
assert!(options.banner);
let options = parse(&["bat-official-sync", "--no-progress"]).unwrap(); let options = parse(&["bat-official-sync", "--no-progress"]).unwrap();
assert!(!options.progress); assert!(!options.progress);
assert!(!options.banner);
let options = parse(&["bat-official-sync", "--no-progress", "--progress"]).unwrap(); let options = parse(&["bat-official-sync", "--no-progress", "--progress"]).unwrap();
assert!(options.progress); assert!(options.progress);
assert!(options.banner);
}
#[test]
fn parses_banner_flags() {
let options = parse(&["bat-official-sync", "--no-banner"]).unwrap();
assert!(options.progress);
assert!(!options.banner);
let options = parse(&["bat-official-sync", "--no-progress", "--banner"]).unwrap();
assert!(!options.progress);
assert!(options.banner);
}
#[test]
fn startup_banner_contains_product_name() {
assert!(STARTUP_BANNER.contains("BlueArchiveToolkit"));
} }
#[test] #[test]