diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md index 6b79044..9e2403e 100644 --- a/CURRENT_STATUS.md +++ b/CURRENT_STATUS.md @@ -14,17 +14,17 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口: -1. 首次运行可以通过 `--auto-discover` 从官方 HTTP metadata 解析 `GameMainConfig`,自动获得 `app-version`、`connection-group` 和 `server-info`。 +1. 首次运行可以通过 `--auto-discover` 从官方 HTTP metadata 解析 `GameMainConfig`,自动获得 `app-version`、`connection-group` 和 `server-info`;解密出的 `GameMainConfig` JSON 会校验已知字段,避免把错误解密结果当成成功。 2. 不安装、不启动、不依赖已安装官方启动器。 3. 默认平台为 `Windows + Android`。 4. 能生成官方全量 pull plan,执行真实下载,维护 release 内的 `official-download-manifest.json`。 -5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;所有 `.zip` 在下载验收、复用、本地 audit/verify 时做 ZIP 结构校验;官方 seed `.hash` 使用 `xxHash32(seed=0)` 强校验。 +5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;所有 `.zip` 在下载验收、复用、本地 audit/verify 时做 ZIP 结构校验;官方 seed `.hash` 使用标准 `xxHash32(seed=0)` 强校验(早期实现的非标准 avalanche 常量已修正)。 6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair、403/404/5xx 分类重试、下载 quarantine 诊断,以及旧 launcher 包官方 primary/backup CDN 切换。 7. `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`。 8. 远端 snapshot 未变化但输出目录为空时,会按首次运行执行全量拉取;官方 seed `.hash` 校验失败时会清理对应 manifest 条目,避免失败产物被后续本地 audit 误判为可复用。 9. 默认资源目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`;资源目录是发布根目录,包含 `current` symlink、`versions/` 和 `.staging/`,非 dry-run 会先写 staging,校验完成后发布 versioned 目录并原子切换 `current`;后台状态目录包含 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期 `bat-control.lock`;非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一资源目录,live daemon 会阻止前台写命令直接修改它正在管理的同一目录。 10. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。 -11. `/official-version-state.json` 会明确保存当前已完成版本、正在拉取版本、上一个可用版本和失败版本;`bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、结构化日志路径和轮转日志路径。 +11. `/official-version-state.json` 会明确保存当前已完成版本、正在拉取版本、上一个可用版本和失败版本;重复失败按 app version、bundle version、Addressables root 和错误原因合并只保留最新一条,同一版本后续发布成功时会清理对应失败记录;`bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近失败版本和原因、结构化日志路径和轮转日志路径,人类可读输出不会把完整版本状态 JSON 内联打印。 12. 资源导入链路已支持 CAS + `ResourceRepository` 索引写入,AssetBundle 导入会记录 UnityFS 摘要,TextAsset/Table/Media 会按类型分类;当前/上一个/结构变化 catalog、403/404、hash mismatch 均有离线回归 fixture。 仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量拉取 smoke 已固化为可重复脚本和 runbook(G-018 已关闭),当前正在进行长期运行测试,运行报告将在后续提供;真实大文件产物与运行报告默认保存在 `/tmp` 隔离目录,不纳入 Git。 @@ -261,7 +261,7 @@ cargo run -p bat-infrastructure --bin bat -- \ ## 6. 当前阻塞项 -GitHub issue 状态:#4–#15 已全部关闭,当前 open 的是 #1(P1)、#2(P2)、#3(P2)。 +GitHub issue 状态:#4–#16 已全部关闭(#16 为 daemon status 版本失败输出与重复堆积 bug,已由失败版本去重和状态输出优化修复),当前 open 的是 #1(P1)、#2(P2)、#3(P2)。 下一阶段必须优先完成: diff --git a/infrastructure/src/bin/bat_official_sync.rs b/infrastructure/src/bin/bat_official_sync.rs index edcb149..e009efa 100644 --- a/infrastructure/src/bin/bat_official_sync.rs +++ b/infrastructure/src/bin/bat_official_sync.rs @@ -1894,7 +1894,7 @@ fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> { print_json_field(value, "next_check_unix_seconds", "下次检查时间"); print_json_field(value, "current_stage", "当前阶段"); print_json_field(value, "current_message", "当前消息"); - print_json_field(value, "download_progress", "下载进度"); + print_daemon_download_progress_json_summary(value.get("download_progress")); print_json_field(value, "version_state_path", "版本状态路径"); print_daemon_version_state_json_summary(value.get("version_state")); print_json_field(value, "resource_output_root", "资源目录"); @@ -1935,6 +1935,26 @@ fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> { Ok(()) } +fn print_daemon_download_progress_json_summary(value: Option<&serde_json::Value>) { + let Some(value) = value else { + return; + }; + if value.is_null() { + return; + } + print_field( + "下载进度", + format_daemon_download_progress_json(value) + .unwrap_or_else(|error| format!("无法解析:{error}")), + ); +} + +fn format_daemon_download_progress_json(value: &serde_json::Value) -> Result { + let progress = serde_json::from_value::(value.clone()) + .map_err(|error| error.to_string())?; + Ok(format_daemon_download_progress(&progress)) +} + fn print_daemon_version_state_json_summary(value: Option<&serde_json::Value>) { let Some(value) = value else { return; @@ -4337,6 +4357,49 @@ mod tests { assert_eq!(json["zip_structure_verified_count"], 3); } + #[test] + fn daemon_rpc_download_progress_formats_as_human_text() { + let progress = serde_json::json!({ + "index": 2, + "total": 19, + "url": "https://prod-clientpatch.bluearchiveyostar.com/file.zip", + "status": "finished", + "bytes": 100, + "transferred_bytes": 100 + }); + + let formatted = format_daemon_download_progress_json(&progress).unwrap(); + + assert_eq!( + formatted, + "2/19 finished https://prod-clientpatch.bluearchiveyostar.com/file.zip" + ); + assert!(!formatted.contains('{')); + } + + #[test] + fn daemon_rpc_failed_download_progress_formats_as_human_text() { + let progress = serde_json::json!({ + "index": 3, + "total": 19, + "url": "https://prod-clientpatch.bluearchiveyostar.com/missing.zip", + "status": "failed", + "failure_kind": "http_404", + "failure_http_status": 404, + "failure_retryable": false, + "failure_attempts": 1, + "quarantined": true + }); + + let formatted = format_daemon_download_progress_json(&progress).unwrap(); + + assert_eq!( + formatted, + "3/19 failed kind=http_404 http=404 retryable=false attempts=1 quarantined=true https://prod-clientpatch.bluearchiveyostar.com/missing.zip" + ); + assert!(!formatted.contains('{')); + } + #[test] fn daemon_socket_path_uses_state_dir() { assert_eq!(