mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 05:34:55 +08:00
+4
-4
@@ -28,9 +28,9 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
||||
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 内联打印。
|
||||
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 明细与解析错误;`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`)。
|
||||
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 翻译任务状态与跳过/失败原因;`translation.task.update` 可回写 provider worker 状态,`translation.proofread` 可把汉化 workflow 标记为人工校对中;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`)。
|
||||
|
||||
15. issue 43 已补齐 Rust `bat` 的 `res` / `parse` / `i18n` 工作流入口:支持资源拉取、解析刷新、可再生解析缓存清理、离线翻译工作台、人工文本查看/修改/清空、工作台发布前校验、有限 TextAsset 汉化发布、既有 patch 能力的批量重打包、单次/限定次数/周期执行和版本化 schedule CRUD。schedule 查询现在按一级工作流过滤,删除/执行会校验作用域,单轮执行可限制计划数;schedule CRUD 和翻译任务状态回写已通过 `bat.sock` 的 RPC 以及 `bat-api` 的鉴权管理接口暴露,dashboard 不维护第二套状态。该例外只编排已有解析和 patch 能力,不扩大解析器覆盖;完整 AssetBundle 重打包、真实 provider worker 和 Web 前端仍是后续工作。G-008(产品级 Go 同步 CLI)已决策关闭。真实官方网络全量拉取 smoke 已固化(G-018 已关闭);真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
||||
15. issue 43 已补齐 Rust `bat` 的 `res` / `parse` / `i18n` 工作流入口:支持资源拉取、解析刷新、可再生解析缓存清理、离线翻译工作台、人工文本查看/修改/清空、工作台发布前校验、有限 TextAsset 汉化发布、人工校对状态标记、既有 patch 能力的批量重打包、单次/限定次数/周期执行和版本化 schedule CRUD。schedule 查询现在按一级工作流过滤,删除/执行会校验作用域,单轮执行可限制计划数;schedule CRUD、翻译任务状态回写和 `translation.proofread` 状态标记已通过 `bat.sock` 的 RPC 以及 `bat-api` 的鉴权管理接口暴露,dashboard 不维护第二套状态。该例外只编排已有解析和 patch 能力,不扩大解析器覆盖;完整 AssetBundle 重打包、真实 provider worker 和 Web 前端仍是后续工作。G-008(产品级 Go 同步 CLI)已决策关闭。真实官方网络全量拉取 smoke 已固化(G-018 已关闭);真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
||||
|
||||
当前翻译交接还包括 `translation-tasks.sqlite` 和版本化 `translation-handoff.json`;
|
||||
`translation.tasks` 查询单项 worker 状态,`translation.handoff` 查询完整
|
||||
@@ -291,9 +291,9 @@ GitHub issue 状态:#1 已关闭;#17 的历史决定不代表当前下载实
|
||||
|
||||
下一阶段必须优先完成:
|
||||
|
||||
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)、`schedule.*`(list/add/update/remove/run)、`parse.*`(status/text_units/errors)、`translation.*`(tasks/handoff/task.update)、`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)、`schedule.*`(list/add/update/remove/run)、`parse.*`(status/text_units/errors)、`translation.*`(tasks/handoff/task.update/proofread)、`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 基础章节;仓库内 Rust/Go snapshot contract fixture 已落地,剩余为远程服务器全量 release 联调、refresh mtime/size 增量缓存和可选持久化。
|
||||
3. 文本提取 / 翻译队列 / Patch 输入:`official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json` 已生成;TextUnit 明细、解析错误和离线翻译任务状态/失败原因已可查询,`translation.task.update` 已提供 Rust CLI、Go typed helper 和 bat-api dashboard 状态回写入口。剩余为真实 Crowdin/provider worker、翻译记忆和 Patch 构建。
|
||||
3. 文本提取 / 翻译队列 / Patch 输入:`official-textunit-index.json`、`official-textunit-tasks.json` 与 `crowdin-textunit-queue.json` 已生成;TextUnit 明细、解析错误和离线翻译任务状态/失败原因已可查询,`translation.task.update` 已提供 Rust CLI、Go typed helper 和 bat-api dashboard 状态回写入口,`translation.proofread` 已提供人工校对状态标记入口。剩余为真实 Crowdin/provider worker、翻译记忆和 Patch 构建。
|
||||
4. Issue #3(P1):AssetBundle UnityFS 基础解析校验已具备离线和隔离真实样本覆盖;对象级解析继续跟踪 G-005。
|
||||
5. Issue #2(P1):继续逆向 Addressables catalog,提取 bundle hash/size/CRC 等可校验字段。
|
||||
6. 通用 Binary/JSON/Text Patch 基础已落地;复杂 AssetBundle 重打包和真实翻译系统仍应后置,UnityFS TextAsset patch 发布前置已具备回归测试。
|
||||
|
||||
+3
-3
@@ -34,9 +34,9 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
||||
6. `bat-infrastructure` 已提供官方资源 pull/update 服务,正式入口是 Rust binary `bat`。
|
||||
7. `bat` 支持 `--auto-discover`、`--watch`、`--daemon`、默认 1 小时间隔、本地 manifest audit/repair、官方 seed `.hash` 校验、snapshot/cache,以及基于 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.*`);`daemon.restart` 通过 Rust lifecycle controller 复用 CLI restart 路径,`clean-stable` 仍由 CLI 侧按进程生命周期显式执行。
|
||||
8. `bat-ffi` 已提供 Manifest inspect 和官方 sync plan 的可选无状态粗粒度 JSON C ABI helper。
|
||||
9. 官方原版资源默认发布到 `./bat-resources`,汉化产物默认发布到独立的 `./bat-localized`;当前官方同步报告会标记 `localized_release_status=not_localized`,表示原版资源已发布、汉化资源未发布。
|
||||
9. 官方原版资源默认发布到 `./bat-resources`,汉化产物默认发布到独立的 `./bat-localized`;当前官方同步报告会标记 `localized_release_status=not_localized`,表示原版资源已发布、汉化资源未发布;`translation.proofread` 可把汉化 workflow 标记为人工校对中,但不会覆盖已发布的汉化 release。
|
||||
10. 官方同步校验完成并发布新 release 后会生成 `official-resource-changes.json`、`crowdin-translation-handoff.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json` 和 `crowdin-textunit-queue.json`,用 Added/Modified 资源驱动后续解析/翻译增量;up-to-date 轮询在已有有效缓存、TextUnit 明细索引和队列时只读取摘要,不重复解析。
|
||||
11. issue 43 已补齐 Rust `bat` 的 `res` / `parse` / `i18n` 工作流:单次/限定次数/周期执行、版本化 schedule CRUD 与作用域过滤、解析缓存清理、翻译工作台校验、离线翻译工作台、人工文本查看/修改/清空、翻译任务状态回写、既有 patch 能力的批量重打包和独立汉化 release 发布;schedule CRUD 与 `translation.task.update` 已经通过 `bat.sock` 和 `bat-api` 管理接口暴露,dashboard 不维护第二套状态;该能力不扩大冻结期解析器覆盖。
|
||||
11. issue 43 已补齐 Rust `bat` 的 `res` / `parse` / `i18n` 工作流:单次/限定次数/周期执行、版本化 schedule CRUD 与作用域过滤、解析缓存清理、翻译工作台校验、离线翻译工作台、人工文本查看/修改/清空、翻译任务状态回写、人工校对状态标记、既有 patch 能力的批量重打包和独立汉化 release 发布;schedule CRUD、`translation.task.update` 和 `translation.proofread` 已经通过 `bat.sock` 和 `bat-api` 管理接口暴露,dashboard 不维护第二套状态;该能力不扩大冻结期解析器覆盖。
|
||||
12. 文档已整理:根目录保留入口文档,历史报告进入 `docs/reports/historical/`,误嵌套的 `docs/docs` 已合并。
|
||||
|
||||
### 仍是骨架或占位
|
||||
@@ -45,7 +45,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 前置链路。
|
||||
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 兼容层。
|
||||
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` 查询,`translation.task.update` 已提供 worker 状态回写 contract,真实 provider worker 集成仍需继续推进。
|
||||
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` 查询,`translation.task.update` 已提供 worker 状态回写 contract,`translation.proofread` 已提供汉化 workflow 人工校对标记 contract,真实 provider worker 集成仍需继续推进。
|
||||
6. 汉化 Patch 发布前置已具备 UnityFS TextAsset manifest/apply/diff/rollback/完整性校验和 `localized.status` 严格校验;真实 Crowdin worker、翻译记忆到完整汉化文件集合的构建仍未完成。
|
||||
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook(G-018 已关闭);真实运行记录处于长期运行测试阶段,报告待后续提供。
|
||||
8. 完整 Web、数据库迁移、插件加载机制尚未实现;`bat-api` 资源 bootstrap/分发 OpenAPI 已通过 `/openapi.yaml` 提供,完整游戏业务 API 的 OpenAPI 仍未完成。
|
||||
|
||||
+13
-1
@@ -49,9 +49,15 @@ HTTPS_PROXY=http://user:pass@127.0.0.1:7890 bat --auto-discover --daemon
|
||||
| `parse run` | 执行当前官方 release 的解析和 TextUnit 队列刷新 |
|
||||
| `parse clear-cache` | 使用 `--force` 清理当前 release 的可再生解析缓存和翻译队列 |
|
||||
| `parse repack` | 根据 JSON spec 批量重打包 UnityFS bundle |
|
||||
| `parse schedule` | 管理解析计划;与 `res schedule` / `i18n schedule` 共用同一计划状态 |
|
||||
| `i18n run` / `i18n export` | 刷新离线翻译队列或导出可编辑翻译工作台 |
|
||||
| `i18n set` / `i18n get` / `i18n unset` | 手动查看、修改或清空一个翻译工作台条目;也可通过 `i18n workbench ...` 访问 |
|
||||
| `i18n set` / `i18n get` / `i18n unset` | 手动查看、修改或清空一个翻译工作台条目;也可通过 `i18n workbench ...` 或 `--workbench` 访问 |
|
||||
| `i18n validate` | 发布前校验工作台 release、source text 和 patch 目标 |
|
||||
| `i18n proofread` | 将当前汉化 workflow 标记为人工校对中 |
|
||||
| `i18n tasks` / `i18n task list` / `i18n task status` | 查询当前离线 TextUnit 翻译任务状态 |
|
||||
| `i18n handoff` | 查询当前翻译交接视图 |
|
||||
| `i18n status` | 显示当前汉化 release 状态 |
|
||||
| `i18n task update` | 回写 provider worker 任务状态 |
|
||||
| `i18n publish` | 校验工作台并发布独立汉化 release;`--force` 使用新的手动 release ID |
|
||||
| `i18n schedule` | 管理翻译和汉化发布计划 |
|
||||
| `refresh` | 执行一次更新检查;若有 live daemon,则通过 RPC 请求其刷新 |
|
||||
@@ -71,6 +77,11 @@ Rust `bat` 工作流的完整命令、工作台字段、重打包 spec、调度
|
||||
`bat-api` 调度接口见 [`docs/guides/bat-workflows.md`](docs/guides/bat-workflows.md)。
|
||||
一级命令推荐使用短名称 `res`、`parse`、`i18n`;`resource`、`resources`、
|
||||
`translation`、`translate` 仍是兼容别名。
|
||||
其中 `translation` / `translate` 也支持 `tasks`、`handoff`、`status` 和
|
||||
`task update`;`--translation-file` 也可写成 `--workbench`,`--schedule-id` /
|
||||
`--schedule-action` 也可简写为 `--id` / `--action`。
|
||||
`resource status`、`resource schedule`、`translation tasks`、`translation handoff`
|
||||
和 `translation status` 也都与对应短命令一致。
|
||||
|
||||
### bat-api 资源 bootstrap / 分发服务
|
||||
|
||||
@@ -409,6 +420,7 @@ curl -i -H 'Range: bytes=0-1023' \
|
||||
| `translation.tasks` | ✅ | 查询离线 TextUnit 翻译任务及 worker 状态 |
|
||||
| `translation.handoff` | ✅ | 查询完整 job/unit/provider run 交接视图 |
|
||||
| `translation.task.update` | ✅ | 回写当前 release 的 provider worker 状态 |
|
||||
| `translation.proofread` | ✅ | 将当前汉化 workflow 标记为人工校对中 |
|
||||
| `localized.status` | ✅ | 查询汉化 release 与当前官方 release 的匹配状态 |
|
||||
| `catalog.status` | ✅ | 当前已发布版本的 catalog 概览(app/bundle 版本、addressables 根、端点与 marker 计数、launcher 元数据) |
|
||||
| `catalog.versions` | ✅ | 版本历史:current / in_progress / previous / failed |
|
||||
|
||||
@@ -237,7 +237,7 @@ release 根目录写入 `translation-tasks.sqlite`,由版本化 `schema_migrat
|
||||
13. 发布完成后先对比上一完整 release 和当前 release 的 `official-download-manifest.json`,写出 `official-resource-changes.json` 和 `crowdin-translation-handoff.json`。同一 destination 只有 size 或 BLAKE3 变化才算 modified;新增+变更资源进入解析/翻译 handoff,删除资源只进入差异记录。当前只预留 Crowdin 本地 handoff,不发外部 API 请求。
|
||||
14. 随后刷新 active release 下的 `official-parse-cache.json` 和 `official-textunit-index.json`,并从 Added/Modified 资源、parse cache 与 TextUnit 明细索引派生 `official-textunit-tasks.json`、`crowdin-textunit-queue.json` 和版本化的 `translation-tasks.sqlite`;up-to-date 轮询在已有有效解析缓存、TextUnit 明细索引和 TextUnit 队列时只读取摘要,不重复解析,重新同步队列时保留已有 worker 状态。
|
||||
15. 若启用 `--import-repository`,已校验 release 会被导入 CAS + `ResourceRepository`,并可经 `resource.index` 查询。
|
||||
16. 官方同步报告默认给出 `localized_release_status=not_localized`,表示原版资源已发布、汉化资源未发布;UnityFS TextAsset patch 发布成功并通过 `localized-patch-manifest.json`、current symlink 和 release ID 校验后,`localized.status` 才返回 `localized`,表示原版和汉化两套资源都已发布。
|
||||
16. 官方同步报告默认给出 `localized_release_status=not_localized`,表示原版资源已发布、汉化资源未发布;UnityFS TextAsset patch 发布成功并通过 `localized-patch-manifest.json`、current symlink 和 release ID 校验后,`localized.status` 才返回 `localized`,表示原版和汉化两套资源都已发布。`translation.proofread` 只会把 workflow 标记成 `manual_proofreading` / `translation.manual_proofreading`,不会回退已发布汉化 release 的发布状态。
|
||||
|
||||
维护期特殊分支:如果官方 launcher/server-info 已经指向新资源根,但 client-patch seed marker 或必需 seed catalog 仍返回 403/404 等未开放状态,`bat` 返回 `waiting_for_official_resources`,保留现有 `current`,不创建失败 staging;若本轮启用 `--auto-discover`,会在 `<output>/official-launcher-bootstrap.pending.json` 写入待处理 launcher bootstrap 证据,供后续排障和自研客户端开发使用。
|
||||
|
||||
@@ -337,7 +337,7 @@ JSON-RPC 2.0 服务,是面向上层服务(Go 层)的**主要跨语言边
|
||||
- 方法命名空间与实现状态、请求/响应示例见
|
||||
`docs/reference/rpc-backend-api.md`:`daemon.status/logs/stop/restart/reload/refresh/doctor`、
|
||||
`resource.state/sync/verify/repair/manifest/list/index`、`parse.status/text_units/errors`、
|
||||
`translation.tasks/handoff/task.update`、`localized.status`、`catalog.*` 与
|
||||
`translation.tasks/handoff/task.update/proofread`、`localized.status`、`catalog.*` 与
|
||||
`task.status/list/cancel/logs` 已实现;文件级 `patch.apply` / `unityfs.patch_*`
|
||||
已实现,发布级 patch 与复杂 UnityFS 语义编辑待引擎;
|
||||
`task.create` 按设计暂不开放通用任务入口;
|
||||
|
||||
@@ -6,7 +6,7 @@ Rust `bat` 的工作流入口按三个一级命令组织:
|
||||
- `parse`:当前官方 release 的解析和 UnityFS 重打包。
|
||||
- `i18n`:离线翻译工作台、人工文本修改和汉化 release 发布。
|
||||
|
||||
`resource`、`resources`、`translation` 和 `translate` 仍作为长别名接受,但文档示例统一使用 `res` 和 `i18n`。
|
||||
`resource`、`resources`、`translation` 和 `translate` 仍作为长别名接受,但文档示例统一使用 `res` 和 `i18n`;例如 `resource status`、`resource schedule`、`translation tasks`、`translation handoff` 和 `translation status` 都会落到同一组已实现命令。
|
||||
|
||||
## 资源拉取
|
||||
|
||||
@@ -106,7 +106,8 @@ bat i18n unset \
|
||||
--translation-id <text-unit-id>
|
||||
```
|
||||
|
||||
这些工作台操作也可以写成 `bat i18n workbench set|get|unset|validate ...`。
|
||||
这些工作台操作也可以写成 `bat i18n workbench set|get|unset|validate ...`,
|
||||
`--translation-file` 也可简写为 `--workbench`。
|
||||
工作台会保存 source text、release ID、TextUnit 目标和人工译文;发布前会重新读取当前
|
||||
TextUnit 索引,拒绝过期 release、source text 或 patch 目标。
|
||||
|
||||
@@ -149,10 +150,27 @@ bat i18n task update \
|
||||
`--task-status` 支持 Rust contract 中的 `queued`、`running`、`failed`、
|
||||
`completed` 和 `skipped`;命令只更新当前 release 的
|
||||
`translation-tasks.sqlite`,不会创建任意翻译任务。
|
||||
任务查询和交接查询可以用 `bat i18n tasks` / `bat i18n handoff`;
|
||||
汉化发布状态可以用 `bat i18n status`。这些只读入口也可以写成
|
||||
`bat translation tasks|handoff|status`,其中 `translation` / `translate`
|
||||
是一级命令长别名。
|
||||
|
||||
需要把当前汉化 workflow 切到人工校对中时,可用:
|
||||
|
||||
```bash
|
||||
bat i18n proofread \
|
||||
--output /tmp/bat-resources \
|
||||
--localized-output /tmp/bat-localized
|
||||
```
|
||||
|
||||
该命令只改写 `localized-version-state.json` 中的工作流标记,不会改动已发布的
|
||||
汉化 release 指针;如果自动汉化已经发布,后续仍可继续正常发布汉化资源。
|
||||
|
||||
## 持久化调度
|
||||
|
||||
每个一级工作流都可以管理自己的 schedule。调度计划保存在 `--state-dir/bat-schedules.json`,计划记录包含动作、参数、下一次执行时间、周期、剩余次数、启用状态和最近错误。
|
||||
`parse schedule` 与 `res schedule` / `i18n schedule` 共用同一份计划库,
|
||||
`--id` / `--action` 分别是 `--schedule-id` / `--schedule-action` 的简写。
|
||||
|
||||
新增一个每天执行的资源拉取计划:
|
||||
|
||||
@@ -175,6 +193,7 @@ bat res schedule list --state-dir /tmp/bat-schedule
|
||||
bat res schedule update --state-dir /tmp/bat-schedule --schedule-id daily-pull --schedule-every 12h
|
||||
bat res schedule remove --state-dir /tmp/bat-schedule --schedule-id daily-pull
|
||||
bat res schedule run --state-dir /tmp/bat-schedule
|
||||
bat parse schedule list --state-dir /tmp/bat-schedule
|
||||
```
|
||||
|
||||
`parse schedule add` 默认动作是 `run`,`i18n schedule add` 默认动作也是 `run`;可以用 `--schedule-action repack` 或 `--schedule-action publish` 选择对应动作。`--schedule-count` 限定执行次数,省略表示周期无限执行;没有 `--schedule-every` 的计划执行一次后自动停用。
|
||||
@@ -210,6 +229,9 @@ contract:`id`、`group`、`action`、`args`、`next_run_unix_seconds`、
|
||||
`task_id`、`status`,以及可选的 `failure_reason`、`provider_run_id`;
|
||||
该接口只转发 `translation.task.update`。
|
||||
|
||||
`POST /admin/control/translation-proofread` 会把当前汉化 workflow 标记为人工校对中;
|
||||
该接口只转发 `translation.proofread`,不会改动已发布汉化 release 指针。
|
||||
|
||||
## 边界
|
||||
|
||||
解析器新增类型覆盖和新的解析格式仍受 `docs/reports/PARSER_FREEZE.md` 约束。本次 issue 43 的例外只开放已有解析输出的手动编排、缓存刷新、工作台编辑、既有 patch 实现的重打包和独立汉化发布,不扩展 UnityFS/AssetBundle/Addressables/TypeTree 的解析类型覆盖。
|
||||
|
||||
@@ -273,7 +273,10 @@ worker 状态、parse status、TextUnit format、队列 reason 和 provider fail
|
||||
`resource-index` 返回的资源 JSON 包含 release、平台、bundle path、TextAsset 和 TextUnit metadata,
|
||||
并可按 release、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 做资源级过滤;
|
||||
`localized-status` 只有在 `localized-version-state.json`、`current` symlink 和
|
||||
`localized-patch-manifest.json` 都匹配当前官方 release 时才返回 `localized`。
|
||||
`localized-patch-manifest.json` 都匹配当前官方 release 时才返回 `localized`;
|
||||
当 workflow 被 `translation.proofread` 标记为人工校对中时,会额外返回
|
||||
`translation_workflow_status=manual_proofreading` 与
|
||||
`translation_workflow_status_code=translation.manual_proofreading`,但不会遮蔽已发布的汉化 release。
|
||||
|
||||
文件级写入命令只处理显式输入/输出文件,不切换官方或汉化 release:
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@ SQLite `ResourceRepository`,索引不存在时返回 `ok=true` 且
|
||||
| `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 翻译任务状态分页。 |
|
||||
| `translation.handoff` | 已实现 | `null` | 当前官方 release 的 job、unit、provider run 交接视图;动态合并队列和 SQLite worker 状态。 |
|
||||
| `translation.task.update` | 已实现 | `{ "task_id": "...", "status": "failed", "failure_reason": "...", "provider_run_id": "..." }` | 写入当前 release 的 provider worker 状态,返回可回查任务记录。 |
|
||||
| `translation.proofread` | 已实现 | `null` | 将当前汉化 workflow 标记为人工校对中,返回工作流状态报告。 |
|
||||
|
||||
`parse.status` 是只读查询;没有当前 release 或没有解析缓存时返回
|
||||
`ok=true` 且 `data.available=false`。解析缓存来自官方原版资源目录,不读取
|
||||
@@ -270,7 +271,9 @@ bat-api 可通过 `translation.tasks` 查询单项任务,也可通过
|
||||
`status` / `status_code` 使用生命周期短状态和稳定状态码,例如
|
||||
`pending` / `localized.pending`、`stale` / `localized.stale`、`published` /
|
||||
`localized.published`;旧的 `localized` / `not_localized` 业务标签放在
|
||||
`localized_release_status`。返回 `localized_release_status=localized` 的条件是:
|
||||
`localized_release_status`。`translation_workflow_status` / `translation_workflow_status_code`
|
||||
用于表示汉化工作流的人工校对状态,例如 `manual_proofreading` /
|
||||
`translation.manual_proofreading`。返回 `localized_release_status=localized` 的条件是:
|
||||
`localized-version-state.json` 的官方 release ID 匹配当前官方 release,
|
||||
`current` symlink 指向汉化发布根下对应的 `versions/<id>`,并且该版本目录中的
|
||||
`localized-patch-manifest.json` 存在且 release ID 匹配。响应会返回
|
||||
@@ -391,10 +394,15 @@ CLI 对应关系:
|
||||
| `bat parse-errors` | `parse.errors` |
|
||||
| `bat translation-tasks` | `translation.tasks` |
|
||||
| `bat translation-handoff` | `translation.handoff` |
|
||||
| `bat i18n task list` / `bat i18n task status` | `translation.tasks` |
|
||||
| `bat i18n task update` | `translation.task.update` |
|
||||
| `bat i18n proofread` | `translation.proofread` |
|
||||
| `bat localized-status` | `localized.status` |
|
||||
| `bat resource-index` | `resource.index` |
|
||||
|
||||
`bat translation-tasks` / `bat i18n tasks`、`bat translation-handoff` / `bat i18n handoff`、
|
||||
`bat localized-status` / `bat i18n status` 都对应同一 RPC;这里列出的是推荐命令形态。
|
||||
|
||||
`bat resource-index` 支持 `--offset`、`--limit`、`--resource-type`、`--hash`、
|
||||
`--path-pattern`、`--release-id`、`--platform`、`--destination`、
|
||||
`--bundle-path`、`--archive-entry`、`--parse-status` 和 `--format`;
|
||||
@@ -415,7 +423,7 @@ CLI 对应关系:
|
||||
- typed helper 已覆盖 daemon 已实现方法(`status/logs/stop/restart/reload/refresh/doctor`)、
|
||||
`resource.state/sync/verify/repair/manifest/list`、`schedule.list/add/update/remove/run`、
|
||||
`catalog.*`、`parse.*`、
|
||||
`localized.status`、`translation.task.update`、`task.*` 和三个
|
||||
`localized.status`、`translation.task.update`、`translation.proofread`、`task.*` 和三个
|
||||
`unityfs.patch_*` 方法。
|
||||
- `resource.index`、`translation.tasks`、`translation.handoff` 和
|
||||
`patch.apply` 当前没有专用 typed helper;需要直接使用 `Call`,并仍须遵守
|
||||
@@ -428,7 +436,7 @@ CLI 对应关系:
|
||||
| `Backend` | `daemon.status`、`daemon.doctor`、`resource.state`、`catalog.status`、`resource.manifest` | 启动发现、周期刷新和资源分发 |
|
||||
| `ControlBackend` | `daemon.restart`、`daemon.reload`、`daemon.refresh`、`resource.sync`、`resource.verify`、`resource.repair`、`catalog.refresh` | 鉴权后的管理控制白名单 |
|
||||
| `ScheduleBackend` | `schedule.list`、`schedule.add`、`schedule.update`、`schedule.remove`、`schedule.run` | 鉴权后的 dashboard 调度计划控制 |
|
||||
| `TranslationBackend` | `translation.task.update` | 鉴权后的 dashboard 翻译任务状态回写 |
|
||||
| `TranslationBackend` | `translation.task.update`、`translation.proofread` | 鉴权后的 dashboard 翻译任务状态回写与人工校对标记 |
|
||||
|
||||
`daemon.stop`、`daemon.clean-stable` 和任意通用 RPC 不属于 bat-api 管理控制面。
|
||||
Rust dispatch、Go transport 和 bat-api 接口的权威实现位置分别是
|
||||
|
||||
@@ -293,8 +293,8 @@ issue 43 例外说明:新增的 `parse repack` 只编排已有 TextAsset、Typ
|
||||
- 官方 release 发布后还会写入版本化 `translation-handoff.json`;`translation.handoff` RPC/CLI 动态合并该快照与 SQLite worker 状态,暴露 job、unit、provider run、attempt 和 failure reason。
|
||||
- `translation-tasks.sqlite` 由 `schema_migrations` 管理 durable task state、attempt count、provider run ID 和 failure reason;重复同步会保留已有 worker 状态。
|
||||
- `translation.tasks` RPC/CLI 优先查询 `translation-tasks.sqlite`,旧 release 没有状态库时回退到 `official-textunit-tasks.json`;返回队列 `status`、worker `task_status`、failure reason 和时间/尝试次数。
|
||||
- `translation.task.update` 已提供 queued/running/failed/completed/skipped 状态回写契约,并已通过 `bat i18n task update`、Go typed helper 和 bat-api 鉴权控制面暴露;provider worker 可在消费 handoff 后按 task_id 更新并由同一查询接口反查。
|
||||
- `i18n get/set/unset` 可查看、修改或清空单条工作台译文,`i18n validate` 可在发布前校验工作台 release、source text、重复 patch 目标,并区分可直接发布的 TextAsset 与必须进入 `parse repack` 的条目;`parse clear-cache` 只删除可再生解析/队列 JSON,保留 `translation-tasks.sqlite` 的 worker 状态。
|
||||
- `translation.task.update` 已提供 queued/running/failed/completed/skipped 状态回写契约,并已通过 `bat i18n task update`、Go typed helper 和 bat-api 鉴权控制面暴露;`translation.proofread` 已提供人工校对状态标记契约,provider worker 可在消费 handoff 后按 task_id 更新并由同一查询接口反查。
|
||||
- `i18n get/set/unset` 可查看、修改或清空单条工作台译文,`i18n validate` 可在发布前校验工作台 release、source text、重复 patch 目标,并区分可直接发布的 TextAsset 与必须进入 `parse repack` 的条目;`i18n proofread` 可把当前汉化 workflow 标记为人工校对中,且不会遮蔽已发布汉化 release;`parse clear-cache` 只删除可再生解析/队列 JSON,保留 `translation-tasks.sqlite` 的 worker 状态。
|
||||
- 真实 Crowdin 网络 worker、翻译记忆和完整 localized repack 仍属于后续翻译系统工作,不在当前 Rust 离线状态仓储范围内。
|
||||
|
||||
验收:
|
||||
@@ -365,10 +365,10 @@ issue 43 例外说明:新增的 `parse repack` 只编排已有 TextAsset、Typ
|
||||
|
||||
- 官方原版资源发布根为 `./bat-resources`,汉化产物发布根为 `./bat-localized`。
|
||||
- CLI 支持 `--localized-output` / `BAT_LOCALIZED_OUTPUT`,并拒绝官方目录和汉化目录相同或互相嵌套。
|
||||
- 官方同步报告新增 `localized_release_status=not_localized`,明确表示原版资源已发布、汉化资源未发布。
|
||||
- 官方同步报告新增 `localized_release_status=not_localized`,明确表示原版资源已发布、汉化资源未发布;`translation.proofread` 只写 workflow 标记,不会把 `localized_release_status` 从 `localized` 回退成 `not_localized`。
|
||||
- `LocalizedPatchService` 已具备将给定汉化文件按官方相对路径发布到 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 指定目录下的 `.staging/<id>`、校验后移动到 `versions/<id>`、原子切换 `current` 并写入 `localized-version-state.json` 的基础能力。
|
||||
- `LocalizedPatchService` 已写入结构化 `localized-patch-manifest.json`,记录 TextAsset 操作、原始/汉化 hash、size、byte delta 和 rollback 信息;发布前后会校验 manifest hash/size 与 current symlink,失败时清理 staging / 未完成 version。
|
||||
- `localized.status` RPC 会读取 `.env` / daemon 配置中的汉化输出目录,校验汉化状态是否匹配当前官方 release,且要求 patch manifest 存在并匹配 release,避免写死 `./bat-localized` 或误报手工状态;其中 `status` / `status_code` 返回生命周期状态,`localized_release_status` 保留 `localized` / `not_localized` 发布标签。
|
||||
- `localized.status` RPC 会读取 `.env` / daemon 配置中的汉化输出目录,校验汉化状态是否匹配当前官方 release,且要求 patch manifest 存在并匹配 release,避免写死 `./bat-localized` 或误报手工状态;其中 `status` / `status_code` 返回生命周期状态,`localized_release_status` 保留 `localized` / `not_localized` 发布标签,`translation_workflow_status=manual_proofreading` 仅描述人工校对流程,不影响已发布汉化资源继续正常发布。
|
||||
|
||||
仍未完成:
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
| 组件 | 路径 | 状态 | 说明 |
|
||||
|---|---|---|---|
|
||||
| Module | `go.mod` → `bat-api` | 已用 | 服务层模块名 |
|
||||
| RPC client | `internal/backendrpc` | **完成** | Unix socket JSON-RPC transport + typed helper;typed helper 覆盖 daemon 已实现控制/查询、`resource.state/sync/verify/repair/manifest/list`、`catalog.*`、`parse.*`、`localized.status`、`task.*` 和文件级 UnityFS patch 调用;`resource.index`、`translation.*`、`patch.apply` 仍通过通用 `Call` 走同一 contract;fake transport 单测,配合 `internal/api/testdata/contract/` 固化 Rust 输出 mirror |
|
||||
| RPC client | `internal/backendrpc` | **完成** | Unix socket JSON-RPC transport + typed helper;typed helper 覆盖 daemon 已实现控制/查询、`resource.state/sync/verify/repair/manifest/list`、`catalog.*`、`parse.*`、`localized.status`、`task.*`、`translation.task.update`、`translation.proofread` 和文件级 UnityFS patch 调用;`resource.index`、其余 `translation.*`、`patch.apply` 仍通过通用 `Call` 走同一 contract;fake transport 单测,配合 `internal/api/testdata/contract/` 固化 Rust 输出 mirror |
|
||||
| 资源 bootstrap/分发 | `cmd/bat-api` + `internal/api` | **MVP+生产控制面** | RPC 发现 + 周期刷新/诊断 + `/v1/bootstrap` + `/v1/launcher/bootstrap` + launcher 资源 metadata 兼容 + `/readyz` + CDN Range/缓存头 + 鉴权/限流/访问日志/反代适配 + OpenAPI + 管理控制白名单 + `.env` |
|
||||
| 试验 CLI | `cmd/bat` | **试验** | doctor 固定 ok;manifest/sync 走 FFI |
|
||||
| FFI | `internal/ffi` | **可选** | 需 `build-ffi` |
|
||||
@@ -134,7 +134,7 @@ make build-go-cli # 产出 bin/bat-go
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| G-008 Go 同步 CLI | **已决策关闭**(正式同步 CLI = Rust `bat`) |
|
||||
| G-009 bat-api 资源 bootstrap/分发 | **部分完成**(MVP+生产控制面);已含资源 bootstrap 关系面、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、RPC 周期刷新/诊断、readiness、OpenAPI、管理控制白名单、Rust-owned `schedule.*` dashboard 代理、`translation.task.update` 状态回写代理和部署模板,后续远程服务器联调/可选持久化 |
|
||||
| G-009 bat-api 资源 bootstrap/分发 | **部分完成**(MVP+生产控制面);已含资源 bootstrap 关系面、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、RPC 周期刷新/诊断、readiness、OpenAPI、管理控制白名单、Rust-owned `schedule.*` dashboard 代理、`translation.task.update` 状态回写代理、`translation.proofread` 状态标记代理和部署模板,后续远程服务器联调/可选持久化 |
|
||||
| issue #19 | 资源面 MVP 与 USERGUIDE 基础章节已编码;真机联调后继续补充实战样例;**未自动关 issue** |
|
||||
| G-010 Web | 未开始 |
|
||||
|
||||
|
||||
@@ -26,9 +26,11 @@ use bat_infrastructure::{
|
||||
SqliteResourceRepository, SqliteTranslationTaskRepository, TranslationTaskStatus,
|
||||
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
|
||||
UnityFsTextAssetPatchParams, CROWDIN_TEXTUNIT_QUEUE_FILE, LOCALIZED_CURRENT_LINK,
|
||||
LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE,
|
||||
MAX_DOWNLOAD_CONCURRENCY, MIN_DOWNLOAD_CONCURRENCY, OFFICIAL_PARSE_CACHE_FILE,
|
||||
OFFICIAL_TEXTUNIT_INDEX_FILE, OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE, PRIVATE_FILE_MODE,
|
||||
LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING,
|
||||
LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL, LOCALIZED_VERSIONS_DIR,
|
||||
LOCALIZED_VERSION_STATE_FILE, MAX_DOWNLOAD_CONCURRENCY, MIN_DOWNLOAD_CONCURRENCY,
|
||||
OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE, OFFICIAL_TEXTUNIT_TASK_QUEUE_FILE,
|
||||
PRIVATE_FILE_MODE,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -81,8 +83,8 @@ use translation_query::{
|
||||
};
|
||||
use workflow_commands::{
|
||||
run_parse_clear_cache, run_parse_once, run_publish_localized, run_repack, run_translate_once,
|
||||
run_translation_get, run_translation_set, run_translation_task_update, run_translation_unset,
|
||||
run_translation_validate,
|
||||
run_translation_get, run_translation_proofread, run_translation_set,
|
||||
run_translation_task_update, run_translation_unset, run_translation_validate,
|
||||
};
|
||||
|
||||
const EXIT_ERROR: i32 = 1;
|
||||
@@ -223,6 +225,10 @@ fn run() -> anyhow::Result<i32> {
|
||||
run_translation_task_update(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::TranslationProofread => {
|
||||
run_translation_proofread(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
CliCommand::Repack => {
|
||||
run_repack(&options)?;
|
||||
Ok(0)
|
||||
@@ -540,6 +546,7 @@ enum CliCommand {
|
||||
TranslationGet,
|
||||
TranslationUnset,
|
||||
TranslationTaskUpdate,
|
||||
TranslationProofread,
|
||||
Repack,
|
||||
PublishLocalized,
|
||||
ScheduleList,
|
||||
@@ -1017,6 +1024,7 @@ const RPC_METHOD_PARSE_ERRORS: &str = "parse.errors";
|
||||
const RPC_METHOD_TRANSLATION_TASKS: &str = "translation.tasks";
|
||||
const RPC_METHOD_TRANSLATION_HANDOFF: &str = "translation.handoff";
|
||||
const RPC_METHOD_TRANSLATION_TASK_UPDATE: &str = "translation.task.update";
|
||||
const RPC_METHOD_TRANSLATION_PROOFREAD: &str = "translation.proofread";
|
||||
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
|
||||
const RPC_METHOD_CATALOG_STATUS: &str = "catalog.status";
|
||||
const RPC_METHOD_CATALOG_VERSIONS: &str = "catalog.versions";
|
||||
@@ -2013,6 +2021,12 @@ fn dispatch_rpc_method(
|
||||
"translation.task.update",
|
||||
update_translation_task_status_report(state_dir, request.params.as_ref()),
|
||||
),
|
||||
RPC_METHOD_TRANSLATION_PROOFREAD => rpc_envelope_from_result(
|
||||
request_id,
|
||||
"translation.proofread",
|
||||
mark_localized_manual_proofreading_report(state_dir, &tasks.base_config)
|
||||
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from)),
|
||||
),
|
||||
RPC_METHOD_LOCALIZED_STATUS => rpc_envelope_from_result(
|
||||
request_id,
|
||||
"localized.status",
|
||||
@@ -2836,6 +2850,22 @@ fn build_parse_errors_report(
|
||||
}))
|
||||
}
|
||||
|
||||
fn mark_localized_manual_proofreading_report(
|
||||
state_dir: &Path,
|
||||
base_config: &OfficialUpdateConfig,
|
||||
) -> anyhow::Result<bat_infrastructure::LocalizedTranslationWorkflowReport> {
|
||||
let (_, version_state) = read_daemon_resource_state(state_dir)?;
|
||||
let official_release_id = version_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.current_completed_version.as_ref())
|
||||
.map(|record| record.id.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("没有可标记人工校对状态的当前官方 release"))?;
|
||||
bat_infrastructure::mark_localized_manual_proofreading(
|
||||
&base_config.localized_output_root,
|
||||
official_release_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// `localized.status`:当前官方版本对应的汉化 release 状态。
|
||||
fn build_localized_status_report(
|
||||
state_dir: &Path,
|
||||
@@ -2854,6 +2884,12 @@ fn build_localized_status_report(
|
||||
let current_path = localized_root.join(LOCALIZED_CURRENT_LINK);
|
||||
let state = read_localized_version_state(&localized_root)?;
|
||||
let mut localized_release_status = "not_localized";
|
||||
let mut translation_workflow_status = None;
|
||||
let mut translation_workflow_status_code = None;
|
||||
let mut translation_workflow_status_phase = None;
|
||||
let mut translation_workflow_status_terminal = None;
|
||||
let mut translation_workflow_status_retryable = None;
|
||||
let mut translation_workflow_label = None;
|
||||
let mut published_version_path = None;
|
||||
let mut matches_current_official_release = false;
|
||||
let mut current_points_to_published_version = false;
|
||||
@@ -2873,6 +2909,23 @@ fn build_localized_status_report(
|
||||
matches_current_official_release = official_version_id
|
||||
.as_deref()
|
||||
.is_some_and(|id| localized_state.official_release_id == id);
|
||||
if matches_current_official_release
|
||||
&& localized_state.translation_workflow_status()
|
||||
== Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING)
|
||||
{
|
||||
let (status, code, phase, terminal, retryable) =
|
||||
flow_status_fields(ReleaseFlowStatusCode::TranslationManualProofreading);
|
||||
translation_workflow_status = Some(status);
|
||||
translation_workflow_status_code = Some(code);
|
||||
translation_workflow_status_phase = Some(phase);
|
||||
translation_workflow_status_terminal = Some(terminal);
|
||||
translation_workflow_status_retryable = Some(retryable);
|
||||
translation_workflow_label = Some(
|
||||
localized_state
|
||||
.translation_workflow_label()
|
||||
.unwrap_or(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL),
|
||||
);
|
||||
}
|
||||
if official_version_id.is_some() && !matches_current_official_release {
|
||||
flow_status_code = ReleaseFlowStatusCode::LocalizedStale;
|
||||
}
|
||||
@@ -2915,6 +2968,12 @@ fn build_localized_status_report(
|
||||
"status_terminal": status_terminal,
|
||||
"status_retryable": status_retryable,
|
||||
"localized_release_status": localized_release_status,
|
||||
"translation_workflow_status": translation_workflow_status,
|
||||
"translation_workflow_status_code": translation_workflow_status_code,
|
||||
"translation_workflow_status_phase": translation_workflow_status_phase,
|
||||
"translation_workflow_status_terminal": translation_workflow_status_terminal,
|
||||
"translation_workflow_status_retryable": translation_workflow_status_retryable,
|
||||
"translation_workflow_label": translation_workflow_label,
|
||||
"official_current_version_id": official_version_id,
|
||||
"localized_output_root": localized_root,
|
||||
"state_path": state_path,
|
||||
@@ -4144,6 +4203,24 @@ impl HumanReport for LocalizedPatchReport {
|
||||
}
|
||||
}
|
||||
|
||||
impl HumanReport for bat_infrastructure::LocalizedTranslationWorkflowReport {
|
||||
fn print_human(&self) -> anyhow::Result<()> {
|
||||
print_title("汉化工作流状态");
|
||||
print_field("命令", self.command);
|
||||
print_field("状态", self.status);
|
||||
print_field("官方 release", &self.official_release_id);
|
||||
print_optional_field("汉化 release", self.current_release_id.as_deref());
|
||||
print_field("汉化发布状态", &self.localized_release_status);
|
||||
print_field("工作流状态", &self.translation_workflow_status);
|
||||
print_field("工作流状态码", self.translation_workflow_status_code);
|
||||
print_field("工作流标签", self.translation_workflow_label);
|
||||
print_field("允许发布", format_bool(self.publish_allowed));
|
||||
print_path_field("汉化输出目录", &self.localized_output_root);
|
||||
print_path_field("状态文件", &self.state_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> {
|
||||
if value.get("running").is_some() && value.get("state_dir").is_some() {
|
||||
print_title("后台状态");
|
||||
@@ -7329,6 +7406,39 @@ fn parse_args_with_env(
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::TranslationProofread => {
|
||||
if options.watch || options.daemon || options.daemon_child {
|
||||
return Err(anyhow::anyhow!("i18n proofread 只支持单次执行或 RPC 调用"));
|
||||
}
|
||||
if options.config.force
|
||||
|| options.config.dry_run
|
||||
|| options.run_count.is_some()
|
||||
|| options.sync_option_explicit
|
||||
|| options.proxy_option_explicit
|
||||
|| tools_are_non_default(&options.config, &options.env_baseline_config)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n proofread 只接受 --output、--localized-output、--resource-root、--state-dir 和 --json/--human"
|
||||
));
|
||||
}
|
||||
if options.translation_file.is_some()
|
||||
|| options.translation_id.is_some()
|
||||
|| options.translation_text.is_some()
|
||||
|| options.translation_text_file.is_some()
|
||||
|| options.translation_failure_reason.is_some()
|
||||
|| options.translation_provider_run_id.is_some()
|
||||
|| options.localized_release_id.is_some()
|
||||
|| options.repack_spec.is_some()
|
||||
|| options.query_option_explicit
|
||||
|| options.schedule_option_explicit
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"i18n proofread 不接受工作台、任务、查询、调度或发布 release 参数"
|
||||
));
|
||||
}
|
||||
options.progress = false;
|
||||
options.banner = false;
|
||||
}
|
||||
CliCommand::ScheduleList
|
||||
| CliCommand::ScheduleAdd
|
||||
| CliCommand::ScheduleUpdate
|
||||
@@ -7530,6 +7640,7 @@ fn parse_translation_command(
|
||||
"unset" | "clear" => CliCommand::TranslationUnset,
|
||||
"validate" => CliCommand::TranslationValidate,
|
||||
"publish" => CliCommand::PublishLocalized,
|
||||
"proofread" => CliCommand::TranslationProofread,
|
||||
"tasks" => CliCommand::TranslationTasks,
|
||||
"handoff" => CliCommand::TranslationHandoff,
|
||||
"status" => CliCommand::LocalizedStatus,
|
||||
@@ -7688,12 +7799,19 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" parse run Parse current official release");
|
||||
eprintln!(" parse clear-cache Clear regenerable parse and translation queue files");
|
||||
eprintln!(" parse repack Repack a UnityFS bundle from a JSON spec");
|
||||
eprintln!(" parse schedule Manage parse schedules");
|
||||
eprintln!(" i18n run Refresh offline translation work");
|
||||
eprintln!(" i18n export Export an editable translation workbench");
|
||||
eprintln!(" i18n set Update one translation workbench entry");
|
||||
eprintln!(" i18n get Show one translation workbench entry");
|
||||
eprintln!(" i18n unset Clear one translated workbench entry");
|
||||
eprintln!(" i18n validate Validate workbench against the current official release");
|
||||
eprintln!(" i18n proofread Mark localized workflow as manual proofreading");
|
||||
eprintln!(
|
||||
" i18n tasks / i18n task list / i18n task status Query current offline TextUnit translation task status"
|
||||
);
|
||||
eprintln!(" i18n handoff Query current translation handoff");
|
||||
eprintln!(" i18n status Show localized release status for current official release");
|
||||
eprintln!(" i18n task update Update one provider worker task status");
|
||||
eprintln!(" i18n publish Publish a localized release");
|
||||
eprintln!(" i18n schedule Manage translation schedules");
|
||||
@@ -7727,6 +7845,7 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" {binary} --auto-discover --daemon");
|
||||
eprintln!(" {binary} res pull --auto-discover --run-count 3 --interval 1h");
|
||||
eprintln!(" {binary} parse run --force --resource-root /tmp/bat-release");
|
||||
eprintln!(" {binary} parse schedule list --state-dir /tmp/bat-schedule");
|
||||
eprintln!(" {binary} i18n export --translation-file /tmp/bat-workbench.json");
|
||||
eprintln!(
|
||||
" {binary} i18n get --translation-file /tmp/bat-workbench.json --translation-id unit-1"
|
||||
@@ -7734,6 +7853,10 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(
|
||||
" {binary} i18n unset --translation-file /tmp/bat-workbench.json --translation-id unit-1"
|
||||
);
|
||||
eprintln!(" {binary} i18n proofread --json");
|
||||
eprintln!(" {binary} i18n tasks --json");
|
||||
eprintln!(" {binary} i18n handoff --json");
|
||||
eprintln!(" {binary} i18n status --json");
|
||||
eprintln!(" {binary} i18n publish --translation-file /tmp/bat-workbench.json --force");
|
||||
eprintln!(" {binary} status");
|
||||
eprintln!(" {binary} refresh --force --json");
|
||||
@@ -7779,7 +7902,7 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" --run-count <N> Run pull/parse/translate/publish N times");
|
||||
eprintln!(" --once Explicitly select one run");
|
||||
eprintln!(" --resource-root <DIR> Use an explicit published official release root");
|
||||
eprintln!(" --translation-file <PATH> Translation workbench JSON file");
|
||||
eprintln!(" --translation-file <PATH> / --workbench <PATH> Translation workbench JSON file");
|
||||
eprintln!(" --translation-id <ID> TextUnit ID for i18n set");
|
||||
eprintln!(" --translated-text <TEXT> Inline translation for i18n set");
|
||||
eprintln!(" --translated-file <PATH> UTF-8 translation file for i18n set");
|
||||
@@ -7847,8 +7970,10 @@ fn print_usage(binary: &str) {
|
||||
eprintln!(" --quiet-up-to-date Suppress clean up-to-date reports");
|
||||
eprintln!(" --no-quiet-up-to-date Always print reports");
|
||||
eprintln!(" --tail <N> Log lines for logs command (default: 200)");
|
||||
eprintln!(" --schedule-id <ID> Schedule identifier");
|
||||
eprintln!(" --schedule-action <ACTION> Schedule action (pull/run/repack/publish)");
|
||||
eprintln!(" --schedule-id <ID> / --id <ID> Schedule identifier");
|
||||
eprintln!(
|
||||
" --schedule-action <ACTION> / --action <ACTION> Schedule action (pull/run/repack/publish)"
|
||||
);
|
||||
eprintln!(" --schedule-at-unix <SECONDS> First execution time");
|
||||
eprintln!(" --schedule-delay <DURATION> Delay first execution from now");
|
||||
eprintln!(" --schedule-every <DURATION> Period between executions");
|
||||
|
||||
@@ -254,6 +254,34 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationUnset);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"proofread",
|
||||
"--output",
|
||||
"/tmp/bat-resources",
|
||||
"--localized-output",
|
||||
"/tmp/bat-localized",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationProofread);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"proofread",
|
||||
"--resource-root",
|
||||
"/tmp/bat-resources/versions/v-current",
|
||||
"--localized-output",
|
||||
"/tmp/bat-localized",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationProofread);
|
||||
assert_eq!(
|
||||
options.resource_root,
|
||||
Some(PathBuf::from("/tmp/bat-resources/versions/v-current"))
|
||||
);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
@@ -286,6 +314,25 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
||||
Some("provider-run-1")
|
||||
);
|
||||
assert!(parse(&["bat", "i18n", "task", "update", "--task-id", "task-only",]).is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"workbench",
|
||||
"proofread",
|
||||
"--translation-file",
|
||||
"/tmp/workbench.json",
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"proofread",
|
||||
"--translation-file",
|
||||
"/tmp/workbench.json",
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
@@ -355,7 +402,7 @@ fn translation_workbench_commands_read_update_and_clear_entries() {
|
||||
"bat",
|
||||
"i18n",
|
||||
"set",
|
||||
"--translation-file",
|
||||
"--workbench",
|
||||
&path_arg,
|
||||
"--translation-id",
|
||||
"unit-1",
|
||||
@@ -424,6 +471,49 @@ fn grouped_command_long_aliases_and_schedule_options_are_accepted() {
|
||||
assert_eq!(options.schedule_count, Some(4));
|
||||
assert_eq!(options.schedule_args, vec!["--auto-discover"]);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"res",
|
||||
"schedule",
|
||||
"add",
|
||||
"--state-dir",
|
||||
"/tmp/bat-schedule",
|
||||
"--id",
|
||||
"alias-pull",
|
||||
"--action",
|
||||
"pull",
|
||||
"--schedule-delay",
|
||||
"1s",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::ScheduleAdd);
|
||||
assert_eq!(options.schedule_id.as_deref(), Some("alias-pull"));
|
||||
assert_eq!(options.schedule_action.as_deref(), Some("pull"));
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"parse",
|
||||
"schedule",
|
||||
"list",
|
||||
"--state-dir",
|
||||
"/tmp/bat-parse-schedule",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::ScheduleList);
|
||||
assert_eq!(options.schedule_group.as_deref(), Some("parse"));
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"resource",
|
||||
"schedule",
|
||||
"list",
|
||||
"--state-dir",
|
||||
"/tmp/bat-resource-schedule",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::ScheduleList);
|
||||
assert_eq!(options.schedule_group.as_deref(), Some("res"));
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"translate",
|
||||
@@ -438,6 +528,36 @@ fn grouped_command_long_aliases_and_schedule_options_are_accepted() {
|
||||
assert_eq!(options.schedule_group.as_deref(), Some("i18n"));
|
||||
assert!(options.config.force);
|
||||
assert_eq!(options.schedule_max_runs, Some(2));
|
||||
|
||||
let options = parse(&["bat", "translation", "tasks", "--json"]).unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationTasks);
|
||||
|
||||
let options = parse(&["bat", "translation", "task", "list", "--json"]).unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationTasks);
|
||||
|
||||
let options = parse(&["bat", "translation", "task", "status", "--json"]).unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationTasks);
|
||||
|
||||
let options = parse(&["bat", "translation", "handoff", "--json"]).unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationHandoff);
|
||||
|
||||
let options = parse(&["bat", "translation", "status", "--json"]).unwrap();
|
||||
assert_eq!(options.command, CliCommand::LocalizedStatus);
|
||||
|
||||
let options = parse(&[
|
||||
"bat",
|
||||
"translation",
|
||||
"task",
|
||||
"update",
|
||||
"--state-dir",
|
||||
"/tmp/bat-state",
|
||||
"--task-id",
|
||||
"textunit/v-current/TextAssets/Scenario.json",
|
||||
"--task-status",
|
||||
"running",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.command, CliCommand::TranslationTaskUpdate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1100,6 +1220,9 @@ fn parses_management_and_resource_commands() {
|
||||
assert_eq!(options.command, expected);
|
||||
}
|
||||
|
||||
let options = parse(&["bat", "resource", "status"]).unwrap();
|
||||
assert_eq!(options.command, CliCommand::ResourceIndex);
|
||||
|
||||
let index = parse(&[
|
||||
"bat",
|
||||
"resource-index",
|
||||
@@ -3067,12 +3190,28 @@ fn write_catalog_fixture(
|
||||
output_root: &Path,
|
||||
current_bundle: &str,
|
||||
previous_bundle: Option<&str>,
|
||||
) -> PathBuf {
|
||||
write_catalog_fixture_with_localized(
|
||||
state_dir,
|
||||
output_root,
|
||||
&output_root.with_file_name("localized"),
|
||||
current_bundle,
|
||||
previous_bundle,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_catalog_fixture_with_localized(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
localized_output_root: &Path,
|
||||
current_bundle: &str,
|
||||
previous_bundle: Option<&str>,
|
||||
) -> PathBuf {
|
||||
fs::create_dir_all(state_dir).unwrap();
|
||||
fs::create_dir_all(output_root).unwrap();
|
||||
write_daemon_status_file(
|
||||
&daemon_status_path(state_dir),
|
||||
&test_daemon_status_file(state_dir, output_root),
|
||||
&test_daemon_status_file_with_localized(state_dir, output_root, localized_output_root),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -4090,6 +4229,7 @@ fn dispatch_localized_status_verifies_current_release_pointer() {
|
||||
official_release_id: "v-current".to_string(),
|
||||
current_release_id: Some("v-current".to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 123,
|
||||
})
|
||||
.unwrap(),
|
||||
@@ -4138,6 +4278,165 @@ fn dispatch_localized_status_verifies_current_release_pointer() {
|
||||
assert_eq!(value["data"]["patch_manifest_matches_release"], true);
|
||||
assert_eq!(value["data"]["patch_file_count"], 0);
|
||||
assert_eq!(value["data"]["patch_text_asset_operation_count"], 0);
|
||||
assert_eq!(
|
||||
value["data"]["translation_workflow_status"],
|
||||
serde_json::Value::Null
|
||||
);
|
||||
assert_eq!(
|
||||
value["data"]["published_version_path"].as_str().unwrap(),
|
||||
localized_version.to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_translation_proofread_marks_state_for_dashboard() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let state_dir = temp.path().join("state");
|
||||
let output_root = temp.path().join("output");
|
||||
let localized_root = temp.path().join("localized");
|
||||
write_catalog_fixture_with_localized(
|
||||
&state_dir,
|
||||
&output_root,
|
||||
&localized_root,
|
||||
"bundle-b2",
|
||||
None,
|
||||
);
|
||||
let tasks = test_task_context_with_config(OfficialUpdateConfig {
|
||||
localized_output_root: localized_root.clone(),
|
||||
..OfficialUpdateConfig::default()
|
||||
});
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request("translation.proofread", None),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&tasks,
|
||||
"req-proofread-1".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(
|
||||
value["data"]["translation_workflow_status"],
|
||||
"manual_proofreading"
|
||||
);
|
||||
assert_eq!(
|
||||
value["data"]["translation_workflow_status_code"],
|
||||
"translation.manual_proofreading"
|
||||
);
|
||||
assert_eq!(value["data"]["translation_workflow_label"], "人工校对中");
|
||||
assert_eq!(value["data"]["publish_allowed"], false);
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request("localized.status", None),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&tasks,
|
||||
"req-proofread-status-1".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["status_code"], "localized.pending");
|
||||
assert_eq!(
|
||||
value["data"]["translation_workflow_status_code"],
|
||||
"translation.manual_proofreading"
|
||||
);
|
||||
assert_eq!(value["data"]["translation_workflow_label"], "人工校对中");
|
||||
assert_eq!(
|
||||
value["data"]["state"]["translation_workflow_status"],
|
||||
"manual_proofreading"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn localized_status_keeps_published_release_during_manual_proofreading() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let state_dir = temp.path().join("state");
|
||||
let output_root = temp.path().join("output");
|
||||
let localized_root = temp.path().join("localized");
|
||||
write_catalog_fixture_with_localized(
|
||||
&state_dir,
|
||||
&output_root,
|
||||
&localized_root,
|
||||
"bundle-b2",
|
||||
None,
|
||||
);
|
||||
let localized_version = localized_root
|
||||
.join(LOCALIZED_VERSIONS_DIR)
|
||||
.join("v-current-auto");
|
||||
fs::create_dir_all(&localized_version).unwrap();
|
||||
symlink(
|
||||
Path::new(LOCALIZED_VERSIONS_DIR).join("v-current-auto"),
|
||||
localized_root.join(LOCALIZED_CURRENT_LINK),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
localized_root.join(LOCALIZED_VERSION_STATE_FILE),
|
||||
serde_json::to_vec(&bat_infrastructure::LocalizedVersionState {
|
||||
state_version: bat_infrastructure::LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: "v-current".to_string(),
|
||||
current_release_id: Some("v-current-auto".to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 123,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
localized_version.join(LOCALIZED_PATCH_MANIFEST_FILE),
|
||||
serde_json::to_vec(&bat_infrastructure::LocalizedPatchManifest {
|
||||
manifest_version: bat_infrastructure::LOCALIZED_PATCH_MANIFEST_VERSION,
|
||||
official_release_id: "v-current".to_string(),
|
||||
localized_release_id: "v-current-auto".to_string(),
|
||||
generated_unix_seconds: 124,
|
||||
file_count: 0,
|
||||
text_asset_operation_count: 0,
|
||||
files: Vec::new(),
|
||||
rollback: bat_infrastructure::LocalizedPatchRollbackInfo {
|
||||
previous_current_target: None,
|
||||
remove_version_path: localized_version.clone(),
|
||||
},
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let tasks = test_task_context_with_config(OfficialUpdateConfig {
|
||||
localized_output_root: localized_root.clone(),
|
||||
..OfficialUpdateConfig::default()
|
||||
});
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request("translation.proofread", None),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&tasks,
|
||||
"req-proofread-2".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["publish_allowed"], true);
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request("localized.status", None),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&tasks,
|
||||
"req-proofread-status-2".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(&envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["status"], "published");
|
||||
assert_eq!(value["data"]["status_code"], "localized.published");
|
||||
assert_eq!(value["data"]["localized_release_status"], "localized");
|
||||
assert_eq!(
|
||||
value["data"]["translation_workflow_status_code"],
|
||||
"translation.manual_proofreading"
|
||||
);
|
||||
assert_eq!(value["data"]["translation_workflow_label"], "人工校对中");
|
||||
assert_eq!(value["data"]["current_points_to_published_version"], true);
|
||||
assert_eq!(
|
||||
value["data"]["published_version_path"].as_str().unwrap(),
|
||||
localized_version.to_string_lossy()
|
||||
@@ -4168,6 +4467,7 @@ fn dispatch_localized_status_rejects_stale_release_state() {
|
||||
official_release_id: "v-old".to_string(),
|
||||
current_release_id: Some("v-old".to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 123,
|
||||
})
|
||||
.unwrap(),
|
||||
|
||||
@@ -235,6 +235,24 @@ pub(super) fn run_translation_task_update(options: &CliOptions) -> anyhow::Resul
|
||||
print_json_value(options.output_format, &report)
|
||||
}
|
||||
|
||||
pub(super) fn run_translation_proofread(options: &CliOptions) -> anyhow::Result<()> {
|
||||
if daemon_rpc_available(&options.state_dir)
|
||||
&& options.resource_root.is_none()
|
||||
&& !options.output_explicit
|
||||
{
|
||||
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
|
||||
let report = daemon_rpc_call(&options.state_dir, RPC_METHOD_TRANSLATION_PROOFREAD, None)?;
|
||||
print_json_value(options.output_format, &report)?;
|
||||
return Ok(());
|
||||
}
|
||||
let (_, official_release_id) = current_official_release(options)?;
|
||||
let report = bat_infrastructure::mark_localized_manual_proofreading(
|
||||
&options.config.localized_output_root,
|
||||
&official_release_id,
|
||||
)?;
|
||||
print_report(options.output_format, &report)
|
||||
}
|
||||
|
||||
pub(super) fn run_repack(options: &CliOptions) -> anyhow::Result<()> {
|
||||
let spec = options
|
||||
.repack_spec
|
||||
|
||||
@@ -46,12 +46,15 @@ pub use import::{
|
||||
ResourceImportService,
|
||||
};
|
||||
pub use localized_patch::{
|
||||
read_localized_patch_manifest_at, read_localized_version_state, LocalizedPatchConfig,
|
||||
mark_localized_manual_proofreading, read_localized_patch_manifest_at,
|
||||
read_localized_version_state, write_localized_version_state, LocalizedPatchConfig,
|
||||
LocalizedPatchFile, LocalizedPatchIntegrity, LocalizedPatchManifest, LocalizedPatchOperation,
|
||||
LocalizedPatchReport, LocalizedPatchRollbackInfo, LocalizedPatchService,
|
||||
LocalizedTextAssetPatch, LocalizedVersionState, LOCALIZED_CURRENT_LINK,
|
||||
LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_PATCH_MANIFEST_VERSION, LOCALIZED_STAGING_DIR,
|
||||
LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE,
|
||||
LocalizedTextAssetPatch, LocalizedTranslationWorkflowReport, LocalizedVersionState,
|
||||
LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_PATCH_MANIFEST_VERSION,
|
||||
LOCALIZED_STAGING_DIR, LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING,
|
||||
LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL, LOCALIZED_VERSIONS_DIR,
|
||||
LOCALIZED_VERSION_STATE_FILE, LOCALIZED_VERSION_STATE_VERSION,
|
||||
};
|
||||
pub use official_changes::{
|
||||
read_resource_change_set_at, write_crowdin_translation_handoff_at,
|
||||
|
||||
@@ -23,6 +23,12 @@ pub const LOCALIZED_VERSION_STATE_FILE: &str = "localized-version-state.json";
|
||||
pub const LOCALIZED_PATCH_MANIFEST_FILE: &str = "localized-patch-manifest.json";
|
||||
/// Current localized patch manifest schema version.
|
||||
pub const LOCALIZED_PATCH_MANIFEST_VERSION: u32 = 1;
|
||||
/// Current localized version state schema version.
|
||||
pub const LOCALIZED_VERSION_STATE_VERSION: u32 = 1;
|
||||
/// Stable marker for localized releases that are under human proofreading.
|
||||
pub const LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING: &str = "manual_proofreading";
|
||||
/// Human label for `LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING`.
|
||||
pub const LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL: &str = "人工校对中";
|
||||
|
||||
/// One patch operation against a bundle in an official release.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -94,6 +100,7 @@ impl LocalizedPatchConfig {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LocalizedVersionState {
|
||||
/// State schema version.
|
||||
#[serde(default = "default_localized_version_state_version")]
|
||||
pub state_version: u32,
|
||||
/// Official release ID used as the patch source.
|
||||
pub official_release_id: String,
|
||||
@@ -101,10 +108,59 @@ pub struct LocalizedVersionState {
|
||||
pub current_release_id: Option<String>,
|
||||
/// Stable status label.
|
||||
pub status: String,
|
||||
/// Review/proofreading workflow marker independent from publish status.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub translation_workflow_status: Option<String>,
|
||||
/// Last update time.
|
||||
pub updated_unix_seconds: u64,
|
||||
}
|
||||
|
||||
impl LocalizedVersionState {
|
||||
/// Returns the stable translation workflow status, if set.
|
||||
pub fn translation_workflow_status(&self) -> Option<&str> {
|
||||
self.translation_workflow_status.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the user-facing translation workflow label, if set.
|
||||
pub fn translation_workflow_label(&self) -> Option<&'static str> {
|
||||
match self.translation_workflow_status() {
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING) => {
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Report produced when changing localized translation workflow state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct LocalizedTranslationWorkflowReport {
|
||||
/// Stable command name.
|
||||
pub command: &'static str,
|
||||
/// Stable operation status.
|
||||
pub status: &'static str,
|
||||
/// Localized output root.
|
||||
pub localized_output_root: PathBuf,
|
||||
/// State file written under the localized output root.
|
||||
pub state_path: PathBuf,
|
||||
/// Official release associated with the workflow state.
|
||||
pub official_release_id: String,
|
||||
/// Current published localized release ID, when one exists.
|
||||
pub current_release_id: Option<String>,
|
||||
/// Stable localized publication label, such as `localized`.
|
||||
pub localized_release_status: String,
|
||||
/// Stable translation workflow marker.
|
||||
pub translation_workflow_status: String,
|
||||
/// Stable lifecycle status code for read-side callers.
|
||||
pub translation_workflow_status_code: &'static str,
|
||||
/// Human label for the workflow status.
|
||||
pub translation_workflow_label: &'static str,
|
||||
/// Whether an existing localized release remains publishable after the change.
|
||||
pub publish_allowed: bool,
|
||||
/// Last update time written to the state file.
|
||||
pub updated_unix_seconds: u64,
|
||||
}
|
||||
|
||||
/// One changed file in a localized patch manifest.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LocalizedPatchFile {
|
||||
@@ -295,6 +351,10 @@ impl LocalizedPatchService {
|
||||
.localized_output_root
|
||||
.join(LOCALIZED_VERSION_STATE_FILE);
|
||||
let patch_manifest_path = version_path.join(LOCALIZED_PATCH_MANIFEST_FILE);
|
||||
let previous_state = read_localized_version_state(&config.localized_output_root)?;
|
||||
let translation_workflow_status = previous_state
|
||||
.filter(|state| state.official_release_id == config.release_id)
|
||||
.and_then(|state| state.translation_workflow_status);
|
||||
|
||||
if version_path.exists() {
|
||||
let message = if config.force {
|
||||
@@ -373,10 +433,11 @@ impl LocalizedPatchService {
|
||||
)?;
|
||||
|
||||
let state = LocalizedVersionState {
|
||||
state_version: 1,
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: config.release_id.clone(),
|
||||
current_release_id: Some(config.published_release_id().to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status,
|
||||
updated_unix_seconds: unix_seconds_now(),
|
||||
};
|
||||
write_file_atomic(
|
||||
@@ -415,7 +476,7 @@ pub fn read_localized_version_state(
|
||||
return Ok(None);
|
||||
};
|
||||
let state: LocalizedVersionState = serde_json::from_slice(&bytes)?;
|
||||
if state.state_version != 1 {
|
||||
if state.state_version != LOCALIZED_VERSION_STATE_VERSION {
|
||||
return Err(anyhow::anyhow!(
|
||||
"不支持的汉化版本状态 schema:{},当前版本=1",
|
||||
state.state_version
|
||||
@@ -424,6 +485,70 @@ pub fn read_localized_version_state(
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
/// Writes the localized version state atomically.
|
||||
pub fn write_localized_version_state(
|
||||
localized_output_root: &Path,
|
||||
state: &LocalizedVersionState,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
ensure_safe_directory_path(localized_output_root, "汉化输出目录")
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let path = localized_output_root.join(LOCALIZED_VERSION_STATE_FILE);
|
||||
write_file_atomic(
|
||||
&path,
|
||||
&serde_json::to_vec_pretty(state)?,
|
||||
STATE_FILE_MODE,
|
||||
"汉化版本状态",
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Marks the current localized workflow as under manual proofreading without
|
||||
/// changing the published localized release pointer.
|
||||
pub fn mark_localized_manual_proofreading(
|
||||
localized_output_root: &Path,
|
||||
official_release_id: &str,
|
||||
) -> anyhow::Result<LocalizedTranslationWorkflowReport> {
|
||||
let mut state = read_localized_version_state(localized_output_root)?.unwrap_or_else(|| {
|
||||
LocalizedVersionState {
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: official_release_id.to_string(),
|
||||
current_release_id: None,
|
||||
status: "not_localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 0,
|
||||
}
|
||||
});
|
||||
if state.official_release_id != official_release_id {
|
||||
return Err(anyhow::anyhow!(
|
||||
"汉化状态 release={} 与当前官方 release={} 不一致;请先重新发布或指定匹配的官方 release",
|
||||
state.official_release_id,
|
||||
official_release_id
|
||||
));
|
||||
}
|
||||
state.state_version = LOCALIZED_VERSION_STATE_VERSION;
|
||||
state.translation_workflow_status =
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING.to_string());
|
||||
state.updated_unix_seconds = unix_seconds_now();
|
||||
let state_path = write_localized_version_state(localized_output_root, &state)?;
|
||||
|
||||
Ok(LocalizedTranslationWorkflowReport {
|
||||
command: "translation-proofread",
|
||||
status: "updated",
|
||||
localized_output_root: localized_output_root.to_path_buf(),
|
||||
state_path,
|
||||
official_release_id: state.official_release_id.clone(),
|
||||
current_release_id: state.current_release_id.clone(),
|
||||
localized_release_status: state.status.clone(),
|
||||
translation_workflow_status: LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING.to_string(),
|
||||
translation_workflow_status_code:
|
||||
crate::ReleaseFlowStatusCode::TranslationManualProofreading.as_str(),
|
||||
translation_workflow_label: LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING_LABEL,
|
||||
publish_allowed: state.status == "localized" && state.current_release_id.is_some(),
|
||||
updated_unix_seconds: state.updated_unix_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads a localized patch manifest from a published version directory.
|
||||
pub fn read_localized_patch_manifest_at(
|
||||
version_path: &Path,
|
||||
@@ -754,6 +879,10 @@ fn default_patch_manifest_version() -> u32 {
|
||||
LOCALIZED_PATCH_MANIFEST_VERSION
|
||||
}
|
||||
|
||||
fn default_localized_version_state_version() -> u32 {
|
||||
LOCALIZED_VERSION_STATE_VERSION
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -846,6 +975,7 @@ mod tests {
|
||||
serde_json::from_slice(&fs::read(report.state_path).unwrap()).unwrap();
|
||||
assert_eq!(state.status, "localized");
|
||||
assert_eq!(state.current_release_id.as_deref(), Some("release-1"));
|
||||
assert_eq!(state.translation_workflow_status(), None);
|
||||
assert!(report.patch_manifest_path.is_file());
|
||||
assert_eq!(report.manifest.file_count, 0);
|
||||
assert_eq!(report.integrity.verified_changed_file_count, 0);
|
||||
@@ -857,6 +987,62 @@ mod tests {
|
||||
assert_eq!(manifest.rollback.previous_current_target, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn localized_state_reads_legacy_file_without_workflow_status() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let localized = temp.path().join("localized");
|
||||
fs::create_dir_all(&localized).unwrap();
|
||||
fs::write(
|
||||
localized.join(LOCALIZED_VERSION_STATE_FILE),
|
||||
br#"{
|
||||
"state_version": 1,
|
||||
"official_release_id": "release-1",
|
||||
"current_release_id": "release-1",
|
||||
"status": "localized",
|
||||
"updated_unix_seconds": 123
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let state = read_localized_version_state(&localized).unwrap().unwrap();
|
||||
|
||||
assert_eq!(state.status, "localized");
|
||||
assert_eq!(state.translation_workflow_status(), None);
|
||||
assert_eq!(state.translation_workflow_label(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_proofreading_marker_preserves_published_state() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let localized = temp.path().join("localized");
|
||||
fs::create_dir_all(&localized).unwrap();
|
||||
write_localized_version_state(
|
||||
&localized,
|
||||
&LocalizedVersionState {
|
||||
state_version: LOCALIZED_VERSION_STATE_VERSION,
|
||||
official_release_id: "release-1".to_string(),
|
||||
current_release_id: Some("release-1-auto".to_string()),
|
||||
status: "localized".to_string(),
|
||||
translation_workflow_status: None,
|
||||
updated_unix_seconds: 123,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report = mark_localized_manual_proofreading(&localized, "release-1").unwrap();
|
||||
let state = read_localized_version_state(&localized).unwrap().unwrap();
|
||||
|
||||
assert_eq!(report.translation_workflow_label, "人工校对中");
|
||||
assert_eq!(
|
||||
state.translation_workflow_status(),
|
||||
Some(LOCALIZED_TRANSLATION_STATUS_MANUAL_PROOFREADING)
|
||||
);
|
||||
assert_eq!(state.translation_workflow_label(), Some("人工校对中"));
|
||||
assert_eq!(state.status, "localized");
|
||||
assert_eq!(state.current_release_id.as_deref(), Some("release-1-auto"));
|
||||
assert!(report.publish_allowed);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn failed_patch_publish_cleans_staging_and_unpublished_version() {
|
||||
|
||||
@@ -63,6 +63,9 @@ pub enum ReleaseFlowStatusCode {
|
||||
/// Translation tasks have been queued to local offline handoff files.
|
||||
#[serde(rename = "translation.queued_offline")]
|
||||
TranslationQueuedOffline,
|
||||
/// Human proofreading is in progress for the observed localized release.
|
||||
#[serde(rename = "translation.manual_proofreading")]
|
||||
TranslationManualProofreading,
|
||||
/// Localized publication is blocked because there is no official release.
|
||||
#[serde(rename = "localized.blocked_official")]
|
||||
LocalizedBlockedOfficial,
|
||||
@@ -105,6 +108,7 @@ impl ReleaseFlowStatusCode {
|
||||
Self::TranslationUnavailable => "translation.unavailable",
|
||||
Self::TranslationHandoffPreparing => "translation.handoff_preparing",
|
||||
Self::TranslationQueuedOffline => "translation.queued_offline",
|
||||
Self::TranslationManualProofreading => "translation.manual_proofreading",
|
||||
Self::LocalizedBlockedOfficial => "localized.blocked_official",
|
||||
Self::LocalizedPending => "localized.pending",
|
||||
Self::LocalizedStale => "localized.stale",
|
||||
@@ -137,6 +141,7 @@ impl ReleaseFlowStatusCode {
|
||||
Self::TranslationUnavailable => "unavailable",
|
||||
Self::TranslationHandoffPreparing => "handoff_preparing",
|
||||
Self::TranslationQueuedOffline => "queued_offline",
|
||||
Self::TranslationManualProofreading => "manual_proofreading",
|
||||
Self::LocalizedBlockedOfficial => "blocked_official",
|
||||
Self::LocalizedPending => "pending",
|
||||
Self::LocalizedStale => "stale",
|
||||
@@ -167,6 +172,7 @@ impl ReleaseFlowStatusCode {
|
||||
"translation.unavailable" => Self::TranslationUnavailable,
|
||||
"translation.handoff_preparing" => Self::TranslationHandoffPreparing,
|
||||
"translation.queued_offline" => Self::TranslationQueuedOffline,
|
||||
"translation.manual_proofreading" => Self::TranslationManualProofreading,
|
||||
"localized.blocked_official" => Self::LocalizedBlockedOfficial,
|
||||
"localized.pending" => Self::LocalizedPending,
|
||||
"localized.stale" => Self::LocalizedStale,
|
||||
@@ -197,7 +203,8 @@ impl ReleaseFlowStatusCode {
|
||||
| Self::ParseCompletedWithErrors => "parse",
|
||||
Self::TranslationUnavailable
|
||||
| Self::TranslationHandoffPreparing
|
||||
| Self::TranslationQueuedOffline => "translation",
|
||||
| Self::TranslationQueuedOffline
|
||||
| Self::TranslationManualProofreading => "translation",
|
||||
Self::LocalizedBlockedOfficial
|
||||
| Self::LocalizedPending
|
||||
| Self::LocalizedStale
|
||||
@@ -288,6 +295,7 @@ mod tests {
|
||||
ReleaseFlowStatusCode::TranslationUnavailable,
|
||||
ReleaseFlowStatusCode::TranslationHandoffPreparing,
|
||||
ReleaseFlowStatusCode::TranslationQueuedOffline,
|
||||
ReleaseFlowStatusCode::TranslationManualProofreading,
|
||||
ReleaseFlowStatusCode::LocalizedBlockedOfficial,
|
||||
ReleaseFlowStatusCode::LocalizedPending,
|
||||
ReleaseFlowStatusCode::LocalizedStale,
|
||||
|
||||
@@ -50,6 +50,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/admin/control/schedule-remove",
|
||||
"/admin/control/schedule-run",
|
||||
"/admin/control/translation-task-update",
|
||||
"/admin/control/translation-proofread",
|
||||
},
|
||||
}
|
||||
if r.Method == http.MethodHead {
|
||||
@@ -81,6 +82,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminTranslationTaskUpdate(w, r)
|
||||
return
|
||||
}
|
||||
if action == "translation-proofread" {
|
||||
s.handleAdminTranslationProofread(w, r)
|
||||
return
|
||||
}
|
||||
request, ok := decodeAdminControlRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -182,6 +187,26 @@ func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminTranslationProofread(w http.ResponseWriter, r *http.Request) {
|
||||
backend, ok := s.backend.(TranslationBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_backend_unavailable", "Rust bat translation backend is unavailable")
|
||||
return
|
||||
}
|
||||
result, err := backend.TranslationProofread(r.Context())
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "translation-proofread", err)
|
||||
return
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||
Service: "bat-api",
|
||||
Action: "translation-proofread",
|
||||
RPCMethod: "translation.proofread",
|
||||
Status: "accepted",
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||
|
||||
@@ -593,6 +593,11 @@ func (b *controlBackend) TranslationTaskUpdate(ctx context.Context, params backe
|
||||
return json.RawMessage(`{"task_status":"` + params.Status + `"}`), nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) TranslationProofread(ctx context.Context) (json.RawMessage, error) {
|
||||
b.calls = append(b.calls, "translation.proofread")
|
||||
return json.RawMessage(`{"translation_workflow_status":"manual_proofreading"}`), nil
|
||||
}
|
||||
|
||||
type scheduleBackend struct {
|
||||
*controlBackend
|
||||
scheduleCalls []string
|
||||
@@ -1156,6 +1161,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
||||
{name: "repair", action: "repair", rpcMethod: "resource.repair", call: "resource.repair"},
|
||||
{name: "catalog refresh", action: "catalog-refresh", rpcMethod: "catalog.refresh", call: "catalog.refresh"},
|
||||
{name: "translation task update", action: "translation-task-update", body: `{"task_id":"textunit/v-current/Scenario","status":"failed","failure_reason":"provider rejected payload","provider_run_id":"provider-run-1"}`, rpcMethod: "translation.task.update", call: "translation.task.update"},
|
||||
{name: "translation proofread", action: "translation-proofread", rpcMethod: "translation.proofread", call: "translation.proofread"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -148,7 +148,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, translation-task-update]
|
||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, translation-task-update, translation-proofread]
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
|
||||
@@ -53,6 +53,7 @@ type ScheduleBackend interface {
|
||||
// by the dashboard. It does not create arbitrary translation jobs.
|
||||
type TranslationBackend interface {
|
||||
TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error)
|
||||
TranslationProofread(ctx context.Context) (json.RawMessage, error)
|
||||
}
|
||||
|
||||
// RPCClient adapts *backendrpc.Client to Backend.
|
||||
@@ -114,6 +115,10 @@ func (r RPCClient) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRu
|
||||
func (r RPCClient) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) {
|
||||
return r.Client.TranslationTaskUpdate(ctx, params)
|
||||
}
|
||||
|
||||
func (r RPCClient) TranslationProofread(ctx context.Context) (json.RawMessage, error) {
|
||||
return r.Client.TranslationProofread(ctx)
|
||||
}
|
||||
func (r RPCClient) ParseStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return r.Client.ParseStatus(ctx)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ type rpcRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID uint64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params any `json:"params,omitempty"`
|
||||
Params any `json:"params"`
|
||||
}
|
||||
|
||||
type rpcResponse struct {
|
||||
@@ -273,6 +273,12 @@ type TranslationTaskUpdateParams struct {
|
||||
ProviderRunID string `json:"provider_run_id,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationProofread marks the current localized workflow as manual
|
||||
// proofreading. Rust owns the persisted localized state.
|
||||
func (c *Client) TranslationProofread(ctx context.Context) (json.RawMessage, error) {
|
||||
return c.rawData(ctx, "translation.proofread", nil)
|
||||
}
|
||||
|
||||
// Ack is returned by accepted daemon control methods.
|
||||
type Ack struct {
|
||||
Command string `json:"command"`
|
||||
|
||||
@@ -375,6 +375,35 @@ func TestTranslationTaskUpdateSendsWorkerParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "translation.proofread" {
|
||||
t.Fatalf("method = %s", req.Method)
|
||||
}
|
||||
if string(req.Params) != "null" {
|
||||
t.Fatalf("params = %s, want null", req.Params)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true,
|
||||
Status: "ok",
|
||||
RequestID: "req-test-translation-proofread",
|
||||
Data: map[string]any{
|
||||
"translation_workflow_status": "manual_proofreading",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
raw, err := client.TranslationProofread(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("TranslationProofread error: %v", err)
|
||||
}
|
||||
if !json.Valid(raw) {
|
||||
t.Fatalf("invalid raw JSON: %s", string(raw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationErrorReturnsAPIError(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "task.status" {
|
||||
|
||||
Reference in New Issue
Block a user