mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
feat(translation): 增加离线任务状态查询
This commit is contained in:
+5
-5
@@ -28,7 +28,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
11. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
11. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
||||||
12. `<output>/official-version-state.json` 会明确保存当前已完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,同一版本开始重新拉取或后续发布成功时会清理对应失败记录;`bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、结构化日志路径和轮转日志路径,人类可读输出不会把完整版本状态 JSON 内联打印。
|
12. `<output>/official-version-state.json` 会明确保存当前已完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,同一版本开始重新拉取或后续发布成功时会清理对应失败记录;`bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、结构化日志路径和轮转日志路径,人类可读输出不会把完整版本状态 JSON 内联打印。
|
||||||
13. 资源导入链路已支持 CAS + `ResourceRepository` 索引写入,官方同步可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后触发导入,默认 CAS 路径为 `<output>/.cas`、SQLite 索引为 `<output>/resources.sqlite`,也可通过 `--import-cas-root`、`--import-resource-db`、`BAT_IMPORT_CAS_ROOT`、`BAT_IMPORT_RESOURCE_DB` 覆盖;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 分页查询现有索引,数据库不存在时返回 `available=false` 且不会创建空库。`Resource` metadata 已通过 `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;当前/上一个/结构变化 catalog、失败 staging 复用、403/404、hash mismatch、CRC、metadata 迁移与 UnityFS 边界校验均有离线回归 fixture 或单测覆盖。
|
13. 资源导入链路已支持 CAS + `ResourceRepository` 索引写入,官方同步可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后触发导入,默认 CAS 路径为 `<output>/.cas`、SQLite 索引为 `<output>/resources.sqlite`,也可通过 `--import-cas-root`、`--import-resource-db`、`BAT_IMPORT_CAS_ROOT`、`BAT_IMPORT_RESOURCE_DB` 覆盖;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 分页查询现有索引,数据库不存在时返回 `available=false` 且不会创建空库。`Resource` metadata 已通过 `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;当前/上一个/结构变化 catalog、失败 staging 复用、403/404、hash mismatch、CRC、metadata 迁移与 UnityFS 边界校验均有离线回归 fixture 或单测覆盖。
|
||||||
14. 非 dry-run 官方同步在校验完成并发布后,会先对比上一完整 release 与当前 release 的 `official-download-manifest.json`,在当前 release 下写入 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`;同一 destination 只有 size 或 BLAKE3 改变才算 modified,仅 URL/CDN 根变化但内容相同不会触发解析/翻译候选。随后刷新 `official-parse-cache.json` 和 `official-textunit-index.json`,并从 Added/Modified 资源、parse cache 与 TextUnit 明细索引派生 `official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`;删除资源只进入差异记录,不进入 TextUnit/Crowdin 队列。`parse.text_units` 和 `parse.errors` RPC/CLI 可按 destination、archive entry、path id、class id、field path 和 format 查询当前 release 的 TextUnit 明细与解析错误;TextUnit 已包含 class id、field path、字段 offset/byte size 等可追溯定位。Crowdin 当前仅预留本地离线队列,不发网络请求;up-to-date 轮询在已有有效解析缓存、TextUnit 明细索引和 TextUnit 队列时只读取摘要,不重复解析。官方同步报告默认 `localized_release_status=not_localized`,含义是原版资源已经发布、汉化资源未发布;UnityFS TextAsset patch 发布成功后会写 `localized-patch-manifest.json`,校验 hash/size/diff/rollback 后才允许 `localized.status` 返回 `status=published`、`status_code=localized.published` 和 `localized_release_status=localized`。`bat` 首次启动会在二进制所在目录释放 `.env` 配置模板(`0600`),之后每次启动自动加载(不覆盖已存在的环境变量),支持 `BAT_OUTPUT`/`BAT_LOCALIZED_OUTPUT`/`BAT_IMPORT_REPOSITORY`/`BAT_IMPORT_CAS_ROOT`/`BAT_IMPORT_RESOURCE_DB`/`BAT_STATE_DIR`/`BAT_AUTO_DISCOVER`/`BAT_WATCH`/`BAT_DAEMON`/`BAT_PROXY` 等键,实现编辑 `.env` 后无参启动;优先级为命令行参数 > 进程环境变量 > `.env` > 内置默认值,`BAT_SKIP_ENV_FILE=1` 可整体禁用;Redis 键为预留。daemon 任务历史持久化在 `<state-dir>/bat-tasks.json`(版本化、`0600` 原子写),重启后任务经 `task.*` 仍可查,中断任务标记 `task_interrupted`(`BAT-ERR-700005`)。
|
14. 非 dry-run 官方同步在校验完成并发布后,会先对比上一完整 release 与当前 release 的 `official-download-manifest.json`,在当前 release 下写入 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`;同一 destination 只有 size 或 BLAKE3 改变才算 modified,仅 URL/CDN 根变化但内容相同不会触发解析/翻译候选。随后刷新 `official-parse-cache.json` 和 `official-textunit-index.json`,并从 Added/Modified 资源、parse cache 与 TextUnit 明细索引派生 `official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`;删除资源只进入差异记录,不进入 TextUnit/Crowdin 队列。`parse.text_units` 和 `parse.errors` RPC/CLI 可按 destination、archive entry、path id、class id、field path 和 format 查询当前 release 的 TextUnit 明细与解析错误;`translation.tasks` RPC/CLI 可按 release、destination、archive entry、任务状态、parse status、TextUnit format 和 reason presence 查询离线 TextUnit 翻译任务状态与跳过/失败原因;TextUnit 已包含 class id、field path、字段 offset/byte size 等可追溯定位。Crowdin 当前仅预留本地离线队列,不发网络请求;up-to-date 轮询在已有有效解析缓存、TextUnit 明细索引和 TextUnit 队列时只读取摘要,不重复解析。官方同步报告默认 `localized_release_status=not_localized`,含义是原版资源已经发布、汉化资源未发布;UnityFS TextAsset patch 发布成功后会写 `localized-patch-manifest.json`,校验 hash/size/diff/rollback 后才允许 `localized.status` 返回 `status=published`、`status_code=localized.published` 和 `localized_release_status=localized`。`bat` 首次启动会在二进制所在目录释放 `.env` 配置模板(`0600`),之后每次启动自动加载(不覆盖已存在的环境变量),支持 `BAT_OUTPUT`/`BAT_LOCALIZED_OUTPUT`/`BAT_IMPORT_REPOSITORY`/`BAT_IMPORT_CAS_ROOT`/`BAT_IMPORT_RESOURCE_DB`/`BAT_STATE_DIR`/`BAT_AUTO_DISCOVER`/`BAT_WATCH`/`BAT_DAEMON`/`BAT_PROXY` 等键,实现编辑 `.env` 后无参启动;优先级为命令行参数 > 进程环境变量 > `.env` > 内置默认值,`BAT_SKIP_ENV_FILE=1` 可整体禁用;Redis 键为预留。daemon 任务历史持久化在 `<state-dir>/bat-tasks.json`(版本化、`0600` 原子写),重启后任务经 `task.*` 仍可查,中断任务标记 `task_interrupted`(`BAT-ERR-700005`)。
|
||||||
|
|
||||||
仍需明确:这不是完整产品完成。完整 AssetBundle 重打包、翻译、Web、以及 `bat-api` 的服务器联调/可选业务扩展仍是后续工作;G-008(产品级 Go 同步 CLI)已决策关闭。真实官方网络全量拉取 smoke 已固化(G-018 已关闭);真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
仍需明确:这不是完整产品完成。完整 AssetBundle 重打包、翻译、Web、以及 `bat-api` 的服务器联调/可选业务扩展仍是后续工作;G-008(产品级 Go 同步 CLI)已决策关闭。真实官方网络全量拉取 smoke 已固化(G-018 已关闭);真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
|
|
||||||
待完成:
|
待完成:
|
||||||
|
|
||||||
- 将 `crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建;继续扩展 ResourceRepository 对翻译任务状态和 CAS 诊断的查询面。
|
- 将 `crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建;继续扩展真实 worker 状态、失败原因写回和 CAS 诊断查询面。
|
||||||
- 真实线上全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`;实际运行报告由脚本写入隔离输出目录。
|
- 真实线上全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`;实际运行报告由脚本写入隔离输出目录。
|
||||||
- 增加更多权限和极端文件系统场景测试。
|
- 增加更多权限和极端文件系统场景测试。
|
||||||
|
|
||||||
@@ -285,9 +285,9 @@ GitHub issue 状态:#1 已关闭;#17 已按 wontfix 关闭(多线程下载
|
|||||||
|
|
||||||
下一阶段必须优先完成:
|
下一阶段必须优先完成:
|
||||||
|
|
||||||
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/restart/reload/refresh/doctor)、`resource.*`(state/sync/verify/repair/manifest/list/index)、`parse.*`(status/text_units/errors)、`localized.*`(status)、`catalog.*`(status/refresh/diff/versions)、`task.*`(status/list/cancel/logs)、文件级 `patch.apply` 与 `unityfs.patch_text_asset` / `unityfs.patch_string_field` / `unityfs.patch_field` 已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:发布级 `patch build` / `patch rollback`、复杂 `unityfs.*` 语义编辑、`task.create`(按设计由语义方法创建)、`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/restart/reload/refresh/doctor)、`resource.*`(state/sync/verify/repair/manifest/list/index)、`parse.*`(status/text_units/errors)、`translation.*`(tasks)、`localized.*`(status)、`catalog.*`(status/refresh/diff/versions)、`task.*`(status/list/cancel/logs)、文件级 `patch.apply` 与 `unityfs.patch_text_asset` / `unityfs.patch_string_field` / `unityfs.patch_field` 已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:发布级 `patch build` / `patch rollback`、复杂 `unityfs.*` 语义编辑、`task.create`(按设计由语义方法创建)、`daemon.clean-stable`(由 CLI 侧按进程生命周期显式执行,live RPC 内不做在线清理)、Redis 任务后端(`.env` 已预留配置键,接入时机另议)。Go 层通过 RPC 调用 Rust backend,不走 FFI(FFI 降级说明见 `docs/architecture/official-resource-backend.md` §7)。
|
||||||
2. Go 侧:进度见 `docs/reports/GO_STATUS.md`。G-008 已关闭;`bat-api` 资源 bootstrap/分发 MVP 已落地,已含 `/v1/bootstrap`、`/v1/launcher/bootstrap`、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、动态 JSON no-store、OpenAPI、管理控制白名单、CDN Range/缓存头、RPC 周期刷新和 USERGUIDE 基础章节;剩余为远程服务器全量 release 联调、Rust/Go snapshot contract fixture(用户审核后)、refresh mtime/size 增量缓存和可选持久化。
|
2. Go 侧:进度见 `docs/reports/GO_STATUS.md`。G-008 已关闭;`bat-api` 资源 bootstrap/分发 MVP 已落地,已含 `/v1/bootstrap`、`/v1/launcher/bootstrap`、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、动态 JSON no-store、OpenAPI、管理控制白名单、CDN Range/缓存头、RPC 周期刷新和 USERGUIDE 基础章节;剩余为远程服务器全量 release 联调、Rust/Go snapshot contract fixture(用户审核后)、refresh mtime/size 增量缓存和可选持久化。
|
||||||
3. 文本提取 / 翻译队列 / Patch 输入:`official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json` 已生成并可查询 TextUnit 明细;剩余为真实 Crowdin worker、翻译记忆、Patch 构建和翻译任务状态查询。
|
3. 文本提取 / 翻译队列 / Patch 输入:`official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json` 已生成;TextUnit 明细、解析错误和离线翻译任务状态/失败原因已可查询。剩余为真实 Crowdin worker、翻译记忆、Patch 构建和 worker 状态写回。
|
||||||
4. Issue #3(P1):AssetBundle UnityFS 基础解析校验已具备离线和隔离真实样本覆盖;对象级解析继续跟踪 G-005。
|
4. Issue #3(P1):AssetBundle UnityFS 基础解析校验已具备离线和隔离真实样本覆盖;对象级解析继续跟踪 G-005。
|
||||||
5. Issue #2(P1):继续逆向 Addressables catalog,提取 bundle hash/size/CRC 等可校验字段。
|
5. Issue #2(P1):继续逆向 Addressables catalog,提取 bundle hash/size/CRC 等可校验字段。
|
||||||
6. 通用 Binary/JSON/Text Patch 基础已落地;复杂 AssetBundle 重打包和真实翻译系统仍应后置,UnityFS TextAsset patch 发布前置已具备回归测试。
|
6. 通用 Binary/JSON/Text Patch 基础已落地;复杂 AssetBundle 重打包和真实翻译系统仍应后置,UnityFS TextAsset patch 发布前置已具备回归测试。
|
||||||
@@ -309,4 +309,4 @@ GitHub issue 状态:#1 已关闭;#17 已按 wontfix 关闭(多线程下载
|
|||||||
|
|
||||||
- **当前总体完成度**:不再固定写单一百分比,以各模块状态、`GO_STATUS.md` 和 issue 为准。
|
- **当前总体完成度**:不再固定写单一百分比,以各模块状态、`GO_STATUS.md` 和 issue 为准。
|
||||||
- **当前基线状态**:Rust `bat` 同步闭环可用;Go `bat-api` 资源 bootstrap/分发 MVP + 玩家-facing HTTP 控制面 + launcher 资源引导兼容 + RPC 周期刷新/诊断 + readiness + `backendrpc` 可用;CAS 用户级导入、TextUnit 明细索引/查询、增量 Crowdin 离线队列、通用 Binary/JSON/Text Patch 基础和 UnityFS TextAsset patch 发布前置可用;完整 AssetBundle 重打包未完成。
|
- **当前基线状态**:Rust `bat` 同步闭环可用;Go `bat-api` 资源 bootstrap/分发 MVP + 玩家-facing HTTP 控制面 + launcher 资源引导兼容 + RPC 周期刷新/诊断 + readiness + `backendrpc` 可用;CAS 用户级导入、TextUnit 明细索引/查询、增量 Crowdin 离线队列、通用 Binary/JSON/Text Patch 基础和 UnityFS TextAsset patch 发布前置可用;完整 AssetBundle 重打包未完成。
|
||||||
- **下一工程里程碑**:bat-api 联调、真实 Crowdin worker / 翻译记忆、翻译任务状态查询、复杂 AssetBundle 解析和重打包。
|
- **下一工程里程碑**:bat-api 联调、真实 Crowdin worker / 翻译记忆、worker 状态写回、复杂 AssetBundle 解析和重打包。
|
||||||
|
|||||||
+3
-3
@@ -44,7 +44,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
2. `bat-patch` 已具备确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、通用 Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,当前发布级可用的是 `bat-assetbundle` + `LocalizedPatchService` 的 UnityFS TextAsset patch 前置链路。
|
2. `bat-patch` 已具备确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、通用 Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,当前发布级可用的是 `bat-assetbundle` + `LocalizedPatchService` 的 UnityFS TextAsset patch 前置链路。
|
||||||
3. Go 侧边界已冻结(见 `docs/reports/GO_STATUS.md`):同步/运维命令行 = Rust `bat`;资源分发 = `cmd/bat-api` MVP;`internal/backendrpc` 完成;`cmd/bat` 仅为试验(`bin/bat-go`)。完整游戏业务 API / Web / SDK 仍未完成。
|
3. Go 侧边界已冻结(见 `docs/reports/GO_STATUS.md`):同步/运维命令行 = Rust `bat`;资源分发 = `cmd/bat-api` MVP;`internal/backendrpc` 完成;`cmd/bat` 仅为试验(`bin/bat-go`)。完整游戏业务 API / Web / SDK 仍未完成。
|
||||||
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
||||||
5. 官方同步结果可配置为发布后自动导入 CAS + ResourceRepository,并通过 `resource.index` RPC/CLI 查询;Resource metadata 已保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 摘要,资源级查询已覆盖 release、平台、destination、archive entry、parse status 和 TextUnit format;单条 TextUnit 明细和解析错误已持久化到 `official-textunit-index.json`,可通过 `parse.text_units` / `parse.errors` 查询,翻译任务状态仍需继续推进。
|
5. 官方同步结果可配置为发布后自动导入 CAS + ResourceRepository,并通过 `resource.index` RPC/CLI 查询;Resource metadata 已保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 摘要,资源级查询已覆盖 release、平台、destination、archive entry、parse status 和 TextUnit format;单条 TextUnit 明细和解析错误已持久化到 `official-textunit-index.json`,可通过 `parse.text_units` / `parse.errors` 查询;离线 TextUnit 翻译任务状态和跳过/失败原因可通过 `translation.tasks` 查询,真实 worker 状态写回仍需继续推进。
|
||||||
6. 汉化 Patch 发布前置已具备 UnityFS TextAsset manifest/apply/diff/rollback/完整性校验和 `localized.status` 严格校验;真实 Crowdin worker、翻译记忆到完整汉化文件集合的构建仍未完成。
|
6. 汉化 Patch 发布前置已具备 UnityFS TextAsset manifest/apply/diff/rollback/完整性校验和 `localized.status` 严格校验;真实 Crowdin worker、翻译记忆到完整汉化文件集合的构建仍未完成。
|
||||||
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
||||||
8. Web、数据库迁移、OpenAPI、插件加载机制尚未实现。
|
8. Web、数据库迁移、OpenAPI、插件加载机制尚未实现。
|
||||||
@@ -178,7 +178,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
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 入口边界:**已冻结**。同步命令行 = Rust `bat`(G-008 关闭);资源分发 = `bat-api` MVP(G-009 部分完成)。详见 `docs/reports/GO_STATUS.md`。
|
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。
|
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、archive entry、parse status 和 TextUnit format 查询现有索引和资源 metadata;`parse.text_units` / `parse.errors` 可查询当前 release 的 TextUnit 明细与解析错误。剩余工作是翻译任务状态、CAS 诊断入口和面向大索引的查询优化。
|
7. 下载结果写入 CAS + ResourceRepository:**部分完成**。CAS 和 SQLite ResourceRepository 已存在,官方同步入口可用 `--import-repository` / `BAT_IMPORT_REPOSITORY=1` 在已校验 release 发布后导入;`resource.index` 可按资源级 release、平台、destination、archive entry、parse status 和 TextUnit format 查询现有索引和资源 metadata;`parse.text_units` / `parse.errors` 可查询当前 release 的 TextUnit 明细与解析错误;`translation.tasks` 可查询离线 TextUnit 翻译任务状态和跳过/失败原因。剩余工作是真实 worker 状态写回、CAS 诊断入口和面向大索引的查询优化。
|
||||||
8. Linux 生产同步不依赖已安装官方启动器:**已完成当前 Rust 入口**。`--auto-discover` 只使用官方 HTTP metadata 和临时目录解析 `GameMainConfig`。
|
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 的可重复流程;真实运行处于长期运行测试阶段,报告待后续提供。
|
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`,本地文件未变化且缓存/索引有效时跳过重复解析。
|
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`,本地文件未变化且缓存/索引有效时跳过重复解析。
|
||||||
@@ -404,7 +404,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
3. G-009 / issue #19:`bat-api` 资源分发 MVP 已落地;优先服务器联调;拉取仍在 Rust `bat`。
|
3. G-009 / issue #19:`bat-api` 资源分发 MVP 已落地;优先服务器联调;拉取仍在 Rust `bat`。
|
||||||
4. 继续 Addressables(issue #2)与 UnityFS(issue #3 / G-005)。
|
4. 继续 Addressables(issue #2)与 UnityFS(issue #3 / G-005)。
|
||||||
5. 将 `crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建。
|
5. 将 `crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建。
|
||||||
6. 继续扩展 G-011 剩余查询面:翻译任务状态、`doctor cas` 诊断入口和更丰富 TextUnit 查询。
|
6. 继续扩展 G-011 剩余查询面:真实 worker 状态写回、`doctor cas` 诊断入口和面向大索引的查询优化。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
- Go 边界权威说明:[`docs/reports/GO_STATUS.md`](docs/reports/GO_STATUS.md)(G-008 已关闭:同步 CLI = Rust `bat`)。
|
- Go 边界权威说明:[`docs/reports/GO_STATUS.md`](docs/reports/GO_STATUS.md)(G-008 已关闭:同步 CLI = Rust `bat`)。
|
||||||
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
||||||
- 资源导入链路可配置为在官方 release 发布后写入 CAS + `ResourceRepository`,资源 metadata 会记录 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式,TextAsset/Table/Media 会按类型分类索引;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、archive entry、parse status 和 TextUnit format 分页查询索引。
|
- 资源导入链路可配置为在官方 release 发布后写入 CAS + `ResourceRepository`,资源 metadata 会记录 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式,TextAsset/Table/Media 会按类型分类索引;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、archive entry、parse status 和 TextUnit format 分页查询索引。
|
||||||
- 新 release 发布后会生成 `official-resource-changes.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json`、`crowdin-translation-handoff.json` 和 `crowdin-textunit-queue.json`;其中 TextUnit/Crowdin 队列只使用 Added/Modified 资源,不调用 Crowdin 网络 API。
|
- 新 release 发布后会生成 `official-resource-changes.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json`、`crowdin-translation-handoff.json` 和 `crowdin-textunit-queue.json`;其中 TextUnit/Crowdin 队列只使用 Added/Modified 资源,不调用 Crowdin 网络 API,离线 TextUnit 翻译任务可通过 `translation.tasks` RPC/CLI 查询状态和跳过/失败原因。
|
||||||
- `LocalizedPatchService` 已具备 UnityFS TextAsset patch 发布前置能力:在 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 配置的独立汉化目录 staging 中复制官方 release、应用 TextAsset patch、写 `localized-patch-manifest.json`(hash、size、diff、rollback)、校验后发布到 `versions/<id>` 并切换 `current`。
|
- `LocalizedPatchService` 已具备 UnityFS TextAsset patch 发布前置能力:在 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 配置的独立汉化目录 staging 中复制官方 release、应用 TextAsset patch、写 `localized-patch-manifest.json`(hash、size、diff、rollback)、校验后发布到 `versions/<id>` 并切换 `current`。
|
||||||
- `bat-patch` 已具备通用 Patch 基础:确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,TypeTree 语义字段支持基础标量、固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、PPtr、managed-reference registry payload 字符串、object 字段组合、unknown fixed-size raw bytes 同长度替换和 TypeTree schema 支撑的 array/vector/map 整体替换;TextUnit 提取会把 managed-reference 类型信息保留为上下文而非翻译文本,汉化发布当前仍走 UnityFS TextAsset 前置链路。
|
- `bat-patch` 已具备通用 Patch 基础:确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,TypeTree 语义字段支持基础标量、固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、PPtr、managed-reference registry payload 字符串、object 字段组合、unknown fixed-size raw bytes 同长度替换和 TypeTree schema 支撑的 array/vector/map 整体替换;TextUnit 提取会把 managed-reference 类型信息保留为上下文而非翻译文本,汉化发布当前仍走 UnityFS TextAsset 前置链路。
|
||||||
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
||||||
|
|||||||
@@ -171,8 +171,9 @@
|
|||||||
|
|
||||||
这层的意义是把“下载到磁盘的文件”变成“可查询、可复用、可去重”的资源对象。
|
这层的意义是把“下载到磁盘的文件”变成“可查询、可复用、可去重”的资源对象。
|
||||||
`resource.index` RPC / CLI 只读查询现有 SQLite 索引;索引不存在时返回
|
`resource.index` RPC / CLI 只读查询现有 SQLite 索引;索引不存在时返回
|
||||||
`available=false`,不会因为查询创建空库。G-011 剩余工作是翻译任务状态、
|
`available=false`,不会因为查询创建空库。`translation.tasks` RPC / CLI
|
||||||
CAS 诊断入口和更丰富查询。
|
可查询离线 TextUnit 任务状态和跳过/失败 reason;G-011 剩余工作是真实
|
||||||
|
worker 状态写回、CAS 诊断入口和面向大索引的查询优化。
|
||||||
|
|
||||||
对应实现主要在:
|
对应实现主要在:
|
||||||
|
|
||||||
@@ -269,7 +270,7 @@ Linux 生产路径:
|
|||||||
- pull plan 会同时包含 discovery URLs 和 content URLs
|
- pull plan 会同时包含 discovery URLs 和 content URLs
|
||||||
- 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL
|
- 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL
|
||||||
- `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策
|
- `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策
|
||||||
- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖代理决策、下载已完成计数、单文件开始/完成状态、下载中断失败分类和校验结果摘要;支持 `--proxy` / `--no-proxy` 控制 curl 传输代理,支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC live control/backend(`daemon.status/logs/stop/restart/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list/index`、`parse.status/text_units/errors`、`localized.status`、`catalog.*`、`task.*`);`restart` 通过 Rust lifecycle controller 复用 CLI restart 路径,`clean-stable` 仍由 CLI 侧按进程生命周期显式执行,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度、失败类型、HTTP 状态和调度状态
|
- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖代理决策、下载已完成计数、单文件开始/完成状态、下载中断失败分类和校验结果摘要;支持 `--proxy` / `--no-proxy` 控制 curl 传输代理,支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC live control/backend(`daemon.status/logs/stop/restart/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list/index`、`parse.status/text_units/errors`、`translation.tasks`、`localized.status`、`catalog.*`、`task.*`);`restart` 通过 Rust lifecycle controller 复用 CLI restart 路径,`clean-stable` 仍由 CLI 侧按进程生命周期显式执行,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度、失败类型、HTTP 状态和调度状态
|
||||||
- curl 失败分类和重试策略已覆盖 404 不重试、5xx 重试耗尽后 quarantine、launcher primary CDN 失败后切换 official backup CDN
|
- curl 失败分类和重试策略已覆盖 404 不重试、5xx 重试耗尽后 quarantine、launcher primary CDN 失败后切换 official backup CDN
|
||||||
- `official-version-state.json` 已覆盖当前完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,重新拉取或成功发布后清理同版本失败记录,同版本失败 staging 会在路径安全且未发布时复用,`bat status` 会暴露版本状态摘要和最近历史失败原因
|
- `official-version-state.json` 已覆盖当前完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,重新拉取或成功发布后清理同版本失败记录,同版本失败 staging 会在路径安全且未发布时复用,`bat status` 会暴露版本状态摘要和最近历史失败原因
|
||||||
- 资源导入链路已覆盖可配置 CAS 写入、`ResourceRepository` 索引、`metadata_json` release/平台/bundle/TextAsset/TextUnit 摘要,以及 TextAsset/Table/Media 分类;`resource.index` 可只读查询现有索引
|
- 资源导入链路已覆盖可配置 CAS 写入、`ResourceRepository` 索引、`metadata_json` release/平台/bundle/TextAsset/TextUnit 摘要,以及 TextAsset/Table/Media 分类;`resource.index` 可只读查询现有索引
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ CAS V1 和 Rust 官方同步闭环完成后,下一阶段优先推进:
|
|||||||
1. 继续联调 Go `bat-api` 与 Rust daemon 的资源分发路径;Go 同步 CLI 不再作为产品目标。
|
1. 继续联调 Go `bat-api` 与 Rust daemon 的资源分发路径;Go 同步 CLI 不再作为产品目标。
|
||||||
2. 按 `docs/guides/official-full-pull-smoke.md` 在隔离目录执行真实官方网络全量下载 smoke,并保留运行报告。
|
2. 按 `docs/guides/official-full-pull-smoke.md` 在隔离目录执行真实官方网络全量下载 smoke,并保留运行报告。
|
||||||
3. 将 `crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建。
|
3. 将 `crowdin-textunit-queue.json` 接入真实 Crowdin worker、翻译记忆和 Patch 构建。
|
||||||
4. 扩展 ResourceRepository 查询面:翻译任务状态、CAS 诊断入口和更丰富 TextUnit 查询。
|
4. 扩展 ResourceRepository 查询面:真实 worker 状态写回、CAS 诊断入口和更丰富 TextUnit 查询。
|
||||||
5. 继续完善 AssetBundle 复杂对象解析、复杂对象重打包和 Patch 发布流程统一;通用 Binary/JSON/Text Patch 基础与 UnityFS TextAsset patch 发布前置链路已可用。
|
5. 继续完善 AssetBundle 复杂对象解析、复杂对象重打包和 Patch 发布流程统一;通用 Binary/JSON/Text Patch 基础与 UnityFS TextAsset patch 发布前置链路已可用。
|
||||||
|
|
||||||
优先阅读:
|
优先阅读:
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ cargo run -p bat-infrastructure --bin bat -- \
|
|||||||
cargo run -p bat-infrastructure --bin bat -- parse-status
|
cargo run -p bat-infrastructure --bin bat -- parse-status
|
||||||
cargo run -p bat-infrastructure --bin bat -- parse-text-units --limit 50
|
cargo run -p bat-infrastructure --bin bat -- parse-text-units --limit 50
|
||||||
cargo run -p bat-infrastructure --bin bat -- parse-errors --limit 50
|
cargo run -p bat-infrastructure --bin bat -- parse-errors --limit 50
|
||||||
|
cargo run -p bat-infrastructure --bin bat -- translation-tasks --task-status skipped_parse_failed --has-reason --limit 50
|
||||||
cargo run -p bat-infrastructure --bin bat -- localized-status
|
cargo run -p bat-infrastructure --bin bat -- localized-status
|
||||||
cargo run -p bat-infrastructure --bin bat -- resource-index --limit 50
|
cargo run -p bat-infrastructure --bin bat -- resource-index --limit 50
|
||||||
cargo run -p bat-infrastructure --bin bat -- resource-index --release-id <ID> --platform windows --archive-entry <PATH> --format json --limit 50
|
cargo run -p bat-infrastructure --bin bat -- resource-index --release-id <ID> --platform windows --archive-entry <PATH> --format json --limit 50
|
||||||
@@ -250,6 +251,8 @@ cargo run -p bat-infrastructure --bin bat -- resource-index --release-id <ID> --
|
|||||||
`parse-status` 会额外显示 TextUnit 明细索引和队列摘要;`parse-text-units` /
|
`parse-status` 会额外显示 TextUnit 明细索引和队列摘要;`parse-text-units` /
|
||||||
`parse-errors` 可按 destination、archive entry、path id、class id、field path
|
`parse-errors` 可按 destination、archive entry、path id、class id、field path
|
||||||
和 format 分页查询当前官方 release 的 TextUnit 明细与解析错误;
|
和 format 分页查询当前官方 release 的 TextUnit 明细与解析错误;
|
||||||
|
`translation-tasks` 可按 release、destination、archive entry、任务状态、parse status、
|
||||||
|
TextUnit format 和 reason presence 查询离线 TextUnit 翻译任务状态与跳过/失败原因;
|
||||||
`resource-index` 返回的资源 JSON 包含 release、平台、bundle path、TextAsset 和 TextUnit metadata,
|
`resource-index` 返回的资源 JSON 包含 release、平台、bundle path、TextAsset 和 TextUnit metadata,
|
||||||
并可按 release、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 做资源级过滤;
|
并可按 release、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 做资源级过滤;
|
||||||
`localized-status` 只有在 `localized-version-state.json`、`current` symlink 和
|
`localized-status` 只有在 `localized-version-state.json`、`current` symlink 和
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ SQLite `ResourceRepository`,索引不存在时返回 `ok=true` 且
|
|||||||
| `parse.status` | 已实现 | `null` | 当前官方 release 的解析缓存状态。 |
|
| `parse.status` | 已实现 | `null` | 当前官方 release 的解析缓存状态。 |
|
||||||
| `parse.text_units` | 已实现 | `{ "offset": 0, "limit": 100, "destination": "*Table*", "archive_entry": "*.bytes", "path_id": 1, "class_id": 114, "field_path": "*Text*", "format": "json" }` | 当前官方 release 的 TextUnit 明细分页。 |
|
| `parse.text_units` | 已实现 | `{ "offset": 0, "limit": 100, "destination": "*Table*", "archive_entry": "*.bytes", "path_id": 1, "class_id": 114, "field_path": "*Text*", "format": "json" }` | 当前官方 release 的 TextUnit 明细分页。 |
|
||||||
| `parse.errors` | 已实现 | `{ "offset": 0, "limit": 100, "destination": "*Table*", "archive_entry": "*.bytes", "path_id": 1, "class_id": 114, "field_path": "*Text*", "format": "json" }` | 当前官方 release 的解析错误分页。 |
|
| `parse.errors` | 已实现 | `{ "offset": 0, "limit": 100, "destination": "*Table*", "archive_entry": "*.bytes", "path_id": 1, "class_id": 114, "field_path": "*Text*", "format": "json" }` | 当前官方 release 的解析错误分页。 |
|
||||||
|
| `translation.tasks` | 已实现 | `{ "offset": 0, "limit": 100, "task_id": "...", "release_id": "...", "destination": "...", "archive_entry": "...", "status": "skipped_parse_failed", "parse_status": "failed", "format": "json", "has_reason": true }` | 当前官方 release 的离线 TextUnit 翻译任务状态分页。 |
|
||||||
|
|
||||||
`parse.status` 是只读查询;没有当前 release 或没有解析缓存时返回
|
`parse.status` 是只读查询;没有当前 release 或没有解析缓存时返回
|
||||||
`ok=true` 且 `data.available=false`。解析缓存来自官方原版资源目录,不读取
|
`ok=true` 且 `data.available=false`。解析缓存来自官方原版资源目录,不读取
|
||||||
@@ -194,6 +195,16 @@ offset 和 error。TypeTree-covered managed reference 字段会进入结构化
|
|||||||
完整 managed reference registry 等暂不支持结构会进入解析错误,而不是静默降级为
|
完整 managed reference registry 等暂不支持结构会进入解析错误,而不是静默降级为
|
||||||
低保真文本。
|
低保真文本。
|
||||||
|
|
||||||
|
`translation.tasks` 查询 `official-textunit-tasks.json`,用于查看离线
|
||||||
|
TextUnit 翻译任务候选、跳过状态和失败/跳过原因;没有当前 release 或没有任务队列时
|
||||||
|
返回 `ok=true` 且 `data.available=false`。过滤参数包括 `task_id`、
|
||||||
|
`official_release_id`/`release_id`、`destination`、`path_pattern`、
|
||||||
|
`archive_entry`、`status`/`task_status`、`parse_status`、`text_unit_format`/`format`
|
||||||
|
和 `has_reason`。`entries[]` 会包含 `official_release_id`、`destination`、
|
||||||
|
`archive_entry`、`parse_status`、`status`、`reason`、TextAsset/TextUnit 摘要和
|
||||||
|
校验指纹。当前它只反映 Rust 侧离线 handoff 状态;真实 Crowdin worker 消费、
|
||||||
|
重试和完成状态仍需后续 worker 模型写回后才能暴露。
|
||||||
|
|
||||||
### localized
|
### localized
|
||||||
|
|
||||||
| 方法 | 状态 | params | data |
|
| 方法 | 状态 | params | data |
|
||||||
|
|||||||
@@ -287,7 +287,8 @@
|
|||||||
- `Resource` metadata 已通过 SQLite `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;`resource.index` 会返回这些 metadata。
|
- `Resource` metadata 已通过 SQLite `metadata_json` 兼容迁移保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式;`resource.index` 会返回这些 metadata。
|
||||||
- 官方 release 发布后会持久化 `official-textunit-index.json`,记录单条 TextUnit 和解析错误;`parse.text_units` / `parse.errors` RPC 和 `parse-text-units` / `parse-errors` CLI 可按 destination、archive entry、path id、class id、field path 和 format 分页过滤。
|
- 官方 release 发布后会持久化 `official-textunit-index.json`,记录单条 TextUnit 和解析错误;`parse.text_units` / `parse.errors` RPC 和 `parse-text-units` / `parse-errors` CLI 可按 destination、archive entry、path id、class id、field path 和 format 分页过滤。
|
||||||
- 官方 release 发布后会从 Added/Modified 资源、parse cache 和 TextUnit 明细索引派生 `official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`;删除资源不会进入队列。
|
- 官方 release 发布后会从 Added/Modified 资源、parse cache 和 TextUnit 明细索引派生 `official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`;删除资源不会进入队列。
|
||||||
- 翻译任务状态和 Crowdin worker 失败原因查询仍需补齐。
|
- `translation.tasks` RPC/CLI 已能分页查询 `official-textunit-tasks.json` 中的离线 TextUnit 任务状态、parse 状态、TextUnit format 和跳过/失败 reason。
|
||||||
|
- 真实 Crowdin worker 消费后的任务状态、重试次数、远端失败原因和完成结果查询仍需补齐。
|
||||||
|
|
||||||
验收:
|
验收:
|
||||||
|
|
||||||
@@ -297,7 +298,8 @@
|
|||||||
- `official-parse-cache.json` 的 bundle、zip entry、TextAsset 和 TextUnit 摘要能进入 ResourceRepository 查询面。
|
- `official-parse-cache.json` 的 bundle、zip entry、TextAsset 和 TextUnit 摘要能进入 ResourceRepository 查询面。
|
||||||
- `parse.status` 能报告 TextUnit 索引、TextUnit 队列路径与摘要。
|
- `parse.status` 能报告 TextUnit 索引、TextUnit 队列路径与摘要。
|
||||||
- `parse.text_units` / `parse.errors` 能分页查询当前 release 的 TextUnit 明细和解析错误。
|
- `parse.text_units` / `parse.errors` 能分页查询当前 release 的 TextUnit 明细和解析错误。
|
||||||
- Crowdin handoff 被后续翻译 worker 消费后,任务状态和失败原因能反查到对应官方 release 与资源 destination。
|
- 离线 TextUnit 翻译任务状态和跳过/失败 reason 可反查到对应官方 release、资源 destination 和 archive entry。
|
||||||
|
- Crowdin handoff 被后续翻译 worker 消费后,worker 状态、远端失败原因和完成结果能反查到对应官方 release 与资源 destination。
|
||||||
|
|
||||||
解析补全路线图:
|
解析补全路线图:
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ use bat_infrastructure::{
|
|||||||
redact_proxy_url, resolve_curl_proxy, validate_output_root, validate_runtime_state_dir,
|
redact_proxy_url, resolve_curl_proxy, validate_output_root, validate_runtime_state_dir,
|
||||||
write_file_atomic, CurlProxyConfig, CurlProxyMode, OfficialEndpointMarkerRole,
|
write_file_atomic, CurlProxyConfig, CurlProxyMode, OfficialEndpointMarkerRole,
|
||||||
OfficialFailedVersionRecord, OfficialResourceHashVerification, OfficialResourceVerification,
|
OfficialFailedVersionRecord, OfficialResourceHashVerification, OfficialResourceVerification,
|
||||||
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialUpdateConfig, OfficialUpdateProgress,
|
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialTextUnitTaskQuery,
|
||||||
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
|
||||||
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
|
OfficialUpdateSnapshot, OfficialUpdateStatus, OfficialVerificationSummary,
|
||||||
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, SqliteResourceRepository,
|
OfficialVersionRecord, OfficialVersionState, PatchApplyKind, PatchApplyParams,
|
||||||
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
|
PatchApplyReport, ReleaseFlowStatusCode, SqliteResourceRepository, UnityFsFieldPatchParams,
|
||||||
UnityFsTextAssetPatchParams, LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE,
|
UnityFsPatchReport, UnityFsStringFieldPatchParams, UnityFsTextAssetPatchParams,
|
||||||
LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE,
|
LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_VERSIONS_DIR,
|
||||||
OFFICIAL_TEXTUNIT_INDEX_FILE, PRIVATE_FILE_MODE,
|
LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE,
|
||||||
|
PRIVATE_FILE_MODE,
|
||||||
};
|
};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -173,6 +174,7 @@ fn run() -> anyhow::Result<i32> {
|
|||||||
CliCommand::ParseStatus
|
CliCommand::ParseStatus
|
||||||
| CliCommand::ParseTextUnits
|
| CliCommand::ParseTextUnits
|
||||||
| CliCommand::ParseErrors
|
| CliCommand::ParseErrors
|
||||||
|
| CliCommand::TranslationTasks
|
||||||
| CliCommand::LocalizedStatus
|
| CliCommand::LocalizedStatus
|
||||||
| CliCommand::ResourceIndex => {
|
| CliCommand::ResourceIndex => {
|
||||||
run_readonly_query_command(&options)?;
|
run_readonly_query_command(&options)?;
|
||||||
@@ -231,6 +233,7 @@ struct CliOptions {
|
|||||||
tail_lines: usize,
|
tail_lines: usize,
|
||||||
query_offset: usize,
|
query_offset: usize,
|
||||||
query_limit: usize,
|
query_limit: usize,
|
||||||
|
query_task_id: Option<String>,
|
||||||
query_resource_type: Option<ResourceType>,
|
query_resource_type: Option<ResourceType>,
|
||||||
query_hash: Option<String>,
|
query_hash: Option<String>,
|
||||||
query_path_pattern: Option<String>,
|
query_path_pattern: Option<String>,
|
||||||
@@ -239,11 +242,13 @@ struct CliOptions {
|
|||||||
query_destination: Option<String>,
|
query_destination: Option<String>,
|
||||||
query_bundle_path: Option<String>,
|
query_bundle_path: Option<String>,
|
||||||
query_archive_entry: Option<String>,
|
query_archive_entry: Option<String>,
|
||||||
|
query_task_status: Option<String>,
|
||||||
query_parse_status: Option<String>,
|
query_parse_status: Option<String>,
|
||||||
query_path_id: Option<i64>,
|
query_path_id: Option<i64>,
|
||||||
query_class_id: Option<i32>,
|
query_class_id: Option<i32>,
|
||||||
query_field_path: Option<String>,
|
query_field_path: Option<String>,
|
||||||
query_format: Option<String>,
|
query_format: Option<String>,
|
||||||
|
query_has_reason: Option<bool>,
|
||||||
query_option_explicit: bool,
|
query_option_explicit: bool,
|
||||||
patch_kind: Option<PatchApplyKind>,
|
patch_kind: Option<PatchApplyKind>,
|
||||||
patch_source_path: Option<PathBuf>,
|
patch_source_path: Option<PathBuf>,
|
||||||
@@ -287,6 +292,7 @@ impl Default for CliOptions {
|
|||||||
tail_lines: 200,
|
tail_lines: 200,
|
||||||
query_offset: 0,
|
query_offset: 0,
|
||||||
query_limit: 100,
|
query_limit: 100,
|
||||||
|
query_task_id: None,
|
||||||
query_resource_type: None,
|
query_resource_type: None,
|
||||||
query_hash: None,
|
query_hash: None,
|
||||||
query_path_pattern: None,
|
query_path_pattern: None,
|
||||||
@@ -295,11 +301,13 @@ impl Default for CliOptions {
|
|||||||
query_destination: None,
|
query_destination: None,
|
||||||
query_bundle_path: None,
|
query_bundle_path: None,
|
||||||
query_archive_entry: None,
|
query_archive_entry: None,
|
||||||
|
query_task_status: None,
|
||||||
query_parse_status: None,
|
query_parse_status: None,
|
||||||
query_path_id: None,
|
query_path_id: None,
|
||||||
query_class_id: None,
|
query_class_id: None,
|
||||||
query_field_path: None,
|
query_field_path: None,
|
||||||
query_format: None,
|
query_format: None,
|
||||||
|
query_has_reason: None,
|
||||||
query_option_explicit: false,
|
query_option_explicit: false,
|
||||||
patch_kind: None,
|
patch_kind: None,
|
||||||
patch_source_path: None,
|
patch_source_path: None,
|
||||||
@@ -340,6 +348,7 @@ enum CliCommand {
|
|||||||
ParseStatus,
|
ParseStatus,
|
||||||
ParseTextUnits,
|
ParseTextUnits,
|
||||||
ParseErrors,
|
ParseErrors,
|
||||||
|
TranslationTasks,
|
||||||
LocalizedStatus,
|
LocalizedStatus,
|
||||||
ResourceIndex,
|
ResourceIndex,
|
||||||
PatchApply,
|
PatchApply,
|
||||||
@@ -790,6 +799,7 @@ const RPC_METHOD_RESOURCE_LIST: &str = "resource.list";
|
|||||||
const RPC_METHOD_PARSE_STATUS: &str = "parse.status";
|
const RPC_METHOD_PARSE_STATUS: &str = "parse.status";
|
||||||
const RPC_METHOD_PARSE_TEXT_UNITS: &str = "parse.text_units";
|
const RPC_METHOD_PARSE_TEXT_UNITS: &str = "parse.text_units";
|
||||||
const RPC_METHOD_PARSE_ERRORS: &str = "parse.errors";
|
const RPC_METHOD_PARSE_ERRORS: &str = "parse.errors";
|
||||||
|
const RPC_METHOD_TRANSLATION_TASKS: &str = "translation.tasks";
|
||||||
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
|
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
|
||||||
const RPC_METHOD_CATALOG_STATUS: &str = "catalog.status";
|
const RPC_METHOD_CATALOG_STATUS: &str = "catalog.status";
|
||||||
const RPC_METHOD_CATALOG_VERSIONS: &str = "catalog.versions";
|
const RPC_METHOD_CATALOG_VERSIONS: &str = "catalog.versions";
|
||||||
@@ -2247,6 +2257,27 @@ fn dispatch_rpc_method(
|
|||||||
build_parse_errors_report(state_dir, query, offset, limit),
|
build_parse_errors_report(state_dir, query, offset, limit),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
RPC_METHOD_TRANSLATION_TASKS => {
|
||||||
|
let (query, offset, limit) =
|
||||||
|
match rpc_translation_task_query_params(request.params.as_ref()) {
|
||||||
|
Ok(params) => params,
|
||||||
|
Err(error) => {
|
||||||
|
return rpc_envelope_error(
|
||||||
|
request_id,
|
||||||
|
ApiError::new(
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
|
"translation.tasks",
|
||||||
|
error.to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rpc_envelope_from_result(
|
||||||
|
request_id,
|
||||||
|
"translation.tasks",
|
||||||
|
build_translation_tasks_report(state_dir, query, offset, limit),
|
||||||
|
)
|
||||||
|
}
|
||||||
RPC_METHOD_LOCALIZED_STATUS => rpc_envelope_from_result(
|
RPC_METHOD_LOCALIZED_STATUS => rpc_envelope_from_result(
|
||||||
request_id,
|
request_id,
|
||||||
"localized.status",
|
"localized.status",
|
||||||
@@ -3041,6 +3072,55 @@ fn build_parse_errors_report(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `translation.tasks`:当前已发布版本的离线翻译任务状态查询。
|
||||||
|
fn build_translation_tasks_report(
|
||||||
|
state_dir: &Path,
|
||||||
|
query: OfficialTextUnitTaskQuery,
|
||||||
|
offset: usize,
|
||||||
|
limit: usize,
|
||||||
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
|
let (_, version_state) = read_daemon_resource_state(state_dir)?;
|
||||||
|
let current = version_state
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|state| state.current_completed_version.as_ref());
|
||||||
|
let Some(record) = current else {
|
||||||
|
return Ok(serde_json::json!({ "available": false }));
|
||||||
|
};
|
||||||
|
let task_queue_path = record
|
||||||
|
.resource_root
|
||||||
|
.join(bat_infrastructure::OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE);
|
||||||
|
let Some(queue) = bat_infrastructure::read_textunit_task_queue_at(&record.resource_root)
|
||||||
|
.map_err(anyhow::Error::msg)?
|
||||||
|
else {
|
||||||
|
return Ok(serde_json::json!({
|
||||||
|
"available": false,
|
||||||
|
"current_version_id": record.id,
|
||||||
|
"resource_root": record.resource_root,
|
||||||
|
"textunit_task_queue_path": task_queue_path,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let matches = bat_infrastructure::query_textunit_tasks(&queue, &query);
|
||||||
|
let total_entries = matches.len();
|
||||||
|
let entries = matches
|
||||||
|
.into_iter()
|
||||||
|
.skip(offset)
|
||||||
|
.take(limit)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"available": true,
|
||||||
|
"current_version_id": record.id,
|
||||||
|
"resource_root": record.resource_root,
|
||||||
|
"textunit_task_queue_path": task_queue_path,
|
||||||
|
"summary": queue.summary,
|
||||||
|
"total_entries": total_entries,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
|
"query": translation_task_query_json(&query),
|
||||||
|
"entries": entries,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn textunit_query_json(query: &OfficialTextUnitQuery) -> serde_json::Value {
|
fn textunit_query_json(query: &OfficialTextUnitQuery) -> serde_json::Value {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"destination": query.destination.clone(),
|
"destination": query.destination.clone(),
|
||||||
@@ -3053,6 +3133,20 @@ fn textunit_query_json(query: &OfficialTextUnitQuery) -> serde_json::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn translation_task_query_json(query: &OfficialTextUnitTaskQuery) -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"task_id": query.task_id.clone(),
|
||||||
|
"official_release_id": query.official_release_id.clone(),
|
||||||
|
"destination": query.destination.clone(),
|
||||||
|
"path_pattern": query.path_pattern.clone(),
|
||||||
|
"archive_entry": query.archive_entry.clone(),
|
||||||
|
"status": query.status.clone(),
|
||||||
|
"parse_status": query.parse_status.clone(),
|
||||||
|
"text_unit_format": query.text_unit_format.clone(),
|
||||||
|
"has_reason": query.has_reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// `localized.status`:当前官方版本对应的汉化 release 状态。
|
/// `localized.status`:当前官方版本对应的汉化 release 状态。
|
||||||
fn build_localized_status_report(
|
fn build_localized_status_report(
|
||||||
state_dir: &Path,
|
state_dir: &Path,
|
||||||
@@ -3289,6 +3383,30 @@ fn rpc_textunit_query_params(
|
|||||||
Ok((query, offset, limit))
|
Ok((query, offset, limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rpc_translation_task_query_params(
|
||||||
|
params: Option<&serde_json::Value>,
|
||||||
|
) -> anyhow::Result<(OfficialTextUnitTaskQuery, usize, usize)> {
|
||||||
|
let (offset, limit) = rpc_page_params(params)?;
|
||||||
|
let query = OfficialTextUnitTaskQuery {
|
||||||
|
task_id: rpc_string_param(params, "task_id").map(str::to_string),
|
||||||
|
official_release_id: rpc_string_param(params, "official_release_id")
|
||||||
|
.or_else(|| rpc_string_param(params, "release_id"))
|
||||||
|
.map(str::to_string),
|
||||||
|
destination: rpc_string_param(params, "destination").map(str::to_string),
|
||||||
|
path_pattern: rpc_string_param(params, "path_pattern").map(str::to_string),
|
||||||
|
archive_entry: rpc_string_param(params, "archive_entry").map(str::to_string),
|
||||||
|
status: rpc_string_param(params, "status")
|
||||||
|
.or_else(|| rpc_string_param(params, "task_status"))
|
||||||
|
.map(str::to_string),
|
||||||
|
parse_status: rpc_string_param(params, "parse_status").map(str::to_string),
|
||||||
|
text_unit_format: rpc_string_param(params, "text_unit_format")
|
||||||
|
.or_else(|| rpc_string_param(params, "format"))
|
||||||
|
.map(str::to_string),
|
||||||
|
has_reason: rpc_bool_param(params, "has_reason"),
|
||||||
|
};
|
||||||
|
Ok((query, offset, limit))
|
||||||
|
}
|
||||||
|
|
||||||
fn rpc_string_param<'a>(params: Option<&'a serde_json::Value>, key: &str) -> Option<&'a str> {
|
fn rpc_string_param<'a>(params: Option<&'a serde_json::Value>, key: &str) -> Option<&'a str> {
|
||||||
params
|
params
|
||||||
.and_then(|params| params.get(key))
|
.and_then(|params| params.get(key))
|
||||||
@@ -4238,6 +4356,7 @@ fn readonly_query_rpc_method(command: CliCommand) -> Option<&'static str> {
|
|||||||
CliCommand::ParseStatus => Some(RPC_METHOD_PARSE_STATUS),
|
CliCommand::ParseStatus => Some(RPC_METHOD_PARSE_STATUS),
|
||||||
CliCommand::ParseTextUnits => Some(RPC_METHOD_PARSE_TEXT_UNITS),
|
CliCommand::ParseTextUnits => Some(RPC_METHOD_PARSE_TEXT_UNITS),
|
||||||
CliCommand::ParseErrors => Some(RPC_METHOD_PARSE_ERRORS),
|
CliCommand::ParseErrors => Some(RPC_METHOD_PARSE_ERRORS),
|
||||||
|
CliCommand::TranslationTasks => Some(RPC_METHOD_TRANSLATION_TASKS),
|
||||||
CliCommand::LocalizedStatus => Some(RPC_METHOD_LOCALIZED_STATUS),
|
CliCommand::LocalizedStatus => Some(RPC_METHOD_LOCALIZED_STATUS),
|
||||||
CliCommand::ResourceIndex => Some(RPC_METHOD_RESOURCE_INDEX),
|
CliCommand::ResourceIndex => Some(RPC_METHOD_RESOURCE_INDEX),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -4247,7 +4366,10 @@ fn readonly_query_rpc_method(command: CliCommand) -> Option<&'static str> {
|
|||||||
fn readonly_query_rpc_params(options: &CliOptions) -> Option<serde_json::Value> {
|
fn readonly_query_rpc_params(options: &CliOptions) -> Option<serde_json::Value> {
|
||||||
let mut params = serde_json::Map::new();
|
let mut params = serde_json::Map::new();
|
||||||
match options.command {
|
match options.command {
|
||||||
CliCommand::ResourceIndex | CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
CliCommand::ResourceIndex
|
||||||
|
| CliCommand::ParseTextUnits
|
||||||
|
| CliCommand::ParseErrors
|
||||||
|
| CliCommand::TranslationTasks => {
|
||||||
params.insert(
|
params.insert(
|
||||||
"offset".to_string(),
|
"offset".to_string(),
|
||||||
serde_json::json!(options.query_offset),
|
serde_json::json!(options.query_offset),
|
||||||
@@ -4324,6 +4446,41 @@ fn readonly_query_rpc_params(options: &CliOptions) -> Option<serde_json::Value>
|
|||||||
params.insert("format".to_string(), serde_json::json!(format));
|
params.insert("format".to_string(), serde_json::json!(format));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CliCommand::TranslationTasks => {
|
||||||
|
if let Some(task_id) = options.query_task_id.as_ref() {
|
||||||
|
params.insert("task_id".to_string(), serde_json::json!(task_id));
|
||||||
|
}
|
||||||
|
if let Some(release_id) = options.query_official_release_id.as_ref() {
|
||||||
|
params.insert(
|
||||||
|
"official_release_id".to_string(),
|
||||||
|
serde_json::json!(release_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(destination) = options.query_destination.as_ref() {
|
||||||
|
params.insert("destination".to_string(), serde_json::json!(destination));
|
||||||
|
}
|
||||||
|
if let Some(path_pattern) = options.query_path_pattern.as_ref() {
|
||||||
|
params.insert("path_pattern".to_string(), serde_json::json!(path_pattern));
|
||||||
|
}
|
||||||
|
if let Some(archive_entry) = options.query_archive_entry.as_ref() {
|
||||||
|
params.insert(
|
||||||
|
"archive_entry".to_string(),
|
||||||
|
serde_json::json!(archive_entry),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(status) = options.query_task_status.as_ref() {
|
||||||
|
params.insert("status".to_string(), serde_json::json!(status));
|
||||||
|
}
|
||||||
|
if let Some(parse_status) = options.query_parse_status.as_ref() {
|
||||||
|
params.insert("parse_status".to_string(), serde_json::json!(parse_status));
|
||||||
|
}
|
||||||
|
if let Some(format) = options.query_format.as_ref() {
|
||||||
|
params.insert("text_unit_format".to_string(), serde_json::json!(format));
|
||||||
|
}
|
||||||
|
if let Some(has_reason) = options.query_has_reason {
|
||||||
|
params.insert("has_reason".to_string(), serde_json::json!(has_reason));
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Some(serde_json::Value::Object(params))
|
Some(serde_json::Value::Object(params))
|
||||||
@@ -4352,6 +4509,12 @@ fn build_readonly_query_report(
|
|||||||
options.query_offset,
|
options.query_offset,
|
||||||
options.query_limit,
|
options.query_limit,
|
||||||
),
|
),
|
||||||
|
RPC_METHOD_TRANSLATION_TASKS => build_translation_tasks_report(
|
||||||
|
&options.state_dir,
|
||||||
|
translation_task_query_from_options(options),
|
||||||
|
options.query_offset,
|
||||||
|
options.query_limit,
|
||||||
|
),
|
||||||
RPC_METHOD_LOCALIZED_STATUS => {
|
RPC_METHOD_LOCALIZED_STATUS => {
|
||||||
build_localized_status_report(&options.state_dir, &options.config)
|
build_localized_status_report(&options.state_dir, &options.config)
|
||||||
}
|
}
|
||||||
@@ -4367,34 +4530,46 @@ fn build_readonly_query_report(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_readonly_query_options(options: &CliOptions) -> anyhow::Result<()> {
|
fn validate_readonly_query_options(options: &CliOptions) -> anyhow::Result<()> {
|
||||||
let has_resource_index_filter = options.query_resource_type.is_some()
|
let has_resource_index_only_filter = options.query_resource_type.is_some()
|
||||||
|| options.query_hash.is_some()
|
|| options.query_hash.is_some()
|
||||||
|| options.query_official_release_id.is_some()
|
|
||||||
|| options.query_platform.is_some()
|
|| options.query_platform.is_some()
|
||||||
|| options.query_bundle_path.is_some()
|
|| options.query_bundle_path.is_some();
|
||||||
|| options.query_parse_status.is_some();
|
|
||||||
let has_parse_object_filter = options.query_path_id.is_some()
|
let has_parse_object_filter = options.query_path_id.is_some()
|
||||||
|| options.query_class_id.is_some()
|
|| options.query_class_id.is_some()
|
||||||
|| options.query_field_path.is_some();
|
|| options.query_field_path.is_some();
|
||||||
|
let has_translation_task_filter = options.query_task_id.is_some()
|
||||||
|
|| options.query_task_status.is_some()
|
||||||
|
|| options.query_has_reason.is_some();
|
||||||
|
|
||||||
match options.command {
|
match options.command {
|
||||||
CliCommand::ResourceIndex => {
|
CliCommand::ResourceIndex => {
|
||||||
if has_parse_object_filter {
|
if has_parse_object_filter || has_translation_task_filter {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"--path-id/--class-id/--field-path 只适用于 parse-text-units 或 parse-errors"
|
"--path-id/--class-id/--field-path 只适用于 parse-text-units 或 parse-errors;--task-id/--task-status/--has-reason 只适用于 translation-tasks"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
CliCommand::ParseTextUnits | CliCommand::ParseErrors => {
|
||||||
if has_resource_index_filter {
|
if has_resource_index_only_filter
|
||||||
|
|| has_translation_task_filter
|
||||||
|
|| options.query_official_release_id.is_some()
|
||||||
|
|| options.query_parse_status.is_some()
|
||||||
|
{
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"--resource-type/--hash/--release-id/--platform/--bundle-path/--parse-status 只适用于 resource-index"
|
"--resource-type/--hash/--release-id/--platform/--bundle-path/--parse-status 只适用于 resource-index 或 translation-tasks;--task-id/--task-status/--has-reason 只适用于 translation-tasks"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CliCommand::TranslationTasks => {
|
||||||
|
if has_resource_index_only_filter || has_parse_object_filter {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"--resource-type/--hash/--platform/--bundle-path 只适用于 resource-index;--path-id/--class-id/--field-path 只适用于 parse-text-units 或 parse-errors"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CliCommand::ParseStatus | CliCommand::LocalizedStatus if options.query_option_explicit => {
|
CliCommand::ParseStatus | CliCommand::LocalizedStatus if options.query_option_explicit => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"查询过滤参数只适用于 resource-index、parse-text-units 或 parse-errors"
|
"查询过滤参数只适用于 resource-index、parse-text-units、parse-errors 或 translation-tasks"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -4602,6 +4777,20 @@ fn textunit_query_from_options(options: &CliOptions) -> OfficialTextUnitQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn translation_task_query_from_options(options: &CliOptions) -> OfficialTextUnitTaskQuery {
|
||||||
|
OfficialTextUnitTaskQuery {
|
||||||
|
task_id: options.query_task_id.clone(),
|
||||||
|
official_release_id: options.query_official_release_id.clone(),
|
||||||
|
destination: options.query_destination.clone(),
|
||||||
|
path_pattern: options.query_path_pattern.clone(),
|
||||||
|
archive_entry: options.query_archive_entry.clone(),
|
||||||
|
status: options.query_task_status.clone(),
|
||||||
|
parse_status: options.query_parse_status.clone(),
|
||||||
|
text_unit_format: options.query_format.clone(),
|
||||||
|
has_reason: options.query_has_reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resource_type_rpc_label(resource_type: ResourceType) -> &'static str {
|
fn resource_type_rpc_label(resource_type: ResourceType) -> &'static str {
|
||||||
match resource_type {
|
match resource_type {
|
||||||
ResourceType::AssetBundle => "asset_bundle",
|
ResourceType::AssetBundle => "asset_bundle",
|
||||||
@@ -7052,6 +7241,10 @@ fn parse_args_with_env(
|
|||||||
ensure_command_not_set(options.command, "parse-errors")?;
|
ensure_command_not_set(options.command, "parse-errors")?;
|
||||||
options.command = CliCommand::ParseErrors;
|
options.command = CliCommand::ParseErrors;
|
||||||
}
|
}
|
||||||
|
"translation-tasks" => {
|
||||||
|
ensure_command_not_set(options.command, "translation-tasks")?;
|
||||||
|
options.command = CliCommand::TranslationTasks;
|
||||||
|
}
|
||||||
"localized-status" => {
|
"localized-status" => {
|
||||||
ensure_command_not_set(options.command, "localized-status")?;
|
ensure_command_not_set(options.command, "localized-status")?;
|
||||||
options.command = CliCommand::LocalizedStatus;
|
options.command = CliCommand::LocalizedStatus;
|
||||||
@@ -7308,6 +7501,10 @@ fn parse_args_with_env(
|
|||||||
}
|
}
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--task-id" => {
|
||||||
|
options.query_task_id = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
"--resource-type" => {
|
"--resource-type" => {
|
||||||
options.query_resource_type = Some(parse_resource_type_param(&next_option_value(
|
options.query_resource_type = Some(parse_resource_type_param(&next_option_value(
|
||||||
&mut args, &flag,
|
&mut args, &flag,
|
||||||
@@ -7342,6 +7539,10 @@ fn parse_args_with_env(
|
|||||||
options.query_archive_entry = Some(next_option_value(&mut args, &flag)?);
|
options.query_archive_entry = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--task-status" => {
|
||||||
|
options.query_task_status = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
"--parse-status" => {
|
"--parse-status" => {
|
||||||
options.query_parse_status = Some(next_option_value(&mut args, &flag)?);
|
options.query_parse_status = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
@@ -7378,6 +7579,14 @@ fn parse_args_with_env(
|
|||||||
options.query_format = Some(next_option_value(&mut args, &flag)?);
|
options.query_format = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.query_option_explicit = true;
|
options.query_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--has-reason" => {
|
||||||
|
options.query_has_reason = Some(true);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
|
"--no-reason" => {
|
||||||
|
options.query_has_reason = Some(false);
|
||||||
|
options.query_option_explicit = true;
|
||||||
|
}
|
||||||
"--patch-kind" => {
|
"--patch-kind" => {
|
||||||
options.patch_kind = Some(parse_patch_apply_kind(&next_option_value(
|
options.patch_kind = Some(parse_patch_apply_kind(&next_option_value(
|
||||||
&mut args, &flag,
|
&mut args, &flag,
|
||||||
@@ -7494,6 +7703,7 @@ fn parse_args_with_env(
|
|||||||
CliCommand::ParseStatus
|
CliCommand::ParseStatus
|
||||||
| CliCommand::ParseTextUnits
|
| CliCommand::ParseTextUnits
|
||||||
| CliCommand::ParseErrors
|
| CliCommand::ParseErrors
|
||||||
|
| CliCommand::TranslationTasks
|
||||||
| CliCommand::LocalizedStatus
|
| CliCommand::LocalizedStatus
|
||||||
| CliCommand::ResourceIndex => {
|
| CliCommand::ResourceIndex => {
|
||||||
validate_readonly_query_options(&options)?;
|
validate_readonly_query_options(&options)?;
|
||||||
@@ -7685,6 +7895,7 @@ fn print_usage(binary: &str) {
|
|||||||
eprintln!(" parse-status Show current official parse-cache status");
|
eprintln!(" parse-status Show current official parse-cache status");
|
||||||
eprintln!(" parse-text-units Query current official TextUnit detail index");
|
eprintln!(" parse-text-units Query current official TextUnit detail index");
|
||||||
eprintln!(" parse-errors Query current official parse/extraction diagnostics");
|
eprintln!(" parse-errors Query current official parse/extraction diagnostics");
|
||||||
|
eprintln!(" translation-tasks Query current offline TextUnit translation task status");
|
||||||
eprintln!(" localized-status Show localized release status for current official release");
|
eprintln!(" localized-status Show localized release status for current official release");
|
||||||
eprintln!(" resource-index Query CAS + ResourceRepository index");
|
eprintln!(" resource-index Query CAS + ResourceRepository index");
|
||||||
eprintln!(" patch-apply Apply a Binary/JSON/Text patch file");
|
eprintln!(" patch-apply Apply a Binary/JSON/Text patch file");
|
||||||
@@ -7745,23 +7956,28 @@ fn print_usage(binary: &str) {
|
|||||||
eprintln!(" --repair | --no-repair Enable/disable automatic repair");
|
eprintln!(" --repair | --no-repair Enable/disable automatic repair");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Read-only queries:");
|
eprintln!("Read-only queries:");
|
||||||
eprintln!(" --offset <N> Query offset for resource-index/parse-text-units/parse-errors");
|
eprintln!(" --offset <N> Query offset for resource-index/parse-text-units/parse-errors/translation-tasks");
|
||||||
eprintln!(" --limit <N> Query limit for resource-index/parse-text-units/parse-errors (1..=1000)");
|
eprintln!(" --limit <N> Query limit for resource-index/parse-text-units/parse-errors/translation-tasks (1..=1000)");
|
||||||
|
eprintln!(" --task-id <ID> Filter translation-tasks by stable task ID");
|
||||||
eprintln!(" --resource-type <TYPE> asset_bundle, manifest, table_bundle, text_asset, media, other");
|
eprintln!(" --resource-type <TYPE> asset_bundle, manifest, table_bundle, text_asset, media, other");
|
||||||
eprintln!(" --hash <HASH> Filter resource-index by full CAS hash");
|
eprintln!(" --hash <HASH> Filter resource-index by full CAS hash");
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" --path-pattern <GLOB> Filter resource-index or parse detail by path pattern"
|
" --path-pattern <GLOB> Filter resource-index or parse detail by path pattern"
|
||||||
);
|
);
|
||||||
eprintln!(" --release-id <ID> Filter resource-index by official release ID");
|
eprintln!(" --release-id <ID> Filter resource-index or translation-tasks by official release ID");
|
||||||
eprintln!(" --platform <NAME> Filter resource-index by metadata platform");
|
eprintln!(" --platform <NAME> Filter resource-index by metadata platform");
|
||||||
eprintln!(" --destination <PATH> Filter resource-index or parse detail by official destination");
|
eprintln!(" --destination <PATH> Filter resource-index, parse detail, or translation-tasks by official destination");
|
||||||
eprintln!(" --bundle-path <PATH> Filter resource-index by metadata bundle path");
|
eprintln!(" --bundle-path <PATH> Filter resource-index by metadata bundle path");
|
||||||
eprintln!(" --archive-entry <PATH> Filter resource-index or parse detail by ZIP/archive entry");
|
eprintln!(" --archive-entry <PATH> Filter resource-index, parse detail, or translation-tasks by ZIP/archive entry");
|
||||||
eprintln!(" --parse-status <STATUS> Filter resource-index by parse status");
|
eprintln!(" --task-status <STATUS> Filter translation-tasks by task status");
|
||||||
|
eprintln!(" --parse-status <STATUS> Filter resource-index or translation-tasks by parse status");
|
||||||
eprintln!(" --path-id <ID> Filter parse detail by Unity object path ID");
|
eprintln!(" --path-id <ID> Filter parse detail by Unity object path ID");
|
||||||
eprintln!(" --class-id <ID> Filter parse detail by Unity class ID");
|
eprintln!(" --class-id <ID> Filter parse detail by Unity class ID");
|
||||||
eprintln!(" --field-path <PATH> Filter parse detail, or TypeTree field path after UnityFS field patch commands");
|
eprintln!(" --field-path <PATH> Filter parse detail, or TypeTree field path after UnityFS field patch commands");
|
||||||
eprintln!(" --format <NAME> Filter resource-index or parse text units by payload format");
|
eprintln!(" --format <NAME> Filter resource-index, parse text units, or translation-tasks by payload format");
|
||||||
|
eprintln!(
|
||||||
|
" --has-reason | --no-reason Filter translation-tasks by diagnostic reason presence"
|
||||||
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!("Write patch:");
|
eprintln!("Write patch:");
|
||||||
eprintln!(" --patch-kind <binary|json|text> Patch type for patch-apply");
|
eprintln!(" --patch-kind <binary|json|text> Patch type for patch-apply");
|
||||||
@@ -8397,6 +8613,7 @@ mod tests {
|
|||||||
("parse-status", CliCommand::ParseStatus),
|
("parse-status", CliCommand::ParseStatus),
|
||||||
("parse-text-units", CliCommand::ParseTextUnits),
|
("parse-text-units", CliCommand::ParseTextUnits),
|
||||||
("parse-errors", CliCommand::ParseErrors),
|
("parse-errors", CliCommand::ParseErrors),
|
||||||
|
("translation-tasks", CliCommand::TranslationTasks),
|
||||||
("localized-status", CliCommand::LocalizedStatus),
|
("localized-status", CliCommand::LocalizedStatus),
|
||||||
("resource-index", CliCommand::ResourceIndex),
|
("resource-index", CliCommand::ResourceIndex),
|
||||||
] {
|
] {
|
||||||
@@ -8497,6 +8714,57 @@ mod tests {
|
|||||||
assert_eq!(text_units.query_offset, 2);
|
assert_eq!(text_units.query_offset, 2);
|
||||||
assert_eq!(text_units.query_limit, 10);
|
assert_eq!(text_units.query_limit, 10);
|
||||||
|
|
||||||
|
let tasks = parse(&[
|
||||||
|
"bat",
|
||||||
|
"translation-tasks",
|
||||||
|
"--task-id",
|
||||||
|
"textunit/v-current/TextAssets/Scenario.json",
|
||||||
|
"--release-id",
|
||||||
|
"v-current",
|
||||||
|
"--destination",
|
||||||
|
"TextAssets/Scenario.json",
|
||||||
|
"--archive-entry",
|
||||||
|
"story/Scenario.json",
|
||||||
|
"--task-status",
|
||||||
|
"skipped_parse_failed",
|
||||||
|
"--parse-status",
|
||||||
|
"failed",
|
||||||
|
"--format",
|
||||||
|
"json",
|
||||||
|
"--has-reason",
|
||||||
|
"--offset",
|
||||||
|
"1",
|
||||||
|
"--limit",
|
||||||
|
"20",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(tasks.command, CliCommand::TranslationTasks);
|
||||||
|
assert_eq!(
|
||||||
|
tasks.query_task_id.as_deref(),
|
||||||
|
Some("textunit/v-current/TextAssets/Scenario.json")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tasks.query_official_release_id.as_deref(),
|
||||||
|
Some("v-current")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tasks.query_destination.as_deref(),
|
||||||
|
Some("TextAssets/Scenario.json")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tasks.query_archive_entry.as_deref(),
|
||||||
|
Some("story/Scenario.json")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tasks.query_task_status.as_deref(),
|
||||||
|
Some("skipped_parse_failed")
|
||||||
|
);
|
||||||
|
assert_eq!(tasks.query_parse_status.as_deref(), Some("failed"));
|
||||||
|
assert_eq!(tasks.query_format.as_deref(), Some("json"));
|
||||||
|
assert_eq!(tasks.query_has_reason, Some(true));
|
||||||
|
assert_eq!(tasks.query_offset, 1);
|
||||||
|
assert_eq!(tasks.query_limit, 20);
|
||||||
|
|
||||||
let error = parse(&["bat", "parse-status", "--limit", "10"]).unwrap_err();
|
let error = parse(&["bat", "parse-status", "--limit", "10"]).unwrap_err();
|
||||||
assert!(error.to_string().contains("查询过滤参数只适用于"));
|
assert!(error.to_string().contains("查询过滤参数只适用于"));
|
||||||
|
|
||||||
@@ -8507,6 +8775,12 @@ mod tests {
|
|||||||
assert!(error
|
assert!(error
|
||||||
.to_string()
|
.to_string()
|
||||||
.contains("--path-id/--class-id/--field-path"));
|
.contains("--path-id/--class-id/--field-path"));
|
||||||
|
|
||||||
|
let error =
|
||||||
|
parse(&["bat", "translation-tasks", "--resource-type", "text_asset"]).unwrap_err();
|
||||||
|
assert!(error
|
||||||
|
.to_string()
|
||||||
|
.contains("--resource-type/--hash/--platform/--bundle-path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -10040,6 +10314,70 @@ mod tests {
|
|||||||
assert_eq!(params["limit"], 3);
|
assert_eq!(params["limit"], 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn readonly_translation_tasks_command_uses_rpc_with_filters() {
|
||||||
|
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",
|
||||||
|
"translation-tasks",
|
||||||
|
"--json",
|
||||||
|
"--state-dir",
|
||||||
|
state_dir.to_str().unwrap(),
|
||||||
|
"--task-id",
|
||||||
|
"textunit/v-current/Bundles/story.bundle",
|
||||||
|
"--release-id",
|
||||||
|
"v-current",
|
||||||
|
"--destination",
|
||||||
|
"Bundles/story.bundle",
|
||||||
|
"--archive-entry",
|
||||||
|
"story/TextAsset",
|
||||||
|
"--task-status",
|
||||||
|
"skipped_parse_failed",
|
||||||
|
"--parse-status",
|
||||||
|
"failed",
|
||||||
|
"--format",
|
||||||
|
"json",
|
||||||
|
"--has-reason",
|
||||||
|
"--offset",
|
||||||
|
"4",
|
||||||
|
"--limit",
|
||||||
|
"5",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
run_readonly_query_command_with_rpc(
|
||||||
|
&options,
|
||||||
|
|_| true,
|
||||||
|
move |_state_dir, method, params| {
|
||||||
|
seen_calls
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push((method.to_string(), params.clone()));
|
||||||
|
Ok(serde_json::json!({ "available": true, "entries": [] }))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let seen = seen.lock().unwrap();
|
||||||
|
assert_eq!(seen.len(), 1);
|
||||||
|
assert_eq!(seen[0].0, RPC_METHOD_TRANSLATION_TASKS);
|
||||||
|
let params = seen[0].1.as_ref().unwrap();
|
||||||
|
assert_eq!(params["task_id"], "textunit/v-current/Bundles/story.bundle");
|
||||||
|
assert_eq!(params["official_release_id"], "v-current");
|
||||||
|
assert_eq!(params["destination"], "Bundles/story.bundle");
|
||||||
|
assert_eq!(params["archive_entry"], "story/TextAsset");
|
||||||
|
assert_eq!(params["status"], "skipped_parse_failed");
|
||||||
|
assert_eq!(params["parse_status"], "failed");
|
||||||
|
assert_eq!(params["text_unit_format"], "json");
|
||||||
|
assert_eq!(params["has_reason"], true);
|
||||||
|
assert_eq!(params["offset"], 4);
|
||||||
|
assert_eq!(params["limit"], 5);
|
||||||
|
}
|
||||||
|
|
||||||
#[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(&[
|
||||||
@@ -10843,6 +11181,67 @@ mod tests {
|
|||||||
assert_eq!(value["data"]["textunit_index_summary"]["unit_count"], 2);
|
assert_eq!(value["data"]["textunit_index_summary"]["unit_count"], 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_textunit_task_queue_fixture(current_dir: &Path) {
|
||||||
|
let queue = bat_infrastructure::OfficialTextUnitTaskQueue {
|
||||||
|
queue_version: bat_infrastructure::OFFICIAL_TEXTUNIT_TASK_QUEUE_VERSION,
|
||||||
|
official_release_id: "v-current".to_string(),
|
||||||
|
previous_release_id: Some("v-previous".to_string()),
|
||||||
|
generated_unix_seconds: 43,
|
||||||
|
current_resource_root: current_dir.to_path_buf(),
|
||||||
|
summary: bat_infrastructure::OfficialTextUnitTaskSummary {
|
||||||
|
resource_candidate_count: 2,
|
||||||
|
parse_entry_count: 2,
|
||||||
|
queued_task_count: 1,
|
||||||
|
skipped_parse_failed_count: 1,
|
||||||
|
text_unit_count: 5,
|
||||||
|
..bat_infrastructure::OfficialTextUnitTaskSummary::default()
|
||||||
|
},
|
||||||
|
tasks: vec![
|
||||||
|
bat_infrastructure::OfficialTextUnitTask {
|
||||||
|
task_id: "textunit/v-current/Bundle/a.bundle".to_string(),
|
||||||
|
official_release_id: "v-current".to_string(),
|
||||||
|
destination: "Bundle/a.bundle".to_string(),
|
||||||
|
change_kind: bat_infrastructure::OfficialResourceChangeKind::Added,
|
||||||
|
url: "https://example.invalid/a.bundle".to_string(),
|
||||||
|
bytes: 10,
|
||||||
|
blake3: "hash-a".to_string(),
|
||||||
|
parse_entry_key: Some("direct:a".to_string()),
|
||||||
|
archive_entry: None,
|
||||||
|
source_kind: Some(bat_infrastructure::OfficialParseSourceKind::DirectBundle),
|
||||||
|
parse_status: Some(bat_infrastructure::OfficialParseStatus::Parsed),
|
||||||
|
text_asset_count: 1,
|
||||||
|
text_assets: vec!["Scenario".to_string()],
|
||||||
|
text_unit_count: 5,
|
||||||
|
text_unit_formats: vec!["plain".to_string()],
|
||||||
|
text_unit_error_count: 0,
|
||||||
|
status: bat_infrastructure::OfficialTextUnitTaskStatus::QueuedOffline,
|
||||||
|
reason: None,
|
||||||
|
},
|
||||||
|
bat_infrastructure::OfficialTextUnitTask {
|
||||||
|
task_id: "textunit/v-current/Bundle/b.bundle#assets/b.bundle".to_string(),
|
||||||
|
official_release_id: "v-current".to_string(),
|
||||||
|
destination: "Bundle/b.bundle".to_string(),
|
||||||
|
change_kind: bat_infrastructure::OfficialResourceChangeKind::Modified,
|
||||||
|
url: "https://example.invalid/b.bundle".to_string(),
|
||||||
|
bytes: 20,
|
||||||
|
blake3: "hash-b".to_string(),
|
||||||
|
parse_entry_key: Some("zip:b".to_string()),
|
||||||
|
archive_entry: Some("assets/b.bundle".to_string()),
|
||||||
|
source_kind: Some(bat_infrastructure::OfficialParseSourceKind::ZipEntry),
|
||||||
|
parse_status: Some(bat_infrastructure::OfficialParseStatus::Failed),
|
||||||
|
text_asset_count: 0,
|
||||||
|
text_assets: Vec::new(),
|
||||||
|
text_unit_count: 0,
|
||||||
|
text_unit_formats: Vec::new(),
|
||||||
|
text_unit_error_count: 0,
|
||||||
|
status: bat_infrastructure::OfficialTextUnitTaskStatus::SkippedParseFailed,
|
||||||
|
reason: Some("parser failed at serialized object table".to_string()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
bat_infrastructure::write_textunit_task_queue_at(current_dir, &queue).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn write_textunit_index_fixture(current_dir: &Path) {
|
fn write_textunit_index_fixture(current_dir: &Path) {
|
||||||
let index = bat_infrastructure::OfficialTextUnitIndex {
|
let index = bat_infrastructure::OfficialTextUnitIndex {
|
||||||
version: bat_infrastructure::OFFICIAL_TEXTUNIT_INDEX_VERSION,
|
version: bat_infrastructure::OFFICIAL_TEXTUNIT_INDEX_VERSION,
|
||||||
@@ -10920,6 +11319,81 @@ mod tests {
|
|||||||
bat_infrastructure::write_textunit_index_at(current_dir, &index).unwrap();
|
bat_infrastructure::write_textunit_index_at(current_dir, &index).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_translation_tasks_filters_current_queue() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let state_dir = temp.path().join("state");
|
||||||
|
let output_root = temp.path().join("output");
|
||||||
|
let current_dir = write_catalog_fixture(&state_dir, &output_root, "bundle-b2", None);
|
||||||
|
write_textunit_task_queue_fixture(¤t_dir);
|
||||||
|
|
||||||
|
let envelope = dispatch_rpc_method(
|
||||||
|
&rpc_request(
|
||||||
|
"translation.tasks",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"release_id": "v-current",
|
||||||
|
"path_pattern": "Bundle/*.bundle",
|
||||||
|
"archive_entry": "assets/b.bundle",
|
||||||
|
"status": "skipped_parse_failed",
|
||||||
|
"parse_status": "failed",
|
||||||
|
"has_reason": true,
|
||||||
|
"offset": 0,
|
||||||
|
"limit": 10
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
&state_dir,
|
||||||
|
&new_daemon_control(),
|
||||||
|
&test_task_context(),
|
||||||
|
"req-translation-1".to_string(),
|
||||||
|
);
|
||||||
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
|
assert_eq!(value["ok"], true);
|
||||||
|
assert_eq!(value["data"]["available"], true);
|
||||||
|
assert_eq!(value["data"]["current_version_id"], "v-current");
|
||||||
|
assert_eq!(value["data"]["summary"]["queued_task_count"], 1);
|
||||||
|
assert_eq!(value["data"]["summary"]["skipped_parse_failed_count"], 1);
|
||||||
|
assert_eq!(value["data"]["total_entries"], 1);
|
||||||
|
assert_eq!(value["data"]["query"]["official_release_id"], "v-current");
|
||||||
|
assert_eq!(value["data"]["query"]["status"], "skipped_parse_failed");
|
||||||
|
assert_eq!(value["data"]["query"]["parse_status"], "failed");
|
||||||
|
assert_eq!(value["data"]["query"]["has_reason"], true);
|
||||||
|
let entries = value["data"]["entries"].as_array().unwrap();
|
||||||
|
assert_eq!(entries.len(), 1);
|
||||||
|
assert_eq!(entries[0]["destination"], "Bundle/b.bundle");
|
||||||
|
assert_eq!(entries[0]["archive_entry"], "assets/b.bundle");
|
||||||
|
assert_eq!(entries[0]["status"], "skipped_parse_failed");
|
||||||
|
assert_eq!(
|
||||||
|
entries[0]["reason"],
|
||||||
|
"parser failed at serialized object table"
|
||||||
|
);
|
||||||
|
|
||||||
|
let envelope = dispatch_rpc_method(
|
||||||
|
&rpc_request(
|
||||||
|
"translation.tasks",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"task_id": "textunit/v-current/Bundle/a.bundle",
|
||||||
|
"status": "queued_offline",
|
||||||
|
"format": "plain",
|
||||||
|
"has_reason": false,
|
||||||
|
"offset": 0,
|
||||||
|
"limit": 10
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
&state_dir,
|
||||||
|
&new_daemon_control(),
|
||||||
|
&test_task_context(),
|
||||||
|
"req-translation-2".to_string(),
|
||||||
|
);
|
||||||
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
|
assert_eq!(value["ok"], true);
|
||||||
|
assert_eq!(value["data"]["total_entries"], 1);
|
||||||
|
assert_eq!(value["data"]["entries"][0]["status"], "queued_offline");
|
||||||
|
assert_eq!(
|
||||||
|
value["data"]["entries"][0]["reason"],
|
||||||
|
serde_json::Value::Null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dispatch_parse_text_units_filters_current_index() {
|
fn dispatch_parse_text_units_filters_current_index() {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ pub use official_sync::{
|
|||||||
default_official_platforms, OfficialSyncDecision, OfficialSyncPlan,
|
default_official_platforms, OfficialSyncDecision, OfficialSyncPlan,
|
||||||
};
|
};
|
||||||
pub use official_textunit_queue::{
|
pub use official_textunit_queue::{
|
||||||
read_textunit_task_queue_at, write_crowdin_textunit_queue_at, write_official_textunit_queues,
|
query_textunit_tasks, read_textunit_task_queue_at, write_crowdin_textunit_queue_at,
|
||||||
write_textunit_task_queue_at, CrowdinTextUnitQueue, CrowdinTextUnitQueueItem,
|
write_official_textunit_queues, write_textunit_task_queue_at, CrowdinTextUnitQueue,
|
||||||
OfficialTextUnitQueueReport, OfficialTextUnitTask, OfficialTextUnitTaskQueue,
|
CrowdinTextUnitQueueItem, OfficialTextUnitQueueReport, OfficialTextUnitTask,
|
||||||
OfficialTextUnitTaskStatus, OfficialTextUnitTaskSummary, CROWDIN_TEXTUNIT_QUEUE_FILE,
|
OfficialTextUnitTaskQuery, OfficialTextUnitTaskQueue, OfficialTextUnitTaskStatus,
|
||||||
CROWDIN_TEXTUNIT_QUEUE_VERSION, OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE,
|
OfficialTextUnitTaskSummary, CROWDIN_TEXTUNIT_QUEUE_FILE, CROWDIN_TEXTUNIT_QUEUE_VERSION,
|
||||||
OFFICIAL_TEXTUNIT_TASK_QUEUE_VERSION,
|
OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE, OFFICIAL_TEXTUNIT_TASK_QUEUE_VERSION,
|
||||||
};
|
};
|
||||||
pub use official_update::{
|
pub use official_update::{
|
||||||
cached_game_main_config_for_metadata, diff_extended_snapshot, gc_orphan_staging,
|
cached_game_main_config_for_metadata, diff_extended_snapshot, gc_orphan_staging,
|
||||||
|
|||||||
@@ -100,6 +100,29 @@ pub struct OfficialTextUnitTask {
|
|||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query filters for incremental TextUnit translation tasks.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct OfficialTextUnitTaskQuery {
|
||||||
|
/// Filter by stable task ID.
|
||||||
|
pub task_id: Option<String>,
|
||||||
|
/// Filter by official release ID.
|
||||||
|
pub official_release_id: Option<String>,
|
||||||
|
/// Filter by resource destination.
|
||||||
|
pub destination: Option<String>,
|
||||||
|
/// Filter by destination glob pattern.
|
||||||
|
pub path_pattern: Option<String>,
|
||||||
|
/// Filter by ZIP/archive entry.
|
||||||
|
pub archive_entry: Option<String>,
|
||||||
|
/// Filter by task status, for example `queued_offline` or `skipped_parse_failed`.
|
||||||
|
pub status: Option<String>,
|
||||||
|
/// Filter by parse status, for example `parsed`, `failed`, or `skipped_unsupported`.
|
||||||
|
pub parse_status: Option<String>,
|
||||||
|
/// Filter by TextUnit format.
|
||||||
|
pub text_unit_format: Option<String>,
|
||||||
|
/// Filter tasks by whether a diagnostic reason is present.
|
||||||
|
pub has_reason: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Aggregate counters for an incremental TextUnit task queue.
|
/// Aggregate counters for an incremental TextUnit task queue.
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct OfficialTextUnitTaskSummary {
|
pub struct OfficialTextUnitTaskSummary {
|
||||||
@@ -363,6 +386,18 @@ pub fn read_textunit_task_queue_at(
|
|||||||
Ok(Some(queue))
|
Ok(Some(queue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns TextUnit translation tasks matching a query.
|
||||||
|
pub fn query_textunit_tasks<'a>(
|
||||||
|
queue: &'a OfficialTextUnitTaskQueue,
|
||||||
|
query: &OfficialTextUnitTaskQuery,
|
||||||
|
) -> Vec<&'a OfficialTextUnitTask> {
|
||||||
|
queue
|
||||||
|
.tasks
|
||||||
|
.iter()
|
||||||
|
.filter(|task| textunit_task_matches(task, query))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns whether the persisted TextUnit queue still matches current inputs.
|
/// Returns whether the persisted TextUnit queue still matches current inputs.
|
||||||
pub fn is_textunit_task_queue_current(
|
pub fn is_textunit_task_queue_current(
|
||||||
resource_root: &Path,
|
resource_root: &Path,
|
||||||
@@ -457,6 +492,97 @@ fn parse_entries_by_destination(
|
|||||||
by_destination
|
by_destination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn textunit_task_matches(task: &OfficialTextUnitTask, query: &OfficialTextUnitTaskQuery) -> bool {
|
||||||
|
if query
|
||||||
|
.task_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|task_id| &task.task_id != task_id)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if query
|
||||||
|
.official_release_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|release_id| &task.official_release_id != release_id)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if query
|
||||||
|
.destination
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|destination| &task.destination != destination)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if query
|
||||||
|
.path_pattern
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|pattern| !glob_matches(pattern, &task.destination))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if query
|
||||||
|
.archive_entry
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|archive_entry| task.archive_entry.as_ref() != Some(archive_entry))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if query
|
||||||
|
.status
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|status| task.status.as_str() != status)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(status) = &query.parse_status {
|
||||||
|
if task.parse_status.map(parse_status_label) != Some(status.as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if query.text_unit_format.as_ref().is_some_and(|format| {
|
||||||
|
!task
|
||||||
|
.text_unit_formats
|
||||||
|
.iter()
|
||||||
|
.any(|task_format| task_format == format)
|
||||||
|
}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if query
|
||||||
|
.has_reason
|
||||||
|
.is_some_and(|has_reason| task.reason.is_some() != has_reason)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_status_label(status: OfficialParseStatus) -> &'static str {
|
||||||
|
match status {
|
||||||
|
OfficialParseStatus::Parsed => "parsed",
|
||||||
|
OfficialParseStatus::SkippedUnsupported => "skipped_unsupported",
|
||||||
|
OfficialParseStatus::Failed => "failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glob_matches(pattern: &str, value: &str) -> bool {
|
||||||
|
glob_matches_bytes(pattern.as_bytes(), value.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glob_matches_bytes(pattern: &[u8], value: &[u8]) -> bool {
|
||||||
|
match pattern.split_first() {
|
||||||
|
None => value.is_empty(),
|
||||||
|
Some((&b'*', rest)) => {
|
||||||
|
glob_matches_bytes(rest, value)
|
||||||
|
|| (!value.is_empty() && glob_matches_bytes(pattern, &value[1..]))
|
||||||
|
}
|
||||||
|
Some((&b'?', rest)) => !value.is_empty() && glob_matches_bytes(rest, &value[1..]),
|
||||||
|
Some((&literal, rest)) => value
|
||||||
|
.split_first()
|
||||||
|
.is_some_and(|(&head, tail)| head == literal && glob_matches_bytes(rest, tail)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn skipped_no_parse_entry_task(
|
fn skipped_no_parse_entry_task(
|
||||||
change_set: &OfficialResourceChangeSet,
|
change_set: &OfficialResourceChangeSet,
|
||||||
change: &OfficialResourceChange,
|
change: &OfficialResourceChange,
|
||||||
@@ -772,6 +898,61 @@ mod tests {
|
|||||||
assert_eq!(crowdin.items[0].destination, "Bundles/a.bundle");
|
assert_eq!(crowdin.items[0].destination, "Bundles/a.bundle");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_textunit_tasks_filters_status_reason_and_format() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let queue = OfficialTextUnitTaskQueue::from_change_set_and_parse_cache(
|
||||||
|
&change_set(temp.path()),
|
||||||
|
&parse_cache(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let queued = query_textunit_tasks(
|
||||||
|
&queue,
|
||||||
|
&OfficialTextUnitTaskQuery {
|
||||||
|
official_release_id: Some("release-new".to_string()),
|
||||||
|
path_pattern: Some("Bundles/*.bundle".to_string()),
|
||||||
|
status: Some("queued_offline".to_string()),
|
||||||
|
parse_status: Some("parsed".to_string()),
|
||||||
|
text_unit_format: Some("plain".to_string()),
|
||||||
|
has_reason: Some(false),
|
||||||
|
..OfficialTextUnitTaskQuery::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(queued.len(), 1);
|
||||||
|
assert_eq!(queued[0].destination, "Bundles/a.bundle");
|
||||||
|
assert_eq!(queued[0].reason, None);
|
||||||
|
|
||||||
|
let skipped_with_reason = query_textunit_tasks(
|
||||||
|
&queue,
|
||||||
|
&OfficialTextUnitTaskQuery {
|
||||||
|
status: Some("skipped_unsupported".to_string()),
|
||||||
|
has_reason: Some(true),
|
||||||
|
..OfficialTextUnitTaskQuery::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(skipped_with_reason.len(), 1);
|
||||||
|
assert_eq!(skipped_with_reason[0].destination, "Bundles/b.bundle");
|
||||||
|
assert_eq!(
|
||||||
|
skipped_with_reason[0].reason.as_deref(),
|
||||||
|
Some("unsupported")
|
||||||
|
);
|
||||||
|
|
||||||
|
let by_task_id = query_textunit_tasks(
|
||||||
|
&queue,
|
||||||
|
&OfficialTextUnitTaskQuery {
|
||||||
|
task_id: Some("textunit/release-new/Bundles/c.bundle".to_string()),
|
||||||
|
status: Some("skipped_no_parse_entry".to_string()),
|
||||||
|
has_reason: Some(true),
|
||||||
|
..OfficialTextUnitTaskQuery::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(by_task_id.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
by_task_id[0].reason.as_deref(),
|
||||||
|
Some("parse cache entry not found for changed resource")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_textunit_queues_persists_files() {
|
fn write_textunit_queues_persists_files() {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user