mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
+2
-2
@@ -30,7 +30,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
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 明细与解析错误;`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 翻译任务状态与跳过/失败原因;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` 的 `schedule.*` 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 和翻译任务状态回写已通过 `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.sqlite` 和版本化 `translation-handoff.json`;
|
||||||
`translation.tasks` 查询单项 worker 状态,`translation.handoff` 查询完整
|
`translation.tasks` 查询单项 worker 状态,`translation.handoff` 查询完整
|
||||||
@@ -293,7 +293,7 @@ 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)、`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 增量缓存和可选持久化。
|
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` 已提供 worker 状态回写 contract。剩余为真实 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 状态回写入口。剩余为真实 Crowdin/provider worker、翻译记忆和 Patch 构建。
|
||||||
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 发布前置已具备回归测试。
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
8. `bat-ffi` 已提供 Manifest inspect 和官方 sync plan 的可选无状态粗粒度 JSON C ABI helper。
|
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`,表示原版资源已发布、汉化资源未发布。
|
||||||
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 明细索引和队列时只读取摘要,不重复解析。
|
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 已经通过 `bat.sock` 和 `bat-api` 管理接口暴露,dashboard 不维护第二套状态;该能力不扩大冻结期解析器覆盖。
|
11. issue 43 已补齐 Rust `bat` 的 `res` / `parse` / `i18n` 工作流:单次/限定次数/周期执行、版本化 schedule CRUD 与作用域过滤、解析缓存清理、翻译工作台校验、离线翻译工作台、人工文本修改、翻译任务状态回写、既有 patch 能力的批量重打包和独立汉化 release 发布;schedule CRUD 与 `translation.task.update` 已经通过 `bat.sock` 和 `bat-api` 管理接口暴露,dashboard 不维护第二套状态;该能力不扩大冻结期解析器覆盖。
|
||||||
12. 文档已整理:根目录保留入口文档,历史报告进入 `docs/reports/historical/`,误嵌套的 `docs/docs` 已合并。
|
12. 文档已整理:根目录保留入口文档,历史报告进入 `docs/reports/historical/`,误嵌套的 `docs/docs` 已合并。
|
||||||
|
|
||||||
### 仍是骨架或占位
|
### 仍是骨架或占位
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ launcher 兼容端点只服务启动前资源发现。它们复用 Rust `bat` sn
|
|||||||
| `schedule-update` | `schedule.update` | 调度 mutation JSON | `202` + Rust schedule report |
|
| `schedule-update` | `schedule.update` | 调度 mutation JSON | `202` + Rust schedule report |
|
||||||
| `schedule-remove` | `schedule.remove` | `{ "id": "..." }` | `202` + Rust schedule report |
|
| `schedule-remove` | `schedule.remove` | `{ "id": "..." }` | `202` + Rust schedule report |
|
||||||
| `schedule-run` | `schedule.run` | 可选 `{ "id": "...", "force": true }` | `202` + 执行报告 |
|
| `schedule-run` | `schedule.run` | 可选 `{ "id": "...", "force": true }` | `202` + 执行报告 |
|
||||||
|
| `translation-task-update` | `translation.task.update` | `{ "task_id": "...", "status": "...", "failure_reason": "...", "provider_run_id": "..." }` | `202` + 当前任务记录 |
|
||||||
|
|
||||||
`stop`、`clean-stable`、patch 和 UnityFS 写入命令不会经 HTTP 暴露。
|
`stop`、`clean-stable`、patch 和 UnityFS 写入命令不会经 HTTP 暴露。
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ bat i18n publish \
|
|||||||
|
|
||||||
当前 `i18n run` 是离线工作流:刷新 TextUnit 队列,并可用 `--translation-file` 导出工作台;不会假装调用未接入的外部 provider。
|
当前 `i18n run` 是离线工作流:刷新 TextUnit 队列,并可用 `--translation-file` 导出工作台;不会假装调用未接入的外部 provider。
|
||||||
|
|
||||||
|
provider worker 消费 handoff 后,可以用 `i18n task update` 回写当前 release 的任务状态:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bat i18n task update \
|
||||||
|
--state-dir /tmp/bat-state \
|
||||||
|
--task-id textunit/v-current/TextAssets/Scenario.json \
|
||||||
|
--task-status failed \
|
||||||
|
--failure-reason "provider rejected payload" \
|
||||||
|
--provider-run-id provider-run-1 \
|
||||||
|
--json
|
||||||
|
```
|
||||||
|
|
||||||
|
`--task-status` 支持 Rust contract 中的 `queued`、`running`、`failed`、
|
||||||
|
`completed` 和 `skipped`;命令只更新当前 release 的
|
||||||
|
`translation-tasks.sqlite`,不会创建任意翻译任务。
|
||||||
|
|
||||||
## 持久化调度
|
## 持久化调度
|
||||||
|
|
||||||
每个一级工作流都可以管理自己的 schedule。调度计划保存在 `--state-dir/bat-schedules.json`,计划记录包含动作、参数、下一次执行时间、周期、剩余次数、启用状态和最近错误。
|
每个一级工作流都可以管理自己的 schedule。调度计划保存在 `--state-dir/bat-schedules.json`,计划记录包含动作、参数、下一次执行时间、周期、剩余次数、启用状态和最近错误。
|
||||||
@@ -170,6 +186,11 @@ contract:`id`、`group`、`action`、`args`、`next_run_unix_seconds`、
|
|||||||
`enabled`;`schedule.list` 额外接受 `id`、`group`、`enabled` 过滤,
|
`enabled`;`schedule.list` 额外接受 `id`、`group`、`enabled` 过滤,
|
||||||
`schedule.run` 额外接受 `group`、`force` 和 `max_runs`。
|
`schedule.run` 额外接受 `group`、`force` 和 `max_runs`。
|
||||||
|
|
||||||
|
翻译任务状态也可由已鉴权的 dashboard 通过
|
||||||
|
`POST /admin/control/translation-task-update` 回写,请求字段为
|
||||||
|
`task_id`、`status`,以及可选的 `failure_reason`、`provider_run_id`;
|
||||||
|
该接口只转发 `translation.task.update`。
|
||||||
|
|
||||||
## 边界
|
## 边界
|
||||||
|
|
||||||
解析器新增类型覆盖和新的解析格式仍受 `docs/reports/PARSER_FREEZE.md` 约束。本次 issue 43 的例外只开放已有解析输出的手动编排、缓存刷新、工作台编辑、既有 patch 实现的重打包和独立汉化发布,不扩展 UnityFS/AssetBundle/Addressables/TypeTree 的解析类型覆盖。
|
解析器新增类型覆盖和新的解析格式仍受 `docs/reports/PARSER_FREEZE.md` 约束。本次 issue 43 的例外只开放已有解析输出的手动编排、缓存刷新、工作台编辑、既有 patch 实现的重打包和独立汉化发布,不扩展 UnityFS/AssetBundle/Addressables/TypeTree 的解析类型覆盖。
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ CLI 对应关系:
|
|||||||
| `bat parse-errors` | `parse.errors` |
|
| `bat parse-errors` | `parse.errors` |
|
||||||
| `bat translation-tasks` | `translation.tasks` |
|
| `bat translation-tasks` | `translation.tasks` |
|
||||||
| `bat translation-handoff` | `translation.handoff` |
|
| `bat translation-handoff` | `translation.handoff` |
|
||||||
|
| `bat i18n task update` | `translation.task.update` |
|
||||||
| `bat localized-status` | `localized.status` |
|
| `bat localized-status` | `localized.status` |
|
||||||
| `bat resource-index` | `resource.index` |
|
| `bat resource-index` | `resource.index` |
|
||||||
|
|
||||||
@@ -414,11 +415,11 @@ CLI 对应关系:
|
|||||||
- typed helper 已覆盖 daemon 已实现方法(`status/logs/stop/restart/reload/refresh/doctor`)、
|
- typed helper 已覆盖 daemon 已实现方法(`status/logs/stop/restart/reload/refresh/doctor`)、
|
||||||
`resource.state/sync/verify/repair/manifest/list`、`schedule.list/add/update/remove/run`、
|
`resource.state/sync/verify/repair/manifest/list`、`schedule.list/add/update/remove/run`、
|
||||||
`catalog.*`、`parse.*`、
|
`catalog.*`、`parse.*`、
|
||||||
`localized.status`、`task.*` 和三个
|
`localized.status`、`translation.task.update`、`task.*` 和三个
|
||||||
`unityfs.patch_*` 方法。
|
`unityfs.patch_*` 方法。
|
||||||
- `resource.index`、`translation.tasks`、`translation.handoff`、
|
- `resource.index`、`translation.tasks`、`translation.handoff` 和
|
||||||
`translation.task.update` 和 `patch.apply` 当前没有专用 typed helper;
|
`patch.apply` 当前没有专用 typed helper;需要直接使用 `Call`,并仍须遵守
|
||||||
需要直接使用 `Call`,并仍须遵守本契约的参数和响应定义。
|
本契约的参数和响应定义。
|
||||||
|
|
||||||
`internal/api` 对 bat-api 生产路径进一步收窄接口:
|
`internal/api` 对 bat-api 生产路径进一步收窄接口:
|
||||||
|
|
||||||
@@ -427,6 +428,7 @@ CLI 对应关系:
|
|||||||
| `Backend` | `daemon.status`、`daemon.doctor`、`resource.state`、`catalog.status`、`resource.manifest` | 启动发现、周期刷新和资源分发 |
|
| `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` | 鉴权后的管理控制白名单 |
|
| `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 调度计划控制 |
|
| `ScheduleBackend` | `schedule.list`、`schedule.add`、`schedule.update`、`schedule.remove`、`schedule.run` | 鉴权后的 dashboard 调度计划控制 |
|
||||||
|
| `TranslationBackend` | `translation.task.update` | 鉴权后的 dashboard 翻译任务状态回写 |
|
||||||
|
|
||||||
`daemon.stop`、`daemon.clean-stable` 和任意通用 RPC 不属于 bat-api 管理控制面。
|
`daemon.stop`、`daemon.clean-stable` 和任意通用 RPC 不属于 bat-api 管理控制面。
|
||||||
Rust dispatch、Go transport 和 bat-api 接口的权威实现位置分别是
|
Rust dispatch、Go transport 和 bat-api 接口的权威实现位置分别是
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ issue 43 例外说明:新增的 `parse repack` 只编排已有 TextAsset、Typ
|
|||||||
- 官方 release 发布后还会写入版本化 `translation-handoff.json`;`translation.handoff` RPC/CLI 动态合并该快照与 SQLite worker 状态,暴露 job、unit、provider run、attempt 和 failure reason。
|
- 官方 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.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.tasks` RPC/CLI 优先查询 `translation-tasks.sqlite`,旧 release 没有状态库时回退到 `official-textunit-tasks.json`;返回队列 `status`、worker `task_status`、failure reason 和时间/尝试次数。
|
||||||
- `translation.task.update` RPC 已提供 queued/running/failed/completed/skipped 状态回写契约,provider worker 可在消费 handoff 后按 task_id 更新并由同一查询接口反查。
|
- `translation.task.update` 已提供 queued/running/failed/completed/skipped 状态回写契约,并已通过 `bat i18n task update`、Go typed helper 和 bat-api 鉴权控制面暴露;provider worker 可在消费 handoff 后按 task_id 更新并由同一查询接口反查。
|
||||||
- `i18n validate` 可在发布前校验工作台 release、source text、重复 patch 目标,并区分可直接发布的 TextAsset 与必须进入 `parse repack` 的条目;`parse clear-cache` 只删除可再生解析/队列 JSON,保留 `translation-tasks.sqlite` 的 worker 状态。
|
- `i18n validate` 可在发布前校验工作台 release、source text、重复 patch 目标,并区分可直接发布的 TextAsset 与必须进入 `parse repack` 的条目;`parse clear-cache` 只删除可再生解析/队列 JSON,保留 `translation-tasks.sqlite` 的 worker 状态。
|
||||||
- 真实 Crowdin 网络 worker、翻译记忆和完整 localized repack 仍属于后续翻译系统工作,不在当前 Rust 离线状态仓储范围内。
|
- 真实 Crowdin 网络 worker、翻译记忆和完整 localized repack 仍属于后续翻译系统工作,不在当前 Rust 离线状态仓储范围内。
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ make build-go-cli # 产出 bin/bat-go
|
|||||||
| 项 | 状态 |
|
| 项 | 状态 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| G-008 Go 同步 CLI | **已决策关闭**(正式同步 CLI = Rust `bat`) |
|
| G-008 Go 同步 CLI | **已决策关闭**(正式同步 CLI = Rust `bat`) |
|
||||||
| G-009 bat-api 资源 bootstrap/分发 | **部分完成**(MVP+生产控制面);已含资源 bootstrap 关系面、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、RPC 周期刷新/诊断、readiness、OpenAPI、管理控制白名单、Rust-owned `schedule.*` dashboard 代理和部署模板,后续远程服务器联调/可选持久化 |
|
| G-009 bat-api 资源 bootstrap/分发 | **部分完成**(MVP+生产控制面);已含资源 bootstrap 关系面、launcher 资源 metadata 兼容、HTTP 鉴权/限流/日志/反代适配、RPC 周期刷新/诊断、readiness、OpenAPI、管理控制白名单、Rust-owned `schedule.*` dashboard 代理、`translation.task.update` 状态回写代理和部署模板,后续远程服务器联调/可选持久化 |
|
||||||
| issue #19 | 资源面 MVP 与 USERGUIDE 基础章节已编码;真机联调后继续补充实战样例;**未自动关 issue** |
|
| issue #19 | 资源面 MVP 与 USERGUIDE 基础章节已编码;真机联调后继续补充实战样例;**未自动关 issue** |
|
||||||
| G-010 Web | 未开始 |
|
| G-010 Web | 未开始 |
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ use translation_query::{
|
|||||||
};
|
};
|
||||||
use workflow_commands::{
|
use workflow_commands::{
|
||||||
run_parse_clear_cache, run_parse_once, run_publish_localized, run_repack, run_translate_once,
|
run_parse_clear_cache, run_parse_once, run_publish_localized, run_repack, run_translate_once,
|
||||||
run_translation_set, run_translation_validate,
|
run_translation_set, run_translation_task_update, run_translation_validate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXIT_ERROR: i32 = 1;
|
const EXIT_ERROR: i32 = 1;
|
||||||
@@ -209,6 +209,10 @@ fn run() -> anyhow::Result<i32> {
|
|||||||
run_translation_set(&options)?;
|
run_translation_set(&options)?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
CliCommand::TranslationTaskUpdate => {
|
||||||
|
run_translation_task_update(&options)?;
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
CliCommand::Repack => {
|
CliCommand::Repack => {
|
||||||
run_repack(&options)?;
|
run_repack(&options)?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
@@ -356,6 +360,8 @@ struct CliOptions {
|
|||||||
translation_id: Option<String>,
|
translation_id: Option<String>,
|
||||||
translation_text: Option<String>,
|
translation_text: Option<String>,
|
||||||
translation_text_file: Option<PathBuf>,
|
translation_text_file: Option<PathBuf>,
|
||||||
|
translation_failure_reason: Option<String>,
|
||||||
|
translation_provider_run_id: Option<String>,
|
||||||
localized_release_id: Option<String>,
|
localized_release_id: Option<String>,
|
||||||
repack_spec: Option<PathBuf>,
|
repack_spec: Option<PathBuf>,
|
||||||
schedule_group: Option<String>,
|
schedule_group: Option<String>,
|
||||||
@@ -439,6 +445,8 @@ impl Default for CliOptions {
|
|||||||
translation_id: None,
|
translation_id: None,
|
||||||
translation_text: None,
|
translation_text: None,
|
||||||
translation_text_file: None,
|
translation_text_file: None,
|
||||||
|
translation_failure_reason: None,
|
||||||
|
translation_provider_run_id: None,
|
||||||
localized_release_id: None,
|
localized_release_id: None,
|
||||||
repack_spec: None,
|
repack_spec: None,
|
||||||
schedule_group: None,
|
schedule_group: None,
|
||||||
@@ -519,6 +527,7 @@ enum CliCommand {
|
|||||||
Translate,
|
Translate,
|
||||||
TranslationValidate,
|
TranslationValidate,
|
||||||
TranslationSet,
|
TranslationSet,
|
||||||
|
TranslationTaskUpdate,
|
||||||
Repack,
|
Repack,
|
||||||
PublishLocalized,
|
PublishLocalized,
|
||||||
ScheduleList,
|
ScheduleList,
|
||||||
@@ -6621,6 +6630,12 @@ fn parse_args_with_env(
|
|||||||
options.translation_text_file =
|
options.translation_text_file =
|
||||||
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
|
Some(PathBuf::from(next_option_value(&mut args, &flag)?));
|
||||||
}
|
}
|
||||||
|
"--failure-reason" | "--reason" => {
|
||||||
|
options.translation_failure_reason = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
}
|
||||||
|
"--provider-run-id" => {
|
||||||
|
options.translation_provider_run_id = Some(next_option_value(&mut args, &flag)?);
|
||||||
|
}
|
||||||
"--localized-release-id" => {
|
"--localized-release-id" => {
|
||||||
options.localized_release_id = Some(next_option_value(&mut args, &flag)?);
|
options.localized_release_id = Some(next_option_value(&mut args, &flag)?);
|
||||||
}
|
}
|
||||||
@@ -7202,6 +7217,60 @@ fn parse_args_with_env(
|
|||||||
options.progress = false;
|
options.progress = false;
|
||||||
options.banner = false;
|
options.banner = false;
|
||||||
}
|
}
|
||||||
|
CliCommand::TranslationTaskUpdate => {
|
||||||
|
if options.watch || options.daemon || options.daemon_child {
|
||||||
|
return Err(anyhow::anyhow!("i18n task update 只支持单次执行"));
|
||||||
|
}
|
||||||
|
if options.config.force
|
||||||
|
|| options.config.dry_run
|
||||||
|
|| options.run_count.is_some()
|
||||||
|
|| options.sync_option_explicit
|
||||||
|
|| options.output_explicit
|
||||||
|
|| options.proxy_option_explicit
|
||||||
|
|| tools_are_non_default(&options.config, &options.env_baseline_config)
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"i18n task update 只接受 --state-dir、--json/--human 和翻译任务参数"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if options.resource_root.is_some()
|
||||||
|
|| options.translation_file.is_some()
|
||||||
|
|| options.translation_id.is_some()
|
||||||
|
|| options.translation_text.is_some()
|
||||||
|
|| options.translation_text_file.is_some()
|
||||||
|
|| options.localized_release_id.is_some()
|
||||||
|
|| options.repack_spec.is_some()
|
||||||
|
|| options.query_offset != 0
|
||||||
|
|| options.query_limit != 100
|
||||||
|
|| options.query_resource_type.is_some()
|
||||||
|
|| options.query_hash.is_some()
|
||||||
|
|| options.query_path_pattern.is_some()
|
||||||
|
|| options.query_official_release_id.is_some()
|
||||||
|
|| options.query_platform.is_some()
|
||||||
|
|| options.query_destination.is_some()
|
||||||
|
|| options.query_bundle_path.is_some()
|
||||||
|
|| options.query_archive_entry.is_some()
|
||||||
|
|| options.query_worker_status.is_some()
|
||||||
|
|| options.query_parse_status.is_some()
|
||||||
|
|| options.query_path_id.is_some()
|
||||||
|
|| options.query_class_id.is_some()
|
||||||
|
|| options.query_field_path.is_some()
|
||||||
|
|| options.query_format.is_some()
|
||||||
|
|| options.query_has_reason.is_some()
|
||||||
|
|| options.query_has_failure_reason.is_some()
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"i18n task update 只接受 --task-id、--task-status、--failure-reason 和 --provider-run-id"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if options.query_task_id.is_none() || options.query_task_status.is_none() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"i18n task update 必须同时指定 --task-id 和 --task-status"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
options.progress = false;
|
||||||
|
options.banner = false;
|
||||||
|
}
|
||||||
CliCommand::ScheduleList
|
CliCommand::ScheduleList
|
||||||
| CliCommand::ScheduleAdd
|
| CliCommand::ScheduleAdd
|
||||||
| CliCommand::ScheduleUpdate
|
| CliCommand::ScheduleUpdate
|
||||||
@@ -7389,6 +7458,9 @@ fn parse_translation_command(
|
|||||||
options: &mut CliOptions,
|
options: &mut CliOptions,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let action = next_option_value(args, "translation")?;
|
let action = next_option_value(args, "translation")?;
|
||||||
|
if action == "task" {
|
||||||
|
return parse_translation_task_command(args, options);
|
||||||
|
}
|
||||||
let command = match action.as_str() {
|
let command = match action.as_str() {
|
||||||
"run" => CliCommand::Translate,
|
"run" => CliCommand::Translate,
|
||||||
"export" => CliCommand::Translate,
|
"export" => CliCommand::Translate,
|
||||||
@@ -7409,6 +7481,21 @@ fn parse_translation_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_translation_task_command(
|
||||||
|
args: &mut impl Iterator<Item = String>,
|
||||||
|
options: &mut CliOptions,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let action = next_option_value(args, "translation task")?;
|
||||||
|
let command = match action.as_str() {
|
||||||
|
"update" => CliCommand::TranslationTaskUpdate,
|
||||||
|
"list" | "status" => CliCommand::TranslationTasks,
|
||||||
|
other => return Err(anyhow::anyhow!("未知 translation task 二级命令:{other}")),
|
||||||
|
};
|
||||||
|
ensure_command_not_set(options.command, &format!("translation task {action}"))?;
|
||||||
|
options.command = command;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_schedule_command(
|
fn parse_schedule_command(
|
||||||
args: &mut impl Iterator<Item = String>,
|
args: &mut impl Iterator<Item = String>,
|
||||||
options: &mut CliOptions,
|
options: &mut CliOptions,
|
||||||
@@ -7520,6 +7607,7 @@ fn print_usage(binary: &str) {
|
|||||||
eprintln!(" i18n export Export an editable translation workbench");
|
eprintln!(" i18n export Export an editable translation workbench");
|
||||||
eprintln!(" i18n set Update one translation workbench entry");
|
eprintln!(" i18n set Update one translation workbench entry");
|
||||||
eprintln!(" i18n validate Validate workbench against the current official release");
|
eprintln!(" i18n validate Validate workbench against the current official release");
|
||||||
|
eprintln!(" i18n task update Update one provider worker task status");
|
||||||
eprintln!(" i18n publish Publish a localized release");
|
eprintln!(" i18n publish Publish a localized release");
|
||||||
eprintln!(" i18n schedule Manage translation schedules");
|
eprintln!(" i18n schedule Manage translation schedules");
|
||||||
eprintln!(" refresh Run one update check, or ask a live daemon to refresh");
|
eprintln!(" refresh Run one update check, or ask a live daemon to refresh");
|
||||||
@@ -7602,6 +7690,8 @@ fn print_usage(binary: &str) {
|
|||||||
eprintln!(" --translation-id <ID> TextUnit ID for i18n set");
|
eprintln!(" --translation-id <ID> TextUnit ID for i18n set");
|
||||||
eprintln!(" --translated-text <TEXT> Inline translation for i18n set");
|
eprintln!(" --translated-text <TEXT> Inline translation for i18n set");
|
||||||
eprintln!(" --translated-file <PATH> UTF-8 translation file for i18n set");
|
eprintln!(" --translated-file <PATH> UTF-8 translation file for i18n set");
|
||||||
|
eprintln!(" --failure-reason <TEXT> Provider failure reason for i18n task update");
|
||||||
|
eprintln!(" --provider-run-id <ID> Provider run ID for i18n task update");
|
||||||
eprintln!(" --localized-release-id <ID> Explicit localized publication ID");
|
eprintln!(" --localized-release-id <ID> Explicit localized publication ID");
|
||||||
eprintln!(" --repack-spec <PATH> UnityFS batch repack JSON spec");
|
eprintln!(" --repack-spec <PATH> UnityFS batch repack JSON spec");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|||||||
@@ -228,6 +228,65 @@ fn grouped_workflow_commands_use_short_top_level_aliases() {
|
|||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(options.command, CliCommand::TranslationValidate);
|
assert_eq!(options.command, CliCommand::TranslationValidate);
|
||||||
|
|
||||||
|
let options = parse(&[
|
||||||
|
"bat",
|
||||||
|
"i18n",
|
||||||
|
"task",
|
||||||
|
"update",
|
||||||
|
"--state-dir",
|
||||||
|
"/tmp/bat-state",
|
||||||
|
"--task-id",
|
||||||
|
"textunit/v-current/TextAssets/Scenario.json",
|
||||||
|
"--task-status",
|
||||||
|
"failed",
|
||||||
|
"--failure-reason",
|
||||||
|
"provider rejected payload",
|
||||||
|
"--provider-run-id",
|
||||||
|
"provider-run-1",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(options.command, CliCommand::TranslationTaskUpdate);
|
||||||
|
assert_eq!(
|
||||||
|
options.query_task_id.as_deref(),
|
||||||
|
Some("textunit/v-current/TextAssets/Scenario.json")
|
||||||
|
);
|
||||||
|
assert_eq!(options.query_task_status.as_deref(), Some("failed"));
|
||||||
|
assert_eq!(
|
||||||
|
options.translation_failure_reason.as_deref(),
|
||||||
|
Some("provider rejected payload")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.translation_provider_run_id.as_deref(),
|
||||||
|
Some("provider-run-1")
|
||||||
|
);
|
||||||
|
assert!(parse(&["bat", "i18n", "task", "update", "--task-id", "task-only",]).is_err());
|
||||||
|
assert!(parse(&[
|
||||||
|
"bat",
|
||||||
|
"i18n",
|
||||||
|
"task",
|
||||||
|
"update",
|
||||||
|
"--task-id",
|
||||||
|
"task",
|
||||||
|
"--task-status",
|
||||||
|
"running",
|
||||||
|
"--worker-status",
|
||||||
|
"failed",
|
||||||
|
])
|
||||||
|
.is_err());
|
||||||
|
assert!(parse(&[
|
||||||
|
"bat",
|
||||||
|
"i18n",
|
||||||
|
"task",
|
||||||
|
"update",
|
||||||
|
"--task-id",
|
||||||
|
"task",
|
||||||
|
"--task-status",
|
||||||
|
"running",
|
||||||
|
"--translation-file",
|
||||||
|
"/tmp/workbench.json",
|
||||||
|
])
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -156,6 +156,35 @@ pub(super) fn run_translation_set(options: &CliOptions) -> anyhow::Result<()> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn run_translation_task_update(options: &CliOptions) -> anyhow::Result<()> {
|
||||||
|
let task_id = options
|
||||||
|
.query_task_id
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("i18n task update 必须指定 --task-id"))?;
|
||||||
|
let status = options
|
||||||
|
.query_task_status
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("i18n task update 必须指定 --task-status"))?;
|
||||||
|
let mut params = serde_json::Map::from_iter([
|
||||||
|
("task_id".to_string(), serde_json::json!(task_id)),
|
||||||
|
("status".to_string(), serde_json::json!(status)),
|
||||||
|
]);
|
||||||
|
if let Some(reason) = options.translation_failure_reason.as_deref() {
|
||||||
|
params.insert("failure_reason".to_string(), serde_json::json!(reason));
|
||||||
|
}
|
||||||
|
if let Some(provider_run_id) = options.translation_provider_run_id.as_deref() {
|
||||||
|
params.insert(
|
||||||
|
"provider_run_id".to_string(),
|
||||||
|
serde_json::json!(provider_run_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let report = update_translation_task_status_report(
|
||||||
|
&options.state_dir,
|
||||||
|
Some(&serde_json::Value::Object(params)),
|
||||||
|
)?;
|
||||||
|
print_json_value(options.output_format, &report)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn run_repack(options: &CliOptions) -> anyhow::Result<()> {
|
pub(super) fn run_repack(options: &CliOptions) -> anyhow::Result<()> {
|
||||||
let spec = options
|
let spec = options
|
||||||
.repack_spec
|
.repack_spec
|
||||||
|
|||||||
+43
-2
@@ -49,6 +49,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/control/schedule-update",
|
"/admin/control/schedule-update",
|
||||||
"/admin/control/schedule-remove",
|
"/admin/control/schedule-remove",
|
||||||
"/admin/control/schedule-run",
|
"/admin/control/schedule-run",
|
||||||
|
"/admin/control/translation-task-update",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if r.Method == http.MethodHead {
|
if r.Method == http.MethodHead {
|
||||||
@@ -76,6 +77,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleAdminScheduleControl(w, r, action)
|
s.handleAdminScheduleControl(w, r, action)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if action == "translation-task-update" {
|
||||||
|
s.handleAdminTranslationTaskUpdate(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
request, ok := decodeAdminControlRequest(w, r)
|
request, ok := decodeAdminControlRequest(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -149,6 +154,34 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAdminTranslationTaskUpdate(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
|
||||||
|
}
|
||||||
|
var params backendrpc.TranslationTaskUpdateParams
|
||||||
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(params.TaskID) == "" || strings.TrimSpace(params.Status) == "" {
|
||||||
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_params", "translation task update requires task_id and status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := backend.TranslationTaskUpdate(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.writeControlBackendError(w, "translation-task-update", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||||
|
Service: "bat-api",
|
||||||
|
Action: "translation-task-update",
|
||||||
|
RPCMethod: "translation.task.update",
|
||||||
|
Status: "accepted",
|
||||||
|
Result: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||||
@@ -263,6 +296,14 @@ func (s *Server) requireAdminToken(w http.ResponseWriter, r *http.Request) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
||||||
|
return decodeAdminJSON(w, r, target, "invalid_schedule_params", "schedule request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAdminTranslationJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
||||||
|
return decodeAdminJSON(w, r, target, "invalid_translation_params", "translation task request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAdminJSON(w http.ResponseWriter, r *http.Request, target any, errorCode string, subject string) bool {
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -272,11 +313,11 @@ func decodeAdminScheduleJSON(w http.ResponseWriter, r *http.Request, target any)
|
|||||||
if errors.Is(err, io.EOF) {
|
if errors.Is(err, io.EOF) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_schedule_params", "schedule request must be a JSON object")
|
writeErrorJSON(w, http.StatusBadRequest, errorCode, subject+" must be a JSON object")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_schedule_params", "schedule request must contain exactly one JSON object")
|
writeErrorJSON(w, http.StatusBadRequest, errorCode, subject+" must contain exactly one JSON object")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -588,6 +588,11 @@ func (b *controlBackend) CatalogRefresh(ctx context.Context, force bool) (*backe
|
|||||||
return &backendrpc.TaskAccepted{TaskID: "task-catalog-refresh-1", Kind: "catalog.refresh"}, nil
|
return &backendrpc.TaskAccepted{TaskID: "task-catalog-refresh-1", Kind: "catalog.refresh"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *controlBackend) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) {
|
||||||
|
b.calls = append(b.calls, "translation.task.update")
|
||||||
|
return json.RawMessage(`{"task_status":"` + params.Status + `"}`), nil
|
||||||
|
}
|
||||||
|
|
||||||
type scheduleBackend struct {
|
type scheduleBackend struct {
|
||||||
*controlBackend
|
*controlBackend
|
||||||
scheduleCalls []string
|
scheduleCalls []string
|
||||||
@@ -1150,6 +1155,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
{name: "force sync", action: "sync", body: `{"force":true}`, rpcMethod: "resource.sync", call: "resource.sync"},
|
{name: "force sync", action: "sync", body: `{"force":true}`, rpcMethod: "resource.sync", call: "resource.sync"},
|
||||||
{name: "repair", action: "repair", rpcMethod: "resource.repair", call: "resource.repair"},
|
{name: "repair", action: "repair", rpcMethod: "resource.repair", call: "resource.repair"},
|
||||||
{name: "catalog refresh", action: "catalog-refresh", rpcMethod: "catalog.refresh", call: "catalog.refresh"},
|
{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"},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -1172,6 +1178,14 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/admin/control/translation-task-update", strings.NewReader(`{"task_id":""}`))
|
||||||
|
request.Header.Set("Authorization", "Bearer control-token")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("invalid translation update status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminControlRejectsUnauthenticatedDangerousAndUnsupportedActions(t *testing.T) {
|
func TestAdminControlRejectsUnauthenticatedDangerousAndUnsupportedActions(t *testing.T) {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run]
|
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, translation-task-update]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
content:
|
content:
|
||||||
@@ -191,6 +191,14 @@ paths:
|
|||||||
type: boolean
|
type: boolean
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
task_id:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
failure_reason:
|
||||||
|
type: string
|
||||||
|
provider_run_id:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"202":
|
"202":
|
||||||
description: Rust bat accepted the control request.
|
description: Rust bat accepted the control request.
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ type ScheduleBackend interface {
|
|||||||
ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error)
|
ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TranslationBackend exposes the narrow provider-worker state mutation used
|
||||||
|
// by the dashboard. It does not create arbitrary translation jobs.
|
||||||
|
type TranslationBackend interface {
|
||||||
|
TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error)
|
||||||
|
}
|
||||||
|
|
||||||
// RPCClient adapts *backendrpc.Client to Backend.
|
// RPCClient adapts *backendrpc.Client to Backend.
|
||||||
type RPCClient struct {
|
type RPCClient struct {
|
||||||
Client *backendrpc.Client
|
Client *backendrpc.Client
|
||||||
@@ -105,6 +111,9 @@ func (r RPCClient) ScheduleRemove(ctx context.Context, params backendrpc.Schedul
|
|||||||
func (r RPCClient) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error) {
|
func (r RPCClient) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error) {
|
||||||
return r.Client.ScheduleRun(ctx, params)
|
return r.Client.ScheduleRun(ctx, params)
|
||||||
}
|
}
|
||||||
|
func (r RPCClient) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) {
|
||||||
|
return r.Client.TranslationTaskUpdate(ctx, params)
|
||||||
|
}
|
||||||
func (r RPCClient) ParseStatus(ctx context.Context) (json.RawMessage, error) {
|
func (r RPCClient) ParseStatus(ctx context.Context) (json.RawMessage, error) {
|
||||||
return r.Client.ParseStatus(ctx)
|
return r.Client.ParseStatus(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,6 +264,15 @@ type ScheduleRunParams struct {
|
|||||||
MaxRuns *uint64 `json:"max_runs,omitempty"`
|
MaxRuns *uint64 `json:"max_runs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TranslationTaskUpdateParams is used by translation.task.update to persist
|
||||||
|
// provider worker state for one task in the current official release.
|
||||||
|
type TranslationTaskUpdateParams struct {
|
||||||
|
TaskID string `json:"task_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
FailureReason string `json:"failure_reason,omitempty"`
|
||||||
|
ProviderRunID string `json:"provider_run_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Ack is returned by accepted daemon control methods.
|
// Ack is returned by accepted daemon control methods.
|
||||||
type Ack struct {
|
type Ack struct {
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
@@ -483,6 +492,10 @@ func (c *Client) ScheduleRun(ctx context.Context, params ScheduleRunParams) (jso
|
|||||||
return c.rawData(ctx, "schedule.run", params)
|
return c.rawData(ctx, "schedule.run", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) TranslationTaskUpdate(ctx context.Context, params TranslationTaskUpdateParams) (json.RawMessage, error) {
|
||||||
|
return c.rawData(ctx, "translation.task.update", params)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
func (c *Client) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||||
return c.rawData(ctx, "catalog.status", nil)
|
return c.rawData(ctx, "catalog.status", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,45 @@ func TestScheduleRunSendsScopeAndMaxRuns(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTranslationTaskUpdateSendsWorkerParams(t *testing.T) {
|
||||||
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
|
if req.Method != "translation.task.update" {
|
||||||
|
t.Fatalf("method = %s", req.Method)
|
||||||
|
}
|
||||||
|
var params TranslationTaskUpdateParams
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
t.Fatalf("decode params: %v", err)
|
||||||
|
}
|
||||||
|
if params.TaskID != "textunit/v-current/Scenario" ||
|
||||||
|
params.Status != "failed" ||
|
||||||
|
params.FailureReason != "provider rejected payload" ||
|
||||||
|
params.ProviderRunID != "provider-run-1" {
|
||||||
|
t.Fatalf("params = %#v", params)
|
||||||
|
}
|
||||||
|
return testResponse{
|
||||||
|
Result: testEnvelope{
|
||||||
|
OK: true,
|
||||||
|
Status: "ok",
|
||||||
|
RequestID: "req-test-translation-update",
|
||||||
|
Data: map[string]any{"task_status": "failed"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
raw, err := client.TranslationTaskUpdate(context.Background(), TranslationTaskUpdateParams{
|
||||||
|
TaskID: "textunit/v-current/Scenario",
|
||||||
|
Status: "failed",
|
||||||
|
FailureReason: "provider rejected payload",
|
||||||
|
ProviderRunID: "provider-run-1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TranslationTaskUpdate error: %v", err)
|
||||||
|
}
|
||||||
|
if !json.Valid(raw) {
|
||||||
|
t.Fatalf("invalid raw JSON: %s", string(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplicationErrorReturnsAPIError(t *testing.T) {
|
func TestApplicationErrorReturnsAPIError(t *testing.T) {
|
||||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
if req.Method != "task.status" {
|
if req.Method != "task.status" {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ OpenAPI. `GET /admin/schedules` and mutating
|
|||||||
`/admin/control/schedule-{add,update,remove,run}` routes are protected by the
|
`/admin/control/schedule-{add,update,remove,run}` routes are protected by the
|
||||||
configured admin token and forward the Rust-owned schedule contract.
|
configured admin token and forward the Rust-owned schedule contract.
|
||||||
`GET /admin/schedules` accepts optional `id`, `group`, and `enabled` filters.
|
`GET /admin/schedules` accepts optional `id`, `group`, and `enabled` filters.
|
||||||
|
`POST /admin/control/translation-task-update` uses the same token and forwards
|
||||||
|
the narrow `translation.task.update` provider-worker status contract.
|
||||||
|
|
||||||
The production panel must reuse the same HTTP authentication, rate limiting,
|
The production panel must reuse the same HTTP authentication, rate limiting,
|
||||||
access logging, reverse-proxy handling, and no-store dynamic response policy as
|
access logging, reverse-proxy handling, and no-store dynamic response policy as
|
||||||
|
|||||||
Reference in New Issue
Block a user