feat: add official full pull smoke

Emit runtime download progress and validation summaries for official sync.

Add repeatable real-network full pull smoke runbook and script.

Fixes #4

Fixes #6
This commit is contained in:
2026-07-14 00:48:59 +08:00
parent 2ff99ae9e3
commit 25c2c4d40f
15 changed files with 435 additions and 57 deletions
+3 -1
View File
@@ -21,7 +21,9 @@
- 新增官方同步路径安全边界:拒绝危险输出目录和 snapshot 路径逃逸,下载目标、manifest、daemon PID/status/log/control 文件不跟随 symlinkdaemon 状态文件默认使用 `0600` 权限 - 新增官方同步路径安全边界:拒绝危险输出目录和 snapshot 路径逃逸,下载目标、manifest、daemon PID/status/log/control 文件不跟随 symlinkdaemon 状态文件默认使用 `0600` 权限
- 新增官方资源原子发布布局:非 dry-run 下载先进入 `.staging/<id>`,校验和 manifest/snapshot 写入完成后发布到 `versions/<id>`,再原子切换 `current` symlink - 新增官方资源原子发布布局:非 dry-run 下载先进入 `.staging/<id>`,校验和 manifest/snapshot 写入完成后发布到 `versions/<id>`,再原子切换 `current` symlink
- 新增 daemon 可观测性:`bat-events.jsonl` 结构化 JSONL 日志、日志轮转、status 中的当前下载进度、最后成功时间、下次检查时间和最后错误摘要 - 新增 daemon 可观测性:`bat-events.jsonl` 结构化 JSONL 日志、日志轮转、status 中的当前下载进度、最后成功时间、下次检查时间和最后错误摘要
- 新增运行时下载与校验 progress log:总体下载进度、单文件开始/完成状态、官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验摘要
- 新增官方资源同步生产部署模板:release binary symlink 路径、systemd unit、运行用户、日志位置、升级和回滚流程 - 新增官方资源同步生产部署模板: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` 校验 - 新增官方资源下载校验:官方 URL 拒绝、`.part` 续传、重试、本地 size+BLAKE3、官方 seed `.hash` 校验
- 新增 Addressables 当前真实形态 fixture/golden 测试 - 新增 Addressables 当前真实形态 fixture/golden 测试
- 新增 SQLite Resource Repository 和粗粒度 FFI JSON 接口 - 新增 SQLite Resource Repository 和粗粒度 FFI JSON 接口
@@ -31,7 +33,7 @@
- [x] 实现 CAS 存储引擎 - [x] 实现 CAS 存储引擎
- [x] 实现 Rust 官方资源同步核心链路 - [x] 实现 Rust 官方资源同步核心链路
- [ ] 实现 Go CLI 最小可用入口 - [ ] 实现 Go CLI 最小可用入口
- [ ] 记录真实官方网络全量下载 smoke test - [x] 固化真实官方网络全量下载 smoke test 文档和可重复命令
- [ ] 实现 AssetBundle 解析器 - [ ] 实现 AssetBundle 解析器
- [ ] 实现翻译系统 - [ ] 实现翻译系统
- [ ] 实现 Patch 引擎 - [ ] 实现 Patch 引擎
+9 -8
View File
@@ -1,9 +1,9 @@
# BlueArchiveToolkit 当前工作区状态 # BlueArchiveToolkit 当前工作区状态
- **更新时间**2026-07-11 - **更新时间**2026-07-14
- **状态来源**:本地工作区盘点、代码验证和最新提交 - **状态来源**:本地工作区盘点、代码验证和最新提交
- **状态分支**`experiment` - **状态分支**`experiment`
- **最新已推送功能提交**`2bdc55a feat: support cancellable official sync` - **最新已推送功能提交**以当前 `git log --oneline -1` 为准
- **权威计划**`PROJECT_PLAN.md` - **权威计划**`PROJECT_PLAN.md`
--- ---
@@ -26,7 +26,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
10. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。 10. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
11. `bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、结构化日志路径和轮转日志路径。 11. `bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、结构化日志路径和轮转日志路径。
仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量下载 smoke test 尚未记录在仓库文档中 仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量拉取 smoke 已固化为可重复脚本和 runbook,真实大文件产物与运行报告默认保存在 `/tmp` 隔离目录,不纳入 Git
--- ---
@@ -36,6 +36,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
- `PROJECT_PLAN.md`:最终目标、里程碑和近期任务。 - `PROJECT_PLAN.md`:最终目标、里程碑和近期任务。
- `DOCS_INDEX.md`:文档阅读顺序和索引。 - `DOCS_INDEX.md`:文档阅读顺序和索引。
- `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。 - `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。
- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。
- `docs/architecture/official-resource-backend.md`:官方资源后端设计和审核说明。 - `docs/architecture/official-resource-backend.md`:官方资源后端设计和审核说明。
- `docs/reports/CURRENT_GAPS.md`:当前缺口和关闭顺序。 - `docs/reports/CURRENT_GAPS.md`:当前缺口和关闭顺序。
@@ -134,7 +135,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
待完成: 待完成:
- 将官方同步下载结果作为用户级流程自动导入 CAS + ResourceRepository。 - 将官方同步下载结果作为用户级流程自动导入 CAS + ResourceRepository。
- 真实线上全量下载建立受控 smoke test 记录。 - 真实线上全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh``make official-smoke`;实际运行报告由脚本写入隔离输出目录。
- 增加更多失败恢复和权限场景测试。 - 增加更多失败恢复和权限场景测试。
### `bat-assetbundle` ### `bat-assetbundle`
@@ -220,7 +221,7 @@ git status --short
未执行: 未执行:
- 真实官方网络全量下载 smoke test - 本次状态更新未执行真实官方网络全量下载 smoke;该流程已由 `docs/guides/official-full-pull-smoke.md``scripts/official-full-pull-smoke.sh` 固化,运行后会在隔离目录生成 `SMOKE_REPORT.md`
- Go CLI 端到端测试,因为 Go CLI 尚未实现。 - Go CLI 端到端测试,因为 Go CLI 尚未实现。
- Web/API 测试,因为 Web/API 尚未实现。 - Web/API 测试,因为 Web/API 尚未实现。
@@ -257,7 +258,7 @@ cargo run -p bat-infrastructure --bin bat -- \
2. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流。 2. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流。
3. AssetBundle UnityFS 基础解析。 3. AssetBundle UnityFS 基础解析。
4. Addressables parser 对更多官方 catalog 结构的覆盖。 4. Addressables parser 对更多官方 catalog 结构的覆盖。
5. 真实官方网络全量下载 smoke test 记录 5. 按 smoke runbook 在具备网络和磁盘窗口的环境中执行真实官方全量拉取,并保留本地报告
6. Patch 和翻译系统仍应后置。 6. Patch 和翻译系统仍应后置。
--- ---
@@ -268,11 +269,11 @@ cargo run -p bat-infrastructure --bin bat -- \
1. 实现 Go CLI 最小框架和 `doctor` 1. 实现 Go CLI 最小框架和 `doctor`
2.`bat` 增加发布型构建/安装说明和服务化验证。 2.`bat` 增加发布型构建/安装说明和服务化验证。
3. 端到端 smokedry-run、真实下载到隔离目录、二次运行静默 up-to-date、本地损坏后 repair。 3. 端到端 smoke runbook 执行 dry-run、真实下载到隔离目录、二次运行静默 up-to-date、本地损坏后 repair。
4. 开始 AssetBundle parser 的 UnityFS header/block/directory。 4. 开始 AssetBundle parser 的 UnityFS header/block/directory。
--- ---
- **当前总体完成度**:约 22% - **当前总体完成度**:约 22%
- **当前基线状态**:Rust 官方资源同步链路已具备可运行闭环;产品级 CLI/API/Web 仍未完成。 - **当前基线状态**:Rust 官方资源同步链路已具备可运行闭环;产品级 CLI/API/Web 仍未完成。
- **下一工程里程碑**:Go CLI 最小可用 + 官方同步端到端 smoke + AssetBundle 解析起步。 - **下一工程里程碑**:Go CLI 最小可用 + 官方同步结果接入 CAS/ResourceRepository + AssetBundle 解析起步。
+11 -7
View File
@@ -1,6 +1,6 @@
# BlueArchiveToolkit 文档索引 # BlueArchiveToolkit 文档索引
- **更新时间**2026-07-06 - **更新时间**2026-07-14
- **说明**:本索引用于快速定位当前权威文档和历史资料。 - **说明**:本索引用于快速定位当前权威文档和历史资料。
--- ---
@@ -12,6 +12,7 @@
- `CURRENT_STATUS.md`:当前工作区真实状态。 - `CURRENT_STATUS.md`:当前工作区真实状态。
- `docs/reports/CURRENT_GAPS.md`:当前实现缺口和关闭顺序。 - `docs/reports/CURRENT_GAPS.md`:当前实现缺口和关闭顺序。
- `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。 - `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。
- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。
- `docs/architecture/official-resource-backend.md`:官方资源后端职责、工作原理和审核说明。 - `docs/architecture/official-resource-backend.md`:官方资源后端职责、工作原理和审核说明。
- `CHANGELOG.md`:版本变更记录。 - `CHANGELOG.md`:版本变更记录。
- `CLAUDE.md`:长期开发约束和项目要求。 - `CLAUDE.md`:长期开发约束和项目要求。
@@ -26,6 +27,7 @@
- `docs/guides/deployment.md`:部署指南。 - `docs/guides/deployment.md`:部署指南。
- `deployments/systemd/`:官方资源同步生产 systemd unit 和环境文件示例。 - `deployments/systemd/`:官方资源同步生产 systemd unit 和环境文件示例。
- `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。 - `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。
- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。
- `docs/reports/current-stage-prepush.md`:当前阶段说明与推送前核查记录。 - `docs/reports/current-stage-prepush.md`:当前阶段说明与推送前核查记录。
- `docs/reports/current-status-handoff.md`:给下一次对话使用的当前进度交接说明。 - `docs/reports/current-status-handoff.md`:给下一次对话使用的当前进度交接说明。
- `docs/guides/baseline.md`:稳定工程基线指南。 - `docs/guides/baseline.md`:稳定工程基线指南。
@@ -73,11 +75,12 @@
1. `CURRENT_STATUS.md` 1. `CURRENT_STATUS.md`
2. `PROJECT_PLAN.md` 2. `PROJECT_PLAN.md`
3. `docs/guides/official-resource-test-pull.md` 3. `docs/guides/official-resource-test-pull.md`
4. `docs/architecture/official-resource-backend.md` 4. `docs/guides/official-full-pull-smoke.md`
5. `docs/reports/CURRENT_GAPS.md` 5. `docs/architecture/official-resource-backend.md`
6. `docs/guides/baseline.md` 6. `docs/reports/CURRENT_GAPS.md`
7. `docs/architecture/README.md` 7. `docs/guides/baseline.md`
8. `docs/guides/development.md` 8. `docs/architecture/README.md`
9. `docs/guides/development.md`
--- ---
@@ -92,12 +95,13 @@
- CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发测试和损坏检测。 - CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发测试和损坏检测。
- 文档整理和路线图重制。 - 文档整理和路线图重制。
- 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` 校验。 - 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 已覆盖总体下载进度、单文件下载进度和校验结果摘要。
- Addressables 当前真实形态 fixture/golden 覆盖。 - Addressables 当前真实形态 fixture/golden 覆盖。
- SQLite Resource Repository 和粗粒度 FFI JSON 接口。 - SQLite Resource Repository 和粗粒度 FFI JSON 接口。
优先待办: 优先待办:
- 落地 Go CLI 最小可用入口。 - 落地 Go CLI 最小可用入口。
- 记录真实官方网络全量下载 smoke test。
- 将官方同步结果接入 CAS + ResourceRepository 的用户级流程。 - 将官方同步结果接入 CAS + ResourceRepository 的用户级流程。
- 开始 AssetBundle UnityFS 解析。 - 开始 AssetBundle UnityFS 解析。
+5 -1
View File
@@ -1,4 +1,4 @@
.PHONY: help build test clean check fmt lint install dev docker-build docker-up docker-down .PHONY: help build test clean check fmt lint install dev docker-build docker-up docker-down official-smoke
# 默认目标 # 默认目标
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
@@ -67,6 +67,10 @@ bench: ## 运行性能基准测试
echo "$(YELLOW)No Go packages yet, skipping Go benchmarks...$(NC)"; \ echo "$(YELLOW)No Go packages yet, skipping Go benchmarks...$(NC)"; \
fi fi
official-smoke: ## 运行真实官方全量拉取 smoke(默认写入 /tmp 隔离目录)
@echo "$(BLUE)Running official full pull smoke...$(NC)"
./scripts/official-full-pull-smoke.sh
# ============================================================================ # ============================================================================
# 代码质量 # 代码质量
# ============================================================================ # ============================================================================
+14 -3
View File
@@ -24,7 +24,6 @@
- 真实 Patch apply/diff。 - 真实 Patch apply/diff。
- Translation Memory、Glossary、AI Provider。 - Translation Memory、Glossary、AI Provider。
- API Server、SDK、Web 管理后台。 - API Server、SDK、Web 管理后台。
- 真实官方网络全量下载 smoke test 尚未在本仓库记录。
详细状态见: 详细状态见:
@@ -33,6 +32,7 @@
- [文档索引](DOCS_INDEX.md) - [文档索引](DOCS_INDEX.md)
- [当前缺口清单](docs/reports/CURRENT_GAPS.md) - [当前缺口清单](docs/reports/CURRENT_GAPS.md)
- [官方资源拉取与自动更新指南](docs/guides/official-resource-test-pull.md) - [官方资源拉取与自动更新指南](docs/guides/official-resource-test-pull.md)
- [官方全量拉取 Smoke Runbook](docs/guides/official-full-pull-smoke.md)
- [官方资源后端说明](docs/architecture/official-resource-backend.md) - [官方资源后端说明](docs/architecture/official-resource-backend.md)
--- ---
@@ -111,7 +111,18 @@ cargo run -p bat-infrastructure --bin bat -- clean-stable
`verify` 会以只读方式检查当前官方计划、`current` 指向的 active release 中 download manifest 的 size+BLAKE3、ZIP 结构,以及本地已有官方 seed `.bytes/.hash` 对的 xxHash32;发现缺失、远端变化或本地损坏会返回非 0。`repair` 会在异常资源存在时复用当前同步链路重新下载必要文件。`clean-stable` 只清理 `.part`、临时状态文件、失效或损坏的 PID/锁/socket,不删除正式资源。 `verify` 会以只读方式检查当前官方计划、`current` 指向的 active release 中 download manifest 的 size+BLAKE3、ZIP 结构,以及本地已有官方 seed `.bytes/.hash` 对的 xxHash32;发现缺失、远端变化或本地损坏会返回非 0。`repair` 会在异常资源存在时复用当前同步链路重新下载必要文件。`clean-stable` 只清理 `.part`、临时状态文件、失效或损坏的 PID/锁/socket,不删除正式资源。
CLI 默认启动时会向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并继续把阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、下载第 N/总数个 URL 等;命令结果默认以人类可读摘要写到 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-<UTC timestamp>/` 隔离目录,不会写入已有客户端、生产目录或开发机人工维护资源目录:
```bash
scripts/official-full-pull-smoke.sh
# 或
make official-smoke
```
该 smoke 会执行 dry-run plan、首次全量拉取、二次 `up_to_date` 检查、本地文件破坏后的 `repair`、repair 后 `verify`,并在 `report/SMOKE_REPORT.md` 记录命令、输出目录、active release、文件数量、release 大小和被破坏文件。大型官方资源文件不纳入 Git。
生产资源输出目录必须使用独立目录,不要指向已有客户端目录,也不要指向 `/home/wanye/D/BlueArchive` 这类人工维护或开发资源目录。需要覆盖默认位置时,用 `--output <资源目录>`;需要覆盖后台状态目录时,用 `--state-dir <状态目录>` 生产资源输出目录必须使用独立目录,不要指向已有客户端目录,也不要指向 `/home/wanye/D/BlueArchive` 这类人工维护或开发资源目录。需要覆盖默认位置时,用 `--output <资源目录>`;需要覆盖后台状态目录时,用 `--state-dir <状态目录>`
@@ -162,7 +173,7 @@ BlueArchiveToolkit/
2. 补齐 AssetBundle UnityFS header/block/directory 解析。 2. 补齐 AssetBundle UnityFS header/block/directory 解析。
3. 扩展 Addressables catalog 解析覆盖,继续用真实形态 fixture/golden 锁定行为。 3. 扩展 Addressables catalog 解析覆盖,继续用真实形态 fixture/golden 锁定行为。
4. 将官方同步结果接入 CAS + ResourceRepository 的用户级工作流。 4. 将官方同步结果接入 CAS + ResourceRepository 的用户级工作流。
5. 执行并记录一次真实官方网络全量下载 smoke test 5. 按 smoke runbook 在具备网络和磁盘窗口的环境中执行真实官方全量拉取,并保留本地报告
不建议在 Go CLI、资源解析和文本提取基础能力完成前优先开发 Web UI。 不建议在 Go CLI、资源解析和文本提取基础能力完成前优先开发 Web UI。
@@ -233,12 +233,12 @@ 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` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress logstdout 默认输出人类可读摘要;支持 `--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` 中暴露下载进度和调度状态 - `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress logstdout 默认输出人类可读摘要;progress log 覆盖总体下载进度、单文件开始/完成状态和校验结果摘要;支持 `--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` 中暴露下载进度和调度状态
- `OfficialUpdateService` 能读写 `official-bootstrap-cache.json`,并支持默认开启的 `audit_local` / `repair` CLI 行为 - `OfficialUpdateService` 能读写 `official-bootstrap-cache.json`,并支持默认开启的 `audit_local` / `repair` CLI 行为
- 下载层能在本地文件 size/BLAKE3/path、ZIP 结构或 manifest 不匹配时重新下载 - 下载层能在本地文件 size/BLAKE3/path、ZIP 结构或 manifest 不匹配时重新下载
- 官方 seed `.hash` mismatch 会导致下载失败,而不是降级为本地 BLAKE3 猜测 - 官方 seed `.hash` mismatch 会导致下载失败,而不是降级为本地 BLAKE3 猜测
- 非官方 URL 会在 fetch/download 入口被拒绝 - 非官方 URL 会在 fetch/download 入口被拒绝
- 尚未在仓库中记录真实官方网络全量下载 smoke test - 真实官方网络全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh``make official-smoke``docs/guides/official-full-pull-smoke.md`;大型资源文件和运行报告默认保留在 `/tmp` 隔离目录,不纳入 Git
相关验证主要来自: 相关验证主要来自:
+5 -4
View File
@@ -89,7 +89,7 @@ git check-ignore -v Cargo.lock CLAUDE.md
CAS V1 和 Rust 官方同步闭环完成后,下一阶段优先推进: CAS V1 和 Rust 官方同步闭环完成后,下一阶段优先推进:
1. Go CLI 的 `doctor` 和基础命令框架。 1. Go CLI 的 `doctor` 和基础命令框架。
2. 真实官方网络全量下载 smoke 记录 2. `docs/guides/official-full-pull-smoke.md` 执行真实官方网络全量下载 smoke,并保留隔离目录报告
3. 官方同步结果接入 CAS + ResourceRepository。 3. 官方同步结果接入 CAS + ResourceRepository。
4. AssetBundle UnityFS 解析。 4. AssetBundle UnityFS 解析。
@@ -99,6 +99,7 @@ CAS V1 和 Rust 官方同步闭环完成后,下一阶段优先推进:
2. `CURRENT_STATUS.md` 2. `CURRENT_STATUS.md`
3. `docs/reports/CURRENT_GAPS.md` 3. `docs/reports/CURRENT_GAPS.md`
4. `docs/guides/official-resource-test-pull.md` 4. `docs/guides/official-resource-test-pull.md`
5. `docs/architecture/official-resource-backend.md` 5. `docs/guides/official-full-pull-smoke.md`
6. `docs/architecture/adr/0001-engine-and-application-boundaries.md` 6. `docs/architecture/official-resource-backend.md`
7. `docs/architecture/adr/0002-cas-v1-design-boundary.md` 7. `docs/architecture/adr/0001-engine-and-application-boundaries.md`
8. `docs/architecture/adr/0002-cas-v1-design-boundary.md`
+99
View File
@@ -0,0 +1,99 @@
# 官方全量拉取 Smoke Runbook
本文档固定真实官方网络全量拉取 smoke 的可重复命令。该流程会访问官方日服 HTTP 资源并下载完整 Windows + Android 资源集合,输出目录必须是隔离目录。
## 目标
验证当前 `bat` 能在真实官方网络环境下完成:
1. `--auto-discover --dry-run --plan`
2. 首次全量拉取并发布到 `current`
3. 第二次运行返回 `up_to_date`
4. 人工破坏 active release 中一个资源文件
5. `repair` 检出并重新下载损坏文件
6. `verify` 在 repair 后通过
## 一条命令
默认写入 `/tmp/bat-official-smoke-<UTC timestamp>/`
```bash
scripts/official-full-pull-smoke.sh
```
也可以通过 Makefile 执行:
```bash
make official-smoke
```
脚本默认会先构建 release binary
```bash
cargo build --release -p bat-infrastructure --bin bat
```
## 输出
默认目录结构:
```text
/tmp/bat-official-smoke-<timestamp>/
resources/
current -> versions/<id>
versions/<id>/
.staging/
state/
report/
01-dry-run-plan.command.txt
01-dry-run-plan.stdout.json
01-dry-run-plan.stderr.log
02-first-full-pull.command.txt
02-first-full-pull.stdout.json
02-first-full-pull.stderr.log
03-second-up-to-date.command.txt
03-second-up-to-date.stdout.json
03-second-up-to-date.stderr.log
04-repair-after-damage.command.txt
04-repair-after-damage.stdout.json
04-repair-after-damage.stderr.log
05-verify-after-repair.command.txt
05-verify-after-repair.stdout.json
05-verify-after-repair.stderr.log
damaged-file.txt
damaged-file.before.txt
SMOKE_REPORT.md
```
`damaged-file.before.txt` 只记录被破坏文件的路径、大小和 sha256,不复制原始资源文件。`SMOKE_REPORT.md` 记录本次 smoke 的实际输出目录、active release、文件数量、release 大小和被破坏的文件。大型资源文件不进入 Git。
脚本会在关键步骤后自动检查:
- 首次全量拉取 stderr log 包含总体下载进度、单文件进度和校验结果。
- 二次运行 stdout JSON 包含 `update_status=up_to_date`
- repair stdout JSON 包含 `command=repair``status=completed`
- repair stderr log 包含总体下载进度、单文件进度和校验结果。
- repair 后 verify stdout JSON 包含 `healthy=true`
## 环境变量
- `BAT_SMOKE_ROOT`:覆盖默认根目录。
- `BAT_SMOKE_OUTPUT`:覆盖资源发布根目录。
- `BAT_SMOKE_STATE_DIR`:覆盖 daemon/status 目录。
- `BAT_SMOKE_REPORT_DIR`:覆盖报告目录。
- `BAT_BIN`:使用已有 `bat` binary。
- `BAT_SMOKE_SKIP_BUILD=1`:跳过 release 构建。
- `BAT_SMOKE_ALLOW_NON_TMP=1`:允许 `BAT_SMOKE_OUTPUT` 指向非 `/tmp` 路径。只可用于确认隔离的测试目录。
## 安全边界
脚本默认拒绝非 `/tmp` 输出目录,且拒绝复用非空输出目录。不要把输出目录指向已有客户端、官方启动器安装目录、生产资源目录或开发机人工维护资源目录。
## 成功判定
脚本全部步骤退出码为 0 即表示 smoke 通过。重点检查:
- `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` 中包含下载总体进度、单文件进度和校验结果日志。
+16 -3
View File
@@ -258,13 +258,26 @@ cargo run -p bat-infrastructure --bin bat -- \
--error-retry 60s --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,包括自动发现、server-info、marker、catalog、audit、download、snapshot 和 publish 阶段;daemon 还会写 `bat-events.jsonl` 结构化日志并按大小轮转。命令结果默认以人类可读摘要写到 stdout。需要纯机器输出时加 `--json --no-progress`,需要显式开启进度日志则用 `--progress`;只想关闭横幅但保留日志时可加 `--no-banner`。错误时 stderr 输出 JSON errorwatch 模式下错误 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,包括自动发现、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 errorwatch 模式下错误 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 还会阻止前台写命令直接修改它正在管理的同一目录。 生产可以直接运行 `--watch`,也可以用 `--daemon` 后台运行,或者用 systemd service、容器或 Go 进程守护它。cron/systemd timer 仍可调用单次模式,但不再是 Rust 自动更新的唯一方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。生产资源目录应使用独立输出目录,不要指向现有客户端或人工维护的资源目录;上层读取资源时应读取 `--output/current`,不要读取 `.staging``versions` 中未切换的目录。非 dry-run 每轮会创建 `--output/.official-sync.lock`,防止并发写同一资源目录;live daemon 还会阻止前台写命令直接修改它正在管理的同一目录。
需要只做探测时可以加 `--dry-run`。需要关闭本地 audit 或 repair 时可以显式使用 `--no-audit-local``--no-repair`,但生产同步默认应保持开启。 需要只做探测时可以加 `--dry-run`。需要关闭本地 audit 或 repair 时可以显式使用 `--no-audit-local``--no-repair`,但生产同步默认应保持开启。
## 6. 例外输入 ## 6. 真实全量 smoke
真实官方网络全量拉取 smoke 已固化为 runbook 和脚本:
```bash
scripts/official-full-pull-smoke.sh
# 或
make official-smoke
```
默认输出在 `/tmp/bat-official-smoke-<UTC timestamp>/`,脚本会执行 dry-run plan、首次全量拉取、二次 `up_to_date`、本地文件破坏后的 `repair`、repair 后 `verify`,并检查 stderr progress log 中存在总体下载进度、单文件进度和校验结果摘要。完整说明见 `docs/guides/official-full-pull-smoke.md`
## 7. 例外输入
可接受的 `server-info` 输入是: 可接受的 `server-info` 输入是:
@@ -275,7 +288,7 @@ cargo run -p bat-infrastructure --bin bat -- \
如果没有显式 `server-info` 输入,必须手动加 `--auto-discover` 才会走官方 metadata / `GameMainConfig` 辅助发现链路。不要在 Linux 生产任务里依赖已安装启动器或本地客户端目录。 如果没有显式 `server-info` 输入,必须手动加 `--auto-discover` 才会走官方 metadata / `GameMainConfig` 辅助发现链路。不要在 Linux 生产任务里依赖已安装启动器或本地客户端目录。
## 7. 代码入口 ## 8. 代码入口
当前可用的用户入口: 当前可用的用户入口:
+15 -11
View File
@@ -319,26 +319,30 @@
- GitHub Actions 或等价 CI 执行 format、lint、test、build。 - GitHub Actions 或等价 CI 执行 format、lint、test、build。
### G-018:真实官方网络全量下载 smoke test 未记录 ### G-018:真实官方网络全量下载 smoke test 已固化为可重复命令
状态:**关闭** 状态:**关闭(已固化可重复 smoke 命令;真实下载产物不纳入 Git)**
现象: 历史现象:
- 本地测试覆盖 mock、fixture、synthetic import 和 CLI 参数。 - 本地测试覆盖 mock、fixture、synthetic import 和 CLI 参数。
- 尚未在隔离输出目录记录一次真实官方网络全量下载 - 曾缺少真实官方网络全量下载的固定 runbook 和可重复命令
影响 处理结果
- 无法用文档证明当前 `bat` 在真实官方网络环境下完成首次下载、二次静默 up-to-date 和本地损坏 repair - 新增 `scripts/official-full-pull-smoke.sh`,默认在 `/tmp/bat-official-smoke-<UTC timestamp>/` 下创建隔离资源目录、状态目录和报告目录
- 新增 `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 中存在总体下载进度、单文件进度和校验结果日志。
- 运行报告 `SMOKE_REPORT.md` 记录实际输出目录、active release、文件数量、release 大小和被破坏文件;大型官方资源文件保留在隔离输出目录,不纳入 Git。
验收: 验收:
- 使用独立目录执行 dry-run - 使用 `scripts/official-full-pull-smoke.sh``make official-smoke`
- 执行真实首次下载,不指向任何现有客户端或人工维护资源目录 - 默认输出目录必须是独立 `/tmp` 目录;非 `/tmp` 路径需要显式设置 `BAT_SMOKE_ALLOW_NON_TMP=1`,且输出目录必须为空
- 二次运行返回 `up_to_date`watch 模式在 up-to-date 时静默 - 脚本退出码为 0 即表示 runbook 验收通过
- 人工破坏一个本地文件后,audit 检出并 repair - 真实网络执行需要外部网络和足够磁盘空间;本仓库只保存 runbook、脚本和测试,不保存官方大文件
- 记录命令、摘要、输出目录和未纳入 Git 的大文件位置。
--- ---
+6 -5
View File
@@ -1,8 +1,8 @@
# 当前阶段说明 # 当前阶段说明
- **更新时间**2026-07-06 - **更新时间**2026-07-14
- **状态分支**`experiment` - **状态分支**`experiment`
- **最新功能提交**`789402c feat: add official resource sync pipeline` - **最新功能提交**以当前 `git log --oneline -1` 为准
- **用途**:当前阶段交付说明、推送前核查和下一步依据。 - **用途**:当前阶段交付说明、推送前核查和下一步依据。
## 1. 当前目标 ## 1. 当前目标
@@ -28,6 +28,8 @@
- `OfficialResourcePullService` 的官方 URL 拒绝、`.part` 续传、重试、本地 manifest、官方 seed `.hash` 校验。 - `OfficialResourcePullService` 的官方 URL 拒绝、`.part` 续传、重试、本地 manifest、官方 seed `.hash` 校验。
- `OfficialUpdateService` 的 auto-discover、bootstrap cache、v2 snapshot、remote marker diff、本地 audit/repair。 - `OfficialUpdateService` 的 auto-discover、bootstrap cache、v2 snapshot、remote marker diff、本地 audit/repair。
- `bat` one-shot 和 `--watch` - `bat` one-shot 和 `--watch`
- `bat` 运行时 progress log 覆盖总体下载进度、单文件进度和校验结果摘要。
- 真实官方网络全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh``make official-smoke``docs/guides/official-full-pull-smoke.md`
- `bat-ffi` 的 Manifest inspect 和 sync plan JSON API。 - `bat-ffi` 的 Manifest inspect 和 sync plan JSON API。
- `SqliteResourceRepository` - `SqliteResourceRepository`
@@ -35,7 +37,6 @@
- Go CLI 最小可用入口。 - Go CLI 最小可用入口。
- 官方同步下载结果自动导入 CAS + ResourceRepository 的用户级流程。 - 官方同步下载结果自动导入 CAS + ResourceRepository 的用户级流程。
- 真实官方网络全量下载 smoke 记录。
- 完整 AssetBundle 解析。 - 完整 AssetBundle 解析。
- Patch、翻译系统、API Server、Web。 - Patch、翻译系统、API Server、Web。
@@ -62,11 +63,11 @@ git status --short
## 4. 当前结论 ## 4. 当前结论
当前 Rust 官方资源同步链路已经具备可运行闭环,但仓库还不是完整产品。下一步不应继续只扩展 example,而应补齐 Go CLI 最小入口、真实网络 smoke、CAS/ResourceRepository 编排和 AssetBundle 解析。 当前 Rust 官方资源同步链路已经具备可运行闭环,并已固化真实网络 smoke 的可重复命令,但仓库还不是完整产品。下一步不应继续只扩展 example,而应补齐 Go CLI 最小入口、CAS/ResourceRepository 编排和 AssetBundle 解析。
## 5. 下一步 ## 5. 下一步
1. 实现 Go CLI`bat doctor``bat sync --help``bat official sync --help` 1. 实现 Go CLI`bat doctor``bat sync --help``bat official sync --help`
2. `bat`真实官方网络 smoke,输出目录必须隔离。 2. `docs/guides/official-full-pull-smoke.md` 在具备网络和磁盘窗口的环境中执行真实官方网络 smoke,输出目录必须隔离。
3. 将官方同步下载结果接入 CAS + `SqliteResourceRepository` 3. 将官方同步下载结果接入 CAS + `SqliteResourceRepository`
4. 开始 AssetBundle UnityFS header/block/directory 解析。 4. 开始 AssetBundle UnityFS header/block/directory 解析。
+7 -4
View File
@@ -1,13 +1,13 @@
# 当前进度交接 # 当前进度交接
- **更新时间**2026-07-06 - **更新时间**2026-07-14
- **状态分支**`experiment` - **状态分支**`experiment`
- **最新功能提交**`789402c feat: add official resource sync pipeline` - **最新功能提交**以当前 `git log --oneline -1` 为准
- **用途**:给下一次对话快速恢复上下文,优先以当前工作区和已验证结果为准。 - **用途**:给下一次对话快速恢复上下文,优先以当前工作区和已验证结果为准。
## 1. 现在处在什么阶段 ## 1. 现在处在什么阶段
项目已经完成基础整理、CAS V1Rust 官方资源同步闭环。下一阶段重点是 Go CLI 最小可用、真实官方网络 smoke、同步结果进入 CAS/ResourceRepository,以及 AssetBundle 解析起步。 项目已经完成基础整理、CAS V1Rust 官方资源同步闭环,以及真实官方网络 smoke 的可重复命令固化。下一阶段重点是 Go CLI 最小可用、同步结果进入 CAS/ResourceRepository,以及 AssetBundle 解析起步。
已确认的事实: 已确认的事实:
@@ -19,6 +19,8 @@
- 默认平台是 `Windows + Android` - 默认平台是 `Windows + Android`
- 真实下载会维护 `official-download-manifest.json`,并使用 size + BLAKE3、本地 audit/repair 和官方 seed `.hash` 校验。 - 真实下载会维护 `official-download-manifest.json`,并使用 size + BLAKE3、本地 audit/repair 和官方 seed `.hash` 校验。
- `--watch` 是 Rust 内部常驻检查模式,正常检查默认 1 小时,失败重试默认 60 秒;外部 systemd/container 只负责守护进程。 - `--watch` 是 Rust 内部常驻检查模式,正常检查默认 1 小时,失败重试默认 60 秒;外部 systemd/container 只负责守护进程。
- `bat` progress log 会输出总体下载进度、单文件进度和校验结果摘要。
- 真实官方网络 smoke 已固化为 `scripts/official-full-pull-smoke.sh``make official-smoke``docs/guides/official-full-pull-smoke.md`,默认写入 `/tmp` 隔离目录。
## 2. 现在不要误解的点 ## 2. 现在不要误解的点
@@ -34,7 +36,7 @@
仍待实现的主线工作: 仍待实现的主线工作:
1. Go CLI 最小入口:`bat doctor``bat sync --help``bat official sync --help` 1. Go CLI 最小入口:`bat doctor``bat sync --help``bat official sync --help`
2. 真实官方网络 smoke:dry-run、首次下载、二次 up-to-date、本地损坏 repair 2. 按 smoke runbook 在具备网络和磁盘窗口的环境中执行真实官方网络 smoke,并保留隔离目录报告
3. 官方同步下载结果进入 CAS + `SqliteResourceRepository` 的用户级流程。 3. 官方同步下载结果进入 CAS + `SqliteResourceRepository` 的用户级流程。
4. Addressables parser 覆盖更多真实 catalog 结构。 4. Addressables parser 覆盖更多真实 catalog 结构。
5. AssetBundle UnityFS header/block/directory 解析。 5. AssetBundle UnityFS header/block/directory 解析。
@@ -57,4 +59,5 @@
- `DOCS_INDEX.md` - `DOCS_INDEX.md`
- `docs/reports/CURRENT_GAPS.md` - `docs/reports/CURRENT_GAPS.md`
- `docs/guides/official-resource-test-pull.md` - `docs/guides/official-resource-test-pull.md`
- `docs/guides/official-full-pull-smoke.md`
- `docs/architecture/official-resource-backend.md` - `docs/architecture/official-resource-backend.md`
+42 -8
View File
@@ -1048,6 +1048,10 @@ impl OfficialUpdateService {
}; };
if !should_download { if !should_download {
progress(OfficialUpdateProgress::new(
"audit",
verification_progress_message(&report.verification_summary),
));
progress(OfficialUpdateProgress::new( progress(OfficialUpdateProgress::new(
"finish", "finish",
"资源已是最新;无需下载", "资源已是最新;无需下载",
@@ -1138,6 +1142,16 @@ impl OfficialUpdateService {
let final_audit = staging_fetcher let final_audit = staging_fetcher
.audit_local_manifest(&pull_plan) .audit_local_manifest(&pull_plan)
.map_err(anyhow::Error::msg)?; .map_err(anyhow::Error::msg)?;
let final_verification_summary = OfficialVerificationSummary::new(
final_audit.manifest_blake3_verified_count(),
final_audit.repair_needed_count(),
pull_report.official_hash_verified_count(),
final_audit.zip_structure_verified_count(),
);
progress(OfficialUpdateProgress::new(
"audit",
verification_progress_message(&final_verification_summary),
));
progress(OfficialUpdateProgress::new( progress(OfficialUpdateProgress::new(
"snapshot", "snapshot",
format!("写入快照 {}", staging_snapshot_path.display()), format!("写入快照 {}", staging_snapshot_path.display()),
@@ -1175,12 +1189,7 @@ impl OfficialUpdateService {
report.official_seed_hash_verified_count = pull_report.official_hash_verified_count(); report.official_seed_hash_verified_count = pull_report.official_hash_verified_count();
report.local_manifest_verified_count = final_audit.verified_count(); report.local_manifest_verified_count = final_audit.verified_count();
report.local_manifest_repair_needed_count = final_audit.repair_needed_count(); report.local_manifest_repair_needed_count = final_audit.repair_needed_count();
report.verification_summary = OfficialVerificationSummary::new( report.verification_summary = final_verification_summary;
final_audit.manifest_blake3_verified_count(),
final_audit.repair_needed_count(),
pull_report.official_hash_verified_count(),
final_audit.zip_structure_verified_count(),
);
report.snapshot_written = Some(final_snapshot_path); report.snapshot_written = Some(final_snapshot_path);
progress(OfficialUpdateProgress::new( progress(OfficialUpdateProgress::new(
@@ -1356,7 +1365,13 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
match event.kind { match event.kind {
OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new( OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new(
"download", "download",
format!("({}/{}) 开始 {}", event.index, event.total, event.url), format!(
"下载进度:总体 {}/{} ({:.1}%);单文件开始 URL={}",
event.index,
event.total,
download_progress_percent(event.index, event.total),
event.url
),
) )
.with_download_progress(&event), .with_download_progress(&event),
OfficialResourcePullProgressKind::Finished => { OfficialResourcePullProgressKind::Finished => {
@@ -1364,9 +1379,10 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
OfficialUpdateProgress::new( OfficialUpdateProgress::new(
"download", "download",
format!( format!(
"({}/{}) 完成 状态={} 文件字节={} 本轮传输字节={} {}", "下载进度:总体 {}/{} ({:.1}%);单文件完成 状态={} 文件字节={} 本轮传输字节={} URL={}",
event.index, event.index,
event.total, event.total,
download_progress_percent(event.index, event.total),
status, status,
event.bytes.unwrap_or(0), event.bytes.unwrap_or(0),
event.transferred_bytes.unwrap_or(0), event.transferred_bytes.unwrap_or(0),
@@ -1378,6 +1394,24 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda
} }
} }
fn download_progress_percent(index: usize, total: usize) -> f64 {
if total == 0 {
100.0
} else {
(index as f64 / total as f64) * 100.0
}
}
fn verification_progress_message(summary: &OfficialVerificationSummary) -> String {
format!(
"校验结果:官方 .hash={};本地 BLAKE3 通过={};本地需修复={}ZIP 结构通过={}",
summary.official_hash_verified_count,
summary.local_manifest_blake3_verified_count,
summary.local_manifest_repair_needed_count,
summary.zip_structure_verified_count
)
}
fn localized_pull_status(status: crate::OfficialResourcePullStatus) -> &'static str { fn localized_pull_status(status: crate::OfficialResourcePullStatus) -> &'static str {
match status { match status {
crate::OfficialResourcePullStatus::SkippedExisting => "已复用", crate::OfficialResourcePullStatus::SkippedExisting => "已复用",
@@ -229,6 +229,17 @@ fn official_update_first_run_pulls_without_pre_audit() {
assert!(events assert!(events
.iter() .iter()
.any(|event| { event.stage == "decision" && event.message.contains("首次拉取=true") })); .any(|event| { event.stage == "decision" && event.message.contains("首次拉取=true") }));
assert!(events.iter().any(|event| {
event.stage == "download"
&& event.message.contains("下载进度:总体")
&& event.message.contains("单文件")
}));
assert!(events.iter().any(|event| {
event.stage == "audit"
&& event.message.contains("校验结果:官方 .hash=5")
&& event.message.contains("本地 BLAKE3 通过=19")
&& event.message.contains("ZIP 结构通过=6")
}));
let published = report.published_version_path.as_ref().unwrap(); let published = report.published_version_path.as_ref().unwrap();
assert!(published.exists()); assert!(published.exists());
@@ -280,6 +291,12 @@ fn official_update_second_run_audits_existing_resources_before_reuse() {
assert!(events.iter().any(|event| { assert!(events.iter().any(|event| {
event.stage == "audit" && event.message.contains("审计本地下载 manifest") event.stage == "audit" && event.message.contains("审计本地下载 manifest")
})); }));
assert!(events.iter().any(|event| {
event.stage == "audit"
&& event.message.contains("校验结果:官方 .hash=0")
&& event.message.contains("本地 BLAKE3 通过=19")
&& event.message.contains("ZIP 结构通过=6")
}));
assert!(events.iter().any(|event| { assert!(events.iter().any(|event| {
event.stage == "decision" && event.message.contains("首次拉取=false") event.stage == "decision" && event.message.contains("首次拉取=false")
})); }));
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${REPO_ROOT}"
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
SMOKE_ROOT="${BAT_SMOKE_ROOT:-/tmp/bat-official-smoke-${timestamp}}"
OUTPUT_DIR="${BAT_SMOKE_OUTPUT:-${SMOKE_ROOT}/resources}"
STATE_DIR="${BAT_SMOKE_STATE_DIR:-${SMOKE_ROOT}/state}"
REPORT_DIR="${BAT_SMOKE_REPORT_DIR:-${SMOKE_ROOT}/report}"
BAT_BIN="${BAT_BIN:-${REPO_ROOT}/target/release/bat}"
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
real_output="$(realpath -m "${OUTPUT_DIR}")"
case "${real_output}" in
/tmp/*) ;;
*)
if [[ "${BAT_SMOKE_ALLOW_NON_TMP:-}" != "1" ]]; then
fail "BAT_SMOKE_OUTPUT must be under /tmp by default; set BAT_SMOKE_ALLOW_NON_TMP=1 only for an isolated test path"
fi
;;
esac
if [[ -e "${OUTPUT_DIR}" ]] && [[ -n "$(find "${OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
fail "output directory is not empty: ${OUTPUT_DIR}"
fi
mkdir -p "${OUTPUT_DIR}" "${STATE_DIR}" "${REPORT_DIR}"
if [[ "${BAT_SMOKE_SKIP_BUILD:-}" != "1" ]]; then
cargo build --release -p bat-infrastructure --bin bat
fi
[[ -x "${BAT_BIN}" ]] || fail "bat binary is not executable: ${BAT_BIN}"
write_command_file() {
local name="$1"
shift
local command_file="${REPORT_DIR}/${name}.command.txt"
: >"${command_file}"
printf '$' >>"${command_file}"
printf ' %q' "$@" >>"${command_file}"
printf '\n' >>"${command_file}"
}
run_step() {
local name="$1"
shift
printf '==> %s\n' "${name}"
write_command_file "${name}" "$@"
"$@" >"${REPORT_DIR}/${name}.stdout.json" 2>"${REPORT_DIR}/${name}.stderr.log"
}
require_contains() {
local path="$1"
local needle="$2"
local description="$3"
if ! grep -Fq "${needle}" "${path}"; then
fail "${description}; expected '${needle}' in ${path}"
fi
}
run_step "01-dry-run-plan" \
"${BAT_BIN}" \
--auto-discover \
--output "${OUTPUT_DIR}" \
--state-dir "${STATE_DIR}" \
--dry-run \
--plan \
--json \
--no-progress
run_step "02-first-full-pull" \
"${BAT_BIN}" \
--auto-discover \
--output "${OUTPUT_DIR}" \
--state-dir "${STATE_DIR}" \
--json \
--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 single-file download progress"
require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "校验结果:" "first pull did not emit verification summary"
run_step "03-second-up-to-date" \
"${BAT_BIN}" \
--auto-discover \
--output "${OUTPUT_DIR}" \
--state-dir "${STATE_DIR}" \
--json \
--no-progress
require_contains "${REPORT_DIR}/03-second-up-to-date.stdout.json" '"update_status": "up_to_date"' "second run did not report up_to_date"
current_link="${OUTPUT_DIR}/current"
[[ -e "${current_link}" ]] || fail "current release symlink/path was not created: ${current_link}"
current_dir="$(realpath "${current_link}")"
damage_file="$(find "${current_dir}" -type f \
! -name 'official-sync-snapshot.json' \
! -name 'official-download-manifest.json' \
-print -quit)"
[[ -n "${damage_file}" ]] || fail "no resource file found to damage under ${current_dir}"
printf '%s\n' "${damage_file}" >"${REPORT_DIR}/damaged-file.txt"
{
printf 'path: %s\n' "${damage_file}"
printf 'bytes: %s\n' "$(wc -c <"${damage_file}" | tr -d ' ')"
printf 'sha256: %s\n' "$(sha256sum "${damage_file}" | awk '{print $1}')"
} >"${REPORT_DIR}/damaged-file.before.txt"
printf 'BAT official smoke intentional corruption\n' >"${damage_file}"
run_step "04-repair-after-damage" \
"${BAT_BIN}" \
repair \
--auto-discover \
--output "${OUTPUT_DIR}" \
--state-dir "${STATE_DIR}" \
--json \
--progress \
--no-banner
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 single-file download progress"
require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "校验结果:" "repair did not emit verification summary"
run_step "05-verify-after-repair" \
"${BAT_BIN}" \
verify \
--auto-discover \
--output "${OUTPUT_DIR}" \
--state-dir "${STATE_DIR}" \
--json \
--no-progress
require_contains "${REPORT_DIR}/05-verify-after-repair.stdout.json" '"healthy": true' "verify after repair did not report healthy=true"
file_count="$(find "${current_dir}" -type f | wc -l | tr -d ' ')"
current_size="$(du -sh "${current_dir}" | awk '{print $1}')"
report_file="${REPORT_DIR}/SMOKE_REPORT.md"
cat >"${report_file}" <<EOF
# Official Full Pull Smoke Report
- generated_at_utc: ${timestamp}
- repo: ${REPO_ROOT}
- bat: ${BAT_BIN}
- output: ${OUTPUT_DIR}
- state_dir: ${STATE_DIR}
- current_release: ${current_dir}
- current_release_size: ${current_size}
- current_release_file_count: ${file_count}
- damaged_file: ${damage_file}
## Steps
1. dry-run with full URL plan: 01-dry-run-plan
2. first full pull: 02-first-full-pull
3. second up-to-date check: 03-second-up-to-date
4. intentional local file damage and repair: 04-repair-after-damage
5. verify after repair: 05-verify-after-repair
Each step has:
- <step>.command.txt
- <step>.stdout.json
- <step>.stderr.log
The large resource output is intentionally outside Git and remains under:
\`${OUTPUT_DIR}\`
EOF
printf '\nSmoke report: %s\n' "${report_file}"
printf 'Resource output: %s\n' "${OUTPUT_DIR}"