mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
bat
|
/bat
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|||||||
+7
-6
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## 1. 总体判断
|
## 1. 总体判断
|
||||||
|
|
||||||
当前项目处于 **稳定基线完成、CAS V1 已落地、Rust 官方资源同步链路已具备可持续生产运行形态、Go CLI/API/Web 仍未形成产品入口** 阶段。
|
当前项目处于 **稳定基线完成、CAS V1 已落地、Rust 官方资源同步链路已具备可持续生产运行形态、Go `bat-api` 已有 Rust daemon RPC client 但 CLI/API/Web 仍未形成产品入口** 阶段。
|
||||||
|
|
||||||
Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
||||||
|
|
||||||
@@ -198,12 +198,13 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
|
|
||||||
### Go / API / Web
|
### Go / API / Web
|
||||||
|
|
||||||
状态:**Go 产品入口仍未完成,仅有可选 CGO 兼容包装和试验性 `cmd/bat` 骨架**
|
状态:**Go 产品入口仍未完成;`bat-api` 已有 typed Rust daemon RPC client、可选 CGO 兼容包装和试验性 `cmd/bat` 骨架**
|
||||||
|
|
||||||
当前情况:
|
当前情况:
|
||||||
|
|
||||||
|
- `internal/backendrpc` 已提供 Go typed Unix socket JSON-RPC client,作为 `bat-api` 调用 Rust daemon 的默认路径。
|
||||||
- `internal/ffi/ffi.go` 已存在。
|
- `internal/ffi/ffi.go` 已存在。
|
||||||
- Go CLI 的稳定集成方向仍应优先通过 Rust `bat --json` 进程边界;`cmd/bat` 目前只是试验性骨架,不代表产品级 CLI 已完成。
|
- Go CLI 的稳定集成方向仍应优先通过 Rust daemon RPC 或 Rust `bat --json` one-shot 进程边界;`cmd/bat` 目前只是试验性骨架,不代表产品级 CLI 已完成。
|
||||||
- `cmd/`、`pkg/`、`api/`、`web/` 仍无可用产品入口,`cmd/bat` 目前只覆盖 `doctor`、`manifest inspect`、`sync plan` 这类最小演示能力。
|
- `cmd/`、`pkg/`、`api/`、`web/` 仍无可用产品入口,`cmd/bat` 目前只覆盖 `doctor`、`manifest inspect`、`sync plan` 这类最小演示能力。
|
||||||
- `go test ./...` 目前只有空测试包结果,`go vet ./...` 可作为基础门禁。
|
- `go test ./...` 目前只有空测试包结果,`go vet ./...` 可作为基础门禁。
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ git status --short --branch
|
|||||||
未执行:
|
未执行:
|
||||||
|
|
||||||
- 本次状态更新未执行一次性真实官方网络全量下载 smoke;该流程已由 `docs/guides/official-full-pull-smoke.md` 和 `scripts/official-full-pull-smoke.sh` 固化并关闭(G-018),当前处于长期运行测试阶段,运行报告将在后续提供。
|
- 本次状态更新未执行一次性真实官方网络全量下载 smoke;该流程已由 `docs/guides/official-full-pull-smoke.md` 和 `scripts/official-full-pull-smoke.sh` 固化并关闭(G-018),当前处于长期运行测试阶段,运行报告将在后续提供。
|
||||||
- Go CLI 端到端测试,因为 Go 产品入口尚未完成。
|
- Go CLI 端到端测试,因为 Go 产品入口尚未完成;`internal/backendrpc` 已有 fake transport 单测覆盖。
|
||||||
- Web/API 测试,因为 Web/API 尚未实现。
|
- Web/API 测试,因为 Web/API 尚未实现。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -269,7 +270,7 @@ GitHub issue 状态:#1 已升为 P0;#17 的实现已合入 HEAD,但 issue
|
|||||||
下一阶段必须优先完成:
|
下一阶段必须优先完成:
|
||||||
|
|
||||||
1. Issue #1(P0,主体已实现):`bat.sock` Unix socket JSON-RPC 已扩展为面向 Go 服务层的 Rust Resource Backend API。统一 envelope(`ok`、`status`、`error`、`data`、`request_id`)与 `BAT-ERR` 错误码模型已落地;`daemon.*`(status/logs/stop/reload/refresh/doctor)、`resource.*`(state/sync/verify/repair/manifest/list)、`catalog.*`(status/refresh/diff/versions)、`task.*`(status/list/cancel/logs)已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:`patch.*` / `unityfs.*`(被引擎阻塞)、`task.create`(按设计由语义方法创建)、`daemon.restart` / `daemon.clean-stable`(由 CLI 侧按进程生命周期显式执行,live RPC 内不做自重启或在线清理)、Redis 任务后端(`.env` 已预留配置键,接入时机另议)。Go 层通过 RPC 调用 Rust backend,不走 FFI(FFI 降级说明见 `docs/architecture/official-resource-backend.md` §7)。
|
1. Issue #1(P0,主体已实现):`bat.sock` Unix socket JSON-RPC 已扩展为面向 Go 服务层的 Rust Resource Backend API。统一 envelope(`ok`、`status`、`error`、`data`、`request_id`)与 `BAT-ERR` 错误码模型已落地;`daemon.*`(status/logs/stop/reload/refresh/doctor)、`resource.*`(state/sync/verify/repair/manifest/list)、`catalog.*`(status/refresh/diff/versions)、`task.*`(status/list/cancel/logs)已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:`patch.*` / `unityfs.*`(被引擎阻塞)、`task.create`(按设计由语义方法创建)、`daemon.restart` / `daemon.clean-stable`(由 CLI 侧按进程生命周期显式执行,live RPC 内不做自重启或在线清理)、Redis 任务后端(`.env` 已预留配置键,接入时机另议)。Go 层通过 RPC 调用 Rust backend,不走 FFI(FFI 降级说明见 `docs/architecture/official-resource-backend.md` §7)。
|
||||||
2. `cmd/bat` Go CLI 骨架:当前只实现 `doctor`、`manifest inspect` 和 `sync plan` 这类试验性入口,不能视作产品级 CLI;是否继续作为长期产品入口需要单独收敛。
|
2. `bat-api` Go 侧:`internal/backendrpc` typed RPC client 已起步,`cmd/bat` 当前只实现 `doctor`、`manifest inspect` 和 `sync plan` 这类试验性入口,不能视作产品级 CLI;是否继续作为长期产品入口需要单独收敛。
|
||||||
3. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流(G-011 剩余部分:自动导入触发、schema 迁移、CLI 查询)。
|
3. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流(G-011 剩余部分:自动导入触发、schema 迁移、CLI 查询)。
|
||||||
4. Issue #3(P2):AssetBundle UnityFS 基础解析校验。
|
4. Issue #3(P2):AssetBundle UnityFS 基础解析校验。
|
||||||
5. Issue #2(P2):继续逆向 Addressables catalog,提取 bundle hash/size/CRC 等可校验字段。
|
5. Issue #2(P2):继续逆向 Addressables catalog,提取 bundle hash/size/CRC 等可校验字段。
|
||||||
@@ -291,5 +292,5 @@ GitHub issue 状态:#1 已升为 P0;#17 的实现已合入 HEAD,但 issue
|
|||||||
---
|
---
|
||||||
|
|
||||||
- **当前总体完成度**:不再固定写单一百分比,以各模块状态和 issue 为准。
|
- **当前总体完成度**:不再固定写单一百分比,以各模块状态和 issue 为准。
|
||||||
- **当前基线状态**:Rust 官方资源同步链路已具备可运行闭环;Go 产品入口、`bat-api`、CAS 用户级导入和完整 AssetBundle 引擎仍未完成。
|
- **当前基线状态**:Rust 官方资源同步链路已具备可运行闭环;Go `bat-api` 已有 Rust daemon RPC client,但产品级 CLI/API、CAS 用户级导入和完整 AssetBundle 引擎仍未完成。
|
||||||
- **下一工程里程碑**:Rust Resource Backend RPC API 收尾、Go 产品入口收敛、官方同步结果接入 CAS/ResourceRepository、AssetBundle 解析起步。
|
- **下一工程里程碑**:Rust Resource Backend RPC API 收尾、Go 产品入口收敛、官方同步结果接入 CAS/ResourceRepository、AssetBundle 解析起步。
|
||||||
|
|||||||
+11
-7
@@ -1,6 +1,6 @@
|
|||||||
# BlueArchiveToolkit 文档索引
|
# BlueArchiveToolkit 文档索引
|
||||||
|
|
||||||
- **更新时间**:2026-07-20
|
- **更新时间**:2026-07-24
|
||||||
- **说明**:本索引用于快速定位当前权威文档和历史资料。
|
- **说明**:本索引用于快速定位当前权威文档和历史资料。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
- `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。
|
- `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。
|
||||||
- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。
|
- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。
|
||||||
- `docs/architecture/official-resource-backend.md`:官方资源后端职责、工作原理和审核说明。
|
- `docs/architecture/official-resource-backend.md`:官方资源后端职责、工作原理和审核说明。
|
||||||
|
- `docs/reference/rpc-backend-api.md`:Rust Resource Backend JSON-RPC 稳定 contract。
|
||||||
- `CHANGELOG.md`:版本变更记录。
|
- `CHANGELOG.md`:版本变更记录。
|
||||||
- `AGENTS.md`:AI agent 和自动化开发助手长期规则。
|
- `AGENTS.md`:AI agent 和自动化开发助手长期规则。
|
||||||
- `CONTRIBUTING.md`:贡献者协作、提交和验证要求。
|
- `CONTRIBUTING.md`:贡献者协作、提交和验证要求。
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
|
|
||||||
- `docs/architecture/README.md`:总体架构设计。
|
- `docs/architecture/README.md`:总体架构设计。
|
||||||
- `docs/api/README.md`:API 设计入口。
|
- `docs/api/README.md`:API 设计入口。
|
||||||
|
- `docs/reference/rpc-backend-api.md`:Rust Resource Backend JSON-RPC 稳定 contract。
|
||||||
- `docs/guides/development.md`:开发指南。
|
- `docs/guides/development.md`:开发指南。
|
||||||
- `docs/guides/deployment.md`:部署指南。
|
- `docs/guides/deployment.md`:部署指南。
|
||||||
- `deployments/systemd/`:官方资源同步生产 systemd unit 和环境文件示例。
|
- `deployments/systemd/`:官方资源同步生产 systemd unit 和环境文件示例。
|
||||||
@@ -78,12 +80,13 @@
|
|||||||
3. `docs/guides/official-resource-test-pull.md`
|
3. `docs/guides/official-resource-test-pull.md`
|
||||||
4. `docs/guides/official-full-pull-smoke.md`
|
4. `docs/guides/official-full-pull-smoke.md`
|
||||||
5. `docs/architecture/official-resource-backend.md`
|
5. `docs/architecture/official-resource-backend.md`
|
||||||
6. `docs/reports/CURRENT_GAPS.md`
|
6. `docs/reference/rpc-backend-api.md`
|
||||||
7. `docs/guides/baseline.md`
|
7. `docs/reports/CURRENT_GAPS.md`
|
||||||
8. `docs/architecture/README.md`
|
8. `docs/guides/baseline.md`
|
||||||
9. `docs/guides/development.md`
|
9. `docs/architecture/README.md`
|
||||||
10. `CONTRIBUTING.md`
|
10. `docs/guides/development.md`
|
||||||
11. `AGENTS.md`
|
11. `CONTRIBUTING.md`
|
||||||
|
12. `AGENTS.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -98,6 +101,7 @@
|
|||||||
- 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` 校验。
|
||||||
|
- `bat-api/internal/backendrpc` typed Unix socket JSON-RPC client。
|
||||||
- 真实官方网络全量拉取 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`,默认写入 `/tmp` 隔离目录并输出本地运行报告。
|
- 真实官方网络全量拉取 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`,默认写入 `/tmp` 隔离目录并输出本地运行报告。
|
||||||
- `bat` 运行时 progress log 已覆盖下载已完成计数、单文件下载进度和校验结果摘要。
|
- `bat` 运行时 progress log 已覆盖下载已完成计数、单文件下载进度和校验结果摘要。
|
||||||
- Addressables 当前真实形态 fixture/golden 覆盖。
|
- Addressables 当前真实形态 fixture/golden 覆盖。
|
||||||
|
|||||||
+4
-4
@@ -40,7 +40,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
1. `bat-assetbundle` 仍是占位 crate;完整 UnityFS、压缩块、TypeTree 或对象表解析未完成。
|
1. `bat-assetbundle` 仍是占位 crate;完整 UnityFS、压缩块、TypeTree 或对象表解析未完成。
|
||||||
2. `bat-patch` 的 Binary/JSON 模块仍返回明确的未实现错误,不具备真实补丁能力。
|
2. `bat-patch` 的 Binary/JSON 模块仍返回明确的未实现错误,不具备真实补丁能力。
|
||||||
3. Go CLI/API/SDK 仍没有产品级入口;当前只有 `cmd/bat` 与 `internal/ffi` 的试验/兼容骨架。
|
3. Go CLI/API/SDK 仍没有产品级入口;当前 `bat-api` module 只有 `internal/backendrpc` Rust daemon RPC client、`cmd/bat` 试验骨架与 `internal/ffi` 兼容包装。
|
||||||
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
||||||
5. 官方同步结果尚未作为用户级流程自动导入 CAS + ResourceRepository。
|
5. 官方同步结果尚未作为用户级流程自动导入 CAS + ResourceRepository。
|
||||||
6. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
6. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
||||||
@@ -141,7 +141,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
**目标**:完成可长期使用的 Content Addressable Storage。
|
**目标**:完成可长期使用的 Content Addressable Storage。
|
||||||
|
|
||||||
**当前状态**:已完成 CAS V1。Go CLI 产品入口尚未完成,当前仅存在 `cmd/bat` 的试验骨架;Rust 继续承载完整资源拉取与更新检查核心逻辑;`bat-ffi` 仅保留为可选兼容层。
|
**当前状态**:已完成 CAS V1。Go CLI/API 产品入口尚未完成;`bat-api` 已有 `internal/backendrpc` typed RPC client 和 `cmd/bat` 试验骨架;Rust 继续承载完整资源拉取与更新检查核心逻辑;`bat-ffi` 仅保留为可选兼容层。
|
||||||
|
|
||||||
交付物:
|
交付物:
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
2. 资源版本、区域、渠道、远端 URL、Hash、大小、依赖关系模型:**部分完成**。`Resource` 和官方 endpoint/snapshot 模型已扩展;仍需冻结 Go CLI/API 可见模型。
|
2. 资源版本、区域、渠道、远端 URL、Hash、大小、依赖关系模型:**部分完成**。`Resource` 和官方 endpoint/snapshot 模型已扩展;仍需冻结 Go CLI/API 可见模型。
|
||||||
3. Rust 官方下载器:**已完成当前生产入口需要的核心能力**。包含官方 URL 校验、`.part` 续传、重试、本地 manifest size+BLAKE3 校验、官方 seed `.hash` 校验和 repair。
|
3. Rust 官方下载器:**已完成当前生产入口需要的核心能力**。包含官方 URL 校验、`.part` 续传、重试、本地 manifest size+BLAKE3 校验、官方 seed `.hash` 校验和 repair。
|
||||||
4. Rust 自动更新入口:**已完成当前生产入口**。`bat` 支持 snapshot、marker diff、bootstrap cache、one-shot、`--watch`、`--daemon`、默认 1 小时间隔、北京时间固定强制刷新,以及 Unix socket JSON-RPC 后台运维命令返回。
|
4. Rust 自动更新入口:**已完成当前生产入口**。`bat` 支持 snapshot、marker diff、bootstrap cache、one-shot、`--watch`、`--daemon`、默认 1 小时间隔、北京时间固定强制刷新,以及 Unix socket JSON-RPC 后台运维命令返回。
|
||||||
5. Go 产品入口:**未完成**。当前 `cmd/bat` 仅有 `doctor`、`manifest inspect`、`sync plan` 试验能力,尚不构成产品级 CLI;若要继续由 Go 承担用户入口,需要单独收敛命令集和调用边界。
|
5. Go 产品入口:**未完成**。`internal/backendrpc` 已提供 Go 调 Rust daemon 的 typed RPC client;当前 `cmd/bat` 仅有 `doctor`、`manifest inspect`、`sync plan` 试验能力,尚不构成产品级 CLI;若要继续由 Go 承担用户入口,需要单独收敛命令集和调用边界。
|
||||||
6. 用户级 `sync`、`manifest inspect`、`cache status`:**未完成**。Rust `bat --json` 是当前稳定进程边界;`bat-ffi` 只提供可选兼容用的 Manifest inspect 和 sync plan JSON helper。
|
6. 用户级 `sync`、`manifest inspect`、`cache status`:**未完成**。Rust `bat --json` 是当前稳定进程边界;`bat-ffi` 只提供可选兼容用的 Manifest inspect 和 sync plan JSON helper。
|
||||||
7. 下载结果写入 CAS + ResourceRepository:**部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口尚未把完整下载结果自动作为用户级流程导入。
|
7. 下载结果写入 CAS + ResourceRepository:**部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口尚未把完整下载结果自动作为用户级流程导入。
|
||||||
8. Linux 生产同步不依赖已安装官方启动器:**已完成当前 Rust 入口**。`--auto-discover` 只使用官方 HTTP metadata 和临时目录解析 `GameMainConfig`。
|
8. Linux 生产同步不依赖已安装官方启动器:**已完成当前 Rust 入口**。`--auto-discover` 只使用官方 HTTP metadata 和临时目录解析 `GameMainConfig`。
|
||||||
@@ -380,7 +380,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
## 6. 近期具体任务
|
## 6. 近期具体任务
|
||||||
|
|
||||||
优先完善 Rust `bat` 后端,并同步收敛 Go 产品入口边界。当前事实是 Rust `bat` 已承担可用的资源同步/运维入口,Go `cmd/bat` 仍只是试验骨架,`bat-api` 是独立的 Go HTTP 服务目标(issue #19 / G-009):
|
优先完善 Rust `bat` 后端,并同步收敛 Go 产品入口边界。当前事实是 Rust `bat` 已承担可用的资源同步/运维入口,`bat-api/internal/backendrpc` 已提供 Go 到 Rust daemon 的本机 RPC client,Go `cmd/bat` 仍只是试验骨架,`bat-api` HTTP 服务仍是独立目标(issue #19 / G-009):
|
||||||
|
|
||||||
1. 对 issue #17 做验收并关闭或更新范围:多线程下载与指数退避实现已合入,但 GitHub issue 仍 open。
|
1. 对 issue #17 做验收并关闭或更新范围:多线程下载与指数退避实现已合入,但 GitHub issue 仍 open。
|
||||||
2. 继续逆向 Addressables catalog,扩大 bundle hash/size/CRC 等可校验字段覆盖(issue #2)。
|
2. 继续逆向 Addressables catalog,扩大 bundle hash/size/CRC 等可校验字段覆盖(issue #2)。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**BlueArchiveToolkit** 是一个面向长期维护的 Blue Archive 资源管理、解析、翻译和补丁工具套件。
|
**BlueArchiveToolkit** 是一个面向长期维护的 Blue Archive 资源管理、解析、翻译和补丁工具套件。
|
||||||
|
|
||||||
当前仓库仍不是完整产品,但 Rust 侧已经具备一条可运行的官方日服资源同步链路:可以在 Linux 上通过官方 HTTP metadata 自动发现资源入口,拉取 Windows + Android 官方资源,保存同步 snapshot,校验本地下载清单,并用 `--watch` 常驻定期检查更新。Go 侧目前只有试验性的 `cmd/bat` 骨架和 `internal/ffi` 兼容包装,产品级 CLI、API Server、Web、完整 AssetBundle 解析、翻译系统和 Patch 系统仍在后续阶段。
|
当前仓库仍不是完整产品,但 Rust 侧已经具备一条可运行的官方日服资源同步链路:可以在 Linux 上通过官方 HTTP metadata 自动发现资源入口,拉取 Windows + Android 官方资源,保存同步 snapshot,校验本地下载清单,并用 `--watch` 常驻定期检查更新。Go module 名为 `bat-api`,目前包含试验性的 `cmd/bat` 骨架、`internal/backendrpc` Rust daemon RPC client 和 `internal/ffi` 兼容包装;产品级 CLI、API Server、Web、完整 AssetBundle 解析、翻译系统和 Patch 系统仍在后续阶段。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
- `bat-cas-engine` CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发写入测试、损坏检测。
|
- `bat-cas-engine` CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发写入测试、损坏检测。
|
||||||
- `bat-infrastructure` CAS 适配层、SQLite Resource Repository、资源导入服务、官方资源 pull/update 服务。
|
- `bat-infrastructure` CAS 适配层、SQLite Resource Repository、资源导入服务、官方资源 pull/update 服务。
|
||||||
- `bat`:官方资源自动发现、全量拉取、原子发布到 `current -> versions/<id>`、本地 manifest audit/repair、`.part` 断点续传、403/404/5xx 分类重试、指数退避、顺序下载、下载 quarantine 诊断、ZIP 结构校验、官方 seed `.hash` 校验、snapshot/cache、`--watch` 常驻更新、`--daemon` 后台运行,以及 Unix socket JSON-RPC live control/backend 方法(`daemon.status/logs/stop/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list`、`catalog.*`、`task.*`)。
|
- `bat`:官方资源自动发现、全量拉取、原子发布到 `current -> versions/<id>`、本地 manifest audit/repair、`.part` 断点续传、403/404/5xx 分类重试、指数退避、顺序下载、下载 quarantine 诊断、ZIP 结构校验、官方 seed `.hash` 校验、snapshot/cache、`--watch` 常驻更新、`--daemon` 后台运行,以及 Unix socket JSON-RPC live control/backend 方法(`daemon.status/logs/stop/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list`、`catalog.*`、`task.*`)。
|
||||||
|
- `internal/backendrpc`:Go 侧 typed Unix socket JSON-RPC client,是 `bat-api` 调用 Rust daemon 的默认路径。
|
||||||
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
||||||
- 资源导入链路可将 manifest 条目写入 CAS + `ResourceRepository`,AssetBundle 会记录 UnityFS 摘要,TextAsset/Table/Media 会按类型分类索引。
|
- 资源导入链路可将 manifest 条目写入 CAS + `ResourceRepository`,AssetBundle 会记录 UnityFS 摘要,TextAsset/Table/Media 会按类型分类索引。
|
||||||
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
||||||
@@ -156,6 +157,7 @@ BlueArchiveToolkit/
|
|||||||
│ ├── bat-assetbundle/
|
│ ├── bat-assetbundle/
|
||||||
│ ├── bat-patch/
|
│ ├── bat-patch/
|
||||||
│ └── bat-ffi/ # 可选无状态 C ABI 兼容层
|
│ └── bat-ffi/ # 可选无状态 C ABI 兼容层
|
||||||
|
├── internal/backendrpc/ # Go -> Rust daemon 的 typed JSON-RPC client
|
||||||
├── internal/ffi/ # 可选 CGO 兼容包装,不是 Go CLI 主路径
|
├── internal/ffi/ # 可选 CGO 兼容包装,不是 Go CLI 主路径
|
||||||
├── cmd/ # Go CLI 试验骨架与后续产品入口
|
├── cmd/ # Go CLI 试验骨架与后续产品入口
|
||||||
├── pkg/ # Go SDK 包,尚未实现
|
├── pkg/ # Go SDK 包,尚未实现
|
||||||
|
|||||||
+3
-1
@@ -239,7 +239,9 @@ HTTPS_PROXY=http://user:pass@127.0.0.1:7890 bat --auto-discover --daemon
|
|||||||
|
|
||||||
## 6. Daemon RPC 接口
|
## 6. Daemon RPC 接口
|
||||||
|
|
||||||
`bat --daemon` 在后台状态目录下创建 `bat.sock`(Unix socket),提供**换行分隔的 JSON-RPC 2.0** 控制面。CLI 的 `status`/`stop`/`logs`/`reload`/`refresh` 优先走它;Go 服务层也应通过这个进程边界调用,而非 FFI。
|
稳定 contract 以 `docs/reference/rpc-backend-api.md` 为准,本节保留常用说明和命令行示例。
|
||||||
|
|
||||||
|
`bat --daemon` 在后台状态目录下创建 `bat.sock`(Unix socket),提供**换行分隔的 JSON-RPC 2.0** 控制面。CLI 的 `status`/`stop`/`logs`/`reload`/`refresh`/`repair` 优先走它;Go 服务层也应通过这个进程边界调用,而非 FFI 或执行 `bat` binary 后再解析 stdout。
|
||||||
|
|
||||||
### 传输与 envelope
|
### 传输与 envelope
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runDoctor() error {
|
||||||
|
fmt.Println("bat doctor: ok")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "bat-api/internal/ffi"
|
||||||
|
|
||||||
|
func InspectManifest(rawJSON string) (string, error) {
|
||||||
|
return ffi.InspectManifest(rawJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildSyncPlan(currentJSON, previousJSON string) (string, error) {
|
||||||
|
return ffi.BuildSyncPlan(currentJSON, previousJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func batVersion() (string, error) {
|
||||||
|
return ffi.Version()
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "doctor":
|
||||||
|
err = runDoctor()
|
||||||
|
case "manifest":
|
||||||
|
err = runManifest(os.Args[2:])
|
||||||
|
case "sync":
|
||||||
|
err = runSync(os.Args[2:])
|
||||||
|
case "help", "-h", "--help":
|
||||||
|
printUsage()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Println("bat - BlueArchiveToolkit CLI")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Usage:")
|
||||||
|
fmt.Println(" bat doctor")
|
||||||
|
fmt.Println(" bat manifest inspect <file>")
|
||||||
|
fmt.Println(" bat sync plan <current-json> [previous-json]")
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runManifest(args []string) error {
|
||||||
|
if len(args) < 2 || args[0] != "inspect" {
|
||||||
|
return fmt.Errorf("usage: bat manifest inspect <file>")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := args[1]
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := InspectManifest(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runSync(args []string) error {
|
||||||
|
if len(args) < 2 || args[0] != "plan" {
|
||||||
|
return fmt.Errorf("usage: bat sync plan <current-json> [previous-json]")
|
||||||
|
}
|
||||||
|
|
||||||
|
current := args[1]
|
||||||
|
previous := ""
|
||||||
|
if len(args) > 2 {
|
||||||
|
previous = args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := BuildSyncPlan(current, previous)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
|
|
||||||
集成边界:
|
集成边界:
|
||||||
|
|
||||||
1. 当前生产集成路径是运行 `bat --json` 并消费结构化 report;未来 Go CLI 若继续作为产品入口,也应优先使用该进程边界或 daemon RPC。
|
1. 当前生产集成路径是 Rust `bat --watch` / `bat --daemon` 持久运行;Go `bat-api` 应优先通过 `internal/backendrpc` 调用 daemon RPC,one-shot/fallback 场景才运行 `bat --json` 并消费结构化 report。
|
||||||
2. systemd、容器或上层 Go 进程只负责守护 `bat --watch` / `bat --daemon`,不直接接管下载器内部状态。
|
2. systemd、容器或上层 Go 进程只负责守护 `bat --watch` / `bat --daemon`,不直接接管下载器内部状态。
|
||||||
3. `bat-ffi` 只允许作为可选无状态 C ABI 兼容层,用于 Manifest inspect 和 sync plan 这类一次性 JSON helper;它不是官方同步 daemon、下载器、资源锁、CAS handle 或主控制面的承载位置。
|
3. `bat-ffi` 只允许作为可选无状态 C ABI 兼容层,用于 Manifest inspect 和 sync plan 这类一次性 JSON helper;它不是官方同步 daemon、下载器、资源锁、CAS handle 或主控制面的承载位置。
|
||||||
|
|
||||||
@@ -280,6 +280,7 @@ Linux 生产路径:
|
|||||||
|
|
||||||
daemon(`bat --daemon`)在 `<state-dir>/bat.sock` 上提供 Unix socket
|
daemon(`bat --daemon`)在 `<state-dir>/bat.sock` 上提供 Unix socket
|
||||||
JSON-RPC 2.0 服务,是面向上层服务(Go 层)的**主要跨语言边界**。
|
JSON-RPC 2.0 服务,是面向上层服务(Go 层)的**主要跨语言边界**。
|
||||||
|
稳定方法、schema 和错误语义以 `docs/reference/rpc-backend-api.md` 为准。
|
||||||
|
|
||||||
### 7.1 协议契约
|
### 7.1 协议契约
|
||||||
|
|
||||||
@@ -304,8 +305,9 @@ JSON-RPC 2.0 服务,是面向上层服务(Go 层)的**主要跨语言边
|
|||||||
### 7.2 Go 层职责边界
|
### 7.2 Go 层职责边界
|
||||||
|
|
||||||
- Go 层负责:BlueArchive 客户端请求处理、HTTP API、鉴权、内容分发,
|
- Go 层负责:BlueArchive 客户端请求处理、HTTP API、鉴权、内容分发,
|
||||||
以及作为 RPC client 调用本机 daemon(连接 `bat.sock`,每行一个
|
以及通过 `internal/backendrpc` 作为 RPC client 调用本机 daemon
|
||||||
JSON-RPC 请求/响应)。当前 Go 产品入口尚未完成,`cmd/bat` 仍是试验骨架。
|
(连接 `bat.sock`,每行一个 JSON-RPC 请求/响应)。当前 Go 产品入口
|
||||||
|
尚未完成,`cmd/bat` 仍是试验骨架。
|
||||||
- Rust daemon 负责:官方资源自动拉取与校验、catalog 更新检查、
|
- Rust daemon 负责:官方资源自动拉取与校验、catalog 更新检查、
|
||||||
版本状态与发布、任务队列/日志/错误/进度管理等长期状态型工作。
|
版本状态与发布、任务队列/日志/错误/进度管理等长期状态型工作。
|
||||||
- Go 层**不**直接嵌入 Rust FFI,不直接读写 daemon 的状态文件与资源
|
- Go 层**不**直接嵌入 Rust FFI,不直接读写 daemon 的状态文件与资源
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ go vet ./...
|
|||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
1. 当前已有 `cmd/bat` 和 `internal/ffi` 骨架,但没有 Go 产品级测试覆盖;`go test ./...` 出现 `[no test files]` 不代表 CLI/API 已完成。
|
1. 当前已有 `internal/backendrpc` fake socket 单测、`cmd/bat` 试验骨架和 `internal/ffi` 兼容包装;这些不代表 CLI/API 产品入口已完成。
|
||||||
2. 如果后续新增 Go 产品 package,必须让 `go test ./...` 和 `go vet ./...` 纳入硬性验证。
|
2. 后续新增 Go 产品 package,必须让 `go test ./...` 和 `go vet ./...` 纳入硬性验证。
|
||||||
3. 当前 `golangci-lint` 可选;当 Go 代码进入主要开发阶段后,应纳入本地门禁。
|
3. 当前 `golangci-lint` 可选;当 Go 代码进入主要开发阶段后,应纳入本地门禁。
|
||||||
4. 官方同步相关修改必须额外运行 `cargo test -p bat-infrastructure --bin bat -- --nocapture`。
|
4. 官方同步相关修改必须额外运行 `cargo test -p bat-infrastructure --bin bat -- --nocapture`。
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ go test ./...
|
|||||||
go vet ./...
|
go vet ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
Go 产品入口尚未完成,但仓库已有 `cmd/bat` 与 `internal/ffi` 骨架。提交前应运行 `go test ./...` 和 `go vet ./...`;目前输出可能只有 `[no test files]`,这代表缺少 Go 产品测试覆盖,不代表 Go CLI 已完成。
|
Go 产品入口尚未完成,但仓库已有 `internal/backendrpc` Rust daemon RPC client、`cmd/bat` 试验骨架与 `internal/ffi` 兼容包装。提交前应运行 `go test ./...` 和 `go vet ./...`;`internal/backendrpc` 使用 fake transport 覆盖 JSON-RPC envelope、错误和 typed helper,不能把这些测试误认为 Go 产品级 CLI/API 已完成。
|
||||||
|
|
||||||
### 常用聚焦命令
|
### 常用聚焦命令
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ cargo clippy -p bat-core -p bat-adapters -p bat-infrastructure --all-targets --
|
|||||||
|
|
||||||
官方资源同步、下载、daemon、status、verify 或 repair 相关改动必须至少覆盖 `bat-infrastructure` 和 `bat` 二进制测试。
|
官方资源同步、下载、daemon、status、verify 或 repair 相关改动必须至少覆盖 `bat-infrastructure` 和 `bat` 二进制测试。
|
||||||
|
|
||||||
`bat-ffi` 只是可选无状态 C ABI 兼容层。修改 FFI 导出、JSON schema、错误返回或 `internal/ffi` CGO 包装时必须运行 `cargo test -p bat-ffi -- --nocapture`;未来 Go 产品入口和生产同步默认应通过 Rust `bat --json` 或 daemon RPC 进程边界集成。
|
`bat-ffi` 只是可选无状态 C ABI 兼容层。修改 FFI 导出、JSON schema、错误返回或 `internal/ffi` CGO 包装时必须运行 `cargo test -p bat-ffi -- --nocapture`;未来 Go 产品入口和生产同步默认应通过 `internal/backendrpc` 调用 daemon RPC,或在 one-shot/fallback 场景使用 Rust `bat --json` 进程边界。
|
||||||
|
|
||||||
### 集成测试
|
### 集成测试
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Rust Resource Backend RPC API
|
||||||
|
|
||||||
|
本文档冻结本机 Rust Resource Backend API 的稳定调用边界。Go 项目
|
||||||
|
`bat-api`、Go 服务层、运维脚本和 `bat` CLI 都应以这里的 JSON-RPC
|
||||||
|
contract 为准,不应绕过 daemon 状态文件或扩展 `bat-ffi` 作为主路径。
|
||||||
|
|
||||||
|
## 传输
|
||||||
|
|
||||||
|
- 传输:Unix domain socket。
|
||||||
|
- 默认 socket:`/tmp/bat-pid/bat.sock`。
|
||||||
|
- 协议:JSON-RPC 2.0,每行一个 request,每行一个 response。
|
||||||
|
- 编码:UTF-8 JSON。
|
||||||
|
- 访问控制:依赖本机文件权限和状态目录权限;不要把 socket 暴露到公网。
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"jsonrpc":"2.0","id":1,"method":"resource.repair","params":null}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应的 JSON-RPC 顶层 `result` 一律是应用层 envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"ok": true,
|
||||||
|
"status": "accepted",
|
||||||
|
"data": {"task_id": "task-1234-1", "kind": "resource.repair"},
|
||||||
|
"request_id": "req-1234-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
应用层失败也放在 `result` 的 envelope 中:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"status": "error",
|
||||||
|
"error": {
|
||||||
|
"code": "BAT-ERR-700003",
|
||||||
|
"kind": "not_implemented",
|
||||||
|
"domain": "rpc",
|
||||||
|
"location": "rpc.dispatch",
|
||||||
|
"message": "方法尚未实现:daemon.clean-stable",
|
||||||
|
"retryable": false
|
||||||
|
},
|
||||||
|
"request_id": "req-1234-2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
只有 JSON 解析失败等传输层错误使用 JSON-RPC 顶层 `error`。
|
||||||
|
|
||||||
|
## Envelope
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ok` | bool | 应用层是否成功。 |
|
||||||
|
| `status` | string | `ok`、`accepted` 或 `error`。 |
|
||||||
|
| `data` | object/null | 成功结果。失败时省略。 |
|
||||||
|
| `error` | object/null | `ApiError`。成功时省略。 |
|
||||||
|
| `request_id` | string | daemon 进程内请求 ID,用于日志关联。 |
|
||||||
|
|
||||||
|
`ApiError` 结构以 `core/src/error_code.rs` 码表为准:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `code` | string | `BAT-ERR-<6位>`。 |
|
||||||
|
| `kind` | string | 错误类别。 |
|
||||||
|
| `domain` | string | 错误域。 |
|
||||||
|
| `location` | string | Rust 侧出错位置。 |
|
||||||
|
| `message` | string | 可诊断错误信息。 |
|
||||||
|
| `retryable` | bool | 调用方是否可以按策略重试。 |
|
||||||
|
|
||||||
|
## 方法
|
||||||
|
|
||||||
|
### daemon
|
||||||
|
|
||||||
|
| 方法 | 状态 | params | data |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `daemon.status` | 已实现 | `null` | 后台状态报告。 |
|
||||||
|
| `daemon.logs` | 已实现 | `{ "tail": 200 }` | 日志尾部报告。 |
|
||||||
|
| `daemon.stop` | 已实现 | `null` | accepted ack。 |
|
||||||
|
| `daemon.reload` | 已实现 | `null` | accepted ack。 |
|
||||||
|
| `daemon.refresh` | 已实现 | `{ "force": false }` | accepted ack。 |
|
||||||
|
| `daemon.doctor` | 已实现 | `null` | 只读诊断报告。 |
|
||||||
|
| `daemon.restart` | 保留 | `null` | live RPC 不执行;由 CLI 生命周期入口处理。 |
|
||||||
|
| `daemon.clean-stable` | 保留 | `null` | live RPC 不执行;由 CLI 离线清理入口处理。 |
|
||||||
|
|
||||||
|
`bat.status`、`bat.stop`、`bat.reload`、`bat.refresh`、`bat.logs`、
|
||||||
|
`bat.doctor`、`bat.clean-stable` 是兼容别名;新代码应使用 `daemon.*`。
|
||||||
|
|
||||||
|
### resource
|
||||||
|
|
||||||
|
| 方法 | 状态 | params | data |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `resource.state` | 已实现 | `null` | 资源发布根、版本状态、上次同步结果。 |
|
||||||
|
| `resource.sync` | 已实现 | `{ "force": false }` | `{ "task_id": "...", "kind": "resource.sync" }`。 |
|
||||||
|
| `resource.verify` | 已实现 | `null` | `{ "task_id": "...", "kind": "resource.verify" }`。 |
|
||||||
|
| `resource.repair` | 已实现 | `null` | `{ "task_id": "...", "kind": "resource.repair" }`。 |
|
||||||
|
| `resource.manifest` | 已实现 | `{ "offset": 0, "limit": 100 }` | 当前 download manifest 分页。 |
|
||||||
|
| `resource.list` | 已实现 | `{ "offset": 0, "limit": 100 }` | `resource.manifest` 的兼容别名。 |
|
||||||
|
|
||||||
|
`resource.repair` 会开启本地 manifest audit + repair,不继承 `force`。
|
||||||
|
`limit` 范围是 `1..=1000`,非法参数返回 `BAT-ERR-700002`。
|
||||||
|
|
||||||
|
### catalog
|
||||||
|
|
||||||
|
| 方法 | 状态 | params | data |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `catalog.status` | 已实现 | `null` | 当前已发布 catalog 概览。 |
|
||||||
|
| `catalog.versions` | 已实现 | `null` | current / in_progress / previous / failed。 |
|
||||||
|
| `catalog.diff` | 已实现 | `null` | 当前 snapshot 相对上一可用版本的差异。 |
|
||||||
|
| `catalog.refresh` | 已实现 | `{ "force": false }` | `{ "task_id": "...", "kind": "catalog.refresh" }`。 |
|
||||||
|
|
||||||
|
只读查询在没有可用版本时返回 `ok=true` 且 `data.available=false`。
|
||||||
|
|
||||||
|
### task
|
||||||
|
|
||||||
|
| 方法 | 状态 | params | data |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `task.status` | 已实现 | `{ "task_id": "..." }` | 单个任务记录。 |
|
||||||
|
| `task.list` | 已实现 | `null` | `{ "tasks": [...] }`。 |
|
||||||
|
| `task.cancel` | 已实现 | `{ "task_id": "..." }` | cancel ack。 |
|
||||||
|
| `task.logs` | 已实现 | `{ "task_id": "..." }` | `{ "task_id": "...", "lines": [...] }`。 |
|
||||||
|
| `task.create` | 保留 | object | 不开放通用任务入口;由语义方法创建任务。 |
|
||||||
|
|
||||||
|
任务记录:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "task-1234-1",
|
||||||
|
"kind": "resource.repair",
|
||||||
|
"status": "queued",
|
||||||
|
"stage": null,
|
||||||
|
"message": null,
|
||||||
|
"created_at": 1780000000,
|
||||||
|
"updated_at": 1780000000,
|
||||||
|
"started_at": null,
|
||||||
|
"finished_at": null,
|
||||||
|
"error": null,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`status` 取值:`queued`、`running`、`succeeded`、`failed`、`cancelled`。
|
||||||
|
daemon 重启后仍处于 `queued` 或 `running` 的历史任务会被标记为
|
||||||
|
`failed`,错误码为 `BAT-ERR-700005`。
|
||||||
|
|
||||||
|
### patch / unityfs
|
||||||
|
|
||||||
|
`patch.*` 和 `unityfs.*` 是已规划命名空间,目前返回
|
||||||
|
`BAT-ERR-700003`。它们依赖后续 `bat-patch`、`bat-assetbundle`
|
||||||
|
引擎,不作为 issue #1 的关闭阻塞项。
|
||||||
|
|
||||||
|
## Go 调用边界
|
||||||
|
|
||||||
|
`bat-api` 应直接调用本 RPC contract,不通过 `exec` 调用 `bat` binary。
|
||||||
|
`bat` binary 是人类 CLI 和进程生命周期工具;默认 `refresh` / `repair`
|
||||||
|
在 daemon 可用时也会作为 RPC client 调用同一个 socket。
|
||||||
|
|
||||||
|
禁止事项:
|
||||||
|
|
||||||
|
- Go 服务层不直接读写 `bat-status.json`、`bat-tasks.json` 等 daemon 内部状态文件。
|
||||||
|
- Go 服务层不扩展 `bat-ffi` 为主控制面。
|
||||||
|
- Go 服务层不通过 stdout 解析 `bat status --json` 作为常规调用路径。
|
||||||
|
|
||||||
@@ -177,14 +177,14 @@
|
|||||||
现象:
|
现象:
|
||||||
|
|
||||||
- 当前可用的用户同步/运维入口是 Rust `bat` binary。
|
- 当前可用的用户同步/运维入口是 Rust `bat` binary。
|
||||||
- `cmd/bat` 已存在,但仅有 `doctor`、`manifest inspect`、`sync plan` 试验能力;`doctor` 只输出固定 `ok`,`manifest`/`sync` 依赖可选 CGO/FFI helper。
|
- `internal/backendrpc` 已提供 Go 到 Rust daemon 的 typed JSON-RPC client;`cmd/bat` 已存在,但仅有 `doctor`、`manifest inspect`、`sync plan` 试验能力;`doctor` 只输出固定 `ok`,`manifest`/`sync` 依赖可选 CGO/FFI helper。
|
||||||
- Go 侧尚未实现通过 Rust `bat --json` 或 daemon RPC 包装官方同步命令、稳定 human/json 输出、真实 doctor 检查和端到端测试。
|
- Go 侧尚未实现通过 Rust `bat --json` 或 daemon RPC 包装官方同步命令、稳定 human/json 输出、真实 doctor 检查和端到端测试。
|
||||||
- 如果项目决策改为“用户 CLI 永久由 Rust `bat` 承担,Go 只做 `bat-api`/服务层”,必须同步更新 `AGENTS.md`、`PROJECT_PLAN.md` 和 issue 跟踪;在完成该决策前,不能把 Go CLI 写成已完成。
|
- 如果项目决策改为“用户 CLI 永久由 Rust `bat` 承担,Go 只做 `bat-api`/服务层”,必须同步更新 `AGENTS.md`、`PROJECT_PLAN.md` 和 issue 跟踪;在完成该决策前,不能把 Go CLI 写成已完成。
|
||||||
|
|
||||||
验收:
|
验收:
|
||||||
|
|
||||||
- `cmd/bat doctor` 做真实环境诊断,而不是固定字符串。
|
- `cmd/bat doctor` 做真实环境诊断,而不是固定字符串。
|
||||||
- `cmd/bat sync` 能通过 Rust `bat --json` 或 RPC 触发/查询官方同步,不走 FFI 控制下载器或 daemon。
|
- `cmd/bat sync` 能通过 `internal/backendrpc` daemon RPC 或 Rust `bat --json` one-shot/fallback 触发/查询官方同步,不走 FFI 控制下载器或 daemon。
|
||||||
- human/json 输出、退出码和错误码与 Rust `bat` 契约一致。
|
- human/json 输出、退出码和错误码与 Rust `bat` 契约一致。
|
||||||
- `go test ./...`、`go vet ./...` 覆盖命令解析、错误输出和至少一个 mocked Rust 边界。
|
- `go test ./...`、`go vet ./...` 覆盖命令解析、错误输出和至少一个 mocked Rust 边界。
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@
|
|||||||
## 6. 当前关闭顺序建议
|
## 6. 当前关闭顺序建议
|
||||||
|
|
||||||
1. issue #24:失败 staging 复用已补离线回归;继续核对 issue 口径、状态与后续是否仍有真实场景无法复现的残余。
|
1. issue #24:失败 staging 复用已补离线回归;继续核对 issue 口径、状态与后续是否仍有真实场景无法复现的残余。
|
||||||
2. issue #1:Rust daemon/backend API 边界收口;`resource.repair`、`resource.list`、`daemon.doctor` 已补齐,剩余确认 `patch.*` / `unityfs.*`(待引擎)、`task.create`(暂不开放通用入口)以及 `daemon.restart` / `daemon.clean-stable`(CLI 生命周期入口)的 issue 验收口径。
|
2. issue #1:Rust daemon/backend API 边界收口;`resource.repair`、`resource.list`、`daemon.doctor`、`internal/backendrpc` Go client 和稳定 RPC reference 已补齐,剩余确认 `patch.*` / `unityfs.*`(待引擎)、`task.create`(暂不开放通用入口)以及 `daemon.restart` / `daemon.clean-stable`(CLI 生命周期入口)的 issue 验收口径。
|
||||||
3. issue #17/#20/#21/#22:多线程下载入口已按最新决定移除,下载回归顺序执行并保留指数退避与单调进度上报;daemon 子进程不再透传并发参数,但 GitHub issue 仍 open。
|
3. issue #17/#20/#21/#22:多线程下载入口已按最新决定移除,下载回归顺序执行并保留指数退避与单调进度上报;daemon 子进程不再透传并发参数,但 GitHub issue 仍 open。
|
||||||
4. issue #2 / G-007:继续扩大 Addressables 可校验字段和结构变体覆盖。
|
4. issue #2 / G-007:继续扩大 Addressables 可校验字段和结构变体覆盖。
|
||||||
5. issue #3 / G-005:把 UnityFS 基础摘要推进到 `bat-assetbundle` 引擎级解析。
|
5. issue #3 / G-005:把 UnityFS 基础摘要推进到 `bat-assetbundle` 引擎级解析。
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module bluearchive-toolkit
|
module bat-api
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26.4
|
||||||
|
|||||||
@@ -3036,8 +3036,17 @@ struct CommandReport<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_sync_command(options: &CliOptions, command_name: &'static str) -> anyhow::Result<()> {
|
fn run_sync_command(options: &CliOptions, command_name: &'static str) -> anyhow::Result<()> {
|
||||||
if let Some(rpc_method) = sync_command_rpc_method(options, command_name)
|
run_sync_command_with_rpc(options, command_name, daemon_rpc_available, daemon_rpc_call)
|
||||||
.filter(|_| daemon_rpc_available(&options.state_dir))
|
}
|
||||||
|
|
||||||
|
fn run_sync_command_with_rpc(
|
||||||
|
options: &CliOptions,
|
||||||
|
command_name: &'static str,
|
||||||
|
rpc_available: impl Fn(&Path) -> bool,
|
||||||
|
rpc_call: impl Fn(&Path, &str, Option<serde_json::Value>) -> anyhow::Result<serde_json::Value>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let Some(rpc_method) =
|
||||||
|
sync_command_rpc_method(options, command_name).filter(|_| rpc_available(&options.state_dir))
|
||||||
{
|
{
|
||||||
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
|
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
|
||||||
let params = if rpc_method == RPC_METHOD_REFRESH {
|
let params = if rpc_method == RPC_METHOD_REFRESH {
|
||||||
@@ -3045,7 +3054,7 @@ fn run_sync_command(options: &CliOptions, command_name: &'static str) -> anyhow:
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let report = daemon_rpc_call(&options.state_dir, rpc_method, params)?;
|
let report = rpc_call(&options.state_dir, rpc_method, params)?;
|
||||||
print_json_value(options.output_format, &report)?;
|
print_json_value(options.output_format, &report)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -7264,6 +7273,44 @@ mod tests {
|
|||||||
assert_eq!(sync_command_rpc_method(&options, "repair"), None);
|
assert_eq!(sync_command_rpc_method(&options, "repair"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repair_command_uses_rpc_when_default_daemon_shape() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let state_dir = temp.path().join("state");
|
||||||
|
fs::create_dir_all(&state_dir).unwrap();
|
||||||
|
let seen = Arc::new(Mutex::new(Vec::<(String, Option<serde_json::Value>)>::new()));
|
||||||
|
let seen_calls = Arc::clone(&seen);
|
||||||
|
|
||||||
|
let options = parse(&[
|
||||||
|
"bat",
|
||||||
|
"repair",
|
||||||
|
"--json",
|
||||||
|
"--state-dir",
|
||||||
|
state_dir.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
run_sync_command_with_rpc(
|
||||||
|
&options,
|
||||||
|
"repair",
|
||||||
|
|_| true,
|
||||||
|
move |_state_dir, method, params| {
|
||||||
|
seen_calls
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push((method.to_string(), params.clone()));
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"task_id": "task-fake-repair",
|
||||||
|
"kind": "resource.repair",
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let seen = seen.lock().unwrap();
|
||||||
|
assert_eq!(seen.len(), 1);
|
||||||
|
assert_eq!(seen[0].0, RPC_METHOD_RESOURCE_REPAIR);
|
||||||
|
assert_eq!(seen[0].1, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_no_quiet_up_to_date_overrides_watch_default() {
|
fn explicit_no_quiet_up_to_date_overrides_watch_default() {
|
||||||
let options = parse(&[
|
let options = parse(&[
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
// Package backendrpc is the Go client for the local Rust Resource Backend.
|
||||||
|
//
|
||||||
|
// The package talks directly to bat.sock over newline-delimited JSON-RPC 2.0.
|
||||||
|
// It is the default Go integration path for bat-api; do not shell out to the
|
||||||
|
// bat binary for normal service calls.
|
||||||
|
package backendrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonRPCVersion = "2.0"
|
||||||
|
|
||||||
|
// Client calls the local Rust daemon through a Unix domain socket.
|
||||||
|
type Client struct {
|
||||||
|
SocketPath string
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// DialContext exists for tests and alternative local transports.
|
||||||
|
DialContext func(ctx context.Context, network string, address string) (net.Conn, error)
|
||||||
|
|
||||||
|
seq atomic.Uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a client for the given bat.sock path.
|
||||||
|
func New(socketPath string) *Client {
|
||||||
|
return &Client{SocketPath: socketPath, Timeout: 30 * time.Second}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIError mirrors the Rust ApiError envelope payload.
|
||||||
|
type APIError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Retryable bool `json:"retryable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil api error>"
|
||||||
|
}
|
||||||
|
if e.Code == "" {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONRPCError is a transport-level JSON-RPC error.
|
||||||
|
type JSONRPCError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *JSONRPCError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "<nil json-rpc error>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("json-rpc error %d: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envelope is the application-level payload carried in JSON-RPC result.
|
||||||
|
type Envelope struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data json.RawMessage `json:"data,omitempty"`
|
||||||
|
Error *APIError `json:"error,omitempty"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpcRequest struct {
|
||||||
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params any `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpcResponse struct {
|
||||||
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
|
Error *JSONRPCError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call invokes method and decodes the envelope data into out when out is non-nil.
|
||||||
|
func (c *Client) Call(ctx context.Context, method string, params any, out any) (*Envelope, error) {
|
||||||
|
envelope, err := c.callEnvelope(ctx, method, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !envelope.OK {
|
||||||
|
if envelope.Error != nil {
|
||||||
|
return envelope, envelope.Error
|
||||||
|
}
|
||||||
|
return envelope, errors.New("backend rpc returned ok=false without error payload")
|
||||||
|
}
|
||||||
|
if out != nil && len(envelope.Data) > 0 && string(envelope.Data) != "null" {
|
||||||
|
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
||||||
|
return envelope, fmt.Errorf("decode %s response data: %w", method, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return envelope, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) callEnvelope(ctx context.Context, method string, params any) (*Envelope, error) {
|
||||||
|
if c.SocketPath == "" {
|
||||||
|
return nil, errors.New("backend rpc socket path is empty")
|
||||||
|
}
|
||||||
|
conn, err := c.dial(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if deadline, ok := c.deadline(ctx); ok {
|
||||||
|
_ = conn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := rpcRequest{
|
||||||
|
JSONRPC: jsonRPCVersion,
|
||||||
|
ID: c.seq.Add(1),
|
||||||
|
Method: method,
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(conn).Encode(&req); err != nil {
|
||||||
|
return nil, fmt.Errorf("write backend rpc request %s: %w", method, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
line, err := bufio.NewReader(conn).ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read backend rpc response %s: %w", method, err)
|
||||||
|
}
|
||||||
|
var resp rpcResponse
|
||||||
|
if err := json.Unmarshal(line, &resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode backend rpc response %s: %w", method, err)
|
||||||
|
}
|
||||||
|
if resp.Error != nil {
|
||||||
|
return nil, resp.Error
|
||||||
|
}
|
||||||
|
if len(resp.Result) == 0 {
|
||||||
|
return nil, fmt.Errorf("backend rpc %s response missing result", method)
|
||||||
|
}
|
||||||
|
var envelope Envelope
|
||||||
|
if err := json.Unmarshal(resp.Result, &envelope); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode backend rpc envelope %s: %w", method, err)
|
||||||
|
}
|
||||||
|
return &envelope, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dial(ctx context.Context) (net.Conn, error) {
|
||||||
|
if c.DialContext != nil {
|
||||||
|
return c.DialContext(ctx, "unix", c.SocketPath)
|
||||||
|
}
|
||||||
|
var dialer net.Dialer
|
||||||
|
return dialer.DialContext(ctx, "unix", c.SocketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) deadline(ctx context.Context) (time.Time, bool) {
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
return deadline, true
|
||||||
|
}
|
||||||
|
if c.Timeout > 0 {
|
||||||
|
return time.Now().Add(c.Timeout), true
|
||||||
|
}
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolParam struct {
|
||||||
|
Force bool `json:"force"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pageParam struct {
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tailParam struct {
|
||||||
|
Tail int `json:"tail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskIDParam struct {
|
||||||
|
TaskID string `json:"task_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ack is returned by accepted daemon control methods.
|
||||||
|
type Ack struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
StateDir string `json:"state_dir"`
|
||||||
|
SocketPath string `json:"socket_path"`
|
||||||
|
Force *bool `json:"force,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskAccepted is returned when an async backend task is queued.
|
||||||
|
type TaskAccepted struct {
|
||||||
|
TaskID string `json:"task_id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskRecord is the pollable task state.
|
||||||
|
type TaskRecord struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Stage *string `json:"stage,omitempty"`
|
||||||
|
Message *string `json:"message,omitempty"`
|
||||||
|
CreatedAt uint64 `json:"created_at"`
|
||||||
|
UpdatedAt uint64 `json:"updated_at"`
|
||||||
|
StartedAt *uint64 `json:"started_at,omitempty"`
|
||||||
|
FinishedAt *uint64 `json:"finished_at,omitempty"`
|
||||||
|
Error *APIError `json:"error,omitempty"`
|
||||||
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskList struct {
|
||||||
|
Tasks []TaskRecord `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskCancelResult struct {
|
||||||
|
TaskID string `json:"task_id"`
|
||||||
|
CancelRequested bool `json:"cancel_requested"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskLogs struct {
|
||||||
|
TaskID string `json:"task_id"`
|
||||||
|
Lines []string `json:"lines"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoctorCheck struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoctorReport struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Healthy bool `json:"healthy"`
|
||||||
|
Checks []DoctorCheck `json:"checks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogsReport struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
LogPath string `json:"log_path"`
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Empty bool `json:"empty"`
|
||||||
|
Bytes int `json:"bytes"`
|
||||||
|
TotalLines int `json:"total_lines"`
|
||||||
|
ReturnedLines int `json:"returned_lines"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DaemonStatusReport struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Running bool `json:"running"`
|
||||||
|
PID *int `json:"pid,omitempty"`
|
||||||
|
ResourceOutputRoot *string `json:"resource_output_root,omitempty"`
|
||||||
|
StateDir string `json:"state_dir"`
|
||||||
|
SocketPath string `json:"socket_path"`
|
||||||
|
RPCAvailable bool `json:"rpc_available"`
|
||||||
|
CurrentStage *string `json:"current_stage,omitempty"`
|
||||||
|
CurrentMessage *string `json:"current_message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceState struct {
|
||||||
|
ResourceOutputRoot *string `json:"resource_output_root,omitempty"`
|
||||||
|
VersionState json.RawMessage `json:"version_state,omitempty"`
|
||||||
|
LastUpdateStatus *string `json:"last_update_status,omitempty"`
|
||||||
|
LastSuccessUnixSeconds *uint64 `json:"last_success_unix_seconds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceManifestEntry struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Destination string `json:"destination"`
|
||||||
|
Bytes *uint64 `json:"bytes,omitempty"`
|
||||||
|
BLAKE3 string `json:"blake3,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceManifestPage struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
ResourceRoot string `json:"resource_root,omitempty"`
|
||||||
|
ManifestVersion int `json:"manifest_version,omitempty"`
|
||||||
|
TotalEntries int `json:"total_entries,omitempty"`
|
||||||
|
Offset int `json:"offset,omitempty"`
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
Entries []ResourceManifestEntry `json:"entries,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DaemonStatus(ctx context.Context) (*DaemonStatusReport, error) {
|
||||||
|
var out DaemonStatusReport
|
||||||
|
_, err := c.Call(ctx, "daemon.status", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DaemonLogs(ctx context.Context, tail int) (*LogsReport, error) {
|
||||||
|
var out LogsReport
|
||||||
|
_, err := c.Call(ctx, "daemon.logs", tailParam{Tail: tail}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DaemonStop(ctx context.Context) (*Ack, error) {
|
||||||
|
var out Ack
|
||||||
|
_, err := c.Call(ctx, "daemon.stop", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DaemonReload(ctx context.Context) (*Ack, error) {
|
||||||
|
var out Ack
|
||||||
|
_, err := c.Call(ctx, "daemon.reload", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DaemonRefresh(ctx context.Context, force bool) (*Ack, error) {
|
||||||
|
var out Ack
|
||||||
|
_, err := c.Call(ctx, "daemon.refresh", boolParam{Force: force}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DaemonDoctor(ctx context.Context) (*DoctorReport, error) {
|
||||||
|
var out DoctorReport
|
||||||
|
_, err := c.Call(ctx, "daemon.doctor", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResourceState(ctx context.Context) (*ResourceState, error) {
|
||||||
|
var out ResourceState
|
||||||
|
_, err := c.Call(ctx, "resource.state", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResourceSync(ctx context.Context, force bool) (*TaskAccepted, error) {
|
||||||
|
var out TaskAccepted
|
||||||
|
_, err := c.Call(ctx, "resource.sync", boolParam{Force: force}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResourceVerify(ctx context.Context) (*TaskAccepted, error) {
|
||||||
|
var out TaskAccepted
|
||||||
|
_, err := c.Call(ctx, "resource.verify", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResourceRepair(ctx context.Context) (*TaskAccepted, error) {
|
||||||
|
var out TaskAccepted
|
||||||
|
_, err := c.Call(ctx, "resource.repair", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResourceManifest(ctx context.Context, offset int, limit int) (*ResourceManifestPage, error) {
|
||||||
|
var out ResourceManifestPage
|
||||||
|
_, err := c.Call(ctx, "resource.manifest", pageParam{Offset: offset, Limit: limit}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResourceList(ctx context.Context, offset int, limit int) (*ResourceManifestPage, error) {
|
||||||
|
var out ResourceManifestPage
|
||||||
|
_, err := c.Call(ctx, "resource.list", pageParam{Offset: offset, Limit: limit}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||||
|
return c.rawData(ctx, "catalog.status", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CatalogVersions(ctx context.Context) (json.RawMessage, error) {
|
||||||
|
return c.rawData(ctx, "catalog.versions", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CatalogDiff(ctx context.Context) (json.RawMessage, error) {
|
||||||
|
return c.rawData(ctx, "catalog.diff", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CatalogRefresh(ctx context.Context, force bool) (*TaskAccepted, error) {
|
||||||
|
var out TaskAccepted
|
||||||
|
_, err := c.Call(ctx, "catalog.refresh", boolParam{Force: force}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) TaskStatus(ctx context.Context, taskID string) (*TaskRecord, error) {
|
||||||
|
var out TaskRecord
|
||||||
|
_, err := c.Call(ctx, "task.status", taskIDParam{TaskID: taskID}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) TaskList(ctx context.Context) (*TaskList, error) {
|
||||||
|
var out TaskList
|
||||||
|
_, err := c.Call(ctx, "task.list", nil, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) TaskCancel(ctx context.Context, taskID string) (*TaskCancelResult, error) {
|
||||||
|
var out TaskCancelResult
|
||||||
|
_, err := c.Call(ctx, "task.cancel", taskIDParam{TaskID: taskID}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) TaskLogs(ctx context.Context, taskID string) (*TaskLogs, error) {
|
||||||
|
var out TaskLogs
|
||||||
|
_, err := c.Call(ctx, "task.logs", taskIDParam{TaskID: taskID}, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) rawData(ctx context.Context, method string, params any) (json.RawMessage, error) {
|
||||||
|
envelope, err := c.Call(ctx, method, params, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return envelope.Data, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package backendrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testRequest struct {
|
||||||
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
ID json.RawMessage `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params json.RawMessage `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testEnvelope struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
Error any `json:"error,omitempty"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testResponse struct {
|
||||||
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
ID json.RawMessage `json:"id"`
|
||||||
|
Result any `json:"result,omitempty"`
|
||||||
|
Error *JSONRPCError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestClient(t *testing.T, handler func(t *testing.T, req testRequest) testResponse) *Client {
|
||||||
|
t.Helper()
|
||||||
|
client := New("bat.sock")
|
||||||
|
client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||||
|
clientConn, serverConn := net.Pipe()
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
line, err := bufio.NewReader(conn).ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req testRequest
|
||||||
|
if err := json.Unmarshal(line, &req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := handler(t, req)
|
||||||
|
if len(resp.ID) == 0 {
|
||||||
|
resp.ID = req.ID
|
||||||
|
}
|
||||||
|
if resp.JSONRPC == "" {
|
||||||
|
resp.JSONRPC = jsonRPCVersion
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(conn).Encode(resp)
|
||||||
|
}(serverConn)
|
||||||
|
return clientConn, nil
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceRepairQueuesTask(t *testing.T) {
|
||||||
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
|
if req.Method != "resource.repair" {
|
||||||
|
t.Fatalf("method = %s", req.Method)
|
||||||
|
}
|
||||||
|
return testResponse{
|
||||||
|
Result: testEnvelope{
|
||||||
|
OK: true,
|
||||||
|
Status: "accepted",
|
||||||
|
RequestID: "req-test-1",
|
||||||
|
Data: map[string]any{
|
||||||
|
"task_id": "task-1",
|
||||||
|
"kind": "resource.repair",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
task, err := client.ResourceRepair(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResourceRepair error: %v", err)
|
||||||
|
}
|
||||||
|
if task.TaskID != "task-1" || task.Kind != "resource.repair" {
|
||||||
|
t.Fatalf("unexpected task: %#v", task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceListSendsPagination(t *testing.T) {
|
||||||
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
|
if req.Method != "resource.list" {
|
||||||
|
t.Fatalf("method = %s", req.Method)
|
||||||
|
}
|
||||||
|
var params pageParam
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
t.Fatalf("decode params: %v", err)
|
||||||
|
}
|
||||||
|
if params.Offset != 2 || params.Limit != 1 {
|
||||||
|
t.Fatalf("params = %#v", params)
|
||||||
|
}
|
||||||
|
bytes := uint64(42)
|
||||||
|
return testResponse{
|
||||||
|
Result: testEnvelope{
|
||||||
|
OK: true,
|
||||||
|
Status: "ok",
|
||||||
|
RequestID: "req-test-2",
|
||||||
|
Data: map[string]any{
|
||||||
|
"available": true,
|
||||||
|
"resource_root": "/tmp/resources/current",
|
||||||
|
"manifest_version": 1,
|
||||||
|
"total_entries": 3,
|
||||||
|
"offset": 2,
|
||||||
|
"limit": 1,
|
||||||
|
"entries": []map[string]any{
|
||||||
|
{
|
||||||
|
"url": "https://example.invalid/a.zip",
|
||||||
|
"destination": "a.zip",
|
||||||
|
"bytes": bytes,
|
||||||
|
"blake3": "abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
page, err := client.ResourceList(context.Background(), 2, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResourceList error: %v", err)
|
||||||
|
}
|
||||||
|
if !page.Available || page.TotalEntries != 3 || len(page.Entries) != 1 {
|
||||||
|
t.Fatalf("unexpected page: %#v", page)
|
||||||
|
}
|
||||||
|
if page.Entries[0].Bytes == nil || *page.Entries[0].Bytes != 42 {
|
||||||
|
t.Fatalf("unexpected entry bytes: %#v", page.Entries[0].Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplicationErrorReturnsAPIError(t *testing.T) {
|
||||||
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
|
if req.Method != "task.status" {
|
||||||
|
t.Fatalf("method = %s", req.Method)
|
||||||
|
}
|
||||||
|
return testResponse{
|
||||||
|
Result: testEnvelope{
|
||||||
|
OK: false,
|
||||||
|
Status: "error",
|
||||||
|
Error: APIError{
|
||||||
|
Code: "BAT-ERR-700004",
|
||||||
|
Kind: "task_not_found",
|
||||||
|
Domain: "rpc",
|
||||||
|
Location: "task.status",
|
||||||
|
Message: "任务不存在:missing",
|
||||||
|
Retryable: false,
|
||||||
|
},
|
||||||
|
RequestID: "req-test-3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := client.TaskStatus(context.Background(), "missing")
|
||||||
|
var apiErr *APIError
|
||||||
|
if !errors.As(err, &apiErr) {
|
||||||
|
t.Fatalf("expected APIError, got %T %v", err, err)
|
||||||
|
}
|
||||||
|
if apiErr.Code != "BAT-ERR-700004" {
|
||||||
|
t.Fatalf("code = %s", apiErr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransportErrorReturnsJSONRPCError(t *testing.T) {
|
||||||
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
|
return testResponse{
|
||||||
|
Error: &JSONRPCError{
|
||||||
|
Code: -32700,
|
||||||
|
Message: "parse error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := client.DaemonStatus(context.Background())
|
||||||
|
var rpcErr *JSONRPCError
|
||||||
|
if !errors.As(err, &rpcErr) {
|
||||||
|
t.Fatalf("expected JSONRPCError, got %T %v", err, err)
|
||||||
|
}
|
||||||
|
if rpcErr.Code != -32700 {
|
||||||
|
t.Fatalf("code = %d", rpcErr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextDeadlineIsApplied(t *testing.T) {
|
||||||
|
client := New("bat.sock")
|
||||||
|
client.Timeout = time.Millisecond
|
||||||
|
client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := client.DaemonStatus(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected dial error")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user