mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 11:56:23 +08:00
fix(tm):建立 Trusted 唯一性与 Supersede 治理
This commit is contained in:
@@ -1,268 +0,0 @@
|
|||||||
# Gitea Actions workflow for the Rust workspace.
|
|
||||||
# Self-hosted runner friendly.
|
|
||||||
# Does not use external GitHub Actions.
|
|
||||||
|
|
||||||
name: bat-rust
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rust:
|
|
||||||
name: Build and test Rust
|
|
||||||
runs-on: linux
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUST_BACKTRACE: 1
|
|
||||||
BAT_SKIP_ENV_FILE: "1"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
|
|
||||||
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
|
||||||
: "${GITHUB_SHA:?GITHUB_SHA is required}"
|
|
||||||
|
|
||||||
repo_url="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}.git"
|
|
||||||
|
|
||||||
if [ -d .git ]; then
|
|
||||||
git remote set-url origin "${repo_url}"
|
|
||||||
else
|
|
||||||
git init .
|
|
||||||
git remote add origin "${repo_url}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ref="${GITHUB_REF:-${GITHUB_SHA}}"
|
|
||||||
|
|
||||||
git fetch \
|
|
||||||
--no-tags \
|
|
||||||
--depth=1 \
|
|
||||||
origin \
|
|
||||||
"${ref}" \
|
|
||||||
|| git fetch \
|
|
||||||
--no-tags \
|
|
||||||
--depth=1 \
|
|
||||||
origin \
|
|
||||||
"${GITHUB_SHA}"
|
|
||||||
|
|
||||||
git checkout --force --detach FETCH_HEAD
|
|
||||||
|
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
|
|
||||||
- name: Show tool versions
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
|
|
||||||
command -v git
|
|
||||||
command -v rustc
|
|
||||||
command -v cargo
|
|
||||||
|
|
||||||
rustc --version
|
|
||||||
cargo --version
|
|
||||||
rustfmt --version
|
|
||||||
cargo clippy --version
|
|
||||||
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
cargo fmt --all -- --check
|
|
||||||
|
|
||||||
|
|
||||||
- name: Check workspace
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
cargo check --workspace --locked
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build workspace
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
|
|
||||||
cargo build \
|
|
||||||
--workspace \
|
|
||||||
--release \
|
|
||||||
--locked
|
|
||||||
|
|
||||||
|
|
||||||
- name: Run clippy
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
|
|
||||||
cargo clippy \
|
|
||||||
--workspace \
|
|
||||||
--all-targets \
|
|
||||||
--locked \
|
|
||||||
-- -D warnings
|
|
||||||
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
|
|
||||||
cargo test \
|
|
||||||
--workspace \
|
|
||||||
--locked
|
|
||||||
|
|
||||||
|
|
||||||
- name: Package binary
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
mkdir -p artifact
|
|
||||||
|
|
||||||
cp target/release/bat artifact/
|
|
||||||
|
|
||||||
tar \
|
|
||||||
-czf \
|
|
||||||
bat-linux-x86_64.tar.gz \
|
|
||||||
-C artifact \
|
|
||||||
bat
|
|
||||||
|
|
||||||
sha256sum \
|
|
||||||
bat-linux-x86_64.tar.gz \
|
|
||||||
> bat-linux-x86_64.sha256
|
|
||||||
|
|
||||||
|
|
||||||
- name: Prepare artifact
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
mkdir upload
|
|
||||||
|
|
||||||
cp bat-linux-x86_64.tar.gz upload/
|
|
||||||
cp bat-linux-x86_64.sha256 upload/
|
|
||||||
|
|
||||||
cd upload
|
|
||||||
|
|
||||||
zip -q \
|
|
||||||
../bat-linux-x86_64.zip \
|
|
||||||
*
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
ls -lh bat-linux-x86_64.zip
|
|
||||||
|
|
||||||
go-api:
|
|
||||||
name: Build and test Go API
|
|
||||||
runs-on: linux
|
|
||||||
|
|
||||||
env:
|
|
||||||
GOCACHE: /tmp/bat-go-cache
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
|
|
||||||
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
|
||||||
: "${GITHUB_SHA:?GITHUB_SHA is required}"
|
|
||||||
|
|
||||||
repo_url="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}.git"
|
|
||||||
|
|
||||||
if [ -d .git ]; then
|
|
||||||
git remote set-url origin "${repo_url}"
|
|
||||||
else
|
|
||||||
git init .
|
|
||||||
git remote add origin "${repo_url}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ref="${GITHUB_REF:-${GITHUB_SHA}}"
|
|
||||||
|
|
||||||
git fetch \
|
|
||||||
--no-tags \
|
|
||||||
--depth=1 \
|
|
||||||
origin \
|
|
||||||
"${ref}" \
|
|
||||||
|| git fetch \
|
|
||||||
--no-tags \
|
|
||||||
--depth=1 \
|
|
||||||
origin \
|
|
||||||
"${GITHUB_SHA}"
|
|
||||||
|
|
||||||
git checkout --force --detach FETCH_HEAD
|
|
||||||
|
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
- name: Show Go tool version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
|
|
||||||
command -v go
|
|
||||||
go version
|
|
||||||
|
|
||||||
- name: Run Go API tests
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
make test-go-api
|
|
||||||
|
|
||||||
- name: Check Go formatting
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
make check-go-format
|
|
||||||
|
|
||||||
- name: Run Go API vet
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
go vet ./internal/api/... ./internal/backendrpc/... ./cmd/bat-api/...
|
|
||||||
|
|
||||||
- name: Build Go API
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
go build -o /tmp/bat-api ./cmd/bat-api
|
|
||||||
|
|
||||||
- name: Required Go lint
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source /var/lib/act_runner/env.sh
|
|
||||||
source scripts/ci-versions.sh
|
|
||||||
command -v golangci-lint >/dev/null 2>&1
|
|
||||||
actual="$(golangci_lint_actual_version)"
|
|
||||||
test "${actual}" = "${GOLANGCI_LINT_VERSION}"
|
|
||||||
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-/tmp/bat-xdg-cache}"
|
|
||||||
golangci-lint run ./...
|
|
||||||
|
|
||||||
- name: Run documentation status gate
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
make check-docs
|
|
||||||
+11
-8
@@ -32,13 +32,13 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
|
|
||||||
15. Rust `bat` 已提供 `res` / `parse` / `i18n` 工作流入口:支持资源拉取、解析刷新、可再生解析缓存清理、离线翻译工作台、人工文本查看/修改/清空、工作台发布前校验、generic manifest 驱动的 Binary/JSON/Text/受支持 UnityFS 汉化发布、人工校对状态标记、既有 patch 能力的批量重打包、单次/限定次数/周期执行和版本化 schedule CRUD。`translation.worker.run` 已接入 provider worker:默认并发 8、范围 `1..=256`,每个 worker 独立 claim 下一项任务并落库 lease、失败分类、重试计划和 TextUnit 译文结果。schedule 查询现在按一级工作流过滤,删除/执行会校验作用域,单轮执行可限制计划数;schedule CRUD、翻译任务查询/交接视图、翻译任务状态回写、provider worker 触发和 `translation.proofread` 状态标记已通过 `bat.sock` 的 RPC 以及 `bat-api` 的鉴权管理接口暴露,dashboard 不维护第二套状态。`bat-api` 已提供内嵌 dashboard MVP,静态资产由 Go embed 暴露在 `/admin/dashboard/`,页面直接调用已有鉴权接口控制资源、调度、翻译、任务、日志、parse TextUnit 查询和 localized 发布/回滚。该工作流只编排已有解析和 patch 能力,不扩大解析器覆盖;完整 AssetBundle 重打包和完整 Web 协作后台仍是后续工作。真实官方网络全量拉取 smoke 已固化,真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
15. Rust `bat` 已提供 `res` / `parse` / `i18n` 工作流入口:支持资源拉取、解析刷新、可再生解析缓存清理、离线翻译工作台、人工文本查看/修改/清空、工作台发布前校验、generic manifest 驱动的 Binary/JSON/Text/受支持 UnityFS 汉化发布、人工校对状态标记、既有 patch 能力的批量重打包、单次/限定次数/周期执行和版本化 schedule CRUD。`translation.worker.run` 已接入 provider worker:默认并发 8、范围 `1..=256`,每个 worker 独立 claim 下一项任务并落库 lease、失败分类、重试计划和 TextUnit 译文结果。schedule 查询现在按一级工作流过滤,删除/执行会校验作用域,单轮执行可限制计划数;schedule CRUD、翻译任务查询/交接视图、翻译任务状态回写、provider worker 触发和 `translation.proofread` 状态标记已通过 `bat.sock` 的 RPC 以及 `bat-api` 的鉴权管理接口暴露,dashboard 不维护第二套状态。`bat-api` 已提供内嵌 dashboard MVP,静态资产由 Go embed 暴露在 `/admin/dashboard/`,页面直接调用已有鉴权接口控制资源、调度、翻译、任务、日志、parse TextUnit 查询和 localized 发布/回滚。该工作流只编排已有解析和 patch 能力,不扩大解析器覆盖;完整 AssetBundle 重打包和完整 Web 协作后台仍是后续工作。真实官方网络全量拉取 smoke 已固化,真实大文件与运行报告默认在 `/tmp` 隔离目录,不纳入 Git。Go 细节见 `docs/reports/GO_STATUS.md`。
|
||||||
|
|
||||||
当前翻译交接还包括 `translation-tasks.sqlite` 和版本化 `translation-handoff.json`;跨 release 的 Translation Memory V1 位于 `<output>/translation-memory.sqlite`,不放在 `versions/<id>` 或 release task 库中;
|
当前翻译交接还包括 `translation-tasks.sqlite` 和版本化 `translation-handoff.json`;跨 release 的 Translation Memory SQLite persistence schema V2 位于 `<output>/translation-memory.sqlite`,不放在 `versions/<id>` 或 release task 库中;
|
||||||
`translation.tasks` 查询单项 worker 状态,`translation.handoff` 查询完整
|
`translation.tasks` 查询单项 worker 状态,`translation.handoff` 查询完整
|
||||||
job/unit/provider run 状态;`translation.memory.summary/query/confirm` 提供
|
job/unit/provider run 状态;`translation.memory.summary/query/confirm/conflicts/resolve_conflict`
|
||||||
Rust-owned TM 的摘要、source/context 查询和显式 trusted 确认,`bat-api` 仅作
|
提供 Rust-owned TM 的摘要、source/context 查询、显式 trusted 确认和冲突治理,
|
||||||
typed 管理转发。当前下载实现使用默认 8 个独立 worker,完成后动态领取
|
`bat-api` 仅作 typed 管理转发。当前下载实现使用默认 8 个独立 worker,完成后动态领取
|
||||||
任务,最终资源报告按 pull plan 顺序输出。
|
任务,最终资源报告按 pull plan 顺序输出。
|
||||||
项目级 Glossary V2 位于 `<output>/glossary.sqlite`,独立于 release task 和 TM;
|
项目级 Glossary 使用 Glossary domain/feature contract V1,由 SQLite persistence schema V2 承载,位于 `<output>/glossary.sqlite`,独立于 release task 和 TM;
|
||||||
Rust `bat` 持有 term/alias/recommended/allowed/category/priority、全局或
|
Rust `bat` 持有 term/alias/recommended/allowed/category/priority、全局或
|
||||||
TextUnit scope、source history 和 approved review。worker、TM 复用、人工 task
|
TextUnit scope、source history 和 approved review。worker、TM 复用、人工 task
|
||||||
结果和 workbench publish 都执行确定性 QA;blocking deviation 必须携带
|
结果和 workbench publish 都执行确定性 QA;blocking deviation 必须携带
|
||||||
@@ -48,8 +48,11 @@ TextUnit scope、source history 和 approved review。worker、TM 复用、人
|
|||||||
|
|
||||||
三个长期 SQLite owner 现在统一使用只读 schema preflight、精确 component
|
三个长期 SQLite owner 现在统一使用只读 schema preflight、精确 component
|
||||||
fingerprint 和 `BEGIN IMMEDIATE` writer transaction:Translation Tasks 从 V1
|
fingerprint 和 `BEGIN IMMEDIATE` writer transaction:Translation Tasks 从 V1
|
||||||
按显式 `v1 -> v2` step 迁移,当前版本为 V2;Translation Memory 当前版本为 V1,
|
按显式 `v1 -> v2` step 迁移,当前版本为 V2;Translation Memory persistence schema
|
||||||
Glossary 当前版本为 V2。Glossary V2 正式吸收历史上未升版本的
|
当前版本为 V2,Glossary domain/feature contract 为 V1、persistence schema 为 V2。
|
||||||
|
Translation Memory V2 正式建立
|
||||||
|
current Trusted 唯一性、显式 supersede、冲突只读诊断、resolve_conflict 和 audit event;
|
||||||
|
Glossary persistence schema V2 正式吸收历史上未升版本的
|
||||||
`glossary_term_deletions` schema drift:原始 V1-A 会在事务内创建 deletion audit
|
`glossary_term_deletions` schema drift:原始 V1-A 会在事务内创建 deletion audit
|
||||||
表,带 deletion audit 的 V1-B 只提升 bookkeeping version。future、未知或版本与结构
|
表,带 deletion audit 的 V1-B 只提升 bookkeeping version。future、未知或版本与结构
|
||||||
不一致的数据库在任何 schema/data mutation 前 fail closed;migration 失败会 rollback,
|
不一致的数据库在任何 schema/data mutation 前 fail closed;migration 失败会 rollback,
|
||||||
@@ -321,7 +324,7 @@ cargo run -p bat-infrastructure --bin bat -- \
|
|||||||
--watch
|
--watch
|
||||||
```
|
```
|
||||||
|
|
||||||
资源 HTTP bootstrap / 只读分发入口是 Go `cmd/bat-api`。生产拓扑下它与 Rust `bat` 同环境运行,经 `bat.sock` RPC 获取 Rust 当前 official `release.attestation`,再按 release/publication/mapping/manifest identity 和 verification generation 绑定读取 `resource.manifest`,不在配置里写死资源目录;轻量 attestation 只读取 current、canonical versioned root、publication anchor、manifest 元数据和 freshness,不遍历历史 release 或计算资源文件 BLAKE3。Rust watch 周期负责 current 本地 manifest 验证并更新 attestation,默认 freshness window 为 `2 * 3600 + 60 = 7260` 秒;HTTP readiness 还要求 Go 分页快照完整且本地路径安全;本地开发不能全量跑 `bat` 时用 fixture 和 Go 门禁验证。`internal/api/testdata/contract/` 已固化来自 Rust 输出并经归一化的 `catalog.status`、`resource.manifest`、`official-sync-snapshot.json` 和 Glossary query contract fixture,Go mirror 测试会防止字段名、null 语义和 provenance 再次漂移;TM/Glossary 另有 Rust/Go 字段镜像测试覆盖 match、trust、translated text、term history 和 source provenance。`bat-api` 已补 launcher 资源引导兼容端点、玩家-facing HTTP 控制面和鉴权调度/translation/TM/Glossary 管理接口(token 鉴权、限流、访问日志、反代 IP 适配、动态 JSON no-store、OpenAPI、管理控制白名单;`reload` / `refresh` / `restart` / `sync` / `verify` / `repair` / `catalog-refresh`、`schedule.*`、`task.*` 查询/取消、`daemon.logs`、`parse.*` 查询、`translation.tasks` / `translation.handoff` 查询、`translation.task.update`、`translation.worker.run`、`translation.proofread`、`translation.memory.summary/query/confirm`、`translation.glossary.*`、`localized.publish` 和 `localized.rollback` 可经 dashboard/API 转发),响应只来自已发布 snapshot/RPC,不提供官方账号登录、游戏网关协议或完整 package update manifest。
|
资源 HTTP bootstrap / 只读分发入口是 Go `cmd/bat-api`。生产拓扑下它与 Rust `bat` 同环境运行,经 `bat.sock` RPC 获取 Rust 当前 official `release.attestation`,再按 release/publication/mapping/manifest identity 和 verification generation 绑定读取 `resource.manifest`,不在配置里写死资源目录;轻量 attestation 只读取 current、canonical versioned root、publication anchor、manifest 元数据和 freshness,不遍历历史 release 或计算资源文件 BLAKE3。Rust watch 周期负责 current 本地 manifest 验证并更新 attestation,默认 freshness window 为 `2 * 3600 + 60 = 7260` 秒;HTTP readiness 还要求 Go 分页快照完整且本地路径安全;本地开发不能全量跑 `bat` 时用 fixture 和 Go 门禁验证。`internal/api/testdata/contract/` 已固化来自 Rust 输出并经归一化的 `catalog.status`、`resource.manifest`、`official-sync-snapshot.json` 和 Glossary query contract fixture,Go mirror 测试会防止字段名、null 语义和 provenance 再次漂移;TM/Glossary 另有 Rust/Go 字段镜像测试覆盖 match、trust、translated text、term history 和 source provenance。`bat-api` 已补 launcher 资源引导兼容端点、玩家-facing HTTP 控制面和鉴权调度/translation/TM/Glossary 管理接口(token 鉴权、限流、访问日志、反代 IP 适配、动态 JSON no-store、OpenAPI、管理控制白名单;`reload` / `refresh` / `restart` / `sync` / `verify` / `repair` / `catalog-refresh`、`schedule.*`、`task.*` 查询/取消、`daemon.logs`、`parse.*` 查询、`translation.tasks` / `translation.handoff` 查询、`translation.task.update`、`translation.worker.run`、`translation.proofread`、`translation.memory.summary/query/confirm/conflicts/resolve_conflict`、`translation.glossary.*`、`localized.publish` 和 `localized.rollback` 可经 dashboard/API 转发),响应只来自已发布 snapshot/RPC,不提供官方账号登录、游戏网关协议或完整 package update manifest。
|
||||||
|
|
||||||
生产要求:
|
生产要求:
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -46,7 +46,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
3. Go 侧边界已确定(见 `docs/reports/GO_STATUS.md`):同步/运维命令行 = Rust `bat`;资源分发和内嵌 dashboard = `cmd/bat-api` MVP;`internal/backendrpc` 完成;`cmd/bat` 仅为试验(`bin/bat-go`)。完整游戏业务 API / 完整 Web 协作后台 / SDK 仍未完成。
|
3. Go 侧边界已确定(见 `docs/reports/GO_STATUS.md`):同步/运维命令行 = Rust `bat`;资源分发和内嵌 dashboard = `cmd/bat-api` MVP;`internal/backendrpc` 完成;`cmd/bat` 仅为试验(`bin/bat-go`)。完整游戏业务 API / 完整 Web 协作后台 / SDK 仍未完成。
|
||||||
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
4. Addressables parser 已覆盖当前真实形态 fixture/golden,但还不是完整 Unity Addressables/SBP catalog 兼容层。
|
||||||
5. 官方同步结果可配置为发布后自动导入 CAS + ResourceRepository,并通过 `resource.index` RPC/CLI 查询;Resource metadata 已保存 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 摘要,资源级查询已覆盖 release、平台、destination、archive entry、parse status 和 TextUnit format;单条 TextUnit 明细和解析错误已持久化到 `official-textunit-index.json`,可通过 `parse.text_units` / `parse.errors` 查询;离线 TextUnit 翻译任务状态和跳过/失败原因可通过 `translation.tasks` 查询,`translation.task.update` 已提供 worker 状态回写 contract,`translation.worker.run` 已提供真实 provider worker 触发、lease/retry 和结果落库 contract,`translation.proofread` 已提供汉化 workflow 人工校对标记 contract,`translation.memory.*` 已提供 Rust-owned TM 摘要、raw source/context 查询、provenance 和显式 confirm contract,Go 侧仅代理。
|
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.worker.run` 已提供真实 provider worker 触发、lease/retry 和结果落库 contract,`translation.proofread` 已提供汉化 workflow 人工校对标记 contract,`translation.memory.*` 已提供 Rust-owned TM 摘要、raw source/context 查询、provenance 和显式 confirm contract,Go 侧仅代理。
|
||||||
6. 受支持汉化 Patch 发布已具备 UnityFS TextAsset、TypeTree string field 和 managed-reference string field 的 manifest/apply/rollback/完整性校验和 `localized.status` 严格校验;ZIP 内 bundle 在 `archive_entry` 可验证时会重写外层 ZIP。真实 provider worker 与项目级 Translation Memory V1 已接入,翻译记忆到完整汉化文件集合的构建仍未完成。
|
6. 受支持汉化 Patch 发布已具备 UnityFS TextAsset、TypeTree string field 和 managed-reference string field 的 manifest/apply/rollback/完整性校验和 `localized.status` 严格校验;ZIP 内 bundle 在 `archive_entry` 可验证时会重写外层 ZIP。真实 provider worker 与项目级 Translation Memory persistence schema V2 已接入,翻译记忆到完整汉化文件集合的构建仍未完成。
|
||||||
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook;真实运行记录处于长期运行测试阶段,报告待后续提供。
|
7. 真实官方网络全量下载 smoke 已固化为可重复脚本和 runbook;真实运行记录处于长期运行测试阶段,报告待后续提供。
|
||||||
8. 内嵌 dashboard MVP 已实现;完整 Web 协作后台、数据库迁移、插件加载机制尚未实现;`bat-api` 资源 bootstrap/分发/OpenAPI/管理控制面已通过 `/openapi.yaml` 提供,完整游戏业务 API 的 OpenAPI 仍未完成。
|
8. 内嵌 dashboard MVP 已实现;完整 Web 协作后台、数据库迁移、插件加载机制尚未实现;`bat-api` 资源 bootstrap/分发/OpenAPI/管理控制面已通过 `/openapi.yaml` 提供,完整游戏业务 API 的 OpenAPI 仍未完成。
|
||||||
9. 原 Git 历史未恢复;当前仓库以新初始化基线为准。
|
9. 原 Git 历史未恢复;当前仓库以新初始化基线为准。
|
||||||
@@ -169,7 +169,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
**目标**:能够获取、解析和同步 Blue Archive 资源清单。
|
**目标**:能够获取、解析和同步 Blue Archive 资源清单。
|
||||||
|
|
||||||
**当前状态**:部分完成。Rust 官方日服资源同步链路已经具备正式 one-shot 和 `--watch` 常驻入口;`bat-api` 资源 bootstrap/分发入口已落地,CAS + ResourceRepository 导入、历史 release/CAS 复用和 Translation Memory V1 已可用,但完整解析覆盖、丰富查询扩展和真实线上 smoke 仍待完成。
|
**当前状态**:部分完成。Rust 官方日服资源同步链路已经具备正式 one-shot 和 `--watch` 常驻入口;`bat-api` 资源 bootstrap/分发入口已落地,CAS + ResourceRepository 导入、历史 release/CAS 复用和 Translation Memory persistence schema V2 已可用,但完整解析覆盖、丰富查询扩展和真实线上 smoke 仍待完成。
|
||||||
|
|
||||||
交付物:
|
交付物:
|
||||||
|
|
||||||
@@ -260,10 +260,10 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
交付物:
|
交付物:
|
||||||
|
|
||||||
1. Translation Memory V1 已使用项目级 SQLite schema:source raw/hash、translation、完整 context、candidate/trusted 和 provenance。
|
1. Translation Memory persistence schema V2 已使用项目级 SQLite schema:source raw/hash、translation、完整 context、candidate/trusted、provenance、supersede 关系和 audit event。
|
||||||
2. 已实现 raw source + 完整 context exact match;模糊匹配和完整导入导出仍待实现。Glossary V2 已作为独立项目级 SQLite 资产接入 approved review、scope/alias/priority、provider constraints、确定性 QA 和显式 override。
|
2. 已实现 raw source + 完整 context exact match、current Trusted 唯一性和冲突诊断;模糊匹配和完整导入导出仍待实现。Glossary domain/feature contract V1 已由 SQLite persistence schema V2 承载,接入 approved review、scope/alias/priority、provider constraints、确定性 QA 和显式 override。
|
||||||
3. 已实现显式 per-record confirm;Glossary V2 已实现术语优先级、别名、分类、冲突检测和审核历史,批量审核与完整导入导出仍待实现。
|
3. 已实现显式 per-record confirm、supersede 和历史冲突 resolve;Glossary 已实现术语优先级、别名、分类、冲突检测和审核历史,批量审核与完整导入导出仍待实现。
|
||||||
4. 已实现 `bat i18n memory summary|query|confirm` 与对应 Rust RPC。
|
4. 已实现 `bat i18n memory summary|query|confirm|conflicts|resolve-conflict` 与对应 Rust RPC。
|
||||||
|
|
||||||
验收标准:
|
验收标准:
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
交付物:
|
交付物:
|
||||||
|
|
||||||
1. 发布验证:format、lint、test、build、security audit、release artifact 由本地可重复命令、自托管 Gitea linux-runner workflow 与脚本承担;当前不引入托管 CI。
|
1. 发布验证:format、lint、test、build、security audit、release artifact 由本地可重复命令和脚本承担;项目以本地 `make ci-check` 作为唯一完整 required quality gate,当前不依赖 Gitea、GitHub Actions 或其它远端 CI runner。
|
||||||
2. Docker Compose:本地开发、服务端部署。
|
2. Docker Compose:本地开发、服务端部署。
|
||||||
3. 数据备份与恢复文档。
|
3. 数据备份与恢复文档。
|
||||||
4. 用户文档、开发文档、故障排查文档。
|
4. 用户文档、开发文档、故障排查文档。
|
||||||
@@ -462,7 +462,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
|
|
||||||
按最终目标计算,当前总体完成度不固定写单一百分比,以模块状态、源码、测试和契约为准。
|
按最终目标计算,当前总体完成度不固定写单一百分比,以模块状态、源码、测试和契约为准。
|
||||||
|
|
||||||
已完成的是稳定基线、架构骨架、部分接口、CAS V1、Rust 官方资源同步闭环、可配置 CAS/ResourceRepository 导入、TextUnit 明细索引/查询、增量 Crowdin 离线队列、provider worker、Translation Memory V1、Glossary V2、通用 Binary/JSON/Text Patch 基础、generic manifest V1、已验证结构的 AssetBundle 变长重建、受支持 localized patch 发布/rollback、Rust-owned 双 release 查询/分发/cleanup V1,以及 Go `bat-api` 资源分发、内嵌 dashboard 和同机 live 联调。下一阶段的关键是 TM/Glossary 扩展、真实版本与复杂 AssetBundle 兼容和官方资源长期运行报告。
|
已完成的是稳定基线、架构骨架、部分接口、CAS V1、Rust 官方资源同步闭环、可配置 CAS/ResourceRepository 导入、TextUnit 明细索引/查询、增量 Crowdin 离线队列、provider worker、Translation Memory persistence schema V2、Glossary domain/feature contract V1(SQLite persistence schema V2)、通用 Binary/JSON/Text Patch 基础、generic manifest V1、已验证结构的 AssetBundle 变长重建、受支持 localized patch 发布/rollback、Rust-owned 双 release 查询/分发/cleanup V1,以及 Go `bat-api` 资源分发、内嵌 dashboard 和同机 live 联调。下一阶段的关键是 TM/Glossary 扩展、真实版本与复杂 AssetBundle 兼容和官方资源长期运行报告。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
||||||
- 资源导入链路可配置为在官方 release 发布后写入 CAS + `ResourceRepository`,资源 metadata 会记录 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式,TextAsset/Table/Media 会按类型分类索引;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 分页查询索引,常用 metadata 过滤会下推到 SQLite;历史 release 复用会重新校验 size、BLAKE3 和 ZIP 结构,失败时按历史 release、CAS、网络顺序回退,CAS 引用记录在 `official-cas-reuse-references.json` 中;`bat doctor cas` 可只读诊断既有 CAS 目录、对象数、对象字节数和元数据库文件状态。
|
- 资源导入链路可配置为在官方 release 发布后写入 CAS + `ResourceRepository`,资源 metadata 会记录 release、平台、bundle path、parse status、TextAsset 名称和 TextUnit 数量/格式,TextAsset/Table/Media 会按类型分类索引;`resource.index` RPC/CLI 可按类型、hash、路径模式、官方 release ID、平台、destination、bundle path、archive entry、parse status 和 TextUnit format 分页查询索引,常用 metadata 过滤会下推到 SQLite;历史 release 复用会重新校验 size、BLAKE3 和 ZIP 结构,失败时按历史 release、CAS、网络顺序回退,CAS 引用记录在 `official-cas-reuse-references.json` 中;`bat doctor cas` 可只读诊断既有 CAS 目录、对象数、对象字节数和元数据库文件状态。
|
||||||
- 新 release 发布后会生成 `official-resource-changes.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json`、`crowdin-translation-handoff.json`、`crowdin-textunit-queue.json`、`translation-tasks.sqlite` 和 `translation-handoff.json`;其中 TextUnit/Crowdin 队列只使用 Added/Modified 资源,不调用 Crowdin 网络 API,离线 TextUnit 翻译任务可通过 `translation.tasks` / `translation.handoff` RPC 或 CLI 查询状态、跳过/失败原因和 provider run 交接。
|
- 新 release 发布后会生成 `official-resource-changes.json`、`official-parse-cache.json`、`official-textunit-index.json`、`official-textunit-tasks.json`、`crowdin-translation-handoff.json`、`crowdin-textunit-queue.json`、`translation-tasks.sqlite` 和 `translation-handoff.json`;其中 TextUnit/Crowdin 队列只使用 Added/Modified 资源,不调用 Crowdin 网络 API,离线 TextUnit 翻译任务可通过 `translation.tasks` / `translation.handoff` RPC 或 CLI 查询状态、跳过/失败原因和 provider run 交接。
|
||||||
- `translation.worker.run` 已提供 Rust `bat` 的 mock/Crowdin provider worker,支持 lease、失败重试、TextUnit 译文结果落库、Translation Memory V1 和 Glossary V2;Glossary 独立于 release task/TM,支持全局与 TextUnit scope、alias、priority、approved review、冲突诊断、provider constraints、deletion audit 和确定性 QA。TM 独立于 release task 库,支持 candidate/trusted、完整 context exact match、显式 confirm 和 provenance 查询。模糊匹配和完整 Provider 扩展体系仍待实现。
|
- `translation.worker.run` 已提供 Rust `bat` 的 mock/Crowdin provider worker,支持 lease、失败重试、TextUnit 译文结果落库、Translation Memory persistence schema V2 和 Glossary domain/feature contract V1(SQLite persistence schema V2);Glossary 独立于 release task/TM,支持全局与 TextUnit scope、alias、priority、approved review、冲突诊断、provider constraints、deletion audit 和确定性 QA。TM 独立于 release task 库,支持 candidate/trusted、完整 context exact match、显式 confirm、supersede、冲突诊断/解决和 provenance 查询。模糊匹配和完整 Provider 扩展体系仍待实现。
|
||||||
- `LocalizedPatchService` 已具备受支持的 UnityFS localized patch 发布/回滚能力:在 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 配置的独立汉化目录 staging 中复制官方 release、应用 TextAsset、TypeTree string field 或 managed-reference string field patch、写入带 TextUnit/provider/review/rollback trace 的 `localized-patch-manifest.json`,校验后发布到 `versions/<id>` 并切换 `current`,也可显式 rollback。
|
- `LocalizedPatchService` 已具备受支持的 UnityFS localized patch 发布/回滚能力:在 `--localized-output` / `BAT_LOCALIZED_OUTPUT` 配置的独立汉化目录 staging 中复制官方 release、应用 TextAsset、TypeTree string field 或 managed-reference string field patch、写入带 TextUnit/provider/review/rollback trace 的 `localized-patch-manifest.json`,校验后发布到 `versions/<id>` 并切换 `current`,也可显式 rollback。
|
||||||
- `bat-patch` 已具备通用 Patch 基础:确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,TypeTree 语义字段支持基础标量、固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、PPtr、managed-reference registry payload 字符串、object 字段组合、unknown fixed-size raw bytes 同长度替换和 TypeTree schema 支撑的 array/vector/map 整体替换;TextUnit 提取会把 managed-reference 类型信息保留为上下文而非翻译文本,受支持 localized 发布通过独立 manifest/staging/current 流程完成。
|
- `bat-patch` 已具备通用 Patch 基础:确定性 Binary hunk diff/apply、RFC 6902 JSON Patch apply、UTF-8 Text Patch、Patch manifest、BLAKE3/size 完整性校验和 rollback 元数据;文件级 `patch.apply` RPC / `patch-apply` CLI 与 UnityFS TextAsset / TypeTree string / TypeTree 语义字段写入入口已开放,TypeTree 语义字段支持基础标量、固定 Unity float/int/hash 值类型的 leaf/direct-child 形态、PPtr、managed-reference registry payload 字符串、object 字段组合、unknown fixed-size raw bytes 同长度替换和 TypeTree schema 支撑的 array/vector/map 整体替换;TextUnit 提取会把 managed-reference 类型信息保留为上下文而非翻译文本,受支持 localized 发布通过独立 manifest/staging/current 流程完成。
|
||||||
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
- `bat-api` 完整游戏业务 API / launcher 安装包更新全链仍未完成;资源 CDN、HTTP 控制面、launcher 资源引导兼容和内嵌 dashboard MVP 已可用。
|
- `bat-api` 完整游戏业务 API / launcher 安装包更新全链仍未完成;资源 CDN、HTTP 控制面、launcher 资源引导兼容和内嵌 dashboard MVP 已可用。
|
||||||
- 完整 AssetBundle 对象级解析(UnityFS 解包、object table、TypeTree node 元数据、TextAsset bytes、MonoBehaviour/ScriptableObject 基础 TypeTree 字段级解析已起步;复杂字段覆盖、发布级重打包和 Patch 发布统一仍未完成)。
|
- 完整 AssetBundle 对象级解析(UnityFS 解包、object table、TypeTree node 元数据、TextAsset bytes、MonoBehaviour/ScriptableObject 基础 TypeTree 字段级解析已起步;复杂字段覆盖、发布级重打包和 Patch 发布统一仍未完成)。
|
||||||
- 复杂 AssetBundle 重打包和完整翻译资产编排仍未完成;当前 generic manifest 已驱动已验证的 Binary/JSON/Text 与 UnityFS localized 操作,未知结构仍明确拒绝。
|
- 复杂 AssetBundle 重打包和完整翻译资产编排仍未完成;当前 generic manifest 已驱动已验证的 Binary/JSON/Text 与 UnityFS localized 操作,未知结构仍明确拒绝。
|
||||||
- Translation Memory、Glossary 和完整 Provider 扩展体系:Translation Memory V1 与 Glossary V2 已由 Rust `bat` 持有;仍未实现的是模糊匹配、完整 Provider 扩展体系和完整 Web 协作后台。
|
- Translation Memory、Glossary 和完整 Provider 扩展体系:Translation Memory persistence schema V2 与 Glossary domain/feature contract V1(SQLite persistence schema V2)已由 Rust `bat` 持有;仍未实现的是模糊匹配、完整 Provider 扩展体系和完整 Web 协作后台。
|
||||||
- SDK、完整 Web 协作后台。
|
- SDK、完整 Web 协作后台。
|
||||||
|
|
||||||
详细状态见:
|
详细状态见:
|
||||||
|
|||||||
@@ -319,14 +319,15 @@ T03 → T16
|
|||||||
显式迁移。V1-A/V1-B 均兼容缺失 `schema_migrations` 或 component row,未知和 future
|
显式迁移。V1-A/V1-B 均兼容缺失 `schema_migrations` 或 component row,未知和 future
|
||||||
schema 仍 fail closed;已验证业务 term/history、deletion audit 保留、失败回滚重试、
|
schema 仍 fail closed;已验证业务 term/history、deletion audit 保留、失败回滚重试、
|
||||||
V1-A 并发迁移和 V2 reopen no-op。当前 Translation Tasks 为 V2、Translation Memory
|
V1-A 并发迁移和 V2 reopen no-op。当前 Translation Tasks 为 V2、Translation Memory
|
||||||
为 V1、Glossary 为 V2。
|
persistence schema 为 V2、Glossary domain/feature contract 为 V1 且 persistence schema
|
||||||
|
为 V2。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# T04 — Translation Memory Trusted 唯一性与 Supersede 治理
|
# T04 — Translation Memory Trusted 唯一性与 Supersede 治理
|
||||||
|
|
||||||
**类型:** P2
|
**类型:** P2
|
||||||
**状态:** Ready
|
**状态:** Done
|
||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
|
|
||||||
@@ -570,17 +571,18 @@ CI 检查必须:
|
|||||||
* 明确报告每项 gate;
|
* 明确报告每项 gate;
|
||||||
* required tool 缺失时不应伪装成全部通过;
|
* required tool 缺失时不应伪装成全部通过;
|
||||||
* required tool 缺失或版本不匹配必须失败;
|
* required tool 缺失或版本不匹配必须失败;
|
||||||
* 与实际 Gitea CI 尽量保持一致。
|
* 与本地 `make ci-check` 的完整 required quality gate 保持一致。
|
||||||
|
|
||||||
## 完成记录
|
## 完成记录
|
||||||
|
|
||||||
`make format` / `make fmt` 保留为显式写入命令,`make ci-check` 和兼容的
|
`make format` / `make fmt` 保留为显式写入命令,`make ci-check` 和兼容的
|
||||||
`make ci` 只执行 read-only required gates;共享 `scripts/check-go-format.sh` 由
|
`make ci` 只执行 read-only required gates;共享 `scripts/check-go-format.sh` 由
|
||||||
`make check-go-format`、本地 `scripts/ci-check.sh` 和 Gitea workflow 共用。`golangci-lint
|
`make check-go-format` 和本地 `scripts/ci-check.sh` 共用。`golangci-lint 2.12.2`
|
||||||
2.12.2` 由 `scripts/ci-versions.sh` 固定,缺失或版本不匹配失败;OpenAPI、RPC contract
|
由 `scripts/ci-versions.sh` 固定,缺失或版本不匹配失败;OpenAPI、RPC contract
|
||||||
和文档一致性由 `make check-docs` 纳入。Gitea self-hosted runner 执行相同的 required
|
和文档一致性由 `make check-docs` 纳入。项目以本地 `make ci-check` 作为唯一完整
|
||||||
Rust/Go/docs 语义,不添加 GitHub Actions。最终 workspace gates、`make test-go-api`、
|
required quality gate,开发过程中可运行 focused checks 以快速反馈,但提交前完整 gate
|
||||||
`make check-docs` 和 `make ci-check` 均已通过。
|
不得省略;仓库不依赖 Gitea、GitHub Actions 或其它远端 CI runner。最终 workspace
|
||||||
|
gates、`make test-go-api`、`make check-docs` 和 `make ci-check` 均已通过。
|
||||||
|
|
||||||
建议统一覆盖:
|
建议统一覆盖:
|
||||||
|
|
||||||
@@ -835,12 +837,21 @@ T09/T10 ─→ T11 增加 AssetBundle/Patch 真实验证
|
|||||||
|
|
||||||
**Hard Blocker:T02**
|
**Hard Blocker:T02**
|
||||||
|
|
||||||
|
## 完成记录
|
||||||
|
|
||||||
|
已完成 Translation Memory persistence schema V2、current Trusted 唯一性、显式
|
||||||
|
supersede、历史 Trusted 冲突诊断与 `resolve_conflict`。确认和冲突解决均使用
|
||||||
|
`BEGIN IMMEDIATE`、稳定 record ID 和 stale snapshot 校验,保留双向 supersede
|
||||||
|
关系与 audit event;worker 遇到冲突不会自动复用。Rust RPC/CLI、Go
|
||||||
|
`bat-api` typed forwarding、OpenAPI、focused regression/concurrency tests 和
|
||||||
|
文档已同步。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# T14 — G-012 Translation Memory 后续扩展
|
# T14 — G-012 Translation Memory 后续扩展
|
||||||
|
|
||||||
**类型:** Feature
|
**类型:** Feature
|
||||||
**状态:** Blocked
|
**状态:** Ready
|
||||||
|
|
||||||
## 前置
|
## 前置
|
||||||
|
|
||||||
@@ -874,7 +885,7 @@ Embedding/vector 不应默认进入第一阶段。
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
在现有 Glossary V2 基础上逐步增加:
|
在现有 Glossary domain/feature contract V1、persistence schema V2 基础上逐步增加:
|
||||||
|
|
||||||
* bulk import/export;
|
* bulk import/export;
|
||||||
* richer search;
|
* richer search;
|
||||||
|
|||||||
+17
-9
@@ -189,7 +189,8 @@ launcher 兼容端点只服务启动前资源发现。它们复用 Rust `bat` sn
|
|||||||
| `translation-task-update` | `translation.task.update` | `{ "task_id": "...", "status": "completed", "provider": "manual", "provider_run_id": "...", "translation_results": [{ "unit_id": "...", "source_text": "...", "translated_text": "...", "glossary_override": { "qa_identity": "...", "reviewer": "...", "reason": "...", "provenance": "...", "confirmed_unix_seconds": 1 } }] }` | `202` + 当前任务记录 |
|
| `translation-task-update` | `translation.task.update` | `{ "task_id": "...", "status": "completed", "provider": "manual", "provider_run_id": "...", "translation_results": [{ "unit_id": "...", "source_text": "...", "translated_text": "...", "glossary_override": { "qa_identity": "...", "reviewer": "...", "reason": "...", "provenance": "...", "confirmed_unix_seconds": 1 } }] }` | `202` + 当前任务记录 |
|
||||||
| `translation-worker-run` | `translation.worker.run` | `{ "provider": "mock", "concurrency": 8, "max_tasks": 2 }` | `202` + worker task |
|
| `translation-worker-run` | `translation.worker.run` | `{ "provider": "mock", "concurrency": 8, "max_tasks": 2 }` | `202` + worker task |
|
||||||
| `translation-proofread` | `translation.proofread` | 无 | `202` + 汉化状态 |
|
| `translation-proofread` | `translation.proofread` | 无 | `202` + 汉化状态 |
|
||||||
| `translation-memory-confirm` | `translation.memory.confirm` | `{ "record_id": "...", "reviewer": "...", "reason": "..." }` | `202` + 已确认的 TM 记录 |
|
| `translation-memory-confirm` | `translation.memory.confirm` | `{ "record_id": "...", "reviewer": "...", "reason": "...", "supersede_record_id": "..." }` | `202` + 已确认的 TM 记录 |
|
||||||
|
| `translation-memory-resolve-conflict` | `translation.memory.resolve_conflict` | `{ "winner_record_id": "...", "expected_trusted_record_ids": ["..."], "reviewer": "...", "reason": "..." }` | `202` + 冲突解决报告 |
|
||||||
| `translation-glossary-add` | `translation.glossary.add` | term draft JSON | `202` + Glossary term |
|
| `translation-glossary-add` | `translation.glossary.add` | term draft JSON | `202` + Glossary term |
|
||||||
| `translation-glossary-update` | `translation.glossary.update` | term draft + `reviewer` | `202` + Glossary term |
|
| `translation-glossary-update` | `translation.glossary.update` | term draft + `reviewer` | `202` + Glossary term |
|
||||||
| `translation-glossary-approve` | `translation.glossary.approve` | `{ "term_id": "...", "reviewer": "...", "reason": "..." }` | `202` + approved term |
|
| `translation-glossary-approve` | `translation.glossary.approve` | `{ "term_id": "...", "reviewer": "...", "reason": "..." }` | `202` + approved term |
|
||||||
@@ -300,7 +301,7 @@ curl -i -H 'Range: bytes=0-1023' \
|
|||||||
- 已运行的 daemon 不会热读 `config.toml`;默认 `reload` 只唤醒后台重新发现和刷新。需要应用配置文件变更时,使用带显式启动参数的 `restart`/`reload`,或先 `stop` 再重新启动 daemon。
|
- 已运行的 daemon 不会热读 `config.toml`;默认 `reload` 只唤醒后台重新发现和刷新。需要应用配置文件变更时,使用带显式启动参数的 `restart`/`reload`,或先 `stop` 再重新启动 daemon。
|
||||||
- `BAT_REDIS_URL` / `BAT_REDIS_PASSWORD` 为**预留键**:Redis 任务后端尚未接入,当前任务历史持久化在 `<state-dir>/bat-tasks.json`。
|
- `BAT_REDIS_URL` / `BAT_REDIS_PASSWORD` 为**预留键**:Redis 任务后端尚未接入,当前任务历史持久化在 `<state-dir>/bat-tasks.json`。
|
||||||
|
|
||||||
### Translation Memory V1
|
### Translation Memory persistence schema V2
|
||||||
|
|
||||||
Translation Memory 由 Rust `bat` 独立持有,默认路径为
|
Translation Memory 由 Rust `bat` 独立持有,默认路径为
|
||||||
`<output>/translation-memory.sqlite`,不在 `versions/<id>` 内,也不使用当前 release
|
`<output>/translation-memory.sqlite`,不在 `versions/<id>` 内,也不使用当前 release
|
||||||
@@ -311,16 +312,23 @@ Translation Memory 由 Rust `bat` 独立持有,默认路径为
|
|||||||
bat i18n memory summary
|
bat i18n memory summary
|
||||||
bat i18n memory query --tm-source-text '原始文本' --tm-context-json '{"destination":"Table.bytes","archive_entry":"","field_path":"Text"}'
|
bat i18n memory query --tm-source-text '原始文本' --tm-context-json '{"destination":"Table.bytes","archive_entry":"","field_path":"Text"}'
|
||||||
bat i18n memory confirm --tm-record-id 'tm-...' --tm-reviewer 'operator' --tm-reason '人工校对通过'
|
bat i18n memory confirm --tm-record-id 'tm-...' --tm-reviewer 'operator' --tm-reason '人工校对通过'
|
||||||
|
bat i18n memory conflicts --tm-limit 100
|
||||||
|
bat i18n memory resolve-conflict --tm-record-id 'tm-winner-...' \
|
||||||
|
--tm-expected-trusted-record-ids-json '["tm-winner-...","tm-loser-..."]' \
|
||||||
|
--tm-reviewer 'operator' --tm-reason '确认唯一译文'
|
||||||
```
|
```
|
||||||
|
|
||||||
只有 raw source 完全相同、完整 context 完全相同且状态为 `trusted` 的记录会被 worker
|
只有 raw source 完全相同、完整 context 完全相同且状态为 current `trusted` 的单条记录会被
|
||||||
自动复用。provider 输出写入先是 `candidate`;manual task result 即使 completed 也不会自动
|
worker 自动复用。provider 输出写入先是 `candidate`;manual task result 即使 completed
|
||||||
建立 TM 或 trusted。查询、诊断和显式 confirm 对应 Rust
|
也不会自动建立 TM 或 trusted。查询、诊断、确认和冲突治理对应 Rust
|
||||||
RPC `translation.memory.summary`、`translation.memory.query`、`translation.memory.confirm`。
|
RPC `translation.memory.summary`、`translation.memory.query`、
|
||||||
context 不完整或不一致、normalized source 辅助命中和 workflow `proofread` 都不会自动
|
`translation.memory.confirm`、`translation.memory.conflicts` 和
|
||||||
建立 trusted 记录。
|
`translation.memory.resolve_conflict`。同一 identity 存在多个 current Trusted 时,
|
||||||
|
查询返回 `trusted_conflict`,worker 禁止自动复用;确认不同译文必须显式指定 supersede
|
||||||
|
目标,历史冲突必须通过 resolve_conflict 选择稳定 record ID。context 不完整或不一致、
|
||||||
|
normalized source 辅助命中和 workflow `proofread` 都不会自动建立 trusted 记录。
|
||||||
|
|
||||||
### Glossary V2
|
### Glossary domain/feature contract V1,SQLite persistence schema V2
|
||||||
|
|
||||||
Glossary 由 Rust `bat` 独立持有,默认路径为 `<output>/glossary.sqlite`,不位于
|
Glossary 由 Rust `bat` 独立持有,默认路径为 `<output>/glossary.sqlite`,不位于
|
||||||
`versions/<id>`,也不与 TM 或当前 release 的 task 库共用。配置覆盖方式为
|
`versions/<id>`,也不与 TM 或当前 release 的 task 库共用。配置覆盖方式为
|
||||||
|
|||||||
@@ -505,6 +505,31 @@ paths:
|
|||||||
description: Missing or invalid admin token.
|
description: Missing or invalid admin token.
|
||||||
"503":
|
"503":
|
||||||
description: Rust bat Translation Memory backend is unavailable.
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
|
/admin/translation/memory/conflicts:
|
||||||
|
get:
|
||||||
|
summary: List Rust-owned Translation Memory Trusted conflicts
|
||||||
|
parameters:
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 1
|
||||||
|
maximum: 1000
|
||||||
|
default: 100
|
||||||
|
- name: translation_memory_path
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Translation Memory exact-identity Trusted conflict groups.
|
||||||
|
"400":
|
||||||
|
description: Invalid conflict list limit.
|
||||||
|
"401":
|
||||||
|
description: Missing or invalid admin token.
|
||||||
|
"503":
|
||||||
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
/admin/translation/glossary/summary:
|
/admin/translation/glossary/summary:
|
||||||
get:
|
get:
|
||||||
summary: Read Rust-owned Glossary summary
|
summary: Read Rust-owned Glossary summary
|
||||||
@@ -630,7 +655,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, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, translation-glossary-delete, localized-publish, localized-rollback, release-cleanup]
|
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, translation-memory-resolve-conflict, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, translation-glossary-delete, localized-publish, localized-rollback, release-cleanup]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
content:
|
content:
|
||||||
@@ -745,6 +770,18 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
record_id:
|
record_id:
|
||||||
type: string
|
type: string
|
||||||
|
winner_record_id:
|
||||||
|
type: string
|
||||||
|
expected_trusted_record_ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
supersede_record_id:
|
||||||
|
type: string
|
||||||
|
reviewer:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
term_id:
|
term_id:
|
||||||
type: string
|
type: string
|
||||||
source_term:
|
source_term:
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ pub use translation::{
|
|||||||
TranslationStatus,
|
TranslationStatus,
|
||||||
};
|
};
|
||||||
pub use translation_memory::{
|
pub use translation_memory::{
|
||||||
TranslationMemoryContext, TranslationMemoryDraft, TranslationMemoryEntry,
|
TranslationMemoryConflict, TranslationMemoryContext, TranslationMemoryDraft,
|
||||||
TranslationMemoryMatch, TranslationMemoryMatchKind, TranslationMemorySourceKind,
|
TranslationMemoryEntry, TranslationMemoryMatch, TranslationMemoryMatchKind,
|
||||||
TranslationMemorySourceTrace, TranslationMemorySummary, TranslationMemoryTrustStatus,
|
TranslationMemorySourceKind, TranslationMemorySourceTrace, TranslationMemorySummary,
|
||||||
|
TranslationMemoryTrustStatus,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ impl TranslationMemoryTrustStatus {
|
|||||||
pub enum TranslationMemoryMatchKind {
|
pub enum TranslationMemoryMatchKind {
|
||||||
/// 原始 source 和上下文都完全匹配,且记录可信,可自动复用。
|
/// 原始 source 和上下文都完全匹配,且记录可信,可自动复用。
|
||||||
StrongExact,
|
StrongExact,
|
||||||
|
/// 同一 exact identity 存在多个 Trusted,必须人工治理。
|
||||||
|
TrustedConflict,
|
||||||
/// 原始 source 完全匹配,但上下文不同或不足,不能自动复用。
|
/// 原始 source 完全匹配,但上下文不同或不足,不能自动复用。
|
||||||
CandidateExact,
|
CandidateExact,
|
||||||
/// 原始 source 匹配,但上下文不兼容,不能自动复用。
|
/// 原始 source 匹配,但上下文不兼容,不能自动复用。
|
||||||
@@ -68,6 +70,7 @@ impl TranslationMemoryMatchKind {
|
|||||||
pub const fn as_str(&self) -> &'static str {
|
pub const fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::StrongExact => "strong_exact",
|
Self::StrongExact => "strong_exact",
|
||||||
|
Self::TrustedConflict => "trusted_conflict",
|
||||||
Self::CandidateExact => "candidate_exact",
|
Self::CandidateExact => "candidate_exact",
|
||||||
Self::SourceOnly => "source_only",
|
Self::SourceOnly => "source_only",
|
||||||
}
|
}
|
||||||
@@ -210,6 +213,23 @@ pub struct TranslationMemoryMatch {
|
|||||||
pub can_auto_reuse: bool,
|
pub can_auto_reuse: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 一个 exact identity 的历史多 Trusted 冲突组。
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TranslationMemoryConflict {
|
||||||
|
/// 原始 source text。
|
||||||
|
pub source_text: String,
|
||||||
|
/// source text hash,仅用于稳定定位和辅助查询。
|
||||||
|
pub source_hash: String,
|
||||||
|
/// 完整 source context。
|
||||||
|
pub source_context: TranslationMemoryContext,
|
||||||
|
/// source context hash,仅用于稳定定位和辅助查询。
|
||||||
|
pub source_context_hash: String,
|
||||||
|
/// 当前数据库中属于该冲突组的 Trusted record ID。
|
||||||
|
pub trusted_record_ids: Vec<String>,
|
||||||
|
/// 冲突组记录及其原始 trust provenance。
|
||||||
|
pub records: Vec<TranslationMemoryEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
/// TM 仓储摘要。
|
/// TM 仓储摘要。
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct TranslationMemorySummary {
|
pub struct TranslationMemorySummary {
|
||||||
@@ -225,4 +245,8 @@ pub struct TranslationMemorySummary {
|
|||||||
pub superseded_count: u64,
|
pub superseded_count: u64,
|
||||||
/// 已拒绝记录数。
|
/// 已拒绝记录数。
|
||||||
pub rejected_count: u64,
|
pub rejected_count: u64,
|
||||||
|
/// exact identity 的 Trusted 冲突组数量。
|
||||||
|
pub trusted_conflict_group_count: u64,
|
||||||
|
/// 具备 current Trusted authorization 的 exact identity 数量。
|
||||||
|
pub current_trusted_count: u64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
//! Translation Memory 仓储契约。
|
//! Translation Memory 仓储契约。
|
||||||
|
|
||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
TranslationMemoryContext, TranslationMemoryDraft, TranslationMemoryEntry,
|
TranslationMemoryConflict, TranslationMemoryContext, TranslationMemoryDraft,
|
||||||
TranslationMemoryMatch, TranslationMemorySummary,
|
TranslationMemoryEntry, TranslationMemoryMatch, TranslationMemorySummary,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
/// 跨 official release 持久化的 Translation Memory 仓储。
|
/// 跨 official release 持久化的 Translation Memory 仓储。
|
||||||
///
|
///
|
||||||
/// 该契约只描述 V1 的精确查询和明确人工确认。仓储实现不得把
|
/// 该契约描述 exact-match TM 和明确人工 Trusted 治理。仓储实现不得把
|
||||||
/// `TranslationTaskStatus::Completed` 或 provider 成功隐式解释为 trusted。
|
/// `TranslationTaskStatus::Completed` 或 provider 成功隐式解释为 trusted。
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TranslationMemoryRepository: Send + Sync {
|
pub trait TranslationMemoryRepository: Send + Sync {
|
||||||
@@ -39,6 +39,27 @@ pub trait TranslationMemoryRepository: Send + Sync {
|
|||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
) -> crate::Result<TranslationMemoryEntry>;
|
) -> crate::Result<TranslationMemoryEntry>;
|
||||||
|
|
||||||
|
/// 确认记录,并在需要时显式 supersede 当前唯一 Trusted。
|
||||||
|
async fn confirm_with_supersede(
|
||||||
|
&self,
|
||||||
|
record_id: &str,
|
||||||
|
reviewer: &str,
|
||||||
|
reason: Option<String>,
|
||||||
|
supersede_record_id: Option<&str>,
|
||||||
|
) -> crate::Result<TranslationMemoryEntry>;
|
||||||
|
|
||||||
|
/// 列出历史上存在多个 Trusted 的 exact identity 冲突组。
|
||||||
|
async fn list_conflicts(&self, limit: usize) -> crate::Result<Vec<TranslationMemoryConflict>>;
|
||||||
|
|
||||||
|
/// 使用事务内精确的 expected set 显式解决一个 Trusted 冲突组。
|
||||||
|
async fn resolve_conflict(
|
||||||
|
&self,
|
||||||
|
winner_record_id: &str,
|
||||||
|
expected_trusted_record_ids: &[String],
|
||||||
|
reviewer: &str,
|
||||||
|
reason: &str,
|
||||||
|
) -> crate::Result<TranslationMemoryEntry>;
|
||||||
|
|
||||||
/// 按稳定记录 ID 读取一条 TM 记录。
|
/// 按稳定记录 ID 读取一条 TM 记录。
|
||||||
async fn find(&self, record_id: &str) -> crate::Result<TranslationMemoryEntry>;
|
async fn find(&self, record_id: &str) -> crate::Result<TranslationMemoryEntry>;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ Go `cmd/bat-api` 是资源 bootstrap、已发布资源分发和鉴权控制服
|
|||||||
- `/admin/` 与白名单 `/admin/control/{action}`;其中翻译管理面包含
|
- `/admin/` 与白名单 `/admin/control/{action}`;其中翻译管理面包含
|
||||||
`/admin/translation/tasks`、`/admin/translation/handoff`、
|
`/admin/translation/tasks`、`/admin/translation/handoff`、
|
||||||
`/admin/translation/memory/summary`、`/admin/translation/memory/query` 和
|
`/admin/translation/memory/summary`、`/admin/translation/memory/query` 和
|
||||||
`translation-memory-confirm` 转发
|
`translation-memory-confirm`、`translation-memory-resolve-conflict` 转发
|
||||||
- `/openapi.yaml`
|
- `/openapi.yaml`
|
||||||
|
|
||||||
HTTP 路由的 OpenAPI 文本由 `internal/api/openapi.go` 提供,运行中的服务也可
|
HTTP 路由的 OpenAPI 文本由 `internal/api/openapi.go` 提供,运行中的服务也可
|
||||||
|
|||||||
@@ -196,8 +196,10 @@ pub struct ParserRegistry {
|
|||||||
### 4. 翻译系统(目标扩展,Go;当前 worker 由 Rust `bat` 承担)
|
### 4. 翻译系统(目标扩展,Go;当前 worker 由 Rust `bat` 承担)
|
||||||
|
|
||||||
当前已实现的是 Rust `bat` 的离线 TextUnit 队列、mock/Crowdin provider worker、
|
当前已实现的是 Rust `bat` 的离线 TextUnit 队列、mock/Crowdin provider worker、
|
||||||
lease/retry、结果落库、项目级 Translation Memory V1 和独立 Glossary V2。TM 位于独立
|
lease/retry、结果落库、项目级 Translation Memory persistence schema V2 和独立 Glossary
|
||||||
SQLite,按 raw source + 完整 context 做 trusted exact reuse,candidate 必须显式 confirm;
|
domain/feature contract V1(SQLite persistence schema V2)。TM 位于独立 SQLite,按 raw
|
||||||
|
source + 完整 context 做 current Trusted exact reuse,candidate 必须显式 confirm;
|
||||||
|
同一 identity 的不同译文必须显式 supersede,历史 Trusted 冲突必须显式 resolve;
|
||||||
Glossary 只有 approved term 进入 provider/TM 自动流程,并在结果上执行确定性 QA;模糊
|
Glossary 只有 approved term 进入 provider/TM 自动流程,并在结果上执行确定性 QA;模糊
|
||||||
匹配和完整 Provider 体系仍属后续缺口。
|
匹配和完整 Provider 体系仍属后续缺口。
|
||||||
|
|
||||||
@@ -229,7 +231,7 @@ type TranslationProvider interface {
|
|||||||
- Azure Translator Provider
|
- Azure Translator Provider
|
||||||
|
|
||||||
**翻译记忆库**:
|
**翻译记忆库**:
|
||||||
- 当前 V1:raw source 完全相同、完整 context 完全相同且记录为 trusted 时自动复用。
|
- 当前规则:raw source 完全相同、完整 context 完全相同且只有一条 current Trusted 时自动复用。
|
||||||
- provider 输出写入先是 candidate;manual task result 不会自动建立 TM 或 trusted。`bat i18n memory confirm` 显式确认单条记录后才可自动复用。
|
- provider 输出写入先是 candidate;manual task result 不会自动建立 TM 或 trusted。`bat i18n memory confirm` 显式确认单条记录后才可自动复用。
|
||||||
- source、context、release、TextUnit、provider 和 run provenance 保存在 Rust TM SQLite 中。
|
- source、context、release、TextUnit、provider 和 run provenance 保存在 Rust TM SQLite 中。
|
||||||
- 模糊匹配、术语优先级和 PostgreSQL 服务化仍不是当前实现。
|
- 模糊匹配、术语优先级和 PostgreSQL 服务化仍不是当前实现。
|
||||||
@@ -296,7 +298,8 @@ HTTP Request → Middleware (Auth, CORS, Logger) → Handler → Service → Rep
|
|||||||
|
|
||||||
### 7. Web 后台 (Vue 3,目标设计)
|
### 7. Web 后台 (Vue 3,目标设计)
|
||||||
|
|
||||||
当前只有 `bat-api` 内嵌 dashboard MVP;Rust `bat` 的 Glossary V2 已实现,登录、角色、Web 术语管理和完整协作审核仍未实现。
|
当前只有 `bat-api` 内嵌 dashboard MVP;Rust `bat` 的 Glossary domain/feature contract V1
|
||||||
|
及 SQLite persistence schema V2 已实现,登录、角色、Web 术语管理和完整协作审核仍未实现。
|
||||||
|
|
||||||
**技术栈**:
|
**技术栈**:
|
||||||
- Vue 3 + Composition API
|
- Vue 3 + Composition API
|
||||||
|
|||||||
@@ -36,8 +36,10 @@
|
|||||||
- 新的跨语言控制和查询能力优先增加 Rust RPC contract,再由
|
- 新的跨语言控制和查询能力优先增加 Rust RPC contract,再由
|
||||||
`internal/backendrpc` 消费。
|
`internal/backendrpc` 消费。
|
||||||
|
|
||||||
4. **完整游戏业务 API、完整 Web 协作后台和 Provider 扩展体系仍是后续目标;Rust `bat` 已持有 Glossary V2,Web 术语协作视图仍待建设**;
|
4. **完整游戏业务 API、完整 Web 协作后台和 Provider 扩展体系仍是后续目标;Rust `bat` 已持有
|
||||||
Translation Memory V1 已由 Rust `bat` 持有,不能从目标架构图推断 Go 侧拥有第二份状态。
|
Glossary domain/feature contract V1(SQLite persistence schema V2),Web 术语协作视图仍待建设**;
|
||||||
|
Translation Memory persistence schema V2 已由 Rust `bat` 持有,不能从目标架构图推断 Go
|
||||||
|
侧拥有第二份状态。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -180,9 +180,9 @@
|
|||||||
`available=false`,不会因为查询创建空库。发布后的 TextUnit 队列还会在当前
|
`available=false`,不会因为查询创建空库。发布后的 TextUnit 队列还会在当前
|
||||||
release 根目录写入 `translation-tasks.sqlite`,由版本化 `schema_migrations`
|
release 根目录写入 `translation-tasks.sqlite`,由版本化 `schema_migrations`
|
||||||
管理 V2 queued/running/failed/completed/skipped、provider run、lease、失败分类、
|
管理 V2 queued/running/failed/completed/skipped、provider run、lease、失败分类、
|
||||||
重试计划和 TextUnit 级译文结果。跨 release 的 Translation Memory V1 独立存储在
|
重试计划和 TextUnit 级译文结果。跨 release 的 Translation Memory persistence schema V2
|
||||||
`<output>/translation-memory.sqlite`,记录 raw source/hash、完整 context、candidate/
|
独立存储在 `<output>/translation-memory.sqlite`,记录 raw source/hash、完整 context、
|
||||||
trusted 和 release/TextUnit/provider/run provenance;`translation.tasks` 优先查询这份状态库,
|
candidate/trusted 和 release/TextUnit/provider/run provenance;`translation.tasks` 优先查询这份状态库,
|
||||||
`translation.worker.run` 由 Rust worker 回写状态;`translation.task.update` 仍供外部 provider 流程回写状态;
|
`translation.worker.run` 由 Rust worker 回写状态;`translation.task.update` 仍供外部 provider 流程回写状态;
|
||||||
没有状态库的旧 release 才回退到 immutable JSON 队列。`bat doctor cas`
|
没有状态库的旧 release 才回退到 immutable JSON 队列。`bat doctor cas`
|
||||||
已提供只读 CAS 根目录、对象目录、元数据库文件和对象统计诊断;`resource.index`
|
已提供只读 CAS 根目录、对象目录、元数据库文件和对象统计诊断;`resource.index`
|
||||||
|
|||||||
@@ -213,8 +213,8 @@ bat i18n worker run \
|
|||||||
`--watch`,因此可以单次、限定次数或周期执行;`--run-count > 1` 时仍必须
|
`--watch`,因此可以单次、限定次数或周期执行;`--run-count > 1` 时仍必须
|
||||||
显式指定 `--interval`。
|
显式指定 `--interval`。
|
||||||
|
|
||||||
Glossary V2 是 Rust `bat` 持有的独立项目级 SQLite 资产,默认位于
|
Glossary domain/feature contract V1 由 Rust `bat` 持有,并由 SQLite persistence schema V2
|
||||||
`<output>/glossary.sqlite`;V2 正式吸收历史上的 `glossary_term_deletions`
|
承载,默认位于 `<output>/glossary.sqlite`;V2 正式吸收历史上的 `glossary_term_deletions`
|
||||||
schema drift;也可以用 `--glossary-path`、
|
schema drift;也可以用 `--glossary-path`、
|
||||||
`BAT_GLOSSARY_PATH` 或 `[translation.worker].glossary_path` 指定。worker 只把
|
`BAT_GLOSSARY_PATH` 或 `[translation.worker].glossary_path` 指定。worker 只把
|
||||||
`approved` term 转成 provider-neutral constraints,并在 TM 复用、provider 返回
|
`approved` term 转成 provider-neutral constraints,并在 TM 复用、provider 返回
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ BlueArchive Toolkit 的部署文档分为当前可用模式和目标模式:
|
|||||||
2. **官方资源同步生产任务**:当前可用,运行 Rust `bat --watch` 或 RPC/daemon 模式。
|
2. **官方资源同步生产任务**:当前可用,运行 Rust `bat --watch` 或 RPC/daemon 模式。
|
||||||
3. **bat-api 资源 bootstrap / 分发服务**:当前可用,和 Rust `bat` 在同一服务器/容器环境运行,经 `bat.sock` RPC 获取当前 `resource_root`。
|
3. **bat-api 资源 bootstrap / 分发服务**:当前可用,和 Rust `bat` 在同一服务器/容器环境运行,经 `bat.sock` RPC 获取当前 `resource_root`。
|
||||||
4. **可选数据库开发环境**:PostgreSQL/Redis 只服务于未来的 Go 服务层、完整 Web 协作后台和
|
4. **可选数据库开发环境**:PostgreSQL/Redis 只服务于未来的 Go 服务层、完整 Web 协作后台和
|
||||||
Provider 扩展,不是当前 `bat` / `bat-api` 的生产运行依赖;当前 Translation Memory V1
|
Provider 扩展,不是当前 `bat` / `bat-api` 的生产运行依赖;当前 Translation Memory
|
||||||
使用 `<output>/translation-memory.sqlite`。
|
persistence schema V2 使用 `<output>/translation-memory.sqlite`。
|
||||||
5. **完整单机/分布式部署**:尚未提供。完整游戏业务 API、数据库迁移和 Web 未实现前,不把它作为可执行部署方案。
|
5. **完整单机/分布式部署**:尚未提供。完整游戏业务 API、数据库迁移和 Web 未实现前,不把它作为可执行部署方案。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ rustc --version # 验证安装
|
|||||||
cargo --version
|
cargo --version
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 自托管 Gitea runner
|
#### 本地完整质量门禁
|
||||||
|
|
||||||
`.gitea/workflows/bat.yml` 使用 `runs-on: linux`,并且不依赖 `actions/checkout`、`dtolnay/rust-toolchain` 等外部 GitHub Action。runner 需要在执行环境中预装以下命令:
|
项目以本地 `make ci-check` 作为唯一完整 required quality gate。开发过程中可运行 focused
|
||||||
|
checks 以快速反馈,但提交前完整 gate 不得省略;仓库不依赖 Gitea、GitHub Actions 或其它
|
||||||
|
远端 CI runner。执行环境需要预装以下命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git --version
|
git --version
|
||||||
@@ -38,11 +40,9 @@ golangci-lint --version # 必须为 2.12.2
|
|||||||
```
|
```
|
||||||
|
|
||||||
缺少上述命令、版本不匹配或 `golangci-lint` 不是 2.12.2 都会使 required gate 失败;
|
缺少上述命令、版本不匹配或 `golangci-lint` 不是 2.12.2 都会使 required gate 失败;
|
||||||
`golangci-lint 2.12.2` 是 required gate,不是可选检查。该 workflow 会用
|
`golangci-lint 2.12.2` 是 required gate,不是可选检查。`make ci-check` 会执行 Rust
|
||||||
`GITHUB_SERVER_URL`、`GITHUB_REPOSITORY`、`GITHUB_REF` 和 `GITHUB_SHA` 手动 `git fetch`
|
workspace 的只读格式检查、检查、release build、clippy 和测试,以及通过
|
||||||
当前提交,再执行 Rust workspace 的只读格式检查、检查、构建、clippy 和测试,以及通过
|
|
||||||
`make check-go-format` 执行的 Go 格式、测试、vet、构建、2.12.2 lint 和文档状态门禁。
|
`make check-go-format` 执行的 Go 格式、测试、vet、构建、2.12.2 lint 和文档状态门禁。
|
||||||
这样可以避免自托管 runner 在准备阶段通过代理克隆第三方 action 仓库。
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -261,9 +261,11 @@ rollback 或 repair。
|
|||||||
| `translation.task.update` | 已实现 | `{ "task_id": "...", "status": "failed", "failure_reason": "...", "provider_run_id": "..." }` | 写入当前 release 的 provider worker 状态,返回可回查任务记录。 |
|
| `translation.task.update` | 已实现 | `{ "task_id": "...", "status": "failed", "failure_reason": "...", "provider_run_id": "..." }` | 写入当前 release 的 provider worker 状态,返回可回查任务记录。 |
|
||||||
| `translation.worker.run` | 已实现 | provider worker 参数 | 异步触发 Rust provider worker,返回 `{ "task_id": "...", "kind": "translation.worker.run", "worker": {...} }`。 |
|
| `translation.worker.run` | 已实现 | provider worker 参数 | 异步触发 Rust provider worker,返回 `{ "task_id": "...", "kind": "translation.worker.run", "worker": {...} }`。 |
|
||||||
| `translation.proofread` | 已实现 | `null` | 将当前汉化 workflow 标记为人工校对中,返回工作流状态报告。 |
|
| `translation.proofread` | 已实现 | `null` | 将当前汉化 workflow 标记为人工校对中,返回工作流状态报告。 |
|
||||||
| `translation.memory.summary` | 已实现 | 可选 `{ "translation_memory_path": "..." }` | 返回 TM schema 版本、总记录数及 candidate/trusted/rejected/superseded 状态计数。 |
|
| `translation.memory.summary` | 已实现 | 可选 `{ "translation_memory_path": "..." }` | 返回 TM persistence schema 版本、总记录数、candidate/trusted/rejected/superseded 状态计数和 trusted 冲突组计数。 |
|
||||||
| `translation.memory.query` | 已实现 | `{ "source_text": "...", "source_context": {...}, "limit": 100 }` | 按 raw source 查询记录,返回 match kind、trust、translation 和 provenance。 |
|
| `translation.memory.query` | 已实现 | `{ "source_text": "...", "source_context": {...}, "limit": 100 }` | 按 raw source 查询记录,返回 match kind、trust、translation 和 provenance;conflict 结果不可自动复用。 |
|
||||||
| `translation.memory.confirm` | 已实现 | `{ "record_id": "...", "reviewer": "...", "reason": "..." }` | 显式确认一条 candidate 为 trusted;worker 之后才可自动复用。 |
|
| `translation.memory.confirm` | 已实现 | `{ "record_id": "...", "reviewer": "...", "reason": "...", "supersede_record_id": "..." }` | 显式确认 candidate 为 trusted;已有不同 current Trusted 时必须显式 supersede,worker 之后才可自动复用。 |
|
||||||
|
| `translation.memory.conflicts` | 已实现 | 可选 `{ "translation_memory_path": "...", "limit": 100 }` | 只读列出 exact source/context 下存在多个 current Trusted 的冲突组。 |
|
||||||
|
| `translation.memory.resolve_conflict` | 已实现 | `{ "winner_record_id": "...", "expected_trusted_record_ids": ["..."], "reviewer": "...", "reason": "..." }` | 使用稳定 record ID 原子解决历史 Trusted 冲突,保留 supersede 历史并写入 audit event。 |
|
||||||
| `translation.glossary.summary` | 已实现 | 可选 `{ "glossary_path": "..." }` | 返回 Glossary schema 版本和 draft/approved/deprecated/rejected 计数;缺库只返回 `available=false`,不会创建空库。 |
|
| `translation.glossary.summary` | 已实现 | 可选 `{ "glossary_path": "..." }` | 返回 Glossary schema 版本和 draft/approved/deprecated/rejected 计数;缺库只返回 `available=false`,不会创建空库。 |
|
||||||
| `translation.glossary.query` | 已实现 | `{ "source_text": "...", "category": "...", "review_status": "approved", "limit": 100 }` | 查询 term、alias、scope、source provenance 和完整 source/review history。 |
|
| `translation.glossary.query` | 已实现 | `{ "source_text": "...", "category": "...", "review_status": "approved", "limit": 100 }` | 查询 term、alias、scope、source provenance 和完整 source/review history。 |
|
||||||
| `translation.glossary.diagnose` | 已实现 | `{ "source_text": "...", "context": {...} }` | 只对 approved term 生成 provider-neutral constraints,并返回冲突/覆盖诊断和 blocked 决策。 |
|
| `translation.glossary.diagnose` | 已实现 | `{ "source_text": "...", "context": {...} }` | 只对 approved term 生成 provider-neutral constraints,并返回冲突/覆盖诊断和 blocked 决策。 |
|
||||||
@@ -535,7 +537,8 @@ CLI 对应关系:
|
|||||||
| `bat i18n worker run` | `translation.worker.run` |
|
| `bat i18n worker run` | `translation.worker.run` |
|
||||||
| `bat i18n proofread` | `translation.proofread` |
|
| `bat i18n proofread` | `translation.proofread` |
|
||||||
| `bat i18n memory summary` / `bat i18n memory query` | `translation.memory.summary` / `translation.memory.query` |
|
| `bat i18n memory summary` / `bat i18n memory query` | `translation.memory.summary` / `translation.memory.query` |
|
||||||
| `bat i18n memory confirm` | `translation.memory.confirm` |
|
| `bat i18n memory confirm` / `bat i18n memory conflicts` | `translation.memory.confirm` / `translation.memory.conflicts` |
|
||||||
|
| `bat i18n memory resolve-conflict` | `translation.memory.resolve_conflict` |
|
||||||
| `bat i18n glossary summary` / `bat i18n glossary query` | `translation.glossary.summary` / `translation.glossary.query` |
|
| `bat i18n glossary summary` / `bat i18n glossary query` | `translation.glossary.summary` / `translation.glossary.query` |
|
||||||
| `bat i18n glossary diagnose` | `translation.glossary.diagnose` |
|
| `bat i18n glossary diagnose` | `translation.glossary.diagnose` |
|
||||||
| `bat i18n glossary add/update` | `translation.glossary.add` / `translation.glossary.update` |
|
| `bat i18n glossary add/update` | `translation.glossary.add` / `translation.glossary.update` |
|
||||||
@@ -573,7 +576,8 @@ CLI 对应关系:
|
|||||||
`localized.status`、`localized.publish`、`localized.rollback`、
|
`localized.status`、`localized.publish`、`localized.rollback`、
|
||||||
`translation.tasks`、`translation.handoff`、`translation.task.update`、
|
`translation.tasks`、`translation.handoff`、`translation.task.update`、
|
||||||
`translation.worker.run`、`translation.proofread`、`translation.memory.summary`、
|
`translation.worker.run`、`translation.proofread`、`translation.memory.summary`、
|
||||||
`translation.memory.query`、`translation.memory.confirm`、`translation.glossary.summary`、
|
`translation.memory.query`、`translation.memory.confirm`、`translation.memory.conflicts`、
|
||||||
|
`translation.memory.resolve_conflict`、`translation.glossary.summary`、
|
||||||
`translation.glossary.query`、`translation.glossary.diagnose`、`translation.glossary.add`、
|
`translation.glossary.query`、`translation.glossary.diagnose`、`translation.glossary.add`、
|
||||||
`translation.glossary.update`、`translation.glossary.approve`、`translation.glossary.deprecate`、
|
`translation.glossary.update`、`translation.glossary.approve`、`translation.glossary.deprecate`、
|
||||||
`translation.glossary.delete`、
|
`translation.glossary.delete`、
|
||||||
@@ -594,7 +598,7 @@ CLI 对应关系:
|
|||||||
| `TaskBackend` | `task.list`、`task.status`、`task.logs`、`task.cancel` | 鉴权后的 daemon 任务查询和取消 |
|
| `TaskBackend` | `task.list`、`task.status`、`task.logs`、`task.cancel` | 鉴权后的 daemon 任务查询和取消 |
|
||||||
| `ParseBackend` | `parse.status`、`parse.text_units`、`parse.errors` | 鉴权后的当前 release 解析状态、TextUnit 和解析错误只读查询 |
|
| `ParseBackend` | `parse.status`、`parse.text_units`、`parse.errors` | 鉴权后的当前 release 解析状态、TextUnit 和解析错误只读查询 |
|
||||||
| `TranslationBackend` | `translation.tasks`、`translation.handoff`、`translation.task.update`、`translation.worker.run`、`translation.proofread` | 鉴权后的 dashboard 翻译任务查询、交接视图、状态回写、provider worker 触发与人工校对标记 |
|
| `TranslationBackend` | `translation.tasks`、`translation.handoff`、`translation.task.update`、`translation.worker.run`、`translation.proofread` | 鉴权后的 dashboard 翻译任务查询、交接视图、状态回写、provider worker 触发与人工校对标记 |
|
||||||
| `TranslationMemoryBackend` | `translation.memory.summary`、`translation.memory.query`、`translation.memory.confirm` | 鉴权后的 TM 摘要、source/context 查询和显式 candidate 确认;Go 只转发,不持有 TM 状态 |
|
| `TranslationMemoryBackend` | `translation.memory.summary`、`translation.memory.query`、`translation.memory.confirm`、`translation.memory.conflicts`、`translation.memory.resolve_conflict` | 鉴权后的 TM 摘要、source/context 查询和 Trusted 冲突治理;Go 只转发,不持有 TM 状态 |
|
||||||
| `GlossaryBackend` | `translation.glossary.summary/query/diagnose/add/update/approve/deprecate/delete` | 鉴权后的 Glossary 摘要、term/history 查询、确定性诊断和审核/删除 mutation;Go 只转发,不持有 Glossary 状态 |
|
| `GlossaryBackend` | `translation.glossary.summary/query/diagnose/add/update/approve/deprecate/delete` | 鉴权后的 Glossary 摘要、term/history 查询、确定性诊断和审核/删除 mutation;Go 只转发,不持有 Glossary 状态 |
|
||||||
| `LocalizedBackend` | `localized.status`、`localized.publish`、`localized.rollback` | 鉴权后的汉化 release 状态、发布与显式回滚 |
|
| `LocalizedBackend` | `localized.status`、`localized.publish`、`localized.rollback` | 鉴权后的汉化 release 状态、发布与显式回滚 |
|
||||||
| `ReleaseBackend` | `release.status`、`release.list`、`release.distribution`、`release.cleanup` | 鉴权后的双 release 查询、验证分发选择和 dry-run/execute cleanup;Go 不持有 release 状态 |
|
| `ReleaseBackend` | `release.status`、`release.list`、`release.distribution`、`release.cleanup` | 鉴权后的双 release 查询、验证分发选择和 dry-run/execute cleanup;Go 不持有 release 状态 |
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ Rust 窗口请基于当前真实代码生成或导出以下 JSON:
|
|||||||
2. `catalog.status` available=false 响应。
|
2. `catalog.status` available=false 响应。
|
||||||
3. `resource.manifest` 第一页响应,至少包含 1 到 2 个 entries。
|
3. `resource.manifest` 第一页响应,至少包含 1 到 2 个 entries。
|
||||||
4. 对应 release 的 `official-sync-snapshot.json`。
|
4. 对应 release 的 `official-sync-snapshot.json`。
|
||||||
5. Rust Glossary V2 的 `translation.glossary.query` 响应,至少包含 alias、approved
|
5. Rust Glossary domain/feature contract V1、SQLite persistence schema V2 的
|
||||||
|
`translation.glossary.query` 响应,至少包含 alias、approved
|
||||||
review、source provenance 和 created/approved history。
|
review、source provenance 和 created/approved history。
|
||||||
|
|
||||||
输出应来自 Rust 代码路径,而不是手写 JSON。允许使用 fixture resource root 或临时目录,但不能依赖开发机真实资源目录。
|
输出应来自 Rust 代码路径,而不是手写 JSON。允许使用 fixture resource root 或临时目录,但不能依赖开发机真实资源目录。
|
||||||
|
|||||||
@@ -152,17 +152,18 @@ official/localized distribution 均被阻断,普通查询不会自动重建。
|
|||||||
更完整的查询/权限/损坏恢复、模糊 TM、bat.sock peer credential/perms、FFI 生命周期、
|
更完整的查询/权限/损坏恢复、模糊 TM、bat.sock peer credential/perms、FFI 生命周期、
|
||||||
资源大小/限额与更强的持久化 fsync 语义仍按后续专项推进。
|
资源大小/限额与更强的持久化 fsync 语义仍按后续专项推进。
|
||||||
|
|
||||||
### G-012:Translation Memory V1 已实现,扩展能力仍缺失
|
### G-012:Translation Memory persistence schema V2 已实现,扩展能力仍缺失
|
||||||
|
|
||||||
Rust `bat` 已提供独立项目级 SQLite TM,当前 schema version 为 V1;schema 打开遵守
|
Rust `bat` 已提供独立项目级 SQLite TM,当前 persistence schema version 为 V2;schema 打开遵守
|
||||||
只读 preflight、fingerprint、transaction rollback 和 future/unknown fail-closed
|
只读 preflight、fingerprint、transaction rollback 和 future/unknown fail-closed
|
||||||
契约。它记录 raw source/hash、完整 context、release/TextUnit/provider/run provenance,
|
契约。它记录 raw source/hash、完整 context、release/TextUnit/provider/run provenance,
|
||||||
区分 candidate/trusted,只有显式 confirm 才能建立 trusted 记录;worker 只自动复用
|
区分 candidate/trusted,只有显式 confirm 或 conflict resolve 才能建立唯一 current
|
||||||
trusted 的 raw source + 完整 context exact match,并在复用前执行已批准 Glossary 的
|
trusted 记录;worker 只自动复用 raw source + 完整 context exact match 的 current
|
||||||
确定性 QA。Go `bat-api` 已提供鉴权的 summary/query 只读接口和 confirm 转发,但 Go
|
trusted,并在复用前执行已批准 Glossary 的确定性 QA。Go `bat-api` 已提供鉴权的
|
||||||
不持有 TM 状态。仍缺少模糊匹配和更丰富的导入导出历史能力。
|
summary/query/conflicts 只读接口和 confirm/resolve_conflict 转发,但 Go 不持有 TM 状态。
|
||||||
|
仍缺少模糊匹配和更丰富的导入导出历史能力。
|
||||||
|
|
||||||
### G-013:Glossary V2 已实现,协作视图仍缺失
|
### G-013:Glossary domain/feature contract V1、persistence schema V2 已实现,协作视图仍缺失
|
||||||
|
|
||||||
Rust `bat` 已提供独立项目级 `glossary.sqlite`,当前 schema version 为 V2;V2 正式
|
Rust `bat` 已提供独立项目级 `glossary.sqlite`,当前 schema version 为 V2;V2 正式
|
||||||
吸收历史上未升版本的 `glossary_term_deletions` drift,并将历史 V1-A(无 deletion
|
吸收历史上未升版本的 `glossary_term_deletions` drift,并将历史 V1-A(无 deletion
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
| 组件 | 路径 | 状态 | 说明 |
|
| 组件 | 路径 | 状态 | 说明 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Module | `go.mod` → `bat-api` | 已用 | 服务层模块名 |
|
| 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`、`release.attestation/status/list/distribution/cleanup`、`catalog.*`、`parse.*`、`localized.status/publish/rollback`、`task.*`、`translation.tasks`、`translation.handoff`、`translation.task.update`、`translation.worker.run`、`translation.proofread`、`translation.memory.summary/query/confirm`、`translation.glossary.summary/query/diagnose/add/update/approve/deprecate/delete` 和文件级 UnityFS patch 调用;`resource.manifest` typed params 固定 release/publication/manifest identity 和 verification generation;`localized.publish` 的 typed params 支持 `translation_file`、`from_worker` 或 `patch_manifest` 三选一;`resource.index`、`patch.apply` 仍通过通用 `Call` 走同一 contract;fake transport 单测和 `internal/api/testdata/contract/` mirror test 固化 Rust 输出字段 |
|
| RPC client | `internal/backendrpc` | **完成** | Unix socket JSON-RPC transport + typed helper;typed helper 覆盖 daemon 已实现控制/查询、`resource.state/sync/verify/repair/manifest/list`、`release.attestation/status/list/distribution/cleanup`、`catalog.*`、`parse.*`、`localized.status/publish/rollback`、`task.*`、`translation.tasks`、`translation.handoff`、`translation.task.update`、`translation.worker.run`、`translation.proofread`、`translation.memory.summary/query/confirm/conflicts/resolve_conflict`、`translation.glossary.summary/query/diagnose/add/update/approve/deprecate/delete` 和文件级 UnityFS patch 调用;`resource.manifest` typed params 固定 release/publication/manifest identity 和 verification generation;`localized.publish` 的 typed params 支持 `translation_file`、`from_worker` 或 `patch_manifest` 三选一;`resource.index`、`patch.apply` 仍通过通用 `Call` 走同一 contract;fake transport 单测和 `internal/api/testdata/contract/` mirror test 固化 Rust 输出字段 |
|
||||||
| 资源 bootstrap/分发 | `cmd/bat-api` + `internal/api` | **MVP+生产控制面** | RPC 发现 + 周期刷新/诊断 + `/v1/bootstrap` + `/v1/launcher/bootstrap` + `/v1/releases` + `/v1/distribution` + launcher 资源 metadata 兼容 + `/readyz` + CDN Range/缓存头 + 鉴权/限流/访问日志/反代适配 + OpenAPI + 管理控制白名单 + release/localized/TM/Glossary admin forwarding + 内嵌 dashboard + `.env` |
|
| 资源 bootstrap/分发 | `cmd/bat-api` + `internal/api` | **MVP+生产控制面** | RPC 发现 + 周期刷新/诊断 + `/v1/bootstrap` + `/v1/launcher/bootstrap` + `/v1/releases` + `/v1/distribution` + launcher 资源 metadata 兼容 + `/readyz` + CDN Range/缓存头 + 鉴权/限流/访问日志/反代适配 + OpenAPI + 管理控制白名单 + release/localized/TM/Glossary admin forwarding + 内嵌 dashboard + `.env` |
|
||||||
| 试验 CLI | `cmd/bat` | **试验** | doctor 固定 ok;manifest/sync 走 FFI |
|
| 试验 CLI | `cmd/bat` | **试验** | doctor 固定 ok;manifest/sync 走 FFI |
|
||||||
| FFI | `internal/ffi` | **可选** | 需 `build-ffi` |
|
| FFI | `internal/ffi` | **可选** | 需 `build-ffi` |
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ use terminal_output::RotatingStructuredLogger;
|
|||||||
use terminal_output::STARTUP_BANNER;
|
use terminal_output::STARTUP_BANNER;
|
||||||
use translation_query::{
|
use translation_query::{
|
||||||
build_translation_handoff_report, build_translation_memory_confirm_report,
|
build_translation_handoff_report, build_translation_memory_confirm_report,
|
||||||
build_translation_memory_query_report, build_translation_memory_summary_report,
|
build_translation_memory_conflicts_report, build_translation_memory_query_report,
|
||||||
|
build_translation_memory_resolve_conflict_report, build_translation_memory_summary_report,
|
||||||
build_translation_tasks_report, run_translation_memory_command, textunit_query_json,
|
build_translation_tasks_report, run_translation_memory_command, textunit_query_json,
|
||||||
update_translation_task_status_report,
|
update_translation_task_status_report,
|
||||||
};
|
};
|
||||||
@@ -242,7 +243,9 @@ fn run() -> anyhow::Result<i32> {
|
|||||||
}
|
}
|
||||||
CliCommand::TranslationMemorySummary
|
CliCommand::TranslationMemorySummary
|
||||||
| CliCommand::TranslationMemoryQuery
|
| CliCommand::TranslationMemoryQuery
|
||||||
| CliCommand::TranslationMemoryConfirm => {
|
| CliCommand::TranslationMemoryConfirm
|
||||||
|
| CliCommand::TranslationMemoryConflicts
|
||||||
|
| CliCommand::TranslationMemoryResolveConflict => {
|
||||||
run_translation_memory_command(&options)?;
|
run_translation_memory_command(&options)?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
@@ -421,6 +424,8 @@ struct CliOptions {
|
|||||||
translation_memory_source_text: Option<String>,
|
translation_memory_source_text: Option<String>,
|
||||||
translation_memory_context_json: Option<String>,
|
translation_memory_context_json: Option<String>,
|
||||||
translation_memory_record_id: Option<String>,
|
translation_memory_record_id: Option<String>,
|
||||||
|
translation_memory_supersede_record_id: Option<String>,
|
||||||
|
translation_memory_expected_trusted_record_ids_json: Option<String>,
|
||||||
translation_memory_reviewer: Option<String>,
|
translation_memory_reviewer: Option<String>,
|
||||||
translation_memory_reason: Option<String>,
|
translation_memory_reason: Option<String>,
|
||||||
glossary_term_id: Option<String>,
|
glossary_term_id: Option<String>,
|
||||||
@@ -548,6 +553,8 @@ impl Default for CliOptions {
|
|||||||
translation_memory_source_text: None,
|
translation_memory_source_text: None,
|
||||||
translation_memory_context_json: None,
|
translation_memory_context_json: None,
|
||||||
translation_memory_record_id: None,
|
translation_memory_record_id: None,
|
||||||
|
translation_memory_supersede_record_id: None,
|
||||||
|
translation_memory_expected_trusted_record_ids_json: None,
|
||||||
translation_memory_reviewer: None,
|
translation_memory_reviewer: None,
|
||||||
translation_memory_reason: None,
|
translation_memory_reason: None,
|
||||||
glossary_term_id: None,
|
glossary_term_id: None,
|
||||||
@@ -666,6 +673,8 @@ enum CliCommand {
|
|||||||
TranslationMemorySummary,
|
TranslationMemorySummary,
|
||||||
TranslationMemoryQuery,
|
TranslationMemoryQuery,
|
||||||
TranslationMemoryConfirm,
|
TranslationMemoryConfirm,
|
||||||
|
TranslationMemoryConflicts,
|
||||||
|
TranslationMemoryResolveConflict,
|
||||||
GlossarySummary,
|
GlossarySummary,
|
||||||
GlossaryQuery,
|
GlossaryQuery,
|
||||||
GlossaryAdd,
|
GlossaryAdd,
|
||||||
@@ -1223,6 +1232,8 @@ const RPC_METHOD_TRANSLATION_WORKER_RUN: &str = "translation.worker.run";
|
|||||||
const RPC_METHOD_TRANSLATION_MEMORY_SUMMARY: &str = "translation.memory.summary";
|
const RPC_METHOD_TRANSLATION_MEMORY_SUMMARY: &str = "translation.memory.summary";
|
||||||
const RPC_METHOD_TRANSLATION_MEMORY_QUERY: &str = "translation.memory.query";
|
const RPC_METHOD_TRANSLATION_MEMORY_QUERY: &str = "translation.memory.query";
|
||||||
const RPC_METHOD_TRANSLATION_MEMORY_CONFIRM: &str = "translation.memory.confirm";
|
const RPC_METHOD_TRANSLATION_MEMORY_CONFIRM: &str = "translation.memory.confirm";
|
||||||
|
const RPC_METHOD_TRANSLATION_MEMORY_CONFLICTS: &str = "translation.memory.conflicts";
|
||||||
|
const RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT: &str = "translation.memory.resolve_conflict";
|
||||||
const RPC_METHOD_GLOSSARY_SUMMARY: &str = "translation.glossary.summary";
|
const RPC_METHOD_GLOSSARY_SUMMARY: &str = "translation.glossary.summary";
|
||||||
const RPC_METHOD_GLOSSARY_QUERY: &str = "translation.glossary.query";
|
const RPC_METHOD_GLOSSARY_QUERY: &str = "translation.glossary.query";
|
||||||
const RPC_METHOD_GLOSSARY_ADD: &str = "translation.glossary.add";
|
const RPC_METHOD_GLOSSARY_ADD: &str = "translation.glossary.add";
|
||||||
@@ -2304,6 +2315,30 @@ fn dispatch_rpc_method(
|
|||||||
request.params.as_ref(),
|
request.params.as_ref(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_CONFLICTS => translation_memory_rpc_envelope(
|
||||||
|
request_id,
|
||||||
|
translation_memory_conflicts_rpc_report(
|
||||||
|
state_dir,
|
||||||
|
&tasks.base_config.output_root,
|
||||||
|
tasks
|
||||||
|
.translation_worker_config
|
||||||
|
.translation_memory_path
|
||||||
|
.as_deref(),
|
||||||
|
request.params.as_ref(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT => translation_memory_rpc_envelope(
|
||||||
|
request_id,
|
||||||
|
translation_memory_resolve_conflict_rpc_report(
|
||||||
|
state_dir,
|
||||||
|
&tasks.base_config.output_root,
|
||||||
|
tasks
|
||||||
|
.translation_worker_config
|
||||||
|
.translation_memory_path
|
||||||
|
.as_deref(),
|
||||||
|
request.params.as_ref(),
|
||||||
|
),
|
||||||
|
),
|
||||||
RPC_METHOD_GLOSSARY_SUMMARY => glossary_rpc_envelope(
|
RPC_METHOD_GLOSSARY_SUMMARY => glossary_rpc_envelope(
|
||||||
request_id,
|
request_id,
|
||||||
glossary_summary_rpc_report(
|
glossary_summary_rpc_report(
|
||||||
@@ -4157,6 +4192,28 @@ fn translation_memory_rpc_envelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn translation_memory_api_error(method: &'static str, error: anyhow::Error) -> ApiError {
|
||||||
|
let message = error.to_string();
|
||||||
|
let code = if message.starts_with("Invalid argument:")
|
||||||
|
|| message.starts_with("Object not found:")
|
||||||
|
|| [
|
||||||
|
"trusted_conflict_requires_resolution",
|
||||||
|
"explicit_supersede_required",
|
||||||
|
"trusted_translation_already_exists",
|
||||||
|
"stale_supersede_target",
|
||||||
|
"conflict_snapshot_stale",
|
||||||
|
"invalid_conflict_winner",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|marker| message.contains(marker))
|
||||||
|
{
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS
|
||||||
|
} else {
|
||||||
|
ErrorCode::INTERNAL
|
||||||
|
};
|
||||||
|
ApiError::new(code, method, message)
|
||||||
|
}
|
||||||
|
|
||||||
fn translation_memory_rpc_params<'a>(
|
fn translation_memory_rpc_params<'a>(
|
||||||
params: Option<&'a serde_json::Value>,
|
params: Option<&'a serde_json::Value>,
|
||||||
method: &'static str,
|
method: &'static str,
|
||||||
@@ -4240,17 +4297,16 @@ fn translation_memory_rpc_context(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translation_memory_rpc_limit(params: Option<&serde_json::Value>) -> Result<usize, ApiError> {
|
fn translation_memory_rpc_limit(
|
||||||
let limit = match translation_memory_rpc_value(
|
params: Option<&serde_json::Value>,
|
||||||
params,
|
method: &'static str,
|
||||||
&["limit"],
|
) -> Result<usize, ApiError> {
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
let limit = match translation_memory_rpc_value(params, &["limit"], method)? {
|
||||||
)? {
|
|
||||||
None | Some(serde_json::Value::Null) => 100,
|
None | Some(serde_json::Value::Null) => 100,
|
||||||
Some(value) => value.as_u64().ok_or_else(|| {
|
Some(value) => value.as_u64().ok_or_else(|| {
|
||||||
ApiError::new(
|
ApiError::new(
|
||||||
ErrorCode::RPC_INVALID_PARAMS,
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
method,
|
||||||
"limit 必须是非负整数 JSON number",
|
"limit 必须是非负整数 JSON number",
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
@@ -4258,14 +4314,14 @@ fn translation_memory_rpc_limit(params: Option<&serde_json::Value>) -> Result<us
|
|||||||
let limit = usize::try_from(limit).map_err(|error| {
|
let limit = usize::try_from(limit).map_err(|error| {
|
||||||
ApiError::new(
|
ApiError::new(
|
||||||
ErrorCode::RPC_INVALID_PARAMS,
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
method,
|
||||||
format!("limit 无效:{error}"),
|
format!("limit 无效:{error}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if !(1..=1000).contains(&limit) {
|
if !(1..=1000).contains(&limit) {
|
||||||
return Err(ApiError::new(
|
return Err(ApiError::new(
|
||||||
ErrorCode::RPC_INVALID_PARAMS,
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
method,
|
||||||
"limit 必须在 1..=1000 范围内",
|
"limit 必须在 1..=1000 范围内",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -4285,13 +4341,8 @@ fn translation_memory_summary_rpc_report(
|
|||||||
params,
|
params,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
||||||
)?;
|
)?;
|
||||||
build_translation_memory_summary_report(&path).map_err(|error| {
|
build_translation_memory_summary_report(&path)
|
||||||
ApiError::new(
|
.map_err(|error| translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_SUMMARY, error))
|
||||||
ErrorCode::INTERNAL,
|
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
|
||||||
error.to_string(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translation_memory_query_rpc_report(
|
fn translation_memory_query_rpc_report(
|
||||||
@@ -4314,7 +4365,7 @@ fn translation_memory_query_rpc_report(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let context = translation_memory_rpc_context(params)?;
|
let context = translation_memory_rpc_context(params)?;
|
||||||
let limit = translation_memory_rpc_limit(params)?;
|
let limit = translation_memory_rpc_limit(params, RPC_METHOD_TRANSLATION_MEMORY_QUERY)?;
|
||||||
let path = translation_memory_rpc_path(
|
let path = translation_memory_rpc_path(
|
||||||
state_dir,
|
state_dir,
|
||||||
output_root,
|
output_root,
|
||||||
@@ -4322,13 +4373,8 @@ fn translation_memory_query_rpc_report(
|
|||||||
params,
|
params,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||||
)?;
|
)?;
|
||||||
build_translation_memory_query_report(&path, source_text, &context, limit).map_err(|error| {
|
build_translation_memory_query_report(&path, source_text, &context, limit)
|
||||||
ApiError::new(
|
.map_err(|error| translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_QUERY, error))
|
||||||
ErrorCode::INTERNAL,
|
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
|
||||||
error.to_string(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translation_memory_confirm_rpc_report(
|
fn translation_memory_confirm_rpc_report(
|
||||||
@@ -4370,6 +4416,13 @@ fn translation_memory_confirm_rpc_report(
|
|||||||
"reason",
|
"reason",
|
||||||
)?
|
)?
|
||||||
.map(str::to_string);
|
.map(str::to_string);
|
||||||
|
let supersede_record_id = translation_memory_rpc_string_param(
|
||||||
|
params,
|
||||||
|
&["supersede_record_id", "tm_supersede_record_id"],
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
||||||
|
"supersede_record_id",
|
||||||
|
)?
|
||||||
|
.map(str::to_string);
|
||||||
let path = translation_memory_rpc_path(
|
let path = translation_memory_rpc_path(
|
||||||
state_dir,
|
state_dir,
|
||||||
output_root,
|
output_root,
|
||||||
@@ -4377,12 +4430,127 @@ fn translation_memory_confirm_rpc_report(
|
|||||||
params,
|
params,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
||||||
)?;
|
)?;
|
||||||
build_translation_memory_confirm_report(&path, record_id, reviewer, reason).map_err(|error| {
|
build_translation_memory_confirm_report(
|
||||||
|
&path,
|
||||||
|
record_id,
|
||||||
|
reviewer,
|
||||||
|
reason,
|
||||||
|
supersede_record_id.as_deref(),
|
||||||
|
)
|
||||||
|
.map_err(|error| translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_CONFIRM, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translation_memory_conflicts_rpc_report(
|
||||||
|
state_dir: &Path,
|
||||||
|
output_root: &Path,
|
||||||
|
default_translation_memory_path: Option<&Path>,
|
||||||
|
params: Option<&serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, ApiError> {
|
||||||
|
let limit = translation_memory_rpc_limit(params, RPC_METHOD_TRANSLATION_MEMORY_CONFLICTS)?;
|
||||||
|
let path = translation_memory_rpc_path(
|
||||||
|
state_dir,
|
||||||
|
output_root,
|
||||||
|
default_translation_memory_path,
|
||||||
|
params,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_CONFLICTS,
|
||||||
|
)?;
|
||||||
|
build_translation_memory_conflicts_report(&path, limit).map_err(|error| {
|
||||||
|
translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_CONFLICTS, error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translation_memory_resolve_conflict_rpc_report(
|
||||||
|
state_dir: &Path,
|
||||||
|
output_root: &Path,
|
||||||
|
default_translation_memory_path: Option<&Path>,
|
||||||
|
params: Option<&serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, ApiError> {
|
||||||
|
let winner_record_id = translation_memory_rpc_string_param(
|
||||||
|
params,
|
||||||
|
&["winner_record_id", "record_id"],
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"winner_record_id",
|
||||||
|
)?
|
||||||
|
.ok_or_else(|| {
|
||||||
ApiError::new(
|
ApiError::new(
|
||||||
ErrorCode::INTERNAL,
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
error.to_string(),
|
"translation.memory.resolve_conflict 缺少 winner_record_id",
|
||||||
)
|
)
|
||||||
|
})?;
|
||||||
|
let reviewer = translation_memory_rpc_string_param(
|
||||||
|
params,
|
||||||
|
&["reviewer"],
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"reviewer",
|
||||||
|
)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::new(
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"translation.memory.resolve_conflict 缺少 reviewer",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let reason = translation_memory_rpc_string_param(
|
||||||
|
params,
|
||||||
|
&["reason"],
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"reason",
|
||||||
|
)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::new(
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"translation.memory.resolve_conflict 缺少 reason",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let expected = translation_memory_rpc_value(
|
||||||
|
params,
|
||||||
|
&["expected_trusted_record_ids"],
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::new(
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"translation.memory.resolve_conflict 缺少 expected_trusted_record_ids",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let expected = expected.as_array().ok_or_else(|| {
|
||||||
|
ApiError::new(
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"expected_trusted_record_ids 必须是 JSON array",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let expected = expected
|
||||||
|
.iter()
|
||||||
|
.map(|value| {
|
||||||
|
value.as_str().map(str::to_string).ok_or_else(|| {
|
||||||
|
ApiError::new(
|
||||||
|
ErrorCode::RPC_INVALID_PARAMS,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
"expected_trusted_record_ids 必须只包含字符串",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let path = translation_memory_rpc_path(
|
||||||
|
state_dir,
|
||||||
|
output_root,
|
||||||
|
default_translation_memory_path,
|
||||||
|
params,
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||||
|
)?;
|
||||||
|
build_translation_memory_resolve_conflict_report(
|
||||||
|
&path,
|
||||||
|
winner_record_id,
|
||||||
|
&expected,
|
||||||
|
reviewer,
|
||||||
|
reason,
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT, error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7350,10 +7518,20 @@ fn parse_args_with_env(
|
|||||||
Some(next_option_value(&mut args, &flag)?);
|
Some(next_option_value(&mut args, &flag)?);
|
||||||
options.translation_memory_command_option_explicit = true;
|
options.translation_memory_command_option_explicit = true;
|
||||||
}
|
}
|
||||||
"--tm-record-id" => {
|
"--tm-record-id" | "--tm-winner-record-id" => {
|
||||||
options.translation_memory_record_id = Some(next_option_value(&mut args, &flag)?);
|
options.translation_memory_record_id = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.translation_memory_command_option_explicit = true;
|
options.translation_memory_command_option_explicit = true;
|
||||||
}
|
}
|
||||||
|
"--tm-supersede-record-id" => {
|
||||||
|
options.translation_memory_supersede_record_id =
|
||||||
|
Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.translation_memory_command_option_explicit = true;
|
||||||
|
}
|
||||||
|
"--tm-expected-trusted-record-ids-json" => {
|
||||||
|
options.translation_memory_expected_trusted_record_ids_json =
|
||||||
|
Some(next_option_value(&mut args, &flag)?);
|
||||||
|
options.translation_memory_command_option_explicit = true;
|
||||||
|
}
|
||||||
"--tm-reviewer" => {
|
"--tm-reviewer" => {
|
||||||
options.translation_memory_reviewer = Some(next_option_value(&mut args, &flag)?);
|
options.translation_memory_reviewer = Some(next_option_value(&mut args, &flag)?);
|
||||||
options.translation_memory_command_option_explicit = true;
|
options.translation_memory_command_option_explicit = true;
|
||||||
@@ -7938,6 +8116,8 @@ fn parse_args_with_env(
|
|||||||
CliCommand::TranslationMemorySummary
|
CliCommand::TranslationMemorySummary
|
||||||
| CliCommand::TranslationMemoryQuery
|
| CliCommand::TranslationMemoryQuery
|
||||||
| CliCommand::TranslationMemoryConfirm
|
| CliCommand::TranslationMemoryConfirm
|
||||||
|
| CliCommand::TranslationMemoryConflicts
|
||||||
|
| CliCommand::TranslationMemoryResolveConflict
|
||||||
| CliCommand::Restart
|
| CliCommand::Restart
|
||||||
| CliCommand::Reload
|
| CliCommand::Reload
|
||||||
)
|
)
|
||||||
@@ -7981,6 +8161,8 @@ fn parse_args_with_env(
|
|||||||
| CliCommand::TranslationMemorySummary
|
| CliCommand::TranslationMemorySummary
|
||||||
| CliCommand::TranslationMemoryQuery
|
| CliCommand::TranslationMemoryQuery
|
||||||
| CliCommand::TranslationMemoryConfirm
|
| CliCommand::TranslationMemoryConfirm
|
||||||
|
| CliCommand::TranslationMemoryConflicts
|
||||||
|
| CliCommand::TranslationMemoryResolveConflict
|
||||||
| CliCommand::GlossarySummary
|
| CliCommand::GlossarySummary
|
||||||
| CliCommand::GlossaryQuery
|
| CliCommand::GlossaryQuery
|
||||||
| CliCommand::GlossaryAdd
|
| CliCommand::GlossaryAdd
|
||||||
@@ -8004,6 +8186,8 @@ fn parse_args_with_env(
|
|||||||
CliCommand::TranslationMemorySummary
|
CliCommand::TranslationMemorySummary
|
||||||
| CliCommand::TranslationMemoryQuery
|
| CliCommand::TranslationMemoryQuery
|
||||||
| CliCommand::TranslationMemoryConfirm
|
| CliCommand::TranslationMemoryConfirm
|
||||||
|
| CliCommand::TranslationMemoryConflicts
|
||||||
|
| CliCommand::TranslationMemoryResolveConflict
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -8044,7 +8228,9 @@ fn parse_args_with_env(
|
|||||||
}
|
}
|
||||||
CliCommand::TranslationMemorySummary
|
CliCommand::TranslationMemorySummary
|
||||||
| CliCommand::TranslationMemoryQuery
|
| CliCommand::TranslationMemoryQuery
|
||||||
| CliCommand::TranslationMemoryConfirm => {
|
| CliCommand::TranslationMemoryConfirm
|
||||||
|
| CliCommand::TranslationMemoryConflicts
|
||||||
|
| CliCommand::TranslationMemoryResolveConflict => {
|
||||||
if options.watch || options.daemon || options.daemon_child {
|
if options.watch || options.daemon || options.daemon_child {
|
||||||
return Err(anyhow::anyhow!("i18n memory 命令只支持单次执行或 RPC 调用"));
|
return Err(anyhow::anyhow!("i18n memory 命令只支持单次执行或 RPC 调用"));
|
||||||
}
|
}
|
||||||
@@ -8079,6 +8265,10 @@ fn parse_args_with_env(
|
|||||||
}
|
}
|
||||||
CliCommand::TranslationMemoryQuery => {
|
CliCommand::TranslationMemoryQuery => {
|
||||||
if options.translation_memory_record_id.is_some()
|
if options.translation_memory_record_id.is_some()
|
||||||
|
|| options.translation_memory_supersede_record_id.is_some()
|
||||||
|
|| options
|
||||||
|
.translation_memory_expected_trusted_record_ids_json
|
||||||
|
.is_some()
|
||||||
|| options.translation_memory_reviewer.is_some()
|
|| options.translation_memory_reviewer.is_some()
|
||||||
|| options.translation_memory_reason.is_some()
|
|| options.translation_memory_reason.is_some()
|
||||||
{
|
{
|
||||||
@@ -8088,11 +8278,38 @@ fn parse_args_with_env(
|
|||||||
CliCommand::TranslationMemoryConfirm => {
|
CliCommand::TranslationMemoryConfirm => {
|
||||||
if options.translation_memory_source_text.is_some()
|
if options.translation_memory_source_text.is_some()
|
||||||
|| options.translation_memory_context_json.is_some()
|
|| options.translation_memory_context_json.is_some()
|
||||||
|
|| options
|
||||||
|
.translation_memory_expected_trusted_record_ids_json
|
||||||
|
.is_some()
|
||||||
|| options.query_limit != 100
|
|| options.query_limit != 100
|
||||||
{
|
{
|
||||||
return Err(anyhow::anyhow!("i18n memory confirm 不接受 query 参数"));
|
return Err(anyhow::anyhow!("i18n memory confirm 不接受 query 参数"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CliCommand::TranslationMemoryConflicts => {
|
||||||
|
if options.translation_memory_source_text.is_some()
|
||||||
|
|| options.translation_memory_context_json.is_some()
|
||||||
|
|| options.translation_memory_record_id.is_some()
|
||||||
|
|| options.translation_memory_supersede_record_id.is_some()
|
||||||
|
|| options.translation_memory_reviewer.is_some()
|
||||||
|
|| options.translation_memory_reason.is_some()
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"i18n memory conflicts 不接受 confirm/query 参数"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CliCommand::TranslationMemoryResolveConflict => {
|
||||||
|
if options.translation_memory_source_text.is_some()
|
||||||
|
|| options.translation_memory_context_json.is_some()
|
||||||
|
|| options.translation_memory_supersede_record_id.is_some()
|
||||||
|
|| options.query_limit != 100
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"i18n memory resolve-conflict 不接受 query/supersede 参数"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
if matches!(options.command, CliCommand::TranslationMemoryQuery)
|
if matches!(options.command, CliCommand::TranslationMemoryQuery)
|
||||||
@@ -8110,6 +8327,20 @@ fn parse_args_with_env(
|
|||||||
"i18n memory confirm 必须指定 --tm-record-id 和 --tm-reviewer"
|
"i18n memory confirm 必须指定 --tm-record-id 和 --tm-reviewer"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if matches!(
|
||||||
|
options.command,
|
||||||
|
CliCommand::TranslationMemoryResolveConflict
|
||||||
|
) && (options.translation_memory_record_id.is_none()
|
||||||
|
|| options
|
||||||
|
.translation_memory_expected_trusted_record_ids_json
|
||||||
|
.is_none()
|
||||||
|
|| options.translation_memory_reviewer.is_none()
|
||||||
|
|| options.translation_memory_reason.is_none())
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"i18n memory resolve-conflict 必须指定 --tm-record-id(winner)、--tm-expected-trusted-record-ids-json、--tm-reviewer 和 --tm-reason"
|
||||||
|
));
|
||||||
|
}
|
||||||
options.progress = false;
|
options.progress = false;
|
||||||
options.banner = false;
|
options.banner = false;
|
||||||
}
|
}
|
||||||
@@ -8779,6 +9010,8 @@ fn parse_translation_memory_command(
|
|||||||
"summary" | "status" => CliCommand::TranslationMemorySummary,
|
"summary" | "status" => CliCommand::TranslationMemorySummary,
|
||||||
"query" | "find" => CliCommand::TranslationMemoryQuery,
|
"query" | "find" => CliCommand::TranslationMemoryQuery,
|
||||||
"confirm" | "trust" => CliCommand::TranslationMemoryConfirm,
|
"confirm" | "trust" => CliCommand::TranslationMemoryConfirm,
|
||||||
|
"conflicts" | "list-conflicts" => CliCommand::TranslationMemoryConflicts,
|
||||||
|
"resolve-conflict" | "resolve" => CliCommand::TranslationMemoryResolveConflict,
|
||||||
other => return Err(anyhow::anyhow!("未知 translation memory 二级命令:{other}")),
|
other => return Err(anyhow::anyhow!("未知 translation memory 二级命令:{other}")),
|
||||||
};
|
};
|
||||||
ensure_command_not_set(options.command, &format!("translation memory {action}"))?;
|
ensure_command_not_set(options.command, &format!("translation memory {action}"))?;
|
||||||
|
|||||||
@@ -260,6 +260,47 @@ fn translation_memory_commands_parse_and_validate() {
|
|||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(confirm.command, CliCommand::TranslationMemoryConfirm);
|
assert_eq!(confirm.command, CliCommand::TranslationMemoryConfirm);
|
||||||
|
let supersede = parse(&[
|
||||||
|
"bat",
|
||||||
|
"i18n",
|
||||||
|
"memory",
|
||||||
|
"confirm",
|
||||||
|
"--tm-record-id",
|
||||||
|
"tm-new",
|
||||||
|
"--tm-supersede-record-id",
|
||||||
|
"tm-old",
|
||||||
|
"--tm-reviewer",
|
||||||
|
"reviewer",
|
||||||
|
"--tm-reason",
|
||||||
|
"replacement",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(supersede.command, CliCommand::TranslationMemoryConfirm);
|
||||||
|
assert_eq!(
|
||||||
|
supersede.translation_memory_supersede_record_id.as_deref(),
|
||||||
|
Some("tm-old")
|
||||||
|
);
|
||||||
|
let conflicts = parse(&["bat", "i18n", "memory", "conflicts"]).unwrap();
|
||||||
|
assert_eq!(conflicts.command, CliCommand::TranslationMemoryConflicts);
|
||||||
|
let resolve = parse(&[
|
||||||
|
"bat",
|
||||||
|
"i18n",
|
||||||
|
"memory",
|
||||||
|
"resolve-conflict",
|
||||||
|
"--tm-record-id",
|
||||||
|
"tm-winner",
|
||||||
|
"--tm-expected-trusted-record-ids-json",
|
||||||
|
r#"["tm-old","tm-other"]"#,
|
||||||
|
"--tm-reviewer",
|
||||||
|
"reviewer",
|
||||||
|
"--tm-reason",
|
||||||
|
"selected",
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resolve.command,
|
||||||
|
CliCommand::TranslationMemoryResolveConflict
|
||||||
|
);
|
||||||
assert!(parse(&["bat", "i18n", "memory", "query"]).is_err());
|
assert!(parse(&["bat", "i18n", "memory", "query"]).is_err());
|
||||||
assert!(parse(&[
|
assert!(parse(&[
|
||||||
"bat",
|
"bat",
|
||||||
@@ -2914,6 +2955,19 @@ fn dispatch_translation_memory_rejects_invalid_params_with_stable_error_code() {
|
|||||||
"translation.memory.confirm",
|
"translation.memory.confirm",
|
||||||
Some(serde_json::json!({ "record_id": "tm-record", "reviewer": 42 })),
|
Some(serde_json::json!({ "record_id": "tm-record", "reviewer": 42 })),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"translation.memory.conflicts",
|
||||||
|
Some(serde_json::json!({ "limit": "1" })),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"translation.memory.resolve_conflict",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"winner_record_id": "tm-winner",
|
||||||
|
"expected_trusted_record_ids": "tm-old",
|
||||||
|
"reviewer": "reviewer",
|
||||||
|
"reason": "selected"
|
||||||
|
})),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let envelope = dispatch_rpc_method(
|
let envelope = dispatch_rpc_method(
|
||||||
&rpc_request(method, params),
|
&rpc_request(method, params),
|
||||||
|
|||||||
@@ -433,6 +433,10 @@ pub(super) fn run_translation_memory_command(options: &CliOptions) -> anyhow::Re
|
|||||||
CliCommand::TranslationMemorySummary => RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
CliCommand::TranslationMemorySummary => RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
||||||
CliCommand::TranslationMemoryQuery => RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
CliCommand::TranslationMemoryQuery => RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||||
CliCommand::TranslationMemoryConfirm => RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
CliCommand::TranslationMemoryConfirm => RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
||||||
|
CliCommand::TranslationMemoryConflicts => RPC_METHOD_TRANSLATION_MEMORY_CONFLICTS,
|
||||||
|
CliCommand::TranslationMemoryResolveConflict => {
|
||||||
|
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT
|
||||||
|
}
|
||||||
_ => return Err(anyhow::anyhow!("不是 Translation Memory 命令")),
|
_ => return Err(anyhow::anyhow!("不是 Translation Memory 命令")),
|
||||||
};
|
};
|
||||||
if daemon_rpc_available(&options.state_dir)
|
if daemon_rpc_available(&options.state_dir)
|
||||||
@@ -480,6 +484,34 @@ pub(super) fn run_translation_memory_command(options: &CliOptions) -> anyhow::Re
|
|||||||
record_id,
|
record_id,
|
||||||
reviewer,
|
reviewer,
|
||||||
options.translation_memory_reason.clone(),
|
options.translation_memory_reason.clone(),
|
||||||
|
options.translation_memory_supersede_record_id.as_deref(),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
CliCommand::TranslationMemoryConflicts => {
|
||||||
|
build_translation_memory_conflicts_report(&path, options.query_limit)?
|
||||||
|
}
|
||||||
|
CliCommand::TranslationMemoryResolveConflict => {
|
||||||
|
let winner = options
|
||||||
|
.translation_memory_record_id
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 winner record"))?;
|
||||||
|
let expected = options
|
||||||
|
.translation_memory_expected_trusted_record_ids_json
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 expected set"))?;
|
||||||
|
let expected = serde_json::from_str::<Vec<String>>(expected).map_err(|error| {
|
||||||
|
anyhow::anyhow!("expected trusted record IDs 必须是 JSON array:{error}")
|
||||||
|
})?;
|
||||||
|
let reviewer = options
|
||||||
|
.translation_memory_reviewer
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 reviewer"))?;
|
||||||
|
let reason = options
|
||||||
|
.translation_memory_reason
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 reason"))?;
|
||||||
|
build_translation_memory_resolve_conflict_report(
|
||||||
|
&path, winner, &expected, reviewer, reason,
|
||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -538,10 +570,49 @@ fn translation_memory_cli_params(
|
|||||||
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-reviewer"))?;
|
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-reviewer"))?;
|
||||||
params.insert("record_id".to_string(), serde_json::json!(record_id));
|
params.insert("record_id".to_string(), serde_json::json!(record_id));
|
||||||
params.insert("reviewer".to_string(), serde_json::json!(reviewer));
|
params.insert("reviewer".to_string(), serde_json::json!(reviewer));
|
||||||
|
if let Some(supersede_record_id) =
|
||||||
|
options.translation_memory_supersede_record_id.as_deref()
|
||||||
|
{
|
||||||
|
params.insert(
|
||||||
|
"supersede_record_id".to_string(),
|
||||||
|
serde_json::json!(supersede_record_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(reason) = options.translation_memory_reason.as_deref() {
|
if let Some(reason) = options.translation_memory_reason.as_deref() {
|
||||||
params.insert("reason".to_string(), serde_json::json!(reason));
|
params.insert("reason".to_string(), serde_json::json!(reason));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CliCommand::TranslationMemoryConflicts => {
|
||||||
|
params.insert("limit".to_string(), serde_json::json!(options.query_limit));
|
||||||
|
}
|
||||||
|
CliCommand::TranslationMemoryResolveConflict => {
|
||||||
|
let winner = options
|
||||||
|
.translation_memory_record_id
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 winner record"))?;
|
||||||
|
let expected = options
|
||||||
|
.translation_memory_expected_trusted_record_ids_json
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 expected set"))?;
|
||||||
|
let reviewer = options
|
||||||
|
.translation_memory_reviewer
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 reviewer"))?;
|
||||||
|
let reason = options
|
||||||
|
.translation_memory_reason
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("TM resolve-conflict 必须指定 reason"))?;
|
||||||
|
let expected = serde_json::from_str::<Vec<String>>(expected).map_err(|error| {
|
||||||
|
anyhow::anyhow!("expected trusted record IDs 必须是 JSON array:{error}")
|
||||||
|
})?;
|
||||||
|
params.insert("winner_record_id".to_string(), serde_json::json!(winner));
|
||||||
|
params.insert(
|
||||||
|
"expected_trusted_record_ids".to_string(),
|
||||||
|
serde_json::json!(expected),
|
||||||
|
);
|
||||||
|
params.insert("reviewer".to_string(), serde_json::json!(reviewer));
|
||||||
|
params.insert("reason".to_string(), serde_json::json!(reason));
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
Ok(Some(serde_json::Value::Object(params)))
|
Ok(Some(serde_json::Value::Object(params)))
|
||||||
@@ -625,6 +696,7 @@ pub(super) fn build_translation_memory_confirm_report(
|
|||||||
record_id: &str,
|
record_id: &str,
|
||||||
reviewer: &str,
|
reviewer: &str,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
|
supersede_record_id: Option<&str>,
|
||||||
) -> anyhow::Result<serde_json::Value> {
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
if record_id.trim().is_empty() || reviewer.trim().is_empty() {
|
if record_id.trim().is_empty() || reviewer.trim().is_empty() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -645,7 +717,89 @@ pub(super) fn build_translation_memory_confirm_report(
|
|||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||||
repository
|
repository
|
||||||
.confirm(record_id, reviewer, reason)
|
.confirm_with_supersede(record_id, reviewer, reason, supersede_record_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||||
|
})?;
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"available": true,
|
||||||
|
"path": path,
|
||||||
|
"entry": entry,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn build_translation_memory_conflicts_report(
|
||||||
|
path: &std::path::Path,
|
||||||
|
limit: usize,
|
||||||
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
|
if !(1..=1000).contains(&limit) {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"TM conflicts 的 limit 必须在 1..=1000 范围内"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !sqlite_file_exists_no_symlink(path, "Translation Memory 数据库")? {
|
||||||
|
return Ok(serde_json::json!({
|
||||||
|
"available": false,
|
||||||
|
"path": path,
|
||||||
|
"conflicts": [],
|
||||||
|
"reason": "database_missing",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?;
|
||||||
|
let conflicts = runtime.block_on(async {
|
||||||
|
let repository = bat_infrastructure::SqliteTranslationMemoryRepository::open(path)
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||||
|
repository
|
||||||
|
.list_conflicts(limit)
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||||
|
})?;
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"available": true,
|
||||||
|
"path": path,
|
||||||
|
"conflicts": conflicts,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn build_translation_memory_resolve_conflict_report(
|
||||||
|
path: &std::path::Path,
|
||||||
|
winner_record_id: &str,
|
||||||
|
expected_trusted_record_ids: &[String],
|
||||||
|
reviewer: &str,
|
||||||
|
reason: &str,
|
||||||
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
|
if winner_record_id.trim().is_empty()
|
||||||
|
|| reviewer.trim().is_empty()
|
||||||
|
|| reason.trim().is_empty()
|
||||||
|
|| expected_trusted_record_ids.is_empty()
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"TM resolve-conflict 必须指定 winner_record_id、expected_trusted_record_ids、reviewer 和 reason"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !sqlite_file_exists_no_symlink(path, "Translation Memory 数据库")? {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Translation Memory 数据库不存在:{}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?;
|
||||||
|
let entry = runtime.block_on(async {
|
||||||
|
let repository = bat_infrastructure::SqliteTranslationMemoryRepository::open(path)
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||||
|
repository
|
||||||
|
.resolve_conflict(
|
||||||
|
winner_record_id,
|
||||||
|
expected_trusted_record_ids,
|
||||||
|
reviewer,
|
||||||
|
reason,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Project-level Glossary V2 SQLite repository.
|
//! Project-level Glossary SQLite persistence schema V2 repository.
|
||||||
|
|
||||||
use crate::path_security::{
|
use crate::path_security::{
|
||||||
ensure_safe_directory_path, lexical_absolute, set_file_mode, STATE_FILE_MODE,
|
ensure_safe_directory_path, lexical_absolute, set_file_mode, STATE_FILE_MODE,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1522,6 +1522,7 @@ mod tests {
|
|||||||
OfficialTextUnitTask, OfficialTextUnitTaskQueue, OfficialTextUnitTaskStatus,
|
OfficialTextUnitTask, OfficialTextUnitTaskQueue, OfficialTextUnitTaskStatus,
|
||||||
OfficialTextUnitTaskSummary, OFFICIAL_TEXTUNIT_TASK_QUEUE_VERSION,
|
OfficialTextUnitTaskSummary, OFFICIAL_TEXTUNIT_TASK_QUEUE_VERSION,
|
||||||
};
|
};
|
||||||
|
use bat_core::domain::TranslationMemoryMatchKind;
|
||||||
|
|
||||||
fn fixture_root() -> (tempfile::TempDir, OfficialTextUnitTaskQueue) {
|
fn fixture_root() -> (tempfile::TempDir, OfficialTextUnitTaskQueue) {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
@@ -1917,6 +1918,137 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn worker_does_not_reuse_a_trusted_conflict() {
|
||||||
|
let (temp, queue) = fixture_root();
|
||||||
|
let textunit_index = index(temp.path());
|
||||||
|
crate::official_textunit_queue::write_textunit_task_queue_at(temp.path(), &queue).unwrap();
|
||||||
|
crate::official_parse::write_textunit_index_at(temp.path(), &textunit_index).unwrap();
|
||||||
|
|
||||||
|
let translation_memory_path = temp.path().join("translation-memory.sqlite");
|
||||||
|
let translation_memory = SqliteTranslationMemoryRepository::new(&translation_memory_path)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let unit = &textunit_index.units[0];
|
||||||
|
let source_context = translation_memory_context(
|
||||||
|
&unit.destination,
|
||||||
|
unit.archive_entry.as_deref(),
|
||||||
|
unit.serialized_file.as_deref(),
|
||||||
|
unit.path_id,
|
||||||
|
unit.class_id,
|
||||||
|
unit.field_path.as_deref(),
|
||||||
|
unit.format.as_deref(),
|
||||||
|
unit.asset_name.as_deref(),
|
||||||
|
unit.text_source_kind.as_deref(),
|
||||||
|
&unit.context,
|
||||||
|
);
|
||||||
|
let mut trusted_ids = Vec::new();
|
||||||
|
for (release, translated) in [("release-1", "旧译文一"), ("release-2", "旧译文二")]
|
||||||
|
{
|
||||||
|
let entry = translation_memory
|
||||||
|
.upsert_candidate(TranslationMemoryDraft {
|
||||||
|
source_text: unit.source_text.clone(),
|
||||||
|
source_context: source_context.clone(),
|
||||||
|
translated_text: translated.to_string(),
|
||||||
|
translation_source_kind: TranslationMemorySourceKind::Manual,
|
||||||
|
official_release_id: release.to_string(),
|
||||||
|
source_trace: TranslationMemorySourceTrace {
|
||||||
|
official_release_id: release.to_string(),
|
||||||
|
unit_id: Some(unit.id.clone()),
|
||||||
|
task_id: Some(queue.tasks[0].task_id.clone()),
|
||||||
|
destination: Some(unit.destination.clone()),
|
||||||
|
archive_entry: unit.archive_entry.clone(),
|
||||||
|
serialized_file: unit.serialized_file.clone(),
|
||||||
|
path_id: unit.path_id,
|
||||||
|
class_id: unit.class_id,
|
||||||
|
field_path: unit.field_path.clone(),
|
||||||
|
format: unit.format.clone(),
|
||||||
|
asset_name: unit.asset_name.clone(),
|
||||||
|
text_source_kind: unit.text_source_kind.clone(),
|
||||||
|
source_url: Some(unit.source_url.clone()),
|
||||||
|
},
|
||||||
|
provider: None,
|
||||||
|
provider_run_id: None,
|
||||||
|
observed_unix_seconds: 1,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE translation_memory
|
||||||
|
SET trust_status = 'trusted', trusted_unix_seconds = 100,
|
||||||
|
trusted_by = 'legacy-reviewer', trusted_reason = 'legacy fixture'
|
||||||
|
WHERE record_id = ?1",
|
||||||
|
)
|
||||||
|
.bind(&entry.record_id)
|
||||||
|
.execute(&translation_memory.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
trusted_ids.push(entry.record_id);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
translation_memory
|
||||||
|
.find_matches(&unit.source_text, &source_context, 10)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.match_kind == TranslationMemoryMatchKind::TrustedConflict)
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
let fixture = temp.path().join("conflict-mock.json");
|
||||||
|
std::fs::write(
|
||||||
|
&fixture,
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"schema_version": 1,
|
||||||
|
"translations": {
|
||||||
|
"direct:bundle#unit:0": "provider-after-conflict",
|
||||||
|
"direct:bundle#unit:1": "translated-by-provider"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let config = TranslationWorkerConfig {
|
||||||
|
fixture_path: Some(fixture),
|
||||||
|
translation_memory_path: Some(translation_memory_path),
|
||||||
|
concurrency: 1,
|
||||||
|
retry_backoff: Duration::ZERO,
|
||||||
|
..TranslationWorkerConfig::default()
|
||||||
|
};
|
||||||
|
let report = run_translation_worker_at(temp.path(), &config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(report.translation_memory_hit_count, 0);
|
||||||
|
assert_eq!(report.provider_unit_count, 2);
|
||||||
|
assert_eq!(report.completed_count, 1);
|
||||||
|
|
||||||
|
let task_repository = SqliteTranslationTaskRepository::open(
|
||||||
|
SqliteTranslationTaskRepository::repository_path(temp.path()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let task = task_repository.find(&queue.tasks[0].task_id).await.unwrap();
|
||||||
|
assert_eq!(task.translation_results.len(), 2);
|
||||||
|
assert!(task
|
||||||
|
.translation_results
|
||||||
|
.iter()
|
||||||
|
.all(|result| result.source_kind == TranslationTaskResultSourceKind::Provider));
|
||||||
|
assert!(task.translation_results[0]
|
||||||
|
.translation_memory_record_id
|
||||||
|
.is_none());
|
||||||
|
for record_id in trusted_ids {
|
||||||
|
assert_eq!(
|
||||||
|
translation_memory
|
||||||
|
.find(&record_id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.trust_status,
|
||||||
|
bat_core::domain::TranslationMemoryTrustStatus::Trusted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn worker_retains_tm_hits_when_provider_fails_for_remaining_units() {
|
async fn worker_retains_tm_hits_when_provider_fails_for_remaining_units() {
|
||||||
let (temp, queue) = fixture_root();
|
let (temp, queue) = fixture_root();
|
||||||
|
|||||||
+116
-4
@@ -56,6 +56,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/translation/handoff",
|
"/admin/translation/handoff",
|
||||||
"/admin/translation/memory/summary",
|
"/admin/translation/memory/summary",
|
||||||
"/admin/translation/memory/query",
|
"/admin/translation/memory/query",
|
||||||
|
"/admin/translation/memory/conflicts",
|
||||||
"/admin/translation/glossary/summary",
|
"/admin/translation/glossary/summary",
|
||||||
"/admin/translation/glossary/query",
|
"/admin/translation/glossary/query",
|
||||||
"/admin/translation/glossary/diagnose",
|
"/admin/translation/glossary/diagnose",
|
||||||
@@ -78,6 +79,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/control/translation-worker-run",
|
"/admin/control/translation-worker-run",
|
||||||
"/admin/control/translation-proofread",
|
"/admin/control/translation-proofread",
|
||||||
"/admin/control/translation-memory-confirm",
|
"/admin/control/translation-memory-confirm",
|
||||||
|
"/admin/control/translation-memory-resolve-conflict",
|
||||||
"/admin/control/translation-glossary-add",
|
"/admin/control/translation-glossary-add",
|
||||||
"/admin/control/translation-glossary-update",
|
"/admin/control/translation-glossary-update",
|
||||||
"/admin/control/translation-glossary-approve",
|
"/admin/control/translation-glossary-approve",
|
||||||
@@ -133,6 +135,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleAdminTranslationMemoryConfirm(w, r)
|
s.handleAdminTranslationMemoryConfirm(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if action == "translation-memory-resolve-conflict" {
|
||||||
|
s.handleAdminTranslationMemoryResolveConflict(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
if strings.HasPrefix(action, "translation-glossary-") {
|
if strings.HasPrefix(action, "translation-glossary-") {
|
||||||
s.handleAdminGlossaryControl(w, r, action)
|
s.handleAdminGlossaryControl(w, r, action)
|
||||||
return
|
return
|
||||||
@@ -222,6 +228,34 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAdminTranslationMemoryResolveConflict(w http.ResponseWriter, r *http.Request) {
|
||||||
|
backend, ok := s.backend.(TranslationMemoryBackend)
|
||||||
|
if !ok || backend == nil {
|
||||||
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var params backendrpc.TranslationMemoryResolveConflictParams
|
||||||
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateTranslationMemoryResolveConflictParams(params); err != nil {
|
||||||
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_params", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := backend.TranslationMemoryResolveConflict(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.writeControlBackendError(w, "translation-memory-resolve-conflict", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||||
|
Service: "bat-api",
|
||||||
|
Action: "translation-memory-resolve-conflict",
|
||||||
|
RPCMethod: "translation.memory.resolve_conflict",
|
||||||
|
Status: "accepted",
|
||||||
|
Result: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
backend, ok := s.backend.(TranslationBackend)
|
backend, ok := s.backend.(TranslationBackend)
|
||||||
if !ok || backend == nil {
|
if !ok || backend == nil {
|
||||||
@@ -1016,6 +1050,37 @@ func (s *Server) handleAdminTranslationMemoryQuery(w http.ResponseWriter, r *htt
|
|||||||
writeNoStoreJSON(w, http.StatusOK, result)
|
writeNoStoreJSON(w, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAdminTranslationMemoryConflicts(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")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.requireAdminToken(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backend, ok := s.backend.(TranslationMemoryBackend)
|
||||||
|
if !ok || backend == nil {
|
||||||
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params, err := translationMemoryConflictsParams(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_query", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := backend.TranslationMemoryConflicts(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.writeControlBackendError(w, "translation-memory-conflicts", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeNoStoreJSON(w, http.StatusOK, 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")
|
||||||
@@ -1139,6 +1204,20 @@ func translationMemoryQueryParams(r *http.Request) (backendrpc.TranslationMemory
|
|||||||
return params, nil
|
return params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func translationMemoryConflictsParams(r *http.Request) (backendrpc.TranslationMemoryConflictsParams, error) {
|
||||||
|
params := backendrpc.TranslationMemoryConflictsParams{
|
||||||
|
TranslationMemoryPath: firstTrimmedQuery(r.URL.Query(), "translation_memory_path", "tm_path"),
|
||||||
|
}
|
||||||
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||||
|
limit, err := strconv.ParseUint(raw, 10, 64)
|
||||||
|
if err != nil || limit == 0 || limit > 1000 {
|
||||||
|
return backendrpc.TranslationMemoryConflictsParams{}, errors.New("limit must be in 1..=1000")
|
||||||
|
}
|
||||||
|
params.Limit = &limit
|
||||||
|
}
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
|
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
params := backendrpc.TextUnitQueryParams{
|
params := backendrpc.TextUnitQueryParams{
|
||||||
@@ -1334,6 +1413,32 @@ func validateTranslationMemoryConfirmParams(params backendrpc.TranslationMemoryC
|
|||||||
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.Reviewer) == "" {
|
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.Reviewer) == "" {
|
||||||
return errors.New("translation memory confirm requires record_id and reviewer")
|
return errors.New("translation memory confirm requires record_id and reviewer")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(params.SupersedeRecordID) != "" && strings.TrimSpace(params.Reason) == "" {
|
||||||
|
return errors.New("translation memory supersede requires reason")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTranslationMemoryResolveConflictParams(params backendrpc.TranslationMemoryResolveConflictParams) error {
|
||||||
|
if strings.TrimSpace(params.WinnerRecordID) == "" ||
|
||||||
|
strings.TrimSpace(params.Reviewer) == "" ||
|
||||||
|
strings.TrimSpace(params.Reason) == "" {
|
||||||
|
return errors.New("translation memory conflict resolution requires winner_record_id, reviewer, and reason")
|
||||||
|
}
|
||||||
|
if len(params.ExpectedTrustedRecordIDs) == 0 {
|
||||||
|
return errors.New("translation memory conflict resolution requires expected_trusted_record_ids")
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(params.ExpectedTrustedRecordIDs))
|
||||||
|
for _, id := range params.ExpectedTrustedRecordIDs {
|
||||||
|
id = strings.TrimSpace(id)
|
||||||
|
if id == "" {
|
||||||
|
return errors.New("expected_trusted_record_ids cannot contain empty record IDs")
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
return errors.New("expected_trusted_record_ids must be unique")
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1412,10 +1517,17 @@ func (s *Server) writeControlBackendError(w http.ResponseWriter, action string,
|
|||||||
message = "control request was canceled"
|
message = "control request was canceled"
|
||||||
default:
|
default:
|
||||||
var apiErr *backendrpc.APIError
|
var apiErr *backendrpc.APIError
|
||||||
if errors.As(err, &apiErr) && apiErr.Kind == "not_implemented" {
|
if errors.As(err, &apiErr) {
|
||||||
status = http.StatusNotImplemented
|
switch apiErr.Kind {
|
||||||
code = "control_not_implemented"
|
case "not_implemented":
|
||||||
message = "Rust bat does not implement this control action"
|
status = http.StatusNotImplemented
|
||||||
|
code = "control_not_implemented"
|
||||||
|
message = "Rust bat does not implement this control action"
|
||||||
|
case "rpc_invalid_params":
|
||||||
|
status = http.StatusBadRequest
|
||||||
|
code = "invalid_control_params"
|
||||||
|
message = apiErr.Message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Printf("bat-api control action=%s error=%v", action, err)
|
s.logger.Printf("bat-api control action=%s error=%v", action, err)
|
||||||
|
|||||||
+76
-24
@@ -1023,22 +1023,24 @@ func (f *fakeBackend) TaskCancel(ctx context.Context, taskID string) (*backendrp
|
|||||||
|
|
||||||
type controlBackend struct {
|
type controlBackend struct {
|
||||||
*fakeBackend
|
*fakeBackend
|
||||||
calls []string
|
calls []string
|
||||||
parseTextUnitQueries []backendrpc.TextUnitQueryParams
|
parseTextUnitQueries []backendrpc.TextUnitQueryParams
|
||||||
parseErrorQueries []backendrpc.TextUnitQueryParams
|
parseErrorQueries []backendrpc.TextUnitQueryParams
|
||||||
translationTaskUpdates []backendrpc.TranslationTaskUpdateParams
|
translationTaskUpdates []backendrpc.TranslationTaskUpdateParams
|
||||||
translationTaskListParams []backendrpc.TranslationTaskListParams
|
translationTaskListParams []backendrpc.TranslationTaskListParams
|
||||||
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
|
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
|
||||||
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
|
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
|
||||||
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
|
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
|
||||||
glossarySummaryParams []backendrpc.GlossarySummaryParams
|
translationMemoryConflictsParams []backendrpc.TranslationMemoryConflictsParams
|
||||||
glossaryQueryParams []backendrpc.GlossaryQueryParams
|
translationMemoryResolveParams []backendrpc.TranslationMemoryResolveConflictParams
|
||||||
glossaryDiagnoseParams []backendrpc.GlossaryDiagnoseParams
|
glossarySummaryParams []backendrpc.GlossarySummaryParams
|
||||||
glossaryMutationParams []backendrpc.GlossaryTermMutationParams
|
glossaryQueryParams []backendrpc.GlossaryQueryParams
|
||||||
glossaryReviewParams []backendrpc.GlossaryReviewParams
|
glossaryDiagnoseParams []backendrpc.GlossaryDiagnoseParams
|
||||||
glossaryDeleteParams []backendrpc.GlossaryDeleteParams
|
glossaryMutationParams []backendrpc.GlossaryTermMutationParams
|
||||||
localizedPublishParams []backendrpc.LocalizedPublishParams
|
glossaryReviewParams []backendrpc.GlossaryReviewParams
|
||||||
localizedRollbackParams []backendrpc.LocalizedRollbackParams
|
glossaryDeleteParams []backendrpc.GlossaryDeleteParams
|
||||||
|
localizedPublishParams []backendrpc.LocalizedPublishParams
|
||||||
|
localizedRollbackParams []backendrpc.LocalizedRollbackParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
|
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
|
||||||
@@ -1183,18 +1185,19 @@ func (b *controlBackend) TranslationProofread(ctx context.Context) (json.RawMess
|
|||||||
func (b *controlBackend) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
|
func (b *controlBackend) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
|
||||||
b.calls = append(b.calls, "translation.memory.summary")
|
b.calls = append(b.calls, "translation.memory.summary")
|
||||||
b.translationMemorySummaryParams = append(b.translationMemorySummaryParams, params)
|
b.translationMemorySummaryParams = append(b.translationMemorySummaryParams, params)
|
||||||
schemaVersion := uint64(1)
|
schemaVersion := uint64(2)
|
||||||
return &backendrpc.TranslationMemorySummaryReport{
|
return &backendrpc.TranslationMemorySummaryReport{
|
||||||
Available: true,
|
Available: true,
|
||||||
Path: params.TranslationMemoryPath,
|
Path: params.TranslationMemoryPath,
|
||||||
SchemaVersion: &schemaVersion,
|
SchemaVersion: &schemaVersion,
|
||||||
Summary: &backendrpc.TranslationMemorySummary{
|
Summary: &backendrpc.TranslationMemorySummary{
|
||||||
SchemaVersion: schemaVersion,
|
SchemaVersion: schemaVersion,
|
||||||
RecordCount: 2,
|
RecordCount: 2,
|
||||||
TrustedCount: 1,
|
TrustedCount: 1,
|
||||||
CandidateCount: 1,
|
CandidateCount: 1,
|
||||||
SupersededCount: 0,
|
SupersededCount: 0,
|
||||||
RejectedCount: 0,
|
RejectedCount: 0,
|
||||||
|
CurrentTrustedCount: 1,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -1225,10 +1228,33 @@ func (b *controlBackend) TranslationMemoryConfirm(ctx context.Context, params ba
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *controlBackend) TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error) {
|
||||||
|
b.calls = append(b.calls, "translation.memory.conflicts")
|
||||||
|
b.translationMemoryConflictsParams = append(b.translationMemoryConflictsParams, params)
|
||||||
|
return &backendrpc.TranslationMemoryConflictsReport{
|
||||||
|
Available: true,
|
||||||
|
Path: params.TranslationMemoryPath,
|
||||||
|
Conflicts: []backendrpc.TranslationMemoryConflict{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *controlBackend) TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error) {
|
||||||
|
b.calls = append(b.calls, "translation.memory.resolve_conflict")
|
||||||
|
b.translationMemoryResolveParams = append(b.translationMemoryResolveParams, params)
|
||||||
|
return &backendrpc.TranslationMemoryResolveConflictReport{
|
||||||
|
Available: true,
|
||||||
|
Path: params.TranslationMemoryPath,
|
||||||
|
Entry: backendrpc.TranslationMemoryEntry{
|
||||||
|
RecordID: params.WinnerRecordID,
|
||||||
|
TrustStatus: backendrpc.TranslationMemoryStatusTrusted,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *controlBackend) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
|
func (b *controlBackend) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
|
||||||
b.calls = append(b.calls, "translation.glossary.summary")
|
b.calls = append(b.calls, "translation.glossary.summary")
|
||||||
b.glossarySummaryParams = append(b.glossarySummaryParams, params)
|
b.glossarySummaryParams = append(b.glossarySummaryParams, params)
|
||||||
schemaVersion := uint64(1)
|
schemaVersion := uint64(2)
|
||||||
return &backendrpc.GlossarySummaryReport{
|
return &backendrpc.GlossarySummaryReport{
|
||||||
Available: true,
|
Available: true,
|
||||||
Path: params.GlossaryPath,
|
Path: params.GlossaryPath,
|
||||||
@@ -1525,6 +1551,18 @@ func TestAdminTranslationQueryEndpointsProxyAuthenticatedRequests(t *testing.T)
|
|||||||
t.Fatalf("missing TM query source status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("missing TM query source status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request = httptest.NewRequest(http.MethodGet, "/admin/translation/memory/conflicts?translation_memory_path=%2Fvar%2Flib%2Fbat%2Ftranslation-memory.sqlite&limit=7", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer translation-token")
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK ||
|
||||||
|
len(backend.translationMemoryConflictsParams) != 1 ||
|
||||||
|
backend.translationMemoryConflictsParams[0].TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" ||
|
||||||
|
backend.translationMemoryConflictsParams[0].Limit == nil ||
|
||||||
|
*backend.translationMemoryConflictsParams[0].Limit != 7 {
|
||||||
|
t.Fatalf("TM conflicts status=%d body=%s params=%#v", recorder.Code, recorder.Body.String(), backend.translationMemoryConflictsParams)
|
||||||
|
}
|
||||||
|
|
||||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/summary?glossary_path=%2Fvar%2Flib%2Fbat%2Fglossary.sqlite", nil)
|
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/summary?glossary_path=%2Fvar%2Flib%2Fbat%2Fglossary.sqlite", nil)
|
||||||
request.Header.Set("Authorization", "Bearer translation-token")
|
request.Header.Set("Authorization", "Bearer translation-token")
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
@@ -2345,6 +2383,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
{name: "translation worker run", action: "translation-worker-run", body: `{"provider":"mock","concurrency":8,"max_tasks":2,"retry_backoff_seconds":0,"worker_id":"dashboard-worker"}`, rpcMethod: "translation.worker.run", call: "translation.worker.run"},
|
{name: "translation worker run", action: "translation-worker-run", body: `{"provider":"mock","concurrency":8,"max_tasks":2,"retry_backoff_seconds":0,"worker_id":"dashboard-worker"}`, rpcMethod: "translation.worker.run", call: "translation.worker.run"},
|
||||||
{name: "translation proofread", action: "translation-proofread", rpcMethod: "translation.proofread", call: "translation.proofread"},
|
{name: "translation proofread", action: "translation-proofread", rpcMethod: "translation.proofread", call: "translation.proofread"},
|
||||||
{name: "translation memory confirm", action: "translation-memory-confirm", body: `{"record_id":"tm-record-1","reviewer":"reviewer","reason":"reviewed"}`, rpcMethod: "translation.memory.confirm", call: "translation.memory.confirm"},
|
{name: "translation memory confirm", action: "translation-memory-confirm", body: `{"record_id":"tm-record-1","reviewer":"reviewer","reason":"reviewed"}`, rpcMethod: "translation.memory.confirm", call: "translation.memory.confirm"},
|
||||||
|
{name: "translation memory resolve conflict", action: "translation-memory-resolve-conflict", body: `{"winner_record_id":"tm-record-1","expected_trusted_record_ids":["tm-record-1","tm-record-2"],"reviewer":"reviewer","reason":"selected"}`, rpcMethod: "translation.memory.resolve_conflict", call: "translation.memory.resolve_conflict"},
|
||||||
{name: "translation glossary add", action: "translation-glossary-add", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.add", call: "translation.glossary.add"},
|
{name: "translation glossary add", action: "translation-glossary-add", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.add", call: "translation.glossary.add"},
|
||||||
{name: "translation glossary update", action: "translation-glossary-update", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","reviewer":"reviewer","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.update", call: "translation.glossary.update"},
|
{name: "translation glossary update", action: "translation-glossary-update", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","reviewer":"reviewer","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.update", call: "translation.glossary.update"},
|
||||||
{name: "translation glossary approve", action: "translation-glossary-approve", body: `{"term_id":"term-sensei","reviewer":"reviewer","reason":"approved"}`, rpcMethod: "translation.glossary.approve", call: "translation.glossary.approve"},
|
{name: "translation glossary approve", action: "translation-glossary-approve", body: `{"term_id":"term-sensei","reviewer":"reviewer","reason":"approved"}`, rpcMethod: "translation.glossary.approve", call: "translation.glossary.approve"},
|
||||||
@@ -2390,6 +2429,11 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
backend.translationMemoryConfirmParams[0].Reviewer != "reviewer" {
|
backend.translationMemoryConfirmParams[0].Reviewer != "reviewer" {
|
||||||
t.Fatalf("TM confirm params=%#v", backend.translationMemoryConfirmParams)
|
t.Fatalf("TM confirm params=%#v", backend.translationMemoryConfirmParams)
|
||||||
}
|
}
|
||||||
|
if len(backend.translationMemoryResolveParams) != 1 ||
|
||||||
|
backend.translationMemoryResolveParams[0].WinnerRecordID != "tm-record-1" ||
|
||||||
|
len(backend.translationMemoryResolveParams[0].ExpectedTrustedRecordIDs) != 2 {
|
||||||
|
t.Fatalf("TM resolve params=%#v", backend.translationMemoryResolveParams)
|
||||||
|
}
|
||||||
if len(backend.glossaryMutationParams) != 2 ||
|
if len(backend.glossaryMutationParams) != 2 ||
|
||||||
backend.glossaryMutationParams[0].TermID != "term-sensei" ||
|
backend.glossaryMutationParams[0].TermID != "term-sensei" ||
|
||||||
backend.glossaryMutationParams[1].Reviewer != "reviewer" ||
|
backend.glossaryMutationParams[1].Reviewer != "reviewer" ||
|
||||||
@@ -2432,6 +2476,14 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
t.Fatalf("invalid TM confirm status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("invalid TM confirm status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request = httptest.NewRequest(http.MethodPost, "/admin/control/translation-memory-resolve-conflict", strings.NewReader(`{"winner_record_id":"tm-record-1","expected_trusted_record_ids":["tm-record-1","tm-record-1"],"reviewer":"reviewer","reason":"selected"}`))
|
||||||
|
request.Header.Set("Authorization", "Bearer control-token")
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("invalid TM resolve status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
request = httptest.NewRequest(http.MethodPost, "/admin/control/localized-publish", strings.NewReader(`{"from_worker":true,"translation_file":"/tmp/workbench.json"}`))
|
request = httptest.NewRequest(http.MethodPost, "/admin/control/localized-publish", strings.NewReader(`{"from_worker":true,"translation_file":"/tmp/workbench.json"}`))
|
||||||
request.Header.Set("Authorization", "Bearer control-token")
|
request.Header.Set("Authorization", "Bearer control-token")
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
|
|||||||
+38
-1
@@ -512,6 +512,31 @@ paths:
|
|||||||
description: Missing or invalid admin token.
|
description: Missing or invalid admin token.
|
||||||
"503":
|
"503":
|
||||||
description: Rust bat Translation Memory backend is unavailable.
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
|
/admin/translation/memory/conflicts:
|
||||||
|
get:
|
||||||
|
summary: List Rust-owned Translation Memory Trusted conflicts
|
||||||
|
parameters:
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 1
|
||||||
|
maximum: 1000
|
||||||
|
default: 100
|
||||||
|
- name: translation_memory_path
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Translation Memory exact-identity Trusted conflict groups.
|
||||||
|
"400":
|
||||||
|
description: Invalid conflict list limit.
|
||||||
|
"401":
|
||||||
|
description: Missing or invalid admin token.
|
||||||
|
"503":
|
||||||
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
/admin/translation/glossary/summary:
|
/admin/translation/glossary/summary:
|
||||||
get:
|
get:
|
||||||
summary: Read Rust-owned Glossary summary
|
summary: Read Rust-owned Glossary summary
|
||||||
@@ -637,7 +662,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, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, translation-glossary-delete, localized-publish, localized-rollback, release-cleanup]
|
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, translation-memory-resolve-conflict, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, translation-glossary-delete, localized-publish, localized-rollback, release-cleanup]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
content:
|
content:
|
||||||
@@ -752,6 +777,18 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
record_id:
|
record_id:
|
||||||
type: string
|
type: string
|
||||||
|
winner_record_id:
|
||||||
|
type: string
|
||||||
|
expected_trusted_record_ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
supersede_record_id:
|
||||||
|
type: string
|
||||||
|
reviewer:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
term_id:
|
term_id:
|
||||||
type: string
|
type: string
|
||||||
source_term:
|
source_term:
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ type TranslationMemoryBackend interface {
|
|||||||
TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error)
|
TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error)
|
||||||
TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error)
|
TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error)
|
||||||
TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error)
|
TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error)
|
||||||
|
TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error)
|
||||||
|
TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GlossaryBackend exposes Rust-owned Glossary management and diagnostics.
|
// GlossaryBackend exposes Rust-owned Glossary management and diagnostics.
|
||||||
@@ -238,6 +240,14 @@ func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendr
|
|||||||
return r.Client.TranslationMemoryConfirm(ctx, params)
|
return r.Client.TranslationMemoryConfirm(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r RPCClient) TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error) {
|
||||||
|
return r.Client.TranslationMemoryConflicts(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RPCClient) TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error) {
|
||||||
|
return r.Client.TranslationMemoryResolveConflict(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
func (r RPCClient) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
|
func (r RPCClient) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
|
||||||
return r.Client.GlossarySummary(ctx, params)
|
return r.Client.GlossarySummary(ctx, params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.HandleFunc("/admin/translation/handoff", s.handleAdminTranslationHandoff)
|
mux.HandleFunc("/admin/translation/handoff", s.handleAdminTranslationHandoff)
|
||||||
mux.HandleFunc("/admin/translation/memory/summary", s.handleAdminTranslationMemorySummary)
|
mux.HandleFunc("/admin/translation/memory/summary", s.handleAdminTranslationMemorySummary)
|
||||||
mux.HandleFunc("/admin/translation/memory/query", s.handleAdminTranslationMemoryQuery)
|
mux.HandleFunc("/admin/translation/memory/query", s.handleAdminTranslationMemoryQuery)
|
||||||
|
mux.HandleFunc("/admin/translation/memory/conflicts", s.handleAdminTranslationMemoryConflicts)
|
||||||
mux.HandleFunc("/admin/translation/glossary/summary", s.handleAdminGlossarySummary)
|
mux.HandleFunc("/admin/translation/glossary/summary", s.handleAdminGlossarySummary)
|
||||||
mux.HandleFunc("/admin/translation/glossary/query", s.handleAdminGlossaryQuery)
|
mux.HandleFunc("/admin/translation/glossary/query", s.handleAdminGlossaryQuery)
|
||||||
mux.HandleFunc("/admin/translation/glossary/diagnose", s.handleAdminGlossaryDiagnose)
|
mux.HandleFunc("/admin/translation/glossary/diagnose", s.handleAdminGlossaryDiagnose)
|
||||||
@@ -202,6 +203,7 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/translation/handoff",
|
"/admin/translation/handoff",
|
||||||
"/admin/translation/memory/summary",
|
"/admin/translation/memory/summary",
|
||||||
"/admin/translation/memory/query",
|
"/admin/translation/memory/query",
|
||||||
|
"/admin/translation/memory/conflicts",
|
||||||
"/admin/translation/glossary/summary",
|
"/admin/translation/glossary/summary",
|
||||||
"/admin/translation/glossary/query",
|
"/admin/translation/glossary/query",
|
||||||
"/admin/translation/glossary/diagnose",
|
"/admin/translation/glossary/diagnose",
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本
|
|||||||
window 为 7260 秒(默认 3600 秒验证周期和 60 秒失败重试周期)。
|
window 为 7260 秒(默认 3600 秒验证周期和 60 秒失败重试周期)。
|
||||||
- 对应 release 的 `official-sync-snapshot.json`。
|
- 对应 release 的 `official-sync-snapshot.json`。
|
||||||
- `launcher_metadata` 与 `game_main_config_bootstrap` 的 Go mirror 解码。
|
- `launcher_metadata` 与 `game_main_config_bootstrap` 的 Go mirror 解码。
|
||||||
- Rust Glossary V2 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
|
- Rust Glossary domain/feature contract V1、SQLite persistence schema V2 的 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
|
||||||
|
|
||||||
这些 fixture 只用于 schema / mirror 回归,不代表真实资源版本,也不替代 live
|
这些 fixture 只用于 schema / mirror 回归,不代表真实资源版本,也不替代 live
|
||||||
daemon socket 或完整发布切换验证。
|
daemon socket 或完整发布切换验证。
|
||||||
|
|||||||
@@ -479,9 +479,10 @@ const (
|
|||||||
type TranslationMemoryMatchKind string
|
type TranslationMemoryMatchKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TranslationMemoryMatchStrongExact TranslationMemoryMatchKind = "strong_exact"
|
TranslationMemoryMatchStrongExact TranslationMemoryMatchKind = "strong_exact"
|
||||||
TranslationMemoryMatchCandidateExact TranslationMemoryMatchKind = "candidate_exact"
|
TranslationMemoryMatchTrustedConflict TranslationMemoryMatchKind = "trusted_conflict"
|
||||||
TranslationMemoryMatchSourceOnly TranslationMemoryMatchKind = "source_only"
|
TranslationMemoryMatchCandidateExact TranslationMemoryMatchKind = "candidate_exact"
|
||||||
|
TranslationMemoryMatchSourceOnly TranslationMemoryMatchKind = "source_only"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TranslationMemorySummaryParams selects an optional Rust-owned TM database.
|
// TranslationMemorySummaryParams selects an optional Rust-owned TM database.
|
||||||
@@ -504,16 +505,19 @@ type TranslationMemoryConfirmParams struct {
|
|||||||
RecordID string `json:"record_id"`
|
RecordID string `json:"record_id"`
|
||||||
Reviewer string `json:"reviewer"`
|
Reviewer string `json:"reviewer"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
|
SupersedeRecordID string `json:"supersede_record_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationMemorySummary mirrors translation.memory.summary data.
|
// TranslationMemorySummary mirrors translation.memory.summary data.
|
||||||
type TranslationMemorySummary struct {
|
type TranslationMemorySummary struct {
|
||||||
SchemaVersion uint64 `json:"schema_version"`
|
SchemaVersion uint64 `json:"schema_version"`
|
||||||
RecordCount uint64 `json:"record_count"`
|
RecordCount uint64 `json:"record_count"`
|
||||||
TrustedCount uint64 `json:"trusted_count"`
|
TrustedCount uint64 `json:"trusted_count"`
|
||||||
CandidateCount uint64 `json:"candidate_count"`
|
CandidateCount uint64 `json:"candidate_count"`
|
||||||
SupersededCount uint64 `json:"superseded_count"`
|
SupersededCount uint64 `json:"superseded_count"`
|
||||||
RejectedCount uint64 `json:"rejected_count"`
|
RejectedCount uint64 `json:"rejected_count"`
|
||||||
|
TrustedConflictGroupCount uint64 `json:"trusted_conflict_group_count"`
|
||||||
|
CurrentTrustedCount uint64 `json:"current_trusted_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationMemorySummaryReport distinguishes a missing database from an
|
// TranslationMemorySummaryReport distinguishes a missing database from an
|
||||||
@@ -591,6 +595,47 @@ type TranslationMemoryConfirmReport struct {
|
|||||||
Entry TranslationMemoryEntry `json:"entry"`
|
Entry TranslationMemoryEntry `json:"entry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TranslationMemoryConflict groups all records sharing one exact source
|
||||||
|
// identity when more than one current Trusted record exists.
|
||||||
|
type TranslationMemoryConflict struct {
|
||||||
|
SourceText string `json:"source_text"`
|
||||||
|
SourceHash string `json:"source_hash"`
|
||||||
|
SourceContext TranslationMemoryContext `json:"source_context,omitempty"`
|
||||||
|
SourceContextHash string `json:"source_context_hash"`
|
||||||
|
TrustedRecordIDs []string `json:"trusted_record_ids"`
|
||||||
|
Records []TranslationMemoryEntry `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationMemoryConflictsParams lists current Trusted conflicts.
|
||||||
|
type TranslationMemoryConflictsParams struct {
|
||||||
|
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||||
|
Limit *uint64 `json:"limit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationMemoryConflictsReport mirrors translation.memory.conflicts data.
|
||||||
|
type TranslationMemoryConflictsReport struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Conflicts []TranslationMemoryConflict `json:"conflicts"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationMemoryResolveConflictParams resolves one exact-identity conflict.
|
||||||
|
type TranslationMemoryResolveConflictParams struct {
|
||||||
|
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||||
|
WinnerRecordID string `json:"winner_record_id"`
|
||||||
|
ExpectedTrustedRecordIDs []string `json:"expected_trusted_record_ids"`
|
||||||
|
Reviewer string `json:"reviewer"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationMemoryResolveConflictReport mirrors the conflict resolution result.
|
||||||
|
type TranslationMemoryResolveConflictReport struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Entry TranslationMemoryEntry `json:"entry"`
|
||||||
|
}
|
||||||
|
|
||||||
// GlossaryReviewStatus is the Rust-owned term review state.
|
// GlossaryReviewStatus is the Rust-owned term review state.
|
||||||
type GlossaryReviewStatus string
|
type GlossaryReviewStatus string
|
||||||
|
|
||||||
@@ -1067,6 +1112,18 @@ func (c *Client) TranslationMemoryConfirm(ctx context.Context, params Translatio
|
|||||||
return &out, err
|
return &out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) TranslationMemoryConflicts(ctx context.Context, params TranslationMemoryConflictsParams) (*TranslationMemoryConflictsReport, error) {
|
||||||
|
var out TranslationMemoryConflictsReport
|
||||||
|
_, err := c.Call(ctx, "translation.memory.conflicts", params, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) TranslationMemoryResolveConflict(ctx context.Context, params TranslationMemoryResolveConflictParams) (*TranslationMemoryResolveConflictReport, error) {
|
||||||
|
var out TranslationMemoryResolveConflictReport
|
||||||
|
_, err := c.Call(ctx, "translation.memory.resolve_conflict", params, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) GlossarySummary(ctx context.Context, params GlossarySummaryParams) (*GlossarySummaryReport, error) {
|
func (c *Client) GlossarySummary(ctx context.Context, params GlossarySummaryParams) (*GlossarySummaryReport, error) {
|
||||||
var out GlossarySummaryReport
|
var out GlossarySummaryReport
|
||||||
_, err := c.Call(ctx, "translation.glossary.summary", params, &out)
|
_, err := c.Call(ctx, "translation.glossary.summary", params, &out)
|
||||||
|
|||||||
@@ -756,14 +756,15 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
|
|||||||
Data: map[string]any{
|
Data: map[string]any{
|
||||||
"available": true,
|
"available": true,
|
||||||
"path": "/var/lib/bat/translation-memory.sqlite",
|
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"summary": map[string]any{
|
"summary": map[string]any{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"record_count": 3,
|
"record_count": 3,
|
||||||
"trusted_count": 1,
|
"trusted_count": 1,
|
||||||
"candidate_count": 1,
|
"candidate_count": 1,
|
||||||
"superseded_count": 1,
|
"superseded_count": 1,
|
||||||
"rejected_count": 0,
|
"rejected_count": 0,
|
||||||
|
"current_trusted_count": 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -859,6 +860,50 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
case "translation.memory.conflicts":
|
||||||
|
var params TranslationMemoryConflictsParams
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
t.Fatalf("decode conflicts params: %v", err)
|
||||||
|
}
|
||||||
|
if params.TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" ||
|
||||||
|
params.Limit == nil || *params.Limit != limit {
|
||||||
|
t.Fatalf("conflicts params=%#v", params)
|
||||||
|
}
|
||||||
|
return testResponse{
|
||||||
|
Result: testEnvelope{
|
||||||
|
OK: true, Status: "ok", RequestID: "req-tm-conflicts",
|
||||||
|
Data: map[string]any{
|
||||||
|
"available": true,
|
||||||
|
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||||
|
"conflicts": []any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case "translation.memory.resolve_conflict":
|
||||||
|
var params TranslationMemoryResolveConflictParams
|
||||||
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||||
|
t.Fatalf("decode resolve params: %v", err)
|
||||||
|
}
|
||||||
|
if params.WinnerRecordID != "tm-record-1" ||
|
||||||
|
len(params.ExpectedTrustedRecordIDs) != 2 ||
|
||||||
|
params.ExpectedTrustedRecordIDs[1] != "tm-record-2" ||
|
||||||
|
params.Reviewer != "reviewer" ||
|
||||||
|
params.Reason != "selected" {
|
||||||
|
t.Fatalf("resolve params=%#v", params)
|
||||||
|
}
|
||||||
|
return testResponse{
|
||||||
|
Result: testEnvelope{
|
||||||
|
OK: true, Status: "ok", RequestID: "req-tm-resolve",
|
||||||
|
Data: map[string]any{
|
||||||
|
"available": true,
|
||||||
|
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||||
|
"entry": map[string]any{
|
||||||
|
"record_id": "tm-record-1",
|
||||||
|
"trust_status": "trusted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected method %q", req.Method)
|
t.Fatalf("unexpected method %q", req.Method)
|
||||||
return testResponse{}
|
return testResponse{}
|
||||||
@@ -915,6 +960,32 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
|
|||||||
confirmed.Entry.RecordID != "tm-record-1" {
|
confirmed.Entry.RecordID != "tm-record-1" {
|
||||||
t.Fatalf("confirmed=%#v", confirmed)
|
t.Fatalf("confirmed=%#v", confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conflicts, err := client.TranslationMemoryConflicts(context.Background(), TranslationMemoryConflictsParams{
|
||||||
|
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
||||||
|
Limit: &limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TranslationMemoryConflicts error: %v", err)
|
||||||
|
}
|
||||||
|
if !conflicts.Available || len(conflicts.Conflicts) != 0 {
|
||||||
|
t.Fatalf("conflicts=%#v", conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := client.TranslationMemoryResolveConflict(context.Background(), TranslationMemoryResolveConflictParams{
|
||||||
|
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
||||||
|
WinnerRecordID: "tm-record-1",
|
||||||
|
ExpectedTrustedRecordIDs: []string{"tm-record-1", "tm-record-2"},
|
||||||
|
Reviewer: "reviewer",
|
||||||
|
Reason: "selected",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TranslationMemoryResolveConflict error: %v", err)
|
||||||
|
}
|
||||||
|
if !resolved.Available || resolved.Entry.RecordID != "tm-record-1" ||
|
||||||
|
resolved.Entry.TrustStatus != TranslationMemoryStatusTrusted {
|
||||||
|
t.Fatalf("resolved=%#v", resolved)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
|
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
|
||||||
@@ -1064,9 +1135,9 @@ func TestGlossaryTypedContract(t *testing.T) {
|
|||||||
Data: map[string]any{
|
Data: map[string]any{
|
||||||
"available": true,
|
"available": true,
|
||||||
"path": "/var/lib/bat/glossary.sqlite",
|
"path": "/var/lib/bat/glossary.sqlite",
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"summary": map[string]any{
|
"summary": map[string]any{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"term_count": 2,
|
"term_count": 2,
|
||||||
"approved_count": 1,
|
"approved_count": 1,
|
||||||
"draft_count": 1,
|
"draft_count": 1,
|
||||||
|
|||||||
@@ -162,21 +162,22 @@ require_contains "scripts/ci-check.sh" "GOLANGCI_LINT_VERSION"
|
|||||||
require_contains "scripts/ci-versions.sh" 'GOLANGCI_LINT_VERSION="2.12.2"'
|
require_contains "scripts/ci-versions.sh" 'GOLANGCI_LINT_VERSION="2.12.2"'
|
||||||
require_contains "docs/guides/development.md" "golangci-lint --version"
|
require_contains "docs/guides/development.md" "golangci-lint --version"
|
||||||
require_contains "docs/guides/development.md" "required gate"
|
require_contains "docs/guides/development.md" "required gate"
|
||||||
|
require_contains "docs/guides/development.md" "make ci-check"
|
||||||
|
require_contains "docs/guides/development.md" "唯一完整 required quality gate"
|
||||||
if grep -RIEq --exclude=check-doc-status.sh --exclude-dir=.git --exclude-dir=archive --exclude-dir=historical \
|
if grep -RIEq --exclude=check-doc-status.sh --exclude-dir=.git --exclude-dir=archive --exclude-dir=historical \
|
||||||
|
--exclude-dir=target --exclude-dir=bin \
|
||||||
-- '可选 lint|optional lint|optional golangci-lint' .; then
|
-- '可选 lint|optional lint|optional golangci-lint' .; then
|
||||||
fail "current documentation still describes Go lint as optional"
|
fail "current documentation still describes Go lint as optional"
|
||||||
fi
|
fi
|
||||||
if grep -Fq "ci: fmt" Makefile; then
|
if grep -Fq "ci: fmt" Makefile; then
|
||||||
fail "Makefile ci target must not run the mutating fmt target"
|
fail "Makefile ci target must not run the mutating fmt target"
|
||||||
fi
|
fi
|
||||||
require_contains ".gitea/workflows/bat.yml" "make check-docs"
|
if grep -RIEq --include='*.md' --exclude-dir=.git --exclude-dir=archive --exclude-dir=historical \
|
||||||
require_contains ".gitea/workflows/bat.yml" "make test-go-api"
|
--exclude-dir=target --exclude-dir=bin \
|
||||||
require_contains ".gitea/workflows/bat.yml" "go vet ./internal/api/... ./internal/backendrpc/... ./cmd/bat-api/..."
|
-- '自托管 Gitea|Gitea[[:space:]]+(runner|CI|workflow)|self-hosted|self hosted|\.gitea/workflows' \
|
||||||
require_contains ".gitea/workflows/bat.yml" "go build -o /tmp/bat-api ./cmd/bat-api"
|
README.md CURRENT_STATUS.md TODO.md docs PROJECT_PLAN.md AGENTS.md DOCS_INDEX.md 2>/dev/null; then
|
||||||
require_contains ".gitea/workflows/bat.yml" "Check Go formatting"
|
fail "current documentation still describes a Gitea/self-hosted CI runner"
|
||||||
require_contains ".gitea/workflows/bat.yml" "make check-go-format"
|
fi
|
||||||
require_contains ".gitea/workflows/bat.yml" "Required Go lint"
|
|
||||||
require_contains ".gitea/workflows/bat.yml" "source scripts/ci-versions.sh"
|
|
||||||
require_contains "Makefile" "cargo clippy --workspace --all-targets -- -D warnings"
|
require_contains "Makefile" "cargo clippy --workspace --all-targets -- -D warnings"
|
||||||
|
|
||||||
openapi_tmp="$(mktemp)"
|
openapi_tmp="$(mktemp)"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Canonical versions for required local and Gitea CI tools.
|
# Canonical versions for required local quality-gate tools.
|
||||||
GOLANGCI_LINT_VERSION="2.12.2"
|
GOLANGCI_LINT_VERSION="2.12.2"
|
||||||
|
|
||||||
golangci_lint_actual_version() {
|
golangci_lint_actual_version() {
|
||||||
|
|||||||
Reference in New Issue
Block a user