mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 08:54:55 +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`。
|
||||
|
||||
当前翻译交接还包括 `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` 查询完整
|
||||
job/unit/provider run 状态;`translation.memory.summary/query/confirm` 提供
|
||||
Rust-owned TM 的摘要、source/context 查询和显式 trusted 确认,`bat-api` 仅作
|
||||
typed 管理转发。当前下载实现使用默认 8 个独立 worker,完成后动态领取
|
||||
job/unit/provider run 状态;`translation.memory.summary/query/confirm/conflicts/resolve_conflict`
|
||||
提供 Rust-owned TM 的摘要、source/context 查询、显式 trusted 确认和冲突治理,
|
||||
`bat-api` 仅作 typed 管理转发。当前下载实现使用默认 8 个独立 worker,完成后动态领取
|
||||
任务,最终资源报告按 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、全局或
|
||||
TextUnit scope、source history 和 approved review。worker、TM 复用、人工 task
|
||||
结果和 workbench publish 都执行确定性 QA;blocking deviation 必须携带
|
||||
@@ -48,8 +48,11 @@ TextUnit scope、source history 和 approved review。worker、TM 复用、人
|
||||
|
||||
三个长期 SQLite owner 现在统一使用只读 schema preflight、精确 component
|
||||
fingerprint 和 `BEGIN IMMEDIATE` writer transaction:Translation Tasks 从 V1
|
||||
按显式 `v1 -> v2` step 迁移,当前版本为 V2;Translation Memory 当前版本为 V1,
|
||||
Glossary 当前版本为 V2。Glossary V2 正式吸收历史上未升版本的
|
||||
按显式 `v1 -> v2` step 迁移,当前版本为 V2;Translation Memory persistence schema
|
||||
当前版本为 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
|
||||
表,带 deletion audit 的 V1-B 只提升 bookkeeping version。future、未知或版本与结构
|
||||
不一致的数据库在任何 schema/data mutation 前 fail closed;migration 失败会 rollback,
|
||||
@@ -321,7 +324,7 @@ cargo run -p bat-infrastructure --bin bat -- \
|
||||
--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 仍未完成。
|
||||
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 侧仅代理。
|
||||
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;真实运行记录处于长期运行测试阶段,报告待后续提供。
|
||||
8. 内嵌 dashboard MVP 已实现;完整 Web 协作后台、数据库迁移、插件加载机制尚未实现;`bat-api` 资源 bootstrap/分发/OpenAPI/管理控制面已通过 `/openapi.yaml` 提供,完整游戏业务 API 的 OpenAPI 仍未完成。
|
||||
9. 原 Git 历史未恢复;当前仓库以新初始化基线为准。
|
||||
@@ -169,7 +169,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
||||
|
||||
**目标**:能够获取、解析和同步 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。
|
||||
2. 已实现 raw source + 完整 context exact match;模糊匹配和完整导入导出仍待实现。Glossary V2 已作为独立项目级 SQLite 资产接入 approved review、scope/alias/priority、provider constraints、确定性 QA 和显式 override。
|
||||
3. 已实现显式 per-record confirm;Glossary V2 已实现术语优先级、别名、分类、冲突检测和审核历史,批量审核与完整导入导出仍待实现。
|
||||
4. 已实现 `bat i18n memory summary|query|confirm` 与对应 Rust RPC。
|
||||
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、current Trusted 唯一性和冲突诊断;模糊匹配和完整导入导出仍待实现。Glossary domain/feature contract V1 已由 SQLite persistence schema V2 承载,接入 approved review、scope/alias/priority、provider constraints、确定性 QA 和显式 override。
|
||||
3. 已实现显式 per-record confirm、supersede 和历史冲突 resolve;Glossary 已实现术语优先级、别名、分类、冲突检测和审核历史,批量审核与完整导入导出仍待实现。
|
||||
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:本地开发、服务端部署。
|
||||
3. 数据备份与恢复文档。
|
||||
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`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
||||
- 资源导入链路可配置为在官方 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 交接。
|
||||
- `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。
|
||||
- `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 或生产同步的主集成边界。
|
||||
@@ -31,7 +31,7 @@
|
||||
- `bat-api` 完整游戏业务 API / launcher 安装包更新全链仍未完成;资源 CDN、HTTP 控制面、launcher 资源引导兼容和内嵌 dashboard MVP 已可用。
|
||||
- 完整 AssetBundle 对象级解析(UnityFS 解包、object table、TypeTree node 元数据、TextAsset bytes、MonoBehaviour/ScriptableObject 基础 TypeTree 字段级解析已起步;复杂字段覆盖、发布级重打包和 Patch 发布统一仍未完成)。
|
||||
- 复杂 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 协作后台。
|
||||
|
||||
详细状态见:
|
||||
|
||||
@@ -319,14 +319,15 @@ T03 → T16
|
||||
显式迁移。V1-A/V1-B 均兼容缺失 `schema_migrations` 或 component row,未知和 future
|
||||
schema 仍 fail closed;已验证业务 term/history、deletion audit 保留、失败回滚重试、
|
||||
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 治理
|
||||
|
||||
**类型:** P2
|
||||
**状态:** Ready
|
||||
**状态:** Done
|
||||
|
||||
## 问题
|
||||
|
||||
@@ -570,17 +571,18 @@ CI 检查必须:
|
||||
* 明确报告每项 gate;
|
||||
* required tool 缺失时不应伪装成全部通过;
|
||||
* required tool 缺失或版本不匹配必须失败;
|
||||
* 与实际 Gitea CI 尽量保持一致。
|
||||
* 与本地 `make ci-check` 的完整 required quality gate 保持一致。
|
||||
|
||||
## 完成记录
|
||||
|
||||
`make format` / `make fmt` 保留为显式写入命令,`make ci-check` 和兼容的
|
||||
`make ci` 只执行 read-only required gates;共享 `scripts/check-go-format.sh` 由
|
||||
`make check-go-format`、本地 `scripts/ci-check.sh` 和 Gitea workflow 共用。`golangci-lint
|
||||
2.12.2` 由 `scripts/ci-versions.sh` 固定,缺失或版本不匹配失败;OpenAPI、RPC contract
|
||||
和文档一致性由 `make check-docs` 纳入。Gitea self-hosted runner 执行相同的 required
|
||||
Rust/Go/docs 语义,不添加 GitHub Actions。最终 workspace gates、`make test-go-api`、
|
||||
`make check-docs` 和 `make ci-check` 均已通过。
|
||||
`make check-go-format` 和本地 `scripts/ci-check.sh` 共用。`golangci-lint 2.12.2`
|
||||
由 `scripts/ci-versions.sh` 固定,缺失或版本不匹配失败;OpenAPI、RPC contract
|
||||
和文档一致性由 `make check-docs` 纳入。项目以本地 `make ci-check` 作为唯一完整
|
||||
required quality gate,开发过程中可运行 focused checks 以快速反馈,但提交前完整 gate
|
||||
不得省略;仓库不依赖 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**
|
||||
|
||||
## 完成记录
|
||||
|
||||
已完成 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 后续扩展
|
||||
|
||||
**类型:** Feature
|
||||
**状态:** Blocked
|
||||
**状态:** Ready
|
||||
|
||||
## 前置
|
||||
|
||||
@@ -874,7 +885,7 @@ Embedding/vector 不应默认进入第一阶段。
|
||||
|
||||
## 目标
|
||||
|
||||
在现有 Glossary V2 基础上逐步增加:
|
||||
在现有 Glossary domain/feature contract V1、persistence schema V2 基础上逐步增加:
|
||||
|
||||
* bulk import/export;
|
||||
* 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-worker-run` | `translation.worker.run` | `{ "provider": "mock", "concurrency": 8, "max_tasks": 2 }` | `202` + worker task |
|
||||
| `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-update` | `translation.glossary.update` | term draft + `reviewer` | `202` + Glossary 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。
|
||||
- `BAT_REDIS_URL` / `BAT_REDIS_PASSWORD` 为**预留键**:Redis 任务后端尚未接入,当前任务历史持久化在 `<state-dir>/bat-tasks.json`。
|
||||
|
||||
### Translation Memory V1
|
||||
### Translation Memory persistence schema V2
|
||||
|
||||
Translation Memory 由 Rust `bat` 独立持有,默认路径为
|
||||
`<output>/translation-memory.sqlite`,不在 `versions/<id>` 内,也不使用当前 release
|
||||
@@ -311,16 +312,23 @@ Translation Memory 由 Rust `bat` 独立持有,默认路径为
|
||||
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 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
|
||||
自动复用。provider 输出写入先是 `candidate`;manual task result 即使 completed 也不会自动
|
||||
建立 TM 或 trusted。查询、诊断和显式 confirm 对应 Rust
|
||||
RPC `translation.memory.summary`、`translation.memory.query`、`translation.memory.confirm`。
|
||||
context 不完整或不一致、normalized source 辅助命中和 workflow `proofread` 都不会自动
|
||||
建立 trusted 记录。
|
||||
只有 raw source 完全相同、完整 context 完全相同且状态为 current `trusted` 的单条记录会被
|
||||
worker 自动复用。provider 输出写入先是 `candidate`;manual task result 即使 completed
|
||||
也不会自动建立 TM 或 trusted。查询、诊断、确认和冲突治理对应 Rust
|
||||
RPC `translation.memory.summary`、`translation.memory.query`、
|
||||
`translation.memory.confirm`、`translation.memory.conflicts` 和
|
||||
`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`,不位于
|
||||
`versions/<id>`,也不与 TM 或当前 release 的 task 库共用。配置覆盖方式为
|
||||
|
||||
@@ -505,6 +505,31 @@ paths:
|
||||
description: Missing or invalid admin token.
|
||||
"503":
|
||||
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:
|
||||
get:
|
||||
summary: Read Rust-owned Glossary summary
|
||||
@@ -630,7 +655,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
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:
|
||||
required: false
|
||||
content:
|
||||
@@ -745,6 +770,18 @@ paths:
|
||||
type: string
|
||||
record_id:
|
||||
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:
|
||||
type: string
|
||||
source_term:
|
||||
|
||||
@@ -23,7 +23,8 @@ pub use translation::{
|
||||
TranslationStatus,
|
||||
};
|
||||
pub use translation_memory::{
|
||||
TranslationMemoryContext, TranslationMemoryDraft, TranslationMemoryEntry,
|
||||
TranslationMemoryMatch, TranslationMemoryMatchKind, TranslationMemorySourceKind,
|
||||
TranslationMemorySourceTrace, TranslationMemorySummary, TranslationMemoryTrustStatus,
|
||||
TranslationMemoryConflict, TranslationMemoryContext, TranslationMemoryDraft,
|
||||
TranslationMemoryEntry, TranslationMemoryMatch, TranslationMemoryMatchKind,
|
||||
TranslationMemorySourceKind, TranslationMemorySourceTrace, TranslationMemorySummary,
|
||||
TranslationMemoryTrustStatus,
|
||||
};
|
||||
|
||||
@@ -57,6 +57,8 @@ impl TranslationMemoryTrustStatus {
|
||||
pub enum TranslationMemoryMatchKind {
|
||||
/// 原始 source 和上下文都完全匹配,且记录可信,可自动复用。
|
||||
StrongExact,
|
||||
/// 同一 exact identity 存在多个 Trusted,必须人工治理。
|
||||
TrustedConflict,
|
||||
/// 原始 source 完全匹配,但上下文不同或不足,不能自动复用。
|
||||
CandidateExact,
|
||||
/// 原始 source 匹配,但上下文不兼容,不能自动复用。
|
||||
@@ -68,6 +70,7 @@ impl TranslationMemoryMatchKind {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::StrongExact => "strong_exact",
|
||||
Self::TrustedConflict => "trusted_conflict",
|
||||
Self::CandidateExact => "candidate_exact",
|
||||
Self::SourceOnly => "source_only",
|
||||
}
|
||||
@@ -210,6 +213,23 @@ pub struct TranslationMemoryMatch {
|
||||
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 仓储摘要。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TranslationMemorySummary {
|
||||
@@ -225,4 +245,8 @@ pub struct TranslationMemorySummary {
|
||||
pub superseded_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 仓储契约。
|
||||
|
||||
use crate::domain::{
|
||||
TranslationMemoryContext, TranslationMemoryDraft, TranslationMemoryEntry,
|
||||
TranslationMemoryMatch, TranslationMemorySummary,
|
||||
TranslationMemoryConflict, TranslationMemoryContext, TranslationMemoryDraft,
|
||||
TranslationMemoryEntry, TranslationMemoryMatch, TranslationMemorySummary,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// 跨 official release 持久化的 Translation Memory 仓储。
|
||||
///
|
||||
/// 该契约只描述 V1 的精确查询和明确人工确认。仓储实现不得把
|
||||
/// 该契约描述 exact-match TM 和明确人工 Trusted 治理。仓储实现不得把
|
||||
/// `TranslationTaskStatus::Completed` 或 provider 成功隐式解释为 trusted。
|
||||
#[async_trait]
|
||||
pub trait TranslationMemoryRepository: Send + Sync {
|
||||
@@ -39,6 +39,27 @@ pub trait TranslationMemoryRepository: Send + Sync {
|
||||
reason: Option<String>,
|
||||
) -> 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 记录。
|
||||
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/translation/tasks`、`/admin/translation/handoff`、
|
||||
`/admin/translation/memory/summary`、`/admin/translation/memory/query` 和
|
||||
`translation-memory-confirm` 转发
|
||||
`translation-memory-confirm`、`translation-memory-resolve-conflict` 转发
|
||||
- `/openapi.yaml`
|
||||
|
||||
HTTP 路由的 OpenAPI 文本由 `internal/api/openapi.go` 提供,运行中的服务也可
|
||||
|
||||
@@ -196,8 +196,10 @@ pub struct ParserRegistry {
|
||||
### 4. 翻译系统(目标扩展,Go;当前 worker 由 Rust `bat` 承担)
|
||||
|
||||
当前已实现的是 Rust `bat` 的离线 TextUnit 队列、mock/Crowdin provider worker、
|
||||
lease/retry、结果落库、项目级 Translation Memory V1 和独立 Glossary V2。TM 位于独立
|
||||
SQLite,按 raw source + 完整 context 做 trusted exact reuse,candidate 必须显式 confirm;
|
||||
lease/retry、结果落库、项目级 Translation Memory persistence schema V2 和独立 Glossary
|
||||
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;模糊
|
||||
匹配和完整 Provider 体系仍属后续缺口。
|
||||
|
||||
@@ -229,7 +231,7 @@ type TranslationProvider interface {
|
||||
- Azure Translator Provider
|
||||
|
||||
**翻译记忆库**:
|
||||
- 当前 V1:raw source 完全相同、完整 context 完全相同且记录为 trusted 时自动复用。
|
||||
- 当前规则:raw source 完全相同、完整 context 完全相同且只有一条 current Trusted 时自动复用。
|
||||
- provider 输出写入先是 candidate;manual task result 不会自动建立 TM 或 trusted。`bat i18n memory confirm` 显式确认单条记录后才可自动复用。
|
||||
- source、context、release、TextUnit、provider 和 run provenance 保存在 Rust TM SQLite 中。
|
||||
- 模糊匹配、术语优先级和 PostgreSQL 服务化仍不是当前实现。
|
||||
@@ -296,7 +298,8 @@ HTTP Request → Middleware (Auth, CORS, Logger) → Handler → Service → Rep
|
||||
|
||||
### 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
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
- 新的跨语言控制和查询能力优先增加 Rust RPC contract,再由
|
||||
`internal/backendrpc` 消费。
|
||||
|
||||
4. **完整游戏业务 API、完整 Web 协作后台和 Provider 扩展体系仍是后续目标;Rust `bat` 已持有 Glossary V2,Web 术语协作视图仍待建设**;
|
||||
Translation Memory V1 已由 Rust `bat` 持有,不能从目标架构图推断 Go 侧拥有第二份状态。
|
||||
4. **完整游戏业务 API、完整 Web 协作后台和 Provider 扩展体系仍是后续目标;Rust `bat` 已持有
|
||||
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 队列还会在当前
|
||||
release 根目录写入 `translation-tasks.sqlite`,由版本化 `schema_migrations`
|
||||
管理 V2 queued/running/failed/completed/skipped、provider run、lease、失败分类、
|
||||
重试计划和 TextUnit 级译文结果。跨 release 的 Translation Memory V1 独立存储在
|
||||
`<output>/translation-memory.sqlite`,记录 raw source/hash、完整 context、candidate/
|
||||
trusted 和 release/TextUnit/provider/run provenance;`translation.tasks` 优先查询这份状态库,
|
||||
重试计划和 TextUnit 级译文结果。跨 release 的 Translation Memory persistence schema V2
|
||||
独立存储在 `<output>/translation-memory.sqlite`,记录 raw source/hash、完整 context、
|
||||
candidate/trusted 和 release/TextUnit/provider/run provenance;`translation.tasks` 优先查询这份状态库,
|
||||
`translation.worker.run` 由 Rust worker 回写状态;`translation.task.update` 仍供外部 provider 流程回写状态;
|
||||
没有状态库的旧 release 才回退到 immutable JSON 队列。`bat doctor cas`
|
||||
已提供只读 CAS 根目录、对象目录、元数据库文件和对象统计诊断;`resource.index`
|
||||
|
||||
@@ -213,8 +213,8 @@ bat i18n worker run \
|
||||
`--watch`,因此可以单次、限定次数或周期执行;`--run-count > 1` 时仍必须
|
||||
显式指定 `--interval`。
|
||||
|
||||
Glossary V2 是 Rust `bat` 持有的独立项目级 SQLite 资产,默认位于
|
||||
`<output>/glossary.sqlite`;V2 正式吸收历史上的 `glossary_term_deletions`
|
||||
Glossary domain/feature contract V1 由 Rust `bat` 持有,并由 SQLite persistence schema V2
|
||||
承载,默认位于 `<output>/glossary.sqlite`;V2 正式吸收历史上的 `glossary_term_deletions`
|
||||
schema drift;也可以用 `--glossary-path`、
|
||||
`BAT_GLOSSARY_PATH` 或 `[translation.worker].glossary_path` 指定。worker 只把
|
||||
`approved` term 转成 provider-neutral constraints,并在 TM 复用、provider 返回
|
||||
|
||||
@@ -9,8 +9,8 @@ BlueArchive Toolkit 的部署文档分为当前可用模式和目标模式:
|
||||
2. **官方资源同步生产任务**:当前可用,运行 Rust `bat --watch` 或 RPC/daemon 模式。
|
||||
3. **bat-api 资源 bootstrap / 分发服务**:当前可用,和 Rust `bat` 在同一服务器/容器环境运行,经 `bat.sock` RPC 获取当前 `resource_root`。
|
||||
4. **可选数据库开发环境**:PostgreSQL/Redis 只服务于未来的 Go 服务层、完整 Web 协作后台和
|
||||
Provider 扩展,不是当前 `bat` / `bat-api` 的生产运行依赖;当前 Translation Memory V1
|
||||
使用 `<output>/translation-memory.sqlite`。
|
||||
Provider 扩展,不是当前 `bat` / `bat-api` 的生产运行依赖;当前 Translation Memory
|
||||
persistence schema V2 使用 `<output>/translation-memory.sqlite`。
|
||||
5. **完整单机/分布式部署**:尚未提供。完整游戏业务 API、数据库迁移和 Web 未实现前,不把它作为可执行部署方案。
|
||||
|
||||
---
|
||||
|
||||
@@ -23,9 +23,11 @@ rustc --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
|
||||
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,不是可选检查。该 workflow 会用
|
||||
`GITHUB_SERVER_URL`、`GITHUB_REPOSITORY`、`GITHUB_REF` 和 `GITHUB_SHA` 手动 `git fetch`
|
||||
当前提交,再执行 Rust workspace 的只读格式检查、检查、构建、clippy 和测试,以及通过
|
||||
`golangci-lint 2.12.2` 是 required gate,不是可选检查。`make ci-check` 会执行 Rust
|
||||
workspace 的只读格式检查、检查、release build、clippy 和测试,以及通过
|
||||
`make check-go-format` 执行的 Go 格式、测试、vet、构建、2.12.2 lint 和文档状态门禁。
|
||||
这样可以避免自托管 runner 在准备阶段通过代理克隆第三方 action 仓库。
|
||||
|
||||
#### Docker
|
||||
```bash
|
||||
|
||||
@@ -261,9 +261,11 @@ rollback 或 repair。
|
||||
| `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.proofread` | 已实现 | `null` | 将当前汉化 workflow 标记为人工校对中,返回工作流状态报告。 |
|
||||
| `translation.memory.summary` | 已实现 | 可选 `{ "translation_memory_path": "..." }` | 返回 TM schema 版本、总记录数及 candidate/trusted/rejected/superseded 状态计数。 |
|
||||
| `translation.memory.query` | 已实现 | `{ "source_text": "...", "source_context": {...}, "limit": 100 }` | 按 raw source 查询记录,返回 match kind、trust、translation 和 provenance。 |
|
||||
| `translation.memory.confirm` | 已实现 | `{ "record_id": "...", "reviewer": "...", "reason": "..." }` | 显式确认一条 candidate 为 trusted;worker 之后才可自动复用。 |
|
||||
| `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;conflict 结果不可自动复用。 |
|
||||
| `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.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 决策。 |
|
||||
@@ -535,7 +537,8 @@ CLI 对应关系:
|
||||
| `bat i18n worker run` | `translation.worker.run` |
|
||||
| `bat i18n proofread` | `translation.proofread` |
|
||||
| `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 diagnose` | `translation.glossary.diagnose` |
|
||||
| `bat i18n glossary add/update` | `translation.glossary.add` / `translation.glossary.update` |
|
||||
@@ -573,7 +576,8 @@ CLI 对应关系:
|
||||
`localized.status`、`localized.publish`、`localized.rollback`、
|
||||
`translation.tasks`、`translation.handoff`、`translation.task.update`、
|
||||
`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.update`、`translation.glossary.approve`、`translation.glossary.deprecate`、
|
||||
`translation.glossary.delete`、
|
||||
@@ -594,7 +598,7 @@ CLI 对应关系:
|
||||
| `TaskBackend` | `task.list`、`task.status`、`task.logs`、`task.cancel` | 鉴权后的 daemon 任务查询和取消 |
|
||||
| `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 触发与人工校对标记 |
|
||||
| `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 状态 |
|
||||
| `LocalizedBackend` | `localized.status`、`localized.publish`、`localized.rollback` | 鉴权后的汉化 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 响应。
|
||||
3. `resource.manifest` 第一页响应,至少包含 1 到 2 个 entries。
|
||||
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。
|
||||
|
||||
输出应来自 Rust 代码路径,而不是手写 JSON。允许使用 fixture resource root 或临时目录,但不能依赖开发机真实资源目录。
|
||||
|
||||
@@ -152,17 +152,18 @@ official/localized distribution 均被阻断,普通查询不会自动重建。
|
||||
更完整的查询/权限/损坏恢复、模糊 TM、bat.sock peer credential/perms、FFI 生命周期、
|
||||
资源大小/限额与更强的持久化 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
|
||||
契约。它记录 raw source/hash、完整 context、release/TextUnit/provider/run provenance,
|
||||
区分 candidate/trusted,只有显式 confirm 才能建立 trusted 记录;worker 只自动复用
|
||||
trusted 的 raw source + 完整 context exact match,并在复用前执行已批准 Glossary 的
|
||||
确定性 QA。Go `bat-api` 已提供鉴权的 summary/query 只读接口和 confirm 转发,但 Go
|
||||
不持有 TM 状态。仍缺少模糊匹配和更丰富的导入导出历史能力。
|
||||
区分 candidate/trusted,只有显式 confirm 或 conflict resolve 才能建立唯一 current
|
||||
trusted 记录;worker 只自动复用 raw source + 完整 context exact match 的 current
|
||||
trusted,并在复用前执行已批准 Glossary 的确定性 QA。Go `bat-api` 已提供鉴权的
|
||||
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 正式
|
||||
吸收历史上未升版本的 `glossary_term_deletions` drift,并将历史 V1-A(无 deletion
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
| 组件 | 路径 | 状态 | 说明 |
|
||||
|---|---|---|---|
|
||||
| Module | `go.mod` → `bat-api` | 已用 | 服务层模块名 |
|
||||
| RPC client | `internal/backendrpc` | **完成** | Unix socket JSON-RPC transport + typed helper;typed helper 覆盖 daemon 已实现控制/查询、`resource.state/sync/verify/repair/manifest/list`、`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` |
|
||||
| 试验 CLI | `cmd/bat` | **试验** | doctor 固定 ok;manifest/sync 走 FFI |
|
||||
| FFI | `internal/ffi` | **可选** | 需 `build-ffi` |
|
||||
|
||||
@@ -106,7 +106,8 @@ use terminal_output::RotatingStructuredLogger;
|
||||
use terminal_output::STARTUP_BANNER;
|
||||
use translation_query::{
|
||||
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,
|
||||
update_translation_task_status_report,
|
||||
};
|
||||
@@ -242,7 +243,9 @@ fn run() -> anyhow::Result<i32> {
|
||||
}
|
||||
CliCommand::TranslationMemorySummary
|
||||
| CliCommand::TranslationMemoryQuery
|
||||
| CliCommand::TranslationMemoryConfirm => {
|
||||
| CliCommand::TranslationMemoryConfirm
|
||||
| CliCommand::TranslationMemoryConflicts
|
||||
| CliCommand::TranslationMemoryResolveConflict => {
|
||||
run_translation_memory_command(&options)?;
|
||||
Ok(0)
|
||||
}
|
||||
@@ -421,6 +424,8 @@ struct CliOptions {
|
||||
translation_memory_source_text: Option<String>,
|
||||
translation_memory_context_json: 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_reason: Option<String>,
|
||||
glossary_term_id: Option<String>,
|
||||
@@ -548,6 +553,8 @@ impl Default for CliOptions {
|
||||
translation_memory_source_text: None,
|
||||
translation_memory_context_json: 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_reason: None,
|
||||
glossary_term_id: None,
|
||||
@@ -666,6 +673,8 @@ enum CliCommand {
|
||||
TranslationMemorySummary,
|
||||
TranslationMemoryQuery,
|
||||
TranslationMemoryConfirm,
|
||||
TranslationMemoryConflicts,
|
||||
TranslationMemoryResolveConflict,
|
||||
GlossarySummary,
|
||||
GlossaryQuery,
|
||||
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_QUERY: &str = "translation.memory.query";
|
||||
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_QUERY: &str = "translation.glossary.query";
|
||||
const RPC_METHOD_GLOSSARY_ADD: &str = "translation.glossary.add";
|
||||
@@ -2304,6 +2315,30 @@ fn dispatch_rpc_method(
|
||||
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(
|
||||
request_id,
|
||||
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>(
|
||||
params: Option<&'a serde_json::Value>,
|
||||
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> {
|
||||
let limit = match translation_memory_rpc_value(
|
||||
params,
|
||||
&["limit"],
|
||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
)? {
|
||||
fn translation_memory_rpc_limit(
|
||||
params: Option<&serde_json::Value>,
|
||||
method: &'static str,
|
||||
) -> Result<usize, ApiError> {
|
||||
let limit = match translation_memory_rpc_value(params, &["limit"], method)? {
|
||||
None | Some(serde_json::Value::Null) => 100,
|
||||
Some(value) => value.as_u64().ok_or_else(|| {
|
||||
ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
method,
|
||||
"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| {
|
||||
ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
method,
|
||||
format!("limit 无效:{error}"),
|
||||
)
|
||||
})?;
|
||||
if !(1..=1000).contains(&limit) {
|
||||
return Err(ApiError::new(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
method,
|
||||
"limit 必须在 1..=1000 范围内",
|
||||
));
|
||||
}
|
||||
@@ -4285,13 +4341,8 @@ fn translation_memory_summary_rpc_report(
|
||||
params,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
||||
)?;
|
||||
build_translation_memory_summary_report(&path).map_err(|error| {
|
||||
ApiError::new(
|
||||
ErrorCode::INTERNAL,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_SUMMARY,
|
||||
error.to_string(),
|
||||
)
|
||||
})
|
||||
build_translation_memory_summary_report(&path)
|
||||
.map_err(|error| translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_SUMMARY, error))
|
||||
}
|
||||
|
||||
fn translation_memory_query_rpc_report(
|
||||
@@ -4314,7 +4365,7 @@ fn translation_memory_query_rpc_report(
|
||||
)
|
||||
})?;
|
||||
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(
|
||||
state_dir,
|
||||
output_root,
|
||||
@@ -4322,13 +4373,8 @@ fn translation_memory_query_rpc_report(
|
||||
params,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
)?;
|
||||
build_translation_memory_query_report(&path, source_text, &context, limit).map_err(|error| {
|
||||
ApiError::new(
|
||||
ErrorCode::INTERNAL,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
error.to_string(),
|
||||
)
|
||||
})
|
||||
build_translation_memory_query_report(&path, source_text, &context, limit)
|
||||
.map_err(|error| translation_memory_api_error(RPC_METHOD_TRANSLATION_MEMORY_QUERY, error))
|
||||
}
|
||||
|
||||
fn translation_memory_confirm_rpc_report(
|
||||
@@ -4370,6 +4416,13 @@ fn translation_memory_confirm_rpc_report(
|
||||
"reason",
|
||||
)?
|
||||
.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(
|
||||
state_dir,
|
||||
output_root,
|
||||
@@ -4377,12 +4430,127 @@ fn translation_memory_confirm_rpc_report(
|
||||
params,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
||||
)?;
|
||||
build_translation_memory_confirm_report(&path, record_id, reviewer, reason).map_err(|error| {
|
||||
ApiError::new(
|
||||
ErrorCode::INTERNAL,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_CONFIRM,
|
||||
error.to_string(),
|
||||
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(
|
||||
ErrorCode::RPC_INVALID_PARAMS,
|
||||
RPC_METHOD_TRANSLATION_MEMORY_RESOLVE_CONFLICT,
|
||||
"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)?);
|
||||
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_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" => {
|
||||
options.translation_memory_reviewer = Some(next_option_value(&mut args, &flag)?);
|
||||
options.translation_memory_command_option_explicit = true;
|
||||
@@ -7938,6 +8116,8 @@ fn parse_args_with_env(
|
||||
CliCommand::TranslationMemorySummary
|
||||
| CliCommand::TranslationMemoryQuery
|
||||
| CliCommand::TranslationMemoryConfirm
|
||||
| CliCommand::TranslationMemoryConflicts
|
||||
| CliCommand::TranslationMemoryResolveConflict
|
||||
| CliCommand::Restart
|
||||
| CliCommand::Reload
|
||||
)
|
||||
@@ -7981,6 +8161,8 @@ fn parse_args_with_env(
|
||||
| CliCommand::TranslationMemorySummary
|
||||
| CliCommand::TranslationMemoryQuery
|
||||
| CliCommand::TranslationMemoryConfirm
|
||||
| CliCommand::TranslationMemoryConflicts
|
||||
| CliCommand::TranslationMemoryResolveConflict
|
||||
| CliCommand::GlossarySummary
|
||||
| CliCommand::GlossaryQuery
|
||||
| CliCommand::GlossaryAdd
|
||||
@@ -8004,6 +8186,8 @@ fn parse_args_with_env(
|
||||
CliCommand::TranslationMemorySummary
|
||||
| CliCommand::TranslationMemoryQuery
|
||||
| CliCommand::TranslationMemoryConfirm
|
||||
| CliCommand::TranslationMemoryConflicts
|
||||
| CliCommand::TranslationMemoryResolveConflict
|
||||
)
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -8044,7 +8228,9 @@ fn parse_args_with_env(
|
||||
}
|
||||
CliCommand::TranslationMemorySummary
|
||||
| CliCommand::TranslationMemoryQuery
|
||||
| CliCommand::TranslationMemoryConfirm => {
|
||||
| CliCommand::TranslationMemoryConfirm
|
||||
| CliCommand::TranslationMemoryConflicts
|
||||
| CliCommand::TranslationMemoryResolveConflict => {
|
||||
if options.watch || options.daemon || options.daemon_child {
|
||||
return Err(anyhow::anyhow!("i18n memory 命令只支持单次执行或 RPC 调用"));
|
||||
}
|
||||
@@ -8079,6 +8265,10 @@ fn parse_args_with_env(
|
||||
}
|
||||
CliCommand::TranslationMemoryQuery => {
|
||||
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_reason.is_some()
|
||||
{
|
||||
@@ -8088,11 +8278,38 @@ fn parse_args_with_env(
|
||||
CliCommand::TranslationMemoryConfirm => {
|
||||
if options.translation_memory_source_text.is_some()
|
||||
|| options.translation_memory_context_json.is_some()
|
||||
|| options
|
||||
.translation_memory_expected_trusted_record_ids_json
|
||||
.is_some()
|
||||
|| options.query_limit != 100
|
||||
{
|
||||
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!(),
|
||||
}
|
||||
if matches!(options.command, CliCommand::TranslationMemoryQuery)
|
||||
@@ -8110,6 +8327,20 @@ fn parse_args_with_env(
|
||||
"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.banner = false;
|
||||
}
|
||||
@@ -8779,6 +9010,8 @@ fn parse_translation_memory_command(
|
||||
"summary" | "status" => CliCommand::TranslationMemorySummary,
|
||||
"query" | "find" => CliCommand::TranslationMemoryQuery,
|
||||
"confirm" | "trust" => CliCommand::TranslationMemoryConfirm,
|
||||
"conflicts" | "list-conflicts" => CliCommand::TranslationMemoryConflicts,
|
||||
"resolve-conflict" | "resolve" => CliCommand::TranslationMemoryResolveConflict,
|
||||
other => return Err(anyhow::anyhow!("未知 translation memory 二级命令:{other}")),
|
||||
};
|
||||
ensure_command_not_set(options.command, &format!("translation memory {action}"))?;
|
||||
|
||||
@@ -260,6 +260,47 @@ fn translation_memory_commands_parse_and_validate() {
|
||||
])
|
||||
.unwrap();
|
||||
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",
|
||||
@@ -2914,6 +2955,19 @@ fn dispatch_translation_memory_rejects_invalid_params_with_stable_error_code() {
|
||||
"translation.memory.confirm",
|
||||
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(
|
||||
&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::TranslationMemoryQuery => RPC_METHOD_TRANSLATION_MEMORY_QUERY,
|
||||
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 命令")),
|
||||
};
|
||||
if daemon_rpc_available(&options.state_dir)
|
||||
@@ -480,6 +484,34 @@ pub(super) fn run_translation_memory_command(options: &CliOptions) -> anyhow::Re
|
||||
record_id,
|
||||
reviewer,
|
||||
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!(),
|
||||
@@ -538,10 +570,49 @@ fn translation_memory_cli_params(
|
||||
.ok_or_else(|| anyhow::anyhow!("TM confirm 必须指定 --tm-reviewer"))?;
|
||||
params.insert("record_id".to_string(), serde_json::json!(record_id));
|
||||
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() {
|
||||
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!(),
|
||||
}
|
||||
Ok(Some(serde_json::Value::Object(params)))
|
||||
@@ -625,6 +696,7 @@ pub(super) fn build_translation_memory_confirm_report(
|
||||
record_id: &str,
|
||||
reviewer: &str,
|
||||
reason: Option<String>,
|
||||
supersede_record_id: Option<&str>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
if record_id.trim().is_empty() || reviewer.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -645,7 +717,89 @@ pub(super) fn build_translation_memory_confirm_report(
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
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
|
||||
.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::{
|
||||
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,
|
||||
OfficialTextUnitTaskSummary, OFFICIAL_TEXTUNIT_TASK_QUEUE_VERSION,
|
||||
};
|
||||
use bat_core::domain::TranslationMemoryMatchKind;
|
||||
|
||||
fn fixture_root() -> (tempfile::TempDir, OfficialTextUnitTaskQueue) {
|
||||
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]
|
||||
async fn worker_retains_tm_hits_when_provider_fails_for_remaining_units() {
|
||||
let (temp, queue) = fixture_root();
|
||||
|
||||
+113
-1
@@ -56,6 +56,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/admin/translation/handoff",
|
||||
"/admin/translation/memory/summary",
|
||||
"/admin/translation/memory/query",
|
||||
"/admin/translation/memory/conflicts",
|
||||
"/admin/translation/glossary/summary",
|
||||
"/admin/translation/glossary/query",
|
||||
"/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-proofread",
|
||||
"/admin/control/translation-memory-confirm",
|
||||
"/admin/control/translation-memory-resolve-conflict",
|
||||
"/admin/control/translation-glossary-add",
|
||||
"/admin/control/translation-glossary-update",
|
||||
"/admin/control/translation-glossary-approve",
|
||||
@@ -133,6 +135,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminTranslationMemoryConfirm(w, r)
|
||||
return
|
||||
}
|
||||
if action == "translation-memory-resolve-conflict" {
|
||||
s.handleAdminTranslationMemoryResolveConflict(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(action, "translation-glossary-") {
|
||||
s.handleAdminGlossaryControl(w, r, action)
|
||||
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) {
|
||||
backend, ok := s.backend.(TranslationBackend)
|
||||
if !ok || backend == nil {
|
||||
@@ -1016,6 +1050,37 @@ func (s *Server) handleAdminTranslationMemoryQuery(w http.ResponseWriter, r *htt
|
||||
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) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||
@@ -1139,6 +1204,20 @@ func translationMemoryQueryParams(r *http.Request) (backendrpc.TranslationMemory
|
||||
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) {
|
||||
query := r.URL.Query()
|
||||
params := backendrpc.TextUnitQueryParams{
|
||||
@@ -1334,6 +1413,32 @@ func validateTranslationMemoryConfirmParams(params backendrpc.TranslationMemoryC
|
||||
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.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
|
||||
}
|
||||
|
||||
@@ -1412,10 +1517,17 @@ func (s *Server) writeControlBackendError(w http.ResponseWriter, action string,
|
||||
message = "control request was canceled"
|
||||
default:
|
||||
var apiErr *backendrpc.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.Kind == "not_implemented" {
|
||||
if errors.As(err, &apiErr) {
|
||||
switch apiErr.Kind {
|
||||
case "not_implemented":
|
||||
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)
|
||||
|
||||
@@ -1031,6 +1031,8 @@ type controlBackend struct {
|
||||
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
|
||||
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
|
||||
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
|
||||
translationMemoryConflictsParams []backendrpc.TranslationMemoryConflictsParams
|
||||
translationMemoryResolveParams []backendrpc.TranslationMemoryResolveConflictParams
|
||||
glossarySummaryParams []backendrpc.GlossarySummaryParams
|
||||
glossaryQueryParams []backendrpc.GlossaryQueryParams
|
||||
glossaryDiagnoseParams []backendrpc.GlossaryDiagnoseParams
|
||||
@@ -1183,7 +1185,7 @@ func (b *controlBackend) TranslationProofread(ctx context.Context) (json.RawMess
|
||||
func (b *controlBackend) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
|
||||
b.calls = append(b.calls, "translation.memory.summary")
|
||||
b.translationMemorySummaryParams = append(b.translationMemorySummaryParams, params)
|
||||
schemaVersion := uint64(1)
|
||||
schemaVersion := uint64(2)
|
||||
return &backendrpc.TranslationMemorySummaryReport{
|
||||
Available: true,
|
||||
Path: params.TranslationMemoryPath,
|
||||
@@ -1195,6 +1197,7 @@ func (b *controlBackend) TranslationMemorySummary(ctx context.Context, params ba
|
||||
CandidateCount: 1,
|
||||
SupersededCount: 0,
|
||||
RejectedCount: 0,
|
||||
CurrentTrustedCount: 1,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1225,10 +1228,33 @@ func (b *controlBackend) TranslationMemoryConfirm(ctx context.Context, params ba
|
||||
}, 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) {
|
||||
b.calls = append(b.calls, "translation.glossary.summary")
|
||||
b.glossarySummaryParams = append(b.glossarySummaryParams, params)
|
||||
schemaVersion := uint64(1)
|
||||
schemaVersion := uint64(2)
|
||||
return &backendrpc.GlossarySummaryReport{
|
||||
Available: true,
|
||||
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())
|
||||
}
|
||||
|
||||
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.Header.Set("Authorization", "Bearer translation-token")
|
||||
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 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 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 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"},
|
||||
@@ -2390,6 +2429,11 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
||||
backend.translationMemoryConfirmParams[0].Reviewer != "reviewer" {
|
||||
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 ||
|
||||
backend.glossaryMutationParams[0].TermID != "term-sensei" ||
|
||||
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())
|
||||
}
|
||||
|
||||
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.Header.Set("Authorization", "Bearer control-token")
|
||||
recorder = httptest.NewRecorder()
|
||||
|
||||
+38
-1
@@ -512,6 +512,31 @@ paths:
|
||||
description: Missing or invalid admin token.
|
||||
"503":
|
||||
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:
|
||||
get:
|
||||
summary: Read Rust-owned Glossary summary
|
||||
@@ -637,7 +662,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
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:
|
||||
required: false
|
||||
content:
|
||||
@@ -752,6 +777,18 @@ paths:
|
||||
type: string
|
||||
record_id:
|
||||
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:
|
||||
type: string
|
||||
source_term:
|
||||
|
||||
@@ -93,6 +93,8 @@ type TranslationMemoryBackend interface {
|
||||
TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error)
|
||||
TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, 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.
|
||||
@@ -238,6 +240,14 @@ func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendr
|
||||
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) {
|
||||
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/memory/summary", s.handleAdminTranslationMemorySummary)
|
||||
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/query", s.handleAdminGlossaryQuery)
|
||||
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/memory/summary",
|
||||
"/admin/translation/memory/query",
|
||||
"/admin/translation/memory/conflicts",
|
||||
"/admin/translation/glossary/summary",
|
||||
"/admin/translation/glossary/query",
|
||||
"/admin/translation/glossary/diagnose",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本
|
||||
window 为 7260 秒(默认 3600 秒验证周期和 60 秒失败重试周期)。
|
||||
- 对应 release 的 `official-sync-snapshot.json`。
|
||||
- `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
|
||||
daemon socket 或完整发布切换验证。
|
||||
|
||||
@@ -480,6 +480,7 @@ type TranslationMemoryMatchKind string
|
||||
|
||||
const (
|
||||
TranslationMemoryMatchStrongExact TranslationMemoryMatchKind = "strong_exact"
|
||||
TranslationMemoryMatchTrustedConflict TranslationMemoryMatchKind = "trusted_conflict"
|
||||
TranslationMemoryMatchCandidateExact TranslationMemoryMatchKind = "candidate_exact"
|
||||
TranslationMemoryMatchSourceOnly TranslationMemoryMatchKind = "source_only"
|
||||
)
|
||||
@@ -504,6 +505,7 @@ type TranslationMemoryConfirmParams struct {
|
||||
RecordID string `json:"record_id"`
|
||||
Reviewer string `json:"reviewer"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SupersedeRecordID string `json:"supersede_record_id,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemorySummary mirrors translation.memory.summary data.
|
||||
@@ -514,6 +516,8 @@ type TranslationMemorySummary struct {
|
||||
CandidateCount uint64 `json:"candidate_count"`
|
||||
SupersededCount uint64 `json:"superseded_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
|
||||
@@ -591,6 +595,47 @@ type TranslationMemoryConfirmReport struct {
|
||||
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.
|
||||
type GlossaryReviewStatus string
|
||||
|
||||
@@ -1067,6 +1112,18 @@ func (c *Client) TranslationMemoryConfirm(ctx context.Context, params Translatio
|
||||
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) {
|
||||
var out GlossarySummaryReport
|
||||
_, err := c.Call(ctx, "translation.glossary.summary", params, &out)
|
||||
|
||||
@@ -756,14 +756,15 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"summary": map[string]any{
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"record_count": 3,
|
||||
"trusted_count": 1,
|
||||
"candidate_count": 1,
|
||||
"superseded_count": 1,
|
||||
"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:
|
||||
t.Fatalf("unexpected method %q", req.Method)
|
||||
return testResponse{}
|
||||
@@ -915,6 +960,32 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
|
||||
confirmed.Entry.RecordID != "tm-record-1" {
|
||||
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) {
|
||||
@@ -1064,9 +1135,9 @@ func TestGlossaryTypedContract(t *testing.T) {
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"path": "/var/lib/bat/glossary.sqlite",
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"summary": map[string]any{
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"term_count": 2,
|
||||
"approved_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 "docs/guides/development.md" "golangci-lint --version"
|
||||
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 \
|
||||
--exclude-dir=target --exclude-dir=bin \
|
||||
-- '可选 lint|optional lint|optional golangci-lint' .; then
|
||||
fail "current documentation still describes Go lint as optional"
|
||||
fi
|
||||
if grep -Fq "ci: fmt" Makefile; then
|
||||
fail "Makefile ci target must not run the mutating fmt target"
|
||||
fi
|
||||
require_contains ".gitea/workflows/bat.yml" "make check-docs"
|
||||
require_contains ".gitea/workflows/bat.yml" "make test-go-api"
|
||||
require_contains ".gitea/workflows/bat.yml" "go vet ./internal/api/... ./internal/backendrpc/... ./cmd/bat-api/..."
|
||||
require_contains ".gitea/workflows/bat.yml" "go build -o /tmp/bat-api ./cmd/bat-api"
|
||||
require_contains ".gitea/workflows/bat.yml" "Check Go formatting"
|
||||
require_contains ".gitea/workflows/bat.yml" "make check-go-format"
|
||||
require_contains ".gitea/workflows/bat.yml" "Required Go lint"
|
||||
require_contains ".gitea/workflows/bat.yml" "source scripts/ci-versions.sh"
|
||||
if grep -RIEq --include='*.md' --exclude-dir=.git --exclude-dir=archive --exclude-dir=historical \
|
||||
--exclude-dir=target --exclude-dir=bin \
|
||||
-- '自托管 Gitea|Gitea[[:space:]]+(runner|CI|workflow)|self-hosted|self hosted|\.gitea/workflows' \
|
||||
README.md CURRENT_STATUS.md TODO.md docs PROJECT_PLAN.md AGENTS.md DOCS_INDEX.md 2>/dev/null; then
|
||||
fail "current documentation still describes a Gitea/self-hosted CI runner"
|
||||
fi
|
||||
require_contains "Makefile" "cargo clippy --workspace --all-targets -- -D warnings"
|
||||
|
||||
openapi_tmp="$(mktemp)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/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_actual_version() {
|
||||
|
||||
Reference in New Issue
Block a user