mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
refactor(cli): 模块化 Rust 前台输出
This commit is contained in:
+6
-5
@@ -22,18 +22,19 @@
|
||||
- USERGUIDE 补充 `bat-api` 资源 bootstrap / 分发章节,说明与 Rust `bat` 的运行关系、接口、CDN 响应语义和不仿造业务 API 的边界
|
||||
- 官方同步新增 `official-parse-cache.json`:校验发布后从下载 manifest 覆盖直接 UnityFS bundle、zip 内 UnityFS 条目和非候选资源记录;URL、相对路径、size 和 BLAKE3 未变化时跳过重复解析
|
||||
- 官方原版资源和汉化产物目录分离:`BAT_OUTPUT`/`--output` 默认 `./bat-resources`,`BAT_LOCALIZED_OUTPUT`/`--localized-output` 默认 `./bat-localized`;同步报告新增 `localized_release_status=not_localized`,后续 Patch 发布完成后才切换为 `localized`
|
||||
- 官方资源同步支持从已发布历史 release 或 CAS 复用已校验文件:复用前检查 size、BLAKE3 和 ZIP 结构,失败保留诊断并回退网络;release 级 CAS 引用写入 `official-cas-reuse-references.json`,清理流程同步回收引用
|
||||
- Rust `bat` 的报告渲染与前台终端输出分别模块化到 `report_output.rs` 和 `terminal_output.rs`,保持 JSON、人类摘要、帮助、进度和错误输出契约不变
|
||||
|
||||
### 修复
|
||||
- 官方下载失败重试之间加入指数退避(网络类失败 200ms→400ms→800ms…,上限 5s)
|
||||
|
||||
### 计划
|
||||
- [ ] `bat-api` 后续:refresh mtime/size 增量缓存、完整 launcher 安装包更新链(若需要,新 issue)、API 持久化层接入预留 database/redis 配置;Rust/Go snapshot contract fixture 与同机 live smoke 已完成
|
||||
- [ ] 官方同步结果接入 CAS + ResourceRepository 的用户级工作流
|
||||
- [ ] 扩展 CAS + ResourceRepository 的用户级查询、翻译记忆和通用 Patch 发布资源视图
|
||||
- [ ] 官方下载/导入路径接入 CRC/size 校验(复用 `verify_downloaded_bytes`)
|
||||
- [ ] 汉化 Patch 发布:维护 `localized-output/current`、`localized-version-state`、`localized` 状态切换和回滚
|
||||
- [ ] 实现翻译系统
|
||||
- [ ] 完成发布级 Patch build/rollback、复杂 AssetBundle 重打包和汉化发布统一
|
||||
- [ ] 实现 Web 管理后台
|
||||
- [ ] 完成通用 manifest 驱动的 Patch build/rollback、复杂 AssetBundle 重打包和完整汉化文件集合发布
|
||||
- [ ] 扩展 provider 编排、翻译记忆和人工协作工作流
|
||||
- [ ] 完成 Web 协作后台的持久化、权限和长期任务能力
|
||||
|
||||
## [0.2.0] - 2026-07-17
|
||||
|
||||
|
||||
+2
-1
@@ -143,6 +143,7 @@ job/unit/provider run 状态。当前下载实现使用默认 8 个独立 worker
|
||||
- `OfficialResourcePullService`:官方 URL 拒绝策略、目标路径映射、下载 manifest、下载 quarantine、`.part` 续传、curl 代理配置、403/404/5xx 分类重试、ZIP 结构校验、官方 seed `.hash` 校验、本地全量 verify。
|
||||
- `OfficialUpdateService`:官方 metadata auto-discover、bootstrap cache、snapshot diff、marker diff、本地 audit/repair、失败 staging 恢复。
|
||||
- `bat`:正式 CLI binary,支持 one-shot、`--proxy` / `--no-proxy`、`--watch`、`--daemon`、`status`、`stop`、`restart`、`reload`、`refresh`、`logs`、`verify`、`repair`、`doctor` 和 `clean-stable`。
|
||||
- `report_output.rs`、`terminal_output.rs`:分别负责结果报告渲染和前台终端诊断、帮助、进度及结构化日志输出。
|
||||
|
||||
待完成:
|
||||
|
||||
@@ -309,7 +310,7 @@ GitHub issue 状态:#1 已关闭;#17 的历史决定不代表当前下载实
|
||||
1. Issue #1 收尾:协议基础设施、最小方法集、`catalog.*`、`parse.*`、`localized.*`、`task.*`、`resource.repair`、`daemon.restart`、文件级 `patch.apply` / `unityfs.patch_text_asset` / `unityfs.patch_string_field` / `unityfs.patch_field`、任务持久化、错误码模型与文档均已完成;剩余通用 manifest 发布、复杂 `unityfs.*` 语义编辑以及 `task.create`、`daemon.clean-stable` 的设计边界确认。
|
||||
2. 真实官方网络全量下载长期运行报告(`make official-smoke`);issue #19 的同机 live 联调已完成。
|
||||
3. 跟进官方同步长期运行测试报告。
|
||||
4. AssetBundle / Addressables(issue #3 / #2);CAS 用户级导入(G-011)。
|
||||
4. AssetBundle / Addressables(issue #3 / #2)的后续结构变体;G-011 剩余的 TextUnit 查询、翻译记忆和通用 Patch 发布资源视图。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
# BlueArchiveToolkit 文档索引
|
||||
|
||||
- **更新时间**:2026-08-31
|
||||
- **更新时间**:2026-09-02
|
||||
- **说明**:本索引用于快速定位当前权威文档和历史资料。
|
||||
|
||||
---
|
||||
@@ -109,6 +109,7 @@
|
||||
- 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` 校验。
|
||||
- 官方 release 历史文件与 CAS 对象复用:复用前执行 size、BLAKE3 和 ZIP 结构校验,失败回退网络,引用写入版本化清单并由清理流程回收。
|
||||
- 官方原版资源与汉化产物目录分离:`./bat-resources` 只承载原版 release,`./bat-localized` 承载后续汉化 release;当前官方同步报告 `not_localized`,Patch 发布完成后才进入 `localized`。
|
||||
- 官方 release 会维护 `official-parse-cache.json`,用于跳过未变化资源的重复解析。
|
||||
- `bat-api/internal/backendrpc` typed Unix socket JSON-RPC client。
|
||||
@@ -116,6 +117,7 @@
|
||||
- 真实官方网络全量拉取 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`,默认写入 `/tmp` 隔离目录并输出本地运行报告。
|
||||
- `bat-api` 同机 live smoke 已固化为 `scripts/bat-api-local-live-smoke.sh` 和 `make bat-api-local-live-smoke`,覆盖真实 `bat.sock`、release 切换、未 ready、恢复和 CDN 读路径。
|
||||
- `bat` 运行时 progress log 已覆盖下载已完成计数、单文件下载进度和校验结果摘要。
|
||||
- `bat` 前台结果渲染与终端诊断输出已分别收敛到 `report_output.rs` 和 `terminal_output.rs`,CLI 流程模块只负责编排。
|
||||
- Addressables 当前真实形态 fixture/golden 覆盖。
|
||||
- 解析补全路线图已固化到 `docs/architecture/assetbundle.md`:解析缓存、Addressables、UnityFS、Serialized 字段级解析、文本提取、CAS 接入和 Patch 发布前置。
|
||||
- SQLite Resource Repository 和可选无状态 `bat-ffi` JSON 兼容接口。
|
||||
@@ -123,5 +125,5 @@
|
||||
优先待办:
|
||||
|
||||
- `bat-api` 与完整官方网络长期运行(独立于 issue #19 的 `make official-smoke` 跟踪)。
|
||||
- 将官方同步结果接入 CAS + ResourceRepository 的用户级流程。
|
||||
- 扩展 CAS + ResourceRepository 的用户级查询、翻译记忆和通用 Patch 发布资源视图。
|
||||
- 推进 AssetBundle UnityFS 引擎级解析。
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
# BlueArchiveToolkit 完整开发计划
|
||||
|
||||
- **项目名称**:BlueArchiveToolkit
|
||||
- **文档版本**:2026-08-03 状态复核版
|
||||
- **文档版本**:2026-09-02 状态复核版
|
||||
- **权威状态**:以本文档和 `CURRENT_STATUS.md` 为准,旧阶段报告仅作历史参考。
|
||||
- **最终目标**:构建一个可长期维护、可扩展、可审计的 Blue Archive 资源管理、文本提取、翻译和补丁平台。
|
||||
|
||||
@@ -22,7 +22,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
||||
|
||||
## 2. 当前真实状态
|
||||
|
||||
本节来自 2026-08-03 的工作区盘点、本地验证和最新功能提交。
|
||||
本节来自 2026-09-02 的工作区盘点、本地验证和最新功能提交。
|
||||
|
||||
### 已具备
|
||||
|
||||
@@ -169,7 +169,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
||||
|
||||
**目标**:能够获取、解析和同步 Blue Archive 资源清单。
|
||||
|
||||
**当前状态**:部分完成。Rust 官方日服资源同步链路已经具备正式 one-shot 和 `--watch` 常驻入口;`bat-api` 资源 bootstrap/分发入口已落地,但完整解析覆盖、CAS 用户级编排和真实线上 smoke 仍待完成。
|
||||
**当前状态**:部分完成。Rust 官方日服资源同步链路已经具备正式 one-shot 和 `--watch` 常驻入口;`bat-api` 资源 bootstrap/分发入口已落地,CAS + ResourceRepository 导入、历史 release/CAS 复用已可用,但完整解析覆盖、丰富查询/翻译记忆和真实线上 smoke 仍待完成。
|
||||
|
||||
交付物:
|
||||
|
||||
@@ -179,7 +179,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
||||
4. Rust 自动更新入口:**已完成当前生产入口**。`bat` 支持 snapshot、marker diff、bootstrap cache、one-shot、`--watch`、`--daemon`、默认 1 小时间隔、北京时间固定强制刷新,以及 Unix socket JSON-RPC 后台运维命令返回。
|
||||
5. Go 入口边界:**已冻结**。同步命令行 = Rust `bat`(G-008 关闭);资源分发 = `bat-api` MVP(G-009 部分完成)。详见 `docs/reports/GO_STATUS.md`。
|
||||
6. 用户级 `sync`、`manifest inspect`、`cache status`:**未完成**。Rust `bat --json` 是当前稳定进程边界;`bat-ffi` 只提供可选兼容用的 Manifest inspect 和 sync plan JSON helper。
|
||||
7. 下载结果写入 CAS + ResourceRepository:**部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后导入;`resource.index` 可按资源级 release、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 查询现有索引和资源 metadata,常用 metadata 过滤已下推到 SQLite;`bat doctor cas` 可只读诊断既有 CAS 根目录、对象目录、元数据库文件和对象统计;`parse.text_units` / `parse.errors` 可查询当前 release 的 TextUnit 明细与解析错误;`translation.tasks` 可查询离线 TextUnit 翻译任务状态、跳过/失败原因和 worker 结果。剩余工作是更丰富的 TextUnit 查询、翻译记忆查询和通用 Patch 发布所需资源视图。
|
||||
7. 下载结果写入 CAS + ResourceRepository:**基础能力可用,查询面仍部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后导入;`resource.index` 可按资源级 release、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 查询现有索引和资源 metadata,常用 metadata 过滤已下推到 SQLite;`bat doctor cas` 可只读诊断既有 CAS 根目录、对象目录、元数据库文件和对象统计;`parse.text_units` / `parse.errors` 可查询当前 release 的 TextUnit 明细与解析错误;`translation.tasks` 可查询离线 TextUnit 翻译任务状态、跳过/失败原因和 worker 结果。剩余工作是更丰富的 TextUnit 查询、翻译记忆查询和通用 Patch 发布所需资源视图。
|
||||
8. Linux 生产同步不依赖已安装官方启动器:**已完成当前 Rust 入口**。`--auto-discover` 只使用官方 HTTP metadata 和临时目录解析 `GameMainConfig`。
|
||||
9. 真实官方网络全量下载 smoke test:**命令已固化(G-018 已关闭)**。`scripts/official-full-pull-smoke.sh` / `make official-smoke` 已固化 dry-run、首次下载、二次 up-to-date 和本地损坏 repair 的可重复流程;真实运行处于长期运行测试阶段,报告待后续提供。
|
||||
10. 官方发布后的增量 handoff 与解析缓存:**已完成基础入口**。新 release 发布后先生成 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`,新增+变更资源进入解析/翻译候选;`official-parse-cache.json` 基于下载 manifest 覆盖直接 UnityFS bundle、zip 内 UnityFS 条目和非候选资源记录;随后生成 `official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json`,本地文件未变化且缓存/索引有效时跳过重复解析。
|
||||
|
||||
@@ -9,7 +9,9 @@ BlueArchive Toolkit 采用 **Monorepo + 多语言混合** 架构,旨在构建
|
||||
当前已经可用的官方资源入口包括:
|
||||
|
||||
- `infrastructure/src/bin/bat_official_sync.rs`:Linux 官方资源同步薄入口,构建为 `bat`;控制面组合与实现位于 `infrastructure/src/bin/bat/`,支持 one-shot、`--watch`、`--daemon`、`status`、`stop`、`restart`、`reload`、`refresh`、`logs`、`verify`、`repair`、`doctor` 和 `clean-stable`。
|
||||
- `infrastructure/src/bin/bat/app.rs`:CLI/env、daemon/watch、RPC dispatch、状态输出与顶层流程组合。
|
||||
- `infrastructure/src/bin/bat/app.rs`:CLI/env、daemon/watch、RPC dispatch 与顶层流程组合。
|
||||
- `infrastructure/src/bin/bat/report_output.rs`:人类可读报告、JSON 查询结果和报告格式化。
|
||||
- `infrastructure/src/bin/bat/terminal_output.rs`:前台错误、帮助、启动提示、进度和结构化日志输出。
|
||||
- `infrastructure/src/bin/bat/task_registry.rs`:任务注册表、任务持久化、取消和 daemon worker。
|
||||
- `infrastructure/src/bin/bat/readonly_query.rs`:parse/resource/translation/localized 只读查询及 RPC 选择。
|
||||
- `infrastructure/src/bin/bat/translation_query.rs`:翻译任务与 handoff 查询、worker 状态更新。
|
||||
@@ -97,7 +99,7 @@ cas/
|
||||
|
||||
---
|
||||
|
||||
### 2. 官方资源同步器 (Rust 当前实现,Go 后续编排)
|
||||
### 2. 官方资源同步器 (Rust 当前实现,Go 侧读取与编排)
|
||||
|
||||
**职责**:从官方日服 HTTP metadata 自动发现资源入口,下载 Windows + Android 官方资源,增量检查,完整性校验,保持本地状态。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AssetBundle 与资源解析路线图
|
||||
|
||||
- **更新时间**:2026-08-19
|
||||
- **更新时间**:2026-09-02
|
||||
- **适用范围**:Rust 解析引擎、官方同步后的解析缓存、CAS/ResourceRepository 接入、后续文本提取和 Patch 发布。
|
||||
- **权威关联**:`PROJECT_PLAN.md` Milestone 3/4/5/8,`docs/reports/CURRENT_GAPS.md` G-005/G-007/G-011/G-011D。
|
||||
- **维护冻结**:解析扩展遵循 [`docs/reports/PARSER_FREEZE.md`](../reports/PARSER_FREEZE.md),冻结期只接受稳定性、诊断、真实回归和文档一致性修复。
|
||||
|
||||
@@ -251,6 +251,7 @@ release 根目录写入 `translation-tasks.sqlite`,由版本化 `schema_migrat
|
||||
- `infrastructure/src/official_update.rs`
|
||||
- `infrastructure/src/bin/bat_official_sync.rs`(薄入口)
|
||||
- `infrastructure/src/bin/bat/app.rs`(控制面组合)
|
||||
- `infrastructure/src/bin/bat/report_output.rs`、`terminal_output.rs`(前台报告和终端输出)
|
||||
- `infrastructure/src/bin/bat/task_registry.rs`(任务注册表、持久化和 worker)
|
||||
- `infrastructure/src/bin/bat/readonly_query.rs`、`translation_query.rs`(只读查询)
|
||||
- `infrastructure/src/bin/bat/patch_commands.rs`(patch 命令)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 官方资源 Release 布局与资源侧契约
|
||||
|
||||
- **更新时间**:2026-07-27
|
||||
- **更新时间**:2026-09-02
|
||||
- **用途**:冻结日服官方资源在本地发布根上的布局、URL 映射、seed 规则、`bat`/`bat-api` 关系,以及 `bat-api` 分发 path 的 1:1 对应关系。
|
||||
- **范围**:资源发现 / 清单 / 落盘 / 只读分发(**不是**完整游戏业务 API)。
|
||||
- **权威代码**:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# bat-api / Rust bat Contract Fixture Handoff
|
||||
|
||||
更新时间:2026-08-29
|
||||
更新时间:2026-09-02
|
||||
|
||||
本文用于两个 Codex 窗口之间间接联调 `bat-api` 与 Rust `bat` 的跨语言 contract fixture。
|
||||
仓库内归一化 fixture 已交付;本文保留生成、审核和后续扩展的协作协议。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 当前实现缺口清单
|
||||
|
||||
- **更新时间**:2026-08-29
|
||||
- **更新时间**:2026-09-02
|
||||
- **Go 进度权威**:`GO_STATUS.md`
|
||||
- **资源布局 / 逆向契约**:`../architecture/resource-release-layout.md`
|
||||
- **用途**:集中跟踪当前代码中的占位实现、设计缺口和下一步验收项。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Go 侧进度与边界(权威)
|
||||
|
||||
- **更新时间**:2026-08-31
|
||||
- **更新时间**:2026-09-02
|
||||
- **用途**:统一 Go module `bat-api` 的产品边界、既有约定和组件进度;其他文档与此冲突时以本文为准。
|
||||
- **关联**:issue #19 / G-009(资源 bootstrap/分发)、G-008(已决策关闭)、`docs/architecture/official-resource-backend.md` §7、`docs/guides/bat-api-local-live-smoke.md`
|
||||
|
||||
|
||||
+43
-1386
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
||||
use super::report_output::{
|
||||
format_daemon_download_progress_json, verification_summary_lines,
|
||||
visible_historical_failed_versions,
|
||||
};
|
||||
use super::*;
|
||||
use crate::app::schedule_commands::{
|
||||
read_schedule_file, schedule_add_report, schedule_list_report_with_request,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use super::report_output::print_report;
|
||||
|
||||
pub(super) fn run_write_patch_command(options: &CliOptions) -> anyhow::Result<()> {
|
||||
match options.command {
|
||||
CliCommand::PatchApply => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::report_output::print_json_value;
|
||||
use super::*;
|
||||
|
||||
pub(super) fn run_readonly_query_command(options: &CliOptions) -> anyhow::Result<()> {
|
||||
|
||||
@@ -0,0 +1,850 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) trait HumanReport {
|
||||
fn print_human(&self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
pub(super) fn print_report<T>(format: OutputFormat, report: &T) -> anyhow::Result<()>
|
||||
where
|
||||
T: Serialize + HumanReport,
|
||||
{
|
||||
match format {
|
||||
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(report)?),
|
||||
OutputFormat::Human => report.print_human()?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn print_json_value(
|
||||
format: OutputFormat,
|
||||
value: &serde_json::Value,
|
||||
) -> anyhow::Result<()> {
|
||||
match format {
|
||||
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(value)?),
|
||||
OutputFormat::Human => print_human_json_value(value)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl HumanReport for serde_json::Value {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_human_json_value(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for RepackReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("UnityFS 重打包");
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_path_field("源 bundle", &self.source_bundle);
|
||||
print_path_field("目标 bundle", &self.target_bundle);
|
||||
print_field("操作数", self.operation_count);
|
||||
print_field("源字节", self.source_bytes);
|
||||
print_field("目标字节", self.target_bytes);
|
||||
print_field("源 BLAKE3", &self.source_blake3);
|
||||
print_field("目标 BLAKE3", &self.target_blake3);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for LocalizedPatchReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("汉化 release 发布");
|
||||
print_path_field("版本目录", &self.version_path);
|
||||
print_path_field("current", &self.current_path);
|
||||
print_path_field("状态文件", &self.state_path);
|
||||
print_path_field("patch manifest", &self.patch_manifest_path);
|
||||
print_field("变更文件数", self.files.len());
|
||||
print_field("TextAsset 操作数", self.manifest.text_asset_operation_count);
|
||||
print_field("校验文件数", self.integrity.verified_changed_file_count);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for LocalizedRollbackReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("汉化 release 回滚");
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("回滚 release", &self.rolled_back_release_id);
|
||||
print_optional_field("恢复 release", self.restored_release_id.as_deref());
|
||||
print_path_field("汉化输出目录", &self.localized_output_root);
|
||||
print_path_field("current", &self.current_path);
|
||||
print_path_field("状态文件", &self.state_path);
|
||||
print_path_field("删除版本目录", &self.removed_version_path);
|
||||
print_optional_path_field("恢复 current 目标", self.restored_current_target.as_ref());
|
||||
print_field("新状态", &self.state.status);
|
||||
print_optional_field("当前 release", self.state.current_release_id.as_deref());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for bat_infrastructure::LocalizedTranslationWorkflowReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("汉化工作流状态");
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("官方 release", &self.official_release_id);
|
||||
print_optional_field("汉化 release", self.current_release_id.as_deref());
|
||||
print_field("汉化发布状态", &self.localized_release_status);
|
||||
print_field("工作流状态", &self.translation_workflow_status);
|
||||
print_field("工作流状态码", self.translation_workflow_status_code);
|
||||
print_field("工作流标签", self.translation_workflow_label);
|
||||
print_field("允许发布", format_bool(self.publish_allowed));
|
||||
print_path_field("汉化输出目录", &self.localized_output_root);
|
||||
print_path_field("状态文件", &self.state_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for bat_infrastructure::TranslationWorkerReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("翻译 provider worker");
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("官方 release", &self.official_release_id);
|
||||
print_field("provider", &self.provider);
|
||||
print_field("回收 lease", self.recovered_lease_count);
|
||||
print_field("领取任务", self.claimed_count);
|
||||
print_field("完成任务", self.completed_count);
|
||||
print_field("失败任务", self.failed_count);
|
||||
print_field("已安排重试", self.retry_scheduled_count);
|
||||
print_field("剩余任务", self.remaining_count);
|
||||
for failure in &self.failures {
|
||||
println!(
|
||||
" - {} [{}] retryable={} {}",
|
||||
failure.task_id,
|
||||
failure.failure_class,
|
||||
format_bool(failure.retryable),
|
||||
failure.failure_reason
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> {
|
||||
if value.get("running").is_some() && value.get("state_dir").is_some() {
|
||||
print_title("后台状态");
|
||||
print_json_field(value, "status", "状态");
|
||||
print_json_field(value, "message", "消息");
|
||||
print_json_field(value, "running", "运行中");
|
||||
print_json_field(value, "pid", "PID");
|
||||
print_json_field(value, "daemon_state", "后台状态");
|
||||
print_json_field(value, "rpc_available", "RPC 可用");
|
||||
print_json_field(value, "stale_pid_file", "失效 PID");
|
||||
print_json_field(value, "stale_socket", "失效 socket");
|
||||
print_json_field(value, "last_update_status", "上次同步");
|
||||
print_json_field(value, "last_success_unix_seconds", "最后成功时间");
|
||||
print_json_field(value, "last_error", "上次错误");
|
||||
print_json_field(value, "next_retry_seconds", "下次重试秒数");
|
||||
print_json_field(value, "next_check_unix_seconds", "下次检查时间");
|
||||
print_json_field(value, "current_stage", "当前阶段");
|
||||
print_json_field(value, "current_message", "当前消息");
|
||||
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", "资源目录");
|
||||
print_json_field(value, "state_dir", "状态目录");
|
||||
print_json_field(value, "socket_path", "socket");
|
||||
print_json_field(value, "log_path", "日志");
|
||||
print_json_field(value, "structured_log_path", "结构化日志");
|
||||
print_json_field(value, "rotated_structured_log_paths", "轮转日志");
|
||||
return Ok(());
|
||||
}
|
||||
if value.get("command").and_then(serde_json::Value::as_str) == Some("logs") {
|
||||
print_title("后台日志");
|
||||
print_json_field(value, "status", "状态");
|
||||
print_json_field(value, "message", "消息");
|
||||
print_json_field(value, "log_path", "日志");
|
||||
print_json_field(value, "bytes", "字节");
|
||||
print_json_field(value, "total_lines", "总行数");
|
||||
print_json_field(value, "returned_lines", "返回行数");
|
||||
if let Some(content) = value.get("content").and_then(serde_json::Value::as_str) {
|
||||
if !content.is_empty() {
|
||||
println!();
|
||||
println!("{content}");
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if value.get("command").is_some() && value.get("status").is_some() {
|
||||
print_title("后台命令");
|
||||
print_json_field(value, "command", "命令");
|
||||
print_json_field(value, "status", "状态");
|
||||
print_json_field(value, "message", "消息");
|
||||
print_json_field(value, "force", "force");
|
||||
print_json_field(value, "state_dir", "状态目录");
|
||||
print_json_field(value, "socket_path", "socket");
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(value)?);
|
||||
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}")),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn format_daemon_download_progress_json(
|
||||
value: &serde_json::Value,
|
||||
) -> Result<String, String> {
|
||||
let progress = serde_json::from_value::<DaemonDownloadProgress>(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;
|
||||
};
|
||||
if value.is_null() {
|
||||
return;
|
||||
}
|
||||
match serde_json::from_value::<OfficialVersionState>(value.clone()) {
|
||||
Ok(version_state) => print_daemon_version_state_summary(&version_state),
|
||||
Err(error) => print_field("版本状态", format!("无法解析:{error}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_daemon_version_state_summary(version_state: &OfficialVersionState) {
|
||||
print_optional_field(
|
||||
"当前完成版本",
|
||||
version_state
|
||||
.current_completed_version
|
||||
.as_ref()
|
||||
.map(|version| version.id.as_str()),
|
||||
);
|
||||
print_optional_field(
|
||||
"正在拉取版本",
|
||||
version_state
|
||||
.in_progress_version
|
||||
.as_ref()
|
||||
.map(|version| version.id.as_str()),
|
||||
);
|
||||
print_optional_field(
|
||||
"上一个可用版本",
|
||||
version_state
|
||||
.previous_available_version
|
||||
.as_ref()
|
||||
.map(|version| version.id.as_str()),
|
||||
);
|
||||
let historical_failures = visible_historical_failed_versions(version_state);
|
||||
print_field("历史失败版本数", historical_failures.len());
|
||||
if let Some(failed) = historical_failures.last() {
|
||||
print_field("最近历史失败版本", &failed.version.id);
|
||||
print_field("最近历史失败时间", failed.failed_unix_seconds);
|
||||
print_field("最近历史失败原因", &failed.error);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn visible_historical_failed_versions(
|
||||
version_state: &OfficialVersionState,
|
||||
) -> Vec<&OfficialFailedVersionRecord> {
|
||||
let in_progress = version_state.in_progress_version.as_ref();
|
||||
version_state
|
||||
.failed_versions
|
||||
.iter()
|
||||
.filter(|failed| {
|
||||
!in_progress.is_some_and(|version| version_matches_for_status(&failed.version, version))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn version_matches_for_status(left: &OfficialVersionRecord, right: &OfficialVersionRecord) -> bool {
|
||||
left.app_version == right.app_version
|
||||
&& left.bundle_version == right.bundle_version
|
||||
&& left.addressables_root == right.addressables_root
|
||||
}
|
||||
|
||||
fn print_json_field(value: &serde_json::Value, key: &str, label: &str) {
|
||||
let Some(value) = value.get(key) else {
|
||||
return;
|
||||
};
|
||||
if value.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(value) = value.as_str() {
|
||||
print_field(label, value);
|
||||
} else {
|
||||
print_field(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_title(title: &str) {
|
||||
println!("{title}");
|
||||
}
|
||||
|
||||
fn print_field(label: &str, value: impl std::fmt::Display) {
|
||||
println!(" {label:<18} {value}");
|
||||
}
|
||||
|
||||
fn print_optional_field<T>(label: &str, value: Option<T>)
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
if let Some(value) = value {
|
||||
print_field(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_path_field(label: &str, value: &Path) {
|
||||
print_field(label, value.display());
|
||||
}
|
||||
|
||||
fn print_optional_path_field(label: &str, value: Option<&PathBuf>) {
|
||||
if let Some(value) = value {
|
||||
print_path_field(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_list(label: &str, values: &[String], limit: usize) {
|
||||
if values.is_empty() {
|
||||
return;
|
||||
}
|
||||
println!(" {label}:");
|
||||
for value in values.iter().take(limit) {
|
||||
println!(" - {value}");
|
||||
}
|
||||
if values.len() > limit {
|
||||
println!(" ... 还有 {} 项", values.len() - limit);
|
||||
}
|
||||
}
|
||||
|
||||
fn format_daemon_download_progress(progress: &DaemonDownloadProgress) -> String {
|
||||
let status = progress.status.as_deref().unwrap_or("running");
|
||||
if let Some(hash) = progress.official_hash.as_ref() {
|
||||
return format!(
|
||||
"{}/{} official_hash algorithm={} expected={} actual={} data={} hash={}",
|
||||
progress.index,
|
||||
progress.total,
|
||||
hash.algorithm.as_str(),
|
||||
hash.expected,
|
||||
hash.actual,
|
||||
hash.data_url,
|
||||
hash.hash_url
|
||||
);
|
||||
}
|
||||
if status == "failed" {
|
||||
return format!(
|
||||
"{}/{} failed kind={} http={} retryable={} attempts={} quarantined={} {}",
|
||||
progress.index,
|
||||
progress.total,
|
||||
progress.failure_kind.as_deref().unwrap_or("unknown"),
|
||||
progress
|
||||
.failure_http_status
|
||||
.map(|status| status.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
progress
|
||||
.failure_retryable
|
||||
.map(|retryable| retryable.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
progress
|
||||
.failure_attempts
|
||||
.map(|attempts| attempts.to_string())
|
||||
.unwrap_or_else(|| "0".to_string()),
|
||||
progress
|
||||
.quarantined
|
||||
.map(|quarantined| quarantined.to_string())
|
||||
.unwrap_or_else(|| "false".to_string()),
|
||||
progress.url
|
||||
);
|
||||
}
|
||||
if let Some(verification) = progress.verification.as_ref() {
|
||||
return format!(
|
||||
"{}/{} {} bytes={} blake3={} zip_checked={} zip_verified={} {}",
|
||||
progress.index,
|
||||
progress.total,
|
||||
status,
|
||||
verification.actual_bytes,
|
||||
verification.actual_blake3,
|
||||
verification.zip_checked,
|
||||
verification.zip_structure_verified,
|
||||
progress.url
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"{}/{} {} {}",
|
||||
progress.index, progress.total, status, progress.url
|
||||
)
|
||||
}
|
||||
|
||||
fn print_verification_summary(summary: &OfficialVerificationSummary) {
|
||||
println!(" 校验摘要:");
|
||||
for line in verification_summary_lines(summary) {
|
||||
println!(" - {line}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn verification_summary_lines(summary: &OfficialVerificationSummary) -> Vec<String> {
|
||||
vec![
|
||||
format!(
|
||||
"官方 .hash 强校验: {} 对 ({})",
|
||||
summary.official_hash_verified_count, summary.official_hash_scope
|
||||
),
|
||||
format!(
|
||||
"本地 BLAKE3 复用校验: {} 项通过, {} 项需修复 ({})",
|
||||
summary.local_manifest_blake3_verified_count,
|
||||
summary.local_manifest_repair_needed_count,
|
||||
summary.local_manifest_blake3_scope
|
||||
),
|
||||
format!(
|
||||
"ZIP 结构校验: {} 个 ZIP 通过 ({})",
|
||||
summary.zip_structure_verified_count, summary.zip_structure_scope
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn format_bool(value: bool) -> &'static str {
|
||||
if value {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
}
|
||||
}
|
||||
|
||||
fn format_bytes(value: u64) -> String {
|
||||
const KIB: f64 = 1024.0;
|
||||
const MIB: f64 = 1024.0 * 1024.0;
|
||||
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
|
||||
let value_f = value as f64;
|
||||
if value_f >= GIB {
|
||||
format!("{value_f:.2} GiB", value_f = value_f / GIB)
|
||||
} else if value_f >= MIB {
|
||||
format!("{value_f:.2} MiB", value_f = value_f / MIB)
|
||||
} else if value_f >= KIB {
|
||||
format!("{value_f:.2} KiB", value_f = value_f / KIB)
|
||||
} else {
|
||||
format!("{value} B")
|
||||
}
|
||||
}
|
||||
|
||||
fn platform_label(platform: PatchPlatform) -> &'static str {
|
||||
match platform {
|
||||
PatchPlatform::Windows => "Windows",
|
||||
PatchPlatform::Android => "Android",
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_kind_label_for_human(kind: YostarJpResourceEndpointKind) -> &'static str {
|
||||
match kind {
|
||||
YostarJpResourceEndpointKind::TableCatalog => "table_catalog",
|
||||
YostarJpResourceEndpointKind::TableCatalogHash => "table_catalog_hash",
|
||||
YostarJpResourceEndpointKind::AddressablesCatalog => "addressables_catalog",
|
||||
YostarJpResourceEndpointKind::AddressablesCatalogHash => "addressables_catalog_hash",
|
||||
YostarJpResourceEndpointKind::BundlePackingInfo => "bundle_packing_info",
|
||||
YostarJpResourceEndpointKind::BundlePackingInfoHash => "bundle_packing_info_hash",
|
||||
YostarJpResourceEndpointKind::MediaCatalog => "media_catalog",
|
||||
YostarJpResourceEndpointKind::MediaCatalogHash => "media_catalog_hash",
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for OfficialUpdateReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("官方资源同步");
|
||||
print_field("状态", self.update_status.as_str());
|
||||
print_field("状态码", self.status_code.as_str());
|
||||
print_field("应用版本", &self.app_version);
|
||||
print_optional_field("Bundle 版本", self.bundle_version.as_deref());
|
||||
print_field("连接组", &self.connection_group);
|
||||
print_field(
|
||||
"平台",
|
||||
self.platforms
|
||||
.iter()
|
||||
.map(|platform| platform_label(*platform))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
print_field("需要下载", format_bool(self.should_download));
|
||||
print_field(
|
||||
"等待官方资源",
|
||||
format_bool(self.waiting_for_official_resources),
|
||||
);
|
||||
print_field("首次同步", format_bool(self.is_initial));
|
||||
print_field("强制刷新", format_bool(self.force));
|
||||
print_field("本地审计", format_bool(self.audit_local));
|
||||
print_field("自动修复", format_bool(self.repair));
|
||||
print_field("dry-run", format_bool(self.dry_run));
|
||||
print_path_field("官方资源目录", &self.output_root);
|
||||
print_path_field("汉化输出目录", &self.localized_output_root);
|
||||
print_field("汉化发布状态", self.localized_release_status.as_str());
|
||||
print_path_field("汉化 current", &self.localized_current_path);
|
||||
print_optional_path_field(
|
||||
"汉化 published",
|
||||
self.localized_published_version_path.as_ref(),
|
||||
);
|
||||
print_path_field("active release", &self.active_resource_root);
|
||||
print_path_field("current", &self.current_path);
|
||||
print_path_field("version state", &self.version_state_path);
|
||||
print_optional_path_field("staging", self.staging_path.as_ref());
|
||||
print_optional_path_field("published", self.published_version_path.as_ref());
|
||||
print_path_field("snapshot", &self.snapshot_path);
|
||||
print_path_field("manifest", &self.download_manifest);
|
||||
print_optional_path_field("资源变更集", self.resource_change_set_path.as_ref());
|
||||
print_optional_path_field("Crowdin handoff", self.crowdin_handoff_path.as_ref());
|
||||
print_optional_path_field("解析缓存", self.parse_cache_path.as_ref());
|
||||
print_optional_path_field("TextUnit 任务队列", self.textunit_task_queue_path.as_ref());
|
||||
print_optional_path_field(
|
||||
"Crowdin TextUnit 队列",
|
||||
self.crowdin_textunit_queue_path.as_ref(),
|
||||
);
|
||||
print_optional_path_field("写入 snapshot", self.snapshot_written.as_ref());
|
||||
print_optional_path_field(
|
||||
"启动器引导产物",
|
||||
self.launcher_bootstrap_artifact_path.as_ref(),
|
||||
);
|
||||
print_optional_path_field("bootstrap cache", self.bootstrap_cache_path.as_ref());
|
||||
print_optional_field("bootstrap 命中", self.bootstrap_cache_hit.map(format_bool));
|
||||
print_optional_field("计划 URL 数", self.download_url_count);
|
||||
print_optional_field("资源数", self.resource_count);
|
||||
print_field("已下载", self.downloaded_count);
|
||||
print_field("已续传", self.resumed_count);
|
||||
print_field("当前 manifest 复用", self.skipped_count);
|
||||
print_field("历史 release 复用", self.release_reused_count);
|
||||
print_field("CAS 复用", self.cas_reused_count);
|
||||
print_field("复用量", format_bytes(self.reused_bytes));
|
||||
print_field("传输量", format_bytes(self.transferred_bytes));
|
||||
print_field("复用诊断", self.reuse_warnings.len());
|
||||
for warning in &self.reuse_warnings {
|
||||
println!(
|
||||
" - 复用回退 [{}] {} {}",
|
||||
warning.source, warning.url, warning.message
|
||||
);
|
||||
}
|
||||
print_field("最终大小", format_bytes(self.final_bytes));
|
||||
print_field("本地校验通过", self.local_manifest_verified_count);
|
||||
print_field("需修复", self.local_manifest_repair_needed_count);
|
||||
print_field("官方 hash 校验", self.official_seed_hash_verified_count);
|
||||
print_verification_summary(&self.verification_summary);
|
||||
if let Some(summary) = self.resource_change_summary.as_ref() {
|
||||
print_field("新增资源", summary.added_count);
|
||||
print_field("变更资源", summary.modified_count);
|
||||
print_field("删除资源", summary.removed_count);
|
||||
print_field("解析候选", summary.parse_candidate_count);
|
||||
print_field("Crowdin 候选", summary.translation_candidate_count);
|
||||
}
|
||||
if let Some(summary) = self.parse_summary.as_ref() {
|
||||
print_field("解析缓存条目", summary.cache_entry_count);
|
||||
print_field("解析成功 bundle", summary.parsed_bundle_count);
|
||||
print_field("解析复用", summary.skipped_unchanged_count);
|
||||
print_field("解析不支持", summary.unsupported_count);
|
||||
print_field("解析失败", summary.failed_count);
|
||||
print_field("TextAsset", summary.text_asset_count);
|
||||
print_field("TextUnit", summary.text_unit_count);
|
||||
print_field("二进制 TextAsset", summary.skipped_binary_text_asset_count);
|
||||
print_field("TextUnit 诊断", summary.text_unit_error_count);
|
||||
}
|
||||
if let Some(summary) = self.textunit_task_summary.as_ref() {
|
||||
print_field("TextUnit 资源候选", summary.resource_candidate_count);
|
||||
print_field("TextUnit 解析条目", summary.parse_entry_count);
|
||||
print_field("TextUnit 任务", summary.queued_task_count);
|
||||
print_field("增量 TextUnit", summary.text_unit_count);
|
||||
print_field("TextUnit 无解析", summary.skipped_no_parse_entry_count);
|
||||
print_field("TextUnit 无文本", summary.skipped_no_text_unit_count);
|
||||
print_field("TextUnit 解析失败", summary.skipped_parse_failed_count);
|
||||
print_field("TextUnit 不支持", summary.skipped_unsupported_count);
|
||||
}
|
||||
print_field("catalog marker", self.addressables_marker_checked_count);
|
||||
if !self.unavailable_endpoints.is_empty() {
|
||||
let unavailable = self
|
||||
.unavailable_endpoints
|
||||
.iter()
|
||||
.map(|endpoint| {
|
||||
format!(
|
||||
"{}{} kind={} http={} {}",
|
||||
endpoint_kind_label_for_human(endpoint.kind),
|
||||
endpoint
|
||||
.platform
|
||||
.map(|platform| format!(" ({})", platform_label(platform)))
|
||||
.unwrap_or_default(),
|
||||
endpoint.error_kind,
|
||||
endpoint
|
||||
.http_status
|
||||
.map(|status| status.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
endpoint.url
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
print_list("不可用官方 endpoint", &unavailable, 8);
|
||||
}
|
||||
print_list("变更 endpoint", &self.changed_endpoint_urls, 8);
|
||||
print_list("计划 URL", &self.download_urls, 8);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> HumanReport for CommandReport<T>
|
||||
where
|
||||
T: Serialize + HumanReport,
|
||||
{
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
self.data.print_human()
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for PatchApplyReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("Patch 类型", self.kind.as_str());
|
||||
print_path_field("源文件", &self.source_path);
|
||||
print_path_field("Patch 文件", &self.patch_path);
|
||||
print_path_field("目标文件", &self.target_path);
|
||||
print_field("源字节", self.source_size);
|
||||
print_field("Patch 字节", self.patch_size);
|
||||
print_field("目标字节", self.target_size);
|
||||
print_field("源 BLAKE3", &self.source_blake3);
|
||||
print_field("Patch BLAKE3", &self.patch_blake3);
|
||||
print_field("目标 BLAKE3", &self.target_blake3);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for UnityFsPatchReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_path_field("源 bundle", &self.bundle_path);
|
||||
print_field("Serialized 文件", &self.serialized_file_path);
|
||||
print_field("Path ID", self.path_id);
|
||||
print_optional_field("字段路径", self.field_path.as_deref());
|
||||
print_path_field("目标 bundle", &self.target_path);
|
||||
print_field("源字节", self.source_size);
|
||||
print_field("替换字节", self.replacement_size);
|
||||
print_field("目标字节", self.target_size);
|
||||
print_field("源 BLAKE3", &self.source_blake3);
|
||||
print_field("替换 BLAKE3", &self.replacement_blake3);
|
||||
print_field("目标 BLAKE3", &self.target_blake3);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DaemonStartReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("状态", self.status);
|
||||
print_field("PID", self.pid);
|
||||
print_path_field("资源目录", &self.resource_output_root);
|
||||
print_path_field("汉化目录", &self.localized_output_root);
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
print_path_field("socket", &self.socket_path);
|
||||
print_path_field("日志", &self.log_path);
|
||||
print_path_field("结构化日志", &self.structured_log_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DaemonStatusReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("后台状态");
|
||||
print_field("状态", self.status);
|
||||
print_field("消息", self.message);
|
||||
print_field("运行中", format_bool(self.running));
|
||||
print_optional_field("PID", self.pid);
|
||||
print_optional_field("后台状态", self.daemon_state.as_deref());
|
||||
print_field("RPC 可用", format_bool(self.rpc_available));
|
||||
print_field("失效 PID", format_bool(self.stale_pid_file));
|
||||
print_field("失效 socket", format_bool(self.stale_socket));
|
||||
print_optional_field("上次同步", self.last_update_status.as_deref());
|
||||
print_optional_field("最后成功时间", self.last_success_unix_seconds);
|
||||
print_optional_field("上次错误", self.last_error.as_deref());
|
||||
print_optional_field("下次重试秒数", self.next_retry_seconds);
|
||||
print_optional_field("下次检查时间", self.next_check_unix_seconds);
|
||||
print_optional_field("当前阶段", self.current_stage.as_deref());
|
||||
print_optional_field("当前消息", self.current_message.as_deref());
|
||||
if let Some(progress) = self.download_progress.as_ref() {
|
||||
print_field("下载进度", format_daemon_download_progress(progress));
|
||||
}
|
||||
if let Some(version_state) = self.version_state.as_ref() {
|
||||
print_daemon_version_state_summary(version_state);
|
||||
}
|
||||
print_optional_path_field("资源目录", self.resource_output_root.as_ref());
|
||||
print_optional_path_field("汉化目录", self.localized_output_root.as_ref());
|
||||
print_optional_path_field("版本状态", self.version_state_path.as_ref());
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
print_path_field("socket", &self.socket_path);
|
||||
print_optional_path_field("日志", self.log_path.as_ref());
|
||||
print_optional_path_field("结构化日志", self.structured_log_path.as_ref());
|
||||
let rotated = self
|
||||
.rotated_structured_log_paths
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
print_list("轮转日志", &rotated, 5);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DaemonStopReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("状态", self.status);
|
||||
print_field("已停止", format_bool(self.stopped));
|
||||
print_optional_field("PID", self.pid);
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
print_path_field("socket", &self.socket_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DaemonControlReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("策略", self.strategy);
|
||||
print_optional_field("旧 PID", self.previous_pid);
|
||||
print_field("PID", self.pid);
|
||||
print_path_field("资源目录", &self.resource_output_root);
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
print_path_field("socket", &self.socket_path);
|
||||
print_path_field("日志", &self.log_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for VerifyCommandReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("健康", format_bool(self.healthy));
|
||||
print_field("远端状态", &self.remote_update_status);
|
||||
print_path_field("校验资源目录", &self.verified_resource_root);
|
||||
print_optional_field("计划 URL 数", self.planned_url_count);
|
||||
print_field("计划异常数", self.expected_plan_failure_count);
|
||||
print_field("本地 manifest 项", self.local_manifest_entry_count);
|
||||
print_field("本地校验通过", self.local_manifest_verified_count);
|
||||
print_field("本地失败数", self.local_manifest_failure_count);
|
||||
print_field("官方 hash 对", self.official_hash_pair_count);
|
||||
print_field("官方 hash 通过", self.official_hash_verified_count);
|
||||
print_verification_summary(&self.verification_summary);
|
||||
if !self.official_hash_errors.is_empty() {
|
||||
print_list("官方 hash 错误", &self.official_hash_errors, 8);
|
||||
}
|
||||
if !self.failures.is_empty() {
|
||||
println!(" 失败项:");
|
||||
for item in self.failures.iter().take(12) {
|
||||
println!(" - {} -> {}", item.status, item.destination.display());
|
||||
}
|
||||
if self.failures.len() > 12 {
|
||||
println!(" ... 还有 {} 项", self.failures.len() - 12);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for LogsReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("状态", self.status);
|
||||
print_path_field("日志", &self.log_path);
|
||||
print_field("存在", format_bool(self.exists));
|
||||
print_field("为空", format_bool(self.empty));
|
||||
print_field("字节", self.bytes);
|
||||
print_field("总行数", self.total_lines);
|
||||
print_field("返回行数", self.returned_lines);
|
||||
if !self.content.is_empty() {
|
||||
println!();
|
||||
println!("{}", self.content);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DoctorReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("健康", format_bool(self.healthy));
|
||||
println!(" 检查:");
|
||||
for check in &self.checks {
|
||||
println!(
|
||||
" [{}] {} - {}",
|
||||
if check.ok { "OK" } else { "FAIL" },
|
||||
check.name,
|
||||
check.message
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DoctorCasReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("健康", format_bool(self.healthy));
|
||||
print_path_field("CAS 目录", &self.cas_root);
|
||||
print_path_field("对象目录", &self.objects_dir);
|
||||
print_path_field("元数据库", &self.metadata_db_path);
|
||||
print_field("对象数", self.object_count);
|
||||
print_field("总字节", self.total_size);
|
||||
print_field("无效对象文件", self.invalid_object_count);
|
||||
println!(" 检查:");
|
||||
for check in &self.checks {
|
||||
println!(
|
||||
" [{}] {} - {}",
|
||||
if check.ok { "OK" } else { "FAIL" },
|
||||
check.name,
|
||||
check.message
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for CleanStableReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_path_field("资源目录", &self.output_root);
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
if !self.removed_paths.is_empty() {
|
||||
println!(" 已清理:");
|
||||
for path in &self.removed_paths {
|
||||
println!(" - {}", path.display());
|
||||
}
|
||||
}
|
||||
if !self.skipped_paths.is_empty() {
|
||||
println!(" 已跳过:");
|
||||
for path in &self.skipped_paths {
|
||||
println!(" - {}", path.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for DaemonRpcAck {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title(self.message);
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_optional_field("force", self.force.map(format_bool));
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
print_path_field("socket", &self.socket_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::report_output::print_json_value;
|
||||
use super::*;
|
||||
|
||||
const SCHEDULES_FILE_NAME: &str = "bat-schedules.json";
|
||||
|
||||
@@ -475,10 +475,14 @@ impl TaskStore {
|
||||
Ok(bytes) => {
|
||||
if let Err(error) = write_file_atomic(path, &bytes, PRIVATE_FILE_MODE, "任务历史")
|
||||
{
|
||||
eprintln!("[daemon] 任务历史落盘失败:{error}");
|
||||
super::terminal_output::print_daemon_error(format!(
|
||||
"任务历史落盘失败:{error}"
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(error) => eprintln!("[daemon] 任务历史序列化失败:{error}"),
|
||||
Err(error) => {
|
||||
super::terminal_output::print_daemon_error(format!("任务历史序列化失败:{error}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) const STARTUP_BANNER: &str = r#"
|
||||
=====================================================================================
|
||||
____ _ _ _ _ _____ _ _ _ _
|
||||
| __ )| |_ _ ___ / \ _ __ ___| |__ (_)_ _____|_ _|__ ___ | | | _(_) |_
|
||||
| _ \| | | | |/ _ \/ _ \ | '__/ __| '_ \| \ \ / / _ \ | |/ _ \ / _ \| | |/ / | __|
|
||||
| |_) | | |_| | __/ ___ \| | | (__| | | | |\ V / __/ | | (_) | (_) | | <| | |_
|
||||
|____/|_|\__,_|\__/_/ \_\_| \___|_| |_|_|\_/ \___/ |_|\___/ \___/|_|_|\_\_|\__|
|
||||
|
||||
BlueArchiveToolkit
|
||||
Official Resource Sync
|
||||
=====================================================================================
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorReport {
|
||||
status: &'static str,
|
||||
exit_code: i32,
|
||||
error: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
next_retry_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
pub(super) fn print_fatal_error(error: String, exit_code: i32) -> ! {
|
||||
let payload = ErrorReport {
|
||||
status: "error",
|
||||
exit_code,
|
||||
error,
|
||||
next_retry_seconds: None,
|
||||
};
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&payload)
|
||||
.unwrap_or_else(|_| "{\"status\":\"error\"}".to_string())
|
||||
);
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
pub(super) fn print_error_report(
|
||||
error: impl Into<String>,
|
||||
next_retry_seconds: Option<u64>,
|
||||
) -> anyhow::Result<()> {
|
||||
let payload = ErrorReport {
|
||||
status: "error",
|
||||
exit_code: EXIT_ERROR,
|
||||
error: error.into(),
|
||||
next_retry_seconds,
|
||||
};
|
||||
eprintln!("{}", serde_json::to_string_pretty(&payload)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn print_repeated_workflow_wait(
|
||||
command_name: &str,
|
||||
interval: Duration,
|
||||
completed_runs: usize,
|
||||
) {
|
||||
eprintln!(
|
||||
"{command_name} 下一轮将在 {} 后执行(已完成 {} 轮)",
|
||||
format_duration(interval),
|
||||
completed_runs
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn print_daemon_error(message: impl std::fmt::Display) {
|
||||
eprintln!("[daemon] {message}");
|
||||
}
|
||||
|
||||
pub(super) fn print_startup_banner() {
|
||||
eprintln!("{STARTUP_BANNER}");
|
||||
}
|
||||
|
||||
pub(super) fn print_env_template_created(path: &Path) {
|
||||
eprintln!(
|
||||
"已生成配置模板 {}(编辑其中的 BAT_* 配置后,直接运行 `bat` 即可按 .env 启动)",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn print_env_template_warning(path: &Path, error: impl std::fmt::Display) {
|
||||
eprintln!("警告:生成 .env 配置模板失败 {}:{error}", path.display());
|
||||
}
|
||||
|
||||
pub(super) fn print_env_read_warning(path: &Path, error: impl std::fmt::Display) {
|
||||
eprintln!("警告:读取 .env 失败 {}:{error}", path.display());
|
||||
}
|
||||
|
||||
pub(super) fn print_env_parse_warning(line_number: usize, raw_line: &str) {
|
||||
eprintln!(
|
||||
"警告:.env 第 {} 行无法解析,已忽略:{raw_line}",
|
||||
line_number
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct ProgressLogger {
|
||||
enabled: bool,
|
||||
started_at: Instant,
|
||||
structured: Option<RotatingStructuredLogger>,
|
||||
}
|
||||
|
||||
impl ProgressLogger {
|
||||
pub(super) fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
started_at: Instant::now(),
|
||||
structured: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn attach_structured_log(&mut self, path: PathBuf) {
|
||||
self.structured = Some(RotatingStructuredLogger::new(
|
||||
path,
|
||||
STRUCTURED_LOG_MAX_BYTES,
|
||||
STRUCTURED_LOG_ROTATE_KEEP,
|
||||
));
|
||||
}
|
||||
|
||||
pub(super) fn log(&mut self, event: OfficialUpdateProgress) {
|
||||
self.log_event(&event);
|
||||
}
|
||||
|
||||
pub(super) fn log_text(&mut self, stage: &str, message: impl AsRef<str>) {
|
||||
self.log_text_inner(stage, message.as_ref());
|
||||
}
|
||||
|
||||
fn log_event(&mut self, event: &OfficialUpdateProgress) {
|
||||
if self.enabled {
|
||||
print_progress_line(
|
||||
self.started_at.elapsed(),
|
||||
event.stage,
|
||||
event.message.as_str(),
|
||||
);
|
||||
}
|
||||
if let Some(structured) = self.structured.as_mut() {
|
||||
let _ = structured.write_event(self.started_at.elapsed(), event);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_text_inner(&mut self, stage: &str, message: &str) {
|
||||
if !self.enabled {
|
||||
if let Some(structured) = self.structured.as_mut() {
|
||||
let event = OfficialUpdateProgress::new(stage_to_static(stage), message);
|
||||
let _ = structured.write_event(self.started_at.elapsed(), &event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
print_progress_line(self.started_at.elapsed(), stage, message);
|
||||
if let Some(structured) = self.structured.as_mut() {
|
||||
let event = OfficialUpdateProgress::new(stage_to_static(stage), message);
|
||||
let _ = structured.write_event(self.started_at.elapsed(), &event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_progress_line(elapsed: Duration, stage: &str, message: &str) {
|
||||
eprintln!(
|
||||
"[+{} 信息] [{}] {}",
|
||||
format_duration(elapsed),
|
||||
localized_stage(stage),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct RotatingStructuredLogger {
|
||||
path: PathBuf,
|
||||
max_bytes: u64,
|
||||
keep: usize,
|
||||
}
|
||||
|
||||
impl RotatingStructuredLogger {
|
||||
pub(super) fn new(path: PathBuf, max_bytes: u64, keep: usize) -> Self {
|
||||
Self {
|
||||
path,
|
||||
max_bytes,
|
||||
keep,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn write_event(
|
||||
&mut self,
|
||||
elapsed: Duration,
|
||||
event: &OfficialUpdateProgress,
|
||||
) -> anyhow::Result<()> {
|
||||
let payload = serde_json::json!({
|
||||
"timestamp_unix_seconds": unix_seconds_now(),
|
||||
"elapsed_ms": elapsed.as_millis(),
|
||||
"level": "info",
|
||||
"stage": event.stage,
|
||||
"stage_label": localized_stage(event.stage),
|
||||
"status_code": event.status_code.as_str(),
|
||||
"status_phase": event.status_code.phase(),
|
||||
"message": event.message.as_str(),
|
||||
"download": event.download_index.map(|index| serde_json::json!({
|
||||
"index": index,
|
||||
"total": event.download_total.unwrap_or(index),
|
||||
"url": event.download_url.as_deref(),
|
||||
"status": event.download_status.as_deref(),
|
||||
"bytes": event.download_bytes,
|
||||
"transferred_bytes": event.download_transferred_bytes,
|
||||
"failure_kind": event.download_failure_kind.as_deref(),
|
||||
"failure_http_status": event.download_failure_http_status,
|
||||
"failure_retryable": event.download_failure_retryable,
|
||||
"failure_attempts": event.download_failure_attempts,
|
||||
"quarantined": event.download_quarantined,
|
||||
"verification": event.download_verification,
|
||||
"official_hash": event.official_hash_verification,
|
||||
})),
|
||||
});
|
||||
let mut line = serde_json::to_vec(&payload)?;
|
||||
line.push(b'\n');
|
||||
self.rotate_if_needed(line.len() as u64)?;
|
||||
let mut file = open_append_file(&self.path, PRIVATE_FILE_MODE, "结构化日志")
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
file.write_all(&line)?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rotate_if_needed(&self, incoming_bytes: u64) -> anyhow::Result<()> {
|
||||
let current_len = match fs::symlink_metadata(&self.path) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"结构化日志不能是 symlink:{}",
|
||||
self.path.display()
|
||||
))
|
||||
}
|
||||
Ok(metadata) if metadata.is_file() => metadata.len(),
|
||||
Ok(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"结构化日志已存在但不是普通文件:{}",
|
||||
self.path.display()
|
||||
))
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => 0,
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
if current_len.saturating_add(incoming_bytes) <= self.max_bytes {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for index in (1..=self.keep).rev() {
|
||||
let from = if index == 1 {
|
||||
self.path.clone()
|
||||
} else {
|
||||
rotated_structured_log_path(&self.path, index - 1)
|
||||
};
|
||||
let to = rotated_structured_log_path(&self.path, index);
|
||||
if !path_exists_no_follow(&from)? {
|
||||
continue;
|
||||
}
|
||||
if path_exists_no_follow(&to)? {
|
||||
fs::remove_file(&to)?;
|
||||
}
|
||||
fs::rename(&from, &to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_to_static(stage: &str) -> &'static str {
|
||||
match stage {
|
||||
"start" => "start",
|
||||
"lock" => "lock",
|
||||
"bootstrap" => "bootstrap",
|
||||
"launcher" => "launcher",
|
||||
"bootstrap-cache" => "bootstrap-cache",
|
||||
"game-main-config" => "game-main-config",
|
||||
"metadata" => "metadata",
|
||||
"server-info" => "server-info",
|
||||
"discovery" => "discovery",
|
||||
"markers" => "markers",
|
||||
"marker" => "marker",
|
||||
"catalog" => "catalog",
|
||||
"local-state" => "local-state",
|
||||
"audit" => "audit",
|
||||
"decision" => "decision",
|
||||
"plan" => "plan",
|
||||
"download" => "download",
|
||||
"snapshot" => "snapshot",
|
||||
"publish" => "publish",
|
||||
"parse" => "parse",
|
||||
"finish" => "finish",
|
||||
"watch" => "watch",
|
||||
"daemon" => "daemon",
|
||||
"dry-run" => "dry-run",
|
||||
_ => "log",
|
||||
}
|
||||
}
|
||||
|
||||
fn localized_stage(stage: &str) -> &str {
|
||||
match stage {
|
||||
"start" => "启动",
|
||||
"lock" => "锁",
|
||||
"bootstrap" => "启动发现",
|
||||
"launcher" => "启动器",
|
||||
"bootstrap-cache" => "启动缓存",
|
||||
"game-main-config" => "游戏配置",
|
||||
"metadata" => "元数据",
|
||||
"server-info" => "服务器信息",
|
||||
"discovery" => "发现",
|
||||
"markers" => "标记",
|
||||
"marker" => "标记",
|
||||
"snapshot" => "快照",
|
||||
"decision" => "决策",
|
||||
"plan" => "计划",
|
||||
"catalog" => "目录",
|
||||
"inventory" => "清单",
|
||||
"local-state" => "本地状态",
|
||||
"audit" => "审计",
|
||||
"dry-run" => "试运行",
|
||||
"download" => "下载",
|
||||
"publish" => "发布",
|
||||
"parse" => "解析",
|
||||
"resource" => "资源",
|
||||
"finish" => "完成",
|
||||
"watch" => "常驻",
|
||||
"daemon" => "后台",
|
||||
_ => stage,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn print_usage(binary: &str) {
|
||||
let usage = format!(
|
||||
r#"BlueArchiveToolkit official resource sync
|
||||
|
||||
Usage:
|
||||
{binary} [OPTIONS]
|
||||
{binary} <COMMAND> [OPTIONS]
|
||||
|
||||
Commands:
|
||||
res pull Pull official resources once or repeatedly
|
||||
res schedule Manage resource pull schedules (CLI/RPC/dashboard)
|
||||
parse run Parse current official release
|
||||
parse clear-cache Clear regenerable parse and translation queue files
|
||||
parse repack Repack a UnityFS bundle from a JSON spec
|
||||
parse schedule Manage parse schedules
|
||||
i18n run Refresh offline translation work
|
||||
i18n export Export an editable translation workbench
|
||||
i18n set Update one translation workbench entry
|
||||
i18n get Show one translation workbench entry
|
||||
i18n unset Clear one translated workbench entry
|
||||
i18n validate Validate workbench against the current official release
|
||||
i18n proofread Mark localized workflow as manual proofreading
|
||||
i18n worker run Run translation provider worker once or repeatedly
|
||||
i18n tasks / i18n task list / i18n task status Query current offline TextUnit translation task status
|
||||
i18n handoff Query current translation handoff
|
||||
i18n status Show localized release status for current official release
|
||||
i18n task update Update one provider worker task status
|
||||
i18n publish Publish a localized release from a workbench or worker results
|
||||
i18n rollback Roll back the current localized release
|
||||
i18n schedule Manage translation schedules
|
||||
refresh Run one update check, or ask a live daemon to refresh
|
||||
verify Verify remote plan, local manifest, and official seed hashes
|
||||
repair Redownload resources that fail local verification
|
||||
parse-status Show current official parse-cache status
|
||||
parse-text-units Query current official TextUnit detail index
|
||||
parse-errors Query current official parse/extraction diagnostics
|
||||
translation-tasks Query current offline TextUnit translation task status
|
||||
translation-handoff Query current translation job/unit/provider handoff
|
||||
localized-status Show localized release status for current official release
|
||||
localized-rollback Roll back the current localized release
|
||||
resource-index Query CAS + ResourceRepository index
|
||||
patch-apply Apply a Binary/JSON/Text patch file
|
||||
unityfs-patch-text-asset Patch one UnityFS TextAsset object
|
||||
unityfs-patch-string-field Patch one UnityFS TypeTree string field
|
||||
unityfs-patch-field Patch one UnityFS TypeTree field with semantic JSON
|
||||
status Show daemon state
|
||||
stop Stop daemon
|
||||
restart Restart daemon, reusing saved args unless explicit args are passed
|
||||
reload Ask daemon to rediscover metadata and force refresh
|
||||
logs Show daemon log tail
|
||||
doctor Run runtime diagnostics
|
||||
doctor cas Inspect local CAS storage
|
||||
clean-stable Remove .part/.tmp/stale lock, pid, and socket files
|
||||
|
||||
Examples:
|
||||
{binary} --auto-discover --dry-run
|
||||
{binary} --auto-discover --watch
|
||||
{binary} --auto-discover --daemon
|
||||
{binary} res pull --auto-discover --run-count 3 --interval 1h
|
||||
{binary} parse run --force --resource-root /tmp/bat-release
|
||||
{binary} parse schedule list --state-dir /tmp/bat-schedule
|
||||
{binary} i18n export --translation-file /tmp/bat-workbench.json
|
||||
{binary} i18n get --translation-file /tmp/bat-workbench.json --translation-id unit-1
|
||||
{binary} i18n unset --translation-file /tmp/bat-workbench.json --translation-id unit-1
|
||||
{binary} i18n proofread --json
|
||||
{binary} i18n worker run --provider mock --worker-concurrency 8 --run-count 2 --interval 30s
|
||||
{binary} i18n tasks --json
|
||||
{binary} i18n handoff --json
|
||||
{binary} i18n status --json
|
||||
{binary} i18n publish --translation-file /tmp/bat-workbench.json --force
|
||||
{binary} i18n publish --from-worker --localized-release-id release-manual-1
|
||||
{binary} i18n rollback --localized-release-id release-manual-1
|
||||
{binary} status
|
||||
{binary} refresh --force --json
|
||||
|
||||
Discovery:
|
||||
--auto-discover Discover app-version, server-info, connection-group
|
||||
--server-info-url <URL> Use an official server-info URL
|
||||
--server-info-file <NAME> Use an official server-info file name
|
||||
--server-info-path <PATH> Use a local server-info JSON file
|
||||
--app-version <VERSION> Override app version
|
||||
--connection-group <NAME> Override connection group
|
||||
--launcher-version <VERSION> Launcher metadata API version (default: 1.7.2)
|
||||
|
||||
Sync:
|
||||
--platforms <LIST> Platforms, e.g. Windows,Android
|
||||
--output <DIR> Official resource publish root (default: ./bat-resources)
|
||||
--localized-output <DIR> Localized output root (default: ./bat-localized)
|
||||
--import-repository Import verified release into CAS + ResourceRepository
|
||||
--no-import-repository Disable CAS + ResourceRepository import
|
||||
--import-cas-root <DIR> CAS root for official release imports
|
||||
--import-resource-db <PATH> SQLite ResourceRepository path
|
||||
--snapshot <PATH> Override snapshot path (default: <output>/current/official-sync-snapshot.json)
|
||||
--curl <PATH> curl executable (default: curl)
|
||||
--download-concurrency <N> Bounded parallel downloads (default: 8, range 1..=256)
|
||||
--proxy <URL|auto|none> curl proxy override (default: auto from env)
|
||||
--no-proxy Force direct curl connections
|
||||
--unzip <PATH> unzip executable (default: unzip)
|
||||
--dry-run Do not write sync state
|
||||
--plan Include planned URLs in dry-run
|
||||
--force Force download/refresh
|
||||
--audit-local | --no-audit-local Enable/disable local manifest audit
|
||||
--repair | --no-repair Enable/disable automatic repair
|
||||
--run-count <N> Run pull/parse/translate/publish N times
|
||||
--once Explicitly select one run
|
||||
--resource-root <DIR> Use an explicit published official release root
|
||||
--translation-file <PATH> / --workbench <PATH> Translation workbench JSON file
|
||||
--translation-id <ID> TextUnit ID for i18n set
|
||||
--translated-text <TEXT> Inline translation for i18n set
|
||||
--translated-file <PATH> UTF-8 translation file for i18n set
|
||||
--from-worker Build publish input from completed provider worker results
|
||||
--failure-reason <TEXT> Provider failure reason for i18n task update
|
||||
--provider-run-id <ID> Provider run ID for i18n task update
|
||||
--translation-provider <NAME> / --provider <NAME> Provider for i18n worker run (mock/crowdin)
|
||||
--translation-fixture <PATH> Mock/provider fixture for i18n worker run
|
||||
--worker-concurrency <N> Translation worker concurrency (default: 8, range 1..=256)
|
||||
--worker-max-attempts <N> Maximum claims per translation task
|
||||
--worker-lease-seconds <N> Lease seconds for one claimed task
|
||||
--worker-retry-backoff <DURATION> Retry backoff after retryable failure
|
||||
--worker-retry-backoff-seconds <N> Retry backoff seconds
|
||||
--worker-max-tasks <N> Maximum tasks claimed in one worker run
|
||||
--worker-id <ID> Worker ID prefix for lease diagnostics
|
||||
--localized-release-id <ID> Explicit localized publication ID
|
||||
--repack-spec <PATH> UnityFS batch repack JSON spec
|
||||
|
||||
Read-only queries:
|
||||
--offset <N> Query offset for resource-index/parse-text-units/parse-errors/translation-tasks
|
||||
--limit <N> Query limit for resource-index/parse-text-units/parse-errors/translation-tasks (1..=1000)
|
||||
--task-id <ID> Filter translation-tasks by stable task ID
|
||||
--resource-type <TYPE> asset_bundle, manifest, table_bundle, text_asset, media, other
|
||||
--hash <HASH> Filter resource-index by full CAS hash
|
||||
--path-pattern <GLOB> Filter resource-index or parse detail by path pattern
|
||||
--release-id <ID> Filter resource-index or translation-tasks by official release ID
|
||||
--platform <NAME> Filter resource-index by metadata platform
|
||||
--destination <PATH> Filter resource-index, parse detail, or translation-tasks by official destination
|
||||
--bundle-path <PATH> Filter resource-index by metadata bundle path
|
||||
--archive-entry <PATH> Filter resource-index, parse detail, or translation-tasks by ZIP/archive entry
|
||||
--task-status <STATUS> Filter translation-tasks by task status
|
||||
--worker-status <STATUS> Filter translation-tasks by provider worker status
|
||||
--parse-status <STATUS> Filter resource-index or translation-tasks by parse status
|
||||
--path-id <ID> Filter parse detail by Unity object path ID
|
||||
--class-id <ID> Filter parse detail by Unity class ID
|
||||
--field-path <PATH> Filter parse detail, or TypeTree field path after UnityFS field patch commands
|
||||
--format <NAME> Filter resource-index, parse text units, or translation-tasks by payload format
|
||||
--has-reason | --no-reason Filter translation-tasks by diagnostic reason presence
|
||||
--has-failure-reason | --no-failure-reason Filter translation-tasks by provider failure reason
|
||||
|
||||
Write patch:
|
||||
--patch-kind <binary|json|text> Patch type for patch-apply
|
||||
--source-file <PATH> Source file for patch-apply
|
||||
--patch-file <PATH> Patch JSON file for patch-apply
|
||||
--bundle-file <PATH> Source UnityFS bundle file
|
||||
--serialized-file <PATH> Serialized file path inside UnityFS
|
||||
--object-path-id <ID> Unity object path ID for UnityFS patch
|
||||
--string-field-path <PATH> Deprecated alias for UnityFS TypeTree field path
|
||||
--replacement-file <PATH> Replacement bytes or UTF-8 string file
|
||||
--replacement-text <TEXT> Inline replacement text for string-field patch
|
||||
--replacement-json <JSON> Semantic replacement, e.g. signed/enum/bit_field JSON
|
||||
--expected-name <NAME> Expected TextAsset name
|
||||
--expected-value <TEXT> Expected source string value
|
||||
--expected-json <JSON> Optional expected semantic source value
|
||||
--target-file <PATH> Target output file written atomically
|
||||
|
||||
Daemon:
|
||||
--watch Run in foreground loop
|
||||
--daemon Start detached watch process
|
||||
--state-dir <DIR> Daemon state dir (default: /tmp/bat-pid)
|
||||
--interval <DURATION> Normal check interval (default: 1h)
|
||||
--error-retry <DURATION> Retry interval after error (default: 60s)
|
||||
--quiet-up-to-date Suppress clean up-to-date reports
|
||||
--no-quiet-up-to-date Always print reports
|
||||
--tail <N> Log lines for logs command (default: 200)
|
||||
--schedule-id <ID> / --id <ID> Schedule identifier
|
||||
--schedule-action <ACTION> / --action <ACTION> Schedule action (pull/run/repack/publish)
|
||||
--schedule-at-unix <SECONDS> First execution time
|
||||
--schedule-delay <DURATION> Delay first execution from now
|
||||
--schedule-every <DURATION> Period between executions
|
||||
--schedule-count <N> Bounded execution count
|
||||
--schedule-max-runs <N> Maximum plans executed by one schedule run
|
||||
--schedule-arg <ARG> Argument passed to scheduled child command
|
||||
--schedule-clear-args Clear args during schedule update
|
||||
--schedule-clear-every Convert a periodic plan to one-shot
|
||||
--schedule-enabled/--schedule-disabled Enable/disable a schedule
|
||||
|
||||
Output:
|
||||
--human Human-readable output (default)
|
||||
--json Stable JSON output for scripts
|
||||
--progress | --no-progress Enable/disable stderr progress logs
|
||||
--banner | --no-banner Enable/disable startup banner
|
||||
-h, --help Show this help
|
||||
|
||||
Defaults:
|
||||
platforms: Windows,Android
|
||||
official resource output: ./bat-resources (current -> versions/<id>, .staging/<id>)
|
||||
localized output: ./bat-localized (separate patch/export target)
|
||||
daemon state: /tmp/bat-pid (bat.sock, bat.pid, bat-status.json, bat-daemon.log, bat-events.jsonl)
|
||||
forced refresh: {DAILY_FORCED_REFRESH_LABEL}
|
||||
"#,
|
||||
binary = binary,
|
||||
DAILY_FORCED_REFRESH_LABEL = DAILY_FORCED_REFRESH_LABEL,
|
||||
);
|
||||
eprint!("{usage}");
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::report_output::{print_json_value, print_report};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn run_parse_once(options: &CliOptions) -> anyhow::Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user